AbstractQueuedSynchronizer的实现阐明(下)
副标题#e#
媒介
颠末本系列的上半部门JDK1.8 AbstractQueuedSynchronizer的实现阐明(上)的解读,相信许多读者已经对AbstractQueuedSynchronizer(下文简称AQS)的独有成果了然于胸,那么这次我们通过对另一个东西类:CountDownLatch的阐明来解读AQS的别的一个成果:共享成果。
AQS共享成果的实现
在开始解读AQS的共享成果前,我们再重温一下CountDownLatch,CountDownLatch为java.util.concurrent包下的计数器东西类,常被用在多线程情况下,它在初始时需要指定一个计数器的巨细,然后可被多个线程并发的实现减1操纵,并在计数器为0后挪用await要领的线程被叫醒,从而实现多线程间的协作。它在多线程情况下的根基利用方法为:
//main thread // 新建一个CountDownLatch,并指拟定一个初始巨细 CountDownLatch countDownLatch = new CountDownLatch(3); // 挪用await要领后,main线程将阻塞在这里,直到countDownLatch 中的计数为0 countDownLatch.await(); System.out.println("over"); //thread1 // do something //........... //挪用countDown要领,将计数减1 countDownLatch.countDown(); //thread2 // do something //........... //挪用countDown要领,将计数减1 countDownLatch.countDown(); //thread3 // do something //........... //挪用countDown要领,将计数减1 countDownLatch.countDown();
留意,线程thread 1,2,3各自挪用 countDown后,countDownLatch 的计数为0,await要领返回,节制台输入“over”,在此之前main thread 会一直甜睡。
可以看到CountDownLatch的浸染雷同于一个“栏栅”,在CountDownLatch的计数为0前,挪用await要领的线程将一直阻塞,直到CountDownLatch计数为0,await要领才会返回,
而CountDownLatch的countDown()要领例一般由各个线程挪用,实现CountDownLatch计数的减1。
知道了CountDownLatch的根基利用方法,我们就从上述DEMO的第一行new CountDownLatch(3)开始,看看CountDownLatch是怎么实现的。
首先,看下CountDownLatch的结构要领:
和ReentrantLock雷同,CountDownLatch内部也有一个叫做Sync的内部类,同样也是用它担任了AQS。
再看下Sync:
假如你看过本系列的上半部门,你对setState要领必然不会生疏,它是AQS的一个“状态位”,在差异的场景下,代表差异的寄义,好比在ReentrantLock中,暗示加锁的次数,在CountDownLatch中,则暗示CountDownLatch的计数器的初始巨细。
#p#副标题#e#
配置完计数器巨细后CountDownLatch的结构要领返回,下面我们再看下CountDownLatch的await()要领:
挪用了Sync的acquireSharedInterruptibly要领,因为Sync是AQS子类的原因,这里其实是直接挪用了AQS的acquireSharedInterruptibly要领:
从要领名上看,这个要领的挪用是响应线程的打断的,所以在前两行会查抄下线程是否被打断。接着,实验着获取共享锁,小于0,暗示获取失败,通过本系列的上半部门的解读, 我们知道AQS在获取锁的思路是,先实验直接获取锁,假如失败会将当前线程放在行列中,凭据FIFO的原则期待锁。而对付共享锁也是这个思路,假如和独有锁一致,这里的tryAcquireShared应该是个空要领,留给子类去判定:
再看看CountDownLatch:
假如state酿成0了,则返回1,暗示获取乐成,不然返回-1则暗示获取失败。
看到这里,读者大概会发明, await要领的获取方法更像是在获取一个独有锁,那为什么这里还会用tryAcquireShared呢?
追念下CountDownLatch的await要领是不是只能在主线程中挪用?谜底是否认的,CountDownLatch的await要领可以在多个线程中挪用,当CountDownLatch的计数器为0后,挪用await的要领城市依次返回。 也就是说可以多个线程同时在期待await要领返回,所以它被设计成了实现tryAcquireShared要领,获取的是一个共享锁,锁在所有挪用await要领的线程间共享,所以叫共享锁。
回到acquireSharedInterruptibly要领:
#p#分页标题#e#
假如获取共享锁失败(返回了-1,说明state不为0,也就是CountDownLatch的计数器还不为0),进入挪用doAcquireSharedInterruptibly要领中,凭据我们上述的意料,应该是要将当前线程放入到行列中去。
在这之前,我们再回首一下AQS行列的数据布局:AQS是一个双向链表,通过节点中的next,pre变量别离指向当前节点后一个节点和前一个节点。个中,每个节点中都包括了一个线程和一个范例变量:暗示当前节点是独有节点照旧共享节点,头节点中的线程为正在占有锁的线程,尔后的所有节点的线程暗示为正在期待获取锁的线程。如下图所示:
黄色节点为头节点,暗示正在获取锁的节点,剩下的蓝色节点(Node1、Node2、Node3)为正在期待锁的节点,他们通过各自的next、pre变量别离指向前后节点,形成了AQS中的双向链表。每个线程被加上范例(共享照旧独有)后即是一个Node, 也就是本文中说的节点。
再看看doAcquireSharedInterruptibly要领:
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException { final Node node = addWaiter(Node.SHARED); //将当前线程包装为范例为Node.SHARED的节点,标示这是一个共享节点。 boolean failed = true; try { for (;;) { final Node p = node.predecessor(); if (p == head) { //假如新建节点的前一个节点,就是Head,说明当前节点是AQS行列中期待获取锁的第一个节点, //凭据FIFO的原则,可以直接实验获取锁。 int r = tryAcquireShared(arg); if (r >= 0) { setHeadAndPropagate(node, r); //获取乐成,需要将当前节点配置为AQS行列中的第一个节点,这是AQS的法则//行列的头节点暗示正在获取锁的节点 p.next = null; // help GC failed = false; return; } } if (shouldParkAfterFailedAcquire(p, node) && //查抄下是否需要将当前节点挂起 parkAndCheckInterrupt()) throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } }
这里有几点需要说明的:
1. setHeadAndPropagate要领:
首先,利用了CAS改换了头节点,然后,将当前节点的下一个节点取出来,假如同样是“shared”范例的,再做一个"releaseShared"操纵。
看下doReleaseShared要领:
for (;;) { Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) //假如当前节点是SIGNAL意味着,它正在期待一个信号, //可能说,它在期待被叫醒,因此做两件事,1是重置waitStatus符号位,2是重置乐成后,叫醒下一个节点。 continue; // loop to recheck cases unparkSuccessor(h); } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) //假如自己头节点的waitStatus是出于重置状态(waitStatus==0)的,将其配置为“流传”状态。 //意味着需要将状态向后一个节点流传。 continue; // loop on failed CAS } if (h == head) // loop if head changed break; }
为什么要这么做呢?这就是共享成果和独有成果最纷歧样的处所,对付独有成果来说,有且只有一个线程(凡是只对应一个节点,拿ReentantLock举例,假如当前持有锁的线程反复挪用lock()要领,那按照本系列上半部门我们的先容,我们知道,会被包装成多个节点在AQS的行列中,所以用一个线程来描写更精确),可以或许获取锁,可是对付共享成果来说。
共享的状态是可以被共享的,也就是意味着其他AQS行列中的其他节点也应能第一时间知道状态的变革。因此,一个节点获取到共享状态流程图是这样的:
好比此刻有如下行列:
当Node1挪用tryAcquireShared乐成后,改换了头节点:
Node1酿成了头节点然后挪用unparkSuccessor()要领叫醒了Node2、Node2中持有的线程A出于上面流程图的park node的位置,
#p#分页标题#e#
线程A被叫醒后,反复黄色线条的流程,从头查抄挪用tryAcquireShared要领,看可否乐成,假如乐成,则又变动头节点,反复以上步调,以实现节点自身获取共享锁乐成后,叫醒下一个共享范例节点的操纵,实现共享状态的向后通报。
2.其实对付doAcquireShared要领,AQS还提供了会合雷同的实现:
别离对应了:
带参数请求共享锁。 (忽略间断)
带参数请求共享锁,且响应间断。(每次轮回时,会查抄当前线程的间断状态,以实现对线程间断的响应)
带参数请求共享锁可是限制期待时间。(第二个参数配置超时时间,超出时间后,要领返回。)
较量出格的为最后一个doAcquireSharedNanos要领,我们一起看下它怎么实现超时时间的节制的。
因为该要领和其余获取共享锁的要领逻辑是雷同的,我用赤色框圈出了它所纷歧样的处所,也就是实现超时时间节制的处所。
可以看到,其实就是在进入要领时,计较出了一个“deadline”,每次轮回的时候用当前时间和“deadline”较量,大于“dealine”说明超时时间已到,直接返回要领。
留意,最后一个红框中的这行代码:
nanosTimeout > spinForTimeoutThreshold
从变量的字面意思可知,这是拿超时时间和超时自旋的最小作较量,在这里Doug Lea把超时自旋的阈值配置成了1000ns,即只有超时时间大于1000ns才会去挂起线程,不然,再次轮回,以实现“自旋”操纵。这是“自旋”在AQS中的应用之处。
看完await要领,我们再来看下countDown()要领:
挪用了AQS的releaseShared要领,并传入了参数1:
同样先实验去释放锁,tryReleaseShared同样为空要领,留给子类本身去实现,以下是CountDownLatch的内部类Sync的实现:
死轮回更新state的值,实现state的减1操纵,之所以用死轮回是为了确保state值的更新乐成。
从上文的阐明中可知,假如state的值为0,在CountDownLatch中意味:所有的子线程已经执行完毕,这个时候可以叫醒挪用await()要领的线程了,而这些线程正在AQS的行列中,并被挂起的,
所以下一步应该去叫醒AQS行列中的头节点了(AQS的行列为FIFO行列),然后由头节点去依次叫醒AQS行列中的其他共享节点。
假如tryReleaseShared返回true,进入doReleaseShared()要领:
private void doReleaseShared() { for (;;) { Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) //假如当前节点是SIGNAL意味着,它正在期待一个信号, //可能说,它在期待被叫醒,因此做两件事,1是重置waitStatus符号位,2是重置乐成后,叫醒下一个节点。 continue; // loop to recheck cases unparkSuccessor(h); } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) //假如自己头节点的waitStatus是出于重置状态(waitStatus==0)的,将其配置为“流传”状态。 //意味着需要将状态向后一个节点流传。 continue; // loop on failed CAS } if (h == head) // loop if head changed break; } }
当线程被叫醒后,会从头实验获取共享锁,而对付CountDownLatch线程获取共享锁判定依据是state是否为0,而这个时候显然state已经酿成了0,因此可以顺利获取共享锁而且依次叫醒AQS队里中后头的节点及对应的线程。
总结
本文从CountDownLatch入手,深入阐明白AQS关于共享锁方面的实现方法:
假如获取共享锁失败后,将请求共享锁的线程封装成Node工具放入AQS的行列中,并挂起Node工具对应的线程,实现请求锁线程的期待操纵。待共享锁可以被获取后,从新节点开始,依次叫醒头节点及其今后的所有共享范例的节点。实现共享状态的流传。
这里有几点值得留意:
与AQS的独有成果一样,共享锁是否可以被获取的判定为空要领,交由子类去实现。
#p#分页标题#e#
与AQS的独有成果差异,当锁被头节点获取后,独有成果是只有头节点获取锁,其余节点的线程继承甜睡,期待锁被释放后,才会叫醒下一个节点的线程,而共享成果是只要头节点获取锁乐成,就在叫醒自身节点对应的线程的同时,继承叫醒AQS行列中的下一个节点的线程,每个节点在叫醒自身的同时还会叫醒下一个节点对应的线程,以实现共享状态的“向后流传”,从而实现共享成果。
以上的阐明都是从AQS子类的角度去对待AQS的部门成果的,而假如直接对待AQS,或者可以这么去解读:
首先,AQS并不体贴“是什么锁”,对付AQS来说它只是实现了一系列的用于判定“资源”是否可以会见的API,而且封装了在“会见资源”受限时将请求会见的线程的插手行列、挂起、叫醒等操纵, AQS只体贴“资源不行以会见时,怎么处理惩罚?”、“资源是可以被同时会见,照旧在同一时间只能被一个线程会见?”、“假如有线程等不及资源了,怎么从AQS的行列中退出?”等一系列环绕资源会见的问题,而至于“资源是否可以被会见?”这个问题则交给AQS的子类去实现。
当AQS的子类是实现独有成果时,譬喻ReentrantLock,“资源是否可以被会见”被界说为只要AQS的state变量不为0,而且持有锁的线程不是当前线程,则代表资源不能会见。
当AQS的子类是实现共享成果时,譬喻:CountDownLatch,“资源是否可以被会见”被界说为只要AQS的state变量不为0,说明资源不能会见。
这是典范的将法则和操纵分隔的设计思路:法则子类界说,操纵逻辑因为具有公用性,放在父类中去封装。
虽然,正式因为AQS只是体贴“资源在什么条件下可被会见”,所以子类还可以同时利用AQS的共享成果和独有成果的API以实现更为巨大的成果。
好比:ReentrantReadWriteLock,我们知道ReentrantReadWriteLock的中也有一个叫Sync的内部类担任了AQS,而AQS的行列可以同时存放共享锁和独有锁,对付ReentrantReadWriteLock来说别离代表读锁和写锁,当行列中的头节点为读锁时,代表读操纵可以执行,而写操纵不能执行,因此请求写操纵的线程会被挂起,当读操纵依次推出后,写锁成为头节点,请求写操纵的线程被叫醒,可以执行写操纵,而此时的读请求将被封装成Node放入AQS的行列中。如此来去,实现读写锁的读写瓜代举办。
而本系列文章上半部门提到的FutureTask,其实思路也是:封装一个存放线程执行功效的变量A,利用AQS的独有API实现线程对变量A的独有会见,判定法则是,线程没有执行完毕:call()要领没有返回前,不能会见变量A,可能是超时时间没到前不能会见变量A(这就是FutureTask的get要领可以实现获取线程执行功效时,配置超时时间的原因)。
综上所述,本系列文章从AQS独有锁和共享锁两个方面深入阐明白AQS的实现方法和奇特的设计思路,但愿对读者有开导,下一篇文章,我们将继承JDK 1.8下 J.U.C (java.util.concurrent)包中的其他东西类,敬请等候。