JDK提供的线程协调API
suspend/resume(不推荐)、wait/notify、park/unpark
文章目录
- 系列文章目录
- 前言
- 一、suspend/resume
- 二、wait/notify
- 三、park/unpark
- 总结
前言
JDK中对于需要多线程协作完成某一任务的场景,提供了对应的API支持。多线程写作的典型场景是:生产者-消费者模型(线程阻塞、线程唤醒)。
示例:线程1去蛋挞店买蛋挞,没有蛋挞,则不再执行。线程2生产蛋挞,通知线程1继续执行。
一、suspend/resume(不推荐,被JDK弃用)
调用suspend挂起目标线程,通过resume可以恢复线程执行。
1、suspendResume()方法是正常执行,线程1(消费者)能买到蛋挞吃下午茶
2、 suspendResumeSynchronizedDead()属于死锁,线程1不能买到蛋挞
3、suspendResumeOrderDead(): resume 先执行,导致线程1买不到蛋挞,因为suspend比resume后执行
二、wait/notify
wait/notify这些方法只能由同一对象锁的持有者线程调用(写在同步代码块里),否则会抛异常。
wait()导致当前线程等待,加入该对象的等待集合中,并且释放当前持有的对象锁。notify、notifyAll方法唤醒一个/多个正在等待这个对象锁的线程。
注意:虽然notify会释放锁,但是有顺序要求,如果notify被调用之后才调用wait(),线程会永远处于WAITING状态。
示例:
1、在同步代码块中使用wait(),线程挂起,然后生产线程获取到锁通知消费者,然后在执行剩下消费者线程的代码。
2、线程死锁状态,wait()在notify()后面执行就会存在死锁。消费者线程先睡眠5s,此时生产者已经获取到锁,并已经通知消费者,最后才到消费者线程挂起释放锁,线程处于等待状态,一直等待。。
三、park/unpark
(java.util.concurrent.locks下的LockSupport的方法)
线程调用park是等待“许可”,unpark方法为指定线程提供“许可”。park、unpark方法的调用顺序不做要求,顺序随意。
注意:多次调用unpark后在调用park线程会直接运行。但是不会叠加,就是说,连续多次调用park方法,第一次的park会拿到“许可”直接运行,后续的调用会进入等待。
示例:
1、park后线程等待,等待unpark为线程提供许可,然后消费者开始继续执行。(此处,park和unpark的先后顺序对执行效果没有影响,最后都能买到蛋挞)
2、park、unpark死锁 ,在同步代码块中存在死锁的问题(park不会释放锁,所以会导致死锁)。
消费者线程 中的同步代码块中,park后,并未释放this锁对象,然后生产者获取不到锁,最后死锁。
伪唤醒问题:警告,之前的代码中使用if来判断,是否进入等待状态是错误的!官方建议在循环中检查等待条件,因为处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就可能在没有满足结束条件的情况下退出。伪唤醒是指线程并非notify、notifyall、unpark等api调用唤醒而是更底层原因导致的。(具体原因我也懂)
wait()方法中有提示,防止伪唤醒的方式。
完整代码示例:
/**
* 线程协作通信的三种方式:suspend/resume(不推荐)、wait/notify、park/unpark
*/
import java.util.concurrent.locks.LockSupport;
/**
* 场景:线程1去蛋挞店买蛋挞,没有蛋挞,则不再执行。线程2生产蛋挞,通知线程1继续执行。
*/
public class ThreadCommunicateDemo {
/** 蛋挞店 */
public static Object eggTart = null;
public void suspendResume() throws InterruptedException {
Thread consumerThread = new Thread(() -> {
while(eggTart == null) {
System.out.println("1、等待购买蛋挞。。。。");
Thread.currentThread().suspend();
}
System.out.println("2、买到蛋挞啦!下午茶开吃");
});
consumerThread.start();
//5s后生产蛋挞
Thread.sleep(5000L);
eggTart = new Object();
consumerThread.resume();
System.out.println("3、通知consumer");
}
/**
* 在同步代码块中使用suspend/resume,导致死锁例子(suspend并不会像wait一样释放锁)
*/
public void suspendResumeSynchronizedDead() throws InterruptedException {
Thread consumerThread = new Thread(() -> {
while(eggTart == null) {
System.out.println("1、等待购买蛋挞。。。。");
synchronized (this){
Thread.currentThread().suspend();
}
}
System.out.println("2、买到蛋挞啦!下午茶开吃");
});
consumerThread.start();
//5s后生产蛋挞
Thread.sleep(5000L);
eggTart = new Object();
//争取到锁以后,再恢复consumerThread
synchronized (this){
consumerThread.resume();
}
System.out.println("3、通知consumer");
}
/**
* resume 比 suspend先执行,导致程序永久挂起
*/
public void suspendResumeOrderDead() throws InterruptedException {
Thread consumerThread = new Thread(() -> {
while(eggTart == null) {
System.out.println("1、等待购买蛋挞。。。。");
try {
Thread.sleep(5000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread.currentThread().suspend();
}
System.out.println("2、买到蛋挞啦!下午茶开吃");
});
consumerThread.start();
//5s后生产蛋挞
Thread.sleep(2000L);
eggTart = new Object();
consumerThread.resume();
System.out.println("3、通知consumer");
}
/**
* wait/notify 正常方法
*/
public void waitNotify() throws InterruptedException {
new Thread(() -> {
while(eggTart == null) {
synchronized (this){
System.out.println("1、等待购买蛋挞。。。。");
try {
//wait会释放锁
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println("2、买到蛋挞啦!下午茶开吃");
}).start();
//2s后生产蛋挞
Thread.sleep(2000L);
eggTart = new Object();
synchronized (this){
this.notifyAll();
System.out.println("3、通知consumer");
}
}
/**
* wait/notify 死锁,wait()在notify后面执行就会存在死锁
*/
public void waitNotifyOrderDead() throws InterruptedException {
new Thread(() -> {
while(eggTart == null) {
try {
Thread.sleep(5000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (this){
System.out.println("1、等待购买蛋挞。。。。");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println("2、买到蛋挞啦!下午茶开吃");
}).start();
//2s后生产蛋挞
Thread.sleep(2000L);
eggTart = new Object();
synchronized (this){
this.notifyAll();
System.out.println("3、通知consumer");
}
}
/**
* park/unpark 正常方法
*/
public void parkUnpark() throws InterruptedException {
Thread consumerThread = new Thread(() -> {
while(eggTart == null) {
try {
Thread.sleep(5000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("1、等待购买蛋挞。。。。");
LockSupport.park();
}
System.out.println("2、买到蛋挞啦!下午茶开吃");
});
consumerThread.start();
//2s后生产蛋挞
Thread.sleep(2000L);
eggTart = new Object();
//unpark需要指定线程,park和unpark没有顺序,先后谁都可以
LockSupport.unpark(consumerThread);
System.out.println("3、通知consumer");
}
/**
* park/unpark 死锁,在同步代码块中存在死锁的问题(park不会释放锁,所以会导致死锁)
*/
public void parkUnparkDead() throws InterruptedException {
Thread consumerThread = new Thread(() -> {
while(eggTart == null) {
System.out.println("1、等待购买蛋挞。。。。");
// 当前线程拿到锁,然后挂起
synchronized (this){
//park不会释放锁,所以会导致死锁
LockSupport.park();
}
}
System.out.println("2、买到蛋挞啦!下午茶开吃");
});
consumerThread.start();
//2s后生产蛋挞
Thread.sleep(2000L);
eggTart = new Object();
// 需要争取到锁以后,再恢复consumerThread
synchronized (this){
LockSupport.unpark(consumerThread);
}
System.out.println("3、通知consumer");
}
public static void main(String[] args) throws InterruptedException {
ThreadCommunicateDemo threadCommunicateDemo = new ThreadCommunicateDemo();
// threadCommunicateDemo.suspendResume();
// threadCommunicateDemo.suspendResumeSynchronizedDead();
// threadCommunicateDemo.suspendResumeOrderDead();
// threadCommunicateDemo.waitNotify();
// threadCommunicateDemo.waitNotifyOrderDead();
threadCommunicateDemo.parkUnpark();
// threadCommunicateDemo.parkUnparkDead();
}
}
补充: 线程封闭:多线程访问共享变量时,涉及到线程间数据同步问题,但是并不是所有时候都需要用到共享数据,所以就有了线程封闭。数据被封闭都自己的线程中,就不需要同步,这中将数据封闭在线程中而避免使用同步的技术就叫做线程封闭。线程封闭的具现:ThreadLocal、局部变量(栈封闭)。
ThreadLocal:它是java里特殊的变量。它是一个线程级别变量,每个线程都有一个ThreadLocal就是每个线程都拥有了自己独立的一个变量,在并发模式下是绝对安全的变量。
ThreadLocal原理:会自动在每个线程上创建一个副本,副本之间彼此独立互不影响。可以理解为JVM维护了一个Map<Thread,T>每个线程都要用到这个T的时候,用当前的线程去Map里面取值。
例子:执行结果如下,实现了线程封闭。
总结
1、suspend/resume对调用顺序有要求,也要开发自己注意锁的释放。这个被弃用的API, 容易死锁,也容易导致永久挂起。
2、wait/notify要求再同步关键字里面使用,免去了死锁的困扰,但是一定要先调用wait,再调用notify,否则永久等待了
3、 park/unpark没有顺序要求,但是park并不会释放锁,所有再同步代码中使用要注意