ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java并发编程面试123题:从JMM到线程池核心考点全解析

Java并发编程面试123题:从JMM到线程池核心考点全解析 每年到了金三银四这个节点Java后端岗位的面试准备就成了圈子里最热闹的话题。我整理了近两年大厂和中小型公司的Java并发编程面试题一共123道覆盖了从基础语法到JUC源码、从原子操作到线程池调优的完整链路。说实话并发编程这一块在Java面试里的地位太特殊了——它既是初中级开发的分水岭也是高级工程师必须迈过去的坎。很多人在简历上写熟悉多线程结果一问到AQS、锁升级、线程池拒绝策略就卡壳非常可惜。这份题单不是我凭空凑出来的而是结合了2022年实际的面试反馈、各大技术社区的高频讨论以及我这些年参与技术面试时真正问过的问题。它不追求偏题怪题而是把所有可能出现的基础题、原理题、场景题、手写代码题按照知识体系重新编排。下面我把123道题的分类逻辑、每个模块的核心考点和应对思路拆开讲清楚并且把最容易踩坑的部分单独拎出来说希望能帮准备面试的朋友少走弯路。1. 并发编程面试到底在考什么123道题的分类逻辑1.1 从面试官视角拆解考察点先说一个很多候选人没想明白的问题面试官考并发编程到底想考什么我在面试别人的时候通常会沿着三条线去追问第一Java内存模型JMM和线程安全的基础认知第二synchronized、ReentrantLock、CAS、AQS这些同步机制的底层原理第三面对真实并发场景时的设计能力和问题排查能力。这三条线恰好对应了123道题里的基础题、源码题和场景题。很多候选人把大量时间花在背八股文上比如synchronized和ReentrantLock的区别能一口气说出七八条。但当我追问synchronized在JDK 1.6之后做了哪些优化锁升级的触发条件是什么时就哑火了。这说明他只是在背结论没有真正理解锁的实现逻辑。面试官其实很清楚工作中不一定天天写并发代码但底层原理决定了你遇到线上问题时能不能快速定位这才是考察的初衷。1.2 123道题的板块分布与复习优先级我按照考察频率和难度把123道题划分成了七个板块这份题单的核心价值就在于这份分类。基础概念题线程状态、start与run、sleep与wait等约20道JMM与可见性相关约15道synchronized与锁机制约18道CAS与原子类约15道AQS与JUC工具类约25道线程池约15道最后的场景设计与综合题约15道。如果你准备时间紧张我的建议是先吃透synchronized与锁机制、线程池这两个板块因为它们在面试中出现频率最高而且和工作中的性能排查直接相关。JMM与可见性则是理解一切并发问题的基石不能跳过。AQS那25道题是最能拉开差距的部分想冲高级岗位的必须拿下如果目标是初中级岗位至少要把ReentrantLock的公平与非公平实现讲清楚。2. 核心知识点深度解析从八股到源码2.1 synchronized与锁升级面试第一题123道题里关于synchronized的题目数量和提问深度都排在前列。基础一点的会问synchronized修饰静态方法和实例方法的区别进阶一点的会问JDK 1.6之后synchronized做了哪些优化而真正拉开差距的是锁升级的完整过程是什么偏向锁一定比轻量级锁高效吗。这里我先解释锁升级的完整链路因为它特别容易被讲错。Java对象在JVM中由对象头、实例数据和对齐填充组成对象头中的Mark Word记录了锁状态。无锁状态下Mark Word存储的是对象的hashCode和分代年龄。当一个线程第一次访问同步块时JVM会将锁升级为偏向锁Mark Word中记录持有锁的线程ID此时不需要任何CAS操作效率最高。但如果有另一个线程竞争偏向锁会被撤销升级为轻量级锁也就是我们常说的自旋锁通过CAS来获取锁。注意很多人以为锁升级是偏向锁→轻量级锁→重量级锁单向升级、不可降级这个理解基本是对的但要补充一个细节——偏向锁在JDK 15开始被默认禁用JDK 17中已经被标记为废弃。面试时主动提到这个演进趋势会显得你对版本变化足够敏感。至于自旋锁它的核心思想是与其立刻阻塞线程不如让线程在原地空转一会儿。默认自旋次数是10次超过这个次数或者自旋等待的线程数超过CPU核数的一半就会升级为重量级锁进入操作系统的内核态阻塞。这块还有个高频追问自旋会占用CPU为什么还要用它标准答案是重量级锁涉及用户态和内核态的切换开销远大于自旋消耗的CPU时间片在临界区执行时间短的场景下自旋是划算的。2.2 volatile与JMM内存可见性的真相volatile是面试中出镜率极高的关键字123道题里至少有三个经典问题volatile能保证原子性吗、volatile和synchronized的区别、volatile的实现原理。先说结论volatile保证可见性和有序性但不保证原子性。很多候选人能答出这句话但解释不清楚为什么不保证原子性。我用一个生活化类比来辅助理解volatile相当于在多个线程之间建立了一个公告板某个线程修改了变量后立即写到主内存并通知其他线程重新读取但是先读后写这种复合操作比如count本身就是三件事volatile管不了中间的竞争。所以典型答案是volatile适合做状态标记位不适合做计数器。关于可见性的底层实现面试官希望听到内存屏障这四个字。JMM规定了八种原子操作lock、unlock、read、load、use、assign、store、write而volatile变量的读写会在指令序列中插入内存屏障防止重排序。具体来说volatile写操作前插入StoreStore屏障写操作后插入StoreLoad屏障volatile读操作后插入LoadLoad和LoadStore屏障。面试的时候描述到这个粒度就足够证明你真的看过相关资料了。2.3 CAS与原子类无锁并发的取舍CASCompare And Swap是整个java.util.concurrent包的地基。123道题里关于它的问题集中在三件事CAS的原理、CAS的ABA问题、CAS和synchronized怎么选。CAS是无锁算法核心是一个原子操作比较内存中的值是否等于预期值如果相等就更新为新值否则不操作。Java中Unsafe类提供了compareAndSwapInt等本地方法直接调用CPU的cmpxchg指令整个过程是硬件级别的原子操作。Java 8之后更新的AtomicInteger等原子类内部就是用CAS自旋实现的。ABA问题是CAS绕不开的坑经典的例子是线程1读取到内存值A随后线程2把A改成B又改回A此时线程1再次CAS时发现值还是A就认为没人动过但实际上数据已经被改过两轮了。解决方案是使用AtomicStampedReference它除了维护对象值还维护了一个版本号每次修改都会带一个时间戳从根源上杜绝ABA。我在讲这个问题时习惯先让对方描述ABA问题再问你在实际项目中遇到过ABA问题吗大部分人说没遇到过这很正常但能说出用版本号解决就说明真正理解了。2.4 AQS与ReentrantLock走向源码深水区AQSAbstractQueuedSynchronizer是JUC最核心的类ReentrantLock、Semaphore、CountDownLatch全部构建在它之上。123道题里AQS相关的问题我建议按是什么、怎么用、核心流程、源码细节四层来准备。先明确AQS是什么它是一个基于FIFO等待队列的同步框架核心是volatile int state变量和CLH队列。state的含义由子类定义比如ReentrantLock中state表示锁的重入次数Semaphore中表示剩余许可证数量。获取资源时通过CAS修改state修改成功就持有资源失败则把线程封装成Node节点挂入队列尾部并阻塞线程。关于ReentrantLock面试必问的就是公平锁和非公平锁的区别。源码层面看非公平锁在lock()时先进行一次CAS尝试抢锁这就是插队抢不到才进入acquire流程公平锁则直接检查队列中是否有前驱节点有就乖乖排队。这个差异导致了非公平锁的吞吐量通常更高因为减少了线程挂起和唤醒的开销但可能造成线程饥饿。回答这个问题时如果能顺带提一句AQS中tryAcquire和acquireQueued两个方法配合完成加锁流程面试官就会认为你真的读过源码。2.5 线程池七个参数背下来还要会用线程池是123道题里最贴近实际工作的板块。基础题就会要求列举ThreadPoolExecutor的七个参数corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler。但面试官绝不会只让你背参数他会接着问当核心线程满了新任务先进队列还是先开新线程标准答案是先尝试放入工作队列队列满了之后才会创建新线程直到达到maximumPoolSize如果最大线程数也满了就执行拒绝策略。这个顺序很多人都能背下来但有一个容易忽略的细节线程池创建时并不会立即创建核心线程而是等任务提交后按需创建。除非调用prestartAllCoreThreads()方法。拒绝策略也是高频考点四种内置策略要能说出来AbortPolicy直接抛异常、CallerRunsPolicy调用者线程执行、DiscardPolicy默默丢弃、DiscardOldestPolicy丢弃队列中最老的任务。我建议实际项目中优先使用CallerRunsPolicy因为它有一个容易被忽略的背压效果——当任务过多时由提交任务的线程自己去执行等于减慢了任务提交速度反而能保护系统不被压垮。面试中主动说出这个实战理解比单纯背四种策略亮眼得多。2.6 ThreadLocal的内存泄漏坑ThreadLocal在面试中的出现率极高而且几乎必问内存泄漏问题。这个问题有标准答案ThreadLocalMap中的Entry继承了WeakReferencekey是弱引用value是强引用。当ThreadLocal对象没有外部强引用时key会被GC回收但value仍然被强引用引用着只有等线程销毁时value才会被回收。如果线程是线程池里的长生命周期线程这个value就永远无法回收造成内存泄漏。回答完标准答案后最好能补上实际项目中的处理方式在线程使用完ThreadLocal后调用remove()方法清除Entry。我在自己的代码里始终遵循一个原则——ThreadLocal的生命周期和业务操作绑定用try-finally包裹finally里调用remove不给内存泄漏留机会。这里还有一个容易翻车的点子线程能不能读取父线程的ThreadLocal答案是不能。ThreadLocal是线程私有的但InheritableThreadLocal可以让子线程继承父线程的值而TransmittableThreadLocal阿里开源则解决了线程池中值传递的问题。能主动说出这三个类的区别和适用场景属于加分项。2.7 ConcurrentHashMap的演化ConcurrentHashMap也是必考的集合类并发题。面试的经典问题是ConcurrentHashMap在JDK 1.7和JDK 1.8之间有什么变化为什么JDK 1.7版本的ConcurrentHashMap使用Segment分段锁默认16个Segment每个Segment继承ReentrantLock锁粒度是Segment级别。JDK 1.8之后抛弃了分段锁改用CAS synchronized保证并发安全锁粒度细化到桶table数组的每个槽位的级别。put操作时如果桶为空就直接CAS插入如果不为空就用synchronized锁住这个桶的首节点再进行插入或链表转红黑树的操作。这个设计的妙处在于synchronized在JDK 1.6之后引入了锁升级机制性能并不输给ReentrantLock而且锁粒度更细并发度更高。还有一个追问出现的概率很高size()方法如何统计元素个数1.8版本的ConcurrentHashMap使用baseCount和CounterCell数组来分散计数并发更新时先CAS更新baseCount如果CAS失败就在CounterCell数组中随机选一个槽位进行计数这样避免了单点竞争。final size()会先尝试不加锁地累加baseCount和所有CounterCell的值如果两次判断期间modCount发生变化说明有并发修改才会加锁重算。能在面试中说出这个细节说明你对高并发性能优化是有真实感觉的。3. 高频场景题与手写代码题3.1 三个线程交替打印的N种写法123道题里手写代码题占比不低其中三个线程交替打印ABC绝对是最经典的一道它考察的是线程协作的综合能力。这道题最简单的解法是synchronized wait/notify核心思路是三个线程共享一个state变量每个线程循环检查state是否等于自己的编号不等于就wait()等于就打印并更新state然后notifyAll()唤醒所有线程。public class PrintABC { private final Object lock new Object(); private int state 0; public void print(int target, String name) { for (int i 0; i 10; i) { synchronized (lock) { while (state % 3 ! target) { try { lock.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } System.out.print(name); state; lock.notifyAll(); } } } public static void main(String[] args) { PrintABC p new PrintABC(); new Thread(() - p.print(0, A)).start(); new Thread(() - p.print(1, B)).start(); new Thread(() - p.print(2, C)).start(); } }注意这里用while而不是if判断条件这是wait/notify的标准操作因为线程可能被虚假唤醒。这道题的进阶写法有Lock Condition每个线程持有一个条件变量精确唤醒、Semaphore三个信号量循环控制、CompletableFuture链式编排。面试时推荐先写最简单、最稳的synchronized版本然后主动说我还可以用Condition改写展示你的知识广度。3.2 手写生产者消费者生产者消费者模型考察的是阻塞队列的使用和数据同步能力。最推荐的写法是使用ArrayBlockingQueue因为它天然支持线程安全加上take()和put()方法自带阻塞效果代码量最少、出错概率最低。public class ProducerConsumer { private final BlockingQueueInteger queue new ArrayBlockingQueue(10); private final AtomicInteger count new AtomicInteger(0); public void produce() { for (int i 0; i 100; i) { try { int val count.incrementAndGet(); queue.put(val); System.out.println(生产: val); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } public void consume() { while (true) { try { Integer val queue.take(); System.out.println(消费: val); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } }面试官通常会再追问两个问题如果不用BlockingQueue你怎么自己实现ArrayBlockingQueue和LinkedBlockingQueue有什么区别第一个问题考察wait/notify的功底第二个问题需要答出ArrayBlockingQueue是有界数组容量固定LinkedBlockingQueue是链表默认容量是Integer.MAX_VALUE容易造成OOM所以生产环境一定要指定容量。3.3 死锁排查与避免这道题几乎每次面试都会出现写一段死锁代码并说明如何排查。写死锁代码很容易两个线程各自持有一把锁再去获取对方的锁即可。难的是排查。我给出的标准答案是三步走第一步用jps找到Java进程的PID第二步用jstack PID导出线程快照第三步在快照中搜索deadlock关键字定位到具体行号。注意jstack找到死锁后解决思路比修复代码更重要。要分清为什么会产生死锁——是锁的获取顺序不一致还是锁的持有时间过长。日常开发中我强烈建议遵循一个原则使用显式锁时多把锁的获取顺序必须全局一致能用tryLock加上超时时间的绝不用不带超时的lock()。4. 面试现场实战与答题话术4.1 一道连环追问是怎么展开的我用一个真实的面试对话来展示面试官是怎么从一道题展开到整个并发体系的。面试官问你了解线程池吗候选人回答了解然后面试官开始连环追问第一问线程池的核心参数有哪些——考察背功必须流利答出七个参数。第二问假设corePoolSize5、maximumPoolSize10、队列容量100现在同时来了200个任务会发生什么——考察参数组合的理解。正确的反应是先创建5个核心线程处理前5个任务剩下195个任务中100个进队列队列满后继续创建5个新线程达到最大值10剩下90个任务触发拒绝策略。第三问其中有一个任务执行时间很长其他任务很快会不会有问题——考察线程池的局限性和调优经验。这里可以展开corePoolSize设太小会导致积压设太大可能造成频繁线程切换需要结合任务类型CPU密集型还是IO密集型来确定合理值。第四问如果你发现某个接口的RT突然升高怎么确认是不是线程池的问题——考察线上排查能力。从监控指标入手活跃线程数、队列大小、拒绝次数、任务耗时如果队列持续积压、拒绝次数增加说明线程池容量不足需要扩容或调整拒绝策略。这样一环扣一环地问下来候选人有没有真实项目经验、理解深度够不够一下就清楚了。准备面试的时候不能只背一道题的答案要站在面试官角度想想他会顺着哪个方向追问。4.2 简历上写熟悉并发编程要承担的后果必须坦白说一件事并发编程是Java面试中翻车率最高的领域原因就是很多候选人把会用和熟悉混为一谈。如果简历上写了熟悉并发编程面试官默认你具备以下能力能解释JMM和happens-before原则能说清synchronized的锁升级过程和volatile的内存屏障能读懂AQS源码讲清加锁和释放的流程能根据业务场景选择合适的并发工具类具备基本的线程池调优和死锁排查经验。这五条如果有一条做不到最好不要写熟悉写了解并发编程基础或者掌握JUC常用工具类会更稳妥。简历上的每一个词都会被追问写上去的内容就是给自己画的一个圈面试官只会在圈里问你绝不会超出这个圈——但如果你把圈画太大自己又填不满反而会暴露短板。5. 复习路线与避坑指南5.1 时间不足时的优先级排序如果你距离面试只有两周我建议按以下顺序分配时间第一优先级是线程状态与synchronized、volatile、ThreadLocal、线程池这五个点几乎每场面试必考投入产出比最高第二优先级是AQS家族ReentrantLock、CountDownLatch、Semaphore、CyclicBarrier和ConcurrentHashMap这部分能拉开差距值得投入三到四天第三优先级是并发工具类的源码细节比如ForkJoinPool的work-stealing机制、CompletableFuture的异步编排这些属于加分项有时间再深入。一个很实用的备考方法把123道题按看一眼就能答出和需要写代码验证分成两堆前者靠背诵和复述后者必须动手写一遍示例代码。很多人栽在手写题上不是因为不会写而是因为长时间没写过手生了。5.2 我踩过的复习弯路第一次系统准备并发编程面试时我犯过一个典型的错误花大量时间啃AQS源码的每一个细节结果到了面试现场被一个简单的volatile为什么不能保证原子性问懵了。后来我意识到面试考题的难度分布是金字塔形的基础题占大头源码题只是塔尖。把地基打牢远比强行拔高更重要。另一个教训是关于背答案这件事。我不反对背但反对纯背——你必须能用自己的话把一道题讲明白。检验方法很简单找一个不懂Java的朋友试着让他听懂你在说什么。如果你发现自己不断蹦出专业术语却解释不清楚说明你还没有真正理解。把每一道题都用自己的语言重新组织一遍这个过程本身就是最好的复习。5.3 从背题到理解的最后一步最后说一个我很推崇的复习收尾动作把123道题里的原理类问题一一对应到JDK源码中去找证据。比如回答ConcurrentHashMap为什么高效不要只背CAS synchronized 红黑树去打开源码看看putVal()的真实实现回答线程池的核心线程怎么做到不销毁去看看getTask()方法里keepAliveTime的用法。源码可能读不完但只要在读代码的过程中弄懂过几个关键方法的执行流程你的面试表达就会明显更有底气因为每一句话背后都有真实的代码支撑。这套123道题的价值就在于此——它不只是用来背的更是一个搭建知识体系的索引。把每个问题当成一个钩子勾出背后的原理、源码、场景和坑面试的时候自然就能做到从容应对。预祝看到这里的你顺利拿下心仪的offer。
返回列表