Condition
基本认识
- Condition是在java1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition,阻塞队列实际上是使用了Condition来模拟线程间协作。
基本用法
ConditionAwait
public class ConditionAwait implements Runnable{
private Lock lock;
private Condition condition;
public ConditionAwait(Lock lock,Condition condition) {
this.lock = lock;
this.condition = condition;
}
@Override
public void run() {
try {
lock.lock();//先上锁
System.out.println("Condition开始!!");
condition.await();//中断
System.out.println("Condition结束!!");
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}finally {
lock.unlock();//最后再释放锁
}
}
}
ConditionSignal
public class ConditionSignal implements Runnable{
private Lock lock;
private Condition condition;
public ConditionSignal(Lock lock,Condition condition) {
this.lock = lock;
this.condition = condition;
}
@Override
public void run() {
try {
lock.lock();//先上锁
System.out.println("ConditionSignal开始!!");
condition.signal();//唤醒
System.out.println("ConditionSignal结束!!");
} catch (Exception e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}finally {
lock.unlock();//最终都会释放锁
}
}
}
最终运行
public class app {
public static void main(String[] args) {
Lock lock = new ReentrantLock();//创建重入锁
Condition condition = lock.newCondition();
new Thread(new ConditionAwait(lock, condition)).start();
new Thread(new ConditionSignal(lock, condition)).start();
}
}
结果:
总结:
通过这个案例简单实现了 wait 和 notify 这个两个方法的功能,当调用 await 方法后,当前线程会释放锁并等待,而其他线程调用 condition 对象的 signal 或者 signalall 方法通知并被阻塞的线程,然后自己执行 unlock 释放锁,被唤醒的线程获得之前的锁继续执行,最后释放锁。所以,condition 中两个最重要的方法,一个是 await ,一个是 signal 方法
- await: 把当前线程阻塞挂起
- signal:唤醒阻塞的线程
源码分析
-
第一步如果要调用Condition的话,就需要获得 lock 的锁,两个线程就会使用到AQS同步队列
-
调用 Condition 的 await 方法会调用AQS里面的await()方法。
- 源码运行到 Node node = addConditionWaiter()
- 运行到 int savedState = fullyRelease(node) ,目的是为了释放当前锁,得到锁的状态去唤醒AQS队列中的线程。
- 运行到while循环,判断这个节点是否在 AQS 队列上
- 运行到 LockSupport.park(this),挂起当前线程。
- 源码运行到 Node node = addConditionWaiter()
-
线程B 被唤醒,然后执行到 signal()方法。
- 第一步先做一个判断,判断当前线程是否是拿到锁的线程,如果不是就报错
- 第二步拿到 condition 队列中的第一个结点,如果有就开始执行 doSignal()方法
- 第一步先将 Condition 队列的当前节点移除(ThreadC是假设的)
- 第二步执行 transferForSignal()方法,该方法先是 CAS 修改了节点状态,如果成功,就将这个节点放到 AQS 队列中,然后唤醒这个节点上的线程。此时,那个节点就会在 await 方法中苏醒
- 第一步更新节点的状态为0,如果更新失败,只有一种可能就是节点被CANCELLED了
- 第二步调用enq,把当前节点添加到AQS队列。并且返回按当前节点的上一个节点,也就是原tail节点
- 第三步进行判断,如果上一个节点的状态被取消了, 或者尝试设置上一个节点的状态为 SIGNAL 失败了(SIGNAL 表示: 他的 next 节点需要停止阻塞),
- 第四步如果失败了,唤醒结点上的线程
- 第一步先将 Condition 队列的当前节点移除(ThreadC是假设的)
- 执行完 doSignal 以后,会把 condition 队列中的节点转移到 aqs 队列上, 这个时候会判断 ThreadA 的 prev 节点也就是 head 节点的 waitStatus 如果大于 0 或者设置 SIGNAL 失败,表示节点被设置成了 CANCELLED 状态 。 这个时候会唤醒ThreadA 这个线程 。 否则就基于 AQS 队列的机制来唤醒,也就是等到 ThreadB 释放锁之后来唤醒 ThreadA
-
唤醒之后回到 await()方法
- 总结流程:
-
第五步又会回到原来 await()方法
- 执行 if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) 判断是否被中断
- 这里需要注意的地方是,如果第一次 CAS 失败了,则不能判断当前线程是先进行了中断还是先进行了 signal 方法的调用,可能是先执行了 signal 然后中断,也可能是先执行了中断,后执行了 signal ,当然,这两个操作肯定是发生在 CAS 之前。这时需要做的就是等待 当前线程的 node被添加到 AQS 队列后,也就是 enq 方法返回后,返回false 告诉 checkInterruptWhileWaiting 方法返回 REINTERRUPT ( 1),后续进行重新中断。
- 简单来说,该方法的返回值代表当前线程是否在 park 的时候被中断唤醒,如果为 true 表示中断在 signal 调用之前, signal 还未执行 ,那么这个时候会根据 await 的语义,在 await 时遇到中断需要抛出 interruptedException ,返回 true 就是告诉 checkInterruptWhileWaiting 返回 (THROW _IE 1) 。如果返回 false ,否则表示 signal 已经执行过了 ,只需要重新响应中断即可
- 执行到
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
这一步为了恢复线程的锁,如果没有中断的话- acquireQueued 方法是让当前被唤醒的节点 ThreadA 去抢占同步锁 。并且要恢复到原本的重入次数状态。
- 执行到
if (interruptMode != 0) reportInterruptAfterWait(interruptMode);
这一步主要是根据 checkInterruptWhileWaiting 方法返回的中断标识来进行中断上报 。如果是 THROW_IE ,则抛出中断异常,如果是 REINTERRUPT ,则重新响应中断
- 执行 if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) 判断是否被中断
总结
- 阻塞: await() 方法中,在线程释放锁资源 之后,如果节点不在 AQS 等待队列,则阻塞当前线程,如果在等待队列,则自旋等待尝试获取锁
- 释放: signal() 后,节点会从 condition 队列移动到 AQS等待队列,则进入正常锁的获取流程