Java 并发原理与性能优化:从 JMM 到高并发实战

一、开篇:并发编程的三大困境

并发编程被誉为 Java 开发者的”成年礼”——几乎所有经历过生产环境重大事故的工程师,最终都会回到同一个原点:对并发原理的理解不够透彻。

从 CPU 缓存一致性到指令重排序,从 volatile 的可见性语义到 synchronized 的锁升级路径,这些底层机制决定了你写出的代码究竟是在”并发执行”还是在”碰运气”。

本文将从 Java 内存模型(JMM)出发,沿着”原理 → 工具 → 模式 → 调优”的路径,系统性地梳理 Java 并发编程的核心知识体系。无论你是正在准备面试的中级工程师,还是面对高并发生产环境的高级开发者,这篇文章都能给你一张可落地的”并发地图”。


二、Java 内存模型(JMM):并发的地基

2.1 为什么需要 JMM?

现代 CPU 架构下,每个核心都有自己的 L1/L2 缓存。线程在 CPU 上执行时,操作的并不是主内存(RAM),而是 CPU 缓存。这就带来了 缓存一致性 问题:

线程 A (Core 0)          线程 B (Core 1)
    │                        │
    ▼                        ▼
  L1 Cache ◄──────────────► L1 Cache
    │                        │
    └──────────┬─────────────┘
               ▼
          Main Memory

JMM 的核心工作就是:定义一套规则,规定一个线程对共享变量的写入何时对另一个线程可见。

2.2 JMM 的三大特性

2.3 happens-before 规则(面试必考)

JMM 通过 happens-before 关系来保证有序性。如果两个操作之间存在 happens-before 关系,那么第一个操作的结果对第二个操作可见。

八大规则中,最常被问到的:

  1. 程序顺序规则:一个线程中的每个操作,happens-before 于该线程中的任意后续操作
  2. volatile 规则:对一个 volatile 变量的写,happens-before 于任意后续对这个变量的读
  3. 锁规则:对一个锁的解锁,happens-before 于后续对这个锁的加锁
  4. 传递性:如果 A happens-before B,且 B happens-before C,则 A happens-before C
// 经典示例:volatile 保证可见性
public class ThreadStopExample {
    // 不加 volatile → 可能永远无法退出循环
    private volatile boolean running = true;

    public void run() {
        new Thread(() -> {
            while (running) {
                // do work
            }
            System.out.println("线程退出");
        }).start();
    }

    public void stop() {
        running = false; // 写 volatile → 对读线程可见
    }
}

三、synchronized 深度解析:从偏向锁到重量锁

3.1 锁升级过程

JDK 6 之后,synchronized 经历了大重构。不再是”重量级锁”的代名词,而是引入了锁升级机制:

无锁 → 偏向锁 → 轻量级锁 → 重量级锁
// 观察锁状态(需开启偏向锁:-XX:+UseBiasedLocking)
public class LockUpgradeDemo {
    static final Object lock = new Object();

    public static void main(String[] args) throws Exception {
        // 1. 无锁状态:object header = 0x0000000000000001
        Thread.sleep(5000); // 偏向锁激活延迟约4秒

        // 2. 偏向锁:同一个线程多次获取
        synchronized (lock) {
            System.out.println("偏向锁");
        }

        // 3. 轻量级锁:另一个线程短暂竞争
        Thread t2 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("轻量级锁");
            }
        });
        t2.start();
        t2.join();

        // 4. 重量级锁:长时间自旋竞争
        // 使用 -XX:PreBlockSpin=10 控制自旋次数
    }
}

3.2 锁消除与锁粗化

JIT 编译器在运行时还会做两个重要优化:

    • 锁消除:检测到锁对象只在当前线程使用 → 直接去掉锁
  // JIT 会消除这里的锁,因为 sb 不会逃逸
  public static String concat(String s1, String s2) {
      StringBuilder sb = new StringBuilder();
      sb.append(s1);
      sb.append(s2);
      return sb.toString();
  }
    • 锁粗化:连续对同一对象加锁 → 合并为一次
  // JIT 会将三次加锁合并为一次
  for (int i = 0; i < 100; i++) {
      synchronized (lock) {
          count++;
      }
  }

四、AQS 架构:JUC 锁的基石

4.1 AQS 的设计哲学

AbstractQueuedSynchronizer(AQS) 是 JUC 包(java.util.concurrent)的基石。ReentrantLock、CountDownLatch、Semaphore、ThreadPoolExecutor 的核心都是 AQS。

AQS 维护两个核心变量:

public abstract class AbstractQueuedSynchronizer {
    // 1. 同步状态(volatile 保证可见性)
    //    0 = 未锁定, 1 = 锁定, >1 = 可重入计数
    private volatile int state;

    // 2. CLH 变体队列(双向链表 + CAS 入队)
    private transient volatile Node head;
    private transient volatile Node tail;
}

4.2 独占锁 vs 共享锁

// 独占锁模式示例:ReentrantLock
// acquire(1) → tryAcquire(1) → 失败则入队并阻塞
// release(1) → tryRelease(1) → 成功则唤醒后继节点

// 共享锁模式示例:CountDownLatch
// acquireShared(1) → tryAcquireShared(1) → 负数表示失败
// releaseShared(1) → tryReleaseShared(1) → 成功则广播唤醒

4.3 自定义同步器实战

public class SimpleLatch {
    private static final class Sync extends AbstractQueuedSynchronizer {
        @Override
        protected int tryAcquireShared(int acquires) {
            // count == 0 时通过
            return getState() == 0 ? 1 : -1;
        }

        @Override
        protected boolean tryReleaseShared(int releases) {
            // 每次释放将 count -1
            for (;;) {
                int c = getState();
                if (c == 0) return false;
                int next = c - 1;
                if (compareAndSetState(c, next)) {
                    return next == 0;
                }
            }
        }
    }

    private final Sync sync = new Sync();

    public void countDown() { sync.releaseShared(1); }

    public void await() { sync.acquireShared(1); }
}

关键理解:AQS 的核心不是 Lock,而是一个 状态机 + 阻塞队列 的通用框架。任何需要”资源不够就排队”的场景,都可以用 AQS 实现。


五、线程池源码级剖析

5.1 ThreadPoolExecutor 的核心变量

// 用 int 高 3 位存状态,低 29 位存线程数(经典位运算)
// RUNNING  = 111 | 000...0
// SHUTDOWN = 000 | 000...0
// STOP     = 001 | 000...0
// TIDYING  = 010 | 000...0
// TERMINATED = 011 | 000...0
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

5.2 提交任务的完整流程

public void execute(Runnable command) {
    int c = ctl.get();
    // Step 1: 工作线程 < corePoolSize → 新建核心线程
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // Step 2: 线程池运行中 → 尝试入队
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        // 二次检查:状态变了则回滚
        if (!isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // Step 3: 队列满 → 尝试新建非核心线程
    else if (!addWorker(command, false))
        // Step 4: 也失败了 → 拒绝策略
        reject(command);
}

5.3 四种拒绝策略的选择

5.4 最佳实践:动态线程池

@Slf4j
public class DynamicThreadPool {
    private final ThreadPoolExecutor executor;

    public DynamicThreadPool(int core, int max, int queueSize) {
        this.executor = new ThreadPoolExecutor(
            core, max, 60, TimeUnit.SECONDS,
            new LinkedBlockingQueue(queueSize),
            new ThreadPoolExecutor.CallerRunsPolicy()
        );
        // 开启监控线程
        monitor();
    }

    private void monitor() {
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
        scheduler.scheduleAtFixedRate(() -> {
            log.info("线程池状态 - 活跃: {}, 队列: {}, 完成: {}, 峰值: {}",
                executor.getActiveCount(),
                executor.getQueue().size(),
                executor.getCompletedTaskCount(),
                executor.getLargestPoolSize()
            );

            // 动态调整:队列积压 → 临时扩容
            int queueSize = executor.getQueue().size();
            if (queueSize > 100 && executor.getMaximumPoolSize() < 200) {
                executor.setMaximumPoolSize(200);
                executor.setCorePoolSize(100);
                log.warn("队列积压 {},临时扩容至 core=100, max=200", queueSize);
            }
        }, 0, 30, TimeUnit.SECONDS);
    }
}

六、常见并发陷阱与最佳实践

6.1 陷阱一:this 引用逃逸

// ❌ 错误:构造期间发布 this
public class ThisEscape {
    public ThisEscape() {
        new Thread(() -> {
            // 此时 this 可能尚未构造完成!
            doSomething();
        }).start();
    }
}

// ✅ 正确:私有构造器 + 工厂方法
public class SafePublication {
    private final int value;

    private SafePublication(int v) { this.value = v; }

    public static SafePublication create(int v) {
        SafePublication obj = new SafePublication(v);
        // 等完全构造后再启动线程
        return obj;
    }
}

6.2 陷阱二:双重检查锁定(DCL)与 volatile

// ❌ 错误:缺少 volatile,可能读到半构造对象
private static Singleton instance;

public static Singleton getInstance() {
    if (instance == null) {                // 第1次检查
        synchronized (Singleton.class) {
            if (instance == null) {        // 第2次检查
                instance = new Singleton(); // 可能指令重排序!
            }
        }
    }
    return instance;
}

// ✅ 正确:volatile 禁止重排序
private static volatile Singleton instance;

6.3 陷阱三:ConcurrentHashMap 的错误使用

// ❌ 错误:get 和 put 不是原子操作
if (!map.containsKey(key)) {
    map.put(key, value);  // 两个线程可能同时进入
}

// ✅ 正确:使用原子方法
map.putIfAbsent(key, value);
// 或 computeIfAbsent
map.computeIfAbsent(key, k -> computeExpensive(k));

6.4 陷阱四:ThreadLocal 内存泄漏

// ❌ 错误:线程池中使用不清理
private static final ThreadLocal ctx = new ThreadLocal();

public void handleRequest() {
    ctx.set(new Context());  // 线程复用,上次的 Context 没清理
    // ... 业务逻辑
    // ctx.remove(); // 忘记清理 → OOM
}

// ✅ 正确:finally 块中确保清理
try {
    ctx.set(context);
    // ... 业务逻辑
} finally {
    ctx.remove();
}

七、性能调优实战案例

7.1 场景:高并发 API 网关限流

问题:QPS 5000 的 API 网关,用 synchronized 做计数器,导致 CPU 飙升。

❌ 原始方案:synchronized 计数器
      QPS 5000 → 50% 线程在竞争同一把锁 → CPU 100%

✅ 优化方案:LongAdder 分段累加
      QPS 5000 → 几乎无锁竞争 → CPU 35%
public class RateLimiter {
    // ❌ 错误:synchronized 全局竞争
    private final AtomicLong counter = new AtomicLong(0);

    // ✅ 正确:LongAdder 分段累加(Striped64 思想)
    private final LongAdder counter2 = new LongAdder();
    // LongAdder 内部维护一个 Cell[] 数组
    // 每个线程映射到不同 Cell,最后 sum() 时汇总
}

7.2 场景:数据库连接池参数调优

生产环境实测数据(8核16G JDBC 连接池):

连接数  | TPS   | 平均延迟 | P99 延迟
10     | 1200  | 5ms     | 18ms
20     | 1800  | 8ms     | 32ms
50     | 2100  | 18ms    | 85ms     ← 连接数过多导致上下文切换
100    | 1900  | 35ms    | 220ms    ← 明显过载

结论:连接数 ≈ (核数 × 2) 通常是最优值

7.3 场景:生产者-消费者模式优化

public class OptimizedPipeline {
    // 使用 disruptor 风格的环形缓冲区思想
    private final BlockingQueue queue =
        // LinkedBlockingQueue: 可扩容但加锁
        // ConcurrentLinkedQueue: 无锁但无阻塞

        // 折中方案:有界队列 + 合理容量
        new LinkedBlockingQueue(1024);

    // Java 21 虚拟线程(Virtual Threads)的应用
    public void processWithVT() {
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i  handleTask(taskId));
            }
        }
    }
}

八、监控与诊断工具

8.1 快速定位死锁

# 1. 获取 Java 进程 PID
jps -l

# 2. 打印线程转储(含死锁检测)
jstack 

# 3. 输出示例:
Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x00007f... (object: LockB)
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x00007f... (object: LockA)
  which is held by "Thread-1"

8.2 JMC(Java Mission Control)飞行记录器

# 启动时录制(生产友好,低开销)
-XX:StartFlightRecording=duration=60s,filename=recording.jfr

# 或运行时动态开启
jcmd  JFR.start duration=60s filename=recording.jfr

8.3 Arthas 在线诊断

# 查看线程池状态
thread -n 5  # 显示最忙的5个线程
stack com.example.MyService#method  # 查看方法调用栈

# 查看锁竞争情况
sc -d com.example.MyLock  # 查看对象的 monitor 信息

九、Java 21+ 的新并发特性

9.1 虚拟线程(Virtual Threads)

Java 21 正式 GA 的虚拟线程,彻底改变了并发编程的性价比模型:

// 之前:2000个平台线程就很吃力
ExecutorService pt = Executors.newFixedThreadPool(200);

// 现在:20万个虚拟线程轻松运行
try (var vt = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 200_000).forEach(i ->
        vt.submit(() -> handleRequest(i))
    );
}

适用场景:IO 密集型(数据库查询、RPC 调用、HTTP 请求)

注意事项:

  • 避免 synchronized 阻塞虚拟线程(会导致 Pin 到平台线程)
  • 推荐使用 ReentrantLock 替代 synchronized
  • 虚拟线程不适用于 CPU 密集型任务

9.2 ScopedValue(结构化并发)

// 替代 ThreadLocal,更安全、更高效
private static final ScopedValue USER_ID = ScopedValue.newInstance();

public void handleRequest() {
    ScopedValue.where(USER_ID, "user-001").run(() -> {
        // 这里的代码可以直接访问 USER_ID
        String uid = USER_ID.get(); // "user-001"
        service.call();
    });
    // 离开 scope 后自动清理,不会泄漏
}

十、总结与推荐阅读路径

核心学习路径

第一阶段 原理篇
  └─ JMM + happens-before + volatile/synchronized 原理
  └─ 推荐:《Java并发编程的艺术》第1-3章

第二阶段 工具篇
  └─ JUC 包:AQS + Lock + ConcurrentHashMap + ThreadPool
  └─ 推荐:阅读 JDK 源码(java.util.concurrent 包)

第三阶段 实战篇
  └─ 性能调优 + 故障诊断 + 监控工具
  └─ 推荐:《Java性能权威指南》+ Arthas 官方文档

第四阶段 前瞻篇
  └─ 虚拟线程 + 结构化并发 + Project Loom
  └─ 推荐:JEP 444 (Virtual Threads) + JEP 429 (Scoped Values)

一句话记住

> 并发编程的本质,是对”可见性、原子性、有序性”的理解和权衡。理解 JMM 你就能看懂每一行同步代码的背后发生了什么,理解 AQS 你就能驾驭整个 JUC 生态。


参考资源