Skip to content
第 40 / 250 章后端⏱ 10 分钟阅读

第 40 章:JUC 与 JVM

学习目标

  • 掌握 JUC 常用工具类
  • 理解 CAS 与 AQS
  • 理解 JVM 内存结构与 GC
  • 学会基本的 JVM 调优与排查

一、JUC 全景

二、原子类与 CAS

java
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();          // ++i,线程安全
count.getAndIncrement();          // i++
count.addAndGet(5);
count.compareAndSet(5, 10);       // 期望是 5 就改成 10

CAS 原理

CAS = Compare And Swap,由 CPU 的 cmpxchg 指令保证原子性,无需加锁

三个问题

问题说明解决
ABA值从 A→B→A,CAS 以为没变AtomicStampedReference(加版本号)
自旋开销竞争激烈时一直失败重试,空耗 CPULongAdder 或改用锁
只能保证一个变量多变量无法原子更新AtomicReference 包成对象

高并发计数用 LongAdder

java
// ❌ 高竞争下 AtomicLong 自旋严重
AtomicLong counter = new AtomicLong();

// ✅ LongAdder:分段累加,最后求和
LongAdder adder = new LongAdder();
adder.increment();
long sum = adder.sum();

原理AtomicLong 所有线程抢一个变量;LongAdder 内部维护一个 Cell 数组,不同线程改不同 Cell,最后求和。高并发下性能可以差 5-10 倍

三、ReentrantLock

java
private final ReentrantLock lock = new ReentrantLock();

public void doWork() {
    lock.lock();                         // ① 加锁
    try {
        // 临界区
    } finally {
        lock.unlock();                   // ② 必须放 finally!否则异常时死锁
    }
}

对比 synchronized

维度synchronizedReentrantLock
实现JVM 关键字JDK 类(AQS)
释放锁自动必须手动 unlock
可中断lockInterruptibly()
超时tryLock(3, SECONDS)
公平锁new ReentrantLock(true)
多条件变量❌ 只有一个等待队列newCondition()
java
// 独有能力:超时获取锁,避免无限等待
if (lock.tryLock(3, TimeUnit.SECONDS)) {
    try { doWork(); } finally { lock.unlock(); }
} else {
    log.warn("获取锁超时,降级处理");
}

选择建议默认用 synchronized(简单、不会忘记释放)。只有需要超时/中断/公平/多条件时才用 ReentrantLock

四、AQS 简介

AQS(AbstractQueuedSynchronizer)ReentrantLockCountDownLatchSemaphore 的共同底层。

不同实现只是对 state 的含义定义不同:

state 含义
ReentrantLock重入次数(0=未锁)
CountDownLatch剩余计数
Semaphore剩余许可数
ReadWriteLock高 16 位读锁,低 16 位写锁

五、同步工具类

java
// ① CountDownLatch:等待 N 个任务完成(一次性)
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
    pool.execute(() -> {
        try { doWork(); } finally { latch.countDown(); }   // 计数 -1
    });
}
latch.await();                    // 阻塞直到计数为 0
System.out.println("全部完成");

// ② Semaphore:限流,控制同时访问的线程数
Semaphore semaphore = new Semaphore(10);
semaphore.acquire();              // 拿许可,没有就阻塞
try { callThirdPartyApi(); } finally { semaphore.release(); }

// ③ CyclicBarrier:所有线程到齐后一起继续(可重复使用)
CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("集合完毕"));
barrier.await();
工具场景
CountDownLatch主线程等待多个子任务完成
Semaphore限制并发数(连接池、接口限流)
CyclicBarrier多线程分阶段同步

六、CompletableFuture 异步编排

java
// 串行:A 完成后用 A 的结果做 B
CompletableFuture.supplyAsync(() -> getUserId(), pool)
        .thenApply(id -> getUserById(id))
        .thenAccept(user -> System.out.println(user));

// 并行:A 和 B 同时跑,都完成后合并
CompletableFuture<User> f1 = CompletableFuture.supplyAsync(this::getUser, pool);
CompletableFuture<Order> f2 = CompletableFuture.supplyAsync(this::getOrder, pool);

CompletableFuture<String> result = f1.thenCombine(f2,
        (user, order) -> user.getName() + " 的订单: " + order.getId());

// 等待全部完成
CompletableFuture.allOf(f1, f2).join();

// 异常处理
CompletableFuture.supplyAsync(() -> riskyCall(), pool)
        .exceptionally(e -> {                    // ① 出异常时的兜底值
            log.error("调用失败", e);
            return defaultValue();
        })
        .orTimeout(3, TimeUnit.SECONDS);         // ② JDK 9+ 超时控制

⚠️ 必须传自定义线程池。不传的话用 ForkJoinPool.commonPool(),它的线程数 = CPU 核数 - 1,一旦有阻塞任务会拖垮整个应用的所有异步逻辑。

实战价值:接口原本串行调 3 个服务各 100ms = 300ms,并行后 = 100ms。

七、JVM 内存结构

区域存什么常见异常
所有对象实例OutOfMemoryError: Java heap space
元空间类元信息(JDK 8 后用本地内存)OutOfMemoryError: Metaspace
虚拟机栈方法调用的栈帧StackOverflowError(递归太深)
程序计数器字节码行号唯一不会 OOM 的区域

八、对象的一生

GC 类型范围影响
Minor GC / Young GC新生代频繁但快(几毫秒)
Major GC / Old GC老年代
Full GC整个堆 + 元空间最慢,STW 可能几秒

调优的核心目标:减少 Full GC 的频率和时长。

九、垃圾回收器选择

回收器特点适用
Serial单线程客户端小应用
Parallel多线程,吞吐优先后台计算任务
CMS并发标记清除(JDK 14 已移除)已淘汰
G1分区,可预测停顿(JDK 9+ 默认)通用推荐
ZGC停顿 < 1ms(JDK 15+ 生产可用)超大堆、低延迟
bash
# G1(推荐,JDK 17 默认)
-XX:+UseG1GC -XX:MaxGCPauseMillis=200

# ZGC(超低延迟)
-XX:+UseZGC

十、常用 JVM 参数

bash
java -jar app.jar \
  -Xms2g -Xmx2g \                              # ① 堆初始/最大,设成一样避免动态扩容抖动
  -Xss512k \                                   # ② 单线程栈大小
  -XX:MetaspaceSize=256m \
  -XX:MaxMetaspaceSize=512m \
  -XX:+UseG1GC \
  -XX:MaxGCPauseMillis=200 \                   # ③ 期望的最大停顿时间
  -XX:+HeapDumpOnOutOfMemoryError \            # ④ OOM 时自动 dump,排查救命
  -XX:HeapDumpPath=/data/dump/ \
  -Xlog:gc*:file=/data/gc.log:time,uptime:filecount=5,filesize=100M

④ 强烈建议加上。线上 OOM 是偶发的,没有 dump 文件基本无法定位。

十一、排查工具

bash
# 查进程
jps -l

# 看堆内存使用(每秒刷新一次,共 10 次)
jstat -gcutil <pid> 1000 10

# 看堆里对象占用 Top 20
jmap -histo:live <pid> | head -20

# 导出堆快照(用 MAT / VisualVM 分析)
jmap -dump:format=b,file=heap.hprof <pid>

# 看线程栈(排查死锁、CPU 高)
jstack <pid> > thread.txt

# 看 JVM 参数
jinfo -flags <pid>

CPU 100% 排查四步法

bash
# ① 找出 CPU 高的 Java 进程
top

# ② 找出该进程内 CPU 高的线程(记下 TID)
top -Hp <pid>

# ③ TID 转 16 进制
printf "%x\n" <tid>

# ④ 在线程栈中定位
jstack <pid> | grep -A 30 <十六进制tid>

生产环境更推荐 Arthas(见第 77 章),一条 thread -n 3 就能列出 CPU 最高的 3 个线程及其堆栈。

十二、本章小结

要点关键
CAS无锁,有 ABA 问题
LongAdder高并发计数首选
ReentrantLock需超时/中断/公平时才用
AQSJUC 锁的统一底层
CompletableFuture必须传自定义线程池
新生代(Eden+S0+S1)+ 老年代
Full GC调优核心目标是减少它
G1JDK 17 默认,通用推荐
OOM 排查必须开 HeapDumpOnOutOfMemoryError

动手练习

练习 1:基础题

CountDownLatch 实现:主线程等待 5 个子线程全部完成后打印「全部完成」,并统计总耗时。

练习 2:进阶题

CompletableFuture 实现商品详情页聚合:并行调用「商品基本信息」「库存」「评价数」三个接口(各 sleep 100ms),合并结果。对比串行版本的耗时。

练习 3:实操题

写一段代码故意造成 OOM(不断往 List 里加对象),加上 -Xmx64m -XX:+HeapDumpOnOutOfMemoryError,用 VisualVM 打开 dump 文件找出占内存最大的对象。


🎉 Java 基础部分完结

恭喜!你已经学完了 Java 基础的 40 章。接下来进入 SpringBoot 企业级开发部分。

下一章第 41 章:Spring 生态全景

本站基于 VitePress 构建 · 由 Codebook 团队维护