1. 引入wait/notify
回顾:在之前的学习中,当我们创建一个对象后,synchronized给对象上锁,JVM会给对象头关联一个Monitor对象,这个Monitor由三部分组成。
一是Owner对象,里面存储的是创建该对象的线程
二是EntryList,想试图获取该对象资源的其它堵塞线程队列
三是WaitSet,存储的是放弃对象锁的线程
- Owner线程中的锁对象,如果发现条件不满足,调用wait()方法,既可以进入到WaitSet变为WAITING状态
- EntryList下和WaitSet下的线程都属于堵塞状态,不占用CPU时间片
- EntryList下的线程会在Owner是释放锁时被唤醒
- WaitSet下的线程会在Owner线程调用notify或notifyAll时被唤醒,但是唤醒后并不意味立刻获得锁,需要进入EntryList重新竞争锁
2. API介绍
obj.wait() 会让obj对象由拥有锁到暂时放弃锁,进入到waitset中
obj.notify() 会唤醒在waitset中的线程,然后需要进入EntryList重新竞争锁
obj.notifyAll() 会唤醒所有在waitset中的线程,然后需要进入EntryList重新竞争锁
case:
public class TestWaitNotify {
final static Object obj = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
},"t1").start();
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
},"t2").start();
// 主线程两秒后执行
sleep(2);
log.debug("唤醒 obj 上其它线程");
synchronized (obj) {
// obj.notify(); // 唤醒obj上一个线程
obj.notifyAll(); // 唤醒obj上所有等待线程
}
}
wait(long n)和wait()的区别是,前者是有时限的等待,时间到后自己进入EntryList中,后者如果没有notify()或notifyAll()唤醒则进入永久等待。
3.wait notify 的正确姿势
开始之前先看看sleep()和wait()方法有什么区别?
- sleep()是Thread的静态方法,而wait()方法是Object类下的方法
- sleep()方法不需要与synchronized配合使用,但是wait()方法必须和sysnchronized配合才能使用
- 调用sleep()方法,对象不会释放锁,调用wait()方法时对象会释放锁
- 相同点就是调用两个方法后,线程的状态都是TIMED_WAITING状态
case one:观察下面例子,有什么不妥的地方吗?
@Slf4j(topic = "c.TestWaitNotify")
public class TestWaitNotify {
final static Object room = new Object();
static boolean hasCigarette = false;
public static void main(String[] args) {
//小南
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
sleep(2);
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
}
}
}, "小南").start();
//其它5个人
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (room) {
log.debug("可以开始干活了");
}
}, "其它人").start();
}
//送烟人
sleep(1);
new Thread(() -> {
// 这里能不能加 synchronized (room)?
hasCigarette = true;
log.debug("烟到了噢!");
}, "送烟的").start();
}
}
输出:
不妥之处:
1. 在小南线程睡咩后,由于sleep()方法没有释放锁,导致其余5个人和送烟人都无法执行代码
2. 送烟人已经送到了,小南还在睡
3. 其它干活的线程,都要一直阻塞,效率太低
解决办法:使用wait()/notify()方法
case two:改进case one
public class TestWaitNotify {
final static Object room = new Object();
static boolean hasCigarette = false;
public static void main(String[] args) {
//小南
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
}
}
}, "小南").start();
//其它5个人
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (room) {
log.debug("可以开始干活了");
}
}, "其它人").start();
}
//送烟人
sleep(1);
new Thread(() -> {
// 这里能不能加 synchronized (room)?
synchronized (room){
hasCigarette = true;
room.notify();
log.debug("烟到了噢!");
}
}, "送烟的").start();
}
}
输出:
使用wait()/notify()方法的确解决了case one的问题,但是这样也是存在弊端的,例如如果存在多个等待者呢?
case three:引入多个等待者
public class TestWaitNotify {
final static Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
public static void main(String[] args) {
//小南
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
}
}
}, "小南").start();
//小女等外卖
new Thread(() -> {
synchronized (room) {
Thread thread = Thread.currentThread();
log.debug("外卖送到没?[{}]", hasTakeout);
if (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("外卖送到没?[{}]", hasTakeout);
if (hasTakeout) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小女").start();
//送外卖
sleep(1);
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了噢!");
room.notify();
}
}, "送外卖的").start();
}
}
输出:
出现了一种特殊情况,就是我送外卖的唤醒的room.notify();
却是等烟的小南,这也是不正常的现象,出现该问题就是两个线程拥有了room对象并且进入了waitset,此时就不明确哪一个会被唤醒了。
但是如果使用notifyAll()方法去唤醒的话,会唤醒两个线程小南,小女,此时这样也不符合我们预期,我们只需要小女,不需要唤醒小南!
最终解决方法:
public class TestWaitNotify {
final static Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
public static void main(String[] args) {
//小南
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
while (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
}
}
}, "小南").start();
//小女等外卖
new Thread(() -> {
synchronized (room) {
Thread thread = Thread.currentThread();
log.debug("外卖送到没?[{}]", hasTakeout);
if (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("外卖送到没?[{}]", hasTakeout);
if (hasTakeout) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小女").start();
//送外卖
sleep(1);
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了噢!");
room.notifyAll();
}
}, "送外卖的").start();
}
}
输出:
将不去唤醒的线程在wait()外层用while()去处理,但满足某种条件说明改线程要去唤醒,此时while()就可以退出了,符合我们的预期。
synchronized(lock) {
while(条件不成立) {
lock.wait();
}
// 干活
}
//另一个线程
synchronized(lock) {
lock.notifyAll();
}
学习资料:https://www.bilibili.com/video/BV16J411h7Rd?p=96