结合Condition实现生产者与消费者示例,来进一步分析AbstractQueuedSynchronizer的内部工作机制

生产者 - 消费者示例代码

我们将创建一个有界缓冲区(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 内部工作机制分析

ReentrantLockCondition 都是基于 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 的一个属性)设置为当前线程,表示当前线程已获取锁。
  • 如果获取成功 :线程继续执行 puttake 方法的后续逻辑。

  • 如果获取失败 (即 state 不为 0,且持有锁的不是当前线程):

    • AQS 会创建一个独占模式Node 对象,将当前线程封装进去。
    • 这个 Node 会被加入到 AQS 内部维护的一个FIFO 阻塞队列(CLH 队列)的尾部。
    • 然后,当前线程会调用 LockSupport.park() 方法将自己阻塞。
2. condition.await() - 等待条件

当线程获取锁后,进入 puttake 方法,发现条件不满足(如缓冲区满或空),它会调用 condition.await()

  • 创建条件队列节点:

    • Condition 对象内部也维护着一个条件队列。这个队列和 AQS 的主阻塞队列是分开的。
    • await() 方法会创建一个新的 Node(其 waitStatusNode.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() - 释放锁

当线程完成 puttake 操作后,会在 finally 块中调用 lock.unlock()

  • 这同样会调用 AQS 的 release(1) 方法。
  • tryRelease(1) 会减少 state。对于非重入锁,state 变为 0。
  • AQS 会检查主阻塞队列。如果队列不为空,它会唤醒队列头部的线程,让它尝试获取锁。

总结:AQS 的核心角色

在这个生产者 - 消费者示例中,AQS 扮演了以下几个关键角色:

  1. 同步状态管理器 (state) : 通过一个简单的整数 state 和 CAS 操作,高效地管理锁的获取与释放。
  2. 独占锁持有者记录器 (exclusiveOwnerThread) : 记录当前持有独占锁的线程,用于实现重入和锁释放的正确性。
  3. 线程阻塞队列 (CLH Queue) : 当线程获取锁失败时,AQS 将其封装成 Node 放入此队列,并阻塞线程。这是线程的 "候客厅"。
  4. 条件变量支持 : Condition 对象利用 AQS 的基础功能,实现了独立的条件等待队列。它允许线程在某个条件不满足时,暂时放弃锁并等待,直到被其他线程唤醒。

整个过程就像是:

  • 线程是想要进入房间(临界区)的人。
  • AQS 的 state 是房间的钥匙。
  • AQS 主队列是房间外的等候队伍。
  • Condition 的条件队列 是一个特殊的休息室,当房间里的人发现 "咖啡还没煮好"(条件不满足),他会暂时把钥匙还给前台(释放锁),然后去休息室等待,直到有人通知他 "咖啡煮好了"(signal),他才会回到等候队伍的末尾,重新排队等待进入房间。
相关推荐
戴着眼镜的平头哥11 分钟前
前端卷Java系列之一个接口的诞生
后端
9号达人12 分钟前
@NotBlank 不生效报错 No validator could be found:Hibernate Validator 版本匹配指北
后端·面试·程序员
随风飘的云12 分钟前
mysql在查询的时候走索引比不走索引一定快吗?
后端
h贤14 分钟前
高可靠微服务消息设计:Outbox模式、延迟队列与Watermill集成实践
后端
架构师专栏16 分钟前
Spring Boot 4 概述与重大变化
spring boot·后端
武子康19 分钟前
大数据-162 Apache Kylin 增量 Cube 与 Segment 实战:按天分区增量构建指南
大数据·后端·apache kylin
SimonKing42 分钟前
IntelliJ IDEA 2025.2.x的小惊喜和小BUG
java·后端·程序员
青梅主码1 小时前
介绍一下我用AI开发的一款新工具:函数图像绘制工具(二)
后端
q***01771 小时前
Spring Boot 热部署
java·spring boot·后端