java.util.concurrent 包里的源码频繁使用的 LockSupport 是一个线程阻塞的工具类,是用来创建锁和其他同步类的基本线程阻塞原语。如 AQS 的底层实现就用到了 LockSupport.park()方法和 LockSupport.unpark()方法。该类中的所有方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的解锁方法,底层调用了Unsafe中的native方法。主要核心API为 park 和 unpark 方法,分别用来阻塞线程和解除阻塞线程。
1、线程等待和唤醒的方式
1.1 方式
java多线程中,常用的线程等待和唤醒的方式有3种。
- 使用Object类中的 wait 和 notify 方法进行线程的等待和唤醒。
- 使用JUC中的 Conditon 的 await 和 singal 方法进行线程的等待和唤醒。
- 使用 LockSupport 的 park 和 unpark 方法阻塞唤醒当前线程。
1.2 区别
上述三种线程等待唤醒方式的区别如下:
Object的 wait 和 notify
- 必须使用 sycnchronized 关键字包裹
- wait必须要写在synchronized代码块中,否则会报异常。
- 必须由同一个lock对象调用wait、notify方法
- 如果 notify 放在 wait 之前,线程无法被唤醒,必须先 wait 后 notify。
JUC中的 Condition的 await 和 singal
- 必须使用 lock 和 unlock 方法包裹
- 如果先使用 singal 再 await,线程无法被唤醒,必须先 await 后 singal
LockSupport 的 park 和 unpark方法
- LockSupport是用来创建锁和其他同步类的基本线程阻塞原语,提供了一个Permit语序的概念来做到阻塞和唤醒线程功能。每个线程都有一个Permit,Permit是一个0/1的信号量,只用0和1两个值,默认是0。与Semaphore不同的是,许可的累加上限是1。
- 核心API park()/park(Object blocker)、unpark(Thread thread)分别用于阻塞和唤醒线程
- 没有锁代码块的要求
- 先unpark后park也可以正常执行
- Object 的 wait()/notify 方法需要获取到对象锁之后在同步代码块里才能调用。
- 而 LockSupport 不需要获取锁。所以使用 LockSupport 线程间不需要维护一个共享的同步对象,从而实现了线程间的解耦。
- unark 方法可提前 park 方法调用,所以不需要担心线程间执行的先后顺序,而 wait & notify 不能先 notify。
- park 和 unpark 的使用不会出现死锁的情况
- unpark 可以先于 park 执行,就相当于给这个线程一块免死金牌,但是只有一块,就算提前 unpark 了 100 次也只有一块。有了免死金牌之后,第一次 park 这个线程,该线程是不会被 park 住,只是收走了这块免死金牌,下一次park,该线程就进入等待状态了。
- 多次调用 unpark 方法和调用一次 unpark 方法效果一样,因为 unpark 方法是直接将_counter 赋值为 1,而不是加 1。
- 许可不可重入,也就是说只能调用一次 park()方法,如果多次调用 park()线程会一直阻塞。
- park & unpark 是以线程为单位来【阻塞】和 【唤醒】,notify 则是随机唤醒一个等待的线程,notifyAll 是唤醒所有等待线程,不精确。
- 两者的阻塞队列并不交叉。object.notifyAll()不能唤醒LockSupport的阻塞Thread。
- 暂停线程的机制也不一样,wait 是让线程在monitor 的 waitSet 中等待,而LockSupport 是与被park的线程的互斥量进行控制。
2、LockSupport原理介绍
原理:
每个java 线程都有一个Parker实例,parker实例的_counter字段,映射了操作系统底层的互斥量(mutex)。使用它来进行线程的并发控制。
LockSupport和每个使用它的线程都与一个许可(permit)关联。permit相当于1,0的开关,默认是0,调用一次unpark就加1变成1,调用一次park会消费permit, 也就是将1变成0,同时park立即返回。再次调用park会变成block(因为permit为0了,会阻塞在这里,直到permit变为1), 这时调用unpark会把permit置为1。每个线程都有一个相关的permit, permit最多只有一个,重复调用unpark也不会积累。
特点:
- LockSupport是用来创建锁和其他同步类的基本线程阻塞原语LockSupport是一个线程阻塞的工具类
- 所有方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的解锁方法
- 底层调用了Unsafe中的native方法
- 每一个使用它的线程都有一个许可证Permit,调用一次unpark就+1,调用一次park会消费permit,将1变成0.同时park立刻返回,如果再调用一次park会变成阻塞(permit为0会阻塞在这里,一直到permit变成1)
- 每个线程都有相关的permit最多一个,重复调用unpark也不会累积凭证
- 调用park方法,如果有凭证则会消耗掉这个凭证然后正常退出,如果无凭证,就必阻塞等待凭证可用,而unpark相反,他会添加一个凭证,但凭证最多只有1个累加无效
LockSupport为什么可以先唤醒线程后阻塞线程?
因为unpark提供了一个凭证,Permit=1,之后再调用park,Permit变成0,正常的凭证消费则不会阻塞
为什么唤醒两次后阻塞两次,最终结果会阻塞线程
因为Permit最多数量是1,连续调用2次unpark方法,结果Permit的值还是1,调用第一次park方法将Permit改为0,第二次调用 park 时 Permit还是0,故阻塞。
3、LockSupprot 方法介绍
LockSupport 提供 park()和 unpark()方法实现阻塞线程和解除线程阻塞。
阻塞线程的方法:
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 对象,用来记录导致线程阻塞的阻塞对象,方便进行问题排查;
每个 park 方法都对应有一个带有 Object 阻塞对象的重载方法。增加了一个 Object 对象作为参数,此对象在线程受阻塞时被记录,以允许监视工具和诊断工具确定线程受阻塞的原因。
唤醒线程的方法:
void unpark(Thread thread):唤醒处于阻塞状态的指定线程
简单示例:
public class LockSupportDemo {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
LockSupport.park();
System.out.println("thread线程被唤醒");
});
thread.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
LockSupport.unpark(thread);
}
}
该示例中,首先thread线程会被 park 阻塞,主线程在睡眠3秒之后,执行unpark(thread)方法,这时候thread线程 被唤醒,输出"thread线程被唤醒"。
4、LockSupport实战示例
4.1 基本使用示例
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
public class LockSupportDemo {
public static void main(String[] args) {
simpleLockSupport();
}
/**
* lockSupport最基本的使用
*/
private static void simpleLockSupport() {
Thread t1 = new Thread("A") {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 进入 !");
LockSupport.park();
System.out.println(Thread.currentThread().getName() + " 被唤醒 !");
}
};
t1.start();
Thread t2 = new Thread("B") {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 进入 !");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
}
LockSupport.unpark(t1);
System.out.println(Thread.currentThread().getName() + " 唤醒 线程A !");
}
};
t2.start();
}
}
4.2 unpark调用时机
需要注意的是unpark方法如果在线程启动之前调用没有任何作用,在线程启动之后unpark,然后再调用park。可以正常解锁。因为unpark本质把permit改成了1,此时调用park正常消费。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
public class LockSupportDemo {
public static void main(String[] args) {
unparkBeforePark();
}
/**
* unpark必须在 线程启动之后调用 如果线程启动之前调用 unpark没有效果
* unpark在线程启动之后先调用了 再调用park 可以正常解锁,因为unpark本质把permit改成了1 调用park正常消费
*/
private static void unparkBeforePark() {
Thread t1 = new Thread("A") {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " before park !");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
}
LockSupport.park();
System.out.println(Thread.currentThread().getName() + " after park !");
}
};
// 线程启动之前先unpark 没有任何效果 park还是阻塞
// LockSupport.unpark(t1);
System.out.println("start");
t1.start();
// 线程启动之后先调用unpark,线程里面 park 可以正常执行完
LockSupport.unpark(t1);
}
}
5、LockSupport与wait¬ify比较示例
使用LockSupport与wait¬ify实现同样逻辑的代码:
- 线程A执行一段业务逻辑后调用wait/park阻塞住自己。主线程调用notify/unpark方法唤醒线程A,线程A然后打印自己执行的结果。
5.1 wait¬ify方式
import java.util.concurrent.locks.LockSupport;
/**
* LockSupport与wait¬ify的使用对比
*/
public class LockSupportAndWait {
public static void main(String[] args) throws Exception {
waitNotifyDemo();
}
// wait¬ify示例
public static void waitNotifyDemo() throws Exception {
final Object obj = new Object();
Thread A = new Thread(() -> {
int sum = 0;
for (int i = 0; i < 10; i++) {
sum += i;
}
try {
// wait和notify/notifyAll方法只能在同步代码块里用, 下述代码会报java.lang.IllegalMonitorStateException
// obj.wait();
// 使用synchronized包裹wait方法才是正确的
synchronized (obj) {
obj.wait();
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("waitNotify方式:sum = " + sum);
});
A.start();
//睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
Thread.sleep(1000);
synchronized (obj) {
obj.notify();
}
}
}
- 注意:wait和notify/notifyAll方法只能在同步代码块里用(在synchronized代码块内部),否则会报 java.lang.IllegalMonitorStateException 异常。
5.2 LockSupport方式
import java.util.concurrent.locks.LockSupport;
/**
* LockSupport与wait¬ify的使用对比
*/
public class LockSupportAndWait {
public static void main(String[] args) throws Exception {
lockSupportDemo();
}
// lockSupport示例
public static void lockSupportDemo() throws Exception {
Thread A = new Thread(() -> {
int sum = 0;
for (int i = 0; i < 10; i++) {
sum += i;
}
LockSupport.park();
System.out.println("LockSupport方式:sum = " + sum);
});
A.start();
//睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
Thread.sleep(1000);
LockSupport.unpark(A);
}
}
可以看出,LockSupport使用不需要在同步代码块里,线程间不需要维护一个共享的同步对象,实现了线程间的解耦。比Object的wait/notify方式简单很多。
5.3 wait¬ify和Locksupport去掉sleep后的对比
仔细观察上述2个示例,可以发现在主线程唤醒线程之前都有一个睡眠操作 Thread.sleep(1000);
,主线程调用了Thread.sleep(1000)方法主要用来等待线程A计算完成进入wait状态。
waitNotifyDemo去掉Thread.sleep()调用后:
多次执行后,我们会发现:有的时候能够正常打印结果并退出程序,但有的时候线程无法打印结果阻塞住了。原因就在于主线程调用完notify后,线程A才进入wait方法,导致线程A一直阻塞住。由于线程A不是后台/精灵线程,所以整个程序无法退出。
Locksupport去掉Thread.sleep()调用后:
我们会发现:不管你执行多少次,这段代码都能正常打印结果并退出。LockSupport支持主线程先调用unpark后,线程A再调用park而不被阻塞。这就是LockSupport最大的灵活所在。
6、总结
LockSupport比Object的wait/notify有两大优势:
- LockSupport不需要在同步代码块里,所以线程间也不需要维护一个共享的同步对象,实现了线程间的解耦。
- unpark函数可以先于park调用,所以不需要担心线程间的执行的先后顺序,park和unpark的使用不会出现死锁的情况。
- park和unpark可以实现类似wait和notify的功能,但是并不和wait和notify交叉,也就是说unpark不会对wait起作用,notify也不会对park起作用。
评论区