返回Notes

/ notes

关于并发编程(2)

synchronized、volatile、CAS、锁、死锁、ThreadLocal、单例、ConcurrentHashMap

同步工具、锁与 JUC

synchronized

synchronized 通过对象监视器(Monitor)实现互斥。

  • 修饰实例方法:锁是当前对象 this
  • 修饰静态方法:锁是当前类的 Class 对象;
  • 修饰代码块:锁是括号中指定的对象。

它不仅保证互斥(原子性),也能保证临界区内共享变量的可见性和有序性。

volatile

volatile 适合状态标志、配置刷新等场景:

  • 保证变量的可见性
  • 禁止相关读写发生特定重排序,保证一定的有序性
  • 不保证复合操作原子性,如 count++

CAS 与原子类

CAS(Compare And Swap):比较内存值是否等于预期值;相等则更新,否则失败。它是乐观并发控制的基础,Java 原子类广泛使用 CAS。

优缺点:

  • 优点:非阻塞,竞争较低时性能好;
  • 缺点:可能发生 ABA、自旋重试消耗 CPU、单次通常只能原子更新一个位置。

ABA 问题

变量从 A 变为 B 又变回 A,CAS 只比较值时会误判“未变化”。可用版本号解决,例如 AtomicStampedReference

保证原子性的常见方式

  • synchronized
  • ReentrantLock
  • AtomicIntegerAtomicLongAtomicReference 等原子类;
  • 数据库原子条件更新、事务或分布式协调机制(跨进程场景)。

乐观锁与悲观锁

  • 悲观锁:假设冲突经常发生,访问前先加锁,典型如 synchronizedReentrantLock、数据库 SELECT ... FOR UPDATE
  • 乐观锁:假设冲突较少,更新时校验版本或条件;冲突后失败或重试,典型如 CAS、数据库版本号更新。

选择依据是冲突率、操作时长、重试成本和一致性要求。

死锁

死锁是多个线程互相等待对方释放资源,且无法继续推进的状态。

死锁的四个必要条件:

  1. 互斥:资源一次只能被一个线程占用;
  2. 持有并等待:线程持有资源的同时继续等待其他资源;
  3. 不可抢占:资源不能被强制剥夺;
  4. 循环等待:线程间形成循环等待链。

常见预防措施:统一加锁顺序、缩短持锁时间、避免嵌套锁、使用 tryLock 超时获取锁、必要时进行死锁检测。

活锁:线程一直在响应对方但没有实际进展;饥饿:线程长期得不到 CPU 或锁。这两者不同于死锁。

ThreadLocal

ThreadLocal<T> 为每个线程保存独立变量副本,适合跨方法传递线程上下文,而非线程间共享数据。

private static final ThreadLocal<String> TRACE_ID = new ThreadLocal<>();

TRACE_ID.set("trace-001");
String traceId = TRACE_ID.get();
TRACE_ID.remove();

常见场景:链路追踪 ID、当前用户上下文、租户信息、日期格式化器(旧代码)等。

使用注意

在线程池中线程会复用。ThreadLocal 的值如果未清理,后续请求可能读到脏数据,也可能造成内存泄漏。因此必须在 finally 中调用 remove()

try {
    TRACE_ID.set(traceId);
    // 业务逻辑
} finally {
    TRACE_ID.remove();
}

不建议将数据库连接长期直接放入 ThreadLocal;更推荐使用连接池和框架管理事务边界。

单例的线程安全

饿汉式

类加载时初始化,天然线程安全,但无法延迟加载:

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() { return INSTANCE; }
}

双重检查锁(DCL)

实现懒加载时,实例字段必须加 volatile,以避免对象初始化重排序:

private static volatile Singleton instance;

public static Singleton getInstance() {
    if (instance == null) {
        synchronized (Singleton.class) {
            if (instance == null) {
                instance = new Singleton();
            }
        }
    }
    return instance;
}

更推荐枚举单例或静态内部类单例,代码更简洁且安全。

JUC 与 ConcurrentHashMap

JUC 即 java.util.concurrent,提供线程池、锁、原子类、并发容器、同步器等工具。

为什么 HashMap 线程不安全?

多个线程并发 put、扩容或修改时可能发生数据覆盖、丢失、结构不一致等问题。Java 7 扩容并发场景下曾可能形成环形链表并导致查询死循环;Java 8 虽修复了该具体问题,但 HashMap 仍然不是线程安全容器。

ConcurrentHashMap

它是线程安全的哈希表:Java 8 主要采用 CAS + synchronized 对桶级节点进行细粒度协调,支持较高并发读写。

注意:单个操作如 put 是线程安全的,但“先 getput”的复合业务逻辑未必原子,应使用 putIfAbsentcomputeIfAbsentmerge 等原子方法。