@@ -14,13 +14,40 @@ head:
14
14
15
15
# 第十八节:线程阻塞唤醒类 LockSupport
16
16
17
- LockSupport 位于 ` java.util.concurrent.locks ` 包下,我们只讲在 [ 讲 Lock 包的时候也讲过 ] ( https://javabetter.cn/thread/lock .html ) ,大家还有印象没?
17
+ LockSupprot 用来阻塞和唤醒线程,底层实现依赖于 [ Unsafe 类 ] ( https://javabetter.cn/thread/Unsafe .html ) (后面会细讲)。
18
18
19
- LockSupprot 是线程的阻塞原语,用来阻塞线程和唤醒线程。每个使用 LockSupport 的线程都会与一个许可关联,如果该许可可用,并且可在线程中使用,则调用 ` park() ` 会立即返回,否则可能阻塞。如果许可尚不可用,则可以调用 unpark 使其可用。但是注意许可** 不可重入** ,也就是说只能调用一次 ` park() ` 方法,否则会一直阻塞。
19
+ 该类包含一组用于阻塞和唤醒线程的静态方法,这些方法主要是围绕 park 和 unpark 展开,话不多说,直接来看一个简单的例子吧。
20
+
21
+ ``` java
22
+ public class LockSupportDemo1 {
23
+ public static void main (String [] args ) {
24
+ Thread mainThread = Thread . currentThread();
25
+
26
+ // 创建一个线程从1数到1000
27
+ Thread counterThread = new Thread (() - > {
28
+ for (int i = 1 ; i <= 1000 ; i++ ) {
29
+ System . out. println(i);
30
+ if (i == 500 ) {
31
+ // 当数到500时,唤醒主线程
32
+ LockSupport . unpark(mainThread);
33
+ }
34
+ }
35
+ });
36
+
37
+ counterThread. start();
38
+
39
+ // 主线程调用park
40
+ LockSupport . park();
41
+ System . out. println(" Main thread was unparked." );
42
+ }
43
+ }
44
+ ```
45
+
46
+ 上面的代码中,当 counterThread 数到 500 时,它会唤醒 mainThread。而 mainThread 在调用 park 方法时会被阻塞,直到被 unpark。
20
47
21
48
LockSupport 中的方法不多,这里将这些方法做一个总结:
22
49
23
- ## ** 阻塞线程**
50
+ ## 阻塞线程
24
51
25
52
1 . ` void park() ` :阻塞当前线程,如果调用 unpark 方法或线程被中断,则该线程将变得可运行。请注意,park 不会抛出 InterruptedException,因此线程必须单独检查其中断状态。
26
53
2 . ` void park(Object blocker) ` :功能同方法 1,入参增加一个 Object 对象,用来记录导致线程阻塞的对象,方便问题排查。
@@ -29,19 +56,19 @@ LockSupport 中的方法不多,这里将这些方法做一个总结:
29
56
5 . ` void parkUntil(long deadline) ` :阻塞当前线程直到某个指定的截止时间(以毫秒为单位),或直到被 unpark 调用,或线程被中断。
30
57
6 . ` void parkUntil(Object blocker, long deadline) ` :功能同方法 5,入参增加一个 Object 对象,用来记录导致线程阻塞的对象,方便问题排查。
31
58
32
- ## ** 唤醒线程**
59
+ ## 唤醒线程
33
60
34
61
` void unpark(Thread thread) ` :唤醒一个由 park 方法阻塞的线程。如果该线程未被阻塞,那么下一次调用 park 时将立即返回。这允许“先发制人”式的唤醒机制。
35
62
36
- 实际上,LockSupport 阻塞和唤醒线程的功能依赖于 ` sun.misc.Unsafe ` ,这是一个很底层的类,感兴趣的小伙伴可以看看美团技术出品的 [ 这篇文章 ] ( https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe .html ) ,比如 park 方法是通过 ` unsafe.park() ` 方法实现的。
63
+ 实际上,LockSupport 阻塞和唤醒线程的功能依赖于 ` sun.misc.Unsafe ` ,这是一个很底层的类,[ 后面这篇文章会细讲 ] ( https://javabetter.cn/thread/Unsafe .html ) ,比如 LockSupport 的 park 方法是通过 ` unsafe.park() ` 方法实现的。
37
64
38
- 另外在阻塞线程这块,有一个很有意思的现象,每个方法都会新增一个带有 Object 的阻塞对象的重载方法,直接看 dump 线程的信息。
65
+ ## Dump 线程
39
66
40
67
"Dump 线程"通常是指获取线程的当前状态和调用堆栈的详细快照。这可以提供关于线程正在执行什么操作以及线程在代码的哪个部分的重要信息。
41
68
42
69
下面是线程转储中可能包括的一些信息:
43
70
44
- - 线程ID和名称 :线程的唯一标识符和可读名称。
71
+ - 线程 ID 和名称 :线程的唯一标识符和可读名称。
45
72
- 线程状态:线程的当前状态,例如运行(RUNNABLE)、等待(WAITING)、睡眠(TIMED_WAITING)或阻塞(BLOCKED)。
46
73
- 调用堆栈:线程的调用堆栈跟踪,显示线程从当前执行点回溯到初始调用的完整方法调用序列。
47
74
- 锁信息:如果线程正在等待或持有锁,线程转储通常还包括有关这些锁的信息。
@@ -95,6 +122,8 @@ public class LockSupportDemo {
95
122
96
123
有意思的事情是,Java 1.5 推出 LockSupport 时遗漏了阻塞信息的描述,于是在 Java 1.6 的时候进行了补充。
97
124
125
+ ## 与 synchronzed 的区别
126
+
98
127
还有一点需要注意的是:** [ synchronzed] ( https://javabetter.cn/thread/synchronized-1.html ) 会使线程阻塞,线程会进入 BLOCKED 状态,而调用 LockSupprt 方法阻塞线程会使线程进入到 WAITING 状态。**
99
128
100
129
来一个简单的例子演示一下该怎么用。
@@ -130,13 +159,106 @@ Thread is parked now
130
159
Thread is unparked now
131
160
```
132
161
133
- ## 总结
162
+ ## 设计思路
134
163
135
- LockSupport 提供了一种更底层和灵活的线程调度方式。它不依赖于同步块或特定的锁对象。可以用于构建更复杂的同步结构,例如自定义锁或并发容器。LockSupport.park 与 LockSupport.unpark 的组合使得线程之间的精确控制变得更容易,而不需要复杂的同步逻辑和对象监视。
164
+ LockSupport 的设计思路是通过许可证来实现的,就像汽车上高速公路,入口处要获取通行卡,出口处要交出通行卡,如果没有通行卡你就无法出站,当然你可以选择补一张通行卡。
165
+
166
+ LockSupport 会为使用它的线程关联一个许可证(permit)状态,permit 的语义「是否拥有许可」,0 代表否,1 代表是,默认是 0。
167
+
168
+ - ` LockSupport.unpark ` :指定线程关联的 permit 直接更新为 1,如果更新前的` permit<1 ` ,唤醒指定线程
169
+ - ` LockSupport.park ` :当前线程关联的 permit 如果>0,直接把 permit 更新为 0,否则阻塞当前线程
170
+
171
+ 来看时间线:
172
+
173
+ ![ ] ( https://cdn.tobebetterjavaer.com/stutymore/LockSupport-20230901163159.png )
174
+
175
+ - 线程 A 执行` LockSupport.park ` ,发现 permit 为 0,未持有许可证,阻塞线程 A
176
+ - 线程 B 执行` LockSupport.unpark ` (入参线程 A),为 A 线程设置许可证,permit 更新为 1,唤醒线程 A
177
+ - 线程 B 流程结束
178
+ - 线程 A 被唤醒,发现 permit 为 1,消费许可证,permit 更新为 0
179
+ - 线程 A 执行临界区
180
+ - 线程 A 流程结束
181
+
182
+ 经过上面的分析得出结论 unpark 的语义明确为「使线程持有许可证」,park 的语义明确为「消费线程持有的许可」,所以 unpark 与 park 的执行顺序没有强制要求,只要控制好使用的线程即可,` unpark=>park ` 执行流程如下
183
+
184
+ ![ ] ( https://cdn.tobebetterjavaer.com/stutymore/LockSupport-20230901163443.png )
185
+
186
+ - permit 默认是 0,线程 A 执行 LockSupport.unpark,permit 更新为 1,线程 A 持有许可证
187
+ - 线程 A 执行 LockSupport.park,此时 permit 是 1,消费许可证,permit 更新为 0
188
+ - 执行临界区
189
+ - 流程结束
190
+
191
+ 最后再补充下 park 的注意点,因 park 阻塞的线程不仅仅会被 unpark 唤醒,还可能会被线程中断(` Thread.interrupt ` )唤醒,而且不会抛出 InterruptedException 异常,所以建议在 park 后自行判断线程中断状态,来做对应的业务处理。
192
+
193
+ 为什么推荐使用 LockSupport 来做线程的阻塞与唤醒(线程间协同工作),因为它具备如下优点:
194
+
195
+ - 以线程为操作对象更符合阻塞线程的直观语义
196
+ - 操作更精准,可以准确地唤醒某一个线程(notify 随机唤醒一个线程,notifyAll 唤醒所有等待的线程)
197
+ - 无需竞争锁对象(以线程作为操作对象),不会因竞争锁对象产生死锁问题
198
+ - unpark 与 park 没有严格的执行顺序,不会因执行顺序引起死锁问题,比如「Thread.suspend 和 Thread.resume」没按照严格顺序执行,就会产生死锁
136
199
200
+ ## 面试题
137
201
202
+ 阿里面试官:有 3 个独立的线程,一个只会输出 A,一个只会输出 B,一个只会输出 C,在三个线程启动的情况下,请用合理的方式让他们按顺序打印 ABCABC。
203
+
204
+ ``` java
205
+ public class ABCPrinter {
206
+ private static Thread t1, t2, t3;
207
+
208
+ public static void main (String [] args ) {
209
+ t1 = new Thread (() - > {
210
+ for (int i = 0 ; i < 2 ; i++ ) {
211
+ LockSupport . park();
212
+ System . out. print(" A" );
213
+ LockSupport . unpark(t2);
214
+ }
215
+ });
216
+
217
+ t2 = new Thread (() - > {
218
+ for (int i = 0 ; i < 2 ; i++ ) {
219
+ LockSupport . park();
220
+ System . out. print(" B" );
221
+ LockSupport . unpark(t3);
222
+ }
223
+ });
224
+
225
+ t3 = new Thread (() - > {
226
+ for (int i = 0 ; i < 2 ; i++ ) {
227
+ LockSupport . park();
228
+ System . out. print(" C" );
229
+ LockSupport . unpark(t1);
230
+ }
231
+ });
232
+
233
+ t1. start();
234
+ t2. start();
235
+ t3. start();
236
+
237
+ // 主线程稍微等待一下,确保其他线程已经启动并且进入park状态。
238
+ try {
239
+ Thread . sleep(100 );
240
+ } catch (InterruptedException e) {
241
+ e. printStackTrace();
242
+ }
243
+
244
+ // 启动整个流程
245
+ LockSupport . unpark(t1);
246
+ }
247
+ }
248
+ ```
249
+
250
+ 这里的实现方式是:
251
+
252
+ - 我们首先为每个线程创建一个 Runnable,使其在循环中 park 自身,然后输出其对应的字符,并 unpark 下一个线程。
253
+ - 所有线程在启动后会先调用 park 将自己阻塞。
254
+ - 主线程稍微延迟后调用 t1 的 unpark,启动整个打印流程。
255
+ 这样可以保证每个线程按照预期的顺序进行工作。
256
+
257
+ ## 总结
258
+
259
+ LockSupport 提供了一种更底层和灵活的线程调度方式。它不依赖于同步块或特定的锁对象。可以用于构建更复杂的同步结构,例如自定义锁或并发容器。LockSupport.park 与 LockSupport.unpark 的组合使得线程之间的精确控制变得更容易,而不需要复杂的同步逻辑和对象监视。
138
260
139
- > 编辑:沉默王二,编辑前的内容主要来自于CL0610的 GitHub 仓库[ https://github.com/CL0610/Java-concurrency ] ( https://github.com/CL0610/Java-concurrency/blob/master/13.LockSupport%E5%B7%A5%E5%85%B7/LockSupport%E5%B7%A5%E5%85%B7.md )
261
+ > 编辑:沉默王二,编辑前的内容主要来自于 CL0610 的 GitHub 仓库[ https://github.com/CL0610/Java-concurrency ] ( https://github.com/CL0610/Java-concurrency/blob/master/13.LockSupport%E5%B7%A5%E5%85%B7/LockSupport%E5%B7%A5%E5%85%B7.md ) ,另外一部分内容和图片来自于读者 [ 程序猿阿星的写给小白看的 LockSupport ] ( https://mp.weixin.qq.com/s/xSro-bwg__ir9EXwoCJ-rg ) ,强烈推荐。
140
262
141
263
---
142
264
0 commit comments