【并发编程】 --- 线程间的通信wait、notify、notifyAll

Vicki ·
更新时间:2024-11-11
· 826 次阅读

文章目录1 wait、notify、notifyAll简单介绍1.1 使用方法 + 为什么不是Thread类的方法1.2 什么时候加锁、什么时候释放锁?1.3 notify、notifyAll的区别2 两个比较经典的使用案例2.1 案例1 --- ABCABC。。。三个线程顺序打印问题2.1.1 题目2.1.2 题目分析2.1.3 我的答案2.2 生产者消费者问题2.2.1 题目2.2.2 题目分析2.2.3 我的答案

源码地址:https://github.com/nieandsun/concurrent-study.git

1 wait、notify、notifyAll简单介绍 1.1 使用方法 + 为什么不是Thread类的方法

为什么不是Thread类的方法

首先应该明确wait、notify、notifyAll三个方法都是对锁对象的操作,而锁可以是任何对象。在java的世界中任何对象都属于Object类,因此这三个方法都是Object的方法, 而不是线程对象Thread的方法。

使用方法

需要注意两点:

(1)这三个方法必须在synchronized关键字包含的临界区(简单理解,就是代码块)内使用 (2)使用方式为锁对象.方法(),比如obj.wait(); 1.2 什么时候加锁、什么时候释放锁?

必须要明确以下几点:

(1)notify和notifyAll方法不会释放锁,这两个方法只是通知其他使用该锁当锁但是在wait状态的线程,可以准备抢锁了 这里还要格外注意一点,其他使用该锁当锁且处于wait状态的线程只有被notify或notifyAll唤醒了,才有资格抢锁 (2)某个锁对象调用wait方法会立即释放当前线程的该对象锁 , 且其他线程通过notify/notifyAll方法通知该线程可以抢该对象锁时,如果当前线程抢到了,会从当前锁的wait方法之后开始执行 — 即从哪里wait,从哪里执行; (3)在synchronized、wait、notify、notifyAll的组合里 加锁的方式只有一个即进入同步代码块时加锁; 释放锁的方式有两个: ①锁对象调用wait方法时会释放锁 ;② 走完同步代码块时自动释放锁 1.3 notify、notifyAll的区别 某个锁对象的notify只会唤醒一个使用该锁当锁且处于wait状态的线程; 某个锁对象的notifyAll方法会把所有使用该锁当锁且处于wait状态的线程都唤醒;

使用建议: 为了防止某些线程无法被通知到,建议都使用notifyAll。

2 两个比较经典的使用案例

感觉上学的时候好像就考过下面这两个案例☺☺☺

2.1 案例1 — ABCABC。。。三个线程顺序打印问题 2.1.1 题目

三个线程,线程A不停打印A、线程B不停的打印B、线程C不停的打印C,如何通过synchronized、wait、notifyAll(或notify)的组合,使三个线程不停地且顺序地打印出ABCABC。。。

2.1.2 题目分析

其实我在《【并发编程】— Thread类中的join方法》这篇文章里用join实现过类似的功能,有兴趣的可以看一下。。。

如果使用synchronized、wait、notifyAll(或notify)的组合的话,这个问题可以归结为下图所示的问题。即:

线程A走完 ,线程B走 —> 线程B走完,线程C走 —》 线程C走完,线程A走 。。。。

在这里插入图片描述
以线程A为起点进行分析,可知:

(1)要想线程A走完,线程B接着走,那肯定是线程A释放了线程B所需要的锁,这里设该锁为U,做进一步分析可知:

既然线程B需要线程A释放的锁U,那就意味着此时线程B中的锁U肯定处于wait状态; 同时要想线程A释放了锁U之后,线程B可以被唤醒,线程A还必须得进行锁U的notify或notifyAll

(2)同理,要想线程B走完,线程C走,那肯定是线程C有一把处于wait状态的锁,这里设为V,需要线程B进行该锁的notify或notifyAll 并释放

(3)再同理,要想线程C走完,线程A接着走,那肯定是线程A有一把处于wait的锁,这里设为W,需要线程C进行该锁的notify或notifyAll 并释放

用图可以表示成下面的样子:

在这里插入图片描述
分析到这里我们可以再提炼一下:

(1)每个线程都应该有两把锁 (2)第一把锁是前面的线程释放后自己要抢到的锁、第二把锁是自己要notify或notifyAll的锁,对应到每个线程,就可以这样描述 线程A需要两把锁,一把为线程C需要notify(或notifyAll)+ 释放的锁,可以认为该锁为C锁;另一把是自己需要notify(或notifyAll)+释放的锁,可以认为该锁为A锁 同理,线程B需要A线程notify(或notifyAll)+ 释放的锁A锁,自己需要notify(或notifyAll)+释放的B锁 再同理,线程C需要B线程notify(或notifyAll)+ 释放的锁B锁,自己需要notify(或notifyAll)+释放的C锁

分析到这里后,可以将上图改成下面的样子,这样理解起来,我感觉会更好一些:

在这里插入图片描述

分析到这里就可以写代码了。

2.1.3 我的答案 code package com.nrsc.ch1.base.producer_consumer.ABCABC; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.junit.Test; @Slf4j @AllArgsConstructor public class ABCABC implements Runnable { private String obj; //前一个线程需要释放,本线程需要wait的锁 private Object prev; //本线程需要释放,下一个线程需要wait的锁 private Object self; @Override public void run() { int i = 3; while (i > 0) { //为了在控制台好看到效果,我这里打印3轮 synchronized (prev) { //抢前面线程的锁 synchronized (self) {// 抢到自己应该释放的锁 System.out.println(obj); i--; self.notifyAll(); //唤醒其他线程抢self }//释放自己应该释放的锁 try { //走到这里本线程已经释放了自己应该释放的锁,接下来就需要让自己需要等待的锁进行等待就可以了 if (i > 0) { //我最开始没加这个条件,但是测试发现程序没停,其实分析一下就可以知道 //当前面i--使i=0了,其实该线程就已经完成3次打印了,就不需要再等前面的锁了 //因此这里加了该if判断 prev.wait(); } } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String[] args) throws InterruptedException { Object lockA = new Object(); Object lockB = new Object(); Object lockC = new Object(); //线程A需要等待C线程释放的锁,同时需要释放本线程该释放的锁A new Thread(new ABCABC("A", lockC, lockA)).start(); Thread.sleep(1); //确保开始时A线程先执行 //线程B需要等待A线程释放的锁,同时需要释放本线程该释放的锁B new Thread(new ABCABC("B", lockA, lockB)).start(); Thread.sleep(1); //确保开始时B线程第2个执行 //线程C需要等待B线程释放的锁,同时需要释放本线程该释放的锁C new Thread(new ABCABC("C", lockB, lockC)).start(); } } 测试结果:

在这里插入图片描述

2.2 生产者消费者问题 2.2.1 题目

如下图所示:

(1)有多个生产者,每个生产者都在不断的抢面包厂里的机器生产面包 —> 某个时间段只能有一个生产者进行生产 (2)厂里最多能存储20箱,也就是说当已经有20箱了,各个生产者就不能生产了,需要等待消费者消费了,才能继续生产 (3)消费者也有多个,他们也会抢着去面包厂买面包,但也是某个时间段,只能有一个消费者抢到买面包的资格

在以上条件的基础上,写一个多线程程序,保证在生产者不断生产面包的同时,消费者也在不断的购买面包。
注意: 不能写成生产者先生产了20箱,然后消费者再去消费20箱)
在这里插入图片描述

2.2.2 题目分析

其实我觉得这个很简单,只需要想明白下面的两点肯定就可以把这个代码写出来。

对于生产者

(1)它们要不停地生产,直到面包的箱数大于等于20时,生产者就等待 —> 等着消费者去消费 (2)当面包的箱数小于20时,抢到生产权的生产者就生产,并通知消费者,我刚生产了一个,你们可以再继续消费了

对于消费者

(1)他们要不停地消费,知道面包的箱数为0时,它们就等待 —> 等着生产这去生产 (2)当面包的箱数大于0时,抢到消费权的消费者就消费,并通知生产者,我刚消费了一个,你们可以再继续生产了 2.2.3 我的答案 生产者和消费者 package com.nrsc.ch1.base.producer_consumer.multi; import lombok.extern.slf4j.Slf4j; @Slf4j public class BreadProducerAndConsumer2 { /***面包集合*/ private int i = 0; /*** * 生产者 ,注意这里锁是当前对象,即this */ public synchronized void produceBread() { //如果大于等于20箱,就等待 --- 如果这里为大于20的话,则20不会进入while,则会生产出21箱,所以这里应为>= while (i >= 20) { try { this.wait(); } catch (InterruptedException e) { log.error("生产者{},等待出错", Thread.currentThread().getName(), e); } } //如果不到20箱就继续生产 i++; //生产一箱 log.warn("{}生产一箱面包,现有面包{}个", Thread.currentThread().getName(), i); //生产完,通知消费者进行消费 this.notifyAll(); } /*** * 消费者 */ public synchronized void consumeBread() { //如果没有了就等待 while (i 0,所以进行消费 i--; //消费一箱 log.info("{}消费一个面包,现有面包{}个", Thread.currentThread().getName(), i); //消费完,通知生产者进行生产 this.notifyAll(); } } 测试类 package com.nrsc.ch1.base.producer_consumer.multi; public class MultiTest { public static void main(String[] args) throws InterruptedException { BreadProducerAndConsumer2 pc = new BreadProducerAndConsumer2(); /*** * 不睡眠几秒,效果不是很好, * 因此我在 * 生产者线程里睡了12秒 --- 因为我觉得生产面包的时间应该长 ☻☻☻ * 消费者线程里睡了6秒 --- 因为我觉得买面包的时间应该快 ☻☻☻ */ //生产者线程 for (int i = 0; i { //每个线程都不停的生产 while (true) { try { Thread.sleep(12); } catch (InterruptedException e) { e.printStackTrace(); } pc.produceBread(); } }, "生产者" + i).start(); } //消费者线程 for (int i = 0; i { //每个线程都不停的消费 while (true) { try { Thread.sleep(6); } catch (InterruptedException e) { e.printStackTrace(); } pc.consumeBread(); } }, "消费者" + i).start(); } } } 测试效果如下:

在这里插入图片描述


作者:nrsc



notify 并发编程 wait 并发 线程

需要 登录 后方可回复, 如果你还没有账号请 注册新账号