后端精进笔记03:线程间的通信

249 阅读5分钟

一、概述

线程之间的协同,必然需要线程间的相互通信,主要分为如下4类:

  • 文件共享
  • 网络共享
  • 变量共享
  • JDK提供的线程协同API

本文将重点讲述JDK提供的3套线程协同API

二、被弃用的 suspend/resume 机制

2.1 概述

调用suspend方法挂起当前线程,通过resume方法恢复线程执行。

2.2 代码实例

public void suspendResumeTest() throws Exception {
        // 启动线程
        Thread consumerThread = new Thread(() -> {
            if (baozidian == null) { // 如果没包子,则进入等待
                System.out.println("1、进入等待");
                Thread.currentThread().suspend();
            }
            System.out.println("2、买到包子,回家");
        });
        consumerThread.start();
        // 3秒之后,生产一个包子
        Thread.sleep(3000L);
        baozidian = new Object();
        System.out.println("3、通知消费者");
        consumerThread.resume();
    }

执行结果:

1、进入等待
3、通知消费者
2、买到包子,回家

这样看来,似乎很好用的样子,但是这俩方式是被标记了废弃的,why?

2.3 死锁问题

2.3.1 同步代码块中的死锁

由于同步代码块中的suspend操作会挂起当前线程,但并不会释放锁 ,这就导致了线程A在同步代码块中调用了suspend方法,线程B在尝试进入同步代码块中调用resume方法时,无法获取锁而阻塞,最终造成死锁。

public void suspendResumeDeadLockTest() throws Exception {
        // 启动线程
        Thread consumerThread = new Thread(() -> {
            if (baozidian == null) { // 如果没包子,则进入等待
                System.out.println("1、进入等待");
                // 当前线程拿到锁,然后挂起
                synchronized (this) {
                    Thread.currentThread().suspend();
                }
            }
            System.out.println("2、买到包子,回家");
        });
        consumerThread.start();
        // 3秒之后,生产一个包子
        Thread.sleep(3000L);
        baozidian = new Object();
        // 争取到锁以后,再恢复consumerThread
        synchronized (this) {
            System.out.println("3、通知消费者");
            consumerThread.resume();
        }
    }

执行结果(线程始终在运行,未能结束):

1、进入等待

2.3.2 suspend比resume先执行造成的死锁

正常情况下,是先调用suspend挂起线程,再调用resume方法唤醒线程。但在一些复杂业务场景下,并不能保证两者执行的先后顺序,而如果两者未能按照先suspendresume的顺序执行,将会造成死锁。

public void suspendResumeDeadLockTest2() throws Exception {
        // 启动线程
        Thread consumerThread = new Thread(() -> {
            if (baozidian == null) {
                System.out.println("1、没包子,进入等待");
                try { // 为这个线程加上一点延时来模拟耗时业务
                    Thread.sleep(5000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 这里的挂起执行在resume后面
                Thread.currentThread().suspend();
            }
            System.out.println("2、买到包子,回家");
        });
        consumerThread.start();
        // 3秒之后,生产一个包子
        Thread.sleep(3000L);
        baozidian = new Object();
        System.out.println("3、通知消费者");
        consumerThread.resume();
    }

执行结果(线程始终在运行,未能自行结束):

1、没包子,进入等待

三、wait/notify机制

3.1 概述

类似的,调用wait方法可以挂起当前线程,调用notify/notifyAll方法可以唤醒随机一个/所有线程。

但是waitnotifynotifyAll只能由同一个对象锁的持有线程调用(由于其内部是基于对象的等待集,所以方法必须在同步代码块中调用),否则会抛出IllegalMonitorStateExecption

3.2 代码实例

public void waitNotifyTest() throws Exception {
        // 启动线程
        new Thread(() -> {
            synchronized (this) {
                while (baozidian == null) { // 如果没包子,则进入等待
                    try {
                        System.out.println("1、进入等待");
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            System.out.println("2、买到包子,回家");
        }).start();
        // 3秒之后,生产一个包子
        Thread.sleep(3000L);
        baozidian = new Object();
        synchronized (this) {
            System.out.println("3、通知消费者");
            this.notifyAll();
        }
    }

执行结果:

1、进入等待
3、通知消费者
2、买到包子,回家

3.3 死锁问题

相对于suspend/resume机制,wait/notify机制改进了挂起线程时,自动释放锁的问题,但是未能解决执行顺序的问题(notify在wait之前执行同样会造成死锁):

public void waitNotifyDeadLockTest() throws Exception {
        // 启动线程
        new Thread(() -> {
            if (baozidian == null) { // 如果没包子,则进入等待
                try {
                    Thread.sleep(5000L);
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                }
                synchronized (this) {
                    try {
                        System.out.println("1、进入等待");
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            System.out.println("2、买到包子,回家");
        }).start();
        // 3秒之后,生产一个包子
        Thread.sleep(3000L);
        baozidian = new Object();
        synchronized (this) {
            this.notifyAll();
            System.out.println("3、通知消费者");
        }
    }

执行结果(线程始终在运行,未能自行结束):

3、通知消费者
1、进入等待

四、park/unpark机制

4.1 概述

park/unpark机制类似于令牌机制,调用park方法则需要等待一个许可(令牌),而unpark方法则为指定线程提供了一个许可(令牌)。park/unpark机制对执行的顺序没有要求。但是如果unpark先执行了多次(实现提供了多次许可)之后,park方法只能执行一次(即:unpark提供的许可不能累计),后面依旧需要再次执行unpark提供许可之后,才能再次执行park。

4.2 代码实例

public void parkUnparkTest() throws Exception {
        // 启动线程
        Thread consumerThread = new Thread(() -> {
            while (baozidian == null) { // 如果没包子,则进入等待
                System.out.println("1、进入等待");
                LockSupport.park();
            }
            System.out.println("2、买到包子,回家");
        });
        consumerThread.start();
        // 3秒之后,生产一个包子
        Thread.sleep(3000L);
        baozidian = new Object();
        LockSupport.unpark(consumerThread);
        System.out.println("3、通知消费者");
    }

执行结果:

1、进入等待
3、通知消费者
2、买到包子,回家

4.3 死锁问题

同步代码块中的park操作会挂起当前线程,但并不会释放锁:

public void parkUnparkDeadLockTest() throws Exception {
        // 启动线程
        Thread consumerThread = new Thread(() -> {
            if (baozidian == null) { // 如果没包子,则进入等待
                System.out.println("1、进入等待");
                // 当前线程拿到锁,然后挂起
                synchronized (this) {
                    LockSupport.park();
                }
            }
            System.out.println("2、买到包子,回家");
        });
        consumerThread.start();
        // 3秒之后,生产一个包子
        Thread.sleep(3000L);
        baozidian = new Object();
        // 争取到锁以后,再恢复consumerThread
        synchronized (this) {
            LockSupport.unpark(consumerThread);
        }
        System.out.println("3、通知消费者");
    }

执行结果(线程始终在运行,未能自行结束):

1、进入等待

五、关于伪唤醒的说明

注意,上面给出的大部分例子,都是通过if (baozidian == null)来检查线程等待条件的,然后如果线程被唤醒,则会在if的代码块中继续执行,而不会再去校验if中的判断。

官方建议使用while语句来检查线程等待条件,因为处于等待状态下的线程可能会错误警报或伪唤醒(涉及操作系统底层以及CPU硬件底层,此处不做详述),如果不在循环中检查线程等待条件,程序就会在并没有满足条件的情况下继续执行,出现意想不到的错误。