本章介绍 线程间的协作方式,主要包含wait(),notify(),notifyAll()方法的使用以及代码示例

一、等待/通知机制介绍

在多线程环境下,为了保证线程安全,我们使用同步锁来保证任何时刻只有一个线程可以访问共享资源。而在实际场景中,除了线程互斥,我们也希望线程间可以通过协作来解决问题,而java并没有手段使得线程间直接的交互,通常的做法是通过一个共享变量来实现线程间的通讯,例如:A线程需要读取B线程发送的信息,代码如下:

public class Demo1 {
private static String msg;
public static void main(String[] args) {
work();
}
public static void work() {
Thread t1 = new Thread(() -> {
System.out.println(msg);
});
Thread t2 = new Thread(() -> {
msg = "t2 set the msg";
});
t1.start();
t2.start();
}
}

结果输出:

null 或者 t2 set the msg

结果说明:这个例子中线程t1,t2通过msg来通信,t1需要读取t2设置的msg,由于线程的执行无法预测,我们不能保证t1一定能读取t2设置的msg,所以会输出如上的结果。

如果我们想保证t1能够读取到t2设置的值,可以调整为如下代码:

public class Demo1 {
private static String msg;
public static void main(String[] args) {
work();
}
public static void work() {
Thread t1 = new Thread(() -> {
while (msg == null) {
}
System.out.println(msg + "!!!");
});
Thread t2 = new Thread(() -> {
msg = "t2 set the msg";
});
t1.start();
t2.start();
}
}

结果输出:

t2 set the msg!!!

结果说明:t1通过死循环的方式直到msg有值为止,这种方式虽然能满足需求,但是非常耗CPU资源。

为了更好的实现线程间的交互,java提供了等待/通知机制,本质上也是通过锁来实现线程间的通信。这种机制是通过Object类的wait(),notify()方法来实现,下面对这几个方法进行介绍。

二、wait,notify,notifyAll方法介绍

/**
* Causes the current thread to wait until another thread invokes the
* {@link java.lang.Object#notify()} method or the
* {@link java.lang.Object#notifyAll()} method for this object.
* In other words, this method behaves exactly as if it simply
* performs the call {@code wait(0)}.
* 
* The current thread must own this object's monitor. The thread
* releases ownership of this monitor and waits until another thread
* notifies threads waiting on this object's monitor to wake up
* either through a call to the {@code notify} method or the
* {@code notifyAll} method. The thread then waits until it can
* re-obtain ownership of the monitor and resumes execution.
* 
* As in the one argument version, interrupts and spurious wakeups are
* possible, and this method should always be used in a loop:
* 
 
  
 
* synchronized (obj) {
* while ()
* obj.wait();
* ... // Perform action appropriate to condition
* }
* 
* This method should only be called by a thread that is the owner
* of this object's monitor. See the {@code notify} method for a
* description of the ways in which a thread can become the owner of
* a monitor.
*/
public final native void wait() throws InterruptedException;
/**
* Wakes up a single thread that is waiting on this object's
* monitor. If any threads are waiting on this object, one of them
* is chosen to be awakened. The choice is arbitrary and occurs at
* the discretion of the implementation. A thread waits on an object's
* monitor by calling one of the {@code wait} methods.
* 
* The awakened thread will not be able to proceed until the current
* thread relinquishes the lock on this object. The awakened thread will
* compete in the usual manner with any other threads that might be
* actively competing to synchronize on this object; for example, the
* awakened thread enjoys no reliable privilege or disadvantage in being
* the next thread to lock this object.
* 
* This method should only be called by a thread that is the owner
* of this object's monitor. A thread becomes the owner of the
* object's monitor in one of three ways:
* 
* 
By executing a synchronized instance method of that object. 
 
* 
By executing the body of a {@code synchronized} statement 
 
* that synchronizes on the object.
* 
For objects of type {@code Class,} by executing a 
 
* synchronized static method of that class.
* 
* 
* Only one thread at a time can own an object's monitor.
*/
public final native void notify();
/**
* Wakes up all threads that are waiting on this object's monitor. A
* thread waits on an object's monitor by calling one of the
* {@code wait} methods.
* 
* The awakened threads will not be able to proceed until the current
* thread relinquishes the lock on this object. The awakened threads
* will compete in the usual manner with any other threads that might
* be actively competing to synchronize on this object; for example,
* the awakened threads enjoy no reliable privilege or disadvantage in
* being the next thread to lock this object.
* 
* This method should only be called by a thread that is the owner
* of this object's monitor. See the {@code notify} method for a
* description of the ways in which a thread can become the owner of
* a monitor.
*/
public final native void notifyAll();

wait,notify,notifyAll方法总结:

这几个方法都定义在Object类中

都是final方法,不能被重写

wait是阻塞方法,线程被中断会抛出InterruptedException异常。当前线程必须拥有对象锁,才能调用对象的wait方法,调用wait方法会使当前线程阻塞,当前线程会进入到对象的等待队列,同时释放对象锁,调用wait的伪代码如下:

synchronized (obj) {
while (condition does not hold)
obj.wait();
... // do something
}

wait(timeout) 在wait()的基础上增加了超时时间,超过该时间线程会被重新唤醒

调用notify、notifyAll方法的线程必须拥有对象锁,notify会唤醒等待对象监视器的单个线程,被唤醒的线程是随机的,notifyAll方法唤醒等待对象监视器的所有线程,被唤醒的线程(等待线程)不会立即竞争对象锁,只有当前的唤醒线程释放对象锁后,等待线程才能竞争对象锁

三、代码示例

先描述一个场景:

外婆家餐厅一直很火爆,我去过杭州西湖、北京APM、沈阳万象城这三家,味道都差不多,不过西湖边上的新白鹿感觉更胜一筹啊_,扯远了....

我们去外婆家吃饭,如果餐厅有空位,我们可以直接进去用餐,如果餐厅满员,我们需要排队,我们等着叫"XXX号,外婆让你回家吃饭了",然后我们就可以进入餐厅吃饭,餐厅的容量实际是消费者和服务员之间的共享资源

这个场景抽象一下,可以认为用户是一个线程,而餐厅的服务人员相当于另外一个线程,餐厅是用餐者和服务人员的共享信息

代码如下:

public class WaitNotifyDemo1 {
public static void main(String[] args) {
final Queue queue = new LinkedList<>();
Thread producer = new Producer("producer", queue);
Thread consumer = new Consumer("consumer", queue);
producer.start();
consumer.start();
}
}
public class Producer extends Thread {
private final Queue sharedQueue;//餐厅容量
public Producer(String name, Queue queue) {
super(name);
this.sharedQueue = queue;
}
@Override
public void run() {
//假设餐厅最多容纳10桌客人
for (int i = 0; i <= 10; i++) {
synchronized (sharedQueue) {
while (sharedQueue.size() == 10) {
try {
sharedQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
sharedQueue.add(i);
System.out.println("producer " + i);
sharedQueue.notify();
}
}
}
}
public class Consumer extends Thread {
private final Queue sharedQueue;
public Consumer(String name, Queue queue) {
super(name);
this.sharedQueue = queue;
}
@Override
public void run() {
//消费者持续不断的来就餐
while (true) {
synchronized (sharedQueue) {
while (sharedQueue.size() == 0) {
try {
sharedQueue.wait();
sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Integer o = (Integer) sharedQueue.poll();
sharedQueue.notify();
if (o == 10) {//只接纳10桌客人
System.out.println("stop service");
break;
}
}
}
}
}

结果输出:

producer 0

producer 1

producer 2

producer 3

producer 4

producer 5

producer 6

producer 7

producer 8

producer 9

producer 10

stop service

结果分析:

生产者和消费者共享一个队列Queue

核心流程都是先获取对象锁,然后根据条件判断是等待还是唤醒

四、几个问题

(1) 为什么调用wait,notify 方法都必须获得对像锁?

wait方法使当前线程挂起,notify会唤醒在该对象上挂起的线程,也就是说wait,notify一定是成对出现且针对同一个对象的挂起、唤醒操作,我们知道每个对象都有一个监视器对象,synchronized就是通过进入监视器来实现线程间互斥的,基于这个特性,恰好能使wait,notify建立关系,形成协作,所以通过对象锁,既能实现线程间互斥,也能实现线程间协作

(2)为什么 wait,notify,notifyAll都定义在Object类中?

因为wait,notify是依赖 同步锁进行协作的,而每个对象都有且仅有一把对象锁,所以定义在Object类中可以完美解决线程间协作的问题

(3)如果不在同步块或者同步方法中执行wait,notify会怎样?

大家可以自行验证一下,这里就不做实践了

五、总结

(1)等待/通知 机制是通过对象锁来实现的

(2)notify,notifyAll,wait必须在同步块或者同步方法中执行

(3)线程间的交互模型如图所示: