@@ -201,6 +201,150 @@ AQS 定义两种资源共享方式:`Exclusive`(独占,只有一个线程
201201
202202一般来说,自定义同步器的共享方式要么是独占,要么是共享,他们也只需实现` tryAcquire-tryRelease ` 、` tryAcquireShared-tryReleaseShared ` 中的一种即可。但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如` ReentrantReadWriteLock ` 。
203203
204+ #### 独占模式与共享模式的深入对比
205+
206+ 虽然都是基于 AQS 实现,但独占模式和共享模式在设计理念和实现细节上有着本质的区别。
207+
208+ ** 独占模式(Exclusive)的特点:**
209+
210+ 1 . ** 排他性** :同一时刻只能有一个线程持有资源。当一个线程获取到资源后,其他线程必须等待该线程释放资源才能竞争。
211+ 2 . ** state 语义** :` state ` 表示资源的占用状态和重入次数。` state = 0 ` 表示未被占用,` state > 0 ` 表示被占用(值代表重入次数)。
212+ 3 . ** 实现方法** :需要实现 ` tryAcquire() ` 和 ` tryRelease() ` 方法。
213+ 4 . ** 典型应用** :` ReentrantLock ` 、` ReentrantReadWriteLock ` 的写锁。
214+
215+ ** 共享模式(Shared)的特点:**
216+
217+ 1 . ** 共享性** :允许多个线程同时持有资源。一个线程获取资源成功后,可能会触发其他等待线程的连锁唤醒。
218+ 2 . ** state 语义** :` state ` 表示剩余可用资源数量。比如 ` Semaphore ` 中,` state ` 表示剩余许可证数量;` CountDownLatch ` 中,` state ` 表示还需要完成的计数。
219+ 3 . ** 实现方法** :需要实现 ` tryAcquireShared() ` 和 ` tryReleaseShared() ` 方法。返回值有特殊含义:负数表示失败,0 表示成功但无剩余资源,正数表示成功且有剩余资源。
220+ 4 . ** 典型应用** :` Semaphore ` 、` CountDownLatch ` 、` CyclicBarrier ` 、` ReentrantReadWriteLock ` 的读锁。
221+
222+ ** 核心区别举例** :
223+
224+ 假设有一个停车场,有 10 个车位。
225+
226+ - ** 独占模式** 就像是** 单人车位** :一个车位只能停一辆车,其他车必须等这辆车开走才能进来。这就是 ` ReentrantLock ` 的模型。
227+ - ** 共享模式** 就像是** 共享停车场** :10 个车位可以同时停 10 辆车,每辆车进来时检查是否还有空位(` state > 0 ` ),有就停进去并把可用车位数减 1。这就是 ` Semaphore ` 的模型。
228+
229+ ** 传播机制的区别** :
230+
231+ 这是共享模式最特殊的地方。在共享模式下,当一个线程释放资源后,不仅会唤醒后继节点,被唤醒的节点获取资源成功后,还可能继续唤醒它的后继节点,形成"传播效应"。这就是为什么会有 ` PROPAGATE ` 状态的原因。
232+
233+ 而在独占模式下,资源释放后只会唤醒一个后继节点,不存在连锁唤醒的情况。
234+
235+ ** 举个例子** :想象一个会议室预订系统。如果是独占模式,一个会议室同一时间只能被一个团队使用。但如果改成共享模式(比如一个大型阶梯教室可以容纳多个小组同时开会),当有一个小组释放了位置,可能会触发多个等待的小组同时进入。第一个小组进入后发现还有空间,就会通知第二个小组也可以进来(传播机制),第二个小组进入后发现还有空间,又会通知第三个小组,形成连锁反应。
236+
237+ ### Condition 条件队列的工作机制
238+
239+ Condition 是 AQS 提供的另一个重要功能,它实现了类似 ` Object.wait() ` 和 ` Object.notify() ` 的等待/通知机制,但功能更强大也更灵活。
240+
241+ #### Condition 队列与同步队列的关系
242+
243+ AQS 内部实际上维护了** 两种队列** :
244+
245+ 1 . ** 同步队列(Sync Queue)** :就是前面讲的 CLH 变体队列,用于线程竞争锁。
246+ 2 . ** 条件队列(Condition Queue)** :每个 Condition 对象内部维护的单向链表,用于线程等待特定条件。
247+
248+ 一个锁可以有多个 Condition 对象,每个 Condition 都有自己独立的条件队列。这比 ` synchronized ` 的 ` wait/notify ` 机制灵活得多,后者只有一个等待队列。
249+
250+ #### Condition 的工作流程
251+
252+ ** await() 操作的流程:**
253+
254+ 1 . 当前线程必须先持有锁(否则抛出 ` IllegalMonitorStateException ` )
255+ 2 . 将当前线程封装成 Node 节点,加入到 Condition 的条件队列尾部
256+ 3 . 完全释放锁(即使是重入锁,也要将 state 减到 0)
257+ 4 . 阻塞当前线程,等待被 ` signal ` 唤醒
258+ 5 . 被唤醒后,节点从条件队列移到同步队列,重新竞争锁
259+ 6 . 获取锁成功后,从 ` await() ` 方法返回
260+
261+ ** signal() 操作的流程:**
262+
263+ 1 . 当前线程必须先持有锁
264+ 2 . 从条件队列的头部取出一个节点
265+ 3 . 将这个节点从条件队列移到同步队列
266+ 4 . 通过 ` unpark ` 唤醒该节点对应的线程
267+ 5 . 被唤醒的线程会在同步队列中竞争锁
268+
269+ ** 举个生动的例子** :
270+
271+ 可以把 Condition 想象成医院的候诊系统:
272+
273+ - ** 同步队列** 就像是** 挂号大厅** :所有人都在这里排队等待叫号看病(竞争锁)
274+ - ** 条件队列** 就像是** 检查室的等候区** :医生让你去做检查,你从挂号大厅(同步队列)进入检查等候区(条件队列),等待检查结果(await)
275+ - 当检查结果出来(signal),护士会通知你,你再次回到挂号大厅(同步队列)重新排队等待叫号
276+
277+ 一个医院可以有多个检查室(多个 Condition 对象),每个检查室都有自己的等候区(独立的条件队列)。这比 ` synchronized ` 只有一个等候区要灵活得多。
278+
279+ #### Condition 的经典应用场景
280+
281+ ** 生产者-消费者模型** :
282+
283+ ``` java
284+ // 使用两个 Condition 分别控制生产和消费
285+ Condition notFull = lock. newCondition(); // 队列未满
286+ Condition notEmpty = lock. newCondition(); // 队列非空
287+
288+ // 生产者
289+ while (队列已满) {
290+ notFull. await(); // 等待队列有空位
291+ }
292+ // 生产数据
293+ notEmpty. signal(); // 通知消费者来消费
294+
295+ // 消费者
296+ while (队列为空) {
297+ notEmpty. await(); // 等待队列有数据
298+ }
299+ // 消费数据
300+ notFull. signal(); // 通知生产者可以生产
301+ ```
302+
303+ 这种设计比使用 ` synchronized ` + ` wait/notify ` 更清晰,避免了"惊群效应"(所有等待线程都被唤醒但只有一个能工作)。
304+
305+ ### 公平锁与非公平锁的性能对比
306+
307+ 在 AQS 的基础上,可以实现公平锁和非公平锁两种策略,它们在性能和公平性之间做了不同的权衡。
308+
309+ #### 实现机制的区别
310+
311+ ** 公平锁(Fair Lock)** :
312+ - 严格按照线程到达的顺序分配锁
313+ - 新来的线程总是先检查队列中是否有等待的线程,如果有就乖乖排队
314+ - 实现:在 ` tryAcquire() ` 中会调用 ` hasQueuedPredecessors() ` 检查队列中是否有前驱节点
315+
316+ ** 非公平锁(Nonfair Lock)** :
317+ - 新来的线程会先尝试抢占锁,抢占失败才排队
318+ - 如果锁刚好被释放,新线程可能直接获得锁,而不管队列中是否有等待的线程
319+ - 实现:直接执行 CAS 操作尝试获取锁,不检查队列
320+
321+ #### 性能对比与权衡
322+
323+ 根据 Doug Lea(AQS 作者)的测试和实际生产环境的统计:
324+
325+ ** 吞吐量对比** :
326+ - 在高并发场景下,非公平锁的吞吐量通常是公平锁的 ** 10-20 倍**
327+ - 原因:减少了线程切换的开销。公平锁每次都要排队,涉及线程的阻塞和唤醒;非公平锁允许"插队",如果锁刚好可用,当前线程可以立即获得,避免了上下文切换
328+
329+ ** 响应时间对比** :
330+ - 公平锁的响应时间更稳定,不会出现某个线程长时间得不到锁的情况
331+ - 非公平锁的平均响应时间更短,但可能出现"线程饥饿"现象(某些线程长时间得不到锁)
332+
333+ ** 实际应用建议** :
334+
335+ 1 . ** 默认使用非公平锁** :` ReentrantLock ` 的无参构造器创建的就是非公平锁,这是因为在大多数场景下,吞吐量比顺序性更重要。
336+ 2 . ** 需要严格顺序时使用公平锁** :比如任务调度系统、售票系统等需要保证先来先服务的场景。
337+ 3 . ** 阿里巴巴的实践** :在《Java 开发手册》中建议,除非业务场景明确需要公平性,否则应该使用非公平锁以获得更好的性能。
338+
339+ ** 举个例子** :
340+
341+ 想象一个咖啡店的点单台:
342+
343+ - ** 公平锁模式** :所有顾客必须严格按照到达顺序排队,即使前一个顾客刚点完单走开,收银员在处理订单,新来的顾客也必须等前面所有人都轮到后才能点单。
344+ - ** 非公平锁模式** :如果前一个顾客刚点完单,收银员正好空闲,新来的顾客可以直接上前点单,不用管后面是否还有排队的人。虽然可能对排队的人不公平,但整体效率更高,顾客等待时间的平均值更短。
345+
346+ 在大型互联网公司的实践中(如阿里、美团),非公平锁因为性能优势被广泛采用。但在金融系统、抢票系统等需要保证公平性的场景,会选择使用公平锁,即使牺牲一些性能。
347+
204348### AQS 资源获取源码分析(独占模式)
205349
206350AQS 中以独占模式获取资源的入口方法是 ` acquire() ` ,如下:
0 commit comments