Java后端面试核心知识体系
本文专为Java后端面试求职者制作的面试经典题汇总,覆盖95%的高频面试题。
以下的问题通过"问题 + 要点 + 回答"的格式展现,请通过文章目录快速找到对应问题。
Java 基础
1. hashCode() 与 equals()
问题:请你聊一聊 hashCode() 和 equals() 这两个方法。
要点:
- 二者来源。
- 各自功能。
- 重写
equals()必须重写hashCode()的原因。
回答:
- 来源 :两个方法均来自
Object类。 - 功能 :
equals():用于判断两个对象在逻辑上是否相等。默认实现为==,比较内存地址,通常根据业务逻辑重写,按照对象实际内容判断。hashCode():返回对象的哈希码(int值)。默认实现通常与内存地址相关,通常也是根据业务逻辑重写,重写是为了与equals()保持一致。
- 重写原因 :Java约定,当
equals()判定两个对象相等时,它们的hashCode()返回的哈希码也必须相等。如果不遵守,在哈希集合(如HashSet、HashMap)中,相等的对象可能被存入不同桶位,导致逻辑错误。例如,HashSet先通过hashCode()定位桶的位置,若两个相等对象hashCode()不同,则会判定为不同对象,无法正确查找。
2. ArrayList 与 LinkedList
问题:请你聊一聊ArrayList和LinkedList的区别。
要点:
- 与List、Collection接口的关系。接口来源
- 查询效率及原因。
- 增删效率及原因。
- 内存使用
回答 :
-
接口关系 :两者都是
List接口的实现类,都实现了Collection接口。ArrayList实现了RandomAccess接口,支持快速随机访问;LinkedList实现了Deque接口,可作为双端队列使用。 -
查询效率:
ArrayList:时间复杂度 O(1)。基于数组,内存连续,支持通过索引直接定位。LinkedList:时间复杂度 O(n)。基于双向链表,内存不连续,需从头/尾遍历查找。其内部优化了遍历策略,最坏情况为 n/2,但整体仍为 O(n)。
- 增删效率 :
ArrayList:尾部操作均摊 O(1) ;中间操作 O(n),需移动后续元素。LinkedList:头尾操作 O(1) ;中间操作本身 O(1) ,但定位到中间位置需 O(n),总体为 O(n)。
- 内存 :
ArrayList更节省空间,但可能预占内存;LinkedList每个节点需额外存储前后指针,内存开销约为ArrayList的 2-3 倍。
3. HashMap 底层原理
问题:请你聊一聊HashMap的底层原理。
要点:
- 底层数据结构及转化条件。
- 哈希冲突及解决方案。
- 为何使用红黑树而非二叉树。
- 线程安全性。
回答:
- 底层数据结构 :JDK 1.8 后为
数组 + 链表 + 红黑树。- 链表 -> 红黑树 :链表长度
>= 8且数组长度>= 64。如果只满足链表长度而没满足数组长度,那就优先数组扩容,重新分散节点,扩大哈希范围,减少哈希冲突。 - 红黑树 -> 链表 :树节点数
<= 6。
- 链表 -> 红黑树 :链表长度
- 哈希冲突 :多个key映射到同一桶。
- 解决方式 :① 链地址法 (HashMap采用,桶中的不同对象组织成链表或红黑树);② 开放定址法 (如线性探测,遇到非空桶就下一个,直到遇到空桶);③ 再哈希法(准备多个哈希函数,直到计算出的哈希值映射到非空桶)。
- 为何用红黑树 :普通二叉搜索树(BST)在极端情况下(如哈希值有序插入)会退化为链表,时间复杂度恶化为 O(n) 。红黑树是自平衡的,通过增删旋转、颜色标记、颜色变化保证相对平衡,根节点到叶子节点的最长路径不超过最短路径的二倍,能保证操作时间复杂度稳定在 O(log n),避免性能恶化。
- 线程安全 :
put()方法非线程安全。推荐使用ConcurrentHashMap实现线程安全,JDK7以前采用分段锁,JDK 1.8以后采用synchronized + CAS锁住单个桶节点,并发性能高。
并发编程
4. 线程生命周期
问题:请你聊一聊线程的几种生命周期状态。
要点:
- 线程的6种/7种状态。
NEW->READY。READY<->RUNNING。RUNNING->BLOCKED->READY。RUNNING->WAITING->READY。RUNNING->TIMED_WAITING->READY。RUNNING->TERMINATED。
回答 :
线程在 Thread.State 中定义了 NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED 六种状态。为便于理解,常将 RUNNABLE 细分为 READY 和 RUNNING。
- NEW :
new Thread()后,未调用start()。- 进入
READY:调用start(),线程进入就绪队列等待CPU调度。
- 进入
- READY :线程已具备运行条件,等待CPU时间片。
- 进入
RUNNING:获得CPU时间片。
- 进入
- RUNNING :正在执行。
- 退至
READY:时间片用完,或调用Thread.yield()。 - 进入
BLOCKED:尝试获取synchronized锁但锁被其他线程持有。- 退至
READY:持有锁的线程释放锁,被唤醒的线程重新竞争锁,抢到的进入READY。
- 退至
- 进入
WAITING:调用Object.wait()、Thread.join()、LockSupport.park()。- 退至
READY:被notify/notifyAll、目标线程结束、unpark唤醒。被唤醒的线程需重新竞争锁才能从wait返回。
- 退至
- 进入
TIMED_WAITING:调用Thread.sleep()、带超时的wait/join/park。- 退至
READY:超时结束或被提前唤醒。
- 退至
- 进入
TERMINATED:run()方法正常执行完毕或异常终止。
- 退至
5. 死锁
问题:你知道线程死锁吗?请你聊一聊死锁。
要点:
- 死锁定义。
- 四个必要条件。
- 如何避免。
回答:
- 定义:两个或以上线程在执行中,因争夺资源而相互等待,导致所有线程无限阻塞,程序无法继续。
- 四个必要条件 :
- 互斥:资源一次只能被一个线程占用。
- 请求与保持:线程持有资源的同时请求新资源。
- 不可剥夺:已获得的资源在使用完前不能被强行剥夺。
- 循环等待:存在一个线程-资源的环形等待链。
- 避免方法 :
- 破坏请求与保持:一次性申请所有资源。
- 破坏不可剥夺:申请新资源失败时,释放已持有的资源。
- 破坏循环等待:对所有资源进行排序,规定线程必须按固定顺序申请资源(最常用)。
- 使用超时机制 :如
tryLock(long time, TimeUnit unit),超时后释放已有资源并重试。
6. 进程与线程
问题:请你聊一聊进程和线程的区别。
要点:
- 基本概念。
- 数据空间区别。
- 资源消耗区别。
- 数据通信区别。
- 对协程的简单理解。
回答:
- 基本概念:进程是操作系统资源分配的基本单位,线程是CPU调度的基本单位。一个进程可包含多个线程。
- 数据空间:进程拥有独立的内存地址空间,互不干扰;线程共享所属进程的地址空间,能方便地访问共享数据,但也带来了线程安全问题。
- 资源消耗:线程的创建、切换、销毁开销远小于进程。进程切换涉及虚拟地址空间切换,会导致TLB/缓存失效;线程切换因共享地址空间,开销小得多。
- 数据通信:进程间通信(IPC)需借助操作系统提供的复杂机制(如管道、消息队列、共享内存等);线程间通信可直接读写进程内的全局变量,但需要同步机制。
- 协程:用户态轻量级线程,由程序自身调度(协作式),而非操作系统内核(抢占式)。一个线程可承载大量协程,切换开销极小,尤其适合高并发I/O密集型场景。
7. volatile 关键字
问题:你知道 volatile 关键字吗?请你聊一聊。
要点:
- 基本概念。
- 可见性及保证。
- 有序性及保证。
- 原子性及是否保证。
- 与 synchronized 区别。
回答:
- 基本概念 :
volatile是轻量级同步机制,用于修饰变量。它保证了 可见性 和 有序性 ,但不保证 原子性。 - 可见性 :一个线程对
volatile变量的修改会立即刷新到主内存,其他线程读取时会从主内存重新加载。底层通过 内存屏障 和 缓存一致性协议(如MESI)实现。 - 有序性 :通过 禁止指令重排序 来保证。在
volatile读写操作前后添加内存屏障,确保代码执行的相对顺序。 - 原子性 :不能保证原子性。例如
count++操作包含"读-改-写",多个线程可能同时读到旧值,导致最终结果错误。 - 与 synchronized 区别 :
volatile只保证可见性和有序性,不保证原子性;synchronized三者都保证。volatile不会阻塞线程;synchronized会导致线程阻塞。volatile只能修饰变量;synchronized可修饰方法和代码块。volatile开销较低;synchronized开销较高。
8. 线程安全手段
问题:如何保证线程安全?你都有哪些手段?
要点:
- 线程安全定义。
- 添加同步锁。
- 使用CAS模型。
- 使用原子类。
- 使用ThreadLocal。
回答:
- 线程安全 :多线程并发访问共享资源时,程序能表现出与单线程一致的正确行为,核心是保证 原子性 、可见性 和 有序性。
- 同步锁 :
- synchronized:JVM内置锁,使用简单,自动释放锁。
- ReentrantLock:JDK显式锁,支持公平锁、可中断锁、超时等待等高级特性。
- CAS与原子类 :基于无锁的CAS(Compare And Swap)操作,性能高。
java.util.concurrent.atomic包提供了AtomicInteger等原子类,用于对单个变量的原子操作。 - ThreadLocal :为每个线程创建独立的变量副本,从根本上避免共享,是空间换时间。使用时需注意
remove()避免内存泄漏。 - 其他手段 :使用
ConcurrentHashMap等线程安全容器、使用不可变对象(final)、使用volatile作为状态标志。
9. sleep() 与 wait()
问题:聊一下 sleep() 方法和 wait() 方法的区别?
要点:
- 分别来自哪个类。
- 哪个方法必须在同步块中使用。
- 哪个方法会释放锁。
回答:
- 来源 :
sleep()是Thread的静态方法;wait()是Object的实例方法。 - 调用前提 :
sleep()可在任意地方调用;wait()必须 在同步块或同步方法中调用,否则抛IllegalMonitorStateException。 - 锁行为 :
sleep()不会释放 任何已持有的锁;wait()会释放当前对象的锁,让其他线程有机会执行。 - 唤醒机制 :
sleep()在指定时间后自动唤醒;wait()需被动唤醒(notify/notifyAll或超时)。 - 使用场景 :
sleep()用于暂停执行;wait()用于线程间通信。
10. synchronized 关键字
问题:聊一下 synchronized 的概念。
要点:
- 作用及使用范围。
- 原子性、可见性、有序性保证。
- 是否可重入。
- 公平锁还是非公平锁。
- 锁升级过程。
回答:
- 作用及范围 :互斥同步机制。可修饰 实例方法 (锁当前实例)、静态方法 (锁当前类Class对象)、代码块(锁指定对象)。
- 三大特性 :能保证 原子性 (同一时刻只有一个线程执行)、可见性 (释放锁前刷新修改到主内存,获取锁时清空工作内存)、有序性(通过互斥保证临界区代码不会被重排序打乱)。
- 可重入性:是可重入锁。同一线程可多次获取同一把锁,JVM为锁关联一个计数器和持有者线程,计数递增递减。
- 公平性 :是 非公平锁。锁释放时,等待队列中的线程和"新来的"线程一起竞争,新线程可能插队,优点是吞吐量高。
- 锁升级 :JDK 1.6 后引入,状态从低到高:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁,且不可逆。目的是在低竞争下减少锁开销,在高竞争下才使用重量级锁。
11. 线程池
问题:聊一下线程池。
要点:
- 7个核心参数。
- 优缺点。
- 任务分配顺序。
- 4种拒绝策略。
- 常见线程池类型。
回答:
- 7个核心参数 :
corePoolSize:核心线程数。maximumPoolSize:最大线程数。keepAliveTime:非核心线程空闲存活时间。unit:时间单位。workQueue:任务队列。threadFactory:线程工厂。handler:拒绝策略。
- 优缺点:优点(资源复用、响应快、可管理);缺点(配置复杂、调试困难)。
- 任务分配顺序 :先核心 -> 再排队 -> 后扩容 -> 最后拒 。
- 核心线程数未满 → 创建核心线程执行。
- 核心线程满 → 任务加入队列。
- 队列满 → 创建非核心线程执行。
- 线程数达到最大值 → 执行拒绝策略。
- 4种拒绝策略 :
AbortPolicy:抛异常(默认)。CallerRunsPolicy:调用者线程自己执行。DiscardPolicy:静默丢弃。DiscardOldestPolicy:丢弃队列头任务,重试提交。
- 常见线程池 :
FixedThreadPool:固定大小。CachedThreadPool:弹性伸缩。SingleThreadExecutor:单线程顺序执行。ScheduledThreadPool:支持定时任务。WorkStealingPool:工作窃取,适用于并行计算。
12. 线程创建方式
问题:聊一下线程有哪几种创建方式。
要点:
Thread、Runnable、Callable三种方式的优缺点。- 通过线程池创建。
回答:
- 显式创建 :
- 继承Thread:简单,但受单继承限制,不利于资源共享。
- 实现Runnable:解耦,突破单继承,便于资源共享。无返回值。
- 实现Callable :有返回值,可抛出异常。需配合
ExecutorService和Future使用。
- 线程池创建 :生产环境唯一推荐方式。通过
Executors工具类创建,或手动new ThreadPoolExecutor。优点包括资源复用、控制并发、统一管理。
JVM
13. JVM内存模型
问题:聊一下JVM内存模型。
要点:
- 组成部分。
- 每个部分存储内容。
- 线程私有/共享。
回答:
- 程序计数器 :记录当前线程执行的字节码行号。线程私有。
- Java虚拟机栈 :存储栈帧,包含局部变量表、操作数栈、动态链接、方法出口等。线程私有。
- 本地方法栈 :为Native方法服务。线程私有。
- Java堆 :存储对象实例和数组。线程共享,是GC主要管理区域。
- 方法区 :存储类元信息、常量、静态变量、JIT编译后的代码等。线程共享 。JDK8后由 元空间(Metaspace) 取代永久代,使用本地内存。
- 运行时常量池 :方法区的一部分,存储Class文件中的常量池表(字面量和符号引用)。线程共享。
14. 双亲委派模型
问题:聊一下双亲委派模型。
要点:
- JDK8与JDK9类加载器架构区别。
- 双亲委派模型区别。
- 使用原因。
- 如何打破。
- Tomcat打破的例子。
回答:
- JDK8类加载器 :
Bootstrap(加载核心类)→Extension(加载ext目录)→Application(加载classpath)。
JDK9类加载器 :为支持模块化,Extension变为Platform类加载器。加载粒度从JAR变为模块(如java.base)。 - 双亲委派模型:类加载器收到加载请求,先委派给父加载器,父无法加载才自己加载。JDK9的模型本身仍在,但委派逻辑更复杂,会优先遵循模块依赖关系,可能发生"兄弟"类加载器之间的委派,而非严格的层级委派。
- 使用原因 :安全性 (防止核心类库被覆盖)、避免重复加载。
- 打破方式 :① 重写
loadClass方法,修改委派逻辑;② 使用线程上下文类加载器(TCCL),父类加载器可通过Thread.currentThread().getContextClassLoader()获取子类加载器,实现逆向加载。 - Tomcat打破例子:Web应用需要隔离不同应用的类库。Tomcat的自定义类加载器先尝试自己加载Web应用下的类,找不到再委派给父加载器,打破了双亲委派模型。
15. 类加载过程
问题:聊一下类的加载过程。
要点:
- 加载(Load)。
- 验证(Verify)。
- 准备(Prepare)。
- 解析(Analyze)。
- 初始化(Init)。
回答:
- 加载 :① 获取二进制字节流;② 转换为方法区数据结构(
Klass);③ 生成堆中Class对象。 - 验证:确保字节流信息符合规范,保护JVM安全。包括文件格式、元数据、字节码、符号引用验证。
- 准备 :为 类变量(static) 分配内存,并设置为数据类型默认零值。
final static变量在此阶段直接赋值为指定值。 - 解析 :将常量池中的 符号引用 替换为 直接引用(如指向方法区的指针或偏移量)。
- 初始化 :执行类构造器
<clinit>()方法,为静态变量赋予程序设定的值,执行静态代码块。这是类加载的最后一步。
16. 引用类型
问题:聊一下强软弱虚四种引用类型。
要点:
- 四种引用类型。
- 每种引用指向的对象、GC情况及例子。
回答:
- 强引用 :最常见的引用(如
Object obj = new Object())。只要有强引用存在,GC永不回收。 - 软引用 :
SoftReference。在内存不足时回收,适合实现内存敏感缓存。 - 弱引用 :
WeakReference。发生GC时立即回收,常用于防止内存泄漏(如ThreadLocal中的Entry)。 - 虚引用 :
PhantomReference。无法通过它获取对象实例,必须与ReferenceQueue配合,用于跟踪对象回收状态,如DirectByteBuffer释放堆外内存。
17. 垃圾判定算法
问题:聊一下垃圾判定算法。
要点:
- 引用计数法。
- 可达性分析法。
- 当前虚拟机使用哪种。
- GC Roots对象。
finalize()方法。
回答:
- 引用计数法:为对象维护计数器,引用+1,失效-1。为0时回收。无法解决循环引用问题。
- 可达性分析法 :从一组 GC Roots 出发,通过引用链向下搜索,不可达的对象被判定为可回收。能解决循环引用。
- 当前使用算法 :主流JVM均使用 可达性分析法。
- GC Roots对象:① 虚拟机栈引用的对象;② 方法区静态属性/常量引用的对象;③ 本地方法栈JNI引用的对象;④ 同步锁持有的对象;⑤ JVM内部引用。
- finalize():对象被判定为可回收后,有机会重写此方法实现"自救"(重新与GC Roots建立连接)。但该方法执行具有不确定性且只会执行一次,已被标记为过时,不推荐使用。
18. 垃圾回收算法
问题:聊一下垃圾回收算法。
要点:
- 复制、标记-清除、标记-整理。
- 复制算法流程、优缺点。
- 标记-清除算法流程、优缺点。
- 标记-整理算法流程、优缺点。
- 分代收集算法。
回答:
- 复制算法 :内存分为两块(Eden + Survivor)。回收时将存活对象复制到另一块,清空原块。
- 优点:无碎片,效率高。
- 缺点 :内存利用率低(最高50%)。适用于 年轻代。
- 标记-清除算法 :先标记存活对象,再清除未标记对象。
- 优点:无需移动对象,效率尚可。
- 缺点 :产生内存碎片。适用于 老年代(如CMS)。
- 标记-整理算法 :标记后,将所有存活对象向一端移动,再清理边界外内存。
- 优点:无碎片。
- 缺点 :移动对象成本高,STW时间长。适用于 老年代(如Serial Old)。
- 分代收集算法 :根据对象存活周期划分代,不同代采用不同算法。
- 年轻代 :朝生夕灭,使用 复制算法(Eden:Survivor=8:1)。
- 老年代 :存活久,使用 标记-清除 或 标记-整理。
19. 垃圾回收器
问题:聊一下垃圾回收器。
要点:
- Serial。
- Parallel。
- CMS。
- G1。
- JDK8/11默认收集器。
回答:
- Serial :单线程,STW。复制算法 (新生代)+ 标记-整理(老年代)。适合客户端模式或小内存应用。
- Parallel :多线程,吞吐量优先。JDK8默认。复制算法 (新生代)+ 标记-整理(老年代)。
- CMS :并发低停顿。复制算法 (与ParNew配合)+ 标记-清除(老年代)。流程:初始标记(STW) → 并发标记 → 重新标记(STW) → 并发清除。缺点:CPU敏感、产生碎片、可能退化为Serial Old。
- G1 :Region化内存布局 ,可预测停顿。JDK9+默认。整体标记-整理 ,局部复制。通过维护每个Region的回收价值,在用户指定的停顿时间内优先回收价值高的Region。
- JDK8默认 :Parallel收集器。JDK11默认:G1收集器。
MySQL
20. 索引类型
问题:聊一下MySQL的几种索引类型。
要点:
- 聚簇索引与非聚簇索引。
- 唯一索引。
- 复合索引。
- 全文索引。
- 前缀索引。
回答:
- 聚簇索引:叶子节点存储完整行数据。InnoDB中主键自动成为聚簇索引,每个表有且只有一个。
- 非聚簇索引:叶子节点存储主键值,需回表查询。
- 唯一索引:索引列值必须唯一,允许NULL,主键是特殊的唯一索引。
- 复合索引 :多个列组成的索引,遵循 最左前缀原则。
- 全文索引 :用于全文搜索,使用
MATCH...AGAINST。 - 前缀索引:对字符串列前N个字符建立索引,节省空间。
21. InnoDB索引结构
问题:InnoDB索引使用的数据结构是什么?为什么?
要点:
- 使用的数据结构。
- B树与B+树区别。
- 选择B+树的原因。
回答:
- 数据结构 :InnoDB使用 B+树 作为索引结构。
- B树 vs B+树 :
- B树每个节点都存数据;B+树 非叶子节点只存键值和指针,数据全在叶子节点。
- B+树叶子节点通过指针 连成链表,便于范围查询。
- B树可能在非叶子节点找到数据,查询时间不稳定;B+树所有查询都必须到叶子节点,时间稳定。
- 选择B+树原因 :
- 磁盘I/O友好:非叶子节点不存数据,能存储更多键值,树更矮胖,减少I/O次数。
- 范围查询高效:利用叶子节点链表遍历即可。
- 查询稳定:所有查询均需到叶子节点,性能可预测。
22. 事务隔离级别
问题:聊一下MySQL的事务隔离级别。
要点:
- 四种并发问题。
- 四种隔离级别。
- MVCC机制。
回答:
- 并发问题 :脏写 、脏读 、不可重复读 、幻读。
- 隔离级别 :
- 读未提交:允许读未提交数据,脏读、幻读都可能。
- 读已提交:解决脏读,但可能有不可重复读。
- 可重复读:MySQL默认级别,通过MVCC解决不可重复读,通过间隙锁(Gap Lock)基本解决幻读。
- 串行化:所有事务串行执行,解决一切问题,但性能最差。
- MVCC :通过为每行记录维护 创建版本号 和 删除版本号 ,实现读不加锁,读写不冲突,是
可重复读和读已提交的实现基础。
23. 索引失效情况
问题:聊一下MySQL索引失效的常见情况。
要点:常见索引失效场景。
回答:
- 最左前缀不满足 :复合索引
(a,b,c),查询条件未从a开始。 - 数据类型不匹配 :隐式类型转换,如
varchar字段条件传入int。 - 索引列使用函数或运算 :如
WHERE DATE(create_time) = ...,WHERE age + 1 > 20。 - LIKE以通配符开头 :
LIKE '%张'失效,LIKE '张%'有效。 - 使用否定操作符 :
NOT IN、!=、NOT EXISTS等可能导致失效。 - OR条件:如果OR两侧的列不是都有索引,可能失效。
24. 乐观锁与悲观锁
问题:聊一下MySQL的乐观锁和悲观锁。
要点:
- 基本概念。
- 实现方式。
- 优缺点。
- 使用场景。
回答:
- 悲观锁 :认为并发冲突一定会发生,操作前先加锁。
SELECT ... FOR UPDATE实现行锁。- 优点:保证强一致性,简单。
- 缺点:性能差,可能导致死锁。
- 乐观锁 :认为冲突很少发生,提交更新时才检查。通过 版本号 或 CAS 实现。
- 优点:并发性能高,无死锁。
- 缺点:冲突频繁时大量重试,不保证强一致。
- 场景建议:写多读少用悲观锁;读多写少用乐观锁。
Redis
25. 分布式锁
问题:聊一下Redis分布式锁的实现。
要点:
- 不加锁的问题。
synchronized问题。setnx。setnx + expire。- Lua脚本。
- Redisson。
回答:
- 不加锁:并发超卖。
synchronized:仅单机有效,集群下失效。setnx:setnx lock_key uuid。可保证互斥,但无过期时间,宕机可能死锁。setnx + expire:两命令非原子,中间宕机仍可能死锁。set lock_key uuid NX PX 30000可保证原子性。- Lua脚本:将加锁/解锁逻辑写入Lua,保证原子性。
- Redisson :生产推荐。提供 看门狗机制(自动续期)、可重入锁、公平锁等,解决了锁过期和误删问题。但主从切换时仍可能丢锁(RedLock方案有争议)。
26. 高并发三大问题
问题:聊一下Redis高并发下的缓存击穿、雪崩、穿透。
要点:
- 三个问题的定义及解决方案。
回答:
- 击穿 :热点key 过期,大量请求直达DB。
- 方案 :互斥锁(
setnx)、逻辑过期(值中存过期时间,异步更新)、永不过期。
- 方案 :互斥锁(
- 雪崩 :大量key 同时过期,或Redis宕机,DB压力陡增。
- 方案:过期时间加随机偏移、数据预热、Redis集群、降级限流。
- 穿透 :查询 不存在的数据 ,缓存和DB都查不到。
- 方案 :缓存空对象(短过期时间)、布隆过滤器(先过滤不存在的数据)、接口层校验。
27. 过期策略
问题:聊一下Redis的过期策略。
要点:
- 过期字典。
- 三种策略。
- Redis实际采用的策略。
回答:
- 过期字典:Redis维护一个字典,存储key及其过期时间戳,用于快速判断key是否过期。
- 三种策略 :
- 定时删除:为每个key创建定时器,内存友好但CPU不友好,未采用。
- 惰性删除:访问时检查,CPU友好但可能内存泄露。
- 定期删除:定时随机抽查一部分key,删除其中过期的。是CPU和内存的折中方案。
- Redis实际策略 :惰性删除 + 定期删除 相结合。
28. 淘汰策略
问题:聊一下Redis的内存淘汰策略。
要点:
- 8种淘汰策略。
- LRU算法原理。
- LFU算法原理。
回答:
- 8种策略 :
noeviction:不淘汰,写操作报错(默认)。allkeys-lru:所有key中淘汰最近最少使用。volatile-lru:设置过期key中淘汰LRU。allkeys-random:随机淘汰。volatile-random:设置过期key中随机淘汰。volatile-ttl:淘汰剩余时间最短的。allkeys-lfu:淘汰最不经常使用的(4.0+)。volatile-lfu:设置过期key中淘汰LFU。
- LRU(近似) :随机取N个key,淘汰其中 空闲时间 最长的。通过抽样(默认5)避免维护全局链表。
- LFU:基于访问频率淘汰。使用对数计数器记录频率,并随时间衰减,避免历史数据长期占据。
29. 数据同步
问题:聊一下Redis与MySQL的数据同步策略。
要点:
- 旁路缓存。
- 双删缓存。
- 延迟双删。
- 基于binlog监听。
回答:
- 旁路缓存:读时先查Redis,未命中查DB后写Redis;写时更新DB,再删除Redis。并发下可能读到旧数据。
- 双删缓存:写时先删Redis,再更新DB,最后再删一次Redis。仍可能在更新DB前,其他线程读旧数据并写缓存。
- 延迟双删:在双删基础上,第二次删除延迟执行,确保其他线程"读旧数据并写缓存"的操作在第二次删除之前完成。
- 基于binlog :通过
Canal监听MySQL binlog,同步至Kafka,消费者再更新Redis。解耦、最终一致,是推荐方案。
30. 数据类型
问题:聊一下Redis的几种数据类型。
要点:常用数据类型及底层结构、场景。
回答:
- String:SDS。用于缓存、计数器、分布式锁。
- Hash:ziplist或hashtable。用于存储对象、购物车。
- List:quicklist。用于消息队列、最新列表。
- Set:intset或hashtable。用于标签、共同好友、抽奖。
- ZSet:ziplist或skiplist+dict。用于排行榜、延迟队列。
- 新类型:Bitmaps(签到)、HyperLogLog(UV统计)、GEO(地理位置)、Stream(消息流)。
31. 持久化
问题:聊一下Redis的持久化机制。
要点:
- RDB。
- AOF。
- 混合持久化。
回答:
- RDB :在指定时间间隔生成数据集快照。触发方式(
save、bgsave)。优点(文件紧凑、恢复快);缺点(可能丢失最后一次快照后的数据)。 - AOF :记录所有写操作命令。刷盘策略(
always、everysec、no)。优点(数据安全、可读性强);缺点(文件大、恢复慢)。 - 混合持久化(4.0+):AOF文件以RDB格式开头,后跟增量命令。结合了RDB的快速恢复和AOF的数据安全优势。
32. 高可用方案
问题:聊一下Redis的高可用方案。
要点:
- 主从复制。
- 哨兵模式。
- 集群模式。
回答:
- 主从复制:一主多从,主写从读。解决了数据备份和读写分离,但故障需手动切换,写能力有单点瓶颈。
- 哨兵模式:在主从复制基础上,增加哨兵集群进行监控、自动故障转移。解决了高可用,但写能力仍为单点。
- 集群模式:数据分片(16384个槽位),每个槽位对应一个主从节点组。解决了写能力扩展和容量扩展问题,但客户端需支持集群协议。
Spring
33. Bean生命周期
问题:聊一下Spring Bean的生命周期。
要点:从实例化到销毁的完整过程。
回答:
- 实例化:通过反射调用构造函数创建对象实例。
- 属性注入 :填充Bean属性,执行Aware接口回调(
BeanNameAware、BeanFactoryAware、ApplicationContextAware)。 - 初始化 :
BeanPostProcessor.postProcessBeforeInitialization。- 执行初始化方法:
@PostConstruct→InitializingBean.afterPropertiesSet()→init-method。 BeanPostProcessor.postProcessAfterInitialization(AOP代理创建时机)。
- 使用:Bean完全就绪,可供应用使用。
- 销毁 :
@PreDestroy→DisposableBean.destroy()→destroy-method。
34. IOC与AOP
问题:聊一下Spring的IOC和AOP。
要点:
- IOC、DI、AOP定义。
- 项目中IOC/DI使用场景。
- 项目中AOP使用场景。
- AOP核心术语(切面、通知、连接点、切入点)。
- 通知种类。
回答:
- IOC(控制反转) :将对象的创建、组装、管理权交给外部容器,应用程序从主动
new变为被动接收,达到解耦目的。- DI(依赖注入):IOC的具体实现,容器动态地将依赖注入到组件中(构造器、Setter、字段注入)。
- AOP(面向切面编程):将分散在各处的横切关注点(日志、事务等)模块化成切面,在运行时动态织入到业务方法中。
- 项目中使用IOC/DI :
Controller中注入Service,Service中注入Mapper。 - 项目中使用AOP :
@Transactional(事务管理)、自定义切面记录日志或权限校验。 - AOP术语 :
- 切面:封装横切关注点的模块(类)。
- 通知:切面要完成的工作,定义了"何时"做。
- 连接点:程序执行的某个点(如方法执行)。
- 切入点:匹配连接点的表达式,定义了"何处"做。
- 通知种类 :
@Before、@After、@AfterReturning、@AfterThrowing、@Around。
35. SpringMVC原理
问题:聊一下SpringMVC的核心原理。
要点:
DispatcherServlet核心工作。HandlerMapping。HandlerAdapter。ViewResolver。- 调用链路及设计思想。
回答:
DispatcherServlet:前端控制器,请求入口。统一接收请求,协调各组件处理,是流程控制中心。HandlerMapping:处理器映射器。根据请求URL找到能处理的Handler(Controller方法)。HandlerAdapter:处理器适配器。统一调用不同Handler,负责参数解析、数据绑定、返回值处理。ViewResolver:视图解析器。将ModelAndView中的逻辑视图名解析为具体的View对象(如JSP、Thymeleaf)。- 调用链路 :
- 请求 →
DispatcherServlet DispatcherServlet→HandlerMapping→ 得到HandlerDispatcherServlet→HandlerAdapter→ 调用Handler→ 得到ModelAndViewDispatcherServlet→ViewResolver→ 得到ViewDispatcherServlet→ 渲染视图 → 响应。- 设计思想 :责任分离,各组件通过标准接口解耦,可灵活替换。非串行调用保证了扩展性。
- 请求 →
36. SpringBoot启动流程
问题:SpringBoot项目的启动流程是什么?
要点:按步骤回答。
回答:
- 运行
main方法,执行SpringApplication.run()。 - 创建
SpringApplication,推断应用类型(Web/非Web)。 - 加载
META-INF/spring.factories,获取所有ApplicationListener等初始化器。 - 根据推断结果,启动内嵌的Servlet容器(如Tomcat)。
- 创建并刷新
ApplicationContext:- 扫描并加载
@Component等组件。 - 执行自动装配(加载
spring.factories中配置类)。 - 加载外部配置(
application.properties/yml),绑定到@ConfigurationPropertiesBean。 - 执行各组件初始化逻辑(如数据库连接池、缓存、MQ)。
- 扫描并加载
- 调用
ApplicationRunner和CommandLineRunner。 - 启动内嵌Servlet容器,监听端口。
- 启动完毕,等待请求。
37. 自动装配原理
问题:SpringBoot的自动装配流程是什么?
要点:
- 定义及好处。
- 流程步骤。
回答:
- 定义 :框架根据类路径依赖、环境、配置,自动注册Bean的机制。
- 好处:减少配置,降低出错,开箱即用。
- 流程 :
- 启动时,
@EnableAutoConfiguration导入AutoConfigurationImportSelector。 - 加载所有
META-INF/spring.factories,获取EnableAutoConfiguration键对应的配置类列表。 - 根据
@ConditionalXXX条件注解(如@ConditionalOnClass、@ConditionalOnMissingBean)进行筛选。 - 符合条件的配置类中的
@Bean方法被注册到容器。 - 加载外部配置,通过
@ConfigurationProperties绑定属性,对自动配置的Bean进行定制。 - 执行各组件的初始化逻辑。
- 完成所有自动装配和初始化。
- 启动时,
消息队列 (RocketMQ)
38. RocketMQ核心原理
问题:聊一下RocketMQ的核心组件和消息收发流程。
要点:
- 四大核心组件。
- 消息发送、存储、消费流程。
- 与其他MQ对比。
回答:
- 四大组件 :
- NameServer:轻量级路由中心,管理Broker集群元数据,无状态可集群。
- Broker:消息存储核心,负责消息接收、存储、投递。可集群部署(Master-Slave)。
- Producer:消息生产者。
- Consumer:消息消费者,支持Pull和Push两种模式。
- 收发流程 :
- Producer从NameServer获取Topic路由信息,选择一个Master Broker发送消息。
- Broker收到消息后,写入CommitLog(顺序写),再转发至ConsumeQueue(索引)。
- Consumer从NameServer获取路由,向Broker发起Pull请求,获取消息后消费,并提交位点。
- 与其他MQ对比 :
- RocketMQ vs Kafka:RocketMQ事务消息、延迟消息、死信队列功能更丰富;Kafka吞吐量更高,适合海量日志场景。
- RocketMQ vs RabbitMQ:RocketMQ吞吐量高、支持分布式事务、顺序消息;RabbitMQ社区活跃、延迟低、支持复杂路由。
- 选型建议:金融/电商订单场景用RocketMQ;大数据日志用Kafka;企业内部简单场景用RabbitMQ。
39. 消息可靠性保障
问题:RocketMQ如何保证消息不丢失?
要点:生产端、存储端、消费端三端保障。
回答:
- 生产端 :同步发送/事务消息,等待Broker确认(
sendStatus为SEND_OK)。若失败,可重试或记录日志。 - 存储端 :
- 同步刷盘:消息写入CommitLog后同步落盘,性能较低但可靠。
- 同步复制:Master收到消息后,同步复制到Slave,返回成功。
- 主从切换:Master宕机,Slave可快速切换。
- 消费端 :消费成功后,再提交位点(
CONSUME_SUCCESS)。若业务异常,可返回RECONSUME_LATER,消息会重试。
40. 消息重复消费及解决方案
问题:RocketMQ如何解决消息重复消费问题?
要点:重复消费的原因及幂等处理。
回答:
- 重复原因 :
- 生产者:发送超时后重试,可能造成Broker端消息重复。
- 消费者:消费成功但因网络/宕机未能及时提交位点,重启后重新拉取。
- 解决方案 :幂等处理 。
- 唯一ID :使用
messageId或业务唯一ID(如订单号)。 - Redis去重 :消费前
SETNX messageId,成功则消费,失败则跳过。 - 数据库唯一键:利用数据库唯一索引,重复插入会抛异常,业务捕获后忽略。
- 状态机:根据消息状态判断是否已处理。
- 唯一ID :使用
41. 消息顺序性保障
问题:如何保证RocketMQ的顺序消息?
要点:全局顺序 vs 分区顺序。
回答:
- 分区顺序 :RocketMQ默认支持的顺序消息模型。通过
MessageQueueSelector将同一业务ID(如订单号)的消息发送到同一个Queue,Consumer端使用一个线程消费一个Queue,即可保证该分区内消息的顺序性。 - 全局顺序:牺牲高可用和吞吐量,使用一个Topic只有一个Queue。不推荐。
- 实现 :Producer端
SendResult send(Message msg, MessageQueueSelector selector, Object arg);Consumer端注册MessageListenerOrderly。
42. 延迟消息与事务消息
问题:聊一下RocketMQ的延迟消息和事务消息。
要点:使用场景及实现。
回答:
- 延迟消息 :
- 使用场景:订单超时取消、定时任务。
- 实现 :
msg.setDelayTimeLevel(3),支持18个预定义级别(1s, 5s, ... 2h)。 - 原理 :消息写入
SCHEDULE_TOPIC_XXXX,由专属定时线程转投至目标Topic。
- 事务消息 :
- 使用场景:分布式事务(如下单-扣库存)。
- 流程 :
- Producer发送半消息(Half Message)到Broker。
- Broker存储半消息,Producer执行本地事务。
- Producer向Broker发送提交/回滚状态。
- Broker若未收到最终状态,会回查Producer本地事务状态。
- 事务提交后,消息才被Consumer消费。
网络
43. TCP与UDP
问题:聊一下TCP和UDP的区别。
要点:
- 基础概念。
- 网络开销。
- 可靠性。
- 数据容量限制。
- 重传机制。
- TCP流量控制和拥塞控制。
回答:
- 基础概念 :
- TCP:面向连接的可靠传输协议,类似打电话。
- UDP:无连接的不可靠传输协议,类似寄明信片。
- 网络开销:TCP开销大(三次握手、四次挥手、ACK确认);UDP开销小(无连接、头部仅8字节)。
- 可靠性:TCP通过序列号、确认应答、重传等机制保证可靠;UDP不保证。
- 数据容量:TCP面向字节流,无严格限制;UDP单包最大64KB。
- 重传机制:TCP有超时重传和快速重传;UDP无重传。
- TCP流量控制 :滑动窗口协议,防止接收方缓冲区溢出。
TCP拥塞控制:慢启动、拥塞避免、快速重传、快速恢复。
44. HTTP与HTTPS
问题:聊一下HTTP和HTTPS的区别。
要点:
- 数据加密。
- 身份验证。
- 数据完整性。
- 信任和认证体系。
- 对抗劫持能力。
- HTTPS工作原理。
回答:
- 数据加密:HTTP明文传输;HTTPS通过SSL/TLS加密传输。
- 身份验证:HTTP无身份验证;HTTPS通过CA证书验证服务器身份。
- 数据完整性:HTTP无完整性保护;HTTPS通过MAC校验保证完整性。
- 信任体系:HTTPS基于PKI体系和证书链,浏览器信任根CA,逐级验证服务器证书。
- 对抗劫持:HTTPS可有效防止DNS劫持和中间人攻击。
- TLS握手流程:Client Hello → Server Hello(含证书)→ 客户端验证证书并加密预主密钥 → 服务器解密生成会话密钥 → 后续对称加密通信。
45. 三次握手与四次挥手
问题:聊一下TCP的三次握手和四次挥手。
要点:
- 三次握手流程。
- 四次挥手流程。
- 为何握手三次、挥手四次。
- 为何不能两次握手。
回答:
- 三次握手流程 :
- 第一次:客户端发
SYN=1, seq=x。 - 第二次:服务端回复
SYN=1, ACK=1, seq=y, ack=x+1。 - 第三次:客户端回复
ACK=1, seq=x+1, ack=y+1。连接建立。
- 第一次:客户端发
- 四次挥手流程 :
- 第一次:主动方发
FIN=1, seq=u。 - 第二次:被动方回复
ACK=1, ack=u+1(半关闭)。 - 第三次:被动方数据发完后发
FIN=1, seq=v。 - 第四次:主动方回复
ACK=1, ack=v+1,等待2MSL后关闭。
- 第一次:主动方发
- 为何握手三次、挥手四次 :
- 握手时,服务器可将
SYN和ACK合并发送,故为三次。 - 挥手时,被动方收到
FIN后可能还有数据要发,故ACK和FIN不能合并,需四次。
- 握手时,服务器可将
- 为何不能两次握手:防止已失效的连接请求报文突然传到服务器,导致服务器误以为客户端要建立连接而白白等待,浪费资源。第三次握手是客户端对服务器允许连接的最终确认。
46. 浏览器HTTP请求详细流程
问题:聊一下从输入URL到页面展示的完整过程。
要点:
- DNS解析。
- 建立TCP连接。
- 发送HTTP请求。
- 服务器处理请求。
- 返回HTTP响应。
- 断开TCP连接。
回答:
- DNS解析:域名 → IP地址。查询顺序:浏览器缓存 → 操作系统hosts → 本地DNS服务器(递归/迭代查询)。
- 建立TCP连接:向解析出的IP发起三次握手,建立可靠传输通道。
- 发送HTTP请求:构建请求报文(请求行、请求头、空行、请求体),通过Socket写入TCP缓冲区。
- 服务器处理请求:Web服务器(Nginx)接收 → 反向代理/负载均衡 → 应用服务器(Tomcat)解析路由 → Controller执行业务逻辑。
- 返回HTTP响应:构建响应报文(状态行、响应头、空行、响应体),通过TCP连接返回。
- 断开TCP连接:HTTP/1.0默认短连接(请求完即断开);HTTP/1.1+默认长连接(keep-alive),空闲超时或页面加载完后触发四次挥手断开。HTTPS全程在TLS加密隧道中进行。
设计模式
47. 单例设计模式
问题:手写一个单例设计模式。
要点:
- 饿汉式的优缺点及写法。
- 懒汉式的优缺点及写法。
- 静态内部类Holder模式的优缺点及写法。
- 枚举单例模式的优缺点及写法。
回答:
1. 饿汉式
java
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() { return INSTANCE; }
}
- 优点:类加载时完成初始化,天然线程安全(由JVM类加载机制保证),无锁竞争,性能高。
- 缺点:无论是否使用都会创建实例,可能造成资源浪费。无法实现延迟加载。
- 适用场景:单例对象较小、一定会被使用、初始化逻辑简单的场景。
2. 懒汉式
线程不安全版本:
java
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) instance = new Singleton();
return instance;
}
}
线程安全版本(synchronized方法):
java
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) instance = new Singleton();
return instance;
}
}
双重检查锁定(DCL)- 推荐懒汉式写法:
java
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
- 优点:延迟加载,节约资源。
- 缺点 :线程不安全版本需注意;
synchronized方法版本性能较差;DCL写法稍复杂,需注意volatile防止指令重排。 - 适用场景:单例对象较大、可能不被使用、需要延迟加载的场景。
3. 静态内部类Holder模式
java
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
- 优点 :结合了饿汉式和懒汉式的优点------既实现了延迟加载(Holder类在第一次调用
getInstance时才被加载),又由JVM类加载机制保证线程安全。写法简单,性能高。 - 缺点:无法防止反射攻击(可通过在构造方法中抛异常解决)。
- 适用场景:大多数单例场景的首选推荐写法。
4. 枚举单例模式
java
public enum Singleton {
INSTANCE;
public void someMethod() { /* 业务方法 */ }
}
- 优点:由JVM天然保证线程安全、防止反射攻击、防止反序列化重新创建对象。代码最简洁。
- 缺点:非延迟加载(枚举类加载时即创建实例);不够灵活(无法继承其他类,但可实现接口)。
- 适用场景:需要绝对防止反射和反序列化攻击的场景,是《Effective Java》推荐的单例最佳实践。
总结对比
| 实现方式 | 线程安全 | 延迟加载 | 防反射/反序列化 | 性能 | 推荐度 |
|---|---|---|---|---|---|
| 饿汉式 | ✓ | ✗ | ✗ | 高 | ⭐⭐⭐ |
| 懒汉式(DCL) | ✓ | ✓ | ✗ | 高 | ⭐⭐⭐⭐ |
| 静态内部类 | ✓ | ✓ | ✗ | 高 | ⭐⭐⭐⭐⭐ |
| 枚举 | ✓ | ✗ | ✓ | 高 | ⭐⭐⭐⭐⭐ |