JUC在深入面试题——三种方式实现线程等待和唤醒(wait/notify,await/sign

[复制链接]
发表于 2022-9-23 09:03:31 | 显示全部楼层 |阅读模式
一、前言

在多线程的场景下,我们会经常使用加锁,来保证线程安全。如果锁用的不好,就会陷入死锁,我们以前可以使用Object的wait/notify来解决死锁问题。也可以使用Condition的await/signal来解决,当然最优还是LockSupport的park/unpark。他们都是解决线程等待和唤醒的。下面来说说具体的优缺点和例子证明一下。
二、wait/notify的使用

1. 代码演示
  1. public class JUC {
  2.     static Object lock = new Object();
  3.     public static void main(String[] args) {
  4.         new Thread(()->{
  5.             synchronized (lock) {// 1
  6.                 System.out.println(Thread.currentThread().getName() + "进来");
  7.                 try {
  8.                     // 释放锁,陷入阻塞,直到有人唤醒
  9.                     lock.wait();
  10.                 } catch (Exception e) {
  11.                     e.printStackTrace();
  12.                 }
  13.             }// 1
  14.             System.out.println(Thread.currentThread().getName() + "我被唤醒了");
  15.         }, "A").start();
  16.         new Thread(()->{
  17.             synchronized (lock) {// 2
  18.                 lock.notify();
  19.                 System.out.println(Thread.currentThread().getName() + "随机唤醒一个线程");
  20.             }// 2
  21.         }, "B").start();
  22.     }
  23. }
复制代码
2. 执行结果


3. 测试不在代码块执行(把上面代码注释1给删除)


4. 修改代码
  1. try {
  2.     TimeUnit.SECONDS.sleep(3);
  3. } catch (InterruptedException e) {
  4.     e.printStackTrace();
  5. }
复制代码

5. 总结

wait和notify方法必须要在同步块或者方法里面且成对出现使用,否则会抛出java.lang.IllegalMonitorStateException。
调用顺序要先wait后notify才可以正常阻塞和唤醒。
三、await/signal的使用

1. 代码演示
  1. public class JUC {
  2.     static ReentrantLock reentrantLock = new ReentrantLock();
  3.     static Condition condition = reentrantLock.newCondition();
  4.     public static void main(String[] args) {
  5.         new Thread(()->{
  6.             reentrantLock.lock();// 1
  7.             try {
  8.                 System.out.println(Thread.currentThread().getName()+"进来");
  9.                 condition.await();
  10.             } catch (InterruptedException e) {
  11.                 e.printStackTrace();
  12.             } finally {
  13.                 reentrantLock.unlock();// 1
  14.             }
  15.             System.out.println(Thread.currentThread().getName()+"我被唤醒了");
  16.         },"A").start();
  17.         new Thread(()->{
  18.             reentrantLock.lock();// 1
  19.             try {
  20.                 condition.signal();
  21.                 System.out.println(Thread.currentThread().getName()+"随机唤醒一个线程");
  22.             }finally {
  23.                 reentrantLock.unlock();// 1
  24.             }
  25.         },"B").start();
  26.     }
  27. }
复制代码
2. 执行结果


3. 测试不在代码块执行(把上面代码注释1给删除)


4. 修改代码
  1. try {
  2.     TimeUnit.SECONDS.sleep(3);
  3. } catch (InterruptedException e) {
  4.     e.printStackTrace();
  5. }
复制代码
5. 总结

await和signal方法必须要在同步块或者方法里面且成对出现使用,否则会抛出java.lang.IllegalMonitorStateException。
调用顺序要先await后signal才可以正常阻塞和唤醒。——和wait/notify一致。
四、LockSupport的park/unpark的使用

1. LockSupport介绍

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit只有两个值1和0,默认是0。
可以把许可看成是一种(0、1)信号量(Semaphore),但与Semaphore不同的是,许可的累加上限是1。
2. park源码查看
  1. public static void park(Object blocker) {
  2.     Thread t = Thread.currentThread();
  3.     setBlocker(t, blocker);
  4.     UNSAFE.park(false, 0L);
  5.     setBlocker(t, null);
  6. }
  7. public static void park() {
  8.     UNSAFE.park(false, 0L);
  9. }
复制代码
作用:park()/park(Object blocker) - 阻塞当前线程阻塞传入的具体线程
我们会发现底层是调用sun.misc.Unsafe:这个类的提供了一些绕开JVM的更底层功能,基于它的实现可以提高效率。
permit默认是0,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park方法会被唤醒,然后会将permit再次设置为0并返回。
3. unpark源码查看
  1. public static void unpark(Thread thread) {
  2.     if (thread != null)
  3.         UNSAFE.unpark(thread);
  4. }
复制代码
作用:unpark(Thread thread) - 唤醒处于阻塞状态的指定线程
我们会发现底层都是调用sun.misc.Unsafe。
调用unpark(thread)方法后,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,pemit值还是1)会自动唤醒thead线程,即之前阻塞中的LockSupport.park()方法会立即返回。
4. 代码演示
  1. public class JUC {
  2.     public static void main(String[] args) {
  3.         Thread a = new Thread(()->{
  4.             System.out.println(Thread.currentThread().getName() + "进来");
  5.             LockSupport.park();
  6.             System.out.println(Thread.currentThread().getName() + " 被换醒了");
  7.         }, "A");
  8.         a.start();
  9.         Thread b = new Thread(()->{
  10.             try {
  11.                 TimeUnit.SECONDS.sleep(1);
  12.             } catch (InterruptedException e) {
  13.                 e.printStackTrace();
  14.             }
  15.             LockSupport.unpark(a);
  16.             System.out.println(Thread.currentThread().getName()+"唤醒传入的线程");
  17.         }, "B");
  18.         b.start();
  19.     }
  20. }
复制代码
5. 结果展示


6. 修改代码
  1. try {
  2.         TimeUnit.SECONDS.sleep(3);
  3. } catch (InterruptedException e) {
  4.         e.printStackTrace();
  5. }
  6. System.out.println(Thread.currentThread().getName() + "进来" + System.currentTimeMillis());
  7. LockSupport.park();
  8. System.out.println(Thread.currentThread().getName() + " 被换醒了" + System.currentTimeMillis());
复制代码

7. 与前两者比的优点

park/unpark不需要在同步块或者方法内才能执行,解决了上面两种不在同步块或者方法就报错的情况。
park/unpark不需要先执行park,在执行unpark,无需在意顺序。解决了上面两种必须有前后顺序的情况。
8.总结

LockSupport是用来创建锁和共他同步类的基本线程阻塞原语。
LockSuport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻寨之后也有对应的唤醒方法。归根结底,LockSupport调用的Unsafe中的native代码(C++)。
  1. public native void park(boolean var1, long var2);
复制代码
LockSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞的过程。
LockSupport和每个使用它的线程都有一个许可(permit)关联。permit相当于1,0的开关,默认是0,调用一次unpark就加1变成1,调用一次park会消费permit,也就是将1变成0,同时park立即返回。
如再次调用park会变成阻塞(因为permit为零了会阻塞在这里,一直到permit变为1),这时调用unpark会把permit置为1。每个线程都有一个相关的permit,permit最多只有一个,重复调用unpark也不会积累凭证。

阻塞原因:根据上面代码,我们会先执行线程B,调用unpark方法,虽然进行两次unpark。但是只有一个有效,此时permit为1。此时A线程开始,来到第一个park,permit消耗后为0,为0是阻塞,等待unpark,此时没有unpark了,所以一直陷入阻塞。
9.白话文理解

[size=3.5]线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。
当调用park方法时
如果有凭证,则会直接消耗掉这个凭证然后正常退出。
如果无凭证,就必须阻塞等待凭证可用。
而unpark则相反,它会增加一个凭证,但凭证最多只能有1个,累加无放。

五、面试题

为什么可以先唤醒线程后阻塞线程?
因为unpark获得了一个凭证,之后再调用park方法,此时permit为1,就可以名正言顺的凭证消费,permit为0,故不会阻塞。
为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?
因为凭证的数量最多为1(不能累加),连续调用两次 unpark和调用一次 unpark效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证,证不够,不能放行。
六、总结

看到这里的小伙伴,点个赞不过分吧,小编也是整理了一下午,参考阳哥课件。

推广自己网站时间到了!!!
点击访问!欢迎访问,里面也是有很多好的文章哦!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表