线程通信(主讲wait方法和notifyAll方法)
在 Java 多线程编程中,线程通信是指多个线程之间相互协作、交换信息的过程。为了实现线程通信,Java 提供了一些方法,下面详细介绍常用的线程通信方法:
基于 Object
类的方法
在 Java 中,每个对象都有一个内置的监视器(锁),基于这个特性,Object
类提供了三个用于线程通信的方法:wait()
、notify()
和 notifyAll()
。这些方法必须在 synchronized
代码块或方法中调用,因为它们需要获取对象的锁。
1. wait()
方法
- 作用:使当前线程进入等待状态,直到其他线程调用该对象的
notify()
或notifyAll()
方法。调用wait()
方法时,当前线程会释放对象的锁,允许其他线程进入该对象的同步代码块。 - 示例代码:
class Message {
private String content;
private boolean isAvailable = false;
public synchronized void put(String content) {
while (isAvailable) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
this.content = content;
isAvailable = true;
notifyAll();
}
public synchronized String take() {
while (!isAvailable) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
isAvailable = false;
notifyAll();
return content;
}
}
2. notify()
方法
- 作用:唤醒在此对象监视器上等待的单个线程。如果有多个线程在等待,只会唤醒其中一个线程,具体唤醒哪个线程是不确定的。被唤醒的线程会等待获取对象的锁,然后继续执行。
- 示例代码:参考上面
Message
类的put()
和take()
方法,当满足条件时,调用notifyAll()
唤醒等待的线程。
3. notifyAll()
方法
- 作用:唤醒在此对象监视器上等待的所有线程。所有被唤醒的线程会竞争获取对象的锁,只有一个线程能获取到锁并继续执行,其他线程会继续等待。
- 示例代码:同样参考上面
Message
类的put()
和take()
方法,当条件满足时,调用notifyAll()
唤醒所有等待的线程。
利用管程法来解决生产者消费者问题(利用缓冲区)
示例代码
以麦当当为例,消费者就是顾客,生产者就是工作人员,产品是炸鸡,缓冲区是前台放炸鸡的位置(Container)
package com.demo01;
import java.util.zip.CheckedInputStream;
// 管程法
// 生产者消费者模型——利用缓冲区解决
// 四个对象:生产者、消费者、缓冲区、产品
// 因为生产者和消费者是有行为的,例如生产者生成产品、消费者取产品,所以生产者和消费者可以定义为线程(继承Thread)
// 缓冲区是共享资源,操作时记得处理并发问题
// 以麦当劳为例
public class ThreadPC1 {
public static void main(String[] args) {
// 直接开启消费者和生产者线程即可,但是只有先把前台给他们,前台是共享的
Container container = new Container();
Worker worker = new Worker(container);
Customer customer = new Customer(container);
worker.start();
customer.start();
}
}
// 顾客和员工都得有前台才能与炸鸡交互
// 员工
class Worker extends Thread {
private Container container;
public Worker(Container container) {
this.container = container;
}
@Override
public void run() {
// 员工就是调用前台的做炸鸡方法,目标做100只炸鸡
for (int i = 0; i < 100; i++) {
Chicken chicken = new Chicken(i);
container.push(chicken);
System.out.println("已经做出来第----->"+chicken.getId()+"只炸鸡");
}
}
}
// 顾客
class Customer extends Thread {
private Container container;
public Customer(Container container) {
this.container = container;
}
@Override
public void run() {
// 消费者目标是吃一百只炸鸡
for (int i = 0; i < 100; i++) {
Chicken chicken = container.pop();
System.out.println("已经在吃第------->"+chicken.getId()+"只炸鸡");
}
}
}
// 前台(理解为缓冲区)
class Container {
// 前台有多少炸鸡,最多可以有10只
Chicken[] chickens = new Chicken[10];
// 记录前台有多少炸鸡
int count = 0;
//员工生产炸鸡
public synchronized void push(Chicken chicken){
// 前台已经放满了不能放了
if(count == chickens.length){
// 生产者进程陷入等待
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// 前台没满
chickens[count++] = chicken;
// 在这里通知消费者来取餐
this.notifyAll();
}
// 消费者取炸鸡
public synchronized Chicken pop(){
// 没有炸鸡了,那就消费者等待,并通知生产者快做
if(count == 0){
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// 有多余的炸鸡
count--;
this.notifyAll();
return chickens[count];
}
}
// 炸鸡
class Chicken{
// 炸鸡的编号
int id;
public Chicken(int id) {
this.id = id;
}
public Chicken(){}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
代码解释
这段代码实现了基于管程法的生产者 - 消费者模型,以麦当劳制作和消费炸鸡为例,通过多线程和缓冲区实现了生产者与消费者之间的交互,具体总结如下:
- 整体架构:
ThreadPC1
类的main
方法作为程序入口,创建共享的缓冲区Container
实例,以及生产者线程类Worker
和消费者线程类Customer
的实例,并启动这两个线程。 - 生产者线程(
Worker
类):继承自Thread
类,通过构造函数接收缓冲区实例。run
方法中循环制作 100 只炸鸡,每制作一只就调用缓冲区的push
方法放入炸鸡,并打印已制作的炸鸡编号。 - 消费者线程(
Customer
类):同样继承自Thread
类,接收缓冲区实例。run
方法循环从缓冲区调用pop
方法取出 100 只炸鸡,并打印正在吃的炸鸡编号。 - 缓冲区(
Container
类):作为共享资源,维护一个可容纳 10 只炸鸡的数组和记录炸鸡数量的变量count
。push
方法和pop
方法都使用synchronized
关键字保证线程安全,在缓冲区满或空时分别调用wait
方法使生产者或消费者线程等待,条件满足时调用notifyAll
方法唤醒等待线程。 - 产品类(
Chicken
类):表示炸鸡,拥有编号id
,提供构造函数及getId
、setId
方法来操作编号。
该代码通过多线程、线程同步(wait
、notifyAll
)和共享缓冲区,成功模拟了生产者不断生产、消费者不断消费的过程,解决了多线程环境下的并发问题。
聊聊这个过程的线程通信
刚开始两个线程一起运行,但是由于此时前台没有炸鸡,所以消费者线程会等待,然后生产者线程继续执行,做出炸鸡后会通知消费者线程,两个线程再次一起进行,一个做一个吃;如果生产者已经做了10只炸鸡,前台放不下了,生产者线程就会等待,消费者线程继续执行,当消费者吃了一只炸鸡后就会再通知生产者线程继续执行,因为此时前台还可以放炸鸡。
利用信号灯法解决(利用标志位)
以演员表演,观众观看为例。
示例代码
package com.demo01;
public class ThreadPC2 {
public static void main(String[] args) {
TV tv = new TV();
Watcher watcher = new Watcher(tv);
Actor actor =new Actor(tv);
actor.start();
watcher.start();
}
}
class Watcher extends Thread{
TV tv =new TV();
public Watcher(TV tv){
this.tv = tv;
}
@Override
public void run() {
// 观众看电视就好了(假设有20个节目)
for (int i = 0; i < 20; i++) {
tv.watch();
}
}
}
class Actor extends Thread{
TV tv =new TV();
public Actor(TV tv){
this.tv = tv;
}
@Override
public void run() {
// 演员演20个节目
for (int i = 0; i < 20; i++) {
if(i%2==0){
tv.act("快乐大本营");
}else{
tv.act("花千骨");
}
}
}
}
class TV {
// true表示该演员表演了
// false表示该观看者看了
private String voice; // 表演的节目
private boolean flag = true;
public synchronized void act(String voice){
if(!flag){
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
this.voice = voice;
System.out.println("演员正在表演---->"+voice);
// 表演完了通知观众看
flag = !flag;
this.notify();
}
public synchronized void watch(){
if(flag){
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
flag =!flag;
System.out.println("观众正在看---->"+voice);
// 看完了通知演员来演
this.notify();
}
}
代码解释:
main
方法:- 创建
TV
对象、Watcher
线程和Actor
线程,并启动它们。 - 使用
join
方法等待actor
线程和watcher
线程执行完毕,避免主线程提前退出。 - 当两个线程都执行完毕后,输出提示信息表示表演和观看都已结束,程序退出。
- 创建
Watcher
类:- 在
run
方法中,观众观看 20 个节目。 - 观看完所有节目后,输出提示信息表示观众已看完所有节目。
- 在
Actor
类:- 在
run
方法中,演员表演 20 个节目,根据循环次数的奇偶性决定表演的节目。 - 表演完所有节目后,输出提示信息表示演员已表演完所有节目。
- 在
TV
类:act
方法:演员表演节目,如果flag
为false
,表示观众还没看完,演员线程等待;表演完节目后,修改flag
并通知观众线程。watch
方法:观众观看节目,如果flag
为true
,表示演员还没表演,观众线程等待;看完节目后,修改flag
并通知演员线程。
聊聊这个过程的线程通信
这个跟上面的麦当当示例不同,因为上面的可以在缓存区存10只炸鸡再取,但是在标志位的情况下,只能是演员表演一个节目后就要等待,等待观众看完这个节目后才会表演新的节目。
线程通信是通过标志位的判断来进行的,如果flag为true演员表演,所以观众会wait,演员表演完一个节目后flag就会变为false并通知观众,且此时flag为false演员不能继续表演会wait,会释放锁,然后观众可以拿到锁开始观看节目,观看后flag又变为true,并通知演员线程,并且自己这个线程wait,循环往复。
结果:
演员正在表演---->快乐大本营
观众正在看---->快乐大本营
演员正在表演---->花千骨
观众正在看---->花千骨
演员正在表演---->快乐大本营
观众正在看---->快乐大本营
演员正在表演---->花千骨
观众正在看---->花千骨
记住一句话:休眠放在通知前面,可以有效避免两条线程同时休眠。