AbstractQueuedSynchronizer简称AQS是Java大部分Lock、Semaphore、CountDownLatch等公共依赖框架,实现依赖于先进先出(FIFO)等待队列的阻塞锁。读懂它的代码原理有利我们去理解Java Lock衍生类原理,帮组我们开发自定义Lock。
主要原理
由上图所示,在队列内的元素都为执行线程,在队列头部head就是获取到独占锁的执行线程,其他线程都在队列中排队沉睡中,想要获取锁的线程会从tail中加入队列。当head释放锁了,会将next线程唤醒,去获取锁,成功获取后再将head指向next线程。这里主要简单说一下基本原理,具体怎么操作我们一起进入代码讲解吧。
Node内部类属性
1 | static final class Node { |
AbstractQueuedSynchronizer 内部属性
1 |
|
进入ReentrantLock,获取锁的时候,调用AQS那个方法
1 | public void lock() { |
Sync是ReentrantLock内部类,继承自AbstractQueuedSynchronizer,用于实现公平锁和非公平锁。
acquire
1 | public final void acquire(int arg) { |
主要流程先获取锁,如果失败了进入队列中排队。
acquireQueued
1 | final boolean acquireQueued(final Node node, int arg) { |
在acquireQueued
方法要么获取锁成功跳出循环,要么出现异常进入异常处理逻辑。
shouldParkAfterFailedAcquire
1 | private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { |
这个方法就两个作用,比较waitStatus或者设置,将node结点向前移动。
当p=head 相等,有可能是head还是空,队列还没有初始化成功,这个时候才一次去获取锁。还有就是node就head后置结点,或者node重入尝试获取锁,获取锁成功了,重新设置头结点。
cancelAcquire
1 | private void cancelAcquire(Node node) { |
这个方法主要任务就寻找状态合法前置结点,设置线程状态CANCELLED,退出线程锁竞争,找到后置线程,将前后指引关联起来,从队列中删除自己。但是有可能前置节点head,这时就需要唤醒后置节点,删除状态为CANCELLED节点,获取锁
unparkSuccessor
看下怎么唤醒线程的
1 | private void unparkSuccessor(Node node) { |
addWaiter
1 | private Node addWaiter(Node mode) { |
根据给定的模式去创建排队节点,mode只要分成两个模式Node.EXCLUSIVE
排他锁和Node.SHARED
共享锁。
结合上面三个方法分析,对获取锁失败有了些简单了解。线程使用tryAcquire
去获取锁,如果获取失败了,进入排队方法,此时先判断队列tail存在,已经存在直接尾插法插入,否则先初始化队列tail和head。这里知道队列初始化不是由先获取到锁去初始化的,而是由竞争失败的线程去创建队列,这样做是为了性能吧。acquire
会根据前置节点为head的情况下不断去获取锁,设置一个不间断的锁竞争趋势。然后将当前线程挂起,当线程被唤醒,第一个要去做的就是去获取锁成功,跳出acquire
循环。返回线程中断状态,只有当线程中断状态存在,再次调用线程中断。
本篇开头线程队列图片在有些情况下,并不是图片显示情况那样的。在队列刚开始初始化的时候,head节点并不是获取锁的节点,head只是随机创建Node对象,和tail都是同一个对象。当有线程从tail.next关联,进入队列,也简介和head建立起关系了,只要head最靠近的结点获取到锁后,将head指向自己才会往开篇队列图片方向走。
分析tryAcquire
tryAcquire是获取锁唯一实现,主要有子类实现。主要因为AQS是支持独占锁和共享锁、是否支持重入,更适合由子类去实现获取锁逻辑。进入ReentranLock了解公平锁和非公平锁如何实现tryAcquire
,这两种情况一起分析下。
非公平锁
1 | static final class NonfairSync extends Sync { |
非常简单,使用CAS(交换比较)最先设置成功线程,就算获取成功,在判断获取锁线程是否是占有锁线程,支持重入锁。
公平锁
1 | static final class FairSync extends Sync { |
主要区别多了一个hasQueuedPredecessors
判断,进入方法分析下
1 | public final boolean hasQueuedPredecessors() { |
先判断head节点是否初始化了,如果没有直接返回false。已经存在直接获取head节点后继节点,只有判断节点线程和当前线程不相等就返回true。这是为什么呢? 想一下锁开始释放的时候,唤醒下一个节点去获取锁,这时候有一个线程也去获取锁,有可能抢占了队列中排队节点获取到锁,相当于插队这是”不公平”的。
公共和不公平主要区别就是在获取锁的时候先判断队列中是否有排队线程,如果存在,直接获取失败,入列排队,强制保证排队最长队列最先获取到锁的原则。公平锁会比非公平锁多一点点性能消耗,但是影响不是很大的,在平常开发上使用公平锁也是一个不错选择,毕竟大家都是选择追求公平的😝。
释放锁
ReentranLock.unlock()的代码
1 | public void unlock() { |
release
1 | public final boolean release(int arg) { |
h.waitStatus = 0 就是状态还是默认状态,我们知道head状态是由shouldParkAfterFailedAcquire修改的,这时候线程还在自旋获取锁,不需要唤醒。
tryRelease
ReentranLock的tryRelease 是公平锁和非公平锁的释放锁的实现。
1 | protected final boolean tryRelease(int releases) { |
每次调用tryRelease(),只要不是非法线程都可以让state - releases,又不会唤醒线程,只要state=0了,才真正释放锁,设置占有线程为null。唤醒队列中等待线程。
一个ReentranLock获取锁释放锁流程就已经走完了,但是AbstractQueuedSynchronizer
中还有很多方法,本着解析这类来的,把其他功能也分析一下吧。
lockInterruptibly
lockInterruptibly
在获取锁的时候或者入列排队等待,线程可以被中断强制退出锁竞争。这个是ReentrantLock才有的功能,synchronized
关键字并不支持获取锁中断。
ReentrantLock 代码
1 | public void lockInterruptibly() throws InterruptedException { |
acquireInterruptibly
看一下acquireInterruptibly是怎么样的
1 | public final void acquireInterruptibly(int arg) |
看下入队线程怎么支持中断的
doAcquireInterruptibly
1 | private void doAcquireInterruptibly(int arg) |
获取锁流程和lock方法基本上一样,在获取锁之前会判断线程中断,并且去处理它。在入队的线程,线程已经被挂起是不能处理中断的,只要当线程被唤醒了,直接抛出异常,退出锁竞争。
获取锁超时
ReentrantLock 有个方法可以设置在指定时间内去获取锁,避免等待获取锁的时候,线程被一直堵塞下去。通过设置超时时间,在锁获取失败了可以让调用者自己去处理。直接进入代码讲解
1 | public boolean tryLock(long timeout, TimeUnit unit) |
看下 AQS怎么实现
1 | public final boolean tryAcquireNanos(int arg, long nanosTimeout) |
doAcquireNanos
1 | private boolean doAcquireNanos(int arg, long nanosTimeout) |
根据百度上描述纳秒,计算机执行一个执行一道指令(如将两数相加)约需2至4纳秒,太小时间间隔就没有意义了。这个方法比正常方法多了超时判断和超时唤醒线程,其他都一样。
ConditionObject
ConditionObject是AbstractQueuedSynchronizer内部类,实现Condition接口主要功能堵塞线程和唤醒堵塞,有点类似wait、notfiy、notifyAll,一般都是用在同步队列的生产者和消费者的线程堵塞唤醒。先解析Condition 接口每个方法含义,在具体分析方法实现。
Condition
1 | public interface Condition { |
在解析实现类之前,要先知道ConditionObject
内部属性
1 | public class ConditionObject implements Condition, java.io.Serializable { |
内部属性就两个,头结点、尾结点,下面开始分析实现方法。
await
1 | public final void await() throws InterruptedException { |
- 先判断线程是否存在中断,中断则直接抛出异常。
- 将Node加入有ConditionObject 维护的单向链表,这个列表主要用在队列唤醒,和获取锁的队列并不冲突。
- 释放锁,刚开始有点想不明白这里的,这个要结合同步队列来理解。当线程被挂起的时候必须要释放锁,不然变成死锁了。生产者获取不到锁,不能插入数据,就无法唤醒消费者。
- while 循环处理,只要node已经加入线程队列参与锁的竞争了,才会退出循环,或者先将当前线程挂起。当线程被唤醒了,判断线程是否因为中断而被唤醒,如果是就直接跳出while 循环。
- 因为已经加入队列,现在可以去获取锁 。
- 此时node结点已经获取到锁了waitStatus 已经产生变化了,需要清除单向链表中状态不合法结点包括它自己。
- 当interruptMode 不等于0,则说明有中断需要处理,需要调用者自己去处理。
addConditionWaiter
1 | private Node addConditionWaiter() { |
在ConditioinObject中会维护一个nextWaiter连起来的单向condition链表,这个链表主要特性就是,所以Node.waitStatus 必须是Node.CONDITION,会将链表头部结点、尾部结点放入lastWaiter、firstWaiter结点中。
unlinkCancelledWaiters
1 | private void unlinkCancelledWaiters() { |
遍历整个condition链表,将Node.waitStatus != Node.CONDITION删除。
isOnSyncQueue
1 | final boolean isOnSyncQueue(Node node) { |
checkInterruptWhileWaiting
1 | private int checkInterruptWhileWaiting(Node node) { |
检查线程中断,并且清理它。没有中断直接返回0,不会执行transferAfterCancelledWait
。中断了就会执行执行transferAfterCancelledWait
并且返回一个中断结构。
- REINTERRUPT 在等待退出时中断退出
- THROW_IE 抛出一个InterruptedException 异常退出
transferAfterCancelledWait
1
2
3
4
5
6
7
8
9
10final boolean transferAfterCancelledWait(Node node) {
if (node.compareAndSetWaitStatus(Node.CONDITION, 0)) { //waitStatus CONDITION => 0 设置成功
enq(node); //加入队列
return true;
}
while (!isOnSyncQueue(node)) //node 不在队列中
Thread.yield(); //线程让出执行权 等待node进入队列后 跳出自旋
return false;
}
根据之前获取锁的代码,加入队列结点waitStatus 都是默认值0,只要aitStatus CONDITION => 0 设置成功才能加入队列,参与锁竞争。设置失败就会自旋判断node 是否进入队列中,必须进入队列中才会退出这个方法。
reportInterruptAfterWait
1 | private void reportInterruptAfterWait(int interruptMode) |
根据不同中断情况,做出不同处理。
signal
1 | public final void signal() { |
看下唤醒线程如何实现的
doSignal
1 | private void doSignal(Node first) { |
transferForSignal
1 | final boolean transferForSignal(Node node) { |
这里唤醒线程的条件,是前置结点是否可用,只要当前置结点准备退出队列,或者已经被删除了。node被唤醒获取锁。
剩下几个方法,我不打算在写了,内容太多了。有兴趣自己去了解,内容都是差不多重复的。
总结
本篇通过获取锁释放锁原理去解析AbstractQueuedSynchronizer内部源码原理,也分析了公平锁和非公平锁的区别。讲解一些衍生功能的锁,如超时锁,中断锁,算是比较全部解析了AbstractQueuedSynchronizer作为Java ReentrantLock这类同位锁的框架支持,也简单分析了ConditionObject 实现线程协调signal、await方法。这篇文章大部分内容都是我自己思考想出来,如果有那些地方说错,请出来大家一起讨论。