当前位置: 首页 > news >正文

2023大厂Java面试八股文核心考点全解析:从HashMap到分布式锁

2023年的Java面试,大家感受最深的一点是什么?我的感受是:八股文依然是躲不过去的一道关卡。网上有太多人说八股文没用,但当你真正坐在面试官对面,被问到HashMap扩容为什么是2的次幂、synchronized锁升级的中间状态、Redis分布式锁到底怎么续期,如果只靠背结论,很容易被追问到怀疑人生。这段时间我集中复盘了蚂蚁金服、滴滴、美团、拼多多、腾讯几家公司的面试题,发现高频考点的重合度非常高。这篇文章不是我凭空整理的标准答案,而是结合我自己刷题、面试、复盘的全过程,把2023年大厂Java面试里反复出现的核心考点、追问链路线、容易踩的坑一次讲清楚。适合正准备校招、社招、跳槽的Java工程师,也适合刚工作两年想系统地补一遍基础的开发同学。

1. 为什么大厂还在问八股文?我2023年的备考思路

1.1 八股文不是背诵比赛,是技术深度的索引

很多人把八股文等同于死记硬背,这个理解有偏差。我自己一开始也这么觉得,后来复盘面试录音才发现,面试官问HashMap,并不是想听你背出源码注释,而是通过一个常用类的设计,顺藤摸瓜考察三件事:你对数据结构的理解是否到位、你是否真的读过源码、你能不能把原理落到线上问题里。一个HashMap可以串出hash算法、链表和红黑树、扩容机制、并发安全、ThreadLocalMap的设计差异,甚至能聊到CPU缓存命中率。所以八股文的本质是技术深度的索引,不是考点本身。

我这次备考给自己定了一个原则:每个高频考点都准备三层回答。第一层是"是什么",用一两句话给出定义;第二层是"为什么",讲清楚底层原理和设计动机;第三层是"生产环境怎么用或怎么排查",用实际场景证明你不是纸上谈兵。比如synchronized,第一层说它是Java提供的互斥锁;第二层讲偏向锁、轻量级锁、重量级锁的升级过程;第三层举例说在秒杀系统里大量线程竞争时,锁膨胀到重量级会导致性能断崖,所以会优先考虑减少锁粒度或者用CAS。面试官听到这个回答,一般就会知道你确实在项目里思考过并发问题。

1.2 高频考点优先级排序,别想一口吃成胖子

Java八股文的覆盖面非常广,如果从Java基础、集合、并发、JVM、Spring、MySQL、Redis、消息队列、分布式、算法全部平均用力,三个月都不一定够。我用自己的复盘结果给考点排了个优先级,原则很简单:先盯住所有公司都问的,再看不同公司有特色的。

考点方向优先级大厂出现频率追问深度
Java集合:HashMap、ArrayList极高几乎每轮都有源码级
并发编程:synchronized、volatile、线程池极高每轮必有会延展到AQS和Lock
JVM:内存区域、GC、类加载极高高频会结合OOM和调优
MySQL:索引、事务、MVCC高频结合SQL优化场景
Spring:Bean生命周期、循环依赖高频结合项目启动报错
Redis:持久化、缓存问题、分布式锁中高高频结合高并发场景
消息队列:Kafka基础原理中高美团、拼多多喜欢问偏架构
项目场景设计:秒杀、短链、限流二面/三面开放式

这个表格不是让大家只背前面的内容,而是告诉你可以按照这个顺序投入时间。如果时间不够,算法题每天要保证两题,八股文优先把集合和并发吃透。如果时间充裕,再把Spring、MySQL、Redis这些结合项目做深度扩展。

1.3 用"一问三连"的方式准备,否则容易一问就懵

我在模拟面试时发现,光自己背很容易产生"我会了"的错觉。真正的面试不会只问一个点,面试官会连环追问。比如美团面试官问我:"HashMap线程安全吗?"我回答不安全。他接着问:"那并发场景用什么?"我说ConcurrentHashMap。他又问:"ConcurrentHashMap是怎么保证线程安全的?JDK1.7和1.8有什么区别?"这就是典型的三连追问。如果只背了第一层,第二层就容易卡住。

所以我的做法是把每个考点做成一张问题卡片,正面是主问题,背面是两到三个追问。每天早晚各抽十张卡片,先不看答案,自己口述回答。这个过程非常痛苦,但对记忆的巩固效果比读十遍笔记都好。后文我会把这次备考中整理的高频问题直接列出来,大家可以直接按这个思路去做自己的卡片。

2. Java基础与集合:最容易翻车的"送分题"

2.1 HashMap:从put到resize的完整链路

HashMap是Java面试的固定开头,几乎每家大厂都问。面试官嘴上说"我们聊点基础的",下一句就是HashMap。想把这个题答好,我建议按一条主线把代码里的关键设计串起来。

首先说数据结构:JDK1.8之后的HashMap是数组加链表加红黑树。数组的默认长度是16,负载因子是0.75,所以默认情况下元素个数达到12个时就会触发扩容。为什么负载因子取0.75?这是时间成本和空间成本的折中。负载因子太高,比如1.0,空间利用率上去了,但哈希冲突概率变大,链表会变长,查询效率下降;负载因子太低,比如0.5,冲突少了,但数组很多空间闲置,浪费内存。

然后说put流程:先计算key的哈希值,但并不是直接用hashCode(),而是做了一次扰动处理:(h = key.hashCode()) ^ (h >>> 16)。这样做的目的是把高16位的特征也混入低16位,因为HashMap计算数组下标时只用了低n位,如果不扰动,高位变化而低位相同的key就会全部冲突。计算下标时用的是(n - 1) & hash,而不是取模运算。因为n是2的次幂,n - 1的二进制全是低位1,与hash做与运算等价于对n取模,但位运算性能更高。这也是扩容时长度必须保持2的次幂的原因。

这里有一个面试官特别爱追问的细节:为什么JDK1.8要把头插法改成尾插法?因为JDK1.7在多线程并发扩容时,链表可能形成环形结构,下一次get就会死循环。JDK1.8改成尾插法,并且引入红黑树,当链表长度超过8且数组长度大于等于64时,链表会转成红黑树,把查询时间复杂度从O(n)降到O(logn)。为什么阈值是8?源码注释里给了泊松分布的数据,在负载因子0.75的默认设定下,链表长度达到8的概率已经极低,不到千万分之一,所以这个阈值并不是随便写的。

回答完这些,还可以主动补一句:HashMap不是线程安全的,并发写会丢数据,Java里可以用ConcurrentHashMap。这一句话就能把话题引到并发方向,面试官往往顺势往深了问,这也是你展示知识体系的机会。

2.2 ArrayList、LinkedList、Vector的对比

很多候选人能背出ArrayList底层是数组、LinkedList是双向链表,但一到具体场景就露馅。面试官会问:"现在有一个场景,需要频繁在列表头部插入元素,选哪个?"很多人脱口而出LinkedList,因为链表插入是O(1)。但如果数据量很大,LinkedList每个节点还要额外维护前驱后继指针,内存开销比数组大很多;而且LinkedList的插入虽然理论上是O(1),但如果是按索引插入,仍然需要遍历找到插入位置,实际复杂度还是O(n)。所以单纯说"链表插入快"是不严谨的。

更好的回答思路是:先说明两者的本质差异,ArrayList适合随机访问和尾部追加,LinkedList适合已知节点指针时的插入删除;但从真实项目角度看,绝大多数业务场景下ArrayList反而更优,因为局部性好、内存连续、CPU缓存命中率高。Java中LinkedList在实际代码里用得很少,这个结论本身就说明问题。

Vector这个类现在基本是面试考古题。它和ArrayList的唯一显著区别是方法用synchronized修饰,线程安全但性能差。现在并发场景都用CopyOnWriteArrayList或者Collections.synchronizedList,不会再用Vector。如果面试官问Vector,你要能说出它被淘汰的原因,而不是简单说"线程安全"。

2.3 String、Integer、泛型的几个隐藏坑

String是Java基础里最容易被问出彩的知识点。首先要说清楚String是不可变类,为什么不可变?因为String被final修饰,内部char数组也被final修饰并且在构造后没有暴露修改入口。不可变带来了字符串常量池的复用、hashCode缓存、线程安全等好处。然后面试官会深入问String a = "abc"String b = new String("abc")的区别。前者先在常量池找,找不到就创建并放入常量池;后者直接在堆上创建新对象,同时如果常量池没有"abc"也会在常量池创建一份。所以a == b是false,a.equals(b)是true。

Integer也有一个高频坑:Integer x = 127; Integer y = 127; x == y为true,因为IntegerCache缓存了-128到127之间的对象;但Integer x = 128; Integer y = 128; x == y为false,因为超过缓存范围会各自new。这个问题的潜台词是"拆箱装箱要注意什么",所以面试官可能会继续问Integer x = 128; int y = 128; x == y的结果,答案是true,因为遇到基本类型会自动拆箱比较数值。

泛型的坑主要是类型擦除。Java泛型只在编译期生效,运行时List 和List 都是同一个List类型。所以没有办法在运行时判断一个List到底装的是String还是Integer,也没有办法直接用泛型创建T[]数组。常见的解决方案是传入Class对象,或者用Object数组再强转。

3. 并发编程:蚂蚁和美团都喜欢深挖的硬骨头

3.1 synchronized锁升级的完整链路

并发是Java大厂面试的分水岭,而synchronized是并发里最经典的入口题。很多人只知道" synchronized是一个重量级锁",这个回答在2023年已经过时了。JDK1.6之后synchronized做了大量优化,现在的正确说法是:synchronized是一把可以升级的锁,无锁、偏向锁、轻量级锁、重量级锁四个状态,锁只能升级不能降级。

我习惯用"厕所门锁"来类比。无锁状态就是没人在意厕所;偏向锁就像门上贴了个标签"第一次使用的人优先",只要没有别人竞争,这个线程下次再来不用重新抢锁;一旦有另一个线程来竞争,偏向锁就升级成轻量级锁,相当于两个人在门口通过CAS自旋抢锁;如果竞争非常激烈,自旋一直在消耗CPU,锁就会升级成重量级锁,抢不到锁的线程直接阻塞,等待操作系统唤醒。

面试官一般会追问:"偏向锁为什么后来被废弃了?"这里需要答出来:偏向锁的撤销过程需要等待安全点,而且在高并发场景下撤销偏向锁的代价很高,JDK15之后默认已经禁用了偏向锁。这就是一个很好的加分点。还可以继续说轻量级锁的核心是自旋,自旋次数不能太多,否则白白浪费CPU;重量级锁会锁的线程进入EntryList等待,涉及用户态和内核态切换,所以性能最差。

3.2 volatile:可见性、原子性、有序性

volatile是另一个必问点。它的核心作用有两个:保证变量在多线程之间的可见性,以及禁止指令重排序。但volatile不保证原子性,这个经常被拿出来做文章。

面试官喜欢问:"为什么volatile不能保证原子性?"可以用volatile int count举例,多个线程同时执行count++,这一步实际被拆成了读、加、写三个操作,volatile只能保证读的时候看到最新值,但读和写之间可能被其他线程插入操作,所以最终结果会小于期望值。要解决这个问题,必须用AtomicInteger的CAS或者synchronized。

volatile的底层实现是用内存屏障。写一个volatile变量时,JVM会在写操作后面插入StoreStore屏障和StoreLoad屏障,把当前线程工作内存中的值强制刷新到主内存;读volatile变量时,会插入LoadLoad屏障和LoadStore屏障,让工作内存中的缓存失效,必须从主内存重新读取。面试官如果继续追问JMM,你可以展开:JMM规定所有变量存在主内存中,每个线程有自己的工作内存,线程对变量的所有操作必须在自己工作内存中进行,不能直接操作主内存,所以没有同步机制时,线程间的修改对其他线程不一定可见。

3.3 线程池七个参数与现场推导

线程池是并发编程里实战性最强的部分。面试官会让你现场写一个ThreadPoolExecutor,然后逐个解释参数。代码很简单:

ThreadPoolExecutor executor = new ThreadPoolExecutor( 2, // corePoolSize 核心线程数 8, // maximumPoolSize 最大线程数 30L, TimeUnit.SECONDS, // 空闲线程存活时间 new ArrayBlockingQueue<>(100), // 任务队列 Executors.defaultThreadFactory(), // 线程工厂 new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略 );

七个参数要按顺序答:核心线程数、最大线程数、空闲存活时间、时间单位、工作队列、线程工厂、拒绝策略。然后讲执行流程:提交一个任务时,如果当前线程数小于核心线程数,直接创建核心线程执行;如果核心线程满了,任务进入队列;如果队列也满了,继续创建非核心线程执行;如果线程数已经达到最大线程数,执行拒绝策略。很多人会把这套流程说反,尤其是"先入队再创建非核心线程"这一步,一定要记牢。

面试官还会问:"核心线程数怎么设置?"这是一个开放题,没有标准答案。如果是CPU密集型任务,线程数建议设为CPU核心数加1,因为CPU密集型任务几乎没有等待,线程太多反而频繁切换;如果是IO密集型任务,线程数可以设为核心线程数乘2,或者用公式CPU核心数 * (1 + 平均等待时间 / 平均计算时间)来计算。更稳妥的回答是:不要靠公式硬套,线上通过压测不断调整,观察CPU利用率和RT。

线程池的拒绝策略也要说清楚。默认是AbortPolicy,也就是直接抛RejectedExecutionException,生产环境一般不直接用,因为任务丢了很危险。我常用CallerRunsPolicy,让提交任务的线程自己去执行被拒绝的任务,起到天然限流的作用。

3.4 AQS与ReentrantLock

synchronized讲完之后,面试官往往会问:"除了synchronized,你还用过哪些锁?"这时候ReentrantLock就上场了。ReentrantLock底层依靠AQS实现。AQS的全称是AbstractQueuedSynchronizer,核心思想是维护一个volatile的state状态变量和一个FIFO双向等待队列。

以ReentrantLock加锁为例:如果state是0,表示锁空闲,线程通过CAS把state改成1,获得锁;如果state不是0,判断当前线程是不是锁的持有者,如果是,state加1,这就是可重入;如果不是,当前线程封装成Node节点放入等待队列,然后通过LockSupport.park挂起。释放锁时state减1,减到0说明完全释放,唤醒队列头部下一个线程。

面试官可能会追问:"AQS为什么用CAS而不是synchronized?"因为CAS是无锁操作,不会导致线程阻塞,在锁竞争不激烈时性能更高。但CAS有ABA问题,AQS里通过版本号或者状态机设计来规避。到这里并发部分就可以自然收尾了,能答完这一串,大厂一面通常不会在并发上扣分。

4. JVM与内存:滴滴和腾讯必问的调优场景

4.1 内存区域划分与OOM实战

JVM部分滴滴和腾讯都问得比较深,至少会追问一次线上OOM的排查经历。先要把内存区域说清楚:线程私有的虚拟机栈、本地方法栈、程序计数器,线程共享的堆、方法区(在JDK8之后实现为元空间),还有直接内存。堆里面继续分新生代和老年代,新生代又分Eden区和两个Survivor区,默认比例是8:1:1。

面试官通常会给一个场景:"线上突然报java.lang.OutOfMemoryError: Java heap space,你怎么排查?"这个问题不能只说"调大堆内存",要给出完整的排查链路。我的习惯是先用jmap -dump:format=b,file=heap.hprof <pid>导出堆快照,再用MAT或者JProfiler分析,找到占用内存最大的对象;如果看不出问题,就用jstat -gc <pid> 1000观察GC频率和老年代增长趋势,判断是内存泄漏还是内存溢出。有一个热词是java: outofmemoryerror: insufficient memory,这个和Java heap space不一样,通常不是堆内存满了,而是操作系统本身可用内存不足,可能是申请直接内存失败或者进程地址空间不够,排查方向要转向mmap和线程栈。

还有栈溢出StackOverflowError,递归调用太深会触发。元空间OOM经常是因为CGLib动态生成类太多,或者在应用重启频繁的场景下类加载器没有卸载。回答时要能区分堆OOM、元空间OOM、栈溢出和直接内存OOM,每种的排查方式和处理手段完全不同。

4.2 垃圾收集器与CMS/G1的取舍

GC部分是JVM最常见的追问线。先解释分代收集理论:新生代对象存活率低,适合复制算法;老年代对象存活率高,适合标记清除或标记整理。然后说收集器。Serial是单线程,ParNew是Serial的多线程版本,Parallel Scavenge关注吞吐量,CMS关注停顿时间,G1是区域化分代收集。

CMS的完整流程要能说出来:初始标记、并发标记、重新标记、并发清除。初始标记和重新标记需要STW,两个并发阶段和用户线程一起跑。CMS的缺点也很明显:并发标记和清除阶段会占用CPU,导致吞吐量下降;标记清除算法会产生内存碎片,可能触发Full GC;最大的坑是并发模式下用户线程继续运行会生成新对象,可能出现"Concurrent Mode Failure",被迫退化为Serial Old单线程Full GC,停顿时间爆炸。

G1的设计理念和CMS不同,它把堆分成一个个Region,不再严格区分新生代和老年代物理区域。G1可以设置预期的最大停顿时间,通过控制回收Region的数量来保证停顿可控。G1的Remembered Set用来记录跨Region引用,解决了"扫描整个堆找引用"的问题。如果面试官问"你线上用的哪个收集器",可以根据Java版本答:JDK11之后默认是G1,如果还追求更低的停顿时间,可以了解ZGC的染色指针和读屏障,但不用展开太深。

4.3 类加载机制与双亲委派

类加载的题相对固定,但很多候选人讲得不够完整。整个过程分加载、验证、准备、解析、初始化五个阶段。加载阶段通过全限定名读取class文件二进制字节流;验证阶段校验格式;准备阶段为静态变量分配内存并赋零值;解析阶段把符号引用替换为直接引用;初始化阶段执行静态代码块和静态变量赋值。

双亲委派模型是重点。类加载器从顶到下依次是Bootstrap ClassLoader、Platform ClassLoader、Application ClassLoader。一个类加载器收到加载请求后,不会自己先去加载,而是先委托给父加载器,父加载器又往上委托,直到Bootstrap。只有父加载器加载不了,子加载器才自己加载。这样做最重要的原因是防止核心API被篡改,比如你写一个java.lang.String,最终会由Bootstrap加载,不会用你的自定义类。

面试官会继续追问:"有没有打破双亲委派的情况?"答案有。Tomcat的WebAppClassLoader为了每个应用能加载不同版本的类库,会先自己加载;JDBC用SPI机制,让线程上下文类加载器加载Driver实现;OSGi模块化框架也是典型打破双亲委派的场景。能答出这三类,这个题就稳了。

5. Spring与MySQL:项目经验背后的硬核基础

5.1 Spring Bean生命周期与循环依赖

Spring在各大厂面试里几乎算标配。Bean生命周期可以从两个角度讲:宏观上就是实例化、属性填充、初始化、使用、销毁。但要拿高分,需要把细节展开:实例化之前会先扫描并解析BeanDefinition,推断构造方法;实例化之后做属性填充,也就是@Autowired依赖注入;然后执行Aware接口回调、BeanPostProcessor的postProcessBeforeInitialization、@PostConstruct、InitializingBean.afterPropertiesSet、自定义init-method、BeanPostProcessor的postProcessAfterInitialization;如果开启AOP,最后还会生成代理对象。

循环依赖是大厂三面特别爱问的题。比如A依赖B,B又依赖A,Spring怎么解决?答案是通过三级缓存。一级缓存存成品对象,二级缓存存早期暴露的原始对象,三级缓存存ObjectFactory,也就是lambda表达式。创建A时,A还没填充属性,先把A的三级缓存放好;填充B时,B需要A,于是从三级缓存拿到A的早期引用,提前暴露给B;B创建完成后,A再注入B。这就是三级缓存存在的意义。

面试官最爱问:"二级缓存不够吗?为什么需要三级缓存?"如果只需要解决循环依赖,二级缓存就够了。但Spring还想保证AOP代理对象是在B中引用的那份。三级缓存里的ObjectFactory会在需要时执行getEarlyBeanReference,在这里完成AOP代理的创建,所以B拿到的是A的代理对象。如果直接使用二级缓存存原始对象,AOP代理的时机就不对,可能导致B注入的是普通对象而不是代理对象。这个点是Spring面试里区分"背过答案"和"真正理解"的关键。

5.2 MySQL索引失效场景与SQL优化

MySQL在Java面试中的地位非常高,尤其是美团这种重视后端基本功的公司。索引部分建议先讲B+树:B+树的非叶子节点只存索引键和指针,不存数据,所以一个页能放更多索引项,树更矮,磁盘IO更少;叶子节点用双向链表串联,非常适合范围查询。这和红黑树、哈希索引相比各有优劣,但B+树是OLTP业务里最稳的选择。

索引失效的高频场景要背熟,最好能现场举例。我用一个联合索引(a, b, c)来说明。如果查询条件是where a = 1 and c = 3,b字段没用到,c字段也走不了索引,因为违反了最左前缀原则,中间断了;如果where b = 2 and c = 3,a都没出现,索引直接失效。还有:对索引列做函数操作,比如where DATE(create_time) = '2023-01-01',会让优化器放弃索引;使用like '%keyword',前面带百分号会失效;隐式类型转换也会失效,比如索引字段是varchar,查询条件用int。

SQL优化不能只背理论。我通常给出的回答框架是:先看慢查询日志确认SQL,再用EXPLAIN查看执行计划,重点看type、key、rows、Extra。type从好到坏依次是system、const、eq_ref、ref、range、index、all,如果出现all就要考虑加索引。Extra里如果出现Using filesort、Using temporary,通常需要优化排序和分组。最后再结合业务,看能不能通过覆盖索引避免回表,能不能用分页延迟关联解决深度分页。

5.3 事务隔离级别与MVCC

事务这块,面试官一般会让你说出MySQL四种隔离级别:读未提交、读已提交、可重复读、串行化。其中可重复读是InnoDB默认级别。然后引出三个问题:脏读、不可重复读、幻读。读已提交解决了脏读,可重复读解决了不可重复读,但InnoDB在可重复读下通过MVCC和间隙锁解决了大部分幻读问题。

MVCC的实现要展开讲。每行数据有两个隐藏列,一个是trx_id,记录最近一次修改这行数据的事务ID;另一个是roll_pointer,指向undo log里的旧版本记录。事务执行快照读时,会生成一个ReadView,ReadView里维护了活跃事务ID列表,以及最小和最大活跃事务ID。判断一个版本是否可见的规则是:如果trx_id小于活跃事务最小ID,说明该版本在快照前已提交,可见;如果trx_id大于活跃事务最大ID,说明该版本在快照后创建,不可见;如果trx_id在两者之间,再判断是否在活跃列表里,在则不可见。

关键区别是:读已提交每次select都会生成新的ReadView,所以同一个事务里两次查询可能看到不同数据;可重复读只在第一次select时生成ReadView,之后一直复用,所以看到的数据是快照一致的。这也是面试官特别喜欢追问的"为什么RR级别下普通查询没有不可重复读问题"的原因。

6. Redis与分布式:拼多多和美团的高并发考题

6.1 Redis持久化与缓存三大问题

Redis的题在大厂后端面试里通常是用来考察高并发设计能力的。先从持久化说起。Redis提供RDB和AOF两种方式。RDB是内存快照,fork一个子进程生成二进制文件,恢复速度快但可能丢数据;AOF是追加日志,默认每秒刷盘,最多丢一秒数据,但文件体积大,恢复慢。生产环境常用AOF加RDB混合:用RDB做冷备和快速恢复,用AOF保证数据不丢太多。Redis 4.0之后支持混合持久化,AOF文件里先存RDB头部,再追加增量命令,重启恢复时先加载RDB再回放AOF,兼顾速度和安全性。

接下来是缓存穿透、缓存击穿、缓存雪崩三个问题。穿透是请求了一个缓存和数据库中都不存在的数据,导致每次请求都打到数据库,可以用布隆过滤器,或者把空值也缓存一小段时间。击穿是某一个热点key突然过期,大量请求同时打到数据库,可以用互斥锁重建缓存,或者对热点key设置逻辑过期时间,在访问时异步刷新。雪崩是大量key在同一时间失效,或者Redis实例宕机,导致数据库被压垮。解决方案有:给过期时间加随机数打散,使用Redis集群高可用,做多级缓存。

这里我加一个自己的体会:很多面试者能把三大问题背出来,但没想过线上怎么监控。如果Redis的QPS突然翻倍,同时数据库慢查询增多,你要能想到先用redis-cli --bigkeys扫描大key,再结合业务日志确认是否出现热点key过期或者缓存穿透。八股文加上真实排查思路,才是面试官想要的回答。

6.2 分布式锁:为什么不能只靠setnx

分布式锁是Redis面试的另一个高峰。最基础的做法是用SET key value NX EX 30加锁,用Lua脚本释放锁。为什么释放锁要用Lua脚本?因为要保证"判断是不是自己的锁"和"删除锁"两个操作原子执行。如果先get再del,可能在get之后锁过期被别的线程拿到,然后你的del把别人的锁删了。用Lua脚本可以把get和del放在一个原子操作里,避免这个问题。

但setnx方案还有个核心问题:锁过期时间设多少?设太短,业务没执行完锁就过期了,其他线程又能抢到锁;设太长,如果持有锁的线程崩了,其他线程要等很久。生产环境不能靠人工拍板,要用Redisson这样的客户端。Redisson加锁后,会启动一个WatchDog看门狗线程,默认锁过期时间是30秒,看门狗每10秒检查一次,如果业务还在执行,就自动把锁续期到30秒。这就解决了锁时间不好设置的痛点。

如果面试官继续往上追问:"Redis主从切换时锁丢了怎么办?"可以答RedLock红锁方案,也就是向多个互相独立的Redis节点加锁,超过半数成功才算加锁成功。但RedLock本身也有争议,所以更好的回答是说清楚它的适用场景和成本,不要盲目使用。我认为分布式锁的考察重点不是方案多花哨,而是你有没有想过锁超时、锁误删、主从切换这些异常情况。

6.3 Kafka为什么能支撑百万并发

消息队列在美团和拼多多的面试里出现频率很高,尤其是Kafka。为什么Kafka能支撑百万级并发?这个问题至少要从三个层面回答。

第一是顺序写。Kafka的日志文件在磁盘上顺序追加,顺序写磁盘的速度可以达到数百MB/s,比随机写快几个数量级,所以Kafka敢把数据直接落到磁盘而不是先放内存。第二是零拷贝。Kafka在消费者拉取数据时使用sendfile系统调用,数据从磁盘文件直接拷贝到网卡,中间跳过用户态和内核态之间的多次拷贝,大幅降低了CPU开销。第三是分区并行。Kafka一个Topic可以分成多个Partition,每个Partition在不同的Broker上,生产者和消费者可以并行读写不同分区,吞吐量随分区数线性扩展。

还要答Kafka的高可用机制。每个分区有多个副本,其中一个是Leader,负责读写;其他是Follower,从Leader拉取数据同步。ISR集合里是跟得上Leader进度的副本,如果Leader挂了,Controller会从ISR中选一个新的Leader。这保证了Broker宕机时分区依然可用。最后可以提一句:Kafka是吞吐优先,所以可能丢数据,如果要更高可靠性,需要把acks设置为all,并开启min.insync.replicas。这样既回答了百万并发,又补充了可靠性的权衡,面试官会认为你是有架构意识的。

7. 算法与场景设计:大厂加试题的真实套路

7.1 手撕算法高频题型

算法题是很多Java开发者的痛点。我的经验是,不用追求把LeetCode刷到一千题,但要保证常见题型形成肌肉记忆。2023年大厂高频手撕题集中在:快速排序、链表反转、LRU缓存、最长回文子串、二叉树的层序遍历、两数之和、无重复字符的最长子串。其中快速排序出现的频率尤其高,因为Java的Arrays.sort默认排序就是快速排序的变种,面试官喜欢让你写一个能跑的版本。

以快速排序为例,核心逻辑是选基准值、分区、递归排序。写的时候要注意边界条件,避免死循环和下标越界。除了快速排序,LRU缓存也很值得准备,因为它在Redis淘汰策略和本地缓存里都有应用。一个精简的LRU可以用LinkedHashMap实现,把accessOrder设为true,重写removeEldestEntry即可。但如果面试官想考察你的数据结构能力,会让你自己用HashMap加双向链表实现。

我建议每天至少手写一题,写完用测试用例跑通。面试手撕题不要闷头写,先和面试官确认输入输出和边界条件,再动手。一边写一边说思路,写完主动走一遍测试用例。这个过程比题目本身更影响评分。

7.2 秒杀系统与短链系统设计

场景设计题在二面和三面出现,通常没有标准答案,只有合理的取舍。我遇到最多的设计题是秒杀系统和短链系统。秒杀系统要解决的核心问题是高并发下不超卖、不击穿。我的回答框架是:前端做按钮置灰和验证码限流;后端用Redis预减库存,先把库存预热到Redis,请求进来先减Redis库存,减成功才允许进入下单流程;下单消息扔到MQ异步处理,数据库层再做一次库存扣减,防止Redis记录和数据库不一致;最后用乐观锁update stock set version = version + 1 where id = ? and version = ?保证数据库不超卖。

短链系统的核心是长链接转短码的映射。生成短码有两种思路:一种是哈希,比如MD5或CRC32取前几位,然后用布隆过滤器防止冲突;另一种是发号器,利用全局唯一ID生成器生成自增ID,再转成62进制字符串。用户访问短链时,通过短码查映射表,拿到长链接后302重定向。稍微展开可以提到:要区分一次性短链和永久短链,要设计过期策略,要防止短链被封禁,要支持自定义短码。能够把这些场景聊清楚,比单纯背八股更能体现工程能力。

最后的最后,我想分享一个面试技巧:不要把自己当成"背八股的人",要当成"给面试官讲技术方案的人"。面试官问一个问题,你回答完后可以接一句"这块在实际项目中我曾经遇到过类似情况",然后自然而然地引出你自己的排查或优化经历。八股文只是入场券,真正让你从候选人里被记住的,是你把知识变成解决方案的能力。2023年的大厂Java面试依然很难,但把高频考点吃透、把每一条原理都落到真实场景里,你会发现所谓的八股文,其实就是技术成长的捷径。

http://www.cnnetsun.cn/news/4309649.html

相关文章:

  • Windows11专业版使用虚拟化技术安装Linux(CentOS7)
  • 用AI让AI更聪明:最小Agent的四大关键工程实践
  • GradCuit:信用分配梯度流如何增强大模型潜在空间推理
  • ComfyUI工作流从零搭建:从文生图到AI视频生成全攻略
  • CAD 2027零基础入门:安装、画图到出图全流程避坑指南
  • DeepSeek V4 Flash 接入 Codex 完整指南:配置、API Key与报错排查
  • Wasserstein距离度量下的ULA混合时间测量与Python实验
  • 中段面试制胜指南:二面三面与HR面全攻略
  • STM32U3 USBX设备开发:HAL PCD初始化“缺失”的真相与排查
  • Dubbo面试八股文:服务暴露、Nacos适配与性能调优全解析
  • Adapter+持续学习:恶意流量识别少样本增量更新的新思路
  • Goose AI Agent 入门指南:10 分钟装好跑通第一次会话,MCP 扩展 70+ 外部工具
  • 英伟达拟收购Hugging Face:AI模型分发与GPU推理生态将如何重塑
  • Starship 提示符 5 分钟上手:3 行配置改出你自己的终端提示符
  • BT 公共 Tracker 列表上手指南:选列表、配 qBittorrent、验证效果
  • 如何搭建 Gitea Actions 自动化流水线
  • OBS Studio 免费直播录制教程:从零搭场景到稳定开播
  • 3 分钟给 Windows 减重:Win11Debloat 卸载预装软件与隐私优化上手指南
  • 算力黑洞下的AI成本控制:大模型API选型与优化指南
  • DBeaver 插件安装与冲突排查完全指南:第三方扩展怎么选、怎么集成
  • Cloudflare Computer同步协议30分钟深入:从ChangeEntry到applyChanges全流程
  • Wi-Fi室内定位实战指南:不依赖UWB/蓝牙的低成本部署方案
  • 全桥峰值电流控制实战:LAT1319 Push-Pull模式斜坡补偿与调试
  • 一周入门大模型:从本地部署到LoRA微调完整路线
  • AI写代码的完整边界:从工具选型到本地部署实践指南
  • 工具调用不能只看演示效果
  • 文献综述怎么按主题分类而不是逐篇罗列:BunnyScholar三版综述生成实测
  • Docker中轻量化部署Windows X Lite完整指南:三步跑通容器
  • 如何用 3 条命令跑通 Expo:React Native 跨端开发的快速上手指南
  • STM32WL5MOC RTC走时偏差实测:从软件排错到硬件根因分析