- 程序(program)
程序是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码(还没有运行起来),静态对象。
- 进程(process)
进程是程序的一次执行过程,也就是说程序运行起来了,加载到了内存中,并占用了cpu的资源。这是一个动态的过程:有自身的产生、存在和消亡的过程,这也是进程的生命周期。
进程是系统资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。
- 线程(thread)
进程可进一步细化为线程,是一个程序内部的执行路径。
若一个进程同一时间并行执行多个线程,那么这个进程就是支持多线程的。
线程是cpu调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小。
一个进程中的多个线程共享相同的内存单元/内存地址空间——》他们从同一堆中分配对象,可以访问相同的变量和对象。这就使得相乘间通信更简便、搞笑。但索格线程操作共享的系统资源可能就会带来安全隐患(隐患为到底哪个线程操作这个数据,可能一个线程正在操作这个数据,有一个线程也来操作了这个数据v)。
- 配合JVM内存结构了解(只做了解即可)
class文件会通过类加载器加载到内存空间。
其中内存区域中每个线程都会有虚拟机栈和程序计数器。
每个进程都会有一个方法区和堆,多个线程共享同一进程下的方法区和堆。
- 配合JVM内存结构了解(只做了解即可)
- CPU单核和多核的理解
单核的CPU是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。同时间段内有多个线程需要CPU去运行时,CPU也只能交替去执行多个线程中的一个线程,但是由于其执行速度特别快,因此感觉不出来。
多核的CPU才能更好的发挥多线程的效率。
对于Java应用程序java.exe来讲,至少会存在三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。如过发生异常时会影响主线程。
- Java线程的分类:用户线程 和 守护线程
- Java的gc()垃圾回收线程就是一个守护线程
- 守护线程是用来服务用户线程的,通过在start()方法前调用thread.setDaemon(true)可以吧一个用户线程变成一个守护线程。
- 并行和并发
- 并行:多个cpu同时执行多个任务。比如,多个人做不同的事。
- 并发:一个cpu(采用时间片)同时执行多个任务。比如,渺少、多个人做同一件事。
- 多线程的优点
- 提高应用程序的响应。堆图像化界面更有意义,可以增强用户体验。
- 提高计算机系CPU的利用率。
- 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。
- 何时需要多线程
- 程序需要同时执行两个或多个任务。
- 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
- 需要一些后台运行的程序时。
- Java语言的JVM允许程序运行多个线程,多线程可以通过Java中的java.lang.Thread类来体现。
- Thread类的特性
- 每个线程都是通过某个特定的Thread对象的run()方法来完成操作的,经常吧run()方法的主体称为线程体。
- 通过Thread方法的start()方法来启动这个线程,而非直接调用run()。
- 创建一个继承于Thread类的子类。
- 重写Thread类的run()方法。
- 创建Thread类的子类的对象。
- 通过此对象调用start()来启动一个线程。
代码实现:多线程执行同一段代码
多线程代码运行图解
多线程执行多段代码
- 创建一个实现Runnable接口的类。
- 实现类去实现Runnable接口中的抽象方法:run()。
- 创建实现类的对象。
- 将此对象作为参数传到Thread类的构造器中,创建Thread类的对象。
- 通过Thread类的对象调用start()方法。
- Java中只允许单进程,以卖票程序TiketSales类来说,很有可能这个类本来就有父类,这样一来就不可以继承Thread类来完成多线程了,但是一个类可以实现多个接口,因此实现的方式没有类的单继承性的局限性,用实现Runnable接口的方式来完成多线程更加实用。
- 实现Runnable接口的方式天然具有共享数据的特性(不用static变量)。因为继承Thread的实现方式,需要创建多个子类的对象来进行多线程,如果子类中有变量A,而不使用static约束变量的话,每个子类的对象都会有自己独立的变量A,只有static约束A后,子类的对象才共享变量A。而实现Runnable接口的方式,只需要创建一个实现类的对象,要将这个对象传入Thread类并创建多个Thread类的对象来完成多线程,而这多个Thread类对象实际上就是调用一个实现类对象而已。实现的方式更适合来处理多个线程有共享数据的情况。
- 联系:Thread类中也实现了Runnable接口
- 相同点两种方式都需要重写run()方法,线程的执行逻辑都在run()方法中
与Runnable相比,Callable功能更强大
- 相比run()方法,可以有返回值
- 方法可以抛出异常
- 支持泛型的返回值
- 需要借助FutureTask类,比如获取返回结果
背景:
经常创建和销毁、使用量特别大的资源、比如并发情况下的线程、对性能影响很大。
思路:
提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
优点:
提高响应速度(减少了创建新线程的时间)
降低资源消耗(重复利用线程池中线程,不需要每次都创建)
便于线程管理
- start() : 启动当前线程, 调用当前线程的run()方法
- run() : 通常需要重写Thread类中的此方法, 将创建的线程要执行的操作声明在此方法中
- currentThread() : 静态方法, 返回当前代码执行的线程
- getName() : 获取当前线程的名字
- setName() : 设置当前线程的名字
- yield() : 释放当前CPU的执行权
- join() : 在线程a中调用线程b的join(), 此时线程a进入阻塞状态, 知道线程b完全执行完以后, 线程a才结束阻塞状态
- stop() : 已过时. 当执行此方法时,强制结束当前线程.
- sleep(long militime) : 让线程睡眠指定的毫秒数,在指定时间内,线程是阻塞状态
- isAlive() :判断当前线程是否存活
- 时间片:cpu正常情况下的调度策略。即CPU分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片,即该进程允许运行的时间,使各个程序从表面上看是同时进行的。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。而不会造成CPU资源浪费。在宏观上:我们可以同时打开多个应用程序,每个程序并行不悖,同时运行。但在微观上:由于只有一个CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行。
- 抢占式:高优先级的线程抢占cpu。
- 同优先级线程组成先进先出队列(先到先服务),使用时间片策略。
- 堆高优先级,使用优先调度的抢占式策略。
线程的优先级等级(一共有10挡)
- MAX_PRIORITY:10
- MIN_PRIORITY:1
- NORM_PRIORITY:5 (默认优先级)
获取和设置当前线程的优先级
- 获取
- 设置
说明:高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只有高优先级的线程执行完成以后,低优先级的线程才执行。
- JDk中用Thread.State类定义了线程的几种状态
想要实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在他的一个完整的生命周期中通常要经历如下的五种状态:
- 新建:当一个Thread类或其子类的对象被声明并创建时,新的线程对象处于新建状态。
- 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源。
- 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能。
- 阻塞:在某种特殊情况下,被认为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态。
- 死亡:线程完成了它的全部工作或线程被提前强制性的中止或出现异常倒置导致结束。
- 线程的安全问题
- 多个线程执行的不确定性硬气执行结果的不稳定性
- 多个线程对账本的共享, 会造成操作的不完整性, 会破坏数据.
- 多个线程访问共享的数据时可能存在安全性问题
- 线程的安全问题Demo: 卖票过程中出现了重票和错票的情况 (以下多窗口售票demo存在多线程安全问题)
错票分析:
当票数为1的时候,三个线程中有线程被阻塞没有执行票数-1的操作,这是其它线程就会通过if语句的判断,这样一来就会造成多卖了一张票,出现错票的情况。
极端情况为,当票数为1时,三个线程同时判断通过,进入阻塞,然后多执行两侧卖票操作。
重票分析:
如果t1在输出票号22和票数-1的操作之间被阻塞,这就导致这时候t1卖出了22号票,但是总票数没有减少。在t1被阻塞期间,如果t2运行到输出票号时,那么t2也会输出和t1相同的票号22.
通过以上两种情况可以看出,线程的安全性问题时因为多个线程正在执行代码的过程中,并且尚未完成的时候,其他线程参与进来执行代码所导致的。
原理:
当一个线程在操作共享数据的时候,其他线程不能参与进来。知道这个线程操作完共享数据的时候,其他线程才可以操作。即使当这个线程操作共享数据的时候发生了阻塞,依旧无法改变这种情况。
在Java中,我们通过同步机制,来解决线程的安全问题。
说明:
- 操作共享数据(多个线程共同操作的变量)的代码,即为需要被同步的代码。 不能多包涵代码(效率低,如果包到while前面就变成了单线程了),也不能少包含代码
- 共享数据:多个线程共同操作的变量。
- 同步监视器:俗称,锁。任何一个类的对象都可以充当锁。但是所有的线程都必须共用一把锁,共用一个对象。
锁的选择:
- 自行创建,共用对象,如下面demo中的Object对象。
- 使用this表示当前类的对象
继承Thread的方法中的锁不能使用this代替,因为继承thread实现多线程时,会创建多个子类对象来代表多个线程,这个时候this指的时当前这个类的多个对象,不唯一,无法当作锁。
实现Runnable接口的方式中,this可以当作锁,因为这种方式只需要创建一个实现类的对象,将实现类的对象传递给多个Thread类对象来当作多个线程,this就是这个一个实现类的对象,是唯一的,被所有线程所共用的对象。
- 使用类当作锁,以下面demo为例,其中的锁可以写为, 从这里可以得出结论,类也是一个对象
优点:同步的方式,解决了线程安全的问题
缺点:操作同步代码时,只能有一个线程参与,其他线程等待。相当于时一个单线程的过程,效率低。
Demo
将所要同步的代码放到一个方法中,将方法声明为synchronized同步方法。之后可以在run()方法中调用同步方法。
要点:
- 同步方法仍然涉及到同步监视器,只是不需要我们显示的声明。
- 非静态的同步方法,同步监视器是:this。
- 静态的同步方法,同步监视器是:当前类本身。
Demo
使用同步解决懒汉模式的线程安全问题
JDK5.0之后,可以通过实例化ReentrantLock对象,在所需要同步的语句前,调用ReentrantLock对象的lock()方法,实现同步锁,在同步语句结束时,调用unlock()方法结束同步锁
synchronized和lock的异同:(面试题)
建议使用顺序:Lock—》同步代码块(已经进入了方法体,分配了相应的资源)—》同步方法(在方法体之外)
Demo:
原理:
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了死锁。
出现死锁后,并不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
使用同步时应避免出现死锁。
Java中死锁最简单的情况:
一个线程T1持有锁L1并且申请获得锁L2,而另一个线程T2持有锁L2并且申请获得锁L1,因为默认的锁申请操作都是阻塞的,所以线程T1和T2永远被阻塞了。导致了死锁。这是最容易理解也是最简单的死锁的形式。但是实际环境中的死锁往往比这个复杂的多。可能会有多个线程形成了一个死锁的环路,比如:线程T1持有锁L1并且申请获得锁L2,而线程T2持有锁L2并且申请获得锁L3,而线程T3持有锁L3并且申请获得锁L1,这样导致了一个锁依赖的环路:T1依赖T2的锁L2,T2依赖T3的锁L3,而T3依赖T1的锁L1。从而导致了死锁。
从这两个例子,我们可以得出结论,产生死锁可能性的最根本原因是:线程在获得一个锁L1的情况下再去申请另外一个锁L2,也就是锁L1想要包含了锁L2,也就是说在获得了锁L1,并且没有释放锁L1的情况下,又去申请获得锁L2,这个是产生死锁的最根本原因。另一个原因是默认的锁申请操作是阻塞的。
死锁的解决方法:
很多情况下,尽管我们创建了多个线程,也会出现几乎一个线程执行完所有操作的时候,这时候我们就需要让线程间相互交流。
原理:
当一个线程执行完成其所应该执行的代码后,手动让这个线程进入阻塞状态,这样一来,接下来的操作只能由其他线程来操作。当其他线程执行的开始阶段,再手动让已经阻塞的线程停止阻塞,进入就绪状态,虽说这时候阻塞的线程停止了阻塞,但是由于现在正在运行的线程拿着同步锁,所以停止阻塞的线程也无法立马执行。如此操作就可以完成线程间的通信。
所用的到方法:
wait():一旦执行此方法,当前线程就会进入阻塞,一旦执行wait()会释放同步监视器。
notify():一旦执行此方法,将会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先度最高的。
notifyAll() :一旦执行此方法,就会唤醒所有被wait的线程
说明:
这三个方法必须在同步代码块或同步方法中使用。
三个方法的调用者必须是同步代码块或同步方法中的同步监视器。
这三个方法并不时定义在Thread类中的,而是定义在Object类当中的。因为所有的对象都可以作为同步监视器,而这三个方法需要由同步监视器调用,所以任何一个类都要满足,那么只能写在Object类中。
sleep()和wait()的异同:(面试题)
- 相同点:两个方法一旦执行,都可以让线程进入阻塞状态。
- 不同点:1) 两个方法声明的位置不同:Thread类中声明sleep(),Object类中声明wait()
2) 调用要求不同:sleep()可以在任何需要的场景下调用。wait()必须在同步代码块中调用。
2) 关于是否释放同步监视器:如果两个方法都使用在同步代码块呵呵同步方法中,sleep不会释放锁,wait会释放锁。
Demo:
- 练习1:
银行有一个账户。
有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。
- 经典例题:生产者和消费着问题
生产者( Productor)将产品交给店员( Clerk),而消费者( (Customer)从店员处取走产品, 店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产; 如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
版权声明:
本文来源网络,所有图片文章版权属于原作者,如有侵权,联系删除。
本文网址:https://www.bianchenghao6.com/java-jiao-cheng/18038.html