Java面试-Condition 接口与 await()/signal() 方法
Java并发编程中的Condition接口与await()/signal()方法详解 本文深入解析Java并发编程中的Condition接口及其核心方法await()和signal()。Condition作为Lock的配套工具,提供了比传统Object.wait()/notify()更灵活的线程通信机制。文章通过交通信号灯类比,形象说明了Condition的多路通知优势,并详细剖析了await()
👋 欢迎阅读《Java面试200问》系列博客!
🚀大家好,我是Jinkxs,一名热爱Java、深耕技术一线的开发者。在准备和参与了数十场Java面试后,我深知面试不仅是对知识的考察,更是对理解深度与表达能力的综合检验。
✨本系列将带你系统梳理Java核心技术中的高频面试题,从源码原理到实际应用,从常见陷阱到大厂真题,每一篇文章都力求深入浅出、图文并茂,帮助你在求职路上少走弯路,稳拿Offer!
🔍今天我们要聊的是:《Condition 接口与 await()/signal() 方法》。准备好了吗?Let’s go!
🧰 Condition 接口与 await()/signal() 方法:Java 并发中的“信号灯大师”
“如果说
synchronized
是原始的‘打手势’,那Condition
就是现代交通中的‘红绿灯系统’——精准、可控、多路并行!”欢迎阅读《Condition 接口与 await/signal 方法的深度解析》—— 专为 Java 高并发面试打造的“线程通信指南”。我们将用生动的比喻、丰富的代码示例、直观的图表和深度源码剖析,带你彻底掌握
Condition
如何优雅地实现线程间的等待与唤醒。本文目录:
1. 前言:为什么我们需要“信号灯”?
🚶♂️ 场景一:原始社会“打手势”(Object 的 wait/notify)
- 规则:一个人喊“等会儿!”,另一个人喊“好了!”,但谁喊谁听,全靠默契。
- 缺点:混乱、不可控、容易误唤醒。
🚦 场景二:现代交通“红绿灯”(Condition)
- 规则:多个方向有独立的红绿灯,左转、直行、右转各走各的。
- 优点:精准、高效、可扩展。
在 Java 并发中:
Object.wait()
/notify()
→ 原始“打手势”Condition.await()
/signal()
→ 现代“红绿灯”
一句话定义:
Condition
是java.util.concurrent.locks
包下的接口,它允许线程在某个条件下挂起(await
),并在另一个线程中唤醒(signal
),与Lock
配合使用,提供比synchronized
更灵活的线程通信机制。
2. Condition 接口基础
📦 获取 Condition 实例
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionDemo {
private final ReentrantLock lock = new ReentrantLock();
private final Condition condition = lock.newCondition(); // 通过 Lock 创建 Condition
}
关键:一个
Lock
可以创建多个Condition
实例,实现多路通知。
📊 Condition 接口主要方法
方法 | 描述 |
---|---|
void await() |
当前线程进入等待状态,释放锁,直到被 signal 或中断 |
void signal() |
唤醒一个等待中的线程 |
void signalAll() |
唤醒所有等待中的线程 |
long awaitNanos(long nanosTimeout) |
超时等待(纳秒) |
boolean awaitUntil(Date deadline) |
等待到指定时间 |
boolean await(long time, TimeUnit unit) |
超时等待(指定单位) |
3. await() 与 signal() 方法详解
🔄 await() 方法流程
┌─────────────────────┐
│ 调用 await() │
└─────────────────────┘
│
┌─────────▼─────────┐
│ 释放当前持有的锁 │
└─────────┬─────────┘
│
┌─────────▼─────────┐
│ 当前线程进入 Condition │
│ 等待队列(阻塞) │
└─────────┬─────────┘
│
┌─────────▼─────────┐
│ 被 signal() 唤醒 │
└─────────┬─────────┘
│
┌─────────▼─────────┐
│ 重新尝试获取锁 │
└─────────┬─────────┘
│
┌─────────▼─────────┐
│ 获取锁成功,继续执行 │
└─────────────────────┘
🔄 signal() 方法流程
┌─────────────────────┐
│ 调用 signal() │
└─────────────────────┘
│
┌─────────▼─────────┐
│ 从 Condition 等待队列 │
│ 中唤醒一个线程 │
└─────────┬─────────┘
│
┌─────────▼─────────┐
│ 被唤醒的线程尝试重新 │
│ 获取锁(此时仍持有锁)│
└─────────┬─────────┘
│
┌─────────▼─────────┐
│ signal() 返回,继续执行 │
└─────────────────────┘
🧪 代码演示:基础 await/signal
public class BasicConditionDemo {
private final ReentrantLock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private boolean isReady = false;
public void waitForReady() throws InterruptedException {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 等待准备就绪...");
while (!isReady) {
condition.await(); // 释放锁,进入等待
}
System.out.println(Thread.currentThread().getName() + " 被唤醒,继续执行!");
} finally {
lock.unlock();
}
}
public void setReady() {
lock.lock();
try {
System.out.println("准备就绪,发出信号!");
isReady = true;
condition.signal(); // 唤醒一个等待线程
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
BasicConditionDemo demo = new BasicConditionDemo();
Thread waiter = new Thread(demo::waitForReady, "waiter-thread");
waiter.start();
Thread.sleep(1000);
Thread setter = new Thread(demo::setReady, "setter-thread");
setter.start();
waiter.join();
setter.join();
}
}
📊 输出
waiter-thread 等待准备就绪...
准备就绪,发出信号!
waiter-thread 被唤醒,继续执行!
关键点:
await()
会释放锁,否则setter
线程无法获取锁来调用setReady()
。- 使用
while
而不是if
检查条件,防止虚假唤醒。
4. 生产者-消费者模型实战
🏭 经典问题
- 生产者生产数据,放入缓冲区。
- 消费者从缓冲区取数据。
- 缓冲区满时,生产者等待。
- 缓冲区空时,消费者等待。
🧪 代码实现
import java.util.LinkedList;
import java.util.Queue;
public class ProducerConsumer {
private final Queue<Integer> buffer = new LinkedList<>();
private final int MAX_SIZE = 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 {
while (buffer.size() == MAX_SIZE) {
System.out.println("缓冲区满,生产者等待...");
notFull.await();
}
buffer.offer(item);
System.out.println("生产者生产: " + item);
notEmpty.signal(); // 唤醒消费者
} finally {
lock.unlock();
}
}
public int consume() throws InterruptedException {
lock.lock();
try {
while (buffer.isEmpty()) {
System.out.println("缓冲区空,消费者等待...");
notEmpty.await();
}
int item = buffer.poll();
System.out.println("消费者消费: " + item);
notFull.signal(); // 唤醒生产者
return item;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ProducerConsumer pc = new ProducerConsumer();
Thread producer = new Thread(() -> {
try {
for (int i = 1; i <= 10; i++) {
pc.produce(i);
Thread.sleep(500);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Producer");
Thread consumer = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
pc.consume();
Thread.sleep(800);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Consumer");
producer.start();
consumer.start();
}
}
📊 输出(节选)
生产者生产: 1
消费者消费: 1
生产者生产: 2
生产者生产: 3
消费者消费: 2
消费者消费: 3
...
缓冲区空,消费者等待...
生产者生产: 9
消费者消费: 9
生产者生产: 10
消费者消费: 10
关键:使用两个
Condition
实现精准唤醒,避免了notifyAll()
的“惊群效应”。
5. 多条件变量:交通灯的“左转/直行/右转”
🚦 一个 Lock,多个 Condition
public class TrafficLight {
private final ReentrantLock lock = new ReentrantLock();
private final Condition leftTurn = lock.newCondition(); // 左转车道
private final Condition straight = lock.newCondition(); // 直行车道
private final Condition rightTurn = lock.newCondition(); // 右转车道
public void waitForLeftTurn() throws InterruptedException {
lock.lock();
try {
System.out.println("左转车辆等待...");
leftTurn.await();
System.out.println("左转车辆通过!");
} finally {
lock.unlock();
}
}
public void allowLeftTurn() {
lock.lock();
try {
System.out.println("绿灯:左转通行!");
leftTurn.signal();
} finally {
lock.unlock();
}
}
// 其他车道类似...
}
💡 优势
- 精准控制:只唤醒特定条件的线程。
- 避免惊群:
signal()
只唤醒一个,signalAll()
只唤醒对应队列的线程。 - 可扩展性:可定义任意多个条件。
6. 源码探秘:AQS 中的 ConditionObject
🔍 ConditionObject
内部结构
Condition
的实现类是 AbstractQueuedSynchronizer.ConditionObject
。
它内部维护了一个单向链表(等待队列),与 AQS 的双向同步队列分离。
AQS 同步队列(FIFO):
Head <-> Node1 <-> Node2 <-> ... <-> Tail
▲
│
Condition 等待队列(单向):
FirstWaiter -> NodeA -> NodeB -> ... -> LastWaiter
🔍 await()
源码关键步骤
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 1. 将当前线程包装成 Node,加入 Condition 等待队列
Node node = addConditionWaiter();
// 2. 完全释放锁(可能可重入多次)
int savedState = fullyRelease(node);
int interruptMode = 0;
// 3. 检查线程是否在同步队列中(被 signal 后会转移)
while (!isOnSyncQueue(node)) {
// 4. 阻塞线程
LockSupport.park(this);
// 处理中断...
}
// 5. 被唤醒后,重新竞争锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 清理取消的节点...
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
🔍 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);
}
// 将节点从 Condition 队列转移到 AQS 同步队列
final boolean transferForSignal(Node node) {
// 将节点状态改为 0(等待被唤醒)
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 加入 AQS 同步队列
Node p = enq(node);
int ws = p.waitStatus;
// 唤醒线程,使其进入 AQS 的 acquire 流程
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
关键:
signal()
并不立即唤醒线程,而是将其转移到 AQS 同步队列,等待signal()
方法的调用者释放锁后,该线程才能竞争锁。
7. 与 Object 的 wait/notify 对比
📊 表格:Condition vs Object 通信机制
特性 | Condition | Object (wait/notify) |
---|---|---|
所属 | java.util.concurrent.locks |
java.lang.Object |
依赖 | 必须与 Lock 配合 |
必须与 synchronized 配合 |
多条件 | ✅ 支持多个 Condition |
❌ 只有一个等待队列 |
唤醒精度 | 高(可指定唤醒) | 低(notify 随机,notifyAll 全部) |
中断响应 | 更好(await 可中断) |
一般(wait 可中断) |
超时控制 | 更丰富(多种方法) | 基本(wait(long) ) |
灵活性 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
🧪 代码对比:生产者-消费者
❌ 使用 wait/notify(低效)
synchronized (this) {
while (buffer.isEmpty()) {
wait(); // 可能唤醒生产者或消费者
}
// 消费...
notifyAll(); // 必须 notifyAll,否则可能死锁
}
✅ 使用 Condition(高效)
lock.lock();
try {
while (buffer.isEmpty()) {
notEmpty.await(); // 只等待 notEmpty
}
// 消费...
notFull.signal(); // 只唤醒生产者
} finally {
lock.unlock();
}
结论:
Condition
提供了更细粒度的控制,避免了不必要的唤醒和竞争。
8. 面试篇:“信号灯连环问”
🤔 面试题 1:await()
为什么必须在 lock()
和 unlock()
之间调用?
答:
await()
需要释放当前持有的锁,否则其他线程无法获取锁来修改条件或调用signal()
。- 这与
wait()
必须在synchronized
块中调用同理。
🤔 面试题 2:signal()
后,被唤醒的线程会立即执行吗?
答:
- 不会。
signal()
只是将线程从Condition
队列转移到 AQS 同步队列。- 被唤醒的线程必须等到
signal()
的调用者释放锁后,才能竞争锁并继续执行。
🤔 面试题 3:signal()
和 signalAll()
的区别?何时用哪个?
答:
signal()
:唤醒一个等待线程(通常是队列头部)。signalAll()
:唤醒所有等待线程。- 何时用:
- 如果条件满足后,只有一个线程能继续执行(如生产者-消费者中的
notFull
),用signal()
。 - 如果多个线程都可能满足条件,用
signalAll()
。
- 如果条件满足后,只有一个线程能继续执行(如生产者-消费者中的
🤔 面试题 4:如何避免“虚假唤醒”?
答:
- 使用
while
循环检查条件,而不是if
。 - 因为即使没有收到
signal
,线程也可能被意外唤醒(如系统信号)。
while (!condition) {
condition.await();
}
🤔 面试题 5:Condition
的等待队列和 AQS 的同步队列有什么关系?
答:
- 等待队列(Condition Queue):单向链表,存放因
await()
而阻塞的线程。 - 同步队列(Sync Queue):双向链表,存放竞争锁的线程。
- 当线程调用
await()
时,从同步队列转移到等待队列。 - 当调用
signal()
时,从等待队列转移到同步队列,重新参与锁竞争。
9. 最佳实践:何时用 Condition?
✅ 推荐使用 Condition 的场景
- 需要多路通知的场景(如生产者-消费者)
- 高并发、高性能要求的系统
- 需要精确唤醒,避免“惊群效应”
- 使用
ReentrantLock
作为锁机制
private final ReentrantLock lock = new ReentrantLock();
private final Condition conditionA = lock.newCondition();
private final Condition conditionB = lock.newCondition();
❌ 不推荐使用 Condition 的场景
- 简单的线程协作,
synchronized
+wait/notify
足够 - 代码复杂度要求极低的场景
10. 终极总结:做一名智慧的“信号灯指挥官”
经过这场“信号灯系统”的深度探索,你已经掌握了 Condition
如何优雅地实现线程通信。
记忆口诀:
await 释放锁,进队列,等信号。
signal 唤醒它,转队列,争锁去。
多条件,精准控,效率高,推荐用!
决策树:
┌─────────────────────┐
│ 是否使用 ReentrantLock?│
└─────────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌──────────────────────────────┐ ┌──────────────────────────────┐
│ 需要多条件或精确唤醒? │ │ 用 synchronized + wait/notify │
└────────────────────┬───────┘ └──────────────────────────────┘
▼
┌──────────────────────────────┐
│ 使用 Condition │
│ - 创建多个 Condition 实例 │
│ - await()/signal() 精准控制 │
└──────────────────────────────┘
下次在设计并发程序时,请像一位真正的“信号灯指挥官”一样,根据业务需求选择合适的“通信机制”。记住:
真正的并发高手,不在于用最复杂的锁,而在于用最精准的“信号”,指挥最混乱的“车流”。
🚦 灯亮车行,灯灭车停,秩序井然,系统畅通!
🎯 总结一下:
本文深入探讨了《Condition 接口与 await()/signal() 方法》,从原理到实践,解析了面试中常见的考察点和易错陷阱。掌握这些内容,不仅能应对面试官的连环追问,更能提升你在实际开发中的技术判断力。
🔗 下期预告:我们将继续深入Java面试核心,带你解锁《ReadWriteLock 读写锁的实现与应用场景》 的关键知识点,记得关注不迷路!
💬 互动时间:你在面试中遇到过类似问题吗?或者对本文内容有疑问?欢迎在评论区留言交流,我会一一回复!
如果你觉得这篇文章对你有帮助,别忘了 点赞 + 收藏 + 转发,让更多小伙伴一起进步!我们下一篇见 👋
更多推荐
所有评论(0)