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可能仍未满足!)
}
相关推荐
一只乔哇噻1 分钟前
java后端工程师进修ing(研一版 || day41)
java·开发语言·学习·算法
User_芊芊君子19 分钟前
【Java】设计模式——单例、工厂、代理模式
java·设计模式·代理模式
2301_8035545228 分钟前
正向代理,反向代理,负载均衡还有nginx
java·nginx·负载均衡
要开心吖ZSH29 分钟前
软件设计师备考-(十六)数据结构及算法应用(重要)
java·数据结构·算法·软考·软件设计师
向上的车轮37 分钟前
基于Java Spring Boot的云原生TodoList Demo 项目,验证云原生核心特性
java·spring boot·云原生
程序员清风39 分钟前
快手一面:为什么要求用Static来修饰ThreadLocal变量?
java·后端·面试
逍遥德40 分钟前
Java8 Comparator接口 和 List Steam 排序使用案例
java·spring boot·list·排序算法
前行的小黑炭1 小时前
Android :如何快速让布局适配手机和平板?
android·java·kotlin
_BugMan2 小时前
【IDEA】干活?一个IDEA即可,集成开发平台打造攻略
java·ide·intellij-idea
YA3333 小时前
java设计模式二、工厂
java·开发语言·设计模式