1. 为什么需要线程间通信?
在Java多线程编程中,线程间通信(Inter-Thread Communication)是一个核心概念。想象一下,你正在指挥一个交响乐团——每个乐手就像独立的线程,他们需要根据指挥的手势(通信机制)来协调演奏。当多个线程需要协同完成某个任务时,就必须有某种机制让它们能够交换信息和协调行动。
1.1 线程隔离与共享内存的矛盾
每个Java线程都有自己的工作内存(Working Memory),这是Java内存模型(JMM)规定的。工作内存中存储了该线程使用到的变量的副本,这就导致了一个关键问题:一个线程对变量的修改,另一个线程未必能立即看到。
java复制// 典型的问题示例
public class VisibilityProblem {
private static boolean flag = true;
public static void main(String[] args) {
new Thread(() -> {
while (flag) {
// 空循环
}
System.out.println("线程1退出");
}).start();
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = false;
System.out.println("线程2修改flag");
}).start();
}
}
这段代码中,你可能会惊讶地发现"线程1退出"这条消息永远不会打印。这是因为线程1的工作内存中缓存了flag的值,即使线程2修改了主内存中的flag,线程1也无法感知。
1.2 实际应用场景
线程间通信的典型场景包括:
- 生产者-消费者模式:生产者线程生成数据,消费者线程处理数据
- 任务分发:主线程将任务分发给工作线程,并收集结果
- 事件处理:一个线程触发事件,另一个线程响应事件
- 资源共享:多个线程需要有序访问共享资源
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 基础通信机制:wait/notify
Java中最基础的线程通信机制是Object类提供的wait()、notify()和notifyAll()方法。这三个方法必须在一个同步代码块或同步方法中调用,因为它们依赖于对象的监视器锁(monitor lock)。
2.1 wait-notify的工作原理
java复制public class WaitNotifyDemo {
private final Object lock = new Object();
private boolean condition = false;
public void waitForCondition() throws InterruptedException {
synchronized (lock) {
while (!condition) {
lock.wait(); // 释放锁并等待
}
// 条件满足后继续执行
System.out.println("条件满足,继续执行");
}
}
public void setCondition() {
synchronized (lock) {
condition = true;
lock.notifyAll(); // 通知所有等待线程
}
}
}
关键点解析:
- wait()调用会释放对象锁,使当前线程进入WAITING状态
- notify()随机唤醒一个等待线程,notifyAll()唤醒所有等待线程
- 被唤醒的线程需要重新获取锁才能继续执行
- 条件检查应该使用while循环而不是if,防止虚假唤醒(spurious wakeup)
重要提示:永远在循环中检查条件,不要用if!这是Java官方文档明确建议的做法,因为线程可能在没有收到通知的情况下被唤醒(称为"虚假唤醒")。
2.2 常见错误与最佳实践
错误示例1:不在同步块中调用wait/notify
java复制// 错误代码!
public void wrongWait() throws InterruptedException {
lock.wait(); // 抛出IllegalMonitorStateException
}
错误示例2:忽略条件检查
java复制// 危险代码!
synchronized(lock) {
if (!condition) { // 应该用while而不是if
lock.wait();
}
}
最佳实践建议:
- 总是使用专用的锁对象(如private final Object),不要锁住this或类对象
- 考虑使用notifyAll()而不是notify(),避免某些线程永远不被唤醒
- 为wait()设置超时时间,防止永久等待:lock.wait(1000)
- 保持同步块尽可能短,减少锁竞争
3. 高级通信工具:Java并发工具类
Java 5引入的java.util.concurrent包提供了一系列更强大、更安全的线程通信工具。
3.1 CountDownLatch:一次性门闩
CountDownLatch就像一个倒计时门闩,允许一个或多个线程等待,直到其他线程完成一组操作。
java复制public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
final int THREAD_COUNT = 5;
CountDownLatch startSignal = new CountDownLatch(1);
CountDownLatch doneSignal = new CountDownLatch(THREAD_COUNT);
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(() -> {
try {
startSignal.await(); // 等待开始信号
System.out.println(Thread.currentThread().getName() + " 开始工作");
Thread.sleep((long) (Math.random() * 1000));
doneSignal.countDown(); // 完成工作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
}
System.out.println("主线程准备开始所有工作线程");
Thread.sleep(1000);
startSignal.countDown(); // 发出开始信号
doneSignal.await(); // 等待所有线程完成
System.out.println("所有工作线程已完成");
}
}
使用场景:
- 启动多个线程同时开始任务
- 等待多个线程全部完成后再继续
3.2 CyclicBarrier:可重复使用的栅栏
CyclicBarrier类似于CountDownLatch,但可以重复使用,并且支持在所有线程到达屏障后执行一个回调操作。
java复制public class CyclicBarrierDemo {
public static void main(String[] args) {
final int THREAD_COUNT = 3;
CyclicBarrier barrier = new CyclicBarrier(THREAD_COUNT, () -> {
System.out.println("所有线程已到达屏障,执行回调");
});
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " 到达屏障点1");
barrier.await();
System.out.println(Thread.currentThread().getName() + " 到达屏障点2");
barrier.await();
System.out.println(Thread.currentThread().getName() + " 完成");
} catch (Exception e) {
Thread.currentThread().interrupt();
}
}).start();
}
}
}
与CountDownLatch的区别:
- CyclicBarrier是可重置的,CountDownLatch是一次性的
- CyclicBarrier的计数器由线程自己递增,CountDownLatch由外部控制
- CyclicBarrier可以在所有线程到达后执行一个回调操作
3.3 Semaphore:控制并发数量的信号量
Semaphore维护一组许可证,用于控制同时访问某个资源的线程数量。
java复制public class SemaphoreDemo {
public static void main(String[] args) {
final int MAX_CONCURRENT = 3;
Semaphore semaphore = new Semaphore(MAX_CONCURRENT);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " 获取许可,剩余许可: " + semaphore.availablePermits());
Thread.sleep(1000);
semaphore.release();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
}
}
}
使用场景:
- 资源池管理(如数据库连接池)
- 限流控制
- 互斥锁(当许可证数量为1时)
4. 生产者-消费者模式的多种实现
生产者-消费者模式是线程通信的经典案例,我们来看几种不同的实现方式。
4.1 使用wait/notify实现
java复制public class TraditionalProducerConsumer {
private final Queue<Integer> queue = new LinkedList<>();
private final int MAX_SIZE = 5;
private final Object lock = new Object();
class Producer implements Runnable {
@Override
public void run() {
int value = 0;
while (true) {
synchronized (lock) {
while (queue.size() == MAX_SIZE) {
try {
lock.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
queue.offer(value++);
System.out.println("生产: " + value);
lock.notifyAll();
}
try {
Thread.sleep((long) (Math.random() * 1000));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}
}
class Consumer implements Runnable {
@Override
public void run() {
while (true) {
synchronized (lock) {
while (queue.isEmpty()) {
try {
lock.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
int value = queue.poll();
System.out.println("消费: " + value);
lock.notifyAll();
}
try {
Thread.sleep((long) (Math.random() * 1000));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}
}
}
4.2 使用BlockingQueue实现
Java的BlockingQueue接口及其实现类(如ArrayBlockingQueue、LinkedBlockingQueue)内部已经实现了线程安全的通信机制。
java复制public class BlockingQueueProducerConsumer {
private final BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(5);
class Producer implements Runnable {
@Override
public void run() {
int value = 0;
while (true) {
try {
queue.put(value++);
System.out.println("生产: " + value);
Thread.sleep((long) (Math.random() * 1000));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}
}
class Consumer implements Runnable {
@Override
public void run() {
while (true) {
try {
int value = queue.take();
System.out.println("消费: " + value);
Thread.sleep((long) (Math.random() * 1000));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}
}
}
BlockingQueue的优势:
- 代码更简洁,不需要手动处理wait/notify
- 提供了更多有用方法(如offer、poll带超时版本)
- 可以选择公平策略,避免线程饥饿
4.3 使用Exchanger实现
Exchanger允许两个线程在某个点交换数据,适用于"生产-消费"一对一的情况。
java复制public class ExchangerDemo {
public static void main(String[] args) {
Exchanger<String> exchanger = new Exchanger<>();
new Thread(() -> {
try {
String data = "数据A";
System.out.println(Thread.currentThread().getName() + " 发送: " + data);
String received = exchanger.exchange(data);
System.out.println(Thread.currentThread().getName() + " 收到: " + received);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "线程A").start();
new Thread(() -> {
try {
String data = "数据B";
System.out.println(Thread.currentThread().getName() + " 发送: " + data);
String received = exchanger.exchange(data);
System.out.println(Thread.currentThread().getName() + " 收到: " + received);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "线程B").start();
}
}
5. 线程通信中的常见陷阱与优化
5.1 死锁问题
线程通信中最危险的问题就是死锁。下面是一个典型的死锁示例:
java复制public class DeadlockDemo {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
public void method1() {
synchronized (lock1) {
synchronized (lock2) {
System.out.println("method1执行");
}
}
}
public void method2() {
synchronized (lock2) {
synchronized (lock1) {
System.out.println("method2执行");
}
}
}
}
避免死锁的策略:
- 按固定顺序获取锁
- 使用tryLock()尝试获取锁,并设置超时
- 减少锁的粒度
- 使用更高级的并发工具代替显式锁
5.2 性能优化建议
-
减少锁竞争:
- 缩小同步块的范围
- 使用读写锁(ReentrantReadWriteLock)替代独占锁
- 考虑使用无锁数据结构(如ConcurrentHashMap)
-
避免过早优化:
- 先保证正确性,再考虑性能
- 使用性能分析工具(如JProfiler)定位真正的瓶颈
-
线程池的使用:
- 合理配置线程池大小
- 根据任务类型选择不同的线程池(如IO密集型 vs CPU密集型)
java复制// 更优的线程池配置示例
public class BetterThreadPool {
public static void main(String[] args) {
// 根据CPU核心数设置线程池大小
int coreCount = Runtime.getRuntime().availableProcessors();
ExecutorService executor = Executors.newFixedThreadPool(coreCount);
// 使用CompletionService处理任务结果
CompletionService<String> completionService = new ExecutorCompletionService<>(executor);
for (int i = 0; i < 10; i++) {
final int taskId = i;
completionService.submit(() -> {
Thread.sleep((long) (Math.random() * 1000));
return "任务" + taskId + "完成";
});
}
for (int i = 0; i < 10; i++) {
try {
Future<String> future = completionService.take();
System.out.println(future.get());
} catch (InterruptedException | ExecutionException e) {
Thread.currentThread().interrupt();
break;
}
}
executor.shutdown();
}
}
5.3 调试技巧
-
线程转储分析:
- 使用jstack命令获取线程转储
- 分析线程状态(RUNNABLE, BLOCKED, WAITING等)
- 查找死锁或长时间等待的线程
-
可视化工具:
- JConsole:监控线程状态和死锁
- VisualVM:分析线程转储和性能
- YourKit:商业级性能分析工具
-
日志记录:
- 为关键同步点添加日志
- 使用ThreadLocal为每个线程维护独立的日志上下文
java复制// 使用ThreadLocal维护线程特定信息
public class ThreadLocalLogger {
private static final ThreadLocal<SimpleDateFormat> dateFormat =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"));
public void log(String message) {
String timestamp = dateFormat.get().format(new Date());
System.out.println(timestamp + " [" + Thread.currentThread().getName() + "] " + message);
}
}
在实际项目中,线程间通信的选择应该基于具体需求:
- 对于简单的协调,wait/notify可能足够
- 对于复杂的同步场景,优先考虑java.util.concurrent中的高级工具
- 对于数据交换,BlockingQueue通常是首选
- 考虑使用不可变对象和线程封闭技术减少同步需求
记住,多线程编程的第一原则是保证正确性,然后才是性能优化。每次添加同步机制时,都要问自己:这真的是必要的吗?有没有更简单、更安全的方式?
