Java并发——AQS源码深度解析

435 阅读15分钟

一. AQS是什么?

  ASQ是AbstractQueuedSynchronizer的简称,它的类全限定名为:java.util.concurrent.locks.AbstractQueuedSynchronizer. 后文中我们称之为同步器。
同步器是构建同步锁和其他同步组件的基础框架。它使用了一个int成员变量表示同步状态,通过内部的FIFO队列来完成资源的获取和线程的排队工作。
  同步器的主要使用方式是继承。子类继承同步器并实现它的抽象方式来管理同步状态。在实现的过程中,主要就是修改成员变量state的值。
同步器中提供了3个方法来操作state:

方法名 说明
getState() 获取变量state的当前值
setState(int newState) 设置state的值
compareAndSetState(int expect, int update) 通过CAS操作更新state的值

关于CAS操作网络上有很多精辟的博客,这里不再重复造轮子。

  要想理解同步器的工作原理,无非是彻底理解基于同步器中的锁的特征和获取锁与释放锁这两个过程与各自的使用方式。比如锁的模式有共享模式和独占模式;获取锁的方式有普通获取、可中断获取、可自旋获取等。

二.AQS内部结构

2.1 三个重要的属性

同步器内部最重要的的三个属性如下:

属性名 类型 修饰符 解释
state int volitile 同步状态。AQS所有的操作都是围绕着state变量展开的。
head Node volitile 指向FIFO队列的头部,除了初始化之外,只能通过setHead()方法设置。只有成功获取锁之后才能设置head
tail Node volitile 指向FIFO队列的尾部。除了初始化之外,只有在获取锁失败时,将线程构建成Node节点添加到FIFO队列尾部并重新设置tail指向的节点。

2.2重要的内部类——Node

  同步器的FIFO队列实际上是基于内部类Node构建的。 它的源码如下:

static final class Node {
        // 表示Node为共享模式
        static final Node SHARED = new Node();
        // 表示Node为独占模式
        static final Node EXCLUSIVE = null;

        // ws状态为 1,表示当前线程执行了取消竞争锁资源的操作。那么这个线程所在的Node节点将会从FIFO队列中剔除掉。
        // 另外,需要注意的是:ws状态只要CANCELLED会大于0,即:wd>0成立的话,说明当前线程只有执行取消竞争锁的操作这一种情况。
        static final int CANCELLED =  1;
        // wd状态为 -1,表示当前线程已经获取到了锁资源。
        static final int SIGNAL    = -1;
        // ws状态为-2,表示当前Node节点所在的线程获取到了锁,但是不满足condition,
        // 这种情况下,需要将当前Node节点从FIFO队列中移除,并添加到等待队列中。
        static final int CONDITION = -2;
        // ws状态为-3,只有在共享模式下才会存在此值。表示锁应该向下传播。
        static final int PROPAGATE = -3;

        // waitStatus状态就是上文中所说的ws,它的值只可能是以上5种情况外加0.
        // 当值为0时表示什么都不是。
        volatile int waitStatus;

        // 当前节点的前置节点
        volatile Node prev;

        // 当前节点的后继节点
        volatile Node next;

        // 当前节点所代表的线程
        volatile Thread thread;

       // 当存在Condition时,此值表示在condition队列下,此节点的后继等待节点。
       // 当不存在Conditon时,此值如果指向SHARED,则表示此节点是共享模式,如果指向EXCLUSIVE,即nextWaiter == null,则表示此节点是独占模式。
        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
        }

        // 构造方法,在addWaiter时调用此构造方法构建Node节点
        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }

        // 构造方法,在使用Condition时调用此构造方法构建Node节点
        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

2.3 重要的内部类——ConditionObject

  由于ConditionObject的源码太多,这里我们只挑最重要的来说。

2.3.1 重要的属性及说明

属性名称 类型 说明
firstWaiter Node 同一condition下,指向等待队列的中的第一个Node
lastWaiter Node 同一condition下,指向等待队列的中的最后一个Node

2.3.2 重要的方法及说明

方法名称 说明
await() 将当前线程挂起,直到其他线程调用了signal()或者signalAll(),或者是当前线程被中断
await(long time, TimeUnit unit) 和await方法一样,只是在没有被signal唤醒的话在经过time时间之后将自动醒来
awaitNanos(long nanosTimeout) 和await方法一样,只是在没有被signal唤醒的话,经过nanosTimeout时间之后将自动醒来
awaitUninterruptibly() 和await方法一样,只是此方法会忽略掉线程中断标志
awaitUntil(Date deadline) 和await方法一样,只是在没有被signal唤醒的话,达到deadline时间之后将会自动醒来
signal() 将唤醒condition queue中的第一个Node节点(FIFO)
signalAll() 将唤醒condition queue中的所有的Node节点

2.4 AQS的队列模型

  通过以上的介绍之后,在脑海里就会浮现出同步器中的队列模型了。不管是普通的队列还是基于condition的条件队列都是FIFO的。

2.4.1 普通的FIFO双向队列

AQS结构

  某个节点是共享模式时,如果这个节点所在的线程获取到了锁资源,锁资源的获取仍然会基于FIFO向后继节点继续传播;
  反之,如果某个节点时独占模式时,如果这个节点所在的线程获取到了锁资源,则锁资源将会被这个线程独占,直到其释放了锁资源之后,才会唤醒后继节点参与锁资源的竞争。

2.4.2 基于condition的FIFO单向队列

AQS结构-condition queue

2.4.3 FIFO队列和Condition队列如何互转

  在使用condition时,必须先获取到同步锁,才能使用condition.await功能。这就意味着:当await时,线程将会从FIFO队列中移除,然后基于Node(Thread thread, int waitStatus)构造方法构建成另一个Node节点添加到condition队列的尾部。
  当调用signal()方法时,会将condition队列的头部即firstWaiter指向的Node节点移除并添加到FIFO队列的尾部参与同步锁的竞争。
  signalAll()方法本质上是和signal()一样的,只是它会通过循环的方式将condition队列上的所有节点都按照signal()的方式处理。

  另外,在一个同步器中,可以存在多个condition队列!关于FIFO队列以及基于condition的队列我们在下面通过例子来进行说明。

三. AQS的使用示例

public class MyLock {

    private Sync sync = new Sync();

    public MyLock() {
        sync = new Sync();
    }
    
    // 加锁,委托给内部类
    public void lock() {
        sync.acquire(1);
    }
    // 解锁,委托给内部类
    public void release() {
        sync.release(1);
    }

    //通过静态内部类继承AQS,并实现抽象方法
    private static class Sync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int arg) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        @Override
        protected boolean tryRelease(int arg) {
            if (getState() == 0) {
                throw new IllegalArgumentException();
            }
            if (compareAndSetState(1, 0)) {
                setExclusiveOwnerThread(null);
                return true;
            }
            return false;
        }
    }
}

  以上便是一个简单的同步锁的实现。接下来我们一步一步的来解析。

3.1 同步加锁过程解析

  首先是通过调用acquire()方法来进行同步,它的源码如下:

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

  这个方法主要过程如下:

acquire方法

3.1.1 添加队列源码解析

private Node addWaiter(Node mode) {
        // 以当前线程构建节点
        Node node = new Node(Thread.currentThread(), mode);
        // 判断尾节点是否存在,如果存在,则通过CAS操作尝试快速将当前节点添加到队列尾部,如果添加成功,则返回。
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 如果尾部节点不存在或者快速添加失败,则调用enq()方法添加到尾部。
        enq(node);
        return node;
    }

  1.首先构建以当前线程和模式构建一个Node节点;AQS有两种模式:共享模式和独占模式。
  2.在队列不为空的情况下,通过CAS操作将当前节点添加到队列的尾部。tail是同步器的一个变量,它指向尾节点。
  3.在队列为空的情况下,调用enq()方法添加节点到队列中。enq()源码如下:

private Node enq(final Node node) {
        // 自旋
        for (;;) {
            Node t = tail;
            // 如果队列为空,则通过CAS操作设置head节点,目的是为了初始化队列。
            // 如果队列不为空,则通过CAS操作尝试将当前节点添加到队列尾部。
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

  enq方法的主要目的是确保队列能够初始化成功,并且就当前节点添加到节点中。如何确保操作成功?自旋!如何确保线程安全?CAS操作

3.1.2 获取同步锁源码解析

  4.当前线程添加到队列之后, 通过acquireQueued()方法尝试获取锁。它的源码如下:

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);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 如果获取锁失败,则判断是否需要将当前节点的线程挂起
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

  4.1 首先获取当前节点的前置节点p,如果p就是队列中的头结点(即head指向的节点),则调用子类的实现的tryAcquire方法尝试获取锁;
  4.2 如果获取失败,则判断当前节点所拥有的是否需要park(挂起),判断是否需要挂起的源码解读如下。

3.1.3 判断当前线程是否需要挂起源码解析

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        // 获取前置节点的等待状态即ws
        int ws = pred.waitStatus;
        // 如果前置节点ws为SINGAL状态,表示其已经获取锁并处于运行状态,此时返回true
        // 把当前节点挂起等待;等待前置节点运行完毕释放锁并将当前节点唤醒。
        if (ws == Node.SIGNAL) {
            return true;
        }
        // 如果前置节点的等待状态>0,表示前置节点已经取消了获取锁的竞争,
        // 此时需要将前置节点移除队列。
        // 如果前置节点不是取消状态,则尝试CAS操作尝试将当前节点的ws状态设置为SIGNAL状态。如果设置成功,说明前置节点已经获取到锁并处于运行状态咯!
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        // 在前置节点不是SIGNAL状态下,此时都会返回false,表示当前节点将继续自旋来尝试获取锁。
        return false;
    }

  4.2.1 首先检查前置节点的ws状态是否为SIGNAL,如果是SIGNAL状态,表示前置节点已经获取锁并处于运行中,当前节点的线程处于等待运行状态,因此,返回true;
  4.2.2 否则,如果ws的值 > 0 即CANCEL状态,说明前置节点已经取消了参与锁的竞争,此时需要将这个前置节点从FIFO队列中剔除!
  4.2.3 如果前置节点没有CANCEL,则通过CAS机制尝试将前置节点的ws状态更新为SIGNAL,然后返回false。
到这里,线程获取同步锁的过程便解析完毕,奉上详细的流程图:

3.2 释放锁过程解析

  释放锁的源码如下:

public final boolean release(int arg) {
        // 调用子类实现的tryRelease来释放锁
        if (tryRelease(arg)) {
            Node h = head;
            // 如果释放成功了,则判断head节点是否存在,并且不是取消状态,
            if (h != null && h.waitStatus != 0)
                // 唤醒park中的节点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
  1. 首先会调用子类实现的tryRelease来释放锁;
  2. 如果释放成功,则判断head节点是否存在,并且不是取消状态。这里的head节点其实就是拥有当前线程的节点。在获取锁过程的解析中给出了答案:只有在线程成功获取锁的情况下,才能将线程自己所在的节点设置为head节点。
  3. 唤醒park中的节点,其源码如下:
private void unparkSuccessor(Node node) {

        // 首先,通过CAS操作将当前ws状态设置为0,
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        // 获取head节点的后继节点。
        Node s = node.next;
        // 如果后继节点不存在或者ws>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);
    }

  释放锁的过程比较简单,但其中也暗含玄机。
  1.为什么这里一定要调用unpark方法?
  答:因为在获取锁的过程中,线程有可能会被park,也有可能不会被park,因此这里必须要调用unpark方法来确保线程处于可运行的状态。否则有可能导致队列永远的阻塞了。

  2.当后继节点不存在时,为什么要从队列尾部开始遍历寻找队列最前面的节点,而不是从队列最前面开始遍历没有取消的节点?
  答:这个问题先不着急解答,待分析了cancelAcquire()源码之后,便能了然!

3.3 取消锁获取源码解析

  在获取锁时,不论何种获取锁的方式的任何原因,导致最终获取锁失败了,则会调用cancelAcquire()方法来取消获取锁。比如acquireQueued()/acquireInterruptibly()等... cancelAcquire()方法的源码如下:

private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        if (node == null)
            return;

        node.thread = null;

        // Skip cancelled predecessors
        // 注意,这里只是维护了每个节点的prev节点,并没有维护每个节点的next节点
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

        Node predNext = pred.next;
        // 将当前节点的ws状态设置为取消状态。
        node.waitStatus = Node.CANCELLED;
        
        // 从FIFO队列中剔除当前节点。
        // 如果当前节点是FIFO队列中的尾节点,则通过CAS操作重新设置tail
        if (node == tail && compareAndSetTail(node, pred)) {
            // 如果设置成功,则通过CAS操作重新设置尾部节点的后继节点为null。
            // 这样,当前节点就从FIFO队列中剔除了。
            compareAndSetNext(pred, predNext, null);
        } else {
            // 否则,如果当前置节点不是头节点并且前置节点的ws状态为SIGNAL或通过CAS操作设置为SIGNAL时,说明前置节点已经获取到同步锁,此时,会通过CAS操作将当前节点从FIFO队列中剔除。
            // 如果以上条件不成立,说明当前节点时头节点,则会调用unparkSuccessor方法唤醒它的后继节点。
            int ws;
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.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 {
                unparkSuccessor(node);
            }

            node.next = node; // help GC
        }
    }

  在3.2中我们遗留了一个问题:后继节点不存在时,为什么要从队列尾部开始遍历寻找队列最前面的节点,而不是从队列最前面开始遍历没有取消的节点?现在,我们可以给出答案了:
在cancelAcquire()方法中,只维护了节点的prev的关系,并没有维护节点的next关系。因此,只有从FIFO队列的尾部往前遍历,才能得到各个节点的正确链路关系。

3.4 获取锁资源的其他方法及说明

方法名 说明
acquire() 独占模式下,获取锁资源, 源码解读请看3.1章节
acquireShared() 共享模式下,和acquire方法一样
acquireInterruptibly() 独占模式下,和acquire方法一样,只不过它支持中断机制,当线程被中断时,将会抛出InterruptedException异常
acquireSharedInterruptibly() 共享模式下,和acquireInterruptibly方法一样
tryAcquireNanos() 独占模式下,支持在指定nano时间段内获取自旋,如果在nanos时间内为获取到锁资源,则返回false,表示获取锁资源失败;同样地,它也支持线程中断机制。
tryAcquireSharedNanos() 共享模式下,和tryAcquireNanos方法一样

3.5 释放锁资源的其他方法及说明

  独占模式下,释放锁资源的的方法为release()方法,它的源码解读请看3.3章节。
共享模式下,释放锁资源的方法为releaseShared()方法,本质上它和release()的功能是一样的,只不过在共享模式下,如果当前线程所在的节点不是头结点,则会设置它的ws状态为PROPAGATE状态,即无条件的传播锁资源。

四. 总结

  同步器是J.U.C包中很多高级并发工具类的基础框架,比如ReentrantLock、CountDownLatch、Semaphore等,虽然我们在使用这个工具类时只需会应用即可,但是如果某些需求场景需要自定义并发工具类时,对于同步器的原理的深入掌握是必不可少的,另外,对于同步器的掌握一定程度上能够体现一个开发者对于Java并发的理解程度,因为它涉及到的理论思想还是挺多的!
  通过本文的介绍,我们知道了同步器的工作原理以及使用方式。实际上同步器的底层是基于很多并发理论、技术和思想的,比如CAS、自旋、volatile、FIFO队列和对于LockSupport工具类、UnSafe工具类的使用。因此,如果要彻底弄懂同步器的工作原理,很不容易!首先必须要理解Java并发中的这些基本理论和思想。如果对于这些知识还不够清晰,可以参考前面的Java并发系列文章。