第 40 章:JUC 与 JVM
学习目标
- 掌握 JUC 常用工具类
- 理解 CAS 与 AQS
- 理解 JVM 内存结构与 GC
- 学会基本的 JVM 调优与排查
一、JUC 全景
二、原子类与 CAS
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // ++i,线程安全
count.getAndIncrement(); // i++
count.addAndGet(5);
count.compareAndSet(5, 10); // 期望是 5 就改成 10CAS 原理
CAS = Compare And Swap,由 CPU 的 cmpxchg 指令保证原子性,无需加锁。
三个问题:
| 问题 | 说明 | 解决 |
|---|---|---|
| ABA | 值从 A→B→A,CAS 以为没变 | AtomicStampedReference(加版本号) |
| 自旋开销 | 竞争激烈时一直失败重试,空耗 CPU | 用 LongAdder 或改用锁 |
| 只能保证一个变量 | 多变量无法原子更新 | AtomicReference 包成对象 |
高并发计数用 LongAdder
// ❌ 高竞争下 AtomicLong 自旋严重
AtomicLong counter = new AtomicLong();
// ✅ LongAdder:分段累加,最后求和
LongAdder adder = new LongAdder();
adder.increment();
long sum = adder.sum();原理:
AtomicLong所有线程抢一个变量;LongAdder内部维护一个 Cell 数组,不同线程改不同 Cell,最后求和。高并发下性能可以差 5-10 倍。
三、ReentrantLock
private final ReentrantLock lock = new ReentrantLock();
public void doWork() {
lock.lock(); // ① 加锁
try {
// 临界区
} finally {
lock.unlock(); // ② 必须放 finally!否则异常时死锁
}
}对比 synchronized
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 实现 | JVM 关键字 | JDK 类(AQS) |
| 释放锁 | 自动 | 必须手动 unlock |
| 可中断 | ❌ | ✅ lockInterruptibly() |
| 超时 | ❌ | ✅ tryLock(3, SECONDS) |
| 公平锁 | ❌ | ✅ new ReentrantLock(true) |
| 多条件变量 | ❌ 只有一个等待队列 | ✅ newCondition() |
// 独有能力:超时获取锁,避免无限等待
if (lock.tryLock(3, TimeUnit.SECONDS)) {
try { doWork(); } finally { lock.unlock(); }
} else {
log.warn("获取锁超时,降级处理");
}选择建议:默认用
synchronized(简单、不会忘记释放)。只有需要超时/中断/公平/多条件时才用ReentrantLock。
四、AQS 简介
AQS(AbstractQueuedSynchronizer) 是 ReentrantLock、CountDownLatch、Semaphore 的共同底层。
不同实现只是对 state 的含义定义不同:
| 类 | state 含义 |
|---|---|
ReentrantLock | 重入次数(0=未锁) |
CountDownLatch | 剩余计数 |
Semaphore | 剩余许可数 |
ReadWriteLock | 高 16 位读锁,低 16 位写锁 |
五、同步工具类
// ① 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 异步编排
// 串行: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+ 生产可用) | 超大堆、低延迟 |
# G1(推荐,JDK 17 默认)
-XX:+UseG1GC -XX:MaxGCPauseMillis=200
# ZGC(超低延迟)
-XX:+UseZGC十、常用 JVM 参数
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 文件基本无法定位。
十一、排查工具
# 查进程
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% 排查四步法
# ① 找出 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 | 需超时/中断/公平时才用 |
| AQS | JUC 锁的统一底层 |
| CompletableFuture | 必须传自定义线程池 |
| 堆 | 新生代(Eden+S0+S1)+ 老年代 |
| Full GC | 调优核心目标是减少它 |
| G1 | JDK 17 默认,通用推荐 |
| OOM 排查 | 必须开 HeapDumpOnOutOfMemoryError |
动手练习
练习 1:基础题
用 CountDownLatch 实现:主线程等待 5 个子线程全部完成后打印「全部完成」,并统计总耗时。
练习 2:进阶题
用 CompletableFuture 实现商品详情页聚合:并行调用「商品基本信息」「库存」「评价数」三个接口(各 sleep 100ms),合并结果。对比串行版本的耗时。
练习 3:实操题
写一段代码故意造成 OOM(不断往 List 里加对象),加上 -Xmx64m -XX:+HeapDumpOnOutOfMemoryError,用 VisualVM 打开 dump 文件找出占内存最大的对象。
🎉 Java 基础部分完结
恭喜!你已经学完了 Java 基础的 40 章。接下来进入 SpringBoot 企业级开发部分。
下一章:第 41 章:Spring 生态全景 →