当前位置 : 主页 > 编程语言 > java >

Java多线程通信问题深入了解

来源:互联网 收集:自由互联 发布时间:2021-08-21
目录 概述 引入 加入线程安全 实现生产者与消费者问题 总结 概述 多线程通信问题,也就是生产者与消费者问题 生产者和消费者为两个线程,两个线程在运行过程中交替睡眠,生产者
目录
  • 概述
    • 引入
  • 加入线程安全
    • 实现生产者与消费者问题
      • 总结

        概述

        多线程通信问题,也就是生产者与消费者问题

        生产者和消费者为两个线程,两个线程在运行过程中交替睡眠,生产者在生产时消费者没有在消费,消费者在消费时生产者没有在生产,确保数据安全

        以下为百度百科对于该问题的解释:

        生产者与消费者问题:
        生产者消费者问题(Producer-consumer problem),也称有限缓冲问题(Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了两个共享固定大小缓冲区的线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。

        解决办法:
        要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。通常采用进程间通信的方法解决该问题,常用的方法有信号灯法等。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。该问题也能被推广到多个生产者和消费者的情形。

        引入

        该过程可以类比为一个栗子:

        厨师为生产者,服务员为消费者,假设只有一个盘子盛放食品。

        厨师在生产食品(厨师线程运行)的过程中,服务员应当等待(服务员线程睡眠),等到食品生产完成(厨师线程结束)后将食品放入盘子中,服务员将盘子端出去(服务员线程运行),此时没有盘子可以放食品,因此厨师休息(厨师线程休眠),一段时间过后服务员将盘子拿回来(服务员线程结束),厨师开始进行生产食品(厨师线程运行),服务员在一旁等待(服务员线程睡眠)…

        在此过程中,厨师和服务员两个线程交替睡眠,厨师在做饭时服务员没有端盘子(厨师线程运行时服务员线程睡眠),服务员在端盘子时厨师没有在做饭(服务员线程运行时厨师线程睡眠),确保了数据的安全

        根据厨师和服务员这个栗子,我们可以通过代码来一步步实现

        • 定义厨师线程
         /**
             * 厨师,是一个线程
             */
            static class Cook extends Thread{
                private Food f;
                public Cook(Food f){
                    this.f = f;
                }
                //运行的线程,生成100道菜
                @Override
                public void run() {
                    for (int i = 0 ; i < 100; i ++){
                        if(i % 2 == 0){
                            f.setNameAneTaste("小米粥","没味道,不好吃");
                        }else{
                            f.setNameAneTaste("老北京鸡肉卷","甜辣味");
                        }
                    }
                }
            }
        
        • 定义服务员线程
        /**
             * 服务员,是一个线程
             */
            static class Waiter extends Thread{
                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();
                    }
                }//end run
            }//end waiter
        
        • 新建食物类
         /**
             * 食物,对象
             */
            static class Food{
                private String name;
                private String taste;
                public void setNameAneTaste(String name,String taste){
                    this.name = name;
                    //加了这段之后,有可能这个地方的时间片更有可能被抢走,从而执行不了this.taste = taste
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    this.taste = taste;
                }//end set
                public void get(){
                    System.out.println("服务员端走的菜的名称是:" + this.name + " 味道:" + this.taste);
                }
            }//end food
        

        main方法中去调用两个线程

            public static void main(String[] args) {
                Food f = new Food();
                Cook c = new Cook(f);
                Waiter w = new Waiter(f);
                c.start();//厨师线程
                w.start();//服务生线程     
            }
        

        运行结果:

        只截取了一部分,我们可以看到,“小米粥”并没有每次都对应“没味道,不好吃”,“老北京鸡肉卷”也没有每次都对应“甜辣味”,而是一种错乱的对应关系

        ...
        服务员端走的菜的名称是:老北京鸡肉卷 味道:没味道,不好吃
        服务员端走的菜的名称是:小米粥 味道:甜辣味
        服务员端走的菜的名称是:老北京鸡肉卷 味道:没味道,不好吃
        服务员端走的菜的名称是:小米粥 味道:甜辣味
        服务员端走的菜的名称是:老北京鸡肉卷 味道:没味道,不好吃
        服务员端走的菜的名称是:小米粥 味道:甜辣味
        服务员端走的菜的名称是:老北京鸡肉卷 味道:没味道,不好吃
        服务员端走的菜的名称是:小米粥 味道:甜辣味
        服务员端走的菜的名称是:老北京鸡肉卷 味道:没味道,不好吃
        服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
        ...

        name和taste对应错乱的原因:

        当厨师调用set方法时,刚设置完name,程序进行了休眠,此时服务员可能已经将食品端走了,而此时的taste是上一次运行时保留的taste。

        两个线程一起运行时,由于使用抢占式调度模式,没有协调,因此出现了该现象

        以上运行结果解释如图:

        在这里插入图片描述

        加入线程安全

        针对上面的线程不安全问题,对厨师set和服务员get这两个线程都使用synchronized关键字,实现线程安全,即:当一个线程正在执行时,另外的线程不会执行,在后面排队等待当前的程序执行完后再执行

        代码如下所示,分别给两个方法添加synchronized修饰符,以方法为单位进行加锁,实现线程安全

        	/**
             * 食物,对象
             */
            static class Food{
                private String name;
                private String taste;
                public synchronized void setNameAneTaste(String name,String taste){
                    this.name = name;
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    this.taste = taste;
                }//end set
                public synchronized void get(){
                    System.out.println("服务员端走的菜的名称是:" + this.name + " 味道:" + this.taste);
                }
            }//end food
        

        输出结果:

        由输出可见,又出现了新的问题:
        虽然加入了线程安全,set和get方法不再像前面一样同时执行并且菜名和味道一一对应,但是set和get方法并没有交替执行(通俗地讲,不是厨师一做完服务员就端走),而是无序地执行(厨师有可能做完之后继续做,做好几道,服务员端好几次…无规律地做和端)

        ...
        服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
        服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
        服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
        服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
        服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
        服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
        服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
        服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
        服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
        服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
        服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
        服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
        服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
        服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
        服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
        服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
        服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
        服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
        服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
        服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
        ...

        实现生产者与消费者问题

        由上面可知,加入线程安全依旧无法实现该问题。因此,要解决该问题,回到前面的引入部分,严格按照生产者与消费者问题中所说地去编写程序

        生产者与消费者问题:
        生产者和消费者为两个线程,两个线程在运行过程中交替睡眠,生产者在生产时消费者没有在消费,消费者在消费时生产者没有在生产,确保数据安全

        厨师在生产食品(厨师线程运行)的过程中,服务员应当等待(服务员线程睡眠),等到食品生产完成(厨师线程结束)后将食品放入盘子中,服务员将盘子端出去(服务员线程运行),此时没有盘子可以放食品,因此厨师休息(厨师线程休眠),一段时间过后服务员将盘子拿回来(服务员线程结束),厨师开始进行生产食品(厨师线程运行),服务员在一旁等待(服务员线程睡眠)…

        在此过程中,厨师和服务员两个线程交替睡眠,厨师在做饭时服务员没有端盘子(厨师线程运行时服务员线程睡眠),服务员在端盘子时厨师没有在做饭(服务员线程运行时厨师线程睡眠),确保数据的安全

        需要用到的java.lang.Object 中的方法:

        变量和类型 方法 描述 void notify() 唤醒当前this下的单个线程 void notifyAll() 唤醒当前this下的所有线程 void wait() 当前线程休眠 void wait​(long timeoutMillis) 当前线程休眠一段时间 void wait​(long timeoutMillis, int nanos) 当前线程休眠一段时间
        • 首先在Food类中加一个标记flag:

        True表示厨师生产,服务员休眠

        False表示服务员端菜,厨师休眠

        private boolean flag = true;

        对set方法进行修改

        当且仅当flag为True(True表示厨师生产,服务员休眠)时,才能进行做菜操作

        做菜结束时,将flag置为False(False表示服务员端菜,厨师休眠),这样厨师在生产完之后不会继续生产,避免了厨师两次生产、服务员端走一份的情况

        然后唤醒在当前this下休眠的所有进程,而厨师线程进行休眠

        		public synchronized void setNameAneTaste(String name,String taste){
                    if(flag){//当标记为true时,表示厨师可以生产,该方法才执行
                        this.name = name;
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        this.taste = taste;
                        flag = false;//生产完之后,标记置为false,这样厨师在生产完之后不会继续生产,避免了厨师两次生产、服务员端走一份的情况
                        this.notifyAll();//唤醒在当前this下休眠的所有进程
                        try {
                            this.wait();//此时厨师线程进行休眠
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }//end set
        
        • 对get方法进行修改

        当且仅当flag为False(False表示服务员端菜,厨师休眠)时,才能进行端菜操作

        端菜结束时,将flag置为True(True表示厨师生产,服务员休眠),这样服务员在端完菜之后不会继续端菜,避免了服务员两次端菜、厨师生产一份的情况

        然后唤醒在当前this下休眠的所有进程,而服务员线程进行休眠

                public synchronized void get(){
                    if(!flag){//厨师休眠的时候,服务员开始端菜
                        System.out.println("服务员端走的菜的名称是:" + this.name + " 味道:" + this.taste);
                        flag = true;//端完之后,标记置为true,这样服务员在端完菜之后不会继续端菜,避免了服务员两次端菜、厨师只生产一份的情况
                        this.notifyAll();//唤醒在当前this下休眠的所有进程
                        try {
                            this.wait();//此时服务员线程进行休眠
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }// end if
                }//end get
        

        作了以上调整之后的程序输出:

        我们可以看到,没有出现数据错乱,并且菜的顺序是交替依次进行的

        ...
        服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
        服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
        服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
        服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
        服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
        服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
        服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
        服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
        服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
        服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
        服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
        服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
        ...

        这就是生产者与消费者问题的一个典型例子

        总结

        本篇文章就到这里了,希望能给你带来帮助,也希望您能够多多关注自由互联的更多内容!

        网友评论