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 流程:
- 计算 hash 定位数组下标;
- 桶位为空直接放入;
- 桶位不为空:key 完全相等直接覆盖;树节点走红黑树插入;链表遍历到尾部追加;
- 插入完成判断是否达到扩容阈值,触发扩容。
扩容:容量翻倍,JDK8 不再全部重算 hash,通过hash & oldCap判断元素留在原下标,还是迁移到原下标+oldCap。
线程不安全:JDK7 并发 put 会环形链表死循环;JDK8 消除死循环,但依旧存在数据覆盖丢失。
追问:负载因子 0.75,是泊松分布下冲突概率和内存占用的权衡。
3. ConcurrentHashMap(JDK8)
JDK7:Segment 分段锁,Segment 继承 ReentrantLock,分段加锁。 JDK8:废弃 Segment,采用 CAS + synchronized 锁住桶头节点,锁粒度降低到链表 / 红黑树根节点,并发能力更高。底层结构和 HashMap 一致:数组 + 链表 + 红黑树。
put 逻辑:
- 数组未初始化先完成初始化;
- 桶位为 null,使用 CAS 尝试写入;
- 桶位有数据,synchronized 锁定桶头,执行链表 / 红黑树插入;
- 支持多线程协助扩容。
计数:size()使用baseCount基础计数 + counterCells竞争计数,求和得到总数,不保证强一致性;key、value 都不允许为 null。
面试重点:synchronized 经过锁升级优化性能提升,直接复用对象头 markword,不需要额外锁对象;不能完全替代 Hashtable,size、containsValue 这类聚合操作是弱一致。
4. Synchronized 锁升级
对象头 MarkWord 存储锁标记;锁只能升级,不能降级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。
- 偏向锁:适用于单线程反复获取锁。MarkWord 记录持有锁的线程 ID,获取锁不需要 CAS;出现竞争就撤销偏向锁。JDK15 默认关闭偏向锁。
- 轻量级锁:多线程交替使用锁,无真正并发竞争。线程在栈帧创建锁记录,CAS 替换对象 MarkWord;自旋抢锁,不阻塞,消耗 CPU。竞争加剧自旋失败,膨胀重量级锁。
- 重量级锁:向操作系统申请互斥锁;竞争失败的线程阻塞,进入内核态。
自适应自旋:JVM 动态调整自旋次数,不是固定数值。
5. 线程池原理与参数
七大构造参数:
corePoolSize核心线程数maximumPoolSize最大线程数keepAliveTime非核心线程空闲存活时间unit时间单位workQueue阻塞队列,存放等待任务threadFactory线程工厂,创建线程handler拒绝策略
任务执行流程:
- 任务到来,线程数小于核心线程数,新建核心线程执行;
- 核心线程已满,任务放入阻塞队列;
- 队列满,线程数小于最大线程数,创建非核心线程;
- 线程达到最大线程数,执行拒绝策略。
四种拒绝策略:
AbortPolicy:抛出异常,默认策略DiscardPolicy:直接丢弃任务,无异常DiscardOldestPolicy:丢弃队列队头任务,重新提交当前任务CallerRunsPolicy:由提交任务的调用者线程执行任务
面试必考点:禁止直接使用 Executors 工具类。
newFixedThreadPool无界队列容易 OOM;newCachedThreadPool最大线程无上限,线程暴涨 OOM;业务使用ThreadPoolExecutor手动构建。 默认核心线程不会回收;开启allowCoreThreadTimeOut=true,核心线程也会超时回收。
6. Spring Bean 的生命周期
- 实例化:通过构造函数反射创建对象,此时仅仅完成对象创建,属性未赋值。
- 属性填充 populateBean:依赖注入,给对象属性赋值。
- 初始化阶段
BeanPostProcessor#postProcessBeforeInitialization前置处理@PostConstruct注解方法InitializingBean#afterPropertiesSet()- xml 配置
init‑method指定方法 BeanPostProcessor#postProcessAfterInitialization后置处理,AOP 代理在这里生成
- Bean 就绪,供业务使用。
- 销毁阶段(容器关闭)
@PreDestroy注解方法DisposableBean#destroy()- xml 配置
destroy‑method指定销毁方法
记忆顺序:实例化 → 属性注入 → 前置后置处理器、@PostConstruct、afterPropertiesSet、init‑method → AOP 代理生成 → 使用 → 销毁。 注意:BeanPostProcessor 本身不会被 AOP 代理。
7. SpringBoot 启动流程,自动装配原理
SpringBoot 启动流程
调用SpringApplication.run()
- 实例化
SpringApplication:推断 web 应用类型;读取META‑INF/spring.factories,加载ApplicationContextInitializer、ApplicationListener。 - run 方法内部:
- 启动计时器;
- 获取监听器,发布应用启动事件;
- 构建 Environment,加载 application 配置文件;
- 创建 ApplicationContext 容器;
- 执行容器初始化器回调;
- 执行
refresh(),刷新 Spring 容器(Spring 核心流程); - 执行
ApplicationRunner、CommandLineRunner。
自动装配原理 @EnableAutoConfiguration
@EnableAutoConfiguration导入AutoConfigurationImportSelector;selectImports()读取META‑INF/spring.factories,拿到全部自动配置类全限定名;- 通过
@ConditionalOnClass、@ConditionalOnMissingBean条件注解,满足条件才创建 Bean。
SpringBoot3 变更:废弃 spring.factories,使用
META‑INF/spring/org.springframework.boot.autoconfigure.imports。
8. Mysql 索引失效
索引失效会触发全表扫描,explain 中 type 为 ALL。
like '%xxx',通配符 % 写在开头;like 'xxx%'索引有效。- 索引字段上使用函数、运算,例如
where date(create_time)='2026‑01‑01'。 - 隐式类型转换,字段和传入参数类型不一致。
- or 条件一侧字段没有索引,整个条件索引失效。
- 联合索引,不遵循最左前缀原则。
!=、not in、is not null不一定完全失效,数据占比高优化器放弃索引。- 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 排查
- 开启慢查询日志
slow_query_log,设置long_query_time阈值,记录执行超时 SQL。 show processlist查看正在运行的会话,观察耗时较高 SQL。explain分析执行计划,重点看 type、key、rows、Extra;type 尽量 ref/range 以上,ALL 代表全表扫描。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;计数等于 0 才删除锁 key。
- 看门狗 WatchDog 续期 :没有手动指定 leaseTime 时,获取锁成功开启后台定时任务,默认每 10s 续期锁过期时间(锁默认 30s),防止业务未执行完锁过期;手动指定过期时间看门狗失效。
- Lua 脚本保证原子性:加锁、释放锁全部使用 Lua 脚本执行,保证多条命令原子执行。
- 锁等待:拿不到锁订阅锁释放消息,阻塞等待,不是无限自旋。
- Redlock 红锁:多独立 Redis 节点,半数以上获取锁才算拿到锁;性能差,生产很少使用。
分布式锁四大要求:互斥、防死锁、可重入、高可用。
13. SortedSet 底层实现
两种编码:
- ziplist 压缩列表:元素少、score 与 member 内容较小时;连续存储 member 和 score,保持有序。
- 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 至少一次投递,不保证恰好一次。
解决方案:
- 业务幂等(主流方案) ,重复消费业务结果不变。
- 数据库唯一索引约束;
- 业务唯一 id 存入 Redis 标记已消费;
- 业务处理完成之后再提交 offset,禁止先提交 offset;
- 本地消息表,数据库事务保证。
不能依赖 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;
}