boy-learning-thread | 1.1.4 线程通信

相关源码:boy-learning-thread
个人博客:http://bruce.bugmakers.club
内容来自《网易微专业 - 高性能编程章节》

线程通信

通信方式

要想实现多个线程之间的协同,如:线程执行先后顺序、获取某个线程的执行结果等待。
涉及到线程时间的相互通信,分为下面四类:

  1. 文件共享
  2. 网络共享
  3. 共享变量
  4. jdk 提供的线程协调 API,细分为:suspend/resume、wait/notify、park/unpark

1、文件共享

// 共享文件
File file = new File("a.txt");

thread1 写文件
thread2 读文件

2、变量共享

// 共享变量
public static String content = "init-txt";

thread1 写入内存变量
thread2 读取内存变量

3、网络共享

4、线程协作 - JDK API

JDK 中对于需要多线程协作完成某一任务的场景,提供了对应的API支持。
多线程协作的典型场景是:生产者 - 消费者模型。(线程阻塞、线程唤醒)

场景示例

线程1去买包子,没有包子,则不再执行。 线程2生产包子,通知线程1继续执行。

4.1、suspend/resume 机制

thread.suspend() // 挂起
thread.resume    // 激活

suspend/resume 这种组合已经被 jdk 遗弃掉了,原因是容易出现死锁。

死锁场景一:同步代码块中

Object baozi = null;
Thread consumer = new Thread(() -> {
    if (baozi == null) {
        System.out.println("来晚了,卖光了");
        synchronized(this) {
            Thread.currentThread().suspend();
        }
    }
    System.out.println("买到包子,回家");
});

// 包子制作中
Thread.sleep(3000L);

baozi = new Object();
System.out.println("新一笼的包子做好啦");

// 争取到锁后,在恢复 consumer
synchronized(this) {
    consumer.resume();
}

死锁场景二:先后顺序没有控制好

Object baozi = null;
Thread consumer = new Thread(() -> {
    if (baozi == null) {
        System.out.println("来晚了,卖光了");
        
        Thread.sleep(5000L);
        
        Thread.currentThread().suspend();
    }
    System.out.println("买到包子,回家");
});

// 包子制作中
Thread.sleep(3000L);

baozi = new Object();
System.out.println("新一笼的包子做好啦");

consumer.resume();

consumer.join();

4.2、wait/notify 机制

synchronized(LOCK) {
    Object.wait();  // wait会释放LOCK对象锁    
}
synchronized(LOCK) {
    Object.notify();   
}

这些方法只能由同一对象锁的持有者线程调用,也就是写在同步代码块里边,否则会抛出 IllegalMonitorStateException 异常。

wait 方法导致当前线程等待,加入该对象的等待集合中,并且放弃当前持有的对象锁。
notify/notifyAll 方法唤醒一个或所有正在等待这个对象锁的线程。

注意:虽然会 wait 自动解锁,但是对顺序有要求,如果在 notify 被调用之后,才开始 wait 方法的调用,线程会永远处于 WAITING 状态。

4.3、park/unpark 机制

LockSupport.park();
LockSupport.unpark();

线程调用 park 则等待 “许可”,unpark 方法为指定线程提供 “许可(permit)”。

不要求 park/unpark 的调用顺序。

多次调用 unpark 后,再调用 park,线程会直接运行。
但不会叠加,也就是说,连续多次调用 park 方法,第一次会拿到 “许可” 直接运行,后续调用会进入等待。

LockSupport 不会释放持有的对象锁,所以在同步代码块中,会出现死锁的现象。

死锁场景:

synchronized(LOCK) {
    LockSupport.park();
}

//...

synchronized(LOCK) {
    LockSuport.unpark();
}

伪唤醒

警告!之前代码中用 if 语句来判断是否进入等待状态,是错误的!
官方建议,应该在循环中检查等待条件,原因是处于等待状态的线程可能会收到错误报警和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。

伪唤醒是指线程并非因为 notify、notifyAll、unpark 等 api 调用唤醒,是更底层原因导致的。

推荐阅读更多精彩内容