Java中的wait()与notify()以及线程挂起

在多线程中,synchronized关键字使用的较多,对于wait()和notify()两个方法使用的较少,因为业务中很少遇见对线程进行精细控制的需求。
最近在网上查询相关用法时,发现这两个方法使用不当会造成线程挂起,一开始我也不清楚什么叫做线程挂起,通过实验得知就是一段时间内所有的线程都进入等待状态,这些线程都不继续运转了。


两个方法的作用:

wait():通当前线程释放锁

notify():通知其它线程获取锁,至于哪个线程会获取锁是随机分配的


这里通过生产者消费者的案例来说明这个问题,使用的代码也比较简洁。

  • 新建一个仓库
public class Repository {

    private ArrayList<Object> buf = new ArrayList<>();
    public final static int MAX_SIZE = 1;

    public synchronized void put(Object o) throws InterruptedException {
        while (buf.size() == MAX_SIZE){
            wait();
        }
        System.out.println("生产:" + o);
        buf.add(o);
        notify();
    }

    public synchronized Object get() throws InterruptedException {
        while (buf.size() == 0){
            wait();
            Thread.sleep(1000);
        }
        Object o = buf.remove(0);
        System.out.println("消费:" + o);
        notify();
        return o;
    }

}
  • 新建生产者
public static class MyProductRunnable implements Runnable {

        private Repository repository;

        public MyProductRunnable(Repository repository) {
            this.repository = repository;
        }

        @Override
        public void run() {
            try {
                while (true) {

                    repository.put(new Date().toString());
                }

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
  • 新建消费者
public static class MyConsumerRunnable implements Runnable {

        private Repository repository;

        public MyConsumerRunnable(Repository repository) {
            this.repository = repository;
        }

        @Override
        public void run() {
            try {
                while (true) {

                    Object o = repository.get();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

上述三个类将框架搭建完成,下面来模拟运行:

public static void main(String[] args) {
        Repository repository = new Repository();

        new Thread(new MyProductRunnable(repository)).start();

        for (int i = 0; i < 1000; i++) {

            new Thread(new MyConsumerRunnable(repository)).start();
        }

    }

这里为了演示,我们将仓库的容量设置为一,并且生产者与消费者的比例为1:1000,这样我们就可以比较直观的看到线程挂起的效果。
可以看到我在Repository中的put和get方法中的while循环都加入了休眠的代码,这样做的原因是将现象进行放大,类似以前物理实验中的一种思想。

运行代码后的效果如下所示:

程序运行一段时间之后就停止了,这种现象产生的原因是线程都进入了等待状态。
我们这里只有一个生产者,1000个消费者,因为仓库的容量被我们设置为了一,所以生产者很容易进入等待状态(通过调用wait()),消费者同样如此,消费者的个数有1000个,也就说大部分消费者也是出于等待状态。
但只要消费者消费了仓库中的物品它就会释放仓库的锁(通过调用notify()),其它线程就有机会得到这把锁继续运行,但我们知道线程的时间片是随机分布的,就算仓库里没有货物了CPU也不一定会将时间片分配给生产者。
就算按概率来算消费者被分配到时间片的概率为1/1000,也就是说大部分时间片都是被分配给消费者线程的,这也是我将消费者线程个数设置得很大的原因,仓库的货物数量很少,消费者拿到时间片也只能继续进入等待状态。
消费者拿到时间片的概率是很高的,所以上述现象发生的概率也很高,这就造成了在一段时间内所有的线程都挂起了,程序没有运作起来。

上面还有一个比较特别的点是我将休眠的时间设置的比较长,而且还设置在get方法中的wait方法之后。
第一是因为生产者中的while循环满足条件的概率很高,其二是生产者获取到时间片之后都是继续执行wait方法之后的代码
这样就可以加大每次获取时间片的开销(如果get方法放在wait方法之前,线程挂起的更快,因为每次释放锁之前还得等待一段时间,而放在wait方法之后则很快就释放了锁,相同时间内越快释放锁其它线程拿到时间片的概率越高)。
如果设置的时间过短,消费者拿到锁之后快速释放,相同时间内生产者拿到锁的次数还是会很多,线程就不会挂起了。

在 “Java中的wait()与notify()以及线程挂起” 上有 2 条评论

  1. 老师您好,我是图书策划编辑,杨孟然,看到您的文章想要和你约下稿,关于开发实战,方便的话可以加下我的微信:yzbook05,或者留下您的微信具体沟通下,如有打扰,还请谅解

发表评论

电子邮件地址不会被公开。 必填项已用*标注