队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作,并发包的作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。
顾名思义,AQS不是一个实际的类,它是一个抽象类,需要继承该类并且实现抽象方法来管理同步状态。而管理同步状态时不免要对同步状态进行更改,这就需要使用到以下三个方法:
-
getState()
获取当前同步状态。 -
setState(int newState)
设置当前同步状态。 -
compareAndSetState(int expect,int update)
使用CAS设置当前状态,该方法能够保证状态 设置的原子性。
子类推荐被定义为自定义同步组件的静态内部 类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来 供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获 取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、 ReentrantReadWriteLock和CountDownLatch等)
同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步 器实现锁的语义。可以这样理解二者之间的关系:
- 锁是面向使用者的,它定义了使用者与锁交 互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;
- 同步器面向的是锁的实现者, 它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。
锁和同 步器很好地隔离了使用者和实现者所需关注的领域。
同步器接口实例:
同步器可重写的方法:
方法名称 | 方法描述 |
---|---|
boolean tryAcquire(int arg) |
独占式获取同步状态,实现该方法需要查询当前 状态并判断同步状态是否符合预期,然后再进行 CAS设置同步状态 |
boolean tryRelease(int arg) |
独占式释放同步状态,等待获取同步状态的 线程将有机会获取同步状态 |
int tryAcquireShared(int arg) |
共享式获取同步状态,返回大于等于0的值, 表示获取成功,反之,获取失败 |
boolean tryReleaseShared(int arg) |
共享式释放同步状态 |
boolean isHeldExclusively() |
当前同步器是否在独占模式下被线程占用, 一般该方法表示是否被当前线程所独占 |
实现自定义同步组件时,将会调用同步器提供的模板方法,这些(部分)模板方法与描述如下表所示
方法名称 | 方法描述 |
---|---|
void acquire(int arg) |
独占式获取同步状态,如果当前线程获取同 步状态成功,则由该方法返回 否则,将会进 入同步队列等待,该方法将会调用重写的 tryAcquire(int arg)方法 |
void acquireInterruptibly(int arg) |
与 acquire(int arg)相同,但是该方法响应中断 ,当前线程未获取到 同步状态而进入同步 队列中,如果当前线程被中断,则该方法会 抛出Interruptedexception并返回 |
tryAcquireNanos(int arg, long nanosTimeout) |
在 acquireinterruptibly(int arg)基础上增加 了超时限制,如果当前线程在超时时间内没 有获取到同步状态,那么将会返回 false, 如果获取到了返回true |
void acquireShared(int arg) |
共享式的获取同步状态,如果当前线程未获 取到同步状态,将会进入同步队列等待,与 独占式获取的主要区别是在同一时刻可以 有多个线程获取到同步状态 |
void acquireSharedInterruptibly(int arg) |
与 acquireShared(int arg)相同,该方法响应中断 |
boolean tryAcquireSharedNanos(int arg, long nanosTimeout) |
在 acquireSharedinterruptibly(int arg) 基础上增加了超时限制 |
boolean release(int arg) |
独占式的释放同步状态,该方法会在释放同 步状态之后,将同步队列中第一个节点包含的线程唤醒 |
boolean releaseShared(int arg) |
共享式的释放同步状态 |
Collection |
获取等待在同步队列上的线程集合 |
同步器提供的模板方法基本上分为3类:
- 独占式获取与释放同步状态
- 共享式获取与释放
- 同步状态和查询同步队列中的等待线程情况
通过上述模板方法,我们可以将锁大致分为独占式锁与共享式锁。
独占式锁
顾名思义,独占锁就是在同一时刻只能有一个线程获取到锁,而其他获取锁的线程只能 处于同步队列中等待,只有获取锁的线程释放了锁,后继的线程才能够获取锁。下面我们通过一段代码来演示一下:
public class Mutex implements Lock {
// 静态内部类,自定义同步器
private static class Sync extends AbstractQueuedSynchronizer {
// 是否处于占用状态
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
// 当状态为0的时候获取锁
@Override
public boolean tryAcquire(int acquires) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// 释放锁,将状态设置为0
@Override
protected boolean tryRelease(int releases) {
if (getState() == 0) throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
return true;
}
// 返回一个Condition,每个condition都包含了一个condition队列
Condition newCondition() {
return new ConditionObject();
}
}
// 仅需要将操作代理到Sync上即可
private final Sync sync = new Sync();
public void lock() {
sync.acquire(1);
}
public boolean tryLock() {
return sync.tryAcquire(1);
}
public void unlock() {
sync.release(1);
}
public Condition newCondition() {
return sync.newCondition();
}
public boolean isLocked() {
return sync.isHeldExclusively();
}
public boolean hasQueuedThreads() {
return sync.hasQueuedThreads();
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
}
上述示例中,独占锁Mutex是一个自定义同步组件,它在同一时刻只允许一个线程占有锁。Mutex中定义了一个静态内部类,该内部类继承了同步器并实现了独占式获取和释放同步状态。在tryAcquire(int acquires)方法中,如果经过CAS设置成功(同步状态设置为1),则代表获取了同步状态,而在tryRelease(int releases)方法中只是将同步状态重置为0。用户使用Mutex时并不会直接和内部同步器的实现打交道,而是调用Mutex提供的方法,在Mutex的实现中,以获取锁的lock()方法为例,只需要在方法实现中调用同步器的模板方法acquire(int args)即可,当前线程调用该方法获取同步状态失败后会被加入到同步队列中等待,这样就大大降低了实现一个可靠自定义同步组件的门槛。
共享式锁
共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。以文件的读写为例,如果一个程序在对文件进行读操作,那么这一时刻对于该文件的写操作均被阻塞,而读操作能够同时进行。写操作要求对资源的独占式访问,而读操作可以是共享式访问,两种不同的访问模式在同一时刻对文件或资源的访问情况如下图所示:
上图中左半部分,共享式访问资源时,其他共享式的访问均被允许,而独占式访问被阻塞,右半部分是独占式访问资源时,同一时刻其他访问均被阻塞。
下面我们通过一段代码来实际演示共享式同步锁的使用:
public class TwinsLock implements Lock {
private final Sync sync = new Sync(2);
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
if (count <= 0) {
throw new IllegalArgumentException("count must large than zero.");
}
setState(count);
}
public int tryAcquireShared(int reduceCount) {
for (; ; ) {
int current = getState();
int newCount = current - reduceCount;
if (newCount < 0 || compareAndSetState(current, newCount)) {
return newCount;
}
}
}
public boolean tryReleaseShared(int returnCount) {
for (; ; ) {
int current = getState();
int newCount = current + returnCount;
if (compareAndSetState(current, newCount)) {
return true;
}
}
}
}
public void lock() {
sync.acquireShared(1);
}
public void unlock() {
sync.releaseShared(1);
}
// 其他接口方法略
}
在上述示例中,TwinsLock实现了Lock接口,提供了面向使用者的接口,使用者调用lock()方法获取锁,随后调用unlock()方法释放锁,而同一时刻只能有两个线程同时获取到锁。TwinsLock同时包含了一个自定义同步器Sync,而该同步器面向线程访问和同步状态控制。以共享式获取同步状态为例:同步器会先计算出获取后的同步状态,然后通过CAS确保状态的正确设置,当tryAcquireShared(int reduceCount)方法返回值大于等于0时,当前线程才获取同步状态,对于上层的TwinsLock而言,则表示当前线程获得了锁。
下面编写一个测试来验证TwinsLock是否能按照预期工作。在测试用例中,定义了工作者线程Worker,该线程在执行过程中获取锁,当获取锁之后使当前线程睡眠0.5秒(并不释放锁),随后打印当前线程名称,最后再次睡眠1秒并释放锁,测试用例如下:
@Test
public void twinsLockTest() throws InterruptedException {
final Lock lock = new TwinsLock();
class Worker extends Thread {
public void run() {
while (true) {
// System.out.println("before - " + Thread.currentThread().getName());
lock.lock();
try {
Thread.sleep(1000);
System.err.println("got - " + Thread.currentThread().getName());
Thread.sleep(1000);
} catch (InterruptedException e) {
} finally {
lock.unlock();
}
// System.out.println("after - " + Thread.currentThread().getName());
}
}
}
// 启动10个线程
for (int i = 0; i < 10; i++) {
Worker w = new Worker();
w.start();
}
// 每隔1秒换行
for (int i = 0; i < 10; i++) {
Thread.sleep(1000);
System.out.println();
}
}
运行该测试用例,可以看到线程名称成对输出,也就是在同一时刻只有两个线程能够获 取到锁,这表明TwinsLock可以按照预期正确工作。
启动运行之后会看到基本获取到共享资源的线程都是固定的那两个,这是因为在没获取到共享资源时该线程会被加入AQS的等待队列,在释放资源之后再被唤醒重新竞争资源,而由于之前等待的线程需要被唤醒才能重新竞争共享资源,而释放资源的线程由于不需要唤醒,所以大概率会比其他线程优先再次获取到锁,可以将before和after打印语句取消注释在运行,这样可以解决这个问题。
对于AQS队列同步器的实现分析我们放到下一节去分析,本节主要简单的讲解和举例AQS对于共享式和独占式锁的实现