JAVA多线程11-基础篇-线程间通讯wait,notify

本章介绍 线程间的协作方式,主要包含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)}.
* <p>
* 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.
* <p>
* As in the one argument version, interrupts and spurious wakeups are
* possible, and this method should always be used in a loop:
* <pre>
* synchronized (obj) {
* while (<condition does not hold>)
* obj.wait();
* ... // Perform action appropriate to condition
* }
* </pre>
* 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.
* <p>
* 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.
* <p>
* 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:
* <ul>
* <li>By executing a synchronized instance method of that object.
* <li>By executing the body of a {@code synchronized} statement
* that synchronizes on the object.
* <li>For objects of type {@code Class,} by executing a
* synchronized static method of that class.
* </ul>
* <p>
* 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.
* <p>
* 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.
* <p>
* 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

结果分析:

  1. 生产者和消费者共享一个队列Queue
  2. 核心流程都是先获取对象锁,然后根据条件判断是等待还是唤醒

四、几个问题

(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)线程间的交互模型如图所示:


转载请注明作者及出处,并附上链接http://www.jianshu.com/u/ada8c4ee308b

推荐阅读更多精彩内容