1. 线程通信的本质与价值
在多线程编程的世界里,线程通信就像办公室里的同事协作。想象这样一个场景:财务部的张三需要市场部的李四提供销售数据才能完成报表,而李四又需要张三确认预算才能开展推广活动。如果两人都埋头各干各的,整个公司的运作就会陷入混乱。Java线程间的通信机制,就是为解决这类协同问题而生的核心技术。
我处理过最典型的案例是一个电商秒杀系统。库存扣减线程必须实时通知订单线程库存状态,同时支付线程又需要等待订单线程生成订单号后才能执行扣款。没有高效的线程通信,整个系统就会像失去指挥的交响乐团——每个乐手都在演奏,但合在一起就是噪音。
2. 共享内存:最直接的对话方式
2.1 同步代码块的秘密握手
synchronized关键字就像会议室的门锁,保证同一时间只有一个人能进去修改白板上的数据。我曾用这个方案解决过支付系统的余额并发修改问题:
java复制public class Account {
private double balance;
public synchronized void deposit(double amount) {
balance += amount; // 这个操作现在原子化了
}
}
但这里有个坑:同步范围过大反而会降低性能。有次排查系统卡顿,发现有人把整个HTTP请求处理都加了synchronized,导致QPS直接腰斩。经验法则是——锁的粒度要尽可能小,只保护真正需要互斥的操作。
2.2 volatile变量的可见性魔法
volatile变量就像办公室的公告栏,任何人的修改都会立即被所有人看到。去年优化过一个监控系统,就用它来优雅地停止工作线程:
java复制public class Worker implements Runnable {
private volatile boolean running = true;
public void stop() {
running = false;
}
@Override
public void run() {
while(running) {
// 执行监控任务
}
}
}
但要注意,volatile只能保证可见性,不能保证复合操作的原子性。有次线上事故就是因为误以为volatile int可以安全地做计数器,结果出现了少计数的情况。
3. 等待/通知机制:线程的专属信号灯
3.1 wait/notify的经典范式
这组方法就像医院的分诊系统——患者(线程)拿号等待,医生(另一个线程)叫号通知。在物流调度系统中,我是这样实现车辆和货物的匹配:
java复制public class DispatchCenter {
private final Object lock = new Object();
private boolean goodsReady = false;
public void waitForGoods() throws InterruptedException {
synchronized(lock) {
while(!goodsReady) {
lock.wait(); // 释放锁进入等待
}
// 货物到位后的处理逻辑
}
}
public void notifyGoodsReady() {
synchronized(lock) {
goodsReady = true;
lock.notifyAll();
}
}
}
血泪教训:一定要用while循环检查条件,而不是if!有次因为用错导致通知丢失,系统卡死了两小时。因为notify可能被意外唤醒,循环检查才是王道。
3.2 Condition对象的精准控制
Condition就像高级版的wait/notify,可以建立多个等待队列。在实现多优先级任务队列时,我用它来区分紧急和普通任务:
java复制public class TaskQueue {
private final Lock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private final Condition urgentCondition = lock.newCondition();
public void putUrgentTask(Task task) {
lock.lock();
try {
// 添加紧急任务
urgentCondition.signal();
} finally {
lock.unlock();
}
}
public Task take() throws InterruptedException {
lock.lock();
try {
while(queue.isEmpty()) {
notEmpty.await();
}
// 优先处理紧急任务
return queue.poll();
} finally {
lock.unlock();
}
}
}
4. 线程安全的集合:现成的通信管道
4.1 BlockingQueue的生产者-消费者模式
BlockingQueue就像工厂的传送带,天然适合生产者消费者场景。在日志收集系统中,我是这样设计的:
java复制public class LogSystem {
private final BlockingQueue<String> queue = new ArrayBlockingQueue<>(1000);
// 日志收集线程
public void collectLog(String log) {
queue.put(log); // 队列满时自动阻塞
}
// 日志处理线程
public void processLog() {
while(true) {
String log = queue.take(); // 队列空时自动等待
// 处理日志
}
}
}
关键参数是队列容量。有次设置太小导致生产者频繁阻塞,设置太大又引发OOM。经过压测,最终确定1000是最佳平衡点。
4.2 ConcurrentHashMap的高并发妙用
这个并发Map就像共享的白板,多个线程可以安全地读写。在实现全局缓存时,我用了这样的模式:
java复制public class UserCache {
private final ConcurrentHashMap<Long, User> cache = new ConcurrentHashMap<>();
public User getOrLoad(Long userId) {
return cache.computeIfAbsent(userId, id -> {
// 缓存不存在时加载数据库
return loadFromDB(id);
});
}
}
但要注意,computeIfAbsent里的加载逻辑不能太耗时,否则会阻塞其他线程访问这个key。有次数据库查询慢导致整个缓存响应延迟,后来改成了异步加载模式。
5. 高级通信工具:CountDownLatch与CyclicBarrier
5.1 CountDownLatch的起跑枪
这个工具就像运动会上的发令枪,所有运动员(线程)准备好后统一开跑。在微服务启动时,我用它来协调多个组件的初始化:
java复制public class ServiceInitializer {
private final CountDownLatch latch = new CountDownLatch(3);
public void init() throws InterruptedException {
new Thread(this::initDB).start();
new Thread(this::initCache).start();
new Thread(this::initMQ).start();
latch.await(); // 等待所有组件初始化完成
System.out.println("所有服务启动完毕");
}
private void initDB() {
// 初始化数据库
latch.countDown();
}
// 其他init方法类似
}
曾经犯过的错:在countDown前没有检查初始化是否真的成功,导致系统带着隐患运行。后来加上了状态检查逻辑。
5.2 CyclicBarrier的团队协作
这个屏障就像旅游团的集合点,所有人到齐后才能前往下一个景点。在批量数据处理时,我是这样使用的:
java复制public class DataBatchProcessor {
private final CyclicBarrier barrier;
private final List<Worker> workers;
public DataBatchProcessor(int workerCount) {
this.barrier = new CyclicBarrier(workerCount, () -> {
// 所有worker完成一批数据处理后的回调
System.out.println("一批数据处理完成");
});
this.workers = IntStream.range(0, workerCount)
.mapToObj(i -> new Worker(barrier))
.collect(Collectors.toList());
}
class Worker implements Runnable {
private final CyclicBarrier barrier;
Worker(CyclicBarrier barrier) {
this.barrier = barrier;
}
@Override
public void run() {
while(hasMoreData()) {
processData();
barrier.await(); // 等待其他worker
}
}
}
}
6. 实战中的陷阱与解决方案
6.1 死锁的四种常见场景
-
锁顺序死锁:就像两个人在狭窄的走廊相遇,都等着对方先让路。解决方案是统一锁的获取顺序。
-
资源死锁:线程A持有锁1等锁2,线程B持有锁2等锁1。用
tryLock设置超时可以避免:
java复制if(lock1.tryLock(1, TimeUnit.SECONDS)) {
try {
if(lock2.tryLock(1, TimeUnit.SECONDS)) {
try {
// 临界区
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
-
线程饥饿死锁:单线程池中提交的任务又向同一个线程池提交任务。解决方案是用不同的线程池。
-
协作对象死锁:在持有锁时调用外部方法,而外部方法又尝试获取另一个锁。应该先释放锁再调用。
6.2 性能优化实战记录
在订单系统中,通过以下优化将吞吐量提升了3倍:
- 将
synchronized改为ReentrantLock的公平模式,减少线程饥饿 - 使用
ConcurrentHashMap替代Collections.synchronizedMap - 对读多写少的数据采用读写锁(
ReentrantReadWriteLock) - 用
ThreadLocal保存线程本地变量,减少竞争
关键指标变化:
| 优化前 | 优化后 |
|---|---|
| 800 TPS | 2400 TPS |
| 平均延迟120ms | 平均延迟45ms |
| CPU利用率85% | CPU利用率65% |
7. 线程通信的最佳实践
- 优先选择高层工具:能用
BlockingQueue就别自己实现wait/notify - 最小化同步范围:锁的粒度要尽可能小
- 优先考虑不变性:使用不可变对象可以避免同步
- 文档化锁策略:在代码中明确记录哪些锁保护哪些数据
- 避免在持有锁时调用外部方法:这是死锁的常见根源
- 考虑使用消息传递:如Akka等actor模型框架
在最近的一个物联网项目中,我最终采用了混合方案:
- 设备状态更新使用
CopyOnWriteArrayList - 命令下发使用
LinkedBlockingQueue - 全局配置使用
ConcurrentHashMap - 批量数据处理使用
CyclicBarrier
这种组合在各种压力测试中表现稳定,CPU利用率保持在健康水平。