一、前言

在多线程的场景下,我们会经常使用加锁,来保证线程安全。如果锁用的不好,就会陷入死锁,我们以前可以使用Objectwait/notify来解决死锁问题。也可以使用Conditionawait/signal来解决,当然最优还是LockSupportpark/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.白话文理解

线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。

当调用park方法时

如果有凭证,则会直接消耗掉这个凭证然后正常退出。

如果无凭证,就必须阻塞等待凭证可用。

而unpark则相反,它会增加一个凭证,但凭证最多只能有1个,累加无放。

五、面试题

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

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

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

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

六、总结

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


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

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

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

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

  1. 多线程,线程类三种方式,线程调度,线程同步,死锁,线程间的通信,阻塞队列,wait和sleep区别?

    重难点梳理 知识点梳理 学习目标 1.能够知道什么是进程什么是线程(进程和线程的概述,多进程和多线程的意义) 2.能够掌握线程常见API的使用 3.能够理解什么是线程安全问题 4.能够知道什么是锁 5 ...

  2. 三种方式上传文件-Java

    前言:负责,因为该项目他(jetty嵌入式开始SpringMvc)实现文件上传的必要性,并拥有java文件上传这一块还没有被曝光.并 Http 更多晦涩协议.因此,这种渐进的方式来学习和实践上载文件的 ...

  3. Android 接入 OpenCV库的三种方式

           OpenCV是一个基于BSD许可(开源)发行的跨平台计算机视觉库,可以运行在Linux.Windows.Android和Mac OS操作系统上.它轻量级而且高效——由一系列 C 函数和少 ...

  4. python 全栈开发,Day94(Promise,箭头函数,Django REST framework,生成json数据三种方式,serializers,Postman使用,外部python脚本调用django)

    昨日内容回顾 1. 内容回顾 1. VueX VueX分三部分 1. state 2. mutations 3. actions 存放数据 修改数据的唯一方式 异步操作 修改state中数据的步骤: ...

  5. 架构设计哲学【三种方式:支持DevOps的原则】

    三种方式:支持DevOps的原则 2012年8月22日作者Gene Kim 45条评论 这篇文章是杨波老师分享的一篇文章:这几年对他架构影响最深的一篇文章.主要描述是关于DevOps的,但对系统架构同 ...

  6. 监视EntityFramework中的sql流转你需要知道的三种方式Log,SqlServerProfile, EFProfile

    大家在学习entityframework的时候,都知道那linq写的叫一个爽,再也不用区分不同RDMS的sql版本差异了,但是呢,高效率带来了差灵活性,我们 无法控制sql的生成策略,所以必须不要让自 ...

  7. iOS字体加载三种方式

    静态加载 动态加载 动态下载苹果提供的多种字体 其他 打印出当前所有可用的字体 检查某字体是否已经下载 这是一篇很简短的文章,介绍了 iOS 自定义字体加载的三种方式. 静态加载 这个可以说是最简单最 ...

  8. 0036 Java学习笔记-多线程-创建线程的三种方式

    创建线程 创建线程的三种方式: 继承java.lang.Thread 实现java.lang.Runnable接口 实现java.util.concurrent.Callable接口 所有的线程对象都 ...

  9. 【整理】Linux下中文检索引擎coreseek4安装,以及PHP使用sphinx的三种方式(sphinxapi,sphinx的php扩展,SphinxSe作为mysql存储引擎)

          一,软件准备 coreseek4.1 (包含coreseek测试版和mmseg最新版本,以及测试数据包[内置中文分词与搜索.单字切分.mysql数据源.python数据源.RT实时索引等测 ...

随机推荐

  1. 基于 Github Actions 自动部署 Hexo 博客

    前言 前不久使用了 Hexo 搭建独立博客,我是部署在我的腾讯云轻量应用服务器上的,每次都需要 hexo deploy 然后打包.上传.解压和刷新 CDN,非常麻烦.我的服务器配置也不高 2C2G 无 ...

  2. MyBatis 映射文件

    Mybatis映射文件简介 1) MyBatis 的真正强大在于它的映射语句.由于它的异常强大,映射器的 XML 文件就显得相对简单.如果拿它跟具有相同功能的 JDBC 代码进行对比,你会立即发现省掉 ...

  3. 2018 CSP-J 初赛解析

    做题记录与答案 今天这个做的是真的烂,60分,妙极了(微笑 可以看看人家的解析 选择: 选择好多不太懂的,一个个的来解析 先分析一下选择的知识点: 计算机基础 :T1.T3.T4.T5.T8 进制转换 ...

  4. 关于Thymeleaf无法取值问题

    SpringBoot2.7以前的版本在获取model中数据的时候不需要注释,2.7以后的版本需要加注释,它无法直接取存在model中的数据,不加注释的时候会爆红但是可以正常使用,这个注释的含义就是指定 ...

  5. day05 Java_循环_基本类型数组

    精华笔记: 循环结构: for结构:应用率高.与次数相关的循环 三种循环结构的选择规则: 先看循环是否与次数相关: 若相关----------------------------直接上for 若无关, ...

  6. K阶斐波那契数列--------西工大NOJ习题.10

    K阶斐波那契数列--------西工大NOJ习题.10 原创不易,转载请说明出处!!! 科普:k阶斐波那契数列的0到n-1项需要有初始值. 其中,0到n-2项初始化为0,第n-1项初始化为1. 在这道 ...

  7. 自建docker仓库

    一.仓库安装 1.系统:CentOS7.9,采用yum安装方式 [root@master ~]# yum install docker-distribution -y ... ... [root@ma ...

  8. 2505-springboot使用spring.profiles.active来分区配置

    参考文献: spring boot 入门 使用spring.profiles.active来分区配置 http://www.leftso.com/blog/111.html 很多时候,我们项目在开发环 ...

  9. 1个小时!从零制作一个! AI图片识别WEB应用!

    0 前言 近些年来,所谓的人工智能也就是AI. 在媒体的炒作下,变得神乎其神,但实际上,类似于图片识别的AI,其原理只不过是数学的应用. 线性代数,概率论,微积分(著名的反向传播算法). 大家觉得这些 ...

  10. 自动挂载mount

    # 自动挂载mount(/etc/fstab) /dev/fd0 /media/floppy0 auto rw,user,noauto,exec,utf8 0 0 # 第一列:/dev/fd0 挂载源 ...