1.介绍AQS
AbstractQueuedSynchronizer (抽象队列同步器,以下简称 AQS)出现在 JDK 1.5 中,AQS 这个东西在Java的并发中是很重要的一部分,因为他是很多同步器的基础框架,比如 ReentrantLock、CountDownLatch 和 Semaphore 等等都是基于 AQS 实现的。基于AQS来构建同步器可以带来很多好处。它不仅能够极大地减少实现工作,而且也不必处理在多个位置上发生的竞争问题。
在基于AQS构建的同步器中,只能在一个时刻发生阻塞,从而降低上下文切换的开销,提高了吞吐量。同时在设计AQS时充分考虑了可伸缩行,因此J.U.C中所有基于AQS构建的同步器均可以获得这个优势。
AQS的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。
AQS使用一个int类型的成员变量state来表示同步状态,当state>0时表示已经获取了锁,当state = 0时表示释放了锁。它提供了三个方法(getState()
、setState(int newState)
、compareAndSetState(int expect,int update)
)来对同步状态state进行操作,当然AQS可以确保对state的操作是安全的。
AQS通过内置的FIFO同步队列(这个会重点分析一下)来完成资源获取线程的排队工作,如果当前线程获取同步状态失败(锁)时,AQS则会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。
一句话:AQS使用一个Volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作,通过CAS完成对State值的修改。
这里列出了AQS主要提供的一些方法,方便快速定位:
getState()
:返回同步状态的当前值;setState(int newState)
:设置当前同步状态;compareAndSetState(int expect, int update)
:使用CAS设置当前状态,该方法能够保证状态设置的原子性;tryAcquire(int arg)
:独占式获取同步状态,获取同步状态成功后,其他线程需要等待该线程释放同步状态才能获取同步状态;tryRelease(int arg)
:独占式释放同步状态;tryAcquireShared(int arg)
:共享式获取同步状态,返回值大于等于0则表示获取成功,否则获取失败;tryReleaseShared(int arg)
:共享式释放同步状态;isHeldExclusively()
:当前同步器是否在独占式模式下被线程占用,一般该方法表示是否被当前线程所独占;acquire(int arg)
:独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待,该方法将会调用可重写的tryAcquire(int arg)方法;acquireInterruptibly(int arg)
:与acquire(int arg)相同,但是该方法响应中断,当前线程为获取到同步状态而进入到同步队列中,如果当前线程被中断,则该方法会抛出InterruptedException异常并返回;tryAcquireNanos(int arg,long nanos)
:超时获取同步状态,如果当前线程在nanos时间内没有获取到同步状态,那么将会返回false,已经获取则返回true;acquireShared(int arg)
:共享式获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式的主要区别是在同一时刻可以有多个线程获取到同步状态;acquireSharedInterruptibly(int arg)
:共享式获取同步状态,响应中断;tryAcquireSharedNanos(int arg, long nanosTimeout)
:共享式获取同步状态,增加超时限制;release(int arg)
:独占式释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒;releaseShared(int arg)
:共享式释放同步状态;
以上这些方法在下面的源码分析中都会有所涉及,这个AQS框架还是有些代码量挺复杂的,好好理解的话对于后面的其他的一些锁相关机制、通信工具类都是很有帮助的,内功心法都学会了,其他的就简单很多了。
大体整个框架图(图出自美团):
2.AQS理论的数据结构
AQS类中维护了一个双向链表(FIFO队列), 这个队列也称CLH同步队列,有什么用呢?AQS就是靠这个队列来完成同步状态的管理的!
怎么进行管理的?这个问题问得好,大概流程就是这样:当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。
Node源码如下:
1 | static final class Node { |
线程两种锁的模式:
模式 | 含义 |
---|---|
SHARED | 表示线程以共享的模式等待锁 |
EXCLUSIVE | 表示线程正在以独占的方式等待锁 |
waitStatus有下面几个枚举值:
枚举 | 含义 |
---|---|
0 | 当一个Node被初始化的时候的默认值 |
CANCELLED | 为1,表示线程获取锁的请求已经取消了 |
CONDITION | 为-2,表示节点在等待队列中,节点线程等待唤醒 |
PROPAGATE | 为-3,当前线程处在SHARED情况下,该字段才会使用 |
SIGNAL | 为-1,表示线程已经准备好了,就等资源释放了 |
Node的内部其实是这样的:
如上图所示,Node 的数据结构其实也挺简单的,就是 thread + waitStatus + pre + next 四个属性而已。
而AQS还有哪些属性呢?如下所示:
1 | // 头结点,可以理解为:当前持有锁的线程 |
再抽象一点就是这样了,使用图示的话就如下可以很清晰的表达了(图源文末参考文章),不过需要注意的是:阻塞队列不包含 head 节点。(如上图所示)
解释一下head:head是队列中标志,用于指示下一个被unpack的node,head来源于初始化的或曾取得过锁的node。
AbstractQueuedSynchronizer 的等待队列示意如下所示:
关于双向队列的入队操作和出队操作这些应该比较容易理解,这里就不再讲了。
3.源码分析
AQS同时提供了互斥模式(exclusive)和共享模式(shared)两种不同的同步逻辑。一般情况下,子类只需要根据需求实现其中一种模式,当然也有同时实现两种模式的同步类,如ReadWriteLock
。接下来将详细介绍这两种模式。
3.1 独占模式
3.1.1 独占式同步状态获取:acquire
独占式获取同步状态时通过 acquire 进行的,他是AQS提供的模板方法,该方法为独占式获取同步状态。但是该方法对中断不敏感,也就是说由于线程获取同步状态失败加入到CLH同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移除。
acquire的主要完成的事情是这样的:
- 获取独占锁,对中断不敏感。
- 首先尝试获取一次锁,如果成功,则返回,就结束了!!!
- 否则会把当前线程包装成Node插入到队列中,在队列中会检测是否为head的直接后继,并尝试获取锁
- 如果获取失败,则会通过LockSupport阻塞当前线程,直至被释放锁的线程唤醒或者被中断,随后再次尝试获取锁,如此反复。
其源码如下:
1 | public final void acquire(int arg) { |
tryAcquire(int)
上面提到的tryAcquire
方法, tryAcquire尝试以独占的方式获取资源,如果获取成功,则直接返回true,否则直接返回false。该方法可以用于实现Lock中的tryLock()方法。而AQS中并没有实现上面的tryAcquire(arg)
方法,当你跟进去的时候会发现,只是抛出一个异常而已,该方法自定义同步组件自己实现,该方法必须要保证线程安全的获取同步状态。AQS在这里只负责定义了一个公共的方法框架。
这里之所以没有定义成abstract,是因为独占模式下只用实现tryAcquire-tryRelease,而共享模式下只用实现tryAcquireShared-tryReleaseShared。如果都定义成abstract,那么每个模式也要去实现另一模式下的接口。
其代码如下所示:
1 | /* |
而具体获取锁的操作需要由其子类进行实现,比如ReentrantLock中的Sync实现,如下:
1 | /** |
这里你会发现上面有重入锁的概念,意思就是已经获取到锁的线程还可以再次获取到同一个锁,这里多嘴一下,有哪些锁是重入锁呢?比如:syschronized、ReentrantLock都属于重入锁,而自旋锁不属于重入锁。
addWaiter(Node)
假设tryAcquire(arg)
返回false,那么代码将执行:acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
,这个方法,首先需要执行:addWaiter(Node.EXCLUSIVE)
该方法用于将当前线程根据不同的模式(Node.EXCLUSIVE
互斥模式、Node.SHARED
共享模式)加入到等待队列的队尾,并返回当前线程所在的结点。其添加过程是一个自旋过程,会去尝试能否添加到尾结点,如果队列为空会进行同步队列的初始化。需要注意的是,这里取消了快速尝试这个方法,addWaiter
直接就自旋了,所以在这里是没有enq(node);
这个方法的。
源码如下:
1 | private Node addWaiter(Node mode) { |
initializeSyncQueue就是一个初始化同步队列的方法,其源码如下:
1 | /** |
acquireQueued(Node, int)
返回acquire
方法中,参数node,经过addWaiter(Node.EXCLUSIVE),此时已经进入阻塞队列,注意一下:如果acquireQueued(addWaiter(Node.EXCLUSIVE), arg))返回true的话,意味着上面这段代码将进入selfInterrupt()
,所以正常情况下,下面应该返回false。
这个方法非常重要,应该说真正的线程挂起,然后被唤醒后去获取锁,都在这个方法里了!!!
acquireQueued
方法为一个自旋的过程,也就是说当前线程(Node)进入同步队列后,就会进入一个自旋的过程,每个节点都会自省地观察,当条件满足,获取到同步状态后,就可以从这个自旋过程中退出,否则会一直执行下去。如下:
1 | final boolean acquireQueued(final Node node, int arg) { |
从上面代码中可以看到,当前线程会一直尝试获取同步状态,当然前提是只有其前驱节点为头结点才能够尝试获取同步状态,理由:
- 保持FIFO同步队列原则。
- 头节点释放同步状态后,将会唤醒其后继节点,后继节点被唤醒后需要检查自己是否为头节点。
上面还有一点,就是当获取同步状态失败后,线程并不是立马进行阻塞,需要检查该线程的状态,检查状态的方法为 shouldParkAfterFailedAcquire(Node pred, Node node)
方法。该方法主要用途是:当线程在获取同步状态失败时,根据前驱节点的等待状态,决定后续的动作。比如前驱节点等待状态为 SIGNAL,表明当前节点线程应该被阻塞住了。不能老是尝试,避免 CPU 忙等。
代码如下:
1 | // 会到这里就是没有抢到锁,这个方法说的是:"当前线程没有抢到锁,是否需要挂起当前线程?" |
这段代码主要检查当前线程是否需要被阻塞,具体规则如下:
- 如果当前线程的前驱节点状态为SIGNAL,则表明当前线程需要被阻塞,调用unpark()方法唤醒,直接返回true,当前线程阻塞
- 如果当前线程的前驱节点状态为CANCELLED(ws > 0),则表明该线程的前驱节点已经等待超时或者被中断了,则需要从CLH队列中将该前驱节点删除掉,直到回溯到前驱节点状态 <= 0 ,返回false
- 如果前驱节点非SINGAL,非CANCELLED,则通过CAS的方式将其前驱节点设置为SIGNAL,返回false
上面一堆看得眼花,简略来说如下:
- 前驱节点为SIGNAL:阻塞。
- 前驱节点为CANCELLED :向前遍历, 移除前面所有为该状态的节点。
- 前驱节点为waitStatus < 0:将前驱节点状态设为 SIGNAL, 并再次尝试获取同步状态。
如果shouldParkAfterFailedAcquire
返回true,则acquireQueued
则会接着调用parkAndCheckInterrupt
来阻塞当前线程,该方法主要是把当前线程挂起,从而阻塞住线程的调用栈,同时返回当前线程的中断状态,其源码如下:
1 | private final boolean parkAndCheckInterrupt() { |
而关于LockSupport
相关的东西,可以查看我对其分析的笔记:Java 并发 - AQS:LockSupport阻塞唤醒线程
再回到acquireQueued中,如果在获取同步状态中出现异常,failed = true
,cancelAcquire
方法会被执行。因为tryAcquire 需同步组件开发者覆写,难免不了会出现异常。该方法主要的作用就是:取消获取同步状态。
1 | private void cancelAcquire(Node node) { |
上面大概就是:获取当前节点的前驱节点,如果前驱节点的状态是CANCELLED,那就一直往前遍历,找到第一个waitStatus <= 0的节点,将找到的Pred节点和当前Node关联,将当前Node设置为CANCELLED。
进行了上述的一些操作之后根据当前节点的位置,其实需要考虑以下三种情况:
- 当前节点是尾节点。
- 当前节点是Head的后继节点。
- 当前节点不是Head的后继节点,也不是尾节点。
具体分析一下一上三种情况:
当前节点是尾节点
当前节点是Head的后继节点:
取消节点的next可以设置为自己本身,不设置为null,上面的注释中有进行解释了,这里就不再解释了,如果有疑惑就往上面翻一下下。
当前节点不是Head的后继节点,也不是尾节点
selfInterrupt
在上面如果acquireQueued为True,就会执行selfInterrupt方法。
1 | static void selfInterrupt() { |
该方法其实是为了中断线程。但为什么获取了锁以后还要中断线程呢?这部分属于Java提供的协作式中断知识内容,这里简单介绍一下:
- 当中断线程被唤醒时,并不知道被唤醒的原因,可能是当前线程在等待中被中断,也可能是释放了锁以后被唤醒。因此我们通过Thread.interrupted()方法检查中断标记(该方法返回了当前线程的中断状态,并将当前线程的中断标识设置为False),并记录下来,如果发现该线程被中断过,就再中断一次。
- 线程在等待资源的过程中被唤醒,唤醒后还是会不断地去尝试获取锁,直到抢到锁为止。也就是说,在整个流程中,并不响应中断,只是记录中断记录。最后抢到锁返回了,那么如果被中断过的话,就需要补充一次中断。
这里的处理方式主要是运用线程池中基本运作单元Worder中的runWorker,通过Thread.interrupted()进行额外的判断处理。
整个流程大概就是这么一个回事了,大概的流程还是清楚的,但是个中细节还有待深挖。
小结
总结一下,acquire的大概流程如下:
- 调用 tryAcquire 方法尝试获取同步状态
- 获取成功,直接返回
- 获取失败,将线程封装到节点中,并将节点入队
- 入队节点在 acquireQueued 方法中自旋获取同步状态
- 若节点的前驱节点是头节点,则再次调用 tryAcquire 尝试获取同步状态
- 获取成功,当前节点将自己设为头节点并返回
- 获取失败,可能再次尝试,也可能会被阻塞。这里简单认为会被阻塞。
acquire的流程图如下(图源见文末文章出处):
示例分析:
以下摘自:一行一行源码分析清楚AbstractQueuedSynchronizer,他以reentrantLock
进行一个简单的分析:
首先,第一个线程调用 reentrantLock.lock(),tryAcquire(1)
直接就返回 true 了,结束。只是设置了 state=1
,连 head 都没有初始化,更谈不上什么阻塞队列了。要是线程 1 调用 unlock() 了,才有线程 2 来,完全没有交集嘛,AQS就派不上用场了。
于是便引出一个问题:如果线程 1 没有调用 unlock() 之前,线程 2 调用了 lock(), 想想会发生什么?
线程 2 会初始化 head【new Node()
】,同时线程 2 也会插入到阻塞队列并挂起 (注意看这里是一个 for 循环,而且设置 head 和 tail 的部分是不 return 的,只有入队成功才会跳出循环)
首先,是线程 2 初始化 head 节点,此时 head== tail
, waitStatus==0
然后线程 2 入队:
同时我们也要看此时节点的 waitStatus
,我们知道 head 节点是线程 2 初始化的,此时的 waitStatus
没有设置, java 默认会设置为 0,但是到 shouldParkAfterFailedAcquire
这个方法的时候,线程 2 会把前驱节点,也就是 head 的waitStatus
设置为 -1。
那线程 2 节点此时的 waitStatus
是多少呢,由于没有设置,所以是 0;
如果线程 3 此时再进来,直接插到线程 2 的后面就可以了,此时线程 3 的 waitStatus 是 0,到 shouldParkAfterFailedAcquire
方法的时候把前驱节点线程 2 的 waitStatus 设置为 -1。
这里可以简单说下 waitStatus
中 SIGNAL(-1)
状态的意思,Doug Lea 注释的是:代表后继节点需要被唤醒。也就是说这个 waitStatus 其实代表的不是自己的状态,而是后继节点的状态,我们知道,每个 node 在入队的时候,都会把前驱节点的状态改为 SIGNAL,然后阻塞,等待被前驱唤醒。这里涉及的是两个问题:有线程取消了排队、唤醒操作。其实本质是一样的,可以顺着 “waitStatus代表后继节点的状态” 这种思路去看一遍源码。
3.1.2 独占式获取响应中断
AQS提供了acquire(int arg)
方法以供独占式获取同步状态,但是该方法对中断不响应,对线程进行中断操作后,该线程会依然位于CLH同步队列中等待着获取同步状态。为了响应中断,AQS提供了acquireInterruptibly(int arg)
方法,该方法在等待获取同步状态时,如果当前线程被中断了,会立刻响应中断抛出异常`InterruptedException
1 | public final void acquireInterruptibly(int arg) |
首先校验该线程是否已经中断了,如果是则抛出InterruptedException
,否则执行tryAcquire(int arg)
方法获取同步状态,如果获取成功,则直接返回,否则执行doAcquireInterruptibly(int arg)
。doAcquireInterruptibly(int arg)
定义如下:
doAcquireInterruptibly(int arg)方法与acquire(int arg)方法仅有两个差别:
- 方法声明抛出InterruptedException异常
- 在中断方法处不再是使用interrupted标志,而是直接抛出InterruptedException异常。
3.1.3 独占式超时获取
AQS除了提供上面两个方法外,还提供了一个增强版的方法:tryAcquireNanos(int arg,long nanos)
。该方法为acquireInterruptibly
方法的进一步增强,它除了响应中断外,还有超时控制。即如果当前线程没有在指定时间内获取同步状态,则会返回false,否则返回true。如下:
1 | public final boolean tryAcquireNanos(int arg, long nanosTimeout) |
其大概流程如下:
3.1.4 独占式同步状态释放:release
释放的过程会比较简单点:
- 调用
tryRelease(arg)
尝试释放同步状态 - 如果
tryRelease
返回true也就是独占锁被完全释放,唤醒后继线程。
这里的唤醒是根据head几点来判断的,下面代码的注释中也分析了head节点的情况,只有在head存在并且等待状态小于零的情况下唤醒。
1 | /** |
跟tryAcquire一样,tryRelease也是由用户自己去实现了,其源码如下:
1 | /* |
3.2 共享模式
其实如果理解了上面的独享模式之后再来理解共享模式,难度不大,主要是与共享模式下,同一时刻会有多个线程获取共享同步状态。共享模式是实现读写锁中的读锁、CountDownLatch 和 Semaphore 等同步组件的基础,这样再去理解一些共享同步组件就不难了。
3.2.1 同步状态获取:acquireShared
共享式获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式的主要区别是在同一时刻可以有多个线程获取到同步状态;
1 | public final void acquireShared(int arg) { |
其中doAcquireShared
以自旋方式获取同步状态,共享式获取同步状态的标志是返回 >= 0 的值表示获取成功,该方法不响应中断,与独占式相似;
1 | private void doAcquireShared(int arg) { |
setHeadAndPropagate
这个函数主要做了两件事:
- 在获取共享锁成功后,设置head节点
- 根据调用tryAcquireShared返回的状态以及节点本身的等待状态来判断是否要需要唤醒后继线程。
1 | private void setHeadAndPropagate(Node node, int propagate) { |
那继续到doReleaseShared
里面看看做了些什么:
doReleaseShared
该方法用于在 acquires/releases 存在竞争的情况下,确保唤醒动作向后传播。这是共享锁中的核心唤醒函数,主要做的事情就是唤醒下一个线程或者设置传播状态。后继线程被唤醒后,会尝试获取共享锁,如果成功之后,则又会调用setHeadAndPropagate,将唤醒传播下去。
总的来说:这个函数的作用是保障在acquire和release存在竞争的情况下,保证队列中处于等待状态的节点能够有办法被唤醒。
1 | /** |
最后说一下共享模式下获取同步状态的大致流程,如下:
- 获取共享同步状态
- 若获取失败,则生成节点,并入队
- 如果前驱为头结点,再次尝试获取共享同步状态
- 获取成功则将自己设为头结点,如果后继节点是共享类型的,则唤醒
- 若失败,将节点状态设为 SIGNAL,再次尝试。若再次失败,线程进入等待状态
3.2.2 共享状态释放:releaseShared
释放共享状态主要逻辑在 doReleaseShared ,而我们前面已经分析过他了,所以就不继续了。共享节点线程在获取同步状态和释放同步状态时都会调用 doReleaseShared,所以 doReleaseShared 是多线程竞争集中的地方。
1 | public final boolean releaseShared(int arg) { |
4.一些疑问
4.1 插入节点时的代码顺序
addWaiter
方法中新增一个节点时为什么要先将新节点的prev置为tail再尝试CAS,而不是CAS成功后来构造节点之间的双向链接?
这是因为,双向链表目前没有基于CAS原子插入的手段,如果我们将node.prev = t
和t.next = node
(t为方法执行时读到的tail,引用封闭在栈上)放到compareAndSetTail(t, node)
成功后执行,如下所示:
1 | if (compareAndSetTail(t, node)) { |
会导致这一瞬间的tail也就是t的prev为null,这就使得这一瞬间队列处于一种不一致的中间状态。
4.2 唤醒节点时为什么从tail向前遍历
unparkSuccessor方法中为什么唤醒后继节点时要从tail向前查找最接近node的非取消节点,而不是直接从node向后找到第一个后break掉?
其实上面的注释中也解释得很清楚了,如果读到s == null
,不代表node就为tail。
考虑如下场景:
- node某时刻为tail
- 有新线程通过addWaiter中的if分支或者enq方法添加自己
- compareAndSetTail成功
- 此时这里的Node s = node.next读出来s == null,但事实上node已经不是tail,它有后继了!
4.3 AQS如何保证队列活跃
AQS如何保证在节点释放的同时又有新节点入队的情况下,不出现原持锁线程释放锁,后继线程被自己阻塞死的情况,保持同步队列的活跃?
回答这个问题,需要理解shouldParkAfterFailedAcquire
和unparkSuccessor
这两个方法。
- 以独占锁为例,后继争用线程阻塞自己的情况是读到前驱节点的等待状态为SIGNAL,只要不是这种情况都会再试着去争取锁。假设后继线程读到了前驱状态为SIGNAL,说明之前在tryAcquire的时候,前驱持锁线程还没有tryRelease完全释放掉独占锁。
- 此时如果前驱线程完全释放掉了独占锁,则在
unparkSuccessor
中还没执行完置waitStatus
为0的操作,也就是还没执行到下面唤醒后继线程的代码,否则后继线程会再去争取锁。那么就算后继争用线程此时把自己阻塞了,也一定会马上被前驱线程唤醒。 - 那么是否可能持锁线程执行唤醒后继线程的逻辑时,后继线程读到前驱等待状态为SIGNAL把自己给阻塞,再也无法苏醒呢?
- 确实可能在扫描后继需要唤醒线程时读不到新来的线程,但只要
tryRelease
语义实现正确,在true时表示完全释放独占锁,则后继线程理应能够tryAcquire
成功,shouldParkAfterFailedAcquire
在读到前驱状态不为SIGNAL
会给当前线程再一次获取锁的机会的。
4.4 AQS如何防止内存泄露
AQS维护了一个FIFO队列,它是如何保证在运行期间不发生内存泄露的?
AQS在无竞争条件下,甚至都不会new出head和tail节点。线程成功获取锁时设置head节点的方法为setHead,由于头节点的thread并不重要,此时会置node的thread和prev为null,完了之后还会置原先head也就是线程对应node的前驱的next为null,从而实现队首元素的安全移出。而在取消节点时,也会令node.thread = null
,在node不为tail的情况下,会使node.next = node
(之所以这样也是为了isOnSyncQueue
实现更加简洁)
5小结
在并发环境下,加锁和解锁需要以下三个部件的协调:
- 锁状态。我们要知道锁是不是被别的线程占有了,这个就是 state 的作用,它为 0 的时候代表没有线程占有锁,可以去争抢这个锁,用 CAS 将 state 设为 1,如果 CAS 成功,说明抢到了锁,这样其他线程就抢不到了,如果锁重入的话,state进行 +1 就可以,解锁就是减 1,直到 state 又变为 0,代表释放锁,所以 lock() 和 unlock() 必须要配对啊。然后唤醒等待队列中的第一个线程,让其来占有锁。
- 线程的阻塞和解除阻塞。AQS 中采用了 LockSupport.park(thread) 来挂起线程,用 unpark 来唤醒线程。
- 阻塞队列。因为争抢锁的线程可能很多,但是只能有一个线程拿到锁,其他的线程都必须等待,这个时候就需要一个 queue 来管理这些线程,AQS 用的是一个 FIFO 的队列,就是一个链表,每个 node 都持有后继节点的引用。AQS 采用了 CLH 锁的变体来实现。
用了好几天,看了很多博客还有翻了一些书,对着JDK源码一点一点的抠了出来上面的这些阅读理解,感觉这个源码还是有些难度,还是需要时不时的回头看看,其实主要就分为独占式和共享式,然后各有没有完成的方法需要继承AQS的子类去完成,要对大致的状态获取、状态释放有所了解,这些会比较重要点,对那几个状态需要多了解了解是什么个意思,一般会出现在什么情况,感觉看了一些源码之后,发现这些源码中的状态位其实很重要,每个方法都伴随着状态位的改变,通过状态位可以了解到很多内部细节,最后还是说分析得太烂,以后又有认识之后一定要把上面这个重新整理一遍,还是不太深刻,盲人摸象,只了解到了一小部分罢了。
以上参考: