腾讯天美一面:从 HashMap 到 Spring 事务,面试官这套连环问到底在考什么

最近在牛客上刷到一篇「腾讯天美一面」的面经,是 27 届秋招提前批期间(2026 年 7 月)的真实记录,Java 后端方向。评论区还有人问「楼主 AI 编程用的什么 skill」,能看出发帖时间很新。

这篇面经乍一看是一堆零散的八股:HashMap、ConcurrentHashMap、锁的类型、一个并发场景题、Spring 事务、动态代理。但把问题顺序摆在一起看会发现,面试官其实是顺着一条线在往下钻。从数据结构本身的线程不安全,问到怎么把它变安全,再问到安全的代价该怎么控制,最后落到业务代码里事务和锁到底该怎么写。背答案的人会在任何一个追问点卡住,理解这条线的人可以一路跟着往下讲。

把这几个问题拆开揉碎讲一遍,顺便把常见的过时说法纠正一下。

HashMap 和 ConcurrentHashMap,别再说分段锁了

这题很多人张口就是「ConcurrentHashMap 用分段锁保证线程安全」,这其实是 JDK 7 的老实现,JDK 8 之后已经彻底变了,还这么答会被追问一句就露馅。

JDK 7 的 ConcurrentHashMap 由多个 Segment 组成,每个 Segment 继承自 ReentrantLock,本质是一个小号的 HashMap,加锁粒度是整个 Segment(默认 16 个)。并发度上限就是 Segment 的数量,扩容也是分段扩,逻辑不算复杂但粒度粗。

JDK 8 把 Segment 整个砍掉了,改成跟 HashMap 一样的「数组 + 链表 + 红黑树」结构,线程安全靠 CAS + synchronized 联合实现:

  • 往空桶插入节点:用 CAS 直接抢,不用加锁;
  • 往非空桶插入(说明发生了哈希冲突):synchronized 锁住这个桶的首节点,别的线程只要不动这个桶就完全不受影响;
  • 链表长度达到 8 且数组容量已经 ≥ 64,才会把链表转成红黑树(TREEIFY_THRESHOLD = 8MIN_TREEIFY_CAPACITY = 64),红黑树节点数缩到 6 以下会退化回链表,避免频繁转换抖动;
  • 扩容时新增了一个 ForwardingNode,某个桶迁移完就在原位置放一个 ForwardingNode 当路标,这样多个线程可以并发协助扩容,而不是只能单线程一个个般;
  • size() 都不是靠一把全局锁数出来的,而是用 baseCount + CounterCell[] 分散计数再汇总,思路跟 LongAdder 一样,高并发下热点数据不要用一把锁去抢,分散开再汇总就行。

这套设计的方向很清楚:能不加锁就不加锁,必须加锁就把粒度切到最小。这也是这道题的第二问「ConcurrentHashMap 怎么平衡性能和安全」真正想听的答案,不是「因为它线程安全」这种正确的废话。

Java 里的锁,其实是好几个维度叠加的

面试官问「Java 中锁的类型有哪些」,容易答得东一榔头西一棒子。其实这是几个正交的维度,任何一把锁都能在每个维度上找到自己的位置:

  • 乐观锁 vs 悲观锁 :悲观锁认为一定会冲突,先加锁再操作(synchronizedReentrantLock);乐观锁认为大概率不冲突,操作时用版本号或 CAS 校验,冲突了再重试(AtomicInteger、数据库乐观锁字段)。
  • 公平锁 vs 非公平锁ReentrantLock 构造函数传 true 是公平锁,严格按请求顺序排队;默认非公平锁允许插队,吞吐量更高但可能饿死排队久的线程。synchronized 只有非公平这一种。
  • 可重入锁 vs 不可重入锁 :同一个线程能不能重复获取自己已经持有的锁。synchronizedReentrantLock 都可重入,这也是它们俩命名里都有意无意在强调的点。
  • 独占锁 vs 共享锁ReentrantLock 是独占的,同一时刻只有一个线程能拿到;ReentrantReadWriteLock 的读锁是共享的,允许多个线程同时读,写锁独占。读多写少的场景换成读写锁能明显提升并发度。
  • 偏向锁 / 轻量级锁 / 重量级锁 :这是 synchronized 自身的锁升级路径,没有竞争时是偏向锁,出现竞争后升级成轻量级锁(自旋 CAS),自旋失败太多次才会膨胀成靠操作系统互斥量实现的重量级锁。要注意偏向锁在 JDK 15 默认关闭、JDK 18 直接被移除了,现在的 JVM 上 synchronized 基本是从轻量级锁起步。

回答的时候按维度分组说,比堆砌名词更能体现出对锁的理解程度。

统计今日登录次数,怎么保证并发安全

这是一道典型的场景题:多个请求同时给同一个用户的今日登录次数加一,怎么保证不丢计数。常见的几种方案各有取舍:

  1. 数据库行级锁UPDATE user_login_count SET count = count + 1 WHERE user_id = ?,靠 InnoDB 的行锁天然串行化。简单可靠,但登录是高频操作,容易把这行数据变成热点,扛不住太高的 QPS。
  2. Redis INCRINCR 是原子操作,天然线程安全,配合 EXPIRE 设到当天 24 点自动过期,性能和实现复杂度都很友好,是这道题最常见的推荐答案。
  3. 本地锁 / 分布式锁 + 普通变量:先加锁再读改写,逻辑直白但性能最差,分布式锁本身还有额外的网络开销,纯粹是能用但不划算的方案,一般只在没有原子操作可用、又必须跨节点互斥时才考虑。

面试官真正想看的不是背出 Redis INCR 这四个字母,而是能不能讲出为什么数据库行锁在这个场景会成为瓶颈、为什么 Redis 的单命令原子性天然适合计数这层判断依据。

Spring 事务和代理,两个最容易踩的坑

@Transactional 能生效,前提是方法调用经过了 Spring 的代理对象。两个最常见的坑都是从这句话推出来的:

  • 同类自调用失效A 类里的 a() 方法内部直接调用同类的 b() 方法,即便 b() 标了 @Transactional,事务也不会生效,因为这次调用是 this.b(),压根没经过代理对象,AOP 切面自然拦不住。
  • 默认只回滚 RuntimeExceptionError :受检异常(比如自定义的 Exception 子类)默认不会触发回滚,除非显式配置 @Transactional(rollbackFor = Exception.class)。这个坑经常出现在业务逻辑抛了自定义异常、事务却没回滚、脏数据留在库里的排查现场。

代理这块顺带把 Spring 用的两种动态代理方式讲清楚:JDK 动态代理 基于接口,用 Proxy.newProxyInstance + InvocationHandler 生成实现类,前提是目标类必须实现至少一个接口;CGLIB 基于继承,在运行时用字节码生成目标类的子类并重写方法做拦截,能代理没有接口的类,但代理不了 final 类和 final 方法。Spring Boot 2.x 开始默认全部走 CGLIB,就是为了不管有没有接口都能统一处理。

事务传播机制,7 种里真正容易混的是这两种

Spring 事务传播机制一共 7 种:REQUIRED(默认,有事务就加入,没有就新建)、SUPPORTS(有事务就加入,没有就非事务运行)、MANDATORY(必须处在事务中,否则抛异常)、REQUIRES_NEW(无论如何都新建一个事务,原事务挂起)、NOT_SUPPORTED(非事务运行,原事务挂起)、NEVER(必须非事务运行,否则抛异常)、NESTED(有事务就作为嵌套事务运行,靠保存点实现)。

真正容易被面试官抓住深挖的是 REQUIRES_NEWNESTED 的区别:两者都能做到内层方法回滚不影响外层,但 REQUIRES_NEW 是彻底开了一个独立事务,内外事务互不相关,外层回滚不会牵连已经提交的内层;NESTED 本质还是同一个物理事务,内层只是打了个保存点,外层一旦整体回滚,内层的保存点也会被一起回滚掉。选错了,一旦触发回滚场景,数据一致性的表现完全不一样。

写在最后

这套问题串起来看,其实是在考一件事:能不能把线程安全从数据结构一路讲到业务代码,而不是把每个知识点当成孤立的问答对去背。备考的时候如果只是把这些问题的标准答案背下来,遇到任何一个追问都会露馅;反过来如果能讲清楚每个设计背后为什么这么做,追问越深答得越顺。我自己准备的时候习惯把这类真实面经丢进面灵 AI 的模拟面试功能里,让它接着面经里的问题往下追问,比自己对着答案背更容易发现哪里其实没吃透。

相关推荐
触底反弹2 小时前
面试被问 RAG 只能说出六个字?这篇用 100 行 Node.js 代码帮你彻底搞懂
javascript·人工智能·面试
浩瀚地学2 小时前
【Java基础复习】IO流(二)
java·开发语言·经验分享·笔记·学习
hdsoft_huge2 小时前
SpringBoot系列06:RESTful接口开发,请求参数接收、全局跨域CORS完整配置
java·spring boot
IVVi0jToe2 小时前
高效C++线程池设计与实现
java·c++·安全
用户852495071842 小时前
Agent 智能体到底是什么:一文讲透 ReAct 循环
面试
CRMEB定制开发2 小时前
企业级Java电商系统选型路线图:从零到上线全流程拆解
java·开发语言·商城系统·小程序商城
Zhou1411362 小时前
Java常见面试题3
java·开发语言
Solis2 小时前
百万并发的点赞计数,我们为什么要把“事实“和“计数“拆开?
java·架构
tachibana22 小时前
hot100 排序链表(148)
java·数据结构·算法·leetcode·链表