在Java里如何避免线程安全问题_Java并发安全设计思路说明

应优先使用细粒度锁、并发工具类和ThreadLocal,避免方法级同步、同步块内调用外部方法及ThreadLocal内存泄漏。

synchronized 保护共享状态,但别锁整个方法

直接在方法上加 synchronized 很方便,但容易锁粒度太大——比如一个类有 10 个字段,只改其中 1 个,却让所有线程排队等这一个方法,性能掉得明显。更稳妥的做法是锁具体对象或代码块:

private final Object lock = new Object();
public void updateCounter() {
    synchronized (lock) {
        count++;
    }
}
注意:锁对象必须是 final 且不对外暴露,否则别人也能 synchronized 它,导致意外串行或死锁。

优先用 java.util.concurrent 包里的线程安全类型

手写同步逻辑容易漏边界、错顺序。能用现成的就别自己造:

  • ConcurrentHashMap 替代 HashMap + 手动同步
  • AtomicInteger 替代 int + synchronized 自增
  • CopyOnWriteArrayList 适合读多写少、迭代中可能修改的场景
注意:ConcurrentHashMapsize() 不保证实时准确;AtomicIntegergetAndIncrement() 是原子的,但 ++ 运算符不是。

避免在同步块里调用外部可变对象的方法

这是隐蔽的死锁高发区。比如你在 synchronized(lockA) 里调用了某个第三方服务的 doSomething(),而它内部又去获取了 lockB ——如果另一个线程正拿着 lockB

并试图拿 lockA,就卡住了。

  • 同步块内只做确定可控的操作:读写本类字段、调用纯函数(无副作用、不加锁)
  • 把外部调用(DB 查询、HTTP 请求、回调)移出同步块
  • 如必须组合操作,考虑用 ReentrantLock.tryLock(timeout, unit) 设超时,避免无限等待

ThreadLocal 隔离线程间数据,但记得 remove()

当每个线程需要自己的副本(比如数据库连接、用户上下文),ThreadLocal 比共享变量+同步更轻量:

private static final ThreadLocal DATE_FORMAT =
    ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
但 Web 应用中线程常被池化复用,不清理会导致内存泄漏——尤其 ThreadLocal 值是大对象或持有 ClassLoader 时。

  • 每次用完显式调用 DATE_FORMAT.remove()
  • 在 Filter 或拦截器的 finally 块里清理
  • 不要依赖线程结束自动回收,JVM 不保证及时触发
线程安全问题往往不在“有没有加锁”,而在“锁什么”“锁多久”“锁之外做了什么”。最棘手的不是编译报错,而是偶发的计数偏差、脏读、或某天流量上来后突然的响应延迟飙升——这些通常意味着同步策略和实际访问模式没对齐。