初步理解AQS

原创 2013年12月03日 16:16:10

欢迎移步google drive 阅读原文:

https://drive.google.com/file/d/0B758ryeeYYQYa1k2cHhzT0ZOSlU/edit?usp=sharing


摘要

AQS全称AbstractQueuedSynchronizer,它是concurrent包中最重要的基础设施类之一,负责作为模板类向业务层提供对临界区的管理。本文以FutureTask的实现机制作为引子,介绍了AQS的业务背景和设计思路,最后梳理了AQS的代码实现。

目录

摘要............................................................................................................................... 1

动机............................................................................................................................... 2

interrupt的真相.............................................................................................................................................................. 2

FUTURE TASK问题......................................................................................................................................................... 2

内部类Sync........................................................................................................................................................................... 4

层次结构....................................................................................................................... 4

业务层...................................................................................................................................................................................... 4

复用层...................................................................................................................................................................................... 7

代码分析....................................................................................................................... 8

如何复用AQS...................................................................................................................................................................... 8

【一个例子】.............................................................................................................................................................. 11

AQS内部细节................................................................................................................................................................... 15

概述.................................................................................................................................................................................... 15

线程控制......................................................................................................................................................................... 15

队列.................................................................................................................................................................................... 15

节点.................................................................................................................................................................................... 15

排他获取......................................................................................................................................................................... 17

排他释放......................................................................................................................................................................... 21

共享获取&释放.......................................................................................................................................................... 24

回到Future Task.......................................................................................................................................................... 29

 

 

动机

interrupt的真相

一提起interrupt,大家往往想到的是:

1、其中文直译为“中断”

2、多线程编程中无处不在的InterruptedException

 

或许是受到了中文翻译的误导,我们不少人会以为调用Thread.interrupt()能中断任意线程。

 

其实,它只是表示有人要求中断,具体的实现是更新了一个boolean型的标志位(interrupt status)。至于是否响应中断以及何时中断,都是线程自己的事了。

 

这就好比老师在台上讲课,下面有学生想要提问,必须先举手(请求中断)。如果老师在写板书没看到(尚未轮询到中断标志位),则什么也不会发生;如果面对学生,那么老师一般会等到一句话说完,再允许该生发言(延迟响应中断)或者宣布“讲完这一节再统一提问”(忽略并重置中断)。

 

所以,调用Thread.interrupt()只是“举手”而已。

 

有关interrupt和InterruptedException的细节,请参考此文:http://blog.csdn.net/axman/article/details/431796

正如文中所说,“只有当线程执行到sleep,wait,join等方法时,或者自己检查中断标志而抛出异常的情况下,线程才会抛出InterruptedException。”

这样做的目的是允许线程安全可控地结束,避免盲目结束出现中间状态、资源没被释放等情况。

 

FUTURE TASK问题

接下来转到FutureTask。这是concurrent包里提供一个很方便的工具类,用于接受指定线程作为宿主,异步运行一个任务。其get()方法能阻塞当前用于查询结果的线程(以下简称查询线程)直到异步线程运行结束。

之前一度以为,若调用FutureTask.cancel()方法,能随时中止异步任务。

事实并非如此。就像中断机制一样,若异步任务本身没有通过Thread.isInterrupted()主动检查中断标志,那么它不仅不会结束,也不会抛出中断异常。倒是多个查询线程都能即时抛出CancellationException。

 

观察FutureTask.cancel()方法,其代码调用了内部类Sync.innnerCancel(),后者看起来只是简单地触发了Thread.interrupt(),这符合我们的预期。但又是怎样的机制分别唤醒了处于阻塞状态的查询线程呢?代码中releaseShared(0)引起了我的注意。

 

 

       boolean innerCancel(boolean mayInterruptIfRunning) {

            for(;;) {

                int s= getState();

                if(ranOrCancelled(s))

                    return false;

                if(compareAndSetState(s,CANCELLED))

                    break;

            }

            if(mayInterruptIfRunning) {

                Thread r = runner;

                if(r != null)

                    r.interrupt();

            }

            releaseShared(0);

            done();

            return true;

        }

内部类Sync

为了搞清楚这个问题的来龙去脉,我展开了对FutureTask内部类Sync的调查,于是自然延伸到了其基类AbstractQueuedSynchronizer(以下简称AQS)。AQS是如此庞杂,绝非三言两语能说得清楚;更糟糕的是,网上大量有关该类的文章和学习笔记,要么蜻蜓点水一笔带过,要么一上来就跳入了代码细节的深渊,为此我绕了一个大圈才理顺。当时多么希望有一篇文章能深入浅出,用先宏观再微观的梳理方式,把这个类讲讲清楚。现在我也来写一篇AQS的学习笔记,以飨小伙伴们。

 

 

层次结构

业务层

先考虑如下现实世界中的情况:


(图片引用自http://www.jq1997.cn/mkldfffiles/2012531105117204.jpg)

 

l   例如,男厕是临界区,有些高端大气的小便槽(上图)可供一起使用(共享访问),容纳人数有限(资源有限);保洁员可以”在入口处放一块牌子“清洁中”(排他访问),也可以同时清洁。

l   又如,游泳池是临界区,泳客是共享访问,一场结束的时候倒消毒液的人“必须”先清场,再操作(排他访问)

l   再如,停车场是临界区,车辆均共享访问,车位(资源)有限,且小车占一个车位,大巴占两个车位。

 

可见,上述业务经常变化的部分是对临界区的资源管理,主要包含:

l   临界区内不同的“资源总数”;

l   每次访问占用的“资源数量”;

l   支持两种不同的“访问方式”(排他/共享)。

 

至于临界区被占满后,如何排队、能否插队(此处特指跳过排队直接进入临界区,注1)等管理细节都是雷同的。

 

接下来回到java的世界

让我们先来看看concurrent包里常见的并发控制相关的业务类。

 

类名

功能

资源总数

如何访问

备注

Mutex

互斥锁

1

仅排他

出现在AQS类的注释中

Semaphore

信号量

可指定(构造参数permits)

仅共享

 

ReentrantReadWriteLock

读写(可重入)锁

无上限(实际为16位unsigned short)

读锁共享访问,写锁排他访问

关于“可重入”见注2;

 

注意到,表内与前述资源管理过程中抽象出来的“变数”是一致的。

因此,业务层的重点是掌控资源管理中易变的部分,把不变的部分交给复用层。这就好比为了细分市场,我们可以出多款不同外观、大小的汽车,但引擎、变速箱之类的动力系统只要经过简单的调校就可以复用了。

 

关于锁的术语有很多,小注如下:

注1 此处涉及锁的公平性。锁被释放后,排队中的线程只有唤醒后才能争夺锁,可能竞争不过新到来的活动线程,意即不公平;极端情况下甚至会造成排队的线程始终拿不到锁,即饿死锁。

 

注2 “可重入”意为允许已经获得锁的对象再次获得这把锁,只要标记信号量++,可避免上述场景下死锁。

 

注3 偏向锁、自旋锁。参考http://kenwublog.com/theory-of-java-biased-locking

 

 

复用层

下面简述业务层与复用层之间的依赖关系。


 

职责简述:

类名

角色

职责

Semaphore

业务类

通过把业务方法代理给内部类Sync,提供同步工具的语义。

Sync

代理类

通过继承AQS,并按需覆写钩子方法,来指定业务“变数”(对代理和钩子不明白的请参考代理模式和模板模式)

AbstractQueuedSynchronizer

模板类

向业务类提供模板方法,封装了如何以共享或排他的形式访问临界区,处理包括临界区满后如何排队等细节。(Queued暗示排队相关)

AbstractOwnableSynchronizer

 

记录排他访问的线程信息。(Ownable暗示排他相关)

 

代码分析

如何复用AQS

 

Step1/3. 定义state。

state是一个int,它没有具体语义,但作为AQS的核心成员变量,可根据业务类的需要赋予state具体的语义。

 

例如,对于信号量Semaphore,它可以表示当前临界区内可用资源的数量。这种场景下,一般会把它初始化成资源总量。占用时--,释放时++。为0时表示资源被占满。

 

又如,对于可重入锁ReentrantLock,初始化为0表示锁可用;为1表示被占用,为n表示重入的次数。

 

再如,对于ReentrantReadWriteLock,state逻辑上被分成两个16位的unsigned short,分别记录读锁被多少线程共享和写锁被重入的次数。

 

特别地,对于异步任务FutureTask,state承载着任务的运行状态:0代表ready;1代表running;2代表ran;4代表cancelled。

 

为了保证线程安全,读写state必须通过下述三个protected final方法。

方法

简介

getState()

读状态

setState (int newState)

写状态

compareAndSetState (int expect, int update)

CAS写状态

 

注:AQS的另一个版本AbstractQueuedLongSynchronizer,采用了64位的long作为state的数据类型。

 

Step2/3. 新建内部类Sync,继承自AQS并按需实现钩子方法。

钩子方法

简介

tryAcquire (int arg)

排他获取(资源数)

tryRelease(int arg)

排他释放(资源数)

tryAcquireShared(int arg)

共享获取(资源数)

tryReleaseShared(int arg)

共享释放(资源数)

isHeldExclusively()

是否排他状态

 

获取和释放操作本质即变更state。

 

此处强调一下“按需”,如果你的业务只关心排他获取资源,比如Mutex这样的,就无需覆写共享相关的那一组方法了。这也是为何AQS身为一个抽象类,没有把钩子方法设为抽象方法,而是统一带默认实现(直接抛出UnsupportedOperationException)的原因。

 

 

Step3/3. 业务类把业务方法的实现委托给Sync。Sync可以直接使用继承自AQS的模板方法。

模板方法

简介

acquire(int arg)

排他获取(获取过程不可中断)

acquireInterruptibly(int arg)

排他获取(获取过程可中断)

tryAcquireNanos(int arg, long timeout)

排他获取(超时结束)

 

 

acquireShared(int arg)

共享获取(一组同上)

acquireSharedInterruptibly(int arg)

 

tryAcquireSharedNanos(int arg, long timeout)

 

 

 

release(int arg)

排他释放

releaseShared(int arg)

共享释放

 

 

hasQueuedThreads()

是否排队

getQueueLength ()

排队长度

(忽略若干次要方法)

 

 

【一个例子】

下面以业务最为简单的“不可重入互斥锁”Mutex举例说明(出现在AQS类的注释中)。请阅读下述代码并回顾上述三步。

 

一些重点:

l   state初始化为0,表示排他锁可用;变为1表示排他锁被占用。

l   业务类mutex.acquire()的实现委托给了sync.acquire()表明这是在排他获取资源。

l   sync.tryAcquire()作为钩子方法在Sync内实现,它将被sync.acquire()(继承自AQS)这个模板方法调用。可见业务类委托给了模板方法,而模板方法调用了钩子方法。

l   sync.tryAcquire()内部调用了继承自AbstractOwnableSynchronizer 的setExclusiveOwnerThread()方便地标记排他线程。

 

 

class Mutex implementsLock, java.io.Serializable {

 

      // Our internal helper class

      private static class Syncextends AbstractQueuedSynchronizer {

         // Report whether in locked state

         protected boolean isHeldExclusively(){

            return getState() == 1;

         }

 

         // Acquire the lock if state is zero

         public boolean tryAcquire(int acquires) {

            assert acquires == 1;// Otherwise unused

            if (compareAndSetState(0, 1)) {

               setExclusiveOwnerThread(Thread.currentThread());

               return true;

            }

            return false;

         }

 

         // Release the lock by setting stateto zero

         protected booleantryRelease(int releases) {

            assert releases == 1;// Otherwise unused

            if (getState() == 0)

               throw new IllegalMonitorStateException();

            setExclusiveOwnerThread(null);

            setState(0);

            return true;

         }

 

         // Provide a Condition

         ConditionnewCondition() {

            return new ConditionObject();

         }

 

         // Deserialize properly

         private void readObject(ObjectInputStream s) throws IOException,

               ClassNotFoundException{

            s.defaultReadObject();

            setState(0);// reset to unlocked state

         }

      }

 

      // The sync object does all the hardwork. We just forward to it.

      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(longtimeout, TimeUnit unit)

            throws InterruptedException {

         return sync.tryAcquireNanos(1,unit.toNanos(timeout));

      }

   }

 

 

AQS内部细节

概述

AQS实现了一个FIFO队列,用于管理等候在临界区外的线程。队列的每个节点保留了线程信息,因此线程可以安然休眠,待临界区的资源可用时再次唤醒。注意,队列的存在并不保证公平,因为AQS并不确保被唤醒的队首线程能进入临界区,唤醒过程中可能会被其它尚未入队的竞争线程抢占先机。

线程控制

线程的休眠和唤醒是通过LockSupport.park()和LockSupport.unpark()实现的。LockSupport是整个concurrent包最底层的API之一。在AQS的注释中写到,如果你对其排队机制不满,可以重新定义队列的数据结构并通过LockSupport调度。

队列

数据结构:双向链表构成的FIFO队列(链表是其物理结构,队列是其逻辑用途。注释中称其为CLH Lock队列的变种,而CLH Lock常用于实现自旋锁,其确保无饥饿性和公平性)

 

 

l   入队和出队针对的是节点(内部类Node),每个节点代表一个排队的线程。

l   头结点是一个哑元(dummy)节点,始终代表队列中最后一个进入了临界区的节点(线程)。

l   next指针用于维护队列顺序;当临界区的资源被释放时,头结点通过next指针找到队首节点。

l   prev指针用于在节点(线程)被取消时,让节点的上家直接指向下家完成出队动作(标准的链表操作)。

 

 

节点

节点类型有2种:共享和排他,对应两种访问方式。

 

此处引用一张图来说明排他和共享访问的特点。一般说来,读操作允许共享访问,而写操作属于排他访问。图中绿色表示允许进入临界区,红色表示被block。这意味着,被读锁占用的临界区,后续的读可以立即进入,写需要等待;而被写锁占用的临界区,后续无论读写都必须等待。(假设并发读数量不设上限)


(图片引用自http://ifeve.com/introduce-abstractqueuedsynchronizer/)

 

 

 

节点状态有5种,与队列的通知机制紧密相关。

节点状态waitStatus

用途

备注

0

非下述节点

默认状态。

 

1

CANCELLED

已取消。表示当前节点已放弃进入临界区。

 

-1

SIGNAL

发信号。当前节点在入队后、进入休眠状态前,应确保将其prev节点类型改为SIGNAL,以便后者取消或释放时将当前节点唤醒。

 

-2

CONDITION

 

条件队列专用。

-3

PROPAGATE

传播。由于共享访问的特点(上图),连续的读操作节点可以依次进入临界区,设为PROPAGATE有助于实现这种迭代操作。

共享专用

 

上述内容显得晦涩,为了更好理解,需要简化一下(抛开-2条件队列)并配合阅读下面的源码。要点是,排他的情况只需关注前三项-1,0,1;共享的情况再来看-3。

 

排他获取

简述acquire()

    public finalvoid acquire(intarg) {

       if (!tryAcquire(arg)&&

           acquireQueued(addWaiter(Node.EXCLUSIVE),arg))

           selfInterrupt();

}

 

在排他获取资源之前,不允许被中断。其实现可简单描述为:通过tryAcquire()尝试获取,不成功便包装为排他节点入队。注意acquireQueued()方法进入临界区后才会返回,且返回的是中断标志位,若为true,则触发selfInterrupt()抛出InterruptedException。可见中断请求不是被忽略了,而是推迟响应。

简述acquireQueue()

    final booleanacquireQueued(final Node node,intarg) {

       boolean failed = true;

       try {

            booleaninterrupted =false;

           for (;;) {

                finalNode p = node.predecessor();

                if(p == head && tryAcquire(arg)) {

                    setHead(node);

                    p.next= null;//help GC

                    failed = false;

                    returninterrupted;

                }

                if(shouldParkAfterFailedAcquire(p, node) &&

                    parkAndCheckInterrupt())

                    interrupted = true;

           }

        } finally{

           if (failed)

                cancelAcquire(node);

       }

}

 

注意无限循环代码块for (;;){…},下文中“无限for循环”特指它。

 

无限循环的内容是,首先取当前节点的上家,若上家为头节点,表示当前节点在队首,有资格尝试获取资源。成功的话会将当前节点设为头结点(头节点始终代表队列中最后一个进入了临界区的节点),清理并结束。

 

若上家非头节点,通过shouldParkAfterFailedAcquire()确保上家的节点状态被置为SIGNAL(会在合适的时候唤醒当前节点),再通过parkAndCheckInterrupt()进入休眠状态。

简述shouldParkAfterFailedAcquire()

    private staticboolean shouldParkAfterFailedAcquire(Node pred, Nodenode) {

       int ws = pred.waitStatus;

       if (ws == Node.SIGNAL)

           /*

             * This node has already set statusasking a release

             * to signal it, so it can safelypark.

             */

           return true;

       if (ws > 0) {

           /*

             * Predecessor was cancelled. Skipover predecessors and

             * indicate retry.

             */

           do {

                node.prev= pred = pred.prev;

           } while (pred.waitStatus > 0);

           pred.next = node;

       } else {

           /*

             * waitStatus must be 0 orPROPAGATE.  Indicate that we

             * need a signal, but don't parkyet.  Caller will need to

             * retry to make sure it cannotacquire before parking.

             */

           compareAndSetWaitStatus(pred, ws, Node.SIGNAL);

       }

       return false;

}

 

若上家状态为SIGNAL,当前节点可安然入睡;

 

ws>0(只能是1 Cancelled)表示上家被取消,通过do…while循环,递归迭代链表使无效上家们出队。正好借此过程清理无效节点。

 

else其它情况,CAS确保上家状态置为SIGNAL。

 

此函数return true表示允许当前节点(线程)休眠。return false表示需要重入无限for循环检查(可能在修改状态的过程中当前节点已经来到了队首,那么在休眠之前再tryAcquire()一次,可提高性能)。

简述parkAndCheckInterrupt()

    private finalboolean parkAndCheckInterrupt() {

       LockSupport.park(this);

       return Thread.interrupted();

    }

由于“&&”短路表达式的关系,仅当shouldParkAfterFailedAcquire()返回true的前提下,parkAndCheckInterrupt()才会被执行。前者表示当前节点(线程)可入眠,后者执行入眠动作LockSupport.park(this);并在唤醒后返回中断标志位。

 

唤醒后即使中断标志为true,无限for循环仍将继续,也不会即时抛出异常。对比acquireInterruptibly()所依赖的doAcquireInterruptibly(),最大的区别就是唤醒后会根据中断标志即时抛出InterruptedException。

 

小结

排他获取的重点是观察无限for循环代码块:仅当自己为队首时才尝试进入临界区;否则会在确保上家将会唤醒自己的前提下进入休眠状态。

排他释放

简述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;

    }

 

若tryRelease()成功,则试图唤醒队首(头节点的下家)

 

简述unparkSuccessor()

   private void unparkSuccessor(Nodenode) {

       /*

         * If status is negative (i.e.,possibly needing signal) try

         * to clear in anticipation ofsignalling.  It is OK if this

         * fails or if status is changed bywaiting thread.

         */

       int ws = node.waitStatus;

       if (ws < 0)

            compareAndSetWaitStatus(node,ws, 0);

 

       /*

         * Thread to unpark is held insuccessor, which is normally

         * just the next node.  But if cancelled or apparently null,

         * traverse backwards from tail to findthe actual

         * non-cancelled successor.

         */

       Node s = node.next;

       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 SIGNAL),表示下家需要唤醒,则预先将其状态重置为0,表示已尝试唤醒。这一步无伤大雅,即使被新入队的节点改回去了也没关系。

 

正常情况下会直接唤醒下家;但当下家不可唤醒时(被取消或表面上为null),则尝试从对尾反向寻找可用节点作为下家后,再尝试唤醒。

 

这样做的原因是,入队操作始终先变更new.prev指针,再变更tail.next指针,且不是原子操作,因此中间状态时next指针可能为空(即所谓“表面上为null”)。

 

通过观察入队的源码进一步说明问题:

 

   private Node enq(finalNode node) {

       for (;;) {

            Node t = tail;

            if(t == null) { // Must initialize

                if(compareAndSetHead(new Node()))

                    tail =head;

            } else {

                node.prev =t;

                if (compareAndSetTail(t,node)) {

                    t.next =node;

                    returnt;

                }

            }

       }

    }

 

注意尾节点tail跟头结点head都是dummy节点,类似地,前者始终指向队尾节点。



(图片引用自http://ifeve.com/introduce-abstractqueuedsynchronizer/)

 

tail==null时执行初始化,else块中是真正的入队操作:

1、           新入队节点的上家指向原队尾节点;

2、           尝试把尾节点指向新入队节点;

3、           原尾节点的下家指向新入队节点;

(注意t始终是原队尾节点)

 

小结

只要成功唤醒,线程便会重新进入无限for循环再次尝试获取资源。

 

共享获取&释放

掌握了前述内容后,会发现共享与排他在结构和内容上是雷同的。

来看看acquireShared()依赖的doAcquireShared()较之acquireQueued()有什么区别:

 

   private voiddoAcquireShared(int arg) {

       final Node node = addWaiter(Node.SHARED);

       boolean failed = true;

       try {

            booleaninterrupted =false;

            for(;;) {

                finalNode 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);

       }

    }

 

差异主要有二:

1、           入队的节点类型换成了共享,即Node.SHARED。

2、           setHead()换成了setHeadAndPropagate()。此处可以解释清楚节点状态-3PROPAGATE的用法。

 

   private void setHeadAndPropagate(Nodenode,int propagate) {

       Node h = head; // Record old head for check below

       setHead(node);

       /*

         * Try to signal next queued node if:

         *  Propagation was indicated by caller,

         *    or was recorded (as h.waitStatus) by a previous operation

         *    (note: this uses sign-check of waitStatus because

         *     PROPAGATE status may transition to SIGNAL.)

         * and

         *  The next node is waiting in shared mode,

         *    or we don't know, because it appears null

         *

         * The conservatism in both of thesechecks may cause

         * unnecessary wake-ups, but only whenthere are multiple

         * racing acquires/releases, so mostneed signals now or soon

         * anyway.

         */

       if (propagate > 0 || h ==null || h.waitStatus < 0) {

            Node s = node.next;

            if(s == null || s.isShared())

                doReleaseShared();

       }

}

 

 

满足if语句中的三个表达式之一,即可尝试传播(PROPAGATE),意即唤醒从队首开始一系列连续的共享节点,允许它们挨个进入临界区。

 

三个表达式作为准入条件貌似粗放,只要下家可能存在或已明示需要传播即可满足。例如参数propagate直接代入了tryAcquireShared(尝试共享获取)的返回值,后者与tryAcquire()(尝试排他获取)返回boolean表示成功与否所不同的是,它可以返回:

l  负数,表示获取失败;

l  0表示共享获取成功,但不允许后续共享节点继续获取;

l  正数则表示允许后续共享节点继续获取。

因此propagate > 0表示共享资源未达上限,后续共享节点可继续尝试获取。

 

而实现传播的关键是通过doReleaseShared(),该方并不释放资源,只是唤醒队首节点。然而队首节点被唤醒后立即尝试获取资源,成功后会再次触发setHeadAndPropagate(),形成多米诺骨牌的效果。注意,传播机制没有一次唤醒多个,而仅仅唤醒了队首的共享节点;节点被唤醒后也不保证能进入临界区,若获取失败(如共享资源数耗尽)可能再次入眠。

 

   private voiddoReleaseShared() {

       /*

         * Ensure that a release propagates,even if there are other

         * in-progress acquires/releases.  This proceeds in the usual

         * way of trying to unparkSuccessor ofhead if it needs

         * signal. But if it does not, statusis set to PROPAGATE to

         * ensure that upon release,propagation continues.

         * Additionally, we must loop in case anew node is added

         * while we are doing this. Also,unlike other uses of

         * unparkSuccessor, we need to know ifCAS to reset status

         * fails, if so rechecking.

         */

       for (;;) {

            Node h = head;

            if(h != null && h != tail) {

                intws = h.waitStatus;

                if(ws == Node.SIGNAL) {

                    if(!compareAndSetWaitStatus(h, Node.SIGNAL,0))

                        continue;           // loop to recheck cases

                    unparkSuccessor(h);

                }

                else if (ws == 0 &&

                         !compareAndSetWaitStatus(h,0, Node.PROPAGATE))

                    continue;               // loop on failed CAS

            }

            if(h == head)                   // loop if head changed

                break;

       }

   }

 

代码意图是,若队首节点存在,则唤醒它。ws == Node.SIGNAL 表示队首有节点在等候(入队节点会确保prev节点的状态为SIGNAL后入眠)

 

若无人排队,简单设头结点状态为PROPAGATE。

 

至此,SIGNAL特指有下家在排队待唤醒;而PROPAGATE表示当前无下家(无人排队),且新节点应立即尝试进入临界区。体会必胜客门前的指示牌“请在此排队”与“欢迎光临”的区别。0可以理解成没有指示牌。

 

回到Future Task

在理解了Sync和AQS之后,最后再来看看究竟FutureTask通过怎样的机制分别唤醒了处于阻塞状态的查询线程。

再次回顾,FutureTask定义的state承载着任务的运行状态:0(初始)代表ready;1代表running;2代表ran;4代表cancelled。

其临界区仅允许共享访问。通常任务开始后,临界区即“关门大吉”(伪装成临界区满),后续所有查询线程依次入队,等待任务结束或取消。

 

        protected int tryAcquireShared(int ignore) {

            return innerIsDone() ? 1: -1;

        }

细节是,每当查询线程尝试进入临界区,首先触发tryAcquireShared(),若任务未结束始终返回-1,表示共享获取失败,随即查询线程入队;否则返回1,表示共享获取成功,且允许后续线程继续获取。这样看来,共享访问的意义在于,当多个不同的查询线程进入队列,一旦任务结束,可径由“传播”的方式,迅速依次被唤醒。

 

查询线程被唤醒后,先检查自身的中断标志,并据此抛出中断异常。未被中断,则成功进入临界区、获取到共享资源(0,暗示忽略请求的资源数),退出了acquireSharedInterruptibly()内的无限for循环,于是检查任务的cancel状态,并据此抛出CancellationException。

 

        V innerGet() throwsInterruptedException, ExecutionException {

           acquireSharedInterruptibly(0);

            if(getState() ==CANCELLED)

                throw newCancellationException();

            if (exception !=null)

                throw newExecutionException(exception);

            return result;

        }

 

至此,文章开头的问题得出了答案。

相关文章推荐

Java多线程(七)之同步器基础:AQS框架深入分析

一、什么是同步器 多线程并发的执行,之间通过某种 共享 状态来同步,只有当状态满足 xxxx 条件,才能触发线程执行 xxxx 。 这个共同的语义可以称之为同步器。可以认为以上所有的锁...

AQS源码分析

前言:最近在学习AQS源码,学习过程中也查找过很多资料,但是大都只是翻译了源码的英文意思,并没有对一些难点做出分析和对AQS的队列结构进行讲解。所以我打算写下这篇文章,希望能帮助到有需要的人。 ...

AQS实现分析

下文将从实现角度分析AQS是如何完成线程同步,主要包括:同步队列、独占式同步状态获取与释放、共享式同步状态获取与释放、超时获取同步状态等AQS的核心数据结构模板方法。同步队列AQS依赖同步队列(一个F...
  • fjse51
  • fjse51
  • 2017年01月23日 17:40
  • 551

JAVA并发编程学习笔记之AQS源码分析(获取与释放)

同步状态 AQS采用的是CLH队列,CLH队列是由一个一个结点构成的,前面提到结点中有一个状态位,这个状态位与线程状态密切相关,这个状态位(waitStatus)是一个32位的整型常量,它的取值如下...

JAVA并发编程学习笔记之AQS源码分析(共享与互斥)(r)

共享模式与独占模式 AQL的内部队列采用的是CLH队列锁模型,CLH队列是由一个一个结点(Node)构成的。Node类中有两个常量SHARE和EXCLUSIVE,顾名思义这两个常量用于表示这个结点支...

JAVA并发编程学习笔记之AQS源码分析(共享与互斥)

共享模式与独占模式 AQL的内部队列采用的是CLH队列锁模型,CLH队列是由一个一个结点(Node)构成的。Node类中有两个常量SHARE和EXCLUSIVE,顾名思义这两个常量用于表示这个结点支...

【死磕Java并发】-----J.U.C之AQS:阻塞和唤醒线程

此篇博客所有源码均来自JDK 1.8 在线程获取同步状态时如果获取失败,则加入CLH同步队列,通过通过自旋的方式不断获取同步状态,但是在自旋的过程中则需要判断当前线程是否需要阻塞,其主要方法在acqu...
  • chenssy
  • chenssy
  • 2017年03月23日 21:41
  • 4840

Java并发编程-Lock和condition的原理及AQS的运用

AQS的全称为(AbstractQueuedSynchronizer),这个类也是在java.util.concurrent.locks下面。这个类似乎很不容易看懂,因为它仅仅是提供了一系列公共的方法...

JAVA并发编程: CAS和AQS

说起JAVA并发编程,就不得不聊聊CAS(Compare And Swap)和AQS了(AbstractQueuedSynchronizer)。 CAS(Compare And Swap),即比较并交...

JAVA CAS原理深度分析

看了一堆文章,终于把JAVA CAS的原理深入分析清楚了。 感谢GOOGLE强大的搜索,借此挖苦下百度,依靠百度什么都学习不到!   参考文档: http://www.blogjava.net...
  • Hsuxu
  • Hsuxu
  • 2013年07月25日 13:07
  • 102820
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:初步理解AQS
举报原因:
原因补充:

(最多只允许输入30个字)