JUC核心--AQS及源码分析

289 阅读32分钟

简单介绍

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情

什么是AQS?

什么是AQS?

java.util.concurrentJ.U.C)大大提高了并发性能,AQS 被认为是 J.U.C 的核心。

AQS全称为AbstractQueuedSynchronizer, 存在于java.util.concurrent.locks包下面。

顾名思义,AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器。

比如我们提到的ReentrantLockSemaphore,其他的诸如ReentrantReadWriteLockSynchronousQueueFutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。

AQS原理

AQS核心思想

AQS控制线程同步的核心思想

AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态如果被请求的共享资源被占用,那么就需要一套「线程阻塞等待以及被唤醒时锁分配的机制」

这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

  • 什么是CLH队列?

    CLH(Craig,Landin and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。

    AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。

结合我们上面的核心思想概述,以及CLH的结构画出如下原理图:

image.png

AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列(CLH)来完成获取资源线程的排队工作。AQS 使用 CAS对该同步状态进行原子操作实现对其值的修改。

private volatile int state; // 共享变量,使用volatile关键字修饰保证线程可见性

AQS对资源的共享方式

AQS定义两种资源共享方式

  • Exclusive(独占):只有一个线程能执行,如 ReentrantLock。又可分为公平锁和非公平锁:

    • 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
    • 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
  • Share(共享):多个线程可同时执行,如CountDownLatchSemaphoreCyclicBarrierReadWriteLock 我们都会在后面讲到。

关于独占/共享锁,公平/非公平锁的内容我在博客ReentrantLock及源码分析中有提到。

自定义同步器时需要注意什么?需要实现什么?

不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS 已经在顶层实现好了。

  • 独占锁的实现

    ReentrantLock 为例,state 初始化为 0,表示未锁定状态。 A 线程 lock() 时,会调用 tryAcquire() 独占该锁并将 state+1 。此后,其他线程再 tryAcquire() 时就会失败,直到 A 线程 unlock()state=0(即释放锁)为止,其它线程才有机会获取该锁。

    当然,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多少次,这样才能保证 state 是能回到零态的。

  • 共享锁的实现

    再以 CountDownLatch 以例,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。

    这 N 个子线程是并行执行的,每个子线程执行完后countDown() 一次,state 会 CAS(Compare and Swap) 减 1。等到所有子线程都执行完后(即 state=0 ),会 unpark() 主调用线程,然后主调用线程就会从 await() 函数返回,继续后余动作。

AQS底层设计--模板方法模式

同步器的设计和模板方法模式的关系

同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):

  • 使用者继承 AbstractQueuedSynchronizer 并重写指定的方法。(这些重写方法很简单,无非是对于共享资源 state 的获取和释放)
  • AQS 组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
  • 这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。具体是什么区别呢?

    AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的钩子方法:

    protected boolean tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
    protected boolean tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
    protected int tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
    protected boolean tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
    protected boolean isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
    
  • 什么是钩子方法?

    钩子方法是一种被声明在抽象类中的方法,它可以是空方法(由子类实现),也可以是默认实现的方法。

    模板设计模式通过钩子方法控制固定步骤的实现。

    除了上面提到的钩子方法之外,AQS 类中的其他方法都是 final ,所以无法被其他类重写。

一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryReleasetryAcquireShared-tryReleaseShared中的一种即可。

AQS 也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock

AQS源码分析

内部类Node

内部类Node

static final class Node {
    // 模式,分为共享与独占
    // 共享模式
    static final Node SHARED = new Node();
    // 独占模式
    static final Node EXCLUSIVE = null;        
    // 结点状态
    // CANCELLED,值为1,表示当前的线程被取消
    // SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行,也就是unpark
    // CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列中
    // PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行
    // 值为0,表示当前节点在sync队列中,等待着获取锁
    static final int CANCELLED =  1;
    static final int SIGNAL    = -1;
    static final int CONDITION = -2;
    static final int PROPAGATE = -3;        
​
    // 结点状态
    volatile int waitStatus;        
    // 前驱结点
    volatile Node prev;    
    // 后继结点
    volatile Node next;        
    // 结点所对应的线程
    volatile Thread thread;        
    // 下一个等待者
    Node nextWaiter;
    
    // 结点是否在共享模式下等待
    final boolean isShared() {
        return nextWaiter == SHARED;
    }
    
    // 获取前驱结点,若前驱结点为空,抛出异常
    final Node predecessor() throws NullPointerException {
        // 保存前驱结点
        Node p = prev; 
        if (p == null) // 前驱结点为空,抛出异常
            throw new NullPointerException();
        else // 前驱结点不为空,返回
            return p;
    }
    
    // 无参构造方法
    Node() {    // Used to establish initial head or SHARED marker
    }
    
    // 构造方法
        Node(Thread thread, Node mode) {    // Used by addWaiter
        this.nextWaiter = mode;
        this.thread = thread;
    }
    
    // 构造方法
    Node(Thread thread, int waitStatus) { // Used by Condition
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

每个线程被阻塞的线程都会被封装成一个Node结点,放入队列。 每个节点包含了一个Thread类型的引用,并且每个节点都存在一个状态,具体状态如下。

  • CANCELLED,值为1,表示当前的线程被取消。

    作废状态,该节点的线程由于超时,中断等原因而处于作废状态。是不可逆的,一旦处于这个状态,说明应该将该节点移除了。

  • SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行,需要进行unpark操作。

    待唤醒后继状态,当前节点的线程处于此状态,后继节点会被挂起,当前节点释放锁或取消之后必须唤醒它的后继节点。

  • CONDITION,值为-2,表示当前节点在等待condition,也就是在condition queue中。

    等待状态,表明节点对应的线程因为不满足一个条件(Condition)而被阻塞。

  • PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行。

  • 当值为0,表示当前节点在sync queue中,等待着获取锁。

内部类ConditionObject

内部类ConditionObject

以下代码只展示主要方法的方法体:

// 内部类
public class ConditionObject implements Condition, java.io.Serializable {
    // 版本号
    private static final long serialVersionUID = 1173984872572414699L;
    // condition队列的头节点
    private transient Node firstWaiter;
    // condition队列的尾结点
    private transient Node lastWaiter;
​
    // 构造方法
    public ConditionObject() { }
​
    // private methods
    
    // 添加新的waiter到wait队列
    private Node addConditionWaiter() {
        // 保存尾结点
        Node t = lastWaiter;
        if (t != null && t.waitStatus != Node.CONDITION) { // 尾结点不为空,并且尾结点的状态不为CONDITION
            // 清除状态为CONDITION的结点
            unlinkCancelledWaiters(); 
            // 将最后一个结点重新赋值给t
            t = lastWaiter;
        }
        // 新建一个结点
        Node node = new Node(Thread.currentThread(), Node.CONDITION);
        if (t == null) // 尾结点为空
            // 设置condition队列的头节点
            firstWaiter = node;
        else // 尾结点不为空
            // 设置为节点的nextWaiter域为node结点
            t.nextWaiter = node;
        // 更新condition队列的尾结点
        lastWaiter = node;
        return node;
    }
​
    private void doSignal(Node first) {
        // 循环
        do {
            if ( (firstWaiter = first.nextWaiter) == null) // 该节点的nextWaiter为空
                // 设置尾结点为空
                lastWaiter = null;
            // 设置first结点的nextWaiter域
            first.nextWaiter = null;
        } while (!transferForSignal(first) &&
                    (first = firstWaiter) != null); // 将结点从condition队列转移到sync队列失败并且condition队列中的头节点不为空,一直循环
    }
​
    private void doSignalAll(Node first) {
        // condition队列的头节点尾结点都设置为空
        lastWaiter = firstWaiter = null;
        // 循环
        do {
            // 获取first结点的nextWaiter域结点
            Node next = first.nextWaiter;
            // 设置first结点的nextWaiter域为空
            first.nextWaiter = null;
            // 将first结点从condition队列转移到sync队列
            transferForSignal(first);
            // 重新设置first
            first = next;
        } while (first != null);
    }
​
    // 从condition队列中清除状态为CANCEL的结点
    private void unlinkCancelledWaiters() {
        // 保存condition队列头节点
        Node t = firstWaiter;
        Node trail = null;
        while (t != null) { // t不为空
            // 下一个结点
            Node next = t.nextWaiter;
            if (t.waitStatus != Node.CONDITION) { // t结点的状态不为CONDTION状态
                // 设置t节点的nextWaiter域为空
                t.nextWaiter = null;
                if (trail == null) // trail为空
                    // 重新设置condition队列的头节点
                    firstWaiter = next;
                else // trail不为空
                    // 设置trail结点的nextWaiter域为next结点
                    trail.nextWaiter = next;
                if (next == null) // next结点为空
                    // 设置condition队列的尾结点
                    lastWaiter = trail;
            }
            else // t结点的状态为CONDTION状态
                // 设置trail结点
                trail = t;
            // 设置t结点
            t = next;
        }
    }
​
    // public methods
​
    // 唤醒一个等待线程。如果所有的线程都在等待此条件,则选择其中的一个唤醒。在从 await 返回之前,该线程必须重新获取锁。
    public final void signal() {
        if (!isHeldExclusively()) // 不被当前线程独占,抛出异常
            throw new IllegalMonitorStateException();
        // 保存condition队列头节点
        Node first = firstWaiter;
        if (first != null) // 头节点不为空
            // 唤醒一个等待线程
            doSignal(first);
    }
​
    // 唤醒所有等待线程。如果所有的线程都在等待此条件,则唤醒所有线程。在从 await 返回之前,每个线程都必须重新获取锁。
    public final void signalAll() {
        if (!isHeldExclusively()) // 不被当前线程独占,抛出异常
            throw new IllegalMonitorStateException();
        // 保存condition队列头节点
        Node first = firstWaiter;
        if (first != null) // 头节点不为空
            // 唤醒所有等待线程
            doSignalAll(first);
    }
​
    // 等待,当前线程在接到信号之前一直处于等待状态,不响应中断
    public final void awaitUninterruptibly() {
        // 添加一个结点到等待队列
        Node node = addConditionWaiter();
        // 获取释放的状态
        int savedState = fullyRelease(node);
        boolean interrupted = false;
        while (!isOnSyncQueue(node)) { // 
            // 阻塞当前线程
            LockSupport.park(this);
            if (Thread.interrupted()) // 当前线程被中断
                // 设置interrupted状态
                interrupted = true; 
        }
        if (acquireQueued(node, savedState) || interrupted) // 
            selfInterrupt();
    }
​
    /** Mode meaning to reinterrupt on exit from wait */
    private static final int REINTERRUPT =  1;
    /** Mode meaning to throw InterruptedException on exit from wait */
    private static final int THROW_IE    = -1;
​
    private int checkInterruptWhileWaiting(Node node) {
        return Thread.interrupted() ?
            (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
            0; 
    }
    
    
    private void reportInterruptAfterWait(int interruptMode)
        // ...
    }
​
    // 等待,当前线程在接到信号或被中断之前一直处于等待状态
    public final void await() throws InterruptedException {
        if (Thread.interrupted()) // 当前线程被中断,抛出异常
            throw new InterruptedException();
        // 在wait队列上添加一个结点
        Node node = addConditionWaiter();
        // 
        int savedState = fullyRelease(node);
        int interruptMode = 0;
        while (!isOnSyncQueue(node)) {
            // 阻塞当前线程
            LockSupport.park(this);
            if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) // 检查结点等待时的中断类型
                break;
        }
        if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
            interruptMode = REINTERRUPT;
        if (node.nextWaiter != null) // clean up if cancelled
            unlinkCancelledWaiters();
        if (interruptMode != 0)
            reportInterruptAfterWait(interruptMode);
    }
​
    // 等待,当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态 
    public final long awaitNanos(long nanosTimeout)
            throws InterruptedException {
        // ...
    }
​
    // 等待,当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态
    public final boolean awaitUntil(Date deadline)
            throws InterruptedException {
        // ...
    }
​
    // 等待,当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。此方法在行为上等效于: awaitNanos(unit.toNanos(time)) > 0
    public final boolean await(long time, TimeUnit unit)
            throws InterruptedException {
        // ...
    }
​
    //  support for instrumentation
​
    final boolean isOwnedBy(AbstractQueuedSynchronizer sync) {
        return sync == AbstractQueuedSynchronizer.this;
    }
​
    //  查询是否有正在等待此条件的任何线程
    protected final boolean hasWaiters() {
        // ...
    }
​
    // 返回正在等待此条件的线程数估计值
    protected final int getWaitQueueLength() {
        // ...
    }
​
    // 返回包含那些可能正在等待此条件的线程集合
    protected final Collection<Thread> getWaitingThreads() {
        // ...
    }
}

此类实现了Condition接口,Condition接口定义了条件操作规范,具体如下:

public interface Condition {
​
    // 等待,当前线程在接到信号或被中断之前一直处于等待状态
    void await() throws InterruptedException;
    
    // 等待,当前线程在接到信号之前一直处于等待状态,不响应中断
    void awaitUninterruptibly();
    
    //等待,当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态 
    long awaitNanos(long nanosTimeout) throws InterruptedException;
    
    // 等待,当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。此方法在行为上等效于: awaitNanos(unit.toNanos(time)) > 0
    boolean await(long time, TimeUnit unit) throws InterruptedException;
    
    // 等待,当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态
    boolean awaitUntil(Date deadline) throws InterruptedException;
    
    // 唤醒一个等待线程。如果所有的线程都在等待此条件,则选择其中的一个唤醒。在从 await 返回之前,该线程必须重新获取锁。
    void signal();
    
    // 唤醒所有等待线程。如果所有的线程都在等待此条件,则唤醒所有线程。在从 await 返回之前,每个线程都必须重新获取锁。
    void signalAll();
}

看完了源码接下来我们解决几个问题

ConditionObject是什么?有什么用?

ConditionObject实现了Condition接口。

Condition用于线程间的通信,能够把锁粒度减小。

我们在ReentrantLock及源码分析中有说明Condition的用法,主要就是代替wait/noitfy的线程同步方式。

这个内部类还维护了一个condition队列,而且Node.nextWaiter就是用来将condition连接起来的。

//condition队头
private transient Node firstWaiter;
//condition队尾
private transient Node lastWaiter;
//发生了中断,但在后续不抛出中断异常,而是“补上”这次中断
private static final int REINTERRUPT =  1;
//发生了中断,且在后续需要抛出中断异常
private static final int THROW_IE    = -1;

ConditionObject类中,重点是await()signal()两个方法。这两个方法我们都将一一解释。

await方法

await方法的流程

当前线程处于阻塞状态,直到调用signal()或中断才能被唤醒。代码如下:

public final void await() throws InterruptedException {
    //如果被中断,就处理中断异常
    if (Thread.interrupted())
        throw new InterruptedException();
    //初始化链表的功能,设置当前线程为链尾
    Node node = addConditionWaiter();
    //释放当前节点持有的所有资源
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    //如果当前线程不在等待队列中,
    //说明此时一定在条件队列里,将其阻塞。
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        //说明中断状态发生变化
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    //当前线程执行了signal方法会经过这个,也就是重新将当前线程加入同步队列中
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    //删除条件队列中不满足要求的元素
    if (node.nextWaiter != null) 
        unlinkCancelledWaiters();
    //处理被中断的情况
    if (interruptMode != 0)
        //如果是THROW_IE,说明signal之前发生中断
        //如果是REINTERRUPT,signal之后中断,
        //所以成功获取资源后会调用selfInterrupt()
        reportInterruptAfterWait(interruptMode);
}
  • (1) 将当前线程封装成node且等待状态为CONDITION
  • (2) 释放当前线程持有的所有资源,让下一个线程能获取资源。
  • (3) 加入到条件队列后,则阻塞当前线程,等待被唤醒。
  • (4) 如果是因signal被唤醒,则节点会从条件队列转移到等待队列;如果是因中断被唤醒,则记录中断状态。两种情况都会跳出循环。
  • (5) 若是因signal被唤醒,就自旋获取资源;否则处理中断异常。

接着我们看addConditionWaiter方法

  • addConditionWaiter方法

    private Node addConditionWaiter() {
        Node t = lastWaiter;
        // 判断队尾元素,如果非条件等待状态则清理出去
        if (t != null && t.waitStatus != Node.CONDITION) {
            unlinkCancelledWaiters();
            //可能t之前引用的节点被删除了,所以要重新引用
            t = lastWaiter;
        }
        //这个节点就表示当前线程
        Node node = new Node(Thread.currentThread(), Node.CONDITION);
        //说明条件按队列中没有元素
        if (t == null)
            firstWaiter = node;
        else
            t.nextWaiter = node;
        lastWaiter = node;
        return node;
    }
    

    将当前线程封装成节点,添加到条件队列尾部,并返回当前节点。

接着我们看unlinkCancelledWaiters方法

  • unlinkCancelledWaiters方法

    private void unlinkCancelledWaiters() {
        Node t = firstWaiter;
        //记录在循环中上一个waitStatus有效的节点
        Node trail = null;
        while (t != null) {
            Node next = t.nextWaiter;
            //再次判断等待状态,保证节点都是CONDITION状态
            //确保当前节点无效后删除引用
            if (t.waitStatus != Node.CONDITION) {
                t.nextWaiter = null;
                if (trail == null)
                    firstWaiter = next;
                else
                    //否则就直接加到队尾的后面
                    trail.nextWaiter = next;
                if (next == null)
                    lastWaiter = trail;
            }
            else
                //记录有效的节点并向后遍历
                trail = t;
            t = next;
        }
    }
    

    遍历一遍条件队列,删除非CONDITION状态的节点。

signal方法

signal方法流程

唤醒一个被阻塞的线程

public final void signal() {
    //判断当前线程是否为资源的持有者
    //这也是必须在lock()与unlock()代码中间执行的原因
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    //开始唤醒条件队列的第一个节点
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}
  • (1) 判断当前线程是否为资源的持有者,如果不是则抛出异常
  • (2) 唤醒条件队列的第一个节点

接着我们看doSignal方法

  • doSignal方法

    private void doSignal(Node first) {
        do {
            //后续的等待条件为空,说明condition队列中只有一个节点
            if ( (firstWaiter = first.nextWaiter) == null)
                lastWaiter = null;
            first.nextWaiter = null;
            //transferForSignal()是真正唤醒头节点的地方
        } while (!transferForSignal(first) &&
                 (first = firstWaiter) != null);
    }
    

    将条件队列的头节点从条件队列转移到等待队列,并且,将该节点从条件队列删除。

    接着我们看transferForSignal方法

    • transferForSignal方法

      final boolean transferForSignal(Node node) {
          //当前节点等待状态改变失败,则说明当前节点不是CONDITION
          //状态,那就不能进行接下来的操作,返回false
          //0是正常状态
          if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
              return false;
      
          //放入等待队列队尾中,并返回之前队列的前一个节点
          Node p = enq(node);
          int ws = p.waitStatus;
          //如果节点没有被取消,或更改状态失败,则唤醒被阻塞的线程
          if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
              LockSupport.unpark(node.thread);
          return true;
      }
      

      将节点放入等待队列并唤醒。并不需要在条件队列中移除,因为条件队列每次插入时都会把状态不为CONDITION的节点清理出去。

类的属性

类的属性

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
    implements java.io.Serializable {    
    // 版本号
    private static final long serialVersionUID = 7373984972572414691L;    
    // 头节点
    private transient volatile Node head;    
    // 尾结点
    private transient volatile Node tail;    
    // 状态
    private volatile int state;    
    // 自旋时间
    static final long spinForTimeoutThreshold = 1000L;
    
    // Unsafe类实例
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    // state内存偏移地址
    private static final long stateOffset;
    // head内存偏移地址
    private static final long headOffset;
    // state内存偏移地址
    private static final long tailOffset;
    // tail内存偏移地址
    private static final long waitStatusOffset;
    // next内存偏移地址
    private static final long nextOffset;
    // 静态初始化块
    static {
        try {
            stateOffset = unsafe.objectFieldOffset
                (AbstractQueuedSynchronizer.class.getDeclaredField("state"));
            headOffset = unsafe.objectFieldOffset
                (AbstractQueuedSynchronizer.class.getDeclaredField("head"));
            tailOffset = unsafe.objectFieldOffset
                (AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
            waitStatusOffset = unsafe.objectFieldOffset
                (Node.class.getDeclaredField("waitStatus"));
            nextOffset = unsafe.objectFieldOffset
                (Node.class.getDeclaredField("next"));

        } catch (Exception ex) { throw new Error(ex); }
    }
}

属性中包含了:

  • 头节点head,尾结点tail

    这个就是我们在「AQS核心思想」中提及的CLH队列(同步队列)的头尾节点

    CLH队列由链表实现,以自旋的方式获取资源,是可阻塞的先进先出的双向队列。(这点可以重新回顾「内部类Node」)

    通过自旋和CAS操作保证节点插入和移除的原子性。当有线程获取锁失败,就被添加到队列末尾

  • 状态state(锁状态表示)

    state是临界资源,也是锁的描述。表示有多少线程获取了锁。

  • 自旋时间spinForTimeoutThreshold

  • 还有AbstractQueuedSynchronizer抽象的属性在内存中的偏移地址

    通过该偏移地址,可以获取和设置该属性的值,同时还包括一个静态初始化块,用于加载内存偏移地址。

acquire方法

acquire--独占模式获取资源

该方法以独占模式获取(资源),忽略中断,即线程在aquire过程中,中断此线程是无效的。源码如下:

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        //让线程处于一种自旋状态,
        //尝试让该线程重新获取锁!当条件满足获取到了锁则可以从自旋过程中
        //退出,否则继续。
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

由上述源码可以知道,当一个线程调用acquire时,调用方法流程如下:

image.png

我们对acqure的过程先进行简单的介绍,后面再依次对其中的各个方法源码进行分析:

  • (1) 首先调用tryAcquire方法,调用此方法的线程会试图在独占模式下获取对象状态。

    此方法应该查询是否允许它在独占模式下获取对象状态,如果允许,则获取它。在AbstractQueuedSynchronizer源码中默认会抛出一个异常,即需要子类去重写此方法完成自己的逻辑。之后会进行分析。

  • (2) 若tryAcquire失败,则调用addWaiter方法,addWaiter方法完成的功能是将调用此方法的线程封装成为一个结点并放入Sync queue队尾,并且标记为独占模式。

  • (3) 插入等待队列后(addWaiter),并没有放弃获取资源。调用acquireQueued方法,此方法完成的功能是Sync queue中的结点不断尝试获取资源,根据前置节点的状态判断是否应该继续获取资源(如果前驱是头节点,继续尝试获取资源),若成功,则返回true,否则,返回false。

  • (4) 在每一次自旋获取资源过程中(acquireQueued),失败后调用shouldParkAfterFailedAcquire(Node, Node)检测当前节点是否应该park()。若返回true,则调用parkAndCheckInterrupt()中断当前节点中的线程。若返回false,则接着自旋获取资源。

  • (5) 在进行是否需要挂起的判断中(shouldParkAfterFailedAcquire),如果前置节点是SIGNAL状态,就挂起,返回true。如果前置节点状态为CANCELLED,就一直往前找,直到找到最近的一个处于正常等待状态的节点,并排在它后面,返回false,acquireQueed()接着自旋尝试,回到(3)。

  • (6) 前置节点处于其他状态,利用CAS将前置节点状态置为SIGNAL。当前置节点刚释放资源,状态就不是SIGNAL了,导致失败,返回false。但凡返回false,就导致acquireQueed()接着自旋尝试。

  • (7) 最终当tryAcquire(int)返回false,acquireQueued(Node,int)返回true,调用selfInterrupt(),中断当前线程

我们首先分析tryAcquire方法,接着分析addWaiter方法,acquireQueued方法,最后分析selfInterrupt方法。(分析过程中的主要被方法调用也会介绍)

tryAcquire方法

在「AQS底层设计--模板方法模式」中我们提到AQS使用了模板方法模式。

tryAcuire方法就是一个钩子方法。

AQS中,此方法会抛出UnsupportedOperationException,所以需要子类去实现。(具体的子类实现我在博客ReentrantLock及源码分析有分析)

tryAcquire(arg)返回false,其实就是获取锁失败的情况。这时候就需要做自旋,重新获取。

addWaiter方法

// 添加等待者
private Node addWaiter(Node mode) {
    // 新生成一个结点,默认为独占模式
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    // 保存尾结点
    Node pred = tail;
    if (pred != null) { // 尾结点不为空,即已经被初始化
        // 将node结点的prev域连接到尾结点
        node.prev = pred; 
        if (compareAndSetTail(pred, node)) { // 比较pred是否为尾结点,是则将尾结点设置为node 
            // 设置尾结点的next域为node
            pred.next = node;
            return node; // 返回新生成的结点
        }
    }
    enq(node); // 尾结点为空(即还没有被初始化过),或者是compareAndSetTail操作失败,则入队列
    return node;
}

addWaiter方法使用快速添加的方式往sync queue尾部添加结点(将当前线程插入至队尾,返回在等待队列中的节点)

如果sync queue队列还没有初始化,则会使用enq插入队列中

接下来我们来分析enq方法。

  • enq方法

    private Node enq(final Node node) {
        for (;;) { // 无限循环,确保结点能够成功入队列
            // 保存尾结点
            Node t = tail;
            if (t == null) { // 尾结点为空,即还没被初始化
                if (compareAndSetHead(new Node())) // 头节点为空,并设置头节点为新生成的结点
                    tail = head; // 头节点与尾结点都指向同一个新生结点
            } else { // 尾结点不为空,即已经被初始化过
                // 将node结点的prev域连接到尾结点
                node.prev = t; 
                if (compareAndSetTail(t, node)) { // 比较结点t是否为尾结点,若是则将尾结点设置为node
                    // 设置尾结点的next域为node
                    t.next = node; 
                    return t; // 返回尾结点
                }
            }
        }
    }
    

    enq方法会使用无限循环来确保节点的成功插入。(将节点插入队尾,失败则自旋直到成功)

接着我们分析acquireQueue方法。

acquireQueue方法

// sync队列中的结点在独占且忽略中断的模式下获取(资源)
final boolean acquireQueued(final Node node, int arg) {
    // 标志
    boolean failed = true;
    try {
        // 中断标志
        boolean interrupted = false;
        for (;;) { // 无限循环
            // 获取node节点的前驱结点
            final Node p = node.predecessor(); 
            if (p == head && tryAcquire(arg)) { // 前驱为头节点并且成功获得锁
                setHead(node); // 设置头节点
                p.next = null; // help GC
                failed = false; // 设置标志
                return interrupted; 
            }
            // 首先判断当前节点是否应该被park(挂起)
            if (shouldParkAfterFailedAcquire(p, node) &&
                // 确定有必要park才会执行该方法
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

首先获取当前节点的前驱节点,如果前驱节点是头节点并且能够获取(资源),代表该当前节点能够占有锁,设置头节点为当前节点,返回。否则,调用shouldParkAfterFailedAcquireparkAndCheckInterrupt方法。

(自旋方式获取资源并判断是否需要被挂起)

接下来,我们先看shouldParkAfterFailedAcquire方法

  • shouldParkAfterFailedAcquire方法

    // 当获取(资源)失败后,检查并且更新结点状态
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        // 获取前驱结点的状态
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL) // 状态为SIGNAL,为-1,说明前驱节点释放资源后会通知自己
            // 可以进行安全的park操作,所以返回true
            return true;
        if (ws > 0) { // 表示状态为CANCELLED,为1,说明前驱节点已经放弃获取资源了
            // 一直往前寻找
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0); // 找到pred结点前面最近的一个状态不为CANCELLED的结点
            // 赋值pred结点的next域
            pred.next = node; 
        } else { // 为PROPAGATE -3 或者是0 表示无状态,(为CONDITION -2时,表示此节点在condition queue中) 
            // 比较并设置前驱结点的状态为SIGNAL
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL); 
        }
        // 不能进行park操作
        return false;
    }
    

    只有当该节点的前驱结点的状态为SIGNAL时,才可以对该结点所封装的线程进行park操作。否则,将不能进行park操作。

    再看parkAndCheckInterrupt方法。

    (判断当前节点是否应该被挂起,涉及到「内部类Node」中的等待状态)

  • parkAndCheckInterrupt方法

    // 进行park操作并且返回该线程是否被中断
    private final boolean parkAndCheckInterrupt() {
        // 在许可可用之前禁用当前线程,并且设置了blocker
        LockSupport.park(this);
        return Thread.interrupted(); // 当前线程是否已被中断,并清除中断标记位
    }
    

    parkAndCheckInterrupt方法里的逻辑是首先执行park操作,即禁用当前线程,然后返回该线程是否已经被中断。

    (确定有必要park,才会执行该方法)

    再看final块中的cancelAcquire方法。

  • cancelAcquire方法

    // 取消继续获取(资源)
    private void cancelAcquire(Node node) {
        // node为空,返回
        if (node == null)
            return;
        // 设置node结点的thread为空
        node.thread = null;
    
        // 保存node的前驱结点
        Node pred = node.prev;
        while (pred.waitStatus > 0) // 找到node前驱结点中第一个状态小于0的结点,即不为CANCELLED状态的结点
            node.prev = pred = pred.prev;
    
        // 获取pred结点的下一个结点
        Node predNext = pred.next;
    
        // 设置node结点的状态为CANCELLED
        node.waitStatus = Node.CANCELLED;
    
        if (node == tail && compareAndSetTail(node, pred)) { // node结点为尾结点,则设置尾结点为pred结点
            // 比较并设置pred结点的next节点为null
            compareAndSetNext(pred, predNext, null); 
        } else { // node结点不为尾结点,或者比较设置不成功
            // If successor needs signal, try to set pred's next-link
            // so it will get one. Otherwise wake it up to propagate.
            int ws;
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                    (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) { // (pred结点不为头节点,并且pred结点的状态为SIGNAL)或者 
                                    // pred结点状态小于等于0,并且比较并设置等待状态为SIGNAL成功,并且pred结点所封装的线程不为空
                // 保存结点的后继
                Node next = node.next;
                if (next != null && next.waitStatus <= 0) // 后继不为空并且后继的状态小于等于0
                    compareAndSetNext(pred, predNext, next); // 比较并设置pred.next = next;
            } else {
                unparkSuccessor(node); // 释放node的前一个结点
            }
    
            node.next = node; // help GC
        }
    }
    

    该方法完成的功能就是取消当前线程对资源的获取,即设置该结点的状态为CANCELLED

    接着我们再看unparkSuccessor方法。

    • unparkSuccessor方法

      // 释放后继结点
      private void unparkSuccessor(Node node) {
      
          // 获取node结点的等待状态
          int ws = node.waitStatus;
          if (ws < 0) // 状态值小于0,为SIGNAL -1 或 CONDITION -2 或 PROPAGATE -3
              // 比较并且设置结点等待状态,设置为0
              compareAndSetWaitStatus(node, ws, 0);
      
          // 获取node节点的下一个结点
          Node s = node.next;
          if (s == null || s.waitStatus > 0) { // 下一个结点为空或者下一个节点的等待状态大于0,即为CANCELLED
              // s赋值为空
              s = null; 
              // 从尾结点开始从后往前开始遍历
              for (Node t = tail; t != null && t != node; t = t.prev)
                  if (t.waitStatus <= 0) // 找到等待状态小于等于0的结点,找到最前的状态小于等于0的结点
                      // 保存结点
                      s = t;
          }
          if (s != null) // 该结点不为为空,释放许可
              LockSupport.unpark(s.thread);
      }
      

      该方法的作用就是为了释放node节点的后继结点。

      对于cancelAcquireunparkSuccessor方法,如下示意图可以清晰的表示:

      image.png

      其中node为参数,在执行完cancelAcquire方法后的效果就是unpark了s结点所包含的t4线程。

      现在,再来看acquireQueued方法的整个的逻辑。逻辑如下:

      • 判断结点的前驱是否为head并且是否成功获取(资源)。
      • 若步骤1均满足,则设置结点为head,之后会判断是否finally模块,然后返回。
      • 若步骤2不满足,则判断是否需要park当前线程,是否需要park当前线程的逻辑是判断结点的前驱结点的状态是否为SIGNAL,若是,则park当前结点,否则,不进行park操作。
      • park了当前线程,之后某个线程对本线程unpark后,并且本线程也获得机会运行。那么,将会继续进行步骤①的判断。

最后我们分析selfInterrupt方法

selfInterrupt方法

static void selfInterrupt() {
    Thread.currentThread().interrupt();
}

对当前线程产生一个中断请求。

能走到这个方法,说明acquireQueued()返回true,就进行自我中断。

acquiredShared方法

acquiredShared方法--共享模式获取资源

public final void acquireShared(int arg) {
    //模板方法模式,tryAcquireShared由子类实现
    //想看的话推荐读写锁的源码,这里就不细述了
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

以共享模式获取对象,忽略中断。先是tryAcquireShared(int)尝试直接去获取资源,如果成功,acquireShared(int)就结束了;否则,调用doAcquireShared(Node)将线程加入等待队列,直到获取到资源为止。

我们接着分析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);
    }
}

实现上和acquire()方法差不多,就是多判断了是否还有剩余资源,唤醒后继节点。

接着我们补充一个方法:hasQueuedPredecessors

hasQueuedPredecessors方法

ReentrantLock如果是公平锁的话,会在自己的tryAcquire的实现中调用AQS中的这个方法

public final boolean hasQueuedPredecessors() {
    //判断当前节点在等待队列中是否为头节点的后继节点(头节点不存储数据),
    //如果不是,则说明有线程比当前线程更早的请求资源,
    //根据公平性,当前线程请求资源失败。
    //如果当前节点没有前驱节点的话,才有做后面的逻辑判断的必要性
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

判断当前线程是否位于CLH同步队列中的第一个。如果是则返回flase,否则返回true。

release方法

release--独占方式释放资源

以独占模式释放对象,其源码如下:

public final boolean release(int arg) {
    if (tryRelease(arg)) { // 释放成功
        // 保存头节点
        Node h = head; 
        if (h != null && h.waitStatus != 0) // 头节点不为空并且头节点状态不为0
            unparkSuccessor(h); //释放头节点的后继结点
        return true;
    }
    return false;
}

其中,tryRelease的默认实现是抛出异常,需要具体的子类实现,如果tryRelease成功,那么如果头节点不为空并且头节点的状态不为0,则释放头节点的后继结点。

我们接着分析unparkSuccessor方法

  • unparkSuccessor方法

    private void unparkSuccessor(Node node) {
        //如果状态为负说明是除CANCEL以外的状态,
        //尝试在等待信号时清除。
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
    
        Node s = node.next;
        //下一个节点为空或CANCELLED状态
        //则从队尾往前找,找到正常状态的节点作为之后的继承人
        //也就是下一个能拿到资源的节点
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        //此时找到继承人了,那么就唤醒它
        if (s != null)
            LockSupport.unpark(s.thread);
    }
    

    尝试找到下一位继承人,就是确定下一个获取资源的线程,唤醒指定节点的后继节点。

releaseShared方法

releaseShared()--共享模式释放资源

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

在释放一部分资源后就可以通知其他线程获取资源。

接着我们分析doReleaseShared方法

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))
                    //将自旋尝试修改等待状态为0
                    continue;
                //唤醒下一个被阻塞的节点
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                //设置为PROPAGATE状态
                continue;               
        }
        //上面代码块执行过程中如果head变更了,就接着循环尝试
        if (h == head)
            break;
    }
}

把当前结点的状态由SIGNAL设置为PROPAGATE状态。

AQS组件

AQS有哪些组件?都是拿来干什么的呢?

  • Semaphore(信号量)-允许多个线程同时访问:

    synchronizedReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。

    Semaphore在限制资源访问量的问题上用处很大,比如限制一个文件的并发访问次数等。

  • CountDownLatch(倒计时器):

    CountDownLatch 是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。

  • CyclicBarrier(循环栅栏):

    CyclicBarrierCountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。

    CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用 await() 方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。

小结

本篇我们主要分析了AQS源码,详细的拆解成内部类,类的属性,还有几个主要方法。

AQS是我们JUC的核心,我们在最后也介绍了JUC中的几个常用同步器和它们的用法,当然详细内容及源码解释得另开章节分析了。

本篇参考: