`
hhhhh-kk#qq.com
  • 浏览: 58344 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

JAVA线程间通信问题

阅读更多

问题

在前一小节,介绍了在多线程编程中使用同步机制的重要性,并学会了如何实现同步的方法来正确地访问共享资源。这些线程之间的关系是平等的,彼此之间并不存在任何依赖,它们各自竞争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 描述:生产者和消费者之间的消息传递过程 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四个字符,消费者消费这四个字符的全过程,程序结果如图4.6.1所示:

图4.6.1  生产者和消费者举例

通过程序的运行结果可以看到,尽管在主方法中先启动了Consumer线程,但是,由于仓库中没有产品,因此,Consumer线程就会调用wait()方法进入等待队列进行等待,直到Producer线程将产品生产出来并放进仓库,然后使用notify()方法将其唤醒。

由于在两个线程中都指定了一定的休眠时间,因此也可能出现这样的情况:生产者将产品生产出来放入仓库,并通知等待队列中的Consumer线程,然而,由于休眠时间过长,Consumer线程还没有打算消费产品,此时,Producer线程欲生产下一个产品,结果由于仓库中的产品没有被消费掉,故Producer线程执行wait()方法进入等待队列等待,直到Consumer线程将仓库中的产品消费掉以后通过notify()方法去唤醒等待队列中的Producer线程为止。可见,两个线程之间除了必须保持同步之外,还要通过相互通信才能继续向前推进。

前面这个程序中,生产者一次只能生产一个产品,而消费者也只能一次消费一个产品。那么现实中也有这样的情况,生产者可以一次生产多个产品,只要仓库容量够大,就可以一直生产。而消费者也可以一次消费多个产品,直到仓库中没有产品为止。

但是,无论是生产产品到仓库,还是从仓库中消费,每一次都只能允许一个操作。显然,这也是个同步问题,只不过在这个问题中共享资源是一个资源池,可以存放多个资源。下面就以栈结构为例给出如何在这个问题中解决线程通信的程序代码。

// 例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和本例的相似点以及区别之处,体会作者的用心。程序结果输出如图4.6.2所示:

图4.6.2  共享资源池的生产者和消费者问题

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

专家说明

本小节介绍了三个重要的方法: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代表了数据在管道中的输入端,也就是线程向管道写入数据的一端,这两个类一起使用就可以创建出数据输入输出的管道流对象。

一旦创建了管道之后,就可以利用多线程的通信机制对磁盘中的文件通过管道进行数据的读写,从而使多线程的程序设计在实际应用中发挥更大的作用。

分享到:
评论
1 楼 robin35java 2011-03-03  
这篇文章好,对于信号量进行了解释,并且扩展了线程的实现思考方式,java.util.concurrent 包

相关推荐

    Java 线程间通信,生产者与消费者模型

    使用wait()和notify()实现的生产者与消费者模型,可以了解如何使用wait()和notify()进行线程间通信。(上一次上传的代码有一个问题没有考虑到,这次修补了——CSDN没法撤销资源,只能再上传了)

    关于Java线程间通信-回调.docx

    总的来说,回调在Java线程间通信中起到桥梁的作用,使得线程能够以非阻塞的方式互相协作,提高了程序的并发性能和响应速度。理解并熟练掌握回调以及其他线程通信机制是Java并发编程的关键,这对于开发高效、稳定的多...

    Java线程间通信的代码示例.zip

    Java线程间通信是多线程编程中的一个重要概念,它涉及到如何在并发执行的线程之间有效地传递信息和协调工作。在Java中,线程间通信主要通过共享内存(如共享变量)和消息传递(如wait(), notify(), notifyAll()等...

    JAVA100例之实例64 JAVA线程间通讯

    在"JAVA100例之实例64 JAVA线程间通讯"这个主题中,我们将深入探讨Java中实现线程间通信的几种主要方法。 1. **共享数据**:最直观的线程间通信方式是通过共享内存空间,即共享变量。只要对共享变量的操作是线程...

    Java的多线程-线程间的通信.doc

    在Java多线程编程中,线程间的通信是非常重要的概念,用于协调多个并发执行的任务。线程的状态转换是理解线程通信的基础,主要包括四个状态:新(New)、可执行(Runnable)、死亡(Dead)和停滞(Blocked)。新状态...

    Java 线程通信示例 源代码

    3. **wait(), notify(), notifyAll() 方法**:这些方法是Object类的成员,用于线程间通信。在线程A调用`wait()`后,它会被放入等待池,释放锁并暂停执行,直到其他线程调用同一对象的`notify()`或`notifyAll()`唤醒...

    Java线程间的通信方式详解

    Java线程间的通信是多线程编程中的重要概念,它涉及到如何协调多个并发执行的线程,确保数据的一致性和正确性。本文将详细介绍两种常见的Java线程通信方式:同步和while轮询。 1. 同步(Synchronized) 同步是Java...

    java线程.pdf

    Java线程间通信主要包括线程间的同步和线程间的协作两部分。常用的通信方法有`wait()`、`notify()`和`notifyAll()`等。 1. **wait()**:使当前线程暂停执行,并释放当前持有的锁。 2. **notify()**:唤醒正在等待该...

    java并发之线程间通信协作.docx

    在Java并发编程中,线程间通信协作是一个关键的概念,特别是在多线程环境中,如生产者-消费者模型。这个模型中,生产者线程负责生产数据并放入队列,而消费者线程则负责取出并消费这些数据。为了保证数据的安全和...

    android 线程间通信

    ### Android线程间通信详解 #### 一、引言 Android应用程序通常运行在单个主线程上,称为“主线程”或“UI线程”。为了提高应用性能和用户体验,开发者经常需要利用多线程技术来执行后台任务,比如下载图片、获取...

    java多线程代码案例(创建线程,主线程,线程优先级,线程组,线程同步,线程间的通信)

    Java提供了多种线程间通信的手段,如`BlockingQueue`、`Future`、`ExecutorService`等。其中,`wait()`, `notify()`, `notifyAll()`是基于对象监视器的通信方式,用于在线程间传递信号。`BlockingQueue`则提供了...

    java线程同步及通信

    2. **线程间通信**: 在多线程环境中,线程之间可能需要交换数据或协调工作。Java提供了一些机制,如`wait()`、`notify()`和`notifyAll()`方法,这些方法存在于`Object`类中,用于线程间的通信。在`Q.java`的`get()...

    深入理解JAVA多线程之线程间的通信方式

    除了上述两种方式,Java还提供了其他线程间通信的方法: 3. Wait/Notify机制: `wait()`, `notify()`, `notifyAll()`是Object类提供的方法,用于线程间通信。在线程A执行完特定操作后,可以调用`notify()`或`...

    Java多线程编程线程间通信详解.docx

    Java多线程编程中的线程间通信是解决并发问题的关键技术之一。在多线程环境中,线程间的协作和同步往往需要依赖于等待与通知机制,以确保在正确的时间执行正确的操作,避免资源的竞争和死锁。等待与通知是Java中实现...

    创建线程及线程间通信

    接下来,我们关注线程间通信。线程通信是线程协作完成任务的关键,它允许线程之间交换数据或同步操作。一种常见的通信机制是“生产者-消费者”模型,其中生产者线程生成数据,而消费者线程消费这些数据。在没有适当...

    线程的几种控制方式以及线程间的几种通信方式

    4. **线程间通信**:线程间通信允许线程之间交换信息,Java提供了多种机制,如`wait()`, `notify()`, `notifyAll()`,Python中则有`Condition`对象。 5. **线程休眠**:Java的`Thread.sleep()`方法可以让线程暂停...

    java实现多线程间的通信

    本文将探讨线程间通信的概念,分析基于Java的多线程程序的关键技术,并指出设计多线程程序时需要充分理解线程同步机制以及操作系统进程之间的关系,以便优化程序性能。 关键词:多线程、同步 在当前大多数操作系统...

    线程间通信求和逆序源代码

    在IT行业中,线程间通信(Inter-Thread Communication, ITC)是多线程编程中的一个关键概念。当一个程序包含多个并发执行的线程时,线程间通信用于协调它们之间的活动,确保数据的一致性和正确性。在这个场景中,...

Global site tag (gtag.js) - Google Analytics