线程间通信

为什么会有等待通知机制

无限循环,浪费CPU资源,而解决死锁的思路之一就是 破坏请求和保持条件——多个线程实现互斥访问共享资源时会互相发送信号或等待信号,比如线程等待数据到来的通知,线程收到变量改变的信号等。

医院就医过程:

序号 就医 程序解释(自己的视角)
1 挂号成功,到诊室门口排号候诊 排号的患者(线程)尝试获取【互斥锁】
2 大夫叫到自己,进入诊室就诊 自己【获取到互斥锁】
3 大夫简单询问,要求做检查(患者缺乏报告不能诊断病因) 进行【条件判断】,线程要求的条件【没满足】
4 自己出去做检查 线程【主动释放】持有的互斥锁
5 大夫叫下一位患者 另一位患者(线程)获取到互斥锁
6 自己拿到检测报告 线程【曾经】要求的条件得到满足(实则【被通知】)
7 再次在诊室门口排号候诊 再次尝试获取互斥锁
8

Java语言中,其内置的关键字 synchronized 和 方法wait()notify()/notifyAll() 就能实现上面提到的等待/通知机制:

注意:

  1. 一个锁对应一个【入口等待队列】,不同锁的入口等待队列没任何关系,不存在竞争关系。(不同患者进入眼科和耳鼻喉科看大夫一点冲突都没有)

  2. wait()notify()/notifyAll() 要在synchronized内部被使用,并且,如果锁的对象是this,就要 this.wait(),this.notify()/this.notifyAll() , 否则JVM就会抛出java.lang.IllegalMonitorStateException

    因为调用这几个方法前必须拿到当前对象的监视器monitor对象,也就是说notify/notifyAllwait方法依赖于monitor对象,而monitor存在于对象头的Mark Word中(存储monitor引用指针),而synchronized关键字可以获取monitor

生产者-消费者模型

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

  • 内存队列本身要加锁,才能实现线程安全;
  • 阻塞。当内存队列满了,生产者放不进去时,会被阻塞;当内存队列是空的时候,消费者无事可做,会被阻塞。
    • 线程自己阻塞自己,也就是生产者、消费者线程各自调用wait()notify()
    • 用一个阻塞队列,当取不到或者放不进去数据的时候,入队/出队函数本身就是阻塞的。这也就是BlockingQueue的实现。
  • 双向通知。消费者被阻塞之后,生产者放入新数据,要通知消费者;反之,生产者被阻塞之后,消费者消费了数据,要通知生产者。
    • wait()notify()机制
    • Condition机制

wait和notify

waitnotify方法并不是Thread特有的方法,而是Object中的方法!

public class Object {
    
    public final void wait() throws InterruptedException {
        wait(0);  // 0代表永不超时
    }
    
    public final void wait(long timeout, int nanos) throws InterruptedException {
        if (timeout < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos > 0) {
            timeout++;
        }

        wait(timeout);
    }
    
    public final native void wait(long timeout) throws InterruptedException;

    public final native void notify();
    public final native void notifyAll();
}

wait

  • wait方法必须拥有该对象的monitor,也就是wait方法必须在同步方法中使用

  • 当前线程执行了该对象的wait方法之后,将会放弃对该monitor的所有权并且进入与该对象关联的wait set中,也就是说一旦线程执行了某个object的wait方法之后,它就会释放对该对象monitor的所有权,其他线程也会有机会继续争抢该monitor的所有权。Object的wait(long timeout)方法导致当前线程进入阻塞,直到有其他线程调用了Object的notify或者notifyAll方法才能将其唤醒,或者阻塞时间到达了timeout时间而自动唤醒。——释放锁;阻塞,等待被其他线程唤醒;重新拿锁

  • wait方法是可中断方法,这也就意味着,当前线程一旦调用了wait方法进入阻塞状态,其他线程是可以使用interrupt方法将其打断的可中断方法被打断后会收到中断异常InterruptedException,同时interrupt标识也会被擦除

notify

随机唤醒一个:一个线程调用共享对象的notify()方法,会唤醒一个在该共享变量上调用wait()方法后被挂起的线程,一个共享变量上可能有多个线程在等待,具体唤醒那一个,是随机的

  • 唤醒单个正在执行该对象wait方法的线程。
  • 如果有某个线程由于执行该对象的wait方法而进入阻塞则会被唤醒,如果没有则会忽略。
  • 被唤醒的线程,需要重新获取对该对象所关联monitor的lock才能继续执行。
  • notify方法必须拥有该对象的monitor,也就是notify方法必须在同步方法中使用

使用notify方法的情景——线程池:

  1. 所有等待线程拥有相同的等待条件;
  2. 所有等待线程被唤醒后,执行相同的操作;
  3. 只需唤醒一个线程。

wait和sleep

相同点

  • wait和sleep方法都可以使线程进入阻塞状态
  • wait和sleep方法均是可中断方法,由于两个方法进入阻塞状态的线程,被中断后都会收到中断异常。

不同点

  • wait是Object的方法,而sleep是Thread特有的方法;
  • wait方法的执行必须在同步方法中进行,而sleep则不需要;
  • 线程在同步方法中执行sleep方法时,并不会释放monitor的锁,而wait方法则会释放monitor的锁
  • sleep方法短暂休眠之后会主动退出阻塞,而wait方法(没有指定wait时间)则需要被其他线程中断后才能退出阻塞。

单线程间通信

服务端有若干个线程,会从队列中获取相应的Event进行异步处理,那么这些线程又是如何从队列中获取数据的呢?

  • 不断地轮询:如果有数据,则读取数据并处理;如果没有则等待若干时间,再次轮询;
  • 通知机制:如果队列中有Event,则通知工作的线程开始工作;没有Event,则工作线程休息并等待通知。

实现一个EventQueue

  • 队列满——最多可容纳多少个Event;
  • 队列空——当所有的Event都被处理,并且没有新的Event被提交的时候;
  • 有Event但是没有满。
package communication;

import java.util.LinkedList;
import static java.lang.Thread.currentThread;

/**
 * 多个线程向队列添加元素
 */
public class EventQueue
{

    private final int max;

    static class Event {
    }

    // 共享的资源
    private final LinkedList<Event> eventQueue = new LinkedList<>();

    private final static int DEFAULT_MAX_EVENT = 5;

    public EventQueue() {
        this(DEFAULT_MAX_EVENT);
    }

    public EventQueue(int max) {
        this.max = max;
    }

    /**
     * 向队列添加事件
     * @param event 事件
     */
    public void offer (Event event) {
        synchronized (eventQueue) {
	    // 注意这里使用的if
            if (eventQueue.size() >= max) {
                try {
                    console(" the queue is full.");
                    // 队列已满,执行提交的线程开始等待
                    eventQueue.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            console(" the new event is submitted");
            eventQueue.addLast(event);
            // 通知所有被阻塞的线程,可以开始执行相关操作
            eventQueue.notify();
        }
    }

    /**
     * 从队列中取出元素
     * @return 取出的事件
     */
    public Event take() {
        synchronized (eventQueue) {
            // 注意这里使用的if
            if (eventQueue.isEmpty()) {
                try {
                    console(" the queue is empty.");
                    // 队列是空的,执行提取的线程开始等待
                    eventQueue.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            Event event = eventQueue.removeFirst();
            // 通知所有被阻塞的线程,可以开始执行相关操作
            this.eventQueue.notify();
            console(" the event " + event + " is handled.");
            return event;
        }
    }

    private void console(String message) {
        System.out.printf("%s:%s\n", currentThread().getName(), message);
    }
}

EventQueue中定义了一个队列

  • offer方法会提交一个Event至队尾,如果此时队列已经满了,那么提交的线程将会被阻塞,这是调用了wait方法的结果。
  • take方法会从队头获取数据,如果队列中没有可用数据,那么工作线程就会被阻塞,这也是调用wait方法的直接结果。
  • notify方法的作用是唤醒那些曾经执行monitor的wait方法进入阻塞的线程
package communication;

import java.util.concurrent.TimeUnit;

public class EventClient
{

    public static void main(String[] args)
    {
        final EventQueue eventQueue = new EventQueue();

        // 单线程提交
        for (int i = 1; i < 2; i++) {
            new Thread(() ->
            {
                for (; ; )
                {
                    eventQueue.offer(new EventQueue.Event());
                    // 防止新的线程还没来得及创建,而一直执行旧线程
                    try
                    {
                        TimeUnit.MILLISECONDS.sleep(1000);
                    } catch (InterruptedException e)
                    {
                        e.printStackTrace();
                    }
                }
            }, "Producer-" + i).start();
        }

        // 单线程获取并处理
        for (int i = 1; i < 2; i++) {
            new Thread(() ->
            {
                for (; ; )
                {
                    eventQueue.take();
                    // 防止新的线程还没来得及创建,而一直执行旧线程
                    try
                    {
                        TimeUnit.MILLISECONDS.sleep(1000);
                    } catch (InterruptedException e)
                    {
                        e.printStackTrace();
                    }
                }
            }, "Consumer-" + i).start();
        }
    }
}

输出结果:

Consumer-1: the queue is empty.
Producer-1: the new event is submitted
Consumer-1: the event communication.EventQueue$Event@cc8b1a is handled.
Producer-1: the new event is submitted
Consumer-1: the event communication.EventQueue$Event@1aad818 is handled.
Consumer-1: the queue is empty.
Producer-1: the new event is submitted
Consumer-1: the event communication.EventQueue$Event@332131 is handled.
Consumer-1: the queue is empty.
Producer-1: the new event is submitted
...

多线程间通信

package communication;

import java.util.concurrent.TimeUnit;

public class EventClient
{

    public static void main(String[] args)
    {
        final EventQueue eventQueue = new EventQueue();

        // 多线程提交
        for (int i = 1; i < 3; i++) {
            new Thread(() ->
            {
                for (; ; )
                {
                    eventQueue.offer(new EventQueue.Event());
                    // 防止新的线程还没来得及创建,而一直执行旧线程
                    try
                    {
                        TimeUnit.MILLISECONDS.sleep(100);
                    } catch (InterruptedException e)
                    {
                        e.printStackTrace();
                    }
                }
            }, "Producer-" + i).start();
        }

        // 多线程获取并处理
        for (int i = 1; i < 3; i++) {
            new Thread(() ->
            {
                for (; ; )
                {
                    eventQueue.take();
                    // 防止新的线程还没来得及创建,而一直执行旧线程
                    try
                    {
                        TimeUnit.MILLISECONDS.sleep(1000);
                    } catch (InterruptedException e)
                    {
                        e.printStackTrace();
                    }
                }
            }, "Consumer-" + i).start();
        }
    }
}

如果对EventQueue.java不做修改,会出现:

  • Linkedlist元素为5时执行addlast方法(将take线程sleep时间设置得比offer线程sleep时间长)
Consumer-1: the queue is empty.
Producer-2: the new event is submitted
Producer-1: the new event is submitted
Consumer-2: the event communication.EventQueue$Event@12b9c83 is handled.
Consumer-1: the event communication.EventQueue$Event@1330f48 is handled.
Producer-1: the new event is submitted
Producer-2: the new event is submitted
Producer-1: the new event is submitted
Producer-2: the new event is submitted
Producer-1: the new event is submitted
Producer-2: the queue is full.
Producer-1: the queue is full. // 此时已经满了:5
Consumer-2: the event communication.EventQueue$Event@1ec6496 is handled. // 此时为4
Producer-2: the new event is submitted // 此时已经满了:5
Producer-1: the new event is submitted // 又添加了,出现问题 

假设某个时刻EventQueue中存在5个Event数据,其中两个线程在执行offer方法的时候分别因为调用了wait方法而进入阻塞中,另外的一个线程执行take方法消费了event元素并且唤醒了一个offer线程,而该offer线程执行了addLast方法之后,queue中的元素为5,并且再次执行唤醒方法,恰巧另外一个offer线程也被唤醒,因此可以绕开阀值检查eventQueue.size() >= max由于是if语句来判断,阻塞的线程被唤醒后,是从eventQueue.wait();往后开始继续执行的),致使EventQueue中的元素超过5个。

  • Linkedlist为空时执行removeFirst方法(将take线程sleep时间设置得比offer线程sleep时间短)
Consumer-1: the queue is empty.
Consumer-2: the queue is empty.
Exception in thread "Consumer-2" java.util.NoSuchElementException
	at java.util.LinkedList.removeFirst(LinkedList.java:270)
	at communication.EventQueue.take(EventQueue.java:72)
...

假设EventQueue中的元素为空,两个线程在执行take方法时分别调用wait方法进入了阻塞之中,另外一个offer线程执行addLast方法之后唤醒了其中一个阻塞的take线程,该线程顺利消费了一个元素之后恰巧再次唤醒了一个take线程,这时就会导致执行空LinkedList的removeFirst方法。

生产者消费者实例(多线程间通信)

wait标准范式:

synchronized() {
  while(条件不满足) {
    wait();
  }
}

将临界值判断语句if改成while,将notify改成notifyAll

package communication;

import java.util.LinkedList;

import static java.lang.Thread.currentThread;

/**
 * 多个线程向队列添加元素
 * @author Chenzf
 */
public class EventQueue
{

    private final int max;

    static class Event {
    }

    // 共享的资源
    private final LinkedList<Event> eventQueue = new LinkedList<>();

    private final static int DEFAULT_MAX_EVENT = 5;

    public EventQueue() {
        this(DEFAULT_MAX_EVENT);
    }

    public EventQueue(int max) {
        this.max = max;
    }

    /**
     * 向队列添加事件
     * @param event 事件
     */
    public void offer (Event event) {
        synchronized (eventQueue) {
			// 注意这里使用的while
            while (eventQueue.size() >= max) {
                try {
                    console(" the queue is full.");
                    // 队列已满,执行提交的线程开始等待
                    eventQueue.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            console(" the new event is submitted");
            eventQueue.addLast(event);
            // 通知所有被阻塞的线程,可以开始执行相关操作
            eventQueue.notifyAll();
        }
    }

    /**
     * 从队列中取出元素
     * @return 取出的事件
     */
    public Event take() {
        synchronized (eventQueue) {
			// 注意这里使用的while
            while (eventQueue.isEmpty()) {
                try {
                    console(" the queue is empty.");
                    // 队列是空的,执行提取的线程开始等待
                    eventQueue.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            Event event = eventQueue.removeFirst();
            // 通知所有被阻塞的线程,可以开始执行相关操作
            this.eventQueue.notifyAll();
            console(" the event " + event + " is handled.");
            return event;
        }
    }

    private void console(String message) {
        System.out.printf("%s:%s\n", currentThread().getName(), message);
    }
}

notify和Condition

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

ConditionawaitsignalsingalAllObjectwaitnotifynotifyAll都可以实现的需求,两者在使用上也是非常类似,都需要先获取某个锁之后才能调用,而不同的是Objectwaitnotify对应的是synchronized方式的锁,Conditionawaitsingal则对应的是ReentrantLock(实现Lock接口的锁对象)对应的锁。

加入队列已满,所有的生产者现场阻塞,某个时刻消费者消费了一个元素,则需要唤醒某个生产者线程,而通过Objectnotify方式唤醒的线程不能确保一定就是一个生产者线程,因为notify随机唤醒某一个正在该synchronized对应的锁上面通过wait方式阻塞的线程,如果这时正好还有消费者线程也在阻塞中,则很可能唤醒的是一个消费者线程notifyAll更是会唤醒所有在对应锁上通过wait方式阻塞的线程,而不管是生产者还是消费者线程

posted @ 2021-03-14 18:48  chenzufeng  阅读(48)  评论(0编辑  收藏  举报