AQS底层源码分析

87 阅读20分钟

AQS底层源码分析

什么是AQS?

AQS(AbstractQueuedSynchronizer)使用一个整数变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作,并发利用CAS操作来保证状态的安全更新

AQS的主要思想是,当一个线程尝试获取锁或执行特定操作时,如果失败了,它会被放入一个等待队列中,直到满足某个条件(如锁的释放或特定信号的到来)才能重新尝试获取锁或执行操作。这种机制可以有效地管理多个线程之间的竞争,确保线程的安全执行。

什么是公平锁?

公平锁是指多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。而非公平锁则是多个线程争抢同一把锁,不管是新来的线程还是等待时间最长的线程都有可能获得锁

公平锁的优点是所有的线程都能得到资源,不会饿死在队列中。但是,吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞CPU唤醒阻塞线程的开销会很大。

AQS底层实现

假设这是一个热门的展览,门外有一个安排有序的队伍,每个人都希望进入展览。这个队列就是同步队列,而门内的展览资源就是需要保护的共享资源。

  1. 排队等候进入展览
    • 当人们想要进入展览时,他们会排队等候。如果有人已经在展览内,其他人会被要求等待。在AQS中,每个线程就像一个等待进入展览的人,他们都希望获得共享资源的访问权。
    • 如果有人进入了展览,即成功获取了共享资源,那么他们就可以畅通无阻地访问展览内的内容。
  2. 等候和获取资源
    • 一些人可能会看到展览内有人,于是他们选择等待。在AQS中,线程也会通过自旋等待,不断尝试获取共享资源的访问权。
    • 如果等候的人数多,(如果是公平锁的情况下)AQS会将这些线程按照先来先服务的原则放入同步队列,就像人们在队伍中等待进入展览一样。(如果不是公平锁, 无论什么线程拿到的CPU资源都可以获取锁)
  3. 释放资源
    • 当一个人观看完展览后离开,他会让出他的位置,以便其他人可以进入。在AQS中,线程释放共享资源后,会通知等待的线程,让他们有机会获取资源,从而实现了资源的有序释放。
  4. 维护队列和状态
    • 在队伍中,每个人都知道前面的人和后面的人。在AQS中,每个线程也知道前驱节点和后继节点,这种信息帮助线程管理和维护同步队列。
  5. 虚拟队列头
    • 就像在队伍中没有第一个人前,我们放一个虚拟的“第一个人”来确保大家都能理解前面有人,AQS也会使用一个虚拟的头节点,以确保链表的结构正常运行。

AQS源码分析

我们从两个AQS的子类来分析AQS的源码

  • ReentrantLock
  • Semaphore

独占锁模式

独占模式我们使用ReentrantLock非公平锁方式分析独占锁源码

独占锁的加锁过程开始,大体分析过程:

lock入手然后是 acquire 方法, tryAcquireaddWaiterenqacquireQueuedshouldParkAfterFailedAcquireparkAndCheckInterrupt 方法的实现。然后了解锁释放过程,包括 release 方法、tryRelease 方法、unparkSuccessor 方法等。

public void lock() {
    // 非公平锁, 也就是NonfairSync
    // 可重入锁
    sync.acquire(1);
}
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

该方法的作用是获取指定数量的资源,其中 arg 参数表示请求获取的资源数量。下面是方法的大致执行流程:

  • acquire方法首先会调用tryAcquire方法尝试获取锁。

    • 如果tryAcquire方法返回false,则说明当前线程没有获取到锁,需要进入等待队列。此时,acquire方法会调用addWaiter方法将当前线程封装成一个Node节点,并加入到等待队列的末尾。然后,acquireQueued方法会将当前线程阻塞,并等待锁被释放。
    • 当锁被释放时,acquireQueued方法会将等待队列中的第一个线程唤醒,并使其再次尝试获取锁。如果该线程成功获取到了锁,则acquireQueued方法会返回true,否则返回false
  • acquire方法中,如果当前线程被阻塞,则会调用selfInterrupt方法中断自己的阻塞状态。这是为了防止当前线程永远无法被唤醒。

不公平锁tryAcquire源码分析

@ReservedStackAccess
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread(); // 获取当前线程的引用,并将其赋值给`current`变量。
    int c = getState(); // 获取锁的状态
    // 检查锁的状态是否为0
    if (c == 0) {
        // 如果锁的状态为0,表示当前没有线程持有该锁,执行下面的代码块。
        if (compareAndSetState(0, acquires)) {
            // 使用`compareAndSetState`方法将锁的状态从0设置为`acquires`指定的值。
            // 将当前线程设置为独占锁的所有者线程。
            setExclusiveOwnerThread(current);
            return true; // 返回`true`,表示当前线程成功获取了锁。
        }
    }
    // 只有当前线程是独占锁的所有者线程时,才能继续获取锁。
    else if (current == getExclusiveOwnerThread()) {
        // 如果当前线程是独占锁的所有者线程,它会更新锁的状态并返回获取成功。
        int nextc = c + acquires; // 计算新的锁状态,加上`acquires`指定的值。
        if (nextc < 0) // overflow 如果新的锁状态小于0,表示发生了溢出。
            throw new Error("Maximum lock count exceeded"); // 抛出一个错误,表示锁的计数超过了最大限制。
        // 将锁的状态设置为新的值`nextc`。
        setState(nextc);
        return true; // 返回`true`,表示当前线程成功获取了锁。
    }
    // 如果以上条件都不满足,表示当前线程无法获取锁,返回`false`。
    return false;
}

nonfairTryAcquire方法中,首先会调用Thread.currentThread()方法获取当前线程。然后,它会调用getState()方法获取当前同步状态。如果同步状态为0,则说明当前没有线程持有锁,可以直接获取锁。此时,nonfairTryAcquire方法会调用compareAndSetState(0, acquires)方法尝试获取锁,并将当前线程设置为独占线程。

如果同步状态不为0,则说明当前有线程持有锁。此时,如果当前线程就是独占线程,则可以直接增加同步状态的值,并返回true。否则,当前线程无法获取锁,返回false

这个锁就像一个房间,只能有一个人进入。如果房间里没有人,那么任何人都可以进入,但是要把门锁上,并且在门上写上自己的名字,表示自己是房间的主人。如果房间里已经有人了,那么只有房间的主人才能再次进入,但是要在门上记录自己进入的次数,表示自己占用了多少空间。如果房间的主人进入的次数太多,超过了房间的容量,那么就会发生错误,表示房间已经满了。如果不是房间的主人想要进入,那么就会被拒绝,表示房间已经被别人占用了。

这种方式是非公平的,因为它不考虑等待队列中是否有其他线程比当前线程更早地请求锁,而是直接尝试获取锁。这样可以减少一些上下文切换的开销,但也可能导致某些线程长时间得不到锁。

int nextc = c + acquires; // 计算新的房间状态,加上指定的值(acquires)。其中这段就是可重入锁的功能支持

公平锁tryAcquire源码分析

/**
 * 用于公平锁的同步对象
 */
static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    /**
     * 公平版本的tryAcquire。除非是递归调用、没有等待者或者是第一个,否则不授予访问权限。
     */
    @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("超过最大锁定次数");
            // 更新厕所使用次数,并继续停留
            setState(nextc);
            return true;
        }
        // 你不是第一个在排队的,也不能进入厕所
        return false;
    }
}
  • 获取当前线程和锁的状态(state)。
  • 如果state0,表示锁是空闲的,那么就判断当前线程是否在AQS队列的头部,如果是,就用CAS操作尝试将state设置为acquires(默认为1),如果成功,就将当前线程设置为锁的持有者,并返回true。这里的hasQueuedPredecessors()方法是用来检查当前线程是否在队列头部的,它是公平锁和非公平锁的主要区别,公平锁会考虑等待队列中的线程顺序,而非公平锁不会。
  • 如果state不为0,表示锁已经被占用,那么就判断当前线程是否是锁的持有者,如果是,就实现可重入性,将state加上acquires,并返回true
  • 如果当前线程不是锁的持有者,也不在队列头部,就返回false,表示获取锁失败。

这种方式是公平的,因为它会按照等待队列中的线程顺序来获取锁,避免了某些线程长时间得不到锁的情况。但是这样也会增加一些上下文切换和CAS操作的开销。

公平和非公平的区别就在!hasQueuedPredecessors()这里

检查是否没有人在你前面排队

hasQueuedPredecessors保证了只有队列头部的线程才能尝试获取锁,避免了其他线程插队的情况。

/**
 * 判断当前线程是否有在等待队列中的前驱节点(前面排队等待的线程)。
 */
public final boolean hasQueuedPredecessors() {
    Node h, s;

    // 获取头节点
    if ((h = head) != null) {
        // 获取头节点的下一个节点
        if ((s = h.next) == null || s.waitStatus > 0) {
            s = null; // 并发取消时遍历队列
            // 从队尾向头节点方向遍历等待队列
            for (Node p = tail; p != h && p != null; p = p.prev) {
                // 找到等待状态不大于 0 的节点,即前驱节点
                if (p.waitStatus <= 0)
                    s = p;
            }
        }
        // 如果存在前驱节点,且前驱节点的线程不是当前线程,则有前驱节点在等待
        if (s != null && s.thread != Thread.currentThread())
            return true;
    }
    // 没有前驱节点在等待
    return false;
}

  • 获取队列的头结点和尾结点。
  • 如果头结点不为空,那么就获取头结点的下一个结点,如果下一个结点为空或者状态大于0,表示下一个结点被取消了,那么就从尾结点开始向前遍历,找到第一个状态小于等于0的结点,作为下一个结点。
  • 如果下一个结点不为空,并且它的线程不是当前线程,那么就返回true,表示当前线程有前驱结点,也就是说当前线程不在队列头部。
  • 如果下一个结点为空或者它的线程是当前线程,那么就返回false,表示当前线程没有前驱结点,也就是说当前线程在队列头部。

判断当前线程在等待队列中是否有前驱节点。

addWaiter源码分析

addWaiter方法的作用是将一个新的等待节点添加到同步队列的尾部,如果队列为空,则初始化一个新的队列。该方法的参数mode是一个Node对象,表示节点的等待模式,可以是独占模式或共享模式。该方法返回添加的节点,以便后续的操作

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; // 将原来的尾节点的next引用指向新节点
                return node; // 返回新节点,表示成功添加到等待队列
            }
        } else {
            initializeSyncQueue(); // 初始化同步队列
        }
    }
}

acquireQueued函数源码分析

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; // 帮助垃圾回收,断开前驱节点的链接
                return interrupted; // 返回是否被中断过
            }
            if (shouldParkAfterFailedAcquire(p, node)) // 判断是否应该挂起当前线程
                interrupted |= parkAndCheckInterrupt(); // 挂起当前线程并检查是否被中断过
        }
    } catch (Throwable t) {
        cancelAcquire(node); // 取消获取资源的操作
        if (interrupted)
            selfInterrupt(); // 自己标记为中断状态
        throw t; // 抛出异常
    }
}
/**
 * 在排队等候进入餐厅的过程中,判断当前人是否应该进入等待状态。
 * @param pred 前一个人(节点)
 * @param node 当前人(节点)
 * @return 是否应该进入等待状态
 */
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus; // 获取前一个人的等待状态
    if (ws == Node.SIGNAL)
        /*
         * 前一个人已经设置了等待状态为 SIGNAL,所以可以安全地挂起等待。
         */
        return true;
    if (ws > 0) {
        /*
         * 前一个人已经取消了等待状态。跳过所有已经取消等待的前一个人,并指示重试。
         */
        do {
            node.prev = pred = pred.prev; // 跳过前一个人,继续查找
        } while (pred.waitStatus > 0);
        pred.next = node; // 更新前一个人的下一个人为当前人
    } else {
        /*
         * 等待状态必须是 0 或 PROPAGATE。指示需要一个信号,但是还不要挂起等待。
         * 调用者需要重试,以确保在挂起等待之前不能获取资源。
         */
        pred.compareAndSetWaitStatus(ws, Node.SIGNAL); // 设置等待状态为 SIGNAL
    }
    return false;
}

lock源码部分总结

假设你是一名学生,而锁代表着一间图书馆中的一个座位。你想要获取座位来进行学习。下面是一个通过比喻来解释整个过程的描述:

你走进图书馆,发现图书馆的座位已经被其他人占用了。你想要获取一个座位,于是你进行如下操作:

  1. 首先,你去找图书馆管理员(sync)询问是否有空余的座位。
  2. 管理员告诉你当前座位已经被其他人占用了,你无法直接获取到座位。你需要按照规定排队等待。
  3. 于是,你将自己的信息写在一张等待的名单上(addWaiter(Node.EXCLUSIVE)),然后加入到等待队列的末尾。
  4. 等待队列中已经有一些人在等待获取座位了。你加入到队列末尾后,需要等待前面的人依次获取到座位,直到轮到你。
  5. 当前面的人获取到座位并离开后,你终于轮到你了。你被唤醒并尝试再次获取座位。
  6. 如果你成功获取到座位,你可以开始学习了。如果没有成功获取到座位,说明还有其他人比你更早进入队列,你需要继续等待。
  7. 如果你持续无法获取到座位,为了避免你永远无法获取座位,你会主动中断自己的等待状态,以便有机会重新查看是否有空余的座位。

整个过程中,你尝试获取座位的方式遵循了非公平策略(NonfairSync)。你首先尝试直接获取座位,如果失败则进入等待队列,并等待轮到你获取座位。当座位被释放时,等待队列中的下一个人会被唤醒并尝试获取座位。

unlock源码分析

public void unlock() {
    sync.release(1); // 好比释放一个厕位,让其他人可以使用
}

public final boolean release(int arg) {
    if (tryRelease(arg)) { // 尝试释放指定数量的厕位, 这里arg = 1 表示有人拉完了
        Node h = head; // 获取等待队列的头部,类似获取第一个排队的人
        if (h != null && h.waitStatus != 0) // 检查队列不为空且第一个人等待状态不为0
            unparkSuccessor(h); // 唤醒后继的排队者,让他们有机会竞争使用厕位
        return true; // 返回 true 表示成功释放了厕位
    }
    return false; // 返回 false 表示未成功释放厕位
}

这段代码的核心是通过 AQS 的机制来管理同步队列中等待的线程。当锁被释放时,AQS 会唤醒等待队列中的一个后继线程,让它有机会获取资源。这样可以保证资源的有序释放和获取,实现了锁的正确释放和竞争。

@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); // 将独占线程设置为 null,类比为厕所不再被占用
    }

    setState(c); // 更新状态,类比为厕位释放通知其他等待线程

    return free; // 返回是否完全释放了资源
}

共享锁模式

这里我们基于Semaphore类进行分析共享锁模式

例子:Semaphore 控制并发访问

假设我们有一个咖啡机,可以同时为最多3个人冲泡咖啡。在这个场景中,我们可以使用 Semaphore 来模拟这个过程。

  1. 创建 Semaphore 对象: 创建一个 Semaphore 对象,设置初始许可证数量为3,表示同时允许最多3个人使用咖啡机。

    Semaphore coffeeMachineSemaphore = new Semaphore(3);
    
  2. 模拟多人使用咖啡机: 假设有5个人同时想要使用咖啡机。

    for (int i = 0; i < 5; i++) {
        new Thread(() -> {
            try {
                coffeeMachineSemaphore.acquire(); // 获取许可证,如果没有可用则等待
                // 使用咖啡机冲泡咖啡
                System.out.println("Brewing coffee...");
                Thread.sleep(2000);
                System.out.println("Coffee brewed!");
            } catch (InterruptedException e) {
                // 处理中断异常
            } finally {
                coffeeMachineSemaphore.release(); // 释放许可证,离开咖啡机
            }
        }).start();
    }
    

在这个例子中,只有三个人可以同时使用咖啡机,因为我们创建了一个允许最多3个许可证的 Semaphore。前三个人能够获取许可证并使用咖啡机,而其他人需要等待,直到有人释放许可证。

根据上面的案例我们可以找到底层源码:

public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
    // 检查当前线程是否被中断,如果被中断则抛出 InterruptedException 异常
    if (Thread.interrupted())
        throw new InterruptedException();
    
    // 尝试以非阻塞方式获取共享资源
    if (tryAcquireShared(arg) < 0)
        // 如果资源不可用,进行进一步处理
        doAcquireSharedInterruptibly(arg);
}

tryAcquireShared公平锁方式

static final class FairSync extends Sync {
    private static final long serialVersionUID = 2014338818796000944L;
	// permits 用于确定初始的共享资源数量或其他与同步机制相关的设置
    FairSync(int permits) {
        super(permits);
    }

    // 尝试获取共享资源的方法
    protected int tryAcquireShared(int acquires) {
        for (;;) {
            // 检查是否有排在当前线程之前的等待线程
            if (hasQueuedPredecessors())
                return -1; // 如果有等待线程存在,返回 -1,表示无法获取资源

            // 获取当前可用的共享资源数量
            int available = getState();

            // 计算剩余的共享资源数量
            int remaining = available - acquires;

            // 如果剩余的共享资源数量小于 0,或者通过 compareAndSetState 方法成功将状态更新为剩余数量,则返回剩余数量
            if (remaining < 0 || compareAndSetState(available, remaining))
                return remaining;
        }
    }
}

doAcquireSharedInterruptibly源码分析

private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
    // 将一个新的等待节点(Node)添加到等待队列中,模式为共享模式(Node.SHARED)
    final Node node = addWaiter(Node.SHARED);
    try {
        for (;;) {
            // 获取当前节点的前驱节点
            final Node p = node.predecessor();

            // 如果前驱节点是头节点(即当前节点是等待队列中的下一个节点),则尝试获取共享资源
            if (p == head) {
                // 调用 tryAcquireShared 方法尝试获取共享资源,传递 arg 参数
                int r = tryAcquireShared(arg);

                // 如果成功获取到共享资源(返回值大于等于 0)
                if (r >= 0) {
                    // 将当前节点设置为新的头节点,并根据返回值 r 进行共享状态的传播
                    setHeadAndPropagate(node, r);
                    p.next = null; // 帮助 GC
                    return; // 成功获取共享资源,方法结束
                }
            }

            // 如果无法获取共享资源,根据等待策略决定是否需要挂起当前线程,并检查是否被中断
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } catch (Throwable t) {
        // 捕获可能抛出的异常
        cancelAcquire(node);
        throw t;
    }
}

通过循环尝试获取共享资源,并根据等待策略决定是否需要挂起当前线程。如果成功获取共享资源,方法返回;如果线程被中断,抛出 InterruptedException 异常;如果捕获到其他异常,取消获取操作并重新抛出异常。

static final class FairSync extends Sync {

    protected int tryAcquireShared(int acquires) {
        for (;;) {
            if (hasQueuedPredecessors())
                return -1;
            int available = getState();
            int remaining = available - acquires;
            if (remaining < 0 ||
                compareAndSetState(available, remaining))
                return remaining;
        }
    }
}
  • 方法使用一个无限循环,每次循环都判断是否有前驱节点在等待队列中,如果有,则返回-1,表示当前线程需要排队等待。这是为了保证公平性,避免当前线程插队抢占许可证。
  • 方法获取当前状态(许可证数量)available,并计算请求许可证后的剩余数量remainingremaining等于available减去请求的数量acquires
  • 方法判断remaining是否小于0,如果是,则说明许可证不足,返回remaining,表示当前线程需要排队等待。如果不是,则使用CAS操作将状态从available更新为remaining,如果成功,则返回remaining,表示当前线程获取许可证成功。如果失败,则继续循环,重新尝试CAS操作。
public final boolean hasQueuedPredecessors() {
    Node h, s;
    if ((h = head) != null) {
        if ((s = h.next) == null || s.waitStatus > 0) {
            s = null;
            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;
}

hasQueuedPredecessors()的作用是判断当前线程是否有在队列中排在它前面的线程。如果有,那么返回true,否则返回false

  • 获取队列的头结点h和尾结点tail
  • 如果头结点h不为空,那么获取头结点的下一个结点s。
  • 如果下一个结点s为空或者swaitStatus大于0,说明s已经被取消或者已经获取到锁,那么将s置为null,并从尾结点开始向前遍历队列,找到第一个waitStatus小于等于0的结点作为s。这样做是为了处理并发取消的情况。
  • 如果s不为空,并且s的线程不是当前线程,那么返回true,表示当前线程有在队列中排在它前面的线程。
  • 否则,返回false,表示当前线程是队列的第一个有效结点或者队列为空。

释放共享锁

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) { // 尝试释放共享许可证
        doReleaseShared(); // 执行共享许可证释放操作
        return true;
    }
    return false;
}
  • 方法调用tryReleaseShared(arg)尝试释放共享许可证,arg表示要释放的数量。如果释放成功,则返回true,否则返回false。这个方法将state加上arg,并判断是否有剩余资源

  • 如果tryReleaseShared(arg)返回true,表示释放许可证成功,那么方法就调用doReleaseShared()执行共享许可证释放操作。这个方法是在AQS中定义的,它的作用是唤醒后继节点并确保传播效果。

  • 方法根据tryReleaseShared(arg)的返回值返回truefalse,表示释放操作是否成功。

protected final boolean tryReleaseShared(int releases) {
    for (;;) { // 无限循环,直到成功或抛出异常
        int current = getState(); // 获取当前状态(许可证数量)
        int next = current + releases; // 计算释放后的状态(许可证数量)
        if (next < current) // 如果状态溢出,抛出异常
            throw new Error("Maximum permit count exceeded");
        if (compareAndSetState(current, next)) // 使用 CAS 更新状态
            return true; // 成功释放许可证,返回 true
    }
}
  • 方法使用一个无限循环,每次循环都重新获取当前状态(许可证数量)current,并计算释放后的状态(许可证数量)nextnext等于current加上释放的数量releases
  • 方法判断next是否小于current,如果是,则说明状态溢出,抛出错误。这是为了防止许可证数量超过int的最大值。
  • 方法使用CAS操作将状态从current更新为next,如果成功,则返回true,表示释放许可证成功。如果失败,则继续循环,重新尝试CAS操作。

它的作用是增加许可证的数量,并唤醒后继节点

这段代码int next = current + releases; // 计算释放后的状态(许可证数量)和上面的tryAcquireSharedint remaining = available - acquires;代码对应, 获取资源是减法, 释放资源是加法, 释放一次, 就可以有一个新的线程来争夺一个资源

doReleaseShared的作用是唤醒后继节点并确保传播效果

private void doReleaseShared() {
    for (;;) { // 无限循环,直到条件满足
        // 每次循环都重新获取头节点h,并判断h是否为空或者是否等于尾节点。
        Node h = head;
        // 并判断h是否为空或者是否等于尾节点。如果是,则说明同步队列为空或者只有一个哑节点,不需要进行后续操作,直接跳出循环。
        // 否则, 方法获取头节点h的等待状态ws,并根据ws的值进行不同的处理
        if (h != null && h != tail) {
            int ws = h.waitStatus; // 获取头节点的等待状态
            if (ws == Node.SIGNAL) {
                // 如果ws等于Node.SIGNAL,表示头节点h的后继节点需要被唤醒。此时,方法使用CAS操作将h的等待状态从Node.SIGNAL修改为0,如果成功,则调用unparkSuccessor(h)方法唤醒h的后继节点。如果失败,则继续循环,重新检查情况。
                if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
                    continue; // 重新检查,继续循环
                // 如果设置成功,调用`unparkSuccessor(h)`方法来唤醒后继节点。
                unparkSuccessor(h); // 唤醒后继节点
            }
            // 如果ws等于0,表示头节点h的后继节点可能已经被唤醒或即将被唤醒,并且需要传播效果。此时,方法使用CAS操作将h的等待状态从0修改为Node.PROPAGATE,如果成功,则继续循环。如果失败,则也继续循环,重新尝试CAS操作。
            else if (ws == 0 && !h.compareAndSetWaitStatus(0, Node.PROPAGATE))
                continue; // 重新检查,继续循环
        }
        // 方法判断头节点h是否发生了变化,如果是,则说明有新的节点获取了资源并成为了新的头节点,需要再次循环检查是否需要唤醒后继节点。如果否,则说明头节点h没有变化,可以跳出循环。
        if (h == head)
            break;
    }
}

问: 哑节点是什么?

答: AQS哑节点是一个用于实现CLH队列的虚拟节点,它不代表任何真实的线程,而是作为队列的头节点,方便操作和管理队列。AQS哑节点的作用是:

  • 保证队列的非空性,避免空指针异常
  • 作为锁的持有者,当锁被释放时,哑节点会唤醒它的后继节点(即等待锁的第一个线程)
  • 作为一个标志,当哑节点的后继节点是唯一的等待线程时,表示队列已经处于空闲状态

AQSwaitStatus状态是一个表示同步器节点等待状态的整数字段,它有以下几种取值:

CANCELLED(1):表示该节点由于超时或中断而被取消。一旦节点被取消,它的状态就不会再改变,也不会再参与同步队列的操作。取消节点的线程也不会再阻塞。 SIGNAL(-1):表示该节点的后继节点已经或即将被阻塞(通过park方法),因此当前节点在释放或取消时必须唤醒后继节点。为了避免竞争,acquire方法在前驱节点为SIGNAL状态时才会重试原子获取锁,如果失败则阻塞自己。 CONDITION(-2):表示该节点当前在条件队列中。标记为CONDITION的节点会被移动到一个特殊的条件等待队列,直到收到条件信号时才会重新移动到同步等待队列。这个值与字段的其他用途无关,但是简化了机制。 PROPAGATE(-3):表示应该将releaseShared方法的传播效果传递给其他节点。这个值在doReleaseShared方法中设置(仅适用于头节点),以确保传播继续,即使之后有其他操作介入。 0:表示节点不需要发出信号或被传播。对于正常的同步节点,该字段初始化为0;对于条件节点,该字段初始化为CONDITION。它是使用CAS方法修改的(或者在可能的情况下,使用无条件的volatile写入)。

帮助理解共享锁模式

  • 活动背景: 在一场美食节上,多名顾客(线程)聚集在3个摊位前,准备品尝美味食物,这些食物就如同共享资源。每位顾客都拿着食物盘(资源获取的容器)。
  • 排队情况: 每位顾客会看看前面有没有人在排队,如果有,他们会礼貌地等待。毕竟,大家都希望有序地品尝美味,不希望造成混乱。
  • 可品尝食物估计: 每位顾客都对自己能够品尝到多少美味心里有数。他们知道自己的胃口,也了解摊位提供的美味数量。
  • 品尝美味: 3个顾客站在3个摊位前(N表示Semaphore生产几个资源),看着美味的食物。他决定是否有足够的胃口来品尝食物。如果够,他会高兴地拿起食物,表示他成功获取了。同时,他会向其他顾客传达这个好消息。
  • 资源不足: 如果第N+1个顾客发现美味食物不够了,他会留下遗憾的眼神,因为他明白资源不能浪费。然后,他会离开摊位,为其他顾客腾出机会。
  • 等待与观望: 后面的顾客会看到前面的人没有成功获取食物,他们会明白食物可能不够分了,所以他们会耐心地等待,但也会保持警觉,观察前面的情况。
  • 资源释放: 如果前面的顾客放下食物,后面的顾客会认识到美味已经释放出来了,他们会迅速走上前去,准备品尝。就像是顾客们依次品尝,确保每个人都能尝到美味。
  • 中断处理: 如果在品尝的过程中突然传来紧急公告,说明有其他事情需要处理,每个顾客都会停下,因为他们知道在处理这个情况之前,不能继续品尝。