1、LockSupport是什么
LockSupport
是java.util.concurrent.locks
包下的一个类。LockSupport
是一个线程阻塞工具类。
官方解释:LockSupport
是用于创建锁和其他同步类的基本线程阻塞原语。可以理解为,是原本的线程等待唤醒机制wait()/notify()
的加强版。LockSupport
下的两个重要方法:
park()
:用于阻塞线程;unpark()
:用于解除阻塞线程;2、使用LockSupport进行线程间通信
之前,我们使用synchronized
关键字时,可以通过wait()/notify()/notifyAll()
实现线程间通信;使用Lock
锁,可以通过Condition
对象的await()/signal()/signalAll()
实现线程间的通信。
现在,突然冒出来一个LockSupprot
,也是实现线程间通信,那么为什么要用LockSuppot
呢?它是线程等待唤醒机制wait()/notify()
的改良版,功能更强大。2.1 三种线程等待和唤醒的方法(线程间通信方法)
Object
中的wait
、notify
、notifyAll
方法(要在synchronized
方法或者sunchronized
代码块中使用)。- JUC 中
Condition
接口提供的await
、signal
、signalAll
方法,一般是配合Lock接口的子类使用。 JUC 中的
LockSupport
提供的park
、unpark
方法。2.2 LockSupport 方法介绍
阻塞线程
void park()
:阻塞当前线程,如果调用unpark
方法或者当前线程被中断,能从park()
方法中返回。void park(Object blocker)
:功能同方法1,入参增加一个Object
对象,用来记录导致线程阻塞的阻塞对象,方便进行问题排查。void parkNanos(long nanos)
:阻塞当前线程,最长不超过nanos纳秒,增加了超时返回的特性。void parkNanos(Object blocker, long nanos)
:功能同方法3,入参增加一个Object
对象,用来记录导致线程阻塞的阻塞对象,方便进行问题排查。void parkUntil(long deadline)
:阻塞当前线程,直到 deadline。void parkUntil(Object blocker, long deadline)
:功能同方法5,入参增加一个Object
对象,用来记录导致线程阻塞的阻塞对象,方便进行问题排查。
唤醒线程
void unpark(Thread thread)
:唤醒处于阻塞状态的指定线程。
2.3 LockSupprot细节说明
LockSupport
的设计思路是通过许可证来实现的,就像汽车上高速公路,入口处要获取通行卡,出口处要交出通行卡,如果没有通行卡你就无法出站。LockSupport
是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。在LockSupport
的底层,是调用Unsafe
类中的 native 代码来实现的。
park()
和unpark()
方法阻塞线程和解除线程阻塞的过程:
LockSupprot
和每个使用它的线程都有一个 permit(许可)关联。permit 只有两种值,1和0,相当于一个开关,默认是0,为0时就是无许可,处于阻塞状态。- 调用一次
unpark()
方法,permit 就变成1。 - 调用一次
park()
会消费 permit,也就是将 permit 从1变成0,同时park()
立即返回。 - 如果再次调用
park()
,线程会变成阻塞,这时调用unpark()
会把 permit 置为1。 - 每个线程都有一个相关的 permit,且 permit 只有一个,重复调用
unpark()
也不会积累凭证(这也说明,LockSupport
是不可重入的,如果一个线程连续调用LockSupprot.park()
两次,那么线程一定会一直阻塞下去)。
形象些理解:
线程阻塞需要消耗凭证,这个凭证最多只有一个。
调用park()
方法时:
- 如果有凭证,则会直接消耗掉这个凭证然后正常退出;
- 如果无凭证,就必须阻塞等待凭证可用;
而unpark()
则相反,它会增加一个凭证,但是凭证最多只有一个,不会累加。
这样就可以解释如下两个问题:
为什么
LockSupprot
可以先唤醒线程,后阻塞线程?
- 因为
unpark()
获得了一个凭证,之后再调用park()
方法,可以直接消耗掉这个凭证,所以可以先唤醒。为什么LockSupprot唤醒两次后,阻塞两次,最终结果还是会阻塞线程?
- 因为凭证最多为1,不可累加。唤醒两次permit都只是1,两次
park()
需要两次凭证。
最后再补充下park
注意点,因park
阻塞的线程不仅仅会被unpark
唤醒,还可能会被线程中断(Thread.interrupt
)唤醒,而且不会抛出InterruptedException
异常,所以建议在park
后自行判断线程中断状态,来做对应的业务处理。
使用示例
1. 普通示例
先看一个先park()
进行阻塞,然后unpark()
进行唤醒的案例:
public class LockSupportDemo {
public static void main(String[] args) {
Thread threadA = new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"\t"+"---come in");
LockSupport.park(); // 被阻塞,等待通知等待放行,需要通过permit
System.out.println(Thread.currentThread().getName()+"\t"+"---被唤醒");
}, "A");
threadA.start();
// 暂停线程几秒钟
try {
TimeUnit.SECONDS.sleep(3L);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread threadB = new Thread(() -> {
LockSupport.unpark(threadA);
System.out.println(Thread.currentThread().getName()+"\t"+"---通知了");
}, "B");
threadB.start();
}
}
运行结果:
A ---come in
B ---通知了
A ---被唤醒
更改一下这个案例,先通知,再唤醒:
public class LockSupportDemo {
public static void main(String[] args) {
Thread threadA = new Thread(() -> {
// 暂停线程几秒钟,实现让B线程先进行通知
try {
TimeUnit.SECONDS.sleep(3L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t"+"---come in"+System.currentTimeMillis());
LockSupport.park(); // 被阻塞,等待通知等待放行,需要通过permit
System.out.println(Thread.currentThread().getName()+"\t"+"---被唤醒"+System.currentTimeMillis());
}, "A");
threadA.start();
Thread threadB = new Thread(() -> {
LockSupport.unpark(threadA);
System.out.println(Thread.currentThread().getName()+"\t"+"---通知了");
}, "B");
threadB.start();
}
}
运行结果:
B ---通知了
A ---come in1653205417579
A ---被唤醒1653205417579
可以看到,先通知,再阻塞,线程A还是被唤醒了。同时,从打印出的毫秒数可以看出,线程A中的LockSupport.park()
根本没有起作用。 (这是:提前发放通行证)
2. 多线程协同
使用LockSupport
来完成一道阿里经典的多线程协同工作面试题。
有3个独立的线程,一个只会输出A,一个只会输出B,一个只会输出C,在三个线程启动的情况下,请用合理的方式让他们按顺序打印ABCABC。
思路如下:
- 准备3个线程,分别固定打印A、B、C。
- 线程输出完A、B、C后需要阻塞等待唤醒。
额外准备第4个线程,作为另外3个线程的调度器,有序的控制3个线程执行。 ```java 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 ``` 4种方法实现多线程按着指定顺序执行