BIGO 后台开发一面面经:HashMap 扩容、线程池参数与 MySQL 锁
- 轮次
- 一面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
- 自我介绍。
- 项目深挖(帖内提到约占 15 分钟)。
八股
- HashMap 和 ConcurrentHashMap 讲一下?扩容机制分别是什么?
- 线程池的核心线程数和拒绝策略如何选择?
- MySQL 中乐观锁、悲观锁、唯一约束的区别是什么?
手撕
- 合并两个有序数组。
反问
- 今年秋招 HC 少,加上自身背景和实习经历一般,在众多候选人中想要通过面试,需要加强哪些地方?
(帖内提到:投递较晚,约面时已有较多人在泡池子;面完两天没有动静,帖主估计是凉了。)
《参考解析》
HashMap 与 ConcurrentHashMap 的扩容机制。 HashMap 的底层是数组 + 链表 + 红黑树(JDK 8 起链表长度到 8 且数组长度不小于 64 时树化,退化阈值是 6)。扩容的触发条件是元素个数超过容量乘以负载因子(默认 0.75),扩容为原容量的两倍,因为容量始终是 2 的幂,所以下标可以用 hash & (n-1) 位运算代替取模。JDK 8 的 rehash 有个很有名的优化:不需要重新计算每个 key 的 hash,只需要看 hash & oldCap 这一位是 0 还是 1,是 0 就留在原下标,是 1 就搬到「原下标 + oldCap」,这样把整条链表拆成两条(lo 链与 hi 链),并且保持相对顺序——这同时修掉了 JDK 7 头插法在并发扩容时可能形成环形链表的经典 bug,JDK 7 的并发扩容会导致 CPU 打满。要注意的坑:初始容量应预估为「预期元素数 / 0.75 向上取整到 2 的幂」,避免反复扩容;key 必须正确实现 hashCode 与 equals;HashMap 本身不是线程安全的,并发 put 会丢数据。
ConcurrentHashMap 在 JDK 8 里放弃了分段锁,改成「CAS + synchronized 锁桶头节点」:数组的每个槽位是并发的基本单位,put 时如果槽为空就用 CAS 写入,否则 synchronized 锁住该槽的头节点再操作链/树;扩容计数用 baseCount + CounterCell 数组分散热点(和 LongAdder 一个思路),size() 是估算值。并发扩容是它最有意思的设计:扩容时会把旧表的槽位逐个迁移,并给迁移过的槽放一个 ForwardingNode,其他线程遇到 ForwardingNode 就知道「正在扩容」,会调用 helpTransfer 一起搬数据,所以扩容是协作式的,不会阻塞整个表;并发扩容的迁移单位是「槽 + 步长 stride」,多线程各自领一段。JDK 7 的分段锁(Segment 继承 ReentrantLock,默认 16 段)并发度受段数限制,JDK 8 之后锁粒度细到桶,并发度更高、内存开销更小。回答时把「为什么要改」「CAS 与 synchronized 各自负责什么」「size 为什么是估算」「并发扩容怎样保证不丢数据」讲清,基本就满分了。
线程池的核心线程数与拒绝策略怎么选。 核心线程数与最大线程数、队列类型是联动的:任务提交后先看运行线程数是否小于 corePoolSize,是就新建线程;否则入队;队列满了才扩到 maximumPoolSize;再满就走拒绝策略。所以「队列选什么」会直接决定 maximumPoolSize 是否有意义——用无界 LinkedBlockingQueue 时队列永远不会满,最大线程数形同虚设,任务无限堆积直到 OOM,表现的典型症状是「服务不报错但请求全部超时」。核心线程数的经验公式:CPU 密集型取核数 + 1(减少线程切换),IO 密集型取核数 × (1 + 等待时间/计算时间),实践中更稳妥的做法是先按公式给一个初值,再用压测确认真实吞吐与 P99 延迟,而不是迷信公式。队列必须是有界队列并配一个合理的容量(容量决定能容忍多少突发)。拒绝策略四种:AbortPolicy 抛异常(默认,适合能感知失败并让上游重试的场景)、CallerRunsPolicy 由提交线程自己执行(天然形成背压,但可能阻塞调用方)、DiscardPolicy 静默丢弃、DiscardOldestPolicy 丢最老的(都可能丢任务,只适合可丢的日志类任务)。真实系统里更常用的做法是自定义拒绝策略:落库或入重试队列 + 打点告警,保证任务不悄无声息地消失。此外还要说清线程池隔离:按业务分池(避免一个慢接口拖垮所有接口)、给线程起有业务含义的名字、监控活跃线程数、队列深度与拒绝次数。
MySQL 里的乐观锁、悲观锁与唯一约束。 悲观锁假设冲突一定发生,在读取时就加锁:SELECT ... FOR UPDATE(写锁 / 排他锁)或 LOCK IN SHARE MODE(共享锁),InnoDB 下锁的是索引记录(行锁),不走索引会退化成锁全表,这是最常见的性能事故。它适合临界区较长、冲突概率高的场景,代价是持锁期间其他事务排队、可能死锁(要统一加锁顺序并处理死锁回滚)。乐观锁假设冲突少见,不在数据库层阻塞别人:用 version 字段做条件更新(UPDATE t SET v = v + 1, ... WHERE id = ? AND version = ?,看影响行数判断是否失败),或者用状态机式的条件更新(WHERE status = '待支付')。失败要有重试策略(有限次数 + 退避),并且业务必须能承受重试。唯一约束(唯一索引)与它们不是同一层级的东西:它是一条数据库层面的不变式,是「最终防线」——不管应用层的锁有没有失效、有没有跨服务并发、有没有消息重复投递,违反唯一约束的写入一定会失败。工程上的组合拳是:唯一索引保证正确性(比如订单号、用户 + 业务唯一键、防重复提交的幂等键),乐观锁处理并发更新,悲观锁只用在必须串行的短临界区。此外还要理解隔离级别(RR 下的 MVCC 快照读与当前读、间隙锁)对锁行为的影响——面试里经常顺着这道题问「FOR UPDATE 在什么情况下会锁到不存在的行」。
合并两个有序数组与手撕的答题节奏。 经典解法是双指针从后往前填:两个数组分别是 nums1(末尾有足够空位)与 nums2,用三个指针 i、j、k 分别指向 nums1 有效部分末尾、nums2 末尾、nums1 合并后末尾,每次比较取较大者放到 k 位置再左移,最后把 nums2 剩余元素补上,时间复杂度 O(m+n)、空间 O(1)。面试时先把「为什么从后往前」讲清楚(从前往前需要额外数组或频繁搬移),再写代码,最后主动过一遍边界:nums2 为空、nums1 有效长度为 0、有相等元素时的稳定性(相等时优先取 nums1 可以保持相对顺序)。手撕题在 50 分钟的面试里通常只占几分钟,评分点不只是「写出来」,还包括:先确认输入约束(是否原地、m 与 n 的语义、是否允许相等元素)、边写边说思路、写完自己举一个例子走一遍。八股部分同样如此——面试官问 HashMap 不是为了听定义,而是看你能否顺着追问答到「位运算扩容」「红黑树阈值」「并发扩容协作」这些点上。至于反问里问「如何提高通过率」,可以从「补齐项目中的量化成果与难点复盘」「针对目标部门的技术栈做定向准备」两个方向去问,比直接问「我是不是背景不行」更有效。