引言

并发编程是 Java 后端开发的核心能力之一,从基础的请求处理线程池,到微服务的异步执行、缓存组件、消息队列、数据库连接池,都依赖多线程。而在多线程场景下最危险的问题就是:线程安全 Thread Safety

线程安全不是一个概念,而是一个系统工程。要在 Java 中写出线程安全且高性能的代码,你必须掌握:

  • Java 内存模型(Memory Model)
  • 并发语义中的三大特性(原子性、可见性、有序性)
  • 关键字(volatile、synchronized)
  • AQS 锁框架与 ReentrantLock
  • CAS 无锁化机制
  • 以及最终目标:锁优化与并发性能优化体系

本文将构建一个从基础原理到工程实战的完整知识体系,非常适合作为系统学习 Java 并发的文章。

线程安全为什么如此重要?从 CPU 缓存与线程调度开始说起

初学者往往误以为“多线程 === 并发”,但实际上多线程的核心挑战来自:

1. CPU 多核架构

每个 CPU 核都有自己的缓存(L1/L2),线程读取变量时通常读取的是 CPU 缓存副本,而不是主内存

→ 导致 可见性问题:一个线程修改变量,其它线程看不到。

2. JVM 的编译优化

Java 编译器、JIT、CPU 指令集都会做重排序以提高速度。

→ 导致 有序性问题:指令执行顺序不同于代码顺序。

3. 线程上下文切换

线程执行中途可能被操作系统强制换下。

→ 导致 原子性问题:例如 count++ 被打断时产生竞态条件。

因此,线程安全本质上是:

让多线程情况下程序的运行结果和单线程执行一致。

要做到这点,就必须理解 Java 内存模型。

Java 内存模型(JMM)三大特性:理解线程安全的基础

JMM 定义了线程如何与内存交互,规定了三大关键特性:

1. 原子性(Atomicity)

操作不可被拆分,中间不可被打断。

反例:

int count = 0;

public void incr() {
    count++;  // 非原子操作,等价于 3 步
}

多个线程对同一变量自增时,必然出现结果错误。

解决方式:

  • synchronized
  • ReentrantLock
  • AtomicInteger(无锁 CAS)

2. 可见性(Visibility)

线程对共享变量的修改必须对其他线程可见。

反例:

boolean flag = true;

new Thread(() -> {
    while (flag) {}
}).start();

Thread.sleep(1000);
flag = false;  // 子线程可能永不停止

解决方式:

  • volatile(强内存语义)
  • synchronized(锁释放带有内存语义)
  • Lock 实现的内存屏障

3. 有序性(Ordering)

编译器与 CPU 会改变指令执行顺序。

典型 bug:双重检查锁(DCL)单例在没有 volatile 时会失败。

为何?
对象创建会被重排序:

  1. 分配内存
  2. 赋值引用(未初始化完成)
  3. 执行构造方法初始化

新线程可能看到“未完全初始化”的对象。

正确写法:

private static volatile Singleton instance;

Java 并发关键字:volatile / synchronized / Lock 全解析

这里的重点是“规范背后的原理”。

1. volatile:轻量级可见性关键字

作用:

  • 保证可见性
  • 禁止指令重排序(有序性)
  • 不保证原子性(这是初学者最大误区)

典型使用场景:

  • 状态标志位(如 while(flag))
  • 单例模式 DCL
  • 配置热更新

2. synchronized:Java 内置锁(Monitor)

特点:

  • 可重入锁(同线程可多次获得)
  • 阻塞式锁(线程挂起需要操作系统参与)
  • 保证:原子性 + 可见性 + 有序性

语法:

public synchronized void method() {}

synchronized (obj) {}

synchronized 的底层:锁的升级四阶段(JDK 1.6+)

为了性能,锁不是一开始就是重量级的,而是按需升级:

1)无锁状态

没有任何线程竞争。

2)偏向锁(Biased Locking)
  • 只有一个线程访问
  • JVM 在对象头记录持有线程 ID
  • 进入同步代码几乎零开销
3)轻量级锁(自旋锁)
  • 多线程竞争时
  • 线程会 CAS 尝试获取锁
  • 如果短时间竞争轻微 → 自旋比挂起更快
4)重量级锁(OS 层 mutex)
  • 竞争激烈时
  • 线程阻塞进入 OS 等待队列
  • 切换成本高

锁的进化过程体现了 Java 对性能的极致优化。

3. ReentrantLock 与 Lock 框架

比 synchronized 更灵活:

功能 synchronized ReentrantLock
可中断锁 ✔️
尝试获取 tryLock ✔️
公平锁 ✔️
多条件变量 Condition ✔️
性能 高频竞争下更差 更可控

使用:

Lock lock = new ReentrantLock();

lock.lock();
try {
    // 临界区
} finally {
    lock.unlock();
}

ReentrantLock 的底层全部依赖 AQS 实现。

AQS 深度剖析:Java 并发的基石

AQS(AbstractQueuedSynchronizer)是 Java 并发包的灵魂。

基于 AQS 的组件包括:

  • ReentrantLock
  • Semaphore
  • CountDownLatch
  • CyclicBarrier
  • FutureTask
  • ReadWriteLock

1. AQS 核心思想

核心:state + CLH 队列

  • state = 锁的占用状态
  • CAS 用于更新 state
  • 失败就进入 FIFO 阻塞队列(CLH)
  • 通过 park / unpark 挂起与唤醒线程

2. 获取锁流程(简化)

CAS 修改 state 尝试加锁
    成功 → 获得锁
    失败 → 进入队列 → park 阻塞

3. 释放锁流程

state = 0
唤醒队列中下一个节点(unpark)

AQS 保证了可扩展性:开发者只需重写 tryAcquire/tryRelease 即可实现各种同步器。

Java 锁优化实践:构建高性能并发系统

下面的优化内容非常贴近大型后端系统实践。

1. 减少锁粒度:从粗锁变细锁

反例:

synchronized(List.class) {
    // 大量逻辑
}

优化方式:

  • 锁分段(ConcurrentHashMap 早期使用的 Segment 分段锁)
  • 读写分离(ReadWriteLock)
  • 使用线程安全写少读多结构:CopyOnWriteArrayList

2. 使用无锁结构替代锁

例如:

  • ConcurrentLinkedQueue(无锁队列)
  • AtomicInteger / LongAdder(热点消除)
  • ConcurrentHashMap(CAS + 分段 + 红黑树)

3. CAS 优化与 ABA 问题

CAS 问题:

  • ABA:值从 A → B → A,CAS 误认为没变
  • 自旋时间长 → CPU 占用高
  • 多变量 CAS 无法保证一致性

解决方案:

  • AtomicStampedReference
  • 带退避(backoff)策略的 CAS
  • 分离热点变量(LongAdder)

4. 合理使用线程池减少锁竞争

例如:

  • 避免创建过多短命线程
  • 降低线程切换
  • 用 Disruptor 之类的队列降低锁开销

5. 将锁移到业务更高层减少临界区

示例:

// 不推荐:锁保护过大区域
synchronized(this) {
    queryFromDB();
    compute();
    writeBack();
}

// 推荐:只锁共享资源
queryFromDB();
compute();
synchronized(this) {
    writeBack();
}

总结

线程安全不是单一问题,而是:

  • CPU 缓存可见性问题
  • 编译器/CPU 重排序问题
  • JVM 内存模型语义问题
  • 锁的选择与性能问题
  • AQS 框架的高层使用问题
  • 锁优化与无锁化的工程实践

可以总结为一句话:

优秀的后端从来不是“加更多的锁”,而是“减少对锁的需求,让系统自然做到线程安全”。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐