1. wait / notify
1.1 已掌握的实现线程间通信的方法
-
之前有学习Java的管道流:Java管道输入/输出流的简单学习
-
通过管道流,可以实现线程间通信
-
除此之外,还可以使用volatile共享变量实现线程间通信
public class VolatileCommunicate { private volatile boolean ready; public VolatileCommunicate() { ready = false; } public static void sleep(TimeUnit timeUnit, long timeout) { try { timeUnit.sleep(timeout); } catch (InterruptedException e) { e.printStackTrace(); } } public void eat() { while (!ready) { // 食物没有准备好,循环等待 sleep(TimeUnit.SECONDS, 1); } System.out.println(Thread.currentThread().getName() + ": 我要开吃了"); } public void cook() { // cook food ... ready = true; System.out.println(Thread.currentThread().getName() + ": 食物准备好了"); } public static void main(String[] args) { VolatileCommunicate communicate = new VolatileCommunicate(); Thread consumer = new Thread(communicate::eat, "顾客老张"); Thread cook = new Thread(communicate::cook, "厨师老李"); consumer.start(); // 一段时间后,厨师做好了食物 sleep(TimeUnit.SECONDS, 5); cook.start(); } }
1.2 等待/通知机制
-
一个线程改变了程序状态,另一个线程感知到符合预期的改变,执行相应操作
-
前者是生产者,后者是消费者,这种设计模式具有很好的伸缩性
-
对于消费者,想要感知到状态改变,需要不断地循环检查状态是否符合预期;若符合预期,则退出循环,执行特定操作
-
伪代码如下:
while (status!=expectedStatus) { sleep someTime; } do something
-
这种检查、睡眠等待的时间方式存在不足
- 难以保证及时性:如果睡眠时间太长,则不能及时发现状态的改变,不能保证后续操作的时效性
- 难以降低开销:如果睡眠时间过短,虽然保证了及时性,却有可能浪费处理器资源
-
很难找到一个合适的睡眠时间,这时候可以考虑使用Java对象内置的等待 / 通知机制
- 线程因为条件不满足,进行等待
- 另一个线程修改条件,通知等待的线程
1.3 wait / notify / notifyAll
1.3.1 基本方法
-
Java的Object类定义了可以实现等待 / 通知机制的方法:
方法名 描述 wait() 调用wait()方法的线程将进入等待状态,直到其他线程通知或被中断 wait(long timeout) 超时等待一段时间,可能因为超时退出,也可能因为其他线程的通知或被中断而退出
native方法,wait(0)表示一直等待,也是wait()方法的实际实现wait(long timeout, int nanos) 纳秒粒度的超时控制 notify() native方法,通知一个处于wait状态的线程,使其从wait方法返回;
如果有多个线程处于wait状态,则随机选择一个进行通知(唤醒)notifyAll() native方法,通知所有所有wait状态的线程 -
执行以上方法前,都必须获取对象的synchronized锁;等待和通知的线程必须获取的是同一对象锁,才能基于synchronized构建等待 / 通知机制
-
也就是说,Object的等待 / 通知方法 + 基于Object的synchronized = 等待 / 通知机制
1.3.2 实现原理
-
基于wait() 和notify()方法,展示等待 / 通知机制的实现
时间 等待线程 通知线程 t1 获取对象obj的锁,进入同步方法或同步代码块 t2 发现条件不满足,调用wait()方法:
1. 进入等待队列;2. 释放对象锁t3 获取对象obj的锁,进入同步方法或同步代码块 t4 修改条件,调用notify()方法通知等待的线程:
将等待线程从等待队列移入同步队列,使得等待线程拥有竞争对象锁的机会t5 执行完同步方法或同步代码块,释放锁 t6 其他线程释放锁,尝试获取锁成功,从wait()方法返回 t7 发现条件满足,执行某些操作;完成同步方法或同步代码块的执行,释放锁 -
可以使用下图简单描述:
1.3.3 等待 / 通知机制的经典范式
等待线程
- 获取到对象锁
- 条件不满足,调用对象的wait方法进行等待
- 当等待线程再次获取到对象锁从wait方法返回时,由于多线程的不确定性,可能条件仍然不满足,还需要继续wait
- 因此,等待线程需要通过while循环进行条件检查、wait
- 条件满足执行特定操作
- 伪代码如下:
通知线程
- 获取到对象锁
- 改变条件,调用对象的notify() 或notifyAll() 方法通知等待线程
- 执行完同步代码,释放锁
等待 / 通知模式的代码示例
-
实现一个简单的查询引擎:
- session收到client传来的SQL后,启动一个SQL结果获取线程,一个SQL执行线程
- 结果获取线程等待SQL执行结束,执行线程执行完SQL后,通知等待的结果获取线程
-
代码如下:
public class QuerySession { private Object engine = new Object(); private boolean finished = false; public static void main(String[] args) { QuerySession test = new QuerySession(); test.startQuery(); } public void startQuery() { System.out.println(Thread.currentThread().getName() + ": 收到SQL"); // 一个线程等待执行结果,一个执行SQL new Thread(this:: getQueryResult, "result-thread").start(); new Thread(this::execute, "query-thread").start(); } public void getQueryResult() { synchronized (engine) { while (!finished) { try { engine.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // SQL执行结束 System.out.println(Thread.currentThread().getName() + ":获取到SQL执行结果"); } } public void execute() { synchronized (engine) { // 执行SQL查询 sleep(TimeUnit.MILLISECONDS, 100); // 查询结束,唤醒等待线程 finished = true; engine.notify(); System.out.println(Thread.currentThread().getName() + ":SQL执行结束"); } } public static void sleep(TimeUnit timeUnit, long timeout) { try { timeUnit.sleep(timeout); } catch (InterruptedException e) { e.printStackTrace(); } } }
-
执行结果
1.3.4 注意对象的一致性
-
等待 / 通知方法的调用者,必须为synchronized锁住的对象,否则将抛出
IllegalMonitorStateException
异常 -
上述示例程序的getQueryResult()方法改造如下:
public void getQueryResult() { synchronized (engine) { while (!finished) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // SQL执行结束 System.out.println(Thread.currentThread().getName() + ":获取到SQL执行结果"); } }
-
执行时,将抛出
IllegalMonitorStateException
异常
2. join方法
- 个人觉得,使用wait / notify实现等待 / 通知模式挺费事儿的:对象、synchronized、要在调用同一对象的等待 / 通知方法等
- 如果我的需求很简单,例如:父线程创建一个子线程执行某些操作,等子线程执行完以后,父线程再继续执行
- 如果使用传统的sleep方法,无法确定父线程需要睡眠多久;如果使用等待 / 通知模式, 子线程和父线程的操作其实无需保证线程安全,这样反而比较麻烦
- 这时候,可以使用join()方法,让父线程等待子线程执行完毕后,再继续执行
2.1 三种join方法
-
和wait方法一样,join方法提供有超时和无超时的两种类型
方法名 描述 join() Thread类的非静态方法,需要通过线程实例进行调用: thread.join()
:表示调用者线程需要等待被调线程执行结束(死亡),才能继续执行 join() 方法之后的代码join(long millis) 与 join() 方法不同的是,调用者线程等待一段时间(单位:ms)即可继续执行;可能因为被调线程执行结束,而提前结束等待 join(0)
表示永久等待(没有超时限制),这也是join() 方法的底层实现join(long millis, int nanos) 等待 millis毫秒 + nanos纳秒
,从源码上看,实际按照毫秒进行等待的,会将纳秒四舍五入成毫秒
2.2 源码解读
-
join(long millis)方法的代码实现代码如下:
public final synchronized void join(long millis) throws InterruptedException { long base = System.currentTimeMillis(); long now = 0; if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (millis == 0) { // 永久等待 while (isAlive()) { wait(0); } } else { while (isAlive()) { // 超时等待的实现:只要当前线程并未执行结束,就需要不停等待 long delay = millis - now; if (delay <= 0) { break; } wait(delay); // 在等待中可能被通知,需要根据当前线程的状态和delay决定是否继续等待 now = System.currentTimeMillis() - base; } } }
-
从源代码可知:
- 线程本身就是一个Java对象,其join 方法实际通过自身的
wait()
方法实现 - 是否等待的前提是,当前线程是否终止:通过
isAlive()
进行判断 - 如果等待时间为0,表示永久等待;
- 等待时间大于0,进行超时等待:等待中可能被通知使其从wait 方法返回,如果线程未终止,需要继续等待
delay
时间
- 线程本身就是一个Java对象,其join 方法实际通过自身的
是谁进入等待状态?
- 在调用线程中执行被调线程的join方法,调用者线程将会进入等待状态
- 基于被调线程的wait方法,实现调用者线程的等待
- 被调线程终止,调用自身的notifyAll 方法,通知调用者线程
- 调用者线程成功获取到被调线程的对象锁,从被调线程的wait方法返回
- 最终,调用者线程将从被调线程的join方法返回,从而可以继续执行jion方法之后的代码
进入等待状态的调用者线程,何时被通知?
- 线程终止时,
this.notifyAll()
方法会被调用,使其从 wait 方法返回 - 也有可能应用程序调用了线程的 notify/ notifyAll 方法,使得线程提前从 wait 方法返回:这时线程并未终止、等待也未超时,线程会继续等待
- 这也是 join 方法的超时等待部分,为什么这样实现的原因
- 注意: JDK源码的编写者,不建议在应用程序中主动调用线程的wait、notify或notifyAll 方法
提前结束超时等待的代码示例
-
下面的代码,主线程需要等待子线程1000ms,但是子线程实际只需执行5ms左右
-
子线程会因为自身终止而提前结束超时等待,主线程实际无需等待1000ms即可继续执行
public static void main(String[] args) throws InterruptedException { SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); Thread thread1 = new Thread(() -> { System.out.println(format.format(new Date()) + "子线程开始执行"); for (int i = 0; i < 5; i++) { System.out.println("A ---- " + i); sleep(TimeUnit.MILLISECONDS, 1); } System.out.println(format.format(new Date()) + "子线程执行结束"); }); thread1.start(); // thread1.join(); thread1.join(1000); System.out.println(format.format(new Date()) + "等待结束,子线程状态:" + thread1.getState()); }
-
执行结果与预期一致:
总结
- 三种 join 方法都是
final
方法,不能被子类重写 - 其他两种种 join 方法,其实质都是调用
join(long millis)
方法 - join 方法和wait或sleep方法一样,也是不可中断;执行过程中被中断,会抛出
InterruptedException
异常 - join 方法,必须在start 方法之后调用
2.3 join、wait、sleep方法的区别
- 三种方法都是不可中断方法,一旦中断将抛出InterruptedException
| 方法 | 位置 | 特点|
|–|--|–|
| sleep() | Thread类的静态方法 | ① 当前线程会让出CPU资源,但不会释放锁资源。② 在哪个线程中调用Thread.sleep()
方法,哪个线程就会睡眠|
| wait() | Java Object类的方法 | ① 是所有Java对象具有的方法,必须与对象和对象锁一起使用。② 当前线程会让出CPU资源,并释放对象的锁;③ 在哪个线程调用对象的wait方法,哪个线程会等待 |
| join() | Thread类的非静态方法,即线程对象的方法 | ① 基于线程对象的 wait 方法实现,执行过程中会释放锁资源;② 在哪个线程中调用另一个线程的join方法,哪个线程会等待 |
3. 总结
Java Object的等待 / 通知机制
- Object的等待/ 通知方法 + Object的synchronized锁 = Object的等待 / 通知机制
- 等待 / 通知机制的整个流程:
- 注意事项:
- 必须是同一个对象的锁、等待 / 通知方法,否则将抛出
IllegalMonitorStateException
异常 - 经典的等待 / 通知模式,在从wait 方发返回时,必须再次检查条件是否满足(多线程的影响、可能条件的更新不符合预期)
- wait()方法的实质是native的wait(0)方法,表示永久等待
- 必须是同一个对象的锁、等待 / 通知方法,否则将抛出
join方法
- 调用者线程等待被调线程执行完毕,才继续执行
- join()方法的实质是 synchronized 的join(0)方法,表示永久等待直到被调线程终止
join(long millis)
方法的源码解析:等待的前提,线程未终止;超时等待的实现(isAlive()
和delay的更新);何时被通知- join方法的实质:被调线程自身的等待 、 通知机制
sleep、wait和join方法的比较
- 重点在方法的位置、是否释放锁、谁进入等待状态等
参考文档: