1.wait/notify
1.1.为什么需要wait?
小故事:
①.假设多个用户(线程)都需要进入房间使用算盘(CPU)进行计算工作,但是为了保证计算过程中的安全,老王设计了一把锁(Synchronized),每次只允许一个用户(线程)拿到钥匙进入房间(成为Owner线程);
②.小南(线程)费了九牛二虎之力,抢到了锁,进入到房间(成为Owner线程),但是在工作过程中由于条件不满足,小南不能继续进行计算,此时小南就需要等待.但小南如果一直占用着锁,其它人就得一直阻塞,效率太低;
③.于是老王单开了一间休息室(调用wait方法),让小南到休息室(WaitSet)等着去了,此时锁就被释放开,其它人可以由老王随机安排进屋(不满足条件的线程不会影响其他线程的运行!);
④.直到小M将烟送来,大叫一声"你的烟到了" (调用notify方法);
⑤.于是小南就离开了休息室,(为了公平起见)重新进入竞争锁的队列(EntryList),进行下一轮锁的竞争;
1.2.原理
①.Owner线程(获取对象关联的Monitor对象的线程,即获取Monitor锁的线程)发现条件不满足,为了不占用锁资源,它会调用wait()方法,即可进入WaitSet变为WAITING状态(锁会被释放,同时唤醒EntryList等待队列中处于BLOCKED阻塞状态的线程,然后这些线程竞争锁);
②.BLOCKED和WAITING状态的线程都处于阻塞状态,不占用CPU时间片;
③.BLOCKED状态的线程会在Owner线程释放锁时自动被唤醒;而WAITING状态的线程会在Owner线程调用notify或notifyAll方法时才能唤醒(注意此时的Owner线程已经不是之前的Owner线程),而且唤醒后并不意味着立刻获得锁,仍需进入EntryList等待队列中变成BLOCKED状态,等待再次被唤醒,然后参与锁竞争;
当执行其他线程中的"obj.notifyAll()或者obj.notify()"这一行代码之后,WaitSet中的WAITING状态的线程会被唤醒尝试获取对象锁,但是此时其他线程Synchronized(obj)代码还没有执行完毕,也就是说对象锁还没有释放,此时被唤醒的线程获取对象锁失败,然后这些线程会进入EntryList等待队列中变成BLOCKED状态,等待被唤醒,重新竞争对象锁;
1.3.API介绍
1>.obj.wait(long timeout)
: 让进入object监视器的线程(不满足条件无法继续运行的Owner线程)到waitSet等待;
①.如果wait()方法没有添加参数或者参数为’0’,那么就表示该线程会一直/无限制等待下去,直到notify/notifyAll为止;
②.如果wait()方法加了参数(/超时时间),那么就表示该线程只会等待指定的时间,如果超过这个时间还没有被唤醒则自动结束等待,继续执行后面的代码;如果在指定的等待时间之内提前被唤醒了,那么就可以提前执行而不需要等到指定时间到达;
2>.obj.notify()
: 在object上正在waitSet等待的线程中挑一个唤醒;
3>.obj.notifyAll()
: 让object上正在waitSet等待的线程全部唤醒;
它们都是线程之间进行协作/通信的手段,都属于Object对象的方法.必须获得此对象的锁(必须成为对象关联的Monitor对象的所有者),才能调用这几个方法(即必须是在synchronized代码块中才能调用这几个方法),否则会出现'IllegalMonitorStateException'异常!!!
1.4.案例
@Slf4j
public class TestNotifyOrNotifyAll {
//对象锁
static final Object OBJ = new Object();
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (OBJ) {
log.info("线程t1开始执行....");
try {
//必须获取对象的锁,才能调用相关的方法,否则会出现'IllegalMonitorStateException'异常;
//让线程在obj上一直等待下去
OBJ.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("线程t1执行其它代码....");
}
}, "t1").start();
new Thread(() -> {
synchronized (OBJ) {
log.info("线程t2开始执行....");
try {
//让线程在obj上一直等待下去
OBJ.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("线程t2执行其它代码....");
}
}, "t2").start();
// 主线程两秒后执行
TimeUnit.SECONDS.sleep(2);
log.info("main线程唤醒obj上其它线程");
synchronized (OBJ) {
//唤醒obj上一个线程,另外一个线程会一直等待,导致程序一直处于运行中,无法正常停止!
//OBJ.notify();
//唤醒obj上所有等待线程
OBJ.notifyAll();
}
}
}
1.5.sleep(long n)和wait(long n)的区别
1>.sleep是Thread中的(静态)方法,而wait是Object的(原生)方法;
2>.sleep不需要强制和synchronized配合使用,但wait必须和synchronized一起用,而且是在synchronized代码块内部使用;
3>.sleep在睡眠的同时不会释放对象锁的,但wait在等待的时候会释放对象锁;
4>.它们都会让线程的状态变成TIMED_WAITING阻塞;
1.6.wait/notify的正确姿势
1>.示例代码
@Slf4j
public class TestNotifyOrWait {
//注意:锁对象最好使用"final"关键字来修饰,可以防止其他子类对其修改!!
static final Object room = new Object();
static boolean hasCigarette = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (room) {
log.info("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.info("没烟,先歇会!");
try {
//小南线程获取到对象锁,但是由于条件不满足,无法继续执行
//小南线程必须睡足2s后才能醒来,就算烟提前送到,也无法立刻醒来
//释放CPU时间片,但是不释放对象锁;
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.info("可以开始干活了");
}
}
}, "小南").start();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
//由于对象锁一直被小南线程占用,因此这里多个线程无法获取到对象锁而进入EntryList等待;
//等到小南线程执行完synchronized同步代码块中的代码释放了对象锁,唤醒这些线程,他们才可以执行;
synchronized (room) {
log.info("可以开始干活了");
}
}, "其它人").start();
}
//main线程阻塞
Thread.sleep(1000);
//送烟的线程修改小南线程的执行条件
new Thread(() -> {
//这里能不能加 synchronized(room)?
//如果在这里加上synchronized同步块,那么该送烟线程必须等到小南线程执行完毕释放对象锁之后才有可能修改小南线程的执行条件,可是此时小南线程已经运行完毕(任务没有做);
hasCigarette = true;
log.info("烟到了噢!");
}, "送烟的").start();
}
}
分析:
①.其它干活的线程在小南线程执行完释放对象锁之前都要一直阻塞,效率太低;
②.小南线程必须睡足2s后才能醒来,就算烟提前送到,也无法立刻醒来;
③.加了synchronized (room)后,就好比小南在里面反锁了门睡觉,烟根本没法送进门,main线程没加synchronized就好像main线程是翻窗户进来的;
③.加了synchronized (room)后,就好比小南在里面反锁了门睡觉,烟根本没法送进门,main线程没加synchronized就好像main线程是翻窗户进来的;
2>.示例代码
@Slf4j
public class TestNotifyOrWait2 {
//注意:锁对象最好使用"final"关键字来修饰,可以防止其他子类对其修改!!
static final Object room = new Object();
static boolean hasCigarette = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (room) {
log.info("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.info("没烟,先歇会!");
try {
//小南线程获取到对象锁,但是条件不满足,进入WaitSet中变成WAITING状态
//如果在等待期间提前被唤醒,那可以提前执行(进入到EntryList中)
//如果在等待时间到达之后还没有被唤醒,也会自动结束等待,继续执行(进入到EntryList中)
//在等待期间小南线程会释放对象锁
room.wait(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.info("可以开始干活了");
}
}
}, "小南").start();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
//这里多个其他线程可以拿到对象锁,执行自己的任务
synchronized (room) {
log.info("可以开始干活了");
}
}, "其它人").start();
}
Thread.sleep(1000);
new Thread(() -> {
//这里的送烟线程也可以拿到对象锁,修改小南线程的执行条件
//同时还会唤醒WaitSet中处于WAITING状态的线程(小南线程)
//之后小南线程进入到EntryList等待队列中进行BLOCKED阻塞,等待被唤醒;
synchronized (room) {
hasCigarette = true;
log.info("烟到了噢!");
room.notify();
}
}, "送烟的").start();
}
}
分析:
①.解决了其它干活的线程阻塞的问题;
②.但如果有其它线程也在等待条件,那么送烟线程会不会把其他WAITING等待中的线程唤醒?
3>.示例代码
@Slf4j
public class TestWaitOrNotify {
//注意:锁对象最好使用"final"关键字来修饰,可以防止其他子类对其修改!!
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (room) {
log.info("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.info("没烟,先歇会!");
try {
//小南线程进入WAITING状态,释放锁
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.info("可以开始干活了");
} else {
log.info("没干成活...");
}
}
}, "小南").start();
new Thread(() -> {
synchronized (room) {
log.info("外卖送到没?[{}]", hasTakeout);
if (!hasTakeout) {
log.info("没外卖,先歇会!");
try {
//小女线程进入WAITING状态,释放锁
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("外卖送到没?[{}]", hasTakeout);
if (hasTakeout) {
log.info("可以开始干活了");
} else {
log.info("没干成活...");
}
}
}, "小女").start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.info("外卖到了噢!");
//随机唤醒WaitSet中的一个线程
//注意:有可能这个送外卖的线程唤醒的是小南线程,但是小南线程需要的烟不是外卖(虚假唤醒),而且另外一个线程
//没有被唤醒.就会一直处于WAITING状态,最终导致程序无法正常退出;
room.notify();
}
}, "送外卖的").start();
}
}
分析:
①.notify只能随机唤醒一个WaitSet中的线程,这时如果还有其它线程也在等待,那么notify就可能唤醒不了正确的线程,称之为"虚假唤醒";
②.解决方法: 改为
notifyAll
;
4>.示例代码
@Slf4j
public class TestWaitOrNotify {
//注意:锁对象最好使用"final"关键字来修饰,可以防止其他子类对其修改!!
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (room) {
log.info("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.info("没烟,先歇会!");
try {
//小南线程进入WAITING状态,释放锁
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.info("可以开始干活了");
} else {
log.info("没干成活...");
}
}
}, "小南").start();
new Thread(() -> {
synchronized (room) {
log.info("外卖送到没?[{}]", hasTakeout);
if (!hasTakeout) {
log.info("没外卖,先歇会!");
try {
//小女线程进入WAITING状态,释放锁
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("外卖送到没?[{}]", hasTakeout);
if (hasTakeout) {
log.info("可以开始干活了");
} else {
log.info("没干成活...");
}
}
}, "小女").start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.info("外卖到了噢!");
//随机唤醒WaitSet中的一个线程
//注意:有可能这个送外卖的线程唤醒的是小南线程,但是小南线程需要的烟不是外卖,而且另外一个线程
//没有被唤醒.就会一直处于WAITING状态,最终导致程序无法正常退出;
//room.notify();
//唤醒WaitSet中所有的线程
room.notifyAll();
}
}, "送外卖的").start();
}
}
分析:
①.用notifyAll仅解决某个线程的唤醒问题,但使用 if + wait判断仅有一次机会,一旦条件不成立,就没有重新判断的机会了;
②.解决方法:
用while + wait
,当条件不成立,再次wait;
5>.示例代码
@Slf4j
public class TestWaitOrNotify {
//注意:锁对象最好使用"final"关键字来修饰,可以防止其他子类对其修改!!
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (room) {
log.info("有烟没?[{}]", hasCigarette);
while (!hasCigarette) {
log.info("没烟,先歇会!");
try {
//小南线程进入WAITING状态,释放锁
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.info("可以开始干活了");
} else {
log.info("没干成活...");
}
}
}, "小南").start();
new Thread(() -> {
synchronized (room) {
log.info("外卖送到没?[{}]", hasTakeout);
while (!hasTakeout) {
log.info("没外卖,先歇会!");
try {
//小女线程进入WAITING状态,释放锁
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("外卖送到没?[{}]", hasTakeout);
if (hasTakeout) {
log.info("可以开始干活了");
} else {
log.info("没干成活...");
}
}
}, "小女").start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.info("外卖到了噢!");
//随机唤醒WaitSet中的一个线程
//注意:有可能这个送外卖的线程唤醒的是小南线程,但是小南线程需要的烟不是外卖,而且另外一个线程
//没有被唤醒.就会一直处于WAITING状态,最终导致程序无法正常退出;
//room.notify();
room.notifyAll();
}
}, "送外卖的").start();
}
}
分析:
把if改成while可以防止虚假唤醒!!!
小总结: 正确使用wait/notify的套路
//某一个线程
synchronized(对象锁) {
while(条件不成立) {
对象锁.wait();
}
// 干活
}
//另一个线程
synchronized(同一个对象锁) {
同一个对象锁.notifyAll();
}