AQS AbstractQueuedSynchronizer 原理分析

119 阅读12分钟

概述

AbstractQueuedSynchronizer,队列同步器,简称AQS,它是java并发用来构建锁或者其他同步组件的基础框架。 事实上从下图看出很多并发组建确实都使用了AQS实现同步需求,学习并发编程,AQS是基础中的基础,重点中的重点。 image.png

1 同步器的接口

同步器的设计基于模版方法模式,使用者继承同步器,重写指定的方法,并调用同步器提供的模版方法。

同步状态变量相关方法:

  1. protected final int getState(); 获取当前同步状态
  2. protected final void setState(int newState); 设置当前同步状态
  3. protected final boolean compareAndSetState(int expect, int update); CAS设置当前同步状态,保证原子更新。

独占式和共享式同步状态的区别如下

state[0,N]{ N=独占式重入次数 N=不确定,里边包含重入和共享线程数state \in [0,N] \begin{cases} & \text{ N} =独占式重入次数 \\ & \text{ N} =不确定,里边包含重入和共享线程数 \end{cases}

image.png

AQS提供的重写方法

可重写方法名称描述
protected boolean tryAcquire(int arg)独占式获取同步状态,该方法的实现需要先查询当前的同步状态是否可以获取,如果可以获取再进行获取;
protected boolean tryRelease(int arg)独占式释放同步状态
protected int tryAcquireShared(int arg)共享式获取同步状态
protected boolean tryReleaseShared(int arg)共享式释放状态
protected boolean isHeldExclusively()独占模式下,判断同步状态是否已经被占用

我们随便找一个可重写方法,发现其默认实现是抛出了一个异常。⾃定义的同步组件或者锁不可能既是独占式⼜是共享式,为了避免强制重写不相⼲⽅法,所以就没有用abstract来修饰了,但要抛出异常告知不能直接使⽤该⽅法:

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

我们继承AQS,重写以上方法时,需要调用AQS提供的模版方法。

模版方法名称描述
acquire(int arg)独占式获取同步状态。如果当前线程获取同步状态成功,则由该方法返回;否则,将会进入同步队列等待。该方法将会调用可重写的 tryAcquire(int arg) 方法
acquireInterruptibly(int arg)与acquire(int arg) 相同,但是该方法响应中断。当前线程为获取到同步状态而进入到同步队列中,如果当前线程被中断,则该方法会抛出InterruptedException 异常并返回
tryAcquireSharedNanos(int arg, long nanosTimeout)在acquireInterruptibly(int arg)基础上增加了超时限制
tryAcquireNanos(int arg, long nanos)超时获取同步状态。如果当前线程在 nanos 时间内没有获取到同步状态,那么将会返回 false ,已经获取则返回 true
acquireShared(int arg)共享式获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式的主要区别是在同一时刻可以有多个线程获取到同步状态
acquireSharedInterruptibly(int arg)共享式获取同步状态,与acquireShared相同不过响应中断
release(int arg)独占式释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒
releaseShared(int arg)共享式释放同步状态
Collection getQueuedThreads()获取等待在同步队列上的线程集合

上述方法都由final修饰,表示这些模版方法只能被调用不能被重写。 如果我们需要自定义互斥锁, 应该实现Lock接口,聚合自定义队列同步器,同时自定义队列同步器继承AQS。 image.png

自定义独占锁的代码实例如下

import java.io.Serializable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

public class MyMutex implements Lock, Serializable {
    // 静态内部类,继承 AQS 并重写其中方法
    private static class Sync extends AbstractQueuedSynchronizer {
        // 是否处于占用状态
        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }
        // Acquire the lock if state is zero 当状态为0时获取到锁
        public boolean tryAcquire(int acquires) {
            assert acquires == 1; // Otherwise unused
            if (compareAndSetState(0, 1)) { // 通过 CAS 设置
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }
        // Release the lock by setting state to zero 释放锁,将状态设置为0
        protected boolean tryRelease(int releases) {
            assert releases == 1; // Otherwise Unused
            if (getState() == 0) {
                throw new IllegalMonitorStateException();
            }
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
        // Providers a Condition
        Condition newCondition() { return new ConditionObject(); }
    }
    // 将操作代理到 Sync 上
    private final Sync sync = new Sync();

    public void lock() { sync.acquire(1); }
    public boolean tryLock() { return sync.tryAcquire(1); }
    public void unlock() { sync.release(1); }
    public Condition newCondition() { return sync.newCondition(); }
    public boolean isLocked() { return sync.isHeldExclusively();}
    public boolean hasQueuedThreads() { return sync.hasQueuedThreads();}
    public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); }
    public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(timeout)); }
}

2 同步器实现分析

上文我们知道lock.tryLock()其实就是调用了自定义同步器重写的tryAcquire()方法。

2.1 同步队列

AQS底层的数据结构是CLH变体的虚拟双向队列,这个队列遵循FIFO。AQS利用该同步队列管理同步状态。 image.png

  • 当线程获取同步状态失败时,就会将当前线程以及等待状态等信息构造成⼀个Node 节点,将其加⼊到同步队列中尾部,阻塞该线程
  • 当同步状态被释放时,会唤醒同步队列中“⾸节点”的线程获取同步状态

队列中节点几个方法和属性值的含义:

方法和属性值含义
waitStatus当前节点在队列中的状态
thread表示处于该节点的线程
prev前驱指针
predecessor返回前驱节点,没有的话抛出 npe
nextWaiter指向下一个处于 CONDITION 状态的节点(由于本篇文章不讲述 Condition Queue 队列,这个指针不多介绍)
next后继指针

线程两种锁的模式:

模式含义
SHARED表示线程以共享的模式等待锁
EXCLUSIVE表示线程正在以独占的方式等待锁

waitStatus 有下面几个枚举值:

枚举含义
0当一个 Node 被初始化的时候的默认值
CANCELLED为 1,表示线程获取锁的请求已经取消了
CONDITION为-2,表示节点在等待队列中,节点线程等待唤醒
PROPAGATE为-3,当前线程处在 SHARED 情况下,该字段才会使用
SIGNAL为-1,表示线程已经准备好了,就等资源释放了

队列的首节点是获取同步状态成功的节点,当一个线程获取到同步状态,其他节点被阻塞并以CAS的方式线程安全地加入队列。 image.png

首节点的线程在执行完相关的方法后释放同步状态时,将会唤醒后继节点,后继节点在成功获取到同步状态时将自己设置为首节点。 image.png

2.2 同步状态的获取和释放

acquire(int arg)是AQS独占式获取同步状态的模版方法,下文若无特殊说明,源码都是从java8版本拷贝来的。

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

⾸先,尝试⾮阻塞的获取同步状态,如果获取失败(tryAcquire返回false),则会调⽤ addWaiter ⽅法构造 Node 节点(Node.EXCLUSIVE 独占式)并安全的(CAS)加⼊到同步队列【尾部】,最后调用acquireQueued使得该节点以 死循环(自旋)的方式获取同步状态。

//这个函数比较简单,就是将node放到队列末尾,mode表示是独占锁还是共享锁以后再讨论
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) {//如果tail不是null,表示队列已被初始化,尝试快速在尾部添加当前节点
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
            //cas将tail加到队尾,如果失败走到enq函数继续cas+自旋到队尾
                pred.next = node;
                return node;
            }
        }

        enq(node);  //初始化队列或者再次cas队尾
        return node;
 }
 

//该函数会初始化队列(如果队列未被初始化)
private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize  初始化队列
                if (compareAndSetHead(new Node())) 
                    // 如果队列空,初始化队列,首和尾都指向新建的节点
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                //cas队尾,如果还失败看到这个是死循环会一直去放,直到放到队尾为止
                    t.next = node;
                    return t;
                }
            }
        }
    }

获取同步状态失败的节点被正确添加到队尾后,会进行死循环不断尝试获取同步状态,这种死循环不断乐观尝试的过程我们称之为自旋image.png

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;//标记是否成功拿到资源
    try {
        boolean interrupted = false;//标记等待过程中是否被中断过
        //又是一个“自旋”!
        for (;;) {
            final Node p = node.predecessor();//拿到前驱
            //如果前驱是head,节点为老二,尝试获取同步状态。
            if (p == head && tryAcquire(arg)) {
                setHead(node);   //获取同步状态后,将head指向该结点。
                p.next = null;   // 上一个获取同步状态节点(哨兵节点)指向空便于gc
                failed = false;  // 成功获取资源
                return interrupted;   //返回等待过程中是否被中断过
            }
            //如果自己可以休息了则通过park()进入waiting状态,直到被unpark()。如果不可中断的情况下被中断了,那么会从park()中醒过来,发现拿不到资源,从而继续进入park()等待。
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                interrupted = true;
              //如果等待过程中被中断过,哪怕只有那么一次,就将interrupted标记为true
        }
    } finally {
        if (failed) 
        // 如果等待过程中没有成功获取资源(如timeout,或者可中断的情况下被中断了)
        // 那么取消结点在队列中的等待。
            cancelAcquire(node);
    }
}

shouldParkAfterFailedAcquire(p, node)和parkAndCheckInterrupt()就会将线程获取同步状态失败的线程挂起,避免获取同步状态失败的陷入“死循环”浪费资源。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;//拿到前驱的状态
    if (ws == Node.SIGNAL)
        //如果已经告诉前驱拿完号后通知自己一下,那就可以安心休息了
        return true;
    if (ws > 0) {
        /*
         * 如果前驱放弃了,那就一直往前找,直到找到最近一个正常等待的状态,并排在它的后边。
         * 注意:那些放弃的结点,由于被自己“加塞”到它们前边,它们相当于形成一个无引用链,稍后就会被保安大叔赶走了(GC回收)!
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
         //如果前驱正常,那就把前驱的状态设置成SIGNAL,告诉它拿完号后通知自己一下。
         // 有可能失败,人家说不定刚刚释放完呢!
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

// 如果前驱节点的 waitStatus 是 SIGNAL状态,即 shouldParkAfterFailedAcquire ⽅法会返回 true 
// 程序会继续向下执⾏ parkAndCheckInterrupt ⽅法,⽤于将当前线程挂起
private final boolean parkAndCheckInterrupt() {
    // 线程挂起,程序不会继续向下执⾏; 当前线程进入waiting状态
    LockSupport.park(this);
    // 根据 park ⽅法 API描述,程序在下述三种情况会继续向下执⾏
    //1.被 unpark
    //2. 被中断(interrupt)
    //3. 其他不合逻辑的返回才会继续向下执⾏
    
    // 因上述三种情况程序执⾏⾄此,返回当前线程的中断状态(查看是否被中断),并清空中断状态
    // 如果由于被中断,该⽅法会返回 true
    return Thread.interrupted();
}

被唤醒的程序会继续执⾏acquireQueued⽅法⾥的循环(自旋),如果获取同步状态成功,则会返回 interrupted = true的结果。最终回到最上层的方法。

public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();   // 由于之前中断状态被清空,需要重置中断标识
}

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

中断是一种【协同】机制,怎么理解这么高大上的词呢?就是女朋友叫你吃饭,你收到了中断游戏通知,但是否⻢上放下手中的游戏去吃饭看你心情 。在程序中怎样演绎这个心情就看具体的业务逻辑了,Java 的中断机制就是这么简单 image.png

2.2.1 取消等待

正常情况下,如果跳出循环,failed 的值为false,所以只有不正常的情况才会执行到这里,也就是会发生异常,才会执行到此处。

final boolean acquireQueued(final Node node, int arg) {
    ......   
    finally {
        if (failed) 
        // 如果等待过程中没有成功获取资源(如timeout,或者可中断的情况下被中断了)
        // 那么取消结点在队列中的等待。
            cancelAcquire(node);
    }
}

查看 try 代码块,只有两个方法会抛出异常:

  • node.processor() 方法
  • 自己重写的 tryAcquire()

前驱节点这个方法返回的空显然不是我们要关心的(不会出现这种情况)

/**
 * Returns previous node, or throws NullPointerException if null.
 * Use when predecessor cannot be null.  The null check could
 * be elided, but is present to help the VM.
 * @return the predecessor of this node
 */
final Node predecessor() throws NullPointerException {
    Node p = prev;
    if (p == null)
        throw new NullPointerException();
    else
        return p;
}

因此我们把目光转向自定义同步器重写的tryAcquire()方法,这里以ReentrantLock.FairSync.tryAcquire()为例。我们看到抛出了 Maximum lock count exceeded的Error,这种情况下会取消在队列中排队等待获取同步状态。

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;
}

另外,上面分析 shouldParkAfterFailedAcquire 方法还对 CANCELLED 的状态进行了判断,CANCELLED状态的阶段会被从等待队列中清除。

那么问题又来了, 节点清楚具体是什么做的,我们从 cancelAcquire() 这个方法的源码中寻找答案。

private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist 忽略无效节点
        if (node == null)
            return;
        // 将关联的线程信息清空
        node.thread = null;

        // Skip cancelled predecessors 跳过同样是取消状态的前驱节点
        Node pred = node.prev;
        while (pred.waitStatus > 0) 
        //waitStatus>0表示该节点是被取消的节点 
       node.prev = pred = pred.prev;

        // predNext is the apparent node to unsplice. CASes below will
        // fail if not, in which case, we lost race vs another cancel
        // or signal, so no further action is necessary.
        // 跳出上面循环后找到前驱有效节点 pred,并获取该有效节点的后继节点predNext
        Node predNext = pred.next;

        // Can use unconditional write instead of CAS here.
        // After this atomic step, other Nodes can skip past us.
        // Before, we are free of interference from other threads.
        // 将当前节点的状态置为 CANCELLED
        node.waitStatus = Node.CANCELLED;

        // If we are the tail, remove ourselves.
        // 情况1,当前节点是尾节点,那么直接从队尾将自己删除
        if (node == tail && compareAndSetTail(node, pred)) {
              // 队尾是前驱有效节点 pred,predNext=null
            compareAndSetNext(pred, predNext, null);  
        } else {
        //进入else说明node不是队尾(或者是队尾但是cas队尾失败(其实结果也不是队尾,因为被别的线程抢先了))
            // If successor needs signal, try to set pred's next-link
            // so it will get one. Otherwise wake it up to propagate.
            int ws;
            
            // 情况2
            // 1. 如果当前节点的有效前驱节点不是头节点,也就是说当前节点不是头节点的后继节点
            if (pred != head &&
                //  2. 判断当前节点有效前驱节点的状态是否为 SIGNAL
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 // 3. 如果不是,尝试将前驱节点的状态置为 SIGNAL
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                 // 判断当前节点有效前驱节点的线程信息是否为空
                pred.thread != null) {
                // 上述条件满足
                Node next = node.next;
              // 将当前节点有效前驱节点的后继节点指针指向当前节点的后继节点
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
            // 情况3,如果当前节点的前驱节点是头节点,或者上述其他条件不满足,就唤醒当前节点的后继节点
                unparkSuccessor(node);
            }
            node.next = node; // help GC
        }
    }

其核心目的就是从等待队列中移除 CANCELLED 的节点,并重新拼接整个队列,总结来看,其实设置 CANCELLED 状态节点只是有三种情况,我们通过画图来分析一下。

image.png

image.png

image.png

至此,获取同步状态的过程就结束了,我们简单的用流程图说明一下整个过程。 image.png

2.3 独占式释放同步状态

AQS 模版方法 release() 是入口

public final boolean release(int arg) {
    // 调用自定义同步器重写的 tryRelease 方法尝试释放同步状态
    if (tryRelease(arg)) {
    // 释放成功,获取头节点
        Node h = head;
    // 存在头节点,并且waitStatus不是初始状态
    // 通过获取的过程我们已经分析了,在获取的过程中会将 waitStatus的值从初始状态更新成 SIGNAL 状态
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

private void unparkSuccessor(Node node) {
        // 获取头节点的waitStatus
        int ws = node.waitStatus;
        if (ws < 0)
            // 清空头节点的waitStatus值,即置为0
            compareAndSetWaitStatus(node, ws, 0);

        // 获取头节点的后继节点
        Node s = node.next;
        // 判断当前节点的后继节点是否是取消状态,如果是,需要移除,重新连接队列
        // s.waitStatus > 0 是取消状态
        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);
    }

参考资料

# 从Lock到AQS了解Java中的锁