1. 背景
最近团队内部技术分享,我做了个关于AQS的分享。ppt中涵盖的部分要点内容,现在整理到博客上。
关于AQS本身的源码解读,可以参考我之前的博文。
2. 要点梳理
下面是一些技术分享的要点梳理。
2.1 LockSupport的实现
AQS中的阻塞/唤醒最终是基于LockSupport的park/unpark实现的。那么park和unpark又是怎么实现的呢?
对于Mac OS,我们主要调试os_bsd.cpp,以上截图取自os_bsd.cpp
我们通过调试JVM可以看到LockSupport中调用的UNSAFE#park最终会通过调用pthread_cond_wait实现阻塞。
而唤醒是通过pthread_cond_signal实现的。
在JVM中每个线程有个自己的Parker类,对一个线程进行阻塞/唤醒操作需要获取到线程Parker对应的互斥锁,Parker内部有个计数器,取值为0和1,调用unpark会置计数器为1.
因此在Java程序中,如果先unpark线程A,再park线程A,线程不会阻塞;但是如果unpark两次,再park两次,线程会阻塞。
2.2 Lock语义实现
java.util.concurrent.locks.lock接口写清了锁的实现必须保证的内存语义。
我们举一个例子:
以上代码截图自java.util.concurrent.ArrayBlockingQueue
JUC中的ArrayBlockingQueue是基于单锁保护的阻塞队列,其中一些关键的共享变量并没有使用volatile关键字,原因是ArrayBlockingQueue的操作使用了同一把ReentrantLock来保护,因为Lock的内存语义保证,ArrayBlockingQueue中的这些共享变量不需要使用volatile保证可见性。
那么就ReentrantLock为例,它是如何实现的呢?我们知道ReentrantLock内部组合了一个Sync类,Sync类继承了AQS,ReentrantLock将Lock本身的API委托给Sync(子类)处理。
其奥秘就是AQS中的state变量,它被volatile修饰。
AQS#setState: 具有volatile写语义,AQS#getState: 具有volatile读语义,而AQS#compareAndSetState: 具有volatile读与写语义。
我们以非公平锁ReentrantLock#NonfairSync为例:
这是lock方法的实现,我们可以看到
如果线程通过CAS state进入临界区,那么在进入临界区前的一步操作便是一个具有volatile读写语义的操作。
如上展示的是另一种获取独占锁进入临界区的路径。
可以看到仍然是一个具有volatile读写语义的操作。
那么以释放锁为例:
在出临界区的过程中使用了setState,这是一个volatile写语义的操作。
我们可以看到整个独占锁的获取和释放都被一对volatile读和volatile写语义的操作包着。
根据happens-before传递性,线程A释放锁,线程B获得锁后,线程A所有可见的共享变量,对于线程B都可见。
可以说ReentrantLock对Lock内存语义的实现便是基于对AQS的volatile state的操作
2.3 共享锁中PROPAGATE状态的意义
市面上的书籍或者网上的资料基本都对节点的PROPAGATE状态的意义简略带过。
而实际上在AQS中共享模式下的唤醒,仅仅依赖tryAcquireShared返回的int值是不够的,此状态拓展了释放后继线程的条件,保证共享模式下线程唤醒行为传播下去。
可以翻翻Doug Lea网站上当时的代码提交记录,可以看到此状态的引入是为了修复一个在共享锁并发释放情况下潜在的线程hang住问题。此问题更多的细节,可以参考我之前的博文。
2.4 一些重要的断言
- head节点绝对不会是CANCELLED
- 一个节点的前驱为head,不代表锁正被占用,而是代表当前节点有可能可以成功获取到锁
- 一个节点的后继为null,不代表这个节点就是tail
- tryRelease的语义是在完全释放独占锁时才返回true
2.5 如何调试AQS
想不明白的地方想改代码调试,怎么办?
拷出来改,然后再拷个ReentrantLock、Semaphore啥的调调吧
注意Unsafe这个东西用户代码直接拿实例会出SecurityException,需要改一下拷出来的AQS代码,用反射拿Unsafe。
2.6 如何琢磨AQS
- 琢磨并发程序设计需要有一定时空想象力,把自己大脑当成一个线程调度器吧。
- Doug Lea老爷子的网站上宝贝很多
- 网上文章质量参差不齐,关键要有自己理解