Java中的锁,如,ReentrantLock(显式锁)、CountDownLatch(闭锁)、CyclicBarrier
Semaphore、Exchange等都是基于AQS(队列同步器)来实现的,AQS又是CLH队列锁的一种变体实现。下面对这些锁的知识进行详细介绍。
AbstractQueuedSynchronizer
AbstractQueuedSynchronizer简写是AQS,翻译过来叫“队列同步器”。它是由并发包大师Doug Lea所设计,设计的初衷是期望它能够成为实现大部分同步需求的基础框架。
AQS的主要使用方式是继承,子类通过继承AQS并实现它的抽象方法来管理同步状态。在AQS里使用一个int型的变量(state)来表示同步状态。在方法的实现中主要通过使用同步器提供的getState()、setState(int newState)和compareAndSetState(int expect,int update)),3个方法来进行操作。这些方法对状态的修改保证是线程安全的。
getState() : 获取当前同步状态。
setState(int newState) : 设置当前同步状态。
compareAndSetState(int expect,int update) : 通过CAS操作,保证状态操作的原子性。
/**
* The synchronization state.
*/
private volatile int state;
AQS定义了多种同步状态获取和释放的接口来供自定义同步组件使用,而自身没有对同步接口的实现。同步器即支持独占式地获取同步状态,也支持共享式地获取同步状态。使用AQS可以很方便的实现多种类型的同步组件。如ReentrantLock、ReentrantReadWriteLock、CountDownLatch等都是基于同步器来实现。
AQS的接口方法
同步器的设计基于模板方法模式。模板方法设计模式,通过定义一个操作中的算法的骨架,将实现步骤中具体的实现放到子类中,由子类自定义实现。模板方法可以在不改变结构的情况下,灵活定义 灵活的,使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
AQS中提供的接口方法,主要有4类:独占式获取与释放同步状态、共享式获取与释放、查询操作同步状态、查询同步队列中的线程等待情况。共享式与独占式获取状态的区别,是共享式在同一时刻可以有多个线程获取到同步状态,而独占式只能有一个。AQS中提供的接口方法方法有:
public final void acquire(int arg) : 独占式获取同步状态。如果当前线程获取同步状态成功,则返回;否则,进入同步队列等待,该方法将会调用重写的tryAcquire方法。
public final void acquireInterruptibly(int arg) : 独占式获取同步状态,功能与acquire方法相同,但是该方法会响应中断。当前线程未获取到同步状态而进入同步队列时,如果线程被中断,则方法抛出InterruptedException并返回。
public final boolean tryAcquireNanos(int arg, long nanos) : 独占式获取同步状态,在acquireInterruptibly方法基础上增加了超时机制。当线程等待在同步队列中,当超时时间到时,方法也会返回。
public final boolean release(int arg) : 释放独占式同步状态。该方法在释放同步状态之后,会将同步队列中第一个节点的线程唤醒。
public final void acquireShared(int arg) : 共享式的获取同步状态。如果该线程未获取到同步状态,将会进入同步队列等待。
public final void acquireSharedInteruptibly(int arg) : 共享式的获取同步状态,功能与acquireShared方法相同,该方法响应中断。
public final boolean tryAcquireSharedNanos(int arg, long nanos) : 共享式的获取同步状态,在acquireSharedInteruptibly基础上增加了超时机制。
public final boolean releseShared(int arg),释放共享式同步状态。
public final Collection<Thread> getQueuedThreads(),获取等待在同步队列上的线程。
提供给实现类用于重写的方法
protected boolean tryAcquire(int arg),独占式获取同步状态,实现该方法通常先查询和判断当前同步状态是否符合预期,然后再通过接口设置同步状态。
protected boolean tryRelease(int arg),独占式释放同步状态,释放后,其他等待的线程有机会获取同步状态。
protected int tryAcquireShared(int arg),共享式获取同步状态,返回大于等于0表示成功;为0,表示获取失败。
protected boolean tryReleaseShared(int arg),共享式释放同步状态。
protected boolean isHeldExclusively(),返回当前同步状态是否被当前线程独占式访问。
CLH队列锁
AQS实际上是CLH队列锁的一种变体。CLH即Craig,Landin,and Hagersten locks,是用三个大佬的名字来命名。该锁是基于链表结构,提供可扩展、高性能、公平访问的自旋锁。
CLH主要的实现思路是:申请锁的线程将挂载到链表上,在自己的本地变量上进行自旋,同时轮询它前一个节点的同步状态,当前驱节点释放锁并将状态改变后,立刻结束自旋,进行同步操作。同步操作完成后,修改自己的同步状态,这样它的后继节点将获取同步状态。依次类推。
ReentrantLock
ReentrantLock的静态内部类Sync继承了AbstractQueuedSynchronizer,对于锁的操作主要是通过Sync类来完成,这里使用的是独占锁。即,ReentrantLock是基于AQS来实现的。
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
// ...
// 默认提供非公平锁的获取实现
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
// ...
}
ReentrantLock中实现了公平锁和非公平锁两种方式。ReentrantLock提供实现公平锁和非公平锁的构造函数,ReentrantLock(boolean fair),传入true时表示实现“公平锁”,否则表示实现“非公平锁”。而默认的无参构造函数将实现非公平锁。
从公平锁和非公平锁实现看出,主要是tryAcquire方法实现不同,公平锁是在自己类中实现,非公平锁实现在父类Sync中。他们的区别是,公平锁判断条件多了hasQueuedPredecessors(),判断是否有前驱,即是否有更早的线程在请求锁。理解为公平锁请求要参与排队。
// 非公平锁实现
static final class NonfairSync extends Sync {
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
// 调用到AQS中的方法,最终通过子类重写的tryAcquire实现
acquire(1);
}
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
// 公平锁实现
static final class FairSync extends Sync {
final void lock() {
// 调用到AQS中的方法,最终通过子类重写的tryAcquire实现
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
setExclusiveOwnerThread()和getExclusiveOwnerThread()方法用于实现可重入锁。即获取到锁后设置为当前线程,后续请求锁判断设置的是否为当前线程,如果是则可以获取锁,并将同步状态+1。同一个线程多次获取锁后,在释放锁时,也需要释放多次,每次释放状态-1,直到状态为0。
CountDownLatch
CountDownLatch也是AQS的一种实现,与ReentrantLock不同,主要用AQS共享锁来实现。
该工具用于当前线程需要等待其他线程完成后才能继续进行。如用与初始化的场景,Tt线程,需要等待Ta、Tb、Tc、Td的部分或全部功能完成后才能继续运行。如下图,Tt线程调用await()方法,并设置计数(CNT=5)。Ta~Td各自完成指定的任务则调用countDown方法将计数减1。当计数减为0时Tt线程将继续运行。
public class CountDownLatch {
// 继承自AbstractQueuedSynchronizer,使用AQS框架的共享锁来实现
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
setState(count);
}
int getCount() {
return getState();
}
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
protected boolean tryReleaseShared(int releases) {
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c - 1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
private final Sync sync;
// 初始化时设置同步状态的计数
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
// 等待同步技术状态变为0后继续运行
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
// 每次调用同步状态计数减1
public void countDown() {
sync.releaseShared(1);
}
}
另外,CyclicBarrier、Semaphore、Exchange三种锁也是基于AQS实现,这里不做介绍。读者可自行阅读源码学习。
AQS和锁的关系辨析
AQS(同步器)是用于实现锁的框架,锁是基于同步器来实现的。他们的关系是,锁面向使用者,线程通过锁来实现操作的同步安全;同步器用于进行锁实现,它友好的封装了同步状态管理、线程排队、等待、唤醒等等操作,使得锁的实现被大大的简化。
锁和同步器很好地隔离了锁的使用者和实现者所需关注的知识领域,使用者只需要关注锁所提供的接口,而实现者只需要实现同步器指定的模板方法。由此使得锁的使用和实现都得到了简化。