科大讯飞Java开发【牛客网真实面经整理】
以下是我自己搜集的比较全面的牛客网上的面经,然后用deepseek生成的答案,分享一下。
以下是我自己搜集的比较全面的牛客网上的面经,然后用deepseek生成的答案,分享一下。
一、Java 基础
- Java 为什么是跨平台的?Java 和 C++ 有什么区别?
● 跨平台原因:Java 源码(.java)被编译成与平台无关的字节码(.class),而不是机器码。然后由针对不同平台的 Java 虚拟机(JVM)来解释执行字节码。因此,只要设备上有对应的 JVM,就能运行 Java 程序,实现了“一次编译,到处运行”。
● 与 C++ 的区别:
○ 内存管理:Java 有自动垃圾回收(GC)机制,开发者无需手动释放内存;C++ 需要手动 new/delete 进行内存管理。
○ 指针:Java 取消了显式的指针概念,提供了引用,更安全;C++ 有指针,功能强大但容易出错。
○ 多重继承:Java 类不支持多重继承(通过 extends),但可以通过实现多个接口(implements)来模拟;C++ 支持多重继承。
○ 预处理器:Java 没有预处理器;C++ 有 #define 等预处理功能。 - 重载(Overload)和重写(Override)的区别?
● 重载:发生在同一个类中,方法名相同,但参数列表不同(类型、个数、顺序)。与返回值类型和访问修饰符无关。
● 重写:发生在子类和父类之间,方法名、参数列表、返回值类型都必须相同。访问权限不能比父类更严格,抛出的异常不能比父类更宽泛。 - final 关键字的作用?
● 修饰类:该类不能被继承。
● 修饰方法:该方法不能被子类重写。
● 修饰变量:该变量变为常量,一旦初始化后其值就不能再被修改(对于基本类型,值不变;对于引用类型,引用指向的地址不变,但对象内部的状态可能改变)。 - 重写 equals() 为什么一定要重写 hashCode()?
● 这是基于 HashMap、HashSet 等哈希集合的通用约定:如果两个对象通过 equals() 方法比较是相等的,那么调用这两个对象的 hashCode() 方法必须返回相同的整数结果。
● 如果不重写:假设两个对象 a 和 b 在业务逻辑上是相等的(a.equals(b) == true),但它们可能产生不同的 hashCode。当将 a 存入 HashSet 后,再用 b 去检查是否存在时,会因为 hashCode 不同而直接去错误的哈希桶里查找,导致找不到 a,从而认为 b 不存在,这违反了集合的逻辑。 - Object 类有哪些常用方法?
● getClass(): 返回对象的运行时类。
● hashCode(): 返回对象的哈希码值。
● equals(Object obj): 判断两个对象是否“相等”。
● clone(): 创建并返回此对象的一个副本。
● toString(): 返回对象的字符串表示。
● notify(), notifyAll(), wait(): 与线程同步相关的方法。 - 泛型是什么?为什么需要泛型?Java 和 C++ 的泛型有什么区别?
● 是什么/为什么:泛型是参数化类型,允许在定义类、接口、方法时使用类型参数。它提供了编译时类型安全检查,避免了运行时的 ClassCastException;同时无需进行显式的类型转换,代码更简洁。
● Java vs C++:
○ 实现机制:C++ 泛型是模板,在编译时进行类型展开,会为每一种类型参数生成一份新的代码,可能导致代码膨胀。Java 泛型是类型擦除,在编译后类型参数会被擦除为其边界类型(如 Object),只在编译阶段进行类型检查,运行时没有泛型信息。
○ 实例化:C++ 模板可以使用基本数据类型;Java 泛型的类型参数必须是引用类型,使用基本类型需要装箱。 - 异常和错误的区别?
● Error:是程序无法处理的严重错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM 出现的问题(如 OutOfMemoryError)。应用程序不应捕获处理这些错误。
● Exception:是程序本身可以处理的异常。分为受检异常(必须被捕获或声明抛出,如 IOException)和非受检异常(即运行时异常,RuntimeException 及其子类,如 NullPointerException,通常由程序逻辑错误引起,不强制要求处理)。 - 常量池是什么?
● Java 中的常量池分为静态常量池(.class 文件中的常量池)和运行时常量池(JVM 方法区的一部分,存放编译期生成的各种字面量和符号引用)。
● 通常我们讨论的是字符串常量池(String Table),它是运行时常量池的一部分,用于存储字符串对象的引用,以避免创建重复的字符串对象,实现字符串的复用。 - 反射机制?
● 反射允许程序在运行时动态地获取类的信息(如类名、方法、字段、注解等)并操作类的字段和方法。核心类:Class, Method, Field, Constructor。
● 优点:灵活性高,可以实现动态创建对象、调用方法。
● 缺点:性能较低,破坏了封装性。 - 面向对象三大特征(封装、继承、多态)?哪个最重要?
● 封装:将数据和行为组合在类中,并隐藏内部实现细节,只暴露必要的接口。
● 继承:子类继承父类的特征和行为,实现代码复用和扩展。
● 多态:同一操作作用于不同的对象,可以有不同的解释和执行结果。主要包括编译时多态(重载)和运行时多态(重写)。
● 哪个最重要:这是一个开放性问题。封装通常被认为是基石,因为它定义了对象的边界和交互方式,是继承和多态的基础。良好的封装是设计高质量、低耦合系统的前提。
二、Java 集合
- 常见集合的分类、特点及继承关系?哪些允许重复?哪些不允许?
● Collection (单列集合)
○ List (有序、可重复)
■ ArrayList: 基于动态数组,随机访问快,增删慢。
■ LinkedList: 基于双向链表,增删快,随机访问慢。
■ Vector: 线程安全的动态数组,已过时。
○ Set (无序、不可重复)
■ HashSet: 基于 HashMap,无序。
■ LinkedHashSet: 维护插入顺序的 HashSet。
■ TreeSet: 基于红黑树,元素有序。
● Map (双列集合,键值对)
○ HashMap: 基于哈希表,无序。
○ LinkedHashMap: 维护插入顺序或访问顺序。
○ TreeMap: 基于红黑树,键有序。
○ Hashtable: 线程安全的 HashMap,已过时。
○ ConcurrentHashMap: 线程安全的高性能 HashMap。
2. ArrayList 和 LinkedList 的区别?底层数据结构?动态数组如何实现(扩容机制)?双向链表和单向链表的区别?
● 区别:
○ ArrayList:基于动态数组,支持 O(1) 的随机访问,但在中间插入或删除元素需要移动后续所有元素,时间复杂度 O(n)。
○ LinkedList:基于双向链表,插入和删除元素(只需修改指针)很快,时间复杂度 O(1),但随机访问需要遍历,时间复杂度 O(n)。
● ArrayList 扩容:
a. 创建一个新的、更大的数组(默认是原容量的 1.5 倍)。
b. 将旧数组中的数据浅拷贝(复制引用)到新数组中。
c. 将新数组设置为底层存储数组。
● 双向链表 vs 单向链表:
○ 单向链表:每个节点(Node)包含数据和指向下一个节点的指针(next)。只能从头到尾单向遍历。
○ 双向链表:每个节点包含数据、指向前一个节点的指针(prev)和指向下一个节点的指针(next)。可以双向遍历,删除某个已知节点时效率更高(因为可以直接找到前驱节点)。
3. HashMap 的底层实现(数据结构、put/get 流程、扩容机制)?为什么容量是 2 的幂?线程安全方案?与 TreeMap 的区别?
● 数据结构:JDK 1.8 之前是数组+链表,1.8 及之后是数组+链表/红黑树。当链表长度超过阈值(默认为 8)且数组长度大于 64 时,链表会转化为红黑树;当树节点数小于 6 时,会退化为链表。
● put 流程:
a. 计算 key 的 hash 值。
b. 如果数组为空,则进行初始化扩容。
c. 计算应存入的数组索引 i((n - 1) & hash)。
d. 如果 table[i] 为空,直接插入。
e. 如果不为空,则遍历链表或树:
■ 如果 key 已存在,则覆盖 value。
■ 如果不存在,则插入到链表末尾或树中。
f. 插入后,如果元素总数超过阈值(容量 * 负载因子,默认 0.75),则进行扩容。
● 扩容机制:创建一个新数组(大小为原来的 2 倍),然后重新计算所有元素的位置(rehash),并放入新数组。
● 容量为 2 的幂的原因:为了高效计算索引 index = (n - 1) & hash。当 n 是 2 的幂时,n-1 的二进制位全为 1,这使得 & 操作的结果能均匀分布在 [0, n-1] 范围内,减少哈希碰撞。
● 线程安全方案:
○ 使用 Collections.synchronizedMap(new HashMap())。
○ 使用 ConcurrentHashMap(推荐)。
● HashMap vs TreeMap:
○ HashMap:基于哈希表,无序,O(1) 时间复杂度(平均)。
○ TreeMap:基于红黑树,键有序(可按自然顺序或自定义比较器排序),O(log n) 时间复杂度。
4. ConcurrentHashMap 的底层实现(JDK 1.7 vs 1.8)?
● JDK 1.7:采用 Segment 分段锁 机制,将数据分段存储,每段配一把锁,多线程访问不同段就不冲突。
● JDK 1.8:摒弃了分段锁,改用 synchronized + CAS 对单个数组元素(桶的头节点)进行加锁,粒度更细,并发度更高。同时使用 volatile、CAS 等实现无锁化的读操作和并发控制。
三、JVM
- JVM 内存模型(运行时数据区)?哪些是线程共享的?哪些是线程私有的?栈里有什么?
● 线程共享:
○ 堆:存放对象实例和数组。是 GC 管理的主要区域。
○ 方法区:存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等。JDK 1.8 后称为“元空间”(Metaspace)。
● 线程私有:
○ 程序计数器:当前线程所执行的字节码的行号指示器。
○ 虚拟机栈:每个方法执行时会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
○ 本地方法栈:为 Native 方法服务。
● 栈帧内容:局部变量表(存放基本数据类型和对象引用)、操作数栈、动态链接、方法返回地址等。 - 对象创建过程?
- 类加载检查:检查 new 指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个类是否已被加载、解析和初始化过。
- 分配内存:在堆中为新生对象分配内存(方式有“指针碰撞”和“空闲列表”)。
- 初始化零值:将分配到的内存空间都初始化为零值。
- 设置对象头:设置对象的哈希码、GC 分代年龄、元数据信息等。
- 执行 init 方法:按照程序员的意愿进行初始化(构造方法)。
- 垃圾回收机制(哪些内存需要回收?如何判断对象已死?垃圾回收算法?常见的垃圾回收器?)
● 回收区域:主要回收堆和方法区。
● 判断对象已死:
○ 引用计数法:循环引用问题。
○ 可达性分析算法(主流):从 GC Roots 对象作为起点,向下搜索,走过的路径称为引用链。如果一个对象到 GC Roots 没有任何引用链相连,则证明此对象不可用。
● 垃圾回收算法:
○ 标记-清除:标记所有需要回收的对象,然后统一回收。问题:效率不高,产生内存碎片。
○ 标记-复制:将内存分为两块,每次只使用一块。当这一块用完了,就将还存活的对象复制到另一块上,然后把已使用的内存空间一次清理掉。优点:没有碎片。缺点:内存利用率低。
○ 标记-整理:标记过程与“标记-清除”一样,但后续不是直接清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。优点:没有碎片,利用率高。
● 常见垃圾回收器:
○ Serial/Serial Old:单线程,新生代(复制算法),老年代(标记-整理)。
○ ParNew:Serial 的多线程版本。
○ Parallel Scavenge/Old:吞吐量优先的收集器。
○ CMS:以获取最短回收停顿时间为目标,基于“标记-清除”算法。过程:初始标记 -> 并发标记 -> 重新标记 -> 并发清除。
○ G1:面向服务端的收集器,将堆划分为多个 Region,可预测停顿时间。
○ ZGC:JDK 11 引入,低延迟垃圾回收器。 - Full GC 和 Young GC 的区别?Full GC 的触发条件?
● Young GC (Minor GC):只回收新生代(Eden 和 Survivor 区),频率高,速度快。
● Full GC (Major GC):回收整个堆(新生代和老年代)和方法区,速度慢,停顿时间长。
● Full GC 触发条件:
○ 老年代空间不足。
○ 方法区(元空间)空间不足。
○ System.gc() 调用(建议而非强制)。
○ CMS GC 过程中的“Concurrent Mode Failure”等。 - 如何排查频繁 Full GC 和 CPU 飙升的问题?
● 频繁 Full GC 排查流程:
a. 监控:使用 jstat -gcutil 1000 观察 GC 频率和内存占用情况。
b. dump 堆内存:使用 jmap -dump:live,format=b,file=heap.hprof 导出堆转储文件。
c. 分析:使用 MAT (Eclipse Memory Analyzer)、JProfiler 等工具分析 heap dump,查看是什么对象占用了大量内存,并找到其 GC Roots,定位泄漏点(如内存泄漏、大对象、不合理的数据结构等)。
● CPU 飙升排查流程:
a. 定位高CPU线程:top -Hp 找到占用 CPU 最高的线程 ID。
b. 线程ID转换:将线程 ID 转换为 16 进制。
c. 查看线程栈:使用 jstack | grep -A 20 <nid(16进制)> 查看该线程的堆栈信息。
d. 分析:根据堆栈信息定位代码,常见原因:死循环、频繁 GC、锁竞争等。 - 强引用、软引用、弱引用、虚引用的区别?
● 强引用:最常见的引用,只要强引用存在,垃圾收集器就永远不会回收掉被引用的对象。
● 软引用:有用但非必需的对象。在内存不足时,会被回收。用 SoftReference 类实现。
● 弱引用:非必需对象,比软引用更弱。只能生存到下一次垃圾收集发生之前。用 WeakReference 类实现。
● 虚引用:最弱的引用,完全不会影响对象的生命周期。唯一目的是为了能在这个对象被收集器回收时收到一个系统通知。用 PhantomReference 类实现。
四、多线程与并发
- 线程和进程的区别?
● 根本区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位。
● 开销:进程有独立的地址空间,上下文切换开销大;线程共享进程的资源,上下文切换开销小。
● 通信:进程间通信(IPC)需要特殊机制(如管道、消息队列、共享内存等);线程间可以直接读写同一进程的数据段(如全局变量)来进行通信,但需要同步机制。 - 创建线程的方式?start() 和 run() 的区别?
● 创建方式:
a. 继承 Thread 类,重写 run() 方法。
b. 实现 Runnable 接口,实现 run() 方法,然后将 Runnable 实例作为参数传给 Thread 构造函数。(推荐,避免单继承局限)
c. 实现 Callable 接口,配合 FutureTask 使用,可以获取返回值。
d. 使用线程池(ExecutorService)。
● start() vs run():
○ start():启动一个新线程,新线程会执行 run() 方法。不能多次调用。
○ run():仅仅是普通的方法调用,会在当前线程中执行,不会启动新线程。 - 线程池(核心参数、工作流程、好处、使用场景、参数设计考量)?
● 核心参数(ThreadPoolExecutor):
○ corePoolSize:核心线程数。
○ maximumPoolSize:最大线程数。
○ keepAliveTime:非核心线程空闲存活时间。
○ unit:时间单位。
○ workQueue:任务队列。
○ threadFactory:线程工厂。
○ handler:拒绝策略。
● 工作流程:
a. 提交任务。
b. 如果运行线程数 < corePoolSize,创建新线程执行任务。
c. 否则,将任务放入 workQueue。
d. 如果队列已满且运行线程数 < maximumPoolSize,创建新线程执行任务。
e. 如果队列已满且线程数已达最大值,执行拒绝策略。
● 好处:降低资源消耗(线程复用)、提高响应速度(无需创建线程)、提高线程可管理性。
● 高并发下参数设计:需根据任务类型(CPU 密集型 vs IO 密集型)和硬件资源进行压测调优。通常 CPU 密集型可设置 corePoolSize = CPU核数 + 1,IO 密集型可设置 corePoolSize = 2 * CPU核数。 - 保证线程安全的方式(synchronized, Lock, volatile, CAS, ThreadLocal)?
● synchronized:JVM 层面的关键字,可修饰方法或代码块,保证原子性和可见性,可重入,非公平锁。
● Lock 接口(如 ReentrantLock):API 层面的锁,需要手动加锁解锁,更灵活(可尝试非阻塞获取锁、可中断、可设置公平/非公平)。
● volatile:保证变量的可见性和禁止指令重排序,但不保证原子性。
● CAS:Compare-And-Swap,一种乐观锁机制,在硬件层面(如 CPU 的 CMPXCHG 指令)实现,保证单个变量的原子性操作。java.util.concurrent.atomic 包下的类使用了 CAS。
● ThreadLocal:为每个线程提供一个独立的变量副本,实现了线程间的数据隔离,从根本上避免了共享变量的问题。 - synchronized 和 ReentrantLock 的区别?
● 本质:synchronized 是关键字,JVM 实现;ReentrantLock 是类,API 实现。
● 功能:ReentrantLock 更丰富,支持公平/非公平锁、可中断锁、超时获取锁、绑定多个条件(Condition)。
● 释放:synchronized 在代码块执行完或异常时自动释放;ReentrantLock 必须手动调用 unlock() 释放,通常在 finally 块中进行。
● 性能:在低竞争环境下,synchronized 优化后(偏向锁、轻量级锁)性能很好;高竞争环境下 ReentrantLock 通常表现更好。 - ReentrantLock 是公平锁还是非公平锁?
● ReentrantLock 默认是非公平锁。但可以通过构造函数 new ReentrantLock(true) 来创建一个公平锁。
● 公平锁:按照线程申请锁的顺序来获取锁,先到先得。
● 非公平锁:允许“插队”,后申请的线程可能比先申请的线程先获取到锁,吞吐量通常更高。 - CAS 原理?如果失败会发生什么?ABA 问题?
● 原理:包含三个操作数——内存位置(V)、预期原值(A) 和新值(B)。如果 V 的值等于 A,则将 V 的值更新为 B,否则什么都不做(或重试)。
● 失败后:通常会在一个循环中不断进行 CAS 操作(自旋),直到成功为止。
● ABA 问题:一个变量从 A 变为 B,然后又变回 A。CAS 操作会误以为它没有被修改过。解决方案是使用版本号(如 AtomicStampedReference)。 - AQS (AbstractQueuedSynchronizer) 原理?为什么适用于写的场景?基于什么场景使用?
● 原理:AQS 是一个用于构建锁和同步器的框架。它维护了一个volatile int state(代表共享资源)和一个FIFO 线程等待队列(CLH 变体)。通过 CAS 操作 state 来实现同步状态的管理。
● 适用写的场景:AQS 采用了自旋(CAS) 和 CLH 队列 的方式,减少了线程上下文切换的开销,这在写竞争激烈的场景下比传统的 synchronized(重量级锁涉及用户态/内核态切换)性能更高。
● 使用场景:ReentrantLock, Semaphore, CountDownLatch, ReentrantReadWriteLock 等都是基于 AQS 构建的。 - 讲讲 ThreadLocal?如何避免内存泄漏?
● 是什么:ThreadLocal 提供了线程局部变量,每个线程都可以通过 get() 和 set() 方法来访问属于自己的独立变量副本,避免了线程安全问题。
● 内存泄漏风险:
○ ThreadLocal 对象本身是弱引用(WeakReference),会被 GC 回收。
○ 但 ThreadLocal 中设置的 value 是强引用,只要线程(通常是线程池中的线程)一直存活,value 就无法被回收,即使 ThreadLocal 实例已被回收,从而造成内存泄漏。
● 如何避免:每次使用完 ThreadLocal 后,必须调用 remove() 方法来清除当前线程的 value。
五、Spring & Spring Boot
- Spring 框架的理解(IoC, AOP)?为什么好?
● IoC (控制反转):将对象的创建、组装、管理权交给 Spring 容器。开发者不再需要自己 new 对象,而是通过依赖注入(DI) 的方式从容器中获取所需依赖。好处:解耦,提高了代码的灵活性和可测试性。
● AOP (面向切面编程):将那些与业务无关,却为业务模块所共同调用的逻辑(如事务管理、日志、权限控制)封装起来,减少系统的重复代码,降低模块间的耦合度。好处:代码复用,关注点分离。
● 为什么好:通过 IoC 和 AOP 实现了高内聚、低耦合的软件设计目标,极大地简化了企业级应用的开发。 - Spring Bean 的生命周期?
- 实例化(通过构造函数或工厂方法)。
- 属性赋值( populate properties,依赖注入)。
- 调用 BeanNameAware 的 setBeanName() 方法。
- 调用 BeanFactoryAware 的 setBeanFactory() 方法。
- 调用 ApplicationContextAware 的 setApplicationContext() 方法(前置处理)。
- 调用 BeanPostProcessor 的 postProcessBeforeInitialization() 方法。
- 调用 InitializingBean 的 afterPropertiesSet() 方法。
- 调用自定义的 init-method 方法。
- 调用 BeanPostProcessor 的 postProcessAfterInitialization() 方法(Bean 已就绪)。
- 使用 Bean。
- 容器关闭时,调用 DisposableBean 的 destroy() 方法。
- 调用自定义的 destroy-method 方法。
- Spring 如何解决循环依赖?
● 循环依赖:A 依赖 B,B 又依赖 A。
● 解决原理:Spring 通过三级缓存来解决单例Bean的Setter注入/字段注入方式的循环依赖。
○ 一级缓存(singletonObjects):存放已经完全初始化好的单例 Bean。
○ 二级缓存(earlySingletonObjects):存放提前暴露的、尚未完成属性注入和初始化的原始 Bean 对象(早期引用)。
○ 三级缓存(singletonFactories):存放创建 Bean 的工厂 ObjectFactory。
● 过程(以 A 和 B 循环依赖为例):
a. 创建 A,实例化后将自己(一个 ObjectFactory)放入三级缓存。
b. A 进行属性注入,发现需要 B,于是去创建 B。
c. 创建 B,实例化后将自己放入三级缓存。
d. B 进行属性注入,发现需要 A,于是从三级缓存中找到 A 的 ObjectFactory,获取到 A 的早期引用(并将此引用放入二级缓存,同时从三级缓存删除),并注入给 B。
e. B 完成属性注入和初始化,变成一个完整的 Bean,放入一级缓存(并清空二、三级缓存)。
f. A 获取到完整的 B,注入成功,A 也完成初始化,放入一级缓存。
● 注意:构造器注入的循环依赖无法解决,因为 Java 在调用构造器之前,对象还未创建,无法提前暴露引用。 - Spring Boot 自动装配原理?启动过程?
● 自动装配原理:
a. @SpringBootApplication 注解包含了 @EnableAutoConfiguration。
b. @EnableAutoConfiguration 会导入 AutoConfigurationImportSelector。
c. 这个类会从 META-INF/spring.factories 文件中读取所有自动配置类的全限定名。
d. 根据条件注解(@ConditionalOnClass, @ConditionalOnProperty 等)判断是否生效,并将符合条件的配置类加载到 IoC 容器中,实现自动配置。
● 启动过程:
a. 创建 SpringApplication 实例。
b. 运行 run() 方法。
c. 准备环境(Environment)。
d. 创建应用上下文(ApplicationContext),如 AnnotationConfigApplicationContext。
e. 刷新上下文(核心refresh() 方法),这一步完成了 Bean 的加载、解析、注册、生命周期管理等所有 IoC 流程。
f. 执行 CommandLineRunner 和 ApplicationRunner。 - Spring MVC 工作原理?
- DispatcherServlet 接收前端请求。
- HandlerMapping 根据请求 URL 找到对应的 Controller 和方法(Handler)。
- HandlerAdapter 适配并执行找到的 Handler。
- Handler 执行完毕后返回一个 ModelAndView。
- ViewResolver 根据视图名解析出具体的 View 对象。
- View 进行视图渲染,将模型数据填充到视图中。
- 最终将响应返回给客户端。
- Spring AOP 的实现原理?在项目中如何使用(如记录日志)?如何保证多线程下的安全?
● 实现原理:基于动态代理。
○ 如果目标对象实现了接口,默认使用 JDK 动态代理。
○ 如果目标对象没有实现接口,则使用 CGLIB 字节码生成。
● 使用:通过 @Aspect 注解定义切面类,用 @Pointcut 定义切入点表达式,用 @Before, @After, @Around 等注解定义通知(Advice)。
● 多线程安全:AOP 切面本身通常是无状态的(不包含可变的成员变量),因此它本身是线程安全的。如果在通知中需要处理线程隔离的数据,应使用 ThreadLocal。 - 拦截器(Interceptor)和过滤器(Filter)的区别?
● 归属:Filter 是 Servlet 规范规定的,依赖于 Servlet 容器(如 Tomcat)。Interceptor 是 Spring MVC 框架提供的,依赖于 Spring 容器。
● 拦截范围:Filter 能过滤几乎所有请求(包括静态资源)。Interceptor 通常只拦截 Controller 的请求。
● 获取 Bean:在 Filter 中无法直接注入 Spring Bean,而 Interceptor 可以,因为它本身就在 Spring 容器中。
● 执行顺序:Filter -> DispatcherServlet -> Interceptor -> Controller。
六、MySQL & 数据库
- MySQL 的体系结构和一条 SQL 语句的执行过程?
● 连接器:管理连接,权限验证。
● 查询缓存:执行查询前先看缓存是否有(MySQL 8.0 已移除此功能)。
● 分析器:词法分析、语法分析,判断 SQL 语法是否正确。
● 优化器:生成执行计划,选择索引。
● 执行器:调用存储引擎接口,执行 SQL,返回结果。 - InnoDB 引擎的特性?为什么用 B+ 树而不是 B 树?
● 特性:支持事务、行级锁、外键、MVCC,是 MySQL 的默认存储引擎。
● B+ 树 vs B 树:
○ 查询效率更稳定:B+ 树所有数据都存储在叶子节点,任何查询都需要从根节点走到叶子节点,查询路径长度相同。
○ 范围查询更强:B+ 树叶节点间有指针相连,形成链表,便于范围查询和全表扫描。B 树需要进行中序遍历。
○ 磁盘读写代价更低:B+ 树内部节点(非叶子节点)不存储数据,只存储键和子节点指针,因此一个节点可以容纳更多的键,树的高度更低,减少了磁盘 I/O 次数。 - 事务的 ACID 特性?隔离级别?默认级别?
● ACID:
○ 原子性:事务要么全部完成,要么全部不完成。
○ 一致性:事务执行前后,数据库的完整性约束不被破坏。
○ 隔离性:并发事务之间互不干扰。
○ 持久性:事务提交后,对数据的修改是永久的。
● 隔离级别(从低到高):
○ 读未提交:可能发生脏读、不可重复读、幻读。
○ 读已提交:解决脏读,可能发生不可重复读、幻读。(Oracle、SQL Server 默认)
○ 可重复读:解决脏读、不可重复读,可能发生幻读。(MySQL InnoDB 默认)
○ 串行化:解决所有问题,但性能最低。
● MySQL 默认级别:可重复读。 - MVCC 是如何实现的?
● MVCC:多版本并发控制,是 InnoDB 实现读已提交和可重复读隔离级别的重要手段。
● 实现原理:
○ 每行记录都有两个(或三个)隐藏字段:DB_TRX_ID(最近修改该行的事务ID)、DB_ROLL_PTR(指向该行回滚段的指针,即 undo log 记录)。
○ 在事务开始时,会生成一个快照(Read View),其中记录了当前活跃的事务ID列表。
○ 当读取数据时,会根据 DB_TRX_ID 和 Read View 的规则来判断当前事务能看到的版本是哪个(可能是当前行,也可能是 undo log 中的某个历史版本)。 - 锁的种类?
● 按粒度:表锁、行锁、间隙锁。
● 按功能:
○ 共享锁(S锁):读锁,允许其他事务读,但不能写。
○ 排他锁(X锁):写锁,不允许其他事务加任何锁。
● 意向锁:表级锁,表示事务即将对表中的行加 S 锁或 X 锁(IS, IX),用于快速判断表是否被锁定。
● 间隙锁:锁定一个范围,但不包括记录本身。用于解决幻读问题。 - 索引(数据结构、创建原则、优化、失效场景、聚簇/非聚簇索引)?
● 数据结构:B+ 树。
● 创建原则:
○ 对查询频繁、区分度高的列创建索引。
○ 经常需要排序、分组和联合操作的字段建立索引。
○ 避免对经常更新的表创建过多索引。
○ 使用联合索引而非多个单列索引(考虑最左前缀原则)。
● 优化:
○ 使用 EXPLAIN 分析 SQL 执行计划。
○ 避免 SELECT *,只查询需要的字段。
○ 优化查询条件,避免在索引列上做计算、函数、类型转换等操作。
● 失效场景:
○ 违反最左前缀原则。
○ 在索引列上使用函数、计算、表达式。
○ 使用 !=、<>、IS NULL、IS NOT NULL(并非绝对,取决于优化器)。
○ 以通配符开头的 LIKE 查询(%abc)。
○ 类型转换(如字符串列用数字查询)。
● 聚簇索引 vs 非聚簇索引:
○ 聚簇索引:索引的叶节点就是数据行本身。一张表只能有一个聚簇索引(通常是主键)。
○ 非聚簇索引(二级索引):索引的叶节点存储的是主键值。查询时需要回表,即根据主键值再去聚簇索引中查找数据行。 - 慢查询如何优化?EXPLAIN 有哪些关键字段及其含义?
● 优化步骤:
a. 使用 EXPLAIN 分析 SQL。
b. 检查是否使用了索引,如果没有,考虑添加。
c. 检查索引是否失效。
d. 优化 SQL 语句(避免复杂 JOIN、子查询,分页优化等)。
e. 考虑数据库层面优化(调整参数、读写分离、分库分表)。
● EXPLAIN 关键字段:
○ type:访问类型,从好到坏:system > const > eq_ref > ref > range > index > ALL。
○ key:实际使用的索引。
○ rows:预估需要读取的行数。
○ Extra:额外信息,如 Using index(覆盖索引)、Using where、Using temporary(使用临时表)、Using filesort(需要额外排序)等。 - 分库分表的方式?
● 垂直分库:按业务将不同表拆分到不同数据库中。
● 垂直分表:将一个宽表按字段访问频率拆分成多个小表(“大表拆小表”)。
● 水平分库:将同一个表的数据按某种规则(如取模、范围)分布到不同的数据库中。
● 水平分表:将同一个表的数据按某种规则分布到同一个数据库的多个表中。
● 常用中间件:ShardingSphere, MyCat。 - 除了 MySQL 还知道哪些数据库?适用场景?
● Redis:内存键值数据库,用作缓存、Session 共享、消息队列、计数器等。
● Elasticsearch:分布式搜索引擎,适用于全文检索、日志分析、复杂聚合查询。
● MongoDB:文档数据库,适用于存储非结构化或半结构化数据,模式灵活。
● TiDB:分布式 NewSQL 数据库,兼容 MySQL 协议,适用于高可用、强一致、大数据量的 OLTP 和 OLAP 场景。
七、Redis
- Redis 数据类型及使用场景?
● String:缓存、计数器、分布式锁。
● Hash:存储对象(如用户信息)。
● List:消息队列、排行榜、最新列表。
● Set:去重、共同好友(交集)、随机推荐(srandmember)。
● ZSet:带权重的排行榜、延迟队列。
● Geo:地理位置信息。
● HyperLogLog:基数统计(UV统计)。
● BitMap:位操作(用户签到、活跃状态)。 - Redis 为什么快?单线程体现在哪?
● 快的原因:
a. 基于内存:数据存储在内存中,读写速度快。
b. IO 多路复用:使用 epoll 等机制,单线程处理大量网络连接。
c. 高效的数据结构:如跳跃表、哈希表等。
● 单线程:指的是 网络 I/O 和键值对读写 是由一个线程来完成的。所以避免了多线程的上下文切换和竞争条件。持久化、异步删除等操作是由其他线程执行的。 - 持久化机制?
● RDB:在指定的时间间隔生成数据集的快照。优点:文件小,恢复快。缺点:可能会丢失最后一次快照之后的数据。
● AOF:记录每次写操作命令。优点:数据完整性高。缺点:文件大,恢复慢。
● 混合持久化(4.0+):RDB 和 AOF 一起用。AOF 文件重写时,先把当前数据以 RDB 格式写入,后续的写操作再追加到 AOF 文件中。兼顾速度和数据安全。 - 缓存穿透、缓存击穿、缓存雪崩及解决方案?
● 缓存穿透:查询一个根本不存在的数据,缓存和数据库都不命中。
○ 解决:① 缓存空对象并设置短过期时间。② 使用布隆过滤器快速判断数据是否存在。
● 缓存击穿:某个热点key在过期瞬间,大量请求击穿缓存,直接访问数据库。
○ 解决:① 设置热点数据永不过期。② 使用互斥锁(如 Redis SETNX),只让一个请求去查数据库重建缓存,其他请求等待。
● 缓存雪崩:同一时间大量key集中过期,或Redis宕机,导致所有请求都落到数据库。
○ 解决:① 给过期时间加上随机值,避免同时过期。② Redis 高可用(集群、哨兵)。③ 服务降级和熔断。 - 集群模式?
● 主从复制:一主多从,主节点写,从节点读,实现读写分离和数据备份。
● 哨兵模式:监控主从节点,在主节点宕机时自动进行故障转移,选举新的主节点。
● Cluster 集群:数据分片(16384个槽),每个节点存储部分数据,实现水平扩展和高可用。
八、消息队列 (MQ)
- RabbitMQ 和 Kafka 的区别?为什么选择 RabbitMQ?
● RabbitMQ:
○ ** broker **,支持多种协议(AMQP)。
○ 消息可靠性强,支持多种确认机制。
○ 延迟队列(通过插件或死信队列实现)。
○ 适合对消息可靠性、顺序要求高的场景。
● Kafka:
○ 分布式流式平台,高吞吐、低延迟。
○ 持久化能力强,适合海量数据存储和实时流处理。
○ 消息有序性靠分区保证(同一分区内有序)。
○ 适合日志收集、流处理、大数据领域。
● 选择原因:可根据项目对吞吐量和可靠性的侧重来选择。RabbitMQ 在传统企业级应用、复杂路由、高可靠性场景更常见;Kafka 在大数据、日志处理场景是事实标准。 - 如何保证消息不被丢失?
● 生产者端:开启 confirm 模式,收到 broker 的 ack 确认。
● Broker 端:开启持久化(消息和队列都持久化)。
● 消费者端:关闭自动 ack,改为手动 ack,确保业务处理成功后再确认。 - 消费失败了怎么办?
● 如果业务允许,可以将消息放入死信队列,后续进行人工处理或重试。
● 如果不允许,可以在消费者代码中进行重试(注意幂等性),达到最大重试次数后记录日志并告警。
九、系统设计 & 项目
- 秒杀系统如何设计?考虑哪些问题?
● 核心思路:削峰填谷、层层过滤、极致的性能。
● 前端:静态化页面、CDN、按钮防重复提交、验证码。
● 网关:限流(令牌桶、漏桶)。
● 服务:
○ 独立部署,避免影响其他服务。
○ 库存缓存到 Redis 中,用 Lua 脚本保证原子性扣减。
○ 业务逻辑异步化(扣减库存成功后,发送 MQ 消息,由下游服务进行订单创建等耗时操作)。
● 数据库:最终一致性,分库分表。
● 问题:超卖、恶意请求、系统高可用、数据一致性。 - 如何保证数据一致性?
● 强一致性:分布式事务(如 2PC, 3PC, TCC),性能开销大。
● 最终一致性(更常用):
○ 使用 MQ 进行异步消息通知。
○ 基于数据库 binlog 的订阅(如 Canal)。
○ 保证操作的幂等性。 - 项目中的难点和解决方案?
● 这是一个开放性问题,需要结合你自己的项目经验回答。思路:
○ 背景:简单描述项目和你负责的模块。
○ 难点:遇到了一个什么样的具体问题(如性能瓶颈、并发问题、数据一致性问题、技术选型难题等)。
○ 探索:你尝试了哪些方法,查阅了哪些资料。
○ 解决方案:最终采用了什么方案,为什么选这个方案(可以提到技术原理,如用了 Redis 缓存、MQ 异步、分库分表等)。
○ 结果:方案的效果如何(如 QPS 从 100 提升到 1000,解决了超卖问题等)。 - 项目规模,负责内容,如何写设计文档和接口设计?
● 规模:说明项目是个人、小组还是公司级项目,用户量、数据量级。
● 负责内容:清晰说明你负责的模块和具体工作(如“负责用户中心的开发,包括登录注册、个人信息管理等接口”)。
● 设计文档:应包括需求背景、架构图、模块设计、数据库设计、接口设计、风险评估等。
● 接口设计:遵循 RESTful 风格,定义清晰的 URL、请求方法、请求/响应参数、状态码。使用 Swagger 等工具生成文档。
十、Linux & 运维
- 常用 Linux 命令?
● 进程相关:ps -ef | grep java, top, jps(Java进程), kill -9 。
● 文件相关:cat, tail -f(查看日志), grep, find, chmod。
● 网络相关:netstat -tlnp | grep 端口号, ping, curl。 - 容器化(Docker)和 K8s 了解吗?
● Docker:应用容器引擎,将应用及其依赖打包成一个镜像,实现一次构建,到处运行。
● Kubernetes (K8s):容器编排系统,用于自动化部署、扩展和管理容器化应用。核心概念:Pod, Deployment, Service, Ingress 等。
十一、学习与规划
- 学习渠道?
● 官方文档、技术书籍、优质博客(掘金、博客园)、视频课程(B站、极客时间)、GitHub 开源项目、技术社区(Stack Overflow, SegmentFault)。 - 有阅读过源码吗?
● 回答有或没有。如果看过,可以简要说说你看了哪个框架的哪部分源码(如 HashMap 的 put 方法、Spring 的 refresh 过程),并说说你的收获(如理解了实现原理、学习了设计模式)。 - 对自己的开发语言倾向和职业规划?
● 语言倾向:表达你对 Java 生态的深入和热爱,同时保持开放心态,表示愿意根据工作需要学习其他语言(如 Go, Python)。
● 职业规划:短期希望深入后端技术,成为某领域的专家;长期希望具备系统架构能力,并能带动团队成长。(结合应聘岗位进行调整) - 平时怎么解决问题?
● 先独立思考,查阅日志和文档。
● 使用搜索引擎(Google, Stack Overflow)和技术社区。
● 与同事讨论。
● 如果问题复杂,会简化问题,定位最小复现单元。
十二、手撕代码 & 场景题
- 0.1 + 0.2 != 0.3?浮点数底层存储?
● 原因:计算机使用二进制表示浮点数,而有些十进制小数(如 0.1)无法用二进制精确表示,会导致精度丢失。
● 底层存储:遵循 IEEE 754 标准,采用科学计数法表示:符号位 + 指数位 + 尾数位。
● 解决:使用 BigDecimal 进行精确计算。 - 如何求二叉树深度?
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
int leftDepth = maxDepth(root.left);
int rightDepth = maxDepth(root.right);
return Math.max(leftDepth, rightDepth) + 1;
} - 100G 字母文件,10G 内存如何排序?
● 外部排序:先拆分,再归并。
a. 分割:将 100G 文件分割成多个小块(如每个 1G),依次读入内存进行排序,然后将排好序的小块写入临时文件。
b. 归并:使用一个最小堆(大小为归并路数),每次从各个已排序的小文件中读取一个数据到堆中,取出堆顶元素(最小值)写入最终文件,然后从该元素所在文件再读入一个数据补充到堆中,重复此过程直到所有文件处理完毕。 - 红黑树的特点和结构?
● 特点:一种自平衡的二叉查找树,不是绝对平衡,但保证了从根到叶子的最长路径不超过最短路径的 2 倍,是近似平衡的。
● 五大性质:
a. 节点是红色或黑色。
b. 根节点是黑色。
c. 所有叶子节点(NIL)是黑色。
d. 红色节点的子节点必须是黑色(即不能有连续的两个红色节点)。
e. 从任一节点到其每个叶子节点的所有路径都包含相同数目的黑色节点。
希望这份超全面的总结能对你的面试准备有所帮助!祝你面试顺利!
更多推荐
所有评论(0)