深入理解并发编程之Condition源码解读分析
文章目录
一、Condition是什么
Condition是一个接口,其提供的就两个核心方法,await()和signal()方法,相当于Object的wait和notify方法,执行await()方法进入等待状态的线程会放在Condition的单项队列中。注意等待和唤醒方法都需要在锁的代码块之间才能执行。
- firstWaiter:Condition队列中的第一个等待者
- lastWaiter:Condition队列中的最后一个等待者
- await():当前线程进入等待状态
- signal():唤醒一个等待状态的线程
- signalAll():唤醒所有等待状态的线程
二、Condition详细分析
1.举例说明Condition的用法
public class Test {
private static Lock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "线程获取到锁");
condition.await();
System.out.println(Thread.currentThread().getName() + "线程执行结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
try {
Thread.sleep(3000);
} catch (Exception e) {
}
lock.lock();
condition.signal(); //唤醒等待的线程,唤醒了但是不会执行,因为锁标记此时被主线程持有了
System.out.println(Thread.currentThread().getName() + "主线程执行结束");
lock.unlock(); //lock锁需要释放了,线程0才能获取到锁标记才能执行
}
}
看一下执行结果,一开始线程执行打印了获取到锁,执行了await()方法进入等待,睡眠3秒过去了,主线程获取到了锁(因为进入阻塞状态锁标记会释放),主线程执行结束后,释放锁子线程从等待的地方继续执行,这里需要注意主线程不释放锁标记子线程就会一直在阻塞状态,因为或全部不到锁标记:
1.Condition的源码分析
await()
先看一下condition的等待方法源码
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter(); //Condition等待队列中增加一个等待者
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) { //如果当前线程的waitStatus状态为-2等待状态,则执行循环。创建等待队列的节点状态肯定是-2
LockSupport.park(this); //阻塞了当前线程
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) //相应中断跳出循环
break;
}
//acquireQueued在前面Lock也讲了,是执行lock()方法,里面先CAS尝试获取锁如果获取不到则进入阻塞状态。当多个线程同时唤醒就会进入lock阻塞状态,这里看把原来的state状态修改回来了
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) //没有下一个等待者了进行清除
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
/**
1. 增加等待者方法
*/
private Node addConditionWaiter() {
Node t = lastWaiter; //第一次进来t为空
if (t != null && t.waitStatus != Node.CONDITION) { //如果最后一个节点不为空并且状态不是等待状态则清理阻塞队列中不是等待状态的线程
unlinkCancelledWaiters();
t = lastWaiter;
}
//创建一个当前线程的节点挂在等待队列的后面
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node; //从这里就可以看出来,只有next而没有pred说明Condition的队列是单向的
lastWaiter = node;
return node; //返回当前线程的节点
}
/**
2. 尝试释放当前线程的锁标记并返回原来的状态,因为lock锁是可重入的,所以state的值是不确定的
*/
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState(); //获取原来的state状态
if (release(savedState)) { //当前线程解锁,这个在Lock的unlock()方法已经讲过了
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
/**
3. 将等待当前等待-2状态的节点修改为0等待获取锁状态
*/
final boolean transferAfterCancelledWait(Node node) {
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) { //此时修改状态由-2变成了0,等待变为阻塞
enq(node); //这里又将当前节点加入了阻塞队列中
return true;
}
while (!isOnSyncQueue(node))
Thread.yield();
return false;
}
总结下await()的步骤:
- 在Condition增加当前线程为其中的等待者
- 尝试让当前线程释放掉锁标记并且记录下来当前线程的state
- 将当前线程节点的状态修改为-2等待状态,然后阻塞当前线程直到中断响应获取到锁标记并且被唤醒了才能继续执行
- 中断响应跳出循环,将线程的state状态(重入锁次数)修改为原来的状态。
signal()
先看一下condition的唤醒方法源码:
/**
1. 唤醒第一个等待者
*/
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
/**
2. 唤醒第一个等待者
*/
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null) //这里判断如果第一个是第一个的下一个节点,则就一个等待的,清空
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
/**
3. 将当前线程置为0阻塞状态,并且加入到阻塞队列最后一个节点
*/
final boolean transferForSignal(Node node) {
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) //将状态由-2变为0,等待变为阻塞
return false;
Node p = enq(node); //将当前节点设置为阻塞队列的最后一个节点
int ws = p.waitStatus;
//如果节点状态>0,就看看锁标记是否是当前线程持有的,是放弃锁标记,一般不会等待线程自己持有锁标记的
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
唤醒的方法还是比较简单的,简单描述下具体干了啥
- 将第一个节点的状态由-2等待状态修改为0阻塞状态
- 把这个节点增加到阻塞队列里面去,就等着获取到锁标记就行了
看一下下面的图假设T1,T2,T3三个线程是如何执行的,假设t2先执行了等待,然后t3又执行了等待方法,等待池中是t2和t3,此时唤醒了t2,则t2进入了阻塞队列中,并不是直接执行了,因为只是唤醒了还没有获取到锁标记。
内容来源:蚂蚁课堂