并发同步机制全览

graph LR
    A[并发同步机制] --> B[锁(悲观锁)]
    A --> C[volatile]
    A --> D[CAS(乐观锁)]

    B --> B1[内核态锁:上下文切换慢,缓存失效]
    B --> B2[用户态锁:无竞争时有效]
    B --> B3[synchronized:独占锁,低并发可用]

    C --> C1[保证可见性]
    C --> C2[不保证原子性]
    C --> C3[适合状态标志、配置刷新]

    D --> D1[乐观锁实现:失败重试,不挂起]
    D --> D2[CAS原子操作,CPU指令完成]
    D --> D3[可能出现ABA问题]
    D3 --> D31[解决:版本号/AtomicStampedReference]
    D --> D4[应用场景:高并发计数器、累加器]

    A --> E[高并发优化思路]
    E --> E1[优化锁实现:ConcurrentHashMap桶粒度锁]
    E --> E2[Lock-free结构:CAS、Disruptor、无锁队列]
    E --> E3[单线程循环+异步I/O:Nginx/Node.js模式]
    E --> E4[内存处理:多级缓存、Redis、读写分离]
    E --> E5[定时同步缓存到DB]

    style B fill:#f9f,stroke:#333,stroke-width:1px
    style C fill:#9f9,stroke:#333,stroke-width:1px
    style D fill:#ff9,stroke:#333,stroke-width:1px
    style E fill:#9ff,stroke:#333,stroke-width:1px

锁 vs volatile vs CAS 对比

方案 可见性 原子性 性能 适用场景 备注
volatile 单独使用 ⭐⭐⭐⭐ 状态标志位、配置刷新、单次写入 不能用于依赖旧值的操作(如 count++
AtomicInteger (CAS) ✅(乐观锁+重试) ⭐⭐⭐⭐ 高并发计数器、累加器 可能出现自旋重试,适合短时间冲突
volatile + synchronized ✅(互斥锁) ⭐⭐ 低并发计数器、逻辑简单的临界区 存在阻塞,线程切换开销大

锁的代价

在并发编程中,锁是最直接的同步手段,但代价也最高

  • 内核态锁:涉及上下文切换,CPU 缓存失效,性能损失大;操作系统对多线程锁的判断也相对滞后,响应慢
  • 用户态锁:避免上下文切换,但仅在没有真正竞争时有效

Java 1.5 通过 synchronized 实现独占锁(悲观锁),保证线程对共享变量的独占访问。然而,悲观锁存在以下问题:

  • 高竞争下,加锁和释放锁会导致上下文切换和调度延时,性能下降
  • 一个线程持有锁时,其他线程必须挂起
  • 高优先级线程等待低优先级线程释放锁可能导致优先级倒置

volatile 变量

相比锁,volatile 更轻量,因为它不会触发线程调度或上下文切换。但它无法保证原子性,因此当变量更新依赖旧值时不能使用

1. 作用

  • 保证可见性:一个线程修改 volatile 变量后,其他线程立即可见
  • 禁止编译器/CPU 对 volatile 变量的读写进行危险优化(寄存器缓存、重排序等)

⚠️ 注意volatile 不保证复合操作的原子性

2. 原子性问题示例

volatile int count = 0;

public void increment() {
    count = count + 1; // 或 count++
}

虽然看似只修改一次,实际上拆分为三步:

  1. 读取旧值
  2. 计算新值
  3. 写回

多线程环境下可能出现竞态条件(race condition):

  • 线程 A、B 同时读取 count = 10
  • A 写回 11,B 写回 11
  • 最终 count 只增加 1,而不是 2

总结一句话volatile 解决的是可见性,但复合操作需要原子性。一旦变量的写入依赖旧值,volatile 就不够了,必须借助锁或原子类

%%{init: {"sequence": {"rightAngles":true,"width":350}}}%%
sequenceDiagram
    participant MainMemory as 主内存(count=10)
    participant ThreadA as 线程A
    participant ThreadB as 线程B

    ThreadA->>MainMemory: 读取 count (10)
    ThreadB->>MainMemory: 读取 count (10)
    ThreadA->>ThreadA: 计算 10+1=11
    ThreadB->>ThreadB: 计算 10+1=11
    ThreadA->>MainMemory: 写回 11
    ThreadB->>MainMemory: 写回 11

即使 volatile 保证了线程 A 写入 11 后能立即对 B 可见,但因为两个线程都基于旧值 10 来计算,导致结果丢失一次更新

3. 解决方案

  • 锁机制synchronizedLock
  • CAS 原子类AtomicIntegerAtomicLong

CAS 示例(AtomicInteger.incrementAndGet)

sequenceDiagram
    participant MainMemory as 主内存(count=10)
    participant ThreadA as 线程A
    participant ThreadB as 线程B

    Note over MainMemory: count 初始值 = 10

    ThreadA->>MainMemory: 读取 count (10)
    ThreadB->>MainMemory: 读取 count (10)

    ThreadA->>MainMemory: CAS(预期=10, 新值=11) → 成功
    Note over MainMemory: count = 11

    ThreadB->>MainMemory: CAS(预期=10, 新值=11) → 失败 (因当前值=11)
    ThreadB->>MainMemory: 重新读取 count=11
    ThreadB->>MainMemory: CAS(预期=11, 新值=12) → 成功
    Note over MainMemory: count = 12 (正确)

volatile + synchronized

sequenceDiagram
    participant MainMemory as 主内存(count=10)
    participant ThreadA as 线程A
    participant ThreadB as 线程B

    Note over MainMemory: count 初始值 = 10

    ThreadA->>ThreadA: synchronized 加锁
    ThreadA->>MainMemory: 读取 count (10)
    ThreadA->>ThreadA: 计算 10+1 = 11
    ThreadA->>MainMemory: 写回 11
    ThreadA->>ThreadA: 释放锁
    Note over MainMemory: count = 11

    ThreadB->>ThreadB: synchronized 加锁 (等待A释放)
    ThreadB->>MainMemory: 读取 count (11)
    ThreadB->>ThreadB: 计算 11+1 = 12
    ThreadB->>MainMemory: 写回 12
    ThreadB->>ThreadB: 释放锁
    Note over MainMemory: count = 12 (正确)

变量更新方式对比

方案 可见性 原子性 性能 适用场景 备注
volatile 单独使用 ✅ 保证 ❌ 不保证 ⭐⭐⭐⭐ 高 状态标志位、配置刷新、单次写入 不能用于依赖旧值的操作(如 count++
AtomicInteger (CAS) ✅ 保证 ✅ 保证(乐观锁+重试) ⭐⭐⭐⭐ 较高 高并发计数器、累加器 可能出现自旋重试,适合短时间冲突
volatile + synchronized ✅ 保证 ✅ 保证(互斥锁) ⭐⭐ 一般 低并发下的计数器、逻辑简单的临界区 存在阻塞,线程切换开销大

只需要可见性 → 用 volatile

需要原子性 → 首选 AtomicInteger

逻辑复杂 or 低并发synchronized

CAS(Compare-And-Swap)

CAS 是一种乐观锁实现方式:

  • 基于共享数据不会被修改的假设
  • 采用 commit-retry 模式,线程失败不会挂起,可再次尝试

CAS 开销

CAS 基于 CPU 指令,原子操作在 CPU 内部完成,非常快,避免了 OS 内核锁裁定。但存在 cache missABA 问题

CPU 缓存线竞争

当 CAS 操作变量所在 cache line 不在本地 CPU 时,需要跨 CPU 和系统互联模块请求缓存线,增加延迟

ABA 问题

问题:CAS 只检查内存当前值是否等于预期,忽略了中间可能发生的修,如果在此过程中值由 A→B→A,CAS 会误判成功

解决方案:使用版本号(version)或 AtomicStampedReference,每次修改都更新版本号,避免 ABA

示例:AtomicInteger vs AtomicStampedReference

AtomicInteger atomicInt = new AtomicInteger(100);
AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<>(100, 0);
  • AtomicInteger CAS 会成功(ABA 问题存在)

  • AtomicStampedReference CAS 会失败(版本号保护,避免 ABA)

jvm 对CAS的支持

  • JDK 1.5 引入底层 CAS 支持:intlong、对象引用等

  • CPU 不支持 CAS 时,JVM 使用自旋锁

  • AtomicLong.incrementAndGet() 使用 sun.misc.Unsafe 提供的 CAS 指令,无锁自增,比 synchronized 效率高数倍

高并发优化思路

高并发下性能杀手:

  1. 线程切换开销
  2. 非必要内存拷贝

优化方向:

  • 优化锁实现
    • 例如 ConcurrentHashMap 使用桶粒度锁、锁分离,避免整个 map 锁定
  • Lock-free 无锁结构
    • 利用 CAS、Disruptor 等无锁队列
    • 或单线程大循环(Nginx、Node.js)+ 异步 I/O

优化目标:

  • 减少资源争用
  • 任务尽量在内存中处理(多级缓存、读写分离、ConcurrentHashMap、Redis)
  • 定时同步缓存到数据库

对于大型分布式系统,需要分而治之,SOA 思路更适合管理复杂的并发场景