线程间协作——等待与通知

文章目录

前言

系统的稳定运行,在单线程程序中得益于类与类之间的协作,在多线程程序中,还得益于线程与线程之间的协作。

一段逻辑代码块的执行可能会依赖于某个先决条件,在单线程程序中可以使用if来构建分支,在多线程程序中可以使用Java提供的等待-通知功能。

例如:生产者消费者模式中,消费者工作的先决条件是产品的数量大于0,生产者工作的先决条件是产品没有造成堆积。

等待:线程因执行目标动作的先决条件暂时没有满足而被暂停的过程。

通知:线程修改了先决条件的结果,使得其他线程可以继续工作而被唤醒的过程。

wait和notify

在Java中,Object类是所有类的父类,Object类提供了实现等待-通知功能的方法,意味着所有对象都具有等待-通知功能,方法如下:

调用wait()会释放当前线程持有的锁并阻塞,直到被其他线程唤醒或等待超时。

  • Object::wait()
  • Object::wait(long timeout)
  • Object::wait(long timeout, int nanos)

notify()会随机唤醒一个线程,notifyAll()会唤醒所有线程。

  • Object::notify()
  • Object::notifyAll()

不管是wait()还是notify(),既然是方法就意味着可以被多个线程反复执行,因此一个对象可能存在多个等待线程。
JVM除了会为每个对象维护一个入口集(Entry Set),用于存储申请对象监视器锁的线程外,还会维护一个等待集(Wait Set)的队列,用于存储该对象上的等待线程。
Object.wait()会释放当前线程抢占的对象锁,然后将当前线程暂停并加入到对象的等待集中。
Object.notify()会唤醒对象等待集中的任意一个线程,被唤醒的线程并不会立马从等待集中剔除,而是继续抢占对象锁,只有当线程成功抢到锁后,才会从等待集中剔除。

wait、notify、notifyAll调用的先决条件是线程已经获得对象锁,因此只能在临界区中调用。

存在的问题

  • 过早唤醒
    一个类可能存在多个先决条件,有的线程满足先决条件A时执行,有的线程满足先决条件B时执行。使用notify()可能发生漏唤醒,这时不得不使用notifyAll(),但是notifyAll()会唤醒所有等待线程,使得不该唤醒的线程被唤醒,线程唤醒后抢占不到锁又会被阻塞,会导致过多的线程上下文切换,影响性能。
  • 信号丢失
    如果线程在进入wait()前没有判断先决条件是否成立,就会导致其他线程已经修改先决条件结果并发出通知,但是此时等待线程还没有被暂停,也就没法被唤醒,错过了通知信号。将先决条件的判断和wait()放在循环语句中可以解决。
  • 虚假唤醒
    等待线程存在没有任何线程通知的情况下被唤醒的可能,虽然概率非常低,但是OS和Java是允许这种情况存在的,如果等待线程被虚假唤醒,但是先决条件却没有成立就会导致问题。将先决条件的判断和wait()放在循环语句中可以解决。
  • 线程调度开销
    notify()本身并不会释放锁,只有当同步代码块执行完毕才会释放,这会导致等待线程虽然被唤醒,但是由于抢占不到对象锁而被再次挂起,无故增加了操作系统线程上下文切换的开销。

notify和notifyAll用哪个?

notify()只会唤醒一个线程,存在漏唤醒信号丢失的可能,notifyAll()效率不高,会唤醒本不应该被唤醒的线程,但是它在正确性方面有保障。

如果满足以下条件,那么优先使用notify(),否则使用notifyAll():

  • 每次最多只会唤醒一个线程。
  • 等待集中的所有线程均为同质线程(线程干的活一模一样)。

生产者消费者实战

如下例子,分别启动5个生产者和消费者,库存最多为1,生产完即通知消费,消费完即通知生产。

如果不把先决条件和wait()放在循环里,将导致产品被重复消费和生产。

java 复制代码
public class Store {
	int stock = 0;

	synchronized void consumer() {
		if (stock <= 0) {
			try {
				wait();
				// 先决条件和wait()应该放在循环中,因为被唤醒后stock可能已经被其他线程消费掉了。
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		stock--;
		System.out.println(Thread.currentThread().getName() + " 消费成功,库存:" + stock);
		notifyAll();
	}

	synchronized void provider() {
		if (stock > 0) {
			try {
				wait();
				// 先决条件和wait()应该放在循环中,因为被唤醒后stock可能已经被其他线程生产,导致重复生产。
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		stock++;
		notifyAll();
	}

	public static void main(String[] args) {
		Store store = new Store();
		for (int i = 0; i < 5; i++) {
			new Thread(() -> {
				while (true) {
					store.consumer();
				}
			}).start();
		}
		for (int i = 0; i < 5; i++) {
			new Thread(() -> {
				while (true) {
					store.provider();
				}
			}).start();
		}
	}
}

程序运行错误结果:

bash 复制代码
Thread-0 消费成功,库存:6
Thread-0 消费成功,库存:5
Thread-4 消费成功,库存:4
Thread-4 消费成功,库存:3
Thread-4 消费成功,库存:2
Thread-4 消费成功,库存:1
Thread-4 消费成功,库存:0
Thread-2 消费成功,库存:-1
Thread-1 消费成功,库存:-2
Thread-2 消费成功,库存:-3
Thread-4 消费成功,库存:-4

将if判断改为while循环即可解决该问题。

条件变量Condition

除了使用Object提供的通知-等待功能外,JUC提供了功能更加强大的条件变量类Condition。

Condition需要配合显示锁Lock使用,增强功能如下:

  • 支持等待超时唤醒、被通知唤醒的判断。
  • 支持纳秒级的等待超时。
  • 支持多条件等待队列,避免了过早唤醒。

使用lock.newCondition()即可创建一个Condition实例,每个Condition实例内部都维护了一个存储等待线程的队列,调用不同Condition实例的signal()只会唤醒该队列里的线程,其他队列中的线程不会受影响,解决了notifyAll()线程过早唤醒的问题。

Object.wait()虽然支持等待超时,但是程序无法判断线程被唤醒是因为超时唤醒还是被通知唤醒,Condition支持这种判断。
awaitUntil(Date deadline)返回一个boolean值,false表示等待超时,true表示被通知唤醒。

如下示例代码,构建了两个Condition实例,不用的业务逻辑执行取决于不同的先决条件:

java 复制代码
public class ConditionDemo {
	private Lock lock = new ReentrantLock();
	//先决条件A
	private Condition conditionA = lock.newCondition();
	//先决条件B
	private Condition conditionB = lock.newCondition();

	//逻辑A
	void logicA(){
		lock.lock();
		try {
			conditionA.await();
			System.out.println(Thread.currentThread().getName() + " logicA...");
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}

	//逻辑B
	void logicB(){
		lock.lock();
		try {
			conditionB.await();
			System.out.println(Thread.currentThread().getName() + " logicB...");
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}

	//只唤醒队列A
	void signalA(){
		lock.lock();
		try {
			conditionA.signalAll();
		}finally {
			lock.unlock();
		}
	}

	//只唤醒队列B
	void signalB(){
		lock.lock();
		try {
			conditionB.signalAll();
		}finally {
			lock.unlock();
		}
	}

	public static void main(String[] args) throws InterruptedException {
		ConditionDemo demo = new ConditionDemo();
		for (int i = 0; i < 5; i++) {
			new Thread(()->{
				demo.logicA();
			}).start();
		}

		for (int i = 0; i < 5; i++) {
			new Thread(()->{
				demo.logicB();
			}).start();
		}
		Thread.sleep(1000);
		demo.signalA();

		Thread.sleep(3000);
		demo.signalB();
	}
}
相关推荐
腾渊信息科技公司8 小时前
Spring Boot对接MES实战:视觉检测数据自动同步方案
java·人工智能·spring boot·后端·计算机视觉·ai·软件需求
爱笑的源码基地9 小时前
高并发 Redis 缓存门诊HIS系统源码,含财务统计药房进销存
java·程序·门诊系统·诊所系统·云诊所源码
IT_陈寒9 小时前
Vue这个特性差点让我加班到凌晨,谁懂啊
前端·人工智能·后端
段一凡-华北理工大学9 小时前
向量数据库实战:选型、调优与落地~系列文章12:文本分块策略实战:chunk_size 怎么选?重叠多少?
开发语言·数据库·后端·oracle·rust·工业智能体·高炉智能化
码事漫谈10 小时前
1999年的电商前夜和2026年的AI前夜 历史押韵但从不重复
后端
Conan在掘金11 小时前
鸿蒙报错速查:arkts-strict-typing Property does not exist on type 'object',object 装函数就炸,根因 + 真解法
后端
颜酱11 小时前
01 | 骨架搭建:FastAPI + Vue 跑通第一个 SSE 流式问答
前端·人工智能·后端
程序员cxuan12 小时前
Grok Build 被众人唾骂,结果老马把它开源了
人工智能·后端·程序员
莫逸风12 小时前
【AgentScope 2.0】 0. 学习指南
java·llm·agent·agentscope
从零开始的代码生活_12 小时前
C++ 继承详解:访问控制、对象模型、菱形继承与设计取舍
开发语言·c++·后端·学习·算法