# JUC(java.util.concurrent)完整学习笔记 ## 整体架构┌─────────────────────────────────────────────────────────────────────────────┐
│ JUC 四层架构 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 第四层:应用层(开箱即用) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ConcurrentHashMap | CopyOnWriteList | BlockingQueue │ │
│ │ ThreadPoolExecutor | ForkJoinPool | CompletableFuture │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ ↑ │
│ 第三层:同步器(基于 AQS 扩展) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ReentrantLock | Semaphore | CountDownLatch | ReadWriteLock │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ ↑ │
│ 第二层:AQS + 原子类(核心框架) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ AbstractQueuedSynchronizer(state + CLH队列 + 模板方法) │ │
│ │ AtomicInteger | AtomicReference | LongAdder │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ ↑ │
│ 第一层:底层原语(JVM/硬件级) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ CAS(CPU指令) | volatile(内存屏障) | LockSupport(阻塞原语) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
--- ## 第一层:底层原语 ### 1.1 CAS(Compare And Swap) **本质**:CPU 硬件指令(x86 的 CMPXCHG) **语义**:if (内存值 == 预期值) {
内存值 = 新值
return true
} else {
return false
}
**作用**:无锁化编程的基石,实现原子性更新 **关联理解**: - CAS 是乐观锁思想的具体技术实现 - 数据库乐观锁(version 字段)是同一思想的另一种实现 ### 1.2 volatile **本质**:内存屏障(Memory Barrier) **保证**: - ✅ 可见性:一个线程修改,其他线程立即看到 - ✅ 有序性:禁止指令重排序 - ❌ 原子性:不保证! **⚠️ 重点理解**:volatile int i = 0;
i++; // 这不是原子的!
// 因为 i++ 实际是三步:
// 1. 读取 i 的值
// 2. 计算 i + 1
// 3. 写回 i
// 步骤 1 和 3 之间可能被其他线程打断
### 1.3 LockSupport **本质**:线程阻塞/唤醒的底层机制(每个线程一个许可证) **核心方法**: - `park()` = 消耗许可证,没有则阻塞 - `unpark(thread)` = 发放许可证,如果线程在阻塞则唤醒 **vs wait/notify**: | 特性 | wait/notify | LockSupport | |------|-------------|-------------| | 是否需要锁 | 必须在 synchronized 中 | 不需要 | | 调用顺序 | wait 必须先于 notify | unpark 可先于 park | | 唤醒目标 | 随机或全部 | 精确指定线程 | **⚠️ 重点理解**: - LockSupport 和 wait/notify 是两套独立机制,不是封装关系 - 都最终调用操作系统的线程挂起/唤醒 - AQS 选择 LockSupport 因为可以精确唤醒队列中的下一个线程 ### 1.4 三者配合CAS → 解决"如何无锁地原子更新状态"
volatile → 解决"如何让多线程看到最新状态"
LockSupport → 解决"获取不到时如何等待,释放时如何唤醒"
三者组合 → 构建 AQS
### 1.5 相关联想:进程与线程 **概念澄清**: - 进程 = 资源分配的基本单位(JVM 就是一个进程) - 线程 = CPU 调度的基本单位(JUC 操作的是线程) **CPU 与线程的关系**: - 某一瞬间:一个核心执行一个线程(一对一) - 宏观上:通过时间片轮转,多核服务多线程(多对多) **大核 vs 小核**: - 本质一样,区别是性能和功耗 - 由操作系统调度器决定分配,应用程序无法直接控制 --- ## 第二层:AQS(AbstractQueuedSynchronizer) ### 2.1 AQS 解决的问题 Doug Lea 发现各种同步器有大量重复逻辑,提取出 AQS:ReentrantLock、Semaphore、CountDownLatch 的共同点:
- 都需要一个状态
- 获取失败都要排队
- 排队时都要阻塞
- 释放时都要唤醒
↓ 提取共同点 ↓
AQS:
- 一个状态(state)
- 一个等待队列(CLH)
- 阻塞/唤醒机制
- 获取/释放的流程控制
**这就是 DRY 原则的典范** ### 2.2 AQS 核心组成AQS
├── state(volatile int)
│ └── 用 CAS 修改,含义由子类定义
│
└── CLH 队列(双向链表)
└── 存放等待的线程,FIFO 顺序
### 2.3 模板方法模式┌─────────────────────────────────────────────────────────────────────┐
│ AQS │
├─────────────────────────────────────────────────────────────────────┤
│ 框架提供(固定流程): │
│ - acquire():tryAcquire → 失败则入队 → 阻塞 → 被唤醒重试 │
│ - release():tryRelease → 成功则唤醒队首 │
│ - 队列管理、阻塞/唤醒 │
├─────────────────────────────────────────────────────────────────────┤
│ 子类实现(定义语义): │
│ - tryAcquire():什么条件算"获取成功" │
│ - tryRelease():什么条件算"释放成功" │
└─────────────────────────────────────────────────────────────────────┘
### 2.4 独占模式 vs 共享模式 **⚠️ 重点理解**: 区别不是"能有几个线程获取",而是"有没有所有者": | 模式 | 核心特征 | 代表 | |------|----------|------| | 独占 | 有主人,谁加锁谁解锁,支持重入 | ReentrantLock、写锁 | | 共享 | 无主人,获取和释放可以是不同线程 | Semaphore、CountDownLatch、读锁 |Semaphore(1) 效果上像独占,但:
- 线程 A acquire,线程 B 可以 release
- 甚至可以凭空 release(增加许可)
ReentrantLock:
- 线程 A lock,线程 B unlock → 抛异常!
- 必须是同一个线程
### 2.5 AQS 总结AQS 本质 = 抽象队列同步器
= 一个 state + 一个双向队列
= 利用 CAS 保证原子性
= 利用 volatile 保证可见性
= 利用 LockSupport 控制线程阻塞/唤醒
= 模板方法让子类定义语义
--- ## 第三层:基于 AQS 的同步器 ### 3.1 ReentrantLock(可重入锁) **state 含义**:0 = 无锁,>0 = 重入次数 **核心特性**: - 可重入:同一线程多次获取,state + 1 - 有所有者:记录持有锁的线程 - 公平/非公平可选 **公平 vs 非公平**:非公平(默认):新线程来了先尝试 CAS 抢锁,失败才排队
公平:新线程来了先看队列有没有人,有人就乖乖排队
非公平性能更好:减少线程切换开销
**⚠️ 为什么需要可重入**: ```java methodA() { lock.lock(); methodB(); // B 里也要加锁 lock.unlock(); } methodB() { lock.lock(); // 如果不可重入,这里会死锁! lock.unlock(); }3.2 Semaphore(信号量)
state 含义:剩余许可数
工作方式:
- acquire():state > 0 则 state - 1,否则等待
- release():state + 1,唤醒等待者
典型场景:限流、资源池
3.3 CountDownLatch(倒计时门闩)
state 含义:剩余计数
工作方式:
- countDown():state - 1,到 0 时唤醒所有等待者
- await():state > 0 则阻塞,= 0 则通过
特点:一次性,用完不能重置
3.4 CyclicBarrier(循环屏障)
注意:不是直接基于 AQS,而是基于 ReentrantLock + Condition
vs CountDownLatch:
| CountDownLatch | CyclicBarrier |
|---|---|
| 一次性 | 可重用 |
| countDown 和 await 分离 | 所有人都调用 await |
| 一方等多方 | 多方互相等待 |
3.5 ReentrantReadWriteLock(读写锁)
state 含义:一个 int 拆成两部分
- 高 16 位 = 读锁持有线程数
- 低 16 位 = 写锁重入次数
⚠️ 重点理解:
为什么读锁要计数?
场景:3 个线程同时读 线程 A 获取读锁 → 读锁数 = 1 线程 B 获取读锁 → 读锁数 = 2 线程 C 获取读锁 → 读锁数 = 3 线程 A 释放读锁 → 读锁数 = 2(B、C 还在读) 线程 B 释放读锁 → 读锁数 = 1(C 还在读) 线程 C 释放读锁 → 读锁数 = 0(没人读了,写锁可以进来) 如果只用 0/1: 线程 A 释放 → 读锁 = 0,写锁进来,破坏 B、C 正在读的数据!读锁计数的作用:防止写锁在读的过程中插入(读写冲突)
互斥规则:
- 读读并行
- 读写互斥
- 写写互斥
与数据库读写锁的关系:
- 思想层面:一样的
- 实现层面:数据库更复杂(多粒度、意向锁、MVCC、死锁检测)
3.6 同步器对比
| 同步器 | 模式 | 可重用 | 典型场景 |
|---|---|---|---|
| ReentrantLock | 独占 | 是 | 替代 synchronized |
| Semaphore | 共享 | 是 | 限流、资源池 |
| CountDownLatch | 共享 | 否 | 等待 N 个任务完成 |
| CyclicBarrier | 条件变量 | 是 | 多阶段并行计算 |
| ReadWriteLock | 独占+共享 | 是 | 读多写少场景 |
第四层:应用层
4.1 并发集合设计思想演进
思路 1:全表锁(Hashtable) → 每个方法加 synchronized,并发度 = 1,太慢 思路 2:分段锁(JDK 1.7 ConcurrentHashMap) → 分成 16 段,每段一把锁,并发度 = 16 思路 3:CAS + 细粒度锁(JDK 1.8 ConcurrentHashMap) → 空桶 CAS 插入,非空桶 synchronized 锁头节点 思路 4:写时复制(CopyOnWriteArrayList) → 读无锁,写时复制整个数组4.2 ConcurrentHashMap(JDK 1.8)
结构:数组 + 链表 + 红黑树
并发控制:
| 场景 | 操作方式 | 锁粒度 |
|---|---|---|
| 桶为空 | CAS 插入 | 无锁 |
| 桶非空 | synchronized(头节点) | 锁单个桶 |
| 扩容 | 多线程协助 | 分段处理 |
为什么不用 ReentrantLock:
- synchronized 在 JDK 1.6 后优化很多
- 不需要手动释放
- 内存占用更小
4.3 CopyOnWriteArrayList
核心思想:写时复制
读操作:直接读取数组,不加锁 写操作: 1. 加锁(ReentrantLock) 2. 复制原数组 3. 在新数组上修改 4. 把引用指向新数组 5. 释放锁优缺点:
- ✅ 读无锁,性能高
- ✅ 读写不阻塞
- ❌ 写要复制,内存开销大
- ❌ 弱一致性(可能读到旧数据)
适用场景:读多写少(配置列表、监听器列表、黑名单)
4.4 BlockingQueue
核心思想:生产者-消费者模式
实现原理(ArrayBlockingQueue):
ReentrantLock + 两个 Condition put(): while (队列满) notFull.await() 插入元素 notEmpty.signal() take(): while (队列空) notEmpty.await() 取出元素 notFull.signal()常见实现:
| 实现 | 特点 |
|---|---|
| ArrayBlockingQueue | 数组,有界,一把锁 |
| LinkedBlockingQueue | 链表,可选有界,两把锁 |
| SynchronousQueue | 不存储,直接传递 |
| PriorityBlockingQueue | 优先级队列 |
| DelayQueue | 延迟队列 |
4.5 ThreadPoolExecutor
为什么需要线程池:
- 每次 new Thread() 开销大
- 线程数不可控
- 线程用完就销毁,浪费
核心参数:
- corePoolSize:核心线程数
- maximumPoolSize:最大线程数
- keepAliveTime:非核心线程空闲存活时间
- workQueue:任务队列(BlockingQueue)
- threadFactory:线程工厂
- handler:拒绝策略
执行流程:
提交任务 ↓ 核心线程有空闲? → 是 → 核心线程执行 ↓ 否 队列未满? → 是 → 放入队列等待 ↓ 否 线程数 < 最大? → 是 → 创建非核心线程执行 ↓ 否 执行拒绝策略拒绝策略:
| 策略 | 行为 |
|---|---|
| AbortPolicy | 抛异常(默认) |
| CallerRunsPolicy | 调用者线程执行 |
| DiscardPolicy | 静默丢弃 |
| DiscardOldestPolicy | 丢弃队列最老的 |
4.6 ForkJoinPool
两大核心思想:
1. 分治思想:
大任务 ↓ fork ┌──────┴──────┐ 子任务1 子任务2 ↓ ↓ 结果1 结果2 └─────┬─────┘ ↓ join 最终结果2. 工作窃取:
每个线程有自己的双端队列 - 自己的任务从头部取(LIFO) - 偷别人的任务从尾部偷(FIFO) - 减少竞争,自动负载均衡⚠️ 联想:Go 的 GMP 调度
工作窃取思想是跨语言的:
- Java ForkJoinPool:线程闲了偷别人的任务
- Go GMP:M 闲了偷别人 P 的 G
共同的设计哲学:本地优先 + 全局兜底
- 优先处理自己的任务(无锁,快)
- 自己没了,去偷别人的(有锁,频率低)
- 都没了,去全局队列找(兜底)
4.7 CompletableFuture
解决的问题:复杂的异步任务编排
核心能力:
- 创建异步任务:supplyAsync() / runAsync()
- 链式处理:thenApply() / thenAccept() / thenRun()
- 组合任务:allOf() / anyOf() / thenCombine()
- 异常处理:exceptionally() / handle()
vs CountDownLatch:
以前: CountDownLatch latch = new CountDownLatch(3); // 手动管理... 现在: CompletableFuture.allOf(task1, task2, task3) .thenAccept(result -> 处理); 更简洁、更直观本质:下层同步器的集大成者,封装了 CountDownLatch、Semaphore 等的复杂性
4.8 第四层总结
线程池 = 装线程的集合,管理、复用、调度 ForkJoinPool = 分治思想 + 工作窃取 CompletableFuture = 集大成者 → 封装底层同步器 → 链式调用 → 不用手动写 CountDownLatch、Semaphore 等JUC 核心设计思想
1. 分层抽象
底层原语 → AQS 框架 → 具体同步器 → 应用组件 每层只关心自己的职责,向上提供抽象2. DRY(Don’t Repeat Yourself)
AQS 提取了所有同步器的共同逻辑,子类只需定义语义
3. 模板方法模式
AQS 定义流程骨架,子类填空
4. 锁粒度细化
全表锁 → 分段锁 → 节点锁 → 无锁(CAS)5. 空间换时间
- CopyOnWrite:复制数组换取读无锁
- LongAdder:多个 Cell 换取减少竞争
- ThreadLocal:每线程一份换取无竞争
6. 工作窃取
本地优先 + 闲时帮忙,自动负载均衡
易错点和重点标注
⚠️ volatile 不保证原子性
i++ 是三步操作,volatile 无法保证原子性
⚠️ LockSupport 不是 wait/notify 的封装
两套独立机制,LockSupport 解决了 wait/notify 的痛点
⚠️ 独占 vs 共享的核心区别
不是"几个线程能获取",而是"有没有所有者"
⚠️ 读写锁为什么读锁要计数
防止写锁在读的过程中插入,只有读锁数 = 0 写锁才能进来
⚠️ ForkJoinPool 的工作窃取
和 Go GMP 调度思想一致,是跨语言的设计智慧
整体关系图
┌─────────────────────────────────────────────────────────────────────────────┐ │ │ │ CompletableFuture / ThreadPoolExecutor / ConcurrentHashMap │ │ ↑ │ │ 依赖/组合 │ │ │ │ │ ReentrantLock / Semaphore / CountDownLatch / BlockingQueue │ │ ↑ │ │ 继承 │ │ │ │ │ AQS │ │ (state + CLH队列) │ │ ↑ │ │ 基于 │ │ │ │ │ CAS + volatile + LockSupport │ │ ↑ │ │ 依赖 │ │ │ │ │ Unsafe / JVM / CPU │ │ │ └─────────────────────────────────────────────────────────────────────────────┘学习心得:好的设计思想是跨语言的,理解底层原理比记住 API 更重要。