Java知多少(63)线程间通信

上述例题无条件的阻塞了其他线程异步访问某个方法。Java对象中隐式管程的应用是很强大的,但是你可以通过进程间通信达到更微妙的境界。这在Java中是尤为简单的。

像前面所讨论过的,多线程通过把任务分成离散的和合乎逻辑的单元代替了事件循环程序。线程还有第二优点:它远离了轮询。轮询通常由重复监测条件的循环实现。一旦条件成立,就要采取适当的行动。这浪费了CPU时间。举例来说,考虑经典的序列问题,当一个线程正在产生数据而另一个程序正在消费它。为使问题变得更有趣,假设数据产生器必须等待消费者完成工作才能产生新的数据。在轮询系统,消费者在等待生产者产生数据时会浪费很多CPU周期。一旦生产者完成工作,它将启动轮询,浪费更多的CPU时间等待消费者的工作结束,如此下去。很明显,这种情形不受欢迎。

为避免轮询,Java包含了通过wait( ),notify( )和notifyAll( )方法实现的一个进程间通信机制。这些方法在对象中是用final方法实现的,所以所有的类都含有它们。这三个方法仅在synchronized方法中才能被调用。尽管这些方法从计算机科学远景方向上来说具有概念的高度先进性,实际中用起来是很简单的:
  • wait( ) 告知被调用的线程放弃管程进入睡眠直到其他线程进入相同管程并且调用notify( )。
  • notify( ) 恢复相同对象中第一个调用 wait( ) 的线程。
  • notifyAll( ) 恢复相同对象中所有调用 wait( ) 的线程。具有最高优先级的线程最先运行。

这些方法在Object中被声明,如下所示:
    final void wait( ) throws InterruptedException
    final void notify( )
    final void notifyAll( )
wait( )存在的另外的形式允许你定义等待时间。

下面的例子程序错误的实行了一个简单生产者/消费者的问题。它由四个类组成:Q,设法获得同步的序列;Producer,产生排队的线程对象;Consumer,消费序列的线程对象;以及PC,创建单个Q,Producer,和Consumer的小类。
 1 // An incorrect implementation of a producer and consumer.
 2 class Q {
 3     int n;
 4     synchronized int get() {
 5         System.out.println("Got: " + n);
 6         return n;
 7     }
 8     synchronized void put(int n) {
 9         this.n = n;
10         System.out.println("Put: " + n);
11     }
12 }
13 class Producer implements Runnable {
14     Q q;
15     Producer(Q q) {
16         this.q = q;
17         new Thread(this, "Producer").start();
18     }
19     public void run() {
20         int i = 0;
21         while(true) {
22             q.put(i++);
23         }
24     }
25 }
26 class Consumer implements Runnable {
27     Q q;
28     Consumer(Q q) {
29         this.q = q;
30         new Thread(this, "Consumer").start();
31     }
32     public void run() {
33         while(true) {
34            q.get();
35         }
36     }
37 }
38 class PC {
39     public static void main(String args[]) {
40         Q q = new Q();
41         new Producer(q);
42         new Consumer(q);
43         System.out.println("Press Control-C to stop.");
44     }
45 }

尽管Q类中的put( )和get( )方法是同步的,没有东西阻止生产者超越消费者,也没有东西阻止消费者消费同样的序列两次。这样,你就得到下面的错误输出(输出将随处理器速度和装载的任务而改变):

Put: 1
Got: 1
Got: 1
Got: 1
Got: 1
Got: 1
Put: 2
Put: 3
Put: 4
Put: 5
Put: 6
Put: 7
Got: 7
生产者生成1后,消费者依次获得同样的1五次。生产者在继续生成2到7,消费者没有机会获得它们。

用Java正确的编写该程序是用wait( )和notify( )来对两个方向进行标志,如下所示:
 1 // A correct implementation of a producer and consumer.
 2 class Q {
 3     int n;
 4     boolean valueSet = false;
 5     synchronized int get() {
 6         if(!valueSet)
 7             try {
 8                 wait();
 9             } catch(InterruptedException e) {
10                 System.out.println("InterruptedException caught");
11             }
12             System.out.println("Got: " + n);
13             valueSet = false;
14             notify();
15             return n;
16         }
17         synchronized void put(int n) {
18             if(valueSet)
19             try {
20                 wait();
21             } catch(InterruptedException e) {
22                 System.out.println("InterruptedException caught");
23             }
24             this.n = n;
25             valueSet = true;
26             System.out.println("Put: " + n);
27             notify();
28         }
29     }
30     class Producer implements Runnable {
31         Q q;
32         Producer(Q q) {
33         this.q = q;
34         new Thread(this, "Producer").start();
35     }
36     public void run() {
37         int i = 0;
38         while(true) {
39             q.put(i++);
40         }
41     }
42 }
43 class Consumer implements Runnable {
44     Q q;
45     Consumer(Q q) {
46         this.q = q;
47         new Thread(this, "Consumer").start();
48     }
49     public void run() {
50         while(true) {
51             q.get();
52         }
53     }
54 }
55 class PCFixed {
56     public static void main(String args[]) {
57         Q q = new Q();
58         new Producer(q);
59         new Consumer(q);
60         System.out.println("Press Control-C to stop.");
61     }
62 }

内部get( ), wait( )被调用。这使执行挂起直到Producer 告知数据已经预备好。这时,内部get( ) 被恢复执行。获取数据后,get( )调用notify( )。这告诉Producer可以向序列中输入更多数据。在put( )内,wait( )挂起执行直到Consumer取走了序列中的项目。当执行再继续,下一个数据项目被放入序列,notify( )被调用,这通知Consumer它应该移走该数据。


下面是该程序的输出,它清楚的显示了同步行为:
Put: 1
Got: 1
Put: 2
Got: 2
Put: 3
Got: 3
Put: 4
Got: 4
Put: 5
Got: 5
posted @ 2015-05-15 08:10  Coda  阅读(1156)  评论(1编辑  收藏  举报