并发编程-Condition
本篇主要讲述的内容是线程之间的通信——Conditon ,那什么是线程通信?线程之间的通信是指当某个线程修改了一个对象的值时,另外一个线程能够感知到该值的变化并进行相应的操作。实现线程之间通信的方法有以下几种
- 基于volatile修饰共享变量
- 通过wait/notify机制
- Thread.join方法
- 使用 synchronized 关键字
- Conditon 中的await/signal 方法
在介绍Conditon 之前我们先来认识一下wait和notify
wait/notify
wait()和 notify()是 Java 提供的两个方法,它们属于 Object类,因此所有的对象都可以使用这两个方法。这两个方法主要用于线程间的通信和协调,是实现多线程同步的重要手段。
- wait() 方法:
- 当一个线程需要等待某个条件成立才能继续执行时,它可以调用对象的wait()方法。
- 调用wait()方法的线程会被放入对象的等待队列中,等待其他线程调用同一对象的notify() 或notifyAll() 方法。
- 当前线程在等待期间会释放对象的锁,这样其他线程可以获取该对象的锁并修改其状态。
- wait()方法会抛出InterruptedException异常,所以需要使用 try-catch 块来处理。
- notify() 方法:
- 当一个线程修改了对象的状态,并希望唤醒等待该对象的等待队列中的一个线程时,它可以调用对象的notify() 方法。
- 被唤醒的线程将从等待队列中移除,并从对象锁中获得执行机会。
- 注意,notify() 只唤醒一个等待的线程,如果有多个线程在等待,则随机选择一个唤醒。如果想要唤醒所有等待的线程,应该使用 notifyAll()方法。
使用
wait/notify 方法实际上时针对同一共享对象的竞争来实现数据变更的通知,也就是当某个共享变量满足某种条件时会触发唤醒和阻塞,从而实现线程的通信。所以 wait/notify更适用于生产者和消费者的场景,接下来我们就用wait/notify来模拟生产者和消费者的使用场景。
//缓冲
public class Buffer {
private Queue<Integer> queue;
private int maxSize;
public Buffer(int maxSize) {
this.maxSize = maxSize;
queue = new LinkedList<>();
}
public synchronized void put(int value) throws InterruptedException {
while (queue.size() == maxSize) {
wait();
}
queue.add(value);
notify();
}
public synchronized Integer take() throws InterruptedException {
while (queue.isEmpty()) {
wait();
}
notify();
return queue.poll();
}
}
//生产者
public class Producer implements Runnable {
private Buffer buffer;
public Producer(Buffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
int i = 0;
while (true) {
try {
System.out.println("生产者 生产消息: " + i);
TimeUnit.SECONDS.sleep(1);
buffer.put(i++);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//消费者
public class Consumer implements Runnable{
private Buffer buffer;
public Consumer(Buffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
while (true) {
try {
Integer value = buffer.take();
System.out.println("消费者 消费消息: " + value);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//测试类
public class TestExample {
public static void main(String[] args) throws InterruptedException {
Buffer buffer = new Buffer(8);
new Thread(new Producer(buffer)).start();
new Thread(new Consumer(buffer)).start();
}
}
- 在上述代码中,我们定义了Queue作为缓冲区,并使用synchronized关键字来确保线程安全。
- 当queue.size() == maxSize时,生产者调用wait()方法释放锁并进入等待状态。当queue.size() != maxSize时(即消费者从缓冲区中取出了一个消息),它会调用notify()唤醒等待的生产者。
- 当queue.isEmpty() == true时,消费者调用wait()方法释放锁并进入等待状态。当queue.isEmpty() == false 时(即生产者向缓冲区中放入了一个消息),它会调用notify()唤醒等待的消费者。
整个过程图来展示的话如下图所示
原理
结合代码和功能描述我们来梳理一下wait()/notify()的原理。
-
线程间的通信必然涉及到锁且wait()/notify()方法依赖于synchronized锁
-
调用wait()方法的线程会阻塞,假设有多个线程同时调用wait()方法,那这些线程会阻塞在哪里?换句话说阻塞的线程怎么存储。使用synchronized关键字抢占锁失败会进入一个同步队列。那基于这个思路,我们可以大致猜出这些阻塞的线程基本上也是放入一个等待队列中
-
当调用notify() 方法时,之前再一个队列中阻塞的线程会移动到synchronized的同步队列并唤醒,整个过程我们通过一个图来简单描述一下
补充
在文章的开头我们提到了Thread.join()方法也能实现线程间的通信,我们从源码层面可以一探究竟。
public final void join() throws InterruptedException {
join(0);
}
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
从源码中我们可以清楚的看到join方法的本质就是wait()和notify()。
Conditon
在了解了wait()/notify()的基本原理之后,我们正式进入Condition的解析。Condition本身的作用和wait()/notify()的作用相同,都是基于条件去唤醒和阻塞。就相当于用java重新写了一个wait()/notify()。所以它们的大致思想是相通的,有了这个前提我们理解起来就会清晰明了很多。
使用
Conditon的使用也很简单,我们还是拿生产者消费者的例子改造一下
public class Buffer {
private Queue<Integer> queue;
private int maxSize;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public Buffer(int maxSize) {
this.maxSize = maxSize;
queue = new LinkedList<>();
}
public void put(int value) {
try {
lock.lock();
while (queue.size() == maxSize) {
condition.await();
}
queue.add(value);
condition.signal();
}catch (InterruptedException e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public Integer take() {
try {
lock.lock();
while (queue.isEmpty()) {
condition.await();
}
condition.signal();
return queue.poll();
}catch (InterruptedException e){
e.printStackTrace();
}finally {
lock.unlock();
}
return null;
}
}
//生产者
public class Producer implements Runnable {
private Buffer buffer;
public Producer(Buffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
int i = 0;
while (true) {
try {
System.out.println("生产者 生产消息: " + i);
TimeUnit.SECONDS.sleep(1);
buffer.put(i++);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//消费者
public class Consumer implements Runnable{
private Buffer buffer;
public Consumer(Buffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
while (true) {
try {
Integer value = buffer.take();
System.out.println("消费者 消费消息: " + value);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
//main方法
public static void main(String[] args) throws InterruptedException {
Buffer buffer = new Buffer(5);
new Thread(new Producer(buffer)).start();
new Thread(new Consumer(buffer)).start();
}
运行结果
需求分析
既然Conditon的作用与wait()/notify()的作用相同,那我们不妨来推导一下Conditon做了什么
-
实现了线程的阻塞和唤醒。
-
它是通过await()和signal()或signalAll() 来实现的阻塞与唤醒
await() : 使线程阻塞并释放锁
signal(): 唤醒阻塞的线程
-
Condition 的使用前提是基于Lock 加锁,加锁的话我们很容易联想到AQS队列
-
当调用await() 释放锁的时候,这个释放锁的线程肯定不会进入AQS队列,当调用signal()方法时又去唤醒阻塞的线程。那么问题来了await()释放的线程阻塞在哪里,signal()又是从哪唤醒的。我们通过对wait()/notify()的了解既然它们是等价的,那是不是会有一个区别于AQS队列之外的数据结构来存储阻塞的线程。signal()方法唤醒的时候也是从这个数据结构中去唤醒。
-
唤醒之后的线程干什么,应该去抢占锁,但它是基于Lock中AQS的机制来抢占锁,所以唤醒之后的线程应该加入到AQS队列中。
类图
源码分析
有了上面的分析之后我们正式开始源码的解析,Condition的主要方法就两个await() 和signal()。我们先从await()入手。
await()
那await()又做了哪些事情
- 调用await()方法的线程要释放锁
- 释放锁的线程要阻塞并且存储到一个数据结构中(队列)可以简称为Condition队列
- 当唤醒之后需要重新抢占锁
- 处理interrupt()的中断响应
有了上述几点梳理之后,我们再来看await()源码,我们先从大的层面来看
public final void await() throws InterruptedException {
//Thread.interrupted() 为ture直接抛出中断异常
if (Thread.interrupted())
throw new InterruptedException();
//构建一个状态为Condition的节点,所以这里采用的数据结构仍然是链表。这里就是把当前线程加入到Condition队列中
Node node = addConditionWaiter();
//fullyRelease直译过来可以理解为完全的释放锁(这里这么做是考虑有重入的情况)
int savedState = fullyRelease(node);
int interruptMode = 0;
//判断构建的node节点在不在同步队列中(即AQS队列)为false则阻塞当前线程
while (!isOnSyncQueue(node)) {
//阻塞当前线程
LockSupport.park(this);
//判断当前被阻塞的线程是不是因为interrupted()方法唤醒的
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//被阻塞的线程去唤醒时尝试获取锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
//处理中断
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
在上述的await()源码分析中我们发现整体逻辑跟我们分析要点一致,接下来我们着手细节,逐步分析
addConditionWaiter()
//从整体上看 Condition中维护了一个等待队列 我们称之为Condition队列,从源码中大致可以看出这个队列是个单向链表
private Node addConditionWaiter() {
//定义一个尾节点
Node t = lastWaiter;
// 如果尾节点不为空则队列不为空并且 节点状态不等于Node.CONDITION 时清理失效节点
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
//第一次构建时 t == null,把当前线程封装成Node且状态为CONDITION并赋值为头节点
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
//如果不为空则加入到队尾
t.nextWaiter = node;
//若t == null 则当前这个节点既是头节点也是尾节点,若t!=null 则把刚加入链表中的节点作为尾节点,这里链表采用的是尾插法
lastWaiter = node;
return node;
}
fullyRelease(node)
//该方法的主要作用就是释放锁
final int fullyRelease(Node node) {
boolean failed = true;
try {
//获取state的值
int savedState = getState();
//release(savedState) 方法就是释放锁的方法,在上一篇讲述Lock时已经有详细解释,这里就不作过多描述了
if (release(savedState)) {
failed = false;
//完全释放锁savedState == 0
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
//如果failed == true 那么这个节点的状态就是CANCELLED,这就跟addConditionWaiter()方法中清理失效节点 //对应起来了
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
isOnSyncQueue(node)
//判断当前节点是否在同步队列中
final boolean isOnSyncQueue(Node node) {
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
if (node.next != null) // If has successor, it must be on queue
return true;
return findNodeFromTail(node);
}
//findNodeFromTail字面意思就是从尾部开始遍历查找,整体比较简单
private boolean findNodeFromTail(Node node) {
Node t = tail;
for (;;) {
//在队列中找到该节点返回true
if (t == node)
return true;
//没有找到返回false
if (t == null)
return false;
t = t.prev;
}
}
这里强调一下isOnSyncQueue()是判断当前节点是否在AQS队列中,由于fullyRelease()已经释放了锁,所以第一次判断的结果是false。则去阻塞当前线程
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
await()源码整体上相对简单,接下来我们来看signal()方法
signal()
signal() 方法做了什么
- 将阻塞的线程唤醒
- 把唤醒后的线程从Condition队列移动到AQS队列中
理清了这两点之后我们再来看signal()的源码
public final void signal() {
//isHeldExclusively()是判断当前线程是不是获得锁的线程,因为signal()调用的前提是获得锁。
//如果isHeldExclusively()==false,则直接抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//获取到队列的头节点,因为Condition队列是个单向链表,所以有了头节点就能找到整个链表
Node first = firstWaiter;
if (first != null)
//如果头节点不为空 调用doSignal(first)方法,很明显这个方法是唤醒阻塞的线程和队列转移的具体的执行
doSignal(first);
}
//doSignal()方法里面是个do-while循环,do{}里面我们可以看到是操作链表的指向,也就是说while()里边的条件才是唤醒和转移队列
//transferForSignal()是重点方法
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
//通过CAS操作修改Node的状态,由CONDITION修改为0,如果不成功则该节点需要取消
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//看到enq这个方法我们会非常的眼熟,AQS中获取锁失败时也是调用这个方法加入等待队列中,这里就完成了从Condition队列到等待队列的转移
Node p = enq(node);
int ws = p.waitStatus;
//如果ws > 0 或者通过CAS修改状态失败唤醒线程
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
这个地方有的小伙伴可能对唤醒的条件有疑问,既然加入了AQS队列了那直接等着触发锁竞争的机制不就能唤醒,为什么满足ws > 0或者compareAndSetWaitStatus(p, ws, Node.SIGNAL) == false其中一个就直接唤醒了。我们先来屡一下条件
- 因为我们通过enq(node)得到返回的p节点是原来AQS队列的尾节点,那ws>0表示原来的尾节点的状态是CANCELLED
- compareAndSetWaitStatus(p, ws, Node.SIGNAL) 修改原来AQS队列的尾节点的状态为SINGAL ,如果结果为false 实际上不会唤醒任何正在等待的线程。这是一种回退策略,确保即使无法原子性地更新等待状态,线程仍然能够被正确地唤醒并继续执行。这样做的目的是为了提高并发程序的健壮性,防止线程因为无法获取所需的资源而永久地被阻塞。所以为了避免死锁直接调用LockSupport.unpark(node.thread)唤醒节点上的线程
众所周知,唤醒之后的线程会回到调用await()方法里面 LockSupport.park()的位置,我们再来看一段代码
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
//此时checkInterruptWhileWaiting(node) == 0.不是因为中断被唤醒的所以跳出循环去acquireQueued() 抢占锁
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
由此我们可以推断出为什么ws>0这个条件成立时就去唤醒node.thread,是因为上一个节点是CANCELLED的话,AQS队列会清理失效的节点,当节点从Condition队列移动到AQS队列不用等待清理动作完成就可直接进行唤醒,一定程度上优化了性能。
再回到await()
调用了signal()方法后被唤醒的线程会在之前阻塞的位置继续执行,之前我们粗略的说了一下checkInterruptWhileWaiting(node)这个方法是判断被唤醒的线程是不是因为interrupt()方法唤醒的,它具体做了什么我们没有细说,那接下来我们来具体聊一聊这个方法
checkInterruptWhileWaiting(node)
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
在解析这段代码之前,我们先来看一下THROW_IE 和 REINTERRUPT这两个常量代表什么意思
/** Mode meaning to reinterrupt on exit from wait */
//通过注释大致翻译为结束时重新中断
private static final int REINTERRUPT = 1;
/** Mode meaning to throw InterruptedException on exit from wait */
//通过注释大致翻译为结束时抛出InterruptedException异常
private static final int THROW_IE = -1;
- 当Thread.interrupted() == true 代表被中断过,则调用transferAfterCancelledWait(node)方法。
- 当Thread.interrupted() == false 代表没有被中段,直接返回0,继续进入while(!isOnSyncQueue(node)) 这个循环判断,这里又是通过调用signal() 方法,此时节点由Condition队列转移到了AQS对队列,所以isOnSyncQueue(node) == true 直接跳出循环
- 如果transferAfterCancelledWait(node) == true 返回-1则是抛出中断异常,反之返回1 重新中断
我们再来看transferAfterCancelledWait(node) 这个方法
transferAfterCancelledWait(node)
final boolean transferAfterCancelledWait(Node node) {
//因为这是通过Thread.interrupted() == true才执行的方法,如果CAS能成功则证明线程中断的时候还没有调用signal()方法
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
enq(node);
return true;
}
while (!isOnSyncQueue(node))
Thread.yield();
return false;
}
- 如果 compareAndSetWaitStatus(node, Node.CONDITION, 0) == true 则证明还没有调用 signal() 方法线程就已经中断了,那把包含该线程的节点直接加入到AQS队列等待锁的抢占。
- 如果compareAndSetWaitStatus(node, Node.CONDITION, 0) == false 则证明线程的中断是在signal()方法调用之后发生的,判断包含该节点的线程是否在AQS队列中,如果不在,调用yield()方法让出CPU时间片,让其他线程执行。
源码分析到这里我们就把await() 到 signal() 执行流程就都串联起来了,我们再把目光聚焦到await() 方法中来
public final void await() throws InterruptedException {
//........省略部分代码
//await() 方法中处理中断的这部分逻辑就变得清晰了
//唤醒之后先让当前线程去尝试获得锁,抢占锁成功,继续执行后续代码
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
//清理cancelled节点
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
//根据interruptMode来判断是抛异常还是重新触发一次中断
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
总结
本篇我们先是通过wait()/notify()来了解了线程通信的机制。因为wait()/notify()是JVM层面控制的,我们无法窥探它的源码。好在J.U.C中提供了Condition,可以把它理解为wait()/notify()的平替,但它们的原理是相通的,都是在持有同一把锁的基础上,通过线程的阻塞和唤醒来实现的线程通信。Condition在我们日常开发中基本不会用到,但它更多会出现在其他中间件的源码中,所以了解它的核心原理也是有必要的。