生产者 - 消费者示例代码
我们将创建一个有界缓冲区(Bounded Buffer),生产者线程向缓冲区中放入数据,消费者线程从中取出数据。当缓冲区满时,生产者会被阻塞;当缓冲区空时,消费者会被阻塞。
java
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class BoundedBuffer<T> {
private final Queue<T> queue;
private final int capacity;
private final ReentrantLock lock;
private final Condition notFull; // 条件:缓冲区未满
private final Condition notEmpty; // 条件:缓冲区未空
public BoundedBuffer(int capacity) {
this.capacity = capacity;
this.queue = new LinkedList<>();
this.lock = new ReentrantLock();
this.notFull = lock.newCondition();
this.notEmpty = lock.newCondition();
}
// 生产者放入数据
public void put(T item) throws InterruptedException {
lock.lock(); // 1. 获取独占锁
try {
// 2. 检查条件,如果缓冲区已满,则等待
while (queue.size() == capacity) {
System.out.println(Thread.currentThread().getName() + ":缓冲区已满,等待消费者消费...");
notFull.await(); // 3. 释放锁并阻塞当前线程
}
// 4. 条件满足,执行生产操作
queue.add(item);
System.out.println(Thread.currentThread().getName() + ":生产了 " + item + ",当前缓冲区大小:" + queue.size());
// 5. 唤醒等待"非空"条件的消费者线程
notEmpty.signal();
} finally {
lock.unlock(); // 6. 释放锁
}
}
// 消费者取出数据
public T take() throws InterruptedException {
lock.lock(); // 1. 获取独占锁
try {
// 2. 检查条件,如果缓冲区为空,则等待
while (queue.isEmpty()) {
System.out.println(Thread.currentThread().getName() + ":缓冲区为空,等待生产者生产...");
notEmpty.await(); // 3. 释放锁并阻塞当前线程
}
// 4. 条件满足,执行消费操作
T item = queue.poll();
System.out.println(Thread.currentThread().getName() + ":消费了 " + item + ",当前缓冲区大小:" + queue.size());
// 5. 唤醒等待"非满"条件的生产者线程
notFull.signal();
return item;
} finally {
lock.unlock(); // 6. 释放锁
}
}
public static void main(String[] args) {
BoundedBuffer<Integer> buffer = new BoundedBuffer<>(5);
// 启动3个生产者线程
for (int i = 0; i < 3; i++) {
final int producerId = i;
new Thread(() -> {
try {
for (int j = 0; j < 5; j++) {
int item = producerId * 100 + j;
buffer.put(item);
Thread.sleep(100); // 模拟生产耗时
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "生产者-" + i).start();
}
// 启动2个消费者线程
for (int i = 0; i < 2; i++) {
final int consumerId = i;
new Thread(() -> {
try {
for (int j = 0; j < 7; j++) {
buffer.take();
Thread.sleep(150); // 模拟消费耗时
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "消费者-" + i).start();
}
}
}
AQS 内部工作机制分析
ReentrantLock 和 Condition 都是基于 AQS 实现的。我们来分步解析上述代码中 AQS 的工作流程。
1. lock.lock() - 获取独占锁
当一个线程调用 lock.lock() 时,它实际上是在调用 AQS 的 acquire(1) 方法。
-
AQS 状态 (
state) :-
state初始值为 0,表示锁未被持有。 -
第一个线程调用
tryAcquire(1)(由ReentrantLock实现):- 它会检查
state是否为 0。 - 如果是,它会使用 CAS (Compare-and-Swap) 操作将
state从 0 修改为 1。 - 如果 CAS 成功,它会将
exclusiveOwnerThread(AQS 的一个属性)设置为当前线程,表示当前线程已获取锁。
- 它会检查
-
-
如果获取成功 :线程继续执行
put或take方法的后续逻辑。 -
如果获取失败 (即
state不为 0,且持有锁的不是当前线程):- AQS 会创建一个独占模式 的
Node对象,将当前线程封装进去。 - 这个
Node会被加入到 AQS 内部维护的一个FIFO 阻塞队列(CLH 队列)的尾部。 - 然后,当前线程会调用
LockSupport.park()方法将自己阻塞。
- AQS 会创建一个独占模式 的
2. condition.await() - 等待条件
当线程获取锁后,进入 put 或 take 方法,发现条件不满足(如缓冲区满或空),它会调用 condition.await()。
-
创建条件队列节点:
Condition对象内部也维护着一个条件队列。这个队列和 AQS 的主阻塞队列是分开的。await()方法会创建一个新的Node(其waitStatus为Node.CONDITION),并将当前线程放入这个条件队列中。
-
释放锁:
- 这是关键一步。
await()会调用 AQS 的release(1)方法,释放当前线程持有的锁。 release方法会调用tryRelease(1)(由ReentrantLock实现),将state从 1 减回 0,并清空exclusiveOwnerThread。- 然后,它会检查 AQS 主队列是否为空。如果不为空,它会唤醒队列头部的线程,让它有机会去竞争锁。
- 这是关键一步。
-
阻塞线程:
- 在释放锁之后,调用
await()的线程会再次调用LockSupport.park()将自己阻塞。此时,它等待的不再是锁,而是等待其他线程的唤醒信号。
- 在释放锁之后,调用
3. condition.signal() - 唤醒等待条件的线程
当另一个线程(比如消费者)执行了 notFull.signal() 或 notEmpty.signal() 时:
-
从条件队列转移到主队列:
signal()方法会从它的条件队列中取出一个等待最久的Node(通常是头部节点)。- 它会将这个
Node的状态从Node.CONDITION改变为Node.SIGNAL。 - 然后,这个
Node会被转移 到 AQS 的主阻塞队列的尾部。
-
等待再次竞争锁:
- 被转移的线程此时仍然是阻塞状态。它需要等待主队列中排在它前面的线程都处理完,并且当它自己成为队列头部时,才有机会被唤醒,重新去竞争锁。
4. lock.unlock() - 释放锁
当线程完成 put 或 take 操作后,会在 finally 块中调用 lock.unlock()。
- 这同样会调用 AQS 的
release(1)方法。 tryRelease(1)会减少state。对于非重入锁,state变为 0。- AQS 会检查主阻塞队列。如果队列不为空,它会唤醒队列头部的线程,让它尝试获取锁。
总结:AQS 的核心角色
在这个生产者 - 消费者示例中,AQS 扮演了以下几个关键角色:
- 同步状态管理器 (
state) : 通过一个简单的整数state和 CAS 操作,高效地管理锁的获取与释放。 - 独占锁持有者记录器 (
exclusiveOwnerThread) : 记录当前持有独占锁的线程,用于实现重入和锁释放的正确性。 - 线程阻塞队列 (CLH Queue) : 当线程获取锁失败时,AQS 将其封装成
Node放入此队列,并阻塞线程。这是线程的 "候客厅"。 - 条件变量支持 :
Condition对象利用 AQS 的基础功能,实现了独立的条件等待队列。它允许线程在某个条件不满足时,暂时放弃锁并等待,直到被其他线程唤醒。
整个过程就像是:
- 线程是想要进入房间(临界区)的人。
- AQS 的
state是房间的钥匙。 - AQS 主队列是房间外的等候队伍。
Condition的条件队列 是一个特殊的休息室,当房间里的人发现 "咖啡还没煮好"(条件不满足),他会暂时把钥匙还给前台(释放锁),然后去休息室等待,直到有人通知他 "咖啡煮好了"(signal),他才会回到等候队伍的末尾,重新排队等待进入房间。