本文是自己的学习笔记,主要参考资料如下
JavaSE文档
- [1. ConditionObject的应用](#1. ConditionObject的应用)
- 2、ConditionObject源码分析
1. ConditionObject的应用
1.1、park()和unpark()
ConditionObject
本质上是通过这两个方法将线程挂起暂停操作和唤醒线程继续执行。在源码中可以看到这两个方法的执行。
这两个方法和wait()
和notify
类似,但也有不同。下面是park()
和unpark()
的特点。
park()
挂起的线程不会自动释放锁,这点和wait()
不同。- 当
park()
线程后,如果有其他线程执行了被挂起线程的interrupt()
中断方法,那被挂起的线程会被自动唤醒。这是比较重要的特性,很多时候线程的中断都是没有实际作用的,这里不太一样。
1.1、await()和signal() ```synchronized```里提供了```wait()```和```notify```的方法来实现线程挂起和唤醒的功能。
而ReentrantLock
也提供了await()
和signal()
来实现类似的事。
await()
会挂起当前线程并释放锁。当有其他的线程再这个锁上执行了signal()
方式唤醒该线程,并且该线程争取到了锁,那线程才会开始继续执行await()
后面的代码。signal()
会唤醒一个因执行await()
被挂起的线程,这里并不意味着被唤醒的线程能拿到锁,它需要竞争成功后才能拿到锁。执行signal()
的线程不会释放锁资源。要通过unlock()
才会释放锁。
上面的这两个方法本质是是通过park()
和unpark()
将线程挂起暂停操作和唤醒线程继续执行。在源码中可以看到这两个方法的执行。
只有获得锁资源的线程才能执行这两个方法。而执行了await()
后,被挂起的线程会自动释放锁资源。
下面来看一段代码示例来更好地理解await()
和signal()
。
1.2、代码示例
java
public static void main(String[] args) throws InterruptedException, IOException {
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(() -> {
lock.lock();
System.out.println("1.子线程获取锁资源并await挂起线程");
try {
// sleep不释放锁资源
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
// 子线程挂起,释放锁。
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("4.子线程挂起后被唤醒!持有锁资源");
}).start();
// 主线程沉睡,保证子线程能开始执行并获取到锁。
Thread.sleep(100);
// lock会阻塞挂起主线程,因为子线程已经拿到锁了。
lock.lock();
// 子线程因signal()释放锁,主线程拿到锁继续执行
System.out.println("2.主线程等待5s拿到锁资源,子线程执行了await方法");
condition.signal();
// 前面只是唤醒子线程,但还没释放锁,所以主线程会继续打印3.
System.out.println("3.主线程唤醒了await挂起的子线程");
// 释放锁后子线程才能拿到锁继续执行打印4.
lock.unlock();
}
结果会按序打印。
- 一开始子线程启动,并且主线程睡眠,所以子线程一定会拿到锁并打印1。
- 主线程执行
lock()
但因为锁在子线程手里所以主线程陷入阻塞。 - 子线程沉睡5s后执行
await()
主动释放锁并阻塞挂起自己。于是主线程能获取到锁继续执行打印2. - 主线程执行
signal()
唤醒子线程,但因为主线程没有释放锁,所以子线程拿不到锁不会执行。主线程会继续打印3。 - 主线程释放锁之后,被唤醒的子线程拿到锁开始执行,打印4。
1.3、ConditionObject原理简介
Condition
是接口,ConditionObject
是其实现类。文中有时候会说Condition
,有时候ConditionObject
,但我指的是同一个东西。
Condition
是依附于ReentrantLock
存在的,提供控制想获取这个锁的线程的方法。
和AQS
类似,Condition
内部有一个队列,但是是单向的,用于封装想获取锁的线程。
2、ConditionObject源码分析
2.1、ConditionObject的构造方法
ConditionOject
实例通过下面的方式获取。
java
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
进入newCondition()
内部。发现它只是直接new了一个ConditionObject
。
java
public Condition newCondition() {
return sync.newCondition();
}
final ConditionObject newCondition() {
return new ConditionObject();
}
所以一个lock
可以创建出不止一个ConditionObject
。
在使用的时候要注意同一个锁上的不同的ConditionObject
理论上是互不影响的。比如一个lock
上创建了两个Condition
实例,condition1
的signal()
是不会唤醒condition2
的线程。
2.2、await()的前置分析
await()
方法很复杂,这里先分析前面的部分,即线程挂起前的操作。
java
public final void await() throws InterruptedException {
//判断中断
if (Thread.interrupted())
throw new InterruptedException();
// 把封装成Node的线程放到Condition的单向队列中,中间会删除状态是Cancel的节点
Node node = addConditionWaiter();
// 充分释放锁资源。因为是可重入锁,用该方法将state直接变成0,同时返回重入次数保存。
// 如果线程没有持有锁会在其中抛出异常。同时该Node的状态会置为Cancel。
int savedState = fullyRelease(node);
int interruptMode = 0;
// 检查当前Node是否在AQS中,只有不在了才需要挂起当前线程。
// 因为前面线程已经通过fullyRelease()释放了锁,所以其他在AQS中的线程是有可能立刻获取锁并执行signal()方法唤醒该线程。
// 所谓的singal()就是从Condition的队列把一个Node拿出来放到AQS的队列中。
// 所以如果发生了上面的情况,即当前线程在AQS中,意味着线程已经被唤醒,无需再挂起。
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
// 省略
}
// 省略
}
- 判断中断,如果线程中断了就抛出异常。
- 将线程封装成
Node
放入Condition
的单向队列中。放入前会清除队列中状态为Cancel
的节点。 - 将锁完全释放,
status = 0
。 - 如果线程释放锁后因其他获得锁的线程执行的
signal()
方法唤醒,那就无需再挂起当前线程。
2.2.1、addConditionWaiter()
该方法会将封装成Node
的线程加到Condition
自己的单向队列队尾。在加入之前会删除队列中状态为Cancel
的节点。
java
private Node addConditionWaiter() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 拿到尾结点
Node t = lastWaiter;
// 如果尾结点不是null,说明有其他线程在队列中。
// 状态不是Condiiton,那该节点状态不正常。
// 所以需要执行unlinkCancelledWaiters()去除取消的节点后再把Node加入到队列中。
if (t != null && t.waitStatus != Node.CONDITION) {
// 去除队列中状态是Cancel的节点
unlinkCancelledWaiters();
t = lastWaiter;
}
// 构建当前线程的Node
Node node = new Node(Node.CONDITION);
// 把当前线程放到队尾
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
- 首先中断状态判断,常规操作了,中断直接抛异常。
- 执行
unlinkCanceledWaiters()
,去除状态为Cancel
的节点。 - 最后将线程封装成
Node
放入队尾。
2.2.2、fullyRelease()
该方法会充分释放锁资源。因为是可重入锁,用该方法将state直接变成0,同时返回重入次数保存。
如果线程没有锁会在这里抛出异常。所以await()
方法必须是线程持有锁时才能执行。这种情况下该Node
不是个正常的节点,状态会被置为Cancel
。
java
final int fullyRelease(Node node) {
try {
int savedState = getState();
// 失败则表示当前线程不持有锁,抛出异常
if (release(savedState))
return savedState;
throw new IllegalMonitorStateException();
} catch (Throwable t) {
// 该节点状态变为Cancel,后续会被删除
node.waitStatus = Node.CANCELLED;
throw t;
}
}
2.3、signal()方法分析
signal()
方法比较简单,检查是否持有线程,然后有节点在等待的话才执行唤醒操作。
具体唤醒的逻辑在doSignal()
。
java
public final void signal() {
// 不持有锁的话就抛异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 拿到排队的第一个节点
Node first = firstWaiter;
// 有节点才需要唤醒,没有就结束。
if (first != null)
doSignal(first);
}
下面看看doSignal()
的逻辑。
java
private void doSignal(Node first) {
do {
// 将firstWaiter指向第二个节点,因为第一个节点要被唤醒了
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
// 唤醒Node,将Condition的Node移到AQS
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
java
final boolean transferForSignal(Node node) {
// 需要改变Node的状态,从CONDITION改成SIGNAL
if (!node.compareAndSetWaitStatus(Node.CONDITION, 0))
return false;
// enq()将node放到AQS中,这个方法返回的是node在AQS中的上一个节点
Node p = enq(node);
int ws = p.waitStatus;
// 如果ws>0,那上一个节点的状态是CANCEL,
if (ws > 0 || !p.compareAndSetWaitStatus(ws, Node.SIGNAL))
// 如果前置节点状态是取消,继续在这个节点后面会导致当前节点永远无法被唤醒。所以执行unpark()方法直接唤醒线程。
// 线程会基于acquireQueued方法再AQS中找到一个正常的前置节点挂在后面。
// 如果状态改变失败,那可能是并发导致的。为了防止节点无法正常唤醒,也需要直接唤醒当前线程。
LockSupport.unpark(node.thread);
return true;
}
所以signal()
主要做了下面几件事。
- 确保执行
signal()
的线程持有锁。 - 脱离
Condition
队列。 - 将
Node
状态改成Signal
后插入到AQS队列。 - 为了避免
Node
无法在AQS
中正常唤醒做了一些判断和操作。
2.4、await()后置分析
前面的前置分析只分析到了线程挂起前的操作,即循环isOnSyncQueue()
的第一句。这里会对之后的代码分析。
做完前面的操作,包括进入Condition
队列和释放锁,如果没有其他线程唤醒该线程,那该线程应该执行park()
方法将自己挂起。
过一段时间后线程被唤醒,park()
之后的代码才能继续执行,这部分的代码比较重要,可以更好理解Node
在Condition
队列和AQS
队列之间的转换和中断对他们的影响。
线程会因为下面三个原因被唤醒。
- 被其他线程的
signal()
唤醒,这属于正常操作,后续不需要额外做处理。 - 被中断唤醒。这是异常情况,需要抛出异常,还需要移除在
Condition
队列中的Node
。 - 被
signal()
唤醒,但是在执行unpark()
之前就遇到了中断。这种情况也算是被正常唤醒,但需要做一些额外的操作。
这时候Node
从Condition
队列到AQS
的处理可能还没完成线程就被唤醒。那需要保证Node
如预期一样从Condition
脱离并顺利进入AQS
。同时需要需要将中段位重置,以免后续因中段位发生错误。
while (!isOnSyncQueue(node)) {
之后的代码就是围绕着这三种情况做处理。
java
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
// Node不在AQS队列中,还在Condition队列中,所以需要挂起线程。
LockSupport.park(this);
// 上一行执行了park()方法线程已经被挂起,如果执行到了这一行说明线程被唤醒了。
// 线程被唤醒有三种情况,对应的操作也不同。
// 1. 被signal()唤醒,那Node应该在AQS中。一切正常可以退出循环
// 2. 中断唤醒,Node还在Condition队列中。需要手动将Node移到AQS中。
// 3. 被signal()唤醒,紧接着遇到中断。
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 此时Node一定在AQS中,但如果是被中断唤醒那Node也存在于Condition中
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 对于第二种情况,Node此时还是Condition队列中,需要从Condition队列中移除
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
// 如果等于0,线程再signal()以及持有锁的过程中没有被中断过。
// 如果被中断过,第二种情况需要抛出异常,第三种情况则只需要将中段位重置即可。
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
2.4.1、checkInterruptWhileWaiting()
需要判断被唤醒属于下面的那种情况。
- 被signal()唤醒,那Node应该在AQS中。一切正常可以退出循环
- 中断唤醒,Node还在Condition队列中。需要手动将Node移到AQS中。因为是非正常唤醒,需要抛出异常。
- 被signal()唤醒,紧接着遇到中断。
直接通过中断位判断,无中断则第一种情况,有中断则继续。
这里因为使用的是Thread.interrupted()
,该方法读取了标志位后会重置中断标志位为false
。所以对于第三种情况,如果线程后续还会执signal()
或者await()
方法,不需要担心这两个方法会因为中断位抛异常。
java
private int checkInterruptWhileWaiting(Node node) {
// 没有中断则返回0,一切正常。有中断则看具体情况。
// 第二种情况,需要抛出异常。
// 第三种情况,不需要抛出异常。
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
如果通过CAS
成功将状态从Condititon
换成0,那节点原来的状态是Condition
,即节点仍在Condiiton
的队列中。
可以推断这不属于第三种情况,所以需要手动将Node
从Condition
移到AQS
。但是需要注意,Node
还没从Condition
移除。
java
final boolean transferAfterCancelledWait(Node node) {
//属于第二种情况,将Node从Condition移到AQS
if (node.compareAndSetWaitStatus(Node.CONDITION, 0)) {
enq(node);
return true;
}
// 第三种情况下,signal()方法可能还没执行到unpark(),线程就被中断唤醒了。
// 但这属于正常情况,所以只需要让线程一直自旋,直到Node完全从Condition移到AQS
while (!isOnSyncQueue(node))
Thread.yield();
return false;
}
2.4.1.1、总结
线程执行await()
后,会因park()
方法被挂起。过一段时间后线程会唤醒,但需要确定被唤醒的原因。总共有三种情况。
- 被signal()唤醒,那Node应该在AQS中。一切正常可以退出循环
- 中断唤醒,Node还在Condition队列中。需要手动将Node移到AQS中。因为是非正常唤醒,需要抛出异常。
- 被signal()唤醒,紧接着遇到中断。本质上是正常被唤醒,但需要做一些操作以免后续有错误。
如果中断位是false
那一定是第一种情况。
之后则需checkInterruptWhileWaiting()
方法就是用于判断具体是第二种还是第三种。