关键词:Java并发编程、Condition接口、Lock锁、等待/通知机制、线程同步、AQS、生产者-消费者模型
摘要:在Java并发编程中,线程同步是绕不开的核心问题。传统的
synchronized
配合wait/notify
机制虽然能实现基本的线程协作,但存在“无法精确唤醒特定线程”的短板。本文将深入解析Java中的Condition
接口——这个被称为“升级版等待/通知机制”的并发工具,通过生活类比、原理拆解、代码实战,带您理解它为何能成为复杂线程同步场景的“瑞士军刀”。
本文聚焦Java并发编程中的Condition
接口,重点讲解其核心作用、实现原理及实战应用。我们将从传统wait/notify
的局限性出发,逐步拆解Condition
的设计逻辑,最终通过“有界阻塞队列”等经典案例,展示其在真实项目中的价值。
synchronized
关键字)的开发者;ReentrantLock
有一定使用经验,但对Condition
的具体作用和优势感到困惑的学习者。本文将按照“问题引入→核心概念→原理拆解→实战演练→场景扩展”的逻辑展开:首先通过生活案例说明传统同步机制的不足;然后用“候车室”类比解释Condition
的核心功能;接着结合AQS(抽象队列同步器)讲解其底层实现;最后通过代码实战展示如何用Condition
解决复杂同步问题。
java.util.concurrent
)中的接口,提供与Object.wait()
/Object.notify()
类似的等待/通知功能,但支持更灵活的多条件队列管理。ReentrantLock
),替代synchronized
的可中断、可超时、可公平的锁实现。ReentrantLock
和Condition
均基于其实现,管理同步状态和线程队列。Condition.await()
后的线程)。Lock
失败的线程)。wait()
或await()
中苏醒的现象(需通过循环检查条件避免)。假设你在学校食堂打工,负责管理两个窗口:一个是“热菜窗口”,一个是“汤品窗口”。中午高峰期,学生们会围在窗口前排队。
synchronized
+wait/notify
):所有学生都挤在一个大队伍里。当热菜做好时,你只能大喊“所有人过来!”,结果汤品窗口的学生也会涌过来,导致混乱;Lock
+Condition
):你为热菜和汤品分别设置了“专用候餐区”(Condition
)。当热菜做好时,你只需要去热菜候餐区喊“热菜好了!”,只有等待热菜的学生会过来;汤品做好时,去汤品候餐区通知,精准又高效。这就是Condition
的核心价值:为不同的等待条件创建独立的“候餐区”(等待队列),实现线程的精准唤醒。
Lock
是Java并发包提供的锁接口(常用实现是ReentrantLock
),可以理解为“线程的门钥匙”。只有拿到钥匙(调用lock()
)的线程,才能进入“房间”(执行同步代码块);用完后必须归还钥匙(调用unlock()
),其他线程才能继续使用。
相比synchronized
的“傻锁”(要么拿到要么等待),Lock
更灵活:可以尝试超时获取(tryLock(long timeout, TimeUnit unit)
),可以响应中断(lockInterruptibly()
),还能创建多个Condition
(后文重点)。
Condition
可以理解为Lock
的“附属候餐区”。一个Lock
可以创建多个Condition
(比如热菜候餐区、汤品候餐区),每个Condition
管理一个独立的等待队列。
当线程需要等待某个特定条件(比如“热菜已做好”)时,它可以调用Condition.await()
:先归还Lock
钥匙(释放锁),然后进入该Condition
的候餐区睡觉;
当其他线程让条件满足(比如“热菜做好了”),可以调用Condition.signal()
(唤醒一个候餐区的线程)或Condition.signalAll()
(唤醒所有候餐区的线程),让睡觉的线程苏醒,重新竞争Lock
钥匙,继续执行。
Condition
管理):线程因调用await()
而进入的“睡觉区”,等待特定条件满足;Lock
管理):线程因竞争Lock
失败而进入的“排队区”,等待获取锁。可以想象成医院的“候诊流程”:
Lock
、Condition
、等待队列的关系可以总结为:Lock
是总管理处,Condition
是分候诊区,等待队列是分候诊区的座位。
Lock
)可以设立多个分候诊区(Condition
),比如“热菜区”“汤品区”。总管理处负责发号(锁),分候诊区负责管理特定条件的等待线程;Condition
)有自己的座位(等待队列),线程进入候诊区时会坐在对应的座位上(加入等待队列),被通知时离开座位(从等待队列移除);Lock(总管理处)
├─ 同步队列(大厅排队区):等待获取锁的线程
└─ Condition1(热菜候诊区)
│ └─ 等待队列1(热菜座位):等待热菜条件的线程
└─ Condition2(汤品候诊区)
└─ 等待队列2(汤品座位):等待汤品条件的线程
graph TD
A[线程A获取Lock锁] --> B{条件是否满足?}
B -->|不满足| C[调用Condition.await()]
C --> D[释放Lock锁]
D --> E[线程A进入Condition的等待队列]
E --> F[线程A进入等待状态]
F --> G[线程B获取Lock锁]
G --> H[修改条件(如热菜做好)]
H --> I[调用Condition.signal()]
I --> J[将线程A从等待队列移到同步队列]
J --> K[线程B释放Lock锁]
K --> L[线程A从同步队列竞争锁]
L --> M[线程A重新获取锁,继续执行]
Java的Condition
接口默认实现类是AbstractQueuedSynchronizer.ConditionObject
(AQS的内部类)。其核心原理是通过AQS管理两个队列:同步队列(Sync Queue)和等待队列(Wait Queue)。
await():
Lock
锁(否则抛IllegalMonitorStateException
);Node
节点,加入Condition
的等待队列;Lock
锁(即修改AQS的同步状态);LockSupport.park()
);Lock
锁,若成功则从await()
返回。signal():
Lock
锁;Condition
的等待队列中取出第一个节点(最早等待的线程);Lock
的同步队列中;signalAll():
与signal()
类似,但会将等待队列中的所有节点转移到同步队列(相当于清空等待队列)。
// ConditionObject(AQS内部类)的await()简化逻辑
public final void await() throws InterruptedException {
if (Thread.interrupted()) throw new InterruptedException();
Node node = addConditionWaiter(); // 将当前线程包装为Node,加入等待队列
int savedState = fullyRelease(node); // 释放当前锁(返回原同步状态)
int interruptMode = 0;
while (!isOnSyncQueue(node)) { // 检查节点是否在同步队列中(未被signal)
LockSupport.park(this); // 线程挂起(等待)
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState)) // 重新竞争锁
interruptMode = THROW_IE;
if (node.nextWaiter != null) // 清理已取消的节点
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
// ConditionObject的signal()简化逻辑
public final void signal() {
if (!isHeldExclusively()) // 检查当前线程是否持有锁
throw new IllegalMonitorStateException();
Node first = firstWaiter; // 取出等待队列的头节点
if (first != null)
doSignal(first); // 转移节点到同步队列
}
private void doSignal(Node first) {
do {
if ((firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null; // 从等待队列断开
} while (!transferForSignal(first) && (first = firstWaiter) != null);
}
// 将节点从等待队列转移到同步队列
final boolean transferForSignal(Node node) {
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
Node p = enq(node); // 将节点加入同步队列尾部
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread); // 唤醒线程(若前驱节点已取消)
return true;
}
虽然Condition
的核心是队列操作,不涉及复杂数学公式,但可以用“状态转移”模型描述线程的行为:
线程在Condition
协作中的状态变化可表示为:
运行状态 → 调用 a w a i t ( ) 等待状态(等待队列) → 调用 s i g n a l ( ) 阻塞状态(同步队列) → 获取锁 运行状态 \text{运行状态} \xrightarrow{调用await()} \text{等待状态(等待队列)} \xrightarrow{调用signal()} \text{阻塞状态(同步队列)} \xrightarrow{获取锁} \text{运行状态} 运行状态调用await()等待状态(等待队列)调用signal()阻塞状态(同步队列)获取锁运行状态
举例说明:
假设一个线程T1正在执行put()
方法(向有界队列添加元素),当队列已满时,T1调用notFull.await()
:
notFull
的等待队列(等待状态);take()
方法(从队列取出元素),取出后队列不满,T2调用notFull.signal()
;notFull
等待队列转移到同步队列(阻塞状态),等待获取锁;await()
返回,继续执行put()
操作(运行状态)。Condition
接口在JDK1.5引入);java.util.concurrent.locks
包)。我们将用ReentrantLock
和Condition
实现一个经典的“有界阻塞队列”(Bounded Blocking Queue)。队列有固定容量,当队列满时,put()
方法阻塞;当队列空时,take()
方法阻塞。
import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class BoundedBlockingQueue<T> {
private final LinkedList<T> queue = new LinkedList<>(); // 存储元素的链表
private final int capacity; // 队列最大容量
private final ReentrantLock lock = new ReentrantLock(); // 全局锁
private final Condition notFull = lock.newCondition(); // 队列未满的条件
private final Condition notEmpty = lock.newCondition(); // 队列非空的条件
public BoundedBlockingQueue(int capacity) {
this.capacity = capacity;
}
// 向队列添加元素(队列满时阻塞)
public void put(T element) throws InterruptedException {
lock.lock(); // 获取锁
try {
// 循环检查条件(避免虚假唤醒)
while (queue.size() == capacity) {
notFull.await(); // 队列已满,等待“未满”条件
}
queue.addLast(element); // 添加元素到队尾
notEmpty.signal(); // 唤醒等待“非空”条件的线程
} finally {
lock.unlock(); // 释放锁(必须在finally中执行)
}
}
// 从队列取出元素(队列空时阻塞)
public T take() throws InterruptedException {
lock.lock(); // 获取锁
try {
// 循环检查条件(避免虚假唤醒)
while (queue.isEmpty()) {
notEmpty.await(); // 队列空,等待“非空”条件
}
T element = queue.removeFirst(); // 取出队首元素
notFull.signal(); // 唤醒等待“未满”条件的线程
} finally {
lock.unlock(); // 释放锁
}
return element;
}
// 查看当前队列大小(仅用于演示)
public int size() {
lock.lock();
try {
return queue.size();
} finally {
lock.unlock();
}
}
}
锁与条件的创建:
ReentrantLock
是可重入锁,用于保护队列的所有操作;notFull
和notEmpty
是通过lock.newCondition()
创建的两个Condition
,分别管理“队列未满”和“队列非空”的等待线程。put()方法:
while (queue.size() == capacity)
)。这里必须用while
而非if
,因为可能发生虚假唤醒(线程被唤醒时条件可能仍不满足);notFull.await()
:释放锁,进入notFull
的等待队列;notEmpty.signal()
:唤醒一个等待notEmpty
条件的线程(比如被阻塞的take()
线程)。take()方法:
put()
对称:循环检查队列是否为空,若空则调用notEmpty.await()
;notFull.signal()
:唤醒一个等待notFull
条件的线程(比如被阻塞的put()
线程)。线程安全的保障:
所有对队列的操作(addLast
、removeFirst
、size
)都在lock
的保护下,确保同一时间只有一个线程修改队列状态,避免竞态条件。
Condition
在需要“多条件精确唤醒”的场景中优势显著,常见应用包括:
生产者(put()
)和消费者(take()
)需要根据“队列满”和“队列空”两个不同条件阻塞,Condition
的notFull
和notEmpty
可精准唤醒对应线程。
当连接池无空闲连接时,请求线程需要等待(await()
);当连接被释放时,释放线程调用signal()
唤醒等待线程。
例如,任务A需要等待任务B和任务C完成后才能执行,可通过两个Condition
分别监控B和C的完成状态。
线程池中的工作线程在无任务时等待(await()
),当新任务提交时,调用signal()
唤醒线程处理任务。
Condition
的设计和使用;Condition
的底层实现。jstack
:用于查看线程堆栈,定位await()
阻塞的线程;Java 19引入的虚拟线程(Project Loom)旨在降低线程开销,支持百万级线程并发。Condition
作为线程同步工具,未来可能需要优化与虚拟线程的协作,例如减少上下文切换开销。
随着微服务、分布式系统的普及,单机Condition
无法解决跨进程的同步问题。未来可能结合Distributed Lock
(如Redis RedLock)和分布式Condition
(如基于Raft协议的条件通知),实现跨节点的精确唤醒。
Condition
的强大功能也带来了更高的使用门槛。常见错误包括:
finally
中释放锁(导致死锁);if
代替while
检查条件(导致虚假唤醒后逻辑错误);Condition
混用(如用notFull.signal()
唤醒notEmpty
的等待线程)。Lock
的“专用候餐区”,每个Condition
管理独立的等待队列;Condition
的等待队列用于管理特定条件的等待线程,Lock
的同步队列用于管理锁竞争的线程。Lock
是总管理处,负责发放和回收钥匙(锁);Condition
是分候诊区,每个候诊区对应一个等待条件;生活类比题:你能想到生活中还有哪些场景需要“多条件精准唤醒”?试着用Condition
的模型描述它(例如:健身房的淋浴间,当有空闲淋浴头时唤醒等待的用户)。
代码实战题:修改本文的BoundedBlockingQueue
,添加一个drainTo(Collection
方法,将队列中的最多maxElements
个元素转移到target
集合中(队列空时阻塞)。需要使用Condition
实现。
原理思考题:为什么Condition.await()
必须在Lock
的保护下调用?如果不在锁保护下调用会发生什么?
Q1:Condition
和Object.wait()
/Object.notify()
有什么区别?
A:主要区别有三点:
Lock
可以创建多个Condition
(多等待队列),而一个Object
只能有一个等待队列(所有线程共享);Condition.signal()
可以唤醒特定条件的线程,而Object.notify()
只能随机唤醒一个线程;Condition
支持超时等待(await(long time, TimeUnit unit)
)、可中断等待(awaitInterruptibly()
),而Object.wait()
的中断支持较弱。Q2:为什么await()
需要在循环中检查条件?
A:因为可能发生虚假唤醒(Spurious Wakeup):即使没有收到signal()
,线程也可能从await()
中苏醒。虽然概率低,但Java规范允许这种情况。通过循环检查条件(while
而非if
),可以确保线程苏醒时条件确实满足。
Q3:signal()
和signalAll()
应该如何选择?
A:优先使用signal()
,因为它只唤醒一个线程,减少竞争;只有当多个线程的等待条件都满足时(例如所有等待线程的条件都因同一事件满足),才使用signalAll()
(如关闭队列时唤醒所有等待线程)。