深度解析Java8 – AbstractQueuedSynchronizer的实现分析(下),
深度解析Java8 – AbstractQueuedSynchronizer的实现分析(下),
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的构造方法:

.png)
![Image [1]](http://www.liuinsect.com/wp-content/uploads/2014/09/Image-1.png)
![Image [2]](http://www.liuinsect.com/wp-content/uploads/2014/09/Image-2.png)
.png)
![Image [3]](http://www.liuinsect.com/wp-content/uploads/2014/09/Image-3.png)
.png)
![Image [4]](http://www.liuinsect.com/wp-content/uploads/2014/09/Image-4.png)
![Image [5]](http://www.liuinsect.com/wp-content/uploads/2014/09/Image-5.png)
.png)
![Image [6]](http://www.liuinsect.com/wp-content/uploads/2014/09/Image-6.png)
.png)
.png)
![Image [7]](http://www.liuinsect.com/wp-content/uploads/2014/09/Image-7.png)
![Image [10] [1]](http://www.liuinsect.com/wp-content/uploads/2014/09/Image-10-1.png)
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方法:
.png)
![Image [8]](http://www.liuinsect.com/wp-content/uploads/2014/09/Image-8.png)
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队列中的其他节点也应能第一时间知道状态的变化。因此,一个节点获取到共享状态流程图是这样的:
![Image [9]](http://www.liuinsect.com/wp-content/uploads/2014/09/Image-9.png)
.png)
![Image [10] [1]](http://www.liuinsect.com/wp-content/uploads/2014/09/Image-10-1.png)
.png)
![Image [11]](http://www.liuinsect.com/wp-content/uploads/2014/09/Image-11.png)
![Image [12]](http://www.liuinsect.com/wp-content/uploads/2014/09/Image-12.png)
.png)
![Image [13]](http://www.liuinsect.com/wp-content/uploads/2014/09/Image-13.png)
.png)
![Image [14]](http://www.liuinsect.com/wp-content/uploads/2014/09/Image-14.png)
.png)
![Image [15]](http://www.liuinsect.com/wp-content/uploads/2014/09/Image-15.png)
.png)
![Image [16]](http://www.liuinsect.com/wp-content/uploads/2014/09/Image-16.png)
.png)
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对象对应的线程,实现请求锁线程的等待操作。待共享锁可以被获取后,从头节点开始,依次唤醒头节点及其以后的所有共享类型的节点。实现共享状态的传播。
这里有几点值得注意:
1. 与AQS的独占功能一样,共享锁是否可以被获取的判断为空方法,交由子类去实现。
2. 与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)包中的其他工具类,敬请期待。
用户点评