多线程中的生产者消费者问题

为了完成多个任务,常创建多个线程,它们可能毫不相关,但有时它们完成的任务在某种程度上有一定的关系,此时就需要线程之间有一些交互。在Java中,使用一对方法wait()和notify()/notifyAll()实现线程的交互。

同步问题的提出

操作系统中的生产者消费者问题,就是一个经典的同步问题。举一个例子,有两个人,一个人在刷盘子,另一个人在烘干。这两个人各自代表一个线程,他们之间有一个共享的对象 --- 盘架,刷好而等待烘干的盘子放在盘架上。两个人在没有事做事都愿意歇着。显然,盘架上有刷好的盘子时,烘干的人才能开始工作;而如果刷盘子的人刷的太快,刷好的盘子占满了盘架时,他就不能再继续工作了,而要等到盘架上有空位置才行。

这个示例要说明的问题是,生产者生产一个产品后就放入共享对象中,而不管共享对象中是否有产品。消费者从共享对象中取用产品,但不检测是否已经取过。

若共享对象中只能存放一个数据,可能出现以下问题(线程不同步的情况下):

  • 生产者比消费者快时,消费者会漏掉一些数据没有取到。
  • 消费者比生产者快时,消费者取相同的数据。

在java语言中,可以用wait()和notify()/notifyAll()方法来协调线程间的运行速度关系,这些方法都定义在java.lang.Object类中。

解决方法

为了解决线程运行速度问题,Java提供了一种建立在对象实例之上的交互方法。Java中的每个对象实例都有两个线程队列和他相连第一个用来排列等待锁定标志的线程。第二个则用来实现wait()和notify()的交互机制

类java.lang.Object中定义了三个方法wait()和notify()/notifyAll()。

wait方法导致当前的线程等待,它的作用是让当先线程释放其所持有的“对象互斥锁”,进入wait队列(等待队列);而notify()/notifyAll()方法的作用是唤醒一个或所有正在等待队列中等待的线程,并将它(们)移入用一个“对象互斥锁”队列。notify()/notifyAll()方法和wait()方法都只能在被声明为synchronized的方法或代码中调用。方法notify()最多只能释放等待队列中的第一个线程,如果有多个线程在等待,则其他的线程将继续留在队列中。notifyAll()方法能够释放所有等待线程。

再来看看前面刷盘子的例子。线程t1代表刷盘子,线程t2代表烘干,它们都有对盘架drainingBoard的访问权。假设线程t2(烘干线程)想要进行烘干工作,而此时盘架时空的,则应表示如下:

if(drainingBoard.isEmpty())
drainingBoard.wait(); //盘架空时则等待

当线程t2执行了wait()调用后,它不可以再执行,并加入到对象drainingBoard的等待队列中。在有线程将它从这个队列释放之前,它不能再次运行。

那么,烘干线程怎样才能重新运行呢?这应该有洗刷线程t1来通知它已经有工作可以做了,运行drainingBoard的notify调用可以做到这一点:

drainingBoard.addItem(); //放入一个盘子
drainingBoard.notify();

此时,drainingBoard的等待队列中第一个阻塞线程由队列中释放出来,并可重新参加运行的竞争。

注意,在这里使用notify调用时,没有考虑是否有正在等待的线程。事实上,应该只有在增加盘子后使得盘架不再空时才执行这个调用。如果等待队列中没有阻塞线程时调用了方法notify(),则这个调用不做任何工作。notify()调用不会被保留到以后再发生效用。

使用这个机制,程序能够非常简单的协调洗刷线程和烘干线程,而且并不需要了解这些线程的身份。每当执行一项工作,使得另一个线程能够开始工作,就通知对象drainingBoard(调用notify());每当由于盘架空或满而不能继续工作时,就等待对象drainingBoard(调用wait())。

在调用一个对象的wait(),notify()/notifyAll()时,必须首先持有该对象的锁定标志,因此这些方法必须在同步程序块中调用。这样,应该将代码改写如下:

synchronized(drainingBoard) {
if(drainingBoard.isEmpty())
​ drainingBoard.wait();
}

synchronized(drainingBoard) {
​ drainingBoard.addItem();
​ drainingBoard.notify();
}

原文地址:https://www.cnblogs.com/LisonLiou/p/13690118.html