1. 线程安全问题的本质与危害
我第一次遭遇线程安全问题是在一个电商促销系统里。凌晨三点,客服主管打来电话:"库存显示卖出了2000件商品,但仓库实际只找到1800件的订单记录!"这个血淋淋的教训让我深刻理解了线程安全的重要性。
线程安全问题的本质在于:当多个线程同时访问共享资源时,如果没有正确的同步机制,会导致数据状态出现不可预测的错误。就像十字路口没有红绿灯,车辆就会发生碰撞。在计算机系统中,这种"碰撞"表现为数据损坏、逻辑错误甚至系统崩溃。
典型的问题场景包括:
- 计数器数值异常(如库存少算)
- 集合元素丢失(如订单漏单)
- 状态不一致(如支付成功但订单未更新)
- 死锁(系统完全卡死)
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 线程安全问题的三大根源
2.1 竞态条件(Race Condition)
当多个线程对共享数据的操作顺序影响最终结果时,就会发生竞态条件。比如两个线程同时执行"读取-修改-写入"操作:
java复制// 线程不安全的计数器
public class UnsafeCounter {
private int value;
public void increment() {
value++; // 实际包含读取-加1-写入三个步骤
}
}
如果线程A和B同时读取value为10,各自加1后都写入11,最终结果应该是12却得到11。我在金融系统中就遇到过因此导致的金额计算错误。
2.2 内存可见性问题
由于CPU缓存的存在,一个线程对共享变量的修改可能不会立即被其他线程看到。例如:
java复制public class VisibilityProblem {
private boolean flag = true;
public void worker() {
while (flag) { // 可能永远看不到主线程的修改
// do work
}
}
public void stop() {
flag = false;
}
}
这个案例中,worker线程可能永远无法退出循环,因为flag的修改对worker不可见。我在日志收集系统中就踩过这个坑。
2.3 指令重排序
编译器和处理器会优化指令执行顺序,可能导致意想不到的结果。经典案例是双重检查锁定(DCL)的单例模式:
java复制public class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 问题出在这里!
}
}
}
return instance;
}
}
由于new操作可能被重排序,其他线程可能拿到未初始化完成的对象。这个陷阱让我们的系统在高峰期出现了诡异的NPE。
3. 线程安全的解决方案
3.1 不可变对象
最简单的线程安全方案就是让对象不可变。比如String类:
java复制public final class String {
private final char value[];
// 所有方法都不修改value数组
}
我在配置中心系统里大量使用不可变配置对象,既安全又高效。但要注意"伪不可变"对象:
java复制public final class FakeImmutable {
private final List<String> list = new ArrayList<>();
public List<String> getList() {
return list; // 虽然引用不可变,但内容可变!
}
}
3.2 同步控制
3.2.1 synchronized关键字
最基本的同步工具,但要注意:
- 锁对象选择(不要用String等常量池对象)
- 锁粒度控制(过大会影响性能)
- 避免嵌套锁导致的死锁
java复制public class SafeCounter {
private int value;
private final Object lock = new Object(); // 专用锁对象
public void increment() {
synchronized (lock) { // 细粒度锁
value++;
}
}
}
3.2.2 volatile变量
适用于一写多读场景,保证可见性但不保证原子性:
java复制public class VolatileExample {
private volatile boolean shutdown;
public void shutdown() {
shutdown = true; // 对所有线程立即可见
}
public void doWork() {
while (!shutdown) {
// 正常工作
}
}
}
3.2.3 原子类
Java提供的AtomicInteger等类,使用CAS实现无锁线程安全:
java复制public class AtomicCounter {
private final AtomicInteger value = new AtomicInteger();
public void increment() {
value.incrementAndGet(); // 原子操作
}
}
在高并发计数场景,原子类比synchronized性能高5-10倍。
3.3 并发容器
JDK提供的线程安全容器:
| 非线程安全类 | 线程安全替代方案 |
|---|---|
| ArrayList | CopyOnWriteArrayList |
| HashMap | ConcurrentHashMap |
| HashSet | ConcurrentHashMap.KeySetView |
| LinkedList | ConcurrentLinkedQueue |
特别推荐ConcurrentHashMap的分段锁设计,我在用户会话管理系统里用它实现了每秒10万+的并发更新。
4. 实战中的经验与陷阱
4.1 性能优化误区
我曾为了性能去掉了一个"不必要的"synchronized,结果导致线上数据混乱。教训是:
- 先保证正确性,再考虑优化
- 用JMH进行基准测试,不要靠猜测
- 优先考虑无锁方案(如原子类)
4.2 死锁预防
死锁的四个必要条件:
- 互斥条件
- 占有并等待
- 不可抢占
- 循环等待
预防措施:
- 锁排序:按固定顺序获取锁
- 锁超时:tryLock设置超时时间
- 使用更高级的并发工具(如ReentrantLock)
4.3 线程池注意事项
java复制// 错误示范:使用无界队列
ExecutorService pool =
new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>()); // OOM风险!
// 正确做法
ExecutorService safePool =
new ThreadPoolExecutor(10, 10, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000), // 有界队列
new ThreadPoolExecutor.CallerRunsPolicy()); // 拒绝策略
我们的邮件发送系统就曾因无界队列导致内存溢出。
5. 现代并发编程实践
5.1 CompletableFuture组合异步任务
java复制CompletableFuture.supplyAsync(() -> getOrderFromDB(orderId))
.thenApplyAsync(order -> calculatePrice(order))
.thenAcceptAsync(price -> sendNotification(price))
.exceptionally(ex -> {
log.error("处理失败", ex);
return null;
});
这种声明式编程比传统回调清晰多了,在我们的支付对账系统中大大简化了代码。
5.2 StampedLock优化读多写少场景
java复制public class Point {
private double x, y;
private final StampedLock lock = new StampedLock();
public double distanceFromOrigin() {
long stamp = lock.tryOptimisticRead(); // 乐观读
double currentX = x, currentY = y;
if (!lock.validate(stamp)) { // 检查是否被修改
stamp = lock.readLock(); // 退化为悲观读
try {
currentX = x;
currentY = y;
} finally {
lock.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}
在我们的GPS轨迹分析服务中,这种锁使读取性能提升了3倍。
5.3 协程(虚拟线程)探索
Java 19引入的虚拟线程可以大幅提升IO密集型任务的吞吐量:
java复制try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return i;
});
});
} // 这里会等待所有任务完成
我们在文件处理服务中测试发现,相比传统线程池,虚拟线程可以支持10倍以上的并发量。
