外观
并发编程 速查卡
🎯 覆盖 34 题 | ⭐ 高频 15 题 | 预计扫描 12 分钟 📌 先看⭐一句话答案 → 展开要点 → 自测清单检验
一、线程基础
知识地图:进程(资源分配) → 线程(CPU调度) → 虚拟线程(JVM调度,M:N)
⭐ 线程 vs 进程
一句话: 进程是资源分配的基本单位(独立地址空间),线程是 CPU 调度的基本单位(共享进程堆/方法区,私有栈/PC)。
| 维度 | 进程 | 线程 |
|---|---|---|
| 内存 | 独立地址空间 | 共享堆/方法区,私有栈/PC |
| 切换 | 大(切页表,TLB全失效) | 小(不切地址空间) |
| 崩溃 | 不影响其他进程 | 可能拖垮整个进程 |
死锁四条件: "互占不循"——互斥、占有且等待、不可剥夺、循环等待
破坏方法: 固定加锁顺序(破循环) / tryLock 超时(破占有等待) / 避免嵌套锁
二、线程池
⭐ ThreadPoolExecutor 七参数
一句话: 核心参数——corePoolSize(核心线程) + maximumPoolSize(最大线程) + keepAliveTime(空闲存活) + workQueue(任务队列) + threadFactory(线程工厂) + handler(拒绝策略)。
⭐ 线程池执行流程
提交任务 → 线程数 < core? → 是: 创建核心线程执行
→ 否: 队列未满? → 是: 入队等待
→ 否: 线程数 < max? → 是: 创建非核心线程
→ 否: 执行拒绝策略⚠️ 易错:核心线程不是 new ThreadPoolExecutor 时创建,而是第一次提交任务时按需创建
⭐ 四种拒绝策略
| 策略 | 行为 | 适用 |
|---|---|---|
| AbortPolicy(默认) | 抛 RejectedExecutionException | 任务不可丢 |
| CallerRunsPolicy | 调用者线程自己执行(自然限流) | 不想丢任务 |
| DiscardPolicy | 静默丢弃 | 允许丢(如日志) |
| DiscardOldestPolicy | 丢弃队列最老任务 | 保留最新 |
⚠️ 易错:不要用
Executors.newFixedThreadPool(),它用无界队列(Integer.MAX_VALUE),会 OOM
线程数设置: CPU 密集型 = N+1;IO 密集型 = 2N(精确公式 = N/(1-阻塞系数))
三、锁与同步
知识地图:无锁 → 偏向锁 → 轻量级锁(CAS自旋) → 重量级锁(OS Mutex) [单向升级]
⭐ synchronized 原理 + 锁升级
一句话: synchronized 基于 Monitor 对象监视器实现,JDK 6 引入锁升级(偏向锁→轻量级锁→重量级锁)优化性能,锁状态存在对象头 Mark Word 中。
无锁 → 偏向锁(记录线程ID,同线程再次获取无需CAS)
→ 轻量级锁(CAS自旋,栈帧Lock Record写入Mark Word)
→ 重量级锁(ObjectMonitor,_EntryList阻塞+_WaitSet等待)可重入: ObjectMonitor 的 _count 计数器,同线程获锁 _count++,释放 _count--,=0 时真正释放
⭐ volatile
一句话: volatile 保证可见性(写立即刷主存,读从主存取)和有序性(禁止重排序),但不保证原子性(i++ 不安全)。
经典应用——DCL 单例: 必须加 volatile 防止 new 操作的指令重排(分配内存→初始化→赋值引用,2和3可能重排)
| 维度 | volatile | synchronized |
|---|---|---|
| 可见性 | ✅ | ✅ |
| 有序性 | ✅ | ✅ |
| 原子性 | ❌ | ✅ |
| 互斥 | ❌ | ✅ |
| 性能 | 轻量(内存屏障) | 重(可能阻塞) |
⭐ synchronized vs ReentrantLock
一句话: 大多数场景用 synchronized(简洁+JVM自动管理),需要超时获锁/可中断/公平锁/多条件变量时才用 ReentrantLock。
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 锁释放 | 自动(退出同步块) | 手动(必须finally unlock) |
| 可中断 | ❌ | ✅ lockInterruptibly() |
| 超时 | ❌ | ✅ tryLock(timeout) |
| 公平锁 | ❌ | ✅ new ReentrantLock(true) |
| 条件变量 | 1个(wait/notify) | 多个(newCondition) |
| 锁升级 | ✅ | ❌(直接CAS+AQS) |
⭐ AQS 原理
一句话: AQS 是 Java 并发包的骨架(ReentrantLock/Semaphore/CountDownLatch 都基于它),核心 = volatile int state(资源状态) + CLH 双向队列(等待线程)。
独占锁流程:tryAcquire(CAS改state) → 失败入CLH队列 → 前驱是Head时再尝试 → 成功出队
释放锁:tryRelease(state归0) → unpark 队列下一个线程公平 vs 非公平: 非公平锁新来线程直接 CAS 抢占(不检查队列),吞吐量更高;公平锁先 hasQueuedPredecessors() 检查
四、ThreadLocal
⭐ ThreadLocal 原理 + 内存泄漏
一句话: 每个 Thread 持有一个 ThreadLocalMap(key=ThreadLocal 弱引用,value=强引用),线程隔离;使用完必须 remove() 防止线程池场景内存泄漏。
泄漏原因: ThreadLocal 被 GC 后 key=null,但 value 仍被 ThreadLocalMap 强引用 → 线程池中线程长期存活 → value 无法回收
解决: finally { threadLocal.remove(); } 必须执行
⚠️ 易错:key 用弱引用是"尽力而为"设计(至少 ThreadLocal 对象本身能被 GC),不代表不需要 remove
↳ 追问"子线程获取父线程值":InheritableThreadLocal(仅 new Thread 时复制) → TransmittableThreadLocal(线程池场景,任务提交时捕获快照)
五、虚拟线程
⭐ 虚拟线程 + 是否取代线程池
一句话: 虚拟线程(JDK 21)由 JVM 调度,创建成本极低(~1KB),IO 阻塞时自动卸载 carrier thread;用 per-task 模型替代传统线程池。
| 维度 | 平台线程 | 虚拟线程 |
|---|---|---|
| 创建成本 | ~1MB 栈 | ~1KB |
| 调度 | OS 内核 | JVM 用户态(ForkJoinPool) |
| 数量 | 几千 | 百万级 |
| 适合 | CPU 密集型 | IO 密集型 |
三个注意: ① ThreadLocal 内存膨胀(百万线程×副本)→用 ScopedValue ② synchronized 会 pin carrier thread→改用 ReentrantLock ③ CPU 密集型无优势
Spring Boot 3.2+: spring.threads.virtual.enabled=true 一行配置,Tomcat 自动用虚拟线程
补充速览
| 关键词 | 核心答案 |
|---|---|
| happens-before | A hb B = A 的结果对 B 可见;八条规则:程序顺序/锁/volatile/线程启动终止/中断/终结/传递 |
| CAS | Compare And Swap,CPU 原子指令(cmpxchg);三问题:ABA(加版本号)/自旋开销/只保证单变量 |
| LongAdder | 分段CAS(base+Cell数组),高并发累加比 AtomicLong 快;sum()非原子 |
| 乐观锁 vs 悲观锁 | 乐观(CAS/版本号,读多写少) vs 悲观(synchronized/lock,写多冲突频繁) |
| 锁分类总览 | 可重入/公平/独占/共享/自旋/分段/偏向+轻量+重量 |
| JMM | 线程工作内存(CPU缓存) ↔ 主内存;volatile/synchronized 保证同步 |
| 结构化并发 | StructuredTaskScope(JDK21预览):子任务生命周期跟随父任务,自动取消 |
| 线程创建方式 | Thread/Runnable/Callable+FutureTask/线程池;本质都是 Thread.start() |
| ⭐ CountDownLatch | 一个线程等N个线程完成(计数减到0);一次性不可重置;底层AQS共享锁 |
| ⭐ CyclicBarrier | N个线程互相等待(到齐后同时继续);可重用;典型:多阶段并行计算 |
| ⭐ Semaphore | 控制并发数(限流);acquire获取/release释放许可;典型:连接池限流 |
| ⭐ CompletableFuture | 非阻塞链式异步:supplyAsync→thenApply→thenCombine/allOf/anyOf;指定业务线程池 |
| ⭐ ForkJoinPool | 分治+工作窃取(自己LIFO取,偷别人FIFO);parallelStream底层用commonPool |
🧠 助记汇总
| 口诀 | 含义 |
|---|---|
| 互占不循 | 死锁四条件:互斥/占有等待/不可剥夺/循环等待 |
| 偏轻重(单向升级) | synchronized 锁升级:偏向锁→轻量级锁→重量级锁 |
| core→queue→max→reject | 线程池执行流程四步 |
| 弃拒丢旧 | 四种拒绝策略:AbortPolicy/CallerRuns/Discard/DiscardOldest |
| state + CLH | AQS 两大核心:volatile state + CLH 等待队列 |
| 用完必remove | ThreadLocal 防泄漏铁律 |
✅ 自测清单
| # | 问题 | 你能说出... |
|---|---|---|
| 1 | 线程 vs 进程 | 核心区别 + 切换开销差异原因 |
| 2 | 线程池七参数 | 每个参数含义 |
| 3 | 线程池执行流程 | 四步判断链 |
| 4 | 拒绝策略 | 四种 + 各自适用场景 |
| 5 | synchronized 锁升级 | 偏向→轻量→重量 + 各阶段原理 |
| 6 | volatile | 保证什么 + 不保证什么 + DCL 为什么需要 |
| 7 | sync vs ReentrantLock | 6 个核心区别 |
| 8 | AQS | state + CLH 队列 + 独占锁流程 |
| 9 | ThreadLocal 泄漏 | 弱引用key+强引用value + remove |
| 10 | 虚拟线程 | 与平台线程区别 + 三个注意事项 |
| 11 | 死锁 | 四条件 + 排查(jstack) + 解决 |
| 12 | 并发工具三件套 | CountDownLatch/CyclicBarrier/Semaphore 各自等待模型和场景 |
| 13 | CompletableFuture | allOf+join 并行查询 + 异常处理(exceptionally/handle) |
| 14 | ForkJoinPool | 工作窃取算法 + parallelStream 为什么不适合 IO |
| 12 | CAS | 原理 + 三个问题 |
💡 首次全部过一遍 → 第2天只过答不上来的 → 第4天再复习 → 面试前一天最后扫一遍