线程安全是多线程领域的问题,线程安全可以简单理解为一个方法或者一个实例可以在多线程环境中使用而不会出现问题。

在 Java 多线程编程当中,提供了多种实现 Java 线程安全的方式:

  • 最简单的方式,使用 Synchronization 关键字
  • 使用 java.util.concurrent.atomic 包中的原子类,例如 AtomicInteger
  • 使用 java.util.concurrent.locks 包中的锁
  • 使用线程安全的集合 ConcurrentHashMap
  • 使用 volatile 关键字,保证变量可见性(直接从内存读,而不是从线程 cache 读)

工具清单齐了,但”什么问题需要这些工具”才是起点。线程安全问题的根源只有三个来源:原子性、可见性、有序性。本文按”三源定位 → 工具匹配 → 设计优先级”展开,把清单变成决策路径。

三大根源与典型症状

  • 原子性:操作序列被线程切换打断。症状:计数器少算(i++ 丢失更新)、check-then-act 竞态(先判空再 put 出现重复)、懒加载双实例
  • 可见性:一个线程的写对另一个线程不可见(各自 CPU 缓存副本)。症状:A 线程改了 flag,B 线程循环永远读旧值”停不下来”
  • 有序性:编译器/处理器指令重排。症状:双重检查锁(DCL)拿到”半初始化对象”——引用已赋值但构造未完成
// 原子性反例:count++ 是读-改-写三步,并发下丢失更新
count++;
// 可见性反例:无 volatile 时循环可能永不退出
while (!stop) { doWork(); }

工具与根源的匹配表

工具 解决 代价/边界
synchronized 原子+可见+有序(全管) 竞争时阻塞;JDK6 后性能已优化
ReentrantLock 等 同上 + 超时/中断/公平/多条件 必须 finally unlock
atomic 包(CAS) 原子(单变量复合操作) 只覆盖单变量;高竞争自旋开销
volatile 可见+有序 不保证原子:volatile count++ 仍错
并发集合 容器级原子+可见 复合操作需用 putIfAbsent/compute
ThreadLocal 避开共享(空间隔离) 泄漏/串味需 remove 规范
不可变对象 三源全免 状态变化需整体替换引用

最常考的陷阱是最后一列第一行:volatile 只治可见性与有序性,不治原子性。volatile int count; count++ 并发下依旧丢更新,因为 ++ 的读-改-写三步之间可被切换。

设计优先级:能不加锁就不加锁

按成本从低到高的防御顺序:

  1. 不共享:线程封闭(方法局部变量)、ThreadLocal(注意 remove)
  2. 不可变:final 字段 + 无 setter + 防御性拷贝;配置对象、值对象首选
  3. 无锁原子:AtomicInteger/LongAdder/原子引用,单变量复合操作
  4. 并发容器:CHM/ConcurrentLinkedQueue 替代手写同步集合
  5. 锁:synchronized 优先(不会忘释放);需要超时/中断/多条件才上 Lock
  6. 缩小临界区:锁内只做共享状态操作,IO/远程调用移出

这个顺序的本质是:越靠前的方案越把并发问题”设计掉”,越靠后的方案越是在”管理”并发。管理总有成本与遗漏,设计掉才是一劳永逸。

识别线程安全需求的三个问题

  • 这段代码会被多线程执行吗?(Servlet/线程池/并行流 = 是;main 单线程脚本 = 否)
  • 共享可变状态在哪?(成员变量、static、容器、外部缓存)——局部变量天然线程封闭
  • 操作是原子的吗?(单步读写基本类型可见性够;复合操作必须同步)

三个问题答完,要不要加锁、加在哪一层就清楚了。

一个综合案例

class Counter {
private final LongAdder adder = new LongAdder(); // 无锁原子,高并发计数首选
public void inc() { adder.increment(); }
public long sum() { return adder.sum(); } // 注意:sum 非瞬时一致快照
}

对比 synchronized 方法版:无阻塞、无锁升级开销;代价是 sum() 读到的是”过程中”的值——并发工具往往用一致性强度换吞吐,选型时先问业务能接受多弱的一致性。

线程安全不是”加不加锁”的二选题,而是”三源定位 → 按成本选工具 → 优先设计掉共享”的决策链。清单里的五种方式各有管辖范围,用错范围(如 volatile 当原子用)比不用更危险。