AQS(AbstractQueuedSynchronizer)源码解析(共享锁)

本文深入剖析了AQS(AbstractQueuedSynchronizer)中的共享锁机制,包括共享锁的获取和释放流程,以及与独占锁的主要区别。重点介绍了响应中断和忽略中断两种共享锁获取方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

阅读须知

  • JDK版本:1.8
  • 文章中使用/* */注释的方法会做深入分析

正文

之前我们分析了 AQS 的独占锁,AQS 同样支持共享锁,jdk 并发包中的 ReentrantReadWriteLock、Semaphore 等,都是基于 AQS 共享锁实现,推荐读者首先阅读 AQS(AbstractQueuedSynchronizer)源码解析(独占锁)这篇文章,里面有对 AQS 的基本介绍和独占锁部分的源码分析,可以方便大家更好的理解本文。下面我们来分析 AQS 的共享锁,首先我们来看可以响应中断的共享锁获取方法:
AbstractQueuedSynchronizer:

public final void acquireSharedInterruptibly(int arg)
    throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 尝试以共享模式获得锁
    if (tryAcquireShared(arg) < 0)
        /* 采用共享中断模式 */
        doAcquireSharedInterruptibly(arg);
}

这里的 tryAcquireShared 方法由子类实现共享锁获取逻辑,默认抛出 UnsupportedOperationException 表示不支持共享模式,我们看到 tryAcquireShared 方法的返回值是 int 型,我们来看一下 tryAcquireShared 方法返回值的含义:

返回负值代表获取共享锁失败;如果当前线程获取共享锁成功,但后续线程没有办法成功获取共享锁,则返回零;如果当前线程获取共享锁成功并且后续线程也有可能成功获取共享锁,则返回正值,在这种情况下,后续等待线程必须检查可用性。(对三种不同返回值的支持使得这个方法可以在独占时才能执行的环境中使用。)一旦成功,就获得了锁。

通过这段介绍,我们能获取到另外一个结论,tryAcquireShared 方法返回值等于0时,因为后续线程没有办法获取共享锁,所以后续的线程是不需要唤醒的,反之如果返回值大于0,因为后续线程是有可能成功获取共享锁的,所以当前线程获取到共享锁的同时也要尝试唤醒后续的线程来获取共享锁。

AbstractQueuedSynchronizer:

private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    // 为当前线程和给定模式创建并排队节点
    final Node node = addWaiter(Node.SHARED);
    // 获取共享锁是否成功标记
    boolean failed = true;
    try {
        for (;;) {
            // 获取当前节点的前任节点
            final Node p = node.predecessor();
            // 如果前任节点是头节点,则表示当前节点是下一个应该获得锁的节点
            if (p == head) {
                // 尝试获取共享锁
                int r = tryAcquireShared(arg);
                // 根据上面说明的 tryAcquireShared 方法返回值的含义,这个判断成立代表获取共享锁成功
                if (r >= 0) {
                    /* 设置头结点并传播 */
                    setHeadAndPropagate(node, r);
                    // 帮助 GC
                    p.next = null;
                    failed = false;
                    return;
                }
            }
            // 判断获取锁失败后是否应该阻塞,如果需要阻塞,则在阻塞后判断线程的中断状态
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            // 如果获取锁失败,取消正在进行的获取尝试
            cancelAcquire(node);
    }
}

这里 addWaiter、shouldParkAfterFailedAcquire、parkAndCheckInterrupt、cancelAcquire 四个方法我们在 AQS 独占锁源码分析的文章中都进行过详细说明,这里不再赘述,其中不同的是这里调用 addWaiter 方法传入的是共享模式。
AbstractQueuedSynchronizer:

private void setHeadAndPropagate(Node node, int propagate) {
	// 为下面的检查记录旧的头结点
    Node h = head;
    // 将当前节点设置为头结点
    setHead(node);
    // propagate > 0 表示调用方指明了后继节点需要被唤醒
    // 头结点(旧的或新的)为 null 或者 waitStatus < 0,表明后继节点需要被唤醒
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        // 如果当前节点的后继节点是共享类型或者为 null,则进行唤醒
        // 这里可以理解为除非明确指明不需要唤醒(后继等待节点是独占类型),否则都要唤醒
        if (s == null || s.isShared())
            /* 释放共享锁,并确保转播 */
            doReleaseShared();
    }
}

其实 AQS 独占锁和共享锁最主要的区别就是在于这个方法了,对于独占锁来说,当前线程获取到锁之后,将自己设置为头结点就可以了,不需要再做其他的事情了,但是对于共享锁来说,因为锁是共享的,所以当前线程获取到锁了,要尝试唤醒另外一个以共享模式在队列中等待的线程去获取共享锁,另外一个线程被唤醒后,同样要做相同的事情将唤醒一直传递下去。
AbstractQueuedSynchronizer:

private void doReleaseShared() {
    for (;;) {
        // 这里的头结点已经是上面新设置的头结点了,也就是刚刚获取到共享锁的节点
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            // 如果节点的等待状态是 SIGNAL,证明后面的节点需要被唤醒
            if (ws == Node.SIGNAL) {
                // CAS 操作防止并发重复唤醒
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                	// 如果 CAS 失败循环重新检查
                    continue;
                // 唤醒后继节点
                unparkSuccessor(h);
            }
            // 如果后继节点暂时不需要唤醒,则把当前节点状态设置为 PROPAGATE 确保传播
            else if (ws == 0 &&
             !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
             	// 如果 CAS 失败循环重新检查
                continue;
        }
        // 如果头结点发生变化,需要循环保证唤醒动作的传播
        // 如果头节点未发生变化,则结束当前循环
        if (h == head)
            break;
    }
}

unparkSuccessor 方法我们在 AQS 独占锁源码分析的文章中分析过。doReleaseShared 方法是共享锁中的核心唤醒方法,主要做的事情就是唤醒下一个线程或者设置传播状态。后继线程被唤醒后,会尝试获取共享锁,如果成功获取,则又会调用 setHeadAndPropagate 将唤醒传播下去。这个方法的作用是保障在 acquire 和 release 存在竞争的情况下,保证队列中处于等待状态的节点能够有办法被唤醒。

这里是 PROPAGATE 状态第一次出现也是唯一一次出现的地方,在早期 JDK 版本的 JUC 中,是没有 PROPAGATE 状态的,它的出现是为了修复使用 Semaphore 时可能出现的 bug JDK-6801020,这个 bug 会导致极端情况下信号量中存在1个可用的许可,但是确有一个线程一直等待得不到唤醒,有兴趣的读者可以访问 Java Bug 数据库查看 bug 详情:https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6801020

按照 bug 数据库中的描述,导致 bug 的原因是:“调用 release 方法时,头结点的状态可能为0,因为之前调用 acquire 方法的线程可能正在 doAcquireShared 方法中运行,但是还没有运行到 setHeadAndPropagate 这一行,所以 release 操作就没有唤醒任何线程,紧接着调用 acquire 方法的线程调用 setHeadAndPropagate 方法时,因为头结点的状态还是0,所以 if 中的判断条件不满足也没有运行到下面的 doReleaseShared 方法去唤醒任何线程”。这样就出现了这个 bug 可能会导致的现象。

而在加入 PROPAGATE 状态后,release 操作虽然没有唤醒任何线程,但是会将头结点的状态改为 PROPAGATE,这时调用 acquire 方法的线程调用 setHeadAndPropagate 方法时可以满足h.waitStatus < 0这个判断条件,所以会继续运行到下面 doReleaseShared 方法唤醒等待的线程,这样就解决的这个 bug 出现的问题。

这里可以看到 PROPAGATE 状态增加前后的 diff:http://gee.cs.oswego.edu/cgi-bin/viewcvs.cgi/jsr166/src/main/java/util/concurrent/locks/AbstractQueuedSynchronizer.java?r1=1.73&r2=1.74

通过查看 AQS 代码的提交记录,我们看到 setHeadAndPropagate 方法演进了几个版本:

/**
 * 早期版本
 */
private void setHeadAndPropagate(Node node, int propagate) {
    setHead(node);
    if (propagate > 0 && node.waitStatus != 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            unparkSuccessor(node);
    }
}

/**
 * jdk 7,也就是修复了前面提到的 bug 的版本
 */
private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head;
    setHead(node);
    if (propagate > 0 || h == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

/**
 * jdk 8
 */
private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head;
    setHead(node);
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

对于前两个版本,引入 PROPAGATE 状态确实可能解决前面提到的 bug,但是到了 jdk 8 的版本,增加了对新头结点状态的判断,笔者认为即使没有 PROPAGATE 状态也不会出现之前的 bug 了,所以现在还保留 PROPAGATE 这个状态是因为历史加入了这个状态没有删掉还是有别的原因?这个问题目前还没有找到明确的答案,如果有了解的读者,希望能够指点一下。

下面我们来看共享锁释放的流程:
AbstractQueuedSynchronizer:

public final boolean releaseShared(int arg) {
    // 尝试释放共享锁
    if (tryReleaseShared(arg)) {
        // 释放共享锁,并确保转播,刚刚分析过
        doReleaseShared();
        return true;
    }
    return false;
}

这里的 tryReleaseShared 由子类覆盖,通过尝试设置 state 变量来释放共享锁,默认抛出 UnsupportedOperationException,表示不支持共享模式。

上文我们介绍了响应中断的共享锁获取方法,下面我们来看忽略中断的共享锁获取方法:
AbstractQueuedSynchronizer:

public final void acquireShared(int arg) {
    // 尝试以共享模式获得锁
    if (tryAcquireShared(arg) < 0)
        /* 采用共享不中断模式 */
        doAcquireShared(arg);
}

AbstractQueuedSynchronizer:

private void doAcquireShared(int arg) {
    // 为当前线程和给定模式创建并排队节点
    final Node node = addWaiter(Node.SHARED);
    // 获取共享锁是否成功标记
    boolean failed = true;
    try {
    	// 中断标记,初始化为 false
        boolean interrupted = false;
        for (;;) {
            // 获取当前节点的前任节点
            final Node p = node.predecessor();
            // 如果前任节点是头节点,则表示当前节点是下一个应该获得锁的节点
            if (p == head) {
                // 尝试获取共享锁
                int r = tryAcquireShared(arg);
                // 根据上面说明的 tryAcquireShared 方法返回值的含义,这个判断成立代表获取共享锁成功
                if (r >= 0) {
                    // 设置头结点并传播
                    setHeadAndPropagate(node, r);
                    // 帮助 GC
                    p.next = null;
                    if (interrupted)
                        // 如果中断标记为 true,则中断当前线程
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            // 判断获取锁失败后是否应该阻塞,如果需要阻塞,则在阻塞后判断线程的中断状态
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                // 设置中断标记为 true
                interrupted = true;
        }
    } finally {
        if (failed)
            // 如果获取锁失败,取消正在进行的获取尝试
            cancelAcquire(node);
    }
}

整体逻辑和响应中断的共享锁获取方法很相似,不同的就是在阻塞后判断线程的中断状态后的处理。

到这里,AQS 共享锁部分的源码解析就完成了。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值