Java 并发编程(三)锁与 AQS
本文 JDK 对应的版本为 JDK 13
由于传统的 synchronized
关键字提供的内置锁存在的一些缺点,自 JDK 1.5 开始提供了 Lock
接口来提供内置锁不具备的功能。显式锁的出现不是为了替代 synchronized
提供的内置锁,而是当内置锁的机制不适用时,作为一种可选的高级功能
内置锁与显式锁
内置锁于显式锁的比较如下表:
类别 | synchronized | Lock |
---|---|---|
存在层次 | Java的关键字 | 是一个类 |
锁的释放 | 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 |
在finally中必须释放锁, 不然容易造成线程死锁 |
锁的获取 | 假设A线程获得锁,B线程等待。 如果A线程阻塞,B线程会一直等待 |
Lock有多个锁获取的方式 |
锁状态 | 无法判断 | 可以判断 |
锁类型 | 可重入 不可中断 非公平 |
可重入 可判断 可公平(两者皆可) |
性能 | 少量同步 | 大量同步 |
显式锁的基本使用
Lock
的定义如下:
public interface Lock {
// 显式地获取锁
void lock();
// 可中断地获取锁,与 lock() 方法的不同之处在于在锁的获取过程可以被中断
void lockInterruptibly() throws InterruptedException;
// 以非阻塞的方式获取锁,调用该方法将会立即返回,如果成功获取到锁则返回 true,否则返回 false
boolean tryLock();
/* 带时间参数的 tryLock,
有三种情况:在规定时间内获取到了锁;在规定的时间内线程被中断了;在规定的时间内没有获取到锁
*/
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 释放锁
void unlock();
/*
获取 “等待/通知” 组件,该组件和当前的线程绑定,当前的线程只有获取到了锁,
才能调用该组件的 wait 方法,而调用之后,当前线程将会释放锁
*/
Condition newCondition();
}
常用的 Lock
的实现类为 java.util.concurrent.locks.ReentrantLock
,使用的示例如下:
private final static Lock lock = new ReentrantLock();
static int value = 0;
static class Demo implements Runnable {
@Override
public void run() {
lock.lock();
try {
value++;
} finally { // 一定要讲解锁操作放入到 finally 中,否则有可能会造成死锁
lock.unlock();
}
}
}
ReentrantLock
是基于 java.util.concurrent.locks.AbstractQueuedSynchronizer
的具体子类来实现同步的,这个类也被称为 AQS
,是 JUC
中实现 Lock
最为核心的部分
AQS
构建同步类
使用 AQS
构建同步类时获取锁和释放锁的标准形式如下:[1]
boolean acquire() throws InterruptedException {
while (当前状态不允许获取操作) {
if (需要阻塞获取请求) {
如果当前线程不在队列中,则将其插入队列
阻塞当前线程
} else {
返回失败
}
}
可能更新同步器的状态
如果线程位于队列中,则将其移出队列
返回成功
}
void release () {
更新同步器状态
if (新的状态允许某个阻塞的线程获取成功) {
解除队列中一个或多个线程的阻塞状态
}
}
对于支持独占式的同步器,需要实现一些
protected
修饰的方法,包括tryAcquire
、tryRelease
、isHeldExclusively
等;对于支持共享式的同步器,应该实现的方法有
tryAcquireShared
、tryReleaseShared
等
AQS
的acquire
、acquireShared
和release
、releaseShared
等方法都将调用这些方法在子类中带有的前缀try
的版本来判断某个操作能否被执行。在同步器的子类中,可以根据其获取操作和释放操作的语义,使用
getState
、setState
以及compareAndSetState
来检查和更新状态,并根据返回的状态值来告知基类 “获取” 和 “释放” 同步的操作是否是成功的。
源码解析
AQS
的类结构图如下:
类属性分析
AQS
实例对象的属性AQS
中存在非static
的字段如下(static
字段没有分析的必要):// 头节点,即当前持有锁的线程
private transient volatile Node head; // 阻塞队列的尾结点,每个新的节点进来都会插入到尾部
private transient volatile Node tail; /*
代表锁的状态,0 表示没有被占用,大于 0 表示有线程持有当前的锁
这个值可以大于 1,因为锁是可重入的,每次重入时都会将这个值 +1
*/
private volatile int state; /*
这个属性继承自 AbstractOwnableSynchronizer,
表示当前持有独占锁的线程
*/
private transient Thread exclusiveOwnerThread;
队列节点对象的属性
static final class Node {
// 标记当前的节点处于共享模式
static final Node SHARED = new Node();
// 表示当前的节点处于独占模式
static final Node EXCLUSIVE = null;
// 这个值表示当前节点的线程已经被取消了
static final int CANCELLED = 1;
// 表示当前节点的下一个节点需要被唤醒
static final int SIGNAL = -1;
// 表示当前节点在等待一个条件
static final int CONDITION = -2;
// 表示下一个 acquireShared 应当无条件地传播
static final int PROPAGATE = -3; /*
当前节点的等待状态,取值为上面的
CANCELLED、SIGNAL、CONDITION、PROPAGATE 或者 0
*/
volatile int waitStatus;
// 当前节点的前节点
volatile Node prev;
// 当前节点的下一个节点
volatile Node next;
// 当前节点存储的线程
volatile Thread thread;
// 链接到下一个等待条件的节点(条件队列),或者是特殊值为 SHARED 的节点
Node nextWaiter;
}
最后得到的阻塞队列如下图所示:
[2]
注意,这里的阻塞队列不包含头结点 head
具体分析
acquire(int arg)
该方法位于
java.util.concurrent.locks.AbstractQueuedSynchronizer
中,具体对应的源代码如下:public final void acquire(int arg) {
/*
如果 tryAcquire(arg) 成功了(即尝试获取锁成功了),那么就直接获取到了锁
否则,就需要调用 acquireQueued 方法将这个线程放入到阻塞队列中
*/
if (!tryAcquire(arg) &&
// 如果尝试获取锁没有成功,那么久将当前的线程挂起,放入到阻塞队列中
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire(arg)
对应的源代码如下:// AbstractQueuedSynchronizer 中定义的。。。
protected boolean tryAcquire(int arg) { // 在 AbstractQueuedSynchronizer 中定义的模版方法,需要具体的子类来实现
throw new UnsupportedOperationException();
}
为了简化这个过程,以
ReentrantLock
的FairSync
为例查看具体的实现:// ReentrantLock.FairSync。。。
@ReservedStackAccess
/*
尝试直接获取锁,返回值为 boolean,表示是否获取到锁
返回为 true: 1.没有线程在等待锁 2.重入锁,线程本来就持有锁,因此可以再次获取当前的锁
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { // state 为 0 表示此时没有线程持有锁
/*
当前的锁为公平锁(FairSync),因此即使当前锁是可以获取的,
但是需要首先检查是否已经有别的线程在等待这个锁
*/
if (!hasQueuedPredecessors() &&
/*
如果没有线程在等待,那么则尝试使用 CAS 修改状态获取锁,如果成功,则获取到当前的锁
如果使用 CAS 获取锁失败,那么就说明几乎在同一时刻有个线程抢先获取了这个锁
*/
compareAndSetState(0, acquires)) {
// 到这里就已经获取到锁了,标记一下当前的锁,表示已经被当前的线程占用了
setExclusiveOwnerThread(current);
return true;
}
}
/*
如果已经有线程持有了当前的锁,那么首先需要检测一下是不是当前线程持有的锁
如果是当前线程持有的锁,那么就是一个重入锁,需要对 state 变量 +1
否则,当前的锁已经被其它线程持有了,获取失败
*/
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
现在再回到
acquire
方法,如果trAcquire(arg)
成功获取到了锁,那么就是成功获取到了锁,直接返回即可;如果tryAcquire(arg)
获取锁失败了,则再执行acquireQueued
方法将当前线程放入到阻塞队列尾部在那之前,首先会执行
acquireQueued
方法中调用的addWaiter(Node.EXCLUSIVE)
方法,具体的源代码如下:// AbstractQueuedSynchronizer /*
这个方法的作用是将当前的线程结合给定的 mode 组合成为一个 Node,以便插入到阻塞队列的末尾
结合当前的上下文,传入的 mode 为 Node.EXCLUSIVE,即独占锁的模式
*/
private Node addWaiter(Node mode) {
Node node = new Node(mode); for (;;) { // 注意这里的永真循环。。。
Node oldTail = tail;
/*
如果尾结点不为 null,则使用 CAS 的方式将 node 插入到阻塞队列的尾部
*/
if (oldTail != null) {
node.setPrevRelaxed(oldTail); // 设置当前 node 的前驱节点为原先的 tail 节点
if (compareAndSetTail(oldTail, node)) { // CAS 的方式设置尾结点
oldTail.next = node;
return node; // 返回当前的节点
}
} else {
// 如果当前的阻塞队列为空的话,那么首先需要初始化阻塞队列
initializeSyncQueue();
}
}
} // 初始化阻塞队列对应的源代码如下
private final void initializeSyncQueue() {
Node h;
// 依旧是使用 CAS 的方式,这里的 h 的初始化为延迟初始化
if (HEAD.compareAndSet(this, null, (h = new Node())))
tail = h;
}
之后就是执行
acquireQueued
方法了,对应的源代码如下:// AbstractQueuedSynchronizer /*
此时的参数 node 已经经过 addWaiter 的处理,已经被添加到阻塞队列的末尾了
如果 acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 调用之后返回 true,那么就会执行 acquire(int arg) 方法中的 selfInterrupt() 方法 这个方法是比较关键的部分,是真正处理线程挂起,然后被唤醒去获取锁,都在这个方法中定义
*/
final boolean acquireQueued(final Node node, int arg) {
boolean interrupted = false;
try {
for (;;) { // 注意这里的永真循环
// predecessor() 返回的是当前 node 节点的前驱节点
final Node p = node.predecessor(); /*
p == head 表示当前的节点虽然已经进入到了阻塞队列,但是是阻塞队列中的第一个元素(阻塞队列不包含 head 节点)
因此当前的节点可以尝试着获取一下锁,这是由于当前的节点是阻塞队列的第一个节点,而 head 节点又是延迟初始化的,在这种情况下是有可能获取到锁的
*/
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return interrupted;
} /*
如果执行到这个位置,则说明 node 要么就不是队头元素,要么就是尝试获取锁失败
*/
if (shouldParkAfterFailedAcquire(p, node))
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
cancelAcquire(node);
if (interrupted)
selfInterrupt();
throw t;
}
} // parkAndCheckInterrupt() 对应的源代码
/*
该方法的主要任务是挂起当前线程,使得当前线程在此等待被唤醒
*/
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 该方法用于挂起当前线程
return Thread.interrupted();
}
shouldParkAfterFailedAcquire(p, node)
对应的源代码如下:// AbstractQueuedSynchronizer /*
这个方法的主要任务是判断当前没有抢到锁的线程是否需要阻塞
第一个参数表示当前节点的前驱节点,第二个参数表示当前线程的节点
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// 前驱节点正常,则需要阻塞当前线程节点
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true; /*
前驱节点的状态值大于 0 表示前驱节点取消了排队
如果当前的节点被阻塞了,唤醒它的为它的前驱节点,因此为了使得能够正常工作,
需要将当前节点的前驱节点设置为一个正常的节点,使得当前的节点能够被正常地唤醒
*/
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 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.
*/ /*
如果不满足以上两个条件,那么当前的 ws 的状态就只能为 0, -2, -3 了
在当前的上下文环境中,ws 的状态为 0,因此这里就是将当前节点的前驱节点的 ws 值设置为 Node.SIGNAL
*/
pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
} /*
本次执行到此处会返回 false,而 acquireQueued 中的永真循环将会再次进入这个方法
由于上面的一系列操作,当前节点的前驱节点一定是正常的 Node.SIGNAL,因此会在第一个 if 语句中直接返回 true
*/
return false;
}
release(int arg)
该方法用于释放当前获取到的锁,对应的具体的源代码如下:
// AbstractQueuedSynchronizer // 释放在独占模式中获取到的锁
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(arg)
对应的源代码如下:// AbstractQueuedSynchronizer // 很明显,这也是一个模版方法,需要具体子类来定义对应的实现
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
依旧以
ReentrantLock
为例,查看一下tryRelease(int arg)
的具体实现// ReentrantLock.Sync @ReservedStackAccess
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException(); boolean free = false; // 是否已经完全释放锁的标记 // 如果 c > 0,则说明获取的锁是一个重入锁,还没有完全释放
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
再回到
release(int arg)
方法中,如果是已经完全释放了锁,则执行后面的return false
语句,执行结束。如果没有完全释放锁,那么则会继续执行unparkSuccessor(h)
方法,对应的源代码如下:// AbstractQueuedSynchronizer // 唤醒后继节点
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;
if (ws < 0)
node.compareAndSetWaitStatus(ws, 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.
*/ /*
唤醒后继节点,但是可能后继节点取消了等待(即 waitStatus = Node.CANCELLED)
在这种情况下,将会从队尾向前查找,找到最靠近 head 的 waitStatus < 0 的节点
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
// 从队尾开始向前查找,找到第一个合适的节点
for (Node p = tail; p != node && p != null; p = p.prev)
if (p.waitStatus <= 0) // 可能排在前面的节点取消的可能性更大
s = p;
} if (s != null) // 唤醒这个合适的节点对应的线程
LockSupport.unpark(s.thread);
}
在释放了所有的锁之后,唤醒后继的一个还没有被取消的线程节点,然后唤醒它,唤醒之后的节点将恢复原来在
parkAndCheckInterrupt()
中的执行状态private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 被唤醒后将继续执行后面的代码
return Thread.interrupted(); // 此时应当是没有被中断的
}
再回到原先的
acquireQueued(node, arg)
方法,此时由于 head 已经释放了锁,而当前的 node 节点是距离 head 最近的一个有效的线程节点,因此它能够获取到锁,线程在获取锁之后再继续执行对应的代码逻辑
ConditionObject
ConditionObject
一般用于 “生产者—消费者” 的模式中,与基于Object
的 wait()
和 notifyAll()
实现的通信机制十分类似。
对应的 ConditionObject
的源代码如下:
public class ConditionObject implements Condition, java.io.Serializable {
// 条件队列的第一个节点
private transient Node firstWaiter;
// 条件队列的最后一个节点
private transient Node lastWaiter;
}
与前文的阻塞队列相对应,条件队列与阻塞队列的对应关系图如下所示:
[3]
具体解释:
- 条件队列和阻塞队列的节点,都是 Node 的实例对象,因为条件队列的节点最终会转移到阻塞队列中去
ReentrantLock
的实例对象可以通过多次调用newCondition()
方法来生成新的Condition
对象(最终由AQS
的具体子类对象生成)。在AQS
中,对于Condition
的具体实现为ConditionObject
,这个对象只有两个属性字段:firstWaiter
和lastWaiter
- 每个
ConditionObject
都有一个自己的条件队列,线程 1 通过调用Condition
对象的await
方法即可将当前的调用线程包装成为 Node 后加入到条件队列中,然后阻塞在条件队列中,不再继续执行后面的代码 - 调用
Condition
对象的signal()
方法将会触发一次唤醒事件,与Object
的notify()
方法类似。此时唤醒的是条件队列的队头节点,唤醒后会将firstWaiter
的节点移动到阻塞队列的末尾,然后在阻塞队列中等待获取锁,之后获取锁之后才能继续执行
await
方法
await
方法对应的源代码如下:
// AbstractQueuedSynchronizer.ConditionObject
/*
抛出 InterruptedException 表示这个方法是可以被中断的
这个方法会被阻塞,直到调用 signal 方法(singnal 和 singnalAll)唤醒或者被中断
*/
public final void await() throws InterruptedException {
// 按照规范,应该在最开始的位置就首先检测一次中断
if (Thread.interrupted())
throw new InterruptedException();
// 将当前的线程封装成 Node,添加到条件队列中
Node node = addConditionWaiter();
/*
释放锁,返回值是释放锁之前的 state 值
在调用 await 方法之前,当前的线程肯定是持有锁的,在这里需要释放掉当前持有的锁
*/
int savedState = fullyRelease(node);
int interruptMode = 0;
/*
isOnSyncQueue(node) 返回 true 表示当前的节点已经从条件队列转移到阻塞队列了
*/
while (!isOnSyncQueue(node)) {
/*
如果当前的节点不在阻塞队列中,那么将当前节点中的线程挂起,
直到通过调用 Condition 对象的 signal* 方法来唤醒它
*/
LockSupport.park(this);
// 线程被中断,因此需要退出当前的循环
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 进入阻塞队列之后,等待获取锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
addConditionWaiter()
对应的源代码如下:
// AbstractQueuedSynchronizer.ConditionObject
/*
将当前线程包装成一个 Node,插入的条件队列末尾
*/
private Node addConditionWaiter() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 当前 ConditionObject 中条件队列的尾节点
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
/*
如果尾结点的线程已经被取消了,那么就清除它
注意当前节点所处的队列为条件队列,因此每个节点的状态都应该是 Node.CONDITION
*/
if (t != null && t.waitStatus != Node.CONDITION) {
// 该方法会从前到后清除所有的不满足条件的节点
unlinkCancelledWaiters();
t = lastWaiter;
}
// 创建一个新的 Node,当前的 Node 的 waitStatus 为 Node.CONDITION
Node node = new Node(Node.CONDITION);
// 处理初始队列为空的情况
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
/*
清除当前 ConditionObject 的条件队列中所有 waitStatus 不为 CONDITION 的节点
*/
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
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;
}
}
fullyRelease(node)
对应的源代码如下:
// AbstractQueuedSynchronizer.ConditionObject
/*
该方法的主要目的是完全释放当前节点中线程持有的锁
之所以是完全释放,这是因为锁是可重入的
*/
final int fullyRelease(Node node) {
try {
/*
由于显式锁是可重入的,因此在调用 await() 时也必须再恢复到原来的状态
回忆一下 Node 节点中 state 属性代表的意义,如果 state > 0 表示当前持有的锁的数量
获取这个锁的数量,使得在进入阻塞队列中的 Node 能够再恢复到原来的状态
*/
int savedState = getState();
if (release(savedState)) // 参见上文有关 release 方法的介绍
return savedState;
throw new IllegalMonitorStateException();
} catch (Throwable t) {
/*
如果在释放锁的过程中失败了,那么就将这个节点的状态设置为 CANCELLED,
在之后的处理中会移除这个节点
*/
node.waitStatus = Node.CANCELLED;
throw t;
}
}
isOnSyncQueue(node)
对应的源代码:
// AbstractQueuedSynchronizer.ConditionObject
/*
判断当前的节点是否是从条件队列中转移到了阻塞队列,并且正在等待被唤醒
*/
final boolean isOnSyncQueue(Node node) {
/*
从条件队列中移动到阻塞队列中时,node 的 waitStatus 将会被设置为 0
如果 node 的 waitStatus 依旧为 Node.CONDITION,那么则说明它还在条件队列中
如果 node 的前驱节点为 null,那么也一定还在等待队列中(阻塞队列中每个节点都会有前驱节点)
*/
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
// 如果 node 都已经存在后继节点了,那么肯定在阻塞队列中了
if (node.next != null) // If has successor, it must be on queue
return true;
/*
* node.prev can be non-null, but not yet on queue because
* the CAS to place it on queue can fail. So we have to
* traverse from tail to make sure it actually made it. It
* will always be near the tail in calls to this method, and
* unless the CAS failed (which is unlikely), it will be
* there, so we hardly ever traverse much.
*/
/*
由于 CAS 在将条件队列中的节点移动到阻塞队列中时可能会失败,(具体可以查看 AQS 的入队方法)
此时当前节点的前驱节点不为 null,为了解决这个问题,
需要遍历阻塞队列来确保当前的节点确实是已经进入到了阻塞队列
*/
return findNodeFromTail(node);
}
// 对应的源代码。。。。
private boolean findNodeFromTail(Node node) {
// We check for node first, since it's likely to be at or near tail.
// tail is known to be non-null, so we could re-order to "save"
// one null check, but we leave it this way to help the VM.
/*
从尾结点开始遍历搜索节点,检查是否在阻塞队列中
*/
for (Node p = tail;;) {
if (p == node)
return true;
if (p == null)
return false;
p = p.prev;
}
}
signal
方法
signal
方法用于唤醒正在等待的线程,在当前的环境下,signal
的主要目的是唤醒在条件队列中线程节点,将它们移动到阻塞队列中
AQS
中对于 signal()
方法的实现如下:
// AbstractQueuedSynchronizer.ConditionObject
/*
移动等待了最久的线程,将它从条件队列移动到阻塞队列
*/
public final void signal() {
// 调用 signal 的线程必须持有当前的独占锁
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 一般第一个节点就被视作 “等待最久” 的线程
Node first = firstWaiter;
// 真正唤醒线程
if (first != null)
doSignal(first);
}
/*
从前往后查找第一个符合条件的节点(有的线程可能已经被取消或者被中断了)
*/
private void doSignal(Node first) {
do {
// 移除第一个节点
/*
如果移除第一个节点之后条件队列中不再有节点了,那么需要将 lastWaiter
节点也置为 null
*/
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
// 移除该节点和队列之间的连接关系
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null); // 遍历队列,直到找到第一个满足条件的节点
}
// AbstractQueuedSynchronizer
/*
将条件队列中的节点移动到阻塞队列
返回 true 表示转移成功,false 则表示这个节点在调用 signal 之前就被取消了
*/
final boolean transferForSignal(Node node) {
/*
* If cannot change waitStatus, the node has been cancelled.
*/
/*
CAS 修改当前节点的 waitStatus如果失败,说明该节点所在的线程已经被取消了
*/
if (!node.compareAndSetWaitStatus(Node.CONDITION, 0))
return false;
/*
* Splice onto queue and try to set waitStatus of predecessor to
* indicate that thread is (probably) waiting. If cancelled or
* attempt to set waitStatus fails, wake up to resync (in which
* case the waitStatus can be transiently and harmlessly wrong).
*/
/*
这里的的 p 是 node 在进入阻塞队列之后的前驱节点
*/
Node p = enq(node); // 以自旋的方式进入阻塞队列的队尾
int ws = p.waitStatus;
/*
ws > 0 表示 node 在阻塞队列中的前驱节点取消了等待,直接唤醒 node 对应的线程
ws <= 0,那么在进入阻塞队列的时候需要将 node 的前驱节点设置为 SIGNAL,表示前驱节点会唤醒后继节点
*/
if (ws > 0 || !p.compareAndSetWaitStatus(ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
在唤醒线程之后,再查看 await()
方法中的逻辑:
public final void await() throws InterruptedException {
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this); // 当前线程被挂起
// 挂起后的后置处理
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// ……………………………………………………
}
interruptMode
可选的值如下:
REINTERRUPT
:在await
方法返回的时候,需要重新设置中断状态THROW_IE
:代表await
方法返回的时候,需要抛出InterruptedException
异常- 0:表示在
await
方法调用期间,该线程没有被中断
线程被唤醒之后的第一步操作是调用 checkInterruptWhileWaiting(node)
检查当前的线程是否被中断了,对应的源代码如下:
// AbstractQueuedSynchronizer.ConditionObject
// 返回对应 interruptMode 中的三个值
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
// AbstractQueuedSynchronizer
/*
只有线程被中断的情况下,才会调用此方法
如果需要的话,将这个已经取消等待的节点转移到阻塞队列
返回 true :如果此线程在 signal 调用之前被取消
*/
final boolean transferAfterCancelledWait(Node node) {
/*
CAS 将节点状态设置为 0
如果这一步 CAS 成功,则说明是调用 signal 方法之前就已经发生了中断,
因为 signal 方法会将条件队列的首个节点的 waitStatus 置为 0 再移动到阻塞队列
如果不为 0 则说明要么被取消了,要么还没有调用 signal 进行处理
*/
if (node.compareAndSetWaitStatus(Node.CONDITION, 0)) {
enq(node); // 可以看到,即使被中断了,依旧会将这个节点放入到阻塞队列
return true;
}
/*
* If we lost out to a signal(), then we can't proceed
* until it finishes its enq(). Cancelling during an
* incomplete transfer is both rare and transient, so just
* spin.
*/
/*
如果会走到这,那么一定是 CAS 设置 node 的 waitStatus 失败了,
即是在调用 signal 之后发生的中断
signal 会将节点移动从条件队列移动到阻塞队列,但是可能由于某些原因还没有移动完成,
因此在这里通过自旋的方式等待其完成
*/
while (!isOnSyncQueue(node))
Thread.yield();
return false;
}
可以看到,即使发生了中断,依旧会完成将 node 从条件队列转移到阻塞队列
唤醒线程后继续向下走,对应的源代码如下:
public final void await() throws InterruptedException {
// 省略部分代码
/*
当 acquireQueued 方法返回 true 时,说明线程已经被中断了
如果此时 interruptMode 为 THROW_IE 的话,说明在调用 signal 方法之前就已经被中断了
在这种情况下,将 interruptMode 置为 REINTERRUPT,以便之后重新中断
*/
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
}
继续向下执行,对应的源代码:
/*
在调用 signal 时会断开当前节点和后继节点之间的连接,
如果此时后继节点不为 null,说明是被中断的,同样需要断开这个节点在条件队列中的连接
*/
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
// 处理中断
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
reportInterruptAfterWait(interruptMode)
对应的源代码:
// AbstractQueuedSynchronizer.ConditionObject
// 处理中断
private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {
// 根据 interruptMode 对中断进行不同的处理
if (interruptMode == THROW_IE)
throw new InterruptedException();
else if (interruptMode == REINTERRUPT)
selfInterrupt();
}
共享模式
获取锁
主要对应 AQS
中的 acquireSharedInterruptibly
方法,具体的定义如下:
// AbstractQueuedSynchronizer
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
// 检测当前线程是否被中断
if (Thread.interrupted())
throw new InterruptedException();
// 由子类具体实现的模板方法
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
doAcquireSharedInterruptibly(arg)
方法对应的源代码:
// AbstractQueuedSynchronizer
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
// 同样地,将当前线程封装,然后加入阻塞队列
final Node node = addWaiter(Node.SHARED);
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
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
释放锁
具体对应 AQS
中的 releaseShared(int arg)
方法,具体的定义如下:
// AbstractQueuedSynchronizer
public final boolean releaseShared(int arg) {
// AQS 定义的模板方法,由具体的子类来实现
if (tryReleaseShared(arg)) {
doReleaseShared(); // 关键的方法
return true;
}
return false;
}
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
/*
每个 Node 在加入到阻塞队列的时候,都会将前驱节点的 waitStatus
设置为 Node.SIGNAL
*/
if (ws == Node.SIGNAL) {
if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
continue; // loop to recheck cases
/*
唤醒 head 的后继节点,也就是阻塞队列中的第一个节点
*/
unparkSuccessor(h);
}
else if (ws == 0 &&
!h.compareAndSetWaitStatus(0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
再回到 doAcquireSharedInterruptibly
:
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
/*
2. 由于前一个线程持有的锁已经被释放了,当前线程已经被唤醒,继续执行
*/
int r = tryAcquireShared(arg);
if (r >= 0) {
// 3. 然后会进入这个方法
setHeadAndPropagate(node, r);
p.next = null; // help GC
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
/*
1. 线程在这个方法中被挂起,因此当线程被唤醒时也会从这个方法中返回
假设当前的线程没有被中断
*/
parkAndCheckInterrupt())
throw new InterruptedException();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
setHeadAndPropagate(node, r)
对应的源代码如下:
// AbstractQueuedSynchronizer
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
/*
唤醒当前 node 节点之后的后继节点
*/
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared(); // 释放当前节点持有的锁
}
}
再回到 doReleaseShared
方法:
private void doReleaseShared() {
for (;;) {
Node h = head;
/*
h == null 表示当前的阻塞队列为空;h == tail 表示头节点可能是刚刚初始化的头结点
或者 h 只是一个普通的线程节点,但是由于它已经被唤醒了,说明阻塞队列中已经没有节点了
因此不再需要唤醒后继节点
*/
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
/*
在当前上下文环境下 CAS 可能会失败
*/
if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
continue;
// CAS 成功则唤醒后继节点
unparkSuccessor(h);
}
else if (ws == 0 &&
/*
这里也会对头节点的 waitStatus 修改,因此上面中的 CAS 可能会失败
*/
!h.compareAndSetWaitStatus(0, Node.PROPAGATE))
continue;
}
/*
如果唤醒的线程已经占领了 head,那么再循环,否则,退出当前循环
*/
if (h == head)
break;
}
}
处理中断
在 acquireQueued
方法的执行过程中,对于中断的处理代码如下:
if (shouldParkAfterFailedAcquire(p, node))
interrupted |= parkAndCheckInterrupt();
// 重点在于 parkAndCheckInterrupt 方法
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted(); // 该方法会清除中断标记
}
在 acquireQueued
中,只是单纯地使用一个变量 interrupted
来标记是否被中断过,也就是说,在 acquireQueued
中,并不会处理中断,即使当前的线程节点被中断了,它依旧会尝试去获取锁
具体对于中断的处理由具体的实现来定义,可以忽略这个中断,也可以抛出一个异常
以 ReentrantLock
对于 lockInterruptibly()
的实现为例,具体的实现代码如下:
// ReentrantLock
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1); // 该方法为 AQS 中定义的方法
}
AQS
中对于 acquireInterruptibly
方法的定义如下:
// AbstractQueuedSynchronizer
public final void acquireInterruptibly(int arg)
throws InterruptedException {
/*
在 parkAndCheckInterrupt() 方法中通过 Thread.interrupted()
方法清除了线程的中断标记,因此不会走这
*/
if (Thread.interrupted())
throw new InterruptedException();
// 继续往下走
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
// doAcquireInterruptibly 方法的定义如下
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return;
}
/*
关键在这,与不抛出 InterruptedException 的相比,最大的区别就在于对于中断的处理,
上文的 acquireQueued 则只是将中断标记返回给调用者而不是显式地抛出一个异常
*/
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} catch (Throwable t) {
cancelAcquire(node); // 取消该节点去获取锁的行为
throw t; // 传递捕获到的异常
}
}
cancelAcquire(node)
对应的源代码如下:
// AbstractQueuedSynchronizer
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
node.thread = null;
// Skip cancelled predecessors
/*
找到符合条件的前驱节点,将不符合条件的前驱节点都清除
*/
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary, although with
// a possibility that a cancelled node may transiently remain
// reachable.
Node predNext = pred.next;
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
node.waitStatus = Node.CANCELLED;
/*
一般的链表清除节点工作
*/
// If we are the tail, remove ourselves.
if (node == tail && compareAndSetTail(node, pred)) {
pred.compareAndSetNext(predNext, null);
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && pred.compareAndSetWaitStatus(ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
pred.compareAndSetNext(predNext, next);
} else {
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
参考:
[1] 《Java 并发编程实战》
[2] https://javadoop.com/post/AbstractQueuedSynchronizer
[3] https://javadoop.com/post/AbstractQueuedSynchronizer-2
Java 并发编程(三)锁与 AQS的更多相关文章
- Java并发编程:锁的释放
Java并发编程:锁的释放 */--> code {color: #FF0000} pre.src {background-color: #002b36; color: #839496;} Ja ...
- Java并发编程三个性质:原子性、可见性、有序性
并发编程 并发程序要正确地执行,必须要保证其具备原子性.可见性以及有序性:只要有一个没有被保证,就有可能会导致程序运行不正确 线程不安全在编译.测试甚至上线使用时,并不一定能发现,因为受到当时的 ...
- 【Java并发编程三】闭锁
1.什么是闭锁? 闭锁(latch)是一种Synchronizer(Synchronizer:是一个对象,它根据本身的状态调节线程的控制流.常见类型的Synchronizer包括信号量.关卡和闭锁). ...
- Java并发编程之锁机制
锁分类 悲观锁与乐观锁 悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改.因此对于同一个数据的并发操作,悲观锁采取加锁的形式.悲观的认为,不加锁的并发操作一定会出问题 ...
- java并发编程:锁的相关概念介绍
理解同步,最好先把java中锁相关的概念弄清楚,有助于我们更好的去理解.学习同步.java语言中与锁有关的几个概念主要是:可重入锁.读写锁.可中断锁.公平锁 一.可重入锁 synchronized和R ...
- Java并发编程-各种锁
安全性和活跃度通常相互牵制.我们使用锁来保证线程安全,但是滥用锁可能引起锁顺序死锁.类似地,我们使用线程池和信号量来约束资源的使用, 但是缺不能知晓哪些管辖范围内的活动可能形成的资源死锁.Java应用 ...
- Java并发编程 (三) 项目准备
个人博客网:https://wushaopei.github.io/ (你想要这里多有) 一.案例环境初始化 1.环境搭建与准备 Spring Boot 项目,https://start.spr ...
- Java 并发编程(三):如何保证共享变量的可见性?
上一篇,我们谈了谈如何通过同步来保证共享变量的原子性(一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行),本篇我们来谈一谈如何保证共享变量的可见性(多个线程访问同一个变 ...
- Java并发编程(三):ReentrantLock
ReentrantLock是可以用来代替synchronized的.ReentrantLock比synchronized更加灵活,功能上面更加丰富,性能方面自synchronized优化后两者性能没有 ...
- 【Java并发编程实战】----- AQS(二):获取锁、释放锁
上篇博客稍微介绍了一下AQS,下面我们来关注下AQS的所获取和锁释放. AQS锁获取 AQS包含如下几个方法: acquire(int arg):以独占模式获取对象,忽略中断. acquireInte ...
随机推荐
- Redis最常见的5种应用场景
Redis作为当今最流行的内存数据库,已经成为服务端加速的必备工具之一.对于Redis为什么那么快?以及Redis采用单线程,但为什么反而获得更高的性能的疑问,在之前的Redis为什么那么快?一文中, ...
- Python面向对象——Mixin机制、重载、多态与鸭子类型、绑定与非绑定方法、Python常见的内置函数
文章目录 内容回顾 Mixin机制 1.什么是Mixin 2.Mixin来源 3.定义及优点 4.在python中的应用 5.在Django项目中的应用 重载(在子类派生的新方法中如何重用父类的功能) ...
- Go with Protobuf
原文在这里. 本教程为 Go 程序员提供了使用Protocol buffer的基本介绍. 本教程使用proto3向 Go 程序员介绍如何使用 protobuf.通过创建一个简单的示例应用程序,它向你展 ...
- 使用Github Action实现构建、发布到 nuget.org
使用Github Action实现构建.发布到 nuget.org GitHub Actions是GitHub提供的持续集成和持续部署(CI/CD)工具,它能够自动化构建.测试和部署你的项目.在这篇教 ...
- Sell Pigs 题解
Sell Pigs 双倍经验 题目大意 有 \(n\) 个顾客前来买猪,共有 \(m\) 个猪圈,每个顾客携带着某一些猪圈的钥匙,需要买一定数量的猪.在顾客买完后,我们可以将打开的猪圈中的猪随意移动, ...
- 使用 Ant Design Vue 你可能会遇到的14个问题
公司有一个新需求,在原来项目基础上开发,项目中使用 Ant Design Vue,版本是 1.X ,在此记录下遇到的问题:对于没有使用过或者使用程度不深的同学来说,希望可以帮助你在开发中遇到问题时有个 ...
- Redis 技术整理
认识Redis Redis官网:https://redis.io/ Redis诞生于2009年全称是Remote Dictionary Server 远程词典服务器,是一个基于内存的键值型NoSQL数 ...
- 整理unity资料
https://www.cnblogs.com/fly-100/p/3910515.html 协同的概念介绍
- 2023 PolarD&N靶场通关笔记 Crypto
简单类: keyboard2.0 21 31 41 53 63 73 62 72 82 81 91 01 51 61 71 提示:解出来的内容转换为md5值后加上flag{} 数字被分成五组,每组包含 ...
- 题解 CF742B
题目大意: 给定 \(n\) 个数,找数对使其异或值为 \(k\),求满足这样数对的个数. 题目分析: 考验位运算功底的题目(实际上也不是很难),主要运用到了下列性质: \[\begin{aligne ...