问题
线程之间的关系是平等的,彼此之间并不存在任何依赖,它们各自竞争CPU资源,互不相让,并且还无条件地阻止其他线程对共享资源的异步访问。然而,也有很多现实问题要求不仅要同步的访问同一共享资源,而且线程间还彼此牵制,通过相互通信来向前推进。那么,多个线程之间是如何进行通信的呢?
解决思路
在现实应用中,很多时候都需要让多个线程按照一定的次序来访问共享资源,例如,经典的生产者和消费者问题。这类问题描述了这样一种情况,假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中的产品取走消费。如果仓库中没有产品,则生产者可以将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止。如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止。显然,这是一个同步问题,生产者和消费者共享同一资源,并且,生产者和消费者之间彼此依赖,互为条件向前推进。但是,该如何编写程序来解决这个问题呢?
传统的思路是利用循环检测的方式来实现,这种方式通过重复检查某一个特定条件是否成立来决定线程的推进顺序。比如,一旦生产者生产结束,它就继续利用循环检测来判断仓库中的产品是否被消费者消费,而消费者也是在消费结束后就会立即使用循环检测的方式来判断仓库中是否又放进产品。显然,这些操作是很耗费CPU资源的,不值得提倡。那么有没有更好的方法来解决这类问题呢?
首先,当线程在继续执行前需要等待一个条件方可继续执行时,仅有 synchronized 关键字是不够的。因为虽然synchronized关键字可以阻止并发更新同一个共享资源,实现了同步,但是它不能用来实现线程间的消息传递,也就是所谓的通信。而在处理此类问题的时候又必须遵循一种原则,即:对于生产者,在生产者没有生产之前,要通知消费者等待;在生产者生产之后,马上又通知消费者消费;对于消费者,在消费者消费之后,要通知生产者已经消费结束,需要继续生产新的产品以供消费。
其实,Java提供了3个非常重要的方法来巧妙地解决线程间的通信问题。这3个方法分别是:wait()、notify()和notifyAll()。它们都是Object类的最终方法,因此每一个类都默认拥有它们。
虽然所有的类都默认拥有这3个方法,但是只有在synchronized关键字作用的范围内,并且是同一个同步问题中搭配使用这3个方法时才有实际的意义。
这些方法在Object类中声明的语法格式如下所示:
final void wait() throws InterruptedException
final void notify()
final void notifyAll()
其中,调用wait()方法可以使调用该方法的线程释放共享资源的锁,然后从运行态退出,进入等待队列,直到被再次唤醒。而调用notify()方法可以唤醒等待队列中第一个等待同一共享资源的线程,并使该线程退出等待队列,进入可运行态。调用notifyAll()方法可以使所有正在等待队列中等待同一共享资源的线程从等待状态退出,进入可运行状态,此时,优先级最高的那个线程最先执行。显然,利用这些方法就不必再循环检测共享资源的状态,而是在需要的时候直接唤醒等待队列中的线程就可以了。这样不但节省了宝贵的CPU资源,也提高了程序的效率。
由于wait()方法在声明的时候被声明为抛出InterruptedException异常,因此,在调用wait()方法时,需要将它放入try…catch代码块中。此外,使用该方法时还需要把它放到一个同步代码段中,否则会出现如下异常:
"java.lang.IllegalMonitorStateException: current thread not owner"
这些方法是不是就可以实现线程间的通信了呢?下面将通过多线程同步的模型: 生产者和消费者问题来说明怎样通过程序解决多线程间的通信问题。
具体步骤
下面这个程序演示了多个线程之间进行通信的具体实现过程。程序中用到了4个类,其中ShareData类用来定义共享数据和同步方法。在同步方法中调用了wait()方法和notify()方法,并通过一个信号量来实现线程间的消息传递。
// 例4.6.1 CommunicationDemo.java 描述:生产者和消费者之间的消息传递过程
- package chenwenbiao.test;
- 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(); // 通知消费者已经生产,可以消费。调用它的隐藏方法notify
- }
- public synchronized char getShareChar() // 同步方法getShareChar()
- {
- if (!isProduced) // 如果产品还未生产,则消费者等待
- {
- try
- {
- wait(); // 消费者等待。调用它的隐藏方法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 <= 'Z'; 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 != 'Z');
- }
- }
- public class CommunicationDemo
- {
- public static void main(String[] args)
- {
- ShareData s = new ShareData();
- new Consumer(s).start();
- new Producer(s).start();
- }
- }
上面的例子是当还没有生产出产品时,消费者进入等待状态,而当产品生产出来后,就唤醒消费者,而当消费者还没有消费时,生产品就等待消费者消费后,再搞生产。
生产者和消费者线程在条件不满足的条件下交出CPU使用权,由操作系统将它们挂在等待队列,那它们在条件没够的情况下就不会有执行的机会,而不像用while来不停的判断条件是否满足,这样做,那么这两个线程在还是会占用CPU,就执行空操作,就是占着坑,不...的一样,浪费CPU宝贵资源,所以使用wait和notify会提高多线程环境下的CPU利用率。