Condition案例和synchronized案例对比

一、先看下 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 只能:
生产者生产 → 唤醒所有线程(生产者和消费者)
消费者消费 → 唤醒所有线程(生产者和消费者)
  1. 性能问题
java 复制代码
由于频繁的"惊群效应",导致:大量不必要的线程唤醒、频繁的线程上下文切换、
CPU 资源浪费、在高并发场景下性能严重下降

总结:synchronized 实现的缺点

java 复制代码
1、惊群效应:notifyAll()唤醒所有线程,造成大量不必要的唤醒
2、无法精确通知:不能针对特定条件的线程进行唤醒
3、性能较差:在高并发场景下,频繁的无效唤醒导致性能下降
4、功能有限:缺少超时、中断等高级特性
5、公平性不可控:无法实现公平锁

什么时候可以用 synchronized?

java 复制代码
1、虽然有这么多的缺点,但 synchronized在以下场景仍然适用:
2、简单的同步场景:线程数量少,竞争不激烈
3、代码简洁性优先:快速开发,逻辑简单
4、性能要求不高:不是性能关键路径

但对于复杂的生产者-消费者场景,Lock + Condition 是明显更好的选择。