一、概述
线程之间的协同,必然需要线程间的相互通信,主要分为如下4类:
- 文件共享
- 网络共享
- 变量共享
- JDK提供的线程协同API
本文将重点讲述JDK提供的3套线程协同API
二、被弃用的 suspend/resume 机制
2.1 概述
调用suspend方法挂起当前线程,通过resume方法恢复线程执行。
2.2 代码实例
public void suspendResumeTest() throws Exception {
// 启动线程
Thread consumerThread = new Thread(() -> {
if (baozidian == null) { // 如果没包子,则进入等待
System.out.println("1、进入等待");
Thread.currentThread().suspend();
}
System.out.println("2、买到包子,回家");
});
consumerThread.start();
// 3秒之后,生产一个包子
Thread.sleep(3000L);
baozidian = new Object();
System.out.println("3、通知消费者");
consumerThread.resume();
}
执行结果:
1、进入等待
3、通知消费者
2、买到包子,回家
这样看来,似乎很好用的样子,但是这俩方式是被标记了废弃的,why?
2.3 死锁问题
2.3.1 同步代码块中的死锁
由于同步代码块中的suspend操作会挂起当前线程,但并不会释放锁 ,这就导致了线程A在同步代码块中调用了suspend方法,线程B在尝试进入同步代码块中调用resume方法时,无法获取锁而阻塞,最终造成死锁。
public void suspendResumeDeadLockTest() throws Exception {
// 启动线程
Thread consumerThread = new Thread(() -> {
if (baozidian == null) { // 如果没包子,则进入等待
System.out.println("1、进入等待");
// 当前线程拿到锁,然后挂起
synchronized (this) {
Thread.currentThread().suspend();
}
}
System.out.println("2、买到包子,回家");
});
consumerThread.start();
// 3秒之后,生产一个包子
Thread.sleep(3000L);
baozidian = new Object();
// 争取到锁以后,再恢复consumerThread
synchronized (this) {
System.out.println("3、通知消费者");
consumerThread.resume();
}
}
执行结果(线程始终在运行,未能结束):
1、进入等待
2.3.2 suspend比resume先执行造成的死锁
正常情况下,是先调用suspend挂起线程,再调用resume方法唤醒线程。但在一些复杂业务场景下,并不能保证两者执行的先后顺序,而如果两者未能按照先suspend再resume的顺序执行,将会造成死锁。
public void suspendResumeDeadLockTest2() throws Exception {
// 启动线程
Thread consumerThread = new Thread(() -> {
if (baozidian == null) {
System.out.println("1、没包子,进入等待");
try { // 为这个线程加上一点延时来模拟耗时业务
Thread.sleep(5000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 这里的挂起执行在resume后面
Thread.currentThread().suspend();
}
System.out.println("2、买到包子,回家");
});
consumerThread.start();
// 3秒之后,生产一个包子
Thread.sleep(3000L);
baozidian = new Object();
System.out.println("3、通知消费者");
consumerThread.resume();
}
执行结果(线程始终在运行,未能自行结束):
1、没包子,进入等待
三、wait/notify机制
3.1 概述
类似的,调用wait方法可以挂起当前线程,调用notify/notifyAll方法可以唤醒随机一个/所有线程。
但是wait、notify、notifyAll只能由同一个对象锁的持有线程调用(由于其内部是基于对象的等待集,所以方法必须在同步代码块中调用),否则会抛出IllegalMonitorStateExecption。
3.2 代码实例
public void waitNotifyTest() throws Exception {
// 启动线程
new Thread(() -> {
synchronized (this) {
while (baozidian == null) { // 如果没包子,则进入等待
try {
System.out.println("1、进入等待");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println("2、买到包子,回家");
}).start();
// 3秒之后,生产一个包子
Thread.sleep(3000L);
baozidian = new Object();
synchronized (this) {
System.out.println("3、通知消费者");
this.notifyAll();
}
}
执行结果:
1、进入等待
3、通知消费者
2、买到包子,回家
3.3 死锁问题
相对于suspend/resume机制,wait/notify机制改进了挂起线程时,自动释放锁的问题,但是未能解决执行顺序的问题(notify在wait之前执行同样会造成死锁):
public void waitNotifyDeadLockTest() throws Exception {
// 启动线程
new Thread(() -> {
if (baozidian == null) { // 如果没包子,则进入等待
try {
Thread.sleep(5000L);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
synchronized (this) {
try {
System.out.println("1、进入等待");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println("2、买到包子,回家");
}).start();
// 3秒之后,生产一个包子
Thread.sleep(3000L);
baozidian = new Object();
synchronized (this) {
this.notifyAll();
System.out.println("3、通知消费者");
}
}
执行结果(线程始终在运行,未能自行结束):
3、通知消费者
1、进入等待
四、park/unpark机制
4.1 概述
park/unpark机制类似于令牌机制,调用park方法则需要等待一个许可(令牌),而unpark方法则为指定线程提供了一个许可(令牌)。park/unpark机制对执行的顺序没有要求。但是如果unpark先执行了多次(实现提供了多次许可)之后,park方法只能执行一次(即:unpark提供的许可不能累计),后面依旧需要再次执行unpark提供许可之后,才能再次执行park。
4.2 代码实例
public void parkUnparkTest() throws Exception {
// 启动线程
Thread consumerThread = new Thread(() -> {
while (baozidian == null) { // 如果没包子,则进入等待
System.out.println("1、进入等待");
LockSupport.park();
}
System.out.println("2、买到包子,回家");
});
consumerThread.start();
// 3秒之后,生产一个包子
Thread.sleep(3000L);
baozidian = new Object();
LockSupport.unpark(consumerThread);
System.out.println("3、通知消费者");
}
执行结果:
1、进入等待
3、通知消费者
2、买到包子,回家
4.3 死锁问题
同步代码块中的park操作会挂起当前线程,但并不会释放锁:
public void parkUnparkDeadLockTest() throws Exception {
// 启动线程
Thread consumerThread = new Thread(() -> {
if (baozidian == null) { // 如果没包子,则进入等待
System.out.println("1、进入等待");
// 当前线程拿到锁,然后挂起
synchronized (this) {
LockSupport.park();
}
}
System.out.println("2、买到包子,回家");
});
consumerThread.start();
// 3秒之后,生产一个包子
Thread.sleep(3000L);
baozidian = new Object();
// 争取到锁以后,再恢复consumerThread
synchronized (this) {
LockSupport.unpark(consumerThread);
}
System.out.println("3、通知消费者");
}
执行结果(线程始终在运行,未能自行结束):
1、进入等待
五、关于伪唤醒的说明
注意,上面给出的大部分例子,都是通过if (baozidian == null)来检查线程等待条件的,然后如果线程被唤醒,则会在if的代码块中继续执行,而不会再去校验if中的判断。
官方建议使用while语句来检查线程等待条件,因为处于等待状态下的线程可能会错误警报或伪唤醒(涉及操作系统底层以及CPU硬件底层,此处不做详述),如果不在循环中检查线程等待条件,程序就会在并没有满足条件的情况下继续执行,出现意想不到的错误。