
这类主题最值得先看的不是概念列表而是面试官到底在问什么。高并发和多线程的面试核心不是让你背出所有名词而是考察你能否把零散的知识点串联成一个能解决实际问题的、有逻辑的技术判断体系。很多人准备了大量八股文但一被问到“如果让你设计一个抢票系统线程池参数怎么设”或者“线上服务线程数暴涨从哪开始查”就卡住了。这篇文章会从一个面试官和一线开发者的双重角度帮你把“高并发多线程”这个庞大的话题拆解成几个可以层层递进、现场组织答案的模块。我会假设你已经有了一些基础概念比如知道线程是什么、锁是什么但面对综合性的场景题和深度追问时感觉知识点是散的。我们的目标是把这些散点变成你面试时可以自如调用的“工具箱”。1. 面试官到底在考什么从“知识点背诵”到“问题解决能力”很多人一看到“高并发多线程面试题”就去背“进程和线程的区别”、“synchronized和ReentrantLock的区别”、“volatile关键字的作用”。这些是必要的砖块但面试官想看的是你用这些砖块盖房子的能力。1.1 三个核心考察维度面试官的问题通常围绕三个层面展开你需要有意识地把答案往这三个方向上靠基础原理与机制这是地基。比如Java内存模型JMM、happens-before原则、线程的生命周期、锁的实现原理AQS、线程池的核心参数和工作机制。这部分问题通常比较直接但回答要有深度不能只停留在表面。并发工具的正确使用这是工具。面试官会考察你是否真的会用java.util.concurrent包下的工具比如CountDownLatch/CyclicBarrier/Semaphore的区别和使用场景ConcurrentHashMap的实现原理ThreadLocal的内存泄露问题以及各种阻塞队列的特性。综合性场景设计与问题排查这是盖房子。这是区分普通候选人和优秀候选人的关键。例如“如何设计一个秒杀系统”、“如何实现一个生产者-消费者模型”、“线上服务出现死锁如何定位和解决”、“线程池任务堆积可能的原因有哪些如何优化”我建议你在准备时就按照这三个维度去整理自己的知识树。看到一个基础知识点立刻去想它在工具里是怎么用的在复杂场景下会引出什么问题1.2 回答问题的“STAR-R”框架对于场景题不要一上来就陷入技术细节。可以借鉴“STAR”原则但调整为更适合技术面试的“STAR-R”SSituation简要描述问题场景。例如“这是一个高并发写入的场景比如优惠券库存扣减。”TTask明确你的任务目标。例如“需要保证在数万QPS下库存扣减的绝对准确不超卖。”AAction阐述你采取的技术行动。这是核心要分层第一层架构选型。是直接用数据库乐观锁还是引入Redis分布式锁或是用RedisLua脚本还是用消息队列削峰填谷第二层具体实现。如果用Redis分布式锁选择setnx还是Redisson锁的粒度怎么控制商品ID维度锁的超时时间设多少如何避免锁过期但业务未执行完的问题看门狗机制第三层细节考量。异常处理锁获取失败怎么办、降级策略直接返回失败还是排队、监控指标锁竞争次数、持有时间。RResult说明行动带来的结果。例如“最终实现了在xxx压力下TPS达到xxx且未出现超卖。”RReflection加分项。反思与优化。例如“后续复盘发现锁粒度还可以细化到库存批次进一步减少竞争。另外我们增加了热点库存的本地缓存预热减少对中心存储的访问压力。”用这个框架组织语言你的回答会显得非常有结构性和专业性。2. 必须吃透的底层基础JMM、锁与线程池这一部分是面试的必答题也是理解所有高级问题的基础。不能有模糊地带。2.1 Java内存模型JMM与并发三大问题面试官问“volatile有什么用”他期待的绝不仅仅是“保证可见性禁止指令重排序”。他是在考察你对JMM的理解。可见性、原子性、有序性必须能用自己的话解释清楚并举出反例。可见性线程A修改了共享变量线程B不一定立刻能看到。volatile通过读写内存屏障解决。原子性一个或多个操作要么全部执行成功要么全部不执行。i不是原子操作。需要通过synchronized或CAS如AtomicInteger解决。有序性程序执行的顺序不一定和代码顺序一致编译器/处理器会做优化。volatile和happens-before规则可以保证有序性。Happens-Before原则这是理解Java并发规则的总纲。要能说出几条关键的并举例程序次序规则、管程锁定规则unlock先于lock、volatile变量规则、线程启动/终止/中断规则、对象终结规则、传递性。举例为什么Thread.start()调用前的修改对run()方法可见线程启动规则volatile的深度剖析底层实现内存屏障LoadLoad, StoreStore, LoadStore, StoreLoad。StoreLoad屏障是最重的。典型场景状态标志位while (!stop)、DCL双重检查锁单例模式JDK5以后配合volatile才能正确工作。常见误区volatile不能保证复合操作的原子性如count。2.2 锁机制从synchronized到AQS锁是协调多线程访问共享资源的基石。要能对比知道怎么选。synchronized用法修饰实例方法、静态方法、代码块。升级过程这是高频考点。无锁 - 偏向锁Mark Word记录线程ID - 轻量级锁自旋CAS - 重量级锁向操作系统申请互斥量。要能说清楚升级的条件和目的在无竞争或低竞争时减少开销。锁优化自适应自旋、锁消除、锁粗化。ReentrantLock特点相比synchronized功能更丰富可中断、可超时、可尝试非阻塞获取、支持公平/非公平锁、可以绑定多个条件变量Condition。底层基于AQSAbstractQueuedSynchronizer。这是必须啃下的硬骨头。AQS核心原理它维护了一个volatile int state表示资源状态和一个FIFO线程等待队列CLH变体。模板方法模式子类需要实现tryAcquire/tryRelease独占模式或tryAcquireShared/tryReleaseShared共享模式。以ReentrantLock.NonfairSync为例lock()时先直接CAS尝试将state从0改为1非公平的体现。如果失败调用acquire(1)。acquire会先调用子类的tryAcquire再次尝试。如果还失败则将当前线程包装成Node加入等待队列并调用LockSupport.park()挂起。前驱节点释放锁时会唤醒后继节点。能画出AQS队列的大致结构并说明入队、出队、挂起、唤醒的过程面试官会眼前一亮。2.3 线程池不只是“七大参数”线程池是工程中使用并发的最重要工具。绝不能只背参数。核心参数与工作流程corePoolSizemaximumPoolSizeworkQueuekeepAliveTimethreadFactoryhandler。流程口诀核心线程未满 - 创建新线程执行核心线程已满任务队列未满 - 入队队列已满最大线程未满 - 创建临时线程全都满了 - 执行拒绝策略。必须手写一遍这个流程并能在白板上画出来。阻塞队列选型LinkedBlockingQueue无界队列默认Integer.MAX_VALUE任务可能无限堆积导致OOM。适用于任务量可预估且稳定的场景。ArrayBlockingQueue有界队列可以防止资源耗尽。SynchronousQueue不存储元素直接传递。适用于快速处理、不希望任务堆积的场景。CachedThreadPool用它。PriorityBlockingQueue优先级队列。拒绝策略AbortPolicy默认抛RejectedExecutionException。CallerRunsPolicy让提交任务的线程自己去执行。这是一种有效的回退和削峰策略能减缓任务提交速度。DiscardOldestPolicy丢弃队列里最老的任务。DiscardPolicy直接丢弃新任务。实践建议生产环境不要用默认的AbortPolicy至少要用CallerRunsPolicy并根据业务场景考虑自定义策略如记录日志、持久化到数据库稍后重试。参数设置经验CPU密集型corePoolSizemaximumPoolSize CPU核数 1。减少线程切换开销。IO密集型corePoolSize CPU核数 * 2maximumPoolSize可以设大一些如CPU核数 * 4 / (1 - 阻塞系数)。因为线程大部分时间在等待IO。队列容量需要根据任务处理速度和内存来权衡。一定要设置监控观察队列堆积情况。常见坑点线程池的复用不要每次执行都new一个线程池要用全局共享的。Spring中的Async默认使用SimpleAsyncTaskExecutor它为每个任务创建新线程生产环境必须配置自定义线程池。ThreadLocal内存泄露线程池中的线程是复用的ThreadLocal变量如果不及时remove可能会一直持有对大对象的引用导致内存泄露。3. 高阶工具与模式JUC包的精髓掌握了基础就要学习如何使用更高级的工具来优雅地解决复杂问题。java.util.concurrentJUC包是你的武器库。3.1 同步工具类控制线程的执行节奏CountDownLatch vs CyclicBarrier vs SemaphoreCountDownLatch倒计时闩锁一个线程或多个等待其他一组线程完成工作。初始化一个计数线程调用countDown()减1调用await()的线程会阻塞直到计数为0。一次性使用。场景主线程等待所有子线程初始化完毕后再执行模拟并发测试同时发起请求。CyclicBarrier循环屏障一组线程互相等待到达一个公共屏障点后再同时继续执行。可重复使用reset()。场景多阶段任务比如数据分片计算每个阶段所有线程都完成后才能进入下一阶段。Semaphore信号量控制同时访问特定资源的线程数量。用于做流量控制。场景数据库连接池限流限制某个接口的并发调用数。简单记忆CountDownLatch是“等人齐了开会”CyclicBarrier是“等人齐了才能一起出发去下一个景点”Semaphore是“停车场只有N个车位”。3.2 并发容器线程安全的集合ConcurrentHashMap高频考点。必须了解其演进。JDK7分段锁Segment。每个Segment继承ReentrantLock。JDK8及以后Node数组 链表/红黑树。锁的粒度更细锁住的是数组的每个桶链表头节点使用synchronized和CAS。关键方法putValgetsize()是一个估计值非强一致。为什么get不用加锁因为Node的val和next都用volatile修饰保证了可见性。CopyOnWriteArrayList写时复制。适合读多写极少的场景。每次修改add set都会复制底层数组开销大。迭代器使用快照不会抛出ConcurrentModificationException。BlockingQueue线程池的核心也是生产者-消费者模式的经典实现。前面已介绍要熟悉put/take阻塞和offer/poll非阻塞/超时的区别。3.3 原子类与CASAtomicInteger, AtomicLong, AtomicReference等基于Unsafe类提供的compareAndSwapCAS操作实现无锁更新。CAS原理比较并交换。V-内存值A-预期值B-新值。只有当V A时才将V更新为B。这是一个原子性的CPU指令。ABA问题一个值从A变成B又变回ACAS会认为它没变过。解决方案使用带版本号的原子引用如AtomicStampedReference。CAS的缺点循环时间长开销大自旋只能保证一个共享变量的原子操作ABA问题。4. 实战场景设计、问题排查与系统优化这是面试中最能体现价值的环节。你需要把前面的知识点像拼图一样组合起来。4.1 经典场景设计题剖析场景一如何设计一个秒杀系统这不是问你怎么写代码是问架构思路。分层削峰前端按钮置灰、验证码、请求频率限制。网关/负载均衡限流令牌桶、漏桶、恶意IP过滤。服务层读多写少商品详情等静态数据用CDN浏览器缓存。库存热点库存数据预热到Redis。扣减库存是核心。库存扣减方案对比方案A数据库乐观锁。update stock set stockstock-1 where id? and stock0。简单但数据库压力大成功率随并发增高而急剧下降。方案BRedis递减。DECR命令是原子的。性能极高。但存在超卖风险因为Redis扣减成功到数据库真正扣减之间可能失败。方案CRedis Lua脚本。将“判断库存”和“扣减库存”写在一个原子性的Lua脚本中执行解决超卖。然后异步通知数据库更新最终库存。这是主流方案。方案D消息队列。所有请求先入队服务端按自己的能力从队列里消费实现绝对的顺序和流量控制。用户体验为“排队中”。最终一致性采用方案C或D时要保证Redis和数据库的最终一致可以通过监听binlog或异步任务来同步。降级与熔断如果Redis或数据库扛不住要有预案比如直接返回“活动太火爆请稍后再试”。场景二实现一个生产者-消费者模型这考察你对线程协作和阻塞队列的理解。经典写法使用BlockingQueueBlockingQueueTask queue new LinkedBlockingQueue(100); // 生产者 public void produce(Task task) throws InterruptedException { queue.put(task); // 队列满则阻塞 } // 消费者 public Task consume() throws InterruptedException { return queue.take(); // 队列空则阻塞 }进阶追问如果不用BlockingQueue用wait()/notify()怎么实现需要维护一个普通队列并用synchronized保护在队列空/满时让线程等待。如何支持多个生产者和多个消费者如何优雅地停止消费者线程设置一个“毒丸”对象放入队列消费者读到它就退出。4.2 线上问题排查链路当面试官问“线上服务CPU飙高/线程死锁/内存泄露怎么排查”他希望你有一套方法论。定位问题进程和线程top -Hp [pid]查看哪个线程CPU占用高。记下线程ID十进制。printf “%x\n” [线程id]将线程ID转为十六进制。查看线程堆栈jstack [pid] jstack.log导出Java线程堆栈。在jstack.log中搜索上一步得到的十六进制nid找到对应的线程堆栈看它在执行什么代码。CPU高通常是线程在疯狂执行比如死循环或频繁GC。死锁jstack输出最后会明确提示“Found one Java-level deadlock”并列出死锁线程和锁资源。分析内存jmap -heap [pid]看堆内存概况。jmap -histo:live [pid]看存活对象 histogram。jmap -dump:live,formatb,fileheap.hprof [pid]导出堆转储文件。使用MAT或JVisualVM分析heap.hprof查找Retained Heap最大的对象看是谁在引用它GC Root路径定位内存泄露点。其他工具jstat -gcutil [pid] 1000每秒打印一次GC情况观察GC频率和耗时。Arthas阿里开源的Java诊断神器。thread -b直接找死锁watch/trace命令动态监控方法调用耗时和参数。4.3 性能优化与最佳实践锁优化减小锁粒度从方法锁缩小到代码块锁使用ConcurrentHashMap的分段思想。减少锁持有时间把与共享变量无关的计算移出同步块。锁分离读写锁ReentrantReadWriteLock读读不互斥。无锁化尝试使用原子类、ThreadLocal、不可变对象、CAS操作。上下文切换线程数不是越多越好。过多的线程会导致大量CPU时间花在线程切换上。用vmstat或pidstat查看cs上下文切换次数。伪共享False Sharing由于CPU缓存行通常64字节的机制两个无关的变量如果位于同一个缓存行一个线程修改其中一个会导致另一个线程的缓存行失效即使它没修改那个变量。解决方案使用sun.misc.Contended注解JDK8进行字节填充。面试到最后面试官可能会问“你还有什么问题”。不要问薪资福利这留给HR可以问一些有深度的问题比如“团队目前遇到的最有挑战性的并发问题是什么”、“咱们的系统在并发方面的技术选型是怎样的”。这能体现出你的思考深度和主动性。把高并发和多线程的知识从散点的记忆变成连成线的理解再织成面的解决方案这是从“知道”到“掌握”的关键。面试前找几个完整的场景比如秒杀、对账、数据导出自己从头到尾推演一遍把可能被问到的点都覆盖到。真正理解的东西是能经得住连环追问的。