Skip to content

Commit b0b6588

Browse files
committed
第三遍 review 结束
1 parent da21846 commit b0b6588

17 files changed

+517
-941
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,11 +260,11 @@
260260
- [锁分类和 JUC](docs/thread/lock.md)
261261
- [重入锁ReentrantLock](docs/thread/reentrantLock.md)
262262
- [读写锁ReentrantReadWriteLock](docs/thread/ReentrantReadWriteLock.md)
263-
- [协作类Condition](docs/thread/condition.md)
263+
- [等待通知条件Condition](docs/thread/condition.md)
264264
- [线程阻塞唤醒类LockSupport](docs/thread/LockSupport.md)
265265
- [Java的并发容器](docs/thread/map.md)
266266
- [并发容器ConcurrentHashMap](docs/thread/ConcurrentHashMap.md)
267-
- [非阻塞性队列ConcurrentLinkedQueue](docs/thread/ConcurrentLinkedQueue.md)
267+
- [非阻塞队列ConcurrentLinkedQueue](docs/thread/ConcurrentLinkedQueue.md)
268268
- [阻塞队列BlockingQueue](docs/thread/BlockingQueue.md)
269269
- [并发容器CopyOnWriteArrayList](docs/thread/CopyOnWriteArrayList.md)
270270
- [本地变量ThreadLocal](docs/thread/ThreadLocal.md)

docs/home.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,11 +269,11 @@ head:
269269
- [锁分类和 JUC](thread/lock.md)
270270
- [重入锁ReentrantLock](thread/reentrantLock.md)
271271
- [读写锁ReentrantReadWriteLock](thread/ReentrantReadWriteLock.md)
272-
- [协作类Condition](thread/condition.md)
272+
- [等待通知条件Condition](thread/condition.md)
273273
- [线程阻塞唤醒类LockSupport](thread/LockSupport.md)
274274
- [Java的并发容器](thread/map.md)
275275
- [并发容器ConcurrentHashMap](thread/ConcurrentHashMap.md)
276-
- [非阻塞性队列ConcurrentLinkedQueue](thread/ConcurrentLinkedQueue.md)
276+
- [非阻塞队列ConcurrentLinkedQueue](thread/ConcurrentLinkedQueue.md)
277277
- [阻塞队列BlockingQueue](thread/BlockingQueue.md)
278278
- [并发容器CopyOnWriteArrayList](thread/CopyOnWriteArrayList.md)
279279
- [本地变量ThreadLocal](thread/ThreadLocal.md)

docs/thread/BlockingQueue.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,8 @@ dequeue 方法主要做了两件事情:
216216

217217
从以上分析可以看出,put 和 take 方法主要通过 [Condition](https://javabetter.cn/thread/condition.html) 的通知机制来完成阻塞式的数据生产和消费。
218218

219+
### 3)使用示例
220+
219221
OK,我们再来看一个 ArrayBlockingQueue 的使用示例:
220222

221223
```java
@@ -452,7 +454,7 @@ public E take() throws InterruptedException {
452454

453455
09)返回取出的元素:最后返回被取出的元素 x。
454456

455-
### 3)LinkedBlockingQueue 的使用示例
457+
### 3)使用示例
456458

457459
```java
458460
public class LinkedBlockingQueueTest {

docs/thread/ConcurrentHashMap.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ public V put(K key, V value) {
3636

3737
为什么不用 [ReentrantLock](https://javabetter.cn/thread/reentrantLock.html) 而是 synchronzied 呢?
3838

39-
实际上,synchronzied 做了很多的优化,这个我们前面也讲过了,包括偏向锁、轻量级锁、重量级锁,可以依次向上升级锁状态,因此,synchronized 相较于 ReentrantLock 的性能其实差不多,甚至在某些情况更优。
39+
实际上,synchronzied 做了很多的优化,[这个我们前面也讲过了](https://javabetter.cn/thread/synchronized.html),包括偏向锁、轻量级锁、重量级锁,可以依次向上升级锁状态,因此,synchronized 相较于 ReentrantLock 的性能其实差不多,甚至在某些情况更优。
4040

41-
## JDK 1.7 和 JDK 1.8 中 ConcurrentHashMap 的区别
41+
## ConcurrentHashMap 的变化
4242

4343
ConcurrentHashMap 在 JDK 1.7 和 JDK 1.8 中有一些区别。这里我们分开介绍一下。
4444

@@ -131,7 +131,7 @@ static class Node<K,V> implements Map.Entry<K,V> {
131131
}
132132
```
133133

134-
## ConcurrentHashMap 的关键属性
134+
## ConcurrentHashMap的字段
135135

136136
1、**table**`volatile Node<K,V>[] table`:
137137

@@ -170,7 +170,7 @@ static {
170170
}
171171
```
172172

173-
## ConcurrentHashMap 的关键内部类
173+
## ConcurrentHashMap的内部类
174174

175175
### 1、Node
176176

@@ -239,7 +239,7 @@ static final class ForwardingNode<K,V> extends Node<K,V> {
239239
}
240240
```
241241

242-
## CAS 的关键操作
242+
## ConcurrentHashMap的CAS
243243

244244
ConcurrentHashMap 会大量使用 CAS 来修改它的属性和进行一些操作。因此,在理解 ConcurrentHashMap 的方法前,我们需要了解几个常用的利用 CAS 算法来保障线程安全的操作。
245245

@@ -274,7 +274,7 @@ static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
274274

275275
该方法用来设置 table 数组中索引为 i 的元素
276276

277-
## ConcurrentHashMap 的重点方法
277+
## ConcurrentHashMap的方法
278278

279279
### 构造方法
280280

@@ -875,7 +875,7 @@ private final void addCount(long x, int check) {
875875
}
876876
```
877877

878-
## ConcurrentHashMap 的代码示例
878+
## ConcurrentHashMap示例
879879

880880
假设我们想要构建一个线程安全的高并发统计用户访问次数的功能。在这里,ConcurrentHashMap是一个很好的选择,因为它提供了高并发性能。
881881

docs/thread/ConcurrentLinkedQueue.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@ head:
1212
content: Java,并发编程,多线程,Thread,ConcurrentLinkedQueue
1313
---
1414

15-
# 第二十一节:非阻塞性队列ConcurrentLinkedQueue
15+
# 第二十一节:非阻塞队列ConcurrentLinkedQueue
1616

1717
ConcurrentLinkedQueue 是 `java.util.concurrent`(JUC) 包下的一个线程安全的队列实现。基于非阻塞算法(Michael-Scott 非阻塞算法的一种变体),这意味着 ConcurrentLinkedQueue 不再使用传统的锁机制来保护数据安全,而是依靠底层原子的操作(如 [CAS](https://javabetter.cn/thread/cas.html))来实现。
1818

1919
Michael-Scott 由 Maged M. Michael 和 Michael L. Scott 在 1996 年提出,在这种算法中,一个线程的失败或挂起不会导致其他线程也失败或挂起。
2020

2121
好,接下来一起来看一下 ConcurrentLinkedQueue 的源码实现。
2222

23-
## Node
23+
## 节点类Node
2424

2525
先从它的节点类 Node 看起,好明白 ConcurrentLinkedQueue 的底层数据结构。Node 类的源码如下:
2626

@@ -87,7 +87,7 @@ boolean casNext(Node<E> cmp, Node<E> val) {
8787

8888
Unsafe 允许分配、释放和访问本机内存,就像使用 C 语言中的 malloc 和 free 一样。我们在讲 [CAS](https://javabetter.cn/thread/cas.html) 的时候有详细讲过,相信大家都还有印象。
8989

90-
## offer 方法
90+
## offer方法
9191

9292
ConcurrentLinkedQueue 是一种先进先出(FIFO,First-In-First-Out)的队列,offer 方法用于在队列尾部插入一个元素。如果成功添加元素,则返回 true。下面是这个方法的一般定义:
9393

@@ -174,7 +174,7 @@ public boolean offer(E e) {
174174
}
175175
```
176176

177-
### 单线程执行角度分析
177+
### 单线程执行角度分析
178178

179179
我们再从**单线程的角度**分析 offer 1 的过程。
180180

@@ -230,7 +230,7 @@ p = (p != t && t != (t = tail)) ? t : q;
230230

231231
在单线程环境下,`p = (p != t && t != (t = tail)) ? t : q;`这行代码永远不会将 p 赋值为 t,我们试着在**多线程**的环境下继续分析。
232232

233-
### 多线程执行角度分析
233+
### 多线程执行角度分析
234234

235235
**多线程环境**下,`p = (p != t && t != (t = tail)) ? t : q;` 这行代码就有意思了。
236236

@@ -244,7 +244,7 @@ p = (p != t && t != (t = tail)) ? t : q;
244244

245245
`if (p == q)`为 true 时,说明 p 节点的 next 也指向它自己,这种节点称之为**哨兵节点****这种节点在队列中存在的价值不大,一般表示要删除的节点或者空节点**。为了能够更好地理解这种情况,我们先看看 poll 方法的执行过程,再回过头来看,总之这是一个很有意思的事情。
246246

247-
## poll 方法
247+
## poll方法
248248

249249
poll 方法的源码如下:
250250

@@ -294,7 +294,7 @@ public E poll() {
294294

295295
6、移动到下一个节点:将p设置为q,即下一个节点,并继续循环。
296296

297-
### 单线程执行角度分析
297+
### 单线程执行角度分析
298298

299299
为了便于分析,我把代码注释删掉了,并标上行号。
300300

@@ -368,7 +368,7 @@ Node1 的 next 指向它自己,head 指向了 Node3。
368368
2. **如果当前 head、h 和 p 指向的节点 item 为 null 的话,说明该节点不是真正待删除的节点,那么应该继续寻找 item 不为 null 的节点。通过让 q 指向 p 的下一个节点(q = p.next)进行试探,若找到则通过 updateHead 方法更新 head 节点以及构造哨兵节点(`通过updateHead方法的h.lazySetNext(h)`**
369369

370370

371-
### **多线程执行情况分析**
371+
### 多线程执行情况分析
372372

373373
现在回过头来看 poll 方法的源码,有这样一部分:
374374

@@ -435,7 +435,7 @@ thread1 先执行到第 8 行代码`if ((q = p.next) == null)`,由于队列为
435435

436436
线程 A 执行判断 `if (p == q)` 为 true,继续执行 `p = (t != (t = tail)) ? t : head;`,由于 tail 没有发生改变,所以 p 被赋值为 head,重新从 head 开始完成插入操作。
437437

438-
## HOPS 设计
438+
## 延迟更新策略
439439

440440
通过上面对 offer 和 poll 方法的分析,我们发现 tail 和 head 是延迟更新的,两者更新的触发时机为:
441441

@@ -453,7 +453,7 @@ thread1 先执行到第 8 行代码`if ((q = p.next) == null)`,由于队列为
453453

454454
对 head 的更新也是同样的道理,虽然这样设计会多出在循环中定位尾节点的操作,但总体来说,读的操作效率要远远高于写的效率,因此,多出来的定位尾节点的性能损耗相对就很小了。
455455

456-
### ConcurrentLinkedQueue 使用示例
456+
## 使用示例
457457

458458
```java
459459
public class ConcurrentLinkedQueueTest {

docs/thread/CopyOnWriteArrayList.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ CopyOnWriteArrayList 是线程安全的,可以在多线程环境下使用。Co
3636

3737
我们在介绍[并发容器](https://javabetter.cn/thread/map.html)的时候,也曾提到过,相信大家都还有印象。
3838

39-
## CopyOnWriteArrayList 的实现原理
39+
## CopyOnWriteArrayList原理
4040

4141
OK,接下来我们来看一下 CopyOnWriteArrayList 的源码。顾名思义,实际上 CopyOnWriteArrayList 内部维护的就是一个数组:
4242

docs/thread/LockSupport.md

Lines changed: 132 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,40 @@ head:
1414

1515
# 第十八节:线程阻塞唤醒类 LockSupport
1616

17-
LockSupport 位于`java.util.concurrent.locks`包下,我们只讲在[讲 Lock 包的时候也讲过](https://javabetter.cn/thread/lock.html),大家还有印象没?
17+
LockSupprot 用来阻塞和唤醒线程,底层实现依赖于 [Unsafe 类](https://javabetter.cn/thread/Unsafe.html)(后面会细讲)。
1818

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。
2047

2148
LockSupport 中的方法不多,这里将这些方法做一个总结:
2249

23-
## **阻塞线程**
50+
## 阻塞线程
2451

2552
1. `void park()`:阻塞当前线程,如果调用 unpark 方法或线程被中断,则该线程将变得可运行。请注意,park 不会抛出 InterruptedException,因此线程必须单独检查其中断状态。
2653
2. `void park(Object blocker)`:功能同方法 1,入参增加一个 Object 对象,用来记录导致线程阻塞的对象,方便问题排查。
@@ -29,19 +56,19 @@ LockSupport 中的方法不多,这里将这些方法做一个总结:
2956
5. `void parkUntil(long deadline)`:阻塞当前线程直到某个指定的截止时间(以毫秒为单位),或直到被 unpark 调用,或线程被中断。
3057
6. `void parkUntil(Object blocker, long deadline)`:功能同方法 5,入参增加一个 Object 对象,用来记录导致线程阻塞的对象,方便问题排查。
3158

32-
## **唤醒线程**
59+
## 唤醒线程
3360

3461
`void unpark(Thread thread)`:唤醒一个由 park 方法阻塞的线程。如果该线程未被阻塞,那么下一次调用 park 时将立即返回。这允许“先发制人”式的唤醒机制。
3562

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()` 方法实现的。
3764

38-
另外在阻塞线程这块,有一个很有意思的现象,每个方法都会新增一个带有 Object 的阻塞对象的重载方法,直接看 dump 线程的信息。
65+
## Dump 线程
3966

4067
"Dump 线程"通常是指获取线程的当前状态和调用堆栈的详细快照。这可以提供关于线程正在执行什么操作以及线程在代码的哪个部分的重要信息。
4168

4269
下面是线程转储中可能包括的一些信息:
4370

44-
- 线程ID和名称:线程的唯一标识符和可读名称。
71+
- 线程 ID 和名称:线程的唯一标识符和可读名称。
4572
- 线程状态:线程的当前状态,例如运行(RUNNABLE)、等待(WAITING)、睡眠(TIMED_WAITING)或阻塞(BLOCKED)。
4673
- 调用堆栈:线程的调用堆栈跟踪,显示线程从当前执行点回溯到初始调用的完整方法调用序列。
4774
- 锁信息:如果线程正在等待或持有锁,线程转储通常还包括有关这些锁的信息。
@@ -95,6 +122,8 @@ public class LockSupportDemo {
95122

96123
有意思的事情是,Java 1.5 推出 LockSupport 时遗漏了阻塞信息的描述,于是在 Java 1.6 的时候进行了补充。
97124

125+
## 与 synchronzed 的区别
126+
98127
还有一点需要注意的是:**[synchronzed](https://javabetter.cn/thread/synchronized-1.html) 会使线程阻塞,线程会进入 BLOCKED 状态,而调用 LockSupprt 方法阻塞线程会使线程进入到 WAITING 状态。**
99128

100129
来一个简单的例子演示一下该怎么用。
@@ -130,13 +159,106 @@ Thread is parked now
130159
Thread is unparked now
131160
```
132161

133-
## 总结
162+
## 设计思路
134163

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」没按照严格顺序执行,就会产生死锁
136199

200+
## 面试题
137201

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 的组合使得线程之间的精确控制变得更容易,而不需要复杂的同步逻辑和对象监视。
138260

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),强烈推荐。
140262
141263
---
142264

0 commit comments

Comments
 (0)