搞不懂Condition内存模型
前言:核心代码
import java.util.concurrent.ExecutionException;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class Main
{
static boolean isNum=true;
static ReentrantLock lock = new ReentrantLock();
static Condition printNum=lock.newCondition();
static Condition printChar=lock.newCondition();
public static void main(String[] args) {
Thread t1=new Thread(()->{
lock.lock();
try {
for (int i = 1; i <= 26; i++) {
while (!isNum) {
try {
printNum.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.print(i);
isNum=false;
printChar.signal();
}
}finally {
lock.unlock();
}
});
Thread t2=new Thread(()->{
lock.lock();
try{
for (int i = 1; i <= 26; i++) {
while (isNum) {
printChar.await();
}
System.out.print((char) (i + 'A' - 1));
isNum=true;
printNum.signal();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
});
t1.start();
t2.start();
}
}
要从架构层面彻底搞懂 Condition(printNum 和 printChar)在内存和队列中的变化模型,我们需要扒开 Java 并发包(JUC)的底层,直面 AQS(AbstractQueuedSynchronizer) 的核心架构。
你觉得模糊,是因为你把 Condition 误看作了一个单纯的“内存变量”。实际上,每一个 Condition 本质上都是一个独立的、由底层管理的「单向等待队列」。
下面从内存模型、底层架构、以及核心流程可视化三个层面为你深度拆解。
一、 底层架构模型:三大队列与锁持有者
在你的代码中,lock.lock() 触达的底层世界由一个核心状态和三个队列组成:
- Lock Holder(当前锁持有者):指针,指向当前独占 CPU 执行权的线程。
- AQS 同步队列(Sync Queue):一个双向链表。没有抢到锁的线程,在这个队列里排队挂起,等待惊醒去争抢锁。
- printNum 条件队列:
Condition内部的一个单向链表。调用printNum.await()的线程会释放锁,并被塞进这里。 - printChar 条件队列:另一个独立的单向链表。调用
printChar.await()的线程被塞进这里。
二、 内存可见性模型:为什么 isNum 不需要 volatile?
你可能会奇怪:isNum 是一个普通的 static boolean 变量,没有加 volatile,为什么两个线程能绝对实时地看到它的变化?
这是因为 Lock(AQS)的内存屏障机制(Happens-Before 规则):
- 当线程
t1执行printChar.signal()或显式释放锁时,底层会触发布局内存屏障,将工作内存中的变量(isNum=false)强行刷新回主内存。 - 当线程
t2成功获取锁(不管是通过lock.lock()还是从await()中醒来重新获得锁),底层内存屏障会使t2的缓存行失效,强行从主内存重新读取最新值。 - 因此,在
lock保护的临界区内,普通变量也具备绝对的可见性。
三、 架构级核心演进流程(ASCII 流程图)
注意你的代码特征:lock.lock() 写在 for 循环外面。这意味着线程只要拿到锁,不遇到 await() 是绝对不会交出锁的。
我们来可视化整个演进过程(以 i = 1 i=1 i=1 到 i = 2 i=2 i=2 的交替为例):
阶段 1:初始状态(t1 率先抢到锁)
t1 抢到锁,成为 Holder。t2 执行 lock.lock() 失败,进入 AQS 同步队列挂起。
主内存状态: [ isNum = true ]
+-------------------------------------------------------------+
| ReentrantLock (AQS) |
| [Lock Holder]: Thread-t1 |
| [AQS Sync Q]: Head <-> [ Thread-t2 ] <-> Tail |
+-------------------------------------------------------------+
+-------------------------+ +-------------------------+
| printNum 条件队列 | | printChar 条件队列 |
| [ 空 ] | | [ 空 ] |
+-------------------------+ +-------------------------+
- 执行动作:
t1发现isNum == true,跳过while。打印1,将isNum设为false。调用printChar.signal()(此时 char 队列为空,什么都不发生)。
阶段 2:t1 进入第二轮,主动释放锁并发生「线程迁移」
t1 循环进入 i = 2 i=2 i=2。此时 isNum == false,满足 !isNum 条件,t1 执行 printNum.await()。
💡 架构级魔幻操作发生:
t1释放锁,从 Holder 位置脱离,化身为一个节点,移入printNum条件队列;同时,AQS 同步队列头部的t2被唤醒,晋升为 Holder。
主内存状态: [ isNum = false ]
+-------------------------------------------------------------+
| ReentrantLock (AQS) |
| [Lock Holder]: Thread-t2 (刚被唤醒) |
| [AQS Sync Q]: Head <-> Tail (空了) |
+-------------------------------------------------------------+
+-------------------------+ +-------------------------+
| printNum 条件队列 | | printChar 条件队列 |
| [ Thread-t1 ] -> null | | [ 空 ] |
+-------------------------+ +-------------------------+
^
| (t1 await 降落到这里)
阶段 3:t2 执行,利用 signal 搬运线程
t2 成为 Holder 正常执行。isNum 此时为 false,跳过 while。打印 A,将 isNum 设为 true。紧接着 t2 调用 printNum.signal()。
💡 架构级魔幻操作再次发生:
signal()不会唤醒线程!它的作用是搬运。底层的 AQS 将printNum条件队列头部的t1节点弹出来,重新塞回 AQS 同步队列的尾部,让它重新排队等锁。
主内存状态: [ isNum = true ]
+-------------------------------------------------------------+
| ReentrantLock (AQS) |
| [Lock Holder]: Thread-t2 |
| [AQS Sync Q]: Head <-> [ Thread-t1 ] <-> Tail (回来排队了) |
+-------------------------------------------------------------+
^
| (被 t2.signal() 从下面搬上来)
+-------------------------+ | +-------------------------+
| printNum 条件队列 |---+ | printChar 条件队列 |
| [ 空 ] (变空了) | | [ 空 ] |
+-------------------------+ +-------------------------+
阶段 4:t2 进入第二轮,重演「角色互换」
t2 循环进入 i = 2 i=2 i=2。此时 isNum == true,满足 t2 的 while(isNum) 条件,t2 执行 printChar.await()。
t2释放锁,降落到printChar条件队列。AQS 同步队列里的t1顶上,重新成为 Holder。
主内存状态: [ isNum = true ]
+-------------------------------------------------------------+
| ReentrantLock (AQS) |
| [Lock Holder]: Thread-t1 (重新执掌大权) |
| [AQS Sync Q]: Head <-> Tail |
+-------------------------------------------------------------+
+-------------------------+ +-------------------------+
| printNum 条件队列 | | printChar 条件队列 |
| [ 空 ] | | [ Thread-t2 ] -> null |
+-------------------------+ +-------------------------+
^
| (t2 await 降落到这里)
此时 t1 满血复活,从刚才挂起的 printNum.await() 位置苏醒并直接向下执行,打印 2,周而复始。
四、 核心总结
Condition不是变量,是节点的接驳站:
await()= 卸下锁所有权 + 把自己关进当前 Condition 的单向专属小黑屋。signal()= 把对方从 Condition 的小黑屋里释放出来,重新扔回 AQS 双向同步大队中去排队抢锁。
while(!isNum)是安全阀门:
为了防止线程被虚假唤醒或抢锁失败,每次从await()睁开眼(阶段 4 的 t1),都要顺着while往上看一眼主内存的isNum到底是不是老天爷安排给我的信号。如果是,才放心往下打印。
为什么signal()不是直接摘出条件队列头结点让他去竞争锁?而是要放到同步队列尾部??
这个问题问得极其深入,直接一脚踩中了操作系统线程调度、CPU 上下文切换开销以及面向对象架构中“单一职责原则”的核心战场。
如果直接让它醒过来去盲目竞争锁,整个 Java 并发包的性能和设计优雅度都会遭到毁灭性打击。Doug Lea(AQS 的作者)之所以选择“先扔到同步队列尾部排队”,主要基于以下三个不可动摇的底层原因:
原因一:致命的“尴尬苏醒”与 CPU 浪费(最核心原因)
请紧跟代码的执行顺序:在你的代码里,t2 是先调用 printNum.signal(),然后才执行到 finally 块里的 lock.unlock()。
这意味着:在 signal() 被调用的一瞬间,当前线程 t2 依然死死地攥着锁呢!
- 假设按你说的,直接唤醒
t1去竞争锁:t1迫不及待地在 CPU 中苏醒,大喊一声“我要抢锁!”。结果转头一看,发现t2还没释放锁。t1抢锁失败,只能认栽,再次调用操作系统的底层的park()让自己重新睡过去。 - 代价是什么:
这导致t1经历了一次毫无意义的 “苏醒 → \rightarrow → 抢锁失败 → \rightarrow → 重新挂起” 的过程。在操作系统中,线程的唤醒和挂起需要经历用户态到内核态的切换,极为消耗 CPU 算力。这种现象在并发工程中被称为“惊群/无效上下文切换(Context Switch Overhead)”。 - AQS 的聪明做法:
signal()只是悄悄把你挪到同步队列的尾部排队,并不叫醒你。等到t2真正执行unlock()时,AQS 才会顺着同步队列去唤醒下一个该醒来的线程。此时锁已经空出来了,醒来的线程一气呵成拿到锁,绝不浪费一次 CPU 资源。
原因二:架构层面的“单一职责”与代码复用
AQS 的同步队列(Sync Queue)是一个经过极致优化的、极其复杂的状态机。它内部要处理:
- 线程抢锁失败后的自旋(Spin)
- 线程在排队时如果被外部
interrupt()中断了该怎么办? - 线程设置了超时时间(Timeout)到期了怎么退出队列?
- 公平锁与非公平锁的抢占逻辑。
如果让条件队列的线程直接去竞争锁,意味着 Doug Lea 必须在 Condition 内部把上述所有关于中断、超时、公平性的复杂代码全部重写复制一遍。
AQS 的架构解耦思路是:
Condition 只需要管好自己的一亩三分地(只负责装载因为条件不满足而等待的线程)。一旦条件满足了(signal),它就把线程打包,转交给专业的“锁管理大队”(AQS 同步队列)。这样不仅实现了 100% 的代码复用,更保证了锁核心逻辑的严密和安全。
原因三:为了维护“公平锁”的先来后到原则(FIFO)
如果你的 ReentrantLock 配置成了公平锁(Fair Lock):
此时,AQS 的同步队列里可能已经有 t3、t4 在老老实实排队等锁了。如果 t2 调用 signal() 时直接把 t1 丢去竞争锁,这就相当于允许 t1 插队。这不仅打破了同步队列的 FIFO(先进先出)公平性,还会导致原本排队的 t3、t4 产生线程饥饿。
通过把 t1 挪到同步队列的尾部,t1 就能顺理成章地排在 t3、t4 的后面,完美捍卫了队列的公平秩序。
💡 终极总结
- 条件队列是“冷宫”,里面的人连竞争锁的资格都没有。
signal()是发配大赦令,把你从“冷宫”提拔到“正规军排队大队”(同步队列尾部)。unlock()才是军号,按照排队顺序,真正吹醒(unpark)排在最前面的人去执掌大权。
这种各司其职的设计,让 Java 的锁在即便面对成千上万个线程的高并发冲击时,依然能表现得井然有序、极其高效。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)