ConditionObject
ConditionObject作为AQS的内部类,条件变量对象,该类实现了Condition接口,而Condition接口提供了await()、signal()等方法用于替代 Object 的监视器方法(wait()、notify()、notifyAll()),用于线程间的协调
Condition接口是里面的方法具体实现逻辑在ConditionObject类中实现
ConditionObject类结构
//一个实现Condition接口并序列化的类
public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;
//条件队列中的第一个节点
private transient Node firstWaiter;
//条件队列中的最后一个节点
private transient Node lastWaiter;
// 模式标识:不抛出异常 并清除中断标识 相当于一个记录标识(代表发生过中断)
private static final int REINTERRUPT = 1;
//模式标识:抛出InterruptedException
private static final int THROW_IE = -1;
//无参构造
public ConditionObject() { };
//实现重写Condition接口的6个方法
public final void await() throws InterruptedException {};
public final long awaitNanos(long nanosTimeout)throws InterruptedException {};
public final boolean awaitUntil(Date deadline)throws InterruptedException {};
public final boolean await(long time,TimeUnit unit)throws InterruptedException;
public final void signal() {};
public final void signalAll() {};
//类中自己定义的其他方法
-------------------------------------
ConditionObject类中工具类性质的方法
ConditionObject类是重写了Condition接口的方法,这6个重写的方法之外的在ConditionObject类中自己定义的几个重要方法,本质上是为6个接口重写方法服务的,先解读这些方法对后面解读6个重写方法打基础
addConditionWaiter()
将当前线程封装为节点并加入条件队列,等待被唤醒
private Node addConditionWaiter() {
// 获取当前条件队列尾节点
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters(); // 清理已取消的节点
t = lastWaiter;
}
// 创建新节点,状态为CONDITION(条件队列节点状态为 Node.CONDITION)
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)// 队列为空,头尾指针均指向新节点
firstWaiter = node;
else//队列非空:将新节点链接到当前尾节点之后,并更新尾指针。
t.nextWaiter = node; // 链接到队尾(单向链表结构(通过nextWaiter链接)
lastWaiter = node; // 更新尾指针
return node;
}
这里是将需要主动等待的线程放到一个队列管理,这个队列存放的都是因为不满足某种执行条件主动放入的节点,称为条件队列,条件队列特点从上面代码总结
1.单向链表结构(通过nextWaiter链接)
2.节点状态都为 Node.CONDITION
fullyRelease()
当前线程一次性完全释放锁
final int fullyRelease(Node node) {
boolean failed = true;
try {
// 获取同步状态值
int savedState = getState();
//将同步状态值传递给释放锁的方法,这里就考虑了重入锁的状态 比如savedState =3
if (release(savedState)) {
failed = false;
return savedState;// 返回释放前的状态,用于后续恢复
} else {
//若线程未持有锁时调用此方法,会抛出 IllegalMonitorStateException。
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
这里面释放锁使用的是release方法–> tryRelease方法,fullyRelease 调用的 tryRelease 方法属于锁的具体实现类(如 ReentrantLock.Sync 或 ReentrantReadWriteLock.Sync),具体引用哪个类方法看具体锁的类型,是独占锁还是读写锁
但实现一次性释放所有锁方式一致:通过传递state原值,调用release(savedState),原子性清零
// 通过fullyRelease 传递来的releases可能是1 也可能大于1 如3
tryRelease(int releases)
{
//总归保证得到的c的值为0 这样实现一次性释放所有层级
int c = getState() - releases;
}
这一步骤就是使用原子性清零,计算锁状态值为0方法,让当前线程释放出锁
同时,该方法也没有在共享模式的实现类中体现,只在独占模式的实现类中体现,使用condition只适用于独占模式,其实也好理解
- 独占模式是同一时间只有一个线程可以持有锁。state表示线程重入锁的状态值,但总共是一个线程,使用条件进行管理这个线程满不满足执行条件
- 共享模式,允许多个线程同时持有锁。通过 state 表示可用资源的数量(例如,state=5 表示信号量允许 5 个线程同时访问),比如线程a b c d e 5个线程,我们想要对线程a使用条件管理,让他不满足执行条件让出锁,那线程a具体线程的持有次数不晓得,怎么管理,共享模式下,state 表示全局资源计数,无法区分具体线程的持有次数。例如,state=5 可能是由多个线程共同贡献的,无法确定某个线程需要释放多少资源
isOnSyncQueue(node)
是否在同步队列:检查节点是否已从条件队列转移到同步队列
// AbstractQueuedSynchronizer 中的实现
final boolean isOnSyncQueue(Node node) {
// 情况1:判断是否存在条件队列中
// 条件1 节点状态为CONDITION,在条件队列,满足不看条件2
// 条件2 执行说明条件1不满足,条件2 不在同步队列。
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
// 情况 2:节点有后继节点,说明已链接到同步队列
if (node.next != null)
return true;
// 情况3:遍历同步队列,确认节点是否存在(兜底检查)
return findNodeFromTail(node);
}
private boolean findNodeFromTail(Node node) {
Node t = tail;// 获取同步队列队尾节点
while (t != null) {//子循环检查
if (t == node) //判断当前节点是否是队尾节点 是则返回
return true;
t = t.prev;//将队尾设置往前移动 并继续和当前节点比较 确认当前节点是否在同步队列中
}
return false;
}
checkInterruptWhileWaiting()
作用:检查等待期间的中断类型
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
注意:这个方法隐藏式处理中断并返回中断标识值,因为这个方法通过Thread.interrupted() ?(线程是否中断)触发进行了transferAfterCancelledWait(node) 方法
transferAfterCancelledWait(node):将因中断或超时而被取消等待的线程节点从条件队列(Condition Queue)转移到同步队列(Sync Queue)
final boolean transferAfterCancelledWait(Node node) {
// 尝试通过CAS将节点状态从CONDITION(-2)设为0
if (node.compareAndSetWaitStatus(Node.CONDITION, 0)) {
// CAS成功:说明中断/超时发生在signal()之前,需手动将节点加入同步队列
enq(node); // 自旋将节点插入同步队列尾部
return true;
}
// CAS失败:说明signal()可能已经触发了转移(节点状态已被其他线程修改)
// 等待直到节点被成功转移到同步队列(自旋等待其他线程完成enq操作)
while (!isOnSyncQueue(node))
Thread.yield(); // 短暂让出CPU,避免忙等
return false;
}
通过transferAfterCancelledWait方法将因为中断的当前节点加入到同步队列中,并且是自旋直到成功,并且transferAfterCancelledWait返回值决定checkInterruptWhileWaiting的返回值取哪个
中断在signal()之前发生:THROW_IE :线程应抛出 InterruptedException,并取消等待。
中断在signal()之后发生:REINTERRUPT:线程需在退出等待后重新标记中断状态,而不是立即响应
那么checkInterruptWhileWaiting方法在判断等待期间是否发生过中断,如果中断,做的隐藏处理就是将当前节点从条件队列转移到同步队列,所以这里中断触发节点转移:即使没有signal()
reportInterruptAfterWait()
根据中断标识模式值判断是否需要抛出异常还是重新设置中断位置
private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {
// 模式标识为THROW_IE 抛出中断异常
if (interruptMode == THROW_IE)
throw new InterruptedException();
// 模式标识为REINTERRUPT 不抛出中断异常,重新设置中断标识
else if (interruptMode == REINTERRUPT)
selfInterrupt();
}
unlinkCancelledWaiters()
遍历条件队列,移除所有已取消的节点(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; // 继续处理下一个节点
}
}
unlinkCancelledWaiters()
ConditionObject类中重写的6个方法
await()
作用:使当前线程等待,释放与之关联的锁,直到被唤醒(通过signal()或signalAll()或中断
特点:该方法会响应中断,如果当前线程在等待过程中被中断,会抛出InterruptedException异常
public final void await() throws InterruptedException {
//开始就是判断是否中断,成立就抛出InterruptedException异常
if (Thread.interrupted())throw new InterruptedException();
Node node = addConditionWaiter(); // 1. 创建节点并加入条件队列
int savedState = fullyRelease(node); // 2. 完全释放锁(考虑多重入锁情况)
int interruptMode = 0;
while (!isOnSyncQueue(node)) { // 3. 循环检查是否被转移到同步队列
LockSupport.park(this); // 4. 挂起线程
//5. 处理中断并退出:检查线程是否中断并隐藏式处理中断
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break; //退出自旋
}
// 6. 重新竞争锁并恢复状态
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 7. 清理替换队列中已取消的等待节点
if (node.nextWaiter != null)
unlinkCancelledWaiters();
// 8. 根据中断模式抛出异常或标记中断
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
关键步骤详解
1.addConditionWaiter():关联本地线程,创建节点并加入条件队列
2.fullyRelease():一次性完全释放锁 原子性清零
3. while (!isOnSyncQueue(node)) :循环检查是否被转移到同步队列。这是一个自旋检查,这一步是该类功能的核心设计之一,isOnSyncQueue(node)方法上面讲解了,判断节点是否在同步队列中,循环的退出条件是 节点已被成功转移到同步队列 或 线程被中断
① isOnSyncQueue(node) 当前节点已转移到同步队列中返回 true,即while (false) ,循环条件不成立开始执行步骤6 重新竞争锁并恢复状态
②isOnSyncQueue(node) 当前节点还在条件队列中返回 false,执行步骤4 5
4. 挂起线程操作等待后续唤醒:LockSupport.park(this),这里的this应该是ConditionObject的实例对象代码,但是别忘了,我们创建节点时候是关联了本地线程的,挂起的实例对象实际上就是挂起的线程,这里挂起的线程可以说是调用await方法本身的线程,走到这一步挂起线程就停下了,有两种情况会继续往下走,unpark和中断,unpark好理解主动性质解挂。中断:中断线程的操作行为,如果线程正阻塞在 park()、sleep() 或 I/O 操作中,操作系统会触发中断,使阻塞方法立即返回,这是一种被动性质的解挂,并且中断唤醒优先于 unpark(),这一步骤在文章最后总结里做核心说明
5.处理中断并退出:走到这一步 意味着线程已经解挂,那就判断在等待唤醒signal()期间是否发生了中断异常,并且这个异常是在执行signal()前还是后发生的,有中断并且做了隐藏中断处理:将当前节点转移到了同步队列。这里就体现了await() 设计的功能之一: 中断触发节点转移:即使没有signal(),这一步骤也在文章最后总结里做核心说明
6. 重新竞争锁并恢复状态:能走到这一步意味这节点已经转移到同步节点去了,acquireQueued()一定能获取锁除,并且返回值代表是否发生过中断,结合关联判断条件interruptMode != THROW_IE决定是否需要给更新interruptMode中断模式标识
7. 清理替换队列中已取消的等待节点 :这个时候当前节点已经转移到同步队列,执行方法遍历条件队列全部的节点取消无效的节点
8. 根据中断模式抛出异常或标记中断 :使用reportInterruptAfterWait方法根据中断标识模式值判断是抛出异常还是重新设置中断标识位置
awaitUninterruptibly()
作用:类似于await(),但不会响应中断。即使当前线程在等待过程中被中断,也不会抛出中断异常InterruptedException,而是继续等待。
特点:适用于那些不希望被中断的场景
public final void awaitUninterruptibly() {
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
boolean interrupted = false;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if (Thread.interrupted()) // 仅记录中断,不退出循环
interrupted = true;
}
if (acquireQueued(node, savedState) || interrupted)
selfInterrupt(); // 最终标记中断状态
}
前面和awit方法一致,加入条件队列,一次性释放锁,直到自旋检查,这里循环的退出条件是 节点已被成功转移到同步队列 这一种,所以使用awaitUninterruptibly方法的需要被唤醒方法 signal() / signalAll() ,不然容易死锁
然后退出循环转移到同步队列中的去获取锁 并根据中断标识值判断是否发生过中断(而且不区分中断发生的时间是在唤醒前后了) ,并依此决定是否重新设置中断标识
awaitNanos(long nanosTimeout)
作用:使当前线程进入等待队列,并释放与之关联的锁。当前线程会等待指定的时间(纳秒),或者被其他线程唤醒。
特点:该方法返回剩余的等待时间,如果在指定时间内未被唤醒,则返回值为负数
public final long awaitNanos(long nanosTimeout) throws InterruptedException {
// 第一步判断中断,抛出异常,说明这个方法也是支持抛出异常的
if (Thread.interrupted())throw new InterruptedException();
Node node = addConditionWaiter();//步骤 1
int savedState = fullyRelease(node);//步骤 2
final long deadline = System.nanoTime() + nanosTimeout;//步骤3
int interruptMode = 0;
while (!isOnSyncQueue(node)) { // 步骤4
if (nanosTimeout <= 0L) { //步骤5 判断剩余时间还有没有
transferAfterCancelledWait(node); // 超时后强制转移到同步队列
break;
}
// 步骤6 剩余时间大于1秒精准挂起线程 spinForTimeoutThreshold==1000L 1秒
if (nanosTimeout >= spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) // 步骤7
break;
nanosTimeout = deadline - System.nanoTime(); // 更新剩余时间
}
//后面的步骤和await方法相似
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null)
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
return deadline - System.nanoTime();
}
步骤1 2 和await()一样
步骤3 当前时间+超时时间 获得一个未来的时间点 即截止时间点
步骤4 判断是不是同步队列中
步骤5 根据截止时间点和当前时间点 减法计算判断 允许超时的时间还剩下多少,如果小于等于0 说明已经超时或即将超时,超时后强制转移到同步队列
步骤6 LockSupport.parkNanos(this, nanosTimeout) 实现精确超时,超过了指定的纳秒时间,那么该线程将会自动解除阻塞状态
步骤7 和await 方法一致 判断是否中断,如果有中断,隐藏式处理中断并退出
后面的步骤和await方法一致
区别在于最后的返回值:返回剩余的等待时间,如果在指定时间内未被唤醒,则返回值为负数
总结:
①awaitNanos方法支持抛出异常 执行方法步骤和await大体一致
②awaitNanos方法 实现了时间控制,如果超时自行将节点转移到同步队列并解挂线程
awaitUntil(Date deadline)
作用:使当前线程等待,直到被唤醒、中断或到达指定的绝对时间(deadline)
特点:该方法返回true表示在截止日期之前被唤醒,返回false表示在截止日期之后仍未被唤醒
// 首先方法体上是boolen类型的 这是一个判断方法
public final boolean awaitUntil(Date deadline) throws InterruptedException {
if (deadline == null)
throw new NullPointerException();
//将 Date 对象转换为 毫秒级 时间戳(abstime),用于后续超时判断
long abstime = deadline.getTime();
// 可以看出这个方法也是可以抛出中断异常的
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter(); // 1. 加入条件队列
int savedState = fullyRelease(node); // 2. 完全释放锁
boolean timedout = false;
int interruptMode = 0;
while (!isOnSyncQueue(node)) { // 3. 循环检查是否被转移
if (System.currentTimeMillis() >= abstime) {
timedout = transferAfterCancelledWait(node); // 4. 超时转移
break;
}
LockSupport.parkUntil(this, abstime); // 5. 挂起至绝对时间
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break; // 6. 处理中断
}
// 重新获取锁并处理中断(与await()逻辑一致)
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null)
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
return !timedout; // 7. 返回是否超时
}
这个方法与awaitNanos(long nanosTimeout)大体一致,区别在于返回值上
awaitNanos 方法返回值:返回剩余的等待时间,到时间未被唤醒,则返回值为负数
awaitUntil方法返回值:布尔值 true规定时间唤醒了,false规定时间内没有唤醒
signal()
作用:唤醒等待队列中最前面的一个线程。被唤醒的线程会在重新获取到锁之后继续执行。
特点:如果等待队列中没有线程,则该方法不做任何操作
public final void signal() {
// 1. 检查当前线程是否独占锁,调用线程必须持有锁
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//AQS的父类AbstractOwnableSynchronizer中定义了一个字段exclusiveOwnerThread,用于记录当前独占锁的线程,比较Thread.currentThread() == exclusiveOwnerThread 是否一致
// 2. 取条件队列第一个节点
Node first = firstWaiter;
//检查 first第一个节点 是否为空:
if (first != null)
//唤醒条件队列中的第一个节点
doSignal(first);
}
doSignal(first);方法具体实现
private void doSignal(Node first) {
do {
//更新条件队列头节点:将first.nextWaiter赋值给firstWaiter并检查新的firstWaiter是否为空
if ((firstWaiter = first.nextWaiter) == null)
//first.nextWaiter为null,说明当前节点是队列中的最后一个节点,因此将lastWaiter设为null
lastWaiter = null;
//断开原头节点:这行代码将first节点的nextWaiter设置为 null,以断开与条件队列的连接
first.nextWaiter = null;
}
// 3. 尝试转移节点到同步队列
while (!transferForSignal(first) && // 转移失败则继续循环
(first = firstWaiter) != null);//移动到下一个节点
}
transferForSignal方法:添加到同步队列并判断是否成功
final boolean transferForSignal(Node node) {
//1.CAS修改节点状态:若CAS失败(如节点已被取消,直接返回 false,通知调用方跳过该节点
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//2.将节点插入同步队列,注意:enq(node)返回值是原队尾节点啊
Node p = enq(node);
//3.处理前驱节点状态
int ws = p.waitStatus;
//ws > 0:前驱节点已取消(CANCELLED),无法通知当前节点
//CAS设置SIGNAL失败:前驱节点状态已变更(如被其他线程修改)无法保证唤醒当前节点
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
//上述条件满足,说明前驱节点不靠谱了,就直接唤醒 当前节点 的线程 不靠前驱节点唤醒了
LockSupport.unpark(node.thread);
return true;
}
在这个方法里做了唤醒这个节点的线程操作LockSupport.unpark(node.thread);
条件一:如果ws > 0 满足 :前驱节点已取消(CANCELLED),无法通知当前节点,说明前驱节点不靠谱了,就直接唤醒 当前节点 的线程,不看后面的条件了
条件二:如果ws > 0 不满足,执行 !compareAndSetWaitStatus(p, ws, Node.SIGNAL),设置前驱为SIGNAL,条件成立说明前驱节点状态设置失败,可能因竞争或前驱状态已变。
如果上述条件成立 触发了LockSupport.unpark(node.thread);唤醒线程,线程从 await() 的挂起点恢复,在await方法中我们执行的挂起线程操作,然后继续循环判断是否同步队列中往下走去,这是一种主动唤醒方式
如果条件不成立,不会触发解挂操作,被动唤醒方式,节点在同步队列中等待前驱节点释放锁时的唤醒,当前线程仍处于 park() 状态,停留在 await() 的循环中,前驱节点释放锁:AQS 会唤醒同步队列中的下一个有效节点(即当前节点)。然后跳转到await方法的park代码处 开始继续执行
signalAll()
作用:唤醒等待队列中的所有线程。这些线程会在重新获取到锁之后继续执行。
特点:通常用于广播通知,确保所有等待线程都能被唤醒
public final void signalAll() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignalAll(first); // 转移条件队列所有节点到同步队列
}
private void doSignalAll(Node first) {
//置空条件队列:将lastWaiter和firstWaiter置为 null
lastWaiter = firstWaiter = null;
do {
Node next = first.nextWaiter;
first.nextWaiter = null; // 断开节点链接
transferForSignal(first); // 逐个转移节点
first = next; // 转移到下一个节点
} while (first != null);
}
第一步 置空条件队列很重要,主要为了保证条件队列状态的原子性和一致性,立即清空 firstWaiter 和 lastWaiter,使得后续新调用的 await() 会重新构建新的条件队列,与原队列完全隔离。当前 doSignalAll 只需处理当前时刻之前的所有等待节点,确保操作的原子性,至于头节点传参进来了,原尾节点可以通过 倒数第二节点通过.nextWaiter得到(nextWaiter 既是节点链接也算节点备份)
后续遍历整个条件队列,转移所有节点到同步队列,核心方法依然是transferForSignal(first)
ConditionObject类中一些核心设计细节
ConditionObject中再await方法被park的线程恢复执行细节
在await() 系列方法中,挂起线程的方式有两种
while (!isOnSyncQueue(node)) {
LockSupport.park(this); // 挂起线程
LockSupport.parkNanos(this, nanosTimeout); // 精准挂起线程
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break; // 检查中断
}
}
上面我们说了park里面的参数应该是ConditionObject实例,park方法是挂起当前线程,这里挂起的是本地当前线程(先称为线程A) ,即调用await类型的方法这顺序行为,this这里实例对象是park方法用来记录阻塞信息等辅助性质用的,详情可以查验park底层方法
当前线程(调用await类型的方法这顺序行为) 被挂起,那代码就停在park这一行不往下走了,等待被解挂才会退出park,代码才会继续往下走
park解挂方式:unpark(精准目标线程) 和中断解挂
parkNanos解挂方式:多了个到了指定之间字段解挂
unpark(精准目标线程):在通知类型方法signal方法里执行了unpark方法
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
调用唤醒方法的线程我们称为线程B,调用signal方法判断是否满足unpark条件
①如果满足unpark条件,唤醒unpark(node.thread)精准目标线程node.thread,我们在await方法中添加到条件队列中的节点是将本地线程(即线程 A)关联封装成一个节点加入到条件队列的,所以唤醒之后代码执行就会跳转到await方法的LockSupport.park(this); 挂起的那一行去
Node node = new Node(Thread.currentThread(), Node.CONDITION);
② 如果不满足唤醒条件,说明当前节点已经转移到同步队列了,并且前驱节点设置成了唤醒状态(意思就是在同步队列了 而且前驱节点状态也是一个健康状态,等前驱节点竞争锁成功排到我再执行)
线程A仍处于 park() 状态,停留在 await() 的循环中,如何恢复 await() park处代码继续往下走,就需要依靠AQS执行机制了, 前驱节点释放锁:通过 AQS 的 release() 逻辑会唤醒同步队列中的下一个有效节点(即当前节点),release()里面的unpark方法会解挂线程,然后跳转到await方法的park代码处 开始继续执行
中断解挂:中断可以直接唤醒线程,无需依赖 unpark(),中断就是停止某一种操作,挂起 也是一种操作,停止挂起呗
LockSupport.park() 对中断的响应:线程立即从 park() 返回,中断状态被设置(Thread.interrupted() 返回 true)并且不会自动清除中断状态,需手动处理。中断阻止park行为无需依赖 unpark()且优先级高于unpark()
超时解挂:这个好理解,到时间了自动解挂 恢复从park处开始执行
还有一种伪唤醒:不知道在哪里莫名其妙的执行了unpark(thread)导致当前线程从park处恢复执行了,这个场景可能会有
等待方法和唤醒机制的执行步骤大致流程
调用者ConditionObject实例通过调用await类型的方法,这一系列执行顺序流我们称为线程A
步骤1:将本地线程(即线程 A)关联封装成一个节点加入到条件队列,addConditionWaiter 方法
Node node = new Node(Thread.currentThread(), Node.CONDITION);
步骤2:一次性释放当前线程持有锁,fullyRelease()方法
// 获取线程A的锁数量 这里savedState 可能大于1代表多次重入,比如3 就直接传参3 实现一次性释放
int savedState = getState();
release(savedState)
//总归保证得到的c的值为0 这样实现一次性释放所有层级
int c = getState() - releases;
步骤3:开始检查是否在同步队列中 如果在就跳过自旋开始 锁竞争方法acquireQueued
如果不在同步队列中执行的操作,就涉及到 主动唤醒 中断唤醒 超时唤醒 伪唤醒这4种情况,上面讲了关于恢复从park处执行的几种情况
主动唤醒:主动unpark恢复执行的情况 比如Signal类型方法
中断唤醒:park挂起有中断,解除park恢复执行 并且checkInterruptWhileWaiting方法判断是否方式中断并且隐藏式处理转移节点到同步队列,从而退出自旋继续往下执行锁竞争步骤,这里就体现了await() 设计的功能之一: 中断触发节点转移:即使没有signal()
超时唤醒:到时间自动解挂,transferAfterCancelledWait方法超时强制转移节点到同步队列
伪唤醒:不知道那哪里unpark触发了恢复从park执行,所以这里自旋检查的必要性:确保当前节点一定再同步队列中了才会开始后续锁竞争步骤
步骤4:进入同步队列锁竞争方法
await() 系列方法后续步骤都执行锁竞争方法acquireQueued(node, savedState)方法,走到这一步都代表已经转移到同步队列中去了,因为这个方法里面是有自旋的,理论上acquireQueued方法一定能获取到锁,只是时间早晚问题
步骤4之后就是各个await类型方法特性了,比如是否抛出异常,是否返回剩余等待时间,判断在规定等待期间完成重新拿到锁了
在await() 系列方法中的步骤自旋检查的必要性
通过循环反复检查,直到确认节点已成功进入同步队列,避免因线程调度延迟或内存可见性问题导致的状态误判
while (!isOnSyncQueue(node)) { // 循环检查
LockSupport.park(this); // 挂起线程 // 如果是伪唤醒后,再次检查条件!
if (被中断) break;
if (超时) break;
}
isOnSyncQueue(node)方法判断是否在同步队列
上面讲了几种唤醒方式(从park处恢复执行),如果超时或者中断都会触发退出自旋,那就剩余unpark的方式不会主动退出自旋,需要再次检查是否在同步队列中条件决定是否结束自旋
unpark方式上面也讲了主动唤醒和伪唤醒,伪唤醒可能是误发导致恢复从park执行,再次检查是否在同步队列中确保当前节点在同步队列中才开始后续执行锁竞争,这是必要的兜底检查方式
那主动唤醒:调用通知类型方法signal()/signalAll(),节点从条件队列转移到同步队列的过程包含是多个非原子步骤的
1.修改节点的 waitStatus(从 CONDITION 变为 0)。
2.将节点链接到同步队列尾部(设置 prev 和 next 指针)
3.唤醒线程等待锁
因为在线程被唤醒后,需要重新获取锁,而在等待锁的过程中,其他线程可能已经修改了条件变量。因此,即使线程被signal唤醒,当它重新获得锁时,条件可能已经被其他线程改变,因此必须再次检查
throw_ie和reinterrupt 设计使用场景
这两个自动是该类中定义的两个中断类型标识
THROW_IE :线程应抛出 InterruptedException,并取消等待。中断在signal()之前发生
REINTERRUPT:线程需在退出等待后重新标记中断状态,而不是立即响应,中断在signal()之后发生
特意拿出来说明是 await方法基于抛出中断还是不抛出异常的两种方式 定义了不被中断打断的是场景使用 如awaitUninterruptibly 也是不可中断的机制的一种体现
使用await() signal()为什么必须确保当前线程持有锁
Condition.await() 或 Condition.signal() 时,必须确保当前线程持有锁,这句话看似是矛盾的,比如当前线程都持有锁了,为什么还要唤醒线程再去竞争锁
首先这里先阐述一个观点就是Condition.await() 和 Condition.signal() 调用执行的一般不是一个线程的,单一线程中 里面执行先await() 后执行signal() 无意义,因为如果满足执行条件触发了await() 方法,这个时候会怎么样,代码就停在await方法里(因为park),不往下走了,到不了singal()那里,就造成了死锁,所以这两个方法的使用都是多线程才有意义
// 同一个方法里先执行await() 后执行signal(),类似这样就在同一个线程里 没意义的
test(){
Condition.await();
Condition.signal()
}
举例解析说明使用await() signal()为什么必须确保当前线程持有锁
ReentrantLock lock = new ReentrantLock();
Condition A= lock.newCondition();// 必须由锁创建
Queue<Integer> queue = new ArrayDeque<>(1); // 容量为1
// 线程A
public void produce(int item) throws InterruptedException {
lock.lock();
try {
while (queue.size() >= 1) { // 队列满时等待
A.await();
}
queue.add(item);
} finally {
lock.unlock();
}
}
// 线程B
public int consume() throws InterruptedException {
lock.lock();
try {
int item = queue.remove();
A.signal(); // 持有锁时发送信号
return item;
} finally {
lock.unlock();
}
}
上述代码的步骤,先执行哪个线程取决于我们先执行调用哪个线程
线程A的操作
获取锁 → 检查队列 → 条件不满足 → 调用await()。
持有锁时将节点加入条件队列 → 释放锁 → 挂起。
线程B的操作
获取锁 → 修改共享状态 → 持有锁时调用signal()。
此时线程A的节点已在条件队列中,线程B可安全将其转移到锁的同步队列。
await方法确保当前线程持有锁 :await方法执行内容 加入条件队列 释放锁 然后挂起等待
每个Condition都关联锁 封装关联的本地线程的 ,加入队列addConditionWaiter()和释放锁fullyRelease
//持有锁时加入条件队列
Node node = new Node(Thread.currentThread(), Node.CONDITION);
//fullyRelease方法--》release方法--》tryRealse方法
//最终通过getExclusiveOwnerThread判断当前线程是否是持有当前线程
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
在线程加入队列和释放锁时候都持有当前线程锁,保证这一操作是原子性的,因为其他线程无法在此时获取锁或修改条件队列。
signal方法在操作前也必须持有当前线程锁,已上面线程B执行内容步骤为例 ,获取到锁之后才执行singal方法,singal方法执行内容:最重要的一点是将节点从条件队列转移到同步队列中,此时线程A的节点已在条件队列中,再持有锁的状态下线程B可安全将其转移到锁的同步队列,这样同步队列的能安全,因为其他线程无法在此时获取锁或修改同步队列,同一时刻只有一个锁使用,这样保证队列的安全性
再就是线程B执行完释放锁了,线程A才能有机会拿到锁去执行,线程A执行时候再自旋判断队列大小,线程B是在移除队列元素后才去唤醒线程A的,这样能确保共享状态(队列大小)已被正确修改并且在合适时机提醒对应的线程执行任务,就是通过锁的分配,该线程B执行了锁给线程B,然后轮到线程A了锁给线程A,并且在线程B在把锁给线程A之前通过修改共享状态确保线程A能正常推进执行
//signal方法对于是否当前线程持有锁判断 如果否直接抛出异常
if (!isHeldExclusively())throw new IllegalMonitorStateException();
Condition 使用样例
经典的还是生产 消费模型,生产者线程A 消费者线程B
class Buffer {
private final LinkedList<Integer> queue = new LinkedList<>();
private final int capacity;
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();// condition必须由锁创建
private final Condition notEmpty = lock.newCondition();
public Buffer(int capacity) {
this.capacity = capacity;
}
//生产者添加数据
public void put(int item) throws InterruptedException {
lock.lock();
try {
// 检查缓冲区是否已满
while (queue.size() == capacity) {
System.out.println("[生产者] 缓冲区已满,等待消费...");
notFull.await();
}
// 生产数据
queue.addLast(item);
System.out.printf("[生产者] 生产 %3d,当前库存: %2d\n", item, queue.size());
// 唤醒消费者
notEmpty.signal();
} finally {
lock.unlock();
}
}
//消费者取出数据
public int take() throws InterruptedException {
lock.lock();
try {
// 检查缓冲区是否为空
while (queue.isEmpty()) {
System.out.println("[消费者] 缓冲区为空,等待生产...");
notEmpty.await();
}
// 消费数据
int item = queue.removeFirst();
System.out.printf("[消费者] 消费 %3d,当前库存: %2d\n", item, queue.size());
// 唤醒生产者
notFull.signal();
return item;
} finally {
lock.unlock();
}
}
}
生产消费模型 使用condition必须使用多线程 单一线程无意义
public class ProducerConsumerExample {
private static volatile boolean running = true;
private static final Random random = new Random();
public static void main(String[] args) throws InterruptedException {
Buffer buffer = new Buffer(5); // 缓冲区容量为5
// 创建2个生产者和3个消费者
Thread[] producers = new Thread[2];
Thread[] consumers = new Thread[3];
// 启动生产者线程
for (int i = 0; i < producers.length; i++) {
producers[i] = new Thread(() -> {
try {
while (running) {
int item = random.nextInt(1000);
buffer.put(item);
Thread.sleep(random.nextInt(500)); // 模拟生产耗时
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producers[i].start();
}
// 启动消费者线程
for (int i = 0; i < consumers.length; i++) {
consumers[i] = new Thread(() -> {
try {
while (running) {
buffer.take();
Thread.sleep(random.nextInt(800)); // 模拟消费耗时
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
consumers[i].start();
}
// 运行10秒
Thread.sleep(10_000);
running = false; // 停止所有线程
// 等待所有线程结束
for (Thread p : producers) p.join();
for (Thread c : consumers) c.join();
System.out.println("程序运行结束");
}
}
控制台打印输出
[生产者] 生产 642,当前库存: 1
[生产者] 生产 103,当前库存: 2
[消费者] 消费 642,当前库存: 1
[生产者] 生产 882,当前库存: 2
[消费者] 消费 103,当前库存: 1
[生产者] 生产 929,当前库存: 2
[消费者] 消费 882,当前库存: 1
[生产者] 缓冲区已满,等待消费...
[消费者] 消费 929,当前库存: 0
[生产者] 生产 266,当前库存: 1
该模型通过多个线程来启用两个线程 生产和消费线程,这两个线程改变共享条件变量值之后相互调用singal方法,从而确保总有任务在执行,不会一直处于等待状态,也不会一直处于执行状态,合理利用资源和调用的平衡关系