多线程中的生产者消费者问题 - 线程的同步
为了完成多个任务,常创建多个线程,它们可能毫不相关,但有时它们完成的任务在某种程度上有一定的关系,此时就需要线程之间有一些交互。在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();
}