java5之后的java.util.concurrent包是世界级并发大师Doug Lea的作品,里面主要实现了
- atomic包里Integer/Long对应的原子类,主要基于CAS;
- 一些同步子,包括Lock,CountDownLatch,Semaphore,FutureTask等,这些都是基于AbstractQueuedSynchronizer类;
- 关于线程执行的Executors类等;
- 一些并发的集合类,比如ConcurrentHashMap,ConcurrentLinkedQueue,CopyOnWriteArrayList等。
今天我们主要介绍AbstractQueuedSynchronizer这个可以说是最核心的类,没有之一。整个concurrent包里,基本都直接或间接地用到了这个类。Doug Lea的这篇论文里面讲AQS的实现。
#AQS
首先,我们来想象一下,一间屋里有一个大家都想要得到的会让你很爽的东西(something which makes you so happy, e.g. W.C)。当有人进去把门关起来在独占享用的时候,其他人就只能在外面排队等待,既然在等待,你就不能老是去敲门说哎,好了没有啊。老是这样的话里面的人就很不爽了,而且你可以利用这点等待时间干点别的,比如看看小说视频背背单词或者就干脆椅子上睡觉,当前面独占的人爽完之后,就会出来说,啊,好爽,到你们了。然后大家可能按照排队顺序获取或者大家疯抢这个状态,有可能一个人自己进去独占,有可能几个人说,哎没关系,我们可以一起来。然后他们进去爽,爽完之后再出来通知下一个。
我们来把上面这段话翻译成AQS里面的术语。有一个状态state,会有多个Thread尝试获取,当一个Thread独占(EXCLUSIVE,比如Lock)之后,其他后面到来的Thread就会被放到一个Queue的队尾(tail),然后睡眠(park),一直等到前面的Thread唤醒(unpark)它,当然这里有可能被假唤醒(就好比你定了闹钟8点起床,结果7点就自然醒或者被外面车吵醒),所以这个Thread会判断一下是不是到自己了,没有的话就继续park(在一个死循环里);当拥有state的Thread释放(release)之后,它会唤醒Queue中的下一个Thread(unparkSuccessor)。然后下一个Thread获取(acquire)到state,完成自己的任务,然后继续unparkSuccessor。前面主要说的是EXCLUSIVE模式,AQS还支持共享(SHARED)模式,区别在于尝试获取(tryAcquireShared)的时候即使之前已经有Thread获取了state,但是可能仍然能获取(比如ReadLock)。同样释放(doReleaseShared)的时候除了通知Queue里面第一个(head),还会继续通知后续的节点(Node),只要它们是SHARED。
AQS就是实现了:
- 自动管理这个同步状态state(int类型),更新的时候需要用CAS保证原子性
- 阻塞和唤醒线程park/unpark
- 队列管理,一个双向链表实现queue
AQS是一个abstract class,可以通过继承AQS,定义state的含义,以及tryAcquire,tryRelease,以及对应的share模式下tryAcquireShared,tryReleaseShared这几个方法,定义出自己想要的同步子(Synchronizers)。一般而言,是定义一个内部类Sync extends AQS,实现前面说的几个方法,然后再包一层,暴露出相应的方法。这样做的好处是你可以在包装器类里面取更直观的名字,如ReentrantLock里的lock,unlock和CountDownLatch里的countDown,await,而不是太通用的acquire和release等。而且AQS里面一些方法是为了监控和调试使用,直接暴露出来也不好。
下面我们来看J.U.C里面两个常用的Synchronizers。
#ReentrantLock
##使用
ReentrantLock的语义跟synchronized关键字基本一样,而且我之前看《深入理解Java虚拟机》里面的评测说JDK6之后,两者的效率基本一致了(JDK5之前ReentrantLock要比synchronized快很多)。Javadoc里面说基本用法如下:
##源码
ReentrantLock用state表示是否被锁,0表示没有线程获取到锁,>=1表示某个线程获取了N次锁(因为是重入的,只要保证lock和unlock成对出现就没有问题)。
定义了一个内部类,基本任务都代理给sync完成。而Sync又是一个abstract class,这里主要是因为实现了两种抢占锁的机制,公平锁和非公平锁。
所谓公平不公平简单来说就是本文开头说的,当资源释放的时候,大家是按照排队顺序先到先得,还是有人插队大家疯抢。
提供了两个构造函数:
加锁的实现
简单代理给了sync,在FairSync里为
acquire的实现在AQS里面:
tryAcquire是要在子类里自己实现的,在FairSync如下;
如果获取失败,addWaiter(Node.EXCLUSIVE)将当前线程加入队尾
现在我们已经将获取不到锁的线程加入队尾了,现在要将它挂起acquireQueued(addWaiter(Node.EXCLUSIVE), arg)):
上面完成了获取锁的过程,简单来说就是尝试获取,失败就加入队尾,挂起,等待被唤醒。
下面来看看释放锁
看看需要在子类里实现的tryRelease:
到这里,基本都已经完成,对了,还没有说非公平锁NonfairSync是怎么抢占锁的。
跟FairSync.lock()对比,可以看出,只是在acquire(1)之前,先抢一把,抢不到才乖乖的去排队。
我们再看看NonfairSync.tryAcquire()怎么实现的
#CountDownLatch
我们之前说了,AQS支持独占EXCLUSIVE和共享SHARED两种模式,而刚刚的ReentrantLock的就是独占模式,我们来看看一个使用共享模式的类。
##使用
CountDownLatch就好比一道门,它可以用来等所有资源都到齐了,才开门,让这些线程同时通过。比如如下是CountDownLatch一个通用用法:
对了,上面代码是拿来验证volatile不具备原子性的,是错误的代码哦。如果想并发安全,大家可以想想可以用哪些方式实现。
##源码
CountDownLatch同样也是定义了一个继承自AQS的内部类Sync:
构造函数如下:
count表示有多少个任务还在运行,每个Thread完成了任务或者准备好开始之前,就会调用countDown方法将count-1,当count==0时候,await就不再阻塞,所有在上面阻塞的Thread都可以顺利通过。
直接调用AQS的acquireSharedInterruptibly方法,从方法名可以看出,支持中断响应
tryAcquireShared在子类中实现:
如果没有获取到,将Thread加入队尾,挂起。下面这个方法跟独占模式下acquireQueued(addWaiter(Node.EXCLUSIVE), arg))这个方法代码是基本一致的。
前面完成了等待CountDownLatch的count变成0的过程,下面看看countDown
unparkSuccessor跟之前独占模式里面的是同一个函数,即调用unpark唤醒Thread。
我们知道为了避免获取不到锁长时间等待,一般阻塞的方法都会支持带超时时间的方法,比如CountDownLatch里就有
调用AQS里面的tryAcquireSharedNanos方法
可以看到,跟不带超时的doAcquireSharedInterruptibly方法相比,区别主要在于每次for循环期间,检查时间是否过期和调用带超时的park。nanosTimeout > spinForTimeoutThreshold这个判断主要是因为park/unpark本身也需要花时间,为了更准确地完成超时的机制,在超时时间马上就要到了的时候,就进入自旋,不再park了,这应该是Doug Lea测试了park/unpark时间比1000纳秒要长吧。
#总结
J.U.C里AQS是一个相当核心的类,可以说没有它就没有J.U.C包。推荐大家看看AQS这篇论文(网上有一些翻译,推荐大家还是看原文吧)。主要是用一个state表示状态,子类可以根据需要来定义state的含义,以及获取释放资源时具体如何操作state,当然需要通过CAS实现原子更改。当获取不到state的时候,线程加入队列,挂起。释放之后,唤醒队列中的线程。AQS支持两种模式,独占EXCLUSIVE和共享SHARED。J.U.C里本身也有很多直接继承AQS实现的类,包括Lock,CountDownLatch,Semaphore,FutureTask等,如果这些还不能满足你的使用,那么可以直接继承AQS来实现需要。
#Refers
- http://gee.cs.oswego.edu/dl/papers/aqs.pdf
- http://ifeve.com/introduce-abstractqueuedsynchronizer/
- http://ifeve.com/jdk1-8-abstractqueuedsynchronizer/
- http://ifeve.com/jdk1-8-abstractqueuedsynchronizer-part2/
- http://book.douban.com/subject/6522893/
- http://my.oschina.net/magicly007/blog/364102
Written with StackEdit.