后端开发面试题总结

Java后端面试核心知识体系

本文专为Java后端面试求职者制作的面试经典题汇总,覆盖95%的高频面试题。

以下的问题通过"问题 + 要点 + 回答"的格式展现,请通过文章目录快速找到对应问题。


Java 基础

1. hashCode() 与 equals()

问题:请你聊一聊 hashCode() 和 equals() 这两个方法。

要点

  1. 二者来源。
  2. 各自功能。
  3. 重写 equals() 必须重写 hashCode() 的原因。

回答

  1. 来源 :两个方法均来自 Object 类。
  2. 功能
    • equals():用于判断两个对象在逻辑上是否相等。默认实现为 ==,比较内存地址,通常根据业务逻辑重写,按照对象实际内容判断。
    • hashCode():返回对象的哈希码(int值)。默认实现通常与内存地址相关,通常也是根据业务逻辑重写,重写是为了与 equals() 保持一致。
  3. 重写原因 :Java约定,当 equals() 判定两个对象相等时,它们的 hashCode() 返回的哈希码也必须相等。如果不遵守,在哈希集合(如 HashSetHashMap)中,相等的对象可能被存入不同桶位,导致逻辑错误。例如,HashSet 先通过 hashCode() 定位桶的位置,若两个相等对象 hashCode() 不同,则会判定为不同对象,无法正确查找。

2. ArrayList 与 LinkedList

问题:请你聊一聊ArrayList和LinkedList的区别。

要点

  1. 与List、Collection接口的关系。接口来源
  2. 查询效率及原因。
  3. 增删效率及原因。
  4. 内存使用

回答

  1. 接口关系 :两者都是 List 接口的实现类,都实现了 Collection 接口。ArrayList 实现了 RandomAccess 接口,支持快速随机访问;LinkedList 实现了 Deque 接口,可作为双端队列使用。

  2. 查询效率

  • ArrayList:时间复杂度 O(1)。基于数组,内存连续,支持通过索引直接定位。
  • LinkedList:时间复杂度 O(n)。基于双向链表,内存不连续,需从头/尾遍历查找。其内部优化了遍历策略,最坏情况为 n/2,但整体仍为 O(n)。
  1. 增删效率
    • ArrayList:尾部操作均摊 O(1) ;中间操作 O(n),需移动后续元素。
    • LinkedList:头尾操作 O(1) ;中间操作本身 O(1) ,但定位到中间位置需 O(n),总体为 O(n)。
  2. 内存ArrayList 更节省空间,但可能预占内存;LinkedList 每个节点需额外存储前后指针,内存开销约为 ArrayList 的 2-3 倍。

3. HashMap 底层原理

问题:请你聊一聊HashMap的底层原理。

要点

  1. 底层数据结构及转化条件。
  2. 哈希冲突及解决方案。
  3. 为何使用红黑树而非二叉树。
  4. 线程安全性。

回答

  1. 底层数据结构 :JDK 1.8 后为 数组 + 链表 + 红黑树
    • 链表 -> 红黑树 :链表长度 >= 8 且数组长度 >= 64。如果只满足链表长度而没满足数组长度,那就优先数组扩容,重新分散节点,扩大哈希范围,减少哈希冲突。
    • 红黑树 -> 链表 :树节点数 <= 6
  2. 哈希冲突 :多个key映射到同一桶。
    • 解决方式 :① 链地址法 (HashMap采用,桶中的不同对象组织成链表或红黑树);② 开放定址法 (如线性探测,遇到非空桶就下一个,直到遇到空桶);③ 再哈希法(准备多个哈希函数,直到计算出的哈希值映射到非空桶)。
  3. 为何用红黑树 :普通二叉搜索树(BST)在极端情况下(如哈希值有序插入)会退化为链表,时间复杂度恶化为 O(n) 。红黑树是自平衡的,通过增删旋转、颜色标记、颜色变化保证相对平衡,根节点到叶子节点的最长路径不超过最短路径的二倍,能保证操作时间复杂度稳定在 O(log n),避免性能恶化。
  4. 线程安全put() 方法非线程安全。推荐使用 ConcurrentHashMap 实现线程安全,JDK7以前采用分段锁,JDK 1.8以后采用 synchronized + CAS 锁住单个桶节点,并发性能高。

并发编程

4. 线程生命周期

问题:请你聊一聊线程的几种生命周期状态。

要点

  1. 线程的6种/7种状态。
  2. NEW -> READY
  3. READY <-> RUNNING
  4. RUNNING -> BLOCKED -> READY
  5. RUNNING -> WAITING -> READY
  6. RUNNING -> TIMED_WAITING -> READY
  7. RUNNING -> TERMINATED

回答

线程在 Thread.State 中定义了 NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED 六种状态。为便于理解,常将 RUNNABLE 细分为 READYRUNNING

  • NEWnew 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:超时结束或被提前唤醒。
    • 进入 TERMINATEDrun() 方法正常执行完毕或异常终止。

5. 死锁

问题:你知道线程死锁吗?请你聊一聊死锁。

要点

  1. 死锁定义。
  2. 四个必要条件。
  3. 如何避免。

回答

  1. 定义:两个或以上线程在执行中,因争夺资源而相互等待,导致所有线程无限阻塞,程序无法继续。
  2. 四个必要条件
    • 互斥:资源一次只能被一个线程占用。
    • 请求与保持:线程持有资源的同时请求新资源。
    • 不可剥夺:已获得的资源在使用完前不能被强行剥夺。
    • 循环等待:存在一个线程-资源的环形等待链。
  3. 避免方法
    • 破坏请求与保持:一次性申请所有资源。
    • 破坏不可剥夺:申请新资源失败时,释放已持有的资源。
    • 破坏循环等待:对所有资源进行排序,规定线程必须按固定顺序申请资源(最常用)。
    • 使用超时机制 :如 tryLock(long time, TimeUnit unit),超时后释放已有资源并重试。

6. 进程与线程

问题:请你聊一聊进程和线程的区别。

要点

  1. 基本概念。
  2. 数据空间区别。
  3. 资源消耗区别。
  4. 数据通信区别。
  5. 对协程的简单理解。

回答

  1. 基本概念:进程是操作系统资源分配的基本单位,线程是CPU调度的基本单位。一个进程可包含多个线程。
  2. 数据空间:进程拥有独立的内存地址空间,互不干扰;线程共享所属进程的地址空间,能方便地访问共享数据,但也带来了线程安全问题。
  3. 资源消耗:线程的创建、切换、销毁开销远小于进程。进程切换涉及虚拟地址空间切换,会导致TLB/缓存失效;线程切换因共享地址空间,开销小得多。
  4. 数据通信:进程间通信(IPC)需借助操作系统提供的复杂机制(如管道、消息队列、共享内存等);线程间通信可直接读写进程内的全局变量,但需要同步机制。
  5. 协程:用户态轻量级线程,由程序自身调度(协作式),而非操作系统内核(抢占式)。一个线程可承载大量协程,切换开销极小,尤其适合高并发I/O密集型场景。

7. volatile 关键字

问题:你知道 volatile 关键字吗?请你聊一聊。

要点

  1. 基本概念。
  2. 可见性及保证。
  3. 有序性及保证。
  4. 原子性及是否保证。
  5. 与 synchronized 区别。

回答

  1. 基本概念volatile 是轻量级同步机制,用于修饰变量。它保证了 可见性有序性 ,但不保证 原子性
  2. 可见性 :一个线程对 volatile 变量的修改会立即刷新到主内存,其他线程读取时会从主内存重新加载。底层通过 内存屏障缓存一致性协议(如MESI)实现。
  3. 有序性 :通过 禁止指令重排序 来保证。在 volatile 读写操作前后添加内存屏障,确保代码执行的相对顺序。
  4. 原子性 :不能保证原子性。例如 count++ 操作包含"读-改-写",多个线程可能同时读到旧值,导致最终结果错误。
  5. 与 synchronized 区别
    • volatile 只保证可见性和有序性,不保证原子性;synchronized 三者都保证。
    • volatile 不会阻塞线程;synchronized 会导致线程阻塞。
    • volatile 只能修饰变量;synchronized 可修饰方法和代码块。
    • volatile 开销较低;synchronized 开销较高。

8. 线程安全手段

问题:如何保证线程安全?你都有哪些手段?

要点

  1. 线程安全定义。
  2. 添加同步锁。
  3. 使用CAS模型。
  4. 使用原子类。
  5. 使用ThreadLocal。

回答

  1. 线程安全 :多线程并发访问共享资源时,程序能表现出与单线程一致的正确行为,核心是保证 原子性可见性有序性
  2. 同步锁
    • synchronized:JVM内置锁,使用简单,自动释放锁。
    • ReentrantLock:JDK显式锁,支持公平锁、可中断锁、超时等待等高级特性。
  3. CAS与原子类 :基于无锁的CAS(Compare And Swap)操作,性能高。java.util.concurrent.atomic 包提供了 AtomicInteger 等原子类,用于对单个变量的原子操作。
  4. ThreadLocal :为每个线程创建独立的变量副本,从根本上避免共享,是空间换时间。使用时需注意 remove() 避免内存泄漏。
  5. 其他手段 :使用 ConcurrentHashMap 等线程安全容器、使用不可变对象(final)、使用 volatile 作为状态标志。

9. sleep() 与 wait()

问题:聊一下 sleep() 方法和 wait() 方法的区别?

要点

  1. 分别来自哪个类。
  2. 哪个方法必须在同步块中使用。
  3. 哪个方法会释放锁。

回答

  • 来源sleep()Thread 的静态方法;wait()Object 的实例方法。
  • 调用前提sleep() 可在任意地方调用;wait() 必须 在同步块或同步方法中调用,否则抛 IllegalMonitorStateException
  • 锁行为sleep() 不会释放 任何已持有的锁;wait() 会释放当前对象的锁,让其他线程有机会执行。
  • 唤醒机制sleep() 在指定时间后自动唤醒;wait() 需被动唤醒(notify/notifyAll 或超时)。
  • 使用场景sleep() 用于暂停执行;wait() 用于线程间通信。

10. synchronized 关键字

问题:聊一下 synchronized 的概念。

要点

  1. 作用及使用范围。
  2. 原子性、可见性、有序性保证。
  3. 是否可重入。
  4. 公平锁还是非公平锁。
  5. 锁升级过程。

回答

  1. 作用及范围 :互斥同步机制。可修饰 实例方法 (锁当前实例)、静态方法 (锁当前类Class对象)、代码块(锁指定对象)。
  2. 三大特性 :能保证 原子性 (同一时刻只有一个线程执行)、可见性 (释放锁前刷新修改到主内存,获取锁时清空工作内存)、有序性(通过互斥保证临界区代码不会被重排序打乱)。
  3. 可重入性:是可重入锁。同一线程可多次获取同一把锁,JVM为锁关联一个计数器和持有者线程,计数递增递减。
  4. 公平性 :是 非公平锁。锁释放时,等待队列中的线程和"新来的"线程一起竞争,新线程可能插队,优点是吞吐量高。
  5. 锁升级 :JDK 1.6 后引入,状态从低到高:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁,且不可逆。目的是在低竞争下减少锁开销,在高竞争下才使用重量级锁。

11. 线程池

问题:聊一下线程池。

要点

  1. 7个核心参数。
  2. 优缺点。
  3. 任务分配顺序。
  4. 4种拒绝策略。
  5. 常见线程池类型。

回答

  1. 7个核心参数
    • corePoolSize:核心线程数。
    • maximumPoolSize:最大线程数。
    • keepAliveTime:非核心线程空闲存活时间。
    • unit:时间单位。
    • workQueue:任务队列。
    • threadFactory:线程工厂。
    • handler:拒绝策略。
  2. 优缺点:优点(资源复用、响应快、可管理);缺点(配置复杂、调试困难)。
  3. 任务分配顺序先核心 -> 再排队 -> 后扩容 -> 最后拒
    • 核心线程数未满 → 创建核心线程执行。
    • 核心线程满 → 任务加入队列。
    • 队列满 → 创建非核心线程执行。
    • 线程数达到最大值 → 执行拒绝策略。
  4. 4种拒绝策略
    • AbortPolicy:抛异常(默认)。
    • CallerRunsPolicy:调用者线程自己执行。
    • DiscardPolicy:静默丢弃。
    • DiscardOldestPolicy:丢弃队列头任务,重试提交。
  5. 常见线程池
    • FixedThreadPool:固定大小。
    • CachedThreadPool:弹性伸缩。
    • SingleThreadExecutor:单线程顺序执行。
    • ScheduledThreadPool:支持定时任务。
    • WorkStealingPool:工作窃取,适用于并行计算。

12. 线程创建方式

问题:聊一下线程有哪几种创建方式。

要点

  1. ThreadRunnableCallable 三种方式的优缺点。
  2. 通过线程池创建。

回答

  1. 显式创建
    • 继承Thread:简单,但受单继承限制,不利于资源共享。
    • 实现Runnable:解耦,突破单继承,便于资源共享。无返回值。
    • 实现Callable :有返回值,可抛出异常。需配合 ExecutorServiceFuture 使用。
  2. 线程池创建 :生产环境唯一推荐方式。通过 Executors 工具类创建,或手动 new ThreadPoolExecutor。优点包括资源复用、控制并发、统一管理。

JVM

13. JVM内存模型

问题:聊一下JVM内存模型。

要点

  1. 组成部分。
  2. 每个部分存储内容。
  3. 线程私有/共享。

回答

  • 程序计数器 :记录当前线程执行的字节码行号。线程私有
  • Java虚拟机栈 :存储栈帧,包含局部变量表、操作数栈、动态链接、方法出口等。线程私有
  • 本地方法栈 :为Native方法服务。线程私有
  • Java堆 :存储对象实例和数组。线程共享,是GC主要管理区域。
  • 方法区 :存储类元信息、常量、静态变量、JIT编译后的代码等。线程共享 。JDK8后由 元空间(Metaspace) 取代永久代,使用本地内存。
  • 运行时常量池 :方法区的一部分,存储Class文件中的常量池表(字面量和符号引用)。线程共享

14. 双亲委派模型

问题:聊一下双亲委派模型。

要点

  1. JDK8与JDK9类加载器架构区别。
  2. 双亲委派模型区别。
  3. 使用原因。
  4. 如何打破。
  5. Tomcat打破的例子。

回答

  1. JDK8类加载器Bootstrap(加载核心类)→ Extension(加载ext目录)→ Application(加载classpath)。
    JDK9类加载器 :为支持模块化,Extension 变为 Platform 类加载器。加载粒度从JAR变为模块(如java.base)。
  2. 双亲委派模型:类加载器收到加载请求,先委派给父加载器,父无法加载才自己加载。JDK9的模型本身仍在,但委派逻辑更复杂,会优先遵循模块依赖关系,可能发生"兄弟"类加载器之间的委派,而非严格的层级委派。
  3. 使用原因安全性 (防止核心类库被覆盖)、避免重复加载
  4. 打破方式 :① 重写 loadClass 方法,修改委派逻辑;② 使用线程上下文类加载器(TCCL),父类加载器可通过 Thread.currentThread().getContextClassLoader() 获取子类加载器,实现逆向加载。
  5. Tomcat打破例子:Web应用需要隔离不同应用的类库。Tomcat的自定义类加载器先尝试自己加载Web应用下的类,找不到再委派给父加载器,打破了双亲委派模型。

15. 类加载过程

问题:聊一下类的加载过程。

要点

  1. 加载(Load)。
  2. 验证(Verify)。
  3. 准备(Prepare)。
  4. 解析(Analyze)。
  5. 初始化(Init)。

回答

  1. 加载 :① 获取二进制字节流;② 转换为方法区数据结构(Klass);③ 生成堆中 Class 对象。
  2. 验证:确保字节流信息符合规范,保护JVM安全。包括文件格式、元数据、字节码、符号引用验证。
  3. 准备 :为 类变量(static) 分配内存,并设置为数据类型默认零值。final static 变量在此阶段直接赋值为指定值。
  4. 解析 :将常量池中的 符号引用 替换为 直接引用(如指向方法区的指针或偏移量)。
  5. 初始化 :执行类构造器 <clinit>() 方法,为静态变量赋予程序设定的值,执行静态代码块。这是类加载的最后一步。

16. 引用类型

问题:聊一下强软弱虚四种引用类型。

要点

  1. 四种引用类型。
  2. 每种引用指向的对象、GC情况及例子。

回答

  1. 强引用 :最常见的引用(如 Object obj = new Object())。只要有强引用存在,GC永不回收。
  2. 软引用SoftReference。在内存不足时回收,适合实现内存敏感缓存。
  3. 弱引用WeakReference。发生GC时立即回收,常用于防止内存泄漏(如 ThreadLocal 中的 Entry)。
  4. 虚引用PhantomReference。无法通过它获取对象实例,必须与 ReferenceQueue 配合,用于跟踪对象回收状态,如 DirectByteBuffer 释放堆外内存。

17. 垃圾判定算法

问题:聊一下垃圾判定算法。

要点

  1. 引用计数法。
  2. 可达性分析法。
  3. 当前虚拟机使用哪种。
  4. GC Roots对象。
  5. finalize() 方法。

回答

  1. 引用计数法:为对象维护计数器,引用+1,失效-1。为0时回收。无法解决循环引用问题。
  2. 可达性分析法 :从一组 GC Roots 出发,通过引用链向下搜索,不可达的对象被判定为可回收。能解决循环引用。
  3. 当前使用算法 :主流JVM均使用 可达性分析法
  4. GC Roots对象:① 虚拟机栈引用的对象;② 方法区静态属性/常量引用的对象;③ 本地方法栈JNI引用的对象;④ 同步锁持有的对象;⑤ JVM内部引用。
  5. finalize():对象被判定为可回收后,有机会重写此方法实现"自救"(重新与GC Roots建立连接)。但该方法执行具有不确定性且只会执行一次,已被标记为过时,不推荐使用。

18. 垃圾回收算法

问题:聊一下垃圾回收算法。

要点

  1. 复制、标记-清除、标记-整理。
  2. 复制算法流程、优缺点。
  3. 标记-清除算法流程、优缺点。
  4. 标记-整理算法流程、优缺点。
  5. 分代收集算法。

回答

  1. 复制算法 :内存分为两块(Eden + Survivor)。回收时将存活对象复制到另一块,清空原块。
    • 优点:无碎片,效率高。
    • 缺点 :内存利用率低(最高50%)。适用于 年轻代
  2. 标记-清除算法 :先标记存活对象,再清除未标记对象。
    • 优点:无需移动对象,效率尚可。
    • 缺点 :产生内存碎片。适用于 老年代(如CMS)。
  3. 标记-整理算法 :标记后,将所有存活对象向一端移动,再清理边界外内存。
    • 优点:无碎片。
    • 缺点 :移动对象成本高,STW时间长。适用于 老年代(如Serial Old)。
  4. 分代收集算法 :根据对象存活周期划分代,不同代采用不同算法。
    • 年轻代 :朝生夕灭,使用 复制算法(Eden:Survivor=8:1)。
    • 老年代 :存活久,使用 标记-清除标记-整理

19. 垃圾回收器

问题:聊一下垃圾回收器。

要点

  1. Serial。
  2. Parallel。
  3. CMS。
  4. G1。
  5. JDK8/11默认收集器。

回答

  1. Serial :单线程,STW。复制算法 (新生代)+ 标记-整理(老年代)。适合客户端模式或小内存应用。
  2. Parallel :多线程,吞吐量优先。JDK8默认。复制算法 (新生代)+ 标记-整理(老年代)。
  3. CMS :并发低停顿。复制算法 (与ParNew配合)+ 标记-清除(老年代)。流程:初始标记(STW) → 并发标记 → 重新标记(STW) → 并发清除。缺点:CPU敏感、产生碎片、可能退化为Serial Old。
  4. G1Region化内存布局 ,可预测停顿。JDK9+默认。整体标记-整理 ,局部复制。通过维护每个Region的回收价值,在用户指定的停顿时间内优先回收价值高的Region。
  5. JDK8默认 :Parallel收集器。JDK11默认:G1收集器。

MySQL

20. 索引类型

问题:聊一下MySQL的几种索引类型。

要点

  1. 聚簇索引与非聚簇索引。
  2. 唯一索引。
  3. 复合索引。
  4. 全文索引。
  5. 前缀索引。

回答

  1. 聚簇索引:叶子节点存储完整行数据。InnoDB中主键自动成为聚簇索引,每个表有且只有一个。
  2. 非聚簇索引:叶子节点存储主键值,需回表查询。
  3. 唯一索引:索引列值必须唯一,允许NULL,主键是特殊的唯一索引。
  4. 复合索引 :多个列组成的索引,遵循 最左前缀原则
  5. 全文索引 :用于全文搜索,使用 MATCH...AGAINST
  6. 前缀索引:对字符串列前N个字符建立索引,节省空间。

21. InnoDB索引结构

问题:InnoDB索引使用的数据结构是什么?为什么?

要点

  1. 使用的数据结构。
  2. B树与B+树区别。
  3. 选择B+树的原因。

回答

  1. 数据结构 :InnoDB使用 B+树 作为索引结构。
  2. B树 vs B+树
    • B树每个节点都存数据;B+树 非叶子节点只存键值和指针,数据全在叶子节点。
    • B+树叶子节点通过指针 连成链表,便于范围查询。
    • B树可能在非叶子节点找到数据,查询时间不稳定;B+树所有查询都必须到叶子节点,时间稳定。
  3. 选择B+树原因
    • 磁盘I/O友好:非叶子节点不存数据,能存储更多键值,树更矮胖,减少I/O次数。
    • 范围查询高效:利用叶子节点链表遍历即可。
    • 查询稳定:所有查询均需到叶子节点,性能可预测。

22. 事务隔离级别

问题:聊一下MySQL的事务隔离级别。

要点

  1. 四种并发问题。
  2. 四种隔离级别。
  3. MVCC机制。

回答

  1. 并发问题脏写脏读不可重复读幻读
  2. 隔离级别
    • 读未提交:允许读未提交数据,脏读、幻读都可能。
    • 读已提交:解决脏读,但可能有不可重复读。
    • 可重复读:MySQL默认级别,通过MVCC解决不可重复读,通过间隙锁(Gap Lock)基本解决幻读。
    • 串行化:所有事务串行执行,解决一切问题,但性能最差。
  3. MVCC :通过为每行记录维护 创建版本号删除版本号 ,实现读不加锁,读写不冲突,是 可重复读读已提交 的实现基础。

23. 索引失效情况

问题:聊一下MySQL索引失效的常见情况。

要点:常见索引失效场景。

回答

  1. 最左前缀不满足 :复合索引 (a,b,c),查询条件未从 a 开始。
  2. 数据类型不匹配 :隐式类型转换,如 varchar 字段条件传入 int
  3. 索引列使用函数或运算 :如 WHERE DATE(create_time) = ...WHERE age + 1 > 20
  4. LIKE以通配符开头LIKE '%张' 失效,LIKE '张%' 有效。
  5. 使用否定操作符NOT IN!=NOT EXISTS 等可能导致失效。
  6. OR条件:如果OR两侧的列不是都有索引,可能失效。

24. 乐观锁与悲观锁

问题:聊一下MySQL的乐观锁和悲观锁。

要点

  1. 基本概念。
  2. 实现方式。
  3. 优缺点。
  4. 使用场景。

回答

  1. 悲观锁 :认为并发冲突一定会发生,操作前先加锁。SELECT ... FOR UPDATE 实现行锁。
    • 优点:保证强一致性,简单。
    • 缺点:性能差,可能导致死锁。
  2. 乐观锁 :认为冲突很少发生,提交更新时才检查。通过 版本号CAS 实现。
    • 优点:并发性能高,无死锁。
    • 缺点:冲突频繁时大量重试,不保证强一致。
  3. 场景建议:写多读少用悲观锁;读多写少用乐观锁。

Redis

25. 分布式锁

问题:聊一下Redis分布式锁的实现。

要点

  1. 不加锁的问题。
  2. synchronized 问题。
  3. setnx
  4. setnx + expire
  5. Lua脚本。
  6. Redisson。

回答

  1. 不加锁:并发超卖。
  2. synchronized:仅单机有效,集群下失效。
  3. setnxsetnx lock_key uuid。可保证互斥,但无过期时间,宕机可能死锁。
  4. setnx + expire :两命令非原子,中间宕机仍可能死锁。set lock_key uuid NX PX 30000 可保证原子性。
  5. Lua脚本:将加锁/解锁逻辑写入Lua,保证原子性。
  6. Redisson :生产推荐。提供 看门狗机制(自动续期)、可重入锁、公平锁等,解决了锁过期和误删问题。但主从切换时仍可能丢锁(RedLock方案有争议)。

26. 高并发三大问题

问题:聊一下Redis高并发下的缓存击穿、雪崩、穿透。

要点

  1. 三个问题的定义及解决方案。

回答

  1. 击穿热点key 过期,大量请求直达DB。
    • 方案 :互斥锁(setnx)、逻辑过期(值中存过期时间,异步更新)、永不过期。
  2. 雪崩大量key 同时过期,或Redis宕机,DB压力陡增。
    • 方案:过期时间加随机偏移、数据预热、Redis集群、降级限流。
  3. 穿透 :查询 不存在的数据 ,缓存和DB都查不到。
    • 方案 :缓存空对象(短过期时间)、布隆过滤器(先过滤不存在的数据)、接口层校验。

27. 过期策略

问题:聊一下Redis的过期策略。

要点

  1. 过期字典。
  2. 三种策略。
  3. Redis实际采用的策略。

回答

  1. 过期字典:Redis维护一个字典,存储key及其过期时间戳,用于快速判断key是否过期。
  2. 三种策略
    • 定时删除:为每个key创建定时器,内存友好但CPU不友好,未采用。
    • 惰性删除:访问时检查,CPU友好但可能内存泄露。
    • 定期删除:定时随机抽查一部分key,删除其中过期的。是CPU和内存的折中方案。
  3. Redis实际策略惰性删除 + 定期删除 相结合。

28. 淘汰策略

问题:聊一下Redis的内存淘汰策略。

要点

  1. 8种淘汰策略。
  2. LRU算法原理。
  3. LFU算法原理。

回答

  1. 8种策略
    • noeviction:不淘汰,写操作报错(默认)。
    • allkeys-lru:所有key中淘汰最近最少使用。
    • volatile-lru:设置过期key中淘汰LRU。
    • allkeys-random:随机淘汰。
    • volatile-random:设置过期key中随机淘汰。
    • volatile-ttl:淘汰剩余时间最短的。
    • allkeys-lfu:淘汰最不经常使用的(4.0+)。
    • volatile-lfu:设置过期key中淘汰LFU。
  2. LRU(近似) :随机取N个key,淘汰其中 空闲时间 最长的。通过抽样(默认5)避免维护全局链表。
  3. LFU:基于访问频率淘汰。使用对数计数器记录频率,并随时间衰减,避免历史数据长期占据。

29. 数据同步

问题:聊一下Redis与MySQL的数据同步策略。

要点

  1. 旁路缓存。
  2. 双删缓存。
  3. 延迟双删。
  4. 基于binlog监听。

回答

  1. 旁路缓存:读时先查Redis,未命中查DB后写Redis;写时更新DB,再删除Redis。并发下可能读到旧数据。
  2. 双删缓存:写时先删Redis,再更新DB,最后再删一次Redis。仍可能在更新DB前,其他线程读旧数据并写缓存。
  3. 延迟双删:在双删基础上,第二次删除延迟执行,确保其他线程"读旧数据并写缓存"的操作在第二次删除之前完成。
  4. 基于binlog :通过 Canal 监听MySQL binlog,同步至 Kafka,消费者再更新Redis。解耦、最终一致,是推荐方案。

30. 数据类型

问题:聊一下Redis的几种数据类型。

要点:常用数据类型及底层结构、场景。

回答

  1. String:SDS。用于缓存、计数器、分布式锁。
  2. Hash:ziplist或hashtable。用于存储对象、购物车。
  3. List:quicklist。用于消息队列、最新列表。
  4. Set:intset或hashtable。用于标签、共同好友、抽奖。
  5. ZSet:ziplist或skiplist+dict。用于排行榜、延迟队列。
  6. 新类型:Bitmaps(签到)、HyperLogLog(UV统计)、GEO(地理位置)、Stream(消息流)。

31. 持久化

问题:聊一下Redis的持久化机制。

要点

  1. RDB。
  2. AOF。
  3. 混合持久化。

回答

  1. RDB :在指定时间间隔生成数据集快照。触发方式(savebgsave)。优点(文件紧凑、恢复快);缺点(可能丢失最后一次快照后的数据)。
  2. AOF :记录所有写操作命令。刷盘策略(alwayseverysecno)。优点(数据安全、可读性强);缺点(文件大、恢复慢)。
  3. 混合持久化(4.0+):AOF文件以RDB格式开头,后跟增量命令。结合了RDB的快速恢复和AOF的数据安全优势。

32. 高可用方案

问题:聊一下Redis的高可用方案。

要点

  1. 主从复制。
  2. 哨兵模式。
  3. 集群模式。

回答

  1. 主从复制:一主多从,主写从读。解决了数据备份和读写分离,但故障需手动切换,写能力有单点瓶颈。
  2. 哨兵模式:在主从复制基础上,增加哨兵集群进行监控、自动故障转移。解决了高可用,但写能力仍为单点。
  3. 集群模式:数据分片(16384个槽位),每个槽位对应一个主从节点组。解决了写能力扩展和容量扩展问题,但客户端需支持集群协议。

Spring

33. Bean生命周期

问题:聊一下Spring Bean的生命周期。

要点:从实例化到销毁的完整过程。

回答

  1. 实例化:通过反射调用构造函数创建对象实例。
  2. 属性注入 :填充Bean属性,执行Aware接口回调(BeanNameAwareBeanFactoryAwareApplicationContextAware)。
  3. 初始化
    • BeanPostProcessor.postProcessBeforeInitialization
    • 执行初始化方法:@PostConstructInitializingBean.afterPropertiesSet()init-method
    • BeanPostProcessor.postProcessAfterInitializationAOP代理创建时机)。
  4. 使用:Bean完全就绪,可供应用使用。
  5. 销毁@PreDestroyDisposableBean.destroy()destroy-method

34. IOC与AOP

问题:聊一下Spring的IOC和AOP。

要点

  1. IOC、DI、AOP定义。
  2. 项目中IOC/DI使用场景。
  3. 项目中AOP使用场景。
  4. AOP核心术语(切面、通知、连接点、切入点)。
  5. 通知种类。

回答

  1. IOC(控制反转) :将对象的创建、组装、管理权交给外部容器,应用程序从主动new变为被动接收,达到解耦目的。
    • DI(依赖注入):IOC的具体实现,容器动态地将依赖注入到组件中(构造器、Setter、字段注入)。
  2. AOP(面向切面编程):将分散在各处的横切关注点(日志、事务等)模块化成切面,在运行时动态织入到业务方法中。
  3. 项目中使用IOC/DIController中注入ServiceService中注入Mapper
  4. 项目中使用AOP@Transactional(事务管理)、自定义切面记录日志或权限校验。
  5. AOP术语
    • 切面:封装横切关注点的模块(类)。
    • 通知:切面要完成的工作,定义了"何时"做。
    • 连接点:程序执行的某个点(如方法执行)。
    • 切入点:匹配连接点的表达式,定义了"何处"做。
  6. 通知种类@Before@After@AfterReturning@AfterThrowing@Around

35. SpringMVC原理

问题:聊一下SpringMVC的核心原理。

要点

  1. DispatcherServlet 核心工作。
  2. HandlerMapping
  3. HandlerAdapter
  4. ViewResolver
  5. 调用链路及设计思想。

回答

  1. DispatcherServlet:前端控制器,请求入口。统一接收请求,协调各组件处理,是流程控制中心。
  2. HandlerMapping :处理器映射器。根据请求URL找到能处理的 Handler(Controller方法)。
  3. HandlerAdapter :处理器适配器。统一调用不同 Handler,负责参数解析、数据绑定、返回值处理。
  4. ViewResolver :视图解析器。将 ModelAndView 中的逻辑视图名解析为具体的 View 对象(如JSP、Thymeleaf)。
  5. 调用链路
    • 请求 → DispatcherServlet
    • DispatcherServletHandlerMapping → 得到 Handler
    • DispatcherServletHandlerAdapter → 调用 Handler → 得到 ModelAndView
    • DispatcherServletViewResolver → 得到 View
    • DispatcherServlet → 渲染视图 → 响应。
    • 设计思想责任分离,各组件通过标准接口解耦,可灵活替换。非串行调用保证了扩展性。

36. SpringBoot启动流程

问题:SpringBoot项目的启动流程是什么?

要点:按步骤回答。

回答

  1. 运行 main 方法,执行 SpringApplication.run()
  2. 创建 SpringApplication,推断应用类型(Web/非Web)。
  3. 加载 META-INF/spring.factories,获取所有 ApplicationListener 等初始化器。
  4. 根据推断结果,启动内嵌的Servlet容器(如Tomcat)。
  5. 创建并刷新 ApplicationContext
    • 扫描并加载 @Component 等组件。
    • 执行自动装配(加载 spring.factories 中配置类)。
    • 加载外部配置(application.properties/yml),绑定到 @ConfigurationProperties Bean。
    • 执行各组件初始化逻辑(如数据库连接池、缓存、MQ)。
  6. 调用 ApplicationRunnerCommandLineRunner
  7. 启动内嵌Servlet容器,监听端口。
  8. 启动完毕,等待请求。

37. 自动装配原理

问题:SpringBoot的自动装配流程是什么?

要点

  1. 定义及好处。
  2. 流程步骤。

回答

  1. 定义 :框架根据类路径依赖、环境、配置,自动注册Bean的机制。
    • 好处:减少配置,降低出错,开箱即用。
  2. 流程
    • 启动时,@EnableAutoConfiguration 导入 AutoConfigurationImportSelector
    • 加载所有 META-INF/spring.factories,获取 EnableAutoConfiguration 键对应的配置类列表。
    • 根据 @ConditionalXXX 条件注解(如 @ConditionalOnClass@ConditionalOnMissingBean)进行筛选。
    • 符合条件的配置类中的 @Bean 方法被注册到容器。
    • 加载外部配置,通过 @ConfigurationProperties 绑定属性,对自动配置的Bean进行定制。
    • 执行各组件的初始化逻辑。
    • 完成所有自动装配和初始化。

消息队列 (RocketMQ)

38. RocketMQ核心原理

问题:聊一下RocketMQ的核心组件和消息收发流程。

要点

  1. 四大核心组件。
  2. 消息发送、存储、消费流程。
  3. 与其他MQ对比。

回答

  1. 四大组件
    • NameServer:轻量级路由中心,管理Broker集群元数据,无状态可集群。
    • Broker:消息存储核心,负责消息接收、存储、投递。可集群部署(Master-Slave)。
    • Producer:消息生产者。
    • Consumer:消息消费者,支持Pull和Push两种模式。
  2. 收发流程
    • Producer从NameServer获取Topic路由信息,选择一个Master Broker发送消息。
    • Broker收到消息后,写入CommitLog(顺序写),再转发至ConsumeQueue(索引)。
    • Consumer从NameServer获取路由,向Broker发起Pull请求,获取消息后消费,并提交位点。
  3. 与其他MQ对比
    • RocketMQ vs Kafka:RocketMQ事务消息、延迟消息、死信队列功能更丰富;Kafka吞吐量更高,适合海量日志场景。
    • RocketMQ vs RabbitMQ:RocketMQ吞吐量高、支持分布式事务、顺序消息;RabbitMQ社区活跃、延迟低、支持复杂路由。
    • 选型建议:金融/电商订单场景用RocketMQ;大数据日志用Kafka;企业内部简单场景用RabbitMQ。

39. 消息可靠性保障

问题:RocketMQ如何保证消息不丢失?

要点:生产端、存储端、消费端三端保障。

回答

  1. 生产端 :同步发送/事务消息,等待Broker确认(sendStatus 为SEND_OK)。若失败,可重试或记录日志。
  2. 存储端
    • 同步刷盘:消息写入CommitLog后同步落盘,性能较低但可靠。
    • 同步复制:Master收到消息后,同步复制到Slave,返回成功。
    • 主从切换:Master宕机,Slave可快速切换。
  3. 消费端 :消费成功后,再提交位点(CONSUME_SUCCESS)。若业务异常,可返回 RECONSUME_LATER,消息会重试。

40. 消息重复消费及解决方案

问题:RocketMQ如何解决消息重复消费问题?

要点:重复消费的原因及幂等处理。

回答

  1. 重复原因
    • 生产者:发送超时后重试,可能造成Broker端消息重复。
    • 消费者:消费成功但因网络/宕机未能及时提交位点,重启后重新拉取。
  2. 解决方案幂等处理
    • 唯一ID :使用 messageId 或业务唯一ID(如订单号)。
    • Redis去重 :消费前 SETNX messageId,成功则消费,失败则跳过。
    • 数据库唯一键:利用数据库唯一索引,重复插入会抛异常,业务捕获后忽略。
    • 状态机:根据消息状态判断是否已处理。

41. 消息顺序性保障

问题:如何保证RocketMQ的顺序消息?

要点:全局顺序 vs 分区顺序。

回答

  1. 分区顺序 :RocketMQ默认支持的顺序消息模型。通过 MessageQueueSelector 将同一业务ID(如订单号)的消息发送到同一个Queue,Consumer端使用一个线程消费一个Queue,即可保证该分区内消息的顺序性。
  2. 全局顺序:牺牲高可用和吞吐量,使用一个Topic只有一个Queue。不推荐。
  3. 实现 :Producer端 SendResult send(Message msg, MessageQueueSelector selector, Object arg);Consumer端注册 MessageListenerOrderly

42. 延迟消息与事务消息

问题:聊一下RocketMQ的延迟消息和事务消息。

要点:使用场景及实现。

回答

  1. 延迟消息
    • 使用场景:订单超时取消、定时任务。
    • 实现msg.setDelayTimeLevel(3),支持18个预定义级别(1s, 5s, ... 2h)。
    • 原理 :消息写入 SCHEDULE_TOPIC_XXXX,由专属定时线程转投至目标Topic。
  2. 事务消息
    • 使用场景:分布式事务(如下单-扣库存)。
    • 流程
      1. Producer发送半消息(Half Message)到Broker。
      2. Broker存储半消息,Producer执行本地事务。
      3. Producer向Broker发送提交/回滚状态。
      4. Broker若未收到最终状态,会回查Producer本地事务状态。
      5. 事务提交后,消息才被Consumer消费。

网络

43. TCP与UDP

问题:聊一下TCP和UDP的区别。

要点

  1. 基础概念。
  2. 网络开销。
  3. 可靠性。
  4. 数据容量限制。
  5. 重传机制。
  6. TCP流量控制和拥塞控制。

回答

  1. 基础概念
    • TCP:面向连接的可靠传输协议,类似打电话。
    • UDP:无连接的不可靠传输协议,类似寄明信片。
  2. 网络开销:TCP开销大(三次握手、四次挥手、ACK确认);UDP开销小(无连接、头部仅8字节)。
  3. 可靠性:TCP通过序列号、确认应答、重传等机制保证可靠;UDP不保证。
  4. 数据容量:TCP面向字节流,无严格限制;UDP单包最大64KB。
  5. 重传机制:TCP有超时重传和快速重传;UDP无重传。
  6. TCP流量控制 :滑动窗口协议,防止接收方缓冲区溢出。
    TCP拥塞控制:慢启动、拥塞避免、快速重传、快速恢复。

44. HTTP与HTTPS

问题:聊一下HTTP和HTTPS的区别。

要点

  1. 数据加密。
  2. 身份验证。
  3. 数据完整性。
  4. 信任和认证体系。
  5. 对抗劫持能力。
  6. HTTPS工作原理。

回答

  1. 数据加密:HTTP明文传输;HTTPS通过SSL/TLS加密传输。
  2. 身份验证:HTTP无身份验证;HTTPS通过CA证书验证服务器身份。
  3. 数据完整性:HTTP无完整性保护;HTTPS通过MAC校验保证完整性。
  4. 信任体系:HTTPS基于PKI体系和证书链,浏览器信任根CA,逐级验证服务器证书。
  5. 对抗劫持:HTTPS可有效防止DNS劫持和中间人攻击。
  6. TLS握手流程:Client Hello → Server Hello(含证书)→ 客户端验证证书并加密预主密钥 → 服务器解密生成会话密钥 → 后续对称加密通信。

45. 三次握手与四次挥手

问题:聊一下TCP的三次握手和四次挥手。

要点

  1. 三次握手流程。
  2. 四次挥手流程。
  3. 为何握手三次、挥手四次。
  4. 为何不能两次握手。

回答

  1. 三次握手流程
    • 第一次:客户端发 SYN=1, seq=x
    • 第二次:服务端回复 SYN=1, ACK=1, seq=y, ack=x+1
    • 第三次:客户端回复 ACK=1, seq=x+1, ack=y+1。连接建立。
  2. 四次挥手流程
    • 第一次:主动方发 FIN=1, seq=u
    • 第二次:被动方回复 ACK=1, ack=u+1(半关闭)。
    • 第三次:被动方数据发完后发 FIN=1, seq=v
    • 第四次:主动方回复 ACK=1, ack=v+1,等待2MSL后关闭。
  3. 为何握手三次、挥手四次
    • 握手时,服务器可将 SYNACK 合并发送,故为三次。
    • 挥手时,被动方收到 FIN 后可能还有数据要发,故 ACKFIN 不能合并,需四次。
  4. 为何不能两次握手:防止已失效的连接请求报文突然传到服务器,导致服务器误以为客户端要建立连接而白白等待,浪费资源。第三次握手是客户端对服务器允许连接的最终确认。

46. 浏览器HTTP请求详细流程

问题:聊一下从输入URL到页面展示的完整过程。

要点

  1. DNS解析。
  2. 建立TCP连接。
  3. 发送HTTP请求。
  4. 服务器处理请求。
  5. 返回HTTP响应。
  6. 断开TCP连接。

回答

  1. DNS解析:域名 → IP地址。查询顺序:浏览器缓存 → 操作系统hosts → 本地DNS服务器(递归/迭代查询)。
  2. 建立TCP连接:向解析出的IP发起三次握手,建立可靠传输通道。
  3. 发送HTTP请求:构建请求报文(请求行、请求头、空行、请求体),通过Socket写入TCP缓冲区。
  4. 服务器处理请求:Web服务器(Nginx)接收 → 反向代理/负载均衡 → 应用服务器(Tomcat)解析路由 → Controller执行业务逻辑。
  5. 返回HTTP响应:构建响应报文(状态行、响应头、空行、响应体),通过TCP连接返回。
  6. 断开TCP连接:HTTP/1.0默认短连接(请求完即断开);HTTP/1.1+默认长连接(keep-alive),空闲超时或页面加载完后触发四次挥手断开。HTTPS全程在TLS加密隧道中进行。

设计模式

47. 单例设计模式

问题:手写一个单例设计模式。

要点

  1. 饿汉式的优缺点及写法。
  2. 懒汉式的优缺点及写法。
  3. 静态内部类Holder模式的优缺点及写法。
  4. 枚举单例模式的优缺点及写法。

回答

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) ⭐⭐⭐⭐
静态内部类 ⭐⭐⭐⭐⭐
枚举 ⭐⭐⭐⭐⭐
相关推荐
wb043072014 分钟前
使用 Java 开发 MCP 服务并发布到 Maven 中央仓库完整指南
java·开发语言·spring boot·ai·maven
Rsun045515 分钟前
设计模式应该怎么学
java·开发语言·设计模式
5系暗夜孤魂28 分钟前
系统越复杂,越需要“边界感”:从 Java 体系理解大型工程的可维护性本质
java·开发语言
二月夜41 分钟前
Spring循环依赖深度解析:从三级缓存原理到跨环境“灵异”现象
java·spring
nbwenren1 小时前
Springboot中SLF4J详解
java·spring boot·后端
wellc2 小时前
java进阶知识点
java·开发语言
灰色小旋风2 小时前
力扣合并K个升序链表C++
java·开发语言
_MyFavorite_2 小时前
JAVA重点基础、进阶知识及易错点总结(28)接口默认方法与静态方法
java·开发语言·windows
helx822 小时前
SpringBoot中自定义Starter
java·spring boot·后端
_MyFavorite_2 小时前
JAVA重点基础、进阶知识及易错点总结(31)设计模式基础(单例、工厂)
java·开发语言·设计模式