在学习 synchronized 的时候,有学过 wait/notify 的基本使用,结合 synchronized 可以实现对线程的通信。那么既然 J.U.C 里面提供了锁的实现机制,那 J.U.C 里面有没有提供类似的线程通信的工具呢? 于是找啊找,发现了一个 Condition 工具类。
Condition 是一个多线程协调通信的工具类,可以让某些线程一起等待某个条件 (condition),只有满足条件时,线程才会被唤醒
这篇文章写的不错https://blog.csdn.net/zx48822821/article/details/86768484
同步队列和等待队列
首先我们需要了解同步队列和等待队列的概念。简单的理解是同步队列存放着竞争同步资源的线程的引用(不是存放线程),而等待队列存放着待唤醒的线程的引用。
Condition使用示例
插播一道编程题
三个线程循环打印abc
package com.xhc.test.thread.mianshi;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author xuehuichen
* @version V1.0
* @Package com.xhc.test.thread.mianshi
* @date 2021/3/9 下午7:21
* @Copyright © 2016-2017 同程旅行
*/
public class ThirdThreadPrintABC {
private int count = 0;
private Lock lock = new ReentrantLock();
private Condition conditionA = lock.newCondition();
private Condition conditionB = lock.newCondition();
private Condition conditionC = lock.newCondition();
public static void main(String[] args) {
ThirdThreadPrintABC printABC = new ThirdThreadPrintABC();
ThreadA threadA = printABC.new ThreadA();
ThreadB threadB = printABC.new ThreadB();
ThreadC threadC = printABC.new ThreadC();
threadA.start();
threadB.start();
threadC.start();
}
/**
* 打印A
* @author LKB
*
*/
class ThreadA extends Thread{
public void run(){
try {
lock.lock();
while(count < 30){
if (count%3 != 0) {
conditionA.await();
}
System.out.println("A");
count ++;
conditionB.signalAll();
}
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}finally {
lock.unlock();
System.out.println("ThreadA 中执行了 unlock");
}
}
}
/**
* 打印B
* @author LKB
*
*/
class ThreadB extends Thread{
public void run(){
try {
lock.lock();
while(count < 30){
if (count%3 != 1) {
conditionB.await();
}
System.out.println("B");
count ++;
conditionC.signalAll();
}
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}finally {
lock.unlock();
System.out.println("ThreadB 中执行了 unlock");
}
}
}
/**
* 打印C
* @author LKB
*
*/
class ThreadC extends Thread{
public void run(){
try {
lock.lock();
while(count < 30){
if (count%3 != 2) {
conditionC.await();
}
System.out.println("C");
System.out.println("+++++++");
System.out.println("-------");
count ++;
if(count < 30){
conditionA.signalAll();
}
}
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}finally {
lock.unlock();
System.out.println("ThreadC 中执行了 unlock");
}
}
}
}
我们还是创建一个生产者消费者的模型
生产者Producer
public class Producer implements Runnable{
private Queue<String> msg;
private int maxSize;
Lock lock;
Condition condition;
public Producer(Queue<String> msg, int maxSize, Lock lock, Condition condition) {
this.msg = msg;
this.maxSize = maxSize;
this.lock = lock;
this.condition = condition;
}
@Override
public void run() {
int i=0;
while(true){
i++;
lock.lock();
while(msg.size()==maxSize){
System.out.println("生产者队列满了,先等待");
try {
condition.await(); //阻塞线程并释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("生产消息:"+i);
msg.add("生产者的消息内容"+i);
condition.signal(); //唤醒阻塞状态下的线程
lock.unlock();
}
}
}
消费者 Consumer
public class Consumer implements Runnable{
private Queue<String> msg;
private int maxSize;
Lock lock;
Condition condition;
public Consumer(Queue<String> msg, int maxSize, Lock lock, Condition condition) {
this.msg = msg;
this.maxSize = maxSize;
this.lock = lock;
this.condition = condition;
}
@Override
public void run() {
int i=0;
while(true){
i++;
lock.lock(); //synchronized
while(msg.isEmpty()){
System.out.println("消费者队列空了,先等待");
try {
condition.await(); //阻塞线程并释放锁 wait
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费消息:"+msg.remove());
condition.signal(); //唤醒阻塞状态下的线程
lock.unlock();
}
}
}
main方法
public class App
{
public static void main( String[] args )
{
Queue<String> queue=new LinkedList<>();
Lock lock=new ReentrantLock(); //重入锁
Condition condition=lock.newCondition();
int maxSize=5;
Producer producer=new Producer(queue,maxSize,lock,condition);
Consumer consumer=new Consumer(queue,maxSize,lock,condition);
Thread t1=new Thread(producer);
Thread t2=new Thread(consumer);
t1.start();
t2.start();
}
}
通过这个案例简单实现了 wait 和 notify 的功能,当调用 await 方法后,当前线程会释放锁并等待,而其他线程调用 condition 对象的 signal 或者 signalall 方法通知被阻塞的线程,然后自己执行 unlock 释放锁,被唤醒的线程获得之前的锁继续执行,最后释放锁。所以,condition 中两个最重要的方法:
- await:把当前线程阻塞挂起
- signal:唤醒阻塞的线程
Condition 源码分析
调用 Condition,需要获得 Lock 锁,所以意味着会存在一个 AQS 同步队列,先来看 Condition.await 方法
condition.await
调用 Condition 的 await()方法(或者以 await 开头的方法),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从 await()方法返回时,当前线程一定获取了 Condition 相关联的锁
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();//创建一个新的节点,节点状态为condition,采用的数据结构仍然是链表
int savedState = fullyRelease(node);//释放当前的锁,得到锁的状态,并唤醒AQS队列中的一个线程
int interruptMode = 0;
//如果当前节点没有在同步队列上,即还没有被 signal,则将当前线程阻塞
while (!isOnSyncQueue(node)) {//判断这个节点是否在 AQS 队列上,第一次判断的是 false,因为前面已 经释放锁了
LockSupport.park(this);//第一次总是 park 自己,开始阻塞等待
// 线程判断自己在等待过程中是否被中断了,如果没有中断,则再次循环,会在 isOnSyncQueue 中判断自己是否在队列上.
// isOnSyncQueue 判断当前 node 状态,如果是 CONDITION 状态,或者不在队列上了,就继续阻塞.
// isOnSyncQueue 判断当前 node 还在队列上且不是 CONDITION 状态了,就结束循环和阻塞.
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 当这个线程醒来,会尝试拿锁, 当 acquireQueued 返回 false 就是拿到锁了.
// interruptMode != THROW_IE -> 表示这个线程没有成功将 node 入队,但 signal 执行了 enq方法让其入队了.
// 将这个变量设置成 REINTERRUPT.
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 如果 node 的下一个等待者不是 null, 则进行清理,清理 Condition 队列上的节点.
// 如果是 null ,就没有什么好清理的了.
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
// 如果线程被中断了,需要抛出异常.或者什么都不做
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
fullyRelease
fullRelease,就是彻底的释放锁,什么叫彻底呢,就是如果当前锁存在多次重入,那么在这个方法中只需要释放一次就会把所有的重入次数归零。
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState();
//获得重入的次数
if (release(savedState)) {
//释放锁并且唤醒下一个同步队列中的线程
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
isOnSyncQueue
判断当前节点是否在同步队列中,返回 false 表示不在,返回 true 表示在
如果不在 AQS 同步队列,说明当前节点没有唤醒去争抢同步锁,所以需要把当前线程阻塞起来,直到其他的线程调用 signal 唤醒
如果在 AQS 同步队列,意味着它需要去竞争同步锁去获得执行程序执行权限
为什么要做这个判断呢?原因是在 condition 队列中的节点会重新加入到 AQS 队列去竞争锁。也就是当调用 signal 的时候,会把当前节点从 condition 队列转移到 AQS 队列
判断线程是否在AQS队列中的方法:
- 如果 ThreadA 的 waitStatus 的状态为 CONDITION,说明它存在于 condition 队列中,不在 AQS 队列。因为AQS 队列的状态一定不可能有 CONDITION
- 如果 node.prev 为空,说明也不存在于 AQS 队列,原因是 prev=null 在 AQS 队列中只有一种可能性,就是它是head 节点,head 节点意味着它是获得锁的节点。
- findNodeFromTail,表示从 tail 节点往前扫描 AQS 队列,一旦发现 AQS 队列的节点和当前节点相等,说明节点一定存在于 AQS 队列中
Condition.signal
await 方法会阻塞 ThreadA,然后 ThreadB 抢占到了锁获得了执行权限,这个时候在 ThreadB 中调用了 Condition 的 signal()方法,将会唤醒在等待队列中节点,会将节点移到同步队列中
public final void signal() {
if (!isHeldExclusively()) // 先判断当前线程是否获得了锁,这个判断比较简单,直接用获得锁的线程和当前线程相比即可
throw new IllegalMonitorStateException();
Node first = firstWaiter; // 拿到 Condition 队列上第一个节点
if (first != null)
doSignal(first);
}
Condition.doSignal
对 condition 队列中从首部开始的第一个 condition 状态的节点,执行 transferForSignal 操作,将 node 从 condition 队列中转换到 AQS 队列中,同时修改 AQS 队列中原先尾节点的状态
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null) // 如果第一个 节点的下一个节点是 null, 那么, 最后一个节点也是 null.
lastWaiter = null; // 将 next 节点设置成 null
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
AQS.transferForSignal
该方法先是 CAS 修改了节点状态,如果成功,就将这个节点放到 AQS 队列中, 然后唤醒这个节点上的线程。此时,那个节点就会在 await 方法中苏醒
final boolean transferForSignal(Node node) {
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))//更新节点的状态为0,如果更新失败,只有一种可能就是节点被 CANCELLED 了
return false;
Node p = enq(node);//调用enq,把当前节点添加到AQS队列。并且返回当前节点的上一个节点,也就是原 tail 节点
int ws = p.waitStatus;
// 如果上一个节点的状态被取消了, 或者尝试设置上一个节点的状态为 SIGNAL失败了(SIGNAL 表示: 他的 next 节点需要停止阻塞)
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread); // 唤醒节点上的线程
return true;
}
Condition 总结
阻塞:await() 方法中,在线程释放锁资源之后,如果节点不在 AQS 等待队 列,则阻塞当前线程,如果在等待队列,则自旋等待尝试获取锁
释放:signal() 后,节点会从 condition 队列移动到 AQS 等待队列,则进入 正常锁的获取流程