一、前言
前文介绍了 CompletableFuture 和 线程池的几种对线程的管理方式后,本质上,通过这些工具,可以直接帮我们对线程进行很好的管理和运作,什么时间需要启动哪个线程,以及线程的执行顺序等。毕竟,线程间的通信技术是非常有价值的,如果仅仅是孤立的运行各个线程不产生任何协作,那多线程也就失去了更多利用价值。本文再一种 JDK 自带的线程等待唤醒机制--LockSupport,通过它的使用,可以学习更优雅的线程间通信技术,同时为后续的 AQS 的学习做好准备。
二、概述
2.1 LockSupport 介绍
LockSupport 是 java.util.concurrent.locks 包下的一个工具类。
LockSupport 可以类比于一个列车检票员的角色,每个进入的线程都得亮出自己的 “车票” ,如果有车票,那列车员就会在你的车票上剪出一个口子,这样你的车票就相当于销毁了,无法用这个票上其他的车,如果没有车票就拒之门外,无法上车,如果你想买票,它也可以让你买一张车票。
分析它的特点:
- 1、如果有 “车票” ,就放你通过,同时你的 车票会销毁。
- 2、如果没有车票,则阻塞,无法通信。
- 3、车票最多只有一张。
LockSupport 是用于创建锁和其他同步类的基本线程的阻塞原语。
LockSupport 不支持构造(private 修饰),主要的作用是阻塞和唤醒线程,提供了一堆 static 方法,例如 park 和 unpark,用于停车和发车的动作。park 消耗一个车票,unpark 新增一个车票,车票最多只有一个。
主要有如上的静态方法,常用的 park 和 unpark 需要承成对使用,类比于 Object 的 wait 和 notify 为一对。
permit:
LockSupport 的设计思路是为每个线程设置一个 permit,其实就是一个值。
这个值默认就是 0,有0和1两种值,0代表许可证不可用;1 代表许可证可用。
- 调用 park 方法进行减一 扣除许可证,调用 unPark 方法进行加一,将许可证置为可用。
- 当为 0 时,无论 park 多少次,都只能为0;
- 当为 1 时,无论 unpark 多少次,最大只能为 1。
2.2 入门使用
@Test
public void testLockSupport() {
Thread parkThread = new Thread(() -> {
System.out.println("开始线程阻塞");
LockSupport.park();
System.out.println("结束线程阻塞");
});
parkThread.start();
System.out.println("开始线程唤醒");
LockSupport.unpark(parkThread);
System.out.println("结束线程唤醒");
}
复制代码
这串代码最终 parkThread 会在 unpark 线程后才会结束线程阻塞,这也说明了 LockSupport 可以对线程进行阻塞和唤醒的操作,阻塞时可以传入具体线程进行唤醒。
三、三种线程间通信
介绍三种对线程等待和唤醒的方法。
- 1、使用 Object 种的 wait()方法让线程等待,使用 Object种的 notify()方法唤醒线程;
- 2、使用 JUC 包种 Condition 的 await()方法让线程等待,使用 signal()方法唤醒线程;
- 3、LockSupport 类可以阻塞当前线程以及唤醒指定被阻塞的线程。
3.1 Object 的wait 和 notify 方法实现线程等待和唤醒
使用该方法有两个问题:
- 1)要使用 Object 的wait 和 notify,必须要用个对象锁,才能使用
- 2)先 wait 后 notify 才 OK
3.1.1 无对象锁
public class ThreadCommunication {
public static void main(String[] args) {
Resource resource = new Resource();
new Thread(()->{
for (int i = 0; i < 5; i++) {
resource.decr();
}
},"减一线程").start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
resource.incr();
}
},"加一线程").start();
}
}
复制代码
/**
* 锁唤醒机制实现
*/
// 资源类
class Resource{
// 加减对象
private int number = 0;
// 加法
public void incr(){
try {
while (number!=0){
this.wait();
}
number++;
System.out.println("------- " + Thread.currentThread().getName() + "加一成功------------ ,值为:" + number);
notify();
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public void decr(){
try {
while (number==0){
this.wait();
}
number--;
System.out.println("------- " + Thread.currentThread().getName() + "减一成功------------ ,值为:" + number);
notify();
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
复制代码
两个方法上没有对象锁。
3.1.2 先 notify 后 wait
* 3 将notify放在wait方法前先执行,t1先notify了,3秒钟后t2线程再执行wait方法
* 3.1 程序一直无法结束
* 3.2 结论
* 先wait后notify、notifyall方法,等待中的线程才会被唤醒,否则无法唤醒
*/
public class LockSupportDemo
{
public static void main(String[] args)//main方法,主线程一切程序入口
{
Object objectLock = new Object(); //同一把锁,类似资源类
new Thread(() -> {
synchronized (objectLock) {
objectLock.notify();
}
System.out.println(Thread.currentThread().getName()+"\t"+"通知了");
},"t1").start();
//t1先notify了,3秒钟后t2线程再执行wait方法
try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
synchronized (objectLock) {
try {
objectLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"\t"+"被唤醒了");
},"t2").start();
}
}
复制代码
先wait后notify、notifyall方法,等待中的线程才会被唤醒,否则无法唤醒.
3.2 Condition 接口的 await 和 signal 方法实现线程的等待和唤醒
3.2.1 正常使用
/**
* 使用 Condition 来实现线程通信
* 此时,完成案例:三个线程依次打印 A、B、C
* @Author xiaolei
* @Date 2022/12/9 13:30
**/
public class ConditionCommunication {
public static void main(String[] args) {
Resource2 resource2 = new Resource2();
new Thread(()->{
for (int i = 0; i < 10; i++) {
resource2.soutA();
}
},"线程A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
resource2.soutB();
}
},"线程B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
resource2.soutC();
}
},"线程C").start();
}
}
class Resource2{
private int number =0;
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
public void soutA(){
lock.lock();
try {
while (number!=0){
condition.await();
}
System.out.println(Thread.currentThread().getName()+"输出A");
System.out.println("A");
number=1;
// signal 是精准唤醒线程,而 notify 是随机唤醒
condition2.signal();
}catch (Exception e){
}finally {
lock.unlock();
}
}
public void soutB(){
lock.lock();
try {
while (number!=1){
condition2.await();
}
System.out.println(Thread.currentThread().getName()+"输出B");
System.out.println("B");
number=2;
condition3.signal();
}catch (Exception e){
}finally {
lock.unlock();
}
}
public void soutC(){
lock.lock();
try {
while (number!=2){
condition3.await();
}
System.out.println(Thread.currentThread().getName()+"输出C");
System.out.println("C");
number=0;
condition.signal();
}catch (Exception e){
}finally {
lock.unlock();
}
}
}
复制代码
3.2.2 异常两种情况
1)去掉 Lock 和 unLock 后,会触发同样的都触发了IllegalMonitorStateException异常 。使用这个必须配合 Lock 和 unLock 使用
2)如果先唤醒后阻塞,那么程序会一直阻塞,注意执行顺序。
3.3 LockSupport 的 park 和 unPark
按顺序打印 A、B、C 三个数。
public class LockSupportCommDemoV2 {
public static void main(String[] args) {
Thread t1 = new Thread(new Resource4("A"));
Thread t2 = new Thread(new Resource4("B"));
Thread t3 = new Thread(new Resource4("C"));
t1.start();
t2.start();
t3.start();
// 发车
LockSupport.unpark(t1);
for (int i = 0; i < 12; i++) {
if(i%3==0){
LockSupport.unpark(t1);
}else if(i%3 ==1){
LockSupport.unpark(t2);
}else{
LockSupport.unpark(t3);
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
}
}
class Resource4 implements Runnable{
private String number;
public Resource4(String number){
this.number = number;
}
@Override
public void run() {
while (true){
System.out.println(this.number);
LockSupport.park(this);
}
}
}
复制代码
LockSupport 是 JDK 中用来实现线程阻塞和唤醒的工具,使用它可用在任何场合使线程阻塞,可用指定任何线程进行唤醒,并且不用担心阻塞和唤醒操作的顺序,连续多次唤醒和一次唤醒的效果是一样的。
与其他的区别:
- 可用先唤醒后等待
- 简洁优雅使用,无需结束 对象锁 或 lock\unLock 实现。
- 三种实现机制不同,unpark 无法对 wait 进行唤醒,notify 也无法对 park 进行唤醒。
四、源码分析
LockSupport 和 CAS 是 Java 并发包中很多并发工具控制机制的基础,它们的底层都是依赖 UnSafe 实现的(后续章节再介绍)。
这个类与使用它的每个线程关联一个许可证(类似 Semaphore 信号量),如果许可证可用,就停车呼叫将立即返回,并在过程中使用掉该许可证;否则它就阻塞。调用 unPark 将使许可证可用(许可证最多一个)。
该方法被设计用于创建更高级别的同步实用程序工具,而这些方法本身对大多数并发控制应用程序并不有用,park 方法仅适用于以下结构。
while (!canProceed()) {
...
LockSupport.park(this);
}
复制代码
即我们最好按照这种格式来写我们的 LockSupport 代码,通过 while 条件 判断共享资源是否满足条件,然后再通过 是否进行 park 消费,因为一旦执行这句话,它就会立马返回结果。
4.1 park 方法
Disables the current thread for thread scheduling purposes unless the permit is available.(除非我能获取许可证,否则我就禁用该线程)
If the permit is available then it is consumed and the call returns immediately; (如果给我的许可证可用,我就消费它,然后立即返回)
otherwise the current thread becomes disabled for thread scheduling purposes and lies dormant until one of three things happens:(否则当前线程就会一直阻塞直到下面三种情况)
- Some other thread invokes unpark with the current thread as the target; or(其他线程以当前线程为目标调用 unPark)
- Some other thread interrupts the current thread; or (其他线程中断当前线程)
- The call spuriously (that is, for no reason) returns.(或错误返回,即没有原因的)
// 阻塞当前线程,并把当前线程的 parkBlocker 字段设置为 blocker
public static void park(Object blocker) {
// 获取当前线程
Thread t = Thread.currentThread();
//将当前线程的parkBlocker字段设置为blocker
setBlocker(t, blocker);
//阻塞当前线程,第一个参数表示isAbsolute,是否为绝对时间,第二个参数就是代表时间
UNSAFE.park(false, 0L);
//重新可运行后再此设置Blocker
setBlocker(t, null);
}
复制代码
调用 park 方法时,首先获取当前线程,然后设置当前线程的 parkBlocker 字段,即调用 setBlocker 函数,然后才调用 UNSAFE 的park 方法,后又调用 setBlocker ,为什么调用两次呢?
//设置线程t的parkBlocker字段的值为arg
private static void setBlocker(Thread t, Object arg) {
// Even though volatile, hotspot doesn't need a write barrier here.
//尽管hotspot易变,但在这里并不需要写屏障。
UNSAFE.putObject(t, parkBlockerOffset, arg);
}
复制代码
因为 在执行到 UNSAFE.park 的时候,这个线程已经阻塞到这里了,我告知其他线程,这个线程已经阻塞了,然后再进行 park,当其他线程调用 unpark 时,就执行第二个 setBlocker,然后它就知道,我解放了,然后将这个线程的 blocker 设置为 null。
4.2 unpark 方法
将指定的线程许可置为可用,也就相当于唤醒了阻塞对象。
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
复制代码
五、小结
LockSupport 本身的方法非常少,作为一个工具类使用,提供接口间接操作 Unsafe 类,源码也不多,对比之前学的线程通信方式更为优雅,另外,多了解一种线程通信方法,然后对后续的深入学习来说,这个类还是非常有必要学习了解的。