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

泡芙掠夺者 关注

2017.08.24 22:00* 字数 1361 阅读 249评论 3喜欢 7赞赏 1

本文通过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的区别之前,先阐述两个概念:锁池和等待池
  • 锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。
  • 等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中。
再说notify和notifyAll的区别
  • 线程调用了对象的 wait()方法,便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁(即不会参与线程调度,大家理解就好~~)。
  • notifyAll调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。
  • notify调用后,只会将等待池中的一个随机线程移到锁池。
知道上面的原理后,1.3.x的例子就很好理解了

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

  • C1,C2观察到缓存cache中无数据,进入等待池;
  • P1获取锁并设置cache数据,通过notify唤醒等待池中某个线程C1,假设C1被唤醒并放入锁池,然后P1释放锁、继续循环重新获取锁并因为检测到cache.size()==1而进入等待池;
  • 此时锁池中的线程为C1,C1会竞争到锁,从而消费数据,然后执行notify方法并释放锁,并假设其notify方法会将C2从等待池移入锁池;
  • C2检测到cache为空,执行await()使自身进入锁池。因为自身的阻塞所以不能唤醒C1或P1,从而导致死锁!

1.3.3:分两种情况分析:

  • cache为空
  • 先假设所有线程P都一直停留在等待池。但是这种情况是不可能存在的,因为cache肯定是某个C线程消费后才为空,然后该C线程会执行notifyAll方法将所有等待池中的线程都移入锁池,所以不可能所有P线程一直在等待池;
  • 既然P线程不可能一直在等待池,那么这种P线程会竞争锁从而设置cache的值导致cache不为空;
  • cache不空的情况分析过程同上

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();
}
}
  • 消费者C1、C2发现cache为空,相继进入等待池;
  • P1生产数据,放入cache并唤醒C1,同时自己进入等待池;
  • C1消费数据,唤醒C2,C2从cache.wait()处开始执行,因为我们将while(cache.isEmpty())改成了if(cache.isEmpty()),C2不会再次检查cache是否为空,而是直接执行后续代码,这时cache的数据已经被C1消费完了,调用cache.get(0)产生数组下标越界!

notify notifyAll 死锁的更多相关文章

  1. 零基础学习java------day18------properties集合,多线程(线程和进程,多线程的实现,线程中的方法,线程的声明周期,线程安全问题,wait/notify.notifyAll,死锁,线程池),

    1.Properties集合 1.1 概述: Properties类表示了一个持久的属性集.Properties可保存在流中或从流中加载.属性列表中每个键及其对应值都是一个字符串 一个属性列表可包含另 ...

  2. wait notify notifyAll await signal signalAll 的理解及示例

    从常见的一道面试题开始,题目的描述是这样子的: 有三个线程分别打印A.B.C,请用多线程编程实现,在屏幕上循环打印10次ABCABC- 网上大都教了你怎么去实现,其实我也写过一篇 https://bl ...

  3. java 多线程之wait(),notify,notifyAll(),yield()

    wait(),notify(),notifyAll()不属于Thread类,而是属于Object基础类,也就是说每个对像都有wait(),notify(),notifyAll()的功能.因为都个对像都 ...

  4. java中的wait(),notify(),notifyAll(),synchronized方法

    wait(),notify(),notifyAll()三个方法不是Thread的方法,而是Object的方法.意味着所有对象都有这三个方法,因为每个对象都有锁,所以自然也都有操作锁的方法了.这三个方法 ...

  5. java 多线程(wait/notify/notifyall)

    package com.example; public class App { /* wait\notify\notifyAll 都属于object的内置方法 * wait: 持有该对象的线程把该对象 ...

  6. Java多线程8:wait()和notify()/notifyAll()

    轮询 线程本身是操作系统中独立的个体,但是线程与线程之间不是独立的个体,因为它们彼此之间要相互通信和协作. 想像一个场景,A线程做int型变量i的累加操作,B线程等待i到了10000就打印出i,怎么处 ...

  7. 使用Object的wait,notify,notifyAll做线程调度

    我们知道java中的所有类的祖先都是Object,Object类有四个个方法wait(),wait(long timeout),notify(),notifyAll(),这四个方法可以用来做线程的调度 ...

  8. Java多线程之wait(),notify(),notifyAll()

    在多线程的情况下,因为同一进程的多个线程共享同一片存储空间,在带来方便的同一时候,也带来了訪问冲突这个严重的问题.Java语言提供了专门机制以解决这样的冲突,有效避免了同一个数据对象被多个线程同一时候 ...

  9. Java多线程的wait(),notify(),notifyAll()

    在多线程的情况下.因为多个线程与存储空间共享相同的过程,同时带来的便利.它也带来了访问冲突这个严重的问题. Java语言提供了一种特殊的机制来解决这类冲突,避免同一数据对象由多个线程在同一时间访问. ...

随机推荐

  1. 如何从Windows远程上传文件到Linux(例如CentOS 7)

    一.先看Linux系统是否安装有vsftp软件(vs是very secure的意思) [root@localhost /]# rpm -qa | grep vsftpdvsftpd-3.0.2-9.e ...

  2. HTML中显示的文字自动换行

    在html中控制自动换行 http://www.cnblogs.com/zjxbetter/articles/1323449.html eg: <table> <tr> < ...

  3. firedac使用UNIXODBC连接SQLSERVER

    firedac使用UNIXODBC连接SQLSERVER 1)下载 SQL SERVER ODBC DRIVER FOR 64-BIT LINUX. 如果你有旧版mssql 工具安装,请删除任何较旧的 ...

  4. 【maven】排除maven中jar包依赖的解决过程 例子:spring cloud启动zipkin,报错maven依赖jar包冲突 Class path contains multiple SLF4J bindings.

    一直对于maven中解决jar包依赖问题的解决方法纠结不清: 下面这个例子可以说明一个很简单的解决方法: 项目启动报错: Connected to the target VM, address: '1 ...

  5. tomcat启动时间修改

    Server Tomcat v6.0 Server at localhost was unable to start within 45 seconds. If the server requires ...

  6. Java中List效率的比较

    Java Collections Framework(JCF) 是Java SE中一个基本的类集,几乎所有的项目都会用到,其中的List 则是JCF中最最常用的一个接口.围绕List 接口,有很多实现 ...

  7. 《Windows核心编程》第七章——进程优先级实验

    其实就是做个实验,看看SetPriorityClass是否真的会生效. 设计思路:主线程一直在进行某种操作(这里是写文件操作),以保证有一定的CPU占用率:生成的子线程来接收你的命令,决定将进程改变为 ...

  8. 深度学习研究组Deep Learning Research Groups

    Deep Learning Research Groups Some labs and research groups that are actively working on deep learni ...

  9. hdu 1242 Rescue(BFS入门)

    第一次用容器做的BFS题目,题目有个地方比较坑,就是遍历时的方向,比如上下左右能AC,右上左下就WA #include <stdio.h> #include <string.h> ...

  10. Same Tree leetcode java

    题目: Given two binary trees, write a function to check if they are equal or not. Two binary trees are ...