我们在上一节有分析过AQS的一些基础原理及独占锁,接下来继续分析AQS的共享锁,共享锁的获取与独占锁的获取流程大概上是相似的,在接下来的源码分析中会发现这个问题。
获取锁-acquireShared
- 共享模式下获取锁,忽略中断(即使中断,线程依然会获取锁)
- 源码分析
public class AbstractQueuedSynchronizer {
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0) // 获取共享锁,有子类实现
doAcquireShared(arg);
}
}
复制代码
tryAcquireShared由子类实现并确认最终获取锁的逻辑,在获取锁失败之后,通过doAcquireShared方法以自旋的方式获取锁
public class AbstractQueuedSynchronizer {
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) { // 前驱节点为head
int r = tryAcquireShared(arg); // 再次尝试获取锁
if (r >= 0) { // 获取锁成功
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,此方法 的作用主要是完成当当前线程获取锁成功的出队以及唤醒后继节点(如果存在的话).其中finally的目的是为了在执行doAcquireShared方法的过程中如果出现异 常,取消该线程获取锁的尝试,并从等待队列中清除该线程。
- 锁获取过程
获取锁-acquireSharedInterruptibly
- 在共享模式下获取锁,若线程中断则中止
- 源码分析
public class AbstractQueuedSynchronizer {
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted()) // 中断抛出异常
throw new InterruptedException();
if (tryAcquireShared(arg) < 0) // 获取共享锁
doAcquireSharedInterruptibly(arg); // 可中断的获取锁
}
}
复制代码
与获取可中断独占锁的逻辑一致,只是由tryAcquire方法变成了tryAcquireShared
public class AbstractQueuedSynchronizer {
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED); // 添加到等待队列
boolean failed = true; // 失败标记
try {
for (;;) { // 自旋
final Node p = node.predecessor(); // 前驱节点
if (p == head) { // 是否为头部节点
int r = tryAcquireShared(arg); // 再次尝试获取锁
if (r >= 0) { // 成功
setHeadAndPropagate(node, r); // 出队并唤醒后继节点
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) && // 挂起判断
parkAndCheckInterrupt()) // 挂起并进行中断校验
throw new InterruptedException(); // 抛出中断异常
}
} finally {
if (failed) // 处理异常导致的获取失败
cancelAcquire(node); // 取消该线程获取锁的尝试并从等待队列中删除
}
}
}
复制代码
与中断独占锁相比,其进入等待队列、自旋、线程挂起的方式是一致的,不一致的是其在自旋过程中获取锁成功时的操作setHeadAndPropagate,此方法 的作用主要是完成当当前线程获取锁成功的出队以及唤醒后继节点(如果存在的话).其中finally的目的是为了在执行doAcquireShared方法的过程中如果出现异 常,取消该线程获取锁的尝试,并从等待队列中清除该线程。
- 锁获取过程
获取锁-tryAcquireSharedNanos(int arg, long nanosTimeout)
- 以独占模式在给定到时间内获取锁,超时失败,如果被中断,锁获取终止
- 源码分析
public class AbstractQueuedSynchronizer {
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted()) // 中断检查
throw new InterruptedException();
return tryAcquireShared(arg) >= 0 || // 尝试获取共享锁
doAcquireSharedNanos(arg, nanosTimeout); // 在给定的时间内获取锁
}
}
复制代码
首先判断线程是否中断,中断则抛出异常,同时擦除中断标记;其次通过tryAcquireShared尝试获取锁,成功返回true,失败返回false;最后调用 doAcquireSharedNanos方法在设定到时间内获取锁
public class AbstractQueuedSynchronizer {
private boolean doAcquireSharedNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout; // 获取锁的最后期限
final Node node = addWaiter(Node.SHARED); // 添加到等待队列
boolean failed = true; // 失败标记
try {
for (;;) { // 自旋
final Node p = node.predecessor(); // 前驱节点
if (p == head) { // 是否为头部节点
int r = tryAcquireShared(arg); // 尝试获取锁
if (r >= 0) { // 成功
setHeadAndPropagate(node, r); // 出队并唤醒后继节点(存在的话)
p.next = null; // help GC
failed = false;
return true;
}
}
nanosTimeout = deadline - System.nanoTime(); // 剩余时间
if (nanosTimeout <= 0L) // 小于0,表示已经过了最后期限,返回false
return false;
if (shouldParkAfterFailedAcquire(p, node) && // 是否挂起判断
nanosTimeout > spinForTimeoutThreshold) // 剩余时间是否大于自旋时间
LockSupport.parkNanos(this, nanosTimeout); // 线程挂起
if (Thread.interrupted()) // 线程中断判断
throw new InterruptedException(); // 抛出异常
}
} finally {
if (failed) // 线程在执行过程中出现异常的后续操作
cancelAcquire(node); // 取消锁获取并从等待队列中清除该线程
}
}
}
复制代码
在进入自旋前获取超时时间、构造当前线程节点并添加到等待队列,然后在进入自旋。如果前驱节点为head,再次尝试获取锁,成功的话当前节点出队并获取锁, 失败则计算剩余等待时间,其次判断是否需要将线程挂起及剩余等待大于自旋时间时,则将线程挂起,否则将在下一次自旋时返回false,同时取消当前线程获取 锁。当线程被挂起后,如果因线程中断被唤醒,则抛出异常,同时取消当前线程获取锁。 3. 锁获取过程
释放锁-releaseShared
- 释放共享锁
- 源码分析
public class AbstractQueuedSynchronizer {
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) { // 此方法有子类实现,用于判断是否可以唤醒后继节点
doReleaseShared(); //
return true;
}
return false;
}
}
复制代码
tryReleaseShared用于确认锁是否可释放,可释放的话执行doReleaseShared释放锁
- 释放流程
setHeadAndPropagate-出队并唤醒后继节点分析
public class AbstractQueuedSynchronizer {
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node); // 设置head,可以理解为出队
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared()) // 当前节点的后继节点为null或当前节点的的nextWaiter为SHARED
doReleaseShared();
}
}
}
复制代码
首先是出队操作,然后根据条件判断是否需要通过doReleaseShared方法唤醒后继节点。
doReleaseShared--释放锁
public class AbstractQueuedSynchronizer {
private void doReleaseShared() {
for (;;) { // 循环释放
Node h = head;
if (h != null && h != tail) { // 头部节点不为null且头部节点!=尾部节点,意思是等待队列中有线程在等待
int ws = h.waitStatus; // 获取waitStatus
if (ws == Node.SIGNAL) { // 如果为-1
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) // 设置头部节点中的waitStatus为0,失败则跳过
continue; // loop to recheck cases
unparkSuccessor(h); // 释放锁,唤醒后继节点
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) // 设置头部节点中的waitStatus为-3,失败则跳过
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
}
复制代码
与独占锁释放锁不同的是,共享锁在执行一次锁释放操作的时候,将通过循环的方法唤醒所有在等待队中的线程,直到所有线程都被唤醒.
问题与总结
什么是独占锁和共享锁?
- 独占锁:当一个线程持有锁时,其他线程必须等待锁释放后才能重新去获取锁
- 共享锁:同一把锁可允许多个线程同时持有
什么是可中断锁和不可中断锁?
- 可中断锁:一个线程在获取锁或在等待获取锁的过程中如果被中断,将会抛出InterruptedException,并会取消其获取锁的动作,如果在等待队列中,将从等待队列中将其清除
- 不可中断锁:一个线程在获取锁或在等待获取锁的过程中如果被中断,仍将继续获取锁
AQS中共享锁和独占锁的释放有啥区别?
独占锁:根据FIFO规则,唤醒等待队列中第一个有效的节点 共享锁:从头部节点开始,依次唤醒所有等待节点。