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

文章目录

前言

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

一段逻辑代码块的执行可能会依赖于某个先决条件,在单线程程序中可以使用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();
	}
}
相关推荐
zwhdlb10 分钟前
Java + 工业物联网 / 智慧楼宇 面试问答模板
java·物联网·面试
Pitayafruit11 分钟前
Spring AI 进阶之路04:集成 SearXNG 实现联网搜索
spring boot·后端·ai编程
风象南14 分钟前
SpringBoot 自研「轻量级 API 防火墙」:单机内嵌,支持在线配置
后端
刘一说26 分钟前
CentOS 系统 Java 开发测试环境搭建手册
java·linux·运维·服务器·centos
Victor35631 分钟前
Redis(14)Redis的列表(List)类型有哪些常用命令?
后端
Victor35631 分钟前
Redis(15)Redis的集合(Set)类型有哪些常用命令?
后端
卷福同学33 分钟前
来上海三个月,我在马路边上遇到了阿里前同事...
java·后端
bingbingyihao2 小时前
多数据源 Demo
java·springboot
在努力的前端小白7 小时前
Spring Boot 敏感词过滤组件实现:基于DFA算法的高效敏感词检测与替换
java·数据库·spring boot·文本处理·敏感词过滤·dfa算法·组件开发
bobz9659 小时前
小语言模型是真正的未来
后端