资讯详情

资讯详情

Java高并发面试进阶:从背题到理解,掌握面试官考察逻辑

最近帮几个准备跳槽的朋友做了几轮Java面试模拟发现一个特别有意思的现象大家都在背高并发八股文什么synchronized、volatile、ThreadPoolExecutor、ConcurrentHashMap张口就来。但只要面试官把问题稍微换一个角度比如“你这个线程池参数为什么这么配”“volatile在什么情况下真的失效”不少人就卡住了。这其实暴露了一个很典型的问题——很多人把高并发题当成背诵题来准备而不是当成一种思维方式来训练。这篇文章我想换个角度不按“题目-答案”那种老套路堆内容而是站在面试官视角拆解这些问题背后到底在考什么你该怎么把一个知识点讲得有深度、有体系、能落地。内容会覆盖Java高并发面试最常考的几条主线——线程与线程池、JMM与并发三大特性、锁与同步工具、并发容器以及我这两年帮候选人复盘时整理出来的追问实录和避坑清单。正在准备面试的Java后端工程师或者想系统梳理并发基础的开发者都可以拿这篇文章当作复习地图用。1. 高并发面试到底在考什么先搞懂面试官的提问逻辑1.1 为什么Java面试永远绕不开高并发高并发这个词很多初学者一听就觉得离自己很远觉得那是大厂架构师才需要关心的事。但实际上面试官考高并发并不是想招一个架构师而是想通过这个问题快速判断你的技术深度和实战经验。Java后端开发的日常工作中几乎处处都是并发问题多个用户同时下单、多个线程同时写缓存、多个服务之间异步调用、定时任务和业务线程互相竞争资源。如果你对并发没有清晰的认识轻则出现数据错乱、重复下单重则线上OOM、应用假死。所以高并发问题本质上考察的是你有没有能力写出在生产环境里真正靠得住的代码。面试官问一道并发题心里其实有一套隐形的评分标准先看你基础概念对不对再追一层看你知不知道底层原理再追一层看你能不能结合业务场景给出合理选型最后还会挖一挖你踩过什么坑。这四个层次对应的是初级、中级、高级和资深很多人停留在第一层所以被追问几次就露馅了。1.2 一条主线串起所有考点我习惯把Java并发面试题分类成五条线面试官不管怎么问基本都绕不开这五条第一是线程基础包括线程的创建方式、生命周期、状态流转以及线程池的核心参数和拒绝策略。这是并发编程的地基也是最容易被追问细节的地方。第二是并发三大特性也就是原子性、可见性、有序性。这里会牵出JMMJava内存模型、volatile、synchronized、final等关键字的作用原理。第三是锁体系从synchronized的锁升级到JDK的显式锁ReentrantLock再到AQS的底层设计思路。第四是并发工具类和容器包括CountDownLatch、CyclicBarrier、Semaphore、ConcurrentHashMap、BlockingQueue等考察的是你面对具体场景时能不能选对工具。第五是实战相关的权衡比如线程池参数设置、并发性能调优以及如何排查死锁、线程阻塞、内存溢出等问题。这五条线并不是孤立的而是互相支撑的。你在回答线程池时提到“为什么核心线程满了之后先入队列而不是直接开新线程”这个问题往下挖就到JMM和线程调度你在回答volatile时提到“不能保证原子性”接着就会引出synchronized和锁。所以不要一个个知识点零散地背要用这条主线把所有内容串成一张网。1.3 “背题”和“理解”的分水岭在哪里同样一个知识点背题的人回答出来就一句话理解的人能用三层结构讲透。举一个最典型的例子“请你说一下synchronized的原理”背题版本synchronized是Java内置锁可以修饰方法和代码块能保证原子性、可见性和有序性。理解版本synchronized在JDK早期是重量级锁直接依赖操作系统的互斥量实现线程阻塞唤醒开销很大。JDK 6之后做了锁升级优化锁对象头里有Mark Word轻量级锁先通过CAS自旋获取如果竞争激烈再膨胀为重量级锁。它保证的是多条线程执行到同一个监视器锁时的互斥性配合锁的释放建立Happens-Before关系从而实现可见性。只要面试官不是赶时间他一定希望听到的是第二个版本。所以这篇文章后面所有的内容我都会尽量用“是什么—为什么—怎么落地”三层结构来拆解帮你把每道题讲出层次感。2. 线程与线程池高频起点但大多数人卡在追问上2.1 线程创建方式的“标准答案”其实是个陷阱“创建线程有几种方式”这个问题我听过太多人上来就答“四种继承Thread、实现Runnable、实现Callable、用线程池”。这个答案本身没错但它暴露了你没有思考过“创建线程”和“提交任务”的区别。实际上真正意义上的创建线程只有一种方式就是new Thread()然后调用start()启动。Runnable、Callable、线程池这些本质上都是把“要执行的任务”和“执行任务的线程”进行解耦的手段。Runnable和Callable是任务的定义方式线程池是线程的管理方式它们跟手动new Thread()不在同一个维度上。面试如果要体现你的理解深度可以这么答日常开发中我不会直接new Thread因为每次新建线程都会经历创建、销毁、再创建的过程频繁操作会带来性能开销而且线程一多没法统一管理。所以我会把业务逻辑封装成Runnable或Callable任务交给线程池执行这样既能复用线程又能控制并发度还能做任务队列缓冲。顺带说一句Callable和Runnable最大的区别是Callable有返回值能抛异常提交给线程池后可以拿到Future去获取结果。如果面试官追问Future的实现原理就要往FutureTask和AQS方向去答了这块很多人会接不住。2.2 线程生命周期从NEW到TERMINATED状态流转别答漏线程状态是基础题但也是失分重灾区。Thread.State枚举里定义了六种状态NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。很多人背得很熟但一追问就出问题最常见的漏洞有三个。第一个漏洞是把RUNNABLE和操作系统的运行态搞混。Java里的RUNNABLE其实覆盖了操作系统里的Ready和Running两种状态因为Java线程调度是由操作系统负责的JVM没办法精确区分当前线程是真的在执行还是在等待CPU时间片。所以如果你答“运行中的线程是RUNNING状态”那就错了Java里没有RUNNING。第二个漏洞是BLOCKED和WAITING的区别说不清。BLOCKED是线程想进入synchronized代码块但锁被其他线程持有所以在入口处阻塞WAITING是线程在等待其他线程的显式通知比如调用了wait()、join()、LockSupport.park()。前者是被动地被锁挡住后者是主动地让出CPU等待条件满足。第三个漏洞是TIMED_WAITING容易被漏掉。接受带超时参数的方法时线程会进入这个状态比如sleep(1000)、wait(1000)、join(1000)。它和WAITING的唯一区别就是多了一个超时时间超时后由JVM自动唤醒。我记得有一次面试一个简历上写“熟悉并发编程”的候选人让他描述从调用start()到线程正常结束的状态变化他全程说成了“新建、就绪、运行、阻塞、死亡”一听就是背的操作系统教材里的五态模型跟Java的线程状态完全对不上。这种基础概念一旦答错后面的回答无论多精彩面试官心里都会打个折扣。2.3 ThreadPoolExecutor七个参数每一个背后都有面试官的“下一问”线程池是Java高并发面试里当之无愧的“压轴题”。ThreadPoolExecutor有七个参数先说结论完整的参数表是这样的参数作用常见追问corePoolSize核心线程数即使空闲也会保留怎么估算maximumPoolSize最大线程数线程数上限为什么有上限keepAliveTime非核心线程空闲存活时间核心线程能回收吗unitkeepAliveTime的时间单位一般用什么单位workQueue任务等待队列为什么先入队列threadFactory创建线程的工厂不设置有什么问题handler拒绝策略四种选哪个大多数人能报出这七个参数的名字但一个都扛不住追问。我挑几个最关键的展开说。第一个追问“为什么线程数小于corePoolSize时新任务来是先创建新线程而不是让已有线程去取任务”原因是线程池的设计理念里corePoolSize是“池子最低保有量”任务来了如果线程不够优先把线程数拉起来这样才能充分利用CPU。如果一开始就排队corePoolSize就形同虚设了。第二个追问“corePoolSize满了为什么新任务进队列而不是继续开线程到maximumPoolSize”这个问题太经典了。线程的创建和上下文切换是有成本的队列起到缓冲作用是应对突发流量的“蓄水池”。直接把线程数拉到最大反而会因为频繁切换导致吞吐量下降。所以线程池的原则是先保证已创建线程的利用率再考虑扩大规模。第三个追问“队列也满了怎么办”这时候才会创建非核心线程直到达到maximumPoolSize。如果maximumPoolSize也满了就触发拒绝策略。这个顺序其实就是线程池设计者用“资源分阶段使用”的思路在管理线程核心线程→队列→非核心线程→拒绝层层递进。第四个追问“你刚才说keepAliveTime是非核心线程的存活时间那核心线程能不能被回收”正常情况下不能但如果调用了allowCoreThreadTimeOut(true)核心线程也会在空闲超过keepAliveTime后销毁这样在低峰期可以节省资源。这个细节没写过代码的人基本不知道。然后是threadFactory很多初学者没注意这个参数。如果你不自定义ThreadFactory线程池里出来的线程名字就是pool-1-thread-1这种线上排查问题时根本分不清是哪个业务线程池。我自己的习惯是一定会自定义ThreadFactory把线程名命名为业务前缀加自增编号比如order-pool-thread-1将来翻线程dump的时候就会感谢这个好习惯。2.4 线程池参数到底怎么设置别信网上流传的“万能公式”关于核心线程数和最大线程数的设置我见过太多人张口就是“CPU密集型设为N1IO密集型设为2N”。这个说法不是完全错误但它是极其粗糙的经验值面试时如果只说这个会被认为没有真实项目经验。正确的思路是分场景处理。CPU密集型任务理论上是线程数等于CPU核数时效率最高因为再多线程反而要频繁切换N1是为了留一个线程应对缺页中断等意外阻塞。IO密集型任务线程大部分时间在等待IO真正用CPU的时间很少所以可以设置更多线程2N是一个保守的起点。但真实生产环境里任务往往不是纯粹的CPU密集型或IO密集型而且还有内存、连接池、外部依赖响应时间等多个约束。所以更稳妥的做法是先根据经验定一个初始配置再用压测去验证。我一般会先用一个相对保守的配置上线然后通过JVisualVM或者Arthas观察线程使用率、等待队列长度、任务执行耗时再逐步调整。面试时如果能答出“没有万能公式关键是压测和监控调整”这个思路就已经胜过了绝大多数只会背公式的人。如果再能讲一次你用压测数据反推参数的案例那这道题就是加分项。拒绝策略也值得单独说一句。AbortPolicy直接抛异常是默认策略DiscardPolicy静默丢弃容易造成任务无声无息丢失DiscardOldestPolicy丢弃最老任务适合允许丢消息的场景CallerRunsPolicy是让提交任务的线程自己执行起到天然限流的作用。我特别推荐记住CallerRunsPolicy因为它在“任务不能丢”的场景下非常实用——线程池满了就把任务反馈给调用方去执行无形中降低了任务提交速度。3. JMM与并发三大特性回答“可见性”时的底层逻辑3.1 并发bug的源头可以从一次线上订单重复说起我在很多场合讲并发编程都会先抛一个真实场景线上订单系统偶尔出现同一个用户下单两次数据库里出现了两条一模一样的订单。代码里明明判断过用户未登录就返回为什么还会这样绝大多数并发问题的根源可以归结到三个方面原子性、可见性、有序性。先说可见性。现代CPU为了提速每个核心都有自己的缓存线程A改了共享变量的值线程B不一定能马上看到因为A写的是自己核心的缓存还没刷到主内存。这就像两个人在不同房间各记了一本账A改了数字但没喊一声B在自己的账本上看到的还是旧数字。再比如编译器为了提高性能会把代码重新排序这就是有序性问题。原本顺序执行的结果不受影响但一旦多线程共享变量重排序就可能导致匪夷所思的结果。原子性就不用多说了多个线程同时执行count这个“读取-修改-写回”三步操作不是原子的就会丢更新。Java为了解决这三大问题才设计了JMM——Java内存模型。JMM规定所有变量存在主内存每个线程有自己的工作内存线程对变量的所有操作都要在工作内存中进行然后再同步回主内存。这个抽象模型解释了为什么会有可见性问题也为后面理解volatile和synchronized提供了依据。3.2 volatile能保证什么又保证不了什么volatile应该是面试最容易被高估的关键字。很多人一看到volatile就以为它神通广大可以实现线程安全其实它只保证可见性和有序性不保证原子性。可见性体现在一个线程写了volatile变量JMM会强制把这个新值刷回主内存同时让其他线程工作内存里这个变量的缓存失效。也就是说其他线程再读这个变量时必须回主内存拿最新值。有序性体现在JMM会对volatile变量读写前后插入内存屏障禁止指令重排序保证不会出现DCL单例那种非预期问题。但你要注意volatile不能解决count的原子性问题。因为volatile本身没有锁的能力它不能让“读取-修改-写回”这个复合操作变成一个原子步骤。如果两个线程同时读到count1各自加1后都想写回2结果两个线程都写的是2实际执行了两次累加结果却丢了一次。DCLDouble-Checked Locking单例是volatile最经典的面试题。回答这个问题时要讲三层为什么用双重检查——为了既保证线程安全又避免每次加锁的性能损耗为什么能解决——第一层判断不获取锁大多数情况可以直接走为什么要加volatile——因为new对象不是原子操作它会经历“分配内存-初始化-引用赋值”多个步骤不加volatile另一个线程可能读到尚未初始化完成的半成品对象。3.3 synchronized的锁升级是理解Java性能优化的一个缩影synchronized早期的实现确实性能很差因为它直接依赖底层操作系统的互斥量Mutex Lock线程一旦竞争不到锁就会被挂起进入阻塞状态而线程挂起和唤醒的设计需要从用户态切换到内核态这个开销非常大。JDK 6之后做了一次关键优化引入了锁升级机制方向是无锁→偏向锁→轻量级锁→重量级锁。偏向锁的核心逻辑是如果锁没有被多个线程竞争就让获得锁的线程在锁对象头里记录自己的线程ID之后这个线程再进入同步块时无需任何CAS操作直接判定持有者是自己。偏向锁在只有一个线程反复进入同步块时性能非常好。一旦有第二个线程来竞争偏向锁撤销升级为轻量级锁。轻量级锁通过CAS自旋来获取锁线程不会立刻阻塞而是在用户态反复尝试。自旋会占用CPU但避免了内核态切换适合锁持有时间很短的场景。如果自旋超过一定次数仍然拿不到锁说明锁竞争很激烈轻量级锁就膨胀为重量级锁线程进入真正的阻塞状态。自旋的次数JVM会根据竞争情况动态调整这就是自适应自旋。面试时能把这个升级路径讲清楚再补一句“偏向锁在JDK 15之后被默认禁用因为它的维护成本在如今的多线程场景下已经大于收益”面试官会对你另眼相看。JDK 15开始偏向锁默认就是关闭的这个信息很多人不知道。3.4 Happens-Before规则面试回答可见性问题时的“杀手锏”说到可见性如果你只会说“volatile可以保证可见性”那就还差一层。面试官这时候大概率会追问“你凭什么说synchronized释放锁之后其他线程立刻能看到最新值”这就是Happens-Before规则要回答的问题。Happens-Before原则是JMM定义的一组偏序关系用来保证两个操作之间的内存可见性。如果操作A Happens-Before操作B那么A的结果对B可见。面试时不需要把八条规则全背下来但至少要说清下面这几条常用的程序次序规则单线程内按程序代码顺序前面的操作Happens-Before后面的操作。注意这是对单线程而言。锁规则对一个锁的解锁unlockHappens-Before于后续对这个锁的加锁lock。这是synchronized为什么能保持可见性的核心。volatile规则对一个volatile变量的写Happens-Before于后续对这个变量的读。这条规则是volatile实现可见性的底层逻辑。传递性如果A Happens-Before BB Happens-Before C那么A Happens-Before C。上次面试一个候选人我问他synchronized为什么能保证线程安全。他说“因为它是互斥锁同一时间只有一个线程能进入”。这个答案对但不完整。如果他能补一句“synchronized的解锁和加锁之间满足Happens-Before的锁规则所以前一个线程对共享变量的所有修改后一个线程加锁后都能看到”这道题的评价会完全不一样。4. 锁、同步工具与并发容器从八股文到真正会选型4.1 AQSJava并发工具类的“地基工程”面试Java并发绕不开一个名字——AbstractQueuedSynchronizer也就是AQS。很多工具类、锁实现都是基于AQS封装的比如ReentrantLock、Semaphore、CountDownLatch。面试官问你“ReentrantLock的原理”本质上问的就是AQS。AQS的核心思想可以概括为用一个volatile的int类型变量state表示同步状态用内置的FIFO双向队列CLH队列变体来管理等待线程。获取资源失败时当前线程会被封装成Node节点加入队列尾部并挂起持有资源的线程释放资源后会从队列头部唤醒等待线程去重新竞争。这个设计的巧妙之处在于state的语义由子类自己定义AQS只负责维护同步状态和线程阻塞唤醒的框架。比如ReentrantLock里state0表示锁空闲state0表示锁被持有重入一次state就加1Semaphore里state表示剩余许可数量CountDownLatch里state表示还需要等待的计数。这就叫模板方法模式。面试诚实说很多人对AQS的理解就停留在“一个抽象类底层是CAS队列”。能真正把acquire和release的流程走一遍的很少。建议自己花一个下午的时间把AQS源码里acquireQueued的循环流程过一遍你会发现很多面试题背后的逻辑一下子就通了。比如“LockSupport.park/unpark是怎么实现线程阻塞唤醒的”“为什么说中断对Lock是不响应的”这些问题答案全在AQS的源码里。4.2 ReentrantLock对比synchronized从面试题到业务选型“ReentrantLock和synchronized有什么区别”这道题出现在所有面试里但大多数答案都漏掉了最关键的一点。常规答案无非是ReentrantLock需要手动加锁解锁synchronized自动释放ReentrantLock可以尝试非阻塞获取锁tryLocksynchronized不能ReentrantLock可以响应中断synchronized会一直阻塞ReentrantLock支持公平锁和非公平锁synchronized只有非公平锁ReentrantLock可以绑定多个Condition条件队列。这些都是对的但如果你只说这些面试官会觉得你在背文档。真正体现理解的是最后一句在很多业务场景里我优先使用synchronized。因为它写法简单、不容易出忘释放锁的问题而且JDK在持续优化它的性能它的性能在大多数场景下和ReentrantLock已经没有明显差距。ReentrantLock的真正优势场景是需要超时获取锁、需要可中断获取锁、需要多个Condition队列、需要公平锁。在这些场景里用ReentrantLock才是合理的。所以面试时如果能补充“我平时的选择逻辑是先考虑synchronized只有碰到上述特殊诉求才引入ReentrantLock”这道题就能拿高分。选型不是选功能最全的而是选最合适场景的。4.3 ConcurrentHashMap从锁分段到CAS红黑树到底变了什么ConcurrentHashMap在面试中的地位大概相当于英语考试里的阅读理解——必考。如果你还在答“JDK 7用的是分段锁JDK 8用的是CASsynchronized”只能说明你背过答案但不一定真的理解。JDK 7的实现是Segment数组每个Segment继承ReentrantLock锁粒度是Segment级别。默认有16个Segment理论上支持16个线程并发写每个Segment内部是HashEntry数组和HashMap结构类似。它的问题是扩容时需要锁住整个Segment而且锁粒度过粗并发度上限取决于Segment数量。JDK 8彻底放弃了Segment直接使用Node数组加链表/红黑树。写入时先通过CAS尝试把新节点放到桶的第一个位置如果CAS失败说明桶里有并发竞争就锁住链表头节点用synchronized保证链表内部的并发安全。这样锁粒度从Segment细化为单个桶并发度大大提高。链表长度超过阈值8且数组长度不低于64时会把链表转成红黑树降低查询时间复杂度。面试时如果能再补充一句“synchronized比ReentrantLock实现更轻量而且JVM对synchronized有锁升级优化所以JDK 8选择用synchronized来锁桶头”这道题基本就通关了。这句话能展示出你对性能演进有自己持续关注的敏感度。4.4 CountDownLatch、CyclicBarrier、Semaphore三个解决不同问题的工具并发工具类面试频率也很高但很多人容易把CountDownLatch和CyclicBarrier搞混。我自己的记忆方法很简单CountDownLatch是一个人等其他多个人CyclicBarrier是很多人互相等待到齐后同时出发。CountDownLatch的典型场景是主线程等所有子线程执行完成后再继续。比如一个批量导入任务分成多个线程各处理一个文件主线程用countDownLatch.await()等所有人都处理完再统一汇总结果。它的计数只能减不能增所以不能复用用完就得新建。这里我特别想说一下相关热词“java线程等待都完成”我每次面试都会追问一个问题“主线程等待多个任务完成除了CountDownLatch还有没有其他实现方式”如果你能答出Future.get()、CompletableFuture.allOf()、CyclicBarrier等方案并且分析各自的适用场景那就眼前一亮。CyclicBarrier的典型场景是多个线程到达一个屏障点后一起往下执行。比如跑批任务里每个线程处理完各自的数据分片后等待其他线程也处理完再一起进入下一个阶段。CyclicBarrier的计数可以重置所以叫cyclic可以循环使用这跟CountDownLatch是本质区别。Semaphore是拿许可来控制同时访问资源的线程数量可以做限流。比如一个接口同时只允许3个线程进来执行其他线程排队等许可释放。它强调的是“不阻塞全部线程只限制并发量”。Semaphore也要注意一个问题tryAcquire非阻塞获取许可在业务里特别常用可以构建“如果拿不到许可就走降级逻辑”的健壮方案。5. 面试追问实录与避坑清单5.1 wait/notify为什么必须在synchronized代码块里这个问题我面试的时候几乎必问因为它能区分“背过wait和notify”和“真正理解线程协作机制”两种人。wait/notify是Object类的方法作用是让线程在某个条件不满足时进入等待池等条件满足时被其他线程唤醒。为什么Java要强制要求调用wait/notify时必须持有锁核心原因是避免Lost Wake-up问题也就是通知丢失。举个例子生产者消费者模型里消费者先判断队列为空然后进入wait等待生产者通知。如果wait不放在同步块里可能发生一个经典的时间窗消费者刚判断完“队列为空”线程切换生产者写入了一条数据并执行notify但此时消费者还没进入wait所以通知没被它收到。等消费者再被调度时它执行wait却永远等不到那个已经错过的通知了就造成永久阻塞。把wait和notify都放进synchronized里通过统一的锁保证互斥就能避免这个问题。队列为空这种条件判断和wait动作就是在同一个锁的保护下原子执行的生产者如果要在同一把锁下修改队列状态并notify消费者要么在锁外等待要么在锁内进入wait两者不存在时间窗。这个问题的回答思路对你的启发是任何多线程协作代码都不能只考虑“正确”的路径还要考虑“时序错开”的路径。很多线上诡异bug都是这类问题导致的。5.2 线程池执行过程中抛异常到底会发生什么这个题目是我在模拟面试时特别爱加的“附加题”因为它不是一个单纯背知识点的问题而是检验你有没有认真分析过线程池的源码。先说结论如果任务是submit方式提交的话异常会被封装到Future里调用Future.get()时才会重新抛出。如果任务是execute方式提交的话线程池会捕获并处理异常默认处理方式是把异常打印到控制台/日志然后线程不会被销毁继续从队列里取下一个任务执行。这个结论背后的源码逻辑是Worker线程执行任务时外面套了一层try-finally无论任务是否抛异常都会进入finally块把当前Worker从线程池的workers集合中移除再重新获取工作任务。如果Worker数量少于核心线程数线程池会补一个新Worker否则就正常退出。这意味着任务抛异常并不会直接导致线程池挂掉但异常任务里已经执行的数据库操作等副作用是回滚不了的。这里还有一个非常值得注意的坑用execute提交任务时如果任务抛出的不是业务异常而是Error比如StackOverflowError线程池默认也是捕获不了的。所以线上要用线程池做重任务时我会在任务入口统一try-catch一遍确保任何异常都不会裸传到线程池框架层并且把关键异常打到独立的监控告警里。5.3 一个ForkJoinPool没关闭导致进程不退出问题的复盘分享一个我个人经历过的坑特别适合放在面试“项目经验”里讲。有一次线上应用发布新版本运维反馈说“执行完定期任务后进程没有正常退出”。查了线程dump发现ForkJoinPool里还有非守护线程挂在那里而主线程早就执行完了。原因是ForkJoinPool默认创建的线程是非守护线程应用里用了它做并行计算但没有在任务结束后显式调用shutdown()JVM只能在非守护线程全部结束后才退出。所以进程就像“任务干完但人不走”一样一直挂着不退出。这个是特别好的面试素材因为它同时考察了线程类型、线程池生命周期、JVM退出机制几个点。你可以在面试里讲所有线程分两类守护线程和非守护线程。守护线程服务于其他线程例如GC线程当所有非守护线程结束后JVM不管还有没有守护线程都会退出。所以编写长生命周期线程池代码时尤其要注意执行完任务要么关闭线程池要么把线程设置为daemon线程要么用Spring管理的线程池跟着容器一起管理生命周期。5.4 如何向面试官讲好一道高并发题表达结构比储备更重要最后给一个特别实在的建议关于面试表达。很多候选人技术储备是够的但表达一团乱一会儿讲原理、一会儿讲使用、一会儿又跳回源码面试官听得一头雾水。我自己总结了三个层次的表达结构简称“约束-机制-场景”可以套用在几乎所有的并发问题上。第一层讲约束说清楚这个方案/工具解决的是什么问题比如“volatile要解决的是多个线程之间共享变量的可见性问题”。第二层讲机制讲清楚它是怎么解决的比如“volatile通过内存屏障和禁止重排序让写操作立即同步到主内存让读操作从主内存读取最新值”。第三层讲场景讲清楚什么时候用、什么时候不用比如“volatile适合状态标志位和单例双重检查不适合count这种复合操作”。用这个结构去回答面试问题哪怕知识点本身有点小瑕疵面试官也能感觉到你是有体系的。比如面试官问“说一下ConcurrentHashMap”你的回答就可以是它要解决的是HashMap线程不安全的问题约束JDK 8用CAS加synchronized锁桶头来保证线程安全查询不锁写入只在单个桶加锁机制所以我通常在高并发场景下用它替代HashTable和Collections.synchronizedMap场景。还有一个小技巧面试官追问到你不确定的细节时不要硬答更不要编。可以说“这块我只了解到这里我目前理解是……如果不对很期待您指正”。真实、坦诚的态度在面试官这里通常是加分的比胡编乱造要好得多。我个人在带团队和面试新人时最大的体会是真正拉开差距的从来不是记住了多少工具类而是遇到并发问题时能不能快速定位到“资源竞争”和“共享状态”这两个根因然后想清楚“谁来管状态、怎么管状态、管道不动时怎么办”。把这些想透了高并发面试题对你来说就不难了。希望这篇内容能帮你在准备阶段少走一些弯路也欢迎你在评论区聊一聊自己遇到的经典并发问题。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →