一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第3天,点击查看活动详情。
- 之前简单介绍了一下AQS-> ReentrantLock(重入锁)之聊聊AQS,接下来看看AQS中独占锁的源码。
AQS独占锁源码分析
1)独占锁
独占模式,即只允许一个线程获取同步状态,当这个线程还没有释放同步状态时,其他线程是获取不了的,只能加入到同步队列,进行等待。
首先调用的是acquire方法,两种结果:1. 成功,则方法结束返回,2. 失败,先调用addWaiter()然后在调用acquireQueued()方法
acquire(int arg)
//以独占模式获取,忽略中断。通过调用至少一次 tryAcquire 来实现,成功返回。否则线程排队,可能重复阻塞和解除阻塞,调用 tryAcquire 直到成功。此方法可用于实现方法 Lock.lock。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))// 这里 Node.EXCLUSIVE 的值是 null,Node.EXCLUSIVE互斥模式、Node.SHARED共享模式
selfInterrupt();
}
tryAcquire方法,由具体的锁来实现的,这个方法主要是尝试获取锁,获取成功就不会再执行其他代码了。
tryAcquire(int arg)
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
获取锁失败的情况下则将进行入队操作,即addWaiter(Node.EXCLUSIVE),这里的Node.EXCLUSIVE是空,用于构造nextWaiter,这是在独占锁的模式下,共享锁的话则使用Node.SHARED。之前说到过同步队列中的节点有两种,一种是共享模式,队列中的每个节点都指向一个静态的SHARED 节点,即下图中的SHARED,而独占队列每个节点都指向的是空,即EXCLUSIVE。
接下来来看一下addWaiter的源码
addWaiter(Node mode)
private Node addWaiter(Node mode) {//mode = Node.EXCLUSIVE = null
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {// 如果尾节点不为空,就把节点放在尾节点后面并设置为新的尾节点
node.prev = pred;
if (compareAndSetTail(pred, node)) {// 尝试把节点设置为新的尾节点
pred.next = node;
return node;
}
}
enq(node);
return node;
}
设置失败的话会进入一个方法enq()
。如果当前没有尾节点,则会直接进入到enq()
方法,用于完成对同步队列的头结点初始化工作以及CAS操作失败的重试
enq(final Node node)
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // 如果尾节点为空,那么队列也为空,新建一个头节点,让 head 和 tail 都指向它
if (compareAndSetHead(new Node()))
tail = head;
} else {// 如果有尾节点,把传入的节点放入队尾
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
该方法先判断是否有尾节点,没有的话则说明需要初始化(因为没有尾节点也就意味着没有头节点),通过CAS新增一个空的头节点,然后尾指针也指向这个节点。如果有尾节点的话就直接将这个节点加在尾节点后面,然后通过CAS将尾指针指向新的尾节点。
addWaiter()
方法结束后,接下来就是方法acquireQueued()
,用于已在队列中的线程以独占且不间断模式获取state状态,直到获取锁后返回
acquireQueued(final Node node, int arg)
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; // 将之前头节点的 next 指针置空,后面 GC 时会回收p
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&// 若没有获取锁则判断是否应该阻塞当前线程(核心是判断并修正前面节点的 waitStatus)
parkAndCheckInterrupt())// 阻塞当前线程、返回并清除中断标记
interrupted = true;
}
} finally {
//将当前节点设置为取消状态
if (failed)
cancelAcquire(node);
}
}
acquireQueued()方法的流程大致如下:
获取锁的代码已经讲完了,接下来看看前节点不是头节点时判断是否需要阻塞的方法shouldParkAfterFailedAcquire(),先来回顾一下节点的waitStatus的作用
CANCELLED:代表取消状态,该线程节点已释放(超时、中断),已取消的节点不会再阻塞
SIGNAL:代表通知状态,这个状态下的节点如果被唤醒,就有义务去唤醒它的后继节点。这也就是为什么一个节点的线程阻塞之前必须保证前一个节点是 SIGNAL 状态,因为这样才能保证前一个节点可以去唤醒他的后继节点。
CONDITION :代表条件等待状态,条件等待队列里每一个节点都是这个状态,它的节点被移到同步队列之后状态会修改为 0。
PROPAGATE:代表传播状态,在一些地方用于修复 bug 和提高性能,减少不必要的循环。
ps: 如果 waiterStatus 的值为 0,有两种情况:1、节点状态值没有被更新过(同步队列里最后一个节点的状态);2、在唤醒线程之前头节点状态会被被修改为 0。
tips: 负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来判断结点的状态是否正常。
shouldParkAfterFailedAcquire(Node pred, Node node)
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/* 判断前面节点状态为 SIGNAL ,返回 true
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/* 如果前面的节点状态为取消(CANCEL值为1),就一直向前查找,直到找到状态不为取消的节点,把它放在这个节点后面
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/* 如果前面节点不是取消也不是 SIGNAL 状态,将其设置为 SIGNAL 状态
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
当shouldParkAfterFailedAcquire返回 true 时,就会进入下一个方法parkAndCheckInterrupt():
parkAndCheckInterrupt()
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 阻塞当前线程,通过LockSupport类调用 Unsafe 这个类的 park() 方法进行阻塞
return Thread.interrupted();// 返回并清除当前线程中断状态
}
到此加锁的过程就结束了,接下来时解锁的部分-> release()
release(int arg)
public final boolean release(int arg) {
if (tryRelease(arg)) {// 尝试释放锁,如果成功则唤醒后继节点的线程
//tryRelease()跟tryAcquire()一样实现都是由具体的锁来实现的。
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);// 唤醒后面节点中第一个非取消状态节点的线程
return true;
}
return false;
}
方法开始尝试释放锁,若失败直接返回,如果释放锁成功,那么就会接着判断头节点是否为空和头节点 waitStatus 是否不为 0 ,因为在唤醒头节点的后继之前会通过CAS尝试将头节点状态置0的操作。如果头节点的状态为 0 了,说明正在释放后继节点,这时候也就不再需要释放了,直接返回 true。判断状态之后就是unparkSuccessor方法:
unparkSuccessor(Node node)
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;// 获取头节点(head)的状态
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0); //通过CAS操作尝试将头节点状态置0
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
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);// 唤醒符合条件的后继节点,通过LockSupport类调用 Unsafe 这个类的 unpark() 方法进行唤醒
}
总结:
在获取同步状态时,AQS维护一个同步队列,获取同步状态失败的线程会加入到队列中进行自旋;移除队列(或停止自旋)的条件是前驱节点是头结点并且成功获得了同步状态。在释放同步状态时,同步器会调用unparkSuccessor()方法唤醒后继节点。