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可能仍未满足!)
}