2025年Java最全面试题,八股文速通
1、为什么重写 equals() 时必须重写 hashCode() 方法? ⭐⭐⭐⭐
hashCode() 和 equals() 方法之间有一个重要的契约(约定)。这个契约规定了,如果两个对象通过 equals() 方法比较认为是相等的(即 equals() 返回 true),那么这两个对象的 hashCode() 方法必须返回相同的哈希码。这个契约是确保哈希表(如 HashMap、HashSet)等哈希数据结构正常工作的基础。
2、字符串处理
1、String、StringBuffer、StringBuilder 的区别? ⭐⭐⭐⭐⭐
- String:不可变,线程安全,适合不需要修改的字符串。
- StringBuffer:可变,线程安全,适合多线程环境中频繁修改的字符串。
- StringBuilder:可变,不线程安全,适合单线程环境中频繁修改的字符串。
3、String 为什么是不可变的? ⭐⭐⭐
不可变原理:
-
final 类:String 类被声明为 final,这意味着它不能被继承,从而防止了通过继承改变其行为。
-
final 字段:String 类中的 value 字段是 final 的,这保证了 String 对象在创建后其内部字符数组不会被改变。
-
没有提供修改方法:String 类没有提供任何可以修改其内容的方法。所有对 String 的操作(如拼接、替换等)都会生成一个新的String 对象。
为什么设计成不可变
- 线程安全:在多线程环境下,不可变对象可以被安全共享,无需额外同步机制。
- 缓存哈希值: String 的哈希值在创建时被缓存(hash 字段),避免重复计算。
- 字符串常量池优化:JVM 维护字符串常量池(String Pool),相同字面量的字符串共享同一实例。
4、字符串拼接用“+” 还是 StringBuilder? ⭐⭐
- “+”运算符的底层机制:在单行代码中通过“+”拼接字符串(例如 String s = a+ b + c;),编译器会自动优化为一个 StringBuilder,调用其 append() 方法完成拼接,最终通过 toString() 生成新字符串。这种方式在简单场景下高效且代码简洁。
- 循环中使用“+”的缺陷:在循环内使用“+”拼接字符串时,编译器无法优化为单个 StringBuilder,每次循环都会创建新的 StringBuilder 对象。
- 显式使用 StringBuilder 的优势:直接在循环中使用 StringBuilder,可以复用同一个对象,避免内存浪费
5、字符串常量池的作用 ⭐⭐⭐
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
// 在堆中创建字符串对象”ab“
// 将字符串对象”ab“的引用保存在字符串常量池中
String aa = "ab";
// 直接返回字符串常量池中字符串对象”ab“的引用
String bb = "ab";
System.out.println(aa==bb);// true
AI写代码java运行
6、String s1 = new String(“abc”);这句话创建了几个字符串对象? ⭐⭐⭐
- 如果字符串常量池中不存在字符串对象“abc”的引用,那么它会在堆上创建两个字符串对象,其中一个字符串对象的引用会被保存在字符串常量池中
- 如果字符串常量池中已存在字符串对象“abc”的引用,则只会在堆中创建 1 个字符串对象“abc”
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc
需要全套面试笔记及答案【点击此处即可/免费获取】
https://docs.qq.com/doc/DQXdYWE9LZ2ZHZ1ho
1.4、异常处理
1、Exception 和 Error 有什么区别? ⭐⭐⭐⭐⭐
在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类:Exception :程序本身可以处理的异常,可以通过 catch 来进行捕获,Exception 又可以分为 Checked Exception 和 Unchecked Exception 。Error:属于程序无法处理的错误 ,不建议通过catch捕获
-
Error:表示系统级不可恢复的故障。例如 栈溢出(StackOverflowError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
-
Checked Exception 预期可能发生的异常(如文件不存在) ,Java 代码在编译过程中,如果受检查异常没有被 catch或者throws关键字处理的话,就没办法通过编译。除了RuntimeException及其子类以外,其他的Exception类及其子类都属于受检查异常。
常见的受检查异常有:IO 相关的异常、ClassNotFoundException、SQLException、IOException。 -
Unchecked Exception 编程错误(如空指针),Java 代码在编译过程中,我们即使不处理不受检查异常也可以正常通过编译。RuntimeException及其子类都统称为非受检查异常,常见的有(NullPointerException(空指针错误)、IllegalArgumentException(参数错误比如方法入参类型错误)、ArrayIndexOutOfBoundsException(数组越界错误)、ClassCastException(类型转换错误)
2、try-catch-finally 如何使用? ⭐⭐⭐⭐⭐
- try块:用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
- catch块:用于处理 try 捕获到的异常。
- finally 块:无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return语句时,finally 语句块将在方法返回之前被执行。
注意:不要在 finally 语句块中使用 return! 当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。
3、finally 中的代码一定会执行吗? ⭐⭐⭐⭐
在某些情况下,finally 中的代码不会被执行:
-
finally 之前虚拟机被终止运行。System.exit(0); 强制终止 JVM
-
线程中断:当线程在执行 try 或 finally 代码块时被中断(interrupt()),
-
JVM 崩溃:若在执行 finally 前 JVM 发生崩溃(如内存溢出且无法处理),finally 无法执行。
5、如何使用 try-with-resources 代替try-catch-finally? ⭐⭐⭐
在 Java 中,try-with-resources 语句是处理资源管理的一种简洁方法。它可以自动关闭资源,避免了显式的 finally 块和手动关闭资源的复杂性。try-with-resources 语句的引入主要是为了简化资源管理,特别是在处理如文件、流、数据库连接等需要显式关闭的资源时。
基本用法:
try-with-resources 语句需要在 try 关键字后面定义一个或多个实现了 AutoCloseable 接口的资源。资源将在 try 块执行完毕后自动关闭,无论 try 块中是否抛出了异常。这使得资源管理变得更加简单和安全。
语法结构:
try (ResourceType resource = new ResourceType()) {
// 使用资源的代码
} catch (Exception e) {
// 异常处理代码
}
// 资源会在 try 块结束后自动调用 close() 关闭,无需 finally 块
AI写代码java运行
- 1
- 2
- 3
- 4
- 5
- 6
6、异常使用有哪些需要注意的地方? ⭐⭐⭐
- 不要定义静态异常变量:静态异常会共享堆栈信息,掩盖真实错误位置。
- 不要复用异常对象:每次抛出异常时必须 new 新对象,否则堆栈信息会指向首次创建位置,导致调试困难。
- 异常信息应具体且有意义:包含上下文信息(如参数值、操作类型),避免模糊描述。建议抛出更加具体的异常而不是其父类
- 避免重复记录日志:若已在捕获处记录完整日志,再次抛出时无需重复记录。
- finally块注意:避免在finally中抛出异常会覆盖原始异常。,避免在 finally 中 return会覆盖 try/catch 的返回值。
1.5、其他高级特征
1、什么是泛型?有什么作用? ⭐⭐⭐⭐
泛型是 Java 中的一种机制,它允许在类、接口和方法中使用类型参数,从而提高代码的重用性和类型安全性。泛型是在 Java 5 中引入的,它使得代码更加灵活和安全,同时也能减少强制类型转换的需求。
- 类型安全:泛型提供了编译时的类型检查,防止了类型转换错误。例如,在泛型集合中添加非预期类型的元素会在编译时被检测到,从而避免了运行时的ClassCastException。
- 代码重用:使用泛型可以编写通用的算法和数据结构,而不必为每种类型编写重复的代码。例如,Java 标准库中的 List, Map, Set等集合类都使用了泛型,使得它们可以存储不同类型的对象。
- 消除强制类型转换:在没有泛型的情况下,你可能需要使用强制类型转换来将对象从 Object类型转换为特定类型。泛型避免了这种强制转换,使得代码更清晰、安全。
2、什么是反射及其优缺点? ⭐⭐⭐⭐
反射允许程序在运行时动态获取类的信息(如字段、方法、注解),并操作对象的属性和行为。
反射的优点:
- 灵活性:允许在运行时动态操作类、方法和字段,使得代码能够适应不断变化的需求。例如,可以在不知道具体类的情况下操作它们。
- 简化框架的实现:许多框架和库(如 Spring 和 Mybatis)使用反射来实现配置和动态行为,从而使得框架的使用更加灵活和简洁。
- 动态创建对象:在一些动态语言或需要动态配置的系统中,反射可以用来创建对象、调用方法,而无需在编译时知道所有信息。
- 测试和调试:反射可以用来访问和测试私有字段和方法,这对于单元测试和调试很有帮助。
反射的缺点:
- 性能开销:反射涉及到大量的动态检查和操作,这会比正常的静态类型操作要慢。频繁使用反射可能会导致性能下降。
- 安全问题:反射可以访问私有字段和方法,这可能导致安全问题。如果不加以控制,可能会泄露敏感信息或破坏对象的封装性。
- 编译时检查失效:使用反射时,很多错误(如方法不存在、字段不匹配等)只能在运行时发现。这意味着程序的类型安全性会降低,可能导致难以发现和调试的问题。
- 代码复杂性:过度使用反射可能导致代码变得复杂和难以维护。反射代码通常较难理解,因为它绕过了静态类型检查和编译时验证。
3、什么是注解,注解的解析方法有哪几种? ⭐⭐
注解是 Java 1.5 引入的一种元数据(Metadata) 机制,用于为代码添加描述性信息。它不直接影响程序逻辑,但可被编译器、框架或运行时环境读取。实现以下功能:编译时检查(如 @Override 确保方法正确重写);代码生成(如 Lombok 自动生成 getter/setter);框架配置(如 Spring 的 @Component 标记组件);文档生成(如 @Deprecated 标记废弃接口)。
常见的解析方法有三种:
- 编译期解析:编译器在编译 Java 代码的时候扫描对应的注解并处理,如使用@Override
注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。 - 运行期解析:在程序运行时,通过反射 API 动态获取对象 / 方法 / 字段的注解,执行对应逻辑。如Spring 通过 @Autowired 实现依赖注入;
- 类加载期解析:例如:Spring AOP 通过
@Aspect生成代理类
4、序列化和反序列化 ⭐⭐⭐⭐⭐
如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。
- 序列化:将数据结构或对象转换成二进制字节流的过程
- 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程
- 综上:序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。
- 对于不想进行序列化的变量,使用 transient 关键字修饰。
5、为什么不推荐使用 JDK 自带的序列化? ⭐⭐⭐
-
不支持跨语言调用 : 如果调用的是其他语言开发的服务的时候就不支持了。
-
性能差:相比于其他序列化框架性能更低,主要原因是序列化之后的字节数组体积较大,导致传输成本加大。
-
存在安全问题:序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。
6、IO流为什么要分为字节流和字符流 ⭐⭐⭐
-
数据类型的针对性:字节流处理二进制数据,字符流处理文本数据。
-
编码处理的自动化:字符流自动处理字符编码,避免手动转换的复杂性和错误。
-
性能优化:字符流针对文本操作进行了优化(如缓冲、按行读取)。
7、BIO,NIO,AIO区别 ⭐⭐⭐⭐⭐
- BIO(Blocking I/O):即传统的阻塞式 I/O。在 BIO 中,当线程执行 I/O操作时,如读取文件或网络数据,线程会被阻塞,直到操作完成。这意味着在 I/O 操作进行期间,线程无法执行其他任务,会一直处于等待状态。
- NIO(Non - Blocking I/O):也叫新 I/O 或非阻塞式 I/O。NIO 允许线程在执行 I/O操作时不会被阻塞。线程可以在 I/O 操作未完成时继续执行其他任务,通过轮询或事件通知的方式来获取 I/O 操作的结果。
- AIO(Asynchronous I/O):即异步 I/O。AIO 与 NIO 的非阻塞不同,它是基于事件和回调机制实现的。当发起一个I/O 操作后,线程会继续执行其他任务,I/O 操作完成后会通过回调函数来通知线程,线程不需要主动去查询 I/O 操作的状态。
二、Java集合
Java 集合,也叫作容器,主要是由两大接口派生而来:一个是 Collection接口,主要用于存放单一元素;另一个是 Map 接口,主要用于存放键值对。对于Collection 接口,下面又有三个主要的子接口:List、Set 、 Queue。

2.1、集合概述
1、集合框架底层数据结构 ⭐⭐⭐⭐⭐
List(有序、可重复)
- ArrayList:Object[] 数组。线程不安全,支持快速随机访问。
- Vector:Object[] 数组。线程安全(方法用synchronized修饰),但性能差,已淘汰。
- LinkedList:双向链表。线程不安全,插入删除高效,随机访问慢。
- CopyOnWriteArrayList:Object[] 数组。线程安全,写操作复制新数组,适合读多写少场景。
Set(无序,唯一)
- HashSet:基于HashMap实现,元素作为HashMap的key存储(value为固定PRESENT对象)。线程不安全。
- LinkedHashSet:继承HashSet,内部通过LinkedHashMap实现,维护插入顺序。线程不安全。
- TreeSet:基于TreeMap(红黑树)实现,元素按自然顺序或Comparator排序。线程不安全。
Queue(有序,可重复)
- PriorityQueue:Object[] 数组实现小顶堆。线程不安全,元素按自然顺序或Comparator排序。
- ArrayDeque:可扩容动态双向数组。线程不安全,高效实现栈和队列操作。
Map(key唯一,value可重复)
- HashMap:JDK8+为数组+链表/红黑树,链表长度≥8且数组长度≥64时树化。线程不安全。
- LinkedHashMap:继承HashMap,通过双向链表维护插入顺序或访问顺序。线程不安全。
- ConcurrentHashMap:JDK8+采用数组+链表/红黑树,CAS+synchronized实现线程安全,锁粒度更细。
- Hashtable:数组+链表,全表锁,线程安全但已淘汰,建议用ConcurrentHashMap替代。
- TreeMap:红黑树实现,key按自然顺序或Comparator排序。线程不安全。
2、Comparable 和 Comparator 的区别 ⭐⭐⭐
Comparable 是类内部实现的自然排序,Comparator 是外部定义的自定义排序。
Comparable 接口
- 定义: Comparable 接口用于定义对象的自然排序。实现此接口的类需要重写 compareTo 方法。
- 比较方式: compareTo 方法只允许以当前对象与另一个对象进行比较。一般来说,该对象的排序顺序在类的实现中是固定的。
- 实现类: 通常在类中实现此接口,例如,如果你想让 Person 类根据年龄进行排序,你可以在 Person 类中实现 Comparable
接口。
public class Person implements Comparable<Person> {
private int age;
public Person(int age) {
this.age = age;
}
@Override
public int compareTo(Person other) {
return Integer.compare(this.age, other.age);
}
}
AI写代码java运行
Comparator
- 定义: Comparator 接口用于定义一个比较器,可以在外部实现以定义多个不同的排序方式。实现此接口的类需要重写 compare方法。
- 比较方式: compare方法可以接受两个对象进行比较,允许你在多个比较器之间选择不同的排序规则,也就是说,你可以为同一类型的对象定义多种排序方式。
- 实现类: 通常在一个单独的类中实现,例如,你可以创建一个 AgeComparator 类来根据年龄排序,一个 NameComparator类来根据名字排序。
import java.util.Comparator;
public class AgeComparator implements Comparator<Person> {
@Override
public int compare(Person p1, Person p2) {
return Integer.compare(p1.getAge(), p2.getAge());
}
}
AI写代码java运行
2.2、List集合
1、ArrayList 和 Array(数组)的区别? ⭐⭐⭐
ArrayList 内部基于动态数组实现,比 Array(静态数组) 使用起来更加灵活:
- 长度可变性:ArrayList会根据实际存储的元素动态地扩容或缩容,而 Array 被创建之后就不能改变它的长度了。
- 类型安全:ArrayList 允许你使用泛型来确保类型安全,Array 则不可以。
- 存储内容:ArrayList 中只能存储对象。对于基本类型数据,需要使用其对应的包装类(如 Integer、Double 等)。Array可以直接存储基本类型数据,也可以存储对象。
- 功能支持:ArrayList 支持插入、删除、遍历等常见操作,并且提供了丰富的 API 操作方法,比如 add()、remove()等。Array只是一个固定长度的数组,只能按照下标访问其中的元素,不具备动态添加、删除元素的能力。
- 创建方式:ArrayList创建时不需要指定大小,而Array创建时必须指定大小。
2、如何实现数组和List之间的转换 ⭐⭐⭐
- 数组 → List
Arrays.asList(array) 快速转换,但返回的 固定长度 List(底层共享原数组)。
相互影响:修改元素值会同步到原数组,但增删元素会抛异常。
new ArrayList<>(Arrays.asList(array))
创建独立 List,与原数组无关联,支持增删操作。 - List → 数组
list.toArray(new T[0])
返回独立数组,与原 List 无关联,修改互不影响。
3、ArrayList 可以添加 null 值吗? ⭐⭐⭐
ArrayList 中可以存储任何类型的对象,包括 null 值。不过,不建议向ArrayList 中添加 null 值, null 值无意义,会让代码难以维护比如忘记做判空处理就会导致空指针异常。
4、ArrayList 和LinkedList插入和删除元素的时间复杂度? ⭐⭐⭐⭐
它们的性能差异主要源于底层数据结构:
-
ArrayList 基于数组,尾部插入/删除在无需扩容时是 O(1)(扩容时为 O(n)),而头部或中间操作需移动元素,时间复杂度O(n)。
-
LinkedList 基于双向链表,头尾插入/删除直接操作指针(O(1)),但中间操作需遍历链表,时间复杂度 O(n)。因此,LinkedList 更适合频繁头尾操作,而 ArrayList 适合随机访问和尾部操作。”
5、ArrayList和LinkedList有什么区别 ⭐⭐⭐⭐ ⭐
ArrayList和LinkedList是)java集合框架中List接口的两个常见实现类,它们在底层实现和性能特点上有以下几点区别:
1).底层数据结构:ArrayList使用动态数组来存储元素,而LinkedList使用双向链表来存储元素。
2).随机访向性能:Arraylist支持高效的随机访问,因为它可以通过下标计算元素在数组中的位置。而Linkedlst在随机访问方面性能较差,获取元素需要从头或尾部开始遍历链表找到对应位置。
3)、插入和删除性能:ArayList在尾部添加或删除元素的性能较好,因为它不涉及数组的移动。而在中间插入或删除元素时,ArrayList涉及到元素的移动,性能相对较低。LinkedList在任意位置进行插入和删除操作的性能较好,因为只需要调整链表中的指针即可。
4).内存占用:ArrayList底层是数组,内存连续,节省内存,而LinkedList 是双向链表需要存储数据,和两个指针,更占用内存
5)、扩展性:ArrayList 在元素超过当前容量时,需要创建一个更大的数组并复制原有元素,可能会造成性能开销。LinkedList 可以方便地在任意位置插入元素,不需要移动其他元素。
综上所述,当频繁访问元素时,使用 ArrayList 更高效,当频繁插入和删除操作时,使用 LinkedList 更合适。
6、ArrayList底层的实现原理是什么/扩容机制 ⭐⭐⭐⭐⭐
1. 底层数据结构
ArrayList 底层基于 Object[] 数组 实现,是一个动态数组,支持自动扩容。
2. 初始容量
无参构造:初始容量为 0(实际是一个空数组),第一次添加元素时扩容为默认容量 10。
指定容量构造:例如 new ArrayList(100),直接初始化为指定容量(100)。
3. 扩容机制
扩容条件:当添加元素后,数组已使用长度(size+1)超过当前数组容量时触发扩容。
扩容规则:新容量为原容量的 1.5 倍(源码:newCapacity = oldCapacity + (oldCapacity >> 1))。
扩容代价:需通过 Arrays.copyOf 将旧数组数据拷贝到新数组,频繁扩容会影响性能。
4. 添加元素流程
容量检查:确保当前容量足够存入新元素(size+1 > capacity)。
触发扩容:若不足,按 1.5 倍扩容并拷贝数据。
插入元素:将新元素放入数组 size 位置,size 自增 1。
返回结果:返回 true 表示添加成功。
2.3、Set集合
1、比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同 ⭐⭐⭐⭐⭐
| 特性 | HashSet | LinkedHashSet | TreeSet |
|---|---|---|---|
| 底层数据结构 | 哈希表 | 哈希表 + 双向链表 | 红黑树 |
| 顺序性 | 无序 | 插入顺序 | 自然/自定义排序 |
| 性能 | O(1) 查找/插入 | 略低于 HashSet | O(log n) 操作 |
| 是否允许 null | ✅ | ✅ | ❌(需排序无法比较) |
| 线程安全 | ❌ | ❌ | ❌ |
| 适用场景 | 快速去重、无需顺序 | 需保留插入顺序的去重 | 需排序、范围查询 |
2.4、Map集合
1、HashMap底层的实现原理 ⭐⭐⭐⭐⭐
- 数据存储:HashMap底层采用哈希表结构,通过key的哈希值计算数组下标,若哈希冲突,则同一位置的元素以链表或红黑树存储。
- 扰动算法:JDK1.8通过高位参与运算(key.hashCode()高16位异或低16位)减少哈希碰撞。
- 版本差异:JDK1.8前:数组+链表,冲突时链表插入头部(头插法)。JDK1.8后:数组+链表+红黑树,当链表长度超过8且数组长度≥64时,链表转为红黑树(提高查询效率);红黑树节点数≤6时退化为链表。冲突时插入链表尾部(尾插法)
2、HashMap的put流程 ⭐⭐⭐⭐⭐
- 初始化检查:若数组(table)为空,先调用resize()初始化(默认容量16,阈值12)。
- 计算下标:通过key计算hash值确定键值对存放的桶位置。
- 处理空桶:若当前桶无数据(table[i]==null),直接新建节点放入。
- 处理冲突:若桶已有数据:Key相同:若头节点key与待插入key相同(equals为真),直接覆盖value。
红黑树插入:若桶为红黑树结构,调用putTreeVal()插入或更新。
链表遍历:遍历链表,若无相同key则在尾部插入新节点。插入后若链表长度≥8,触发树化检查(需数组长度≥64才转为红黑树)。若有相同key则直接覆盖value - 扩容判断:插入成功后,若总键值对数超过阈值(size > threshold),调用resize()扩容。
3、HashMap的扩容机制 ⭐⭐⭐⭐⭐
- 在添加元素或初始化的时候需要调用resize方法进行扩容,第一次添加数据初始化数组长度为16,以后每次每次扩容都是达到了扩容阈值(数组长度 * 0.75)
- 每次扩容的时候,都是扩容之前容量的2倍;
- 扩容之后,会新创建一个数组,需要把老数组中的数据挪动到新的数组中
没有hash冲突的节点,则直接使用 e.hash & (newCap - 1) 计算新数组的索引位置
如果是红黑树,走红黑树的添加
如果是链表,则需要遍历链表,可能需要拆分链表,判断(e.hash & oldCap)是否为0,为0的话该元素的位置停留在原始位置,不为0的话移动到原始位置+增加的数组大小这个位置上
4、HashMap 和 Hashtable 的区别 ⭐⭐⭐⭐⭐
- 线程是否安全: HashMap 是非线程安全的,Hashtable 是线程安全的,因为 Hashtable内部的方法基本都经过synchronized 修饰。(如果要保证线程安全的话就使用 ConcurrentHashMap ); 因为线程安全的问题,HashMap 要比 Hashtable 效率高一点。另外,Hashtable 基本被淘汰,不要在代码中使用它
- 对 Null key 和 Null value 的支持: HashMap 可以存储 null 的 key 和 value,但 null作为键只能有一个,null 作为值可以有多个;Hashtable 不允许有 null 键和 null 值,否则会抛出NullPointerException。
- 初始容量大小和每次扩充容量大小的不同: ① 创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2的幂次方大小(HashMap 中的tableSizeFor()方法保证)。也就是说 HashMap 总是使用 2的幂作为哈希表的大小,
- 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间。Hashtable没有这样的机制。
- 哈希函数的实现:HashMap 对哈希值进行了高位和低位的混合扰动处理以减少冲突,而 Hashtable 直接使用键的hashCode()值。
5、HashMap 和 TreeMap 区别 ⭐⭐⭐⭐⭐
-
1、底层数据结构: HashMap:采用哈希表作为底层数据结构,通过哈希函数将键映射到桶中,存放键值对。 TreeMap:基于红黑树实现,能够保持键的排序。
-
2、顺序: HashMap:不保证元素的顺序,插入的顺序可能会被打乱。 TreeMap:根据键的自然顺序(或根据比较器提供的顺序)进行排序,因此迭代时会按照键的顺序返回元素。
-
3、性能: HashMap:在大多数情况下,插入、删除和查找的时间复杂度为 O(1),但最坏情况下为 O(n)(当发生哈希冲突严重时)。 TreeMap:插入、删除和查找的时间复杂度为 O(log n),因为需要维护树的平衡性。
-
4、键的要求: HashMap:键可以为 null,最多只能有一个 null 键。 TreeMap:不允许使用 null 作为键,因为其需要比较键的大小。
-
5、用途: HashMap:适合对元素的顺序没有要求,主要用于快速访问。 TreeMap:适合需要排序或者范围查询的场景。
总结来说,如果对元素的顺序没有要求并且希望获得更好的性能,使用 HashMap。如果需要对元素进行排序并能接受更高的操作复杂度,使用 TreeMap。
6、HashMap 的长度为什么是 2 的幂次方 ⭐⭐⭐⭐⭐
- 高效位运算替代取模:
哈希计算下标时,公式为 (n-1) & hash(n 为容量)。
若 n 是 2 的幂,n-1 的二进制全为 1(如 n=16 → 15=0b1111),此时 & 操作等价于 hash % n,但位运算效率远高于取模。 - 哈希分布更均匀:
2 的幂次方的容量设计,配合扰动函数(hash = key.hashCode() ^ (hash >>> 16)),能让哈希值的高位参与下标计算,减少哈希碰撞。 - 扩容便捷性:
每次扩容容量翻倍(仍保持 2 的幂),旧数据迁移时只需判断 hash & oldCap 是否为 0,即可快速分配到新位置(原位置或原位置+旧容量),无需重新计算所有哈希。 - 避免空间浪费:
若容量非 2 的幂,用户自定义初始容量时可能导致实际分配的容量与预期不符(需通过 tableSizeFor() 方法修正为最近的 2 的幂),可能浪费内存。
7、HashMap 多线程操作导致死循环问题 ⭐⭐⭐
- JDK1.7 及之前版本的 HashMap在多线程环境下扩容操作可能存在死循环问题,这是由于当一个桶位中有多个元素需要进行扩容时,多个线程同时对链表进行操作,头插法可能会导致链表中的节点指向错误的位置,从而形成一个环形链表,进而使得查询元素的操作陷入死循环无法结束。
- 为了解决这个问题,JDK1.8 版本的 HashMap采用了尾插法而不是头插法来避免链表倒置,使得插入的节点永远都是放在链表的末尾,避免了链表中的环形结构。
- 但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在数据覆盖的问题。并发环境下,推荐使用ConcurrentHashMap 。
8、HashMap 为什么线程不安全? ⭐⭐⭐⭐⭐
核心原因:HashMap 的设计未考虑多线程并发操作,导致以下问题:
- 多线程扩容导致死循环(JDK1.7 及之前)
原因:多线程扩容时,头插法可能导致链表反转,形成环形链表(如线程 A 和 B同时扩容并修改节点引用)。
结果:后续操作遍历链表时陷入死循环,CPU 飙升。
JDK1.8 改进:改用尾插法,但线程不安全问题依然存在(只是降低了死循环概率)。 - 多线程扩容导致数据丢失
场景:多线程同时插入数据,触发哈希冲突。
结果:若两个线程计算相同下标,后插入的键值对可能覆盖前一个线程的数据。 - 多线程扩容导致数据丢失
场景:多线程同时触发扩容(resize)。
结果:链表或红黑树结构被破坏,部分数据丢失或引用混乱。 - 迭代导致抛出异常
场景:一个线程迭代遍历时,另一个线程修改结构(如增删)。
结果:迭代器的 modCount 与预期值不一致,抛出异常(即使单线程也可能触发,但多线程更不可控)。
9、ConcurrentHashMap 线程安全的具体实现方式/底层具体实现 ⭐⭐⭐⭐⭐
- JDK1.7(分段锁)
数据结构:由多个 Segment 组成,每个 Segment 是一个独立的哈希表。
并发控制:每个 Segment用 ReentrantLock 加锁,不同 Segment 可并发操作。
缺点:并发度固定,扩容仅限于单个 Segment。 - JDK1.8(CAS + synchronized)
数据结构:数组 + 链表/红黑树,Node 的 val 和 next 用 volatile 保证可见性。
并发控制:无锁读:直接访问 volatile 变量。CAS 写:空桶用 CAS 插入;非空桶锁头节点(synchronized)。多线程扩容:线程插入时若发现扩容,协助迁移数据。
优点:锁粒度更细,并发度动态提升。
总结
ConcurrentHashMap 通过分段锁(JDK 1.7 及之前版本)和基于 CAS 操作的无锁算法(JDK 1.8 及之后版本)来实现线程安全,从而在高并发环境下提供了更好的性能。读操作通常无锁,写操作使用 CAS 操作或加锁来确保线程安全。ConcurrentHashMap 的设计使得它能够在多线程环境下安全高效地操作。
10、ConcurrentHashMap 为什么 key 和 value 不能为 null? ⭐⭐⭐⭐⭐
- 二义性问题: get(key) 返回 null 时,无法区分是“键不存在”还是“值本身为 null”。 并发场景下,containsKey和 get 之间的修改可能导致误判。
- 简化并发设计: 避免处理 null 的特殊逻辑,减少代码复杂度。
- 与 Hashtable 一致性: 延续 Hashtable 的设计原则,强制显式处理缺失键值。
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc
需要全套面试笔记及答案【点击此处即可/免费获取】
https://docs.qq.com/doc/DQXdYWE9LZ2ZHZ1ho
11、ConcurrentHashMap 能保证复合操作的原子性吗? ⭐⭐⭐
-
ConcurrentHashMap本身是线程安全的,但它不能自动保证复合操作的原子性。例如‘检查后更新’操作,在并发环境下可能导致竞态条件。
-
为了保证原子性,ConcurrentHashMap 提供了以下内置方法: putIfAbsent():不存在则插入。 compute()/ computeIfAbsent():根据 key 计算新值。 merge():合并新旧值。 这些方法通过内部锁或 CAS 操作保证了原子性,避免了手动实现复合操作的风险。
12、ConcurrentHashMap 和 Hashtable 的区别 ⭐⭐⭐⭐⭐
| 特性 | ConcurrentHashMap | Hashtable |
|---|---|---|
| 并发机制 | JDK1.7分段锁;JDK1.8 CAS+桶锁 | 全局锁(所有方法加 synchronized) |
| 性能 | 高并发下高效 | 高并发下性能差 |
| 迭代器 | 强一致性(不抛异常) | 弱一致性(可能抛异常) |
| 初始容量 | 16(2的幂) | 11 |
| 扩容规则 | 翻倍(2的幂) | 2n+1 |
| Null支持 | 键值均不能为null | 键值均不能为null |
| 版本 | JDK1.5+ | JDK1.0+ |
核心区别:
锁粒度:ConcurrentHashMap 细粒度锁(段或桶),Hashtable 全局锁。
设计目标:ConcurrentHashMap 为高并发优化,Hashtable 已淘汰。
三、Redis
3.1、概述
1、什么是Redis? ⭐⭐⭐⭐
基于内存的键值(Key-Value)数据库系统。常见的有五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)
**2、Redis为什么快?**⭐⭐⭐⭐⭐
- 单线程模型:避免上下文切换和锁竞争,通过IO多路复用监听多个Socket事件
- 内存操作:数据全存内存,读写速度远高于磁盘
- 高效数据结构:如跳表、压缩列表等,优化内存和查询效率
3、解释一下I/O多路复用模型? ⭐⭐⭐⭐
- I/O多路复用是一种高效的网络通信模型,它允许单个线程同时监听多个Socket连接,并在其中任何一个Socket可读或可写时得到通知,从而避免无效等待,提高CPU利用率。
- 目前的I/O多路复用都是采用的epoll模式实现,epoll会在通知Redis服务Socket就绪的同时,把已就绪的Socket信息直接传递给Redis服务,避免了遍历所有Socket的开销,大幅提升了性能。"
- 在Redis6.0之后,为了提升更好的性能,多线程处理网络I/O(解析请求/发送回复),单线程执行命令:保证原子性,避免锁竞争 这种设计既提升吞吐量,又维持了Redis的线程安全特性。
3.2、缓存
1、什么是Redis的缓存穿透,怎么解决 ⭐⭐⭐⭐⭐
缓存穿透是指查询一个既不在缓存也不在数据库中的数据,导致每次请求都直接访问数据库,从而造成数据库压力过大甚至宕机的问题。这种情况通常由恶意攻击或无效请求(如不存在的ID)引发。
缓存穿透的解决方案
- 空值缓存(缓存空对象)
原理:当数据库查询结果为空时,将空值(如key-null)写入缓存,并设置较短的过期时间(如30秒)。后续相同请求会命中空值缓存,避免重复访问数据库。
优点:实现简单,能有效拦截短时间内的高频无效请求。
缺点:短时间内存占用增加。若后续数据库新增了该数据,可能导致缓存与数据库的短期不一致 - 布隆过滤器
原理:它的底层原理是,先初始化一个比较大的数组,里面存放的是二进制0或1。一开始都是0,当一个key来了之后,经过3次hash计算,模数组长度找到数据的下标,然后把数组中原来的0改为1。这样,三个数组的位置就能标明一个key的存在。
请求到达时,先通过布隆过滤器判断数据是否存在:
若返回“不存在”,直接拦截请求。
若返回“可能存在”,继续查询缓存和数据库。
优点:内存占用极小,适合大规模数据场景。
缺点:存在误判率(可能将不存在的误判为存在,但不会漏判)。需在数据写入时同步更新布隆过滤器
2、什么是Redis的缓存击穿,怎么解决 ⭐⭐⭐⭐⭐
Redis的缓存击穿是指某个热点数据在缓存中过期(或失效)的瞬间,大量并发请求直接穿透缓存层访问数据库,导致数据库压力骤增甚至崩溃的现象。这种情况通常发生在高并发场景下,例如秒杀商品、热门新闻等热点数据突然失效时。
解决方案
- 互斥锁(分布式锁)
原理:当缓存失效时,仅允许一个线程查询数据库并重建缓存,其他线程等待或重试。例如通过Redis的SETNX命令实现锁机制。
优点:强一致性,确保数据最新。
缺点:性能下降(线程需等待锁),可能引发死锁或超时问题 - 逻辑过期(逻辑删除)
原理:缓存中不设置物理过期时间(TTL),而是将过期时间写入数据值中。当发现逻辑过期后,异步更新缓存。
优点:高可用性,避免线程等待,性能高。
缺点:短期数据不一致,需容忍旧数据
3、什么是Redis的缓存雪崩,怎么解决 ⭐⭐⭐⭐⭐
Redis的缓存雪崩是指在同一时间段内,大量缓存数据集中过期失效或缓存服务器宕机,导致所有请求直接涌向后端数据库,造成数据库压力激增甚至崩溃的现象。
解决方案
- 随机化过期时间:为每个Key的TTL添加随机值(例如基础时间±随机分钟),分散过期时间点,避免同一时段大量数据失效
- 热点数据永不过期:对高频访问的数据(如首页推荐商品),设置逻辑过期而非物理过期,通过异步线程定期更新数据,保证缓存始终可用
- Redis高可用部署:采用主从复制(Master-Slave)、哨兵模式(Sentinel)或集群模式(Cluster)实现故障自动转移,确保单点故障不影响整体服务
- 多级缓存架构:结合本地缓存与分布式缓存(Redis),形成多级缓存屏障。例如,本地缓存应对高频请求,Redis作为二级缓存分担压力
- 熔断与限流策略:在网关或服务层设置限流(如令牌桶算法),限制数据库访问QPS;当请求量超过阈值时触发熔断,直接返回降级内容(如默认页面)(这个缓存三兄弟都可以说)
4、mysql的数据如何与Redis同步呢(双写一致性) ⭐⭐⭐⭐⭐
- 延时双删(不保证强一致):1在更新数据库前,先删除 Redis 中的旧缓存,防止后续查询直接命中旧数据。2执行 MySQL 的更新操作,确保数据持久化。3设置延时时间(通常为 1-5 秒),等待主从同步完成。延时结束后再次删除缓存,清理可能在此期间被其他线程写入的旧数据。延时机制的作用:等待主从数据库同步完成(主库到从库的同步延迟)。允许其他可能的并发操作完成缓存写入,确保第二次删除能清理残留的脏数据
- 分布式锁(强一致):机制:写锁(排他锁):更新数据时加锁,阻塞其他读写操作。读锁(共享锁):允许并发读,但禁止写操作。性能较低
- MQ或者Canal中间件异步通知(最终一致性):
MQ
步骤1:数据库更新后发送消息。业务代码在更新 MySQL 后,向 MQ(如 Kafka、RabbitMQ)发送一条消息,携带数据标识及操作类型(如删除缓存或更新缓存)。
步骤2:消费者异步处理缓存。独立服务监听 MQ 消息,解析后执行缓存操作(如删除或更新 Redis 数据)
Canal
步骤1:监听 MySQL Binlog。Canal 伪装为 MySQL 从库,实时解析主库的 Binlog 日志,捕获数据变更事件(如 INSERT、UPDATE)。
步骤2:发送变更消息到 MQ。将解析后的变更数据(如表名、主键 ID、新值)发送到 MQ(如 Kafka)。
步骤3:消费者更新缓存。消费者从 MQ 读取消息,根据操作类型删除或更新 Redis 数据。
5、redis做为缓存,数据的持久化是怎么做的 ⭐⭐⭐⭐⭐
- RDB:定期将 Redis 内存中的数据生成 二进制快照文件(dump.rdb)。
可通过 SAVE(阻塞式)或 BGSAVE(后台异步)手动触发。
优点:
恢复速度快(二进制格式,文件小)。
适合大规模数据备份(如每天全量备份)。
对性能影响小(子进程处理,主进程不阻塞)。
缺点:
可能丢失数据(最后一次快照后的修改会丢失)。
大数据量时 fork 可能短暂阻塞 Redis(取决于内存大小)。 - AOF:记录所有写操作命令(文本格式),类似 MySQL 的 binlog。
重启时 重放 AOF 日志 恢复数据。
优点
数据安全性高(最多丢失1秒数据)。
支持日志重写(BGREWRITEAOF 压缩冗余命令)。
可读性强(可用于数据审计)。
缺点:
文件体积通常比 RDB 大(需定期重写优化)。
恢复速度较慢(需逐条执行命令)。
推荐组合使用 RDB + AOF:RDB 用于快速恢复和备份、AOF 确保数据安全性
6、Redis的数据过期策略 ⭐⭐⭐⭐
在redis中提供了两种数据过期删除策略。
- 第一种是惰性删除。在设置该key过期时间后,我们不去管它。当需要该key时,我们检查其是否过期。如果过期,我们就删掉它,返回 nil;反之,返回该key。对 CPU 友好,只处理被访问的 key,但可能导致大量已过期但未被访问的 key 堆积
- 第二种是定期删除。就是说,每隔一段时间,我们就对一些key进行检查,并删除里面过期的key。默认每秒 10 次。可以减少内存浪费,但CPU 开销比惰性删除大
Redis的过期删除策略是:惰性删除 + 定期删除两种策略配合使用
7、Redis的数据淘汰策略 ⭐⭐⭐⭐
1)不淘汰(默认)
策略名:noeviction
行为:当内存不足时,新写入操作会报错(如 OOM),拒绝所有可能增加内存的请求。
适用场景:数据不允许丢失(例如关键业务数据),需严格避免内存超限。
2)淘汰设置了过期时间的键
策略名:
volatile-lru:淘汰最近最少使用(LRU)的键(仅针对有过期时间的键)。
volatile-lfu:淘汰最不经常使用(LFU)的键(Redis 4.0+,针对有过期时间的键)。
volatile-ttl:淘汰存活时间最短(TTL 最小)的键。
volatile-random:随机淘汰任意有过期时间的键。
适用场景:内存中同时存在永久数据和临时缓存数据,需优先淘汰缓存。
3)淘汰所有键(不区分是否设置过期时间)
策略名:
allkeys-lru:淘汰最近最少使用(LRU)的键(所有键)。
allkeys-lfu:淘汰最不经常使用(LFU)的键(Redis 4.0+,所有键)。
allkeys-random:随机淘汰任意键。
适用场景:内存中所有数据都可被淘汰(例如纯缓存场景)。
3.3、分布式锁
1、Redis分布式锁如何实现? ⭐⭐⭐⭐⭐
在redis中提供了一个命令SETNX(SET if not exists)。由于redis是单线程的,用了这个命令之后,只能有一个客户端对某一个key设置值。在没有过期或删除key的时候,其他客户端是不能设置这个key的。
2、如何控制Redis实现分布式锁的有效时长呢 ⭐⭐⭐⭐
Redis 的 SETNX 指令确实难以直接控制锁的有效时长,但可以通过 Redisson 框架优化实现,其核心逻辑如下:
- 看门狗自动续期机制:如果业务未执行完但锁即将过期,Redisson的看门狗会每隔一段时间检查锁是否仍被持有。若持有,则自动延长锁的失效时间,避免锁提前释放。
- 释放锁与流程闭环:业务执行完成后,手动释放锁即可,确保锁资源及时回收。
- 自旋锁提升并发性能:在高并发场景下,若客户1持有锁,客户2不会立即被拒绝,而是以自旋(循环尝试)的方式等待锁释放。一旦客户1释放锁,客户2能立即获取,减少阻塞时间,提升系统吞吐量。
3、Redisson实现的分布式锁是可重入的吗? ⭐⭐⭐⭐
是可重入的。这样做是为了避免死锁的产生。
第一次加锁时,记录你的线程ID和持有锁的次数(计数器=1)。
第二次加锁时,发现是同一个线程,计数器+1(变成2),直接放行。
每次解锁时,计数器-1,直到计数器归零才真正释放锁。
4、Redisson实现的分布式锁能解决主从一致性的问题吗? ⭐⭐⭐
这个是不能的。
Redisson 分布式锁的默认实现(主从场景风险)
依赖 Redis 主节点:Redisson 的 RLock 默认通过 Redis 主节点加锁(SET + Lua 脚本)
主从异步复制问题:
当主节点加锁成功后,若数据未同步到从节点时主节点崩溃
从节点晋升为新主节点后,锁状态丢失,导致多个客户端同时获取锁
Redisson 的解决方案:RedLock 算法:
向多个独立 Redis 节点(通常 ≥3,且主从部署在不同机器)发起加锁请求
当大多数节点(N/2 +1)加锁成功时,视为全局加锁成功
但是,如果使用了红锁,因为需要同时在多个节点上都添加锁,性能就变得非常低,并且运维维护成本也非常高,所以,我们一般在项目中也不会直接使用红锁,并且官方也暂时废弃了这个红锁。
3.4、集群
1、介绍一下Redis主从同步 ⭐⭐⭐⭐
单节点Redis的并发能力是有上限的,要进一步提高Redis的高并发能力,可以搭建主从集群,实现读写分离。一般都是一主多从,主节点负责写数据,从节点负责读数据,主节点写入数据之后,需要把数据同步到从节点中。
主从同步分为 全量同步 和 增量同步 两个阶段,核心目标是确保主从节点数据一致性。
- 全量同步(首次建立连接时触发)
触发条件:从节点首次连接主节点,或主从 replication id 不匹配。
流程:
1)从节点发起请求:携带自身 replication id 和 offset(偏移量)。
2)主节点校验:若 replication id 不同,判定为首次同步。主节点将自己的 replication id 和 offset 发送给从节点,达成元数据一致。
3)生成并传输 RDB 快照:主节点执行 BGSAVE,生成 RDB 文件(内存数据快照)。
发送 RDB 文件到从节点,从节点清空旧数据后加载 RDB。
4)同步缓冲命令:主节点在生成 RDB 期间,将新写入命令记录到 缓冲区(repl_backlog)。RDB 传输完成后,主节点将缓冲区命令发送给从节点执行,确保数据完全一致。 - 增量同步(从节点重启或断线重连时触发)
触发条件:主从 replication id 一致,但 offset 落后。
流程:
1)从节点上报 offset:发送当前 replication id 和 offset。
2)主节点推送差异数据:从缓冲区(repl_backlog)中找到从节点 offset 之后的命令。发送这些命令到从节点执行,追平数据差异。
2、怎么保证Redis的高并发高可用? ⭐⭐⭐⭐⭐
- 主从集群(读写分离) 作用:
高并发:主节点处理写操作,从节点分担读请求,提升整体吞吐量。
数据冗余:主节点数据同步到从节点,实现数据备份。 - 哨兵模式(故障自动恢复) 核心功能:
监控:持续检查主从节点的健康状态。
自动故障转移:主节点(Master)宕机时,哨兵(Sentinel)选举一个从节点(Slave)晋升为新主节点。原主节点恢复后,自动降级为从节点并同步新主节点数据。
服务发现:客户端通过哨兵获取最新主节点地址,故障转移后自动切换连接。
3、Redis集群脑裂,该怎么解决呢? ⭐⭐⭐⭐⭐
- 脑裂场景: 网络分区导致 Sentinel 误判原主节点(old master)宕机,触发故障转移,选举从节点(slave)为新主节点。此时客户端仍可能向原主节点写入数据,但新主节点无法同步这些数据。
后果:网络恢复后,原主节点降级为从节点并清空数据,导致写入原主节点的数据永久丢失。 - 在Redis的配置中可以设置:第一可以设置最少的slave节点个数,比如设置至少要有一个从节点才能同步数据,第二个可以设置主从数据复制和同步的延迟时间,达不到要求就拒绝请求,就可以避免大量的数据丢失。
4、Redis的分片集群有什么作用? ⭐⭐⭐⭐⭐
分片集群主要解决的是海量数据存储的问题
- 集群中有多个master,每个master保存不同数据,并且还可以给每个master设置多个slave节点,就可以继续增大集群的高并发能力。
- 同时每个master之间通过ping监测彼此健康状态,就类似于哨兵模式了。当客户端请求可以访问集群任意节点,最终都会被转发到正确节点。
- Redis 集群引入了哈希槽的概念,有 16384 个哈希槽,集群中每个主节点绑定了一定范围的哈希槽范围,key通过CRC16校验后对16384取模来决定放置哪个槽,通过槽找到对应的节点进行存储。取值的逻辑是一样的。
四、MySQL
4.1、慢查询与优化
1、MySQL 中如何定位慢查询? ⭐⭐⭐
- 方法一:运维监控系统(如 Skywalking) 监控接口响应时间:通过报表查看哪些接口响应时间超过 2 秒。
分析接口耗时:定位接口中耗时较多的部分,包括具体的 SQL 执行时间。 - 方法二:MySQL 慢查询日志 开启慢查询日志:在 MySQL 配置文件中设置: Ini slow_query_log = 1 long_query_time = 2 # 记录执行超过 2 秒的 SQL
查看日志:从慢查询日志中找到执行慢的 SQL。
2、 如何分析慢 SQL? ⭐⭐⭐
使用 EXPLAIN 命令分析 SQL 执行情况:
- 检查索引:key 和 key_len:查看是否命中索引及索引长度。
- 优化扫描方式:type:判断是否存在全表扫描(ALL)或全索引扫描(index),优化查询条件或索引。性能排序:system > const > ref > range > index > ALL
- 避免回表:Extra:查看是否出现回表(如 Using where),尝试添加索引或减少返回字段。
3、MySQL超大分页怎么处理? ⭐⭐⭐⭐
超大分页通常发生在数据量大的情况下,使用LIMIT分页查询且需要排序时效率较低。可以通过覆盖索引和子查询来解决。首先查询数据的ID字段进行分页,然后根据ID列表用子查询来过滤只查询这些ID的数据,因为查询ID时使用的是覆盖索引,所以效率可以提升。
4、SQL的优化经验有哪些? ⭐⭐⭐⭐⭐
-
表结构优化
原则:根据数据范围选择最小适用类型,避免空间浪费。
字段类型:数值用TINYINT/INT/BIGINT,字符串用CHAR/VARCHAR(定长选CHAR),大文本用TEXT。 -
索引优化
高频字段建索引:WHERE、ORDER BY、GROUP BY的字段。
复合索引覆盖查询:如(a,b)覆盖SELECT a,b。
避免失效操作:索引字段不做运算、不隐式类型转换。
前缀索引:长字段(如地址)取前N个字符。 -
SQL语句优化
拒绝SELECT *:只取必要字段,减少传输和回表。
JOIN策略:优先INNER JOIN,必须用LEFT/RIGHT JOIN时小表驱动大表。
聚合优化:UNION ALL替代UNION(无需去重时)。
分页技巧:避免大偏移量,改用WHERE id > N。 -
架构扩展
读写分离:主库写,从库读,分摊压力。
分库分表:数据量大时,按业务垂直拆分,或按规则(如用户ID哈希)水平拆分。
4.2、索引
1、什么是索引 ⭐⭐⭐⭐⭐
它是一种帮助MySQL高效获取数据的数据结构,主要用来提高数据检索效率,降低数据库的I/O成本。同时,索引列可以对数据进行排序,降低数据排序的成本,也能减少CPU的消耗。
2、索引的底层数据结构了解过吗?B树和B+树的区别是什么呢? ⭐⭐⭐⭐⭐
MySQL的默认存储引擎InnoDB使用的是B+树作为索引的存储结构。选择B+树的原因包括:
- 树高更低:B+树每个节点可容纳更多子节点,显著降低树的高度,缩短查询路径。
- 磁盘IO更少:非叶子节点仅存储键值和指针,节省空间以容纳更多索引层级;叶子节点集中存储数据,减少磁盘IO次数。
- 范围查询高效:叶子节点通过双向链表连接,天然支持顺序遍历和范围查询,避免回退到上层节点。
3、什么是聚簇索引、非聚簇索引、回表查询、覆盖索引 ⭐⭐⭐⭐⭐
- 聚簇索引是指数据与索引放在一起,B+树的叶子节点保存了整行数据,通常只有一个聚簇索引,一般是由主键构成。
- 非聚簇索引则是数据与索引分开存储,B+树的叶子节点保存的是主键值,可以有多个非聚簇索引,通常我们自定义的索引都是非聚簇索引。
- 回表查询是指通过二级索引(非聚簇索引)找到对应的主键值,然后再通过主键值查询聚簇索引中对应的整行数据的过程。
- 覆盖索引是指在SELECT查询中,返回的列全部能在索引中找到,避免了回表查询,提高了性能。使用覆盖索引可以减少对主键索引的查询次数,提高查询效率。
4、索引创建原则有哪些? ⭐⭐⭐⭐
- 表中的数据量超过10万以上时考虑创建索引。
- 选择查询频繁的字段作为索引,如查询条件、排序字段或分组字段。
- 尽量使用复合索引,覆盖SQL的返回值。
- 如果字段区分度不高,可以将其放在组合索引的后面。
- 对于内容较长的字段,考虑使用前缀索引。
- 控制索引数量,因为索引虽然可以提高查询速度,但也会影响插入、更新的速度
5、什么情况下索引会失效? ⭐⭐⭐⭐⭐
- 没有遵循最左匹配原则。
- 使用了模糊查询且%号在前面。
- 在索引字段上进行了运算或类型转换。
- 使用了复合索引但在中间使用了范围查询,导致右边的条件索引失效。
4.3、事务与并发
1、事务的特性是什么? ⭐⭐⭐⭐⭐
事务的特性是ACID,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。例如,A向B转账500元,这个操作要么都成功,要么都失败,体现了原子性。转账过程中数据要保持一致,A扣除了500元,B必须增加500元。隔离性体现在A向B转账时,不受其他事务干扰。持久性体现在事务提交后,数据要被持久化存储。
2、 并发事务带来哪些问题? 怎么解决 ⭐⭐⭐⭐⭐
并发事务三大问题:
- 脏读:读到其他事务未提交的数据(如回滚后变无效)
- 不可重复读:同事务内,多次读同一数据结果不同(数据被其他事务修改)
- 幻读:同事务内,相同条件查询出现新数据行(其他事务插入/删除导致)
解决这些问题的方法是使用事务隔离。MySQL支持四种隔离级别:
- 未提交读(READ UNCOMMITTED):解决不了所有问题。
- 读已提交(READ COMMITTED):能解决脏读,但不能解决不可重复读和幻读。
- 可重复读(REPEATABLE READ):能解决脏读和不可重复读,但不能解决幻读,这也是MySQL的默认隔离级别。
- 串行化(SERIALIZABLE):可以解决所有问题,但性能较低。
3、undo log和redo log的区别是什么? ⭐⭐⭐⭐⭐
redo log记录的是数据页的物理变化,用于服务宕机后的恢复,保证事务的持久性。
undo log记录的是逻辑日志,用于事务回滚时恢复原始数据,保证事务的原子性和一致性,支持MVCC
4、事务中的隔离性是如何保证的呢?(MVCC多版本并发控制) ⭐⭐⭐⭐⭐
1)锁机制(解决写冲突)
排他锁(X锁):事务修改数据时加锁,阻止其他事务读写
共享锁(S锁):事务读取数据时可加锁,阻止其他事务修改,但允许读
2)MVCC机制(解决读冲突):
-
核心思想
通过维护数据的多个版本,实现读写并发控制,避免加锁阻塞。 -
关键机制
版本链: 每行数据隐藏DB_TRX_ID(事务ID)和DB_ROLL_PTR(回滚指针),通过undo log链接历史版本。
ReadView: 事务首次查询时生成,记录活跃事务ID列表(m_ids),用于判断哪些版本对当前事务可见。 -
可见性规则
数据行版本对当前事务可见,当:
版本DB_TRX_ID < ReadView中的min_trx_id(已提交);
或版本DB_TRX_ID不在m_ids中(已提交);
否则通过DB_ROLL_PTR查找更早版本。 -
隔离级别差异
RC(读已提交):每次查询生成新ReadView(能看到最新提交)。
RR(可重复读):复用首次ReadView(保证多次读一致)。 -
一句话总结
“MVCC通过版本链+ReadView实现无锁读,RC和RR通过ReadView生成时机控制可见性。”
4.4、其他
1、MySQL主从同步原理是什么? ⭐⭐⭐⭐
MySQL主从复制的核心是二进制日志(Binlog)。步骤如下:
- 主库在事务提交时记录数据变更到Binlog。
- 从库读取主库的Binlog并写入中继日志(Relay Log)。
- 从库重做中继日志中的事件,反映到自己的数据中。
2、什么是分库分表 ⭐⭐⭐⭐
目的:解决单库/表数据量大、性能瓶颈、高并发压力。
分类:垂直拆分(按结构)、水平拆分(按数据分布)。
-
垂直分库: 拆分方式:按业务模块划分库(如用户库、订单库)。
优点:业务解耦、降低单库压力。 -
水平分库:拆分方式:同一表数据按规则分到多库(如用户ID哈希取模)。
优点:负载均衡、扩展性强。 -
垂直分表:拆分方式:按字段拆分表(如常用字段+冷字段/大字段分离)。
优点:减少单表IO、提升高频查询效率。 -
水平分表:拆分方式:同一表数据分到多表(如按时间、ID范围)。
优点:单表数据量小、读写更快。
3、锁机制 ⭐⭐⭐⭐⭐
共享锁(S)与排他锁(X):
共享锁(S锁):读锁,事务读取数据时加锁,其他事务可加S锁但不能加X锁。
排他锁(X锁):写锁,事务修改数据时加锁,其他事务不能加任何锁。
规则:S锁与S锁兼容,S锁与X锁互斥,X锁与X锁互斥。
行锁 vs 表锁:
行锁:InnoDB默认,锁定某一行(基于索引),并发高,但死锁风险高。
表锁:MyISAM默认,锁定整张表,并发低,但无死锁。
间隙锁(Gap Lock):
锁定索引记录的间隙(如id=5和id=10之间),防止其他事务插入数据,解决幻读。
仅在**可重复读(RR)**隔离级别下生效。
死锁排查:
执行 SHOW ENGINE INNODB STATUS,查看 LATEST DETECTED DEADLOCK 段,分析冲突事务和锁信息。
解决:重试事务或调整业务逻辑(如按固定顺序访问资源)。
4、InnoDB vs MyISAM: ⭐⭐⭐⭐
| 对比项 | InnoDB | MyISAM |
|---|---|---|
| 事务 | ✅ 支持ACID,适合高并发事务场景 | ❌ 不支持事务 |
| 锁粒度 | 支持行锁、表锁(默认行锁) | 仅表锁,并发性能低 |
| 外键 | ✅ 支持外键约束 | ❌ 不支持外键 |
| 崩溃恢复 | ✅ 通过redo log实现崩溃后数据自动恢复 | ❌ 无崩溃恢复机制,易数据损坏 |
| 索引结构 | 🌳 聚簇索引(数据与主键绑定) | 🌲 非聚簇索引(数据与索引分离) |
| 适用场景 | 读写频繁、需要事务(如订单、支付) | 读多写少(如日志表、配置表) |
5、数据库三大范式 ⭐⭐
- 第一范式(1NF) 每一列都是不可再分的原子值,确保表中每个字段都是单一值。例如,将“课程”字段中的“数学, 英语”拆分为两行。
- 第二范式(2NF) 所有信息必须完全依赖主键,不能只依赖主键的一部分
- 第三范式(3NF)所有信息必须直接依赖主键,不能间接依赖(A→B→C,则A→C是传递依赖)。
五、SSM+SpringBoot
5.1、Spring
1、单例 Bean 在多线程环境下是否线程安全? ⭐⭐⭐⭐
不是线程安全的。当多用户同时请求一个服务时,容器会给每个请求分配一个线程,这些线程会并发执行业务逻辑。如果处理逻辑中包含对单例状态的修改,比如修改单例的成员属性,就必须考虑线程同步问题。Spring框架本身并不对单例bean进行线程安全封装,线程安全和并发问题需要开发者自行处理。
通常在项目中使用的Spring bean是不可变状态(如Service类和DAO类),因此在某种程度上可以说Spring的单例bean是线程安全的。如果bean有多种状态,就需要自行保证线程安全。最简单的解决办法是将单例bean的作用域由“singleton”变更为“prototype”。
2、请阐述 AOP(面向切面编程)的概念、原理及应用场景 ⭐⭐⭐⭐⭐
- AOP,即面向切面编程,在Spring中用于将那些与业务无关但对多个对象产生影响的公共行为和逻辑抽取出来,实现公共模块复用,降低耦合。其核心原理是通过动态代理对目标方法进行拦截。常见的应用场景包括公共日志保存和事务处理。
- 使用AOP来记录系统操作日志。定义切入点:使用切点表达式确定哪些方法需要被记录日志。编写环绕通知:在这些方法执行前后,通过环绕通知获取请求方法的相关参数,如类信息、方法信息、注解、请求方式等。保存日志:将获取到的参数保存到数据库中。
- Spring实现事务的本质是利用AOP完成的,通过@Transactional注解。它对方法前后进行拦截,在执行方法前开启事务,在执行完目标方法后根据执行情况提交或回滚事务。
3、详细解释 IOC(控制反转)的概念、实现方式 ⭐⭐⭐⭐⭐
- 概念:将对象的创建、依赖管理从程序代码“反转”到容器(如Spring框架)完成。核心是解耦——对象不再直接new,而是由容器统一管理并注入依赖。
示例:A类依赖B类,传统方式需在A中new B();IOC模式下,容器自动创建B并注入A。
依赖注入三种方法(DI):
构造器注入:通过构造函数参数注入。
public class UserService {
private UserDao userDao;
// 构造器注入UserDao
public UserService(UserDao userDao) {
this.userDao = userDao;
}
}
AI写代码java运行
Setter注入:通过setXxx()方法注入依赖。
public class UserService {
private UserDao userDao;
// setter方法注入UserDao
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
}
AI写代码java运行
注解注入:@Autowired自动装配(常用)。
public class UserService {
// @Autowired注解自动注入UserDao
@Autowired
private UserDao userDao;
// 或使用@Resource(JDK标准注解)
@Resource
private UserDao userDao;
}
AI写代码java运行
4、请列举 Spring 事务失效的常见场景,并如何解决 ⭐⭐⭐⭐⭐
-
如果方法内部捕获并处理了异常,没有将异常抛出,会导致事务失效。因此,处理异常后应该确保异常能够被抛出。
-
如果方法抛出检查型异常(checked exception),并且没有在@Transactional注解上配置rollbackFor属性为Exception,那么异常发生时事务可能不会回滚。
-
如果事务注解的方法不是公开(public)修饰的,也可能导致事务失效。
5、描述 Spring 中 Bean 的生命周期 ⭐⭐⭐⭐⭐
| 阶段 | 关键步骤与扩展点 | 说明 |
|---|---|---|
| 1. 元数据加载 | 加载 Bean 定义 (BeanDefinition) | 容器读取配置(XML/注解/JavaConfig),解析成 BeanDefinition 对象(Bean 的“蓝图”)。 |
| 2. 实例化 | 创建 Bean 实例 | 根据 BeanDefinition,通过构造函数反射或工厂方法创建对象(此时对象是“空壳”,属性未设置)。 |
| 3. 依赖注入 | 填充属性 (populateBean) | 核心步骤! 容器自动注入: - @Autowired/@Value 注解的字段/方法- XML/JavaConfig 定义的属性值 |
| 4. Aware 注入 | 处理 Aware 接口 | 回调接口! 如果 Bean 实现了: - BeanNameAware: 注入 Bean 的 ID- BeanFactoryAware: 注入 BeanFactory- ApplicationContextAware: 注入 ApplicationContext |
| 5. 初始化前 | BeanPostProcessor.postProcessBeforeInitialization()执行 @PostConstruct | 关键扩展点! - 所有 BeanPostProcessor 的 before 方法执行- @PostConstruct 注解的方法在此执行 |
| 6. 初始化 | 执行初始化方法 | 按顺序执行: 1. InitializingBean.afterPropertiesSet() 接口方法2. XML/ @Bean 中定义的 init-method |
| 7. 初始化后 | BeanPostProcessor.postProcessAfterInitialization() | 最重要扩展点! - 所有 BeanPostProcessor 的 after 方法执行- AOP 动态代理在此阶段生成! (返回的可能是代理对象) |
| 8. 使用中 | Bean 就绪 | Bean 完全初始化,存在于单例池(Singleton Pool)中,供应用程序使用。 |
| 9. 销毁 | 销毁 Bean | 容器关闭时触发,顺序: 1. @PreDestroy 注解的方法2. DisposableBean.destroy() 接口方法3. XML/ @Bean 中定义的 destroy-method |
流程:定义 -> 实例化 -> 注入 -> Aware -> (Before -> 初始化 -> After) -> 使用 -> 销毁
6、什么是 Spring 的循环依赖?Spring 是如何解决循环依赖问题的? ⭐⭐⭐⭐⭐
循环依赖发生在两个或两个以上的bean互相持有对方,形成闭环。Spring框架允许循环依赖存在,并通过三级缓存解决大部分循环依赖问题:
-
一级缓存:单例池,缓存已完成初始化的bean对象。
-
二级缓存:存储已实例化但未初始化的Bean。
-
三级缓存:缓存ObjectFactory,用于创建bean对象。
解决循环依赖的流程如下:
- 实例化A对象,并创建ObjectFactory存入三级缓存。
- A在初始化时需要B对象,开始B的创建逻辑。
- B实例化完成,也创建ObjectFactory存入三级缓存。
- B需要注入A,通过三级缓存获取ObjectFactory生成A对象,存入二级缓存。
- B通过二级缓存获得A对象后,B创建成功,存入一级缓存。
- A对象初始化时,由于B已创建完成,可以直接注入B,A创建成功存入一级缓存。
- 清除二级缓存中的临时对象A。
需要三级缓存的原因:
二级缓存看似足够,但无法处理AOP代理场景。三级缓存通过ObjectFactory延迟决定返回原始对象还是代理对象,确保依赖注入的一致性。
7、Bean的作用域有哪些** ⭐⭐⭐
- 基础作用域(通用)
🎯 Singleton(单例) 关键字:容器级唯一 | 无状态工具类 记忆点:Spring 的默认选项,类似 Java 的静态类
🌀Prototype(原型) 关键字:每次请求新对象 | 有状态会话类 记忆点:类似 new 操作符,适合需要隔离状态的场景 - Web 核心作用域
📨 Request(请求级) 关键字:HTTP 请求周期 | 表单处理/请求参数 记忆点:每个请求独立(如购物车临时数据)
🪪Session(会话级) 关键字:用户会话周期 | 登录凭证/个性化配置 记忆点:浏览器标签页级数据隔离 - Web 扩展作用域
🌐 Application(应用级) 关键字:ServletContext 生命周期 | 全局缓存/共享配置 记忆点:相当于 Web
版的 Singleton,但绑定 Web 容器
🔌 WebSocket(长连接级) 关键字:WebSocket 会话周期 | 实时通信状态管理 记忆点:聊天室消息会话这类长连接场景
8、Spring事务传播机制 ⭐⭐⭐⭐⭐
Spring 的事务传播机制定义了事务方法在调用其他事务方法时事务的传播行为,共有 7 种:
- REQUIRED(默认),如果当前存在事务则加入,否则创建新事务,适用于大多数业务方法;
- REQUIRES_NEW,无论当前是否有事务都创建新事务并挂起当前事务,适用于需要独立事务的方法(如日志记录);
- SUPPORTS,有事务则加入,无事务则以非事务方式执行,适用于方法可在事务或非事务环境下执行;
- NOT_SUPPORTED,以非事务方式执行并挂起当前事务,适用于方法不需要事务支持(如查询操作);
- MANDATORY,有事务则加入,无事务则抛出异常,适用于方法必须在事务中执行;
- NEVER,以非事务方式执行,有事务则抛出异常,适用于方法不能在事务中执行;
- NESTED,有事务则在嵌套事务中执行,无事务则创建新事务,适用于部分回滚的场景(如批量处理)。
9、 Spring常见的注解有哪些? ⭐⭐⭐
| 注解 | 说明 |
|---|---|
| @Component、@Controller、@Service、@Repository | 使用在类上用于实例化Bean |
| @Autowired | 使用在字段上用于根据类型依赖注入 |
| @Qualifier | 结合@Autowired一起使用用于根据名称进行依赖注入 |
| @Scope | 标注Bean的作用范围 |
| @Configuration | 指定当前类是一个Spring配置类,当创建容器时会从该类上加载注解 |
| @ComponentScan | 用于指定Spring在初始化容器时要扫描的包 |
| @Bean | 使用在方法上,标注将该方法的返回值存储到Spring容器中 |
| @Import | 使用@Import导入的类会被Spring加载到IOC容器中 |
| @Aspect、@Before、@After、@Around、@Pointcut | 用于切面编程(AOP) |
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc
需要全套面试笔记及答案【点击此处即可/免费获取】
https://docs.qq.com/doc/DQXdYWE9LZ2ZHZ1ho
5.2、SpringMVC
1、简述 SpringMVC 的完整执行流程 ⭐⭐⭐⭐
SpringMVC的执行流程包括以下步骤:
- 用户发送请求到前端控制器DispatcherServlet。
- DispatcherServlet调用HandlerMapping(处理器映射器)找到具体处理器(Controller)。
- HandlerMapping返回处理器对象及拦截器(如果有)给DispatcherServlet。
- DispatcherServlet调用HandlerAdapter(处理器适配器)。
- HandlerAdapter适配并调用具体处理器(Controller)。
- Controller执行相应的业务逻辑并返回ModelAndView对象。
- HandlerAdapter将ModelAndView返回给DispatcherServlet。
- DispatcherServlet传给ViewResolver(视图解析器)进行视图解析。
- ViewResolver返回具体视图给DispatcherServlet。
- DispatcherServlet渲染视图并响应用户。
2、SpringMVC常见的注解有哪些? ⭐⭐⭐
| 注解 | 说明 |
|---|---|
| @RequestMapping | 用于映射请求路径,可以定义在类上和方法上。用于类上,则表示类中的所有的方法都是以该地址作为父路径 |
| @RequestBody | 注解实现接收http请求的json数据,将json转换为java对象 |
| @RequestParam | 指定请求参数的名称 |
| @PathVariable | 从请求路径下中获取请求参数(/user/{id}),传递给方法的形式参数 |
| @ResponseBody | 注解实现将controller方法返回对象转化为json对象响应给客户端 |
| @RequestHeader | 获取指定的请求头数据 |
| @RestController | @Controller + @ResponseBody |
5.3、SpringBoot
1、请阐述 Spring Boot 自动配置的原理 ⭐⭐⭐⭐⭐
- 在Spring Boot项目中的引导类上有一个注解@SpringBootApplication,这个注解对三个注解进行了封装,分别是:@SpringBootConfiguration @EnableAutoConfiguration @ComponentScan
- 其中@EnableAutoConfiguration是实现自动化配置的核心注解。@EnableAutoConfiguration通过@Import注解导入AutoConfigurationImportSelector类。这个类会读取项目及其引用的 Jar 包的 classpath 路径下META - INF/spring.factories文件。该文件里定义了一系列自动配置类的全类名
- 从spring.factories文件中读取到的自动配置类并非都会被加载到 Spring 容器,而是要依据条件注解来判断。常见的条件注解有@ConditionalOnClass这样的注解,判断是否有对应的class文件,如果有则加载该类,把这个配置类的所有的Bean放入spring容器中使用。
2、Springboot常见注解有哪些? ⭐⭐⭐
| 注解 | 说明 |
|---|---|
| @SpringBootConfiguration | 组合了@Configuration注解,实现配置文件的功能 |
| @EnableAutoConfiguration | 打开自动配置的功能,也可以关闭某个自动配置选项 |
5.4、Mybatis
1、mybatis的#{} 和 ${} 的区别是什么? ⭐⭐⭐⭐
-
SQL 注入风险
#{}:使用预编译语句(PreparedStatement)的参数占位符(?),能有效防止 SQL 注入。
${}:直接进行字符串替换,相当于拼接 SQL 字符串,存在 SQL 注入风险。 -
适用场景
#{}:适用于所有需要传入参数值的场景(如 WHERE 条件、INSERT 值等)。
${}:主要用于动态传入表名、列名或排序字段等 SQL 结构部分(需确保参数来源安全)。 -
参数处理
#{}:MyBatis 会自动处理参数类型,如字符串会自动添加引号。
${}:直接替换为原始值,需手动处理引号等格式。
2、 Mybatis是否支持延迟加载以及底层原理 ⭐⭐⭐
MyBatis支持延迟加载,即在需要用到数据时才加载。可以通过配置文件中的lazyLoadingEnabled配置启用或禁用延迟加载。
MyBatis 的延迟加载是通过 动态代理 实现的。具体步骤如下:
-
创建代理对象:当查询主对象时,MyBatis 不会立即加载关联对象,而是为关联对象创建一个代理对象(如 Proxy)。这个代理对象看起来和实际对象一样,但实际上并未加载数据。
-
触发加载:当程序首次访问代理对象的某个方法(如 getXXX())时,代理对象会拦截这次调用。在拦截器中,MyBatis 执行关联查询(即延迟加载),并将结果设置到代理对象中。
-
返回结果:延迟加载完成后,代理对象将加载的数据返回给调用方。
3、讲讲Mybatis的一级、二级缓存 ⭐⭐⭐⭐
- 一级缓存(本地缓存)是 SqlSession 级别 的缓存,默认开启。同一个 SqlSession 中执行的查询结果会被缓存。当 SqlSession被关闭或清空时,一级缓存会被清除。执行 insert、update、delete 操作时,一级缓存也会被清空。
- 二级缓存(全局缓存)是 Mapper 级别的缓存,需要手动开启。一级缓存未命中时,查找二级缓存。多个 SqlSession共享同一个二级缓存。二级缓存的生命周期与整个应用一致,只有当应用关闭或显式清空缓存时,二级缓存才会被清除。执行insert、update、delete 操作时,二级缓存会被清空。
六、JUC
6.1丶线程基础知识
1、进程和线程的区别 ⭐⭐⭐⭐
定义:
- 进程是操作系统资源分配的基本单位,是正在运行的程序的实例。
- 线程是进程内的执行单元,是CPU调度的基本单位。
资源分配:
- 进程拥有独立的内存空间和资源,进程间资源隔离。
- 线程共享进程的内存空间和资源,线程间通信更方便。
上下文切换:
- 进程切换开销大,需要保存和恢复整个进程的状态。
- 线程切换开销小,只需保存和恢复线程的执行状态。
独立性:
- 进程间相互独立,一个进程崩溃不会影响其他进程。
- 线程共享进程资源,一个线程崩溃可能导致整个进程崩溃。
2、并行和并发有什么区别? ⭐⭐⭐⭐
-
并发是同一时间段内处理多件事情(如多线程轮流使用CPU),适合I/O密集型任务。
-
并行是同一时刻执行多件事情(如多核CPU同时运行多个线程),适合CPU密集型任务。
3、 创建线程的四种方式 ⭐⭐⭐⭐
-
继承Thread类: 重写run()方法,直接调用start()启动线程。 缺点:单继承限制,扩展性差。
-
实现Runnable接口:实现run()方法,通过Thread类启动线程。 优点:避免单继承限制,适合资源共享。
-
实现Callable接口:实现call()方法,可以返回结果和抛出异常,通常与FutureTask或线程池结合使用。
-
线程池创建线程:通过线程池管理线程,避免频繁创建和销毁线程的开销。 优点:资源复用、控制并发数、提供定时任务等功能。
-
总结:项目中通常使用线程池创建线程,因为线程池可以更好地管理线程资源,避免资源浪费和性能问题。
4、runnable 和 callable 有什么区别 ⭐⭐⭐⭐
返回值:
- Runnable 的 run() 方法没有返回值。
- Callable 的 call() 方法有返回值,返回类型是泛型。
异常处理:
- Runnable 的 run() 方法不能抛出受检异常,只能在内部处理。
- Callable 的 call() 方法可以抛出受检异常。
使用场景:
- Runnable 适合简单的异步任务,不需要返回值。
- Callable 适合需要返回结果或抛出异常的任务。
执行方式:
- Runnable 通过 Thread 或线程池执行。
- Callable 通常与 Future 或线程池结合,通过 FutureTask.get() 获取结果(此方法会阻塞主线程)。
5、 线程的 run()和 start()有什么区别? ⭐⭐⭐⭐
功能:
- start() 用于启动新线程,JVM 会自动调用 run() 方法执行任务。
- run() 封装了线程要执行的逻辑代码,直接调用不会启动新线程。
调用次数:
- start() 只能调用一次,多次调用会抛出异常。
- run() 可以调用多次。
执行方式:
- start() 是异步执行,不会阻塞主线程。
- run() 是同步执行,会阻塞当前线程。
实际应用:
- start() 用于启动新线程,执行异步任务(如并发处理、耗时操作)。
- run() 通常用于测试或直接执行任务逻辑,不启动新线程。
6、线程包括哪些状态,状态之间是如何变化的 ⭐⭐⭐⭐⭐
线程的六种状态:
新建(NEW)、可运行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、有时限等待(TIMED_WAITING)、终结(TERMINATED)。
状态切换:
- NEW → RUNNABLE:调用 start() 方法,线程进入可运行状态。
- RUNNABLE → TERMINATED:线程代码执行完毕,进入终止状态。
- RUNNABLE → BLOCKED:线程获取锁失败,进入阻塞状态;锁释放后恢复为可运行状态。
- RUNNABLE → WAITING:调用 wait()、join() 方法,线程进入等待状态;被唤醒后恢复为可运行状态。
- RUNNABLE → TIMED_WAITING:调用 sleep(long)、wait(long)方法,线程进入有时限等待状态;超时后恢复为可运行状态。
实际应用:
- 多线程竞争锁时会发生 RUNNABLE → BLOCKED 切换。
- 线程间协作或等待资源时会发生 RUNNABLE → WAITING/TIMED_WAITING 切换。
7、新建 T1、T2、T3 三个线程,如何保证它们按顺序执行? ⭐⭐⭐
可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。
比如说:使用join方法,T3调用T2,T2调用T1,这样就能确保T1就会先完成而T3最后完成
8、notify()和 notifyAll()有什么区别? ⭐⭐⭐⭐
-
notifyAll:唤醒所有wait的线程
-
notify:只随机唤醒一个 wait 线程
9、在 java 中 wait 和 sleep 方法的不同? ⭐⭐⭐⭐⭐
共同点:wait() 和 sleep() 都会让当前线程暂时放弃 CPU 的使用权,进入阻塞状态。
方法归属:
- wait() 是 Object 的成员方法,每个对象都有。
- sleep() 是 Thread 的静态方法。
醒来时机:
- wait() 可以在指定时间后自动醒来,也可以被 notify() 或 notifyAll() 唤醒。
- sleep() 只能超时自动醒来或被中断唤醒。
锁特性:
- wait() 调用前必须先获取对象锁,执行后会释放锁。
- sleep() 调用前无需获取锁,执行期间也不会释放锁。
使用场景:
- wait() 用于线程间通信,通常与 synchronized 配合使用。
- sleep() 用于让线程暂停执行一段时间。
10、如何停止一个正在运行的线程? ⭐⭐⭐
有三种方式可以停止线程
- 使用退出标志:定义一个布尔类型的标志位,线程在执行过程中会不断检查该标志位的状态。若标志位被设置为特定值,线程就会停止执行。这是一种较为安全且推荐使用的方法。
- 使用stop方法:强行终止,可能会导致线程持有的锁被突然释放(不推荐,方法已作废)
- 使用interrupt方法:线程在运行过程中可以通过检查自身的中断状态来决定是否停止执行。
6.2丶线程并发安全
1、 讲一下synchronized关键字的底层原理? ⭐⭐⭐⭐⭐
synchronized 是 Java 中用于实现线程同步的关键字,它的底层原理是基于 JVM 的 Monitor(监视器锁)实现的,每个 Java 对象都可以关联一个 Monitor。synchronized 通过操作对象头中的锁标志位来获取和释放锁,若锁标志位显示对象处于无锁状态,线程会尝试修改锁标志位以获取锁,同时关联对应的 Monitor,之后进入同步代码块执行。
Monitor 内部维护了三个关键部分:
- Owner:当前持有锁的线程。
- EntryList:等待获取锁的线程队列(阻塞状态)。
- WaitSet:调用了 wait() 方法而进入等待状态的线程队列。
当线程尝试获取锁时,如果锁已被占用,线程会进入 EntryList 等待;当锁释放时,JVM 会从 EntryList 中唤醒线程竞争锁。synchronized 是一种悲观锁,假设并发环境下会发生冲突,因此每次访问共享资源时都会加锁。
在 JDK 1.6 之后,synchronized 引入了锁升级机制(无锁 → 偏向锁 → 轻量级锁 → 重量级锁),以优化性能。不过,由于 synchronized 依赖于 JVM 级别的 Monitor,它的性能相对较低,尤其是在高并发场景下。
2、了解synchronized锁升级吗? ⭐⭐⭐⭐
-
无锁
对象刚被创建时,Mark Word 的锁标志位为 01,偏向锁标志位为 0,表示无锁状态。 -
偏向锁
适用场景:单线程反复访问同步代码块(无竞争)。
实现原理:对象头记录第一个获取锁的线程ID。后续该线程进入同步代码块时,只需检查线程ID是否一致,无需任何锁操作(直接通行)。
优点:无竞争时几乎零开销(仅内存读取)。
缺点:一旦有其他线程竞争,立即升级为轻量级锁。 -
轻量级锁
适用场景:低竞争(如线程交替执行,无并发冲突)。
实现原理:使用CAS尝试修改对象头的锁标记。成功:获取锁;失败:自旋(循环等待)一小段时间。若自旋失败,升级为重量级锁。
优点:避免线程阻塞(用户态解决竞争),开销小于重量级锁。
缺点:自旋消耗CPU,高竞争下性能下降。 -
重量级锁
适用场景:高竞争(多线程同时抢锁)。
实现原理:基于操作系统底层的互斥量(mutex)实现。未抢到锁的线程会被挂起,进入等待队列,由操作系统调度唤醒。
优点:严格保证线程安全,适合高并发场景。
缺点:涉及用户态到内核态的切换,性能开销最大。 -
锁升级流程
无锁 → 偏向锁:首次线程访问同步块时,对象头记录线程ID。
偏向锁 → 轻量级锁:其他线程尝试竞争时,撤销偏向锁,改用CAS自旋。
轻量级锁 → 重量级锁:自旋失败(如自旋次数超过阈值,默认10次)或竞争加剧时升级。
⚠️ 注意:锁升级是单向的(不可降级),由JVM自动完成。
3、谈谈 JMM(Java 内存模型) ⭐⭐⭐⭐
Java内存模型是Java虚拟机规范中定义的一种非常重要的内存模型。它的主要作用是描述Java程序中线程共享变量的访问规则,以及这些变量在JVM中是如何被存储和读取的,涉及到一些底层的细节。
-
这个模型有几个核心的特点。首先,所有的共享变量,包括实例变量和类变量,都被存储在主内存中,也就是计算机的RAM。需要注意的是,局部变量并不包含在内,因为它们是线程私有的,所以不存在竞争问题。
-
其次,每个线程都有自己的工作内存,这里保留了线程所使用的变量的工作副本。这意味着,线程对变量的所有操作,无论是读还是写,都必须在自己的工作内存中完成,而不能直接读写主内存中的变量。
-
最后,不同线程之间不能直接访问对方工作内存中的变量。如果线程间需要传递变量的值,那么这个过程必须通过主内存来完成。
4、 什么是CAS? ⭐⭐⭐⭐
CAS(Compare And Swap) 是一种乐观锁的实现方式。该操作涉及三个操作数:内存位置(V)、预期原值(A)和新值(B)。其核心思想是,仅当内存位置 V 中的值与预期原值 A 相同时,才会将该内存位置的值更新为新值 B;若不同,则不进行更新操作。
应用场景:
- AQS 框架:用于实现线程的排队和锁的获取与释放。
- AtomicXXX 类:是Java提供的原子操作工具类,如 AtomicInteger、AtomicLong 等
- 集合框架:ConcurrentHashMap、CopyOnWriteArrayList等使用CAS优化线程安全操作。
- 轻量级锁(自旋锁):Java的轻量级锁在竞争时通过CAS尝试修改对象头中的锁标记。
优点:
- 无锁并发:CAS 是一种无锁算法,避免了传统锁机制带来的线程阻塞和上下文切换开销,在高并发场景下能显著提升性能。
- 原子性:CAS 操作是原子性的,这意味着在多线程环境中,该操作要么全部完成,要么完全不执行,不会出现部分更新的情况,保证了数据的一致性。
缺点:
- ABA 问题:若一个值从 A 变为 B,再从 B 变回 A,CAS 操作会认为该值没有发生变化,从而继续执行更新操作。可以通过版本号或时间戳解决。
- 自旋开销:当 CAS 操作失败时,线程通常会进行自旋重试。若长时间重试都无法成功,会消耗大量的 CPU 资源。
5、乐观锁和悲观锁 ⭐⭐⭐⭐
乐观锁:
- 思想:假设在大多数情况下不会发生冲突,允许多个线程同时访问共享资源,只有在更新数据时才会检查是否有冲突。
- 实现方式:通常通过版本号或时间戳来实现。
- 优点:在高并发、低冲突的场景下,性能较好。
- 缺点:在冲突较多的情况下,可能会导致频繁的重试。
悲观锁:
- 思想:假设在大多数情况下会发生冲突,因此在访问共享资源时,先获取锁,确保其他线程无法修改数据,直到当前线程释放锁。
- 实现方式:通常通过 synchronized 关键字或 ReentrantLock 来实现。
- 优点:在冲突较多的场景下,可以有效地保证数据的一致性。
- 缺点:性能开销较大,可能会导致线程阻塞。
6、谈谈你对 volatile 的理解 ⭐⭐⭐⭐
volatile 是 Java 中的一个关键字,用于修饰类的成员变量或静态成员变量,主要解决多线程环境下的可见性和有序性问题。
功能:
- 可见性:volatile修饰的变量在被修改后,会立即写回主内存,并且其他线程在读取该变量时,会直接从主内存中获取最新值,确保变量的修改对所有线程立即可见。
- 禁止指令重排序:Java 编译器和处理器为了提高性能,可能会对指令进行重排序,只要保证程序的最终执行结果和顺序执行的结果一致即可。但在多线程环境下,指令重排序可能会导致程序出现错误。volatile 修饰的变量会禁止编译器和处理器对其进行指令重排序,保证代码执行的有序性。
底层实现:
通过插入内存屏障来实现可见性和禁止指令重排序。
7、什么是AQS? ⭐⭐⭐⭐
AQS (AbstractQueuedSynchronizer) 是 Java 并发包中的一个核心框架,它为构建锁和其他同步器提供了基础实现。许多 Java 并发工具如 ReentrantLock、Semaphore、CountDownLatch 等都是基于 AQS 构建的。
核心思想:AQS 的核心是基于一个 volatile int 类型的同步状态变量(state)来表示同步状态,通过 CAS 操作来修改这个状态。当线程请求获取同步资源时,如果当前状态满足条件,则获取成功;否则,线程会被包装成一个节点(Node)并加入到一个双向队列(CLH 队列)中等待。当持有同步资源的线程释放资源时,会从队列中唤醒等待的线程,使其有机会获取同步资源。
应用场景:
AQS 定义了需要子类实现的抽象方法,具体同步器实现这些方法来定义自己的同步逻辑
- 实现阻塞式锁: ReentrantLock 就是基于 AQS 实现的独占锁,通过 AQS 来管理锁的获取和释放,实现线程的同步。
- 实现信号量:Semaphore 利用 AQS 来控制同时访问某个资源的线程数量,通过 acquire 和 release 方法来获取和释放信号量。
- 实现倒计时锁:CountDownLatch 通过 AQS 实现了一种线程等待机制,让一组线程等待其他线程完成一系列操作后再继续执行。
8、ReentrantLock的实现原理 ⭐⭐⭐
ReentrantLock 是 Java 并发包中的一个可重入锁实现,属于 API 层面的锁,与 synchronized 一样,都是悲观锁。
底层实现:
AQS:状态变量 state,初始值为 0,表示锁未被持有。被线程持有时,state 递增(支持重入),释放时递减,减为 0 时锁被完全释放。
FIFO 等待队列:未获取到锁的线程会被封装成 Node 节点进入队列阻塞,等待唤醒。
核心特性:
- 可重入性:线程可以重复获取同一把锁,重入次数通过 state变量记录。每次获取锁时,重入次数增加;每次释放锁时,重入次数减少。重入次数减到 0 时,锁完全释放。
- 核心方法:lock() 获取锁,unlock() 释放锁,tryLock() 尝试获取锁,lockInterruptibly()可中断获取锁。
公平锁与非公平锁:
- 非公平锁:默认模式,允许“插队”,提高吞吐量。
- 公平锁:按照线程等待顺序分配锁,避免线程饥饿。
9、 synchronized和ReentrantLock有什么区别 ? ⭐⭐⭐⭐⭐
好的,以下是优化后的对比表格:
ReentrantLock 与 synchronized 的核心差异
| 特性 | ReentrantLock | synchronized |
|---|---|---|
| 公平性 | 支持公平/非公平锁(通过构造函数选择) | 仅支持非公平锁 |
| 可中断性 | 支持(lockInterruptibly()) | 不支持 |
| 尝试锁 | 支持(tryLock()) | 不支持 |
| 锁超时 | 支持(tryLock(timeout, unit)) | 不支持 |
| 条件变量 | 支持多个 Condition 对象(精准唤醒) | 单个 wait()/notify() 队列 |
| 性能 | 高并发下表现更优 | 低竞争下 JVM 会优化(锁升级) |
关键差异解释
-
公平锁:
ReentrantLock可通过new ReentrantLock(true)创建公平锁,保证线程按请求顺序获取锁,避免饥饿;synchronized无法实现公平性。 -
可中断锁:
lockInterruptibly()允许线程在等待锁时响应中断,防止死锁。 -
尝试锁:
tryLock()支持无阻塞获取锁,或通过超时参数避免长时间等待。 -
条件变量:
ReentrantLock的Condition支持多个等待队列(如生产者-消费者模式中的notFull和notEmpty),而synchronized只能有一个等待队列。 -
性能:
- 低竞争:两者性能接近,JVM 会对
synchronized进行锁升级优化。 - 高竞争:
ReentrantLock的 CAS 操作避免线程挂起,性能更优。
- 低竞争:两者性能接近,JVM 会对
**10、死锁产生的条件是什么?**⭐⭐⭐⭐
死锁产生的条件包括以下四个必要条件:
- 互斥条件:资源一次只能被一个线程占用。
- 占有并等待:线程在持有资源的同时,还在等待获取其他资源。
- 不可抢占:线程已获取的资源不能被其他线程强行抢占。
- 循环等待:存在一个线程等待的循环链,每个线程都在等待下一个线程占用的资源。
如果 T1 持有 A 锁并等待 B 锁,而 T2 持有 B 锁并等待 A 锁,就会形成死锁。
避免死锁的方法:
- 破坏占有并等待条件:一次性申请所有资源。
- 破坏不可抢占条件:允许资源抢占。
- 破坏循环等待条件:对资源进行排序并按顺序申请。
**11、 如何进行死锁诊断?**⭐⭐⭐
jps:输出JVM中运行的进程状态信息
jstack:查看java进程内线程的堆栈信息
- 先通过jps来查看当前java程序运行的进程id,
输入:
jps
AI写代码java运行
- 1
输出:
1234 MainClass
5678 AnotherClass
AI写代码java运行
- 1
- 2
- 然后通过jstack来查看这个进程id,就能展示出来死锁的问题
输入:
jstack 1234
AI写代码java运行
- 1
- 并且,可以定位代码的具体行号范围,我们再去找到对应的代码进行排查就行了。
输出:
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x0000000012345678 (object 0x0000000765432100, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x0000000087654321 (object 0x0000000712345678, a java.lang.Object),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
at DeadlockExample.lambda$main$1(DeadlockExample.java:22)
- waiting to lock <0x0000000765432100> (a java.lang.Object)
- locked <0x0000000712345678> (a java.lang.Object)
"Thread-0":
at DeadlockExample.lambda$main$0(DeadlockExample.java:12)
- waiting to lock <0x0000000712345678> (a java.lang.Object)
- locked <0x0000000765432100> (a java.lang.Object)
Found 1 deadlock.
AI写代码java运行
12、 导致并发程序出现问题的根本原因是什么 ⭐⭐⭐
导致并发程序出现问题的根本原因 可以归结为 Java 并发编程的三大核心特性:原子性、可见性 和 有序性。
原子性:
-
定义:操作不可分割,要么全部执行成功,要么全部不执行。
-
问题:如 i++ 操作可能导致数据不一致。
-
解决方案:使用 synchronized、Lock 或 AtomicXXX 类。
可见性:
-
定义:一个线程对共享变量的修改能够及时被其他线程看到。
-
问题:线程可能读取到共享变量的旧值。
-
解决方案:使用 volatile、synchronized 或 Lock。
有序性:
-
定义:程序执行顺序按照代码的先后顺序执行。
-
问题:指令重排序可能导致多线程环境下程序行为异常。
-
解决方案:使用 volatile 或 synchronized 禁止指令重排序。
6.3丶线程池
1、说一下线程池的核心参数(线程池的执行原理知道嘛) ⭐⭐⭐⭐⭐
在线程池中一共有7个核心参数:
- corePoolSize 核心线程数目 - 池中会保留的最多线程数
- maximumPoolSize 最大线程数目 - 核心线程+救急线程的最大数目
- keepAliveTime 生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放
- unit 时间单位 - 救急线程的生存时间单位,如秒、毫秒等
- workQueue 队列 - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
- threadFactory 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等
- handler 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略
拒绝策略有4种,当线程数过多以后,第一种是抛异常、第二种是由调用者执行任务、第三是丢弃当前的任务,第四是丢弃最早排队任务。默认是直接抛异常。
执行原理
- 判断核心线程数:线程池首先会检查当前正在运行的线程数量是否小于核心线程数(corePoolSize)。如果是,则创建一个新的工作线程来执行该任务,即使此时线程池中可能有空闲的线程。
- 放入任务队列:如果当前正在运行的线程数量已经达到或超过核心线程数,线程池会尝试将任务放入任务队列中。如果任务队列未满,任务会被成功放入队列,等待有空闲的工作线程来执行。
- 创建新线程:如果任务队列已满,线程池会检查当前正在运行的线程数量是否小于最大线程数(maximumPoolSize)。如果是,则创建一个新的工作线程来执行该任务。
- 执行拒绝策略:如果当前正在运行的线程数量已经达到最大线程数,并且任务队列也已满,线程池会执行拒绝策略来处理新提交的任务。
2、线程池中有哪些常见的阻塞队列 ⭐⭐⭐⭐
ArrayBlockingQueue和LinkedBlockingQueue是Java中两种常见的阻塞队列,它们在实现和使用上有一些关键的区别。
- 首先,ArrayBlockingQueue是一个有界队列,它在创建时必须指定容量,并且这个容量不能改变。而LinkedBlockingQueue默认是无界的,但也可以在创建时指定最大容量,使其变为有界队列。
- 其次,它们在内部数据结构上也有所不同。ArrayBlockingQueue是基于数组实现的,而LinkedBlockingQueue则是基于链表实现的。这意味着ArrayBlockingQueue在访问元素时可能会更快,因为它可以直接通过索引访问数组中的元素。而LinkedBlockingQueue则在添加和删除元素时可能更快,因为它不需要移动其他元素来填充空间。
- 另外,它们在加锁机制上也有所不同。ArrayBlockingQueue使用一把锁来控制对队列的访问,这意味着读写操作都是互斥的。而LinkedBlockingQueue则使用两把锁,一把用于控制读操作,另一把用于控制写操作,这样可以提高并发性能。
3、如何确定核心线程数 ⭐⭐⭐
CPU 密集型任务:
核心线程数 = CPU 核心数 + 1,充分利用 CPU 资源,减少空闲。如 8 核 CPU,设核心线程数为 9。
I/O 密集型任务:
理论公式:核心线程数 = CPU 核心数 * [1 + (I/O 等待时间 / CPU 计算时间)]。例如,I/O 等待 5ms,CPU 计算 1ms,4 核 CPU,则核心线程数为 24。
经验法则:常将核心线程数设为 CPU 核心数的 2 倍或更多,如 4 核 CPU,可设 8 - 16,再性能测试定最佳值。
混合型任务:
分析 CPU 与 I/O 占比,依占比大的部分参考对应类型设置,或先按经验值设初始值,经性能测试和监控调整优化。
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc
需要全套面试笔记及答案【点击此处即可/免费获取】
https://docs.qq.com/doc/DQXdYWE9LZ2ZHZ1ho
4、 线程池的种类有哪些 ⭐⭐
在jdk中默认提供了4中方式创建线程池
- FixedThreadPool
特点:线程池中的线程数量固定。当提交任务时,如果线程池中有空闲线程,则直接使用;如果没有空闲线程,任务会在队列中等待,直到有线程可用。
使用场景:适用于处理稳定的、并发量相对固定的任务,比如服务器中处理固定数量的客户端连接请求。 - CachedThreadPool
特点:线程池中的线程数量不固定,会根据任务的提交情况动态创建和销毁线程。如果线程池中有空闲线程,会优先使用空闲线程;如果没有空闲线程,会创建新的线程来处理任务。当线程空闲时间超过一定阈值时,线程会被销毁。
使用场景:适用于处理突发性、并发量变化较大的任务,比如在 Web 应用中处理突发的大量短时间任务。 - ScheduledThreadPool
特点:主要用于定时任务和周期性任务的执行。可以指定任务在特定的延迟后执行,或者按照一定的周期定期执行。
使用场景:常用于需要定时执行的任务,如定时备份数据、定时发送邮件等。 - SingleThreadExecutor
特点:线程池中只有一个线程,所有任务会按照提交的顺序依次在这个线程中执行。它保证了任务的顺序执行,不会出现并发执行的情况。
使用场景:适用于需要顺序执行的任务,且不希望有多个线程同时访问共享资源导致数据不一致的情况,比如对数据库进行单线程的顺序操作。
5、 为什么不建议用Executors创建线程池 ⭐⭐⭐⭐
主要原因是如果使用Executors创建线程池的话,它允许的请求队列默认长度是Integer.MAX_VALUE,这样的话,有可能导致堆积大量的请求,从而导致OOM(内存溢出)。
所以,我们一般推荐使用ThreadPoolExecutor来创建线程池,这样可以明确规定线程池的参数,避免资源的耗尽。
6.4丶使用场景
1 、你们项目哪里用到了线程池 ⭐⭐⭐
订单创建:当用户下单后,需要进行一系列操作,如验证用户信息、检查商品库存、计算订单金额等。将这些操作封装成任务提交到线程池,可以并行处理多个订单的创建任务,提高系统的并发处理能力,减少用户等待时间。
2、如何控制某个方法允许并发访问线程的数量? ⭐⭐⭐
在jdk中提供了一个Semaphore类(信号量)
它提供了两个方法,semaphore.acquire() 请求信号量,可以限制线程的个数,是一个正数,如果信号量是-1,就代表已经用完了信号量,其他线程需要阻塞了。第二个方法是semaphore.release(),代表是释放一个信号量,此时信号量的个数+1
3、谈谈你对ThreadLocal的理解 ⭐⭐⭐⭐⭐
两个主要功能:
- 资源对象的线程隔离:能让每个线程拥有并使用自己独立的资源对象,避免因多个线程争用同一资源而引发线程安全问题。
- 线程内的资源共享:可在同一个线程的不同代码片段之间共享资源对象。
底层原理
ThreadLocal 内部维护了一个 ThreadLocalMap 类型的成员变量,用于存储资源对象。具体操作如下:
- set 方法:调用此方法时,会以 ThreadLocal 自身作为 key,要存储的资源对象作为 value,将其存入当前线程的ThreadLocalMap 集合中。
- get 方法:调用该方法时,会以 ThreadLocal 自身作为 key,在当前线程的 ThreadLocalMap中查找与之关联的资源值。
- remove 方法:调用此方法时,会以 ThreadLocal 自身作为 key,从当前线程的 ThreadLocalMap中移除与之关联的资源值。
内存溢出问题
ThreadLocalMap 中的 key 被设计为弱引用,在垃圾回收(GC)时,key 会被被动回收。然而,value 是强引用,不会被自动回收。通常,ThreadLocal 会被定义为静态变量(强引用),无法依赖 GC 自动回收。为避免内存溢出,建议在使用完 ThreadLocal 后,主动调用 remove 方法释放 key 和对应的 value。
更多推荐


所有评论(0)