本文目录导读:

这是一份 Java并发安全流程规范,这份规范旨在帮助开发团队在设计、编码和审查阶段系统性地规避并发问题(如竞态条件、死锁、内存一致性错误),该规范涵盖了从“共享资源识别”到“线程协作”的完整流程。
核心原则
- 最小化共享:优先设计无共享(无状态Bean、ThreadLocal)或只读共享(
final、不可变对象)的架构。 - 封装可变性:可变状态必须通过明确的锁或原子变量访问。
- 文档先行:每个可能被多线程访问的类或方法,必须明确标注其线程安全策略。
并发安全流程规范
阶段 1:识别与建模
- 步骤 1:识别共享资源
- 检查:静态变量、实例域(特别是集合、缓存、计数器)、文件句柄、数据库连接池、HttpSession。
- 行动:在类头部注释中列出所有共享的可变状态。
- 步骤 2:选择并发策略
- 线程封闭(推荐优先考虑):栈封闭(局部变量)、
ThreadLocal。 - 只读共享:
final字段(不可变对象如String、Integer、自定义record/不可变类)。 - 线程安全对象:使用
ConcurrentHashMap、CopyOnWriteArrayList、AtomicInteger、LongAdder等。 - 同步策略:
synchronized(隐式锁)、ReentrantLock、ReadWriteLock、StampedLock。
- 线程封闭(推荐优先考虑):栈封闭(局部变量)、
- 步骤 3:定义可见性保证
- 必须使用
volatile或锁来确保写操作的可见性。 - 规则:如果变量被一个线程写入并被另一个线程读取,必须使用
happens-before机制。
- 必须使用
阶段 2:编码实现规范
规则 1:锁的使用
- 锁粒度:
- 避免在同步块中执行耗时操作(如I/O、RPC调用)。
- 对同一共享资源,应使用同一个锁对象。
- 边界:能锁方法尽量不锁类,能锁代码块尽量不锁方法。
- 锁顺序:若要获取多个锁,必须定义全局顺序,否则易死锁。
- 例子:固定先锁
A再锁B。
- 例子:固定先锁
- 锁降级/释放:
- 对于
Lock接口,必须在finally块中释放锁。
- 对于
- 饥饿与公平:默认
ReentrantLock为非公平锁,高竞争场景可考虑公平锁(性能代价较高)。
// 正确示例
Lock lock = new ReentrantLock();
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}
规则 2:复合操作
- 对于“检查再运行”(Check-Then-Act)或“读取-修改-写入”复合操作,必须加锁或使用原子类。
- 错误示例:
if (!map.containsKey(key)) { // 检查 map.put(key, value); // 运行 -> 竞态条件! } - 正确方案:
- 使用
ConcurrentHashMap.putIfAbsent(key, value)。 - 使用
synchronized(map) { if(!map.containsKey(key)) map.put(key,value); }。
- 使用
规则 3:发布与逸出
- 禁止在构造函数中将
this引用发布给外部(如注册监听器、启动线程)。- 原因:对象尚未完全初始化,其他线程可能看到半构造状态。
- 正确:先完成构造,再单独调用
register()或start()。
规则 4:线程池
- 必须通过
ThreadPoolExecutor(或Executors工厂方法,但需理解其限制)管理线程,禁止手动new Thread()。 - 核心参数:根据 CPU 密集型 或 IO 密集型 合理设置
corePoolSize、maxPoolSize、workQueue和handler(拒绝策略)。 - 使用有界队列(如
ArrayBlockingQueue),防止任务堆积导致 OOM。
规则 5:停止线程
- 禁止使用
Thread.stop()、Thread.suspend()、Thread.resume()(已废弃,不安全)。 - 标准方式:使用中断信号
interrupt()或volatile boolean标志位。- 任务应定期检查
Thread.currentThread().isInterrupted()。
- 任务应定期检查
阶段 3:测试与验证
- 并发测试:
- 编写专门的并发测试用例(如使用
CountDownLatch模拟并发启动)。 - 进行高负载、长时间压测,观察死锁、活锁或数据不一致。
- 编写专门的并发测试用例(如使用
- 静态分析:
- 使用 FindBugs、SpotBugs 的
-Dfindbugs.failOnBugPattern=BIT_ATOMIC_READ_WRITE,DC_DOUBLECHECK等规则。 - 使用 Checkstyle 或 PMD 检查
synchronized滥用或锁顺序。
- 使用 FindBugs、SpotBugs 的
- 代码审查 Checklist:
- [ ] 共享变量是否用 volatile 或锁保护? (可见性)
- [ ] 是否有 Check-Then-Act 未加锁? (原子性)
- [ ] 是否在同步块中执行了耗时 I/O 操作?
- [ ] 多个锁是否按固定顺序获取? (死锁)
- [ ] 锁是否在 finally 块中释放?
- [ ] 线程池参数是否基于压测验证过?
- [ ] 是否存在 this 逸出?
关键技术选型建议
| 场景 | 推荐方案 | 备注 |
|---|---|---|
| 计数器/统计 | LongAdder / AtomicLong |
高并发下 LongAdder 性能优于 AtomicLong |
| 全局缓存 | ConcurrentHashMap + computeIfAbsent |
避免二次检查 |
| 有序或排他访问 | ReentrantLock |
支持超时、公平、条件变量 |
| 读多写少 | ReadWriteLock / StampedLock |
StampedLock 乐观读性能极高 |
| 生产者-消费者 | BlockingQueue (如 LinkedBlockingQueue) |
与线程池配合使用 |
| 避免锁的开销 | ThreadLocal / 不可变对象 |
设计上无竞态 |
| 分布式互斥 | 分布式锁 (如 Redis RedLock, ZooKeeper) | 非 JVM 内部,需额外中间件 |
规范检查清单(代码审查速查)
- 原子性:复合操作是否加锁或使用原子类?
- 可见性:共享标志位是否
volatile?synchronized块是否覆盖所有读和写? - 有序性:是否存在依赖重排序导致的问题?
final字段是否确保安全发布? - 死锁:获取多个锁的顺序是否一致? 是否在持有锁时调用外部方法?(警惕回调死锁)
- 资源泄漏:
Lock是否在finally中释放? 线程池是否在应用关闭时优雅停服?
附录:常见反模式
- Double-Checked Locking 错误实现:
- 问题:未使用
volatile,导致读取到未完全构造的对象。 - 修复:使用
volatile或static inner class(Initialization-on-demand holder idiom)。
- 问题:未使用
- String 作为锁:
- 问题:
String.intern()或常量池可能导致误锁不相关的代码。 - 修复:使用
new Object()或专用对象作为锁。
- 问题:
- 同步集合遍历修改:
- 问题:
ConcurrentModificationException。 - 修复:使用
CopyOnWriteArrayList或在同步块中使用迭代器的remove()方法。
- 问题:
“先写正确,再写高效”,在 Java 并发中,清晰的安全边界(如明确的同步策略)远比微小的性能优化重要,开发人员在提交代码前,应比照以上规范逐项自查。