作为一名摸爬滚打5年的全栈老司机,我见过太多新手在多线程通信上栽跟头。明明代码逻辑看起来天衣无缝,运行时却莫名其妙卡死、数据错乱,甚至直接崩溃。别慌,今天咱们就用最接地气的方式,把Java多线程通信那点事儿彻底讲透。

一、为什么线程间需要通信?
想象一下餐厅后厨的工作场景:厨师(线程A)做好菜后,需要通知服务员(线程B)上菜。如果厨师闷头做菜不吭声,服务员可能一直在发呆,菜都凉了还没端走;如果服务员不停问"好了没",又会浪费精力。这就是典型的线程协作问题——不同线程需要按照特定顺序配合完成工作。
Java多线程通信的核心痛点集中在三点:
- 线程执行顺序不可控(谁先谁后全看CPU心情)
- 共享资源访问冲突(俩人同时改一个数据)
- 状态可见性问题(A改了数据B却看不见)
二、实战:wait()/notify()的经典用法
先来看最基础的等待通知机制。下面这个例子模拟生产者-消费者场景,生产者生产数据后通知消费者,消费者处理完后通知生产者:
public class WaitNotifyDemo {
private String message;
private boolean empty = true;
// 生产者方法
public synchronized void produce(String msg) {
while (!empty) {
try {
wait(); // 等待消费者消费
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
message = msg;
empty = false;
notify(); // 唤醒消费者线程
System.out.println("生产了: " + msg);
}
// 消费者方法
public synchronized String consume() {
while (empty) {
try {
wait(); // 等待生产者生产
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
String msg = message;
empty = true;
notify(); // 唤醒生产者线程
System.out.println("消费了: " + msg);
return msg;
}
}
关键要点:
- wait()和notify()必须在synchronized代码块内调用
- 一定要用while循环检查条件,不要用if(避免虚假唤醒)
- notify()随机唤醒一个线程,notifyAll()唤醒所有等待线程
三、更现代的通信方式:Lock和Condition
JDK5提供的Lock接口及其配套的Condition对象,提供了更灵活的线程通信机制:
public class LockConditionDemo {
private final Lock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private final Condition notFull = lock.newCondition();
private String[] items = new String[10];
private int putPtr, takePtr, count;
public void produce(String item) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
notFull.await(); // 队列满时等待
}
items[putPtr] = item;
if (++putPtr == items.length) putPtr = 0;
count++;
notEmpty.signal(); // 唤醒消费者
} finally {
lock.unlock();
}
}
public String consume() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await(); // 队列空时等待
}
String item = items[takePtr];
if (++takePtr == items.length) takePtr = 0;
count--;
notFull.signal(); // 唤醒生产者
return item;
} finally {
lock.unlock();
}
}
}
使用Condition的优势:
- 可以创建多个Condition实现精确通知(比如单独通知生产者或消费者)
- 支持超时等待(awaitNanos()/awaitUntil())
- 支持中断响应(lockInterruptibly())
四、避坑指南:常见问题及解决方案
1. 死锁问题
两个线程互相等待对方释放锁,就像两个人相遇在窄桥上都等着对方先过。
解决方案:使用tryLock()尝试获取锁,设置超时时间
if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
// 业务逻辑
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
2. 活锁问题
线程不断重试失败的操作,就像两个人试图让路却总是同步移动。
解决方案:引入随机退避机制
// 失败后随机等待一段时间再重试
Thread.sleep((long) (Math.random() * 100));
3. 资源竞争
多个线程同时修改共享数据导致数据不一致。
解决方案:使用线程安全容器或原子变量
// 使用ConcurrentHashMap代替HashMap
private ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 使用AtomicInteger代替int
private AtomicInteger counter = new AtomicInteger(0);
五、最佳实践总结
- 优先使用高层工具:CountDownLatch、CyclicBarrier、Semaphore等并发工具能解决90%的通信需求
- 避免过早优化:不要一开始就用复杂锁机制,先用简单方案验证
- 锁粒度要小:尽量缩小同步代码块的范围
- 使用线程池:避免频繁创建销毁线程,推荐使用ThreadPoolExecutor
- 善用volatile:保证变量的可见性(但不保证原子性)
建议新手先从wait()/notify()机制练起,理解基本通信原理后再学习更高级的并发工具。多线程调试虽然麻烦,但掌握了正确方法后,你会发现它并没有想象中那么可怕。
记住:好的多线程程序不是没有bug,而是能够 predictable(可预测)和reproducible(可复现)——这才是我们追求的终极目标。


评论