🧵 并发编程知识点全景图

原子性 · 可见性 · 有序性 · 缓存一致性 · 内存屏障 · AQS · 锁机制 · 线程池

🧵 并发编程
─── 从 JMM 出发,8 大核心知识点 ───

原子性

操作不可被中断。i++ 非原子,需 synchronized 或 Atomic 类保证。

三大特性之一
👁️

可见性

线程修改变量后其他线程立即可见。volatile 强制刷主内存 + 缓存失效。

三大特性之一
📐

有序性

指令不被重排。happens-before 规则定义合法执行顺序。

三大特性之一
💾

缓存一致性

MESI 协议保证多核缓存一致:M/E/S/I 四态转换。伪共享问题需 Padding。

硬件层
🧱

内存屏障

4 种屏障禁止指令重排。volatile 写后插 StoreLoad,读后插 LoadLoad+LoadStore。

CPU 指令层
🔗

AQS

CLH 队列 + state 同步状态。ReentrantLock/Semaphore/CountDownLatch 的基础。

框架层
🔒

锁机制

synchronized 锁升级(偏向→轻量→重量)、CAS/乐观锁、读写锁、可重入锁。

实现层
🏊

线程池

7 大参数、4 种拒绝策略、5 种状态。核心→队列→最大→拒绝 提交流程。

工程层

原子性

Atomicity
操作不可分割,要么全部执行要么全不执行
❌ 问题
i++ 实际是 读→+1→写 三步,多线程下数据丢失
✅ 解决
synchronized · Atomic*(CAS) · 数据库事务
👁️

可见性

Visibility
一个线程修改,其他线程立即看到最新值
❌ 问题
CPU 缓存未及时刷新,线程从本地缓存读到旧值 → 死循环
✅ 解决
volatile · synchronized · Lock
📐

有序性

Ordering
程序执行顺序符合 happens-before 规则
❌ 问题
编译器/CPU 指令重排 → DCL 单例拿到未初始化对象
✅ 解决
volatile 禁止重排 · happens-before 规则 · 内存屏障

📋 happens-before 规则(JMM 保证)

1 程序顺序规则:同一线程内,前面的操作 hb 后面的操作
2 volatile 规则:volatile 写 hb 后续对该变量的 volatile 读
3 锁规则:unlock 操作 hb 后续对同一锁的 lock 操作
4 线程启动规则:Thread.start() hb 该线程内的所有操作
5 线程终止规则:线程所有操作 hb Thread.join() 返回
6 传递性:A hb B 且 B hb C → A hb C

📊 三大特性 × 四种机制对比

特性 synchronized ReentrantLock volatile CAS / Atomic
原子性 ✅ 保证 ✅ 保证 ❌ 复合操作不保证 ✅ 保证
可见性 ✅ 保证 ✅ 保证 ✅ 保证 ✅ 保证
有序性 ✅ 保证 ✅ 保证 ✅ 保证 ❌ 不保证
是否阻塞 阻塞 阻塞/可中断 不阻塞 不阻塞(自旋)
适用场景 简单同步 复杂锁控制 标志位/状态 计数器/状态机
M
Modified 已修改
该缓存行已被当前 CPU 修改
主内存数据已过时
拥有最新数据的唯一副本
E
Exclusive 独占
只有当前 CPU 缓存了该行
与主内存数据一致
可直接修改无需通知
S
Shared 共享
多个 CPU 共享该缓存行
与主内存数据一致
修改前需发失效通知
I
Invalid 无效
该缓存行已失效
需重新从主内存或其他 CPU 加载
不能直接使用

🖥️ 多核 CPU 缓存交互示意

CPU 0
M: x=5
I: y
CPU 1
I: x
S: y=3
CPU 2
S: x=5
S: y=3
🗄️ 主内存: x=3 (过时)   y=3

🔄 MESI 状态转换流程

1 CPU 读数据 → 其他 CPU 无缓存 → E 独占
2 另一个 CPU 也读 → 双方都变 S 共享
3 CPU A 写数据 → A 变 M 已修改,B 的缓存行变 I 无效
4 CPU B 再读 → 从 CPU A 缓存(或主内存)重新加载 → 双方变 S 共享

⚠️ 伪共享(False Sharing)

! 两个不相关的变量落在同一缓存行(64 字节),一个变量修改导致另一个也失效
解决:缓存行填充(Padding)/ @Contended 注解 / @jdk.internal.vm.annotation.Contended

StoreStore 屏障

禁止 Store1 重排到 Store2 之后
Store1 Store2
确保 Store1 的写入对其他处理器可见后,才执行 Store2

StoreLoad 屏障

禁止 Store 重排到 Load 之后(最强屏障)
Store Load
确保写入对所有处理器可见后,再执行读操作。开销最大

LoadLoad 屏障

禁止 Load1 重排到 Load2 之后
Load1 Load2
确保 Load1 读取的数据在 Load2 之前完成

LoadStore 屏障

禁止 Load 重排到 Store 之后
Load Store
确保读操作完成后,才执行写操作

⚡ volatile 读/写的内存屏障插入策略

📝 volatile 写(前插 StoreStore,后插 StoreLoad)
普通写 StoreStore 屏障 volatile 写 StoreLoad 屏障 后续操作
含义:volatile 写之前的普通写不会被重排到 volatile 写之后;volatile 写不会重排到后续读之前
📖 volatile 读(后插 LoadLoad + LoadStore)
前置操作 volatile 读 LoadLoad 屏障 LoadStore 屏障 后续读/写
含义:volatile 读之后的读不会被重排到 volatile 读之前;volatile 读之后的写也不会重排到 volatile 读之前

🔧 x86 枂编编层面实现

1 volatile 写 → 生成 lock addl $0x0, (%rsp)mfence,相当于 StoreLoad 全屏障
2 synchronized → MonitorEnter/Exit 隐含完整屏障语义
3 x86 特点 → 是强有序模型(TSO),只允许 Store-Load 重排,因此 volatile 写只需 StoreLoad 屏障

📦 核心字段

state 同步状态(0=无锁,>0=加锁/重入次数)
head CLH 等待队列头节点(哨兵)
tail CLH 等待队列尾节点
exclusiveOwnerThread 独占模式持有线程

⚙️ 模板方法(子类覆写)

tryAcquire() 独占模式获取锁
tryRelease() 独占模式释放锁
tryAcquireShared() 共享模式获取(返回 ≥0 表示成功)
tryReleaseShared() 共享模式释放

🔗 CLH 等待队列(双向链表)

null (哨兵)
waitStatus=0
Thread A
ws=SIGNAL(-1)
Thread B
ws=SIGNAL(-1)
Thread C
ws=0
SIGNAL(-1):后继节点需要被唤醒 · CANCELLED(1):线程已取消 · CONDITION(-2):在条件队列中

🔐 ReentrantLock 加锁/解锁流程

🔒 lock() 流程
1 tryAcquire() → CAS 设 state 0→1
2 成功 → 设 exclusiveOwnerThread
3 失败 → addWaiter() 加入 CLH 队尾
4 acquireQueued() 自旋尝试
5 仍失败 → LockSupport.park() 阻塞
🔓 unlock() 流程
1 tryRelease() → state--
2 state==0 → 清空 exclusiveOwnerThread
3 unparkSuccessor() 唤醒后继
4 LockSupport.unpark(head.next)
5 被唤醒线程继续自旋 tryAcquire

⚖️ 公平锁 vs 非公平锁

非公平 tryAcquire 时直接 CAS 抢锁,不管队列中是否有等待者 → 吞吐量高,但可能饥饿
公平 tryAcquire 先检查 !hasQueuedPredecessors(),有等待者则排队 → 公平但吞吐量低
ReentrantLock
独占
可重入互斥锁
ReadWriteLock
独占+共享
读共享,写独占
Semaphore
共享
信号量,控制并发数
CountDownLatch
共享
等待N个任务完成
CyclicBarrier
独占
N线程互相等待

📈 synchronized 锁升级(JVM 优化)

无锁
对象头无锁标记
→ 竞争 →
偏向锁
记录线程ID
→ 竞争 →
轻量级锁
CAS 自旋
→ 竞争激烈 →
重量级锁
OS Mutex 阻塞
无锁
对象刚创建,无任何线程访问同步块
适用:单线程环境
偏向锁
对象头 Mark Word 记录线程 ID,下次同线程直接进入
适用:只有一个线程访问
轻量级锁
CAS 操作对象头 Mark Word,失败则自旋等待
适用:线程交替执行,竞争不激烈
重量级锁
依赖 OS 互斥量(Mutex),线程阻塞进入内核态
适用:竞争激烈,持有时间长

按互斥性

独占锁(互斥锁)
同一时刻只有一个线程访问
synchronized、ReentrantLock
共享锁(读写锁)
读-读不互斥,读-写/写-写互斥
ReentrantReadWriteLock
乐观锁
不加锁,提交时验证冲突
CAS、版本号
悲观锁
先加锁再操作,假设一定冲突
synchronized

按行为特性

自旋锁
不挂起,循环 CAS 等待
适合锁持有时间极短
阻塞锁
线程挂起进入内核态
适合锁持有时间长
可重入锁
同一线程可多次获取同一把锁
state 递增计数
公平/非公平锁
公平:按队列顺序获取
非公平:直接 CAS 抢锁

🎰 CAS(Compare And Swap)

读取内存值 V
比较 V == A?
相等 → 写入新值 B
|
不等 → 重试(自旋)
❌ ABA 问题
A → B → A,CAS 误判没变化
Thread1 读 A → Thread2 改 A→B→A → Thread1 CAS 成功但值已变过
✅ 解决 ABA
AtomicStampedReference(值+版本号)
compareAndSet(期望值, 新值, 期望版本, 新版本) 双重比对

💀 死锁四要素 & 解决

1 互斥:资源同时只能被一个线程占有
2 占有并等待:持有资源同时等待其他资源
3 不可剥夺:已获得的资源不能被强制回收
4 循环等待:A 等 B,B 等 A
破坏循环等待:资源统一编号,按序申请
破坏占有等待:tryLock(timeout) 超时放弃
检测死锁:jstack / jconsole / ThreadMXBean

📥 线程池任务提交流程

提交任务
execute() / submit()
当前线程 < corePoolSize?
① 是
创建核心线程执行
否 → 队列未满?
② 是
放入任务队列等待
否 → 线程 < maxPoolSize?
③ 是
创建非核心线程执行
执行拒绝策略

🔧 ThreadPoolExecutor 七大核心参数

1 corePoolSize:核心线程数(常驻,即使空闲也不回收)
2 maximumPoolSize:最大线程数(核心+非核心的上限)
3 keepAliveTime:非核心线程空闲存活时间
4 unit:存活时间单位
5 workQueue:任务队列(ArrayBlockingQueue / LinkedBlockingQueue / SynchronousQueue)
6 threadFactory:线程工厂(自定义线程名、优先级等)
7 handler:拒绝策略
🚫
AbortPolicy
直接抛出 RejectedExecutionException
默认策略
🔄
CallerRunsPolicy
调用者线程直接执行任务
降速背压
🗑️
DiscardPolicy
默默丢弃任务,不抛异常
任务丢失
DiscardOldestPolicy
丢弃队列最老的任务,重新提交当前任务
牺牲旧任务

🔄 线程池状态机

RUNNING
接收新任务+处理队列
shutdown()
SHUTDOWN
不接收新任务+处理队列
队列&线程空
TIDYING
所有任务终止
terminated()
TERMINATED
线程池完全终止
RUNNING
shutdownNow()
STOP
不接收+不处理+中断
线程空
TIDYING
terminated()
TERMINATED

⚠️ Executors 创建的线程池为什么被禁用?

newFixedThreadPool
无界队列 → OOM
newCachedThreadPool
max=Integer.MAX → 线程数爆炸 OOM
newSingleThreadExecutor
无界队列 → OOM
newScheduledThreadPool
max=Integer.MAX → 线程数爆炸

📏 线程池参数设置公式

CPU CPU 密集型:线程数 = CPU 核心数 + 1
IO IO 密集型:线程数 = CPU 核心数 × 2
📐 通用公式:线程数 = CPU 核心数 × (1 + 等待时间/计算时间)