文章中的源码均来自JDK1.8
前言
上一篇 Java并发学习【一】AbstractQueuedSynchronizer队列实现和API简介 的文章中介绍了 AQS 的 API 和队列节点Node的源码,接下来两篇分别介绍 AQS 的独占模式和共享模式,先来看看独占模式。
独占模式:是指当一个线程获取到了锁,其他线程则无法获取到锁而进入阻塞状态,直到拥有锁的线程释放了锁,其他线程才能去争夺锁。
1. 独占模式示例
上一篇API介绍中自定义同步组件需要继承AQS并重写指定的方法来实现功能,要想实现独占锁,需要重写其 tryAcquire 和 tryRelease ,如下简单的独占锁实现
public class ExclusiveLock {
private final Sync sync;
public ExclusiveLock () {
sync = new Sync();
}
static final class Sync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int acquires) {
// 获取同步状态
int state = this.getState();
// 判断当前同步状态是否为0,为0代表可以尝试获取锁
if (state == 0) {
// 使用CAS修改同步状态state,成功则将当前线程设置为独占锁的拥有,并返回true
if (this.compareAndSetState(0, acquires)) {
this.setExclusiveOwnerThread(Thread.currentThread());
return true;
}
}
return false;
}
@Override
protected boolean tryRelease(int acquires) {
// 如果当前线程不是占用锁的线程则抛出异常
if (Thread.currentThread() != this.getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// 设置占用锁的线程为null
this.setExclusiveOwnerThread(null);
// 将同步状态state设置为0
this.setState(0);
return true;
}
@Override
protected boolean isHeldExclusively() {
return this.getState() == 1;
}
}
/**
* 获取锁
*/
public void lock () {
sync.acquire(1);
}
/**
* 释放锁
*/
public void unlock () {
sync.release(1);
}
/**
* 锁是否被获取了
* @return
*/
public boolean isLocked () {
return sync.isHeldExclusively();
}
}
上面代码中使用一个内部类 Sync 继承 AbstractQueuedSynchronizer,继承对应的方法,而外部类中方法的功能是通过Sync来实现的。
2. 源码解析
2.1 独占式获取同步状态
示例中的lock方法中使用Sync的示例调用acquire方法,那我们先来看看acquire方法
public final void acquire(int arg) {
// tryAcquire由子类实现,返回true则获取到同步状态
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // EXCLUSIVE是独占模式
selfInterrupt(); // 中断当前线程
}
tryAcquire方法是由子类实现的,所以这里会回调示例中重写的tryAcquire,当该方法返回true的时候说明获取同步状态成功,如果返回false则进入 && 后面的方法,接下来看看 addWaiter 方法的实现
/**
* 加入同步队列
*/
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) { // 如果尾节点不为空
node.prev = pred;
if (compareAndSetTail(pred, node)) { // CAS 设置为尾节点
pred.next = node;
return node;
}
}
enq(node); // 设置队列为空或者设置尾节点失败则使用自旋加入队列
return node;
}
addWaiter方法是将创建包含当前线程的Node对象,然后使用CAS方式放入同步队列中去,如果失败则进入enq(node)方法
/**
* CAS循环加入同步队列
*/
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // 如果同步队列为空则初始化头节点
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
enq方法是使用循环加入同步队列,方法中先判断尾节点tail是否为空,如果为空说明同步队列为空,则初始化队列,因为该线程是获取锁失败才进来这方法,说明有线程占有锁,所以创建个空的头节点,之后再将当前线程的节点Node加入队列,加入成功后返回当前线程的节点。
上面acquire方法中addWaiter执行完后返回的Node对象被当作参数传入acquireQueued方法,下面来看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 等待并检查是否中断了
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
在这个方法中,节点会进行自我检查,当检查到自己的前驱节点为头节点时,会去调用子类重写的tryAcquire方法尝试获取锁,获取成功则将当前节点设置为头节点,并删除原来的头节点。如果失败的话会进入下面的判断,先来看看shouldParkAfterFailedAcquire方法
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// 表示后继节点处于等待状态返回true
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) { // 当前节点超时或者中断状态,删除该节点
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 将waitStatus设置为 -1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
获取前驱节点的waitStatus, 当ws = 1(SIGNAL)时,则返回true说明当前节点的线程需要进行阻塞,当ws > 0(CANCELLD)则前驱节点为超时或者中断,删除该节点。当前ws为0或者-3(PROPAGATE)时尝试将前驱节点的waitStatus状态改成1。如果当前的节点需要进行阻塞则进入parkAndCheckInterrupt方法,该方法主要是阻塞当前线程并检查当前线程是否被中断。
讲到这里独占锁的获取已经结束了,下面画个图来回顾下整个获取独占锁的过程
2.2 独占式释放同步状态
示例中的锁的释放是通过Sync调用release方法来实现的,下面来看release方法的代码
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0) // 这里两个 != 是判断有头节点并且该头节点有后继节点
unparkSuccessor(h); // 唤醒后继节点
return true;
}
return false;
}
release方法首先会调用子类重写的tryRelease方法释放同步状态,如果释放成功则调用uparkSuccessor方法唤醒后继节点
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0); // 设置头节点的waitStatus为0
/*
* 获取下个节点
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) { // 下一个节点为null或者超时中断,则从尾节点往前查找可唤醒的最前面的节点
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); // 唤醒查找到的节点对应的线程
}
uparkSuccessor的过程是获取头节点的后继节点,如果不为空并且不是超时或者中断状态的情况下则直接唤醒,否则从尾节点往前查找非超时或中断的最前面的节点,查找到则唤醒该节点对应的线程。
参考资料:《Java并发编程的艺术》