在多线程程序中,多个线程经常需要协作完成一项任务,例如生产者线程负责生成数据,消费者线程负责处理数据。如果仅仅使用普通的成员变量在线程之间传递信息,很容易因为Java内存模型带来的可见性延迟和指令重排序,导致一个线程修改了数据,另一个线程却读到了旧值,进而产生难以排查的业务逻辑错误。因此,掌握线程间安全通信的机制,是写出健壮并发程序的基础。

一、使用volatile实现轻量级状态通信
volatile是Java提供的最轻量级的线程间通信手段之一。被volatile修饰的变量,保证了不同线程对该变量操作时的可见性,即一个线程对其的修改会立即刷新到主内存,其他线程读取时也会从主内存重新加载。同时,volatile禁止了特定场景下的指令重排序,从而满足了一定的happens-before关系。
不过,volatile只适合用来传递简单的状态标志,比如开关、阶段标记,不适合用于复合操作(如i++)。下面示例展示了一个通过volatile变量控制工作线程退出的典型用法:
public class Worker extends Thread {
// 使用volatile保证停止信号对所有线程可见
private volatile boolean running = true;
public void stopWork() {
running = false;
}
@Override
public void run() {
while (running) {
// 执行具体任务
System.out.println("工作中...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.println("线程已安全退出");
}
public static void main(String[] args) throws Exception {
Worker w = new Worker();
w.start();
Thread.sleep(3000);
w.stopWork();
}
}
上述代码中,主线程调用stopWork方法修改running值,工作线程能够立刻感知并结束循环。这种方式简单高效,但如果线程间需要传递复杂数据或需要等待某个条件成立,volatile就力不从心了。
二、基于synchronized与wait、notify的协作
当线程需要等待某个条件满足时,可以使用对象监视器锁配合wait、notify和notifyAll方法。调用这些方法的前提是线程必须已经持有该对象的synchronized锁,否则会抛出IllegalMonitorStateException。wait会让当前线程释放锁并进入等待队列,直到其他线程调用同一对象的notify将其唤醒。
下面模拟一个简易的消息队列,生产者往里面放消息,消费者在队列为空时等待:
import java.util.LinkedList;
public class SafeQueue {
private final LinkedList<String> queue = new LinkedList<>();
private final int maxSize = 5;
public synchronized void put(String msg) throws InterruptedException {
// 使用while而非if,防止虚假唤醒
while (queue.size() == maxSize) {
wait();
}
queue.addLast(msg);
notifyAll();
}
public synchronized String take() throws InterruptedException {
while (queue.isEmpty()) {
wait();
}
String msg = queue.removeFirst();
notifyAll();
return msg;
}
}
这里必须注意两点。第一,等待条件判断要用while循环而不是if,因为线程被唤醒后条件可能由于其他线程操作又不再满足,这就是虚假唤醒问题。第二,尽量使用notifyAll而不是notify,notify只随机唤醒一个线程,可能导致本该被唤醒的线程永远等待。
虽然synchronized加wait、notify可以实现通信,但代码容易写错,且锁的竞争激烈时性能一般。在复杂场景下,更推荐使用并发包中的工具类。
三、利用并发容器与阻塞队列解耦通信
Java并发包(java.util.concurrent)提供了大量线程安全且高效的通信组件,其中最常用的是各类阻塞队列(BlockingQueue)。阻塞队列内部已经处理了所有的加锁与等待逻辑,生产者往满队列放数据时会自动阻塞,消费者从空队列取数据也会自动阻塞,使用者完全不需要关心底层同步细节。
以下代码演示了使用ArrayBlockingQueue让两个线程安全传递数据:
import java.util.concurrent.ArrayBlockingQueue;
public class BlockingDemo {
public static void main(String[] args) {
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
Thread producer = new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
queue.put("消息" + i);
System.out.println("生产了消息" + i);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread consumer = new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
String msg = queue.take();
System.out.println("消费了" + msg);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
}
}
使用阻塞队列后,生产者和消费者之间实现了完全解耦,代码清晰且不容易出错。除了队列,ConcurrentHashMap、CountDownLatch、CyclicBarrier等也能在不同场景中承担线程间数据交换和进度协调的职责。
在实际选型时,如果只是控制运行状态的开关,volatile足够;如果需要条件等待,用synchronized配合wait、notify并小心处理虚假唤醒;如果涉及数据传递与任务协作,优先采用BlockingQueue等并发容器,既安全又易于维护。