1.notify 线程“假死”
所谓的线程“假死”是指,在使用 notify
唤醒多个等待的线程时,却意外的唤醒了一个没有“准备好”的线程,从而导致整个程序进入了阻塞的状态不能继续执行。
以多线程编程中的经典案例生产者和消费者模型为例,我们先来演示一下线程“假死”的问题。
1.1 正常版本
在演示线程“假死”的问题之前,我们先使用 wait
和 notify
来实现一个简单的生产者和消费者模型,为了让代码更直观,我这里写一个超级简单的实现版本。我们先来创建一个工厂类,工厂类里面包含两个方法,一个是循环生产数据的(存入)方法,另一个是循环消费数据的(取出)方法,实现代码如下。
/** * 工厂类,消费者和生产者通过调用工厂类实现生产/消费 */ class Factory { private int[] items = new int[1]; // 数据存储容器(为了演示方便,设置容量最多存储 1 个元素) private int size = 0; // 实际存储大小 /** * 生产方法 */ public synchronized void put() throws InterruptedException { // 循环生产数据 do { while (size == items.length) { // 注意不能是 if 判断 // 存储的容量已经满了,阻塞等待消费者消费之后唤醒 System.out.println(Thread.currentThread().getName() + " 进入阻塞"); this.wait(); System.out.println(Thread.currentThread().getName() + " 被唤醒"); } System.out.println(Thread.currentThread().getName() + " 开始工作"); items[0] = 1; // 为了方便演示,设置固定值 size++; System.out.println(Thread.currentThread().getName() + " 完成工作"); // 当生产队列有数据之后通知唤醒消费者 this.notify(); } while (true); } /** * 消费方法 */ public synchronized void take() throws InterruptedException { // 循环消费数据 do { while (size == 0) { // 生产者没有数据,阻塞等待 System.out.println(Thread.currentThread().getName() + " 进入阻塞(消费者)"); this.wait(); System.out.println(Thread.currentThread().getName() + " 被唤醒(消费者)"); } System.out.println("消费者工作~"); size--; // 唤醒生产者可以添加生产了 this.notify(); } while (true); } }
接下来我们来创建两个线程,一个是生产者调用 put
方法,另一个是消费者调用 take
方法,实现代码如下:
public class NotifyDemo { public static void main(String[] args) { // 创建工厂类 Factory factory = new Factory(); // 生产者 Thread producer = new Thread(() -> { try { factory.put(); } catch (InterruptedException e) { e.printStackTrace(); } }, "生产者"); producer.start(); // 消费者 Thread consumer = new Thread(() -> { try { factory.take(); } catch (InterruptedException e) { e.printStackTrace(); } }, "消费者"); consumer.start(); } }
执行结果如下:
从上述结果可以看出,生产者和消费者在循环交替的执行任务,场面非常和谐,是我们想要的正确结果。
1.2 线程“假死”版本
当只有一个生产者和一个消费者时,wait
和 notify
方法不会有任何问题,然而将生产者增加到两个时就会出现线程“假死”的问题了,程序的实现代码如下:
public class NotifyDemo { public static void main(String[] args) { // 创建工厂方法(工厂类的代码不变,这里不再复述) Factory factory = new Factory(); // 生产者 Thread producer = new Thread(() -> { try { factory.put(); } catch (InterruptedException e) { e.printStackTrace(); } }, "生产者"); producer.start(); // 生产者 2 Thread producer2 = new Thread(() -> { try { factory.put(); } catch (InterruptedException e) { e.printStackTrace(); } }, "生产者2"); producer2.start(); // 消费者 Thread consumer = new Thread(() -> { try { factory.take(); } catch (InterruptedException e) { e.printStackTrace(); } }, "消费者"); consumer.start(); } }
程序执行结果如下:
从以上结果可以看出,当我们将生产者的数量增加到 2 个时,就会造成线程“假死”阻塞执行的问题,当生产者 2 被唤醒又被阻塞之后,整个程序就不能继续执行了。
线程“假死”问题分析
我们先把以上程序的执行步骤标注一下,得到如下结果:
从上图可以看出:当执行到第 ④ 步时,此时生产者为工作状态,而生产者 2 和消费者为等待状态,此时正确的做法应该是唤醒消费着进行消费,然后消费者消费完之后再唤醒生产者继续工作;但此时生产者却错误的唤醒了生产者 2,而生产者 2 因为队列已经满了,所以自身并不具备继续执行的能力,因此就导致了整个程序的阻塞,流程图如下所示:
正确执行流程应该是这样的: