wait和notify

什么是wait和notify

在java中我们可以使用「synchronized」解决线程同步的问题,通过该关键字能保证多线程并发情况下的线程安全。

package com.buydeem.share;

public class WaitNotifyDemo1 {
    static final Object LOCK = new Object();
    static Integer number = 0;

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            for (int i = 0; i < 1000; i++) {
                synchronized (LOCK){
                    number++;
                    System.out.printf("线程:[%s]打印number:[%d]%n",Thread.currentThread().getName(),number);
                }
            }
        };

        Thread t1 = new Thread(runnable);
        t1.setName("t1");

        Thread t2 = new Thread(runnable);
        t2.setName("t2");

        t1.start();
        t2.start();

        t1.join();
        t2.join();
    }
}

例如上述的示例代码,我们通过synchronized关键字来使线程同步,最后的打印结果就是我们2000。(如果上述代码还不是很懂的话,建议不要浪费时间阅读本文,可以先去了解了synchronized之后再来看。关注公众号查看「多线程」专题。)

存在的问题

上面的示例中线程安全的问题解决了,但是还存在一个问题没有解决,那就是线程之间的协调问题。那什么是协调问题呢?假如说现在我增加一个需求,这个需求是不仅要保证线程安全,同时还需要让两个线程交替的打印出结果。想要完成这个任务,就必须要让线程之间通信,然后协调工作,这个就是前面协调的定义。

解决办法

针对上面的问题,我们可以使用wait和notify来解决。这里我先给出实现代码,后面我们再仔细分析。

public class WaitNotifyDemo2 {
    static final Object LOCK = new Object();
    static Integer number = 0;

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            for (int i = 0; i < 1000; i++) {
                synchronized (LOCK){
                    LOCK.notify();
                    number++;
                    System.out.printf("线程:[%s]打印number:[%d]%n",Thread.currentThread().getName(),number);
                    try {
                        LOCK.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };

        Thread t1 = new Thread(runnable);
        t1.setName("t1");

        Thread t2 = new Thread(runnable);
        t2.setName("t2");

        t1.start();
        t2.start();

        t1.join();
        t2.join();
    }
}

修改代码如上所示,然后运行代码整个打印结果如下:

线程:[t2]打印number:[1]
线程:[t1]打印number:[2]
线程:[t2]打印number:[3]
线程:[t1]打印number:[4]
……省略部分内容
线程:[t2]打印number:[1997]
线程:[t1]打印number:[1998]
线程:[t2]打印number:[1999]
线程:[t1]打印number:[2000]

其实实现代码并没大改,我们只是在增加了notify和wait两个方法而已,那下面我们就来细说这两个方法。

wait和notify

这两个方法被定义在Object中,从这一点我们可以看出,只要是java中的对象都会有这两个方法,因为Object类是所有类的父类。

wait

调用该方法,会导致调用该方法的线程进入等待。但是调用该方法前必须要获取到该对象的对象锁,否则将抛出「IllegalMonitorStateException」。同时还提供了两个支持指定等待时间的方法,可以指定等待时间。

notify

唤醒在此对象锁上等待的单个线程。同样该方法也需要先获取到对象锁,否则也将抛出「IllegalMonitorStateException」异常。同时还提供了一个唤醒在此对象锁上等待的所有线程notifyAll()。

为什么要先获取到对象锁

在对上面wait和notify介绍的过程中,我们强调了一点那就是必须要先获取到对象锁。这句话是什么意思呢?先看下面的例子:

public class WaitNotifyDemo3 {
    static final Object LOCK = new Object();

    public static void main(String[] args) throws InterruptedException {
        LOCK.notify();
    }
}

直接运行代码抛出下面的错误:

Exception in thread “main” java.lang.IllegalMonitorStateException at java.lang.Object.notify(Native Method) at com.buydeem.share.WaitNotifyDemo3.main(WaitNotifyDemo3.java:14)

而我们所说的需要先获取对象锁指的是我们调用wait或者notify方法时,必须是在同步代码块中(即synchronized代码块),同时还必须是同一个对象。也就是说你同步对象是a(synchronized(a)),那么你调用的就应该是「a.wait或者a.notify」

public class WaitNotifyDemo3 {
    static final Object LOCK = new Object();

    public static void main(String[] args) throws InterruptedException {
        synchronized (LOCK){
            LOCK.notify();
        }
    }
}

关于为什么这两个方法都要在同步代码块中调用,其实你可以这样想,线程调用对象的wait方法后,线程会立马释放持有的对象锁,而调用对象的notify同样该线程在同步代码块执行完成之后也要释放对象锁。这两个方法都需要释放对象锁,如果没有本身没有持有,那么哪来的释放呢?

wait和notify释放对象锁的时机

在上面我们讲过wait和notify的释放对象锁的时机不一样,但是没有细说。这里我们使用示例代码来验证我们上面说的。首先我们确定结论:

  • wait在对象调用完该方法之后释放对象锁
  • notify在该方法调用完之后并不会立马释放锁,而是需要退出同步代码块才会释放对象锁(有点废话,退出同步代码块当然就释放锁了)。
public class WaitNotifyDemo4 {
    static final Object LOCK = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (LOCK){
                    System.out.println("线程t1进入同步代码块获取到锁");
                    try {
                        LOCK.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("线程t1 wait后继续执行");
                }
            }
        });
        t1.start();

        Thread.sleep(1000);

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (LOCK){
                    System.out.println("线程t2进入同步代码块");
                    System.out.println("线程t1的状态:"+t1.getState());
                    LOCK.notify();
                    System.out.println("线程t2已执行notify");
                    System.out.println("线程t1的状态:"+t1.getState());
                }
            }
        });
        t2.start();

        t1.join();
        t2.join();
    }
}

上面的代码很简单,直接看运行结果:

线程t1进入同步代码块获取到锁
线程t2进入同步代码块
线程t1的状态:WAITING
线程t2已执行notify
线程t1的状态:BLOCKED
线程t1 wait后继续执行

首先是线程t1先执行,进入同步代码块后调用wait释放了对象锁。接着就是线程t2开始执行,t2之所以能进入同步代码块是因为线程t1调用了wait释放了锁它才能拿到锁。接着t2打印出线程t1的状态为「WAITING」。接着线程t2调用了notify,然后打印出线程t1的状态为「BLOCKED」。此处说明了notify确实唤醒了线程t1(线程t1状态变化了),同时说明了线程t2并没有释放锁(线程t1的状态是BLOCKED,再次强调线程状态必须要了解清楚)。

死锁问题

关于什么是死锁这里就不说了,在使用wait和notify时我们需要牢记一点,调用wait只会释放当前对象上的锁,而不会把所有持有的锁都释放掉。如果不注意这一点,很容易造成死锁的发生。废话不多说,直接上代码。

public class WaitNotifyDemo5 {
    static final Object LOCK1 = new Object();
    static final Object LOCK2 = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            synchronized (LOCK1) {
                synchronized (LOCK2) {
                    try {
                        System.out.println("线程t1调用wait");
                        LOCK1.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        t1.setName("t1");
        t1.start();

        Thread.sleep(1000);

        Thread t2 = new Thread(() ->{
            synchronized (LOCK1){
                synchronized (LOCK2){
                    System.out.println("线程t2执行");
                }
            }
        });
        t2.setName("t2");
        t2.start();

        t1.join();
        t2.join();
    }
}

上述代码中,线程t1先获取到LOCK1对象锁,接着再获取LOCK2的对象锁,最后调用LOCK1对象的wait方法。这个wait释放的是LOCK1上的对象锁,而线程t1持有的LOCK2对象锁并没有被释放。这就是前面我们说的wait只会释放该对象持有的锁而不是线程持有的所有锁。因为这一点直接导致了线程t2无法获取到LOCK1的对象锁,程序死锁了。我们可以通过jstack查看线程状态。

Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.281-b09 mixed mode):

"t2" #14 prio=5 os_prio=0 tid=0x000001f320a57800 nid=0x24b4 waiting for monitor entry [0x000000c6553fe000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.buydeem.share.WaitNotifyDemo5.lambda$main$1(WaitNotifyDemo5.java:35)
        - waiting to lock <0x00000000d6088aa8> (a java.lang.Object)
        - locked <0x00000000d6088a98> (a java.lang.Object)
        at com.buydeem.share.WaitNotifyDemo5$$Lambda$2/400136488.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

"t1" #13 prio=5 os_prio=0 tid=0x000001f320a57000 nid=0x3490 in Object.wait() [0x000000c6552ff000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x00000000d6088a98> (a java.lang.Object)
        at java.lang.Object.wait(Object.java:502)
        at com.buydeem.share.WaitNotifyDemo5.lambda$main$0(WaitNotifyDemo5.java:20)
        - locked <0x00000000d6088aa8> (a java.lang.Object)
        - locked <0x00000000d6088a98> (a java.lang.Object)
        at com.buydeem.share.WaitNotifyDemo5$$Lambda$1/1879492184.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

从jstack打印的结果也印证了这一点。

经典模型-生产者和消费者

关于wait和notify最经典的问题就是生产者和消费者问题了。简单的说就是生产者负责一直产生数据,而消费者负责处理生产者的产出。

public class WaitNotifyDemo6 {
    public static void main(String[] args) throws InterruptedException {
        Store store = new Store(100);

        Thread product = new Thread(() -> {
            while (true) {
                try {
                    store.add();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread customer = new Thread(() -> {
            while (true) {
                try {
                    store.remove();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        product.start();
        customer.start();

        product.join();
        product.join();

    }
}

class Store {
    /**
     * 仓库的最大容量
     */

    private int maxCount;
    /**
     * 当前仓库的数量
     */

    private int currentCount;

    /**
     * 创建一个仓库
     *
     * @param maxCount     最大仓库容量
     * @param currentCount 初始化仓库数量
     */

    public Store(int maxCount, int currentCount) {
        this.maxCount = maxCount;
        this.currentCount = currentCount;
    }

    /**
     * 存储产品
     */

    public synchronized void add() throws InterruptedException {
        if (currentCount >= maxCount) {
            wait();
        }
        System.out.printf("生产者:当前库存:[%d],增加后的库存为:[%d]%n", currentCount, ++currentCount);
        notify();
    }

    /**
     * 消费产品
     */

    public synchronized void remove() throws InterruptedException {
        if (currentCount <= 0) {
            wait();
        }
        System.out.printf("消费者:当前库存:[%d],减掉的库存为:[%d]%n", currentCount, --currentCount);
        notify();
    }
}

上面的代码就是一个简单的生产者和消费者模型。代码比较简单,「Store」提供入库和出库的操作,然后生产者线程和消费者线程同时对仓库进行入库和出库操作。在一个生产者和一个消费者的情况下,并没有什么问题。

多生产者和多消费者问题

上面我们只是一个生产者和一个消费者的情况,现在我们改用成多个生产者和消费者的情况,核心代码没有做改变,只是增加了一个消费者和生产者。

public class WaitNotifyDemo7 {
    public static void main(String[] args) throws InterruptedException {
        Store2 store = new Store2(100);

        Thread p1 = new Thread(() -> {
            while (true) {
                try {
                    store.add();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        p1.setName("p1");

        Thread p2 = new Thread(() -> {
            while (true) {
                try {
                    store.add();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        p2.setName("p2");

        Thread c1 = new Thread(() -> {
            while (true) {
                try {
                    store.remove();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        c1.setName("c1");

        Thread c2 = new Thread(() -> {
            while (true) {
                try {
                    store.remove();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        c2.setName("c2");

        p1.start();
        p2.start();
        c1.start();
        c2.start();

        p1.join();

    }
}

class Store2 {
    /**
     * 仓库的最大容量
     */

    private int maxCount;
    /**
     * 当前仓库的数量
     */

    private int currentCount;

    /**
     * 创建一个仓库
     *
     * @param maxCount     最大仓库容量
     * @param currentCount 初始化仓库数量
     */

    public Store2(int maxCount, int currentCount) {
        this.maxCount = maxCount;
        this.currentCount = currentCount;
    }

    /**
     * 存储产品
     */

    public synchronized void add() throws InterruptedException {
        if (currentCount >= maxCount) {
            wait();
        }
        System.out.printf("生产者[%s]:当前库存:[%d],增加后的库存为:[%d]%n", Thread.currentThread().getName(), currentCount, ++currentCount);
        notify();
    }

    /**
     * 消费产品
     */

    public synchronized void remove() throws InterruptedException {
        if (currentCount <= 0) {
            wait();
        }
        System.out.printf("消费者[%s]:当前库存:[%d],减掉的库存为:[%d]%n", Thread.currentThread().getName(), currentCount, --currentCount);
        notify();
    }
}

再次运行代码会发现,会出现过度消费和过度产出的问题。这是为什么呢?

修改if为while

核心的入库和出库代码如下:

    /**
     * 存储产品
     */

    public synchronized void add() throws InterruptedException {
        if (currentCount >= maxCount) {
            wait();
        }
        System.out.printf("生产者[%s]:当前库存:[%d],增加后的库存为:[%d]%n", Thread.currentThread().getName(), currentCount, ++currentCount);
        notify();
    }

    /**
     * 消费产品
     */

    public synchronized void remove() throws InterruptedException {
        if (currentCount <= 0) {
            wait();
        }
        System.out.printf("消费者[%s]:当前库存:[%d],减掉的库存为:[%d]%n", Thread.currentThread().getName(), currentCount, --currentCount);
        notify();
    }

假设现在有一个生产者t1生产时(即调用了add方法),进行条件判断「currentCount >= maxCount」的值为「true」(即仓库满了),这时候线程t1将进入WAITING状态。如果此时线程t1被其他线程唤醒,线程t1则会执行后续的增加库存操作,而不会再去判断「currentCount >= maxCount」条件是否真的满足。此时和可能库存还是到了最大库存,这时候还往仓库添加产品,库存就超过了最大的库存。
所以对于该情况下的条件判断使用while替代if是一个更好的选择。我们修改入库和出库代码如下:

    /**
     * 存储产品
     */

    public synchronized void add() throws InterruptedException {
//        if (currentCount >= maxCount) {
//            wait();
//        }
        while (currentCount >= maxCount){
            wait();
        }
        System.out.printf("生产者[%s]:当前库存:[%d],增加后的库存为:[%d]%n", Thread.currentThread().getName(), currentCount, ++currentCount);
        notify();
    }

    /**
     * 消费产品
     */

    public synchronized void remove() throws InterruptedException {
//        if (currentCount <= 0) {
//            wait();
//        }
        while (currentCount <= 0){
            wait();
        }
        System.out.printf("消费者[%s]:当前库存:[%d],减掉的库存为:[%d]%n", Thread.currentThread().getName(), currentCount, --currentCount);
        notify();
    }

我们使用while替代了if,再次运行代码并未出现库存超出或者库存为负的情况。

notifyAll的妙用

上面的代码好像是没有什么问题了,现在我修改最大仓库数量。假设我的仓库最大数量为1,我们再次运行代码。最后的结果是,程序正常运行了一小会儿之后,然后控制台不再打印任何信息。使用jstack工具查看应用的状态如下:

Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.281-b09 mixed mode):

"c2" #16 prio=5 os_prio=0 tid=0x000001f6cd2d5000 nid=0x2bec in Object.wait() [0x00000086188fe000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x00000000d608bc58> (a com.buydeem.share.Store2)
        at java.lang.Object.wait(Object.java:502)
        at com.buydeem.share.Store2.remove(WaitNotifyDemo7.java:111)
        - locked <0x00000000d608bc58> (a com.buydeem.share.Store2)
        at com.buydeem.share.WaitNotifyDemo7.lambda$main$3(WaitNotifyDemo7.java:50)
        at com.buydeem.share.WaitNotifyDemo7$$Lambda$4/400136488.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

"c1" #15 prio=5 os_prio=0 tid=0x000001f6cd2ce000 nid=0x1d20 in Object.wait() [0x00000086187ff000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x00000000d608bc58> (a com.buydeem.share.Store2)
        at java.lang.Object.wait(Object.java:502)
        at com.buydeem.share.Store2.remove(WaitNotifyDemo7.java:111)
        - locked <0x00000000d608bc58> (a com.buydeem.share.Store2)
        at com.buydeem.share.WaitNotifyDemo7.lambda$main$2(WaitNotifyDemo7.java:39)
        at com.buydeem.share.WaitNotifyDemo7$$Lambda$3/984213526.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

"p2" #14 prio=5 os_prio=0 tid=0x000001f6cd2cd000 nid=0xfc4 in Object.wait() [0x00000086186fe000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x00000000d608bc58> (a com.buydeem.share.Store2)
        at java.lang.Object.wait(Object.java:502)
        at com.buydeem.share.Store2.add(WaitNotifyDemo7.java:97)
        - locked <0x00000000d608bc58> (a com.buydeem.share.Store2)
        at com.buydeem.share.WaitNotifyDemo7.lambda$main$1(WaitNotifyDemo7.java:28)
        at com.buydeem.share.WaitNotifyDemo7$$Lambda$2/2094777811.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

"p1" #13 prio=5 os_prio=0 tid=0x000001f6cd2cc800 nid=0x3228 in Object.wait() [0x00000086185ff000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x00000000d608bc58> (a com.buydeem.share.Store2)
        at java.lang.Object.wait(Object.java:502)
        at com.buydeem.share.Store2.add(WaitNotifyDemo7.java:97)
        - locked <0x00000000d608bc58> (a com.buydeem.share.Store2)
        at com.buydeem.share.WaitNotifyDemo7.lambda$main$0(WaitNotifyDemo7.java:17)
        at com.buydeem.share.WaitNotifyDemo7$$Lambda$1/1879492184.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

从结果可以看出,所有的线程都处于wait状态。这是为什么呢?现在想象这么一个场景

  • 线程c1获取到锁
  • 线程c2,p1,p2都处于wait状态
  • 当前库存是0

现在线程c1判断库存为0,那么线程c1入库完成之后(库存改为1)调用notify。notify唤醒的不是消费者而是生产者c2,生产者c2判断「currentCount >= maxCount」调用wait。至此所有线程处于waiting状态,生产者不能生产产品,消费者也无法消费产品。
通过上面的分析,我们知道了问题的核心在于「notify」是不能唤醒指定的线程的。这将导致一个问题就是当需要唤醒消费者消费时,我们和可能唤醒的不是消费者。这时候我们可以使用「notifyAll」来唤醒所有等待在同一个对象锁上的所有线程,这样不符合条件的线程再次调用wait进入waiting状态,而符合执行条件的线程则能正常执行。

    /**
     * 存储产品
     */

    public synchronized void add() throws InterruptedException {
//        if (currentCount >= maxCount) {
//            wait();
//        }
        while (currentCount >= maxCount){
            wait();
        }
        System.out.printf("生产者[%s]:当前库存:[%d],增加后的库存为:[%d]%n", Thread.currentThread().getName(), currentCount, ++currentCount);
        //notify();
        notifyAll();
    }

    /**
     * 消费产品
     */

    public synchronized void remove() throws InterruptedException {
//        if (currentCount <= 0) {
//            wait();
//        }
        while (currentCount <= 0){
            wait();
        }
        System.out.printf("消费者[%s]:当前库存:[%d],减掉的库存为:[%d]%n", Thread.currentThread().getName(), currentCount, --currentCount);
//        notify();
        notifyAll();
    }
}

小结

通过上文我们知道了解了wait和notify的作用,它主要解决了synchronized同步代码块之间线程的协调问题。
同样它也不是那么完美,它并不是适用于Lock。例如如果我们使用ReentrantLock时,我们并不能通过wait和notify来实现线程间的协调。同样notify和notifyAll存在无法唤醒指定线程的问题。对于Lock情况下如何解决此类问题,关注公众号,后续我会更新到这部分内容。
同时我们还在文章了说了经典的生产者和消费者模型,以及实现过程中存在的问题,还有问题分析以及解决。

  • 工程代码示例: https://gitee.com/zengchao_workspace/wait_notify


原文始发于微信公众号(一只菜鸟程序员):wait和notify

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/73046.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!