AbstractQueuedSynchronizer(AQS)源码分析

AbstractQueuedSynchronizer,简称为AQS,它是构建JDK中多个并发工具的基础。下图展示了JDK中使用AQS构建的并发工具。

可见,AQS在Java并发编程中是多么的重要。所以,我们有必要搞清楚其实现的原理。

一、AQS中的数据结构

在AQS类文件的注释中,作者已经给出了内部数据结构的说明。AQS里使用的是同步队列是CLH(Craig, Landin, and Hagersten)锁队列的一个变形,其中CLH锁通常用于自旋锁。CLH队列从节点的结构与节点等待机制两方面进行了改造:

①在结构上:引入了头结点和尾节点,他们分别指向队列的头和尾,尝试获取锁、入队列、释放锁等实现都与头尾节点相关,并且每个节点都引入前驱节点和后后续节点的引用;

②在等待机制上:由原来的自旋改成阻塞唤醒

AQS的核心原理:

当多个线程在竞争获取同步状态时,如果当前线程获取同步状态成功,则AQS会将当前线程标识为锁的持有者。如果当前线程如果获取同步状态失败时,AQS则会将当前线程以及等待状态等信息包装成一个节点,并将其添加到同步队列尾部等待锁的释放,同时阻塞当前线程。而当同步状态释放时,会唤醒后继结点,使其再次尝试获取同步状态。

 

下面来看一下结点的声明

    static final class Node {
        /**竞争锁的两种模式**/
        //共享模式
        static final Node SHARED = new Node();
        //排它模式
        static final Node EXCLUSIVE = null;
        
        /**线程等待状态常量**/
        //表明线程等待锁超时或已被取消。处于该状态后,状态不会再发生变化
        static final int CANCELLED = 1;
        static final int SIGNAL = -1;
        static final int CONDITION = -2;
        static final int PROPAGATE = -3;
        //等待状态:取值范围为上面的几个值,初始值为0
        volatile int waitStatus;
        
        //前驱节点
        volatile Node prev;
        //后继节点
        volatile Node next;
        //持有的线程(线程会被包装成节点)
        volatile Thread thread;

        //下一个在Condition条件上等待的节点。
        //因为condition队列仅存在于排它模式下,所以当线程在condition上等待时,我们只需要一个简单的链表队列持有节点即可。之后他们可以被转移到竞争锁的同步队列中重新获取锁。
        Node nextWaiter;
    }

Node结点是对每一个访问同步状态的线程的封装,其包含了需要同步的线程本身以及线程的状态,例如是否被阻塞,是否等待唤醒,是否已经被取消等。waitStatus则表示当前被封装成Node结点的等待状态,共有4种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE。

  • SIGNAL:值为-1。被标识为等待唤醒状态的后继结点,当其前继结点的线程释放了同步锁或被取消,将会通知该后继结点的线程执行。说白了,就是处于唤醒状态,只要前继结点释放锁,就会通知标识为SIGNAL状态的后继结点的线程执行
  • 初始状态:值为0,初始化状态。
  • CANCELLED:值为1。在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该结点,其结点的waitStatus为CANCELLED,即结束状态,进入该状态后的结点将不会再变化。
  • CONDITION:值为-2。与Condition相关,该标识的结点处于等待队列中,结点的线程等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
  • PROPAGATE:值为-3。与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。

 

总结一下:

同步队列中waitStatus为SIGNAL(-1)的节点会被选为下一个获取锁的后继节点。

等待队列中waitStatus为CONDITION(-2)的节点会被在其它线程调用signal()后从等待队列转移到同步队列

等待队列中watiStatus为CANCELLED(1)的节点由于超时等待或被中断,而放弃获取锁。

同步队列中的节点如果想最终获得锁,则必须经过两个过程:被标记为CONDITION(-2)----->被标记为SIGNAL(-1)

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    /**
     * 创建AQS实例,初始同步状态为0
     */
    protected AbstractQueuedSynchronizer() { }    
    
    static final class Node {……}
    
    //头指针
    private transient volatile Node head;
    //尾指针
    private transient volatile Node tail;
    //同步状态
    private volatile int state;
    
}

二、获取/释放锁流程

1.排它模式获取锁acquire

    public final void acquire(int arg) {
        //获取锁失败,则将线程封装成节点加入队列尾部,并中断当前线程。
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    private Node addWaiter(Node mode) {
        //1.创建节点,并指定排它/模式模式
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        //2.尝试用快速方式直接放到队尾。如果失败,则使用自旋方式添加。
        Node pred = tail;
        if (pred != null) {
            //与旧尾节点连接(新节点prev指向旧尾节点)
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                //与旧尾节点连接(旧尾节点next指向新节点)
                pred.next = node;
                return node;
            }
        }
        //3.通过自旋+CAS方式,将节点加入到队列尾部
        enq(node);
        return node;
    }
    
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;// 标识是否成功获取同步状态
        try {
            boolean interrupted = false;//标识等待过程中是否被中断过
            
            //自旋操作:不停的判断自己是否排在head后面
            for (;;) {
                final Node p = node.predecessor();//前驱
                //如果前驱是head,说明自己正排在head后面,便有资格去尝试获取资源(head释放同步状态后唤醒自己,当然也可能被interrupt)。
                if (p == head && tryAcquire(arg)) {
                    setHead(node);//获取到同步状态后,就自己设置为head结点
                    p.next = null; // 便于GC回收以前的head结点
                    failed = false; 
                    return interrupted;
                }
                
                //执行到这里,说明:没有排在head后面,或者排在head后面但未获取到同步状态

                //判断自己是否应该休息,如果是则通过park()进入waiting状态,直到被unpark。
                //如果不可中断的情况下被中断了,那么会从park()中醒过来,发现拿不到同步状态,从而继续进入park。
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;//如果等待过程中被中断过,将interrupted标记为true
            }
        } finally {
            if (failed) // 如果等待过程中没有成功获取资源(如超时,或可中断的情况下被中断了),那么取消结点在队列中的等待。
                cancelAcquire(node);
        }
    }
    
    private static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }

如果获取同步状态成功,则直接返回。

如果获取同步状态失败。则加入到队列尾部。

  • 首先尝试通过快速模式添加。
  • 如果不成功,则通过CAS+自旋的方式添加。

成功加入到队列尾部后,结点即将进入等待状态。但不会立即进入等待状态,因为很可能此时head结点即将释放同步状态,自己立马就有资格获取到同步状态了。所以会先经历一段自旋时间,自旋时会不停的判断自己是否为正排在head结点后。

  • 如果是排在head后则尝试获取同步状态,如果head恰好释放了同步状态,则会顺利拿到同步状态,之后将自身设为head结点。(运气好)
  • 当然更常见的情况可能是发现自己没排在head后面,或者即使排在head后却一直拿不到同步状态,说明head可能一时半会不会立马释放同步状态,那要不干脆就休息一会吧!

通过判断,如果确实需要休息,则通过park进入waiting状态,直到被unpark。

2.排它模式释放锁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;
    }

    private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual non-cancelled successor.
         *
         * 要唤醒的线程是被后继节点所持有,后继节点通常来说就是下一个节点。
         * 但如果后继节点被取消(CANCELED状态)或为空,则从尾节点向前遍历来查找真正非取消状态的后继。
         */

        //后继节点
        Node s = node.next;
        //后继为空或为被取消状态(CANCELED)
        if (s == null || s.waitStatus > 0) {
            s = null;//若是被取消状态,则设置为null有助于GC
            //从尾部往前遍历查找最靠前的非取消状态的节点【为什么不从前往后查找?我的猜测:因为s==null,则s.next和s.prev都为null,无法遍历】
            for (Node t = tail; t != null && t != node; t = t.prev)
                //只要等待状态<=0(-1,-2,-3,0),就可以唤醒
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            //唤醒
            LockSupport.unpark(s.thread);
    }

    public static void unpark(Thread thread) {
        if (thread != null)
            UNSAFE.unpark(thread);
    }

3.获取锁失败加入同步队列addWaiter

将竞争锁失败的线程加入到同步队列

    public final void acquire(int arg) {
        //获取锁失败,则将线程封装成节点加入队列尾部,并中断当前线程。
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

    /**
     * Creates and enqueues node for current thread and given mode.
     * 将当前线程包装成节点加入等待队列,并指定模式(排它模式/共享模式)
     */
    private Node addWaiter(Node mode) {
        //1.创建节点,并指定排它/模式模式
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        //2.尝试用快速方式直接放到队尾。如果失败,则使用自旋方式添加。
        Node pred = tail;
        if (pred != null) {
            //与旧尾节点连接(新节点prev指向旧尾节点)
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                //与旧尾节点连接(旧尾节点next指向新节点)
                pred.next = node;
                return node;
            }
        }
        //3.通过自旋+CAS方式,将节点加入到队列尾部
        enq(node);
        return node;
    }

    /**
     * Inserts node into queue, initializing if necessary. See picture above.
     * 将节点入队列,必要时会进行初始化。
     */
    private Node enq(final Node node) {
        //自旋+CAS方式将节点加到队列尾部
        for (; ; ) {
            Node t = tail;
            //队列为空,则先初始化创建一个空节点,并将tail指针指向它
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {//队列非空,则插入队尾
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

将竞争锁失败的线程入队列,大致分为以下3步:
①将该线程封装成节点,并指定当前所处于的模式(排它模式/共享模式)
②首先使用快捷方式尝试加入到队列:直接插入队列尾部。
③如果上述方式失败则使用自旋+CAS的方式插入队列尾部。

4.释放锁时唤醒后继unparkSuccessor

唤醒后继

head节点是获取同步状态成功的节点。首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为head节点。

    /**
     * 唤醒后继节点
     */
    private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual non-cancelled successor.
         *
         * 要唤醒的线程是被后继节点所持有,后继节点通常来说就是下一个节点。
         * 但如果后继节点被取消(CANCELED状态)或为空,则从尾节点向前遍历来查找真正非取消状态的后继。
         */

        //后继节点
        Node s = node.next;
        //后继为空或为被取消状态(CANCELED)
        if (s == null || s.waitStatus > 0) {
            s = null;//若是被取消状态,则设置为null有助于GC
            //从尾部往前遍历查找最靠前的非取消状态的节点【为什么不从前往后查找?我的猜测:因为s==null,则s.next和s.prev都为null,无法遍历】
            for (Node t = tail; t != null && t != node; t = t.prev)
                //只要等待状态<=0(-1,-2,-3,0),就可以唤醒
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            //唤醒
            LockSupport.unpark(s.thread);
    }

三、等待通知流程

5.等待await

    public final void await() throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        //将当前线程包装成节点,加入到Condition等待队列(非同步队列)尾部
        Node node = addConditionWaiter();
        //释放当前线程的独占锁(不管重入几次,都把state释放为0)
        int savedState = fullyRelease(node);
        int interruptMode = 0;
        //如果当前节点没有在同步队列上,即还没有被signal,则将当前线程阻塞
        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);
    }

调用await方法后,当前获取了锁的线程会被包装成节点加入到等待队列尾部,同时会释放锁,通知后继线程来获取锁。

6.通知signal

    public final void signal() {
        if (!isHeldExclusively())
            throw new IllegalMonitorStateException();
        Node first = firstWaiter;
        if (first != null)
            doSignal(first);
    }
    
    private void doSignal(Node first) {
        do {
            if ((firstWaiter = first.nextWaiter) == null)
                lastWaiter = null;
            first.nextWaiter = null;
        } while (!transferForSignal(first) &&
                (first = firstWaiter) != null);
    }

    //将被标记为CONDITION的节点转移到同步队列
    final boolean transferForSignal(Node node) {

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

线程调用signal后,会发通知将等待队列中的节点转移到同步队列中来获取锁。 

 

总结:

1.AQS的实现原理?

2.CLH同步队列是怎么实现非公平和公平的?

 

 

 

参考:

Java并发之AQS详解 

AbstractQueuedSynchronizer同步队列与Condition等待队列协同机制

JUC回顾之-AQS同步器的实现原理

posted @ 2019-01-03 15:14  静水楼台/Java部落阁  阅读(238)  评论(0编辑  收藏  举报