Java线程间是如何通信的

1. 线程间通信

这里介绍两种线程之间通信的核心方式

  • 通过共享变量,实现线程之间的信息交换
  • 基于等待/通知的通信,主要借助Object类的wait()/Object.notify()/Object.notifyAll()方法来实现。

1.1. 共享变量

共享变量指的是多个线程共同读写同一个内存区域,来控制彼此的执行流程。下面是一段代码示例:

Java 复制代码
import java.util.concurrent.TimeUnit;
public class ShareVariate {

    // 这里必须加上volatile,不然t2_thread对flag的修改对t1_thread不可见
    private static volatile boolean flag = false;

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


        Thread t1 = Thread.ofVirtual().name("t1_thread").start(() -> {
            System.out.println(Thread.currentThread().getName() + " start execute ");
            while (!flag) {
            }
            System.out.println(Thread.currentThread().getName() + " end");
        });

        TimeUnit.SECONDS.sleep(2);

        Thread t2 = Thread.ofVirtual().name("t2_thread").start(() -> {
            System.out.println(Thread.currentThread().getName() + " start execute ");
            flag = true;
        });

        t1.join();
        t2.join();
    }
}

上面代码中t1_thread和t2_thread通过读写同一个内存变量flag,来进行线程之间的信息传递,t2_thread成功控制值了t1_thread的执行流程。

1.2. 等待/通知

基于等待/通知的通信方式,主要借助Object类的wait()/notify()/notifyAll()方法来实现

wait():该方法必须在synchronized代码块中使用,会让线程进入阻塞状态,并释放锁。调用wait()方法的对象和提供锁的对象必须是同一个。

notify():该方法可以用于随机唤醒一个被wait()方法阻塞的线程,必须在synchronized代码块中使用。被唤醒的线程也不一定能获取到锁,可能在尝试获取锁失败后,继续阻塞。调用notify()方法的对象和提供锁的对象必须是同一个。和调用wait()方法的对象也必须是同一个。

notifyAll():该方法可以用于唤醒所有被同一个对象wait()方法阻塞的线程,必须在synchronized代码块中使用。虽然唤醒了所有线程,但最终只能有一个线程获取到锁。调用notifyAll()方法的对象和提供锁的对象必须是同一个。和调用wait()方法的对象也必须是同一个。

java 复制代码
import java.util.Date;
import java.util.concurrent.TimeUnit;

public class WaitNotifyDemo01 {


    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();

        Thread t1 = Thread.ofVirtual().name("t1_thread").start(() -> {
            synchronized (object) {
                System.out.println(Thread.currentThread().getName() + "into synchronized block, time is " + new Date());
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(Thread.currentThread().getName() + "out synchronized block, time is " + new Date());
            }

        });

        TimeUnit.MILLISECONDS.sleep(10);

        Thread t2 = Thread.ofVirtual().name("t2_thread").start(() -> {
            synchronized (object) {
                System.out.println(Thread.currentThread().getName() + "into synchronized block, time is " + new Date());
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                object.notify();
                System.out.println(Thread.currentThread().getName() + "out synchronized block, time is " + new Date());
            }

        });

        t1.join();
        t2.join();

    }
}

输出结果为

arduino 复制代码
t1_threadinto synchronized block, time is Thu Jul 24 14:20:22 CST 2025
t2_threadinto synchronized block, time is Thu Jul 24 14:20:22 CST 2025
t2_threadout synchronized block, time is Thu Jul 24 14:20:27 CST 2025
t1_threadout synchronized block, time is Thu Jul 24 14:20:27 CST 2025

从上面的示例可以看出,如果线程t2不调用object.notify()方法,线程t1将一直处于等待状态。

为什么wait()方法必须在同步块中使用?

  • 竞态条件。所谓的竞态条件指的是在多线程的情况下,每个线程的执行顺序不同,可能导致不同的结果,即有序性不能得到保证。下面是一段示例代码:
Java 复制代码
import java.util.ArrayList;
import java.util.List;

public class WaitWithoutSync {
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<>();

        Thread t1 = Thread.ofVirtual().name("t1_thread").start(()->{
            if(list.size() == 0){   // 1
                try {
                    list.wait();    // 2 
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("t1_thread end");
        });

        Thread t2 = Thread.ofVirtual().name("t2_thread").start(()->{
            list.add("str");  // 3
            if(list.size() > 0){ // 4
                list.notify();  // 5
            }
        });

        t1.join();
        t2.join();
    }
}

上面的代码片段其实是执行不了的,会报IllegalMonitorStateException。这里想说的是,线程t1和t2都没加同步锁,这样可能线程t1执行完 1 那一步的时候,线程t2获得时间片,一口气将3、4、5步都执行完了,这时线程t1再来执行2那一步,就会导致线程t1一直阻塞在那里,因为这时已经没有notify()方法来唤醒线程t1中的wait()方法了。这是wait()等方法必须在同步块的第一个原因,如果不在同步块中,线程执行顺序不能保证,容易导致异常

  • 原子性问题。我们知道wait()方法会做两件事情:释放锁和阻塞当前线程,若不在同步块中调用,不能保证这两个操作同时完成。想想线程A将锁释放了,但是还在执行,线程B随后获得了锁,也开始执行,会出现上面的竞态问题。

虚假唤醒问题是什么?

虚拟唤醒指的是在没有调用notify()/notifyAll()方法或没有收到中断信号的情况下,线程被莫名奇妙的从wait()状态唤醒了,这是一个底层操作系统或JVM实现可能导致的现象,并非Java设计缺陷,但开发者必须处理它。为了处理这种情况,需要在编码中多一步校验。

Java 复制代码
synchronized (lock) {
    // !condition 是类似 list.size() == 0这样的条件,这样意外唤醒后,能够被再次阻塞
    if (!condition) {  // 错误:用if而不是while
        lock.wait();   // 可能被虚假唤醒
    }
    // 执行后续操作(但condition可能仍未满足!)
}
相关推荐
玩代码14 分钟前
Spring Boot2 静态资源、Rest映射、请求映射源码分析
java·spring boot·源码分析·spring boot2
小白的代码日记21 分钟前
Java经典笔试题
java·开发语言
sakoba34 分钟前
nginx学习
java·运维·学习·nginx·基础
经典19921 小时前
Spring Boot 遇上 MyBatis-Plus:高效开发的奇妙之旅
java·spring boot·mybatis
1.01^10001 小时前
# 四、String与其他数据类型的转换:
java
rzl021 小时前
SpringBoot(黑马)
java·spring boot·后端
玖疯子1 小时前
PyCharm高效入门指南大纲
java·运维·服务器·apache·wordpress
香饽饽~、1 小时前
[第十三篇] Spring Boot监控
java·spring boot·后端
shawya_void2 小时前
算法:数组part02: 209. 长度最小的子数组 + 59.螺旋矩阵II + 代码随想录补充58.区间和 + 44. 开发商购买土地
java
javadaydayup2 小时前
Java注解底层竟然是个Map?
java