前言:核心代码

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();
    }

}

要从架构层面彻底搞懂 ConditionprintNumprintChar)在内存和队列中的变化模型,我们需要扒开 Java 并发包(JUC)的底层,直面 AQS(AbstractQueuedSynchronizer) 的核心架构。

你觉得模糊,是因为你把 Condition 误看作了一个单纯的“内存变量”。实际上,每一个 Condition 本质上都是一个独立的、由底层管理的「单向等待队列」

下面从内存模型、底层架构、以及核心流程可视化三个层面为你深度拆解。


一、 底层架构模型:三大队列与锁持有者

在你的代码中,lock.lock() 触达的底层世界由一个核心状态三个队列组成:

  1. Lock Holder(当前锁持有者):指针,指向当前独占 CPU 执行权的线程。
  2. AQS 同步队列(Sync Queue):一个双向链表。没有抢到锁的线程,在这个队列里排队挂起,等待惊醒去争抢锁。
  3. printNum 条件队列Condition 内部的一个单向链表。调用 printNum.await() 的线程会释放锁,并被塞进这里。
  4. 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,满足 t2while(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,周而复始。


四、 核心总结

  1. Condition 不是变量,是节点的接驳站
  • await() = 卸下锁所有权 + 把自己关进当前 Condition 的单向专属小黑屋。
  • signal() = 把对方从 Condition 的小黑屋里释放出来,重新扔回 AQS 双向同步大队中去排队抢锁。
  1. 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 的同步队列里可能已经有 t3t4 在老老实实排队等锁了。如果 t2 调用 signal() 时直接把 t1 丢去竞争锁,这就相当于允许 t1 插队。这不仅打破了同步队列的 FIFO(先进先出)公平性,还会导致原本排队的 t3t4 产生线程饥饿。

通过把 t1 挪到同步队列的尾部t1 就能顺理成章地排在 t3t4 的后面,完美捍卫了队列的公平秩序。


💡 终极总结

  • 条件队列是“冷宫”,里面的人连竞争锁的资格都没有。
  • signal() 是发配大赦令,把你从“冷宫”提拔到“正规军排队大队”(同步队列尾部)。
  • unlock() 才是军号,按照排队顺序,真正吹醒(unpark)排在最前面的人去执掌大权。

这种各司其职的设计,让 Java 的锁在即便面对成千上万个线程的高并发冲击时,依然能表现得井然有序、极其高效。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐