侧边栏壁纸
博主头像
牧云

怀璧慎显,博识谨言。

  • 累计撰写 198 篇文章
  • 累计创建 19 个标签
  • 累计收到 8 条评论

目 录CONTENT

文章目录

Java 线程协作全景图:从 wait/notify 到 Condition 再到 LockSupport

秋之牧云
2026-05-14 / 0 评论 / 0 点赞 / 1 阅读 / 0 字

在 Java 并发编程中,线程协作(Thread Cooperation) 是让多个线程有序执行、避免资源竞争的核心机制。很多开发者熟悉 wait()/notify(),但在面对复杂场景时,往往对 Conditionawait()/signal() 以及底层的 LockSupport.park()/unpark() 感到困惑。

这三者究竟是什么关系?线程在等待时到底去了哪里?唤醒后又是如何重新获取锁的?本文将通过层层递进的方式,结合数据结构图解源码逻辑,为你彻底梳理 Java 线程协作的底层脉络。


第一层:基础协作 —— Object 的 wait() / notify()

这是 Java 最原始的线程协作方式,基于 synchronized 关键字和对象监视器(Monitor)。

1. 核心机制:单队列模型

在 HotSpot JVM 中,每个对象头都关联一个 Monitor。Monitor 内部主要维护两个队列:

  • Entry List(入口队列/阻塞队列):竞争锁失败的线程在此排队,状态为 BLOCKED
  • Wait Set(等待集合):调用 wait() 的线程在此排队,状态为 WAITING

2. 线程流转过程

  1. 等待:线程调用 obj.wait(),释放锁,从 Owner 身份退出,进入 Wait Set
  2. 通知:其他线程调用 obj.notify(),JVM 从 Wait Set 中随机选择一个线程。
  3. 转移:被选中的线程从 Wait Set 移动到 Entry List,状态变为 BLOCKED
  4. 竞争:该线程必须与其他在 Entry List 中的线程重新竞争锁,只有获取锁后,wait() 方法才会返回。

痛点:由于只有一个 Wait Set,notify() 无法区分唤醒的是“生产者”还是“消费者”。为了安全,通常被迫使用 notifyAll(),导致所有等待线程都被唤醒并竞争锁,造成“惊群效应”,性能低下。


第二层:精准协作 —— Lock 与 Condition

为了解决上述痛点,JUC 包提供了 ReentrantLockCondition。这是现代 Java 并发编程的推荐方案

1. 核心突破:多等待队列

一个 ReentrantLock 可以创建多个 Condition 对象。每个 Condition 维护一个独立的等待队列

  • 隔离性:生产者等待在 notFull 队列,消费者等待在 notEmpty 队列。
  • 精准唤醒signal() 只唤醒特定队列的头节点,互不干扰。

2. 代码实战:生产者-消费者模型

import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionDemo {
    private final LinkedList<Integer> buffer = new LinkedList<>();
    private final int CAPACITY = 5;
    private final ReentrantLock lock = new ReentrantLock();
    
    // 创建两个独立的条件对象
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();

    public void produce(int item) throws InterruptedException {
        lock.lock();
        try {
            // 缓冲区满,进入 notFull 队列等待
            while (buffer.size() == CAPACITY) {
                notFull.await(); 
            }
            buffer.add(item);
            System.out.println("生产: " + item);
            // 精准唤醒消费者
            notEmpty.signal(); 
        } finally {
            lock.unlock();
        }
    }

    public void consume() throws InterruptedException {
        lock.lock();
        try {
            // 缓冲区空,进入 notEmpty 队列等待
            while (buffer.isEmpty()) {
                notEmpty.await();
            }
            int item = buffer.removeFirst();
            System.out.println("消费: " + item);
            // 精准唤醒生产者
            notFull.signal();
        } finally {
            lock.unlock();
        }
    }
}

第三层:底层揭秘 —— AQS 的双队列架构

Condition 的高效源于其底层 AQS (AbstractQueuedSynchronizer) 的精妙设计。理解线程在队列间的物理移动是掌握并发原理的关键。

1. 数据结构:Node 的复用与独立链表

AQS 并没有为 Condition 创建新的节点类,而是复用了内部的 Node 类,但维护了两条独立的链表:

特性AQS 同步队列 (Sync Queue)Condition 等待队列 (Condition Queue)
作用竞争锁失败的线程排队调用 await() 的线程排队
结构双向链表 (prev/next)单向链表 (nextWaiter)
头节点有虚拟 Head 节点无虚拟头,firstWaiter 指向首元素
状态标记waitStatus 为 0, SIGNAL, CANCELLEDwaitStatus 强制为 CONDITION (-2)

2. 线程流转图解:从 await 到 signal

线程并不是在两个队列间“瞬间跳跃”,而是经历了移除添加的物理过程。

graph LR
    subgraph Condition_Queue [Condition 等待队列]
        C1[Node: CONDITION] --> C2[Node: CONDITION]
    end

    subgraph Sync_Queue [AQS 同步队列]
        S_Head[Head] <--> S1[Node: SIGNAL] <--> S2[Node: 0]
    end

    C1 -- signal() 触发转移 --> S2
    
    style C1 fill:#f9f,stroke:#333,stroke-width:2px
    style S2 fill:#ccf,stroke:#333,stroke-width:2px

详细流转步骤:

  1. await():进入 Condition 队列
    • 线程释放锁。
    • 封装成 Node (waitStatus = CONDITION)。
    • 尾插入 Condition 队列
    • 调用 LockSupport.park() 阻塞
  2. signal():转移到同步队列
    • 找到 Condition 队列的头节点。
    • 关键动作:将该节点从 Condition 队列移除
    • 调用 enq(node) 将该节点添加AQS 同步队列 尾部。
    • 修改 waitStatus0SIGNAL
    • 注意:此时线程依然处于 park 阻塞状态,只是换了个队列排队。
  3. acquireQueued():竞争锁
    • 当同步队列的前驱节点释放锁后,unpark 当前节点。
    • 线程被唤醒,尝试获取锁。
    • 获取成功后,成为 Sync Queue 的新 Head,await() 返回。

第四层:原子基石 —— LockSupport 的 park() / unpark()

无论是 wait/notify 还是 Condition,底层最终都依赖 LockSupport 来挂起和恢复线程。

1. 核心原理:许可(Permit)机制

  • park():如果 permit 为 0,线程阻塞;如果为 1,消费 permit 并立即返回。
  • unpark(Thread t):将线程 t 的 permit 设为 1。如果 t 正在阻塞,则立即唤醒。

2. 为什么它是基石?

  • 无需持锁:可以在任何地方调用,不像 wait 必须在 synchronized 块中。
  • 顺序无关:可以先 unparkpark,信号不会丢失。这使得它在实现复杂的非阻塞算法时非常灵活。
  • 精准指向unpark 必须指定具体的线程对象,天然支持精准唤醒。

总结与对比

维度wait / notifyCondition (await/signal)LockSupport (park/unpark)
层级高级 API (JVM 内置)高级 API (JUC 框架)底层原语 (Unsafe)
锁依赖必须持有 synchronized 锁必须持有 Lock 锁无需任何锁
队列数量每对象 1 个 Wait Set每 Lock N 个 Condition 队列无队列概念,基于 Permit
唤醒精度随机或全部精准指定条件队列精准指定线程
适用场景简单同步复杂业务协作(推荐)编写底层并发工具
  1. 日常开发:优先使用 ReentrantLock + Condition,利用多队列实现高效、精准的线程协作。
  2. 源码阅读:重点关注 ConditionObjectawait()signal() 如何将 Node 在两个队列间转移,这是理解 AQS 的钥匙。
  3. 底层理解:记住 park/unpark 是基于 Permit 的原子操作,它是所有上层同步工具的“发动机”。

通过理解这四层结构,你不仅能写出更高效的并发代码,更能透过现象看本质,掌握 Java 并发编程的灵魂。

0

评论区