基础 | 并发编程 - [LockSupport]
INDEX
- §1 是什么
- §2 为什么出现
- §3 使用
- §4 理解 `par()` 和 `unpark()` 的 许可证(permit)
§1 是什么
- 属于 JUC
- 是个类
- 用于创建锁和其他同步类的基本线程阻塞原语
是 JDK 对线程阻塞的实现,AQS 与其他锁都是基于它实现的
§2 为什么出现
主要是因为 synchronized
和 Lock
存在限制,无论是 wari() + notify() 还是 await() + signal()
- 都 依赖于 java 的 monitor
当没有synchronized
或lock()
就调用上述方法都会导致抛出IllegalMonitorStateException
- 都 强调顺序
先进行唤醒后阻塞语法上虽然允许但逻辑中会导致无法退出锁
LockSupport
则可以绕开上述两点,
这是因为 LockSupport
是直接对线程进行阻塞唤醒
而 synchronized
和 Lock
则是基于线程上挂载的锁对象的 monitor
LockSupport
通过 par()
和 unpark()
方法提供线程的阻塞和唤醒功能
这两个方法在底层依赖于 Unsafe
的 par()
和 unpark()
方法实现,Unsafe
的这两个方法属于原语方法
更具体的细节参考 理解 par()
和 unpark()
的 许可证(permit)
§3 使用
public static void main(String[] args) {
Thread a = new Thread(()->{
System.out.println("A");
LockSupport.park();
System.out.println("AA");
});
Thread b = new Thread(()->{
System.out.println("B");
LockSupport.unpark(a);
System.out.println("BB");
});
a.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) { e.printStackTrace(); }
b.start();
}
§4 理解 par()
和 unpark()
的 许可证(permit)
par()
和 unpark()
对线程的效果受 许可证(permit) 影响
- permit 相当于一块 <免死金牌>
- 每个线程只有一个 permit,即只有一块 <免死金牌>
- permit 具有且只具有 两种状态,0、1
这意味着连续两次发放线程的 permit 后,线程持有的 permit 数量还是 1- 0,线程不再持有 permit,相当于被
LockSupport
扣留,或本身就没有 - 1,线程持有 permit,相当于被
LockSupport
发放
- 0,线程不再持有 permit,相当于被
par()
会试图将线程从预计可执行的行列中剔除出去,即对线程造成 <阻塞打击>- 线程持有 permit,相当于持有 <免死金牌>,则 消耗掉 <免死金牌> 以免疫 此次 打击,
par()
立即返回,
这意味着par()
失败,即线程 不阻塞,同时没有 <免死金牌> 了 - 否则,相当于没有 <免死金牌>,线程直接承受 <阻塞打击>,于是线程被从预计可执行的行列中剔除
这意味着par()
成功,即线程 阻塞
- 线程持有 permit,相当于持有 <免死金牌>,则 消耗掉 <免死金牌> 以免疫 此次 打击,
unpar()
会增加 permit ,相当于又发放一块 <免死金牌>- 若线程本身阻塞,就从阻塞状态中唤醒,但不获取的 <免死金牌> 效果
- 若线程本身没阻塞,则持有一块 <免死金牌>,免疫下一次 <阻塞打击>
相关说明见下面源码注释
这里有两个需要注意的点
- permit 并不是直接线程的运行状态的变更生效的,这与传统的锁不同
permit 直接影响的是par()
和unpark()
的执行效果 - permit 有 0,1 两个状态,但对应到线程其实是三个状态
- 眩晕:blocked
- 裸奔:unblocked & no-permit
- 带盾:unblocked & permit
基于上面两点的理解, par()
和 unpark()
的作用其实可以总结为让线程在上面三个状态中切换,结合下表
状态 | par() | unpark() |
---|---|---|
眩晕 | 无变化_眩晕 | 裸奔 |
裸奔 | 眩晕 | 带盾 |
带盾 | 裸奔 | 无变化_带盾 |
死 / 未运行 | 无变化 | 无变化 |
示意图
代码验证
public static void main(String[] args) {
final boolean[] sword_twice = new boolean[]{false};//对线程 a,是刀 1 次,还是刀 2 次
// a 中的 sleep 都是 10 毫秒,如果没有阻塞,会很快执行到下一句
// 如果阻塞了,会出现 b 的信息,然后才是 a 后面的输出
Thread a = new Thread(()->{
System.out.println("A");
LockSupport.park();
longSound(10,1);
System.out.println("AA");
if(sword_twice[0]){
longSound(10,1); // longSound(100,1) in case 3
LockSupport.park();
longSound(10,1);
System.out.println("AAA");
}
},"a");
// b 中的 sleep 都是 3000 毫秒,如果有阻塞,会有很明显的 b 的信息,然后才轮到 a 的信息
Thread b = new Thread(()->{
System.out.println("B");
longSound(1000,3);
LockSupport.unpark(a);
System.out.println("BB");
},"b");
//cases
}
private static void longSound(int length,int times) {
try {
for(int i=0; i<times; i++) {
TimeUnit.MILLISECONDS.sleep(length);
System.out.println("......long......sound......by......"+Thread.currentThread().getName());
}
} catch (InterruptedException e) { e.printStackTrace(); }
}
case 1 正常的锁功能
System.out.println("========== case 1 ==============");
a.start();
longSound(100,1);
b.start();
case 2 (提前给金牌,可以免疫一次 park)
System.out.println("========== case 2 ==============");
sword_twice[0] = true;
a.start();
LockSupport.unpark(a);
longSound(100,1);
b.start();
case 3 从阻塞状态给两次金牌,可以免疫一次 park,第一次救活线程,第二次免疫
注意按注释调一下睡眠时间,否则不严谨
System.out.println("========== case 3 ==============");
sword_twice[0] = true;
a.start();
longSound(10,1);
//连续执行两次的话会卡在第二次 park,怀疑是紧邻的两次 unpark 其实是同时生效于一个状态,导致第二个失效
//第二次 park 前,会睡 100ms,足够这里的两次 10ms 的 unpark 跑完
LockSupport.unpark(a);
longSound(10,1);
LockSupport.unpark(a);
case 4 & 5 对比,线程没有启动时,park 和 unpark 是不一定生效的
System.out.println("========== case 4 ==============");
sword_twice[0] = false;
a.start();
LockSupport.unpark(a);
longSound(100,1);
b.start();
System.out.println("========== case 5 ==============");
sword_twice[0] = false;
LockSupport.unpark(a);
a.start();
longSound(100,1);
b.start();