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

目录
  • 概述
    • 引入
  • 加入线程安全
  • 实现生产者与消费者问题
  • 总结

概述

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

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

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

生产者与消费者问题:
生产者消费者问题(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

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

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

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

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

总结

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

(0)

相关推荐

  • Java多线程深入理解

    目录 线程 Thread类 Runnable接口创建线程 Thread和Runnable的区别 匿名内部类方式实现线程的创建 线程安全 线程安全 线程同步 同步方法 Lock锁 线程状态 等待唤醒机制 线程间通信 等待唤醒机制 生产者与消费者问题 线程池 线程池的概念 线程池的使用 总结 多线程 并发与并行 并发:指两个或多个事件在同一个时间段内发生. 并行:指两个或多个事件在同一时刻发生(同时发生). 在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU

  • Java多线程通信实现方式详解

    这篇文章主要介绍了Java多线程通信实现方式详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 线程通信的方式: 1.共享变量 线程间通信可以通过发送信号,发送信号的一个简单方式是在共享对象的变量里设置信号值.线程A在一个同步块里设置boolean型成员变量hasDataToProcess为true,线程B也在同步代码块里读取hasDataToProcess这个成员变量.这个简单的例子使用了一个持有信号的对象,并提供了set和get方法. pu

  • Java多线程通信:交替打印ABAB实例

    使用wait()和notify()实现Java多线程通信:两个线程交替打印A和B,如ABABAB public class Test { public static void main(String[] args) { final PrintAB print = new PrintAB(); new Thread(new Runnable() { public void run(){ for(int i=0;i<5;i++) { print.printA(); } } }).start(); n

  • Java Socket实现多线程通信功能示例

    本文实例讲述了Java Socket实现多线程通信功能的方法.分享给大家供大家参考,具体如下: 前面的文章<Java Socket实现单线程通信的方法示例>说到怎样写一个最简单的Java Socket通信,但是文章中的例子有一个问题就是Server只能接受一个Client请求,当第一个Client连接后就占据了这个位置,后续Client不能再继续连接,所以需要做些改动,当Server没接受到一个Client连接请求之后,都把处理流程放到一个独立的线程里去运行,然后等待下一个Client连接请求

  • Java多线程通信wait()和notify()代码实例

    1.wait()方法和sleep()方法: wait()方法在等待中释放锁:sleep()在等待的时候不会释放锁,抱着锁睡眠. 2.notify(): 随机唤醒一个线程,将等待队列中的一个等待线程从等待队列中移到同步队列中. 代码如下 public class Demo_Print { public static void main(String[] args) { Print p = new Print(); new Thread() { public void run() { while (

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

    目录 概述 引入 加入线程安全 实现生产者与消费者问题 总结 概述 多线程通信问题,也就是生产者与消费者问题 生产者和消费者为两个线程,两个线程在运行过程中交替睡眠,生产者在生产时消费者没有在消费,消费者在消费时生产者没有在生产,确保数据安全 以下为百度百科对于该问题的解释: 生产者与消费者问题: 生产者消费者问题(Producer-consumer problem),也称有限缓冲问题(Bounded-buffer problem),是一个多线程同步问题的经典案例.该问题描述了两个共享固定大小缓

  • Java多线程编程实现socket通信示例代码

    流传于网络上有关Java多线程通信的编程实例有很多,这一篇还算比较不错,代码可用.下面看看具体内容. TCP是Tranfer Control Protocol的 简称,是一种面向连接的保证可靠传输的协议.通过TCP协议传输,得到的是一个顺序的无差错的数据流.发送方和接收方的成对的两个socket之间必须建 立连接,以便在TCP协议的基础上进行通信,当一个socket(通常都是server socket)等待建立连接时,另一个socket可以要求进行连接,一旦这两个socket连接起来,它们就可以

  • Java多线程实现TCP网络Socket编程(C/S通信)

    开篇必知必会 在前一篇<基于TCP协议网络socket编程(java实现C/S通信)>,实际存在一个问题,如果服务器端在建立连接后发送多条信息给客户端,客户端是无法全部接收的,原因在于客户端为单线程,只接受了第一条信息,剩余信息阻塞等待下一次发送.所以,这造成了客户端无法处理消息队列,每次只接收并输出一条服务器信息,出现信息不同步问题. 本篇将解决这个问题,详细记录实现java多线程通信,目标是使客户端可以一次接收服务器发送的多条信息,避免阻塞.方法是将客户端接收信息功能独立为一个线程来完成,

  • Java多线程之线程通信生产者消费者模式及等待唤醒机制代码详解

    前言 前面的例子都是多个线程在做相同的操作,比如4个线程都对共享数据做tickets–操作.大多情况下,程序中需要不同的线程做不同的事,比如一个线程对共享变量做tickets++操作,另一个线程对共享变量做tickets–操作,这就是大名鼎鼎的生产者和消费者模式. 正文 一,生产者-消费者模式也是多线程 生产者和消费者模式也是多线程的范例.所以其编程需要遵循多线程的规矩. 首先,既然是多线程,就必然要使用同步.上回说到,synchronized关键字在修饰函数的时候,使用的是"this"

  • java多线程编程学习(线程间通信)

    一.概要 线程是操作系统中独立的个体,但这些个体如果不经过特殊的处理就不能成为一个整体,线程间的通信就是成为整体的必用方案之一.可以说,使线程进行通信后,系统之间的交互性会更强大,在大大提高cpu利用率的同时还会使程序员对各线程任务在处理过程中进行有效的把控和监督. 二.等待/通知机制 1."wait/notify"机制:等待/通知机制,wait使线程暂停运行,而notify 使暂停的线程继续运行.用一个厨师和服务员的交互来说明: (1) 服务员取到菜的时间取决于厨师,所以服务员就有&

  • Java多线程中线程间的通信实例详解

    Java多线程中线程间的通信 一.使用while方式来实现线程之间的通信 package com.ietree.multithread.sync; import java.util.ArrayList; import java.util.List; public class MyList { private volatile static List list = new ArrayList(); public void add() { list.add("apple"); } publ

随机推荐