3.10 wait notify 的正确姿势
3.10.0 sleep(long n) 和 wait(long n) 的区别
sleep 是 Thread 方法,而 wait 是 Object 的方法
sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
它们状态 都是 TIMED_WAITING (相同点)
当不满足条件等待,最好使用wait.因为sleep不会释放锁。sleep一般是让CPU休息…:
/** * 演示sleep和wait睡眠后,是否释放锁 */ @Slf4j(topic = "c.Test19") public class Test19 { static final Object lock = new Object(); public static void main(String[] args) { new Thread(() -> { synchronized (lock) { log.debug("获得锁"); try { // Thread.sleep(20000); lock.wait(20000); } catch (InterruptedException e) { e.printStackTrace(); } } }, "t1").start(); Sleeper.sleep(1); synchronized (lock) { log.debug("获得锁"); } } }
3.10.1 问题描述
小南只有叼有烟的时候才干活,其他人只要有时间片就干活,用代码模拟这个过程
3.10.2 step 1
@Slf4j(topic = "c.TestCorrectPosture") public class TestCorrectPostureStep1 { static final Object room = new Object();//建议:引用用final修饰,保证锁的对象就不可变。 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("没烟,先歇会!"); sleep(2); } log.debug("有烟没?[{}]", hasCigarette); if (hasCigarette) { log.debug("可以开始干活了"); } } }, "小南").start(); for (int i = 0; i < 5; i++) { new Thread(() -> { synchronized (room) { log.debug("可以开始干活了"); } }, "其它人").start(); } sleep(1); new Thread(() -> { // 这里能不能加 synchronized (room)? //synchronized (room) { hasCigarette = true; log.debug("烟到了噢!"); // } }, "送烟的").start(); } }
输出1:
17:17:31.927 c.TestCorrectPosture [小南] - 有烟没?[false] 17:17:31.935 c.TestCorrectPosture [小南] - 没烟,先歇会! 17:17:32.929 c.TestCorrectPosture [送烟的] - 烟到了噢! 17:17:33.940 c.TestCorrectPosture [小南] - 有烟没?[true] 17:17:33.940 c.TestCorrectPosture [小南] - 可以开始干活了 17:17:33.940 c.TestCorrectPosture [其它人] - 可以开始干活了 17:17:33.940 c.TestCorrectPosture [其它人] - 可以开始干活了 17:17:33.940 c.TestCorrectPosture [其它人] - 可以开始干活了 17:17:33.940 c.TestCorrectPosture [其它人] - 可以开始干活了 17:17:33.940 c.TestCorrectPosture [其它人] - 可以开始干活了
输出2:(当加上synchronized (room)
)
17:25:44.996 c.TestCorrectPosture [小南] - 有烟没?[false] 17:25:45.004 c.TestCorrectPosture [小南] - 没烟,先歇会! 17:25:47.007 c.TestCorrectPosture [小南] - 有烟没?[false] 17:25:47.007 c.TestCorrectPosture [送烟的] - 烟到了噢! 17:25:47.007 c.TestCorrectPosture [其它人] - 可以开始干活了 17:25:47.007 c.TestCorrectPosture [其它人] - 可以开始干活了 17:25:47.007 c.TestCorrectPosture [其它人] - 可以开始干活了 17:25:47.007 c.TestCorrectPosture [其它人] - 可以开始干活了 17:25:47.007 c.TestCorrectPosture [其它人] - 可以开始干活了
分析:
其它干活的线程,都要一直阻塞,效率太低
小南线程必须睡足 2s 后才能醒来,就算烟提前送到,也无法立刻醒来
main加了synchronized (room)后,就好比小南在里面反锁了门睡觉,烟根本没法送进门。没加
synchronized 就好像 main 线程是翻窗户进来的
解决方法,使用 wait - notify 机制
3.10.3 step 2
使用wait-notify改进上面代码
@Slf4j(topic = "c.TestCorrectPosture") public class TestCorrectPostureStep2 { static final 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(2000); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("有烟没?[{}]", hasCigarette); if (hasCigarette) { log.debug("可以开始干活了"); } } }, "小南").start(); for (int i = 0; i < 5; i++) { new Thread(() -> { synchronized (room) { log.debug("可以开始干活了"); } }, "其它人").start(); } sleep(1); new Thread(() -> { synchronized (room) { hasCigarette = true; log.debug("烟到了噢!"); room.notify(); } }, "送烟的").start(); } }
输出:
17:42:24.719 c.TestCorrectPosture [小南] - 有烟没?[false] 17:42:24.727 c.TestCorrectPosture [小南] - 没烟,先歇会! 17:42:24.727 c.TestCorrectPosture [其它人] - 可以开始干活了 17:42:24.727 c.TestCorrectPosture [其它人] - 可以开始干活了 17:42:24.727 c.TestCorrectPosture [其它人] - 可以开始干活了 17:42:24.727 c.TestCorrectPosture [其它人] - 可以开始干活了 17:42:24.727 c.TestCorrectPosture [其它人] - 可以开始干活了 17:42:25.719 c.TestCorrectPosture [送烟的] - 烟到了噢! 17:42:25.719 c.TestCorrectPosture [小南] - 有烟没?[true] 17:42:25.719 c.TestCorrectPosture [小南] - 可以开始干活了
分析:
- 此改进可以让其他线程同时运行,不会占用锁,并发效率大大提升
- 思考:如果有其他线程也在等待,那么会不会错误的叫醒了其他线程?
3.10.4 step 3-4
加入另外一个线程 小女线程时,当外卖送到,小女可开始工作。思考并分析运行结果
@Slf4j(topic = "c.TestCorrectPosture") public class TestCorrectPostureStep3 { static final 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("可以开始干活了"); } else { 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(); // room.notifyAll(); } }, "送外卖的").start(); } }
输出1:(使用notify
)
- notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线
程,称之为【虚假唤醒】 - 解决方法,改为 notifyAll
输出2:(使用notifyAll
)
用 notifyAll 仅解决某个线程的唤醒问题,但使用 if + wait 判断仅有一次机会,一旦条件不成立,就没有重新
判断的机会了(比如小南被错误唤醒后,就不能重新判断了)
解决方法,用 while + wait,当条件不成立,再次 wait
3.10.5 step 5
将 if
改为 while
,防止虚假唤醒
@Slf4j(topic = "c.TestCorrectPosture") public class TestCorrectPostureStep5 { static final 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("可以开始干活了"); } else { log.debug("没干成活..."); } } }, "小南").start(); new Thread(() -> { synchronized (room) { Thread thread = Thread.currentThread(); log.debug("外卖送到没?[{}]", hasTakeout); while (!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(); } }
输出:
*结论:**此方法满足需求。即提高了并发效率问题,又不会出现虚假唤醒问题。
思考:while中使用wait()相比wait(long num)会不会浪费CPU呢?
只有线程被唤醒才会接着while,不唤醒就是wait,所以不会有cpu空转
3.10.6 使用wait和notify的正确姿势总结
synchronized(lock) { while(条件不成立) {//while防止虚假唤醒 lock.wait(); } // 干活 } //另一个线程 synchronized(lock) { lock.notifyAll();//notifyAll唤醒所有等待序列中国的线程 }
3.11 Park & Unpark
3.11.1 基本使用
它们是 LockSupport 类中的方法
// 暂停当前线程 LockSupport.park(); // 恢复某个线程的运行 LockSupport.unpark(暂停线程对象)
先 park 再 unpark
@Slf4j(topic = "c.TestParkUnpark") public class TestParkUnpark { public static void main(String[] args) { Thread t1 = new Thread(() -> { log.debug("start..."); Sleeper.sleep(1); log.debug("park..."); LockSupport.park(); log.debug("resume..."); }, "t1"); t1.start(); Sleeper.sleep(2); log.debug("unpark..."); LockSupport.unpark(t1); } }
先 unpark 再 park:交换main和t1线程sleep的时间
3.11.2 特点
与 Object 的 wait & notify 相比
wait,notify 和 notifyAll 必须配合 Object Monitor(锁) 一起使用,而 park,unpark 不必
park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】
- park & unpark 可以先 unpark,而 wait & notify 不能先 notify (先notify的代码会被忽略)
3.11.3 原理之 park & unpark
3.12 重新理解线程状态转换
NEW:初始状态;创建了Java线程对象,还没有与操作系统的线程相关联。
情况 1: NEW --> RUNNABLE
当调用 t.start() 方法时,由 NEW --> RUNNABLE
情况 2: RUNNABLE <--> WAITING
t 线程用 synchronized(obj) 获取了对象锁后
调用 obj.wait() 方法时,t 线程从 RUNNABLE --> WAITING
调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
竞争锁成功,t 线程从 WAITTING --> RUNNABLE
竞争锁失败,t 线程依旧是 WAITTING --> BLOCKED
**注意:**第二步中,调用 obj.notify() , obj.notifyAll() , t.interrupt() 但未释放锁,t 线程会先进入EntryList 等待竞争锁,此时为BLOCKED状态。持锁线程释放锁后 EntryList中的线程会进行竞争。然后 根据竞争结果,t 线程会处于不同的状态。此过程可在后续Debug分析中清晰看到…
测试代码
@Slf4j(topic = "c.TestWaitNotify") 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上所有等待线程 } } }
Debug分析:
情况 3: RUNNABLE <--> WAITING
当前线程调用 t.join() 方法时,当前线程从 RUNNABLE --> WAITING
注意是当前线程在t 线程对象的监视器上等待
t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING --> RUNNABLE
情况 4 RUNNABLE <--> WAITING
当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING
调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING --> RUNNABLE
情况 5 RUNNABLE <--> TIMED_WAITING
t 线程用 synchronized(obj) 获取了对象锁后
调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE --> TIMED_WAITING
t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
竞争锁成功,t 线程从 TIMED_WAITING --> RUNNABLE
竞争锁失败,t 线程从 TIMED_WAITING --> BLOCKED
情况 6 RUNNABLE <--> TIMED_WAITING
当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING
注意是当前线程在t 线程对象的监视器上等待
当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从
TIMED_WAITING --> RUNNABLE
情况 7 RUNNABLE <–> TIMED_WAITING
当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING
当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE
情况 8 RUNNABLE <--> TIMED_WAITING
当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线
程从 RUNNABLE --> TIMED_WAITING
调用 LockSupport.unpark(目标线程) 或调用了线程 的interrupt(),或是等待超时,会让目标线程从
TIMED_WAITING--> RUNNABLE
情况 9 RUNNABLE <--> BLOCKED
t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED
持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争
成功,从BLOCKED --> RUNNABLE,其它失败的线程仍然 BLOCKED
情况 10 RUNNABLE <--> TERMINATED
当前线程所有代码运行完毕,进入TERMINATED