
文章收录专栏:Java 核心原理全解:源码・并发・面试实战
系列文章:
深入理解 Java 内存模型(JMM):从抽象规范到生产实践
Java 线程从生到死:创建三式、六态流转、中断协作与死锁活锁饥饿,一篇讲透
Java 线程池七问七答:参数、执行流程、拒绝策略到 ThreadLocal 内存泄漏,面试必背
JUC 并发工具全家桶:CountDownLatch、CyclicBarrier、Semaphore、CompletableFuture 与并发容器一网打尽
衔接引子 :本专栏的《JMM》与《锁机制》解决了 "并发对不对",三篇基础篇介绍了 "并发有什么"。本文把它们合为一张并发层架构图 :以五域(正确性 / 容量 / 共享 / 少共享 / 治理)为骨架,把 JMM 与锁的深度作为地基,把线程、线程池、JUC 三篇升维为架构决策,并补齐 java.util.concurrent 全量知识点(【补遗】标注),最后用贯穿案例把全篇串成一条线。
引言:并发层的五域定位与权衡三原则
高并发系统的并发层,本质是回答五个问题:
| 域 | 问题 | 承载章节 |
|---|---|---|
| 正确性 | 并发对不对? | JMM、锁(已有深度篇,本文引用) |
| 容量 | 并发扛不扛得住? | 第 1/2/3 章:线程模型、线程池、异步编排 |
| 共享 | 共享怎么管? | 第 4/5 章:JUC 工具、并发容器、原子类 |
| 少共享 | 能不能不共享? | 第 6 章:并发安全设计 |
| 治理 | 出了问题怎么办? | 第 7 章:问题治理与监控 |
架构师权衡三原则(全文主线):
- 正确性优先:一切性能优化建立在可见性、原子性、有序性正确之上 ------ 先对,再快
- 少共享优先:能用不可变 / 封闭解决,绝不用锁解决;锁是最后手段
- 量化优先:线程数、池大小、队列长度、监控指标都要有依据,不靠感觉
第 1 章 并发层的架构骨架:线程模型
1.1 线程是资源,不是玩具
上下文切换的代价:内核态切换 + 寄存器保存恢复 + Cache/TLB 失效。活跃线程数超过 CPU 核数后,新增线程不是并行而是排队切换------"线程越多吞吐越高" 是错误认知的根源。
1.2 三种并发模型选型
| 模型 | 代表 | 线程与请求关系 | 架构适用 |
|---|---|---|---|
| 阻塞模型(BIO) | 传统 Tomcat | 一连接一线程 | 连接少、请求快 |
| 事件循环(NIO/Reactor) | Tomcat NIO、Netty | IO 线程 + 业务线程池 | 高连接数、IO 密集 |
| 虚拟线程 | JDK21 | 一任务一虚拟线程 | 大规模阻塞 IO |
架构决策 :NIO 模型的核心是 IO 线程与业务线程分离 。Netty 中 BossGroup 只做 accept,WorkerGroup 做 Channel 读写,慢业务必须丢到独立业务线程池 ------ 否则一个慢任务卡死 EventLoop,拖垮它负责的所有连接。
1.3 线程数建模
IO 密集:线程数 = CPU 核数 × (1 + 平均等待时间 / 平均计算时间) //《Java并发编程实战》
CPU 密集:线程数 = 核数 + 1
架构级提醒:公式只是起点,最终要压测校准并留过载冗余(见第 7 章)。
1.4 虚拟线程的架构影响
JDK21 虚拟线程让 "一任务一线程" 廉价,IO 密集服务可以用虚拟线程替代业务线程池,直接改变第 2 章的隔离架构。但三个边界必须清楚:
- Pinning:长时间持 synchronized 或执行本地方法时无法卸载,钉住载体线程;JDK24 的 JEP 491 已修复绝大多数 synchronized 场景,长临界区仍推荐 ReentrantLock
- 仍不适合 CPU 密集:CPU 任务不阻塞、无法卸载,虚拟线程无收益反而增调度开销
- 不解决过载治理 :容量监控、背压、限流依然需要 ------虚拟线程解决 "阻塞成本",不解决 "容量失控"
架构判断:存量系统先做 "长阻塞调用 → 虚拟线程" 的局部替换验证,不要一上来推翻线程池隔离架构。
第 2 章 线程池的架构化设计
2.1 池化架构、背压与初始参数
线程池是 "池化架构" 家族一员(线程池 / 连接池 / 对象池)。有界队列 = 背压机制,拒绝策略 = 过载降级信号,绝不是异常兜底:
ThreadPoolExecutor pool = new ThreadPoolExecutor(16, 32, 60, SECONDS,
new ArrayBlockingQueue<>(512),
r -> new Thread(r, "pay-core-" + n.incrementAndGet()),
(r, p) -> {
log.warn("线程池过载拒绝,转补偿队列: {}", r);
compensationQueue.offer(r); // MQ/DB 落库,稍后重放
metrics.incRejected(); // 拒绝=过载信号,必须告警
});
初始参数怎么定(经验法,压测校准):
corePoolSize:IO 任务按 1.3 节公式取参考值;CPU 任务 = 核数 + 1maximumPoolSize:core 的 2 倍左右作为弹性上限(防瞬时尖峰)- 队列长度 :按
峰值 QPS × 任务 P99 耗时 × 缓冲倍数(2~5)估算 ------ 太小频繁触发拒绝、太大失败延迟不可控 - 经验参考:多数业务池 core 8~32、队列 200~1000 量级,最终以压测 P99 与拒绝率为准
2.2 线程池隔离:并发层最重要的架构决策
不隔离的后果:一次慢查询占满全部线程,正常业务全部排队 ------局部故障蔓延成全局雪崩。隔离维度:
-
按业务域:支付 / 下单 / 库存各自独立池
-
按任务类型:CPU 密集池 / IO 密集池 / 定时池分开
-
与容器隔离 :Tomcat 请求线程只收发请求,业务进独立池 ------Tomcat 线程被占满会导致服务 "假死"(拒绝新连接)
ExecutorService bizPool = newPool("biz", 32, 64); // 业务处理
ExecutorService rpcPool = newPool("rpc", 16, 32); // 下游 RPC,含超时
ExecutorService timerPool = newPool("timer", 4, 8); // 定时任务
2.3 动态线程池
静态参数在峰谷流量下要么浪费要么过载。架构级解法是动态线程池 (美团 dynamic-threadpool 开源方案):核心 / 最大线程数、队列长度、拒绝策略从配置中心热更新,内置队列积压 / 拒绝次数 / 活跃水位告警 ------ 线程池从 "启动时定死的资源" 变成 "可观测可治理单元"。动态化之前必须先有 2.1 的初始值 + 第 7 章的监控,否则调参无依据。
2.4 定时调度:ScheduledThreadPoolExecutor【补遗】
Executors.newScheduledThreadPool 返回的正是它,不要用它执行普通任务(无界延迟队列,任务堆积 OOM):
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(4, factory);
scheduler.scheduleAtFixedRate(task, 0, 30, TimeUnit.SECONDS); // 固定速率(上次开始计时)
scheduler.scheduleWithFixedDelay(task, 0, 30, TimeUnit.SECONDS);// 固定延迟(上次结束计时)
架构决策 :周期任务用 scheduleAtFixedRate(对账、心跳);scheduleWithFixedDelay 适合 "任务耗时不稳定" 场景,避免任务堆积。
2.5 批量任务编排:ExecutorCompletionService【补遗】
批量异步任务 "谁先完成先处理谁"(并发抓取多个数据源)------invokeAll 是等全部完成,ExecutorCompletionService 是完成一个消费一个:
ExecutorCompletionService<Result> ecs = new ExecutorCompletionService<>(rpcPool);
for (Task t : tasks) ecs.submit(() -> fetch(t));
for (int i = 0; i < tasks.size(); i++) {
Result r = ecs.take().get(); // 先完成的先返回,不被慢任务阻塞
handle(r);
}
2.6 线程池生命周期与优雅停机
线程池有自身状态机:RUNNING → SHUTDOWN → STOP → TIDYING → TERMINATED。
shutdown():拒绝新任务,处理完已在队列中的任务后进入 TERMINATEDshutdownNow():尝试interrupt正在执行的任务,返回尚未执行的任务列表awaitTermination(timeout, unit):阻塞等待池终止,返回是否按时终止
架构决策(优雅停机):服务下线 / 发版必须按序收尾,否则流量还在打、线程已被杀,任务半途丢失:
- 先摘除流量(注册中心下线,停止新请求进来)
- 再
shutdown()让存量任务 drain(配awaitTermination) - 超时兜底
shutdownNow(),未完成的可落补偿队列 / MQ 重放 - 各隔离池按依赖顺序关闭(先业务池、再 RPC 池,避免关闭期间还在调下游)
铁律 :shutdown() 不阻塞,必须配 awaitTermination 才叫 "等它停完"。
第 3 章 上下文与异步架构
3.1 ThreadLocal 的架构角色:链路上下文
ThreadLocal 在架构里的核心价值是传递一次请求的上下文(TraceId、userId、租户、灰度标)。链路追踪(SkyWalking/OpenTelemetry)靠它传递 span 上下文:
InheritableThreadLocal:子线程创建时继承,但线程池复用不传递------ 坑TransmittableThreadLocal(TTL):捕获→回放→恢复,线程池场景标配- 架构铁律 :线程池里用 ThreadLocal 必须 finally
remove(),否则复用线程把上一位请求的上下文带给下一位 ------ 数据越权事故
3.2 FutureTask 状态机与 execute/submit 差异
FutureTask 内部状态机(面试深挖点):
NEW → COMPLETING → NORMAL(正常完成)
├→ EXCEPTIONAL(异常)
├→ CANCELLED(取消)
└→ INTERRUPTING → INTERRUPTED(中断)
get() 阻塞直到终态;cancel(true) 走 INTERRUPTING→INTERRUPTED;任务只能执行一次(状态离开 NEW 后 run 不再执行)。
execute vs submit 差异 :execute(Runnable) 不返回 Future,任务异常由线程的 UncaughtExceptionHandler 处理;submit(Runnable/Callable) 返回 Future,异常被封装进 Future,get() 时才抛 ExecutionException------ 两者异常 "消失" 的位置不同,排查时先分清任务是怎么提交的。
3.3 CompletableFuture:把串行阻塞改成并行编排
CompletableFuture<Order> orderF = supplyAsync(() -> orderService.get(id), rpcPool);
CompletableFuture<User> userF = supplyAsync(() -> userService.get(uid), rpcPool);
Order result = orderF.thenCombine(userF, (o, u) -> o.setUser(u))
.orTimeout(800, TimeUnit.MILLISECONDS) // 铁律1:异步必须设超时
.exceptionally(ex -> { metrics.incFail(); return fallback(); }) // 铁律2:兜底降级
.join();
架构铁律 :① 必须显式指定线程池(默认 commonPool 全局共享、线程数 CPU-1,IO 任务拖垮整个 JVM);② 必须设超时(orTimeout/completeOnTimeout),否则挂起任务泄漏吃满线程池;③ exceptionally 降级而不是抛回调用方。
3.4 ForkJoinPool:分治 + 工作窃取
- RecursiveTask (有返回值)/ RecursiveAction (无返回值),重写
compute(),fork()拆分、join()汇总 - 工作窃取算法 :每个工作线程一个双端队列,自己的任务从队头取,空闲时偷其他线程队列的队尾任务------ 减少线程间竞争
- commonPool 默认并行度 = CPU 核数 - 1,IO 场景必须自定义
- 适用:CPU 密集型分治(排序、求和、图像处理);不适合 IO 密集
第 4 章 JUC 工具的架构级运用
4.1 同步工具选型矩阵
| 工具 | 一句话场景 | 架构注意 |
|---|---|---|
| CountDownLatch | 等 N 个任务完成 | 一次性;await 必须带超时 |
| CyclicBarrier | N 线程到齐放行 | 可 reset 复用 |
| Semaphore | 并发数限流 | 保护下游 / 连接池 |
| Phaser / Exchanger | 多阶段同步 / 两线程交换 | 用得少,浅谈 |
架构注意 :await 必须用带超时版本(await(timeout)),否则任务失败时 Latch 永不归零,调用线程挂死。
4.2 限流架构:信号量 vs 令牌桶 vs 分布式
| 手段 | 限制对象 | 代表 | 架构位置 |
|---|---|---|---|
| Semaphore | 并发数 | tryAcquire |
单机保护下游连接数 |
| 令牌桶 | 速率 QPS | Guava RateLimiter | 单机入口 |
| 分布式限流 | 集群总速率 | Redis/Sentinel | 网关层 |
架构决策 :三者叠加而非替代 ------ 网关分布式限流挡全局,应用内信号量护资源。只用单机限流,集群扩容后总 QPS 翻倍仍打挂下游。
4.3 并发随机数:ThreadLocalRandom【补遗】
高并发下 Math.random()(内部 AtomicLong CAS 竞争)是性能热点,ThreadLocalRandom 每个线程独立种子、无竞争:
int x = ThreadLocalRandom.current().nextInt(1, 100);
架构注意 :current() 返回当前线程实例,多线程下不要共享同一个实例(会退化竞争)。
第 5 章 并发容器的架构选型
5.1 本地缓存架构:CHM 与多级缓存
| 方案 | 并发特性 | 架构定位 |
|---|---|---|
| Hashtable / synchronizedMap | 全表锁 | 不要用于高并发 |
| ConcurrentHashMap | 桶级锁 + CAS | 高并发读写基础 |
| Caffeine | 高并发 + 淘汰 | 本地缓存首选 |
架构决策:两级缓存 ------ 本地 Caffeine(微秒级)+ 远程 Redis(跨实例一致),本地解决热点、Redis 解决一致性。注意缓存击穿:未命中回填要加锁(单飞)。
CHM 生产坑 :computeIfAbsent(key, fn) 的 fn 是在锁内执行 的 ------ 慢操作(查库、远程调用)会锁住整个桶拖垮并发;且语义下 fn 可能被并发执行多次、后写覆盖先写。高成本回填不要直接塞进 fn,应改为快速占位 + 异步回填 ,或用 " 先 get 判空 → putIfAbsent" 模式自己控制。
5.2 配置与监听:CopyOnWriteArrayList
读多写极少的典型:路由表、开关配置、监听器注册表 。读路径每次请求都走、写路径分钟级一次 ------ 正是 COW 的甜蜜点。禁止用于高频写(每次写复制数组、GC 压力大);迭代弱一致。
5.3 队列与背压:BlockingQueue 全家族
| 队列 | 特性 | 架构角色 |
|---|---|---|
| ArrayBlockingQueue | 有界数组、单锁 | 线程池默认背压 |
| LinkedBlockingQueue | 双锁、默认无界 | 高吞吐但无界有 OOM 风险 |
| SynchronousQueue | 不存元素直接传递 | 零缓冲直通 |
| DelayQueue | 无界延迟 | 定时任务、超时控制 |
| PriorityBlockingQueue | 无界优先 | 优先级任务 |
| ConcurrentLinkedQueue | 无锁、非阻塞 | 高频入队出队的无界无锁场景 |
| LinkedTransferQueue | 无界 + transfer() |
生产者阻塞等消费者 "接手" |
| LinkedBlockingDeque | 双端阻塞 | 工作窃取 / 双向队列 |
方法族三分法 :put/take 阻塞;offer(e)/poll() 非阻塞(立即返回 false/null);offer(e,t)/poll(t) 限时阻塞 ------生产代码默认用限时版本,阻塞版本用于明确需要强背压的场景。
架构铁律 :无界队列就是隐患,除非有明确的消费保障;SynchronousQueue 配 CachedThreadPool 是 "任务即来即走" 组合,但线程数无上限。
5.4 有序并发集合:ConcurrentSkipListMap【补遗】
需要并发 + 有序 的 Map(排行榜、范围查询、热点 key 有序)时,HashMap 系列不保证顺序,用跳表实现:
ConcurrentSkipListMap<Long, String> ranking = new ConcurrentSkipListMap<>();
ranking.put(score, userId);
ranking.firstKey(); // 最高分
ranking.subMap(a, b); // 范围查询,天然有序
架构优势:无锁读 + 有序遍历,替代 "ConcurrentHashMap + 排序" 或 "加锁 TreeMap";代价是内存略高、写稍慢。
5.5 无锁架构:原子类家族全景【补遗】
| 类别 | 代表 | 场景 |
|---|---|---|
| 标量原子类 | AtomicInteger/AtomicLong/AtomicBoolean | 计数器、状态位 |
| 原子数组 | AtomicIntegerArray/LongArray/ReferenceArray | 数组元素原子更新 |
| 原子字段更新器 | AtomicIntegerFieldUpdater/LongFieldUpdater/ReferenceFieldUpdater | 直接更新普通类的 volatile 字段,省包装对象开销 |
| 累加器 | LongAdder/LongAccumulator、DoubleAdder/DoubleAccumulator | 高并发累加,sum () 非强一致 |
| 引用 | AtomicReference、AtomicReferenceArray | 引用原子替换 |
架构提醒:LongAdder 用于 "展示型统计"(PV、指标)可以,用于 "精确对账" 不行;Disruptor(无锁环形缓冲)是极致吞吐的架构彩蛋(日志、撮合)。
第 6 章 并发安全设计:正确性的架构基石
锁和容器解决的是 "共享怎么管",但最好的并发是 "没有共享"。
6.1 线程封闭
- 栈封闭:局部变量天然线程私有,无需同步
- ThreadLocal 封闭:每个线程一份副本,彻底避免共享
6.2 不可变对象:最强并发安全
状态不可变 → 多线程只读不写,天然无竞态。架构上优先设计不可变对象(如不可变的配置项、订单快照):
public final class Config { // final 类
private final int timeout; // final 字段,构造内赋值
private final String url;
public Config(int timeout, String url) { this.timeout = timeout; this.url = url; }
// 只提供 getter,无 setter
}
配合 JMM 的 final 语义(构造内写入 + 安全发布后可见),不可变对象是无锁高性能的基础。
6.3 安全发布
对象在构造完成前被 "暴露" 会破坏 final / 可见性保证。安全发布四方式:volatile 引用、final、锁(synchronized/Lock)、并发容器 。禁止 this 逃逸(构造内把 this 传给别的线程 / 注册监听器)。简单原则:能 final 就 final,不能就 volatile,再不能才上锁 / 容器。
6.4 线程安全单例
| 方式 | 并发安全依据 | 架构推荐度 |
|---|---|---|
| 饿汉(static final) | 类加载即初始化 | 简单场景 |
| 静态内部类 Holder | <clinit> 同步 + HB 规则 |
推荐,无锁无 DCL 隐患 |
| 枚举 | JVM 保证实例唯一 + 防反射 / 反序列化 | 安全敏感场景 |
| DCL + volatile | volatile 禁重排(详见锁篇) | 面试考点 |
6.5 生产者 - 消费者:队列解耦
架构本质:用 BlockingQueue 把 "生产速率" 和 "消费速率" 解耦,天然削峰:
BlockingQueue<Task> queue = new ArrayBlockingQueue<>(1024);
queue.put(task); // 生产者,满了阻塞=背压
Task t = queue.take(); // 消费者,空了阻塞
第 7 章 并发问题治理架构
7.1 三大问题的架构治理
| 问题 | 本质 | 架构治理 |
|---|---|---|
| 死锁 | 资源环 | 资源编号按序申请;tryLock(timeout);统一收口加锁 |
| 活锁 | 对称谦让 | 随机 / 指数退避、限重试次数、线程 ID 排序不对称退让 |
| 饥饿 | 持续插队 | ReentrantLock 公平锁(很大程度避免);synchronized 无法解决 |
死锁治理三阶段:设计期 (资源排序杜绝循环等待)→ 运行期 (锁等待时长监控)→ 兜底期(jstack/jcmd/Arthas/JFR)。
7.2 容量监控:指标→动作闭环
| 指标 | 含义 | 告警方向 | 动作 |
|---|---|---|---|
| 活跃线程数 / 核心线程数 | 池利用率 | 持续 > 80% | 扩容 / 查慢任务 |
| 队列积压数 | 背压水位 | 持续增长 | 动态扩容 / 触发降级 |
| 拒绝次数 | 过载信号 | > 0 即告警 | 转补偿队列 / 限流收紧 |
| 任务 P99 耗时 | 处理能力 | 劣化即告警 | 定位瓶颈 / 优化依赖 |
| 各隔离池独立监控 | 隔离是否生效 | 单池异常 | 仅隔离单域,不扩散 |
架构含义 :指标必须能触发动作(扩容 / 降级 / 限流 / 补偿)才是闭环 ------ 没有动作的指标只是日志。
7.3 排查体系:生产级三板斧
jstack -l <pid>(线程转储看 BLOCKED/WAITING 分布)→ jcmd <pid> Thread.print(JDK8+ 更全)→ JFR(生产持续记录、事后回放,无需重启)。
结尾:贯穿案例 ------ 订单服务的并发层设计
把全篇串成一条线,看一个订单创建接口如何用五域思维设计:
// ① 少共享:订单上下文设计为不可变对象(第6章)
OrderContext ctx = new OrderContext(orderId, userId); // final 字段 + 安全发布
// ② 容量:业务隔离池异步并行编排(第2/3章)
CompletableFuture<Stock> stockF = supplyAsync(() -> stockService.check(ctx), bizPool)
.orTimeout(500, MILLISECONDS)
.exceptionally(e -> { metrics.incFail("stock"); return Stock.empty(); }); // 可识别降级
CompletableFuture<Account> acctF = supplyAsync(() -> accountService.deduct(ctx), bizPool)
.orTimeout(500, MILLISECONDS)
.exceptionally(e -> { metrics.incFail("account"); return Account.empty(); });
// ③ 降级不等于忽略:先校验再组装,缺依赖走补偿,而不是 NPE
Stock stock = stockF.join();
Account acct = acctF.join();
if (!stock.isValid() || !acct.isValid()) {
compensationQueue.offer(ctx); // 补偿重放(治理域闭环)
} else {
Order order = orderService.create(ctx, stock, acct); // 订单表 CHM 缓存热点
}
这个接口的架构答案 :不可变减少竞争 → 隔离池防雪崩 → 异步编排降延迟 → 可识别降级 + 补偿兜底 → CHM/COW 管共享 → 指标闭环保稳态。降级不是忽略错误 ------降级结果必须可识别、可校验、可补偿。
并发层架构设计 Checklist
- 线程模型:阻塞 / 事件循环 / 虚拟线程,匹配 IO 特性?(虚拟线程不解决过载)
- 线程池:有界队列 + 明确拒绝策略 + 命名工厂 + 独立埋点 + 初始参数有依据?
- 线程池隔离:业务 / IO / 定时 / Tomcat 分离?优雅停机流程就位?
- 异步编排:CompletableFuture 显式线程池 + 超时 + 降级(可识别、可校验、可补偿)?
- 上下文:TTL 传递 + finally remove,杜绝跨请求污染?
- 限流:分布式(入口)+ 信号量(资源)叠加?
- 容器:CHM 缓存、COW 配置、队列背压、跳表有序,匹配读写比?
- 安全设计:优先不可变对象 + 安全发布 + 线程封闭,减少共享?
- 治理:线程池指标→动作闭环 + 拒绝告警 + 死锁 / 活锁 / 饥饿兜底就位?
金句:并发架构的本质不是 "怎么用锁",而是 "哪段并行、哪段串行、哪里限流、哪里隔离、哪里观测、哪里不共享"。JMM 与锁解决 "正确",线程池与工具解决 "容量",容器解决 "共享",安全设计解决 "少共享",治理解决 "稳态"------ 五域合一才叫架构。
