Java中线程等待和唤醒
本文主要是对Java中线程等待、唤醒相关的内容进行总结。

 线程的生命周期和状态
Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态:
- NEW: 初始状态,线程被创建出来但没有被调用- start()。
- RUNNABLE: 运行状态,线程被调用了- start()等待运行的状态。
- BLOCKED:阻塞状态,需要等待锁释放。
- WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
- TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
- TERMINATED:终止状态,表示该线程已经运行完毕。
在操作系统层面,线程有 READY 和 RUNNING 状态;而在 JVM 层面,只能看到 RUNNABLE 状态(图源:HowToDoInJavaopen in new window:Java Thread Life Cycle and Thread Statesopen in new window),所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。

线程进入等待状态,即线程因为某种原因放弃了CPU使用权,阻塞也分为几种情况:
- 等待阻塞:运行的线程执行wait方法,JVM会把当前线程放入到等待队列
- 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被其他线程锁占用了,那么JVM会把当前的线程放入到锁池中
- 其他阻塞:运行的线程执行Thread.sleep或者join方法,或者发出了I/O请求时,JVM会把当前线程设置为阻塞状态,当sleep结束join线程终止、I/O处理完毕则线程恢复’
 让线程等待和唤醒的使用方法
 方式1: wait/notify
使用 Object 中的 wait() 方法让线程等待,使用 Object 中的 notify() 方法唤醒线程。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 
 | public class WaitNotifyTest {public static void main(String[] args) {
 Object lock = new Object();
 
 new Thread(() -> {
 System.out.println("线程A等待获取lock锁");
 synchronized (lock) {
 try {
 System.out.println("线程A获取了lock锁");
 Thread.sleep(1000);
 System.out.println("线程A将要运行lock.wait()方法进行等待");
 lock.wait();
 System.out.println("线程A等待结束");
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 }
 }
 ).start();
 
 new Thread(() -> {
 System.out.println("线程B等待获取lock锁");
 synchronized (lock) {
 System.out.println("线程B获取了lock锁");
 try {
 Thread.sleep(1000);
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 System.out.println("线程B将要运行lock.notify()方法进行通知");
 lock.notify();
 System.out.println("线程B结束!");
 }
 }
 ).start();
 }
 }
 
 | 
上面这段代码的输出为:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 
 | 线程A等待获取lock锁线程A获取了lock锁
 线程B等待获取lock锁
 线程A将要运行lock.wait()方法进行等待
 线程B获取了lock锁
 线程B将要运行lock.notify()方法进行通知
 线程B结束!
 线程A等待结束
 
 进程已结束,退出代码0
 
 | 
注意:此种方式必须使用同一把锁并且必须包含在synchronized代码块中,如果未使用synchronized包裹,则会报错。
下面了解一下Object对象的wait和notify方法,源码分析内容可见参考资料3。
| 方法名称 | 描述 | 
|---|
| notify() | 通知一个在对象上等待的线程,使其从wait()返回,而返回的前提是该线程获取到了对象的锁(举例为上述代码中最后两条输出相关的部分) | 
| notifyAll() | 通知所有等待在该对象上的线程。 | 
| wait() | 调用该方法的线程进入WAITING状态,只有等待另外线程的通知或被中断才会返回,需要注意, 调用wait()方法后,会释放对象的锁。 | 
| wait(long) | 超时等待一段时间,这里的参数是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回。 | 
| wait(long, int) | 对于超时时间更细粒度的控制,可以达到毫秒。 | 
 方式2: Condition
使用 JUC 包中 Condition 的 await() 方法让线程等待,使用 signal() 方法唤醒线程。
Condition的作用是对锁进行更精确的控制。Condition中的await()方法相当于Object的wait()方法,Condition中的signal()方法相当于Object的notify()方法,Condition中的signalAll()相当于Object的notifyAll()方法。不同的是,Object中的wait(),notify(),notifyAll()方法是和"同步锁"synchronized关键字捆绑使用的;而Condition是需要与"互斥锁"/"共享锁"捆绑使用的。
举三个例子:
示例1是通过Object的wait(), notify()来演示线程的休眠/唤醒功能。
示例2是通过Condition的await(), signal()来演示线程的休眠/唤醒功能。
示例3是通过Condition的高级功能。
示例1
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 
 | public class WaitTest1 {public static void main(String[] args) {
 ThreadA ta = new ThreadA("ta");
 synchronized(ta) {
 try {
 System.out.println(Thread.currentThread().getName()+" start ta");
 ta.start();
 System.out.println(Thread.currentThread().getName()+" block");
 ta.wait();
 System.out.println(Thread.currentThread().getName()+" continue");
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 }
 }
 
 static class ThreadA extends Thread{
 public ThreadA(String name) {
 super(name);
 }
 public void run() {
 synchronized (this) {
 System.out.println(Thread.currentThread().getName()+" wakup others");
 notify();
 }
 }
 }
 }
 
 | 
示例2
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 
 | import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.Condition;
 import java.util.concurrent.locks.ReentrantLock;
 public class ConditionTest1 {
 private static Lock lock = new ReentrantLock();
 private static Condition condition = lock.newCondition();
 public static void main(String[] args) {
 ThreadA ta = new ThreadA("ta");
 lock.lock();
 try {
 System.out.println(Thread.currentThread().getName()+" start ta");
 ta.start();
 System.out.println(Thread.currentThread().getName()+" block");
 condition.await();
 System.out.println(Thread.currentThread().getName()+" continue");
 } catch (InterruptedException e) {
 e.printStackTrace();
 } finally {
 lock.unlock();
 }
 }
 
 static class ThreadA extends Thread{
 public ThreadA(String name) {
 super(name);
 }
 public void run() {
 lock.lock();
 try {
 System.out.println(Thread.currentThread().getName()+" wakup others");
 condition.signal();
 } finally {
 lock.unlock();
 }
 }
 }
 }
 
 | 
运行结果:
| 12
 3
 4
 
 | main start tamain block
 ta wakup others
 main continue
 
 | 
通过“示例1”和“示例2”,我们知道Condition和Object的方法有一下对应关系:
| 12
 3
 4
 
 |               Object      Condition  休眠          wait        await
 唤醒个线程     notify      signal
 唤醒所有线程   notifyAll   signalAll
 
 | 
Condition除了支持上面的功能之外,它更强大的地方在于:能够更加精细的控制多线程的休眠与唤醒。对于同一个锁,我们可以创建多个Condition,在不同的情况下使用不同的Condition。
例如,假如多线程读/写同一个缓冲区:当向缓冲区中写入数据之后,唤醒"读线程";当从缓冲区读出数据之后,唤醒"写线程";并且当缓冲区满的时候,"写线程"需要等待;当缓冲区为空时,“读线程"需要等待。 如果采用Object类中的wait(), notify(), notifyAll()实现该缓冲区,当向缓冲区写入数据之后需要唤醒"读线程"时,不可能通过notify()或notifyAll()明确的指定唤醒"读线程”,而只能通过notifyAll唤醒所有线程(但是notifyAll无法区分唤醒的线程是读线程,还是写线程)。 但是,通过Condition,就能明确的指定唤醒读线程。
看看下面的示例3,可能对这个概念有更深刻的理解。
示例3
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 
 | import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.Condition;
 import java.util.concurrent.locks.ReentrantLock;
 
 class BoundedBuffer {
 final Lock lock = new ReentrantLock();
 final Condition notFull  = lock.newCondition();
 final Condition notEmpty = lock.newCondition();
 
 final Object[] items = new Object[5];
 int putptr, takeptr, count;
 
 public void put(Object x) throws InterruptedException {
 lock.lock();
 try {
 
 while (count == items.length)
 notFull.await();
 
 items[putptr] = x;
 
 if (++putptr == items.length) putptr = 0;
 
 ++count;
 
 notEmpty.signal();
 
 
 System.out.println(Thread.currentThread().getName() + " put  "+ (Integer)x);
 } finally {
 lock.unlock();
 }
 }
 
 public Object take() throws InterruptedException {
 lock.lock();
 try {
 
 while (count == 0)
 notEmpty.await();
 
 Object x = items[takeptr];
 
 if (++takeptr == items.length) takeptr = 0;
 
 --count;
 
 notFull.signal();
 
 
 System.out.println(Thread.currentThread().getName() + " take "+ (Integer)x);
 return x;
 } finally {
 lock.unlock();
 }
 }
 }
 
 public class ConditionTest2 {
 private static BoundedBuffer bb = new BoundedBuffer();
 
 public static void main(String[] args) {
 
 
 for (int i=0; i<10; i++) {
 new PutThread("p"+i, i).start();
 new TakeThread("t"+i).start();
 }
 }
 
 static class PutThread extends Thread {
 private int num;
 public PutThread(String name, int num) {
 super(name);
 this.num = num;
 }
 public void run() {
 try {
 Thread.sleep(1);
 bb.put(num);
 } catch (InterruptedException e) {
 }
 }
 }
 
 static class TakeThread extends Thread {
 public TakeThread(String name) {
 super(name);
 }
 public void run() {
 try {
 Thread.sleep(10);
 Integer num = (Integer)bb.take();
 } catch (InterruptedException e) {
 }
 }
 }
 }
 
 | 
总结一下方式1方式2的区别:
- 方式1 可以使用任意对象作为锁,方式2 需创建一个Lock对象
- Object#wait -> Condition#await 两者的返回条件不同,wait方法需要锁对象调用notif方法,而await需要condition对象调用signal,而且必须是调用await 的同一个condition。
 方式3: LockSupport
LockSupport 类可以阻塞当前线程以及唤醒指定被阻塞的线程。LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
LockSupport中的park() 和 unpark() 的作用分别是阻塞线程和解除阻塞线程,而且park()和unpark()不会遇到“Thread.suspend 和 Thread.resume所可能引发的死锁”问题。
因为park() 和 unpark()有许可的存在;调用 park() 的线程和另一个试图将其 unpark() 的线程之间的竞争将保持活性。
LockSupport 类使用了一种名为 Permit ( 许可) 的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),可以把许可堪称是一种 (0,1)信号量(Semaphore), 但与 Semaphore 不同的是,许可的累加上限是 1。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 
 | Thread a = new Thread(() -> {System.out.println(Thread.currentThread().getName() + " \t ======= 进入锁");
 LockSupport.park();
 System.out.println(Thread.currentThread().getName() + "\t ======== 被唤醒");
 }, "A");
 a.start();
 
 TimeUnit.SECONDS.sleep(3);
 
 Thread b = new Thread(() -> {
 LockSupport.unpark(a);
 System.out.println(Thread.currentThread().getName() + "\t ======== 通知了");
 }, "A");
 b.start();
 
 
 | 
试验结论:
1、支持无锁的情况调用,执行线程的阻塞;
2、支持先 unpark , 然后 park 操作依然有效。
LockSupport 是用来创建锁和其他同步类的基本线程阻塞原语。
LockSupport 是一个线程阻塞工具类, 所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。
归根结底, LockSupport 调用 Unsafe 的 native 代码
LockSupport 提供 park() 和 unpark() 方法实现阻塞吓成和解除线程阻塞的过程。
LockSupport 和每个使用它的线程都有一个许可(permit)关联。permit 相当于 1, 0 的开关,默认是0,
调用一次 unpark 就加 1 变成 1。
调用一次 park 会消费 permit , 也就是将 1 变成 0, 同时 park 立即返回。
如果再次调用 park 就会变成阻塞(因为 permit 为 0 了会阻塞在这里,直到 permit 变为 1),这时候调用 unpark 会把 permit 设置为 1。每个线程都有一个相关的 permit, permit 最多只有一个, 重复调用 unpark 也不会累积凭证。
形象的理解
线程阻塞需要消耗凭证(permit), 这个凭证最多只有 1个
当调用 park 方法时
- 如果有凭证,则会直接消耗掉这个凭证然后正常退出。
- 如果无凭证,就必须阻塞等待凭证可用。
而 unpark 则相反,它会增加一个凭证,但凭证最多只能有 1 个,累加无效。
 参考资料
- 线程的几种状态你真的了解么 (qq.com)
- Java并发常见面试题总结(上) | JavaGuide(Java面试 + 学习指南)
- Java并发编程之Object.wait()/notify()详解_java object wait_DivineH的博客-CSDN博客
- Java多线程系列–“基础篇”05之 线程等待与唤醒 - 如果天空不死 - 博客园 (cnblogs.com)
- Java多线程系列目录(共43篇) - 如果天空不死 - 博客园 (cnblogs.com)
- LockSupport 原理解析 - 掘金 (juejin.cn)