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

文章目录

前言

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

一段逻辑代码块的执行可能会依赖于某个先决条件,在单线程程序中可以使用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();
	}
}
相关推荐
Xxxx. .Xxxx4 分钟前
C语言程序设计实验与习题指导 (第4版 )课后题-第二章+第三章
java·c语言·开发语言
姜西西_6 分钟前
[Spring]Spring MVC 请求和响应及用到的注解
java·spring·mvc
逸狼6 分钟前
【JavaEE初阶】多线程6(线程池\定时器)
java·开发语言·算法
qq_35323353898 分钟前
【原创】java+springboot+mysql科研成果管理系统设计与实现
java·spring boot·mysql·mvc·web
dawn1912288 分钟前
SpringMVC 入门案例详解
java·spring·html·mvc
极客先躯10 分钟前
高级java每日一道面试题-2024年9月16日-框架篇-Spring MVC和Struts的区别是什么?
java·spring·面试·mvc·struts2·框架篇·高级java
Counter-Strike大牛11 分钟前
MySQL迁移达梦报错,DMException: 第1 行附近出现错误: 无效的表或视图名[ACT_GE_PROPERTY]
java·数据库
我要学编程(ಥ_ಥ)2 小时前
滑动窗口算法专题(1)
java·数据结构·算法·leetcode
niceffking2 小时前
JVM 一个对象是否已经死亡?
java·jvm·算法
真的很上进2 小时前
【Git必看系列】—— Git巨好用的神器之git stash篇
java·前端·javascript·数据结构·git·react.js