线程间之间是如何通信的?

75 阅读6分钟

| wait() | 调用方的线程进入WAITING状态,只有等待其他线程的通知或被中断才会返回,需要注意,调用wait()方法会释放锁 |

| wait(long) | 超时等待一段时间,如果时间到没有通知就超时返回。单位ms |

| wait(long, int) | 对于超时时间做更加细粒度的控制可以精确到纳秒 |

等待/通知机制描述:

等待/通知机制是指一个线程A调用了对象O的wait()方法进入等待状态,另一个线程B调用了对象O的notify()或notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。对象O上的wait()和notify()/notifyAll()就好比一个开关信号,用来完成等待方和通知方的交互工作(就好比一开始说的生产者-消费者模型)

示例代码:

package com.lizba.p3;

import com.lizba.p2.SleepUtil;

import java.text.SimpleDateFormat;

import java.util.Date;

/**

  •   wait()和notify()/notifyAll()示例代码
    
  • @Author: Liziba

  • @Date: 2021/6/15 23:28

*/

public class WaitNotify {

static boolean flag = true;

static Object lock = new Object();

static final SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");

public static void main(String[] args) {

Thread waitThread = new Thread(new Wait(), "waitThread");

waitThread.start();

SleepUtil.sleepSecond(1);

Thread notifyThread = new Thread(new Notify(), "notifyThread");

notifyThread.start();

}

/**

  • wait线程,当条件不满足时wait()

*/

static class Wait implements Runnable{

@Override

public void run() {

// 加锁

synchronized(lock) {

// 当条件不满足时,继续wait

while (flag) {

System.out.println(Thread.currentThread()

  • " flag is true. wait at " +sdf.format(new Date()));

try {

// 此操作会释放锁

lock.wait();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

// 满足条件是完成工作

System.out.println(Thread.currentThread()

  • " flag is false. finished at " + sdf.format(new Date()));

}

}

}

static class Notify implements Runnable {

@Override

public void run() {

// 加锁

synchronized (lock) {

// 获取到锁或通知等待在锁上的线程

// 通知不会释放锁,直到当前线程执行完释放lock锁后,waitThread才能从wait方法返回

System.out.println(Thread.currentThread()

  • "hold lock. notify at " + sdf.format(new Date()));

lock.notifyAll();

flag = false;

SleepUtil.sleepSecond(5);

}

// 再次加锁

synchronized (lock) {

System.out.println(Thread.currentThread()

  • "hold lock again. notify at " + sdf.format(new Date()));

SleepUtil.sleepSecond(5);

}

}

}

}

查看输出:

在这里插入图片描述

注意上述的hold lock again 和 flag is flase这两行代码可能执行顺序会互换。

总结:

  1. 使用wait()、notify()和notifyAll()需要先对该对象加锁

  2. 调用wait()方法后线程由RUNNING状态变为WAITING状态,并且将当前线程放置到对象的等待队列中

  3. notify()方法和notifyAll()调用后,等待的线程需要等到调用notify()和notifyAll()的线程释放锁后,等待队列中的线程才有机会从wait()返回

  4. notify()移动一个线程从等待队列到同步队列,notifyAll()移动所有等待线程,过程是将线程从等待队列移动到同步队列中,被移动的线程由WAITING变为BLOCKED状态

  5. 从wait()方法返回的前提是获取了对象的锁

  6. wait()、notify()和notifyAll()机制依赖的是同步机制,其目的是为了从wait()方法返回的线程能感知到其他线程对变量作出的修改

图示上述过程:

在这里插入图片描述

总结上图:

WaitThread线程首先获取了锁,然后调用对象的wait()方法,从而释放了锁进入对象的等待队列WaitQueue中,进入等待状态。由于WaitThread释放了对象的锁,NotifyThread随后获取了对象的锁,并且调用了对象的notify()方法,将处于等待队列WaitQueue的WaitThread移动到了SynchronizedQueue中,此时WaitThread的状态变为阻塞状态。NotifyThread释放了锁之后,WaitThread再次获取到锁从wait()方法返回继续执行。

3、等待/通知的经典范式

等待/通知的经典范式,分为等待方和通知方,这两者需要分别遵循如下规则。

等待方遵循如下规则:

  1. 获取对象的锁

  2. 如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件

  3. 条件满足则执行对应的逻辑

// 示例等待方伪代码

synchronized(对象) {

while(条件不满足) {

对象.wait();

}

// ToDo...

}

通知方遵循如下规则:

  • 获取对象的锁

  • 改变条件

  • 通知所有等待在对象上的线程

// 示例通知方伪代码

synchronized(对象) {

改变条件

对象.notifyAll();

}

4、管道输入/输出流

管道输入/输出流和普通文件输入/输出流或者网络输入/输出流的不同之处在于,管道输出/输出流主要用于线程之间的数据传输,传输的媒介为内存。

管道输入/输出流的具体实现:

  1. PipedInputStream

  2. PipedOutputStream

  3. PipedReader

  4. PipedWriter

1、2为字节流,3、4为字符流。

示例代码:

package com.lizba.p3;

import java.io.IOException;

import java.io.PipedReader;

import java.io.PipedWriter;

/**

  •  管道流
    
  • @Author: Liziba

  • @Date: 2021/6/16 21:07

*/

public class Piped {

public static void main(String[] args) throws IOException {

PipedWriter out = new PipedWriter();

PipedReader in = new PipedReader();

// 输入输出流连接(不连接会报错)

out.connect(in);

Thread printThread = new Thread(new Print(in), "PrintThread");

printThread.start();

// 输入

int receive = 0;

try {

while ((receive = System.in.read()) != -1) {

out.write(receive);

}

} finally {

out.close();

}

}

/**

  • 单个字符读取并输出

*/

static class Print implements Runnable {

private PipedReader in;

public Print(PipedReader in) {

this.in = in;

}

@Override

public void run() {

int receive = 0;

try {

while (true) {

// 单个字符读取

if ((receive = in.read()) != -1){

System.out.print((char)receive);

}

}

} catch (IOException e) {

e.printStackTrace();

}

}

}

}

测试代码样例:

## 输入

hello liziba

## 输出

hello liziba

5、Thread.join()

Thread.join()的语义含义:当前线程A等待Thread线程终止之后才从Thread.join()处返回。线程提供的join()方法的api如下:

public final void join() throws InterruptedException

// 下面两个具有超时等待,线程再给定的时间没有返回,那么超时的方法会返回

public final synchronized void join(long millis, int nanos)

public final synchronized void join(long millis)

示例代码:

设置十个线程,分别从0-9,每个线程需要调用前一个线程的join()方法, 比如线程0结束了,线程1才能从join()返回线,程1结束了,线程2才能从join()返回。

package com.lizba.p3;

import com.lizba.p2.SleepUtil;

import java.util.concurrent.TimeUnit;

/**

  • join()等待通知机制
    
  • @Author: Liziba

  • @Date: 2021/6/16 21:25

*/

public class Join {

public static void main(String[] args) {

// 前一个线程

Thread previous = Thread.currentThread();

for (int i = 0; i < 10; i++) {

Thread t = new Thread(new Domino(previous), String.valueOf(i));

t.start();

previous = t;

}

SleepUtil.sleepSecond(5);

System.out.println(Thread.currentThread().getName() + " end.");

}

static class Domino implements Runnable {

private Thread thread;

public Domino(Thread thread) {

this.thread = thread;

}

@Override

public void run() {

try {

thread.join();

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println(Thread.currentThread().getName() + " end.");

}

}

}

查看输出结果:

在这里插入图片描述

总结上述代码:

每个线程终止的前提是前驱线程的终止,每个线程等待前驱线程终止后,才从join()返回,这里涉及了等待/通知机制,具体原理我们可以通过看JDK的源码来了解:

public final synchronized void join(long millis)

throws InterruptedException {

long base = System.currentTimeMillis();

long now = 0;

if (millis < 0) {

throw new IllegalArgumentException("timeout value is negative");

}

// 超时等待时间未设置则为0,也就是join()方法

if (millis == 0) {

// 判断当前线程是否终止

while (isAlive()) {

// 如果未终止,继续wait()

wait(0);

}

} else {

// 判断当前线程是否终止

while (isAlive()) {

long delay = millis - now;

// 判断超时等待时间是否已经到了,如果到了则返回

if (delay <= 0) {

break;

}

// 否则继续等待,计算新的时间传入

wait(delay);

now = System.currentTimeMillis() - base;

}

}

}

// 尝试判断当前线程时候已经执行完毕(是否还活着)

public final native boolean isAlive();

6、ThreadLocal的使用

本文不会详细讲述ThreadLocal的核心原理,之后简单的介绍ThreadLocal的使用,后续会单独分一篇文章来详述其原理和使用。

ThreadLocal即线程变量,它是以ThreadLocal对象为键、任意对象为值的存储结构。这个存储结构可以附带在线程上,我们可以通过一个ThreadLocal对象来查询绑定在这个线程上的一个值。

示例代码: