JUC
JUC 核心知识点整理
26-4-7 与 Arwen 复习整理
目录
- 一、ConcurrentHashMap
- 二、LongAdder
- 三、AQS
- 四、CountDownLatch
- 五、CyclicBarrier
- 六、Semaphore
- 七、ThreadPoolExecutor
- 八、BlockingQueue
- 九、LockSupport
一、ConcurrentHashMap
1.7 结构
- 数据结构: Segment数组 + HashEntry数组 + 链表,头插法
- 锁机制: Segment继承ReentrantLock,锁的粒度是整个Segment,一个Segment下挂多个槽位
- 并发度: 默认16个Segment,最多16个线程同时写不同Segment
1.8 结构
- 数据结构: Node数组 + 链表/红黑树,尾插法
- 锁机制: synchronized锁住槽位的头节点,粒度从段级别细化到槽级别
- 空槽位写入: 不加锁,用CAS(Unsafe.compareAndSwapObject),期望值为null,只有一个线程能成功,失败的线程重新走put流程进入synchronized逻辑
- ABA问题: 此场景不存在。槽位从null→非null是单向的,扩容时旧槽位放的是ForwardingNode而不是null,构不成ABA
链表转红黑树
- 树化条件: 链表长度 ≥ 8 且 数组长度 ≥ 64,两个条件同时满足才树化
- 退化条件: 红黑树节点数 ≤ 6 时退回链表
- 为什么不都是8: 防止在临界值附近频繁转换(抖动),6和8之间留7作为缓冲带
- 为什么数组要≥64: 数组小的时候碰撞多是因为槽位不够,正确做法是扩容rehash分散碰撞,而不是树化。树化是最后手段
size() 计数机制
- 内部维护
baseCount+CounterCell[],机制和LongAdder一致 - 写入时: put成功+1,remove成功-1。先CAS改baseCount,失败则根据线程hash找一个CounterCell去CAS
- 读取时: size() = baseCount + 所有CounterCell求和
- Cell数组和槽位没有映射关系,纯粹分散计数竞争
- size()是近似值,不是强一致的
- CounterCell用
@Contended防伪共享
二、LongAdder
为什么要LongAdder
并发场景下需要对一个数做高频的+1/-1操作。最直觉的做法是用AtomicLong,所有线程CAS同一个变量。问题是并发越高,CAS失败越多,失败了就自旋重试,大量线程空转抢一个变量,吞吐量急剧下降,这叫热点竞争。
LongAdder的思路:既然抢一个变量是瓶颈,那就把这个变量拆成多个格子,让线程分散去写,读的时候再加起来。
底层数据结构
内部维护两部分:
- base:一个long值,竞争不激烈时线程直接CAS改这里
- Cell[]:一个Cell数组,每个Cell内部包一个long值
写入流程:
- 线程先尝试CAS修改base
- 如果CAS成功,直接返回
- 如果CAS失败(说明有竞争),根据线程hash值映射到Cell[]中的某个Cell,对该Cell做CAS
- 如果Cell的CAS也失败,会rehash换一个Cell重试,或者对Cell[]扩容
读取流程:
sum() = base + Cell[0].value + Cell[1].value + ... + Cell[n].value- 遍历求和,但求和过程中不加锁,结果是近似值
Cell细节:
- 每个Cell用
@Contended注解做缓存行填充,防止伪共享(false sharing)
本质
用空间换时间,牺牲了精确性
三、AQS
是什么
JUC中绝大多数同步工具的底层骨架。提供:一个状态变量 + 一个等待队列 + 一组模板方法。子类只需定义”获取和释放的语义”,排队、阻塞、唤醒AQS全包。
基于AQS的实现:
| 实现 | state语义 |
|---|---|
| ReentrantLock | 重入次数,0=无人占,1=被占,>1=重入 |
| CountDownLatch | 剩余计数 |
| Semaphore | 剩余许可 |
| ReentrantReadWriteLock | 高16位读锁计数,低16位写锁计数 |
两个核心组件
1. state(int类型)
- volatile修饰,保证可见性
- 通过CAS修改,保证原子性
- 语义由子类定义,AQS本身不关心state代表什么
2. CLH等待队列(双向链表)
- 每个节点是一个Node,代表一个等待中的线程
- 有虚拟head节点(哨兵),不代表任何线程
- Node的waitStatus:
0:初始状态SIGNAL(-1):后继节点需要被唤醒(是对后继的承诺,不是自己的状态)CANCELLED(1):线程取消了,跳过CONDITION(-2):在Condition队列中PROPAGATE(-3):共享模式下释放需要向后传播
代码块收起展开
head(哨兵) ⇄ Node(线程A) ⇄ Node(线程B) ⇄ Node(线程C) = tail独占模式——获取流程
代码块收起展开
acquire(arg)
→ tryAcquire(arg) // 子类实现,尝试一次获取
→ 成功:返回
→ 失败:
→ addWaiter() // 封装成Node,CAS加入队尾
→ acquireQueued() // 自旋+阻塞
for(;;) {
前驱是head?→ 再试tryAcquire → 成功则自己变成新head
不是或失败 → shouldParkAfterFailedAcquire()
把前驱waitStatus设为SIGNAL
→ parkAndCheckInterrupt()
LockSupport.park() 阻塞
→ 被唤醒后回到for循环再试
}关键细节:
- 先设prev再CAS设tail,保证从后往前遍历安全
- 不是入队就立刻阻塞,先看自己是不是排第一个
- 必须先把前驱设成SIGNAL再park,否则release时不知道要唤醒后继
独占模式——释放流程
代码块收起展开
release(arg)
→ tryRelease(arg) // state减,归零返回true
→ true:
→ unparkSuccessor() // 从head.next开始找第一个有效节点
→ LockSupport.unpark() 唤醒- 从tail往前找有效节点,不是从head往后找(因为next可能还没设上)
- 只唤醒一个节点,不是惊群
公平与非公平
唯一区别在tryAcquire():
- 非公平: 新线程直接CAS抢state,不管队列
- 公平: 先看队列里有没有人排着,有就乖乖排队
- 非公平性能更好但可能导致饥饿
共享模式
与独占模式的核心区别:
- 独占:一个线程获取成功后,其他线程必须等
- 共享:多个线程可以同时获取成功
- 传播唤醒: 一个线程被唤醒拿到资源后,如果还有剩余资源,继续唤醒后面的节点,形成链式传播
Condition队列
- 每个Condition维护一个独立的单向等待队列,与CLH队列是两个独立队列
condition.await():完全释放锁 → 从CLH队列移到Condition队列 → park阻塞condition.signal():把Condition队列头节点移回CLH队列尾部 → 重新竞争锁
代码块收起展开
await(): CLH队列 → Condition队列(释放锁,阻塞)
signal(): Condition队列 → CLH队列(重新排队竞争锁)四、CountDownLatch
是什么
一次性的线程等待工具。一方线程await()阻塞等待,另一方线程countDown()通知。计数归零时所有等待线程被唤醒。
底层机制
- AQS的state = 初始计数n
countDown():CAS将state减1,减到0时唤醒所有等待线程await():检查state是否为0,不为0则入CLH队列park阻塞- 一次性:没有方法把state重新设回n
核心特征
等的人和通知的人是两拨,通知的人之间互不等待
代码块收起展开
CountDownLatch latch = new CountDownLatch(5);
// 工人线程:doWork() → latch.countDown()(干完就走)
// 主线程: latch.await()(等所有工人完成)五、CyclicBarrier
是什么
可循环使用的屏障。所有参与线程都调await(),最后一个到达时全部放行,屏障自动重置。
底层机制
- 不是基于AQS,而是基于ReentrantLock + Condition
- 内部维护count计数器,初始值 = parties
await():加锁 → count减1 → 不为0则condition.await()阻塞 → 减到0则signalAll()唤醒所有人 + 重置count
核心特征
所有人既是等待者也是通知者,没有主从之分
CDL vs CB 对比
| CountDownLatch | CyclicBarrier | |
|---|---|---|
| 谁等谁 | 一方等另一方 | 所有人互等 |
| 可否复用 | 一次性 | 自动重置,可循环 |
| 底层实现 | AQS | ReentrantLock + Condition |
| 到达后行为 | countDown后可以继续走 | 必须等所有人到齐 |
| 额外能力 | 无 | 可传入barrierAction,到齐时执行 |
六、Semaphore
是什么
信号量,控制同时访问某个资源的线程数量。本质是许可计数器。
底层机制
- AQS的state = 许可数n
acquire():CAS将state减1,减到0则阻塞release():CAS将state加1,唤醒一个等待线程- 支持公平/非公平模式
和 ReentrantLock 的区别(许可数=1时)
| Semaphore(1) | ReentrantLock | |
|---|---|---|
| 所有权 | 无,线程A acquire线程B可以release | 有,谁lock的必须谁unlock |
| 可重入 | 不可重入,同一线程acquire两次死锁 | 可重入 |
| 语义 | 资源许可 | 互斥 |
七、ThreadPoolExecutor
核心参数
| 参数 | 含义 |
|---|---|
| corePoolSize | 核心线程数 |
| maximumPoolSize | 最大线程数 |
| keepAliveTime | 非核心线程空闲存活时间 |
| workQueue | 任务队列(BlockingQueue) |
| threadFactory | 线程工厂 |
| handler | 拒绝策略 |
ctl字段
一个AtomicInteger同时存两个信息:
- 高3位:线程池状态(RUNNING → SHUTDOWN → STOP → TIDYING → TERMINATED)
- 低29位:当前工作线程数
execute() 流程
代码块收起展开
提交任务
→ 工作线程数 < corePoolSize?
→ Yes:创建核心线程执行
→ No:workQueue.offer() 入队成功?
→ Yes:double-check状态
→ No(队列满):创建非核心线程?
→ Yes:非核心线程执行
→ No(达到maximumPoolSize):拒绝策略顺序:核心线程 → 队列 → 非核心线程 → 拒绝策略
Worker 工作循环
代码块收起展开
Worker.run() → runWorker()
→ while循环 { 执行firstTask → getTask()从队列取 → 执行 → 取 → ... }
→ getTask()返回null → 线程退出getTask()——决定线程生死
- 核心线程:
workQueue.take(),无限等待,永不超时 - 非核心线程:
workQueue.poll(keepAliveTime),超时没任务返回null,线程退出 - 这就是keepAliveTime生效的地方
四种拒绝策略
| 策略 | 行为 |
|---|---|
| AbortPolicy(默认) | 抛 RejectedExecutionException |
| CallerRunsPolicy | 调用者线程自己执行任务 |
| DiscardPolicy | 静默丢弃 |
| DiscardOldestPolicy | 丢弃队列最老的任务,重新提交当前任务 |
八、BlockingQueue
ArrayBlockingQueue vs LinkedBlockingQueue
| ArrayBlockingQueue | LinkedBlockingQueue | |
|---|---|---|
| 底层结构 | 数组 | 链表 |
| 有界/无界 | 有界,构造时必须指定容量 | 默认无界(Integer.MAX_VALUE) |
| 锁设计 | 一把锁,put和take共用 | 两把锁,putLock和takeLock分开 |
| 并发性能 | 较低,生产消费互斥 | 较高,生产消费可同时进行 |
为什么LinkedBQ能用两把锁: 链表头尾是两个独立节点,放在尾部取在头部,操作不同内存。数组是共享连续内存,放和取都操作count和下标,无法拆开。
其他实现
- SynchronousQueue: 不存储元素,put必须有人正在take才能成功,一手交一手
- PriorityBlockingQueue: 带优先级排序,无界
- DelayQueue: 元素到了延迟时间才能取出
BlockingQueue vs 线程池
- BlockingQueue满了 →
put()阻塞等待,等别人take腾空间 - 线程池用的是
offer(),满了直接返回false不阻塞,再走创建非核心线程 → 拒绝策略的逻辑
九、LockSupport
是什么
JUC底层的线程阻塞/唤醒工具,AQS内部用的就是它。
vs Object.wait()/notify()
| LockSupport | wait/notify | |
|---|---|---|
| 前置条件 | 无,随时可调 | 必须在synchronized块内 |
| 顺序问题 | 先unpark后park,不会阻塞(许可存着) | 先notify后wait,永远阻塞(notify白费了) |
| 精确性 | unpark(指定线程),精确唤醒 | notify()随机唤醒一个 |
许可模型(燃料模型)
unpark(thread):给一块燃料(许可),最多存一块park():消耗一块燃料,有就直接通过,没有就阻塞等- 许可不累积: 连续unpark两次 → 还是只有一块燃料 → 第一次park通过,第二次park阻塞