在 Java 中,wait、notify 和 notifyAll 通常用来实现线程间的通信。如果一个线程想要告诉另一个线程某些事情,就可以使用java.lang.Object
中的notify()
和notifyAll()
方法。wait()和notify()方法的典型实例就是经典的生产者-消费者设计模式,一条线程即生产者生产数据并将数据放入共享的“篮子”中,那么生产者可以通知另一条线程即消费者,让消费者开始消耗数据,因为队列缓冲区即“篮子”中有内容待消费(不为空)。相应的,消费者可以通知生产者可以开始生成更多的数据,因为当它消耗掉某些数据后缓冲区不再为满。
wait()
java.lang.Object
中有三种不同参数的wait()方法。一种是无参方法,表示让线程一直等待下去直到调用了notify()或者notifyAll()方法。另外两种则需要给定一定的时间,在既定时间内线程将会是等待状态。
notify
notify()方法只会唤醒一个在等待状态下的线程,因此如果有多个线程等待一个对象,这个方法将唤醒他们中的一个。唤醒的线程的选择取决于线程管理的操作系统。
notifyAll()
notifyAll方法唤醒所有等待的线程对象,虽然这一进程将首先取决于操作系统的实现。
让我们看一个例子,多个线程对同一对象,我们使用wait(),notify()和notifyAll()方法。
step 1:首先是一个java bean类
Message.java :
public class Message {
private String msg;
public Message(String str){
this.msg=str;
}
public String getMsg() {
return msg;
}
public void setMsg(String str) {
this.msg=str;
}
}
step 2:接着是等待线程类
Waiter.java :
public class Waiter implements Runnable{
private Message msg;
public Waiter(Message msg) {
this.msg = msg;
// TODO Auto-generated constructor stub
}
@Override
public void run() {
// TODO Auto-generated method stub
//获得线程名称
String name = Thread.currentThread().getName();
synchronized (msg) {
System.out.println(name + " waiting to get notified at time:"
+ System.currentTimeMillis());
try {
msg.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println(name+" waiter thread got notified at time:"
+System.currentTimeMillis());
System.out.println(name+" processed: "+msg.getMsg());
}
}
step 3:然后是通知线程类
Notifier.java :
public class Notifier implements Runnable{
private Message msg;
public Notifier(Message m) {
this.msg = m;
// TODO Auto-generated constructor stub
}
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println(name+" started");
try {
Thread.sleep(1000);
synchronized (msg) {
msg.setMsg(name+" Notifier work done");
msg.notify();
//msg.notifyAll();
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
step 4:最后是测试类
WaiterNotifierTest.java :
public class WaiterNotifierTest {
public static void main(String[] args) {
Message msg = new Message("process it");
Waiter waiter = new Waiter(msg);
Thread st1 = new Thread(waiter,"waiter");
st1.start();
Waiter waiter1 = new Waiter(msg);
Thread st11 = new Thread(waiter1,"waiter1");
st11.start();
Notifier notifier = new Notifier(msg);
Thread st2 = new Thread(notifier,"notifier");
st2.start();
System.out.println("All the threads are started");
}
}
当我们将调用上面的程序,我们会看到下面的输出,程序有两个线程等待消息对象,调用notify()方法之后其中之一的线程waiter1已经醒来,其他线程仍然等待通知。
waiter1 waiting to get notified at time:1476096659142
waiter waiting to get notified at time:1476096659143
All the threads are started
notifier started
waiter1 waiter thread got notified at time:1476096660145
waiter1 processed: notifier Notifier work done
如果我们调用notifyall()通知类,下面将产生的输出:
waiter waiting to get notified at time:1476096862914
waiter1 waiting to get notified at time:1476096862915
All the threads are started
notifier started
waiter1 waiter thread got notified at time:1476096863917
waiter waiter thread got notified at time:1476096863917
waiter1 processed: notifier Notifier work done
waiter processed: notifier Notifier work done
可以看到两条程序都被唤醒。
如何使用Wait
从上面的Waiter类中,我们需要注意两点:
- 第一个问题就是,我们怎么在代码里使用wait()呢?因为wait()并不是Thread类下的函数,我们并不能使用Thread.call()。事实上很多Java程序员都喜欢这么写,因为它们习惯了使用Thread.sleep(),所以他们会试图使用Thread.wait()来达成相同的目的,但很快他们就会发现这并不能顺利解决问题。正确的方法是对在多线程间共享的那个Object来使用wait。在生产者消费者问题中,这个共享的Object就是那个缓冲区队列,在本例中就是Message对象。
- 第二个问题是,既然我们应该在synchronized的函数或是对象里调用wait,那哪个对象应该被synchronized呢?答案是,那个你希望上锁的对象就应该被synchronized,即那个在多个线程间被共享的对象。在生产者消费者问题中,应该被synchronized的就是那个缓冲区队列,在本例中就是Message对象。
永远在循环(loop)里调用 wait 和 notify,不是在 If 语句
现在你知道wait应该永远在被synchronized的背景下和那个被多线程共享的对象上调用,下一个一定要记住的问题就是,你应该永远在while循环,而不是if语句中调用wait。因为线程是在某些条件下等待的——在我们的例子里,即“如果缓冲区队列是满的话,那么生产者线程应该等待”,你可能直觉就会写一个if语句。但if语句存在一些微妙的小问题,导致即使条件没被满足,你的线程你也有可能被错误地唤醒。所以如果你不在线程被唤醒后再次使用while循环检查唤醒条件是否被满足,你的程序就有可能会出错——例如在缓冲区为满的时候生产者继续生成数据,或者缓冲区为空的时候消费者开始小号数据。
基于以上认知,下面这个是使用wait和notify函数的规范代码模板:
synchronized (sharedObject) {
while (condition) {
sharedObject.wait();
// (Releases lock, and reacquires on wakeup)
}
// do action based upon condition e.g. take or put into queue
}
就像我之前说的一样,在while循环里使用wait的目的,是在线程被唤醒的前后都持续检查条件是否被满足。如果条件并未改变,wait被调用之前notify的唤醒通知就来了,那么这个线程并不能保证被唤醒,有可能会导致死锁问题。
生产者-消费者模型
在上面的例子中我们只是简单的了解了wait()和notify()以及notifyAll()的基本用法。接下来我们将进入经典的生产者消费者模型来认识wait()和notify()方法。
那么什么事生产者-消费者模型呢?举个现实生活中的例子吧。李小明爸爸老王的苹果园丰收了。老王往篮子里放苹果,篮子只能装得下10个苹果,规定只有当篮子里的苹果装满的时候,小明才可以开始吃苹果了。当小明吃完篮子里的苹果时,老王才会往篮子里放苹果。这个其实就是多线程同步的问题。
我们有两个线程,分别名为Producer(生产者)和Consumer(消费者),他们分别继承了Producer和Consumer类,而Producer和Consumer都继承了Thread类。Producer和Consumer想要实现的代码逻辑都在run()函数内。Main线程开始了生产者和消费者线程,并声明了一个LinkedList作为缓冲区队列(在Java中,LinkedList实现了队列的接口)。生产者在无限循环中持续往LinkedList里插入随机整数直到LinkedList满。我们在while(queue.size == maxSize)循环语句中检查这个条件。请注意到我们在做这个检查条件之前已经在队列对象上使用了synchronized关键词,因而其它线程不能在我们检查条件时改变这个队列。如果队列满了,那么Producer线程会在Consumer线程消耗掉队列里的任意一个整数,并用notify来通知Producer线程之前持续等待。在我们的例子中,wait和notify都是使用在同一个共享对象上的。
生产者(Producer):
Producer.java :
public class Producer extends Thread{
private final Queue queue;
private int maxSize;
public Producer(Queue queue,int maxSize,String name) {
super("Producer");
this.maxSize = maxSize;
this.queue = queue;
// TODO Auto-generated constructor stub
}
@Override
public void run() {
// TODO Auto-generated method stub
while(true){
synchronized (queue) {
while(queue.size()==maxSize){
try {
System.out .println("Queue is full, " +
"Producer thread waiting for " +
"consumer to take something from queue");
queue.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
Random random = new Random();
int i = random.nextInt();
System.out.println("Producer value : "+i);
queue.add(i);
queue.notifyAll();
}
}
}
}
消费者(Constomer):
Constomer.java :
public class Customer extends Thread{
private Queue queue;
private int maxSize;
public Customer(Queue queue,int maxSize,String name) {
super("Customer");
this.maxSize = maxSize;
this.queue = queue;
// TODO Auto-generated constructor stub
}
@Override
public void run() {
// TODO Auto-generated method stub
while(true){
synchronized (queue) {
while(queue.size()==0){
try {
System.out.println("Queue is empty," +
"Consumer thread is waiting" +
" for producer thread to put something in queue");
queue.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
int number = (int) queue.poll();
System.out.println("Consuming value : " + queue.remove());
queue.notifyAll();
}
}
}
}
测试类(ProducerCustomerTest):
ProducerCustomerTest.java :
public class ProducerCustomerTest {
public static void main(String[] args) {
System.out.println("How to use wait and notify method in Java");
System.out.println("Solving Producer Consumper Problem");
Queue buffer = new LinkedList();
int maxSize =10;
Thread producer = new Producer(buffer,maxSize,"Producer");
Thread customer = new Customer(buffer,maxSize,"Customer");
producer.start();
customer.start();
}
}
输出结果如下:
生产者将不停地生产数据,消费者也将不停地消耗数据。
这就是在这个简单的例子中使用java线程间通信wait和notify方法。你可以看到,生产者和消费者线程彼此交流和使用共享队列的数据共享。这是生产者消费者设计模式以及经典的例子,它本身涉及的线程间的通信和数据在java线程之间共享。