主题
界面
主题
《Lock、CAS 与 AQS》 讲了底层(CAS/AQS);本节是日常开发真正在用的 JUC 工具:
① ConcurrentHashMap —— 并发容器(读改写原子)
② Semaphore —— 限流/资源池(共享模式 AQS)
③ CountDownLatch —— 等待一组任务完成
④ 虚拟线程 —— 海量阻塞 IO(下节)★ 本节实测(JucVirtualThreadLesson):
CHM 100 线程 merge 10000 次无丢失 ✓✓
Semaphore(3) 峰值恰为 3 ✓✓★ HashMap 并发下不仅丢数据,还可能死循环(JDK7 扩容) ✗✗; Collections.synchronizedMap 全表锁,吞吐极差 ✗✓; CHM 用 CAS + 分段/桶级锁,读几乎无锁 ✓✓✓
MAP.merge(key, 1, Integer::sum); // ★ 原子的「读-改-写」✓✓
MAP.computeIfAbsent(key, k -> new ArrayList<>()).add(x); // ★ 原子的「不存在则建」✓✓★ merge/computeIfAbsent 是按 key 原子的 ✓✓ —— 替代了恶心的「if (!map.containsKey) map.put」check-then-act ✗✗ (呼应 《操作系统:调度、同步与死锁》 与项目 A 的 tryBorrowOne ✓)
★ 分段与锁粒度 ✓:JDK8 起按桶(bin)加锁,读几乎无锁 ✓✓;size() 是近似值 ⚠️
var sem = new Semaphore(3);
sem.acquire(); // state-1,0 则阻塞
try { ... } finally { sem.release(); } // state+1实测:10 个调用方、3 个许可 → 同时在临界区内的峰值恰为 3 ✓✓
★ 用途:连接池、限流、资源池 ——阈值 = 被保护资源的容量 ✓✓
| CountDownLatch | CyclicBarrier | |
|---|---|---|
| 语义 | 等 N 个事件完成 | N 个线程互相等到齐 |
| 可复用 | 不可 ✓ | 可(自动重置)✓ |
| 典型 | 主线程等子任务 | 分阶段计算 |
| 需求 | 工具 |
|---|---|
| 并发映射 | ConcurrentHashMap ✓✓ |
| 限流/资源池 | Semaphore ✓ |
| 等待一组任务 | CountDownLatch ✓ |
| 异步编排(并行 IO/远程调用) | CompletableFuture + allOf ✓✓✓ |
| 高竞争计数 | LongAdder ✓(低竞争 AtomicLong ✓) |
| 延迟/周期任务 | ScheduledThreadPoolExecutor ✓ |
| 阻塞队列 | ArrayBlockingQueue(有界)/ LinkedBlockingQueue(⚠️ 无界) |
是什么:Future 的增强版 —— Future 只能「提交任务 + 阻塞 get」✗; CompletableFuture 把结果变成可编排的流水线:链式转换、多任务聚合、 异常兜底、超时控制,全程不阻塞 ✗✓。
为什么(一个结构性对比)✗✓:订单详情页要查 3 个远程服务(用户 120ms、 优惠券 80ms、积分 150ms)——
| 写法 | 总耗时 |
|---|---|
| 三个 Future 依次 get ✗✗ | 120+80+150 = 350ms(相加) |
allOf 并行等待 ✓✓✓ | ≈150ms(取最慢者) |
★ 核心 API(六组就够用):
| 类别 | API | 一句话 |
|---|---|---|
| 创建 | supplyAsync / runAsync | 有返回 / 无返回;IO 任务显式传线程池 ✗✓ |
| 转换 | thenApply | 同步转换(像 map) |
| 串行依赖 | thenCompose | 下一步要用上一步的结果(像 flatMap) |
| 聚合 | allOf / anyOf | 等全部 / 任一完成 |
| 异常 | exceptionally / handle | 兜底默认值 / 正常异常都处理 |
| 超时 | orTimeout / completeOnTimeout(JDK 9+) | 超时抛异常 / 超时给默认值 |
★★ 两个必踩的坑 ✗✗:
- IO 任务用默认 ForkJoinPool.commonPool() ✗✗ —— 公用池被 IO 占满, 全 JVM 的并行流和其他任务跟着遭殃;显式传自定义池 ✓✓
- 链上任何一步抛异常会沿链传播 —— 在链尾一个
exceptionally统一兜底, 不用每层 try-catch ✓✓(对拍实验见第 7 节)
MAP.merge(word, 1, Integer::sum); // 词频统计,无锁 ✓✓var sem = new Semaphore(5); // 最多 5 个并发下载
for (var url : urls) {
executor.submit(() -> { sem.acquire(); try { download(url); } finally { sem.release(); } });
}// ✗ 多线程下 map.put(word, map.getOrDefault(word,0)+1) 丢计数 ✗✗CompletableFuture<String> user = CompletableFuture.supplyAsync(() -> queryUser(id), pool);
CompletableFuture<String> coupon = CompletableFuture.supplyAsync(() -> queryCoupon(id), pool);
CompletableFuture<String> points = CompletableFuture.supplyAsync(() -> queryPoints(id), pool);
CompletableFuture.allOf(user, coupon, points).get(3, TimeUnit.SECONDS); // 等全部
// ★ 总耗时 ≈ 最慢的那个,不是三者之和(实测见第 7 节)✓✓✓代码:labs/java-deep/src/main/java/cn/kb/learn/deep/concurrency/JucVirtualThreadLesson.java、labs/java-deep/src/main/java/cn/kb/learn/deep/concurrency/CompletableFutureLesson.java
实测输出:
== 1. CHM: 100 threads x 100 merge ==
total=10000 (expect 10000) OK
== 2. Semaphore(3): 10 callers ==
peak inside = 3 (limit 3)CompletableFuture 实测输出(CompletableFutureLesson,sleep 模拟 IO):
===== 3. 聚合:三个并行查询,allOf =====
用户:张三 / 券:满100减20 / 积分:1200
总耗时 165 ms ≈ 最慢的积分查询(150ms),不是 120+80+150=350ms
★ 串行 get 三次 = 耗时相加 ✗✗;allOf 并行 = 耗时取最大 ✓✓✓
===== 4. 异常兜底与超时 =====
异常兜底:降级默认值(原因:远程服务挂了)
超时给默认值:超时默认值
★ exceptionally=兜底;orTimeout=超时抛异常;completeOnTimeout=超时给默认值 ✓| # | 错误 | 说明 |
|---|---|---|
| 1 | CHM 的 size() 当精确值 ✗ | 近似值 ✓ |
| 2 | computeIfAbsent 里再改同 key ✗✗ | 死锁/ISE ⚠️ |
| 3 | Semaphore 忘 release ✗✗ | 许可泄漏 → 永久限死 ✓✓ |
| 4 | 用 synchronized Map 替代 CHM ✗ | 全表锁吞吐差 ✓ |
| 5 | 无界队列 + 固定消费者 ✗ | 积压 OOM ⚠️ |
| 6 | IO 任务用默认 ForkJoinPool ✗✗ | 公用池被占满,显式传池 ✓✓ |
验收方式:能说出每个 JUC 工具「在 AQS 里是独占还是共享」。
扩展任务:实现一个基于 Semaphore 的批量下载器(限并发 + 全部完成回调 + 超时取消)✓✓
暂无