线程间的通信-生产消费者模式

JAVA线程间通信问题

有很多现实问题要求不仅要同步的访问同一共享资源,而且线程间还彼此牵制,通过相互通信来向前推进。那么,多个线程之间是如何进行通信的呢?



解决思路
在现实应用中,很多时候都需要让多个线程按照一定的次序来访问共享资源,例如,经典的生产者和消费者问题。这类问题描述了这样一种情况,假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中的产品取走消费。如果仓库中没有产品,则生产者可以将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止。如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止。显然,这是一个同步问题,生产者和消费者共享同一资源,并且,生产者和消费者之间彼此依赖,互为条件向前推进。但是,该如何编写程序来解决这个问题呢?

传统的思路是利用循环检测的方式来实现,这种方式通过重复检查某一个特定条件是否成立来决定线程的推进顺序。比如,一旦生产者生产结束,它就继续利用循环检测来判断仓库中的产品是否被消费者消费,而消费者也是在消费结束后就会立即使用循环检测的方式来判断仓库中是否又放进产品。显然,这些操作是很耗费CPU资源的,不值得提倡。那么有没有更好的方法来解决这类问题呢?

首先,当线程在继续执行前需要等待一个条件方可继续执行时,仅有 synchronized 关键字是不够的。因为虽然synchronized关键字可以阻止并发更新同一个共享资源,实现了同步,但是它不能用来实现线程间的消息传递,也就是所谓的通信。而在处理此类问题的时候又必须遵循一种原则,即:对于生产者,在生产者没有生产之前,要通知消费者等待;在生产者生产之后,马上又通知消费者消费;对于消费者,在消费者消费之后,要通知生产者已经消费结束,需要继续生产新的产品以供消费。

其实,Java提供了3个非常重要的方法来巧妙地解决线程间的通信问题。这3个方法分别是:wait()、notify()和 notifyAll()。它们都是Object类的最终方法,因此每一个类都默认拥有它们。
虽然所有的类都默认拥有这3个方法,但是只有在synchronized关键字作用的范围内,并且是同一个同步问题中搭配使用这3个方法时才有实际的意义。

这些方法在Object类中声明的语法格式如下所示:

Java代码 复制代码  收藏代码
  1. final void wait() throws InterruptedException      
  2.      
  3. final void notify()      
  4.      
  5. final void notifyAll()    
final void wait() throws InterruptedException   
  
final void notify()   
  
final void notifyAll()  


其中,调用wait()方法可以使调用该方法的线程释放共享资源的锁,然后从运行态退出,进入等待队列,直到被再次唤醒。而调用notify()方法可以唤醒等待队列中第一个等待同一共享资源的线程,并使该线程退出等待队列,进入可运行态。调用notifyAll()方法可以使所有正在等待队列中等待同一共享资源的线程从等待状态退出,进入可运行状态,此时,优先级最高的那个线程最先执行。显然,利用这些方法就不必再循环检测共享资源的状态,而是在需要的时候直接唤醒等待队列中的线程就可以了。这样不但节省了宝贵的CPU资源,也提高了程序的效率。

由于wait()方法在声明的时候被声明为抛出InterruptedException异常,因此,在调用wait()方法时,需要将它放入 try…catch代码块中。此外,使用该方法时还需要把它放到一个同步代码段中,否则会出现如下异常:

Java代码 复制代码  收藏代码
  1. Java代码    
  2. "java.lang.IllegalMonitorStateException: current thread not owner"    
  3.    
Java代码 
"java.lang.IllegalMonitorStateException: current thread not owner" 
 


这些方法是不是就可以实现线程间的通信了呢?下面将通过多线程同步的模型: 生产者和消费者问题来说明怎样通过程序解决多线程间的通信问题。
具体步骤
下面这个程序演示了多个线程之间进行通信的具体实现过程。程序中用到了4个类,其中ShareData类用来定义共享数据和同步方法。在同步方法中调用了wait()方法和notify()方法,并通过一个信号量来实现线程间的消息传递。

Java代码 复制代码  收藏代码
  1. Java代码    
  2. // 例4.6.1  CommunicationDemo.java 描述:生产者和消费者之间的消息传递过程      
  3.      
  4. class ShareData      
  5.      
  6. {      
  7.     private char c;      
  8.     private boolean isProduced = false// 信号量      
  9.     public synchronized void putShareChar(char c) // 同步方法putShareChar()      
  10.     {      
  11.         if (isProduced) // 如果产品还未消费,则生产者等待      
  12.         {      
  13.             try     
  14.             {      
  15.                 wait(); // 生产者等待      
  16.             } catch (InterruptedException e) {      
  17.                 e.printStackTrace();      
  18.             }      
  19.         }      
  20.         this.c = c;      
  21.         isProduced = true// 标记已经生产      
  22.         notify(); // 通知消费者已经生产,可以消费      
  23.     }      
  24.      
  25.     public synchronized char getShareChar() // 同步方法getShareChar()      
  26.     {      
  27.         if (!isProduced) // 如果产品还未生产,则消费者等待      
  28.         {      
  29.             try     
  30.             {      
  31.                 wait(); // 消费者等待      
  32.             } catch (InterruptedException e) {      
  33.                 e.printStackTrace();      
  34.             }      
  35.         }      
  36.         isProduced = false// 标记已经消费      
  37.         notify(); // 通知需要生产      
  38.         return this.c;      
  39.     }      
  40. }      
  41.      
  42. class Producer extends Thread // 生产者线程      
  43. {      
  44.     private ShareData s;      
  45.          
  46.     Producer(ShareData s)      
  47.     {      
  48.         this.s = s;      
  49.     }      
  50.      
  51.     public void run()      
  52.     {      
  53.         for (char ch = 'A'; ch <= 'D'; ch++)      
  54.         {      
  55.             try     
  56.             {      
  57.                 Thread.sleep((int) (Math.random() * 3000));      
  58.             } catch (InterruptedException e) {      
  59.                 e.printStackTrace();      
  60.             }      
  61.             s.putShareChar(ch); // 将产品放入仓库      
  62.             System.out.println(ch + " is produced by Producer.");      
  63.         }      
  64.     }      
  65. }      
  66.      
  67. class Consumer extends Thread // 消费者线程      
  68. {      
  69.     private ShareData s;      
  70.      
  71.     Consumer(ShareData s)      
  72.     {      
  73.         this.s = s;      
  74.     }      
  75.      
  76.     public void run()      
  77.     {      
  78.         char ch;      
  79.         do {      
  80.             try     
  81.             {      
  82.                 Thread.sleep((int) (Math.random() * 3000));      
  83.             } catch (InterruptedException e) {      
  84.                 e.printStackTrace();      
  85.             }      
  86.             ch = s.getShareChar(); // 从仓库中取出产品      
  87.             System.out.println(ch + " is consumed by Consumer. ");      
  88.         } while (ch != 'D');      
  89.     }      
  90. }      
  91.      
  92. class CommunicationDemo      
  93. {      
  94.     public static void main(String[] args)      
  95.     {      
  96.         ShareData s = new ShareData();      
  97.         new Consumer(s).start();      
  98.         new Producer(s).start();      
  99.     }      
  100. }    
Java代码 
// 例4.6.1  CommunicationDemo.java 描述:生产者和消费者之间的消息传递过程   
  
class ShareData   
  
{   
    private char c;   
    private boolean isProduced = false; // 信号量   
    public synchronized void putShareChar(char c) // 同步方法putShareChar()   
    {   
        if (isProduced) // 如果产品还未消费,则生产者等待   
        {   
            try  
            {   
                wait(); // 生产者等待   
            } catch (InterruptedException e) {   
                e.printStackTrace();   
            }   
        }   
        this.c = c;   
        isProduced = true; // 标记已经生产   
        notify(); // 通知消费者已经生产,可以消费   
    }   
  
    public synchronized char getShareChar() // 同步方法getShareChar()   
    {   
        if (!isProduced) // 如果产品还未生产,则消费者等待   
        {   
            try  
            {   
                wait(); // 消费者等待   
            } catch (InterruptedException e) {   
                e.printStackTrace();   
            }   
        }   
        isProduced = false; // 标记已经消费   
        notify(); // 通知需要生产   
        return this.c;   
    }   
}   
  
class Producer extends Thread // 生产者线程   
{   
    private ShareData s;   
      
    Producer(ShareData s)   
    {   
        this.s = s;   
    }   
  
    public void run()   
    {   
        for (char ch = 'A'; ch <= 'D'; ch++)   
        {   
            try  
            {   
                Thread.sleep((int) (Math.random() * 3000));   
            } catch (InterruptedException e) {   
                e.printStackTrace();   
            }   
            s.putShareChar(ch); // 将产品放入仓库   
            System.out.println(ch + " is produced by Producer.");   
        }   
    }   
}   
  
class Consumer extends Thread // 消费者线程   
{   
    private ShareData s;   
  
    Consumer(ShareData s)   
    {   
        this.s = s;   
    }   
  
    public void run()   
    {   
        char ch;   
        do {   
            try  
            {   
                Thread.sleep((int) (Math.random() * 3000));   
            } catch (InterruptedException e) {   
                e.printStackTrace();   
            }   
            ch = s.getShareChar(); // 从仓库中取出产品   
            System.out.println(ch + " is consumed by Consumer. ");   
        } while (ch != 'D');   
    }   
}   
  
class CommunicationDemo   
{   
    public static void main(String[] args)   
    {   
        ShareData s = new ShareData();   
        new Consumer(s).start();   
        new Producer(s).start();   
    }   
}  




上面的程序演示了生产者生产出A、B、C、D四个字符,消费者消费这四个字符的全过程

通过程序的运行结果可以看到,尽管在主方法中先启动了Consumer线程,但是,由于仓库中没有产品,因此,Consumer线程就会调用 wait()方法进入等待队列进行等待,直到Producer线程将产品生产出来并放进仓库,然后使用notify()方法将其唤醒。
由于在两个线程中都指定了一定的休眠时间,因此也可能出现这样的情况:生产者将产品生产出来放入仓库,并通知等待队列中的Consumer线程,然而,由于休眠时间过长,Consumer线程还没有打算消费产品,此时,Producer线程欲生产下一个产品,结果由于仓库中的产品没有被消费掉,故 Producer线程执行wait()方法进入等待队列等待,直到Consumer线程将仓库中的产品消费掉以后通过notify()方法去唤醒等待队列中的Producer线程为止。可见,两个线程之间除了必须保持同步之外,还要通过相互通信才能继续向前推进。
前面这个程序中,生产者一次只能生产一个产品,而消费者也只能一次消费一个产品。那么现实中也有这样的情况,生产者可以一次生产多个产品,只要仓库容量够大,就可以一直生产。而消费者也可以一次消费多个产品,直到仓库中没有产品为止。
但是,无论是生产产品到仓库,还是从仓库中消费,每一次都只能允许一个操作。显然,这也是个同步问题,只不过在这个问题中共享资源是一个资源池,可以存放多个资源。下面就以栈结构为例给出如何在这个问题中解决线程通信的程序代码。

Java代码 复制代码  收藏代码
  1. // 例4.6.2  CommunicationDemo2.java      
  2.      
  3. class SyncStack // 同步堆栈类,可以一次放入多个数据      
  4. {      
  5.     private int index = 0// 堆栈指针初始值为0      
  6.     private char[] buffer = new char[5]; // 堆栈有5个字符的空间      
  7.     public synchronized void push(char c) // 入栈同步方法      
  8.     {      
  9.         if (index == buffer.length) // 堆栈已满,不能入栈      
  10.         {      
  11.             try     
  12.             {      
  13.                 this.wait(); // 等待出栈线程将数据出栈      
  14.             } catch (InterruptedException e) {      
  15.             }      
  16.         }      
  17.         buffer[index] = c; // 数据入栈      
  18.         index++; // 指针加1,栈内空间减少      
  19.         this.notify(); // 通知其他线程把数据出栈      
  20.     }      
  21.      
  22.     public synchronized char pop() // 出栈同步方法      
  23.     {      
  24.         if (index == 0// 堆栈无数据,不能出栈      
  25.         {      
  26.             try     
  27.             {      
  28.                 this.wait(); // 等待入栈线程把数据入栈      
  29.             } catch (InterruptedException e) {      
  30.             }      
  31.         }      
  32.         this.notify(); // 通知其他线程入栈      
  33.         index--; // 指针向下移动      
  34.         return buffer[index]; // 数据出栈      
  35.     }      
  36. }      
  37.      
  38. class Producer implements Runnable // 生产者类      
  39. {      
  40.     SyncStack s; // 生产者类生成的字母都保存到同步堆栈中      
  41.     public Producer(SyncStack s)      
  42.     {      
  43.         this.s = s;      
  44.     }      
  45.      
  46.     public void run()      
  47.     {      
  48.         char ch;      
  49.         for (int i = 0; i < 5; i++)      
  50.         {      
  51.             try     
  52.             {      
  53.                 Thread.sleep((int) (Math.random() * 1000));      
  54.             } catch (InterruptedException e) {      
  55.             }      
  56.             ch = (char) (Math.random() * 26 + 'A'); // 随机产生5个字符      
  57.             s.push(ch); // 把字符入栈      
  58.             System.out.println("Push " + ch + " in Stack"); // 打印字符入栈      
  59.         }      
  60.     }      
  61. }      
  62.      
  63. class Consumer implements Runnable // 消费者类      
  64. {      
  65.     SyncStack s; // 消费者类获得的字符都来自同步堆栈      
  66.     public Consumer(SyncStack s)      
  67.     {      
  68.         this.s = s;      
  69.     }      
  70.      
  71.     public void run()      
  72.     {      
  73.         char ch;      
  74.         for (int i = 0; i < 5; i++)      
  75.         {      
  76.             try     
  77.             {      
  78.                 Thread.sleep((int) (Math.random() * 3000));      
  79.             } catch (InterruptedException e) {      
  80.             }      
  81.             ch = s.pop(); // 从堆栈中读取字符      
  82.             System.out.println("Pop  " + ch + " from Stack"); // 打印字符出栈      
  83.         }      
  84.     }      
  85. }      
  86.      
  87. public class CommunicationDemo2      
  88. {      
  89.     public static void main(String[] args)      
  90.     {      
  91.         SyncStack stack = new SyncStack();      
  92.         // 下面的消费者类对象和生产者类对象所操作的是同一个同步堆栈对象      
  93.         Thread t1 = new Thread(new Producer(stack)); // 线程实例化      
  94.         Thread t2 = new Thread(new Consumer(stack)); // 线程实例化      
  95.         t2.start(); // 线程启动      
  96.         t1.start(); // 线程启动      
  97.     }      
  98. }    
// 例4.6.2  CommunicationDemo2.java   
  
class SyncStack // 同步堆栈类,可以一次放入多个数据   
{   
    private int index = 0; // 堆栈指针初始值为0   
    private char[] buffer = new char[5]; // 堆栈有5个字符的空间   
    public synchronized void push(char c) // 入栈同步方法   
    {   
        if (index == buffer.length) // 堆栈已满,不能入栈   
        {   
            try  
            {   
                this.wait(); // 等待出栈线程将数据出栈   
            } catch (InterruptedException e) {   
            }   
        }   
        buffer[index] = c; // 数据入栈   
        index++; // 指针加1,栈内空间减少   
        this.notify(); // 通知其他线程把数据出栈   
    }   
  
    public synchronized char pop() // 出栈同步方法   
    {   
        if (index == 0) // 堆栈无数据,不能出栈   
        {   
            try  
            {   
                this.wait(); // 等待入栈线程把数据入栈   
            } catch (InterruptedException e) {   
            }   
        }   
        this.notify(); // 通知其他线程入栈   
        index--; // 指针向下移动   
        return buffer[index]; // 数据出栈   
    }   
}   
  
class Producer implements Runnable // 生产者类   
{   
    SyncStack s; // 生产者类生成的字母都保存到同步堆栈中   
    public Producer(SyncStack s)   
    {   
        this.s = s;   
    }   
  
    public void run()   
    {   
        char ch;   
        for (int i = 0; i < 5; i++)   
        {   
            try  
            {   
                Thread.sleep((int) (Math.random() * 1000));   
            } catch (InterruptedException e) {   
            }   
            ch = (char) (Math.random() * 26 + 'A'); // 随机产生5个字符   
            s.push(ch); // 把字符入栈   
            System.out.println("Push " + ch + " in Stack"); // 打印字符入栈   
        }   
    }   
}   
  
class Consumer implements Runnable // 消费者类   
{   
    SyncStack s; // 消费者类获得的字符都来自同步堆栈   
    public Consumer(SyncStack s)   
    {   
        this.s = s;   
    }   
  
    public void run()   
    {   
        char ch;   
        for (int i = 0; i < 5; i++)   
        {   
            try  
            {   
                Thread.sleep((int) (Math.random() * 3000));   
            } catch (InterruptedException e) {   
            }   
            ch = s.pop(); // 从堆栈中读取字符   
            System.out.println("Pop  " + ch + " from Stack"); // 打印字符出栈   
        }   
    }   
}   
  
public class CommunicationDemo2   
{   
    public static void main(String[] args)   
    {   
        SyncStack stack = new SyncStack();   
        // 下面的消费者类对象和生产者类对象所操作的是同一个同步堆栈对象   
        Thread t1 = new Thread(new Producer(stack)); // 线程实例化   
        Thread t2 = new Thread(new Consumer(stack)); // 线程实例化   
        t2.start(); // 线程启动   
        t1.start(); // 线程启动   
    }   
}  


程序中引入了一个堆栈数组buffer[]来模拟资源池,并使生产者类和消费者类都实现了Runnable接口,然后在主程序中通过前面介绍的方法创建两个共享同一堆栈资源的线程,并且有意先启动消费者线程,后启动生产者线程。请在阅读程序的时候仔细观察例4.6.1和本例的相似点以及区别之处,体会作者的用心。

由于是栈结构,所以符合后进先出原则。有兴趣的读者还可以用符合先进先出原则的队列结构来模拟线程间通信的过程,相信可以通过查阅相关的资料来解决这个问题,在这里就不再给出程序代码了,作为一个思考题供读者练习。

专家说明
本小节介绍了三个重要的方法:wait()、notify()和notifyAll()。使用它们可以高效率地完成多个线程间的通信问题,这样在通信问题上就不必再使用循环检测的方法来等待某个条件的发生,因为这种方法是极为浪费CPU资源的,当然这种情况也不是所期望的。在例4.6.1中,为了更好地通信,引入了一个专门用来传递信息的信号量。利用信号量来决定线程是否等待无疑是一种非常安全的操作,值得提倡。此外,在例4.6.2中引入了资源池作为共享资源,并解决了在这种情况下如何实现多线程之间的通信问题。希望读者能够举一反三,编写出解决更加复杂问题的程序。

专家指点
可以肯定的是,合理地使用wait()、notify()和notifyAll()方法确实能够很好地解决线程间通信的问题。但是,也应该了解到这些方法是更复杂的锁定、排队和并发性代码的构件。尤其是使用 notify()来代替notifyAll()时是有风险的。除非确实知道每一个线程正在做什么,否则最好使用notifyAll()。其实,在 JDK1.5中已经引入了一个新的包:java.util.concurrent 包,该包是一个被广泛使用的开放源码工具箱,里面都是有用的并发性实用程序。完全可以代替wait()和notify()方法用来编写自己的调度程序和锁。有关信息可以查阅相关资料,本书中不再赘述。

相关问题
Java提供了各种各样的输入输出流(stream),使程序员能够很方便地对数据进行操作。其中,管道(pipe)流是一种特殊的流,用于在不同线程间直接传送数据。一个线程发送数据到输出管道,另一个线程从输入管道中读出数据。通过使用管道,达到实现多个线程间通信的目的。那么,如何创建和使用管道呢?

Java提供了两个特殊的专门用来处理管道的类,它们就是PipedInputStream类和PipedOutputStream类。

其中,PipedInputStream代表了数据在管道中的输出端,也就是线程从管道读出数据的一端;PipedOutputStream代表了数据在管道中的输入端,也就是线程向管道写入数据的一端,这两个类一起使用就可以创建出数据输入输出的管道流对象。
一旦创建了管道之后,就可以利用多线程的通信机制对磁盘中的文件通过管道进行数据的读写,从而使多线程的程序设计在实际应用中发挥更

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
一、 课程设计目的 在多道程序环境下,进程同步问题十分重要,通过解决“生产者-消费者”问题,可以帮助我们更好的理解进程同步的概念及实现方法。掌握线程创建和终止的方法,加深对线程和进程概念的理解,会用同步与互斥方法实现线程的进行操作。 在学习操作系统课程的基础上,通过实践加深对进程同步的认识,同时,可以提高运用操作系统知识解决实际问题的能力;锻炼实际的编程能力、创新能力及团队组织、协作开发软件的能力;还能提高调查研究、查阅技术文献、资料以及编写软件设计文档的能力。 二、 课程设计内容 模拟仿真“生产者-消费者”问题的解决过程及方法。 三、 系统分析与设计 1、 系统分析 在OS中引入进程后,虽然提高了资源的利用率和系统的吞吐量,但由于进程的异步性,也会给系统造成混乱,尤其是在他们争用临界资源时。为了对多个相关进程在执行次序上进行协调,以使并发执行的诸程序之能有效地共享资源和相互合作,使程序的执行具有可再现性,所以引入了进程同步的概念。信号量机制是一种卓有成效的进程同步工具。 在生产者---消费者问题中应注意(信号量名称以多个生产者和多个消费者中的为例):首先,在每个程序中用于互斥的wait(mutex)和signal(mutex)必须成对出现;其次,对资源信号量empty和full的wait和signal操作,同样需要成对地出现,但它们分别处于不同的程序中。生产者与消费者进程共享一个大小固定的缓冲区。其中,一个或多个生产生产数据,并将生产的数据存入缓冲区,并有一个或多个消费者从缓冲区中取数据。 2、 系统设计: 系统的设计必须要体现进程之的同步关系,所以本系统采用2个生产者、2个消费者 和20个缓冲区的框架体系设计。为了更能体现该系统进程之的同步关系,系统的生产者、 消费者的速度应该可控,以更好更明显的表现出结果。 为了使本系统以更加简单、直观的形式把“消费者-生产者”问题表现出来,我选择了使 用可视化界面编程。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值