Bootstrap

Java线程通信-假死

概述

什么线程通信假死?

即程序没有死锁,但是却卡住不执行了。对于线程间通信来说,就是所有的线程都wait了。

代码演示

public class Goods {

    private final int MAX = 1;

    private int goodsCount = 0;

    public synchronized void produce() {

        try {
            Thread.sleep(100);
            System.out.println(Thread.currentThread().getName() + "获取了锁");
            if (goodsCount >= MAX) {
                try {
                    System.out.println(Thread.currentThread().getName() + "进行wait()");
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } else {
                goodsCount++;
                System.out.println(Thread.currentThread().getName() + "生产了第"+ goodsCount +"个商品,并notify");
                notify();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public synchronized void consume() {
        try {
            Thread.sleep(100);
            System.out.println(Thread.currentThread().getName() + "获取了锁");
            if (goodsCount > 0) {
                System.out.println(Thread.currentThread().getName() + "消费了第"+ goodsCount +"个商品,并notify");
                goodsCount--;
                notify();
            } else {
                System.out.println(Thread.currentThread().getName() + "进行wait()");
                wait();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

public class Test{

    public static void main(String[] args) {

        Goods goods = new Goods();

        Arrays.asList("P1").forEach(p -> {
            new Thread(p){
                @Override
                public void run() {
                    while (true){
                        goods.produce();
                    }
                }
            }.start();
        });

        Arrays.asList("C1").forEach(c -> {
            new Thread(c){
                @Override
                public void run() {
                    while (true){
                        goods.consume();
                    }
                }
            }.start();
        });
    }
}

输入日志:

P1获取了锁
P1生产了第1个商品,并notify
P1获取了锁
P1进行wait()
C1获取了锁
C1消费了第1个商品,并notify
C1获取了锁
C1进行wait()
P1获取了锁
P1生产了第1个商品,并notify
P1获取了锁
P1进行wait()
C1获取了锁
C1消费了第1个商品,并notify
C1获取了锁
C1进行wait()
P1获取了锁
P1生产了第1个商品,并notify
P1获取了锁
P1进行wait()

从输出结果来看,日志交错打印了生产者和消费者的信息。似乎没有什么问题。

改造一下生产者和消费者的数量:

public static void main(String[] args) {

    Goods goods = new Goods();

    Arrays.asList("P1", "P2").forEach(p -> {
        new Thread(p) {
            @Override
            public void run() {
                while (true) {
                    goods.produce();
                }
            }
        }.start();
    });

    Arrays.asList("C1", "C2").forEach(c -> {
        new Thread(c) {
            @Override
            public void run() {
                while (true) {
                    goods.consume();
                }
            }
        }.start();
    });
    
}

输出日志:

......
P1获取了锁
P1生产了第1个商品,并notify
P1获取了锁
P1进行wait()
C1获取了锁
C1消费了第1个商品,并notify
C1获取了锁
C1进行wait()
P1获取了锁
P1生产了第1个商品,并notify
P1获取了锁
P1进行wait()
C1获取了锁
C1消费了第1个商品,并notify
C1获取了锁
C1进行wait()
P1获取了锁
P1生产了第1个商品,并notify
P1获取了锁
P1进行wait()

日志输出一段时间后就暂停了,难道死锁了吗?

可以通过:jstack 进程id 来查看应用程序是否发生了死锁:

C:\Users\asus>jps
13824 ShamDeadTest
C:\Users\asus>jstack 13824
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.77-b03 mixed mode):

"DestroyJavaVM" #16 prio=5 os_prio=0 tid=0x0000000003382800 nid=0x492c waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2" #15 prio=5 os_prio=0 tid=0x000000001fc0b800 nid=0x2dac in Object.wait() [0x00000000207de000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x000000076b6aa8e8> (a com.thread.demo14.Goods)
        at java.lang.Object.wait(Object.java:502)
        at com.thread.demo14.Goods.consume(Goods.java:41)
        - locked <0x000000076b6aa8e8> (a com.thread.demo14.Goods)
        at com.thread.demo14.ShamDeadTest$2.run(ShamDeadTest.java:30)

"C1" #14 prio=5 os_prio=0 tid=0x000000001fc0a800 nid=0x1aa0 in Object.wait() [0x00000000206de000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x000000076b6aa8e8> (a com.thread.demo14.Goods)
        at java.lang.Object.wait(Object.java:502)
        at com.thread.demo14.Goods.consume(Goods.java:41)
        - locked <0x000000076b6aa8e8> (a com.thread.demo14.Goods)
        at com.thread.demo14.ShamDeadTest$2.run(ShamDeadTest.java:30)

"P2" #13 prio=5 os_prio=0 tid=0x000000001fc09000 nid=0x33d0 in Object.wait() [0x00000000205de000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x000000076b6aa8e8> (a com.thread.demo14.Goods)
        at java.lang.Object.wait(Object.java:502)
        at com.thread.demo14.Goods.produce(Goods.java:17)
        - locked <0x000000076b6aa8e8> (a com.thread.demo14.Goods)
        at com.thread.demo14.ShamDeadTest$1.run(ShamDeadTest.java:17)

"P1" #12 prio=5 os_prio=0 tid=0x000000001fbff800 nid=0x1388 in Object.wait() [0x00000000204df000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x000000076b6aa8e8> (a com.thread.demo14.Goods)
        at java.lang.Object.wait(Object.java:502)
        at com.thread.demo14.Goods.produce(Goods.java:17)
        - locked <0x000000076b6aa8e8> (a com.thread.demo14.Goods)
        at com.thread.demo14.ShamDeadTest$1.run(ShamDeadTest.java:17)

从输出结果来看,程序并没有发生死锁。那具体是什么原因呢?

这是在代码中埋下的隐患:notify()和notifyAll()的问题。

notify()和notifyAll()

notify()会导致线程假死

notify()方法一次只能从等待队列中唤醒一个线程;notifyAll()能唤醒所有等待队列中的线程。
在这里插入图片描述
根据上图所示,当线程进行wait()时,线程会进入等待队列,并释放持有的锁。当进行notify()的时候,等待队列中的某一个线程才有可能被唤醒;如果是notifyAll(),则是等待队列中所有的线程都会被唤醒。

在上面的代码中,使用了notify()方式唤醒线程。那么会存在如下的运行可能:

说明:一开始,所有线程P1/P2、C1/C2都是在就绪状态,产品池容量为1:

(1)P1获取到了锁,进入product方法,生产了产品,产品满了:

​ 打印:P1获取了锁;P1生产了第1个商品,并notify

(2)P2获取到了锁,进入product方法,此时商品已满:

​ 打印:P2获取了锁;P2进行wait()

(3)C1获取到了锁,进入consume方法,此时有商品消费:

​ 打印:C1获取了锁;C1消费了第1个商品,并notify

(4)C1获取到了锁,进入consume方法,此时没有商品消费:

​ 打印:C1获取了锁;C1进行wait()

(5)C2获取到了锁,进入consume方法,此时没有商品消费:

​ 打印:C2获取了锁;C2进行wait()

(6)P1获取到了锁,进入product方法,生产了产品,产品满了:

​ 打印:P1获取了锁;P1生产了第1个商品,并notify

// 关键一步来了,此时等待状态的线程有:P2、C1、C2。这里是让P2线程获得唤醒

(7)P2获取到了锁,进入product方法,此时商品已满:

​ 打印:P2获取了锁;P2进行wait()

(8)此时,还剩下P1还是就绪状态,所以P1获取到了锁,进入product方法,此时商品已满:

​ 打印:P1获取了锁;P1进行wait()

(9)到此,程序中的P1/P2、C1/C2线程中,全部都是wait()状态,

全都线程都是wait()状态,程序停止活动了,就是假死了!

注:如果只有一个生产者,一个消费者,那么notify()也不会造成假死。可以自行分析。

notifyAll()不会导致线程假死

为什么说notifyAll()不会导致线程假死呢?

首先notifyAll()会唤醒等待队列中的所有等待状态的线程,将其状态有等待 -> 可运行。

只要有notifyAll()发生,那么程序会将等待状态的所有线程清空,所以都不会出现假死。

;