多线程--线程间通信(二)

来源:互联网 发布:内部类java 编辑:程序博客网 时间:2024/05/18 00:43

上篇说到了线程间的同步互斥和比较经典的消费者生产者问题,其中涉及到了线程间通信和线程锁这两个概念,下面就来谈谈这两个概念和具体实现方法。

一、线程状态

正式开始之前,先来普及线程的几种状态:

    1. 新建状态(New):新创建了一个线程对象。
    2. 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
    3. 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
    4. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
    (1)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。
    (2)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
    (3)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或        者I/O处理完毕时,线程重新转入就绪状态。
    5. 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

附上状态图:

二、线程间通信方法

当线程在继续执行前需要等待一个条件方可继续执行时,仅有 synchronized 关键字是不够的。因为虽然synchronized关键字可以阻止并发更新同一个共享资源,实现了同步,但是它不能用来实现线程间的消息传递,也就是所谓的通信。而在处理此类问题的时候又必须遵循一种原则,即:对于生产者,在生产者没有生产之前,要通知消费者等待;在生产者生产之后,马上又通知消费者消费;对于消费者,在消费者消费之后,要通知生产者已经消费结束,需要继续生产新的产品以供消费。JAVA提供了三个方法来解决此类问题:wait()、notify()和notifyAll()。它们都是Object类的最终方法,因此每一个类都默认拥有它们。注意:虽然所有的类都默认拥有这3个方法,但是只有在synchronized关键字作用的范围内,并且是同一个同步问题中搭配使用这3个方法时才有实际的意义。这些方法在Object类中声明的语法格式如下所示:
<span style="white-space:pre"></span>final void wait() throws InterruptedException<span style="white-space:pre"></span>final void notify()<span style="white-space:pre"></span>final void notifyAll()

1、wait():调用wait()方法可以使调用该方法的线程释放共享资源的锁,然后从运行态退出,进入等待队列,直到被再次唤醒。

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

"java.lang.IllegalMonitorStateException: current thread not owner" 
2、notify():而调用notify()方法可以唤醒等待队列中第一个等待同一共享资源的线程,并使该线程退出等待队列,进入可运行态。
3、notifyAll():调用notifyAll()方法可以使所有正在等待队列中等待同一共享资源的线程从等待状态退出,进入可运行状态,此时,优先级最高的那个线程最先 执行。

利用这些方法就不必再循环检测共享资源的状态,而是在需要的时候直接唤醒等待队列中的线程就可以了。这样不但节省了宝贵的CPU资源,也提高了程序的效率。

三、实例说明

下面再次用一个消费者-生产者的问题来说明线程间的通信。程序中用到了4个类,其中ShareData类用来定义共享数据和同步方法。在同步方法中调用了wait()方法和notify()方法,并通过一个信号量来实现线程间的消息传递。

// 例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();}}

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

下面就以栈结构为例给出如何在这个问题中解决线程通信的程序代码。

// 例4.6.2  CommunicationDemo2.javaclass SyncStack   // 同步堆栈类,可以一次放入多个数据{ private int index = 0; // 堆栈指针初始值为0private 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接口,然后在主程序中通过前面介绍的方法创建两个共享同一堆栈资源的线程,并且有意先启动消费者线程,后启动生产者线程。


总结:

1、上面介绍了三个重要的方法:wait()、notify()和notifyAll()。使用它们可以高效率地完成多个线程间的通信问题,这样在通信问题上就不必再使用循环检测的方法来等待某个条件的发生,因为这种方法是极为浪费CPU资源的,当然这种情况也不是所期望的。

2、合理地使用wait()、notify()和notifyAll()方法确实能够很好地解决线程间通信的问题。但是,也应该了解到这些方法是更复杂的锁定、排队和并发性代码的构件。尤其是使用 notify()来代替notifyAll()时是有风险的。除非确实知道每一个线程正在做什么,否则最好使用notifyAll()。

3、线程间通信的另一种方式--pipe

Java提供了各种各样的输入输出流(stream),使程序员能够很方便地对数据进行操作。其中,管道(pipe)流是一种特殊的流,用于在不同线程间直接传送数据。一个线程发送数据到输出管道,另一个线程从输入管道中读出数据。通过使用管道,达到实现多个线程间通信的目的。那么,如何创建和使用管道呢?
Java提供了两个特殊的专门用来处理管道的类,它们就是PipedInputStream类和PipedOutputStream类。
其中,PipedInputStream代表了数据在管道中的输出端,也就是线程从管道读出数据的一端;PipedOutputStream代表了数据在管道中的输入端,也就是线程向管道写入数据的一端,这两个类一起使用就可以创建出数据输入输出的管道流对象。
一旦创建了管道之后,就可以利用多线程的通信机制对磁盘中的文件通过管道进行数据的读写,从而使多线程的程序设计在实际应用中发挥更大的作用。 


最后建议参考下这篇文章:http://www.cnblogs.com/mengdd/archive/2013/02/20/2917956.html,对wait()和notify()有比较深的认识。

0 0
原创粉丝点击