Loading

AQS从应用到原理

从高层来看

AQS,即AbstractQueuedSynchronizer类,无论是听起来还是看起来,它都很令人畏惧,但抛离它的实现原理,站在AQS的用户——比如Mutex、CountDownLatch这些类——的视角来看,AQS是一个非常好的助手。

AQS是一个用于实现同步器工具的框架,信号量、Mutex、CountDownLatch、可重入锁都是基于它实现的。它适合编写那些具有一个整数类型的状态的同步器类,对于上面我提到的那些类,它们内部都依赖这个整数状态,只是该整数的意义对于不同的同步器是不一样的:

  • Semaphore:代表当前剩余的可获取信号数
  • Mutex:代表当前互斥量是否被占用,它的状态只用两个数字就能表示,0和1即可
  • CountDownLatch:代表当前距离打开门闩还需要多少次countDown
  • ReentrantLock:代表当前锁被同一个线程重入了多少次

所以,AQS对其使用者提供一个整数状态,使用者可以自定义某一个整数对应的状态,以及其含义。

AQS中最核心的操作称为获取释放,分别对应它的acquirerelease方法。基于AQS实现的同步器会在获取和释放时改变它的同步状态。获取和释放操作都携带一个整型参数,这里我们就称它arg吧,不同的同步器对这两个操作的定义也是不一样的:

同步器 acquire release
Semaphore 如果信号量足够,将同步状态值减少arg,否则获取失败 将同步状态值增加arg
Mutex 如果同步状态为0,将其增加arg(arg必须为1),否则获取失败 如果状态为1,将减少arg(arg必须为1)
... ... ...

同步器要做的最重要的事就是在多个线程间提供同步,在获取时,如果获取失败,线程会被阻塞,当当前持有同步状态的线程释放时,其它阻塞的线程可能会被唤醒。所以,AQS为其使用者提供一个阻塞队列,使用者可以按照AQS的约定,用模板方法和状态与AQS交互,AQS会自动为你实现调用线程的阻塞和唤醒。这个阻塞队列是FIFO的,但并不代表获取操作的顺序也是FIFO的。可以基于AQS实现公平与不公平的获取调度。

同步器有共享和互斥之分,共享的同步器允许多个线程获取,互斥的只允许一个线程获取,当有其它线程已经获取(并尚未释放)时,它的获取将失败。Semaphore应该是共享的,Mutex则无需是。AQS支持exclusiveshared模式

小总结:

  • AQS提供整数状态,状态的含义以及状态之间的迁移由子类定义
  • AQS可以被获取和释放,子类可以在获取和释放操作中自定义状态的迁移逻辑,如果获取失败,阻塞
  • AQS为子类提供了FIFO的阻塞队列,但你可以在其上实现公平或非公平的唤醒调度
  • AQS的获取操作支持共享和互斥两种模式

模板方法以及状态

上面我们大致上了解了AQS是啥,大体上提供了啥,需要使用者提供啥,下面我们来看一些较为实际的。

在使用AQS框架开发同步器时,你要做的就是定义状态,实现框架规定的模板方法,并在模板方法中使用这些状态实现与AQS交互的逻辑。

你需要实现的主要模板方法如下:

  • tryAcquire:独占式获取同步状态
  • tryRelease:独占式释放同步状态
  • tryAcquireShared:共享式获取同步状态
  • tryReleaseShared:共享式释放同步状态
  • isHeldExclusively:是否被独占持有

在这些方法中,你可以通过AQS提供的getStatesetState以及compareAndSetState来获取和改变状态。

比如在一个基于AQS的Mutex实现中,你可能需要定义两个状态:

// 互斥量尚未被占用     =>     0
private static final int STATE_NOTLOCKED = 0;  
// 互斥量已经被占用     =>     0
private static final int STATE_LOCKED = 1;

然后,你需要重写那些模板方法来在这两个状态之间进行迁移,以完成和AQS的交互。如果你不重写,默认的实现是抛出UnsupportedOperationException

AQS的推荐用法

AQS官方文档中给出了该框架的推荐用法,并且JUC包中也都是这样使用的。

同步器需要创建一个私有的内部helper类来继承AQS类,上面所说的定义状态、实现模板方法与AQS交互,都是在这个私有helper类中来完成。而同步器,只需要简单的委托这个helper类即可。一个基于AQS的Mutex实现看起来大概像这样:

public class AQSMutex implements Lock {  
	// helper类
    private static class Sync extends AbstractQueuedSynchronizer {  
	    // Mutex的状态
        private static final int STATE_NOTLOCKED = 0;  
        private static final int STATE_LOCKED = 1;  

		// 模板方法的实现
        @Override  
        protected boolean tryAcquire(int acquires) { ... }  
        @Override  
        protected boolean tryRelease(int releases) { ... }  
        // ...
    }  
	
	// 新建一个helper类的实例
    private final Sync sync = new Sync();  

	// 在Mutex向外暴露的API中,简单的委托sync
    @Override  
    public void lock() { sync.acquire(1); }  
    public void unlock() { sync.release(1); }  
	// ... 
}

部分模板方法的语义

tryAcquire

尝试在exclusive模式下获取。这个方法总是被执行acquire方法的线程调用,如果该方法报告失败(返回false),acquire方法将线程入队(如果它尚未入队的话),直到它被来自其它线程的释放操作通知。

AQS的acquire方法会调用tryAcquire方法,并根据获取的结果决定是否阻塞线程,下面的这个代码虽然短,但是涉及很多东西,先看一眼即可:

public final void acquire(int arg) {  
    if (!tryAcquire(arg) &&  // 若tryAcquire报告失败
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))  
        // addWaiter方法以指定模式将当前线程入队,并返回队列中的新节点,
        // acquireQueued方法会判断是否需要park线程,并且在需要时park,当park被打断时返回true
        selfInterrupt(); // park被打断,恢复当前线程打断状态 
}

下面是基于AQSMutex实现里,tryAcquire的代码:

@Override  
protected boolean tryAcquire(int acquires) {  
    assert acquires == 1;  // 参数必须为1
    // 如果以CAS切换状态成功
    if (compareAndSetState(STATE_NOTLOCKED, STATE_LOCKED)) {  
	    // 设置互斥的拥有者线程为当前线程
        setExclusiveOwnerThread(Thread.currentThread());  
        return true;  // 返回成功
    }  
    return false;  // 返回失败
}

tryRelease

尝试以exclusive模式设置状态,以反应释放操作。这个方法总是被执行release的线程调用。如果操作后该同步器已经被完全释放(比如可重入锁的同步状态已经为0,当前线程已经没有再持有锁了),tryRelease才应该返回true,这时,调用它的release方法会让等待的线程尝试再次acquire

下面是AQS中的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;  
}

下面是基于AQSMutex实现里,tryRelease的代码:

@Override  
protected boolean tryRelease(int releases) {  
    assert releases == 1;  
    if (getState() == STATE_NOTLOCKED) throw new IllegalMonitorStateException();  
    setExclusiveOwnerThread(null);  
    setState(STATE_NOTLOCKED);  
    return true;  
}

tryAcquireShared/tryReleaseShared

以共享模式获取或释放,下面是使用它们实现Semaphere的代码:

@Override  
protected int tryAcquireShared(int arg) {  
    for (;;) {  
        int before = getState();  
        int after = before - arg;  
        if (after < 0 || compareAndSetState(before, after)) {  
            return after;  
        }  
    }  
}  
  
@Override  
protected boolean tryReleaseShared(int arg) {  
    for (;;) {  
        int before = getState();  
        int after = before + arg;  
        // 不考虑整数溢出  
        if (compareAndSetState(before, after)) {  
            return true;  
        }  
    }  
}

在高层部分,我们大体上知道了AQS是干嘛的,使用它实现同步器的基本策略,但有很多细节依然不清楚。这些细节,我们留到低层部分中解决。

从低层来看

从CLH谈起

CLH等待队列是为了实现公平的自旋锁而设计的,AQS中设计了一个它的变体,用于实现阻塞同步器,虽然一个是自旋,一个是阻塞,但它们的基本策略是相同的,每一个线程绑定到队列中的一个节点上,而该有关该线程的控制信息则存在它的节点的前驱节点中

CLH队列的一个示例如下:

img

先说一下CLH的基本原则:

  1. 每个节点的locked属性代表当前节点的线程是否还需要锁
    1. locked == false代表当前线程已经释放了锁
    2. locked == true的一个可能是当前线程正在持有锁,尚未释放
    3. locked == true的另一个可能是当前线程尚未持有锁,正在自旋
  2. 只有该线程的前驱节点locked == false时,当前线程才可以获取到锁

这一系列规则保证了CLH的公平性,head的下一个节点是已经持有锁正在执行的线程,其余的后继节点都是在自旋的线程。在上图中,节点2中的线程1正在执行,节点3中的线程2正在自旋。

CLH中,当前线程能够获取锁的唯一条件是,前驱节点的lockedfalse。那如果当前等待队列里没有正在等待的线程呢?新进入的线程没有前驱节点。所以,CLH中总是有一个虚拟的头节点,它不绑定到任何线程,它的locked始终为false,如果队列中没有线程,它就会作为新线程的前驱节点,这时,新线程可以立即执行。AQS中也有类似的设计。

CLH中,当一个线程申请加锁,它的节点就会被插入到等待队列队尾,并开始在它的前驱节点状态上自旋。有可能有多个线程并发竞争队尾,所以队尾的修改通过CAS操作

一个持有锁的线程释放锁时,它需要滚出队列,但不能破坏那个虚拟头节点。最简单的办法是让它的后继节点接到头结点上,如下图:

img

很遗憾,CLH中的节点并没有保存后继节点的指针,上面的操作难以完成。但我们可以把释放锁的线程绑定到原始的头结点上,让它原来的那个节点作为新的头节点,并切断原来的指针:

img

关于CLH锁的实现,可以看:自旋锁&CLH锁&MCS锁 - 掘金 (juejin.cn)

AQS中的阻塞队列

在开始之前,我们要扭转几个概念:

  1. AQS中操作的是同步状态,不是锁
  2. CLH部分中所说的锁定,也就是lock操作,对应AQS中的acquire同步状态
  3. CLH部分所说的解锁,也就是unlock操作,对应AQS中的release同步状态

AQS中应用了CLH的一个变体,不过这个队列不是通过自旋来让其它线程等待前一个线程释放同步状态的,而是通过将线程阻塞(通过LockSupport.park(this))。线程一旦被阻塞,它便没有能力再去检测前面节点的状态,所以当前驱节点释放时,当前节点会被通知到,并取消阻塞(unpark)。

AQS中的节点定义是这样的:

static final class Node {
	// 状态
    volatile int waitStatus;
	// 前一个节点
    volatile Node prev;
	// 后一个节点
    volatile Node next;
	// 绑定的线程
    volatile Thread thread;
}

waitStatus代表状态,和CLH中的locked作用一致,不过AQS要处理的状态有多种,所以不能只用只能表示两种状态的boolean。AQS中的节点还保存了后一个节点的指针用于实现阻塞机制,因为前置节点在释放时需要唤醒后置节点。它会沿着后置节点遍历,并根据后置节点的waitStatus选择需要被唤醒的那个线程。

waitStatus的状态列表如下,这里我们暂且只关心SIGNALCANCELLED

// 表示线程已经被取消的状态
static final int CANCELLED =  1;
// 表示此节点的后继节点已经(或即将)被park
static final int SIGNAL    = -1;
// 表示线程在条件上等待
static final int CONDITION = -2;
// 表示下一个`acquireShared`应无条件传播
static final int PROPAGATE = -3;

acquire

现在我们来解析acquire操作,首先我们要明白,它是以互斥模式进行获取,只要有一个线程获取成功,并且设置了它是互斥状态的拥有者,在它释放前,其它线程就必须阻塞。

public final void acquire(int arg) {  
    if (!tryAcquire(arg) &&  
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))  
        selfInterrupt();  
}

上面的代码有些奇怪,为了方便,我们给它转换一下:

public final void acquire(int arg) {
    if (!tryAcquire(arg)) {
	    Node curNode = addWaiter(Node.EXCLUSIVE);
	    if (acquireQueued(curNode, arg)) {
		    selfInterrupt();
	    }
    }  
}

首先,它调用子类实现的tryAcquire方法,尝试获取同步状态,如果没有成功,代表当前同步状态被其它线程持有,当前线程需要阻塞了。

addWaiter方法接收一个模式参数,向阻塞队列中添加一个与当前线程绑定的新节点,并且返回新创建的节点。

private Node addWaiter(Node mode) {  
	// 以指定模式创建一个新节点
    Node node = new Node(Thread.currentThread(), mode);  
    // 获取当前尾节点,因为新节点要成为新的尾,原始尾要成为新节点的prev,所以命名pred 
    Node pred = tail;  
    if (pred != null) {  // 如果有尾节点,也就是当前队列不为空
        node.prev = pred; // 当前节点的前一个设成原始尾节点
        if (compareAndSetTail(pred, node)) {  // CAS设置node为新尾节点
            pred.next = node;  // 将原始尾节点的下一个设成当前节点
            return node;  // 返回当前节点
        }  
    }  
    enq(node);  // 如果队列为空,入队
    return node;  // 返回node
}

上面的代码注释已经将addWaiter的实现阐述的较为清晰,其实这里没什么难的,我们再看看enq方法,它只有在当前队列为空时才被调用:

private Node enq(final Node node) {  
    for (;;) {  
        Node t = tail;
        // 重新获取尾节点,并进行空队列判断
        // 因为可能已经有其他线程插入了尾节点,队列已经不为空了 
        // 也有可能是第二次迭代,虚拟节点已经被添加了(稍后就会看到虚拟节点相关的内容)
        if (t == null) {
		    // 如果队列仍然为空,CAS方式设置头尾,为一个虚拟的空节点
            if (compareAndSetHead(new Node()))  
                tail = head;  
        } else {  
	        // 如果队列不为空,则重复`addWaiter`中的CAS设置尾节点这段代码
            node.prev = t;  
            if (compareAndSetTail(t, node)) {  
                t.next = node;  
                return t;  
            }  
        }  
    }  
}

enq方法很巧妙,它动态的创建了虚拟的空节点(只在需要时创建,也就是队列中需要有实际节点时),并且利用一个循环迭代,既做了实际节点在虚拟节点创建后插入,又完成了两个CAS操作的失败后的重试,又解决了多个线程的竞相enq时的错乱问题。

下面回到acquire

Node curNode = addWaiter(Node.EXCLUSIVE);
if (acquireQueued(curNode, arg)) {
	selfInterrupt();
}

创建新节点后,还得阻塞当前线程啊,这里调用了acquireQueued,传入了当前线程绑定的节点,还传入了获取的参数:

final boolean acquireQueued(final Node node, int arg) {  
    boolean failed = true;  
    try {  
        boolean interrupted = false;  
        for (;;) {  // 注意这里是个循环
	        // 获取节点的前驱节点
            final Node p = node.predecessor();  
            // 如果它的前驱已经是虚拟的头节点了,代表前面已经没有正在执行的线程了
            // 再次尝试acquire,如果成功
            if (p == head && tryAcquire(arg)) {  
                setHead(node);  // 将节点设为头 这个操作会清空当前节点绑定的线程和当前节点的前置节点
                p.next = null; // 帮助GC 忽略
                failed = false;  // 设置failed标志为true,代表成功
                return interrupted;  // 返回是否被打断
            }  
            // 判断在获取失败后是否需要park线程
            if (shouldParkAfterFailedAcquire(p, node) &&  
                parkAndCheckInterrupt())  
                interrupted = true;  
        }  
    } finally {  
        if (failed)  // 如果失败,就取消获取操作
            cancelAcquire(node);  
    }  
}

这里的代码其实也很好理解,其实你理解了CLH就不难理解这个。还有这个代码写的真是太巧妙了,很多地方让你想在文字里大说特说,但又让你觉得这样写本就是理所当然的,没必要说,其它方式都是弯路。太牛逼了简直。我希望你也体会到了这种巧妙。

acquireQueued中有个死循环,这也就说明,它会一直尝试获取前驱节点,直到它已经是第一个节点了(前驱节点为head)并且尝试acquire已经成功。这两个条件若有一个没满足,都调用shouldParkAfterFaildAcquire(p, node)获取是否应该阻塞线程,如果返回应该阻塞,那么就阻塞并检查线程的打断状态,并设置interrupt标志。当线程的阻塞被唤醒时,代表有线程释放了,它又会走到死循环的入口,并tryAcquire,如此往复,直到成功(或者发生异常进入finally)。

下面,我们看看shouldParkAfterFailedAcquire的实现,这里传入的是当前节点的前驱节点和当前节点。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus; // 获取前驱节点的waitStatus
    if (ws == Node.SIGNAL) // 如果前驱节点状态为SIGNAL
        return true; // 直接返回true,这代表一个节点的状态如果为SIGNAL,它就会在释放时唤醒后继节点,所以直接返回true,让调用者park当前节点的线程就行

    // 如果ws > 0,大于0的状态只有一个,就是CANCELLED,代表它的直接前驱已经被取消了
    if (ws > 0) {
        // 从前驱中获取第一个尚未被取消的节点,我们称之为有效节点吧
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        // 跳过中间节点,直接让这个有效的节点的下一个指向当前节点
        pred.next = node; 
    } else {
        // 将前置节点的waitStatus从ws设成SIGNAL
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    // 返回false
    return false;
}

这里主要是摘除已经被取消的节点,并且从代码中我们也能看出来,一个节点的状态为SIGNAL,代表会在释放时通知后继节点,这时我们可以让其后继节点park了。

如果前置节点的状态不为SIGNAL并且也不为CANCELLED,这个方法会尝试用CAS方式将其状态设置成SIGNAL。不过不管怎样,只要前置节点之前不为SIGNAL,都返回false。在acquireQueued的死循环中,tryAcquire将再次发生,失败后shouldParkAfterFailedAcquire将再次被调用,直到某次它返回true,当前节点的线程被挂起。

acquireQueued方法会在挂起的线程停止挂起状态时,检查其打断状态,并报告给调用者,也就是acquireacquire中会使用selfInterrupt方法恢复打断状态。

release

tryRelease返回true时,该方法会尝试取消后继的阻塞。successor是后继的意思。

public final boolean release(int arg) {  
    if (tryRelease(arg)) {  
        Node h = head;  // 获取头节点,如果头节点不等于空(队列中有数据)并且头节点的阻塞状态不为`0`
        if (h != null && h.waitStatus != 0)  
            unparkSuccessor(h);  // 取消后继的阻塞
        return true;  
    }  
    return false;  
}

waitStatus == 0这个状态是节点的默认状态,并且文档中显示当与Condition共同使用时这个状态好像有用。我们看unparkSuccessor(h),这里传入的是头节点。

private void unparkSuccessor(Node node) {
    // 如果当前节点的status是负数(比如需要signal),尝试清空
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    // 待唤醒的线程在后继节点中,通常就是下一个节点。
    // 但是如果出现取消或者下一个节点为null的情况
    // 从尾节点反向遍历,找到非取消状态的后继
    Node s = node.next; // 找到下一个节点
    if (s == null || s.waitStatus > 0) { // 如果下一个节点是空或者已经被取消了
        s = null; // 将s设成null
        for (Node t = tail; t != null && t != node; t = t.prev) // 从尾节点向前遍历
            if (t.waitStatus <= 0) // 队列中第一个(正序)没被取消的,设成s
                s = t;
    }
    // 取消s节点绑定线程的阻塞
    if (s != null)
        LockSupport.unpark(s.thread);
}

Java AQS unparkSuccessor 方法中for循环从tail开始而不是head的疑问?

一个没明白的点,这里后继节点被唤醒,也并不要求当前节点nodewaitStatusSIGNAL啊。

公平性

嘶。。。老夫看了这么多代码,花了一上午的时间。。。这特喵的AQS本来不就是公平的么!

肯定是我漏掉了什么。。。我百度之后,发现了这篇文章,一语惊醒梦中人!

Java并发编程:AQS的公平性_Java_码农架构_InfoQ写作社区

如果跟着读下来,你并没有掉入“感觉AQS本来就是公平的”这一技术陷阱,那你可以跳过这一节了。对于那些和我一样的,我们可能忘了很重要的一点:新来的线程可能和已经在阻塞队列中的线程争抢同步状态。重新看下acquire的代码:

public final void acquire(int arg) {  
    if (!tryAcquire(arg) &&  
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))  
        selfInterrupt();  
}

阻塞队列确实是按照FIFO机制唤醒的,但是新来的线程上来就凋了一次tryAcquire,它会和阻塞队列中的线程争抢同步状态。这对性能来说是好的,但这让公平性完全丢失了。

我们可以看下ReentrantLock如何在这种非公平的抢占式闯入策略下如何实现公平锁的:

protected final boolean tryAcquire(int acquires) {  
	// ...省略一些代码
		if (!hasQueuedPredecessors() &&  
			compareAndSetState(0, acquires)) {  
			setExclusiveOwnerThread(current);  
			return true;  
		}  

	// ...省略一些代码
}

ReentrantLock中的公平锁的tryAcquire实现只比非公平版本多了一个调用,hasQueuedPredecessors,该函数是AQS实现的,用于判断当前队列中是否已经有排队的线程了。公平版本的锁在没有排队线程时才进行实际的获取操作,否则直接返回false,向AQS宣告获取失败,接着它就会被放到阻塞队列尾部并park起来了。

共享模式

获取

public final void acquireShared(int arg) {  
    if (tryAcquireShared(arg) < 0)  
        doAcquireShared(arg);  
}

相比acquire方法,acquireShared没那么多花活儿,很简单,但我怕花活藏在doAcquireShared里......

还是先回顾一下,tryAcquireShared方法应该由子类同步器实现,返回负数时代表失败,返回0时代表成功,但后续的共享模式获取不会成功(至少在同步状态没被释放之前),返回正数代表成功,后续的共享模式获取仍有可能成功。

这里可以看到,在tryAcquireShared报告失败时,AQS会调用doAcquireShared,我们去看doAcquireShared的代码:

private void doAcquireShared(int arg) {  
	// 以共享方式添加节点
    final Node node = addWaiter(Node.SHARED);  
    boolean failed = true;  
    try {  
        boolean interrupted = false;  
        for (;;) {  
            final Node p = node.predecessor();  // 寻找前驱节点
            if (p == head) {  // 如果前驱节点是头
                int r = tryAcquireShared(arg);  // 重新尝试获取 
                if (r >= 0) {  // 如果成功
                    setHeadAndPropagate(node, r); // [ 注意这里 ]  
                    p.next = null; // help GC  
                    if (interrupted)  
                        selfInterrupt();  
                    failed = false;  
                    return;  
                }  
            }  
            if (shouldParkAfterFailedAcquire(p, node) &&  
                parkAndCheckInterrupt())  
                interrupted = true;  
        }  
    } finally {  
        if (failed)  
            cancelAcquire(node);  
    }  
}

其实整体逻辑没啥不一样的,只不过,原先的setHead调用,在这里变成了setHeadAndPropagate,从名字来看,它将当前节点设成了头并进行了某种传播。还要注意的是,这里多了一个新的参数,tryAcquireShared方法的返回值r被传入了。

我们看看这个返回值代表了啥,共享同步器Semaphere类中,这个返回值是当前还有的信号量数量,我们可以推测,在返回值大于零(实际上等于也在代码范围内)的情况下,AQS会想办法调度其它的线程节点,因为当前还有同步状态可以使用:

final int nonfairTryAcquireShared(int acquires) {  
    for (;;) {  
        int available = getState();  
        int remaining = available - acquires;  
        if (remaining < 0 ||  
            compareAndSetState(available, remaining))  
            return remaining;  
    }  
}

我们去到setHeadAndPropagate中:

private void setHeadAndPropagate(Node node, int propagate) {
	Node h = head; // Record old head for check below
	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();
	}
}

前两行我就不解释了,我们之前都见过,就是把当前节点废掉,作为head

然后,propagate参数,也就是tryAcquireShared返回的值r,如果它大于零,或者hwaitStatus为正常情况,或者hnull,或者当前head为null的情况下,就获取后继节点,如果后继节点也是Shared的,就调用doReleaseShared

hhead不一定是一个,并且它们也不一定不为null,这在具有多个acquirerelease相竞争的情况下可能出现,所以这里是一个保守的判断,即使它可能导致不必要的唤醒。

释放

用于共享释放的releaseShared方法里肯定也调用了doReleaseShared,所以我们先看releaseShared

public final boolean releaseShared(int arg) {
	if (tryReleaseShared(arg)) {
		doReleaseShared();
		return true;
	}
	return false;
}

平平无奇,不想解释。

我们看看doReleaseShared

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

有了前面的知识,其实这个方法也没啥。

该方法首先获取头节点,判断头节点的状态是否为SIGNAL,然后尝试清空头节点状态,成功就调用我们之前已经读过的unparkSuccessor,不成功可能就是有正在竞争操作头节点的线程,这时外层的循环会再次重复前面的步骤。如果头节点的状态是0,将其状态设为Node.PROPAGATE,并继续。如果某一次循环并没发生竞争,后面的h == head用于作为循环的终止条件,当没有竞争发生时,h == head一定成立,这时,该做的操作都做完了,跳出循环。

unparkSuccessor会让一个节点的后继节点中第一个等待被唤醒的节点中的线程unpark,这个线程被unpark后,它会继续尝试tryAcquireShared,如果成了,上面所说的一系列操作又会重复发生。所以,共享模式允许多个线程共同持有同步状态。

更多

AQS还有一些地方我们没有挖掘到,但是对于想了解一下其内部工作原理的我来说,目前已经够了,更多的东西,比如条件队列,你可以自行去研究~

posted @ 2023-01-31 11:17  yudoge  阅读(91)  评论(0编辑  收藏  举报