一、先看下 Condition的使用案例:
经典案例:生产者-消费者问题(面包店模型)
java
/**
* 面包店:生产者做面包,消费者买面包
* - 货架最多放5个面包
* - 货架满时,生产者要等待
* - 货架空时,消费者要等待
*/
public class Bakery {
private final Queue<String> shelf = new LinkedList<>(); // 货架
private final int CAPACITY = 5; // 货架容量
private final Lock lock = new ReentrantLock();
// 创建两个条件:货架"非满"和"非空"
private final Condition notFull = lock.newCondition(); // 用于生产者等待
private final Condition notEmpty = lock.newCondition(); // 用于消费者等待
/**
* 生产者:制作面包
*/
public void produceBread(String breadName) throws InterruptedException {
lock.lock();
try {
// 如果货架满了,生产者就要等待"货架非满"这个条件
while (shelf.size() == CAPACITY) {
System.out.println("货架已满," + Thread.currentThread().getName() + " 等待中...");
notFull.await(); // 释放锁,进入等待状态
}
// 货架有空间了,放上面包
shelf.offer(breadName);
System.out.println(Thread.currentThread().getName() + " 制作了: " + breadName +
",货架现有: " + shelf.size() + "个面包");
// 通知消费者:货架现在"非空"了,可以来买了
notEmpty.signal();
} finally {
lock.unlock();
}
}
/**
* 消费者:购买面包
*/
public String consumeBread() throws InterruptedException {
lock.lock();
try {
// 如果货架空了,消费者就要等待"货架非空"这个条件
while (shelf.isEmpty()) {
System.out.println("货架已空," + Thread.currentThread().getName() + " 等待中...");
notEmpty.await(); // 释放锁,进入等待状态
}
// 货架有面包了,取走一个
String bread = shelf.poll();
System.out.println(Thread.currentThread().getName() + " 购买了: " + bread +
",货架剩余: " + shelf.size() + "个面包");
// 通知生产者:货架现在"非满"了,可以继续生产了
notFull.signal();
return bread;
} finally {
lock.unlock();
}
}
}
测试代码
java
public class BakeryTest {
public static void main(String[] args) {
Bakery bakery = new Bakery();
// 创建2个生产者线程
for (int i = 1; i <= 2; i++) {
new Thread(() -> {
try {
for (int j = 1; j <= 10; j++) {
bakery.produceBread("面包-" + Thread.currentThread().getName() + "-" + j);
Thread.sleep(100); // 模拟制作时间
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "生产者" + i).start();
}
// 创建3个消费者线程
for (int i = 1; i <= 3; i++) {
new Thread(() -> {
try {
for (int j = 1; j <= 7; j++) {
bakery.consumeBread();
Thread.sleep(150); // 模拟购买时间
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "消费者" + i).start();
}
}
}
完整流程分析
场景1:货架未满,多个生产者竞争
java
// 假设当前货架有3个面包(未满)
// 生产者1和生产者2同时到达
生产者1: lock.lock() 成功获取锁
生产者2: lock.lock() ❌ 阻塞等待(因为锁被生产者1持有)
// 生产者1继续执行
生产者1: 检查货架未满 → 制作面包 → notEmpty.signal() → lock.unlock()
// 生产者1释放锁后
生产者2: 从阻塞状态被唤醒,成功获取锁 → 检查货架 → 制作面包...
关键点:在货架未满时,生产者之间是公平竞争锁的关系。
场景2:货架已满,Condition 开始发挥作用
java
// 假设货架已经有5个面包(满了)
// 生产者1先获取锁
生产者1: lock.lock() 成功
生产者1: 检查货架已满 → notFull.await()
// 重点来了!在await() 方法内部:
1. 生产者1会**自动释放锁**(这样其他线程就能获取锁了)
2. 生产者1线程进入等待状态,被挂起
// 此时锁被释放,消费者可以获取锁了
消费者1: lock.lock() 成功获取锁
消费者1: 购买面包 → notFull.signal() → lock.unlock()
// notFull.signal() 唤醒了在 notFull 条件上等待的生产者1
生产者1: 从 await() 中醒来,但需要**重新获取锁**才能继续执行
注意:生产者因为调用**notFull.await()**进入notFull队列等待,并且会释放锁,当消费者通过notFull.signal()唤醒生成者的时候,是从notFull.await()这行代码继续执行,只不过还要获取锁,如果没有获取到就要等待,但是他不需要写lock.lock();这种代码。这里需要注意,生产者执行notFull.await()代码,只是释放锁和进入等待,没有做唤醒消费者动作。
notFull.await()做了什么
java
//1、也就是执行notFull.await()这个代码,里面做了好几个事情,先创建等待队列,
//2、然后释放锁,自己挂起等待,唤醒后还要在获取一次锁。用伪代码表示就是下面:
public final void await() throws InterruptedException {
// 1. 创建新的节点加入到条件队列
Node node = addConditionWaiter();
// 2. 完全释放当前线程持有的锁(让其他线程可以获取锁)
int savedState = fullyReleaseLock();
// 3. 将当前线程挂起,进入等待状态
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
}
// 4. 被唤醒后,需要重新获取锁
acquireQueued(node, savedState);
}
java
生产者1获取锁 → 检查货架已满 → 调用 notFull.await()
↓
自动释放锁,线程挂起
↓
消费者获取锁 → 购买面包 → 调用 notFull.signal()
↓
生产者1被唤醒,尝试重新获取锁
↓
成功获取锁后,从await()方法继续执行
整体执行流程:
java
生产者1 尝试获取锁...
生产者1 成功获取锁
生产者2 尝试获取锁... ← 生产者2在这里阻塞
货架已满,生产者1 开始等待... ← 生产者1调用await()释放锁
生产者2 成功获取锁 ← 生产者2立即获取到锁
货架已满,生产者2 也开始等待... ← 生产者2调用await()释放锁
消费者1 尝试获取锁...
消费者1 成功获取锁
消费者1 购买了面包
消费者1 唤醒了生产者1
消费者1 释放锁
生产者1 被唤醒,重新检查条件 ← 生产者1需要重新获取锁
生产者1 成功获取锁 ← 获取锁成功
生产者1 制作了: 面包+1
生产者1 释放锁
问题
问题:当多个生产者调用notFull.await();后,被消费者唤醒,那么唤醒的顺序是按进入notFull的顺序唤醒的吗?
java
是的,FIFO(先进先出)顺序唤醒的,被唤醒的顺序严格按进入 await()的顺序,
与锁的公平性无关。但是获取锁的顺序,这取决于使用的 Lock 是公平锁还是非公平锁。
二、如果使用synchronized实现:
java
/**
* 使用 synchronized 实现的有界缓冲区
*/
public class BoundedBufferSynchronized {
private final Queue<String> buffer = new LinkedList<>();
private final int capacity;
public BoundedBufferSynchronized(int capacity) {
this.capacity = capacity;
}
/**
* 生产者方法
*/
public void produce(String item) throws InterruptedException {
synchronized (this) { // 使用 synchronized 块
// 如果缓冲区已满,等待
while (buffer.size() == capacity) {
System.out.println(Thread.currentThread().getName() + ": 缓冲区已满,等待中...");
wait(); // 释放锁,进入等待
}
// 生产物品
buffer.offer(item);
System.out.println(Thread.currentThread().getName() + " 生产了: " + item + ",当前数量: " + buffer.size());
// 通知所有等待的线程(包括生产者和消费者)
notifyAll();
}
}
/**
* 消费者方法
*/
public String consume() throws InterruptedException {
synchronized (this) {
// 如果缓冲区为空,等待
while (buffer.isEmpty()) {
System.out.println(Thread.currentThread().getName() + ": 缓冲区为空,等待中...");
wait(); // 释放锁,进入等待
}
// 消费物品
String item = buffer.poll();
System.out.println(Thread.currentThread().getName() + " 消费了: " + item +
",当前数量: " + buffer.size());
// 通知所有等待的线程
notifyAll();
return item;
}
}
public synchronized int size() {
return buffer.size();
}
}
测试代码:
java
public class SynchronizedProducerConsumerTest {
public static void main(String[] args) {
BoundedBufferSynchronized buffer = new BoundedBufferSynchronized(3);
// 生产者线程
Thread producer1 = new Thread(() -> {
try {
for (int i = 1; i <= 5; i++) {
buffer.produce("产品-" + Thread.currentThread().getName() + "-" + i);
Thread.sleep(100);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "生产者1");
Thread producer2 = new Thread(() -> {
try {
for (int i = 1; i <= 5; i++) {
buffer.produce("产品-" + Thread.currentThread().getName() + "-" + i);
Thread.sleep(150);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "生产者2");
// 消费者线程
Thread consumer1 = new Thread(() -> {
try {
for (int i = 1; i <= 6; i++) {
buffer.consume();
Thread.sleep(200);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "消费者1");
Thread consumer2 = new Thread(() -> {
try {
for (int i = 1; i <= 4; i++) {
buffer.consume();
Thread.sleep(180);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "消费者2");
producer1.start();
producer2.start();
consumer1.start();
consumer2.start();
try {
producer1.join();
producer2.join();
consumer1.join();
consumer2.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("最终缓冲区大小: " + buffer.size());
}
}
synchronized 实现的严重缺点
"惊群效应"(最严重的缺点)
生产者
java
while (buffer.size() == capacity) {
wait(); // 生产者等待
}
// ... 生产面包 ...
notifyAll(); // 唤醒所有线程
消费者
java
while (buffer.isEmpty()) {
wait(); // 消费者等待
}
// ... 消费 ...
notifyAll(); // 唤醒所有线程
问题分析:
java
当生产者生产一个产品后调用 notifyAll(),会唤醒所有等待的线程
包括:其他生产者 + 所有消费者
但被唤醒的生产者发现缓冲区仍然满,只能继续等待
造成大量不必要的线程唤醒和上下文切换。大量不必要的线程唤醒、频繁的线程上下文切换、CPU 资源浪费、在高并发场景下性能严重下降
无法实现精确通知
java
// 我们真正想要的是:
生产者生产 → 只唤醒消费者
消费者消费 → 只唤醒生产者
// 但 synchronized 只能:
生产者生产 → 唤醒所有线程(生产者和消费者)
消费者消费 → 唤醒所有线程(生产者和消费者)
- 性能问题
java
由于频繁的"惊群效应",导致:大量不必要的线程唤醒、频繁的线程上下文切换、
CPU 资源浪费、在高并发场景下性能严重下降
总结:synchronized 实现的缺点
java
1、惊群效应:notifyAll()唤醒所有线程,造成大量不必要的唤醒
2、无法精确通知:不能针对特定条件的线程进行唤醒
3、性能较差:在高并发场景下,频繁的无效唤醒导致性能下降
4、功能有限:缺少超时、中断等高级特性
5、公平性不可控:无法实现公平锁
什么时候可以用 synchronized?
java
1、虽然有这么多的缺点,但 synchronized在以下场景仍然适用:
2、简单的同步场景:线程数量少,竞争不激烈
3、代码简洁性优先:快速开发,逻辑简单
4、性能要求不高:不是性能关键路径
但对于复杂的生产者-消费者场景,Lock + Condition 是明显更好的选择。