[!TIP]
准备Java面试需要系统性地复习核心知识点、算法与数据结构、框架技术以及项目经验。
以下是一个详细的指南,帮助你高效备战。
本文所包含的内容仅供面试参考,可能存在错漏,欢迎指正。
Java核心知识点
一、基础语法
1. 数据类型
8种基本数据类型
数据类型 | 关键字 | 位数 | 默认值 |
---|---|---|---|
整数 | byte | 8 | 0 |
short | 16 | 0 | |
int | 32 | 0 | |
long | 64 | 0 | |
浮点数 | float | 32 | 0.0f |
double | 64 | 0.0d | |
字符 | char | 16 | |
布尔 | boolean | 8 | true/false |
-
整数数据类型有:
-
byte:最小的整数数据类型,在内存中占用1个字节
-
short:占用2个字节
-
int:最常用的整数数据类型,占用4个字节
-
long:占用8个字节,加上字母L表示为长整型
long num = 10000000000L; // 注意:long类型的值后面要加上字母L表示为长整型 System.out.println(num); // 输出:10000000000
-
-
浮点数数据类型有:
-
float:占用4个字节,通常在数值后面加上字母f或F来表示
float num = 3.14f; System.out.println(num); // 输出:3.14
-
double:占用8个字节
[!IMPORTANT]
在使用浮点数据类型时,需要注意浮点数的精度问题。由于浮点数采用二进制浮点数表示法,可能存在精度损失的情况。因此,在涉及到精确计算的场景中,应尽量避免使用浮点数,而使用BigDecimal类等其他方式进行精确计算。
-
-
布尔数据类型有:
- boolean:
- true/false,可以用于判断条件、控制流程以及表示真假等逻辑操作
- 布尔数据类型也常与逻辑运算符一起使用,例如逻辑与(&&)、逻辑或(||)、逻辑非(!)
- boolean:
-
字符数据类型有:
-
char:
-
使用单引号将字符包围起来来声明一个字符变量
char ch1 = 'A'; char ch2 = 'B'; System.out.println(ch1); // 输出:A System.out.println(ch2); // 输出:B
-
除了使用单个字符外,还可以使用转义序列来表示特殊字符,例如换行符(\n)、制表符(\t)等
char newLine = '\n'; char tab = '\t'; System.out.println("Hello" + newLine + "World"); System.out.println("Java" + tab + "Programming"); // 输出: // Hello // World // Java Programming ("Java"和"Programming"之间有多个制表符)
-
字符数据类型也支持直接使用Unicode编码来表示字符,使用反斜杠u后面跟上4位16进制数字
char chineseChar = '\u4E2D'; // 表示“中”字 System.out.println(chineseChar); // 输出:中
-
字符数据类型可以与整数数据类型互相转换,字符数据类型的底层存储是Unicode字符编码的整数值
char ch = 'A'; int asciiValue = ch; // 将字符转换为ASCII值 System.out.println(asciiValue); // 输出:65 int intValue = 66; char character = (char) intValue; // 将ASCII值转换为字符 System.out.println(character); // 输出:B
[!IMPORTANT]
字符数据类型只能用于表示单个字符,不能表示字符串。如果需要表示多个字符,应使用字符串数据类型:String
-
-
引用数据类型
-
类(Class)
- 类是Java中最常见的引用数据类型。类定义了一种数据结构,它包含字段(属性)和方法(行为)。
-
接口(Interface)
- 接口是一种特殊的引用数据类型,它定义了一组方法,但不提供实现。类可以实现一个或多个接口,以定义特定的行为。
-
数组(Array)
- 数组是一种引用数据类型,用于存储固定大小的相同类型元素的集合。数组在创建时需要指定大小,且大小不能更改。
-
枚举(Enum)
- 枚举是一种特殊的引用数据类型,它表示一组固定的常量。枚举用于定义一组预定义的值。
-
字符串(String)
- 字符串在Java中是一个类,专门用来表示字符序列。字符串是不可变的,即一旦创建就不能修改。
-
泛型类型(Generic Types)
- 泛型类型允许类、接口和方法操作任意类型的数据,这样就可以使用一种通用方法,而不需要为每种数据类型编写特定的方法。
2. 数据类型的转换
基本数据类型之间可以转换,主要包含:byte、short、int、long、float、double和char,不包含boolean类型。当你需要将一个数据类型转换为另一个数据类型时,可以使用自动转换和强制转换。
自动转换指的是容量小的数据类型可以自动转换为容量大的数据类型或者从低精度的数据类型自动转换为高精度的数据类型。
强制转换则相反,强制转换过程中可能会丢失精度或引发数据溢出的风险。在进行强制转换时,你需要使用括号将目标数据类型括起来,并在其前面加上转换操作符。
自动转换(隐式转换)
-
把int类型的值转换为double类型
int i = 3; double d = i; System.out.println(d); // 输出:3.00
-
Java支持自动类型转换的类型如下,左边的数值类型可以自动转换成箭头右边的数值类型。
- byte -> short -> int -> long -> float -> double
- char -> int -> long -> float -> double
强制转换(显式转换)
-
把double类型的值转换为int类型
double d = 3.14; int i = (int) d; System.out.println(i); // 输出:3,丢失小数点后面的数值
3. 基本数据类型和引用数据类型的区别
存储
- “基本数据”类型是直接保存在栈中的
- “引用数据”类型在栈中保存的是一个地址引用,这个地址指向的是其在堆内存中的实际位置(栈中保存的是一个地址,而实际的内容是在堆中,通过地址去找它实际存放的位置)
比较
- “基本数据”类型的比较是值的比较
- “引用类型”的比较是引用地址的比较
赋值
- 基本数据类型的赋值是简单赋值,如果一个变量向另一个变量赋值“基本类型”的值,会在变量对象上创建一个新值,然后把这个值复制到为新变量分配的位置上。
- 当一个变量向另一个变量赋值“引用类型”的值时,同样也会将栈内存中的值复制一份放到新变量分配的空间中,但是引用类型保存在栈内存中的变量是一个地址,这个地址指向的是堆内存中的对象,所以这个变量其实复制了一个地址,两个地址指向同一个对象,改变其中任何一个变量都会互相影响。
4. 自动装箱和自动拆箱
装箱
-
自动将基本数据类型转换为包装器类型
-
在JavaSE 5之前,如果要生成一个数值为10的Integer对象,必须这样进行:
Integer i = new Integer(10);
而在从JavaSE 5开始就提供了自动装箱的特性,如果要生成一个数值为10的Integer对象,只需要这样就可以了:
Integer i = 10;
-
装箱就是调用了包装类的valueOf()方法
Integer i = 10; 等价于 Integer i = Integer.valueOf(10)
拆箱
-
自动将包装器类型转换为基本数据类型
Integer i = 10; //装箱 int n = i; //再将装箱好的Integer类型的数值i赋值给int类型就是拆箱
-
拆箱就是调用了 xxxValue()方法
int n = i; 等价于 int n = i.intValue();
5. String字符串的不可变性
为什么不可变
- String 内部通过 final 修饰的 char[] value 保存字符串内容,这确保引用本身不会改变,但数组内容实际上是可变的
- 操作 String 内容的方法(如 replace、concat)并不会修改原对象,而是创建一个新的字符串对象
为什么 String 要设计成不可变
- 我们使用String类对象存放常量字符串时,会涉及到将字符串对象放到字符串常量池中,字符串常量池可以看成是一个字符串资源区。当我们存放一个字符串对象在字符串常量池内,下一次再使用这个字符串对象,就可以直接从常量池内拿出来使用,可以节省创建新字符串对象所浪费的时间和空间
- 但如果能够修改字符串的内容,那么意味着修改后的内容在字符串常量池内可能就要换个位置存放,如果不换在后续存放时就会发生存放重复字符串的情况
- 而如果选择换位置,那每对字符串常量池中的对象内容进行修改就要重新换位置,这样又复杂又降低了性能
- 而字符串中的hash即是定位字符串对象在字符串常量池中位置,如果字符串内容能被更改,那么其hash每次也要进行修改,那么每次还要重新计算hash值,而在多线程情况下,当字符串对象能修改,那么每个线程都可以对字符串进行修改,可能就会发生多个线程同时对字符串内容进行修改的情况,导致线程不安全
不可变的效率问题
- 由于 String 不可变,所有修改操作都会创建新的字符串对象,这在频繁修改字符串时效率较低。
6. String/StringBuilder/StringBuffer
类型 | 可变性 | 线程安全 | 性能 |
---|---|---|---|
String | 不可变 | 安全 | 正常 |
StringBuilder | 可变 | 不安全 | 单线程最佳 |
StringBuffer | 可变 | 安全 | 多线程较优 |
-
是否可变性
-
String是不可变字符串,每次修改都会创建新对象
-
StringBuilder和StringBuffer是可变字符串,修改会影响原对象
-
-
是否线程安全
-
String 是线程安全的(不可变)
-
StringBuffer 是线程安全的(方法加了同步锁)
-
StringBuilder 是非线程安全的
-
-
性能
-
单线程下,StringBuilder 性能最佳
-
多线程下,StringBuffer 性能较优
-
-
String转换为StringBuilder、StringBuffer
-
构造方法
String str = "Star Ocean"; StringBuffer buffer1 = new StringBuffer(str);
-
append方法
String str = "Star Ocean"; StringBuffer buffer2 = new StringBuffer(); buffer2.append(str);
-
-
StringBuilder、StringBuffer转换为String
-
构造方法
StringBuffer buffer = new StringBuffer("Star Ocean"); String str1 = new String(buffer);
-
toString方法
StringBuffer buffer = new StringBuffer("Star Ocean"); String str2 = buffer.toString();
-
7. 字符串常量池
具体内容较为复杂,给出参考链接。
参考博客:https://blog.csdn.net/sermonlizhi/article/details/124945205
8. ==和equals()
区别 | == | equals() |
---|---|---|
比较的内容 | 基本数据类型:比较值 | 重写后:可与==类似 |
引用数据类型:比较引用地址 | 未重写:比较引用地址 | |
适用类型 | 基本数据类型和引用数据类型 | 引用数据类型 |
是否可重写 | 不可以 | 可以 |
- equals() 是 Object 类中的方法,默认情况下用于比较两个对象的引用地址,即判断两个对象是否为同一个对象。然而,许多类(如 String、Integer)都会重写 equals() 方法,使其能够比较对象的内容。
9. hashCode()和equals()
作用
- hashCode()方法和equals()方法的作用其实一样,在Java里都是用来对比两个对象是否相等一致
区别
- 性能
- 因为重写的 equals() 一般比较的内容全面和复杂,这样效率就比较低,而利用hashCode()进行对比,则只要生成一个hash值进行比较就可以了,效率很高
- 可靠性
- 因为hashCode()并不是完全可靠,有时候不同的对象他们生成的hashcode也会一样(生成hash值的公式可能存在的问题),所以hashCode()只能说是大部分时候可靠,并不是绝对可靠
- 所以equals()相等的两个对象他们的hashCode()肯定相等,而hashCode()相等的两个对象他们的equals()不一定相等
使用场景
- 对于需要大量并且快速的对比,如果都用equals()去做显然效率太低
- 解决方式是:每当需要对比的时候,首先用hashCode()去对比,如果hashCode()不一样,则表示这两个对象肯定不相等(也就是不必再用equals()去对比了),如果hashCode()相同,此时再对比他们的equals(),如果equals()也相同,则表示这两个对象是真的相同了
在某些时候,只要重写 equals,就必须重写 hashCode。
-
因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须重写这两个方法
-
如果自定义对象做为 Map 的键,那么必须重写 hashCode 和 equals
10. 异常
分类
- 错误:
Error
,程序直接结束 - 异常:
Exception
- 检查异常:
Checked Exception
,编译时检查(如IOException
),必须捕获或声明抛出 - 非检查异常:
Unchecked Exception
,运行时异常(如NullPointerException
、ArrayIndexOutOfBoundsException
),不强制处理
- 检查异常:
异常抛出
-
thrwos
-
主要是在方法定义上使用的,表示的是此方法之中不进行异常的处理,而交给被调用处处理
-
在调用throws声明方法的时候,一定要使用异常处理操作进行异常的处理,这属于强制性的处理
-
-
throw
-
上面的异常类对象都是由JVM自动进行实例化操作的,用户也可以自己手动的抛出一个异常类实例化对象,通过throw完成
-
-
throw和throws的区别
- throw:在方法体内使用,表示人为的抛出一个异常类对象(这个对象可可以是自己实例化的,也可以是已存在的)
- throws:在方法的声明上使用,表示在此方法中不进行异常的处理,而交给被调用处处理
异常处理
-
try-catch-finally语句
-
try块:负责捕获异常,一旦try中发现异常,程序的控制权将被移交给catch块中的异常处理程序
-
catch块:负责处理异常,比如发出警告:提示、检查配置、网络连接,记录错误等。执行完catch块之后程序跳出catch块,继续执行后面的代码
-
finally:最终执行的代码,用于关闭和释放资源
-
语法结构
try{ //一些会抛出的异常 } catch(Exception e){ //第一个catch //处理该异常的代码块 } catch(Exception e){ //第二个catch,可以有多个catch //处理该异常的代码块 } finally { //最终要执行的代码 }
[!IMPORTANT]
finally块中的return语句可能会覆盖try块、catch块中的return语句,如果finally块中包含了return语句,即使前面的catch块重新抛出了异常,而调用该方法的语句也不会获得catch块重新抛出的异常,而是会得到finally块的返回值,并且不会捕获异常。
面对上述情况,其实更合理的做法是,既不在try/catch内部中使用return语句,也不在finally内部使用return语句,而应该在 finally 语句之后使用return来表示函数的结束和返回。
-
-
try-with-resources
-
try-with-resources 是 Java 7 中引入的一个新特性,用于简化资源管理,一般是用于处理实现了 AutoCloseable 或 Closeable 接口的资源(如文件、数据库连接等),用于确保在使用完资源后自动关闭资源,避免资源泄漏。简言之就是:自动关闭资源。
-
在try-catch-finally中关闭资源如下:
try { //声明自己的资源 } catch(Exception e) { //抛出异常 } finally { //关闭资源 }
-
而在try-with-resources中:
try( //需要自动关闭的资源 执行结束会自动关闭 ) { //对资源的操作 } catch(Exception e) { //抛出异常 } //由于不需要手动关闭资源,所以不需要finally
[!IMPORTANT]
查看父类有没有实现AutoCloseable或者Closeable接口,只有实现了 AutoCloseable 或 Closeable 接口的资源才能使用try-with-resources
-
11. 自定义异常
-
定义
- 继承Throwable或者他的子类Exception的用户自己定义的异常类
-
语法结构
class UserException extends Exception { UserException(){ super(); ...//其他语句 } }
-
系统定义的异常与用户定义的异常区别
- 系统定义的异常是特定情况出现的问题,而此时用来对可能遇到的问题进行处理。
- 系统异常有两种,一种是检查异常,一种是非检查异常。检查异常要求用户捕获或者抛出的,不捕获或者抛出就会编译不通过。非检查异常编译可以通过,但是运行时才显露出来。
- 用户定义的异常是用户自己觉得可能会出现问题时,需要处理的。这样避免程序中断或是出现未知错误。
12. 控制流语句
条件判断语句
-
if-else
- 过于简单,不做赘述
-
switch-case
-
用于根据变量的值执行不同的代码块,它是多分支选择的一种简洁写法,适用于处理多个条件分支的情况。
-
代码样例
int day = 3; String dayName; switch (day) { case 1: dayName = "Sunday"; break; case 2: dayName = "Monday"; break; case 3: dayName = "Tuesday"; break; case 4: dayName = "Wednesday"; break; case 5: dayName = "Thursday"; break; case 6: dayName = "Friday"; break; case 7: dayName = "Saturday"; break; default: dayName = "Invalid day"; break; } System.out.println("Today is " + dayName);
-
default
是可选的,用于在没有匹配的情况下执行默认的代码块
[!IMPORTANT]
switch语句不能作用在long类型上
在 Java 中,long 是一种 64 位有符号整数类型,它的取值范围非常大(-2^63 到 2^63-1)。然而,switch 语句的表达式类型必须是可以高效比较的类型,而 long 类型的比较相对较复杂,且 switch 语句的设计初衷是为了高效处理。因此,long 类型不能作为 switch 语句的表达式。
如果尝试将 long 类型变量用在 switch 语句中,编译器会报错。
-
循环语句
-
for
- 过于简单,不做赘述
-
for-each
-
底层使用了迭代器来遍历集合
-
语法结构
for (类型 变量 : 迭代对象) { // 循环体 }
-
优点
- 简洁,不需要手动管理索引或迭代器,代码易读
- 适用性广泛,可用于数组、集合、键值对
-
限制
- 不能对集合进行add和remove操作
- 不能直接获取元素的索引
- 只支持顺序访问
-
在Map中的使用
-
keySet,遍历Map中的所有键
-
代码示例
for (String key : map.keySet()) { System.out.println("Key: " + key + ", Value: " + map.get(key)); }
-
-
entrySet,遍历Map中的所有键和值
-
代码示例
for (Map.Entry<String, Integer> entry : map.entrySet()) { System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue()); }
-
-
-
-
while
- 过于简单,不做赘述
-
do-while
- 过于简单,不做赘述
跳转语句
- break
- 用于立即退出循环或switch语句。它可以用于在满足特定条件时终止循环
- continue
- 用于跳过当前循环的剩余部分,直接进入下一次迭代
13. static与final
static
-
修饰类中的成员变量
- 称作静态变量
- 不需要通过对象(
即 new 类名
)来访问,而是可以直接通过类(类名.静态变量
)访问
-
修饰方法
- 称作静态方法
- 调用方式和静态变量的调用方式相似,直接通过类(
类名.静态方法
)访问
-
修饰常量
- 称作静态常量
- 访问方式和静态方法相似
-
修饰代码块
-
称作静态代码块
-
执行优先级高于非静态的初始化块,它会在类初始化的时候执行一次,执行完成便销毁,它仅能初始化类变量,即 static 修饰的数据成员
-
语法结构
static { // 根据需求填写语句 }
-
静态代码块与非静态代码块的区别
- 静态代码块的执行顺序:静态代码块 -> 非静态代码块 -> 构造函数
- 静态代码块被 static 修饰,是属于类的,只在项目运行后类初始化时执行一次。而非静态代码块在每个对象生成时都会被执行一次
-
-
修饰类
- 如果一个类要被声明为static的,只有一种情况,就是静态内部类
- 静态内部类和普通内部类的区别
- 静态内部类跟静态方法一样,只能访问静态的成员变量和方法,不能访问非静态的方法和属性,但是普通内部类可以访问任意外部类的成员变量和方法
- 静态内部类可以声明普通成员变量和方法,而普通内部类不能声明static成员变量和方法。
- 静态内部类可以单独初始化
final
- 修饰类
- 被修饰的类不能被继承
- 修饰方法
- 不能被子类重写
- 修饰属性
- 该属性为常量,需要初始化,且不能被修改
- 常量命名通常用大写字母,每个字母中间用下划线隔开
[!IMPORTANT]
用final修饰引用数据类型时,可以修改该引用数据类型的内容,但是不能修改它的引用地址。
14. final、finally、finalize的区别
- final为关键字
- 用于标识常量的关键字,final标识的关键字存储在常量池中
- 被修饰的类不能被继承
- 被修饰的方法不能被子类重写
- 被修饰的属性必须初始化且不能被修改
- finally为区块标志
- finally{}用于标识代码块,与try{ }进行配合使用
- finalize为方法
- 在Object中进行了定义,用于在对象“消失”时,由JVM进行调用用于对对象进行垃圾回收
- 在实际应用中,不要依赖使用该方法回收任何短缺的资源,这是因为很难知道这个方法什么时候被调用
15. return在try-catch-finally中的使用
- finally块中的return语句可能会覆盖try块、catch块中的return语句,如果finally块中包含了return语句,即使前面的catch块重新抛出了异常,而调用该方法的语句也不会获得catch块重新抛出的异常,而是会得到finally块的返回值,并且不会捕获异常。
- 面对上述情况,其实更合理的做法是,既不在try/catch内部中使用return语句,也不在finally内部使用return语句,而应该在 finally 语句之后使用return来表示函数的结束和返回。
16. 过滤器(Filter)与拦截器(Interceptor)区别
特性 | 过滤器(Filter) | 拦截器(Interceptor) |
---|---|---|
用途 | 偏重于设置字符集、权限控制等通用功能 | 偏重于权限控制、日志等业务功能 |
触发机制 | 在请求进入容器后,但请求进入servlet之前 或 在servlet处理完后,返回给前端之前 | 在请求进入servlet后,在进入Controller之前进行预处理的 |
实现方式 | 基于回调函数 | 基于反射 |
使用范围 | 依赖于Servlet容器,属于Servlet规范的一部分 | 独立存在的,可以在任何情况下使用 |
拦截范围 | 对所有访问 | 仅针对SpringMVC的访问 |
-
过滤器实现方式
-
实现
Filter
类 -
重写
init、doFilter、destroy
方法 -
代码示例
public class MyFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { Filter.super.init(filterConfig); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { log.info("MyFilter"); } @Override public void destroy() { Filter.super.destroy(); } }
-
-
拦截器实现方式
-
自定义拦截器
-
实现
HandlerInterceptor
类 -
重写
preHandle、postHandle、afterCompletion
方法public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception { log.info("preHandle"); String clientIP = ServletUtil.getClientIP(httpServletRequest); log.info("访问IP:"+clientIP); log.info("请求路径:{}", httpServletRequest.getRequestURI()); return true; } @Override public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { log.info("postHandle"); } @Override public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { log.info("afterCompletion"); } }
-
注册拦截器
-
继承
WebMvcConfigurerAdapter
类@Configuration public class WebMvcConfig extends WebMvcConfigurerAdapter { // addPathPatterns 用于添加拦截规则 // excludePathPatterns 排除拦截 registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/login"); registry.addInterceptor(new MyInterceptor()) .addPathPatterns("/**"); super.addInterceptors(registry); }
-
二、面向对象
1. 三大特征
- 封装
- 核心思想就是“隐藏细节”、“数据安全”
- 将对象不需要让外界访问的成员变量和方法私有化,只提供符合开发者意愿的公有方法来访问这些数据和逻辑,保证了数据的安全和程序的稳定
- 简言之,所有的内容对外部不可见
- 继承
- 子类可以继承父类的属性和方法,并对其进行拓展
- 多态
- 同一种类型的对象执行同一个方法时可以表现出不同的行为特征
- 通过继承的上下转型、接口的回调以及方法的重写和重载可以实现多态
2. 栈和堆
栈(Stack)
- 一种后进先出(LIFO)的数据结构,在程序运行中用于存储数据和指令的内存空间
- 栈是由系统自动分配和管理的
- 栈的大小是固定的,由操作系统指定
- 栈空间用于存储基本数据类型和引用类型的地址
堆(Heap)
- 用于分配程序中动态数据结构的内存空间
- 堆需要程序员手动进行分配和管理,提供了极为灵活的空间分配和管理手段
- 堆空间通常由系统分配初始大小,程序员可以自由地调整大小
- 用于存储动态分配的对象和复杂数据结构
3. 抽象类和接口
功能 | 普通类 | 抽象类 | 接口 |
---|---|---|---|
修饰词 | class | abstract class | interface |
是否可以实例化 | 可以 | 不可以 | 不可以 |
是否有构造方法 | 有 | 有 | 没有 |
属性和方法的权限修饰符 | 全部 | 不能被private修饰 不能被final和static修饰 | 只能被public修饰 |
子类的继承关系 | 只能继承 | 只能继承 | 只能实现 |
属性限制 | 全部 | 全部 | 只有常量 |
子类重写 | 不需要 | 抽象方法必须重写 | 不需要 |
抽象类
- 被关键字 abstract 修饰的类是抽象类,子类使用关键字extends继承
- 抽象类不能被实例化,即不能使用 new 关键字创建实例
- 抽象方法不能是 private 的
- 抽象方法要被子类重写,所以抽象方法不能被final和static修饰
- 抽象类必须被继承,并且继承后子类要重写父类中的抽象方法,否则子类也是抽象类,必须要使用 abstract 修饰,否则报错
- 抽象类可以定义自己的成员变量和常量、抽象方法、普通方法
- 抽象类有构造方法
- 构造方法不是作用于实例化的,主要作用是对类属性初始化操作,抽象类可以定义变量,自然需要构造函数来赋值等操作
- 抽象类属于特殊类,而Java规定每个类都需要构造函数
接口
-
用 interface 定义的是接口,子类使用关键字 implements 实现接口
-
接口不能被实例化
-
接口内方法只能被权限修饰符 public 修饰
-
接口内属性定义必须初始化
- 在定义属性时,编译器在编译的时会自动加上 public static final 修饰符,所以必须初始化,即赋值
- 方法也会自动加上 public abstract
-
接口中不能有静态代码块和构造方法
-
接口可以在接口内定义静态方法,并且必须要有方法体
-
代码示例
public interface Demo { // 静态方法 static void test(){ // 方法体 } }
-
调用示例
Demo.test(); // 接口名.方法名
[!TIP]
随着发展,需求的增加,有时候需要在接口内定义静态方法,才能达到特定目的,而接口中的静态方法有个很多的特点就是可以直接使用接口名调用静态方法,不用创建实例,而对于一些工具类,把特定的方法聚集在接口中,是更好的选择,更加方便也提高了接口的适用性。
-
-
接口可以使用关键字 default 定义默认方法,有方法体,默认方法可实现,也可不实现
- 大部分子类都需要新增一个方法时,就可以在接口定义一个默认方法,子类调用即可
- 当有一些重复性代码、共性代码时,就可使用默认方法写在接口中,大大减少了代码的重复性,提高代码的复用性
4. 重写和重载
功能 | 重写 | 重载 |
---|---|---|
定义 | 子类只改写父类方法的方法体 | 不改变方法名,但会改变方法的参数和方法体 |
范围 | 发生在父类和子类之间 | 发生在一个类中 |
多态 | 体现在运行时的多态性 | 体现在编译时的多态性 |
参数 | 参数必须相同 | 参数个数、类型、顺序均可不同 |
修饰符 | 不小于父类的范围 | 无要求 |
5. 反射机制的作用及缺点
是什么
- 在程序运行时动态加载类并获取类的详细信息,从而操作类或对象的属性和方法
作用
- 反射机制一般用来解决Java 程序运行期间,对某个实例对象一无所知的情况下,如何调用该对象内部的方法问题。通过反射,可以在运行时动态地创建对象并调用其属性,不需要提前在编译期知道运行的对象是谁。
优点
- 可以动态地创建和使用对象,反射机制是 Java 框架的底层核心,其使用灵活,没有反射机制,底层框架就失去支撑
缺点
- 使用反射基本是解释执行,对程序执行速度有影响
- 反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题
6. 深拷贝与浅拷贝的区别
浅拷贝
- 拷贝对象时,原对象的引用类型字段(如数组、集合、其他对象等)并不会被复制,而是直接复制其引用
- 要使用自带的浅拷贝机制,需要让被拷贝的类实现
Cloneable
接口,并且重写clone()
方法
深拷贝
- 拷贝对象时,原对象的所有字段,包括引用类型字段,都被递归地复制一份新的对象
- 对于简单的对象结构,可以通过手动递归的方式来实现深拷贝
- 比较通用且有效的实现深拷贝的方式是利用序列化和反序列化机制,被拷贝的类需要实现
Serializable
接口
对比
特性 | 浅拷贝 (Shallow Copy) | 深拷贝 (Deep Copy) |
---|---|---|
对象拷贝 | 引用类型字段只拷贝地址 | 引用类型字段拷贝整个对象 |
共享对象 | 共享引用类型字段 | 不共享引用类型字段 |
性能 | 比较快,因为只是拷贝引用 | 较慢,因为需要递归复制每个对象 |
使用场景 | 引用类型字段不需要独立修改时适用 | 引用类型字段需要独立修改时适用 |
适用场景
- 浅拷贝适用于那些对象的引用类型字段不需要独立修改的场景。比如,如果两个对象都应该共享同一个数据资源,浅拷贝是一个合适的选择。
- 深拷贝适用于当你需要创建一个完全独立的对象副本时,特别是当对象中包含需要独立管理的复杂数据时,深拷贝是不可或缺的。
7. 泛型中extends和super的区别
泛型是什么
- 参数化类型
- 在编程期间没有指定具体类型,而是给出一个限制
泛型的使用
- 泛型的数据类型只能填写引用数据类型,基本数据类型不可以
- 指定泛型的具体类型之后,可以传入该类类型或其子类类型
- 如果不手动添加泛型,则默认泛型为 Object
extends
-
上界限制:使用
extends
关键字定义了一个类型参数的上界- 意味着传入的类型必须是指定类型或其子类型
-
读取操作:允许进行读取(get)操作
- 因为可以确保所有对象至少有指定类型的方法和属性
-
写入限制:由于Java的类型擦除和安全性考虑,如果有上界限制,直接添加对象到集合可能会受到限制(特别是当使用非具体类型如
? extends T
时) -
示例
List<? extends Number>
可以接受List<Integer>
或List<Double>
等,但你不能直接向这样的列表中添加元素,除了null
,因为编译器无法确定确切的类型。
-
extends
用于确保可以安全地使用对象的特性(即读取)
super
-
下界限制: 使用
super
关键字定义了一个类型参数的下界- 意味着传入的类型必须是指定类型或其父类型
-
读取限制:读取时可能需要类型转换
- 因为集合中可能包含多种类型,所有只能通过基类引用访问
-
写入操作:允许写入(put)操作
- 因为你总是可以向上转型为基类,所以可以安全地添加基类及其子类的实例
-
示例
List<? super Number>
可以接受List<Number>
、List<Object>
或任何Number的父类的List,你可以向这样的列表中添加Number
或其任何子类的实例。
-
super
用于确保可以安全地插入对象(即写入)
8. 内部类
概述
- 定义在另一个类内部的类
分类
-
成员内部类
-
定义在外部类的成员位置上,没有 static 修饰,与成员变量和方法平级
-
代码结构
class OuterClass { private String outerClassField = "outer class's field~"; class MemberInnerClass { // 成员内部类 private String memberInnerClassField = "member inner class's field"; } }
-
-
静态内部类
-
使用static关键字修饰,相当于外部类的一个静态成员
-
代码结构
class OuterClass { static class StaticNestedClass { // 静态内部类 } }
-
-
局部内部类
-
定义在方法、构造器或代码块内部,类似于方法的局部变量,它的作用域仅限于定义它的块中
-
代码结构
class OuterClass { public OuterClass() { class LocalInnerClassA { // 在构造器中定义局部内部类 } } { class LocalInnerClassB { // 在实例代码块中定义局部内部类 } } static { class LocalInnerClassC { // 在静态代码块中定义局部内部类 } } public void fun() { int localVar; class LocalInnerClassD { // 在成员函数中定义局部内部类 } } }
-
-
匿名内部类
-
没有名字的内部类,常用于简化代码
-
匿名内部类通常用于创建一个类的实例,而这个类只需要使用一次。匿名内部类可以继承一个类或实现一个接口,并且可以在定义类的内容的同时创建对象。
-
代码结构
new 父类构造器(参数列表) { // 类体 }; new 接口名() { // 类体 };
-
三、集合
[!IMPORTANT]
注意,以下所有集合中提到的无序,若无特别说明,都特指:元素存入集合的顺序与元素在集合中保存的顺序无关,而不是指集合中元素的排列顺序
1. 集合框架
Collection
-
集合,存储元素集合
Map
-
图,存储键值对
2. List
ArrayList
- 特点
- 底层是数组
- 初始长度为0,长度可变
- 查询快,增删慢
- 线程不安全
- 元素有序、可重复
- 常用方法
- add(Object o):向数组的尾部添加指定的元素
- set(int i, Object o):设置数组指定位置的元素的值
- remove(Object o):删除数组中第一次出现的元素
- size():返回数组的元素个数
- get(int index):返回数组中指定位置的元素,下表从0开始
- clear():移除数组中所有元素
- isEmpty():判断数组中是否有元素
LinkedList
- 特点
- 底层是双向链表
- 增删快,查询慢
- 线程不安全
- 元素有序、可重复
- 常用方法
- add(Object o):向链表末尾添加元素
- get(int i):获取指定下标的元素
- getFirst():获取链表中的第一个元素
- getLast():获取链表中的最后一个元素
- contains(Object o):判断元素是否存在链表中
- set(int i, Object o):设置链表指定位置的元素的值
- remove(int i):删除指定位置的元素
- removeFirst():删除链表中的第一个元素
- removeLast():删除链表中的最后一个元素
- clear():清空链表
3. Set
HashSet
- 特点
- 底层是哈希表
- HashSet的底层是通过HashMap实现
- HashMap是通过数组加链表加红黑树实现的
- 元素无序,不重复
- 不重复的原理
- 把对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的hashcode值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现,会将对象插入到相应的位置中。如果有相同hasdcode值的对象,会调用对象的equals()方法来检查对象是否真的相同,如果相同,则HashSet就不会让重复的对象加入到HashSet中,这样就达到去重效果,保证元素不会重复
- 不重复的原理
- 元素可为null
- 线程不安全
- 底层是哈希表
- 常用方法
- add(Object o):向集合中添加元素
- size():返回集合的元素个数
- remove(Object o):删除集合中指定的元素
- clear():清空集合
- contains(Object o):判断元素是否存在集合中
LinkedHashSet
- 特点
- 底层是哈希表和链表
- 底层是通过HashSet实现
- 同时使用链表保证元素的插入顺序
- 元素有序,不重复
- 线程不安全
- 底层是哈希表和链表
- 常用方法
- add(Object o):向集合中添加元素
- size():返回集合的元素个数
- remove(Object o):删除集合中指定的元素
- clear():清空集合
- contains(Object o):判断元素是否存在集合中
TreeSet
- 特点
- 底层是二叉树
- TreeSet的底层是通过TreeMap实现
- TreeMap是通过红黑树实现的
- 元素无序,不重复
- 元素不能为null
- 线程不安全
- 底层是二叉树
- 常用方式
- add(Object o):向集合中添加元素
- size():返回集合的元素个数
- remove(Object o):删除集合中指定的元素
- clear():清空集合
- contains(Object o):判断元素是否存在集合中
4. Map
HashMap
- 特点
- 底层是数组加链表加红黑树
- 键不允许重复,值可以重复
- 元素无序
- 键和值均可为null
- 线程不安全
- 常用方法
- put(key, value):添加一个键值对到集合中
- get(key):获取指定键所对应的值,没有返回null
- size():获取集合的大小
- clear():清空集合
- isEmpty():判断集合是否为空
- remove(key):删除集合中对应的键和它的值
- containsKey(key):判断集合中是否包含该键
- containsValue(value):判断集合中是否包含该值
LinkedHashMap
- 特点
- 继承于HashMap,是基于数组和双向链表实现的
- 元素有序,并且我们可以自己决定使用哪种顺序
- LinkedHashMap头部存的是最久前访问的节点或最先插入的节点,尾部存的是最近访问的和最近插入的节点
ConcurrentHashMap
-
概述
- ConcurrentHashMap相当于是HashMap的多线程版本,它的功能本质上和HashMap没什么区别。因为HashMap在并发操作的时候会出现各种问题,比如死循环问题、数据覆盖等问题。而这些问题,只要使用ConcurrentHashMap就可以完美地解决。
-
特点
- 线程安全
- 在理想状态下,ConcurrentHashMap 可以支持 16 个线程执行并发写操作(如果并发级别设为16),及任意数量线程的读操作
- 主要是使用了 CAS 加 volatile 或者 synchronized 的方式来保证线程安全
- 底层是数组加链表加红黑树
- 当链表长度大于8,并且数组长度大于64时,链表就会升级为红黑树的结构
- 这样设计的好处是,使得锁的粒度相比Segment来说更小了,发生hash冲突 和 加锁的频率也降低了,在并发场景下的操作性能也提高了。而且,当数据量比较大的时候,查询性能也得到了很大的提升。
- 键不允许重复,值可以重复
- 元素无序
- 线程安全
-
常用方法
- put(key, value):添加一个键值对到集合中
- get(key):获取指定键所对应的值,没有返回null
- size():获取集合的大小
- clear():清空集合
- isEmpty():判断集合是否为空
- remove(key):删除集合中对应的键和它的值
- containsKey(key):判断集合中是否包含该键
- containsValue(value):判断集合中是否包含该值
-
参考博客,原理部分讲的非常详细:https://blog.csdn.net/gupaoedu_tom/article/details/124449788
TreeMap
- 特点
- 底层是二叉树
- 元素无序,不重复
- 元素的键将会按照特定要求排序
- 在使用TreeMap时其key必须实现Comparable接口或采用自定义的比较器,否则会抛出java.lang.ClassCastExption异常
- 常用方法
- put(key, value):添加一个键值对到集合中
- remove(key):删除集合中对应的键和它的值
- get(key):获取指定键所对应的值,没有返回null
- 特殊方法
- ceilingEntry(K key):获取大于等于key的最小值所对应的元素
- ceilingKey(K key):获取大于等于key的最小值所对应的键
- comparator():如果使用默认的比较器,就返回null,如果使用其他的比较器,就返回比较器的哈希码值
- descendingKeySet():获取整个集合的键的逆序排列
- descendingMap():逆序输出整个集合
- firstEntry():获取整个集合中最小的键所对应的元素
- firstKey():获取整个元素中最小的键
- floorEntry(K key):与ceilingEntry相反
- floorKey(K key):与ceilingKey相反
- headMap(K key):获取集合中所有小于key的元素
- higherEntry(K key):获取集合中所有大于key的元素
- lastEntry():与firstEntry相反
- lastKey():与firstKey相反
- pollFirstEntry():删除键最小的元素
- pollLastEntry():删除键最大的元素
- subMap(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive):截取集合中键从fromKey到toKey的元素,boolean表示是否截取它们本身
- tailMap(K key):截取集合中所有大于等于key的元素
5. 线程安全的集合
Vector
- 实现线程安全的原理是为其所有需要保证线程安全的方法都添加了synchronized关键字,锁住了整个对象
- 锁的种类:互斥锁
HashTable
- 与Vector类似,都是为每个方法添加了synchronized关键字,来实现的线程安全,锁住了整个对象
- 锁的种类:互斥锁
使用Collections包装成线程安全
-
使用Collections包装成线程安全,本质上是将原本的集合在执行之前加上了synchronized(){}的对象锁,将对象先锁定再来运行
-
锁的种类:互斥锁
-
包装集合List
List<String> list = Collections.synchronizedList(new ArrayList<>());
-
包装集合Set
Set<String> set = Collections.synchronizedSet(new HashSet<>());
-
包装集合Map
Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
CopyOnWriteArrayList和CopyOnWriteArraySet
-
CopyOnWriteArrayList和CopyOnWriteArraySet是Java并发包java.util.concurrent中的两个线程安全的集合类
-
CopyOnWriteArrayList
-
ArrayList的一个线程安全的变体,与ArrayList的使用方式一样
-
写入时,先copy一个容器副本,再添加新元素,最后替换引用
-
CopyOnWriteArrayList 的底层数组还被 volatile 关键字修饰,意思是一旦数组被修改,其它线程立马能够感知到
-
-
CopyOnWriteArraySet
-
CopyOnWriteArraySet则是基于CopyOnWriteArrayList实现的线程安全的Set集合
-
如果元素存在,则不添加,避免重复
-
ConcurrentHashMap(重点)
-
用法与HashMap一样
-
初始容量默认为16,不对整个Map加锁,而是为每个数组元素加锁,当多个对象存入同一个数组元素时,才需要互斥
-
锁的种类:CAS算法和同步锁
-
同步锁锁的是表头对象,拿到锁的对象要先做节点遍历,查看有没有相同的key
-
相同则覆盖
-
不同则挂在最后一个节点的next上(即在最后一个节点的链表最后面加上一个新的key),即尾插法
[!NOTE]
jdk1.8 中的 ConcurrentHashMap 中废弃了 Segment 锁,直接使用了数组元素,数组中的每个元素都可以作为一个锁。在元素中没有值的情况下,可以直接通过 CAS 算法来设值,同时保证并发安全;如果元素里面已经存在值的话,那么就使用 synchronized 关键字对元素加锁,再进行之后的 hash 冲突处理。jdk1.8 的 ConcurrentHashMap 加锁粒度比 jdk1.7 里的 Segment 来加锁粒度更细,并发性能更好。
-
ConcurrentLinkedQueue
- 线程安全、可高效的读写的队列,高并发下性能最好的队列
- 锁的种类:无锁、CAS算法
- 修改的方法包括三个核心参数
- V:要更新的变量
- E:预期值
- N:新值
- 当V=E时,V=N;否则表示已经被更新过,取消当前操作
- 修改的方法包括三个核心参数
6. HashMap(数组+链表/红黑树)原理
-
HashMap底层通过Entry数组来存储元素,插入元素和查询元素时首先需要计算元素在数组中的存储下标:
- 通过key的hashCode()方法获取其hashcode值
- 将这个hashcode值再通过hash()方法进行计算得到值h
- 最后将这个h值和length-1进行与运算得到最终的存储下标
-
但是由于数组长度有限,不同哈希值计算出的存储下标可能会相同,这就是哈希冲突,解决哈希冲突有:
- 开放定址法:遇到哈希冲突时,去寻找一个新的空闲的哈希地址
- 再哈希法:使用多个不同的哈希函数,等发生哈希冲突时就使用第二个、第三个…,直到不发生冲突为止
- 拉链法:将所有哈希计算地址相同的数据都链接在同一链表中
- 公共溢出区:将哈希表分为基本表和溢出表,将发生冲突的都存放在溢出表中
-
HashMap中是通过拉链法解决哈希冲突(即数组+链表),并采用尾插法将元素插入链表,当链表超过8时链表就要转换为红黑树,这时候要用到扩容机制
-
扩容机制:HashMap中默认初始容量为16,默认负载因子为0.75。这里的容量是指Entry数组的长度,不是HashMap中元素数的总数
[!IMPORTANT]
如果负载因子设计大了,则容易发生哈希冲突降低使用效率,如果设计小了,则不能充分利用空间,0.75这个值是通过数学中的泊松分布计算出来的,在“冲突的概率”与“空间利用率”之间可以达到一个最好的平衡折中
-
当HashMap中Entry数组已使用容量达到负载因子*容量后,会调用resize()方法自动进行扩容,将容量扩大为原来的2倍,并重新计算元素在数组中的位置,然后将元素复制到新数组中
[!IMPORTANT]
数组长度设计为2的次幂主要是为了优化取模运算,在最后计算存储下标时如果通过h % length取模来计算存储下标的话效率不高,如果数组长度为2的次幂,那么length-1的二进制的数值位就全为1,那么就可以像源码中那样通过与运算h & (length-1)来计算存储下标,效率更高
-
7. CAS算法
基础的算法问题,只给出参考,感兴趣可以自己学学
参考博客:https://blog.csdn.net/weixin_44936828/article/details/89525758
原理
CAS 涉及到三个操作数:
- V :要更新的变量值(Var)
- E :预期值(Expected)
- N :拟写入的新值(New)
当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了 V,则当前线程放弃更新。
8. Fail-Fast 和 Fail-Safe
Fail-Fast
-
什么是Fail-Fast
-
一种错误检测机制
-
在遍历一个集合时,当集合结构被修改,会抛出ConcurrentModificationException(并发修改异常)
-
-
Fail-Fast产生的原因
-
单线程环境
- 集合被创建后,在使用Iterator遍历它的过程中修改了集合结构
- 注意:Iterator的 remove()方法会让expectModcount和modcount 相等,所以是不会抛出这个异常
-
多线程环境
- 发生在多个线程对同一个集合进行操作的时候
- 即某个线程访问集合的过程中,该集合的内容被其他线程所改变(其它线程通过add、remove、clear等方法,改变了modCount的值),这时就会抛出ConcurrentModificationException异常,产生fail-fast事件
-
-
Fail-Fast的解决办法
- 使用java.util.concurrent包下面的ArrayList对应的CopyOnWriteArrayList
Fail-Safe
-
什么是Fail-Safe
- 与Fail-Fast相反,任何对集合结构的修改都会在一个复制的集合上进行修改,因此不会抛出ConcurrentModificationException
-
Fail-Safe的问题
-
需要复制集合,产生大量的无效对象,开销大
-
无法保证读取的数据是目前原始数据结构中的数据
-
9. Java数据去重
方法一
- 可以考虑使用Set集合,Set集合存储的元素都是无序不重复的
方法二
- 使用循环遍历加判断的方式去除重复的元素
方法三
-
使用Stream流中的distinct关键字的方式去重
// Employee 有三个字段: 姓名、年龄、收入 List<Employee> employeeList = Arrays.asList( new Employee("张三",18,9999.99), new Employee("李四",58,5555.55), new Employee("王五",26,3333.33), new Employee("赵六",36,6666.66), new Employee("田七",12,8888.88), new Employee("田七",12,8888.88) ); Stream<Employee> stream = employeeList.stream() .filter((e) -> e.getAge() > 35 ) .limit(2) .skip(2) .distinct();
10. Java数据分组
方式一
- 循环遍历加判断的方式进行分组并使用集合保存分组后的数据
方式二
-
使用Stream流中的Collectors.groupingBy方法进行分组
//分组 Map<Status,List<Employee>> map=employeeList.stream().collect(Collectors.groupingBy(Employee::getAge)); System.out.println(map); //多级分组 Map<Status,Map<String,List<Employee>>> map2=employeeList.stream().collect( Collectors.groupingBy(Employee::getStatus, Collectors.groupingBy((e)->{ if(e.getAge()<=35){ return "青年"; }else if(e.getAge()<=50){ return "中年"; }else{ return "老年"; } }) ) ); //分区,分成两组(满足条件和不满足条件) Map<Boolean,List<Employee>> map3 = employeeList.stream().collect(Collectors.partitioningBy((e)->e.getAge()>8000));
11. ArrayList和LinkedList的区别
对比
特性 | ArrayList | LinkedList |
---|---|---|
底层 | 数组 | 双向链表 |
效率 | 随机访问效率很高 | 增删效率很高 |
大小 | 固定大小 | 动态改变 |
开销 | 需要预留空间 | 需要存储节点信息和指针 |
12. TreeMap排序规则如何实现
其key实现Comparable接口
-
实现
Comparable
接口,重写compareTo()
方法 -
代码示例
class Student implements Comparable<Student>{ private String name; private int age; public Student() { } public Student(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override public int compareTo(Student o) { // 实现Comparable接口在这里也是自定义排序规则 // 如果什么也不写,直接默认return 0的话,只能存储第一个被put的元素 // 注意:升序就这么个写法,不要看网上其他的什么相等返回0,相等的话要返回this.age,否则会出问题 if(age > o.age){ return 1; }else if(age < o.age){ return -1; } return this.age; } }
采用自定义比较器
-
在TreeMap的构造函数中创建
new Comparator
匿名内部类,重写compare
方法 -
代码示例
public class StuTreeMap2 { public static void main(String[] args) { TreeMap<Student2,String> treeMap = new TreeMap<>(new Comparator<Student2>() { @Override public int compare(Student2 o1, Student2 o2) { // 基本和key实现Comparable接口,重写compareTo方法一致 // 升序排序就是这么写的 if(o1.getAge() > o2.getAge()){ return 1; }else if(o1.getAge() < o2.getAge()){ return -1; } // 相等的时候不能返回0 return o1.getAge(); } }); treeMap.put(new Student2("大哥",24),"大哥"); treeMap.put(new Student2("二哥",23),"二哥"); treeMap.put(new Student2("三哥",22),"三哥"); treeMap.put(new Student2("四哥",21),"四哥"); Set<Student2> studentSet = treeMap.keySet(); for (Student2 student : studentSet) { System.out.println(student.toString()); } } }
13. CopyOnWriteArrayList的使用
为什么
- 解决java.util.ConcurrentModificationException异常,即Fail-Fast问题
是什么
-
CopyOnWrite容器即写时复制的容器
-
当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素后,再将原容器的引用指向新的容器
-
CopyOnWriteArrayList 在对数组进行操作的时候,基本会分四步走:
- 加锁
- 从原数组中拷贝出新数组
- 在新数组上进行操作,并把新数组赋值给数组容器
- 解锁
-
除了加锁之外,CopyOnWriteArrayList 的底层数组还被 volatile 关键字修饰,意思是一旦数组被修改,其它线程立马能够感知到
使用场景
- CopyOnWrite并发容器用于读多写少的并发场景
14. LRU算法
概述
- Least Recently Used,最近最久未使用算法
- 该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问内以来锁经历的时间t;当淘汰一个页面时,选择现有页面中 t值最大的页面进行淘汰
- 参考博客
- https://blog.csdn.net/weixin_61440595/article/details/137973738
15. 比较器Comparator与Comparable的区别
对比
特性 | Comparator | Comparable |
---|---|---|
接口 | 函数式接口 | 普通接口 |
方法 | int compare(T o1, T o2) | int compareTo(T o) |
优先级 | 高 | 次之 |
耦合度 | 修改少 | 修改多 |
比较结果
- Comparator
- o1 - 要比较的第一个对象,o2 - 要比较的第二个对象
- 如果第一个参数大就返回正数,相等返回0,否则返回负数
- Comparable
- o - 要比较的对象
- 返回负整数、零或正整数,根据此对象是小于、等于还是大于指定对象
16. ConcurrentHashMap的size()方法是如何实现的
是什么
- ConcurrentHashMap 的 size() 方法用于返回当前映射中键值对的数量
怎么做
-
ConcurrentHashMap 采用分段(Segment)结构,每个段都有自己的计数器来跟踪该段中元素的数量。这样可以减少在获取大小时的锁竞争。
-
在 ConcurrentHashMap 中,size() 方法的实现通常包含以下步骤:
- 访问所有段:遍历所有段,获取每个段的大小。
- 累加段大小:将每个段的大小累加起来以获得总大小。
- 考虑并发情况:由于在获取大小的过程中可能有其他线程正在进行添加或删除操作,因此可能会有一定的误差(即返回值可能不是完全准确的),但通常会在可接受的范围内。
四、多线程
1. 进程和线程
- 一个进程可以有多个线程
- 线程是CPU调度和执行的最小单位
- 很多线程都是模拟出来的,真正的多线程是指有多个CPU,即多核,如服务器,如果是模拟出来的多线程,即一个CPU的情况下,在同一个时间点,CPU只能执行一个代码,因为切换很快,所以就有同时执行的错觉
- 使用多线程机制之后,main方法结束只是主线程结束了,其他线程还没结束,但没有主线程也不能运行
2. 线程创建
继承Thread类
-
通过创建一个新的类继承Thread类,并重写其run方法
-
代码示例
class MyThread extends Thread { @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println("数据:" + i); } } } public class Demo01 { public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); MyThread thread2 = new MyThread(); thread2.start(); for (int i = 10; i < 100; i++) { System.err.println("Main:" + i); } } }
-
优点
- 简单直观
-
缺点
- Java不支持多重继承,因此如果一个类已经继承了另一个类,那么它就不能再继承Thread类
实现Runnable接口
-
通过创建一个新的类实现Runnable接口,并重写其run方法
-
代码示例
class A implements Runnable { @Override public void run() { for (int i = 0; i < 1000; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); } } } public class Demo03 { public static void main(String[] args) { A a = new A(); new Thread(a).start(); new Thread(() -> { for (int i = 0; i < 1000; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); } }).start(); for (int i = 0; i < 1000; i++) { System.err.println(Thread.currentThread().getName() + ":" + i); } } }
-
优点
- 避免了Java单继承的限制,可以更容易地扩展功能
-
缺点
- 需要手动管理线程的生命周期
- 没有返回值
-
适用场景
- 简单的后台任务
实现Callable接口
-
通过创建一个新的类实现Callable接口,并重写其call方法
-
Callable可以有返回值并抛出异常
-
使用FutureTask来创建线程任务
-
代码示例
class MyCall implements Callable<Integer> { @Override public Integer call() throws Exception { return 200; } } public class Demo08 { public static void main(String[] args) throws ExecutionException, InterruptedException { // 使用FutureTask来创建线程任务 FutureTask<Integer> futureTask = new FutureTask<>(new MyCall()); new Thread(futureTask, "计算线程").start(); Integer i = futureTask.get(); System.out.println(i); } }
-
优点
- 可以有返回值和抛出异常
-
缺点
- 使用起来比Runnable复杂,需要配合FutureTask使用
-
适用场景
- 需要返回结果的任务
使用线程池
- 创建Java线程需要给线程分配堆栈内存以及初始化内存,还需要进行系统调用,频繁地创建和销毁线程会大大降低系统的运行效率,采用线程池来管理线程有以下好处:
- 提升性能:线程池能独立负责线程的创建、维护和分配
- 线程管理:每个Java线程池会保持一些基本的线程统计信息,对线程进行有效管理
- 适用场景
- 需要频繁地创建和销毁线程
3. 四种常见线程池
单线程化线程池
- 创建方式
- 使用
Executors.newSingleThreadExecutor()
创建
- 使用
- 特点
- 单线程化的线程池中的任务是按照提交的次序顺序执行的
- 只有一个线程的线程池
- 池中的唯一线程的存活时间是无限的
- 当池中的唯一线程正繁忙时,新提交的任务实例会进入内部的阻塞队列中,并且其阻塞队列是无界的
- 适用场景
- 任务按照提交次序,一个任务一个任务地逐个执行的场景
固定数量的线程池
- 创建方式
- 使用
Executors.newFixedThreadPool(3)
创建
- 使用
- 特点
- 如果线程数没有达到“固定数量”,每次提交一个任务线程池内就创建一个新线程,直到线程达到线程池固定的数量
- 线程池的大小一旦达到“固定数量”就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程
- 在接收异步任务的执行目标实例时,如果池中的所有线程均在繁忙状态,新任务会进入阻塞队列中,并且其阻塞队列是无界的
- 适用场景
- 需要任务长期执行的场景
- CPU密集型任务
可缓存线程池
- 创建方式
- 使用
Executors.newCachedThreadPool()
创建
- 使用
- 特点
- 在接收新的异步任务时,如果池内所有线程繁忙,此线程池就会添加新线程来处理任务
- 不会对线程池大小进行限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程数量
- 如果部分线程空闲,也就是存量线程的数量超过了处理任务数量,就会回收空闲的线程(60秒不执行任务)
- 适用场景
- 需要快速处理突发性强、耗时较短的任务场景,如Netty的NIO处理场景、REST API接口的瞬时削峰场景
可调度线程池
- 创建方式
- 使用
ScheduledExecutorService pool=Executors.newScheduledThreadPool(2)
创建 - 配合
pool.scheduleAtFixedRate(new Task(), 0, 500, TimeUnit.MILLISECONDS)
来指定执行周期- 参数1: 待执行任务
- 参数2: 首次执行任务的延迟时间
- 参数3: 周期性执行的时间
- 参数4: 时间单位
- 使用
- 特点
- 延时性,可延时执行线程
- 周期性,可周期性的执行线程
- 适用场景
- 定时任务或者需要周期执行的业务场景
4. 线程池中存在的问题
阻塞队列无界
- 使用无界队列来存放排队任务,当大量任务超过线程池最大容量需要处理时,队列无限增大,使服务器资源迅速耗尽
最大线程数不设上限
- 由于其maximumPoolSize的默认值为Integer.MAX_VALUE(2147483647,非常大),可以认为无限创建线程,如果任务提交较多,就会造成大量的线程被启动,很有可能造成OOM(Out Of Memory)异常,甚至导致CPU线程资源耗尽
5. 自定义线程池
代码结构
ExecutorService threadPool = new ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
)
参数解释
- corePoolSize:核心线程数,即使线程是空闲的,也不会被销毁的线程数
- maximumPoolSize:最大线程数,当任务较多时,线程池允许创建的最大线程数量
- keepAliveTime:非核心线程的空闲时间,超过该时间的空闲线程将被销毁
- unit:keepAliveTime 参数的时间单位
- workQueue:用于存放等待执行任务的队列,通常使用 BlockingQueue 的实现类,如
- LinkedBlockingQueue
- 无界队列,如果不指定大小,默认值是
Integer.MAX_VALUE
- 为了避免队列过大造成机器负载或者内存爆满的情况出现,我们在使用的时候建议手动传一个队列的大小
- 无界队列,如果不指定大小,默认值是
- SynchronousQueue
- 没有容量,是无缓冲等待队列,是一个不存储元素的阻塞队列,会直接将任务交给消费者(即丢给空闲的线程去执行),必须等队列中的添加元素被消费后才能继续添加新的元素,否则会走拒绝策略,所以使用SynchronousQueue阻塞队列一般要求maximumPoolSizes为无界,避免线程拒绝执行操作
- 插入元素到队列的线程被阻塞,直到另一个线程从队列中获取了队列中存储的元素。同样,如果线程尝试获取元素并且当前不存在任何元素,则该线程将被阻塞,直到线程将元素插入队列
- ArrayBlockingQueue
- 基于数组的有界阻塞队列,特点FIFO(先进先出)
- 当线程池中已经存在最大数量的线程时候,再请求新的任务,这时就会将任务加入工作队列的队尾,一旦有空闲线程,就会取出队头执行任务。因为是基于数组的有界阻塞队列,所以可以避免系统资源的耗尽
- LinkedBlockingQueue
- threadFactory:线程工厂,用于创建新线程,通常可以用来给线程设置名字、优先级等
- handler:拒绝策略,当任务过多且队列已满时,线程池如何处理新任务,可以选择 Java 提供的四种拒绝策略
- AbortPolicy(默认):直接抛出 RejectedExecutionException 异常
- CallerRunsPolicy:由调用线程(提交任务的线程)直接执行被拒绝的任务
- DiscardPolicy:直接丢弃任务,不抛出任何异常
- DiscardOldestPolicy:丢弃任务队列中最旧的任务,然后尝试提交新任务
向线程池提交任务
-
execute方法
-
execute()方法是Executor接口中定义的一个方法,用于提交一个任务用于执行。它是最基本的任务提交方式,适用于需要执行Runable类型的任务
-
方法签名
void execute(Runnable command);
-
-
submit方法
-
submit()方法是ExecutorService接口中定义的一个方法,它是execute()的增强版本。submit()不仅可以提交Runnable任务,还可以提交Callable任务,并且它会返回一个Future对象,代表任务的执行结果或状态
-
方法签名
<T> Future<T> submit(Callable<T> task); Future<?> submit(Runnable task); <T> Future<T> submit(Runnable task, T result);
-
代码示例
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,
2L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(3),
Executors.defaultThreadFactory(),
// new ThreadPoolExecutor.AbortPolicy()
// new ThreadPoolExecutor.CallerRunsPolicy()
// new ThreadPoolExecutor.DiscardOldestPolicy()
new ThreadPoolExecutor.DiscardOldestPolicy()
);
}
自定义线程池的使用建议
- 根据任务特性选择适当的任务队列:例如,使用 LinkedBlockingQueue 可以缓解任务堆积压力,而 SynchronousQueue 可以快速创建新线程处理任务,但可能会导致线程数量过多
- 合理配置线程数和队列大小:根据应用场景和服务器硬件资源,合理配置核心线程数、最大线程数和队列大小,避免过多的上下文切换或资源耗尽
- 优先选择内置的拒绝策略:如果内置的拒绝策略不能满足需求,可以自定义拒绝策略来更好地控制任务的拒绝行为
- 监控和调优:在生产环境中,建议监控线程池的使用情况(如线程数量、队列大小、拒绝任务数等),根据实际运行情况不断调整线程池的参数配置
6. execute和submit的区别
任务类型
- execute只能提交Runnable类型的任务
- submit既能提交Runnable类型任务也能提交Callable类型任务
异常
- execute会直接抛出任务执行时的异常,可以用try、catch来捕获,和普通线程的处理方式完全一致
- submit不抛出异常,可通过Future的get方法将任务执行时的异常重新抛出
返回值
- execute没有返回值
- submit有返回值,所以需要返回值的时候必须使用submit
注意
- 在ThreadPoolExecutor类的实现中,内部核心的任务提交方法是execute()方法,虽然用户程序通过submit()也可以提交任务,但是实际上submit()方法中最终调用的还是execute()方法
7. 线程池的工作流程
- 在创建了线程池后,线程池中的线程数为零
- 当调用execute()方法添加一个请求任务时,线程池会做出如下判断:
- 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务
- 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列
- 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务
- 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动拒绝策略
- 当一个线程完成任务时,它会从队列中取下一个任务来执行
- 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉
- 线程池的所有任务完成后,它最终会收缩到corePoolSize的大小
8. synchronized 关键字
同步实例方法
-
当一个实例方法使用 synchronized 修饰时,锁的是当前实例对象 this
public class Counter { private int count = 0; // 使用 synchronized 修饰实例方法,确保每次只有一个线程能调用此方法 public synchronized void increment() { count++; } public synchronized int getCount() { return count; } }
-
在上面的例子中,increment() 和 getCount() 方法是同步的,多个线程同时访问时,只有一个线程能进入这些方法,其他线程需要等待
同步静态方法
-
当一个静态方法使用 synchronized 修饰时,锁的是类的 Class 对象,而不是实例对象
public class Counter { private static int count = 0; // 使用 synchronized 修饰静态方法,锁的是 Counter.class public static synchronized void increment() { count++; } public static synchronized int getCount() { return count; } }
-
在这种情况下,所有线程都会被同一个类的锁(Counter.class)同步,所以即使是不同的实例,它们也会争抢同一个类的锁
同步代码块
-
如果你只想同步方法中的一部分代码,可以使用同步代码块,指定一个对象作为锁对象。通常,synchronized 用于局部同步,锁定更细粒度的资源
public class Counter { private int count = 0; public void increment() { synchronized (this) { // 锁定当前实例 count++; } } public int getCount() { synchronized (this) { // 锁定当前实例 return count; } } }
-
在这种情况下,synchronized 只会锁定方法体中的一部分,而不是整个方法,这样可以减少同步的开销
9. ReentrantLock(重入锁)
-
ReentrantLock 是 java.util.concurrent.locks 包中的一个类,提供了比 synchronized 更加灵活和强大的同步机制。它显式地获取和释放锁,相较于 synchronized 更加灵活,允许进行尝试锁定和中断锁定等操作
import java.util.concurrent.locks.ReentrantLock; public class Counter { private int count = 0; private final ReentrantLock reentrantLock = new ReentrantLock(); public void increment() { reentrantLock.lock(); // 获取锁 try { count++; } finally { reentrantLock.unlock(); // 确保释放锁 } } public int getCount() { reentrantLock.lock(); // 获取锁 try { return count; } finally { reentrantLock.unlock(); // 确保释放锁 } } }
10. ReadWriteLock(读写锁)
-
ReadWriteLock 是一种更为精细的锁,它允许多个线程同时读取共享资源,但在写操作时会阻塞所有的读线程和写线程
-
ReentrantReadWriteLock 是 ReadWriteLock 的常见实现
import java.util.concurrent.locks.ReentrantReadWriteLock; public class Counter { private int count = 0; private final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(); public void increment() { reentrantReadWriteLock.writeLock().lock(); // 获取写锁 try { count++; } finally { reentrantReadWriteLock.writeLock().unlock(); // 释放写锁 } } public int getCount() { reentrantReadWriteLock.readLock().lock(); // 获取读锁 try { return count; } finally { reentrantReadWriteLock.readLock().unlock(); // 释放读锁 } } }
11. volatile 关键字
-
内存可见性
- volatile 关键字确保变量的可见性。即,当一个线程修改了 volatile 变量的值,其他线程能够立即看到修改后的值。volatile 并不提供互斥的锁定,因此不能保证原子性,但它确保了数据的最新值被共享
-
屏蔽jvm指令重排序
- 指令重排序是编译器和处理器为了高效对程序进行优化的手段,它只能保证程序执行的结果是正确的,但是无法保证程序的操作顺序与代码顺序一致。这在单线程中不会构成问题,但是在多线程中就会出现问题
-
补充
- sychronized可以解决内存可见性,但是不能解决重排序问题
12. Atomic 类 (原子变量)
-
Java 提供了 java.util.concurrent.atomic 包中的一组类,如 AtomicInteger、AtomicLong、AtomicReference 等,它们通过 CAS(比较并交换)算法提供了线程安全的操作
import java.util.concurrent.atomic.AtomicInteger; public class Counter { private AtomicInteger count = new AtomicInteger(0); public void increment() { count.incrementAndGet(); // 原子性操作 } public int getCount() { return count.get(); // 获取原子值 } }
13. Semaphore (信号量)
-
Semaphore 是一个计数信号量,可以控制同时访问某个特定资源的线程数量。它在一定程度上提供了一种高级的同步机制,可以用于实现限流等功能
import java.util.concurrent.Semaphore; public class Resource { private final Semaphore semaphore = new Semaphore(1); // 只有一个许可 public void accessResource() throws InterruptedException { semaphore.acquire(); // 获取许可 try { // 访问共享资源 } finally { semaphore.release(); // 释放许可 } } }
14. CountDownLatch 和 CyclicBarrier
-
这两种类用于线程之间的协调,帮助实现线程的同步
- CountDownLatch:常用于一个线程等待其他线程完成后再继续执行
- CyclicBarrier:允许多个线程互相等待,直到所有线程都达到某个公共屏障点
import java.util.concurrent.CountDownLatch; public class Example { public static void main(String[] args) throws InterruptedException { CountDownLatch latch = new CountDownLatch(3); // 启动多个线程 for (int i = 0; i < 3; i++) { new Thread(() -> { System.out.println("Thread finished"); latch.countDown(); }).start(); } latch.await(); // 主线程等待 System.out.println("All threads finished"); } }
15. CompletableFuture
是什么
- CompletableFuture是Java 8中引入的一个异步编程工具类,用于进行非阻塞的异步编程。它是Future接口的扩展,提供了更灵活、更强大的功能
- CompletableFuture可以用于处理异步操作,例如网络请求、数据库查询等。与传统的线程和回调方法相比,CompletableFuture提供了更简洁和方便的编程模型
创建CompletableFuture
-
有三种方式可以创建
// 从一个供给函数创建 CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello"); // 从一个运行函数创建 CompletableFuture<Void> future = CompletableFuture.runAsync(() -> System.out.println("Hello")); // 从一个已有的结果创建 CompletableFuture<String> future = CompletableFuture.completedFuture("Hello");
链式调用
-
CompletableFuture支持链式调用,可以方便地对异步结果进行转换和组合
CompletableFuture<String> resultFuture = CompletableFuture.supplyAsync(() -> "Hello") .thenApply(s -> s + " World") // 对结果进行转换 .thenCompose(s -> getResult(s)); // 组合另一个异步操作
异常处理
-
通过exceptionally()方法可以对异常情况进行处理
String result = CompletableFuture.supplyAsync(() -> { throw new RuntimeException("error"); }).exceptionally(ex -> { // 处理异常 return "Default Value"; }).get();
组合多个CompletableFuture
-
通过allOf,anyOf这两种方式我们可以让任务之间协同工作,join()和get()方法都是阻塞调用它们的线程(通常为主线程)来获取CompletableFuture异步之后的返回值
- get() 方法会抛出经检查的异常,可被捕获,自定义处理或者直接抛出
- join() 会抛出未经检查的异常
// 等待所有任务完成 CompletableFuture.allOf(future1, future2, future3).get(); CompletableFuture.allOf(future1, future2, future3).join(); // 只要任意一个任务完成即可 CompletableFuture.anyOf(future1, future2, future3).get(); CompletableFuture.anyOf(future1, future2, future3).join(); // 规定超时时间,防止一直堵塞 CompletableFuture.allOf(future1, future2, future3).get(6, TimeUnit.SECONDS);
设置超时时间
-
可以通过下面的方式给某个CompletableFuture设置超时时间
String result = CompletableFuture.supplyAsync(() -> "Hello") .completeOnTimeout("Timeout!", 1, TimeUnit.SECONDS) .get();
参考博客
- https://blog.csdn.net/m0_47743175/article/details/131274291
- https://blog.csdn.net/qq_35716689/article/details/136868259
16. CountDownLatch/CyclicBarrier/CompletableFuture的区别
参考博客:https://blog.csdn.net/qq_40813329/article/details/125498494
17. 悲观锁和乐观锁
悲观锁
- 悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放
- 共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程
synchronized
和ReentrantLock
等独占锁就是悲观锁- 缺点
- 高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销
- 悲观锁还可能会存在死锁问题,影响代码的正常运行
- 适用场景
- 用于写比较多的情况下
- 数据一致性要求极高
- 事务执行时间较长
- 高并发且数据竞争激烈
乐观锁
- 乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证(具体方法可以使用版本号机制或 CAS 算法)对应的资源(也就是数据)是否被其它线程修改了
java.util.concurrent.atomic
包下面的原子变量类(比如AtomicInteger
、LongAdder
)就是乐观锁- 缺点
- 高并发的场景下,乐观锁相比悲观锁来说,不存在锁竞争造成线程阻塞,也不会有死锁的问题,在性能上往往会更胜一筹。但是,如果冲突频繁发生(写占比非常多的情况),会频繁失败和重试(悲观锁的开销是固定的),这样同样会非常影响性能,导致 CPU 消耗飙升
- ABA 问题
- 循环时间长开销大
- 只能保证一个共享变量的原子操作
- 适用场景
- 用于写比较少的情况下
- 数据冲突较少
- 系统能够容忍一定程度的失败
18. sleep和wait的区别
功能 | sleep | wait |
---|---|---|
语法 | 可以单独使用 | 需要配合synchronized一起使用 |
唤醒方式 | 通过传递超时参数,到时自动唤醒 | 必须调用notify或notifyAll方法才能唤醒 |
释放锁 | 不会主动释放锁 | 会主动释放锁 |
线程状态 | 进入TIMED_WAITING有时限等待状态 | 进入WAITING无时限等待状态 |
19. 多线程 Start()与 run()的区别
特性 | start方法 | run方法 |
---|---|---|
功能 | 启动一个新的线程 | 定义线程的具体执行任务 |
调用 | 一个线程只能调用一次,重复调用报错 | 可以多次调用该方法 |
使用 | 启动线程,并使其并发执行 | 直接执行任务,不会创建新线程 |
20. 线程池最佳核心线程数该如何确定
三步走
-
理论预估
-
首先需要确定我们系统的任务类型,大体上可以分为三类
-
IO密集型
-
比如读写文件、网络通信等
-
IO密集型线程池线程数预估
线程数 = 核心数 / ( 1 − 阻塞系数 ) 线程数 = 核心数 / (1 - 阻塞系数) 线程数=核心数/(1−阻塞系数)
一般情况下我们在设计阶段很难对我们的IO阻塞系统进行预估的,所以这里大家一般情况下都是粗估为CPU核数的2倍 + 1~2都可以,计算如下:
线程数 = C P U 核数 ∗ 2 + 2 线程数 = CPU核数*2 + 2 线程数=CPU核数∗2+2
-
-
CPU密集型
-
比如复杂的计算、数据处理等
-
对于CPU密集型任务,一个比较简单的公式是
线程数 = 核心数 + 1 线程数 = 核心数 + 1 线程数=核心数+1
-
-
混合型
-
既有IO操作又有计算任务
-
混合型线程池线程数预估, 参考下面的的公式:
最佳线程数 = ((线程等待时间 + 线程 C P U 时间) / 线程 C P U 时间) ∗ C P U 核数 最佳线程数 = ((线程等待时间 + 线程 CPU 时间) / 线程 CPU 时间 ) * CPU 核数 最佳线程数=((线程等待时间+线程CPU时间)/线程CPU时间)∗CPU核数
-
-
-
-
压测验证
- 按照上面的线程池理论预估,我们开发好了我们的程序,现在得看看这个数在实际中表现如何。这里就需要用到压测工具。压测就是模拟实际的使用情况,看看我们的系统能不能顶住。既然压测,那就肯定少不了压测工具,常见的压测工具有:
- JMeter:开源、功能强大、上手容易,是做压测的神器。
- Apache Bench (ab):轻量级工具,特别适合对单一URL进行高并发测试。
- Gatling:相对新一点,但非常强大,特别适合高并发场景。
- 按照上面的线程池理论预估,我们开发好了我们的程序,现在得看看这个数在实际中表现如何。这里就需要用到压测工具。压测就是模拟实际的使用情况,看看我们的系统能不能顶住。既然压测,那就肯定少不了压测工具,常见的压测工具有:
-
监控动态调整
-
测的场景,是有限的。而线上的业务, 是复杂的,多样的。由于系统运行过程中存在的不确定性,很难一劳永逸地规划一个合理的线程数。所以,需要进行生产阶段线程数的两个目标:
-
第一维度:可监控预警
-
第二维度:可在线调整
-
-
21. synchronized和ReentrantLock的区别
特征 | synchronized | ReentrantLock |
---|---|---|
用法 | 用于修饰普通方法、静态方法、代码块 | 只用于代码块 |
使用 | 直接在需要加锁地方使用synchronized关键字 | 要先创建 ReentrantLock 对象,然后使用 lock 方法进行加锁,使用完之后再调用 unlock 方法释放锁 |
释放锁 | 自动加锁、自动释放 | 手动加锁、手动释放 |
锁类型 | 非公平锁 | 可以是公平的也可以是不公平的 |
死锁 | 不能响应中断,会一直等待,会发生死锁 | 可以使用响应中断指令并释放锁,从而解决死锁问题 |
底层 | 在jvm层面通过监视器实现 | 基于AQS实现 |
22. Java内存模型(JMM)中的happens-before原则
是什么
- Java内存模型(JMM)是一种规范,用于定义多线程程序中,线程如何与主内存、工作内存以及其他线程之间进行通信和交互。
- happens-before是JMM中的一个重要概念,指的是一个操作的执行结果对后续操作可见
- 如果操作A操作B,操作B可以看到操作A的执行结果
有什么用
- 保证了多线程程序的可见性和有序性,它提供了一种可靠的方式来避免数据竞争和不确定性行为。
五、JVM
1. 垃圾回收机制
是什么
- 垃圾回收是一种自动的存储管理机制
- 当一些被占用的内存不再需要时,就应该予以释放,以让出空间,这种存储资源管理,称为垃圾回收。
什么是垃圾
- 如果一个对象已经没有任何地方引用他,就是垃圾
怎么确认垃圾
- 引用计数法
- 给对象添加一个引用计数器,每当一个地方引用他,计数器就加一,反之每当一个引用失效时,计数器就减一。当计数器为0,则表示对象不被引用。
- 可达性分析
- 设立若干根集合(GC Root),每个对象都是它的一个子节点,当一个对象找不到根时,就认为该对象不可达。
- 注意
- 在Java语言中,将引用又分为强引用、软引用、弱引用、虚引用4种,这四种引用强度依次逐渐减弱。
- 无论引用计数算法还是可达性分析算法都是基于强引用而言的
常见的垃圾收集算法
- 标记清除
- 从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收
- 由于标记清除算法直接回收不存活的对象,因此会造成内存碎片
- 复制算法
- 把堆分成一个对象面和多个空闲面,程序从对象面为对象分配空间,当对象面满了,就从根集合中扫描活动对象,并将每个活动对象复制到空闲面,这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。
- 为了克服句柄的开销和解决内存碎片的问题
- 标记整理
- 标记整理算法是在标记清除算法的基础上,又进行了对象的移动,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针
- 解决了内存碎片的问题
- 分代收集
- 不同对象的生命周期是不一样的。因此,把不同生命周期的对象存放在不同的代上,可以采取不同的收集方式,以便提高回收效率
- 老年代的特点是每次垃圾收集时只有少量对象需要被回收
- 新生代的特点是每次垃圾回收时都有大量的对象需要被回收
2. JVM的五个区间
-
JVM将内存划分为五个区间
- 方法区
- 栈
- Java虚拟机栈
- 本地方法栈
- 堆
- 程序计数器
-
JVM分为五大内存空间,程序计数器、虚拟机栈、本地方法栈3个区域随线程而生、随线程而灭,因此这几个区域的内存分配和回收都具备确定性,就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。而Java堆区和方法区则不一样,这部分内存的分配和回收是动态的,正是垃圾收集器所需关注的部分。
3. 垃圾回收器
是什么
- 目前常用的垃圾回收器有7个
- 按照工作的内存区间的不同,可以分为
- 新生代收集器
- 老年代收集器
- 按线程运行情况,可以分为
- 串行垃圾回收器
- 串行垃圾回收器一次只使用一个线程进行垃圾回收
- 并行垃圾回收器
- 并行垃圾回收器一次将开启多个线程同时进行垃圾回收
- 并发垃圾回收器
- 串行垃圾回收器
- 以及不分代收集器
- 按照工作的内存区间的不同,可以分为
六、Java 8 新特性
1. Lambda表达式
-
语法结构
// 格式一:无参数,无返回值 () -> System.out.println("Hello World") // 格式二:一个参数,无返回值 (x) -> System.out.println(x) // 格式三:两个以上参数 (x,y) -> { System.out.println("业务处理"); return x+y; } // 格式四 (int x,int y) -> Integer.compare(x,y);
-
重要特征
-
可选类型声明:不需要声明参数类型,编译器可以统一识别参数值
// 类型声明 MathOperation addition = (int a, int b) -> a + b; // 不用类型声明 MathOperation addition = (a, b) -> a + b;
-
可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号
// 用括号 GreetingService greetService = (message) -> System.out.println("Hello " + message); // 不用括号 GreetingService greetService = message -> System.out.println("Hello " + message);
-
可选的大括号:如果主体包含了一个语句,就不需要使用大括号
// 大括号中的返回语句 MathOperation multiplication = (int a, int b) -> { return a * b; }; // 没有大括号及返回语句 MathOperation multiplication = (int a, int b) -> a * b;
-
-
函数式接口
-
被
@FunctionalInterface
修饰,或者只有一个抽象方法的接口 -
因为默认方法不算抽象方法,所以你也可以给你的函数式接口添加默认方法
-
注意函数式接口不是新特性,但Lambda表达式可以认为是函数式接口的实例
-
若Lambda体中的内容有方法已经实现了,我们可以使用“方法引用”
-
-
方法引用的语法格式
// 对象::实例方法名 ClassName::method // 类::静态方法名 ClassName::new // 类::实例方法名 Type::new
2. Stream API
使用
-
创建Stream(stream()是创建串行流,parallelStream()是创建并行流)
default Stream<E> stream(); // 返回一个顺序流 default Stream<E> parallelStream(); // 返回一个并行流
//1.可以通过 Collection 系列集合提供的stream() 、parallelStream() List<String> list = new ArrayList<>(); Stream<String> stream1 = list.stream(); //2.通过 Arrays 中的静态方法stream()获取数组流 User[] emps=new User[5]; Stream<User> stream2=Arrays.stream(emps); //3.通过 Stream 类中的静态方法of() Stream<String> stream3=Stream.of("a","b","c"); //4.创建无限流 Stream<Integer> stream4=Stream.iterate(0, (x) -> x+2); stream4.limit(10).forEach(System.out::println);
-
中间操作(对数据进行操作)
-
筛选和切片
-
filter:过滤,从流中排除某些元素
-
limit(n):截断,使其元素不超过给定数量
-
skip(n):跳过元素,返回一个扔掉了前n个元素的流,若流中元素不足n个,则返回一个空流,与limit(n) 互补
-
distinct:去重,通过流所生成元素的 hashCode() 和 equals() 去掉重复元素
// Employee 有三个字段: 姓名、年龄、收入 List<Employee> employeeList = Arrays.asList( new Employee("张三",18,9999.99), new Employee("李四",58,5555.55), new Employee("王五",26,3333.33), new Employee("赵六",36,6666.66), new Employee("田七",12,8888.88), new Employee("田七",12,8888.88) ); Stream<Employee> stream = employeeList.stream() .filter((e) -> e.getAge() > 35 ) .limit(2) .skip(2) .distinct();
-
-
映射
-
map:接收Lambda,将元素转换成其他形式或提取信息,接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新元素
List<String> list = Arrays.asList("aaa","bbb","ccc","ddd"); list.stream().map((str)->str.toUpperCase()).forEach(System.out::println);
-
flatMap:接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流,map和flatMap的关系类似于add和addAll
-
-
排序
-
sorted():自然排序(按照对象类实现Comparable接口的compareTo()方法排序)
// 自定义排序 employeeList.stream().sorted((e1,e2)->{ if(e1.getAge().equals(e2.getAge())){ return e1.getName().compareTo(e2.getName()); }else{ return e1.getAge().compareTo(e2.getAge()); } }).forEach(System.out::println); // 正序排序 employeeList.stream().sorted(Comparator.comparing(Employee::getAge)).forEach(System.out::println); // 倒序排序 employeeList.stream().sorted(Comparator.comparing(Employee::getAge).reversed()).forEach(System.out::println);
-
-
-
终止操作(如果没有终止操作,中间操作是不执行的)
-
查找和匹配
-
allMatch:检查是否匹配所有元素
employeeList.stream().allMatch((e)-> e.getName().equals(”张三“));
-
anyMatch:检查是否至少匹配一个元素
employeeList.stream().anyMatch((e)-> e.getName().equals(”张三“));
-
noneMatch:检查是否没有匹配所有元素
-
findFirst:返回第一个元素
employeeList.stream().sorted((e1,e2)->Double.compare(e1.getAge(), e2.getAge())).findFirst();
-
findAny:返回当前流中的任意元素,注意是并行流
employeeList.parallelStream().findAny();
-
count:返回流中元素的总个数
employeeList.stream().count();
-
max:返回流中最大值
employeeList.stream().max((e1,e2)->Double.compare(e1.getSalary(), e2.getSalary()));
-
min:返回流中最小值
employeeList.stream().map(Employee::getSalary).min(Double::compare);
-
-
规约
-
reduce:可以将流中元素反复结合起来,得到一个值
List<Integer> list=Arrays.asList(1,2,3,4,5,6,7,8,9,10); Integer sum=list.stream().reduce(0, (x,y)->x+y); // 0为起始值
-
-
收集
-
collect:将流转换为其他形式,接收一个Collector接口的实现,用于给Stream中元素做汇总的方法
//stream流转List List<String> list = employeeList.stream().map(Employee::getName).collect(Collectors.toList()); //转Set Set<Employee> set = employeeList.stream().collect(Collectors.toSet()); //转HashSet HashSet<Employee> hs = employeeList.stream().collect(Collectors.toCollection(HashSet::new)); //总和 Long count=employeeList.stream().collect(Collectors.counting()); //平均值 Double avg=employeeList.stream().collect(Collectors.averagingDouble(Employee::getAge)); //总和 Double sum=employeeList.stream().collect(Collectors.summingDouble(Employee::getAge)); //最大值 Optional<Employee> max=employeeList.stream().collect(Collectors.maxBy((e1,e2)->Double.compare(e1.getAge(), e2.getAge()))); System.out.println(max.get()); //最小值 Optional<Double> min=employeeList.stream().map(Employee::getSalary).collect(Collectors.minBy(Double::compare)); //分组 Map<Status,List<Employee>> map=employeeList.stream().collect(Collectors.groupingBy(Employee::getAge)); System.out.println(map); //多级分组 Map<Status,Map<String,List<Employee>>> map2=employeeList.stream().collect( Collectors.groupingBy(Employee::getStatus, Collectors.groupingBy((e)->{ if(e.getAge()<=35){ return "青年"; }else if(e.getAge()<=50){ return "中年"; }else{ return "老年"; } }) ) ); //分区,分成两组(满足条件和不满足条件) Map<Boolean,List<Employee>> map3 = employeeList.stream().collect(Collectors.partitioningBy((e)->e.getAge()>8000)); //收集平均值、最大值、总和... DoubleSummaryStatistics des = employeeList.stream().collect(Collectors.summarizingDouble(Employee::getSalary)); System.out.println(dss.getSum()); System.out.println(dss.getAverage()); System.out.println(dss.getMax()); //连接字符串 String strr=employees.stream().map(Employee::getName).collect(Collectors.joining(","));
-
-
关于并行流和顺序流的相互切换
- Stream API 可以声明性地通过
parallel()
与sequential()
在并行流与顺序流之间进行切换
3. Optional类
- Optional< T>类(java.util.Optional) 是一个容器类,代表一个值存在或不存在
- 原来用null表示一个值不存在,现在 Optional可以更好的表达这个概念,并且可以避免空指针异常
- 常用方法
- Optional.of(T t):创建一个 Optional 实例
- Optional.empty():创建一个空的 Optional 实例
- Optional.ofNullable(T t):若 t 不为 null,创建 Optional 实例,否则创建空实例
- isPresent():判断是否包含值
- orElse(T t):如果调用对象包含值,返回该值,否则返回t
- orElseGet(Supplier s):如果调用对象包含值,返回该值,否则返回 s 获取的值
- map(Function f):如果有值对其处理,并返回处理后的Optional,否则返回Optional.empty()
- flatMap(Function mapper):与 map 类似,要求返回值必须是Optional
4. 接口的默认方法
-
接口可以有实现方法,而且不需要实现类去实现其方法,只需在方法名前面加个default关键字即可实现默认方法
-
代码结构
interface Demo{ default String getName(){ return "静态方法"; } }
5. 新的时间日期API
-
以前的时间API是线程不安全的
-
新的时间日期LocalDate、LocalTime、LocalDateTime 类的实例是不可变的对象,分别表示使用 ISO-8601日历系统的日期、时间、日期和时间
-
全新的API在time包下,他们都是不可变的,线程安全的
-
使用
//当前时间 LocalDateTime now = LocalDateTime.now(); System.out.println(now); //设置时间 LocalDateTime of = LocalDateTime.of(2018, 2, 22, 2, 22, 54); System.out.println(of); //加上2年 LocalDateTime of2 = of.plusYears(2); System.out.println(of2); //减上2年 LocalDateTime of3 = of.minusYears(2); System.out.println(of3); //获取 年 月 日 System.out.println(of.getYear()); System.out.println(of.getMonthValue()); System.out.println(of.getDayOfMonth()); //获取秒数时间戳(10位) long l = LocalDateTime.now().toEpochSecond(ZoneOffset.of("+8")); System.out.println("获取秒数时间戳:" +l); //获取毫秒数时间戳(13位) long l1 = LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli(); System.out.println("获取毫秒数时间戳:" +l1); //时间比较 Instant i1 = Instant.now(); // 获取一个时间 TimeUnit.SECONDS.sleep(1); Instant i2 = Instant.now(); // 延时一秒再获取一个时间 long l = Duration.between(i1, i2).toMillis(); // 对比两个时间 System.out.println(l); //日期比较 LocalDate ld = LocalDate.of(2018,12,11); // 指定一个日期 TimeUnit.SECONDS.sleep(1); LocalDate ld2 = LocalDate.now(); // 延时一秒再获取一个日期 Period between = Period.between(ld, ld2); // 对比两个日期 System.out.println(between.getYears()); System.out.println(between.getMonths()); System.out.println(between.getDays()); //格式化 LocalDateTime l = LocalDateTime.now(); DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss"); //时间转字符串 String format = dtf.format(l); System.out.println(format); //字符串转时间 LocalDateTime parse = l.parse(format,dtf); System.out.println(parse); //LocalDate 转为 Date LocalDate localDate = LocalDate.now(); Instant instant = localDate.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant(); Date date = Date.from(instant); System.out.println("LocalDate 转为 Date: " + date); //Date 转为 LocalDateTime Date date3 = new Date(); LocalDateTime localDateTime3 = LocalDateTime.ofInstant(date3.toInstant(), ZoneId.systemDefault()); System.out.println("Date 转为 LocalDateTime: " + date3); //Date 转为 LocalDate Date date4 = new Date(); LocalDateTime localDateTime4 = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()); LocalDate localDate4 = localDateTime4.toLocalDate(); System.out.println("Date 转为 LocalDate: " + localDate4);
-
TemporalAdjuster(时间调节器)
-
可以执行复杂的日期操作,例如,可以获得下一个星期日的日期、当月的最后一天(再也不用计算当月是28,29还是30天了)、下一年的第一天、下一个工作日等等
//以2021-11-30为例 LocalDate now = LocalDate.now(); System.out.println("当前时间:"+now); //获取当月第一天 System.out.println("当月第一天:"+now.with(TemporalAdjusters.firstDayOfMonth()));// 2021-11-01 //获取本月第2天: System.out.println("本月第2天:"+now.withDayOfMonth(2)); //2021-11-02 //获取下月第一天 System.out.println("下月第一天:"+now.with(TemporalAdjusters.firstDayOfNextMonth())); //2021-12-01 //获取明年第一天 System.out.println("明年第一天:"+now.with(TemporalAdjusters.firstDayOfNextYear())); //2022-01-01 //获取本年第一天 System.out.println("本年第一天:"+now.with(TemporalAdjusters.firstDayOfYear()));//2021-01-01 //获取当月最后一天,再也不用计算是28,29,30还是31: System.out.println("当月最后一天:"+now.with(TemporalAdjusters.lastDayOfMonth())); //2021-11-30 //获取本年最后一天 System.out.println("本年最后一天:"+now.with(TemporalAdjusters.lastDayOfYear())); //2021-12-31 //获取当月的第一个星期一 System.out.println("当月的第一个星期一:"+now.with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY))); //2021-11-01 //获取当月的最后一个星期一 System.out.println("当月的最后一个星期一:"+now.with(TemporalAdjusters.lastInMonth(DayOfWeek.MONDAY))); //2021-11-29 //获取当月第三周星期五 System.out.println("当月第三周星期五:"+now.with(TemporalAdjusters.dayOfWeekInMonth(3, DayOfWeek.FRIDAY))); //2021-11-19 //获取本周一 System.out.println("本周一:"+now.with(DayOfWeek.MONDAY)); //2021-11-29 //获取上周二 System.out.println("上周二:"+now.minusWeeks(1).with(ChronoField.DAY_OF_WEEK, 2)); //2021-11-23 //(往前不包括当天)获取当前日期的上一个周一 如果今天是周一,则返回上周一 System.out.println("上一个周一(不包括当天):"+now.with(TemporalAdjusters.previous(DayOfWeek.MONDAY))); //2021-11-29 //(往前包括当天)最近星期五的日期 如果今天是星期五,则返回今天日期 System.out.println("上一个周一(包括当天):"+now.with(TemporalAdjusters.previousOrSame(DayOfWeek.FRIDAY))); //2021-11-26 //获取下周二 System.out.println("下周二:"+now.plusWeeks(1).with(ChronoField.DAY_OF_WEEK, 2)); //2021-12-07 //(往后不包括当天)获取当前日期的下一个周日 如果今天是周日,则返回下周日的时间 如果今天是星期一,则返回本周日的时间 System.out.println("下一个周日(不包括当天):"+now.with(TemporalAdjusters.next(DayOfWeek.SUNDAY))); //2021-12-05 //(往后包括当天)最近星期五的日期 如果今天是星期五,则返回今天日期 System.out.println("下一个周五(包括当天):"+now.with(TemporalAdjusters.nextOrSame(DayOfWeek.FRIDAY))); //2021-12-03
-
七、Java 17 新特性
未完待续
框架与中间件
一、Spring
1. Spring/SpringBoot/SpringCloud
- Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器(框架)
- Spring Boot 是 Spring 的一套快速配置脚手架,可以基于Spring Boot 快速开发单个微服务
- Spring Cloud是基于Spring Boot实现的,Spring Cloud很大的一部分是基于Spring Boot来实现,Spring Boot可以离开Spring Cloud独立使用开发项目,但是Spring Cloud离不开Spring Boot,属于依赖的关系
2. 控制反转(IOC)
概述
- 控制反转IoC(Inversion of Control),是一种设计思想,DI(Dependency Injection,依赖注入)是实现IoC的一种方法
- 控制反转是一种通过描述(XML或注解)并通过第三方去生产或获取特定对象的方式,在Spring中实现控制反转的是IoC容器,其实现方法是依赖注入(Dependency Injection,DI)
- IoC是Spring框架的核心内容,使用多种方式完美的实现了IoC,可以使用XML配置,也可以使用注解
- 采用XML方式配置Bean的时候,Bean的定义信息是和实现分离的
- 采用注解的方式可以把两者合为一体,Bean的定义信息直接以注解的形式定义在实现类中,从而达到了零配置的目的
- 控制:谁来控制对象的创建 , 传统应用程序的对象是由程序本身控制创建的 , 使用Spring后 , 对象是由Spring来创建的
- 反转:程序本身不创建对象 , 而变成被动的接收对象
- 依赖注入:就是利用构造器或set方法来进行注入的
3. @Autowired原理
工作原理
- 容器启动:当 Spring 应用上下文启动时,refresh() 方法会被调用,在这个方法中会注册AutowiredAnnotationBeanPostProcessor
- Bean 实例化:当需要创建一个新的 Bean 实例时,AbstractAutowireCapableBeanFactory.doCreateBean() 方法被调用,这是 Bean 创建的核心流程
- 属性填充:在 doCreateBean() 方法中,populateBean() 方法负责为 Bean 填充属性值。在这个过程中,applyMergedBeanDefinitionPostProcessors() 方法会调用所有注册过的 MergedBeanDefinitionPostProcessor,包括 AutowiredAnnotationBeanPostProcessor
- 依赖解析:AutowiredAnnotationBeanPostProcessor 会寻找标注了 @Autowired 的字段、构造函数或方法,并尝试根据类型或者名称找到相应的 Bean 进行依赖注入。如果找不到匹配的 Bean 或者有多个匹配项且没有指定具体哪一个,那么将会抛出异常
- 依赖注入:一旦找到了合适的 Bean,AutowiredAnnotationBeanPostProcessor 就会使用反射机制为这些字段、构造函数参数或方法参数设置对应的依赖
使用场景
-
字段注入
-
字段注入是最简单的注入方式。你只需要在需要注入依赖的字段上添加
@Autowired
注解即可。Spring 容器会在启动时自动查找匹配类型的 Bean 并进行注入import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class UserService { @Autowired private UserRepository userRepository; // 直接在字段上标注@Autowired public void createUser(User user) { userRepository.save(user); } }
-
在这个例子中,
UserRepository
是一个接口,它被 Spring 容器管理并且实现了具体的操作(比如保存用户信息)。UserService
类中的userRepository
字段将会由 Spring 自动注入
-
-
构造函数注入
-
构造函数注入是推荐的方式,特别是对于必填的依赖。这种方式可以确保依赖对象在实例化时就存在,并且有助于不可变对象的创建和测试
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class UserService { private final UserRepository userRepository; // 使用构造函数进行依赖注入 @Autowired public UserService(UserRepository userRepository) { this.userRepository = userRepository; } public void createUser(User user) { userRepository.save(user); } }
-
这里,
UserService
的构造函数接收UserRepository
作为参数,通过@Autowired
注解告诉 Spring 这是一个需要注入依赖的构造函数
-
-
Setter方法注入
-
Setter 方法注入允许在 Bean 初始化后更改依赖。这通常用于可选依赖或在运行时可能变化的依赖
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class UserService { private UserRepository userRepository; // 使用Setter方法进行依赖注入 @Autowired public void setUserRepository(UserRepository userRepository) { this.userRepository = userRepository; } public void createUser(User user) { userRepository.save(user); } }
-
在这个例子中,
setUserRepository
方法被标记为@Autowired
,Spring 将会调用这个方法来设置userRepository
属性
-
-
任意命名的方法注入
-
除了 setter 方法外,你可以使用
@Autowired
来标注任何方法,只要这些方法有合适的参数类型,Spring 就会尝试注入相应的依赖import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class UserService { private UserRepository userRepository; // 可以是任意命名的方法 @Autowired public void initialize(UserRepository userRepository) { this.userRepository = userRepository; } public void createUser(User user) { userRepository.save(user); } }
-
-
数组、集合和映射注入
-
你可以将多个相同类型的 Bean 注入到数组、
Set
或Map
中。这对于需要处理一组服务或者组件的情况非常有用import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class NotificationService { private final List<NotificationChannel> notificationChannels; // 构造函数注入列表 @Autowired public NotificationService(List<NotificationChannel> notificationChannels) { this.notificationChannels = notificationChannels; } public void sendNotifications(String message) { for (NotificationChannel channel : notificationChannels) { channel.send(message); } } } // 假设有两个实现了 NotificationChannel 接口的类 EmailChannel 和 SmsChannel @Component public class EmailChannel implements NotificationChannel { @Override public void send(String message) { System.out.println("Sending email: " + message); } } @Component public class SmsChannel implements NotificationChannel { @Override public void send(String message) { System.out.println("Sending SMS: " + message); } }
-
在这个例子中,
NotificationService
将所有实现了NotificationChannel
接口的 Bean 收集到一个List
中,并遍历它们来发送通知
-
处理多个相同类型的Bean
-
当有多个相同类型的 Bean 存在时,可以通过
@Qualifier
注解指定要注入的具体 Beanimport org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; @Service public class PaymentService { private final PaymentGateway paymentGateway; // 使用@Qualifier指定要注入的具体Bean @Autowired public PaymentService(@Qualifier("paypalGateway") PaymentGateway paymentGateway) { this.paymentGateway = paymentGateway; } public void processPayment(double amount) { paymentGateway.charge(amount); } } // 假设有两个实现了 PaymentGateway 接口的类 PaypalGateway 和 StripeGateway @Component("paypalGateway") public class PaypalGateway implements PaymentGateway { @Override public void charge(double amount) { System.out.println("Charging via PayPal: " + amount); } } @Component("stripeGateway") public class StripeGateway implements PaymentGateway { @Override public void charge(double amount) { System.out.println("Charging via Stripe: " + amount); } }
-
在这个例子中,
PaymentService
有两个可以选择的PaymentGateway
实现,但通过@Qualifier("paypalGateway")
指定了使用PaypalGateway
注意事项
- @Autowired 默认是必须的(required = true),这意味着如果没有找到匹配的 Bean,Spring 会抛出异常。可以通过设置 required = false 来允许可选依赖
- 当存在多个相同类型的 Bean 时,可以通过 @Qualifier 注解来指定要注入的具体 Bean
- 在某些情况下,可能需要结合使用 @Primary 注解来指定默认的 Bean
- 对于循环依赖的问题,Spring 提供了多种解决方案,但通常建议尽量避免这种情况的发生
4. @Autowired和@Resource
参考
- 这个博客写的非常详细:https://blog.csdn.net/weixin_41821642/article/details/130106055,我这里只做面试总结
异同
功能 | @Autowired | @Resource |
---|---|---|
支持 | 由Spring 框架提供的注解 | JDK1.6支持的注解,由J2EE提供 |
匹配 | 按照type进行匹配 | 默认按照name进行匹配,找不到则按照type匹配 |
适用对象 | 构造器、方法、参数、字段 | 方法、字段 |
注意匹配顺序
- @Autowired
- 如果有多个bean,则按照
name
进行匹配- 如果有
@Qualifier
注解,则按照@Qualifier
指定的name
进行匹配 - 如果没有,则按照变量名进行匹配
- 如果有
- 如果有多个bean,则按照
- @Resource
- 如果没有指定name属性,当注解写在字段上时,默认取字段名,按照名称查找
- 当注解标注在属性的setter方法上,即默认取属性名作为bean名称寻找依赖对象
- 当找不到与名称匹配的bean时才按照类型进行装配,但是需要注意的是,如果name属性一旦指定,就只会按照名称进行装配
5. 动态代理
参考
参考博客写的非常详细:
https://blog.csdn.net/qq_36756227/article/details/131146734,
https://blog.csdn.net/m0_45067620/article/details/136176227
这里只做摘要和总结。
JDK动态代理原理
- 创建实现InvocationHandler接口的代理类工厂:在调用Proxy类的静态方法newProxyInstance时,会动态生成一个代理类。该代理类实现了目标接口,并且持有一个InvocationHandler类型的引用
- InvocationHandler接口:InvocationHandler是一个接口,它只有一个方法invoke。在代理对象的方法被调用时,JVM会自动调用代理类的invoke方法,并将被调用的方法名、参数等信息传递给该方法
- 调用代理对象的方法:当代理对象的方法被调用时,JVM会自动调用代理类的invoke方法。在invoke方法中,可以根据需要执行各种逻辑,比如添加日志、性能统计、事务管理等
- invoke方法调用:在invoke方法中,通过反射机制调用目标对象的方法,并返回方法的返回值。在调用目标对象的方法前后,可以执行额外的逻辑
CGlib 动态代理原理
- 创建Enhancer对象:Enhancer是CGLIB库中用于动态生成子类的主要类。通过创建Enhancer对象并设置需要代理的目标类、拦截器等参数,可以生成一个代理类
- 设置回调拦截器:在生成代理类时,需要指定拦截器。拦截器是实现代理逻辑的关键,它会在代理类的方法被调用时拦截调用,并执行相应的逻辑。在CGLIB中,拦截器需要实现MethodInterceptor接口
- 创建代理对象:通过调用Enhancer对象的create方法,可以生成一个代理对象。代理对象会继承目标类的方法,并且在调用代理对象的方法时会先调用拦截器的intercept方法,再执行目标方法
- 调用代理对象:通过调用代理对象的方法,会触发拦截器的intercept方法。在intercept方法中,可以根据需要执行各种逻辑,比如添加日志、性能统计、事务管理等
对比
功能 | JDK动态代理 | CGlib动态代理 |
---|---|---|
支持 | 基于Java标准库 | 基于ASM |
实现原理 | 在运行时动态生成一个代理对象,代理对象实现和原始类一样的接口,并将方法调用转发给被代理对象 | 在运行时动态生成目标类的子类作为代理类,并覆盖其中的方法来实现代理功能 |
机制 | 通过反射机制实现 | 通过方法拦截实现 |
补充 | 不能代理没有实现接口的类 | 可以代理没有实现接口的类 |
基于接口 | 基于继承 |
- 一般情况下,JDK动态代理的性能要略优于CGLIB动态代理,因为JDK动态代理是基于接口的代理,而CGLIB动态代理是基于继承的代理,生成代理类的过程更为复杂
6. 面向切面编程(AOP)
概述
- 面向切面编程是一种通过预编译方式和运行期动态代理实现程序功能统一维护的一种技术
- AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型
- 优点
- 利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低
- 提高程序的可重用性
- 同时提高了开发的效率
- AOP技术是建立在Java语言的反射机制与动态代理机制之上的
- 实现原理
- 务逻辑组件在运行过程中,AOP容器会动态创建一个代理对象供使用者调用,该代理对象已经按Java EE程序员的意图将切面成功切入到目标方法的连接点上,从而使切面的功能与业务逻辑的功能同时得以执行。从原理上讲,调用者直接调用的其实是AOP容器动态生成的代理对象,再由代理对象调用目标对象完成原始的业务逻辑处理,而代理对象则已经将切面与业务逻辑方法进行了合成
基本概念
- 切面(Aspect):其实就是共有功能的实现。如日志切面、权限切面、事务切面等。在实际应用中通常是一个存放共有功能实现的普通Java类,之所以能被AOP容器识别成切面,是在配置中指定的。
- 通知(Advice):是切面的具体实现。以目标方法为参照点,根据放置的地方不同,可分为前置通知(Before)、后置通知(AfterReturning)、异常通知(AfterThrowing)、最终通知(After)与环绕通知(Around)5种。在实际应用中通常是切面类中的一个方法,具体属于哪类通知,同样是在配置中指定的。
- 连接点(Joinpoint):就是程序在运行过程中能够插入切面的地点。例如,方法调用、异常抛出或字段修改等,但Spring只支持方法级的连接点。
- 切入点(Pointcut):用于定义通知应该切入到哪些连接点上。不同的通知通常需要切入到不同的连接点上,这种精准的匹配是由切入点的正则表达式来定义的。
- 目标对象(Target):就是那些即将切入切面的对象,也就是那些被通知的对象。这些对象中已经只剩下干干净净的核心业务逻辑代码了,所有的共有功能代码等待AOP容器的切入。
- 代理对象(Proxy):将通知应用到目标对象之后被动态创建的对象。可以简单地理解为,代理对象的功能等于目标对象的核心业务逻辑功能加上共有功能。代理对象对于使用者而言是透明的,是程序运行过程中的产物。
- 织入(Weaving):将切面应用到目标对象从而创建一个新的代理对象的过程。这个过程可以发生在编译期、类装载期及运行期,当然不同的发生点有着不同的前提条件。譬如发生在编译期的话,就要求有一个支持这种AOP实现的特殊编译器;发生在类装载期,就要求有一个支持AOP实现的特殊类装载器;只有发生在运行期,则可直接通过Java语言的反射机制与动态代理机制来动态实现
在SpringBoot中使用
-
依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
-
核心注解
- @Aspect 用在类上,代表这个类是一个切面
- @Before 用在方法上,代表这个方法是一个前置通知方法
- @After 用在方法上,代表这个方法是一个后置通知方法
- @Around 用在方法上,代表这个方法是一个环绕的方法
-
代码格式
/** * 自定义切面配置类 */ @Configuration //代表这个类是spring的配置类 @Aspect //代表这个类是切面配置类 public class MyAspectConfig { //切面Aspect=Advice附加操作+Pointcut切入点 @Before("execution(* com.demo.springbootaoptest.service.*.*(..))")//代表这是一个核心业务逻辑执行之前的附加操作,value属性表明切入点 public void before(JoinPoint joinPoint){ System.out.println("=========前置附加操作========="); System.out.println("当前执行目标类: "+ joinPoint.getTarget()); System.out.println("当前执行目标类中方法: "+ joinPoint.getSignature().getName()); } @After("execution(* com.demo.springbootaoptest.service.*.*(..))") public void after(){ System.out.println("后置通知"); } @Around("execution(* com.demo.springbootaoptest.service.*.*(..))") public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { System.out.println("==========进入环绕的前置操作==========="); System.out.println("当前执行类: "+proceedingJoinPoint.getTarget()); System.out.println("方法名: "+proceedingJoinPoint.getSignature().getName()); //放入目标方法执行 Object proceed = proceedingJoinPoint.proceed();//继续处理 业务逻辑方法执行 System.out.println("==========进入环绕的后置操作==========="); return null; } }
- “execution() ”方法的参数解释
- “*****” 表示匹配任意类型的方法
- com.demo.springbootaoptest.service 表示匹配 com.demo.springbootaoptest.service 包下的所有类
- .* 表示匹配所有类的所有方法
- (…) 表示匹配方法的任意参数
- “execution() ”方法的参数解释
-
切点表达式
-
execution(方法修饰符 返回类型 方法所属的包.类名.方法名称(方法参数))
-
匹配特定类中的方法
execution(* com.example.MyClass.myMethod(..))
- 这个表达式匹配
com.example
包中MyClass
类里的myMethod
方法,*
表示任何修饰符,..
表示任何参数
- 这个表达式匹配
-
匹配任何返回类型的方法
execution(* *.methodName(..))
- 这里第一个
*
代表任何返回类型,第二个*
代表任何类,methodName
是你想要匹配的方法名
- 这里第一个
-
匹配特定包及子包下所有类的方法
execution(* com.example.*.*(..))
- 这个表达式匹配
com.example
包及其所有子包中所有类的方法
- 这个表达式匹配
-
匹配特定修饰符的方法
execution(public * com.example.MyClass.myMethod(..))
- 这个表达式仅匹配公开(
public
)的myMethod
方法
- 这个表达式仅匹配公开(
-
匹配特定参数类型的方法
execution(* com.example.MyClass.myMethod(java.lang.String, int))
- 这个表达式匹配
MyClass
中接受String
和int
作为参数的myMethod
方法
- 这个表达式匹配
-
匹配继承自特定类的类中的方法
execution(* com.example.*+.myMethod(..))
+
表示匹配继承自com.example
包中任何类的myMethod
方法
-
匹配注解了特定注解的方法
execution(@com.example.MyAnnotation * *(..))
- 这个表达式匹配任何被
@com.example.MyAnnotation
注解的方法
- 这个表达式匹配任何被
-
匹配特定异常类型的抛出
execution(* *.*(..) throws java.lang.Exception)
- 这个表达式匹配任何可能抛出
java.lang.Exception
或其子类异常的方法
- 这个表达式匹配任何可能抛出
-
组合使用多个条件
execution(public * com.example.service.*.*(..))
- 这个表达式匹配
com.example.service
包中公开的任何类的方法
- 这个表达式匹配
-
7. Bean的生命周期
概述
- Spring其实就是一个管理Bean对象的工厂,它负责对象的创建,对象的销毁等
- 所谓的生命周期就是:对象从创建开始到最终销毁的整个过程
五步分析法
- Bean生命周期可以粗略的划分为五大步:
- 第一步:实例化Bean(调用无参数构造方法)
- 第二步:Bean属性赋值(调用set方法)
- 第三步:初始化Bean(会调用Bean的init方法。注意:这个init方法需要自己写)
- 第四步:使用Bean
- 第五步:销毁Bean(会调用Bean的destroy方法。注意:这个destroy方法需要自己写)
七步分析法
-
Bean生命周期可以细化为七大步:
- 第一步:实例化Bean
- 第二步:Bean属性赋值
- 第三步:执行“Bean后处理器”的before方法
- 第四步:初始化Bean
- 第五步:执行“Bean后处理器”的after方法
- 第六步:使用Bean
- 第七步:销毁Bean
8. Spring事务管理
事务
- 事务的属性(ACID)
- 原子性(Atomicity):事务是一个原子操作,由一系列动作组成。事务的原子性确保动作要么全部完成,要么完全不起作用
- 一致性(Consistency):一旦事务完成(不管成功还是失败),系统必须确保它所建模的业务处于一致的状态,而不会是部分完成部分失败
- 隔离性(Isolation):可能有许多事务会同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏
- 持久性(Durability):一旦事务完成,无论发生什么系统错误,它的结果都不应该受到影响,这样就能从任何系统崩溃中恢复过来。通常情况下,事务的结果被写到持久化存储器中
概述
- 在Spring框架当中就已经把事务控制的代码都已经封装好了,并不需要我们手动实现
- 只需要通过一个简单的注解
@Transactional
就可以完成事务的控制
@Transactional
- 在当前方法执行开始之前来开启事务,方法执行完毕后来提交事务,如果该方法在执行的过程中出现了异常,Spring会自动地进行事务的回滚操作
- @Transactional注解一般会在业务层当中来控制事务,因为在业务层当中一个业务功能可能包含多个数据访问的操作,在业务层来控制事务,我们就可以将多个数据访问操作控制在一个事务范围内
- @Transactional注解不仅可以作用在方法上,也可以作用在类和接口上
- 如果作用在方法上,代表当前方法交给Spring进行事务管理
- 如果作用在类上,代表当前类当中所有的方法都交由Spring进行事务管理
- 如果是直接作用在接口上,就代表这个接口下所有的实现类当中所有的方法都交给Spring进行事务管理
日志
-
可以在application.yml配置文件中开启事务管理日志,这样就可以在控制台看到和事务相 关的日志信息了
#开启Spring进行事务管理的日志开关{spring事务管理日志} logging: level: org.springframework.jdbc.support.JdbcTransactionManager: debug
rollbackFor异常回滚的属性
- 默认情况下,只有出现RuntimeException(运行时异常)才会回滚事务
- 假如我们想让所有的异常都回滚,需要来配置@Transactional注解当中的rollbackFor属性,通过rollbackFor这个属性可以指定出现何种异常类型回滚事务
propagation事务的传播行为
- 事务的传播行为关注于当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制
- 事务传播行为定义了在方法调用链中嵌套事务的创建和使用方式
- 要想控制事务的传播行为,在@Transactional注解的后面指定一个属性propagation,通过 propagation 属性来指定传播行为
- 事务的传播行为一共有七种,但是我们只需要关注其中的两种:
- REQUIRED(默认值):需要事务,有则加入,无则创建新事务
- REQUIRES_NEW:需要新事务,无论有无,总是创建新事务
- 当我们希望多个嵌套的事务(比如一个事务方法里面调用了另外一个事务方法)之间互相不影响时,可以使用这个属性值
9. Spring事务的隔离级别
隔离级别 | 含义 |
---|---|
ISOLATION_DEFAULT | 使用后端数据库默认的隔离级别 |
ISOLATION_READ_UNCOMMITTED | 允许读取尚未提交的更改。可能导致脏读、幻读或不可重复读 |
ISOLATION_READ_COMMITTED | (Oracle 默认级别)允许从已经提交的并发事务读取。可防止脏读,但幻读和不可重复读仍可能会发生 |
ISOLATION_REPEATABLE_READ | (MYSQL默认级别)对相同字段的多次读取的结果是一致的,除非数据被当前事务本身改变。可防止脏读和不可重复读,但幻读仍可能发生 |
ISOLATION_SERIALIZABLE | 完全服从ACID的隔离级别,确保不发生脏读、不可重复读和幻影读。这在所有隔离级别中也是最慢的,因为它通常是通过完全锁定当前事务所涉及的数据表来完成的 |
二、SpringBoot
1. SpingBoot分析
概述
- 通过自动装配简化了spring中繁琐的xml配置
- 同时自身已嵌入Tomcat、Jetty等web容器,无须以war包形式部署项目
- 集成了Spring MVC
- Spring MVC是Spring Framework提供的Web组件,全称是Spring Web MVC,是目前主流的实现MVC设计模式的框架,提供前端路由映射、视图解析等功能
- 参考博客:https://blog.csdn.net/qq_52797170/article/details/125591705
- 可以以jar包的形式独立运行
- 使用pom文件简化Maven配置
2. 自动装配
概述
- SpringBoot的自动装配实际上就是为了从
spring.factories
文件中获取到对应的需要进行自动装配的类,并生成相应的Bean对象,然后将它们交给spring容器来帮我们进行管理 - 在传统的 Java 开发中,我们需要手动管理对象之间的依赖关系,通过创建对象实例并将其注入到其他对象中。这种方式需要编写大量的样板代码,而且在应用程序的规模变大时,维护和管理这些依赖关系会变得非常复杂。Spring Boot 的自动装配机制解决了这个问题。自动装配通过使用依赖注入和反射技术,使得对象之间的依赖关系可以自动完成,无需手动编写大量的配置代码。
@SpringBootApplication
- 主要作用就是标记说明这个类是springboot的主配置类,springboot应该运行这个类里面的main()方法来启动程序
- 这个注解主要由三个子注解组成
- @SpringBootConfiguration
- 这个注解标注在哪个类上,就表示当前这个类是一个配置类,而配置类也是spring容器中的组件
- @EnableAutoConfiguration
- 这个注解是开启自动配置的功能
- @ComponentScan
- 这个注解的作用就是扫描当前包及子包的注解
- @SpringBootConfiguration
自动装配的流程
- 在springboot启动的时候会创建一个SpringApplication对象,在对象的构造方法里面会进行一些参数的初始化工作,最主要的是判断当前应用程序的类型以及设置初始化器以及监听器,并在这个过程中会加载整个应用程序的spring.factories文件,将文件中的内容放到缓存当中,方便后续获取
- SpringApplication对象创建完成之后会执行run()方法来完成整个应用程序的启动,启动的过程中有两个最主要的方法prepareContext()和refreshContext(),在这两个方法中完成了自动装配的核心功能,在run()方法里还执行了一些包括上下文对象的创建,打印banner图,异常报告期的准备等各个准备工作,方便后续进行调用
- 在prepareContext()中主要完成的是对上下文对象的初始化操作,包括属性的设置,比如设置环境变量。在整个过程中有一个load()方法,它主要是完成一件事,那就是将启动类作为一个beanDefinition注册到registry,方便后续在进行BeanFactoryPostProcessor调用执行的时候,可以找到对应执行的主类,来完成对@SpringBootApplication、@EnableAutoConfiguration等注解的解析工作
- 在refreshContext()方法中会进行整个容器的刷新过程,会调用spring中的refresh()方法,refresh()方法中有13个非常关键的方法,来完成整个应用程序的启动。而在自动装配过程中,会调用的关键的一个方法就是invokeBeanFactoryPostProcessors()方法,在这个方法中主要是对ConfigurationClassPostProcessor类的处理,这个类是BFPP(BeanFactoryPostProcessor)的子类,因为实现了BDRPP(BeanDefinitionRegistryPostProcessor)接口,在调用的时候会先调用BDRPP中的postProcessBeanDefinitionRegistry()方法,然后再调用BFPP中的postProcessBeanFactory()方法,在执行postProcessBeanDefinitionRegistry()方法的时候会解析处理各种的注解,包含@PropertySource、@ComponentScan、@Bean、@Import等注解,最主要的是对@Import注解的解析
- 在解析@Import注解的时候,会有一个getImport()方法,从主类开始递归解析注解,把所有包含@Import的注解都解析到,然后在processImport()方法中对import的类进行分类,例如AutoConfigurationImportSelect归属于ImportSelect的子类,在后续的过程中会调用DeferredImportSelectorHandler类里面的process方法,来完成整个EnableAutoConfiguration的加载
优点
- 减少样板代码
- 灵活性和可扩展性
- 依赖解耦
- 单元测试和集成测试
3. 条件注解
概述
- 条件注解,即**@Conditional**,可用于根据某个特定的条件来判断是否需要创建某个特定的Bean
- @Conditional注解可以添加在被@Configuration、@Component、@Service等修饰的类,或在@Bean修饰的方法上,用于控制类或方法对应的Bean是否需要创建
- @Conditional注解需要和Condition接口搭配一起使用,通过对应Condition接口来告知是否满足匹配条件
- 条件注解存在的意义在于动态识别,即代码自动化执行。如@ConditionalOnClass会检查类加载器中是否存在对应的类,如果有的话被注解修饰的类就有资格被Spring容器所注册,否则会被跳过不注册
- @Conditional注解属性中可以持有多个Condition接口的实现类,所有的Condition接口需要全部匹配成功后这个@Conditional修饰的组件才有资格被注册
参考
参考博客:https://blog.csdn.net/lonelymanontheway/article/details/128423648
4. 启动流程
直接上图
参考博客:https://cloud.tencent.com/developer/article/2131866
- 整个过程极其复杂,了解为主,理解整个的设计思路是为了帮助我们更好的认识和使用SpringBoot
- 重点掌握
SpringApplication.run()
的执行步骤,即1-5步
5. 简化依赖管理
概述
- Spring Boot 的自动装配功能为我们简化了依赖管理的过程,使得开发更加高效和灵活。通过使用@Autowired 注解和其他自动装配相关的注解,我们能够轻松地在应用程序中管理对象之间的依赖关系。这种自动装配的方式减少了样板代码,提高了代码的可维护性和可测试性。同时,它也带来了灵活性和可扩展性,使得应用程序更易于开发和维护
原理
- springboot程序启动的时候会先加载所有springboot提供的场景启动器对应的自动配置类,大概有100多个,但实际会根据@Condition注解加载满足条件的自动配置类。这些条件是根据我们自己导入的starter依赖进行判断。最后只会你引入的starter依赖来加入对应的自动配置类。
三、数据库
1. SQL优化
避免使用
select *
- 问题
- 查了很多数据,但是不用,白白浪费了数据库资源,比如:内存或者cpu
- 多查出来的数据,通过网络IO传输的过程中,也会增加数据传输的时间
- 还有一个最重要的问题是:
select *
不会走覆盖索引,会出现大量的回表操作,而从导致查询sql的性能很低
- 优化
- sql语句查询时,只查需要用到的列,多余的列根本无需查出来
用
union all
代替union
- 问题
- 使用
union
关键字后,可以获取排重后的数据,排重的过程需要遍历、排序和比较,它更耗时,更消耗cpu资源
- 使用
- 优化
- 能用union all的时候,尽量不用union
- 除非是有些特殊的场景,比如union all之后,结果集中出现了重复数据,而业务场景中是不允许产生重复数据的,这时可以使用union
小表驱动大表
- 问题
- 假如有order和user两张表,其中order表有10000条数据,而user表有100条数据,这时如果想查一下所有有效的用户下过的订单列表,如果直接对order表进行查询数据量太大,查询速度慢
- 优化
- 先查询user表中符合条件的,在此基础上再去查询order表中符合条件的,可以提高查询效率
- 补充
- in和exists
in
适用于左边大表,右边小表exists
适用于左边小表,右边大表
- in和exists
批量操作
-
问题
- 如果需要对大量数据进行同一种操作的时候,如果一个一个的去做,那么每次都要访问数据库导致性能消耗巨大
-
优化
- 提供一个批量处理数据的方法,将所有要处理的数据放在一个请求中处理,这样性能可以的到巨大提升
- 但需要注意的是,不建议一次批量操作太多的数据,如果数据太多数据库响应也会很慢,批量操作需要把握一个度,建议每批数据尽量控制在500以内,如果数据多于500,则分多批次处理
-
示例
-
优化前
insert into order(id,code,user_id) values(123,'001',100); insert into order(id,code,user_id) values(124,'002',100); insert into order(id,code,user_id) values(125,'003',101);
-
优化后
insert into order(id,code,user_id) values(123,'001',100),(124,'002',100),(125,'003',101);
-
多用limit
-
问题场景一
-
当我们需要查询某些数据中的第一条时
-
优化前
select id, create_date from order where user_id=123 order by create_date asc;
-
优化后
select id, create_date from order where user_id=123 order by create_date asc limit 1;
-
-
问题场景二
-
在删除或者修改数据时,为了防止误操作,导致删除或修改了不相干的数据
-
优化前
update order set status=0,edit_time=now(3) where id>=100 and id<200;
-
优化后
update order set status=0,edit_time=now(3) where id>=100 and id<200 limit 100;
-
-
问题场景三
-
查找指定数据是否"存在"时
-
优化前
#### SQL写法: SELECT count(*) FROM table WHERE a = 1 AND b = 2 #### Java写法: int nums = xxDao.countXxxxByXxx(params); if ( nums > 0 ) { //当存在时,执行这里的代码 } else { //当不存在时,执行这里的代码 }
-
优化后
#### SQL写法: SELECT 1 FROM table WHERE a = 1 AND b = 2 LIMIT 1 #### Java写法: Integer exist = xxDao.existXxxxByXxx(params); if ( exist != NULL ) { //当存在时,执行这里的代码 } else { //当不存在时,执行这里的代码 }
-
in中值太多
- 问题
- 对于批量查询接口,我们通常会使用
in
关键字过滤出数据,但in的值太多会导致查询时间很长,最后可能导致接口超时
- 对于批量查询接口,我们通常会使用
- 优化
- 在sql中对数据用limit做限制
- 在业务代码中限制查询数量
- 分批用多线程去查询数据
增量查询
-
问题
- 有时候,我们需要通过远程接口查询数据,然后同步到另外一个数据库。如果直接获取所有的数据,然后同步过去。这样虽说非常方便,但是带来了一个非常大的问题,就是如果数据很多的话,查询性能会非常差。
-
优化
-
代码示例
select * from user where id>#{lastId} and create_time >= #{lastCreateTime} limit 100;
按id和时间升序,每次只同步一批数据,这一批数据只有100条记录。每次同步完成之后,保存这100条数据中最大的id和时间,给同步下一批数据的时候用。通过这种增量查询的方式,能够提升单次查询的效率。
-
高效的分页
-
问题
- 在mysql中分页一般用的
limit
关键字,如果表中数据量少,用limit关键字做分页,没啥问题。但如果表中数据量很多,用它就会出现性能问题。比如查询从第1000000条到1000020的数据,这时候就非常浪费资源。
- 在mysql中分页一般用的
-
优化
-
优化前
select id,name,age from user limit 1000000,20;
-
优化方案一
-
先找到上次分页最大的id,然后利用id上的索引查询
-
不过该方案,要求id是连续的,并且有序的
select id,name,age from user where id > 1000000 limit 20;
-
-
优化方案二
-
使用
between
优化分页 -
需要注意的是between要在唯一索引上分页,不然会出现每页大小不一致的问题
select id,name,age from user where id between 1000000 and 1000020;
-
-
用连接查询代替子查询
-
问题
- 从两张以上的表中查询数据,在使用in关键字进行子查询时,需要创建临时表,查询完毕后,需要再删除这些临时表,有一些额外的性能消耗
-
优化
-
使用连接查询
-
代码示例
select o.* from order o inner join user u on o.user_id = u.id where u.status=1
-
join的表不宜过多
- 问题
- 如果join太多,mysql在选择索引的时候会非常复杂,很容易选错索引
- join分别从两个表读一行数据进行两两对比,复杂度是 n^2
- 优化
- 如果实现业务场景中需要查询出另外几张表中的数据,可以在其他表中加入
冗余字段
- 如果实现业务场景中需要查询出另外几张表中的数据,可以在其他表中加入
控制索引数量
- 问题
- 索引能够显著的提升查询sql的性能,但索引数量并非越多越好,当表中新增数据时,需要同时为它创建索引,而索引是需要额外的存储空间的,而且还会有一定的性能消耗
- mysql使用的B+树的结构来保存索引的,在insert、update和delete操作时,需要更新B+树索引。如果索引过多,会消耗很多额外的性能
- 优化
- 单表的索引数量应该尽量控制在5个以内,并且单个索引中的字段数不超过5个
- 如果非要超过5个,那么能够建联合索引,就别建单个索引,尽量删除无用的单个索引
选择合理的字段类型
- 规则
- 能用数字类型,就不用字符串,因为字符的处理往往比数字要慢
- 尽可能使用小的类型,比如:用bit存布尔值,用tinyint存枚举值等
- 长度固定的字符串字段,用char类型
- 长度可变的字符串字段,用varchar类型
- 金额字段用decimal,避免精度丢失问题
尽量缩小数据范围
-
问题
- 在进行一些耗时操作之前,应该尽量缩小数据范围
-
示例
-
优化前
select user_id,user_name from order group by user_id,user_name having user_id <= 200;
-
优化后
select user_id,user_name from order where user_id <= 200 group by user_id,user_name
-
2. 覆盖索引
概述
- 覆盖索引(Covering Index)是 MySQL 中的一种优化技术,它能够显著提高查询性能。在使用覆盖索引的情况下,查询操作只需要访问索引即可获取所需的数据,而不必再访问表的实际数据行(即不需要回表)
- 定义:覆盖索引是指一个索引包含了查询所需要的所有列的数据
典型特征
- 索引包含了 SELECT 子句中的所有列
- 索引包含了 WHERE 子句中的所有列
- 索引包含了 ORDER BY 子句中的所有列(如果有)
示例
-
假设我们有一个表
employees
,结构如下:CREATE TABLE employees ( emp_id INT PRIMARY KEY, first_name VARCHAR(50), last_name VARCHAR(50), department_id INT, salary DECIMAL(10, 2), INDEX idx_dept_salary(department_id, salary) );
-
现在我们执行以下查询:
SELECT department_id, salary FROM employees WHERE department_id = 5;
在这个查询中,
SELECT
子句只涉及department_id
和salary
列,而这些列都包含在idx_dept_salary
索引中。因此,MySQL 可以利用这个覆盖索引来优化查询。
工作原理
- 在没有覆盖索引的情况下,查询执行的过程通常如下:
- MySQL 使用索引查找满足查询条件的记录的主键值(或聚簇索引)
- MySQL 使用主键值回表(即访问表数据)来读取查询所需的列
- 在有覆盖索引的情况下,查询执行的过程可以简化为:
- MySQL 使用索引查找满足查询条件的记录,并直接从索引中获取所有查询所需的列(由于索引已经覆盖了查询所需的所有数据,MySQL 不需要回表读取数据)
优点
- 减少 I/O 操作:覆盖索引允许查询只读取索引,而不必回表读取实际数据行。这减少了磁盘 I/O 操作,从而提高了查询性能
- 提高查询速度:由于查询的数据可以直接从索引中获取,覆盖索引可以显著减少查询的响应时间,特别是在数据量较大的情况下
- 减少锁竞争:由于减少了回表操作,覆盖索引也可以减少表上的行级锁定,降低锁竞争的概率
局限性
- 索引大小的限制:为了让索引覆盖查询,索引必须包含查询所需的所有列。这可能导致索引变得非常大,从而增加了维护索引的开销(如插入、更新、删除操作的成本)
- 冗余数据:在索引中包含所有查询列可能会导致数据冗余,特别是当表中有许多列且查询涉及的列较多时,创建覆盖索引可能会导致索引的存储空间显著增加
- 适用场景有限:覆盖索引对那些查询列较少且频繁执行的查询最有效。如果查询涉及的列较多,或者查询模式变化频繁,覆盖索引的作用可能会减弱
使用场景
- 频繁查询特定列:如果应用程序经常查询某些列,而这些列可以通过索引覆盖,可以考虑创建覆盖索引
- 优化读性能:在只读或读操作远多于写操作的场景中,覆盖索引可以显著提高查询性能
- 减少回表操作:对于那些数据量大、需要频繁读取的表,覆盖索引可以减少回表操作,降低 I/O 开销
查看是否使用了覆盖索引
-
我们可以通过 EXPLAIN 关键字来查看 MySQL 是否使用了覆盖索引来执行查询
-
在 EXPLAIN 输出中,如果 Extra 列包含 Using index,则表示查询使用了覆盖索引
EXPLAIN SELECT department_id, salary FROM employees WHERE department_id = 5;
-
结合InnoDB的覆盖索引
- 在 InnoDB 存储引擎中,聚簇索引(主键索引)会包含表的所有列。因此,InnoDB 的二级索引自动包含主键列,这在某些情况下会对覆盖索引的设计产生影响
3. union和union all
作用
- 如果我们需要将两个 select 语句的结果作为一个整体显示出来,我们就需要用到 union 或者 union all 关键字,union (或称为联合)的作用是将多个结果合并在一起显示出来
区别
- union
- 对两个结果集进行并集操作,不包括重复行
- 同时进行默认规则的排序
- 删除重复的记录再返回结果
- union all
- 对两个结果集进行并集操作,包括重复行
- 不进行排序
- 如果返回的两个结果集中有重复的数据,那么返回的结果集就会包含重复的数据
注意事项
- union和union all内部的 SELECT 语句必须拥有相同数量的列
- 每条 SELECT 语句中列的顺序必须相同
- 如果系统中进行了分表,一定要保证各个表的字段顺序一致,特别是修改的时候
补充说明
- sql 中的组合in,可用 union all 来代替,提高查询效率
4. in和exists
in
-
在查询的时候,首先查询子查询的表,然后将内表和外表做一个笛卡尔积,然后按照条件进行筛选
-
语法结构
SELECT * FROM `user` WHERE `user`.id IN ( SELECT `order`.user_id FROM `order` )
exists
-
遍历循环外表,然后看外表中的记录有没有和内表的数据一样的,匹配上就将结果放入结果集中
-
语法结构
SELECT `user`.* FROM `user` WHERE EXISTS ( SELECT `order`.user_id FROM `order` WHERE `user`.id = `order`.user_id )
区别
- 如果子查询得出的结果集记录较少,主查询中的表较大且又有索引时应该用in。反之如果外层的主查询记录较少,子查询中的表大,又有索引时使用exists
- in不对NULL进行处理
- in是把外表和内表作hash 连接,而exists是对外表作loop循环,每次loop循环再对内表进行查询
- 如果查询语句使用了not in那么内外表都进行全表扫描,没有用到索引。而not extsts的子查询依然能用到表上的索引。所以无论那个表大,用not exists都比not in要快
5. 脏读
- 对于两个事务 Session A 、 Session B, Session A 读取了已经被 Session B 更新但还没有被提交的字段。之后若 Session B 回滚,Session A 读取的内容就是临时且无效的,这种现象就称之为脏读
6. 脏写
- 对于两个事务 Session A 、 Session B,如果事务 Session A 修改了 另一个未提交事务 Session B 修改过的数据,那就意味着发生了脏写。
7. 不可重复读
- 对于两个事务 Session A 、 Session B,Session A 读取了一个字段,然后 Session B 更新了该字段。之后 Session A 再次读取同一个字段,值就不同了,那就意味着发生了不可重复读
- 我们在 Session B 中提交了几个隐式事务(注意是隐式事务,意味着语句结束事务就提交了),这些事务都修改了studentno 列为 1 的记录的列 name 的值,每次事务提交之后,如果 Session A 中的事务都可以查看到最新的值,这种现象也被称之为不可重复读
8. 幻读
- 对于两个事务 Session A 、 Session B,Session A 从一个表中读取了一个字段,然后 Session B 在该表中插入了一些新的行。之后,如果 Session A 再次读取同一个表,就会多出几行,那就意味着发生了幻读
9. 事务的隔离级别
参考博客
https://blog.csdn.net/weixin_43823808/article/details/124343700
隔离级别
隔离级别 | 脏读 | 不可重复读 | 幻读 | 加锁读 |
---|---|---|---|---|
READ UNCOMMITTED(读未提交) | 可能 | 可能 | 可能 | 没有 |
READ COMMITTED(读已提交) | 不会 | 可能 | 可能 | 没有 |
REPEATABLE READ(可重复读) | 不会 | 不会 | 可能 | 没有 |
SERIALIZABLE(可串行化) | 不会 | 不会 | 不会 | 有 |
- REPEATABLE READ:可重复读,事务 A 在读到一条数据之后,此时事务 B 对该数据进行了修改并提交,那么 事务A 再读该数据,读到的还是原来的内容。可以避免脏读、不可重复读,但幻读问题仍然存在。这是MySQL 的默认隔离级别
- READ UNCOMMITTED:读未提交,在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。不能避免脏读、不可重复读、幻读
- READ COMMITTED:读已提交,它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这是大多数数据库系统的默认隔离级别(Oracle 默认的)。可以避免脏读,但不可重复读、幻读问题仍然存在
- SERIALIZABLE:可串行化,确保事务可以从一个表中读取相同的行。在这个事务持续期间,禁止其他事务对该表执行插入、更新和删除操作。所有的并发问题都可以避免,但性能十分低下。能避免脏读、不可重复读和幻读
查看MySQL数据库的隔离级别
-
代码
show variables like 'transaction_isolation';
设置数据库的隔离级别
-
代码
SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL 隔离级别; #其中,隔离级别格式: > READ UNCOMMITTED > READ COMMITTED > REPEATABLE READ > SERIALIZABLE #或者(推荐下面这种) SET [GLOBAL|SESSION] TRANSACTION_ISOLATION = '隔离级别' #其中,隔离级别格式: > READ-UNCOMMITTED > READ-COMMITTED > REPEATABLE-READ > SERIALIZABLE
-
global和session的区别
- 使用global
- 在全局范围影响
- 当前已经存在的会话无效
- 只对执行完该语句之后产生的会话起作用
- 使用session
- 在会话范围影响
- 对当前会话的所有后续的事务有效
- 如果在事务之间执行,则对后续的事务有效
- 该语句可以在已经开启的事务中间执行,但不会影响当前正在执行的事务
- 使用global
10. #{} 和 ${}
- #{}是预编译处理,${}是字符直接替换
- #{}可以防⽌SQL注⼊并且性能更高,${}存在SQL注⼊的⻛险
- 在排序功能、表名、字段名作为参数时,需要使⽤${}
- 模糊查询时,虽然${}可以完成,但因为存在SQL注⼊的问题,所以通常使⽤mysql内置函数concat来完成
11. 一级/二级缓存
一级缓存
- 一级缓存是指在同一个SqlSession中,对于相同的查询语句和参数,第一次查询的结果会被缓存到内存中,后续的查询会直接从缓存中获取结果,而不会再次查询数据库。一级缓存是MyBatis默认开启的,可以通过在SqlSession中调用clearCache()方法来清空缓存。
二级缓存
- 二级缓存是指在多个SqlSession中,对于相同的查询语句和参数,第一次查询的结果会被缓存到内存中,后续的查询会直接从缓存中获取结果,而不会再次查询数据库。二级缓存是需要手动开启的,可以通过在Mapper.xml文件中添加标签来开启。二级缓存的作用范围是Mapper级别的,也就是说,同一个Mapper.xml文件中的查询语句会共享同一个缓存。
区别
功能 | 一级缓存 | 二级缓存 |
---|---|---|
作用范围 | SqlSession级别 | Mapper级别 |
生命周期 | 和SqlSession一样 | 和应用程序一样 |
开启 | 默认开启 | 手动开启 |
执行流程
- 参考博客:https://blog.csdn.net/qq_48157004/article/details/130604672
-
代码示例
public void testMybatis()throws Exception{ SqlSessionFactoryBuilder sqlSessionFactoryBuilder=new SqlSessionFactoryBuilder(); org.springframework.core.io.ClassPathResource classPathResource=new ClassPathResource("org/apache/ibatis/user/mybatis.xml"); InputStream inputStream = classPathResource.getInputStream(); // 1.读取配置文件获取SqlSessionFactory SqlSessionFactory sqlSessionFactory= sqlSessionFactoryBuilder.build(inputStream); // 2.获取sqlsession SqlSession sqlSession = sqlSessionFactory.openSession(); // 3.执行获取结果 User user = new User(1L, null, null, null); UserMapper mapper = sqlSession.getMapper(UserMapper.class); List<User> users = mapper.selectUser(user); mapper.selectUser(user); System.out.println(users); }
一级缓存失效的场景
- 必须在同一个statementID的语句中执行
- SQL语句和参数值必须一致
- 分页条件必须一致
- 必须在同一个会话中
四、分布式
== == == == == == == == == == == == 未完待续 == == == == == == == == == == == ==