【并发编程三:Java线程的状态及转换】
【衔接上一章 【并发编程二:Therad-api和main线程和子线程的关系】】
1.1Object类的wait/notify/notifyAll方法
1、wait方法的作用
wait方法的作用是使当前线程等待,当前线程必须拥有此对象(调用wait方法的那个对象)的监视器(锁)才能调用wait方法,否则抛出IllegalMonitorStateException异常,当调用wait方法后线程会释放此监视器的所有权(锁)并等待;
2、wait方法调用后,
wait方法调用后,直到另一个线程为此对象(调用wait方法的那个对象)调用notify()方法或notifyAll()方法,通知和唤醒在此对象上等待的线程,然后线程等待直到它可以重新获得监视器的所有权(锁)并恢复执行;
3、wait()方法的行为和执行调用wait(0)一样,(0表示如果不通知,则无限等待)
4、有可能被线程中断和虚假唤醒,应始终在循环中使用while进行判断:
Wait方法的模板代码:synchronized (obj) { while () {obj.wait(); ... // Perform action appropriate to condition }}
5、如果任何线程在当前线程等待通知之前或期间中断了当前线程,抛InterruptedException异常,并清除当前线程的中断状态;
- notify方法唤醒在此对象(调用wait方法的那个对象)上等待的单个线程,如果在此对象上有多个线程正在等待,则选择其中一个线程唤醒,选择是任意随机的,并且由实现决定;
- 当前线程必须拥有此对象(调用wait方法的那个对象)的监视器(锁)才能调用notify方法,否则抛出IllegalMonitorStateException异常;
- 被唤醒的线程将无法继续,直到当前线程放弃对该对象的锁,被唤醒的线程将与此对象上的任何其他线程进行锁竞争,拿到锁才能继续执行;
- notify方法只有拿到此对象(调用wait方法的那个对象)监视器的锁之后才能调用,线程通过以下三种方式之一拿到对象监视器的锁:
(1)通过执行该对象的同步实例方法;
(2)通过执行同步对象的同步语句块;
(3)对于Class类型的对象,通过执行该类的同步静态方法;
1.2wait、notify、notifyAll场景应用
经典的生产者-消费者模式;
生产者生产数据到缓冲区中,消费者从缓冲区中取数据;
如果缓冲区已经满了,则生产者线程阻塞等待;
如果缓冲区为空,那么消费者线程阻塞等待;
Java实现生产者消费者模式的几种常用方法:
1、wait() / notifyAll()方法
2、await() / signal()方法;
3、信号量Semaphore方法;
4、阻塞队列BlockingQueue方法;
5、管道PipedWriter/PipedReader方法;
6、无锁队列Disruptor框架方法;
7、分布式下的消息队列(分布式下的消息队列也是生产者-消费者模式);
wait() / notify()方法
- 1、当生产者向缓冲区放入一个数据时,向其他等待的消费线程发出可消费的通知,当缓冲区满了,生产者停止生产并等待
- 2、当消费者从缓冲区取出一个数据时,向其他等待的生产线程发出可生产的通知,当缓冲区空了,消费者停止消费并等待;
1.3LockSupport工具类
LockSupport是java.util.concurrent.locks包下的一个类,是用来创建锁和其他同步类的基本线程阻塞工具类,它里面的方法都是静态方法;
我们前面介绍了等待/唤醒机制wait/notify,那么这个LockSupport可以说是它的改良版;
LockSupport主要就是用park(等待)和unpark(唤醒)方法来实现等待唤醒;
park方法是将当前Thread阻塞,而unpark方法将指定线程Thread唤醒;
纳秒是多久:1秒等于1000毫秒,等于100万微秒,等于10亿纳秒;
与Object类的wait/notify机制相比,park/unpark的特点:
- 1、LockSupport不需要在同步块中使用;
- 2、LockSupport以thread为操作对象更符合阻塞线程的直观定义;
- 3、LockSupport操作更精准,可以精确地唤醒某一个线程(notify随机唤醒一个线程,notifyAll唤醒所有等待的线程);
- 4、LockSupport先unpark再park也不会报错,先unpark相当于(先吃药不晕车,先吃解药不中毒),而notify先唤醒再等待的话,都会导致线程无法被唤醒;
- 5、LockSupport中断park不会抛出InterruptedException异常,需要在park之后自行判断中断状态做额外的处理
LockSupport底层实现?
LockSupport底层是通过java的Unsafe类的park和unpark方法直接调用底层操作系统来完成对线程的阻塞;
LockSupport的原理就是使用了一种permit(许可证)的概念来实现等待唤醒功能,每个线程都有一个许可证,许可证只有两个值,一个是0,一个是1;
默认许可证的值是0,表示没有许可证,就会被阻塞;
调用unpark方法就把permit的值改为1,相当于发放一个许可证;
调用park方法就把permit的值改为0,相当于收回许可证;
每调用一次unpark方法,permit就会变成1,每调一次park方法,就会消耗掉一个许可证,permit就变成0;
每个线程都有一个permit,permit最多也就一个,多次调用unpark也不会累加;
根据是否有permit来判断是否要阻塞线程,所以先unpark再park也可以,跟顺序无关,只看是否有permit;(0阻塞,1不阻塞)
如果先unpark了两次(值1),再park两次(第一次1可以执行,第二次0,阻塞),那么线程还是会被阻塞,因为permit不会累加,unpark两次,permit的值还是1,第一次park的时变成0了,所以第二次park就会阻塞线程;
park线程不能用notify来唤醒,wait线程也不能用unpark来唤醒;
1.4Java线程的状态及转换
Java线程定义了6种状态,在任何时刻,有且只能处于其中某一种状态;
1、新建(New):
线程创建后但还没有启动就处于这种状态;
2、运行(Runnable):
运行状态包括操作系统线程状态中的Ready和Running,也就是处于该状态的线程有可能正在执行,也有可能正在等待着操作系统为它分配执行时间;
(注:操作系统线程5种状态:初始状态、可运行状态、运行状态、阻塞状态、终止状态)
3、无限期等待(Waiting):
处于这种状态的线程不会被分配处理器执行时间,它们要等待被其他线程唤醒;
4、超时等待(Timed Waiting):
处于这种状态的线程也不会被分配处理器执行时间,不过它不需要等待其他线程唤醒,在一定时间之后它们会由系统自动唤醒;
5、阻塞(Blocked):
表示线程被阻塞了,在程序进入同步代码区域的时候,线程将进入这种阻塞状态;
“阻塞状态”与“等待状态”的区别在于:
- “阻塞状态”在等待着获取到一个排它锁,当获取到锁的时候就不会阻塞;
- “等待状态”则是在等待(可能是有限时间等待或者是永久等待),直到等待时间超时或者有另一个线程来唤醒,才不会等待;
6、终止(Terminated):
已终止的线程状态,表示线程已经结束执行;
上述6种状态在遇到特定事件发生的时候将会互相转换;
阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态,没有获取到锁进入blocked状态;
但是阻塞在java.util.concurrent包中Lock接口的线程状态却是等待状态,因为java.util.concurrent包中Lock接口对于阻塞的实现均使用了LockSupport类中的相关方法;
1.5jstack查看线程状态
jstack -h 获取帮助;
jstack -F 强制输出线程栈信息;
jstack -m 混合模式,java线程和本地方法线程栈都输出;
jstack -l 输出更多的信息;
【衔接下一章 【并发编程四:Java中的线程池(1)】】