Java多线程(3) wait、notify 详解

上一篇 :Java多线程(2)synchronized详解

wait、notify / notifyAll

一. 对于wait()和notify()、notifyAll的理解

对于wait()和notify()的理解,还是要从jdk官方文档中开始,在Object类方法中有

  • void notify()
    Wakes up a single thread that is waiting on this object’s monitor.
    译:唤醒在此对象监视器上等待的单个线程

  • void notifyAll()
    Wakes up all threads that are waiting on this object’s monitor.
    译:唤醒在此对象监视器上等待的所有线程

  • void wait( )
    Causes the current thread to wait until another thread invokes the notify() method or the notifyAll( ) method for this object.
    译:导致当前的线程等待,直到其他线程调用此对象的notify( ) 方法或 notifyAll( ) 方法

  • void wait(long timeout)
    Causes the current thread to wait until either another thread invokes the notify( ) method or the notifyAll( ) method for this object, or a specified amount of time has elapsed.
    译:导致当前的线程等待,直到其他线程调用此对象的notify() 方法或 notifyAll() 方法,或者指定的时间过完。

  • void wait(long timeout, int nanos)
    Causes the current thread to wait until another thread invokes the notify( ) method or the notifyAll( ) method for this object, or some other thread interrupts the current thread, or a certain amount of real time has elapsed.
    译:导致当前的线程等待,直到其他线程调用此对象的notify( ) 方法或 notifyAll( ) 方法,或者其他线程打断了当前线程,或者指定的时间过完。

上面是官方文档的简介,下面我们根据官方文档总结一下:

  • wait()、notify()、notifyAll()是三个定义在Object类里的本地final方法,可以用来控制线程的状态,无法被重写

  • 如果对象调用了wait方法就会使持有该对象的线程把该对象的控制权交出去,然后处于等待状态

  • wait()使当前线程阻塞,前提是 必须先获得锁,一般配合synchronized 关键字使用,即,一般在synchronized 同步代码块里使用 wait()、notify/notifyAll() 方法。

  • 由于 wait()、notify/notifyAll() 在synchronized 代码块执行,说明当前线程一定是获取了锁的

  • notify( )方法只会通知等待队列中的第一个相关线程(不会通知优先级比较高的线程)

  • notifyAll( )通知所有等待该竞争资源的线程(也不会按照线程的优先级来执行)

  • 在调用wait的时候,线程自动释放其占有的对象锁,同时不会去申请对象锁。当线程被唤醒的时候,它才再次获得了去获得对象锁的权利
    假设有三个线程执行了obj.wait( ),那么obj.notifyAll( )则能全部唤醒三个线程,但是要继续执行obj.wait()的下一条语句,必须获得obj锁,因此只有一个线程有机会获得锁继续执行,例如thread1,其余的需要等待thread1释放obj锁之后才能继续执行

  • 当调用obj.notify/notifyAll后,调用线程依旧持有obj锁,因此,三个线程虽被唤醒,但是仍无法获得obj锁。直到调用线程退出synchronized块,释放obj锁后,三个线程中的一个才有机会获得锁继续执行

二. wait和notify代码应用

public class WaitNotifyTest {
    // 在多线程间共享的对象上使用wait
    private String[] shareObj = { "true" };
    public static void main(String[] args) {
        WaitNotifyTest test = new WaitNotifyTest();
        ThreadWait threadWait1 = test.new ThreadWait("wait thread1");
        threadWait1.setPriority(2);
        ThreadWait threadWait2 = test.new ThreadWait("wait thread2");
        threadWait2.setPriority(3);
        ThreadWait threadWait3 = test.new ThreadWait("wait thread3");
        threadWait3.setPriority(4);
        ThreadNotify threadNotify = test.new ThreadNotify("notify thread");
        threadNotify.start();
        threadWait1.start();
        threadWait2.start();
        threadWait3.start();
    }
    class ThreadWait extends Thread {
        public ThreadWait(String name){
            super(name);
        }
        public void run() {
            synchronized (shareObj) {
                while ("true".equals(shareObj[0])) {
                    System.out.println("线程"+ this.getName() + "开始等待");
                    long startTime = System.currentTimeMillis();
                    try {
                        shareObj.wait(); // 此时这个线程的代码运行在这里休眠了(jvm通过程序计数器来实现标记到代码执行到这里),等待唤醒后 再接着往下执行
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    long endTime = System.currentTimeMillis();
                    System.out.println("线程" + this.getName()
                            + "等待时间为:" + (endTime - startTime));
                }
            }
            System.out.println("线程" + getName() + "等待结束");
        }
    }
    class ThreadNotify extends Thread {
        public ThreadNotify(String name){
            super(name);
        }
        public void run() {
            try {
                // 给等待线程等待时间
                sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (shareObj) {
                System.out.println("线程" + this.getName() + "开始准备通知");
                shareObj[0] = "false";
                // 唤醒全部线程
                shareObj.notifyAll();
                System.out.println("线程" + this.getName() + "通知结束");
            }
            System.out.println("线程" + this.getName() + "运行结束");
        }
    }
}

三. 生产者消费者模型

在这里插入图片描述

生产者消费者模型是我们学习多线程知识的一个经典案例,一个典型的生产者消费者模型如下:
上面代码很容易引申出来两个问题:1.ThreadWait 类 wait()方法外面为什么是while循环而不是if判断,2. ThreadNotify类结尾处的为什么要用notifyAll()方法,用notify()行吗

很多人在回答第二个问题的时候会想当然的说notify()是唤醒一个线程,notifyAll()是唤醒全部线程,但是唤醒然后呢,不管是notify()还是notifyAll(),最终拿到锁的只会有一个线程,那它们到底有什么区别呢?

其实这是一个对象内部锁的调度问题,要回答这两个问题,首先我们要明白java中对象锁的模型,JVM会为一个使用内部锁(synchronized)的对象维护两个集合,Entry Set和Wait Set,也有人翻译为锁池和等待池,意思基本一致。

对于Entry Set:如果线程A已经持有了对象锁,此时如果有其他线程也想获得该对象锁的话,它只能进入Entry Set,并且处于线程的BLOCKED状态。

对于Wait Set:如果线程A调用了wait()方法,那么线程A会释放该对象的锁,进入到Wait Set,并且处于线程的WAITING状态

还有需要注意的是,某个线程B想要获得对象锁,一般情况下有两个先决条件,一是对象锁已经被释放了(如曾经持有锁的前任线程A执行完了synchronized代码块或者调用了wait()方法等等),二是线程B已处于RUNNABLE状态。

那么这两类集合中的线程都是在什么条件下可以转变为RUNNABLE呢?

  • 对于Entry Set中的线程,当对象锁被释放的时候,JVM会唤醒处于Entry Set中的某一个线程,这个线程的状态就从BLOCKED转变为RUNNABLE
  • 对于Wait Set中的线程,当对象的notify()方法被调用时,JVM会唤醒处于Wait Set中的某一个线程,这个线程的状态就从WAITING转变为RUNNABLE;或者当notifyAll()方法被调用时,Wait Set中的全部线程会转变为RUNNABLE状态。所有Wait Set中被唤醒的线程会被转移到Entry Set

然后,每当对象的锁被释放后,那些所有处于RUNNABLE状态的线程会共同去竞争获取对象的锁,最终会有一个线程(具体哪一个取决于JVM实现,队列里的第一个?随机的一个?)真正获取到对象的锁,而其他竞争失败的线程继续在Entry Set中等待下一次机会。

有了这些知识点作为基础,上述的两个问题就能解释的清了

  • 首先来看第一个问题,我们在调用wait()方法的时候,心里想的肯定是因为当前方法不满足我们指定的条件,因此执行这个方法的线程需要等待直到其他线程改变了这个条件并且做出了通知。那么为什么要把wait()方法放在循环而不是if判断里呢,因为wait()的线程永远不能确定其他线程会在什么状态下notify(),所以必须在被唤醒、抢占到锁并且从wait()方法退出的时候再次进行指定条件的判断,还有一种情况就是存在虚假唤醒的场景,需要while循环判断是否继续wait()
    虛假唤醒:多个线程生产,多个线程消费,那么就极有可能出现唤醒生产者的是另一个生产者或者唤醒消费者的是另一个消费者这样的情况下用if就必然会现类似过度生产或者过度消费的情况了(比如抢票场景,票数会成为负数)。所以所有的java书籍都会建议开发者永远都要把wait()放到循环判断语句里面。

  • 然后来看第二个问题,既然notify()和notifyAll()最终的结果都是只有一个线程能拿到锁,那唤醒一个和唤醒多个有什么区别呢
    下面这个两个生产者两个消费者的场景,如果我们代码中使用了notify()而非notifyAll(),假设消费者线程1拿到了锁,判断buffer为空,那么wait(),释放锁;然后消费者2拿到了锁,同样buffer为空,wait(),也就是说此时Wait Set中有两个线程;然后生产者1拿到锁,生产,buffer满,notify()了,那么可能消费者1被唤醒了,但是此时还有另一个线程生产者2在Entry Set中盼望着锁,并且最终抢占到了锁,但因为此时buffer是满的,因此它要wait();然后消费者1拿到了锁,消费,notify();这时就有问题了,此时生产者2和消费者2都在Wait Set中,buffer为空,如果唤醒生产者2,没毛病;但如果唤醒了消费者2,因为buffer为空,它会再次wait(),这就尴尬了,万一生产者1已经退出不再生产了,没有其他线程在竞争锁了,只有生产者2和消费者2在Wait Set中互相等待,那传说中的死锁就发生了

public class Something {
    private Buffer mBuf = new Buffer();

    //生产者
    public void produce() {
        synchronized (this) {
            while (mBuf.isFull()) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            mBuf.add();
            notifyAll();
        }
    }
    //消费者
    public void consume() {
        synchronized (this) {
            while (mBuf.isEmpty()) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            mBuf.remove();
            notifyAll();
        }
    }
    //产品
    private class Buffer {
        private static final int MAX_CAPACITY = 1;
        private List innerList = new ArrayList<>(MAX_CAPACITY);

        void add() {
            if (isFull()) {
                throw new IndexOutOfBoundsException();
            } else {
                innerList.add(new Object());
            }
            System.out.println(Thread.currentThread().getName() + " add");

        }
        void remove() {
            if (isEmpty()) {
                throw new IndexOutOfBoundsException();
            } else {
                innerList.remove(MAX_CAPACITY - 1);
            }
            System.out.println(Thread.currentThread().getName() + " remove");
        }
        boolean isEmpty() {
            return innerList.isEmpty();
        }
        boolean isFull() {
            return innerList.size() == MAX_CAPACITY;
        }
    }

    public static void main(String[] args) {
        Something sth = new Something();
        // 消费者线程
        for (int i = 0; i < 2; i++) {
            new Thread(new Runnable() {
                int count = 4;
                @Override
                public void run() {
                    while (count-- > 0) {
                        sth.consume();
                    }
                }
            }, "消费者" + (i + 1)).start();
        }
        // 生产者线程
        for (int i = 0; i < 2; i++) {
            new Thread(new Runnable() {
                int count = 4;
                @Override
                public void run() {
                    while (count-- > 0) {
                        sth.produce();
                    }
                }
            },"生产者" + (i+1)).start();
        }
    }
}

正确的运行结果:

生产者1 add
消费者1 remove
生产者2 add
消费者1 remove
生产者1 add
消费者1 remove
生产者2 add
消费者2 remove
生产者1 add
消费者1 remove
生产者2 add
消费者2 remove
生产者1 add
消费者2 remove
生产者2 add
消费者2 remove

如果把while改成if,结果如下,程序可能产生运行时异常:

生产者1 add
消费者1 remove
生产者2 add
消费者1 remove
生产者2 add
消费者1 remove
生产者2 add
消费者1 remove
生产者2 add
Exception in thread "消费者2" java.lang.IndexOutOfBoundsException
	at com.haiyangblog.threadpool.test_thread.Something$Buffer.remove(Something.java:53)
	at com.haiyangblog.threadpool.test_thread.Something.consume(Something.java:33)
	at com.haiyangblog.threadpool.test_thread.Something$1.run(Something.java:76)
	at java.lang.Thread.run(Thread.java:748)
Exception in thread "生产者1" java.lang.IndexOutOfBoundsException
	at com.haiyangblog.threadpool.test_thread.Something$Buffer.add(Something.java:44)
	at com.haiyangblog.threadpool.test_thread.Something.produce(Something.java:19)
	at com.haiyangblog.threadpool.test_thread.Something$2.run(Something.java:88)
	at java.lang.Thread.run(Thread.java:748)

如果把notifyAll改为notify,结果如下,死锁,程序没有正常退出:

生产者1 add
消费者2 remove
生产者1 add
消费者1 remove

但如果你把上述例子中的notify()换成notifyAll(),这样的情况就不会再出现了,因为每次notifyAll()都会使其他等待的线程从Wait Set进入Entry Set,从而有机会获得锁

其实说了这么多,一句话解释就是之所以我们应该尽量使用notifyAll()的原因就是,notify()非常容易导致死锁。当然notifyAll并不一定都是优点,毕竟一次性将Wait Set中的线程都唤醒是一笔不菲的开销,如果你能控制你的线程调度(比如:消费者和生产者各只有一个线程),那么使用notify()也是有好处的

通常,多线程之间需要协调工作:如果条件不满足,则等待;当条件满足时,等待该条件的线程将被唤醒。在Java中,这个机制的实现依赖于wait/notify。等待机制与锁机制是密切关联的。

猜你喜欢

转载自blog.csdn.net/haiyanghan/article/details/109304638