01、Java并发编程:学习攻略如何才能学好并发编程?一文搞懂分工、同步、互斥

你是不是也有这种感觉:Java并发这东西,书买了一堆,博客看了几百篇,synchronized、Lock、volatile、AQS、ThreadLocal、ConcurrentHashMap、ThreadPoolExecutor 每个单词都认识,拼在一起就头大。面试前背得滚瓜烂熟,一到线上排查问题,jstack 日志一拉,一堆 BLOCKED、WAITING 的线程,大脑当场空白。
我自己当年也是这么过来的。最早一头扎进 JUC 包,ArrayList 换成 CopyOnWriteArrayList,HashMap 换成 ConcurrentHashMap,觉得这就叫并发编程了。直到线上一个订单系统卡死,CPU 使用率 0%,所有业务线程都挂在 Future.get() 上,我才意识到:我学的不是并发,是 JDK API 使用说明书。
这篇文章不讲零散 API,只做一件事:帮你建立一张并发编程的全景地图。这张地图的骨架来自 Doug Lea 对并发问题的拆解,也是我在无数次线上故障排查中反复验证过的思考框架。读完之后,你再看到任何一个并发类、任何一个线上 Bug,都能迅速定位它属于哪一类问题,该用什么工具解决。
文章目录
- 01、Java并发编程:学习攻略如何才能学好并发编程?一文搞懂分工、同步、互斥
-
- 一、痛点场景:线上那些让你凌晨三点跳起来的并发事故
-
- [痛点 1:CPU 使用率 0%,服务却全线卡死](#痛点 1:CPU 使用率 0%,服务却全线卡死)
- [痛点 2:明明加了 synchronized,为什么数据还是丢了](#痛点 2:明明加了 synchronized,为什么数据还是丢了)
- [痛点 3:单例模式写了十年,上线第一天就 OOM](#痛点 3:单例模式写了十年,上线第一天就 OOM)
- 二、解决方案:并发编程其实只有三个核心问题
- 三、是什么:用三种视角把并发编程讲透
-
- [3.1 专业解释](#3.1 专业解释)
- [3.2 大白话](#3.2 大白话)
- [3.3 生活案例](#3.3 生活案例)
- 四、为什么要学:不学并发会怎样
-
- [4.1 性能层面](#4.1 性能层面)
- [4.2 正确性层面](#4.2 正确性层面)
- [4.3 职业发展层面](#4.3 职业发展层面)
- 五、演进路线:并发编程是怎么一步步发展到今天的
-
- [5.1 石器时代:Java 1.0 - 1.4](#5.1 石器时代:Java 1.0 - 1.4)
- [5.2 JUC 时代:Java 5(JSR 166)](#5.2 JUC 时代:Java 5(JSR 166))
- [5.3 函数式异步时代:Java 8](#5.3 函数式异步时代:Java 8)
- [5.4 虚拟线程时代:Java 21](#5.4 虚拟线程时代:Java 21)
- 六、怎么用:企业项目里的实战代码
-
- [6.1 分工:让多个线程同时干活](#6.1 分工:让多个线程同时干活)
-
- [6.1.1 正确姿势:手动创建 ThreadPoolExecutor](#6.1.1 正确姿势:手动创建 ThreadPoolExecutor)
- [6.1.2 并行调用多个下游服务](#6.1.2 并行调用多个下游服务)
- [6.1.3 生产者-消费者模式](#6.1.3 生产者-消费者模式)
- [6.2 同步:让线程之间步调一致](#6.2 同步:让线程之间步调一致)
-
- [6.2.1 CountDownLatch:等所有子任务完成](#6.2.1 CountDownLatch:等所有子任务完成)
- [6.2.2 CyclicBarrier:多线程互相等待到同一个屏障](#6.2.2 CyclicBarrier:多线程互相等待到同一个屏障)
- [6.3 互斥:保护共享变量](#6.3 互斥:保护共享变量)
-
- [6.3.1 synchronized:能不加锁就不加锁,加锁就用它](#6.3.1 synchronized:能不加锁就不加锁,加锁就用它)
- [6.3.2 ReentrantLock:需要高级功能时才用](#6.3.2 ReentrantLock:需要高级功能时才用)
- [6.3.3 volatile:解决可见性,不解决原子性](#6.3.3 volatile:解决可见性,不解决原子性)
- [6.3.4 无锁方案:原子类](#6.3.4 无锁方案:原子类)
- [6.3.5 线程封闭:从根上避免共享](#6.3.5 线程封闭:从根上避免共享)
- [七、竞品对比:JDK 里那些"长得像"的东西到底怎么选](#七、竞品对比:JDK 里那些"长得像"的东西到底怎么选)
-
- [7.1 synchronized vs ReentrantLock](#7.1 synchronized vs ReentrantLock)
- [7.2 Executors 工厂方法 vs 手动 ThreadPoolExecutor](#7.2 Executors 工厂方法 vs 手动 ThreadPoolExecutor)
- [7.3 ConcurrentHashMap vs HashMap vs Collections.synchronizedMap](#7.3 ConcurrentHashMap vs HashMap vs Collections.synchronizedMap)
- [7.4 CountDownLatch vs CyclicBarrier](#7.4 CountDownLatch vs CyclicBarrier)
- 八、企业项目里的高频使用场景
- 九、面试官高频面试题(建议背下来)
- 十、总结
一、痛点场景:线上那些让你凌晨三点跳起来的并发事故
在讲理论之前,先看三个真实发生过的生产事故。这三个事故覆盖了企业里 90% 以上的并发故障类型,看完你就知道为什么必须学并发。
痛点 1:CPU 使用率 0%,服务却全线卡死
这是最诡异的一类故障。监控大盘显示 CPU 闲得发慌,内存也没爆,但接口全部超时,用户疯狂投诉。运维拉一份 jstack 出来一看,所有业务线程都停在同一个地方。

典型的代码长这样:
java
// 订单服务:线程池核心线程 20 个
ExecutorService orderPool = new ThreadPoolExecutor(
20, 20, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(1000));
// 主任务:处理一笔订单,内部要调用库存服务
public void handleOrder(Order order) {
Future<StockResult> future = orderPool.submit(() -> queryStock(order));
StockResult result = future.get(); // 阻塞等子任务
// ... 后续处理
}
高峰期 20 个核心线程全部被主任务占满,每个主任务都 submit 了一个子任务然后 get() 阻塞等待。子任务排在队列里永远得不到执行,因为没有空闲线程;主任务又永远卡在 get() 上不肯释放线程。整个线程池瞬间死锁,这就是经典的线程池饥饿死锁(Thread Pool Starvation Deadlock)。
更隐蔽的是 CompletableFuture.supplyAsync() 默认用的是 ForkJoinPool.commonPool(),这个池子的线程数默认是 CPU 核数。你在里面做 HTTP 调用、数据库查询这种阻塞操作,几个并发请求就把公共池占满了,整个应用所有用到 CompletableFuture 的地方全部躺平。
痛点 2:明明加了 synchronized,为什么数据还是丢了
新人最容易犯的错:给 i++ 加了 synchronized,觉得万事大吉,结果高并发下计数器还是少加了。或者更隐蔽------写操作加了锁,读操作没加,读到一个半更新的对象,金额字段是新的,状态字段还是旧的,对账系统直接炸了。
java
public class BadCounter {
private int count = 0;
public void incr() {
synchronized (this) {
count++;
}
}
// 读没加锁,可能拿到中间态
public int get() {
return count;
}
}
问题不在锁本身,在于你根本没意识到:共享变量的跨线程可见性、指令重排序、64 位 long 的非原子读写,这些问题就算加了 synchronized 也未必能完全解决------因为你的锁粒度可能选错了,或者你该用 volatile 的地方用了普通变量,编译器和 CPU 偷偷把指令重排了,单线程下跑得好好的,多线程下就是玄学 Bug。
痛点 3:单例模式写了十年,上线第一天就 OOM
java
public class ContextHolder {
private static Map<String, UserContext> CACHE = new HashMap<>();
public static UserContext get(String userId) {
return CACHE.computeIfAbsent(userId, id -> loadFromDb(id));
}
}
这段代码在单线程下毫无问题。高并发下 HashMap 链表在扩容时可能形成环形链表,读操作 CPU 打满,老年代迅速撑爆,Full GC 停不下来。线上一次大促,这个全局共享的 HashMap 被并发扩容打环,读线程陷入死循环,线程池被瞬间耗尽,千万级资损。
这类事故的共同点是:代码在测试环境跑半年没事,一到生产高并发就炸。因为并发 Bug 是概率性的、时机敏感的、不可复现的,你单元测试覆盖率 100% 也抓不到。
二、解决方案:并发编程其实只有三个核心问题
上面三个痛点,看起来五花八门,本质上都能被归纳到三个核心问题上。这是原作者在文章里反复强调的观点,也是 Doug Lea 设计 JUC 包时的底层思考框架。

- 分工:怎么把一个大任务拆成多个子任务,交给多个线程去做,谁先做谁后做,谁做什么。
- 同步:任务之间有依赖,一个任务做完了,怎么通知下一个任务开工。
- 互斥:多个线程同时访问同一个共享变量,怎么保证结果正确。
你以后再看到任何一个并发类、任何一个线上故障,先问自己三个问题:这是分工没做好?同步没做好?还是互斥没做好?分类清楚了,方案自然就出来了。
三、是什么:用三种视角把并发编程讲透
3.1 专业解释
并发编程(Concurrent Programming)是指在操作系统层面,通过时间片轮转让多个线程在单个或多个 CPU 核心上逻辑上同时推进,从而提高资源利用率和系统吞吐量的编程范式。它解决的核心矛盾是:CPU 速度与 I/O 速度之间的数量级差异。一个数据库查询要 50ms,CPU 在这 50ms 里可以执行上亿条指令,不让它闲着就是并发编程存在的全部意义。
在 Java 里,并发的载体是 Thread,底层依赖操作系统的用户态线程调度;Java 5 之后通过 JUC(java.util.concurrent)包提供了一整套分工、同步、互斥的工具;Java 21 引入虚拟线程(Virtual Thread),把线程调度权从操作系统收回到 JVM,进一步降低了并发编程的门槛。
3.2 大白话
你可以把并发编程理解成开餐馆。
一个厨师(单线程)炒菜,客人点 10 个菜,只能一个一个炒,客人等到饿晕。你招 10 个厨师(多线程),同时开工,10 个菜一起出,这就是并发。
但是光招厨师不够:
- 10 个厨师同时抢一口锅,锅只有一个,这就是互斥问题。
- 厨师 A 炒完了青菜,需要厨师 B 拿盘子端走,A 怎么知道 B 准备好了?这就是同步问题。
- 今天来了 100 桌客人,怎么派活?谁炒川菜谁炒粤菜,谁先做谁后做,这就是分工问题。
整个 JUC 包,就是 Doug Lea 给你准备好的一套餐馆管理制度。
3.3 生活案例
再举个例子:你写一篇 10000 字的报告。
- 单线程:你自己从头到尾写,查资料、写大纲、写正文、排版,串行,一天写 2000 字,五天交稿。
- 多线程分工:你负责写正文,同学 A 负责查资料,同学 B 负责排版。查资料和写正文可以部分并行,效率翻倍。
- 同步:你写到一半发现要引用某个数据,停下来等 A 把资料发你,这就是
Future.get();A 查完资料主动发消息告诉你,这就是事件回调。 - 互斥:你和 B 同时修改同一段 Word 文档,B 刚改完第三段,你又把它覆盖回旧版本,文档就乱了。所以同一时刻只能有一个人改文档,这就是锁。
理解了这三个类比,你就理解了并发编程 80% 的本质。
四、为什么要学:不学并发会怎样
4.1 性能层面
一台 32 核的服务器,如果你只写单线程代码,CPU 利用率上限是 3%。剩下 31 核在那儿空转,你付了 32 核的钱,只用了 1 核。高并发场景下,一个接口串行调三个下游服务,每个 100ms,接口耗时 300ms;用 CompletableFuture 并行调用,耗时 100ms,吞吐量直接翻 3 倍。这就是为什么大厂面试必问并发------因为它直接关系到系统性能和公司服务器成本。
4.2 正确性层面
互联网系统本质上是共享资源的系统。库存只有 100 件,1000 个用户同时下单;账户余额 1000 块,两个客户端同时发起转账。没有正确的互斥保护,超卖、丢单、重复扣款这些资金事故就是必然事件,不是偶然事件。
4.3 职业发展层面
我面过的中高级 Java 岗位,JD 里写"熟悉多线程并发编程"的概率是 100%。初级开发写 CRUD,高级开发要写并发安全的中间件,架构师要设计整个系统的并发模型。并发能力是区分初级和高级的硬门槛。
五、演进路线:并发编程是怎么一步步发展到今天的

5.1 石器时代:Java 1.0 - 1.4
只有最原始的 Thread、Runnable、synchronized、wait/notify。开发者自己 new 线程、自己管生命周期、自己写锁,相当于手工打铁。这个阶段的问题是:线程太贵(一个线程默认栈 1MB),创建销毁开销大,没有线程池,没有并发集合,写出的程序稍微复杂一点就死锁。
5.2 JUC 时代:Java 5(JSR 166)
这是并发编程历史上最重要的一次升级。Doug Lea 把他之前在 util.concurrent 包里的成熟设计贡献给 JDK,才有了今天的 java.util.concurrent。核心成员包括:
- 分工:
ExecutorService、ThreadPoolExecutor、Future、Callable - 同步:
CountDownLatch、CyclicBarrier、Semaphore - 互斥:
ReentrantLock、Condition、ReentrantReadWriteLock - 并发容器:
ConcurrentHashMap、BlockingQueue - 原子类:
AtomicInteger、AtomicLong
Java 6 又做了两件大事:给 synchronized 做了偏向锁、轻量级锁、自旋锁的优化;引入了 ForkJoinPool。
5.3 函数式异步时代:Java 8
CompletableFuture 登场,把异步编程从"回调地狱"拉回到链式调用风格。同时 StampedLock 提供了比读写锁更快的乐观读模式,LongAdder 在高并发计数场景下比 AtomicLong 性能好一个数量级。
5.4 虚拟线程时代:Java 21
JEP 444 正式预览虚拟线程。以前一个请求一个平台线程,线程数受 OS 限制(几百到几千就顶天);虚拟线程由 JVM 调度,一台机器可以开几百万个,写同步风格代码享受异步性能。配合 JEP 543 结构化并发,这是未来 5 年 Java 并发的主战场。
六、怎么用:企业项目里的实战代码
这一节按分工、同步、互斥三个维度给出可以直接抄进项目的代码。
6.1 分工:让多个线程同时干活
6.1.1 正确姿势:手动创建 ThreadPoolExecutor
阿里巴巴 Java 开发手册明确禁止用 Executors.newXXX() 工厂方法,因为它们的队列要么无界要么允许创建海量线程,容易 OOM。企业里正确的写法是手动 new ThreadPoolExecutor:
java
@Configuration
public class ThreadPoolConfig {
@Bean("orderExecutor")
public ExecutorService orderExecutor() {
int cpuCores = Runtime.getRuntime().availableProcessors();
return new ThreadPoolExecutor(
cpuCores * 2, // 核心线程数:I/O 密集型给 CPU*2
cpuCores * 4, // 最大线程数
60L, TimeUnit.SECONDS, // 空闲线程存活时间
new ArrayBlockingQueue<>(500), // 有界队列,绝不允许无界
new ThreadFactoryBuilder().setNameFormat("order-pool-%d").build(),
// 拒绝策略:让调用方自己跑,起到背压作用,而不是丢任务
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
}
参数怎么定?经验公式:
- CPU 密集型(计算、加密):核心线程数 = CPU 核数 + 1
- I/O 密集型(查库、调接口):核心线程数 = CPU 核数 * 2 到 * 8
- 队列一定要有界,拒绝策略一定要显式指定,不要用默认的
AbortPolicy直接抛异常。
6.1.2 并行调用多个下游服务
一个商品详情页要调用商品、库存、价格、推荐四个服务,串行要 400ms,并行只要 100ms:
java
@Service
public class ProductFacade {
@Autowired
@Qualifier("orderExecutor")
private ExecutorService executor;
public ProductDetailVO detail(Long skuId) throws Exception {
Future<Sku> skuFuture = executor.submit(() -> skuService.getById(skuId));
Future<Stock> stockFuture = executor.submit(() -> stockService.getBySku(skuId));
Future<Price> priceFuture = executor.submit(() -> priceService.getBySku(skuId));
// 四个 future 同时在跑,这里 get 才开始阻塞等结果
Sku sku = skuFuture.get(800, TimeUnit.MILLISECONDS);
Stock stock = stockFuture.get(800, TimeUnit.MILLISECONDS);
Price price = priceFuture.get(800, TimeUnit.MILLISECONDS);
return ProductDetailVO.of(sku, stock, price);
}
}
注意这里有个坑:你用的是自己定义的 orderExecutor,千万不要用 CompletableFuture.supplyAsync() 默认的 commonPool,否则下游服务慢一点,整个应用的所有异步调用都会被拖垮。
6.1.3 生产者-消费者模式
这是最经典的分工模式,用 BlockingQueue 实现,解耦生产和消费:
java
public class OrderQueue {
private final BlockingQueue<Order> queue = new ArrayBlockingQueue<>(1000);
// 生产者:下单接口
public void produce(Order order) throws InterruptedException {
queue.put(order);
}
// 消费者:后台线程池
public void startConsume(ExecutorService pool) {
for (int i = 0; i < 4; i++) {
pool.submit(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
Order order = queue.take();
payService.doPay(order);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
});
}
}
}
6.2 同步:让线程之间步调一致
6.2.1 CountDownLatch:等所有子任务完成
导出报表,要查 10 个部门的数据,全部查完才能生成 Excel:
java
public ReportVO exportReport(List<Long> deptIds) throws Exception {
CountDownLatch latch = new CountDownLatch(deptIds.size());
Map<Long, List<Data>> result = new ConcurrentHashMap<>();
for (Long deptId : deptIds) {
executor.submit(() -> {
try {
result.put(deptId, reportService.queryByDept(deptId));
} finally {
latch.countDown(); // 不管成功失败都要减计数
}
});
}
// 主线程阻塞等待所有任务完成,最多等 30 秒
boolean done = latch.await(30, TimeUnit.SECONDS);
if (!done) {
throw new BizException("导出超时");
}
return ReportVO.of(result);
}
关键点:countDown() 一定要放在 finally 里,否则某个子任务抛异常,计数器永远减不到 0,主线程会一直等到超时。
6.2.2 CyclicBarrier:多线程互相等待到同一个屏障
多线程计算矩阵,每个线程算一行,所有行算完才能进入下一步:
java
public void parallelMatrix(double[][] matrix) {
int n = matrix.length;
CyclicBarrier barrier = new CyclicBarrier(n, () -> {
// 所有线程到达屏障后,由最后一个到达的线程执行
System.out.println("所有行计算完成,开始合并");
});
for (int i = 0; i < n; i++) {
final int row = i;
new Thread(() -> {
try {
computeRow(matrix[row]);
barrier.await(); // 等其他行
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
CountDownLatch 是一次性的,CyclicBarrier 可以复用,这是两者最核心的区别。
6.3 互斥:保护共享变量
6.3.1 synchronized:能不加锁就不加锁,加锁就用它
java
public class Account {
private long balance;
// 修饰实例方法:锁的是 this
public synchronized void withdraw(long amount) {
if (balance < amount) {
throw new RuntimeException("余额不足");
}
balance -= amount;
}
// 修饰代码块:锁对象可以自己选,更灵活
public void transfer(Account target, long amount) {
// 必须按固定顺序加锁,否则会死锁
Account first = this.hashCode() < target.hashCode() ? this : target;
Account second = this.hashCode() < target.hashCode() ? target : this;
synchronized (first) {
synchronized (second) {
this.balance -= amount;
target.balance += amount;
}
}
}
}
6.3.2 ReentrantLock:需要高级功能时才用
java
public class Stock {
private int count;
private final ReentrantLock lock = new ReentrantLock(true); // 公平锁
public boolean deduct() {
// 尝试拿锁,最多等 1 秒,拿不到就失败,防止线程无限等待
if (lock.tryLock()) {
try {
if (count <= 0) return false;
count--;
return true;
} finally {
lock.unlock(); // 必须在 finally 里释放
}
}
return false; // 拿不到锁快速失败,让用户稍后重试
}
}
ReentrantLock 相比 synchronized 的优势:可超时、可中断、可公平、可绑定多个 Condition。代价是必须手动释放锁,忘了写 finally 就是灾难。
6.3.3 volatile:解决可见性,不解决原子性
java
public class Switch {
// 没有 volatile,工作线程可能永远看不到 main 线程的修改
private volatile boolean running = true;
public void start() {
new Thread(() -> {
while (running) {
// 处理任务
}
}).start();
}
public void shutdown() {
running = false;
}
}
记住一句话:volatile 保证可见性和有序性,不保证复合操作的原子性。i++ 这种操作哪怕加了 volatile 也还是线程不安全,必须用 AtomicInteger 或加锁。
6.3.4 无锁方案:原子类
java
public class Counter {
// 高并发计数场景,LongAdder 比 AtomicLong 吞吐量高 10 倍以上
private final LongAdder hit = new LongAdder();
public void incr() {
hit.increment();
}
public long get() {
return hit.sumThenReset();
}
}
6.3.5 线程封闭:从根上避免共享
最好的锁,是不需要锁。如果一个变量只被一个线程访问,根本不存在并发问题。
java
// ThreadLocal:每个线程自己一份 SimpleDateFormat,避免多线程抢同一个
public class DateUtils {
private static final ThreadLocal<SimpleDateFormat> TL =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static String format(Date date) {
return TL.get().format(date);
}
public static void remove() {
TL.remove(); // 线程池场景必须清理,否则内存泄漏
}
}
注意:在线程池里用 ThreadLocal 一定要 remove(),因为线程复用,上一个任务的数据会被下一个任务读到。
七、竞品对比:JDK 里那些"长得像"的东西到底怎么选

7.1 synchronized vs ReentrantLock
| 对比维度 | synchronized | ReentrantLock |
|---|---|---|
| 实现层面 | JVM 关键字,monitorenter/monitorexit | JDK 层基于 AQS 实现 |
| 锁释放 | 自动释放,异常也会释放 | 必须手动 finally 释放,忘写就死锁 |
| 可中断 | 不可中断,拿不到就死等 | 可中断,lockInterruptibly() |
| 可超时 | 不支持 | tryLock(timeout) 支持 |
| 公平锁 | 只支持非公平 | 可选公平/非公平 |
| 条件变量 | 只能用一个 wait set | 可绑定多个 Condition,精确唤醒 |
| 性能 | JDK 6 后大幅优化,低竞争下更快 | 高竞争下略优,但差距很小 |
| 适用场景 | 一般互斥,90% 场景首选 | 需要超时、多条件、公平锁时才用 |
结论:除非你确实需要 ReentrantLock 的高级功能,否则老老实实用 synchronized。它是官方重点优化的方向,代码也更短更不容易写错。
7.2 Executors 工厂方法 vs 手动 ThreadPoolExecutor
| 对比维度 | Executors.newFixedThreadPool | 手动 new ThreadPoolExecutor |
|---|---|---|
| 队列 | LinkedBlockingQueue 无界,请求堆积 OOM | ArrayBlockingQueue 有界,可控 |
| 最大线程数 | 等于核心线程数,无法扩容 | 可配置 maxPoolSize |
| 拒绝策略 | 默认 AbortPolicy 直接抛异常 | 可自定义 CallerRunsPolicy 背压 |
| 线程命名 | 默认 pool-x-thread-y,日志难排查 | 自定义 ThreadFactory,有业务名 |
| 合规性 | 阿里手册明令禁止 | 生产推荐写法 |
7.3 ConcurrentHashMap vs HashMap vs Collections.synchronizedMap
| 对比维度 | HashMap | Hashtable | ConcurrentHashMap |
|---|---|---|---|
| 线程安全 | 不安全 | 安全,全方法 synchronized | 安全,分段锁/CAS |
| 锁粒度 | 无 | 整个对象 | 数组桶级别,JDK8 后 CAS+synchronized |
| 并发度 | 单线程 | 同时只有一个线程能进 | 高并发读写不互斥 |
| 是否允许 null key/value | 允许 | 不允许 | 不允许 |
| 性能 | 单线程最快 | 最差 | 高并发下最优 |
| 适用场景 | 单线程局部变量 | 废弃,不要用 | 全局共享缓存 |
7.4 CountDownLatch vs CyclicBarrier
| 对比维度 | CountDownLatch | CyclicBarrier |
|---|---|---|
| 计数方向 | 倒数到 0,一次性 | 加数到 parties,可重置 |
| 等待方 | 一个或多个线程等 | 所有参与线程互相等 |
| 使用后 | 不能重置,报废 | 可以 reset() 复用 |
| 典型场景 | 等 N 个子任务完成 | 多线程互相等待到齐后一起开工 |
八、企业项目里的高频使用场景
把上面的知识落到实处,企业里并发编程主要出现在这几个地方:
场景 1:接口聚合。 一个详情页要调 4-5 个下游服务,用 CompletableFuture 或 Future 并行调用,接口耗时从 500ms 降到 120ms。
场景 2:报表导出。 数据量大,一次性查库超时,用线程池分片查询,主线程用 CountDownLatch 等所有分片查完再合并。
场景 3:秒杀库存扣减。 单机用 LongAdder 预扣计数,分布式用 Redis Lua;单机内存态用 ReentrantLock.tryLock() 防止超卖。
场景 4:异步解耦。 用户下单后,发短信、发优惠券、推风控这些非核心流程丢到独立线程池异步执行,主流程只做落库和返回,接口 RT 从 300ms 降到 30ms。
场景 5:配置缓存。 全局配置用 volatile 引用一个不可变对象,后台线程定时刷新引用,读线程无锁读,这就是典型的无锁读写。
场景 6:定时任务分片。 一个定时任务要处理 100 万条数据,用 CyclicBarrier 让 8 个工作线程各处理 12.5 万条,全部完成后再汇总。
九、面试官高频面试题(建议背下来)
Q1:synchronized 锁的是什么?锁升级过程了解吗?
A:普通方法锁 this,静态方法锁 Class 对象,代码块锁括号里的对象。JDK 6 后引入锁升级:无锁 -> 偏向锁 -> 轻量级锁(自旋)-> 重量级锁。锁只能升级不能降级,偏向锁在 JDK 15 后默认关闭。
Q2:volatile 能保证原子性吗?底层原理是什么?
A:不能保证复合操作原子性。底层通过内存屏障(Memory Barrier)禁止指令重排,并强制将工作内存的值刷新到主内存、使其他 CPU 缓存行失效。典型用途是状态标志位和 DCL 单例。
Q3:什么是 AQS?
A:AbstractQueuedSynchronizer,JUC 同步器的底层框架。核心是一个 volatile int state 表示同步状态,一个 CLH 等待队列存放阻塞线程。ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 都是基于 AQS 实现的。
Q4:线程池核心参数有哪些?任务提交后执行流程是什么?
A:7 个参数:corePoolSize、maximumPoolSize、keepAliveTime、workQueue、threadFactory、handler。流程:核心线程没满就建核心线程;满了进队列;队列满了建非核心线程到 max;max 也满了执行拒绝策略。
Q5:线程池为什么不建议用 Executors?
A:FixedThreadPool 和 SingleThreadPool 用无界队列,请求堆积会 OOM;CachedThreadPool 最大线程数是 Integer.MAX_VALUE,会创建海量线程 OOM。所以生产环境必须手动 new ThreadPoolExecutor。
Q6:死锁发生的四个必要条件?怎么避免?
A:互斥、持有并等待、不可剥夺、循环等待。破坏任意一个即可:固定加锁顺序破坏循环等待;tryLock 超时破坏不可剥夺;一次性申请所有资源破坏持有并等待。
Q7:ConcurrentHashMap 在 JDK 7 和 8 的实现区别?
A:JDK 7 用 Segment 分段锁,默认 16 段,并发度 16;JDK 8 放弃 Segment,改用 数组 + 链表/红黑树,锁粒度细化到每个桶节点,用 CAS + synchronized,并发度大幅提升。
Q8:ThreadLocal 内存泄漏是怎么回事?怎么解决?
A:ThreadLocalMap 的 key 是 ThreadLocal 的弱引用,value 是强引用。ThreadLocal 对象被回收后 key 变 null,但 value 还被 Entry 链引用,线程池里线程不销毁就一直泄漏。解决:用完显式调用 remove()。
Q9:CountDownLatch 和 CyclicBarrier 的区别?
A:CountDownLatch 是一次性的,一个或多个线程等其他线程完成;CyclicBarrier 是可重置的,所有线程互相等待到屏障点后一起继续。
Q10:虚拟线程和传统线程的区别?
A:传统平台线程一对一映射 OS 线程,数量受 OS 限制(几千);虚拟线程是 JVM 调度的轻量级线程,一个机器可以开几百万个,阻塞 I/O 时自动让出底层平台线程,写同步代码享受异步性能。
十、总结
回到文章开头那个问题:怎么才能学好 Java 并发编程?
答案还是那八个字:跳出来看全景,钻进去看本质。
跳出来,就是先建立分工、同步、互斥这张全景图。以后看到任何一个类、任何一个 Bug,先归类:这是分工没分好,还是同步没同步好,还是互斥没保护好。归类清楚了,方案就在眼前。
钻进去,就是对每个核心问题再往深挖一层。比如看到 ReentrantLock,要追问它底层是 AQS;看到 AQS,要追问 CLH 队列和 state 变量;看到 CAS,要追问 CPU 的 lock 前缀指令和缓存一致性协议。技术的本质是背后的理论模型,理论通了,换一门语言、换一个框架,你依然会。
不要一上来就死磕 JUC 源码。先把这张全景图印在脑子里,再在实际项目里反复用、反复踩坑、反复排查线上问题,半年后你再回头看 Doug Lea 的设计,会发现每一个类都恰如其分地放在它该在的位置。
祝你早日从"看啥都眼熟、写啥都不会"进化到"系统一出问题,扫一眼 jstack 就知道哪类问题"。
本文为原创文章,如需转载,请联系作者获得授权,并注明出处。