这篇文章整理一次快手外包 Java 一面的面试记录。整体难度不算偏,但覆盖面比较典型:两道基础算法题,加上一串 Java 后端八股,包括锁、线程池、Redis 持久化、MySQL 索引和 explain

如果是准备 Java 后端一面,这份题单很适合当作一次查漏补缺。

面试题目

算法

  1. 无重复字符的最长子串
  2. 反转链表

Java 八股

  1. Java 里的锁有哪些?区别是什么?
  2. ReentrantLock 的加锁过程是什么?
  3. synchronized 锁升级过程
  4. 线程池核心参数有哪些?拒绝策略有哪些?
  5. Redis 备份策略
  6. AOF 的三个刷盘级别
  7. MySQL 索引类型、索引失效、explain
  8. 最左匹配原则

算法一:无重复字符的最长子串

题目要求:给定一个字符串,找出其中不含重复字符的最长子串长度。

典型做法是滑动窗口。用两个指针维护一个窗口 [left, right],窗口内保证没有重复字符。遍历右指针,如果当前字符上一次出现的位置在窗口内,就把左指针移动到重复字符的后一位。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.HashMap;
import java.util.Map;

class Solution {
public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> lastIndex = new HashMap<>();
int left = 0;
int ans = 0;

for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
if (lastIndex.containsKey(c) && lastIndex.get(c) >= left) {
left = lastIndex.get(c) + 1;
}
lastIndex.put(c, right);
ans = Math.max(ans, right - left + 1);
}

return ans;
}
}

面试时可以重点说:

  • 时间复杂度是 O(n),每个字符最多被访问常数次。
  • 空间复杂度和字符集大小有关,通常可认为是 O(k)
  • 左指针只能向右移动,不能回退,否则会破坏窗口的线性复杂度。

算法二:反转链表

反转链表是链表基础题。最常见的是迭代法,用 prev 保存前一个节点,用 cur 遍历当前节点,每次把 cur.next 指向 prev

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class ListNode {
int val;
ListNode next;

ListNode(int val) {
this.val = val;
}
}

class Solution {
public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode cur = head;

while (cur != null) {
ListNode next = cur.next;
cur.next = prev;
prev = cur;
cur = next;
}

return prev;
}
}

回答时可以强调三个指针的作用:

  • prev:已经反转好的链表头。
  • cur:当前正在处理的节点。
  • next:临时保存下一个节点,避免断链。

Java 里的锁

Java 里常见的锁可以从不同维度分类。

synchronized 和 ReentrantLock

synchronized 是 JVM 层面的内置锁,使用简单,进入同步块自动加锁,退出同步块自动释放锁。发生异常时也会自动释放,不容易忘记解锁。

ReentrantLock 是 JUC 包里的显式锁,需要手动 lock()unlock(),通常要把 unlock() 放到 finally 里。它提供了更丰富的能力,比如可中断锁、尝试加锁、公平锁、多个条件队列。

1
2
3
4
5
6
7
8
ReentrantLock lock = new ReentrantLock();

lock.lock();
try {
// 临界区
} finally {
lock.unlock();
}

可以这样总结:

对比项 synchronized ReentrantLock
实现层面 JVM 内置 JDK 代码实现,基于 AQS
释放锁 自动释放 手动释放
可中断 不支持等待中断 支持 lockInterruptibly()
尝试加锁 不支持 支持 tryLock()
公平锁 不支持显式配置 支持公平锁和非公平锁
条件队列 一个对象一个等待队列 可创建多个 Condition

公平锁和非公平锁

公平锁会尽量按照线程等待顺序获取锁,避免饥饿,但吞吐量通常低一些。

非公平锁允许线程插队竞争锁,吞吐量更高,但极端情况下可能让某些线程等待更久。

ReentrantLock 默认是非公平锁:

1
2
ReentrantLock nonFairLock = new ReentrantLock();
ReentrantLock fairLock = new ReentrantLock(true);

可重入锁

可重入指同一个线程拿到锁以后,可以再次获取同一把锁,不会被自己阻塞。

synchronizedReentrantLock 都是可重入锁。可重入主要解决递归调用、同步方法之间互相调用时的死锁问题。

ReentrantLock 加锁过程

ReentrantLock 底层主要依赖 AQS。AQS 用一个 state 表示锁状态,用一个 CLH 变体队列管理等待线程。

以非公平锁为例,加锁过程大致是:

  1. 线程调用 lock()
  2. 先尝试通过 CAS 把 state0 改成 1
  3. 如果成功,说明当前线程获得锁,并记录独占线程。
  4. 如果失败,判断当前持锁线程是不是自己。
  5. 如果是自己,说明发生重入,state + 1
  6. 如果不是自己,线程会进入 AQS 等待队列。
  7. 等前驱节点释放锁后,当前节点再尝试获取锁。

释放锁时:

  1. 当前线程调用 unlock()
  2. AQS 把 state - 1
  3. 如果 state 还不为 0,说明只是释放了一层重入。
  4. 如果 state == 0,清空独占线程,并唤醒等待队列里的后继节点。

面试里可以抓住几个关键词:AQSstateCAS可重入等待队列LockSupport.park/unpark

synchronized 锁升级过程

synchronized 在 HotSpot 里经历过锁优化,常见说法是:无锁、偏向锁、轻量级锁、重量级锁。

不过需要注意:偏向锁在 JDK 15 之后被默认禁用,并且后续版本已经逐步移除相关机制。所以如果面试官问锁升级,一般还是按经典流程回答,同时补一句“新版本 JDK 对偏向锁有调整”会更稳。

经典锁升级过程:

  1. 无锁:对象刚创建时,没有线程进入同步块。
  2. 偏向锁:如果总是同一个线程获取锁,就把对象头 Mark Word 偏向这个线程,减少 CAS 操作。
  3. 轻量级锁:当出现其他线程竞争时,偏向锁撤销,线程通过 CAS 尝试获取锁,失败时会自旋。
  4. 重量级锁:如果竞争激烈或自旋失败,锁膨胀为重量级锁,线程阻塞,依赖操作系统互斥量。

一句话总结:锁升级是 JVM 为了降低加锁成本做的优化,从低竞争场景到高竞争场景逐步升级,升级后通常不会降级。

线程池参数和拒绝策略

Java 线程池最核心的类是 ThreadPoolExecutor

构造参数如下:

1
2
3
4
5
6
7
8
9
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
)

核心参数解释:

参数 含义
corePoolSize 核心线程数
maximumPoolSize 最大线程数
keepAliveTime 非核心线程空闲存活时间
unit 时间单位
workQueue 任务队列
threadFactory 线程工厂,用于命名线程等
handler 拒绝策略

线程池执行流程:

  1. 当前线程数小于核心线程数,直接创建核心线程执行任务。
  2. 核心线程已满,把任务放入阻塞队列。
  3. 队列也满了,且线程数小于最大线程数,创建非核心线程执行任务。
  4. 线程数达到最大线程数,执行拒绝策略。

常见拒绝策略:

策略 说明
AbortPolicy 默认策略,直接抛出 RejectedExecutionException
CallerRunsPolicy 由提交任务的线程自己执行
DiscardPolicy 直接丢弃任务,不抛异常
DiscardOldestPolicy 丢弃队列中最旧的任务,再尝试提交新任务

实际开发里一般不建议静默丢弃任务。更常见的是自定义拒绝策略,记录日志、打点监控、保存任务或做降级处理。

Redis 备份策略

Redis 常见持久化方式有两种:RDB 和 AOF。

RDB

RDB 是快照持久化,会在某个时间点生成 Redis 数据的二进制快照文件。

优点:

  • 文件紧凑,适合备份和灾难恢复。
  • 恢复速度通常较快。
  • 对主线程影响相对可控,通常由子进程执行持久化。

缺点:

  • 两次快照之间的数据可能丢失。
  • fork 子进程时可能带来短暂性能抖动。

适合场景:定期全量备份、冷备、数据恢复。

AOF

AOF 会把写命令追加到日志文件中,Redis 重启时通过重放 AOF 恢复数据。

优点:

  • 数据安全性更高。
  • 可读性比 RDB 好,日志是 Redis 协议格式。
  • 支持 AOF rewrite,避免文件无限增长。

缺点:

  • 文件通常比 RDB 大。
  • 恢复速度可能比 RDB 慢。
  • 刷盘频率越高,对性能影响越明显。

混合持久化

Redis 4.0 之后支持 RDB + AOF 混合持久化。AOF rewrite 后的新文件前半部分是 RDB 快照,后半部分是增量 AOF 命令。

它结合了两者优点:恢复速度接近 RDB,数据完整性接近 AOF。

AOF 的三个级别

AOF 的刷盘策略由 appendfsync 控制,常见有三个级别。

策略 含义 优点 缺点
always 每次写命令都刷盘 数据安全性最高 性能最差
everysec 每秒刷盘一次 性能和可靠性均衡 最多丢失约 1 秒数据
no 交给操作系统决定刷盘 性能最好 数据丢失风险最大

生产环境最常见的是 everysec。如果业务对数据安全要求极高,可以考虑 always,但要接受性能下降。

MySQL 索引类型

MySQL 索引可以从不同维度分类。

按数据结构分:

  • B+Tree 索引:InnoDB 默认索引结构,适合范围查询、排序、等值查询。
  • Hash 索引:Memory 引擎支持,InnoDB 的自适应 Hash 索引由数据库内部维护。
  • FullText 全文索引:适合文本检索。
  • R-Tree 空间索引:适合 GIS 空间数据。

按逻辑功能分:

  • 主键索引:PRIMARY KEY,唯一且非空。
  • 唯一索引:UNIQUE,保证字段值唯一。
  • 普通索引:普通加速查询。
  • 联合索引:多个字段组成一个索引。
  • 覆盖索引:查询字段都能从索引中拿到,不需要回表。

InnoDB 中还要区分聚簇索引和二级索引:

  • 聚簇索引:叶子节点存放整行数据,一张表只能有一个,通常是主键。
  • 二级索引:叶子节点存放主键值,查询到主键后可能需要回表。

索引失效场景

常见索引失效情况:

  1. 对索引列使用函数,例如 where date(create_time) = '2026-05-07'
  2. 对索引列做计算,例如 where age + 1 = 20
  3. 字符串类型不加引号,发生隐式类型转换。
  4. 使用前置模糊查询,例如 like '%abc'
  5. 联合索引不满足最左匹配原则。
  6. or 两边不是都能走索引。
  7. 范围查询后面的联合索引字段无法继续用于精确匹配。
  8. 数据量太小,或优化器判断全表扫描成本更低。
  9. 查询条件选择性太差,例如性别字段单独建索引。

这里要注意:索引“失效”不是说索引文件不存在,而是优化器没有使用它,或者只使用了联合索引的一部分。

explain 怎么看

explain 用来查看 SQL 的执行计划。面试里重点关注这几个字段。

字段 关注点
id 查询执行顺序,值越大优先级越高
select_type 查询类型,比如 SIMPLE、PRIMARY、SUBQUERY
table 当前访问的表
type 访问类型,判断查询效率的重要字段
possible_keys 可能使用的索引
key 实际使用的索引
key_len 使用索引的长度,可判断联合索引用到了几列
rows 预估扫描行数
filtered 条件过滤比例
Extra 额外信息,比如 Using index、Using filesort

type 的常见顺序大致是:

1
system > const > eq_ref > ref > range > index > ALL

通常来说,至少要尽量达到 rangeref,如果出现 ALL,就要重点关注是否全表扫描。

Extra 中常见信息:

  • Using index:使用覆盖索引,通常是好事。
  • Using where:存储引擎返回数据后,Server 层还要过滤。
  • Using filesort:需要额外排序,可能需要优化索引。
  • Using temporary:使用临时表,常见于复杂排序、分组。

最左匹配原则

最左匹配原则主要针对联合索引。

假设有联合索引:

1
KEY idx_user_age_city (user_name, age, city)

能较好使用索引的查询:

1
2
3
select * from user where user_name = 'Tom';
select * from user where user_name = 'Tom' and age = 18;
select * from user where user_name = 'Tom' and age = 18 and city = 'Beijing';

不能完整使用联合索引的查询:

1
2
3
select * from user where age = 18;
select * from user where city = 'Beijing';
select * from user where age = 18 and city = 'Beijing';

因为它们没有从联合索引的最左列 user_name 开始匹配。

再看范围查询:

1
2
3
4
select * from user
where user_name = 'Tom'
and age > 18
and city = 'Beijing';

这里通常能用到 user_nameage,但 city 很可能无法继续用于索引过滤。可以简单记成:联合索引从左到右匹配,遇到范围查询后,后面的字段使用能力会明显下降。

设计联合索引时,一般把区分度高、经常用于等值查询、排序或分组的字段放在前面。但最终还是要结合 SQL 场景和 explain 结果判断。

面试回答思路

这次面试题的特点是“基础但宽”。每个点不用讲到源码级别,但要能讲清楚主线。

算法题重点是:

  • 先说思路,再写代码。
  • 写完主动说复杂度。
  • 链表题注意不要断链。

Java 锁重点是:

  • synchronizedReentrantLock 的区别。
  • ReentrantLock 要提 AQS、CAS、state、等待队列。
  • 锁升级要讲清楚从低竞争到高竞争的变化。

线程池重点是:

  • 七个核心参数。
  • 任务提交流程。
  • 四种拒绝策略。
  • 生产环境避免使用无界队列和静默丢弃。

Redis 重点是:

  • RDB 适合快照备份。
  • AOF 数据安全性更高。
  • appendfsync everysec 是常见折中。
  • 混合持久化兼顾恢复速度和数据完整性。

MySQL 重点是:

  • InnoDB 默认 B+Tree 索引。
  • 聚簇索引和二级索引的区别。
  • 索引失效场景。
  • explain 重点看 typekeyrowsExtra
  • 联合索引遵循最左匹配原则。

总结

快手外包一面的题目比较标准,主要考察候选人对 Java 后端基础的熟练程度。算法题是 LeetCode 高频基础题,八股部分集中在并发、缓存、数据库三个方向。

如果准备类似面试,建议把每个知识点都整理成“定义 + 原理 + 使用场景 + 常见坑”的结构。这样面试官追问时,回答不会只停留在背概念,而是能顺着一个清晰的技术主线往下讲。