ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java五年面试高频考点:HashMap、JVM、并发与分布式锁全解析

Java五年面试高频考点:HashMap、JVM、并发与分布式锁全解析 最近帮一位准备跳槽的朋友做了一场模拟面试他简历上写着五年Java开发经验。我问了一个看起来特别基础的问题JDK 8 的 HashMap 里put 一个键值对的完整流转是怎样的他答得还算顺定位数组、挂链表、转红黑树都提到了。我接着追问扩容为什么是 2 倍不是 1.5 倍或者 3 倍他安静了几秒。我又问并发写入下ConcurrentHashMap 靠什么保证线程安全为什么不用全局锁他开始绕圈子。说实话那一刻我挺替他着急的因为对“五年开发”这个标签来说这些问题几乎是默认要过关的。这篇是“Java技术栈/面试题专题”的第一篇。我打算把五年开发面试里出现频率最高、区分度最大的几个方向整理出来集合类源码、JVM 内存与 GC、并发编程、Spring 事务与数据一致性、MySQL 索引与 SQL 调优再加上分布式锁和缓存一致性。不追求面面俱到只挑面试官反复追问、最能拉开差距的高频点。适合 2 到 5 年经验、准备跳槽想系统过一遍底子的 Java 开发也适合刚带团队、需要给新人压题库的老手直接拿去参考。1. 五年开发面试到底在考什么原理深度、排障能力与方案权衡1.1 面试官视角三年和五年的技术分水岭在哪里三年以内的面试核心是确认“你能干活”。问 API 用法、问框架怎么配、问踩过什么坑基本能判断一个人能不能胜任日常开发。但五年这个节点很特殊你已经是团队里的中坚力量面试官默认你会干活不再把会不会用框架当作主要考察点。他们真正想要确认的是三件事能不能讲清楚原理、能不能独立排查线上故障、能不能在规定约束下做技术选型和方案权衡。我面过不少候选人简历写得很满JVM、Redis、分布式消息队列样样都写。一问到“你们线上有没有出现过 FGC怎么定位的”就开始含糊。再问“如果让你设计一个分布式锁你会考虑哪些问题”就只会背 Redission 的看门狗。这种表现基本是一票否决的因为面试官会担心把你招进来之后线上出了问题你接不住。所以准备五年面试别再把时间花在背语法和 API 上。那些是初级阶段的功课。你需要把知识体系从“怎么用”升级到“为什么这样设计”和“出问题了怎么查”。1.2 复习优先级怎么排我的建议顺序准备跳槽的时间通常很紧完整过一遍八股文不太现实。我建议按下面这张表的优先级来分配精力把时间花在区分度最高的模块上考察方向典型追问问题区分度Java 集合源码HashMap 扩容机制、ConcurrentHashMap 并发控制高JVM 内存与 GCOOM 场景定位、GC 参数与日志分析高并发编程锁机制、线程池参数、并发 Bug 分析高Spring 事务事务失效场景、传播行为、分布式事务中高MySQL 索引与事务慢 SQL 优化、隔离级别与 MVCC高分布式锁与一致性Redis 锁的正确姿势、缓存一致性方案高注意这只是一个“专题(1)”的范围微服务治理、消息队列、容器化部署这些方向留着后续专题展开。先把这些高频点打透。1.3 最容易丢分的两种回答方式第一种是背题式回答。比如问“HashMap 什么时候转红黑树”标准答案是链表长度大于 8 且数组长度大于等于 64。但如果只背到这里面试官一追问“为什么是 8不是 16”就露馅了。第二种是只讲结论不讲推理。比如问“为什么扩容是 2 倍”回答“因为源码就是这样写的”这和没答没有区别。我给自己定的模拟面试标准是每个知识点都能从“底层原理”推到“业务影响”。比如 HashMap 扩容 2 倍这件事不仅要说出 2 倍还要能推导到位运算、rehash 优化、哈希分散性。后面我会逐个展开这种推导方式。2. 集合类源码细节HashMap、ArrayList、ConcurrentHashMap 的追问链2.1 HashMap 的 put 流程、扩容机制与“为什么是 2 的幂次”JDK 8 里 HashMap 的 put 流程可以拆成几步先对 key 做扰动计算也就是把 hashCode 高 16 位异或低 16 位目的是让高位也参与定位减少哈希碰撞然后用(n - 1) hash定位到数组下标这里用位运算而不是取模前提是 n 必须是 2 的幂次如果这个位置是空的直接放入如果不是空判断是链表还是红黑树分别插入如果链表长度超过 8 且数组长度达到 64就转为红黑树最后size超过当前阈值threshold就触发扩容。面试官真正想听的是扩容为什么是 2 倍。这里有个很精妙的设计当容量是 2 的幂次时hash % n等价于(n - 1) hash位运算比取模快。扩容时容量从 n 变成 2n(2n - 1) hash相比原来的(n - 1) hash只是在二进制高位上多取了一位。也就是说每个旧元素在新数组里的位置只有两种情况留在原下标或者移动到“原下标 旧容量”。判断方法就是看旧元素 hash 的新增那一位是 0 还是 1。这样一来扩容不需要重新计算每个 key 的哈希值只需要遍历旧链表按位判断后拆成两个链表分别放到新数组的两个位置即可。这个设计既减少了计算开销又保证了元素在扩容后仍然均匀分布。2.2 ArrayList 扩容、快速失败机制与循环删除陷阱ArrayList 的考察频率没有 HashMap 高但五年面试里经常作为热身题出现。它的底层是 Object 数组默认初始容量是 10。每次添加元素超出容量时会按oldCapacity (oldCapacity 1)扩容也就是 1.5 倍然后通过Arrays.copyOf把旧数组复制到新数组。这背后是System.arraycopy这个 native 方法。modCount是另一个必考点。它是集合结构被修改的次数记录。创建迭代器时迭代器会保存一个expectedModCount每次迭代时检查二者是否一致不一致就抛出ConcurrentModificationException。这就是快速失败机制。我可以给你一道经典的面试题在一个 ArrayList 里循环删除指定条件的元素为什么用for循环或foreach会出问题因为foreach底层用的就是迭代器删除后modCount变了下一次next()检查时直接抛异常。for循环则是因为删除元素后下标偏移会跳过相邻元素。正确的做法是使用Iterator.remove()或者从后往前倒序遍历删除。2.3 ConcurrentHashMap 的并发控制与选型依据并发环境下为什么不用 HashTable因为 HashTable 对整张表加锁并发度极低。JDK 7 的 ConcurrentHashMap 用的是分段锁默认 16 个 Segment把数据分散到多段每一段独立加锁并发度提升到 16。JDK 8 放弃了分段锁改成 CAS synchronized 的方式。JDK 8 的设计更细粒度插入时如果对应数组下标为空用 CAS 直接放进去不需要加锁如果不为空对这个桶的头节点加 synchronized 锁然后插入链表或树。锁只覆盖单个桶而不是整张表并发度比分段锁更高。size()方法也不是简单遍历加锁而是通过baseCount和CounterCell的累加机制来统计。面试追问链通常是这样的先问“ConcurrentHashMap 和 HashMap 的区别”再问“JDK 7 和 JDK 8 的实现差异”最后问“为什么 JDK 8 不用分段锁”。你如果能把每一个设计决策背后的性能考量讲出来这题基本就拿下了。3. JVM 内存与 GC五年 Java 开发最该补齐的底层课3.1 内存区域划分与 OOM 定位方向JVM 内存这块我一直建议大家先背清楚“哪些区域是线程私有的哪些是共享的”。程序计数器、虚拟机栈、本地方法栈是线程私有堆和方法区JDK 8 后变为元空间是线程共享。这个维度直接决定了 OOM 出现时该往哪个方向排查。堆内存不足异常信息是java.lang.OutOfMemoryError: Java heap space这是最常见的内存溢出。如果元空间不足报的是Metaspace。如果创建太多线程导致无法分配原生栈内存报的是unable to create new native thread这种情况往往不是堆的问题而是操作系统线程数限制。你把异常类型和内存区域对应起来排查方向就不会错。3.2 对象分配、对象晋升与 Stop The World对象优先在 Eden 区分配。Eden 区放不下时触发 Minor GC存活对象会复制到 Survivor 区。每经历一次 Minor GC 存活下来对象的年龄就加一。默认年龄达到 15 时会晋升到老年代这个阈值可以用-XX:MaxTenuringThreshold调整。老年代还有几种主动晋升的情况大对象直接进入老年代Survivor 区放不下存活对象时多余对象直接进入老年代动态年龄判断比如 Survivor 中相同年龄的所有对象大小总和超过 Survivor 空间一半时年龄大于或等于该年龄的对象直接晋升。GC 里的核心矛盾是 Stop The World。任何 GC 算法在执行时为了保证引用关系一致都要暂停用户线程。只是暂停时间长短不同。CMS 尝试并发标记来缩短停顿G1 则把堆划分为多个 Region通过追踪每个 Region 的回收价值和停顿时间目标来控制 GC 开销。3.3 生产环境 OOM 排查实战步骤这块是你的实战分水岭。我总结了一套可复制的排查链路先看应用日志确认 OOM 的具体类型和触发线程。用top -Hp pid看 CPU 占用率高的线程确认是不是 GC 线程异常。用jstat -gcutil pid 1000观察 GC 频率如果 FGC 频繁且老年代一直不降基本就是内存泄漏。用jmap -dump:formatb,fileheap.hprof pid导出堆快照然后丢到 MAT 或 VisualVM 里分析对象树和引用链。如果是线上环境优先使用jcmd、Arthas 这类对服务影响较小的工具必要时再考虑jmap。我见过一个典型的案例一个报表服务每隔几个小时就 FGC 一次每次都停顿好几秒。用 MAT 分析后发现是本地缓存用了HashMap存储查询结果key 是包含时间戳的字符串导致缓存永远不淘汰。这种情况下你就算把 JVM 参数调到再大也只是延缓问题。3.4 JVM 参数配置的实际经验线上 JVM 参数我习惯这样配-Xms4g -Xmx4g -Xmn2g -XX:MetaspaceSize256m -XX:MaxMetaspaceSize512m -XX:UseG1GC -XX:MaxGCPauseMillis200 -XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/data/logs/heapdump.hprof-Xms和-Xmx设成一样避免运行期堆扩容带来的抖动。新生代大小影响 Minor GC 的频率要结合对象存活率来调不要拍脑袋。G1 的停顿时间目标只是一个软目标实际效果要靠观察日志来验证。4. 并发编程的高频考点锁、线程池与不可忽略的坑4.1 synchronized 锁升级与 ReentrantLock 怎么选五年面试里synchronized 是必考中的必考。JDK 6 之后 synchronized 不是一上来就是重量级锁而是有一个锁升级过程无锁、偏向锁、轻量级锁、重量级锁。偏向锁是假设同一个线程会反复获取同一把锁所以只在 Mark Word 里记录线程 ID避免 CAS 开销。一旦有竞争升级为轻量级锁通过自旋等待持有锁的线程释放。如果自旋超过阈值或等待线程数过多就升级为重量级锁此时未获取锁的线程会进入操作系统内核的等待队列涉及用户态和内核态切换开销最大。面试官经常接着问“什么时候用 ReentrantLock”。答案不是“synchronized 是 JVM 层面的ReentrantLock 是 API 层面的”就完了。要说到点子上ReentrantLock 支持可中断获取锁、超时获取锁、公平锁模式以及多个 Condition 条件队列。synchronized 只支持非公平锁一个等待队列。实操中如果能用 synchronized 就优先用它已经很快了只有你需要“尝试获取锁失败就返回”“等待超时不阻塞”“精细控制多个条件变量”这些特性时才换 ReentrantLock。4.2 AQS不要只背原理要能讲出使用场景AQSAbstractQueuedSynchronizer是很多并发工具类的地基。它维护一个volatile int state和一个 CLH 同步队列通过模板方法定义获取锁和释放锁的框架把“什么条件下可以获取资源”留给子类实现。面试官一般不要求你把 AQS 源码背下来但要求你能解释 CountDownLatch、Semaphore、ReentrantLock 和 AQS 的关系。我通常用一个类比AQS 像一套餐厅叫号系统state 是剩余座位数同步队列是等位区每个来吃饭的线程都先看有没有座尝试获取没座就排队入队等待有人离席就叫下一个号释放唤醒。4.3 线程池参数设置从公式到业务经验线程池是并发里最容易踩坑的地方。核心参数是corePoolSize、maximumPoolSize、keepAliveTime、workQueue、threadFactory、handler。提交任务时流程是核心线程不满就直接创建线程满了先塞队列队列满了再扩展非核心线程还是不行就走拒绝策略。拒绝策略有四种默认 AbortPolicy 直接抛异常。简单场景可以用 CallerRunsPolicy让提交任务的线程自己去跑起到天然降级作用。DiscardPolicy 和 DiscardOldestPolicy 除非你有明确诉求否则不推荐。核心线程数怎么定网上流传的公式是 CPU 密集型 N1IO 密集型 2N。我的经验是公式只能当起点。实际要通过压测看三个指标队列积压量、线程活跃度、任务耗时分布。I/O 密集型场景因为线程在等网络、等数据库、等下游接口确实可以多开线程但 2N 不代表越大越好线程多了上下文切换成本也会上升。面试最常问的坑是为什么不能直接用 Executors 的工厂方法因为Executors.newFixedThreadPool用的是无界LinkedBlockingQueue任务无限堆积可能导致 OOM。newCachedThreadPool的最大线程数是Integer.MAX_VALUE极端情况下也能把线程耗尽。自定义线程池时一定要给有界队列并设置合理的拒绝策略。4.4 并发场景的三个经典坑第一个是 ArrayList 在多线程下 add 导致数据丢失或数组越界。因为扩容和赋值不是原子的两个线程同时扩容时相互覆盖。解决方案就是前面说的 CopyOnWriteArrayList 或者加锁。第二个是SimpleDateFormat线程不安全。底层Calendar的共享状态在处理日期时会被并发破坏。线上表现是解析出错误日期甚至抛NumberFormatException。解决方案是每次用DateTimeFormatter不可变、线程安全或配合 ThreadLocal。第三个是ThreadLocal内存泄漏。ThreadLocalMap 的 key 是弱引用value 是强引用如果线程长期存活value 无法被回收。解决方法是每次使用完finally里调用remove()。尤其是线程池场景线程复用会让这个问题被无限放大。5. Spring 事务与数据一致性从注解失效到分布式事务5.1 事务传播行为与隔离级别用业务场景串起来Spring 的Transactional注解人人会用但五年面试问的不是“怎么开事务”而是“传播行为和失效场景”。传播行为里必考的是REQUIRED和REQUIRES_NEW。REQUIRED 是默认值如果有事务就加入当前事务没有就新建。REQUIRES_NEW 则是无条件开启新事务挂起当前事务。最常见的业务场景是主流程更新订单同时调用一个独立的日志记录逻辑。如果日志失败不能回滚主流程就应该让日志方法用 REQUIRES_NEW。隔离级别这块直接回答“读已提交和可重复读的区别”还不够。要能说出来MySQL InnoDB 默认是可重复读但可重复读不能解决幻读只是 InnoDB 通过间隙锁和 MVCC 在特定条件下避免了幻读。如果要避免所有幻读需要把隔离级别提升到串行化但代价是并发能力大幅下降。实际项目中我建议先想清楚业务到底能不能接受幻读再决定隔离级别。5.2 事务失效的六大场景面试标准答案我把事务失效场景整理成一个清单面试时最好能一条条讲出来并配上你实际遇到过的情况自调用。同类内部this.method()调用走的是对象本身不是 Spring 代理事务注解不会生效。解决方式是注入自身代理或者拆到另一个 Bean。方法不是 public。Spring 默认用 CGLIB/JDK 动态代理只有通过代理调用的 public 方法才会被拦截。异常被 try-catch 吞掉。事务感知不到异常自然不会回滚。正确的做法是 catch 后记录日志并重新抛出或者手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。抛出的是检查型异常。Spring 默认只回滚 RuntimeException 和 Error。如果要让检查型异常也回滚需要显式配置rollbackFor。多线程调用。事务和当前线程绑定子线程里的数据库操作不在同一个事务上下文里事务无法覆盖。数据库引擎不支持事务。比如 MyISAM 就不支持事务这个比较老但面试冷不丁会问到。5.3 分布式事务与数据一致性常见方案怎么取舍做五年开发单机事务已经不是难点面试官更关注跨服务、跨库的数据一致性。分布式事务方向我建议重点掌握四个方案两阶段提交2PC/XA。强一致但同步阻塞、协调者单点、性能差实际业务很少直接用。TCCTry-Confirm-Cancel。把每个操作拆成预留、确认、取消三步适合资金类、库存类强一致场景但侵入性强、实现复杂。本地消息表。把消息表和业务操作放在同一个本地事务里然后通过定时任务投递消息。最终一致方案简单适合对延迟不敏感的场景。事务消息。RocketMQ 支持事务消息先发送半消息执行本地事务后再确认或回滚。比本地消息表更优雅但依赖消息中间件。这里要说清楚没有银弹每个方案都是“一致性、可用性、性能、复杂度”之间的权衡。面试时能结合自己的业务场景讲出取舍过程比背出十个方案名称有价值得多。另外MyBatis 在事务场景里也有面试点。#{}是预编译占位符能防止 SQL 注入${}是字符串拼接有注入风险只适合表名、排序字段这类不能预编译的场景。一级缓存默认开启作用域是 SqlSession二级缓存作用域是 namespace需要显式开启使用时要特别注意脏读问题。6. MySQL 索引与 SQL 调优慢查询排查的完整链路6.1 索引为什么选 B 树常见失效场景有哪些MySQL 这块五年面试的重点已经从“索引用什么数据结构”升级为“为什么是 B 树而不是 B 树、红黑树、哈希”。B 树的叶子节点用链表串联天然适合范围查询非叶子节点只存索引不存数据单页能放的键更多树更矮磁盘 IO 次数更少。这些点要串起来讲。索引失效的场景要背到形成肌肉记忆违反最左前缀原则对索引列使用函数或计算like %xxx左模糊导致无法走索引or连接非索引列隐式类型转换比如字符串列没加引号。我面试时会追问“你有没有真实遇到过隐式类型转换导致全表扫描”有案例可比没案例分数高一截。6.2 explain 执行计划哪些字段直接决定 SQL 的好坏拿到一条慢 SQL第一步永远是EXPLAIN。你得知道看哪些字段字段关注点type从好到坏const、eq_ref、ref、range、index、ALL尽量避免 ALLkey实际使用的索引为 NULL 说明没走索引rows预估扫描行数越小越好ExtraUsing filesort 说明排序没走索引Using temporary 说明用了临时表我最常让候选人看的是type和Extra。如果一条 SQL 的 type 是 ALL那第一反应不是加索引而是先看条件字段有没有索引如果 Extra 里出现 Using filesort说明 order by 字段和 where 条件不是联合索引里的良好组合。6.3 深分页与慢 SQL 优化深分页是生产环境最容易拖垮数据库的场景之一。LIMIT 1000000, 10的意思是扫描前 1000010 行再扔掉前 1000000 行扫描代价极高。优化的思路有几种如果业务允许用“上一页最大 ID”代替页码比如WHERE id ? ORDER BY id LIMIT 10如果非要分页用延迟关联先只扫主键再回表取数据。事务隔离级别方面InnoDB 的可重复读靠 MVCC 实现。MVCC 的原理是每一行有隐藏的版本号字段通过 undo log 保存历史版本配合 ReadView 决定当前事务能看到哪个版本。快照读走 MVCC不加锁当前读SELECT ... FOR UPDATE、LOCK IN SHARE MODE走的是锁机制配合间隙锁解决幻读。这块几乎是必考题我建议一定要理解版本链和 ReadView 的生成时机。7. 分布式锁与缓存一致性专题(1)的实践收尾7.1 Redis 分布式锁的正确姿势与看门狗机制5 年面试里分布式锁基本是收尾问题。它考察的不只是 Redis 命令而是你对“分布式环境下并发安全”的理解有多深。最基础的方案是用 SET NX EXSET lock_key unique_value NX EX 30但这只是表面工作。面试官真正要听的是细节锁为什么要设过期时间为了防止持有锁的进程崩溃导致死锁。为什么 value 要用随机值为了防止误删别人的锁解锁时要先校验 value 是自己的再用 Lua 脚本保证校验和删除的原子性。为什么要考虑锁续期因为业务可能超过 30 秒还没执行完锁过期了别人就能拿到同一把锁这就有并发安全问题。Redisson 的看门狗机制就是用来解决锁续期的。默认情况下加的锁会有一个 30 秒的过期时间后台有个守护线程会每隔一段时间检查锁是否还被持有如果持有就自动续期。这个机制能避免业务执行时间超过锁过期时间带来的问题但也不是万能的极端场景下还要考虑主从切换导致锁丢失这时会引申到 Redlock 的讨论。我不建议面试中直接背书说“Redlock 完美”因为业界对 Redlock 一直有争议更好的回答是讲清楚你们场景的容忍度。锁粒度也是高频追问点。比如“扣减库存时你是锁商品 ID 还是锁整个库存服务”正确答案是锁商品 ID粒度越小并发越大。我之前真见过有团队直接用一个全局加锁把所有商品的扣减请求都串行化导致大促时热点商品请求全部排队性能和业务上都不能接受。7.2 缓存与数据库一致性方案的取舍缓存一致性是另一个高频追问方向。面试官问“更新数据库后是先更新缓存还是先删缓存”正确的基线方案是 Cache Aside 模式读的时候先读缓存读不到再读数据库然后回填缓存写的时候先更新数据库再删除缓存。为什么是删除缓存而不是更新缓存因为更新缓存存在并发场景下的旧值覆盖问题而且如果每次写都更新缓存写入频繁时浪费严重。删除缓存则会触发下一次读取时回填成本更低。在此基础上面试官会追问“先删缓存再更新数据库”和“先更新数据库再删缓存”的区别。前者的问题是删完缓存后、更新数据库前有一个并发请求读不到缓存会把旧数据回填到缓存导致缓存里长期是旧值。解决方法是“延迟双删”先删缓存更新数据库等一小段时间再删一次缓存。但这个小段时间很难定得准。后者的问题是更新数据库后、删除缓存前如果删除失败也会出现旧值。这时可以引入 MQ 重试或者订阅 binlog 异步删除。这里我想说句实话缓存和数据库的强一致本来就做不到业界主流方案都是最终一致。面试时你要展现的是“我知道风险在哪并给出了可接受的补偿手段”而不是试图证明某个方案能让缓存和数据库永远一样。7.3 从这套题往后还有哪些方向要准备这个专题的定位是“5 年开发高频知识点”的第一篇。集合、JVM、并发、事务、MySQL、分布式锁和缓存一致性这些是面试里最常出现、也最能体现基本功的部分。把这一篇的内容吃透你已经能应付大部分一小时的面试。实际面试里还有几个方向会接着往下延伸消息队列的可靠性投递和顺序消费、分布式事务中的 Seata 细节、微服务场景下的服务发现与熔断降级、数据库分库分表后的全局 ID 方案。这些话题每个都可以单独开篇文章写我后面会陆续整理到专题里。最后说一个我的经验准备面试题不要只盯着“答案”背要把每个知识点当成一块积木面试官问任何一个问题你都能往上下游延伸。比如他问 HashMap你就可以延伸到哈希算法、并发容器、线程安全设计他问事务失效你就可以延伸到代理机制、分布式事务、数据一致性。知识串起来了面试自然就稳了。
返回列表