唯爱架构设计与重构微服务架构和实践

从一个死锁分析wait,notify,notifyAll

2017-08-24  本文已影响353人  2053d13b70b7

本文通过wait(),notify(),notifyAll()模拟生产者-消费者例子,说明为什么使用notify()会发生死锁。

1. 代码示例

1.1 生产者

public class Producer implements Runnable {
    List<Integer> cache;

    public Producer(List<Integer> cache) {
        this.cache = cache;
    }

    @Override
    public void run() {
        while (true) {
            produce();
        }
    }

    private void produce() {
        synchronized (cache) {
            try {
                while (cache.size() == 1) {
                    cache.wait();
                }

                // 模拟一秒生产一条消息
                Thread.sleep(1000);
                cache.add(new Random().nextInt());

                cache.notify();
            }
            catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

1.2 消费者

public class Consumer implements Runnable {
    List<Integer> cache;

    public Consumer(List<Integer> cache) {
        this.cache = cache;
    }

    @Override
    public void run() {
        while (true) {
            consume();
        }
    }

    private void consume() {
        synchronized (cache) {
            try {
                while (cache.isEmpty()) {
                    cache.wait();
                }

                System.out.println("Consumer consumed [" + cache.remove(0) + "]");
                cache.notify();
            }
            catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

1.3 测试代码

1.3.1 一个生产者一个消费者
public class WaitNotifyTest {
    public static void main(String[] args) throws Exception {
        List<Integer> cache = Lists.newArrayList();
        new Thread(new Consumer(cache)).start();
        new Thread(new Producer(cache)).start();
    }
}
运行结果:
Consumer consumed [169154454]
Consumer consumed [511734]
Consumer consumed [-25784306]
Consumer consumed [1648046130]
……

从控制台可以看出,每隔一秒钟消费一条数据,完美的生产者消费者模型!

1.3.2 多个消费者多个生产者

既然生产者和消费者都继承了Runnable,就应该多线程运行嘛~~

public class WaitNotifyTest {
    public static void main(String[] args) throws Exception {
        List<Integer> cache = Lists.newArrayList();
        new Thread(new Consumer(cache)).start();
        new Thread(new Consumer(cache)).start();
        new Thread(new Consumer(cache)).start();

        new Thread(new Producer(cache)).start();
        new Thread(new Producer(cache)).start();
        new Thread(new Producer(cache)).start();
    }
}
运行结果:
Consumer consumed [-1658797021]
Consumer consumed [-2050633449]

程序运行一会后,控制台就没输出了!!! 通过jstack命令可以分析出当前demo发生了死锁。

1.3.3 将Consumer和Producer中的notify()换成notifyAll()试试
运行结果
Consumer consumed [-781807640]
Consumer consumed [42787175]
Consumer consumed [327937050]
Consumer consumed [2140968760]
……

程序又可以欢乐的跑起来了!

我到底做错什么了!

2. notify和notifyAll的区别

1.3.2和1.3.3的不同之处只是将notify换成了notifyAll,肯定是这里有问题

在说明notify和notifyAll的区别之前,先阐述两个概念:锁池和等待池
再说notify和notifyAll的区别
知道上面的原理后,1.3.x的例子就很好理解了

1.3.1:因为只有一个生产者和消费者,所以等待池中始终只有一个线程,要么就是生产者唤醒消费者,要么消费者唤醒生产者,所以程序可以成功跑起来;
1.3.2:为了简化分析过程,假设两个消费者线程C1、C2,一个生产者线程P1。
时序过程如下(只是一种可能性,不绝对,毕竟只是个例子嘛~~)

1.3.3:分两种情况分析:

3 使用wait、notify的基本套路

下面是effective java中推荐的标准写法:

synchronized (obj) {
    while (<condition does not hold>)
        obj.wait(timeout);
    // Perform action appropriate to condition
}

为什么要用while,改成if行不行,就像下面这样:

synchronized (obj) {
    if (<condition does not hold>)
        obj.wait(timeout);
    // Perform action appropriate to condition
}

我们将1.1和1.2代码中的while换成if,启动一个生产者,两个消费者线程,某个消费者线程会出现数组下标越界的异常,代码及原因分析如下:

public class WaitNotifyTest {
    public static void main(String[] args) throws Exception {
        List<Integer> cache = Lists.newArrayList();
        new Thread(new Consumer(cache)).start();
        new Thread(new Consumer(cache)).start();
        Thread.sleep(1000);
        new Thread(new Producer(cache)).start();
    }
}
上一篇下一篇

猜你喜欢

热点阅读