本文已参与掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力。
在java锁:AQS详解(一)这篇文章中,从源码的角度深度剖析了AQS独占锁模式下的获取锁与释放锁的逻辑,下面继续从源码中分析共享锁的实现原理和condition条件。
获取锁
//获取共享锁,忽略中断;
public final void acquireShared(int arg) {
//子类实现,CAS方式获取共享锁,若获取失败,调用doAcquireShared继续获取共享锁
if (tryAcquireShared(arg) < 0)
//尝试获取共享锁,获取失败则将当前线程节点入队,直到被通知或被中断
doAcquireShared(arg);
}
这里的tryAcquireShared方法是留给实现方去实现获取锁的具体逻辑的,需要看的是doAcquireShared方法的实现逻辑:
//获取共享锁,若CAS获取失败,则将当前节点入队并阻塞当前线程,直到获取锁
private void doAcquireShared(int arg) {
//将当前节点插入队尾
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (; ; ) {
//获取当前节点的前驱节点,若前驱节点为头节点,则尝试获取锁;
//若获取失败,则检查节点状态;当节点状态为SIGNAL时将节点线程阻塞
final Node p = node.predecessor();
if (p == head) {
//CAS获取锁
int r = tryAcquireShared(arg);
if (r >= 0) {
//成功则设置当前节点为头节点并将其他节点状态设为PROPAGAE
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
//检查当前节点是否应该阻塞,是则进行阻塞处理,直到被中断
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//若获取锁失败,则进行取消获取处理
if (failed)
cancelAcquire(node);
}
}
还是自旋机制,在线程挂起之前,不断地循环尝试获取锁,不同的是,一旦获取共享锁,会调用 setHeadAndPropagate方法同时唤醒后继节点,实现共享模式,下面是唤醒后继节点代码逻辑:
//设置当前节点为头节点并唤醒共享模式下的线程
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
//设为头节点
setHead(node);
//若propagate > 0或头结点为空且头节点状态为 < 0
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
//若头节点的后继节点为共享模式,则获取头结点
if (s == null || s.isShared())
doReleaseShared();
}
}
该方法主要做了两个重要的步骤:
1.将当前节点设置为新的头节点,即当前节点的前置节点(旧头节点)已经获取共享锁了,从队列中去除; 2.调用doReleaseShared方法,它会调用unparkSuccessor方法唤醒后继节点。
释放锁
//释放共享锁
public final boolean releaseShared(int arg) {
//cas方式释放锁,若失败则doReleaseShared释放锁
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
下面是释放锁逻辑:
private void doReleaseShared() {
for (;;) {
// 从头节点开始执行唤醒操作
// 这里需要注意,如果从setHeadAndPropagate方法调用该方法,那么这里的head是新的头节点
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
//表示后继节点需要被唤醒
if (ws == Node.SIGNAL) {
// 初始化节点状态
//这里需要CAS原子操作,因为setHeadAndPropagate和releaseShared这两个方法都会顶用doReleaseShared,避免多次unpark唤醒操作
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
// 如果初始化节点状态失败,继续循环执行
continue; // loop to recheck cases
// 执行唤醒操作
unparkSuccessor(h);
}
//如果后继节点暂时不需要唤醒,那么当前头节点状态更新为PROPAGATE,确保后续可以传递给后继节点
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// 如果在唤醒的过程中头节点没有更改,退出循环
// 这里防止其它线程又设置了头节点,说明其它线程获取了共享锁,会继续循环操作
if (h == head) // loop if head changed
break;
}
}
共享锁的释放锁逻辑比独占锁的释放锁逻辑稍微复杂,原因是共享锁需要释放队列中所有共享类型的节点,因此需要循环操作,由于释放锁过程中会涉及多个地方修改节点状态,此时需要CAS原子操作来并发安全。共享锁的主要特征是当有一个线程获取到锁之后,那么它就会依次唤醒等待队列中可以跟它共享的节点,当然这些节点也是共享锁类型。
condition条件
Condition接口一共定义了以下几个方法:
await(): 当前线程进入等待状态,直到被通知(siginal)或中断。
awaitUninterruptibly(): 当前线程进入等待状态,直到被通知,对中断不敏感。
awaitNanos(long timeout): 当前线程进入等待状态直到被通知(siginal),中断或超时。
awaitUnitil(Date deadTime): 当前线程进入等待状态直到被通知(siginal),中断或到达某个时间。
signal(): 唤醒一个等待在Condition上的线程,该线程从等待方法返回前必须获得与Condition关联的锁
signalAll(): 唤醒所有等待在Condition上的线程,能够从等待方法返回的线程必须获得与Condition关联的锁。
condition的实现:首先内部存在一个Condition队列,存储着所有在此Condition条件等待的线程。
await系列方法:让当前持有锁的线程释放锁,并唤醒一个在CLH队列上等待锁的线程,再为当前线程创建一个node节点,插入到Condition队列(注意不是插入到CLH队列中)
signal系列方法:其实这里没有唤醒任何线程,而是将Condition队列上的等待节点插入到CLH队列中,所以当持有锁的线程执行完毕释放锁时,就会唤醒CLH队列中的一个线程,这个时候才会唤醒线程。
await相关方法
//让当前持有锁的线程阻塞等待,并释放锁。如果有中断请求,则抛出InterruptedException异常
public final void await() throws InterruptedException {
//若当前线程已被中断,则抛出中断异常
if (Thread.interrupted())
throw new InterruptedException();
// 为当前线程创建新的Node节点,并且将这个节点插入到Condition队列中
Node node = addConditionWaiter();
//释放当前线程持有的锁,并唤醒同步队列中的头结点
int savedState = fullyRelease(node);
int interruptMode = 0;
//如果当前节点补足同步队列中;
//阻塞当前线程,当前当前线程被signal信号唤醒后,将当前节点加入同步队列中;
//等待获取获取锁
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
//检查是否被中断并入队等待锁
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 如果节点node已经在同步队列中了,获取同步锁,只有得到锁才能继续执行,否则线程继续阻塞等待
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 清除Condition队列中状态不是Node.CONDITION的节点
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
// 是否要抛出异常,或者发出中断请求
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
//为当前线程创建新的Node节点,并且将这个节点插入到Condition队列中
private Node addConditionWaiter() {
Node t = lastWaiter;
// 如果等待队列尾节点状态不是CONDITION,则进行清除操作;
// 清除队列中状态不是CONDITION的节点
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
//为当前线程创建一个状态为CONDITION的节点,并将节点插入队尾
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
//从头到尾部遍历等待队列,去除状态不是CONDITION的节点
private void unlinkCancelledWaiters() {
//记录下一个待处理的节点
Node t = firstWaiter;
//记录上一个状态为CONDITION的节点
Node trail = null;
while (t != null) {
Node next = t.nextWaiter;
if (t.waitStatus != Node.CONDITION) {
t.nextWaiter = null;
if (trail == null)
firstWaiter = next;
else
trail.nextWaiter = next;
if (next == null)
lastWaiter = trail;
} else
trail = t;
t = next;
}
}
调用的isOnSyncQueue方法和reportInterruptAfterWait方法分別是:判断节点释放在同步队列中 ,根据当前的模式判断是否抛出异常或重新中断等
signal相关方法
//如果等待队列不为空,则将队列头节点插入同步队列中
public final void signal() {
//如果当前线程不是独占锁,则抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
//将等待队列中的头结点插入同步队列中
if (first != null)
doSignal(first);
}
//将等待队列总的头结点插入同步队列中
private void doSignal(Node first) {
do {
// 原先的Condition队列头节点取消,所以重新赋值Condition队列头节点
// 如果新的Condition队列头节点为null,表示Condition队列为空了
// ,所以也要设置Condition队列尾lastWaiter为null
if ((firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
// 返回true表示节点node插入到同步队列中,返回false表示节点node没有插入到同步队列中
final boolean transferForSignal(Node node) {
//如果无法将节点状态由CONDITION修改为0,表示节点已在同步队列中,直接返回false
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//将节点node插入到同步队列中,p是原先同步队列尾节点,也是node节点的前一个节点
Node p = enq(node);
int ws = p.waitStatus;
// 如果前一个节点是已取消状态,或者不能将它设置成Node.SIGNAL状态。
// 就说明节点p之后也不会发起唤醒下一个node节点线程的操作,
// 所以这里直接调用 LockSupport.unpark(node.thread)方法,唤醒节点node所在线程
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}