重生——第十一次面试之挖财一面2026.8.19已OC

1. ArrayList 与 LinkedList

ArrayList 底层是Object[]数组,默认初始容量 10;扩容为原容量 1.5 倍,通过Arrays.copyOf完成数组拷贝。支持随机访问,查询 O (1);中间插入、删除需要移动数组元素,效率低;线程不安全,允许存 null,元素有序。

LinkedList 底层双向链表,节点维护 prev、next、value。头尾增删 O (1);随机访问需要遍历链表,get (i) 时间复杂度 O (n);线程不安全,允许 null,元素有序。

面试要点:遍历优先 ArrayList;频繁头尾增删选 LinkedList;禁止 for 循环中get(i)遍历 LinkedList;大数据量 ArrayList 建议指定初始容量减少扩容。

2. HashMap(JDK8)

底层结构:数组 + 链表 + 红黑树 核心参数:默认容量 16 ,扩容阈值 = 容量 × 负载因子;链表长度>8,且数组长度≥64,链表转为红黑树;红黑树节点数量小于 6 退化成链表。

hash 计算:(h = key.hashCode()) ^ (h >>> 16),高低 16 位异或,减少 hash 冲突。

put 流程:

  1. 计算 hash 定位数组下标;
  2. 桶位为空直接放入;
  3. 桶位不为空:key 完全相等直接覆盖;树节点走红黑树插入;链表遍历到尾部追加;
  4. 插入完成判断是否达到扩容阈值,触发扩容。

扩容:容量翻倍,JDK8 不再全部重算 hash,通过hash & oldCap判断元素留在原下标,还是迁移到原下标+oldCap

线程不安全:JDK7 并发 put 会环形链表死循环;JDK8 消除死循环,但依旧存在数据覆盖丢失。

追问:负载因子 0.75,是泊松分布下冲突概率和内存占用的权衡。

3. ConcurrentHashMap(JDK8)

JDK7:Segment 分段锁,Segment 继承 ReentrantLock,分段加锁。 JDK8:废弃 Segment,采用 CAS + synchronized 锁住桶头节点,锁粒度降低到链表 / 红黑树根节点,并发能力更高。底层结构和 HashMap 一致:数组 + 链表 + 红黑树。

put 逻辑:

  1. 数组未初始化先完成初始化;
  2. 桶位为 null,使用 CAS 尝试写入;
  3. 桶位有数据,synchronized 锁定桶头,执行链表 / 红黑树插入;
  4. 支持多线程协助扩容。

计数:size()使用baseCount基础计数 + counterCells竞争计数,求和得到总数,不保证强一致性;key、value 都不允许为 null。

面试重点:synchronized 经过锁升级优化性能提升,直接复用对象头 markword,不需要额外锁对象;不能完全替代 Hashtable,size、containsValue 这类聚合操作是弱一致。

4. Synchronized 锁升级

对象头 MarkWord 存储锁标记;锁只能升级,不能降级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。

  1. 偏向锁:适用于单线程反复获取锁。MarkWord 记录持有锁的线程 ID,获取锁不需要 CAS;出现竞争就撤销偏向锁。JDK15 默认关闭偏向锁。
  2. 轻量级锁:多线程交替使用锁,无真正并发竞争。线程在栈帧创建锁记录,CAS 替换对象 MarkWord;自旋抢锁,不阻塞,消耗 CPU。竞争加剧自旋失败,膨胀重量级锁。
  3. 重量级锁:向操作系统申请互斥锁;竞争失败的线程阻塞,进入内核态。

自适应自旋:JVM 动态调整自旋次数,不是固定数值。

5. 线程池原理与参数

七大构造参数:

  1. corePoolSize核心线程数
  2. maximumPoolSize最大线程数
  3. keepAliveTime非核心线程空闲存活时间
  4. unit时间单位
  5. workQueue阻塞队列,存放等待任务
  6. threadFactory线程工厂,创建线程
  7. handler拒绝策略

任务执行流程:

  1. 任务到来,线程数小于核心线程数,新建核心线程执行;
  2. 核心线程已满,任务放入阻塞队列;
  3. 队列满,线程数小于最大线程数,创建非核心线程;
  4. 线程达到最大线程数,执行拒绝策略。

四种拒绝策略:

  1. AbortPolicy:抛出异常,默认策略
  2. DiscardPolicy:直接丢弃任务,无异常
  3. DiscardOldestPolicy:丢弃队列队头任务,重新提交当前任务
  4. CallerRunsPolicy:由提交任务的调用者线程执行任务

面试必考点:禁止直接使用 Executors 工具类。newFixedThreadPool无界队列容易 OOM;newCachedThreadPool最大线程无上限,线程暴涨 OOM;业务使用ThreadPoolExecutor手动构建。 默认核心线程不会回收;开启allowCoreThreadTimeOut=true,核心线程也会超时回收。

6. Spring Bean 的生命周期

  1. 实例化:通过构造函数反射创建对象,此时仅仅完成对象创建,属性未赋值。
  2. 属性填充 populateBean:依赖注入,给对象属性赋值。
  3. 初始化阶段
    • BeanPostProcessor#postProcessBeforeInitialization 前置处理
    • @PostConstruct注解方法
    • InitializingBean#afterPropertiesSet()
    • xml 配置init‑method指定方法
    • BeanPostProcessor#postProcessAfterInitialization后置处理,AOP 代理在这里生成
  4. Bean 就绪,供业务使用。
  5. 销毁阶段(容器关闭)
    • @PreDestroy注解方法
    • DisposableBean#destroy()
    • xml 配置destroy‑method指定销毁方法

记忆顺序:实例化 → 属性注入 → 前置后置处理器、@PostConstruct、afterPropertiesSet、init‑method → AOP 代理生成 → 使用 → 销毁。 注意:BeanPostProcessor 本身不会被 AOP 代理。

7. SpringBoot 启动流程,自动装配原理

SpringBoot 启动流程

调用SpringApplication.run()

  1. 实例化SpringApplication:推断 web 应用类型;读取META‑INF/spring.factories,加载ApplicationContextInitializerApplicationListener
  2. run 方法内部:
    1. 启动计时器;
    2. 获取监听器,发布应用启动事件;
    3. 构建 Environment,加载 application 配置文件;
    4. 创建 ApplicationContext 容器;
    5. 执行容器初始化器回调;
    6. 执行refresh(),刷新 Spring 容器(Spring 核心流程);
    7. 执行ApplicationRunnerCommandLineRunner

自动装配原理 @EnableAutoConfiguration

  1. @EnableAutoConfiguration导入AutoConfigurationImportSelector
  2. selectImports()读取META‑INF/spring.factories,拿到全部自动配置类全限定名;
  3. 通过@ConditionalOnClass@ConditionalOnMissingBean条件注解,满足条件才创建 Bean

SpringBoot3 变更:废弃 spring.factories,使用META‑INF/spring/org.springframework.boot.autoconfigure.imports

8. Mysql 索引失效

索引失效会触发全表扫描,explain 中 type 为 ALL。

  1. like '%xxx',通配符 % 写在开头;like 'xxx%'索引有效。
  2. 索引字段上使用函数、运算,例如where date(create_time)='2026‑01‑01'
  3. 隐式类型转换,字段和传入参数类型不一致。
  4. or 条件一侧字段没有索引,整个条件索引失效。
  5. 联合索引,不遵循最左前缀原则
  6. !=not inis not null不一定完全失效,数据占比高优化器放弃索引。
  7. MySQL 优化器评估,全表扫描比走索引更快,主动放弃索引。

排查:使用 explain 看执行计划。

9. 隐式转换

MySQL 做=><in比较时,两边字段类型不一致,MySQL 自动做类型转换。 示例:user_id是 varchar 索引字段,where user_id = 1001,字符串和数字对比,字符串会被转为数字,等价于where cast(user_id as signed)=1001,索引字段被函数包裹,索引失效。

实操规范:字符串类型字段,条件值加引号;数字字段传入数字,避免类型错位。

10. 索引下推 ICP(Index Condition Pushdown)

MySQL5.6 特性,仅支持二级索引 。 没有 ICP:存储引擎拿到索引记录,回表读取完整行数据,在 MySQL Server 层过滤 where 条件。 开启 ICP:存储引擎层,直接利用索引内的字段过滤条件,不满足条件直接丢弃,减少回表次数

适用场景:联合索引,where 中部分条件属于索引列,但不能用于定位,只能用来过滤。 explain 中 Extra 字段输出Using index condition代表索引下推生效。

ICP 不能替代最左前缀原则,只是减少回表。

11. 线上慢 SQL 排查

  1. 开启慢查询日志slow_query_log,设置long_query_time阈值,记录执行超时 SQL。
  2. show processlist查看正在运行的会话,观察耗时较高 SQL。
  3. explain分析执行计划,重点看 type、key、rows、Extra;type 尽量 ref/range 以上,ALL 代表全表扫描。
  4. show profile观察 SQL 各个执行阶段耗时;performance_schema监控数据库运行 SQL。

优化方向: 建立合适索引,规避索引失效;禁止 select *;大分页做延迟关联主键分页;大表分库分表;业务层增加缓存减轻 DB 压力。

注意:explain 不要在线上业务流量下执行,复制 SQL 到测试环境分析。

12. Redis 常见数据结构

类型 底层编码 业务场景
String SDS 简单动态字符串 缓存、计数器、分布式锁
List quicklist 简易消息队列、栈队列
Hash ziplist / hashtable 存储对象,用户信息
Set intset / hashtable 去重、交集并集
ZSet(SortedSet) ziplist / 跳表 skiplist 排行榜、延时队列

小数据量使用压缩列表 ziplist 节省内存;超过阈值自动切换为哈希、跳表。

13. Redisson 分布式锁的数据结构与底层原理

数据结构:Hash。key 为锁名;hash key 是UUID:线程ID,value 为锁重入计数。

核心原理:

  1. 可重入锁:同一个线程多次加锁,计数 + 1;释放锁计数‑1;计数等于 0 才删除锁 key。
  2. 看门狗 WatchDog 续期 :没有手动指定 leaseTime 时,获取锁成功开启后台定时任务,默认每 10s 续期锁过期时间(锁默认 30s),防止业务未执行完锁过期;手动指定过期时间看门狗失效
  3. Lua 脚本保证原子性:加锁、释放锁全部使用 Lua 脚本执行,保证多条命令原子执行。
  4. 锁等待:拿不到锁订阅锁释放消息,阻塞等待,不是无限自旋。
  5. Redlock 红锁:多独立 Redis 节点,半数以上获取锁才算拿到锁;性能差,生产很少使用。

分布式锁四大要求:互斥、防死锁、可重入、高可用。

13. SortedSet 底层实现

两种编码:

  1. ziplist 压缩列表:元素少、score 与 member 内容较小时;连续存储 member 和 score,保持有序。
  2. skiplist 跳表:数据量超过阈值切换;同时维护一张哈希表,O (1) 通过 member 获取 score;跳表负责排序、范围查询。

zset = 跳表做有序遍历 + hash 映射快速查 score。

14. 跳表 skiplist

多层有序链表,底层是完整有序链表;上层是下层的索引,插入节点随机生成层数。 查询:从最高层索引向右遍历,数值大于目标就向下一层,时间复杂度 O (logn)。 插入:随机生成节点层数,更新多层链表指针。 删除:逐层删除对应节点。

对比红黑树:实现简单,范围遍历性能优异;缺点是索引带来额外内存开销。Redis zset 选用跳表。

15. 拷打实习

16. 消息推模式和拉模式

推模式 Push:Broker 主动把消息推送给消费者。 优点:实时性高;缺点:Broker 推送速率不受消费者控制,消费能力不足会压垮消费者;代表:ActiveMQ。

拉模式 Pull:消费者主动请求 Broker 拉取消息。 优点:消费速率由消费者自己控制,不会被压垮;缺点:轮询会带来延迟,空轮询消耗 CPU;代表:Kafka、RocketMQ。

Kafka 长轮询优化:没有消息的时候,Broker hold 住请求,有消息到达或者超时才返回,兼顾实时性与性能。

17. MQ 重复消费

根本原因:业务处理完成,offset 提交失败,Broker 重新投递消息。MQ 只保证At‑least‑once 至少一次投递,不保证恰好一次

解决方案:

  1. 业务幂等(主流方案) ,重复消费业务结果不变。
    • 数据库唯一索引约束;
    • 业务唯一 id 存入 Redis 标记已消费;
  2. 业务处理完成之后再提交 offset,禁止先提交 offset;
  3. 本地消息表,数据库事务保证。

不能依赖 MQ 保证不重复,网络抖动不可避免,业务层必须做幂等。

18. 拷打项目

19.手搓:

1. 环形链表 判断链表是否有环

复制代码
class ListNode{
    int val;
    ListNode next;
}
//快慢指针
public boolean hasCycle(ListNode head){
    ListNode fast=head,slow=head;
    while(fast!=null&&fast.next!=null){
        fast=fast.next.next;
        slow=slow.next;
        if(fast==slow) return true;
    }
    return false;
}

2.LRU 缓存

双向链表维护访问顺序,HashMap 做映射;get、put O (1)

复制代码
class LRUCache {
    static class Node{
        int key,val;
        Node prev,next;
        Node(int k,int v){key=k;val=v;}
    }
    private Map<Integer,Node> map=new HashMap<>();
    private Node head,tail;
    private int capacity;

    public LRUCache(int cap){
        capacity=cap;
        head=new Node(-1,-1);
        tail=new Node(-1,-1);
        head.next=tail;
        tail.prev=head;
    }
    private void remove(Node node){
        node.prev.next=node.next;
        node.next.prev=node.prev;
    }
    private void addToHead(Node node){
        node.next=head.next;
        node.prev=head;
        head.next.prev=node;
        head.next=node;
    }
    private void moveToHead(Node node){
        remove(node);
        addToHead(node);
    }
    private Node removeTail(){
        Node del=tail.prev;
        remove(del);
        return del;
    }

    public int get(int key) {
        if(!map.containsKey(key)) return -1;
        Node node=map.get(key);
        moveToHead(node);
        return node.val;
    }

    public void put(int key, int value) {
        if(map.containsKey(key)){
            Node node=map.get(key);
            node.val=value;
            moveToHead(node);
        }else{
            Node newNode=new Node(key,value);
            map.put(key,newNode);
            addToHead(newNode);
            if(map.size()>capacity){
                Node del=removeTail();
                map.remove(del.key);
            }
        }
    }
}

3. 反转链表

复制代码
class ListNode{
    int val;
    ListNode next;
}
//迭代
public ListNode reverseList(ListNode head) {
    ListNode pre=null;
    ListNode cur=head;
    while(cur!=null){
        ListNode next=cur.next;
        cur.next=pre;
        pre=cur;
        cur=next;
    }
    return pre;
}

//递归版本
public ListNode reverse(ListNode head){
    if(head==null||head.next==null) return head;
    ListNode newHead=reverse(head.next);
    head.next.next=head;
    head.next=null;
    return newHead;
}
相关推荐
码匠许师傅1 小时前
【C++ 面试真题】26. 聊聊 C++ 的智能指针
java·c++·面试
倔强的石头1061 小时前
向量数据库从相似度检索走向融合数据底座
数据库
To_OC1 小时前
啃完 TS 工具类型我发现:Pick 和 Omit 原来就是一层窗户纸
前端·面试·typescript
AI绘画哇哒哒1 小时前
【建议收藏!】35岁后端血泪忠告,这3类人别硬转Agent(过来人亲述)
java·人工智能·后端·ai·程序员·大模型·agent
最强小杰2 小时前
gpt-5.6-sol 频繁报 503 怎么办?区分容量熔断和限速 429 的排查方法 + 可复用 retry wrapper
java·人工智能·gpt·ai
今天AI了吗2 小时前
Python 基础语法(一):常量、变量、输入输出与运算符
开发语言·数据库·人工智能·python·sql·深度学习·机器学习
liuyicenysabel3 小时前
多服务上线日记三:
运维·服务器·笔记·学习
重生之后端学习4 小时前
283. 移动零[简单]✅
开发语言·数据结构·算法·leetcode·职场和发展