目录
AQS
介绍
原理
以可重入的互斥锁 ReentrantLock 为例
以倒计时器 CountDownLatch 以例
AQS 资源共享方式
实现自定义同步器
示例
性能优化
AQS
介绍
AQS (AbstractQueuedSynchronizer
),抽象队列同步器。AQS 是一个功能强大且灵活的框架,适合于实现高性能的同步工具。这个类在 java.util.concurrent.locks
包下面。AQS 就是一个抽象类,主要用来构建锁和同步器。
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {}
AQS 为构建锁和同步器提供了一些通用功能的实现,因此,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如ReentrantLock
,Semaphore,
ReentrantReadWriteLock
,SynchronousQueue
等等皆是基于 AQS 的。
原理
AQS 核心思想是如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是基于 CLH 锁 (Craig, Landin, and Hagersten locks) 实现的。
CLH 锁是对自旋锁的一种改进,是一个虚拟的双向队列(即不存在队列实例,仅存在结点之间的关联关系),暂时获取不到锁的线程将被加入到该队列中。
AQS 使用 int 成员变量 state 表示同步状态,通过内置的 FIFO 线程等待/等待队列 来完成获取资源线程的排队工作。
- 状态信息
state
变量由volatile
修饰,用于展示当前临界资源的获锁情况。 state
可以通过protected
类型的getState()
、setState()
和compareAndSetState()
进行操作。并且,这几个方法都是final
修饰的,在子类中无法被重写。
以可重入的互斥锁 ReentrantLock
为例
以可重入的互斥锁 ReentrantLock
为例,它的内部维护了一个 state
变量,用来表示锁的占用状态。state
的初始值为 0,表示锁处于未锁定状态。
当线程 A 调用 lock()
方法时,会尝试通过 tryAcquire()
方法独占该锁,并让 state
的值加 1。如果成功了,那么线程 A 就获取到了锁。如果失败了,那么线程 A 就会被加入到一个等待队列(CLH 队列)中,直到其他线程释放该锁。
假设线程 A 获取锁成功了,释放锁之前,A 线程自己是可以重复获取此锁的(state
会累加)。这就是可重入性的体现:一个线程可以多次获取同一个锁而不会被阻塞。但是,这也意味着,一个线程必须释放与获取的次数相同的锁,才能让 state
的值回到 0,也就是让锁恢复到未锁定状态。只有这样,其他等待的线程才能有机会获取该锁。
以倒计时器 CountDownLatch
以例
再以倒计时器 CountDownLatch
以例,任务分为 N 个子线程去执行,state
也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程开始执行任务,每执行完一个子线程,就调用一次 countDown()
方法。该方法会尝试使用 CAS(Compare and Swap) 操作,让 state
的值减少 1。
当所有的子线程都执行完毕后(即 state
的值变为 0),会调用 CountDownLatch.unpark()
唤醒主线程。这时,主线程就可以从 CountDownLatch.await()
返回,继续执行后续的操作。
AQS 资源共享方式
AQS 定义两种资源共享方式:
Exclusive
独占,只有一个线程能执行,如ReentrantLock
Share
共享,多个线程可同时执行,如Semaphore
/CountDownLatch
一般来说,自定义同步器的共享方式要么是独占,要么是共享,他们也只需实现tryAcquire-tryRelease
、tryAcquireShared-tryReleaseShared
中的一种即可。但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock
。
实现自定义同步器
同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):
- 使用者继承
AbstractQueuedSynchronizer
并重写指定的方法。 - 将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。
AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的钩子方法:
//独占方式。尝试获取资源,成功则返回true,失败则返回false。
protected boolean tryAcquire(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
protected boolean tryRelease(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
protected int tryAcquireShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
protected boolean tryReleaseShared(int)//该线程是否正在独占资源。只有用到condition才需要去实现它。
protected boolean isHeldExclusively()
开发者可以通过继承 AQS 类并实现其中的方法(如 tryAcquire()
、tryRelease()
、tryAcquireShared()
和 tryReleaseShared()
)来创建自定义的锁或其他同步工具。
什么是钩子方法呢? 钩子方法是一种被声明在抽象类中的方法,一般使用 protected
关键字修饰,它可以是空方法(由子类实现),也可以是默认实现的方法。模板设计模式通过钩子方法控制固定步骤的实现。
示例
下面是一个简单的示例,展示如何使用 AQS 创建一个独占锁。
import java.util.concurrent.locks.AbstractQueuedSynchronizer;public class MyLock {private static class Sync extends AbstractQueuedSynchronizer {// 尝试获取锁@Overrideprotected boolean tryAcquire(int arg) {if (compareAndSetState(0, 1)) { // 如果状态为0,则设置为1setExclusiveOwnerThread(Thread.currentThread()); // 记录当前线程return true;}return false;}// 尝试释放锁@Overrideprotected boolean tryRelease(int arg) {if (getExclusiveOwnerThread() != Thread.currentThread()) {throw new IllegalMonitorStateException();}setExclusiveOwnerThread(null); // 清空持有线程setState(0); // 重置状态return true;}// 判断锁是否被持有@Overrideprotected boolean isHeldExclusively() {return getState() == 1;}}private final Sync sync = new Sync();public void lock() {sync.acquire(1);}public void unlock() {sync.release(1);}
}
性能优化
AQS 在多线程竞争情况下,通过使用自旋锁和阻塞等技术来优化性能,减少上下文切换的开销。