AQS¶
AQS(AbstractQueuedSynchronizer)在ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch、ThreadPoolExcutor 的 Worker 中都有运用(JDK 1.8),AQS是这些类的底层原理。
ReentrantLock和Semaphore都可以当做一个阀门来使用。比如我们把Semaphore的许可证数量设置为 1,那么由于它只有一个许可证,所以只能允许一个线程通过,并且当之前的线程归还许可证后,会允许其他线程继续获得许可证。其实这点和ReentrantLock很像,只有一个线程能获得锁,并且当这个线程释放锁之后,会允许其他的线程获得锁。那如果线程发现当前没有 额外的许可证时,或者当前得不到锁,那么线程就会被阻塞,并且等到后续有许可证或者锁释放出来后,被唤醒,所以这些环节都是比较类似的。
除了ReentrantLock和Semaphore之外,我们会发现CountDownLatch、ReentrantReadWriteLock 等工具类都有类似的让线程“协作”的功能,其实它们背后都是利用AQS来实现的。
为什么需要AQS¶
有了上面的铺垫,现在就让我们来想一下,为什么需要 AQS?
原因是,上面刚讲的那些协作类,它们有很多工作是类似的,所以如果能把实现类似工作的代码给提取出来,变成一个新的底层工具类(或称为框架)的话,就可以直接使用这个工具类来构建上层代码了,而这个工具类其实就是 AQS。
有了AQS之后,对于ReentrantLock和Semaphore等线程协作工具类而言,它们就不需要关心这么多的线程调度细节,只需要实现它们各自的设计逻辑即可。
如果没有 AQS,那就需要每个线程协作工具类自己去实现至少以下内容,包括:
- 状态的原子性管理
- 线程的阻塞与解除阻塞
- 队列的管理
这里的状态对于不同的工具类而言,代表不同的含义,比如对于ReentrantLock而言,它需要维护锁被重入的次数,但是保存重入次数的变量是会被多线程同时操作的,就需要进行处理,以便保证线程安全。不仅如此,对于那些未抢到锁的线程,还应该让它们陷入阻塞,并进行排队,并在合适的时机唤醒。所以说这些内容其实是比较繁琐的,而且也是比较重复的,而这些工作目前都由 AQS 来承担了。
如果没有 AQS,就需要 ReentrantLock 等类来自己实现相关的逻辑,但是让每个线程协作工具类自己去正确并且高效地实现这些内容,是相当有难度的。AQS 可以帮我们把 “脏活累 活” 都搞定,所以对于 ReentrantLock 和 Semaphore 等类而言,它们只需要关注自己特有的业务逻辑即可。正所谓是“哪有什么岁月静好,不过是有人替你负重前行”。
AQS是一个用于构建锁、同步器等线程协作工具类的框架,有了 AQS 以后,很多用于线程协作的工具类就都可以很方便的被写出来,有了 AQS 之后,可以让更上层的开发极大的 减少工作量,避免重复造轮子,同时也避免了上层因处理不当而导致的线程安全问题,因为AQS把这些事情都做好了。总之,有了AQS之后,我们构建线程协作工具类就容易多了。
AQS原理¶
AQS最核心的三大部分就是状态、队列和期望协作工具类去实现的获取/释放等重要方法。
Sate状态¶
如果我们的 AQS 想要去管理或者想作为协作工具类的一个基础框架,那么它必然要管理一些状态,而这个状态在 AQS 内部就是用 state 变量去表示的。它的定义如下:
而state的含义并不是一成不变的,它会根据具体实现类的作用不同而表示不同的含义:
比如说在信号量里面state表示的是剩余许可证的数量。如果我们最开始把 state 设置为 10,这就代表许可证初始一共有 10 个,然后当某一个线程取走一个许可证之后,这个 state 就会变为 9,所以信号
量的 state 相当于是一个内部计数器。
再比如,在 CountDownLatch 工具类里面,state 表示的是需要“倒数”的数量。一开始我们假设把它设置为 5,当每次调用 CountDown 方法时,state 就会减 1,一直减到 0 的时候就代表这个门闩被放开。
那为什么还会变成 2、3、4 呢?为什么会往上加呢?因为 ReentrantLock 是可重入的,同一个线程可以再次拥有这把锁就叫重入。如果这个锁被同一个线程多次获取,那么 state 就会逐渐的往上加,state 的值表示重入的次数。在释放的时候也是逐步递减,比如一开始是 4,释放一次就变成了 3,再释放一次变成了 2,这样进行的减操作,即便是减到 2 或者 1 了,都不代表这个锁是没有任何线程持有,只 有当它减到 0 的时候,此时恢复到最开始的状态了,则代表现在没有任何线程持有这个锁了。所以,state 等于 0 表示锁不被任何线程所占有,代表这个锁当前是处于释放状态的,其他线程此时就可以来 尝试获取了。
这就是 state 在不同类中不同含义的一个具体表现。我们举了三个例子,如果未来有新的工具要利用到 AQS,它一定也需要利用 state,为这个类表示它所需要的业务逻辑和状态。
下面我们再来看一下关于 state 修改的问题,因为 state 是会被多个线程共享的,会被并发地修改,所以所有去修改 state 的方法都必须要保证 state 是线程安全的。可是 state 本身它仅仅是被 volatile 修饰 的,volatile 本身并不足以保证线程安全,所以我们就来看一下,AQS 在修改 state 的时候具体利用了什么样的设计来保证并发安全。
我们举两个和 state 相关的方法,分别是 compareAndSetState 及 setState,它们的实现已经由 AQS 去完成了,也就是说,我们直接调用这两个方法就可以对 state 进行线程安全的修改。下面就来看一下这两 个方法的源码是怎么实现的。
队列¶
FIFO 队列,即先进先出队列,这个队列最主要的作用是存储等待的线程。假设很多线程都想要同时抢锁,那么大部分的线程是抢不到的,那怎么去处理这些 抢不到锁的线程呢?就得需要有一个队列来存放、管理它们。所以 AQS 的一大功能就是充当线程的“排队管理器”。 当多个线程去竞争同一把锁的时候,就需要用排队机制把那些没能拿到锁的线程串在一起;而当前面的线程释放锁之后,这个管理器就会挑选一个合适的线程来尝试抢刚刚释放的那把锁。所以 AQS 就一 直在维护这个队列,并把等待的线程都放到队列里面。 这个队列内部是双向链表的形式,其数据结构看似简单,但是要想维护成一个线程安全的双向队列却非常复杂,因为要考虑很多的多线程并发问题。
在队列中,分别用 head 和 tail 来表示头节点和尾节点,两者在初始化的时候都指向了一个空节点。头节点可以理解为“当前持有锁的线程”,而在头节点之后的线程就被阻塞了,它们会等待被唤醒,唤醒也是由 AQS 负责操作的。
获取/释放方法¶
获取和释放相关的重要方法是协作工具类的逻辑的具 体体现,需要每一个协作工具类自己去实现,所以在不同的工具类中,它们的实现和含义各不相同。
获取方法¶
获取操作通常会依赖 state 变量的值,根据 state 值不同,协作工具类也会有不同的逻辑,并且在获取的时候也经常会阻塞,下面就让我们来看几个具体的例子。
比如 ReentrantLock 中的 lock 方法就是其中一个“获取方法”,执行时,如果发现 state 不等于 0 且当前线程不是持有锁的线程,那么就代表这个锁已经被其他线程所持有了。这个时候,当然就获取不到 锁,于是就让该线程进入阻塞状态。 再比如,Semaphore 中的 acquire 方法就是其中一个“获取方法”,作用是获取许可证,此时能不能获取到这个许可证也取决于 state 的值。如果 state 值是正数,那么代表还有剩余的许可证,数量足够的 话,就可以成功获取;但如果 state 是 0,则代表已经没有更多的空余许可证了,此时这个线程就获取不到许可证,会进入阻塞状态,所以这里同样也是和 state 的值相关的。
再举个例子,CountDownLatch 获取方法就是 await 方法(包含重载方法),作用是“等待,直到倒数结束”。执行 await 的时候会判断 state 的值,如果 state 不等于 0,线程就陷入阻塞状态,直到其他线程 执行倒数方法把 state 减为 0,此时就代表现在这个门闩放开了,所以之前阻塞的线程就会被唤醒。
我们总结一下,“获取方法”在不同的类中代表不同的含义,但往往和 state 值相关,也经常会让线程进入阻塞状态,这也同样证明了 state 状态在 AQS 类中的重要地位。
释放方法¶
释放方法是站在获取方法的对立面的,通常和刚才的获取方法配合使用。我们刚才讲的获取方法可能会让线程阻塞,比如说获取不到锁就会让线程进入阻塞状态,但是释放方法通常是不会阻塞线程的。
比如在 Semaphore 信号量里面,释放就是 release 方法(包含重载方法),release() 方法的作用是去释放一个许可证,会让 state 加 1;而在 CountDownLatch 里面,释放就是 countDown 方法,作用是倒数 一个数,让 state 减 1。所以也可以看出,在不同的实现类里面,他们对于 state 的操作是截然不同的,需要由每一个协作类根据自己的逻辑去具体实现。
AQS用法¶
我们先讲一下 AQS 的用法。如果想使用 AQS 来写一个自己的线程协作工具类,通常而言是分为以下三步,这也是 JDK 里利 用 AQS 类的主要步骤: * 第一步,新建一个自己的线程协作工具类,在内部写一个 Sync 类,该 Sync 类继承 AbstractQueuedSynchronizer,即 AQS; * 第二步,想好设计的线程协作工具类的协作逻辑,在 Sync 类里,根据是否是独占,来重写对应的方法。如果是独占,则 重写 tryAcquire 和 tryRelease 等方法;如果是非独占,则重写 tryAcquireShared 和 tryReleaseShared 等方法; * 第三步,在自己的线程协作工具类中,实现获取/释放的相关方法,并在里面调用 AQS 对应的方法,如果是独占则调用 acquire 或 release 等方法,非独占则调用 acquireShared 或 releaseShared 或 acquireSharedInterruptibly 等方法。
AQS在CountDownLatch的应用¶
在 CountDownLatch 里面有一个子类,该类的类名叫 Sync,这个类正是继承自 AQS。
构造函数¶
CountDownLatch 只有一个构造方法,传入的参数是需要“倒数”的次数,每次调用 countDown 方法就会 倒数 1,直到达到了最开始设定的次数之后,相当于是“打开了门闩”,所以之前在等待的线程可以继续工作了。
getCount()¶
接下来介绍 getCount 方法,该方法的作用是获取当前剩余的还需要“倒数”的数量,getCount 方法的源码如下:
countDown()¶
方法内是一个 for 的死循环,在循环体中,最开始是通过 getState 拿到当前 state 的值并赋值给变量 c,这个 c 可以理解为是 count 的缩写,如果此时 c = 0,则意味着已经倒数为零了,会直接会执行下面的 return false 语句,一旦 tryReleaseShared 方法 返回 false,再往上看上一层的 releaseShared 方法,就会直接跳过整个 if (tryReleaseShared(arg)) 代码块,直接返回 false,相当 于 releaseShared 方法不产生效果,也就意味着 countDown 方法不产生效果。
再回到 tryReleaseShared 方法中往下看 return false 下面的语句,如果 c 不等于 0,在这里会先把 c-1 的值赋给 nextc,然后再利 用 CAS 尝试把 nextc 赋值到 state 上。如果赋值成功就代表本次 countDown 方法操作成功,也就意味着把 AQS 内部的 state 值 减了 1。最后,是 return nextc == 0,如果 nextc 为 0,意味着本次倒数后恰好达到了规定的倒数次数,门闩应当在此时打开, 所以 tryReleaseShared 方法会返回 true,那么再回到之前的 releaseShared 方法中,可以看到,接下来会调用 doReleaseShared 方 法,效果是对之前阻塞的线程进行唤醒,让它们继续执行。
如果结合具体的数来分析,可能会更清晰。假设 c = 2,则代表需要倒数的值是 2,nextc = c-1,所以 nextc 就是 1,然后利用 CAS 尝试把 state 设置为 1,假设设置成功,最后会 return nextc == 0,此时 nextc 等于 1,不等于 0,所以返回 false,也就意味 着 countDown 之后成功修改了 state 的值,把它减 1 了,但并没有唤醒线程。 下一次执行 countDown时,c 的值就是 1,而 nextc = c - 1,所以 nextc 等于 0,若这时 CAS 操作成功,最后 return nextc == 0, 所以方法返回 true,一旦 tryReleaseShared 方法 return true,则 releaseShared 方法会调用 doReleaseShared 方法,把所有之前阻 塞的线程都唤醒。
await¶
接着我们来看看 await 方法,该方法是 CountDownLatch 的“获取”方法,调用 await 方法会把线程阻塞,直到倒数为 0 才能继续 执行。await 方法和 countDown 是配对的,追踪源码可以看到 await 方法的实现:
它会调用 sync 的 acquireSharedInterruptibly ,并且传入 1。acquireSharedInterruptibly 方法源码如下所示:我们再来看下另一种情况,当 state 如果此时已经等于 0 了,那就意味着倒数其实结束了,不需要再去等待了,就是说门闩是打开状态,所以说此时 getState 返回 0,tryAcquireShared 方法返回 1 ,一旦返回 1,对于 acquireSharedInterruptibly 方法而言相 当于立刻返回,也就意味着 await 方法会立刻返回,那么此时线程就不会进入阻塞状态了,相当于倒数已经结束,立刻放行了。
这里的 await 和 countDown 方法,正对应了本讲一开始所介绍的“第三步,在自己的线程协作工具类中,实现获取/释放的相关 方法,并在里面调用 AQS 对应的方法,如果是独占则调用 acquire 或 release 等方法,非独占则调用 acquireShared 或 releaseShared 或 acquireSharedInterruptibly 等方法。”