多线程——线程通信——一个生产者和消费者

本文通过厨师长和服务员的模拟案例,展示了Java中多线程通信的问题,如线程执行顺序不可控、资源竞争导致的数据不一致等。通过引入同步机制和等待唤醒机制,解决了这些问题,确保了厨师长做菜与服务员端菜的交替进行,并避免了数据错误。代码示例中详细解释了同步方法和等待唤醒机制的使用,以实现线程间的协调通信。
摘要由CSDN通过智能技术生成

多线程间的通信其实就是多个线程都在处理同一个资源,但是处理的任务却不一样。比较经典的案例是生产者与消费者的问题了,下面通过代码进行演示。

说明:通过厨师长做菜,服务员端菜的例子模拟生产者与消费者模型,同时我们要求厨师长做菜与服务员端菜交替进行,即厨师长每做完一道菜,服务员就端一道菜。

public class Demo {
    public static void main(String[] args) {
        Food f = new Food();
        //厨师长进程
        new Thread(new Cook(f)).start();
        //服务员进程
        new Thread(new Waiter(f)).start();
    }
    //厨师长
    static class Cook implements Runnable {
        private Food f;
        public Cook(Food f) {
            this.f = f;
        }
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                if (i % 2 == 0) {
                    f.setNameAndTaste("小白生煎","甜口");
                } else {
                    f.setNameAndTaste("煎饼果子","香辣");
                }
            }
        }
    }
    //服务生
    static class Waiter implements Runnable {
        private Food f;
        public Waiter(Food f) {
            this.f = f;
        }
        @Override
        public void run() {
            for (int i=0; i<100; i++) {
                //控制输出间隔时间,同时让出错更明显
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                f.get();
            }
        }
    }
    //食物
    static class Food {
        private String name;
        private String taste;
        public void setNameAndTaste(String name,String taste) {
            this.name = name;
            //控制输出间隔时间,同时让出错更明显
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.taste = taste;
            System.out.println("厨师长做菜:"+this.name+","+this.taste);
        }
        public void get() {
            System.out.println("服务生端菜:"+this.name+","+this.taste);
        }
    }
}

运行结果:
在这里插入图片描述
能看出代码的几个问题:

  1. 存在乱序的情况,厨师和服务员的行为并不是交替进行的;
  2. 厨师连做两碗菜,服务员连端两碗菜;
  3. 菜的味道“变了”,小白生煎设置是甜口的,煎饼果子设置是香辣的,更离谱的是最开始的菜的味道值可能为null。

结果分析:

对于 问题1 和 问题2,我们知道 线程抢占资源的概率是相同的(不设置优先级的情况下),因此厨师长做菜和服务员端菜的顺序在上面的代码里完全控制不了;

对于 问题3 我们试想这种情况:厨师抢到资源刚设置菜的名字,还没来得及设置菜的味道(代码中通过sleep()实现),服务员就把菜端走了,而此时的菜的味道还是上一碗菜的味道,因此会出现菜的味道值为null的情况。

解决问题3,我们使用同步机制,把Food类中的setNameAndTaste()方法和get()方法设置成同步函数。同步方法使得厨师长进程在调用setNameAndTaste()时,服务员进程呈阻塞状态,不会执行get()方法。同理,当get()方法被调用时,setNameAndTaste()方法不会被调用。修改的代码如下:

public synchronized void setNameAndTaste(String name,String taste) {
            this.name = name;
            //控制输出间隔时间,同时让出错更明显
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.taste = taste;
            System.out.println("厨师长做菜:"+this.name+","+this.taste);
}
public synchronized void get() {
            System.out.println("服务生端菜:"+this.name+","+this.taste);
}

解决问题1、2前,我们先搞清楚问题出在哪:当厨师长线程调用setNameAndTaste()方法后,一个“回首掏”,厨师长线程又抢到资源,接着调用setNameAndTaste()方法,相同的事情也会发生在服务员线程身上,因此会出现厨师长连续做几次菜,服务员连续端几次菜的打印。
根据问题描述我们假设这样一种场景:厨师长线程调用完set()方法后,让它“暂停一下”,并“通知”服务员线程,可以调用get()方法了;当服务员线程调用完get()方法后,同样“暂停一下”,并“通知”厨师长线程,可以调用set()方法了。如果两线程这样交替执行,就可以达到预期的结果。

Java为我们提供了这样一种实现多线程通信的机制——等待唤醒机制

常用方法(所属类:Object类):

  • void notify() 唤醒正在此对象监视器上等待的单个线程。
  • void notifyAll() 唤醒等待此对象监视器的所有线程。
  • void wait() 导致当前线程等待它被唤醒,通常是 通知 或 中断 。

注意:

  • 在线程中调用上述方法时要用synchronized锁住对象,确保代码段不会被多个线程调用。
  • 如果没有synchronized加锁,那么当前的线程不是此对象监视器的所有者, 就会抛出 IllegalMonitorStateException 异常信息。

最后的代码如下:

public class Demo {
    public static void main(String[] args) {
        Food f = new Food();
        //厨师长进程
        new Thread(new Cook(f)).start();
        //服务员进程
        new Thread(new Waiter(f)).start();
    }
    //厨师长
    static class Cook implements Runnable {
        private Food f;
        public Cook(Food f) {
            this.f = f;
        }
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                if (i % 2 == 0) {
                    f.setNameAndTaste("小白生煎","甜口");
                } else {
                    f.setNameAndTaste("煎饼果子","香辣");
                }
            }
        }
    }
    //服务员
    static class Waiter implements Runnable {
        private Food f;
        public Waiter(Food f) {
            this.f = f;
        }
        @Override
        public void run() {
            for (int i = 0; i < 100; i++)
                f.get();
        }
    }
    //食物
    static class Food {
        private String name;
        private String taste;
        //标记,true表示厨师炒菜,false表示服务员上菜
        private boolean flag = true;
        public synchronized void setNameAndTaste(String name,String taste) {
            if (flag) {
                this.name = name;
                this.taste = taste;
                System.out.println("厨师长做菜:"+this.name+","+this.taste);
                flag = false;
                this.notify(); //这里用notify()和notifyAll()都可以,注意在多个生产者和消费者的案例中notify()会引起异常!!!下同
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        public synchronized void get() {
            if (!flag) {
                System.out.println("服务生端菜:"+this.name+","+this.taste);
                flag = true;
                this.notify();
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

运行结果:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值