天天看点

并发编程:wait与notify

作者:日拱一卒程序猿

一、生产者−消费者模型

生产者-消费者模型是一个常见的多线程编程模型,如下图所示:

并发编程:wait与notify

一个内存队列,多个生产者线程往内存队列中放数据;多个消费者线程从内存队列中取数据。要实 现这样一个编程模型,需要做下面几件事情:

1. 内存队列本身要加锁,才能实现线程安全。

2. 阻塞。当内存队列满了,生产者放不进去时,会被阻塞;当内存队列是空的时候,消费者无事 可做,会被阻塞。

3. 双向通知。消费者被阻塞之后,生产者放入新数据,要notify()消费者;反之,生产者被阻塞之 后,消费者消费了数据,要notify()生产者。

第1件事情必须要做,第2件和第3件事情不一定要做。例如,可以采取一个简单的办法,生产者放 不进去之后,睡眠几百毫秒再重试,消费者取不到数据之后,睡眠几百毫秒再重试。但这个办法效率低 下,也不实时。所以,我们只讨论如何阻塞、如何通知的问题。

1.如何阻塞?

办法1:线程自己阻塞自己,也就是生产者、消费者线程各自调用wait()和notify()。

办法2:用一个阻塞队列,当取不到或者放不进去数据的时候,入队/出队函数本身就是阻塞的。

2.如何双向通知?

办法1:wait()与notify()机制。

办法2:Condition机制。

二、为什么必须和synchronized一起使用

在Java里面,wait()和notify()是Object的成员函数,是基础中的基础。为什么Java要把wait()和 notify()放在如此基础的类里面,而不是作为像Thread一类的成员函数,或者其他类的成员函数呢?

先看为什么wait()和notify()必须和synchronized一起使用?请看下面的代码:

并发编程:wait与notify

然后,开两个线程,线程A调用method1(),线程B调用method2()。

答案已经很明显:两个线程之 间要通信,对于同一个对象来说,一个线程调用该对象的wait(),另一个线程调用该对象的notify(),该对象本身就需要同步!所以,在调用wait()、notify()之前,要先通过synchronized关键字同步给对象, 也就是给该对象加锁。

synchronized关键字可以加在任何对象的实例方法上面,任何对象都可能成为锁。因此,wait()和 notify()只能放在Object里面了。

三、为什么wait()的时候必须释放锁

当线程A进入synchronized(obj1)中之后,也就是对obj1上了锁。此时,调用wait()进入阻塞状态, 一直不能退出synchronized代码块;那么,线程B永远无法进入synchronized(obj1)同步块里,永远没 有机会调用notify(),发生死锁。

这就涉及一个关键的问题:在wait()的内部,会先释放锁obj1,然后进入阻塞状态,之后,它被另外 一个线程用notify()唤醒,重新获取锁!其次,wait()调用完成后,执行后面的业务逻辑代码,然后退出 synchronized同步块,再次释放锁。

wait()内部的伪代码如下:

并发编程:wait与notify

如此则可以避免死锁。

四、wait()与notify()的问题

以上述的生产者-消费者模型来看,其伪代码大致如下:

public void enqueue() {
	synchronized(queue) {
	while (queue.full()) {
	queue.wait();
  }
  //... 数据入列
  queue.notify(); // 通知消费者,队列中有数据了。
 }
}
public void dequeue() {
  synchronized(queue) {
  while (queue.empty()) {
  queue.wait();
  }
  // 数据出队列
  queue.notify(); // 通知生产者,队列中有空间了,可以继续放数据了。
 }
}           

生产者在通知消费者的同时,也通知了其他的生产者;消费者在通知生产者的同时,也通知了其他 消费者。原因在于wait()和notify()所作用的对象和synchronized所作用的对象是同一个,只能有一个对 象,无法区分队列空和列队满两个条件。这正是Condition要解决的问题。

继续阅读