• AbstractQueuedSynchronizer源码分析


    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同步器的实现原理

  • 相关阅读:
    图片在网页中不能显示
    利用QQWry.dat显示客户IP所在地 [转贴]
    asp.net时间戳与系统时间互转 mssql
    酷友网 http://www.kuiu.cn/ 再次上线了!!!
    string.Format .net string 补空
    ASP.net中md5加密码的方法[转]
    C#中|(位或)和||(逻辑或)有什么区别?
    .net身份证号码验证
    实用jquery代码片段集合
    ERROR: “System.Web.Mvc.Controller.File(string, string, string)”是一个“方法”
  • 原文地址:https://www.cnblogs.com/rouqinglangzi/p/9891408.html
Copyright © 2020-2023  润新知