Java并发安全流程规范

wen java案例 31

本文目录导读:

Java并发安全流程规范

  1. 核心原则
  2. 并发安全流程规范
  3. 关键技术选型建议
  4. 规范检查清单(代码审查速查)
  5. 附录:常见反模式

这是一份 Java并发安全流程规范,这份规范旨在帮助开发团队在设计、编码和审查阶段系统性地规避并发问题(如竞态条件、死锁、内存一致性错误),该规范涵盖了从“共享资源识别”到“线程协作”的完整流程。


核心原则

  1. 最小化共享:优先设计无共享(无状态Bean、ThreadLocal)或只读共享(final不可变对象)的架构。
  2. 封装可变性:可变状态必须通过明确的锁或原子变量访问。
  3. 文档先行:每个可能被多线程访问的类或方法,必须明确标注其线程安全策略。

并发安全流程规范

阶段 1:识别与建模

  • 步骤 1:识别共享资源
    • 检查:静态变量、实例域(特别是集合、缓存、计数器)、文件句柄、数据库连接池、HttpSession。
    • 行动:在类头部注释中列出所有共享的可变状态。
  • 步骤 2:选择并发策略
    • 线程封闭(推荐优先考虑):栈封闭(局部变量)、ThreadLocal
    • 只读共享final 字段(不可变对象如 StringInteger、自定义 record/不可变类)。
    • 线程安全对象:使用 ConcurrentHashMapCopyOnWriteArrayListAtomicIntegerLongAdder 等。
    • 同步策略synchronized(隐式锁)、ReentrantLockReadWriteLockStampedLock
  • 步骤 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 密集型 合理设置 corePoolSizemaxPoolSizeworkQueuehandler(拒绝策略)。
  • 使用有界队列(如 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 滥用或锁顺序。
  • 代码审查 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 内部,需额外中间件

规范检查清单(代码审查速查)

  1. 原子性:复合操作是否加锁或使用原子类?
  2. 可见性:共享标志位是否 volatilesynchronized 块是否覆盖所有读和写?
  3. 有序性:是否存在依赖重排序导致的问题? final 字段是否确保安全发布?
  4. 死锁:获取多个锁的顺序是否一致? 是否在持有锁时调用外部方法?(警惕回调死锁)
  5. 资源泄漏Lock 是否在 finally 中释放? 线程池是否在应用关闭时优雅停服?

附录:常见反模式

  1. Double-Checked Locking 错误实现
    • 问题:未使用 volatile,导致读取到未完全构造的对象。
    • 修复:使用 volatilestatic inner class (Initialization-on-demand holder idiom)。
  2. String 作为锁
    • 问题:String.intern() 或常量池可能导致误锁不相关的代码。
    • 修复:使用 new Object() 或专用对象作为锁。
  3. 同步集合遍历修改
    • 问题:ConcurrentModificationException
    • 修复:使用 CopyOnWriteArrayList 或在同步块中使用迭代器的 remove() 方法。

“先写正确,再写高效”,在 Java 并发中,清晰的安全边界(如明确的同步策略)远比微小的性能优化重要,开发人员在提交代码前,应比照以上规范逐项自查。

抱歉,评论功能暂时关闭!