JUC在深入面试题——三种方式实现线程等待和唤醒(wait/notify,await/signal,LockSupport的park/unpark)
一、前言
在多线程的场景下,我们会经常使用加锁,来保证线程安全。如果锁用的不好,就会陷入死锁,我们以前可以使用Object
的wait/notify
来解决死锁问题。也可以使用Condition
的await/signal
来解决,当然最优还是LockSupport
的park/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却需要消费两个凭证,证不够,不能放行。
六、总结
看到这里的小伙伴,点个赞不过分吧,小编也是整理了一下午,参考阳哥课件。
欢迎大家关注小编的微信公众号!!
推广自己网站时间到了!!!
JUC在深入面试题——三种方式实现线程等待和唤醒(wait/notify,await/signal,LockSupport的park/unpark)的更多相关文章
- 多线程,线程类三种方式,线程调度,线程同步,死锁,线程间的通信,阻塞队列,wait和sleep区别?
重难点梳理 知识点梳理 学习目标 1.能够知道什么是进程什么是线程(进程和线程的概述,多进程和多线程的意义) 2.能够掌握线程常见API的使用 3.能够理解什么是线程安全问题 4.能够知道什么是锁 5 ...
- 三种方式上传文件-Java
前言:负责,因为该项目他(jetty嵌入式开始SpringMvc)实现文件上传的必要性,并拥有java文件上传这一块还没有被曝光.并 Http 更多晦涩协议.因此,这种渐进的方式来学习和实践上载文件的 ...
- Android 接入 OpenCV库的三种方式
OpenCV是一个基于BSD许可(开源)发行的跨平台计算机视觉库,可以运行在Linux.Windows.Android和Mac OS操作系统上.它轻量级而且高效——由一系列 C 函数和少 ...
- python 全栈开发,Day94(Promise,箭头函数,Django REST framework,生成json数据三种方式,serializers,Postman使用,外部python脚本调用django)
昨日内容回顾 1. 内容回顾 1. VueX VueX分三部分 1. state 2. mutations 3. actions 存放数据 修改数据的唯一方式 异步操作 修改state中数据的步骤: ...
- 架构设计哲学【三种方式:支持DevOps的原则】
三种方式:支持DevOps的原则 2012年8月22日作者Gene Kim 45条评论 这篇文章是杨波老师分享的一篇文章:这几年对他架构影响最深的一篇文章.主要描述是关于DevOps的,但对系统架构同 ...
- 监视EntityFramework中的sql流转你需要知道的三种方式Log,SqlServerProfile, EFProfile
大家在学习entityframework的时候,都知道那linq写的叫一个爽,再也不用区分不同RDMS的sql版本差异了,但是呢,高效率带来了差灵活性,我们 无法控制sql的生成策略,所以必须不要让自 ...
- iOS字体加载三种方式
静态加载 动态加载 动态下载苹果提供的多种字体 其他 打印出当前所有可用的字体 检查某字体是否已经下载 这是一篇很简短的文章,介绍了 iOS 自定义字体加载的三种方式. 静态加载 这个可以说是最简单最 ...
- 0036 Java学习笔记-多线程-创建线程的三种方式
创建线程 创建线程的三种方式: 继承java.lang.Thread 实现java.lang.Runnable接口 实现java.util.concurrent.Callable接口 所有的线程对象都 ...
- 【整理】Linux下中文检索引擎coreseek4安装,以及PHP使用sphinx的三种方式(sphinxapi,sphinx的php扩展,SphinxSe作为mysql存储引擎)
一,软件准备 coreseek4.1 (包含coreseek测试版和mmseg最新版本,以及测试数据包[内置中文分词与搜索.单字切分.mysql数据源.python数据源.RT实时索引等测 ...
随机推荐
- SuperSocket 1.6 创建一个简易的报文长度在头部的Socket服务器
我们来做一个头为6位报文总长度,并且长度不包含长度域自身的例子.比如这样的Socket报文000006123456. 添加SuperSocket.Engine,直接使用Nuget搜索SuperSock ...
- 高精度10m/30米NPP净初级生产力分布数据
数据下载链接:百度云下载链接 引言 第一性生产力是绿色植物呼吸后所剩下的单位面积单位时间内所固定的能量或所生产的有机物质,即是总第一性生产量减去植物呼吸作用所剩下的能量或有机物质.多种卫星遥感数据反 ...
- Oracle归档日志暴增排查优化
1.ORACLE归档日志介绍 归档日志暴增是oracle比较常见的问题,遇到归档日志暴增,我们该如何排查: 归档日志暴增一般都是应用或者人为引起的 理解归档日志存储的是什么 如何排查归档日志暴增原因 ...
- ESP分区重建,解决各种引导问题
电脑装了双系统,win7和win10,每次重启都进入不同系统,郁闷至极,索性把不常用的Win7盘格式化,但依旧解决不了问题.所以有了以下方法. 1.进PE删除ESP分区(先备份). 2.新建ESP分区 ...
- day02 IO
JAVA IO java io可以让我们用标准的读写操作来完成对不同设备的读写数据工作. java将IO按照方向划分为输入与输出,参照点是我们写的程序. 输入:用来读取数据的,是从外界到程序的方向,用 ...
- Python 封装SNMP调用接口
PySNMP 是一个纯粹用Python实现的SNMP,用PySNMP的最抽象的API为One-line Applications,其中有两类API:同步的和非同步的,都在模块pysnmp.entity ...
- Clang-format-12安装
ubu18 clang-format安装 1.更新源 wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - ...
- P1980 计数问题 - 记录
P1980 计数问题 题目描述 试计算在区间 1 到 n的所有整数中,数字x(0 ≤ x ≤ 9)共出现了多少次?例如,在 1到11中,即在 1,2,3,4,5,6,7,8,9,10,11中,数字1出 ...
- Luogu3871 [TJOI2010]中位数 (平衡树)
"#define int long long" 导致100pts \(\rightarrow\) 80pts #include <iostream> #include ...
- Redis 14 发布订阅
参考源 https://www.bilibili.com/video/BV1S54y1R7SB?spm_id_from=333.999.0.0 版本 本文章基于 Redis 6.2.6 概述 Redi ...