
1. 并发编程面试的核心知识框架1.1 为什么并发编程是面试的必考区干了这么多年后端开发带过不少团队也面过很多人我发现一个规律并发编程这块会的人是真会不会的人是真不会几乎没有中间地带。2020年前后那段时间互联网公司的面试对并发编程的考察密度达到了一个高峰原因其实不复杂——业务量上来了单机扛不住分布式和并发就成了绕不开的坎。你去看任何一份后端岗位的面试评价表并发编程几乎都排在考察项的前三位。它不像某些框架API用的时候查文档就行并发编程考察的是你对线程模型、内存模型、锁机制这些底层原理的理解这些理解不到位线上出了问题你连日志都看不懂。这篇文章我打算把当年整理的那套并发编程面试题重新梳理一遍不是简单地列题目给答案而是把每道题背后的知识脉络、常见追问、以及面试官真正想听到的东西讲清楚。适合准备面试的初中级开发者也适合想系统补一下并发基础的老手。1.2 知识体系的四个层次我把并发编程的知识点分成四层面试基本也是按这个顺序往下问的第一层基础概念。进程与线程的区别、线程的生命周期、创建线程的方式。这层是筛人的答不上来基本就结束了。第二层同步与锁。synchronized、volatile、Lock接口、CAS、AQS。这层是核心区分区能答清楚的人不多。第三层线程池与并发工具。ThreadPoolExecutor参数、阻塞队列、CountDownLatch、CyclicBarrier、Semaphore。这层考察工程实践能力。第四层内存模型与底层原理。JMM、happens-before、指令重排、伪共享。这层是加分项答得好能直接拉高评级。下面我按这个层次逐层拆解每道题都会补充面试官可能的追问方向和容易踩的坑。2. 基础概念层的必考题与深挖2.1 进程与线程到底差在哪这题几乎每场面试都会问但很多人答得太浅只说进程是资源分配的单位线程是调度的单位然后就没词了。面试官接着问一句那协程呢就卡住了。我的答法是分三个维度去讲资源维度进程有独立的地址空间线程共享进程的地址空间。这意味着线程间通信成本低但一个线程崩了可能拖垮整个进程。切换维度线程切换比进程切换轻量因为不需要切换页表、刷新TLB。但线程切换也不是免费的一般一次上下文切换在微秒级别高并发场景下切换开销能占到CPU的相当比例。通信维度进程间通信要走管道、消息队列、共享内存、信号量这些机制线程间直接读写共享变量就行但正因为太容易才引出了后面所有的同步问题。注意面试时如果被追问协程和线程的区别核心答法是——协程是用户态的轻量级线程切换不经过内核由程序自己调度所以切换成本极低适合IO密集型场景。2.2 线程的生命周期与状态转换Java里线程有六种状态NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。这题的关键不是背状态名而是说清楚状态之间怎么转换。我一般会画一条主线来讲新建线程是NEW调用start()后进入RUNNABLE。注意RUNNABLE在Java里包含了操作系统层面的就绪和运行两种状态这是很多人搞混的地方。当线程去抢synchronized锁失败进入BLOCKED调用wait()、join()、park()进入WAITING带超时参数的版本进入TIMED_WAITING。这里有个高频追问BLOCKED和WAITING有什么区别答案是BLOCKED特指等待监视器锁而WAITING是主动等待被唤醒两者触发条件和解锁路径完全不同。2.3 创建线程的方式与选择教科书上说有三种继承Thread、实现Runnable、实现Callable配合FutureTask。但实际工程里几乎没人直接new Thread都是用线程池。面试时如果只答这三种面试官会觉得你停留在课本阶段。我会补一句实际项目中通过ThreadPoolExecutor或者Executors工厂创建线程池来管理线程因为线程的创建和销毁成本高复用才是正道。这样答就把话题引到了线程池主动权就到你手里了。3. 同步与锁机制的高频考点3.1 synchronized的底层原理这是并发面试的绝对核心答不好基本就凉了。synchronized的底层实现分三个阶段说修饰代码块编译后生成monitorenter和monitorexit指令依赖对象头的Mark Word和Monitor对象。修饰方法不是靠字节码指令而是靠方法访问标志位ACC_SYNCHRONIZEDJVM在方法调用时隐式加锁。锁升级过程无锁 → 偏向锁 → 轻量级锁 → 重量级锁。这个升级过程是很多人答不全的。偏向锁针对的是同一个线程反复进入同步块的场景在Mark Word里记录线程ID一旦有第二个线程来竞争升级为轻量级锁用CAS自旋抢锁自旋失败到一定次数升级为重量级锁线程挂起进入内核态等待。实操心得面试时如果能把锁升级讲清楚并且说明偏向锁在JDK 15之后默认关闭了面试官会明显对你高看一眼因为这说明你关注版本演进不是死记硬背。3.2 volatile的两个语义volatile有两个作用保证可见性和禁止指令重排但不保证原子性。这三点必须一次说全少一个都会被追问。可见性的实现靠的是内存屏障。写volatile变量时会在写操作后插入StoreLoad屏障强制把写缓冲区的数据刷回主内存读volatile变量时会在读操作前插入LoadLoad屏障强制从主内存重新读取。禁止重排靠的是四种内存屏障的排列组合这个在JMM层面有严格定义。经典的应用场景就是双重检查锁定的单例模式instance变量必须加volatile否则可能出现对象引用已经赋值但构造方法还没执行完的情况另一个线程拿到一个半成品对象。3.3 CAS与ABA问题CAS是Compare And Swap靠CPU的cmpxchg指令实现是乐观锁的典型代表。它有三个操作数内存地址、预期值、新值。只有内存地址的当前值等于预期值才更新为新值。CAS的三大问题必须张口就来ABA问题值从A变成B又变回ACAS检查不出来。解决办法是加版本号Java里用AtomicStampedReference。自旋开销CAS失败会一直重试高竞争下CPU空转严重。解决办法是自适应自旋或者直接上锁。只能保证单个变量多个变量要原子更新得用AtomicReference把多个变量封装成一个对象。3.4 AQS的设计思想AQS全称AbstractQueuedSynchronizer是ReentrantLock、Semaphore、CountDownLatch这些工具的底层基石。它的核心就三样东西一个volatile的state变量、一个CLH双向队列、一套模板方法。state表示同步状态具体含义由子类定义。比如ReentrantLock里state表示重入次数Semaphore里表示可用许可数。抢不到锁的线程被包装成Node节点加入CLH队列然后park挂起。释放锁时从队列头部唤醒后继节点。面试时如果被问ReentrantLock和synchronized的区别从这几个维度答实现层面JVM vs JDK、锁的灵活性手动释放、可中断、可超时、公平/非公平可选、性能现在两者差距不大早期synchronized更重。4. 线程池与并发工具的实战考察4.1 ThreadPoolExecutor七大参数这题是工程能力的试金石七个参数必须一个不落地讲清楚参数含义踩坑点corePoolSize核心线程数即使空闲也不回收除非开allowCoreThreadTimeOutmaximumPoolSize最大线程数队列满了才会用到keepAliveTime空闲存活时间针对非核心线程unit时间单位别填错workQueue工作队列队列选错会导致OOM或任务丢失threadFactory线程工厂一定要自定义方便排查问题handler拒绝策略默认AbortPolicy会抛异常执行流程是必考核心线程没满就创建核心线程核心满了就入队队列满了就创建非核心线程直到maximum再满就触发拒绝策略。注意很多人答不对队列满了才创建非核心线程这个顺序容易说成核心满了就创建非核心这是典型错误。4.2 四种拒绝策略怎么选AbortPolicy直接抛RejectedExecutionException默认策略。适合不能丢任务的场景让上游感知到。CallerRunsPolicy调用者线程自己执行。适合不能丢任务但也不想抛异常的场景能起到反压作用。DiscardPolicy静默丢弃。适合允许丢任务的场景但线上慎用出问题很难查。DiscardOldestPolicy丢掉队列最老的任务再提交。适合任务有时效性的场景。实际项目里我一般用自定义拒绝策略把被拒绝的任务信息打到日志或者落库方便事后补偿。4.3 线程池参数怎么定这是最考验经验的问题没有标准答案但有一套方法论CPU密集型线程数设为CPU核数1因为计算任务切换开销大多了反而慢。IO密集型线程数设为CPU核数 × (1 平均等待时间/平均计算时间)。比如等待时间是计算时间的9倍那线程数就是核数的10倍。但公式只是起点最终要靠压测。我会先用公式估一个初值然后压测观察CPU利用率、队列积压、响应时间再微调。线上还要配监控队列长度和活跃线程数是必须盯的指标。4.4 CountDownLatch、CyclicBarrier、Semaphore的区别这三个工具经常被放在一起问我用一个表格对比工具作用是否可重用典型场景CountDownLatch一个线程等多个线程否主线程等所有子任务完成CyclicBarrier多个线程互相等是分阶段任务每阶段同步Semaphore控制并发数量是限流、资源池CountDownLatch的count减到0就不可重置CyclicBarrier可以循环使用这是最本质的区别。5. 内存模型与底层原理的加分项5.1 JMM与happens-before规则JMM是Java内存模型它定义了一套多线程读写共享变量的可见性规则。核心是happens-before原则一共八条面试常考的有程序顺序规则一个线程内前面的操作happens-before后面的操作。监视器锁规则解锁happens-before后续的加锁。volatile变量规则volatile写happens-before后续的volatile读。传递性A happens-before BB happens-before C则A happens-before C。happens-before不是时间上的先后而是可见性的保证。理解这一点很多看似矛盾的现象就解释得通了。5.2 指令重排与内存屏障指令重排分三种编译器重排、CPU重排、内存系统重排。重排的目的是提高指令级并行度但会破坏多线程的可见性。内存屏障是禁止重排的手段JMM定义了四种LoadLoad禁止读读重排StoreStore禁止写写重排LoadStore禁止读写重排StoreLoad禁止写读重排开销最大volatile写会在前后插入StoreStore和StoreLoad屏障volatile读会在前后插入LoadLoad和LoadStore屏障。5.3 伪共享与缓存行这是进阶考点。CPU缓存以缓存行为单位一般是64字节。如果两个变量在同一个缓存行里被不同CPU核心频繁修改就会导致缓存行反复失效性能急剧下降这就是伪共享。解决办法是缓存行填充在变量前后各填充若干字节让它们落在不同的缓存行。JDK 8里用Contended注解但需要开启JVM参数才能生效。Disruptor框架就是靠这个技巧把性能做到极致的。6. 常见问题与排查技巧实录6.1 死锁的排查思路死锁排查是线上必备技能步骤很固定jps找到Java进程PID。jstack PID导出线程栈。搜索deadlock关键字JVM会直接告诉你哪几个线程死锁了各自持有什么锁、等待什么锁。预防死锁的三板斧加锁顺序一致、加锁超时、死锁检测。实际项目里我一般用tryLock带超时抢不到就放弃并记录日志避免无限等待。6.2 线程池任务丢失怎么查任务丢失一般有三个原因队列满了触发拒绝策略、线程执行时抛异常没捕获、线程池被shutdown。排查顺序先看拒绝策略是不是Discard类再看任务里有没有try-catch兜底最后确认线程池生命周期。我习惯在提交任务时包一层记录任务ID和提交时间出问题能快速定位。6.3 常见问题速查表现象可能原因排查手段CPU飙高自旋过多、死循环top -Hp jstack响应变慢锁竞争激烈jstack看BLOCKED线程内存泄漏线程池队列积压jmap 队列长度监控数据不一致缺volatile或锁检查happens-before6.4 几个容易踩的坑坑一以为volatile能保证原子性。i这种复合操作volatile救不了必须用AtomicInteger或加锁。坑二线程池用Executors.newFixedThreadPool底层是无界队列任务积压会OOM。生产环境必须手动new ThreadPoolExecutor指定有界队列。坑三在synchronized块里调用wait()忘了用while循环判断条件导致虚假唤醒。正确写法是while(!条件){ wait(); }。坑四SimpleDateFormat不是线程安全的多线程共用会出各种诡异问题。要么每次new要么用ThreadLocal要么换DateTimeFormatter。7. 面试答题的策略与心态7.1 怎么把答案说到点子上并发编程的面试有个特点面试官会顺着你的回答往下追问。所以答题策略是先给结论再给原理最后给场景。比如问synchronized先答它是JVM层面的互斥锁通过对象头的Mark Word实现再展开锁升级最后举一个实际用过的场景。这样层次分明面试官想追问也有抓手。7.2 不会的题怎么处理遇到不会的千万别硬编。我的做法是把话题引到相邻的、你熟悉的领域。比如问AQS的源码细节你记不清了可以说源码细节我记得不是特别精确但我知道它的设计思想是模板方法加CLH队列ReentrantLock就是基于它实现的我可以讲讲ReentrantLock的公平锁和非公平锁的区别。这样既诚实又展示了知识面。7.3 平时怎么积累并发编程这东西光看八股文没用必须动手。我的建议是自己写几个demo把死锁、活锁、饥饿都复现一遍。用jstack、jmap、jconsole这些工具实际分析一次线上问题。读一读《Java并发编程实战》和《Java并发编程的艺术》前者讲原理后者讲源码。最后分享一个我自己的习惯每次遇到并发相关的线上问题都记到笔记里写清楚现象、排查过程、根因、解决方案。攒够一年你就是团队里最懂并发的那个人。这个内容后续还可以往分布式锁和无锁编程两个方向扩展那又是另一个大话题了。