My logo
Published on

AQS抽象同步器核心原理-05

使用基于CAS自旋实现的轻量级锁有两个大的问题:
(1)CAS恶性空自旋会浪费大量的CPU资源。
(2)在SMP架构的CPU上会导致“总线风暴”。
解决CAS恶性空自旋的有效方式之一是以空间换时间,较为常见的方案有两种:分散操作热点和使用队列削峰。JUC并发包使用的是队列削峰的方案解决CAS的性能问题,并提供了一个基于双向队列的削峰基类——抽象基础类AbstractQueuedSynchronizer(抽象同步器类,简称为AQS)。

5.1 JUC显式锁与AQS的关系

AQS是java.util.concurrent包的一个同步器,它实现了锁的基本抽象功能,支持独占锁与共享锁两种方式。该类是使用模板模式来实现的,成为构建锁和同步器的框架,使用该类可以简单且高效地构造出应用广泛的同步器(或者等待队列)。
java.util.concurrent.locks包中的显式锁如ReentrantLock、ReentrantReadWriteLock,线程同步工具如Semaphore,异步回调工具如FutureTask等,内部都使用了AQS作为等待队列。通过开发工具进行AQS的子类导航会发现大量的AQS子类以内部类的形式使用。

AQS定义了两种资源共享方式:
·Exclusive(独享锁):只有一个线程能占有锁资源,如ReentrantLock。独享锁又可分为公平锁和非公平锁。
·Share(共享锁):多个线程可同时占有锁资源,如Semaphore、CountDownLatch、CyclicBarrier、ReadWriteLock的Read锁。

5.2 ReentrantLock与AQS的关系

ReentrantLock是一个可重入的互斥锁,又称为“可重入独占锁”。ReentrantLock锁在同一个时间点只能被一个线程锁持有,而可重入的意思是,ReentrantLock锁可以被单个线程多次获取。
经过观察,ReentrantLock把所有Lock接口的操作都委派到一个Sync类上,该类继承了AbstractQueuedSynchronizer
ReentrantLock的显式锁操作是委托(或委派)给一个Sync内部类的实例来完成的。而Sync内部类只是AQS的一个子类,所以本质上ReentrantLock的显式锁操作是委托(或委派)给AQS完成的。一个ReentrantLock对象的内部一定有一个AQS类型的组合实例,二者之间是组合关系。

06-thread-22

5.3 AQS中的钩子方法

自定义同步器时,AQS中需要重写的钩子方法大致如下:
·tryAcquire(int):独占锁钩子,尝试获取资源,若成功则返回true,若失败则返回false。
·tryRelease(int):独占锁钩子,尝试释放资源,若成功则返回true,若失败则返回false。
·tryAcquireShared(int):共享锁钩子,尝试获取资源,负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
·tryReleaseShared(int):共享锁钩子,尝试释放资源,若成功则返回true,若失败则返回false。
·isHeldExclusively():独占锁钩子,判断该线程是否正在独占资源。只有用到condition条件队列时才需要去实现它。

5.4 ReentrantLock 原理

5.4 .1 ReentrantLock的抢锁流程

下面结合AbstractQueuedSynchronizer()的模板方法详细说明ReentrantLock的实现过程。ReentrantLock有两种模式:
·公平锁:按照线程在队列中的排队顺序,先到者先拿到锁。 ·非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的。 ReentrantLock在同一个时间点只能被一个线程获取,ReentrantLock是通过一个FIFO的等待队列(AQS队列)来管理获取该锁所有线程的。ReentrantLock是继承自Lock接口实现的独占式可重入锁,并且ReentrantLock组合一个AQS内部实例完成同步操作。

5.4 .2 ReentrantLock非公平锁的抢占流程

06-thread-22

ReentrantLock为非公平锁实现了一个内部的同步器——NonfairSync,其显式锁获取方法lock()的源码如下:

06-thread-22

首先用一个CAS操作判断state是不是0(表示当前锁未被占用),如果是0就把它置为1,并且设置当前线程为该锁的独占线程,表示获取锁成功。当多个线程同时尝试占用同一个锁时,CAS操作只能保证一个线程操作成功,剩下的只能乖乖去排队。
ReentrantLock“非公平”性体现在这里:如果占用锁的线程刚释放锁,state置为0,而排队等待锁的线程还未唤醒,新来的线程就直接抢占了该锁,那么就“插队”了。举一个例子,当前有三个线程A、B、C去竞争锁,假设线程A、B在排队,但是后来的C直接进行CAS操作成功了,拿到锁开开心心地返回了,那么线程A、B只能乖乖看着。

5.4 .3 ReentrantLock公平锁的抢占流程

06-thread-22

ReentrantLock为公平锁实现了一个内部的同步器——FairSync,其显式锁获取方法lock的源码如下:

06-thread-37

公平抢占的钩子方法中,首先判断是否有后继节点,如果有后继节点,并且当前线程不是锁的占有线程,钩子方法就返回false,模板方法会进入排队的执行流程,可见公平锁是真正公平的。

5.5 AQS条件队列

Condition是JUC用来替代传统Object的wait()/notify()线程间通信与协作机制的新组件,相比调用Object的wait()/notify(),调用Condition的await()/signal()这种方式实现线程间协作更加高效。
Condition基本原理
Condition与Object的wait()/notify()作用是相似的,都是使得一个线程等待某个条件,只有当该条件具备signal()或者signalAll()方法被调用时等待线程才会被唤醒,从而重新争夺锁。不同的是,Object的wait()/notify()由JVM底层实现,而Condition接口与实现类完全使用Java代码实现。当需要进行线程间的通信时,建议结合使用ReetrantLock与Condition,通过Condition的await()和signal()方法进行线程间的阻塞与唤醒
说明:Condition条件队列是单向的,而AQS同步队列是双向的,AQS节点会有前驱指针。一个AQS实例可以有多个条件队列,是聚合关系;但是一个AQS实例只有一个同步队列,是逻辑上的组合关系。

** await()等待方法原理**
当线程调用await()方法时,说明当前线程的节点为当前AQS队列的头节点,正好处于占有锁的状态,await()方法需要把该线程从AQS队列挪到Condition等待队列里,如图:

06-thread-37

await()方法的整体流程如下:
(1)执行await()时,会新创建一个节点并放入Condition队列尾部。
(2)然后释放锁,并唤醒AQS同步队列中的头节点的后一个节点。
(3)然后执行while循环,将该节点的线程阻塞,直到该节点离开等待队列,重新回到同步队列成为同步节点后,线程才退出while循环。
(4)退出循环后,开始调用acquireQueued()不断尝试拿锁。
(5)拿到锁后,会清空Condition队列中被取消的节点。

signal()唤醒方法原理

06-thread-39

signal()方法的整体流程如下:
(1)通过enq()方法自旋(该方法已经介绍过)将条件队列中的头节点放入AQS同步队列尾部,并获取它在AQS队列中的前驱节点。
(2)如果前驱节点的状态是取消状态,或者设置前驱节点为Signal状态失败,就唤醒当前节点的线程;否则节点在同步队列的尾部,参与排队。
(3)同步队列中的线程被唤醒后,表示重新获取了显式锁,然后继续执行condition.await()语句后面的临界区代码。

5.6 AQS的实际应用

首先介绍一下JUC的总体架构,如图:

06-thread-39

AQS建立在CAS原子操作和volatile可见性变量的基础之上,为上层的显式锁、同步工具类、阻塞队列、线程池、并发容器、Future异步工具提供线程之间同步的基础设施。所以,AQS在JUC框架中的使用是非常广泛的。