写给小白看的LockSupport
前言
Java并发编程系列第三篇LockSupport
,上一篇Synchronized
文章中有提过,不推荐读者们使用Object
的wait、notify、notifyAll
等函数做多线程间的通信协同,使用LockSupport
会是更好的选择,本篇就来谈谈LockSupport
,也正好为下篇的A Q S(AbstractQueuedSynchronized)
打基础。
内容大纲
LockSupport基本概念
LockSupport
是线程工具类,主要作用是阻塞和唤醒线程,底层实现依赖Unsafe
,同时它还是锁和其他同步类实现的基础,LockSupport
提供两类静态函数分别是park
和unpark
,即阻塞与唤醒线程,下面是两段代码示例
示例-1
public static void main(String[] agrs) throws InterruptedException {
Thread th = new Thread(() -> {
//阻塞当前线程
LockSupport.park();
System.out.println("子线程执行---------");
});
th.start();
//睡眠2秒
Thread.sleep(2000);
System.out.println("主线程执行---------");
//唤醒线程
LockSupport.unpark(th);
}
}
输出结果:
主线程执行---------
子线程执行---------
上述示例中,子线程th
调用LockSupport.park()
阻塞,主线程睡眠2
秒后,执行LockSupport.unpark(th)
唤醒th
线程,先阻塞后唤醒非常好理解,接下来读者们再看下面的示例
示例-2
public static void main(String[] agrs) throws InterruptedException {
Thread th = new Thread(() -> {
//唤醒当前线程
LockSupport.unpark(Thread.currentThread());
//阻塞当前线程
LockSupport.park();
System.out.println("子线程执行---------");
});
th.start();
//睡眠2秒
Thread.sleep(2000);
System.out.println("主线程执行---------");
}
输出结果:
子线程执行---------
主线程执行---------
嗯?先唤醒th
线程,再阻塞th
线程,最终th
线程没有被阻塞,这是为什么?下面LockSupport
的设计思路会为读者们解开疑惑,并更进一步明确是park
和unpark
的语义(从广义上来说park
和unpark
代表阻塞和唤醒)。
设计思路
LockSupport
的设计思路是通过许可证来实现的,就像汽车上高速公路,入口处要获取通行卡,出口处要交出通行卡,如果没有通行卡你就无法出站,当然你可以选择补一张通行卡。
LockSupport
会为使用它的线程关联一个许可证(permit
)状态,permit
的语义「是否拥有许可」,0
代表否,1
代表是,默认是0
。
LockSupport.unpark
:指定线程关联的permit
直接更新为1
,如果更新前的permit<1
,唤醒指定线程LockSupport.park
:当前线程关联的permit
如果>0
,直接把permit
更新为0
,否则阻塞当前线程
线程 A
执行LockSupport.park
,发现permit
为0
,未持有许可证,阻塞线程A
线程 B
执行LockSupport.unpark
(入参线程A
),为A
线程设置许可证,permit
更新为1
,唤醒线程A
线程 B
流程结束线程 A
被唤醒,发现permit
为1
,消费许可证,permit
更新为0
线程 A
执行临界区线程 A
流程结束
经过上面的分析得出结论unpark
的语义明确为「使线程持有许可证」,park
的语义明确为「消费线程持有的许可」,所以unpark
与park
的执行顺序没有强制要求,只要控制好使用的线程即可,unpark=>park
执行流程如下
permit
默认是0
,线程A
执行LockSupport.unpark
,permit
更新为1
,线程A
持有许可证线程 A
执行LockSupport.park
,此时permit
是1
,消费许可证,permit
更新为0
执行临界区 流程结束
最后再补充下park
注意点,因park
阻塞的线程不仅仅会被unpark
唤醒,还可能会被线程中断(Thread.interrupt
)唤醒,而且不会抛出InterruptedException
异常,所以建议在park
后自行判断线程中断状态,来做对应的业务处理。
优点
为什么推荐使用LockSupport
来做线程的阻塞与唤醒(线程间协同工作),因为它具备如下优点
以线程为操作对象更符合阻塞线程的直观语义 操作更精准,可以准确地唤醒某一个线程( notify
随机唤醒一个线程,notifyAll
唤醒所有等待的线程)无需竞争锁对象(以线程作为操作对象),不会因竞争锁对象产生死锁问题 unpark
与park
没有严格的执行顺序,不会因执行顺序引起死锁问题,比如「Thread.suspend
和Thread.resume
」没按照严格顺序执行,就会产生死锁
另外LockSupport
还提供了park
的重载函数,提升灵活性
void parkNanos(long nanos)
:增加了超时机制void parkUntil(long deadline)
:加入超时机制(指定到某个时间点,1970
年到指定时间点的毫秒数)void park(Object blocker)
:设置blocker
对象,当线程没有许可证被阻塞时,该对象会被记录到该线程的内部,方便后续使用诊断工具进行问题排查void parkNanos(Object blocker, long nanos)
:设置blocker
对象,加入超时机制void parkUntil(Object blocker, long deadline)
:设置blocker
对象,加入超时机制(指定到某个时间点,1970
年到指定时间点的毫秒数)
建议使用时,传入blocker
对象,至于超时根据业务场景选择
实践
使用LockSupport
来完成一道阿里经典的多线程协同工作面试题。
有3
个独立的线程,一个只会输出A
,一个只会输出B
,一个只会输出C
,在三个线程启动的情况下,请用合理的方式让他们按顺序打印ABCABC
。
思路如下
准备 3
个线程,分别固定打印A、B、C
线程输出完 A、B、C
后需要阻塞等待唤醒额外准备第 4
个线程,作为另外3
个线程的调度器,有序的控制3
个线程执行
是不是很简单,下面通过代码来实践
public static void main(String[] agrs) throws InterruptedException {
LockSupportMain lockSupportMain = new LockSupportMain();
//定义线程t1、t2、t3执行的函数方法
Consumer<String> consumer = str -> {
while (true) {
//线程消费许可证,并传入blocker,方便后续排查问题
LockSupport.park(lockSupportMain);
//防止线程是因中断操作唤醒
if (Thread.currentThread().isInterrupted()){
throw new RuntimeException("线程被中断,异常结束");
}
System.out.println(Thread.currentThread().getName() + ":" + str);
}
};
/**
* 定义分别输出A、B、C的线程
*/
Thread t1 = new Thread(() -> {
consumer.accept("A");
},"T1");
Thread t2 = new Thread(() -> {
consumer.accept("B");
},"T2");
Thread t3 = new Thread(() -> {
consumer.accept("C");
},"T3");
/**
* 定义调度线程
*/
Thread dispatch = new Thread(() -> {
int i=0;
try {
while (true) {
if((i%3)==0) {
//线程t1设置许可证,并唤醒线程t1
LockSupport.unpark(t1);
}else if((i%3)==1) {
//线程t2设置许可证,并唤醒线程t2
LockSupport.unpark(t2);
}else {
//线程t3设置许可证,并唤醒线程t3
LockSupport.unpark(t3);
}
i++;
TimeUnit.MILLISECONDS.sleep(500);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
//启动相关线程
t1.start();
t2.start();
t3.start();
dispatch.start();
}
输出内容:
T1:A
T2:B
T3:C
T1:A
T2:B
T3:C
T1:A
T2:B
T3:C
最后再留个题目给读者们思考,使用包含但不限于Synchronized
、ReentrantLock
来完成这个功能
唠叨唠叨
LockSupport
十分简单好用,是作为并发编程的必备基础,阿星觉得是十分有必要掌握的,所以出了这篇文章,后续的计划安排AbstractQueuedSynchronizer、ReentrantLock、ReentrantReadWriteLock
文章,大概两周内出一篇,因为最近公司业务比较忙,所以周更有点困难,但是阿星会尽力做到周更,如果觉得阿星的文章对您有帮助,也请一键三连支持阿星(点赞、再看、转发)
历史好文推荐
13张图,深入理解Synchronized 由浅入深CAS,小白也能与BAT面试官对线 小白也能看懂的Java内存模型 保姆级教学,22张图揭开ThreadLocal 进程、线程与协程傻傻分不清?一文带你吃透! 什么是线程安全?一文带你深入理解
关于我
这里是阿星,一个热爱技术的Java程序猿,公众号 「程序猿阿星」 里将会定期分享操作系统、计算机网络、Java、分布式、数据库等精品原创文章,2021,与您在 Be Better 的路上共同成长!。
非常感谢各位小哥哥小姐姐们能看到这里,原创不易,文章有帮助可以关注、点个赞、分享与评论,都是支持(莫要白嫖)!
愿你我都能奔赴在各自想去的路上,我们下篇文章见
个人二维码
公众号
- END -点击关注公众号,免费领学习资料