• JUC在深入面试题——三种方式实现线程等待和唤醒(wait/notify,await/signal,LockSupport的park/unpark)


    一、前言

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

    二、wait/notify的使用

    1. 代码演示

    public class JUC {
    
        static Object lock = new Object();
    
        public static void main(String[] args) {
            new Thread(()->{
                synchronized (lock) {// 1
                    System.out.println(Thread.currentThread().getName() + "进来");
                    try {
                        // 释放锁,陷入阻塞,直到有人唤醒
                        lock.wait();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }// 1
                System.out.println(Thread.currentThread().getName() + "我被唤醒了");
            }, "A").start();
    
            new Thread(()->{
                synchronized (lock) {// 2
                    lock.notify();
                    System.out.println(Thread.currentThread().getName() + "随机唤醒一个线程");
                }// 2
            }, "B").start();
        }
    }
    

    2. 执行结果

    在这里插入图片描述

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

    在这里插入图片描述

    4. 修改代码

    try {
        TimeUnit.SECONDS.sleep(3);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    

    在这里插入图片描述

    5. 总结

    wait和notify方法必须要在同步块或者方法里面且成对出现使用,否则会抛出java.lang.IllegalMonitorStateException

    调用顺序要先wait后notify才可以正常阻塞和唤醒。

    三、await/signal的使用

    1. 代码演示

    public class JUC {
    
        static ReentrantLock reentrantLock = new ReentrantLock();
        static Condition condition = reentrantLock.newCondition();
    
        public static void main(String[] args) {
            new Thread(()->{
                reentrantLock.lock();// 1
                try {
                    System.out.println(Thread.currentThread().getName()+"进来");
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    reentrantLock.unlock();// 1
                }
    
                System.out.println(Thread.currentThread().getName()+"我被唤醒了");
            },"A").start();
    
            new Thread(()->{
                reentrantLock.lock();// 1
                try {
                    condition.signal();
                    System.out.println(Thread.currentThread().getName()+"随机唤醒一个线程");
                }finally {
                    reentrantLock.unlock();// 1
                }
            },"B").start();
    
        }
    }
    

    2. 执行结果

    在这里插入图片描述

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

    在这里插入图片描述

    4. 修改代码

    try {
        TimeUnit.SECONDS.sleep(3);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    

    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源码查看

    public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, 0L);
        setBlocker(t, null);
    }
    public static void park() {
        UNSAFE.park(false, 0L);
    }
    

    作用:park()/park(Object blocker) - 阻塞当前线程阻塞传入的具体线程

    我们会发现底层是调用sun.misc.Unsafe:这个类的提供了一些绕开JVM的更底层功能,基于它的实现可以提高效率。

    permit默认是0,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时park方法会被唤醒,然后会将permit再次设置为0并返回。

    3. unpark源码查看

    public static void unpark(Thread thread) {
        if (thread != null)
            UNSAFE.unpark(thread);
    }
    

    作用:unpark(Thread thread) - 唤醒处于阻塞状态的指定线程
    我们会发现底层都是调用sun.misc.Unsafe
    调用unpark(thread)方法后,就会将thread线程的许可permit设置成1注意多次调用unpark方法,不会累加,pemit值还是1)会自动唤醒thead线程,即之前阻塞中的LockSupport.park()方法会立即返回。

    4. 代码演示

    public class JUC {
    
        public static void main(String[] args) {
    
            Thread a = new Thread(()->{
                System.out.println(Thread.currentThread().getName() + "进来");
                LockSupport.park();
                System.out.println(Thread.currentThread().getName() + " 被换醒了");
            }, "A");
            a.start();
    
            Thread b = new Thread(()->{
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                LockSupport.unpark(a);
                System.out.println(Thread.currentThread().getName()+"唤醒传入的线程");
            }, "B");
            b.start();
    
        }
    }
    

    5. 结果展示

    在这里插入图片描述

    6. 修改代码

    try {
    	TimeUnit.SECONDS.sleep(3);
    } catch (InterruptedException e) {
    	e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() + "进来" + System.currentTimeMillis());
    LockSupport.park();
    System.out.println(Thread.currentThread().getName() + " 被换醒了" + System.currentTimeMillis());
    

    在这里插入图片描述

    7. 与前两者比的优点

    park/unpark不需要在同步块或者方法内才能执行,解决了上面两种不在同步块或者方法就报错的情况。

    park/unpark不需要先执行park,在执行unpark,无需在意顺序。解决了上面两种必须有前后顺序的情况。

    8.总结

    LockSupport是用来创建锁和共他同步类的基本线程阻塞原语

    LockSuport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻寨之后也有对应的唤醒方法。归根结底,LockSupport调用的Unsafe中的native代码(C++)。

    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.白话文理解

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

    五、面试题

    为什么可以先唤醒线程后阻塞线程?

    因为unpark获得了一个凭证,之后再调用park方法,此时permit为1,就可以名正言顺的凭证消费,permit为0,故不会阻塞。

    为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?

    因为凭证的数量最多为1(不能累加),连续调用两次 unpark和调用一次 unpark效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证,证不够,不能放行。

    六、总结

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


    欢迎大家关注小编的微信公众号!!

    推广自己网站时间到了!!!

    点击访问!欢迎访问,里面也是有很多好的文章哦!

  • 相关阅读:
    定时脚本自动自动将文件push到git
    WPF由文本框输入的内容动态渲染下拉框
    [Linux入门]---gdb调试
    STM32 DMA从存储器发送数据到串口
    项目中根据excel文件生成json多语言文件
    STM32中SPI通信的完整C语言代码范例
    docker项目部署
    LeetCode-剑指44-数字序列中某一位的数字
    附导读 |《网络安全标准实践指南——网络数据安全风险评估实施指引》解读
    SpringBoot实现分页查询——基于SpringBoot和Vue的后台管理系统项目系列博客(七)
  • 原文地址:https://www.cnblogs.com/wang1221/p/16721526.html