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. 执行结果

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

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

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

4. 修改代码

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

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

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. 执行结果

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

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

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

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. 结果展示

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

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());

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

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也不会积累凭证

JUC在深入面试题——三种方式实现线程等待和唤醒(wait/notify,await/signal,LockSupport的park/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却需要消费两个凭证,证不够,不能放行。

六、总结

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

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

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

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

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

Original: https://www.cnblogs.com/wang1221/p/16721526.html
Author: 小王写博客
Title: JUC在深入面试题——三种方式实现线程等待和唤醒(wait/notify,await/signal,LockSupport的park/unpark)

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/616053/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 2021总结、2022展望

    2021总结 2021总结 参与开源 Skywalking Incubator-Shenyu 学习内容 2022展望 深入学习Golang及相关技术栈 参与公司Oteam 组织后端…

    Java 2023年6月5日
    078
  • Https Webservice接口的免证书调用

    在调用https协议的Webservice接口时,如果没有做证书验证,一般会报 javax.net.ssl.SSLHandshakeException错误,如果是Was中间件,更会…

    Java 2023年6月13日
    0118
  • IBM MQ Explorer 示例操作

    此示例为双向传输 建立队列管理器 建立【test01】【test02】两个队列管理器,一直下一步即可,端口号不能一致(需要记住设置的端口号,后面会用到) 【test01】端口号 1…

    Java 2023年5月29日
    086
  • 浅析js前端如何将word文档转为html:docx是怎样存储图片的、Java使用POI如何实现、前端使用Mammoth.js如何实现

    先聊聊需求背景:公司运营需求,说在富文本编辑器中发布包含图片的 Word 文档时,图片和文本内容不能一起复制,每次她们都得分开处理,对于包含较多图片的 Word 时,她们处理起来很…

    Java 2023年5月29日
    085
  • SpringBoot如何实现定时任务

    写在前面 SpringBoot创建定时任务的方式很简单,主要有两种方式:一、基于注解的方式(@Scheduled)二、数据库动态配置。实际开发中,第一种需要在代码中写死表达式,如果…

    Java 2023年6月16日
    095
  • JavaWeb中过滤器Filter的url-pattern设置

    源码 https://github.com/YouXianMing/Java-Web-Study/tree/master/Servlet-Filter-UrlPattern Fil…

    Java 2023年5月29日
    086
  • Java开发学习(六)—-DI依赖注入之setter及构造器注入解析

    一、DI依赖注入 首先来介绍下Spring中有哪些注入方式? 我们先来思考 向一个类中传递数据的方式有几种? 普通方法(set方法) 构造方法 依赖注入描述了在容器中建立bean与…

    Java 2023年5月29日
    0117
  • 快速上手Spring项目

    注 : spring 需要导入commons-logging进行日志记录 . 我们利用maven , 他会自动下载对应的依赖项 . <dependency> <g…

    Java 2023年6月15日
    086
  • 排序总结 O_o

    常见的排序算法对比:时间复杂度上:插入类没有,选择类的 堆排、交换类的 快排、独一档:归并稳定性:归并排序独一档,又快又稳定,剩下的都是慢的(直接插入、冒泡) 参考文章 插入类 【…

    Java 2023年6月7日
    0125
  • Java学习1——计算机基础知识

    计算机组成 计算机是由硬件系统和软件系统组成的 Windows常用快捷键 Ctrl+C——复制 Ctrl+V——粘贴 Ctrl+A——全选 Ctrl+S——保存 Ctrl+X——剪…

    Java 2023年6月5日
    077
  • H3CNE学习笔记

    1.IP的主要作用 2.IP网络的结构 3.路由器和交换机的选择 路由器通常用来连接多个网段(路由器的一个接口相当于一个网段) 交换机通常用来连接多个PC机形成一个网段 4.sou…

    Java 2023年6月6日
    090
  • Java字节码技术 static、final、volatile、synchronized关键字的字节码体现 转

    出处: static、final、volatile关键字 static:static修饰的变量被所有类实例共享,静态变量在其所在类被加载时进行初始化,静态方法中不能引用非静态变量或…

    Java 2023年5月29日
    088
  • Java 单例模式 饿汉式与懒汉式

    posted @2022-03-27 11:34 紫薇哥哥 阅读(5 ) 评论() 编辑 Original: https://www.cnblogs.com/ziweigege/p…

    Java 2023年6月5日
    098
  • Skywalking-05:在Skywalking RocketBot上添加监控图表

    在 Skywalking RocketBot 上添加监控图表 效果图 该图的一些配置信息如下: 标题为: JVM Thread State Count (Java Service)…

    Java 2023年6月5日
    085
  • MyBatis 获取参数值的两种方式

    MyBatis获取参数值的两种方式:${}和#{}${}的本质就是字符串拼接,#{}的本质就是占位符赋值${}使用字符串拼接的方式拼接sql,若为字符串类型或日期类型的字段进行赋值…

    Java 2023年6月7日
    077
  • SpringBoot文件上传

    java;gutter:true; Title</p> <pre><code> ;gutter:true;@RequestMapping(val…

    Java 2023年5月30日
    090
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球