LockSupport的使用
LockSupport
一、为什么需要LockSupport类
来看下在没有LockSupport之前,是怎么实现让线程等待/唤醒的。在没有LockSupport之前,线程的挂起和唤醒咱们都是通过Object的wait和notify/notifyAll方法实现。
那如果咱们换成LockSupport呢?简单得很,看代码:
public static void main(String[] args) throws Exception {
Thread A = new Thread(() -> {
int sum = 0;
for (int i = 0; i < 10; i++) {
sum += i;
}
LockSupport.park();
System.out.println(sum);
});
A.start();
//睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
Thread.sleep(1000);
LockSupport.unpark(A);
}
二、LockSupport灵活性
首先LockSupport在使用起来比Object的wait/notify简单,而且最重要的是灵活性。
上边的例子代码中,主线程调用了Thread.sleep(1000)方法来等待线程A计算完成进入wait状态。如果去掉Thread.sleep()调用:
public static void main(String[] args) throws Exception {
final Object obj = new Object();
Thread A = new Thread(() -> {
int sum = 0;
for (int i = 0; i < 10; i++) {
sum += i;
}
try {
synchronized (obj) {
obj.wait();
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(sum);
});
A.start();
//睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
// Thread.sleep(1000);
synchronized (obj) {
obj.notify();
}
}
多次执行后,我们会发现:有的时候能够正常打印结果并退出程序,但有的时候线程无法打印结果阻塞住了。
原因就在于主线程调用完notify后,线程A才进入wait方法,导致线程A一直阻塞住。由于线程A不是后台线程,所以整个程序无法退出。
那如果换做LockSupport呢?LockSupport就支持主线程先调用unpark后,线程A再调用park而不被阻塞吗?是的,没错。代码如下
public static void main(String[] args) throws Exception {
Thread A = new Thread(() -> {
int sum = 0;
for (int i = 0; i < 10; i++) {
sum += i;
}
LockSupport.park();
System.out.println(sum);
});
A.start();
//睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
// Thread.sleep(1000);
LockSupport.unpark(A);
}
不管你执行多少次,这段代码都能正常打印结果并退出。这就是LockSupport最大的灵活所在。
小结一下,LockSupport比Object的wait/notify有两大优势:
- LockSupport不需要在同步代码块里 。所以线程间也不需要维护一个共享的同步对象了,实现了线程间的解耦。
- unpark函数可以先于park调用,所以不需要担心线程间的执行的先后顺序。
- notify只能随机选择一个线程唤醒,无法唤醒指定的线程,unpark却可以唤醒一个指定的线程。
三、原理
那么原理是什么呢?
3.1、park方法
blocker是用来记录线程被阻塞时被谁阻塞的。用于线程监控和分析工具来定位原因的。setBlocker(t, blocker)方法的作用是记录t线程是被broker阻塞的。因此我们只关注最核心的方法,也就是UNSAFE.park(false, 0L)。
UNSAFE是一个非常强大的类,他的的操作是基于底层的,也就是可以直接操作内存,因此我们从JVM的角度来分析一下:
每个java线程都有一个Parker实例:
我们换一种角度来理解一下park和unpark,可以想一下,unpark其实就相当于一个许可,告诉特定线程你可以停车,特定线程想要park停车的时候一看到有许可,就可以立马停车。
现在有了这个概念,我们体会一下上面JVM层面park的方法,这里面counter字段,就是用来记录所谓的“许可”的。
当调用park时,先尝试直接能否直接拿到“许可”,即_counter>0时,如果成功,则把_counter设置为0,并返回。
如果不成功,则构造一个ThreadBlockInVM,然后检查_counter是不是>0,如果是,则把_counter设置为0,unlock mutex并返回:
否则,再判断等待的时间,然后再调用pthread_cond_wait函数等待,如果等待返回,则把_counter设置为0,unlock mutex并返回:
总结来说,画个图来表示一下:
3.2、unpark方法
还是先来看一下JDK源码:
上面注释的意思是给线程生产许可证。
当unpark时,则简单多了,直接设置_counter为1,再unlock mutext返回。如果_counter之前的值是0,则还要调用pthread_cond_signal唤醒在park中等待的线程:
ok,现在我们已经对源码进行了分析,整个过程其实就是生产许可和消费许可的过程。而且这个生产过程可以反过来。也就是先生产再消费。
3.3、上面案例回顾分析
public static void main(String[] args) throws Exception {
Thread A = new Thread(() -> {
int sum = 0;
for (int i = 0; i < 10; i++) {
sum += i;
}
LockSupport.park();
System.out.println(sum);
});
A.start();
//睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
// Thread.sleep(1000);
LockSupport.unpark(A);
}
这里是不能够确定哪个线程是先执行,哪个后执行的。
1、如果park先执行,发现counter=0,那么等待;在进行unpark的时候,发现=0,先去阻塞的线程执行;
2、如果unpark先执行,那么直接赋值counter=1,而park在执行的时候,发现counter=1之后,就立马执行。
四、LockSupport使用
4.1、park住的线程没有被中断,只会阻塞
public class ThreadWattingTestOne {
public static void main(String[] args) {
final Thread thread1 = new Thread(() -> {
LockSupport.unpark(Thread.currentThread());
System.out.println("hello,world");
LockSupport.park();
System.out.println("hello,park1");
LockSupport.park();
System.out.println("hello,park2");
});
thread1.start();
try {
thread1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
查看控制台输出:
hello,world
hello,park1
先执行unpark,直接赋值counter=1,然后第一次park执行时,发现counter=1之后,执行之后并将counter设置为0;但是第二次的时候发现counter=0之后,阻塞。所以输出不出来park2
4.2、park住的线程被正常唤醒
public class LockSupportTestTwo {
public static void main(String[] args) {
final Thread thread1 = new Thread(() -> {
for (;;){
System.out.println("hello,world");
LockSupport.park();
System.out.println("hello,park");
}
});
thread1.start();
LockSupport.unpark(thread1);
}
}
控制台打印:
hello,world
hello,park
hello,world
1、如果park先执行,那么第一次就会被阻塞住;而在unpark执行之后,会唤醒阻塞住的线程;第二次循环执行的时候,发现还是为0,那么阻塞;
2、如果unpark先执行,那么counter=1,park消费完成之后,counter=0,然后又会阻塞。
二者执行效果上是一样的。
4.3、park线程被中断唤醒
public class LockSupportTestFour {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
System.out.println(String.format("当前线程的名称是:%s", Thread.currentThread().getName()));
LockSupport.park();
System.out.println("hello,locksupport");
LockSupport.park();
System.out.println("hello,locksupport");
});
thread1.start();
Thread.sleep(3000);
thread1.interrupt();
}
}
控制台输出:
当前线程的名称是:Thread-0
hello,locksupport
hello,locksupport
也就是说interrupt会中断掉park方法,让其停止阻塞。
再来一下循环的例子会更加直观明显:
public class LockSupportTestThree {
public static void main(String[] args) {
final Thread thread1 = new Thread(() -> {
for (;;){
System.out.println("hello,world");
LockSupport.park();
System.out.println("hello,park");
}
});
thread1.start();
thread1.interrupt();
}
}
控制台循环执行。线程中断打断了让其进入到休眠,所以线程一直处于活跃状态。也就是说线程中断让其许可一直处于可用状态。
因为从Thread中的State状态中的描述可看到,LockSupport.park()会让线程进入到WAITING状态,而线程中断会打破线程休眠。
4.4、park住的线程和Thread.interupted()结合
/**
* @Description 测试LockSupport.park();,只会park住中断标记为false的,不会打断中断标记为true的
* @Author liguang
* @Date 2022/06/20/17:51
*/
public class ParkInteruptTestOne {
private static Logger logger = LoggerFactory.getLogger(ParkInteruptTestOne.class);
public static void main(String[] args) throws InterruptedException {
testParkTwo();
}
/**
* park的线程因为中断而唤醒之后,将无法再park住
* @throws InterruptedException
*/
private static void testParkTwo() throws InterruptedException {
Thread threadA = new Thread(() -> {
// park住的线程是因为中断唤醒的,那么将无法再次park住
logger.info("当前线程正在执行........");
logger.info("当前线程是因为中断而被唤醒的");
LockSupport.park();
// 但是重置标记
Thread.interrupted();
logger.info("当前线程中断标记是:{}",Thread.currentThread().isInterrupted());
logger.info("因为当前线程标记为false,所以会再次阻塞住,不会再次来打印下面的代码");
LockSupport.park();
logger.info("是否会再次来打印下面的代码?");
}, "threadA");
threadA.start();
TimeUnit.SECONDS.sleep(2);
// 发送中断信号之后,再也无法进行park住了
threadA.interrupt();
}
}
五、总结
调用LockSupport.park()/LockSupport.park(this)代码只是会在线程标记状态为TRUE的时候,不会阻塞住当前线程;
侧重点:在于不满足某个条件的时候让当前线程阻塞住,当满足了条件之后,再让当前线程运行起来。