
Java并发编程高频实战知识体系:死锁排查、线程安全定位与线程Dump分析
一、核心基础理论层
1.1 并发编程核心概念
- 进程与线程:进程是资源分配单位,线程是CPU调度单位;Java线程与操作系统内核线程的映射关系(1:1模型)
- 并发与并行:并发是多个任务交替执行,并行是多个任务同时执行
- 上下文切换:CPU从一个线程切换到另一个线程的过程,涉及寄存器保存与恢复、内核态/用户态切换
- Java内存模型(JMM):定义了线程与主内存之间的抽象关系,规定了可见性、原子性、有序性三大特性
- happens-before原则:判断数据是否存在竞争、线程是否安全的核心依据
1.2 线程安全基础
- 线程安全定义:多线程环境下,无论如何调度,都能得到正确结果
- 线程不安全的根本原因:
- 共享可变状态
- 竞态条件(Race Condition)
- 指令重排序
- 内存可见性问题
- 线程安全的实现方式:
- 不可变对象
- 线程封闭(栈封闭、ThreadLocal)
- 同步机制(synchronized、Lock)
- 原子类(Atomic系列)
- 并发容器
二、死锁排查实战体系
2.1 死锁理论基础
- 死锁定义:两个或多个线程互相等待对方释放资源,导致永久阻塞的状态
- 死锁产生的四个必要条件( Coffman条件):
- 互斥条件:资源同一时间只能被一个线程持有
- 请求与保持条件:线程已持有至少一个资源,又请求其他被持有的资源
- 不可剥夺条件:资源不能被强制剥夺,只能由持有者主动释放
- 循环等待条件:线程之间形成循环等待资源的关系
- 死锁的危害:
- 系统吞吐量急剧下降
- 服务无响应
- 资源耗尽
- 系统崩溃
2.2 死锁的常见场景
- 嵌套锁顺序不一致:最常见的死锁场景
// 线程1 synchronized(lockA) { synchronized(lockB) { ... } } // 线程2 synchronized(lockB) { synchronized(lockA) { ... } } - 数据库死锁:多个事务同时更新多行数据且顺序不同
- 分布式死锁:跨进程/跨服务的资源竞争
- 资源池耗尽导致的类死锁:如线程池、数据库连接池耗尽
- Thread.join()死锁:线程互相等待对方结束
- Lock接口的错误使用:未在finally中释放锁
2.3 死锁排查工具链
2.3.1 JDK内置工具
- jps:查看Java进程ID
jps -l # 列出所有Java进程及其主类全名 - jstack:生成线程快照,最常用的死锁排查工具
jstack <pid> # 生成线程dump jstack -F <pid> # 强制生成dump(当进程无响应时) jstack -l <pid> # 包含锁信息 - jconsole:图形化监控工具,可查看线程状态、检测死锁
- jvisualvm:功能更强大的可视化工具,集成了jstack、jconsole等功能
- jcmd:多功能命令行工具,可替代jps、jstack等
jcmd <pid> Thread.print # 生成线程dump
2.3.2 第三方工具
- Arthas:阿里巴巴开源的Java诊断工具,功能强大
thread # 查看线程堆栈 thread -b # 找出阻塞其他线程的线程 deadlock # 检测死锁 - FastThread:在线线程dump分析工具
- IBM Thread and Monitor Dump Analyzer (TMDA):IBM提供的分析工具
- PerfMa:国内专业的JVM性能分析平台
2.4 死锁排查步骤
确认死锁现象:
- 系统无响应,CPU使用率低但负载高
- 关键业务操作超时
- 日志中出现大量线程阻塞信息
获取线程dump:
- 生产环境建议使用
jstack -l <pid> > dump.txt - 建议连续获取3-5次dump,间隔10-20秒,便于对比分析
- 生产环境建议使用
分析线程dump:
- 查找"Found one Java-level deadlock:"字样
- 查看每个死锁线程的堆栈信息
- 确定互相等待的锁对象
- 找到持有锁的线程及其堆栈
定位代码问题:
- 根据堆栈信息找到对应的代码行
- 分析锁的获取顺序
- 确认是否满足死锁的四个必要条件
验证与复现:
- 在测试环境复现问题
- 验证修复方案的有效性
2.5 死锁解决方案
- 破坏循环等待条件:最常用的方法
- 统一锁的获取顺序
- 使用数字编号对锁进行排序
- 破坏请求与保持条件:
- 一次性申请所有需要的资源
- 申请不到时释放已持有的资源
- 破坏不可剥夺条件:
- 使用
tryLock(long timeout, TimeUnit unit)方法 - 超时自动释放已持有的锁
- 使用
- 破坏互斥条件:
- 使用原子类代替锁
- 使用读写锁(ReentrantReadWriteLock)提高并发性
- 其他预防措施:
- 避免嵌套锁
- 减少锁的持有时间
- 使用开放调用(在调用外部方法时释放锁)
- 避免在一个锁中持有多个资源
三、线程安全问题定位实战体系
3.1 常见线程安全问题类型
3.1.1 竞态条件问题
- check-then-act:先检查后执行,如单例模式的双重检查锁定(DCL)问题
- read-modify-write:读-改-写操作,如
count++ - 复合操作:多个原子操作组合在一起,但整体不是原子的
3.1.2 可见性问题
- 一个线程修改了共享变量,但其他线程看不到修改结果
- 典型场景:
- 没有使用volatile关键字
- 没有使用同步机制
- 指令重排序导致的可见性问题
3.1.3 有序性问题
- 编译器和CPU为了优化性能,会对指令进行重排序
- 典型场景:
- DCL单例模式的指令重排序问题
- 没有正确使用volatile关键字
- 没有正确使用happens-before原则
3.1.4 其他常见问题
- 对象逸出:对象在构造完成前就被发布
- 死循环:多线程环境下的死循环
- 线程泄漏:线程没有正确结束,导致线程数不断增加
- 数据不一致:多个线程同时修改同一数据导致数据错误
3.2 线程安全问题定位工具
3.2.1 静态代码分析工具
- FindBugs/SpotBugs:检测常见的并发问题
- PMD:代码质量分析工具,包含并发规则
- SonarQube:代码质量管理平台,集成了多种静态分析工具
- IntelliJ IDEA内置分析器:实时检测并发问题
3.2.2 动态分析工具
- jstack:查看线程状态和堆栈信息
- jconsole/jvisualvm:监控线程状态、CPU使用率、内存使用情况
- Arthas:
watch # 观察方法调用的参数、返回值和异常 trace # 跟踪方法内部调用路径和耗时 stack # 输出方法的调用堆栈 - BTrace:动态追踪工具,可在不重启应用的情况下插入代码
- Java Flight Recorder (JFR):JDK内置的低开销性能分析工具
- AsyncProfiler:高性能的CPU和内存分析工具
3.2.3 测试工具
- JUnit + TestNG:编写并发测试用例
- OpenJDK jcstress:专门用于测试Java并发代码正确性的工具
- Google ThreadSanitizer:检测数据竞争的工具
- MultiThreadedTC:用于测试并发代码的框架
3.3 线程安全问题定位步骤
复现问题:
- 在测试环境尽可能复现问题
- 增加并发压力,提高问题出现的概率
- 记录问题出现的条件和现象
收集信息:
- 查看应用日志,寻找异常信息
- 生成线程dump,分析线程状态
- 监控系统资源使用情况(CPU、内存、IO)
- 使用动态分析工具收集运行时数据
分析问题:
- 确定问题类型(竞态条件、可见性、有序性等)
- 找到共享可变状态
- 分析线程之间的交互关系
- 定位问题代码
验证问题:
- 编写单元测试复现问题
- 使用并发测试工具验证问题
- 确认问题的根本原因
修复问题:
- 选择合适的解决方案
- 修复代码
- 验证修复效果
3.4 线程安全问题解决方案
- 使用同步机制:
synchronized关键字:方法级、代码块级java.util.concurrent.locks包:ReentrantLock、ReentrantReadWriteLock、StampedLock
- 使用原子类:
java.util.concurrent.atomic包:AtomicInteger、AtomicLong、AtomicReference等- 基于CAS操作实现,无锁并发
- 使用并发容器:
ConcurrentHashMap代替HashMapCopyOnWriteArrayList代替ArrayListBlockingQueue实现生产者-消费者模式
- 使用线程封闭:
- 栈封闭:使用局部变量
- ThreadLocal:每个线程拥有自己的变量副本
- 使用不可变对象:
- 将类声明为final
- 将所有字段声明为private final
- 不提供修改对象状态的方法
- 正确使用volatile关键字:
- 保证可见性
- 禁止指令重排序
- 适用于一写多读的场景
四、线程Dump分析实战体系
4.1 线程Dump基础
- 线程Dump定义:JVM在某一时刻所有线程的状态快照
线程Dump包含的信息:
- 线程名称、ID、优先级
- 线程状态(NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED)
- 线程调用堆栈
- 锁信息(持有哪些锁、等待哪些锁)
- 死锁信息
线程状态详解:
| 状态 | 说明 |
|------|------|
| NEW | 线程已创建但未启动 |
| RUNNABLE | 线程正在JVM中执行,或等待操作系统资源(如CPU) |
| BLOCKED | 线程阻塞,等待获取监视器锁 |
| WAITING | 线程无限期等待另一个线程执行特定操作 |
| TIMED_WAITING | 线程在指定时间内等待另一个线程执行特定操作 |
| TERMINATED | 线程已执行完毕 |
4.2 线程Dump生成方法
4.2.1 命令行方式
- jstack:最常用的方法
jstack <pid> > thread_dump_$(date +%Y%m%d_%H%M%S).txt - jcmd:推荐使用的方法
jcmd <pid> Thread.print > thread_dump.txt - kill命令(Linux/Unix):
kill -3 <pid> # 生成线程dump并输出到标准输出
4.2.2 图形化工具方式
- jvisualvm:在"线程"标签页点击"线程Dump"按钮
- jconsole:在"线程"标签页点击"检测死锁"按钮
- Arthas:
thread > thread_dump.txt
4.2.3 代码方式
- 使用ThreadMXBean:
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean(); ThreadInfo[] threadInfos = threadBean.dumpAllThreads(true, true); for (ThreadInfo info : threadInfos) { System.out.println(info); }
4.3 线程Dump分析步骤
获取多个线程dump:
- 连续获取3-5次dump,间隔10-20秒
- 便于对比分析线程状态变化
整体概览:
- 查看总线程数
- 统计各状态线程的数量
- 查看是否有死锁信息
重点关注异常线程:
- BLOCKED状态线程:数量过多可能存在锁竞争
- WAITING/TIMED_WAITING状态线程:数量过多可能存在资源等待问题
- RUNNABLE状态线程:长时间处于RUNNABLE状态可能存在死循环
分析锁信息:
- 查看哪些线程持有锁
- 查看哪些线程在等待锁
- 分析锁竞争的原因
分析调用堆栈:
- 找到问题线程的调用堆栈
- 定位到具体的代码行
- 分析代码逻辑
对比多个dump:
- 查看线程状态是否有变化
- 查看锁持有情况是否有变化
- 确认是否存在永久阻塞的线程
4.4 常见问题模式分析
4.4.1 死锁模式
特征:
- 线程dump开头有"Found one Java-level deadlock:"字样
- 多个线程互相等待对方释放锁
- 线程状态为BLOCKED
示例:
Found one Java-level deadlock: ============================= "Thread-1": waiting to lock monitor 0x000000001b89c388 (object 0x00000000d600f480, a java.lang.Object), which is held by "Thread-0" "Thread-0": waiting to lock monitor 0x000000001b899c88 (object 0x00000000d600f490, a java.lang.Object), which is held by "Thread-1"
4.4.2 锁竞争模式
特征:
- 大量线程处于BLOCKED状态
- 这些线程都在等待同一个锁
- 持有锁的线程执行时间过长
常见原因:
- 锁的粒度太大
- 锁持有时间过长
- 锁竞争激烈
4.4.3 死循环模式
特征:
- 某个线程长时间处于RUNNABLE状态
- CPU使用率很高
- 线程堆栈停留在同一个方法调用
常见原因:
- 多线程环境下的死循环
- 无限递归
- 网络IO超时设置不当
4.4.4 资源等待模式
特征:
- 大量线程处于WAITING或TIMED_WAITING状态
- 线程都在等待同一个资源
- 系统吞吐量下降
常见原因:
- 数据库连接池耗尽
- 线程池耗尽
- 消息队列阻塞
- 网络IO阻塞
4.5 线程Dump分析最佳实践
- 生成多个dump:单个dump可能无法反映真实情况
- 在问题发生时生成dump:避免在系统正常时生成
- 保留完整的dump信息:包括锁信息和堆栈信息
- 使用专业的分析工具:提高分析效率
- 结合其他监控数据:如CPU使用率、内存使用情况、GC日志等
- 建立线程dump分析知识库:记录常见问题和解决方案
五、高级进阶与最佳实践
5.1 并发编程最佳实践
- 优先使用并发工具类:如
java.util.concurrent包中的类 - 优先使用线程池:避免手动创建线程
- 最小化锁的范围:只在必要的代码块上加锁
- 优先使用读写锁:在读多写少的场景下提高并发性
- 正确使用volatile关键字:只在一写多读的场景下使用
- 避免使用ThreadLocal存储可变状态:防止内存泄漏
- 正确处理中断:不要忽略InterruptedException
- 使用不可变对象:从根本上避免线程安全问题
- 编写并发测试用例:提前发现并发问题
- 监控并发指标:如线程数、锁竞争次数、死锁次数等
5.2 生产环境并发问题应急处理
- 快速定位问题:使用jstack、Arthas等工具快速生成线程dump
- 临时解决方案:
- 重启应用(最快速但会丢失数据)
- 调整线程池大小
- 增加资源(如数据库连接数)
- 根本原因分析:
- 详细分析线程dump和日志
- 在测试环境复现问题
- 找到问题的根本原因
- 预防措施:
- 修复代码问题
- 增加监控告警
- 优化系统架构
5.3 常见误区与陷阱
- 认为原子操作都是线程安全的:多个原子操作组合在一起不是原子的
- 过度使用volatile关键字:volatile不能保证原子性
- 错误使用ThreadLocal:导致内存泄漏
- 在finally中释放锁:避免锁泄漏
- 不要在锁中调用外部方法:可能导致死锁
- 不要忽略InterruptedException:正确处理线程中断
- 不要使用stop()方法停止线程:会导致线程不安全
六、面试高频考点总结
- 死锁产生的四个必要条件及如何破坏这些条件
- 如何排查死锁:工具、步骤、分析方法
- 线程安全的实现方式及各自的优缺点
- synchronized与ReentrantLock的区别
- volatile关键字的作用及原理
- 线程的状态及转换
- 线程dump包含哪些信息及如何分析
- 常见的线程安全问题及解决方案
- 并发容器的实现原理:如ConcurrentHashMap
- 线程池的参数及工作原理
Java并发编程面试高频考点清单(按考察频率排序)
🔥 五星必考题(90%以上概率考察)
1. 死锁相关
死锁产生的四个必要条件(Coffman条件)
- 核心答案:互斥条件、请求与保持条件、不可剥夺条件、循环等待条件
- 延伸:如何破坏每个条件?(破坏循环等待最常用)
如何排查死锁?
- 核心答案:
- 工具:jstack、jconsole、jvisualvm、Arthas(
deadlock命令) - 步骤:确认现象→获取3-5次线程dump→查找"Found one Java-level deadlock"→分析锁等待关系→定位代码
- 工具:jstack、jconsole、jvisualvm、Arthas(
- 延伸:生产环境如何安全获取线程dump?
- 核心答案:
死锁的解决方案
- 核心答案:统一锁获取顺序、一次性申请所有资源、使用
tryLock(timeout)、避免嵌套锁
- 核心答案:统一锁获取顺序、一次性申请所有资源、使用
2. 线程安全基础
什么是线程安全?线程不安全的根本原因是什么?
- 核心答案:多线程任意调度均得正确结果;根本原因是共享可变状态+竞态条件+指令重排序+可见性问题
synchronized与ReentrantLock的区别
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 实现方式 | JVM层面关键字 | JDK层面API |
| 锁类型 | 可重入、非公平 | 可重入、公平/非公平可选 |
| 中断响应 | 不支持 | 支持lockInterruptibly() |
| 超时获取 | 不支持 | 支持tryLock(timeout) |
| 条件变量 | 不支持 | 支持多个Condition |
| 释放方式 | 自动释放 | 必须在finally中手动释放 |
volatile关键字的作用与原理
- 核心答案:保证可见性、禁止指令重排序;不保证原子性
- 原理:内存屏障(Load屏障、Store屏障)
- 适用场景:一写多读、状态标记、DCL单例模式
Java内存模型(JMM)三大特性
- 核心答案:原子性、可见性、有序性
- 延伸:每个特性分别由哪些机制保证?
3. 线程与线程池
Java线程的状态及转换流程
- 核心答案:NEW→RUNNABLE→BLOCKED/WAITING/TIMED_WAITING→TERMINATED
- 重点:各状态转换的触发条件
线程池的核心参数及工作原理
- 核心参数:核心线程数、最大线程数、空闲时间、工作队列、拒绝策略
- 工作原理:任务提交→核心线程处理→入队→非核心线程处理→拒绝策略
- 延伸:如何合理设置线程池参数?
常见的线程池类型及适用场景
- 核心答案:
- FixedThreadPool:固定线程数,适用于任务量稳定的场景
- CachedThreadPool:缓存线程池,适用于大量短任务
- SingleThreadExecutor:单线程池,保证任务顺序执行
- ScheduledThreadPool:定时任务线程池
- 延伸:为什么不推荐使用Executors创建线程池?
- 核心答案:
4. 并发容器
- ConcurrentHashMap的实现原理(JDK7 vs JDK8)
- JDK7:分段锁(Segment),数组+链表
- JDK8:CAS+synchronized,数组+链表+红黑树,锁粒度更细
- 延伸:为什么JDK8放弃分段锁?
⭐⭐⭐⭐ 四星高频题(70%以上概率考察)
1. 线程安全问题
常见的线程安全问题类型及解决方案
- 竞态条件:check-then-act、read-modify-write
- 可见性问题、有序性问题
- 解决方案:同步机制、原子类、并发容器、线程封闭、不可变对象
什么是竞态条件?举个例子
- 核心答案:多个线程对共享变量的执行顺序影响结果
- 例子:
count++、单例模式的双重检查锁定问题
ThreadLocal的实现原理及内存泄漏问题
- 原理:每个Thread持有一个ThreadLocalMap,key为ThreadLocal弱引用,value为线程私有值
- 内存泄漏:ThreadLocal被回收后,key变为null,value无法被回收
- 解决方案:使用完ThreadLocal后调用
remove()方法
2. 线程Dump分析
线程Dump包含哪些信息?
- 核心答案:线程名称、ID、优先级、状态、调用堆栈、锁信息、死锁信息
如何通过线程Dump分析常见问题?
- 死锁:查找"Found one Java-level deadlock"
- 锁竞争:大量BLOCKED线程等待同一个锁
- 死循环:单个线程长时间处于RUNNABLE状态+高CPU
- 资源等待:大量WAITING/TIMED_WAITING线程
生成线程Dump的方法有哪些?
- 命令行:jstack、jcmd、kill -3(Linux)
- 图形化:jvisualvm、jconsole
- 代码:ThreadMXBean.dumpAllThreads()
3. 并发工具类
原子类的实现原理(CAS)
- 核心答案:Compare And Swap,无锁并发
- 缺点:ABA问题、循环时间长开销大、只能保证一个变量的原子性
- 解决方案:AtomicStampedReference解决ABA问题
CountDownLatch、CyclicBarrier、Semaphore的区别
| 工具 | 作用 | 适用场景 | 可重用性 |
|---|---|---|---|
| CountDownLatch | 一个线程等待多个线程完成 | 主线程等待子线程初始化 | 否 |
| CyclicBarrier | 多个线程互相等待 | 多线程计算结果合并 | 是 |
| Semaphore | 控制同时访问资源的线程数 | 限流 | 是 |
- 读写锁(ReentrantReadWriteLock)的原理及适用场景
- 原理:读锁共享、写锁独占
- 适用场景:读多写少
- 缺点:写锁饥饿问题(StampedLock解决)
⭐⭐⭐ 三星常考题(50%左右概率考察)
1. 基础理论
happens-before原则
- 核心答案:判断数据是否存在竞争、线程是否安全的核心依据
- 常见规则:程序次序规则、锁定规则、volatile变量规则、传递性规则
进程与线程的区别
- 核心答案:进程是资源分配单位,线程是CPU调度单位;线程共享进程资源
上下文切换的开销
- 核心答案:寄存器保存与恢复、内核态/用户态切换、缓存失效
2. 实战问题
如何定位线程安全问题?
- 工具:静态分析(SpotBugs、SonarQube)、动态分析(Arthas、JFR)、并发测试(jcstress)
- 步骤:复现问题→收集信息→分析问题→验证问题→修复问题
生产环境遇到并发问题如何应急处理?
- 快速定位:jstack/Arthas生成线程dump
- 临时解决:重启应用、调整线程池参数、增加资源
- 根本解决:分析dump和日志、测试环境复现、修复代码
什么是对象逸出?如何避免?
- 核心答案:对象在构造完成前就被发布
- 避免:不要在构造函数中启动线程、不要在构造函数中暴露this引用
3. 高级进阶
StampedLock的原理及优势
- 核心答案:乐观读模式,比读写锁性能更高
- 优势:解决了读写锁的写锁饥饿问题
什么是锁升级?synchronized的锁升级过程
- 核心答案:无锁→偏向锁→轻量级锁→重量级锁
- 触发条件:竞争程度逐渐增加
AQS(抽象队列同步器)的原理
- 核心答案:基于CLH队列,使用volatile state变量表示同步状态
- 实现:ReentrantLock、CountDownLatch、Semaphore等都基于AQS
💡 面试加分题(考察深度与实战经验)
- 你在项目中遇到过哪些并发问题?如何解决的?
- 如何设计一个线程安全的单例模式?(至少写出3种)
- 如何避免死锁?在项目中你是怎么做的?
- 什么是伪共享?如何避免?
- Java 8之后并发包有哪些新特性?
- 如何进行并发性能调优?
- 分布式环境下如何解决并发问题?
- 什么是线程泄漏?如何排查和避免?
Java并发编程面试高频考点问答卡片(按考察频率排序)
🔥 五星必考题(90%以上概率考察)
卡片1
问题:死锁产生的四个必要条件是什么?如何破坏这些条件?
标准答案:
- 四个必要条件(Coffman条件):
- 互斥条件:资源同一时间只能被一个线程持有
- 请求与保持条件:线程已持有至少一个资源,又请求其他被持有的资源
- 不可剥夺条件:资源不能被强制剥夺,只能由持有者主动释放
- 循环等待条件:线程之间形成循环等待资源的关系
- 破坏方法:
- 破坏循环等待(最常用):统一锁获取顺序、按数字编号排序
- 破坏请求与保持:一次性申请所有资源、申请失败释放已持资源
- 破坏不可剥夺:使用
tryLock(long timeout)超时自动释放 - 破坏互斥:使用原子类代替锁、读写锁提高并发性
卡片2
问题:如何排查死锁?
标准答案:
- 确认现象:系统无响应、CPU使用率低但负载高、业务操作超时、日志出现大量线程阻塞信息
- 获取线程dump:使用
jstack -l <pid>或jcmd <pid> Thread.print,连续获取3-5次,间隔10-20秒 - 分析dump:查找"Found one Java-level deadlock:"字样,查看死锁线程的堆栈和锁等待关系
- 定位代码:根据堆栈信息找到对应的代码行,分析锁的获取顺序
- 验证修复:在测试环境复现问题并验证修复方案
生产环境注意:jstack对性能影响很小,可安全使用;避免在系统负载极高时强制生成dump
卡片3
问题:synchronized与ReentrantLock的区别是什么?
标准答案:
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 实现层面 | JVM内置关键字 | JDK层面API实现 |
| 锁类型 | 可重入、非公平锁 | 可重入、公平/非公平锁可选 |
| 中断响应 | 不支持,线程会一直阻塞 | 支持lockInterruptibly()方法 |
| 超时获取 | 不支持 | 支持tryLock(long timeout, TimeUnit unit) |
| 条件变量 | 不支持 | 支持多个Condition对象 |
| 释放方式 | 自动释放(退出代码块或异常) | 必须在finally块中手动调用unlock() |
| 性能 | 低竞争下性能好 | 高竞争下性能更稳定 |
卡片4
问题:volatile关键字的作用是什么?原理是什么?
标准答案:
- 核心作用:
- 保证可见性:一个线程修改了volatile变量,其他线程能立即看到最新值
- 禁止指令重排序:编译器和CPU不会对volatile变量的读写操作进行重排序
- 不保证原子性:不能保证
count++这类复合操作的原子性
- 实现原理:通过插入内存屏障实现
- 写volatile变量时,在写操作后插入Store屏障,将本地内存数据刷新到主内存
- 读volatile变量时,在读操作前插入Load屏障,从主内存读取最新数据
- 适用场景:一写多读、状态标记、双重检查锁定(DCL)单例模式
卡片5
问题:Java线程池的核心参数有哪些?工作原理是什么?
标准答案:
- 核心参数:
corePoolSize:核心线程数,长期存活的线程数量maximumPoolSize:最大线程数,线程池允许的最大线程数量keepAliveTime:非核心线程的空闲存活时间workQueue:工作队列,用于存放等待执行的任务handler:拒绝策略,当任务无法处理时的处理方式
- 工作原理:
- 任务提交时,若核心线程数未满,创建核心线程执行任务
- 若核心线程数已满,将任务加入工作队列
- 若工作队列已满,创建非核心线程执行任务
- 若总线程数达到maximumPoolSize,执行拒绝策略
卡片6
问题:ConcurrentHashMap在JDK7和JDK8中的实现原理有什么区别?
标准答案:
- JDK7实现:
- 采用分段锁(Segment)机制,将数据分成多个Segment,每个Segment独立加锁
- 数据结构:Segment数组 + HashEntry数组 + 链表
- 并发度:默认16,最多支持16个线程同时写
- JDK8实现:
- 放弃分段锁,采用CAS + synchronized机制
- 数据结构:Node数组 + 链表 + 红黑树(链表长度超过8转为红黑树)
- 锁粒度:只锁当前链表的头节点,并发度更高
- 优势:性能提升明显,解决了JDK7中分段锁粒度较粗的问题
⭐⭐⭐⭐ 四星高频题(70%以上概率考察)
卡片7
问题:什么是线程安全?线程不安全的根本原因是什么?
标准答案:
- 线程安全定义:多线程环境下,无论系统如何调度线程,程序都能得到正确的结果
- 根本原因:
- 共享可变状态:多个线程同时访问和修改同一个变量
- 竞态条件:多个线程对共享变量的执行顺序影响最终结果
- 指令重排序:编译器和CPU为了优化性能,会对指令进行重排序
- 内存可见性问题:一个线程修改了共享变量,其他线程可能看不到修改结果
卡片8
问题:ThreadLocal的实现原理是什么?为什么会发生内存泄漏?
标准答案:
- 实现原理:
- 每个Thread对象都持有一个ThreadLocalMap对象
- ThreadLocalMap的key是ThreadLocal对象的弱引用,value是线程私有值
- 每个线程只能访问自己的ThreadLocalMap,实现了线程隔离
- 内存泄漏原因:
- ThreadLocal被回收后,ThreadLocalMap中的key变为null
- 但value是强引用,不会被回收,导致value一直占用内存
- 解决方案:使用完ThreadLocal后,必须调用
remove()方法手动清理
卡片9
问题:CountDownLatch、CyclicBarrier、Semaphore的区别是什么?
标准答案:
| 工具类 | 核心作用 | 适用场景 | 可重用性 |
|---|---|---|---|
| CountDownLatch | 一个线程等待多个线程完成 | 主线程等待子线程初始化完成 | 否(计数到0后无法重置) |
| CyclicBarrier | 多个线程互相等待,达到屏障点后一起执行 | 多线程计算结果合并 | 是(可重置计数) |
| Semaphore | 控制同时访问特定资源的线程数量 | 限流、资源池控制 | 是(可释放和重新获取许可) |
卡片10
问题:Java线程有哪些状态?状态之间如何转换?
标准答案:
- 6种线程状态:
- NEW:线程已创建但未调用
start()方法 - RUNNABLE:线程正在JVM中执行,或等待操作系统资源(如CPU)
- BLOCKED:线程阻塞,等待获取监视器锁
- WAITING:线程无限期等待另一个线程执行特定操作(如
wait()、join()) - TIMED_WAITING:线程在指定时间内等待(如
sleep()、wait(long)) - TERMINATED:线程已执行完毕
- NEW:线程已创建但未调用
- 主要转换流程:
- NEW → RUNNABLE:调用
start()方法 - RUNNABLE → BLOCKED:等待获取synchronized锁
- RUNNABLE → WAITING:调用
wait()、join()、LockSupport.park() - RUNNABLE → TIMED_WAITING:调用
sleep()、wait(long)、join(long) - BLOCKED/WAITING/TIMED_WAITING → RUNNABLE:获取到锁、被唤醒、等待超时
- RUNNABLE → TERMINATED:线程执行完毕或抛出未捕获异常
- NEW → RUNNABLE:调用
卡片11
问题:如何通过线程Dump分析常见问题?
标准答案:
- 死锁问题:
- 特征:dump开头有"Found one Java-level deadlock:"字样
- 分析:查看死锁线程的锁等待关系,找到互相等待的锁和线程
- 锁竞争问题:
- 特征:大量线程处于BLOCKED状态,都在等待同一个锁
- 分析:找到持有锁的线程,查看其堆栈,分析锁持有时间过长的原因
- 死循环问题:
- 特征:单个线程长时间处于RUNNABLE状态,CPU使用率很高
- 分析:查看该线程的调用堆栈,找到死循环的代码位置
- 资源等待问题:
- 特征:大量线程处于WAITING或TIMED_WAITING状态
- 分析:查看线程等待的资源类型(数据库连接、线程池、网络IO等)
卡片12
问题:原子类的实现原理是什么?CAS有什么缺点?
标准答案:
- 实现原理:基于CAS(Compare And Swap)操作实现无锁并发
- CAS包含三个操作数:内存位置V、预期值A、新值B
- 当且仅当V的值等于A时,将V的值更新为B,否则什么都不做
- 整个操作是原子的,由CPU指令保证
- CAS的缺点:
- ABA问题:变量值从A变成B,又变回A,CAS会误认为没有变化
- 循环时间长开销大:竞争激烈时,CAS会一直自旋,消耗CPU资源
- 只能保证一个变量的原子性:无法同时保证多个变量的原子操作
- ABA问题解决方案:使用AtomicStampedReference,通过版本号标记变量变化
⭐⭐⭐ 三星常考题(50%左右概率考察)
卡片13
问题:什么是happens-before原则?有哪些常见规则?
标准答案:
- 定义:JMM定义的一套规则,用于判断两个操作之间是否存在顺序关系,是判断数据是否存在竞争、线程是否安全的核心依据
- 常见规则:
- 程序次序规则:同一个线程内,前面的操作happens-before后面的操作
- 锁定规则:对一个锁的解锁操作happens-before后续对同一个锁的加锁操作
- volatile变量规则:对一个volatile变量的写操作happens-before后续对该变量的读操作
- 传递性规则:如果A happens-before B,B happens-before C,那么A happens-before C
- 线程启动规则:Thread.start()方法happens-before该线程的所有操作
- 线程终止规则:线程的所有操作happens-before其他线程检测到该线程终止
卡片14
问题:什么是锁升级?synchronized的锁升级过程是怎样的?
标准答案:
- 锁升级定义:JDK6为了优化synchronized的性能,引入了锁升级机制,锁会随着竞争程度的增加而逐渐升级
- 升级过程:
- 无锁状态:对象刚创建时,没有任何线程竞争
- 偏向锁:第一个线程访问对象时,对象头会记录该线程ID,以后该线程访问时无需加锁
- 轻量级锁:当有第二个线程竞争偏向锁时,偏向锁升级为轻量级锁,使用CAS操作获取锁
- 重量级锁:当轻量级锁自旋超过一定次数(默认10次)仍未获取到锁,升级为重量级锁,线程会被阻塞
注意:锁只能升级,不能降级
卡片15
问题:AQS(抽象队列同步器)的原理是什么?
标准答案:
- 定义:AQS是Java并发包中用于实现锁和同步器的基础框架
- 核心原理:
- 使用一个volatile int state变量表示同步状态
- 维护一个CLH双向队列,用于存放等待获取锁的线程
- 线程获取锁失败时,会被加入队列并阻塞
- 当锁被释放时,会唤醒队列中的头节点线程
- 实现方式:
- 独占模式:同一时间只能有一个线程获取锁(如ReentrantLock)
- 共享模式:同一时间可以有多个线程获取锁(如CountDownLatch、Semaphore)
- 常见实现类:ReentrantLock、ReentrantReadWriteLock、CountDownLatch、CyclicBarrier、Semaphore
卡片16
问题:为什么不推荐使用Executors创建线程池?
标准答案:
- Executors提供的默认线程池存在以下问题:
- FixedThreadPool和SingleThreadExecutor:使用无界的LinkedBlockingQueue,任务队列可以无限增长,可能导致OOM
- CachedThreadPool:最大线程数为Integer.MAX_VALUE,可能创建大量线程,导致OOM
- ScheduledThreadPool:最大线程数为Integer.MAX_VALUE,同样可能创建大量线程
- 推荐做法:使用ThreadPoolExecutor的构造方法手动创建线程池,根据业务场景合理设置核心参数,特别是工作队列的容量和拒绝策略
💡 面试加分题(考察深度与实战经验)
卡片17
问题:如何设计一个线程安全的单例模式?
标准答案:
- 饿汉式:类加载时初始化,线程安全,但可能造成资源浪费
public class Singleton { private static final Singleton INSTANCE = new Singleton(); private Singleton() { } public static Singleton getInstance() { return INSTANCE; } } - 双重检查锁定(DCL):懒加载,线程安全,性能高
public class Singleton { private static volatile Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } } - 静态内部类:懒加载,线程安全,实现简单(推荐)
public class Singleton { private Singleton() { } private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return SingletonHolder.INSTANCE; } } - 枚举:最简单,绝对线程安全,防止反射和序列化破坏
public enum Singleton { INSTANCE; public void doSomething() { } }
卡片18
问题:什么是伪共享?如何避免伪共享?
标准答案:
- 伪共享定义:CPU缓存以缓存行(通常64字节)为单位存储数据,当多个线程同时修改同一个缓存行中的不同变量时,会导致缓存行频繁失效,严重影响性能
- 避免方法:
- 缓存行填充:在变量前后添加7个long类型的变量,使每个变量独占一个缓存行
- 使用@Contended注解:JDK8及以上版本提供,自动为变量添加填充
- 数据结构优化:将经常被不同线程访问的变量分开存储
卡片19
问题:你在项目中遇到过哪些并发问题?如何解决的?
标准答案:(结合实际项目经验回答,以下为参考框架)
- 问题1:死锁问题
- 现象:系统某个功能突然无响应,CPU使用率低但负载高
- 排查:使用jstack获取线程dump,发现两个线程互相等待对方释放锁
- 原因:两个方法获取锁的顺序不一致
- 解决:统一锁的获取顺序,按数字编号排序
- 问题2:线程安全问题
- 现象:统计数据不准确,偶尔出现负数
- 排查:发现使用了普通的int变量进行计数,多线程同时修改
- 解决:使用AtomicInteger代替int变量
- 问题3:线程池耗尽问题
- 现象:系统响应越来越慢,最终无响应
- 排查:使用jstack发现大量线程处于WAITING状态,线程池队列已满
- 原因:线程池参数设置不合理,核心线程数太小,任务执行时间过长
- 解决:调整线程池参数,优化任务执行逻辑
卡片20
问题:生产环境遇到并发问题如何应急处理?
标准答案:
- 快速止损:
- 若问题严重影响业务,立即重启应用
- 若只是部分功能受影响,可先将流量切到备用节点
- 保留现场:
- 重启前务必获取线程dump、内存dump、GC日志和应用日志
- 记录系统当时的CPU、内存、IO等资源使用情况
- 问题定位:
- 分析线程dump,查看是否有死锁、锁竞争、死循环等问题
- 结合应用日志,找到问题发生的时间点和相关代码
- 临时解决:
- 调整线程池参数、增加数据库连接数等
- 临时关闭非核心功能,减轻系统压力
- 根本解决:
- 在测试环境复现问题,找到根本原因
- 修复代码,发布补丁版本
- 预防措施:
- 增加监控告警,及时发现并发问题
- 编写并发测试用例,提前发现潜在问题
📚 背诵建议
- 优先背诵五星必考题,这是面试中最常问的内容
- 对于对比类问题(如synchronized vs ReentrantLock),重点记住表格中的核心区别
- 对于原理类问题,先理解再背诵,不要死记硬背
- 加分题一定要结合自己的实际项目经验回答,这样才能给面试官留下深刻印象