Java 并发原理与性能优化:从 JMM 到虚拟线程的完整实践指南

写在前面

并发编程是 Java 开发者从初级迈向高级的必经之路。它不仅关乎多线程的写法,更涉及对 CPU 缓存、内存屏障、指令重排等底层原理的理解。本文从 JMM(Java 内存模型)出发,结合 JDK 各版本演进,系统梳理 Java 并发的核心机制与实战优化策略。

技术栈: Java 8~21 · JMM · JUC · JMH

难度: ⭐⭐⭐⭐

阅读时间: 30 分钟

适合人群: 2 年以上 Java 开发者、后端架构师、性能调优工程师


一、JMM 基础:并发编程的第一性原理

1.1 为什么需要内存模型?

现代 CPU 采用多级缓存架构(L1/L2/L3 Cache),每个核心拥有自己的缓存。当多个线程操作共享变量时,一个线程的修改可能对其他线程不可见——这就是可见性问题。同时,编译器和 CPU 会为了优化性能而对指令进行重排序,导致代码执行顺序与编写顺序不一致。

JMM(Java Memory Model)定义了多线程环境下变量的访问规则,核心目标有三个:

  • 原子性 — 一个或多个操作要么全部执行且不被中断,要么全不执行
  • 可见性 — 一个线程修改共享变量后,其他线程能立即看到
  • 有序性 — 程序执行顺序按代码逻辑进行(禁止不必要的重排序)

1.2 happens-before 规则

JMM 通过 happens-before 关系来保证有序性和可见性。如果操作 A happens-before 操作 B,则 A 的结果对 B 可见,且 A 的执行顺序在 B 之前。

关键规则:

  1. 程序次序规则: 同一个线程中,书写在前的操作 happens-before 书写在后的操作
  2. 锁规则: unlock 操作 happens-before 对同一把锁的 lock 操作
  3. volatile 规则: 对 volatile 变量的写操作 happens-before 后续对该变量的读操作
  4. 传递性: 若 A happens-before B,B happens-before C,则 A happens-before C
  5. 线程启动规则: Thread.start() happens-before 该线程的任何操作
  6. 线程终止规则: 线程中所有操作 happens-before 其他线程检测到该线程终止
  7. 中断规则: 调用 interrupt() happens-before 被中断线程检测到中断事件
  8. 终结器规则: 对象的构造函数完成 happens-before finalize() 方法
// 示例:volatile 保证可见性
public class VisibilityExample {
    private volatile boolean running = true;

    public void worker() {
        while (running) {
            // 由于 volatile,worker 线程一定能看到 running 的修改
        }
    }

    public void stop() {
        running = false; // 写 volatile
    }
}

1.3 内存屏障

JMM 底层通过 内存屏障(Memory Barrier)实现 happens-before 语义。常见的屏障类型:

  • LoadLoad Barrier: 确保 Load1 的数据读取先于 Load2 及其后的读取操作
  • StoreStore Barrier: 确保 Store1 的数据写入对其他处理器可见先于 Store2 及其后的写入
  • LoadStore Barrier: 确保 Load1 的数据读取先于 Store2 及其后的写入操作
  • StoreLoad Barrier: 确保 Store1 的数据写入对其他处理器可见先于 Load2 及其后的读取(最昂贵的屏障)

二、锁机制深度剖析

2.1 synchronized 的演进之路

synchronized 是 Java 最基础的同步原语,JDK 6 之后经历了巨大的性能优化:

锁状态 描述 开销
无锁 对象刚创建,无竞争 0
偏向锁 同一线程重复获取,Mark Word 记录线程 ID 极低
轻量级锁 少量线程交替获取,CAS 自旋
重量级锁 多线程竞争激烈,线程阻塞(内核态)

锁升级过程(不可逆): 无锁 → 偏向锁 → 轻量级锁 → 重量级锁

// JVM 参数控制锁行为
-XX:+UseBiasedLocking   // 启用偏向锁(JDK 15 后默认关闭)
-XX:BiasedLockingStartupDelay=0  // 启动后立即启用偏向锁
-XX:-UseBiasedLocking   // 禁用偏向锁

JDK 15 之后偏向锁被标记为废弃,JDK 21 中彻底移除。原因是偏向锁的撤销逻辑在高度竞争场景下反而增加了复杂度,且现代应用大多使用更高级的并发工具。

2.2 AQS 框架:JUC 的基石

AbstractQueuedSynchronizer(AQS)是 java.util.concurrent 包的灵魂,ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier 等全部基于 AQS 实现。

核心原理:

  • 一个 volatile int state 表示同步状态
  • 一个 CLH 变体的 FIFO 双向队列管理等待线程
  • 子类通过 tryAcquire / tryRelease 等模板方法定义具体同步语义
// AQS 核心设计模式:模板方法
public abstract class AbstractQueuedSynchronizer {
    // 子类需要实现的方法
    protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }
    protected boolean tryRelease(int arg) { throw new UnsupportedOperationException(); }
    protected int tryAcquireShared(int arg) { throw new UnsupportedOperationException(); }
    protected boolean tryReleaseShared(int arg) { throw new UnsupportedOperationException(); }

    // 公开的模板方法(不可重写)
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
}

2.3 ReentrantLock vs synchronized 选择指南

特性 synchronized ReentrantLock
语法简洁 ✅ 自动释放 ❌ 需手动 unlock
可中断 ❌ 不支持 ✅ lockInterruptibly()
超时获取 ❌ 不支持 ✅ tryLock(timeout, unit)
公平性 ❌ 非公平 ✅ 可选公平/非公平
条件变量 wait/notify(单一) 多个 Condition 对象
读/写分离 ✅ ReentrantReadWriteLock
性能(低竞争) 优秀(偏向锁优化) 优秀
性能(高竞争) 重量级锁阻塞 CAS + 自旋 + 阻塞

推荐: 默认用 synchronized(简洁安全),需要高级特性(可中断、超时、读写锁、多条件)时用 ReentrantLock。


三、无锁并发:CAS 与原子类

3.1 CAS 原理

Compare-And-Swap(CAS)是一种乐观锁机制,包含三个操作数:内存地址 V、期望值 A、新值 B。当 V 的值等于 A 时,将 V 更新为 B,否则不操作。

// CAS 的典型实现(sun.misc.Unsafe)
public final native boolean compareAndSwapObject(
    Object o, long offset, Object expected, Object x);

public final native boolean compareAndSwapInt(
    Object o, long offset, int expected, int x);

CAS 通过 CPU 的 cmpxchg 指令实现,是一个原子操作。但 CAS 存在三个经典问题:

  1. ABA 问题: 值从 A→B→A,CAS 误判未修改。解决:AtomicStampedReference 加版本号
  2. 自旋开销: 高竞争下大量 CAS 失败导致 CPU 空转
  3. 只能操作单个变量: 无法同时对多个变量做 CAS
// 手动实现一个基于 CAS 的计数器
public class CasCounter {
    private final AtomicInteger count = new AtomicInteger(0);

    public int increment() {
        // 自旋 CAS
        while (true) {
            int current = count.get();
            int next = current + 1;
            if (count.compareAndSet(current, next)) {
                return next;
            }
        }
    }

    // 等价于一行:
    // public int increment() { return count.incrementAndGet(); }
}

3.2 JDK 8 的 LongAdder:高并发计数之王

AtomicLong 在高并发下 CAS 竞争极其激烈,大量线程自旋浪费 CPU。JDK 8 引入的 LongAdder 将单一热点分解为多个 Cell:

// LongAdder 核心设计
public class LongAdder extends Striped64 {
    // 内部维护一个 base 和一个 Cell[] 数组
    // 低竞争:直接 CAS 修改 base
    // 高竞争:分散到不同的 Cell 上,最终 sum() 汇总

    transient volatile long base;
    transient volatile Cell[] cells;

    public void add(long x) {
        Cell[] as; long b, v; int m; Cell a;
        if ((as = cells) != null || !casBase(b = base, b + x)) {
            // cells 不为空 或 base CAS 失败 → 分散到 Cell
            boolean uncontended = true;
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[getProbe() & m]) == null ||
                !(uncontended = a.cas(v = a.value, v + x)))
                longAccumulate(x, null, uncontended);
        }
    }
}

性能对比(JMH 压测,16 线程):

实现 吞吐量(ops/s) 适用场景
synchronized ~500 万 低并发,代码简洁
AtomicLong ~2000 万 中等并发,需准确数值
LongAdder ~1.2 亿 极高并发,可接受最终一致

四、线程池:企业级实战与调优

4.1 ThreadPoolExecutor 核心参数

public ThreadPoolExecutor(
    int corePoolSize,      // 核心线程数
    int maximumPoolSize,   // 最大线程数
    long keepAliveTime,    // 非核心线程空闲存活时间
    TimeUnit unit,         // 时间单位
    BlockingQueue<Runnable> workQueue,  // 任务队列
    ThreadFactory threadFactory,        // 线程工厂
    RejectedExecutionHandler handler    // 拒绝策略
);

工作流程:

  1. 线程数 < corePoolSize → 创建新线程执行任务
  2. 线程数 ≥ corePoolSize → 任务入队
  3. 队列满,线程数 < maximumPoolSize → 创建新线程执行任务
  4. 队列满,线程数 ≥ maximumPoolSize → 执行拒绝策略

4.2 四种拒绝策略

策略 行为 适用场景
AbortPolicy(默认) 抛出 RejectedExecutionException 必须处理的关键任务
CallerRunsPolicy 调用者线程执行任务 降低任务提交速率,背压控制
DiscardPolicy 静默丢弃 非关键任务,可容忍丢失
DiscardOldestPolicy 丢弃队列中最旧的任务 优先处理最新任务

4.3 线程池大小计算公式

最经典的估算公式:

// CPU 密集型
N_threads = N_CPU + 1  // +1 补偿页缺失等暂停

// IO 密集型
N_threads = N_CPU * (1 + IO_time / CPU_time)
// 示例:CPU 耗时 20ms,IO 耗时 80ms
// N_threads = 8 * (1 + 80/20) = 40

// 混合型
// 使用 CompletableFuture 拆分 CPU 和 IO 任务到不同线程池

4.4 生产级线程池配置模板

@Configuration
public class ThreadPoolConfig {

    @Bean("ioThreadPool")
    public ThreadPoolExecutor ioThreadPool() {
        int cpuCores = Runtime.getRuntime().availableProcessors();
        return new ThreadPoolExecutor(
            cpuCores * 2,
            cpuCores * 4,
            60, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(500),
            new ThreadFactoryBuilder()
                .setNameFormat("io-pool-%d")
                .setDaemon(true)
                .build(),
            new ThreadPoolExecutor.CallerRunsPolicy()
        );
    }

    @Bean("cpuThreadPool")
    public ThreadPoolExecutor cpuThreadPool() {
        int cpuCores = Runtime.getRuntime().availableProcessors();
        return new ThreadPoolExecutor(
            cpuCores + 1,
            cpuCores + 1,
            0, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(200),
            new ThreadFactoryBuilder()
                .setNameFormat("cpu-pool-%d")
                .build(),
            new ThreadPoolExecutor.AbortPolicy()
        );
    }

    @Bean("scheduledThreadPool")
    public ScheduledThreadPoolExecutor scheduledThreadPool() {
        return new ScheduledThreadPoolExecutor(
            4,
            new ThreadFactoryBuilder()
                .setNameFormat("scheduled-pool-%d")
                .build()
        );
    }
}

4.5 常见坑点与最佳实践

  • 禁止使用 Executors.newFixedThreadPool(): 默认队列为 Integer.MAX_VALUE,可能 OOM
  • 禁止使用 Executors.newCachedThreadPool(): 最大线程数 Integer.MAX_VALUE,可能创建过多线程
  • 统一命名线程: 使用自定义 ThreadFactory,方便排查问题
  • 捕获异常: 任务内部 try-catch,避免线程异常退出
  • 监控告警: 暴露队列大小、活跃线程数、拒绝次数等指标
  • 优雅关闭: shutdown() 后 awaitTermination(),确保任务执行完毕
// 优雅关闭模板
public void shutdownPool(ExecutorService pool, String poolName) {
    pool.shutdown(); // 不再接受新任务
    try {
        if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
            pool.shutdownNow();
            if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
                log.error("{} 关闭超时", poolName);
            }
        }
    } catch (InterruptedException e) {
        pool.shutdownNow();
        Thread.currentThread().interrupt();
    }
}

五、并发集合:选型与性能对比

容器 同步策略 读性能 写性能 适用场景
ConcurrentHashMap 分段锁/CAS + synchronized ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ 高并发 KV 存储
CopyOnWriteArrayList 写时复制 ⭐⭐⭐⭐⭐ 读多写极少(白名单、配置)
ConcurrentLinkedQueue CAS 无锁 ⭐⭐⭐⭐ ⭐⭐⭐⭐ 高吞吐消息队列
ConcurrentSkipListMap 跳表 + CAS ⭐⭐⭐⭐ ⭐⭐⭐ 高并发有序 KV
ArrayBlockingQueue ReentrantLock ⭐⭐⭐ ⭐⭐⭐ 有界阻塞队列
LinkedBlockingQueue ReentrantLock ⭐⭐⭐ ⭐⭐⭐ 可选有界/无界

ConcurrentHashMap 核心设计(JDK 8+)

// 核心改进:
// 1. 放弃分段锁(Segment),采用 Node 数组 + CAS + synchronized
// 2. 数组长度 2 的幂次,用 (n-1) & hash 定位
// 3. 链表超过 8 转红黑树(TREEIFY_THRESHOLD)
// 4. 扩容支持多线程协助(transfer)

// 插入逻辑(简化)
final V putVal(K key, V value, boolean onlyIfAbsent) {
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();                    // 懒初始化
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
                break;                            // 空位置直接 CAS
        } else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);           // 协助扩容
        else {
            synchronized (f) {                    // 锁住桶头节点
                // 链表或红黑树插入
            }
        }
    }
}

六、性能优化实战案例

6.1 案例:高并发订单号生成器

需求: 分布式环境下生成全局唯一、趋势递增的订单号,TPS 要求 10 万+。

优化演进:

  1. v1 – synchronized: 性能 ~1 万 TPS,全部线程串行等待
  2. v2 – AtomicLong + 时间戳: ~5 万 TPS,CAS 竞争加剧
  3. v3 – 预分配号段(Segment ID): 每个 JVM 实例预取一段连续 ID,内存自增
public class SegmentIdGenerator {
    private final AtomicLong current;
    private volatile long maxId;
    private final IdAllocDao dao;
    private final ReentrantLock lock = new ReentrantLock();

    public long nextId() {
        long id = current.getAndIncrement();
        if (id <= maxId) return id;  // 快速路径:无需锁

        // 慢速路径:需要从数据库获取新号段
        lock.lock();
        try {
            if (current.get() > maxId) {  // 双重检查
                IdSegment segment = dao.nextIdSegment();
                current.set(segment.getStart());
                maxId = segment.getEnd();
            }
            return current.getAndIncrement();
        } finally {
            lock.unlock();
        }
    }
}

最终: 每个 JVM 实例单机可达 50 万+ TPS,扩容只需增加实例。

6.2 案例:高并发缓存热点 Key 优化

问题: 某个热点 Key 每秒 10 万次并发读,单机缓存被压垮。

优化方案:

  • 本地缓存(Caffeine): 每个节点缓存热点数据,减少远程调用
  • 读写锁: 读多写少场景使用 ReentrantReadWriteLock
  • 缓存行填充(@Contended): 避免伪共享(False Sharing)
// 伪共享示例与解决
// ❌ 问题:x 和 y 在同一缓存行,多核交替修改导致缓存行失效
class FalseSharingExample {
    volatile long x;
    volatile long y;  // 同一缓存行!
}

// ✅ 解决:@Contended 填充缓存行(JDK 8+)
@sun.misc.Contended
class PaddingExample {
    volatile long x;
}

@sun.misc.Contended
class AnotherPaddingExample {
    volatile long y;
}

6.3 JMH 微基准测试

// 使用 JMH 验证优化效果
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Thread)
public class ConcurrencyBenchmark {

    private AtomicLong atomicLong = new AtomicLong();
    private LongAdder longAdder = new LongAdder();

    @Benchmark
    public long atomicLongIncrement() {
        return atomicLong.incrementAndGet();
    }

    @Benchmark
    public long longAdderIncrement() {
        longAdder.increment();
        return longAdder.sum();
    }

    public static void main(String[] args) throws Exception {
        Options opt = new OptionsBuilder()
            .include(ConcurrencyBenchmark.class.getSimpleName())
            .forks(1)
            .threads(16)
            .warmupIterations(5)
            .measurementIterations(5)
            .build();
        new Runner(opt).run();
    }
}

七、JDK 新特性中的并发进化

JDK 版本 并发相关特性 意义
JDK 5 JUC 包诞生(AQS、线程池、并发集合) 里程碑
JDK 7 Fork/Join 框架、Phaser 分治并行
JDK 8 CompletableFuture、LongAdder、StampedLock 异步编程革命
JDK 9 Reactive Streams Flow API 响应式标准
JDK 11 Epsilon GC、Flight Recorder 增强 可观测性
JDK 17 密封类(辅助不可变对象设计) 安全并发
JDK 19+ Virtual Threads(虚拟线程/协程) 终极简化
JDK 21 Virtual Threads GA、Scoped Values 生产就绪

虚拟线程:传统线程池的终结者?

虚拟线程是 JDK 21 正式 GA 的划时代特性。它的核心思想是:一个操作系统线程(载体线程)可以承载成千上万个虚拟线程

// 虚拟线程的使用
public class VirtualThreadDemo {

    public static void main(String[] args) throws Exception {
        // 方式一:直接创建
        Thread vThread = Thread.startVirtualThread(() -> {
            System.out.println("Hello from virtual thread: " + Thread.currentThread());
        });

        // 方式二:使用 Executors
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < 10_000; i++) {
                int taskId = i;
                executor.submit(() -> {
                    // 每个任务一个虚拟线程,无需池化
                    Thread.sleep(100); // 此时虚拟线程让出载体线程
                    return taskId;
                });
            }
        } // 自动关闭,等待所有任务完成
    }
}

虚拟线程最佳实践:

  • 不要池化虚拟线程: 创建开销极低(微秒级),池化反而增加复杂度
  • 不要使用 ThreadLocal: 虚拟线程数量巨大,ThreadLocal 内存开销爆炸 → 使用 ScopedValues
  • 不要使用 synchronized 块: 会固定载体线程 → 使用 ReentrantLock
  • 适用于 IO 密集型任务: 大量阻塞操作时优势最明显
  • CPU 密集型任务仍然需要平台线程池: 虚拟线程不能提高 CPU 利用率

八、总结:并发编程的哲学

回看整个并发编程的发展史,从 synchronized 到 AQS,从 CAS 到 LongAdder,从线程池到虚拟线程,核心追求从未改变:以最低的成本实现正确的同步

几条贯穿始终的原则:

  1. 能不共享就不共享 — ThreadLocal、不可变对象、无状态设计
  2. 能不用锁就不用锁 — CAS、原子类、CopyOnWrite
  3. 要用锁就用对锁 — 细化锁粒度、避免死锁、按固定顺序加锁
  4. 让工具替你管理 — 使用 JUC 工具类而非手写同步
  5. 用数据说话 — JMH 压测、火焰图、Async Profiler 验证优化
  6. 拥抱新特性 — 虚拟线程将极大简化 IO 密集型并发代码

并发编程的魅力在于,它要求你同时理解硬件(CPU 缓存、内存屏障)、操作系统(线程调度、内核态切换)和语言特性(JMM、JUC)。当你能够自如地在这些层次间切换视角时,你就真正掌握了并发的精髓。


本文为每周深度技术教程系列。欢迎在评论区交流你的并发优化实战经验。