一  wait/notify如何使用

  wait\notify 这两个方法是Object 类中的方法,这两个方法包括他们的重载方法一共有5个,而Object 类中一共才 12 个方法,可见这2个方法的重要性。我们先看看 JDK 中的代码:

public final native void notify();
public final native void notifyAll();
public final void wait() throws InterruptedException {
    wait(0);
}
public final native void wait(long timeout) throws InterruptedException;
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);
}

  其中有3个方法是 native 的,也就是由虚拟机本地的c代码执行的。有2个 wait 重载方法最终还是调用了 wait(long) 方法。

  首先还是 know how。来一个最简单的例子,看看如何使用这两个方法。

public class WaitNotify {
    private final static Object lock = new Object();
    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println("线程 A 等待拿锁");
            synchronized (lock) {
                try {
                    System.out.println("线程 A 拿到锁了");
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println("线程 A 开始等待并放弃锁");
                    lock.wait();
                    System.out.println("线程A 被通知可以继续执行 则 继续运行至结束");
                } catch (InterruptedException ignored) {
                }
            }
        }, "线程 A").start();
        new Thread(() -> {
            System.out.println("线程 B 等待锁");
            synchronized (lock) {
                System.out.println("线程 B 拿到锁了");
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException ignored) {
                }
                lock.notify();
                System.out.println("线程 B 随机通知 Lock 对象的某个线程");
            }
        }, "线程 B").start();
    }
}

  在上面的代码中,线程 A 和 B 都会抢这个 lock 对象的锁,A 的运气比较好(也可能使 B 拿到锁),他先拿到了锁,然后调用了 wait 方法,放弃了锁,并挂起了自己,这个时候等待锁的 B 就拿到了锁,然后通知了A,但是请注意,通知完毕之后,B 线程并没有执行完同步代码块中的代码,因此,A 还是拿不到锁的,因此无法运行,等到B线程执行完毕,出了同步块,这个时候 A 线程才被激活得以继续执行。

  使用 wait 方法和 notify 方法可以使 2 个无关的线程进行通信。也就是面试题中常提到的线程之间如何通信

  如果没有 wait 方法和 noitfy 方法,我们如何让两个线程通信呢?简单的办法就是让某个线程循环去检查某个标记变量,比如:

  while (value != flag) {
    Thread.sleep(1000);
  }
  doSomeing();

  上面的这段代码在条件不满足使就睡眠一段时间,这样做到目的是防止过快的”无效尝试“,这种方式看似能够实现所需的功能,但是却存在如下问题:

  1. 难以确保及时性。因为等待的1000时间会导致时间差。
  2. 难以降低开销,如果确保了及时性,休眠时间缩短,将大大消耗CPU。

  但是有了Java 自带的 wait 方法 和 notify 方法,一切迎刃而解。官方说法是等待/通知机制。一个线程在等待,另一个线程可以通知这个线程,实现了线程之间的通信。

二  为什么必须在同步块中

  注意,wait\notify方法的使用必须是在 synchroized 同步块中,并且在当前对象的同步块中,如果在 A 对象的方法中调用 B 对象的 wait 或者 notify 方法,虚拟机会抛出 IllegalMonitorStateException,非法的监视器异常,因为你这个线程持有的监视器和你调用的监视器的不是一个对象。

  那么为什么这两个方法一定要在同步块中呢?

  这里要说一个专业名词:竞态条件。什么是竞态条件呢?

当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。

  竞态条件会导致程序在并发情况下出现一些bugs。多线程对一些资源的竞争的时候就会产生竞态条件,如果首先要执行的程序竞争失败排到后面执行了,那么整个程序就会出现一些不确定的bugs。

  这种bugs很难发现而且会重复出现,这是因为线程间会随机竞争。

  假设有2个线程,分别是生产者和消费者,他们有各自的任务。

    1.1生产者检查条件(如缓存满了)-> 1.2生产者必须等待

    2.1消费者消费了一个单位的缓存 -> 2.2重新设置了条件(如缓存没满) -> 2.3调用notifyAll()唤醒生产者

  我们希望的顺序是: 1.1->1.2->2.1->2.2->2.3

  但是由于CPU执行是随机的,可能会导致 2.3 先执行,1.2 后执行,这样就会导致生产者永远也醒不过来了!

  所以我们必须对流程进行管理,也就是同步,通过在同步块中并结合 wait 和 notify 方法,我们可以手动对线程的执行顺序进行调整。

三  使用wait\notify实现简单的生产消费模型

1  定义资源

/**
 * 资源
 */
public class Resource {

    /*资源序号*/
    private int number = 0;
    /*资源标记*/
    private boolean flag = false;

    /**
     * 生产资源
     */
    public synchronized void create() {
        if (flag) {//先判断标记是否已经生产了,如果已经生产,等待消费;
            try {
                wait();//让生产线程等待
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        number++;//生产一个
        System.out.println(Thread.currentThread().getName() + "生产者------------" + number);
        flag = true;//将资源标记为已经生产
        notify();//唤醒在等待操作资源的线程(队列)
    }

    /**
     * 消费资源
     */
    public synchronized void destroy() {
        if (!flag) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + "消费者****" + number);
        flag = false;
        notify();
    }
}

2  生产者

/**
 * 生产者
 */
public class Producer implements Runnable {

    private Resource resource;

    public Producer(Resource resource) {
        this.resource = resource;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            resource.create();
        }

    }
}

3  消费者

/**
 * 消费者*/
public class Consumer implements Runnable {

    private Resource resource;
    public Consumer(Resource resource) {
        this.resource = resource;
    }
    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            resource.destroy();
        }
    }
}

  基本上经典的生产者消费者模式的有着如下规则:

  等待方遵循如下规则:

    1. 获取对象的锁。

    2. 如果条件不满足,那么调用对象的 wait 方法,被通知后仍要检查条件。

    3. 条件满足则执行相应的逻辑。

  对应的伪代码入下:

synchroize( 对象 ){
    while(条件不满足){
      对象.wait();
    }
    对应的处理逻辑......
}

  通知方遵循如下规则:

    1. 获得对象的锁。

    2. 改变条件。

    3. 通知所有等待在对象上的线程。

对应的伪代码如下:

synchronized(对象){
  改变条件
  对象.notifyAll();
}

四  底层原理

  知道了如何使用,就得知道他的原理到底是什么?

  首先我们看,使用这两个方法的顺序一般是什么?

    1. 使用 wait ,notify 和 notifyAll 时需要先对调用对象加锁。

    2. 调用 wait 方法后,线程状态有 Running 变为 Waiting,并将当前线程放置到对象的 等待池

    3. notify 或者 notifyAll 方法调用后, 等待线程依旧不会从 wait 返回,需要调用 noitfy 的线程释放锁之后,等待线程才有机会从 wait 返回。

    4. notify 方法将等待队列的一个等待线程从等待队列种移到锁池中,而 notifyAll 方法则是将等待池种所有的线程全部移到锁池中,被移动的线程状态由 Waiting 变为 Blocked。

    5. 从 wait 方法返回的前提是线程从等待池进入锁池,然后获得了调用对象的锁。

  从上述细节可以看到,等待/通知机制依托于同步机制,其目的就是确保等待线程从 wait 方法返回后能够感知到通知线程对变量做出的修改。

该图描述了上面的步骤:

  

 

参考:

  https://www.cnblogs.com/aiqiqi/p/10982028.html#_label3_1

 

posted on 2019-10-13 20:33  Vagrant。  阅读(1044)  评论(0编辑  收藏  举报