LockSupport的用法及原理
介绍:
LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,当然阻塞之后肯定得有唤醒的方法。
接下面我来看看LockSupport有哪些常用的方法。主要有两类方法:park和unpark。
// 暂停当前线程
public static void park(Object blocker);
// 暂停当前线程,不过有超时时间的限制
public static void parkNanos(Object blocker, long nanos);
// 暂停当前线程,直到某个时间
public static void parkUntil(Object blocker, long deadline);
// 无期限暂停当前线程
public static void park();
// 暂停当前线程,不过有超时时间的限制
public static void parkNanos(long nanos);
// 暂停当前线程,直到某个时间
public static void parkUntil(long deadline);
// 恢复当前线程
public static void unpark(Thread thread);
public static Object getBlocker(Thread t);
private static void setBlocker(Thread t, Object arg)park英文意思为停车。我们如果把Thread看成一辆车的话,park就是让车停下,unpark就是让车启动然后跑起来。
park函数是将当前调用Thread阻塞,而unpark函数则是将指定线程Thread唤醒。与Object类的wait/notify机制相比,park/unpark有两个优点:
以thread为操作对象更符合阻塞线程的直观定义
操作更精准,可以准确地唤醒某一个线程(notify随机唤醒一个线程,notifyAll唤醒所有等待的线程),增加了灵活性。
每个线程都有一个parkBlocker对象,对象声明为volatile,这个blocker是用来记录线程被阻塞时被谁阻塞的,用于线程监控和分析工具来定位原因。
官方的解释:
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
LockSupport类使用了Permit(许可证)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit只有两个值1和零,默认是零。
许可看成是一种(0和1)信号量(Semaphore),但与Semaphore不同的是,许可的累加上限是1
通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作,此外,park()+park(Object blocker) 可以用来阻塞当前线程阻塞传入的具体线程
@Test
@SneakyThrows
@DisplayName("线程阻塞唤醒测试")
public void test(){
Object u = new Object();
Runnable r = () -> {
System.out.println("进入:" + Thread.currentThread().getName());
LockSupport.park();
if(Thread.currentThread().isInterrupted()){
System.out.println(Thread.currentThread().getName()+"被中断了!");
}
System.out.println(Thread.currentThread().getName()+"继续执行!");
};
Thread a = new Thread(r,"a");
Thread b = new Thread(r,"b");
a.start();
TimeUnit.SECONDS.sleep(1);
b.start();
TimeUnit.SECONDS.sleep(3);
a.interrupt();
LockSupport.unpark(b);
a.join();
b.join();
}
---------
这儿park和unpark其实实现了wait和notify的功能,不过还是有一些差别的。
park不需要获取某个对象的锁,wait和notify是必须在synchronized先获取到锁对象后执行因为中断的时候
park不会抛出InterruptedException异常,所以需要在park之后自行判断中断状态,然后做额外的处理
我们再来看看Object blocker,这是个什么东西呢?这其实就是方便在线程dump的时候看到具体的阻塞对象的信息。
"t1" #10 prio=5 os_prio=31 tid=0x00007f95030cc800 nid=0x4e03 waiting on condition [0x00007000011c9000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:304)
// `下面的这个信息`
at com.wtuoblist.beyond.concurrent.demo.chapter3.LockSupportDemo$ChangeObjectThread.run(LockSupportDemo.java:23) //
- locked <0x0000000795830950> (a java.lang.Object)还有一个地方需要注意,相对于线程的stop和resume,park和unpark的先后顺序并不是那么严格。stop和resume如果顺序反了,会出现死锁现象。而park和unpark却不会。这又是为什么呢?还是看一个例子
@Test
@SneakyThrows
@DisplayName("线程阻塞唤醒测试")
public void test(){
Object u = new Object();
Runnable r = () -> {
System.out.println("进入:" + Thread.currentThread().getName());
LockSupport.park();
if(Thread.currentThread().isInterrupted()){
System.out.println(Thread.currentThread().getName()+"被中断了!");
}
System.out.println(Thread.currentThread().getName()+"继续执行!");
};
Thread a = new Thread(r,"a");
Thread b = new Thread(r,"b");
TimeUnit.SECONDS.sleep(1);
b.start();
TimeUnit.SECONDS.sleep(3);
LockSupport.unpark(b);
a.start();
}t1内部有休眠1s的操作,所以unpark肯定先于park的调用,但是t1最终仍然可以完结。这是因为park和unpark会对每个线程维持一个许可(boolean值)
unpark调用时,如果当前线程还未进入park,则许可为true
park调用时,判断许可是否为true,如果是true,则继续往下执行;如果是false,则等待,直到许可为true
总结一下
park和unpark可以实现类似wait和notify的功能,但是并不和wait和notify交叉,也就是说unpark不会对wait起作用,notify也不会对park起作用。park和unpark的使用不会出现死锁的情况blocker的作用是在dump线程的时候看到阻塞对象的信息
LockSupport面试题
为什么可以先唤醒线程后阻塞线程?
因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞。
为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?
因为凭证的数量最多为1(不能累加),连续调用两次 unpark和调用一次 unpark效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证,证不够,不能放行。
底层使用了Unsafe.park和Unsafe.unpark的底层实现原理
在Linux系统下,是用的Posix线程库pthread中的mutex(互斥量),condition(条件变量)来实现的。
mutex和condition保护了一个_counter的变量,当park时,这个变量被设置为0,当unpark时,这个变量被设置为1。
concurrent包是基于AQS (AbstractQueuedSynchronizer)框架的,AQS框架借助于两个类:
Unsafe(提供CAS操作)
LockSupport(提供park/unpark操作)
因此,LockSupport非常重要。
park函数是将当前调用Thread阻塞,而unpark函数则是将指定线程Thread唤醒。