AQS核心解析:三问三知

欢迎来到啾啾的博客🐱。
记录学习点滴。分享工作思考和实用技巧,偶尔也分享一些杂谈💬。
有很多很多不足的地方,欢迎评论交流,感谢您的阅读和评论😄。

1 引言

阅读本篇可并发认知+3。

在之前的《线程》系列中,我们有提及AQS,但叙述基本照搬文档与《Java高并发核心编程》。
本篇将从问题出发,重新讲解一下AQS,讲解并发编程的基石。

Q:抛开所有JUC的实现,想象一下,在多线程环境下,我们有一个共享资源,一次只允许一个线程访问(这就是“锁”的本质需求)。当一个线程抢到锁后,其他没抢到的线程该怎么办?

A:AQS(AbstractQueuedSynchronizer)被设计出来处理了“一切”。

2 “自旋”还是“挂起”?

Q:当一个线程抢到锁后,其他竞争的线程是应该不断尝试(自旋)还是休息等待通知呢?又或者有其他的合适做法?
A:没有其他更合适的做法,自旋快但耗CPU,挂起省CPU但可能慢。

AQS采取的做法是“挂起”。
AQS认为,对于获取不到锁的场景,长时间的自旋是不可接受的浪费。所以,AQS建立了一个核心机制,让拿不到锁的线程进入一个“等候区”,并调用LockSupport.park()方法将线程挂起,放弃CPU,进入休眠状态。

2.1 synchronized的混合策略

现代的锁(如synchronized自JDK 1.6优化后)其实采用了混合策略:先进行短暂的、次数有限的“自适应自旋”,如果还拿不到锁,再像AQS一样,进入最终的挂起等待。
但AQS本身,专注于管理和调度那些已经被迫放弃CPU的线程,这是它的核心职责。

3 “挂起”的线程要怎么组织?

Q:如果有大量的线程在等待,我们应该如何组织它们?是随机叫一个起来,还是应该有个先来后到的队列?为什么?
A: 使用队列组织等待的线程,借助队列"先进先出"的特性,但也要考虑“插队”的可能。

AQS可以“先进先出”的公平排队,也设计有“插队”机制。
插队有两种,一种是插入原有队列,一种是快速通道。AQS的实现是“插入原有队列”。

3.1 CLH队列

AQS内部维护了一个CLH队列(一个双向链表的变体)。所有等待的线程都会被封装成一个Node节点,并加入到这个队列的尾部。这天然地实现了公平性(Fairness),即先来后到。

AQS通过暴露给子类(如ReentrantLock)的接口,实现了公平模式非公平模式

  • 公平模式 (Fair): 严格按照队列顺序唤醒线程,绝对的先来后到。
  • 非公平模式 (Non-fair): 当一个线程释放锁时,会唤醒队头的等待者。但如果此时正好有一个新的线程要来获取锁,它会先尝试“插队”(Barging),如果成功了,就直接拿走锁。这种模式下,系统吞吐量通常更高,因为减少了线程挂起和唤醒的开销。

4 “挂起”的线程要怎么唤醒?

Q:锁被释放后,“挂机”等待的线程是随机唤醒还是全部唤醒?
A: 唤醒全部开销大,唤醒下一个开销小。一般是“精准唤醒”。

当一个线程释放锁时,AQS只会去唤醒它在CLH队列中的直接后继者(successor)。它绝不会去惊动所有等待的线程,从而避免了“惊群效应”(Thundering Herd),即大量线程被唤醒后再次激烈竞争锁,导致大量无用的CPU消耗和上下文切换。这种精准制导,是AQS性能远超早期synchronized和Object.wait/notifyAll的关键。

5 AQS源码

我们从ReentrantLock.lock()开始追踪:
acquire(1) -> tryAcquire() -> addWaiter() -> acquireQueued()

一起来看AQS源码是如何解决设计上述机制的。

5.1 挂起->打包成node

5.1.1 获取锁

使用ReetrantLock默认是非公平锁。

  • 非公平锁获取锁
    如果锁没有被占,则使用CAS操作获取锁。否则如果是重入,则更新计数状态(+1);
        @ReservedStackAccess
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
  • 公平锁获取锁
    如果锁没有被占,且没有比当前线程等待获取锁时间更长的线程,则使用CAS操作获取锁。
     @ReservedStackAccess
     protected final boolean tryAcquire(int acquires) {
         final Thread current = Thread.currentThread();
         int c = getState();
         if (c == 0) {
             if (!hasQueuedPredecessors() &&
                 compareAndSetState(0, acquires)) {
                 setExclusiveOwnerThread(current);
                 return true;
             }
         }
         else if (current == getExclusiveOwnerThread()) {
             int nextc = c + acquires;
             if (nextc < 0)
                 throw new Error("Maximum lock count exceeded");
             setState(nextc);
             return true;
         }
         return false;
     }
        
    // hasQueuedPredecessors 方法等效于  
    // getFirstQueuedThread() != Thread.currentThread() && hasQueuedThreads()
    public final boolean hasQueuedPredecessors() {
        Node h, s;
        if ((h = head) != null) {
            if ((s = h.next) == null || s.waitStatus > 0) {
                s = null; // traverse in case of concurrent cancellation
                for (Node p = tail; p != h && p != null; p = p.prev) {
                    if (p.waitStatus <= 0)
                        s = p;
                }
            }
            if (s != null && s.thread != Thread.currentThread())
                return true;
        }
        return false;
    }

5.1.2 核心字段state

其中 getState()获取的是AQS的核心字段state

  • private volatile int state
    一个整型变量,代表同步状态。对于ReentrantLock来说,state=0表示锁未被占用,state>0表示锁已被占用,其值代表“重入”的次数。

5.2 挂起

获取锁失败后,调用acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
  • addWaiter(Node.EXCLUSIVE)
    方法的核心逻辑是为当前线程创建一个新节点,并通过循环和原子操作将其安全地加入到等待队列的尾部。如果队列还未初始化,会先调用 initializeSyncQueue 方法进行初始化。
    private Node addWaiter(Node mode) {
        Node node = new Node(mode);

        for (;;) {
            Node oldTail = tail;
            if (oldTail != null) {
                node.setPrevRelaxed(oldTail);
                if (compareAndSetTail(oldTail, node)) {
                    oldTail.next = node;
                    return node;
                }
            } else {
                initializeSyncQueue();
            }
        }
    }
  • acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
    方法核心逻辑为让已经在等待队列中的线程不断尝试获取锁。
    如果当前线程是队列中第一个等待的线程,会尝试获取锁;如果获取失败,会根据情况阻塞线程。
    在整个过程中,会记录线程是否被中断,并在出现异常时取消获取操作。
    final boolean acquireQueued(final Node node, int arg) {
        boolean interrupted = false;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node))
                    interrupted |= parkAndCheckInterrupt();
            }
        } catch (Throwable t) {
            cancelAcquire(node);
            if (interrupted)
                selfInterrupt();
            throw t;
        }
    }

5.3 等待唤醒

在释放锁时,AQS会对等待线程进行精确唤醒。
unlock()->release()

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

以ReentrantLock实现为例

        @ReservedStackAccess
        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

releases默认为1,这里和lock时state +1 对应,释放锁时-1。
唤醒的核心操作为unparkSuccessor(h),传入的node为头节点:

    private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        if (ws < 0)
            node.compareAndSetWaitStatus(ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node p = tail; p != node && p != null; p = p.prev)
                if (p.waitStatus <= 0)
                    s = p;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

unparkSuccessor核心作用为“唤醒指定节点的后继节点”。
方法的核心逻辑是先尝试清除当前节点的等待状态,然后找到合适的后继节点,最后唤醒该节点对应的线程。
在查找后继节点时,若直接后继节点不可用(s == null || s.waitStatus > 0),会从队列尾部向前遍历,确保找到一个非取消状态的节点进行唤醒。

5.3.1 核心字段waitStatus

waitStatus是整个AQS线程状态流转的“灵魂”,是节点与节点之间的“通信协议”。
AQS的等待队列本质就是一个排队区,waitStatus就是每个排队的Node的“状态说明”。

各值说明:

  • SIGNAL (-1)
    这是最重要、最常见的状态。如果一个节点(前一个节点p)的waitStatus是SIGNAL,就意味着它向它的后继节点(当前节点node)做出了一个承诺:“我释放锁的时候,一定会负责把你唤醒(unpark)”。

  • CANCELLED (1)
    这个状态表示节点对应的线程已经放弃了等待,比如因为等待超时或者被中断。

  • 0 (初始状态)
    每个新节点入队时,waitStatus的默认值就是0。这是一个中性的、临时的状态。

  • CONDITION (-2)
    这个状态与我们当前的锁队列无关。当线程调用Condition.await()时,它会进入Condition的等待队列,此时节点状态就是CONDITION。(在AQS篇暂不探究)

  • PROPAGATE (-3)
    这是用于共享锁(如Semaphore, CountDownLatch)的状态,表示释放操作需要向后传播,唤醒更多的等待者。在ReentrantLock的排他锁模式下,我们遇不到它。

6 Debug AQS

上述章节以提问+源码的形式探究了AQS原理,认知+1。
我们继续Debug看看具体流程,让我们对AQS的认知再+1。

import java.util.concurrent.locks.ReentrantLock;

public class AQSDebug {
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();

        // 线程A先获取锁
        new Thread(() -> {
            lock.lock();
            System.out.println("Thread A locked");
            try {
                Thread.sleep(10000); // 占有锁10秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
                System.out.println("Thread A unlocked");
            }
        }, "Thread A").start();

        // 保证A先启动
        try { Thread.sleep(100); } catch (InterruptedException e) {}

        // 线程B来竞争
        new Thread(() -> {
            System.out.println("Thread B trying to lock");
            lock.lock(); // 这里会进入AQS的等待队列
            System.out.println("Thread B locked");
            lock.unlock();
        }, "Thread B").start();
    }
}

在ReentrantLock.lock()方法内部打上断点,单步调试。
debug记得挂起选项勾选线程,以控制两个线程进度。
![[AQS三问三知.png]]

debug参考:
ThreadA获取锁在sleep期间,我们debug Thread B,可以发现Thread B获取锁失败,调用addWaiter尾插自身到队列,因为是第一个队列元素,所以调用initializeSyncQueue()初始化队列,并在下一个循环中,将自身插入到队尾。
注意,initializeSyncQueue有初始化一个头节点head,且tail = head。
插入队尾后,debug回到acquireQueued()方法,这个方法入参就是当前获取锁失败的Thread B封装的node,还有加锁的默认参数 arg(= 1)。
因为队列中只有Thread B,所以将调用shouldParkAfterFailedAcquire(p,node)方法,其中p = null。
在shouldParkAfterFailedAcquire方法中,核心字段volatile int waitStatus值被设置为SIGNAL (-1)。
然后进入acquireQueued()方法循环第二轮,还是进入到shouldParkAfterFailedAcquire,因为ws == Node.SIGNAL,所以这里返回为true。

if (shouldParkAfterFailedAcquire(p, node))
                    interrupted |= parkAndCheckInterrupt();

调用线程parkAndCheckInterrupt()使得Thread B中断(Thread.interrupted())。
![[AQS三问三知-1.png]]

此时我们回到Thread A,debug让Thread A释放锁,释放锁时在release中获取了当前的head node,此时head.next = Thread B,将head node传入unparkSuccessor,里面会调用LockSupport.unpark(s.thread)唤醒Thread B。

此时回到Thread B,其已被唤醒,醒来后的线程会继续执行代码,也就是acquireQueued()方法。
acquireQueued()继续执行“判断是当前节点的predecessor否是head然后获取锁”的循环,此时只有Thread B,所以其可以获取到锁了。

这应该讲清楚了吧,还是有不清晰的地方可以留言。

7 图解

使用图解加深影响,认知再+1。
核心问题简单图解⬇️

![[AQS核心解析:三问三知.png]]

8 其他

8.1 关于state字段的设计

Q:AQS的核心只有一个private volatile int state。这是一个32位的整型。如何利用这一个int变量,同时表示“共享模式的读锁”和“排他模式的写锁”的状态?

A:AQS作者Doug Lea将32位的int state作为位图(Bitmask)来使用,通过位运算来管理不同的状态。
Doug Lea将int类型的32位,劈成了两半:
高16位 (High 16 bits): 用于读锁的计数。
低16位 (Low 16 bits): 用于写锁的计数。

  • 为什么不采取双标识的设计呢?
    因为原子性考虑。获取锁,对于state的操作必须是具备原子性的。
    将读锁和写锁的状态都放在同一个volatile int里,使得AQS可以用一次CAS操作,就同时判断和修改整个锁的状态,保证了操作的原子性,避免了复杂的锁竞争。

state的计数规则非常清晰:

  • 获取写锁 (Exclusive Lock):

    • 当一个线程想获取写锁时,它必须检查整个state是否等于0。如果不为0,意味着要么有读锁,要么有写锁,它就必须等待(除非是自己重入)。
    • 获取成功后,它会给低16位加1。
    • 写锁重入,就是继续给低16位累加。
  • 获取读锁 (Shared Lock):

    • 当一个线程想获取读锁时,它必须检查低16位(写锁部分)是否为0。如果不为0,意味着别的线程占了写锁,它必须等待。
    • 如果写锁部分为0,它就可以安全地给高16位加1,表示自己持有了读锁。
    • 其他线程再来获取读锁,会发现写锁部分依然是0,于是它们也给高16位继续累加。
    • 读锁重入,也是给高16位累加。

在ReentrantReadWriteLock.Sync类里,你可以看到这些常量和方法,它们就是操作这32位state的工具:

// 16位,用来做移位和掩码
static final int SHARED_SHIFT   = 16;
// 读锁的单位,值为 1 << 16 = 65536
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
// 读锁和写锁的最大重入次数 (2^16 - 1) = 65535
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
// 写锁的掩码,低16位全是1,高16位全是0
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

// 获取读锁数量 (右移16位)
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
// 获取写锁数量 (和低16位的掩码做与运算)
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

8.2 CAS问题

基于CAS自旋实现的轻量级锁有两个大的问题:
(1)CAS恶性空自旋会浪费大量的CPU资源。
(2)在SMP架构的CPU上会导致“总线风暴”。
解决CAS恶性空自旋的有效方式之一是以空间换时间,较为常见的方案有两种:分散操作热点和使用队列削峰。JUC并发包使用的是队列削峰的方案解决CAS的性能问题,并提供了一个基于双向队列的削峰基类——抽象基础类AbstractQueuedSynchronizer(抽象同步器类,简称为AQS)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

tataCrayon|啾啾

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值