> 文档中心 > 【并发编程三:Java线程的状态及转换】

【并发编程三: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异常,并清除当前线程的中断状态

  1. notify方法唤醒在此对象(调用wait方法的那个对象)上等待的单个线程,如果在此对象上有多个线程正在等待,则选择其中一个线程唤醒,选择是任意随机的,并且由实现决定;
  2. 当前线程必须拥有此对象(调用wait方法的那个对象)的监视器(锁)才能调用notify方法,否则抛出IllegalMonitorStateException异常;
  3. 被唤醒的线程将无法继续,直到当前线程放弃对该对象的锁,被唤醒的线程将与此对象上的任何其他线程进行锁竞争,拿到锁才能继续执行;
  4. 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)】】

美剧天堂种子