在Java的多线程环境,如果需要控制多个线程访问共享资源时,我们可以使用synchronized来提供同步,实现锁功能。在Java SE 5之后,Java并发包新增了Lock接口以及相关实现类,我们可以使用Lock实现与synchronized相同的锁功能。由于Lock锁在使用时需要显式地区获取和释放锁,使锁的释放和获取具有可操作性,也衍生了一些功能各异的锁。例如可重入锁ReentrantLock,可重入读写锁ReentrantReadWriteLock。当我们去分析ReentrantLock,ReentrantReadWriteLock的源码时,不难发现这些锁功能的实现均由队列同步器AbstractQueuedSynchronizer提供。
队列同步器AbstractQueuedSynchronizer是面向锁开发者的一个基础框架,我们可以利用队列同步器AbstractQueuedSynchronizer构建锁或者其他同步组件。在AbstractQueuedSynchronizer中使用一个int成员变量state表示同步状态,同时使用一个先进先出的队列来完成资源获取线程的排队工作(在AbstractQueuedLongSynchronizer中使用long成员变量state表示同步状态)。
同步器是实现锁以及同步组件的关键,在锁的内部中使用同步器去实现锁的语义。锁是面对使用者,它定义了锁使用者和锁交互的接口。而同步器是面向锁的实现者,简化了锁的实现方式。
Node代表同步队列的同步节点。同步队列用于管理获取同步状态失败的线程。当前线程获取同步状态失败时,同步器将当前线程构造成一个Node节点,将其加入到同步队列尾部,同时阻塞当前线程,而当同步状态被释放时,会唤醒同步队列的头节点,头节点尝试获取同步状态。
Node类有一个重要的成员变量waitStatus,代表Node节点的等待状态。
注:waitStatus为负值表示节点处于等待状态,waitStatus为0表示初始状态,waitStatus为正值表示处于取消状态
在同步器AbstractQueuedSynchronizer中有几个重要的成员变量。
// 同步队列头节点
private transient volatile Node head;
// 同步队列尾节点
private transient volatile Node tail;
// 同步状态
private volatile int state;
在同步器AbstractQueuedSynchronizer分析之前,首先解释一下什么是独占式获取锁,什么是共享式获取锁。以读写锁为例,一个资源可以同时被多个线程读,但是只能被一个线程写。那么这个读所使用的锁就是共享式锁,允许多个线程获取锁,这个写就是独占式锁,只允许一个线程获取锁。在ReentrantReadWriteLock源码中我们也可以发现有两个锁,一个是读锁ReadLock,而一个是写锁WriteLock。读锁ReadLock与写锁WriteLock联合使用实现读写锁的功能。
无论是独占式获取锁,还是共享式获取锁,在同步器AbstractQueuedSynchronizer的实现中都是体现在同步状态的获取和更改。因此我们来看一下在同步器中有关同步状态获取和更改的三个重要方法。
getState():获取当前同步状态
protected final int getState() {
return state;
}
setState():设置当前同步状态
protected final void setState(int newState) {
state = newState;
}
compareAndSetState():使用CAS设置当前同步状态
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
这里我们可以注意到state是一个volatile类型的成员变量。可以保证state在修改的时候能够及时被其他线程感知到。
我们继续看独占式和共享式如何获取释放锁
独占式获取锁
//独占式获取同步状态
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire(arg)方法可以确保在线程安全情况下获取同步状态。如果获取同步状态失败,构造一个同步节点并通过addWaiter方法将该节点加入到同步队列的尾部。接下来调用acquireQueued方法,使该节点以死循环的方式自旋获取同步状态。
注:在同步器的源码中tryAcquire方法只抛出异常,没有具体实现逻辑,需要自定义同步组件实现
private Node addWaiter(Node mode) {
// 创建同步节点
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
// 如果tail节点不为空,尝试快速方式放入队尾
if (pred != null) {
node.prev = pred;
// CAS设置尾节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 将节点设置为尾节点
enq(node);
return node;
}
enq方法设置尾节点。
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) {
// tail为空,初始化head节点,并将node设置为tail节点
if (compareAndSetHead(new Node()))
tail = head;
} else {
// tail不为空,将node设置为尾节点
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
acquireQueued方法确保当前线程以死循环的方式自旋获取同步状态,只有前继节点为头节点,才尝试获取同步状态,否则将线程设置为等待状态。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 循环获取同步状态
for (;;) {
// 获取当前节点的前继节点
final Node p = node.predecessor();
// 前继节点为头节点,尝试获取同步状态
if (p == head && tryAcquire(arg)) {
//设置当前节点为头节点
setHead(node);
// 设置头节点的后继节点为空
p.next = null; // help GC
failed = false;
return interrupted;
}
// shouldParkAfterFailedAcquire检查状态,设置节点为等待状态,因为只有前驱节点为头节点时才尝试获取同步状态
// parkAndCheckInterrupt设置线程为waiting状态,并检测线程是否被中断
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 如果获取同步状态失败,取消获取
if (failed)
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire方法检查节点的状态。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获取前驱节点的状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
// 如果前驱节点是取消等待状态,向前遍历,直至节点是正常等待状态
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
// 直至前驱节点是正常等待状态,设置前驱节点的后继节点为node
pred.next = node;
} else {
// CAS设置前驱节点为SIGNAL状态,即表示前驱节点的后继节点为等待状态
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
parkAndCheckInterrupt方法设置线程为waiting状态,并检测线程是否被中断。
private final boolean parkAndCheckInterrupt() {
// 使线程进入waiting状态
LockSupport.park(this);
// 检测线程是否被中断
return Thread.interrupted();
}
独占式获取锁的过程虽然繁琐但十分重要,因此总结一下流程
独占式释放锁
public final boolean release(int arg) {
// 尝试释放同步状态
if (tryRelease(arg)) {
Node h = head;
// 头节点不为空,唤醒后继节点
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
注:在同步器的源码中tryRelease方法只抛出异常,没有具体实现方法体,需要自定义同步组件实现
唤醒后继节点
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
// 设置当前节点为初始状态
compareAndSetWaitStatus(node, ws, 0);
// 获取节点的后继节点
Node s = node.next;
// 节点为空或者节点处于取消等待状态
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
// 唤醒等待状态的线程
LockSupport.unpark(s.thread);
}
unpark方法唤醒线程
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
共享式获取锁
共享式获取锁与独占式获取锁的区别在于共享式允许多个线程获取锁。
public final void acquireShared(int arg) {
// 获取同步状态,返回值大于等于0,表示能够获取同步状态
if (tryAcquireShared(arg) < 0)
// 未获取同步状态,进入自旋阶段
doAcquireShared(arg);
}
在doAcquireShared方法中,以死循环的方式自旋,如果当前节点的前继节点是头节点,尝试获取同步状态,如果返回值大于等于0,表示盖茨获取同步状态成功并从自旋过程中退出。
private void doAcquireShared(int arg) {
// 向同步队列尾部增加节点
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 获取当前节点的前继节点
final Node p = node.predecessor();
if (p == head) {
// 如果前继节点为头节点,获取同步状态
int r = tryAcquireShared(arg);
if (r >= 0) {
// 成功获取同步状态
// 设置当前节点为头节点,试图唤醒后面的线程
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
// 安全的方式中断线程
selfInterrupt();
failed = false;
return;
}
}
// shouldParkAfterFailedAcquire检查状态,设置节点为等待状态,因为只有前驱节点为头节点时才尝试获取同步状态
// parkAndCheckInterrupt设置线程为waiting状态,并检测线程是否被中断
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 如果获取同步状态失败,取消获取
if (failed)
cancelAcquire(node);
}
}
共享式释放锁
public final boolean releaseShared(int arg) {
// 释放同步状态
if (tryReleaseShared(arg)) {
// 唤醒后续处于等待状态的节点
doReleaseShared();
return true;
}
return false;
}
同步器AbstractQueuedSynchronizer定义了许多实现同步组件的模板方式,自定义的同步组件将使用模板方法来实现自己的同步语义。所以如果想了解同步组件的机制那一定要先了解同步器的工作原理。
同步器AbstractQueuedSynchronizer本身是一个抽象类,部分方法虽然有方法体,也只是抛出异常,没有实现逻辑。需要各个自定义同步组件实现,因此结合同步组件源码理解同步器效果更佳。