黑马程序员——Java多线程—线程同步—wait、notify、notifyAll

来源:互联网 发布:已备案域名注册 编辑:程序博客网 时间:2024/05/09 11:24

参考:

CSDN  高爽:Java线程(三):线程协作-生产者/消费者问题


关于wait、notify、notifyAll如何使用不再说明,可以参考网上的教程,仅在此记录一个小问题。

先看一段代码,这段代码有一个问题:

package org.lgy.study.thread;import java.util.List;import java.util.ArrayList;/* javac -d classes "src/org/lgy/study/thread/WaitNotifyEgg.java"java org.lgy.study.thread.WaitNotifyEgg *//* 这个类用来测试线程同步(通信),不同线程在一个盘子中取放鸡蛋 */public class WaitNotifyEgg{public static void main(String[] args){// 盘子Plate p = new Plate();// 启动3个放鸡蛋的线程for(int i = 0; i < 3; i++){new Thread(new PutEggThread(p), "放-" + i).start();}// 启动3个取鸡蛋的线程for(int i = 0; i < 3; i++){new Thread(new GetEggThread(p), "取-" + i).start();}}/* 定义一个盘子类,用来存放鸡蛋 */public static class Plate{// 存放鸡蛋的盘子private List<Object> eggs = new ArrayList<>();// 放鸡蛋public synchronized void put(Object egg){if(eggs.size() > 0){System.out.println(Thread.currentThread().getName() + ",盘子已满,正在等待... ...");try{this.wait();}catch(InterruptedException e){e.printStackTrace();}}eggs.add(egg);System.out.println(Thread.currentThread().getName() + ",放入鸡蛋:" + egg);this.notify();}// 取鸡蛋public synchronized Object get(){if(this.eggs.size() == 0){System.out.println(Thread.currentThread().getName() + ",盘子为空,正在等待... ...");try{this.wait();  // 如果盘子为空,则阻塞当前取鸡蛋的线程}catch(InterruptedException e){e.printStackTrace();}}Object egg = eggs.get(0);  // 拿到鸡蛋eggs.clear();  // 清空盘子System.out.println(Thread.currentThread().getName() + ",取到鸡蛋:" + egg);this.notify();  // 唤醒因当前对象(即this代表的Plate对象)而阻塞的线程return egg;}}/* 放鸡蛋的线程 */private static class PutEggThread implements Runnable{private Plate plate;  // 往这个盘子里放鸡蛋private Object egg;  // 被放入盘子中的鸡蛋private PutEggThread(Plate plate){this.plate = plate;// this.egg = new Object();}public void run(){while(true){this.egg = new Object();this.plate.put(egg);try{Thread.sleep(100);}catch(InterruptedException e){e.printStackTrace();}}}}/* 取鸡蛋的线程 */private static class GetEggThread implements Runnable{private Plate plate;  // 从这个盘子中取鸡蛋public GetEggThread(Plate plate){this.plate = plate;}public void run(){while(true){plate.get();try{Thread.sleep(100);}catch(InterruptedException e){e.printStackTrace();}}}}}


问题一:连续2次放入鸡蛋


看一下上面代码的运行结果:


下面分析一下为什么会出现上面的错误。

出现问题的代码如下:

// 放鸡蛋public synchronized void put(Object egg){if(eggs.size() > 0){System.out.println(Thread.currentThread().getName() + ",盘子已满,正在等待... ...");try{this.wait();}catch(InterruptedException e){e.printStackTrace();}}eggs.add(egg);System.out.println(Thread.currentThread().getName() + ",放入鸡蛋:" + egg);this.notify();}

线程“放-1、放-2”是在上面的“this.wait()”处被阻塞的,当它们被唤醒时,就会从被阻塞的代码后面继续执行,也就是说它们会继续执行“this.wait()”后面的代码,即“eggs.add(egg)... ...”,直至所在方法结束,所以就出现了2个线程都可以向盘子中放鸡蛋的情况。


我们期望的情况是这样的,当被阻塞的线程被唤醒后,它先判断盘子中是否有鸡蛋,如果有那它再次阻塞,如果没有那它就放鸡蛋。所以我们应该把“this.wait()”放到一个不断判断盘子中是否有鸡蛋的循环中,如果有鸡蛋就执行循环(即阻塞线程),如果没有鸡蛋就退出循环,放鸡蛋。


修改后的代码:

// 放鸡蛋public synchronized void put(Object egg){while(eggs.size() > 0){System.out.println(Thread.currentThread().getName() + ",盘子已满,正在等待... ...");try{this.wait();}catch(InterruptedException e){e.printStackTrace();}}eggs.add(egg);System.out.println(Thread.currentThread().getName() + ",放入鸡蛋:" + egg);this.notify();}

对于取鸡蛋的get方法也做同样的处理。


问题二:死锁


这是基于问题一修改后的代码的运行结果:


分析一下死锁的原因:

先看一下程序的执行

1:“放-0” 因为没有线程阻塞,所以没有唤醒任何线程
2:“放-2” 阻塞
3:“放-1” 阻塞


4:“取-2” 唤醒“放-2”
5:“取-1” 阻塞
6:“取-0” 阻塞


7:“放-2” 唤醒“放-1”
8:“放-1” 阻塞
9:“放-0” 阻塞
10:“放-2” 阻塞


11:“取-2” 唤醒 “取-1”
12:“取-1” 阻塞
13:“取-2” 阻塞




名    初态 每次执行完后的状态
放-0:就绪 就绪 阻塞
放-1:就绪 阻塞(被唤醒)阻塞
放-2:就绪 阻塞(被唤醒)就绪阻塞


取-0:就绪 阻塞
取-1:就绪 阻塞(被唤醒)阻塞
取-2:就绪 就绪 就绪 阻塞


从上面的分析中可以看出,每一个线程的终态都是阻塞,所以发生了死锁。
发生死锁的原因就是唤醒线程时用的是notify,而不是notifyAll。notify在唤醒线程时是随机唤醒一个阻塞的线程,具有不确定性,有可能唤醒放鸡蛋的线程,也有可能唤醒取鸡蛋的线程。


从上面程序的执行过程可以看出,在输出8、9、10的结果后,三个放鸡蛋的线程全部阻塞掉了,这是应该有取鸡蛋的线程来唤醒放鸡蛋的线程,但是实际上取鸡蛋的线程唤醒的不是放鸡蛋的线程,而是唤醒了取鸡蛋的线程,被唤醒的取鸡蛋的线程因为没有鸡蛋可取而阻塞掉(直接阻塞掉是不会唤醒任何线程的)。

0 0