从 AQS 到 ReentrantLock:搞懂同步队列与条件队列,这一篇就够了

0 阅读6分钟

一、队列实现原理

1、同步等待队列和条件等待队列

[

二、同步等待队列

主要用于维护获取锁失败时入队的线程

1、举个例子🌰🌰

1.1、代码

import java.util.concurrent.locks.ReentrantLock;

/**
 * 模拟购买场景
 */
public class ReentrantLockDemo {
    //默认非公平
    private final ReentrantLock lock = new ReentrantLock();
    // 总数
    private static int count = 8;

    /**
     * 模拟购买
     */
    public void buy() {
        // 获取锁
        lock.lock();
        try {
            if (count > 0) {
                try {
                    // 休眠1s
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + ":购买了第" + count-- + "张,同步队列长度"+ lock.getQueueLength());
            } else {
                System.out.println(Thread.currentThread().getName() + ":购买失败,同步队列长度"+ lock.getQueueLength());
            }

        } finally {
            // 释放锁
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ReentrantLockDemo reentrantLockDemo = new ReentrantLockDemo();
        for (int i = 1; i <= 10; i++) {
            Thread thread = new Thread(() -> {
                reentrantLockDemo.buy(); // 抢票

            }, "线程" + i);
            // 启动线程
            thread.start();
        }
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("剩余:" + count);
    }
}

1.2、结果

线程1:购买了第8张,同步队列长度9
线程2:购买了第7张,同步队列长度8
线程3:购买了第6张,同步队列长度7
线程4:购买了第5张,同步队列长度6
线程5:购买了第4张,同步队列长度5
线程6:购买了第3张,同步队列长度4
线程7:购买了第2张,同步队列长度3
线程8:购买了第1张,同步队列长度2
线程9:购买失败,同步队列长度1
线程10:购买失败,同步队列长度0
剩余:0

⚠️注意:
✔️负责管理在竞争锁失败时进入等待状态的线程,基于双向链表数据结构的队列,是FIFO先进先出线程等待队列,Java中的[CLH队列]是原CLH队列的一个变种,线程由原自旋机制改为阻塞机制

2、源码分析

2.1、源码

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
   // 将当前线程封装为一个独占模式的节点(Node),并添加到AQS的同步队列尾部。
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }
// 一个无限循环,用于让线程持续尝试获取资源
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            //自旋等待逻辑
            for (;;) {
                final Node p = node.predecessor();
                //判断当前节点的前驱节点是否为头节点(head),并且尝试获取锁
                if (p == head && tryAcquire(arg)) {
                    //如果成功获取资源,则更新头节点、断开旧节点链接,并返回中断状态。
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //挂起线程等待唤醒
                //如果未能获取资源,则根据前驱节点状态决定是否挂起线
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

📌 for (;;) 的核心意义:
✔️自旋等待头节点对应的线程会在队列中循环尝试获取锁,直到成功获取锁或被中断。这种方式避免了频繁的线程阻塞与唤醒带来的内核态切换开销,从而显著提升并发性能。即便线程被挂起后再次唤醒,也会重新进入自旋逻辑继续竞争锁
✔️灵活控制:通过循环条件动态判断是否可以获取资源或需要挂起线程。支持中断响应,增强程序的健壮性。
✔️高效资源管理:成功获取资源后立即退出循环,减少不必要的计算。


三、条件等待队列

  • 调用await() 的时候会释放锁,然后线程会加入到条件队列
  • 调用signal() 唤醒的时候会把条件队列中的线程节点移动到同步队列中,等待再次获得锁

1、举个例子🌰🌰

1.1、代码

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionDemo {
    /**
     * 锁
     */
    private static ReentrantLock lock = new ReentrantLock();
    /**
     * 判断条件
     */
    private static Condition condition = lock.newCondition();
    /**
     * 条件标志位
     */
    private static volatile boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        new Thread(new DemoOne(), "DemoOne").start();
        Thread.sleep(1000);
        new Thread(new DemoTwo(), "DemoTwo").start();
    }

    /**
     * DemoOne
     */
    static class DemoOne implements Runnable {
        @Override
        public void run() {
            lock.lock();
            try {
                while (!flag) {
                    System.out.println(Thread.currentThread().getName() + "当前条件不满足等待");
                    try {
                        // TODO:something
                        condition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + "接收到通知条件满足");
            } finally {
                lock.unlock();
            }
        }
    }

    /**
     * DemoTwo
     */
    static class DemoTwo implements Runnable {

        @Override
        public void run() {
            lock.lock();
            try {
                flag = true;
                System.out.println(Thread.currentThread().getName() + "唤醒DemoOne");
                condition.signal();
            } finally {
                lock.unlock();
            }
        }
    }
}

1.2、结果

DemoOne当前条件不满足等待
DemoTwo唤醒DemoOne
DemoOne接收到通知条件满足

⚠️注意:
✔️条件等待队列会把条件等待队列中的线程节点移动到同步队列中,等待再次获得锁
✔️如果同步等待队列为空时,会唤醒条件等待队列的第一个节点(node)

2、源码分析

2.1,源码

       // 如果存在等待时间最长的线程,则将其从该条件的等待队列移至拥有该锁的等待队列
       public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }

        // 移除并转移节点,直到遇到未被取消的节点或空值
        private void doSignal(Node first) {
            do {
             //将 first.nextWaiter 赋值给 firstWaiter,即让 firstWaiter 指向当前节点的下一个等待者
             //如果 first.nextWaiter 为 null,说明当前节点是队列中最后一个节点,因此将 lastWaiter 设置为 null,表示队列为空
                if ( (firstWaiter = first.nextWaiter) == null)
                    //断开当前节点的链接
                    lastWaiter = null;
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
                // 尝试唤醒当前节点
                // 如果 firstWaiter 为 null,说明队列已遍历完毕,退出循环
        }

        // 将节点从条件队列转移到同步队列
        final boolean transferForSignal(Node node) {
           //使用 CAS 操作将节点的 waitStatus 从 Node.CONDITION 改为 0。
           //如果失败,说明该节点已经被取消(cancelled),直接返回 false。
           if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
               return false;
          //调用 enq(node) 将节点加入同步队列(Sync Queue)。
          //返回值 p 是该节点在同步队列中的前驱节点。
           Node p = enq(node);
           int ws = p.waitStatus;
          //获取前驱节点 p 的 waitStatus。
          //如果前驱节点的状态大于 0(表示已取消),或者无法将其状态设置为 Node.SIGNAL,则直接唤醒当前节点的线程。
          //否则,依赖前驱节点来唤醒当前节点。
           if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
               LockSupport.unpark(node.thread);
           return true;
       }

⚠️注意
✔️基于LockSupport.unpark(node.thread)、LockSupport.park(node.thread) 实现阻塞和唤醒
✔️条件队列在AQS内部是一个单向链表,节点在被转移到同步队列时,被构建为双向链表节点CLH队列


四、总结

1、ReentrantLock 基于 AQS+CAS 实现了同步队列与条件队列两大核心结构:

  • 同步队列: 负责管理抢锁失败的线程,通过头节点自旋减少线程切换开销,提升并发性能;
  • 条件队列:用于实现精准的等待 / 通知机制,一个锁可以支持多个条件队列,解决了传统通知无法精准唤醒的问题

[

2、ReentrantLock 与synchronized相比:

  • synchronized:只有一套隐式的等待队列,使用简单但功能单一;
  • ReentrantLock :依靠同步队列 + 条件队列的配合,在锁的灵活性、可控性、并发调度精度上都更加强大,是复杂并发场景下更优的选择