
上周有位准备跳槽的朋友找我聊天说他背了两周的Java面试题结果被一个“线程池参数”问得说不出话。我问他怎么准备的他说就是照着网上的八股文背背完就忘一追问就露馅。Java并发编程面试题这块恰恰是最不能靠死记硬背应对的。它考的不是“你知道这个名词”而是“你有没有真的在项目里踩过它的坑”。我整理过一份123道的Java并发编程面试题合集写了答案也标注了哪些题是“送分题”、哪些是“送命题”。这篇博文就是从那堆题里挑出最高频、最容易被追问的考点把答题逻辑和背后的原理一次性掰开讲清楚。不管你是准备校招、社招还是想系统梳理一遍并发知识按这个思路去复习效率比自己盲背高出好几倍。1. 并发编程面试题到底在考什么1.1 面试官出并发题的真实意图很多人把并发编程当成一块独立的“背题模块”这是最大的误区。面试官问并发题真正想判断的不是你记了多少概念而是你有没有在实际开发中遇到过线程安全、性能瓶颈、资源竞争这类问题并且真的思考过解决方案。同样是问“synchronized和ReentrantLock的区别”初级选手能把字面区别背出来比如“synchronized是关键字ReentrantLock是类”“ReentrantLock支持公平锁”——这些都没错但一个在真实项目里处理过并发的人会主动补上一条在JDK 6以后synchronized经过锁升级优化性能已经不输ReentrantLock所以选型时要优先考虑synchronized只有在需要尝试获取锁、超时获取锁、中断响应等能力时才引入ReentrantLock。你看这就是“背过”和“懂”的区别。面试官对并发题的考核也分三个层次。第一层是概念层考察你知不知道线程状态有哪些、volatile和synchronized有什么区别第二层是原理层考察你懂不懂JMM、AQS、锁升级的底层机制第三层是实战层考察你在什么场景下做过多线程编程、有没有写过线程池、线上死锁怎么排查。大部分人卡在第二层但拉开差距的往往是第三层。你的复习顺序应该是“概念打底原理支撑实战收尾”。1.2 高频考点全景从123道题库里筛出的六大块我整理那123道题的时候其实发现总有那么二三十道是反复出现的换着花样出本质考点都一样。我把它们归成六大块这基本就是Java并发面试的全部家当。知识域高频题目类型掌握程度要求线程基础线程状态转换、sleep/wait/join区别、创建线程方式必须熟练到脱口而出锁与同步synchronized原理、锁升级、死锁条件、Lock体系能讲出底层机制与场景对比JMM与volatile可见性、原子性、有序性、happens-before规则原理层必须通透JUC并发工具AQS、CountDownLatch、CyclicBarrier、Semaphore会画流程能谈源码线程池七大参数、执行流程、拒绝策略、核心线程数配置必考中的必考并发容器与异步ConcurrentHashMap、ThreadLocal、CompletableFuture会说场景能讲坑这六大块环环相扣。比如你讲ConcurrentHashMap绕不开CAS和synchronized讲CAS绕不开JMM和volatile讲volatile绕不开happens-before。面试官最喜欢干的事就是从一个问题链式追问下去你要是知识点是割裂的问两轮就露馅了。以我的经验复习时别按题目背按“知识链”去串效果完全不一样。2. 线程基础题别让“简单题”变成送命题2.1 线程状态这道题答到什么程度才算合格线程状态是所有并发面试题的地基面试官几乎必问而且经常作为开场题。Java中线程有六种状态分别是NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。注意很多人会答“就绪”“运行中”那是老版本或者操作系统的概念在Java里RUNNABLE状态统一涵盖了“就绪”和“正在运行”这一点一定要说清楚。完整的状态流转是这样的new一个Thread对象之后线程处于NEW调用start()后进入RUNNABLE等待synchronized锁或者显式锁时进入BLOCKED调用wait()、join()、LockSupport.park()进入WAITING调用sleep(long)、wait(long)、join(long)进入TIMED_WAITING线程执行完进入TERMINATED。这里面最容易被追问的是BLOCKED和WAITING的区别BLOCKED是线程在被动地等一把锁而WAITING是线程主动放弃CPU等待其他线程唤醒或通知。答这道题想拿高分光背六种状态是不够的要能说出Java为什么把“就绪”和“运行中”合并成一个RUNNABLE状态。原因其实很简单——Java的线程调度依赖底层操作系统而操作系统并不保证能精确区分“线程已就绪但还没分到CPU时间片”和“正在执行”。Java干脆把这两种统一了这也从侧面说明Java线程本质是操作系统线程的映射JVM自己并不做真正的调度。能答出这一层面试官会觉得你是理解而不是背题。2.2 wait/sleep/join/yield四个方法一次分清这四个方法每年都在面试题里出现形式还是那种“请简述区别”的送分题。但你别小看它很多人上来就答“sleep不释放锁wait释放锁”这没错但太单薄。我建议你用一个对比维度更全的答法。对比维度sleepwaitjoinyield归属Thread静态方法Object实例方法Thread实例方法Thread静态方法是否释放锁不释放释放不释放不释放进入状态TIMED_WAITINGWAITING或TIMED_WAITINGWAITING或TIMED_WAITINGRUNNABLE是否需要唤醒到时间自动恢复需要notify/notifyAll目标线程执行完自动恢复让出CPU后重新参与竞争是否可中断会抛InterruptedException会抛InterruptedException会抛InterruptedException不响应中断重点说两个容易忽略的细节。第一sleep是Thread类的静态方法作用在当前线程上跟“让哪个线程对象睡”没有关系所以正确写法永远是Thread.sleep(xxx)而不是thread.sleep(xxx)。第二wait必须放在同步代码块或同步方法里因为它释放锁的前提是当前线程已经持有了锁这算是语法层面的硬约束而sleep没有这个要求任何地方都能调。另外建议把join的本质理解透join底层是调用了wait所以在join等待期间锁是释放的。很多人不知道这一点面试被问到“join会不会释放锁”就懵了。你想join的实现里有while (isAlive()) { wait(0); }这样的逻辑本质上就是让当前线程阻塞等待目标线程结束。理解了这一层你顺便也就明白了为什么join会抛InterruptedException——因为wait会响应中断。2.3 创建线程的四种方式怎么答才能体现经验创建线程有四种方式继承Thread类、实现Runnable接口、实现Callable接口配合FutureTask、通过线程池创建。这道题本身不难但面试官几乎必然会追问一句“你实际开发中更喜欢用哪种”以及“为什么不用继承Thread”。这里我建议你给出一个有层次感的答案顺序。先讲继承Thread直接重写run方法简单粗暴但Java是单继承一旦继承Thread就不能继承其他类而且把任务代码和线程绑定在一起不利于解耦所以实际项目中几乎不用。再讲实现Runnable把任务从线程中分离出来可以配合线程池使用这是最常用的方式但run方法没有返回值也不能抛受检异常。然后讲实现Callablecall方法有返回值可以抛异常但需要通过FutureTask包装才能交给Thread或线程池执行适合需要异步获取结果的场景。最后是线程池本质上是一种复用线程的资源管理方式并不是与前面三种并列的“新机制”而是在更高层面管理线程。如果面试官继续追问你还可以补一个未来感更强的方案JDK 8之后的CompletableFuture以及JDK 19引入的虚拟线程Virtual Threads这能体现你持续关注Java演进。但注意别炫技过头虚拟线程的底层调度逻辑跟传统平台线程差异很大如果没深入研究简单提一句“这是后续演进方向”就够了。3. synchronized与锁升级最体现功底的一块3.1 synchronized的本质Monitor机制synchronized是Java并发面试里绕不开的话题它出现的频率高、深度深从基础用法能一路问到锁升级、Monitor、对象头。你准备这块内容时建议直接以“从对象头到Monitor”的完整链路为主干因为面试官很容易顺着这条链路往下追。先说对象头。Java对象在内存中分为对象头、实例数据和对齐填充三部分。对象头里有一个Mark Word它记录了对象的哈希码、GC分代年龄以及锁相关的状态信息。锁的标志位就存在Mark Word里从无锁到偏向锁、轻量级锁、重量级锁Mark Word的内容会随之变化。这是整个锁机制的地基。再说Monitor。每个对象都有一个Monitor与之关联在HotSpot虚拟机里对应ObjectMonitor对象。synchronized加锁的本质就是让线程去竞争目标对象的Monitor的所有权。Monitor内部维护着持有者线程、等待队列等数据结构。当一个线程进入synchronized代码块时它尝试获取Monitor如果获取成功就继续执行失败就进入阻塞状态。最后补一个面试官爱问的点synchronized是可重入锁。什么叫可重入就是同一个线程对同一个对象锁可以反复获取。比如一个synchronized方法内部调用了另一个被同一个锁保护的synchronized方法线程不会把自己锁死因为Monitor记录着持有线程信息发现是同一个线程就直接放行。这个机制的底层实现就是Monitor内部有计数器每次重入计数加一全部退出后减到零才真正释放锁。3.2 锁升级过程每一步都别讲错从JDK 6开始HotSpot对synchronized做了大量优化引入锁升级机制让锁在不同竞争程度下选择不同的实现。升级路径是无锁状态 - 偏向锁 - 轻量级锁 - 重量级锁。这里有一句关键的话必须记住锁只能升级不能降级。也就是说一旦膨胀成重量级锁不会自动降回来。偏向锁解决的是“只有一个线程访问同步块”的场景。初次访问时锁对象的Mark Word里记录线程ID同一个线程再次进入时直接判断是不是自己就无需再做同步操作。这一步非常快几乎没有开销。如果另一个线程来竞争偏向模式就撤销升级成轻量级锁。轻量级锁用CAS操作实现适合“线程交替执行同步块但竞争不激烈”的场景。线程进入临界区之前先在栈帧中创建锁记录Lock Record然后尝试通过CAS把Mark Word复制到锁记录中并更新对象头。如果成功获取轻量级锁成功如果失败说明存在多线程竞争锁就会膨胀。重量级锁就是传统意义上的Monitor锁依赖操作系统底层的mutex互斥量实现。这里有个关键点涉及用户态和内核态的切换上下文切换开销很大所以重量级锁被认为是最低效的。但在高并发竞争场景下线程如果没有抢到锁就进入阻塞反而避免了自旋空转的CPU浪费。这里有一个很容易被忽视的现实变化我必须提醒你偏向锁在JDK 15中默认被禁用JDK 18之后被标记为废弃。原因是现代应用普遍竞争比较激烈偏向锁的撤销逻辑反而带来额外开销。所以你回答锁升级时如果主动提一句“不过偏向锁在新版本JDK中已经不再默认开启”面试官会觉得你是真的关注版本变更而不是只背了老的教学资料。3.3 volatile和JMM和synchronized配合着考的问题Java内存模型Java Memory Model简称JMM是理解并发原理的底层框架。JMM规定所有变量存在主内存每个线程又有自己的工作内存线程对变量的所有操作都必须先在工作内存中执行再刷回主内存。这个模型直接引出了并发三大特性原子性、可见性、有序性。volatile解决的是可见性和有序性。可见性上volatile修饰的变量每次被修改后会立即刷回主内存每次读取时从主内存读不缓存在线程工作内存里。有序性上volatile通过内存屏障禁止编译器重排序和CPU重排序保证指令执行顺序符合程序语义。但volatile不解决原子性比如i这种“读-改-写”操作即使变量是volatile多线程下依然不安全因为volatile没法保证“读取、自增、写回”这三步作为一个整体不可分割。面试题常考的经典场景是双重检查锁Double-Checked Locking的单例模式为什么instance变量必须用volatile修饰原因是new操作不是原子的底层有三步分配内存、初始化对象、将引用指向内存。如果不加volatile编译器或CPU可能把第二和第三步重排序导致一个线程在对象还没初始化完成时就以为创建好了另一个线程拿到一个半初始化的对象引用去用然后出问题。volatile通过禁止重排序避免了这种错乱。回答这里时建议主动关联happens-before原则。JMM通过happens-before规则保证程序的正确同步volatile变量的写-读之间有happens-before关系这意味着一个线程写volatile变量之后的任何操作对后续读取该变量的线程都是可见的。AQS里的state变量就声明为volatile就是依赖这个语义来保证锁状态在线程间的可见性。4. AQS与JUC工具源码层面的“加分点”4.1 AQS设计思路一次看懂模板方法AQSAbstractQueuedSynchronizer是Java并发包中最核心的支撑类ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock等都依赖它。面试题如果考到“你了解哪些并发工具”铺垫一句“这些都是基于AQS实现的”然后讲清楚AQS的核心机制立刻就能把回答提升一个档次。AQS的核心有三样东西。第一是state一个volatile修饰的int类型变量表示同步状态不同子类对state的含义不同ReentrantLock里state表示持有锁的次数Semaphore里表示剩余许可数量CountDownLatch里表示还需要等多少个事件。第二是CLH队列变种AQS内部维护了一个双向链表队列竞争不到锁的线程会被封装成节点放入队列尾部然后阻塞等待。第三是模板方法设计模式AQS定义好了acquire和release的骨架流程把tryAcquire和tryRelease这类具体操作留给子类实现。拿ReentrantLock加锁举例线程调用lock()进入AQS的acquire方法首先执行tryAcquire尝试获取锁这一步由ReentrantLock实现利用CAS对state做修改获取成功就拿到锁失败则把当前线程封装成Node节点加入等待队列尾部然后使用LockSupport.park把自己挂起。释放锁时AQS执行tryRelease把state减掉然后唤醒队列头部的后继节点。整个过程逻辑统一子类只需要关心怎么修改state。面试官大概率会追问一个问题什么时候会用到AQS你可以举一个自定义同步工具的例子。比如实现一个只允许同时通过两个线程的限流器继承AQS重写tryAcquire判断state是否小于2小于则CAS加一并返回true否则返回false。AQS已经把入队、唤醒、中断响应这些最复杂最易错的逻辑处理好了你只需要关心状态判断这就是AQS的设计哲学。4.2 ReentrantLock与synchronized怎么选这道题的完整答法应该是先讲两者的共同点都是可重入锁都保证了互斥性。再讲差异点最后落到场景选型。差异点用一个表格来说最直观。对比维度synchronizedReentrantLock实现机制JVM层面基于MonitorJDK层面基于AQS是否支持尝试获取锁不支持支持tryLock是否支持超时等待不支持支持tryLock(timeout)是否支持中断响应不支持等待锁时不可中断支持lockInterruptibly是否支持公平锁不支持支持构造参数传true条件变量只能配合wait/notify支持多个Condition精确唤醒获取释放方式语法自动完成必须手动lock/unlock要放finally我在项目里的选型原则很简单默认用synchronized因为不需要手动释放锁出错的概率低而且JDK 6之后性能和ReentrantLock基本持平。但遇到这三种情况我会切换到ReentrantLock第一需要限时等待锁比如请求外呼接口时不能一直阻塞等待第二需要可中断响应比如用户点击取消任务时线程要能及时退出等待第三需要多个条件队列比如生产者消费者模型里要分别唤醒生产者和消费者。这里插一个面试技巧回答完选择逻辑之后可以抛出一个自己踩过的坑。比如说说你曾经手动unlock放错位置导致锁未被释放后来才改成try-finally里统一的写法。这种真实经历比背条条框框更容易打动人。4.3 CountDownLatch、CyclicBarrier、Semaphore别只背定义这三个并发工具经常放在一起考因为都涉及“多个线程协同”。面试官最反感的就是你背定义“CountDownLatch是倒计时器CyclicBarrier是循环栅栏Semaphore是信号量。”背定义没有任何价值你必须结合场景讲。CountDownLatch适合“一个或多个线程等待其他线程完成”的场景。我在项目中用它做过主从库数据校验启动多个线程分别校验不同分片的数据主线程用CountDownLatch等待所有校验线程结束最后汇总校验结果。用法核心就两步构造时设置计数器count每个子线程完成一阶段工作后调用countDown()主线程调用await()阻塞等待直到计数器归零。CyclicBarrier适合“一批线程互相等待都到达屏障点后再一起继续”的场景。经典案例是批量导入数据开5个线程分别读取5个文件等5个线程都读完后再同时执行后面的入库操作。注意CyclicBarrier是可以循环使用的所有线程到达屏障后计数器会重置所以能支撑多轮协同。Semaphore就是许可数量控制本质是个计数器每次acquire消耗一个许可每次release归还一个许可。它最适合做限流。我做过一个网关接口同一时间最多允许100个请求同时执行就用Semaphore(100)控制超过的直接返回“系统繁忙”。不过要提醒一点Semaphore并不保证线程安全性之外的事情它只管数量状态的正确性还得靠其他手段保证。4.4 CAS与原子类为什么能比锁快CAS全称Compare-and-Swap是并发编程里的“无锁化”核心操作。它的流程是拿到内存中的值V和预期值A做比较如果相等就把新值B写入如果不相等说明期间有其他线程改过就重新读取再次尝试直到成功。整个过程由CPU的硬件指令保证原子性Java中通过Unsafe类的compareAndSwapInt实现。保证原子性的方案有两种锁是“悲观策略”CAS是“乐观策略”。锁认为冲突一定发生所以先锁住资源再操作CAS认为冲突偶尔发生所以先尝试操作失败再重试。在高并发下锁会频繁阻塞唤醒线程带来巨大的上下文切换开销而CAS是无阻塞算法靠CPU指令和循环所以一些场景下性能更好。面试必问CAS的ABA问题。想象一个栈内存中链头是A线程1想弹出A它读取到当前值是A。线程2先弹出A又压入一个A。线程1执行CAS时发现内存值还是A以为没有变化就弹出了A。但栈的内容其实已经被动过A下面的节点可能已经变了。解决ABA问题的标准方案是加版本号Java里的AtomicStampedReference就是干这个的通过比较引用和版本戳能识别出这中间发生过修改。顺便说一句Java并发包里的原子类比如AtomicInteger、AtomicLong、AtomicBoolean底层全是CAS。面试被问“AtomicInteger为什么线程安全”时不要简单说“因为它用了CAS”而要补一句CAS没有锁所以它不会让线程进入阻塞状态在高竞争下可能因为自旋循环而消耗CPU因此在竞争极激烈时可能反而比锁更慢。能讲出CAS的劣势说明你不是只懂夸它。5. 线程池面试必问的“硬通货”5.1 七大参数与执行流程一条线讲完线程池是Java并发面试题里性价比最高的一个知识点几乎每场面试都有它的位置而且从七大参数到拒绝策略到执行流程环环相扣。我建议先背熟ThreadPoolExecutor的七个参数然后顺着执行流程把它们串起来。七个参数分别是corePoolSize、maximumPoolSize、keepAliveTime、TimeUnit、workQueue、threadFactory、handler。corePoolSize是核心线程数默认常驻maximumPoolSize是最大线程数keepAliveTime和TimeUnit共同决定非核心线程空闲多久后被回收workQueue是任务队列threadFactory是创建线程的工厂可以自定义线程名handler是任务满员时的拒绝策略。线程池提交一个任务后的执行流程用一个四步走就能讲清楚。第一步当前线程数小于corePoolSize时创建核心线程执行任务。第二步线程数大于等于corePoolSize但任务队列还没满任务加入队列等待。第三步队列满了线程数小于maximumPoolSize创建非核心线程去处理任务。第四步队列满了且线程数达到maximumPoolSize触发拒绝策略。这里有一个常见的记忆误区线程池不是“先加非核心线程再往队列里放任务”而是“先让核心线程跑再让队列缓存最后才拉满最大线程”。很多教材里把它简化成“核心线程不够就进队列队列不够就加线程”这个顺序千万别搞反。实际开发时这个微妙的顺序会影响你判断一个任务到底会排队还是会立即执行进而影响你对系统吞吐量的评估。5.2 四种拒绝策略场景决定选择当线程池的任务队列满了、线程数也到最大值时新提交的任务会交给RejectedExecutionHandler处理。JDK内置了四种拒绝策略面试常考但更多时候是给你一个场景让你选。策略行为适用场景AbortPolicy直接抛RejectedExecutionException默认策略任务不能丢需要立即感知CallerRunsPolicy提交任务的线程自己去执行被拒绝的任务不想丢任务允许放慢提交速度DiscardPolicy静默丢弃不抛异常可接受任务丢失不影响核心业务DiscardOldestPolicy丢弃队列中最旧的任务再尝试提交新任务处理新任务优先级高于旧任务的场景我在实际项目中用得最多的是CallerRunsPolicy。因为它的思想是“把任务反推给提交者”线程池满员时提交任务的主线程自己会去执行任务相当于变相限流让生产速度降下来又不丢任务。比如日志异步写入宁可让业务线程自己写日志也不能把日志丢掉。AbortPolicy在不想静默掩盖问题、希望快速报警时也不错但它会直接把异常抛到提交方处理不当可能影响主流程。其实面试时选哪个策略并不关键关键是解释为什么。比如你选CallerRunsPolicy要说清楚它是不是有“慢提交”的副作用以及为什么你能接受这个副作用。能把这个逻辑讲通比告诉你“用哪个”有用得多。5.3 核心线程数怎么定不能只背公式线程池核心线程数的配置是面试里最容易引出“实战经验”的题目。常见的说法是CPU密集型设置为CPU核数加一IO密集型设置为CPU核数乘二或者用“CPU核数除以(1-阻塞系数)”这个公式。这些说法有用但不能生搬硬套。CPU密集型任务比如大量计算、图像处理几乎不阻塞线程一直在占用CPU设置成CPU核数或多一个就可以多了反而因为频繁上下文切换降低效率。IO密集型任务比如读写文件、请求接口的大量时间在等待IO线程被阻塞时不占CPU所以可以设置更多线程。阻塞系数通常取0.8到0.9经验值就是CPU核数的两倍左右。但实际开发你千万别一上来就套公式。我自己的做法是先用一个保守值跑起来比如CPU核数加一配合压测工具观察指标看CPU利用率、请求响应时间、拒绝任务数量。如果CPU利用率一直很低可能增加线程数如果线程大量在等待IO且响应很慢就加大IO密集任务的线程数。通过oom或者性能监控工具持续观察比拍脑袋套公式靠谱得多。这里还要补一个细节threadFactory最好自定义给线程池里的线程起个有业务含义的名字比如“user-order-pool-thread-1”。这样以后查问题看到线程名字就大概知道是哪个业务的线程池在处理排查效率会高出不少。这个细节特别容易被忽视但面试官很吃这一套。5.4 execute与submit之间的坑线程池提交任务有两种方式execute(Runnable)和submit(Callable或Runnable)。这道题几乎必考而且跟着一个非常经典的坑submit能吞掉异常导致你的监控系统根本不知道任务失败了。execute最终执行的是Runnable如果run方法里抛了异常这个异常会由线程池的UncaughtExceptionHandler处理如果你没有设置异常会被打印到控制台线程被回收或替换。submit不一样它把任务包装成FutureTask异常被捕获后存储起来等调用future.get()的时候才会重新抛出。如果代码里从不调用get()异常就永远没机会抛出来问题被彻底吞掉了。所以我的建议是如果任务执行失败需要立刻感知优先用execute或者在submit后立刻get()拿到结果如果任务本身允许失败而且你不在意结果的获取submit也不是不行但必须想清楚异常去向问题。还有一点submit返回的Future可以配合超时等待比如future.get(5, TimeUnit.SECONDS)任务超时未返回就主动失效这在调用外部服务时尤其有用。注意线程池使用结束后记得主动调用shutdown()不然核心线程会一直驻留应用无法正常退出。不过如果你用的是Spring的ThreadPoolTaskExecutor容器销毁时会自动处理这里不用担心。6. ThreadLocal、并发容器和Future容易出陷阱的进阶题6.1 ThreadLocal的内存泄漏是面试官最爱引用的坑ThreadLocal的面试题十道里有八道会问内存泄漏。先说清楚它的原理再谈泄漏原因。ThreadLocal本身不存值值存在当前线程的ThreadLocalMap里key是ThreadLocal对象value是你set进去的对象。所以“每个线程有一份独立变量副本”的本质就是线程内部有一个Map以ThreadLocal作为key。内存泄漏的根源在 ThreadLocalMap.Entry 的继承结构Entry继承WeakReference它的key即ThreadLocal对象是弱引用而value是强引用。弱引用碰到GC就会被回收如果外部没有强引用指向ThreadLocal对象它被GC回收后Entry的key变成null但value依然被Entry引用着不会被回收。只要线程一直存活线程池里的线程往往长时间复用这些key为null的Entry就永远占用内存逐渐堆积最终造成内存泄漏。解决方式也很简单ThreadLocal用完后务必调用remove()把key为null的Entry一并清理掉。我在项目里看到过一些同事只在set之后不做清理时间一长内存爬升得很明显。不要心存侥幸觉得“我这是一个很小的变量不会泄漏”在高频创建和使用ThreadLocal的场景里堆积速度比你想象得快。实际使用场景中ThreadLocal很有价值最常见的是在线程池里存请求上下文、租户ID、traceId之类的东西。比如一个接口处理链路要从入口一直传递一个跟踪ID到最底层如果每层方法都手动传参代码会非常难看ThreadLocal刚好能优雅地解决。但正因为线程池线程会复用如果不清理下一个任务就会读到上一个任务的脏数据这个坑比内存泄漏更隐蔽项目里一旦出现排查起来相当痛苦。6.2 ConcurrentHashMap从JDK7到JDK8的演进并发容器题目中ConcurrentHashMap是绝对的C位。它跟HashMap、HashTable的区别要能一句话说清HashMap线程不安全HashTable全表加锁性能极差ConcurrentHashMap用细粒度锁和无锁技术实现线程安全和高效并存。JDK 7的ConcurrentHashMap采用分段锁Segment设计。整个Map被分成若干Segment每个Segment就是一个小型HashMap并且使用独立的ReentrantLock锁保护。不同线程操作不同Segment时互不干扰锁粒度是Segment级别并发度等于Segment的数量。JDK 8完全重构了实现抛弃Segment改为使用数组加链表加红黑树的结构锁粒度细化到单个数组桶bucket利用CAS加synchronized实现线程安全。JDK 8的逻辑值得好好说插入元素时先通过key的哈希算出桶的位置如果桶为空直接CAS设置头节点不需要加锁如果桶不为空就synchronized锁定这个桶的头节点再往链表或红黑树里插入。链表长度超过8个且数组容量达到64时链表转为红黑树避免查找性能退化。所以JDK 8的并发度比JDK 7高很多它锁的只是一个桶而不是一整个分段而且用CAS避免了大部分加锁开销。还有一个常考的点ConcurrentHashMap的size()方法为什么“不准”因为这是一个并发场景计算size时其他线程随时可能修改Map。JDK 8的实现里size()的返回值是一个估计值它先尝试无锁累加CounterCell数组中的计数如果竞争激烈还会CAS累加baseCount最终返回的值是尽量接近真实的快照值。你可以理解成它给出的更像“当前时刻的近似值”而不是严格一致的数值。这个特性在面试中值得主动提一句说明你理解并发里的“一致性与性能之间的取舍”。6.3 CompletableFuture把异步编排讲出亮点CompletableFuture在JDK 8引入面试中越来越常见。它本质上是Future的增强版解决了传统Future的两个痛点一是future.get()会阻塞当前线程二是多个Future之间的依赖、组合关系写起来很痛苦。CompletableFuture最核心的价值是链式编排。你可以通过supplyAsync提交一个有返回值的异步任务然后用thenApply对结果做转换用thenCompose把两个有依赖关系的异步任务串联起来用thenAccept消费结果而不需要返回值用whenComplete做收尾处理。整个调用链不需要手动get()阻塞等待完成回调自动触发。这就像一条流水线上个工序加工完自动传给下个工序而不是每道工序都要人盯着。业务上最常见的组合是每次请求外部服务时可能需要并发调用两个接口再合并结果这时可以用CompletableFuture.allOf把所有任务组合起来等全部完成后统一处理结果。任一个任务发生异常还可以用exceptionally或handle设置兜底值避免异常在链式调用里被吞掉。anyOf则适合“多个任务谁先完成就取谁的结果”的场景比如同时请求两个相同功能的第三方服务哪个先返回用哪个做故障降级。面试时想拿高分可以补充一句CompletableFuture提交任务时如果不指定线程池默认使用ForkJoinPool.commonPool。这个公共线程池被很多地方共用一旦某个任务阻塞会影响其他公共池里的任务。所以生产环境建议显式传入自定义线程池避免互相干扰。这种经验性细节是很多背题的人根本想不到的。7. 死锁与线上排查让面试官觉得你“实战过”7.1 死锁的四个必要条件与如何避免死锁是面试中出现频率极高而且最后很容易变成“加分题”的内容。要答好死锁先把四个必要条件背到滚瓜烂熟再谈避免手段最后如果能现场演示一个死锁代码效果最好。死锁四个必要条件是互斥条件至少有一个资源只能被一个线程独占持有并等待一个线程持有资源的同时又在等待其他线程占用的资源不可剥夺线程已持有的资源在自己使用完前不能被强行抢走循环等待多个线程之间形成环形等待依赖关系。四个条件同时成立死锁才会发生。避免死锁的思路就是从条件入手。最简单有效的手段是破坏“循环等待”和“持有并等待”。破坏循环等待的做法是规定所有线程按同一个全局顺序加锁比如先锁A再锁B锁的顺序固定就不会出现环形依赖。破坏持有并等待的做法是使用tryLock带超时去获取锁获取不到就释放自己已经持有的资源过会再重试这样资源和线程不会无限期占死。演示死锁的代码其实很短两个线程各自持有锁A和锁B线程1先锁A再请求B线程2先锁B再请求A两边都卡在等对方的锁上程序就永久阻塞。我在给团队分享的时候经常现场写这段代码然后跑一次给他们看效果比讲十分钟理论都直观。面试时能写出来就证明你是真的写过不是只背了定义。7.2 线上死锁排查一条龙死锁不只在面试题里出现生产环境真的会遇到。我经历过一次比较典型的案例两个线程池互相调用一个持有了A事务锁又去请求B锁另一个反着来结果线上接口集体超时应用基本不可用。定位过程很有代表性分享给你。第一步用jps找到目标Java进程的PID。第二步用jstack PID导出线程快照。在jstack输出中搜索“deadlock”关键字如果存在死锁JVM会明确打出一段描述告诉你哪些线程在等待哪把锁并列出锁的所有者。第三步根据线程栈里的类名、方法名、行号定位到业务的哪段代码造成了锁竞争。用jstack看线程快照有两个技巧。第一不要只搜deadlock还要看大量线程是否都卡在同一个位置比如几十个线程同时停在“waiting to lock xxx”说明这是一个热点锁。第二要会看线程的当前状态大量线程处于BLOCKED或WAITING通常意味着锁竞争、死锁或者线程池耗尽。排查这类问题除了jstack也可以辅助用jconsole或jvisualvm的可视化线程面板但命令行方式在任何环境都能用这是底线技能。注意jstack导出的只是一瞬间的快照死锁往往是偶发的一次快照可能抓不到。建议连续导出几次或者结合实际线程数指标一起看。线上排查时多保留几份不同时刻的快照对比分析覆盖率会高很多。另外补充一个排查思路如果没法直接用命令行可以给应用加一个定期输出线程快照的脚本或开关在系统异常时自动保留现场。我在一些关键服务里配置了定时诊断线程每30秒检查是否有线程长时间处于BLOCKED一旦发现就自动dump线程栈并告警。这个方案不算复杂但能在故障发生时留下最关键的现场数据对事后定位帮助极大。7.3 让“并发四问”成为你的万能答题框架其实不管是线程状态、synchronized、AQS还是线程池面试官追问的风格几乎一致就四件事是什么、为什么、什么时候用、有什么坑。把这四件事在脑子里过一遍任何并发问题都能答出层次感。比如面试官问“你了解synchronized吗”你就可以按这个框架来先说是什么它是Java提供的内置锁基于Monitor实现。再说为什么JVM在JDK 6后做了锁升级优化让它在不同竞争场景下都有不错的性能。再说什么场景用它默认加锁场景都用synchronized除非需要超时、中断、公平锁等能力。最后说什么坑它不能尝试获取锁等待时不能响应中断在竞争极其激烈时重量级锁的上下文切换开销很大。我们在准备Java并发编程面试题的时候很多人有个误区觉得题目越多越好刷完一本又一本。实际上真正的核心考点非常集中你与其痛苦地把一道题的答案死背下来不如把背后的机制和场景想透。我给面试者做模拟面试的时候最明显的感受就是能主动讲出“为什么”和“踩过的坑”的候选人评价往往远超那些把所有八股文背得滚瓜烂熟的人。因为知识可以被遗忘但你解决问题的思路和理解深度不会。希望这套从123道并发面试题里提炼出来的核心考法能帮你少走点弯路把这些高频考点真正变成自己的东西。