【java多线程】线程通信(以生产者消费者问题为例)

线程通信(主讲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;
    }
}
代码解释

这段代码实现了基于管程法的生产者 - 消费者模型,以麦当劳制作和消费炸鸡为例,通过多线程和缓冲区实现了生产者与消费者之间的交互,具体总结如下:

  1. 整体架构ThreadPC1 类的 main 方法作为程序入口,创建共享的缓冲区 Container 实例,以及生产者线程类 Worker 和消费者线程类 Customer 的实例,并启动这两个线程。
  2. 生产者线程(Worker 类):继承自 Thread 类,通过构造函数接收缓冲区实例。run 方法中循环制作 100 只炸鸡,每制作一只就调用缓冲区的 push 方法放入炸鸡,并打印已制作的炸鸡编号。
  3. 消费者线程(Customer 类):同样继承自 Thread 类,接收缓冲区实例。run 方法循环从缓冲区调用 pop 方法取出 100 只炸鸡,并打印正在吃的炸鸡编号。
  4. 缓冲区(Container 类):作为共享资源,维护一个可容纳 10 只炸鸡的数组和记录炸鸡数量的变量 countpush 方法和 pop 方法都使用 synchronized 关键字保证线程安全,在缓冲区满或空时分别调用 wait 方法使生产者或消费者线程等待,条件满足时调用 notifyAll 方法唤醒等待线程。
  5. 产品类(Chicken 类):表示炸鸡,拥有编号 id,提供构造函数及 getIdsetId 方法来操作编号。

该代码通过多线程、线程同步(waitnotifyAll)和共享缓冲区,成功模拟了生产者不断生产、消费者不断消费的过程,解决了多线程环境下的并发问题。

聊聊这个过程的线程通信

刚开始两个线程一起运行,但是由于此时前台没有炸鸡,所以消费者线程会等待,然后生产者线程继续执行,做出炸鸡后会通知消费者线程,两个线程再次一起进行,一个做一个吃;如果生产者已经做了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();
    }

}
代码解释:
  1. main 方法
    • 创建 TV 对象、Watcher 线程和 Actor 线程,并启动它们。
    • 使用 join 方法等待 actor 线程和 watcher 线程执行完毕,避免主线程提前退出。
    • 当两个线程都执行完毕后,输出提示信息表示表演和观看都已结束,程序退出。
  2. Watcher
    • run 方法中,观众观看 20 个节目。
    • 观看完所有节目后,输出提示信息表示观众已看完所有节目。
  3. Actor
    • run 方法中,演员表演 20 个节目,根据循环次数的奇偶性决定表演的节目。
    • 表演完所有节目后,输出提示信息表示演员已表演完所有节目。
  4. TV
    • act 方法:演员表演节目,如果 flagfalse,表示观众还没看完,演员线程等待;表演完节目后,修改 flag 并通知观众线程。
    • watch 方法:观众观看节目,如果 flagtrue,表示演员还没表演,观众线程等待;看完节目后,修改 flag 并通知演员线程。
聊聊这个过程的线程通信

这个跟上面的麦当当示例不同,因为上面的可以在缓存区存10只炸鸡再取,但是在标志位的情况下,只能是演员表演一个节目后就要等待,等待观众看完这个节目后才会表演新的节目。

线程通信是通过标志位的判断来进行的,如果flag为true演员表演,所以观众会wait,演员表演完一个节目后flag就会变为false并通知观众,且此时flag为false演员不能继续表演会wait,会释放锁,然后观众可以拿到锁开始观看节目,观看后flag又变为true,并通知演员线程,并且自己这个线程wait,循环往复。

结果:

演员正在表演---->快乐大本营
观众正在看---->快乐大本营
演员正在表演---->花千骨
观众正在看---->花千骨
演员正在表演---->快乐大本营
观众正在看---->快乐大本营
演员正在表演---->花千骨
观众正在看---->花千骨

记住一句话:休眠放在通知前面,可以有效避免两条线程同时休眠。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值