☰
Java并发同步器源码解析:AQS与Semaphore/CountDownLatch/CyclicBarrier原理
2026/10/8 3:51:45 网站建设 项目流程

先说个题外话。最近技术社区里“源码”相关的内容热度一直不减,但真正值得反复翻的源码,其实来来去去就那么几个。Java并发包里的Semaphore、CountDownLatch、CyclicBarrier这三个同步器,属于典型的“面试必背、背完就忘”的组件,很多人能用会写,但问到实现原理就卡壳。我自己读源码的习惯是:先把AQS(AbstractQueuedSynchronizer)这个底层框架吃透,再看这三个同步器只是“几层皮”而已。这篇文章就把三者的源码链路梳理一遍,讲清楚state、CLH队列、Condition这些核心机制是怎么配合的,同时附上我在实际项目中用它们做限流、压测、分批计算的踩坑经验。无论你是刚开始学并发的小白,还是准备面试跳槽的开发者,希望这篇能帮你把浮在表面的“会用”变成“心里有底”的“懂原理”。

1. 先啃地基:AQS为什么是这三个同步器的共同底牌

1.1 一小时搞懂AQS的两大核心:state与CLH变种队列

AQS这个东西,说白了就是一把“并发锁的模板”。它内部维护了一个volatile int state和一个双向等待队列。state的含义由子类自己定:Semaphore拿它当许可证数量,CountDownLatch拿它当计数器,ReentrantLock拿它当重入次数。队列则是CLH锁的一个变种,每个等待的线程被封装成一个Node节点,挂在队尾,通过前驱节点的状态判断自己是否可以尝试获取资源。

这里的Node节点有两个关键字段:waitStatus和thread。waitStatus是一个int,取值有CANCELLED(1)、SIGNAL(-1)、CONDITION(-2)、PROPAGATE(-3)。简单理解:SIGNAL表示“当前节点释放时,需要唤醒后继节点”,这是整个队列得以“接力跑”的核心。CANCELLED表示节点因为超时或中断被取消了,释放资源时会从队列里踢出去。

队列的进出规则也很固定:获取资源失败的线程,通过addWaiter(Node.SHARED)或addWaiter(Node.EXCLUSIVE)入队,然后在一个for循环里反复尝试;如果前驱是head,说明轮到它了,再试一次获取资源,成功就把自己变成新的head。整个过程没有使用synchronized,全靠CAS保证并发安全,所以性能才能撑得住高并发场景。

1.2 模板方法模式:tryAcquireShared与tryReleaseShared

AQS本身的acquireShared和releaseShared不会直接操作业务状态,而是调用子类实现的两个方法。这就是模板方法模式,也是读源码最舒服的地方——你只需要盯住子类重写的那几个方法,就能理解整个同步逻辑。

以共享模式为例,AQS的acquireShared(int arg)大概长这样:

public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) { doAcquireSharedInterruptibly(arg); } }

tryAcquireShared返回负数表示资源不足,需要入队等待;返回非负数表示获取成功,直接往下走。releaseShared(int arg)则负责释放:

public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; }

doReleaseShared()的职责是唤醒队列中阻塞的线程,它内部用CAS把head的waitStatus从SIGNAL改成0,再调用LockSupport.unpark唤醒后继节点。这里有个细节:唤醒操作会从head向后传播,因为共享模式下可能有多个线程同时被唤醒,这也是Semaphore能同时放行多个线程的原因。

一直有人问“这三个同步器哪个最难”,我的看法是:理解顺序应该是先AQS,再Semaphore,再CountDownLatch,最后CyclicBarrier。因为前两个是标准的AQS共享模式实现,第三四个虽然名字像,但CyclicBarrier压根没用AQS,这点后面细说。

2. Semaphore扒皮:信号量如何用state完成限流与公平调度

2.1 核心模型:一个state当N张许可证

Semaphore翻译成“信号量”确实贴切。它的构造器里直接setState(permits),state的大小就是许可证的总数。线程调用acquire()时,其实是在“借”一张许可证;调用release()时,是把许可证“还”回去。如果state已经降到0,再有人来借,就只能去队列里排队等着。

Semaphore有一个很有意思的设计:它区分了公平和非公平两种模式。默认是非公平的,对应的内部类是NonfairSync,它的tryAcquireShared实现如下:

final int nonfairTryAcquireShared(int acquires) { for (;;) { int available = getState(); int remaining = available - acquires; if (remaining < 0 || compareAndSetState(available, remaining)) { return remaining; } } }

这段代码的逻辑是:先读出当前state,算出剩余许可数。如果剩余为负,说明许可证不够,直接返回负数;如果够,就用CAS把state更新成剩余值,然后返回剩余值。这里return的语义要特别注意——返回负数代表失败,返回非负数代表成功,非负数的大小还暗示了“现在还剩多少许可证”。

公平版的FairSync则多了一步检查:

protected int tryAcquireShared(int acquires) { for (;;) { if (hasQueuedPredecessors()) { return -1; } int available = getState(); int remaining = available - acquires; if (remaining < 0 || compareAndSetState(available, remaining)) { return remaining; } } }

hasQueuedPredecessors()就是检查等待队列里有没有比自己排得更早的线程,有就直接返回-1,不去抢。所以公平模式下,新建线程必须老实排队,避免“插队”现象。我在实际使用中,默认都选非公平,因为公平模式在大量线程竞争时会多出一次队列查询,吞吐量会差一截。

2.2 许可证的借与还:acquire和release链路解析

acquire()的入口是sync.acquireSharedInterruptibly(1),这个方法会响应中断。获取失败后进入doAcquireSharedInterruptibly,线程被LockSupport.park挂起,等着被唤醒。所以Semaphore的等待是“挂起-唤醒”式的阻塞,不是自旋空转。

release()的入口是sync.releaseShared(1),它的tryReleaseShared实现如下:

protected final boolean tryReleaseShared(int releases) { for (;;) { int current = getState(); int next = current + releases; if (next < current) { // overflow throw new Error("Maximum permit count exceeded"); } if (compareAndSetState(current, next)) { return true; } } }

这里用到了自旋CAS,因为可能有多个线程同时释放许可证,CAS失败就重试,直到成功。next < current是防溢出保护,正常情况下永远不会触发。有个坑值得提一下:release()不需要调用方持有过许可证,也就是说一个没调用过acquire()的线程也能调用release(),结果就是state被不断加高。这在代码里容易埋雷,比如finally块里忘了判断是否实际获取过许可,就会导致许可证数量虚增。

2.3 实操:用Semaphore给数据库连接池做并发限流

我举个我之前在项目里实际写过的例子。一个订单系统需要访问数据库,但数据库连接数有限,超过一定并发就会报连接超时。最简单的方案就是用Semaphore限制同时执行的SQL数量:

public class SqlGuard { private final Semaphore semaphore = new Semaphore(5); public void execute(Connection conn, String sql) throws InterruptedException { semaphore.acquire(); try { try (Statement stmt = conn.createStatement()) { stmt.execute(sql); } } finally { semaphore.release(); } } }

这里的核心是:acquire和release必须成对出现,release必须放在finally里。否则SQL执行过程中抛异常,许可证就泄漏了,跑一会儿会发现所有线程都堵在acquire上。这是个非常经典的线上事故,我见过不止一次。

用tryAcquire可以做成“优雅降级”的限流:拿不到许可就走兜底逻辑(比如直接返回缓存数据),而不是硬等。

if (semaphore.tryAcquire(200, TimeUnit.MILLISECONDS)) { try { // 执行数据库操作 } finally { semaphore.release(); } } else { // 降级:读缓存 return cache.get(key); }

这种写法在网关层做接口限流很常见,既保证核心链路不被打垮,又不至于因为等待许可把线程全部拖死。

Semaphore还有一个看着反直觉的用法:初始化为0,配合release()做“开关”。所有线程都先执行acquire()挂起,另一个线程执行N次release()逐个放行。不过这个场景我用CountDownLatch更多,毕竟那个语义更清晰。

2.4 避坑清单:许可证泄漏、不可重入、公平性误判

写Semaphore踩过的坑,我整理成几条:

  • 许可证泄漏:acquire之后没在finally释放,或者提前return了,导致可用许可越来越少,最后系统“假死”。排查办法是监控availablePermits(),正常情况下应该回到初始值。
  • Semaphore不可重入:同一线程里再次acquire也会继续扣减许可,不会像ReentrantLock那样放行。所以递归或循环里用Semaphore要特别小心,容易把自己堵死。
  • 不要把限流数设得和连接数一样大:如果底层连接池只有5个连接而Semaphore设成10,等待许可的线程过了这一关,还是会堵在连接池上,等于白限流。我通常是设为连接数的70%左右,留出余量。
  • 公平模式不等于绝对公平:公平模式只是避免了新来的线程插队,但如果是“先唤醒的线程被中断了,下一个线程还没被唤醒”这种窗口期,还是会有些许不公。并发场景没有银弹,要看业务容忍度。

3. CountDownLatch拆解:一次性闸门背后的CLH队列等待机制

3.1 state从count到0:countDown与await源码精读

CountDownLatch的构造器同样是把state设置成初始count。它有两个核心方法:countDown()和await()。await()的实现是sync.acquireSharedInterruptibly(1),其内部的tryAcquireShared很简单:

protected int tryAcquireShared(int acquires) { return (getState() == 0) ? 1 : -1; }

只要state不是0,就返回-1,调用的线程进入等待队列,被挂起。state数到0之后,所有等待的线程都会被一次性放行。这里有个关键点:countDown()用的也是releaseShared,它的tryReleaseShared里有一个非常重要的判断返回值逻辑:

protected boolean tryReleaseShared(int releases) { for (;;) { int c = getState(); if (c == 0) { return false; } int nextc = c - 1; if (compareAndSetState(c, nextc)) { return nextc == 0; } } }

注意这两处细节:第一,state已经到0时,countDown()不会再做什么,返回值是false;第二,只有当CAS成功且nextc == 0时,tryReleaseShared才返回true,触发后续的doReleaseShared()去唤醒等待队列里的线程。也就是说,最后一次countDown的线程承担了“唤醒所有等待者”的职责。

3.2 等待队列里发生了什么:doAcquireSharedInterruptibly逐行拆解

await()真正复杂的部分在于等待逻辑。看AQS的doAcquireSharedInterruptibly:

private void doAcquireSharedInterruptibly(int arg) throws InterruptedException { final Node node = addWaiter(Node.SHARED); try { for (;;) { final Node p = node.predecessor(); if (p == head) { int r = tryAcquireShared(arg); if (r >= 0) { setHeadAndPropagate(node, r); p.next = null; return; } } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) { throw new InterruptedException(); } } } catch (Throwable t) { cancelAcquire(node); throw t; } }

这段代码是整个等待机制的核心。我逐行解释一下:

  • addWaiter(Node.SHARED)创建当前线程的节点,以SHARED模式挂到队列尾部。
  • 进入for循环后,先看自己的前驱是不是head。如果是,说明自己排到了队首,再尝试一次获取资源。tryAcquireShared此时会再次检查state,CountDownLatch就是看state是否为0。
  • 如果获取成功,执行setHeadAndPropagate(node, r),把当前节点设为新head。PROPAGATE状态就是在这个方法里传播的,目的是让“闸门打开”的信号能连续传递下去,唤醒后续所有等待的共享节点。
  • 如果获取失败,执行shouldParkAfterFailedAcquire把前驱节点的waitStatus设为SIGNAL,然后parkAndCheckInterrupt真正挂起线程。

我一开始读这段代码的时候一直想不通:为什么await()明明是个“一次性”的操作,还要用循环反复尝试?后来才明白,挂起中的线程被唤醒后,可能并不满足获取条件(比如被虚假唤醒),必须回到循环开头重新判断。这种循环加CAS的写法,正是无锁编程的标准范式。

3.3 实操:CountDownLatch模拟并发压测,以及多模块初始化协调

CountDownLatch最常见的场景是“主线程等所有子任务完成”,但其实用它做“同时起跑”也特别顺。我做过一个接口压测小工具,需要让500个线程在同一时刻发请求。实现很简单:两个CountDownLatch互相配合。

int threadCount = 500; CountDownLatch ready = new CountDownLatch(threadCount); CountDownLatch start = new CountDownLatch(1); CountDownLatch end = new CountDownLatch(threadCount); for (int i = 0; i < threadCount; i++) { new Thread(() -> { ready.countDown(); // 通知主线程:我准备好了 try { start.await(); // 阻塞等待开跑信号 // 在这里发送HTTP请求,记录耗时 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { end.countDown(); // 通知主线程:我跑完了 } }).start(); } ready.await(); // 等待所有线程就位 long begin = System.currentTimeMillis(); start.countDown(); // 放行所有线程 end.await(); // 等待所有线程完成 long cost = System.currentTimeMillis() - begin;

这里start这个CountDownLatch初始值是1,主线程执行一次countDown()就能让500个线程从await()里同时醒来。实际测试下来,线程唤醒的延迟一般在几毫秒内,对于绝大多数压测场景足够精确了。如果你有更严格的时间对齐要求,那得上专门的压测框架,而不是手动写Latch。

另一个常见场景是服务启动时的多模块预热。比如一个服务要同时预热本地缓存、连接池、线程池,三件事可以并行做,等全部完成后才对外提供服务。用CountDownLatch写出来的代码非常直观,而且好维护。

3.4 拉闸不能复位:为什么CountDownLatch是一次性的

这个特性经常被拿来和CyclicBarrier做对比。CountDownLatch的state一旦减到0,它的tryAcquireShared就永远返回非负数了,后面的countDown()不会再产生任何影响。从源码里也能看到,state == 0时tryReleaseShared直接返回false,后续代码直接跳过。

这意味着如果你需要“多轮等待”能力,不能用CountDownLatch硬撑,要么每次创建新实例,要么换成CyclicBarrier。我在项目里见到有人用for循环给CountDownLatch每个批次都new一个对象,这没问题,就是对象生命周期没管理好容易出小毛病。反观CyclicBarrier把这个需求内置了,后面会讲到它如何通过generation实现复用。

3.5 避坑清单:countDown遗漏、超时设置、中断恢复

  • countDown必须放在finally里:如果任务执行抛异常,当前线程直接退出,count就永远少了一个,主线程会一直挂在await()上。线上最常见的是线程池里任务被拒绝,或者业务异常被吞掉,导致计数一直凑不齐。
  • await必须加超时:尤其是跨系统调用的场景,谁也不能保证子任务一定成功完成。我会习惯写成await(30, TimeUnit.SECONDS),超时后走降级逻辑而不是无限等。
  • 子线程捕获InterruptedException后要恢复中断标志:很多代码在catch (InterruptedException e)里只打印日志,中断信号就丢了。正确做法是Thread.currentThread().interrupt(),把中断状态还回去。
  • 小心“信号提前丢失”:如果某个子线程在countDown()之前就因为异常退出了,主线程这边无法感知到具体是哪个任务失败,只能靠超时兜底。所以任务内最好有明确的成功/失败标记,配合结果汇总,而不是只依赖计数。

4. CyclicBarrier解剖:它为什么不用AQS,而是Lock+Condition

4.1 换个思路:ReentrantLock + Condition如何实现“人齐发车”

CyclicBarrier是我认为三个同步器里设计最有意思的一个。它没有用AQS,而是组合了ReentrantLock + Condition。为什么不用AQS?你可以从语义上琢磨一下:CountDownLatch等的是“数归零”,Semaphore等的是“许可证”,这些都可以抽象成“某个state满足条件就放行”。但CyclicBarrier等的是“所有参与线程到达同一个屏障点”,而且到达之后会自动重置,进入下一轮。这种“多轮协作”的场景,借助Condition的等待/通知机制实现起来更直白。

CyclicBarrier内部有几个关键字段:

  • lock:一个ReentrantLock,所有对屏障状态的操作都要拿锁。
  • trip:lock.newCondition(),等待线程的休息室。
  • generation:内部类Generation,记录当前是第几轮。
  • count:当前轮还差多少个线程到达,初始是parties。
  • barrierCommand:每次屏障打破时执行的Runnable任务,由最后一个到达的线程执行。

4.2 await源码精读:最后一个线程做了什么

每个参与者都会调用await(),真正干活的都在dowait(boolean timed, long nanos)里。我把关键路径拆出来了:

private int dowait(boolean timed, long nanos) throws InterruptedException, BrokenBarrierException, TimeoutException { final ReentrantLock lock = this.lock; lock.lock(); try { final Generation g = generation; if (g.broken) { throw new BrokenBarrierException(); } if (Thread.interrupted()) { breakBarrier(); throw new InterruptedException(); } int index = --count; if (index == 0) { // 我就是最后一个到达的线程 boolean ranAction = false; try { final Runnable command = barrierCommand; if (command != null) { command.run(); } ranAction = true; nextGeneration(); // 唤醒所有等待线程,并开启新一轮 return 0; } finally { if (!ranAction) { breakBarrier(); // 如果barrierCommand执行失败,也要打破屏障 } } } // 不是最后一个,就进入等待循环 for (;;) { try { if (!timed) { trip.await(); // 普通等待 } else if (nanos > 0L) { nanos = trip.awaitNanos(nanos); // 限时等待 } } catch (InterruptedException ie) { if (g == generation && !g.broken) { breakBarrier(); throw ie; } else { Thread.currentThread().interrupt(); } } if (g.broken) { throw new BrokenBarrierException(); } if (g != generation) { return index; // 换代说明本轮到点了,返回自己在第几号位 } if (timed && nanos <= 0L) { breakBarrier(); throw new TimeoutException(); } } } finally { lock.unlock(); } }

这段代码信息量很大,我挑三个重点:

第一,最后一个线程执行了额外的两件事:执行barrierCommand(如果有的话),然后调用nextGeneration(),它内部先trip.signalAll()唤醒所有等待线程,再把count重置回parties,最后generation = new Generation()。整个过程都在持锁状态下完成,保证原子性。

第二,**等待线程被唤醒后,怎么知道自己是“正常完成”还是“屏障坏了”?**它拿到Generation g后,先检查g.broken,如果被打破就抛BrokenBarrierException;再检查g != generation,不等说明已经换代,说明上一轮成功结束,安全返回。这两层检查就是中断、超时与正常唤醒之间的分水岭。

第三,中断与超时的处理规则:如果某个等待线程中断了,它会调用breakBarrier()把屏障标记为broken并唤醒所有伙伴,其他线程随后要么抛BrokenBarrierException,要么继续被唤醒后也抛异常。所以栅栏只要有一个参与者“出事”,整批人都会知道。

Personal note:我在读这段代码前一直以为CyclicBarrier内部也维护了一个计数器,且复用的是state。看完才发现它用generation解决了“多轮复用”这个难题,这个抽象非常干净。

4.3 实操:CyclicBarrier做分批并行计算,多次复用同一个屏障

讲一个我实际接触过的数据处理需求:一个定时任务需要把一批数据分批次写入外部系统,每批要等四个线程都准备好了再合并,批次之间有依赖,必须按顺序处理。用CyclicBarrier实现非常顺:

int batchThreads = 4; CyclicBarrier barrier = new CyclicBarrier(batchThreads, () -> { // 这个回调会由每批最后一个到达的线程执行 // 在这里做汇总、落库、发消息 System.out.println("第 " + batchNo.incrementAndGet() + " 批完成"); }); // 每个工作线程的run方法大概长这样 for (int batch = 0; batch < batchCount; batch++) { // 1. 处理属于自己的那一段数据 processSlice(batch); // 2. 等本批其他线程都处理完 barrier.await(); // 3. 屏障被打破后,所有线程自动进入下一轮 }

第一轮跑完后,所有线程在barrier.await()返回,这里的关键是返回后线程并没有被销毁,而是继续执行for循环进入下一批。所以CyclicBarrier特别适合“多个线程在同一份数据上分片处理,每处理完一片同步一次,然后继续”的多轮任务。

另外要注意:回调barrierCommand是同步执行的,由最后一个到达的线程执行。如果这个回调很耗时,它会拖累整批线程的进度,因为只有它返回后nextGeneration()才会执行,其他线程才能被唤醒。所以回调里尽量别做重IO,要么异步化,要么把汇总逻辑拆到后续步骤。

4.4 避坑清单:broken状态、超时、reset时机

CyclicBarrier的坑比前两个更隐蔽,我列一下我踩过的:

  • BrokenBarrierException不是偶发异常,是“系统性故障”的预警:只要有一个线程中断或超时,整个屏障就broken了,其他线程全部抛异常。排查这类问题不要只看单个线程的日志,要看全量线程的异常分布。
  • reset() 不要在等待过程中调用:源码里reset()先执行breakBarrier()再nextGeneration(),如果此时有其他线程正在await(),它们会因为broken抛异常。正确姿势是先确认没有线程在等待,或者通过try-catch兜底。
  • await别裸奔,超时一定要加:我给合作方讲一次事故,就是因为有个线程长时间卡在IO上,屏障迟迟凑不齐,其他三个线程干等。后来全换成await(10, TimeUnit.SECONDS),超时后做降级或重试,才稳住。
  • parties的数量必须和实际参与线程数一致:多喊一个少喊一个都容易出问题。少一个还行(最后一个线程会等很久),多一个就直接错过“人齐”的判断,永远等不到。这个从源码逻辑上想就明白了:--count到0才会换代,总数不对,永远到不了0。
  • 子线程数比parties少时的处理:如果线程池只有3个线程,而parties是4,那屏障等不到第4个线程,会一直挂起。遇到这种情况,可以用超时+重试的方式兜底,或者干脆用CountDownLatch更合适。

5. 三兄弟怎么选:一张对比表搞定并发场景的选型判断

5.1 底层实现与核心行为对比

把三个组件放在一张表里,差异一目了然:

维度SemaphoreCountDownLatchCyclicBarrier
底层实现AQS共享模式AQS共享模式ReentrantLock + Condition
核心状态许可证数量state计数器state,归零放行参与线程数count + generation
可重用性可反复acquire/release一次性,不能重置可重复使用,自动换代
等待线程数多个线程同时等待许可多个线程同时等待归零多个线程互相等待,人齐放行
中断处理抛出InterruptedException抛出InterruptedException中断会让屏障broken,伙伴线程抛BrokenBarrierException
超时处理tryAcquire(timeout)await(timeout)await(timeout),超时打破屏障
是否区分公平性支持公平/非公平不区分不区分
典型场景限流、资源池、信号量锁多任务汇总、压测同时起跑、服务预热多轮分批计算、并行任务对齐后继续

从表上能看出,CyclicBarrier和前两者完全不是一个路由:前两个是“容器里的状态变化放行外部线程”,CyclicBarrier是“参与线程互相帮扶,到位后一起走”。

5.2 选型逻辑:先想清楚你的业务到底需要“等待什么”

我在实际写代码的时候会先问自己三个问题:

  • 我到底在等一个什么条件?如果条件是“某个共享资源数量够不够”,选Semaphore;如果条件是“一坨子任务有没有全部完事”,选CountDownLatch;如果条件是“每个参与线程有没有各自到达本批次的终点”,选CyclicBarrier。
  • 这个流程是一次性的还是多轮的?一轮定生死,选CountDownLatch;需要反复同步,选CyclicBarrier。
  • 等待的线程是“被动等被通知”还是“主动互相等”?前者是CountDownLatch/Semaphore(有一个释放方),后者是CyclicBarrier(没有专门的释放方,每个线程都是参与者)。

有朋友问过“CyclicBarrier能替代CountDownLatch吗”,严格讲不行。CountDownLatch等的是别人(被等待对象不感知等待者),CyclicBarrier等的是自己人(每个线程都知道别人在等自己)。语义不一样,硬套很容易出bug。

5.3 源码阅读方法论:我是怎么顺着一条主线把并发包读薄的

最后聊点学习方法。我自己读并发包源码的路径是这样的:

  • 先找最典型的AQS实现(ReentrantLock或Semaphore),读它的tryAcquire、tryRelease、tryAcquireShared、tryReleaseShared,把AQS的模板方法跑通。
  • 再看AQS的doAcquireSharedInterruptibly和doReleaseShared,理解排队、挂起、唤醒、传播这几步。
  • 最后看CyclicBarrier,用“Lock + Condition + 换代”的思路去理解它和AQS的差异。

读源码时我习惯先用一个demo把流程跑起来,然后打断点看线程状态。比如CountDownLatch的demo,主线程和子线程都打上await()和countDown()的断点,观察主线程在LockSupport.park前后的状态,一下子就能把“挂起-唤醒”这个过程看成动态的。纸面上读十遍,不如断点盯一遍。

如果你对这个主题有兴趣,下一步可以顺手把ReentrantLock源码也读了,因为你读CyclicBarrier时已经接触了Condition,而ConditionObject本身也是AQS内部类,两者的关联会让你对AQS的理解再上一层楼。

我个人在实际操作中还有一个体会:这三种同步器虽然常被当成面试题,但它们背后的设计思路——模板方法、状态机、队列化等待、Condition与中断协作——才是真正值钱的东西。把这些吃透了,以后碰到再冷门的并发组件,也基本能蒙着源码猜出八九分。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询