目录
[以可重入的互斥锁 ReentrantLock 为例](#以可重入的互斥锁 ReentrantLock 为例)
[以倒计时器 CountDownLatch 以例](#以倒计时器 CountDownLatch 以例)
[AQS 资源共享方式](#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 创建一个独占锁。
java
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
public class MyLock {
private static class Sync extends AbstractQueuedSynchronizer {
// 尝试获取锁
@Override
protected boolean tryAcquire(int arg) {
if (compareAndSetState(0, 1)) { // 如果状态为0,则设置为1
setExclusiveOwnerThread(Thread.currentThread()); // 记录当前线程
return true;
}
return false;
}
// 尝试释放锁
@Override
protected boolean tryRelease(int arg) {
if (getExclusiveOwnerThread() != Thread.currentThread()) {
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null); // 清空持有线程
setState(0); // 重置状态
return true;
}
// 判断锁是否被持有
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
}
private final Sync sync = new Sync();
public void lock() {
sync.acquire(1);
}
public void unlock() {
sync.release(1);
}
}
性能优化
AQS 在多线程竞争情况下,通过使用自旋锁和阻塞等技术来优化性能,减少上下文切换的开销。