首页
/
行业洞察
/
正文
INDUSTRY INSIGHT · 深度
后端面试并发基础高频考点:线程池、synchronized与volatile深度拆解
📅 2026/10/14 1:07:09
✍️ 爱科研究院
👁 阅读 3,247
1. 为什么“每日一面”这个系列值得单独拎出来聊做后端开发这些年面过别人也被别人面过慢慢发现一个规律并发基础这块是区分“背题选手”和“真写过代码的人”最狠的一道分水岭。你问一个候选人“线程和进程的区别”他能给你背出教科书答案但你接着问“那你项目里为什么用线程池而不是每次 new Thread”很多人就开始支支吾吾了。“每日一面”这个系列我关注挺久了它最大的价值不是给你一堆标准答案让你去背而是把面试官真正会追问的那些点给串起来了。尤其是并发基础这一块表面上看就那么几个概念——线程、锁、线程池、volatile、CAS但每一个往下挖三层都能挖出实际编码里踩过的坑。这篇东西我打算按自己的理解把并发基础面试里最高频的几个模块拆开讲。不是给你一份背诵手册而是把每个问题背后的“为什么”讲透让你在面试的时候能接住追问在工作中能避开雷区。适合谁看准备跳槽的后端、刚入行想补基础的、以及那些“会用但说不清”的开发者。我会尽量用大白话加实际场景来解释代码片段也会给可直接跑的版本。2. 并发基础的核心知识地图与面试考察逻辑2.1 面试官到底想考什么很多人以为并发面试就是考概念其实不是。面试官通过并发基础题主要在看三件事你知不知道边界在哪比如 synchronized 能保证可见性但它能不能保证有序性很多人答不上来。知道边界才知道什么时候该换工具。你有没有实际踩过坑死锁怎么排查、线程池队列满了会怎样、volatile 修饰数组为什么没用——这些问题没写过并发代码的人根本答不出来。你能不能做取舍CAS 好还是锁好没有标准答案看场景。面试官想听的是你的判断逻辑不是“CAS 是乐观锁所以更好”这种片汤话。所以准备并发面试正确的姿势不是背答案而是把每个知识点还原到“什么场景下用它、用了会有什么副作用”这个层面。2.2 高频考点分布我把近几年见过的并发基础题做了个粗略统计大致分布是这样的考点模块出现频率典型追问深度线程生命周期与状态转换高状态流转条件、WAITING和BLOCKED区别synchronized与锁升级极高锁消除、锁粗化、偏向锁撤销volatile关键字极高内存屏障、伪共享、不保证原子性CAS与原子类高ABA问题、自旋开销、LongAdder优化线程池核心参数极高队列选择、拒绝策略、参数计算AQS与ReentrantLock中高公平锁实现、条件队列并发容器中ConcurrentHashMap扩容、CopyOnWriteArrayList死锁与排查中jstack分析、避免策略这张表你可以当成复习优先级来用。频率高的不代表其他不用看而是说这些是面试官最常拿来开刀的切入点。2.3 一个容易被忽略的考察维度除了上面这些“硬知识”面试官还会通过并发题看你的编码习惯。举个例子我见过一个候选人问他线程池怎么用他张口就说“Executors.newFixedThreadPool”。这个回答本身没错但如果你补一句“不过生产环境我一般手动 new ThreadPoolExecutor因为 Executors 的默认队列是无界的任务堆积可能 OOM”面试官对你的评价立刻不一样。这就是并发面试的隐藏考点你有没有生产环境的意识。背题的人只知道 API写过代码的人知道 API 背后的坑。3. 线程与锁的核心细节拆解3.1 线程状态转换别只背那六种状态线程状态这块教科书会告诉你六种NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。但面试官真正爱问的是状态之间的转换条件尤其是 BLOCKED 和 WAITING 的区别。简单说BLOCKED是线程在等一把锁它想进 synchronized 块但锁被别人占了。WAITING是线程主动调用 wait()、join()、park() 后进入的等待它在等一个“通知”。这个区别为什么重要因为排查问题时如果你看到大量线程处于 BLOCKED说明锁竞争严重如果大量处于 WAITING可能是某个 notify 没发出来或者线程池空闲。用 jstack 抓线程 dump 的时候这两种状态的排查方向完全不同。我实际遇到过一个案例某服务响应变慢jstack 一看几十个线程卡在 BLOCKED都在等同一把锁。顺着堆栈找到代码发现是一个同步方法里做了远程调用把锁持有时间拉得特别长。这种问题你光背状态定义是发现不了的得知道 BLOCKED 意味着什么。提示jstack 输出里BLOCKED 线程会显示 “waiting to lock 0x...”而 WAITING 会显示 “waiting on 0x...”一个 to 一个 on含义完全不同。3.2 synchronized 的锁升级面试官最爱追问的深水区synchronized 这块现在面试基本不会只问“它和 Lock 的区别”了太浅。高频追问是锁升级过程无锁 → 偏向锁 → 轻量级锁 → 重量级锁。我用大白话解释一下这个升级逻辑。假设有个对象一开始没人抢它的锁那它就是个普通对象。第一个线程来拿锁JVM 就把锁标记成“偏向”这个线程相当于给这个线程发了个 VIP 卡下次它再来直接进不用检查。这就是偏向锁适合“同一个线程反复进同步块”的场景。但如果有第二个线程来抢VIP 卡就失效了锁升级成轻量级锁。轻量级锁用的是 CAS 自旋就是“我转几圈等你释放”。如果自旋能等到那就不用惊动操作系统开销小。如果自旋等不到或者自旋次数太多锁就升级成重量级锁这时候线程会被挂起进入操作系统的等待队列。挂起和唤醒涉及用户态到内核态的切换开销大但适合竞争激烈的场景。这个升级过程是不可逆的一旦升到重量级就不会降回去。所以面试官可能会问“为什么偏向锁要延迟开启”答案是 JVM 启动时有很多同步操作如果一上来就偏向撤销偏向锁的开销反而更大所以默认延迟几秒再启用。3.3 volatile 的三件事和一件它做不到的事volatile 这个关键字面试出现频率极高因为它“看起来简单实际很深”。它保证三件事可见性一个线程改了 volatile 变量其他线程立刻能看到。有序性禁止指令重排序插入内存屏障。单次读写的原子性注意是单次读写不是复合操作。它做不到的一件事不保证复合操作的原子性。最经典的例子就是i。i其实是三步读 i、加一、写回。volatile 只能保证每一步的读写是原子的但三步之间可能被其他线程插进来。public class VolatileDemo { private volatile int count 0; public void increment() { count; // 这行不是原子操作 } }上面这段代码多线程跑下来 count 一定小于预期值。要解决就得用AtomicInteger或者加锁。还有一个坑volatile 修饰数组只保证数组引用的可见性不保证数组元素的可见性。这个点很多人不知道面试官一问一个准。private volatile int[] arr new int[10]; // arr new int[20]; 这行对其他线程可见 // arr[0] 1; 这行不保证可见3.4 内存屏障volatile 的底层实现面试官如果问“volatile 怎么保证可见性的”你答“内存屏障”就对了但最好能说清楚是哪几种屏障。JVM 里主要有四种屏障LoadLoad读操作之间保证前面的读先完成。StoreStore写操作之间保证前面的写先刷出去。LoadStore读之后写保证读完了再写。StoreLoad写之后读开销最大volatile 写之后会插这个。volatile 写操作前面插 StoreStore后面插 StoreLoadvolatile 读操作前面插 LoadLoad后面插 LoadStore。这些屏障的作用是防止 CPU 和编译器乱序执行保证多线程下的执行顺序符合预期。这块内容面试不一定会问这么深但如果你能说出来面试官会觉得你是真研究过不是背的。4. 线程池与并发工具的实操要点4.1 线程池七个参数一个都不能少线程池是并发面试的重灾区因为它是实际项目里用得最多、也最容易出问题的地方。ThreadPoolExecutor的构造函数有七个参数我一个个说重点讲面试官会追问什么。public ThreadPoolExecutor( int corePoolSize, // 核心线程数 int maximumPoolSize, // 最大线程数 long keepAliveTime, // 空闲线程存活时间 TimeUnit unit, // 时间单位 BlockingQueueRunnable workQueue, // 工作队列 ThreadFactory threadFactory, // 线程工厂 RejectedExecutionHandler handler // 拒绝策略 )corePoolSize 和 maximumPoolSize核心线程默认不会被回收除非设置 allowCoreThreadTimeOut非核心线程空闲超过 keepAliveTime 就回收。面试官爱问“任务来了线程池怎么处理”标准流程是核心线程没满就建核心线程核心满了就进队列队列满了就建非核心线程非核心也满了就触发拒绝策略。workQueue这个参数坑最多。Executors.newFixedThreadPool用的是LinkedBlockingQueue默认容量是Integer.MAX_VALUE相当于无界队列。这意味着 maximumPoolSize 根本不会生效任务一直往队列里堆堆到 OOM。所以生产环境我强烈建议手动指定有界队列。拒绝策略四种内置策略——AbortPolicy抛异常、CallerRunsPolicy调用者线程执行、DiscardPolicy静默丢弃、DiscardOldestPolicy丢最老的。实际项目里我一般用自定义策略把被拒绝的任务记录下来方便排查。4.2 线程池参数怎么算面试官经常问“你项目里线程池参数怎么定的”这个问题没有标准答案但有一个通用的估算思路。如果是CPU 密集型任务线程数设为 CPU 核数加一。加一是因为万一有个线程偶尔阻塞多一个能顶上。可以用Runtime.getRuntime().availableProcessors()拿到核数。如果是IO 密集型任务线程数可以设大一些因为线程大部分时间在等 IO。一个经验公式是核数 * 2但更准确的做法是根据 IO 等待时间和 CPU 计算时间的比例来算线程数 核数 * (1 IO等待时间 / CPU计算时间)比如核数 8IO 等待是计算时间的 3 倍那线程数就是 8 * 4 32。但这些都是理论值实际还得压测。我一般会先按公式设一个初始值然后通过监控线程池的活跃线程数、队列大小来调整。如果队列经常满说明线程不够如果线程数一直上不去说明队列太大。4.3 线程池监控别等出事了才看线程池用起来容易监控起来很多人就忽略了。ThreadPoolExecutor提供了一些方法可以拿运行时状态getActiveCount()活跃线程数getQueue().size()队列积压任务数getCompletedTaskCount()已完成任务数getLargestPoolSize()历史最大线程数我习惯在项目里加一个定时任务每隔几秒把这些指标打到日志或者监控系统里。有一次线上服务变慢一看监控发现队列积压了几万个任务赶紧扩容才扛过去。如果没监控等用户反馈就晚了。注意getActiveCount()返回的是近似值不是精确值因为线程状态随时在变。用来做趋势判断没问题别拿它做精确控制。4.4 并发容器的选择并发容器这块面试常问的是ConcurrentHashMap和CopyOnWriteArrayList。ConcurrentHashMap在 JDK 8 之后改成了 CAS synchronized 的实现锁的粒度是单个桶链表头节点比 JDK 7 的分段锁更细。面试官可能会问“为什么 get 操作不需要加锁”因为 Node 的 val 和 next 都是 volatile 的保证了可见性。CopyOnWriteArrayList适合读多写少的场景。每次写操作都会复制整个数组所以写开销大但读完全无锁。如果你的场景是配置列表这种“几乎不改、频繁读”的用它很合适如果是频繁写千万别用复制数组能把内存吃光。5. 常见问题与排查技巧实录5.1 死锁怎么排查死锁是并发面试的经典问题面试官爱问“你怎么排查死锁”。标准答案是用 jstack 抓线程 dump它会自动检测死锁并打印出来。jstack pid thread_dump.txt在输出里搜 “Found one Java-level deadlock”下面会列出互相等待的线程和它们持有的锁。看到这个基本就能定位到代码了。但实际排查中死锁不一定这么明显。有时候是“活锁”或者“线程饥饿”jstack 看不出来。这时候得结合业务日志看哪些操作卡住了再反推锁的持有关系。避免死锁有几个实用原则按固定顺序加锁、设置锁超时用tryLock(timeout)、减少锁的嵌套。我见过一个案例两个方法分别按不同顺序获取两把锁单测没问题一上并发就死锁。后来统一了加锁顺序就好了。5.2 volatile 失效的几种情况前面说了 volatile 不保证复合操作原子性实际编码里还有几个容易踩的坑volatile 修饰对象只保证引用可见不保证对象内部字段可见。要保证内部字段得把字段也声明成 volatile 或者用其他同步手段。volatile 不能替代锁。有人觉得用了 volatile 就不用 synchronized 了这是错的。volatile 只管可见性和有序性不管互斥。volatile 数组元素问题前面提过了再强调一遍。5.3 线程池常见异常排查表现象可能原因排查方向任务不执行队列满了触发拒绝策略看拒绝策略类型、队列容量服务变慢队列积压、线程不够监控 activeCount 和 queue.sizeOOM无界队列堆积任务换有界队列、加拒绝策略线程泄漏线程池没 shutdown检查是否调用了 shutdown死锁任务之间互相等待jstack 抓 dump 分析这张表可以贴在工位上出问题的时候按图索骥。5.4 一个真实的排查案例之前有个服务每隔一段时间就卡顿几秒。查了 GC 没问题查了数据库也正常。最后用 jstack 抓了几次 dump发现卡顿的时候总有大量线程处于 BLOCKED等的是同一把锁。顺着堆栈找到代码是一个日志工具类里面用 synchronized 包了一个写文件的操作。并发高的时候所有线程都排队等这把锁写文件又慢就卡住了。后来换成异步日志问题解决。这个案例说明什么并发问题不一定出在你以为的“并发代码”里有时候一个不起眼的同步方法就是瓶颈。排查的时候要全面看别只盯着线程池和锁。6. 面试答题的一些个人心得6.1 别背答案讲场景面试官问“synchronized 和 ReentrantLock 的区别”如果你只背“一个是关键字一个是类、一个自动释放一个手动释放”那只能算及格。更好的答法是“我一般优先用 synchronized因为 JVM 会优化代码也简洁。但如果需要可中断锁、超时锁或者公平锁就用 ReentrantLock。比如我之前做过一个任务调度需要 tryLock 超时避免死等就选了 ReentrantLock。”这样答面试官知道你不仅知道区别还知道什么时候用哪个。6.2 主动暴露你知道的坑面试的时候如果问到线程池你可以主动补一句“不过我用的时候一般不用 Executors 工厂方法因为 newFixedThreadPool 的队列是无界的任务多了会 OOM我一般手动 new ThreadPoolExecutor 指定有界队列。”这句话一出来面试官对你的印象分立刻上去。主动说坑说明你真写过、真踩过不是背的。6.3 不会的题怎么答并发这块知识点太多不可能全都会。遇到不会的别硬编。可以说“这块我了解得不够深但我知道它大概和 XX 有关我回去会补一下。”诚实比瞎答好面试官更看重你的学习态度和知识边界。6.4 准备建议如果你时间有限我建议按这个顺序复习线程池 → synchronized → volatile → CAS → 并发容器 → AQS。前四个是必考后两个是加分项。每个知识点都要能说出“是什么、为什么、怎么用、有什么坑”这四层。另外强烈建议自己写几个并发的小 demo 跑一跑。比如写个程序复现死锁然后用 jstack 去排查写个 volatile 不生效的例子看看结果。跑过一遍比看十遍书都管用。最后分享一个我自己的习惯每次面试完把没答好的问题记下来回去查清楚下次就不会再卡。面试本身就是最好的复习别浪费每一次被问倒的机会。
📌 标签:
工业官网
设计趋势
AI 建站
SEO
获取完整报告 →
RELATED ARTICLES
推荐阅读
2026/10/14 1:07:09
深入解析Java volatile:内存可见性、指令重排序与实战避坑指南
2026/10/14 1:07:09
OpenCV图像处理基础:从环境搭建到轮廓检测实战
2026/10/14 1:07:09
POJ2240 Arbitrage:Bellman-Ford判正环与套汇算法详解
2026/10/14 2:17:13
从面试官视角拆解:如何打造一个具备工程感的个人项目网站
2026/10/14 2:17:13
网络基础入门:IPv4、IPv6、私网公网、MAC、以太网帧与 DNS 学习笔记
2026/10/14 2:17:13
【数据可视化-149】湖北省民营企业100强分布可视化分析
2026/10/14 2:17:13
【数据可视化-150】浙江省百强企业分布可视化分析:基于Pyecharts的多维度数据洞察
2026/10/14 2:17:13
【数据可视化-151】福建省民营企业100强分布可视化大屏
2026/10/14 2:12:13
DeepMind OGB-LSC 参赛实现解析:PCQM4M-LSC 与 MAG240M-LSC 的深层 GNN 实战指南
2026/10/14 0:02:01
Oracle 19c Windows客户端安装配置与连接排错指南
2026/10/14 0:02:01
React Native截图优化:解决Android兼容性问题
2026/10/14 0:02:01
金属锈蚀速率计算模型与防护方案优化实践
2026/10/13 12:25:09
流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南
2026/10/13 18:15:04
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别
2026/10/13 8:14:22
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容
2026/10/12 19:53:23
我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
2026/10/11 21:41:11
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026/10/11 23:43:10
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)