From 8aaf402c28918ee533184410de2a6c86c21b6227 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 25 Jul 2021 21:46:11 +0800 Subject: [PATCH 001/168] Update Java Notes --- Issue.md | 4 +- Java.md | 148 +++++--- Prog.md | 8 +- SSM.md | 1076 +++++++----------------------------------------------- 4 files changed, 238 insertions(+), 998 deletions(-) diff --git a/Issue.md b/Issue.md index 1cef117..3303f0e 100644 --- a/Issue.md +++ b/Issue.md @@ -154,8 +154,8 @@ * 管道通信:管道是用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,pipe文件 * 匿名管道(Pipes) :用于具有亲缘关系的父子进程间或者兄弟进程之间的通信,只支持半双工通信 * 命名管道(Names Pipes):以磁盘文件的方式存在,可以实现本机任意两个进程通信,遵循FIFO - * 消息队列:消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识,对比管道: - * 匿名管道存在于内存中的文件;命名管道存在于实际的磁盘介质或者文件系统;消息队列存放在内核中,只有在内核重启(操作系统重启)或者显示地删除一个消息队列时,该消息队列才被真正删除 + * 消息队列:内核中存储消息的链表,由消息队列标识符标识,能在不同进程之间提供全双工通信,对比管道: + * 匿名管道存在于内存中的文件;命名管道存在于实际的磁盘介质或者文件系统;消息队列存放在内核中,只有在内核重启(操作系统重启)或者显示地删除一个消息队列时,该消息队列才被真正删除 * 读进程可以根据消息类型有选择地接收消息,而不像 FIFO 那样只能默认地接收 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP diff --git a/Java.md b/Java.md index b214d04..0c76ed3 100644 --- a/Java.md +++ b/Java.md @@ -524,17 +524,17 @@ public class Test1 { ### 运算 -* i++与++i的区别? - i++表示先将i放在表达式中运算,然后再加1 - ++i表示先将i加1,然后再放在表达式中运算 +* i++ 与++i 的区别? + i++ 表示先将 i 放在表达式中运算,然后再加 1 + ++i 表示先将 i 加 1,然后再放在表达式中运算 -* ||和|,&&和&的区别,逻辑运算符 +* || 和 |,&& 和& 的区别,逻辑运算符 **&和| 称为布尔运算符,位运算符。&&和|| 称为条件布尔运算符,也叫短路运算符**。 两种运算符得到的结果完全相同,但得到结果的方式又一个重要区别:条件布尔运算符性能比较好。他检查第一个操作数的值,再根据该操作数的值进行操作,可能根本就不处理第二个操作数。 - 结论:如果&&运算符的第一个操作数是false,就不需要考虑第二个操作数的值了,因为无论第二个操作数的值是什么,其结果都是false;同样,如果第一个操作数是true,||运算符就返回true,无需考虑第二个操作数的值。但&和|却不是这样,它们总是要计算两个操作数。为了提高性能,**尽可能使用&&和||运算符** + 结论:如果 && 运算符的第一个操作数是 false,就不需要考虑第二个操作数的值了,因为无论第二个操作数的值是什么,其结果都是 false;同样,如果第一个操作数是 true,|| 运算符就返回 true,无需考虑第二个操作数的值。但 & 和 | 却不是这样,它们总是要计算两个操作数。为了提高性能,**尽可能使用 && 和 || 运算符** * switch @@ -560,7 +560,7 @@ public class Test1 { * 移位运算 - 计算机里一般用**补码表示数字**,正数、负数的表示区别就是最高位是0还是1 + 计算机里一般用**补码表示数字**,正数、负数的表示区别就是最高位是 0 还是 1 * 正数的原码反码补码相同 @@ -1268,11 +1268,11 @@ class Animal{ -#### 继承访问 +#### 变量访问 继承后成员变量的访问特点:**就近原则**,子类有找子类,子类没有找父类,父类没有就报错 -如果要申明访问父类的成员变量可以使用:super.父类成员变量。super指父类引用 +如果要申明访问父类的成员变量可以使用:super.父类成员变量,super指父类引用 ```java public class ExtendsDemo { @@ -1305,9 +1305,11 @@ class Animal{ -#### 方法重写 +#### 方法访问 + +子类继承了父类就得到了父类的方法,可以直接调用,受权限修饰符的限制,也可以重写方法 -方法重写:子类继承了父类就得到了父类的方法,子类重写一个与父类申明一样的方法来**覆盖**父类的该方法 +**方法重写**:子类重写一个与父类申明一样的方法来**覆盖**父类的该方法 方法重写的校验注解:@Override @@ -4377,6 +4379,34 @@ public class Student implements Comparable{ +#### Queue + +Queue:队列,先进先出的特性 + +PriorityQueue 是优先级队列,底层存储结构为 Object[],默认实现为小顶堆 + +构造方法: + +* `public PriorityQueue()`:构造默认长度为 11 的队列(数组) + +* `public PriorityQueue(Comparator comparator)`:带比较器实现,可以自定义堆排序的规则 + + ```java + Queue pq = new PriorityQueue<>((v1, v2) -> v2 - v1);//实现大顶堆 + +常用 API: + +* `public boolean offer(E e)`:将指定的元素插入到此优先级队列中尾部 +* `public E poll() `:检索并删除此队列的头元素,如果此队列为空,则返回 null +* `public E peek()`:检索但不删除此队列的头,如果此队列为空,则返回 null +* `public boolean remove(Object o)`:从该队列中删除指定元素(如果存在),删除元素 e 使用 o.equals(e) 比较,如果队列包含多个这样的元素,删除第一个 + + + +**** + + + #### Collections java.utils.Collections:集合**工具类**,Collections并不属于集合,是用来操作集合的工具类 @@ -7993,13 +8023,13 @@ public class UserServiceTest { #### 获取类 -反射技术的第一步是先得到Class类对象,有三种方式获取: +反射技术的第一步是先得到 Class 类对象,有三种方式获取: * 类名.class -* 通过类的对象.getClass()方法 +* 类的对象.getClass() * Class.forName("类的全限名"):`public static Class forName(String className) ` -Class类下的方法: +Class 类下的方法: | 方法 | 作用 | | ---------------------- | ------------------------------------------------------------ | @@ -8040,22 +8070,22 @@ class Student{} #### 获取构造 -获取构造器的API: +获取构造器的 API: -* Constructor getConstructor(Class... parameterTypes):根据参数匹配获取某个构造器,只能拿public修饰的构造器,几乎不用! +* Constructor getConstructor(Class... parameterTypes):根据参数匹配获取某个构造器,只能拿 public 修饰的构造器 * **Constructor getDeclaredConstructor(Class... parameterTypes)**:根据参数匹配获取某个构造器,只要申明就可以定位,不关心权限修饰符 -* Constructor[] getConstructors():获取所有的构造器,只能拿public修饰的构造器,几乎不用! -* **Constructor[] getDeclaredConstructors()**:获取所有构造器,只要申明就可以定位,不关心权限修饰符。 +* Constructor[] getConstructors():获取所有的构造器,只能拿 public 修饰的构造器 +* **Constructor[] getDeclaredConstructors()**:获取所有构造器,只要申明就可以定位,不关心权限修饰符 -Constructor的常用API: +Constructor 的常用 API: -| 方法 | 作用 | -| --------------------------------- | -------------------------------------- | -| T newInstance(Object... initargs) | 创建对象,注入构造器需要的数据 | -| void setAccessible(true) | 修改访问权限,true攻破权限(暴力反射) | -| String getName() | 以字符串形式返回此构造函数的名称 | -| int getParameterCount() | 返回参数数量 | -| Class[] getParameterTypes | 返回参数类型数组 | +| 方法 | 作用 | +| --------------------------------- | --------------------------------------- | +| T newInstance(Object... initargs) | 创建对象,注入构造器需要的数据 | +| void setAccessible(true) | 修改访问权限,true 攻破权限(暴力反射) | +| String getName() | 以字符串形式返回此构造函数的名称 | +| int getParameterCount() | 返回参数数量 | +| Class[] getParameterTypes | 返回参数类型数组 | ```java public class TestStudent01 { @@ -8141,12 +8171,12 @@ public class TestStudent02 { 获取Field成员变量API: -* Field getField(String name) : 根据成员变量名获得对应Field对象,只能获得public修饰 -* Field getDeclaredField(String name) : 根据成员变量名获得对应Field对象,所有申明的变量 -* Field[] getFields() : 获得所有的成员变量对应的Field对象,只能获得public的 -* Field[] getDeclaredFields() : 获得所有的成员变量对应的Field对象,只要申明了就可以得到 +* Field getField(String name) : 根据成员变量名获得对应 Field 对象,只能获得 public 修饰 +* Field getDeclaredField(String name) : 根据成员变量名获得对应 Field 对象,所有申明的变量 +* Field[] getFields() : 获得所有的成员变量对应的Field对象,只能获得 public 的 +* Field[] getDeclaredFields() : 获得所有的成员变量对应的 Field 对象,只要申明了就可以得到 -Field的方法:给成员变量赋值和取值 +Field 的方法:给成员变量赋值和取值 | 方法 | 作用 | | ---------------------------------- | --------------------------------------------------------- | @@ -8225,15 +8255,15 @@ public class FieldDemo02 { #### 获取方法 -获取Method方法API: +获取 Method 方法 API: -* Method getMethod(String name,Class...args):根据方法名和参数类型获得方法对象,public修饰 -* Method getDeclaredMethod(String name,Class...args):根据方法名和参数类型获得方法对象,包括private -* Method[] getMethods():获得类中的所有成员方法对象返回数组,只能获得public修饰且包含父类的 +* Method getMethod(String name,Class...args):根据方法名和参数类型获得方法对象,public 修饰 +* Method getDeclaredMethod(String name,Class...args):根据方法名和参数类型获得方法对象,包括 private +* Method[] getMethods():获得类中的所有成员方法对象返回数组,只能获得 public 修饰且包含父类的 * Method[] getDeclaredMethods():获得类中的所有成员方法对象,返回数组,只获得本类申明的方法 -Method常用API: -`public Object invoke(Object obj, Object... args) `:使用指定的参数调用由此方法对象,obj对象名 +Method 常用 API: +`public Object invoke(Object obj, Object... args) `:使用指定的参数调用由此方法对象,obj 对象名 ```java public class MethodDemo{ @@ -13715,7 +13745,7 @@ Java 虚拟机中关于方法重写的判定基于方法描述符,如果子类 1. 找到操作数栈的第一个元素所执行的对象的实际类型,记作 C -2. 如果在类型 C 中找到与常量中的描述符和名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回 java.lang.IllegalAccessError 异常 +2. 如果在类型 C 中找到与常量中的描述符和名称都相符的方法,则进行访问**权限校验**(私有的),如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回 java.lang.IllegalAccessError 异常 IllegalAccessError:表示程序试图访问或修改一个属性或调用一个方法,这个属性或方法没有权限访问,一般会引起编译器异常。如果这个错误发生在运行时,就说明一个类发生了不兼容的改变 @@ -13738,7 +13768,7 @@ Java 虚拟机中关于方法重写的判定基于方法描述符,如果子类 2. Class 结构中有 vtable,查表得到方法的具体地址,执行方法的字节码 * invokeinterface 所使用的接口方法表(interface method table,itable) -虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,jvm会把该类的方法表也初始化完毕 +虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM 会把该类的方法表也初始化完毕 虚方法表的执行过程: @@ -13747,12 +13777,12 @@ Java 虚拟机中关于方法重写的判定基于方法描述符,如果子类 为了优化对象调用方法的速度,方法区的类型信息会增加一个指针,该指针指向一个记录该类方法的方法表。每个类中都有一个虚方法表,本质上是一个数组,每个数组元素指向一个当前类及其祖先类中非私有的实例方法 -方法表满足两个特质: +方法表满足以下的特质: -* 其一,子类方法表中包含父类方法表中的所有方法 -* 其二,子类方法在方法表中的索引值,与它所重写的父类方法的索引值相同 +* 其一,子类方法表中包含父类方法表中的**所有方法**,并且在方法表中的索引值与父类方法表种的索引值相同 +* 其二,**非重写的方法指向父类的方法表项**,与父类共享一个方法表项,**重写的方法指向本身自己的实现** - + Passenger 类的方法表包括两个方法,分别对应 0 号和 1 号。方法表调换了 toString 方法和 passThroughImmigration 方法的位置,是因为 toString 方法的索引值需要与 Object 类中同名方法的索引值一致,为了保持简洁,这里不考虑 Object 类中的其他方法。 @@ -13761,7 +13791,37 @@ Passenger 类的方法表包括两个方法,分别对应 0 号和 1 号。方 * 使用了方法表的动态绑定与静态绑定相比,仅仅多出几个内存解引用操作:访问栈上的调用者、读取调用者的动态类型、读取该类型的方法表、读取方法表中某个索引值所对应的目标方法,但是相对于创建并初始化 Java 栈帧这操作的开销可以忽略不计 * 上述优化的效果看上去不错,但实际上**仅存在于解释执行**中,或者即时编译代码的最坏情况。因为即时编译还拥有另外两种性能更好的优化手段:内联缓存(inlining cache)和方法内联(method inlining) - +```java +class Person { + public String toString() { + return "I'm a person."; + } + public void eat() {} + public void speak() {} +} + +class Boy extends Person { + public String toString() { + return "I'm a boy"; + } + public void speak() {} + public void fight() {} +} + +class Girl extends Person { + public String toString() { + return "I'm a girl"; + } + public void speak() {} + public void sing() {} +} +``` + +![](https://gitee.com/seazean/images/raw/master/Java/JVM-虚方法表指向.png) + + + +参考文档:https://www.cnblogs.com/kaleidoscope/p/9790766.html @@ -14528,7 +14588,7 @@ jstat(JVM Statistics Monitoring Tool):用于监视 JVM 各种运行状态 类装载相关: - - -class:显示ClassLoader的相关信息:类的装载、卸载数量、总空间、类装载所消耗的时间等 + - -class:显示 ClassLoader 的相关信息,类的装载、卸载数量、总空间、类装载所消耗的时间等 垃圾回收相关: diff --git a/Prog.md b/Prog.md index c668413..ca90886 100644 --- a/Prog.md +++ b/Prog.md @@ -53,8 +53,8 @@ * 管道通信:管道是用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,pipe文件 * 匿名管道(Pipes) :用于具有亲缘关系的父子进程间或者兄弟进程之间的通信,只支持半双工通信 * 命名管道(Names Pipes):以磁盘文件的方式存在,可以实现本机任意两个进程通信,遵循FIFO - * 消息队列:消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识,对比管道: - * 匿名管道存在于内存中的文件;命名管道存在于实际的磁盘介质或者文件系统;消息队列存放在内核中,只有在内核重启(操作系统重启)或者显示地删除一个消息队列时,该消息队列才被真正删除 + * 消息队列:内核中存储消息的链表,由消息队列标识符标识,能在不同进程之间提供全双工通信,对比管道: + * 匿名管道存在于内存中的文件;命名管道存在于实际的磁盘介质或者文件系统;消息队列存放在内核中,只有在内核重启(操作系统重启)或者显示地删除一个消息队列时,该消息队列才被真正删除 * 读进程可以根据消息类型有选择地接收消息,而不像 FIFO 那样只能默认地接收 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP @@ -7116,11 +7116,11 @@ CountDownLatch:计数器,用来进行线程同步协作,等待所有线程 构造器: -* `public CountDownLatch(int count)`:初始化唤醒需要的down几步 +* `public CountDownLatch(int count)`:初始化唤醒需要的 down 几步 常用API: -* `public void await() `:让当前线程等待,必须down完初始化的数字才可以被唤醒,否则进入无限等待 +* `public void await() `:让当前线程等待,必须 down 完初始化的数字才可以被唤醒,否则进入无限等待 * `public void countDown()`:计数器进行减1(down 1) 应用:同步等待多个 Rest 远程调用结束 diff --git a/SSM.md b/SSM.md index b1c742f..dee0892 100644 --- a/SSM.md +++ b/SSM.md @@ -2813,7 +2813,7 @@ Spring 优点: 基本属性 -* id:bean 的名称,通过id值获取bean (首字母小写) +* id:bean 的名称,通过 id 值获取 bean (首字母小写) * class:bean 的类型,使用完全限定类名 * name:bean 的名称,可以通过 name值获取bean,用于多人配合时给bean起别名 @@ -2865,14 +2865,19 @@ ctx.getBean("beanId") == ctx.getBean("beanName1") == ctx.getBean("beanName2") 取值:bean 对应的类中对应的具体方法名 +实现接口的方式实现初始化,无需配置文件配置 init-method: + +* 实现 InitializingBean,定义初始化逻辑 +* 实现 DisposableBean,定义销毁逻辑 + 注意事项: - 当 scope=“singleton” 时,Spring 容器中有且仅有一个对象,init 方法在创建容器时仅执行一次 - 当 scope=“prototype” 时,Spring 容器要创建同一类型的多个对象,init 方法在每个对象创建时均执行一次 -- 当 scope=“singleton” 时,关闭容器 (.close()) 会导致bean实例的销毁,调用 destroy 方法一次 +- 当 scope=“singleton” 时,关闭容器(.close())会导致bean实例的销毁,调用 destroy 方法一次 - 当 scope=“prototype” 时,对象的销毁由垃圾回收机制 gc() 控制,destroy 方法将不会被执行 -bean配置: +bean 配置: ```xml @@ -2882,7 +2887,7 @@ bean配置: 业务层实现类: ```java -public class UserServiceImpl implements UserService { +public class UserServiceImpl implements UserService{ public UserServiceImpl(){ System.out.println(" constructor is running..."); } @@ -2909,6 +2914,8 @@ UserService userService = (UserService)ctx.getBean("userService3"); + + *** @@ -3448,13 +3455,13 @@ Spring 提供了对 EL 表达式的支持,统一属性注入格式 #### prop -Spring提供了读取外部properties文件的机制,使用读取到的数据为bean的属性赋值 +Spring 提供了读取外部 properties 文件的机制,使用读取到的数据为 bean 的属性赋值 操作步骤: -1. 准备外部properties文件 +1. 准备外部 properties 文件 -2. 开启context命名空间支持 +2. 开启 context 命名空间支持 ```xml @@ -3469,7 +3476,7 @@ Spring提供了读取外部properties文件的机制,使用读取到的数据 "> ``` -3. 加载指定的properties文件 +3. 加载指定的 properties 文件 ```xml @@ -3481,9 +3488,9 @@ Spring提供了读取外部properties文件的机制,使用读取到的数据 ``` -- 注意:如果需要加载所有的properties文件,可以使用`*.properties`表示加载所有的properties文件 +- 注意:如果需要加载所有的 properties 文件,可以使用`*.properties`表示加载所有的 properties 文件 -- 注意:读取数据使用**${propertiesName}**格式进行,其中 propertiesName 指properties文件中的属性名 +- 注意:读取数据使用**${propertiesName}**格式进行,其中 propertiesName 指 properties 文件中的属性名 代码实现: @@ -3494,7 +3501,7 @@ Spring提供了读取外部properties文件的机制,使用读取到的数据 pwd=123456 ``` -* DAO层:注入的资源 +* DAO 层:注入的资源 ```java public interface UserDao { @@ -3520,7 +3527,7 @@ Spring提供了读取外部properties文件的机制,使用读取到的数据 } ``` -* Service业务层 +* Service 业务层 ```java public class UserServiceImpl implements UserService { @@ -3855,7 +3862,7 @@ Mybatis 核心配置文件消失 #### 注解驱动 -注解:启动时使用注解的形式替代xml配置,将spring配置文件从工程中消除,简化书写 +注解:启动时使用注解的形式替代 xml 配置,将 spring 配置文件从工程中消除,简化书写 缺点:为了达成注解驱动的目的,可能会将原先很简单的书写,变的更加复杂。XML中配置第三方开发的资源是很方便的,但使用注解驱动无法在第三方开发的资源中进行编辑,因此会增大开发工作量 @@ -4004,7 +4011,7 @@ public class MainTest { ##### 设置bean -名称:@Component @Controller @Service @Repository +名称:@Component、@Controller、@Service、@Repository 类型:类注解,写在类定义上方 @@ -4075,11 +4082,6 @@ public class UserServiceImpl implements UserService { } ``` -拓展方式: - -* 实现 InitializingBean,定义初始化逻辑 -* 实现 DisposableBean,定义销毁逻辑 - *** @@ -4092,7 +4094,7 @@ public class UserServiceImpl implements UserService { 类型:方法注解 -作用:设置该方法的返回值作为spring管理的bean +作用:设置该方法的返回值作为 Spring 管理的 bean 格式: @@ -4103,11 +4105,11 @@ public DruidDataSource createDataSource() { return ……; } 说明: -- 因为第三方bean无法在其源码上进行修改,使用@Bean解决第三方bean的引入问题 +- 因为第三方 bean 无法在其源码上进行修改,使用 @Bean 解决第三方 bean 的引入问题 -- 该注解用于替代XML配置中的静态工厂与实例工厂创建bean,不区分方法是否为静态或非静态 +- 该注解用于替代 XML 配置中的静态工厂与实例工厂创建 bean,不区分方法是否为静态或非静态 -- @Bean所在的类必须被spring扫描加载,否则该注解无法生效 +- @Bean 所在的类必须被 Spring 扫描加载,否则该注解无法生效 相关属性 @@ -4159,7 +4161,7 @@ private String username; -##### 属性引入 +##### 属性填充 名称:@Autowired、@Qualifier @@ -4181,7 +4183,7 @@ private UserDao userDao; 相关属性: -- required:定义该属性是否允许为 null +- required:为 true (默认)表示注入 bean 时该 bean 必须存在,不然就会注入失败;为 true 表示注入 false 时该 bean 存在就注入,不存在就忽略跳过 注意:在使用 @Autowired 时,首先在容器中查询对应类型的 bean,如果查询结果刚好为一个,就将该 bean 装配给 @Autowired 指定的数据,如果查询的结果不止一个,那么 @Autowired 会根据名称来查找。如果查询的结果为空,那么会抛出异常。解决方法:使用 required=false @@ -4208,7 +4210,7 @@ public class ClassName{} 说明: -- @Autowired默认按类型装配,当出现相同类型的bean,使用@Primary提高按类型自动装配的优先级,多个@Primary会导致优先级设置无效 +- @Autowired 默认按类型装配,当出现相同类型的 bean,使用 @Primary 提高按类型自动装配的优先级,多个 @Primary 会导致优先级设置无效 @@ -4343,6 +4345,8 @@ public class ClassName { +**** + ##### 应用场景 @@ -4493,9 +4497,11 @@ pom.xml #### 核心类 +##### BeanFactory + ApplicationContext: -1. ApplicationContext 是一个接口,提供了访问 Spring 容器的API +1. ApplicationContext 是一个接口,提供了访问 Spring 容器的 API 2. ClassPathXmlApplicationContext 是一个类,实现了上述功能 @@ -4512,22 +4518,30 @@ ApplicationContext: * 继承 MessageSource,因此支持国际化 * 资源文件访问,如 URL 和文件(ResourceLoader)。 - * 载入多个(有继承关系)上下文(即同时加载多个配置文件) ,使得每一个上下文都专注于一个特定的层次,比如应用的 web 层 + * 载入多个(有继承关系)上下文(即加载多个配置文件) ,使得每一个上下文都专注于一个特定的层次,比如应用的 web 层 * 提供在监听器中注册 bean 的事件 * BeanFactory 创建的 bean 采用延迟加载形式,只有在使用到某个 Bean 时(调用 getBean),才对该 Bean 进行加载实例化(Spring 早期使用该方法获取 bean),这样就不能提前发现一些存在的 Spring 的配置问题;ApplicationContext 是在容器启动时,一次性创建了所有的 Bean,容器启动时,就可以发现 Spring 中存在的配置错误,这样有利于检查所依赖属性是否注入 * ApplicationContext 启动后预载入所有的单实例 Bean,所以程序启动慢,运行时速度快 * 两者都支持 BeanPostProcessor、BeanFactoryPostProcessor 的使用,但两者之间的区别是:BeanFactory 需要手动注册,而 ApplicationContext 则是自动注册 -FileSystemXmlApplicationContext:加载文件系统中任意位置的配置文件,而 ClassPathXmlApplicationContext 只能加载类路径下的配置文件 +FileSystemXmlApplicationContext:加载文件系统中任意位置的配置文件,而 ClassPathXmlAC 只能加载类路径下的配置文件 ![](https://gitee.com/seazean/images/raw/master/Frame/ApplicationContext层级结构图.png) -**BeanFactory的基本使用**: +BeanFactory 的成员属性: ```java -String FACTORY_BEAN_PREFIX = "&";//获取工厂Bean本身,在Bean的id前加此符号 +String FACTORY_BEAN_PREFIX = "&"; ``` +* 区分是 FactoryBean 还是创建的 Bean,加上 & 代表是工厂,getBean 将会返回工厂 +* FactoryBean:如果某个 bean 的配置非常复杂,或者想要使用编码的形式去构建它,可以提供一个构建该 bean 实例的工厂,这个工厂就是 FactoryBean 接口实现类,FactoryBean 接口实现类也是需要 Spring 管理 + * 这里产生两种对象,一种是 FactoryBean 接口实现类(IOC 管理),另一种是 FactoryBean 接口内部管理的对象 + * 获取 FactoryBean 接口实现类,使用 getBean 时传的 beanName 需要带 & 开头 + * 获取 FactoryBean 内部管理的对象,不需要带 & 开头 + +BeanFactory 的基本使用: + ```java Resource res = new ClassPathResource("applicationContext.xml"); BeanFactory bf = new XmlBeanFactory(res); @@ -4536,6 +4550,50 @@ UserService userService = (UserService)bf.getBean("userService"); +**** + + + +##### FactoryBean + +繁琐的 bean 初始化过程处理: + +* FactoryBean:对单一的 bean 的初始化过程进行封装,达到简化配置的目的 + +FactoryBean与 BeanFactory 区别: + +- FactoryBean:封装单个 bean 的创建过程 + +- BeanFactory:Spring 容器顶层接口,定义了 bean 相关的获取操作 + +代码实现: + +* FactoryBean,实现类一般是 MapperFactoryBean,创建 DAO 层接口的实现类 + + ```java + public class EquipmentDaoImplFactoryBean implements FactoryBean { + @Override //获取Bean + public Object getObject() throws Exception { + return new EquipmentDaoImpl(); + } + + @Override //获取bean的类型 + public Class getObjectType() { + return null; + } + + @Override //是否单例 + public boolean isSingleton() { + return false; + } + } + ``` + + + + + + *** @@ -4562,7 +4620,7 @@ UserService userService = (UserService)bf.getBean("userService"); } ``` -* Service业务层 +* Service 业务层 ```java public interface UserService { @@ -4583,7 +4641,12 @@ UserService userService = (UserService)bf.getBean("userService"); } ``` - + + + +*** + + ##### 过滤器 @@ -4719,13 +4782,13 @@ UserService userService = (UserService)bf.getBean("userService"); #### 注册器 -可以取代ComponentScan扫描器 +可以取代 ComponentScan 扫描器 名称:ImportBeanDefinitionRegistrar 类型:**接口** -作用:自定义bean定义注册器 +作用:自定义 bean 定义注册器 * registrar / MyImportBeanDefinitionRegistrar @@ -4774,29 +4837,30 @@ UserService userService = (UserService)bf.getBean("userService"); #### 处理器 -BeanPostProcessor:bean后置处理器,bean创建对象初始化前后进行拦截工作的 +通过创建类**继承相应的处理器的接口**,重写后置处理的方法,来实现**拦截 Bean** 的生命周期来实现自己自定义的逻辑 -BeanFactoryPostProcessor:beanFactory的后置处理器 +BeanPostProcessor:bean 后置处理器,bean 创建对象初始化前后进行拦截工作的 - * 加载时机:在BeanFactory初始化之后调用,来定制和修改BeanFactory的内容;所有的bean定义已经保存加载到beanFactory,但是bean的实例还未创建 +BeanFactoryPostProcessor:beanFactory 的后置处理器 + + * 加载时机:在 BeanFactory 初始化之后调用,来定制和修改 BeanFactory 的内容;所有的 bean 定义已经保存加载到 beanFactory,但是 bean 的实例还未创建 * 执行流程: - * ioc容器创建对象 - * invokeBeanFactoryPostProcessors(beanFactory):执行BeanFactoryPostProcessor - * 在BeanFactory中找到所有类型是BeanFactoryPostProcessor的组件,并执行它们的方法 + * ioc 容器创建对象 + * invokeBeanFactoryPostProcessors(beanFactory):执行 BeanFactoryPostProcessor + * 在 BeanFactory 中找到所有类型是 BeanFactoryPostProcessor 的组件,并执行它们的方法 * 在初始化创建其他组件前面执行 BeanDefinitionRegistryPostProcessor: -* 加载时机:在所有bean定义信息将要被加载,但是bean实例还未创建,优先于BeanFactoryPostProcessor执行;利用BeanDefinitionRegistryPostProcessor给容器中再额外添加一些组件 +* 加载时机:在所有 bean 定义信息将要被加载,但是 bean 实例还未创建,优先于 BeanFactoryPostProcessor 执行;利用 BeanDefinitionRegistryPostProcessor 给容器中再额外添加一些组件 * 执行流程: - * ioc容器创建对象 - * refresh() --> invokeBeanFactoryPostProcessors(beanFactory) - * 从容器中获取到所有的BeanDefinitionRegistryPostProcessor组件 - * 依次触发所有的postProcessBeanDefinitionRegistry()方法 - * 再来触发postProcessBeanFactory()方法 - * 从容器中找到BeanFactoryPostProcessor组件;然后依次触发postProcessBeanFactory()方法 + * ioc 容器创建对象 + * refresh() → invokeBeanFactoryPostProcessors(beanFactory) + * 从容器中获取到所有的 BeanDefinitionRegistryPostProcessor 组件 + * 依次触发所有的 postProcessBeanDefinitionRegistry() 方法 + * 再来触发 postProcessBeanFactory() 方法 @@ -4839,9 +4903,11 @@ public class MyApplicationListener implements ApplicationListener afterSingletonsInstantiated() +原理:使用 EventListenerMethodProcessor 处理器来解析方法上的 @EventListener,Spring 扫描使用注解的方法,并为之创建一个监听对象 - * ioc容器创建对象并refresh() - * finishBeanFactoryInitialization(beanFactory:初始化剩下的单实例bean - * 先创建所有的单实例bean:getBean() - * 获取所有创建好的单实例bean,判断是否是SmartInitializingSingleton类型的,如果是就调用afterSingletonsInstantiated() +SmartInitializingSingleton 原理:→ afterSingletonsInstantiated() + * ioc 容器创建对象并 refresh() + * finishBeanFactoryInitialization(beanFactory):初始化剩下的单实例 bean + * 先创建所有的单实例 bean:getBean() + * 获取所有创建好的单实例 bean,判断是否是 SmartInitializingSingleton 类型的,如果是就调用 afterSingletonsInstantiated() -*** - - - -#### FactoryBean - -繁琐的bean初始化过程处理: - -* FactoryBean:对单一的 bean 的初始化过程进行封装,达到简化配置的目的 - -FactoryBean与BeanFactory 区别: -- FactoryBean:封装单个 bean 的创建过程 - -- BeanFactory:Spring容器顶层接口,定义了 bean 相关的获取操作 - -代码实现: - -* FactoryBean,实现类一般是 MapperFactoryBean - - ```java - public class EquipmentDaoImplFactoryBean implements FactoryBean { - @Override //获取Bean - public Object getObject() th rows Exception { - return new EquipmentDaoImpl(); - } - - @Override //获取bean的类型 - public Class getObjectType() { - return null; - } - - @Override //是否单例 - public boolean isSingleton() { - return false; - } - } - ``` - - - *** @@ -7258,855 +7287,6 @@ TransactionManagementConfigurationSelector类: (整理中,一周之内整理完成) -### XML - -三大对象: - -* **BeanDefinition**:是 Spring 中极其重要的一个概念,它存储了 bean 对象的所有特征信息,如是否单例、是否懒加载、factoryBeanName 等 - -* **BeanDefinationRegistry**:存放 BeanDefination 的容器,是一种键值对的形式,通过特定的 Bean 定义的id,映射到相应的 BeanDefination - -* **BeanDefinitionReader**:读取配置文件,比如 xml 用 dom4j 解析,配置文件用 io 流 - -程序: - -```java -BeanFactory bf = new XmlBeanFactory(new ClassPathResource("applicationContext.xml")); -UserService userService1 = (UserService)bf.getBean("userService"); -``` - -源码解析: - -```java -public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) { - super(parentBeanFactory); - this.reader.loadBeanDefinitions(resource); -} -public int loadBeanDefinitions(Resource resource) { - //将 resource 包装成带编码格式的 EncodedResource - //EncodedResource 中 getReader()方法,调用java.io包下的 转换流 创建指定编码的输入流对象 - return loadBeanDefinitions(new EncodedResource(resource)); -} -``` - -* XmlBeanDefinitionReader.loadBeanDefinitions():把 Resource 解析成 BeanDefinition 对象 - - * `currentResources = this.resourcesCurrentlyBeingLoaded.get()`:拿到当前线程已经加载过的所有 EncodedResoure 资源,用 ThreadLocal 保证线程安全 - * `if (currentResources == null)`:判断 currentResources 是否为空,为空则进行初始化 - * `if (!currentResources.add(encodedResource))`:如果已经加载过该资源会报错,防止重复加载 - * `inputSource = new InputSource(inputStream)`:资源对象包装成 InputSource,InputSource使 SAX 中的资源对象,用来进行 XML 文件的解析 - * `return doLoadBeanDefinitions()`:**加载返回** - * `currentResources.remove(encodedResource)`:加载完成移除当前 encodedResource - * `resourcesCurrentlyBeingLoaded.remove()`:ThreadLocal 为空时移除元素,防止内存泄露 - -* XmlBeanDefinitionReader.doLoadBeanDefinitions(inputSource, resource):真正的加载函数 - - `Document doc = doLoadDocument(inputSource, resource)`:转换成有层次结构的 Document 对象 - - * `getEntityResolver()`:获取用来解析 DTD、XSD 约束的解析器 - - * `getValidationModeForResource(resource)`:获取验证模式 - - `int count = registerBeanDefinitions(doc, resource)`:将 Document 解析成 BD 对象,注册(添加)到 BeanDefinationRegistry 中,返回新注册的数量 - - * `createBeanDefinitionDocumentReader()`:创建 DefaultBeanDefinitionDocumentReader 对象 - * `getRegistry().getBeanDefinitionCount()`:获取解析前 BeanDefinationRegistry 中的 bd 数量 - * `registerBeanDefinitions(doc, readerContext)`:注册 BD - * `this.readerContext = readerContext`:保存上下文对象 - * `doRegisterBeanDefinitions(doc.getDocumentElement())`:真正的注册 BD 函数 - * `doc.getDocumentElement()`:拿出顶层标签 - * `return getRegistry().getBeanDefinitionCount() - countBefore`:返回新加入的数量 - -* DefaultBeanDefinitionDocumentReader.doRegisterBeanDefinitions():注册 BD 到 BR - - * `createDelegate(getReaderContext(), root, parent)`:beans 是标签的解析器对象 - * `delegate.isDefaultNamespace(root)`:判断 beans 标签是否是默认的属性 - * `root.getAttribute(PROFILE_ATTRIBUTE)`:解析 profile 属性 - * `parseBeanDefinitions(root, this.delegate)`:**解析 beans 标签中的子标签** - * `parseDefaultElement(ele, delegate)`:如果是默认的标签,用该方法解析子标签 - * 判断标签名称,进行相应的解析 - * `processBeanDefinition(ele, delegate)`:解析 bean 标签 - * `delegate.parseCustomElement(ele)`:解析自定义的标签 - -* DefaultBeanDefinitionDocumentReader.processBeanDefinition():解析 bean 并注册到注册中心 - - * `delegate.parseBeanDefinitionElement(ele)`:解析 bean 标签封装为 BeanDefinitionHolder - - * `if (!StringUtils.hasText(beanName) && !aliases.isEmpty())`:条件一成立说明 name 没有值,条件二成立说明别名有值, - - `beanName = aliases.remove(0)`:拿别名列表的第一个元素当作 beanName - - * `parseBeanDefinitionElement(ele, beanName, containingBean)`:**解析 bean 标签** - - * `parseState.push(new BeanEntry(beanName))`:当前解析器的状态设置为 BeanEntry - * class 和 parent 属性存在一个,parent 是作为父标签为了被继承 - * `createBeanDefinition(className, parent)`:设置了class 的 GenericBeanDefinition对象 - * `parseBeanDefinitionAttributes()`:解析 bean 标签的属性 - * 接下来解析子标签 - - * `beanName = this.readerContext.generateBeanName(beanDefinition)`:生成 className + # + 序号的名称赋值给 beanName - - * `return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray)`:包装成 BeanDefinitionHolder 对象 - - * `registerBeanDefinition(bdHolder, getReaderContext().getRegistry())`:**注册到容器** - - * `beanName = definitionHolder.getBeanName()`:获取beanName - * `this.beanDefinitionMap.put(beanName, beanDefinition)`:添加到注册中心 - - * `getReaderContext().fireComponentRegistered`:发送注册完成事件 - - - - - -**** - - - -### 容器 - -Spring ioc 容器就是很多 Map 集合,保存了单实例 Bean,环境信息等资源 - -Spring 容器的启动流程:三个步骤 - -ClassPathXmlApplicationContext 同 AnnotationConfigApplicationContext - -```java -public AnnotationConfigApplicationContext(Class... annotatedClasses) { - this();// 注册 Spring 内置后置处理器的 BeanDefinition 到容器 - register(annotatedClasses);// 注册配置类 BeanDefinition 到容器 - refresh();// 加载或者刷新容器中的Bean -} -``` - -1. Spring容器的初始化时,通过this()调用了无参构造函数 - - `this.reader = new AnnotatedBeanDefinitionReader(this)`:实例化BeanDefinitionReader注解配置读取器,用于对特定注解(如@Service、@Repository)的类进行读取转化成BeanDefinition对象 - - * `AnnotationConfigUtils.registerAnnotationConfigProcessors`:向容器添加内置组件 - * ConfigurationClassPostProcessor:beanFactory 后置处理器,用来完成 bean 的扫描与注入工作AutowiredAnnotationBeanPostProcessor:bean 后置处理器,用来完成@AutoWired自动注入 - BeanPostProcessor很多相关的后置处理器 - - `this.scanner = new ClassPathBeanDefinitionScanner(this)`:实例化路径扫描器,用于对指定的包目录进行扫描查找bean对象 - -2. 解析传入的 Spring 配置类,构造成一个 BeanDefinition 并注册到容器 - - * 解析传入的配置类,也可以解析bean对象 - * 根据类上有没有 @Conditional 注解判断是否需要跳过 - * 处理类上的通用注解:Lazy、Primary、DependsOn等 - * 封装成一个 BeanDefinitionHolder,并注册到容器 - -3. refresh()容器刷新流程:`AbstractApplicationContext.refresh()` - - 1. **prepareRefresh()**:刷新前的预处理 - * initPropertySources():初始化一些属性设置,子类自定义个性化的属性设置方法 - * getEnvironment().validateRequiredProperties():检验属性的合法等 - * earlyApplicationEvents= new LinkedHashSet():保存容器中早期的事件 - - 2. **obtainFreshBeanFactory()**:获取在容器初始化时创建的BeanFactory - - * refreshBeanFactory():创建BeanFactory,设置序列化ID、读取BeanDefinition并加载到工厂 - * getBeanFactory():返回创建的DefaultListableBeanFactory对象 - 3. **prepareBeanFactory(beanFactory)**:BeanFactory的预准备工作,向容器中添加一些组件 - * 设置BeanFactory的类加载器、设置表达式解析器、属性编辑器 - * 添加BeanPostProcessor:ApplicationContextAwareProcessor - * 设置忽略自动装配的接口,注册可以解析的自动装配类,即是否在任意组件中通过注解自动注入 - * 添加BeanPostProcessor:ApplicationListenerDetector监听器添加编译时的AspectJ,给BeanFactory中注册的3个组件 - - 4. **postProcessBeanFactory(beanFactory)**:BeanFactory准备工作完成后进行的后置处理工作, - 子类通过重写这个方法来在BeanFactory创建并预准备完成以后做进一步的设置 - - * 以上是BeanFactory的创建及预准备工作 - - 5. **invokeBeanFactoryPostProcessors(beanFactory)**:执行BeanFactoryPostProcessor的方法 - - 首先回调 postProcessBeanDefinitionRegistry() 方法,然后再回调 postProcessBeanFactory() 方法 - - * 先获取实现BeanDefinitionRegistryPostProcessor或BeanFactoryPostProcessor接口类型 - * 先执行实现了PriorityOrdered优先级接口的PostProcessor - * 再执行实现了Ordered顺序接口的PostProcessor - * 最后执行没有实现任何优先级或者是顺序接口PostProcessor - - 6. **registerBeanPostProcessors(beanFactory)**:注册Bean的后置处理器,为了干预Spring初始化bean的流程,从而完成代理、自动注入、循环依赖等功能(intercept bean creation) - 注意:这里仅仅是向容器中注入而非使用 - - * 获取所有实现了BeanPostProcessor接口类型的集合 - * 注册顺序:注册实现PriorityOrdered优先级接口 -->Ordered优先级接口 --> 没有实现任何优先级接口 --> MergedBeanDefinitionPostProcessor类型的BeanPostProcessor - * 给容器注册ApplicationListenerDetector:用于在Bean创建完成后检查是否ApplicationListener,如果是,就把Bean放到监听器容器中保存起来 - - 7. **initMessageSource()**:初始化MessageSource组件,主要用于做国际化功能,消息绑定与消息解析 - - * 判断BeanFactory容器中是否有id为messageSource 并且类型是MessageSource的组件: - * 如果有,直接赋值给messageSource;如果没有,则创建一个DelegatingMessageSource注入 - - 8. **initApplicationEventMulticaster()**:初始化事件派发器,在注册监听器时会用到 - - * 判断BeanFactory容器中是否存在自定义的ApplicationEventMulticaster - * 如果有,直接从容器中获取;如果没有,则创建一个SimpleApplicationEventMulticaster注入 - - 9. **onRefresh()**:留给子容器、子类重写这个方法,在容器刷新的时候可以自定义逻辑 - - 10. **registerListeners()**:注册监听器 - - * 将容器中所有的ApplicationListener注册到事件派发器:addApplicationListenerBean(listener) - * 派发之前步骤产生的事件applicationEvents:multicastEvent(earlyEvent) - - 11. **finishBeanFactoryInitialization()**:初始化所有剩下的单实例bean - - * `().preInstantiateSingletons()`:Instantiate all remaining (non-lazy-init) singletons - * 获取Bean的定义信息RootBeanDefinition,它表示自己的BeanDefinition和可能存在父类的BeanDefinition合并后的对象 - * Bean满足非抽象的,单实例,非懒加载,则执行单例Bean创建流程:**getBean()**,详细步骤在Bean - * 检查所有的Bean是否实现SmartInitializingSingleton接口,是则执行afterSingletonsInstantiated(),进行一些创建后的操作 - - 12. **finishRefresh()**:发布BeanFactory容器刷新完成事件 - - * initLifecycleProcessor():初始化和生命周期有关的后置处理器,容器中有就返回,没有就new - * getLifecycleProcessor().onRefresh():获取该生命周期后置处理器 (BeanFactory) 回调onRefresh() - * publishEvent(new ContextRefreshedEvent(this)):发布容器刷新完成事件 - * liveBeansView.registerApplicationContext(this):暴露 Mbean - -**总结:** - -* Spring容器在启动的时候,先会保存所有注册进来的Bean的定义信息 - - * xml注册bean; - * 注解注册Bean;@Service、@Component、@Bean、xxx - -* Spring容器会寻找合适的时机创建这些Bean - - * 用到bean的时候,利用getBean创建bean,创建好以后保存在容器中 - * 统一创建剩下所有的bean的时候;finishBeanFactoryInitialization() - -* 后置处理器:BeanPostProcessor - - 每一个bean创建完成,都会使用各种后置处理器进行处理、增强bean的功能: - - * AutowiredAnnotationBeanPostProcessor:处理自动注入 - * AnnotationAwareAspectJAutoProxyCreator:来做AOP功能 - -* 事件驱动模型 - - * ApplicationListener:事件监听 - * ApplicationEventMulticaster:事件派发 - - - -*** - - - -### Bean - -#### 生命周期 - -单实例:在容器启动时创建对象 - -多实例:在每次获取的时候创建对象 - -Bean 的获取:**获取 Bean 时先从单例池获取,如果没有则进行第二次获取,带上工厂类创建并添加至单例池** - -Bean 的生命周期:实例化 instantiation,填充属性 populate,初始化 initialization,销毁 destruction - -![](https://gitee.com/seazean/images/raw/master/Frame/Spring-getBean.png) - -![](https://gitee.com/seazean/images/raw/master/Frame/Sprin-AOP+循环依赖.png) - - - -参考视频:https://www.bilibili.com/video/BV1ET4y1N7Sp - - - -*** - - - -#### 源码解析 - -Java 启动 Spring代码: - -```java -ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); -``` - -* 获取 `AbstractBeanFactory.doGetBean` - - **第一次查询**:`DefaultSingletonBeanRegistry.getSingleton()`:从缓存池获取,获取不到继续进行 - - * 获取到以后进行实例化,然后直接return 循环依赖阶段有代码详解 - - 查询成功:`getObjectForBeanInstance`获取给定bean实例的对象,最后详解 - - **第二次查询**:`DefaultSingletonBeanRegistry.getSingleton(String, ObjectFactory)` - - * `singletonObjects.get(beanName)`:检查是否已经被加载,单例模式复用已经创建的bean - * `beforeSingletonCreation(beanName)`:初始化前操作,校验是否 beanName 是否有别的线程在初始化,并记录beanName的初始化状态 - * `singletonObject = singletonFactory.getObject()`:**实例化 bean**,第二阶段详解 - * **创建完成以后,Bean已经填充好,是一个完整的可使用的Bean** - * `afterSingletonCreation(beanName)`:初始化后的操作,移除初始化状态 - * `addSingleton(beanName, singletonObject)`:添加单例池,从二级三级缓存移除 - - **获取对象**:`getObjectForBeanInstance`获取给定bean实例的对象,从缓存中获取的bean是原始状态 - - * 验证 bean 类型:判断是否是工厂bean,对非 FactoryBean 不做处理 - * 处理 FactoryBean:先对bean进行转换,再委托给getObjectFromFactoryBean()方法进行处理 - - 依赖检查:`convertIfNecessary()`检查所需的类型是否与实际bean实例的类型匹配 - - ```java - sharedInstance = getSingleton(beanName, () -> { - return createBean(beanName, mbd, args);//创建,跳转第二阶段 - //lambda表达式,调用了ObjectFactory的getObject()方法,实际回调接口实现的是 createBean()方法进行创建对象 - }); - ``` - - - -* **实例化(创建)**AbstractAutowireCapableBeanFactory类 - - **前置处理**:`createBean().resolveBeforeInstantiation()`BeanPostProcessor拦截进行前置处理 - - * 前置处理:`postProcessBeforeInstantiation()`,**注解AOP部分详解,用来执行处理器** - - * 有返回值触发:`postProcessAfterInitialization()`返回代理对象 - * 如果这个bean没有特定的前置处理,那说明这个bean是一个普通的bean,执行doGetBean() - - **开始创建**:`doCreateBean(beanName, RootBeanDefinition, Object[] args)` - - * 清除缓存:如果bean是单例,就先清除缓存中的bean信息 - - * **创建实例**:`createBeanInstance(beanName, RootBeanDefinition, Object[] args)` - - * 优先级从高到低:工厂方法、有参**构造函数**、无参构造函数 - * Spring给所有创建的Bean实例包装成BeanWrapper,BeanWrapper是对反射相关API的简单封装,使得上层使用反射完成相关的业务逻辑大大简化 - - * 后置处理:`applyMergedBeanDefinitionPostProcessors()` - - * 将所有的后置处理器拿出来,并且把名字叫beanName的类中的变量都封装到InjectionMetadata的injectedElements集合里面,目的是以后从中获取,创建实例,通过反射注入到相应类 - * `AutowiredAnnotationBeanPostProcessor.postProcessMergedBeanDefinition` - - * 添加工厂:`DefaultSingletonBeanRegistry.addSingletonFactory()` - - * 允许提前引用才执行,用来解决**循环依赖** - - * **填充属性 (依赖注入)**:`populateBean(beanName, RootBeanDefinition, BeanWrapper)` - - * 填充准备:通过awareBeanPostProcessor拦截,判断控制程序是否继续进行属性填充 - * 获取依赖:根据autowire类型 (Type/Name)提取依赖,存入PropertyValues并给bean注册依赖 - * 后置处理:判断是否需要进行 BeanPostProcessor 和 依赖检查 - - * `postProcessProperties`:转入**AutowiredAnnotationBeanPostProcessor(注解**) - - * `findAutowiringMetadata()`:找到需要注入的元数据 - * `InjectionMetadata.InjectedElement.inject()`:注入数据(重写实现,注入变量或方法) - * `DefaultListableBeanFactory.resolveDependency()`:解决依赖 - * `doResolveDependency().resolveCandidate()`:通过工厂获取Bean对象 - * `registerDependentBeans()`:**将Bean注册为Autowired自动装配的Bean** - * `ReflectionUtils.makeAccessible()`:修改访问权限,true代表暴力破解 - * `method.invoke()`:利用反射为此对象赋值 - * 填充属性:`applyPropertyValues()`,将所有解析的PropertyValues的属性填充至BeanWrapper - - * **初始化**:`initializeBean(String, Object, RootBeanDefinition)` - - * 填充Aware接口属性:`invokeAwareMethods(beanName,bean)` - * BeanName、ClassLoader对象实例、Spring工厂、Spring上下文ApplicationContext - * 前置处理:`applyBeanPostProcessorsBeforeInitialization()` - * 需要在Bean初始化前进行一些自定义的前置处理,让Bean实现BeanPostProcessor接口 - * 自定义init方法调用:`AbstractAutowireCapableBeanFactory.invokeInitMethods()` - * 如果Bean实现了InitializingBean接口,执行afeterPropertiesSet()方法 - * 如果Bean在Spring配置文件中配置了 init-method属性,则会自动调用其配置的初始化方法 - * 后置处理:`applyBeanPostProcessorsAfterInitialization()` - * **AOP,跳转注解**,`AbstractAutoProxyCreatorwrapIfNecessary -> creatProxy` - * 如果不存在循环依赖,动态代理在此处完成,否则会提前创建 - - * 循环依赖检查:如果存在循环依赖,在属性填充阶段会生成Bean对象的动态代理,则缓存中放置了提前生成的代理对象,然后使用原始bean继续执行初始化,所以返回最终bean前,把原始bean置换为代理对象返回 - - 存在循环依赖,在初始化的后置处理中不会重新创建代理对象,真正创建动态代理Bean的阶段是在获取提前引用阶段,**循环依赖**详解,看后置处理源码: - - ```java - public Object postProcessAfterInitialization() {。。。。。 - //去提前代理引用池中寻找该key,如果存在就不会创建动态代理 - Object cacheKey = getCacheKey(bean.getClass(), beanName); - if (this.earlyProxyReferences.remove(cacheKey) != bean) {//不成立 - return wrapIfNecessary(bean, beanName, cacheKey); - //。。。。。。 - ``` - - * **注册销毁**:`AbstractBeanFactory.registerDisposableBeanIfNecessary`, - - * 根据不同的scope进行disposableBean的注册,在销毁对象时调用destory() - - - -**** - - - -### 依赖 - -循环依赖:是一个或多个对象实例之间存在直接或间接的依赖关系,这种依赖关系构成一个环形调用 - -Spring 循环依赖有三种: - -* 原型模式循环依赖【无法解决】 -* 单例Bean循环依赖-构造参数产生依赖【无法解决】 -* 单例Bean循环依赖-setter产生依赖【可以解决】 - -解决循环依赖:提前引用,提前暴露创建中的 Bean - -* 循环依赖的三级缓存: - - ```java - //一级缓存:存放所有初始化完成单实例bean,单例池,key是beanName,value是对应的单实例对象引用 - private final Map singletonObjects = new ConcurrentHashMap<>(256); - - //二级缓存:存放实例化未进行初始化的Bean,提前引用池 - private final Map earlySingletonObjects = new HashMap<>(16); - - /** Cache of singleton factories: bean name to ObjectFactory. 3*/ - private final Map> singletonFactories = new HashMap<>(16); - ``` - - 为什么需要三级缓存? - - * 循环依赖解决需要提前引用动态代理对象,AOP 动态代理是在 Bean 初始化后的后置处理中进行,这时的 bean 已经是成品对象,需要提前进行动态代理,三级缓存的 ObjectFactory 提前产生需要代理的对象 - * 若存在循环依赖,**后置处理不创建代理对象,真正创建代理对象的过程是在getBean(B)的阶段中** - - 一定会提前引用吗? - - * 出现循环依赖才去使用,不出现就不使用 - - wrapIfNecessary一定创建代理对象吗? - - * 存在增强器会创建动态代理,不需要增强就不需要创建动态代理对象 - * 不创建就会把最原始的实例化的Bean放到二级缓存,因为 addSingletonFactory 参数中传入了实例化的Bean,在singletonFactory.getObject()中返回给singletonObject,放入二级缓存 - - 什么时候将Bean的引用提前暴露给第三级缓存的ObjectFactory持有? - - * 实例化之后,依赖注入之前 - - ```java - createBeanInstance --> addSingletonFactory --> populateBean - ``` - -解决循环依赖,源码解析: - -* 假如 A 依赖 B,B 依赖 A - - 当A创建实例后填充属性前,执行`addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));`方法,注意lambda表达式,getObject()时调用 - - ````java - //添加给定的单例工厂以构建指定的单例 - protected void addSingletonFactory(String beanName, ObjectFactory singletonFactory) { - Assert.notNull(singletonFactory, "Singleton factory must not be null"); - synchronized (this.singletonObjects) { - //单例池包含该Bean说明已经创建完成,不需要循环依赖 - if (!this.singletonObjects.containsKey(beanName)) { - this.singletonFactories.put(beanName,singletonFactory);//加入三级缓存 - this.earlySingletonObjects.remove(beanName); - //从二级缓存移除,因为三个Map中都是一个对象,不能同时存在 - this.registeredSingletons.add(beanName); - } - } - } - ```` - - 填充属性时A依赖B,这时需要getBean(B),接着B填充属性时发现依赖A,去进行**第一次**getSingleton(A) - - (待补充源码) - - 从三级缓存获取A的Bean:`singletonFactory.getObject();`,相当于调用了Lambda表达式的方法: - - ```java - protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { - //....省略 - exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName); - return exposedObject; - } - ``` - - 追踪getEarlyBeanReference(exposedObject, beanName), - - ```java - public Object getEarlyBeanReference(Object bean, String beanName) { - Object cacheKey = getCacheKey(bean.getClass(), beanName); - this.earlyProxyReferences.put(cacheKey, bean); - //提前引用代理池earlyProxyReferences中添加该Bean - return wrapIfNecessary(bean, beanName, cacheKey); - //创建代理对象,createProxy - } - ``` - - wrapIfNecessary - - ```java - // Create proxy if we have advice.获取增强方法 - Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);/// - if (specificInterceptors != DO_NOT_PROXY) { - Object proxy = createProxy(。。。。);。。。 - return proxy; - } - this.advisedBeans.put(cacheKey, Boolean.FALSE); - return bean; - ``` - - - - -*** - - - -### 注解 - -#### Component - -@Component和@Service都是常用的注解,Spring如何解析? - -* **@Component解析流程:** - - 打开源码注释:@see org.....ClassPathBeanDefinitionScanner.doScan() - - findCandidateComponents():从classPath扫描组件,并转换为备选BeanDefinition - - ```java - protected Set doScan(String... basePackages) { - Set beanDefinitions = new LinkedHashSet<>(); - for (String basePackage : basePackages) { - //findCandidateComponents 读资源装换为BeanDefinition - Set candidates = findCandidateComponents(basePackage); - for (BeanDefinition candidate : candidates) {//....} - //....... - return beanDefinitions; - } - ``` - - ClassPathScanningCandidateComponentProvider.findCandidateComponents() - - ```java - public Set findCandidateComponents(String basePackage) { - if (this.componentsIndex != null && indexSupportsIncludeFilters()) { - return addCandidateComponentsFromIndex(this.componentsIndex, basePackage); - } - else { - return scanCandidateComponents(basePackage); - } - } - ``` - - ```java - private Set scanCandidateComponents(String basePackage) {} - ``` - - * `String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX resolveBasePackage(basePackage) + '/' + this.resourcePattern` :将package转化为ClassLoader类资源搜索路径packageSearchPath,例如:`com.wl.spring.boot`转化为`classpath*:com/wl/spring/boot/**/*.class` - * `Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath)`:加载搜素路径下的资源 - * `MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource)`:获取元数据阅读器 - * isCandidateComponent:判断是否是备选组件 - * candidates.add(sbd):添加到返回结果的list - - isCandidateComponent源码: - - ```java - protected boolean isCandidateComponent(MetadataReader m) throws IOException { - //.... - for (TypeFilter tf : this.includeFilters) { - if (tf.match(m, getMetadataReaderFactory())) { - return isConditionMatch(metadataReader); - //.... - } - ``` - - ```java - protected void registerDefaultFilters() { - this.includeFilters.add(new AnnotationTypeFilter(Component.class));//... - } - ``` - - includeFilters由`registerDefaultFilters()`设置初始值,有@Component,没有@Service - - 因为@Component是@Service的元注解,Spring在读取@Service,也读取了它的元注解,并将@Service作为@Component处理 - - ```java - @Target({ElementType.TYPE}) - @Retention(RetentionPolicy.RUNTIME) - @Documented - @Component - public @interface Service {} - ``` - -* **@Component派生性流程:** - - metadataReader本质上:`MetadataReader metadataReader =new SimpleMetadataReader(...);` - - `isCandidateComponent.match()`方法:`TypeFilter.match` -->`AnnotationTypeFilter.matchSelf()` - - ```java - @Override - protected boolean matchSelf(MetadataReader metadataReader) { - AnnotationMetadata metadata = metadataReader.getAnnotationMetadata(); - return metadata.hasAnnotation(this.annotationType.getName()) || - (this.considerMetaAnnotations && metadata.hasMetaAnnotation(this.annotationType.getName())); - } - ``` - - * `metadata = new SimpleMetadataReader(...).getAnnotationMetadata()` - - ```java - @Override - public AnnotationMetadata getAnnotationMetadata() { - return this.annotationMetadata; - } - ``` - - 观察源码:`annotationMetadata = new AnnotationMetadataReadingVisitor(classLoader);` - - * `metadata.hasMetaAnnotation=AnnotationMetadataReadingVisitor.hasMetaAnnotation` - - 判断该注解的元注解在不在metaAnnotationMap中,如果在就返回true - - ```java - @Override - public boolean hasMetaAnnotation(String metaAnnotationType) { - Collection> allMetaTypes = this.metaAnnotationMap.values(); - for (Set metaTypes : allMetaTypes) { - if (metaTypes.contains(metaAnnotationType)) { - return true; - } - } - return false; - } - ``` - - metaAnnotationMap怎么赋值的? - - metaAnnotationMap赋值方法在`SimpleMetadataReader.SimpleMetadataReader`中: - - ```java - classReader.accept(visitor, ClassReader.SKIP_DEBUG); - ``` - - 然后通过readElementValues方法中: - - ```java - annotationVisitor.visitEnd(); - ``` - - 追踪方法:`AnnotationAttributesReadingVisitor.visitEnd()` - - 递归读取:内部方法`recursivelyCollectMetaAnnotations()`递归的读取注解,与注解的元注解(读@Service,再读元注解@Component), - - 添加数据:`this.metaAnnotationMap.put(annotationClass.getName(), metaAnnotationTypeNames);` - - - -*** - - - -#### Autowired - -打开@Autowired,注释上写Please consult the javadoc for the AutowiredAnnotationBeanPostProcessor - -AutowiredAnnotationBeanPostProcessor间接实现InstantiationAwareBeanPostProcessor,就具备了实例化前后 (而不是初始化前后) 管理对象的能力,实现了BeanPostProcessor,具有初始化前后管理对象的能力,实现BeanFactoryAware,具备随时拿到BeanFactory的能力,所以这个类**具备一切后置处理器的能力** - -**在容器启动,为对象赋值的时候,遇到@Autowired注解,会用后置处理器机制,来创建属性的实例,然后再利用反射机制,将实例化好的属性,赋值给对象上,这就是Autowired的原理** - -作用时机: - -* Spring容器在每个Bean实例化之后,调用AutowiredAnnotationBeanPostProcessor的`postProcessMergedBeanDefinition`方法,查找该Bean是否有@Autowired注解 - -* Spring在每个Bean调用`populateBean`进行属性注入的时候,即调用`postProcessProperties`方法,查找该Bean属性是否有@Autowired注解 - - - -*** - - - -#### AOP - -##### 注解 - -@EnableAspectJAutoProxy - -给容器中导入AspectJAutoProxyRegistrar:`@Import(AspectJAutoProxyRegistrar.class)` - -利用AspectJAutoProxyRegistrar给容器中注册**AnnotationAwareAspectJAutoProxyCreator**,以BeanDefiantion形式存在,在容器初始化时加载。 - -AnnotationAwareAspectJAutoProxyCreator是这种类型的后置处理器:InstantiationAwareBeanPostProcessor - -* InstantiationAwareBeanPostProcessor是在创建Bean实例之前进行拦截,会调用后置处理器来返回对象`postProcessBeforeInstantiation()` -* BeanPostProcessor是在Bean对象创建完成初始化前后调用 -* **该类会在bean的实例化和初始化的前后起作用** - - - -*** - - - -##### 实现流程 - -1. 创建IOC容器,调用refresh()刷新容器 - -2. registerBeanPostProcessors(beanFactory):注册bean的后置处理器来方便拦截bean的创建 - - **注册BeanPostProcessor,实际上就是创建BeanPostProcessor对象,保存在容器中** - - 向IOC容器中注入 BeanPostProcessor 后置处理器,AAAPC间接实现了Order接口: - - ```java - for (String ppName : orderedPostProcessorNames) { - BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class); - orderedPostProcessors.add(pp);//... - }//.... - registerBeanPostProcessors(beanFactory, orderedPostProcessors);//注册 - ``` - - 通过`getBean()`创建AnnotationAwareAspectJAutoProxyCreator对象,调用doGetBean() - - 在initializeBean()方法中执行invokeAwareMethods()方法`if (bean instanceof BeanFactoryAware)`条件判别成功,转入`AbstractAdvisorAutoProxyCreator.setBeanFactory`,代码跟踪转入`AnnotationAwareAspectJAutoProxyCreator.initBeanFactory()`进行工厂的初始化,至此创建成功 - -3. finishBeanFactoryInitialization(beanFactory):创建剩下的单实例bean - - `AbstractAutowireCapableBeanFactory.createBean().resolveBeforeInstantiation()`: - - `if(是否实现接口)`为真进入:`applyBeanPostProcessorsBeforeInstantiation`方法 - - * `this.advisedBeans.containsKey(cacheKey)`:判断当前bean是否在advisedBeans中(保存了所有需要增强bean) - -* `isInfrastructureClass`:判断当前bean是否是基础类型的Advice、Pointcut、Advisor、AopInfrastructureBean,或者(子类中)是否是切面Aspect - - * 是否需要跳过:子类`AspectJAwareAdvisorAutoProxyCreator.shouldSkip()` - - * `findCandidateAdvisors()`:获取候选的增强器(切面里面的通知方法)每一个封装的通知方法的增强器是 InstantiationModelAwarePointcutAdvisor**(AAAPC)** - - * `if()`:判断每一个增强器是否是 AspectJPointcutAdvisor 类型的,返回true,否则继续执行 - * `return super.shouldSkip(beanClass, beanName)`:永远返回false - * `getCustomTargetSource(beanClass, beanName)`:返回为空,doCreateBean() - - - -**进入applyBeanPostProcessorsAfterInitialization:后置处理器创建AOP** - -```java - //如果Bean被子类标识为要代理的bean,则使用配置的拦截器创建代理 - public Object postProcessAfterInitialization(@Nullable Object bean,String bN){ - if (bean != null) { - Object cacheKey = getCacheKey(bean.getClass(), beanName);//获取缓存key - if (this.earlyProxyReferences.remove(cacheKey) != bean) { - //去提前代理引用池中寻找该key,不存在则创建代理 - //如果存在则证明被代理过,则判断是否是当前的bean,不是则创建代理 - return wrapIfNecessary(bean, bN, cacheKey); - } - } - return bean; - } -``` - -创建动态代理:`wrapIfNecessary()`调用`createProxy()`(wrap包装) - -注释:Create proxy if we have advice - -* `getAdvicesAndAdvisorsForBean()`:获取当前bean的所有增强器 (通知方法),**为空就直接返回** - - * findEligibleAdvisors():找到哪些通知方法是需要切入当前bean方法的 - * AopUtils.findAdvisorsThatCanApply():获取到能在bean使用的增强器 - * sortAdvisors(eligibleAdvisors):给增强器排序 - -* `this.advisedBeans.put(cacheKey, Boolean.TRUE)`:保存当前bean在advisedBeans中 - -* `Object proxy = createProxy(...)`:如果增强器不为空就创建代理,创建当前bean的代理对象 - - * buildAdvisors(beanName, specificInterceptors):获取所有增强器(通知方法) - - * 保存到proxyFactory - - * `return proxyFactory.getProxy(getProxyClassLoader())`:返回代理对象 - - * ProxyFactory类:`return createAopProxy().getProxy(classLoader)` - - DefaultAopProxyFactory类:给容器中返回当前组件使用增强了的代理对象 - - ```java - @Override - public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { - if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) { - Class targetClass = config.getTargetClass(); - //根据是否实现接口选择哪种动态代理 - if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { - return new JdkDynamicAopProxy(config); - } - return new ObjenesisCglibAopProxy(config); - } - else { - return new JdkDynamicAopProxy(config); - } - } - ``` - -4. 给容器中返回使用cglib增强了的代理对象,**初始化完成,加入容器** - -5. 以后容器中获取到的就是这个组件的代理对象,执行目标方法的时候,代理对象就会执行通知方法的流程 - - - -*** - - - -##### 链式调用 - -容器中保存了组件的代理对象(cglib增强后的对象),这个对象里面保存了详细信息(增强器,目标对象等) - -1. `CglibAopProxy.intercept()`:拦截目标方法的执行(静态内部类) - -2. `List chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice()`: - 根据ProxyFactory对象获取将要执行的目标方法拦截器链 - - * `config.getAdvisors()`:获取所有拦截器,一个默认ExposeInvocationInterceptor和4个增强器 - - * `List interceptorList`:保存所有的拦截器 - - * `registry.getInterceptors(advisor)`:遍历所有的增强器,将其转为Interceptor - - 将增强器转为List: - - * 如果是MethodInterceptor,直接加入到集合中 - * 如果不是,使用AdvisorAdapter将增强器转为MethodInterceptor(适配器) - - **拦截器链**:每一个通知方法又被包装为方法拦截器,利用MethodInterceptor机制 - -3. 如果没有拦截器链,直接执行目标方法 - -4. 如果有拦截器链,把需要执行的目标对象、目标方法、拦截器链等信息传入CglibMethodInvocation 对象 - -5. `Object retVal = ReflectiveMethodInvocation.proceed`:拦截器链的触发过程 - - * 如果没有拦截器执行执行目标方法,或者拦截器的索引和拦截器数组-1大小一样(指定到了最后一个拦截器)执行目标方法 - * 链式获取每一个拦截器,拦截器执行invoke方法,每一个拦截器等待下一个拦截器执行完成返回以后再来执行;拦截器链的机制,保证通知方法与目标方法的执行顺序 - * 效果:图示先从上往下建立链,然后从下往上依次执行,责任链模式 - * 正常执行:(环绕通知)-> 前置通知 -> 目标方法 -> 后置通知 -> 返回通知 - * 出现异常:(环绕通知)-> 前置通知 -> 目标方法 -> 后置通知 -> 异常通知 - - ![](https://gitee.com/seazean/images/raw/master/Frame/Spring-AOP动态代理执行方法.png) - - - -*** - - - -#### Transactional - -如果一个类或者一个类中的 public 方法上被标注@Transactional 注解的话,Spring 容器就会在启动的时候为其创建一个代理类,在调用被@Transactional注解的 public 方法的时候,实际调用的是TransactionInterceptor类中的 invoke()方法。这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务 - -`TransactionInterceptor` 类中的 `invoke()`方法内部实际调用的是 `TransactionAspectSupport` 类的 `invokeWithinTransaction()`方法 - - - - - **** From 1bd8ecd9b5b97798214d68f7f4315f4b4fe8ac33 Mon Sep 17 00:00:00 2001 From: Seazean Date: Tue, 27 Jul 2021 18:08:38 +0800 Subject: [PATCH 002/168] Update Java Notes --- Java.md | 240 ++++++++++++++++++++++++++++++++++++++++++-------------- SSM.md | 126 ++++++++++++++++------------- 2 files changed, 253 insertions(+), 113 deletions(-) diff --git a/Java.md b/Java.md index 0c76ed3..8001612 100644 --- a/Java.md +++ b/Java.md @@ -2144,7 +2144,7 @@ abstract class Animal{ | 四种修饰符访问权限 | private | 缺省 | protected | public | | ------------------ | :-----: | :--: | :-------: | :----: | | 本类中 | √ | √ | √ | √ | -| 子类中 | X | √ | √ | √ | +| 本包下的子类中 | X | √ | √ | √ | | 本包下其他类中 | X | √ | √ | √ | | 其他包下的子类中 | X | X | √ | √ | | 其他包下的其他类中 | X | X | X | √ | @@ -2473,9 +2473,10 @@ s.replace("-","");//12378 #### 构造方式 构造方法: - `public String()` : 创建一个空白字符串对象,不含有任何内容 - `public String(char[] chs)` : 根据字符数组的内容,来创建字符串对象 - `public String(String original)` : 根据传入的字符串内容,来创建字符串对象 + +* `public String()` : 创建一个空白字符串对象,不含有任何内容 +* `public String(char[] chs)` : 根据字符数组的内容,来创建字符串对象 +* `public String(String original)` : 根据传入的字符串内容,来创建字符串对象 直接赋值:`String s = “abc”` 直接赋值的方式创建字符串对象,内容就是abc @@ -4411,11 +4412,12 @@ PriorityQueue 是优先级队列,底层存储结构为 Object[],默认实现 java.utils.Collections:集合**工具类**,Collections并不属于集合,是用来操作集合的工具类 Collections有几个常用的API: -`public static boolean addAll(Collection c, T... e)` : 给集合对象批量添加元素 -`public static void shuffle(List list)` : 打乱集合顺序。 -`public static void sort(List list)` : 将集合中元素按照默认规则排序。 -`public static void sort(List list,Comparator )` : 集合中元素按照指定规则排序 -`public static List synchronizedList(List list)` : 返回由指定 list 支持的线程安全 list +`public static boolean addAll(Collection c, T... e)`:给集合对象批量添加元素 +`public static void shuffle(List list)`:打乱集合顺序。 +`public static void sort(List list)`:将集合中元素按照默认规则排序。 +`public static void sort(List list,Comparator )`:集合中元素按照指定规则排序 +`public static List synchronizedList(List list)`:返回由指定 list 支持的线程安全 list +`public static Set singleton(T o)`:返回一个只包含指定对象的不可变组 ```java public class CollectionsDemo { @@ -4584,7 +4586,7 @@ HashMap 基于哈希表的 Map 接口实现,是以 key-value 存储形式存 特点: * HashMap的实现不是同步的,这意味着它不是线程安全的 -* key是唯一不重复的,底层的哈希表结构,依赖 hashCode 方法和 equals 方法保证键的唯一 +* key 是唯一不重复的,底层的哈希表结构,依赖 hashCode 方法和 equals 方法保证键的唯一 * key、value 都可以为null,但是 key 位置只能是一个null * HashMap中的映射不是有序的,即存取是无序的 * **key要存储的是自定义对象,需要重写hashCode和equals方法,防止出现地址不同内容相同的key** @@ -7887,17 +7889,17 @@ public class CommonsIODemo01 { 单元测试的经典框架:Junit -* Junit : 是Java语言编写的第三方单元测试框架,可以帮助我们方便快速的测试我们代码的正确性。 +* Junit : 是 Java 语言编写的第三方单元测试框架,可以帮助我们方便快速的测试我们代码的正确性。 * 单元测试: - * 单元:在Java中,一个类就是一个单元 - * 单元测试:Junit编写的一小段代码,用来对某个类中的某个方法进行功能测试或业务逻辑测试 + * 单元:在 Java 中,一个类就是一个单元 + * 单元测试:Junit 编写的一小段代码,用来对某个类中的某个方法进行功能测试或业务逻辑测试 -Junit单元测试框架的作用: +Junit 单元测试框架的作用: * 用来对类中的方法功能进行有目的的测试,以保证程序的正确性和稳定性 * 能够**独立的**测试某个方法或者所有方法的预期正确性 -测试方法注意事项:**必须是public修饰的,没有返回值,没有参数,使用注解@Test修饰** +测试方法注意事项:**必须是 public 修饰的,没有返回值,没有参数,使用注解@Test修饰** Junit常用注解(Junit 4.xxxx版本),@Test 测试方法: @@ -11915,8 +11917,8 @@ init 指的是实例构造器,主要作用是在类实例化过程中执行, * 创建类对象、使用类的静态域、创建子类对象、使用子类的静态域 * 在 JVM 启动时,通过三大类加载器加载 class * 显式加载: - * ClassLoader.loadClass(className),只加载和连接,**不会进行初始化** - * Class.forName(String name, boolean initialize,ClassLoader loader),使用 loader 进行加载和连接,根据参数 initialize 决定是否初始化 + * ClassLoader.loadClass(className):只加载和连接,**不会进行初始化** + * Class.forName(String name, boolean initialize,ClassLoader loader):使用 loader 进行加载和连接,根据参数 initialize 决定是否初始化 类的唯一性: @@ -11932,8 +11934,8 @@ init 指的是实例构造器,主要作用是在类实例化过程中执行, 基本特征: -* 可见性,子类加载器可以访问父加载器加载的类型,但是反过来是不允许的 -* 单一性,由于父加载器的类型对于子加载器是可见的,所以父加载器中加载过的类型,不会在子加载器中重复加载 +* **可见性**,子类加载器可以访问父加载器加载的类型,但是反过来是不允许的 +* **单一性**,由于父加载器的类型对于子加载器是可见的,所以父加载器中加载过的类型,不会在子加载器中重复加载 @@ -12076,10 +12078,9 @@ ClassLoader 类常用方法: } ``` - 此时执行main函数,会出现异常,在类 java.lang.String 中找不到 main 方法,防止恶意篡改核心 API 库 - 出现该信息是因为双亲委派的机制,java.lang.String 的在启动类加载器(Bootstrap)得到加载,启动类加载器优先级更高,在核心 jre 库中有其相同名字的类文件,但该类中并没有 main 方法 + 此时执行 main 函数,会出现异常,在类 java.lang.String 中找不到 main 方法,防止恶意篡改核心 API 库。出现该信息是因为双亲委派的机制,java.lang.String 的在启动类加载器(Bootstrap)得到加载,启动类加载器优先级更高,在核心 jre 库中有其相同名字的类文件,但该类中并没有 main 方法 -双亲委派机制的缺点:检查类是否加载的委托过程是单向的,这个方式虽然从结构上看比较清晰,使各个 ClassLoader 的职责非常明确,但顶层的 ClassLoader 无法访问底层的 ClassLoader 所加载的类 +双亲委派机制的缺点:检查类是否加载的委托过程是单向的,这个方式虽然从结构上看比较清晰,使各个 ClassLoader 的职责非常明确,但**顶层的 ClassLoader 无法访问底层的 ClassLoader 所加载的类** @@ -12095,7 +12096,7 @@ ClassLoader 类常用方法: protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { - //调用当前类加载器的findLoadedClass(name),检查当前类加载器是否已加载过指定name的类 + //调用当前类加载器的 findLoadedClass(name),检查当前类加载器是否已加载过指定 name 的类 Class c = findLoadedClass(name); //当前类加载器如果没有加载过 @@ -12104,22 +12105,19 @@ protected Class loadClass(String name, boolean resolve) try { //判断当前类加载器是否有父类加载器 if (parent != null) { - //如果当前类加载器有父类加载器,则调用父类加载器的loadClass(name,false) -          //父类加载器的loadClass方法,又会检查自己是否已经加载过 + //如果当前类加载器有父类加载器,则调用父类加载器的 loadClass(name,false) +          //父类加载器的 loadClass 方法,又会检查自己是否已经加载过 c = parent.loadClass(name, false); } else { - //当前类加载器没有父类加载器,说明当前类加载器是BootStrapClassLoader -           //则调用BootStrap ClassLoader的方法加载类 + //当前类加载器没有父类加载器,说明当前类加载器是 BootStrapClassLoader +           //则调用 BootStrap ClassLoader 的方法加载类 c = findBootstrapClassOrNull(name); } - } catch (ClassNotFoundException e) { - // ClassNotFoundException thrown if class not found - // from the non-null parent class loader - } + } catch (ClassNotFoundException e) { } if (c == null) { - // 如果调用父类的类加载器无法对类进行加载,则用自己的findClass()方法进行加载 - // k自定义findClass()方法 + // 如果调用父类的类加载器无法对类进行加载,则用自己的 findClass() 方法进行加载 + // 可以自定义 findClass() 方法 long t1 = System.nanoTime(); c = findClass(name); @@ -12154,7 +12152,7 @@ protected Class loadClass(String name, boolean resolve) * 如果不想破坏双亲委派模型,只需要重写 findClass 方法 * 如果想要去破坏双亲委派模型,需要去**重写 loadClass **方法 -* 引入线程上下文类加载器 +* 引入线程**上下文类加载器** Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI 等。这些 SPI 接口由 Java 核心库来提供,而 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径 classpath 里,SPI 接口中的代码需要加载具体的实现类: @@ -12167,7 +12165,7 @@ protected Class loadClass(String name, boolean resolve) IBM 公司主导的 JSR一291(OSGiR4.2)实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(OSGi 中称为 Bundle)都有一个自己的类加载器,当更换一个 Bundle 时,就把 Bundle 连同类加载器一起换掉以实现代码的热替换,在 OSGi 环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构 - 当收到类加载请求时,OSGi将按照下面的顺序进行类搜索: + 当收到类加载请求时,OSGi 将按照下面的顺序进行类搜索: 1. 将以 java.* 开头的类,委派给父类加载器加载 2. 否则,将委派列表名单内的类,委派给父类加载器加载 @@ -12176,16 +12174,8 @@ protected Class loadClass(String name, boolean resolve) 5. 否则,查找类是否在自己的 Fragment Bundle 中,如果在就委派给 Fragment Bundle 类加载器加载 6. 否则,查找 Dynamic Import 列表的 Bundle,委派给对应 Bundle 的类加载器加载 7. 否则,类查找失败 - - - -**** - - - -##### 热替换 - -热替换是指在程序的运行过程中,不停止服务,只通过替换程序文件来修改程序的行为,**热替换的关键需求在于服务不能中断**,修改必须立即表现正在运行的系统之中 + + 热替换是指在程序的运行过程中,不停止服务,只通过替换程序文件来修改程序的行为,**热替换的关键需求在于服务不能中断**,修改必须立即表现正在运行的系统之中 @@ -12206,7 +12196,7 @@ protected Class loadClass(String name, boolean resolve) * JDK1.2:改进了安全机制,增加了代码签名,不论本地代码或是远程代码都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制 * JDK1.6:当前最新的安全机制,引入了域(Domain)的概念。虚拟机会把所有代码加载到不同的系统域和应用域,不同的保护域对应不一样的权限。系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-沙箱机制.png) + @@ -12302,7 +12292,7 @@ public static void main(String[] args) { 为了保证兼容性,JDK9 没有改变三层类加载器架构和双亲委派模型,但为了模块化系统的顺利运行做了一些变动: -* 扩展机制被移除,扩展类加载器由于向后兼容性的原因被保留,不过被重命名为平台类加载器(platform classloader),可以通过 ClassLoader 的新方法 getPlatformClassLoader() 来获取 +* 扩展机制被移除,扩展类加载器由于**向后兼容性**的原因被保留,不过被重命名为平台类加载器(platform classloader),可以通过 ClassLoader 的新方法 getPlatformClassLoader() 来获取 * JDK9 基于模块化进行构建(原来的 rt.jar 和 tools.jar 被拆分成数十个JMOD文件),其中的 Java 类库就满足了可扩展的需求,那就无须再保留 `\lib\ext` 目录,此前使用这个目录或者 `java.ext.dirs` 系统变量来扩展 JDK 功能的机制就不需要继续存在 @@ -14386,7 +14376,7 @@ public class Reflect1 { } ``` -foo.invoke 0 ~ 15次调用的是 MethodAccessor 的实现类`NativeMethodAccessorImpl.invoke0()`,本地方法执行速度慢;当调用到第 16 次时,会采用运行时生成的类`sun.reflect.GeneratedMethodAccessor1`代替 +foo.invoke 0 ~ 15次调用的是 MethodAccessor 的实现类 `NativeMethodAccessorImpl.invoke0()`,本地方法执行速度慢;当调用到第 16 次时,会采用运行时生成的类 `sun.reflect.GeneratedMethodAccessor1` 代替 ```java public Object invoke(Object obj, Object[] args)throws Exception { @@ -18474,7 +18464,7 @@ Java 中提供了一个动态代理类 Proxy,Proxy 并不是代理对象的类 private TrainStation station = new TrainStation(); //也可以在参数中提供 getProxyObject(TrainStation station) public SellTickets getProxyObject() { - //使用Proxy获取代理对象 + //使用 Proxy 获取代理对象 SellTickets sellTickets = (SellTickets) Proxy.newProxyInstance( station.getClass().getClassLoader(), station.getClass().getInterfaces(), @@ -18498,6 +18488,7 @@ Java 中提供了一个动态代理类 Proxy,Proxy 并不是代理对象的类 public static void main(String[] args) { //获取代理对象 ProxyFactory factory = new ProxyFactory(); + //必须时代理ji SellTickets proxyObject = factory.getProxyObject(); proxyObject.sell(); } @@ -18510,7 +18501,7 @@ Java 中提供了一个动态代理类 Proxy,Proxy 并不是代理对象的类 -##### 原理解析 +##### 实现原理 JDK 动态代理方式的优缺点: @@ -18561,14 +18552,151 @@ public class Proxy implements java.io.Serializable { +**** + + + +##### 源码解析 + +```java +public static Object newProxyInstance(ClassLoader loader, + Class[] interfaces, + InvocationHandler h){ + // InvocationHandler 为空则抛出异常 + Objects.requireNonNull(h); + + // 复制一份 interfaces + final Class[] intfs = interfaces.clone(); + final SecurityManager sm = System.getSecurityManager(); + if (sm != null) { + checkProxyAccess(Reflection.getCallerClass(), loader, intfs); + } + + //从缓存中查找 class 类型的代理对象,参数二是代理需要实现的接口 + Class cl = getProxyClass0(loader, intfs); + //proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory()) + + try { + if (sm != null) { + checkNewProxyPermission(Reflection.getCallerClass(), cl); + } + + //获取代理类的构造方法,根据参数 InvocationHandler 匹配获取某个构造器 + final Constructor cons = cl.getConstructor(constructorParams); + final InvocationHandler ih = h; + if (!Modifier.isPublic(cl.getModifiers())) { + AccessController.doPrivileged(new PrivilegedAction() { + public Void run() { + //设置可访问的权限 + cons.setAccessible(true); + return null; + } + }); + } + //cons 是构造方法,并且内部持有 InvocationHandler,在 InvocationHandler 中持有 target 目标对象 + return cons.newInstance(new Object[]{h}); + } catch (IllegalAccessException|InstantiationException e) {} +} +``` + +```java +private static final class ProxyClassFactory { + // 代理类型的名称前缀 + private static final String proxyClassNamePrefix = "$Proxy"; + + //生成唯一数字使用,结合上面的代理类型名称前缀一起生成 + private static final AtomicLong nextUniqueNumber = new AtomicLong(); + + //参数一:Proxy.newInstance 时传递的 + //参数二:Proxy.newInstance 时传递的接口集合 + @Override + public Class apply(ClassLoader loader, Class[] interfaces) { + + Map, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length); + // 遍历接口集合 + for (Class intf : interfaces) { + Class interfaceClass = null; + try { + // 加载接口类到 JVM + interfaceClass = Class.forName(intf.getName(), false, loader); + } catch (ClassNotFoundException e) { + } + if (interfaceClass != intf) { + throw new IllegalArgumentException( + intf + " is not visible from class loader"); + } + // 如果 interfaceClass 不是接口 直接报错,保证集合内都是接口 + if (!interfaceClass.isInterface()) { + throw new IllegalArgumentException( + interfaceClass.getName() + " is not an interface"); + } + // 保证接口 interfaces 集合中没有重复的接口 + if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) { + throw new IllegalArgumentException( + "repeated interface: " + interfaceClass.getName()); + } + } + + //生成的代理类的包名 + String proxyPkg = null; + //生成的代理类访问修饰符 pulic final + int accessFlags = Modifier.PUBLIC | Modifier.FINAL; + + // 检查接口集合内的接口,看看有没有某个接口的访问修饰符不是 public 的 如果不是 public 的接口, + //生成的代理类 class 就必须和它在一个包下,否则访问出现问题 + for (Class intf : interfaces) { + // 获取访问修饰符 + int flags = intf.getModifiers(); + if (!Modifier.isPublic(flags)) { + accessFlags = Modifier.FINAL; + //获取当前接口的全限定名 包名.类名 + String name = intf.getName(); + int n = name.lastIndexOf('.'); + //获取包名 + String pkg = ((n == -1) ? "" : name.substring(0, n + 1)); + if (proxyPkg == null) { + proxyPkg = pkg; + } else if (!pkg.equals(proxyPkg)) { + throw new IllegalArgumentException( + "non-public interfaces from different packages"); + } + } + } + + if (proxyPkg == null) { + // if no non-public proxy interfaces, use com.sun.proxy package + proxyPkg = ReflectUtil.PROXY_PACKAGE + "."; + } + + // 获取唯一的编号 + long num = nextUniqueNumber.getAndIncrement(); + // 包名+ $proxy + 数字,比如 $proxy1 + String proxyName = proxyPkg + proxyClassNamePrefix + num; + + // 生成二进制字节码,这个字节码写入到文件内,就是编译好的 class 文件 + byte[] proxyClassFile = ProxyGenerator.generateProxyClass( + proxyName, interfaces, accessFlags); + try { + //使用加载器加载二进制到 jvm,并且返回 class + return defineClass0(loader, proxyName, + proxyClassFile, 0, proxyClassFile.length); + } catch (ClassFormatError e) { } + } +} +``` + + + + + + + *** #### CGLIB -##### 使用方式 - CGLIB 是一个功能强大,高性能的代码生成包,为没有实现接口的类提供代理,为 JDK 动态代理提供了补充 * CGLIB 是第三方提供的包,所以需要引入 jar 包的坐标: @@ -18608,14 +18736,6 @@ CGLIB 是一个功能强大,高性能的代码生成包,为没有实现接 } ``` - - -*** - - - -##### 原理分析 - CGLIB 的优缺点 * 优点: diff --git a/SSM.md b/SSM.md index dee0892..1de19bf 100644 --- a/SSM.md +++ b/SSM.md @@ -4602,7 +4602,7 @@ FactoryBean与 BeanFactory 区别: ##### 数据准备 -* DAO层 UserDao、AccountDao、BookDao、EquipmentDao +* DAO 层 UserDao、AccountDao、BookDao、EquipmentDao ```java public interface UserDao { @@ -4986,13 +4986,13 @@ SmartInitializingSingleton 原理:→ afterSingletonsInstantiated() ### 基本概述 -AOP(Aspect Oriented Programing):面向切面编程,一种编程**范式**,指导开发者如何组织程序结构 +AOP(Aspect Oriented Programing):面向切面编程,一种编程**范式**,指导开发者如何组织程序结构 -AOP弥补了OOP的不足,基于OOP基础之上进行横向开发: +AOP 弥补了 OOP 的不足,基于 OOP 基础之上进行横向开发: -- uOOP规定程序开发以类为主体模型,一切围绕对象进行,完成某个任务先构建模型 +- uOOP 规定程序开发以类为主体模型,一切围绕对象进行,完成某个任务先构建模型 -- uAOP程序开发主要关注基于OOP开发中的共性功能,一切围绕共性功能进行,完成某个任务先构建可能遇到的所有共性功能(当所有功能都开发出来也就没有共性与非共性之分),将软件开发由手工制作走向半自动化/全自动化阶段,实现“插拔式组件体系结构”搭建 +- uAOP 程序开发主要关注基于 OOP 开发中的共性功能,一切围绕共性功能进行,完成某个任务先构建可能遇到的所有共性功能(当所有功能都开发出来也就没有共性与非共性之分),将软件开发由手工制作走向半自动化/全自动化阶段,实现“插拔式组件体系结构”搭建 AOP作用: @@ -5017,21 +5017,21 @@ AOP开发思想: #### 概念详解 -- Joinpoint(连接点):就是方法 +- Joinpoint(连接点):就是方法 -- Pointcut(切入点):就是挖掉共性功能的方法 +- Pointcut(切入点):就是挖掉共性功能的方法 -- Advice(通知):就是共性功能,最终以一个方法的形式呈现 +- Advice(通知):就是共性功能,最终以一个方法的形式呈现 -- Aspect(切面):就是共性功能与挖的位置的对应关系 +- Aspect(切面):就是共性功能与挖的位置的对应关系 -- Target(目标对象):就是挖掉功能的方法对应的类产生的对象,这种对象是无法直接完成最终工作的 +- Target(目标对象):就是挖掉功能的方法对应的类产生的对象,这种对象是无法直接完成最终工作的 -- Weaving(织入):就是将挖掉的功能回填的动态过程 +- Weaving(织入):就是将挖掉的功能回填的动态过程 -- Proxy(代理):目标对象无法直接完成工作,需要对其进行功能回填,通过创建原始对象的代理对象实现 +- Proxy(代理):目标对象无法直接完成工作,需要对其进行功能回填,通过创建原始对象的代理对象实现 -- Introduction(引入/引介) :就是对原始对象无中生有的添加成员变量或成员方法 +- Introduction(引入/引介):就是对原始对象无中生有的添加成员变量或成员方法 ![](https://gitee.com/seazean/images/raw/master/Frame/AOP连接点.png) @@ -5055,17 +5055,17 @@ AOP开发思想: - 将非共性功能开发到对应的目标对象类中,并制作成切入点方法 - - 将共性功能独立开发出来,制作成**通知** + - 将共性功能独立开发出来,制作成通知 - - 在配置文件中,声明**切入点** + - 在配置文件中,声明切入点 - - 在配置文件中,声明**切入点**与**通知**间的关系(含**通知类型**),即**切面** + - 在配置文件中,声明切入点与通知间的关系(含通知类型),即切面 -- 运行阶段(AOP完成) +- 运行阶段(AOP 完成) - - Spring容器加载配置文件,监控所有配置的**切入点**方法的执行 + - Spring 容器加载配置文件,监控所有配置的**切入点**方法的执行 - - 当监控到切入点方法被运行,使用**代理**机制,动态创建**目标对象**的**代理对象**,根据**通知类别**,在**代理对象**的对应位置将通知对应的功能**织入**,完成完整的代码逻辑并运行 + - 当监控到切入点方法被运行,**使用代理机制,动态创建目标对象的代理对象,根据通知类别,在代理对象的对应位置将通知对应的功能织入**,完成完整的代码逻辑并运行 1. 导入坐标 pom.xml @@ -5104,7 +5104,7 @@ AOP开发思想: ```java //1.制作通知类,在类中定义一个方法用于完成共性功能 - public class AOPAdvice{ + public class AOPAdvice { //共性功能抽取后职称独立的方法 public void function(){ System.out.println("共性功能"); @@ -5153,7 +5153,7 @@ AOP开发思想: public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); UserService userService = (UserService) ctx.getBean("userService"); - userService.save();//先输出共性功能,然后user service running... + userService.save();//先输出共性功能,然后 user service running... } } ``` @@ -5168,9 +5168,13 @@ AOP开发思想: #### AspectJ -Aspect(切面)用于描述切入点与通知间的关系,是AOP编程中的一个概念 +Aspect(切面)用于描述切入点与通知间的关系,是 AOP 编程中的一个概念 -AspectJ是基于java语言对Aspect的实现 +AspectJ 是基于 java 语言对 Aspect 的实现 + + + +*** @@ -5180,7 +5184,7 @@ AspectJ是基于java语言对Aspect的实现 标签:的子标签 -作用:设置AOP +作用:设置 AOP 格式: @@ -5194,9 +5198,13 @@ AspectJ是基于java语言对Aspect的实现 +*** + + + ##### pointcut -标签:,归属于aop:config标签和aop:aspect标签 +标签:,归属于 aop:config 标签和 aop:aspect 标签 作用:设置切入点 @@ -5213,7 +5221,7 @@ AspectJ是基于java语言对Aspect的实现 说明: -* 一个aop:config标签中可以配置多个aop:pointcut标签,且该标签可以配置在aop:aspect标签内 +* 一个 aop:config 标签中可以配置多个 aop:pointcut 标签,且该标签可以配置在 aop:aspect 标签内 属性: @@ -5223,11 +5231,15 @@ AspectJ是基于java语言对Aspect的实现 +*** + + + ##### aspect -标签:,aop:config的子标签 +标签:,aop:config 的子标签 -作用:设置具体的AOP通知对应的切入点(切面) +作用:设置具体的 AOP 通知对应的切入点(切面) 格式: @@ -5241,7 +5253,7 @@ AspectJ是基于java语言对Aspect的实现 属性: -- ref :通知所在的bean的id +- ref :通知所在的 bean 的 id @@ -5259,6 +5271,10 @@ AspectJ是基于java语言对Aspect的实现 +*** + + + ##### 表达式 格式: @@ -5270,8 +5286,8 @@ AspectJ是基于java语言对Aspect的实现 示例: ```java -execution(public User service.UserService.findById(int)) //匹配UserService中只含有一个参数的findById方法 +execution(public User service.UserService.findById(int)) ``` 格式解析: @@ -5295,22 +5311,22 @@ execution(public User service.UserService.findById(int)) * *:单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现 ```java - execution(public * com.seazean.*.UserService.find*(*) //匹配com.seazean包下的任意包中的UserService类或接口中所有find开头的带有一个任意参数的方法 + execution(public * com.seazean.*.UserService.find*(*) ``` * .. :多个连续的任意符号,可以独立出现,常用于简化包名与参数 ```java - execution(public User com..UserService.findById(..)) //匹配com包下的任意包中的UserService类或接口中所有名称为findById参数任意数量和类型的方法 + execution(public User com..UserService.findById(..)) ``` * +:专用于匹配子类类型 ```java - execution(* *..*Service+.*(..)) //匹配任意包下的Service结尾的类或者接口的子类或者实现类 + execution(* *..*Service+.*(..)) ``` 逻辑运算符: @@ -5334,8 +5350,8 @@ execution(public void com.seazean.service.*.*(..)) execution(public void com.seazean.service.User*.*(..)) execution(public void com.seazean.service.*Service.*(..)) execution(public void com.seazean.service.UserService.*(..)) -execution(public User com.seazean.service.UserService.find*(..)) -execution(public User com.seazean.service.UserService.*Id(..)) +execution(public User com.seazean.service.UserService.find*(..)) //find开头 +execution(public User com.seazean.service.UserService.*Id(..)) //I execution(public User com.seazean.service.UserService.findById(..)) execution(public User com.seazean.service.UserService.findById(int)) execution(public User com.seazean.service.UserService.findById(int,int)) @@ -5346,6 +5362,10 @@ execution(List com.seazean.service.*Service+.findAll(..)) +*** + + + ##### 配置方式 XML配置规则: @@ -5358,7 +5378,7 @@ XML配置规则: - 代码走查过程中检测切入点是否存在非包含性进驻 -- 设定AOP执行检测程序,在单元测试中监控通知被执行次数与预计次数是否匹配(不绝对正确:加进一个不该加的,删去一个不该删的相当于结果不变) +- 设定 AOP 执行检测程序,在单元测试中监控通知被执行次数与预计次数是否匹配(不绝对正确:加进一个不该加的,删去一个不该删的相当于结果不变) - 设定完毕的切入点如果发生调整务必进行回归测试 @@ -5969,7 +5989,7 @@ AOP的通知类型共5种:前置通知,后置通知、返回后通知、抛 #### AOP注解 -AOP注解简化xml: +AOP 注解简化 xml: ![](https://gitee.com/seazean/images/raw/master/Frame/AOP注解开发.png) @@ -5981,7 +6001,7 @@ AOP注解简化xml: 3. 切面类中定义的切入点只能在当前类中使用,如果想引用其他类中定义的切入点使用“类名.方法名()”引用 -4. 可以在通知类型注解后添加参数,实现XML配置中的属性,例如after-returning后的returning属性 +4. 可以在通知类型注解后添加参数,实现 XML 配置中的属性,例如 after-returning 后的 returning 性 @@ -5995,7 +6015,7 @@ AOP注解简化xml: ##### XML -开启AOP注解支持: +开启 AOP 注解支持: ```xml @@ -6004,11 +6024,11 @@ AOP注解简化xml: 开发步骤: -1. 导入坐标(伴随spring-context坐标导入已经依赖导入完成) -2. 开启AOP注解支持 -3. 配置切面@Aspect -4. 定义专用的切入点方法,并配置切入点@Pointcut -5. 为通知方法配置通知类型及对应切入点@Before +1. 导入坐标(伴随 spring-context 坐标导入已经依赖导入完成) +2. 开启 AOP 注解支持 +3. 配置切面 @Aspect +4. 定义专用的切入点方法,并配置切入点 @Pointcut +5. 为通知方法配置通知类型及对应切入点 @Before @@ -6016,9 +6036,9 @@ AOP注解简化xml: 注解:@EnableAspectJAutoProxy -位置:Spring注解配置类定义上方 +位置:Spring 注解配置类定义上方 -作用:设置当前类开启AOP注解驱动的支持,加载AOP注解 +作用:设置当前类开启 AOP 注解驱动的支持,加载 AOP 注解 格式: @@ -6276,7 +6296,7 @@ public class UserServiceDecorator implements UserService{ #### Proxy -JDKProxy动态代理是针对对象做代理,要求原始对象具有接口实现,并对接口方法进行增强,因为**代理类继承Proxy** +JDKProxy 动态代理是针对对象做代理,要求原始对象具有接口实现,并对接口方法进行增强,因为**代理类继承Proxy** 静态代理和动态代理的区别: @@ -6381,13 +6401,13 @@ CGLIB 特点: #### 代理选择 -Spirng可以通过配置的形式控制使用的代理形式,Spring会先判断是否实现了接口,如果实现了接口就使用JDK动态代理,如果没有实现接口则使用Cglib动态代理,通过配置可以修改为使用cglib +Spirng 可以通过配置的形式控制使用的代理形式,Spring 会先判断是否实现了接口,如果实现了接口就使用 JDK 动态代理,如果没有实现接口则使用 Cglib 动态代理,通过配置可以修改为使用 Cglib - XML配置 ```xml - + ``` - XML注解支持 @@ -6404,11 +6424,11 @@ Spirng可以通过配置的形式控制使用的代理形式,Spring会先判 @EnableAspectJAutoProxy(proxyTargetClass = true) ``` -* JDK动态代理和Cglib动态代理的区别: +* JDK 动态代理和 Cglib 动态代理的区别: - * JDK动态代理只能对实现了接口的类生成代理,没有实现接口的类不能使用。 - * Cglib动态代理即使被代理的类没有实现接口也可以使用,因为Cglib动态代理是使用继承被代理类的方式进行扩展 - * Cglib动态代理是通过继承的方式,覆盖被代理类的方法来进行代理,所以如果方法是被final修饰的话,就不能进行代理 + * JDK 动态代理只能对实现了接口的类生成代理,没有实现接口的类不能使用。 + * Cglib 动态代理即使被代理的类没有实现接口也可以使用,因为 Cglib 动态代理是使用继承被代理类的方式进行扩展 + * Cglib 动态代理是通过继承的方式,覆盖被代理类的方法来进行代理,所以如果方法是被 final 修饰的话,就不能进行代理 From 448c4b4a5b58f1b37577a451062d5c9a692c5b8a Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 28 Jul 2021 22:48:47 +0800 Subject: [PATCH 003/168] Update Java Notes --- Java.md | 54 +- SSM.md | 1650 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 1658 insertions(+), 46 deletions(-) diff --git a/Java.md b/Java.md index 8001612..3e38f77 100644 --- a/Java.md +++ b/Java.md @@ -786,7 +786,7 @@ public class MethodDemo { * 如果第 1 个阶段中没有找到适配的方法,那么在允许自动装拆箱,但不允许可变长参数的情况下选取重载方法 * 如果第 2 个阶段中没有找到适配的方法,那么在允许自动装拆箱以及可变长参数的情况下选取重载方法 -如果 Java 编译器在同一个阶段中找到了多个适配的方法,那么会选择一个最为贴切的,而决定贴切程度的一个关键就是形式参数类型的继承关系,一般会选择形参为参数类型的子类的方法,因为子类时更具体的实现: +如果 Java 编译器在同一个阶段中找到了多个适配的方法,那么会选择一个最为贴切的,而决定贴切程度的一个关键就是形式参数类型的继承关系,**一般会选择形参为参数类型的子类的方法,因为子类时更具体的实现**: ```java public class MethodDemo { @@ -4318,19 +4318,19 @@ TreeSet 集合自排序的方式: 2. 字符串类型的元素会按照首字符的编号排序 3. 对于自定义的引用数据类型,TreeSet 默认无法排序,执行的时候报错,因为不知道排序规则 -自定义的引用数据类型,TreeSet 默认无法排序,需要定制排序的规则,方案有2种: +自定义的引用数据类型,TreeSet 默认无法排序,需要定制排序的规则,方案有 2 种: * 直接为**对象的类**实现比较器规则接口 Comparable,重写比较方法: - 方法:`public int compareTo(Employee o): this是比较者, o是被比较者` + 方法:`public int compareTo(Employee o): this 是比较者, o 是被比较者` - * 比较者大于被比较者,返回正数 + * 比较者大于被比较者,返回正数(升序) * 比较者小于被比较者,返回负数 * 比较者等于被比较者,返回 0 * 直接为**集合**设置比较器 Comparator 对象,重写比较方法: - 方法:`public int compare(Employee o1, Employee o2): o1比较者, o2被比较者` + 方法:`public int compare(Employee o1, Employee o2): o1 比较者, o2 被比较者` * 比较者大于被比较者,返回正数 * 比较者小于被比较者,返回负数 @@ -8443,12 +8443,16 @@ public class AnnotationDemo01 { +*** + + + #### 特殊属性 注解的特殊属性名称:value -* 如果只有一个value属性的情况下,使用value属性的时候可以省略value名称不写 -* 如果有多个属性,且多个属性没有默认值,那么value是不能省略的 +* 如果只有一个 value 属性的情况下,使用 value 属性的时候可以省略 value 名称不写 +* 如果有多个属性,且多个属性没有默认值,那么 value 是不能省略的 ```java //@Book("/deleteBook.action") @@ -8470,7 +8474,7 @@ public class AnnotationDemo01{ ### 元注解 -元注解是sun公司提供的,用来注解自定义注解 +元注解是 sun 公司提供的,用来注解自定义注解 元注解有四个: @@ -11107,7 +11111,7 @@ Serial GC、Parallel GC、Concurrent Mark Sweep GC 这三个 GC 不同: ##### 静态集合 -静态集合类的生命周期与JVM程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。原因是长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收 +静态集合类的生命周期与 JVM 程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。原因是**长生命周期的对象持有短生命周期对象的引用**,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收 ```java public class MemoryLeak { @@ -11184,7 +11188,7 @@ public class UsingRandom { ##### 改变哈希 -当一个对象被存储进 HashSet 集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段,否则对象修改后的哈希值与最初存储进 HashSet 集合中时的哈希值不同,这种情况下使用该对象的当前引用作为的参数去 HashSet 集合中检索对象返回 false,导致无法从 HashSet 集合中单独删除当前对象,造成内存泄漏 +当一个对象被存储进 HashSet 集合中以后,就**不能修改这个对象中的那些参与计算哈希值的字段**,否则对象修改后的哈希值与最初存储进 HashSet 集合中时的哈希值不同,这种情况下使用该对象的当前引用作为的参数去 HashSet 集合中检索对象返回 false,导致无法从 HashSet 集合中单独删除当前对象,造成内存泄漏 @@ -11280,14 +11284,14 @@ public Object pop() { * 普通对象:分为两部分 - * Mark Word:用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等 + * **Mark Word**:用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等 ```ruby hash(25) + age(4) + lock(3) = 32bit #32位系统 unused(25+1) + hash(31) + age(4) + lock(3) = 64bit #64位系统 ``` - * Klass Word:类型指针,对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;在64位系统中,开启指针压缩(-XX:+UseCompressedOops)或者 JVM 堆的最大值小于 32G,这个指针也是 4byte,否则是 8byte(就是 Java 中的一个引用的大小) + * **Klass Word**:类型指针,指向该对象的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;在64位系统中,开启指针压缩(-XX:+UseCompressedOops)或者 JVM 堆的最大值小于 32G,这个指针也是 4byte,否则是 8byte(就是 Java 中的一个引用的大小) ```ruby |-----------------------------------------------------| @@ -11309,7 +11313,7 @@ public Object pop() { 实例数据:实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的,都需要记录起来 -对齐填充:Padding 起占位符的作用。64 位系统,由于 HotSpot VM 的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,就是对象的大小必须是 8 字节的整数倍,而对象头部分正好是 8 字节的倍数(1倍或者2倍),因此当对象实例数据部分没有对齐时,就需要通过对齐填充来补全 +对齐填充:Padding 起占位符的作用。64 位系统,由于 HotSpot VM 的自动内存管理系统要求**对象起始地址必须是 8 字节的整数倍**,就是对象的大小必须是 8 字节的整数倍,而对象头部分正好是 8 字节的倍数(1倍或者2倍),因此当对象实例数据部分没有对齐时,就需要通过对齐填充来补全 32位系统: @@ -11339,7 +11343,7 @@ public Object pop() { #### 实际大小 -浅堆(Shallow Heap):对象本身占用的内存,不包括内部引用对象的大小,32 位系统中一个**对象引用占 4 个字节**,每个对象头占用 8 个字节,根据堆快照格式不同,对象的大小会同 8 字节进行对齐 +浅堆(Shallow Heap):**对象本身占用的内存,不包括内部引用对象的大小**,32 位系统中一个**对象引用占 4 个字节**,每个对象头占用 8 个字节,根据堆快照格式不同,对象的大小会同 8 字节进行对齐 JDK7 中的 String:2个 int 值共占 8 字节,value 对象引用占用 4 字节,对象头 8 字节,对齐后占 24 字节,为 String 对象的浅堆大小,与 value 实际取值无关,无论字符串长度如何,浅堆大小始终是 24 字节 @@ -11410,14 +11414,14 @@ private int hash32; JVM 是通过栈帧中的对象引用访问到其内部的对象实例: -* 句柄访问 - 使用该方式,Java 堆中会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息 +* 句柄访问:Java 堆中会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息 + 优点:reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集)时只会改变句柄中的实例数据指针,而 reference 本身不需要被修改。 -* 直接指针(HotSpot采用) - 使用该方式,Java 堆对象的布局必须考虑如何放置访问类型数据的相关信息,reference中直接存储的就是对象地址 +* 直接指针(HotSpot采用):Java 堆对象的布局必须考虑如何放置访问类型数据的相关信息,reference 中直接存储的对象地址 + 优点:速度更快,**节省了一次指针定位的时间开销** @@ -11439,7 +11443,7 @@ JVM 是通过栈帧中的对象引用访问到其内部的对象实例: 1. 创建阶段 (Created): 2. 应用阶段 (In Use):对象至少被一个强引用持有着 3. 不可见阶段 (Invisible):程序的执行已经超出了该对象的作用域,不再持有该对象的任何强引用 -4. 不可达阶段 (Unreachable):该对象不再被任何强引用所持有,包括GC Root的强引用 +4. 不可达阶段 (Unreachable):该对象不再被任何强引用所持有,包括 GC Root 的强引用 5. 收集阶段 (Collected):垃圾回收器已经对该对象的内存空间重新分配做好准备,该对象如果重写了finalize()方法,则会去执行该方法 6. 终结阶段 (Finalized):等待垃圾回收器对该对象空间进行回收,当对象执行完finalize()方法后仍然处于不可达状态时进入该阶段 7. 对象空间重分配阶段 (De-allocated):垃圾回收器对该对象的所占用的内存空间进行回收或者再分配 @@ -11458,7 +11462,7 @@ JVM 是通过栈帧中的对象引用访问到其内部的对象实例: 类在第一次实例化加载一次,后续实例化不再加载,引用第一次加载的类 -Java对象创建时机: +Java 对象创建时机: 1. 使用 new 关键字创建对象:由执行类实例创建表达式而引起的对象创建 @@ -11508,17 +11512,17 @@ Java对象创建时机: 4. 初始化分配的空间:虚拟机将分配到的内存空间都初始化为零值(不包括对象头),保证对象实例字段在不赋值时可以直接使用,程序能访问到这些字段的数据类型所对应的零值 -5. 设置对象的对象头:将对象的所属类(类的元数据信息)、对象的HashCode、对象的GC信息、锁信息等数据存储在对象的对象头中 +5. 设置对象的对象头:将对象的所属类(类的元数据信息)、对象的 HashCode、对象的 GC 信息、锁信息等数据存储在对象头中 -6. 执行init方法进行实例化:实例变量初始化、实例代码块初始化 、构造函数初始化 +6. 执行 init 方法进行实例化:实例变量初始化、实例代码块初始化 、构造函数初始化 * 实例变量初始化与实例代码块初始化: - 对实例变量直接赋值或者使用实例代码块赋值,编译器会将其中的代码放到类的构造函数中去,并且这些代码会被放在对超类构造函数的调用语句之后 (**Java要求构造函数的第一条语句必须是超类构造函数的调用语句**),构造函数本身的代码之前 + 对实例变量直接赋值或者使用实例代码块赋值,编译器会将其中的代码放到类的构造函数中去,并且这些代码会被放在对超类构造函数的调用语句之后 (**Java 要求构造函数的第一条语句必须是超类构造函数的调用语句**),构造函数本身的代码之前 * 构造函数初始化: - **Java要求在实例化类之前,必须先实例化其超类,以保证所创建实例的完整性**,在准备实例化一个类的对象前,首先准备实例化该类的父类,如果该类的父类还有父类,那么准备实例化该类的父类的父类,依次递归直到递归到Object类。然后从Object类依次对以下各类进行实例化,初始化父类中的变量和执行构造函数 + **Java 要求在实例化类之前,必须先实例化其超类,以保证所创建实例的完整性**,在准备实例化一个类的对象前,首先准备实例化该类的父类,如果该类的父类还有父类,那么准备实例化该类的父类的父类,依次递归直到递归到 Object 类。然后从 Object 类依次对以下各类进行实例化,初始化父类中的变量和执行构造函数 @@ -18697,7 +18701,7 @@ private static final class ProxyClassFactory { #### CGLIB -CGLIB 是一个功能强大,高性能的代码生成包,为没有实现接口的类提供代理,为 JDK 动态代理提供了补充 +CGLIB 是一个功能强大,高性能的代码生成包,为没有实现接口的类提供代理,为 JDK 动态代理提供了补充($$Proxy) * CGLIB 是第三方提供的包,所以需要引入 jar 包的坐标: diff --git a/SSM.md b/SSM.md index 1de19bf..32ea78f 100644 --- a/SSM.md +++ b/SSM.md @@ -6401,16 +6401,16 @@ CGLIB 特点: #### 代理选择 -Spirng 可以通过配置的形式控制使用的代理形式,Spring 会先判断是否实现了接口,如果实现了接口就使用 JDK 动态代理,如果没有实现接口则使用 Cglib 动态代理,通过配置可以修改为使用 Cglib +Spirng 可以通过配置的形式控制使用的代理形式,Spring 会先判断是否实现了接口,如果实现了接口就使用 JDK 动态代理,如果没有实现接口则使用 CGLIB 动态代理,通过配置可以修改为使用 CGLIB -- XML配置 +- XML 配置 ```xml ``` -- XML注解支持 +- XML 注解支持 ```xml @@ -6420,15 +6420,15 @@ Spirng 可以通过配置的形式控制使用的代理形式,Spring 会先判 - 注解驱动 ```java - //修改为使用cglib创建代理对象 + //修改为使用 cglib 创建代理对象 @EnableAspectJAutoProxy(proxyTargetClass = true) ``` -* JDK 动态代理和 Cglib 动态代理的区别: +* JDK 动态代理和 CGLIB 动态代理的区别: * JDK 动态代理只能对实现了接口的类生成代理,没有实现接口的类不能使用。 - * Cglib 动态代理即使被代理的类没有实现接口也可以使用,因为 Cglib 动态代理是使用继承被代理类的方式进行扩展 - * Cglib 动态代理是通过继承的方式,覆盖被代理类的方法来进行代理,所以如果方法是被 final 修饰的话,就不能进行代理 + * CGLIB 动态代理即使被代理的类没有实现接口也可以使用,因为 CGLIB 动态代理是使用继承被代理类的方式进行扩展 + * CGLIB 动态代理是通过继承的方式,覆盖被代理类的方法来进行代理,所以如果方法是被 final 修饰的话,就不能进行代理 @@ -6495,8 +6495,7 @@ TransactionDefinition 接口中定义了五个表示隔离级别的常量: MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ(可重读)** -**分布式事务**:允许多个独立的事务资源(transactional resources)参与到一个全局的事务中 -事务资源通常是关系型数据库系统,但也可以是其他类型的资源,全局事务要求在其中的所有参与的事务要么都提交,要么都回滚,这对于事务原有的ACID要求又有了提高 +**分布式事务**:允许多个独立的事务资源(transactional resources)参与到一个全局的事务中。事务资源通常是关系型数据库系统,但也可以是其他类型的资源,全局事务要求在其中的所有参与的事务要么都提交,要么都回滚,这对于事务原有的 ACID 要求又有了提高 在使用分布式事务时,InnoDB 存储引擎的事务隔离级别必须设置为 SERIALIZABLE @@ -6600,9 +6599,9 @@ MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ( #### 事务对象 -J2EE开发使用分层设计的思想进行,对于简单的业务层转调数据层的单一操作,事务开启在业务层或者数据层并无太大差别,当业务中包含多个数据层的调用时,需要在业务层开启事务,对数据层中多个操作进行组合并归属于同一个事务进行处理 +J2EE 开发使用分层设计的思想进行,对于简单的业务层转调数据层的单一操作,事务开启在业务层或者数据层并无太大差别,当业务中包含多个数据层的调用时,需要在业务层开启事务,对数据层中多个操作进行组合并归属于同一个事务进行处理 -Spring为业务层提供了整套的事务解决方案: +Spring 为业务层提供了整套的事务解决方案: - PlatformTransactionManager - TransactionDefinition @@ -6631,7 +6630,7 @@ PlatformTransactionManager,平台事务管理器实现类: 管理器: -- JPA(Java Persistence API)Java EE 标准之一,为POJO提供持久化标准规范,并规范了持久化开发的统一API,符合JPA规范的开发可以在不同的JPA框架下运行 +- JPA(Java Persistence API)Java EE 标准之一,为 POJO 提供持久化标准规范,并规范了持久化开发的统一 API,符合 JPA 规范的开发可以在不同的 JPA 框架下运行 **非持久化一个字段**: @@ -6643,7 +6642,7 @@ PlatformTransactionManager,平台事务管理器实现类: String transient4; // not persistent because of @Transient ``` -- JDO(Java Data Object)是Java对象持久化规范,用于存取某种数据库中的对象,并提供标准化API。JDBC 仅针对关系数据库进行操作,JDO 可以扩展到关系数据库、XML、对象数据库等,可移植性更强 +- JDO(Java Data Object)是 Java 对象持久化规范,用于存取某种数据库中的对象,并提供标准化 API。JDBC 仅针对关系数据库进行操作,JDO 可以扩展到关系数据库、XML、对象数据库等,可移植性更强 - JTA(Java Transaction API)Java EE 标准之一,允许应用程序执行分布式事务处理。与 JDBC 相比,JDBC 事务则被限定在一个单一的数据库连接,而一个 JTA 事务可以有多个参与者,比如 JDBC 连接、JDO 都可以参与到一个 JTA 事务中 @@ -7252,7 +7251,7 @@ public void addAccount{} ### 模板对象 -Spring模板对象:TransactionTemplate、JdbcTemplate、RedisTemplate、RabbitTemplate、JmsTemplate、HibernateTemplate、RestTemplate +Spring 模板对象:TransactionTemplate、JdbcTemplate、RedisTemplate、RabbitTemplate、JmsTemplate、HibernateTemplate、RestTemplate * JdbcTemplate:提供标准的sql语句操作API * NamedParameterJdbcTemplate:提供标准的具名sql语句操作API @@ -7279,19 +7278,19 @@ Spring模板对象:TransactionTemplate、JdbcTemplate、RedisTemplate、Rabbit ### 底层原理 -TransactionManagementConfigurationSelector类: +TransactionManagementConfigurationSelector 类: -* 导入AutoProxyRegistrar组件和ProxyTransactionManagementConfiguration组件 +* 导入 AutoProxyRegistrar 组件和 ProxyTransactionManagementConfiguration 组件 -* AutoProxyRegistrar:利用后置处理器机制在对象创建以后包装对象,返回一个代理对象(增强器),代理对象执行方法利用拦截器链进行调用,通过@Transactional作为方法拦截的标记,把有事务管理的类作为目标类,生成代理对象,然后增强@Transactional标记的方法,在使用目标方法的时候,从IOC容器中获取的其实是被增强的代理类,且事务方法会被代理,跟AOP原理一样 +* AutoProxyRegistrar:利用后置处理器机制在对象创建以后包装对象,返回一个代理对象(增强器),代理对象执行方法利用拦截器链进行调用,通过 @Transactional 作为方法拦截的标记,把有事务管理的类作为目标类,生成代理对象,然后增强 @Transactional 标记的方法,在使用目标方法的时候,从 IOC 容器中获取的其实是被增强的代理类,且事务方法会被代理,跟 AOP 原理一样 -* ProxyTransactionManagementConfiguration:向IOC容器中导入事务增强器(BeanFactoryTransactionAttributeSourceAdvisor),事务注解@Transactional的解析器(AnnotationTransactionAttributeSource)和事务方法拦截器(TransactionInterceptor) +* ProxyTransactionManagementConfiguration:向 容器中导入事务增强器 BeanFactoryTransactionAttributeSourceAdvisor,事务注解 @Transactional 的解析器 AnnotationTransactionAttributeSource 和事务方法拦截器 TransactionInterceptor -通过AOP动态织入,进行事务开启和提交 +通过 AOP 动态织入,进行事务开启和提交 事务底层原理解析:策略模式 -策略模式(Strategy Pattern)**使用不同策略的对象实现不同的行为方式**,策略对象的变化导致行为的变化,每个事务对应一个新的connection对象 +策略模式(Strategy Pattern)**使用不同策略的对象实现不同的行为方式**,策略对象的变化导致行为的变化,每个事务对应一个新的 connection 对象 ![](https://gitee.com/seazean/images/raw/master/Frame/Spring-事务底层原理策略模式.png) @@ -7303,9 +7302,1618 @@ TransactionManagementConfigurationSelector类: + + ## 原理 -(整理中,一周之内整理完成) +### XML + +三大对象: + +* **BeanDefinition**:是 Spring 中极其重要的一个概念,它存储了 bean 对象的所有特征信息,如是否单例、是否懒加载、factoryBeanName 等,和 bean 的关系就是类与对象的关系,一个不同的 bean 对应一个 BeanDefinition + +* **BeanDefinationRegistry**:存放 BeanDefination 的容器,是一种键值对的形式,通过特定的 Bean 定义的 id,映射到相应的 BeanDefination,**BeanFactory 的实现类同样继承 BeanDefinationRegistry 接口**,拥有保存 BD 的能力 + +* **BeanDefinitionReader**:读取配置文件,比如 xml 用 dom4j 解析,配置文件用 io 流 + +程序: + +```java +BeanFactory bf = new XmlBeanFactory(new ClassPathResource("applicationContext.xml")); +UserService userService1 = (UserService)bf.getBean("userService"); +``` + +源码解析: + +```java +public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) { + super(parentBeanFactory); + this.reader.loadBeanDefinitions(resource); +} +public int loadBeanDefinitions(Resource resource) { + //将 resource 包装成带编码格式的 EncodedResource + //EncodedResource 中 getReader()方法,调用java.io包下的 转换流 创建指定编码的输入流对象 + return loadBeanDefinitions(new EncodedResource(resource)); +} +``` + +* `XmlBeanDefinitionReader.loadBeanDefinitions()`:**把 Resource 解析成 BeanDefinition 对象** + + * `currentResources = this.resourcesCurrentlyBeingLoaded.get()`:拿到当前线程已经加载过的所有 EncodedResoure 资源,用 ThreadLocal 保证线程安全 + * `if (currentResources == null)`:判断 currentResources 是否为空,为空则进行初始化 + * `if (!currentResources.add(encodedResource))`:如果已经加载过该资源会报错,防止重复加载 + * `inputSource = new InputSource(inputStream)`:资源对象包装成 InputSource,InputSource 使 SAX 中的资源对象,用来进行 XML 文件的解析 + * `return doLoadBeanDefinitions()`:**加载返回** + * `currentResources.remove(encodedResource)`:加载完成移除当前 encodedResource + * `resourcesCurrentlyBeingLoaded.remove()`:ThreadLocal 为空时移除元素,防止内存泄露 + +* `XmlBeanDefinitionReader.doLoadBeanDefinitions(inputSource, resource)`:真正的加载函数 + + `Document doc = doLoadDocument(inputSource, resource)`:转换成有**层次结构**的 Document 对象 + + * `getEntityResolver()`**:获取用来解析 DTD、XSD 约束的解析器** + + * `getValidationModeForResource(resource)`:获取验证模式 + + `int count = registerBeanDefinitions(doc, resource)`:**将 Document 解析成 BD 对象,注册(添加)到 BeanDefinationRegistry 中**,返回新注册的数量 + + * `createBeanDefinitionDocumentReader()`:创建 DefaultBeanDefinitionDocumentReader 对象 + * `getRegistry().getBeanDefinitionCount()`:获取解析前 BeanDefinationRegistry 中的 bd 数量 + * `registerBeanDefinitions(doc, readerContext)`:注册 BD + * `this.readerContext = readerContext`:保存上下文对象 + * `doRegisterBeanDefinitions(doc.getDocumentElement())`:真正的注册 BD 函数 + * `doc.getDocumentElement()`:拿出顶层标签 + * `return getRegistry().getBeanDefinitionCount() - countBefore`:返回新加入的数量 + +* `DefaultBeanDefinitionDocumentReader.doRegisterBeanDefinitions()`:注册 BD 到 BR + + * `createDelegate(getReaderContext(), root, parent)`:beans 是标签的解析器对象 + * `delegate.isDefaultNamespace(root)`:判断 beans 标签是否是默认的属性 + * `root.getAttribute(PROFILE_ATTRIBUTE)`:解析 profile 属性 + * `preProcessXml(root)`:解析前置处理,自定义实现 + * `parseBeanDefinitions(root, this.delegate)`:**解析 beans 标签中的子标签** + * `parseDefaultElement(ele, delegate)`:如果是默认的标签,用该方法解析子标签 + * 判断标签名称,进行相应的解析 + * `processBeanDefinition(ele, delegate)`: + * `delegate.parseCustomElement(ele)`:解析自定义的标签 + * `postProcessXml(root)`:解析后置处理 + +* `DefaultBeanDefinitionDocumentReader.processBeanDefinition()`:**解析 bean 并注册到注册中心** + + * `delegate.parseBeanDefinitionElement(ele)`:解析 bean 标签封装为 BeanDefinitionHolder + + * `if (!StringUtils.hasText(beanName) && !aliases.isEmpty())`:条件一成立说明 name 没有值,条件二成立说明别名有值 + + `beanName = aliases.remove(0)`:拿别名列表的第一个元素当作 beanName + + * `parseBeanDefinitionElement(ele, beanName, containingBean)`:**解析 bean 标签** + + * `parseState.push(new BeanEntry(beanName))`:当前解析器的状态设置为 BeanEntry + * class 和 parent 属性存在一个,parent 是作为父标签为了被继承 + * `createBeanDefinition(className, parent)`:设置了class 的 GenericBeanDefinition对象 + * `parseBeanDefinitionAttributes()`:解析 bean 标签的属性 + * 接下来解析子标签 + + * `beanName = this.readerContext.generateBeanName(beanDefinition)`:生成 className + # + 序号的名称赋值给 beanName + + * `return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray)`:包装成 BeanDefinitionHolder + + * `registerBeanDefinition(bdHolder, getReaderContext().getRegistry())`:**注册到容器** + + * `beanName = definitionHolder.getBeanName()`:获取beanName + * `this.beanDefinitionMap.put(beanName, beanDefinition)`:添加到注册中心 + + * `getReaderContext().fireComponentRegistered`:发送注册完成事件 + + + +**说明:源码部分的笔记做的不一定适合所有人观看,作者采用流水线式的解析重要的代码,解析的结构类似于树状,如果视觉疲劳可以去网上参考一些博客和流程图学习源码。** + + + +**** + + + +### IOC + +#### 容器启动 + +Spring IOC 容器是 ApplicationContext 或者 BeanFactory,使用多个 Map 集合保存单实例 Bean,环境信息等资源,不同层级有不同的容器,比如整合 SpringMVC 的父子容器(先看 Bean 部分的源码解析再回看容器) + +ClassPathXmlApplicationContext 与 AnnotationConfigApplicationContext 差不多: + +```java +public AnnotationConfigApplicationContext(Class... annotatedClasses) { + // 1. 注册 Spring 内置的后置处理器的 BeanDefinition 到容器, + // 方法:AnnotationConfigUtils#registerAnnotationConfigProcessors() + // 2. 实例化路径扫描器,用于对指定的包目录进行扫描查找 bean 对象 + this(); + register(annotatedClasses);// 解析配置类,封装成一个 BeanDefinitionHolder,并注册到容器 + refresh();// 加载刷新容器中的 Bean +} +``` + +AbstractApplicationContext.refresh(): + +* prepareRefresh():刷新前的**预处理** + + * `this.startupDate = System.currentTimeMillis()`:设置容器的启动时间 + * `initPropertySources()`:初始化一些属性设置,可以自定义个性化的属性设置方法 + * `getEnvironment().validateRequiredProperties()`:检查环境变量 + * `earlyApplicationEvents= new LinkedHashSet()`:保存容器中早期的事件 + +* obtainFreshBeanFactory():获取一个**全新的 BeanFactory 接口实例** + `refreshBeanFactory()`:创建 BeanFactory,设置序列化 ID、读取 BeanDefinition 并加载到工厂 + + * `if (hasBeanFactory())`:applicationContext 内部拥有一个 beanFactory 实例,需要将该实例完全释放销毁 + * `destroyBeans()`:销毁原 beanFactory 实例,将 beanFactory 内部维护的单实例 bean 全部清掉,如果哪个 bean 实现了 Disposablejie接口,还会进行 bean distroy 方法的调用处理 + * `this.singletonsCurrentlyInDestruction = true`:设置当前 beanFactory 状态为销毁状态 + * `String[] disposableBeanNames`:获取销毁集合中的 bean,如果当前 bean 有**析构函数**就会在销毁集合 + * `destroySingleton(disposableBeanNames[i])`:遍历所有的 disposableBeans,执行销毁方法 + * `removeSingleton(beanName)`:清除三级缓存和 registeredSingletons 中的当前 beanName 的数据 + * `this.disposableBeans.remove(beanName)`:从销毁集合中清除,每个 bean 只能 destroy 一次 + * `destroyBean(beanName, disposableBean)`:销毁 bean + * dependentBeanMap 记录了依赖当前 bean 的其他 bean 信息,因为依赖的对象要被回收了,所以依赖当前 bean 的其他对象都要执行 destroySingleton,遍历 dependentBeanMap 执行销毁 + * `bean.destroy()`:解决完成依赖后,执行 DisposableBean 的 destroy 方法 + * `this.containedBeanMap.remove(beanName)`:处理映射 + * ` this.dependenciesForBeanMap.remove(beanName)`:保存当前 bean 依赖了谁,直接清除 + * 进行一些集合和缓存的清理工作 + * `closeBeanFactory()`:将容器内部的 beanFactory 设置为空,重新创建 + * `beanFactory = createBeanFactory()`:创建新的 DefaultListableBeanFactory 对象 + * `beanFactory.setSerializationId(getId())`:进行 ID 的设置,可以根据 ID 获取 BeanFactory 对象 + * `customizeBeanFactory(beanFactory)`:设置是否允许覆盖和循环引用 + * `loadBeanDefinitions(beanFactory)`:**加载 BeanDefinition 信息,注册到 BeanFactory 中** + * `this.beanFactory = beanFactory`:把 beanFactory 填充至容器中 + + `getBeanFactory()`:返回创建的 DefaultListableBeanFactory 对象,该对象继承 BeanDefinitionRegistry + +* prepareBeanFactory(beanFactory):**BeanFactory 的预准备**工作,向容器中添加一些组件 + + * `setBeanClassLoader(getClassLoader())`:给当前 bf 设置一个类加载器,加载 bd 的 class 信息 + * `setBeanExpressionResolver()`:设置 EL 表达式解析器 + * `addPropertyEditorRegistrar`:添加一个属性编辑器,解决属性注入时的格式转换 + * `addBeanPostProcessor()`:添加后处理器,主要用于向 bean 内部注入一些框架级别的实例 + * `ignoreDependencyInterface()`:设置忽略自动装配的接口,bean 内部的这些类型的字段 不参与依赖注入 + * `registerResolvableDependency()`:注册一些类型依赖关系 + * `addBeanPostProcessor()`:将配置的监听者注册到容器中,当前 bean 实现 ApplicationListener 接口就是**监听器事件** + +* postProcessBeanFactory(beanFactory):BeanFactory 准备工作完成后进行的后置处理工作,通过重写这个方法来在 BeanFactory 创建并预准备完成以后做进一步的设置 + +**以上是 BeanFactory 的创建及预准备工作,接下来进入 Bean 的流程** + +* invokeBeanFactoryPostProcessors(beanFactory):**执行 BeanFactoryPostProcessor 的方法** + + * `processedBeans = new HashSet<>()`:存储已经执行过的 BeanFactoryPostProcessor 的 beanName + + * `if (beanFactory instanceof BeanDefinitionRegistry)`:**当前 BeanFactory 是 bd 的注册中心,bd 全部注册到 bf** + + * `for (BeanFactoryPostProcessor postProcessor : beanFactoryPostProcessors)`:遍历所有的 bf 后置处理器 + + * `if (postProcessor instanceof BeanDefinitionRegistryPostProcessor)`:是 Registry 类的后置处理器 + + `registryProcessor.postProcessBeanDefinitionRegistry(registry)`:向 bf 中注册一些 bd + + `registryProcessors.add(registryProcessor)`:添加到 registryProcessors 集合 + + * `regularPostProcessors.add(postProcessor)`:添加到普通集合 + + * 获取到所有 BeanDefinitionRegistryPostProcessor 和 BeanFactoryPostProcessor 接口类型了,首先回调 bdrpp 类 + + * 执行实现了 PriorityOrdered(主排序接口)接口的 bdrpp,再执行实现了 Ordered(次排序接口)接口的 bdrpp + + * 最后执行没有实现任何优先级或者是顺序接口 bdrpp + + `boolean reiterate = true`:控制 while 是否需要再次循环,循环内是查找并执行 bdrpp 后处理器的 registry 相关的接口方法,接口方法执行以后会向 bf 内注册 bd,注册的 bd 也有可能是 bdrpp 类型,所以需要该变量控制循环 + + * ` invokeBeanFactoryPostProcessors()`:BeanDefinitionRegistryPostProcessor 也继承了 BeanFactoryPostProcessor,也有 postProcessBeanFactory 方法,所以需要调用 + + * 执行普通 BeanFactoryPostProcessor 的相关 postProcessBeanFactory 方法,同 bdrpp,按照主次无次序执行 + + * `beanFactory.clearMetadataCache()`:清除缓存中合并的 bean 定义,因为后置处理器可能更改了元数据 + +* registerBeanPostProcessors(beanFactory):**注册 Bean 的后置处理器**,为了干预 Spring 初始化 bean 的流程,这里仅仅是向容器中**注入而非使用** + + * `beanFactory.getBeanNamesForType(BeanPostProcessor.class)`:**获取配置中实现了 BeanPostProcessor 接口类型** + + * `int beanProcessorTargetCount`:后置处理器的数量,已经注册的 + 未注册的 + 即将要添加的一个 + + * `beanFactory.addBeanPostProcessor(new BeanPostProcessorChecker())`:添加一个后置处理器 + + `BeanPostProcessorChecker.postProcessAfterInitialization()`:初始化后的后处理器方法 + + * `!(bean instanceof BeanPostProcessor) `:当前 bean 类型是普通 bean,不是后置处理器 + * `!isInfrastructureBean(beanName)`:成立说明当前 beanName 是用户级别的 bean 不是 Spring 框架的 + * `this.beanFactory.getBeanPostProcessorCount() < this.beanPostProcessorTargetCount`:BeanFactory 上面注册后处理器数量 < 后处理器数量,说明后处理框架尚未初始化完成 + + * `for (String ppName : postProcessorNames)`:遍历 PostProcessor 集合,**根据实现不同的接口类型添加到不同集合** + + * `sortPostProcessors(priorityOrderedPostProcessors, beanFactory)`:实现 PriorityOrdered 接口的后处理器排序 + + `registerBeanPostProcessors(beanFactory, priorityOrderedPostProcessors)`:注册到 beanFactory 中 + + * 接着排序注册实现 Ordered 接口的后置处理器,然后注册普通的( 没有实现任何优先级接口)后置处理器 + + * 最后排序 MergedBeanDefinitionPostProcessor 类型的处理器,根据实现的排序接口,排序完注册到 beanFactory 中 + + * `beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(applicationContext))`:重新注册 ApplicationListenerDetector 后处理器,用于在 Bean 创建完成后检查是否属于 ApplicationListener 类型,如果是就把 Bean 放到监听器容器中保存起来 + +* initMessageSource():初始化 MessageSource 组件,主要用于做国际化功能,消息绑定与消息解析 + + * `if (beanFactory.containsLocalBean(MESSAGE_SOURCE_BEAN_NAME))`:容器是否含有名称为 messageSource 的 bean + * `beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME, MessageSource.class)`:如果有证明用户自定义了该类型的 bean,获取后直接赋值给 this.messageSource + * `dms = new DelegatingMessageSource()`:容器中没有就新建一个赋值 + +* initApplicationEventMulticaster():**初始化事件传播器**,在注册监听器时会用到 + + * `if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME))`:**条件成立说明用户自定义了事件传播器**,可以实现 ApplicationEventMulticaster 接口编写自己的事件传播器,通过 bean 的方式提供给 Spring + * 如果有就直接从容器中获取;如果没有则创建一个 SimpleApplicationEventMulticaster 注册 + +* onRefresh():留给用户去实现,可以硬编码提供一些组件,比如提供一些监听器 + +* registerListeners():注册通过配置提供的 Listener,这些监听器最终注册到 ApplicationEventMulticaster 内 + + * `for (ApplicationListener listener : getApplicationListeners()) `:注册硬编码实现的监听器 + + * `getBeanNamesForType(ApplicationListener.class, true, false)`:注册通过配置提供的 Listener + + * `multicastEvent(earlyEvent)`:**发布前面步骤产生的事件 applicationEvents** + + `Executor executor = getTaskExecutor()`:获取线程池,有线程池就异步执行,没有就同步执行 + +* finishBeanFactoryInitialization():**实例化非懒加载状态的单实例** + + * `beanFactory.freezeConfiguration()`:**冻结配置信息**,就是冻结 BD 信息,冻结后无法再向 bf 内注册 bd + + * `beanFactory.preInstantiateSingletons()`:实例化 non-lazy-init singletons + + * `for (String beanName : beanNames)`:遍历容器内所有的 beanDefinitionNames + + * `getMergedLocalBeanDefinition(beanName)`:获取与父类合并后的对象(Bean → 获取流程部分详解此函数) + + * `if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit())`:BD 对应的 Class 满足非抽象、单实例,非懒加载,需要预先实例化 + + `if (isFactoryBean(beanName))`:BD 对应的 Class 是 factoryBean 对象 + + * `getBean(FACTORY_BEAN_PREFIX + beanName)`:获取工厂 FactoryBean 实例本身 + * `isEagerInit`:控制 FactoryBean 内部管理的 Bean 是否也初始化 + * `getBean(beanName)`:初始化 Bean,获取 Bean 详解此函数 + + `getBean(beanName)`:不是工厂 bean 直接获取 + + * `for (String beanName : beanNames)`:检查所有的 Bean 是否实现 SmartInitializingSingleton 接口,实现了就执行 afterSingletonsInstantiated(),进行一些创建后的操作 + +* `finishRefresh()`:完成刷新后做的一些事情,主要是启动生命周期 + + * `clearResourceCaches()`:清空上下文缓存 + * `initLifecycleProcessor()`:**初始化和生命周期有关的后置处理器** + * `if (beanFactory.containsLocalBean(LIFECYCLE_PROCESSOR_BEAN_NAME))`:成立说明自定义了生命周期处理器 + * `defaultProcessor = new DefaultLifecycleProcessor()`:Spring 默认提供的生命周期处理器 + * ` beanFactory.registerSingleton()`:将生命周期处理器注册到 bf 的一级缓存和注册单例集合中 + * `getLifecycleProcessor().onRefresh()`:获取该**生命周期后置处理器回调 onRefresh()**,调用 `startBeans(true)` + * `lifecycleBeans = getLifecycleBeans()`:获取到所有实现了 Lifecycle 接口的对象包装到 Map 内,key 是beanName, value 是 Lifecycle 对象 + * `int phase = getPhase(bean)`:获取当前 Lifecycle 的 phase 值,当前生命周期对象可能**依赖**其他生命周期对象的执行结果,所以需要 phase 决定执行顺序,数值越低的优先执行 + * `LifecycleGroup group = phases.get(phase)`:把 phsae 相同的 Lifecycle 存入 LifecycleGroup + * `if (group == null)`:group 为空则创建,初始情况下是空的 + * `group.add(beanName, bean)`:将当前 Lifecycle 添加到当前 phase 值一样的 group 内 + * `Collections.sort(keys)`:从小到大排序,按优先级启动 + * `phases.get(key).start()`:遍历所有的 Lifecycle 对象开始启动 + * `doStart(this.lifecycleBeans, member.name, this.autoStartupOnly)`:底层调用该方法启动 + * `bean = lifecycleBeans.remove(beanName)`: 确保 Lifecycle 只被启动一次,在一个分组内被启动了在其他分组内就看不到 Lifecycle 了 + * `dependenciesForBean = getBeanFactory().getDependenciesForBean(beanName)`:**获取当前即将被启动的 Lifecycle 所依赖的其他 beanName,需要先启动所依赖的 bean,才能启动自身** + * `if ()`:传入的参数 autoStartupOnly 为 true 表示启动 isAutoStartUp 为 true 的 SmartLifecycle 对象,不会启动普通的生命周期的对象;false 代表全部启动 + * bean.start():**调用启动方法** + * `publishEvent(new ContextRefreshedEvent(this))`:**发布容器刷新完成事件** + * `liveBeansView.registerApplicationContext(this)`:暴露 Mbean + +补充生命周期 stop() 方法的调用 + +* DefaultLifecycleProcessor.stop():调用 DefaultLifecycleProcessor.stopBeans() + + * 获取到所有实现了 Lifecycle 接口的对象并按 phase 数值分组的 + + * `keys.sort(Collections.reverseOrder())`:按 phase 降序排序 Lifecycle 接口,最先启动的最晚关闭(责任链?) + + * `phases.get(key).stop()`:遍历所有的 Lifecycle 对象开始停止 + + * `latch = new CountDownLatch(this.smartMemberCount)`:创建 CountDownLatch,设置 latch 内部的值为当前分组内的 smartMemberCount 的数量 + + * `countDownBeanNames = Collections.synchronizedSet(new LinkedHashSet<>())`:保存当前正在处理关闭的smartLifecycle 的 BeanName + + * `for (LifecycleGroupMember member : this.members)`:处理本分组内需要关闭的 Lifecycle + + `doStop(this.lifecycleBeans, member.name, latch, countDownBeanNames)`:真正的停止方法 + + * `getBeanFactory().getDependentBeans(beanName)`:**获取依赖当前 Lifecycle 的其他对象的 beanName**,因为当前的 Lifecycle 即将要关闭了,所有的依赖了当前 Lifecycle 的 bean 也要关闭 + * `countDownBeanNames.add(beanName)`:将当前 SmartLifecycle beanName 添加到 countDownBeanNames 集合内,该集合表示正在关闭的 SmartLifecycle + + * `bean.stop()`:调用停止的方法 + + + +*** + + + +#### 获取Bean + +单实例:在容器启动时创建对象 + +多实例:在每次获取的时候创建对象 + +获取流程:**获取 Bean 时先从单例池获取,如果没有则进行第二次获取,并带上工厂类去创建并添加至单例池** + +Java 启动 Spring 代码: + +```java +ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); +UserService userService = (UserService) context.getBean("userService"); +``` + +AbstractBeanFactory.doGetBean():获取 Bean,context.getBean() 追踪到此 + +* `beanName = transformedBeanName(name)`:name 可能是一个别名,重定向出来真实 beanName;也可能是一个 & 开头的 name,说明要获取的 bean 实例对象,是一个 FactoryBean 对象(IOC 原理 → 核心类) + + * `BeanFactoryUtils.transformedBeanName(name)`:判断是哪种 name,返回截取 & 以后的 name 并放入缓存 + * `transformedBeanNameCache.computeIfAbsent`:缓存是并发安全集合,key == null || value == null 时 put 成功 + * do while 循环一直去除 & 直到不再含有 & + * `canonicalName(name)`:aliasMap 保存别名信息,其中的 do while 逻辑是迭代查找,比如 A 别名叫做 B,但是 B 又有别名叫 C, aliasMap 为 {"C":"B", "B":"A"},get(C) 最后返回的是 A + +* `DefaultSingletonBeanRegistry.getSingleton()`:**第一次获取从缓存池获取**(循环依赖详解此代码) + + * 缓存中有数据进行 getObjectForBeanInstance() 获取可使用的 Bean(本节结束部分详解此函数) + * 缓存中没有数据进行下面的逻辑进行创建 + +* `if(isPrototypeCurrentlyInCreation(beanName))`:检查 bean 是否在原型(Prototype)正在被创建的集合中,如果是就报错,说明产生了循环依赖,**原型模式解决不了循环依赖** + + 原因:先加载 A,把 A 加入集合,A 依赖 B 去加载 B,B 又依赖 A,发现 A 在正在创建集合中,产生循环依赖 + +* `markBeanAsCreated(beanName)`:把 bean 标记为已经创建 + +* `mbd = getMergedLocalBeanDefinition(beanName)`:**获取合并父 BD 后的 BD 对象**,BD 是直接继承的,合并后的 BD 信息是包含父类的 BD 信息 + + * `this.mergedBeanDefinitions.get(beanName)`:从缓存中获取 + + * `if(bd.getParentName()==null)`:beanName 对应 BD 没有父 BD 就不用处理继承,封装为 RootBeanDefinition 返回 + + * `parentBeanName = transformedBeanName(bd.getParentName())`:处理父 BD 的 name 信息 + + * `if(!beanName.equals(parentBeanName))`:一般情况父子 BD 的名称不同 + + `pbd = getMergedBeanDefinition(parentBeanName)`:递归调用,最终返回父 BD 的父 BD 信息 + + * `mbd = new RootBeanDefinition(pbd)`:按照父 BD 信息创建 RootBeanDefinition 对象 + + * `mbd.overrideFrom(bd)`:**子 BD 信息覆盖 mbd**,因为是要以子 BD 为基准,不存在的才去父 BD 寻找(**类似 Java 继承**) + + * `this.mergedBeanDefinitions.put(beanName, mbd)`:放入缓存 + +* `checkMergedBeanDefinition()`:判断当前 BD 是否为**抽象 BD**,抽象 BD 不能创建实例,只能作为父 BD 被继承 + +* `mbd.getDependsOn()`:获取 bean 标签 depends-on + +* `if(dependsOn != null)`:**遍历所有的依赖加载** + + `isDependent(beanName, dep)`:判断循环依赖,出现循环依赖问题报错 + + * 两个 Map:`` + + * dependentBeanMap:记录依赖了当前 beanName 的其他 beanName(谁依赖我,我记录谁) + * dependenciesForBeanMap:记录当前 beanName 依赖的其它 beanName + * 以 B 为视角 dependentBeanMap {"B":{"A"}},以 A 为视角 dependenciesForBeanMap {"A" :{"B"}} + + * `canonicalName(beanName)`:处理 bean 的 name + + * `dependentBeans = this.dependentBeanMap.get(canonicalName)`:获取依赖了当前 bean 的 name + + * `if (dependentBeans.contains(dependentBeanName))`:依赖了当前 bean 的集合中是否有该 name,有就产生循环依赖 + + * 进行递归处理所有的引用:假如 ` ` + + ```java + dependentBeanMap={A:{C}, B:{A}, C:{B}} + // C 依赖 A 判断谁依赖了C 递归判断 谁依赖了B + isDependent(C, A) → C#dependentBeans={B} → isDependent(B, A); → B#dependentBeans={A} //返回true + ``` + + `registerDependentBean(dep, beanName)`:把 bean 和依赖注册到两个 Map 中,注意参数的位置,被依赖的在前 + + `getBean(dep)`:**先加载依赖的 Bean**,又进入 doGetBean() 的逻辑 + +* `if (mbd.isSingleton())`:**判断 bean 是否是单例的 bean** + + `getSingleton(String, ObjectFactory)`:**第二次获取,传入一个工厂对象**,这个方法更倾向于创建实例并返回 + + ```java + sharedInstance = getSingleton(beanName, () -> { + return createBean(beanName, mbd, args);//创建,跳转生命周期 + //lambda表达式,调用了ObjectFactory的getObject()方法,实际回调接口实现的是 createBean()方法进行创建对象 + }); + ``` + + * `singletonObjects.get(beanName)`:从一级缓存检查是否已经被加载,单例模式复用已经创建的bean + + * `this.singletonsCurrentlyInDestruction`:容器销毁时会设置这个属性为 true,这时就不能再创建 bean 实例了 + + * `beforeSingletonCreation(beanName)`:检查构造参数的依赖,**构造参数产生的循环依赖无法解决** + + `!this.singletonsCurrentlyInCreation.add(beanName)`:将当前 beanName 放入到正在创建中单实例集合,放入成功说明没有产生循环依赖,失败则产生循环依赖,进入判断条件内的逻辑抛出异常 + + 原因:加载 A,向正在创建集合中添加了 {A},根据 A 的构造方法实例化 A 对象,发现 A 的构造方法依赖 B,然后加载 B,B 构造方法的参数依赖于 A,又去加载 A 时来到当前方法,因为创建中集合已经存在 A,所以添加失败 + + * `singletonObject = singletonFactory.getObject()`:**实例化 bean**(生命周期部分详解) + + * **创建完成以后,Bean 已经初始化好,是一个完整的可使用的 Bean** + + * `afterSingletonCreation(beanName)`:从正在创建中的集合中移出 + + * `addSingleton(beanName, singletonObject)`:**添加一级缓存单例池中,从二级三级缓存移除** + + `bean = getObjectForBeanInstance`:**单实例可能是普通单实例或者 FactoryBean**,如果是 FactoryBean 实例,需要判断 name 是带 & 还是不带 &,带 & 说明 getBean 获取 FactoryBean 对象,否则是获取 FactoryBean 内部管理的实例 + + * 参数 bean 是未处理 & 的 name,beanName 是处理过 & 和别名后的 name + + * `if(BeanFactoryUtils.isFactoryDereference(name))`:判断 name 前是否带 & + + * `if(!(beanInstance instanceof FactoryBean) || BeanFactoryUtils.isFactoryDereference(name))`:Bean 是普通单实例或者是 FactoryBean 就可以直接返回,否则进入下面的获取一个 **FactoryBean 内部管理的实例**的逻辑 + + * `getCachedObjectForFactoryBean(beanName)`:尝试到缓存获取,获取到直接返回,获取不到进行下面逻辑 + + * `if (mbd == null && containsBeanDefinition(beanName))`:Spring 中有当前 beanName 的 BeanDefinition 信息 + + `mbd = getMergedLocalBeanDefinition(beanName)`:获取合并后的 BeanDefinition + + * `mbd.isSynthetic()`:默认值是 false 表示这是一个用户对象,如果是 true 表示是系统对象 + + * `object = getObjectFromFactoryBean(factory, beanName, !synthetic)`:从工厂内获取实例 + + * `factory.isSingleton() && containsSingleton(beanName)`:工厂内部维护的对象是单实例并且一级缓存存在该 bean + * 首先去缓存中获取,获取不到就使用工厂获取然后放入缓存,进行循环依赖判断 + +* `else if (mbd.isPrototype())`:**bean 是原型的 bean** + + `beforePrototypeCreation(beanName)`:当前线程正在创建的原型对象 beanName 存入 prototypesCurrentlyInCreation + + * `curVal = this.prototypesCurrentlyInCreation.get()`:获取当前线程的正在创建的原型类集合 + * `this.prototypesCurrentlyInCreation.set(beanName)`:集合为空就把当前 beanName 加入 + * `if (curVal instanceof String)`:已经有线程相关原型类创建了,把当前的创建的加进去 + + `createBean(beanName, mbd, args)`:创建原型类对象 + + `afterPrototypeCreation(beanName)`:从正在创建中的集合中移除该 beanName, **与 beforePrototypeCreation逻辑相反** + +* `convertIfNecessary()`:**依赖检查**,检查所需的类型是否与实际 bean 实例的类型匹配 + + + + +*** + + + +#### 生命周期 + +##### 四个阶段 + +Bean 的生命周期:实例化 instantiation,填充属性 populate,初始化 initialization,销毁 destruction + +AbstractAutowireCapableBeanFactory.createBean():进入 Bean 生命周期的流程 + +* `resolvedClass = resolveBeanClass(mbd, beanName)`:判断 mdb 中的 class 是否已经**加载到 JVM**,如果未加载则使用类加载器将 beanName 加载到 JVM中并返回 class 对象 +* `if (resolvedClass != null && !mbd.hasBeanClass() && mbd.getBeanClassName() != null)`:条件成立封装 mbd 并把 resolveBeanClass 设置到 bd 中 + * 条件二:mbd 在 resolveBeanClass 之前是否有 class + * 条件三:mbd 有 className +* `bean = resolveBeforeInstantiation(beanName, mbdToUse)`:实例化前的后置处理器返回一个代理实例对象(不是 AOP) + * 自定义类继承 InstantiationAwareBeanPostProcessor,重写 postProcessBeforeInstantiation 方法,**方法逻辑为创建对象** + * 并配置文件 `` 导入为 bean + * 条件成立,**短路操作**,直接 return bean + +* `Object beanInstance = doCreateBean(beanName, mbdToUse, args)`:Do it + +AbstractAutowireCapableBeanFactory.**doCreateBean**(beanName, RootBeanDefinition, Object[] args):创建 Bean + +* `BeanWrapper instanceWrapper = null`:**Spring 给所有创建的 Bean 实例包装成 BeanWrapper**,内部最核心的方法是获取实例,提供了一些额外的接口方法,比如属性访问器 + +* `instanceWrapper = this.factoryBeanInstanceCache.remove(beanName)`:单例对象尝试从缓存中获取,会移除缓存 + +* `createBeanInstance()`:**缓存中没有实例就进行创建实例**(逻辑复杂,下一小节详解) + +* `if (!mbd.postProcessed)`:每个 bean 只进行一次该逻辑 + + `applyMergedBeanDefinitionPostProcessors()`:后置处理器,合并 bd 信息,接下来要属性填充了 + + `AutowiredAnnotationBeanPostProcessor.postProcessMergedBeanDefinition()`:进入后置处理逻辑 + + * `metadata = findAutowiringMetadata(beanName, beanType, null)`:提取出当前 beanType 类型整个继承体系内的 **@Autowired、@Value、@Inject** 信息,存入一个 InjectionMetadata 对象的 injectedElements 中并放入缓存 + + * `metadata = buildAutowiringMetadata(clazz)`:查询当前 clazz 感兴趣的注解信息 + + * `ReflectionUtils.doWithLocalFields()`:提取字段的注解信息 + + `findAutowiredAnnotation(field)`:代表感兴趣的注解就是那三种 + + * `ReflectionUtils.doWithLocalMethods()`:提取方法的注解信息 + + * `do{} while (targetClass != null && targetClass != Object.class)`:循环从父类中解析,直到 Object 类 + + * `this.injectionMetadataCache.put(cacheKey, metadata)`:存入缓存 + + `mbd.postProcessed = true`:设置为 true,下次访问该逻辑不会再进入 + +* `earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName)`:单例、解决循环引用、是否在单例正在创建集合中 + + ```java + if (earlySingletonExposure) { + //放入三级缓存 + addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); + //lamda 表达式,用来获取提前引用,循环依赖部分详解该逻辑 + } + ``` + +* ` populateBean(beanName, mbd, instanceWrapper)`:**属性填充,依赖注入,整体逻辑是先处理标签再处理注解,填充至 pvs 中,最后通过 apply 方法最后完成属性依赖注入到 BeanWrapper ** + + * `if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName))`:实例化后的后置处理器,默认返回 true,继承 InstantiationAwareBeanPostProcessor 修改返回值为 false,会造成 continueWithPropertyPopulation 为 false + + * `if (!continueWithPropertyPopulation)`:自定义方法返回值会造成该条件成立,逻辑为直接返回,不能进行依赖注入 + + * `PropertyValues pvs = (mbd.hasPropertyValues() ? mbd.getPropertyValues() : null)`:处理依赖注入逻辑开始 + + * `mbd.getResolvedAutowireMode() == ?`:**根据 bean 标签配置**的 autowire 判断是 **BY_NAME 或者 BY_TYPE** + + `autowireByName(beanName, mbd, bw, newPvs)`:根据字段名称去查找依赖的 bean + + * `propertyNames = unsatisfiedNonSimpleProperties(mbd, bw)`:bean 实例中有该字段和该字段的 setter 方法,但是在 bd 中没有 property 属性 + + * 拿到配置的 property 信息和 bean 的所有字段信息 + + * `pd.getWriteMethod() != null`:**当前字段是否有 setter 方法** + + `!isExcludedFromDependencyCheck(pd)`:当前字段类型是否在忽略自动注入的列表中 + + `!pvs.contains(pd.getName()`:当前字段不在 xml 或者其他方式的配置中,也就是 bd 中不存在对应的 property + + `!BeanUtils.isSimpleProperty(pd.getPropertyType()`:是否是基本数据类型和内置的几种数据类型,基本数据类型不允许自动注入 + + * `if (containsBean(propertyName))`:BeanFactory 中存在当前 property 的 bean 实例,说明找到对应的依赖数据 + + * `getBean(propertyName)`:**拿到 propertyName 对应的 bean 实例** + + * `pvs.add(propertyName, bean)`:填充到 pvs 中 + + * `registerDependentBean(propertyName, beanName))`:添加到两个依赖 Map(dependsOn)中 + + `autowireByType(beanName, mbd, bw, newPvs)`:根据字段类型去查找依赖的 bean + + * `desc = new AutowireByTypeDependencyDescriptor(methodParam, eager)`:依赖描述信息 + * `resolveDependency(desc, beanName, autowiredBeanNames, converter)`:根据描述信息,查找依赖对象,容器中没有对应的实例但是有对应的 BD,会调用 getBean(Type) 获取对象 + + `pvs = newPvs`:newPvs 是处理了依赖数据后的 pvs,所以赋值给 pvs + + * `hasInstAwareBpps`:表示当前是否有 InstantiationAwareBeanPostProcessors 的后置处理器 + + * `pvsToUse = ibp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName)`:**@Autowired 注解的注入** + + * `findAutowiringMetadata()`:包装着当前 bd 需要注入的注解信息集合,**三种注解的元数据** + * `InjectionMetadata.InjectedElement.inject()`:将注解信息解析后注入到 pvs,方法和字段的注入的实现不同 + * `ReflectionUtils.makeAccessible()`:修改访问权限,true 代表暴力破解 + * `method.invoke()`:利用反射为此对象赋值 + + * `applyPropertyValues()`:**将所有解析的 PropertyValues 的注入至 BeanWrapper 实例中**(深拷贝) + +* `initializeBean(String,Object,RootBeanDefinition)`:**初始化,分为配置文件和实现接口两种方式** + + * `invokeAwareMethods(beanName, bean)`:根据 bean 是否实现 Aware 接口执行初始化的方法 + + * `wrappedBean = applyBeanPostProcessorsBeforeInitialization`:初始化前的后置处理器,可以继承接口重写方法 + + * `processor.postProcessBeforeInitialization()`:执行后置处理的方法,默认返回 bean 本身 + * `if (current == null) return result`:重写方法返回 null,会造成后置处理的短路,直接返回 + + * `invokeInitMethods(beanName, wrappedBean, mbd)`:**反射执行初始化方法** + + * `isInitializingBean = (bean instanceof InitializingBean)`:初始化方法的定义有两种方式,一种是自定义类实现 InitializingBean 接口,另一种是配置文件配置 + + * `isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))`: + + * 条件一:当前 bean 是不是实现了 InitializingBean + + * 条件二:InitializingBean 接口中的方法 afterPropertiesSet,判断该方法是否是容器外管理的方法 + + * `if (mbd != null && bean.getClass() != NullBean.class)`:成立说明是配置文件的方式 + + `if(!(接口条件))`表示**如果通过接口实现了初始化方法的话,就不会在调用 init-method 定义的方法**, + + `invokeCustomInitMethod`:执行自定义的方法 + + * `initMethodName = mbd.getInitMethodName()`:获取方法名 + * `Method initMethod = ()`:根据方法名获取到 init-method 方法 + * ` methodToInvoke = ClassUtils.getInterfaceMethodIfPossible(initMethod)`:将方法转成从接口层面获取 + * `ReflectionUtils.makeAccessible(methodToInvoke)`:访问权限设置成可访问 + * ` methodToInvoke.invoke(bean)`:**反射调用 init-method 方法**,以当前 bean 为角度去调用 + + * `wrappedBean = applyBeanPostProcessorsAfterInitialization`:初始化后的后置处理器 + + * `AbstractAutoProxyCreator.postProcessAfterInitialization()`:如果 Bean 被子类标识为要代理的 bean,则使用配置的拦截器**创建代理对象**,AOP 部分详解 + + * 如果不存在循环依赖,创建动态代理 bean 在此处完成;否则真正的创建阶段是在属性填充时获取提前引用的阶段,**循环依赖**详解,源码分析: + + ```java + // 该集合用来避免重复将某个 bean 生成代理对象, + private final Map earlyProxyReferences = new ConcurrentHashMap<>(16); + + public Object postProcessAfterInitialization(@Nullable Object bean,String bN){ + if (bean != null) { + // cacheKey 是 beanName 或者加上 & + Object cacheKey = getCacheKey(bean.getClass(), beanName);y + if (this.earlyProxyReferences.remove(cacheKey) != bean) { + //去提前代理引用池中寻找该key,不存在则创建代理 + //如果存在则证明被代理过,则判断是否是当前的 bean,不是则创建代理 + return wrapIfNecessary(bean, bN, cacheKey); + } + } + return bean; + } + ``` + +* `if (earlySingletonExposure)`:是否循序提前引用 + + `earlySingletonReference = getSingleton(beanName, false)`:从二级缓存获取实例 + + `if (earlySingletonReference != null)`:当前 bean 实例从二级缓存中获取到了,说明产生了循环依赖,在属性填充阶段会提前调用三级缓存中的工厂生成 Bean 对象的动态代理,放入二级缓存中,然后使用原始 bean 继续执行初始化 + + * ` if (exposedObject == bean)`:初始化后的 bean == 创建的原始实例,条件成立的两种情况:当前的真实实例不需要被代理、当前实例已经被代理过了,后处理器直接返回 bean 原实例 + + `exposedObject = earlySingletonReference`:把代理后的 Bean 传给 exposedObject 用来 return + + * **下面逻辑是动态代理提前创建,导致当前 bean 无法增强的情况** + + * `!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)`:是否有其他 bean 依赖当前 bean + + * `dependentBeans = getDependentBeans(beanName)`:取到依赖当前 bean 的其他 beanName + + * `if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean))`:判断 dependentBean 是否创建完成 + + * `if (!this.alreadyCreated.contains(beanName))`:成立当前 bean 尚未创建完成,当前 bean 是依赖exposedObject 的 bean,返回 true + * `return false`:创建完成返回 false + + `actualDependentBeans.add(dependentBean)`:创建完成的 dependentBean 加入该集合 + + * `if (!actualDependentBeans.isEmpty())`:条件成立说明有依赖于当前 bean 的 bean 实例创建完成,但是当前对象的 AOP 操作是在 initializeBean 逻辑里完成的,在之前外部 bean 持有到的当前 bean 都是尚未增强的,所以报错 + +* `registerDisposableBeanIfNecessary`:判断当前 bean 是否需要注册析构回调,当容器销毁时进行回调 + + * `if (!mbd.isPrototype() && requiresDestruction(bean, mbd))` + + * 如果是原型 prototype 不会注册析构回调,不会回调该函数,对象的回收由 JVM 的 GC 机制完成 + + * requiresDestruction: + + `DisposableBeanAdapter.hasDestroyMethod(bean, mbd)`:bd 中定义了 DestroyMethod 返回 true + + `hasDestructionAwareBeanPostProcessors()`:后处理器框架决定是否进行析构回调 + + * `registerDisposableBean()`:条件成立进入该方法,给当前单实例注册回调适配器,适配器内根据当前 bean 实例是继承接口(DisposableBean)还是自定义标签来判定具体调用哪个方法实现 + + * `this.disposableBeans.put(beanName, bean)`:向销毁集合添加实例 + + + +**** + + + +##### 创建实例 + +AbstractAutowireCapableBeanFactory.createBeanInstance(beanName, RootBeanDefinition, Object[] args) + +* `resolveBeanClass(mbd, beanName)`:确保 Bean 的 Class 真正的被加载 + +* 判断类的访问权限是不是 public,不是进入下一个判断,是否允许访问类的 non-public 的构造方法,不允许则报错 + +* `if (mbd.getFactoryMethodName() != null)`:**判断 bean 是否设置了 factory-method 属性** + + ,设置了该属性进入 factory-method 方法创建实例 + +* `resolved = false`:代表 bd 对应的构造信息是否已经解析成可以反射调用的构造方法 + +* `autowireNecessary = false`:是否自动匹配构造方法 + +* `if(mbd.resolvedConstructorOrFactoryMethod != null)`:获取 bd 的构造信息转化成反射调用的 method 信息 + + * method 为 null 则 resolved 和 autowireNecessary 都为默认值 false + * `autowireNecessary = mbd.constructorArgumentsResolved`:构造方法有参数,设置为 true + +* bd 对应的构造信息解析完成: + + * `return autowireConstructor(beanName, mbd, null, null)`:**有参构造**,根据参数匹配最优的构造器创建实例 + + * `return instantiateBean(beanName, mbd)`:**无参构造方法通过反射创建实例** + +* `ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName)`:**AutowiredAnnotation 逻辑** + + * 配置了 lookup 的相关逻辑 + + * `this.candidateConstructorsCache.get(beanClass)`:从缓存中获取构造方法,第一次获取为 null,进入下面逻辑 + + * `rawCandidates = beanClass.getDeclaredConstructors()`:获取所有的构造器 + + * `Constructor requiredConstructor = null`:唯一的选项构造器,**@Autowired(required = "true")** 时有值 + + * `for (Constructor candidate : rawCandidates)`:遍历所有的构造器: + + `ann = findAutowiredAnnotation(candidate)`:有三种注解中的一个会返回注解的属性 + + * 遍历 this.autowiredAnnotationTypes 中的三种注解: + + ```java + this.autowiredAnnotationTypes.add(Autowired.class);//!!!!!!!!!!!!!! + this.autowiredAnnotationTypes.add(Value.class); + this.autowiredAnnotationTypes.add(...ClassUtils.forName("javax.inject.Inject")); + ``` + + * ` AnnotatedElementUtils.getMergedAnnotationAttributes(ao, type)`:获取注解的属性 + + * `if (attributes != null) return attributes`:任意一个注解属性不为空就注解返回 + + `if (ann == null)`:注解属性为空 + + * `userClass = ClassUtils.getUserClass(beanClass)`:如果当前 beanClass 是代理对象,方法上就已经没有注解了,所以**获取原始的用户类型重新获取该构造器上的注解属性**(**事务注解失效**也是这个原理) + + `if (ann != null)`:注解属性不为空了 + + * `required = determineRequiredStatus(ann)`:获取 required 属性的值 + + * `!ann.containsKey(this.requiredParameterName) || `:判断属性是否包含 required,不包含进入后面逻辑 + * `this.requiredParameterValue == ann.getBoolean(this.requiredParameterName)`:获取属性值返回 + + * `if (required)`:代表注解 @Autowired(required = true) + + `if (!candidates.isEmpty())`:true 代表只能有一个构造方法,构造集合不是空代表可选的构造器不唯一,报错 + + `requiredConstructor = candidate`:把构造器赋值给 requiredConstructor + + * `candidates.add(candidate)`:**把当前构造方法添加至 candidates 集合** + + ` if(candidate.getParameterCount() == 0)`:当前遍历的构造器的参数为 0 代表没有参数,是**默认构造器**,赋值给 defaultConstructor + + * `candidateConstructors = candidates.toArray(new Constructor[0])`:**将构造器转成数组返回** + +* `if(ctors != null)`:条件成立代表指定了**构造方法数组** + + `mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR`: 标签内 autowiremode 的属性值,默认是 no,AUTOWIRE_CONSTRUCTOR 代表选择最优的构造方法 + + `mbd.hasConstructorArgumentValues()`:bean 信息中是否配置了构造参数的值 + + `!ObjectUtils.isEmpty(args)`:getBean 时,指定了参数 arg + +* `autowireConstructor(beanName, mbd, ctors, args)`:**选择最优的构造器进行创建实例**(非常复杂,可以放弃深究) + + * `beanFactory.initBeanWrapper(bw)`:向 BeanWrapper 中注册转换器,向工厂中注册属性编辑器 + + * `Constructor constructorToUse = null`:实例化反射构造器 + + `ArgumentsHolder argsHolderToUse`:实例化时真正去用的参数,并持有对象 + + * rawArguments 是转换前的参数,arguments 是类型转换完成的参数 + + `Object[] argsToUse`:参数实例化时使用的参数 + + * `Object[] argsToResolve`:表示构造器参数做转换后的参数引用 + + * `if (constructorToUse != null && mbd.constructorArgumentsResolved)`: + + * 条件一成立说明当前 bd 生成的实例不是第一次,缓存中有解析好的构造器方法可以直接拿来反射调用 + * 条件二成立说明构造器参数已经解析过了 + + * `argsToUse = resolvePreparedArguments()`:argsToResolve 不是完全解析好的,还需要继续解析 + + * `if (constructorToUse == null || argsToUse == null)`:条件成立说明缓存机制失败,进入构造器匹配逻辑 + + * `Constructor[] candidates = chosenCtors`:chosenCtors 只有在构造方法上有 autowaire 三种注解时才有数据 + + * `if (candidates == null)`:candidates 为空就根据 beanClass 是否允许访问非公开的方法来获取构造方法 + + * `if (candidates.length == 1 && explicitArgs == null && !mbd.hasConstructorArgumentValues())`:默认无参 + + `bw.setBeanInstance(instantiate())`:**使用无参构造器反射调用,创建出实例对象,设置到 BeanWrapper 中去** + + * `boolean autowiring`:**需要选择最优的构造器** + + * `cargs = mbd.getConstructorArgumentValues()`:获取参数值 + + `resolvedValues = new ConstructorArgumentValues()`:获取已经解析后的构造器参数值 + + * `final Map indexedArgumentValues`:key 是 index, value 是值 + * `final List genericArgumentValues`:没有 index 的值 + + `minNrOfArgs = resolveConstructorArguments(..,resolvedValues)`:从 bd 中解析并获取构造器参数的个数 + + * `valueResolver.resolveValueIfNecessary()`:将引用转换成真实的对象 + * `resolvedValueHolder.setSource(valueHolder)`:将对象填充至 ValueHolder 中 + * ` resolvedValues.addIndexedArgumentValue()`:将参数值封装至 resolvedValues 中 + + * `AutowireUtils.sortConstructors(candidates)`:排序规则 public > 非公开的 > 参数多的 > 参数少的 + + * ` int minTypeDiffWeight = Integer.MAX_VALUE`:值越低说明构造器**参数列表类型**和构造参数的匹配度越高 + + * `Set> ambiguousConstructors`:模棱两可的构造器,两个构造器匹配度相等时放入 + + * `for (Constructor candidate : candidates)`:遍历筛选出 minTypeDiffWeight 最低的构造器 + + * `Class[] paramTypes = candidate.getParameterTypes()`:获取当前处理的构造器的参数类型 + + * `if()`:candidates 是排过序的,当前筛选出来的构造器的优先级一定是优先于后面的 constructor + + * `if (paramTypes.length < minNrOfArgs)`:需求的小于给的,不匹配 + + * `int typeDiffWeight`:获取匹配度 + + * `mbd.isLenientConstructorResolution()`:true 表示 ambiguousConstructors 允许有数据,false 代表不允许有数据,有数据就报错(LenientConstructorResolution:宽松的构造函数解析) + * `argsHolder.getTypeDifferenceWeight(paramTypes)`:选择参数转换前和转换后匹配度最低的,循环向父类中寻找该方法,直到寻找到 Obejct 类 + + * ` if (typeDiffWeight < minTypeDiffWeight)`:条件成立说明当前循环处理的构造器更优 + + * `else if (constructorToUse != null && typeDiffWeight == minTypeDiffWeight)`:当前处理的构造器的计算出来的 DiffWeight 与上一次筛选出来的最优构造器的值一致,说明有模棱两可的情况 + + * `if (constructorToUse == null)`:未找到可以使用的构造器,报错 + + * ` else if (ambiguousConstructors != null && !mbd.isLenientConstructorResolution())`:模棱两可有数据,LenientConstructorResolution == false,所以报错 + + * `argsHolderToUse.storeCache(mbd, constructorToUse)`:匹配成功,进行缓存,方便后来者使用该 bd 实例化 + + * ` bw.setBeanInstance(instantiate(beanName, mbd, constructorToUse, argsToUse))`:匹配成功调用 instantiate 创建出实例对象,设置到 BeanWrapper 中去 + +* `SimpleInstantiationStrategy.instantiate()`:**真正用来实例化的函数**(无论如何都会走到这一步) + + * `if (!bd.hasMethodOverrides())`:没有方法重写覆盖 + + `BeanUtils.instantiateClass(constructorToUse)`:底层调用 java.lang.reflect.Constructor.newInstance() 实例化 + + * `instantiateWithMethodInjection(bd, beanName, owner)`:有方法重写采用 CGLIB 实例化 + + + +**** + + + +#### 循环依赖 + +##### 循环引用 + +循环依赖:是一个或多个对象实例之间存在直接或间接的依赖关系,这种依赖关系构成一个环形调用 + +Spring 循环依赖有三种: + +* 原型模式循环依赖【无法解决】 +* 单例 Bean 循环依赖:构造参数产生依赖【无法解决】 +* 单例 Bean 循环依赖:setter产生依赖【可以解决】 + +解决循环依赖:提前引用,提前暴露创建中的 Bean + +* Spring 先实例化 A,拿到 A 的构造方法反射创建出来 A 的早期实例对象,这个对象被包装成 ObjectFactory 对象,放入三级缓存 +* 处理 A 的依赖数据,检查发现 A 依赖 B 对象,所以 Spring 就会去根据 B 类型到容器中去 getBean(B.class),这里产生递归 +* 拿到 B 的构造方法,进行反射创建出来 B 的早期实例对象,也会把 B 包装成 ObjectFactory 对象,放到三级缓存,处理 B 的依赖数据,检查发现 B 依赖了 A 对象,然后 Spring 就会去根据A类型到容器中去 getBean(A.class) +* 这时获取到 A 的早期对象进入属性填充 + +循环依赖的三级缓存: + +```java +//一级缓存:存放所有初始化完成单实例bean,单例池,key是beanName,value是对应的单实例对象引用 +private final Map singletonObjects = new ConcurrentHashMap<>(256); + +//二级缓存:存放实例化未进行初始化的Bean,提前引用池 +private final Map earlySingletonObjects = new HashMap<>(16); + +/** Cache of singleton factories: bean name to ObjectFactory. 3*/ +private final Map> singletonFactories = new HashMap<>(16); +``` + +* 为什么需要三级缓存? + + * 循环依赖解决需要提前引用动态代理对象,AOP 动态代理是在 Bean 初始化后的后置处理中进行,这时的 bean 已经是成品对象,需要提前进行动态代理,三级缓存的 ObjectFactory 提前产生需要代理的对象 + * 若存在循环依赖,**后置处理不创建代理对象,真正创建代理对象的过程是在getBean(B)的阶段中** + +* 一定会提前引用吗? + + * 出现循环依赖才去使用,不出现就不使用 + +* wrapIfNecessary 一定创建代理对象吗?(AOP 动态代理部分有源码解析) + + * 存在增强器会创建动态代理,不需要增强就不需要创建动态代理对象 + * 不创建就会把最原始的实例化的Bean放到二级缓存,因为 addSingletonFactory 参数中传入了实例化的Bean,在singletonFactory.getObject() 中返回给 singletonObject,放入二级缓存 + +* 什么时候将 Bean 的引用提前暴露给第三级缓存的 ObjectFactory 持有? + + * 实例化之后,依赖注入之前 + + ```java + createBeanInstance --> addSingletonFactory --> populateBean + ``` + + + + + +*** + + + +##### 源码解析 + +假如 A 依赖 B,B 依赖 A + +* 当 A 创建实例后填充属性前,执行: + + ````java + addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)) + ```` + + ```java + // 添加给定的单例工厂以构建指定的单例 + protected void addSingletonFactory(String beanName, ObjectFactory singletonFactory) { + Assert.notNull(singletonFactory, "Singleton factory must not be null"); + synchronized (this.singletonObjects) { + //单例池包含该Bean说明已经创建完成,不需要循环依赖 + if (!this.singletonObjects.containsKey(beanName)) { + this.singletonFactories.put(beanName,singletonFactory);//加入三级缓存 + this.earlySingletonObjects.remove(beanName); + //从二级缓存移除,因为三个Map中都是一个对象,不能同时存在! + this.registeredSingletons.add(beanName); + } + } + } + ``` + + 填充属性时 A 依赖 B,这时需要 getBean(B),接着 B 填充属性时发现依赖 A,去进行**第一次 ** getSingleton(A) + + ```java + public Object getSingleton(String beanName) { + return getSingleton(beanName, true);//为true代表允许拿到早期引用。 + } + protected Object getSingleton(String beanName, boolean allowEarlyReference) { + // 在一级缓存中获取 beanName 对应的单实例对象。 + Object singletonObject = this.singletonObjects.get(beanName); + //条件一成立:单实例确实尚未创建;单实例正在创建,发生了循环依赖 + //条件二成立:代表单实例正在创建 + if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { + synchronized (this.singletonObjects) { + //从二级缓存获取 + singletonObject = this.earlySingletonObjects.get(beanName); + //二级缓存不存在,并且允许获取早期实例对象,去三级缓存查看 + if (singletonObject == null && allowEarlyReference) { + ObjectFactory singletonFactory = this.singletonFactories.get(beanName); + if (singletonFactory != null) { + //从三级缓存获取工厂对象,并得到bean的提前引用 + singletonObject = singletonFactory.getObject(); + //缓存升级,放入二级缓存,提前引用池 + this.earlySingletonObjects.put(beanName, singletonObject); + //从三级缓存移除该对象 + this.singletonFactories.remove(beanName); + } + } + } + } + return singletonObject; + } + ``` + + 从三级缓存获取 A 的 Bean:`singletonFactory.getObject()`,调用了 Lambda 表达式的 getEarlyBeanReference 方法: + + ```java + public Object getEarlyBeanReference(Object bean, String beanName) { + Object cacheKey = getCacheKey(bean.getClass(), beanName); + this.earlyProxyReferences.put(cacheKey, bean); + //向提前引用代理池 earlyProxyReferences 中添加该Bean,防止对象被重新代理 + return wrapIfNecessary(bean, beanName, cacheKey); + //创建代理对象,createProxy + } + ``` + + B 填充了代理后的 A 后初始化完成,**返回原始 A 的逻辑继续执行,这时的 A 还不是代理后的 A** + + + +*** + + + +### AOP + +#### 注解原理 + +@EnableAspectJAutoProxy:AOP 注解驱动,给容器中导入 AspectJAutoProxyRegistrar + +```java +@Import(AspectJAutoProxyRegistrar.class) +public @interface EnableAspectJAutoProxy { + // 是否强制使用 CGLIB 创建代理对象 + // 配置文件方式: + boolean proxyTargetClass() default false; + + // 将当前代理对象暴露到上下文内,方便代理对象内部的真实对象拿到代理对象 + // 配置文件方式: + boolean exposeProxy() default false; +} +``` + +AspectJAutoProxyRegistrar 在用来向容器中注册 **AnnotationAwareAspectJAutoProxyCreator**,以 BeanDefiantion 形式存在,在容器初始化时加载。AnnotationAwareAspectJAutoProxyCreator 间接实现了 InstantiationAwareBeanPostProcessor,Order 接口,该类会在 Bean 的实例化和初始化的前后起作用 + +工作流程:创建 IOC 容器,调用 refresh() 刷新容器,`registerBeanPostProcessors(beanFactory)` 阶段,通过 getBean() 创建 AnnotationAwareAspectJAutoProxyCreator 对象,在生命周期的初始化方法中执行回调 initBeanFactory() 方法初始化注册三个工具类:BeanFactoryAdvisorRetrievalHelperAdapter、ReflectiveAspectJAdvisorFactory、BeanFactoryAspectJAdvisorsBuilderAdapter + + + +*** + + + +#### 动态代理 + +##### 获取通知 + +创建动态代理:AbstractAutoProxyCreator.wrapIfNecessary() + +```java +protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { + //条件一般不成立,很少使用 TargetSourceCreator 去创建对象 BeforeInstantiation 阶段,doCreateBean 之前的阶段 + if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) { + return bean; + } + // advisedBeans 集合保存的是 bean 是否被增强过了 + // 条件成立说明当前 beanName 对应的实例不需要被增强处理,判断是在 BeforeInstantiation 阶段做的 + if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) { + return bean; + } + //条件一:判断当前 bean 类型是否是基础框架类型,这个类的实例不能被增强 + //条件二:shouldSkip 判断当前 beanName 是否是 .ORIGINAL 结尾,如果是就跳过增强逻辑,直接返回 + if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) { + this.advisedBeans.put(cacheKey, Boolean.FALSE); + return bean; + } + + // 查找适合当前 bean 实例 Class 的通知(本节详解) + Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null); + //条件成立说明上面方法查询到适合当前class的通知 + if (specificInterceptors != DO_NOT_PROXY) { + this.advisedBeans.put(cacheKey, Boolean.TRUE); + //根据查询到的增强创建代理对象(下一节详解) + //参数一:目标对象 + //参数二:beanName + //参数三:匹配当前目标对象 clazz 的 Advisor 数据 + Object proxy = createProxy( + bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean)); + //保存代理对象类型 + this.proxyTypes.put(cacheKey, proxy.getClass()); + //返回代理对象 + return proxy; + } + // 执行到这里说明没有查到通知,当前 bean 不需要增强 + this.advisedBeans.put(cacheKey, Boolean.FALSE); + //返回原始的 bean 实例 + return bean; +} +``` + +AbstractAdvisorAutoProxyCreator.getAdvicesAndAdvisorsForBean() + +```java +protected Object[] getAdvicesAndAdvisorsForBean(Class beanClass, String beanName, @Nullable TargetSource targetSource) { + // 查询适合当前类型的增强通知 + List advisors = findEligibleAdvisors(beanClass, beanName); + if (advisors.isEmpty()) { + //增强为空直接返回 null,不需要创建代理 + return DO_NOT_PROXY; + } + // 不是空,转成数组返回 + return advisors.toArray(); +} +``` + +AbstractAdvisorAutoProxyCreator.findEligibleAdvisors(): + +* `candidateAdvisors = findCandidateAdvisors()`:**获取当前容器内可以使用(所有)的 advisor**,调用的是 AnnotationAwareAspectJAutoProxyCreator 类的方法 + + * `advisors = super.findCandidateAdvisors()`:查询出所有 Advisor 类型 + + * `advisorNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors()`:通过 BF 查询出来 BD 配置的 class 中 是 **Advisor 子类的 BeanName** + * `advisors.add()`:使用 Spring 容器获取当前这个 Advisor 类型的实例 + + * `advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors())`:获取添加 @Aspect 注解类中的 Advisor + + `buildAspectJAdvisors()`:构建的方法,**把 Advice 封装成 Advisor**(非常复杂,不建议深究) + + * ` beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, Object.class, true, false)`:获取出容器内 Object 所有的 beanName,就是全部的 + + * ` for (String beanName : beanNames)`:遍历所有的 beanName,判断每个 beanName 对应的 class 是否是 Aspect 类型,就是**加了 @Aspect 注解的类** + + * `factory = new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName)`:使用工厂模式管理 Aspect 的元数据,关联的真实 @Aspect 注解的实例对象 + + * `classAdvisors = this.advisorFactory.getAdvisors(factory)`:获取当前添加了 @Aspect 注解的 class 的 Advisor 相关信息 + + * aspectClass:@Aspect 标签的类的 class + + * `for (Method method : getAdvisorMethods(aspectClass))`:遍历不包括 @Pointcut 注解的方法 + + * `Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, advisors.size(), aspectName)`:将当前 method 包装成 Advisor 数据 + + * `AspectJExpressionPointcut expressionPointcut = getPointcut()`:获取切点表达式 + + * `return new InstantiationModelAwarePointcutAdvisorImpl()`:把 method 中 Advice 包装成 Advisor,Spring 中每个 Advisor 内部一定是持有一个 Advice 的,Advice 内部最重要的数据是当前 method 和aspectInstanceFactory,工厂用来获取实例 + + `this.instantiatedAdvice = instantiateAdvice(this.declaredPointcut)`:实例化 Advice 对象 + + * `this.aspectJAdvisorFactory.getAdvice()`:获取 Advice,逻辑是获取注解信息,根据注解的不同生成对应的 Advice 对象 + + * `advisors.addAll(classAdvisors)`:保存通过 @Aspect 注解定义的 Advisor 数据 + + * `this.aspectBeanNames = aspectNames`:将所有 @Aspect 注解 beanName 缓存起来,表示提取 Advisor 工作完成 + + * `return advisors`:返回 Advisor 列表 + +* `eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName)`:**选出适合当前类型的增强** + + * `if (candidateAdvisors.isEmpty())`:条件成立说明当前 Spring 没有可以操作的 Advisor + + * `List eligibleAdvisors = new ArrayList<>()`:匹配当前 clazz 的 Advisors 信息 + + * `for (Advisor candidate : candidateAdvisors)`:遍历所有的 Advisor + + ` if (canApply(candidate, clazz, hasIntroductions))`:判断遍历的 advisor 是否匹配当前的 class,匹配就加入集合 + + * `if (advisor instanceof PointcutAdvisor)`:创建的 advisor 是 InstantiationModelAwarePointcutAdvisorImpl 类型 + + `PointcutAdvisor pca = (PointcutAdvisor) advisor`:封装当前 Advisor + + `return canApply(pca.getPointcut(), targetClass, hasIntroductions)`:重载该方法 + + * `if (!pc.getClassFilter().matches(targetClass))`:条件成立说明不满足切点定义,直接返回 false + * `methodMatcher = pc.getMethodMatcher()`:获取方法匹配器 + * `Set> classes`:保存目标对象 class 和目标对象父类超类的接口和自身实现的接口 + * `if (!Proxy.isProxyClass(targetClass))`:判断当前实例是不是代理类,确保 class 内存储的数据包括目标对象的class 而不是代理类的 class + * `for (Class clazz : classes)`:检查目标 class 和上级接口的所有方法,查看是否会被方法匹配器匹配,如果有一个方法匹配成功,就说明目标对象 AOP 代理需要增强 + +* `extendAdvisors(eligibleAdvisors)`:在 eligibleAdvisors 列表的 索引 0 的位置添加 DefaultPointcutAdvisor,封装了 ExposeInvocationInterceptor 拦截器 + +* ` eligibleAdvisors = sortAdvisors(eligibleAdvisors)`:对拦截器链进行排序,数值越小优先级越高,高的排在前面 + * 实现 Ordered 或 PriorityOrdered 接口,PriorityOrdered 的级别要优先于 Ordered,使用 OrderComparator 比较器 + * 使用 @Order(Spring 规范)或 @Priority(JDK 规范)注解,使用 AnnotationAwareOrderComparator 比较器 + * ExposeInvocationInterceptor 实现了 PriorityOrdered ,所以总是排在第一位,MethodBeforeAdviceInterceptor 没实现任何接口,所以优先级最低,排在最后 +* `return eligibleAdvisors`:返回拦截器链 + + + +**** + + + +##### 创建代理 + +AbstractAutoProxyCreator.createProxy():根据增强方法创建代理对象 + +* `ProxyFactory proxyFactory = new ProxyFactory()`:此处是无参构造,讲解一下两种有参构造方法: + + * public ProxyFactory(Object target): + + ```java + public ProxyFactory(Object target) { + // 将目标对象封装成 SingletonTargetSource 保存到父类的字段中 + setTarget(target); + // 获取目标对象 class 所有接口保存到 AdvisedSupport 中的 interfaces 集合中 + setInterfaces(ClassUtils.getAllInterfaces(target)); + } + ``` + + ClassUtils.getAllInterfaces(target) 底层调用 getAllInterfacesForClassAsSet(java.lang.Class, java.lang.ClassLoader): + + * `if (clazz.isInterface() && isVisible(clazz, classLoader))`: + * 条件一:判断当前目标对象是接口 + * 条件二:检查给定的类在给定的 ClassLoader 中是否可见 + * `Class[] ifcs = current.getInterfaces()`:拿到自己实现的接口,拿不到接口实现的接口 + * `current = current.getSuperclass()`:递归寻找父类的接口,去获取父类实现的接口 + + * public ProxyFactory(Class proxyInterface, Interceptor interceptor): + + ```java + public ProxyFactory(Class proxyInterface, Interceptor interceptor) { + // 添加一个代理的接口 + addInterface(proxyInterface); + // 添加通知,底层调用 addAdvisor + addAdvice(interceptor); + + // addAdvisor(pos, new DefaultPointcutAdvisor(advice)); + // Spring 中 Advice 对应的接口就是 Advisor,Spring 使用 Advisor 包装 Advice 实例 + } + ``` + +* `proxyFactory.copyFrom(this)`:填充一些信息到 proxyFactory + +* `if (!proxyFactory.isProxyTargetClass())`:条件成立说明没有配置修改过 proxyTargetClass 为 true + + `if (shouldProxyTargetClass(beanClass, beanName))`:如果 bd 内有 preserveTargetClass = true ,那么这个 bd 对应的 class 创建代理时必须使用 CGLIB,条件成立设置 proxyTargetClass 为 true + + `evaluateProxyInterfaces(beanClass, proxyFactory)`:根据目标类判定是否可以使用 JDK 动态代理 + + * `targetInterfaces = ClassUtils.getAllInterfacesForClass()`:获取当前目标对象 class 和父类的全部实现接口 + * `boolean hasReasonableProxyInterface = false`:实现的接口中是否有一个合理的接口 + * `if (!isConfigurationCallbackInterface(ifc) && !isInternalLanguageInterface(ifc) && ifc.getMethods().length > 0)`:遍历所有的接口,如果有任意一个接口满足条件,设置 hRPI 变量为 true + * 条件一:判断当前接口是否是 Spring 生命周期内会回调的接口 + * 条件二:接口不能是 GroovyObject、Factory、MockAccess 类型的 + * 条件三:找到一个可以使用的被代理的接口 + * `if (hasReasonableProxyInterface)`:有合理的接口,将这些接口设置到 proxyFactory 内 + * `proxyFactory.setProxyTargetClass(true)`:没有合理的代理接口,强制使用 CGLIB 创建对象 + +* `advisors = buildAdvisors(beanName, specificInterceptors)`:匹配目标对象 clazz 的 Advisors,填充至 ProxyFactory + +* `proxyFactory.setPreFiltered(true)`:设置为 true 表示传递给 proxyFactory 的 Advisors 信息做过基础 class 匹配 + +* `return proxyFactory.getProxy(getProxyClassLoader())`:创建代理对象 + + ```java + public Object getProxy() { + return createAopProxy().getProxy(); + } + ``` + + * DefaultAopProxyFactory.createAopProxy(AdvisedSupport config):参数是一个配置对象,保存着创建代理需要的生产资料 + + ```java + public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { + //条件一:积极的优化 + //条件二:为 true 代表强制使用 CGLIB 动态代理, + // + // @EnableAspectJAutoProxy(proxyTargetClass = true) + if (config.isOptimize() || config.isProxyTargetClass() || + //条件三:被代理对象没有实现任何接口或者只实现了 SpringProxy 接口,只能使用 CGLIB 动态代理 + hasNoUserSuppliedProxyInterfaces(config)) { + Class targetClass = config.getTargetClass(); + if (targetClass == null) { + throw new AopConfigException(""); + } + // 条件成立说明 target 是接口或者是已经被代理过的类型,只能使用 JDK 动态代理 + if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { + return new JdkDynamicAopProxy(config); // 使用 JDK 动态代理 + } + return new ObjenesisCglibAopProxy(config); // 使用 CGLIB 动态代理 + } + else { + return new JdkDynamicAopProxy(config); // 有接口的情况下只能使用 JDK 动态代理 + } + } + ``` + + * JdkDynamicAopProxy.getProxy(java.lang.ClassLoader):获取 JDK 的代理对象 + + ```java + public JdkDynamicAopProxy(AdvisedSupport config) throws AopConfigException { + // 配置类封装到 JdkDynamicAopProxy 属性中 + this.advised = config; + } + public Object getProxy(@Nullable ClassLoader classLoader) { + // 获取需要代理的接口数组 + Class[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true); + // 查找当前所有的需要代理的接口,看是否有 equals 方法和 hashcode 方法,如果有就做一个标记 + findDefinedEqualsAndHashCodeMethods(proxiedInterfaces); + // classLoader:类加载器 proxiedInterfaces:生成的代理类,需要实现的接口集合 + // this JdkDynamicAopProxy 实现了 InvocationHandler + // 该方法最终返回一个代理类对象 + return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this); + } + ``` + + AopProxyUtils.completeProxiedInterfaces(this.advised, true):获取代理的接口数组 + + * `specifiedInterfaces = advised.getProxiedInterfaces()`:从 ProxyFactory 中拿到所有的 target 提取出来的接口 + * `if (specifiedInterfaces.length == 0)`:如果没有实现接口,检查当前 target 是不是接口或者已经是代理类,封装到 ProxyFactory 的 interfaces 集合中 + + * ` addSpringProxy = !advised.isInterfaceProxied(SpringProxy.class)`:判断目标对象所有接口中是否有 **SpringProxy** 接口,没有的话需要添加,这个接口**标识这个代理类型是 Spring 管理的** + * `addAdvised = !advised.isOpaque() && !advised.isInterfaceProxied(Advised.class)`:判断目标对象的所有接口,是否已经有 Advised 接口 + * ` addDecoratingProxy = (decoratingProxy && !advised.isInterfaceProxied(DecoratingProxy.class))`:判断目标对象的所有接口,是否已经有 DecoratingProxy 接口 + * `int nonUserIfcCount = 0`:非用户自己定义的接口数量,接下来要添加上面的三个接口了 + * `proxiedInterfaces = new Class[specifiedInterfaces.length + nonUserIfcCount]`:创建一个新的 class 数组,长度是原目标对象提取出来的接口数量和 Spring 追加的数量,然后进行 **System.arraycopy 拷贝到新数组中** + * `int index = specifiedInterfaces.length`:获取原目标对象提取出来的接口数量,当作 index + * `if(addSpringProxy)`:根据上面三个布尔值把接口添加到新数组中 + * `return proxiedInterfaces`:返回追加后的接口集合 + + JdkDynamicAopProxy.findDefinedEqualsAndHashCodeMethods(): + + * `for (Class proxiedInterface : proxiedInterfaces)`:遍历所有的接口 + + ` Method[] methods = proxiedInterface.getDeclaredMethods()`:获取接口中的所有方法 + + `for (Method method : methods)`:遍历所有的方法 + + * `if (AopUtils.isEqualsMethod(method))`:当前方法是 equals 方法,把 equalsDefined 置为 true + * `if (AopUtils.isHashCodeMethod(method))`:当前方法是 hashCode 方法,把 hashCodeDefined 置为 true + + * `if (this.equalsDefined && this.hashCodeDefined)`:如果有一个接口中有这两种方法,直接返回 + + + +*** + + + +#### 方法增强 + +JdkDynamicAopProxy 类中的 invoke 方法是真正执行代理方法 + +```java +public Object invoke(Object proxy, Method method, Object[] args) +//proxy:代理对象 +//method:目标对象的方法 +//args:目标对象方法对应的参数 +``` + +* `targetSource = this.advised.targetSource`:advised 就是初始化 JdkDynamicAopProxy 对象时传入的变量 + +* `if (!this.equalsDefined && AopUtils.isEqualsMethod(method))`:条件成立说明代理类实现的接口没有定义 equals 方法,并且当前 method 调用 equals 方法,就调用 JdkDynamicAopProxy 提供的 equals 方法 + +* `if (this.advised.exposeProxy)`:需不需要暴露当前代理对象到 AOP 上下文内,true 暴露 + + `oldProxy = AopContext.setCurrentProxy(proxy)`:把代理对象设置到上下文环境 + + `setProxyContext = true`:允许提前引用 + +* `target = targetSource.getTarget()`:根据 targetSource 获取真正的代理对象 + +* `chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass)`:**查找适合该方法的增强**,首先从缓存中查找,查找不到进入主方法 + + * `AdvisorAdapterRegistry registry = GlobalAdvisorAdapterRegistry.getInstance()`:向容器注册适配器,**可以将非 Advisor 类型的增强,包装成为 Advisor,将 Advisor 类型的增强提取出来对应的 MethodInterceptor** + + * `instance = new DefaultAdvisorAdapterRegistry()`:该对象向容器中注册了 MethodBeforeAdviceAdapter、AfterReturningAdviceAdapter、ThrowsAdviceAdapter **三个适配器** + + * `advisors = config.getAdvisors()`:获取 ProxyFactory 内部持有的增强信息 + + * `interceptorList = new ArrayList<>(advisors.length)`:拦截器列表 + + * `actualClass = (targetClass != null ? targetClass : method.getDeclaringClass())`:真实的目标对象类型 + + * `Boolean hasIntroductions = null`:引介增强,不关心 + + * `for (Advisor advisor : advisors)`:**遍历所有的增强** + + * `if (advisor instanceof PointcutAdvisor)`:条件成立说明当前 Advisor 是包含切点信息的,做匹配逻辑 + + `pointcutAdvisor = (PointcutAdvisor) advisor`:转成可以获取到切点信息的接口 + + `if()`:当前代理被预处理,或者当前被代理的 class 对象匹配当前 Advisor 成功,只是 class 匹配成功 + + * `mm = pointcutAdvisor.getPointcut().getMethodMatcher()`:获取切点的方法匹配器 + + * `match = mm.matches(method, actualClass)`:**静态匹配成功返回 true,只关注于处理类及其方法,不考虑参数** + + `if (match)`:如果静态切点检查是匹配的,在运行的时候才进行**动态切点检查,会考虑参数匹配**(代表传入了参数)。如果静态匹配失败,直接不需要进行参数匹配,提高了工作效率 + + * `interceptors = registry.getInterceptors(advisor)`:提取出 advisor 内持有的拦截器信息 + * 遍历三个适配器,获取拦截器,比如 MethodBeforeAdviceAdapter: + * `MethodBeforeAdvice advice = (MethodBeforeAdvice) advisor.getAdvice()`:获取增强方法 + * `return new MethodBeforeAdviceInterceptor(advice)`:封装成适配器对象返回 + * `interceptorList.add(new InterceptorAndDynamicMethodMatcher(interceptor, mm))`:向拦截器链添加动态匹配器 + * `interceptorList.addAll(Arrays.asList(interceptors))`:将当前 advisor 内部的方法拦截器追加到 interceptorList + + * `interceptors = registry.getInterceptors(advisor)`:进入 else 的逻辑,说明当前 Advisor 匹配全部 class 的全部 method,全部加入到 interceptorList + + * `return interceptorList`:返回 method 方法的拦截器链 + +* `if (chain.isEmpty())`:查询出来匹配当前方法的拦截器,数量是 0 说明当前 method 不需要被增强,直接调用目标方法 + + `retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse)`:调用目标对象的目标方法 + +* `invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain)`:**有匹配当前 method 的方法拦截器,要做增强处理**,把方法信息封装到方法调用器里 + + `retVal = invocation.proceed()`:**核心拦截器链驱动方法** + + * `if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1)`:条件成立说明方法拦截器全部都已经调用过了,接下来需要执行目标对象的目标方法 + + `return invokeJoinpoint()`:调用连接点 + + * `this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex)`:获取下一个方法拦截器 + + * `if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher)`:需要运行时匹配 + + `if (dm.methodMatcher.matches(this.method, targetClass, this.arguments))`:判断是否匹配成功 + + * `return dm.interceptor.invoke(this)`:匹配成功,执行方法 + * `return proceed()`:匹配失败跳过当前拦截器 + + * `return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this)`:让当前方法拦截器执行 + +* `retVal = proxy`:如果目标方法返回目标对象,这里做个普通替换返回代理对象 + +* `if (setProxyContext)`:如果允许了提前暴露,这里需要设置为初始状态 + + `AopContext.setCurrentProxy(oldProxy)`:当前代理对象已经完成工作,把原始对象设置回上下文 + +proceed() 链式获取每一个拦截器,拦截器执行 invoke方法,每一个拦截器等待下一个拦截器执行完成返回以后再来执行;拦截器链的机制,保证通知方法与目标方法的执行顺序 + +图示先从上往下建立链,然后从下往上依次执行,责任链模式 + +* 正常执行:(环绕通知)→ 前置通知 → 目标方法 → 后置通知 → 返回通知 + * 出现异常:(环绕通知)→ 前置通知 → 目标方法 → 后置通知 → 异常通知 + +![](https://gitee.com/seazean/images/raw/master/Frame/Spring-AOP动态代理执行方法.png) + + + + + + +*** + + + +### 注解 + +#### Component + +解析 @Component 和 @Service 都是常用的注解 + +* **@Component 解析流程:** + + 打开源码注释:@see org.....ClassPathBeanDefinitionScanner.doScan() + + findCandidateComponents():从classPath扫描组件,并转换为备选BeanDefinition + + ```java + protected Set doScan(String... basePackages) { + Set beanDefinitions = new LinkedHashSet<>(); + for (String basePackage : basePackages) { + //findCandidateComponents 读资源装换为BeanDefinition + Set candidates = findCandidateComponents(basePackage); + for (BeanDefinition candidate : candidates) {//....} + //....... + return beanDefinitions; + } + ``` + + ClassPathScanningCandidateComponentProvider.findCandidateComponents() + + ```java + public Set findCandidateComponents(String basePackage) { + if (this.componentsIndex != null && indexSupportsIncludeFilters()) { + return addCandidateComponentsFromIndex(this.componentsIndex, basePackage); + } + else { + return scanCandidateComponents(basePackage); + } + } + ``` + + ```java + private Set scanCandidateComponents(String basePackage) {} + ``` + + * `String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX resolveBasePackage(basePackage) + '/' + this.resourcePattern` :将package转化为ClassLoader类资源搜索路径packageSearchPath,例如:`com.wl.spring.boot`转化为`classpath*:com/wl/spring/boot/**/*.class` + * `Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath)`:加载搜素路径下的资源 + * `MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource)`:获取元数据阅读器 + * isCandidateComponent:判断是否是备选组件 + * candidates.add(sbd):添加到返回结果的list + + isCandidateComponent源码: + + ```java + protected boolean isCandidateComponent(MetadataReader m) throws IOException { + //.... + for (TypeFilter tf : this.includeFilters) { + if (tf.match(m, getMetadataReaderFactory())) { + return isConditionMatch(metadataReader); + //.... + } + ``` + + ```java + protected void registerDefaultFilters() { + this.includeFilters.add(new AnnotationTypeFilter(Component.class));//... + } + ``` + + includeFilters由`registerDefaultFilters()`设置初始值,有@Component,没有@Service + + 因为@Component是@Service的元注解,Spring在读取@Service,也读取了它的元注解,并将@Service作为@Component处理 + + ```java + @Target({ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @Component + public @interface Service {} + ``` + +* **@Component 派生性流程:** + + metadataReader本质上:`MetadataReader metadataReader =new SimpleMetadataReader(...);` + + `isCandidateComponent.match()`方法:`TypeFilter.match` -->`AnnotationTypeFilter.matchSelf()` + + ```java + @Override + protected boolean matchSelf(MetadataReader metadataReader) { + AnnotationMetadata metadata = metadataReader.getAnnotationMetadata(); + return metadata.hasAnnotation(this.annotationType.getName()) || + (this.considerMetaAnnotations && metadata.hasMetaAnnotation(this.annotationType.getName())); + } + ``` + + * `metadata = new SimpleMetadataReader(...).getAnnotationMetadata()` + + ```java + @Override + public AnnotationMetadata getAnnotationMetadata() { + return this.annotationMetadata; + } + ``` + + 观察源码:`annotationMetadata = new AnnotationMetadataReadingVisitor(classLoader);` + + * `metadata.hasMetaAnnotation=AnnotationMetadataReadingVisitor.hasMetaAnnotation` + + 判断该注解的元注解在不在metaAnnotationMap中,如果在就返回true + + ```java + @Override + public boolean hasMetaAnnotation(String metaAnnotationType) { + Collection> allMetaTypes = this.metaAnnotationMap.values(); + for (Set metaTypes : allMetaTypes) { + if (metaTypes.contains(metaAnnotationType)) { + return true; + } + } + return false; + } + ``` + + metaAnnotationMap怎么赋值的? + + metaAnnotationMap赋值方法在`SimpleMetadataReader.SimpleMetadataReader`中: + + ```java + classReader.accept(visitor, ClassReader.SKIP_DEBUG); + ``` + + 然后通过readElementValues方法中: + + ```java + annotationVisitor.visitEnd(); + ``` + + 追踪方法:`AnnotationAttributesReadingVisitor.visitEnd()` + + 递归读取:内部方法`recursivelyCollectMetaAnnotations()`递归的读取注解,与注解的元注解(读@Service,再读元注解@Component), + + 添加数据:`this.metaAnnotationMap.put(annotationClass.getName(), metaAnnotationTypeNames);` + + + +*** + + + +#### Autowired + +打开 @Autowired 源码,注释上写 Please consult the javadoc for the AutowiredAnnotationBeanPostProcessor + +AutowiredAnnotationBeanPostProcessor 间接实现 InstantiationAwareBeanPostProcessor,就具备了实例化前后(而不是初始化前后)管理对象的能力,实现了 BeanPostProcessor,具有初始化前后管理对象的能力,实现 BeanFactoryAware,具备随时拿到 BeanFactory 的能力,所以这个类**具备一切后置处理器的能力** + +**在容器启动,为对象赋值的时候,遇到 @Autowired 注解,会用后置处理器机制,来创建属性的实例,然后再利用反射机制,将实例化好的属性,赋值给对象上,这就是 Autowired 的原理** + +作用时机: + +* Spring 在每个 Bean 实例化之后,调用 AutowiredAnnotationBeanPostProcessor 的 `postProcessMergedBeanDefinition()` 方法,查找该 Bean 是否有 @Autowired 注解 +* Spring 在每个 Bean 调用 `populateBean()` 进行属性注入的时候,即调用 `postProcessProperties()` 方法,查找该 Bean 属性是否有 @Autowired 注解 + + + +*** + + + +#### Transactional + +如果一个类或者一个类中的 public 方法上被标注 @Transactional 注解的话,Spring 容器就会在启动的时候为其创建一个代理类,在调用被@Transactional注解的 public 方法的时候,实际调用的是TransactionInterceptor类中的 invoke()方法。这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务 + +`TransactionInterceptor` 类中的 `invoke()`方法内部实际调用的是 `TransactionAspectSupport` 类的 `invokeWithinTransaction()`方法 + + From 0bbc1d25fbc6c802296f9b67d0e3961b99b19ecf Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 29 Jul 2021 17:31:15 +0800 Subject: [PATCH 004/168] Update Java Notes --- DB.md | 4 +- Java.md | 13 +- Prog.md | 2 +- SSM.md | 510 +++++++++++++++++++++++++++++++++----------------------- 4 files changed, 307 insertions(+), 222 deletions(-) diff --git a/DB.md b/DB.md index 2437621..14d5e3b 100644 --- a/DB.md +++ b/DB.md @@ -6628,11 +6628,11 @@ Connection:数据库连接对象 - 获取普通执行者对象:`Statement createStatement()` - 获取预编译执行者对象:`PreparedStatement prepareStatement(String sql)` - 管理事务 - - 开启事务:`setAutoCommit(boolean autoCommit)`,false开启事务,true自动提交模式(默认) + - 开启事务:`setAutoCommit(boolean autoCommit)`,false 开启事务,true 自动提交模式(默认) - 提交事务:`void commit()` - 回滚事务:`void rollback()` - 释放资源 - - 释放此Connection对象的数据库和JDBC资源:`void close()` + - 释放此 Connection 对象的数据库和 JDBC 资源:`void close()` diff --git a/Java.md b/Java.md index 3e38f77..18e3cab 100644 --- a/Java.md +++ b/Java.md @@ -8378,7 +8378,7 @@ public class ReflectDemo { ### 注解格式 -定义格式:自定义注解用@interface关键字,注解默认可以标记很多地方 +定义格式:自定义注解用 @interface 关键字,注解默认可以标记很多地方 ```java 修饰符 @interface 注解名{ @@ -18588,6 +18588,7 @@ public static Object newProxyInstance(ClassLoader loader, //获取代理类的构造方法,根据参数 InvocationHandler 匹配获取某个构造器 final Constructor cons = cl.getConstructor(constructorParams); final InvocationHandler ih = h; + //构造方法不是 pubic 的需要启用权限 if (!Modifier.isPublic(cl.getModifiers())) { AccessController.doPrivileged(new PrivilegedAction() { public Void run() { @@ -18603,6 +18604,8 @@ public static Object newProxyInstance(ClassLoader loader, } ``` +Proxy 的静态内部类: + ```java private static final class ProxyClassFactory { // 代理类型的名称前缀 @@ -18693,8 +18696,6 @@ private static final class ProxyClassFactory { - - *** @@ -18745,7 +18746,7 @@ CGLIB 的优缺点 * 优点: * CGLIB 动态代理**不限定**是否具有接口,可以对任意操作进行增强 * CGLIB 动态代理无需要原始被代理对象,动态创建出新的代理对象 - * JDKProxy 仅对接口方法做增强,CGLIB 对所有方法做增强,包括 Object 类中的方法,toString、hashCode 等 + * **JDKProxy 仅对接口方法做增强,CGLIB 对所有方法做增强**,包括 Object 类中的方法,toString、hashCode 等 * 缺点:CGLIB 不能对声明为 final 的类或者方法进行代理,因为 CGLIB 原理是**动态生成被代理类的子类,继承被代理类** @@ -18830,7 +18831,7 @@ CGLIB 的优缺点 例如:现有一台电脑只能读取 SD 卡,而要读取 TF 卡中的内容的话就需要使用到适配器模式 -* SD卡: +* SD 卡: ```java //接口 @@ -18930,7 +18931,7 @@ CGLIB 的优缺点 * 适配器类: ```java - public class SDAdapterTF implements SDCard { + public class SDAdapterTF implements SDCard { private TFCard tfCard; public SDAdapterTF(TFCard tfCard) { this.tfCard = tfCard; diff --git a/Prog.md b/Prog.md index ca90886..460d7ed 100644 --- a/Prog.md +++ b/Prog.md @@ -6,7 +6,7 @@ 进程:程序是静止的,进程实体的运行过程就是进程,是系统进行资源分配的基本单位 -进程的特征:动态性、并发性、独立性、异步性、结构性 +进程的特征:并发性、异步性、动态性、独立性、结构性 **线程**:线程是属于进程的,是一个基本的 CPU 执行单元,是程序执行流的最小单元。线程是进程中的一个实体,是被系统独立调度和分配的基本单位,线程本身不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其他线程共享进程所拥有的全部资源。 diff --git a/SSM.md b/SSM.md index 32ea78f..c743f99 100644 --- a/SSM.md +++ b/SSM.md @@ -2449,7 +2449,7 @@ executor.query():开始执行查询语句,参数通过 wrapCollection() 包 * `handler.parameterize()`:进行参数的设置 * `ParameterHandler.setParameters()`:**通过 ParameterHandler 设置参数** * `typeHandler.setParameter()`:**通过 TypeHandler 预编译 SQL** - * `StatementHandler.query()`:**封装成 java 原生的 PreparedStatement 执行 SQL** + * `StatementHandler.query()`:**封装成 JDBC 的 PreparedStatement 执行 SQL** * `resultSetHandler.handleResultSets(ps)`:**通过 ResultSetHandler 对象封装结果集** @@ -3978,7 +3978,7 @@ public class MainTest { 类型:**类注解** -作用:设置spring配置加载类扫描规则 +作用:设置 Spring 配置加载类扫描规则 格式: @@ -4032,6 +4032,10 @@ public class ClassName{} +*** + + + ##### 作用范围 名称:@Scope @@ -4053,6 +4057,10 @@ public class ClassName{} +*** + + + ##### 生命周期 名称:@PostConstruct、@PreDestroy @@ -4113,7 +4121,7 @@ public DruidDataSource createDataSource() { return ……; } 相关属性 -- value(默认):定义bean的访问id +- value(默认):定义 bean 的访问 id - initMethod:声明初始化方法 - destroyMethod:声明销毁方法 @@ -4444,13 +4452,13 @@ public class ClassName { ##### Junit -Spring接管Junit的运行权,使用Spring专用的Junit类加载器,为Junit测试用例设定对应的spring容器 +Spring 接管 Junit 的运行权,使用 Spring 专用的 Junit 类加载器,为 Junit 测试用例设定对应的 Spring 容器 注意: -- 从Spring5.0以后,要求Junit的版本必须是4.12及以上 +- 从 Spring5.0 以后,要求 Junit 的版本必须是4.12及以上 -- Junit仅用于单元测试,不能将Junit的测试类配置成spring的bean,否则该配置将会被打包进入工程中 +- Junit 仅用于单元测试,不能将 Junit 的测试类配置成 Spring 的 bean,否则该配置将会被打包进入工程中 test / java / service / UserServiceTest @@ -4714,15 +4722,14 @@ FactoryBean与 BeanFactory 区别: #### 导入器 -- bean 只有通过配置才可以进入 spring 容器,被 spring 加载并控制 +bean 只有通过配置才可以进入 Spring 容器,被 Spring 加载并控制 - 配置 bean 的方式如下: - XML 文件中使用 标签配置 - - 使用 @Component 及衍生注解配置 -- **快速高效导入大量 bean 的方式,替代 @Import({a.class,b.class}),无需在每个类上添加 @Bean** +导入器可以快速高效导入大量 bean,替代 @Import({a.class,b.class}),无需在每个类上添加 @Bean 名称: ImportSelector @@ -6465,9 +6472,9 @@ Spirng 可以通过配置的形式控制使用的代理形式,Spring 会先判 * 当数据库操作序列中个别操作失败时,提供一种方式使数据库状态恢复到正常状态(**A**),保障数据库即使在异常状态下仍能保持数据一致性(**C**)(要么操作前状态,要么操作后状态) * 当出现并发访问数据库时,在多个访问间进行相互隔离,防止并发访问操作结果互相干扰(**I**) -Spring事务一般加到业务层,对应着业务的操作,Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,Spring是无法提供事务功能的,Spring只提供统一事务管理接口 +Spring 事务一般加到业务层,对应着业务的操作,Spring 事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,Spring 是无法提供事务功能的,Spring 只提供统一事务管理接口 -Spring在事务开始时,根据当前环境中设置的隔离级别,调整数据库隔离级别,由此保持一致。程序是否支持事务首先取决于数据库 ,比如MySQL ,如果是 **innodb 引擎**,是支持事务的;如果MySQL使用myisam引擎,那从根上就是不支持事务的 +Spring 在事务开始时,根据当前环境中设置的隔离级别,调整数据库隔离级别,由此保持一致。程序是否支持事务首先取决于数据库 ,比如 MySQL ,如果是 **innodb 引擎**,是支持事务的;如果 MySQ L使用 myisam 引擎,那从根上就是不支持事务的 **保证原子性**: @@ -6475,8 +6482,6 @@ Spring在事务开始时,根据当前环境中设置的隔离级别,调整 * 在 MySQL 中,恢复机制是通过**回滚日志(undo log)** 实现,所有事务进行的修改都会先先记录到这个回滚日志中,然后再执行相关的操作。如果执行过程中遇到异常的话,直接利用回滚日志中的信息将数据回滚到修改之前的样子即可 * 回滚日志会先于数据持久化到磁盘上,这样保证了即使遇到数据库突然宕机等情况,当用户再次启动数据库的时候,数据库还能够通过查询回滚日志来回滚将之前未完成的事务 -事务不生效的问题:参考 **Transactional注解** - *** @@ -6507,14 +6512,14 @@ MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ( #### 传播行为 -事务传播行为是为了解决业务层方法之间互相调用的事务问题: +事务传播行为是为了解决业务层方法之间互相调用的事务问题,也就是方法嵌套: * 当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。 * 例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行 ```java - //A 类的aMethod()方法中调用了 B 类的 bMethod() 方法 + //外层事务 Service A 的 aMethod 调用内层 Service B 的 bMethod class A { @Transactional(propagation=propagation.xxx) public void aMethod { @@ -6531,18 +6536,26 @@ MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ( **支持当前事务的情况:** * TransactionDefinition.PROPAGATION_REQUIRED: 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务 + * 内外层是相同的事务 + * 在 aMethod 或者在 bMethod 内的任何地方出现异常,事务都会被回滚 * TransactionDefinition.PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行 -* TransactionDefinition.PROPAGATION_MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常(mandatory:强制性) +* TransactionDefinition.PROPAGATION_MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常 **不支持当前事务的情况:** -- TransactionDefinition.PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起 +- TransactionDefinition.PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。 + - 内外层是不同的事务,如果 bMethod 已经提交,如果 aMethod 失败回滚 ,bMethod 不会回滚 + - 如果 bMethod 失败回滚,ServiceB 抛出的异常被 ServiceA 捕获,如果 B 抛出的异常是 A 会回滚的异常,aMethod 事务需要回滚,否则仍然可以提交 - TransactionDefinition.PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起 - TransactionDefinition.PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常 **其他情况:** -* TransactionDefinition.PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 TransactionDefinition.PROPAGATION_REQUIRED +* TransactionDefinition.PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 PROPAGATION_REQUIRED + * 如果 ServiceB 异常回滚,可以通过 try-catch 机制执行 ServiceC + * 如果 ServiceB 提交, ServiceA 可以根据具体的配置决定是 commit 还是 rollback + +requied:必须的、supports:支持的、mandatory:强制的、nested:嵌套的 @@ -6566,28 +6579,8 @@ MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ( 读操作为什么需要启用事务支持: -* MySQL 默认对每一个新建立的连接都启用了`autocommit`模式,在该模式下,每一个发送到 MySQL 服务器的`sql`语句都会在一个**单独**的事务中进行处理,执行结束后会自动提交事务,并开启一个新的事务 -* 执行多条查询语句,如果方法加上了`Transactional`注解,这个方法执行的所有`sql`会被放在一个事务中,如果声明了只读事务的话,数据库就会去优化它的执行,并不会带来其他的收益。如果不加`Transactional`,每条`sql`会开启一个单独的事务,中间被其它事务修改了数据,比如在前条 SQL 查询之后,后条 SQL 查询之前,数据被其他用户改变,则这次整体的统计查询将会出**现读数据不一致的状态** - - - -*** - - - -#### 回滚规则 - -默认情况下,事务只有遇到运行期异常(RuntimeException 的子类)时才会回滚,Error 也会导致事务回滚,但是,在遇到检查型(Checked)异常时不会回滚 - -可以自定义定哪些异常会导致事务回滚而哪些不会: - -```java -@Transactional(rollbackFor= MyException.class) -//回滚定义的特定的异常类型的 -//noRollbackFor设置遇到哪些错误不需要回滚 -``` - -声明式事务部分详解注解 +* MySQL 默认对每一个新建立的连接都启用了 `autocommit` 模式,在该模式下,每一个发送到 MySQL 服务器的 `sql` 语句都会在一个**单独**的事务中进行处理,执行结束后会自动提交事务,并开启一个新的事务 +* 执行多条查询语句,如果方法加上了 `Transactional` 注解,这个方法执行的所有 `sql` 会被放在一个事务中,如果声明了只读事务的话,数据库就会去优化它的执行,并不会带来其他的收益。如果不加 `Transactional`,每条 `sql` 会开启一个单独的事务,中间被其它事务修改了数据,比如在前条 SQL 查询之后,后条 SQL 查询之前,数据被其他用户改变,则这次整体的统计查询将会出**现读数据不一致的状态** @@ -6618,15 +6611,15 @@ Spring 为业务层提供了整套的事务解决方案: PlatformTransactionManager,平台事务管理器实现类: -- DataSourceTransactionManager 适用于Spring JDBC或MyBatis +- DataSourceTransactionManager 适用于 Spring JDBC 或 MyBatis -- HibernateTransactionManager 适用于Hibernate3.0及以上版本 +- HibernateTransactionManager 适用于 Hibernate3.0 及以上版本 -- JpaTransactionManager 适用于JPA +- JpaTransactionManager 适用于 JPA -- JdoTransactionManager 适用于JDO +- JdoTransactionManager 适用于 JDO -- JtaTransactionManager 适用于JTA +- JtaTransactionManager 适用于 JTA 管理器: @@ -6822,11 +6815,13 @@ TransactionStatus 此接口定义了事务在执行过程中某个时间点上 #### 编程式 +编程式事务就是代码显式的给出事务的开启和提交 + * 修改业务层实现提供转账操作:AccountServiceImpl ```java public void transfer(String outName,String inName,Double money){ - //1.创建事务管理器 + //1.创建事务管理器,开启事务 DataSourceTransactionManager dstm = new DataSourceTransactionManager(); //2.为事务管理器设置与数据层相同的数据源 dstm.setDataSource(dataSource); @@ -6858,7 +6853,7 @@ TransactionStatus 此接口定义了事务在执行过程中某个时间点上 #### AOP改造 -* 将业务层的事务处理功能抽取出来制作成AOP通知,利用环绕通知运行期动态织入 +* 将业务层的事务处理功能抽取出来制作成 AOP 通知,利用环绕通知运行期动态织入 ```java public class TxAdvice { @@ -6885,7 +6880,7 @@ TransactionStatus 此接口定义了事务在执行过程中某个时间点上 } ``` -* 配置applicationContext.xml,要开启AOP空间 +* 配置 applicationContext.xml,要开启 AOP 空间 ```xml @@ -6961,9 +6956,9 @@ TransactionStatus 此接口定义了事务在执行过程中某个时间点上 ``` -* aop:advice与aop:advisor区别 - * aop:advice配置的通知类可以是普通java对象,不实现接口,也不使用继承关系 - * aop:advisor配置的通知类必须实现通知接口,底层invoke调用 +* aop:advice 与 aop:advisor 区别 + * aop:advice 配置的通知类可以是普通 java 对象,不实现接口,也不使用继承关系 + * aop:advisor 配置的通知类必须实现通知接口,底层 invoke 调用 - MethodBeforeAdvice @@ -6971,7 +6966,6 @@ TransactionStatus 此接口定义了事务在执行过程中某个时间点上 - ThrowsAdvice - - …… 方法调用:`AbstractAspectJAdvice#invokeAdviceMethod(org.aspectj.weaver.tools.JoinPointMatch, java.lang.Object, java.lang.Throwable)` @@ -6985,7 +6979,7 @@ TransactionStatus 此接口定义了事务在执行过程中某个时间点上 ###### advice -标签:tx:advice,beans的子标签 +标签:tx:advice,beans 的子标签 作用:专用于声明事务通知 @@ -7000,14 +6994,14 @@ TransactionStatus 此接口定义了事务在执行过程中某个时间点上 基本属性: -- id:用于配置aop时指定通知器的id -- transaction-manager:指定事务管理器bean +- id:用于配置 aop 时指定通知器的 id +- transaction-manager:指定事务管理器 bean ###### attributes -类型:tx:attributes,tx:advice的子标签 +类型:tx:attributes,tx:advice 的子标签 作用:定义通知属性 @@ -7024,7 +7018,7 @@ TransactionStatus 此接口定义了事务在执行过程中某个时间点上 ###### method -标签:tx:method,tx:attribute的子标签 +标签:tx:method,tx:attribute 的子标签 作用:设置具体的事务属性 @@ -7040,16 +7034,16 @@ TransactionStatus 此接口定义了事务在执行过程中某个时间点上 ``` -说明:通常事务属性会配置多个,包含1个读写的全事务属性,1个只读的查询类事务属性 +说明:通常事务属性会配置多个,包含 1 个读写的全事务属性,1 个只读的查询类事务属性 属性: -* name:待添加事务的方法名表达式(支持*通配符) -* read-only:设置事务的读写属性,true为只读,false为读写 -* timeout:设置事务的超时时长,单位秒,-1为无限长 -* isolation:设置事务的隔离界别,该隔离级设定是基于Spring的设定,非数据库端 -* no-rollback-for:设置事务中不回滚的异常,多个异常使用`,`分隔 -* rollback-for:设置事务中必回滚的异常,多个异常使用`,`分隔 +* name:待添加事务的方法名表达式(支持 * 通配符) +* read-only:设置事务的读写属性,true 为只读,false 为读写 +* timeout:设置事务的超时时长,单位秒,-1 为无限长 +* isolation:设置事务的隔离界别,该隔离级设定是基于 Spring 的设定,非数据库端 +* no-rollback-for:设置事务中不回滚的异常,多个异常使用 `,` 分隔 +* rollback-for:设置事务中必回滚的异常,多个异常使用 `,` 分隔 * propagation:设置事务的传播行为 @@ -7066,7 +7060,7 @@ TransactionStatus 此接口定义了事务在执行过程中某个时间点上 标签:tx:annotation-driven -归属:beans标签 +归属:beans 标签 作用:开启事务注解驱动,并指定对应的事务管理器 @@ -7082,9 +7076,9 @@ TransactionStatus 此接口定义了事务在执行过程中某个时间点上 名称:@EnableTransactionManagement -类型:类注解,Spring注解配置类上方 +类型:类注解,Spring 注解配置类上方 -作用:开启注解驱动,等同XML格式中的注解驱动 +作用:开启注解驱动,等同 XML 格式中的注解驱动 范例: @@ -7109,6 +7103,12 @@ public class TransactionManagerConfig { + + +*** + + + ##### 配置注解 名称:@Transactional @@ -7134,19 +7134,27 @@ public void addAccount{} 说明: * `@Transactional` 注解只有作用到 public 方法上事务才生效 -* 不推荐在接口上使用`@Transactional` 注解 - 原因:在接口上使用注解,只有**在使用基于接口的代理时才会生效**,因为注解是不能继承的,这就意味着如果正在使用基于类的代理时,那么事务的设置将不能被基于类的代理所识别 + +* 不推荐在接口上使用 `@Transactional` 注解 + + 原因:在接口上使用注解,只有**在使用基于接口的代理时才会生效**,因为**注解是不能继承的**,这就意味着如果正在使用基于类的代理时,那么事务的设置将不能被基于类的代理所识别 + * 正确的设置 `@Transactional` 的 rollbackFor 和 propagation 属性,否则事务可能会回滚失败 -面试题:**事务不生效的问题** +* 默认情况下,事务只有遇到运行期异常 和 Error 会导致事务回滚,但是在遇到检查型(Checked)异常时不会回滚 -* 情况1:确认创建的mysql数据库表引擎是InnoDB,MyISAM不支持事务 + * 继承自 RuntimeException 或 error 的是非检查型异常,比如空指针和索引越界,而继承自 Exception 的则是检查型异常,比如 IOException、ClassNotFoundException + * 非检查型类异常可以不用捕获,而检查型异常则必须用 try 语句块把异常交给上级方法,这样事务才能有效 -* 情况2:注解到protected,private 方法上事务不生效,但不会报错 - 原因:理论上而言,不用public修饰,也可以用aop实现transactional的功能,但是方法私有化让其他业务无法调用 +**事务不生效的问题** + +* 情况 1:确认创建的 mysql 数据库表引擎是 InnoDB,MyISAM 不支持事务 + +* 情况 2:注解到 protected,private 方法上事务不生效,但不会报错 + 原因:理论上而言,不用public修饰,也可以用 aop 实现 Transactional 的功能,但是方法私有化让其他业务无法调用 AopUtils.canApply:`methodMatcher.matches(method, targetClass) --true--> return true` - `TransactionAttributeSourcePointcut.matches()` ,AbstractFallbackTransactionAttributeSource 中 getTransactionAttribute 方法调用了其本身的computeTransactionAttribute 方法,当加了事务注解的方法不是public时,该方法直接返回null + `TransactionAttributeSourcePointcut.matches()` ,AbstractFallbackTransactionAttributeSource 中 getTransactionAttribute 方法调用了其本身的 computeTransactionAttribute 方法,当加了事务注解的方法不是 public 时,该方法直接返回 null,所以造成增强不匹配 ```java private TransactionAttribute computeTransactionAttribute(Method method, Class targetClass) { @@ -7157,19 +7165,19 @@ public void addAccount{} } ``` -* 情况3:注解所在的类没有被加载成Bean +* 情况 3:注解所在的类没有被加载成 Bean -* 情况4:在业务层捕捉异常后未向上抛出,事务不生效 +* 情况 4:在业务层捕捉异常后未向上抛出,事务不生效 - 原因:在业务层捕捉并处理了异常(try..catch)等于把异常处理掉了,Spring就不知道这里有错,也不会主动去回滚数据,推荐做法是在业务层统一抛出异常,然后在控制层统一处理 + 原因:在业务层捕捉并处理了异常(try..catch)等于把异常处理掉了,Spring 就不知道这里有错,也不会主动去回滚数据,推荐做法是在业务层统一抛出异常,然后在控制层统一处理 -* 情况5:遇到非检测异常时,事务不开启,也无法回滚 +* 情况 5:遇到检测异常时,事务不开启,也无法回滚 - 原因:Spring的默认的事务规则是遇到运行异常(RuntimeException)和程序错误(Error)才会回滚。想针对非检测异常进行事务回滚,可以在@Transactional 注解里使用rollbackFor 属性明确指定异常 + 原因:Spring 的默认的事务规则是遇到运行异常(RuntimeException)和程序错误(Error)才会回滚。想针对检测异常进行事务回滚,可以在 @Transactional 注解里使用 rollbackFor 属性明确指定异常 -* 情况6:Spring的事务传播策略在**内部方法**调用时将不起作用,在一个Service内部,事务方法之间的嵌套调用,普通方法和事务方法之间的嵌套调用,都不会开启新的事务。事务注解要加到调用方法上才生效 +* 情况 6:Spring 的事务传播策略在**内部方法**调用时将不起作用,在一个 Service 内部,事务方法之间的嵌套调用,普通方法和事务方法之间的嵌套调用,都不会开启新的事务,事务注解要加到调用方法上才生效 - 原因:Spring的事务都是使用AOP代理的模式,动态代理最终是要调用原始对象,而原始对象在去调用方法时是不会再触发代理,就是方法调用**本对象**的另一个方法,没有通过代理类直接调用,而且事务也就无法生效 + 原因:Spring 的事务都是使用 AOP 代理的模式,动态代理最终是要调用原始对象,而原始对象在去调用方法时是不会再触发代理,就是一个方法调用**本对象**的另一个方法,没有通过代理类直接调用,所以事务也就无法生效 ```java @Transactional @@ -7190,7 +7198,7 @@ public void addAccount{} ##### 使用注解 -* Dao层 +* Dao 层 ```java public interface AccountDao { @@ -7231,7 +7239,7 @@ public void addAccount{} } ``` -* 添加文件Spring.config、Mybatis.config、JDBCConfig (参考ioc_Mybatis)、TransactionManagerConfig +* 添加文件 Spring.config、Mybatis.config、JDBCConfig (参考ioc_Mybatis)、TransactionManagerConfig ```java @Configuration @@ -7253,8 +7261,8 @@ public void addAccount{} Spring 模板对象:TransactionTemplate、JdbcTemplate、RedisTemplate、RabbitTemplate、JmsTemplate、HibernateTemplate、RestTemplate -* JdbcTemplate:提供标准的sql语句操作API -* NamedParameterJdbcTemplate:提供标准的具名sql语句操作API +* JdbcTemplate:提供标准的 sql 语句操作API +* NamedParameterJdbcTemplate:提供标准的具名 sql 语句操作API * RedisTemplate: @@ -7272,30 +7280,6 @@ Spring 模板对象:TransactionTemplate、JdbcTemplate、RedisTemplate、Rabbi -*** - - - -### 底层原理 - -TransactionManagementConfigurationSelector 类: - -* 导入 AutoProxyRegistrar 组件和 ProxyTransactionManagementConfiguration 组件 - -* AutoProxyRegistrar:利用后置处理器机制在对象创建以后包装对象,返回一个代理对象(增强器),代理对象执行方法利用拦截器链进行调用,通过 @Transactional 作为方法拦截的标记,把有事务管理的类作为目标类,生成代理对象,然后增强 @Transactional 标记的方法,在使用目标方法的时候,从 IOC 容器中获取的其实是被增强的代理类,且事务方法会被代理,跟 AOP 原理一样 - -* ProxyTransactionManagementConfiguration:向 容器中导入事务增强器 BeanFactoryTransactionAttributeSourceAdvisor,事务注解 @Transactional 的解析器 AnnotationTransactionAttributeSource 和事务方法拦截器 TransactionInterceptor - -通过 AOP 动态织入,进行事务开启和提交 - -事务底层原理解析:策略模式 - -策略模式(Strategy Pattern)**使用不同策略的对象实现不同的行为方式**,策略对象的变化导致行为的变化,每个事务对应一个新的 connection 对象 - -![](https://gitee.com/seazean/images/raw/master/Frame/Spring-事务底层原理策略模式.png) - - - *** @@ -8349,9 +8333,26 @@ AspectJAutoProxyRegistrar 在用来向容器中注册 **AnnotationAwareAspectJAu #### 动态代理 -##### 获取通知 +##### 后置处理 -创建动态代理:AbstractAutoProxyCreator.wrapIfNecessary() +Bean 初始化完成的执行后置处理器的方法: + +```java +public Object postProcessAfterInitialization(@Nullable Object bean,String bN){ + if (bean != null) { + // cacheKey 是 beanName 或者加上 & + Object cacheKey = getCacheKey(bean.getClass(), beanName); + if (this.earlyProxyReferences.remove(cacheKey) != bean) { + //去提前代理引用池中寻找该key,不存在则创建代理 + //如果存在则证明被代理过,则判断是否是当前的 bean,不是则创建代理 + return wrapIfNecessary(bean, bN, cacheKey); + } + } + return bean; +} +``` + +AbstractAutoProxyCreator.wrapIfNecessary():根据通知创建动态代理,没有通知直接返回原实例 ```java protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { @@ -8371,7 +8372,7 @@ protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) return bean; } - // 查找适合当前 bean 实例 Class 的通知(本节详解) + // 查找适合当前 bean 实例 Class 的通知(下一节详解) Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null); //条件成立说明上面方法查询到适合当前class的通知 if (specificInterceptors != DO_NOT_PROXY) { @@ -8394,7 +8395,15 @@ protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) } ``` -AbstractAdvisorAutoProxyCreator.getAdvicesAndAdvisorsForBean() + + +*** + + + +##### 获取通知 + +AbstractAdvisorAutoProxyCreator.getAdvicesAndAdvisorsForBean():查找适合当前实例的增强 ```java protected Object[] getAdvicesAndAdvisorsForBean(Class beanClass, String beanName, @Nullable TargetSource targetSource) { @@ -8420,7 +8429,7 @@ AbstractAdvisorAutoProxyCreator.findEligibleAdvisors(): * `advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors())`:获取添加 @Aspect 注解类中的 Advisor - `buildAspectJAdvisors()`:构建的方法,**把 Advice 封装成 Advisor**(非常复杂,不建议深究) + `buildAspectJAdvisors()`:构建的方法,**把 Advice 封装成 Advisor**(逻辑很绕,不建议深究) * ` beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, Object.class, true, false)`:获取出容器内 Object 所有的 beanName,就是全部的 @@ -8456,7 +8465,7 @@ AbstractAdvisorAutoProxyCreator.findEligibleAdvisors(): * `List eligibleAdvisors = new ArrayList<>()`:匹配当前 clazz 的 Advisors 信息 - * `for (Advisor candidate : candidateAdvisors)`:遍历所有的 Advisor + * `for (Advisor candidate : candidateAdvisors)`:遍历所有的 AdvisorIntroduction ` if (canApply(candidate, clazz, hasIntroductions))`:判断遍历的 advisor 是否匹配当前的 class,匹配就加入集合 @@ -8467,14 +8476,17 @@ AbstractAdvisorAutoProxyCreator.findEligibleAdvisors(): `return canApply(pca.getPointcut(), targetClass, hasIntroductions)`:重载该方法 * `if (!pc.getClassFilter().matches(targetClass))`:条件成立说明不满足切点定义,直接返回 false - * `methodMatcher = pc.getMethodMatcher()`:获取方法匹配器 + * `methodMatcher = pc.getMethodMatcher()`:**获取方法匹配器** * `Set> classes`:保存目标对象 class 和目标对象父类超类的接口和自身实现的接口 * `if (!Proxy.isProxyClass(targetClass))`:判断当前实例是不是代理类,确保 class 内存储的数据包括目标对象的class 而不是代理类的 class * `for (Class clazz : classes)`:检查目标 class 和上级接口的所有方法,查看是否会被方法匹配器匹配,如果有一个方法匹配成功,就说明目标对象 AOP 代理需要增强 + * `specificMethod = AopUtils.getMostSpecificMethod(method, targetClass)`:方法可能是接口的,判断当前类有没有该方法 + * `return (specificMethod != method && matchesMethod(specificMethod))`:类和方法的匹配,不包括参数(静态匹配) -* `extendAdvisors(eligibleAdvisors)`:在 eligibleAdvisors 列表的 索引 0 的位置添加 DefaultPointcutAdvisor,封装了 ExposeInvocationInterceptor 拦截器 +* `extendAdvisors(eligibleAdvisors)`:在 eligibleAdvisors 列表的索引 0 的位置添加 DefaultPointcutAdvisor,**封装了 ExposeInvocationInterceptor 拦截器** -* ` eligibleAdvisors = sortAdvisors(eligibleAdvisors)`:对拦截器链进行排序,数值越小优先级越高,高的排在前面 +* ` eligibleAdvisors = sortAdvisors(eligibleAdvisors)`:**对拦截器链进行排序**,数值越小优先级越高,高的排在前面 + * 实现 Ordered 或 PriorityOrdered 接口,PriorityOrdered 的级别要优先于 Ordered,使用 OrderComparator 比较器 * 使用 @Order(Spring 规范)或 @Priority(JDK 规范)注解,使用 AnnotationAwareOrderComparator 比较器 * ExposeInvocationInterceptor 实现了 PriorityOrdered ,所以总是排在第一位,MethodBeforeAdviceInterceptor 没实现任何接口,所以优先级最低,排在最后 @@ -8490,7 +8502,7 @@ AbstractAdvisorAutoProxyCreator.findEligibleAdvisors(): AbstractAutoProxyCreator.createProxy():根据增强方法创建代理对象 -* `ProxyFactory proxyFactory = new ProxyFactory()`:此处是无参构造,讲解一下两种有参构造方法: +* `ProxyFactory proxyFactory = new ProxyFactory()`:**无参构造 ProxyFactory**,讲解一下两种有参构造方法: * public ProxyFactory(Object target): @@ -8519,19 +8531,18 @@ AbstractAutoProxyCreator.createProxy():根据增强方法创建代理对象 addInterface(proxyInterface); // 添加通知,底层调用 addAdvisor addAdvice(interceptor); - - // addAdvisor(pos, new DefaultPointcutAdvisor(advice)); - // Spring 中 Advice 对应的接口就是 Advisor,Spring 使用 Advisor 包装 Advice 实例 } ``` + + * `addAdvisor(pos, new DefaultPointcutAdvisor(advice))`:Spring 中 Advice 对应的接口就是 Advisor,Spring 使用 Advisor 包装 Advice 实例 * `proxyFactory.copyFrom(this)`:填充一些信息到 proxyFactory * `if (!proxyFactory.isProxyTargetClass())`:条件成立说明没有配置修改过 proxyTargetClass 为 true - `if (shouldProxyTargetClass(beanClass, beanName))`:如果 bd 内有 preserveTargetClass = true ,那么这个 bd 对应的 class 创建代理时必须使用 CGLIB,条件成立设置 proxyTargetClass 为 true + `if (shouldProxyTargetClass(beanClass, beanName))`:如果 **bd 内有 preserveTargetClass = true** ,那么这个 bd 对应的 class 创建代理时必须使用 CGLIB,条件成立设置 proxyTargetClass 为 true - `evaluateProxyInterfaces(beanClass, proxyFactory)`:根据目标类判定是否可以使用 JDK 动态代理 + `evaluateProxyInterfaces(beanClass, proxyFactory)`:**根据目标类判定是否可以使用 JDK 动态代理** * `targetInterfaces = ClassUtils.getAllInterfacesForClass()`:获取当前目标对象 class 和父类的全部实现接口 * `boolean hasReasonableProxyInterface = false`:实现的接口中是否有一个合理的接口 @@ -8539,8 +8550,8 @@ AbstractAutoProxyCreator.createProxy():根据增强方法创建代理对象 * 条件一:判断当前接口是否是 Spring 生命周期内会回调的接口 * 条件二:接口不能是 GroovyObject、Factory、MockAccess 类型的 * 条件三:找到一个可以使用的被代理的接口 - * `if (hasReasonableProxyInterface)`:有合理的接口,将这些接口设置到 proxyFactory 内 - * `proxyFactory.setProxyTargetClass(true)`:没有合理的代理接口,强制使用 CGLIB 创建对象 + * `if (hasReasonableProxyInterface)`:**有合理的接口,将这些接口设置到 proxyFactory 内** + * `proxyFactory.setProxyTargetClass(true)`:**没有合理的代理接口,强制使用 CGLIB 创建对象** * `advisors = buildAdvisors(beanName, specificInterceptors)`:匹配目标对象 clazz 的 Advisors,填充至 ProxyFactory @@ -8554,78 +8565,76 @@ AbstractAutoProxyCreator.createProxy():根据增强方法创建代理对象 } ``` - * DefaultAopProxyFactory.createAopProxy(AdvisedSupport config):参数是一个配置对象,保存着创建代理需要的生产资料 - - ```java - public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { - //条件一:积极的优化 - //条件二:为 true 代表强制使用 CGLIB 动态代理, - // - // @EnableAspectJAutoProxy(proxyTargetClass = true) - if (config.isOptimize() || config.isProxyTargetClass() || - //条件三:被代理对象没有实现任何接口或者只实现了 SpringProxy 接口,只能使用 CGLIB 动态代理 - hasNoUserSuppliedProxyInterfaces(config)) { - Class targetClass = config.getTargetClass(); - if (targetClass == null) { - throw new AopConfigException(""); - } - // 条件成立说明 target 是接口或者是已经被代理过的类型,只能使用 JDK 动态代理 - if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { - return new JdkDynamicAopProxy(config); // 使用 JDK 动态代理 - } - return new ObjenesisCglibAopProxy(config); // 使用 CGLIB 动态代理 - } - else { - return new JdkDynamicAopProxy(config); // 有接口的情况下只能使用 JDK 动态代理 - } - } - ``` - - * JdkDynamicAopProxy.getProxy(java.lang.ClassLoader):获取 JDK 的代理对象 - - ```java - public JdkDynamicAopProxy(AdvisedSupport config) throws AopConfigException { - // 配置类封装到 JdkDynamicAopProxy 属性中 - this.advised = config; - } - public Object getProxy(@Nullable ClassLoader classLoader) { - // 获取需要代理的接口数组 - Class[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true); - // 查找当前所有的需要代理的接口,看是否有 equals 方法和 hashcode 方法,如果有就做一个标记 - findDefinedEqualsAndHashCodeMethods(proxiedInterfaces); - // classLoader:类加载器 proxiedInterfaces:生成的代理类,需要实现的接口集合 - // this JdkDynamicAopProxy 实现了 InvocationHandler - // 该方法最终返回一个代理类对象 - return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this); - } - ``` + DefaultAopProxyFactory.createAopProxy(AdvisedSupport config):参数是一个配置对象,保存着创建代理需要的生产资料 - AopProxyUtils.completeProxiedInterfaces(this.advised, true):获取代理的接口数组 + ```java + public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { + //条件一:积极的优化 + //条件二:为 true 代表强制使用 CGLIB 动态代理, + //两种配置方法: + // + // @EnableAspectJAutoProxy(proxyTargetClass = true) + if (config.isOptimize() || config.isProxyTargetClass() || + //条件三:被代理对象没有实现任何接口或者只实现了 SpringProxy 接口,只能使用 CGLIB 动态代理 + hasNoUserSuppliedProxyInterfaces(config)) { + Class targetClass = config.getTargetClass(); + if (targetClass == null) { + throw new AopConfigException(""); + } + // 条件成立说明 target 是接口或者是已经被代理过的类型,只能使用 JDK 动态代理 + if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { + return new JdkDynamicAopProxy(config); // 使用 JDK 动态代理 + } + return new ObjenesisCglibAopProxy(config); // 使用 CGLIB 动态代理 + } + else { + return new JdkDynamicAopProxy(config); // 有接口的情况下只能使用 JDK 动态代理 + } + } + ``` - * `specifiedInterfaces = advised.getProxiedInterfaces()`:从 ProxyFactory 中拿到所有的 target 提取出来的接口 - * `if (specifiedInterfaces.length == 0)`:如果没有实现接口,检查当前 target 是不是接口或者已经是代理类,封装到 ProxyFactory 的 interfaces 集合中 + JdkDynamicAopProxy.getProxy(java.lang.ClassLoader):获取 JDK 的代理对象 - * ` addSpringProxy = !advised.isInterfaceProxied(SpringProxy.class)`:判断目标对象所有接口中是否有 **SpringProxy** 接口,没有的话需要添加,这个接口**标识这个代理类型是 Spring 管理的** - * `addAdvised = !advised.isOpaque() && !advised.isInterfaceProxied(Advised.class)`:判断目标对象的所有接口,是否已经有 Advised 接口 - * ` addDecoratingProxy = (decoratingProxy && !advised.isInterfaceProxied(DecoratingProxy.class))`:判断目标对象的所有接口,是否已经有 DecoratingProxy 接口 - * `int nonUserIfcCount = 0`:非用户自己定义的接口数量,接下来要添加上面的三个接口了 - * `proxiedInterfaces = new Class[specifiedInterfaces.length + nonUserIfcCount]`:创建一个新的 class 数组,长度是原目标对象提取出来的接口数量和 Spring 追加的数量,然后进行 **System.arraycopy 拷贝到新数组中** - * `int index = specifiedInterfaces.length`:获取原目标对象提取出来的接口数量,当作 index - * `if(addSpringProxy)`:根据上面三个布尔值把接口添加到新数组中 - * `return proxiedInterfaces`:返回追加后的接口集合 + ```java + public JdkDynamicAopProxy(AdvisedSupport config) throws AopConfigException { + // 配置类封装到 JdkDynamicAopProxy 属性中 + this.advised = config; + } + public Object getProxy(@Nullable ClassLoader classLoader) { + // 获取需要代理的接口数组 + Class[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true); + // 查找当前所有的需要代理的接口,看是否有 equals 方法和 hashcode 方法,如果有就做一个标记 + findDefinedEqualsAndHashCodeMethods(proxiedInterfaces); + // classLoader:类加载器 proxiedInterfaces:生成的代理类,需要实现的接口集合 + // this JdkDynamicAopProxy 实现了 InvocationHandler + // 该方法最终返回一个代理类对象 + return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this); + } + ``` - JdkDynamicAopProxy.findDefinedEqualsAndHashCodeMethods(): + AopProxyUtils.completeProxiedInterfaces(this.advised, true):获取代理的接口数组 - * `for (Class proxiedInterface : proxiedInterfaces)`:遍历所有的接口 + * `specifiedInterfaces = advised.getProxiedInterfaces()`:从 ProxyFactory 中拿到所有的 target 提取出来的接口 + * `if (specifiedInterfaces.length == 0)`:如果没有实现接口,检查当前 target 是不是接口或者已经是代理类,封装到 ProxyFactory 的 interfaces 集合中 - ` Method[] methods = proxiedInterface.getDeclaredMethods()`:获取接口中的所有方法 + * ` addSpringProxy = !advised.isInterfaceProxied(SpringProxy.class)`:判断目标对象所有接口中是否有 **SpringProxy** 接口,没有的话需要添加,这个接口**标识这个代理类型是 Spring 管理的** + * `addAdvised = !advised.isOpaque() && !advised.isInterfaceProxied(Advised.class)`:判断目标对象的所有接口,是否已经有 Advised 接口 + * ` addDecoratingProxy = (decoratingProxy && !advised.isInterfaceProxied(DecoratingProxy.class))`:判断目标对象的所有接口,是否已经有 DecoratingProxy 接口 + * `int nonUserIfcCount = 0`:非用户自己定义的接口数量,接下来要添加上面的三个接口了 + * `proxiedInterfaces = new Class[specifiedInterfaces.length + nonUserIfcCount]`:创建一个新的 class 数组,长度是原目标对象提取出来的接口数量和 Spring 追加的数量,然后进行 **System.arraycopy 拷贝到新数组中** + * `int index = specifiedInterfaces.length`:获取原目标对象提取出来的接口数量,当作 index + * `if(addSpringProxy)`:根据上面三个布尔值把接口添加到新数组中 + * `return proxiedInterfaces`:返回追加后的接口集合 - `for (Method method : methods)`:遍历所有的方法 + JdkDynamicAopProxy.findDefinedEqualsAndHashCodeMethods():查找在任何定义在接口中的 equals 和 hashCode 方法 - * `if (AopUtils.isEqualsMethod(method))`:当前方法是 equals 方法,把 equalsDefined 置为 true - * `if (AopUtils.isHashCodeMethod(method))`:当前方法是 hashCode 方法,把 hashCodeDefined 置为 true + * `for (Class proxiedInterface : proxiedInterfaces)`:遍历所有的接口 + * ` Method[] methods = proxiedInterface.getDeclaredMethods()`:获取接口中的所有方法 + * `for (Method method : methods)`:遍历所有的方法 + * `if (AopUtils.isEqualsMethod(method))`:当前方法是 equals 方法,把 equalsDefined 置为 true + * `if (AopUtils.isHashCodeMethod(method))`:当前方法是 hashCode 方法,把 hashCodeDefined 置为 true - * `if (this.equalsDefined && this.hashCodeDefined)`:如果有一个接口中有这两种方法,直接返回 + * `if (this.equalsDefined && this.hashCodeDefined)`:如果有一个接口中有这两种方法,直接返回 @@ -8660,11 +8669,11 @@ public Object invoke(Object proxy, Method method, Object[] args) * `AdvisorAdapterRegistry registry = GlobalAdvisorAdapterRegistry.getInstance()`:向容器注册适配器,**可以将非 Advisor 类型的增强,包装成为 Advisor,将 Advisor 类型的增强提取出来对应的 MethodInterceptor** - * `instance = new DefaultAdvisorAdapterRegistry()`:该对象向容器中注册了 MethodBeforeAdviceAdapter、AfterReturningAdviceAdapter、ThrowsAdviceAdapter **三个适配器** + * `instance = new DefaultAdvisorAdapterRegistry()`:**该对象向容器中注册了** MethodBeforeAdviceAdapter、AfterReturningAdviceAdapter、ThrowsAdviceAdapter **三个适配器** * `advisors = config.getAdvisors()`:获取 ProxyFactory 内部持有的增强信息 - * `interceptorList = new ArrayList<>(advisors.length)`:拦截器列表 + * `interceptorList = new ArrayList<>(advisors.length)`:拦截器列表有 5 个,一个 ExposeInvocationInterceptor 和 4 个增强器 * `actualClass = (targetClass != null ? targetClass : method.getDeclaringClass())`:真实的目标对象类型 @@ -8672,30 +8681,42 @@ public Object invoke(Object proxy, Method method, Object[] args) * `for (Advisor advisor : advisors)`:**遍历所有的增强** - * `if (advisor instanceof PointcutAdvisor)`:条件成立说明当前 Advisor 是包含切点信息的,做匹配逻辑 + * `if (advisor instanceof PointcutAdvisor)`:条件成立说明当前 Advisor 是包含切点信息的,进入匹配逻辑 `pointcutAdvisor = (PointcutAdvisor) advisor`:转成可以获取到切点信息的接口 - `if()`:当前代理被预处理,或者当前被代理的 class 对象匹配当前 Advisor 成功,只是 class 匹配成功 + `if(config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass))`:当前代理被预处理,或者当前被代理的 class 对象匹配当前 Advisor 成功,只是 **class 匹配成功** - * `mm = pointcutAdvisor.getPointcut().getMethodMatcher()`:获取切点的方法匹配器 + * `mm = pointcutAdvisor.getPointcut().getMethodMatcher()`:获取切点的方法匹配器,不考虑引介增强 * `match = mm.matches(method, actualClass)`:**静态匹配成功返回 true,只关注于处理类及其方法,不考虑参数** - `if (match)`:如果静态切点检查是匹配的,在运行的时候才进行**动态切点检查,会考虑参数匹配**(代表传入了参数)。如果静态匹配失败,直接不需要进行参数匹配,提高了工作效率 + * `if (match)`:如果静态切点检查是匹配的,在运行的时候才进行**动态切点检查,会考虑参数匹配**(代表传入了参数)。如果静态匹配失败,直接不需要进行参数匹配,提高了工作效率 + + `interceptors = registry.getInterceptors(advisor)`:提取出 advisor 内持有的拦截器信息 + + * `Advice advice = advisor.getAdvice()`:获取增强方法 + + * `if (advice instanceof MethodInterceptor)`:当前 advice 是 MethodInterceptor 直接加入集合 + + * `for (AdvisorAdapter adapter : this.adapters)`:**遍历三个适配器进行匹配**(初始化时创建的),以 MethodBeforeAdviceAdapter 为例 - * `interceptors = registry.getInterceptors(advisor)`:提取出 advisor 内持有的拦截器信息 - * 遍历三个适配器,获取拦截器,比如 MethodBeforeAdviceAdapter: - * `MethodBeforeAdvice advice = (MethodBeforeAdvice) advisor.getAdvice()`:获取增强方法 - * `return new MethodBeforeAdviceInterceptor(advice)`:封装成适配器对象返回 - * `interceptorList.add(new InterceptorAndDynamicMethodMatcher(interceptor, mm))`:向拦截器链添加动态匹配器 - * `interceptorList.addAll(Arrays.asList(interceptors))`:将当前 advisor 内部的方法拦截器追加到 interceptorList + `if (adapter.supportsAdvice(advice))`:判断当前 advice 是否是对应的 MethodBeforeAdvice + + `interceptors.add(adapter.getInterceptor(advisor))`:是就往拦截器链中添加 advisor + + * `advice = (MethodBeforeAdvice) advisor.getAdvice()`:**获取增强方法** + * `return new MethodBeforeAdviceInterceptor(advice)`:**封装成 MethodInterceptor 方法拦截器返回** + + `interceptorList.add(new InterceptorAndDynamicMethodMatcher(interceptor, mm))`:向拦截器链添加动态匹配器 + + `interceptorList.addAll(Arrays.asList(interceptors))`:将当前 advisor 内部的方法拦截器追加到 interceptorList * `interceptors = registry.getInterceptors(advisor)`:进入 else 的逻辑,说明当前 Advisor 匹配全部 class 的全部 method,全部加入到 interceptorList * `return interceptorList`:返回 method 方法的拦截器链 -* `if (chain.isEmpty())`:查询出来匹配当前方法的拦截器,数量是 0 说明当前 method 不需要被增强,直接调用目标方法 +* `if (chain.isEmpty())`:查询出来匹配当前方法的拦截器,**数量是 0 说明当前 method 不需要被增强**,直接调用目标方法 `retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse)`:调用目标对象的目标方法 @@ -8703,34 +8724,63 @@ public Object invoke(Object proxy, Method method, Object[] args) `retVal = invocation.proceed()`:**核心拦截器链驱动方法** - * `if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1)`:条件成立说明方法拦截器全部都已经调用过了,接下来需要执行目标对象的目标方法 + * `if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1)`:条件成立说明方法拦截器全部都已经调用过了(0 - 1 = -1),接下来需要执行目标对象的目标方法 `return invokeJoinpoint()`:调用连接点 - * `this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex)`:获取下一个方法拦截器 + * `this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex)`:**获取下一个方法拦截器** - * `if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher)`:需要运行时匹配 + * `if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher)`:**需要运行时匹配** `if (dm.methodMatcher.matches(this.method, targetClass, this.arguments))`:判断是否匹配成功 * `return dm.interceptor.invoke(this)`:匹配成功,执行方法 * `return proceed()`:匹配失败跳过当前拦截器 - * `return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this)`:让当前方法拦截器执行 + * `return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this)`:**所有的方法拦截器都会执行到该方法,然后方法内继续执行 proceed() 完成责任链的构建,直到 MethodBeforeAdviceInterceptor 调用前置通知,然后调用 mi.proceed(),发现是最后一个拦截器了,这里就直接执行目标方法,return 到上一个拦截器的 mi.proceed() 处,依次返回到责任链的上一个拦截器执行通知方法** * `retVal = proxy`:如果目标方法返回目标对象,这里做个普通替换返回代理对象 * `if (setProxyContext)`:如果允许了提前暴露,这里需要设置为初始状态 `AopContext.setCurrentProxy(oldProxy)`:当前代理对象已经完成工作,把原始对象设置回上下文 + +* `return retVal`:返回执行的结果 proceed() 链式获取每一个拦截器,拦截器执行 invoke方法,每一个拦截器等待下一个拦截器执行完成返回以后再来执行;拦截器链的机制,保证通知方法与目标方法的执行顺序 图示先从上往下建立链,然后从下往上依次执行,责任链模式 * 正常执行:(环绕通知)→ 前置通知 → 目标方法 → 后置通知 → 返回通知 + * 出现异常:(环绕通知)→ 前置通知 → 目标方法 → 后置通知 → 异常通知 + * AfterReturningAdviceInterceptor 源码:没有任何异常处理机制,直接抛给上层 + + ```java + public Object invoke(MethodInvocation mi) throws Throwable { + Object retVal = mi.proceed(); + this.advice.afterReturning(retVal, mi.getMethod(), mi.getArguments(), mi.getThis()); + return retVal; + } + ``` + + AspectJAfterThrowingAdvice 执行异常处理: + + ```java + public Object invoke(MethodInvocation mi) throws Throwable { + try { + return mi.proceed(); + } + catch (Throwable ex) { + if (shouldInvokeOnThrowing(ex)) { + invokeAdviceMethod(getJoinPointMatch(), null, ex); + } + throw ex; + } + } + ``` + ![](https://gitee.com/seazean/images/raw/master/Frame/Spring-AOP动态代理执行方法.png) @@ -8862,15 +8912,15 @@ proceed() 链式获取每一个拦截器,拦截器执行 invoke方法,每一 } ``` - metaAnnotationMap怎么赋值的? + metaAnnotationMap 怎么赋值的? - metaAnnotationMap赋值方法在`SimpleMetadataReader.SimpleMetadataReader`中: + metaAnnotationMap 赋值方法在`SimpleMetadataReader.SimpleMetadataReader`中: ```java classReader.accept(visitor, ClassReader.SKIP_DEBUG); ``` - 然后通过readElementValues方法中: + 然后通过 readElementValues 方法中: ```java annotationVisitor.visitEnd(); @@ -8878,7 +8928,7 @@ proceed() 链式获取每一个拦截器,拦截器执行 invoke方法,每一 追踪方法:`AnnotationAttributesReadingVisitor.visitEnd()` - 递归读取:内部方法`recursivelyCollectMetaAnnotations()`递归的读取注解,与注解的元注解(读@Service,再读元注解@Component), + 递归读取:内部方法`recursivelyCollectMetaAnnotations()`递归的读取注解,与注解的元注解(读@Service,再读元注解@Component) 添加数据:`this.metaAnnotationMap.put(annotationClass.getName(), metaAnnotationTypeNames);` @@ -8907,11 +8957,45 @@ AutowiredAnnotationBeanPostProcessor 间接实现 InstantiationAwareBeanPostProc -#### Transactional +#### Transaction + +@EnableTransactionManagement 导入 TransactionManagementConfigurationSelector,该类给 Spring 容器中两个组件: + +* AdviceMode 为 PROXY:导入 AutoProxyRegistrar 和 ProxyTransactionManagementConfiguration(默认) +* AdviceMode 为 ASPECTJ:导入 AspectJTransactionManagementConfiguration(与声明式事务无关) + +AutoProxyRegistrar:给容器中注册 InfrastructureAdvisorAutoProxyCreator,该类实现了 InstantiationAwareBeanPostProcessor 接口,可以拦截 Spring 的 bean 初始化和实例化前后。利用后置处理器机制拦截 bean 以后包装该 bean 并返回一个代理对象,代理对象中保存所有的拦截器,代理对象执行目标方法,利用拦截器的链式机制依次进入每一个拦截器中进行执行(AOP 原理) + +ProxyTransactionManagementConfiguration:是一个 Spring 的配置类,注册 BeanFactoryTransactionAttributeSourceAdvisor 事务增强器,利用注解 @Bean 把该类注入到容器中,该增强器有两个字段: + +* TransactionAttributeSource:用于解析事务注解的相关信息,比如 @Transactional 注解,该类的真实类型是 AnnotationTransactionAttributeSource,初始化方法种注册了三个解析器,用来解析三种类型的事务注解 Spring、JTA、Ejb3 + +* TransactionInterceptor:**事务拦截器**,代理对象执行拦截器方法时,会调用 TransactionInterceptor 的 invoke 方法,底层调用TransactionAspectSupport.invokeWithinTransaction(),通过 PlatformTransactionManager **控制着事务的提交和回滚**,所以事务的底层原理就是通过 AOP 动态织入,进行事务开启和提交 + + ```java + // 创建平台事务管理器对象 + final PlatformTransactionManager tm = determineTransactionManager(txAttr); + // 开启事务 + TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification); + // 执行目标方法 + retVal = invocation.proceedWithInvocation(); + // 调用 java.sql.Connection 提交或者回滚事务 + commitTransactionAfterReturning(txInfo); + ``` + + `createTransactionIfNecessary(tm, txAttr, joinpointIdentification)`: + + * `status = tm.getTransaction(txAttr)`:获取事务状态,方法内通过 doBegin **调用 Connection 的 setAutoCommit 开启事务**,就是 JDBC 原生的方式 + + * `prepareTransactionInfo(tm, txAttr, joinpointIdentification, status)`:方法内调用 bindToThread() 方法,利用 ThreadLocal 把当前事务绑定到当前线程 + + 补充策略模式(Strategy Pattern):**使用不同策略的对象实现不同的行为方式**,策略对象的变化导致行为的变化,事务也是这种模式,每个事务对应一个新的 connection 对象 + +![](https://gitee.com/seazean/images/raw/master/Frame/Spring-图解事务执行流程.jpg) + -如果一个类或者一个类中的 public 方法上被标注 @Transactional 注解的话,Spring 容器就会在启动的时候为其创建一个代理类,在调用被@Transactional注解的 public 方法的时候,实际调用的是TransactionInterceptor类中的 invoke()方法。这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务 -`TransactionInterceptor` 类中的 `invoke()`方法内部实际调用的是 `TransactionAspectSupport` 类的 `invokeWithinTransaction()`方法 +图片来源:https://blog.csdn.net/weixin_45596022/article/details/113749478 From 620eb2d4f86eb321ed712eef6f8e16b80bb4502b Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 29 Jul 2021 22:08:17 +0800 Subject: [PATCH 005/168] Update Java Notes --- Java.md | 2 +- SSM.md | 1450 ++----------------------------------------------------- 2 files changed, 33 insertions(+), 1419 deletions(-) diff --git a/Java.md b/Java.md index 18e3cab..f8e6fa1 100644 --- a/Java.md +++ b/Java.md @@ -18776,7 +18776,7 @@ CGLIB 的优缺点 * 优点: * 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用 - * 代理对象可以扩展目标对象的功能 + * **代理对象可以增强目标对象的功能,内部持有原始的目标对象** * 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度 * 缺点:增加了系统的复杂度 diff --git a/SSM.md b/SSM.md index c743f99..5fee9db 100644 --- a/SSM.md +++ b/SSM.md @@ -7768,6 +7768,8 @@ AbstractBeanFactory.doGetBean():获取 Bean,context.getBean() 追踪到此 * `convertIfNecessary()`:**依赖检查**,检查所需的类型是否与实际 bean 实例的类型匹配 +* `return (T) bean`:返回创建完成的 bean + @@ -7806,17 +7808,17 @@ AbstractAutowireCapableBeanFactory.**doCreateBean**(beanName, RootBeanDefinition `applyMergedBeanDefinitionPostProcessors()`:后置处理器,合并 bd 信息,接下来要属性填充了 - `AutowiredAnnotationBeanPostProcessor.postProcessMergedBeanDefinition()`:进入后置处理逻辑 + `AutowiredAnnotationBeanPostProcessor.postProcessMergedBeanDefinition()`:后置处理逻辑**(@Autowired)** * `metadata = findAutowiringMetadata(beanName, beanType, null)`:提取出当前 beanType 类型整个继承体系内的 **@Autowired、@Value、@Inject** 信息,存入一个 InjectionMetadata 对象的 injectedElements 中并放入缓存 * `metadata = buildAutowiringMetadata(clazz)`:查询当前 clazz 感兴趣的注解信息 - * `ReflectionUtils.doWithLocalFields()`:提取字段的注解信息 + * `ReflectionUtils.doWithLocalFields()`:提取**字段**的注解的属性信息 - `findAutowiredAnnotation(field)`:代表感兴趣的注解就是那三种 + `findAutowiredAnnotation(field)`:代表感兴趣的注解就是那三种注解 - * `ReflectionUtils.doWithLocalMethods()`:提取方法的注解信息 + * `ReflectionUtils.doWithLocalMethods()`:提取**方法**的注解的属性信息 * `do{} while (targetClass != null && targetClass != Object.class)`:循环从父类中解析,直到 Object 类 @@ -7836,7 +7838,7 @@ AbstractAutowireCapableBeanFactory.**doCreateBean**(beanName, RootBeanDefinition * ` populateBean(beanName, mbd, instanceWrapper)`:**属性填充,依赖注入,整体逻辑是先处理标签再处理注解,填充至 pvs 中,最后通过 apply 方法最后完成属性依赖注入到 BeanWrapper ** - * `if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName))`:实例化后的后置处理器,默认返回 true,继承 InstantiationAwareBeanPostProcessor 修改返回值为 false,会造成 continueWithPropertyPopulation 为 false + * `if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName))`:实例化后的后置处理器,默认返回 true,自定义继承 InstantiationAwareBeanPostProcessor 修改返回值为 false,使 continueWithPropertyPopulation 为 false * `if (!continueWithPropertyPopulation)`:自定义方法返回值会造成该条件成立,逻辑为直接返回,不能进行依赖注入 @@ -7877,7 +7879,7 @@ AbstractAutowireCapableBeanFactory.**doCreateBean**(beanName, RootBeanDefinition * `pvsToUse = ibp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName)`:**@Autowired 注解的注入** - * `findAutowiringMetadata()`:包装着当前 bd 需要注入的注解信息集合,**三种注解的元数据** + * `findAutowiringMetadata()`:包装着当前 bd 需要注入的注解信息集合,**三种注解的元数据**,直接缓存获取 * `InjectionMetadata.InjectedElement.inject()`:将注解信息解析后注入到 pvs,方法和字段的注入的实现不同 * `ReflectionUtils.makeAccessible()`:修改访问权限,true 代表暴力破解 * `method.invoke()`:利用反射为此对象赋值 @@ -7941,28 +7943,26 @@ AbstractAutowireCapableBeanFactory.**doCreateBean**(beanName, RootBeanDefinition * `if (earlySingletonExposure)`:是否循序提前引用 - `earlySingletonReference = getSingleton(beanName, false)`:从二级缓存获取实例 + `earlySingletonReference = getSingleton(beanName, false)`:**从二级缓存获取实例**,放入一级缓存是在 doGetBean 中的sharedInstance = getSingleton() 方法中,此时在 createBean 的逻辑还没有返回。 `if (earlySingletonReference != null)`:当前 bean 实例从二级缓存中获取到了,说明产生了循环依赖,在属性填充阶段会提前调用三级缓存中的工厂生成 Bean 对象的动态代理,放入二级缓存中,然后使用原始 bean 继续执行初始化 - * ` if (exposedObject == bean)`:初始化后的 bean == 创建的原始实例,条件成立的两种情况:当前的真实实例不需要被代理、当前实例已经被代理过了,后处理器直接返回 bean 原实例 - - `exposedObject = earlySingletonReference`:把代理后的 Bean 传给 exposedObject 用来 return + * ` if (exposedObject == bean)`:初始化后的 bean == 创建的原始实例,条件成立的两种情况:当前的真实实例不需要被代理;当前实例已经被代理过了,初始化时的后置处理器直接返回 bean 原实例 - * **下面逻辑是动态代理提前创建,导致当前 bean 无法增强的情况** + `exposedObject = earlySingletonReference`:**把代理后的 Bean 传给 exposedObject 用来返回,因为只有代理对象才封装了增强的拦截器链,main 方法中用代理对象调用方法时,会进行增强,代理是对原始对象的包装,所以这里返回的代理对象中含有完整的原实例(属性填充和初始化后的),是一个完整的代理对象** - * `!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)`:是否有其他 bean 依赖当前 bean + * `else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName))`:是否有其他 bean 依赖当前 bean,执行到这里说明是不存在循环依赖、存在增强代理的逻辑 * `dependentBeans = getDependentBeans(beanName)`:取到依赖当前 bean 的其他 beanName * `if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean))`:判断 dependentBean 是否创建完成 * `if (!this.alreadyCreated.contains(beanName))`:成立当前 bean 尚未创建完成,当前 bean 是依赖exposedObject 的 bean,返回 true - * `return false`:创建完成返回 false - + * `return false`:创建完成返回 false + `actualDependentBeans.add(dependentBean)`:创建完成的 dependentBean 加入该集合 - * `if (!actualDependentBeans.isEmpty())`:条件成立说明有依赖于当前 bean 的 bean 实例创建完成,但是当前对象的 AOP 操作是在 initializeBean 逻辑里完成的,在之前外部 bean 持有到的当前 bean 都是尚未增强的,所以报错 + * `if (!actualDependentBeans.isEmpty())`:条件成立说明有依赖于当前 bean 的 bean 实例创建完成,但是当前的 bean 还没创建完成返回,依赖当前 bean 的外部 bean 持有的是不完整的 bean,所以需要报错 * `registerDisposableBeanIfNecessary`:判断当前 bean 是否需要注册析构回调,当容器销毁时进行回调 @@ -8196,11 +8196,12 @@ private final Map> singletonFactories = new HashMap<>(1 * 为什么需要三级缓存? * 循环依赖解决需要提前引用动态代理对象,AOP 动态代理是在 Bean 初始化后的后置处理中进行,这时的 bean 已经是成品对象,需要提前进行动态代理,三级缓存的 ObjectFactory 提前产生需要代理的对象 - * 若存在循环依赖,**后置处理不创建代理对象,真正创建代理对象的过程是在getBean(B)的阶段中** + * 若存在循环依赖,**后置处理不创建代理对象,真正创建代理对象的过程是在 getBean(B) 的阶段中** -* 一定会提前引用吗? +* 三级缓存一定会创建提前引用吗? - * 出现循环依赖才去使用,不出现就不使用 + * 出现循环依赖就会去三级缓存获取提前引用,不出现就不会 + * 如果当前有增强方法,就创建代理对象放入二级缓存,如果没有代理对象就返回 createBeanInstance 创建的实例 * wrapIfNecessary 一定创建代理对象吗?(AOP 动态代理部分有源码解析) @@ -8249,7 +8250,7 @@ private final Map> singletonFactories = new HashMap<>(1 } ``` - 填充属性时 A 依赖 B,这时需要 getBean(B),接着 B 填充属性时发现依赖 A,去进行**第一次 ** getSingleton(A) +* 填充属性时 A 依赖 B,这时需要 getBean(B),也会把 B 的工厂放入三级缓存,接着 B 填充属性时发现依赖 A,去进行**第一次 ** getSingleton(A) ```java public Object getSingleton(String beanName) { @@ -8282,7 +8283,7 @@ private final Map> singletonFactories = new HashMap<>(1 } ``` - 从三级缓存获取 A 的 Bean:`singletonFactory.getObject()`,调用了 Lambda 表达式的 getEarlyBeanReference 方法: +* 从三级缓存获取 A 的 Bean:`singletonFactory.getObject()`,调用了 Lambda 表达式的 getEarlyBeanReference 方法: ```java public Object getEarlyBeanReference(Object bean, String beanName) { @@ -8294,7 +8295,7 @@ private final Map> singletonFactories = new HashMap<>(1 } ``` - B 填充了代理后的 A 后初始化完成,**返回原始 A 的逻辑继续执行,这时的 A 还不是代理后的 A** +* B 填充了 A 的提前引用后会继续初始化直到完成,**返回原始 A 的逻辑继续执行,这时的 A 还不是代理后增强的 A** @@ -8644,6 +8645,8 @@ AbstractAutoProxyCreator.createProxy():根据增强方法创建代理对象 #### 方法增强 +main() 函数中调用用户方法,会进入该逻辑 + JdkDynamicAopProxy 类中的 invoke 方法是真正执行代理方法 ```java @@ -8747,8 +8750,6 @@ public Object invoke(Object proxy, Method method, Object[] args) * `return retVal`:返回执行的结果 -proceed() 链式获取每一个拦截器,拦截器执行 invoke方法,每一个拦截器等待下一个拦截器执行完成返回以后再来执行;拦截器链的机制,保证通知方法与目标方法的执行顺序 - 图示先从上往下建立链,然后从下往上依次执行,责任链模式 * 正常执行:(环绕通知)→ 前置通知 → 目标方法 → 后置通知 → 返回通知 @@ -8802,7 +8803,7 @@ proceed() 链式获取每一个拦截器,拦截器执行 invoke方法,每一 打开源码注释:@see org.....ClassPathBeanDefinitionScanner.doScan() - findCandidateComponents():从classPath扫描组件,并转换为备选BeanDefinition + findCandidateComponents():从 classPath 扫描组件,并转换为备选 BeanDefinition ```java protected Set doScan(String... basePackages) { @@ -12452,9 +12453,9 @@ public class ProjectExceptionAdivce { 注解:@Param -作用:当SQL语句需要多个(大于1)参数时,用来指定参数的对应规则 +作用:当 SQL 语句需要多个(大于1)参数时,用来指定参数的对应规则 -* 注解替代UserDao映射配置文件:dao.UserDao +* 注解替代 UserDao 映射配置文件:dao.UserDao ```java public interface UserDao { @@ -12633,7 +12634,7 @@ public class ProjectExceptionAdivce { ### web.xml -* 注解替代web.xml:ServletContainersInitConfig +* 注解替代 web.xml:ServletContainersInitConfig ```java public class ServletContainersInitConfig extends AbstractDispatcherServletInitializer { @@ -12677,10 +12678,10 @@ public class ProjectExceptionAdivce { } ``` -* WebApplicationContext,生成Spring核心容器(主容器/父容器/跟容器) +* WebApplicationContext,生成 Spring 核心容器(主容器/父容器/跟容器) - * 父容器:Spring环境加载后形成的容器,包含Spring环境下的所有的bean - * 子容器:当前mvc环境加载后形成的容器,不包含Spring环境下的bean + * 父容器:Spring 环境加载后形成的容器,包含 Spring 环境下的所有的 bean + * 子容器:当前 mvc 环境加载后形成的容器,不包含 Spring 环境下的 bean * 子容器可以访问父容器中的资源,父容器不可以访问子容器的资源 @@ -12697,1392 +12698,5 @@ public class ProjectExceptionAdivce { # Boot -## 基本概述 - -(这部分笔记做的非常一般,更新完 Spring 源码马上就会完善) - -SpringBoot提供了一种快速使用Spring的方式,基于约定优于配置的思想,可以让开发人员不必在配置与逻辑业务之间进行思维的切换,全身心的投入到逻辑业务的代码编写中,从而大大提高了开发的效率 - -SpringBoot功能: - -* 自动配置: - - Spring Boot的自动配置是一个运行时(更准确地说,是应用程序启动时)的过程,考虑了众多因素选择使用哪个配置,该过程是SpringBoot自动完成的。 - -* 起步依赖 - - 起步依赖本质上是一个Maven项目对象模型(Project Object Model,POM),定义了对其他库的传递依赖,这些东西加在一起即支持某项功能。 - - 简单的说,起步依赖就是将具备某种功能的坐标打包到一起,并提供一些默认的功能。 - -* 辅助功能 - - 提供了一些大型项目中常见的非功能性特性,如嵌入式服务器、安全、指标,健康检测、外部配置等。 - -**注意:Spring Boot 并不是对 Spring 功能上的增强,而是提供了一种快速使用 Spring 的方式** - - - -*** - - - -## 构建工程 - -普通构建: - -1. 创建Maven项目 - -2. 导入SpringBoot起步依赖 - - ```xml - - - org.springframework.boot - spring-boot-starter-parent - 2.1.8.RELEASE - - - - - - org.springframework.boot - spring-boot-starter-web - - - ``` - -3. 定义Controller - - ```java - @RestController - public class HelloController { - @RequestMapping("/hello") - public String hello(){ - return " hello Spring Boot !"; - } - } - ``` - -4. 编写引导类 - - ```java - //引导类。 SpringBoot项目的入口 - @SpringBootApplication - public class HelloApplication { - public static void main(String[] args) { - SpringApplication.run(HelloApplication.class,args); - } - } - ``` - -快速构建: - -![](https://gitee.com/seazean/images/raw/master/Frame/SpringBoot-IDEA构建工程1.png) - -![](https://gitee.com/seazean/images/raw/master/Frame/SpringBoot-IDEA构建工程2.png) - - - - - -*** - - - -## 基本配置 - -### 起步依赖 - -- 在spring-boot-starter-parent中定义了各种技术的版本信息,组合了一套最优搭配的技术版本。 - -- 在各种starter中,定义了完成该功能需要的坐标合集,其中大部分版本信息来自于父工程。 - -- 我们的工程继承parent,引入starter后,通过依赖传递,就可以简单方便获得需要的jar包,并且不会存在版本冲突等问题 - - - -### 配置文件 - -SpringBoot是基于约定的,很多配置都有默认值,如果想使用自己的配置替换默认配置,可以使用application.properties或者application.yml(application.yaml)进行配置。 - -1. 默认配置文件名称:application - -2. 在同一级目录下优先级为:properties>yml > yaml - -例如:配置内置Tomcat的端口 - -* properties: - - ```properties - server.port=8080 - ``` - -* yml: - - ```yaml - server: port: 8080 - ``` - -* yaml: - - ```yaml - server: port: 8080 - ``` - - - -*** - - - -### yaml语法 - -yml文件优势: - -1. YAML配置有序,支持数组,数组中的元素可以是基本数据类型也可以是对象 - -2. YAML数据在编程语言之间是可移植的 - -3. YAML匹配敏捷语言的本机数据结构 - -4. YAML具有一致的模型来支持通用工具 - -5. YAML支持单程处理 - -6. YAML具有表现力和可扩展性 - -7. YAML易于实现和使用 - -基本语法: - -- 大小写敏感 - -- **数据值前边必须有空格,作为分隔符** - -- 使用缩进表示层级关系 - -- 缩进时不允许使用Tab键,只允许使用空格(各个系统 Tab对应空格数目可能不同,导致层次混乱) - -- 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可 - -- ''#" 表示注释,从这个字符一直到行尾,都会被解析器忽略 - - ```yaml - server: - port: 8080 - address: 127.0.0.1 - name: abc - ``` - - - -数据格式: - -* 对象(map):键值对的集合。 - - ```yaml - person: - name: zhangsan - age: 20 - # 行内写法 - person: {name: zhangsan} - ``` - - 注意:不建议使用 JSON,应该使用 yaml 语法 - -* 数组:一组按次序排列的值 - - ```yaml - address: - - beijing - - shanghai - # 行内写法 - address: [beijing,shanghai] - ``` - -* 纯量:单个的、不可再分的值 - - ```yaml - msg1: 'hello \n world' # 单引忽略转义字符 - msg2: "hello \n world" # 双引识别转义字符 - ``` - -* 参数引用: - - ```yaml - name: lisi - person: - name: ${name} # 引用上边定义的name值 - ``` - - - -*** - - - -### 获取配置 - -* 注解@Value - - ```java - @RestController - public class HelloController { - @Value("${name}") - private String name; - - @Value("${person.name}") - private String name2; - - @Value("${address[0]}") - private String address1; - - @Value("${msg1}") - private String msg1; - - @Value("${msg2}") - private String msg2; - - @RequestMapping("/hello") - public String hello(){ - System.out.println("所有的数据"); - return " hello Spring Boot !"; - } - } - ``` - -* Evironment对象 - - ```java - @Autowired - private Environment env; - - @RequestMapping("/hello") - public String hello() { - System.out.println(env.getProperty("person.name")); - System.out.println(env.getProperty("address[0]")); - return " hello Spring Boot !"; - } - ``` - -* 注解@ConfigurationProperties - - **注意**:参数prefix一定要指定 - - ```java - @Component - @ConfigurationProperties(prefix = "person") - public class Person { - private String name; - private int age; - private String[] address; - } - ``` - - ```java - @Autowired - private Person person; - - @RequestMapping("/hello") - public String hello() { - System.out.println(person); - //Person{name='zhangsan', age=20, address=[beijing, shanghai]} - return " hello Spring Boot !"; - } - ``` - - - -*** - - - -### Profile - -@Profile:指定组件在哪个环境的情况下才能被注册到容器中,不指定,任何环境下都能注册这个组件 - - * 加了环境标识的bean,只有这个环境被激活的时候才能注册到容器中。默认是default环境 - * 写在配置类上,只有是指定的环境的时候,整个配置类里面的所有配置才能开始生效 - * 没有标注环境标识的bean在,任何环境下都是加载的 - -Profile的配置: - -1. **profile是用来完成不同环境下,配置动态切换功能** - -2. **profile配置方式** - - 多profile文件方式:提供多个配置文件,每个代表一种环境 - - * application-dev.properties/yml 开发环境 - * application-test.properties/yml 测试环境 - * sapplication-pro.properties/yml 生产环境 - - yml多文档方式:在yml中使用 --- 分隔不同配置 - - ```yaml - --- - server: - port: 8081 - spring: - profiles:dev - --- - server: - port: 8082 - spring: - profiles:test - --- - server: - port: 8083 - spring: - profiles:pro - --- - ``` - -3. **profile激活方式** - -- 配置文件:在配置文件中配置:spring.profiles.active=dev - - ```properties - spring.profiles.active=dev - ``` - -- 虚拟机参数:在VM options 指定:`-Dspring.profiles.active=dev` - - ![](https://gitee.com/seazean/images/raw/master/Frame/SpringBoot-profile激活方式虚拟机参数.png) - -- 命令行参数:`java –jar xxx.jar --spring.profiles.active=dev` - - 在Program arguments里输入,也可以先package - - - -*** - - - -### 加载顺序 - -项目内部配置文件加载顺序:加载顺序为上文的排列顺序,高优先级配置的属性会生效 - -- file:./config/:当前项目下的/config目录下 - -- file:./ :当前项目的根目录 - - ```yaml - file = Project工程目录 - ``` - -- classpath:/config/:classpath的/config目录 - -- classpath:/ :classpath的根目录 - - ```yaml - classpath = resoureces - ``` - -项目外部配置文件加载顺序:外部配置文件的使用是为了对能不文件的配合 - -* 命令行:在package打包后的target目录下,使用该命令 - - ```sh - java -jar myproject.jar --server.port=9000 - ``` - -* 指定配置文件位置 - - ```sh - java -jar myproject.jar --spring.config.location=e://application.properties - ``` - -* 外部不带profile的properties文件 - - ```sh - classpath:/config/application.properties#优先级更高 - classpath:/application.properties #和jar同等级的配置文件,无需配置,默认加载 - ``` - - 加载命令:`java -jar myproject.jar` - - - -*** - - - -## 整合框架 - -### Junit - -1. 搭建工程 - -2. 导入坐标 - - ```xml - - - junit - junit - 4.12 - - ``` - -3. 测试类 - - ```java - @RunWith(SpringRunner.class) - @SpringBootTest(classes = SpringbootJunitApplication.class ) - public class UserServiceTest { - @Test - public void test(){ - System.out.println(111); - } - } - ``` - - - -*** - - - -### Mybatis - -1. 搭建SpringBoot工程 - -2. 引入mybatis起步依赖,添加mysql驱动 - - ```xml - - - org.mybatis.spring.boot - mybatis-spring-boot-starter - 2.1.0 - - - - mysql - mysql-connector-java - - - - org.springframework.boot - spring-boot-starter-test - test - - - ``` - -3. 编写DataSource和MyBatis相关配置:application.yml - - ```yaml - #datasource - spring: - datasource: - url: jdbc:mysql://192.168.0.137:3306/springboot #?serverTimezone=UTC - username: root - password: 123456 - driver-class-name: com.mysql.cj.jdbc.Driver - - - #mybatis - mybatis: - mapper-locations: classpath:mapper/*Mapper.xml #mapper映射配置文件 - type-aliases-package: com.example.springbootmybatis.domain - - - # config-location: 指定mybatis的核心配置文件 - ``` - -4. 定义表和实体类 - - ```java - public class User { - private int id; - private String username; - private String password; - } - ``` - -5. 编写dao和mapper文件/纯注解开发 - - 编写dao - - ```java - @Mapper //必须加Mapper - @Repository - public interface UserXmlMapper { - public List findAll(); - } - ``` - - mapper.xml - - ```xml - - - - - - ``` - -6. 纯注解开发 - - ```java - @Mapper - @Repository - public interface UserMapper { - @Select("select * from t_user") - public List findAll(); - } - ``` - - - -*** - - - -### Redis - -1. 搭建SpringBoot工程 - -2. 引入redis起步依赖 - - ```xml - - - org.springframework.boot - spring-boot-starter-data-redis - - - - org.springframework.boot - spring-boot-starter-test - test - - - ``` - -3. 配置redis相关属性 - - ```yaml - spring: - redis: - host: 127.0.0.1 # redis的主机ip - port: 6379 - ``` - -4. 注入RedisTemplate模板 - - ```java - @RunWith(SpringRunner.class) - @SpringBootTest - public class SpringbootRedisApplicationTests { - @Autowired - private RedisTemplate redisTemplate; - - @Test - public void testSet() { - //存入数据 - redisTemplate.boundValueOps("name").set("zhangsan"); - } - @Test - public void testGet() { - //获取数据 - Object name = redisTemplate.boundValueOps("name").get(); - System.out.println(name); - } - } - ``` - - - - - -*** - - - -## 自动配置 - -### Condition - -#### 条件注解 - -Condition是Spring4.0后引入的条件化配置接口,通过实现Condition接口可以完成有条件的加载相应的Bean - -注解:@Conditional - -作用:按照一定的条件进行判断,满足条件给容器注册bean - -使用:@Conditional要配合Condition的实现类(ClassCondition)进行使用 - -ConditionContext类API: - -| 方法 | 说明 | -| --------------------------------------------------- | -------------------------- | -| ConfigurableListableBeanFactory getBeanFactory() | 获取到ioc使用的beanfactory | -| ClassLoader getClassLoader() | 获取类加载器 | -| Environment getEnvironment() | 获取当前环境信息 | -| BeanDefinitionRegistry getRegistry() | 获取到bean定义的注册类 | - -* ClassCondition - - ```java - public class ClassCondition implements Condition { - /** - * context 上下文对象。用于获取环境,IOC容器,ClassLoader对象 - * metadata 注解元对象。 可以用于获取注解定义的属性值 - */ - @Override - public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { - - //1.需求: 导入Jedis坐标后创建Bean - //思路:判断redis.clients.jedis.Jedis.class文件是否存在 - boolean flag = true; - try { - Class cls = Class.forName("redis.clients.jedis.Jedis"); - } catch (ClassNotFoundException e) { - flag = false; - } - return flag; - } - } - ``` - -* UserConfig - - ```java - @Configuration - public class UserConfig { - @Bean - @Conditional(ClassCondition.class) - public User user(){ - return new User(); - } - } - ``` - -* 启动类: - - ```java - @SpringBootApplication - public class SpringbootConditionApplication { - public static void main(String[] args) { - //启动SpringBoot应用,返回Spring的IOC容器 - ConfigurableApplicationContext context = SpringApplication.run(SpringbootConditionApplication.class, args); - - Object user = context.getBean("user"); - System.out.println(user); - } - } - ``` - - - -*** - - - -#### 自定义注解 - -需求:将类的判断定义为动态的,判断哪个字节码文件存在可以动态指定 - -* 自定义条件注解类 - - ```java - @Target({ElementType.TYPE, ElementType.METHOD}) - @Retention(RetentionPolicy.RUNTIME) - @Documented - @Conditional(ClassCondition.class) - public @interface ConditionOnClass { - String[] value(); - } - ``` - -* ClassCondition - - ```java - public class ClassCondition implements Condition { - @Override - public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata) { - - //需求:通过注解属性值value指定坐标后创建bean - Map map = metadata.getAnnotationAttributes - (ConditionOnClass.class.getName()); - //map = {value={属性值}} - //获取所有的 - String[] value = (String[]) map.get("value"); - - boolean flag = true; - try { - for (String className : value) { - Class cls = Class.forName(className); - } - } catch (Exception e) { - flag = false; - } - return flag; - } - } - ``` - -* UserConfig - - ```java - @Configuration - public class UserConfig { - @Bean - @ConditionOnClass("com.alibaba.fastjson.JSON") - public User user(){ - return new User(); - } - } - ``` - -* 测试User对象的创建 - - - -*** - - - -#### 常用注解 - -SpringBoot 提供的常用条件注解: - -ConditionalOnProperty:判断**配置文件**中是否有对应属性和值才初始化Bean - -```java -@Configuration -public class UserConfig { - @Bean - @ConditionalOnProperty(name = "it",havingValue = "seazean") - public User user() { - return new User(); - } -} -``` - -```properties -it=seazean -``` - -ConditionalOnClass:判断环境中是否有对应字节码文件才初始化Bean - -ConditionalOnMissingBean:判断环境中没有对应Bean才初始化Bean - - - - - -*** - - - -### 内置web - -pom文件中的排除依赖效果 - -```xml - - org.springframework.boot - spring-boot-starter-web - - - - spring-boot-starter-tomcat - org.springframework.boot - - - - - - - spring-boot-starter-jetty - org.springframework.boot - -``` - - - -*** - - - -### Enable - -SpringBoot不能直接获取在其他工程中定义的Bean(pom导入springboot-enable-other坐标) - -@ComponentScan 扫描范围:当前引导类所在包及其子包 - -* 所在包:com.example.springbootenable; -* 配置包:com.example.config; - -三种解决办法: - -1. 使用@ComponentScan扫描com.example.config包 -2. 使用Import注解,加载类,这些类都会被Spring创建并放入ioc容器 -3. 对Import注解进行封装 - **重点:Enable注解底层原理是使用@Import注解实现Bean的动态加载** - -```java -//1.@ComponentScan("com.example.config") -//2.@Import(UserConfig.class) -@EnableUser -@SpringBootApplication -public class SpringbootEnableApplication { - - public static void main(String[] args) { - ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args); - //获取Bean - Object user = context.getBean("user"); - System.out.println(user); - - } -} -``` - -**UserConfig:** - -```java -@Configuration -public class UserConfig { - @Bean - public User user() { - return new User(); - } -} -``` - -**EnableUser注解类:** - -```java -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -@Documented -@Import(UserConfig.class)//@Import注解实现Bean的动态加载 -public @interface EnableUser { -} -``` - - - -*** - - - -### Import - -@Enable底层依赖于@Import注解导入一些类,使用@Import导入的类会被Spring加载到IOC容器中 -@Import提供4中用法: - -1. 导入Bean:`@Import(User.class)` - -2. 导入配置类:`@Import(UserConfig.class)` - -3. 导入 ImportSelector 实现类,一般用于加载配置文件中的类 - - MyImportSelector,配置在springboot-enable-other项目,环境采用Enable, - - ```java - public class MyImportSelector implements ImportSelector { - @Override - public String[] selectImports(AnnotationMetadata importingClassMetadata) { - return new String[]{"com.example.domain.User", - "com.example.domain.Role"}; - } - } - ``` - - ```java - @Import(MyImportSelector.class) - @SpringBootApplication - public class SpringbootEnableApplication { - - public static void main(String[] args) { - ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args); - //获取Bean - Object user = context.getBean("user"); - System.out.println(user); - - } - } - ``` - -4. 导入 ImportBeanDefinitionRegistrar实现类:@Import({MyImportBeanDefinitionRegistrar.class}) - - ```java - public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { - @Override - public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { - AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class).getBeanDefinition(); - registry.registerBeanDefinition("user",beanDefinition); - } - } - ``` - - - -*** - - - -### Scheduled - -@Scheduled注解:SpringBoot提供的用于定时任务控制的注解 - -@EnableScheduling:启动定时任务,Scheduled配合此注解使用,在启动类上添加该注解 - -作用:用于控制任务在某个指定时间执行,或者每隔一段时间执行 - -注意:@Scheduled不能修饰私有方法 - -参数:cron,cron表达式有7个域,每个域之间用空格隔开,7个域分别是:秒 分钟 小时 日 月 星期 - - - - - -例如: - -* 每隔5秒执行一次:*/5 * * * * ? -* 每隔5分钟的40秒执行一次:40 */5 * * * ? - - - - - -*** - - - -### EAConfig - -注解:@SpringBootApplication - -```java -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -@Documented -@Inherited -@SpringBootConfiguration -@EnableAutoConfiguration -@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), - @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) -public @interface SpringBootApplication { -} -``` - -注解:@EnableAutoConfiguration - -```java -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -@Documented -@Inherited -@AutoConfigurationPackage -@Import(AutoConfigurationImportSelector.class)//Import第三种方式 -public @interface EnableAutoConfiguration { -} -``` - -![](https://gitee.com/seazean/images/raw/master/Frame/SpringBoot-解析EnableAutoConfiguration.png) - -- @EnableAutoConfiguration 注解内部使用 @Import(AutoConfigurationImportSelector.**class**)来加载配置类。 - -- 配置文件位置:META-INF/spring.factories,该配置文件中定义了大量的配置类,当 SpringBoot 应用启动时,会自动加载这些配置类,初始化Bean - -- 并不是所有的Bean都会被初始化,在配置类中使用Condition来加载满足条件的Bean - - - -*** - - - -### starter - -#### 步骤分析 - -需求:自定义redis-starter,要求当导入redis坐标时,SpringBoot自动创建Jedis的Bean - -步骤: - -* redis-spring-boot-autoconfigure 模块 - -* 创建 redis-spring-boot-starter 模块,依赖 redis-spring-boot-autoconfigure的模块 - -* 在redis-spring-boot-autoconfigure模块中初始化Jedis的Bean,并定义META-INF/spring.factories文件 - -* 在测试模块中引入自定义的 redis-starter 依赖,测试获取 Jedis 的Bean,操作 redis - - - -#### 功能实现 - -* 创建redis-spring-boot-starter工程 - - ```xml - - - com.example - redis-spring-boot-autoconfigure - 0.0.1-SNAPSHOT - - ``` - -* 创建redis-spring-boot-autoconfigure配置工程 - - 创建RedisProperties配置文件参数绑定类: - - ```java - @ConfigurationProperties(prefix = "redis") - //读取配置文件中redis下的配置,封装到RedisProperties - public class RedisProperties { - private String host = "127.0.0.1";//配置文件没有配置的情况下的默认值 - private int port = 6379; //默认值 - - } - ``` - - 创建RedisAutoConfiguration自动配置类: - - ```java - @Configuration - @EnableConfigurationProperties(RedisProperties.class) - public class RedisAutoConfiguration { - //提供Jedis的bean - @Bean - public Jedis jedis(RedisProperties redisProperties) { - return new Jedis(redisProperties.getHost(),redisProperties.getPort()); - - } - } - ``` - - 在resource目录下创建META-INF文件夹并创建spring.factories - - 注意:”\ “是换行使用的 - - ```properties - org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ - com.example.redis.config.RedisAutoConfiguration - ``` - - 在springboot-enable工程中引入自定义的redis的starter,进行测试: - - ```java - Jedis jedis = context.getBean(Jedis.class); - System.out.println(jedis); - ``` - - - -*** - - - -### actuator - -SpringBoot监控actuator基本使用 - -1. 导入依赖坐标 - - ```xml - - org.springframework.boot - spring-boot-starter-actuator - - ``` - -2. 访问http://localhost:8080/acruator,json字符串转换 - - ```json - { - "_links":{ - "self":{ - "href":"http://localhost:8080/actuator", - "templated":false - }, - "health":{ - "href":"http://localhost:8080/actuator/health", - "templated":false - }, - "health-component-instance":{ - "href":"http://localhost:8080/actuator/health/{component}/{instance}", - "templated":true - }, - "health-component":{ - "href":"http://localhost:8080/actuator/health/{component}", - "templated":true - }, - "info":{ - "href":"http://localhost:8080/actuator/info", - "templated":false - } - } - } - ``` - - * http://localhost:8080/actuator/info - - 在application.properties中配置 - - ```properties - info.name=lucy - info.age=99 - ``` - - * http://localhost:8080/actuator/health - - 开启健康检查详细信息 - - ```properties - management.endpoint.health.show-details=always - ``` - -3. 开启所有endpoint,在application.properties中配置: - - ```properties - management.endpoints.web.exposure.include=* - ``` - - - -*** - - - -### admin - -SpringBoot监控-springboot admin图形化界面使用: - -SpringBoot Admin 有两个角色,客户端(Client)和服务端(Server)。 - -admin-server: - -1. 创建 admin-server 模块 - -2. 导入依赖坐标 admin-starter-server,web - - ![](https://gitee.com/seazean/images/raw/master/Frame/SpringBoot-监控界面admin-server依赖导入.png) - -3. 在引导类上启用监控功能@EnableAdminServer - - ```java - @EnableAdminServer - @SpringBootApplication - public class SpringbootAdminServerApplication { - public static void main(String[] args) { - SpringApplication.run(SpringbootAdminServerApplication.class, args); - } - } - ``` - -admin-client: - -1. 创建 admin-client 模块 - -2. 导入依赖坐标 admin-starter-client - -3. 配置相关信息:server地址等 - - ```properties - # 执行admin.server地址 - spring.boot.admin.client.url=http://localhost:9000 - - management.endpoint.health.show-details=always - management.endpoints.web.exposure.include=* - ``` - -4. 启动server和client服务,访问server - - - -*** - - - -### 事件监听 - - Java中的事件监听机制定义了以下几个角色: - -* 事件:Event,继承 java.util.EventObject 类的对象 - -* 事件源:Source ,任意对象Object - -* 监听器:Listener,实现 java.util.EventListener 接口 的对象 - -SpringBoot 在项目启动时,会对几个监听器进行回调,可以实现监听器接口,在项目启动时完成一些操作 - -ApplicationContextInitializer, SpringApplicationRunListener, CommandLineRunner, ApplicationRunner - -* MyApplicationRunner - - **自定义监听器的启动时机**:MyApplicationRunner和MyCommandLineRunner都是当项目启动后执行,使用@Component放入容器即可使用 - - ```java - //当项目启动后执行run方法 - @Component - public class MyApplicationRunner implements ApplicationRunner { - @Override - public void run(ApplicationArguments args) throws Exception { - System.out.println("ApplicationRunner...run"); - System.out.println(Arrays.asList(args.getSourceArgs()));//properties配置信息 - } - } - ``` - -* MyCommandLineRunner - - ```java - @Component - public class MyCommandLineRunner implements CommandLineRunner { - @Override - public void run(String... args) throws Exception { - System.out.println("CommandLineRunner...run"); - System.out.println(Arrays.asList(args)); - } - } - ``` - -* MyApplicationContextInitializer的启用要**在resource文件夹下添加META-INF/spring.factories** - - ```properties - org.springframework.context.ApplicationContextInitializer=com.example.springbootlistener.listener.MyApplicationContextInitializer - ``` - - ```java - @Component - public class MyApplicationContextInitializer implements ApplicationContextInitializer { - @Override - public void initialize(ConfigurableApplicationContext applicationContext) { - System.out.println("ApplicationContextInitializer....initialize"); - } - } - ``` - -* MySpringApplicationRunListener的使用要添加**构造器** - - ```java - public class MySpringApplicationRunListener implements SpringApplicationRunListener { - //构造器 - public MySpringApplicationRunListener(SpringApplication sa, String[] args) { - } - - @Override - public void starting() { - System.out.println("starting...项目启动中");//输出SPRING之前 - } - - @Override - public void environmentPrepared(ConfigurableEnvironment environment) { - System.out.println("environmentPrepared...环境对象开始准备"); - } - - @Override - public void contextPrepared(ConfigurableApplicationContext context) { - System.out.println("contextPrepared...上下文对象开始准备"); - } - - @Override - public void contextLoaded(ConfigurableApplicationContext context) { - System.out.println("contextLoaded...上下文对象开始加载"); - } - - @Override - public void started(ConfigurableApplicationContext context) { - System.out.println("started...上下文对象加载完成"); - } - - @Override - public void running(ConfigurableApplicationContext context) { - System.out.println("running...项目启动完成,开始运行"); - } - - @Override - public void failed(ConfigurableApplicationContext context, Throwable exception) { - System.out.println("failed...项目启动失败"); - } - } - ``` - - - -*** - - - -### 初始化 - -1. 配置启动引导类(判断是否有启动主类),判断是否是Web环境,获取初始化类、监听器类 - - ![](https://gitee.com/seazean/images/raw/master/Frame/SpringBoot-初始化.png) - -2. 启动计时器 - -3. 执行监听器 - -4. 准备环境 - -5. 打印banner:可以resource下粘贴自定义的banner - -6. 创建context:`refreshContext(context);`,执行refreshContext方法后才真正创建Bean - - ![](https://gitee.com/seazean/images/raw/master/Frame/SpringBoot-初始化run方法.png) - - - - - -*** - - - -## 部署 - -SpringBoot 项目开发完毕后,支持两种方式部署到服务器: - -* jar包 (官方推荐,默认) -* war包 - -**更改pom文件中的打包方式为war** - -* 修改启动类 - - ```java - @SpringBootApplication - public class SpringbootDeployApplication extends SpringBootServletInitializer { - public static void main(String[] args) { - SpringApplication.run(SpringbootDeployApplication.class, args); - } - - @Override - protected SpringApplicationBuilder configure(SpringApplicationBuilder b) { - return b.sources(SpringbootDeployApplication.class); - } - } - ``` - -* 指定打包的名称 - - ```xml - war - - springboot - - - org.springframework.boot - spring-boot-maven-plugin - - - - ``` - - +(这部分笔记做的非常一般,一周内完善) From ea96275953b563d328cf2ad5c128197bb2a99b2c Mon Sep 17 00:00:00 2001 From: Seazean Date: Fri, 30 Jul 2021 21:56:33 +0800 Subject: [PATCH 006/168] Update Java Notes --- Java.md | 58 ++++++------ SSM.md | 290 +++++++++++++++++++++++--------------------------------- 2 files changed, 146 insertions(+), 202 deletions(-) diff --git a/Java.md b/Java.md index f8e6fa1..2f8e16e 100644 --- a/Java.md +++ b/Java.md @@ -169,8 +169,8 @@ G-->H[double] 包装类的作用: -* 包装类作为类首先拥有了Object类的方法。 -* 包装类作为引用类型的变量可以存储null值。 +* 包装类作为类首先拥有了 Object 类的方法 +* 包装类作为引用类型的变量可以**存储 null 值** ```java @@ -185,12 +185,12 @@ double Double char Character(特殊) boolean Boolean ``` -Java为包装类做了一些特殊功能,具体来看特殊功能主要有: +Java 为包装类做了一些特殊功能,具体来看特殊功能主要有: * 可以把基本数据类型的值转换成字符串类型的值 - 1. 调用toString()方法 - 2. 调用Integer.toString(基本数据类型的值)得到字符串 - 3. 直接把基本数据类型+空字符串就得到了字符串(推荐使用) + 1. 调用 toString() 方法 + 2. 调用 Integer.toString(基本数据类型的值) 得到字符串 + 3. 直接把基本数据类型 + 空字符串就得到了字符串(推荐使用) * 把字符串类型的数值转换成对应的基本数据类型的值(**重要**) @@ -259,6 +259,18 @@ public class PackegeClass { } ``` +**自动装箱**反编译后底层调用 `Integer.valueOf()` 实现,源码: + +```java +public static Integer valueOf(int i) { + if (i >= IntegerCache.low && i <= IntegerCache.high) + return IntegerCache.cache[i + (-IntegerCache.low)]; + return new Integer(i); +} +``` + + + *** @@ -296,28 +308,16 @@ valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中 在 jdk 1.8 所有的数值类缓冲池中,Integer 的缓存池 IntegerCache 很特殊,这个缓冲池的下界是 -128,上界默认是 127,但是上界是可调的,在启动 jvm 的时候,通过 AutoBoxCacheMax= 来指定这个缓冲池的大小,该选项在 JVM 初始化的时候会设定一个名为 java.lang.IntegerCache.high 系统属性,然后 IntegerCache 初始化的时候就会读取该系统属性来决定上界 ```java -Integer x = Integer.valueOf(100); -Integer y = Integer.valueOf(100); +Integer x = 100; //自动装箱,底层调用 Integer.valueOf(1) +Integer y = 100; System.out.println(x == y); // true -Integer x = Integer.valueOf(1000); -Integer y = Integer.valueOf(1000); +Integer x = 1000; +Integer y = 1000; System.out.println(x == y); // false //因为缓存池最大127 ``` -反编译后底层调用 `Integer.valueOf()` 实现自动装箱,源码: - -```java -public static Integer valueOf(int i) { - if (i >= IntegerCache.low && i <= IntegerCache.high) - return IntegerCache.cache[i + (-IntegerCache.low)]; - return new Integer(i); -} -``` - - - *** @@ -4390,7 +4390,7 @@ PriorityQueue 是优先级队列,底层存储结构为 Object[],默认实现 * `public PriorityQueue()`:构造默认长度为 11 的队列(数组) -* `public PriorityQueue(Comparator comparator)`:带比较器实现,可以自定义堆排序的规则 +* `public PriorityQueue(Comparator comparator)`:利用比较器自定义堆排序的规则 ```java Queue pq = new PriorityQueue<>((v1, v2) -> v2 - v1);//实现大顶堆 @@ -5428,18 +5428,18 @@ class LRUCache extends LinkedHashMap { #### TreeMap -TreeMap 实现了 SotredMap 接口,是有序不可重复的键值对集合,基于红黑树(Red-Black tree)实现,每个 key-value 都作为一个红黑树的节点。如果构造 TreeMap 没有指定比较器,则根据key执行自然排序(默认升序),如果指定了比较器则按照比较器来进行排序 +TreeMap 实现了 SotredMap 接口,是有序不可重复的键值对集合,基于红黑树(Red-Black tree)实现,每个 key-value 都作为一个红黑树的节点。如果构造 TreeMap 没有指定比较器,则根据 key 执行自然排序(默认升序),如果指定了比较器则按照比较器来进行排序 TreeSet 集合的底层是基于TreeMap,只是键的附属值为空对象而已 -TreeMap集合指定大小规则有2种方式: +TreeMap 集合指定大小规则有 2 种方式: -* 直接为对象的类实现比较器规则接口Comparable,重写比较方法(拓展方式) -* 直接为集合设置比较器Comparator对象,重写比较方法 +* 直接为对象的类实现比较器规则接口 Comparable,重写比较方法(拓展方式) +* 直接为集合设置比较器 Comparator 对象,重写比较方法 成员属性: -* Entry节点 +* Entry 节点 ```java static final class Entry implements Map.Entry { @@ -5457,7 +5457,7 @@ TreeMap集合指定大小规则有2种方式: ```java //如果comparator为null,采用comparable.compartTo进行比较,否则采用指定比较器比较大小 final int compare(Object k1, Object k2) { - return comparator==null ? ((Comparable)k1).compareTo((K)k2) + return comparator == null ? ((Comparable)k1).compareTo((K)k2) : comparator.compare((K)k1, (K)k2); } ``` diff --git a/SSM.md b/SSM.md index 5fee9db..b23374f 100644 --- a/SSM.md +++ b/SSM.md @@ -3915,7 +3915,7 @@ Mybatis 核心配置文件消失 类型:类注解 -作用:**设置当前类为spring核心配置加载类** +作用:**设置当前类为 Spring 核心配置加载类** 格式: @@ -3928,8 +3928,8 @@ public class SpringConfigClassName{ 说明: -- 核心配合类用于替换spring核心配置文件,此类可以设置空的,不设置变量与属性 -- bean扫描工作使用注解@ComponentScan替代,多个包用`{}和,`隔开 +- 核心配合类用于替换 Spring 核心配置文件,此类可以设置空的,不设置变量与属性 +- bean 扫描工作使用注解 @ComponentScan 替代,多个包用`{}和,`隔开 加载纯注解格式上下文对象,需要使用**AnnotationConfigApplicationContext** @@ -7326,7 +7326,7 @@ public int loadBeanDefinitions(Resource resource) { * `currentResources = this.resourcesCurrentlyBeingLoaded.get()`:拿到当前线程已经加载过的所有 EncodedResoure 资源,用 ThreadLocal 保证线程安全 * `if (currentResources == null)`:判断 currentResources 是否为空,为空则进行初始化 * `if (!currentResources.add(encodedResource))`:如果已经加载过该资源会报错,防止重复加载 - * `inputSource = new InputSource(inputStream)`:资源对象包装成 InputSource,InputSource 使 SAX 中的资源对象,用来进行 XML 文件的解析 + * `inputSource = new InputSource(inputStream)`:资源对象包装成 InputSource,InputSource 是 **SAX** 中的资源对象,用来进行 XML 文件的解析 * `return doLoadBeanDefinitions()`:**加载返回** * `currentResources.remove(encodedResource)`:加载完成移除当前 encodedResource * `resourcesCurrentlyBeingLoaded.remove()`:ThreadLocal 为空时移除元素,防止内存泄露 @@ -7362,7 +7362,7 @@ public int loadBeanDefinitions(Resource resource) { * `delegate.parseCustomElement(ele)`:解析自定义的标签 * `postProcessXml(root)`:解析后置处理 -* `DefaultBeanDefinitionDocumentReader.processBeanDefinition()`:**解析 bean 并注册到注册中心** +* `DefaultBeanDefinitionDocumentReader.processBeanDefinition()`:**解析 bean 标签并注册到注册中心** * `delegate.parseBeanDefinitionElement(ele)`:解析 bean 标签封装为 BeanDefinitionHolder @@ -7409,15 +7409,21 @@ ClassPathXmlApplicationContext 与 AnnotationConfigApplicationContext 差不多 ```java public AnnotationConfigApplicationContext(Class... annotatedClasses) { - // 1. 注册 Spring 内置的后置处理器的 BeanDefinition 到容器, - // 方法:AnnotationConfigUtils#registerAnnotationConfigProcessors() - // 2. 实例化路径扫描器,用于对指定的包目录进行扫描查找 bean 对象 this(); register(annotatedClasses);// 解析配置类,封装成一个 BeanDefinitionHolder,并注册到容器 refresh();// 加载刷新容器中的 Bean } ``` +```java +public AnnotationConfigApplicationContext() { + // 注册 Spring 的注解解析器到容器 + this.reader = new AnnotatedBeanDefinitionReader(this); + // 实例化路径扫描器,用于对指定的包目录进行扫描查找 bean 对象 + this.scanner = new ClassPathBeanDefinitionScanner(this); +} +``` + AbstractApplicationContext.refresh(): * prepareRefresh():刷新前的**预处理** @@ -7428,8 +7434,9 @@ AbstractApplicationContext.refresh(): * `earlyApplicationEvents= new LinkedHashSet()`:保存容器中早期的事件 * obtainFreshBeanFactory():获取一个**全新的 BeanFactory 接口实例** + `refreshBeanFactory()`:创建 BeanFactory,设置序列化 ID、读取 BeanDefinition 并加载到工厂 - + * `if (hasBeanFactory())`:applicationContext 内部拥有一个 beanFactory 实例,需要将该实例完全释放销毁 * `destroyBeans()`:销毁原 beanFactory 实例,将 beanFactory 内部维护的单实例 bean 全部清掉,如果哪个 bean 实现了 Disposablejie接口,还会进行 bean distroy 方法的调用处理 * `this.singletonsCurrentlyInDestruction = true`:设置当前 beanFactory 状态为销毁状态 @@ -7449,9 +7456,9 @@ AbstractApplicationContext.refresh(): * `customizeBeanFactory(beanFactory)`:设置是否允许覆盖和循环引用 * `loadBeanDefinitions(beanFactory)`:**加载 BeanDefinition 信息,注册到 BeanFactory 中** * `this.beanFactory = beanFactory`:把 beanFactory 填充至容器中 - + `getBeanFactory()`:返回创建的 DefaultListableBeanFactory 对象,该对象继承 BeanDefinitionRegistry - + * prepareBeanFactory(beanFactory):**BeanFactory 的预准备**工作,向容器中添加一些组件 * `setBeanClassLoader(getClassLoader())`:给当前 bf 设置一个类加载器,加载 bd 的 class 信息 @@ -7464,8 +7471,6 @@ AbstractApplicationContext.refresh(): * postProcessBeanFactory(beanFactory):BeanFactory 准备工作完成后进行的后置处理工作,通过重写这个方法来在 BeanFactory 创建并预准备完成以后做进一步的设置 -**以上是 BeanFactory 的创建及预准备工作,接下来进入 Bean 的流程** - * invokeBeanFactoryPostProcessors(beanFactory):**执行 BeanFactoryPostProcessor 的方法** * `processedBeans = new HashSet<>()`:存储已经执行过的 BeanFactoryPostProcessor 的 beanName @@ -7484,9 +7489,9 @@ AbstractApplicationContext.refresh(): * 获取到所有 BeanDefinitionRegistryPostProcessor 和 BeanFactoryPostProcessor 接口类型了,首先回调 bdrpp 类 - * 执行实现了 PriorityOrdered(主排序接口)接口的 bdrpp,再执行实现了 Ordered(次排序接口)接口的 bdrpp + * **执行实现了 PriorityOrdered(主排序接口)接口的 bdrpp,再执行实现了 Ordered(次排序接口)接口的 bdrpp** - * 最后执行没有实现任何优先级或者是顺序接口 bdrpp + * **最后执行没有实现任何优先级或者是顺序接口 bdrpp** `boolean reiterate = true`:控制 while 是否需要再次循环,循环内是查找并执行 bdrpp 后处理器的 registry 相关的接口方法,接口方法执行以后会向 bf 内注册 bd,注册的 bd 也有可能是 bdrpp 类型,所以需要该变量控制循环 @@ -7496,13 +7501,16 @@ AbstractApplicationContext.refresh(): * `beanFactory.clearMetadataCache()`:清除缓存中合并的 bean 定义,因为后置处理器可能更改了元数据 + +**以上是 BeanFactory 的创建及预准备工作,接下来进入 Bean 的流程** + * registerBeanPostProcessors(beanFactory):**注册 Bean 的后置处理器**,为了干预 Spring 初始化 bean 的流程,这里仅仅是向容器中**注入而非使用** * `beanFactory.getBeanNamesForType(BeanPostProcessor.class)`:**获取配置中实现了 BeanPostProcessor 接口类型** * `int beanProcessorTargetCount`:后置处理器的数量,已经注册的 + 未注册的 + 即将要添加的一个 - * `beanFactory.addBeanPostProcessor(new BeanPostProcessorChecker())`:添加一个后置处理器 + * `beanFactory.addBeanPostProcessor(new BeanPostProcessorChecker())`:添加一个检查器 `BeanPostProcessorChecker.postProcessAfterInitialization()`:初始化后的后处理器方法 @@ -7510,17 +7518,17 @@ AbstractApplicationContext.refresh(): * `!isInfrastructureBean(beanName)`:成立说明当前 beanName 是用户级别的 bean 不是 Spring 框架的 * `this.beanFactory.getBeanPostProcessorCount() < this.beanPostProcessorTargetCount`:BeanFactory 上面注册后处理器数量 < 后处理器数量,说明后处理框架尚未初始化完成 - * `for (String ppName : postProcessorNames)`:遍历 PostProcessor 集合,**根据实现不同的接口类型添加到不同集合** + * `for (String ppName : postProcessorNames)`:遍历 PostProcessor 集合,**根据实现不同的顺序接口添加到不同集合** * `sortPostProcessors(priorityOrderedPostProcessors, beanFactory)`:实现 PriorityOrdered 接口的后处理器排序 - `registerBeanPostProcessors(beanFactory, priorityOrderedPostProcessors)`:注册到 beanFactory 中 + `registerBeanPostProcessors(beanFactory, priorityOrderedPostProcessors)`:**注册到 beanFactory 中** * 接着排序注册实现 Ordered 接口的后置处理器,然后注册普通的( 没有实现任何优先级接口)后置处理器 * 最后排序 MergedBeanDefinitionPostProcessor 类型的处理器,根据实现的排序接口,排序完注册到 beanFactory 中 - * `beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(applicationContext))`:重新注册 ApplicationListenerDetector 后处理器,用于在 Bean 创建完成后检查是否属于 ApplicationListener 类型,如果是就把 Bean 放到监听器容器中保存起来 + * `beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(applicationContext))`:重新注册 ApplicationListenerDetector 后处理器,用于在 Bean 创建完成后检查是否属于 ApplicationListener 类型,如果是就把 Bean 放到**监听器容器**中保存起来 * initMessageSource():初始化 MessageSource 组件,主要用于做国际化功能,消息绑定与消息解析 @@ -7561,7 +7569,7 @@ AbstractApplicationContext.refresh(): * `getBean(FACTORY_BEAN_PREFIX + beanName)`:获取工厂 FactoryBean 实例本身 * `isEagerInit`:控制 FactoryBean 内部管理的 Bean 是否也初始化 - * `getBean(beanName)`:初始化 Bean,获取 Bean 详解此函数 + * `getBean(beanName)`:**初始化 Bean,获取 Bean 详解此函数** `getBean(beanName)`:不是工厂 bean 直接获取 @@ -7576,11 +7584,11 @@ AbstractApplicationContext.refresh(): * ` beanFactory.registerSingleton()`:将生命周期处理器注册到 bf 的一级缓存和注册单例集合中 * `getLifecycleProcessor().onRefresh()`:获取该**生命周期后置处理器回调 onRefresh()**,调用 `startBeans(true)` * `lifecycleBeans = getLifecycleBeans()`:获取到所有实现了 Lifecycle 接口的对象包装到 Map 内,key 是beanName, value 是 Lifecycle 对象 - * `int phase = getPhase(bean)`:获取当前 Lifecycle 的 phase 值,当前生命周期对象可能**依赖**其他生命周期对象的执行结果,所以需要 phase 决定执行顺序,数值越低的优先执行 + * `int phase = getPhase(bean)`:获取当前 Lifecycle 的 phase 值,当前生命周期对象可能依赖其他生命周期对象的执行结果,所以需要 **phase 决定执行顺序,数值越低的优先执行** * `LifecycleGroup group = phases.get(phase)`:把 phsae 相同的 Lifecycle 存入 LifecycleGroup * `if (group == null)`:group 为空则创建,初始情况下是空的 * `group.add(beanName, bean)`:将当前 Lifecycle 添加到当前 phase 值一样的 group 内 - * `Collections.sort(keys)`:从小到大排序,按优先级启动 + * `Collections.sort(keys)`:**从小到大排序,按优先级启动** * `phases.get(key).start()`:遍历所有的 Lifecycle 对象开始启动 * `doStart(this.lifecycleBeans, member.name, this.autoStartupOnly)`:底层调用该方法启动 * `bean = lifecycleBeans.remove(beanName)`: 确保 Lifecycle 只被启动一次,在一个分组内被启动了在其他分组内就看不到 Lifecycle 了 @@ -7852,7 +7860,7 @@ AbstractAutowireCapableBeanFactory.**doCreateBean**(beanName, RootBeanDefinition * 拿到配置的 property 信息和 bean 的所有字段信息 - * `pd.getWriteMethod() != null`:**当前字段是否有 setter 方法** + * `pd.getWriteMethod() != null`:**当前字段是否有 setter 方法,配置类注入的方式需要 set 方法** `!isExcludedFromDependencyCheck(pd)`:当前字段类型是否在忽略自动注入的列表中 @@ -7907,15 +7915,17 @@ AbstractAutowireCapableBeanFactory.**doCreateBean**(beanName, RootBeanDefinition * `if (mbd != null && bean.getClass() != NullBean.class)`:成立说明是配置文件的方式 - `if(!(接口条件))`表示**如果通过接口实现了初始化方法的话,就不会在调用 init-method 定义的方法**, + `if(!(接口条件))`表示**如果通过接口实现了初始化方法的话,就不会在调用配置类中 init-method 定义的方法** - `invokeCustomInitMethod`:执行自定义的方法 + `((InitializingBean) bean).afterPropertiesSet()`:调用方法 + `invokeCustomInitMethod`:执行自定义的方法 + * `initMethodName = mbd.getInitMethodName()`:获取方法名 * `Method initMethod = ()`:根据方法名获取到 init-method 方法 * ` methodToInvoke = ClassUtils.getInterfaceMethodIfPossible(initMethod)`:将方法转成从接口层面获取 * `ReflectionUtils.makeAccessible(methodToInvoke)`:访问权限设置成可访问 - * ` methodToInvoke.invoke(bean)`:**反射调用 init-method 方法**,以当前 bean 为角度去调用 + * ` methodToInvoke.invoke(bean)`:**反射调用初始化方法**,以当前 bean 为角度去调用 * `wrappedBean = applyBeanPostProcessorsAfterInitialization`:初始化后的后置处理器 @@ -8799,25 +8809,27 @@ public Object invoke(Object proxy, Method method, Object[] args) 解析 @Component 和 @Service 都是常用的注解 -* **@Component 解析流程:** +**@Component 解析流程:** - 打开源码注释:@see org.....ClassPathBeanDefinitionScanner.doScan() - - findCandidateComponents():从 classPath 扫描组件,并转换为备选 BeanDefinition +* 注解类启动容器的时,注册 ClassPathBeanDefinitionScanner 到容器,用来扫描 Bean 的相关信息 ```java protected Set doScan(String... basePackages) { Set beanDefinitions = new LinkedHashSet<>(); + // 遍历指定的所有的包 for (String basePackage : basePackages) { - //findCandidateComponents 读资源装换为BeanDefinition + // 读取当前包下的资源装换为 BeanDefinition,字节流的方式 Set candidates = findCandidateComponents(basePackage); - for (BeanDefinition candidate : candidates) {//....} - //....... + for (BeanDefinition candidate : candidates) { + //遍历,封装,类似于 XML 的解析方式 + // 注册到容器中 + registerBeanDefinition(definitionHolder, this.registry) + } return beanDefinitions; } ``` - ClassPathScanningCandidateComponentProvider.findCandidateComponents() +* ClassPathScanningCandidateComponentProvider.findCandidateComponents() ```java public Set findCandidateComponents(String basePackage) { @@ -8834,104 +8846,31 @@ public Object invoke(Object proxy, Method method, Object[] args) private Set scanCandidateComponents(String basePackage) {} ``` - * `String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX resolveBasePackage(basePackage) + '/' + this.resourcePattern` :将package转化为ClassLoader类资源搜索路径packageSearchPath,例如:`com.wl.spring.boot`转化为`classpath*:com/wl/spring/boot/**/*.class` - * `Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath)`:加载搜素路径下的资源 - * `MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource)`:获取元数据阅读器 - * isCandidateComponent:判断是否是备选组件 - * candidates.add(sbd):添加到返回结果的list - - isCandidateComponent源码: - - ```java - protected boolean isCandidateComponent(MetadataReader m) throws IOException { - //.... - for (TypeFilter tf : this.includeFilters) { - if (tf.match(m, getMetadataReaderFactory())) { - return isConditionMatch(metadataReader); - //.... - } - ``` - - ```java - protected void registerDefaultFilters() { - this.includeFilters.add(new AnnotationTypeFilter(Component.class));//... - } - ``` - - includeFilters由`registerDefaultFilters()`设置初始值,有@Component,没有@Service - - 因为@Component是@Service的元注解,Spring在读取@Service,也读取了它的元注解,并将@Service作为@Component处理 - - ```java - @Target({ElementType.TYPE}) - @Retention(RetentionPolicy.RUNTIME) - @Documented - @Component - public @interface Service {} - ``` - -* **@Component 派生性流程:** + * `String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX resolveBasePackage(basePackage) + '/' + this.resourcePattern` :将 package 转化为 ClassLoader 类资源搜索路径 packageSearchPath,例如:`com.wl.spring.boot` 转化为 `classpath*:com/wl/spring/boot/**/*.class` - metadataReader本质上:`MetadataReader metadataReader =new SimpleMetadataReader(...);` + * `resources = getResourcePatternResolver().getResources(packageSearchPath)`:加载搜素路径下的资源 - `isCandidateComponent.match()`方法:`TypeFilter.match` -->`AnnotationTypeFilter.matchSelf()` + * `for (Resource resource : resources) `:遍历所有的资源 - ```java - @Override - protected boolean matchSelf(MetadataReader metadataReader) { - AnnotationMetadata metadata = metadataReader.getAnnotationMetadata(); - return metadata.hasAnnotation(this.annotationType.getName()) || - (this.considerMetaAnnotations && metadata.hasMetaAnnotation(this.annotationType.getName())); - } - ``` + `metadataReader = getMetadataReaderFactory().getMetadataReader(resource)`:获取元数据阅读器 - * `metadata = new SimpleMetadataReader(...).getAnnotationMetadata()` + `if (isCandidateComponent(metadataReader))`:**当前类不匹配任何排除过滤器,并且匹配一个包含过滤器**,返回 true - ```java - @Override - public AnnotationMetadata getAnnotationMetadata() { - return this.annotationMetadata; - } - ``` + * includeFilters 由 `registerDefaultFilters()` 设置初始值,方法有 @Component,没有 @Service,因为 @Component 是 @Service 的元注解,Spring 在读取 @Service 时也读取了元注解,并将 @Service 作为 @Component 处理 - 观察源码:`annotationMetadata = new AnnotationMetadataReadingVisitor(classLoader);` + ```java + this.includeFilters.add(new AnnotationTypeFilter(Component.class)) + ``` - * `metadata.hasMetaAnnotation=AnnotationMetadataReadingVisitor.hasMetaAnnotation` + ```java + @Target({ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @Component + public @interface Service {} + ``` - 判断该注解的元注解在不在metaAnnotationMap中,如果在就返回true - - ```java - @Override - public boolean hasMetaAnnotation(String metaAnnotationType) { - Collection> allMetaTypes = this.metaAnnotationMap.values(); - for (Set metaTypes : allMetaTypes) { - if (metaTypes.contains(metaAnnotationType)) { - return true; - } - } - return false; - } - ``` - - metaAnnotationMap 怎么赋值的? - - metaAnnotationMap 赋值方法在`SimpleMetadataReader.SimpleMetadataReader`中: - - ```java - classReader.accept(visitor, ClassReader.SKIP_DEBUG); - ``` - - 然后通过 readElementValues 方法中: - - ```java - annotationVisitor.visitEnd(); - ``` - - 追踪方法:`AnnotationAttributesReadingVisitor.visitEnd()` - - 递归读取:内部方法`recursivelyCollectMetaAnnotations()`递归的读取注解,与注解的元注解(读@Service,再读元注解@Component) - - 添加数据:`this.metaAnnotationMap.put(annotationClass.getName(), metaAnnotationTypeNames);` + `candidates.add(sbd)`:添加到返回结果的 list @@ -8949,8 +8888,8 @@ AutowiredAnnotationBeanPostProcessor 间接实现 InstantiationAwareBeanPostProc 作用时机: -* Spring 在每个 Bean 实例化之后,调用 AutowiredAnnotationBeanPostProcessor 的 `postProcessMergedBeanDefinition()` 方法,查找该 Bean 是否有 @Autowired 注解 -* Spring 在每个 Bean 调用 `populateBean()` 进行属性注入的时候,即调用 `postProcessProperties()` 方法,查找该 Bean 属性是否有 @Autowired 注解 +* Spring 在每个 Bean 实例化之后,调用 AutowiredAnnotationBeanPostProcessor 的 `postProcessMergedBeanDefinition()` 方法,查找该 Bean 是否有 @Autowired 注解,进行相关数据的获取 +* Spring 在每个 Bean 调用 `populateBean()` 进行属性注入的时候,即调用 `postProcessProperties()` 方法,查找该 Bean 属性是否有 @Autowired 注解,进行相关数据的填充 @@ -9010,11 +8949,11 @@ ProxyTransactionManagementConfiguration:是一个 Spring 的配置类,注册 # MVC -## 概述 +## 基本介绍 -SpringMVC:是一种基于Java实现MVC模型的轻量级Web框架 +SpringMVC:是一种基于 Java 实现 MVC 模型的轻量级 Web 框架 -SpringMVC优点: +SpringMVC 优点: * 使用简单 * 性能突出(对比现有的框架技术) @@ -9073,24 +9012,24 @@ MVC(Model View Controller),一种用于设计创建Web应用程序表现 ### 工作原理 -在Spring容器初始化时会建立所有的URL和Controller的对应关系,保存到Map中,这样request就能快速根据URL定位到Controller。实现: +在 Spring 容器初始化时会建立所有的 URL 和 Controller 的对应关系,保存到 Map 中,这样 request 就能快速根据 URL 定位到 Controller。实现: -1. 在SpringIOC容器初始化完所有单例bean后 -2. SpringMVC会遍历所有的bean,获取controller中对应的URL(这里获取URL的实现类有多个,用于处理不同形式配置的Controller) -3. 将每一个URL对应一个controller存入Map中 +1. 在 Spring IOC 容器初始化完所有单例 bean 后 +2. SpringMVC 会遍历所有的 bean,获取 controller 中对应的 URL(这里获取 URL 的实现类有多个,用于处理不同形式配置的 Controller) +3. 将每一个 URL 对应一个 controller 存入 Map 中 -注意:将Controller类的注解换成@Component,启动时不会报错,但是在浏览器中输入路径时会出现404,说明Spring没有对所有的bean进行URL映射 +注意:将 Controller 类的注解换成 @Component,启动时不会报错,但是在浏览器中输入路径时会出现 404,说明 Spring 没有对所有的 bean 进行 URL 映射 -**一个Request来了:** +**一个 Request 来了:** -1. 监听端口,获得请求:Tomcat监听8080端口的请求,进行接收、解析、封装,根据路径调用了web.xml中配置的核心控制器DispatcherServlet -2. 获取Handler:进入DispatcherServlet,核心控制器调用HandlerMapping去根据请求的URL获取对应的Handler。这里有个问题,如果获取的Handler为null则返回404 -3. 调用适配器执行Handler: - * 适配器根据request的URL去Handler中寻找对应的处理方法 (Controller的URL与方法的URL拼接后对比) - * 获取到对应方法后,需要将request中的参数与方法参数上的数据进行绑定,根据反射获取方法的参数名和注解,再根据注解或者根据参数名对照进行绑定(找到对应的参数,然后在反射调用方法时传入) - * 绑定完参数后,反射调用方法获取ModelAndView(如果Handler中返回的是String、View等对象,SpringMVC也会将它们重新封装成一个ModelAndView) -4. 调用视图解析器解析:将ModelAndView解析成View对象 -5. 渲染视图:将View对象中的返回地址、参数信息等放入RequestDispatcher,最后进行转发 +1. 监听端口,获得请求:Tomcat 监听 8080 端口的请求,进行接收、解析、封装,根据路径调用了 web.xml 中配置的核心控制器 DispatcherServlet +2. 获取 Handler:进入 DispatcherServlet,核心控制器调用 HandlerMapping 去根据请求的 URL 获取对应的 Handler。这里有个问题,如果获取的 Handler 为 null 则返回 404 +3. 调用适配器执行 Handler: + * 适配器根据 request 的 URL 去 Handler 中寻找对应的处理方法 (Controller 的 URL 与方法的 URL 拼接后对比) + * 获取到对应方法后,需要将 request 中的参数与方法参数上的数据进行绑定,根据反射获取方法的参数名和注解,再根据注解或者根据参数名对照进行绑定(找到对应的参数,然后在反射调用方法时传入) + * 绑定完参数后,反射调用方法获取 ModelAndView(如果 Handler 中返回的是 String、View 等对象,SpringMVC 也会将它们重新封装成一个 ModelAndView) +4. 调用视图解析器解析:将 ModelAndView 解析成 View 对象 +5. 渲染视图:将 View 对象中的返回地址、参数信息等放入 RequestDispatcher,最后进行转发 @@ -9107,14 +9046,14 @@ MVC(Model View Controller),一种用于设计创建Web应用程序表现 流程分析: * 服务器启动 - 1. 加载web.xml中DispatcherServlet - 2. 读取spring-mvc.xml中的配置,加载所有controller包中所有标记为bean的类 - 3. 读取bean中方法上方标注@RequestMapping的内容 + 1. 加载 web.xml 中 DispatcherServlet + 2. 读取 spring-mvc.xml 中的配置,加载所有 controller 包中所有标记为 bean 的类 + 3. 读取 bean 中方法上方标注 @RequestMapping 的内容 * 处理请求 - 1. DispatcherServlet配置拦截所有请求 / - 2. 使用请求路径与所有加载的@RequestMapping的内容进行比对 + 1. DispatcherServlet 配置拦截所有请求 / + 2. 使用请求路径与所有加载的 @RequestMapping 的内容进行比对 3. 执行对应的方法 - 4. 根据方法的返回值在webapp目录中查找对应的页面并展示 + 4. 根据方法的返回值在 webapp 目录中查找对应的页面并展示 代码实现: @@ -9250,9 +9189,9 @@ MVC(Model View Controller),一种用于设计创建Web应用程序表现 ### 加载控制 -Controller加载控制:SpringMVC的处理器对应的bean必须按照规范格式开发,未避免加入无效的bean可通过bean加载过滤器进行包含设定或排除设定,表现层bean标注通常设定为@Controller +Controller 加载控制:SpringMVC 的处理器对应的 bean 必须按照规范格式开发,未避免加入无效的 bean 可通过 bean 加载过滤器进行包含设定或排除设定,表现层 bean 标注通常设定为 @Controller -* resources / spring-mvc.xml配置 +* resources / spring-mvc.xml 配置 ```xml @@ -9262,7 +9201,7 @@ Controller加载控制:SpringMVC的处理器对应的bean必须按照规范格 ``` -* 静态资源加载(webapp目录下的相关资源),spring-mvc.xml配置,开启mvc命名空间 +* 静态资源加载(webapp 目录下的相关资源),spring-mvc.xml 配置,开启 mvc 命名空间 ```xml @@ -9274,7 +9213,7 @@ Controller加载控制:SpringMVC的处理器对应的bean必须按照规范格 ``` -* 中文乱码处理 SpringMVC提供专用的中文字符过滤器,用于处理乱码问题。配置在 web.xml 里面 +* 中文乱码处理 SpringMVC 提供专用的中文字符过滤器,用于处理乱码问题。配置在 web.xml 里面 ```xml @@ -9302,7 +9241,7 @@ Controller加载控制:SpringMVC的处理器对应的bean必须按照规范格 纯注解开发: -* 使用注解形式转化SpringMVC核心配置文件为配置类 java / config / SpringMVCConfiguration.java +* 使用注解形式转化 SpringMVC 核心配置文件为配置类 java / config / SpringMVCConfiguration.java ```java @Configuration @@ -9324,7 +9263,7 @@ Controller加载控制:SpringMVC的处理器对应的bean必须按照规范格 } ``` -* 基于servlet3.0规范,自定义Servlet容器初始化配置类,加载SpringMVC核心配置类 +* 基于 servlet3.0 规范,自定义 Servlet 容器初始化配置类,加载 SpringMVC 核心配置类 ```java public class ServletContainersInitConfig extends AbstractDispatcherServletInitializer { @@ -9373,7 +9312,9 @@ Controller加载控制:SpringMVC的处理器对应的bean必须按照规范格 ### 请求映射 名称:@RequestMapping + 类型:方法注解、类注解 + 位置:处理器类中的方法定义上方、处理器类定义上方 * 方法注解 @@ -9392,10 +9333,11 @@ Controller加载控制:SpringMVC的处理器对应的bean必须按照规范格 ``` * 类注解 + 作用:为当前处理器中所有方法设定公共的访问路径前缀 带有类映射地址访问格式,将类映射地址作为前缀添加在实际映射地址前面:**/user/requestURL1** 最终返回的页面如果未设定绝对访问路径,将从类映射地址所在目录中查找 **webapp/user/page.jsp** - + ```java @Controller @RequestMapping("/user") @@ -9406,7 +9348,7 @@ Controller加载控制:SpringMVC的处理器对应的bean必须按照规范格 } } ``` - + * 常用属性 ```java @@ -10046,11 +9988,11 @@ ModelAndView 是SpringMVC提供的一个对象,该对象可以用作控制器 注解:@ResponseBody -作用:将Controller的方法返回的对象通过适当的转换器转换为指定的格式之后,写入到Response的body区。如果返回值是字符串,那么直接将字符串返回客户端;如果是一个对象,会**将对象转化为Json**,返回客户端 +作用:将 Controller 的方法返回的对象通过适当的转换器转换为指定的格式之后,写入到 Response 的 body 区。如果返回值是字符串,那么直接将字符串返回客户端;如果是一个对象,会**将对象转化为 Json**,返回客户端 -注意:当方法上面没有写ResponseBody,底层会将方法的返回值封装为ModelAndView对象。 +注意:当方法上面没有写 ResponseBody,底层会将方法的返回值封装为 ModelAndView 对象。 -* 使用HttpServletResponse对象响应数据 +* 使用 HttpServletResponse 对象响应数据 ```java @Controller @@ -10072,7 +10014,7 @@ ModelAndView 是SpringMVC提供的一个对象,该对象可以用作控制器 } ``` -* 使用jackson进行json数据格式转化 +* 使用 jackson 进行 json 数据格式转化 导入坐标: @@ -10110,7 +10052,7 @@ ModelAndView 是SpringMVC提供的一个对象,该对象可以用作控制器 } ``` -* 使用SpringMVC提供的消息类型转换器将对象与集合数据自动转换为JSON数据 +* 使用 SpringMVC 提供的消息类型转换器将对象与集合数据自动转换为JSON数据 ```java //使用SpringMVC注解驱动,对标注@ResponseBody注解的控制器方法进行结果转换,由于返回值为引用类型,自动调用jackson提供的类型转换器进行格式转换 @@ -10138,7 +10080,7 @@ ModelAndView 是SpringMVC提供的一个对象,该对象可以用作控制器 @@ -10172,9 +10114,11 @@ ModelAndView 是SpringMVC提供的一个对象,该对象可以用作控制器 -## Servlet +### Servlet + +SpringMVC 提供访问原始 Servlet 接口的功能 -* spring-mvc.xml配置 +* spring-mvc.xml 配置 ```xml @@ -10195,7 +10139,7 @@ ModelAndView 是SpringMVC提供的一个对象,该对象可以用作控制器 ``` -* SpringMVC提供访问原始Servlet接口API的功能,通过形参声明即可 +* SpringMVC 提供访问原始 Servlet 接口 API 的功能,通过形参声明即可 ```java @RequestMapping("/servletApi") @@ -10210,7 +10154,7 @@ ModelAndView 是SpringMVC提供的一个对象,该对象可以用作控制器 } ``` -* Head数据获取快捷操作方式 +* Head 数据获取快捷操作方式 名称:@RequestHeader 类型:形参注解 位置:处理器类中的方法形参前方 @@ -10225,11 +10169,11 @@ ModelAndView 是SpringMVC提供的一个对象,该对象可以用作控制器 } ``` -* Cookie数据获取快捷操作方式 +* Cookie 数据获取快捷操作方式 名称:@CookieValue 类型:形参注解 位置:处理器类中的方法形参前方 - 作用:绑定请求Cookie数据与对应处理方法形参间的关系 + 作用:绑定请求 Cookie 数据与对应处理方法形参间的关系 范例: ```java @@ -10240,7 +10184,7 @@ ModelAndView 是SpringMVC提供的一个对象,该对象可以用作控制器 } ``` -* Session数据获取 +* Session 数据获取 名称:@SessionAttribute 类型:形参注解 位置:处理器类中的方法形参前方 @@ -10261,9 +10205,9 @@ ModelAndView 是SpringMVC提供的一个对象,该对象可以用作控制器 } ``` -* Session数据设置 - 名称: @SessionAttributes - 类型: 类注解 +* Session 数据设置 + 名称:@SessionAttributes + 类型:类注解 位置:处理器类上方 作用:声明放入session范围的变量名称,适用于Model类型数据传参 范例: From 9e01fb30226bcaa31cf20bce5ef1a39f97a16a2a Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 31 Jul 2021 20:24:47 +0800 Subject: [PATCH 007/168] Update Java Notes --- Java.md | 17 +- SSM.md | 1070 ++----------------------------------------------------- Web.md | 164 +++++---- 3 files changed, 133 insertions(+), 1118 deletions(-) diff --git a/Java.md b/Java.md index 2f8e16e..da17b50 100644 --- a/Java.md +++ b/Java.md @@ -8533,14 +8533,15 @@ public class AnnotationDemo01{ * AnnotatedElement:该接口定义了与注解解析相关的方法 * Class、Method、Field、Constructor 类成分:实现 AnnotatedElement 接口,拥有解析注解的能力 -API : +Class 类 API : -* `Annotation[] getDeclaredAnnotations()` : 获得当前对象上使用的所有注解,返回注解数组 -* `T getDeclaredAnnotation(Class annotationClass)` : 根据注解类型获得对应注解对象 -* `T getAnnotation(Class annotationClass)` : 根据注解类型获得对应注解对象 -* `boolean isAnnotationPresent(Class class)` : 判断对象是否使用了指定的注解 +* `Annotation[] getDeclaredAnnotations()`:获得当前对象上使用的所有注解,返回注解数组 +* `T getDeclaredAnnotation(Class annotationClass)`:根据注解类型获得对应注解对象 +* `T getAnnotation(Class annotationClass)`:根据注解类型获得对应注解对象 +* `boolean isAnnotationPresent(Class class)`:判断对象是否使用了指定的注解 +* `boolean isAnnotation()`:此 Class 对象是否表示注释类型 -注解原理:注解本质是一个继承了`Annotation` 的特殊接口,其具体实现类是 Java 运行时生成的**动态代理类**,通过反射获取注解时,返回的是 Java 运行时生成的动态代理对象 `$Proxy1`,通过代理对象调用自定义注解(接口)的方法,会最终调用 `AnnotationInvocationHandler` 的 `invoke` 方法,该方法会从 `memberValues` 这个Map 中找出对应的值,而 `memberValues` 的来源是 Java 常量池 +注解原理:注解本质是一个继承了 `Annotation` 的特殊接口,其具体实现类是 Java 运行时生成的**动态代理类**,通过反射获取注解时,返回的是 Java 运行时生成的动态代理对象 `$Proxy1`,通过代理对象调用自定义注解(接口)的方法,会最终调用 `AnnotationInvocationHandler` 的 `invoke` 方法,该方法会从 `memberValues` 这个Map 中找出对应的值,而 `memberValues` 的来源是 Java 常量池 解析注解数据的原理:注解在哪个成分上,就先拿哪个成分对象,比如注解作用在类上,则要该类的Class对象,再来拿上面的注解 @@ -11534,11 +11535,11 @@ Java 对象创建时机: 1. 一个实例变量在对象初始化的过程中会被赋值几次? - JVM在为一个对象分配完内存之后,会给每一个实例变量赋予默认值,这个实例变量被第一次赋值 + JVM 在为一个对象分配完内存之后,会给每一个实例变量赋予默认值,这个实例变量被第一次赋值 在声明实例变量的同时对其进行了赋值操作,那么这个实例变量就被第二次赋值 在实例代码块中又对变量做了初始化操作,那么这个实例变量就被第三次赋值 在构造函数中也对变量做了初始化操作,那么这个实例变量就被第四次赋值 - 在Java的对象初始化过程中,一个实例变量最多可以被初始化4次 + 在 Java 的对象初始化过程中,一个实例变量最多可以被初始化4次 2. 类的初始化过程与类的实例化过程的异同? diff --git a/SSM.md b/SSM.md index b23374f..22e2f41 100644 --- a/SSM.md +++ b/SSM.md @@ -7961,7 +7961,7 @@ AbstractAutowireCapableBeanFactory.**doCreateBean**(beanName, RootBeanDefinition `exposedObject = earlySingletonReference`:**把代理后的 Bean 传给 exposedObject 用来返回,因为只有代理对象才封装了增强的拦截器链,main 方法中用代理对象调用方法时,会进行增强,代理是对原始对象的包装,所以这里返回的代理对象中含有完整的原实例(属性填充和初始化后的),是一个完整的代理对象** - * `else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName))`:是否有其他 bean 依赖当前 bean,执行到这里说明是不存在循环依赖、存在增强代理的逻辑 + * `else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName))`:是否有其他 bean 依赖当前 bean,执行到这里说明是不存在循环依赖、存在增强代理的逻辑,也就是正常的逻辑 * `dependentBeans = getDependentBeans(beanName)`:取到依赖当前 bean 的其他 beanName @@ -9057,7 +9057,7 @@ MVC(Model View Controller),一种用于设计创建Web应用程序表现 代码实现: -* pom.xml导入坐标 +* pom.xml 导入坐标 ```xml 4.0.0 @@ -9120,7 +9120,7 @@ MVC(Model View Controller),一种用于设计创建Web应用程序表现 ``` -* 设定具体Controller,控制层 java / controller / UserController +* 设定具体 Controller,控制层 java / controller / UserController ```java @Controller //@Component衍生注解 @@ -9371,871 +9371,11 @@ Controller 加载控制:SpringMVC 的处理器对应的 bean 必须按照规 -## 请求响应 - -### 请求 - -#### 数据准备 - -* web.xml - - ```java - //CharacterEncodingFilter + DispatcherServlet - ``` - -* spring-mvc.xml - - ```xml - - - - - ``` - - - -#### 普通类型 - -SpringMVC将传递的参数封装到处理器方法的形参中,达到快速访问参数的目的 - -* 访问URL:http://localhost/requestParam1?name=seazean&age=14 - - ```java - @Controller - public class UserController { - @RequestMapping("/requestParam1") - public String requestParam1(String name ,int age){ - System.out.println("name=" + name + ",age=" + age); - return "page.jsp"; - } - } - ``` - - ```jsp - <%@page pageEncoding="UTF-8" language="java" contentType="text/html;UTF-8" %> - - -

请求参数测试页面

- - - ``` - -@RequestParam的使用: - -* 类型:形参注解 - -* 位置:处理器类中的方法形参前方 - -* 作用:绑定请求参数与对应处理方法形参间的关系 - -* 访问URL:http://localhost/requestParam2?userName=Jock - - ```java - @RequestMapping("/requestParam2") - public String requestParam2(@RequestParam( - name = "userName", - required = true, //为true代表必须有参数 - defaultValue = "s") String name){ - System.out.println("name=" + name); - return "page.jsp"; - } - ``` - - - -*** - - - -#### POJO类型 - -##### 简单类型 - -当POJO中使用简单类型属性时, 参数名称与POJO类属性名保持一致 - -* 访问URL: http://localhost/requestParam3?name=seazean&age=14 - - ```java - @RequestMapping("/requestParam3") - public String requestParam3(User user){ - System.out.println("name=" + user.getName()); - return "page.jsp"; - } - ``` - - ```java - public class User { - private String name; - private Integer age; - //...... - } - ``` - - - -##### 参数冲突 - -当POJO类型属性与其他形参出现同名问题时,将被**同时赋值**,建议使用@RequestParam注解进行区分 - -* 访问URL: http://localhost/requestParam4?name=seazean&age=14 - - ```java - @RequestMapping("/requestParam4") - public String requestParam4(User user,String age){ - System.out.println("user.age=" + user.getAge() + ",age=" + age);//14 14 - return "page.jsp"; - } - ``` - - - -##### 复杂类型 - -当POJO中出现对象属性时,参数名称与对象层次结构名称保持一致 - -* 访问URL: http://localhost/requestParam5?address.province=beijing - - ```java - @RequestMapping("/requestParam5") - public String requestParam5(User user){ - System.out.println("user.address=" + user.getAddress().getProvince()); - return "page.jsp"; - } - ``` - - ```java - public class User { - private String name; - private Integer age; - private Address address; //.... - } - ``` - - ```java - public class Address { - private String province; - private String city; - private String address; - } - ``` - - - -##### 容器类型 - -* 通过URL地址中同名参数,可以为POJO中的集合属性进行赋值,集合属性要求保存简单数据 - - 访问URL:http://localhost/requestParam6?nick=Jock1&nick=Jockme&nick=zahc - - ```java - @RequestMapping("/requestParam6") - public String requestParam6(User user){ - System.out.println("user=" + user); - //user = User{name='null',age=null,nick={Jock1,Jockme,zahc}} - return "page.jsp"; - } - ``` - - ```java - public class User { - private String name; - private Integer age; - private List nick; - } - ``` - -* POJO中出现List保存对象数据,参数名称与对象层次结构名称保持一致,使用数组格式描述集合中对象的位置访问URL: http://localhost/requestParam7?addresses[0].province=bj&addresses[1].province=tj - - ```java - @RequestMapping("/requestParam7") - public String requestParam7(User user){ - System.out.println("user.addresses=" + user.getAddress()); - //{Address{provice=bj,city='null',address='null'}},{Address{....}} - return "page.jsp"; - } - ``` - - ```java - public class User { - private String name; - private Integer age; - private List
addresses; - } - ``` - - - -* POJO中出现Map保存对象数据,参数名称与对象层次结构名称保持一致,使用映射格式描述集合中对象位置 - - URL: http://localhost/requestParam8?addressMap[’home’].province=bj&addressMap[’job’].province=tj - - ```java - @RequestMapping("/requestParam8") - public String requestParam8(User user){ - System.out.println("user.addressMap=" + user.getAddressMap()); - //user.addressMap={home=Address{p=,c=,a=},job=Address{....}} - return "page.jsp"; - } - ``` - - ```java - public class User { - private Map addressMap; - //.... - } - ``` - - - - -*** - - - -#### 数组集合 - -##### 数组类型 - -请求参数名与处理器方法形参名保持一致,且请求参数数量> 1个 - -* 访问URL: http://localhost/requestParam9?nick=Jockme&nick=zahc - - ```java - @RequestMapping("/requestParam9") - public String requestParam9(String[] nick){ - System.out.println(nick[0] + "," + nick[1]); - return "page.jsp"; - } - ``` - - - -##### 集合类型 - -保存简单类型数据,请求参数名与处理器方法形参名保持一致,且请求参数数量> 1个 - -* 访问URL: http://localhost/requestParam10?nick=Jockme&nick=zahc - - ```java - @RequestMapping("/requestParam10") - public String requestParam10(@RequestParam("nick") List nick){ - System.out.println(nick); - return "page.jsp"; - } - ``` - -* 注意: SpringMVC默认将List作为对象处理,赋值前先创建对象,然后将nick**作为对象的属性**进行处理。由于List是接口,无法创建对象,报无法找到构造方法异常;修复类型为可创建对象的ArrayList类型后,对象可以创建,但没有nick属性,因此数据为空。 - 解决方法:需要告知SpringMVC的处理器nick是一组数据,而不是一个单一属性。通过@RequestParam注解,将数量大于1个names参数打包成参数数组后, SpringMVC才能识别该数据格式,并判定形参类型是否为数组或集合,并按数组或集合对象的形式操作数据 - - - -*** - - - -#### 转换器 - -##### 类型转换器 - -开启转换配置:` ` -作用:提供Controller请求转发,Json自动转换等功能 - -如果访问URL:http://localhost/requestParam1?name=seazean&age=seazean,会出现报错,类型转化异常 - -```java -@RequestMapping("/requestParam1") -public String requestParam1(String name ,int age){ - System.out.println("name=" + name + ",age=" + age); - return "page.jsp"; -} -``` - -SpringMVC对接收的数据进行自动类型转换,该工作通过Converter接口实现: - -* **标量转换器** - StringToBooleanConverter String→Boolean - ObjectToStringConverter Object→String - StringToNumberConverterFactory String→Number( Integer、 Long等) - NumberToNumberConverterFactory Number子类型之间(Integer、 Long、 Double等) - StringToCharacterConverter String→java.lang.Character - NumberToCharacterConverter Number子类型(Integer、 Long、 Double等)→java.lang.Character - CharacterToNumberFactory java.lang.Character→Number子类型(Integer、 Long、 Double等) - StringToEnumConverterFactory String→enum类型 - EnumToStringConverter enum类型→String - StringToLocaleConverter String→java.util.Local - PropertiesToStringConverter java.util.Properties→String - StringToPropertiesConverter String→java.util.Properties - -* **集合、数组相关转换器** - ArrayToCollectionConverter 数组→集合( List、 Set) - CollectionToArrayConverter 集合( List、 Set) →数组 - ArrayToArrayConverter 数组间 - CollectionToCollectionConverter 集合间( List、 Set) - MapToMapConverter Map间 - ArrayToStringConverter 数组→String类型 - StringToArrayConverter String→数组, trim后使用“,”split - ArrayToObjectConverter 数组→Object - ObjectToArrayConverter Object→单元素数组 - CollectionToStringConverter 集合( List、 Set) →String - StringToCollectionConverter String→集合( List、 Set), trim后使用“,”split - CollectionToObjectConverter 集合→Object - ObjectToCollectionConverter Object→单元素集合 -* **默认转换器** - ObjectToObjectConverter Object间 - IdToEntityConverter Id→Entity - FallbackObjectToStringConverter Object→String - - - -##### 日期类型转换 - -![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-date数据类型转换.png) - -如果访问URLhttp://localhost/requestParam11?date=1999-09-09会报错,所以需要日期类型转换 - -* 声明自定义的转换格式并覆盖系统转换格式,配置resources / spring-mvc.xml - - ```xml - - - - - - - - - - - - - - - - - ``` - -* @DateTimeFormat - 类型:形参注解、成员变量注解 - 位置:形参前面 或 成员变量上方 - 作用:为当前参数或变量指定类型转换规则 - - ```java - public String requestParam12(@DateTimeFormat(pattern = "yyyy-MM-dd") Date date){ - System.out.println("date=" + date); - return "page.jsp"; - } - ``` - - ```java - @DateTimeFormat(pattern = "yyyy-MM-dd") - private Date date; - ``` - - 依赖注解驱动支持,xml开启配置: - - ```xml - - ``` - - - -*** - - - -##### 自定义类型转换器 - -* 自定义类型转换器,实现Converter接口,并制定转换前与转换后的类型 - - ```java - //自定义类型转换器,实现Converter接口,接口中指定的泛型即为最终作用的条件 - //本例中的泛型填写的是String,Date,最终出现字符串转日期时,该类型转换器生效 - public class MyDateConverter implements Converter { - //重写接口的抽象方法,参数由泛型决定 - public Date convert(String source) { - DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); - Date date = null; - //类型转换器无法预计使用过程中出现的异常,因此必须在类型转换器内部捕获, - //不允许抛出,框架无法预计此类异常如何处理 - try { - date = df.parse(source); - } catch (ParseException e) { - e.printStackTrace(); - } - return date; - } - } - ``` - -* 配置resources / spring-mvc.xml,注册自定义转换器,将功能加入到SpringMVC转换服务ConverterService中 - - ```xml - - - - - - - - - - - - - - - - - ``` - -* 使用转换器 - - ```java - @RequestMapping("/requestParam12") - public String requestParam12(Date date){ - System.out.println(date); - return "page.jsp"; - } - ``` - - - - - -*** - - - -### 响应 - -#### 页面跳转 - -请求转发和重定向: - -* 转发 - - ```java - @Controller - public class UserController { - @RequestMapping("/showPage1") - public String showPage1() { - System.out.println("user mvc controller is running ..."); - return "forward:/WEB-INF/page/page.jsp; - } - } - ``` - -* 重定向 - - ```java - @RequestMapping("/showPage2") - public String showPage2() { - System.out.println("user mvc controller is running ..."); - return "redirect:/WEB-INF/page/page.jsp";//不能访问WEB-INF下的资源 - } - ``` - - - -页面访问快捷设定 (InternalResourceViewResolver): - -* 展示页面的保存位置通常固定且结构相似,可以设定通用的访问路径简化页面配置,配置spring-mvc.xml: - - ```xml - - - - - ``` - -* 简化 - - ```java - @RequestMapping("/showPage3") - public String showPage3() { - System.out.println("user mvc controller is running..."); - return "page"; - } - @RequestMapping("/showPage4") - public String showPage4() { - System.out.println("user mvc controller is running..."); - return "forward:page"; - } - - @RequestMapping("/showPage5") - public String showPage5() { - System.out.println("user mvc controller is running..."); - return "redirect:page"; - } - ``` - -* 如果未设定了返回值,使用void类型,则默认使用访问路径作页面地址的前缀后缀 - - ```java - //最简页面配置方式,使用访问路径作为页面名称,省略返回值 - @RequestMapping("/showPage6") - public void showPage6() { - System.out.println("user mvc controller is running ..."); - } - ``` - - - -*** - - - -#### 带数据跳转 - -ModelAndView 是SpringMVC提供的一个对象,该对象可以用作控制器方法的返回值(Model同) -作用: - -+ 设置数据,向请求域对象中存储数据 -+ 设置视图,逻辑视图 - -代码实现: - -* 使用HttpServletRequest类型形参进行数据传递 - - ```java - @Controller - public class BookController { - @RequestMapping("/showPageAndData1") - public String showPageAndData1(HttpServletRequest request) { - request.setAttribute("name","seazean"); - return "page"; - } - } - ``` - -* 使用Model类型形参进行数据传递 - - ```java - @RequestMapping("/showPageAndData2") - public String showPageAndData2(Model model) { - model.addAttribute("name","seazean"); - Book book = new Book(); - book.setName("SpringMVC入门实战"); - book.setPrice(66.6d); - //添加数据的方式,key对value - model.addAttribute("book",book); - return "page"; - } - ``` - - ```java - public class Book { - private String name; - private Double price; - } - ``` - -* 使用ModelAndView类型形参进行数据传递,将该对象作为返回值传递给调用者 - - ```java - @RequestMapping("/showPageAndData3") - public ModelAndView showPageAndData3(ModelAndView modelAndView) { - //ModelAndView mav = new ModelAndView(); 替换形参中的参数 - Book book = new Book(); - book.setName("SpringMVC入门案例"); - book.setPrice(66.66d); - - //添加数据的方式,key对value - modelAndView.addObject("book",book); - modelAndView.addObject("name","Jockme"); - //设置页面的方式,该方法最后一次执行的结果生效 - modelAndView.setViewName("page"); - //返回值设定成ModelAndView对象 - return modelAndView; - } - ``` - -* ModelAndView扩展 - - ```java - //ModelAndView对象支持转发的手工设定,该设定不会启用前缀后缀的页面拼接格式 - @RequestMapping("/showPageAndData4") - public ModelAndView showPageAndData4(ModelAndView modelAndView) { - modelAndView.setViewName("forward:/WEB-INF/page/page.jsp"); - return modelAndView; - } - - //ModelAndView对象支持重定向的手工设定,该设定不会启用前缀后缀的页面拼接格式 - @RequestMapping("/showPageAndData5") - public ModelAndView showPageAndData6(ModelAndView modelAndView) { - modelAndView.setViewName("redirect:page.jsp"); - return modelAndView; - } - ``` - - - -*** - - - -#### JSON数据 - -注解:@ResponseBody - -作用:将 Controller 的方法返回的对象通过适当的转换器转换为指定的格式之后,写入到 Response 的 body 区。如果返回值是字符串,那么直接将字符串返回客户端;如果是一个对象,会**将对象转化为 Json**,返回客户端 - -注意:当方法上面没有写 ResponseBody,底层会将方法的返回值封装为 ModelAndView 对象。 - -* 使用 HttpServletResponse 对象响应数据 - - ```java - @Controller - public class AccountController { - @RequestMapping("/showData1") - public void showData1(HttpServletResponse response) throws IOException { - response.getWriter().write("message"); - } - } - ``` - -* 使用**@ResponseBody将返回的结果作为响应内容**(页面显示),而非响应的页面名称 - - ```java - @RequestMapping("/showData2") - @ResponseBody - public String showData2(){ - return "{'name':'Jock'}"; - } - ``` - -* 使用 jackson 进行 json 数据格式转化 - - 导入坐标: - - ```xml - - - com.fasterxml.jackson.core - jackson-core - 2.9.0 - - - - com.fasterxml.jackson.core - jackson-databind - 2.9.0 - - - - com.fasterxml.jackson.core - jackson-annotations - 2.9.0 - - ``` - - ```java - @RequestMapping("/showData3") - @ResponseBody - public String showData3() throws JsonProcessingException { - Book book = new Book(); - book.setName("SpringMVC入门案例"); - book.setPrice(66.66d); - - ObjectMapper om = new ObjectMapper(); - return om.writeValueAsString(book); - } - ``` - -* 使用 SpringMVC 提供的消息类型转换器将对象与集合数据自动转换为JSON数据 - - ```java - //使用SpringMVC注解驱动,对标注@ResponseBody注解的控制器方法进行结果转换,由于返回值为引用类型,自动调用jackson提供的类型转换器进行格式转换 - @RequestMapping("/showData4") - @ResponseBody - public Book showData4() { - Book book = new Book(); - book.setName("SpringMVC入门案例"); - book.setPrice(66.66d); - return book; - } - ``` - - * 手工添加信息类型转换器 - - ```xml - - - - - - - - - ``` - -* 转换集合类型数据 - - ```java - @RequestMapping("/showData5") - @ResponseBody - public List showData5() { - Book book1 = new Book(); - book1.setName("SpringMVC入门案例"); - book1.setPrice(66.66d); - - Book book2 = new Book(); - book2.setName("SpringMVC入门案例"); - book2.setPrice(66.66d); - - ArrayList al = new ArrayList(); - al.add(book1); - al.add(book2); - return al; - } - ``` - - - -**** - - - -### Servlet - -SpringMVC 提供访问原始 Servlet 接口的功能 - -* spring-mvc.xml 配置 - - ```xml - - - - - - - - - - - ``` - -* SpringMVC 提供访问原始 Servlet 接口 API 的功能,通过形参声明即可 - - ```java - @RequestMapping("/servletApi") - public String servletApi(HttpServletRequest request, - HttpServletResponse response, HttpSession session){ - System.out.println(request); - System.out.println(response); - System.out.println(session); - request.setAttribute("name","seazean"); - System.out.println(request.getAttribute("name")); - return "page.jsp"; - } - ``` - -* Head 数据获取快捷操作方式 - 名称:@RequestHeader - 类型:形参注解 - 位置:处理器类中的方法形参前方 - 作用:绑定请求头数据与对应处理方法形参间的关系 - 范例: - - ```java - 快捷操作方式@RequestMapping("/headApi") - public String headApi(@RequestHeader("Accept-Language") String headMsg){ - System.out.println(headMsg); - return "page"; - } - ``` - -* Cookie 数据获取快捷操作方式 - 名称:@CookieValue - 类型:形参注解 - 位置:处理器类中的方法形参前方 - 作用:绑定请求 Cookie 数据与对应处理方法形参间的关系 - 范例: - - ```java - @RequestMapping("/cookieApi") - public String cookieApi(@CookieValue("JSESSIONID") String jsessionid){ - System.out.println(jsessionid); - return "page"; - } - ``` - -* Session 数据获取 - 名称:@SessionAttribute - 类型:形参注解 - 位置:处理器类中的方法形参前方 - 作用:绑定请求Session数据与对应处理方法形参间的关系 - 范例: - - ```java - @RequestMapping("/sessionApi") - public String sessionApi(@SessionAttribute("name") String name){ - System.out.println(name); - return "page.jsp"; - } - //用于在session中放入数据 - @RequestMapping("/setSessionData") - public String setSessionData(HttpSession session){ - session.setAttribute("name","seazean"); - return "page"; - } - ``` +## 基本操作 -* Session 数据设置 - 名称:@SessionAttributes - 类型:类注解 - 位置:处理器类上方 - 作用:声明放入session范围的变量名称,适用于Model类型数据传参 - 范例: +(正在更新请求与响应的笔记) - ```java - @Controller - //设定当前类中名称为age和gender的变量放入session范围,不常用 - @SessionAttributes(names = {"age","gender"}) - public class ServletController { - //将数据放入session存储范围,Model对象实现数据set,@SessionAttributes注解实现范围设定 - @RequestMapping("/setSessionData2") - public String setSessionDate2(Model model) { - model.addAttribute("age",39); - model.addAttribute("gender","男"); - return "page"; - } - - @RequestMapping("/sessionApi") - public String sessionApi(@SessionAttribute("age") int age, - @SessionAttribute("gender") String gender){ - System.out.println(name); - System.out.println(age); - return "page"; - } - } - ``` - @@ -10356,9 +9496,9 @@ spring-mvc.xml: ### 响应数据 注解:@ResponseBody -作用:将java对象转为json格式的数据 +作用:将 java 对象转为 json 格式的数据 -方法返回值为Pojo时,自动封装数据成Json对象数据: +方法返回值为 POJO 时,自动封装数据成 Json 对象数据: ```java @RequestMapping("/ajaxReturnJson") @@ -10370,7 +9510,7 @@ public User ajaxReturnJson(){ } ``` -方法返回值为List时,自动封装数据成json对象数组数据: +方法返回值为 List 时,自动封装数据成 json 对象数组数据: ```java @RequestMapping("/ajaxReturnJsonList") @@ -10388,7 +9528,7 @@ public List ajaxReturnJsonList(){ } ``` -AJAX文件 +AJAX 文件: ```js //为id="testAjaxReturnString"的组件绑定点击事件 @@ -10443,16 +9583,16 @@ $("#testAjaxReturnJsonList").click(function(){ 环境搭建: * 为当前主机添加备用域名 - * 修改windows安装目录中的host文件 + * 修改 windows 安装目录中的 host 文件 * 格式: ip 域名 -* 动态刷新DNS +* 动态刷新 DNS * 命令: ipconfig /displaydns * 命令: ipconfig /flushdns 跨域访问支持: -* 名称: @CrossOrigin -* 类型: 方法注解 、 类注解 +* 名称:@CrossOrigin +* 类型:方法注解 、 类注解 * 位置:处理器类中的方法上方 或 类上方 * 作用:设置当前处理器方法 / 处理器类中所有方法支持跨域访问 * 范例: @@ -10471,7 +9611,7 @@ public User cross(HttpServletRequest request){ } ``` -* jsp文件 +* jsp 文件 ```html 跨域访问
@@ -10504,24 +9644,24 @@ public User cross(HttpServletRequest request){ ## 拦截器 -### 概述 +### 基本介绍 -拦截器( Interceptor)是一种动态拦截方法调用的机制 +拦截器(Interceptor)是一种动态拦截方法调用的机制 作用: 1. 在指定的方法调用前后执行预先设定后的的代码 2. 阻止原始方法的执行 -核心原理:AOP思想 +核心原理:AOP 思想 拦截器链:多个拦截器按照一定的顺序,对原始被调用功能进行增强 拦截器和过滤器对比: -1. 归属不同: Filter属于Servlet技术, Interceptor属于SpringMVC技术 +1. 归属不同: Filter 属于 Servlet 技术, Interceptor 属于 SpringMVC 技术 -2. 拦截内容不同: Filter对所有访问进行增强, Interceptor仅针对SpringMVC的访问进行增强 +2. 拦截内容不同: Filter 对所有访问进行增强, Interceptor 仅针对 SpringMVC 的访问进行增强 ![](https://gitee.com/seazean/images/raw/master/Frame/拦截器-过滤器和拦截器的运行机制.png) @@ -10748,7 +9888,7 @@ public void afterCompletion(HttpServletRequest request, ## 异常处理 -### 异常处理器 +### 处理器 异常处理器: **HandlerExceptionResolver**接口 @@ -11004,176 +10144,6 @@ ExceptionHandler注解: -*** - - - - - -## Reatful - -### Rest概述 - -Rest (REpresentational State Transfer):表现层状态转化,定义了**资源”在网络传输中以某种“表现形式”进行“状态转移**,即网络资源的访问方式 - -* 资源:把真实的对象数据称为资源,一个资源既可以是一个集合,也可以是单个个体;每一种资源都有特定的 URI(统一资源标识符)与之对应,如果获取这个资源,访问这个 URI 就可以,比如获取特定的班级`/class/12`;资源也可以包含子资源,比如 `/classes/classId/teachers`某个指定班级的所有老师 -* 表现形式:"资源"是一种信息实体,它可以有多种外在表现形式,把"资源"具体呈现出来的形式比如 json、xml、image、txt 等等叫做它的"表现层/表现形式" -* 状态转移:描述的服务器端资源的状态,比如增删改查(通过 HTTP 动词实现)引起资源状态的改变,互联网通信协议 HTTP 协议,是一个**无状态协议**,所有的资源状态都保存在服务器端 - - - -*** - - - -### Restful - -#### 风格 - -Restful是按照Rest风格访问网络资源 - -* 传统风格访问路径:http://localhost/user/get?id=1 -* Rest风格访问路径:http://localhost/user/1 - -优点:隐藏资源的访问行为,通过地址无法得知做的是何种操作,书写简化 - -Rest行为约定方式: - -* GET(查询) http://localhost/user/1 GET - -* POST(保存) http://localhost/user POST - -* PUT(更新) http://localhost/user PUT - -* DELETE(删除) http://localhost/user DELETE - - 注意:上述行为是约定方式,约定不是规范,可以打破,所以称Rest风格,而不是Rest规范 - - - - - -#### 开发 - -Restful请求路径简化配置方式:@RestController = @Controller + @ResponseBody - -相关注解: - -* `@GetMapping("/poll")` = `@RequestMapping(value = "/poll",method = RequestMethod.GET)` - -* `@PostMapping("/push")` = `@RequestMapping(value = "/push",method = RequestMethod.POST)` - -* `@GetMapping("{id}")`:Restful开发 - - ```java - public String getMessage(@PathVariable("id") Integer id){} - ``` - - `@PathVariable`注解的参数一般在有多个参数的时候添加 - -过滤器:HiddenHttpMethodFilter 是 SpringMVC 对 Restful 风格的访问支持的过滤器 - -代码实现: - -* restful.jsp: - - * 页面表单使用隐藏域提交请求类型,参数名称固定为_method,必须配合提交类型method=post使用 - - * GET请求通过地址栏可以发送,也可以通过设置form的请求方式提交 - * POST请求必须通过form的请求方式提交 - - ```html - <%@page pageEncoding="UTF-8" language="java" contentType="text/html;UTF-8" %> -

restful风格请求表单

- <%--切换请求路径为restful风格--%> -
- <%--当添加了name为_method的隐藏域时,通过设置该隐藏域的值,修改请求的提交方式--%> - <%--切换为PUT请求或DELETE请求,但是form表单的提交方式method属性必须填写post--%> - - -
- ``` - -* java / controller / UserController - - ```java - //设置rest风格的控制器 - @RestController - //设置公共访问路径,配合下方访问路径使用 - @RequestMapping("/user/") - public class UserController { - - //rest风格访问路径完整书写方式,使用@PathVariable注解获取路径上配置的具名变量 - @RequestMapping("/user/{id}") - public String restLocation(@PathVariable Integer id){ - System.out.println("restful is running ...."); - return "success.jsp"; - } - - //rest风格访问路径简化书写方式,配合类注解@RequestMapping使用 - @RequestMapping("{id}") - public String restLocation2(@PathVariable Integer id){ - System.out.println("restful is running ....get:"+id); - return "success.jsp"; - } - - //接收GET请求配置方式 - @RequestMapping(value = "{id}",method = RequestMethod.GET) - //接收GET请求简化配置方式 - @GetMapping("{id}") - public String get(@PathVariable Integer id){ - System.out.println("restful is running ....get:"+id); - return "success.jsp"; - } - - //接收POST请求配置方式 - @RequestMapping(value = "{id}",method = RequestMethod.POST) - //接收POST请求简化配置方式 - @PostMapping("{id}") - public String post(@PathVariable Integer id){ - System.out.println("restful is running ....post:"+id); - return "success.jsp"; - } - - //接收PUT请求简化配置方式 - @PutMapping("{id}") - public String put(@PathVariable Integer id){ - System.out.println("restful is running ....put:"+id); - return "success.jsp"; - } - - //接收DELETE请求简化配置方式 - @DeleteMapping("{id}") - public String delete(@PathVariable Integer id){ - System.out.println("restful is running ....delete:"+id); - return "success.jsp"; - } - } - ``` - -* 配置拦截器 web.xml - - ```xml - - - HiddenHttpMethodFilter - org.springframework.web.filter.HiddenHttpMethodFilter - - - HiddenHttpMethodFilter - DispatcherServlet - - ``` - - - - -### Postman - -**postman** 是 一款可以发送Restful风格请求的工具,方便开发调试,首次运行需要联网注册 - -网址:https://www.postman.com/ - *** diff --git a/Web.md b/Web.md index 1a1e7cf..68f8600 100644 --- a/Web.md +++ b/Web.md @@ -2377,7 +2377,11 @@ HTTP 和 HTTPS 的区别: `JavaEE`的版本是延续了`J2EE`的版本,但是没有继续采用其命名规则。`J2EE`的版本从1.0开始到1.4结束,而`JavaEE`版本是从`JavaEE 5`版本开始,目前最新的的版本是`JavaEE 8`。 -详情请参考:[JavaEE8规范概览](https://www.oracle.com/technetwork/cn/java/javaee/overview/index.html) +详情请参考:[JavaEE8 规范概览](https://www.oracle.com/technetwork/cn/java/javaee/overview/index.html) + + + +*** @@ -2398,6 +2402,10 @@ Web,在计算机领域指网络。像我们接触的`WWW`,它是由3个单 +*** + + + ### 系统结构 基础结构划分:C/S结构,B/S结构两类。 @@ -2455,6 +2463,10 @@ Web,在计算机领域指网络。像我们接触的`WWW`,它是由3个单 +*** + + + ### 基本介绍 #### Windows安装 @@ -2467,6 +2479,8 @@ Web,在计算机领域指网络。像我们接触的`WWW`,它是由3个单 +*** + #### Linux安装 @@ -2485,6 +2499,8 @@ Web,在计算机领域指网络。像我们接触的`WWW`,它是由3个单 +*** + #### 启动停止 @@ -2497,23 +2513,31 @@ Tomcat服务器的停止文件也在二进制文件目录bin中:shutdown.bat +*** + + + #### 常见问题 * 启动一闪而过 - 没有配置环境变量,配置上JAVA_HOME环境变量。 + 没有配置环境变量,配置上 JAVA_HOME 环境变量。 -* Tomcat启动后控制台输出乱码 +* Tomcat 启动后控制台输出乱码 - 打开`/conf/logging.properties`,设置gbk`java.util.logging.ConsoleHandler.encoding = gbk` + 打开 `/conf/logging.properties`,设置 gbk `java.util.logging.ConsoleHandler.encoding = gbk` * Address already in use : JVM_Bind:端口被占用,找到占用该端口的应用 - * 进程不重要:使用cmd命令:netstat -a -o 查看pid 在任务管理器中结束占用端口的进程 + * 进程不重要:使用cmd命令:netstat -a -o 查看 pid 在任务管理器中结束占用端口的进程 - * 进程很重要:修改自己的端口号。修改的是Tomcat目录下`\conf\server.xml`中的配置。 + * 进程很重要:修改自己的端口号。修改的是 Tomcat 目录下`\conf\server.xml`中的配置。 ![](https://gitee.com/seazean/images/raw/master/Web/Tomcat-server.xml端口配置.png) + + + +*** @@ -2533,19 +2557,25 @@ Run -> Edit Configurations -> Templates -> Tomcat Server -> Local #### 虚拟目录 -在`server.xml`的``元素中加一个``元素。 - `path`:访问资源URI。URI名称可以随便起,但是必须在前面加上一个/ - `docBase`:资源所在的磁盘物理地址。 +在 `server.xml` 的 `` 元素中加一个 `` 元素 + +* `path`:访问资源URI,URI名称可以随便起,但是必须在前面加上一个/ +* `docBase`:资源所在的磁盘物理地址 + + + +*** #### 虚拟主机 在``元素中添加一个``,其中: - `name`:指定主机的名称 - `appBase`:当前主机的应用发布目录 - `unparkWARs`:启动时是否自动解压war包 - `autoDeploy`:是否自动发布 + +* `name`:指定主机的名称 +* `appBase`:当前主机的应用发布目录 +* `unparkWARs`:启动时是否自动解压war包 +* `autoDeploy`:是否自动发布 ```xml @@ -2555,6 +2585,8 @@ Run -> Edit Configurations -> Templates -> Tomcat Server -> Local +**** + #### IDEA部署 @@ -2569,15 +2601,17 @@ Run -> Edit Configurations -> Templates -> Tomcat Server -> Local +*** + #### IDEA发布 -把资源移动到Tomcat工程下web目录中,两种访问方式 +把资源移动到 Tomcat 工程下 web 目录中,两种访问方式 * 直接访问:http://localhost:8080/Tomcat/login/login.html -* 在web.xml中配置默认主页 +* 在 web.xml 中配置默认主页 ```xml @@ -2597,13 +2631,14 @@ Run -> Edit Configurations -> Templates -> Tomcat Server -> Local ### Socket -Socket是使用TCP/IP或者UDP协议在服务器与客户端之间进行传输的技术,是网络编程的基础 +Socket 是使用 TCP/IP 或者 UDP 协议在服务器与客户端之间进行传输的技术,是网络编程的基础 + +- **Servlet 是使用 HTTP 协议在服务器与客户端之间通信的技术,是 Socket 的一种应用** +- **HTTP 协议:是在 TCP/IP 协议之上进一步封装的一层协议,关注数据传输的格式是否规范,底层的数据传输还是运用了 Socket 和 TCP/IP** -- **Servlet是使用HTTP协议在服务器与客户端之间通信的技术,是Socket的一种应用** -- **HTTP协议:是在TCP/IP协议之上进一步封装的一层协议,关注数据传输的格式是否规范,底层的数据传输还是运用了Socket和TCP/IP** +Tomcat 和 Servlet 的关系: -Tomcat和Servlet的关系: -Servlet的运行环境叫做Web容器或Servlet服务器,**Tomcat 是Web应用服务器,是一个Servlet/JSP容器**。Tomcat 作为Servlet容器,负责处理客户请求,把请求传送给Servlet,并将Servlet的响应传送回给客户。而Servlet是一种运行在支持Java语言的服务器上的组件,Servlet最常见的用途是扩展Java Web服务器功能,提供非常安全的、可移植的、易于使用的CGI替代品 +Servlet 的运行环境叫做 Web 容器或 Servlet 服务器,**Tomcat 是 Web 应用服务器,是一个 Servlet/JSP 容器**。Tomcat 作为 Servlet 容器,负责处理客户请求,把请求传送给 Servlet,并将 Servlet 的响应传送回给客户。而 Servlet 是一种运行在支持Java语言的服务器上的组件,Servlet 最常见的用途是扩展 Java Web 服务器功能,提供非常安全的、可移植的、易于使用的 CGI 替代品 ![](https://gitee.com/seazean/images/raw/master/Web/Tomcat与Servlet的关系.png) @@ -2636,7 +2671,7 @@ Servlet是SUN公司提供的一套规范,名称就叫Servlet规范,它也是 #### 执行流程 -创建Web工程-->编写普通类继承Servlet相关类-->重写方法 +创建 Web 工程 → 编写普通类继承 Servlet 相关类 → 重写方法 ![](https://gitee.com/seazean/images/raw/master/Web/Servlet入门案例执行.png) @@ -2650,19 +2685,21 @@ Servlet执行过程分析: +*** + #### 实现方式 -实现Servlet功能时,可以选择以下三种方式: +实现 Servlet 功能时,可以选择以下三种方式: -* 第一种:实现Servlet接口,接口中的方法必须全部实现。 +* 第一种:实现 Servlet 接口,接口中的方法必须全部实现。 使用此种方式,表示接口中的所有方法在需求方面都有重写的必要。此种方式支持最大程度的自定义。 -* 第二种:继承GenericServlet,service方法必须重写,其他方可根据需求,选择性重写。 - 使用此种方式,表示只在接收和响应客户端请求这方面有重写的需求,而其他方法可根据实际需求选择性重写,使我们的开发Servlet变得简单。但是,此种方式是和HTTP协议无关的。 +* 第二种:继承 GenericServlet,service 方法必须重写,其他方可根据需求,选择性重写。 + 使用此种方式,表示只在接收和响应客户端请求这方面有重写的需求,而其他方法可根据实际需求选择性重写,使我们的开发Servlet变得简单。但是,此种方式是和 HTTP 协议无关的。 -* 第三种:继承HttpServlet,它是javax.servlet.http包下的一个抽象类,是GenericServlet的子类。选择继承HttpServlet时,**需要重写doGet和doPost方法**,来接收get方式和post方式的请求,不要覆盖service方法。使用此种方式,表示我们的请求和响应需要和HTTP协议相关,我们是通过HTTP协议来访问。每次请求和响应都符合HTTP协议的规范。请求的方式就是HTTP协议所支持的方式(GET POST PUT DELETE TRACE OPTIONS HEAD )。 +* 第三种:继承 HttpServlet,它是 javax.servlet.http 包下的一个抽象类,是 GenericServlet 的子类。选择继承 HttpServlet 时,**需要重写 doGet 和 doPost 方法**,来接收 get 方式和 post 方式的请求,不要覆盖 service 方法。使用此种方式,表示我们的请求和响应需要和 HTTP 协议相关,我们是通过 HTTP 协议来访问。每次请求和响应都符合 HTTP 协议的规范。请求的方式就是 HTTP 协议所支持的方式(GET POST PUT DELETE TRACE OPTIONS HEAD )。 @@ -2690,21 +2727,21 @@ Servlet 3.0 中的异步处理指的是允许Servlet重新发起一条新线程 servlet从创建到销毁的过程: -* 出生:(初始化)请求第一次到达Servlet时,创建对象,并且初始化成功。Only one time +* 出生:(初始化)请求第一次到达 Servlet 时,创建对象,并且初始化成功。Only one time -* 活着:(服务)服务器提供服务的整个过程中,该对象一直存在,每次只是执行service方法 +* 活着:(服务)服务器提供服务的整个过程中,该对象一直存在,每次只是执行 service 方法 * 死亡:(销毁)当服务停止时,或者服务器宕机时,对象删除, serrvlet生命周期方法: -`init(ServletConfig config)`-->`service(ServletRequest req, ServletResponse res)`-->`destroy()` +`init(ServletConfig config)` → `service(ServletRequest req, ServletResponse res)` → `destroy()` -默认情况下, 有了第一次请求, 会调用init()方法进行初始化【调用一次】,任何一次请求,都会调用service()方法处理这个请求,服务器正常关闭或者项目从服务器移除, 调用destory()方法进行销毁【调用一次】 +默认情况下, 有了第一次请求, 会调用 init() 方法进行初始化【调用一次】,任何一次请求,都会调用 service() 方法处理这个请求,服务器正常关闭或者项目从服务器移除, 调用 destory() 方法进行销毁【调用一次】 -**扩展**:servlet是单例多线程的,尽量不要在servlet里面使用全局(成员)变量,可能会导致线程不安全 +**扩展**:servlet 是单例多线程的,尽量不要在 servlet 里面使用全局(成员)变量,可能会导致线程不安全 -* 单例:Servlet对象只会创建一次,销毁一次,Servlet对象只有一个实例。 -* 多线程:服务器会针对每次请求, 开启一个线程调用service()方法处理这个请求 +* 单例:Servlet 对象只会创建一次,销毁一次,Servlet 对象只有一个实例。 +* 多线程:服务器会针对每次请求, 开启一个线程调用 service() 方法处理这个请求 @@ -3529,18 +3566,20 @@ request域:可以在一次请求范围内进行共享数据 #### 请求转发 -请求转发:客户端的一次请求到达后,需要借助其他Servlet来实现功能,进行请求转发。特点: +请求转发:客户端的一次请求到达后,需要借助其他 Servlet 来实现功能,进行请求转发。特点: * 浏览器地址栏不变 * 域对象中的数据不丢失 -* 负责转发的Servlet转发前后响应正文会丢失 +* 负责转发的 Servlet 转发前后响应正文会丢失 * 由转发目的地来响应客户端 -HttpServletRequest类方法: - `RequestDispatcher getRequestDispatcher(String path) ` : 获取任务调度对象 +HttpServletRequest 类方法: -RequestDispatcher类方法: - `void forward(ServletRequest request, ServletResponse response)` : 实现转发。将请求从servlet转发到服务器上的另一个资源(servlet,JSP文件或HTML文件) +* `RequestDispatcher getRequestDispatcher(String path) ` : 获取任务调度对象 + +RequestDispatcher 类方法: + +* `void forward(ServletRequest request, ServletResponse response)` : 实现转发。将请求从servlet转发到服务器上的另一个资源(servlet,JSP文件或HTML文件) 过程:浏览器访问http://localhost:8080/request/servletDemo09,/servletDemo10也会执行 @@ -3991,7 +4030,7 @@ public class ServletDemo06 extends HttpServlet { -#### 请求重定向 +#### 重定向 ##### 实现重定向 @@ -4051,6 +4090,8 @@ public class ServletDemo08 extends HttpServlet { +*** + ##### 重定向和转发 @@ -4133,7 +4174,7 @@ public class ServletDemo08 extends HttpServlet { 浏览器和服务器可能产生多次的请求和响应,从浏览器访问服务器开始,到访问服务器结束(关闭浏览器、到了过期时间),这期间产生的多次请求和响应加在一起称为浏览器和服务器之间的一次对话 -**作用**:保存用户各自的数据(以浏览器为单位),在多次请求间实现数据共享 +作用:保存用户各自的数据(以浏览器为单位),在多次请求间实现数据共享 **常用的会话管理技术**: @@ -4194,19 +4235,21 @@ Cookie:客户端会话管理技术,把要共享的数据保存到了客户 | version | cookie的版本号 | 不重要 | | comment | cookie的说明 | 不重要 | - 注意:Cookie有大小,个数限制。每个网站最多只能存20个cookie,且大小不能超过4kb。同时,所有网站的cookie总数不超过300个。 + 注意:Cookie 有大小,个数限制。每个网站最多只能存20个 Cookie,且大小不能超过 4kb。同时所有网站的 Cookie 总数不超过300个。 * **Cookie类API:** - * `Cookie(String name, String value)` : 构造方法创建Cookie对象 - - * Cookie 属性对应的set和get方法,name属性被final修饰,没有set方法 + * `Cookie(String name, String value)` : 构造方法创建 Cookie 对象 -* HttpServletResponse类API: - `void addCookie(Cookie cookie)` : 向客户端添加Cookie,Adds the specified cookie to the response. + * Cookie 属性对应的 set 和 get 方法,name 属性被 final 修饰,没有 set 方法 +* HttpServletResponse 类 API: + + * `void addCookie(Cookie cookie)`:向客户端添加 Cookie,Adds cookie to the response + * HttpServletRequest类API: - `Cookie[] getCookies()` : 获取所有的Cookie对象,client sent with this request + + * `Cookie[] getCookies()`:获取所有的 Cookie 对象,client sent with this request @@ -4216,14 +4259,14 @@ Cookie:客户端会话管理技术,把要共享的数据保存到了客户 #### 有效期 -如果不设置过期时间,表示这个cookie生命周期为浏览器会话期间,只要关闭浏览器窗口cookie就消失,这种生命期为浏览会话期的cookie被称为会话cookie,会话cookie一般不保存在硬盘上而是保存在内存里。 +如果不设置过期时间,表示这个 Cookie 生命周期为浏览器会话期间,只要关闭浏览器窗口 Cookie 就消失,这种生命期为浏览会话期的 Cookie 被称为会话 Cookie,会话 Cookie 一般不保存在硬盘上而是保存在内存里。 -如果设置过期时间,浏览器就会把cookie保存到硬盘上,关闭后再次打开浏览器,这些cookie依然有效直到超过设定的过期时间。存储在硬盘上的cookie可以在**不同的浏览器进程间共享**,比如两个IE窗口,而对于保存在内存的cookie,不同的浏览器有不同的处理方式 +如果设置过期时间,浏览器就会把 Cookie 保存到硬盘上,关闭后再次打开浏览器,这些 Cookie 依然有效直到超过设定的过期时间。存储在硬盘上的 Cookie 可以在**不同的浏览器进程间共享**,比如两个 IE 窗口,而对于保存在内存的 Cookie,不同的浏览器有不同的处理方式 -设置Cookie存活时间API:`void setMaxAge(int expiry)` +设置 Cookie 存活时间 API:`void setMaxAge(int expiry)` -* -1:默认。代表Cookie数据存到浏览器关闭(保存在浏览器文件中) -* 0:代表删除Cookie,如果要删除Cookie要确保**路径一致**。 +* -1:默认。代表 Cookie 数据存到浏览器关闭(保存在浏览器文件中) +* 0:代表删除 Cookie,如果要删除 Cookie 要确保**路径一致**。 * 正整数:以秒为单位保存数据有有效时间(把缓存数据保存到磁盘中) ```java @@ -4274,15 +4317,15 @@ public class ServletDemo01 extends HttpServlet{ #### 有效路径 -`setPath(String url)` : Cookie设置有效路径 +`setPath(String url)` : Cookie 设置有效路径 有效路径作用 : -1. 保证不会携带别的网站/项目里面的cookie到我们自己的项目 -2. 路径不一样, cookie的key可以相同 -3. 保证自己的项目可以合理的利用自己项目的cookie +1. 保证不会携带别的网站/项目里面的 Cookie 到我们自己的项目 +2. 路径不一样,Cookie 的 key 可以相同 +3. 保证自己的项目可以合理的利用自己项目的 Cookie -判断路径是否携带 Cookie:请求资源URI.startWith(cookie的path),返回true就带 +判断路径是否携带 Cookie:请求资源 URI.startWith(cookie的path),返回 true 就带 | 访问URL | URI部分 | Cookie的Path | 是否携带Cookie | 能否取到Cookie | | ------------------------------------------------------------ | -------------------------- | ------------ | -------------- | -------------- | @@ -4291,8 +4334,9 @@ public class ServletDemo01 extends HttpServlet{ | [servletDemo04](http://localhost:8080/servlet/aaa/servletDemo03) | /servlet/aaa/servletDemo04 | /servlet/ | 带 | 能取到 | | [servletDemo05](http://localhost:8080/bbb/servletDemo03) | /bbb/servletDemo04 | /servlet/ | 不带 | 不能取到 | -只有当访问资源的url包含此cookie的有效path的时候,才会携带这个cookie。 -想要当前项目下的Servlet都可以使用该cookie,一般设置: `cookie.setPath(request.getContextPath());` +只有当访问资源的 url 包含此 cookie 的有效 path 的时候,才会携带这个 cookie + +想要当前项目下的 Servlet 可以使用该 cookie,一般设置:`cookie.setPath(request.getContextPath())` From c75407291d88117498d6e9e6de047966e7a4fe17 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 1 Aug 2021 23:41:23 +0800 Subject: [PATCH 008/168] Update Java Notes --- Java.md | 30 +- SSM.md | 1626 ++++++++++++++++++++++++++++++++++++++++++++++++------- Web.md | 106 ++-- 3 files changed, 1490 insertions(+), 272 deletions(-) diff --git a/Java.md b/Java.md index da17b50..2892701 100644 --- a/Java.md +++ b/Java.md @@ -3990,7 +3990,7 @@ public class ArrayList extends AbstractList } ``` - 指定索引插入,在旧数组上操作: + 指定索引插入,**在旧数组上操作**: ```java public void add(int index, E element) { @@ -4663,11 +4663,11 @@ HashMap继承关系如下图所示: HashMap(int initialCapacity)//构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap ``` - * 为什么必须是2的n次幂? + * 为什么必须是 2 的 n 次幂? - 向HashMap中添加元素时,需要根据key的hash值,确定在数组中的具体位置。HashMap为了存取高效,要尽量较少碰撞,把数据尽可能分配均匀,每个链表长度大致相同,实现该方法的算法就是取模,hash%length,计算机中直接求余效率不如位移运算,所以源码中使用 hash&(length-1),实际上**hash % length == hash & (length-1)的前提是length是2的n次幂** + HashMap 中添加元素时,需要根据 key 的 hash 值,确定在数组中的具体位置。HashMap 为了存取高效,要尽量较少碰撞,把数据尽可能分配均匀,每个链表长度大致相同,实现该方法的算法就是取模,hash%length,计算机中直接求余效率不如位移运算,所以源码中使用 hash&(length-1),实际上**hash % length == hash & (length-1)的前提是 length 是 2 的n次幂** - 散列平均分布:2的n次方是1后面n个0,2的n次方-1 是n个1,可以**保证散列的均匀性**,减少碰撞 + 散列平均分布:2 的 n 次方是 1 后面 n 个 0,2 的 n 次方 -1 是 n 个 1,可以**保证散列的均匀性**,减少碰撞 ```java 例如长度为8时候,3&(8-1)=3 2&(8-1)=2 ,不同位置上,不碰撞; @@ -4678,7 +4678,7 @@ HashMap继承关系如下图所示: 创建HashMap对象时,HashMap通过位移运算和或运算得到的肯定是2的幂次数,并且是大于那个数的最近的数字,底层采用tableSizeFor()方法 -3. 默认的负载因子,默认值是0.75 +3. 默认的负载因子,默认值是 0.75 ```java static final float DEFAULT_LOAD_FACTOR = 0.75f; @@ -4691,10 +4691,10 @@ HashMap继承关系如下图所示: static final int MAXIMUM_CAPACITY = 1 << 30; ``` - 最大容量为什么是2的30次方原因: + 最大容量为什么是 2 的 30 次方原因: - * int类型是32位整型,占4个字节 - * Java的原始类型里没有无符号类型,所以首位是符号位正数为0,负数为1 + * int 类型是 32 位整型,占 4 个字节 + * Java 的原始类型里没有无符号类型,所以首位是符号位正数为 0,负数为 1 5. 当链表的值超过8则会转红黑树(1.8新增**) @@ -4703,9 +4703,9 @@ HashMap继承关系如下图所示: static final int TREEIFY_THRESHOLD = 8; ``` - 为什么Map桶中节点个数大于8才转为红黑树? + 为什么 Map 桶中节点个数大于8才转为红黑树? - * 在HashMap中有一段注释说明:**空间和时间的权衡** + * 在 HashMap 中有一段注释说明:**空间和时间的权衡** ```java TreeNodes占用空间大约是普通节点的两倍,所以我们只在箱子包含足够的节点时才使用树节点。当节点变少(由于删除或调整大小)时,就会被转换回普通的桶。在使用分布良好的用户hashcode时,很少使用树箱。理想情况下,在随机哈希码下,箱子中节点的频率服从"泊松分布",默认调整阈值为0.75,平均参数约为0.5,尽管由于调整粒度的差异很大。忽略方差,列表大小k的预期出现次数是(exp(-0.5)*pow(0.5, k)/factorial(k)) @@ -4723,18 +4723,16 @@ HashMap继承关系如下图所示: ``` * 其他说法 - 红黑树的平均查找长度是log(n),如果长度为8,平均查找长度为log(8)=3,链表的平均查找长度为n/2,当长度为8时,平均查找长度为8/2=4,这才有转换成树的必要;链表长度如果是小于等于6,6/2=3,而log(6)=2.6,虽然速度也很快的,但转化为树结构和生成树的时间并不短 + 红黑树的平均查找长度是 log(n),如果长度为 8,平均查找长度为 log(8)=3,链表的平均查找长度为 n/2,当长度为 8 时,平均查找长度为 8/2=4,这才有转换成树的必要;链表长度如果是小于等于 6,6/2=3,而 log(6)=2.6,虽然速度也很快的,但转化为树结构和生成树的时间并不短 - - -6. 当链表的值小于6则会从红黑树转回链表 +6. 当链表的值小 于6 则会从红黑树转回链表 ```java //当桶(bucket)上的结点数小于这个值时树转链表 static final int UNTREEIFY_THRESHOLD = 6; ``` -7. 当Map里面的数量**大于等于**这个阈值时,表中的桶才能进行树形化 ,否则桶内元素太多时会扩容,而不是树形化。为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD (8) +7. 当 Map 里面的数量**大于等于**这个阈值时,表中的桶才能进行树形化 ,否则桶内元素太多时会扩容,而不是树形化。为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD (8) ```java //桶中结构转化为红黑树对应的数组长度最小的值 @@ -12373,7 +12371,7 @@ Java 语言:跨平台的语言(write once ,run anywhere) #### 语言发展 -机器码:各种用二进制编码方式表示的指令,与CPU紧密相关,所以不同种类的CPU对应的机器指令不同 +机器码:各种用二进制编码方式表示的指令,与 CPU 紧密相关,所以不同种类的 CPU 对应的机器指令不同 指令:指令就是把机器码中特定的0和1序列,简化成对应的指令,例如mov,inc等,可读性稍好,但是不同的硬件平台的同一种指令(比如mov),对应的机器码也可能不同 diff --git a/SSM.md b/SSM.md index 22e2f41..a2a6c00 100644 --- a/SSM.md +++ b/SSM.md @@ -4015,7 +4015,7 @@ public class MainTest { 类型:类注解,写在类定义上方 -作用:设置该类为spring管理的bean +作用:设置该类为Spring 管理的 bean 格式: @@ -4024,7 +4024,7 @@ public class MainTest { public class ClassName{} ``` -说明:@Controller、@Service 、@Repository是@Component的衍生注解,功能同@Component +说明:@Controller、@Service 、@Repository 是 @Component 的衍生注解,功能同 @Component 属性: @@ -8986,30 +8986,38 @@ MVC(Model View Controller),一种用于设计创建Web应用程序表现 + + *** + + ## 技术架构 ### 组件介绍 -* DispatcherServlet:核心控制器, 是SpringMVC的核心,整体流程控制的中心,所有的请求第一步都先到达这里,由其调用其它组件处理用户的请求,它就是在web.xml配置的核心Servlet,有效的降低了组件间的耦合性 +* DispatcherServlet:核心控制器, 是 SpringMVC 的核心,整体流程控制的中心,所有的请求第一步都先到达这里,由其调用其它组件处理用户的请求,它就是在 web.xml 配置的核心 Servlet,有效的降低了组件间的耦合性 -* HandlerMapping:处理器映射器, 负责根据用户请求找到对应具体的Handler处理器,SpringMVC中针对配置文件方式、注解方式等提供了不同的映射器来处理 +* HandlerMapping:处理器映射器, 负责根据用户请求找到对应具体的 Handler 处理器,SpringMVC 中针对配置文件方式、注解方式等提供了不同的映射器来处理 -* Handler:处理器,其实就是Controller,业务处理的核心类,通常由开发者编写,并且必须遵守Controller开发的规则,这样适配器才能正确的执行。例如实现Controller接口,将Controller注册到IOC容器中等 +* Handler:处理器,其实就是 Controller,业务处理的核心类,通常由开发者编写,并且必须遵守 Controller 开发的规则,这样适配器才能正确的执行。例如实现 Controller 接口,将 Controller 注册到 IOC 容器中等 -* HandlAdapter:处理器适配器,根据映射器中找到的Handler,通过HandlerAdapter去执行Handler,这是适配器模式的应用 +* HandlAdapter:处理器适配器,根据映射器中找到的 Handler,通过 HandlerAdapter 去执行 Handler,这是适配器模式的应用 -* View Resolver:视图解析器, 将Handler中返回的逻辑视图(ModelAndView)解析为一个具体的视图(View)对象 +* View Resolver:视图解析器, 将 Handler 中返回的逻辑视图(ModelAndView)解析为一个具体的视图(View)对象 -* View:视图, View最后对页面进行渲染将结果返回给用户。springmvc框架提供了很多的View视图类型,包括:jstlView、freemarkerView、pdfView等 +* View:视图, View 最后对页面进行渲染将结果返回给用户。Springmvc 框架提供了很多的 View 视图类型,包括:jstlView、freemarkerView、pdfView 等 ![](https://gitee.com/seazean/images/raw/master/Frame/SpingMVC技术架构.png) +**** + + + ### 工作原理 在 Spring 容器初始化时会建立所有的 URL 和 Controller 的对应关系,保存到 Map 中,这样 request 就能快速根据 URL 定位到 Controller。实现: @@ -9039,6 +9047,8 @@ MVC(Model View Controller),一种用于设计创建Web应用程序表现 + + ## 基本配置 ### 入门项目 @@ -9122,264 +9132,1462 @@ MVC(Model View Controller),一种用于设计创建Web应用程序表现 * 设定具体 Controller,控制层 java / controller / UserController - ```java - @Controller //@Component衍生注解 - public class UserController { - //设定当前方法的访问映射地址,等同于Servlet在web.xml中的配置 - @RequestMapping("/save") - //设置当前方法返回值类型为String,用于指定请求完成后跳转的页面 - public String save(){ - System.out.println("user mvc controller is running ..."); - //设定具体跳转的页面 - return "success.jsp"; - } - } - ``` + ```java + @Controller //@Component衍生注解 + public class UserController { + //设定当前方法的访问映射地址,等同于Servlet在web.xml中的配置 + @RequestMapping("/save") + //设置当前方法返回值类型为String,用于指定请求完成后跳转的页面 + public String save(){ + System.out.println("user mvc controller is running ..."); + //设定具体跳转的页面 + return "success.jsp"; + } + } + ``` + +* webapp / WEB-INF / web.xml,配置SpringMVC核心控制器,请求转发到对应的具体业务处理器Controller中(等同于Servlet配置) + + ```xml + + + + + DispatcherServlet + org.springframework.web.servlet.DispatcherServlet + + + contextConfigLocation + classpath*:spring-mvc.xml + + + + DispatcherServlet + / + + + ``` + +* resouces / spring-mvc.xml + + ```xml + + + + + + ``` + + + + +*** + + + +### 加载控制 + +Controller 加载控制:SpringMVC 的处理器对应的 bean 必须按照规范格式开发,未避免加入无效的 bean 可通过 bean 加载过滤器进行包含设定或排除设定,表现层 bean 标注通常设定为 @Controller + +* resources / spring-mvc.xml 配置 + + ```xml + + + + ``` + +* 静态资源加载(webapp 目录下的相关资源),spring-mvc.xml 配置,开启 mvc 命名空间 + + ```xml + + + + + + + + ``` + +* 中文乱码处理 SpringMVC 提供专用的中文字符过滤器,用于处理乱码问题。配置在 web.xml 里面 + + ```xml + + + CharacterEncodingFilter + org.springframework.web.filter.CharacterEncodingFilter + + encoding + UTF-8 + + + + CharacterEncodingFilter + /* + + ``` + + + +*** + + + +### 注解驱动 + +纯注解开发: + +* 使用注解形式转化 SpringMVC 核心配置文件为配置类 java / config / SpringMVCConfiguration.java + + ```java + @Configuration + @ComponentScan(value = "com.seazean", includeFilters = @ComponentScan.Filter( + type=FilterType.ANNOTATION, + classes = {Controller.class} ) + ) + public class SpringMVCConfiguration implements WebMvcConfigurer{ + //注解配置放行指定资源格式 + // @Override + // public void addResourceHandlers(ResourceHandlerRegistry registry) { + // registry.addResourceHandler("/img/**").addResourceLocations("/img/"); + // } + //注解配置通用放行资源的格式 建议使用 + @Override + public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { + configurer.enable(); + } + } + ``` + +* 基于 servlet3.0 规范,自定义 Servlet 容器初始化配置类,加载 SpringMVC 核心配置类 + + ```java + //创建Servlet容器时,使用注解方式加载SPRINGMVC配置类中的信息, + //并加载成WEB专用的ApplicationContext对象该对象放入了ServletContext范围, + //在整个WEB容器中可以随时获取调用 + public class ServletContainersInitConfig extends AbstractDispatcherServletInitializer { + @Override + protected WebApplicationContext createServletApplicationContext() { + A.C.W.A ctx = new AnnotationConfigWebApplicationContext(); + ctx.register(SpringMVCConfiguration.class); + return ctx; + } + + //注解配置映射地址方式,服务于SpringMVC的核心控制器DispatcherServlet + @Override + protected String[] getServletMappings() { + return new String[]{"/"}; + } + + @Override + protected WebApplicationContext createRootApplicationContext() { + return null; + } + + //乱码处理作为过滤器,在servlet容器启动时进行配置 + @Override + public void onStartup(ServletContext servletContext) throws ServletException { + super.onStartup(servletContext); + CharacterEncodingFilter cef = new CharacterEncodingFilter(); + cef.setEncoding("UTF-8"); + FilterRegistration.Dynamic registration = servletContext.addFilter("characterEncodingFilter", cef); + registration.addMappingForUrlPatterns(EnumSet.of( + DispatcherType.REQUEST, + DispatcherType.FORWARD, + DispatcherType.INCLUDE), false,"/*"); + } + } + ``` + + + +*** + + + +### 请求映射 + +名称:@RequestMapping + +类型:方法注解、类注解 + +位置:处理器类中的方法定义上方、处理器类定义上方 + +* 方法注解 + + 作用:绑定请求地址与对应处理方法间的关系 + + 无类映射地址访问格式: http://localhost/requestURL2 + + ```java + @Controller + public class UserController { + @RequestMapping("/requestURL2") + public String requestURL2() { + return "page.jsp"; + } + } + ``` + +* 类注解 + + 作用:为当前处理器中所有方法设定公共的访问路径前缀 + + 带有类映射地址访问格式,将类映射地址作为前缀添加在实际映射地址前面:**/user/requestURL1** + + 最终返回的页面如果未设定绝对访问路径,将从类映射地址所在目录中查找 **webapp/user/page.jsp** + + ```java + @Controller + @RequestMapping("/user") + public class UserController { + @RequestMapping("/requestURL2") + public String requestURL2() { + return "page.jsp"; + } + } + ``` + +* 常用属性 + + ```java + @RequestMapping( + value="/requestURL3", //设定请求路径,与path属性、 value属性相同 + method = RequestMethod.GET, //设定请求方式 + params = "name", //设定请求参数条件 + headers = "content-type=text/*", //设定请求消息头条件 + consumes = "text/*", //用于指定可以接收的请求正文类型(MIME类型) + produces = "text/*" //用于指定可以生成的响应正文类型(MIME类型) + ) + public String requestURL3() { + return "/page.jsp"; + } + ``` + + + +*** + + + +## 基本操作 + +### 请求处理 + +#### 普通类型 + +SpringMVC 将传递的参数封装到处理器方法的形参中,达到快速访问参数的目的 + +* 访问 URL:http://localhost/requestParam1?name=seazean&age=14 + + ```java + @Controller + public class UserController { + @RequestMapping("/requestParam1") + public String requestParam1(String name ,int age){ + System.out.println("name=" + name + ",age=" + age); + return "page.jsp"; + } + } + ``` + + ```jsp + <%@page pageEncoding="UTF-8" language="java" contentType="text/html;UTF-8" %> + + +

请求参数测试页面

+ + + ``` + +@RequestParam 的使用: + +* 类型:形参注解 + +* 位置:处理器类中的方法形参前方 + +* 作用:绑定请求参数与对应处理方法形参间的关系 + +* 访问 URL:http://localhost/requestParam2?userName=Jock + + ```java + @RequestMapping("/requestParam2") + public String requestParam2(@RequestParam( + name = "userName", + required = true, //为true代表必须有参数 + defaultValue = "s") String name){ + System.out.println("name=" + name); + return "page.jsp"; + } + ``` + + + +*** + + + +#### POJO类型 + +##### 简单类型 + +当 POJO 中使用简单类型属性时, 参数名称与 POJO 类属性名保持一致 + +* 访问 URL: http://localhost/requestParam3?name=seazean&age=14 + + ```java + @RequestMapping("/requestParam3") + public String requestParam3(User user){ + System.out.println("name=" + user.getName()); + return "page.jsp"; + } + ``` + + ```java + public class User { + private String name; + private Integer age; + //...... + } + ``` + + + +**** + + + +##### 参数冲突 + +当 POJO 类型属性与其他形参出现同名问题时,将被**同时赋值**,建议使用 @RequestParam 注解进行区分 + +* 访问 URL: http://localhost/requestParam4?name=seazean&age=14 + + ```java + @RequestMapping("/requestParam4") + public String requestParam4(User user, String age){ + System.out.println("user.age=" + user.getAge() + ",age=" + age);//14 14 + return "page.jsp"; + } + ``` + + + +*** + + + +##### 复杂类型 + +当 POJO 中出现对象属性时,参数名称与对象层次结构名称保持一致 + +* 访问 URL: http://localhost/requestParam5?address.province=beijing + + ```java + @RequestMapping("/requestParam5") + public String requestParam5(User user){ + System.out.println("user.address=" + user.getAddress().getProvince()); + return "page.jsp"; + } + ``` + + ```java + public class User { + private String name; + private Integer age; + private Address address; //.... + } + ``` + + ```java + public class Address { + private String province; + private String city; + private String address; + } + ``` + + + + +*** + + + +##### 容器类型 + +POJO 中出现集合类型的处理方式 + +* 通过 URL 地址中同名参数,可以为 POJO 中的集合属性进行赋值,集合属性要求保存简单数据 + + 访问 URL:http://localhost/requestParam6?nick=Jock1&nick=Jockme&nick=zahc + + ```java + @RequestMapping("/requestParam6") + public String requestParam6(User user){ + System.out.println("user=" + user); + //user = User{name='null',age=null,nick={Jock1,Jockme,zahc}} + return "page.jsp"; + } + ``` + + ```java + public class User { + private String name; + private Integer age; + private List nick; + } + ``` + +* POJO 中出现 List 保存对象数据,参数名称与对象层次结构名称保持一致,使用数组格式描述集合中对象的位置访问 URL:http://localhost/requestParam7?addresses[0].province=bj&addresses[1].province=tj + + ```java + @RequestMapping("/requestParam7") + public String requestParam7(User user){ + System.out.println("user.addresses=" + user.getAddress()); + //{Address{provice=bj,city='null',address='null'}},{Address{....}} + return "page.jsp"; + } + ``` + + ```java + public class User { + private String name; + private Integer age; + private List
addresses; + } + ``` + +* POJO 中出现 Map 保存对象数据,参数名称与对象层次结构名称保持一致,使用映射格式描述集合中对象位置 + + URL: http://localhost/requestParam8?addressMap[’home’].province=bj&addressMap[’job’].province=tj + + ```java + @RequestMapping("/requestParam8") + public String requestParam8(User user){ + System.out.println("user.addressMap=" + user.getAddressMap()); + //user.addressMap={home=Address{p=,c=,a=},job=Address{....}} + return "page.jsp"; + } + ``` + + ```java + public class User { + private Map addressMap; + //.... + } + ``` + + + + +*** + + + +#### 数组集合 + +##### 数组类型 + +请求参数名与处理器方法形参名保持一致,且请求参数数量> 1个 + +* 访问 URL: http://localhost/requestParam9?nick=Jockme&nick=zahc + + ```java + @RequestMapping("/requestParam9") + public String requestParam9(String[] nick){ + System.out.println(nick[0] + "," + nick[1]); + return "page.jsp"; + } + ``` + + + +*** + + + +##### 集合类型 + +保存简单类型数据,请求参数名与处理器方法形参名保持一致,且请求参数数量> 1个 + +* 访问 URL: http://localhost/requestParam10?nick=Jockme&nick=zahc + + ```java + @RequestMapping("/requestParam10") + public String requestParam10(@RequestParam("nick") List nick){ + System.out.println(nick); + return "page.jsp"; + } + ``` + +* 注意: SpringMVC 默认将 List 作为对象处理,赋值前先创建对象,然后将 nick **作为对象的属性**进行处理。List 是接口无法创建对象,报无法找到构造方法异常;修复类型为可创建对象的 ArrayList 类型后,对象可以创建但没有 nick 属性,因此数据为空 + 解决方法:需要告知 SpringMVC 的处理器 nick 是一组数据,而不是一个单一属性。通过 @RequestParam 注解,将数量大于 1 个 names 参数打包成参数数组后, SpringMVC 才能识别该数据格式,并判定形参类型是否为数组或集合,并按数组或集合对象的形式操作数据 + + + +*** + + + +#### 转换器 + +##### 类型 + +开启转换配置:` ` +作用:提供 Controller 请求转发,Json 自动转换等功能 + +如果访问 URL:http://localhost/requestParam1?name=seazean&age=seazean,会出现报错,类型转化异常 + +```java +@RequestMapping("/requestParam1") +public String requestParam1(String name ,int age){ + System.out.println("name=" + name + ",age=" + age); + return "page.jsp"; +} +``` + +SpringMVC 对接收的数据进行自动类型转换,该工作通过 Converter 接口实现: + +* **标量转换器** + StringToBooleanConverter String → Boolean + ObjectToStringConverter Object → String + StringToNumberConverterFactory String → Number( Integer、 Long 等) + NumberToNumberConverterFactory Number子类型之间(Integer、 Long、 Double 等) + StringToCharacterConverter String → java.lang.Character + NumberToCharacterConverter Number子类型(Integer、 Long、 Double 等)→ java.lang.Character + CharacterToNumberFactory java.lang.Character → Number子类型(Integer、Long、Double 等) + StringToEnumConverterFactory String → enum类型 + EnumToStringConverter enum类型 → String + StringToLocaleConverter String → java.util.Local + PropertiesToStringConverter java.util.Properties → String + StringToPropertiesConverter String → java.util.Properties + +* **集合、数组相关转换器** + ArrayToCollectionConverter 数组 → 集合( List、 Set) + CollectionToArrayConverter 集合( List、 Set) →数组 + ArrayToArrayConverter 数组间 + CollectionToCollectionConverter 集合间( List、 Set) + MapToMapConverter Map间 + ArrayToStringConverter 数组→String类型 + StringToArrayConverter String →数组, trim后使用“,”split + ArrayToObjectConverter 数组 → Object + ObjectToArrayConverter Object → 单元素数组 + CollectionToStringConverter 集合( List、 Set) →String + StringToCollectionConverter String → 集合( List、 Set), trim后使用“,”split + CollectionToObjectConverter 集合 → Object + ObjectToCollectionConverter Object → 单元素集合 +* **默认转换器** + ObjectToObjectConverter Object间 + IdToEntityConverter Id → Entity + FallbackObjectToStringConverter Object → String + + + +*** + + + +##### 日期 + +![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-date数据类型转换.png) + +如果访问 URL:http://localhost/requestParam11?date=1999-09-09 会报错,所以需要日期类型转换 + +* 声明自定义的转换格式并覆盖系统转换格式,配置 resources / spring-mvc.xml + + ```xml + + + + + + + + + + + + + + + + + ``` + +* @DateTimeFormat + 类型:形参注解、成员变量注解 + 位置:形参前面 或 成员变量上方 + 作用:为当前参数或变量指定类型转换规则 + + ```java + public String requestParam12(@DateTimeFormat(pattern = "yyyy-MM-dd") Date date){ + System.out.println("date=" + date); + return "page.jsp"; + } + ``` + + ```java + @DateTimeFormat(pattern = "yyyy-MM-dd") + private Date date; + ``` + + 依赖注解驱动支持,xml 开启配置: + + ```xml + + ``` + + + +*** + + + +##### 自定义 + +* 自定义类型转换器,实现 Converter 接口,并制定转换前与转换后的类型 + + ```java + //自定义类型转换器,实现Converter接口,接口中指定的泛型即为最终作用的条件 + //本例中的泛型填写的是String,Date,最终出现字符串转日期时,该类型转换器生效 + public class MyDateConverter implements Converter { + //重写接口的抽象方法,参数由泛型决定 + public Date convert(String source) { + DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); + Date date = null; + //类型转换器无法预计使用过程中出现的异常,因此必须在类型转换器内部捕获, + //不允许抛出,框架无法预计此类异常如何处理 + try { + date = df.parse(source); + } catch (ParseException e) { + e.printStackTrace(); + } + return date; + } + } + ``` + +* 配置 resources / spring-mvc.xml,注册自定义转换器,将功能加入到 SpringMVC 转换服务 ConverterService 中 + + ```xml + + + + + + + + + + + + + + + + + ``` + +* 使用转换器 + + ```java + @RequestMapping("/requestParam12") + public String requestParam12(Date date){ + System.out.println(date); + return "page.jsp"; + } + ``` + + + + + +*** + + + +### 响应处理 + +#### 页面跳转 + +请求转发和重定向: + +* 请求转发: + + ```java + @Controller + public class UserController { + @RequestMapping("/showPage1") + public String showPage1() { + System.out.println("user mvc controller is running ..."); + return "forward:/WEB-INF/page/page.jsp; + } + } + ``` + +* 请求重定向: + + ```java + @RequestMapping("/showPage2") + public String showPage2() { + System.out.println("user mvc controller is running ..."); + return "redirect:/WEB-INF/page/page.jsp";//不能访问WEB-INF下的资源 + } + ``` + + +页面访问快捷设定(InternalResourceViewResolver): + +* 展示页面的保存位置通常固定且结构相似,可以设定通用的访问路径简化页面配置,配置 spring-mvc.xml: + + ```xml + + + + + ``` + +* 简化 + + ```java + @RequestMapping("/showPage3") + public String showPage3() { + System.out.println("user mvc controller is running..."); + return "page"; + } + @RequestMapping("/showPage4") + public String showPage4() { + System.out.println("user mvc controller is running..."); + return "forward:page"; + } + + @RequestMapping("/showPage5") + public String showPage5() { + System.out.println("user mvc controller is running..."); + return "redirect:page"; + } + ``` + +* 如果未设定了返回值,使用 void 类型,则默认使用访问路径作页面地址的前缀后缀 + + ```java + //最简页面配置方式,使用访问路径作为页面名称,省略返回值 + @RequestMapping("/showPage6") + public void showPage6() { + System.out.println("user mvc controller is running ..."); + } + ``` + + + +*** + + + +#### 带数据跳转 + +ModelAndView 是 SpringMVC 提供的一个对象,该对象可以用作控制器方法的返回值(Model 同) +作用: + ++ 设置数据,向请求域对象中存储数据 ++ 设置视图,逻辑视图 + +代码实现: + +* 使用 HttpServletRequest 类型形参进行数据传递 + + ```java + @Controller + public class BookController { + @RequestMapping("/showPageAndData1") + public String showPageAndData1(HttpServletRequest request) { + request.setAttribute("name","seazean"); + return "page"; + } + } + ``` + +* 使用 Model 类型形参进行数据传递 + + ```java + @RequestMapping("/showPageAndData2") + public String showPageAndData2(Model model) { + model.addAttribute("name","seazean"); + Book book = new Book(); + book.setName("SpringMVC入门实战"); + book.setPrice(66.6d); + //添加数据的方式,key对value + model.addAttribute("book",book); + return "page"; + } + ``` + + ```java + public class Book { + private String name; + private Double price; + } + ``` + +* 使用 ModelAndView 类型形参进行数据传递,将该对象作为返回值传递给调用者 + + ```java + @RequestMapping("/showPageAndData3") + public ModelAndView showPageAndData3(ModelAndView modelAndView) { + //ModelAndView mav = new ModelAndView(); 替换形参中的参数 + Book book = new Book(); + book.setName("SpringMVC入门案例"); + book.setPrice(66.66d); + + //添加数据的方式,key对value + modelAndView.addObject("book",book); + modelAndView.addObject("name","Jockme"); + //设置页面的方式,该方法最后一次执行的结果生效 + modelAndView.setViewName("page"); + //返回值设定成ModelAndView对象 + return modelAndView; + } + ``` + +* ModelAndView 扩展 + + ```java + //ModelAndView对象支持转发的手工设定,该设定不会启用前缀后缀的页面拼接格式 + @RequestMapping("/showPageAndData4") + public ModelAndView showPageAndData4(ModelAndView modelAndView) { + modelAndView.setViewName("forward:/WEB-INF/page/page.jsp"); + return modelAndView; + } + + //ModelAndView对象支持重定向的手工设定,该设定不会启用前缀后缀的页面拼接格式 + @RequestMapping("/showPageAndData5") + public ModelAndView showPageAndData6(ModelAndView modelAndView) { + modelAndView.setViewName("redirect:page.jsp"); + return modelAndView; + } + ``` + + + +*** + + + +#### JSON数据 + +注解:@ResponseBody + +作用:将 Controller 的方法返回的对象通过适当的转换器转换为指定的格式之后,写入到 Response 的 body 区。如果返回值是字符串,那么直接将字符串返回客户端;如果是一个对象,会**将对象转化为 Json**,返回客户端 + +注意:当方法上面没有写 ResponseBody,底层会将方法的返回值封装为 ModelAndView 对象。 + +* 使用 HttpServletResponse 对象响应数据 + + ```java + @Controller + public class AccountController { + @RequestMapping("/showData1") + public void showData1(HttpServletResponse response) throws IOException { + response.getWriter().write("message"); + } + } + ``` + +* 使用 **@ResponseBody 将返回的结果作为响应内容**(页面显示),而非响应的页面名称 + + ```java + @RequestMapping("/showData2") + @ResponseBody + public String showData2(){ + return "{'name':'Jock'}"; + } + ``` + +* 使用 jackson 进行 json 数据格式转化 + + 导入坐标: + + ```xml + + + com.fasterxml.jackson.core + jackson-core + 2.9.0 + + + + com.fasterxml.jackson.core + jackson-databind + 2.9.0 + + + + com.fasterxml.jackson.core + jackson-annotations + 2.9.0 + + ``` + + ```java + @RequestMapping("/showData3") + @ResponseBody + public String showData3() throws JsonProcessingException { + Book book = new Book(); + book.setName("SpringMVC入门案例"); + book.setPrice(66.66d); + + ObjectMapper om = new ObjectMapper(); + return om.writeValueAsString(book); + } + ``` + +* 使用 SpringMVC 提供的消息类型转换器将对象与集合数据自动转换为 JSON 数据 + + ```java + //使用SpringMVC注解驱动,对标注@ResponseBody注解的控制器方法进行结果转换,由于返回值为引用类型,自动调用jackson提供的类型转换器进行格式转换 + @RequestMapping("/showData4") + @ResponseBody + public Book showData4() { + Book book = new Book(); + book.setName("SpringMVC入门案例"); + book.setPrice(66.66d); + return book; + } + ``` + + * 手工添加信息类型转换器 + + ```xml + + + + + + + + + ``` + +* 转换集合类型数据 + + ```java + @RequestMapping("/showData5") + @ResponseBody + public List showData5() { + Book book1 = new Book(); + book1.setName("SpringMVC入门案例"); + book1.setPrice(66.66d); + + Book book2 = new Book(); + book2.setName("SpringMVC入门案例"); + book2.setPrice(66.66d); + + ArrayList al = new ArrayList(); + al.add(book1); + al.add(book2); + return al; + } + ``` + + + +**** + + + +### Restful + +#### 基本介绍 + +Rest(REpresentational State Transfer):表现层状态转化,定义了**资源”在网络传输中以某种“表现形式”进行“状态转移**,即网络资源的访问方式 + +* 资源:把真实的对象数据称为资源,一个资源既可以是一个集合,也可以是单个个体;每一种资源都有特定的 URI(统一资源标识符)与之对应,如果获取这个资源,访问这个 URI 就可以,比如获取特定的班级`/class/12`;资源也可以包含子资源,比如 `/classes/classId/teachers`某个指定班级的所有老师 +* 表现形式:"资源"是一种信息实体,它可以有多种外在表现形式,把"资源"具体呈现出来的形式比如 json、xml、image、txt 等等叫做它的"表现层/表现形式" +* 状态转移:描述的服务器端资源的状态,比如增删改查(通过 HTTP 动词实现)引起资源状态的改变,互联网通信协议 HTTP 协议,是一个**无状态协议**,所有的资源状态都保存在服务器端 + + + +*** + + + +#### 访问方式 + +Restful 是按照 Rest 风格访问网络资源 + +* 传统风格访问路径:http://localhost/user/get?id=1 +* Rest 风格访问路径:http://localhost/user/1 + +优点:隐藏资源的访问行为,通过地址无法得知做的是何种操作,书写简化 + +Restful 请求路径简化配置方式:`@RestController = @Controller + @ResponseBody` + +相关注解: + +* `@GetMapping("/poll")` = `@RequestMapping(value = "/poll",method = RequestMethod.GET)` + +* `@PostMapping("/push")` = `@RequestMapping(value = "/push",method = RequestMethod.POST)` + + +过滤器:HiddenHttpMethodFilter 是 SpringMVC 对 Restful 风格的访问支持的过滤器 + +代码实现: + +* restful.jsp: + + * 页面表单**使用隐藏域提交请求类型**,参数名称固定为 _method,必须配合提交类型 method=post 使用 + + * GET 请求通过地址栏可以发送,也可以通过设置 form 的请求方式提交 + * POST 请求必须通过 form 的请求方式提交 + + ```html +

restful风格请求表单

+ +
+ + + +
+ ``` + +* java / controller / UserController + + ```java + @RestController //设置rest风格的控制器 + @RequestMapping("/user/") //设置公共访问路径,配合下方访问路径使用 + public class UserController { + @GetMapping("/user") + //@RequestMapping(value = "/user",method = RequestMethod.GET) + public String getUser(){ + return "GET-张三"; + } + + @PostMapping("/user") + //@RequestMapping(value = "/user",method = RequestMethod.POST) + public String saveUser(){ + return "POST-张三"; + } + + @PutMapping("/user") + //@RequestMapping(value = "/user",method = RequestMethod.PUT) + public String putUser(){ + return "PUT-张三"; + } + + @DeleteMapping("/user") + //@RequestMapping(value = "/user",method = RequestMethod.DELETE) + public String deleteUser(){ + return "DELETE-张三"; + } + } + ``` + +* 配置拦截器 web.xml + + ```xml + + + HiddenHttpMethodFilter + org.springframework.web.filter.HiddenHttpMethodFilter + + + HiddenHttpMethodFilter + DispatcherServlet + + ``` + + + +*** + + + +#### 参数注解 + +Restful 开发中的参数注解 + +```java +@GetMapping("{id}") +public String getMessage(@PathVariable("id") Integer id){ +} +``` + +使用 @PathVariable 注解获取路径上配置的具名变量,一般在有多个参数的时候添加 + +其他注解: + +* @RequestHeader:获取请求头 +* @RequestParam:获取请求参数(指问号后的参数,url?a=1&b=2) +* @CookieValue:获取Cookie值 +* @RequestAttribute:获取request域属性 +* @RequestBody:获取请求体[POST] +* @MatrixVariable:矩阵变量 +* @ModelAttribute + +```java +@RestController +@RequestMapping("/user/") +public class UserController { + //rest风格访问路径简化书写方式,配合类注解@RequestMapping使用 + @RequestMapping("{id}") + public String restLocation2(@PathVariable Integer id){ + System.out.println("restful is running ....get:" + id); + return "success.jsp"; + } + + //@RequestMapping(value = "{id}",method = RequestMethod.GET) + @GetMapping("{id}") + public String get(@PathVariable Integer id){ + System.out.println("restful is running ....get:" + id); + return "success.jsp"; + } + + @PostMapping("{id}") + public String post(@PathVariable Integer id){ + System.out.println("restful is running ....post:" + id); + return "success.jsp"; + } + + @PutMapping("{id}") + public String put(@PathVariable Integer id){ + System.out.println("restful is running ....put:" + id); + return "success.jsp"; + } + + @DeleteMapping("{id}") + public String delete(@PathVariable Integer id){ + System.out.println("restful is running ....delete:" + id); + return "success.jsp"; + } +} +``` + -* webapp / WEB-INF / web.xml,配置SpringMVC核心控制器,请求转发到对应的具体业务处理器Controller中(等同于Servlet配置) - ```xml - - - - - DispatcherServlet - org.springframework.web.servlet.DispatcherServlet - - - contextConfigLocation - classpath*:spring-mvc.xml - - - - DispatcherServlet - / - - - ``` -* resouces / spring-mvc.xml - ```xml - - - - - - ``` +**** +#### 识别原理 -*** +表单提交要使用 REST 时,会带上 `_method=PUT`,请求过来被 `HiddenHttpMethodFilter` 拦截,进行过滤操作 +org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(): +```java +public class HiddenHttpMethodFilter extends OncePerRequestFilter { + //兼容的请求 PUT、DELETE、PATCH + private static final List ALLOWED_METHODS = + Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(), + HttpMethod.DELETE.name(), HttpMethod.PATCH.name())); + //隐藏域的名字 + public static final String DEFAULT_METHOD_PARAM = "_method"; + + private String methodParam = DEFAULT_METHOD_PARAM; + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + HttpServletRequest requestToUse = request; + //请求必须是 POST, + if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) { + //获取标签中 name="_method" 的 value 值 + String paramValue = request.getParameter(this.methodParam); + if (StringUtils.hasLength(paramValue)) { + //转成大写 + String method = paramValue.toUpperCase(Locale.ENGLISH); + //兼容的请求方式 + if (ALLOWED_METHODS.contains(method)) { + //包装请求 + requestToUse = new HttpMethodRequestWrapper(request, method); + } + } + } + //过滤器链放行的时候用wrapper。以后的方法调用getMethod是调用requesWrapper的 + filterChain.doFilter(requestToUse, response); + } +} +``` -### 加载控制 +Rest 使用客户端工具,如 PostMan 可直接发送 put、delete 等方式请求不被过滤 -Controller 加载控制:SpringMVC 的处理器对应的 bean 必须按照规范格式开发,未避免加入无效的 bean 可通过 bean 加载过滤器进行包含设定或排除设定,表现层 bean 标注通常设定为 @Controller +改变默认的 `_method` 的方式: -* resources / spring-mvc.xml 配置 +```java +@Configuration(proxyBeanMethods = false) +public class WebConfig{ + //自定义filter + @Bean + public HiddenHttpMethodFilter hiddenHttpMethodFilter(){ + HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter(); + //通过set 方法自定义 + methodFilter.setMethodParam("_m"); + return methodFilter; + } +} +``` - ```xml - - - - ``` -* 静态资源加载(webapp 目录下的相关资源),spring-mvc.xml 配置,开启 mvc 命名空间 - ```xml - - - - - - - - ``` -* 中文乱码处理 SpringMVC 提供专用的中文字符过滤器,用于处理乱码问题。配置在 web.xml 里面 - ```xml - - - CharacterEncodingFilter - org.springframework.web.filter.CharacterEncodingFilter - - encoding - UTF-8 - - - - CharacterEncodingFilter - /* - - ``` +**** -*** +### 原理解析 +请求进入原生的 HttpServlet 的 doGet() 方法处理,调用子类 FrameworkServlet 的 doGet() 方法,最终调用 DispatcherServlet 的 doService() 方法,为请求设置相关属性后调用 doDispatch() +![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-请求相应的原理.png) -### 注解驱动 +总体流程: -纯注解开发: +* 所有的请求映射都在 HandlerMapping 中,RequestMappingHandlerMapping 处理 @RequestMapping 注解的所有映射规则 -* 使用注解形式转化 SpringMVC 核心配置文件为配置类 java / config / SpringMVCConfiguration.java +* 请求进来,遍历所有的 HandlerMapping 看是否有请求信息,匹配成功后返回,匹配失败设置 HTTP 响应码 +* 用户可以自定义的映射处理,也可以给容器中放入自定义 HandlerMapping + +访问 URL:http://localhost:8080/user(对应 Restful 中配置的映射规则) + +```java +protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { + HttpServletRequest processedRequest = request; + HandlerExecutionChain mappedHandler = null; + boolean multipartRequestParsed = false; //文件上传请求 + WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);//异步管理器 + try { + //文件上传请求 + processedRequest = checkMultipart(request); + // 找到当前请求使用哪个Handler(Controller的方法)处理 + mappedHandler = getHandler(processedRequest); + // 没有合适的处理请求的方式 handler 直接返回 + if (mappedHandler == null) { + noHandlerFound(processedRequest, response); + return; + } + //... + } +} +``` + +* HandlerMapping 处理器映射器,保存了所有 `@RequestMapping` 和 `handler` 的映射规则 ```java - @Configuration - @ComponentScan(value = "com.seazean", includeFilters = @ComponentScan.Filter( - type=FilterType.ANNOTATION, - classes = {Controller.class} ) - ) - public class SpringMVCConfiguration implements WebMvcConfigurer{ - //注解配置放行指定资源格式 - // @Override - // public void addResourceHandlers(ResourceHandlerRegistry registry) { - // registry.addResourceHandler("/img/**").addResourceLocations("/img/"); - // } - //注解配置通用放行资源的格式 建议使用 - @Override - public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { - configurer.enable(); + protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { + if (this.handlerMappings != null) { + //遍历所有的 HandlerMapping + for (HandlerMapping mapping : this.handlerMappings) { + //尝试去每个 HandlerMapping 中匹配当前请求的处理 + HandlerExecutionChain handler = mapping.getHandler(request); + if (handler != null) { + return handler; + } + } } + return null; } ``` -* 基于 servlet3.0 规范,自定义 Servlet 容器初始化配置类,加载 SpringMVC 核心配置类 + ![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-获取Controller处理器.png) + +* `mapping.getHandler(request)` 中调用 `Object handler = getHandlerInternal(request)`,该 getHandlerInternal 方法是 + + RequestMappingInfoHandlerMapping 类中的,继续调用 `AbstractHandlerMethodMapping.getHandlerInternal()` + +* AbstractHandlerMethodMapping.getHandlerInternal(): ```java - public class ServletContainersInitConfig extends AbstractDispatcherServletInitializer { - //创建Servlet容器时,使用注解方式加载SPRINGMVC配置类中的信息, - //并加载成WEB专用的ApplicationContext对象该对象放入了ServletContext范围, - //在整个WEB容器中可以随时获取调用 - @Override - protected WebApplicationContext createServletApplicationContext() { - A.C.W.A ctx = new AnnotationConfigWebApplicationContext(); - ctx.register(SpringMVCConfiguration.class); - return ctx; - } - - //注解配置映射地址方式,服务于SpringMVC的核心控制器DispatcherServlet - @Override - protected String[] getServletMappings() { - return new String[]{"/"}; - } - - @Override - protected WebApplicationContext createRootApplicationContext() { - return null; + protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception { + // lookupPath = user,地址栏的 uri + String lookupPath = initLookupPath(request); + // 防止并发 + this.mappingRegistry.acquireReadLock(); + try { + //获取当前 HandlerMapping 中的映射规则 + HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request); + return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null); } - - //乱码处理作为过滤器,在servlet容器启动时进行配置 - @Override - public void onStartup(ServletContext servletContext) throws ServletException { - super.onStartup(servletContext); - CharacterEncodingFilter cef = new CharacterEncodingFilter(); - cef.setEncoding("UTF-8"); - FilterRegistration.Dynamic registration = servletContext.addFilter("characterEncodingFilter", cef); - registration.addMappingForUrlPatterns(EnumSet.of( - DispatcherType.REQUEST, - DispatcherType.FORWARD, - DispatcherType.INCLUDE), false,"/*"); + finally { + this.mappingRegistry.releaseReadLock(); } } ``` +* AbstractHandlerMethodMapping.lookupHandlerMethod(): + * `directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath)`:获取与 URI 相关的映射规则 -*** + ![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-HandlerMapping的映射规则.png) + * `addMatchingMappings(directPathMatches, matches, request)`:匹配某个映射规则 + * `Match bestMatch = matches.get(0)`:匹配完成只剩一个,直接获取返回对应的方法 -### 请求映射 + * `if (matches.size() > 1)`:当有多个映射规则符合请求时,报错 -名称:@RequestMapping -类型:方法注解、类注解 -位置:处理器类中的方法定义上方、处理器类定义上方 +*** -* 方法注解 - 作用:绑定请求地址与对应处理方法间的关系 - 无类映射地址访问格式: http://localhost/requestURL2 + +### Servlet + +SpringMVC 提供访问原始 Servlet 接口的功能,这一部分是补充知识,不常用 + +* spring-mvc.xml 配置 + + ```xml + + + + + + + + + + + ``` + +* SpringMVC 提供访问原始 Servlet 接口 API 的功能,通过形参声明即可 ```java - @Controller - public class UserController { - @RequestMapping("/requestURL2") - public String requestURL2() { - return "page.jsp"; - } + @RequestMapping("/servletApi") + public String servletApi(HttpServletRequest request, + HttpServletResponse response, HttpSession session){ + System.out.println(request); + System.out.println(response); + System.out.println(session); + request.setAttribute("name","seazean"); + System.out.println(request.getAttribute("name")); + return "page.jsp"; } ``` -* 类注解 - - 作用:为当前处理器中所有方法设定公共的访问路径前缀 - 带有类映射地址访问格式,将类映射地址作为前缀添加在实际映射地址前面:**/user/requestURL1** - 最终返回的页面如果未设定绝对访问路径,将从类映射地址所在目录中查找 **webapp/user/page.jsp** - +* Head 数据获取快捷操作方式 + 名称:@RequestHeader + 类型:形参注解 + 位置:处理器类中的方法形参前方 + 作用:绑定请求头数据与对应处理方法形参间的关系 + 范例: + ```java - @Controller - @RequestMapping("/user") - public class UserController { - @RequestMapping("/requestURL2") - public String requestURL2() { - return "page.jsp"; - } - } + 快捷操作方式@RequestMapping("/headApi") + public String headApi(@RequestHeader("Accept-Language") String headMsg){ + System.out.println(headMsg); + return "page"; + } ``` - -* 常用属性 + +* Cookie 数据获取快捷操作方式 + 名称:@CookieValue + 类型:形参注解 + 位置:处理器类中的方法形参前方 + 作用:绑定请求 Cookie 数据与对应处理方法形参间的关系 + 范例: ```java - @RequestMapping( - value="/requestURL3", //设定请求路径,与path属性、 value属性相同 - method = RequestMethod.GET, //设定请求方式 - params = "name", //设定请求参数条件 - headers = "content-type=text/*", //设定请求消息头条件 - consumes = "text/*", //用于指定可以接收的请求正文类型(MIME类型) - produces = "text/*" //用于指定可以生成的响应正文类型(MIME类型) - ) - public String requestURL3() { - return "/page.jsp"; - } + @RequestMapping("/cookieApi") + public String cookieApi(@CookieValue("JSESSIONID") String jsessionid){ + System.out.println(jsessionid); + return "page"; + } ``` +* Session 数据获取 + 名称:@SessionAttribute + 类型:形参注解 + 位置:处理器类中的方法形参前方 + 作用:绑定请求Session数据与对应处理方法形参间的关系 + 范例: + ```java + @RequestMapping("/sessionApi") + public String sessionApi(@SessionAttribute("name") String name){ + System.out.println(name); + return "page.jsp"; + } + //用于在session中放入数据 + @RequestMapping("/setSessionData") + public String setSessionData(HttpSession session){ + session.setAttribute("name","seazean"); + return "page"; + } + ``` -*** +* Session 数据设置 + 名称:@SessionAttributes + 类型:类注解 + 位置:处理器类上方 + 作用:声明放入session范围的变量名称,适用于Model类型数据传参 + 范例: + ```java + @Controller + //设定当前类中名称为age和gender的变量放入session范围,不常用 + @SessionAttributes(names = {"age","gender"}) + public class ServletController { + //将数据放入session存储范围,Model对象实现数据set,@SessionAttributes注解实现范围设定 + @RequestMapping("/setSessionData2") + public String setSessionDate2(Model model) { + model.addAttribute("age",39); + model.addAttribute("gender","男"); + return "page"; + } + + @RequestMapping("/sessionApi") + public String sessionApi(@SessionAttribute("age") int age, + @SessionAttribute("gender") String gender){ + System.out.println(name); + System.out.println(age); + return "page"; + } + } + ``` + -## 基本操作 -(正在更新请求与响应的笔记) +**** -**** @@ -9404,9 +10612,9 @@ public class AjaxController { } ``` -* 注解添加到Pojo参数前方时,封装的异步提交数据按照Pojo的属性格式进行关系映射 - * POJO中的属性如果请求数据中没有,属性值为null - * POJO中没有的属性如果请求数据中有,不进行映射 +* 注解添加到 Pojo 参数前方时,封装的异步提交数据按照 Pojo 的属性格式进行关系映射 + * POJO 中的属性如果请求数据中没有,属性值为 null + * POJO 中没有的属性如果请求数据中有,不进行映射 * 注解添加到集合参数前方时,封装的异步提交数据按照集合的存储结构进行关系映射 ```java @@ -9578,7 +10786,7 @@ $("#testAjaxReturnJsonList").click(function(){ ### 跨域访问 -跨域访问:当通过域名A下的操作访问域名B下的资源时,称为跨域访问。跨域访问时,会出现无法访问的现象。 +跨域访问:当通过域名 A 下的操作访问域名 B 下的资源时,称为跨域访问。跨域访问时,会出现无法访问的现象。 环境搭建: @@ -9642,6 +10850,8 @@ public User cross(HttpServletRequest request){ + + ## 拦截器 ### 基本介绍 @@ -9709,6 +10919,10 @@ public boolean preHandle(HttpServletRequest request, +*** + + + #### 后置处理 原始方法运行后运行,如果原始方法被拦截,则不执行: @@ -9728,6 +10942,10 @@ public void postHandle(HttpServletRequest request, +*** + + + #### 完成处理 拦截器最后执行的方法,无论原始方法是否执行: @@ -9882,10 +11100,14 @@ public void afterCompletion(HttpServletRequest request, + + *** + + ## 异常处理 ### 处理器 @@ -10150,6 +11372,8 @@ ExceptionHandler注解: + + ## 实用技术 ### 文件传输 @@ -11515,7 +12739,7 @@ public class ProjectExceptionAdivce { ### spring-mvc.xml -* 注解替代spring-mvc.xml:SpringMvcConfig +* 注解替代 spring-mvc.xml:SpringMvcConfig ```java @Configuration @@ -11592,7 +12816,7 @@ public class ProjectExceptionAdivce { } ``` -* WebApplicationContext,生成 Spring 核心容器(主容器/父容器/跟容器) +* WebApplicationContext,生成 Spring 核心容器(主容器/父容器/根容器) * 父容器:Spring 环境加载后形成的容器,包含 Spring 环境下的所有的 bean * 子容器:当前 mvc 环境加载后形成的容器,不包含 Spring 环境下的 bean diff --git a/Web.md b/Web.md index 68f8600..9c6edbb 100644 --- a/Web.md +++ b/Web.md @@ -2951,9 +2951,9 @@ public class ServletDemo06 extends HttpServlet { #### 默认Servlet -默认Servlet是由服务器提供的一个Servlet,它配置在Tomcat的conf目录下的web.xml中。 +默认 Servlet 是由服务器提供的一个 Servlet,它配置在 Tomcat 的 conf 目录下的 web.xml 中。 -它的映射路径是`/`,我们在发送请求时,首先会在我们应用中的web.xml中查找映射配置。但是当找不到对应的Servlet路径时,就去找默认的Servlet,由默认Servlet处理。 +它的映射路径是`/`,我们在发送请求时,首先会在我们应用中的 web.xml 中查找映射配置。但是当找不到对应的 Servlet 路径时,就去找默认的 Servlet,由默认 Servlet 处理。 @@ -2963,24 +2963,23 @@ public class ServletDemo06 extends HttpServlet { ### ServletConfig -**概念:** - ServletConfig是Servlet的配置参数对象。在Servlet规范中,允许为每个Servlet都提供一些初始化配置。 - 每个Servlet都一个自己的ServletConfig,作用是**在Servlet初始化期间,把一些配置信息传递给Servlet**。 +ServletConfig 是 Servlet 的配置参数对象。在 Servlet 规范中,允许为每个 Servlet 都提供一些初始化配置,每个 Servlet 都有自己的ServletConfig,作用是**在 Servlet 初始化期间,把一些配置信息传递给 Servlet** -**生命周期:** - 在初始化阶段读取了web.xml中为Servlet准备的初始化配置,并把配置信息传递给Servlet,所以生命周期与 Servlet相同。如果Servlet配置了`1`,ServletConfig也会在应用加载时创建。 +生命周期:在初始化阶段读取了 web.xml 中为 Servlet 准备的初始化配置,并把配置信息传递给 Servlet,所以生命周期与 Servlet 相同。如果 Servlet 配置了 `1`,ServletConfig 也会在应用加载时创建。 -**获取ServletConfig**:在init方法中为ServletConfig赋值 +获取 ServletConfig:在 init 方法中为 ServletConfig 赋值 -**常用API:** - `String getInitParameter(String name)` : 根据初始化参数的名称,获取参数的值。 - 根据,获取 - `Enumeration getInitParameterNames()` : 获取所有初始化参数名称的枚举(遍历方式看例子) - `ServletContext getServletContext()` : 获取**ServletContext**对象 - `String getServletName()` : 获取Servlet名称 +常用API: -* web.xml配置: - 初始化参数使用``标签中的``标签来配置,并且每个Servlet都支持有多个初始化参数,并且初始化参数都是以键值对的形式存在的 +* `String getInitParameter(String name)`:根据初始化参数的名称获取参数的值,根据,获取 +* `Enumeration getInitParameterNames()` : 获取所有初始化参数名称的枚举(遍历方式看例子) +* `ServletContext getServletContext()` : 获取**ServletContext**对象 +* `String getServletName()` : 获取Servlet名称 + +代码实现: + +* web.xml 配置: + 初始化参数使用 `` 标签中的 ` `标签来配置,并且每个 Servlet 都支持有多个初始化参数,并且初始化参数都是以键值对的形式存在的 ```xml @@ -3069,31 +3068,27 @@ public class ServletDemo06 extends HttpServlet { ### ServletContext -**概念:** - ServletContext对象是应用上下文对象。服务器为每一个应用(项目)都创建了一个ServletContext对象 - ServletContext属于整个应用,不局限于某个Servlet。它可以实现让应用中所有Servlet间的数据共享。 +ServletContext 对象是应用上下文对象。服务器为每一个应用都创建了一个 ServletContext 对象,ServletContext 属于整个应用,不局限于某个 Servlet,可以实现让应用中所有 Servlet 间的数据共享。 + +上下文代表了程序当下所运行的环境,联系整个应用的生命周期与资源调用,是程序可以访问到的所有资源的总和,资源可以是一个变量,也可以是一个对象的引用 -**上下文:** - 上下文,上下文代表了程序当下所运行的环境,联系你整个应用的生命周期与资源调用,是程序可以访问到的所有资源的总和,资源可以是一个变量,也可以是一个对象的引用 +生命周期: -**生命周期:** - 出生:应用一加载,该对象就被创建出来。一个应用只有一个实例对象(Servlet和ServletContext都是单例的) - 活着:只要应用一直提供服务,该对象就一直存在。 - 死亡:应用被卸载(或者服务器停止),该对象消亡。 +* 出生:应用一加载,该对象就被创建出来。一个应用只有一个实例对象(Servlet 和 ServletContext 都是单例的) +* 活着:只要应用一直提供服务,该对象就一直存在。 +* 死亡:应用被卸载(或者服务器停止),该对象消亡。 -**域对象:** - 域对象的概念:指的是对象有作用域,即有作用范围。 - 域对象的作用:域对象可以实现数据共享,不同作用范围的域对象,共享数据的能力不一样。 - Servlet规范中,共有4个域对象,ServletContext是其中一个,web应用中最大的作用域,叫application域, - application域可以实现整个应用间的数据共享功能。 +域对象:指的是对象有作用域,即有作用范围,可以**实现数据共享**,不同作用范围的域对象,共享数据的能力不一样。 -**数据共享:** +Servlet 规范中,共有4个域对象,ServletContext 是其中一个,web 应用中最大的作用域,叫 application 域,可以实现整个应用间的数据共享功能。 + +数据共享: -**获取ServletContext:** +获取ServletContext: -* Java项目继承HttpServlet,HttpServlet继承GenericServlet,GenericServlet中有一个方法可以直接使用 +* Java 项目继承 HttpServlet,HttpServlet 继承 GenericServlet,GenericServlet 中有一个方法可以直接使用 ```java public ServletContext getServletContext() { @@ -3102,21 +3097,23 @@ public class ServletDemo06 extends HttpServlet { ``` -* ServletRequest类方法: +* ServletRequest 类方法: ```java ServletContext getServletContext()//获取ServletContext对象 ``` - -**常用API:** - `String getInitParameter(String name)` : 根据名称获取全局配置的参数 - `String getContextPath` : 获取当前应用访问的虚拟目录 - `String getRealPath(String path)` : 根据虚拟目录获取应用部署的磁盘绝对路径 - `void setAttribute(String name, Object object)` : 向应用域对象中存储数据 - `Object getAttribute(String name)` : 根据名称获取域对象中的数据,没有则返回null - `void removeAttribute(String name)` : 根据名称移除应用域对象中的数据 +常用API: + +* `String getInitParameter(String name)` : 根据名称获取全局配置的参数 +* `String getContextPath` : 获取当前应用访问的虚拟目录 +* `String getRealPath(String path)` : 根据虚拟目录获取应用部署的磁盘绝对路径 +* `void setAttribute(String name, Object object)` : 向应用域对象中存储数据 +* `Object getAttribute(String name)` : 根据名称获取域对象中的数据,没有则返回null +* `void removeAttribute(String name)` : 根据名称移除应用域对象中的数据 + +代码实现: * web.xml配置: 配置的方式,需要在``标签中使用``来配置初始化参数,它的配置是针对整个应用的配置,被称为应用的初始化参数配置。 @@ -3192,7 +3189,7 @@ public class ServletDemo06 extends HttpServlet { ### 注解开发 -Servlet3.0版本!不需要配置web.xml +Servlet3.0 版本!不需要配置 web.xml * 注解案例 @@ -3276,10 +3273,9 @@ Web服务器收到客户端的http请求,会针对每一次请求,分别创 请求:客户机希望从服务器端索取一些资源,向服务器发出询问 -请求对象:在JavaEE工程中,用于发送请求的对象 - 常用的对象是ServletRequest和HttpServletRequest,它们的区是是否与HTTP协议有关 +请求对象:在 JavaEE 工程中,用于发送请求的对象,常用的对象是 ServletRequest 和 HttpServletRequest ,它们的区是是否与 HTTP 协议有关 -Request作用: +Request 作用: * 操作请求三部分(行,头,体) * 请求转发 @@ -3293,7 +3289,7 @@ Request作用: -### 获取请求路径 +### 请求路径 | 方法 | 作用 | | ------------------------------- | ------------------------------------------------------------ | @@ -3358,7 +3354,7 @@ public class ServletDemo02 extends HttpServlet { -### 获取请求参数 +### 请求参数 #### 请求参数 @@ -3550,7 +3546,7 @@ public class ServletDemo07 extends HttpServlet { #### 请求域 -request域:可以在一次请求范围内进行共享数据 +request 域:可以在一次请求范围内进行共享数据 | 方法 | 作用 | | -------------------------------------------- | ---------------------------- | @@ -5370,13 +5366,12 @@ Filter:过滤器,是JavaWeb三大组件之一,另外两个是Servlet和Lis #### FilterChain -* FilterChain是一个接口,代表过滤器对象。由Servlet容器提供实现类对象,直接使用即可。 +* FilterChain 是一个接口,代表过滤器对象。由Servlet容器提供实现类对象,直接使用即可。 * 过滤器可以定义多个,就会组成过滤器链 -* 核心方法 - `void doFilter(ServletRequest request, ServletResponse response)`:放行方法 - +* 核心方法:`void doFilter(ServletRequest request, ServletResponse response)` 用来放行方法 + 如果有多个过滤器,在第一个过滤器中调用下一个过滤器,以此类推,直到到达最终访问资源。 如果只有一个过滤器,放行时就会直接到达最终访问资源。 @@ -5384,7 +5379,7 @@ Filter:过滤器,是JavaWeb三大组件之一,另外两个是Servlet和Lis #### FilterConfig -* FilterConfig 是一个接口,代表过滤器的配置对象,可以加载一些初始化参数 +FilterConfig 是一个接口,代表过滤器的配置对象,可以加载一些初始化参数 * 核心方法: @@ -5408,6 +5403,7 @@ Filter:过滤器,是JavaWeb三大组件之一,另外两个是Servlet和Lis #### 设置页面编码 请求先被过滤器拦截进行相关操作 + 过滤器放行之后执行完目标资源,仍会回到过滤器中 * Filter代码: @@ -5463,7 +5459,7 @@ Filter:过滤器,是JavaWeb三大组件之一,另外两个是Servlet和Lis 多个过滤器使用的顺序,取决于过滤器映射的顺序。 -* 两个Filter代码: +* 两个 Filter 代码: ```java public class FilterDemo01 implements Filter{ From 5546ef0e0718a81e4c71c3318ebfa93214dbe202 Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 2 Aug 2021 23:04:32 +0800 Subject: [PATCH 009/168] Update Java Notes --- Java.md | 2 +- Prog.md | 6 +- SSM.md | 189 +++++++++++++++++++++----------------------------------- 3 files changed, 76 insertions(+), 121 deletions(-) diff --git a/Java.md b/Java.md index 2892701..2aca15e 100644 --- a/Java.md +++ b/Java.md @@ -1307,7 +1307,7 @@ class Animal{ #### 方法访问 -子类继承了父类就得到了父类的方法,可以直接调用,受权限修饰符的限制,也可以重写方法 +子类继承了父类就得到了父类的方法,**可以直接调用**,受权限修饰符的限制,也可以重写方法 **方法重写**:子类重写一个与父类申明一样的方法来**覆盖**父类的该方法 diff --git a/Prog.md b/Prog.md index 460d7ed..a4f11b7 100644 --- a/Prog.md +++ b/Prog.md @@ -3034,7 +3034,7 @@ CAS算法:有3个操作数(内存值V, 旧的预期值A,要修改的值B CAS 必须借助 volatile 才能读取到共享变量的最新值来实现**比较并交换**的效果 -分析getAndUpdate方法: +分析 getAndUpdate 方法: * getAndUpdate: @@ -3080,7 +3080,7 @@ CAS算法:有3个操作数(内存值V, 旧的预期值A,要修改的值B #### 原子引用 -原子引用:对Object进行原子操作,提供一种读和写都是原子性的对象引用变量 +原子引用:对 Object 进行原子操作,提供一种读和写都是原子性的对象引用变量 原子引用类:AtomicReference、AtomicStampedReference、AtomicMarkableReference @@ -3090,7 +3090,7 @@ AtomicReference类: * 常用API: - `public final boolean compareAndSet(V expectedValue, V newValue)`:CAS操作 + `public final boolean compareAndSet(V expectedValue, V newValue)`:CAS 操作 `public final void set(V newValue)`:将值设置为 newValue `public final V get()`:返回当前值 diff --git a/SSM.md b/SSM.md index a2a6c00..aaac61f 100644 --- a/SSM.md +++ b/SSM.md @@ -8004,6 +8004,8 @@ AbstractAutowireCapableBeanFactory.createBeanInstance(beanName, RootBeanDefiniti * 判断类的访问权限是不是 public,不是进入下一个判断,是否允许访问类的 non-public 的构造方法,不允许则报错 +* `Supplier instanceSupplier = mbd.getInstanceSupplier()`:获取创建实例的函数,可以自定义,没有进入下面的逻辑 + * `if (mbd.getFactoryMethodName() != null)`:**判断 bean 是否设置了 factory-method 属性** ,设置了该属性进入 factory-method 方法创建实例 @@ -8017,13 +8019,13 @@ AbstractAutowireCapableBeanFactory.createBeanInstance(beanName, RootBeanDefiniti * method 为 null 则 resolved 和 autowireNecessary 都为默认值 false * `autowireNecessary = mbd.constructorArgumentsResolved`:构造方法有参数,设置为 true -* bd 对应的构造信息解析完成: +* **bd 对应的构造信息解析完成,可以直接反射调用构造方法了**: * `return autowireConstructor(beanName, mbd, null, null)`:**有参构造**,根据参数匹配最优的构造器创建实例 * `return instantiateBean(beanName, mbd)`:**无参构造方法通过反射创建实例** -* `ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName)`:**AutowiredAnnotation 逻辑** +* `ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName)`:@Autowired 注解对应的后置处理器**AutowiredAnnotationBeanPostProcessor 逻辑** * 配置了 lookup 的相关逻辑 @@ -8080,7 +8082,7 @@ AbstractAutowireCapableBeanFactory.createBeanInstance(beanName, RootBeanDefiniti `!ObjectUtils.isEmpty(args)`:getBean 时,指定了参数 arg -* `autowireConstructor(beanName, mbd, ctors, args)`:**选择最优的构造器进行创建实例**(非常复杂,可以放弃深究) +* `return autowireConstructor(beanName, mbd, ctors, args)`:**选择最优的构造器进行创建实例**(复杂,不建议研究) * `beanFactory.initBeanWrapper(bw)`:向 BeanWrapper 中注册转换器,向工厂中注册属性编辑器 @@ -8157,13 +8159,19 @@ AbstractAutowireCapableBeanFactory.createBeanInstance(beanName, RootBeanDefiniti * ` bw.setBeanInstance(instantiate(beanName, mbd, constructorToUse, argsToUse))`:匹配成功调用 instantiate 创建出实例对象,设置到 BeanWrapper 中去 -* `SimpleInstantiationStrategy.instantiate()`:**真正用来实例化的函数**(无论如何都会走到这一步) +* `return instantiateBean(beanName, mbd)`:默认走到这里 - * `if (!bd.hasMethodOverrides())`:没有方法重写覆盖 + * `SimpleInstantiationStrategy.instantiate()`:**真正用来实例化的函数**(无论如何都会走到这一步) - `BeanUtils.instantiateClass(constructorToUse)`:底层调用 java.lang.reflect.Constructor.newInstance() 实例化 + * `if (!bd.hasMethodOverrides())`:没有方法重写覆盖 - * `instantiateWithMethodInjection(bd, beanName, owner)`:有方法重写采用 CGLIB 实例化 + `BeanUtils.instantiateClass(constructorToUse)`:调用 `java.lang.reflect.Constructor.newInstance()` 实例化 + + * `instantiateWithMethodInjection(bd, beanName, owner)`:有方法重写采用 CGLIB 实例化 + + * `BeanWrapper bw = new BeanWrapperImpl(beanInstance)`:包装成 BeanWrapper 类型的对象 + + * `return bw`:返回实例 @@ -9760,10 +9768,39 @@ SpringMVC 对接收的数据进行自动类型转换,该工作通过 Converter ##### 自定义 -* 自定义类型转换器,实现 Converter 接口,并制定转换前与转换后的类型 +自定义类型转换器,实现 Converter 接口或者直接容器中注入: + +* 方式一: + + ```java + public class WebConfig implements WebMvcConfigurer { + @Bean + public WebMvcConfigurer webMvcConfigurer() { + return new WebMvcConfigurer() { + @Override + public void addFormatters(FormatterRegistry registry) { + registry.addConverter(new Converter() { + @Override + public Pet convert(String source) { + DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); + Date date = null; + //类型转换器无法预计使用过程中出现的异常,因此必须在类型转换器内部捕获, + //不允许抛出,框架无法预计此类异常如何处理 + try { + date = df.parse(source); + } catch (ParseException e) { + e.printStackTrace(); + } + return date; + } + }); + } + } + } + +* 方式二: ```java - //自定义类型转换器,实现Converter接口,接口中指定的泛型即为最终作用的条件 //本例中的泛型填写的是String,Date,最终出现字符串转日期时,该类型转换器生效 public class MyDateConverter implements Converter { //重写接口的抽象方法,参数由泛型决定 @@ -9782,7 +9819,7 @@ SpringMVC 对接收的数据进行自动类型转换,该工作通过 Converter } ``` -* 配置 resources / spring-mvc.xml,注册自定义转换器,将功能加入到 SpringMVC 转换服务 ConverterService 中 + 配置 resources / spring-mvc.xml,注册自定义转换器,将功能加入到 SpringMVC 转换服务 ConverterService 中 ```xml @@ -9900,9 +9937,10 @@ SpringMVC 对接收的数据进行自动类型转换,该工作通过 Converter -#### 带数据跳转 +#### 数据跳转 + +ModelAndView 是 SpringMVC 提供的一个对象,该对象可以用作控制器方法的返回值(Model 同),实现携带数据跳转 -ModelAndView 是 SpringMVC 提供的一个对象,该对象可以用作控制器方法的返回值(Model 同) 作用: + 设置数据,向请求域对象中存储数据 @@ -9989,13 +10027,13 @@ ModelAndView 是 SpringMVC 提供的一个对象,该对象可以用作控制 -#### JSON数据 +#### JSON 注解:@ResponseBody 作用:将 Controller 的方法返回的对象通过适当的转换器转换为指定的格式之后,写入到 Response 的 body 区。如果返回值是字符串,那么直接将字符串返回客户端;如果是一个对象,会**将对象转化为 Json**,返回客户端 -注意:当方法上面没有写 ResponseBody,底层会将方法的返回值封装为 ModelAndView 对象。 +注意:当方法上面没有写 ResponseBody,底层会将方法的返回值封装为 ModelAndView 对象 * 使用 HttpServletResponse 对象响应数据 @@ -10242,11 +10280,11 @@ public String getMessage(@PathVariable("id") Integer id){ * @RequestHeader:获取请求头 * @RequestParam:获取请求参数(指问号后的参数,url?a=1&b=2) -* @CookieValue:获取Cookie值 -* @RequestAttribute:获取request域属性 -* @RequestBody:获取请求体[POST] +* @CookieValue:获取 Cookie 值 +* @RequestAttribute:获取 request 域属性 +* @RequestBody:获取请求体 [POST] * @MatrixVariable:矩阵变量 -* @ModelAttribute +* @ModelAttribute:自定义类型变量 ```java @RestController @@ -10355,105 +10393,6 @@ public class WebConfig{ - - -**** - - - -### 原理解析 - -请求进入原生的 HttpServlet 的 doGet() 方法处理,调用子类 FrameworkServlet 的 doGet() 方法,最终调用 DispatcherServlet 的 doService() 方法,为请求设置相关属性后调用 doDispatch() - -![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-请求相应的原理.png) - -总体流程: - -* 所有的请求映射都在 HandlerMapping 中,RequestMappingHandlerMapping 处理 @RequestMapping 注解的所有映射规则 - -* 请求进来,遍历所有的 HandlerMapping 看是否有请求信息,匹配成功后返回,匹配失败设置 HTTP 响应码 -* 用户可以自定义的映射处理,也可以给容器中放入自定义 HandlerMapping - -访问 URL:http://localhost:8080/user(对应 Restful 中配置的映射规则) - -```java -protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { - HttpServletRequest processedRequest = request; - HandlerExecutionChain mappedHandler = null; - boolean multipartRequestParsed = false; //文件上传请求 - WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);//异步管理器 - try { - //文件上传请求 - processedRequest = checkMultipart(request); - // 找到当前请求使用哪个Handler(Controller的方法)处理 - mappedHandler = getHandler(processedRequest); - // 没有合适的处理请求的方式 handler 直接返回 - if (mappedHandler == null) { - noHandlerFound(processedRequest, response); - return; - } - //... - } -} -``` - -* HandlerMapping 处理器映射器,保存了所有 `@RequestMapping` 和 `handler` 的映射规则 - - ```java - protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { - if (this.handlerMappings != null) { - //遍历所有的 HandlerMapping - for (HandlerMapping mapping : this.handlerMappings) { - //尝试去每个 HandlerMapping 中匹配当前请求的处理 - HandlerExecutionChain handler = mapping.getHandler(request); - if (handler != null) { - return handler; - } - } - } - return null; - } - ``` - - ![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-获取Controller处理器.png) - -* `mapping.getHandler(request)` 中调用 `Object handler = getHandlerInternal(request)`,该 getHandlerInternal 方法是 - - RequestMappingInfoHandlerMapping 类中的,继续调用 `AbstractHandlerMethodMapping.getHandlerInternal()` - -* AbstractHandlerMethodMapping.getHandlerInternal(): - - ```java - protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception { - // lookupPath = user,地址栏的 uri - String lookupPath = initLookupPath(request); - // 防止并发 - this.mappingRegistry.acquireReadLock(); - try { - //获取当前 HandlerMapping 中的映射规则 - HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request); - return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null); - } - finally { - this.mappingRegistry.releaseReadLock(); - } - } - ``` - -* AbstractHandlerMethodMapping.lookupHandlerMethod(): - - * `directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath)`:获取与 URI 相关的映射规则 - - ![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-HandlerMapping的映射规则.png) - - * `addMatchingMappings(directPathMatches, matches, request)`:匹配某个映射规则 - - * `Match bestMatch = matches.get(0)`:匹配完成只剩一个,直接获取返回对应的方法 - - * `if (matches.size() > 1)`:当有多个映射规则符合请求时,报错 - - - *** @@ -11783,6 +11722,22 @@ jsp: +*** + + + +## 运行原理 + + + + + + + + + + + *** From 236479f2967f4784e7aa32de06fc0e37e2aeb042 Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 4 Aug 2021 16:46:34 +0800 Subject: [PATCH 010/168] Update Java Notes --- Java.md | 12 +- Prog.md | 2 +- SSM.md | 1579 +++++++++++++++++++++++++++++++++++++++++++++---------- Web.md | 10 +- 4 files changed, 1313 insertions(+), 290 deletions(-) diff --git a/Java.md b/Java.md index 2aca15e..1201277 100644 --- a/Java.md +++ b/Java.md @@ -9461,7 +9461,7 @@ JVM、JRE、JDK对比: ### 架构模型 -Java 编译器输入的指令流是一种基于栈的指令集架构。因为跨平台的设计,java 的指令都是根据栈来设计的,不同平台 CPU 架构不同,所以不能设计为基于寄存器架构 +Java 编译器输入的指令流是一种基于栈的指令集架构。因为跨平台的设计,Java 的指令都是根据栈来设计的,不同平台 CPU 架构不同,所以不能设计为基于寄存器架构 * 基于栈式架构的特点: * 设计和实现简单,适用于资源受限的系统 @@ -12336,7 +12336,7 @@ public static void main(String[] args) { Java 语言:跨平台的语言(write once ,run anywhere) -* 当 Java 源代码成功编译成字节码后,在不同的平台上面运行无须再次编译 +* 当 Java 源代码成功编译成字节码后,在不同的平台上面运行**无须再次编译** * 让一个 Java 程序正确地运行在 JVM 中,Java 源码就必须要被编译为符合 JVM 规范的字节码 编译过程中的编译器: @@ -12356,9 +12356,9 @@ Java 语言:跨平台的语言(write once ,run anywhere) * JDK 9 引入,是与即时编译相对立的一个概念,即时编译指的是在程序的运行过程中将字节码转换为机器码,AOT 是程序运行之前便将字节码转换为机器码 - * 优点:Java 虚拟机加载已经预编译成二进制库,可以直接执行,不必等待即时编译器的预热,减少 Java 应用第一次运行慢的现象 + * 优点:JVM 加载已经预编译成二进制库,可以直接执行,不必等待即时编译器的预热,减少 Java 应用第一次运行慢的现象 * 缺点: - * 破坏了 Java "一次编译,到处运行”,必须为每个不同硬件编译对应的发行包 + * 破坏了 Java **一次编译,到处运行**,必须为每个不同硬件编译对应的发行包 * 降低了 Java 链接过程的动态性,加载的代码在编译期就必须全部已知 @@ -12373,7 +12373,7 @@ Java 语言:跨平台的语言(write once ,run anywhere) 机器码:各种用二进制编码方式表示的指令,与 CPU 紧密相关,所以不同种类的 CPU 对应的机器指令不同 -指令:指令就是把机器码中特定的0和1序列,简化成对应的指令,例如mov,inc等,可读性稍好,但是不同的硬件平台的同一种指令(比如mov),对应的机器码也可能不同 +指令:指令就是把机器码中特定的0和1序列,简化成对应的指令,例如 mov,inc 等,可读性稍好,但是不同的硬件平台的同一种指令(比如mov),对应的机器码也可能不同 指令集:不同的硬件平台支持的指令是有区别的,每个平台所支持的指令,称之为对应平台的指令集 @@ -12408,7 +12408,7 @@ Java 语言:跨平台的语言(write once ,run anywhere) 字节码是一种二进制的类文件,是编译之后供虚拟机解释执行的二进制字节码文件,**一个 class 文件对应一个 public 类型的类或接口** -字节码内容是 JVM 的字节码指令,不是机器码,C、C++ 经由编译器直接生成机器码,所以 C 执行效率比 Java 高 +字节码内容是 **JVM 的字节码指令**,不是机器码,C、C++ 经由编译器直接生成机器码,所以 C 执行效率比 Java 高 JVM 官方文档:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html diff --git a/Prog.md b/Prog.md index a4f11b7..58d90f2 100644 --- a/Prog.md +++ b/Prog.md @@ -2488,7 +2488,7 @@ Linux查看CPU缓存行: #### 基本特性 -Volatile是Java虚拟机提供的**轻量级**的同步机制(三大特性) +volatile 是 Java 虚拟机提供的**轻量级**的同步机制(三大特性) - 保证可见性 - 不保证原子性 diff --git a/SSM.md b/SSM.md index aaac61f..bce818e 100644 --- a/SSM.md +++ b/SSM.md @@ -7143,7 +7143,7 @@ public void addAccount{} * 默认情况下,事务只有遇到运行期异常 和 Error 会导致事务回滚,但是在遇到检查型(Checked)异常时不会回滚 - * 继承自 RuntimeException 或 error 的是非检查型异常,比如空指针和索引越界,而继承自 Exception 的则是检查型异常,比如 IOException、ClassNotFoundException + * 继承自 RuntimeException 或 error 的是非检查型异常,比如空指针和索引越界,而继承自 Exception 的则是检查型异常,比如 IOException、ClassNotFoundException,RuntimeException 本身继承 Exception * 非检查型类异常可以不用捕获,而检查型异常则必须用 try 语句块把异常交给上级方法,这样事务才能有效 **事务不生效的问题** @@ -7151,7 +7151,7 @@ public void addAccount{} * 情况 1:确认创建的 mysql 数据库表引擎是 InnoDB,MyISAM 不支持事务 * 情况 2:注解到 protected,private 方法上事务不生效,但不会报错 - 原因:理论上而言,不用public修饰,也可以用 aop 实现 Transactional 的功能,但是方法私有化让其他业务无法调用 + 原因:理论上而言,不用 public 修饰,也可以用 aop 实现事务的功能,但是方法私有化让其他业务无法调用 AopUtils.canApply:`methodMatcher.matches(method, targetClass) --true--> return true` `TransactionAttributeSourcePointcut.matches()` ,AbstractFallbackTransactionAttributeSource 中 getTransactionAttribute 方法调用了其本身的 computeTransactionAttribute 方法,当加了事务注解的方法不是 public 时,该方法直接返回 null,所以造成增强不匹配 @@ -8758,7 +8758,7 @@ public Object invoke(Object proxy, Method method, Object[] args) * `return dm.interceptor.invoke(this)`:匹配成功,执行方法 * `return proceed()`:匹配失败跳过当前拦截器 - * `return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this)`:**所有的方法拦截器都会执行到该方法,然后方法内继续执行 proceed() 完成责任链的构建,直到 MethodBeforeAdviceInterceptor 调用前置通知,然后调用 mi.proceed(),发现是最后一个拦截器了,这里就直接执行目标方法,return 到上一个拦截器的 mi.proceed() 处,依次返回到责任链的上一个拦截器执行通知方法** + * `return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this)`:**所有的方法拦截器都会执行到该方法,此方法内继续执行 proceed() 完成责任链的驱动,直到最后一个 MethodBeforeAdviceInterceptor 调用前置通知,然后调用 mi.proceed(),发现是最后一个拦截器就直接执行目标方法,return 到上一个拦截器的 mi.proceed() 处,依次返回到责任链的上一个拦截器执行通知方法** * `retVal = proxy`:如果目标方法返回目标对象,这里做个普通替换返回代理对象 @@ -8916,7 +8916,7 @@ AutoProxyRegistrar:给容器中注册 InfrastructureAdvisorAutoProxyCreator, ProxyTransactionManagementConfiguration:是一个 Spring 的配置类,注册 BeanFactoryTransactionAttributeSourceAdvisor 事务增强器,利用注解 @Bean 把该类注入到容器中,该增强器有两个字段: -* TransactionAttributeSource:用于解析事务注解的相关信息,比如 @Transactional 注解,该类的真实类型是 AnnotationTransactionAttributeSource,初始化方法种注册了三个解析器,用来解析三种类型的事务注解 Spring、JTA、Ejb3 +* TransactionAttributeSource:用于解析事务注解的相关信息,比如 @Transactional 注解,该类的真实类型是 AnnotationTransactionAttributeSource,初始化方法中注册了三个**注解解析器**,解析三种类型的事务注解 Spring、JTA、Ejb3 * TransactionInterceptor:**事务拦截器**,代理对象执行拦截器方法时,会调用 TransactionInterceptor 的 invoke 方法,底层调用TransactionAspectSupport.invokeWithinTransaction(),通过 PlatformTransactionManager **控制着事务的提交和回滚**,所以事务的底层原理就是通过 AOP 动态织入,进行事务开启和提交 @@ -9002,61 +9002,6 @@ MVC(Model View Controller),一种用于设计创建Web应用程序表现 -## 技术架构 - -### 组件介绍 - -* DispatcherServlet:核心控制器, 是 SpringMVC 的核心,整体流程控制的中心,所有的请求第一步都先到达这里,由其调用其它组件处理用户的请求,它就是在 web.xml 配置的核心 Servlet,有效的降低了组件间的耦合性 - -* HandlerMapping:处理器映射器, 负责根据用户请求找到对应具体的 Handler 处理器,SpringMVC 中针对配置文件方式、注解方式等提供了不同的映射器来处理 - -* Handler:处理器,其实就是 Controller,业务处理的核心类,通常由开发者编写,并且必须遵守 Controller 开发的规则,这样适配器才能正确的执行。例如实现 Controller 接口,将 Controller 注册到 IOC 容器中等 - -* HandlAdapter:处理器适配器,根据映射器中找到的 Handler,通过 HandlerAdapter 去执行 Handler,这是适配器模式的应用 - -* View Resolver:视图解析器, 将 Handler 中返回的逻辑视图(ModelAndView)解析为一个具体的视图(View)对象 - -* View:视图, View 最后对页面进行渲染将结果返回给用户。Springmvc 框架提供了很多的 View 视图类型,包括:jstlView、freemarkerView、pdfView 等 - - ![](https://gitee.com/seazean/images/raw/master/Frame/SpingMVC技术架构.png) - - - -**** - - - -### 工作原理 - -在 Spring 容器初始化时会建立所有的 URL 和 Controller 的对应关系,保存到 Map 中,这样 request 就能快速根据 URL 定位到 Controller。实现: - -1. 在 Spring IOC 容器初始化完所有单例 bean 后 -2. SpringMVC 会遍历所有的 bean,获取 controller 中对应的 URL(这里获取 URL 的实现类有多个,用于处理不同形式配置的 Controller) -3. 将每一个 URL 对应一个 controller 存入 Map 中 - -注意:将 Controller 类的注解换成 @Component,启动时不会报错,但是在浏览器中输入路径时会出现 404,说明 Spring 没有对所有的 bean 进行 URL 映射 - -**一个 Request 来了:** - -1. 监听端口,获得请求:Tomcat 监听 8080 端口的请求,进行接收、解析、封装,根据路径调用了 web.xml 中配置的核心控制器 DispatcherServlet -2. 获取 Handler:进入 DispatcherServlet,核心控制器调用 HandlerMapping 去根据请求的 URL 获取对应的 Handler。这里有个问题,如果获取的 Handler 为 null 则返回 404 -3. 调用适配器执行 Handler: - * 适配器根据 request 的 URL 去 Handler 中寻找对应的处理方法 (Controller 的 URL 与方法的 URL 拼接后对比) - * 获取到对应方法后,需要将 request 中的参数与方法参数上的数据进行绑定,根据反射获取方法的参数名和注解,再根据注解或者根据参数名对照进行绑定(找到对应的参数,然后在反射调用方法时传入) - * 绑定完参数后,反射调用方法获取 ModelAndView(如果 Handler 中返回的是 String、View 等对象,SpringMVC 也会将它们重新封装成一个 ModelAndView) -4. 调用视图解析器解析:将 ModelAndView 解析成 View 对象 -5. 渲染视图:将 View 对象中的返回地址、参数信息等放入 RequestDispatcher,最后进行转发 - - - - - -*** - - - - - ## 基本配置 ### 入门项目 @@ -10188,6 +10133,14 @@ Restful 请求路径简化配置方式:`@RestController = @Controller + @Respo * `@GetMapping("/poll")` = `@RequestMapping(value = "/poll",method = RequestMethod.GET)` + ```java + @RequestMapping(method = RequestMethod.GET) + public @interface GetMapping { + @AliasFor(annotation = RequestMapping.class) //与 RequestMapping 相通 + String name() default ""; + } + ``` + * `@PostMapping("/push")` = `@RequestMapping(value = "/push",method = RequestMethod.POST)` @@ -10399,28 +10352,7 @@ public class WebConfig{ ### Servlet -SpringMVC 提供访问原始 Servlet 接口的功能,这一部分是补充知识,不常用 - -* spring-mvc.xml 配置 - - ```xml - - - - - - - - - - - ``` +SpringMVC 提供访问原始 Servlet 接口的功能 * SpringMVC 提供访问原始 Servlet 接口 API 的功能,通过形参声明即可 @@ -10518,172 +10450,1143 @@ SpringMVC 提供访问原始 Servlet 接口的功能,这一部分是补充知 } ``` +* spring-mvc.xml 配置 + + ```xml + + + + + + + + + + + ``` + +*** + + + +## 运行原理 + +### 技术架构 + +#### 组件介绍 + +* DispatcherServlet:核心控制器, 是 SpringMVC 的核心,整体流程控制的中心,所有的请求第一步都先到达这里,由其调用其它组件处理用户的请求,它就是在 web.xml 配置的核心 Servlet,有效的降低了组件间的耦合性 + +* HandlerMapping:处理器映射器, 负责根据用户请求找到对应具体的 Handler 处理器,SpringMVC 中针对配置文件方式、注解方式等提供了不同的映射器来处理 + +* Handler:处理器,其实就是 Controller,业务处理的核心类,通常由开发者编写,并且必须遵守 Controller 开发的规则,这样适配器才能正确的执行。例如实现 Controller 接口,将 Controller 注册到 IOC 容器中等 + +* HandlAdapter:处理器适配器,根据映射器中找到的 Handler,通过 HandlerAdapter 去执行 Handler,这是适配器模式的应用 + +* View Resolver:视图解析器, 将 Handler 中返回的逻辑视图(ModelAndView)解析为一个具体的视图(View)对象 + +* View:视图, View 最后对页面进行渲染将结果返回给用户。Springmvc 框架提供了很多的 View 视图类型,包括:jstlView、freemarkerView、pdfView 等 + + ![](https://gitee.com/seazean/images/raw/master/Frame/SpingMVC技术架构.png) + + **** +#### 工作原理 +在 Spring 容器初始化时会建立所有的 URL 和 Controller 的对应关系,保存到 Map 中,这样 request 就能快速根据 URL 定位到 Controller: -## 异步调用 +* 在 Spring IOC 容器初始化完所有单例 bean 后 +* SpringMVC 会遍历所有的 bean,获取 controller 中对应的 URL(这里获取 URL 的实现类有多个,用于处理不同形式配置的 Controller) +* 将每一个 URL 对应一个 controller 存入 Map 中 -### 请求参数 +注意:将 Controller 类的注解换成 @Component,启动时不会报错,但是在浏览器中输入路径时会出现 404,说明 Spring 没有对所有的 bean 进行 URL 映射 -名称:@RequestBody -类型:形参注解 -位置:处理器类中的方法形参前方 -作用:将异步提交数据**转换**成标准请求参数格式,并赋值给形参 -范例: +**一个 Request 来了:** -```java -@Controller //控制层 -public class AjaxController { - @RequestMapping("/ajaxController") - public String ajaxController(@RequestBody String message){ - System.out.println(message); - return "page.jsp"; - } -} -``` +* 监听端口,获得请求:Tomcat 监听 8080 端口的请求,进行接收、解析、封装,根据路径调用了 web.xml 中配置的核心控制器 DispatcherServlet +* 获取 Handler:进入 DispatcherServlet,核心控制器调用 HandlerMapping 去根据请求的 URL 获取对应的 Handler,如果获取的 Handler 为 null 则返回 404 +* 调用适配器执行 Handler: + * 适配器根据 request 的 URL 去 Handler 中寻找对应的处理方法(Controller 的 URL 与方法的 URL 拼接后对比) + * 获取到对应方法后,需要将 request 中的参数与方法参数上的数据进行绑定,根据反射获取方法的参数名和注解,再根据注解或者根据参数名对照进行绑定(找到对应的参数,然后在反射调用方法时传入) + * 绑定完参数后,反射调用方法获取 ModelAndView(如果 Handler 中返回的是 String、View 等对象,SpringMVC 也会将它们重新封装成一个 ModelAndView) +* 调用视图解析器解析:将 ModelAndView 解析成 View 对象 +* 渲染视图:将 View 对象中的返回地址、参数信息等放入 RequestDispatcher,最后进行转发 -* 注解添加到 Pojo 参数前方时,封装的异步提交数据按照 Pojo 的属性格式进行关系映射 - * POJO 中的属性如果请求数据中没有,属性值为 null - * POJO 中没有的属性如果请求数据中有,不进行映射 -* 注解添加到集合参数前方时,封装的异步提交数据按照集合的存储结构进行关系映射 -```java -@RequestMapping("/ajaxPojoToController") -//如果处理参数是POJO,且页面发送的请求数据格式与POJO中的属性对应,@RequestBody注解可以自动映射对应请求数据到POJO中 -public String ajaxPojoToController(@RequestBody User user){ - System.out.println("controller pojo :"+user); - return "page.jsp"; -} -@RequestMapping("/ajaxListToController") -//如果处理参数是List集合且封装了POJO,且页面发送的数据是JSON格式,数据将自动映射到集合参数 -public String ajaxListToController(@RequestBody List userList){ - System.out.println("controller list :"+userList); - return "page.jsp"; -} -``` +**** -ajax.jsp -```html -<%@page pageEncoding="UTF-8" language="java" contentType="text/html;UTF-8" %> -访问springmvc后台controller
-传递Json格式POJO
-传递Json格式List
- - - + //.... +} ``` -web.xml配置:请求响应章节请求中的web.xml配置 -```xml -CharacterEncodingFilter + DispatcherServlet -``` -spring-mvc.xml: +笔记参考视频:https://www.bilibili.com/video/BV19K4y1L7MT + -```xml - - - -``` +*** -**** +### 请求映射 +#### 映射器 -### 响应数据 +doDispatch() 中调用 getHandler 方法获取所有的映射器 -注解:@ResponseBody -作用:将 java 对象转为 json 格式的数据 +总体流程: -方法返回值为 POJO 时,自动封装数据成 Json 对象数据: +* 所有的请求映射都在 HandlerMapping 中,**RequestMappingHandlerMapping 处理 @RequestMapping 注解的映射规则** + +* 遍历所有的 HandlerMapping 看是否可以匹配当前请求,匹配成功后返回,匹配失败设置 HTTP 404 响应码 +* 用户可以自定义的映射处理,也可以给容器中放入自定义 HandlerMapping + +访问 URL:http://localhost:8080/user ```java -@RequestMapping("/ajaxReturnJson") -@ResponseBody -public User ajaxReturnJson(){ - System.out.println("controller return json pojo..."); - User user = new User("Jockme",40); - return user; -} +@GetMapping("/user") +public String getUser(){ + return "GET"; +} +@PostMapping("/user") +public String postUser(){ + return "POST"; +} +//。。。。。 ``` -方法返回值为 List 时,自动封装数据成 json 对象数组数据: +HandlerMapping 处理器映射器,保存了所有 `@RequestMapping` 和 `handler` 的映射规则 ```java -@RequestMapping("/ajaxReturnJsonList") -@ResponseBody -//基于jackon技术,使用@ResponseBody注解可以将返回的保存POJO对象的集合转成json数组格式数据 -public List ajaxReturnJsonList(){ - System.out.println("controller return json list..."); - User user1 = new User("Tom",3); - User user2 = new User("Jerry",5); - - ArrayList al = new ArrayList(); - al.add(user1); - al.add(user2); - return al; +protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { + if (this.handlerMappings != null) { + //遍历所有的 HandlerMapping + for (HandlerMapping mapping : this.handlerMappings) { + //尝试去每个 HandlerMapping 中匹配当前请求的处理 + HandlerExecutionChain handler = mapping.getHandler(request); + if (handler != null) { + return handler; + } + } + } + return null; } ``` -AJAX 文件: +![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-获取Controller处理器.png) -```js -//为id="testAjaxReturnString"的组件绑定点击事件 -$("#testAjaxReturnString").click(function(){ - //发送异步调用 - $.ajax({ - type:"POST", - url:"ajaxReturnString", +* `mapping.getHandler(request)`:调用 AbstractHandlerMapping#getHandler + + * `Object handler = getHandlerInternal(request)`:获取映射器,底层调用 RequestMappingInfoHandlerMapping 类的方法,又调用 AbstractHandlerMethodMapping#getHandlerInternal + + * `String lookupPath = initLookupPath(request)`:地址栏的 uri,这里的 lookupPath 为 /user + + * `this.mappingRegistry.acquireReadLock()`:防止并发 + + * `handlerMethod = lookupHandlerMethod(lookupPath, request)`:获取当前 HandlerMapping 中的映射规则 + + AbstractHandlerMethodMapping.lookupHandlerMethod(): + + * `directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath)`:获取当前的映射器与当前**请求的 URI 有关的所有映射规则** + + ![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-HandlerMapping的映射规则.png) + + * `addMatchingMappings(directPathMatches, matches, request)`:**匹配某个映射规则** + + * `for (T mapping : mappings)`:遍历所有的映射规则 + * `match = getMatchingMapping(mapping, request)`:去匹配每一个映射规则,匹配失败返回 null + * `matches.add(new Match())`:匹配成功后封装成匹配器添加到匹配集合中 + + * `Match bestMatch = matches.get(0)`:匹配完成只剩一个,直接获取返回对应的处理方法 + + * `if (matches.size() > 1)`:当有多个映射规则符合请求时,报错 + + * `return bestMatch.getHandlerMethod()`:返回匹配器中的处理方法 + + * `executionChain = getHandlerExecutionChain(handler, request)`:**为当前请求和映射器的构建一个拦截器链** + + * `for (HandlerInterceptor interceptor : this.adaptedInterceptors)`:遍历所有的拦截器 + * `chain.addInterceptor(interceptor)`:把所有的拦截器添加到 HandlerExecutionChain 中,形成拦截器链 + + * `return executionChain`:**返回拦截器链,包含 HandlerMapping 和拦截方法** + + + +**** + + + +#### 适配器 + +doDispatch() 中 调用 `HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler())` + +```java +protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException { + if (this.handlerAdapters != null) { + //遍历所有的 HandlerAdapter + for (HandlerAdapter adapter : this.handlerAdapters) { + //判断当前适配器是否支持当前 handle + //return (handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler)) + //这里返回的是True, + if (adapter.supports(handler)) { + //返回的是 RequestMappingHandlerAdapter + return adapter; + } + } + } + throw new ServletException(); +} +``` + + + +*** + + + +#### 方法执行 + +##### 执行流程 + +实例代码: + +```java +@GetMapping("/params") +public String param(Map map, Model model, HttpServletRequest request) { + map.put("k1", "v1"); //都可以向请求域中添加数据 + model.addAttribute("k2", "v2"); //它们两个都在数据封装在 BindingAwareModelMap + request.setAttribute("m", "HelloWorld"); + return "forward:/success"; +} +``` + +![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-Model和Map的数据解析.png) + +doDispatch() 中调用 `mv = ha.handle(processedRequest, response, mappedHandler.getHandler())` 执行目标方法 + +`AbstractHandlerMethodAdapter#handle` → `RequestMappingHandlerAdapter#handleInternal` → `invokeHandlerMethod`: + +```java +//使用适配器执行方法 +protected ModelAndView invokeHandlerMethod(HttpServletRequest request, + HttpServletResponse response, + HandlerMethod handlerMethod) throws Exception { + //封装成 SpringMVC 的接口,用于通用 Web 请求拦截器,使能够访问通用请求元数据,而不是用于实际处理请求 + ServletWebRequest webRequest = new ServletWebRequest(request, response); + try { + //WebDataBinder 用于从 Web 请求参数到 JavaBean 对象的数据绑定,获取创建该实例的工厂 + WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod); + //创建 Model 实例,用于向模型添加属性 + ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory); + //方法执行器 + ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod); + + //参数解析器,有很多 + if (this.argumentResolvers != null) { + invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); + } + //返回值处理器,也有很多 + if (this.returnValueHandlers != null) { + invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); + } + //设置数据绑定器 + invocableMethod.setDataBinderFactory(binderFactory); + //设置参数检查器 + invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer); + + //新建一个 ModelAndViewContainer 并进行初始化和一些属性的填充 + ModelAndViewContainer mavContainer = new ModelAndViewContainer(); + + //设置一些属性 + + //执行目标方法 + invocableMethod.invokeAndHandle(webRequest, mavContainer); + //异步请求 + if (asyncManager.isConcurrentHandlingStarted()) { + return null; + } + // 获取 ModelAndView 对象,封装了 ModelAndViewContainer + return getModelAndView(mavContainer, modelFactory, webRequest); + } + finally { + webRequest.requestCompleted(); + } +} +``` + +**ServletInvocableHandlerMethod#invokeAndHandle**:执行目标方法 + +* `returnValue = invokeForRequest(webRequest, mavContainer, providedArgs)`:**执行自己写的 controller 方法,返回的就是自定义方法中 return 的值** + + `Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs)`:**参数处理的逻辑**,遍历所有的参数解析器解析参数或者将 URI 中的参数进行绑定,绑定完成后开始执行目标方法 + + * `parameters = getMethodParameters()`:获取此处理程序方法的方法参数的详细信息 + + * `Object[] args = new Object[parameters.length]`:存放所有的参数 + + * `for (int i = 0; i < parameters.length; i++)`:遍历所有的参数 + + * `args[i] = findProvidedArgument(parameter, providedArgs)`:获取调用方法时提供的参数,一般是空 + + * `if (!this.resolvers.supportsParameter(parameter))`:**获取可以解析当前参数的参数解析器** + + * `return getArgumentResolver(parameter) != null`:获取参数的解析是否为空 + + * `for (HandlerMethodArgumentResolver resolver : this.argumentResolvers)`:遍历容器内所有的解析器 + + `if (resolver.supportsParameter(parameter))`:是否支持当前参数 + + * `PathVariableMethodArgumentResolver#supportsParameter`:**解析标注 @PathVariable 注解的参数** + * `ModelMethodProcessor#supportsParameter`:解析 Map 类型的参数 + * `ModelMethodProcessor#supportsParameter`:解析 Model 类型的参数,Model 和 Map 的作用一样 + * `ExpressionValueMethodArgumentResolver#supportsParameter`:解析标注 @Value 注解的参数 + * `RequestParamMapMethodArgumentResolver#supportsParameter`:**解析标注 @RequestParam 注解** + * `RequestPartMethodArgumentResolver#supportsParameter`:解析文件上传的信息 + * `ModelAttributeMethodProcessor#supportsParameter`:解析标注 @ModelAttribute 注解或者不是简单类型 + * 子类 ServletModelAttributeMethodProcessor 是**解析自定义类型 JavaBean 的解析器** + * 简单类型有 Void、Enum、Number、CharSequence、Date、URI、URL、Locale、Class + + * `args[i] = this.resolvers.resolveArgument()`:**开始解析参数,每个参数使用的解析器不同** + + `resolver = getArgumentResolver(parameter)`:获取参数解析器 + + `return resolver.resolveArgument()`:开始解析 + + * `PathVariableMapMethodArgumentResolver#resolveArgument`:@PathVariable,包装 URI 中的参数为 Map + * `MapMethodProcessor#resolveArgument`:调用 `mavContainer.getModel()` 返回默认的 BindingAwareModelMap 对象 + * `ModelAttributeMethodProcessor#resolveArgument`:**自定义的 JavaBean 的绑定封装**,下一小节详解 + + `return doInvoke(args)`:真正的执行方法 + + * `Method method = getBridgedMethod()`:从 HandlerMethod 获取要反射执行的方法 +* `ReflectionUtils.makeAccessible(method)`:破解权限 + * `method.invoke(getBean(), args)`:**执行方法**,getBean 获取的是标记 @Controller 的 Bean 类,其中包含执行方法 + +* **进行返回值的处理,响应部分详解**,处理完成进入下面的逻辑 + +**RequestMappingHandlerAdapter#getModelAndView**:获取 ModelAndView 对象 + +* `modelFactory.updateModel(webRequest, mavContainer)`:Model 数据升级到会话域(**请求域中的数据在重定向时丢失**) + + `updateBindingResult(request, defaultModel)`:把绑定的数据添加到 Model 中 + +* `if (mavContainer.isRequestHandled())`:判断请求是否已经处理完成了 + +* `ModelMap model = mavContainer.getModel()`:获取**包含 Controller 方法参数**的 BindingAwareModelMap 对象(本节开头) + +* `mav = new ModelAndView()`:**把 ModelAndViewContainer 和 ModelMap 中的数据封装到 ModelAndView** + +* `if (!mavContainer.isViewReference())`:视图是否是通过名称指定视图引用 + +* `if (model instanceof RedirectAttributes)`:判断 model 是否是重定向数据,如果是进行重定向逻辑 + +* `return mav`:**任何方法执行都会返回 ModelAndView 对象** + + + +*** + + + +##### 参数解析 + +解析自定义的 JavaBean 为例 + +* Person.java: + + ```java + @Data + @Component //加入到容器中 + public class Person { + private String userName; + private Integer age; + private Date birth; + } + ``` + +* Controller: + + ```java + @RestController //返回的数据不是页面 + public class ParameterController { + // 数据绑定:页面提交的请求数据(GET、POST)都可以和对象属性进行绑定 + @GetMapping("/saveuser") + public Person saveuser(Person person){ + return person; + } + } + ``` + +* 访问 URL:http://localhost:8080/saveuser?userName=zhangsan&age=20 + +进入源码:ModelAttributeMethodProcessor#resolveArgument + +* `name = ModelFactory.getNameForParameter(parameter)`:获取名字,此例就是 person + +* `ann = parameter.getParameterAnnotation(ModelAttribute.class)`:是否有 ModelAttribute 注解 + +* `if (mavContainer.containsAttribute(name))`:ModelAndViewContainer 中是否包含 person 对象 + +* `attribute = createAttribute()`:**创建一个实例,空的 Person 对象** + +* `binder = binderFactory.createBinder(webRequest, attribute, name)`:Web 数据绑定器,可以利用 Converters 将请求数据转成指定的数据类型,绑定到 JavaBean 中 + +* `bindRequestParameters(binder, webRequest)`:利用反射向目标对象填充数据 + + `servletBinder = (ServletRequestDataBinder) binder`:类型强转 + + `servletBinder.bind(servletRequest)`:绑定数据 + + * `mpvs = new MutablePropertyValues(request.getParameterMap())`:获取请求 URI 参数中的 KV 键值对 + + * `addBindValues(mpvs, request)`:子类可以用来为请求添加额外绑定值 + + * `doBind(mpvs)`:真正的绑定的方法,调用 `applyPropertyValues` 应用参数值,然后调用 `setPropertyValues` 方法 + + `AbstractPropertyAccessor#setPropertyValues()`: + + * `List propertyValues`:获取到所有的参数的值,就是 URI 上的所有的参数值 + + * `for (PropertyValue pv : propertyValues)`:遍历所有的参数值 + + * `setPropertyValue(pv)`:**填充到空的 Person 实例中** + + * `nestedPa = getPropertyAccessorForPropertyPath(propertyName)`:获取属性访问器 + + * `tokens = getPropertyNameTokens()`:获取元数据的信息 + + * `nestedPa.setPropertyValue(tokens, pv)`:填充数据 + + * `processLocalProperty(tokens, pv)`:处理属性 + + * `if (!Boolean.FALSE.equals(pv.conversionNecessary))`:数据是否需要转换了 + + * `if (pv.isConverted())`:数据已经转换过了,转换了直接赋值,没转换进行转换 + + * `oldValue = ph.getValue()`:获取未转换的数据 + + * `valueToApply = convertForProperty()`:进行数据转换 + + `TypeConverterDelegate#convertIfNecessary`:进入该方法的逻辑 + + * `if (conversionService.canConvert(sourceTypeDesc, typeDescriptor))`:判断能不能转换 + + `GenericConverter converter = getConverter(sourceType, targetType)`:获取**类型转换器** + + * `converter = this.converters.find(sourceType, targetType)`:寻找合适的转换器 + + * `sourceCandidates = getClassHierarchy(sourceType.getType())`:原数据类型 + + * `targetCandidates = getClassHierarchy(targetType.getType())`:目标数据类型 + + ```java + for (Class sourceCandidate : sourceCandidates) { + //双重循环遍历,寻找合适的转换器 + for (Class targetCandidate : targetCandidates) { + ``` + + * `GenericConverter converter = getRegisteredConverter(..)`:匹配类型转换器 + + * `return converter`:返回转换器 + + * `conversionService.convert(newValue, sourceTypeDesc, typeDescriptor)`:开始转换 + + * `converter = getConverter(sourceType, targetType)`:**获取可用的转换器** + * `result = ConversionUtils.invokeConverter()`:执行转换方法 + * `converter.convert()`:**调用转换器的转换方法**(GenericConverter#convert) + * `return handleResult(sourceType, targetType, result)`:返回结果 + + * `ph.setValue(valueToApply)`:设置 JavaBean 属性(BeanWrapperImpl.BeanPropertyHandler) + + * `Method writeMethod`:获取 set 方法 + * `Class cls = getClass0()`:获取 Class 对象 + * `writeMethodName = Introspector.SET_PREFIX + getBaseName()`:set 前缀 + 属性名 + * `writeMethod = Introspector.findMethod(cls, writeMethodName, 1, args)`:获取只包含一个参数的 set 方法 + * `setWriteMethod(writeMethod)`:加入缓存 + * `ReflectionUtils.makeAccessible(writeMethod)`:设置访问权限 + * `writeMethod.invoke(getWrappedInstance(), value)`:执行方法 + +* `bindingResult = binder.getBindingResult()`:获取绑定的结果 + +* `mavContainer.addAllAttributes(bindingResultModel)`:**把所有填充的参数放入 ModelAndViewContainer** + +* `return attribute`:返回填充后的 Person 对象 + + + + + +**** + + + +### 响应处理 + +#### 响应数据 + +以 Person 为例: + +```java +@ResponseBody //利用返回值处理器里面的消息转换器进行处理 +@GetMapping(value = "/person") +public Person getPerson(){ + Person person = new Person(); + person.setAge(28); + person.setBirth(new Date()); + person.setUserName("zhangsan"); + return person; +} +``` + +直接进入方法执行完后的逻辑 ServletInvocableHandlerMethod#invokeAndHandle: + +```java +public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, + Object... providedArgs) throws Exception { + // 执行目标方法,return person 对象 + Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); + // 设置状态码 + setResponseStatus(webRequest); + + // 判断方法是否有返回值 + if (returnValue == null) { + if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) { + disableContentCachingIfNecessary(webRequest); + mavContainer.setRequestHandled(true); + return; + } + } //返回值是字符串 + else if (StringUtils.hasText(getResponseStatusReason())) { + //设置请求处理完成 + mavContainer.setRequestHandled(true); + return; + // 设置请求没有处理完成,还需要进行返回值的逻辑 + mavContainer.setRequestHandled(false); + Assert.state(this.returnValueHandlers != null, "No return value handlers"); + try { + // 返回值的处理 + this.returnValueHandlers.handleReturnValue( + returnValue, getReturnValueType(returnValue), mavContainer, webRequest); + } + catch (Exception ex) {} +} +``` + +* 没有加 @ResponseBody 注解的返回数据按照视图(页面)处理的逻辑,ViewNameMethodReturnValueHandler(视图详解) + +* 此例是加了注解的,返回的数据不是视图,HandlerMethodReturnValueHandlerComposite#handleReturnValue: + + ```java + public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, + ModelAndViewContainer mavContainer, NativeWebRequest webRequest) { + //获取合适的返回值处理器 + HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType); + if (handler == null) { + throw new IllegalArgumentException(); + } + //使用处理器处理返回值(详解源码中的这两个函数) + handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest); + } + ``` + +**HandlerMethodReturnValueHandlerComposite#selectHandler**: + +* `boolean isAsyncValue = isAsyncReturnValue(value, returnType)`:是否是异步请求 + +* `for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers)`:遍历所有的返回值处理器 + * `RequestResponseBodyMethodProcessor#supportsReturnType`:**处理标注 @ResponseBody 注解的返回值** + * `ModelAndViewMethodReturnValueHandler#supportsReturnType`:处理**返回值类型**是 ModelAndView 的处理器 + * `ModelAndViewResolverMethodReturnValueHandler#supportsReturnType`:直接返回 true,处理所有数据 + +**RequestResponseBodyMethodProcessor#handleReturnValue**:处理返回值 + +* `mavContainer.setRequestHandled(true)`:设置请求处理完成 + +* `inputMessage = createInputMessage(webRequest)`:获取输入的数据 + +* `outputMessage = createOutputMessage(webRequest)`:获取输出的数据 + +* `writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage)`:使用消息转换器进行写出 + + * `if (value instanceof CharSequence)`:判断返回的数据是不是字符类型 + + * `body = value`:把 value 赋值给 body,此时 body 中就是填充后的 Person 对象 + + * `if (isResourceType(value, returnType))`:当前数据是不是流数据 + + * `MediaType selectedMediaType`:**内容协商后选择使用的类型,浏览器和服务器都支持的媒体(数据)类型** + + `MediaType contentType = outputMessage.getHeaders().getContentType()`:获取响应头的数据 + + * `if (contentType != null && contentType.isConcrete())`:判断当前响应头中是否已经有确定的媒体类型 + + `selectedMediaType = contentType`:说明前置处理已经使用了媒体类型,直接继续使用该类型 + + * `acceptableTypes = getAcceptableMediaTypes(request)`:**获取浏览器支持的媒体类型,请求头字段** + + `this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request))`:调用该方法 + + * `for(ContentNegotiationStrategy strategy:this.strategies)`:默认策略是提取请求头的字段的内容,策略类为**HeaderContentNegotiationStrategy**,可以配置添加其他类型的策略 + * `List mediaTypes = strategy.resolveMediaTypes(request)`:解析 Accept 字段存储为 List + * `headerValueArray = request.getHeaderValues(HttpHeaders.ACCEPT)`:获取请求头中 Accept 字段 + * `List mediaTypes = MediaType.parseMediaTypes(headerValues)`:解析成 List 集合 + * `MediaType.sortBySpecificityAndQuality(mediaTypes)`:按照相对品质因数 q 降序排序 + + ![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-浏览器支持接收的数据类型.png) + + * `producibleTypes = getProducibleMediaTypes(request, valueType, targetType)`:服务器能生成的媒体类型 + + * `request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE)`:从请求域获取默认的媒体类型 + * ` for (HttpMessageConverter converter : this.messageConverters)`:遍历所有的消息转换器 + * `converter.canWrite(valueClass, null)`:是否支持当前的类型 + * ` result.addAll(converter.getSupportedMediaTypes())`:把当前 MessageConverter 支持的所有类型放入 result + + * `List mediaTypesToUse = new ArrayList<>()`:存储最佳匹配 + + * **内容协商:** + + ```java + for (MediaType requestedType : acceptableTypes) { //遍历所有的浏览器能接受的媒体类型 + for (MediaType producibleType : producibleTypes) { //遍历所有服务器能产出的 + if (requestedType.isCompatibleWith(producibleType)) { //判断类型是否匹配,最佳匹配 + //数据协商匹配成功 + mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType)); + } + } + } + ``` + + * `MediaType.sortBySpecificityAndQuality(mediaTypesToUse)`:按照相对品质因数 q 排序,降序排序,越大的越好 + + * `for (MediaType mediaType : mediaTypesToUse)`:**遍历所有的最佳匹配** + + `selectedMediaType = mediaType`:赋值给选择的类型 + + * `selectedMediaType = selectedMediaType.removeQualityValue()`:媒体类型去除相对品质因数 + + * `for (HttpMessageConverter converter : this.messageConverters)`:遍历所有的 HTTP 数据转换器 + + * `GenericHttpMessageConverter genericConverter`:**MappingJackson2HttpMessageConverter 可以将对象写为 JSON** + + * `((GenericHttpMessageConverter) converter).canWrite()`:转换器是否可以写出给定的类型 + + `AbstractJackson2HttpMessageConverter#canWrit` + + * `if (!canWrite(mediaType))`:是否可以写出指定类型 + * `MediaType.ALL.equalsTypeAndSubtype(mediaType)`:是不是 `*/*` 类型 + * `getSupportedMediaTypes()`:支持 `application/json` 和 `application/*+json` 两种类型 + * `return true`:返回 true + * `objectMapper = selectObjectMapper(clazz, mediaType)`:选择可以使用的 objectMapper + * `causeRef = new AtomicReference<>()`:获取并发安全的引用 + * `if (objectMapper.canSerialize(clazz, causeRef))`:objectMapper 可以序列化当前类 + * `return true`:返回 true + + * ` body = getAdvice().beforeBodyWrite()`:**要响应的所有数据,Person 对象** + + * `addContentDispositionHeader(inputMessage, outputMessage)`:检查路径 + + * `genericConverter.write(body, targetType, selectedMediaType, outputMessage)`:调用消息转换器的 write 方法 + + `AbstractGenericHttpMessageConverter#write`:该类的方法 + + * `addDefaultHeaders(headers, t, contentType)`:设置响应头中的数据类型 + + ![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-服务器设置数据类型.png) + + * `writeInternal(t, type, outputMessage)`:**真正的写出数据的函数** + + * `Object value = object`:value 引用 Person 对象 + * `ObjectWriter objectWriter = objectMapper.writer()`:获取用来输出 JSON 对象的 ObjectWriter + * `objectWriter.writeValue(generator, value)`:写出数据为 JSON + + + + + +*** + + + +#### 协商策略 + +开启基于请求参数的内容协商模式:(SpringBoot 方式) + +```yaml +spring.mvc.contentnegotiation:favor-parameter: true #开启请求参数内容协商模式 +``` + +发请求: http://localhost:8080/person?format=json,解析 format + +策略类为 ParameterContentNegotiationStrategy,运行流程如下: + +* `acceptableTypes = getAcceptableMediaTypes(request)`:获取浏览器支持的媒体类型 + + `mediaTypes = strategy.resolveMediaTypes(request)`:解析请求 URL 参数中的数据 + + * `return resolveMediaTypeKey(webRequest, getMediaTypeKey(webRequest))`: + + `getMediaTypeKey(webRequest)`: + + * `request.getParameter(getParameterName())`:获取 URL 中指定的需求的数据类型 + * `getParameterName()`:获取参数的属性名 format + * `getParameter()`:获取 URL 中 format 对应的数据 + + `resolveMediaTypeKey()`:解析媒体类型,封装成集合 + +自定义内容协商策略: + +```java +public class WebConfig implements WebMvcConfigurer { + @Bean + public WebMvcConfigurer webMvcConfigurer() { + return new WebMvcConfigurer() { + @Override //自定义内容协商策略 + public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { + Map mediaTypes = new HashMap<>(); + mediaTypes.put("json", MediaType.APPLICATION_JSON); + mediaTypes.put("xml",MediaType.APPLICATION_XML); + mediaTypes.put("person",MediaType.parseMediaType("application/x-person")); + //指定支持解析哪些参数对应的哪些媒体类型 + ParameterContentNegotiationStrategy parameterStrategy = new ParameterContentNegotiationStrategy(mediaTypes); + + //请求头解析 + HeaderContentNegotiationStrategy headStrategy = new HeaderContentNegotiationStrategy(); + + //添加到容器中,即可以解析请求头 又可以解析请求参数 + configurer.strategies(Arrays.asList(parameterStrategy,headStrategy)); + } + + @Override //自定义消息转换器 + public void extendMessageConverters(List> converters) { + converters.add(new GuiguMessageConverter()); + } + } + } +} +``` + +也可以自定义 HttpMessageConverter,实现 HttpMessageConverter 接口重写方法即可 + + + +*** + + + +### 视图解析 + +#### 返回解析 + +请求处理: + +```java +@GetMapping("/params") +public String param(){ + return "forward:/success"; + //return "redirect:/success"; +} +``` + +进入执行方法逻辑 ServletInvocableHandlerMethod#invokeAndHandle,进入 `this.returnValueHandlers.handleReturnValue`: + +```java +public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, + ModelAndViewContainer mavContainer, NativeWebRequest webRequest) { + //获取合适的返回值处理器:调用 if (handler.supportsReturnType(returnType))判断是否支持 + HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType); + if (handler == null) { + throw new IllegalArgumentException(); + } + //使用处理器处理返回值 + handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest); +} +``` + +* ViewNameMethodReturnValueHandler#supportsReturnType + + ```java + public boolean supportsReturnType(MethodParameter returnType) { + Class paramType = returnType.getParameterType(); + //返回值是否是void 或者 是 CharSequence 字符序列 + return (void.class == paramType || CharSequence.class.isAssignableFrom(paramType)); + } + ``` + +* ViewNameMethodReturnValueHandler#handleReturnValue + + ```java + public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest) throws Exception { + // 返回值是字符串,是 return "forward:/success" + if (returnValue instanceof CharSequence) { + String viewName = returnValue.toString(); + //把视图名称设置进入 ModelAndViewContainer 中 + mavContainer.setViewName(viewName); + //判断是否是重定向数据 `viewName.startsWith("redirect:")` + if (isRedirectViewName(viewName)) { + //如果是重定向,设置是重定向指令 + mavContainer.setRedirectModelScenario(true); + } + } + else if (returnValue != null) { + // should not happen + throw new UnsupportedOperationException(); + } + } + ``` + + + + + +*** + + + +#### 结果派发 + +doDispatch()中的 processDispatchResult:处理派发结果 + +```java +private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, + @Nullable HandlerExecutionChain mappedHandler, + @Nullable ModelAndView mv, + @Nullable Exception exception) throws Exception { + boolean errorView = false; + if (exception != null) { + } + // mv 是 ModelAndValue + if (mv != null && !mv.wasCleared()) { + // 渲染视图 + render(mv, request, response); + if (errorView) { + WebUtils.clearErrorRequestAttributes(request); + } + } + else {} +} +``` + +DispatcherServlet#render: + +* `Locale locale = this.localeResolver.resolveLocale(request)`:国际化相关 + +* `String viewName = mv.getViewName()`:视图名字,是请求转发 forward:/success(响应数据部分解析了该名字存入 ModelAndView 是**通过 ViewNameMethodReturnValueHandler**) + +* `view = resolveViewName(viewName, mv.getModelInternal(), locale, request)`:解析视图 + + * `for (ViewResolver viewResolver : this.viewResolvers)`:遍历所有的视图解析器 + + `view = viewResolver.resolveViewName(viewName, locale)`:根据视图名字解析视图,调用内容协商视图处理器 ContentNegotiatingViewResolver 的方法 + + * `attrs = RequestContextHolder.getRequestAttributes()`:获取请求的相关属性信息 + + * `requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest())`:获取最佳匹配的媒体类型,函数内进行了匹配的逻辑 + + * `candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes)`:获取候选的视图对象 + + * `for (ViewResolver viewResolver : this.viewResolvers)`:遍历所有的视图解析器 + + * `View view = viewResolver.resolveViewName(viewName, locale)`:解析视图 + + `AbstractCachingViewResolver#resolveViewName`:调用此方法 + + **请求转发**:实例为 InternalResourceView + + * `returnview = createView(viewName, locale)`:UrlBasedViewResolver#createView + + * `if (viewName.startsWith(FORWARD_URL_PREFIX))`:视图名字是否是 **`forward:`** 的前缀 + * `forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length())`:**名字截取前缀** + * `view = new InternalResourceView(forwardUrl)`:新建 InternalResourceView 对象并返回 + + * `return applyLifecycleMethods(FORWARD_URL_PREFIX, view)`:Spring 中的初始化操作 + + **重定向**:实例为 RedirectView + + * `returnview = createView(viewName, locale)`:UrlBasedViewResolver#createView + * `if (viewName.startsWith(REDIRECT_URL_PREFIX))`:视图名字是否是 **`redirect:`** 的前缀 + * `redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length())`:名字截取前缀 + * `RedirectView view = new RedirectView()`:新建 RedirectView 对象并返回 + + * `bestView = getBestView(candidateViews, requestedMediaTypes, attrs)`:选出最佳匹配的视图对象 + +* `view.render(mv.getModelInternal(), request, response)`:**页面渲染** + + * `mergedModel = createMergedOutputModel(model, request, response)`:把请求域中的数据封装到 Map + + * `prepareResponse(request, response)`:响应前的准备工作,设置一些响应头 + + * `renderMergedOutputModel(mergedModel, getRequestToExpose(request), response)`:渲染输出的数据 + + **请求转发** InternalResourceView 的逻辑: + + * `getRequestToExpose(request)`:获取 Servlet 原生的方式 + * `exposeModelAsRequestAttributes(model, request)`:暴露 model 作为请求域的属性 + * `model.forEach()`:遍历 Model 中的数据 + * `request.setAttribute(name, value)`:设置到请求域中 + * `exposeHelpers(request)`:自定义接口 + * `dispatcherPath = prepareForRendering(request, response)`:确定调度分派的路径,此例是 /success + * `rd = getRequestDispatcher(request, dispatcherPath)`:**获取 Servlet 原生的 RequestDispatcher 实现转发** + * `rd.forward(request, response)`:实现请求转发 + + **重定向** RedirectView 的逻辑: + + * `targetUrl = createTargetUrl(model, request)`:获取目标 URL + * `enc = request.getCharacterEncoding()`:设置编码 UTF-8 + * `appendQueryProperties(targetUrl, model, enc)`:添加一些属性,比如 `url + ?name=123&&age=324` + * `sendRedirect(request, response, targetUrl, this.http10Compatible)`:重定向 + + * `response.sendRedirect(encodedURL)`:**使用 Servlet 原生方法实现重定向** + + + + + + + +**** + + + + + +## 异步调用 + +### 请求参数 + +名称:@RequestBody +类型:形参注解 +位置:处理器类中的方法形参前方 +作用:将异步提交数据**转换**成标准请求参数格式,并赋值给形参 +范例: + +```java +@Controller //控制层 +public class AjaxController { + @RequestMapping("/ajaxController") + public String ajaxController(@RequestBody String message){ + System.out.println(message); + return "page.jsp"; + } +} +``` + +* 注解添加到 Pojo 参数前方时,封装的异步提交数据按照 Pojo 的属性格式进行关系映射 + * POJO 中的属性如果请求数据中没有,属性值为 null + * POJO 中没有的属性如果请求数据中有,不进行映射 +* 注解添加到集合参数前方时,封装的异步提交数据按照集合的存储结构进行关系映射 + +```java +@RequestMapping("/ajaxPojoToController") +//如果处理参数是POJO,且页面发送的请求数据格式与POJO中的属性对应,@RequestBody注解可以自动映射对应请求数据到POJO中 +public String ajaxPojoToController(@RequestBody User user){ + System.out.println("controller pojo :"+user); + return "page.jsp"; +} + +@RequestMapping("/ajaxListToController") +//如果处理参数是List集合且封装了POJO,且页面发送的数据是JSON格式,数据将自动映射到集合参数 +public String ajaxListToController(@RequestBody List userList){ + System.out.println("controller list :"+userList); + return "page.jsp"; +} +``` + +ajax.jsp + +```html +<%@page pageEncoding="UTF-8" language="java" contentType="text/html;UTF-8" %> + +访问springmvc后台controller
+传递Json格式POJO
+传递Json格式List
+ + + +``` + +web.xml配置:请求响应章节请求中的web.xml配置 + +```xml +CharacterEncodingFilter + DispatcherServlet +``` + +spring-mvc.xml: + +```xml + + + +``` + + + +**** + + + +### 响应数据 + +注解:@ResponseBody +作用:将 java 对象转为 json 格式的数据 + +方法返回值为 POJO 时,自动封装数据成 Json 对象数据: + +```java +@RequestMapping("/ajaxReturnJson") +@ResponseBody +public User ajaxReturnJson(){ + System.out.println("controller return json pojo..."); + User user = new User("Jockme",40); + return user; +} +``` + +方法返回值为 List 时,自动封装数据成 json 对象数组数据: + +```java +@RequestMapping("/ajaxReturnJsonList") +@ResponseBody +//基于jackon技术,使用@ResponseBody注解可以将返回的保存POJO对象的集合转成json数组格式数据 +public List ajaxReturnJsonList(){ + System.out.println("controller return json list..."); + User user1 = new User("Tom",3); + User user2 = new User("Jerry",5); + + ArrayList al = new ArrayList(); + al.add(user1); + al.add(user2); + return al; +} +``` + +AJAX 文件: + +```js +//为id="testAjaxReturnString"的组件绑定点击事件 +$("#testAjaxReturnString").click(function(){ + //发送异步调用 + $.ajax({ + type:"POST", + url:"ajaxReturnString", //回调函数 success:function(data){ //打印返回结果 @@ -10812,19 +11715,7 @@ public User cross(HttpServletRequest request){ 2. 拦截内容不同: Filter 对所有访问进行增强, Interceptor 仅针对 SpringMVC 的访问进行增强 - ![](https://gitee.com/seazean/images/raw/master/Frame/拦截器-过滤器和拦截器的运行机制.png) - - - -*** - - - -### 执行流程 - -拦截器的执行流程: - -![](https://gitee.com/seazean/images/raw/master/Frame/拦截器执行流程.png) + @@ -10854,7 +11745,7 @@ public boolean preHandle(HttpServletRequest request, * handler:public String controller.InterceptorController.handleRun * handler.getClass():org.springframework.web.method.HandlerMethod * 返回值: - * 返回值为false,被拦截的处理器将不执行 + * 返回值为 false,被拦截的处理器将不执行 @@ -10885,7 +11776,7 @@ public void postHandle(HttpServletRequest request, -#### 完成处理 +#### 异常处理 拦截器最后执行的方法,无论原始方法是否执行: @@ -10914,9 +11805,9 @@ public void afterCompletion(HttpServletRequest request, * `/**`:表示拦截所有映射 * `/* `:表示拦截所有/开头的映射 -* `/user/*`:表示拦截所有/user/开头的映射 -* `/user/add*`:表示拦截所有/user/开头,且具体映射名称以add开头的映射 -* `/user/*All`:表示拦截所有/user/开头,且具体映射名称以All结尾的映射 +* `/user/*`:表示拦截所有 /user/ 开头的映射 +* `/user/add*`:表示拦截所有 /user/ 开头,且具体映射名称以 add 开头的映射 +* `/user/*All`:表示拦截所有 /user/ 开头,且具体映射名称以 All 结尾的映射 ```xml @@ -10955,7 +11846,101 @@ public void afterCompletion(HttpServletRequest request, * 链路过长时,处理效率低下 * 可能存在节点上的循环引用现象,造成死循环,导致系统崩溃 -![](https://gitee.com/seazean/images/raw/master/Frame/拦截器-多拦截器配置.png) + + + + +*** + + + +### 源码解析 + +DispatcherServlet#doDispatch 方法中: + +```java +protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { + try { + // 获取映射器以及映射器的所有拦截器(运行原理部分详解了源码) + mappedHandler = getHandler(processedRequest); + // 前置处理,返回 false 代表条件成立 + if (!mappedHandler.applyPreHandle(processedRequest, response)) { + //请求从这里直接结束 + return; + } + //所有拦截器都返回 true,执行目标方法 + mv = ha.handle(processedRequest, response, mappedHandler.getHandler()) + // 倒序执行所有拦截器的后置处理方法 + mappedHandler.applyPostHandle(processedRequest, response, mv); + } catch (Exception ex) { + //异常处理机制 + triggerAfterCompletion(processedRequest, response, mappedHandler, ex); + } +} +``` + +HandlerExecutionChain#applyPreHandle:前置处理 + +```java +boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception { + //遍历所有的拦截器 + for (int i = 0; i < this.interceptorList.size(); i++) { + HandlerInterceptor interceptor = this.interceptorList.get(i); + //执行前置处理,如果拦截器返回 false,则条件成立,不在执行其他的拦截器,直接返回 false,请求直接结束 + if (!interceptor.preHandle(request, response, this.handler)) { + triggerAfterCompletion(request, response, null); + return false; + } + this.interceptorIndex = i; + } + return true; +} +``` + +HandlerExecutionChain#applyPostHandle:后置处理 + +```java +void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv) + throws Exception { + //倒序遍历 + for (int i = this.interceptorList.size() - 1; i >= 0; i--) { + HandlerInterceptor interceptor = this.interceptorList.get(i); + interceptor.postHandle(request, response, this.handler, mv); + } +} +``` + +DispatcherServlet#triggerAfterCompletion 底层调用 HandlerExecutionChain#triggerAfterCompletion: + +* 前面的步骤有任何异常都会直接倒序触发 afterCompletion + +* 页面成功渲染有异常,也会倒序触发 afterCompletion + +```java +void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) { + //倒序遍历 + for (int i = this.interceptorIndex; i >= 0; i--) { + HandlerInterceptor interceptor = this.interceptorList.get(i); + try { + //执行异常处理的方法 + interceptor.afterCompletion(request, response, this.handler, ex); + } + catch (Throwable ex2) { + logger.error("HandlerInterceptor.afterCompletion threw exception", ex2); + } + } +} +``` + + + +拦截器的执行流程: + + + + + +参考文章:https://www.yuque.com/atguigu/springboot/vgzmgh#wtPLU @@ -10978,7 +11963,7 @@ public void afterCompletion(HttpServletRequest request, } ``` -* 自定义拦截器需要实现HandleInterceptor接口 +* 自定义拦截器需要实现 HandleInterceptor 接口 ```java //自定义拦截器需要实现HandleInterceptor接口 @@ -11014,7 +11999,7 @@ public void afterCompletion(HttpServletRequest request, } ``` - 说明:三个方法的运行顺序为 preHandle -> postHandle -> afterCompletion,如果preHandle返回值为false,三个方法仅运行preHandle + 说明:三个方法的运行顺序为 preHandle → postHandle → afterCompletion,如果 preHandle 返回值为 false,三个方法仅运行preHandle * web.xml: @@ -11051,7 +12036,7 @@ public void afterCompletion(HttpServletRequest request, ### 处理器 -异常处理器: **HandlerExceptionResolver**接口 +异常处理器: **HandlerExceptionResolver** 接口 类继承该接口的以后,当开发出现异常后会执行指定的功能 @@ -11125,9 +12110,7 @@ public class UserController { 使用注解实现异常分类管理,开发异常处理器 -ControllerAdvice注解: - -* 名称:@ControllerAdvice +ControllerAdvice 注解: * 类型:类注解 @@ -11145,9 +12128,7 @@ ControllerAdvice注解: } ``` -ExceptionHandler注解: - -* 名称:@ExceptionHandler +ExceptionHandler 注解: * 类型:方法注解 @@ -11178,7 +12159,22 @@ ExceptionHandler注解: } ``` - + +@ResponseStatus 注解: + +* 类型:类注解、方法注解 + +* 位置:异常处理器类、方法上方 + +* 参数: + + value:出现错误指定返回状态码 + + reason:出现错误返回的错误信息 + + + + *** @@ -11327,9 +12323,9 @@ ExceptionHandler注解: MultipartResolver接口: -* MultipartResolver接口定义了文件上传过程中的相关操作,并对通用性操作进行了封装 -* MultipartResolver接口底层实现类CommonsMultipartResovler -* CommonsMultipartResovler并未自主实现文件上传下载对应的功能,而是调用了apache文件上传下载组件 +* MultipartResolver 接口定义了文件上传过程中的相关操作,并对通用性操作进行了封装 +* MultipartResolver 接口底层实现类 CommonsMultipartResovler +* CommonsMultipartResovler 并未自主实现文件上传下载对应的功能,而是调用了 apache 文件上传下载组件 文件上传下载实现: @@ -11343,59 +12339,49 @@ MultipartResolver接口: ``` -* 页面表单fileupload.jsp +* 页面表单 fileupload.jsp - ```jsp - <%@page pageEncoding="UTF-8" language="java" contentType="text/html;UTF-8" %> - -
- <%--文件上传表单的name属性值一定要与controller处理器中方法的参数对应,否则无法实现文件上传--%> - 上传LOGO:
- 上传照片:
- 上传任意文件:
- + ```html + +
+
``` - + * web.xml ```xml DispatcherServlet + CharacterEncodingFilter ``` -* spring-mvc.xml - - ```xml - - - - - - ``` - * 控制器 ```java - @Controller - public class FileUploadController { - @RequestMapping(value = "/fileupload") - public void fileupload(MultipartFile file){ - System.out.println("file upload is running ..." + file); - file.transferTo(new File("file.png")); - } + @PostMapping("/upload") + public String upload(@RequestParam("email") String email, + @RequestParam("username") String username, + @RequestPart("headerImg") MultipartFile headerImg) throws IOException { + + if(!headerImg.isEmpty()){ + //保存到文件服务器,OSS服务器 + String originalFilename = headerImg.getOriginalFilename(); + headerImg.transferTo(new File("H:\\cache\\" + originalFilename)); + } + return "main"; } ``` + + *** -#### 注意事项 +#### 名称问题 -MultipartFile参数中封装了上传的文件的相关信息。 +MultipartFile 参数中封装了上传的文件的相关信息。 1. 文件命名问题, 获取上传文件名,并解析文件名与扩展名 @@ -11463,6 +12449,57 @@ public class FileUploadController { +**** + + + +#### 源码解析 + +**StandardServletMultipartResolver** 文件上传解析器 + +DispatcherServlet#doDispatch: + +```java +protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { + // 判断当前请求是不是文件上传请求 + processedRequest = checkMultipart(request); + // 文件上传请求会对 request 进行包装,导致两者不相等,此处赋值为 true,代表已经被解析 + multipartRequestParsed = (processedRequest != request); +} +``` + +DispatcherServlet#checkMultipart: + +* `if (this.multipartResolver != null && this.multipartResolver.isMultipart(request))`:判断是否是文件请求 + * `StandardServletMultipartResolver#isMultipart`:根据开头是否符合 multipart/form-data 或者 multipart/ +* `return this.multipartResolver.resolveMultipart(request)`:把请求封装成 StandardMultipartHttpServletRequest 对象 + +开始执行 ha.handle() 目标方法进行数据的解析 + +* RequestPartMethodArgumentResolver#supportsParameter:支持解析文件上传数据 + + ```java + public boolean supportsParameter(MethodParameter parameter) { + // 参数上有 @RequestPart 注解 + if (parameter.hasParameterAnnotation(RequestPart.class)) { + return true; + } + } + ``` + +* RequestPartMethodArgumentResolver#resolveArgument:解析参数数据,封装成 MultipartFile 对象 + + * `RequestPart requestPart = parameter.getParameterAnnotation(RequestPart.class)`:获取注解的相关信息 + * `String name = getPartName(parameter, requestPart)`:获取上传文件的名字 + * `Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument()`:解析参数 + * `List files = multipartRequest.getFiles(name)`:获取文件的所有数据 + +* `return doInvoke(args)`:解析完成执行自定义的方法,完成上传功能 + + + + + *** @@ -11722,20 +12759,6 @@ jsp: -*** - - - -## 运行原理 - - - - - - - - - diff --git a/Web.md b/Web.md index 9c6edbb..f06f46b 100644 --- a/Web.md +++ b/Web.md @@ -3575,7 +3575,7 @@ HttpServletRequest 类方法: RequestDispatcher 类方法: -* `void forward(ServletRequest request, ServletResponse response)` : 实现转发。将请求从servlet转发到服务器上的另一个资源(servlet,JSP文件或HTML文件) +* `void forward(ServletRequest request, ServletResponse response)` : 实现转发,将请求从 servlet 转发到服务器上的另一个资源(servlet,JSP文件或HTML文件) 过程:浏览器访问http://localhost:8080/request/servletDemo09,/servletDemo10也会执行 @@ -4043,10 +4043,10 @@ public class ServletDemo06 extends HttpServlet { * 方式一: - 1. 设置响应状态码:`resp.setStatus(302);` - 2. 设置重定向的路径(响应到哪里,通过响应头location来指定) - `response.setHeader("Location","http://www.baidu.com");` - `response.setHeader("Location","/response/servletDemo08);` + 1. 设置响应状态码:`resp.setStatus(302)` + 2. 设置重定向的路径(响应到哪里,通过响应头 location 来指定) + * `response.setHeader("Location","http://www.baidu.com");` + * `response.setHeader("Location","/response/servletDemo08);` * 方式二: From 861f5186429ea5c51e00cf62b7e2f06e9ef8b349 Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 5 Aug 2021 21:15:11 +0800 Subject: [PATCH 011/168] Update Java Notes --- DB.md | 42 ++++++++++++++++---------------- Java.md | 33 +++++++++++++------------ SSM.md | 75 ++++++++++++--------------------------------------------- 3 files changed, 53 insertions(+), 97 deletions(-) diff --git a/DB.md b/DB.md index 14d5e3b..6544177 100644 --- a/DB.md +++ b/DB.md @@ -1696,7 +1696,7 @@ WHERE #### 子查询 -* 子查询概念:查询语句中嵌套了查询语句,**将嵌套查询称为子查询** +子查询概念:查询语句中嵌套了查询语句,**将嵌套查询称为子查询** * 结果是单行单列:可以将查询的结果作为另一条语句的查询条件,使用运算符判断 @@ -3669,7 +3669,7 @@ MySQL官方对索引的定义为:索引(index)是帮助 MySQL 高效获取 索引的缺点: * 一般来说索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式**存储在磁盘**上 -* 虽然索引大大提高了查询效率,同时却也降低更新表的速度。对表进行 INSERT、UPDATE、DELETE 操作,MySQL 不仅要保存数据,还要保存一下索引文件每次更新添加了索引列的字段,还会调整因为更新所带来的键值变化后的索引信息 +* 虽然索引大大提高了查询效率,同时却也降低更新表的速度。对表进行 INSERT、UPDATE、DELETE 操作,MySQL 不仅要保存数据,还要保存一下索引文件每次更新添加了索引列的字段,还会调整因为更新所带来的键值变化后的索引信息,**但是更新数据也需要先从数据库中获取**,索引加快了获取速度,所以可以相互抵消一下。 * 索引会影响到 WHERE 的查询条件和排序 ORDER BY 两大功能 @@ -4137,7 +4137,7 @@ B+Tree 优点:提高查询速度,减少磁盘的 IO 次数,树形结构较 适用条件: -* 需要存储引擎将索引中的数据与条件进行判断,所以优化是基于存储引擎的,只有特定引擎可以使用,适用于InnoDB 和 MyISAM 引擎 +* 需要存储引擎将索引中的数据与条件进行判断,所以优化是基于存储引擎的,只有特定引擎可以使用,适用于 InnoDB 和 MyISAM * 存储引擎没有调用跨存储引擎的能力,跨存储引擎的功能有存储过程、触发器、视图,所以调用这些功能的不可以进行索引下推优化 * 对于 InnoDB 引擎只适用于二级索引,InnoDB 的聚簇索引会将整行数据读到缓冲区,因为数据已经在内存中了,不再需要去读取了,索引下推的目的减少 IO 次数也就失去了意义 @@ -4928,7 +4928,7 @@ CREATE INDEX idx_emp_age_salary ON emp(age,salary); * 第二种通过有序索引顺序扫描直接返回有序数据,这种情况为 Using index,不需要额外排序,操作效率高 ```mysql - EXPLAIN SELECT id,age,salary FROM emp ORDER BY age DESC; + EXPLAIN SELECT id, age, salary FROM emp ORDER BY age DESC; ``` ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL ORDER BY排序2.png) @@ -4943,7 +4943,7 @@ CREATE INDEX idx_emp_age_salary ON emp(age,salary); ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL ORDER BY排序3.png) - 尽量减少额外的排序,通过索引直接返回有序数据。需要满足 Order by 使用相同的索引、Order By 的顺序和索引顺序相同、Order by 的字段都是升序或都是降序,否则需要额外的操作,就会出现 FileSort + 尽量减少额外的排序,通过索引直接返回有序数据。需要**满足 Order by 使用相同的索引、Order By 的顺序和索引顺序相同、Order by 的字段都是升序或都是降序**,否则需要额外的操作,就会出现 FileSort Filesort 的优化:通过创建合适的索引,能够减少 Filesort 的出现,但是在某些情况,条件限制不能让 Filesort 消失,就需要加快 Filesort 的排序操作。 @@ -4971,7 +4971,7 @@ SHOW VARIABLES LIKE 'sort_buffer_size'; -- 默认262114 #### GROUP BY -GROUP BY 也会进行排序操作,与 ORDER BY 相比,GROUP BY 主要只是多了排序之后的分组操作,所以在GROUP BY 的实现过程中,与 ORDER BY 一样也可以利用到索引 +GROUP BY 也会进行排序操作,与 ORDER BY 相比,GROUP BY 主要只是多了排序之后的分组操作,所以在 GROUP BY 的实现过程中,与 ORDER BY 一样也可以利用到索引 * 分组查询: @@ -8453,7 +8453,7 @@ user:id:3506728370 → {"name":"春晚","fans":12210862,"blogs":83} - 当键值对个数小于 hash-max-ziplist-entries 配置(默认512个) - 所有键值都小于 hash-max-ziplist-value 配置(默认64字节) -ziplist 使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比 hashtable 更加优秀,当 ziplist 无法满足哈希类型时,Redis 会使用 hashtable 作为哈希的内部实现,因为此时 ziplist 的读写效率会下降,而hashtable 的读写时间复杂度为 O(1) +ziplist 使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比 hashtable 更加优秀,当 ziplist 无法满足哈希类型时,Redis 会使用 hashtable 作为哈希的内部实现,因为此时 ziplist 的读写效率会下降,而 hashtable 的读写时间复杂度为 O(1) @@ -8463,7 +8463,7 @@ ziplist 使用更加紧凑的结构实现多个元素的连续存储,所以在 ##### 压缩列表 -压缩列表(ziplist)是列表和哈希的底层实现之一,压缩列表用来紧凑数据存储,节省内存: +压缩列表(ziplist)是列表和哈希的底层实现之一,压缩列表用来紧凑数据存储,节省内存,有序: @@ -8854,7 +8854,7 @@ Redis 使用跳跃表作为有序集合键的底层实现之一,如果一个 * 基于单向链表加索引的方式实现 - Redis 的跳跃表实现由 zskiplist 和 zskiplistnode 两个结构组成,其中 zskiplist 用于保存跳跃表信息(比如表头节点、表尾节点、长度),而 zskiplistnode 则用于表示跳跃表节点 -- Redis 每个跳跃表节点的层高都是 1 至 32 之间的随机数(Redis5 之后最大层数为64) +- Redis 每个跳跃表节点的层高都是 1 至 32 之间的随机数(Redis5 之后最大层数为 64) - 在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的成员对象必须是唯一的。跳跃表中的节点按照分值大小进行排序,当分值相同时节点按照成员对象的大小进行排序 ![](https://gitee.com/seazean/images/raw/master/DB/Redis-跳跃表数据结构.png) @@ -9014,13 +9014,13 @@ redis 应用于地理位置计算 ### 基本使用 -Jedis用于Java语言连接redis服务,并提供对应的操作API +Jedis 用于 Java 语言连接 redis 服务,并提供对应的操作 API -1. jar包导入 +1. jar 包导入 * 下载地址:https://mvnrepository.com/artifact/redis.clients/jedis - * 基于maven: + * 基于 maven: ```xml @@ -9030,12 +9030,12 @@ Jedis用于Java语言连接redis服务,并提供对应的操作API ``` -2. 客户端连接redis - API文档:http://xetorthio.github.io/jedis/ +2. 客户端连接 redis + API 文档:http://xetorthio.github.io/jedis/ - 连接redis:`Jedis jedis = new Jedis("192.168.0.185", 6379);` - 操作redis:`jedis.set("name", "seazean"); jedis.get("name");` - 关闭redis:`jedis.close();` + 连接 redis:`Jedis jedis = new Jedis("192.168.0.185", 6379);` + 操作 redis:`jedis.set("name", "seazean"); jedis.get("name");` + 关闭 redis:`jedis.close();` 代码实现: @@ -9071,12 +9071,12 @@ public class JedisTest { ### 工具类 连接池对象: - JedisPool:Jedis提供的连接池技术 + JedisPool:Jedis 提供的连接池技术 poolConfig:连接池配置对象 - host:redis服务地址 - port:redis服务端口号 + host:redis 服务地址 + port:redis 服务端口号 -JedisPool的构造器如下: +JedisPool 的构造器如下: ```java public JedisPool(GenericObjectPoolConfig poolConfig, String host, int port) { diff --git a/Java.md b/Java.md index 1201277..2f82ef0 100644 --- a/Java.md +++ b/Java.md @@ -1058,11 +1058,11 @@ public class ClassDemo { ### 包 -包:分门别类的管理各种不同的技术,便于管理技术,扩展技术,阅读技术。 +包:分门别类的管理各种不同的技术,便于管理技术,扩展技术,阅读技术 -定义包的格式:`package 包名; `,必须放在类名的最上面。 +定义包的格式:`package 包名`,必须放在类名的最上面 -导包格式:`import 包名.类名;` +导包格式:`import 包名.类名` 相同包下的类可以直接访问;不同包下的类必须导包才可以使用 @@ -3757,14 +3757,15 @@ Collection集合的体系: LinkedHashSet<>(实现类) ``` -**集合的特点:**(非常重要) - Set系列集合:添加的元素是无序,不重复,无索引的。 - -- HashSet: 添加的元素是无序,不重复,无索引的。 - -- LinkedHashSet: 添加的元素是有序,不重复,无索引的。 - -- TreeSet: 不重复,无索引,按照大小默认升序排序!! - List系列集合:添加的元素是有序,可重复,有索引。 - -- ArrayList:添加的元素是有序,可重复,有索引。 - -- LinekdList:添加的元素是有序,可重复,有索引。 +**集合的特点:** + +* Set系列集合:添加的元素是无序,不重复,无索引的 + * HashSet:添加的元素是无序,不重复,无索引的 + * LinkedHashSet:添加的元素是有序,不重复,无索引的 + * TreeSet:不重复,无索引,按照大小默认升序排序 +* List系列集合:添加的元素是有序,可重复,有索引 + * ArrayList:添加的元素是有序,可重复,有索引 + * LinekdList:添加的元素是有序,可重复,有索引 @@ -3774,9 +3775,9 @@ LinkedHashSet<>(实现类) #### API -Collection是集合的祖宗类,它的功能是全部集合都可以继承使用的,所以要学习它。 +Collection 是集合的祖宗类,它的功能是全部集合都可以继承使用的,所以要学习它。 -Collection子类的构造器都有可以包装其他子类的构造方法,如: +Collection 子类的构造器都有可以包装其他子类的构造方法,如: `public ArrayList(Collection c)` : 构造新集合,元素按照由集合的迭代器返回的顺序 `public HashSet(Collection c)` : 构造一个包含指定集合中的元素的新集合 @@ -9770,7 +9771,7 @@ Heap 堆:是JVM内存中最大的一块,由所有线程共享,由垃圾回 * Young 区被划分为三部分,Eden 区和两个大小严格相同的 Survivor 区。Survivo r区间,某一时刻只有其中一个是被使用的,另外一个留做垃圾回收时复制对象。在 Eden 区变满的时候, GC 就会将存活的对象移到空闲的 Survivor 区间中,根据JVM的策略,在经过几次垃圾回收后,仍然存活于 Survivor 的对象将被移动到 Tenured 区间 * Tenured 区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在 Young 复制转移一定的次数以后,对象就会被转移到 Tenured 区 -* Perm 代主要保存**Class、ClassLoader、静态变量、常量、编译后的代码**,在 java7 中堆内方法区会受到 GC 的管理 +* Perm 代主要保存 **Class、ClassLoader、静态变量、常量、编译后的代码**,在 java7 中堆内方法区会受到 GC 的管理 分代原因:不同对象的生命周期不同,70%-99% 的对象都是临时对象,优化 GC 性能 @@ -9802,11 +9803,11 @@ public static void main(String[] args) { 方法区的 GC:针对常量池的回收及对类型的卸载,比较难实现 -为了**避免方法区出现OOM**,在JDK8中将堆内的方法区(永久代)移动到了本地内存上,重新开辟了一块空间,叫做元空间,元空间存储类的元信息,静态变量和字符串常量池等放入堆中 +为了**避免方法区出现OOM**,在 JDK8 中将堆内的方法区(永久代)移动到了本地内存上,重新开辟了一块空间,叫做元空间,元空间存储类的元信息,静态变量和字符串常量池等放入堆中 类元信息:在类编译期间放入方法区,存放了类的基本信息,包括类的方法、参数、接口以及常量池表 -常量池表(Constant Pool Table)是Class文件的一部分,存储了类在编译期间生成的**字面量、符号引用**,JVM为每个已加载的类维护一个常量池 +常量池表(Constant Pool Table)是 Class 文件的一部分,存储了类在编译期间生成的**字面量、符号引用**,JVM 为每个已加载的类维护一个常量池 - 字面量:基本数据类型、字符串类型常量、声明为 final 的常量值等 - 符号引用:类、字段、方法、接口等的符号引用 diff --git a/SSM.md b/SSM.md index bce818e..f034665 100644 --- a/SSM.md +++ b/SSM.md @@ -2,10 +2,7 @@ ## 基本介绍 -框架是一款半成品软件,我们可以基于这个半成品软件继续开发,来完成我们个性化的需求! - -ORM(Object Relational Mapping): 对象关系映射 -指的是持久化数据和实体对象的映射模式,解决面向对象与关系型数据库存在的互不匹配的现象 +ORM(Object Relational Mapping): 对象关系映射,指的是持久化数据和实体对象的映射模式,解决面向对象与关系型数据库存在的互不匹配的现象 ![](https://gitee.com/seazean/images/raw/master/Frame/ORM介绍.png) @@ -4191,9 +4188,9 @@ private UserDao userDao; 相关属性: -- required:为 true (默认)表示注入 bean 时该 bean 必须存在,不然就会注入失败;为 true 表示注入 false 时该 bean 存在就注入,不存在就忽略跳过 +- required:为 true (默认)表示注入 bean 时该 bean 必须存在,不然就会注入失败;为 false 表示注入时该 bean 存在就注入,不存在就忽略跳过 -注意:在使用 @Autowired 时,首先在容器中查询对应类型的 bean,如果查询结果刚好为一个,就将该 bean 装配给 @Autowired 指定的数据,如果查询的结果不止一个,那么 @Autowired 会根据名称来查找。如果查询的结果为空,那么会抛出异常。解决方法:使用 required=false +注意:在使用 @Autowired 时,首先在容器中查询对应类型的 bean,如果查询结果刚好为一个,就将该 bean 装配给 @Autowired 指定的数据,如果查询的结果不止一个,那么 @Autowired 会根据名称来查找。如果查询的结果为空,那么会抛出异常。解决方法:使用 required = false @@ -4570,7 +4567,7 @@ UserService userService = (UserService)bf.getBean("userService"); FactoryBean与 BeanFactory 区别: -- FactoryBean:封装单个 bean 的创建过程 +- FactoryBean:封装单个 bean 的创建过程,就是工厂的 Bean - BeanFactory:Spring 容器顶层接口,定义了 bean 相关的获取操作 @@ -6533,23 +6530,23 @@ MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ( } ``` -**支持当前事务的情况:** +**支持当前事务**的情况: -* TransactionDefinition.PROPAGATION_REQUIRED: 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务 +* TransactionDefinition.PROPAGATION_REQUIRED: 如果当前存在事务,则**加入该事务**;如果当前没有事务,则创建一个新的事务 * 内外层是相同的事务 * 在 aMethod 或者在 bMethod 内的任何地方出现异常,事务都会被回滚 -* TransactionDefinition.PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行 -* TransactionDefinition.PROPAGATION_MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常 +* TransactionDefinition.PROPAGATION_SUPPORTS: 如果当前存在事务,则**加入该事务**;如果当前没有事务,则以非事务的方式继续运行 +* TransactionDefinition.PROPAGATION_MANDATORY: 如果当前存在事务,则**加入该事务**;如果当前没有事务,则抛出异常 -**不支持当前事务的情况:** +**不支持当前事务**的情况: - TransactionDefinition.PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。 - 内外层是不同的事务,如果 bMethod 已经提交,如果 aMethod 失败回滚 ,bMethod 不会回滚 - 如果 bMethod 失败回滚,ServiceB 抛出的异常被 ServiceA 捕获,如果 B 抛出的异常是 A 会回滚的异常,aMethod 事务需要回滚,否则仍然可以提交 -- TransactionDefinition.PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起 -- TransactionDefinition.PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常 +- TransactionDefinition.PROPAGATION_NOT_SUPPORTED: **以非事务方式运行**,如果当前存在事务,则把当前事务挂起 +- TransactionDefinition.PROPAGATION_NEVER: **以非事务方式运行**,如果当前存在事务,则抛出异常 -**其他情况:** +其他情况: * TransactionDefinition.PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 PROPAGATION_REQUIRED * 如果 ServiceB 异常回滚,可以通过 try-catch 机制执行 ServiceC @@ -12773,36 +12770,6 @@ jsp: ## XML -### 整合流程 - -整合步骤分析: - -SSM(Spring+SpringMVC+MyBatis) - -* Spring:框架基础 - -* MyBatis:mysql+druid+pagehelper - -* Spring 整合 MyBatis - -* junit 测试业务层接口 - -* SpringMVC - * rest 风格(postman 测试请求结果) - * 数据封装 json(jackson) - -* Spring 整合 SpringMVC - - * Controller 调用 Service - -* 其他 - - * 表现层数据封装 - - * 自定义异常 - - - ### 结构搭建 * 创建项目,组织项目结构,创建包 @@ -13088,13 +13055,7 @@ SSM(Spring+SpringMVC+MyBatis) @@ -13103,7 +13064,7 @@ SSM(Spring+SpringMVC+MyBatis) ``` - + * MyBatis映射:resources.dao.UserDao.xml ```xml @@ -13145,21 +13106,15 @@ SSM(Spring+SpringMVC+MyBatis) ``` -* Mybatis核心配置:resouces.applicationContext.xml +* Mybatis 核心配置:resouces.applicationContext.xml ```xml + http://www.springframework.org/schema/context/spring-context.xsdd"> From 8fefef0f755cb0b773b4cec1e1ebee5719a36283 Mon Sep 17 00:00:00 2001 From: Seazean Date: Fri, 6 Aug 2021 14:53:31 +0800 Subject: [PATCH 012/168] Update Java Notes --- Java.md | 59 +- SSM.md | 2526 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 2528 insertions(+), 57 deletions(-) diff --git a/Java.md b/Java.md index 2f82ef0..ae674cc 100644 --- a/Java.md +++ b/Java.md @@ -6,14 +6,14 @@ #### 变量类型 -| | 成员变量 | 局部变量 | 静态变量 | -| :------: | :------------: | :------------------------: | :------------------: | -| 定义位置 | 在类中,方法外 | 方法中或者方法的形参 | 在类中,方法外 | -| 初始化值 | 有默认初始化值 | 无,先定义,复制后才能使用 | 有默认初始化值 | -| 调用方法 | 对象调用 | | 对象调用,类名调用 | -| 存储位置 | 堆中 | 栈中 | 方法区 | -| 生命周期 | 与对象共存亡 | 与方法共存亡 | 与类共存亡 | -| 别名 | 实例变量 | | 类变量,静态成员变量 | +| | 成员变量 | 局部变量 | 静态变量 | +| :------: | :------------: | :------------------------: | :-------------------------: | +| 定义位置 | 在类中,方法外 | 方法中或者方法的形参 | 在类中,方法外 | +| 初始化值 | 有默认初始化值 | 无,先定义,赋值后才能使用 | 有默认初始化值 | +| 调用方法 | 对象调用 | | 对象调用,类名调用 | +| 存储位置 | 堆中 | 栈中 | 方法区(JDK8 以后移到堆中) | +| 生命周期 | 与对象共存亡 | 与方法共存亡 | 与类共存亡 | +| 别名 | 实例变量 | | 类变量,静态成员变量 | **静态变量只有一个,成员变量是类中的变量,局部变量是方法中的变量** @@ -269,7 +269,13 @@ public static Integer valueOf(int i) { } ``` +自动拆箱调用 `java.lang.Integer#intValue`,源码: +```java +public int intValue() { + return value; +} +``` @@ -305,17 +311,20 @@ valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中 - Integer values between -128 and 127 - Character in the range \u0000 to \u007F (0 and 127) -在 jdk 1.8 所有的数值类缓冲池中,Integer 的缓存池 IntegerCache 很特殊,这个缓冲池的下界是 -128,上界默认是 127,但是上界是可调的,在启动 jvm 的时候,通过 AutoBoxCacheMax= 来指定这个缓冲池的大小,该选项在 JVM 初始化的时候会设定一个名为 java.lang.IntegerCache.high 系统属性,然后 IntegerCache 初始化的时候就会读取该系统属性来决定上界 +在 jdk 1.8 所有的数值类缓冲池中,**Integer 的缓存池 IntegerCache 很特殊,这个缓冲池的下界是 -128,上界默认是 127**,但是上界是可调的,在启动 jvm 的时候,通过 AutoBoxCacheMax= 来指定这个缓冲池的大小,该选项在 JVM 初始化的时候会设定一个名为 java.lang.IntegerCache.high 系统属性,然后 IntegerCache 初始化的时候就会读取该系统属性来决定上界 ```java Integer x = 100; //自动装箱,底层调用 Integer.valueOf(1) Integer y = 100; -System.out.println(x == y); // true +System.out.println(x == y); // true Integer x = 1000; Integer y = 1000; -System.out.println(x == y); // false -//因为缓存池最大127 +System.out.println(x == y); // false,因为缓存池最大127 + +int x = 1000; +Integer y = 1000; +System.out.println(x == y); // true,因为 y 会调用 intValue 自动拆箱返回 int 原始值进行比较 ``` @@ -2293,15 +2302,15 @@ public boolean equals(Object o) { } ``` -**面试题**:== 和equals的区别 +**面试题**:== 和 equals 的区别 * == 比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的**地址**是否相同,即是否是指相同一个对象,比较的是真正意义上的指针操作。 -* 重写equals方法比较的是两个对象的**内容**是否相等,所有的类都是继承自java.lang.Object类,所以适用于所有对象,如果**没有对该方法进行覆盖的话,调用的仍然是Object类中的方法,比较两个对象的引用** +* 重写 equals 方法比较的是两个对象的**内容**是否相等,所有的类都是继承自 java.lang.Object 类,所以适用于所有对象,如果**没有对该方法进行覆盖的话,调用的仍然是 Object 类中的方法,比较两个对象的引用** -hashCode的作用: +hashCode 的作用: -* hashCode的存在主要是用于查找的快捷性,如Hashtable,HashMap等,可以在散列存储结构中确定对象的存储地址 -* 如果两个对象相同,就是适用于equals(java.lang.Object) 方法,那么这两个对象的hashCode一定要相同 +* hashCode 的存在主要是用于查找的快捷性,如 Hashtable,HashMap 等,可以在散列存储结构中确定对象的存储地址 +* 如果两个对象相同,就是适用于 equals(java.lang.Object) 方法,那么这两个对象的 hashCode 一定要相同 * 哈希值相同的数据不一定内容相同,内容相同的数据哈希值一定相同 @@ -2318,23 +2327,23 @@ hashCode的作用: * 深拷贝 (deepCopy):对基本数据类型进行值传递,对引用数据类型是一个整个独立的对象拷贝,会拷贝所有的属性并指向的动态分配的内存,简而言之就是把所有属性复制到一个新的内存,增加一个指针指向新内存。所以使用深拷贝的情况下,释放内存的时候不会出现使用浅拷贝时释放同一块内存的错误 -Object 的 clone() 是 protected 方法,一个类不显式去重写 clone(),就不能直接去调用该类实例的clone()方法 +Object 的 clone() 是 protected 方法,一个类不显式去重写 clone(),就不能直接去调用该类实例的 clone() 方法 克隆就是制造一个对象的副本,根据所要克隆的对象的成员变量中是否含有引用类型,可以将克隆分为两种:浅克隆(Shallow Clone) 和 深克隆(Deep Clone),默认情况下使用Object中的clone方法进行克隆就是浅克隆,即完成对象域对域的拷贝 -Cloneable 接口是一个标识性接口,即该接口不包含任何方法(包括clone()),但是如果一个类想合法的进行克隆,那么就必须实现这个接口,在使用clone()方法时,若该类未实现 Cloneable 接口,则抛出异常 +Cloneable 接口是一个标识性接口,即该接口不包含任何方法(包括clone()),但是如果一个类想合法的进行克隆,那么就必须实现这个接口,在使用 clone() 方法时,若该类未实现 Cloneable 接口,则抛出异常 * Clone & Copy:`Student s = new Student` - `Student s1 = s`:只是copy了一下reference,s和s1指向内存中同一个object,对对象的修改会影响对方 + `Student s1 = s`:只是 copy 了一下 reference,s 和 s1 指向内存中同一个 object,对对象的修改会影响对方 `Student s2 = s.clone()`:会生成一个新的Student对象,并且和s具有相同的属性值和方法 * Shallow Clone & Deep Clone: - 浅克隆:Object中的clone()方法在对某个对象克隆时对其仅仅是简单地执行域对域的copy + 浅克隆:Object中的 clone() 方法在对某个对象克隆时对其仅仅是简单地执行域对域的 copy - * 对基本数据类型和包装类的克隆是没有问题的。String、Integer等包装类型在内存中是不可以被改变的对象,所以在使用克隆时可以视为基本类型,只需浅克隆引用即可 + * 对基本数据类型和包装类的克隆是没有问题的。String、Integer 等包装类型在内存中是不可以被改变的对象,所以在使用克隆时可以视为基本类型,只需浅克隆引用即可 * 如果对一个引用类型进行克隆时只是克隆了它的引用,和原始对象共享对象成员变量 ![](https://gitee.com/seazean/images/raw/master/Java/Object浅克隆.jpg) @@ -9526,8 +9535,8 @@ JVM 内存结构规定了 Java 在运行过程中内存申请、分配、管理 线程运行诊断: -* 定位:jps定位进程id -* jstack 进程id:用于打印出给定的 java 进程 ID 或 core file 或远程调试服务的 Java 堆栈信息 +* 定位:jps 定位进程 id +* jstack 进程 id:用于打印出给定的 java 进程 ID 或 core file 或远程调试服务的 Java 堆栈信息 常见OOM错误: @@ -9803,7 +9812,7 @@ public static void main(String[] args) { 方法区的 GC:针对常量池的回收及对类型的卸载,比较难实现 -为了**避免方法区出现OOM**,在 JDK8 中将堆内的方法区(永久代)移动到了本地内存上,重新开辟了一块空间,叫做元空间,元空间存储类的元信息,静态变量和字符串常量池等放入堆中 +为了**避免方法区出现OOM**,在 JDK8 中将堆内的方法区(永久代)移动到了本地内存上,重新开辟了一块空间,叫做元空间,元空间存储类的元信息,**静态变量和字符串常量池等放入堆中** 类元信息:在类编译期间放入方法区,存放了类的基本信息,包括类的方法、参数、接口以及常量池表 diff --git a/SSM.md b/SSM.md index f034665..6d90ad4 100644 --- a/SSM.md +++ b/SSM.md @@ -4380,13 +4380,13 @@ public class ClassName { #### 整合资源 -##### 导入资源 +##### 导入 名称:@Import 类型:类注解 -作用:导入第三方bean作为spring控制的资源,这些类都会被Spring创建并放入ioc容器 +作用:导入第三方 bean 作为 Spring 控制的资源,这些类都会被 Spring 创建并放入 ioc 容器 格式: @@ -4399,9 +4399,13 @@ public class ClassName { 说明: -- @Import注解在同一个类上,仅允许添加一次,如果需要导入多个,使用数组的形式进行设定 -- 在被导入的类中可以继续使用@Import导入其他资源(了解) -- @Bean所在的类可以使用导入的形式进入spring容器,无需声明为bean +- @Import 注解在同一个类上,仅允许添加一次,如果需要导入多个,使用数组的形式进行设定 +- 在被导入的类中可以继续使用 @Import 导入其他资源 +- @Bean 所在的类可以使用导入的形式进入 Spring 容器,无需声明为 bean + + + +*** @@ -4887,8 +4891,8 @@ public interface ApplicationListener 应用监听器步骤: * 写一个监听器(ApplicationListener实现类)来监听某个事件(ApplicationEvent及其子类) - * 把监听器加入到容器@Component - * 只要容器中有相关事件的发布,我们就能监听到这个事件; + * 把监听器加入到容器 @Component + * 只要容器中有相关事件的发布,就能监听到这个事件; * ContextRefreshedEvent:容器刷新完成(所有bean都完全创建)会发布这个事件 * ContextClosedEvent:关闭容器会发布这个事件 * 发布一个事件:`applicationContext.publishEvent()` @@ -4900,7 +4904,7 @@ public class MyApplicationListener implements ApplicationListener publishEvent(new ContextRefreshedEvent(this)) + * 容器刷新完成:finishRefresh() → publishEvent(new ContextRefreshedEvent(this)) - 发布ContextRefreshedEvent事件: + 发布 ContextRefreshedEvent 事件: - * 获取事件的多播器(派发器):getApplicationEventMulticaster() + * 获取事件的多播器(派发器):getApplicationEventMulticaster() - 容器初始化过程中执行`initApplicationEventMulticaster()`:初始化事件多播器 + 容器初始化过程中执行 `initApplicationEventMulticaster()`:初始化事件多播器 - * 先去容器中查询`id=applicationEventMulticaster`的组件,有直接返回 - * 如果没有就执行`this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);`并且加入到容器中, - * 以后在其他组件要派发事件,自动注入这个applicationEventMulticaster + * 先去容器中查询 `id=applicationEventMulticaster` 的组件,有直接返回 + * 没有就执行 `this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory)`并且加入到容器中 + * 以后在其他组件要派发事件,自动注入这个 applicationEventMulticaster - * multicastEvent派发事件 + * multicastEvent 派发事件 - * 获取到所有的ApplicationListener + * 获取到所有的 ApplicationListener - 容器初始化过程执行**registerListeners()**注册监听器 + 容器初始化过程执行 **registerListeners()** 注册监听器 - * 从容器中获取所有监听器:`getBeanNamesForType(ApplicationListener.class, true, false)` - * 将listener注册到ApplicationEventMulticaster:`getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName)` + * 从容器中获取所有监听器:`getBeanNamesForType(ApplicationListener.class, true, false)` + * 将 listener 注册到 ApplicationEventMulticaster:`getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName)` - * 遍历 ApplicationListener + * 遍历 ApplicationListener - * 如果有 Executor,可以使用 Executor 异步派发;Executor executor = getTaskExecutor() - * 反之,同步的方式直接执行listener方法;invokeListener(listener, event),拿到listener回调onApplicationEvent方法 + * 如果有 Executor,可以使用 Executor 异步派发;Executor executor = getTaskExecutor() + * 没有就同步直接执行 listener 方法;invokeListener(listener, event),拿到 listener 回调 onApplicationEvent 方法 2. 自己发布事件 @@ -12306,11 +12310,9 @@ ExceptionHandler 注解: -## 实用技术 - -### 文件传输 +## 文件传输 -#### 上传下载 +### 上传下载 上传文件过程: @@ -12376,7 +12378,7 @@ MultipartResolver接口: -#### 名称问题 +### 名称问题 MultipartFile 参数中封装了上传的文件的相关信息。 @@ -12450,7 +12452,7 @@ public class FileUploadController { -#### 源码解析 +### 源码解析 **StandardServletMultipartResolver** 文件上传解析器 @@ -12501,6 +12503,8 @@ DispatcherServlet#checkMultipart: +## 实用技术 + ### 校验框架 #### 校验概述 @@ -12756,6 +12760,53 @@ jsp: +**** + + + +### Lombok + +Lombok 用标签方式代替构造器、getter/setter、toString() 等方法 + +引入依赖: + +```xml + + org.projectlombok + lombok + +``` + +下载插件:IDEA 中 File → Settings → Plugins,搜索安装 Lombok 插件 + +常用注解: + +```java +@NoArgsConstructor // 无参构造 +@AllArgsConstructor // 全参构造 +@Data // set + get +@ToString // toString +@EqualsAndHashCode // hashConde + equals +``` + +简化日志: + +```java +@Slf4j +@RestController +public class HelloController { + @RequestMapping("/hello") + public String handle01(@RequestParam("name") String name){ + log.info("请求进来了...."); + return "Hello, Spring!" + "你好:" + name; + } +} +``` + + + + + @@ -13769,5 +13820,2416 @@ public class ProjectExceptionAdivce { # Boot -(这部分笔记做的非常一般,一周内完善) +## 基本介绍 + +### Boot介绍 + +SpringBoot 提供了一种快速使用 Spring 的方式,基于约定优于配置的思想,可以让开发人员不必在配置与逻辑业务之间进行思维的切换,全身心的投入到逻辑业务的代码编写中,从而大大提高了开发的效率 + +SpringBoot 功能: + +* 自动配置: + + Spring Boot 的自动配置是一个运行时(更准确地说,是应用程序启动时)的过程,考虑了众多因素选择使用哪个配置,该过程是SpringBoot 自动完成的 + +* 起步依赖,起步依赖本质上是一个 Maven 项目对象模型(Project Object Model,POM),定义了对其他库的传递依赖,这些东西加在一起即支持某项功能。简单的说,起步依赖就是将具备某种功能的坐标打包到一起,并提供一些默认的功能 + +* 辅助功能,提供了一些大型项目中常见的非功能性特性,如内嵌 web 服务器、安全、指标,健康检测、外部配置等 + + + +参考视频:https://www.bilibili.com/video/BV19K4y1L7MT + + + + +*** + + + +### 构建工程 + +普通构建: + +1. 创建 Maven 项目 + +2. 导入 SpringBoot 起步依赖 + + ```xml + + + org.springframework.boot + spring-boot-starter-parent + 2.1.8.RELEASE + + + + + + org.springframework.boot + spring-boot-starter-web + + + ``` + +3. 定义 Controller + + ```java + @RestController + public class HelloController { + @RequestMapping("/hello") + public String hello(){ + return " hello Spring Boot !"; + } + } + ``` + +4. 编写引导类 + + ```java + // 引导类,SpringBoot项目的入口 + @SpringBootApplication + public class HelloApplication { + public static void main(String[] args) { + SpringApplication.run(HelloApplication.class, args); + } + } + ``` + +快速构建: + +![](https://gitee.com/seazean/images/raw/master/Frame/SpringBoot-IDEA构建工程.png) + + + + + + + +*** + + + + + +## 自动装配 + +### 依赖管理 + +在 spring-boot-starter-parent 中定义了各种技术的版本信息,组合了一套最优搭配的技术版本。在各种 starter 中,定义了完成该功能需要的坐标合集,其中大部分版本信息来自于父工程。工程继承 parent,引入 starter 后,通过依赖传递,就可以简单方便获得需要的 jar 包,并且不会存在版本冲突,自动版本仲裁机制 + + + +*** + + + +### 底层注解 + +#### SpringBoot + +@SpringBootApplication():启动注解,实现 SpringBoot 的自动部署 + +* 参数 scanBasePackages:可以指定扫描范围 +* 默认扫描当前引导类所在包及其子包 + +假如所在包为 com.example.springbootenable,扫描配置包 com.example.config 的信息,三种解决办法: + +1. 使用 @ComponentScan 扫描 com.example.config 包 + +2. 使用 @Import 注解,加载类,这些类都会被 Spring 创建并放入 ioc 容器,默认组件的名字就是**全类名** + +3. 对 @Import 注解进行封装 + +```java +//1.@ComponentScan("com.example.config") +//2.@Import(UserConfig.class) +@EnableUser +@SpringBootApplication +public class SpringbootEnableApplication { + + public static void main(String[] args) { + ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args); + //获取Bean + Object user = context.getBean("user"); + System.out.println(user); + + } +} +``` + +UserConfig: + +```java +@Configuration +public class UserConfig { + @Bean + public User user() { + return new User(); + } +} +``` + +EnableUser 注解类: + +```java +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Import(UserConfig.class)//@Import注解实现Bean的动态加载 +public @interface EnableUser { +} +``` + + + + + +*** + + + +#### Configuration + +@Configuration:设置当前类为 SpringBoot 的配置类 + +* proxyBeanMethods = true:Full 全模式,每个 @Bean 方法被调用多少次返回的组件都是单实例的,默认值,类组件之间**有依赖关系**,方法会被调用得到之前单实例组件 +* proxyBeanMethods = false:Lite 轻量级模式,每个 @Bean 方法被调用多少次返回的组件都是新创建的,类组件之间**无依赖关系**用 Lite 模式加速容器启动过程 + +```java +@Configuration(proxyBeanMethods = true) +public class MyConfig { + @Bean //给容器中添加组件。以方法名作为组件的 id。返回类型就是组件类型。返回的值,就是组件在容器中的实例 + public User user(){ + User user = new User("zhangsan", 18); + return user; + } +} +``` + + + +*** + + + + + +*** + + + +#### Condition + +##### 条件注解 + +Condition 是 Spring4.0 后引入的条件化配置接口,通过实现 Condition 接口可以完成有条件的加载相应的 Bean + +注解:@Conditional + +作用:条件装配,满足 Conditional 指定的条件则进行组件注入,加上方法或者类上,作用范围不同 + +使用:@Conditional 配合 Condition 的实现类(ClassCondition)进行使用 + +ConditionContext 类API: + +| 方法 | 说明 | +| --------------------------------------------------- | ----------------------------- | +| ConfigurableListableBeanFactory getBeanFactory() | 获取到 ioc 使用的 beanfactory | +| ClassLoader getClassLoader() | 获取类加载器 | +| Environment getEnvironment() | 获取当前环境信息 | +| BeanDefinitionRegistry getRegistry() | 获取到bean定义的注册类 | + +* ClassCondition + + ```java + public class ClassCondition implements Condition { + /** + * context 上下文对象。用于获取环境,IOC容器,ClassLoader对象 + * metadata 注解元对象。 可以用于获取注解定义的属性值 + */ + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + + //1.需求: 导入Jedis坐标后创建Bean + //思路:判断redis.clients.jedis.Jedis.class文件是否存在 + boolean flag = true; + try { + Class cls = Class.forName("redis.clients.jedis.Jedis"); + } catch (ClassNotFoundException e) { + flag = false; + } + return flag; + } + } + ``` + +* UserConfig + + ```java + @Configuration + public class UserConfig { + @Bean + @Conditional(ClassCondition.class) + public User user(){ + return new User(); + } + } + ``` + +* 启动类: + + ```java + @SpringBootApplication + public class SpringbootConditionApplication { + public static void main(String[] args) { + //启动SpringBoot应用,返回Spring的IOC容器 + ConfigurableApplicationContext context = SpringApplication.run(SpringbootConditionApplication.class, args); + + Object user = context.getBean("user"); + System.out.println(user); + } + } + ``` + + + +*** + + + +##### 自定义注解 + +将类的判断定义为动态的,判断哪个字节码文件存在可以动态指定 + +* 自定义条件注解类 + + ```java + @Target({ElementType.TYPE, ElementType.METHOD}) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @Conditional(ClassCondition.class) + public @interface ConditionOnClass { + String[] value(); + } + ``` + +* ClassCondition + + ```java + public class ClassCondition implements Condition { + @Override + public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata) { + + //需求:通过注解属性值value指定坐标后创建bean + Map map = metadata.getAnnotationAttributes + (ConditionOnClass.class.getName()); + //map = {value={属性值}} + //获取所有的 + String[] value = (String[]) map.get("value"); + + boolean flag = true; + try { + for (String className : value) { + Class cls = Class.forName(className); + } + } catch (Exception e) { + flag = false; + } + return flag; + } + } + ``` + +* UserConfig + + ```java + @Configuration + public class UserConfig { + @Bean + @ConditionOnClass("com.alibaba.fastjson.JSON")//JSON加载了才注册 User 到容器 + public User user(){ + return new User(); + } + } + ``` + +* 测试 User 对象的创建 + + + +*** + + + +##### 常用注解 + +SpringBoot 提供的常用条件注解: + +@ConditionalOnProperty:判断**配置文件**中是否有对应属性和值才初始化 Bean + +```java +@Configuration +public class UserConfig { + @Bean + @ConditionalOnProperty(name = "it", havingValue = "seazean") + public User user() { + return new User(); + } +} +``` + +```properties +it=seazean +``` + +@ConditionalOnClass:判断环境中是否有对应类文件才初始化 Bean + +@ConditionalOnMissingClass:判断环境中是否有对应类文件才初始化 Bean + +@ConditionalOnMissingBean:判断环境中没有对应Bean才初始化 Bean + + + +***** + + + +#### ImportRes + +使用 bean.xml 文件生成配置 bean,如果需要继续复用 bean.xml,@ImportResource 导入配置文件即可 + +```java +@ImportResource("classpath:beans.xml") +public class MyConfig { + //... +} +``` + +```xml + + + + + + + + + + +``` + + + +**** + + + +#### Properties + +@ConfigurationProperties:读取到 properties 文件中的内容,并且封装到 JavaBean 中 + +配置文件: + +```properties +mycar.brand=BYD +mycar.price=100000 +``` + +JavaBean 类: + +```java +@Component //导入到容器内 +@ConfigurationProperties(prefix = "mycar")//代表配置文件的前缀 +public class Car { + private String brand; + private Integer price; +} +``` + + + +*** + + + +### 源码解析 + +#### 启动流程 + +应用启动: + +```java +@SpringBootApplication +public class BootApplication { + public static void main(String[] args) { + // 启动代码 + SpringApplication.run(BootApplication.class, args); + } +} +``` + +SpringApplication 构造方法: + +* `this.resourceLoader = resourceLoader`:资源加载器,初始为 null +* `this.webApplicationType = WebApplicationType.deduceFromClasspath()`:判断当前应用的类型,是响应式还是 Web 类 +* `this.bootstrapRegistryInitializers = getBootstrapRegistryInitializersFromSpringFactories()`:**获取引导器** + * 去 **`META-INF/spring.factories`** 文件中找 org.springframework.boot.Bootstrapper + * 寻找的顺序:classpath → spring-beans → boot-devtools → springboot → boot-autoconfigure +* `setInitializers(getSpringFactoriesInstances(ApplicationContextInitializer.class))`:**获取初始化器** + * 去 `META-INF/spring.factories` 文件中找 org.springframework.context.ApplicationContextInitializer +* `setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class))`:**获取监听器** + * 去 `META-INF/spring.factories` 文件中找 org.springframework.context.ApplicationListener + +* `this.mainApplicationClass = deduceMainApplicationClass()`:获取出 main 程序类 + +SpringApplication#run(String... args): + +* `StopWatch stopWatch = new StopWatch()`:停止监听器,**监控整个应用的启停** +* `stopWatch.start()`:记录应用的启动时间 + +* `bootstrapContext = createBootstrapContext()`:**创建引导上下文环境** + * `bootstrapContext = new DefaultBootstrapContext()`:创建默认的引导类环境 + * `this.bootstrapRegistryInitializers.forEach()`:遍历所有的引导器调用 initialize 方法完成初始化设置 +* `configureHeadlessProperty()`:让当前应用进入 headless 模式 + +* `listeners = getRunListeners(args)`:获取所有 RunListener(运行监听器) + * 去 `META-INF/spring.factories` 文件中找 org.springframework.boot.SpringApplicationRunListener +* `listeners.starting(bootstrapContext, this.mainApplicationClass)`:**遍历所有的运行监听器调用 starting 方法** + +* `applicationArguments = new DefaultApplicationArguments(args)`:获取所有的命令行参数 + +* `environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments)`:**准备环境** + + * `environment = getOrCreateEnvironment()`:返回或创建基础环境信息对象 + * `switch (this.webApplicationType)`:根据当前应用的类型创建环境 + * `case SERVLET`:Web 应用环境对应 ApplicationServletEnvironment + * `case REACTIVE`:响应式编程对应 ApplicationReactiveWebEnvironment + * `default`:默认为 Spring 环境 ApplicationEnvironment + * `configureEnvironment(environment, applicationArguments.getSourceArgs())`:读取所有配置源的属性值配置环境 + * `ConfigurationPropertySources.attach(environment)`:属性值绑定环境信息 + * `sources.addFirst(ATTACHED_PROPERTY_SOURCE_NAME,..)`:把 configurationProperties 放入环境的属性信息头部 + + * `listeners.environmentPrepared(bootstrapContext, environment)`:**运行监听器调用 environmentPrepared()**,EventPublishingRunListener 发布事件通知所有的监听器当前环境准备完成 + + * `DefaultPropertiesPropertySource.moveToEnd(environment)`:移动 defaultProperties 属性源到环境中的最后一个源 + + * `bindToSpringApplication(environment)`:与容器绑定当前环境 + + * `ConfigurationPropertySources.attach(environment)`:重新将属性值绑定环境信息 + + * `sources.remove(ATTACHED_PROPERTY_SOURCE_NAME)`:从环境信息中移除 configurationProperties + + * `sources.addFirst(ATTACHED_PROPERTY_SOURCE_NAME,..)`:把 configurationProperties 重新放入环境信息 + +* `configureIgnoreBeanInfo(environment)`:配置忽略的 bean + +* `printedBanner = printBanner(environment)`:打印 SpringBoot 标志 + +* `context = createApplicationContext()`:**创建 IOC 容器** + + `switch (this.webApplicationType)`:根据当前应用的类型创建 IOC 容器 + + * `case SERVLET`:Web 应用环境对应 AnnotationConfigServletWebServerApplicationContext + * `case REACTIVE`:响应式编程对应 AnnotationConfigReactiveWebServerApplicationContext + * `default`:默认为 Spring 环境 AnnotationConfigApplicationContext + +* `context.setApplicationStartup(this.applicationStartup)`:设置一个启动器 + +* `prepareContext()`:配置 IOC 容器的基本信息 + + * `postProcessApplicationContext(context)`:后置处理流程 + + * `applyInitializers(context)`:获取所有的**初始化器调用 initialize() 方法**进行初始化 + * `listeners.contextPrepared(context)`:所有的**运行监听器调用 environmentPrepared() 方法**,EventPublishingRunListener 发布事件通知 IOC 容器准备完成 + * `listeners.contextLoaded(context)`:所有的**运行监听器调用 contextLoaded() 方法**,通知 IOC 加载完成 + +* `refreshContext(context)`:**刷新 IOC 容器** + +* `afterRefresh(context, applicationArguments)`:留给用户自定义容器刷新完成后的处理逻辑 + +* `stopWatch.stop()`:记录应用启动完成的时间 + +* `callRunners(context, applicationArguments)`:调用所有 runners + +* `listeners.started(context)`:所有的运行监听器调用 started() 方法 + +* `listeners.running(context)`:所有的运行监听器调用 running() 方法 + + * 获取容器中的 ApplicationRunner、CommandLineRunner + * `AnnotationAwareOrderComparator.sort(runners)`:合并所有 runner 并且按照 @Order 进行排序 + + * `callRunner()`:遍历所有的 runner,调用 run 方法 + +* `handleRunFailure(context, ex, listeners)`:**处理异常**,出现异常进入该逻辑 + + * `handleExitCode(context, exception)`:处理错误代码 + * `listeners.failed(context, exception)`:运行监听器调用 failed() 方法 + * `reportFailure(getExceptionReporters(context), exception)`:通知异常 + + + +**** + + + +#### 注解分析 + +SpringBoot 定义了一套接口规范,这套规范规定 SpringBoot 在启动时会扫描外部引用 jar 包中的 `META-INF/spring.factories` 文件,将文件中配置的类型信息加载到 Spring 容器,并执行类中定义的各种操作,对于外部的 jar 包,直接引入一个 starter 即可 + +@SpringBootApplication 注解是 `@Configuration`、`@EnableAutoConfiguration`、`@ComponentScan` 注解的集合 + +* @SpringBootApplication 注解 + + ```java + @Inherited + @SpringBootConfiguration //代表 @SpringBootApplication 拥有了该注解的功能 + @EnableAutoConfiguration //同理 + @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), + @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) + // 扫描被 @Component (@Service,@Controller)注解的 bean,容器中将排除TypeExcludeFilter 和 AutoConfigurationExcludeFilter + public @interface SpringBootApplication { } + ``` + +* @SpringBootConfiguration 注解: + + ```java + @Configuration // 代表是配置类 + @Indexed + public @interface SpringBootConfiguration { + @AliasFor(annotation = Configuration.class) + boolean proxyBeanMethods() default true; + } + ``` + + @AliasFor 注解:表示别名,可以注解到自定义注解的两个属性上表示这两个互为别名,两个属性其实是同一个含义相互替代 + +* @ComponentScan 注解:默认扫描当前包及其子级包下的所有文件 + +* **@EnableAutoConfiguration 注解:启用 SpringBoot 的自动配置机制** + + ````java + @AutoConfigurationPackage + @Import(AutoConfigurationImportSelector.class) + public @interface EnableAutoConfiguration { + String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration"; + Class[] exclude() default {}; + String[] excludeName() default {}; + } + ```` + + * @AutoConfigurationPackage:**将添加该注解的类所在的 package 作为自动配置 package 进行管理**,把启动类所在的包设置一次,为了给各种自动配置的第三方库扫描用,比如带 @Mapper 注解的类,Spring 自身其实是不认识的,但自动配置的 Mybatis 需要扫描用到,而 ComponentScan 用来扫描注解类,并没有提供接口给三方使。 + + ```java + @Import(AutoConfigurationPackages.Registrar.class) // 利用 Registrar 给容器中导入组件 + public @interface AutoConfigurationPackage { + String[] basePackages() default {}; //自动配置包,指定了配置类的包 + Class[] basePackageClasses() default {}; + } + ``` + + `register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]))`:注册 BD + + * `new PackageImports(metadata).getPackageNames()`:获取添加当前注解的类的所在包 + * `registry.registerBeanDefinition(BEAN, new BasePackagesBeanDefinition(packageNames))`: + + * @Import(AutoConfigurationImportSelector.class):**首先自动装配的核心类** + + ```java + // 选择导入的类 + public String[] selectImports(AnnotationMetadata annotationMetadata) { + //判断自动装配开关是否打开 + if (!isEnabled(annotationMetadata)) { + return NO_IMPORTS; + } + //获取需要自动装配的配置类 + AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata); + return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations()); + } + ``` + + `getAutoConfigurationEntry(annotationMetadata)`: + + * `attributes = getAttributes(annotationMetadata)`:获取注解的属性信息 + + * `getCandidateConfigurations(annotationMetadata, attributes)`:**获取自动配置的候选项** + + * `List configurations = SpringFactoriesLoader.loadFactoryNames()`:加载资源 + + 参数一:`getSpringFactoriesLoaderFactoryClass()` 获取 @EnableAutoConfiguration 注解类 + + 参数二:`getBeanClassLoader()` 获取类加载器 + + * `urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION)`:获取资源类 + * `FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"`:获取位置 + + * 从 spring-boot-autoconfigure-2.5.3.jar/META-INF/spring.factories 文件中获取自动装配类,**进行条件装配,按需装配** + + * `return new AutoConfigurationEntry(configurations, exclusions)`:封装返回 + +![](https://gitee.com/seazean/images/raw/master/Frame/SpringBoot-自动装配配置文件.png) + + + + + +*** + + + +#### 装配流程 + +Spring Boot 通过 `@EnableAutoConfiguration` 开启自动装配,通过 SpringFactoriesLoader 加载 `META-INF/spring.factories` 中的自动配置类实现自动装配,自动配置类其实就是通过 `@Conditional` 注解按需加载的配置类(JVM 类加载机制),想要其生效必须引入 `spring-boot-starter-xxx` 包实现起步依赖 + +* SpringBoot 先加载所有的自动配置类 xxxxxAutoConfiguration +* 每个自动配置类进行**条件装配**,默认都会绑定配置文件指定的值(xxxProperties 和配置文件进行了绑定) +* SpringBoot 默认会在底层配好所有的组件,如果用户自己配置了**以用户的优先** +* **定制化配置:** + - 用户可以使用 @Bean 新建自己的组件来替换底层的组件 + - 用户可以去看这个组件是获取的配置文件前缀值,在配置文件中修改 + +以 DispatcherServletAutoConfiguration 为例: + +```java +@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) +// 类中的 Bean 默认不是单例 +@Configuration(proxyBeanMethods = false) +@ConditionalOnWebApplication(type = Type.SERVLET) +// 条件装配,环境中有 DispatcherServlet 类才进行自动装配 +@ConditionalOnClass(DispatcherServlet.class) +@AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class) +public class DispatcherServletAutoConfiguration { + // 注册的 DispatcherServlet 的 BeanName + public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME = "dispatcherServlet"; + + @Configuration(proxyBeanMethods = false) + @Conditional(DefaultDispatcherServletCondition.class) + @ConditionalOnClass(ServletRegistration.class) + // 绑定配置文件的属性,从配置文件中获取配置项 + @EnableConfigurationProperties(WebMvcProperties.class) + protected static class DispatcherServletConfiguration { + + // 给容器注册一个 DispatcherServlet,起名字为 dispatcherServlet + @Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME) + public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) { + // 新建一个 DispatcherServlet 设置相关属性 + DispatcherServlet dispatcherServlet = new DispatcherServlet(); + // spring.mvc 中的配置项获取注入,没有就填充默认值 + dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest()); + // ...... + // 返回该对象注册到容器内 + return dispatcherServlet; + } + + @Bean + // 容器中有这个类型组件才进行装配 + @ConditionalOnBean(MultipartResolver.class) + // 容器中没有这个名字 multipartResolver 的组件 + @ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) + // 方法名就是 BeanName + public MultipartResolver multipartResolver(MultipartResolver resolver) { + // 给 @Bean 标注的方法传入了对象参数,这个参数就会从容器中找,因为用户自定义了该类型,以用户配置的优先 + // 但是名字不符合规范,所以获取到该 Bean 并返回到容器一个规范的名称:multipartResolver + return resolver; + } + } +} +``` + +```java +//将配置文件中的 spring.mvc 前缀的属性与该类绑定 +@ConfigurationProperties(prefix = "spring.mvc") +public class WebMvcProperties { } +``` + + + + + +*** + + + +### 事件监听 + +SpringBoot 在项目启动时,会对几个监听器进行回调,可以实现监听器接口,在项目启动时完成一些操作 + +ApplicationContextInitializer、SpringApplicationRunListener、CommandLineRunner、ApplicationRunner + +* MyApplicationRunner + + **自定义监听器的启动时机**:MyApplicationRunner 和 MyCommandLineRunner 都是当项目启动后执行,使用 @Component 放入容器即可使用 + + ```java + //当项目启动后执行run方法 + @Component + public class MyApplicationRunner implements ApplicationRunner { + @Override + public void run(ApplicationArguments args) throws Exception { + System.out.println("ApplicationRunner...run"); + System.out.println(Arrays.asList(args.getSourceArgs()));//properties配置信息 + } + } + ``` + +* MyCommandLineRunner + + ```java + @Component + public class MyCommandLineRunner implements CommandLineRunner { + @Override + public void run(String... args) throws Exception { + System.out.println("CommandLineRunner...run"); + System.out.println(Arrays.asList(args)); + } + } + ``` + +* MyApplicationContextInitializer 的启用要**在 resource 文件夹下添加 META-INF/spring.factories** + + ```properties + org.springframework.context.ApplicationContextInitializer=\ + com.example.springbootlistener.listener.MyApplicationContextInitializer + ``` + + ```java + @Component + public class MyApplicationContextInitializer implements ApplicationContextInitializer { + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + System.out.println("ApplicationContextInitializer....initialize"); + } + } + ``` + +* MySpringApplicationRunListener 的使用要添加**构造器** + + ```java + public class MySpringApplicationRunListener implements SpringApplicationRunListener { + //构造器 + public MySpringApplicationRunListener(SpringApplication sa, String[] args) { + } + + @Override + public void starting() { + System.out.println("starting...项目启动中");//输出SPRING之前 + } + + @Override + public void environmentPrepared(ConfigurableEnvironment environment) { + System.out.println("environmentPrepared...环境对象开始准备"); + } + + @Override + public void contextPrepared(ConfigurableApplicationContext context) { + System.out.println("contextPrepared...上下文对象开始准备"); + } + + @Override + public void contextLoaded(ConfigurableApplicationContext context) { + System.out.println("contextLoaded...上下文对象开始加载"); + } + + @Override + public void started(ConfigurableApplicationContext context) { + System.out.println("started...上下文对象加载完成"); + } + + @Override + public void running(ConfigurableApplicationContext context) { + System.out.println("running...项目启动完成,开始运行"); + } + + @Override + public void failed(ConfigurableApplicationContext context, Throwable exception) { + System.out.println("failed...项目启动失败"); + } + } + ``` + + + + + + + +*** + + + + + +## 配置文件 + +### 配置方式 + +#### 文件类型 + +SpringBoot 是基于约定的,很多配置都有默认值,如果想使用自己的配置替换默认配置,可以使用 application.properties 或者application.yml(application.yaml)进行配置 + +* 默认配置文件名称:application +* 在同一级目录下优先级为:properties > yml > yaml + +例如配置内置 Tomcat 的端口 + +* properties: + + ```properties + server.port=8080 + ``` + +* yml: + + ```yaml + server: port: 8080 + ``` + +* yaml: + + ```yaml + server: port: 8080 + ``` + + + + +*** + + + +#### 加载顺序 + +所有位置的配置文件都会被加载,互补配置,**高优先级配置内容会覆盖低优先级配置内容** + +扫描配置文件的位置按优先级**从高到底**: + +- `file:./config/`:**当前项目**下的 /config 目录下 + +- `file:./`:当前项目的根目录,Project工程目录 + +- `classpath:/config/`:classpath 的 /config 目录 + +- `classpath:/`:classpath 的根目录,就是 resoureces 目录 + +项目外部配置文件加载顺序:外部配置文件的使用是为了对内部文件的配合 + +* 命令行:在 package 打包后的 target 目录下,使用该命令 + + ```sh + java -jar myproject.jar --server.port=9000 + ``` + +* 指定配置文件位置 + + ```sh + java -jar myproject.jar --spring.config.location=e://application.properties + ``` + +* 按优先级从高到底选择配置文件的加载命令 + + ```sh + java -jar myproject.jar + ``` + + + + + +*** + + + +### yaml语法 + +基本语法: + +- 大小写敏感 + +- **数据值前边必须有空格,作为分隔符** + +- 使用缩进表示层级关系 + +- 缩进时不允许使用Tab键,只允许使用空格(各个系统 Tab对应空格数目可能不同,导致层次混乱) + +- 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可 + +- ''#" 表示注释,从这个字符一直到行尾,都会被解析器忽略 + + ```yaml + server: + port: 8080 + address: 127.0.0.1 + ``` + +数据格式: + +* 纯量:单个的、不可再分的值 + + ```yaml + msg1: 'hello \n world' # 单引忽略转义字符 + msg2: "hello \n world" # 双引识别转义字符 + ``` + +* 对象:键值对集合,Map、Hash + + ```yaml + person: + name: zhangsan + age: 20 + # 行内写法 + person: {name: zhangsan} + ``` + + 注意:不建议使用 JSON,应该使用 yaml 语法 + +* 数组:一组按次序排列的值,List、Array + + ```yaml + address: + - beijing + - shanghai + # 行内写法 + address: [beijing,shanghai] + ``` + + ```yaml + allPerson #List + - {name:lisi, age:18} + - {name:wangwu, age:20} + # 行内写法 + allPerson: [{name:lisi, age:18}, {name:wangwu, age:20}] + ``` + +* 参数引用: + + ```yaml + name: lisi + person: + name: ${name} # 引用上边定义的name值 + ``` + + + +*** + + + +### 获取配置 + +三种获取配置文件的方式: + +* 注解 @Value + + ```java + @RestController + public class HelloController { + @Value("${name}") + private String name; + + @Value("${person.name}") + private String name2; + + @Value("${address[0]}") + private String address1; + + @Value("${msg1}") + private String msg1; + + @Value("${msg2}") + private String msg2; + + @RequestMapping("/hello") + public String hello(){ + System.out.println("所有的数据"); + return " hello Spring Boot !"; + } + } + ``` + +* Evironment 对象 + + ```java + @Autowired + private Environment env; + + @RequestMapping("/hello") + public String hello() { + System.out.println(env.getProperty("person.name")); + System.out.println(env.getProperty("address[0]")); + return " hello Spring Boot !"; + } + ``` + +* 注解 @ConfigurationProperties 配合 @Component 使用 + + **注意**:参数 prefix 一定要指定 + + ```java + @Component //不扫描该组件到容器内,无法完成自动装配 + @ConfigurationProperties(prefix = "person") + public class Person { + private String name; + private int age; + private String[] address; + } + ``` + + ```java + @Autowired + private Person person; + + @RequestMapping("/hello") + public String hello() { + System.out.println(person); + //Person{name='zhangsan', age=20, address=[beijing, shanghai]} + return " hello Spring Boot !"; + } + ``` + + + +*** + + + +### 配置提示 + +自定义的类和配置文件绑定一般没有提示,添加如下依赖可以使用提示: + +```xml + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.springframework.boot + spring-boot-configuration-processor + + + + + + +``` + + + + + +*** + + + +### Profile + +@Profile:指定组件在哪个环境的情况下才能被注册到容器中,不指定,任何环境下都能注册这个组件 + + * 加了环境标识的 bean,只有这个环境被激活的时候才能注册到容器中,默认是 default 环境 + * 写在配置类上,只有是指定的环境的时候,整个配置类里面的所有配置才能开始生效 + * 没有标注环境标识的 bean 在,任何环境下都是加载的 + +Profile 的配置: + +* **profile 是用来完成不同环境下,配置动态切换功能** + +* **profile 配置方式**:多 profile 文件方式,提供多个配置文件,每个代表一种环境 + + * application-dev.properties/yml 开发环境 + * application-test.properties/yml 测试环境 + * sapplication-pro.properties/yml 生产环境 + +* yml 多文档方式:在 yml 中使用 --- 分隔不同配置 + + ```yacas + --- + server: + port: 8081 + spring: + profiles:dev + --- + server: + port: 8082 + spring: + profiles:test + --- + server: + port: 8083 + spring: + profiles:pro + --- + ``` + +* **profile 激活方式** + + * 配置文件:在配置文件中配置:spring.profiles.active=dev + + ```properties + spring.profiles.active=dev + ``` + + * 虚拟机参数:在VM options 指定:`-Dspring.profiles.active=dev` + + ![](https://gitee.com/seazean/images/raw/master/Frame/SpringBoot-profile激活方式虚拟机参数.png) + + * 命令行参数:`java –jar xxx.jar --spring.profiles.active=dev` + + 在 Program arguments 里输入,也可以先 package + + + + + + + +*** + + + + + +## Web开发 + +### 功能支持 + +SpringBoot 自动配置了很多约定,大多场景都无需自定义配置 + +* 内容协商视图解析器 ContentNegotiatingViewResolver 和 BeanName 视图解析器 BeanNameViewResolver +* 支持静态资源(包括 webjars)和静态 index.html 页支持 +* 自动注册相关类:Converter、GenericConverter、Formatter +* 内容协商处理器:HttpMessageConverters +* 国际化:MessageCodesResolver + +开发规范: + +* 使用 `@Configuration` + `WebMvcConfigurer` 自定义规则,不使用 `@EnableWebMvc` 注解 +* 声明 `WebMvcRegistrations` 的实现类改变默认底层组件 +* 使用 `@EnableWebMvc` + `@Configuration` + `DelegatingWebMvcConfiguration` 全面接管 SpringMVC + + + +**** + + + +### 静态资源 + +#### 访问规则 + +默认的静态资源路径是 classpath 下的,优先级由高到低为:/META-INF/resources、/resources、 /static、/public 的包内,`/` 表示当前项目的根路径 + +静态映射 `/**` ,表示请求 `/ + 静态资源名` 就直接去默认的资源路径寻找请求的资源 + +处理原理:静请求去寻找 Controller 处理,不能处理的请求就会交给静态资源处理器,静态资源也找不到就响应 404 页面 + +* 修改默认资源路径: + + ```yaml + spring: + web: + resources: + static-locations:: [classpath:/haha/] + ``` + +* 修改静态资源访问前缀,默认是 `/**`: + + ```yaml + spring: + mvc: + static-path-pattern: /resources/** + ``` + + 访问 URL:http://localhost:8080/resources/ + 静态资源名,将所有资源**重定位**到 `/resources/` + +* webjar 访问资源: + + ```xml + + org.webjars + jquery + 3.5.1 + + ``` + + 访问地址:http://localhost:8080/webjars/jquery/3.5.1/jquery.js,后面地址要按照依赖里面的包路径 + + + +**** + + + +#### 欢迎页面 + +静态资源路径下 index.html 默认作为欢迎页面,访问 http://localhost:8080 出现该页面,使用 welcome page 功能不能修改前缀 + +网页标签上的小图标可以自定义规则,把资源重命名为 favicon.ico 放在静态资源目录下即可 + + + +*** + + + +#### 源码分析 + +SpringMVC 功能的自动配置类 WebMvcAutoConfiguration: + +```java +public class WebMvcAutoConfiguration { + //当前项目的根路径 + private static final String SERVLET_LOCATION = "/"; +} +``` + +* 内部类 WebMvcAutoConfigurationAdapter: + + ```java + @Import(EnableWebMvcConfiguration.class) + // 绑定 spring.mvc、spring.web、spring.resources 相关的配置属性 + @EnableConfigurationProperties({ WebMvcProperties.class,ResourceProperties.class, WebProperties.class }) + @Order(0) + public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware { + //有参构造器所有参数的值都会从容器中确定 + public WebMvcAutoConfigurationAdapter(/*参数*/) { + this.resourceProperties = resourceProperties.hasBeenCustomized() ? resourceProperties + : webProperties.getResources(); + this.mvcProperties = mvcProperties; + this.beanFactory = beanFactory; + this.messageConvertersProvider = messageConvertersProvider; + this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable(); + this.dispatcherServletPath = dispatcherServletPath; + this.servletRegistrations = servletRegistrations; + this.mvcProperties.checkConfiguration(); + } + } + ``` + + * ResourceProperties resourceProperties:获取和 spring.resources 绑定的所有的值的对象 + * WebMvcProperties mvcProperties:获取和 spring.mvc 绑定的所有的值的对象 + * ListableBeanFactory beanFactory:Spring 的 beanFactory + * HttpMessageConverters:找到所有的 HttpMessageConverters + * ResourceHandlerRegistrationCustomizer:找到 资源处理器的自定义器。 + * DispatcherServletPath:项目路径 + * ServletRegistrationBean:给应用注册 Servlet、Filter + +* WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter.addResourceHandler():两种静态资源映射规则 + + ```java + public void addResourceHandlers(ResourceHandlerRegistry registry) { + //配置文件设置 spring.resources.add-mappings: false,禁用所有静态资源 + if (!this.resourceProperties.isAddMappings()) { + logger.debug("Default resource handling disabled");//被禁用 + return; + } + //注册webjars静态资源的映射规则 映射 路径 + addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/"); + //注册静态资源路径的映射规则 默认映射 staticPathPattern = "/**" + addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> { + //staticLocations = CLASSPATH_RESOURCE_LOCATIONS + registration.addResourceLocations(this.resourceProperties.getStaticLocations()); + if (this.servletContext != null) { + ServletContextResource resource = new ServletContextResource(this.servletContext, SERVLET_LOCATION); + registration.addResourceLocations(resource); + } + }); + } + ``` + + ```java + @ConfigurationProperties("spring.web") + public class WebProperties { + public static class Resources { + //默认资源路径,优先级从高到低 + static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/", + "classpath:/resources/", + "classpath:/static/", "classpath:/public/" } + private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS; + //可以进行规则重写 + public void setStaticLocations(String[] staticLocations) { + this.staticLocations = appendSlashIfNecessary(staticLocations); + this.customized = true; + } + } + } + ``` + +* WebMvcAutoConfiguration.EnableWebMvcConfiguration.welcomePageHandlerMapping():欢迎页 + + ```java + //spring.web 属性 + @EnableConfigurationProperties(WebProperties.class) + public static class EnableWebMvcConfiguration { + @Bean + public WelcomePageHandlerMapping welcomePageHandlerMapping(/*参数*/) { + WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping( + new TemplateAvailabilityProviders(applicationContext), + applicationContext, getWelcomePage(), + //staticPathPattern = "/**" + this.mvcProperties.getStaticPathPattern()); + return welcomePageHandlerMapping; + } + } + WelcomePageHandlerMapping(/*参数*/) { + //所以限制 staticPathPattern 必须为 /** 才能启用该功能 + if (welcomePage != null && "/**".equals(staticPathPattern)) { + logger.info("Adding welcome page: " + welcomePage); + //重定向 + setRootViewName("forward:index.html"); + } + else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) { + logger.info("Adding welcome page template: index"); + setRootViewName("index"); + } + } + ``` + + WelcomePageHandlerMapping,访问 / 能访问到 index.html + + + +*** + + + +### Rest映射 + +开启 Rest 功能 + +```yaml +spring: + mvc: + hiddenmethod: + filter: + enabled: true #开启页面表单的Rest功能 +``` + +源码分析,注入了 HiddenHttpMethodFilte 解析 Rest 风格的访问: + +```java +public class WebMvcAutoConfiguration { + @Bean + @ConditionalOnMissingBean(HiddenHttpMethodFilter.class) + @ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled") + public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() { + return new OrderedHiddenHttpMethodFilter(); + } +} +``` + +详细源码解析:SpringMVC → 基本操作 → Restful → 识别原理 + +Web 部分源码详解:SpringMVC → 运行原理 + + + +**** + + + +### 内嵌容器 + +SpringBoot 嵌入式 Servlet 容器,默认支持的 webServe:Tomcat、Jetty、Undertow + +配置方式: + +```xml + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + org.springframework.boot + spring-boot-starter-jetty + +``` + +源码分析 ServletWebServerFactoryAutoConfiguration: + +* `SpringApplication.run(BootApplication.class, args)`:应用启动 + +* `ConfigurableApplicationContext.run()`: + + * `context = createApplicationContext()`:创建容器 + + * `applicationContextFactory = ApplicationContextFactory.DEFAULT` + + ```java + ApplicationContextFactory DEFAULT = (webApplicationType) -> { + try { + switch (webApplicationType) { + case SERVLET: + // Servlet 容器,继承自 ServletWebServerApplicationContext + return new AnnotationConfigServletWebServerApplicationContext(); + case REACTIVE: + // 响应式编程 + return new AnnotationConfigReactiveWebServerApplicationContext(); + default: + // 普通 Spring 容器 + return new AnnotationConfigApplicationContext(); + } + } catch (Exception ex) { + throw new IllegalStateException(); + } + } + ``` + + * `applicationContextFactory.create(this.webApplicationType)`:根据应用类型创建容器 + + * `refreshContext(context)`:容器启动 + +内嵌容器工作流程: + +* Web 应用启动,SpringBoot 导入 Web 场景包 tomcat,创建一个 Web 版的 IOC 容器 ServletWebServerApplicationContext + +- ServletWebServerApplicationContext 容器启动时进入 refresh() 逻辑,Spring 容器启动逻辑中,在实例化非懒加载的单例 Bean 之前有一个方法 **onRefresh()**,留给子类去扩展,该**容器就是重写这个方法创建 WebServer** + + ```java + protected void onRefresh() { + //省略.... + createWebServer(); + } + private void createWebServer() { + ServletWebServerFactory factory = getWebServerFactory(); + this.webServer = factory.getWebServer(getSelfInitializer()); + createWebServer.end(); + } + ``` + + 获取 WebServer 工厂 ServletWebServerFactory,并且获取的数量不等于 1 会报错,Spring 底层有三种: + + `TomcatServletWebServerFactory`、`JettyServletWebServerFactory`、`UndertowServletWebServerFactory` + +- 自动配置类 ServletWebServerFactoryAutoConfiguration 导入了 ServletWebServerFactoryConfiguration(配置类),根据条件装配判断系统中到底导入了哪个 Web 服务器的包,创建出服务器并启动 + +- 默认是 web-starter 导入 tomcat 包,容器中就有 TomcatServletWebServerFactory,创建出 Tomcat 服务器并启动, + + ```java + public TomcatWebServer(Tomcat tomcat, boolean autoStart, Shutdown shutdown) { + // 初始化 + initialize(); + } + ``` + + 初始化方法 initialize 中有启动方法:`this.tomcat.start()` + + + +*** + + + +### 自定义 + +#### 定制规则 + +```java +@Configuration +public class MyWebMvcConfigurer implements WebMvcConfigurer { + @Bean + public WebMvcConfigurer webMvcConfigurer() { + return new WebMvcConfigurer() { + //进行一些方法重写,来实现自定义的规则 + //比如添加一些解析器和拦截器,就是对原始容器功能的增加 + } + } + //也可以不加 @Bean,直接从这里重写方法进行功能增加 +} +``` + + + +*** + + + +#### 定制容器 + +@EnableWebMvc:全面接管 SpringMVC,所有规则全部自己重新配置 + +- @EnableWebMvc + WebMvcConfigurer + @Bean 全面接管SpringMVC + +- @Import(DelegatingWebMvcConfiguration.**class**),该类继承 WebMvcConfigurationSupport,自动配置了一些非常底层的组件,只能保证 SpringMVC 最基本的使用 + +原理:自动配置类 **WebMvcAutoConfiguration** 里面的配置要能生效,WebMvcConfigurationSupport 类不能被加载,所以 @EnableWebMvc 导致配置类失效,从而接管了 SpringMVC + +```java +@ConditionalOnMissingBean(WebMvcConfigurationSupport.class) +public class WebMvcAutoConfiguration {} +``` + +注意:一般不适用此注解 + + + + + +*** + + + + + +## 数据访问 + +### JDBC + +#### 基本使用 + +导入 starter: + +```xml + + + org.springframework.boot + spring-boot-starter-data-jdbc + + + + mysql + mysql-connector-java + + +``` + +单独导入 MySQL 驱动是因为不确定用户使用的什么数据库 + +配置文件: + +```yaml +spring: + datasource: + url: jdbc:mysql://192.168.0.107:3306/db1?useSSL=false # 不加 useSSL 会警告 + username: root + password: 123456 + driver-class-name: com.mysql.jdbc.Driver +``` + +测试文件: + +```java +@Slf4j +@SpringBootTest +class Boot05WebAdminApplicationTests { + + @Autowired + JdbcTemplate jdbcTemplate; + + @Test + void contextLoads() { + Long res = jdbcTemplate.queryForObject("select count(*) from account_tbl", Long.class); + log.info("记录总数:{}", res); + } +} +``` + + + + + +**** + + + +#### 自动配置 + +DataSourceAutoConfiguration:数据源的自动配置 + +```java +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class }) +@EnableConfigurationProperties(DataSourceProperties.class) +public class DataSourceAutoConfiguration { + + @Conditional(PooledDataSourceCondition.class) + @ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) + @Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class, + DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class}) + protected static class PooledDataSourceConfiguration {} +} +// 配置项 +@ConfigurationProperties(prefix = "spring.datasource") +public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {} +``` + +- 底层默认配置好的连接池是:**HikariDataSource** +- 数据库连接池的配置,是容器中没有 DataSource 才自动配置的 +- 修改数据源相关的配置:spring.datasource + +相关配置: + +- DataSourceTransactionManagerAutoConfiguration: 事务管理器的自动配置 +- JdbcTemplateAutoConfiguration: JdbcTemplate 的自动配置 + - 可以修改这个配置项 @ConfigurationProperties(prefix = **"spring.jdbc"**) 来修改JdbcTemplate + - `@AutoConfigureAfter(DataSourceAutoConfiguration.class)`:在 DataSource 装配后装配 +- JndiDataSourceAutoConfiguration: jndi 的自动配置 +- XADataSourceAutoConfiguration: 分布式事务相关 + + + + + +**** + + + +### Druid + +导入坐标: + +```xml + + com.alibaba + druid-spring-boot-starter + 1.1.17 + +``` + +```java +@Configuration +@ConditionalOnClass(DruidDataSource.class) +@AutoConfigureBefore(DataSourceAutoConfiguration.class) +@EnableConfigurationProperties({DruidStatProperties.class, DataSourceProperties.class}) +@Import({DruidSpringAopConfiguration.class, + DruidStatViewServletConfiguration.class, + DruidWebStatFilterConfiguration.class, + DruidFilterConfiguration.class}) +public class DruidDataSourceAutoConfigure {} +``` + +自动配置: + +- 扩展配置项 **spring.datasource.druid** +- DruidSpringAopConfiguration: 监控 SpringBean,配置项为 `spring.datasource.druid.aop-patterns` + +- DruidStatViewServletConfiguration:监控页的配置项为 `spring.datasource.druid.stat-view-servlet`,默认开启 +- DruidWebStatFilterConfiguration:Web 监控配置项为 `spring.datasource.druid.web-stat-filter`,默认开启 + +- DruidFilterConfiguration:所有 Druid 自己 filter 的配置 + +配置示例: + +```yaml +spring: + datasource: + url: jdbc:mysql://localhost:3306/db_account + username: root + password: 123456 + driver-class-name: com.mysql.jdbc.Driver + + druid: + aop-patterns: com.atguigu.admin.* #监控SpringBean + filters: stat,wall # 底层开启功能,stat(sql监控),wall(防火墙) + + stat-view-servlet: # 配置监控页功能 + enabled: true + login-username: admin #项目启动访问:http://localhost:8080/druid ,账号和密码是 admin + login-password: admin + resetEnable: false + + web-stat-filter: # 监控web + enabled: true + urlPattern: /* + exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*' + + + filter: + stat: # 对上面filters里面的stat的详细配置 + slow-sql-millis: 1000 + logSlowSql: true + enabled: true + wall: + enabled: true + config: + drop-table-allow: false +``` + + + +配置示例:https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter + +配置项列表:https://github.com/alibaba/druid/wiki/DruidDataSource%E9%85%8D%E7%BD%AE%E5%B1%9E%E6%80%A7%E5%88%97%E8%A1%A8 + + + +**** + + + +### MyBatis + +#### 基本使用 + +导入坐标: + +```xml + + org.mybatis.spring.boot + mybatis-spring-boot-starter + 2.1.4 + +``` + +* 编写 MyBatis 相关配置:application.yml + + ```yaml + # 配置mybatis规则 + mybatis: + # config-location: classpath:mybatis/mybatis-config.xml 建议不写 + mapper-locations: classpath:mybatis/mapper/*.xml + configuration: + map-underscore-to-camel-case: true + + #可以不写全局配置文件,所有全局配置文件的配置都放在 configuration 配置项中即可 + ``` + +* 定义表和实体类 + + ```java + public class User { + private int id; + private String username; + private String password; + } + ``` + +* 编写 dao 和 mapper 文件/纯注解开发 + + dao:**@Mapper 注解一定要加,否则在启动类指定 @MapperScan() 扫描路径(不建议)** + + ```java + @Mapper //必须加Mapper + @Repository + public interface UserXmlMapper { + public List findAll(); + } + ``` + + mapper.xml + + ```xml + + + + + + ``` + +* 纯注解开发 + + ```java + @Mapper + @Repository + public interface UserMapper { + @Select("select * from t_user") + public List findAll(); + } + ``` + + + +**** + + + +#### 自动配置 + +MybatisAutoConfiguration: + +```java +@EnableConfigurationProperties(MybatisProperties.class) //MyBatis配置项绑定类。 +@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class }) +public class MybatisAutoConfiguration { + @Bean + @ConditionalOnMissingBean + public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { + SqlSessionFactoryBean factory = new SqlSessionFactoryBean(); + return factory.getObject(); + } + + @org.springframework.context.annotation.Configuration + @Import(AutoConfiguredMapperScannerRegistrar.class) + @ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class }) + public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {} +} + +@ConfigurationProperties(prefix = "mybatis") +public class MybatisProperties {} +``` + +* 配置文件:`mybatis` +* 自动配置了 SqlSessionFactory +* 导入 `AutoConfiguredMapperScannerRegistra` 实现 @Mapper 的扫描 + + + +**** + + + +#### MyBatis-Plus + +```xml + + com.baomidou + mybatis-plus-boot-starter + 3.4.1 + +``` + +自动配置类:MybatisPlusAutoConfiguration + +只需要 Mapper 继承 **BaseMapper** 就可以拥有 CRUD 功能 + + + +*** + + + +### Redis + +#### 基本使用 + +```xml + + org.springframework.boot + spring-boot-starter-data-redis + +``` + +* 配置redis相关属性 + + ```yaml + spring: + redis: + host: 127.0.0.1 # redis的主机ip + port: 6379 + ``` + +* 注入 RedisTemplate 模板 + + ```java + @RunWith(SpringRunner.class) + @SpringBootTest + public class SpringbootRedisApplicationTests { + @Autowired + private RedisTemplate redisTemplate; + + @Test + public void testSet() { + //存入数据 + redisTemplate.boundValueOps("name").set("zhangsan"); + } + @Test + public void testGet() { + //获取数据 + Object name = redisTemplate.boundValueOps("name").get(); + System.out.println(name); + } + } + ``` + + + +**** + + + +#### 自动配置 + +RedisAutoConfiguration 自动配置类 + +```java +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(RedisOperations.class) +@EnableConfigurationProperties(RedisProperties.class) +@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class }) +public class RedisAutoConfiguration { + @Bean + @ConditionalOnMissingBean(name = "redisTemplate") + @ConditionalOnSingleCandidate(RedisConnectionFactory.class) + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(redisConnectionFactory); + return template; + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnSingleCandidate(RedisConnectionFactory.class) + public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) { + StringRedisTemplate template = new StringRedisTemplate(); + template.setConnectionFactory(redisConnectionFactory); + return template; + } + +} +``` + +- 配置项:`spring.redis` +- 自动导入了连接工厂配置类:LettuceConnectionConfiguration、JedisConnectionConfiguration + +- 自动注入了模板类:RedisTemplate 、StringRedisTemplate,k v 都是 String 类型 + +- 使用 @Autowired 注入模板类就可以操作 redis + + + + + +**** + + + + + +## 单元测试 + +### Junit5 + +Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库,由三个不同的子模块组成: + +* JUnit Platform:在 JVM 上启动测试框架的基础,不仅支持 Junit 自制的测试引擎,其他测试引擎也可以接入 + +* JUnit Jupiter:提供了 JUnit5 的新的编程模型,是 JUnit5 新特性的核心,内部包含了一个测试引擎,用于在 Junit Platform 上运行 + +* JUnit Vintage:JUnit Vintage 提供了兼容 JUnit4.x、Junit3.x 的测试引擎 + + 注意:SpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖,如果需要兼容 Junit4 需要自行引入 + +```java +@SpringBootTest +class Boot05WebAdminApplicationTests { + @Test + void contextLoads() { + + } +} +``` + + + + + +*** + + + +### 常用注解 + +JUnit5 的注解如下: + +- @Test:表示方法是测试方法,但是与 JUnit4 的 @Test 不同,它的职责非常单一不能声明任何属性,拓展的测试将会由 Jupiter 提供额外测试,包是 `org.junit.jupiter.api.Test` +- @ParameterizedTest:表示方法是参数化测试 + +- @RepeatedTest:表示方法可重复执行 +- @DisplayName:为测试类或者测试方法设置展示名称 + +- @BeforeEach:表示在每个单元测试之前执行 +- @AfterEach:表示在每个单元测试之后执行 + +- @BeforeAll:表示在所有单元测试之前执行 +- @AfterAll:表示在所有单元测试之后执行 + +- @Tag:表示单元测试类别,类似于 JUnit4 中的 @Categories +- @Disabled:表示测试类或测试方法不执行,类似于 JUnit4 中的 @Ignore + +- @Timeout:表示测试方法运行如果超过了指定时间将会返回错误 +- @ExtendWith:为测试类或测试方法提供扩展类引用 + + + +**** + + + +### 断言机制 + +#### 简单断言 + +断言(assertions)是测试方法中的核心,用来对测试需要满足的条件进行验证,断言方法都是 org.junit.jupiter.api.Assertions 的静态方法 + +用来对单个值进行简单的验证: + +| 方法 | 说明 | +| --------------- | ------------------------------------ | +| assertEquals | 判断两个对象或两个原始类型是否相等 | +| assertNotEquals | 判断两个对象或两个原始类型是否不相等 | +| assertSame | 判断两个对象引用是否指向同一个对象 | +| assertNotSame | 判断两个对象引用是否指向不同的对象 | +| assertTrue | 判断给定的布尔值是否为 true | +| assertFalse | 判断给定的布尔值是否为 false | +| assertNull | 判断给定的对象引用是否为 null | +| assertNotNull | 判断给定的对象引用是否不为 null | + +```java +@Test +@DisplayName("simple assertion") +public void simple() { + assertEquals(3, 1 + 2, "simple math"); + assertNull(null); + assertNotNull(new Object()); +} +``` + + + +**** + + + +#### 数组断言 + +通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等 + +```java +@Test +@DisplayName("array assertion") +public void array() { + assertArrayEquals(new int[]{1, 2}, new int[] {1, 2}); +} +``` + + + +*** + + + +#### 组合断言 + +assertAll 方法接受多个 org.junit.jupiter.api.Executable 函数式接口的实例作为验证的断言,可以通过 lambda 表达式提供这些断言 + +```java +@Test +@DisplayName("assert all") +public void all() { + assertAll("Math", + () -> assertEquals(2, 1 + 1), + () -> assertTrue(1 > 0) + ); +} +``` + + + +*** + + + +#### 异常断言 + +Assertions.assertThrows(),配合函数式编程就可以进行使用 + +```java +@Test +@DisplayName("异常测试") +public void exceptionTest() { + ArithmeticException exception = Assertions.assertThrows( + //扔出断言异常 + ArithmeticException.class, () -> System.out.println(1 / 0) + ); +} +``` + + + +**** + + + +#### 超时断言 + +Assertions.assertTimeout() 为测试方法设置了超时时间 + +```java +@Test +@DisplayName("超时测试") +public void timeoutTest() { + //如果测试方法时间超过1s将会异常 + Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500)); +} +``` + + + +**** + + + +#### 快速失败 + +通过 fail 方法直接使得测试失败 + +```java +@Test +@DisplayName("fail") +public void shouldFail() { + fail("This should fail"); +} +``` + + + + + +*** + + + +### 前置条件 + +JUnit 5 中的前置条件(assumptions)类似于断言,不同之处在于**不满足的断言会使得测试方法失败**,而不满足的**前置条件只会使得测试方法的执行终止**,前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要 + +```java +@DisplayName("测试前置条件") +@Test +void testassumptions(){ + Assumptions.assumeTrue(false,"结果不是true"); + System.out.println("111111"); + +} +``` + + + +*** + + + +### 嵌套测试 + +JUnit 5 可以通过 Java 中的内部类和 @Nested 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起,在内部类中可以使用 @BeforeEach 和 @AfterEach 注解,而且嵌套的层次没有限制 + +```java +@DisplayName("A stack") +class TestingAStackDemo { + + Stack stack; + + @Test + @DisplayName("is instantiated with new Stack()") + void isInstantiatedWithNew() { + assertNull(stack) + } + + @Nested + @DisplayName("when new") + class WhenNew { + + @BeforeEach + void createNewStack() { + stack = new Stack<>(); + } + + @Test + @DisplayName("is empty") + void isEmpty() { + assertTrue(stack.isEmpty()); + } + + @Test + @DisplayName("throws EmptyStackException when popped") + void throwsExceptionWhenPopped() { + assertThrows(EmptyStackException.class, stack::pop); + } + } +} +``` + + + + + +**** + + + +### 参数测试 + +参数化测试是 JUnit5 很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能 + +利用**@ValueSource**等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。 + +* @ValueSource:为参数化测试指定入参来源,支持八大基础类以及 String 类型、Class 类型 + +* @NullSource:表示为参数化测试提供一个 null 的入参 + +* @EnumSource:表示为参数化测试提供一个枚举入参 + +* @CsvFileSource:表示读取指定 CSV 文件内容作为参数化测试入参 + +* @MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流) + + + + + +*** + + + + + +## 指标监控 + +### Actuator + +每一个微服务在云上部署以后,都需要对其进行监控、追踪、审计、控制等,SpringBoot 抽取了 Actuator 场景,使得每个微服务快速引用即可获得生产级别的应用监控、审计等功能 + +```xml + + org.springframework.boot + spring-boot-starter-actuator + +``` + +暴露所有监控信息为 HTTP: + +```yaml +management: + endpoints: + enabled-by-default: true #暴露所有端点信息 + web: + exposure: + include: '*' #以web方式暴露 +``` + +访问 http://localhost:8080/actuator/[beans/health/metrics/] + +可视化界面:https://github.com/codecentric/spring-boot-admin + + + +**** + + + +### Endpoint + +默认所有的 Endpoint 除过 shutdown 都是开启的 + +```yaml +management: + endpoints: + enabled-by-default: false #禁用所有的 + endpoint: #手动开启一部分 + beans: + enabled: true + health: + enabled: true +``` + +端点: + +| ID | 描述 | +| ------------------ | ------------------------------------------------------------ | +| `auditevents` | 暴露当前应用程序的审核事件信息。需要一个 `AuditEventRepository` 组件 | +| `beans` | 显示应用程序中所有 Spring Bean 的完整列表 | +| `caches` | 暴露可用的缓存 | +| `conditions` | 显示自动配置的所有条件信息,包括匹配或不匹配的原因 | +| `configprops` | 显示所有 `@ConfigurationProperties` | +| `env` | 暴露 Spring 的属性 `ConfigurableEnvironment` | +| `flyway` | 显示已应用的所有 Flyway 数据库迁移。 需要一个或多个 Flyway 组件。 | +| `health` | 显示应用程序运行状况信息 | +| `httptrace` | 显示 HTTP 跟踪信息,默认情况下 100 个 HTTP 请求-响应需要一个 `HttpTraceRepository` 组件 | +| `info` | 显示应用程序信息 | +| `integrationgraph` | 显示 Spring integrationgraph,需要依赖 `spring-integration-core` | +| `loggers` | 显示和修改应用程序中日志的配置 | +| `liquibase` | 显示已应用的所有 Liquibase 数据库迁移,需要一个或多个 Liquibase 组件 | +| `metrics` | 显示当前应用程序的指标信息。 | +| `mappings` | 显示所有 `@RequestMapping` 路径列表 | +| `scheduledtasks` | 显示应用程序中的计划任务 | +| `sessions` | 允许从 Spring Session 支持的会话存储中检索和删除用户会话,需要使用 Spring Session 的基于 Servlet 的 Web 应用程序 | +| `shutdown` | 使应用程序正常关闭,默认禁用 | +| `startup` | 显示由 `ApplicationStartup` 收集的启动步骤数据。需要使用 `SpringApplication` 进行配置 `BufferingApplicationStartup` | +| `threaddump` | 执行线程转储 | + +应用程序是 Web 应用程序(Spring MVC,Spring WebFlux 或 Jersey),则可以使用以下附加端点: + +| ID | 描述 | +| ------------ | ------------------------------------------------------------ | +| `heapdump` | 返回 `hprof` 堆转储文件。 | +| `jolokia` | 通过 HTTP 暴露 JMX bean(需要引入 Jolokia,不适用于 WebFlux),需要引入依赖 `jolokia-core` | +| `logfile` | 返回日志文件的内容(如果已设置 `logging.file.name` 或 `logging.file.path` 属性),支持使用 HTTP Range标头来检索部分日志文件的内容。 | +| `prometheus` | 以 Prometheus 服务器可以抓取的格式公开指标,需要依赖 `micrometer-registry-prometheus` | + +常用 Endpoint: + +- Health:监控状况 +- Metrics:运行时指标 + +- Loggers:日志记录 + + + + + +*** + + + + + +## 项目部署 + +SpringBoot 项目开发完毕后,支持两种方式部署到服务器: + +* jar 包 (官方推荐,默认) +* war 包 + +**更改 pom 文件中的打包方式为 war** + +* 修改启动类 + + ```java + @SpringBootApplication + public class SpringbootDeployApplication extends SpringBootServletInitializer { + public static void main(String[] args) { + SpringApplication.run(SpringbootDeployApplication.class, args); + } + + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder b) { + return b.sources(SpringbootDeployApplication.class); + } + } + ``` + +* 指定打包的名称 + + ```xml + war + + springboot + + + org.springframework.boot + spring-boot-maven-plugin + + + + ``` + + + + From 029168a33f199196270f1fe7382884ae2bdb3076 Mon Sep 17 00:00:00 2001 From: Seazean Date: Fri, 6 Aug 2021 17:01:12 +0800 Subject: [PATCH 013/168] Update Java Notes --- SSM.md | 81 +++++++++++++++++++++++++++++++++++----------------------- 1 file changed, 49 insertions(+), 32 deletions(-) diff --git a/SSM.md b/SSM.md index 6d90ad4..b6b546a 100644 --- a/SSM.md +++ b/SSM.md @@ -6342,22 +6342,22 @@ public class UserServiceJDKProxy { #### CGLIB -CGLIB(Code Generation Library):Code生成类库 +CGLIB(Code Generation Library):Code 生成类库 CGLIB 特点: * CGLIB 动态代理**不限定**是否具有接口,可以对任意操作进行增强 * CGLIB 动态代理无需要原始被代理对象,动态创建出新的代理对象 -* CGLIB **继承被代理类**,如果代理类是final则不能实现 +* CGLIB **继承被代理类**,如果代理类是 final 则不能实现 ![](https://gitee.com/seazean/images/raw/master/Frame/AOP底层原理-cglib.png) -* cglib类 +* CGLIB 类 - * JDKProxy仅对接口方法做增强,cglib对所有方法做增强,包括Object类中的方法 (toString、hashCode) + * JDKProxy 仅对接口方法做增强,CGLIB 对所有方法做增强,包括 Object 类中的方法(toString、hashCode) * 返回值类型采用多态向下转型,所以需要设置父类类型 - 需要对方法进行判断是否是save,来选择性增强 + 需要对方法进行判断是否是 save,来选择性增强 ```java public class UserServiceImplCglibProxy { @@ -14207,7 +14207,7 @@ public class MyConfig { ``` ```xml - + @@ -14346,6 +14346,10 @@ SpringApplication#run(String... args): * `refreshContext(context)`:**刷新 IOC 容器** + * Spring 的容器启动流程 + * `invokeBeanFactoryPostProcessors(beanFactory)`:**实现了自动装配** + * `onRefresh()`:**创建 WebServer** 使用该接口 + * `afterRefresh(context, applicationArguments)`:留给用户自定义容器刷新完成后的处理逻辑 * `stopWatch.stop()`:记录应用启动完成的时间 @@ -14418,7 +14422,7 @@ SpringBoot 定义了一套接口规范,这套规范规定 SpringBoot 在启动 } ```` - * @AutoConfigurationPackage:**将添加该注解的类所在的 package 作为自动配置 package 进行管理**,把启动类所在的包设置一次,为了给各种自动配置的第三方库扫描用,比如带 @Mapper 注解的类,Spring 自身其实是不认识的,但自动配置的 Mybatis 需要扫描用到,而 ComponentScan 用来扫描注解类,并没有提供接口给三方使。 + * @AutoConfigurationPackage:**将添加该注解的类所在的 package 作为自动配置 package 进行管理**,把启动类所在的包设置一次,为了给各种自动配置的第三方库扫描用,比如带 @Mapper 注解的类,Spring 自身是不能识别的,但自动配置的 Mybatis 需要扫描用到,而 ComponentScan 只是用来扫描注解类,并没有提供接口给三方使用 ```java @Import(AutoConfigurationPackages.Registrar.class) // 利用 Registrar 给容器中导入组件 @@ -14431,43 +14435,56 @@ SpringBoot 定义了一套接口规范,这套规范规定 SpringBoot 在启动 `register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]))`:注册 BD * `new PackageImports(metadata).getPackageNames()`:获取添加当前注解的类的所在包 - * `registry.registerBeanDefinition(BEAN, new BasePackagesBeanDefinition(packageNames))`: + * `registry.registerBeanDefinition(BEAN, new BasePackagesBeanDefinition(packageNames))`:存放到容器中 + * `new BasePackagesBeanDefinition(packageNames)`:把当前主类所在的包名封装到该对象中 * @Import(AutoConfigurationImportSelector.class):**首先自动装配的核心类** + 容器刷新时执行:**invokeBeanFactoryPostProcessors()** → invokeBeanDefinitionRegistryPostProcessors() → postProcessBeanDefinitionRegistry() → processConfigBeanDefinitions() → parse() → process() → processGroupImports() → getImports() → process() → **AutoConfigurationImportSelector#getAutoConfigurationEntry()** + ```java - // 选择导入的类 - public String[] selectImports(AnnotationMetadata annotationMetadata) { - //判断自动装配开关是否打开 + protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) { if (!isEnabled(annotationMetadata)) { - return NO_IMPORTS; - } - //获取需要自动装配的配置类 - AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata); - return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations()); + return EMPTY_ENTRY; + } + // 获取注解属性,@SpringBootApplication 注解的 exclude 属性和 excludeName 属性 + AnnotationAttributes attributes = getAttributes(annotationMetadata); + // 获取所有需要自动装配的候选项 + List configurations = getCandidateConfigurations(annotationMetadata, attributes); + // 去除重复的选项 + configurations = removeDuplicates(configurations); + // 获取注解配置的排除的自动装配类 + Set exclusions = getExclusions(annotationMetadata, attributes); + checkExcludedClasses(configurations, exclusions); + // 移除所有的配置的不需要自动装配的类 + configurations.removeAll(exclusions); + // 过滤,条件装配 + configurations = getConfigurationClassFilter().filter(configurations); + // 获取 AutoConfigurationImportListener 类的监听器调用 onAutoConfigurationImportEvent 方法 + fireAutoConfigurationImportEvents(configurations, exclusions); + // 包装成 AutoConfigurationEntry 返回 + return new AutoConfigurationEntry(configurations, exclusions); } ``` - `getAutoConfigurationEntry(annotationMetadata)`: - - * `attributes = getAttributes(annotationMetadata)`:获取注解的属性信息 - - * `getCandidateConfigurations(annotationMetadata, attributes)`:**获取自动配置的候选项** + AutoConfigurationImportSelector#getCandidateConfigurations:获取自动配置的候选项 - * `List configurations = SpringFactoriesLoader.loadFactoryNames()`:加载资源 + * `List configurations = SpringFactoriesLoader.loadFactoryNames()`:加载自动配置类 - 参数一:`getSpringFactoriesLoaderFactoryClass()` 获取 @EnableAutoConfiguration 注解类 + 参数一:`getSpringFactoriesLoaderFactoryClass()` 获取 @EnableAutoConfiguration 注解类 - 参数二:`getBeanClassLoader()` 获取类加载器 + 参数二:`getBeanClassLoader()` 获取类加载器 + * `factoryTypeName = factoryType.getName()`:@EnableAutoConfiguration 注解的全类名 + * `return loadSpringFactories(classLoaderToUse).getOrDefault()`:加载资源 * `urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION)`:获取资源类 - * `FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"`:获取位置 + * `FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"`:**加载的资源的位置** - * 从 spring-boot-autoconfigure-2.5.3.jar/META-INF/spring.factories 文件中获取自动装配类,**进行条件装配,按需装配** + * `return configurations`:返回所有自动装配类的候选项 - * `return new AutoConfigurationEntry(configurations, exclusions)`:封装返回 + * 从 spring-boot-autoconfigure-2.5.3.jar/META-INF/spring.factories 文件中获取自动装配类,**进行条件装配,按需装配** -![](https://gitee.com/seazean/images/raw/master/Frame/SpringBoot-自动装配配置文件.png) + ![](https://gitee.com/seazean/images/raw/master/Frame/SpringBoot-自动装配配置文件.png) @@ -15247,7 +15264,7 @@ SpringBoot 嵌入式 Servlet 容器,默认支持的 webServe:Tomcat、Jetty org.springframework.boot spring-boot-starter-web - + org.springframework.boot spring-boot-starter-tomcat @@ -15259,13 +15276,13 @@ SpringBoot 嵌入式 Servlet 容器,默认支持的 webServe:Tomcat、Jetty ``` -源码分析 ServletWebServerFactoryAutoConfiguration: +创建 Web 容器: * `SpringApplication.run(BootApplication.class, args)`:应用启动 * `ConfigurableApplicationContext.run()`: - * `context = createApplicationContext()`:创建容器 + * `context = createApplicationContext()`:**创建容器** * `applicationContextFactory = ApplicationContextFactory.DEFAULT` @@ -15315,7 +15332,7 @@ SpringBoot 嵌入式 Servlet 容器,默认支持的 webServe:Tomcat、Jetty `TomcatServletWebServerFactory`、`JettyServletWebServerFactory`、`UndertowServletWebServerFactory` -- 自动配置类 ServletWebServerFactoryAutoConfiguration 导入了 ServletWebServerFactoryConfiguration(配置类),根据条件装配判断系统中到底导入了哪个 Web 服务器的包,创建出服务器并启动 +- **自动配置类 ServletWebServerFactoryAutoConfiguration** 导入了 ServletWebServerFactoryConfiguration(配置类),根据条件装配判断系统中到底导入了哪个 Web 服务器的包,创建出服务器并启动 - 默认是 web-starter 导入 tomcat 包,容器中就有 TomcatServletWebServerFactory,创建出 Tomcat 服务器并启动, From 47d58be1b564a2402ae78c7908ebba2eb5e93831 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 7 Aug 2021 23:38:58 +0800 Subject: [PATCH 014/168] Update Java Notes --- DB.md | 10 +- Issue.md | 44 ++-- Java.md | 42 ++-- Prog.md | 648 ++++++++++++++++++++++++++++--------------------------- SSM.md | 23 +- 5 files changed, 399 insertions(+), 368 deletions(-) diff --git a/DB.md b/DB.md index 6544177..6c85b72 100644 --- a/DB.md +++ b/DB.md @@ -4335,7 +4335,7 @@ EXPLAIN SELECT * FROM table_1 WHERE id = 1; | filtered | 按表条件过滤的行百分比 | | extra | 执行情况的说明和描述 | -MySQL执行计划的局限: +MySQL 执行计划的局限: * 只是计划,不是执行 SQL 语句 @@ -4433,13 +4433,13 @@ SQL 执行的顺序的标识,SQL 从大到小的执行 | ALL | Full Table Scan,MySQL将遍历全表以找到匹配的行,全表扫描 | | index | Full Index Scan,index 与 ALL 区别为 index 类型只遍历索引树 | | range | 索引范围扫描,常见于between、<、>等的查询 | -| ref | 非唯一性索引扫描,返回匹配某个单独值的所有,本质上也是一种索引访问,常见于使用非唯一索引即唯一索引的非唯一前缀进行的查找 | +| ref | 非唯一性索引扫描,返回匹配某个单独值的所有,本质上也是一种索引访问 | | eq_ref | 唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配,常见于主键或唯一索引扫描 | | const | 当 MySQL 对查询某部分进行优化,并转换为一个常量时,使用该类型访问,如将主键置于where列表中,MySQL就能将该查询转换为一个常量 | | system | system 是 const 类型的特例,当查询的表只有一行的情况下,使用system | | NULL | MySQL在优化过程中分解语句,执行时甚至不用访问表或索引 | -从上到下,性能从差到好,一般来说需要保证查询至少达到 range 级别, 最好达到ref +从上到下,性能从差到好,一般来说需要保证查询至少达到 range 级别, 最好达到 ref @@ -4508,7 +4508,7 @@ SHOW PROFILES 能够在做 SQL 优化时分析当前会话中语句执行的资 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL- profiling.png) ```mysql - SET profiling=1; //开启profiling 开关; + SET profiling=1; #开启profiling 开关; ``` * 执行 SHOW PROFILES 指令, 来查看 SQL 语句执行的耗时: @@ -5186,7 +5186,7 @@ SQL 提示,是优化数据库的一个重要手段,就是在SQL语句中加 INSERT INTO tb_test VALUES(1,'Tom'),(2,'Cat'),(3,'Jerry'); -- 连接一次 ``` -增加 cache 层:在应用中增加缓存层来达到减轻数据库负担的目的。可以部分数据从数据库中抽取出来放到应用端以文本方式存储, 或者使用框架(Mybatis)提供的一级缓存 / 二级缓存,或者使用Redis数据库来缓存数据 +增加 cache 层:在应用中增加缓存层来达到减轻数据库负担的目的。可以部分数据从数据库中抽取出来放到应用端以文本方式存储, 或者使用框架(Mybatis)提供的一级缓存 / 二级缓存,或者使用 Redis 数据库来缓存数据 diff --git a/Issue.md b/Issue.md index 3303f0e..16e3d57 100644 --- a/Issue.md +++ b/Issue.md @@ -60,14 +60,7 @@ * **为什么要进行三次握手?** - **原因一**:tcp是全双工可靠的传输协议,全双工意味着双方能够同时向对方发送数据,可靠意味着我发送的数据必须确认对方完整收到了。tcp是通过序列号来保证这两种性质的,**三次握手就是互换序列号**的一次过程 - - 1. A 发送同步信号**SYN** + **A's Initial sequence number** - 2. B 确认收到A的同步信号,并记录 A's ISN 到本地,命名 **B's ACK sequence number** - 3. B发送同步信号**SYN** + **B's Initial sequence number** - 4. A确认收到B的同步信号,并记录 B's ISN 到本地,命名 **A's ACK sequence number** - - * 很显然2和3 这两个步骤可以合并,**只需要三次握手,**可以提高连接的速度与效率。 + **原因一**:TCP 是全双工可靠的传输协议,全双工意味着双方能够同时向对方发送数据,可靠意味着我发送的数据必须确认对方完整收到。TCP 通过序列号来保证这两种性质的,**三次握手就是互换序列号**的一次过程,可以确保双方的发信和收信能力都是正常的 **原因二**:第三次握手是为了防止失效的连接请求到达服务器,让服务器错误打开连接。客户端发送的连接请求如果在网络中滞留,那么就会隔很长一段时间才能收到服务器端发回的连接确认。客户端等待一个超时重传时间之后,就会重新请求连接。但是这个滞留的连接请求最后还是会到达服务器,如果不进行三次握手,那么服务器就会打开两个连接。如果有第三次握手,客户端会忽略服务器之后发送的对滞留连接请求的连接确认,不进行第三次握手,因此就不会再次打开连接 @@ -75,27 +68,27 @@ * **三次握手的第三次握手发送ACK能携带数据吗?** - 答:可以。第三次握手,在客户端发送完ACK报文后,就进入ESTABLISHED状态,当服务器收到这个,服务器变为ESTABLISHED状态,可以直接处理携带的数据。 + 答:可以。第三次握手,在客户端发送完ACK报文后,就进入 ESTABLISHED 状态,当服务器收到这个,服务器变为ESTABLISHED状态,可以直接处理携带的数据。 -* **不携带数据的ACK不会超时重传** +* **不携带数据的 ACK 不会超时重传** -* **为什么TCP4次挥手时等待为2MSL?** +* **为什么 TCP4 次挥手时等待为 2MSL?** - **原因一:**A发送完释放连接的应答并不知道B是否接到自己的ACK,所以有两种情况 - 1)如果B没有收到自己的ACK,会超时重传FIN,那么A再次接到重传的FIN,会再次发送ACK - 2)如果B收到自己的ACK,也不会再发任何消息,包括ACK - 无论是1还是2,A都需要等待,要取这两种情况等待时间的最大值,以应对最坏的情况发生,这个最坏情况是:去向ACK消息最大存活时间(MSL) + 来向FIN消息的最大存活时间(MSL), - 这就是**2MSL( Maximum Segment Life)**。等待2MSL时间,A就可以放心地释放TCP占用的资源、端口号,此时可以使用该端口号连接任何服务器。 + **原因一:**A 发送完释放连接的应答并不知道 B 是否接到自己的 ACK,所以有两种情况 + 1)如果 B 没有收到自己的 ACK,会超时重传 FIN,那么 A 再次接到重传的 FIN,会再次发送 ACK + 2)如果 B 收到自己的 ACK,也不会再发任何消息,包括 ACK + 无论是 1 还是 2,A都需要等待,要取这两种情况等待时间的最大值,以应对最坏的情况发生,这个最坏情况是:去向ACK消息最大存活时间(MSL) + 来向 FIN 消息的最大存活时间(MSL), + 这就是**2MSL( Maximum Segment Life)**。等待 2MSL 时间,A 就可以放心地释放 TCP 占用的资源、端口号,此时可以使用该端口号连接任何服务器。 - **原因二:**等待一段时间是为了让本次连接持续时间内所产生的报文都从网络中消失,否则存活在网络里的老的TCP报文可能与新TCP连接报文产生冲突(比如连接同一个端口),为避免此种情况,需要耐心等待网络老的TCP连接的活跃报文全部消失,2MSL时间可以满足这个需求(尽管非常保守) + **原因二:**等待一段时间是为了让本次连接持续时间内所产生的报文都从网络中消失,否则存活在网络里的老的TCP报文可能与新TCP连接报文产生冲突(比如连接同一个端口),为避免此种情况,需要耐心等待网络老的 TCP 连接的活跃报文全部消失,2MSL 时间可以满足这个需求(尽管非常保守) * **为什么连接的时候是三次握手,关闭的时候却是四次握手?** - 答:因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手 + 答:因为当 Server 端收到 Client 端的 SYN 连接请求报文后,可以直接发送 SYN+ACK 报文。其中 ACK 报文是用来应答的,SYN 报文是用来同步的。但是关闭连接时,当 Server 端收到 FIN 报文时,很可能数据还没有处理完成,所以只能先回复一个 ACK 报文,告诉 Client 端,"你发的 FIN 报文我收到了"。只有等到 Server 端所有的报文都发送完了,才能发送 FIN 报文,因此不能一起发送。故需要四步握手 @@ -109,6 +102,21 @@ +### 应用层 + +* 从浏览器地址栏输入 URL 到请求返回发生了什么? + * 进行 URL 解析,进行编码 + * DNS 解析,顺序是先查 hosts 文件是否有记录,有的话就会把相对应映射的 IP 返回,然后去本地 DNS 缓存中寻找,然后去找计算机上配置的 DNS 服务器上有或者有缓存,最后去找全球的根 DNS 服务器,直到查到为止 + * 查找到 IP 之后,进行 TCP 协议的三次握手,发出 HTTP 请求 + * 服务器处理请求,返回响应 + * 浏览器解析渲染页面 + + + +*** + + + ## System diff --git a/Java.md b/Java.md index ae674cc..c63fa6e 100644 --- a/Java.md +++ b/Java.md @@ -592,9 +592,9 @@ public class Test1 { 运算符: - * `>>`运算符:将二进制位进行右移操作,相当于除 2 - * `<<`运算符:将二进制位进行左移操作,相当于乘 2 - * `>>>`运算符:无符号右移,忽略符号位,空位都以0补齐 + * `>>` 运算符:将二进制位进行右移操作,相当于除 2 + * `<<` 运算符:将二进制位进行左移操作,相当于乘 2 + * `>>>` 运算符:无符号右移,忽略符号位,空位都以0补齐 运算规则: @@ -4713,7 +4713,7 @@ HashMap继承关系如下图所示: static final int TREEIFY_THRESHOLD = 8; ``` - 为什么 Map 桶中节点个数大于8才转为红黑树? + 为什么 Map 桶中节点个数大于 8 才转为红黑树? * 在 HashMap 中有一段注释说明:**空间和时间的权衡** @@ -4936,14 +4936,14 @@ HashMap继承关系如下图所示: 存储数据步骤(存储过程): - 1. 先通过hash值计算出key映射到哪个桶 + 1. 先通过 hash 值计算出 key 映射到哪个桶 2. 如果桶上没有碰撞冲突,则直接插入 3. 如果出现碰撞冲突:如果该桶使用红黑树处理冲突,则调用红黑树的方法插入数据;否则采用传统的链式方法插入,如果链的长度达到临界值,则把链转变为红黑树 - 4. 如果数组位置相同,通过equals比较内容是否相同:相同则新的value覆盖之前的value,不相同则将新的键值对添加到哈希表中 - 5. 如果size大于阈值threshold,则进行扩容 + 4. 如果数组位置相同,通过 equals 比较内容是否相同:相同则新的 value 覆盖旧 value,不相同则将新的键值对添加到哈希表中 + 5. 如果 size 大于阈值 threshold,则进行扩容 ```java public V put(K key, V value) { @@ -4951,7 +4951,7 @@ HashMap继承关系如下图所示: } ``` - putVal()方法中key在这里执行了一下hash(),在putVal函数中使用到了上述hash函数计算的哈希值: + putVal() 方法中 key 在这里执行了一下 hash(),在 putVal 函数中使用到了上述 hash 函数计算的哈希值: ```java final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { @@ -11300,7 +11300,7 @@ public Object pop() { unused(25+1) + hash(31) + age(4) + lock(3) = 64bit #64位系统 ``` - * **Klass Word**:类型指针,指向该对象的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;在64位系统中,开启指针压缩(-XX:+UseCompressedOops)或者 JVM 堆的最大值小于 32G,这个指针也是 4byte,否则是 8byte(就是 Java 中的一个引用的大小) + * **Klass Word**:类型指针,指向该对象的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;在64位系统中,开启指针压缩(-XX:+UseCompressedOops)或者 JVM 堆的最大值小于 32G,这个指针也是 4byte,否则是 8byte(就是 **Java 中的一个引用的大小**) ```ruby |-----------------------------------------------------| @@ -11310,7 +11310,7 @@ public Object pop() { |---------------------------|-------------------------| ``` -* 数组对象:如果对象是一个数组,那在对象头中还有一块数据用于记录数组长度 +* 数组对象:如果对象是一个数组,那在对象头中还有一块数据用于记录数组长度(12 字节) ```ruby |-------------------------------------------------------------------------------| @@ -11352,7 +11352,7 @@ public Object pop() { #### 实际大小 -浅堆(Shallow Heap):**对象本身占用的内存,不包括内部引用对象的大小**,32 位系统中一个**对象引用占 4 个字节**,每个对象头占用 8 个字节,根据堆快照格式不同,对象的大小会同 8 字节进行对齐 +浅堆(Shallow Heap):**对象本身占用的内存,不包括内部引用对象的大小**,32 位系统中一个对象引用占 4 个字节,每个对象头占用 8 个字节,根据堆快照格式不同,对象的大小会同 8 字节进行对齐 JDK7 中的 String:2个 int 值共占 8 字节,value 对象引用占用 4 字节,对象头 8 字节,对齐后占 24 字节,为 String 对象的浅堆大小,与 value 实际取值无关,无论字符串长度如何,浅堆大小始终是 24 字节 @@ -11396,7 +11396,7 @@ private int hash32; #### 节约内存 -* 尽量使用基本类型 +* 尽量使用基本数据类型 * 满足容量前提下,尽量用小字段 @@ -11409,7 +11409,7 @@ private int hash32; private int size; ``` - Mark Word 占 4byte,Klass Word 占 4byte,一个 int 字段占 4byte,elementData 数组占 12(4+4+4),数组中 10 个 Integer 对象占 10×16,所以整个集合空间大小为 184byte(深堆) + Mark Word 占 4byte,Klass Word 占 4byte,一个 int 字段占 4byte,elementData 数组占 12byte,数组中 10 个 Integer 对象占 10×16,所以整个集合空间大小为 184byte(深堆) * 时间用 long/int 表示,不用 Date 或者 String @@ -11421,7 +11421,7 @@ private int hash32; #### 对象访问 -JVM 是通过栈帧中的对象引用访问到其内部的对象实例: +JVM 是通过**栈帧中的对象引用**访问到其内部的对象实例: * 句柄访问:Java 堆中会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息 @@ -11429,7 +11429,7 @@ JVM 是通过栈帧中的对象引用访问到其内部的对象实例: -* 直接指针(HotSpot采用):Java 堆对象的布局必须考虑如何放置访问类型数据的相关信息,reference 中直接存储的对象地址 +* 直接指针(HotSpot 采用):Java 堆对象的布局必须考虑如何放置访问类型数据的相关信息,reference 中直接存储的对象地址 优点:速度更快,**节省了一次指针定位的时间开销** @@ -11475,9 +11475,9 @@ Java 对象创建时机: 1. 使用 new 关键字创建对象:由执行类实例创建表达式而引起的对象创建 -2. 使用 Class 类的 newInstance 方法 (反射机制) +2. 使用 Class 类的 newInstance 方法(反射机制) -3. 使用 Constructor 类的 newInstance 方法(反射机制) +3. 使用 Constructor 类的 newInstance 方法(反射机制) ```java public class Student { @@ -12920,7 +12920,7 @@ double j = i / 0.0; System.out.println(j);//无穷大,NaN: not a number ``` -分析 i++:从字节码角度分析:a++ 和 ++a 的区别是先执行 iload 还是 先执行 iinc +**分析 i++**:从字节码角度分析:a++ 和 ++a 的区别是先执行 iload 还是 先执行 iinc ```java 4 iload_1 //存入操作数栈 @@ -13960,7 +13960,7 @@ public class Candy4 { } ``` -可变参数`String... args`其实是`String[] args` , java 编译器会在编译期间将上述代码变换为: +可变参数 `String... args` 其实是 `String[] args` , java 编译器会在编译期间将上述代码变换为: ```java public static void main(String[] args) { @@ -13968,7 +13968,7 @@ public static void main(String[] args) { } ``` -注意:如果调用了foo()则等价代码为`foo(new String[]{})` ,创建了一个空的数组,而不会传递 null 进去 +注意:如果调用了 foo() 则等价代码为 `foo(new String[]{})` ,创建了一个空的数组,而不会传递 null 进去 @@ -17392,7 +17392,7 @@ UML 从目标系统的不同角度出发,定义了用例图、类图、对象 * 双端检锁机制 - 在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作,所以需要使用 `volatile` 关键字 + 在多线程的情况下,可能会出现空指针问题,出现问题的原因是 JVM 在实例化对象的时候会进行优化和指令重排序操作,所以需要使用 `volatile` 关键字 ```java public class Singleton { diff --git a/Prog.md b/Prog.md index 58d90f2..86847d5 100644 --- a/Prog.md +++ b/Prog.md @@ -42,7 +42,7 @@ * 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集 -* 进程拥有共享的资源,如内存空间等,供其内部的线程共享 +* 进程拥有共享的资源,如内存空间等,供其**内部的线程共享** * 进程间通信较为复杂 @@ -50,9 +50,9 @@ * 信号量:信号量是一个计数器,用于多进程对共享数据的访问,解决同步相关的问题并避免竞争条件 * 共享存储:多个进程可以访问同一块内存空间,需要使用信号量用来同步对共享存储的访问 - * 管道通信:管道是用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,pipe文件 - * 匿名管道(Pipes) :用于具有亲缘关系的父子进程间或者兄弟进程之间的通信,只支持半双工通信 - * 命名管道(Names Pipes):以磁盘文件的方式存在,可以实现本机任意两个进程通信,遵循FIFO + * 管道通信:管道是用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,pipe 文件 + * 匿名管道(Pipes):用于具有亲缘关系的父子进程间或者兄弟进程之间的通信,只支持半双工通信 + * 命名管道(Names Pipes):以磁盘文件的方式存在,可以实现本机任意两个进程通信,遵循 FIFO * 消息队列:内核中存储消息的链表,由消息队列标识符标识,能在不同进程之间提供全双工通信,对比管道: * 匿名管道存在于内存中的文件;命名管道存在于实际的磁盘介质或者文件系统;消息队列存放在内核中,只有在内核重启(操作系统重启)或者显示地删除一个消息队列时,该消息队列才被真正删除 * 读进程可以根据消息类型有选择地接收消息,而不像 FIFO 那样只能默认地接收 @@ -63,7 +63,7 @@ * 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量 - Java中的通信机制:volatile、等待/通知机制、join方式、InheritableThreadLocal、MappedByteBuffer + Java 中的通信机制:volatile、等待/通知机制、join方式、InheritableThreadLocal、MappedByteBuffer * 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低 @@ -103,7 +103,7 @@ Thread创建线程方式:创建线程类,匿名内部类方式 * 线程的启动必须调用 start() 方法,如果线程直接调用 run() 方法,相当于变成了普通类的执行,此时将只有主线程在执行该线程 * 建议线程先创建子线程,主线程的任务放在之后,否则主线程(main)永远是先执行完 -Thread构造器: +Thread 构造器: * `public Thread()` * `public Thread(String name)` @@ -142,11 +142,11 @@ class MyThread extends Thread{ #### Runnable -Runnable创建线程方式:创建线程类,匿名内部类方式 +Runnable 创建线程方式:创建线程类,匿名内部类方式 -**Thread类本身也是实现了Runnable接口** +**Thread 类本身也是实现了 Runnable 接口** -Thread的构造器: +Thread 的构造器: * `public Thread(Runnable target)` * `public Thread(Runnable target, String name)` @@ -175,7 +175,7 @@ public class MyRunnable implements Runnable{ * 优点: - 1. 线程任务类只是实现了Runnable接口,可以继续继承其他类,避免了单继承的局限性 + 1. 线程任务类只是实现了 Runnable 接口,可以继续继承其他类,避免了单继承的局限性 2. 同一个线程任务对象可以被包装成多个线程对象 @@ -183,7 +183,7 @@ public class MyRunnable implements Runnable{ 4. 实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码和线程独立 - 5. 线程池可以放入实现Runnable或Callable线程任务对象 + 5. 线程池可以放入实现 Runnable 或 Callable 线程任务对象 ​ @@ -204,11 +204,9 @@ public class MyRunnable implements Runnable{ `public FutureTask(Callable callable)`:未来任务对象,在线程执行完后**得到线程的执行结果** -* 其实就是 Runnable 对象,这样被包装成未来任务对象 +* FutureTask 就是 Runnable 对象,被包装成未来任务对象 -`public V get()`:同步等待 task 执行完毕的结果 - -* 如果在线程中获取另一个线程执行结果,会阻塞等待,用于线程同步 +`public V get()`:同步等待 task 执行完毕的结果,如果在线程中获取另一个线程执行结果,会阻塞等待,用于线程同步 优缺点: @@ -253,9 +251,9 @@ Java Virtual Machine Stacks(Java 虚拟机栈):每个线程启动后,虚 * 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存 * 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法 -线程上下文切换(Thread Context Switch):一些原因导致 cpu 不再执行当前线程,转而执行另一个线程 +线程上下文切换(Thread Context Switch):一些原因导致 CPU 不再执行当前线程,转而执行另一个线程 -* 线程的 cpu 时间片用完 +* 线程的 CPU 时间片用完 * 垃圾回收 * 有更高优先级的线程需要运行 * 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法 @@ -266,7 +264,7 @@ Java Virtual Machine Stacks(Java 虚拟机栈):每个线程启动后,虚 Java 创建的线程是内核级线程,**线程的调度是在内核态运行的,而线程中的代码是在用户态运行**,所以线程切换(状态改变)会导致用户与内核态转换,这是非常消耗性能 -Java 中 main 方法启动的是一个进程也是一个主线程,main 方法里面的其他线程均为子线程 +Java 中 main 方法启动的是一个进程也是一个主线程,main 方法里面的其他线程均为子线程,main 线程是这些线程的父线程 @@ -336,7 +334,7 @@ sleep: yield: -* 调用 yield 会让提示线程调度器让出当前线程对CPU的使用 +* 调用 yield 会让提示线程调度器让出当前线程对 CPU 的使用 * 具体的实现依赖于操作系统的任务调度器 * **会放弃 CPU 资源,锁资源不会释放** @@ -419,10 +417,10 @@ public class Test { ##### 打断线程 `public void interrupt()`:中断这个线程,异常处理机制 -`public static boolean interrupted()`:判断当前线程是否被打断,,打断返回 true,清除打断标记 +`public static boolean interrupted()`:判断当前线程是否被打断,打断返回 true,清除打断标记 `public boolean isInterrupted()`:判断当前线程是否被打断,不清除打断标记 -* sleep,wait,join 方法都会让线程进入阻塞状态,打断进程**会清空打断状态** (false) +* sleep、wait、join 方法都会让线程进入阻塞状态,打断进程**会清空打断状态** (false) ```java public static void main(String[] args) throws InterruptedException { @@ -736,7 +734,7 @@ Java: ### syn-ed -#### 基本使用 +#### 使用锁 ##### 同步代码块 @@ -1201,9 +1199,8 @@ public static void method2() { 自旋锁说明: -* 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会 - 高,就多自旋几次;反之,就少自旋甚至不自旋,比较智能 -* Java 7 之后不能控制是否开启自旋功能,由JVM控制 +* 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,比较智能 +* Java 7 之后不能控制是否开启自旋功能,由 JVM 控制 ```java //手写自旋锁 @@ -1680,7 +1677,7 @@ public static void main(String[] args) { LockSupport 出现就是为了增强 wait & notify 的功能: * wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park、unpark 不需要 -* park & unpark以线程为单位来阻塞和唤醒线程,而notify 只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程 +* park & unpark以线程为单位来阻塞和唤醒线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程 * **park & unpark 可以先 unpark**,而 wait & notify 不能先 notify。类比生产消费,先消费发现有产品就消费,没有就等待;先生产就直接产生商品,然后线程直接消费 * wait 会释放锁资源进入等待队列,park 不会释放锁资源,只负责阻塞当前线程,会释放 CPU @@ -2242,15 +2239,15 @@ Java 内存模型是 Java MemoryModel(JMM),本身是一种**抽象的概 主内存和工作内存: -* 主内存:计算机的内存,也就是经常提到的8G内存,16G内存,存储所有共享变量的值 +* 主内存:计算机的内存,也就是经常提到的 8G 内存,16G 内存,存储所有共享变量的值 * 工作内存:存储该线程使用到的共享变量在主内存的的值的副本拷贝 **jvm和jmm之间的关系**: -* jmm中的主内存、工作内存与jvm中的Java堆、栈、方法区等并不是同一个层次的内存划分,这两者基本上是没有关系的,如果两者一定要勉强对应起来: - * 主内存主要对应于Java堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域 +* jmm 中的主内存、工作内存与 jvm 中的 Java 堆、栈、方法区等并不是同一个层次的内存划分,这两者基本上是没有关系的,如果两者一定要勉强对应起来: + * 主内存主要对应于 Java 堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域 * 从更低层次上说,主内存直接对应于物理硬件的内存,工作内存对应寄存器和高速缓存 @@ -2276,6 +2273,10 @@ Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互 +参考文章:https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20%E5%B9%B6%E5%8F%91.md + + + *** @@ -2307,10 +2308,8 @@ public static void main(String[] args) throws InterruptedException { 原因: * 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存 -* 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中, - 减少对主存中 run 的访问,提高效率 -* 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量 - 的值,结果永远是旧值 +* 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率 +* 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值 ![](https://gitee.com/seazean/images/raw/master/Java/JMM-可见性例子.png) @@ -2326,12 +2325,12 @@ public static void main(String[] args) throws InterruptedException { 定义原子操作的使用规则: -1. 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步会主内存中 -2. 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(assign或者load)的变量,即对一个变量实施use和store操作之前,必须先自行assign和load操作 -3. 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一线程重复执行多次,多次执行lock后,只有**执行相同次数的unlock**操作,变量才会被解锁,**lock和unlock必须成对出现** -4. 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量之前需要重新执行load或assign操作初始化变量的值 -5. 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量 -6. 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作) +1. 不允许一个线程无原因地(没有发生过任何 assign 操作)把数据从工作内存同步会主内存中 +2. 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(assign 或者 load)的变量,即对一个变量实施 use 和 store 操作之前,必须先自行 assign 和 load 操作 +3. 一个变量在同一时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一线程重复执行多次,多次执行 lock 后,只有**执行相同次数的unlock**操作,变量才会被解锁,**lock和unlock必须成对出现** +4. 如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量之前需要重新执行 load 或 assign 操作初始化变量的值 +5. 如果一个变量事先没有被 lock 操作锁定,则不允许对它执行 unlock 操作,也不允许去 unlock 一个被其他线程锁定的变量 +6. 对一个变量执行 unlock 操作之前,必须先把此变量同步到主内存中(执行 store 和 write 操作) @@ -2400,7 +2399,7 @@ CPU 处理器速度远远大于在主内存中的,为了解决速度差异, #### 伪共享 -**缓存以缓存行 cache line 为单位,每个缓存行对应着一块内存**,一般是 64 byte(8 个 long),在CPU从主存获取数据时,以 cache line 为单位加载,于是相邻的数据会一并加载到缓存中 +**缓存以缓存行 cache line 为单位,每个缓存行对应着一块内存**,一般是 64 byte(8 个 long),在 CPU 从主存获取数据时,以 cache line 为单位加载,于是相邻的数据会一并加载到缓存中 缓存会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中,CPU 要保证数据的一致性,需要做到某个 CPU 核心更改了数据,其它 CPU 核心对应的**整个缓存行必须失效**,这就是伪共享 @@ -2412,7 +2411,7 @@ CPU 处理器速度远远大于在主内存中的,为了解决速度差异, * @Contended:原理参考 无锁 → Addr → 优化机制 → 伪共享 -Linux查看CPU缓存行: +Linux 查看 CPU 缓存行: * 命令:`cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size64` * 内存地址格式:[高位组标记] [低位索引] [偏移量] @@ -2429,19 +2428,19 @@ Linux查看CPU缓存行: **MESI**(Modified Exclusive Shared Or Invalid)是一种广泛使用的支持写回策略的缓存一致性协议,CPU 中每个缓存行(caceh line)使用4种状态进行标记(使用额外的两位 (bit) 表示): -* M:被修改(Modified) +* M:被修改(Modified) 该缓存行只被缓存在该 CPU 的缓存中,并且是被修改过的,与主存中的数据不一致 (dirty),该缓存行中的内存需要在未来的某个时间点(其它 CPU 读取主存中相应数据之前)写回( write back )主存 当被写回主存之后,该缓存行的状态会变成独享 (exclusive) 状态。 -* E:独享的(Exclusive) +* E:独享的(Exclusive) 该缓存行只被缓存在该 CPU 的缓存中,它是未被修改过的 (clear),与主存中数据一致,该状态可以在任何时刻有其它 CPU 读取该内存时变成共享状态 (shared) 当 CPU 修改该缓存行中内容时,该状态可以变成 Modified 状态 -* S:共享的(Shared) +* S:共享的(Shared) 该状态意味着该缓存行可能被多个 CPU 缓存,并且各个缓存中的数据与主存数据一致 (clear),当有一个 CPU 修改该缓存行中,其它 CPU 中该缓存行变成无效状态 (Invalid) @@ -2449,7 +2448,7 @@ Linux查看CPU缓存行: 该缓存是无效的,可能有其它 CPU 修改了该缓存行 -解决方法:各个处理器访问缓存时都遵循一些协议,在读写时要根据协议进行操作,协议主要有MSI、MESI等 +解决方法:各个处理器访问缓存时都遵循一些协议,在读写时要根据协议进行操作,协议主要有 MSI、MESI 等 @@ -2461,7 +2460,7 @@ Linux查看CPU缓存行: 单核 CPU 处理器会自动保证基本内存操作的原子性 -多核 CPU 处理器,每个 CPU 处理器内维护了一块内存,每个内核内部维护着一块缓存,当多线程并发读写时,就会出现缓存数据不一致的情况。处理器提供: +多核 CPU 处理器,每个 CPU 处理器内维护了一块内存,每个内核内部维护着一块缓存,当多线程并发读写时,就会出现缓存数据不一致的情况。处理器提供(平台级别): * 总线锁定:当处理器要操作共享变量时,在 BUS 总线上发出一个 LOCK 信号,其他处理器就无法操作这个共享变量,该操作会导致大量阻塞,从而增加系统的性能开销 * 缓存锁定:当处理器对缓存中的共享变量进行了操作,其他处理器有嗅探机制,将该共享变量的缓存失效,其他线程读取时会重新从主内存中读取最新的数据,基于 MESI 缓存一致性协议来实现 @@ -2474,7 +2473,7 @@ Linux查看CPU缓存行: 总线机制: -* 总线嗅探:每个处理器通过嗅探在总线上传播的数据来检查自己缓存值是否过期了,当处理器发现自己的缓存对应的内存地址数据被修改,就将当前处理器的缓存行设置为无效状态,当处理器对这个数据进行操作时,会重新从内存中把数据读取到处理器缓存中 +* 总线嗅探:每个处理器通过嗅探在总线上传播的数据来检查自己缓存值是否过期了,当处理器发现自己的缓存对应的内存地址数据被修改,就**将当前处理器的缓存行设置为无效状态**,当处理器对这个数据进行操作时,会重新从内存中把数据读取到处理器缓存中 * 总线风暴:由于 volatile 的 MESI 缓存一致性协议,需要不断的从主内存嗅探和 CAS 循环,无效的交互会导致总线带宽达到峰值;因此不要大量使用 volatile 关键字,使用 volatile、syschonized 都需要根据实际场景 @@ -2560,9 +2559,9 @@ volatile 是 Java 虚拟机提供的**轻量级**的同步机制(三大特性 #### 底层原理 -使用 volatile 修饰的共享变量,总线会开启 CPU 总线嗅探机制来解决 JMM 缓存一致性问题,也就是共享变量在多线程中可见性的问题,实现 MESI 缓存一致性协议 +使用 volatile 修饰的共享变量,总线会开启 **CPU 总线嗅探机制**来解决 JMM 缓存一致性问题,也就是共享变量在多线程中可见性的问题,实现 MESI 缓存一致性协议 -底层是通过汇编 lock 前缀指令,共享变量加了 lock 前缀指令,在线程修改完共享变量后,会马上执行 store 和 write 操作写回主存。在执行 store 操作前,会先执行缓存锁定的操作,其他的 CPU 上运行的线程根据 CPU 总线嗅探机制会修改其共享变量为失效状态,读取时会重新从主内存中读取最新的数据 +底层是通过汇编 lock 前缀指令,共享变量加了 lock 前缀指令,在线程修改完共享变量后,会马上执行 store 和 write 操作写回主存。在执行 store 操作前,会先执行**缓存锁定**的操作,其他的 CPU 上运行的线程根据 CPU 总线嗅探机制会修改其共享变量为失效状态,读取时会重新从主内存中读取最新的数据 lock 前缀指令就相当于内存屏障,Memory Barrier(Memory Fence) @@ -2652,10 +2651,10 @@ public final class Singleton { private static Singleton INSTANCE = null; public static Singleton getInstance() { - if(INSTANCE == null) { // t2 + if(INSTANCE == null) { // t2,这里的判断不是线程安全的 // 首次访问会同步,而之后的使用没有 synchronized synchronized(Singleton.class) { - if (INSTANCE == null) { // t1 + if (INSTANCE == null) { // t1,这里是线程安全的,判断防止其他线程在当前线程等待锁的期间完成了初始化 INSTANCE = new Singleton(); } } @@ -2665,10 +2664,10 @@ public final class Singleton { } ``` -不锁INSTANCE的原因: +不锁 INSTANCE 的原因: * INSTANCE 要重新赋值 -* INSTANCE 是null,线程加锁之前需要获取对象的引用,null没有引用 +* INSTANCE 是 null,线程加锁之前需要获取对象的引用,null 没有引用 实现特点: @@ -2895,16 +2894,16 @@ public class TestVolatile { ### CAS -#### 实现原理 +#### 原理 -无锁编程:lock free +无锁编程:Lock Free -CAS的全称是Compare-And-Swap,是**CPU并发原语** +CAS的全称是 Compare-And-Swap,是**CPU并发原语** -* CAS并发原语体现在Java语言中就是sun.misc.Unsafe类的各个方法,调用UnSafe类中的CAS方法,JVM会实现出CAS汇编指令,这是一种完全依赖于硬件的功能,通过它实现了原子操作 -* CAS是一种系统原语,原语属于操作系统范畴,是由若干条指令组成 ,用于完成某个功能的一个过程,并且**原语的执行必须是连续的,执行过程中不允许被中断**,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致的问题,所以 CAS 是线程安全的 +* CAS 并发原语体现在 Java 语言中就是 sun.misc.Unsafe 类的各个方法,调用 UnSafe 类中的 CAS 方法,JVM 会实现出 CAS 汇编指令,这是一种完全依赖于硬件的功能,实现了原子操作 +* CAS 是一种系统原语,原语属于操作系统范畴,是由若干条指令组成 ,用于完成某个功能的一个过程,并且**原语的执行必须是连续的,执行过程中不允许被中断**,也就是说 CAS 是一条 CPU 的原子指令,不会造成数据不一致的问题,所以 CAS 是线程安全的 -底层原理:CAS 的底层是 `lock cmpxchg` 指令(X86 架构),在单核和多核 CPU 下都能够保证比较交换的原子性 +底层原理:CAS 的底层是 `lock cmpxchg` 指令(X86 架构),在单核和多核 CPU 下都能够保证比较交换的原子性 * 程序是在单核处理器上运行,会省略 lock 前缀,单处理器自身会维护处理器内的顺序一致性,不需要 lock 前缀的内存屏障效果 @@ -2912,18 +2911,18 @@ CAS的全称是Compare-And-Swap,是**CPU并发原语** 作用:比较当前工作内存中的值和主物理内存中的值,如果相同则执行规定操作,否者继续比较直到主内存和工作内存的值一致为止 -CAS特点: +CAS 特点: * CAS 体现的是**无锁并发、无阻塞并发**,没有使用 synchronized,所以线程不会陷入阻塞,线程不需要频繁切换状态(上下文切换,系统调用) * CAS 是基于乐观锁的思想 -CAS缺点: +CAS 缺点: -- 循环时间长,开销大(因为执行的是do while,如果比较不成功一直在循环,最差的情况某个线程一直取到的值和预期值都不一样,就会无限循环导致饥饿),**使用CAS线程数不要超过CPU的核心数** +- 循环时间长,开销大,因为执行的是循环操作,如果比较不成功一直在循环,最差的情况某个线程一直取到的值和预期值都不一样,就会无限循环导致饥饿,**使用 CAS 线程数不要超过 CPU的 核心数** - 只能保证一个共享变量的原子操作 - - 对于一个共享变量执行操作时,可以通过循环CAS的方式来保证原子操作 - - 对于多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候只能用锁来保证原子性 -- 引出来ABA问题 + - 对于一个共享变量执行操作时,可以通过循环 CAS 的方式来保证原子操作 + - 对于多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候只能用锁来保证原子性 +- 引出来 ABA 问题 @@ -2935,7 +2934,7 @@ CAS缺点: #### 乐观锁 -CAS与Synchronized总结: +CAS 与 Synchronized 总结: * Synchronized是从悲观的角度出发: 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。(**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**),因此Synchronized我们也将其称之为悲观锁。jdk中的ReentrantLock也是一种悲观锁,**性能较差** @@ -2958,18 +2957,18 @@ CAS与Synchronized总结: 构造方法: -* `public AtomicInteger()`:初始化一个默认值为0的原子型Integer -* `public AtomicInteger(int initialValue)`:初始化一个指定值的原子型Integer +* `public AtomicInteger()`:初始化一个默认值为 0 的原子型 Integer +* `public AtomicInteger(int initialValue)`:初始化一个指定值的原子型 Integer 常用API: | 方法 | 作用 | | ------------------------------------- | ------------------------------------------------------------ | -| public final int get() | 获取AtomicInteger的值 | -| public final int getAndIncrement() | 以原子方式将当前值加1,返回的是自增前的值 | -| public final int incrementAndGet() | 以原子方式将当前值加1,返回的是自增后的值 | -| public final int getAndSet(int value) | 以原子方式设置为newValue的值,返回旧值 | -| public final int addAndGet(int data) | 以原子方式将输入的数值与实例中的值相加并返回
实例:AtomicInteger里的value | +| public final int get() | 获取 AtomicInteger 的值 | +| public final int getAndIncrement() | 以原子方式将当前值加 1,返回的是自增前的值 | +| public final int incrementAndGet() | 以原子方式将当前值加 1,返回的是自增后的值 | +| public final int getAndSet(int value) | 以原子方式设置为 newValue 的值,返回旧值 | +| public final int addAndGet(int data) | 以原子方式将输入的数值与实例中的值相加并返回
实例:AtomicInteger 里的 value | @@ -2981,12 +2980,12 @@ CAS与Synchronized总结: **AtomicInteger原理**:自旋锁 + CAS 算法 -CAS算法:有3个操作数(内存值V, 旧的预期值A,要修改的值B) +CAS 算法:有 3 个操作数(内存值 V, 旧的预期值 A,要修改的值 B) -* 当旧的预期值A == 内存值V 此时可以修改,将V改为B -* 当旧的预期值A != 内存值V 此时不能修改,并重新获取现在的最新值,重新获取的动作就是自旋 +* 当旧的预期值 A == 内存值 V 此时可以修改,将 V 改为 B +* 当旧的预期值 A != 内存值 V 此时不能修改,并重新获取现在的最新值,重新获取的动作就是自旋 -分析getAndSet方法: +分析 getAndSet 方法: * AtomicInteger: @@ -3000,7 +2999,7 @@ CAS算法:有3个操作数(内存值V, 旧的预期值A,要修改的值B } ``` - valueOffset:表示该变量值在内存中的偏移地址,Unsafe就是根据内存偏移地址获取数据 + valueOffset:偏移量表示该变量值相对于当前对象地址的偏移,Unsafe 就是根据内存偏移地址获取数据 ```java valueOffset = unsafe.objectFieldOffset @@ -3024,9 +3023,9 @@ CAS算法:有3个操作数(内存值V, 旧的预期值A,要修改的值B } ``` - var5:从主内存中拷贝到工作内存中的值(每次都要从主内存拿到最新的值到自己的本地内存),然后执行`compareAndSwapInt()`再和主内存的值进行比较,假设方法返回false,那么就一直执行 while方法,直到期望的值和真实值一样,修改数据 + var5:从主内存中拷贝到工作内存中的值(每次都要从主内存拿到最新的值到本地内存),然后执行 `compareAndSwapInt()` 再和主内存的值进行比较,假设方法返回 false,那么就一直执行 while 方法,直到期望的值和真实值一样,修改数据 -* 变量value用volatile修饰,保证了多线程之间的内存可见性,避免线程从自己的工作缓存中查找变量 +* 变量 value 用 volatile 修饰,保证了多线程之间的内存可见性,避免线程从自己的工作缓存中查找变量 ```java private volatile int value @@ -3084,12 +3083,11 @@ CAS算法:有3个操作数(内存值V, 旧的预期值A,要修改的值B 原子引用类:AtomicReference、AtomicStampedReference、AtomicMarkableReference -AtomicReference类: - -* 构造方法:`AtomicReference atomicReference = new AtomicReference();` +AtomicReference 类: -* 常用API: +* 构造方法:`AtomicReference atomicReference = new AtomicReference()` +* 常用 API: `public final boolean compareAndSet(V expectedValue, V newValue)`:CAS 操作 `public final void set(V newValue)`:将值设置为 newValue `public final V get()`:返回当前值 @@ -3132,7 +3130,7 @@ class Student { 原子数组类:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray -AtomicIntegerArray类方法: +AtomicIntegerArray 类方法: ```java /** @@ -3155,11 +3153,12 @@ public final boolean compareAndSet(int i, int expect, int update) { 原子更新器类:AtomicReferenceFieldUpdater、AtomicIntegerFieldUpdater、AtomicLongFieldUpdater -利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常`IllegalArgumentException: Must be volatile type` +利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常 `IllegalArgumentException: Must be volatile type` 常用API: -`static AtomicIntegerFieldUpdater newUpdater(Class c, String fieldName)`:构造 -`abstract boolean compareAndSet(T obj, int expect, int update)`:CAS + +* `static AtomicIntegerFieldUpdater newUpdater(Class c, String fieldName)`:构造方法 +* `abstract boolean compareAndSet(T obj, int expect, int update)`:CAS ```java public class UpdateDemo { @@ -3185,20 +3184,20 @@ public class UpdateDemo { 原子累加器类:LongAdder、DoubleAdder、LongAccumulator、DoubleAccumulator -LongAdder和LongAccumulator区别: +LongAdder 和 LongAccumulator 区别: 相同点: -* LongAddr与LongAccumulator类都是使用非阻塞算法CAS实现的, -* LongAddr类是LongAccumulator类的一个特例,只是LongAccumulator提供了更强大的功能,可以自定义累加规则,当accumulatorFunction为null时就等价于LongAddr +* LongAddr 与 LongAccumulator 类都是使用非阻塞算法 CAS 实现的, +* LongAddr 类是 LongAccumulator 类的一个特例,只是 LongAccumulator 提供了更强大的功能,可以自定义累加规则,当accumulatorFunction 为 null 时就等价于 LongAddr 不同点: -* 调用casBase时,LongAccumulator使用function.applyAsLong(b = base, x)来计算,LongAddr使用casBase(b = base, b + x)来计算 -* LongAccumulator类功能更加强大,构造方法参数中 +* 调用 casBase 时,LongAccumulator 使用 function.applyAsLong(b = base, x) 来计算,LongAddr 使用 casBase(b = base, b + x) 来计算 +* LongAccumulator 类功能更加强大,构造方法参数中 - * accumulatorFunction是一个双目运算器接口,可以指定累加规则,比如累加或者相乘,其根据输入的两个参数返回一个计算值,LongAdder内置累加规则 - * identity则是LongAccumulator累加器的初始值,LongAccumulator可以为累加器提供非0的初始值,而LongAdder只能提供默认的0 + * accumulatorFunction 是一个双目运算器接口,可以指定累加规则,比如累加或者相乘,其根据输入的两个参数返回一个计算值,LongAdder 内置累加规则 + * identity 则是 LongAccumulator 累加器的初始值,LongAccumulator 可以为累加器提供非0的初始值,而 LongAdder 只能提供默认的0 @@ -3209,72 +3208,21 @@ LongAdder和LongAccumulator区别: ### Adder -#### 优化CAS - -LongAdder是Java8提供的类,跟AtomicLong有相同的效果,但对CAS机制进行了优化,尝试使用分段CAS以及自动分段迁移的方式来大幅度提升多线程高并发执行CAS操作的性能 - -CAS底层实现是在一个循环中不断地尝试修改目标值,直到修改成功。如果竞争不激烈修改成功率很高,否则失败率很高,失败后这些重复的原子性操作会耗费性能(导致大量线程**空循环,自旋转**) - -优化核心思想:数据分离,将AtomicLong的单点的**更新压力分担到各个节点**,在低并发的时候直接更新,可以保障和AtomicLong的性能基本一致,而在高并发的时候通过分散提高了性能 - - - -*** - - - #### 优化机制 -##### 分段机制 - -分段 CAS 机制: - -* 在发生竞争时,创建Cell数组用于将不同线程的操作离散(通过hash等算法映射)到不同的节点上 -* 设置多个累加单元(会根据需要扩容,最大为CPU核数),Therad-0 累加 Cell[0],而 Thread-1 累加 Cell[1] 等,最后将结果汇总 -* 在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能 - +LongAdder 是 Java8 提供的类,跟 AtomicLong 有相同的效果,但对 CAS 机制进行了优化,尝试使用分段 CAS 以及自动分段迁移的方式来大幅度提升多线程高并发执行 CAS 操作的性能 +CAS 底层实现是在一个循环中不断地尝试修改目标值,直到修改成功。如果竞争不激烈修改成功率很高,否则失败率很高,失败后这些重复的原子性操作会耗费性能(导致大量线程**空循环,自旋转**) -*** +优化核心思想:数据分离,将 AtomicLong 的**单点的更新压力分担到各个节点**,空间换时间,在低并发的时候直接更新,可以保障和 AtomicLong 的性能基本一致,而在高并发的时候通过分散提高了性能 +**分段 CAS 机制**: +* 在发生竞争时,创建 Cell 数组用于将不同线程的操作离散(通过 hash 等算法映射)到不同的节点上 +* 设置多个累加单元(会根据需要扩容,最大为 CPU 核数),Therad-0 累加 Cell[0],而 Thread-1 累加 Cell[1] 等,最后将结果汇总 +* 在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能 -##### 分段迁移 - -自动分段迁移机制:某个Cell的value执行CAS失败,就会自动寻找另一个Cell分段内的value值进行CAS操作 - -```java -// 累加单元数组, 懒惰初始化 -transient volatile Cell[] cells; -// 基础值, 如果没有竞争, 则用 cas 累加这个域 -transient volatile long base; -// 在 cells 创建或扩容时, 置为 1, 表示加锁 -transient volatile int cellsBusy; -``` - -Cells占用内存是相对比较大的,是惰性加载的,在无竞争的情况下直接更新base域,在第一次发生竞争的时候(CAS失败)就会创建一个大小为2的cells数组,每次扩容都是加倍 - -扩容数组等行为只能有一个线程执行,因此需要一个锁,这里通过 CAS 更新 cellsBusy 来实现一个简单的lock - -CAS锁: - -```java -// 不要用于实践!!! -public class LockCas { - private AtomicInteger state = new AtomicInteger(0); - public void lock() { - while (true) { - if (state.compareAndSet(0, 1)) { - break; - } - } - } - public void unlock() { - System.out.println("unlock..."); - state.set(0); - } -} -``` +**自动分段迁移机制**:某个 Cell 的 value 执行 CAS 失败,就会自动寻找另一个 Cell 分段内的 value 值进行 CAS 操作 @@ -3282,9 +3230,9 @@ public class LockCas { -##### 伪共享 +#### 伪共享 -Cell为累加单元:数组访问索引是通过Thread里的threadLocalRandomProbe域取模实现的,这个域是ThreadLocalRandom更新的 +Cell 为累加单元:数组访问索引是通过 Thread 里的 threadLocalRandomProbe 域取模实现的,这个域是 ThreadLocalRandom 更新的 ```java @sun.misc.Contended static final class Cell { @@ -3298,13 +3246,11 @@ Cell为累加单元:数组访问索引是通过Thread里的threadLocalRandomPr } ``` -@sun.misc.Contended注解:防止缓存行伪共享 - -Cell 是数组形式,**在内存中是连续存储的**,一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value),每一个 cache line 为 64 字节,因此缓存行可以存下 2 个的 Cell 对象,当Core-0 要修改 Cell[0]、Core-1 要修改 Cell[1],无论谁修改成功都会导致对方 Core 的缓存行失效,需要重新去主存获取 +Cell 是数组形式,**在内存中是连续存储的**,64 位系统中,一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value),每一个 cache line 为 64 字节,因此缓存行可以存下 2 个的 Cell 对象,当 Core-0 要修改 Cell[0]、Core-1 要修改 Cell[1],无论谁修改成功都会导致当前缓存行失效,导致对方的数据失效,需要重新去主存获取 ![](https://gitee.com/seazean/images/raw/master/Java/JUC-伪共享1.png) -@sun.misc.Contended:在使用此注解的对象或字段的前后各增加 128 字节大小的padding,使用2倍于大多数硬件缓存行让 CPU 将对象预读至缓存时占用不同的缓存行,这样就不会造成对方缓存行的失效 +@sun.misc.Contended:防止缓存行伪共享,在使用此注解的对象或字段的前后各增加 128 字节大小的 padding,使用 2 倍于大多数硬件缓存行让 CPU 将对象预读至缓存时占用不同的缓存行,这样就不会造成对方缓存行的失效 ![](https://gitee.com/seazean/images/raw/master/Java/JUC-伪共享2.png) @@ -3316,136 +3262,193 @@ Cell 是数组形式,**在内存中是连续存储的**,一个 Cell 为 24 -#### 成员方法 +#### 源码解析 -* add:累加方法 +Striped64 类成员属性: + +```java +// 表示当前计算机CPU数量 +static final int NCPU = Runtime.getRuntime().availableProcessors() +// 累加单元数组, 懒惰初始化 +transient volatile Cell[] cells; +// 基础值, 如果没有竞争, 则用 cas 累加这个域,当 cells 扩容时,也会将数据写到 base 中 +transient volatile long base; +// 在 cells 初始化或扩容时只能有一个线程执行, 通过 CAS 更新 cellsBusy 置为 1 来实现一个锁 +transient volatile int cellsBusy; +``` + +Cells 占用内存是相对比较大的,是惰性加载的,在无竞争的情况下直接更新 base 域,在第一次发生竞争的时候(CAS 失败)就会创建一个大小为 2 的 cells 数组,每次扩容都是加倍,所以**数组长度总是 2 的 n 次幂** + +* LongAdder#add:累加方法 ```java public void add(long x) { - // as 为累加单元数组 b 为基础值 x 为累加值 + // as 为累加单元数组的引用,b 为基础值,v 表示期望值 + // m 表示 cells 数组的长度,a 表示当前线程命中的 cell 单元格 Cell[] as; long b, v; int m; Cell a; - // 1. as 有值, 表示已经发生过竞争, 进入 if - // 2. cas 给 base 累加时失败了, 表示 base 发生了竞争, 进入 if + // 条件一: true 说明 cells 已经初始化过了,当前线程需要去 cells 数组累加,不需要在 base 上累加 + // false 说明 cells 未初始化,当前线程应该写到 base 域,进行 || 后的尝试写入 + // 条件二: true 说明 cas 失败,发生竞争,需要扩容或者重试 + // false 说明 cas 成功,累加操作完成 if ((as = cells) != null || !casBase(b = base, b + x)) { - // uncontended 表示 cell 没有竞争 + // uncontended 为 true 表示 cell 没有竞争,false 表示发生竞争 boolean uncontended = true; - if ( - // as 还没有创建 - as == null || (m = as.length - 1) < 0 || - // 当前线程对应的 cell 还没有创建 + + // 条件一: true 说明 cells 未初始化,多线程写 base 发生竞争需要进行初始化 cells 数组 + // fasle 说明 cells 已经初始化,进行下一个条件寻找自己的 cell 去累加 + // 条件二: getProbe() 获取 hash 值,& m 的逻辑和 HashMap 的逻辑相同,保证散列的均匀性 + // true 说明当前线程对应下标的 cell 为空,需要创建 cell + // false 说明当前线程对应的 cell 不为空,进行下一个条件想要将 x 值累加到 cell 中 + // 条件三: true 说明 cas 失败,当前线程对应的 cell 有竞争 + // false 说明 cas 成功,可以直接返回 + if (as == null || (m = as.length - 1) < 0 || (a = as[getProbe() & m]) == null || - // 当前线程的cell累加失败,a为当前线程的cell - !(uncontended = a.cas(v = a.value, v + x)) - //uncontended = false代表有竞争 - ) { - // 进入 cell 数组创建、cell 创建的流程 + !(uncontended = a.cas(v = a.value, v + x))) longAccumulate(x, null, uncontended); - } + // uncontended 在 cell 上累加失败的时候才为 false,其余情况均为 true } } ``` -* longAccumulate:cell数组创建 +* Striped64#longAccumulate:cell 数组创建 ```java - // x null false + // x null false | true final void longAccumulate(long x, LongBinaryOperator fn, boolean w...ed) { int h; - // 当前线程还没有对应的 cell, 需要随机生成一个 h 值用来将当前线程绑定到 cell + // 当前线程还没有对应的 cell, 需要随机生成一个 hash 值用来将当前线程绑定到 cell if ((h = getProbe()) == 0) { - ThreadLocalRandom.current(); // 初始化 probe - h = getProbe(); //h 对应新的 probe 值, 用来对应 cell + // 初始化 probe,获取 hash 值 + ThreadLocalRandom.current(); + h = getProbe(); + // 默认情况下 当前线程肯定是写入到了 cells[0] 位置,不把它当做一次真正的竞争 wasUncontended = true; } - //collide 为 true 表示需要扩容 + // 表示扩容意向,false 一定不会扩容,true 可能会扩容 boolean collide = false; + //自旋 for (;;) { + // as 表示cells引用,a 表示当前线程命中的 cell,n 表示 cells 数组长度,v 表示 期望值 Cell[] as; Cell a; int n; long v; - // cells已经创建 + // CASE1: 表示 cells 已经初始化了,当前线程应该将数据写入到对应的 cell 中 if ((as = cells) != null && (n = as.length) > 0) { - // 线程对应的cell还没被创建 + // CASE1.1: true 表示当前线程对应的下标位置的 cell 为 null,需要创建 new Cell if ((a = as[(n - 1) & h]) == null) { // 判断 cellsBusy 是否被锁 if (cellsBusy == 0) { // 创建 cell, 初始累加值为 x Cell r = new Cell(x); - // 为 cellsBusy 加锁, + // 加锁 if (cellsBusy == 0 && casCellsBusy()) { - boolean created = false; + // 是否创建成功的标记,进入【创建逻辑】 + boolean created = false; try { Cell[] rs; int m, j; + // 把当前 cells 数组赋值给 rs,并且不为n ull if ((rs = cells) != null && (m = rs.length) > 0 && + // 再次判断防止其它线程初始化过该位置,当前线程再次初始化该位置会造成数据丢失 + // 因为这里是线程安全的,这里判断后进行的逻辑不会被其他线程影响 rs[j = (m - 1) & h] == null) { + // 把新创建的 cell 填充至当前位置 rs[j] = r; - created = true; + created = true; // 表示创建完成 } } finally { - cellsBusy = 0; + cellsBusy = 0;//解锁 } - if (created) - break;// 成功则 break, 否则继续 continue 循环 + if (created) // true 表示创建完成,可以推出循环了 + break; // 成功则 break, 否则继续 continue 循环 continue; } } collide = false; } - // 有竞争, 改变线程对应的 cell 来重试 cas + // CASE1.2: 线程对应的 cell 有竞争, 改变线程对应的 cell 来重试 cas else if (!wasUncontended) wasUncontended = true; - //cas尝试累加, fn配合LongAccumulator不为null, 配合LongAdder为null + // CASE 1.3: 当前线程 rehash 过,尝试新命中的 cell 不为空去累加 + // true 表示写成功,退出循环,false 表示 rehash 之后命中的新 cell 也有竞争 else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) break; - // cells长度已经超过了最大长度或者已经扩容, 改变线程对应的cell来重试cas + // CASE 1.4: cells 长度已经超过了最大长度或者已经扩容 else if (n >= NCPU || cells != as) - collide = false; // At max size or stale - // collide 为 false 进入此分支, 就不会进入下面的 else if 进行扩容了 + collide = false; // 扩容意向改为false,表示不扩容了 + // CASE 1.5: 更改扩容意向 else if (!collide) collide = true; - //加锁扩容 + // CASE 1.6: 扩容逻辑,进行加锁 else if (cellsBusy == 0 && casCellsBusy()) { try { - if (cells == as) { // Expand table unless stale + // 再次检查,防止期间被其他线程扩容了 + if (cells == as) { + // 扩容为以前的 2 倍 Cell[] rs = new Cell[n << 1]; + // 遍历移动值 for (int i = 0; i < n; ++i) rs[i] = as[i]; + // 把扩容后的引用给 cells cells = rs; } } finally { - cellsBusy = 0; + cellsBusy = 0; //解锁 } - collide = false; - continue;、 + collide = false; // 扩容意向改为false,表示不扩容了 + continue; } + // 重置当前线程 Hash 值,这就是【分段迁移机制】,case1.3 h = advanceProbe(h); } - //还没有 cells, 尝试给 cellsBusy 加锁 + + // CASE2: 运行到这说明 cells 还未初始化,as 为null + // 条件一: true 表示当前未加锁 + // 条件二: 其它线程可能会在当前线程给 as 赋值之后修改了 cells,这里需要判断(这里不是线程安全的) + // 条件三: true 表示加锁成功 else if (cellsBusy == 0 && cells == as && casCellsBusy()) { + // 初始化标志,这里就进行 【初始化】 boolean init = false; try { - // 初始化 cells, 最开始长度为2, 填充一个初始累加值为x的cell + // 再次判断 cells == as 防止其它线程已经初始化了,当前线程再次初始化导致丢失数据 + // 因为这里是线程安全的,所以重新检查,经典DCL if (cells == as) { Cell[] rs = new Cell[2]; rs[h & 1] = new Cell(x);//填充线程对应的cell cells = rs; - init = true; + init = true; //初始化成功 } } finally { - cellsBusy = 0; + cellsBusy = 0; //解锁啊 } if (init) - break; + break; //初始化成功直接返回 } - // 上两种情况失败, 尝试给 base 累加 + // CASE3: 运行到这说明其他线程在初始化 cells,所以当前线程将值累加到 base,累加成功直接结束自旋 else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) - break; // Fall back on using base + break; + } + } + ``` + +* sum:获取最终结果通过 sum 整合,保持最终一致性,不保证强一致性 + + ```java + public long sum() { + Cell[] as = cells; Cell a; + long sum = base; + if (as != null) { + //遍历 累加 + for (int i = 0; i < as.length; ++i) { + if ((a = as[i]) != null) + sum += a.value; + } } + return sum; } ``` -* sum:获取最终结果通过 sum 整合 + @@ -3455,17 +3458,16 @@ Cell 是数组形式,**在内存中是连续存储的**,一个 Cell 为 24 ### ABA -ABA问题:当进行获取主内存值时,该内存值在写入主内存时已经被修改了N次,但是最终又改成原来的值 +ABA 问题:当进行获取主内存值时,该内存值在写入主内存时已经被修改了 N 次,但是最终又改成原来的值 -其他线程先把A改成B又改回A,主线程仅能判断出共享变量的值与最初值 A 是否相同,不能感知到这种从 A 改为 B 又 改回 A 的情况,这时CAS虽然成功,但是过程存在问题 +其他线程先把 A 改成 B 又改回 A,主线程仅能判断出共享变量的值与最初值 A 是否相同,不能感知到这种从 A 改为 B 又 改回 A 的情况,这时 CAS 虽然成功,但是过程存在问题 -* 构造方法 +* 构造方法: `public AtomicStampedReference(V initialRef, int initialStamp)`:初始值和初始版本号 * 常用API: - ` public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)`:CAS + ` public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)`:期望引用和期望版本号都一致才进行 CAS 修改数据 `public void set(V newReference, int newStamp)`:设置值和版本号 - `public V getReference()`:返回引用的值 `public int getStamp()`:返回当前版本号 @@ -3504,9 +3506,9 @@ public static void main(String[] args) { ### Unsafe -Unsafe是CAS的核心类,由于Java无法直接访问底层系统,需要通过本地 (Native) 方法来访问 +Unsafe 是 CAS 的核心类,由于 Java 无法直接访问底层系统,需要通过本地 (Native) 方法来访问 -Unsafe类存在sun.misc包,其中所有方法都是native修饰的,都是直接调用操作系统底层资源执行相应的任务,基于该类可以直接操作特定的内存数据,其内部方法操作类似C的指针 +Unsafe 类存在 sun.misc 包,其中所有方法都是 native 修饰的,都是直接调用**操作系统底层资源**执行相应的任务,基于该类可以直接操作特定的内存数据,其内部方法操作类似 C 的指针 模拟实现原子整数: @@ -3587,7 +3589,7 @@ public class TestFinal { final 变量的赋值通过 putfield 指令来完成,在这条指令之后也会加入写屏障,保证在其它线程读到它的值时不会出现为 0 的情况 -其他线程访问final修饰的变量会复制一份放入栈中,效率更高 +其他线程访问 final 修饰的变量会复制一份放入栈中,效率更高 @@ -4557,7 +4559,7 @@ class DelayTask implements Delayed { ### 操作Pool -#### 创建方法 +#### 创建方式 ##### Executor @@ -4585,7 +4587,7 @@ public ThreadPoolExecutor(int corePoolSize, * maximumPoolSize:最大线程数,当队列中存放的任务达到队列容量时,当前可以同时运行的数量变为最大线程数,创建线程并立即执行最新的任务,与核心线程数之间的差值又叫救急线程数 -* keepAliveTime:救急线程最大存活时间,当线程池中的线程数量大于 `corePoolSize` 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等到`keepAliveTime`时间超过销毁 +* keepAliveTime:救急线程最大存活时间,当线程池中的线程数量大于 `corePoolSize` 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等到 `keepAliveTime` 时间超过销毁 * unit:`keepAliveTime` 参数的时间单位 @@ -4595,7 +4597,7 @@ public ThreadPoolExecutor(int corePoolSize, * handler:拒绝策略,线程到达最大线程数仍有新任务时会执行拒绝策略 - RejectedExecutionHandler下有4个实现类: + RejectedExecutionHandler下有 4 个实现类: * AbortPolicy:让调用者抛出 RejectedExecutionException 异常,默认策略 * CallerRunsPolicy:"调用者运行"的调节机制,将某些任务回退到调用者,从而降低新任务的流量 @@ -4622,7 +4624,7 @@ public ThreadPoolExecutor(int corePoolSize, * 如果队列满了且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会启动饱和**拒绝策略**来执行 3. 当一个线程完成任务时,会从队列中取下一个任务来执行 -4. 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断:如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉,所以线程池的所有任务完成后最终会收缩到 corePoolSize 大小 +4. 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断:如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉,所以线程池的所有任务完成后最终会收缩到 corePoolSize 大小 @@ -4646,7 +4648,7 @@ Executors提供了四种线程池的创建:newCachedThreadPool、newFixedThrea ``` * 核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间 - * LinkedBlockingQueue是一个单向链表实现的阻塞队列,默认大小为`Integer.MAX_VALUE`,也就是无界队列,可以放任意数量的任务,在任务比较多的时候会导致 OOM(内存溢出) + * LinkedBlockingQueue是一个单向链表实现的阻塞队列,默认大小为 `Integer.MAX_VALUE`,也就是无界队列,可以放任意数量的任务,在任务比较多的时候会导致 OOM(内存溢出) * 适用于任务量已知,相对耗时的长期任务 * newCachedThreadPool:创建一个可扩容的线程池 @@ -4659,11 +4661,11 @@ Executors提供了四种线程池的创建:newCachedThreadPool、newFixedThrea ``` * 核心线程数是 0, 最大线程数是 Integer.MAX_VALUE,全部都是救急线程(60s 后可以回收),可能会创建大量线程,从而导致 **OOM** - * SynchronousQueue 作为阻塞队列,没有容量,对于每一个take的线程会阻塞直到有一个put的线程放入元素为止(类似一手交钱、一手交货) + * SynchronousQueue 作为阻塞队列,没有容量,对于每一个 take 的线程会阻塞直到有一个 put 的线程放入元素为止(类似一手交钱、一手交货) * 适合任务数比较密集,但每个任务执行时间较短的情况 -* newSingleThreadExecutor:创建一个只有1个线程的单线程池 +* newSingleThreadExecutor:创建一个只有 1 个线程的单线程池 ```java public static ExecutorService newSingleThreadExecutor() { @@ -4692,7 +4694,7 @@ Executors提供了四种线程池的创建:newCachedThreadPool、newFixedThrea -#### 开发要求 +##### 开发要求 阿里巴巴 Java 开发手册要求: @@ -4737,7 +4739,9 @@ Executors提供了四种线程池的创建:newCachedThreadPool、newFixedThrea -#### 提交方法 +#### 操作方式 + +##### 提交方法 ExecutorService类API: @@ -4745,16 +4749,16 @@ ExecutorService类API: | ------------------------------------------------------------ | ------------------------------------------------------------ | | void execute(Runnable command) | 执行任务(Executor类API) | | Future submit(Runnable task) | 提交任务 task() | -| Future submit(Callable task) | 提交任务 task,用返回值Future获得任务执行结果 | -| List> invokeAll(Collection> tasks) | 提交 tasks 中所有任务 | -| List> invokeAll(Collection> tasks, long timeout, TimeUnit unit) | 提交 tasks 中所有任务,超时时间针对所有task,超时会取消没有执行完的任务,并抛出超时异常 | -| T invokeAny(Collection> tasks) | 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消 | +| Future submit(Callable task) | 提交任务 task,用返回值Future获得任务执行结果 | +| List> invokeAll(Collection> tasks) | 提交 tasks 中所有任务 | +| List> invokeAll(Collection> tasks, long timeout, TimeUnit unit) | 提交 tasks 中所有任务,超时时间针对所有task,超时会取消没有执行完的任务,并抛出超时异常 | +| T invokeAny(Collection> tasks) | 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消 | -execute和submit都属于线程池的方法,对比: +execute 和 submit 都属于线程池的方法,对比: -* execute只能提交Runnable类型的任务,而submit既能提交Runnable类型任务也能提交Callable类型任务 +* execute 只能提交 Runnable 类型的任务,而 submit 既能提交 Runnable 类型任务也能提交 Callable 类型任务 -* execute会直接抛出任务执行时的异常,submit会吞掉异常,可通过Future的get方法将任务执行时的异常重新抛出 +* execute 会直接抛出任务执行时的异常,submit 会吞掉异常,可通过 Future 的 get 方法将任务执行时的异常重新抛出 @@ -4762,16 +4766,16 @@ execute和submit都属于线程池的方法,对比: -#### 关闭方法 +##### 关闭方法 -ExecutorService类API: +ExecutorService 类 API: | 方法 | 说明 | | ----------------------------------------------------- | ------------------------------------------------------------ | | void shutdown() | 线程池状态变为 SHUTDOWN,等待任务执行完后关闭线程池,不会接收新任务,但已提交任务会执行完 | | List shutdownNow() | 线程池状态变为 STOP,用 interrupt 中断正在执行的任务,直接关闭线程池,不会接收新任务,会将队列中的任务返回, | | boolean isShutdown() | 不在 RUNNING 状态的线程池,此执行者已被关闭,方法返回 true | -| boolean isTerminated() | 线程池状态是否是 TERMINATED,如果所有任务在关闭后完成,返回true | +| boolean isTerminated() | 线程池状态是否是 TERMINATED,如果所有任务在关闭后完成,返回 true | | boolean awaitTermination(long timeout, TimeUnit unit) | 调用 shutdown 后,由于调用线程不会等待所有任务运行结束,如果它想在线程池 TERMINATED 后做些事情,可以利用此方法等待 | @@ -4780,9 +4784,9 @@ ExecutorService类API: -#### 处理异常 +##### 处理异常 -execute会直接抛出任务执行时的异常,submit会吞掉异常,有两种处理方法 +execute 会直接抛出任务执行时的异常,submit 会吞掉异常,有两种处理方法 方法1:主动捉异常 @@ -5004,13 +5008,13 @@ public class ThreadPoolDemo04 { ### ForkJoin -Fork/Join:线程池的实现,体现是分治思想,适用于能够进行任务拆分的 cpu 密集型运算,用于**并行计算** +Fork/Join:线程池的实现,体现是分治思想,适用于能够进行任务拆分的 CPU 密集型运算,用于**并行计算** 任务拆分:是将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的一些计算,如归并排序、斐波那契数列都可以用分治思想进行求解 * Fork/Join 在分治的基础上加入了多线程,把每个任务的分解和合并交给不同的线程来完成,提升了运算效率 -* ForkJoin 使用 ForkJoinPool 来启动,是一个特殊的线程池,默认会创建与 cpu 核心数大小相同的线程池 +* ForkJoin 使用 ForkJoinPool 来启动,是一个特殊的线程池,默认会创建与 CPU 核心数大小相同的线程池 * 任务有返回值继承 RecursiveTask,没有返回值继承 RecursiveAction ```java @@ -9142,7 +9146,7 @@ final void updateHead(Node h, Node p) { # NET -## 介绍 +## DES ### 网络编程 @@ -9224,29 +9228,29 @@ UDP:用户数据报协议(User Datagram Protocol),是一个面向无连接 * 阻塞:在数据没有的情况下,还是要继续等待着读(排队等待) * 非阻塞:在数据没有的情况下,会去做其他事情,一旦有了数据再来获取(柜台取款,取个号,然后坐在椅子上做其它事,等号广播会通知你办理) -Java中的通信模型: +Java 中的通信模型: -1. BIO表示同步阻塞式通信,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善。 +1. BIO 表示同步阻塞式通信,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善。 同步阻塞式性能极差:大量线程,大量阻塞 2. 伪异步通信:引入线程池,不需要一个客户端一个线程,实现线程复用来处理很多个客户端,线程可控。 高并发下性能还是很差:线程数量少,数据依然是阻塞的;数据没有来线程还是要等待 -3. NIO表示**同步非阻塞IO**,服务器实现模式为请求对应一个线程,客户端发送的连接会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理 +3. NIO 表示**同步非阻塞 IO**,服务器实现模式为请求对应一个线程,客户端发送的连接会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求时才启动一个线程进行处理 工作原理:1个主线程专门负责接收客户端,1个线程轮询所有的客户端,发来了数据才会开启线程处理 同步:线程还要不断的接收客户端连接,以及处理数据 非阻塞:如果一个管道没有数据,不需要等待,可以轮询下一个管道是否有数据 -4. AIO表示异步非阻塞IO,AIO 引入异步通道的概念,采用了 Proactor 模式,有效的请求才启动线程,特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用 - 异步:服务端线程接收到了客户端管道以后就交给底层处理IO通信,线程可以做其他事情 +4. AIO 表示异步非阻塞 IO,AIO 引入异步通道的概念,采用了 Proactor 模式,有效的请求才启动线程,特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用 + 异步:服务端线程接收到了客户端管道以后就交给底层处理 IO 通信,线程可以做其他事情 非阻塞:底层也是客户端有数据才会处理,有了数据以后处理好通知服务器应用来启动线程进行处理 各种模型应用场景: -* BIO适用于连接数目比较小且固定的架构,该方式对服务器资源要求比较高,并发局限于应用中,程序简单 -* NIO适用于连接数目多且连接比较短(轻操作)的架构,如聊天服务器,并发局限于应用中,编程复杂,JDK 1.4开始支持 -* AIO适用于连接数目多且连接比较长(重操作)的架构,如相册服务器,充分调用操作系统参与并发操作,编程复杂,JDK 1.7开始支持 +* BIO 适用于连接数目比较小且固定的架构,该方式对服务器资源要求比较高,并发局限于应用中,程序简单 +* NIO 适用于连接数目多且连接比较短(轻操作)的架构,如聊天服务器,并发局限于应用中,编程复杂,JDK 1.4开始支持 +* AIO 适用于连接数目多且连接比较长(重操作)的架构,如相册服务器,充分调用操作系统参与并发操作,编程复杂,JDK 1.7开始支持 @@ -9371,7 +9375,7 @@ select 允许应用程序监视一组文件描述符,等待一个或者多个 int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); ``` -- fd_set 使用 **bitmap 数组**实现,数组大小用 FD_SETSIZE 定义,只能监听少于 FD_SETSIZE 数量的描述符,32位机默认是 1024 个,64位机默认是 2048,可以对进行修改,然后重新编译内核 +- fd_set 使用 **bitmap 数组**实现,数组大小用 FD_SETSIZE 定义,只能监听少于 FD_SETSIZE 数量的描述符,32 位机默认是 1024 个,64 位机默认是 2048,可以对进行修改,然后重新编译内核 - fd_set 有三种类型的描述符:readset、writeset、exceptset,对应读、写、异常条件的描述符集合 @@ -9390,15 +9394,15 @@ int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct t tv_sec == 0 && tv_usec == 0:获取后直接返回,不阻塞等待 tv_sec != 0 || tv_usec != 0:等待指定时间 -- 方法成功调用返回结果为就绪的文件描述符个数,出错返回结果为 -1,超时返回结果为 0 +- 方法成功调用返回结果为**就绪的文件描述符个数**,出错返回结果为 -1,超时返回结果为 0 Linux 提供了一组宏为 fd_set 进行赋值操作: ```c -int FD_ZERO(fd_set *fdset); // 将一个fd_set类型变量的所有值都置为0 -int FD_CLR(int fd, fd_set *fdset); // 将一个fd_set类型变量的fd位置为0 -int FD_SET(int fd, fd_set *fdset); // 将一个fd_set类型变量的fd位置为1 -int FD_ISSET(int fd, fd_set *fdset);// 判断fd位是否被置为1 +int FD_ZERO(fd_set *fdset); // 将一个 fd_set 类型变量的所有值都置为 0 +int FD_CLR(int fd, fd_set *fdset); // 将一个 fd_set 类型变量的 fd 位置为 0 +int FD_SET(int fd, fd_set *fdset); // 将一个 fd_set 类型变量的 fd 位置为 1 +int FD_ISSET(int fd, fd_set *fdset);// 判断 fd 位是否被置为 1 ``` 示例: @@ -9455,10 +9459,10 @@ select 调用流程图: 1. 使用 copy_from_user 从用户空间拷贝 fd_set 到内核空间,进程阻塞 2. 注册回调函数 _pollwait -3. 遍历所有 fd,调用其对应的 poll 方法(对于 socket,这个 poll 方法是 sock_poll,sock_poll 根据情况会调用到 tcp_poll、udp_poll 或者 datagram_poll),以 tcp_poll 为例,其核心实现就是 _pollwait -4. _pollwait 就是把 current(当前进程)挂到设备的等待队列,不同设备有不同的等待队列,对于 tcp_poll ,其等待队列是 sk → sk_sleep(把进程挂到等待队列中并不代表进程已经睡眠),在设备收到消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时 current 便被唤醒 +3. 遍历所有 fd,调用其对应的 poll 方法判断当前请求是否准备就绪,对于 socket,这个 poll 方法是 sock_poll,sock_poll 根据情况会调用到 tcp_poll、udp_poll 或者 datagram_poll,以 tcp_poll 为例,其核心实现就是 _pollwait +4. _pollwait 把 **current(调用 select 的进程)**挂到设备的等待队列,不同设备有不同的等待队列,对于 tcp_poll ,其等待队列是 sk → sk_sleep(把进程挂到等待队列中并不代表进程已经睡眠),在设备收到消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时 current 便被唤醒 5. poll 方法返回时会返回一个描述读写操作是否就绪的 mask 掩码,根据这个 mask 掩码给 fd_set 赋值 -6. 如果遍历完所有的 fd,还没有返回一个可读写的 mask 掩码,则会调用 schedule_timeout 让调用 select 的进程(就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程,如果超过一定的超时时间(schedule_timeout指定),没有其他线程唤醒,则调用 select 的进程会重新被唤醒获得 CPU,进而重新遍历 fd,判断有没有就绪的 fd +6. 如果遍历完所有的 fd,还没有返回**一个**可读写的 mask 掩码,则会调用 schedule_timeout 让 current 进程进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程,如果超过一定的超时时间(schedule_timeout指定),没有其他线程唤醒,则调用 select 的进程会重新被唤醒获得 CPU,进而重新遍历 fd,判断有没有就绪的 fd 7. 把 fd_set 从内核空间拷贝到用户空间,阻塞进程继续执行 @@ -9497,9 +9501,9 @@ select 和 poll 对比: - select 的描述符类型使用数组实现,有描述符的限制;而 poll 使用**链表**实现,没有描述符数量的限制 - poll 提供了更多的事件类型,并且对描述符的重复利用上比 select 高 -* select 和 poll 速度都比较慢,每次调用都需要将全部描述符数组 fd 从应用进程缓冲区复制到内核缓冲区,同时每次都需要在内核遍历传递进来的所有 fd ,这个开销在 fd 很多时会很大 +* select 和 poll 速度都比较慢,**每次调用**都需要将全部描述符数组 fd 从应用进程缓冲区复制到内核缓冲区,同时每次都需要在内核遍历传递进来的所有 fd ,这个开销在 fd 很多时会很大 * 几乎所有的系统都支持 select,但是只有比较新的系统支持 poll -* select 和 poll 的时间复杂度 O(n),对 socket 进行扫描时是线性扫描,即采用轮询的方法,效率较低,因为并不知道具体是哪个 socket 具有事件,所以随着 FD 的增加会造成遍历速度慢的**线性下降**性能问题 +* select 和 poll 的时间复杂度 O(n),对 socket 进行扫描时是线性扫描,即采用轮询的方法,效率较低,因为并不知道具体是哪个 socket 具有事件,所以随着 FD 数量的增加会造成遍历速度慢的**线性下降**性能问题 * poll 还有一个特点是水平触发,如果报告了 fd 后,没有被处理,那么下次 poll 时会再次报告该 fd * 如果一个线程对某个描述符调用了 select 或者 poll,另一个线程关闭了该描述符,会导致调用结果不确定 @@ -9517,7 +9521,7 @@ select 和 poll 对比: ##### 函数 -epoll 使用事件的就绪通知方式,通过 epoll_ctl() 向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵**红黑树**上,一旦该 fd 就绪,内核通过 callback 回调函数将 I/O 准备好的描述符加入到一个**链表**中管理,进程调用 epoll_wait() 便可以得到事件完成的描述符 +epoll 使用事件的就绪通知方式,通过 epoll_ctl() 向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵**红黑树**上,一旦该 fd 就绪,**内核通过 callback 回调函数将 I/O 准备好的描述符加入到一个链表中**管理,进程调用 epoll_wait() 便可以得到事件完成的描述符 ```c int epoll_create(int size); @@ -9525,7 +9529,7 @@ int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout); ``` -* epall_create:一个系统函数,函数将在内核空间内创建一个 epoll 数据结构,可以理解为 epoll 结构空间,返回值为 epoll 的文件描述符编号,后面当有client连接时,向该 epoll 区中添加监听,所以 epoll 使用一个文件描述符管理多个描述符 +* epall_create:一个系统函数,函数将在内核空间内创建一个 epoll 数据结构,可以理解为 epoll 结构空间,返回值为 epoll 的文件描述符编号,以后有 client 连接时,向该 epoll 结构中添加监听,所以 epoll 使用一个文件描述符管理多个描述符 * epall_ctl:epoll 的事件注册函数,select 函数是调用时指定需要监听的描述符和事件,epoll 先将用户感兴趣的描述符事件注册到 epoll 空间。此函数是非阻塞函数,用来增删改 epoll 空间内的描述符,参数解释: @@ -9550,7 +9554,7 @@ int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout events 可以是以下几个宏集合:EPOLLIN、EPOLOUT、EPOLLPRI、EPOLLERR、EPOLLHUP(挂断)、EPOLET(边缘触发)、EPOLLONESHOT(只监听一次,事件触发后自动清除该 fd,从 epoll 列表) -* epoll_wait:等待事件的产生,类似于 select() 调用,返回值为本次就绪的 fd 个数 +* epoll_wait:等待事件的产生,类似于 select() 调用,返回值为本次就绪的 fd 个数,直接从就绪链表获取,时间复杂度 O(1) * epfd:指定感兴趣的 epoll 事件列表 * events:指向一个 epoll_event 结构数组,当函数返回时,内核会把就绪状态的数据拷贝到该数组 @@ -9558,12 +9562,12 @@ int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout * timeout:单位为毫秒 * 0:表示立即返回,非阻塞调用 * -1:阻塞调用,直到有用户感兴趣的事件就绪为止 - * 大于0:阻塞调用,阻塞指定时间内如果有事件就绪则提前返回,否则等待指定时间后返回 + * 大于 0:阻塞调用,阻塞指定时间内如果有事件就绪则提前返回,否则等待指定时间后返回 epoll 的描述符事件有两种触发模式:LT(level trigger)和 ET(edge trigger): * LT 模式:当 epoll_wait() 检测到描述符事件到达时,将此事件通知进程,进程可以不立即处理该事件,下次调用 epoll_wait() 会再次通知进程。是默认的一种模式,并且同时支持 Blocking 和 No-Blocking -* ET 模式:通知之后进程必须立即处理事件,下次再调用 epoll_wait() 时不会再得到事件到达的通知。减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高;只支持 No-Blocking,以避免由于一个文件的阻塞读/阻塞写操作把处理多个文件描述符的任务饥饿 +* ET 模式:通知之后进程必须立即处理事件,下次再调用 epoll_wait() 时不会再得到事件到达的通知。减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高;只支持 No-Blocking,以避免由于一个 fd 的阻塞读/阻塞写操作把处理多个文件描述符的任务饥饿 ```c // 创建 epoll 描述符,每个应用程序只需要一个,用于监控所有套接字 @@ -9624,12 +9628,12 @@ else epoll 的特点: * epoll 仅适用于 Linux 系统 -* epoll 使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中 +* epoll 使用**一个文件描述符管理多个描述符**,将用户关系的文件描述符的事件存放到内核的一个事件表中 * 没有最大描述符数量(并发连接)的限制,打开 fd 的上限远大于1024(1G 内存能监听约10万个端口) * epoll 的时间复杂度 O(1),epoll 理解为 event poll,不同于忙轮询和无差别轮询,调用 epoll_wait **只是轮询就绪链表**。当监听列表有设备就绪时调用回调函数,把就绪 fd 放入就绪链表中,并唤醒在 epoll_wait 中阻塞的进程,所以 epoll 实际上是**事件驱动**(每个事件关联上fd)的,降低了 system call 的时间复杂度 * epoll 内核中根据每个 fd 上的 callback 函数来实现,只有活跃的 socket 才会主动调用 callback,所以使用 epoll 没有前面两者的线性下降的性能问题,效率提高 -* epoll 每次注册新的事件到 epoll 句柄中时,会把所有的 fd 拷贝进内核,但不是每次 epoll_wait 的时重复拷贝,对比前面两种,epoll 只需要将描述符从进程缓冲区向内核缓冲区拷贝一次。 epoll 也可以利用 mmap() 文件映射内存加速与内核空间的消息传递,减少复制开销 +* epoll 每次注册新的事件到 epoll 句柄中时,会把新的 fd 拷贝进内核,但不是每次 epoll_wait 的重复拷贝,对比前面两种,epoll 只需要将描述符从进程缓冲区向内核缓冲区拷贝一次。 epoll 也可以利用 **mmap() 文件映射内存**加速与内核空间的消息传递,减少复制开销 * 前面两者要把 current 往设备等待队列中挂一次,epoll 也只把 current 往等待队列上挂一次,但是这里的等待队列并不是设备等待队列,只是一个 epoll 内部定义的等待队列,这样节省不少的开销 * epoll 对多线程编程更有友好,一个线程调用了 epoll_wait() 另一个线程关闭了同一个描述符,也不会产生像 select 和 poll 的不确定情况 @@ -9650,7 +9654,7 @@ epoll 的特点: 应用场景: * select 应用场景: - * select 的 timeout 参数精度为微秒,而 poll 和 epoll 为毫秒,因此 select 更加适用于实时性要求比较高的场景,比如核反应堆的控制 + * select 的 timeout 参数精度为微秒,poll 和 epoll 为毫秒,因此 select 适用于实时性要求比较高的场景,比如核反应堆的控制 * select 可移植性更好,几乎被所有主流平台所支持 * poll 应用场景:poll 没有最大描述符数量的限制,适用于平台支持并且对实时性要求不高的情况 @@ -9680,7 +9684,7 @@ epoll 的特点: * 进程描述符和用户的进程是一一对应的 * SYS_API 系统调用:如 read、write,系统调用就是 0X80 中断 -* 进程描述符 pd:进程从用户态切换到内核态时,需要保存用户态时的上下文信息, +* 进程描述符 pd:进程从用户态切换到内核态时,需要保存用户态时的上下文信息 * 线程上下文:用户程序基地址,程序计数器、cpu cache、寄存器等,方便程序切回用户态时恢复现场 * 内核堆栈:系统调用函数也是要创建变量的,这些变量在内核堆栈上分配 @@ -9714,6 +9718,8 @@ epoll 的特点: +参考视频:https://www.bilibili.com/video/BV19D4y1o797 + **** @@ -9821,7 +9827,9 @@ Java NIO 对 sendfile 的支持是 `FileChannel.transferTo()/transferFrom()`, -## Inet +## BIO + +### Inet 一个该 InetAddress 类的对象就代表一个IP地址对象 @@ -9859,9 +9867,9 @@ public class InetAddressDemo { -## UDP +### UDP -### 基本介绍 +#### 基本介绍 UDP(User Datagram Protocol)协议的特点: @@ -9879,9 +9887,9 @@ UDP协议的使用场景:在线视频、网络语音、电话 -### 实现UDP +#### 实现UDP -UDP协议相关的两个类 +UDP 协议相关的两个类 * DatagramPacket(数据包对象):用来封装要发送或要接收的数据,比如:集装箱 * DatagramSocket(发送对象):用来发送或接收数据包,比如:码头 @@ -9961,33 +9969,37 @@ public class UDPServerDemo{ -### 通讯方式 +#### 通讯方式 -UDP通信方式: +UDP 通信方式: + 单播:用于两个主机之间的端对端通信 + 组播:用于对一组特定的主机进行通信 + IP : 224.0.1.0 - Socket对象 : MulticastSocket + Socket 对象 : MulticastSocket + + 广播:用于一个主机对整个局域网上所有主机上的数据通信 + IP : 255.255.255.255 - Socket对象 : DatagramSocket + Socket 对象 : DatagramSocket + *** -## TCP +### TCP -### 基本介绍 +#### 基本介绍 -TCP/IP协议 ==> Transfer Control Protocol ==> 传输控制协议 +TCP/IP 协议 ==> Transfer Control Protocol ==> 传输控制协议 -TCP/IP协议的特点: +TCP/IP 协议的特点: * 面向连接的协议 * 只能由客户端主动发送数据给服务器端,服务器端接收到数据之后,可以给客户端响应数据 @@ -9996,7 +10008,7 @@ TCP/IP协议的特点: * 传输数据大小没有限制 * 因为面向连接的协议,速度慢,但是是可靠的协议。 -TCP协议的使用场景:文件上传和下载、邮件发送和接收、远程登录 +TCP 协议的使用场景:文件上传和下载、邮件发送和接收、远程登录 注意:**TCP不会为没有数据的ACK超时重传** @@ -10010,11 +10022,11 @@ TCP协议的使用场景:文件上传和下载、邮件发送和接收、远 -### Socket +#### Socket -TCP通信也叫**Socket网络编程**,只要代码基于Socket开发,底层就是基于了可靠传输的TCP通信 +TCP 通信也叫 **Socket 网络编程**,只要代码基于 Socket 开发,底层就是基于了可靠传输的 TCP 通信 -TCP协议相关的类: +TCP 协议相关的类: * Socket:一个该类的对象就代表一个客户端程序。 * ServerSocket:一个该类的对象就代表一个服务器端程序。 @@ -10022,22 +10034,22 @@ TCP协议相关的类: Socket类 * 构造方法: - `Socket(InetAddress address,int port)` : 创建流套接字并将其连接到指定IP指定端口号 - `Socket(String host, int port)` : 根据ip地址字符串和端口号创建客户端Socket对象 + `Socket(InetAddress address,int port)`:创建流套接字并将其连接到指定 IP 指定端口号 + `Socket(String host, int port)`:根据ip地址字符串和端口号创建客户端 Socket 对象 注意事项:执行该方法,就会立即连接指定的服务器,连接成功,则表示三次握手通过,反之抛出异常 * 常用API: - `OutputStream getOutputStream()` : 获得字节输出流对象 - `InputStream getInputStream()` : 获得字节输入流对象 - `void shutdownInput()` : 停止接受 - `void shutdownOutput()` : 停止发送数据,终止通信 - `SocketAddress getRemoteSocketAddress() `: 返回套接字连接到的端点的地址,未连接返回null + `OutputStream getOutputStream()`:获得字节输出流对象 + `InputStream getInputStream()`:获得字节输入流对象 + `void shutdownInput()`:停止接受 + `void shutdownOutput()`:停止发送数据,终止通信 + `SocketAddress getRemoteSocketAddress() `:返回套接字连接到的端点的地址,未连接返回null ServerSocket类: * 构造方法:`public ServerSocket(int port)` -* 常用API:`public Socket accept()`,**阻塞等待**接收一个客户端的Socket管道连接请求,连接成功返回一个Socket对象 +* 常用API:`public Socket accept()`,**阻塞等待**接收一个客户端的 Socket 管道连接请求,连接成功返回一个 Socket 对象 -相当于客户端和服务器建立一个数据管道,管道一般不用close +相当于客户端和服务器建立一个数据管道,管道一般不用 close @@ -10045,21 +10057,21 @@ ServerSocket类: -### 实现TCP +#### 实现TCP -#### 开发流程 +##### 开发流程 客户端的开发流程: -1. 客户端要请求于服务端的socket管道连接 -2. 从socket通信管道中得到一个字节输出流 +1. 客户端要请求于服务端的 Socket 管道连接 +2. 从 Socket 通信管道中得到一个字节输出流 3. 通过字节输出流给服务端写出数据 服务端的开发流程: -1. 用ServerSocket注册端口 -2. 接收客户端的Socket管道连接 -3. 从socket通信管道中得到一个字节输入流 +1. 用 ServerSocket 注册端口 +2. 接收客户端的 Socket 管道连接 +3. 从 Socket 通信管道中得到一个字节输入流 4. 从字节输入流中读取客户端发来的数据 ![](https://gitee.com/seazean/images/raw/master/Java/BIO工作机制.png) @@ -10068,7 +10080,7 @@ ServerSocket类: * 如果输出缓冲区空间不够存放主机发送的数据,则会被阻塞,输入缓冲区同理 * 缓冲区不属于应用程序,属于内核 -* TCP从输出缓冲区读取数据会加锁阻塞线程 +* TCP 从输出缓冲区读取数据会加锁阻塞线程 @@ -10076,7 +10088,7 @@ ServerSocket类: -#### BIO通信 +##### 实现通信 需求一:客户端发送一行数据,服务端接收一行数据 @@ -10212,7 +10224,7 @@ class ServerReaderThread extends Thread{ -#### 伪异步 +##### 伪异步 一个客户端要一个线程,这种模型是不行的,并发越高系统瘫痪的越快,可以在服务端引入线程池,使用线程池来处理与客户端的消息通信 @@ -10278,9 +10290,9 @@ public class BIOServer { -### 文件传输 +#### 文件传输 -#### 字节流 +##### 字节流 客户端:本地图片: ‪E:\seazean\图片资源\beautiful.jpg 服务端:服务器路径:E:\seazean\图片服务器 @@ -10373,15 +10385,17 @@ class ServerReaderThread extends Thread{ -#### 数据流 +##### 数据流 构造方法: -`DataOutputStream(OutputStream out)` : 创建一个新的数据输出流,以将数据写入指定的底层输出流 -`DataInputStream(InputStream in) ` : 创建使用指定的底层 InputStream 的 DataInputStream + +* `DataOutputStream(OutputStream out)` : 创建一个新的数据输出流,以将数据写入指定的底层输出流 +* `DataInputStream(InputStream in) ` : 创建使用指定的底层 InputStream 的 DataInputStream 常用API: -`final void writeUTF(String str)` : 使用机器无关的方式使用 UTF-8 编码将字符串写入底层输出流 -`final String readUTF()` : 读取以modified UTF-8格式编码的 Unicode 字符串,返回 String 类型 + +* `final void writeUTF(String str)` : 使用机器无关的方式使用 UTF-8 编码将字符串写入底层输出流 +* `final String readUTF()` : 读取以 modified UTF-8 格式编码的 Unicode 字符串,返回 String 类型 ```java public class Client { @@ -11235,7 +11249,7 @@ ssChannel.register(selector, SelectionKey.OP_ACCEPT); 3. 分配指定大小的缓冲区:`ByteBuffer buffer = ByteBuffer.allocate(1024)` 4. 发送数据给服务端 -37行代码,如果判断条件改为 !=-1,需要客户端 shutdown 一下 +37 行代码,如果判断条件改为 !=-1,需要客户端 shutdown 一下 ```java public class Server { @@ -11333,6 +11347,14 @@ ServerSocket ServerSocketChannel AsynchronousServerSocketChannel * 对于读操作,当有流可读取时,操作系统会将可读的流传入 read 方法的缓冲区 * 对于写操作,当操作系统将 write 方法传递的流写入完毕时,操作系统主动通知应用程序 -在JDK1.7中,这部分内容被称作NIO.2,主要在 Java.nio.channels 包下增加了下面四个异步通道: +在 JDK1.7 中,这部分内容被称作 NIO.2,主要在 Java.nio.channels 包下增加了下面四个异步通道: AsynchronousSocketChannel、AsynchronousServerSocketChannel、AsynchronousFileChannel、AsynchronousDatagramChannel + + + + +**** + + + diff --git a/SSM.md b/SSM.md index b6b546a..57b6bd8 100644 --- a/SSM.md +++ b/SSM.md @@ -110,7 +110,7 @@ org.apache.ibatis.session.SqlSession:构建者对象接口,用于执行 SQL * :删除功能标签 * id:属性,唯一标识,配合名称空间使用 * resultType:指定结果映射对象类型,和对应的方法的返回值类型(全限定名)保持一致,但是如果返回值是List则和其泛型保持一致 - * parameterType:指定参数映射对象类型,必须和对应的方法的参数类型(全限定名)保持一致 + * parameterType:指定参数映射对象类型,必须和对应的方法的参数类型(全限定名)保持一致 * statementType:可选 STATEMENT,PREPARED 或 CALLABLE,默认值:PREPARED * STATEMENT:直接操作 sql,不进行预编译,获取数据:$ Statement * PREPARED:预处理参数,进行预编译,获取数据:# PreparedStatement @@ -481,7 +481,7 @@ org.apache.ibatis.session.SqlSession:构建者对象接口,用于执行 SQL ### 批量操作 -两种方式实现批量操作: +三种方式实现批量操作: * 标签属性:这种方式属于全局批量 @@ -1003,7 +1003,7 @@ Mapper 接口开发需要遵循以下规范: | 标签名 | 描述 | 默认值 | | --------------------- | ------------------------------------------------------------ | ------ | -| lazyLoadingEnabled | 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。特定关联关系中可通过设置 `fetchType` 属性来覆盖该项的开关状态。 | false | +| lazyLoadingEnabled | 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载,特定关联关系中可通过设置 `fetchType` 属性来覆盖该项的开关状态。 | false | | aggressiveLazyLoading | 开启时,任一方法的调用都会加载该对象的所有延迟加载属性。否则每个延迟加载属性会按需加载(参考 lazyLoadTriggerMethods) | false | ```xml @@ -1492,9 +1492,9 @@ Mapper 接口开发需要遵循以下规范: 一级缓存的失效: -* SqlSession 不同时 +* SqlSession 不同 * SqlSession 相同,查询条件不同时(还未缓存该数据) -* SqlSession 相同,手动清除了一级缓存,调用`openSession.clearCache()` +* SqlSession 相同,手动清除了一级缓存,调用 `openSession.clearCache()` * SqlSession 相同,执行 commit 操作(执行插入、更新、删除),清空 SqlSession 中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。 测试一级缓存存在 @@ -1556,7 +1556,7 @@ public void testFirstLevelCache(){ - 则表示所有属性使用默认值 + ``` @@ -1588,7 +1588,8 @@ public void testFirstLevelCache(){ 1. select 标签的 useCache 属性 - 映射文件中的 `` 标签中设置 `useCache="true"` 代表当前 statement 要使用二级缓存 + 注意:针对每次查询都需要最新的数据 sql,要设置成 useCache=false,禁用二级缓存 ```xml @@ -2365,7 +2366,7 @@ DefaultSqlSessionFactory.openSessionFromDataSource(...):ExecutorType 为 Execu * `transactionFactory.newTransaction(DataSource, IsolationLevel, boolean`:事务对象 -* `configuration.newExecutor(tx, execType)`:根据参数创建指定类型的 Executor +* `configuration.newExecutor(tx, execType)`:**根据参数创建指定类型的 Executor** * 批量操作笔记的部分有讲解到 的属性 defaultExecutorType,根据设置的创建对象 * 二级缓存默认开启,会包装 Executor 对象 `BaseExecutor.setExecutorWrapper(executor)` @@ -2386,7 +2387,7 @@ Configuration.getMapper(Class, SqlSession):获取代理的 mapper 对象 MapperRegistry.getMapper(Class, SqlSession):MapperRegistry 是 Configuration 属性,在获取工厂对象时初始化 * `(MapperProxyFactory) knownMappers.get(type)`:获取接口信息封装为 MapperProxyFactory 对象 -* `mapperProxyFactory.newInstance(sqlSession)`:创建代理对象 +* `mapperProxyFactory.newInstance(sqlSession)`:**创建代理对象** * `new MapperProxy<>(sqlSession, mapperInterface, methodCache)`:包装对象 * methodCache 是并发安全的 ConcurrentHashMap 集合,存放要执行的方法 * `MapperProxy implements InvocationHandler` 是一个 InvocationHandler 对象 @@ -2427,9 +2428,9 @@ executor.query():开始执行查询语句,参数通过 wrapCollection() 包 * `BaseExecutor.query()`: - * `localCache.getObject(key) `:尝试从**本地缓存**(一级缓存)获取数据 + * `localCache.getObject(key) `:尝试从**本地缓存(一级缓存**)获取数据 -* `BaseExecutor.queryFromDatabase()`:开始从数据库获取数据,并放入本地缓存 +* `BaseExecutor.queryFromDatabase()`:**开始从数据库获取数据,并放入本地缓存** * `SimpleExecutor.doQuery()`:执行 query * `configuration.newStatementHandler()`:创建 StatementHandler 对象 From 9c566cbafbe15d8d28ef631a991d166da6f36ff6 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 8 Aug 2021 21:58:47 +0800 Subject: [PATCH 015/168] Update Java Notes --- Java.md | 60 +-- Prog.md | 1209 ++++++++++++++++++------------------------------------- SSM.md | 75 ++-- 3 files changed, 457 insertions(+), 887 deletions(-) diff --git a/Java.md b/Java.md index c63fa6e..67a998c 100644 --- a/Java.md +++ b/Java.md @@ -580,7 +580,7 @@ public class Test1 { * 负数: 原码:最高位为1,其余位置和正数相同 反码:保证符号位不变,其余位置取反 - 补码:保证符号位不变,其余位置取反加1,即反码+1 + 补码:保证符号位不变,其余位置取反加 1,即反码 +1 ```java -100原码: 10000000 00000000 00000000 01100100 //32位 @@ -588,21 +588,23 @@ public class Test1 { -100补码: 11111111 11111111 11111111 10011100 ``` - 补码 → 原码:符号位不变,其余位置取反加1 + 补码 → 原码:符号位不变,其余位置取反加 1 运算符: * `>>` 运算符:将二进制位进行右移操作,相当于除 2 * `<<` 运算符:将二进制位进行左移操作,相当于乘 2 - * `>>>` 运算符:无符号右移,忽略符号位,空位都以0补齐 + * `>>>` 运算符:无符号右移,忽略符号位,空位都以 0 补齐 运算规则: - * 正数的左移与右移,空位补0 - * 负数原码的左移与右移,空位补0 - 负数反码的左移与右移,空位补1 - 负数补码,左移低位补0,右移高位补1 - * 无符号移位,空位补0 + * 正数的左移与右移,空位补 0 + * 负数原码的左移与右移,空位补 0 + + 负数反码的左移与右移,空位补 1 + + 负数补码,左移低位补 0(会导致负数变为正数的问题,因为移动了符号位),右移高位补 1 + * 无符号移位,空位补 0 @@ -4598,8 +4600,8 @@ HashMap 基于哈希表的 Map 接口实现,是以 key-value 存储形式存 * HashMap的实现不是同步的,这意味着它不是线程安全的 * key 是唯一不重复的,底层的哈希表结构,依赖 hashCode 方法和 equals 方法保证键的唯一 * key、value 都可以为null,但是 key 位置只能是一个null -* HashMap中的映射不是有序的,即存取是无序的 -* **key要存储的是自定义对象,需要重写hashCode和equals方法,防止出现地址不同内容相同的key** +* HashMap 中的映射不是有序的,即存取是无序的 +* **key 要存储的是自定义对象,需要重写 hashCode 和 equals 方法,防止出现地址不同内容相同的 key** JDK7 对比 JDK8: @@ -4612,10 +4614,10 @@ JDK7 对比 JDK8: * 哈希表(Hash table,也叫散列表),根据关键码值(Key value)而直接访问的数据结构。通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度,这个映射函数叫做散列函数,存放记录的数组叫做散列表 -* JDK1.8 之前 HashMap 由 **数组+链表** 组成 +* JDK1.8 之前 HashMap 由 数组+链表 组成 * 数组是 HashMap 的主体 - * 链表则是为了**解决哈希冲突**而存在的(**拉链法**解决冲突),拉链法就是头插法 + * 链表则是为了解决哈希冲突而存在的(**拉链法解决冲突**),拉链法就是头插法 两个对象调用的hashCode方法计算的哈希码值(键的哈希)一致导致计算的数组索引值相同 * JDK1.8 以后 HashMap 由 **数组+链表 +红黑树**数据结构组成 @@ -4686,7 +4688,7 @@ HashMap继承关系如下图所示: * 如果输入值不是2的幂会怎么样? - 创建HashMap对象时,HashMap通过位移运算和或运算得到的肯定是2的幂次数,并且是大于那个数的最近的数字,底层采用tableSizeFor()方法 + 创建 HashMap 对象时,HashMap 通过位移运算和或运算得到的肯定是 2 的幂次数,并且是大于那个数的最近的数字,底层采用 tableSizeFor() 方法 3. 默认的负载因子,默认值是 0.75 @@ -4698,15 +4700,15 @@ HashMap继承关系如下图所示: ```java //集合最大容量的上限是:2的30次幂 - static final int MAXIMUM_CAPACITY = 1 << 30; + static final int MAXIMUM_CAPACITY = 1 << 30;// 0100 0000 0000 0000 0000 0000 0000 0000 = 2 ^ 30 ``` 最大容量为什么是 2 的 30 次方原因: * int 类型是 32 位整型,占 4 个字节 - * Java 的原始类型里没有无符号类型,所以首位是符号位正数为 0,负数为 1 + * Java 的原始类型里没有无符号类型,所以首位是符号位正数为 0,负数为 1, -5. 当链表的值超过8则会转红黑树(1.8新增**) +5. 当链表的值超过 8 则会转红黑树(JDK1.8 新增) ```java //当桶(bucket)上的结点数大于这个值时会转成红黑树 @@ -4735,7 +4737,7 @@ HashMap继承关系如下图所示: * 其他说法 红黑树的平均查找长度是 log(n),如果长度为 8,平均查找长度为 log(8)=3,链表的平均查找长度为 n/2,当长度为 8 时,平均查找长度为 8/2=4,这才有转换成树的必要;链表长度如果是小于等于 6,6/2=3,而 log(6)=2.6,虽然速度也很快的,但转化为树结构和生成树的时间并不短 -6. 当链表的值小 于6 则会从红黑树转回链表 +6. 当链表的值小 于 6 则会从红黑树转回链表 ```java //当桶(bucket)上的结点数小于这个值时树转链表 @@ -4749,16 +4751,16 @@ HashMap继承关系如下图所示: static final int MIN_TREEIFY_CAPACITY = 64; ``` - 原因:数组比较小的情况下变为红黑树结构,反而会降低效率,红黑树需要进行左旋,右旋,变色这些操作来保持平衡。同时数组长度小于64时,搜索时间相对快些,所以为了提高性能和减少搜索时间,底层在阈值大于8并且数组长度大于64时,链表才转换为红黑树,效率也变的更高效 + 原因:数组比较小的情况下变为红黑树结构,反而会降低效率,红黑树需要进行左旋,右旋,变色这些操作来保持平衡。同时数组长度小于 64 时,搜索时间相对快些,所以为了提高性能和减少搜索时间,底层在阈值大于 8 并且数组长度大于 64 时,链表才转换为红黑树,效率也变的更高效 -8. table用来初始化(必须是二的n次幂)(重点) +8. table 用来初始化(必须是二的n次幂) ```java //存储元素的数组 transient Node[] table; ``` - jdk8之前数组类型是Entry类型,从jdk1.8之后是Node类型。只是换了个名字,都实现了一样的接口:Map.Entry,负责存储键值对数据的 + jdk8 之前数组类型是 Entry类型,之后是 Node 类型。只是换了个名字,都实现了一样的接口 Map.Entry,负责存储键值对数据的 9. HashMap中存放元素的个数(**重点**) @@ -4797,11 +4799,11 @@ HashMap继承关系如下图所示: HashMap(int initialCapacity, float loadFactor)//构造指定初始容量和加载因子的空HashMap ``` - * 为什么加载因子设置为0.75,初始化临界值是12? + * 为什么加载因子设置为 0.75,初始化临界值是 12? - loadFactor太大导致查找元素效率低,存放的数据拥挤,太小导致数组的利用率低,存放的数据会很分散。loadFactor的默认值为**0.75f是官方给出的一个比较好的临界值**。 + loadFactor 太大导致查找元素效率低,存放的数据拥挤,太小导致数组的利用率低,存放的数据会很分散。loadFactor 的默认值为 **0.75f 是官方给出的一个比较好的临界值** - * **threshold**计算公式:capacity(数组长度默认16) * loadFactor(负载因子默认0.75)。这个值是当前已占用数组长度的最大值。**当Size>=threshold**的时候,那么就要考虑对数组的resize(扩容),这就是 **衡量数组是否需要扩增的一个标准**, 扩容后的 HashMap 容量是之前容量的**两倍**. + * threshold 计算公式:capacity(数组长度默认16) * loadFactor(负载因子默认 0.75)。这个值是当前已占用数组长度的最大值。**当 size>=threshold** 的时候,那么就要考虑对数组的 resize(扩容),这就是衡量数组是否需要扩增的一个标准, 扩容后的 HashMap 容量是之前容量的**两倍**. @@ -4903,7 +4905,7 @@ HashMap继承关系如下图所示: 1. hash - HashMap是支持Key为空的;HashTable是直接用Key来获取HashCode,key为空会抛异常 + HashMap 是支持 Key 为空的;HashTable 是直接用 Key 来获取 HashCode,key 为空会抛异常 * &(按位与运算):相同的二进制数位上,都是1的时候,结果为1,否则为零 * ^(按位异或运算):相同的二进制数位上,数字相同,结果为0,不同为1,**不进位加法** @@ -4917,13 +4919,13 @@ HashMap继承关系如下图所示: } ``` - 计算 hash 的方法:将hashCode无符号右移16位,高16bit 和低16bit 做了一个异或 + 计算 hash 的方法:将 hashCode 无符号右移 16 位,高 16bit 和低 16bit 做了一个异或,扰动运算 - 原因:当数组长度很小,假设是16,那么 n-1即为 1111 ,这样的值和hashCode()直接做按位与操作,实际上只使用了哈希值的后4位。如果当哈希值的高位变化很大,低位变化很小,就很容易造成哈希冲突了,所以这里**把高低位都利用起来,让高16位也参与运算**,从而解决了这个问题 + 原因:当数组长度很小,假设是 16,那么 n-1即为 1111 ,这样的值和 hashCode() 直接做按位与操作,实际上只使用了哈希值的后4位。如果当哈希值的高位变化很大,低位变化很小,就很容易造成哈希冲突了,所以这里**把高低位都利用起来,让高16 位也参与运算**,从而解决了这个问题 哈希冲突的处理方式: - * 开放定址法:线性探查法(ThreadLocalMap部分详解),平方探查法(i + 1^2、i - 1^2、i + 2^2……)、双重散列(多个哈希函数) + * 开放定址法:线性探查法(ThreadLocalMap 使用),平方探查法(i + 1^2、i - 1^2、i + 2^2……)、双重散列(多个哈希函数) * 链地址法:拉链法 @@ -5015,7 +5017,7 @@ HashMap继承关系如下图所示: 1. `int n = cap - 1`:防止 cap 已经是 2 的幂。如果 cap 已经是 2 的幂, 不执行减 1 操作,则执行完后面的无符号右移操作之后,返回的 capacity 将是这个 cap 的 2 倍 2. n=0 (cap-1 之后),则经过后面的几次无符号右移依然是 0,返回的 capacity 是 1,最后有 n+1 3. |(按位或运算):相同的二进制数位上,都是 0 的时候,结果为 0,否则为 1 - 4. 核心思想:把最高位是 1 的位以及右边的位全部置 1,结果加 1 后就是最小的2的 n 次幂 + 4. 核心思想:**把最高位是 1 的位以及右边的位全部置 1**,结果加 1 后就是最小的2的 n 次幂 例如初始化的值为 10: @@ -9904,7 +9906,7 @@ public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制 #### 直接内存 -直接内存是 Java 堆外的、直接向系统申请的内存区间,不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域 +直接内存是 Java 堆外、直接向系统申请的内存区间,不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域 diff --git a/Prog.md b/Prog.md index 86847d5..52e6c38 100644 --- a/Prog.md +++ b/Prog.md @@ -2233,7 +2233,7 @@ Java 内存模型是 Java MemoryModel(JMM),本身是一种**抽象的概 * 屏蔽各种硬件和操作系统的内存访问差异,实现让 Java 程序在各种平台下都能达到一致的内存访问效果 * 规定了线程和内存之间的一些关系 -根据JMM的设计,系统存在一个主内存(Main Memory),Java 中所有变量都存储在主存中,对于所有线程都是共享的;每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些**变量的拷贝**,线程对所有变量的操作都是先对变量进行拷贝,然后在工作内存中进行,不能直接操作主内存中的变量;线程之间无法相互直接访问,线程间的通信(传递)必须通过主内存来完成 +根据 JMM 的设计,系统存在一个主内存(Main Memory),Java 中所有变量都存储在主存中,对于所有线程都是共享的;每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些**变量的拷贝**,线程对所有变量的操作都是先对变量进行拷贝,然后在工作内存中进行,不能直接操作主内存中的变量;线程之间无法相互直接访问,线程间的通信(传递)必须通过主内存来完成 ![](https://gitee.com/seazean/images/raw/master/Java/JMM内存模型.png) @@ -4498,7 +4498,7 @@ public class LinkedBlockingQueue extends AbstractQueue #### 同步队列 -与其他BlockingQueue不同,SynchronousQueue是一个不存储元素的BlockingQueue +与其他 BlockingQueue 不同,SynchronousQueue 是一个不存储元素的 BlockingQueue (待更新) @@ -4510,8 +4510,6 @@ public class LinkedBlockingQueue extends AbstractQueue #### 延迟队列 -##### 延迟阻塞 - DelayQueue 是一个支持延时获取元素的阻塞队列, 内部采用优先队列 PriorityQueue 存储元素,同时元素必须实现 Delayed 接口;在创建元素时可以指定多久才可以从队列中获取当前元素,只有在延迟期满时才能从队列中提取元素 DelayQueue 只能添加(offer/put/add)实现了 Delayed 接口的对象,不能添加 int、String @@ -4545,12 +4543,6 @@ class DelayTask implements Delayed { -**** - - - -##### 优先队列 - *** @@ -4739,9 +4731,7 @@ Executors提供了四种线程池的创建:newCachedThreadPool、newFixedThrea -#### 操作方式 - -##### 提交方法 +#### 提交方法 ExecutorService类API: @@ -4766,7 +4756,7 @@ execute 和 submit 都属于线程池的方法,对比: -##### 关闭方法 +#### 关闭方法 ExecutorService 类 API: @@ -4784,7 +4774,7 @@ ExecutorService 类 API: -##### 处理异常 +#### 处理异常 execute 会直接抛出任务执行时的异常,submit 会吞掉异常,有两种处理方法 @@ -4816,10 +4806,14 @@ System.out.println(future.get()); + + *** +### 工作原理 + #### 状态信息 ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量 @@ -4848,10 +4842,348 @@ private static int ctlOf(int rs, int wc) { return rs | wc; } +**** + + + +#### Future + +##### 线程使用 + +FutureTask 未来任务对象,继承 Runnable、Future 接口,用于包装 Callable 对象,实现任务的提交 + +```java +public static void main(String[] args) throws ExecutionException, InterruptedException { + FutureTask task = new FutureTask<>(new Callable() { + @Override + public String call() throws Exception { + return "Hello World"; + } + }); + new Thread(task).start(); //启动线程 + String msg = task.get(); //获取返回任务数据 + System.out.println(msg); +} +``` + +构造方法: + +```java +public FutureTask(Callable callable){ + this.callable = callable; // 属性注入 + this.state = NEW; // 任务状态设置为 new +} + +public FutureTask(Runnable runnable, V result) { + //使用装饰者模式将 runnable 转换成 callable 接口,外部线程通过 get 获取 + //当前任务执行结果时,结果可能为 null 也可能为【传进来】的值,传进来什么返回什么 + this.callable = Executors.callable(runnable, result); + this.state = NEW; +} +``` + + + + + +*** + + + +##### 成员属性 + +FutureTask 类的成员属性: + +```java +// 表示当前task状态 +private volatile int state; +// 当前任务尚未执行 +private static final int NEW = 0; +// 当前任务正在结束,尚未完全结束,一种临界状态 +private static final int COMPLETING = 1; +// 当前任务正常结束 +private static final int NORMAL = 2; +// 当前任务执行过程中发生了异常。 内部封装的 callable.run() 向上抛出异常了 +private static final int EXCEPTIONAL = 3; +// 当前任务被取消 +private static final int CANCELLED = 4; +// 当前任务中断中 +private static final int INTERRUPTING = 5; +// 当前任务已中断 +private static final int INTERRUPTED = 6; + +// Runnable 使用 装饰者模式伪装成 Callable +private Callable callable; +// 正常情况下:任务正常执行结束,outcome 保存执行结果,callable 返回值。 +// 非正常情况:callable 向上抛出异常,outcome 保存异常 +private Object outcome; +// 当前任务被线程执行期间,保存当前执行任务的线程对象引用 +private volatile Thread runner; +// 会有很多线程去 get 当前任务的结果,这里使用了一种数据结构头插头取(类似栈)的一个队列来保存所有的 get 线程 +// WaitNode 是单向的链表 +private volatile WaitNode waiters; +``` + + + *** +##### 成员方法 + +FutureTask 类的成员方法: + +* **FutureTask#run**:任务执行入口 + + ```java + public void run() { + //条件一:成立说明当前 task 已经被执行过了或者被 cancel 了,非 NEW 状态的任务,线程就不处理了 + //条件二:线程是 NEW 状态,尝试设置当前任务对象的线程是当前线程,设置失败说明其他线程抢占了该任务 + if (state != NEW || + !UNSAFE.compareAndSwapObject(this, runnerOffset, + null, Thread.currentThread())) + return; //直接返回 + try { + // 执行到这里,当前 task 一定是 NEW 状态,而且当前线程也抢占 task 成功! + Callable c = callable; + // 条件一:防止空指针异常 + // 条件二:防止外部线程在此期间 cancel 掉当前任务。 + if (c != null && state == NEW) { + // 结果引用 + V result; + // true 表示 callable.run 代码块执行成功 未抛出异常 + // false 表示 callable.run 代码块执行失败 抛出异常 + boolean ran; + try { + // 调用自定义的方法 + result = c.call(); + // 没有出现异常 + ran = true; + } catch (Throwable ex) { + // 出现异常,返回值置空,ran 置为 false + result = null; + ran = false; + // 设置返回的异常 + setException(ex); + } + // 代码块执行正常 + if (ran) + // 设置返回的结果 + set(result); + } + } finally { + // 任务执行完成,取消线程的引用 + runner = null; + int s = state; + // 判断任务是不是被中断 + if (s >= INTERRUPTING) + // 执行中断处理方法 + handlePossibleCancellationInterrupt(s); + } + } + ``` + + FutureTask#set:设置正常返回值 + + ```java + protected void set(V v) { + // CAS 方式设置当前任务状态为完成中,设置失败说明其他线程取消了该任务 + if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) { + // 将结果赋值给 outcome + outcome = v; + // 将当前任务状态修改为 NORMAL 正常结束状态。 + UNSAFE.putOrderedInt(this, stateOffset, NORMAL); + finishCompletion(); + } + } + ``` + + FutureTask#setException:设置异常返回值 + + ```java + protected void setException(Throwable t) { + if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) { + // 赋值给返回结果,用来向上层抛出来的异常 + outcome = t; + // 将当前任务的状态 修改为 EXCEPTIONAL + UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); + finishCompletion(); + } + } + ``` + + FutureTask#finishCompletion:完成 + + ```java + private void finishCompletion() { + // 遍历所有的等待的节点,q 指向头节点 + for (WaitNode q; (q = waiters) != null;) { + // 使用cas设置 waiters 为 null,防止外部线程使用 cancel 取消当前任务,也会触发finishCompletion方法 + if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) { + // 自旋 + for (;;) { + // 获取当前 WaitNode 节点封装的 thread + Thread t = q.thread; + // 当前线程不为 null,唤醒当前线程 + if (t != null) { + q.thread = null; + LockSupport.unpark(t); + } + // 获取当前节点的下一个节点 + WaitNode next = q.next; + // 当前节点是最后一个节点了 + if (next == null) + break; + q.next = null; // help gc + q = next; + } + break; + } + } + done(); + callable = null; // help GC + } + ``` + + FutureTask#handlePossibleCancellationInterrupt:任务中断处理 + + ```java + private void handlePossibleCancellationInterrupt(int s) { + if (s == INTERRUPTING) + while (state == INTERRUPTING) + // 等待中断完成 + Thread.yield(); + } + ``` + +* **FutureTask#get**:获取任务执行的返回值 + + ```java + public V get() throws InterruptedException, ExecutionException { + // 获取当前任务状态 + int s = state; + // 条件成立说明任务还没执行完成 + if (s <= COMPLETING) + // 返回 task 当前状态,可能当前线程在里面已经睡了一会 + s = awaitDone(false, 0L); + return report(s); + } + ``` + + FutureTask#awaitDone:线程阻塞等待 + + ```java + private int awaitDone(boolean timed, long nanos) throws InterruptedException { + // 0 不带超时 + final long deadline = timed ? System.nanoTime() + nanos : 0L; + // 引用当前线程,封装成 WaitNode 对象 + WaitNode q = null; + // 表示当前线程 waitNode 对象 有没有入队/压栈 + boolean queued = false; + // 自旋,三次自旋开始休眠 + for (;;) { + // 判断当前 get() 线程是否被打断,打断返回 true,清除打断标记 + if (Thread.interrupted()) { + // 当前线程对应的等待 node 出队, + removeWaiter(q); + throw new InterruptedException(); + } + // 获取任务状态 + int s = state; + // 条件成立:说明当前任务执行完成已经有结果了 + if (s > COMPLETING) { + // 条件成立说明已经为当前线程创建了 WaitNode,置空帮助 GC + if (q != null) + q.thread = null; + // 返回当前的状态 + return s; + } + // 条件成立:说明当前任务接近完成状态,这里让当前线程释放 cpu ,进行下一次抢占 cpu + else if (s == COMPLETING) + Thread.yield(); + // 条件成立:【第一次自旋】,当前线程还未创建 WaitNode 对象,此时为当前线程创建 WaitNode对象 + else if (q == null) + q = new WaitNode();条件成立:第二次自旋,当前线程已经创建 WaitNode对象了,但是node对象还未入队 + // 条件成立:【第二次自旋】,当前线程已经创建 WaitNode 对象了,但是node对象还未入队 + else if (!queued) + // waiters 指向队首,让当前 WaitNode 成为新的队首,头插法 + // 失败说明再次期间有了新的队首 + queued = UNSAFE.compareAndSwapObject(this, waitersOffset, q.next = waiters, q); + // 条件成立:【第三次自旋】,会到这里。 + else if (timed) { + nanos = deadline - System.nanoTime(); + if (nanos <= 0L) { + removeWaiter(q); + return state; + } + // 休眠指定的时间 + LockSupport.parkNanos(this, nanos); + } + // 条件成立:说明需要休眠 + else + // 当前 get 操作的线程就会被 park 了,除非有其它线程将唤醒或者将当前线程中断 + LockSupport.park(this); + } + } + ``` + + FutureTask#report:返回结果 + + ```java + private V report(int s) throws ExecutionException { + // 获取执行结果 + Object x = outcome; + // 当前任务状态正常结束 + if (s == NORMAL) + return (V)x; // 直接返回 callable 的逻辑结果 + // 当前任务被取消或者中断 + if (s >= CANCELLED) + throw new CancellationException(); //抛出异常 + // 执行到这里说明自定义的 callable 中的方法有异常,使用 outcome 上层抛出异常 + throw new ExecutionException((Throwable)x); + } + ``` + +* FutureTask#cancel:任务取消 + + ```java + public boolean cancel(boolean mayInterruptIfRunning) { + // 条件一:表示当前任务处于运行中或者处于线程池任务队列中 + // 条件二:表示修改状态,成功可以去执行下面逻辑,否则 返回 false 表示 cancel 失败。 + if (!(state == NEW && + UNSAFE.compareAndSwapInt(this, stateOffset, NEW, + mayInterruptIfRunning ? INTERRUPTING : CANCELLED))) + return false; + try { + if (mayInterruptIfRunning) { + try { + // 执行当前 FutureTask 的线程 + Thread t = runner; + if (t != null) + // 打断执行的线程 + t.interrupt(); + } finally { + // 设置任务状态为中断完成 + UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED); + } + } + } finally { + // 唤醒所有 get() 阻塞的线程 + finishCompletion(); + } + return true; + } + ``` + + + + + +**** + + + ### 任务调度 #### Timer @@ -4967,42 +5299,9 @@ public ScheduledThreadPoolExecutor(int corePoolSize) { -*** - -#### 定时任务 - -让每周四 18:00:00 定时执行任务 - -```java -public class ThreadPoolDemo04 { - //每周四 18:00:00 执行定时任务 - public static void main(String[] args) { - LocalDateTime now = LocalDateTime.now(); - LocalDateTime time = now.withHour(18).withMinute(0).withSecond(0).withNano(0).with(DayOfWeek.THURSDAY); - - //如果当前时间 > 本周周四 ,必须找下周周四 - if (now.compareTo(time) > 0) { - time = time.plusWeeks(1); - } - - // initialDelay 当前时间和周四的时间差 - // period 每周的间隔 - Duration between = Duration.between(now, time); - long initialDelay = between.toMillis(); - long period = 1000 * 60 * 60 * 24 * 7; - ScheduledExecutorService pool = Executors.newScheduledThreadPool(1); - pool.scheduleAtFixedRate(() -> { - System.out.println("running..."); - },initialDelay,period, TimeUnit.MILLISECONDS); - } -} -``` - - - -*** +**** @@ -5239,7 +5538,7 @@ AQS 核心思想: * 获取锁: ```java - while(state 状态不允许获取) {//tryAcquire(arg) + while(state 状态不允许获取) { //tryAcquire(arg) if(队列中还没有此线程) { 入队并阻塞 park unpark } @@ -5250,7 +5549,7 @@ AQS 核心思想: * 释放锁: ```java - if(state 状态允许了) {//tryRelease(arg) + if(state 状态允许了) { //tryRelease(arg) 恢复阻塞的线程(s) } ``` @@ -7294,746 +7593,6 @@ class ThreadB extends Thread{ (待更新) -#### 并发集合 - -##### 集合对比 - -三种集合: - -* HashMap是线程不安全的,性能好 -* Hashtable线程安全基于synchronized,综合性能差,已经被淘汰 -* ConcurrentHashMap保证了线程安全,综合性能较好,不止线程安全,而且效率高,性能好 - -集合对比: - -1. Hashtable继承Dictionary类,HashMap、ConcurrentHashMap继承AbstractMap,均实现Map接口 -2. Hashtable底层是数组+链表,JDK8以后HashMap和ConcurrentHashMap底层是数组+链表+红黑树 -3. HashMap线程非安全,Hashtable线程安全,Hashtable的方法都加了synchronized关来确保线程同步 -4. ConcurrentHashMap、Hashtable不允许null值,HashMap允许null值 -5. ConcurrentHashMap、HashMap的初始容量为16,Hashtable初始容量为11,填充因子默认都是0.75,两种Map扩容是当前容量翻倍:capacity * 2,Hashtable扩容时是容量翻倍+1:capacity*2 + 1 - -![ConcurrentHashMap数据结构](https://gitee.com/seazean/images/raw/master/Java/ConcurrentHashMap数据结构.png) - -工作步骤: - -1. 初始化,使用 cas 来保证并发安全,懒惰初始化 table -2. 树化,当 table.length < 64 时,先尝试扩容,超过 64 时,并且 bin.length > 8 时,会将链表树化,树化过程 - 会用 synchronized 锁住链表头 -3. put,如果该 bin 尚未创建,只需要使用 cas 创建 bin;如果已经有了,锁住链表头进行后续 put 操作,元素 - 添加至 bin 的尾部 -4. get,无锁操作仅需要保证可见性,扩容过程中 get 操作拿到的是 ForwardingNode 会让 get 操作在新 table 进行搜索 -5. 扩容,扩容时以 bin 为单位进行,需要对 bin 进行 synchronized,但这时其它竞争线程也不是无事可做,它们会帮助把其它 bin 进行扩容 -6. size,元素个数保存在 baseCount 中,并发时的个数变动保存在 CounterCell[] 当中,最后统计数量时累加 - -```java -//需求:多个线程同时往HashMap容器中存入数据会出现安全问题 -public class ConcurrentHashMapDemo{ - public static Map map = new ConcurrentHashMap(); - - public static void main(String[] args){ - new AddMapDataThread().start(); - new AddMapDataThread().start(); - - Thread.sleep(1000 * 5);//休息5秒,确保两个线程执行完毕 - System.out.println("Map大小:" + map.size());//20万 - } -} - -public class AddMapDataThread extends Thread{ - @Override - public void run() { - for(int i = 0 ; i < 1000000 ; i++ ){ - ConcurrentHashMapDemo.map.put("键:"+i , "值"+i); - } - } -} -``` - - - -**** - - - -##### 并发死链 - -JDK1.7的 HashMap 采用的头插法(拉链法)进行节点的添加,HashMap 的扩容长度为原来的 2 倍 - -resize() 中 节点(Entry)转移的源代码: - -```java -void transfer(Entry[] newTable, boolean rehash) { - int newCapacity = newTable.length;//得到新数组的长度 - //遍历整个数组对应下标下的链表,e代表一个节点 - for (Entry e : table) { - //当e == null时,则该链表遍历完了,继续遍历下一数组下标的链表 - while(null != e) { - //先把e节点的下一节点存起来 - Entry next = e.next; - if (rehash) { //得到新的hash值 - e.hash = null == e.key ? 0 : hash(e.key); - } - //在新数组下得到新的数组下标 - int i = indexFor(e.hash, newCapacity); - //将e的next指针指向新数组下标的位置 - e.next = newTable[i]; - //将该数组下标的节点变为e节点 - newTable[i] = e; - //遍历链表的下一节点 - e = next; - } - } -} -``` - -B站视频解析:https://www.bilibili.com/video/BV1n541177Ea - -文章参考:https://www.jianshu.com/p/c4c4ff869149 - -JDK 8 虽然将扩容算法做了调整,改用了尾插法,但仍不意味着能够在多线程环境下能够安全扩容,还会出现其它问题(如扩容丢数据) - - - -*** - - - -#### JDK8源码 - -(待更新) - -##### 成员属性 - -1. 扩容阈值 - - ```java - // 默认为 0、当初始化时为 -1、当扩容时为 -(1 + 扩容线程数) - // 当初始化或扩容完成后,为下一次的扩容的阈值大小,当前数组大小的0.75 - private transient volatile int sizeCtl; - ``` - -2. Node节点 - - ```java - static class Node implements Map.Entry { - final int hash; - final K key; - volatile V val; // 保证并发的可见性 - volatile Node next; - Node(int hash, K key, V val, Node next){//构造方法} - } - ``` - -3. Hash表 - - ```java - transient volatile Node[] table; - private transient volatile Node[] nextTable; //扩容时的新 hash 表 - ``` - -4. 扩容时如果某个 bin 迁移完毕,用 ForwardingNode 作为旧 table bin 的头结点 - - ```java - static final class ForwardingNode extends Node { - ForwardingNode(Node[] tab) { - super(MOVED, null, null, null);// MOVE = -1 - this.nextTable = tab; - } - //super -> Node节点构造方法:Node(int hash, K key, V val, Node next) - } - ``` - -5. compute 以及 computeIfAbsent 时,用来占位,计算完成后替换为普通 Node - - ```java - static final class ReservationNode extends Node{ - ReservationNode() { - super(RESERVED, null, null, null);// RESERVED = -3 - } - } - ``` - -6. treebin 的头节点, 存储 root 和 first - - ```java - static final class TreeBin extends Node{} - ``` - -7. treebin 的节点, 存储 parent、left、right - - ```java - static final class TreeNode extends Node{} - ``` - - - -*** - - - -##### 构造方法 - -懒惰初始化,在构造方法中仅计算了 table 的大小,在第一次使用时才会真正创建: - -```java -public ConcurrentHashMap(int initialCapacity,float loadFactor,int concurrencyLevel){ - // 参数校验 - if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0) - throw new IllegalArgumentException(); - // 检验并发级别 - if (initialCapacity < concurrencyLevel) - initialCapacity = concurrencyLevel; - long size = (long)(1.0 + (long)initialCapacity / loadFactor); - // tableSizeFor 仍然是保证计算的大小是 2^n, 即 16,32,64 ... - int cap = (size >= (long)MAXIMUM_CAPACITY) ? - MAXIMUM_CAPACITY : tableSizeFor((int)size); - this.sizeCtl = cap; -} -``` - - - -*** - - - -##### 成员方法 - -1. put():数组简称(table),链表简称(bin) - - ```java - public V put(K key, V value) { - return putVal(key, value, false); - } - final V putVal(K key, V value, boolean onlyIfAbsent) { - // 不允许存null,和hashmap不同 - if (key == null || value == null) throw new NullPointerException(); - // spread 方法会综合高位低位, 具有更好的 hash 性 - int hash = spread(key.hashCode()); - int binCount = 0; - for (Node[] tab = table;;) { - // f 是链表头节点、fh 是链表头结点的 hash、i 是链表在 table 中的下标 - Node f; int n, i, fh; - if (tab == null || (n = tab.length) == 0) - // 初始化 table 使用 cas 创建成功, 进入下一轮循环 - tab = initTable(); - // 创建头节点 - else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { - if (casTabAt(tab, i, null, new Node(hash, key, value, null))) - break; - } - // 旧table的某个bin的头节点 hash 为-1,表明正在扩容,可以帮忙扩容 - else if ((fh = f.hash) == MOVED) - tab = helpTransfer(tab, f); - else { - V oldVal = null; - // 锁住链表头节点 - synchronized (f) { - if (tabAt(tab, i) == f) { // 确认链表头节点没有被移动 - // 链表 - if (fh >= 0) { - binCount = 1; - // 遍历链表 binCount 对应 链表节点的个数 - for (Node e = f;; ++binCount) { - K ek; - if (e.hash == hash && - ((ek = e.key) == key || - (ek != null && key.equals(ek)))) { - oldVal = e.val; - // 是否允许更新旧值 - if (!onlyIfAbsent) - e.val = value; - break; - } - Node pred = e; - // 最后的节点, 新增 Node 追加至链表尾 - if ((e = e.next) == null) { - pred.next = new Node(hash,key,value,null); - break; - } - } - } - // 红黑树 - else if (f instanceof TreeBin) { - Node p; - binCount = 2; - // 检查 key 是否已经在树中, 是,则返回对应的 TreeNode - if ((p = ((TreeBin)f).putTreeVal(hash, key, - value)) != null) { - oldVal = p.val; - if (!onlyIfAbsent) - p.val = value; - } - } - } - } - if (binCount != 0) { - // 如果链表长度 >= 树化阈值(8), 进行链表转为红黑树 - if (binCount >= TREEIFY_THRESHOLD) - treeifyBin(tab, i); - if (oldVal != null) - return oldVal; - break; - } - } - } - addCount(1L, binCount); - return null; - } - ``` - -2. initTable - - ```java - private final Node[] initTable() { - Node[] tab; int sc; - while ((tab = table) == null || tab.length == 0) { - if ((sc = sizeCtl) < 0) - // 只允许一个线程对表进行初始化,让掉当前线程 CPU 的时间片, - Thread.yield(); - // 尝试将 sizeCtl 设置为 -1(表示初始化 table) - else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { - // 获得锁, 其它线程会在 while() 循环中 yield 直至 table 创建 - try { - if ((tab = table) == null || tab.length == 0) { - int n = (sc > 0) ? sc : DEFAULT_CAPACITY;//16 - @SuppressWarnings("unchecked") - Node[] nt = (Node[])new Node[n]; - table = tab = nt; - sc = n - (n >>> 2);// 16 - 4;n - n/4 = 0.75n - } - } finally { - sizeCtl = sc; - } - break; - } - } - return tab; - } - ``` - -3. get - - ```java - public V get(Object key) { - Node[] tab; Node e, p; int n, eh; K ek; - // spread 方法能确保返回结果是正数 - int h = spread(key.hashCode()); - if ((tab = table) != null && (n = tab.length) > 0 && - (e = tabAt(tab, (n - 1) & h)) != null) { - // 如果头结点已经是要查找的 key - if ((eh = e.hash) == h) { - if ((ek = e.key) == key || (ek != null && key.equals(ek))) - return e.val; - } - // hash 为负数表示该 bin 在扩容中或是 treebin, 这时调用 find 方法来查找 - else if (eh < 0) - return (p = e.find(h, key)) != null ? p.val : null; - // 正常遍历链表,用equals比较 - while ((e = e.next) != null) { - if (e.hash == h && - ((ek = e.key) == key || (ek != null && key.equals(ek)))) - return e.val; - } - } - return null; - } - ``` - -4. size - - size 计算实际发生在 put,remove 改变集合元素的操作之中 - - * 没有竞争发生,向 baseCount 累加计数 - * 有竞争发生,新建 counterCells,向其中的一个 cell 累加计数 - * counterCells 初始有两个 cell - * 如果计数竞争比较激烈,会创建新的 cell 来累加计数 - - ```java - public int size() { - long n = sumCount(); - return ((n < 0L) ? 0 : - (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : - (int)n); - } - final long sumCount() { - CounterCell[] as = counterCells; CounterCell a; - // 将 baseCount 计数与所有 cell 计数累加 - long sum = baseCount; - if (as != null) { - for (int i = 0; i < as.length; ++i) { - if ((a = as[i]) != null) - sum += a.value; - } - } - } - ``` - - - -*** - - - -#### JDK7源码 - -##### 分段锁 - -ConcurrentHashMap 对锁粒度进行了优化,**分段锁技术**,将整张表分成了多个数组(Segment),每个数组又是一个类似 HashMap 数组的结构。`ConcurrentHashMap`允许多个修改操作并发进行,并发时锁住的是每个Segment,其他Segment还是可以操作的,这样不同Segment之间就可以实现并发,大大提高效率 - -底层结构: **Segment 数组 + HashEntry 数组 + 链表**(数组+链表是HashMap的结构) - -* 优点:如果多个线程访问不同的 segment,实际是没有冲突的,这与 jdk8 中是类似的 - -* 缺点:Segments 数组默认大小为16,这个容量初始化指定后就不能改变了,并且不是懒惰初始化 - - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentHashMap 1.7底层结构.png) - - - - - -##### 成员方法 - -1. segment:是一种可重入锁,继承ReentrantLock - - ```java - static final class Segment extends ReentrantLock implements Serializable { - transient volatile HashEntry[] table; //可以理解为包含一个HashMap - } - ``` - -2. 构造方法 - - 无参构造: - - ```java - public ConcurrentHashMap() { - this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL); - } - ``` - - ```java - // 默认初始化容量 - static final int DEFAULT_INITIAL_CAPACITY = 16; - // 默认负载因子 - static final float DEFAULT_LOAD_FACTOR = 0.75f; - // 默认并发级别 - static final int DEFAULT_CONCURRENCY_LEVEL = 16; - ``` - - 说明:并发度就是程序运行时能够**同时更新**ConccurentHashMap且不产生锁竞争的最大线程数,实际上就是ConcurrentHashMap中的分段锁个数。如果并发度设置的过小,会带来严重的锁竞争问题;如果并发度设置的过大,原本位于同一个Segment内的访问会扩散到不同的Segment中,**CPU cache命中率**会下降 - - ```java - public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) { - // 参数校验 - if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0) - throw new IllegalArgumentException(); - // 校验并发级别大小,大于 1<<16,重置为 65536 - if (concurrencyLevel > MAX_SEGMENTS) - concurrencyLevel = MAX_SEGMENTS; - // ssize 必须是 2^n, 即 2, 4, 8, 16 ... 表示了 segments 数组的大小 - int sshift = 0; - int ssize = 1; - // 这个循环可以找到 concurrencyLevel 之上最近的 2的次方值 !!! - while (ssize < concurrencyLevel) { - ++sshift; - ssize <<= 1; - } - // 记录段偏移量 默认是 32 - 4 = 28 - this.segmentShift = 32 - sshift; - // 记录段掩码 默认是 15 即 0000 0000 0000 1111 - this.segmentMask = ssize - 1; - // 最大容量 - if (initialCapacity > MAXIMUM_CAPACITY) - initialCapacity = MAXIMUM_CAPACITY; - // c = 容量/ssize ,默认16/16 = 1,计算每个Segment中的类似于HashMap的容量 - int c = initialCapacity / ssize; - if (c * ssize < initialCapacity) - ++c; //确保向上取值 - int cap = MIN_SEGMENT_TABLE_CAPACITY; - // Segment 中的类似于 HashMap 的容量至少是2或者2的倍数 - while (cap < c) - cap <<= 1; - // 创建 segment数组,设置segments[0] - Segment s0 = new Segment(loadFactor, (int)(cap * loadFactor), - (HashEntry[])new HashEntry[cap]); - // 默认大小为 2,负载因子 0.75,扩容阀值是 2*0.75=1.5,插入第二个值时才会进行扩容 - Segment[] ss = (Segment[])new Segment[ssize]; - UNSAFE.putOrderedObject(ss, SBASE, s0); - this.segments = ss; - } - ``` - -3. put:头插法 - - segmentShift 和 segmentMask 的作用是决定将 key 的 hash 结果匹配到哪个 segment,将 hash 值 高位向低位移动 segmentShift 位,结果再与 segmentMask 做位于运算 - - ```java - public V put(K key, V value) { - Segment s; - if (value == null) - throw new NullPointerException(); - int hash = hash(key); - // 计算出 segment 下标 - int j = (hash >>> segmentShift) & segmentMask; - // 获得 segment 对象, 判断是否为 null, 是则创建该 segment - if ((s = (Segment)UNSAFE.getObject - (segments, (j << SSHIFT) + SBASE)) == null) - // 这时不能确定是否真的为 null, 因为其它线程也发现该 segment 为 null, - // 因此在 ensureSegment 里用 cas 方式保证该 segment 安全性 - s = ensureSegment(j); - // 进入 segment 的put 流程 - return s.put(key, hash, value, false); - } - ``` - - ```java - private Segment ensureSegment(int k) { - final Segment[] ss = this.segments; - long u = (k << SSHIFT) + SBASE; - Segment seg; - // 判断 u 位置的 Segment 是否为null - if ((seg = (Segment)UNSAFE.getObjectVolatile(ss, u)) == null) { - Segment proto = ss[0]; // use segment 0 as prototype - // 获取0号 segment 的 HashEntry 初始化长度 - int cap = proto.table.length; - // 获取0号 segment 的 hash 表里的扩容负载因子,所有的 segment 因子是相同的 - float lf = proto.loadFactor; - // 计算扩容阀值 - int threshold = (int)(cap * lf); - // 创建一个 cap 容量的 HashEntry 数组 - HashEntry[] tab = (HashEntry[])new HashEntry[cap]; - // 再次检查 u 位置的 Segment 是否为null,因为这时可能有其他线程进行了操作 - if ((seg = (Segment)UNSAFE.getObjectVolatile(ss, u)) == null) { - // 初始化 Segment - Segment s = new Segment(lf, threshold, tab); - // 自旋检查 u 位置的 Segment 是否为null - while ((seg = (Segment)UNSAFE.getObjectVolatile(ss, u))==null) { - // 使用CAS 赋值,只会成功一次 - if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s)) - break; - } - } - } - return seg; - } - ``` - - ConcurrentHashMap 在 put 一个数据时的处理流程: - - * 计算要 put 的 key 的位置,获取指定位置的 Segment - * 如果指定位置的 Segment 为空,则初始化这个 Segment - * 检查计算得到的位置的 Segment 是否为null,为 null 继续初始化,使用 Segment[0] 的容量和负载因子创建一个 HashEntry 数组 - * 再次检查计算得到的指定位置的 Segment 是否为null,使用创建的 HashEntry 数组初始化这个 Segment - * 自旋判断指定位置的 Segment 是否为null,使用 CAS 在这个位置赋值为 Segment - * Segment.put 插入 key value 值 - - segment 继承了可重入锁(ReentrantLock),它的 put 方法: - - ```java - final V put(K key, int hash, V value, boolean onlyIfAbsent) { - // 获取 ReentrantLock 独占锁,获取不到,scanAndLockForPut 获取 - // 如果是多核 cpu 最多 tryLock 64 次, 进入 lock 流程 - // 在尝试期间, 还可以顺便看该节点在链表中有没有, 如果没有顺便创建出来 - HashEntry node = tryLock() ? null : scanAndLockForPut(key, hash, value); - - // 执行到这里 segment 已经被成功加锁, 可以安全执行 - V oldValue; - try { - HashEntry[] tab = table; - // 计算要put的数据位置 - int index = (tab.length - 1) & hash; - // CAS 获取 index 坐标的值 - HashEntry first = entryAt(tab, index); - for (HashEntry e = first;;) { - if (e != null) { - // 检查是否 key 已经存在,如果存在,则遍历链表寻找位置,找到后替换 - K k; - if ((k = e.key) == key || - (e.hash == hash && key.equals(k))) { - oldValue = e.value; - if (!onlyIfAbsent) { - e.value = value; - ++modCount; - } - break; - } - e = e.next; - } - else { - // first 有值没说明 index 位置已经有值了,有冲突,链表头插法 - // 之前等待锁时, node 已经被创建, next 指向链表头 - if (node != null) - node.setNext(first); - else - node = new HashEntry(hash, key, value, first); - int c = count + 1; - // 容量大于扩容阀值,小于最大容量,进行扩容 - if (c > threshold && tab.length < MAXIMUM_CAPACITY) - rehash(node); - else - // 将 node 作为链表头 - setEntryAt(tab, index, node); - ++modCount; - count = c; - oldValue = null; - break; - } - } - } finally { - unlock(); - } - return oldValue; - } - ``` - -4. rehash - - 发生在 put 中,因为此时已经获得了锁,因此 rehash 时不需要考虑线程安全 - - 扩容扩容到原来的两倍,老数组里的数据移动到新的数组时,位置要么不变,要么变为 index+ oldSize,参数里的 node 会在扩容之后使用链表**头插法**插入到指定位置 - - ```java - private void rehash(HashEntry node) { - HashEntry[] oldTable = table; - // 老容量 - int oldCapacity = oldTable.length; - // 新容量,扩大两倍 - int newCapacity = oldCapacity << 1; - // 新的扩容阀值 - threshold = (int)(newCapacity * loadFactor); - // 创建新的数组 - HashEntry[] newTable = (HashEntry[]) new HashEntry[newCapacity]; - // 新的掩码,比如2扩容后是4,-1是3,二进制就是11 - int sizeMask = newCapacity - 1; - // 遍历老数组 - for (int i = 0; i < oldCapacity ; i++) { - HashEntry e = oldTable[i]; - if (e != null) { - HashEntry next = e.next; - // 计算新的位置,新的位置只可能是不变或者是老的位置+老的容量 - int idx = e.hash & sizeMask; - // next为空,只有一个节点,直接赋值 - if (next == null) - newTable[idx] = e; - else { - // 如果是链表 - HashEntry lastRun = e; - int lastIdx = idx; - // 遍历 - for (HashEntry last = next; last != null; last = last.next) { - int k = last.hash & sizeMask; - // 与下一个节点位置相等直接继续循环,不相等进入if逻辑块 - if (k != lastIdx) { - // 新位置 - lastIdx = k; - // 把下一个作为新的链表的首部 - lastRun = last; - } - } - // lastRun 后面的元素位置都是相同的,直接作为链表赋值到新位置 - newTable[lastIdx] = lastRun; - - // 遍历剩余元素,头插法到指定 k 位置,需要新建节点 - for (HashEntry p = e; p != lastRun; p = p.next) { - V v = p.value; - int h = p.hash; - int k = h & sizeMask; - HashEntry n = newTable[k]; - newTable[k] = new HashEntry(h, p.key, v, n); - } - } - } - } - // 头插法插入新的节点,put的节点,因为是put节点超过阈值才扩容 - int nodeIndex = node.hash & sizeMask; - node.setNext(newTable[nodeIndex]); - newTable[nodeIndex] = node; - - // 替换为新的 HashEntry table - table = newTable; - } - ``` - - * 第一个 for 是为了寻找一个节点,该节点后面的所有 next 节点的新位置都是相同的,然后把这个作为一个链表搬迁到新位置 - * 第二个 for 循环是为了把剩余的元素通过头插法插入到指定位置链表 - -5. get - - 计算得到 key 的存放位置、遍历指定位置查找相同 key 的 value 值 - - 用于存储键值对数据的`HashEntry`,它的成员变量value跟`next`都是`volatile`类型的,这样就保证别的线程对value值的修改,get方法可以马上看到 - - ```java - public V get(Object key) { - Segment s; - HashEntry[] tab; - int h = hash(key); - // u 为 segment 对象在数组中的偏移量 - long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; - // 计算得到 key 的存放位置 - if ((s = (Segment)UNSAFE.getObjectVolatile(segments, u)) != null && - (tab = s.table) != null) { - for (HashEntry e = (HashEntry) UNSAFE.getObjectVolatile - (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE); - e != null; e = e.next) { - // 如果是链表,遍历查找到相同 key 的 value。 - K k; - if ((k = e.key) == key || (e.hash == h && key.equals(k))) - return e.value; - } - } - return null; - } - ``` - -6. size - - * 计算元素个数前,先不加锁计算两次,如果前后两次结果如一样,认为个数正确返回 - * 如果不一样,进行重试,重试次数超过 3,将所有 segment 锁住,重新计算个数返回 - - ```java - public int size() { - final Segment[] segments = this.segments; - int size; - boolean overflow; - long sum; - long last = 0L; - int retries = -1; - try { - for (;;) { - if (retries++ == RETRIES_BEFORE_LOCK) { - // 超过重试次数, 需要创建所有 segment 并加锁 - for (int j = 0; j < segments.length; ++j) - ensureSegment(j).lock(); - } - sum = 0L; - size = 0; - overflow = false; - for (int j = 0; j < segments.length; ++j) { - Segment seg = segmentAt(segments, j); - if (seg != null) { - sum += seg.modCount; - int c = seg.count; - if (c < 0 || (size += c) < 0) - overflow = true; - } - } - if (sum == last) - break; - last = sum; - } - } finally { - if (retries > RETRIES_BEFORE_LOCK) { - for (int j = 0; j < segments.length; ++j) - segmentAt(segments, j).unlock(); - } - } - return overflow ? Integer.MAX_VALUE : size; - } - ``` - @@ -9293,7 +8852,7 @@ Linux 有五种 I/O 模型: 应用进程通过系统调用 recvfrom 接收数据,会被阻塞,直到数据从内核缓冲区复制到应用进程缓冲区中才返回。阻塞不意味着整个操作系统都被阻塞,其它应用进程还可以执行,只是当前阻塞进程不消耗 CPU 时间,这种模型的 CPU 利用率会比较高 -recvfrom() 用于接收 Socket 传来的数据,并复制到应用进程的缓冲区 buf 中,把 recvfrom() 当成系统调用 +recvfrom() 用于**接收 Socket 传来的数据,并复制到应用进程的缓冲区 buf 中**,把 recvfrom() 当成系统调用 ![](https://gitee.com/seazean/images/raw/master/Java/IO模型-阻塞式IO.png) @@ -9333,7 +8892,7 @@ recvfrom() 用于接收 Socket 传来的数据,并复制到应用进程的缓 #### IO复用 -IO 复用模型使用 select 或者 poll 函数等待数据,select 会监听所有注册好的 IO,等待多个套接字中的任何一个变为可读,等待过程会被**阻塞**,当某个套接字准备好数据变为可读时 select 调用就返回,然后调用 recvfrom 把数据从内核复制到进程中 +IO 复用模型使用 select 或者 poll 函数等待数据,select 会监听所有注册好的 IO,**等待多个套接字中的任何一个变为可读**,等待过程会被阻塞,当某个套接字准备好数据变为可读时 select 调用就返回,然后调用 recvfrom 把数据从内核复制到进程中 IO 复用让单个进程具有处理多个 I/O 事件的能力,又被称为 Event Driven I/O,即事件驱动 I/O @@ -9367,7 +8926,7 @@ IO 复用让单个进程具有处理多个 I/O 事件的能力,又被称为 Ev ##### 函数 -socket 不是文件,只是一个标识符,但是 Unix 操作系统把所有东西都**看作**是文件,所以 socket 说成 file descriptor,也就是 fd +Socket 不是文件,只是一个标识符,但是 Unix 操作系统把所有东西都**看作**是文件,所以 socket 说成 file descriptor,也就是 fd select 允许应用程序监视一组文件描述符,等待一个或者多个描述符成为就绪状态,从而完成 I/O 操作。 @@ -9521,7 +9080,7 @@ select 和 poll 对比: ##### 函数 -epoll 使用事件的就绪通知方式,通过 epoll_ctl() 向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵**红黑树**上,一旦该 fd 就绪,**内核通过 callback 回调函数将 I/O 准备好的描述符加入到一个链表中**管理,进程调用 epoll_wait() 便可以得到事件完成的描述符 +epoll 使用事件的就绪通知方式,通过 epoll_ctl() 向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵**红黑树**上,一旦该 fd 就绪,**内核通过 callback 回调函数将 I/O 准备好的描述符加入到一个链表中**管理,进程调用 epoll_wait() 便可以得到事件就绪的描述符 ```c int epoll_create(int size); @@ -9556,7 +9115,7 @@ int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout * epoll_wait:等待事件的产生,类似于 select() 调用,返回值为本次就绪的 fd 个数,直接从就绪链表获取,时间复杂度 O(1) - * epfd:指定感兴趣的 epoll 事件列表 + * epfd:**指定感兴趣的 epoll 事件列表** * events:指向一个 epoll_event 结构数组,当函数返回时,内核会把就绪状态的数据拷贝到该数组 * maxevents:标明 epoll_event 数组最多能接收的数据量,即本次操作最多能获取多少就绪数据 * timeout:单位为毫秒 @@ -9628,7 +9187,7 @@ else epoll 的特点: * epoll 仅适用于 Linux 系统 -* epoll 使用**一个文件描述符管理多个描述符**,将用户关系的文件描述符的事件存放到内核的一个事件表中 +* epoll 使用**一个文件描述符管理多个描述符**,将用户关系的文件描述符的事件存放到内核的一个事件表(个人理解成哑元节点) * 没有最大描述符数量(并发连接)的限制,打开 fd 的上限远大于1024(1G 内存能监听约10万个端口) * epoll 的时间复杂度 O(1),epoll 理解为 event poll,不同于忙轮询和无差别轮询,调用 epoll_wait **只是轮询就绪链表**。当监听列表有设备就绪时调用回调函数,把就绪 fd 放入就绪链表中,并唤醒在 epoll_wait 中阻塞的进程,所以 epoll 实际上是**事件驱动**(每个事件关联上fd)的,降低了 system call 的时间复杂度 * epoll 内核中根据每个 fd 上的 callback 函数来实现,只有活跃的 socket 才会主动调用 callback,所以使用 epoll 没有前面两者的线性下降的性能问题,效率提高 @@ -9684,9 +9243,9 @@ epoll 的特点: * 进程描述符和用户的进程是一一对应的 * SYS_API 系统调用:如 read、write,系统调用就是 0X80 中断 -* 进程描述符 pd:进程从用户态切换到内核态时,需要保存用户态时的上下文信息 +* 进程描述符 pd:进程从用户态切换到内核态时,需要保存用户态时的上下文信息在 PCB 中 * 线程上下文:用户程序基地址,程序计数器、cpu cache、寄存器等,方便程序切回用户态时恢复现场 -* 内核堆栈:系统调用函数也是要创建变量的,这些变量在内核堆栈上分配 +* 内核堆栈:**系统调用函数也是要创建变量的,**这些变量在内核堆栈上分配 ![](https://gitee.com/seazean/images/raw/master/Java/IO-用户态和内核态.png) @@ -9737,7 +9296,7 @@ DMA (Direct Memory Access) :直接存储器访问,让外部设备不通过 C 把内存数据传输到网卡然后发送: * 没有 DMA:CPU 读内存数据到 CPU 高速缓存,再写到网卡,这样就把 CPU 的速度拉低到和网卡一个速度 -* 使用 DMA:把内存数据读到 socket 内核缓存区(CPU复制),CPU 分配给 DMA 开始**异步**操作,DMA 读取 socket 缓冲区到 DMA 缓冲区,然后写到网卡。DMA 执行完后中断 CPU,这时 socket 内核缓冲区为空,CPU 从用户态切换到内核态,执行中断处理程序,将需要使用 socket 缓冲区的阻塞进程移到运行队列 +* 使用 DMA:把数据读到 Socket 内核缓存区(CPU复制),CPU 分配给 DMA 开始**异步**操作,DMA 读取 Socket 缓冲区到 DMA 缓冲区,然后写到网卡。DMA 执行完后中断(就是通知) CPU,这时 Socket 内核缓冲区为空,CPU 从用户态切换到内核态,执行中断处理程序,将需要使用 Socket 缓冲区的阻塞进程移到就绪队列 一个完整的 DMA 传输过程必须经历 DMA 请求、DMA 响应、DMA 传输、DMA 结束四个步骤: @@ -9764,7 +9323,7 @@ DMA 方式是一种完全由硬件进行组信息传送的控制方式,通常 * JVM 发出 read() 系统调用,OS 上下文切换到内核模式(切换1)并将数据从网卡或硬盘等通过 DMA 读取到内核空间缓冲区(拷贝1) * OS 内核将数据复制到用户空间缓冲区(拷贝2),然后 read 系统调用返回,又会导致一次内核空间到用户空间的上下文切换(切换2) * JVM 处理代码逻辑并发送 write() 系统调用,OS 上下文切换到内核模式(切换3)并从用户空间缓冲区复制数据到内核空间缓冲区(拷贝3) -* write 系统调用返回,导致内核空间到用户空间的再次上下文切换(切换4),将内核空间缓冲区中的数据写到 hardware(拷贝4) +* 将内核空间缓冲区中的数据写到 hardware(拷贝4),write 系统调用返回,导致内核空间到用户空间的再次上下文切换(切换4) 流程图中的箭头反过来也成立,可以从网卡获取数据 @@ -9784,12 +9343,12 @@ read 调用图示:read、write 都是系统调用指令 mmap(Memory Mapped Files)加 write 实现零拷贝,**零拷贝就是没有数据从内核空间复制到用户空间** -用户空间和内核空间都使用内存,所以可以共享同一块物理内存地址,省去用户态和内核态之间的拷贝。写网卡时,共享空间的内容拷贝到 socket 缓冲区,然后交给 DMA 发送到网卡,只需要 3 次复制 +用户空间和内核空间都使用内存,所以可以共享同一块物理内存地址,省去用户态和内核态之间的拷贝。写网卡时,共享空间的内容拷贝到 Socket 缓冲区,然后交给 DMA 发送到网卡,只需要 3 次复制 进行了 4 次用户空间与内核空间的上下文切换,以及 3 次数据拷贝(2 次 DMA,一次 CPU 复制): * 发出 mmap 系统调用,DMA 拷贝到内核缓冲区;mmap系统调用返回,无需拷贝 -* 发出 write 系统调用,将数据从内核缓冲区拷贝到内核 socket 缓冲区;write系统调用返回,DMA 将内核空间 socket 缓冲区中的数据传递到协议引擎 +* 发出 write 系统调用,将数据从内核缓冲区拷贝到内核 Socket 缓冲区;write系统调用返回,DMA 将内核空间 Socket 缓冲区中的数据传递到协议引擎 ![](https://gitee.com/seazean/images/raw/master/Java/IO-mmap工作流程.png) @@ -9797,7 +9356,7 @@ mmap(Memory Mapped Files)加 write 实现零拷贝,**零拷贝就是没有 缺点:不可靠,写到 mmap 中的数据并没有被真正的写到硬盘,操作系统会在程序主动调用 flush 的时候才把数据真正的写到硬盘 -Java NIO 提供了 **MappedByteBuffer** 类可以用来实现 mmap 内存映射,MappedByteBuffer 类对象**只能**通过调用 `FileChannel.map()` 获取 +Java NIO 提供了 **MappedByteBuffer** 类可以用来实现 mmap 内存映射,MappedByteBuffer 类对象**只能通过调用 `FileChannel.map()` 获取** @@ -9813,7 +9372,7 @@ sendfile 实现零拷贝,打开文件的文件描述符 fd 和 socket 的 fd ![](https://gitee.com/seazean/images/raw/master/Java/IO-sendfile工作流程.png) -sendfile2.4 之后,sendfile 实现了更简单的方式,文件到达内核缓冲区后,不必再将数据全部复制到 socket buffer 缓冲区,而是只**将记录数据位置和长度相关等描述符信息**保存到 socket buffer,DMA 根据 socket 缓冲区中描述符提供的位置和偏移量信息直接将内核空间缓冲区中的数据拷贝到协议引擎上(2 次复制 2 次切换) +sendfile2.4 之后,sendfile 实现了更简单的方式,文件到达内核缓冲区后,不必再将数据全部复制到 socket buffer 缓冲区,而是只**将记录数据位置和长度相关等描述符信息**保存到 socket buffer,DMA 根据 Socket 缓冲区中描述符提供的位置和偏移量信息直接将内核空间缓冲区中的数据拷贝到协议引擎上(2 次复制 2 次切换) Java NIO 对 sendfile 的支持是 `FileChannel.transferTo()/transferFrom()`,把磁盘文件读取 OS 内核缓冲区后的 fileChannel,直接转给 socketChannel 发送,底层就是 sendfile @@ -10532,13 +10091,13 @@ Buffer 底层是一个数组,可以保存多个相同类型的数据,根据 #### 基本属性 -* 容量 (capacity):作为一个内存块,Buffer 具有固定大小,缓冲区容量不能为负,并且创建后不能更改 +* 容量(capacity):作为一个内存块,Buffer 具有固定大小,缓冲区容量不能为负,并且创建后不能更改 -* 限制 (limit):表示缓冲区中可以操作数据的大小(limit 后数据不能进行读写),缓冲区的限制不能为负,并且不能大于其容量。 **写入模式,限制等于 buffer 的容量;读取模式下,limit 等于写入的数据量** +* 限制 (limit):表示缓冲区中可以操作数据的大小(limit 后数据不能进行读写),缓冲区的限制不能为负,并且不能大于其容量。 写入模式,限制等于 buffer 的容量;读取模式下,limit 等于写入的数据量 -* 位置 (position):下一个要读取或写入的数据的索引,缓冲区的位置不能为负,并且不能大于其限制 +* 位置(position):**下一个要读取或写入的数据的索引**,缓冲区的位置不能为负,并且不能大于其限制 -* 标记 (mark)与重置 (reset):标记是一个索引,通过Buffer中的 mark() 方法指定 Buffer 中一个特定的位置,可以通过调用 reset() 方法恢复到这个 position +* 标记(mark)与重置(reset):标记是一个索引,通过 Buffer 中的 mark() 方法指定 Buffer 中一个特定的位置,可以通过调用 reset() 方法恢复到这个 position * 位置、限制、容量遵守以下不变式: **0 <= position <= limit <= capacity** @@ -10552,7 +10111,7 @@ Buffer 底层是一个数组,可以保存多个相同类型的数据,根据 #### 常用API -`static XxxBuffer allocate(int capacity)` : 创建一个容量为capacity 的 XxxBuffer 对象 +`static XxxBuffer allocate(int capacity)` : 创建一个容量为 capacity 的 XxxBuffer 对象 Buffer 基本操作: @@ -10662,7 +10221,7 @@ Byte Buffer 有两种类型,一种是基于直接内存(也就是非堆内 Direct Memory 优点: * Java 的 NIO 库允许 Java 程序使用直接内存,用于数据缓冲区,使用 native 函数直接分配堆外内存 -* 读写性能高,读写频繁的场合可能会考虑使用直接内存 +* **读写性能高**,读写频繁的场合可能会考虑使用直接内存 * 大大提高 IO 性能,避免了在 Java 堆和 native 堆来回复制数据 直接内存缺点: @@ -10695,11 +10254,11 @@ JVM 直接内存图解: -#### 源码解析 +#### 通信原理 -直接内存创建 Buffer 对象:`static XxxBuffer allocateDirect(int capacity)` +堆外内存不受 JVM GC 控制,可以使用堆外内存进行通信,防止 GC 后缓冲区位置发生变化的情况 -堆外内存不受 JVM GC 控制,可以使用堆外内存进行**通信**,防止 GC 后缓冲区位置发生变化的情况,源码: +NIO 使用的 SocketChannel 的源码解析: * SocketChannel#write(java.nio.ByteBuffer) → SocketChannelImpl#write(java.nio.ByteBuffer) @@ -10738,6 +10297,8 @@ JVM 直接内存图解: #### 分配回收 +直接内存创建 Buffer 对象:`static XxxBuffer allocateDirect(int capacity)` + DirectByteBuffer 源码分析: ```java @@ -10745,14 +10306,17 @@ DirectByteBuffer(int cap) { //.... long base = 0; try { + // 分配直接内存 base = unsafe.allocateMemory(size); } + // 内存赋值 unsafe.setMemory(base, size, (byte) 0); if (pa && (base % ps != 0)) { address = base + ps - (base & (ps - 1)); } else { address = base; } + // 创建回收函数 cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); } private static class Deallocator implements Runnable { @@ -10763,7 +10327,7 @@ private static class Deallocator implements Runnable { } ``` -分配和回收原理: +**分配和回收原理**: * 使用了 Unsafe 对象的 allocateMemory 方法完成直接内存的分配,setMemory 方法完成赋值 * ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦 ByteBuffer 对象被垃圾回收,那么 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 Deallocator 的 run方法,最后通过 freeMemory 来释放直接内存 @@ -10809,7 +10373,7 @@ public class Demo1_27 { #### 共享内存 -FileChannel 提供 map 方法把文件映射到虚拟内存,通常情况可以映射整个文件,如果文件比较大,可以进行分段映射,完成映射后对物理内存的操作会被同步到硬盘上 +FileChannel 提供 map 方法返回 MappedByteBuffer 对象,把文件映射到内存,通常情况可以映射整个文件,如果文件比较大,可以进行分段映射,完成映射后对物理内存的操作会被同步到硬盘上 FileChannel 中的成员属性: @@ -10820,9 +10384,9 @@ FileChannel 中的成员属性: * `public final FileLock lock()`:获取此文件通道的排他锁 -MappedByteBuffer,可以让文件直接在内存(堆外内存)中进行修改,这种方式叫做内存映射,可以直接调用系统底层的缓存,没有 JVM 和系统之间的复制操作,提高了传输效率,作用: +MappedByteBuffer,可以让文件在直接内存(堆外内存)中进行修改,这种方式叫做**内存映射**,可以直接调用系统底层的缓存,没有 JVM 和 OS 之间的复制操作,提高了传输效率,作用: -* 用在进程间的通信,能达到**共享内存页**的作用,但在高并发下要对文件内存进行加锁,防止出现读写内容混乱和不一致性,Java 提供了文件锁 FileLock,但在父/子进程中锁定后另一进程会一直等待,效率不高 +* **用在进程间的通信,能达到共享内存页的作用**,但在高并发下要对文件内存进行加锁,防止出现读写内容混乱和不一致性,Java 提供了文件锁 FileLock,但在父/子进程中锁定后另一进程会一直等待,效率不高 * 读写那些太大而不能放进内存中的文件 MappedByteBuffer 较之 ByteBuffer新增的三个方法 @@ -10834,9 +10398,12 @@ MappedByteBuffer 较之 ByteBuffer新增的三个方法 ```java public class MappedByteBufferTest { public static void main(String[] args) throws Exception { - RandomAccessFile ra = new RandomAccessFile("1.txt", "rw"); + //RandomAccessFile ra = (RandomAccess) new RandomAccessFile("1.txt", "rw"); + //FileChannel channel = ra.getChannel(); + + FileInputStream is = new FileInputStream("data01.txt"); //获取对应的通道 - FileChannel channel = ra.getChannel(); + FileChannel channel = is.getChannel(); /** * 参数1 FileChannel.MapMode.READ_WRITE 使用的读写模式 @@ -10862,7 +10429,7 @@ public class MappedByteBufferTest { - read() 是系统调用,首先将文件从硬盘拷贝到内核空间的一个缓冲区,再将这些数据拷贝到用户空间,实际上进行了两次数据拷贝 - mmap() 也是系统调用,但没有进行数据拷贝,当缺页中断发生时,直接将文件从硬盘拷贝到共享内存,只进行了一次数据拷贝 -注意:mmap 的文件映射,在 Full GC 时才会进行释放,如果需要手动清除内存映射文件,可以反射调用sun.misc.Cleaner 方法 +注意:mmap 的文件映射,在 Full GC 时才会进行释放,如果需要手动清除内存映射文件,可以反射调用 sun.misc.Cleaner 方法 diff --git a/SSM.md b/SSM.md index 57b6bd8..aad9797 100644 --- a/SSM.md +++ b/SSM.md @@ -7300,7 +7300,7 @@ Spring 模板对象:TransactionTemplate、JdbcTemplate、RedisTemplate、Rabbi * **BeanDefinationRegistry**:存放 BeanDefination 的容器,是一种键值对的形式,通过特定的 Bean 定义的 id,映射到相应的 BeanDefination,**BeanFactory 的实现类同样继承 BeanDefinationRegistry 接口**,拥有保存 BD 的能力 -* **BeanDefinitionReader**:读取配置文件,比如 xml 用 dom4j 解析,配置文件用 io 流 +* **BeanDefinitionReader**:读取配置文件,比如 XML 用 dom4j 解析,配置文件用 IO 流 程序: @@ -8364,8 +8364,8 @@ public Object postProcessAfterInitialization(@Nullable Object bean,String bN){ // cacheKey 是 beanName 或者加上 & Object cacheKey = getCacheKey(bean.getClass(), beanName); if (this.earlyProxyReferences.remove(cacheKey) != bean) { - //去提前代理引用池中寻找该key,不存在则创建代理 - //如果存在则证明被代理过,则判断是否是当前的 bean,不是则创建代理 + // 去提前代理引用池中寻找该key,不存在则创建代理 + // 如果存在则证明被代理过,则判断是否是当前的 bean,不是则创建代理 return wrapIfNecessary(bean, bN, cacheKey); } } @@ -10577,7 +10577,7 @@ protected void doDispatch(HttpServletRequest request, HttpServletResponse respon return; } } - + // 拦截器链的前置处理 if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } @@ -10589,7 +10589,7 @@ protected void doDispatch(HttpServletRequest request, HttpServletResponse respon } // 设置视图名字 applyDefaultViewName(processedRequest, mv); - // 执行拦截器链中的方法 + // 执行拦截器链中的后置处理方法 mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception ex) { dispatchException = ex; @@ -10814,36 +10814,36 @@ protected ModelAndView invokeHandlerMethod(HttpServletRequest request, * `if (!this.resolvers.supportsParameter(parameter))`:**获取可以解析当前参数的参数解析器** - * `return getArgumentResolver(parameter) != null`:获取参数的解析是否为空 + `return getArgumentResolver(parameter) != null`:获取参数的解析是否为空 - * `for (HandlerMethodArgumentResolver resolver : this.argumentResolvers)`:遍历容器内所有的解析器 + * `for (HandlerMethodArgumentResolver resolver : this.argumentResolvers)`:遍历容器内所有的解析器 - `if (resolver.supportsParameter(parameter))`:是否支持当前参数 + `if (resolver.supportsParameter(parameter))`:是否支持当前参数 - * `PathVariableMethodArgumentResolver#supportsParameter`:**解析标注 @PathVariable 注解的参数** - * `ModelMethodProcessor#supportsParameter`:解析 Map 类型的参数 - * `ModelMethodProcessor#supportsParameter`:解析 Model 类型的参数,Model 和 Map 的作用一样 - * `ExpressionValueMethodArgumentResolver#supportsParameter`:解析标注 @Value 注解的参数 - * `RequestParamMapMethodArgumentResolver#supportsParameter`:**解析标注 @RequestParam 注解** - * `RequestPartMethodArgumentResolver#supportsParameter`:解析文件上传的信息 - * `ModelAttributeMethodProcessor#supportsParameter`:解析标注 @ModelAttribute 注解或者不是简单类型 - * 子类 ServletModelAttributeMethodProcessor 是**解析自定义类型 JavaBean 的解析器** - * 简单类型有 Void、Enum、Number、CharSequence、Date、URI、URL、Locale、Class + * `PathVariableMethodArgumentResolver#supportsParameter`:**解析标注 @PathVariable 注解的参数** + * `ModelMethodProcessor#supportsParameter`:解析 Map 类型的参数 + * `ModelMethodProcessor#supportsParameter`:解析 Model 类型的参数,Model 和 Map 的作用一样 + * `ExpressionValueMethodArgumentResolver#supportsParameter`:解析标注 @Value 注解的参数 + * `RequestParamMapMethodArgumentResolver#supportsParameter`:**解析标注 @RequestParam 注解** + * `RequestPartMethodArgumentResolver#supportsParameter`:解析文件上传的信息 + * `ModelAttributeMethodProcessor#supportsParameter`:解析标注 @ModelAttribute 注解或者不是简单类型 + * 子类 ServletModelAttributeMethodProcessor 是**解析自定义类型 JavaBean 的解析器** + * 简单类型有 Void、Enum、Number、CharSequence、Date、URI、URL、Locale、Class * `args[i] = this.resolvers.resolveArgument()`:**开始解析参数,每个参数使用的解析器不同** - + `resolver = getArgumentResolver(parameter)`:获取参数解析器 `return resolver.resolveArgument()`:开始解析 * `PathVariableMapMethodArgumentResolver#resolveArgument`:@PathVariable,包装 URI 中的参数为 Map - * `MapMethodProcessor#resolveArgument`:调用 `mavContainer.getModel()` 返回默认的 BindingAwareModelMap 对象 + * `MapMethodProcessor#resolveArgument`:调用 `mavContainer.getModel()` 返回默认 BindingAwareModelMap 对象 * `ModelAttributeMethodProcessor#resolveArgument`:**自定义的 JavaBean 的绑定封装**,下一小节详解 `return doInvoke(args)`:真正的执行方法 - + * `Method method = getBridgedMethod()`:从 HandlerMethod 获取要反射执行的方法 -* `ReflectionUtils.makeAccessible(method)`:破解权限 + * `ReflectionUtils.makeAccessible(method)`:破解权限 * `method.invoke(getBean(), args)`:**执行方法**,getBean 获取的是标记 @Controller 的 Bean 类,其中包含执行方法 * **进行返回值的处理,响应部分详解**,处理完成进入下面的逻辑 @@ -10980,11 +10980,11 @@ protected ModelAndView invokeHandlerMethod(HttpServletRequest request, * `converter.convert()`:**调用转换器的转换方法**(GenericConverter#convert) * `return handleResult(sourceType, targetType, result)`:返回结果 - * `ph.setValue(valueToApply)`:设置 JavaBean 属性(BeanWrapperImpl.BeanPropertyHandler) + * `ph.setValue(valueToApply)`:**设置 JavaBean 属性**(BeanWrapperImpl.BeanPropertyHandler) * `Method writeMethod`:获取 set 方法 * `Class cls = getClass0()`:获取 Class 对象 - * `writeMethodName = Introspector.SET_PREFIX + getBaseName()`:set 前缀 + 属性名 + * `writeMethodName = Introspector.SET_PREFIX + getBaseName()`:**set 前缀 + 属性名** * `writeMethod = Introspector.findMethod(cls, writeMethodName, 1, args)`:获取只包含一个参数的 set 方法 * `setWriteMethod(writeMethod)`:加入缓存 * `ReflectionUtils.makeAccessible(writeMethod)`:设置访问权限 @@ -11079,7 +11079,7 @@ public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer * `for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers)`:遍历所有的返回值处理器 * `RequestResponseBodyMethodProcessor#supportsReturnType`:**处理标注 @ResponseBody 注解的返回值** - * `ModelAndViewMethodReturnValueHandler#supportsReturnType`:处理**返回值类型**是 ModelAndView 的处理器 + * `ModelAndViewMethodReturnValueHandler#supportsReturnType`:处理返回值类型是 ModelAndView 的处理器 * `ModelAndViewResolverMethodReturnValueHandler#supportsReturnType`:直接返回 true,处理所有数据 **RequestResponseBodyMethodProcessor#handleReturnValue**:处理返回值 @@ -11094,13 +11094,13 @@ public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer * `if (value instanceof CharSequence)`:判断返回的数据是不是字符类型 - * `body = value`:把 value 赋值给 body,此时 body 中就是填充后的 Person 对象 + * `body = value`:把 value 赋值给 body,此时 body 中就是自定义方法执行完后的 Person 对象 * `if (isResourceType(value, returnType))`:当前数据是不是流数据 * `MediaType selectedMediaType`:**内容协商后选择使用的类型,浏览器和服务器都支持的媒体(数据)类型** - `MediaType contentType = outputMessage.getHeaders().getContentType()`:获取响应头的数据 + * `MediaType contentType = outputMessage.getHeaders().getContentType()`:获取响应头的数据 * `if (contentType != null && contentType.isConcrete())`:判断当前响应头中是否已经有确定的媒体类型 @@ -11180,8 +11180,8 @@ public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer * `writeInternal(t, type, outputMessage)`:**真正的写出数据的函数** * `Object value = object`:value 引用 Person 对象 - * `ObjectWriter objectWriter = objectMapper.writer()`:获取用来输出 JSON 对象的 ObjectWriter - * `objectWriter.writeValue(generator, value)`:写出数据为 JSON + * `ObjectWriter objectWriter = objectMapper.writer()`:获取 ObjectWriter 对象 + * `objectWriter.writeValue(generator, value)`:**使用 ObjectWriter 写出数据为 JSON** @@ -11213,7 +11213,7 @@ spring.mvc.contentnegotiation:favor-parameter: true #开启请求参数内容 * `request.getParameter(getParameterName())`:获取 URL 中指定的需求的数据类型 * `getParameterName()`:获取参数的属性名 format - * `getParameter()`:获取 URL 中 format 对应的数据 + * `getParameter()`:**获取 URL 中 format 对应的数据** `resolveMediaTypeKey()`:解析媒体类型,封装成集合 @@ -11366,7 +11366,7 @@ DispatcherServlet#render: * `attrs = RequestContextHolder.getRequestAttributes()`:获取请求的相关属性信息 - * `requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest())`:获取最佳匹配的媒体类型,函数内进行了匹配的逻辑 + * `requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest())`:**获取最佳匹配的媒体类型**,函数内进行了匹配的逻辑 * `candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes)`:获取候选的视图对象 @@ -11376,19 +11376,18 @@ DispatcherServlet#render: `AbstractCachingViewResolver#resolveViewName`:调用此方法 - **请求转发**:实例为 InternalResourceView - * `returnview = createView(viewName, locale)`:UrlBasedViewResolver#createView + **请求转发**:实例为 InternalResourceView + * `if (viewName.startsWith(FORWARD_URL_PREFIX))`:视图名字是否是 **`forward:`** 的前缀 * `forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length())`:**名字截取前缀** * `view = new InternalResourceView(forwardUrl)`:新建 InternalResourceView 对象并返回 * `return applyLifecycleMethods(FORWARD_URL_PREFIX, view)`:Spring 中的初始化操作 - **重定向**:实例为 RedirectView + **重定向**:实例为 RedirectView - * `returnview = createView(viewName, locale)`:UrlBasedViewResolver#createView * `if (viewName.startsWith(REDIRECT_URL_PREFIX))`:视图名字是否是 **`redirect:`** 的前缀 * `redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length())`:名字截取前缀 * `RedirectView view = new RedirectView()`:新建 RedirectView 对象并返回 @@ -11403,9 +11402,10 @@ DispatcherServlet#render: * `renderMergedOutputModel(mergedModel, getRequestToExpose(request), response)`:渲染输出的数据 + `getRequestToExpose(request)`:获取 Servlet 原生的方式 + **请求转发** InternalResourceView 的逻辑: - * `getRequestToExpose(request)`:获取 Servlet 原生的方式 * `exposeModelAsRequestAttributes(model, request)`:暴露 model 作为请求域的属性 * `model.forEach()`:遍历 Model 中的数据 * `request.setAttribute(name, value)`:设置到请求域中 @@ -11419,9 +11419,9 @@ DispatcherServlet#render: * `targetUrl = createTargetUrl(model, request)`:获取目标 URL * `enc = request.getCharacterEncoding()`:设置编码 UTF-8 * `appendQueryProperties(targetUrl, model, enc)`:添加一些属性,比如 `url + ?name=123&&age=324` - * `sendRedirect(request, response, targetUrl, this.http10Compatible)`:重定向 + * `sendRedirect(request, response, targetUrl, this.http10Compatible)`:重定向 - * `response.sendRedirect(encodedURL)`:**使用 Servlet 原生方法实现重定向** + * `response.sendRedirect(encodedURL)`:**使用 Servlet 原生方法实现重定向** @@ -14290,6 +14290,7 @@ SpringApplication#run(String... args): * `stopWatch.start()`:记录应用的启动时间 * `bootstrapContext = createBootstrapContext()`:**创建引导上下文环境** + * `bootstrapContext = new DefaultBootstrapContext()`:创建默认的引导类环境 * `this.bootstrapRegistryInitializers.forEach()`:遍历所有的引导器调用 initialize 方法完成初始化设置 * `configureHeadlessProperty()`:让当前应用进入 headless 模式 From fd53025458356b4af1664a60c68650b8f9eb5e83 Mon Sep 17 00:00:00 2001 From: Seazean Date: Tue, 10 Aug 2021 00:15:15 +0800 Subject: [PATCH 016/168] Update Java Notes --- Java.md | 746 ++++++++++++++++++++++++++++---------------------------- Prog.md | 39 ++- SSM.md | 65 +++-- 3 files changed, 441 insertions(+), 409 deletions(-) diff --git a/Java.md b/Java.md index 67a998c..503cae5 100644 --- a/Java.md +++ b/Java.md @@ -4654,7 +4654,7 @@ HashMap继承关系如下图所示: -##### 成员变量 +##### 成员属性 1. 序列化版本号 @@ -4662,14 +4662,14 @@ HashMap继承关系如下图所示: private static final long serialVersionUID = 362498820763181265L; ``` -2. 集合的初始化容量( **必须是二的n次幂** ) +2. 集合的初始化容量(**必须是二的n次幂** ) ```java //默认的初始容量是16 -- 1<<4相当于1*2的4次方---1*16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; ``` - HashMap构造方法指定集合的初始化容量大小: + HashMap 构造方法指定集合的初始化容量大小: ```java HashMap(int initialCapacity)//构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap @@ -4686,7 +4686,7 @@ HashMap继承关系如下图所示: 例如长度为9时候,3&(9-1)=0 2&(9-1)=0 ,都在0上,碰撞了; ``` - * 如果输入值不是2的幂会怎么样? + * 如果输入值不是 2 的幂会怎么样? 创建 HashMap 对象时,HashMap 通过位移运算和或运算得到的肯定是 2 的幂次数,并且是大于那个数的最近的数字,底层采用 tableSizeFor() 方法 @@ -4744,7 +4744,7 @@ HashMap继承关系如下图所示: static final int UNTREEIFY_THRESHOLD = 6; ``` -7. 当 Map 里面的数量**大于等于**这个阈值时,表中的桶才能进行树形化 ,否则桶内元素太多时会扩容,而不是树形化。为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD (8) +7. 当 Map 里面的数量**大于等于**这个阈值时,表中的桶才能进行树形化 ,否则桶内元素超过 8 时会扩容,而不是树形化。为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD (8) ```java //桶中结构转化为红黑树对应的数组长度最小的值 @@ -4753,7 +4753,7 @@ HashMap继承关系如下图所示: 原因:数组比较小的情况下变为红黑树结构,反而会降低效率,红黑树需要进行左旋,右旋,变色这些操作来保持平衡。同时数组长度小于 64 时,搜索时间相对快些,所以为了提高性能和减少搜索时间,底层在阈值大于 8 并且数组长度大于 64 时,链表才转换为红黑树,效率也变的更高效 -8. table 用来初始化(必须是二的n次幂) +8. table 用来初始化(必须是二的 n 次幂) ```java //存储元素的数组 @@ -4762,28 +4762,28 @@ HashMap继承关系如下图所示: jdk8 之前数组类型是 Entry类型,之后是 Node 类型。只是换了个名字,都实现了一样的接口 Map.Entry,负责存储键值对数据的 - 9. HashMap中存放元素的个数(**重点**) + 9. HashMap 中存放元素的个数(**重点**) ```java //存放元素的个数,HashMap中K-V的实时数量,不是table数组的长度 transient int size; ``` -10. 记录HashMap的修改次数 +10. 记录 HashMap 的修改次数 ```java //每次扩容和更改map结构的计数器 transient int modCount; ``` -11. 调整大小下一个容量的值计算方式为(容量*负载因子) +11. 调整大小下一个容量的值计算方式为(容量 * 负载因子) ```java //临界值,当实际大小(容量*负载因子)超过临界值时,会进行扩容 int threshold; ``` -12. **哈希表的加载因子(重点)** +12. **哈希表的加载因子(重点)** ```java final float loadFactor; @@ -4791,9 +4791,9 @@ HashMap继承关系如下图所示: * 加载因子的概述 - loadFactor加载因子,是用来衡量 HashMap 满的程度,表示**HashMap的疏密程度**,影响hash操作到同一个数组位置的概率,计算HashMap的实时加载因子的方法为:size/capacity,而不是占用桶的数量去除以capacity,capacity 是桶的数量,也就是 table 的长度length。 + loadFactor 加载因子,是用来衡量 HashMap 满的程度,表示 **HashMap 的疏密程度**,影响 hash 操作到同一个数组位置的概率,计算 HashMap 的实时加载因子的方法为:size/capacity,而不是占用桶的数量去除以 capacity,capacity 是桶的数量,也就是 table 的长度 length。 - 当HashMap里面容纳的元素已经达到HashMap数组长度的75%时,表示HashMap拥挤,需要扩容,而扩容这个过程涉及到 rehash、复制数据等操作,非常消耗性能,所以开发中尽量减少扩容的次数,可以通过创建HashMap集合对象时指定初始容量来尽量避免。 + 当 HashMap 里面容纳的元素已经达到 HashMap 数组长度的 75% 时,表示 HashMap 拥挤,需要扩容,而扩容这个过程涉及到 rehash、复制数据等操作,非常消耗性能,所以开发中尽量减少扩容的次数,可以通过创建 HashMap 集合对象时指定初始容量来尽量避免。 ```java HashMap(int initialCapacity, float loadFactor)//构造指定初始容量和加载因子的空HashMap @@ -4813,87 +4813,87 @@ HashMap继承关系如下图所示: ##### 构造方法 -1. 构造一个空的 HashMap ,**默认初始容量(16)和默认负载因子(0.75)** +* 构造一个空的 HashMap ,**默认初始容量(16)和默认负载因子(0.75)** - ```java - public HashMap() { - this.loadFactor = DEFAULT_LOAD_FACTOR; - //将默认的加载因子0.75赋值给loadFactor,并没有创建数组 - } - ``` + ```java + public HashMap() { + this.loadFactor = DEFAULT_LOAD_FACTOR; + //将默认的加载因子0.75赋值给loadFactor,并没有创建数组 + } + ``` -2. 构造一个具有指定的初始容量和默认负载因子(0.75)HashMap +* 构造一个具有指定的初始容量和默认负载因子(0.75)HashMap - ```java - // 指定“容量大小”的构造函数 - public HashMap(int initialCapacity) { - this(initialCapacity, DEFAULT_LOAD_FACTOR); - } - ``` + ```java + // 指定“容量大小”的构造函数 + public HashMap(int initialCapacity) { + this(initialCapacity, DEFAULT_LOAD_FACTOR); + } + ``` -3. 构造一个具有指定的初始容量和负载因子的HashMap +* 构造一个具有指定的初始容量和负载因子的HashMap - ```java - public HashMap(int initialCapacity, float loadFactor) { - //进行判断 - //将指定的加载因子赋值给HashMap成员变量的负载因子loadFactor - this.loadFactor = loadFactor; - //最后调用了tableSizeFor - this.threshold = tableSizeFor(initialCapacity); - } - ``` + ```java + public HashMap(int initialCapacity, float loadFactor) { + //进行判断 + //将指定的加载因子赋值给HashMap成员变量的负载因子loadFactor + this.loadFactor = loadFactor; + //最后调用了tableSizeFor + this.threshold = tableSizeFor(initialCapacity); + } + ``` - * 对于`this.threshold = tableSizeFor(initialCapacity);` + * 对于`this.threshold = tableSizeFor(initialCapacity);` - 有些人会觉得这里是一个bug应该这样书写: - `this.threshold = tableSizeFor(initialCapacity) * this.loadFactor;` - 这样才符合 threshold 的概念,但是在 jdk8 以后的构造方法中,并没有对 table 这个成员变量进行初始化,table 的初始化被推迟到了 put 方法中,在 put 方法中会对 threshold 重新计算 - -4. 包含另一个`Map`的构造函数 + 有些人会觉得这里是一个bug应该这样书写: + `this.threshold = tableSizeFor(initialCapacity) * this.loadFactor;` + 这样才符合 threshold 的概念,但是在 jdk8 以后的构造方法中,并没有对 table 这个成员变量进行初始化,table 的初始化被推迟到了 put 方法中,在 put 方法中会对 threshold 重新计算 - ```java - //构造一个映射关系与指定 Map 相同的新 HashMap - public HashMap(Map m) { - //负载因子loadFactor变为默认的负载因子0.75 - this.loadFactor = DEFAULT_LOAD_FACTOR; - putMapEntries(m, false); - } - ``` +* 包含另一个`Map`的构造函数 + + ```java + //构造一个映射关系与指定 Map 相同的新 HashMap + public HashMap(Map m) { + //负载因子loadFactor变为默认的负载因子0.75 + this.loadFactor = DEFAULT_LOAD_FACTOR; + putMapEntries(m, false); + } + ``` - putMapEntries源码分析: + putMapEntries源码分析: - ```java - final void putMapEntries(Map m, boolean evict) { - //获取参数集合的长度 - int s = m.size(); - if (s > 0) { - //判断参数集合的长度是否大于0 - if (table == null) { // 判断table是否已经初始化 - // pre-size - // 未初始化,s为m的实际元素个数 - float ft = ((float)s / loadFactor) + 1.0F; - int t = ((ft < (float)MAXIMUM_CAPACITY) ? - (int)ft : MAXIMUM_CAPACITY); - // 计算得到的t大于阈值,则初始化阈值 - if (t > threshold) - threshold = tableSizeFor(t); - } - // 已初始化,并且m元素个数大于阈值,进行扩容处理 - else if (s > threshold) - resize(); - // 将m中的所有元素添加至HashMap中 - for (Map.Entry e : m.entrySet()) { - K key = e.getKey(); - V value = e.getValue(); - putVal(hash(key), key, value, false, evict); - } - } - } - ``` + ```java + final void putMapEntries(Map m, boolean evict) { + //获取参数集合的长度 + int s = m.size(); + if (s > 0) { + //判断参数集合的长度是否大于0 + if (table == null) { // 判断table是否已经初始化 + // pre-size + // 未初始化,s为m的实际元素个数 + float ft = ((float)s / loadFactor) + 1.0F; + int t = ((ft < (float)MAXIMUM_CAPACITY) ? + (int)ft : MAXIMUM_CAPACITY); + // 计算得到的t大于阈值,则初始化阈值 + if (t > threshold) + threshold = tableSizeFor(t); + } + // 已初始化,并且m元素个数大于阈值,进行扩容处理 + else if (s > threshold) + resize(); + // 将m中的所有元素添加至HashMap中 + for (Map.Entry e : m.entrySet()) { + K key = e.getKey(); + V value = e.getValue(); + putVal(hash(key), key, value, false, evict); + } + } + } + ``` - `float ft = ((float)s / loadFactor) + 1.0F;`这一行代码中为什么要加1.0F ? + `float ft = ((float)s / loadFactor) + 1.0F` 这一行代码中为什么要加 1.0F ? - s / loadFactor的结果是小数,加1.0F相当于是对小数做一个向上取整以尽可能的保证更大容量,更大的容量能够减少resize的调用次数,这样可以减少数组的扩容 + s / loadFactor 的结果是小数,加 1.0F 相当于是对小数做一个向上取整以尽可能的保证更大容量,更大的容量能够减少 resize 的调用次数,这样可以减少数组的扩容 @@ -4903,335 +4903,345 @@ HashMap继承关系如下图所示: ##### 成员方法 -1. hash +* hash() - HashMap 是支持 Key 为空的;HashTable 是直接用 Key 来获取 HashCode,key 为空会抛异常 + HashMap 是支持 Key 为空的;HashTable 是直接用 Key 来获取 HashCode,key 为空会抛异常 - * &(按位与运算):相同的二进制数位上,都是1的时候,结果为1,否则为零 - * ^(按位异或运算):相同的二进制数位上,数字相同,结果为0,不同为1,**不进位加法** - - ```java - static final int hash(Object key) { - int h; - // 1)如果key等于null:可以看到当key等于null的时候也是有哈希值的,返回的是0. - // 2)如果key不等于null:首先计算出key的hashCode赋值给h,然后与h无符号右移16位后的二进制进行按位异或得到最后的hash值 - return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); - } - ``` - - 计算 hash 的方法:将 hashCode 无符号右移 16 位,高 16bit 和低 16bit 做了一个异或,扰动运算 - - 原因:当数组长度很小,假设是 16,那么 n-1即为 1111 ,这样的值和 hashCode() 直接做按位与操作,实际上只使用了哈希值的后4位。如果当哈希值的高位变化很大,低位变化很小,就很容易造成哈希冲突了,所以这里**把高低位都利用起来,让高16 位也参与运算**,从而解决了这个问题 - - 哈希冲突的处理方式: - - * 开放定址法:线性探查法(ThreadLocalMap 使用),平方探查法(i + 1^2、i - 1^2、i + 2^2……)、双重散列(多个哈希函数) - * 链地址法:拉链法 - - - -2. put + * &(按位与运算):相同的二进制数位上,都是1的时候,结果为1,否则为零 + * ^(按位异或运算):相同的二进制数位上,数字相同,结果为0,不同为1,**不进位加法** - jdk1.8 前是头插法 (拉链法),多线程下扩容出现循环链表,jdk1.8 以后引入红黑树,插入方法变成尾插法 + ```java + static final int hash(Object key) { + int h; + // 1)如果key等于null:可以看到当key等于null的时候也是有哈希值的,返回的是0. + // 2)如果key不等于null:首先计算出key的hashCode赋值给h,然后与h无符号右移16位后的二进制进行按位异或得到最后的hash值 + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); + } + ``` - 第一次调用 put 方法时创建数组 Node[] table,因为散列表耗费内存,为了防止内存浪费,所以**延迟初始化** + 计算 hash 的方法:将 hashCode 无符号右移 16 位,高 16bit 和低 16bit 做了一个异或,扰动运算 - 存储数据步骤(存储过程): + 原因:当数组长度很小,假设是 16,那么 n-1即为 1111 ,这样的值和 hashCode() 直接做按位与操作,实际上只使用了哈希值的后4位。如果当哈希值的高位变化很大,低位变化很小,就很容易造成哈希冲突了,所以这里**把高低位都利用起来,让高16 位也参与运算**,从而解决了这个问题 - 1. 先通过 hash 值计算出 key 映射到哪个桶 + 哈希冲突的处理方式: - 2. 如果桶上没有碰撞冲突,则直接插入 + * 开放定址法:线性探查法(ThreadLocalMap 使用),平方探查法(i + 1^2、i - 1^2、i + 2^2……)、双重散列(多个哈希函数) + * 链地址法:拉链法 - 3. 如果出现碰撞冲突:如果该桶使用红黑树处理冲突,则调用红黑树的方法插入数据;否则采用传统的链式方法插入,如果链的长度达到临界值,则把链转变为红黑树 + - 4. 如果数组位置相同,通过 equals 比较内容是否相同:相同则新的 value 覆盖旧 value,不相同则将新的键值对添加到哈希表中 - 5. 如果 size 大于阈值 threshold,则进行扩容 +* put() - ```java - public V put(K key, V value) { - return putVal(hash(key), key, value, false, true); - } - ``` + jdk1.8 前是头插法 (拉链法),多线程下扩容出现循环链表,jdk1.8 以后引入红黑树,插入方法变成尾插法 - putVal() 方法中 key 在这里执行了一下 hash(),在 putVal 函数中使用到了上述 hash 函数计算的哈希值: + 第一次调用 put 方法时创建数组 Node[] table,因为散列表耗费内存,为了防止内存浪费,所以**延迟初始化** - ```java - final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { - //。。。。。。。。。。。。。。 - if ((p = tab[i = (n - 1) & hash]) == null){//这里的n表示数组长度16 - //..... - } else { - if (e != null) { // existing mapping for key - V oldValue = e.value; - //onlyIfAbsent默认为false,所以可以覆盖已经存在的数据,如果为true说明不能覆盖 - if (!onlyIfAbsent || oldValue == null) - e.value = value; - afterNodeAccess(e); - return oldValue; - } - } - } - ``` + 存储数据步骤(存储过程): - * `(n - 1) & hash`:计算下标位置 + 1. 先通过 hash 值计算出 key 映射到哪个桶 - + 2. 如果桶上没有碰撞冲突,则直接插入 - * 余数本质是不断做除法,把剩余的数减去,运算效率要比位运算低 + 3. 如果出现碰撞冲突:如果该桶使用红黑树处理冲突,则调用红黑树的方法插入数据;否则采用传统的链式方法插入,如果链的长度达到临界值,则把链转变为红黑树 - + 4. 如果数组位置相同,通过 equals 比较内容是否相同:相同则新的 value 覆盖旧 value,不相同则将新的键值对添加到哈希表中 + 5. 如果 size 大于阈值 threshold,则进行扩容 -3. treeifyBin + ```java + public V put(K key, V value) { + return putVal(hash(key), key, value, false, true); + } + ``` - 节点添加完成之后判断此时节点个数是否大于TREEIFY_THRESHOLD临界值8,如果大于则将链表转换为红黑树,转换红黑树的方法 treeifyBin,整体代码如下: + putVal() 方法中 key 在这里执行了一下 hash(),在 putVal 函数中使用到了上述 hash 函数计算的哈希值: - ```java - if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st - //转换为红黑树 tab表示数组名 hash表示哈希值 - treeifyBin(tab, hash); - ``` + ```java + final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { + //。。。。。。。。。。。。。。 + if ((p = tab[i = (n - 1) & hash]) == null){//这里的n表示数组长度16 + //..... + } else { + if (e != null) { // existing mapping for key + V oldValue = e.value; + //onlyIfAbsent默认为false,所以可以覆盖已经存在的数据,如果为true说明不能覆盖 + if (!onlyIfAbsent || oldValue == null) + e.value = value; + afterNodeAccess(e); + return oldValue; + } + } + } + ``` - 1. 如果当前数组为空或者数组的长度小于进行树形化的阈值(MIN_TREEIFY_CAPACITY = 64)就去扩容,而不是将节点变为红黑树 - 2. 如果是树形化遍历桶中的元素,创建相同个数的树形节点,复制内容,建立起联系,类似单向链表转换为双向链表 - 3. 让桶中的第一个元素即数组中的元素指向新建的红黑树的节点,以后这个桶里的元素就是红黑树而不是链表数据结构了 + * `(n - 1) & hash`:计算下标位置 - + -4. tableSizeFor - 创建 HashMap 指定容量时,HashMap 通过位移运算和或运算得到比指定初始化容量大的最小的2的 n 次幂 + * 余数本质是不断做除法,把剩余的数减去,运算效率要比位运算低 - ```java - static final int tableSizeFor(int cap) {//int cap = 10 - int n = cap - 1; - n |= n >>> 1; - n |= n >>> 2; - n |= n >>> 4; - n |= n >>> 8; - n |= n >>> 16; - return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; - } - ``` + - 分析算法: +* treeifyBin() - 1. `int n = cap - 1`:防止 cap 已经是 2 的幂。如果 cap 已经是 2 的幂, 不执行减 1 操作,则执行完后面的无符号右移操作之后,返回的 capacity 将是这个 cap 的 2 倍 - 2. n=0 (cap-1 之后),则经过后面的几次无符号右移依然是 0,返回的 capacity 是 1,最后有 n+1 - 3. |(按位或运算):相同的二进制数位上,都是 0 的时候,结果为 0,否则为 1 - 4. 核心思想:**把最高位是 1 的位以及右边的位全部置 1**,结果加 1 后就是最小的2的 n 次幂 + 节点添加完成之后判断此时节点个数是否大于TREEIFY_THRESHOLD临界值8,如果大于则将链表转换为红黑树,转换红黑树的方法 treeifyBin,整体代码如下: - 例如初始化的值为 10: + ```java + if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st + //转换为红黑树 tab表示数组名 hash表示哈希值 + treeifyBin(tab, hash); + ``` - * 第一次右移 + 1. 如果当前数组为空或者数组的长度小于进行树形化的阈值(MIN_TREEIFY_CAPACITY = 64)就去扩容,而不是将节点变为红黑树 + 2. 如果是树形化遍历桶中的元素,创建相同个数的树形节点,复制内容,建立起联系,类似单向链表转换为双向链表 + 3. 让桶中的第一个元素即数组中的元素指向新建的红黑树的节点,以后这个桶里的元素就是红黑树而不是链表数据结构了 - ```java - int n = cap - 1;//cap=10 n=9 - n |= n >>> 1; - 00000000 00000000 00000000 00001001 //9 - 00000000 00000000 00000000 00000100 //9右移之后变为4 - -------------------------------------------------- - 00000000 00000000 00000000 00001101 //按位或之后是13 - //使得n的二进制表示中与最高位的1紧邻的右边一位为1 - ``` + - * 第二次右移 +* tableSizeFor() + 创建 HashMap 指定容量时,HashMap 通过位移运算和或运算得到比指定初始化容量大的最小的2的 n 次幂 - ```java - n |= n >>> 2;//n通过第一次右移变为了:n=13 - 00000000 00000000 00000000 00001101 // 13 - 00000000 00000000 00000000 00000011 // 13右移之后变为3 - ------------------------------------------------- - 00000000 00000000 00000000 00001111 //按位或之后是15 - //无符号右移两位,会将最高位两个连续的1右移两位,然后再与原来的n做或操作,这样n的二进制表示的高位中会有4个连续的1 - ``` + ```java + static final int tableSizeFor(int cap) {//int cap = 10 + int n = cap - 1; + n |= n >>> 1; + n |= n >>> 2; + n |= n >>> 4; + n |= n >>> 8; + n |= n >>> 16; + return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; + } + ``` - 注意:容量最大是 32bit 的正数,因此最后 `n |= n >>> 16`,最多是 32 个 1(但是这已经是负数了)。在执行 tableSizeFor 之前,对 initialCapacity 做了判断,如果大于 MAXIMUM_CAPACITY(2 ^ 30),则取 MAXIMUM_CAPACITY;如果小于 MAXIMUM_CAPACITY(2 ^ 30),会执行移位操作,所以移位操作之后,最大 30 个 1,加 1 之后得 2 ^ 30 + 分析算法: - * 得到的 capacity 被赋值给了 threshold + 1. `int n = cap - 1`:防止 cap 已经是 2 的幂。如果 cap 已经是 2 的幂, 不执行减 1 操作,则执行完后面的无符号右移操作之后,返回的 capacity 将是这个 cap 的 2 倍 + 2. n=0 (cap-1 之后),则经过后面的几次无符号右移依然是 0,返回的 capacity 是 1,最后有 n+1 + 3. |(按位或运算):相同的二进制数位上,都是 0 的时候,结果为 0,否则为 1 + 4. 核心思想:**把最高位是 1 的位以及右边的位全部置 1**,结果加 1 后就是大于指定容量的最小的 2 的 n 次幂 - ```java - this.threshold = tableSizeFor(initialCapacity);//initialCapacity=10 - ``` + 例如初始化的值为 10: - * JDK 11 + * 第一次右移 - ```java - static final int tableSizeFor(int cap) { - //无符号右移,高位补0 - //-1补码: 11111111 11111111 11111111 11111111 - int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1); - return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; - } - //返回最高位之前的0的位数 - public static int numberOfLeadingZeros(int i) { - if (i <= 0) - return i == 0 ? 32 : 0; - // 如果i>0,那么就表明在二进制表示中其至少有一位为1 - int n = 31; - // i的最高位1在高16位,把i右移16位,让最高位1进入低16位继续递进判断 - if (i >= 1 << 16) { n -= 16; i >>>= 16; } - if (i >= 1 << 8) { n -= 8; i >>>= 8; } - if (i >= 1 << 4) { n -= 4; i >>>= 4; } - if (i >= 1 << 2) { n -= 2; i >>>= 2; } - return n - (i >>> 1); - } - ``` + ```java + int n = cap - 1;//cap=10 n=9 + n |= n >>> 1; + 00000000 00000000 00000000 00001001 //9 + 00000000 00000000 00000000 00000100 //9右移之后变为4 + -------------------------------------------------- + 00000000 00000000 00000000 00001101 //按位或之后是13 + //使得n的二进制表示中与最高位的1紧邻的右边一位为1 + ``` - + * 第二次右移 -5. resize + ```java + n |= n >>> 2;//n通过第一次右移变为了:n=13 + 00000000 00000000 00000000 00001101 // 13 + 00000000 00000000 00000000 00000011 // 13右移之后变为3 + ------------------------------------------------- + 00000000 00000000 00000000 00001111 //按位或之后是15 + //无符号右移两位,会将最高位两个连续的1右移两位,然后再与原来的n做或操作,这样n的二进制表示的高位中会有4个连续的1 + ``` - 当 HashMap 中的元素个数超过`(数组长度)*loadFactor(负载因子)`或者链表过长时,就会进行数组扩容,创建新的数组,伴随一次重新 hash 分配,并且遍历 hash 表中所有的元素非常耗时,所以要尽量避免 resize + 注意:容量最大是 32bit 的正数,因此最后 `n |= n >>> 16`,最多是 32 个 1(但是这已经是负数了)。在执行 tableSizeFor 之前,对 initialCapacity 做了判断,如果大于 MAXIMUM_CAPACITY(2 ^ 30),则取 MAXIMUM_CAPACITY;如果小于 MAXIMUM_CAPACITY(2 ^ 30),会执行移位操作,所以移位操作之后,最大 30 个 1,加 1 之后得 2 ^ 30 - 扩容机制为扩容为原来容量的 2 倍: + * 得到的 capacity 被赋值给了 threshold - ```java - if (oldCap > 0) { - if (oldCap >= MAXIMUM_CAPACITY) { - threshold = Integer.MAX_VALUE; - return oldTab; - } - else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && - oldCap >= DEFAULT_INITIAL_CAPACITY) - newThr = oldThr << 1; // double threshold - } - else if (oldThr > 0) // 初始化的threshold赋值给newCap - newCap = oldThr; - else { // zero initial threshold signifies using defaults - newCap = DEFAULT_INITIAL_CAPACITY; - newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); - } - ``` - - HashMap 在进行扩容后,节点**要么就在原来的位置,要么就被分配到"原位置+旧容量"的位置** - - 判断:e.hash 与 oldCap 对应的有效高位上的值是 1,即当前数组长度 n 为 1 的位为 x,如果 key 的哈希值 x 位也为 1,则扩容后的索引为 now + n - - 注意:这里也要求**数组长度 2 的幂** - - ![](https://gitee.com/seazean/images/raw/master/Java/HashMap-resize扩容.png) - - 普通节点: - - ```java - //oldCap旧数组大小 - if ((e.hash & oldCap) == 0) { - if (loTail == null) - loHead = e; - else - loTail.next = e; - loTail = e; - } - else { - if (hiTail == null) - hiHead = e; - else - hiTail.next = e; - hiTail = e; - } - ``` - - 红黑树节点:扩容时 split 方法会将树**拆成高位和低位两个链表**,判断长度是否小于等于6 - - ```java - //如果低位链表首节点不为null,说明有这个链表存在 - if (loHead != null) { - //如果链表下的元素小于等于6 - if (lc <= UNTREEIFY_THRESHOLD) - //那就从红黑树转链表了,低位链表,迁移到新数组中下标不变,还是等于原数组到下标 - tab[index] = loHead.untreeify(map); - else { - //低位链表,迁移到新数组中下标不变,把低位链表整个赋值到这个下标下 - tab[index] = loHead; - //如果高位首节点不为空,说明原来的红黑树已经被拆分成两个链表了 - if (hiHead != null) - //需要构建新的红黑树了 - loHead.treeify(tab); + ```java + this.threshold = tableSizeFor(initialCapacity);//initialCapacity=10 + ``` + + * JDK 11 + + ```java + static final int tableSizeFor(int cap) { + //无符号右移,高位补0 + //-1补码: 11111111 11111111 11111111 11111111 + int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1); + return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; + } + //返回最高位之前的0的位数 + public static int numberOfLeadingZeros(int i) { + if (i <= 0) + return i == 0 ? 32 : 0; + // 如果i>0,那么就表明在二进制表示中其至少有一位为1 + int n = 31; + // i的最高位1在高16位,把i右移16位,让最高位1进入低16位继续递进判断 + if (i >= 1 << 16) { n -= 16; i >>>= 16; } + if (i >= 1 << 8) { n -= 8; i >>>= 8; } + if (i >= 1 << 4) { n -= 4; i >>>= 4; } + if (i >= 1 << 2) { n -= 2; i >>>= 2; } + return n - (i >>> 1); + } + ``` + + + +* resize() + + 当 HashMap 中的元素个数超过`(数组长度)*loadFactor(负载因子)`或者链表过长时(链表长度 > 8,数组长度 < 64),就会进行数组扩容,创建新的数组,伴随一次重新 hash 分配,并且遍历 hash 表中所有的元素非常耗时,所以要尽量避免 resize + + 扩容机制为扩容为原来容量的 2 倍: + + ```java + if (oldCap > 0) { + if (oldCap >= MAXIMUM_CAPACITY) { + // 以前的容量已经是最大容量了,这时调大 扩容阈值 threshold + threshold = Integer.MAX_VALUE; + return oldTab; + } + else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && + oldCap >= DEFAULT_INITIAL_CAPACITY) + newThr = oldThr << 1; // double threshold + } + else if (oldThr > 0) // 初始化的threshold赋值给newCap + newCap = oldThr; + else { // zero initial threshold signifies using defaults + newCap = DEFAULT_INITIAL_CAPACITY; + newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); + } + ``` + + HashMap 在进行扩容后,节点**要么就在原来的位置,要么就被分配到"原位置+旧容量"的位置** + + 判断:e.hash 与 oldCap 对应的有效高位上的值是 1,即当前数组长度 n 为 1 的位为 x,如果 key 的哈希值 x 位也为 1,则扩容后的索引为 now + n + + 注意:这里也要求**数组长度 2 的幂** + + ![](https://gitee.com/seazean/images/raw/master/Java/HashMap-resize扩容.png) + + 普通节点:把所有节点分成两个链表, + + ```java + // 遍历所有的节点 + do { + next = e.next; + // oldCap 旧数组大小,2 的 n 次幂 + if ((e.hash & oldCap) == 0) { + if (loTail == null) + loHead = e; //指向低位链表头节点 + else + loTail.next = e; + loTail = e; //指向低位链表尾节点 + } + else { + if (hiTail == null) + hiHead = e; + else + hiTail.next = e; + hiTail = e; + } + } while ((e = next) != null); + + if (loTail != null) { + loTail.next = null; // 低位链表的最后一个节点可能在原哈希表中指向其他节点,需要断开 + newTab[j] = loHead; + } + ``` + + 红黑树节点:扩容时 split 方法会将树**拆成高位和低位两个链表**,判断长度是否小于等于 6 + + ```java + //如果低位链表首节点不为null,说明有这个链表存在 + if (loHead != null) { + //如果链表下的元素小于等于6 + if (lc <= UNTREEIFY_THRESHOLD) + //那就从红黑树转链表了,低位链表,迁移到新数组中下标不变,还是等于原数组到下标 + tab[index] = loHead.untreeify(map); + else { + //低位链表,迁移到新数组中下标不变,把低位链表整个赋值到这个下标下 + tab[index] = loHead; + //如果高位首节点不为空,说明原来的红黑树已经被拆分成两个链表了 + if (hiHead != null) + //需要构建新的红黑树了 + loHead.treeify(tab); + } + } + ``` + +​ + +* remove() + 删除是首先先找到元素的位置,如果是链表就遍历链表找到元素之后删除。如果是用红黑树就遍历树然后找到之后做删除,树小于6的时候要转链表 + + ```java + final Node removeNode(int hash, Object key, Object value, + boolean matchValue, boolean movable) { + Node[] tab; Node p; int n, index; + //节点数组tab不为空、数组长度n大于0、根据hash定位到的节点对象p,该节点为树的根节点或链表的首节点)不为空,从该节点p向下遍历,找到那个和key匹配的节点对象 + if ((tab = table) != null && (n = tab.length) > 0 && + (p = tab[index = (n - 1) & hash]) != null) { + Node node = null, e; K k; V v;//临时变量,储存要返回的节点信息 + //key和value都相等,直接返回该节点 + if (p.hash == hash && + ((k = p.key) == key || (key != null && key.equals(k)))) + node = p; + + else if ((e = p.next) != null) { + //如果是树节点,调用getTreeNode方法从树结构中查找满足条件的节点 + if (p instanceof TreeNode) + node = ((TreeNode)p).getTreeNode(hash, key); + //遍历链表 + else { + do { + //e节点的键是否和key相等,e节点就是要删除的节点,赋值给node变量 + if (e.hash == hash && + ((k = e.key) == key || + (key != null && key.equals(k)))) { + node = e; + //跳出循环 + break; + } + p = e;//把当前节点p指向e 继续遍历 + } while ((e = e.next) != null); + } + } + //如果node不为空,说明根据key匹配到了要删除的节点 + //如果不需要对比value值或者对比value值但是value值也相等,可以直接删除 + if (node != null && (!matchValue || (v = node.value) == value || + (value != null && value.equals(v)))) { + if (node instanceof TreeNode) + ((TreeNode)node).removeTreeNode(this, tab, movable); + else if (node == p)//node是首节点 + tab[index] = node.next; + else //node不是首节点 + p.next = node.next; + ++modCount; + --size; + //LinkedHashMap + afterNodeRemoval(node); + return node; + } } + return null; } - ``` + ``` -​ + -4. remove - 删除是首先先找到元素的位置,如果是链表就遍历链表找到元素之后删除。如果是用红黑树就遍历树然后找到之后做删除,树小于6的时候要转链表 +* get() - ```java - final Node removeNode(int hash, Object key, Object value, - boolean matchValue, boolean movable) { - Node[] tab; Node p; int n, index; - //节点数组tab不为空、数组长度n大于0、根据hash定位到的节点对象p,该节点为树的根节点或链表的首节点)不为空,从该节点p向下遍历,找到那个和key匹配的节点对象 - if ((tab = table) != null && (n = tab.length) > 0 && - (p = tab[index = (n - 1) & hash]) != null) { - Node node = null, e; K k; V v;//临时变量,储存要返回的节点信息 - //key和value都相等,直接返回该节点 - if (p.hash == hash && - ((k = p.key) == key || (key != null && key.equals(k)))) - node = p; - - else if ((e = p.next) != null) { - //如果是树节点,调用getTreeNode方法从树结构中查找满足条件的节点 - if (p instanceof TreeNode) - node = ((TreeNode)p).getTreeNode(hash, key); - //遍历链表 - else { - do { - //e节点的键是否和key相等,e节点就是要删除的节点,赋值给node变量 - if (e.hash == hash && - ((k = e.key) == key || - (key != null && key.equals(k)))) { - node = e; - //跳出循环 - break; - } - p = e;//把当前节点p指向e 继续遍历 - } while ((e = e.next) != null); - } - } - //如果node不为空,说明根据key匹配到了要删除的节点 - //如果不需要对比value值或者对比value值但是value值也相等,可以直接删除 - if (node != null && (!matchValue || (v = node.value) == value || - (value != null && value.equals(v)))) { - if (node instanceof TreeNode) - ((TreeNode)node).removeTreeNode(this, tab, movable); - else if (node == p)//node是首节点 - tab[index] = node.next; - else //node不是首节点 - p.next = node.next; - ++modCount; - --size; - //LinkedHashMap - afterNodeRemoval(node); - return node; - } - } - return null; - } - ``` - - - -5. get + 1. 通过hash值获取该key映射到的桶 - 1. 通过hash值获取该key映射到的桶 - - 2. 桶上的key就是要查找的key,则直接找到并返回 + 2. 桶上的key就是要查找的key,则直接找到并返回 - 3. 桶上的key不是要找的key,则查看后续的节点: + 3. 桶上的key不是要找的key,则查看后续的节点: - * 如果后续节点是红黑树节点,通过调用红黑树的方法根据key获取value + * 如果后续节点是红黑树节点,通过调用红黑树的方法根据key获取value - * 如果后续节点是链表节点,则通过循环遍历链表根据key获取value + * 如果后续节点是链表节点,则通过循环遍历链表根据key获取value - 4. 红黑树节点调用的是getTreeNode方法通过树形节点的find方法进行查 - - * 查找红黑树,之前添加时已经保证这个树是有序的,因此查找时就是折半查找,效率更高。 - * 这里和插入时一样,如果对比节点的哈希值相等并且通过equals判断值也相等,就会判断key相等,直接返回,不相等就从子树中递归查找 + 4. 红黑树节点调用的是getTreeNode方法通过树形节点的find方法进行查 - 5. 时间复杂度 O(1) - - * 若为树,则在树中通过key.equals(k)查找,**O(logn)** - - * 若为链表,则在链表中通过key.equals(k)查找,**O(n)** + * 查找红黑树,之前添加时已经保证这个树是有序的,因此查找时就是折半查找,效率更高。 + * 这里和插入时一样,如果对比节点的哈希值相等并且通过equals判断值也相等,就会判断key相等,直接返回,不相等就从子树中递归查找 + + 5. 时间复杂度 O(1) + + * 若为树,则在树中通过key.equals(k)查找,**O(logn)** + + * 若为链表,则在链表中通过key.equals(k)查找,**O(n)** @@ -10372,7 +10382,7 @@ GC Roots说明: 3. 从灰色集合中获取对象: * 将本对象引用到的其他对象全部挪到灰色集合中 * 将本对象挪到黑色集合里面 -4. 重复步骤3,直至灰色集合为空时结束 +4. 重复步骤 3,直至灰色集合为空时结束 5. 结束后,仍在白色集合的对象即为 GC Roots 不可达,可以进行回收 @@ -10414,9 +10424,9 @@ objE.fieldG = null; // 写 objD.fieldG = G; // 写 ``` -为了解决问题,可以操作上面三步,**将对象G记录起来,然后作为灰色对象再进行遍历**,比如放到一个特定的集合,等初始的 GC Roots遍历完(并发标记),再遍历该集合(重新标记) +为了解决问题,可以操作上面三步,**将对象 G 记录起来,然后作为灰色对象再进行遍历**,比如放到一个特定的集合,等初始的 GC Roots 遍历完(并发标记),再遍历该集合(重新标记) -> 所以重新标记需要STW,应用程序一直在运行,该集合可能会一直增加新的对象,导致永远都运行不完 +> 所以重新标记需要 STW,应用程序一直在运行,该集合可能会一直增加新的对象,导致永远都运行不完 解决方法:添加读写屏障,读屏障拦截第一步,写屏障拦截第二三步,在读写前后进行一些后置处理: @@ -10965,7 +10975,7 @@ G1中提供了三种垃圾回收模式:YoungGC、Mixed GC 和 FullGC,在不 4. 复制对象:Eden 区内存段中存活的对象会被复制到 survivor 区,survivor 区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到 old 区中空的内存分段,如果 survivor 空间不够,Eden 空间的部分数据会直接晋升到老年代空间 5. 处理引用:处理 Soft,Weak,Phantom,JNI Weak 等引用,最终 Eden 空间的数据为空,GC 停止工作 -* **并发标记过程**: +* **Concurrent Mark **: * 初始标记:标记从根节点直接可达的对象,这个阶段是 STW 的,并且会触发一次年轻代 GC * 根区域扫描 (Root Region Scanning):G1 扫描 survivor 区直接可达的老年代区域对象,并标记被引用的对象,这一过程必须在 Young GC 之前完成 @@ -11021,14 +11031,14 @@ G1 的设计原则就是简化 JVM 性能调优,只需要简单的三步即可 2. 设置堆的最大内存 3. 设置最大的停顿时间(STW) -**不断调优暂停时间指标**: +不断调优暂停时间指标: * `XX:MaxGCPauseMillis=x` 可以设置启动应用程序暂停的时间,G1会根据这个参数选择 CSet 来满足响应时间的设置 * 设置到 100ms 或者 200ms 都可以(不同情况下会不一样),但设置成50ms就不太合理 * 暂停时间设置的太短,就会导致出现 G1 跟不上垃圾产生的速度,最终退化成 Full GC * 对这个参数的调优是一个持续的过程,逐步调整到最佳状态 -**不要设置新生代和老年代的大小**: +不要设置新生代和老年代的大小: - 避免使用 -Xmn 或 -XX:NewRatio 等相关选项显式设置年轻代大小,G1 收集器在运行的时候会调整新生代和老年代的大小,从而达到我们为收集器设置的暂停时间目标 - 设置了新生代大小相当于放弃了 G1 为我们做的自动调优,我们需要做的只是设置整个堆内存的大小,剩下的交给 G1 自己去分配各个代的大小 @@ -11061,9 +11071,9 @@ ZGC 目标: ZGC 的工作过程可以分为 4 个阶段: -* 并发标记(Concurrent Mark): 遍历对象图做可达性分析的阶段,也要经过类似于 G1的初始标记、最终标记的短暂停顿 +* 并发标记(Concurrent Mark): **遍历对象图做可达性分析**的阶段,也要经过类似于 G1的初始标记、最终标记的短暂停顿 * 并发预备重分配( Concurrent Prepare for Relocate): 根据特定的查询条件统计得出本次收集过程要清理哪些 Region,将这些 Region 组成重分配集(Relocation Set) -* 并发重分配(Concurrent Relocate): 重分配是 ZGC 执行过程中的核心阶段,这个过程要把重分配集中的**存活对象**复制到新的 Region 上,并为重分配集中的每个 Region 维护一个转发表(Forward Table),记录从旧地址到新地址的转向关系 +* 并发重分配(Concurrent Relocate): 重分配是 ZGC 执行过程中的核心阶段,这个过程要把重分配集中的**存活对象**复制到新的 Region 上,并为重分配集中的每个 Region 维护一个**转发表(Forward Table)**,记录从旧地址到新地址的转向关系 * 并发重映射(Concurrent Remap):修正整个堆中指向重分配集中旧对象的所有引用,ZGC 的并发映射并不是一个必须要立即完成的任务,ZGC 很巧妙地把并发重映射阶段要做的工作,合并到下一次垃圾收集循环中的并发标记阶段里去完成,因为都是要遍历所有对象的,这样合并节省了一次遍历的开销 ZGC 几乎在所有地方并发执行的,除了初始标记的是 STW 的,但这部分的实际时间是非常少的,所以响应速度快,在尽可能对吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟 @@ -12258,7 +12268,7 @@ public class MyClassLoader extends ClassLoader{ } //获取内存中的完整的字节数组的数据 byte[] byteCodes = baos.toByteArray(); - //调用defineClass(),将字节数组的数据转换为Class的实例。 + //调 用defineClass(),将字节数组的数据转换为 Class的实例。 Class clazz = defineClass(null, byteCodes, 0, byteCodes.length); return clazz; } catch (IOException e) { diff --git a/Prog.md b/Prog.md index 52e6c38..ee2409a 100644 --- a/Prog.md +++ b/Prog.md @@ -3277,7 +3277,7 @@ transient volatile long base; transient volatile int cellsBusy; ``` -Cells 占用内存是相对比较大的,是惰性加载的,在无竞争的情况下直接更新 base 域,在第一次发生竞争的时候(CAS 失败)就会创建一个大小为 2 的 cells 数组,每次扩容都是加倍,所以**数组长度总是 2 的 n 次幂** +Cells 占用内存是相对比较大的,是惰性加载的,在无竞争的情况下直接更新 base 域,在第一次发生竞争的时候(CAS 失败)就会创建一个大小为 2 的 cells 数组,进行分段累加。如果是更新当前线程对应的 cell 槽位时出现的竞争,就会重新计算线程对应的槽位,继续自旋尝试修改。分段迁移后还出现竞争就会扩容 cells 数组长度为原来的两倍,然后重新 rehash,**数组长度总是 2 的 n 次幂** * LongAdder#add:累加方法 @@ -3286,10 +3286,9 @@ Cells 占用内存是相对比较大的,是惰性加载的,在无竞争的 // as 为累加单元数组的引用,b 为基础值,v 表示期望值 // m 表示 cells 数组的长度,a 表示当前线程命中的 cell 单元格 Cell[] as; long b, v; int m; Cell a; - // 条件一: true 说明 cells 已经初始化过了,当前线程需要去 cells 数组累加,不需要在 base 上累加 - // false 说明 cells 未初始化,当前线程应该写到 base 域,进行 || 后的尝试写入 - // 条件二: true 说明 cas 失败,发生竞争,需要扩容或者重试 - // false 说明 cas 成功,累加操作完成 + + // cells 不为空说明 cells 已经被初始化,线程发生了竞争,去更新对应的 cell 槽位 + // 为空说明没有初始化,条件为 fasle,进入 || 后的逻辑去更新 base 域,更新失败表示发生竞争进入条件 if ((as = cells) != null || !casBase(b = base, b + x)) { // uncontended 为 true 表示 cell 没有竞争,false 表示发生竞争 boolean uncontended = true; @@ -3305,7 +3304,7 @@ Cells 占用内存是相对比较大的,是惰性加载的,在无竞争的 (a = as[getProbe() & m]) == null || !(uncontended = a.cas(v = a.value, v + x))) longAccumulate(x, null, uncontended); - // uncontended 在 cell 上累加失败的时候才为 false,其余情况均为 true + // uncontended 在对应的 cell 上累加失败的时候才为 false,其余情况均为 true } } ``` @@ -3314,7 +3313,7 @@ Cells 占用内存是相对比较大的,是惰性加载的,在无竞争的 ```java // x null false | true - final void longAccumulate(long x, LongBinaryOperator fn, boolean w...ed) { + final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) { int h; // 当前线程还没有对应的 cell, 需要随机生成一个 hash 值用来将当前线程绑定到 cell if ((h = getProbe()) == 0) { @@ -3340,7 +3339,7 @@ Cells 占用内存是相对比较大的,是惰性加载的,在无竞争的 Cell r = new Cell(x); // 加锁 if (cellsBusy == 0 && casCellsBusy()) { - // 是否创建成功的标记,进入【创建逻辑】 + // 是否创建成功的标记,进入【创建 cell 逻辑】 boolean created = false; try { Cell[] rs; int m, j; @@ -3372,13 +3371,13 @@ Cells 占用内存是相对比较大的,是惰性加载的,在无竞争的 else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) break; - // CASE 1.4: cells 长度已经超过了最大长度或者已经扩容 + // CASE 1.4: cells 长度已经超过了最大长度 CPU 内核的数量或者已经扩容 else if (n >= NCPU || cells != as) collide = false; // 扩容意向改为false,表示不扩容了 // CASE 1.5: 更改扩容意向 else if (!collide) collide = true; - // CASE 1.6: 扩容逻辑,进行加锁 + // CASE 1.6: 【扩容逻辑】,进行加锁 else if (cellsBusy == 0 && casCellsBusy()) { try { // 再次检查,防止期间被其他线程扩容了 @@ -3403,16 +3402,16 @@ Cells 占用内存是相对比较大的,是惰性加载的,在无竞争的 // CASE2: 运行到这说明 cells 还未初始化,as 为null // 条件一: true 表示当前未加锁 - // 条件二: 其它线程可能会在当前线程给 as 赋值之后修改了 cells,这里需要判断(这里不是线程安全的) + // 条件二: 其它线程可能会在当前线程给 as 赋值之后修改了 cells,这里需要判断(这里不是线程安全的判断) // 条件三: true 表示加锁成功 else if (cellsBusy == 0 && cells == as && casCellsBusy()) { - // 初始化标志,这里就进行 【初始化】 + // 初始化标志,这里就进行 【初始化 cells 数组】 boolean init = false; try { // 再次判断 cells == as 防止其它线程已经初始化了,当前线程再次初始化导致丢失数据 // 因为这里是线程安全的,所以重新检查,经典DCL if (cells == as) { - Cell[] rs = new Cell[2]; + Cell[] rs = new Cell[2];//初始化数组大小为2 rs[h & 1] = new Cell(x);//填充线程对应的cell cells = rs; init = true; //初始化成功 @@ -4607,7 +4606,7 @@ public ThreadPoolExecutor(int corePoolSize, ![](https://gitee.com/seazean/images/raw/master/Java/JUC-线程池工作原理.png) -1. 创建线程池,这时没有创建线程(**懒惰**),等待提交过来的任务请求,调用execute方法才会创建线程 +1. 创建线程池,这时没有创建线程(**懒惰**),等待提交过来的任务请求,调用 execute 方法才会创建线程 2. 当调用 execute() 方法添加一个请求任务时,线程池会做如下判断: * 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务 @@ -4640,7 +4639,7 @@ Executors提供了四种线程池的创建:newCachedThreadPool、newFixedThrea ``` * 核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间 - * LinkedBlockingQueue是一个单向链表实现的阻塞队列,默认大小为 `Integer.MAX_VALUE`,也就是无界队列,可以放任意数量的任务,在任务比较多的时候会导致 OOM(内存溢出) + * LinkedBlockingQueue 是一个单向链表实现的阻塞队列,默认大小为 `Integer.MAX_VALUE`,也就是无界队列,可以放任意数量的任务,在任务比较多的时候会导致 OOM(内存溢出) * 适用于任务量已知,相对耗时的长期任务 * newCachedThreadPool:创建一个可扩容的线程池 @@ -4695,12 +4694,12 @@ Executors提供了四种线程池的创建:newCachedThreadPool、newFixedThrea - 使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题 - 如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题 -- 线程池不允许使用Executors去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式更加明确线程池的运行规则,规避资源耗尽的风险 +- 线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式更加明确线程池的运行规则,规避资源耗尽的风险 - Executors返回的线程池对象弊端如下: + Executors 返回的线程池对象弊端如下: - FixedThreadPool 和 SingleThreadPool: - - 运行的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM + - 运行的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM - CacheThreadPool 和 ScheduledThreadPool: - 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM @@ -4715,7 +4714,7 @@ Executors提供了四种线程池的创建:newCachedThreadPool、newFixedThrea 核心线程数常用公式: -- **CPU 密集型任务(N+1):** 这种任务消耗的是 CPU 资源,可以将核心线程数设置为 N (CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程发生缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间 +- **CPU 密集型任务(N+1):** 这种任务消耗的是 CPU 资源,可以将核心线程数设置为 N (CPU 核心数) + 1,比 CPU 核心数多出来的一个线程是为了防止线程发生缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间 CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行分析 @@ -4733,7 +4732,7 @@ Executors提供了四种线程池的创建:newCachedThreadPool、newFixedThrea #### 提交方法 -ExecutorService类API: +ExecutorService 类 API: | 方法 | 说明 | | ------------------------------------------------------------ | ------------------------------------------------------------ | diff --git a/SSM.md b/SSM.md index aad9797..368342b 100644 --- a/SSM.md +++ b/SSM.md @@ -111,9 +111,9 @@ org.apache.ibatis.session.SqlSession:构建者对象接口,用于执行 SQL * id:属性,唯一标识,配合名称空间使用 * resultType:指定结果映射对象类型,和对应的方法的返回值类型(全限定名)保持一致,但是如果返回值是List则和其泛型保持一致 * parameterType:指定参数映射对象类型,必须和对应的方法的参数类型(全限定名)保持一致 - * statementType:可选 STATEMENT,PREPARED 或 CALLABLE,默认值:PREPARED - * STATEMENT:直接操作 sql,不进行预编译,获取数据:$ Statement - * PREPARED:预处理参数,进行预编译,获取数据:# PreparedStatement + * **statementType**:可选 STATEMENT,PREPARED 或 CALLABLE,默认值:PREPARED + * STATEMENT:直接操作 sql,使用 Statement 不进行预编译,获取数据:$ + * PREPARED:预处理参数,使用 PreparedStatement 进行预编译,获取数据:# * CALLABLE:执行存储过程,CallableStatement * 参数获取方式: @@ -170,7 +170,7 @@ org.apache.ibatis.session.SqlSession:构建者对象接口,用于执行 SQL * 起别名: - * :为全类名起别名的父标签。 + * :为全类名起别名的父标签 * :为全类名起别名的子标签 * type:指定全类名 @@ -201,14 +201,14 @@ org.apache.ibatis.session.SqlSession:构建者对象接口,用于执行 SQL * 配置环境,可以配置多个标签 - * :配置数据库环境标签。default属性:指定哪个environment - * :配置数据库环境子标签。id属性:唯一标识,与default对应 - * :事务管理标签。type属性:默认JDBC事务 - * :数据源标签。 - * type属性:POOLED使用连接池(mybatis内置); UNPOOLED不使用连接池 + * :配置数据库环境标签,default 属性指定哪个 environment + * :配置数据库环境子标签,id 属性是唯一标识,与 default 对应 + * :事务管理标签,type 属性默认 JDBC 事务 + * :数据源标签 + * type 属性:POOLED 使用连接池(mybatis内置),UNPOOLED 不使用连接池 * :数据库连接信息标签。 - * name属性取值:driver,url,username,password - * value属性取值:与name对应 + * name 属性取值:driver,url,username,password + * value 属性取值:与 name 对应 * 引入映射配置文件 * :引入映射配置文件标签 @@ -483,7 +483,7 @@ org.apache.ibatis.session.SqlSession:构建者对象接口,用于执行 SQL 三种方式实现批量操作: -* 标签属性:这种方式属于全局批量 +* 标签属性:这种方式属于**全局批量** ```xml @@ -493,11 +493,11 @@ org.apache.ibatis.session.SqlSession:构建者对象接口,用于执行 SQL defaultExecutorType:配置默认的执行器 - * SIMPLE 就是普通的执行器 + * SIMPLE 就是普通的执行器(默认) * REUSE 执行器会重用预处理语句(PreparedStatement) * BATCH 执行器不仅重用语句还会执行批量更新 -* SqlSession 会话内批量操作: +* SqlSession **会话内批量**操作: ```java public void testBatch() throws IOException{ @@ -2350,7 +2350,7 @@ return new DefaultSqlSessionFactory(config):返回工厂对象 ![](https://gitee.com/seazean/images/raw/master/Frame/MyBatis-获取工厂对象.png) -总结:解析 xml 是对 Configuration 中的属性进行填充,那么我们同样可以在一个类中创建 Configuration 对象,手动设置其中属性的值来达到配置的效果 +总结:解析 XML 是对 Configuration 中的属性进行填充,那么我们同样可以在一个类中创建 Configuration 对象,手动设置其中属性的值来达到配置的效果 @@ -2405,15 +2405,38 @@ MapperRegistry.getMapper(Class, SqlSession):MapperRegistry 是 Configuration MapperProxy.invoke():执行 SQL 语句,Object 类的方法直接执行 -cachedMapperMethod(method):包装成一个 MapperMethod 对象并初始化该对象 - -MapperMethod.execute():根据 switch-case 判断使用的什么类型的 SQL 进行逻辑处理 +```java +public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + try { + // 当前方法是否是属于 Object 类中的方法 + if (Object.class.equals(method.getDeclaringClass())) { + return method.invoke(this, args); + // 当前方法是否是默认方法 + } else if (isDefaultMethod(method)) { + return invokeDefaultMethod(proxy, method, args); + } + } catch (Throwable t) { + throw ExceptionUtil.unwrapThrowable(t); + } + // 包装成一个 MapperMethod 对象并初始化该对象 + final MapperMethod mapperMethod = cachedMapperMethod(method); + // 根据 switch-case 判断使用的什么类型的 SQL 进行逻辑处理,此处分析查询语句的查询操作 + return mapperMethod.execute(sqlSession, args); +} +``` sqlSession.selectOne(String, Object):查询数据,底层调用 DefaultSqlSession.selectList(String, Object) -configuration.getMappedStatement(statement):获取执行者对象 +```java +public List selectList(String statement, Object parameter, RowBounds rowBounds) { + // 获取执行者对象 + MappedStatement ms = configuration.getMappedStatement(statement); + // 开始执行查询语句,参数通过 wrapCollection() 包装成集合类 + return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); +} +``` -executor.query():开始执行查询语句,参数通过 wrapCollection() 包装成集合类 +Executor#query(): * `CachingExecutor.query()`:先执行 @@ -2426,7 +2449,7 @@ executor.query():开始执行查询语句,参数通过 wrapCollection() 包 * `ms.getCache()`:获取二级缓存,`tcm.getObject(cache, key)`:尝试从**二级缓存**中获取数据 -* `BaseExecutor.query()`: +* `BaseExecutor.query()`:再执行 * `localCache.getObject(key) `:尝试从**本地缓存(一级缓存**)获取数据 From 918b8ca1ac4d6adb5f65817dee5b5ae0f442a070 Mon Sep 17 00:00:00 2001 From: Seazean Date: Tue, 10 Aug 2021 17:35:33 +0800 Subject: [PATCH 017/168] Update Java Notes --- Java.md | 15 +- Prog.md | 1282 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 1257 insertions(+), 40 deletions(-) diff --git a/Java.md b/Java.md index 503cae5..af28bc1 100644 --- a/Java.md +++ b/Java.md @@ -4677,7 +4677,7 @@ HashMap继承关系如下图所示: * 为什么必须是 2 的 n 次幂? - HashMap 中添加元素时,需要根据 key 的 hash 值,确定在数组中的具体位置。HashMap 为了存取高效,要尽量较少碰撞,把数据尽可能分配均匀,每个链表长度大致相同,实现该方法的算法就是取模,hash%length,计算机中直接求余效率不如位移运算,所以源码中使用 hash&(length-1),实际上**hash % length == hash & (length-1)的前提是 length 是 2 的n次幂** + HashMap 中添加元素时,需要根据 key 的 hash 值,确定在数组中的具体位置。为了存取高效,要尽量较少碰撞,把数据尽可能分配均匀,每个链表长度大致相同,实现该方法的算法就是取模,hash%length,计算机中直接求余效率不如位移运算,所以源码中使用 hash&(length-1),实际上**hash % length == hash & (length-1)的前提是 length 是 2 的n次幂** 散列平均分布:2 的 n 次方是 1 后面 n 个 0,2 的 n 次方 -1 是 n 个 1,可以**保证散列的均匀性**,减少碰撞 @@ -4919,7 +4919,7 @@ HashMap继承关系如下图所示: } ``` - 计算 hash 的方法:将 hashCode 无符号右移 16 位,高 16bit 和低 16bit 做了一个异或,扰动运算 + 计算 hash 的方法:将 hashCode 无符号右移 16 位,高 16bit 和低 16bit 做异或,扰动运算 原因:当数组长度很小,假设是 16,那么 n-1即为 1111 ,这样的值和 hashCode() 直接做按位与操作,实际上只使用了哈希值的后4位。如果当哈希值的高位变化很大,低位变化很小,就很容易造成哈希冲突了,所以这里**把高低位都利用起来,让高16 位也参与运算**,从而解决了这个问题 @@ -4938,7 +4938,7 @@ HashMap继承关系如下图所示: 存储数据步骤(存储过程): - 1. 先通过 hash 值计算出 key 映射到哪个桶 + 1. 先通过 hash 值计算出 key 映射到哪个桶,哈希寻址 2. 如果桶上没有碰撞冲突,则直接插入 @@ -9513,7 +9513,7 @@ JVM的生命周期分为三个阶段,分别为:启动、运行、死亡。 - 执行一个 Java 程序时,真真正正在执行的是一个 Java 虚拟机的进程 - JVM 有两种运行模式 Server 与 Client,两种模式的区别在于:Client 模式启动速度较快,Server 模式启动较慢;但是启动进入稳定期长期运行之后 Server 模式的程序运行速度比 Client 要快很多 - Server 模式启动的 JVM 采用的是重量级的虚拟机,对程序采用了更多的优化,而 Client 模式启动的 JVM 采用的是轻量级的虚拟机 + Server 模式启动的 JVM 采用的是重量级的虚拟机,对程序采用了更多的优化;Client 模式启动的 JVM 采用的是轻量级的虚拟机 - **死亡**: - 当程序中的用户线程都中止,JVM 才会退出 @@ -10327,15 +10327,14 @@ public void localvarGC4() { - 虚拟机栈中局部变量表中引用的对象:各个线程被调用的方法中使用到的参数、局部变量等 - 本地方法栈中引用的对象 -- 方法区中类静态属性引用的对象 +- 堆中类静态属性引用的对象 - 方法区中的常量引用的对象 - 字符串常量池(string Table)里的引用 - 同步锁 synchronized 持有的对象 -GC Roots说明: -* **GC Roots是一组活跃的引用,不是对象**,放在 GC Roots Set 集合 -* 如果一个指针保存了堆内存中的对象,但自己不在堆内存中,它就是一个Root + +**GC Roots是一组活跃的引用,不是对象**,放在 GC Roots Set 集合 diff --git a/Prog.md b/Prog.md index ee2409a..8d9bf64 100644 --- a/Prog.md +++ b/Prog.md @@ -3329,7 +3329,7 @@ Cells 占用内存是相对比较大的,是惰性加载的,在无竞争的 for (;;) { // as 表示cells引用,a 表示当前线程命中的 cell,n 表示 cells 数组长度,v 表示 期望值 Cell[] as; Cell a; int n; long v; - // CASE1: 表示 cells 已经初始化了,当前线程应该将数据写入到对应的 cell 中 + // 【CASE1】: 表示 cells 已经初始化了,当前线程应该将数据写入到对应的 cell 中 if ((as = cells) != null && (n = as.length) > 0) { // CASE1.1: true 表示当前线程对应的下标位置的 cell 为 null,需要创建 new Cell if ((a = as[(n - 1) & h]) == null) { @@ -3400,7 +3400,7 @@ Cells 占用内存是相对比较大的,是惰性加载的,在无竞争的 h = advanceProbe(h); } - // CASE2: 运行到这说明 cells 还未初始化,as 为null + // 【CASE2】: 运行到这说明 cells 还未初始化,as 为null // 条件一: true 表示当前未加锁 // 条件二: 其它线程可能会在当前线程给 as 赋值之后修改了 cells,这里需要判断(这里不是线程安全的判断) // 条件三: true 表示加锁成功 @@ -3422,7 +3422,7 @@ Cells 占用内存是相对比较大的,是惰性加载的,在无竞争的 if (init) break; //初始化成功直接返回 } - // CASE3: 运行到这说明其他线程在初始化 cells,所以当前线程将值累加到 base,累加成功直接结束自旋 + // 【CASE3】: 运行到这说明其他线程在初始化 cells,所以当前线程将值累加到 base,累加成功直接结束自旋 else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) break; @@ -7590,7 +7590,1225 @@ class ThreadB extends Thread{ ### ConHashMap -(待更新) +#### 并发集合 + +##### 集合对比 + +三种集合: + +* HashMap是线程不安全的,性能好 +* Hashtable线程安全基于synchronized,综合性能差,已经被淘汰 +* ConcurrentHashMap保证了线程安全,综合性能较好,不止线程安全,而且效率高,性能好 + +集合对比: + +1. Hashtable 继承 Dictionary 类,HashMap、ConcurrentHashMap 继承 AbstractMap,均实现 Map 接口 +2. Hashtable 底层是数组 + 链表,JDK8 以后 HashMap 和 ConcurrentHashMap 底层是数组 + 链表 + 红黑树 +3. HashMap 线程非安全,Hashtable 线程安全,Hashtable 的方法都加了 synchronized 关来确保线程同步 +4. ConcurrentHashMap、Hashtable 不允许 null 值,HashMap 允许 null 值 +5. ConcurrentHashMap、HashMap 的初始容量为 16,Hashtable 初始容量为11,填充因子默认都是 0.75,两种 Map 扩容是当前容量翻倍:capacity * 2,Hashtable 扩容时是容量翻倍 + 1:capacity*2 + 1 + +![ConcurrentHashMap数据结构](https://gitee.com/seazean/images/raw/master/Java/ConcurrentHashMap数据结构.png) + +工作步骤: + +1. 初始化,使用 cas 来保证并发安全,懒惰初始化 table +2. 树化,当 table.length < 64 时,先尝试扩容,超过 64 时,并且 bin.length > 8 时,会将**链表树化**,树化过程 + 会用 synchronized 锁住链表头 +3. put,如果该 bin 尚未创建,只需要使用 cas 创建 bin;如果已经有了,锁住链表头进行后续 put 操作,元素 + 添加至 bin 的尾部 +4. get,无锁操作仅需要保证可见性,扩容过程中 get 操作拿到的是 ForwardingNode 会让 get 操作在新 table 进行搜索 +5. 扩容,扩容时以 bin 为单位进行,需要对 bin 进行 synchronized,但这时其它竞争线程也不是无事可做,它们会帮助把其它 bin 进行扩容 +6. size,元素个数保存在 baseCount 中,并发时的个数变动保存在 CounterCell[] 当中,最后统计数量时累加 + +```java +//需求:多个线程同时往HashMap容器中存入数据会出现安全问题 +public class ConcurrentHashMapDemo{ + public static Map map = new ConcurrentHashMap(); + + public static void main(String[] args){ + new AddMapDataThread().start(); + new AddMapDataThread().start(); + + Thread.sleep(1000 * 5);//休息5秒,确保两个线程执行完毕 + System.out.println("Map大小:" + map.size());//20万 + } +} + +public class AddMapDataThread extends Thread{ + @Override + public void run() { + for(int i = 0 ; i < 1000000 ; i++ ){ + ConcurrentHashMapDemo.map.put("键:"+i , "值"+i); + } + } +} +``` + + + +**** + + + +##### 并发死链 + +JDK1.7 的 HashMap 采用的头插法(拉链法)进行节点的添加,HashMap 的扩容长度为原来的 2 倍 + +resize() 中节点(Entry)转移的源代码: + +```java +void transfer(Entry[] newTable, boolean rehash) { + int newCapacity = newTable.length;//得到新数组的长度 + //遍历整个数组对应下标下的链表,e代表一个节点 + for (Entry e : table) { + //当e == null时,则该链表遍历完了,继续遍历下一数组下标的链表 + while(null != e) { + //先把e节点的下一节点存起来 + Entry next = e.next; + if (rehash) { //得到新的hash值 + e.hash = null == e.key ? 0 : hash(e.key); + } + //在新数组下得到新的数组下标 + int i = indexFor(e.hash, newCapacity); + //将e的next指针指向新数组下标的位置 + e.next = newTable[i]; + //将该数组下标的节点变为e节点 + newTable[i] = e; + //遍历链表的下一节点 + e = next; + } + } +} +``` + +JDK 8 虽然将扩容算法做了调整,改用了尾插法,但仍不意味着能够在多线程环境下能够安全扩容,还会出现其它问题(如扩容丢数据) + + + +B站视频解析:https://www.bilibili.com/video/BV1n541177Ea + + + +*** + + + +#### 成员属性 + +##### 变量 + +* 散列表的长度: + + ```java + private static final int MAXIMUM_CAPACITY = 1 << 30; //最大长度 + private static final int DEFAULT_CAPACITY = 16; //默认长度 + ``` + +* 并发级别,JDK7 遗留下来,1.8 中不代表并发级别: + + ```java + private static final int DEFAULT_CONCURRENCY_LEVEL = 16; + ``` + +* 负载因子,JDK1.8 的 ConcurrentHashMap 中是固定值: + + ```java + private static final float LOAD_FACTOR = 0.75f; + ``` + +* 阈值: + + ```java + static final int TREEIFY_THRESHOLD = 8; //链表树化的阈值 + static final int UNTREEIFY_THRESHOLD = 6; //红黑树转化为链表的阈值 + static final int MIN_TREEIFY_CAPACITY = 64; //当数组长度达到 64 且某个桶位中的链表长度超过 8,才会真正树化 + ``` + +* 扩容相关: + + ```java + private static final int MIN_TRANSFER_STRIDE = 16; //线程迁移数据最小步长,控制线程迁移任务的最小区间 + private static int RESIZE_STAMP_BITS = 16; //用来计算扩容时生成的标识戳 + private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;//65535-1 并发扩容最多线程数 + private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS; //扩容时使用 + ``` + +* 节点哈希值: + + ```java + static final int MOVED = -1; //表示当前节点是 FWD 节点 + static final int TREEBIN = -2; //表示当前节点已经树化,且当前节点为 TreeBin 对象 + static final int RESERVED = -3; //表示节点时临时节点 + static final int HASH_BITS = 0x7fffffff; //正常节点的哈希值的可用的位数 + ``` + +* 扩容过程: + + ```java + // 扩容过程中,会将扩容中的新 table 赋值给 nextTable 保持引用,扩容结束之后,这里会被设置为 null + private transient volatile Node[] nextTable; + // 扩容过程中,记录当前进度。所有线程都要从 transferIndex 中分配区间任务,简单说就是老表转移到哪了,索引从高到低转移 + private transient volatile int transferIndex; + ``` + +* 累加统计: + + ```java + // LongAdder 中的 baseCount 未发生竞争时或者当前LongAdder处于加锁状态时,增量累到到 baseCount 中 + private transient volatile long baseCount; + // LongAdder 中的 cellsBuzy,0 表示当前 LongAdder 对象无锁状态,1 表示当前 LongAdder 对象加锁状态 + private transient volatile int cellsBusy; + // LongAdder 中的 cells 数组, + private transient volatile CounterCell[] counterCells; + ``` + +* 控制变量: + + **sizeCtl** < 0: + + * -1 表示当前 table 正在初始化(有线程在创建 table 数组),当前线程需要自旋等待 + + * 其他负数表示当前 map 的 table 数组正在进行扩容,高 16 位表示扩容的标识戳;低 16 位表示 (1 + nThread) 当前参与并发扩容的线程数量 + + sizeCtl = 0,表示创建 table 数组时使用 DEFAULT_CAPACITY 为数组大小 + + sizeCtl > 0: + + * 如果 table 未初始化,表示初始化大小 + * 如果 table 已经初始化,表示下次扩容时的触发条件(阈值) + + ```java + private transient volatile int sizeCtl; + ``` + + + +*** + + + +##### 内部类 + +* Node 节点: + + ```java + static class Node implements Entry { + final int hash; + final K key; + volatile V val; + //单向链表 + volatile Node next; + } + ``` + +* TreeBin 节点: + + ```java + static final class TreeBin extends Node { + // 红黑树根节点 + TreeNode root; + // 链表的头节点 + volatile TreeNode first; + // 等待者线程 + volatile Thread waiter; + + volatile int lockState; + // 写锁状态 写锁是独占状态,以散列表来看,真正进入到 TreeBin 中的写线程同一时刻只有一个线程 + static final int WRITER = 1; + // 等待者状态(写线程在等待),当 TreeBin 中有读线程目前正在读取数据时,写线程无法修改数据 + static final int WAITER = 2; + // 读锁状态是共享,同一时刻可以有多个线程 同时进入到 TreeBi对象中获取数据,每一个线程都给 lockStat + 4 + static final int READER = 4; + } + ``` + +* TreeNode 节点: + + ```java + static final class TreeNode extends Node { + TreeNode parent; // red-black tree links + TreeNode left; + TreeNode right; + TreeNode prev; //双向链表 + boolean red; + } + ``` + +* ForwardingNode 节点: + + ```java + static final class ForwardingNode extends Node { + // 持有扩容后新的哈希表的引用 + final Node[] nextTable; + ForwardingNode(Node[] tab) { + // ForwardingNode 节点的 hash 值设为 -1 + super(MOVED, null, null, null); + this.nextTable = tab; + } + } + ``` + + + +*** + + + +##### 代码块 + +* 变量: + + ```java + // 表示sizeCtl属性在ConcurrentHashMap中内存偏移地址 + private static final long SIZECTL; + // 表示transferIndex属性在ConcurrentHashMap中内存偏移地址 + private static final long TRANSFERINDEX; + // 表示baseCount属性在ConcurrentHashMap中内存偏移地址 + private static final long BASECOUNT; + // 表示cellsBusy属性在ConcurrentHashMap中内存偏移地址 + private static final long CELLSBUSY; + // 表示cellValue属性在CounterCell中内存偏移地址 + private static final long CELLVALUE; + // 表示数组第一个元素的偏移地址 + private static final long ABASE; + // 用位移运算替代乘法 + private static final int ASHIFT; + ``` + +* 赋值方法: + + ```java + // 表示数组单元所占用空间大小,scale 表示 Node[] 数组中每一个单元所占用空间大小,int 是 4 字节 + int scale = U.arrayIndexScale(ak); + // 判断一个数是不是 2 的 n 次幂,比如 8:1000 & 0111 = 0000 + if ((scale & (scale - 1)) != 0) + throw new Error("data type scale not a power of two"); + + // numberOfLeadingZeros(n):返回当前数值转换为二进制后,从高位到低位开始统计,看有多少个0连续在一起 + // 8 → 1000 numberOfLeadingZeros(8) = 28 + // 4 → 100 numberOfLeadingZeros(4) = 29 int 值就是占4个字节 + + // ASHIFT = 31 - 29 = 2 ,int 的大小就是 2 的 2 次方 + // ABASE + (5 << ASHIFT) 用位移运算替代了乘法,看 tabAt 寻址方法 + ASHIFT = 31 - Integer.numberOfLeadingZeros(scale); + ``` + + + + + +*** + + + +#### 构造方法 + +* 无参构造, 散列表结构延迟初始化,默认的数组大小是 16: + + ```java + public ConcurrentHashMap() { + } + ``` + +* 有参构造: + + ```java + public ConcurrentHashMap(int initialCapacity) { + // 指定容量初始化 + if (initialCapacity < 0) throw new IllegalArgumentException(); + int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ? + MAXIMUM_CAPACITY : + // 假如传入的参数是 16,16 + 8 + 1 ,最后得到 32 + tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1)); + // sizeCtl > 0,当目前 table 未初始化时,sizeCtl 表示初始化容量 + this.sizeCtl = cap; + } + ``` + + ```java + private static final int tableSizeFor(int c) { + int n = c - 1; + n |= n >>> 1; + n |= n >>> 2; + n |= n >>> 4; + n |= n >>> 8; + n |= n >>> 16; + return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; + } + ``` + + HashMap 部分详解了该函数,核心思想就是**把最高位是 1 的位以及右边的位全部置 1**,结果加 1 后就是 2 的 n 次幂 + +* 多个参数构造方法: + + ```java + public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) { + if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0) + throw new IllegalArgumentException(); + + // 初始容量小于并发级别 + if (initialCapacity < concurrencyLevel) + // 把并发级别赋值给初始容量 + initialCapacity = concurrencyLevel; + // loadFactor 默认是 0.75 + long size = (long)(1.0 + (long)initialCapacity / loadFactor); + int cap = (size >= (long)MAXIMUM_CAPACITY) ? + MAXIMUM_CAPACITY : tableSizeFor((int)size); + // sizeCtl > 0,当目前 table 未初始化时,sizeCtl 表示初始化容量 + this.sizeCtl = cap; + } + ``` + +* 集合构造方法 + + ```java + public ConcurrentHashMap(Map m) { + this.sizeCtl = DEFAULT_CAPACITY; //默认16 + putAll(m); + } + public void putAll(Map m) { + //扩容为 2 倍 + tryPresize(m.size()); + for (Entry e : m.entrySet()) + putVal(e.getKey(), e.getValue(), false); + } + ``` + + + +*** + + + +#### 成员方法 + +##### 数据访存 + +* tabAt():获取数组某个槽位的头节点,类似于数组中的直接寻址 arr[i] + + ```java + // i 是数组索引 + static final Node tabAt(Node[] tab, int i) { + // (i << ASHIFT) + ABASE == ABASE + i * 4 (一个 int 占 4 个字节),这就相当于寻址,替代了乘法 + return (Node)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE); + } + ``` + +* casTabAt():指定数组索引位置修改原值为指定的值 + + ```java + static final boolean casTabAt(Node[] tab, int i, Node c, Node v) { + return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v); + } + ``` + +* setTabAt():指定数组索引位置设置值 + + ```java + static final void setTabAt(Node[] tab, int i, Node v) { + U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v); + } + ``` + + + + + +*** + + + +##### 添加方法 + +```java +public V put(K key, V value) { + //第三个参数 onlyIfAbsent 为 false 表示哈希表中存在相同的 key 时用当前数据覆盖旧数据 + return putVal(key, value, false); +} +``` + +* putVal() + + ```java + final V putVal(K key, V value, boolean onlyIfAbsent) { + // ConcurrentHashMap 不能存放 null 值 + if (key == null || value == null) throw new NullPointerException(); + // 扰动运算,高低位都参与寻址运算。 + int hash = spread(key.hashCode()); + // 表示当前 k-v 封装成 node 后插入到指定桶位后,在桶位中的所属链表的下标位置 + // 0 表示当前桶位为 null,node 可以直接放入,2 表示当前桶位已经是红黑树 + int binCount = 0; + // tab 引用当前 map 的数组 table,开始自旋 + for (Node[] tab = table;;) { + // f 表示桶位的头节点,n 表示哈希表数组的长度 + // i 表示 key 通过寻址计算后得到的桶位下标,fh 表示桶位头结点的 hash 值 + Node f; int n, i, fh; + + // 【CASE1】:条件成立表示当前 map 中的 table 尚未初始化 + if (tab == null || (n = tab.length) == 0) + //【延迟初始化】 + tab = initTable(); + + // 【CASE2】:i 表示 key 使用寻址算法得到 key 对应数组的下标位置,tabAt 获取指定桶位的头结点 f + else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { + // 对应的数组为 null 说明没有哈希冲突,直接添加到表中 + if (casTabAt(tab, i, null, + new Node(hash, key, value, null))) + break; + } + // 【CASE3】:走到这里说明数组已经被初始化,并且当前 key 对应的位置不为null + // 条件成立表示当前桶位的头结点为 FWD 结点,表示目前 map 正处于扩容过程中 + else if ((fh = f.hash) == MOVED) + // 当前线程需要去帮助哈希表完成扩容 + tab = helpTransfer(tab, f); + + // 【CASE4】:哈希表没有在扩容,当前桶位可能是链表也可能是红黑树 + else { + // 当插入 key 存在时,会将旧值赋值给 oldVal 返回 + V oldVal = null; + // 锁住当前 key 寻址的桶位的头节点,当前逻辑是前面的 if else 语句判断后到达的,所以这里的 f 有值 + synchronized (f) { + // 这里重新获取一下桶的头节点有没有被修改,因为期间可能被其他线程修改过 + if (tabAt(tab, i) == f) { + // 头节点的哈希值大于 0 说明当前桶位是普通的链表节点 + if (fh >= 0) { + // 当前的插入操作没出现重复的 key,追加到链表的末尾,binCount表示链表长度 -1 + // 插入的key与链表中的某个元素的 key 一致,变成替换操作,binCount表示第几个节点冲突 + binCount = 1; + // 迭代循环当前桶位的链表,e 是每次循环处理节点,e 初始是头节点 + for (Node e = f;; ++binCount) { + // 当前循环节点 key + K ek; + // key 的哈希值与当前节点的哈希一致,并且 key 的值也相同 + if (e.hash == hash && + ((ek = e.key) == key || + (ek != null && key.equals(ek)))) { + // 把当前节点的 value 赋值给 oldVal + oldVal = e.val; + // 允许覆盖 + if (!onlyIfAbsent) + // 新数据覆盖旧数据 + e.val = value; + // 跳出循环 + break; + } + Node pred = e; + // 如果下一个节点为空,把数据封装成节点插入链表尾部,binCount 代表长度 - 1 + if ((e = e.next) == null) { + pred.next = new Node(hash, key, + value, null); + break; + } + } + } + // 当前桶位头节点是红黑树 + else if (f instanceof TreeBin) { + Node p; + binCount = 2; + if ((p = ((TreeBin)f).putTreeVal(hash, key, + value)) != null) { + oldVal = p.val; + if (!onlyIfAbsent) + p.val = value; + } + } + } + } + + // 条件成立说明当前是链表或者红黑树 + if (binCount != 0) { + // 如果 binCount>=8 表示处理的桶位一定是链表,说明长度是9 + if (binCount >= TREEIFY_THRESHOLD) + // 树化 + treeifyBin(tab, i); + if (oldVal != null) + return oldVal; + break; + } + } + } + // 统计当前 table 一共有多少数据 + // 判断是否达到扩容阈值标准,触发扩容 + addCount(1L, binCount); + return null; + } + ``` + +* spread():扰动函数 + + 将 hashCode 无符号右移 16 位,高 16bit 和低 16bit 做异或,最后与 HASH_BITS 相与变成正数,把高低位都利用起来减少哈希冲突,保证散列的均匀性 + + ```java + static final int spread(int h) { + return (h ^ (h >>> 16)) & HASH_BITS; // 0111 1111 1111 1111 1111 1111 1111 1111 + } + ``` + +* initTable():初始化数组,延迟初始化 + + ```java + private final Node[] initTable() { + // tab 引用 map.table,sc 引用 sizeCtl + Node[] tab; int sc; + // table 尚未初始化,开始自旋 + while ((tab = table) == null || tab.length == 0) { + // sc < 0 说明 table 正在初始化或者正在扩容,当前线程释放 CPU 资源 + if ((sc = sizeCtl) < 0) + Thread.yield(); + // sizeCtl 设置为-1,相当于加锁,设置的是 SIZECTL 位置的数据,不是 sc 的数据,sc 只起到对比作用 + else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { + try { + // 线程安全的逻辑,再进行一次判断 + if ((tab = table) == null || tab.length == 0) { + // sc > 0 创建 table 时使用 sc 为指定大小,否则使用 16 默认值. + int n = (sc > 0) ? sc : DEFAULT_CAPACITY; + // 创建哈希表数组 + Node[] nt = (Node[])new Node[n]; + table = tab = nt; + // n >>> 2 => 等于 1/4 n n - (1/4)n = 3/4 n => 0.75 * n + sc = n - (n >>> 2); + } + } finally { + // 解锁,把下一次扩容的阈值赋值给 sizeCtl + sizeCtl = sc; + } + break; + } + } + return tab; + } + ``` + +* treeifyBin():树化方法 + + ```java + private final void treeifyBin(Node[] tab, int index) { + Node b; int n, sc; + if (tab != null) { + //条件成立:说明当前table数组长度 未达到 64,此时不进行树化操作,进行扩容操作。 + if ((n = tab.length) < MIN_TREEIFY_CAPACITY) + // 当前容量的 2 倍 + tryPresize(n << 1); + + //条件成立:说明当前桶位有数据,且是普通 node 数据。 + else if ((b = tabAt(tab, index)) != null && b.hash >= 0) { + synchronized (b) { + //条件成立:表示加锁没问题。 + if (tabAt(tab, index) == b) { + TreeNode hd = null, tl = null; + for (Node e = b; e != null; e = e.next) { + TreeNode p = + new TreeNode(e.hash, e.key, e.val, + null, null); + if ((p.prev = tl) == null) + hd = p; + else + tl.next = p; + tl = p; + } + setTabAt(tab, index, new TreeBin(hd)); + } + } + } + } + } + ``` + + tryPresize(int size):尝试触发扩容 + + ```java + private final void tryPresize(int size) { + // 又扩大一次,4倍 + int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : + tableSizeFor(size + (size >>> 1) + 1); + int sc; + while ((sc = sizeCtl) >= 0) { + Node[] tab = table; int n; + //数组还未初始化,一般是调用集合构造方法才会成立,put 后调用该方法都是不成立的 + if (tab == null || (n = tab.length) == 0) { + n = (sc > c) ? sc : c; + if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { + try { + if (table == tab) { + Node[] nt = (Node[])new Node[n]; + table = nt; + sc = n - (n >>> 2);//扩容阈值:n - 1/4 n + } + } finally { + sizeCtl = sc; //扩容阈值赋值给sizeCtl + } + } + } + // 未达到扩容阈值或者数组长度已经大于最大长度 + else if (c <= sc || n >= MAXIMUM_CAPACITY) + break; + else if (tab == table) // 与 addCount 逻辑相同 + + } + } + ``` + +* addCount():添加计数,代表哈希表中的数据总量 + + ```java + private final void addCount(long x, int check) { + // as 表示 LongAdder.cells,b 表示 LongAdder.base,s 表示当前 map.table 中元素的数量 + CounterCell[] as; long b, s; + // 判断累加数组 cells 是否初始化,没有就去累加 base 域,累加失败进入条件内逻辑 + if ((as = counterCells) != null || + !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) { + // a 表示当前线程 hash 寻址命中的cell,v 表示当前线程写cell时的期望值,m 表示当前cells数组的长度 + CounterCell a; long v; int m; + // true -> 未竞争,false -> 发生竞争 + boolean uncontended = true; + // 判断 cells 是否被其他线程初始化 + if (as == null || (m = as.length - 1) < 0 || + // 前面的条件为 fasle 说明cells 被其他线程初始化,通过 hash 寻址对应的槽位 + (a = as[ThreadLocalRandom.getProbe() & m]) == null || + // 尝试去对应的槽位累加,累加失败进入 fullAddCount 进行重试或者扩容 + !(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) { + // 与 Striped64#longAccumulate 方法相同 + fullAddCount(x, uncontended); + return; + } + if (check <= 1) // 表示当前桶位是 null,或者一个链表节点 + return; + s = sumCount(); // 获取当前散列表元素个数,这是一个期望值 + } + //表示一定 【是一个 put 操作调用的 addCount】 + if (check >= 0) { + // tab 表示map.table,nt 表示map.nextTable,n 表示map.table数组的长度,sc 表示sizeCtl的临时值 + Node[] tab, nt; int n, sc; + // 条件一:true 说明当前 sizeCtl 为一个负数,表示正在扩容中,或者 sizeCtl 是一个正数,表示扩容阈值 + // false 表示哈希表的数据的数量没达到扩容条件 + // 条件二:条件一为 true 进入条件二,判断当前 table 数组是否初始化了 + // 条件三:true->当前 table 长度小于最大值限制,则可以进行扩容 + while (s >= (long)(sc = sizeCtl) && (tab = table) != null && + (n = tab.length) < MAXIMUM_CAPACITY) { + // 扩容批次唯一标识戳 + // 16 -> 32 扩容 标识为:1000 0000 0001 1011,负数 + int rs = resizeStamp(n); + // 条件成立说明表示当前 table,【正在扩容】,sc 高 16 位是扩容标识戳,低 16 位是线程数 + 1 + if (sc < 0) { + // 条件一:判断扩容标识戳是否一样,fasle 代表一样 + // 条件二是:sc == (rs << 16 ) + 1,true 代表扩容完成,因为低 16 位是1代表没有线程扩容了 + // 条件三是:sc == (rs << 16) + MAX_RESIZERS,判断是否已经超过最大允许的并发扩容线程数 + // 条件四:判断新表是否是 null,代表扩容完成 + // 条件五:扩容是从高位到低位扩容,transferIndex < 0 说明没有区间需要扩容了 + if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || + sc == rs + MAX_RESIZERS || (nt = nextTable) == null || + transferIndex <= 0) + break; + // 条件成立表示当前线程成功参与到扩容任务中,并且将 sc 低 16 位值加 1,表示多一个线程参与扩容 + // 条件失败说明其他线程或者 transfer 内部修改了 sizeCtl 值 + if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) + //【协助扩容线程】,持有nextTable参数 + transfer(tab, nt); + } + // 条件成立表示当前线程是触发扩容的第一个线程 + // 1000 0000 0001 1011 0000 0000 0000 0000 +2 => 1000 0000 0001 1011 0000 0000 0000 0010 + else if (U.compareAndSwapInt(this, SIZECTL, sc,(rs << RESIZE_STAMP_SHIFT) + 2)) + //【触发扩容条件的线程】,不持有 nextTable,自己新建 nextTable + transfer(tab, null); + s = sumCount(); + } + } + } + ``` + +* resizeStamp():扩容标识符,每次扩容都会产生一个,不是每个线程都产生,16 扩容到 32 产生一个,32 扩容到 64 产生一个 + + ```java + /** + * 扩容的标识符 + * 16 -> 32 从16扩容到32 + * numberOfLeadingZeros(16) => 1 0000 =>27 =>0000 0000 0001 1011 + * (1 << (RESIZE_STAMP_BITS - 1)) => 1000 0000 0000 0000 => 32768 + * --------------------------------------------------------------- + * 0000 0000 0001 1011 + * 1000 0000 0000 0000 + * 1000 0000 0001 1011 + * 永远是负数 + */ + static final int resizeStamp(int n) { + // 或运算 + return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));//(16 -1 = 15) + } + ``` + + + + + +*** + + + +##### 扩容方法 + +扩容机制: + +* 当链表中元素个数超过 8 个,数组的大小还未超过 64 时,此时进行数组的扩容,如果超过则将链表转化成红黑树 +* put 数据后调用 addCount() 方法,判断当前哈希表的容量超过阈值 sizeCtl,超过进行扩容 +* 发现其他线程正在扩容,帮其扩容 + +常见方法: + +* transfer():数据转移到新表中,完成扩容 + + ```java + private final void transfer(Node[] tab, Node[] nextTab) { + // n 表示扩容之前 table 数组的长度 + int n = tab.length, stride; + //stride 表示分配给线程任务的步长,认为是 16 + if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) + stride = MIN_TRANSFER_STRIDE; + // 条件成立表示当前线程为触发本次扩容的线程,需要做一些扩容准备工作,【协助线程不做这一步】 + if (nextTab == null) { + try { + // 创建一个容量为之前二倍的 table 数组 + Node[] nt = (Node[])new Node[n << 1]; + nextTab = nt; + } catch (Throwable ex) { + sizeCtl = Integer.MAX_VALUE; + return; + } + // 赋值给对象属性 nextTable,方便其他线程获取新表 + nextTable = nextTab; + // 记录迁移数据整体位置的一个标记,transferIndex计数从1开始不是 0,所以这里是长度,不是长度-1 + transferIndex = n; + } + // 新数组的长度 + int nextn = nextTab.length; + // 当某个桶位数据处理完毕后,将此桶位设置为 fwd 节点,其它写线程或读线程看到后,可以从中获取到新表 + ForwardingNode fwd = new ForwardingNode(nextTab); + // 推进标记 + boolean advance = true; + // 完成标记 + boolean finishing = false; + + // i 表示分配给当前线程任务,执行到的桶位 + // bound 表示分配给当前线程任务的下界限制,因为是倒序迁移,16 迁移完 迁移 15,15完成去迁移14 + for (int i = 0, bound = 0;;) { + // f 桶位的头结点,fh 是头节点的哈希值 + Node f; int fh; + + //给当前线程【分配任务区间】 + while (advance) { + //分配任务的开始下标,分配任务的结束下标 + int nextIndex, nextBound; + // 条件一:true 说明当前的迁移任务尚未完成,--i 就让当前线程处理下一个 桶位 + // false 说明线程已经完成或者还未分配 + if (--i >= bound || finishing) + advance = false; + // 迁移的开始下标,小于0说明没有区间需要迁移了,设置当前线程的 i 变量为 -1 跳出循环 + else if ((nextIndex = transferIndex) <= 0) { + i = -1; + advance = false; + } + // 走到这里说明还有区间需要分配 + // 条件成立表示给当前线程分配任务成功,上一个线程结束的下标就是这个线程开始的下标 + else if (U.compareAndSwapInt + (this, TRANSFERINDEX, nextIndex, + //判断剩余的区间是否还够 一个步长的范围,,不够就全部分配 + nextBound = (nextIndex > stride ? + nextIndex - stride : 0))) { + // 当前线程的结束下标 + bound = nextBound; + // 当前线程的开始下标 + i = nextIndex - 1; + //任务分配结束,跳出循环执行迁移操作 + advance = false; + } + } + + // 【数据迁移操作】 + // 【CASE1】:i < 0 成立表示当前线程未分配到任务 + if (i < 0 || i >= n || i + n >= nextn) { + // 保存 sizeCtl 的变量 + int sc; + // 如果迁移完成 + if (finishing) { + nextTable = null; // help GC + table = nextTab; // 新表赋值给当前对象 + sizeCtl = (n << 1) - (n >>> 1);// 扩容阈值为 2n - n/2 = 3n/2 = 0.75*(2n) + return; + } + // 当前线程完成了分配的任务区间,可以退出,先把 sizeCtl 赋值给 sc 保留 + if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) { + // 判断当前线程是不是最后一个线程,不是的话直接 return, + if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT) + return; + // 所以最后一个线程退出的时候,sizeCtl 的低 16 位为 1 + finishing = advance = true; + // 这里表示最后一个线程需要重新检查一遍是否有漏掉的 + i = n; + } + } + + // 【CASE2】:说明当前桶位未存放数据,只需要将此处设置为 fwd 节点即可。 + else if ((f = tabAt(tab, i)) == null) + advance = casTabAt(tab, i, null, fwd); + // 【CASE3】:说明当前桶位已经迁移过了,当前线程不用再处理了,直接处理下一个桶位即可 + else if ((fh = f.hash) == MOVED) + advance = true; + // 【CASE4】:当前桶位有数据,而且 node 节点不是 fwd 节点,说明这些数据需要迁移 + else { + // 锁住头节点 + synchronized (f) { + // 二次检查,防止头节点已经被修改了,因为这里才是线程安全的访问 + if (tabAt(tab, i) == f) { + // ln 表示低位链表引用 + // hn 表示高位链表引用 + Node ln, hn; + // 条件成立:表示当前桶位是链表桶位 + if (fh >= 0) { + // 和 HashMap 的处理方式一致,与老数组长度相与,16 是 10000 + // 判断对应的 1 的位置上是 0 或 1 分成高低位链表 + int runBit = fh & n; + Node lastRun = f; + //遍历链表,获取逆序看最长的对应位相同的链表,看下面的图更好的理解 + for (Node p = f.next; p != null; p = p.next) { + // 将当前节点的哈希 与 n + int b = p.hash & n; + // 如果当前值与前面节点的值 对应位 不同,则修改 runBit,把 lastRun 指向当前节点 + if (b != runBit) { + runBit = b; + lastRun = p; + } + } + // 成立说明筛选出的链表是低位的 + if (runBit == 0) { + ln = lastRun; // ln 指向该链表 + hn = null; // hn 为 null + } + // 否则,说明 lastRun 引用的链表为高位链表,就让 hn 指向 高位链表 + else { + hn = lastRun; + ln = null; + } + // 从头开始遍历所有的链表节点,迭代到 p == lastRun 节点跳出循环 + for (Node p = f; p != lastRun; p = p.next) { + int ph = p.hash; K pk = p.key; V pv = p.val; + if ((ph & n) == 0) + // ln 是上一个节点的下一个节点的引用【头插法】 + ln = new Node(ph, pk, pv, ln); + else + hn = new Node(ph, pk, pv, hn); + } + // 高低位链设置到新表中的指定位置 + setTabAt(nextTab, i, ln); + setTabAt(nextTab, i + n, hn); + // 老表中的该桶位设置为 fwd 节点 + setTabAt(tab, i, fwd); + advance = true; + } + // 条件成立:表示当前桶位是 红黑树结点 + else if (f instanceof TreeBin) { + TreeBin t = (TreeBin)f; + TreeNode lo = null, loTail = null; + TreeNode hi = null, hiTail = null; + int lc = 0, hc = 0; + // 迭代 TreeBin 中的双向链表,从头结点至尾节点 + for (Node e = t.first; e != null; e = e.next) { + // 迭代的当前元素的 hash + int h = e.hash; + TreeNode p = new TreeNode + (h, e.key, e.val, null, null); + // 条件成立表示当前循环节点属于低位链节点 + if ((h & n) == 0) { + if ((p.prev = loTail) == null) + lo = p; + else + //【尾插法】 + loTail.next = p; + // loTail 指向尾节点 + loTail = p; + ++lc; + } + else { + if ((p.prev = hiTail) == null) + hi = p; + else + hiTail.next = p; + hiTail = p; + ++hc; + } + } + // 拆成的高位低位两个链,判断是否需要需要转化为链表,反之保持树化 + ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) : + (hc != 0) ? new TreeBin(lo) : t; + hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) : + (lc != 0) ? new TreeBin(hi) : t; + setTabAt(nextTab, i, ln); + setTabAt(nextTab, i + n, hn); + setTabAt(tab, i, fwd); + advance = true; + } + } + } + } + } + } + ``` + + 链表处理的 LastRun 机制: + + ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentHashMap-LastRun机制.png) + +* helpTransfer():帮助扩容 + + ```java + final Node[] helpTransfer(Node[] tab, Node f) { + Node[] nextTab; int sc; + // 数组不为空,节点是转发节点,获取转发节点指向的新表开始协助主线程扩容 + if (tab != null && (f instanceof ForwardingNode) && + (nextTab = ((ForwardingNode)f).nextTable) != null) { + int rs = resizeStamp(tab.length); + // 判断数据迁移是否完成,迁移完成会把 新表赋值给 nextTable 属性 + while (nextTab == nextTable && table == tab && + (sc = sizeCtl) < 0) { + if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || + sc == rs + MAX_RESIZERS || transferIndex <= 0) + break; + if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) { + // 协助扩容 + transfer(tab, nextTab); + break; + } + } + return nextTab; + } + return table; + } + ``` + + + + + +*** + + + +##### 获取方法 + +ConcurrentHashMap 使用 get() 方法获取指定 key 的数据 + +* get():获取指定数据的方法 + + ```java + public V get(Object key) { + // tab 引用哈希表的数组,e 是当前元素,p 是目标节点,n 是数组长度,eh 是当前元素哈希,k 是当前节点的值 + Node[] tab; Node e, p; int n, eh; K ek; + // 扰动运算 + int h = spread(key.hashCode()); + // 判断当前哈希表的数组是否初始化 + if ((tab = table) != null && (n = tab.length) > 0 && + // 如果 table 已经初始化,进行【哈希寻址】,映射到数组对应索引处,获取该索引处的头节点 + (e = tabAt(tab, (n - 1) & h)) != null) { + //对比头结点 hash 与查询 key 的 hash 是否一致 + if ((eh = e.hash) == h) { + // 进行值的判断,如果成功就说明当前节点就是要查询的节点,直接返回 + if ((ek = e.key) == key || (ek != null && key.equals(ek))) + return e.val; + } + // 哈希值小于0说明是红黑树节点或者是正在扩容的 fwd 节点 + else if (eh < 0) + return (p = e.find(h, key)) != null ? p.val : null; + // 当前桶位已经形成链表,迭代查找 + while ((e = e.next) != null) { + if (e.hash == h && + ((ek = e.key) == key || (ek != null && key.equals(ek)))) + return e.val; + } + } + return null; + } + ``` + +* ForwardingNode#find + + ```java + Node find(int h, Object k) { + // 获取新表的引用 + outer: for (Node[] tab = nextTable;;) { + //e 表示在扩容而创建新表使用寻址算法得到的桶位头结点,n 表示为扩容而创建的新表的长度 + Node e; int n; + + //条件四:在新扩容表中 重新定位 hash 对应的头结点 + // 条件成立说明在 oldTable 中对应的桶位在迁移之前就是 null + if (k == null || tab == null || (n = tab.length) == 0 || + (e = tabAt(tab, (n - 1) & h)) == null) + return null; + + for (;;) { + //eh 新扩容后表指定桶位的当前节点的hash,ek 新扩容后表指定桶位的当前节点的key + int eh; K ek; + //条件成立说明新表当前命中桶位中的数据,即为查询想要数据。 + if ((eh = e.hash) == h && + ((ek = e.key) == k || (ek != null && k.equals(ek)))) + return e; + + // eh < 0 说明当前头节点 TreeBin 类型,或者是 FWD 类型, + // 在并发很大的情况下新扩容的表还没完成可能【再次扩容】,在此方法处再次拿到 FWD 类型 + if (eh < 0) { + if (e instanceof ForwardingNode) { + // 继续获取新的 fwd 指向的新数组的地址 + tab = ((ForwardingNode)e).nextTable; + continue outer; + } + else + //说明此桶位 为 TreeBin 节点,使用TreeBin.find 查找红黑树中相应节点。 + return e.find(h, k); + } + + // 逻辑到这说明当前桶位头结点 并没有命中查询,说明此桶位是链表 + // 将当前元素指向链表的下一个元素,判断当前元素的下一个位置是否为空 + if ((e = e.next) == null) + //条件成立说明迭代到链表末尾,未找到对应的数据,返回 你ull + return null; + } + } + } + ``` + + + + + +**** + + + +##### 删除方法 + +* remove():删除指定元素 + + ```java + public V remove(Object key) { + return replaceNode(key, null, null); + } + ``` + +* replaceNode():替代指定的元素 + + ```java + final V replaceNode(Object key, V value, Object cv) { + // 计算 key 扰动运算后的 hash + int hash = spread(key.hashCode()); + // 自旋 + for (Node[] tab = table;;) { + // f 表示桶位的头节点,n 表示当前 table 数组长度,i 表示 hash 映射的数组下标,fh 表示头节点的哈希值 + Node f; int n, i, fh; + //【CASE1】:table 还未初始化或者哈希寻址的数组索引处为 null,直接结束自旋,返回 null + if (tab == null || (n = tab.length) == 0 || + (f = tabAt(tab, i = (n - 1) & hash)) == null) + break; + // 【CASE2】:条件成立说明当前 table 正在扩容,当前是个写操作,所以当前线程需要协助 table 完成扩容 + else if ((fh = f.hash) == MOVED) + tab = helpTransfer(tab, f); + // 【CASE3】:当前桶位可能是 链表 也可能是 红黑树 + else { + // 保留替换之前数据引用 + V oldVal = null; + // 校验标记 + boolean validated = false; + // 加锁当前桶位头结点,加锁成功之后会进入代码块 + synchronized (f) { + // 双重检查 + if (tabAt(tab, i) == f) { + // 说明当前节点是链表节点 + if (fh >= 0) { + validated = true; + //遍历所有的节点 + for (Node e = f, pred = null;;) { + K ek; + // hash 和值都相同,定位到了具体的节点 + if (e.hash == hash && + ((ek = e.key) == key || + (ek != null && key.equals(ek)))) { + // 当前节点的value + V ev = e.val; + if (cv == null || cv == ev || + (ev != null && cv.equals(ev))) { + // 将当前节点的值 赋值给 oldVal 后续返回会用到 + oldVal = ev; + if (value != null) //条件成立说明是替换操作 + e.val = value; + else if (pred != null) //非头节点删除操作,断开链表 + pred.next = e.next; + else + //说明当前节点即为头结点,将桶位头节点设置为以前头节点的下一个节点 + setTabAt(tab, i, e.next); + } + break; + } + pred = e; + if ((e = e.next) == null) + break; + } + } + // 说明是红黑树节点 + else if (f instanceof TreeBin) { + validated = true; + TreeBin t = (TreeBin)f; + TreeNode r, p; + if ((r = t.root) != null && + (p = r.findTreeNode(hash, key, null)) != null) { + V pv = p.val; + if (cv == null || cv == pv || + (pv != null && cv.equals(pv))) { + oldVal = pv; + // 条件成立说明替换操作 + if (value != null) + p.val = value; + // 删除操作 + else if (t.removeTreeNode(p)) + setTabAt(tab, i, untreeify(t.first)); + } + } + } + } + } + //其他线程修改过桶位头结点时,当前线程 sync 头结点锁错对象,validated 为 false,会进入下次 for 自旋 + if (validated) { + if (oldVal != null) { + //替换的值为 null,说明当前是一次删除操作,更新当前元素个数计数器 + if (value == null) + addCount(-1L, -1); + return oldVal; + } + break; + } + } + } + return null; + } + ``` + + + + + +*** + + + +#### JDK7原理 + +ConcurrentHashMap 对锁粒度进行了优化,**分段锁技术**,将整张表分成了多个数组(Segment),每个数组又是一个类似 HashMap 数组的结构。允许多个修改操作并发进行,Segment 是一种可重入锁,继承 ReentrantLock,并发时锁住的是每个 Segment,其他 Segment 还是可以操作的,这样不同 Segment 之间就可以实现并发,大大提高效率。 + +底层结构: **Segment 数组 + HashEntry 数组 + 链表**(数组+链表是HashMap的结构) + +* 优点:如果多个线程访问不同的 segment,实际是没有冲突的,这与 jdk8 中是类似的 + +* 缺点:Segments 数组默认大小为16,这个容量初始化指定后就不能改变了,并且不是懒惰初始化 + + ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentHashMap 1.7底层结构.png) + + @@ -8407,23 +9625,23 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引指向 #### 非阻塞队列 -并发编程中,需要用到安全的队列,实现安全队列可以使用2种方式: +并发编程中,需要用到安全的队列,实现安全队列可以使用 2 种方式: * 加锁,这种实现方式是阻塞队列 -* 使用循环CAS算法实现,这种方式是非阻塞队列 +* 使用循环 CAS 算法实现,这种方式是非阻塞队列 -ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,采用先进先出的规则对节点进行排序,当添加一个元素时,会添加到队列的尾部,当获取一个元素时,会返回队列头部的元素 +ConcurrentLinkedQueue 是一个基于链接节点的无界线程安全队列,采用先进先出的规则对节点进行排序,当添加一个元素时,会添加到队列的尾部,当获取一个元素时,会返回队列头部的元素 补充:ConcurrentLinkedDeque 是双向链表结构的无界并发队列 -ConcurrentLinkedQueue使用约定: +ConcurrentLinkedQueue 使用约定: -1. 不允许null入列 -2. 队列中所有未删除的节点的item都不能为null且都能从head节点遍历到 -3. 删除节点是将item设置为null,队列迭代时跳过item为null节点 -4. head节点跟tail不一定指向头节点或尾节点,可能存在滞后性 +1. 不允许 null 入列 +2. 队列中所有未删除的节点的 item 都不能为 null 且都能从 head 节点遍历到 +3. 删除节点是将 item 设置为 null,队列迭代时跳过 item 为 null 节点 +4. head 节点跟 tail 不一定指向头节点或尾节点,可能存在滞后性 -ConcurrentLinkedQueue由head节点和tail节点组成,每个节点(Node)由节点元素(item)和指向下一个节点的引用(next)组成,组成一张链表结构的队列 +ConcurrentLinkedQueue 由 head 节点和 tail 节点组成,每个节点(Node)由节点元素(item)和指向下一个节点的引用(next)组成,组成一张链表结构的队列 ```java private transient volatile Node head; @@ -8488,7 +9706,7 @@ private static class Node { 与传统的链表不同,单线程入队的工作流程: * 将入队节点设置成当前队列尾节点的下一个节点 -* 更新tail节点,如果tail节点的next节点不为空,则将入队节点设置成tail节点;如果tail节点的next节点为空,则将入队节点设置成tail的next节点,所以tail节点不总是尾节点,**存在滞后性** +* 更新 tail 节点,如果 tail 节点的 next 节点不为空,则将入队节点设置成 tail 节点;如果 tail 节点的 next 节点为空,则将入队节点设置成 tail 的 next 节点,所以 tail 节点不总是尾节点,**存在滞后性** ```java public boolean offer(E e) { @@ -8532,13 +9750,13 @@ public boolean offer(E e) { ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentLinkedQueue入队操作3.png) -当tail节点和尾节点的距离**大于等于1**时(每入队两次)更新tail,可以减少CAS更新tail节点的次数,提高入队效率 +当 tail 节点和尾节点的距离**大于等于 1** 时(每入队两次)更新 tail,可以减少 CAS 更新 tail 节点的次数,提高入队效率 线程安全问题: -* 线程1线程2同时入队,无论从哪个位置开始并发入队,都可以循环CAS,直到入队成功,线程安全 -* 线程1遍历,线程2入队,所以造成 ConcurrentLinkedQueue 的size是变化,需要加锁保证安全 -* 线程1线程2同时出列,线程也是安全的 +* 线程 1 线程 2 同时入队,无论从哪个位置开始并发入队,都可以循环 CAS,直到入队成功,线程安全 +* 线程 1 遍历,线程 2 入队,所以造成 ConcurrentLinkedQueue 的 size 是变化,需要加锁保证安全 +* 线程 1 线程 2 同时出列,线程也是安全的 @@ -8548,12 +9766,12 @@ public boolean offer(E e) { #### 出队方法 -出队列的就是从队列里返回一个节点元素,并清空该节点对元素的引用,并不是每次出队都更新head节点 +出队列的就是从队列里返回一个节点元素,并清空该节点对元素的引用,并不是每次出队都更新 head 节点 -* 当head节点里有元素时,直接弹出head节点里的元素,而不会更新head节点 -* 当head节点里没有元素时,出队操作才会更新head节点 +* 当 head 节点里有元素时,直接弹出 head 节点里的元素,而不会更新 head 节点 +* 当 head 节点里没有元素时,出队操作才会更新 head 节点 -**批处理方式**可以减少使用CAS更新head节点的消耗,从而提高出队效率 +**批处理方式**可以减少使用 CAS 更新 head 节点的消耗,从而提高出队效率 ```java public E poll() { @@ -8591,7 +9809,7 @@ final void updateHead(Node h, Node p) { } ``` -在更新完head之后,会将旧的头结点h的next域指向为h,图中所示的虚线也就表示这个节点的自引用,被移动的节点(item为null的节点)会被GC回收 +在更新完 head 之后,会将旧的头结点 h 的 next 域指向为 h,图中所示的虚线也就表示这个节点的自引用,被移动的节点(item 为 null 的节点)会被 GC 回收 ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentLinkedQueue出队操作1.png) @@ -8599,7 +9817,7 @@ final void updateHead(Node h, Node p) { -如果这时,有一个线程来添加元素,通过tail获取的next节点则仍然是它本身,这就出现了**p == q** 的情况,出现该种情况之后,则会触发执行head的更新,将p节点重新指向为head +如果这时,有一个线程来添加元素,通过 tail 获取的 next 节点则仍然是它本身,这就出现了**p == q** 的情况,出现该种情况之后,则会触发执行 head 的更新,将 p 节点重新指向为 head 参考文章:https://www.jianshu.com/p/231caf90f30b @@ -8613,7 +9831,7 @@ final void updateHead(Node h, Node p) { * peek() - peek操作会改变head指向,执行peek()方法后head会指向第一个具有非空元素的节点 + peek 操作会改变 head 指向,执行 peek() 方法后 head 会指向第一个具有非空元素的节点 ```java // 获取链表的首部元素,只读取而不移除 @@ -8638,7 +9856,7 @@ final void updateHead(Node h, Node p) { * size() - 用来获取当前队列的元素个数,因为整个过程都没有加锁,在并发环境中从调用size方法到返回结果期间有可能增删元素,导致统计的元素个数不精确 + 用来获取当前队列的元素个数,因为整个过程都没有加锁,在并发环境中从调用 size 方法到返回结果期间有可能增删元素,导致统计的元素个数不精确 ```java public int size() { @@ -8712,8 +9930,8 @@ final void updateHead(Node h, Node p) { 通信一定是基于软件结构实现的: -* C/S 结构 :全称为Client/Server结构,是指客户端和服务器结构,常见程序有QQ、IDEA等软件。 -* B/S 结构 :全称为Browser/Server结构,是指浏览器和服务器结构。 +* C/S 结构 :全称为 Client/Server 结构,是指客户端和服务器结构,常见程序有 QQ、IDEA等软件。 +* B/S 结构 :全称为 Browser/Server 结构,是指浏览器和服务器结构。 两种架构各有优势,但是无论哪种架构,都离不开网络的支持。 @@ -8724,16 +9942,16 @@ final void updateHead(Node h, Node p) { 2. IP地址:互联网协议地址(Internet Protocol Address),用来给一个网络中的计算机设备做唯一的编号 * IPv4 :4个字节,32位组成,192.168.1.1 - * Pv6:可以实现为所有设备分配IP 128位 + * Pv6:可以实现为所有设备分配 IP 128 位 - * ipconfig:查看本机的IP - ​ ping 检查本机与某个IP指定的机器是否联通,或者说是检测对方是否在线。 + * ipconfig:查看本机的 IP + ​ ping 检查本机与某个 IP 指定的机器是否联通,或者说是检测对方是否在线。 ​ ping 空格 IP地址 :ping 220.181.57.216,ping www.baidu.com 特殊的IP地址: 本机IP地址,**127.0.0.1 == localhost**,回环测试 3. 端口:端口号就可以唯一标识设备中的进程(应用程序) - 端口号:用两个字节表示的整数,的取值范围是0-65535,0-1023之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用1024以上的端口号。如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败,报出端口被占用异常 + 端口号:用两个字节表示的整数,的取值范围是 0-65535,0-1023 之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用 1024 以上的端口号。如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败,报出端口被占用异常 利用**协议+IP地址+端口号** 三元组合,就可以标识网络中的进程了,那么进程间的通信就可以利用这个标识与其它进程进行交互。 From be48234f7e3a07a887e5cdfb6b670cf0ccc8b41f Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 11 Aug 2021 23:53:21 +0800 Subject: [PATCH 018/168] Update Java Notes --- Java.md | 14 +- Prog.md | 971 +++++++++++++++++++++++++++++++++++++++++++++++++------- Tool.md | 168 +++++----- 3 files changed, 950 insertions(+), 203 deletions(-) diff --git a/Java.md b/Java.md index af28bc1..51df33e 100644 --- a/Java.md +++ b/Java.md @@ -2327,12 +2327,12 @@ hashCode 的作用: * 浅拷贝 (shallowCopy):对基本数据类型进行值传递,对引用数据类型只是复制了引用,被复制对象属性的所有的引用仍然指向原来的对象,简而言之就是增加了一个指针指向原来对象的内存地址 + Java 中的复制方法基本都是浅克隆:Object.clone()、System.arraycopy()、Arrays.copyOf() + * 深拷贝 (deepCopy):对基本数据类型进行值传递,对引用数据类型是一个整个独立的对象拷贝,会拷贝所有的属性并指向的动态分配的内存,简而言之就是把所有属性复制到一个新的内存,增加一个指针指向新内存。所以使用深拷贝的情况下,释放内存的时候不会出现使用浅拷贝时释放同一块内存的错误 Object 的 clone() 是 protected 方法,一个类不显式去重写 clone(),就不能直接去调用该类实例的 clone() 方法 -克隆就是制造一个对象的副本,根据所要克隆的对象的成员变量中是否含有引用类型,可以将克隆分为两种:浅克隆(Shallow Clone) 和 深克隆(Deep Clone),默认情况下使用Object中的clone方法进行克隆就是浅克隆,即完成对象域对域的拷贝 - Cloneable 接口是一个标识性接口,即该接口不包含任何方法(包括clone()),但是如果一个类想合法的进行克隆,那么就必须实现这个接口,在使用 clone() 方法时,若该类未实现 Cloneable 接口,则抛出异常 * Clone & Copy:`Student s = new Student` @@ -2343,7 +2343,7 @@ Cloneable 接口是一个标识性接口,即该接口不包含任何方法( * Shallow Clone & Deep Clone: - 浅克隆:Object中的 clone() 方法在对某个对象克隆时对其仅仅是简单地执行域对域的 copy + 浅克隆:Object 中的 clone() 方法在对某个对象克隆时对其仅仅是简单地执行域对域的 copy * 对基本数据类型和包装类的克隆是没有问题的。String、Integer 等包装类型在内存中是不可以被改变的对象,所以在使用克隆时可以视为基本类型,只需浅克隆引用即可 * 如果对一个引用类型进行克隆时只是克隆了它的引用,和原始对象共享对象成员变量 @@ -2828,10 +2828,10 @@ Array 的工具类 常用API: -* `public static String toString(int[] a)` : 返回指定数组的内容的字符串表示形式 -* `public static void sort(int[] a)` : 按照数字顺序排列指定的数组 -* `public static int binarySearch(int[] a, int key)` : 利用二分查找返回指定元素的索引 -* `public static List asList(T... a)` : 返回由指定数组支持的列表。 +* `public static String toString(int[] a)`:返回指定数组的内容的字符串表示形式 +* `public static void sort(int[] a)`:按照数字顺序排列指定的数组 +* `public static int binarySearch(int[] a, int key)`:利用二分查找返回指定元素的索引 +* `public static List asList(T... a)`:返回由指定数组支持的列表 ```java public class MyArraysDemo { diff --git a/Prog.md b/Prog.md index 8d9bf64..6dc58f4 100644 --- a/Prog.md +++ b/Prog.md @@ -81,23 +81,9 @@ ### 创建线程 -#### 三种方式 - -运行一个程序就是开启一个进程,在进程中创建线程的方式有三种: - -1. 直接定义一个类继承线程类 Thread,重写 run() 方法,创建线程对象,调用线程对象的 start() 方法启动线程 -2. 定义一个线程任务类实现 Runnable 接口,重写 run() 方法,创建线程任务对象,把线程任务对象包装成线程对象, 调用线程对象的 start() 方法启动线程 -3. 实现 Callable 接口 - - - -*** - - - #### Thread -Thread创建线程方式:创建线程类,匿名内部类方式 +Thread 创建线程方式:创建线程类,匿名内部类方式 * **start() 方法底层其实是给 CPU 注册当前线程,并且触发 run() 方法执行** * 线程的启动必须调用 start() 方法,如果线程直接调用 run() 方法,相当于变成了普通类的执行,此时将只有主线程在执行该线程 @@ -132,7 +118,7 @@ class MyThread extends Thread{ 继承 Thread 类的优缺点: * 优点:编码简单 -* 缺点:线程类已经继承了Thread类无法继承其他类了,功能不能通过继承拓展(单继承的局限性) +* 缺点:线程类已经继承了 Thread 类无法继承其他类了,功能不能通过继承拓展(单继承的局限性) @@ -144,8 +130,6 @@ class MyThread extends Thread{ Runnable 创建线程方式:创建线程类,匿名内部类方式 -**Thread 类本身也是实现了 Runnable 接口** - Thread 的构造器: * `public Thread(Runnable target)` @@ -171,6 +155,22 @@ public class MyRunnable implements Runnable{ } ``` +**Thread 类本身也是实现了 Runnable 接口**,Thread 类中持有 Runnable 的属性,用来执行 run 方法: + +```java +public class Thread implements Runnable { + private Runnable target; + // 底层调用的是 Runnable 的 run 方法 + public void run() { + if (target != null) { + target.run(); + } + } +} +``` + + + * 缺点:代码复杂一点。 * 优点: @@ -204,12 +204,11 @@ public class MyRunnable implements Runnable{ `public FutureTask(Callable callable)`:未来任务对象,在线程执行完后**得到线程的执行结果** -* FutureTask 就是 Runnable 对象,被包装成未来任务对象 +* FutureTask 就是 Runnable 对象,因为 Thread 类只能执行 Runnable 实例的任务对象,所以把 Callable 包装一下 +* 线程池部分详解了 FutureTask 的源码 `public V get()`:同步等待 task 执行完毕的结果,如果在线程中获取另一个线程执行结果,会阻塞等待,用于线程同步 -优缺点: - * 优点:同 Runnable,并且能得到线程执行的结果 * 缺点:编码复杂 @@ -814,7 +813,7 @@ public class demo { 同步方法底层也是有锁对象的: -* 如果方法是实例方法:同步方法默认用this作为的锁对象 +* 如果方法是实例方法:同步方法默认用 this 作为的锁对象 ```java public synchronized void test() {} //等价于 @@ -823,7 +822,7 @@ public class demo { } ``` -* 如果方法是静态方法:同步方法默认用类名.class作为的锁对象 +* 如果方法是静态方法:同步方法默认用类名 .class 作为的锁对象 ```java class Test{ @@ -883,7 +882,7 @@ class Room { ##### 线程八锁 -所谓的“线程八锁”,其实就是考察 synchronized 锁住的是哪个对象,直接百度搜索 +所谓的“线程八锁”,其实就是考察 synchronized 锁住的是哪个对象,直接百度搜索相关的实例 说明:主要关注锁住的对象是不是同一个 @@ -959,12 +958,12 @@ Mark Word 中就被设置指向 Monitor 对象的指针,这就是重量级锁 * 开始时 Monitor 中 Owner 为 null * 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一 - 个 Owner,**obj对象的Mark Word指向Monitor** + 个 Owner,**obj 对象的 Mark Word 指向 Monitor** -* 在 Thread-2 上锁的过程中,Thread-3、Thread-4、Thread-5也来执行 synchronized(obj),就会进入 +* 在 Thread-2 上锁的过程中,Thread-3、Thread-4、Thread-5 也来执行 synchronized(obj),就会进入 EntryList BLOCKED(双向链表) * Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争是**非公平的** -* WaitSet 中的Thread-0,是以前获得过锁,但条件不满足进入 WAITING 状态的线程(wait-notify ) +* WaitSet 中的 Thread-0,是以前获得过锁,但条件不满足进入 WAITING 状态的线程(wait-notify) ![](https://gitee.com/seazean/images/raw/master/Java/JUC-Monitor工作原理2.png) @@ -1555,8 +1554,7 @@ public final native void wait(long timeout):有时限的等待, 到n毫秒后结 * Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态 * BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片 * BLOCKED 线程会在 Owner 线程释放锁时唤醒 -* WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入 - EntryList 重新竞争 +* WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入 EntryList 重新竞争 ![](https://gitee.com/seazean/images/raw/master/Java/JUC-Monitor工作原理2.png) @@ -4588,10 +4586,10 @@ public ThreadPoolExecutor(int corePoolSize, * handler:拒绝策略,线程到达最大线程数仍有新任务时会执行拒绝策略 - RejectedExecutionHandler下有 4 个实现类: + RejectedExecutionHandler 下有 4 个实现类: - * AbortPolicy:让调用者抛出 RejectedExecutionException 异常,默认策略 - * CallerRunsPolicy:"调用者运行"的调节机制,将某些任务回退到调用者,从而降低新任务的流量 + * AbortPolicy:让调用者抛出 RejectedExecutionException 异常,**默认策略** + * CallerRunsPolicy:让调用者运行的调节机制,将某些任务回退到调用者,从而降低新任务的流量 * DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常 * DiscardOldestPolicy:放弃队列中最早的任务,把当前任务加入队列中尝试再次提交当前任务 @@ -4611,7 +4609,7 @@ public ThreadPoolExecutor(int corePoolSize, 2. 当调用 execute() 方法添加一个请求任务时,线程池会做如下判断: * 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务 * 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列 - * 如果这时队列满了且正在运行的线程数量还小于 maximumPoolSize,那么会创建非核心线程**立刻运行**这个任务(对于阻塞队列中的任务不公平) + * 如果这时队列满了且正在运行的线程数量还小于 maximumPoolSize,那么会创建非核心线程**立刻运行这个任务**(对于阻塞队列中的任务不公平) * 如果队列满了且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会启动饱和**拒绝策略**来执行 3. 当一个线程完成任务时,会从队列中取下一个任务来执行 @@ -4619,6 +4617,8 @@ public ThreadPoolExecutor(int corePoolSize, +图片来源:https://space.bilibili.com/457326371/ + *** @@ -4681,6 +4681,8 @@ Executors提供了四种线程池的创建:newCachedThreadPool、newFixedThrea ![](https://gitee.com/seazean/images/raw/master/Java/JUC-newSingleThreadExecutor.png) + + *** @@ -4698,10 +4700,8 @@ Executors提供了四种线程池的创建:newCachedThreadPool、newFixedThrea Executors 返回的线程池对象弊端如下: - - FixedThreadPool 和 SingleThreadPool: - - 运行的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM - - CacheThreadPool 和 ScheduledThreadPool: - - 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM + - FixedThreadPool 和 SingleThreadPool:请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM + - CacheThreadPool 和 ScheduledThreadPool:允许创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,导致 OOM 创建多大容量的线程池合适? @@ -4710,6 +4710,7 @@ Executors提供了四种线程池的创建:newCachedThreadPool、newFixedThrea * 过小会导致程序不能充分地利用系统资源、容易导致饥饿 * 过大会导致更多的线程上下文切换,占用更多内存 + 上下文切换:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态,任务从保存到再加载的过程就是一次上下文切换 核心线程数常用公式: @@ -4736,16 +4737,16 @@ ExecutorService 类 API: | 方法 | 说明 | | ------------------------------------------------------------ | ------------------------------------------------------------ | -| void execute(Runnable command) | 执行任务(Executor类API) | +| void execute(Runnable command) | 执行任务(Executor 类 API) | | Future submit(Runnable task) | 提交任务 task() | -| Future submit(Callable task) | 提交任务 task,用返回值Future获得任务执行结果 | +| Future submit(Callable task) | 提交任务 task,用返回值 Future 获得任务执行结果 | | List> invokeAll(Collection> tasks) | 提交 tasks 中所有任务 | | List> invokeAll(Collection> tasks, long timeout, TimeUnit unit) | 提交 tasks 中所有任务,超时时间针对所有task,超时会取消没有执行完的任务,并抛出超时异常 | | T invokeAny(Collection> tasks) | 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消 | execute 和 submit 都属于线程池的方法,对比: -* execute 只能提交 Runnable 类型的任务,而 submit 既能提交 Runnable 类型任务也能提交 Callable 类型任务 +* execute 只能提交 Runnable 类型的任务,没有返回值,而 submit 既能提交 Runnable 类型任务也能提交 Callable 类型任务 * execute 会直接抛出任务执行时的异常,submit 会吞掉异常,可通过 Future 的 get 方法将任务执行时的异常重新抛出 @@ -4762,7 +4763,7 @@ ExecutorService 类 API: | 方法 | 说明 | | ----------------------------------------------------- | ------------------------------------------------------------ | | void shutdown() | 线程池状态变为 SHUTDOWN,等待任务执行完后关闭线程池,不会接收新任务,但已提交任务会执行完 | -| List shutdownNow() | 线程池状态变为 STOP,用 interrupt 中断正在执行的任务,直接关闭线程池,不会接收新任务,会将队列中的任务返回, | +| List shutdownNow() | 线程池状态变为 STOP,用 interrupt 中断正在执行的任务,直接关闭线程池,不会接收新任务,会将队列中的任务返回 | | boolean isShutdown() | 不在 RUNNING 状态的线程池,此执行者已被关闭,方法返回 true | | boolean isTerminated() | 线程池状态是否是 TERMINATED,如果所有任务在关闭后完成,返回 true | | boolean awaitTermination(long timeout, TimeUnit unit) | 调用 shutdown 后,由于调用线程不会等待所有任务运行结束,如果它想在线程池 TERMINATED 后做些事情,可以利用此方法等待 | @@ -4777,7 +4778,7 @@ ExecutorService 类 API: execute 会直接抛出任务执行时的异常,submit 会吞掉异常,有两种处理方法 -方法1:主动捉异常 +方法 1:主动捉异常 ```java ExecutorService executorService = Executors.newFixedThreadPool(1); @@ -4791,7 +4792,7 @@ pool.submit(() -> { }); ``` -方法2:使用 Future +方法 2:使用 Future 对象 ```java ExecutorService executorService = Executors.newFixedThreadPool(1); @@ -4815,27 +4816,720 @@ System.out.println(future.get()); #### 状态信息 -ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量 +ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量。这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 CAS 原子操作进行赋值 + +* 状态表示: -| 状态 | 高3位 | 接收新任务 | 处理阻塞任务队列 | 说明 | -| ---------- | ----- | ---------- | ---------------- | --------------------------------------- | -| RUNNING | 111 | Y | Y | | -| SHUTDOWN | 000 | N | Y | 不接收新任务,但处理阻塞队列剩余任务 | -| STOP | 001 | N | N | 中断正在执行的任务,并抛弃阻塞队列任务 | -| TIDYING | 010 | - | - | 任务全执行完毕,活动线程为0即将进入终结 | -| TERMINATED | 011 | - | - | 终止状态 | + ```java + // 高3位:表示当前线程池运行状态,除去高3位之后的低位:表示当前线程池中所拥有的线程数量 + private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); + // 表示在 ctl 中,低 COUNT_BITS 位,是用于存放当前线程数量的位 + private static final int COUNT_BITS = Integer.SIZE - 3; + // 低 COUNT_BITS 位所能表达的最大数值,000 11111111111111111111 => 5亿多 + private static final int CAPACITY = (1 << COUNT_BITS) - 1; + ``` -这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 cas 原子操作 -进行赋值 + ![](https://gitee.com/seazean/images/raw/master/Java/JUC-线程池状态转换图.png) + +* 四种状态: + + ```java + // 111 000000000000000000,转换成整数后其实就是一个负数 + private static final int RUNNING = -1 << COUNT_BITS; + // 000 000000000000000000 + private static final int SHUTDOWN = 0 << COUNT_BITS; + // 001 000000000000000000 + private static final int STOP = 1 << COUNT_BITS; + // 010 000000000000000000 + private static final int TIDYING = 2 << COUNT_BITS; + // 011 000000000000000000 + private static final int TERMINATED = 3 << COUNT_BITS; + ``` + + | 状态 | 高3位 | 接收新任务 | 处理阻塞任务队列 | 说明 | + | ---------- | ----- | ---------- | ---------------- | ----------------------------------------- | + | RUNNING | 111 | Y | Y | | + | SHUTDOWN | 000 | N | Y | 不接收新任务,但处理阻塞队列剩余任务 | + | STOP | 001 | N | N | 中断正在执行的任务,并抛弃阻塞队列任务 | + | TIDYING | 010 | - | - | 任务全执行完毕,活动线程为 0 即将进入终结 | + | TERMINATED | 011 | - | - | 终止状态 | + +* 获取当前线程池运行状态: + + ```java + // ~CAPACITY = ~000 11111111111111111111 = 111 000000000000000000000(取反) + // c == ctl = 111 000000000000000000111 + // 111 000000000000000000111 + // 111 000000000000000000000 + // 111 000000000000000000000 获取到了运行状态 + private static int runStateOf(int c) { return c & ~CAPACITY; } + ``` + +* 获取当前线程池线程数量: + + ```java + // c = 111 000000000000000000111 + // CAPACITY = 000 111111111111111111111 + // 000 000000000000000000111 => 7 + private static int workerCountOf(int c) { return c & CAPACITY; } + ``` + +* 重置当前线程池状态 ctl: + + ```java + // rs 表示线程池状态 wc 表示当前线程池中 worker(线程)数量,类似相加操作 + private static int ctlOf(int rs, int wc) { return rs | wc; } + ``` + +* 比较当前线程池 ctl 所表示的状态: + + ```java + // 比较当前线程池 ctl 所表示的状态,是否小于某个状态 s + // 状态对比:RUNNING < SHUTDOWN < STOP < TIDYING < TERMINATED + private static boolean runStateLessThan(int c, int s) { return c < s; } + // 比较当前线程池 ctl 所表示的状态,是否大于等于某个状态s + private static boolean runStateAtLeast(int c, int s) { return c >= s; } + // 小于 SHUTDOWN 的一定是 RUNNING,SHUTDOWN == 0 + private static boolean isRunning(int c) { return c < SHUTDOWN; } + ``` + +* 设置线程池 ctl: + + ```java + // 使用CAS方式 让 ctl 值 +1 ,成功返回 true, 失败返回 false + private boolean compareAndIncrementWorkerCount(int expect) { + return ctl.compareAndSet(expect, expect + 1); + } + // 使用CAS 方式 让 ctl 值 -1 ,成功返回 true, 失败返回 false + private boolean compareAndDecrementWorkerCount(int expect) { + return ctl.compareAndSet(expect, expect - 1); + } + // 将 ctl 值减一,do while 循环会一直重试,直到成功为止 + private void decrementWorkerCount() { + do {} while (!compareAndDecrementWorkerCount(ctl.get())); + } + ``` + + + +**** + + + +#### 成员属性 + +成员变量 + +* 线程池中存放 worker 的容器:线程池没有初始化,直接往池中加线程即可 + + ```java + private final HashSet workers = new HashSet(); + ``` + +* 线程全局锁: + + ```java + // 增加减少 worker 或者时修改线程池运行状态需要持有 mainLock + private final ReentrantLock mainLock = new ReentrantLock(); + ``` + +* 可重入锁的条件变量: + + ```java + // 当外部线程调用 awaitTermination() 方法时,会等待当前线程池状态为 Termination 为止 + private final Condition termination = mainLock.newCondition() + ``` + +* 线程池相关参数: + + ```java + private volatile int corePoolSize; // 核心线程数量 + private volatile int maximumPoolSize; // 线程池最大线程数量 + private volatile long keepAliveTime; // 空闲线程存活时间 + private volatile ThreadFactory threadFactory; // 创建线程时使用的线程工厂,默认是 DefaultThreadFactory + private final BlockingQueue workQueue;// 任务队列池中的线程达到核心线程数量后,再提交任务就放入队列 + ``` + + ```java + private volatile RejectedExecutionHandler handler; // 拒绝策略,juc包提供了4中方式 + private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();// 默认策略 + ``` + +* 记录线程池相关属性的数值: + + ```java + private int largestPoolSize; // 记录线程池生命周期内线程数最大值 + private long completedTaskCount; // 记录线程池所完成任务总数,当某个 worker 退出时将完成的任务累积到该属性 + ``` + +* 控制核心线程数量内的线程是否可以被回收: + + ```java + // false 代表不可以,为 true 时核心数量内的线程空闲超过 keepAliveTime 也会被回收 + private volatile boolean allowCoreThreadTimeOut; + ``` + +内部类: + +* Worker 类:**每个 Worker 对象会绑定一个初始任务**,启动 Worker 时优先执行 + + ```java + // Worker 采用了AQS的独占模式,state = 0 表示未被占用,> 0 表示被占用,< 0 表示初始状态,这种情况下不能被抢锁 + // ExclusiveOwnerThread:表示独占锁的线程 + private final class Worker extends AbstractQueuedSynchronizer implements Runnable { + final Thread thread; // worker 内部封装的工作线程 + Runnable firstTask; // worker 第一个执行的任务 + volatile long completedTasks; // 记录当前 worker 所完成任务数量 + + //构造方法 + Worker(Runnable firstTask) { + // 设置AQS独占模式为初始化中状态,这个状态不能被抢占锁 + setState(-1); + // firstTask不为空时,当worker启动后,内部线程会优先执行firstTask,执行完后会到queue中去获取下个任务 + this.firstTask = firstTask; + // 使用【线程工厂创建一个线程】,并且将当前worker指定为Runnable,所以thread启动时会调用 worker.run() + this.thread = getThreadFactory().newThread(this); + } + } + ``` + +* 拒绝策略相关的内部类 + + + + + +*** + + + +#### 成员方法 + +##### 提交方法 + +* AbstractExecutorService#submit():提交任务的方法 + + ```java + public Future submit(Runnable task) { + // 空指针异常 + if (task == null) throw new NullPointerException(); + // 把 Runnable 封装成未来任务对象 + RunnableFuture ftask = newTaskFor(task, null); + // 执行方法 + execute(ftask); + return ftask; + } + public Future submit(Callable task) { + if (task == null) throw new NullPointerException(); + // 把 Callable 封装成未来任务对象 + RunnableFuture ftask = newTaskFor(task); + // 执行方法 + execute(ftask); + // 返回未来任务对象,用来获取返回值 + return ftask; + } + ``` + + ```java + protected RunnableFuture newTaskFor(Runnable runnable, T value) { + // Runnable 封装成 FutureTask,指定返回值 + return new FutureTask(runnable, value); + } + protected RunnableFuture newTaskFor(Callable callable) { + // Callable 直接封装成 FutureTask + return new FutureTask(callable); + } + ``` + +* execute():执行任务 + + ```java + // command 可以是普通的 Runnable 实现类,也可以是 FutureTask + public void execute(Runnable command) { + // 非空判断 + if (command == null) + throw new NullPointerException(); + // 获取 ctl 最新值赋值给 c,ctl 高3位表示线程池状态,低位表示当前线程池线程数量。 + int c = ctl.get(); + // 【1】条件成立表示当前线程数量小于核心线程数,此次提交任务直接创建一个新的 worker,线程池中多了一个新的线程 + if (workerCountOf(c) < corePoolSize) { + // addWorker 为创建线程的过程,会创建 worker 对象并且将 command 作为 firstTask,优先执行 + if (addWorker(command, true)) + // 创建成功直接返回 + return; + // 执行到这条语句,说明 addWorker 一定是失败的,存在并发现象或者线程池状态被改变, + // SHUTDOWN 状态下也有可能创建成功,前提 firstTask == null 而且当前 queue 不为空(特殊情况) + c = ctl.get(); + } + // 执行到这说明当前线程数量已经达到 corePoolSize 或者 addWorker 失败 + // 【2】条件成立说明当前线程池处于running状态,则尝试将 task 放入到 workQueue 中 + if (isRunning(c) && workQueue.offer(command)) { + // 获取线程池状态 ctl 保存到 recheck + int recheck = ctl.get(); + // 条件一成立说明线程池状态被外部线程给修改了,可能是执行了 shutdown() 方法,需要把刚提交的任务删除 + // 删除成功说明提交之后,线程池中的线程还未消费该任务(处理) + if (!isRunning(recheck) && remove(command)) + // 任务出队成功,走拒绝策略 + reject(command); + // 执行到这说明线程池是 running 状态,获取线程池中的线程数量,判断是否是 0 + // 【担保机制】,保证线程池在 running 状态下,最起码得有一个线程在工作 + else if (workerCountOf(recheck) == 0) + addWorker(null, false); + } + // 【3】offer 失败说明当前 queue 满了,如果当前线程数量尚未达到 maximumPoolSize 的话,会创建新的worker直接执行 command,如果当前线程数量达到 maximumPoolSize 的话,这里 addWorker 也会失败,走拒绝策略 + else if (!addWorker(command, false)) + reject(command); + } + ``` + + + + + +*** + + + +##### 添加线程 + +* addWorker():**添加线程到线程池**,返回 true 表示创建 Worker 成功,且线程启动 + + ```java + // core == true 表示采用核心线程数量限制,false表示采用 maximumPoolSize + private boolean addWorker(Runnable firstTask, boolean core) { + // 自旋判断当前线程池状态是否允许创建线程的,允许就设置线程数量 + 1 + retry: + for (;;) { + // 获取 ctl 的值 + int c = ctl.get(); + // 获取当前线程池运行状态 + int rs = runStateOf(c); + // 判断当前线程池状态【是否允许添加线程】 + // 条件一判断线程池的状态是否是 running 状态,成立则表示不是,不允许添加 worker,返回 false, + // 条件二是当前线程池是 SHUTDOWN 状态,但是队列里面还有任务尚未处理完, + // 【这时需要处理完 queue 中的任务,但是不允许再提交新的 task】 + if (rs >= SHUTDOWN && !(rs == SHUTDOWN && firstTask == null && !workQueue.isEmpty())) + return false; + for (;;) { + // 获取当前线程池中线程数量 + int wc = workerCountOf(c); + // 条件一一般不成立,CAPACITY是5亿多,根据 core 判断使用哪个大小限制线程数量,超过了返回 false + if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize)) + return false; + // 记录线程数量已经加 1,相当于申请到了一块令牌,条件失败说明其他线程修改了数量 + if (compareAndIncrementWorkerCount(c)) + // 申请成功,跳出了 retry 这个 for 自旋 + break retry; + // CAS 失败,没有成功的申请到令牌 + c = ctl.get(); + // 判断当前线程池状态是否发生过变化,被其他线程修改了,可能调用了 shutdown() 方法 + if (runStateOf(c) != rs) + // 返回外层循环检查是否能创建线程,在 if 语句中返回 false + continue retry; + + } + } + //【令牌申请成功,开始创建线程】 + // 运行标记,表示创建的 worker 是否已经启动,false未启动 true启动 + boolean workerStarted = false; + // 添加标记,表示创建的 worker 是否添加到池子中了,默认false未添加,true是添加。 + boolean workerAdded = false; + Worker w = null; + try { + // 创建 Worker,底层通过线程工厂创建执行线程 + w = new Worker(firstTask); + // 将新创建的 worker 节点的线程赋值给 t + final Thread t = w.thread; + // 为了防止 ThreadFactory 程序员自定义的实现类有 bug,创造不出线程 + if (t != null) { + final ReentrantLock mainLock = this.mainLock; + // 加互斥锁 + mainLock.lock(); + try { + // 获取最新线程池运行状态保存到rs中 + int rs = runStateOf(ctl.get()); + // 判断线程池是否为RUNNING状态,不是再判断当前是否为SHUTDOWN状态且firstTask为空(特殊情况) + if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) { + // 当线程start后,线程isAlive会返回true,否则报错 + if (t.isAlive()) throw new IllegalThreadStateException(); + //【将新建的 Worker 添加到线程池中】 + workers.add(w); + int s = workers.size(); + // 条件成立说明当前线程数量是一个新高,更新 largestPoolSize + if (s > largestPoolSize) + largestPoolSize = s; + // 添加标记置为 true + workerAdded = true; + } + } finally { + // 解锁啊 + mainLock.unlock(); + } + // 添加成功就启动线程【执行任务】 + if (workerAdded) { + // Thread 类中持有 Runnable 任务对象,调用的是 Runnable 的 run 方法 + t.start(); + // 运行标记置为 true + workerStarted = true; + } + } + } finally { + // 如果启动线程失败,做清理工作 + if (! workerStarted) + addWorkerFailed(w); + } + // 返回新创建的线程是否启动 + return workerStarted; + } + ``` + +* addWorkerFailed():清理任务 + + ```java + private void addWorkerFailed(Worker w) { + final ReentrantLock mainLock = this.mainLock; + // 持有线程池全局锁,因为操作的是线程池相关的东西 + mainLock.lock(); + try { + //条件成立需要将 worker 在 workers 中清理出去。 + if (w != null) + workers.remove(w); + // 将线程池计数 -1,相当于归还令牌。 + decrementWorkerCount(); + tryTerminate(); + } finally { + //释放线程池全局锁。 + mainLock.unlock(); + } + } + ``` + + + +**** -```java -// c为旧值, ctlOf返回结果为新值 -ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c)))); -// rs为高3位代表线程池状态, wc为低29位代表线程个数,ctl是合并它们 -private static int ctlOf(int rs, int wc) { return rs | wc; } -``` -![](https://gitee.com/seazean/images/raw/master/Java/JUC-线程池状态转换图.png) + +##### 运行方法 + +* Worker#run:当某个 worker 启动时,会执行 run() + + ```java + public void run() { + // ThreadPoolExecutor#runWorker() + runWorker(this); + } + ``` + +* runWorker():**执行任务**,线程会一直 while 循环获取任务执行任务 + + ```java + final void runWorker(Worker w) { + Thread wt = Thread.currentThread(); + // 获取 worker 的 firstTask + Runnable task = w.firstTask; + // 引用置空,防止复用该线程时重复执行该任务 + w.firstTask = null; + // 初始化 worker 时设置 state = -1,这里需要设置 state = 0 和 exclusiveOwnerThread = null + w.unlock(); + // true 表示发生异常退出,false 表示正常退出。 + boolean completedAbruptly = true; + try { + // firstTask 不是 null 就直接运行,否则去 queue 中获取任务 + // 【getTask如果是阻塞获取任务,会一直阻塞在take方法,获取后继续循环,不会走返回null的逻辑】 + while (task != null || (task = getTask()) != null) { + // 加锁,shutdown 时会判断当前worker状态,根据独占锁是否【空闲】来判断当前worker是否正在工作。 + w.lock(); + // 说明线程池状态大于 STOP,目前处于 STOP/TIDYING/TERMINATION,此时给线程一个中断信号 + if ((runStateAtLeast(ctl.get(), STOP) || + // 判断当前线程是否被打断,清楚打断标记,所以最后一个条件会返回false,取反为 true + (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) && !wt.isInterrupted()) + // 中断线程,设置线程的中断标志位为 true + wt.interrupt(); + try { + // 钩子方法,开发者自定义实现 + beforeExecute(wt, task); + Throwable thrown = null; + try { + // task 可能是 FutureTask,也可能是普通的 Runnable 接口实现类。 + task.run(); + } catch (RuntimeException x) { + thrown = x; throw x; + } catch (Error x) { + thrown = x; throw x; + } catch (Throwable x) { + thrown = x; throw new Error(x); + } finally { + // 钩子方法,开发者自定义实现 + afterExecute(task, thrown); + } + } finally { + task = null; // 将局部变量task置为null + w.completedTasks++; // 更新worker完成任务数量 + w.unlock(); // 解锁 + } + } + // getTask() 方法返回 null 时会执行这里,说明 queue 为空或者线程太多,当前【线程应该执行退出逻辑】 + completedAbruptly = false; + } finally { + // 正常退出 completedAbruptly = false + // 异常退出 completedAbruptly = true,task.run() 内部抛出异常时,跳到这一行 + processWorkerExit(w, completedAbruptly); + } + } + ``` + +* unlock():重置锁 + + ```java + public void unlock() { release(1); } + //外部不会直接调用这个方法 这个方法是 AQS 内调用的,外部调用 unlock 时触发此方法 + protected boolean tryRelease(int unused) { + setExclusiveOwnerThread(null); // 设置持有者为 null + setState(0); // 设置 state = 0 + return true; + } + ``` + +* getTask():获取任务,线程空闲时间超过 keepAliveTime 就会被回收,判断的依据是**当前线程超过保活时间没有获取到任务** + + ```java + private Runnable getTask() { + //超时标记,表示当前线程获取任务是否超时,默认 false,true 表示已超时 + boolean timedOut = false; + for (;;) { + // 获取最新ctl值保存到c中 + int c = ctl.get(); + // 获取线程池当前运行状态 + int rs = runStateOf(c); + + // 条件一成立说明当前线程池是非 RUNNING 状态 + // 条件二成立说明线程池已经停止或者 queue 为 null,没有任务需要执行 + if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) { + // 使用 CAS 自旋的方式让 ctl 值 -1 + decrementWorkerCount(); + // 返回null,runWorker 方法就会将返回 null 的线程执行线程退出线程池的逻辑 + return null; + } + // 获取线程池中的线程数量 + int wc = workerCountOf(c); + + // timed = false 表示当前这个线程 获取task时不支持超时机制的,当前线程会使用 queue.take() 阻塞获取 + // timed = true 表示当前这个线程 获取task时支持超时机制,使用 queue.poll(xxx,xxx) 超时获取 + // 当获取task超时的情况下,下一次自旋就可能返回null了 + // 条件一代表允许回收核心线程,那就无所谓了,全部线程都执行超时回收 + // 条件二成立说明线程数量大于核心线程数,执行该方法的线程去超时获取任务,获取不到返回null,执行线程退出逻辑 + boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; + + // 条件一判断线程数量是否超过最大线程数,直接回收 + // wc > 1 说明线程池还用其他线程,当前线程可以直接回收 + // workQueue.isEmpty() 前置条件是 wc = 1,说明当前任务队列已经空了,最后一个线程,也可以放心的退出 + if ((wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty())) { + // 使用CAS机制将 ctl 值 -1 ,减 1 成功的线程,返回 null,可以推出 + if (compareAndDecrementWorkerCount(c)) + return null; + continue; + } + + try { + // 根据当前线程是否需要超时回收【选择从队列获取任务的方法】超时获取或者阻塞获取 + Runnable r = timed ? + workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take(); + // 获取到任务返回任务 + // 【阻塞获取会阻塞到获取任务为止】,不会返回 null + if (r != null) + return r; + // 获取任务为 null,超时标记设置为 true,下次自旋时返回 null + timedOut = true; + } catch (InterruptedException retry) { + // 被打断后超时标记置为 false + timedOut = false; + } + } + } + ``` + +* processWorkerExit():**线程退出线程池** + + ```java + private void processWorkerExit(Worker w, boolean completedAbruptly) { + // 条件成立代表当前 worker 是发生异常退出的,task 任务执行过程中向上抛出异常了 + if (completedAbruptly) + decrementWorkerCount(); + + final ReentrantLock mainLock = this.mainLock; + mainLock.lock(); // 加锁 + try { + // 将当前 worker 完成的 task 数量,汇总到线程池的 completedTaskCount + completedTaskCount += w.completedTasks; + // 将 worker 从线程池中移除 + workers.remove(w); + } finally { + mainLock.unlock(); // 解锁 + } + // 尝试停止线程池 + tryTerminate(); + + int c = ctl.get(); + // 条件成立说明当前线程池状态为 RUNNING 或者 SHUTDOWN 状态 + if (runStateLessThan(c, STOP)) { + // 正常退出的逻辑,是空闲线程回收 + if (!completedAbruptly) { + // 根据是否回收核心线程确定线程池中的最小值 + int min = allowCoreThreadTimeOut ? 0 : corePoolSize; + // 最小值为 0,但是线程队列不为空,需要一个线程来完成任务 + if (min == 0 && !workQueue.isEmpty()) + min = 1; + // 线程池中的线程数量大于最小值可以直接返回 + if (workerCountOf(c) >= min) + return; + } + // 执行task时发生异常,这里要创建一个新 worker 加进线程池 + addWorker(null, false); + } + } + ``` + + + +**** + + + +##### 停止方法 + +* shutdown(): + + ```java + public void shutdown() { + final ReentrantLock mainLock = this.mainLock; + //获取线程池全局锁 + mainLock.lock(); + try { + checkShutdownAccess(); + //设置线程池状态为SHUTDOWN + advanceRunState(SHUTDOWN); + //中断空闲线程 + interruptIdleWorkers(); + //空方法,子类可以扩展 + onShutdown(); + } finally { + //释放线程池全局锁 + mainLock.unlock(); + } + tryTerminate(); + } + ``` + +* interruptIdleWorkers():shutdown 方法会**中断空闲线程** + + ```java + // onlyOne == true 说明只中断一个线程 ,false 则中断所有线程 + private void interruptIdleWorkers(boolean onlyOne) { + final ReentrantLock mainLock = this.mainLock; + //持有全局锁 + mainLock.lock(); + try { + // 遍历所有worker + for (Worker w : workers) { + // 获取当前 worker 的线程 + Thread t = w.thread; + //条件一成立:说明当前迭代的这个线程尚未中断 + //条件二成立:说明当前worker处于空闲状态,阻塞在poll或者take,因为worker执行task时是加锁的 + // w.tryLock() 加锁,独占锁情况下其他线程持有锁会加锁失败返回 false + if (!t.isInterrupted() && w.tryLock()) { + try { + // 中断线程,处于 queue 阻塞的线程会被唤醒,进入下一次自旋,返回null,执行退出相逻辑 + t.interrupt(); + } catch (SecurityException ignore) { + } finally { + // 释放worker的独占锁 + w.unlock(); + } + } + if (onlyOne) + break; + } + + } finally { + //释放全局锁。 + mainLock.unlock(); + } + } + ``` + +* shutdownNow(): + + ```java + public List shutdownNow() { + //返回值引用 + List tasks; + final ReentrantLock mainLock = this.mainLock; + //获取线程池全局锁 + mainLock.lock(); + try { + checkShutdownAccess(); + //设置线程池状态为STOP + advanceRunState(STOP); + //中断线程池中所有线程 + interruptWorkers(); + //导出未处理的task + tasks = drainQueue(); + } finally { + mainLock.unlock(); + } + + tryTerminate(); + //返回当前任务队列中 未处理的任务。 + return tasks; + } + ``` + +* tryTerminate():设置为 TERMINATED 状态 if either (SHUTDOWN and pool and queue empty) or (STOP and pool empty) + + ```java + final void tryTerminate() { + for (;;) { + // 获取 ctl 的值 + int c = ctl.get(); + // 条件一说明线程池正常,条件二说明有其他线程执行了该方法,当前线程直接返回 + if (isRunning(c) || runStateAtLeast(c, TIDYING) || + // 线程池是 SHUTDOWN 并且任务队列不是空,需要去处理队列中的任务 + (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty())) + return; + // 判断线程池中线程的数量 + if (workerCountOf(c) != 0) { + // 中断一个空闲线程,空闲线程,在 queue.take() | queue.poll() 阻塞空闲 + // 唤醒后的线程会在getTask()方法返回null,执行退出逻辑时会再次调用tryTerminate()唤醒下一个空闲线程 + interruptIdleWorkers(ONLY_ONE); + return; + } + // 池中的线程数量为 0 来到这里 + final ReentrantLock mainLock = this.mainLock; + // 加锁 + mainLock.lock(); + try { + // 设置线程池状态为 TIDYING 状态 + if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) { + try { + // 结束线程池 + terminated(); + } finally { + // 设置线程池状态为TERMINATED状态。 + ctl.set(ctlOf(TERMINATED, 0)); + // 唤醒所有调用 awaitTermination() 方法的线程 + termination.signalAll(); + } + return; + } + } finally { + // 释放线程池全局锁 + mainLock.unlock(); + } + // else retry on failed CAS + } + } + ``` @@ -4874,8 +5568,8 @@ public FutureTask(Callable callable){ } public FutureTask(Runnable runnable, V result) { - //使用装饰者模式将 runnable 转换成 callable 接口,外部线程通过 get 获取 - //当前任务执行结果时,结果可能为 null 也可能为【传进来】的值,传进来什么返回什么 + // 使用装饰者模式将 runnable 转换成 callable 接口,外部线程通过 get 获取 + // 当前任务执行结果时,结果可能为 null 也可能为【传进来】的值,传进来什么返回什么 this.callable = Executors.callable(runnable, result); this.state = NEW; } @@ -4893,35 +5587,66 @@ public FutureTask(Runnable runnable, V result) { FutureTask 类的成员属性: -```java -// 表示当前task状态 -private volatile int state; -// 当前任务尚未执行 -private static final int NEW = 0; -// 当前任务正在结束,尚未完全结束,一种临界状态 -private static final int COMPLETING = 1; -// 当前任务正常结束 -private static final int NORMAL = 2; -// 当前任务执行过程中发生了异常。 内部封装的 callable.run() 向上抛出异常了 -private static final int EXCEPTIONAL = 3; -// 当前任务被取消 -private static final int CANCELLED = 4; -// 当前任务中断中 -private static final int INTERRUPTING = 5; -// 当前任务已中断 -private static final int INTERRUPTED = 6; +* 任务状态: -// Runnable 使用 装饰者模式伪装成 Callable -private Callable callable; -// 正常情况下:任务正常执行结束,outcome 保存执行结果,callable 返回值。 -// 非正常情况:callable 向上抛出异常,outcome 保存异常 -private Object outcome; -// 当前任务被线程执行期间,保存当前执行任务的线程对象引用 -private volatile Thread runner; -// 会有很多线程去 get 当前任务的结果,这里使用了一种数据结构头插头取(类似栈)的一个队列来保存所有的 get 线程 -// WaitNode 是单向的链表 -private volatile WaitNode waiters; -``` + ```java + // 表示当前task状态 + private volatile int state; + // 当前任务尚未执行 + private static final int NEW = 0; + // 当前任务正在结束,尚未完全结束,一种临界状态 + private static final int COMPLETING = 1; + // 当前任务正常结束 + private static final int NORMAL = 2; + // 当前任务执行过程中发生了异常。 内部封装的 callable.run() 向上抛出异常了 + private static final int EXCEPTIONAL = 3; + // 当前任务被取消 + private static final int CANCELLED = 4; + // 当前任务中断中 + private static final int INTERRUPTING = 5; + // 当前任务已中断 + private static final int INTERRUPTED = 6; + ``` + +* 任务对象: + + ```java + private Callable callable; // Runnable 使用装饰者模式伪装成 Callable + ``` + +* 返回结果: + + ```java + // 正常情况下:任务正常执行结束,outcome 保存执行结果,callable 返回值。 + // 非正常情况:callable 向上抛出异常,outcome 保存异常 + private Object outcome; + ``` + +* 执行当前任务的线程对象: + + ```java + private volatile Thread runner; // 当前任务被线程执行期间,保存当前执行任务的线程对象引用 + ``` + +* 阻塞线程的队列: + + ```java + // 会有很多线程去 get 当前任务的结果,这里使用了一种数据结构头插头取(类似栈)的一个队列来保存所有的 get 线程 + private volatile WaitNode waiters; + ``` + +* 内部类: + + ```java + static final class WaitNode { + //单向链表 + volatile Thread thread; + volatile WaitNode next; + WaitNode() { thread = Thread.currentThread(); } + } + ``` + + @@ -4938,7 +5663,7 @@ FutureTask 类的成员方法: ```java public void run() { //条件一:成立说明当前 task 已经被执行过了或者被 cancel 了,非 NEW 状态的任务,线程就不处理了 - //条件二:线程是 NEW 状态,尝试设置当前任务对象的线程是当前线程,设置失败说明其他线程抢占了该任务 + //条件二:线程是 NEW 状态,尝试设置当前任务对象的线程是当前线程,设置失败说明其他线程抢占了该任务,直接返回 if (state != NEW || !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread())) @@ -4946,8 +5671,7 @@ FutureTask 类的成员方法: try { // 执行到这里,当前 task 一定是 NEW 状态,而且当前线程也抢占 task 成功! Callable c = callable; - // 条件一:防止空指针异常 - // 条件二:防止外部线程在此期间 cancel 掉当前任务。 + // 判断任务是否为空,防止空指针异常,判断 state 防止外部线程在此期间 cancel 掉当前任务。 if (c != null && state == NEW) { // 结果引用 V result; @@ -4972,7 +5696,7 @@ FutureTask 类的成员方法: set(result); } } finally { - // 任务执行完成,取消线程的引用 + // 任务执行完成,取消线程的引用,help GC runner = null; int s = state; // 判断任务是不是被中断 @@ -5012,7 +5736,7 @@ FutureTask 类的成员方法: } ``` - FutureTask#finishCompletion:完成 + FutureTask#finishCompletion:**唤醒 get() 阻塞线程** ```java private void finishCompletion() { @@ -5024,7 +5748,7 @@ FutureTask 类的成员方法: for (;;) { // 获取当前 WaitNode 节点封装的 thread Thread t = q.thread; - // 当前线程不为 null,唤醒当前线程 + // 当前线程不为 null,唤醒当前 get() 等待获取数据的线程 if (t != null) { q.thread = null; LockSupport.unpark(t); @@ -5050,13 +5774,14 @@ FutureTask 类的成员方法: ```java private void handlePossibleCancellationInterrupt(int s) { if (s == INTERRUPTING) + // 中断状态中 while (state == INTERRUPTING) // 等待中断完成 Thread.yield(); } ``` -* **FutureTask#get**:获取任务执行的返回值 +* **FutureTask#get**:获取任务执行的返回值,执行 run 和 get 的不是同一个线程,可能有多个线程 get,只有一个线程 run ```java public V get() throws InterruptedException, ExecutionException { @@ -5070,7 +5795,7 @@ FutureTask 类的成员方法: } ``` - FutureTask#awaitDone:线程阻塞等待 + FutureTask#awaitDone:**get 线程阻塞等待** ```java private int awaitDone(boolean timed, long nanos) throws InterruptedException { @@ -5103,10 +5828,10 @@ FutureTask 类的成员方法: Thread.yield(); // 条件成立:【第一次自旋】,当前线程还未创建 WaitNode 对象,此时为当前线程创建 WaitNode对象 else if (q == null) - q = new WaitNode();条件成立:第二次自旋,当前线程已经创建 WaitNode对象了,但是node对象还未入队 + q = new WaitNode(); // 条件成立:【第二次自旋】,当前线程已经创建 WaitNode 对象了,但是node对象还未入队 else if (!queued) - // waiters 指向队首,让当前 WaitNode 成为新的队首,头插法 + // waiters 指向队首,让当前 WaitNode 成为新的队首,【头插法】 // 失败说明再次期间有了新的队首 queued = UNSAFE.compareAndSwapObject(this, waitersOffset, q.next = waiters, q); // 条件成立:【第三次自旋】,会到这里。 @@ -5127,7 +5852,7 @@ FutureTask 类的成员方法: } ``` - FutureTask#report:返回结果 + FutureTask#report:封装运行结果 ```java private V report(int s) throws ExecutionException { @@ -5149,7 +5874,7 @@ FutureTask 类的成员方法: ```java public boolean cancel(boolean mayInterruptIfRunning) { // 条件一:表示当前任务处于运行中或者处于线程池任务队列中 - // 条件二:表示修改状态,成功可以去执行下面逻辑,否则 返回 false 表示 cancel 失败。 + // 条件二:表示修改状态,成功可以去执行下面逻辑,否则返回 false 表示 cancel 失败。 if (!(state == NEW && UNSAFE.compareAndSwapInt(this, stateOffset, NEW, mayInterruptIfRunning ? INTERRUPTING : CANCELLED))) @@ -5157,7 +5882,7 @@ FutureTask 类的成员方法: try { if (mayInterruptIfRunning) { try { - // 执行当前 FutureTask 的线程 + // 获取执行当前 FutureTask 的线程 Thread t = runner; if (t != null) // 打断执行的线程 @@ -5175,7 +5900,10 @@ FutureTask 类的成员方法: } ``` - + + + +参考视频:https://www.bilibili.com/video/BV13E411N7pp @@ -5507,8 +6235,7 @@ class MockConnection implements Connection { AQS:AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架,许多同步类实现都依赖于它 -* 用 state 属性来表示资源的状态(分**独占模式和共享模式**),子类需要定义如何维护这个状态,控制如何获取 - 锁和释放锁 +* 用 state 属性来表示资源的状态(分**独占模式和共享模式**),子类需要定义如何维护这个状态,控制如何获取锁和释放锁 * 独占模式是只有一个线程能够访问资源,如 ReentrantLock * 共享模式允许多个线程访问资源,如 Semaphore,ReentrantReadWriteLock 是组合式 * 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList(**同步队列:双向,便于出队入队**) @@ -5752,12 +6479,12 @@ ReentrantLock 相对于 synchronized 它具备如下特点: 构造方法:`ReentrantLock lock = new ReentrantLock();` -ReentrantLock类API: +ReentrantLock 类 API: * `public void lock()`:获得锁 - * 如果锁没有被另一个线程占用,则将锁定计数设置为1。 + * 如果锁没有被另一个线程占用,则将锁定计数设置为 1 - * 如果当前线程已经保持锁定,则保持计数增加1 + * 如果当前线程已经保持锁定,则保持计数增加 1 * 如果锁被另一个线程保持,则当前线程被禁用线程调度,并且在锁定已被获取之前处于休眠状态 @@ -6441,10 +7168,10 @@ synchronized 的条件变量,是当条件不满足时进入 waitSet 等待;R ReentrantLock 类获取 Condition 对象:`public Condition newCondition()` -Condition类API: +Condition 类 API: * `void await()`:当前线程从运行状态进入等待状态,释放锁 -* `void signal()`:唤醒一个等待在Condition上的线程,但是必须获得与该Condition相关的锁 +* `void signal()`:唤醒一个等待在 Condition 上的线程,但是必须获得与该 Condition 相关的锁 使用流程: @@ -6488,9 +7215,13 @@ public static void main(String[] args) throws InterruptedException { +**** + + + ##### 实现原理 -await流程: +await 流程: * 开始 Thread-0 持有锁,调用 await,进入 ConditionObject 的 addConditionWaiter 流程 @@ -7818,7 +8549,7 @@ B站视频解析:https://www.bilibili.com/video/BV1n541177Ea static final int WRITER = 1; // 等待者状态(写线程在等待),当 TreeBin 中有读线程目前正在读取数据时,写线程无法修改数据 static final int WAITER = 2; - // 读锁状态是共享,同一时刻可以有多个线程 同时进入到 TreeBi对象中获取数据,每一个线程都给 lockStat + 4 + // 读锁状态是共享,同一时刻可以有多个线程 同时进入到 TreeBi 对象中获取数据,每一个线程都给 lockStat + 4 static final int READER = 4; } ``` @@ -7860,15 +8591,15 @@ B站视频解析:https://www.bilibili.com/video/BV1n541177Ea * 变量: ```java - // 表示sizeCtl属性在ConcurrentHashMap中内存偏移地址 + // 表示sizeCtl属性在 ConcurrentHashMap 中内存偏移地址 private static final long SIZECTL; - // 表示transferIndex属性在ConcurrentHashMap中内存偏移地址 + // 表示transferIndex属性在 ConcurrentHashMap 中内存偏移地址 private static final long TRANSFERINDEX; - // 表示baseCount属性在ConcurrentHashMap中内存偏移地址 + // 表示baseCount属性在 ConcurrentHashMap 中内存偏移地址 private static final long BASECOUNT; - // 表示cellsBusy属性在ConcurrentHashMap中内存偏移地址 + // 表示cellsBusy属性在 ConcurrentHashMap 中内存偏移地址 private static final long CELLSBUSY; - // 表示cellValue属性在CounterCell中内存偏移地址 + // 表示cellValue属性在 CounterCell 中内存偏移地址 private static final long CELLVALUE; // 表示数组第一个元素的偏移地址 private static final long ABASE; @@ -8790,6 +9521,8 @@ ConcurrentHashMap 使用 get() 方法获取指定 key 的数据 +参考视频:https://space.bilibili.com/457326371/ + *** @@ -8800,7 +9533,7 @@ ConcurrentHashMap 使用 get() 方法获取指定 key 的数据 ConcurrentHashMap 对锁粒度进行了优化,**分段锁技术**,将整张表分成了多个数组(Segment),每个数组又是一个类似 HashMap 数组的结构。允许多个修改操作并发进行,Segment 是一种可重入锁,继承 ReentrantLock,并发时锁住的是每个 Segment,其他 Segment 还是可以操作的,这样不同 Segment 之间就可以实现并发,大大提高效率。 -底层结构: **Segment 数组 + HashEntry 数组 + 链表**(数组+链表是HashMap的结构) +底层结构: **Segment 数组 + HashEntry 数组 + 链表**(数组 + 链表是 HashMap 的结构) * 优点:如果多个线程访问不同的 segment,实际是没有冲突的,这与 jdk8 中是类似的 diff --git a/Tool.md b/Tool.md index abb3ea0..a82cb45 100644 --- a/Tool.md +++ b/Tool.md @@ -4,11 +4,11 @@ ### 版本系统 -SVN是集中式版本控制系统,版本库是集中放在中央服务器的,而开发人员工作的时候,用的都是自己的电脑,所以首先要从中央服务器下载最新的版本,然后开发,开发完后,需要把自己开发的代码提交到中央服务器。 +SVN 是集中式版本控制系统,版本库是集中放在中央服务器的,而开发人员工作的时候,用的都是自己的电脑,所以首先要从中央服务器下载最新的版本,然后开发,开发完后,需要把自己开发的代码提交到中央服务器。 集中式版本控制工具缺点:服务器单点故障、容错性差 -Git是分布式版本控制系统(Distributed Version Control System,简称 DVCS) ,分为两种类型的仓库: +Git 是分布式版本控制系统(Distributed Version Control System,简称 DVCS) ,分为两种类型的仓库: 本地仓库和远程仓库: @@ -25,7 +25,7 @@ Git是分布式版本控制系统(Distributed Version Control System,简称 1.从远程仓库中克隆代码到本地仓库 -2.从本地仓库中checkout代码然后进行代码修改 +2.从本地仓库中 checkout 代码然后进行代码修改 3.在提交前先将代码提交到**暂存区** @@ -43,13 +43,13 @@ Git是分布式版本控制系统(Distributed Version Control System,简称 ### 代码托管 -Git中存在两种类型的仓库,即本地仓库和远程仓库。那么我们如何搭建Git远程仓库呢?我们可以借助互联网上提供的一些代码托管服务来实现,其中比较常用的有GitHub、码云、GitLab等。 +Git 中存在两种类型的仓库,即本地仓库和远程仓库。那么我们如何搭建Git远程仓库呢?我们可以借助互联网上提供的一些代码托管服务来实现,其中比较常用的有 GitHub、码云、GitLab 等。 -gitHub( 地址:https://github.com/ )是一个面向开源及私有软件项目的托管平台,因为只支持Git 作为唯一的版本库格式进行托管,故名gitHub +GitHub(地址:https://github.com/)是一个面向开源及私有软件项目的托管平台,因为只支持 Git 作为唯一的版本库格式进行托管,故名 GitHub -码云(地址: https://gitee.com/ )是国内的一个代码托管平台,由于服务器在国内,所以相比于GitHub,码云速度会更快 +码云(地址: https://gitee.com/)是国内的一个代码托管平台,由于服务器在国内,所以相比于 GitHub,码云速度会更快 -GitLab (地址: https://about.gitlab.com/ )是一个用于仓库管理系统的开源项目,使用Git作为代码管理工具,并在此基础上搭建起来的web服务 +GitLab(地址: https://about.gitlab.com/ )是一个用于仓库管理系统的开源项目,使用 Git 作为代码管理工具,并在此基础上搭建起来的 web 服务 @@ -61,17 +61,19 @@ GitLab (地址: https://about.gitlab.com/ )是一个用于仓库管理系 ## 环境配置 -安装Git后首先要设置用户名称和email地址,因为每次Git提交都会使用该用户信息。此信息和我们注册的代码托管平台的信息无关。 +安装 Git 后首先要设置用户名称和 email 地址,因为每次 Git 提交都会使用该用户信息,此信息和注册的代码托管平台的信息无关 设置用户信息: - git config --global user.name “Seazean” - git config --global user.email “zhyzhyang@sina.com” //用户名和邮箱可以随意填写,不会校对 + +* git config --global user.name “Seazean” +* git config --global user.email “zhyzhyang@sina.com” //用户名和邮箱可以随意填写,不会校对 查看配置信息: - git config --list - git config user.name -通过上面的命令设置的信息会保存在用户目录下/.gitconfig文件中 +* git config --list +* git config user.name + +通过上面的命令设置的信息会保存在用户目录下 /.gitconfig 文件中 @@ -85,27 +87,27 @@ GitLab (地址: https://about.gitlab.com/ )是一个用于仓库管理系 * **本地仓库初始化** - 1. 在电脑的任意位置创建一个空目录(例如repo1)作为本地 Git 仓库 + 1. 在电脑的任意位置创建一个空目录(例如 repo1)作为本地 Git 仓库 2. 进入这个目录中,点击右键打开 Git bash 窗口 3. 执行命令 **git init** - 如果在当前目录中看到.git文件夹(此文件夹为隐藏文件夹)则说明Git仓库创建成功 + 如果在当前目录中看到 .git 文件夹(此文件夹为隐藏文件夹)则说明 Git 仓库创建成功 * **远程仓库克隆** 通过 Git 提供的命令从远程仓库进行克隆,将远程仓库克隆到本地 - 命令:git clone 远程 Git 仓库地址(HTTPS或者SSH) + 命令:git clone 远程 Git 仓库地址(HTTPS 或者 SSH) -* 生成SSH公钥步骤 +* 生成 SSH 公钥步骤 * 设置账户 * cd ~/.ssh(查看是否生成过SSH公钥)//user目录下 - * 生成SSH公钥:`ssh-keygen –t rsa –C "email"` - * -t 指定密钥类型,默认是 rsa ,可以省略。 - * -C 设置注释文字,比如邮箱。 - * -f 指定密钥文件存储文件名。 + * 生成 SSH 公钥:`ssh-keygen –t rsa –C "email"` + * -t 指定密钥类型,默认是 rsa ,可以省略 + * -C 设置注释文字,比如邮箱 + * -f 指定密钥文件存储文件名 * 查看命令: cat ~/.ssh/id_rsa.pub * 公钥测试命令: ssh -T git@gitee.com @@ -124,11 +126,11 @@ GitLab (地址: https://about.gitlab.com/ )是一个用于仓库管理系 ![](https://gitee.com/seazean/images/raw/master/Tool/Git基本工作流程.png) -版本库:.git隐藏文件夹就是版本库,版本库中存储了很多配置信息、日志信息和文件版本信息等 +版本库:.git 隐藏文件夹就是版本库,版本库中存储了很多配置信息、日志信息和文件版本信息等 -工作目录(工作区):包含.git文件夹的目录就是工作目录,主要用于存放开发的代码 +工作目录(工作区):包含 .git 文件夹的目录就是工作目录,主要用于存放开发的代码 -暂存区:.git文件夹中有很多文件,其中有一个index文件就是暂存区,也可以叫做stage。暂存区是一个临时保存修改文件的地方 +暂存区:.git 文件夹中有很多文件,其中有一个 index 文件就是暂存区,也可以叫做 stage,暂存区是一个临时保存修改文件的地方 ![](https://gitee.com/seazean/images/raw/master/Tool/文件流程图.png) @@ -155,7 +157,7 @@ GitLab (地址: https://about.gitlab.com/ )是一个用于仓库管理系 | git log | 查看日志( git 提交的历史日志) | | git reflog | 可以查看所有分支的所有操作记录(包括已经被删除的 commit 记录的操作) | -**其他指令**:可以跳过暂存区域直接从分支中取出修改,或者直接提交修改到分支中。 +**其他指令**:可以跳过暂存区域直接从分支中取出修改,或者直接提交修改到分支中 * git commit -a 直接把所有文件的修改添加到暂存区然后执行提交 * git checkout HEAD -- files 取出最后一次修改,可以用来进行回滚操作 @@ -168,15 +170,14 @@ GitLab (地址: https://about.gitlab.com/ )是一个用于仓库管理系 #### 文件状态 -* Git工作目录下的文件存在两种状态: +* Git 工作目录下的文件存在两种状态: * untracked 未跟踪(未被纳入版本控制) * tracked 已跟踪(被纳入版本控制) * Unmodified 未修改状态 * Modified 已修改状态 * Staged 已暂存状态 -* 查看文件状态 - 文件的状态会随着我们执行Git的命令发生变化 +* 查看文件状态:文件的状态会随着我们执行 Git 的命令发生变化 * git status 查看文件状态 * git status –s 查看更简洁的文件状态 @@ -217,10 +218,11 @@ doc/**/*.pdf ### 工作流程 -git有四个工作空间的概念,分别为 工作空间、暂存区、本地仓库、远程仓库。 +Git 有四个工作空间的概念,分别为 工作空间、暂存区、本地仓库、远程仓库。 pull = fetch + merge -fetch是从远程仓库更新到本地仓库,pull是从远程仓库直接更新到工作空间中 + +fetch 是从远程仓库更新到本地仓库,pull是从远程仓库直接更新到工作空间中 ![](https://gitee.com/seazean/images/raw/master/Tool/图解远程仓库工作流程.png) @@ -228,6 +230,10 @@ fetch是从远程仓库更新到本地仓库,pull是从远程仓库直接更 +*** + + + ### 查看仓库 git remote:显示所有远程仓库的简写 @@ -240,7 +246,7 @@ git remote show :显示某个远程仓库的详细信息 ### 添加仓库 -git remote add :添加一个新的远程仓库,并指定一个可以引用的简写 +git remote add :添加一个新的远程仓库,并指定一个可以引用的简写 @@ -248,7 +254,7 @@ git remote add :添加一个新的远程仓库,并指定一 git clone (HTTPS or SSH):克隆远程仓库 -Git 克隆的是该 Git 仓库服务器上的几乎所有数据(包括日志信息、历史记录等),而不仅仅是复制工作所需要的文件。 当你执行 git clone 命令的时候,默认配置下远程 Git 仓库中的每一个文件的每一个版本都将被拉取下来。 +Git 克隆的是该 Git 仓库服务器上的几乎所有数据(包括日志信息、历史记录等),而不仅仅是复制工作所需要的文件,当你执行 git clone 命令的时候,默认配置下远程 Git 仓库中的每一个文件的每一个版本都将被拉取下来。 @@ -260,17 +266,17 @@ git remote rm :移除远程仓库,从本地移除远程仓库的 ### 拉取仓库 -git fetch 从远程仓库获取最新版本到本地仓库,不会自动merge +git fetch :从远程仓库获取最新版本到本地仓库,不会自动 merge -git pull 从远程仓库获取最新版本并merge到本地仓库 +git pull :从远程仓库获取最新版本并 merge 到本地仓库 -注意:如果当前本地仓库不是从远程仓库克隆,而是本地创建的仓库,并且**仓库中存在文件**,此时再从远程仓库拉取文件的时候会报错(fatal: refusing to merge unrelated histories ),解决此问题可以在git pull命令后加入参数--allow-unrelated-histories +注意:如果当前本地仓库不是从远程仓库克隆,而是本地创建的仓库,并且**仓库中存在文件**,此时再从远程仓库拉取文件的时候会报错(fatal: refusing to merge unrelated histories ),解决此问题可以在 git pull 命令后加入参数 --allow-unrelated-histories ### 推送仓库 -git push 上传本地指定分支到远程仓库 +git push :上传本地指定分支到远程仓库 @@ -280,8 +286,6 @@ git push 上传本地指定分支到远程仓库 - - ## 版本管理 ![](https://gitee.com/seazean/images/raw/master/Tool/版本切换.png) @@ -290,43 +294,49 @@ git push 上传本地指定分支到远程仓库 + + +*** + + + ## 分支管理 ### 查看分支 -git branch 列出所有本地分支 +git branch:列出所有本地分支 -git branch -r 列出所有远程分支 +git branch -r:列出所有远程分支 -git branch -a 列出所有本地分支和远程分支 +git branch -a:列出所有本地分支和远程分支 ### 创建分支 -git branch branch-name 新建一个分支,但依然停留在当前分支 +git branch branch-name:新建一个分支,但依然停留在当前分支 -git checkout -b branch-name新建一个分支,并切换到该分支 +git checkout -b branch-name:新建一个分支,并切换到该分支 ### 推送分支 -git push origin branch-name 推送到远程仓库,origin是引用名 +git push origin branch-name:推送到远程仓库,origin 是引用名 ### 切换分支 -git checkout branch-name 切换到branch-name分支 +git checkout branch-name:切换到branch-name分支 ### 合并分支 -git merge branch-name 合并指定分支到当前分支 +git merge branch-name:合并指定分支到当前分支 -有时候合并操作不会如此顺利。 如果你在两个不同的分支中,对同一个文件的同一个部分进行了不同的修改,Git 就没办法合并它们,同时会提示文件冲突。此时需要我们打开冲突的文件并修复冲突内容,最后执行git add命令来标识冲突已解决 +有时候合并操作不会如此顺利。 如果你在两个不同的分支中,对同一个文件的同一个部分进行了不同的修改,Git 就没办法合并它们,同时会提示文件冲突。此时需要我们打开冲突的文件并修复冲突内容,最后执行 git add 命令来标识冲突已解决 ​ ![](https://gitee.com/seazean/images/raw/master/Tool/合并分支冲突.png) @@ -334,11 +344,11 @@ git merge branch-name 合并指定分支到当前分支 ### 删除分支 -git branch -d branch-name 删除分支 +git branch -d branch-name:删除分支 -git push origin –d branch-name 删除远程仓库中的分支 (origin是引用名) +git push origin –d branch-name:删除远程仓库中的分支 (origin 是引用名) -如果要删除的分支中进行了开发动作,此时执行删除命令并不会删除分支,如果坚持要删除此分支,可以将命令中的-d参数改为-D git branch -D branch-name +如果要删除的分支中进行了开发动作,此时执行删除命令并不会删除分支,如果坚持要删除此分支,可以将命令中的 -d 参数改为 -D:git branch -D branch-name @@ -350,9 +360,9 @@ git push origin –d branch-name 删除远程仓库中的分支 (origin是 ### 查看标签 -git tag:列出所有tag +git tag:列出所有 tag -git show tag-name:查看tag详细信息 +git show tag-name:查看 tag 详细信息 标签作用:在开发的一些关键时期,使用标签来记录这些关键时刻,保存快照,例如发布版本、有重大修改、升级的时候、会使用标签记录这些时刻,来永久标记项目中的关键历史时刻 @@ -382,7 +392,7 @@ git checkout tag-name:切换标签 git tag -d tag-name:删除本地标签 -git push origin :refs/tags/ tag-name :删除远程标签 +git push origin :refs/tags/ tag-name:删除远程标签 @@ -394,15 +404,15 @@ git push origin :refs/tags/ tag-name :删除远程标签 ### 环境配置 -File→Settings打开设置窗口,找到Version Control下的git选项 +File → Settings 打开设置窗口,找到 Version Control 下的 git 选项 -选择git的安装目录后可以点击“Test”按钮测试是否正确配置:D:\Program Files\Git\cmd\git.exe +选择 git 的安装目录后可以点击 Test 按钮测试是否正确配置:D:\Program Files\Git\cmd\git.exe ### 创建仓库 -1、VCS -> Import into Version Control -> Create Git Repository +1、VCS → Import into Version Control → Create Git Repository 2、选择工程所在的目录,这样就创建好本地仓库了 @@ -414,7 +424,7 @@ File→Settings打开设置窗口,找到Version Control下的git选项 ### 文件操作 -右键项目名打开菜单Git -> Add -> commit +右键项目名打开菜单 Git → Add → commit @@ -423,11 +433,9 @@ File→Settings打开设置窗口,找到Version Control下的git选项 * 版本对比 ![](https://gitee.com/seazean/images/raw/master/Tool/版本对比.png) -* 版本切换方式一:控制台Version Control->Log->右键Reset Current Branch...->Reset - 这种切换的特点是会抛弃原来的提交记录 +* 版本切换方式一:控制台 Version Control → Log → 右键 Reset Current Branch → Reset,这种切换会抛弃原来的提交记录 ![](https://gitee.com/seazean/images/raw/master/Tool/版本切换方式一.png) -* 版本切换方式二:控制台Version Control->Log->Revert Commit->Merge->处理代码->commit - 这种切换的特点是会当成一个新的提交记录,之前的提交记录也都保留 +* 版本切换方式二:控制台 Version Control → Log → Revert Commit → Merge → 处理代码 → commit,这种切换会当成一个新的提交记录,之前的提交记录也都保留 ![](https://gitee.com/seazean/images/raw/master/Tool/版本切换方式二.png) ​ ![](https://gitee.com/seazean/images/raw/master/Tool/版本切换方式二(1).png) @@ -436,30 +444,38 @@ File→Settings打开设置窗口,找到Version Control下的git选项 +*** + + + ### 分支管理 -* 创建分支 - VCS->Git->Branches->New Branch->给分支起名字->ok -* 切换分支 - idea右下角Git->选择要切换的分支->checkout -* 合并分支 - VCS->Git->Merge changes->选择要合并的分支->merge -* 删除分支 - idea右下角->选中要删除的分支->Delete +* 创建分支:VCS → Git → Branches → New Branch → 给分支起名字 → ok +* 切换分支:idea 右下角 Git → 选择要切换的分支 → checkout +* 合并分支:VCS → Git → Merge changes → 选择要合并的分支 → merge +* 删除分支:idea 右下角 → 选中要删除的分支 → Delete +*** + ### 推送仓库 -1. VCS->Git->Push->点击master Define remote -2. 将远程仓库的url路径复制过来->Push +1. VCS → Git → Push → 点击 master Define remote +2. 将远程仓库的 url 路径复制过来 → Push ![](https://gitee.com/seazean/images/raw/master/Tool/本地仓库推送到远程仓库.png) + + +*** + + + ### 克隆仓库 -File->Close Project->Checkout from Version Control->Git->指定远程仓库的路径->指定本地存放的路径->clone +File → Close Project → Checkout from Version Control → Git → 指定远程仓库的路径 → 指定本地存放的路径 → clone ![](https://gitee.com/seazean/images/raw/master/Tool/远程仓库克隆到本地仓库.png) @@ -479,15 +495,13 @@ File->Close Project->Checkout from Version Control->Git->指定远程仓库的 ## 操作系统 -操作系统(Operation System, OS),是管理计算机硬件与软件资源的计算机程序,同时也是计算机系统的内核与基石。操作系统需要处理管理与配置内存、决定系统资源供需的优先次序、控制输入设备与输出设备、操作网络与管理文件系统等基本事务,操作系统也提供一个让用户与系统交互的操作界面。 +操作系统(Operation System),是管理计算机硬件与软件资源的计算机程序,同时也是计算机系统的内核与基石。操作系统需要处理管理与配置内存、决定系统资源供需的优先次序、控制输入设备与输出设备、操作网络与管理文件系统等基本事务,操作系统也提供一个让用户与系统交互的操作界面 -操作系统作为接口的示意图: +操作系统作为接口的示意图: - - -**移动设备操作系统** +移动设备操作系统: ![](https://gitee.com/seazean/images/raw/master/Tool/移动设备操作系统.png) From fccbd53de5042f2756e7d00c5fd812fb82f1f9ba Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 12 Aug 2021 23:05:36 +0800 Subject: [PATCH 019/168] Update Java Notes --- DB.md | 36 +++++------ Java.md | 57 ++++++++++------- Prog.md | 187 +++++++++++++++++++++++++++++--------------------------- SSM.md | 91 +++++++++++++++++++++------ 4 files changed, 223 insertions(+), 148 deletions(-) diff --git a/DB.md b/DB.md index 6c85b72..a845ac1 100644 --- a/DB.md +++ b/DB.md @@ -4837,17 +4837,17 @@ LOAD DATA LOCAL INFILE = '/home/seazean/sql1.log' INTO TABLE `tb_user_1` FIELD T 对于 InnoDB 类型的表,有以下几种方式可以提高导入的效率: -1. 主键顺序插入:因为InnoDB类型的表是按照主键的顺序保存的,所以将导入的数据按照主键的顺序排列,可以有效的提高导入数据的效率,如果InnoDB表没有主键,那么系统会自动默认创建一个内部列作为主键。 +1. 主键顺序插入:因为 InnoDB 类型的表是按照主键的顺序保存的,所以将导入的数据按照主键的顺序排列,可以有效的提高导入数据的效率,如果 InnoDB 表没有主键,那么系统会自动默认创建一个内部列作为主键。 - * 插入ID顺序排列数据: + * 插入 ID 顺序排列数据: ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL插入ID顺序排列数据.png) - * 插入ID无序排列数据: + * 插入 ID 无序排列数据: ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL插入ID无序排列数据.png) -2. 关闭唯一性校验:在导入数据前执行`SET UNIQUE_CHECKS=0`,关闭唯一性校验;导入结束后执行SET UNIQUE_CHECKS=1,恢复唯一性校验,可以提高导入的效率。 +2. 关闭唯一性校验:在导入数据前执行 `SET UNIQUE_CHECKS=0`,关闭唯一性校验;导入结束后执行 `SET UNIQUE_CHECKS=1`,恢复唯一性校验,可以提高导入的效率。 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL插入数据关闭唯一性校验.png) @@ -6436,7 +6436,7 @@ SELECT * FROM tb_book WHERE id < 8 慢查询日志记录所有执行时间超过 long_query_time 并且扫描记录数不小于 min_examined_row_limit 的所有的 SQL 语句的日志。long_query_time 默认为 10 秒,最小为 0, 精度到微秒 -慢查询日志默认是关闭的,可以通过两个参数来控制慢查询日志,配置文件`/etc/mysql/my.cnf`: +慢查询日志默认是关闭的,可以通过两个参数来控制慢查询日志,配置文件 `/etc/mysql/my.cnf`: ```sh # 该参数用来控制慢查询日志是否开启,可选值0或者1,0代表关闭,1代表开启 @@ -6459,7 +6459,7 @@ long_query_time=10 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-慢日志读取1.png) -* 如果慢查询日志内容很多,直接查看文件比较繁琐, 可以借助于mysql 自带的 mysqldumpslow 工具, 来对慢查询日志进行分类汇总: +* 如果慢查询日志内容很多,直接查看文件比较繁琐,可以借助 mysql 自带的 mysqldumpslow 工具对慢查询日志进行分类汇总: ```sh mysqldumpslow slow_query.log @@ -6506,14 +6506,14 @@ long_query_time=10 作用:遵守第二范式减少数据冗余,通过主键区分相同数据。 -1. 函数依赖:A --> B,如果通过A属性(属性组)的值,可以确定唯一B属性的值,则称B依赖于A - * 学号 --> 姓名;(学号,课程名称) --> 分数 -2. 完全函数依赖:A --> B,如果A是一个属性组,则B属性值的确定需要依赖于A属性组的所有属性值 - * (学号,课程名称) --> 分数 -3. 部分函数依赖:A --> B,如果A是一个属性组,则B属性值的确定只需要依赖于A属性组的某些属性值 - * (学号,课程名称) --> 姓名 -4. 传递函数依赖:A --> B,B --> C,如果通过A属性(属性组)的值,可以确定唯一B属性的值,在通过B属性(属性组)的值,可以确定唯一C属性的值,则称C传递函数依赖于A - * 学号 --> 系名,系名 --> 系主任 +1. 函数依赖:A → B,如果通过 A 属性(属性组)的值,可以确定唯一 B 属性的值,则称 B 依赖于 A + * 学号 → 姓名;(学号,课程名称) → 分数 +2. 完全函数依赖:A → B,如果A是一个属性组,则 B 属性值的确定需要依赖于 A 属性组的所有属性值 + * (学号,课程名称) → 分数 +3. 部分函数依赖:A → B,如果 A 是一个属性组,则 B 属性值的确定只需要依赖于 A 属性组的某些属性值 + * (学号,课程名称) → 姓名 +4. 传递函数依赖:A → B,B → C,如果通过A属性(属性组)的值,可以确定唯一 B 属性的值,在通过 B 属性(属性组)的值,可以确定唯一 C 属性的值,则称 C 传递函数依赖于 A + * 学号 → 系名,系名 → 系主任 5. 码:如果在一张表中,一个属性或属性组,被其他所有属性所完全依赖,则称这个属性(属性组)为该表的码 * 该表中的码:(学号,课程名称) * 主属性:码属性组中的所有属性 @@ -6848,9 +6848,9 @@ public class JDBCDemo01 { 从数据库读取数据并封装成Student对象,需要: -- Student类成员变量对应表中的列 +- Student 类成员变量对应表中的列 -- 所有的基本数据类型需要使用包装类,**以防null值无法赋值** +- 所有的基本数据类型需要使用包装类,**以防 null 值无法赋值** ```java public class Student { @@ -6990,12 +6990,12 @@ SQL注入攻击演示 ### 攻击解决 -PreparedStatement:预编译 sql 语句的执行者对象,继承`PreparedStatement extends Statement` +PreparedStatement:预编译 sql 语句的执行者对象,继承 `PreparedStatement extends Statement` * 在执行 sql 语句之前,将 sql 语句进行提前编译。明确 sql 语句的格式,剩余的内容都会认为是参数 * sql 语句中的参数使用 ? 作为占位符 -为 ? 占位符赋值的方法:setXxx(参数1,参数2) +为 ? 占位符赋值的方法:`setXxx(int parameterIndex, xxx data)` - 参数1:? 的位置编号(编号从1开始) diff --git a/Java.md b/Java.md index 51df33e..c55c816 100644 --- a/Java.md +++ b/Java.md @@ -2489,7 +2489,7 @@ s.replace("-","");//12378 * `public String(char[] chs)` : 根据字符数组的内容,来创建字符串对象 * `public String(String original)` : 根据传入的字符串内容,来创建字符串对象 -直接赋值:`String s = “abc”` 直接赋值的方式创建字符串对象,内容就是abc +直接赋值:`String s = “abc”` 直接赋值的方式创建字符串对象,内容就是 abc - 通过构造方法创建:通过 new 创建的字符串对象,每一次 new 都会申请一个内存空间,虽然内容相同,但是地址值不同,**返回堆内存中对象的引用** - 直接赋值方式创建:以“ ”方式给出的字符串,只要字符序列相同(顺序和大小写),无论在程序代码中出现几次,JVM 都只会**在 String Pool 中创建一个字符串对象**,并在字符串池中维护 @@ -2497,7 +2497,7 @@ s.replace("-","");//12378 `String str = new String("abc")`创建字符串对象: * 创建一个对象:字符串池中已经存在"abc"对象,那么直接在创建一个对象放入堆中,返回堆内引用 -* 创建两个对象:字符串池中未找到"abc"对象,那么分别在堆中和字符串池中创建一个对象,字符串池中的比较都是采用 equals() 方法 +* 创建两个对象:字符串池中未找到"abc"对象,那么分别在堆中和字符串池中创建一个对象,字符串池中的比较都是采用 equals() `new String("a") + new String("b")`创建字符串对象: @@ -2509,7 +2509,7 @@ s.replace("-","");//12378 * 对象4:new String("b")、对象5:常量池中的"b" -* StringBuilder的toString(): +* StringBuilder 的 toString(): ```java @Override @@ -2533,9 +2533,9 @@ s.replace("-","");//12378 **字符串常量池(String Pool / StringTable / 串池)**保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定,常量池类似于Java系统级别提供的**缓存**,存放对象和引用 -* StringTable,hashtable (哈希表 + 链表)结构,不能扩容,默认值大小长度是1009 +* StringTable,hashtable (哈希表 + 链表)结构,不能扩容,默认值大小长度是 1009 * 常量池中的字符串仅是符号,第一次使用时才变为对象,可以避免重复创建字符串对象 -* 字符串**变量**的拼接的原理是StringBuilder(jdk1.8),append 效率要比字符串拼接高很多 +* 字符串**变量**的拼接的原理是 StringBuilder(jdk1.8),append 效率要比字符串拼接高很多 * 字符串**常量**拼接的原理是编译期优化,结果在常量池 * 可以使用 String 的 intern() 方法在运行过程将字符串添加到 String Pool 中 @@ -2564,7 +2564,7 @@ public class Demo { String s2 = "b"; String s3 = "ab";//串池 // new StringBuilder().append("a").append("b").toString() new String("ab") - String s4 = s1 + s2; // 堆内地址 + String s4 = s1 + s2; // 返回的是堆内地址 String s5 = "a" + "b"; // javac 在编译期间的优化,结果已经在编译期确定为ab System.out.println(s3 == s4); // false @@ -2586,8 +2586,8 @@ public class Demo { 结论: ```java -String s1 = "ab"; //串池 -String s2 = new String("a") + new String("b"); //堆 +String s1 = "ab"; //放入串池 +String s2 = new String("a") + new String("b"); //放入堆 //上面两条指令的结果和下面的 效果 相同 String s = new String("ab"); ``` @@ -2621,7 +2621,7 @@ public static void main(String[] args) { ```java public static void main(String[] args) { String str1 = new StringBuilder("58").append("tongcheng").toString(); - System.out.println(str1 == str1.intern());//true + System.out.println(str1 == str1.intern());//true,字符串池中不存在,把堆中的引用复制一份放入串池 String str2 = new StringBuilder("ja").append("va").toString(); System.out.println(str2 == str2.intern());//false @@ -2743,13 +2743,13 @@ public class Demo1_25 { #### 不可变好处 * 可以缓存 hash 值 - 因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,只需要进行一次计算 + String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,只要进行一次计算 * String Pool 的需要 - 如果一个String对象已经被创建过了,就会从 String Pool中取得引用。只有 String是不可变的,才可能使用 String Pool + 如果一个String对象已经被创建过了,就会从 String Pool 中取得引用,只有 String是不可变的,才可能使用 String Pool * 安全性 String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 的那一方以为现在连接的是其它主机,而实际情况却不一定是 * String 不可变性天生具备线程安全,可以在多个线程中安全地使用 -* 防止子类继承,破坏String的API的使用 +* 防止子类继承,破坏 String 的 API 的使用 @@ -2765,7 +2765,7 @@ String StringBuffer 和 StringBuilder 区别: * String : **不可变**的字符序列,线程安全 * StringBuffer : **可变**的字符序列,线程安全,底层方法加 synchronized,效率低 -* StringBuilder : **可变**的字符序列,JDK5.0新增;线程不安全,效率高 +* StringBuilder : **可变**的字符序列,JDK5.0 新增;线程不安全,效率高 相同点:底层使用 char[] 存储 @@ -3942,6 +3942,10 @@ public static void main(String[] args){ +*** + + + ###### 源码 ArrayList 实现类集合底层**基于数组存储数据**的,查询快,增删慢,支持快速随机访问 @@ -3970,7 +3974,7 @@ public class ArrayList extends AbstractList } ``` - 当add 第 1 个元素到 ArrayList,size 是 0,进入 ensureCapacityInternal 方法, + 当 add 第 1 个元素到 ArrayList,size 是 0,进入 ensureCapacityInternal 方法, ```java private void ensureCapacityInternal(int minCapacity) { @@ -4043,12 +4047,17 @@ public class ArrayList extends AbstractList * 删除元素:需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,在旧数组上操作,该操作的时间复杂度为 O(N),可以看到 ArrayList 删除元素的代价是非常高的, ```java - private void fastRemove(Object[] es, int i) { + public E remove(int index) { + rangeCheck(index); modCount++; - final int newSize; - if ((newSize = size - 1) > i) - System.arraycopy(es, i + 1, es, i, newSize - i); - es[size = newSize] = null; + E oldValue = elementData(index); + + int numMoved = size - index - 1; + if (numMoved > 0) + System.arraycopy(elementData, index+1, elementData, index, numMoved); + elementData[--size] = null; // clear to let GC do its work + + return oldValue; } ``` @@ -4073,7 +4082,7 @@ public class ArrayList extends AbstractList * **Fail-Fast**:快速失败,modCount 用来记录 ArrayList **结构发生变化**的次数,结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化 - 在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了抛出 ConcurrentModificationException异常 + 在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,改变了抛出 ConcurrentModificationException 异常 @@ -4626,7 +4635,11 @@ JDK7 对比 JDK8: * **当链表长度超过(大于)阈值(或者红黑树的边界值,默认为 8)并且当前数组的长度大于等于64时,此索引位置上的所有数据改为红黑树存储** * 即使哈希函数取得再好,也很难达到元素百分百均匀分布。当 HashMap 中有大量的元素都存放到同一个桶中时,就相当于一个长的单链表,假如单链表有 n 个元素,遍历的**时间复杂度是 O(n)**,所以 JDK1.8 中引入了 红黑树(查找**时间复杂度为 O(logn)**)来优化这个问题,使得查找效率更高 - ![](https://gitee.com/seazean/images/raw/master/Java/哈希表.png) + ![](https://gitee.com/seazean/images/raw/master/Java/HashMap底层结构.png) + + + +参考视频:https://www.bilibili.com/video/BV1nJ411J7AA @@ -9774,7 +9787,7 @@ Heap 堆:是JVM内存中最大的一块,由所有线程共享,由垃圾回 * 对象实例:类初始化生成的对象,**基本数据类型的数组也是对象实例**,new 创建对象都使用堆内存 * 字符串常量池: * 字符串常量池原本存放于方法区,jdk7开始放置于堆中 - * 字符串常量池**存储的是 string 对象的直接引用或者对象**,是一张 string table + * 字符串常量池**存储的是 String 对象的直接引用或者对象**,是一张 string table * 静态变量:静态变量是有 static 修饰的变量,jdk7 时从方法区迁移至堆中 * 线程分配缓冲区 Thread Local Allocation Buffer:线程私有但不影响堆的共性,可以提升对象分配的效率 diff --git a/Prog.md b/Prog.md index 6dc58f4..41bc5c7 100644 --- a/Prog.md +++ b/Prog.md @@ -271,7 +271,7 @@ Java 中 main 方法启动的是一个进程也是一个主线程,main 方法 -### 线程API +### 线程方法 #### API @@ -359,7 +359,7 @@ yield: public final void join():等待这个线程结束 -原理:调用者轮询检查线程 alive 状态,t1.join()等价于: +原理:调用者轮询检查线程 alive 状态,t1.join() 等价于: ```java synchronized (t1) { @@ -372,7 +372,7 @@ synchronized (t1) { * join 方法是被 synchronized 修饰的,本质上是一个对象锁,其内部的 wait 方法调用也是释放锁的,但是**释放的是当前线程的对象锁,而不是外面的锁** -* t1 会强占 CPU 资源,直至线程执行结束,当调用某个线程的 join 方法后,该线程抢占到 CPU 资源,就不再释放,直到线程执行完毕 +* t1 会强占 CPU 资源,当调用某个线程的 join 方法后,该线程抢占到 CPU 资源,就不再释放,直到线程执行完毕 线程同步: @@ -415,11 +415,13 @@ public class Test { ##### 打断线程 -`public void interrupt()`:中断这个线程,异常处理机制 -`public static boolean interrupted()`:判断当前线程是否被打断,打断返回 true,清除打断标记 +`public void interrupt()`:打断这个线程,异常处理机制 +`public static boolean interrupted()`:判断当前线程是否被打断,打断返回 true,清除打断标记,连续调用两次一定返回 false `public boolean isInterrupted()`:判断当前线程是否被打断,不清除打断标记 -* sleep、wait、join 方法都会让线程进入阻塞状态,打断进程**会清空打断状态** (false) +打断的线程会发生上下文切换,操作系统会保存线程信息,抢占到 CPU 后会从中断的地方接着运行 + +* sleep、wait、join 方法都会让线程进入阻塞状态,打断进程**会清空打断状态**(false) ```java public static void main(String[] args) throws InterruptedException { @@ -473,7 +475,7 @@ public static void main(String[] args) throws Exception { System.out.println("park..."); LockSupport.park(); System.out.println("unpark..."); - Sout("打断状态:" + Thread.currentThread().isInterrupted());//打断状态:true + Sout("打断状态:" + Thread.currentThread().isInterrupted()); //打断状态:true }, "t1"); t1.start(); Thread.sleep(2000); @@ -481,7 +483,7 @@ public static void main(String[] args) throws Exception { } ``` -如果打断标记已经是 true, 则 park 会失效, +如果打断标记已经是 true, 则 park 会失效 ```java LockSupport.park(); @@ -542,7 +544,7 @@ class TwoPhaseTermination { try { Thread.sleep(1000);//睡眠 System.out.println("执行监控记录");//在此被打断不会异常 - } catch (InterruptedException e) {//在睡眠期间被打断 + } catch (InterruptedException e) {//在睡眠期间被打断,进入异常处理的逻辑 e.printStackTrace(); //重新设置打断标记 thread.interrupt(); @@ -587,13 +589,12 @@ t.start(); 守护线程:服务于用户线程,只要其它非守护线程运行结束了,即使守护线程代码没有执行完,也会强制结束 -说明:当运行的线程都是守护线程,Java 虚拟机将退出,因为普通线程执行完后,守护线程不会继续运行下去 +说明:当运行的线程都是守护线程,Java 虚拟机将退出,因为普通线程执行完后,JVM 是守护线程,不会继续运行下去 常见的守护线程: * 垃圾回收器线程就是一种守护线程 -* Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等 - 待它们处理完当前请求 +* Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求 @@ -601,7 +602,7 @@ t.start(); -#### 不推荐方法 +#### 不推荐 不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁: @@ -623,18 +624,18 @@ t.start(); 线程由生到死的完整过程(生命周期):当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态,在 API 中 `java.lang.Thread.State` 这个枚举中给出了六种线程状态: -| 线程状态 | 导致状态发生条件 | -| ----------------------- | ------------------------------------------------------------ | -| NEW(新建) | 线程刚被创建,但是并未启动,还没调用 start 方法,只有线程对象,没有线程特征 | -| Runnable(可运行) | 线程可以在 java 虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器,调用了 t.start() 方法:就绪(经典叫法) | -| Blocked(锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入 Blocked 状态;当该线程持有锁时,该线程将变成 Runnable 状态 | -| Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入 Waiting 状态,进入这个状态后不能自动唤醒,必须等待另一个线程调用 notify 或者 notifyAll 方法才能唤醒 | -| Timed Waiting(计时等待) | 有几个方法有超时参数,调用将进入 Timed Waiting 状态,这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有 Thread.sleep 、Object.wait | -| Teminated(被终止) | run 方法正常退出而死亡,或者因为没有捕获的异常终止了 run 方法而死亡 | +| 线程状态 | 导致状态发生条件 | +| ------------------------ | ------------------------------------------------------------ | +| NEW (新建) | 线程刚被创建,但是并未启动,还没调用 start 方法,只有线程对象,没有线程特征 | +| Runnable (可运行) | 线程可以在 java 虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器,调用了 t.start() 方法:就绪(经典叫法) | +| Blocked (锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入 Blocked 状态;当该线程持有锁时,该线程将变成 Runnable 状态 | +| Waiting (无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入 Waiting 状态,进入这个状态后不能自动唤醒,必须等待另一个线程调用 notify 或者 notifyAll 方法才能唤醒 | +| Timed Waiting (计时等待) | 有几个方法有超时参数,调用将进入 Timed Waiting 状态,这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有 Thread.sleep 、Object.wait | +| Teminated (被终止) | run 方法正常退出而死亡,或者因为没有捕获的异常终止了 run 方法而死亡 | ![](https://gitee.com/seazean/images/raw/master/Java/JUC-线程6种状态.png) -* NEW --> RUNNABLE:当调用 t.start() 方法时,由 NEW --> RUNNABLE +* NEW → RUNNABLE:当调用 t.start() 方法时,由 NEW → RUNNABLE * RUNNABLE <--> WAITING: @@ -642,10 +643,10 @@ t.start(); 调用 obj.notify()、obj.notifyAll()、t.interrupt(): - * 竞争锁成功,t 线程从 WAITING --> RUNNABLE - * 竞争锁失败,t 线程从 WAITING --> BLOCKED + * 竞争锁成功,t 线程从 WAITING → RUNNABLE + * 竞争锁失败,t 线程从 WAITING → BLOCKED - * 当前线程调用 t.join() 方法,注意是当前线程在t 线程对象的监视器上等待 + * 当前线程调用 t.join() 方法,注意是当前线程在 t 线程对象的监视器上等待 * 当前线程调用 LockSupport.park() 方法 @@ -744,8 +745,8 @@ synchronized 是可重入、不公平的重量级锁 原则上: * 锁对象建议使用共享资源 -* 在实例方法中建议用 this 作为锁对象,锁住的 this 正好是共享资源 -* 在静态方法中建议用类名 .class 字节码作为锁对象 +* 在实例方法中使用 this 作为锁对象,锁住的 this 正好是共享资源 +* 在静态方法中使用类名 .class 字节码作为锁对象 * 因为静态成员属于类,被所有实例对象共享,所以需要锁住类 * 锁住类以后,类的所有实例都相当于同一把锁,参考线程八锁 @@ -962,14 +963,14 @@ Mark Word 中就被设置指向 Monitor 对象的指针,这就是重量级锁 * 在 Thread-2 上锁的过程中,Thread-3、Thread-4、Thread-5 也来执行 synchronized(obj),就会进入 EntryList BLOCKED(双向链表) -* Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争是**非公平的** -* WaitSet 中的 Thread-0,是以前获得过锁,但条件不满足进入 WAITING 状态的线程(wait-notify) +* Thread-2 执行完同步代码块的内容,根据对象头中 Monitor 地址寻找,设置 Owner 为空,唤醒 EntryList 中等待的线程来竞争锁,竞争是**非公平的** +* WaitSet 中的 Thread-0,是以前获得过锁,但条件不满足进入 WAITING 状态的线程(wait-notify 机制) ![](https://gitee.com/seazean/images/raw/master/Java/JUC-Monitor工作原理2.png) 注意: -* synchronized 必须是进入同一个对象的 monitor 才有上述的效果 +* synchronized 必须是进入同一个对象的 Monitor 才有上述的效果 * 不加 synchronized 的对象不会关联监视器,不遵从以上规则 @@ -1081,13 +1082,11 @@ LocalVariableTable: * 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁 * 调用 wait/notify - - **批量撤销**:如果对象被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID -* 批量重偏向:当撤销偏向锁阈值超过 20 次后,jvm会觉得是不是偏向错了,于是在给这些对象加锁时重新偏向至加锁线程 +* 批量重偏向:当撤销偏向锁阈值超过 20 次后,jvm 会觉得是不是偏向错了,于是在给这些对象加锁时重新偏向至加锁线程 -* 批量撤销:当撤销偏向锁阈值超过 40 次后,jvm会觉得自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的 +* 批量撤销:当撤销偏向锁阈值超过 40 次后,jvm 会觉得自己确实偏向错了,根本就不该偏向,于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的 @@ -1120,27 +1119,27 @@ public static void method2() { } ``` -* 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,存储锁定对象的Mark Word +* 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,存储锁定对象的 Mark Word ![](https://gitee.com/seazean/images/raw/master/Java/JUC-轻量级锁原理1.png) -* 让锁记录中Object reference指向锁对象,并尝试用CAS替换Object的Mark Word,将Mark Word的值存 +* 让锁记录中 Object reference 指向锁对象,并尝试用 CAS 替换 Object 的 Mark Word,将 Mark Word 的值存 入锁记录 -* 如果CAS替换成功,对象头中存储了锁记录地址和状态 00(轻量级锁) ,表示由该线程给对象加锁 +* 如果 CAS 替换成功,对象头中存储了锁记录地址和状态 00(轻量级锁) ,表示由该线程给对象加锁 ![](https://gitee.com/seazean/images/raw/master/Java/JUC-轻量级锁原理2.png) -* 如果CAS失败,有两种情况: +* 如果 CAS 失败,有两种情况: * 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程 - * 如果是自己执行了 synchronized 锁重入,就添加一条 Lock Record 作为重入的计数 + * 如果是线程自己执行了 synchronized 锁重入,就添加一条 Lock Record 作为重入的计数 ![](https://gitee.com/seazean/images/raw/master/Java/JUC-轻量级锁原理3.png) * 当退出 synchronized 代码块(解锁时) - * 如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减1 - * 如果锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头 + * 如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减 1 + * 如果锁记录的值不为 null,这时使用 CAS 将 Mark Word 的值恢复给对象头 * 成功,则解锁成功 * 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程 @@ -1162,7 +1161,7 @@ public static void method2() { ![](https://gitee.com/seazean/images/raw/master/Java/JUC-重量级锁原理2.png) -* 当Thread-0退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败,这时进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程 +* 当 Thread-0 退出同步块解锁时,使用 CAS 将 Mark Word 的值恢复给对象头失败,这时进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程 @@ -1431,6 +1430,10 @@ class HoldLockThread implements Runnable { +*** + + + ###### 定位 定位死锁的方法: @@ -1554,7 +1557,7 @@ public final native void wait(long timeout):有时限的等待, 到n毫秒后结 * Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态 * BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片 * BLOCKED 线程会在 Owner 线程释放锁时唤醒 -* WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入 EntryList 重新竞争 +* WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,唤醒后并不意味者立刻获得锁,**需要进入 EntryList 重新竞争** ![](https://gitee.com/seazean/images/raw/master/Java/JUC-Monitor工作原理2.png) @@ -1675,7 +1678,7 @@ public static void main(String[] args) { LockSupport 出现就是为了增强 wait & notify 的功能: * wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park、unpark 不需要 -* park & unpark以线程为单位来阻塞和唤醒线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程 +* park & unpark 以线程为单位来阻塞和唤醒线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程 * **park & unpark 可以先 unpark**,而 wait & notify 不能先 notify。类比生产消费,先消费发现有产品就消费,没有就等待;先生产就直接产生商品,然后线程直接消费 * wait 会释放锁资源进入等待队列,park 不会释放锁资源,只负责阻塞当前线程,会释放 CPU @@ -4609,7 +4612,7 @@ public ThreadPoolExecutor(int corePoolSize, 2. 当调用 execute() 方法添加一个请求任务时,线程池会做如下判断: * 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务 * 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列 - * 如果这时队列满了且正在运行的线程数量还小于 maximumPoolSize,那么会创建非核心线程**立刻运行这个任务**(对于阻塞队列中的任务不公平) + * 如果这时队列满了且正在运行的线程数量还小于 maximumPoolSize,那么会创建非核心线程**立刻运行这个任务**,对于阻塞队列中的任务不公平。这是因为创建每个 Worker (线程)对象会绑定一个初始任务,启动 Worker 时会优先执行 * 如果队列满了且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会启动饱和**拒绝策略**来执行 3. 当一个线程完成任务时,会从队列中取下一个任务来执行 @@ -4746,7 +4749,7 @@ ExecutorService 类 API: execute 和 submit 都属于线程池的方法,对比: -* execute 只能提交 Runnable 类型的任务,没有返回值,而 submit 既能提交 Runnable 类型任务也能提交 Callable 类型任务 +* execute 只能提交 Runnable 类型的任务,没有返回值; submit 既能提交 Runnable 类型任务也能提交 Callable 类型任务,底层是封装成 FutureTask 调用 execute 执行 * execute 会直接抛出任务执行时的异常,submit 会吞掉异常,可通过 Future 的 get 方法将任务执行时的异常重新抛出 @@ -4971,11 +4974,9 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 内部类: -* Worker 类:**每个 Worker 对象会绑定一个初始任务**,启动 Worker 时优先执行 +* Worker 类:**每个 Worker 对象会绑定一个初始任务**,启动 Worker 时优先执行,这也是造成线程池不公平的原因。Worker 继承自 AQS,采用了独占锁的模式,state = 0 表示未被占用,> 0 表示被占用,< 0 表示初始状态,这种情况下不能被抢锁 ```java - // Worker 采用了AQS的独占模式,state = 0 表示未被占用,> 0 表示被占用,< 0 表示初始状态,这种情况下不能被抢锁 - // ExclusiveOwnerThread:表示独占锁的线程 private final class Worker extends AbstractQueuedSynchronizer implements Runnable { final Thread thread; // worker 内部封装的工作线程 Runnable firstTask; // worker 第一个执行的任务 @@ -4992,7 +4993,7 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 } } ``` - + * 拒绝策略相关的内部类 @@ -5007,7 +5008,7 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 ##### 提交方法 -* AbstractExecutorService#submit():提交任务的方法 +* AbstractExecutorService#submit():提交任务,把任务封装成 FutureTask 执行 ```java public Future submit(Runnable task) { @@ -5061,7 +5062,7 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 // SHUTDOWN 状态下也有可能创建成功,前提 firstTask == null 而且当前 queue 不为空(特殊情况) c = ctl.get(); } - // 执行到这说明当前线程数量已经达到 corePoolSize 或者 addWorker 失败 + // 执行到这说明当前线程数量已经达到核心线程数量 或者 addWorker 失败 // 【2】条件成立说明当前线程池处于running状态,则尝试将 task 放入到 workQueue 中 if (isRunning(c) && workQueue.offer(command)) { // 获取线程池状态 ctl 保存到 recheck @@ -5076,7 +5077,9 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 else if (workerCountOf(recheck) == 0) addWorker(null, false); } - // 【3】offer 失败说明当前 queue 满了,如果当前线程数量尚未达到 maximumPoolSize 的话,会创建新的worker直接执行 command,如果当前线程数量达到 maximumPoolSize 的话,这里 addWorker 也会失败,走拒绝策略 + // 【3】offer失败说明queue满了 + // 如果线程数量尚未达到 maximumPoolSize,会创建非核心 worker 线程执行 command,不公平的原因 + // 如果当前线程数量达到 maximumPoolSiz,这里 addWorker 也会失败,走拒绝策略 else if (!addWorker(command, false)) reject(command); } @@ -5105,9 +5108,8 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 // 获取当前线程池运行状态 int rs = runStateOf(c); // 判断当前线程池状态【是否允许添加线程】 - // 条件一判断线程池的状态是否是 running 状态,成立则表示不是,不允许添加 worker,返回 false, - // 条件二是当前线程池是 SHUTDOWN 状态,但是队列里面还有任务尚未处理完, - // 【这时需要处理完 queue 中的任务,但是不允许再提交新的 task】 + // 判断当前线程池是 SHUTDOWN 状态,但是队列里面还有任务尚未处理完, + // 这时需要处理完 queue 中的任务,但是【不允许再提交新的 task】,所以 addWorker 返回 false if (rs >= SHUTDOWN && !(rs == SHUTDOWN && firstTask == null && !workQueue.isEmpty())) return false; for (;;) { @@ -5136,7 +5138,7 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 boolean workerAdded = false; Worker w = null; try { - // 创建 Worker,底层通过线程工厂创建执行线程 + // 创建 Worker,底层通过线程工厂创建执行线程,指定了先执行的任务 w = new Worker(firstTask); // 将新创建的 worker 节点的线程赋值给 t final Thread t = w.thread; @@ -5182,7 +5184,7 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 return workerStarted; } ``` - + * addWorkerFailed():清理任务 ```java @@ -5212,7 +5214,7 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 ##### 运行方法 -* Worker#run:当某个 worker 启动时,会执行 run() +* Worker#run:Worker 实现了 Runnable 接口,当某个 worker 启动时,会执行 run() ```java public void run() { @@ -5238,11 +5240,11 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 // firstTask 不是 null 就直接运行,否则去 queue 中获取任务 // 【getTask如果是阻塞获取任务,会一直阻塞在take方法,获取后继续循环,不会走返回null的逻辑】 while (task != null || (task = getTask()) != null) { - // 加锁,shutdown 时会判断当前worker状态,根据独占锁是否【空闲】来判断当前worker是否正在工作。 + // worker加锁,shutdown 时会判断当前worker状态,根据独占锁是否【空闲】 w.lock(); // 说明线程池状态大于 STOP,目前处于 STOP/TIDYING/TERMINATION,此时给线程一个中断信号 if ((runStateAtLeast(ctl.get(), STOP) || - // 判断当前线程是否被打断,清楚打断标记,所以最后一个条件会返回false,取反为 true + // 说明线程处于 RUNNING 或者 SHUTDOWN 状态,清除打断标记 (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) && !wt.isInterrupted()) // 中断线程,设置线程的中断标志位为 true wt.interrupt(); @@ -5251,7 +5253,7 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 beforeExecute(wt, task); Throwable thrown = null; try { - // task 可能是 FutureTask,也可能是普通的 Runnable 接口实现类。 + // 【执行任务】 task.run(); } catch (RuntimeException x) { thrown = x; throw x; @@ -5269,11 +5271,11 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 w.unlock(); // 解锁 } } - // getTask() 方法返回 null 时会执行这里,说明 queue 为空或者线程太多,当前【线程应该执行退出逻辑】 + // getTask()方法返回null时会执行这里,表示queue为空并且线程空闲超过保活时间,当前【线程应该执行退出逻辑】 completedAbruptly = false; } finally { // 正常退出 completedAbruptly = false - // 异常退出 completedAbruptly = true,task.run() 内部抛出异常时,跳到这一行 + // 异常退出 completedAbruptly = true,从 task.run() 内部抛出异常时,跳到这一行 processWorkerExit(w, completedAbruptly); } } @@ -5291,7 +5293,7 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 } ``` -* getTask():获取任务,线程空闲时间超过 keepAliveTime 就会被回收,判断的依据是**当前线程超过保活时间没有获取到任务** +* getTask():获取任务,线程空闲时间超过 keepAliveTime 就会被回收,判断的依据是**当前线程超过保活时间没有获取到任务**,方法返回 null 就代表当前线程要被回收了,返回到 runWorker 执行线程退出逻辑 ```java private Runnable getTask() { @@ -5302,9 +5304,11 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 int c = ctl.get(); // 获取线程池当前运行状态 int rs = runStateOf(c); - - // 条件一成立说明当前线程池是非 RUNNING 状态 - // 条件二成立说明线程池已经停止或者 queue 为 null,没有任务需要执行 + + // 【tryTerminate】打断线程后执行到这,此时线程池状态为STOP或者线程池状态为SHUTDOWN并且队列已经是空 + // 所以下面的 if 条件一定是成立的,可以直接返回 null + + // 当前线程池是非 RUNNING 状态,并且线程池状态 >= STOP 或者 queue 为 null,线程就应该退出了 if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) { // 使用 CAS 自旋的方式让 ctl 值 -1 decrementWorkerCount(); @@ -5316,33 +5320,33 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 // timed = false 表示当前这个线程 获取task时不支持超时机制的,当前线程会使用 queue.take() 阻塞获取 // timed = true 表示当前这个线程 获取task时支持超时机制,使用 queue.poll(xxx,xxx) 超时获取 - // 当获取task超时的情况下,下一次自旋就可能返回null了 // 条件一代表允许回收核心线程,那就无所谓了,全部线程都执行超时回收 - // 条件二成立说明线程数量大于核心线程数,执行该方法的线程去超时获取任务,获取不到返回null,执行线程退出逻辑 + // 条件二成立说明线程数量大于核心线程数,当前线程认为是非核心线程, + // 空闲一定时间就需要退出,去超时获取任务,获取不到返回null boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; // 条件一判断线程数量是否超过最大线程数,直接回收 - // wc > 1 说明线程池还用其他线程,当前线程可以直接回收 - // workQueue.isEmpty() 前置条件是 wc = 1,说明当前任务队列已经空了,最后一个线程,也可以放心的退出 + // 如果当前线程允许超时回收并且已经超时了,就应该被回收了,但是由于【担保机制】还要做判断: + // wc > 1 说明线程池还用其他线程,当前线程可以直接回收 + // workQueue.isEmpty() 前置条件是 wc = 1,如果当前任务队列也是空了,最后一个线程就可以安全的退出 if ((wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty())) { - // 使用CAS机制将 ctl 值 -1 ,减 1 成功的线程,返回 null,可以推出 + // 使用CAS机制将 ctl 值 -1 ,减 1 成功的线程,返回 null,可以退出 if (compareAndDecrementWorkerCount(c)) return null; continue; } try { - // 根据当前线程是否需要超时回收【选择从队列获取任务的方法】超时获取或者阻塞获取 + // 根据当前线程是否需要超时回收,【选择从队列获取任务的方法】是超时获取或者阻塞获取 Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take(); - // 获取到任务返回任务 - // 【阻塞获取会阻塞到获取任务为止】,不会返回 null + // 获取到任务返回任务,【阻塞获取会阻塞到获取任务为止】,不会返回 null if (r != null) return r; - // 获取任务为 null,超时标记设置为 true,下次自旋时返回 null + // 获取任务为 null 说明超时了,将超时标记设置为 true,下次自旋时返 null timedOut = true; } catch (InterruptedException retry) { - // 被打断后超时标记置为 false + // 阻塞线程被打断后超时标记置为 false, timedOut = false; } } @@ -5352,9 +5356,11 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 * processWorkerExit():**线程退出线程池** ```java + // 正常退出 completedAbruptly = false,异常退出为 true private void processWorkerExit(Worker w, boolean completedAbruptly) { - // 条件成立代表当前 worker 是发生异常退出的,task 任务执行过程中向上抛出异常了 + // 条件成立代表当前 worker 是发生异常退出的,task 任务执行过程中向上抛出异常了, if (completedAbruptly) + // 从异常时到这里 ctl 一直没有 -1,需要在这里 -1 decrementWorkerCount(); final ReentrantLock mainLock = this.mainLock; @@ -5367,17 +5373,17 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 } finally { mainLock.unlock(); // 解锁 } - // 尝试停止线程池 + // 尝试停止线程池,唤醒下一个线程 tryTerminate(); int c = ctl.get(); - // 条件成立说明当前线程池状态为 RUNNING 或者 SHUTDOWN 状态 + // 线程池不是停止状态就应该有线程运行 if (runStateLessThan(c, STOP)) { // 正常退出的逻辑,是空闲线程回收 if (!completedAbruptly) { // 根据是否回收核心线程确定线程池中的最小值 int min = allowCoreThreadTimeOut ? 0 : corePoolSize; - // 最小值为 0,但是线程队列不为空,需要一个线程来完成任务 + // 最小值为 0,但是线程队列不为空,需要一个线程来完成任务【担保机制】 if (min == 0 && !workQueue.isEmpty()) min = 1; // 线程池中的线程数量大于最小值可以直接返回 @@ -5389,7 +5395,7 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 } } ``` - + **** @@ -5407,7 +5413,7 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 mainLock.lock(); try { checkShutdownAccess(); - //设置线程池状态为SHUTDOWN + //设置线程池状态为 SHUTDOWN advanceRunState(SHUTDOWN); //中断空闲线程 interruptIdleWorkers(); @@ -5421,7 +5427,7 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 } ``` -* interruptIdleWorkers():shutdown 方法会**中断空闲线程** +* interruptIdleWorkers():shutdown 方法会**中断空闲线程**,根据是否可以获取 AQS 独占锁锁判断是否处于工作状态 ```java // onlyOne == true 说明只中断一个线程 ,false 则中断所有线程 @@ -5436,7 +5442,7 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 Thread t = w.thread; //条件一成立:说明当前迭代的这个线程尚未中断 //条件二成立:说明当前worker处于空闲状态,阻塞在poll或者take,因为worker执行task时是加锁的 - // w.tryLock() 加锁,独占锁情况下其他线程持有锁会加锁失败返回 false + // 每个worker有一个独占锁,w.tryLock()尝试加锁,如果锁已经加过了会返回 false if (!t.isInterrupted() && w.tryLock()) { try { // 中断线程,处于 queue 阻塞的线程会被唤醒,进入下一次自旋,返回null,执行退出相逻辑 @@ -5447,6 +5453,7 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 w.unlock(); } } + // false,代表中断所有的线程 if (onlyOne) break; } @@ -5492,21 +5499,23 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 for (;;) { // 获取 ctl 的值 int c = ctl.get(); - // 条件一说明线程池正常,条件二说明有其他线程执行了该方法,当前线程直接返回 + // 条件一说明线程池正常,条件二说明有其他线程执行了状态转换的方法,当前线程直接返回 if (isRunning(c) || runStateAtLeast(c, TIDYING) || // 线程池是 SHUTDOWN 并且任务队列不是空,需要去处理队列中的任务 (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty())) return; + + // 执行到这里说明线程池状态为 STOP 或者线程池状态为 SHUTDOWN 并且队列已经是空 // 判断线程池中线程的数量 if (workerCountOf(c) != 0) { - // 中断一个空闲线程,空闲线程,在 queue.take() | queue.poll() 阻塞空闲 + // 中断一个【空闲线程】,在 queue.take() | queue.poll() 阻塞空闲 // 唤醒后的线程会在getTask()方法返回null,执行退出逻辑时会再次调用tryTerminate()唤醒下一个空闲线程 interruptIdleWorkers(ONLY_ONE); return; } // 池中的线程数量为 0 来到这里 final ReentrantLock mainLock = this.mainLock; - // 加锁 + // 加全局锁 mainLock.lock(); try { // 设置线程池状态为 TIDYING 状态 diff --git a/SSM.md b/SSM.md index 368342b..b389fb8 100644 --- a/SSM.md +++ b/SSM.md @@ -229,7 +229,7 @@ org.apache.ibatis.session.SqlSession:构建者对象接口,用于执行 SQL ### #{}和${} -**#{}:**占位符,传入的内容会作为字符串,加上引号,以**预编译**的方式传入,将 sql 中的 #{} 替换为 ? 号,调用 PreparedStatement 的 set方法来赋值,有效的防止 SQL 注入,提高系统安全性 +**#{}:**占位符,传入的内容会作为字符串,加上引号,以**预编译**的方式传入,将 sql 中的 #{} 替换为 ? 号,调用 PreparedStatement 的 set 方法来赋值,有效的防止 SQL 注入,提高系统安全性 **${}:**拼接符,传入的内容会直接替换拼接,不会加上引号,可能存在 sql 注入的安全隐患 @@ -2868,6 +2868,16 @@ ctx.getBean("beanId") == ctx.getBean("beanName1") == ctx.getBean("beanName2") - prototype:设定创建出的对象保存在 Spring 容器中,是一个非单例(原型)的对象 - request、session、application、 websocket :设定创建出的对象放置在 web 容器对应的位置 +Spring 容器中 Bean 的线程安全问题: + +* 原型 Bean,对于原型 Bean,每次创建一个新对象,线程之间并不存在 Bean 共享,所以不会有线程安全的问题 + +* 单例Bean,所有线程共享一个单例实例 Bean,因此是存在资源的竞争,如果单例 Bean是一个**无状态 Bean**,也就是线程中的操作不会对 Bean 的成员执行查询以外的操作,那么这个单例 Bean 是线程安全的 + + 解决方法:开发人员自来进行线程安全的保证,最简单的办法就是把 Bean 的作用域 singleton 改为 protopyte + + + *** @@ -3027,7 +3037,7 @@ UserService userService = (UserService)ctx.getBean("userService3"); ##### 获取Bean -ApplicationContext子类相关API: +ApplicationContext 子类相关API: | 方法 | 说明 | | ------------------------------------------------- | -------------------------------------------- | @@ -7843,18 +7853,23 @@ AbstractAutowireCapableBeanFactory.**doCreateBean**(beanName, RootBeanDefinition `AutowiredAnnotationBeanPostProcessor.postProcessMergedBeanDefinition()`:后置处理逻辑**(@Autowired)** - * `metadata = findAutowiringMetadata(beanName, beanType, null)`:提取出当前 beanType 类型整个继承体系内的 **@Autowired、@Value、@Inject** 信息,存入一个 InjectionMetadata 对象的 injectedElements 中并放入缓存 + * `metadata = findAutowiringMetadata(beanName, beanType, null)`:提取当前 bean 整个继承体系内的 **@Autowired、@Value、@Inject** 信息,存入一个 InjectionMetadata 对象,保存着当前 bean 信息和要自动注入的字段信息 + ```java + private final Class targetClass; //当前 bean + private final Collection injectedElements; //要注入的信息集合 + ``` + * `metadata = buildAutowiringMetadata(clazz)`:查询当前 clazz 感兴趣的注解信息 - - * `ReflectionUtils.doWithLocalFields()`:提取**字段**的注解的属性信息 - - `findAutowiredAnnotation(field)`:代表感兴趣的注解就是那三种注解 - - * `ReflectionUtils.doWithLocalMethods()`:提取**方法**的注解的属性信息 - + + * `ReflectionUtils.doWithLocalFields()`:提取**字段**的注解的信息 + + `findAutowiredAnnotation(field)`:代表感兴趣的注解就是那三种注解,获取这三种注解的元数据 + + * `ReflectionUtils.doWithLocalMethods()`:提取**方法**的注解的信息 + * `do{} while (targetClass != null && targetClass != Object.class)`:循环从父类中解析,直到 Object 类 - + * `this.injectionMetadataCache.put(cacheKey, metadata)`:存入缓存 `mbd.postProcessed = true`:设置为 true,下次访问该逻辑不会再进入 @@ -7869,6 +7884,9 @@ AbstractAutowireCapableBeanFactory.**doCreateBean**(beanName, RootBeanDefinition } ``` + + + * ` populateBean(beanName, mbd, instanceWrapper)`:**属性填充,依赖注入,整体逻辑是先处理标签再处理注解,填充至 pvs 中,最后通过 apply 方法最后完成属性依赖注入到 BeanWrapper ** * `if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName))`:实例化后的后置处理器,默认返回 true,自定义继承 InstantiationAwareBeanPostProcessor 修改返回值为 false,使 continueWithPropertyPopulation 为 false @@ -7877,7 +7895,7 @@ AbstractAutowireCapableBeanFactory.**doCreateBean**(beanName, RootBeanDefinition * `PropertyValues pvs = (mbd.hasPropertyValues() ? mbd.getPropertyValues() : null)`:处理依赖注入逻辑开始 - * `mbd.getResolvedAutowireMode() == ?`:**根据 bean 标签配置**的 autowire 判断是 **BY_NAME 或者 BY_TYPE** + * `mbd.getResolvedAutowireMode() == ?`:**根据 bean 标签配置的 autowire** 判断是 BY_NAME 或者 BY_TYPE `autowireByName(beanName, mbd, bw, newPvs)`:根据字段名称去查找依赖的 bean @@ -7913,12 +7931,43 @@ AbstractAutowireCapableBeanFactory.**doCreateBean**(beanName, RootBeanDefinition * `pvsToUse = ibp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName)`:**@Autowired 注解的注入** * `findAutowiringMetadata()`:包装着当前 bd 需要注入的注解信息集合,**三种注解的元数据**,直接缓存获取 - * `InjectionMetadata.InjectedElement.inject()`:将注解信息解析后注入到 pvs,方法和字段的注入的实现不同 + + * `InjectionMetadata.InjectedElement.inject()`:遍历注解信息解析后注入到 Bean,方法和字段的注入的实现不同 + + 以字段注入为例: + + * `value = resolveFieldValue(field, bean, beanName)`:处理字段属性值 + + `value = beanFactory.resolveDependency()`:解决依赖 + + `result = doResolveDependency()`:**真正处理自动注入依赖的逻辑** + + * `Object shortcut = descriptor.resolveShortcut(this)`:默认返回 null + + * `Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor)`:**获取 @Value 的值** + + * `converter.convertIfNecessary(value, type, descriptor.getTypeDescriptor())`:进行类型转换 + + * `matchingBeans = findAutowireCandidates(beanName, type, descriptor)`:**获取 @Autowired 的 Bean** + + ```java + // addCandidateEntry() → Object beanInstance = descriptor.resolveCandidate() + public Object resolveCandidate(String beanName, Class requiredType, BeanFactory beanFactory) throws BeansException { + // 获取 bean + return beanFactory.getBean(beanName); + } + ``` + * `ReflectionUtils.makeAccessible()`:修改访问权限,true 代表暴力破解 - * `method.invoke()`:利用反射为此对象赋值 + * `field.set(bean, value)`:获取属性访问器为此 field 对象赋值 * `applyPropertyValues()`:**将所有解析的 PropertyValues 的注入至 BeanWrapper 实例中**(深拷贝) + * `if (pvs.isEmpty())`:注解 @Autowired 和 @Value 标注的信息在后置处理的逻辑注入完成,此处为空直接返回 + * 下面的逻辑进行 XML 配置的属性的注入,首先获取转换器进行数据转换,然后**获取 WriteMethod (set) 方法进行反射调用**,完成属性的注入 + + + * `initializeBean(String,Object,RootBeanDefinition)`:**初始化,分为配置文件和实现接口两种方式** * `invokeAwareMethods(beanName, bean)`:根据 bean 是否实现 Aware 接口执行初始化的方法 @@ -7976,6 +8025,9 @@ AbstractAutowireCapableBeanFactory.**doCreateBean**(beanName, RootBeanDefinition } ``` + + + * `if (earlySingletonExposure)`:是否循序提前引用 `earlySingletonReference = getSingleton(beanName, false)`:**从二级缓存获取实例**,放入一级缓存是在 doGetBean 中的sharedInstance = getSingleton() 方法中,此时在 createBean 的逻辑还没有返回。 @@ -8031,7 +8083,7 @@ AbstractAutowireCapableBeanFactory.createBeanInstance(beanName, RootBeanDefiniti * `Supplier instanceSupplier = mbd.getInstanceSupplier()`:获取创建实例的函数,可以自定义,没有进入下面的逻辑 -* `if (mbd.getFactoryMethodName() != null)`:**判断 bean 是否设置了 factory-method 属性** +* `if (mbd.getFactoryMethodName() != null)`:**判断 bean 是否设置了 factory-method 属性,优先使用** ,设置了该属性进入 factory-method 方法创建实例 @@ -8050,7 +8102,7 @@ AbstractAutowireCapableBeanFactory.createBeanInstance(beanName, RootBeanDefiniti * `return instantiateBean(beanName, mbd)`:**无参构造方法通过反射创建实例** -* `ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName)`:@Autowired 注解对应的后置处理器**AutowiredAnnotationBeanPostProcessor 逻辑** +* `ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName)`:**@Autowired 注解**配置在构造方法上,对应的后置处理器AutowiredAnnotationBeanPostProcessor 逻辑 * 配置了 lookup 的相关逻辑 @@ -8210,17 +8262,18 @@ AbstractAutowireCapableBeanFactory.createBeanInstance(beanName, RootBeanDefiniti 循环依赖:是一个或多个对象实例之间存在直接或间接的依赖关系,这种依赖关系构成一个环形调用 -Spring 循环依赖有三种: +Spring 循环依赖有四种: +* DependsOn 依赖加载【无法解决】 * 原型模式循环依赖【无法解决】 * 单例 Bean 循环依赖:构造参数产生依赖【无法解决】 -* 单例 Bean 循环依赖:setter产生依赖【可以解决】 +* 单例 Bean 循环依赖:setter 产生依赖【可以解决】 解决循环依赖:提前引用,提前暴露创建中的 Bean * Spring 先实例化 A,拿到 A 的构造方法反射创建出来 A 的早期实例对象,这个对象被包装成 ObjectFactory 对象,放入三级缓存 * 处理 A 的依赖数据,检查发现 A 依赖 B 对象,所以 Spring 就会去根据 B 类型到容器中去 getBean(B.class),这里产生递归 -* 拿到 B 的构造方法,进行反射创建出来 B 的早期实例对象,也会把 B 包装成 ObjectFactory 对象,放到三级缓存,处理 B 的依赖数据,检查发现 B 依赖了 A 对象,然后 Spring 就会去根据A类型到容器中去 getBean(A.class) +* 拿到 B 的构造方法,进行反射创建出来 B 的早期实例对象,也会把 B 包装成 ObjectFactory 对象,放到三级缓存,处理 B 的依赖数据,检查发现 B 依赖了 A 对象,然后 Spring 就会去根据 A 类型到容器中去 getBean(A.class) * 这时获取到 A 的早期对象进入属性填充 循环依赖的三级缓存: From 897c4df306896a4fb59fc0357597896d25aaa485 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 14 Aug 2021 12:39:47 +0800 Subject: [PATCH 020/168] Update Java Notes --- DB.md | 38 +-- Java.md | 151 ++++++--- Prog.md | 1016 ++++++++++++++++++++++++++++++++++--------------------- 3 files changed, 749 insertions(+), 456 deletions(-) diff --git a/DB.md b/DB.md index a845ac1..f0bdfca 100644 --- a/DB.md +++ b/DB.md @@ -2135,7 +2135,7 @@ undo log 是采用段 (segment) 的方式来记录,每个 undo 操作在记录 rollback segment 称为回滚段,每个回滚段中有 1024 个 undo log segment -* 在以前老版本,只支持1个 rollback segment,只能记录 1024 个 undo log segment +* 在以前老版本,只支持 1 个 rollback segment,只能记录 1024 个 undo log segment * MySQL5.5 开始支持 128 个 rollback segment,支持 128*1024 个 undo 操作 @@ -2168,7 +2168,7 @@ rollback segment 称为回滚段,每个回滚段中有 1024 个 undo log segme * 严格的隔离性,对应了事务隔离级别中的 serializable,实际应用中对性能考虑很少使用可串行化 -* 与原子性、持久性侧重于研究事务本身不同,隔离性研究的是不同事务之间的相互影响 +* 与原子性、持久性侧重于研究事务本身不同,隔离性研究的是**不同事务**之间的相互影响 隔离性让并发情形下的事务之间互不干扰: @@ -2254,18 +2254,18 @@ MySQL中还存在 binlog(二进制日志) 也可以记录写操作并用于数 | ---------------- | -------- | ---------------------- | ------------------- | | read uncommitted | 读未提交 | 脏读、不可重复读、幻读 | | | read committed | 读已提交 | 不可重复读、幻读 | Oracle / SQL Server | -| repeatable read | 可重复读 | 幻读 | MySQL(避免幻读) | +| repeatable read | 可重复读 | 幻读 | MySQL | | serializable | 串行化 | 无 | | 一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差 -* 丢失更新 (Lost Update):当两个或多个事务选择同一行,最初的事务修改的值,被后面事务修改的值覆盖,所有的隔离级别都可以避免丢失更新 +* 丢失更新 (Lost Update):当两个或多个事务选择同一行,最初的事务修改的值,被后面事务修改的值覆盖,所有的隔离级别都可以避免丢失更新(行锁) * 脏读 (Dirty Reads):在事务中先后两次读取同一个数据,两次读取的结果不一样,原因是在一个事务处理过程中读取了另一个**未提交**的事务中的数据 * 不可重复读 (Non-Repeatable Reads):在事务中先后两次读取同一个数据,两次读取的结果不一样,原因是在一个事务处理过程中读取了另一个事务中修改并**已提交**的数据 - > 可重复读的意思是不管读几次,结果都一样,可以重复的读 + > 可重复读的意思是不管读几次,结果都一样,可以重复的读,可以理解为快照读,要读的数据集不会发生变化 * 幻读 (Phantom Reads):在事务中按某个条件先后两次查询数据库,两次查询结果的数量不同,**数据条目**发生了变化。比如查询某数据不存在,准备插入此记录,但执行插入时发现此记录已存在,无法插入;或查询某数据不存在,执行delete删除,却发现删除成功 @@ -2372,18 +2372,18 @@ undo log 主要分为两种: * update undo log:事务在进行 update 或 delete 时产生的 undo log,在事务回滚时需要,在快照读时也需要。不能随意删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被 purge 线程统一清除 -每次对数据库记录进行改动,都会将旧值放到一条undo日志中,算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,把这个链表称之为**版本链**,版本链的头节点就是当前记录最新的值,链尾就是最早的旧记录 +每次对数据库记录进行改动,都会将旧值放到一条 undo 日志中,算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,把这个链表称之为**版本链**,版本链的头节点就是当前记录最新的值,链尾就是最早的旧记录 * 有个事务插入 persion 表一条新记录,name 为 Jerry,age 为 24 -* 事务1修改该行数据时,数据库会先对该行加排他锁,然后先把该行数据拷贝到 undo log 中作为旧记录,拷贝完毕后修改该行 name 为Tom,并且修改隐藏字段的事务ID为当前事务1的ID(默认为 1 之后递增),回滚指针指向拷贝到 undo log 的副本记录,事务提交后,释放锁 +* 事务 1 修改该行数据时,数据库会先对该行加排他锁,然后先把该行数据拷贝到 undo log 中作为旧记录,拷贝完毕后修改该行 name 为 Tom,并且修改隐藏字段的事务 ID 为当前事务 1 的 ID(默认为 1 之后递增),回滚指针指向拷贝到 undo log 的副本记录,事务提交后,释放锁 * 以此类推 purge 线程: -为了实现 InnoDB 的 MVCC 机制,更新或者删除操作都只是设置一下老记录的 deleted_bit,并不真正将过时的记录删除,为了节省磁盘空间,InnoDB 有专门的 purge 线程来清理 deleted_bit 为true的记录,purge 线程维护了一个 Read view(这个 Read view 相当于系统中最老活跃事务的 Read view),如果某个记录的 deleted_bit 为true,并且 DB_TRX_ID 相对于 purge 线程的 Read view 可见,那么这条记录一定是可以被安全清除的 +为了实现 InnoDB 的 MVCC 机制,更新或者删除操作都只是设置一下老记录的 deleted_bit,并不真正将过时的记录删除,为了节省磁盘空间,InnoDB 有专门的 purge 线程来清理 deleted_bit 为 true 的记录,purge 线程维护了一个 Read view(这个 Read view 相当于系统中最老活跃事务的 Read view),如果某个记录的 deleted_bit 为 true,并且 DB_TRX_ID 相对于 purge 线程的 Read view 可见,那么这条记录一定是可以被安全清除的 @@ -2393,25 +2393,25 @@ purge 线程: ##### 读视图 -Read View 是事务进行快照读操作时产生的读视图,该事务执行快照读的那一刻会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID,用来做可见性判断,根据视图判断当前事务能够看到哪个版本的数据 +Read View 是事务进行快照读操作时产生的读视图,该事务执行快照读的那一刻会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的 ID,用来做可见性判断,根据视图判断当前事务能够看到哪个版本的数据 -工作流程:将版本链的头节点的事务ID(最新数据事务ID)DB_TRX_ID 取出来,与系统当前活跃事务的 ID 对比,如果 DB_TRX_ID 不符合可见性,那就通过 DB_ROLL_PTR 回滚指针去取出 undo log 中的下一个 DB_TRX_ID 再比较,直到找到满足特定条件的 DB_TRX_ID,该事务 ID 所在的旧记录就是当前事务能看见的最新的记录 +工作流程:将版本链的头节点的事务ID(最新数据事务ID)DB_TRX_ID 取出来,与系统当前活跃事务的 ID 对比,如果 DB_TRX_ID 不符合可见性,通过 DB_ROLL_PTR 回滚指针去取出 undo log 中的下一个 DB_TRX_ID 比较,直到找到最近的满足特定条件的 DB_TRX_ID,该事务 ID 所在的旧记录就是当前事务能看见的最新的记录 Read View 几个属性: - m_ids:生成 Read View 时当前系统中活跃的事务 id 列表 - up_limit_id:生成 Read View 时当前系统中活跃的最小的事务 id,也就是 m_ids 中的最小值 -- low_limit_id:生成 Read View 时系统应该分配给下一个事务的 id 值,m_ids 中的最大值加1 -- creator_trx_id:生成该 Read View 的事务的事务id,就是判断该id的事务能读到什么数据 +- low_limit_id:生成 Read View 时系统应该分配给下一个事务的 id 值,m_ids 中的最大值加 1 +- creator_trx_id:生成该 Read View 的事务的事务 id,就是判断该 id 的事务能读到什么数据 -creator 创建一个 Read View,进行可见性算法分析:(**解决了读未提交**) +creator 创建一个 Read View,进行可见性算法分析:(解决了读未提交) * db_trx_id == creator_trx_id:表示这个数据就是当前事务自己生成的,自己生成的数据自己肯定能看见,所以这种情况下此数据对 creator 是可见的 -* db_trx_id < up_limit_id:该版本对应的事务 ID 小于 Read view 中的最小活跃事务ID,则这个事务在当前事务之前就已经被 COMMIT 了,对 creator 可见 +* db_trx_id < up_limit_id:该版本对应的事务 ID 小于 Read view 中的最小活跃事务 ID,则这个事务在当前事务之前就已经被 COMMIT 了,对 creator 可见 -* db_trx_id >= low_limit_id:该版本对应的事务 ID 大于 Read view 中当前系统的最大事务ID,则说明该数据是在当前 Read view 创建之后才产生的,对 creator 不可见 +* db_trx_id >= low_limit_id:该版本对应的事务 ID 大于 Read view 中当前系统的最大事务 ID,则说明该数据是在当前 Read view 创建之后才产生的,对 creator 不可见 * up_limit_id <= db_trx_id < low_limit_id:判断 db_trx_id 是否在活跃事务列表 m_ids 中 - * 在列表中,说明该版本对应的事务正在运行,数据不能显示(不能读到未提交的数据) + * 在列表中,说明该版本对应的事务正在运行,数据不能显示(**不能读到未提交的数据**) * 不在列表中,说明该版本对应的事务已经被提交,数据可以显示 @@ -2479,7 +2479,7 @@ RR、RC 生成时机: 解决幻读问题: - 快照读:通过 MVCC 来进行控制的,不用加锁,当前事务只生成一次 Read View,外部操作没有影响 -- 当前读:通过 next-key 锁(行锁+间隙锁)来解决问题 +- 当前读:通过 next-key 锁(行锁 + 间隙锁)来解决问题 RC、RR 级别下的 InnoDB 快照读区别 @@ -5895,7 +5895,7 @@ InnoDB 实现了以下两种类型的行锁: - 共享锁 (S):又称为读锁,简称 S 锁,就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改 - 排他锁 (X):又称为写锁,简称 X 锁,就是不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,只有获取排他锁的事务是可以对数据读取和修改 -对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会自动给涉及数据集加排他锁;对于普通 SELECT 语句,InnoDB 不会加任何锁 +对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会**自动给涉及数据集加排他锁**(行锁);对于普通 SELECT 语句,不会加任何锁 锁的兼容性: @@ -6052,7 +6052,7 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 * 唯一索引加锁只有在值存在时才是行锁,值不存在会变成间隙锁,所以范围查询时容易出现间隙锁 * 对于联合索引且是唯一索引,如果 where 条件只包括联合索引的一部分,那么会加间隙锁 -加锁的基本单位是 next-key lock,该锁是行锁和这条记录前面的 gap lock 的组合,就是行锁加间隙锁。 +加锁的基本单位是 next-key lock,该锁是行锁和这条记录前面的 gap lock 的组合,就是行锁加间隙锁 * 加锁遵循前开后闭原则 * 假设有索引值 10、11、13,那么可能的间隙锁包括:(负无穷,10]、(10,11]、(11,13]、(13,20,正无穷),锁住索引 11 会同时对间隙 (10,11]、(11,13] 加锁 diff --git a/Java.md b/Java.md index c55c816..7aa57d1 100644 --- a/Java.md +++ b/Java.md @@ -2808,7 +2808,7 @@ private void ensureCapacityInternal(int minimumCapacity) { } } public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) { - //将字符串中的字符复制到目标字符数组中 + // 将字符串中的字符复制到目标字符数组中 // 字符串调用该方法,此时value是字符串的值,dst是目标字符数组 System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin); } @@ -2969,17 +2969,18 @@ public static void main(String[] args){ ### Calendar -Calendar代表了系统此刻日期对应的日历对象 -Calendar是一个抽象类,不能直接创建对象 -Calendar日历类创建日历对象的语法: - `Calendar rightNow = Calendar.getInstance()`(**饿汉单例模式**) -Calendar的方法: - `public static Calendar getInstance()`: 返回一个日历类的对象。 - `public int get(int field)`:取日期中的某个字段信息。 - `public void set(int field,int value)`:修改日历的某个字段信息。 - `public void add(int field,int amount)`:为某个字段增加/减少指定的值 - `public final Date getTime()`: 拿到此刻日期对象。 - `public long getTimeInMillis()`: 拿到此刻时间毫秒值 +Calendar 代表了系统此刻日期对应的日历对象,是一个抽象类,不能直接创建对象 + +Calendar 日历类创建日历对象:`Calendar rightNow = Calendar.getInstance()`(**饿汉单例模式**) + +Calendar 的方法: + +* `public static Calendar getInstance()`: 返回一个日历类的对象 +* `public int get(int field)`:取日期中的某个字段信息 +* `public void set(int field,int value)`:修改日历的某个字段信息 +* `public void add(int field,int amount)`:为某个字段增加/减少指定的值 +* `public final Date getTime()`: 拿到此刻日期对象 +* `public long getTimeInMillis()`: 拿到此刻时间毫秒值 ```java public static void main(String[] args){ @@ -3106,8 +3107,8 @@ public class JDK8DateDemo9 { ### Math Math 用于做数学运算 -Math 类中的方法全部是静态方法,直接用类名调用即可 -方法: + +Math 类中的方法全部是静态方法,直接用类名调用即可: | 方法 | 说明 | | -------------------------------------------- | --------------------------------- | @@ -3224,18 +3225,20 @@ public class SystemDemo { Java 在 java.math 包中提供的 API 类,用来对超过16位有效位的数进行精确的运算 构造方法: - `public static BigDecimal valueOf(double val)` : 包装浮点数成为大数据对象。 - `public BigDecimal(double val)` : - `public BigDecimal(String val)` : + +* `public static BigDecimal valueOf(double val)`:包装浮点数成为大数据对象。 +* `public BigDecimal(double val)` +* `public BigDecimal(String val)` 常用API: - `public BigDecimal add(BigDecimal value)` : 加法运算 - `public BigDecimal subtract(BigDecimal value)` : 减法运算 - `public BigDecimal multiply(BigDecimal value)` : 乘法运算 - `public BigDecimal divide(BigDecimal value)` : 除法运算 - `public double doubleValue()` : 把BigDecimal转换成double类型。 - `public int intValue()` : 转为int 其他类型相同 - `public BigDecimal divide (BigDecimal value,精确几位,舍入模式)` : 除法 + +* `public BigDecimal add(BigDecimal value)`:加法运算 +* `public BigDecimal subtract(BigDecimal value)`:减法运算 +* `public BigDecimal multiply(BigDecimal value)`:乘法运算 +* `public BigDecimal divide(BigDecimal value)`:除法运算 +* `public double doubleValue()`:把BigDecimal转换成double类型。 +* `public int intValue()`:转为int 其他类型相同 +* `public BigDecimal divide (BigDecimal value,精确几位,舍入模式)`:除法 ```java public class BigDecimalDemo { @@ -3301,7 +3304,7 @@ java.util.regex 包主要包括以下三个类: - Pattern 类: - pattern 对象是一个正则表达式的编译表示。Pattern 类没有公共构造方法,要创建一个 Pattern 对象,必须首先调用其公共静态编译方法,返回一个 Pattern 对象。该方法接受一个正则表达式作为它的第一个参数 + Pattern 对象是一个正则表达式的编译表示。Pattern 类没有公共构造方法,要创建一个 Pattern 对象,必须首先调用其公共静态编译方法,返回一个 Pattern 对象。该方法接受一个正则表达式作为它的第一个参数 - Matcher 类: @@ -3325,6 +3328,10 @@ java.util.regex 包主要包括以下三个类: +*** + + + ##### 特殊字符 \r\n 是Windows中的文本行结束标签,在Unix/Linux则是 \n @@ -3341,6 +3348,10 @@ java.util.regex 包主要包括以下三个类: +*** + + + ##### 标准字符 标准字符集合 @@ -3360,6 +3371,10 @@ java.util.regex 包主要包括以下三个类: +*** + + + ##### 自定义符 自定义符号集合,[ ]方括号匹配方式,能够匹配方括号中**任意一个**字符 @@ -3381,6 +3396,10 @@ java.util.regex 包主要包括以下三个类: +*** + + + ##### 量词字符 修饰匹配次数的特殊符号。 @@ -3417,6 +3436,10 @@ java.util.regex 包主要包括以下三个类: +*** + + + ##### 捕获组 捕获组是把多个字符当一个单独单元进行处理的方法,它通过对括号内的字符分组来创建。 @@ -3434,6 +3457,8 @@ java.util.regex 包主要包括以下三个类: +*** + ##### 反向引用 @@ -3472,6 +3497,8 @@ java.util.regex 包主要包括以下三个类: +*** + ##### 零宽断言 @@ -3520,16 +3547,18 @@ java.util.regex 包主要包括以下三个类: #### 分组匹配 -Pattern类: - `static Pattern compile(String regex)` : 将给定的正则表达式编译为模式 - `Matcher matcher(CharSequence input)` : 创建一个匹配器,匹配给定的输入与此模式 - `static boolean matches(String regex, CharSequence input)` : 编译正则表达式,并匹配输入 +Pattern 类: + +* `static Pattern compile(String regex)`:将给定的正则表达式编译为模式 +* `Matcher matcher(CharSequence input)`:创建一个匹配器,匹配给定的输入与此模式 +* `static boolean matches(String regex, CharSequence input)`:编译正则表达式,并匹配输入 + +Matcher 类: -Matcher类: - `boolean find()` : 扫描输入的序列,查找与该模式匹配的下一个子序列 - `String group()` : 返回与上一个匹配的输入子序列。同group(0),匹配整个表达式的子字符串 - `String group(int group)` : 返回在上一次匹配操作期间由给定组捕获的输入子序列 - `int groupCount()` : 返回此匹配器模式中捕获组的数量 +* `boolean find()`:扫描输入的序列,查找与该模式匹配的下一个子序列 +* `String group()`:返回与上一个匹配的输入子序列。同group(0),匹配整个表达式的子字符串 +* `String group(int group)`:返回在上一次匹配操作期间由给定组捕获的输入子序列 +* `int groupCount()`:返回此匹配器模式中捕获组的数量 ```java public class Demo01{ @@ -3608,6 +3637,10 @@ public static void main(String[] args){ +*** + + + ##### 验证号码 ```java @@ -3627,6 +3660,10 @@ public static void checkEmail(String email){ +*** + + + ##### 查找替换 * `public String[] split(String regex)`:按照正则表达式匹配的内容进行分割字符串,反回一个字符串数组 @@ -3636,25 +3673,27 @@ public static void checkEmail(String email){ //数组分割 public static void main(String[] args) { // 1.split的基础用法 - String names = "贾乃亮,王宝强,陈羽凡"; + String names = "风清扬,张无忌,周芷若"; // 以“,”分割成字符串数组 String[] nameArrs = names.split(","); // 2.split集合正则表达式做分割 - String names1 = "贾乃亮lv434fda324王宝强87632fad2342423陈羽凡"; + String names1 = "风清扬lv434fda324张无忌87632fad2342423周芷若"; // 以匹配正则表达式的内容为分割点分割成字符串数组 String[] nameArrs1 = names1.split("\\w+"); // 使用正则表达式定位出内容,替换成/ - System.out.println(names1.replaceAll("\\w+","/"));//贾乃亮/王宝强/羽凡 + System.out.println(names1.replaceAll("\\w+","/"));//风清扬/张无忌/周芷若 - String names3 = "贾乃亮,王宝强,羽凡"; - System.out.println(names3.replaceAll(",","-"));//贾乃亮-王宝强-羽凡 + String names3 = "风清扬,张无忌,周芷若"; + System.out.println(names3.replaceAll(",","-"));//风清扬-张无忌-周芷若 } ``` +*** + ##### 面试问题 @@ -11307,9 +11346,9 @@ public Object pop() { ## 类加载 -### 对象结构 +### 对象访存 -#### 存储构造 +#### 存储结构 一个 Java 对象内存中存储为三部分:对象头(Header)、实例数据(Instance Data)和对齐填充 (Padding) @@ -11365,7 +11404,7 @@ public Object pop() { ```ruby # 由于需要8位对齐,所以最终大小为`56byte`。 - 4(Mark Word) + 4(Klass Word) + 4(length) + 4*10(10个int大小) + 4(Padding) = 56sbyte + 4(Mark Word) + 4(Klass Word) + 4(length) + 4*10(10个int大小) + 4(Padding) = 56sbyte ``` @@ -11392,9 +11431,9 @@ private int hash32; 对象的实际大小:一个对象所能触及的所有对象的浅堆大小之和,也就是通常意义上我们说的对象大小 -下图显示了一个简单的对象引用关系图,对象 A 引用了 C 和 D,对象 B 引用了 C 和 E。那么对象 A 的浅堆大小只是 A 本身,A 的实际大小为 A、C、D 三者之和,A 的深堆大小为 A 与 D 之和,由于对象 C 还可以通过对象 B 访问到 C,因此 C 不在对象 A 的深堆范围内 +下图显示了一个简单的对象引用关系图,对象 A 引用了 C 和 D,对象 B 引用了 C 和 E。那么对象 A 的浅堆大小只是 A 本身,**A 的实际大小为 A、C、D 三者之和**,A 的深堆大小为 A 与 D 之和,由于对象 C 还可以通过对象 B 访问到 C,因此 C 不在对象 A 的深堆范围内 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-对象的实际大小.png) + 内存分析工具 MAT 提供了一种叫支配树的对象图,体现了对象实例间的支配关系 @@ -11451,16 +11490,20 @@ JVM 是通过**栈帧中的对象引用**访问到其内部的对象实例: 优点:reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集)时只会改变句柄中的实例数据指针,而 reference 本身不需要被修改。 - + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-对象访问-句柄访问.png) * 直接指针(HotSpot 采用):Java 堆对象的布局必须考虑如何放置访问类型数据的相关信息,reference 中直接存储的对象地址 优点:速度更快,**节省了一次指针定位的时间开销** - + 缺点:对象被移动时(如进行GC后的内存重新排列),对象的 reference 也需要同步更新 + + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-对象访问-直接指针.png) +参考文章:https://www.cnblogs.com/afraidToForget/p/12584866.html + *** @@ -11685,7 +11728,7 @@ Java 对象创建时机: - 由其他文件生成,例如由 JSP 文件生成对应的 Class 类 - 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass -将字节码文件加载至元空间后,会**在堆中**创建一个 java.lang.Class 对象,用来封装类位于方法区内的数据结构,该 Class 对象是在加载类的过程中创建的,每个类都对应有一个 Class 类型的对象 +将字节码文件加载至方法区后,会**在堆中**创建一个 java.lang.Class 对象,用来引用位于方法区内的数据结构,该 Class 对象是在加载类的过程中创建的,每个类都对应有一个 Class 类型的对象 方法区内部采用 C++ 的 instanceKlass 描述 java 类的数据结构: @@ -11842,13 +11885,13 @@ class D { ##### clinit -():类构造器,由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的 +():类构造器,由编译器自动收集类中**所有类变量的赋值动作和静态语句块**中的语句合并产生的 作用:是在类加载过程中的初始化阶段进行静态变量初始化和执行静态代码块 * 如果类中没有静态变量或静态代码块,那么 clinit 方法将不会被生成 * clinit 方法只执行一次,在执行 clinit 方法时,必须先执行父类的clinit方法 -* static 变量的赋值操作和静态代码块的合并顺序由源文件中**出现的顺序**决定 +* static 变量的赋值操作和静态代码块的合并顺序由源文件中出现的顺序决定 * static 不加 final 的变量都在初始化环节赋值 **线程安全**问题: @@ -11868,9 +11911,7 @@ public class Test { } ``` -补充: - -接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 () 方法。但两者不同的是, +接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 () 方法,两者不同的是: * 在初始化一个接口时,并不会先初始化它的父接口,所以执行接口的 () 方法不需要先执行父接口的 () 方法 * 在初始化一个类时,不会先初始化所实现的接口,所以接口的实现类在初始化时不会执行接口的 () 方法 @@ -11897,13 +11938,13 @@ public class Test { * 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化,但这条规则并**不适用于接口** * 当虚拟机启动时,需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类 * MethodHandle 和 VarHandle 可以看作是轻量级的反射调用机制,而要想使用这两个调用, 就必须先使用 findStaticVarHandle 来初始化要调用的类 -* 补充:当一个接口中定义了 JDK8 新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化 +* 补充:当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化 **被动引用**:所有引用类的方式都不会触发初始化,称为被动引用 * 通过子类引用父类的静态字段,不会导致子类初始化,只会触发父类的初始化 * 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法 -* 常量(final修饰)在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化 +* 常量(final 修饰)在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化 * 调用 ClassLoader 类的 loadClass() 方法加载一个类,并不是对类的主动使用,不会导致类的初始化 @@ -17474,7 +17515,7 @@ UML 从目标系统的不同角度出发,定义了用例图、类图、对象 ##### 序列化 -将单例对象序列化再反序列化,对象从内存反序列化到程序中会重新创建一个对象,通过反序列化得到的对象是不同的对象,而且得到的对象不是通过构造器得到的,反序列化得到的对象不执行构造器 +将单例对象序列化再反序列化,对象从内存反序列化到程序中会重新创建一个对象,通过反序列化得到的对象是不同的对象,而且得到的对象不是通过构造器得到的,**反序列化得到的对象不执行构造器** * Singleton diff --git a/Prog.md b/Prog.md index 41bc5c7..348d226 100644 --- a/Prog.md +++ b/Prog.md @@ -416,7 +416,9 @@ public class Test { ##### 打断线程 `public void interrupt()`:打断这个线程,异常处理机制 + `public static boolean interrupted()`:判断当前线程是否被打断,打断返回 true,清除打断标记,连续调用两次一定返回 false + `public boolean isInterrupted()`:判断当前线程是否被打断,不清除打断标记 打断的线程会发生上下文切换,操作系统会保存线程信息,抢占到 CPU 后会从中断的地方接着运行 @@ -1065,10 +1067,9 @@ LocalVariableTable: 一个对象创建时: -* 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的 - thread、epoch、age 都为 0 - -* 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加VM参数 `-XX:BiasedLockingStartupDelay=0` 来禁用延迟 +* 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,thread、epoch、age 都为 0 + +* 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 `-XX:BiasedLockingStartupDelay=0` 来禁用延迟 JDK 8 延迟 4s 开启偏向锁原因:在刚开始执行代码时,会有好多线程来抢锁,如果开偏向锁效率反而降低 @@ -1549,7 +1550,7 @@ public final native void wait(long timeout):有时限的等待, 到n毫秒后结 对比 sleep(): * 原理不同:sleep() 方法是属于 Thread 类,是线程用来控制自身流程的,使此线程暂停执行一段时间而把执行机会让给其他线程;wait() 方法属于 Object 类,用于线程间通信 -* 对锁的处理机制不同:调用 sleep() 方法的过程中,线程不会释放对象锁,当调用 wait() 方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,但是都会释放 CPU +* 对锁的处理机制不同:调用 sleep() 方法的过程中,线程不会释放对象锁,当调用 wait() 方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池(不释放锁其他线程怎么抢占到锁执行唤醒操作),但是都会释放 CPU * 使用区域不同:wait() 方法必须放在**同步控制方法和同步代码块(先获取锁)**中使用,sleep() 方法则可以放在任何地方使用 底层原理: @@ -2575,7 +2576,7 @@ lock 前缀指令就相当于内存屏障,Memory Barrier(Memory Fence) - 阻止屏障两侧的指令重排序 - 强制把缓存中的脏数据写回主内存,让缓存行中相应的数据失效 -保证可见性: +**保证可见性**: * 写屏障(sfence,Store Barrier)保证在该屏障之前的,对共享变量的改动,都同步到主存当中 @@ -2605,7 +2606,7 @@ lock 前缀指令就相当于内存屏障,Memory Barrier(Memory Fence) * 全能屏障:mfence(modify/mix Barrier),兼具 sfence 和 lfence 的功能 -保证有序性: +**保证有序性**: * 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后 * 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前 @@ -2622,7 +2623,7 @@ lock 前缀指令就相当于内存屏障,Memory Barrier(Memory Fence) new Thread(() -> {i--}); ``` - i++反编译后的指令: + i++ 反编译后的指令: ```java 0: iconst_1 //当int取值 -1~5 时,JVM采用iconst指令将常量压入栈中 @@ -2899,7 +2900,7 @@ public class TestVolatile { 无锁编程:Lock Free -CAS的全称是 Compare-And-Swap,是**CPU并发原语** +CAS 的全称是 Compare-And-Swap,是 **CPU 并发原语** * CAS 并发原语体现在 Java 语言中就是 sun.misc.Unsafe 类的各个方法,调用 UnSafe 类中的 CAS 方法,JVM 会实现出 CAS 汇编指令,这是一种完全依赖于硬件的功能,实现了原子操作 * CAS 是一种系统原语,原语属于操作系统范畴,是由若干条指令组成 ,用于完成某个功能的一个过程,并且**原语的执行必须是连续的,执行过程中不允许被中断**,也就是说 CAS 是一条 CPU 的原子指令,不会造成数据不一致的问题,所以 CAS 是线程安全的 @@ -2937,10 +2938,10 @@ CAS 缺点: CAS 与 Synchronized 总结: -* Synchronized是从悲观的角度出发: - 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。(**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**),因此Synchronized我们也将其称之为悲观锁。jdk中的ReentrantLock也是一种悲观锁,**性能较差** -* CAS是从乐观的角度出发: - 总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。**如果别人修改过,则获取现在最新的值。如果别人没修改过,直接修改共享数据的值**,CAS这种机制我们也可以将其称之为乐观锁。**综合性能较好**! +* Synchronized 是从悲观的角度出发: + 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。(**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**),因此 Synchronized 我们也将其称之为悲观锁。jdk中的ReentrantLock也是一种悲观锁,**性能较差** +* CAS 是从乐观的角度出发: + 总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。**如果别人修改过,则获取现在最新的值。如果别人没修改过,直接修改共享数据的值**,CAS 这种机制我们也可以将其称之为乐观锁。**综合性能较好** @@ -3070,7 +3071,6 @@ CAS 算法:有 3 个操作数(内存值 V, 旧的预期值 A,要修改 } ``` - @@ -3194,11 +3194,11 @@ LongAdder 和 LongAccumulator 区别: 不同点: -* 调用 casBase 时,LongAccumulator 使用 function.applyAsLong(b = base, x) 来计算,LongAddr 使用 casBase(b = base, b + x) 来计算 +* 调用 casBase 时,LongAccumulator 使用 function.applyAsLong(b = base, x) 来计算,LongAddr 使用 casBase(b = base, b + x) * LongAccumulator 类功能更加强大,构造方法参数中 * accumulatorFunction 是一个双目运算器接口,可以指定累加规则,比如累加或者相乘,其根据输入的两个参数返回一个计算值,LongAdder 内置累加规则 - * identity 则是 LongAccumulator 累加器的初始值,LongAccumulator 可以为累加器提供非0的初始值,而 LongAdder 只能提供默认的0 + * identity 则是 LongAccumulator 累加器的初始值,LongAccumulator 可以为累加器提供非0的初始值,而 LongAdder 只能提供默认的 0 @@ -3506,7 +3506,7 @@ public static void main(String[] args) { ### Unsafe -Unsafe 是 CAS 的核心类,由于 Java 无法直接访问底层系统,需要通过本地 (Native) 方法来访问 +Unsafe 是 CAS 的核心类,由于 Java 无法直接访问底层系统,需要通过本地(Native)方法来访问 Unsafe 类存在 sun.misc 包,其中所有方法都是 native 修饰的,都是直接调用**操作系统底层资源**执行相应的任务,基于该类可以直接操作特定的内存数据,其内部方法操作类似 C 的指针 @@ -3530,6 +3530,7 @@ class MyAtomicInteger { Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); theUnsafe.setAccessible(true); UNSAFE = (Unsafe) theUnsafe.get(null); + // 获取 value 属性的内存地址,value 属性指向该地址,直接设置该地址的值可以修改 value 的值 VALUE_OFFSET = UNSAFE.objectFieldOffset( MyAtomicInteger.class.getDeclaredField("value")); } catch (NoSuchFieldException | IllegalAccessException e) { @@ -4923,7 +4924,7 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 成员变量 -* 线程池中存放 worker 的容器:线程池没有初始化,直接往池中加线程即可 +* 线程池中存放 Worker 的容器:线程池没有初始化,直接往池中加线程即可 ```java private final HashSet workers = new HashSet(); @@ -4950,7 +4951,7 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 private volatile int maximumPoolSize; // 线程池最大线程数量 private volatile long keepAliveTime; // 空闲线程存活时间 private volatile ThreadFactory threadFactory; // 创建线程时使用的线程工厂,默认是 DefaultThreadFactory - private final BlockingQueue workQueue;// 任务队列池中的线程达到核心线程数量后,再提交任务就放入队列 + private final BlockingQueue workQueue;// 超过核心线程提交任务就放入【阻塞队列】 ``` ```java @@ -4979,7 +4980,7 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 ```java private final class Worker extends AbstractQueuedSynchronizer implements Runnable { final Thread thread; // worker 内部封装的工作线程 - Runnable firstTask; // worker 第一个执行的任务 + Runnable firstTask; // worker 第一个执行的任务,普通的 Runnable 实现类或者是 FutureTask volatile long completedTasks; // 记录当前 worker 所完成任务数量 //构造方法 @@ -5008,13 +5009,13 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 ##### 提交方法 -* AbstractExecutorService#submit():提交任务,把任务封装成 FutureTask 执行 +* AbstractExecutorService#submit():提交任务,**把任务封装成 FutureTask 执行**,可以通过返回的任务对象调用 get 阻塞获取任务执行的结果,源码分析在笔记的 Future 部分 ```java public Future submit(Runnable task) { // 空指针异常 if (task == null) throw new NullPointerException(); - // 把 Runnable 封装成未来任务对象 + // 把 Runnable 封装成未来任务对象,执行结果就是 null,也可以通过参数指定 FutureTask#get 返回数据 RunnableFuture ftask = newTaskFor(task, null); // 执行方法 execute(ftask); @@ -5042,7 +5043,7 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 } ``` -* execute():执行任务 +* execute():执行任务,但是没有返回值,没办法获取任务执行结果 ```java // command 可以是普通的 Runnable 实现类,也可以是 FutureTask @@ -5118,7 +5119,7 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 // 条件一一般不成立,CAPACITY是5亿多,根据 core 判断使用哪个大小限制线程数量,超过了返回 false if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize)) return false; - // 记录线程数量已经加 1,相当于申请到了一块令牌,条件失败说明其他线程修改了数量 + // 记录线程数量已经加 1,类比于申请到了一块令牌,条件失败说明其他线程修改了数量 if (compareAndIncrementWorkerCount(c)) // 申请成功,跳出了 retry 这个 for 自旋 break retry; @@ -5132,6 +5133,7 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 } } //【令牌申请成功,开始创建线程】 + // 运行标记,表示创建的 worker 是否已经启动,false未启动 true启动 boolean workerStarted = false; // 添加标记,表示创建的 worker 是否添加到池子中了,默认false未添加,true是添加。 @@ -5674,8 +5676,7 @@ FutureTask 类的成员方法: //条件一:成立说明当前 task 已经被执行过了或者被 cancel 了,非 NEW 状态的任务,线程就不处理了 //条件二:线程是 NEW 状态,尝试设置当前任务对象的线程是当前线程,设置失败说明其他线程抢占了该任务,直接返回 if (state != NEW || - !UNSAFE.compareAndSwapObject(this, runnerOffset, - null, Thread.currentThread())) + !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread())) return; //直接返回 try { // 执行到这里,当前 task 一定是 NEW 状态,而且当前线程也抢占 task 成功! @@ -5804,7 +5805,7 @@ FutureTask 类的成员方法: } ``` - FutureTask#awaitDone:**get 线程阻塞等待** + FutureTask#awaitDone:**get 线程阻塞等待**,封装成 WaitNode 对象进入阻塞队列 ```java private int awaitDone(boolean timed, long nanos) throws InterruptedException { @@ -6240,21 +6241,20 @@ class MockConnection implements Connection { ### AQS -#### 思想 +#### 核心思想 + +AQS:AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架,许多同步类实现都依赖于该同步器 -AQS:AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架,许多同步类实现都依赖于它 +AQS 用状态属性来表示资源的状态(分**独占模式和共享模式**),子类需要定义如何维护这个状态,控制如何获取锁和释放锁 -* 用 state 属性来表示资源的状态(分**独占模式和共享模式**),子类需要定义如何维护这个状态,控制如何获取锁和释放锁 - * 独占模式是只有一个线程能够访问资源,如 ReentrantLock - * 共享模式允许多个线程访问资源,如 Semaphore,ReentrantReadWriteLock 是组合式 -* 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList(**同步队列:双向,便于出队入队**) -* 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet(**条件队列:单向**) +* 独占模式是只有一个线程能够访问资源,如 ReentrantLock +* 共享模式允许多个线程访问资源,如 Semaphore,ReentrantReadWriteLock 是组合式 AQS 核心思想: * 如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置锁定状态 -* 被请求的共享资源被占用,AQS 用 CLH 队列实现线程阻塞等待以及被唤醒时锁分配的(park)机制,即将暂时获取不到锁的线程加入到队列中 +* 被请求的共享资源被占用,AQS 用 CLH 队列实现线程阻塞等待以及被唤醒时锁分配的(park)机制,将暂时获取不到锁的线程加入到队列中 CLH 是一种基于单向链表的**高性能、公平的自旋锁**,AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配 @@ -6266,16 +6266,16 @@ AQS 核心思想: -#### 原理 +#### 设计原理 -设计思想: +设计原理: * 获取锁: ```java while(state 状态不允许获取) { //tryAcquire(arg) if(队列中还没有此线程) { - 入队并阻塞 park unpark + 入队并阻塞 park } } 当前线程出队 @@ -6285,34 +6285,44 @@ AQS 核心思想: ```java if(state 状态允许了) { //tryRelease(arg) - 恢复阻塞的线程(s) + 恢复阻塞的线程(s) unpark } ``` -AQS 中 state 设计: +AbstractQueuedSynchronizer 中 state 设计: + +* state 使用了 32bit int 来维护同步状态,独占模式 0 表示未加锁状态,大于 0 表示已经加锁状态 + + ```java + private volatile int state; + ``` -* state 使用了 32bit int 来维护同步状态 * state **使用 volatile 修饰配合 cas** 保证其修改时的原子性 + * state 表示**线程重入的次数或者许可进入的线程数** + * state API: - `protected final int getState()`:获取 state 状态 - `protected final void setState(int newState)`:设置 state 状态 - `protected final boolean compareAndSetState(int expect,int update)`:**cas** 设置 state -Node 节点中 waitstate 设计: + * `protected final int getState()`:获取 state 状态 + * `protected final void setState(int newState)`:设置 state 状态 + * `protected final boolean compareAndSetState(int expect,int update)`:**cas** 安全设置 state + +封装线程的 Node 节点中 waitstate 设计: * 使用 **volatile 修饰配合 CAS** 保证其修改时的原子性 * 表示 Node 节点的状态,有以下几种状态: ```java - //由于超时或中断,此节点被取消,不会再改变状态 + // 默认为 0 + volatile int waitStatus; + // 由于超时或中断,此节点被取消,不会再改变状态 static final int CANCELLED = 1; - //此节点后面的节点已(或即将)被阻止(通过park),当前节点在释放或取消时必须唤醒后面的节点 + // 此节点后面的节点已(或即将)被阻止(通过park),当前节点在释放或取消时必须唤醒后面的节点 static final int SIGNAL = -1; - //此节点当前在条件队列中 + // 此节点当前在条件队列中 static final int CONDITION = -2; - //将releaseShared传播到其他节点 + // 将releaseShared传播到其他节点 static final int PROPAGATE = -3; ``` @@ -6324,45 +6334,54 @@ Node 节点中 waitstate 设计: 队列设计: -* 使用了 FIFO 先入先出队列,并不支持优先级队列 - -* 设计时借鉴了 CLH 队列,CLH是一种单向无锁队列 - - +* 使用了 FIFO 先入先出队列,并不支持优先级队列,**同步队列是双向链表,便于出队入队** ```java - // node 放入 AQS 队列尾部,返回尾节点的前驱节点 - private Node enq(final Node node) { - for (;;) { - Node t = tail; - // 队列中还没有元素 tail 为 null - if (t == null) { - // 设置 head 为哨兵节点(不对应线程,状态为 0) - if (compareAndSetHead(new Node())) - tail = head; - } else { - // 将 node 的 prev 设置为原来的 tail 双向队列 - node.prev = t; - // 将 tail 从原来的 tail 设置为 node - if (compareAndSetTail(t, node)) { - //将原来的尾节点(哑元节点)的 next 指向新节点 - t.next = node; - return t; - } - } - } + // 头结点,指向哑元节点 + private transient volatile Node head; + // 阻塞队列的尾节点,阻塞队列不包含头结点,从 head.next → tail 认为是阻塞队列 + private transient volatile Node tail; + + static final class Node { + // 枚举:共享模式 + static final Node SHARED = new Node(); + // 枚举:独占模式 + static final Node EXCLUSIVE = null; + // node需要构建成 FIFO 队列,prev 指向前继节点 + volatile Node prev; + // next 指向后继节点 + volatile Node next; + // 当前node封装的线程 + volatile Thread thread; + // 条件队列是单向链表,只有后继指针 + Node nextWaiter; } ``` + + ![](https://gitee.com/seazean/images/raw/master/Java/JUC-AQS队列设计.png) + +* 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet,**条件队列是单向链表** + + ````java + public class ConditionObject implements Condition, java.io.Serializable { + // 指向条件队列的第一个 node 节点 + private transient Node firstWaiter; + // 指向条件队列的最后一个 node 节点 + private transient Node lastWaiter; + } + ```` + - + + *** -#### 模板 +#### 模板对象 -同步器的设计是基于模板方法模式,该模式是基于”继承“的,主要是为了在不改变模板结构的前提下在子类中重新定义模板中的内容以实现复用代码 +同步器的设计是基于模板方法模式,该模式是基于继承的,主要是为了在不改变模板结构的前提下在子类中重新定义模板中的内容以实现复用代码 * 使用者继承 `AbstractQueuedSynchronizer` 并重写指定的方法 * 将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,这些模板方法会调用使用者重写的方法 @@ -6370,11 +6389,11 @@ Node 节点中 waitstate 设计: AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的模板方法: ```java -isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。 -tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。 -tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。 -tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 -tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。 +isHeldExclusively() //该线程是否正在独占资源。只有用到condition才需要去实现它 +tryAcquire(int) //独占方式。尝试获取资源,成功则返回true,失败则返回false +tryRelease(int) //独占方式。尝试释放资源,成功则返回true,失败则返回false +tryAcquireShared(int) //共享方式。尝试获取资源。负数表示失败;0表示成功但没有剩余可用资源;正数表示成功且有剩余资源 +tryReleaseShared(int) //共享方式。尝试释放资源,成功则返回true,失败则返回false ``` * 默认情况下,每个方法都抛出 `UnsupportedOperationException` @@ -6555,12 +6574,14 @@ public ReentrantLock() { NonfairSync 继承自 AQS -没有竞争:ExclusiveOwnerThread属于 Thread-0,state 设置为1 +没有竞争:ExclusiveOwnerThread 属于 Thread-0,state 设置为 1 ```java +// ReentrantLock.NonfairSync#lock final void lock() { - //首先用 cas 尝试(仅尝试一次)将 state 从 0 改为 1, 如果成功表示获得了独占锁 + // 用 cas 尝试(仅尝试一次)将 state 从 0 改为 1, 如果成功表示获得了独占锁 if (compareAndSetState(0, 1)) + // 设置当前线程为独占线程 setExclusiveOwnerThread(Thread.currentThread()); else acquire(1);//失败进入 @@ -6570,74 +6591,113 @@ final void lock() { 第一个竞争出现: ```java -public final void acquire(int arg) { - // 当 tryAcquire 返回为 false 时, 先调用addWaiter, 接着 acquireQueued - if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); +// AbstractQueuedSynchronizer#acquire +public final void acquire(int arg) { + // tryAcquire 尝试获取锁失败返回为 false 时, 会调用 addWaiter 将当前线程封装成node入队, + // 然后 acquireQueued 挂起当前线程,返回 true 表示挂起过程中线程被中断唤醒过,false 表示未被中断过 + if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) + // 如果线程被中断了逻辑来到这,完成一次真正的打断效果 + selfInterrupt(); } ``` -Thread-1执行: +Thread-1 执行: * CAS 尝试将 state 由 0 改为 1,结果失败(第一次) -* 进入 tryAcquire 逻辑,这时 state 已经是1,结果仍然失败(第二次) +* 进入 tryAcquire 尝试获取锁逻辑,这时 state 已经是1,结果仍然失败(第二次) ```java - protected final boolean tryAcquire(int acquires) { + // ReentrantLock.NonfairSync#tryAcquire + protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } - final boolean nonfairTryAcquire(int acquires) { - final Thread current = Thread.currentThread(); - int c = getState(); - if (c == 0) { - //如果还没有获得锁,尝试用cas获得,这里体现非公平性: 不去检查 AQS 队列 - if (compareAndSetState(0, acquires)) { - setExclusiveOwnerThread(current); - return true; + // 抢占成功返回 true,抢占失败返回 false + final boolean nonfairTryAcquire(int acquires) { + final Thread current = Thread.currentThread(); + // state 值 + int c = getState(); + // 条件成立说明当前处于无锁状态 + if (c == 0) { + //如果还没有获得锁,尝试用cas获得,这里体现非公平性: 不去检查 AQS 队列是否有阻塞线程直接获取锁 + if (compareAndSetState(0, acquires)) { + // 获取锁成功设置当前线程为独占锁线程。 + setExclusiveOwnerThread(current); + return true; } } - // 如果已经获得了锁, 线程还是当前线程, 表示发生了锁重入 - else if (current == getExclusiveOwnerThread()) { - int nextc = c + acquires; - if (nextc < 0) // overflow - throw new Error("Maximum lock count exceeded"); - setState(nextc); - return true; - } - return false;//获取失败 + // 如果已经有线程获得了锁, 独占锁线程还是当前线程, 表示发生了锁重入 + else if (current == getExclusiveOwnerThread()) { + // 更新锁重入的值 + int nextc = c + acquires; + // 越界判断,当重入的深度很深时,会导致 nextc < 0,int值达到最大之后再 + 1 变负数 + if (nextc < 0) // overflow + throw new Error("Maximum lock count exceeded"); + // 更新 state 的值,这里不使用 cas 是因为当前线程正在持有锁,所以这里的操作相当于在一个管程内 + setState(nextc); + return true; + } + // 获取失败 + return false; } ``` -* 接下来进入 addWaiter 逻辑,构造 Node 队列 +* 接下来进入 addWaiter 逻辑,构造 Node 队列,前置条件是当前线程获取锁失败,说明有线程占用了锁 * 图中黄色三角表示该 Node 的 waitStatus 状态,其中 0 为默认**正常状态** * Node 的创建是懒惰的 * 其中第一个 Node 称为 **Dummy(哑元)或哨兵**,用来占位,并不关联线程 ```java - private Node addWaiter(Node mode) { + // AbstractQueuedSynchronizer#addWaiter,返回当前线程的 node 节点 + private Node addWaiter(Node mode) { // 将当前线程关联到一个 Node 对象上, 模式为独占模式 - // Node.EXCLUSIVE for exclusive, Node.SHARED for shared - Node node = new Node(Thread.currentThread(), mode); - Node pred = tail; - // 如果 tail 不为 null, cas 尝试将 Node 对象加入 AQS 队列尾部 - if (pred != null) { - node.prev = pred; - if (compareAndSetTail(pred, node)) { - pred.next = node;// 双向链表 - return node; - } - } - enq(node);//添加到尾节点 + Node node = new Node(Thread.currentThread(), mode); + Node pred = tail; + // 快速入队,如果 tail 不为 null,说明存在阻塞队列 + if (pred != null) { + // 将当前节点的前驱节点指向 尾节点 + node.prev = pred; + // 通过 cas 将 Node 对象加入 AQS 队列,成为尾节点 + if (compareAndSetTail(pred, node)) { + pred.next = node;// 双向链表 + return node; + } + } + // 等待队列为空或者 CAS 失败进入逻辑 + enq(node); return node; } ``` + ```java + // AbstractQueuedSynchronizer#enq + private Node enq(final Node node) { + // 自旋入队,必须入队成功才结束循环 + for (;;) { + Node t = tail; + // 说明当前锁被占用,且当前线程可能是【第一个获取锁失败】的线程,还没有建立队列 + if (t == null) { + // 设置一个【哑元节点】,头尾指针都指向该节点 + if (compareAndSetHead(new Node())) + tail = head; + } else { + //自旋到这,普通入队方式 + node.prev = t; + if (compareAndSetTail(t, node)) { + t.next = node; + return t; //返回当前 node 的前驱节点 + } + } + } + } + ``` + -* 线程进入 acquireQueued 逻辑 +* 线程节点加入阻塞队列成功,进入 AbstractQueuedSynchronizer#acquireQueued 逻辑阻塞线程 * acquireQueued 会在一个死循环中不断尝试获得锁,失败后进入 park 阻塞 @@ -6645,25 +6705,31 @@ Thread-1执行: ```java final boolean acquireQueued(final Node node, int arg) { + //true 表示当前线程抢占锁失败,false 表示成功 boolean failed = true; try { + // 表示当前线程是否被中断 boolean interrupted = false; for (;;) { + // 获得当前线程节点的前驱节点 final Node p = node.predecessor(); - // 上一个节点是 head, 表示轮到自己获取锁 + // 前驱节点 head, FIFO 队列的特性表示轮到当前线程可以去获取锁 if (p == head && tryAcquire(arg)) { - // 获取成功, 设置自己(当前线程对应的 node)为 head + // 获取成功, 设置当前线程自己的 node 为 head setHead(node); p.next = null; // help GC + // 表示抢占锁成功 failed = false; + // 返回当前线程是否被中断 return interrupted; } - // 判断是否应当 park,返回false后需要新一轮的循环 - if (shouldParkAfterFailedAcquire(p, node) && - parkAndCheckInterrupt()) + // 判断是否应当 park,返回 false 后需要新一轮的循环,返回 true 进入条件二阻塞线程 + if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) + // 条件二返回结果是当前线程是否被打断,没有被打断返回 false 不进入这里的逻辑 interrupted = true; } } finally { + // 可打断模式下才会进入该逻辑 if (failed) cancelAcquire(node); } @@ -6675,24 +6741,27 @@ Thread-1执行: ```java private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; + // 表示前置节点是个可以唤醒当前节点的节点,返回 true if (ws == Node.SIGNAL) - // 上一个节点都在阻塞, 那么当前线程也阻塞 return true; + // 前置节点的状态处于取消状态,需要删除前面所有取消的节点, 返回到外层循环重试 if (ws > 0) { - // 上一个节点取消, 那么重构删除前面所有取消的节点, 返回到外层循环重试 do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); + // 获取到非取消的节点,连接上当前节点 pred.next = node; + // 默认情况下 node 的 waitStatus 是 0,进入这里的逻辑 } else { // 设置上一个节点状态为 Node.SIGNAL,返回外层循环重试 compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } + // 返回不应该 park return false; } ``` - * shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued ,再次 tryAcquire 尝试获取锁,这时state 仍为 1 获取失败(第四次) + * shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued ,再次 tryAcquire 尝试获取锁,这时 state 仍为 1 获取失败(第四次) * 当再次进入 shouldParkAfterFailedAcquire 时,这时其前驱 node 的 waitStatus 已经是 -1,返回 true * 进入 parkAndCheckInterrupt, Thread-1 park(灰色表示),再有多个线程经历竞争失败后: @@ -6709,10 +6778,14 @@ Thread-1执行: +*** + ###### 解锁 +ReentrantLock#unlock:释放锁 + ```java public void unlock() { sync.release(1); @@ -6721,77 +6794,87 @@ public void unlock() { Thread-0 释放锁,进入 release 流程 -* 进入 tryRelease - - * 设置 exclusiveOwnerThread 为 null - * state = 0 +* 进入 tryRelease,设置 exclusiveOwnerThread 为 null,state = 0 * 当前队列不为 null,并且 head 的 waitStatus = -1,进入 unparkSuccessor ```java - public final boolean release(int arg) { - if (tryRelease(arg)) { - // 队列头节点 unpark - Node h = head; - if (h != null && h.waitStatus != 0) - unparkSuccessor(h); - return true; + // AbstractQueuedSynchronizer#release + public final boolean release(int arg) { + // 尝试释放锁,tryRelease 返回 true 表示当前线程已经完全释放锁 + if (tryRelease(arg)) { + // 队列头节点 + Node h = head; + // 头节点什么时候是空?没有发生锁竞争,没有竞争线程帮忙创建哑元节点 + // 条件成立说明阻塞队列有等待线程,需要唤醒 head 节点后面的线程 + if (h != null && h.waitStatus != 0) + unparkSuccessor(h); + return true; } return false; } ``` ```java - protected final boolean tryRelease(int releases) { - int c = getState() - releases; - if (Thread.currentThread() != getExclusiveOwnerThread()) - throw new IllegalMonitorStateException(); - boolean free = false; - // 支持锁重入, 只有 state 减为 0, 才释放成功 - if (c == 0) { - free = true; - setExclusiveOwnerThread(null); - } - setState(c); + // ReentrantLock.Sync#tryRelease + protected final boolean tryRelease(int releases) { + // 减去释放的值,可能重入 + int c = getState() - releases; + // 如果当前线程不是持有锁的线程直接报错 + if (Thread.currentThread() != getExclusiveOwnerThread()) + throw new IllegalMonitorStateException(); + // 是否已经完全释放锁 + boolean free = false; + // 支持锁重入, 只有 state 减为 0, 才完全释放锁成功 + if (c == 0) { + free = true; + setExclusiveOwnerThread(null); + } + // 当前线程就是持有锁线程,所以可以直接更新锁,不需要使用 CAS + setState(c); return free; } ``` -* 进入 unparkSuccessor 方法 +* 进入 AbstractQueuedSynchronizer#unparkSuccessor 方法,唤醒当前节点的后继节点 * 找到队列中离 head 最近的一个 Node(没取消的),unpark 恢复其运行,本例中即为 Thread-1 * 回到 Thread-1 的 acquireQueued 流程 ```java - private void unparkSuccessor(Node node) { + private void unparkSuccessor(Node node) { + // 当前节点的状态 int ws = node.waitStatus; if (ws < 0) - // 尝试重置状态为 0 + // 尝试重置状态为 0,因为当前节点要完成对后续节点的唤醒任务了,不需要 -1 了 compareAndSetWaitStatus(node, ws, 0); - // 找到需要 unpark 的节点,头节点的下一个 + // 找到需要 unpark 的节点,当前节点的下一个 Node s = node.next; - // 不考虑已取消的节点 - if (s == null || s.waitStatus > 0) { - s = null; - // 从 AQS 队列从后至前找到队列需要 unpark 的节点 - for (Node t = tail; t != null && t != node; t = t.prev) - if (t.waitStatus <= 0) - s = t; - } - if (s != null) + // 不考虑已取消的节点 + if (s == null || s.waitStatus > 0) { + s = null; + // AQS 队列从后至前找需要 unpark 的节点,直到 t == 当前的 node 为止 + for (Node t = tail; t != null && t != node; t = t.prev) + // 说明当前线程状态需要被唤醒 + if (t.waitStatus <= 0) + // 置换引用 + s = t; + } + // 找到合适的可以被唤醒的 node,则唤醒线程 + if (s != null) LockSupport.unpark(s.thread); } ``` -* 如果加锁成功(没有竞争),会设置 +* 唤醒的线程会从 park 位置开始执行,如果加锁成功(没有竞争),会设置 * exclusiveOwnerThread 为 Thread-1,state = 1 * head 指向刚刚 Thread-1 所在的 Node,该 Node 清空 Thread - * 原本的 head 因为从链表断开,而可被垃圾回收 + * 原本的 head 因为从链表断开,而可被垃圾回收(途中有错误,原来的头节点的 waitStatus 为 0) ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ReentrantLock-非公平锁4.png) -* 如果这时候有其它线程来竞争(非公平),例如这时有 Thread-4 来了并抢占了锁 +* 如果这时候有其它线程来竞争**(非公平)**,例如这时有 Thread-4 来了并抢占了锁 * Thread-4 被设置为 exclusiveOwnerThread,state = 1 * Thread-1 再次进入 acquireQueued 流程,获取锁失败,重新进入 park 阻塞 @@ -6837,10 +6920,10 @@ public final boolean hasQueuedPredecessors() { Node t = tail; Node h = head; Node s; - //头尾指向一个节点,列表为空,返回false, + // 头尾指向一个节点,链表为空,返回false return h != t && // 头尾之间有节点,判断头节点的下一个是不是空 - // 不是空进入最后的判断,第二个节点的线程是否是本线程 + // 不是空进入最后的判断,第二个节点的线程是否是本线程 ((s = h.next) == null || s.thread != Thread.currentThread());} ``` @@ -6852,30 +6935,30 @@ public final boolean hasQueuedPredecessors() { #### 可重入 -可重入是指同一个线程如果首次获得了这把锁,那么它是这把锁的拥有者,因此有权利再次获取这把锁,如果不可重入锁,那么第二次获得锁时,自己也会被锁挡住 +可重入是指同一个线程如果首次获得了这把锁,那么它是这把锁的拥有者,因此有权利再次获取这把锁,如果不可重入锁,那么第二次获得锁时,自己也会被锁挡住,直接造成死锁 -源码解析参考:`nonfairTryAcquire(int acquires)) `和 `tryRelease(int releases)` +源码解析参考:`nonfairTryAcquire(int acquires)) ` 和 `tryRelease(int releases)` ```java static ReentrantLock lock = new ReentrantLock(); -public static void main(String[] args) { +public static void main(String[] args) { method1(); } -public static void method1() { - lock.lock(); - try { +public static void method1() { + lock.lock(); + try { System.out.println(Thread.currentThread().getName() + " execute method1"); - method2(); - } finally { - lock.unlock(); + method2(); + } finally { + lock.unlock(); } } -public static void method2() { - lock.lock(); - try { +public static void method2() { + lock.lock(); + try { System.out.println(Thread.currentThread().getName() + " execute method2"); - } finally { - lock.unlock(); + } finally { + lock.unlock(); } } ``` @@ -6887,14 +6970,14 @@ public static void method2() { * 加锁一次解锁两次:运行程序会直接报错 ```java -public void getLock() { - lock.lock(); - lock.lock(); - try { +public void getLock() { + lock.lock(); + lock.lock(); + try { System.out.println(Thread.currentThread().getName() + "\t get Lock"); } finally { - lock.unlock(); - //lock.unlock(); + lock.unlock(); + //lock.unlock(); } } ``` @@ -6943,19 +7026,22 @@ public static void main(String[] args) throws InterruptedException { +*** + + + ##### 实现原理 * 不可打断模式:即使它被打断,仍会驻留在 AQS 队列中,一直要等到获得锁后方能得知自己被打断了 ```java public final void acquire(int arg) { - if (!tryAcquire(arg) && - acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//阻塞等待 - // 如果acquireQueued返回true,打断状态interrupted = true + if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//阻塞等待 + // 如果acquireQueued返回true,打断状态 interrupted = true selfInterrupt(); } - static void selfInterrupt() { - // 知道自己被打断了,需要重新产生一次中断完成中断效果 + static void selfInterrupt() { + // 知道自己被打断了,需要重新产生一次中断完成中断效果 Thread.currentThread().interrupt(); } ``` @@ -6970,53 +7056,112 @@ public static void main(String[] args) throws InterruptedException { setHead(node); p.next = null; // help GC failed = false; - // 还是需要获得锁后, 才能返回打断状态 + // 还是需要获得锁后, 才能返回打断状态 return interrupted; } - if (shouldParkAfterFailedAcquire(p, node) && - parkAndCheckInterrupt())//被打断 返回true - interrupted = true; + if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()){ + // 条件二中判断当前线程是否被打断,被打断返回true,设置中断标记为 true,获取锁后返回 + interrupted = true; + } } } - private final boolean parkAndCheckInterrupt() { - // 阻塞当前线程,如果打断标记已经是 true, 则 park 会失效 - LockSupport.park(this); - // 判断当前线程是否被打断,清除打断标记,被打断返回true - return Thread.interrupted(); - } } + private final boolean parkAndCheckInterrupt() { + // 阻塞当前线程,如果打断标记已经是 true, 则 park 会失效 + LockSupport.park(this); + // 判断当前线程是否被打断,清除打断标记,被打断返回true + return Thread.interrupted(); + } ``` -* 可打断模式: +* 可打断模式:AbstractQueuedSynchronizer#acquireInterruptibly ```java public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); } - public final void acquireInterruptibly(int arg) { - if (Thread.interrupted())//被其他线程打断了 - throw new InterruptedException(); - if (!tryAcquire(arg)) - // 没获取到锁,进入这里 + public final void acquireInterruptibly(int arg) { + // 被其他线程打断了直接返回 false + if (Thread.interrupted()) + throw new InterruptedException(); + if (!tryAcquire(arg)) + // 没获取到锁,进入这里 doAcquireInterruptibly(arg); } ``` ```java - private void doAcquireInterruptibly(int arg) { - final Node node = addWaiter(Node.EXCLUSIVE); - boolean failed = true; - try { - for (;;) { - //... - if (shouldParkAfterFailedAcquire(p, node)&&parkAndCheckInterrupt()) - throw new InterruptedException(); - // 在 park 过程中如果被 interrupt 会抛出异常, 而不会再次进入循环 + private void doAcquireInterruptibly(int arg) throws InterruptedException { + // 返回封装当前线程的节点 + final Node node = addWaiter(Node.EXCLUSIVE); + boolean failed = true; + try { + for (;;) { + //... + if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) + // 在 park 过程中如果被 interrupt 会抛出异常, 而不会再次进入循环获取锁后才完成打断效果 + throw new InterruptedException(); } + } finally { + // 抛出异常前x进入这里 + if (failed) + // 取消当前线程的节点 + cancelAcquire(node); + } + } + ``` + + ```java + private void cancelAcquire(Node node) { + // 判空 + if (node == null) + return; + // 把当前节点封装的 Thread 置为空 + node.thread = null; + // 获取当前取消的 node 的前驱节点 + Node pred = node.prev; + // 前驱节点也被取消了,循环找到前面最近的不是取消节点的节点 + while (pred.waitStatus > 0) + node.prev = pred = pred.prev; + + // 获取前驱节点的后继节点,可能是当前 node,也可能是 waitStatus > 0 的节点 + Node predNext = pred.next; + + // 把当前节点的状态设置为 【取消状态 1】 + node.waitStatus = Node.CANCELLED; + + // 条件成立说明当前节点是尾节点,把当前节点的前驱节点设置为尾节点 + if (node == tail && compareAndSetTail(node, pred)) { + // 把前驱节点的后继节点置空,这里直接把所有的取消节点出队 + compareAndSetNext(pred, predNext, null); + } else { + // 说明当前节点不是 tail 节点 + int ws; + // 条件一成立说明当前节点不是 head.next 节点 + if (pred != head && + // 判断前驱节点的状态是不是 -1,不成立说明前驱状态可能是 0 或者刚被其他线程取消排队了 + ((ws = pred.waitStatus) == Node.SIGNAL || + // 如果状态不是 -1,设置前驱节点的状态为 -1 + (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && + // 前驱节点的线程不为null + pred.thread != null) { + + Node next = node.next; + // 当前节点的后继节点是正常节点 + if (next != null && next.waitStatus <= 0) + // 把前驱节点的后继节点设置为 当前节点的后继节点,从队列中删除了当前节点 + compareAndSetNext(pred, predNext, next); + } else { + // 当前节点是 head.next 节点,唤醒当前节点的后继节点 + unparkSuccessor(node); + } + node.next = node; // help GC } } ``` + + *** @@ -7027,11 +7172,11 @@ public static void main(String[] args) throws InterruptedException { ##### 基本使用 -`public boolean tryLock()`:尝试获取锁,获取到返回true,获取不到直接放弃,不进入阻塞队列 +`public boolean tryLock()`:尝试获取锁,获取到返回 true,获取不到直接放弃,不进入阻塞队列 `public boolean tryLock(long timeout, TimeUnit unit)`:在给定时间内获取锁,获取不到就退出 -注意:tryLock期间也可以被打断 +注意:tryLock 期间也可以被打断 ```java public static void main(String[] args) { @@ -7067,6 +7212,10 @@ public static void main(String[] args) { +*** + + + ##### 实现原理 * tryLock() @@ -7080,10 +7229,10 @@ public static void main(String[] args) { * tryLock(long timeout, TimeUnit unit) ```java - public final boolean tryAcquireNanos(int arg, long nanosTimeout) { + public final boolean tryAcquireNanos(int arg, long nanosTimeout) { if (Thread.interrupted()) throw new InterruptedException(); - //tryAcquire 尝试一次 + //tryAcquire 尝试一次 return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout); } protected final boolean tryAcquire(int acquires) { @@ -7093,21 +7242,23 @@ public static void main(String[] args) { ```java private boolean doAcquireNanos(int arg, long nanosTimeout) { - if (nanosTimeout <= 0L) - return false; - final long deadline = System.nanoTime() + nanosTimeout; - //... - try { - for (;;) { + if (nanosTimeout <= 0L) + return false; + // 获取最后期限的时间戳 + final long deadline = System.nanoTime() + nanosTimeout; + //... + try { + for (;;) { //... - nanosTimeout = deadline - System.nanoTime(); - if (nanosTimeout <= 0L) //时间已到 - return false; - if (shouldParkAfterFailedAcquire(p, node) && - nanosTimeout > spinForTimeoutThreshold) - LockSupport.parkNanos(this, nanosTimeout); - if (Thread.interrupted()) - throw new InterruptedException(); + // 计算还需等待的时间 + nanosTimeout = deadline - System.nanoTime(); + if (nanosTimeout <= 0L) //时间已到 + return false; + if (shouldParkAfterFailedAcquire(p, node) && + nanosTimeout > spinForTimeoutThreshold) + LockSupport.parkNanos(this, nanosTimeout); + if (Thread.interrupted()) + throw new InterruptedException(); } } } @@ -7115,50 +7266,54 @@ public static void main(String[] args) { +*** + + + ##### 哲学家就餐 ```java -public static void main(String[] args) { - Chopstick c1 = new Chopstick("1");//... - Chopstick c5 = new Chopstick("5"); - new Philosopher("苏格拉底", c1, c2).start(); - new Philosopher("柏拉图", c2, c3).start(); - new Philosopher("亚里士多德", c3, c4).start(); +public static void main(String[] args) { + Chopstick c1 = new Chopstick("1");//... + Chopstick c5 = new Chopstick("5"); + new Philosopher("苏格拉底", c1, c2).start(); + new Philosopher("柏拉图", c2, c3).start(); + new Philosopher("亚里士多德", c3, c4).start(); new Philosopher("赫拉克利特", c4, c5).start(); new Philosopher("阿基米德", c5, c1).start(); } -class Philosopher extends Thread { - Chopstick left; - Chopstick right; - public void run() { - while (true) { - // 尝试获得左手筷子 - if (left.tryLock()) { - try { - // 尝试获得右手筷子 - if (right.tryLock()) { - try { - System.out.println("eating..."); - Thread.sleep(1000); - } finally { - right.unlock(); - } - } - } finally { - left.unlock(); - } - } - } +class Philosopher extends Thread { + Chopstick left; + Chopstick right; + public void run() { + while (true) { + // 尝试获得左手筷子 + if (left.tryLock()) { + try { + // 尝试获得右手筷子 + if (right.tryLock()) { + try { + System.out.println("eating..."); + Thread.sleep(1000); + } finally { + right.unlock(); + } + } + } finally { + left.unlock(); + } + } + } } } -class Chopstick extends ReentrantLock { - String name; - public Chopstick(String name) { - this.name = name; - } - @Override - public String toString() { - return "筷子{" + name + '}'; +class Chopstick extends ReentrantLock { + String name; + public Chopstick(String name) { + this.name = name; + } + @Override + public String toString() { + return "筷子{" + name + '}'; } } ``` @@ -7191,33 +7346,33 @@ Condition 类 API: ```java public static void main(String[] args) throws InterruptedException { - ReentrantLock lock = new ReentrantLock(); - //创建一个新的条件变量 - Condition condition1 = lock.newCondition(); - Condition condition2 = lock.newCondition(); - new Thread(() -> { - try { - lock.lock(); - System.out.println("进入等待"); - //进入休息室等待 - condition1.await(); - System.out.println("被唤醒了"); - } catch (InterruptedException e) { - e.printStackTrace(); - } finally { - lock.unlock(); + ReentrantLock lock = new ReentrantLock(); + //创建一个新的条件变量 + Condition condition1 = lock.newCondition(); + Condition condition2 = lock.newCondition(); + new Thread(() -> { + try { + lock.lock(); + System.out.println("进入等待"); + //进入休息室等待 + condition1.await(); + System.out.println("被唤醒了"); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + lock.unlock(); } - }).start(); - Thread.sleep(1000); - //叫醒 - new Thread(() -> { + }).start(); + Thread.sleep(1000); + //叫醒 + new Thread(() -> { try { - lock.lock(); - //唤醒 - condition2.signal(); - } finally { - lock.unlock(); - } + lock.lock(); + //唤醒 + condition2.signal(); + } finally { + lock.unlock(); + } }).start(); } ``` @@ -7230,35 +7385,43 @@ public static void main(String[] args) throws InterruptedException { ##### 实现原理 -await 流程: +###### await -* 开始 Thread-0 持有锁,调用 await,进入 ConditionObject 的 addConditionWaiter 流程 +* 开始 Thread-0 持有锁,调用 await,线程进入 ConditionObject 等待,直到被唤醒或打断,调用 await 方法的线程都是持锁状态的,所以说逻辑里**不存在并发** ```java - // 等待,直到被唤醒或打断 public final void await() throws InterruptedException { + // 判断当前线程是否是中断状态,是就直接给个中断异常 if (Thread.interrupted()) throw new InterruptedException(); - // 添加一个 Node 至等待队列, + // 将调用 await 的线程包装成 Node 添加到条件队列并返回 Node node = addConditionWaiter(); - // 释放节点持有的锁 + // 完全释放节点持有的锁,因为其他线程唤醒当前线程的前提是 持有锁 int savedState = fullyRelease(node); + + //设置打断模式为没有被打断,状态码为 0 int interruptMode = 0; - // 如果该节点还没有转移至 AQS 队列, park 阻塞 + + // 如果该节点还没有转移至 AQS 阻塞队列, park 阻塞 while (!isOnSyncQueue(node)) { LockSupport.park(this); - // 如果被打断, 退出等待队列,判断打断模式 + // 如果被打断,退出等待队列,对应的 node 【也会被迁移到阻塞队列】 if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } - // 退出等待队列后, 还需要获得 AQS 队列的锁 + // 逻辑到这说明当前线程退出等待队列,进入【阻塞队列】 + + // 释放了多少锁就重新获取多少锁,获取锁成功判断打断模式 if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT; - // 所有已取消的 Node 从队列链表删除 + + // node在条件队列时 如果被外部线程中断唤醒,会加入到阻塞队列,但是并未设nextWaiter = null if (node.nextWaiter != null) + // 清理条件队列内所有已取消的 Node unlinkCancelledWaiters(); - // 应用打断模式 + // 条件成立说明挂起期间发生过中断 if (interruptMode != 0) + // 应用打断模式 reportInterruptAfterWait(interruptMode); } ``` @@ -7270,100 +7433,184 @@ await 流程: private static final int THROW_IE = -1; ``` + ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ReentrantLock-条件变量1.png) + * 创建新的 Node 状态为 -2(Node.CONDITION),关联 Thread-0,加入等待队列尾部 ```java - // 添加一个 Node 至等待队列 private Node addConditionWaiter() { + // 获取当前条件队列的尾节点的引用,保存到局部变量 t 中 Node t = lastWaiter; - // 所有已取消的 Node 从队列链表删除, + // 当前队列中不是空,并且节点的状态不是CONDITION(-2),说明当前节点发生了中断 if (t != null && t.waitStatus != Node.CONDITION) { + // 清理条件队列内所有已取消的 Node unlinkCancelledWaiters(); + // 清理完成重新获取 尾节点 的引用 t = lastWaiter; } - // 创建一个关联当前线程的新 Node, 添加至队列尾部 + // 创建一个关联当前线程的新 node, 设置状态为 CONDITION(-2),添加至队列尾部 Node node = new Node(Thread.currentThread(), Node.CONDITION); if (t == null) - firstWaiter = node; + firstWaiter = node; // 空队列直接放在队首【不用CAS因为执行线程是持锁线程,并发安全】 else - t.nextWaiter = node; - lastWaiter = node;// 单向链表 + t.nextWaiter = node; // 非空队列队尾追加 + lastWaiter = node; // 更新队尾的引用 return node; } ``` ```java - private void unlinkCancelledWaiters() { - Node t = firstWaiter; - Node trail = null; - while (t != null) { - Node next = t.nextWaiter; - // 判断 t 节点不是 CONDITION 节点 - if (t.waitStatus != Node.CONDITION) { - // t 与下一个节点断开 - t.nextWaiter = null; - // 如果第一次循环就进入if语句,说明 t 是首节点 - if (trail == null) - firstWaiter = next; - else - // t 的前节点和后节点相连,删除 t - trail.nextWaiter = next; - // t 是尾节点了 - if (next == null) - lastWaiter = trail; - } else - trail = t; - t = next; // 把 t.next 赋值给 t + // 清理条件队列内所有已取消(不是 CONDITION)的 node + private void unlinkCancelledWaiters() { + // 从头节点开始遍历【FIFO】 + Node t = firstWaiter; + // 指向正常的 CONDITION 节点 + Node trail = null; + // 等待队列不空 + while (t != null) { + // 获取当前节点的后继节点 + Node next = t.nextWaiter; + // 判断 t 节点是不是 CONDITION 节点 + if (t.waitStatus != Node.CONDITION) { + // 不是正常节点,需要 t 与下一个节点断开 + t.nextWaiter = null; + // 条件成立说明遍历到的节点还未碰到过正常节点 + if (trail == null) + // 更新 firstWaiter 指针为下个节点 + firstWaiter = next; + else + // 让上一个正常节点指向 当前取消节点的 下一个节点,删除非正常的节点 + trail.nextWaiter = next; + // t 是尾节点了,更新 lastWaiter 指向最后一个正常节点 + if (next == null) + lastWaiter = trail; + } else { + // 正常节点赋值给 trail + trail = t; + } + // 把 t.next 赋值给 t + t = next; } } ``` * 接下来进入 AQS 的 fullyRelease 流程,释放同步器上的锁 - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ReentrantLock-条件变量1.png) - ```java // 线程可能重入,需要将 state 全部释放 - final int fullyRelease(Node node) { - boolean failed = true; - try { - int savedState = getState(); - // release -> tryRelease 公平锁解锁,会解锁重入锁 - if (release(savedState)) { - failed = false; - return savedState; - } else { - throw new IllegalMonitorStateException(); - } - } finally { - // 没有释放成功,设置为取消状态 - if (failed) - node.waitStatus = Node.CANCELLED; + final int fullyRelease(Node node) { + // 完全释放锁是否成功,false 代表成功 + boolean failed = true; + try { + // 获取当前线程所持有的 state 值总数 + int savedState = getState(); + // release -> tryRelease 解锁重入锁 + if (release(savedState)) { + // 释放成功 + failed = false; + // 返回解锁的深度 + return savedState; + } else { + // 解锁失败抛出异常 + throw new IllegalMonitorStateException(); + } + } finally { + // 没有释放成功,将当前 node 设置为取消状态 + if (failed) + node.waitStatus = Node.CANCELLED; } } ``` -* unpark AQS 队列中的下一个节点,竞争锁,假设没有其他竞争线程,那么 Thread-1 竞争成功 +* 进入 isOnSyncQueue 逻辑判断节点是否移动到阻塞队列 -* park 阻塞 Thread-0 + ```java + final boolean isOnSyncQueue(Node node) { + // node 的状态是 CONDITION,signal方法是先修改状态再迁移,所以前驱节点为空证明还没有迁移 + if (node.waitStatus == Node.CONDITION || node.prev == null) + return false; + // 说明当前节点已经成功入队到阻塞队列,条件队列的 next 指针为 null,且当前节点后面已经有其它 node + if (node.next != null) + return true; + // 阻塞队列的尾巴开始向前遍历查找 node,如果查找到返回 true,查找不到返回 false + return findNodeFromTail(node); + } + ``` + +* unpark AQS 队列中的下一个节点竞争锁,假设没那么 Thread-1 竞争成功,park 阻塞 Thread-0 ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ReentrantLock-条件变量2.png) +* 线程 park 后如果被打断或者 unpark,会进入 checkInterruptWhileWaiting 判断线程是否被打断: + ```java + private int checkInterruptWhileWaiting(Node node) { + // Thread.interrupted() 返回当前线程中断标记位,并且重置当前标记位 为 false + // 如果被中断了,根据是否在条件队列被中断的,设置中断状态码 + return Thread.interrupted() ?(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) : 0; + } + ``` -signal 流程: + ```java + // 这个方法只有在线程是被中断唤醒时才会调用 + final boolean transferAfterCancelledWait(Node node) { + // 条件成立说明当前node一定是在条件队列内,因为 signal 迁移节点到阻塞队列时,会将节点的状态修改为 0 + if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) { + // 【中断唤醒的 node 也会被加入到阻塞队列中】 + enq(node); + // 表示是在条件队列内被中断了 + return true; + } + + //执行到这里的情况: + //1.当前node已经被外部线程调用 signal 方法将其迁移到 阻塞队列 内了 + //2.当前node正在被外部线程调用 signal 方法将其迁移至 阻塞队列 进行中状态 + + // 如果当前线程还没到阻塞队列,一直释放 CPU + while (!isOnSyncQueue(node)) + Thread.yield(); + + // 表示当前节点被中断唤醒时不在条件队列了 + return false; + } + ``` + +* 最后开始处理中断状态: + + ```java + private void reportInterruptAfterWait(int interruptMode) throws InterruptedException { + //条件成立说明在条件队列内发生过中断,此时 await 方法抛出中断异常 + if (interruptMode == THROW_IE) + throw new InterruptedException(); + + //条件成立说明在条件队列外发生的中断,此时设置当前线程的中断标记位为 true + else if (interruptMode == REINTERRUPT) + // 进行一次自己打断,产生中断的效果 + selfInterrupt(); + } + ``` -* 假设 Thread-1 要来唤醒 Thread-0,进入 ConditionObject 的 doSignal 流程,取得等待队列中第一个 Node,即 Thread-0 所在 Node + + + + +*** + + + +###### signal + +* 假设 Thread-1 要来唤醒 Thread-0,进入 ConditionObject 的 doSignal 流程,取得等待队列中第一个 Node,即 Thread-0 所在 Node,必须持有锁才能唤醒, 因此 doSignal 内**线程安全** ```java - // 唤醒 - 必须持有锁才能唤醒, 因此 doSignal 内无需考虑加锁 - public final void signal() { - // 调用方法的线程是否是资源的持有线程 - if (!isHeldExclusively()) - throw new IllegalMonitorStateException(); - // 取得等待队列中第一个 Node - Node first = firstWaiter; - if (first != null) + public final void signal() { + // 判断调用signal方法的线程是否是独占锁持有线程 + if (!isHeldExclusively()) + throw new IllegalMonitorStateException(); + // 获取条件队列中第一个 Node + Node first = firstWaiter; + // 不为空就将第该节点【迁移到阻塞队列】 + if (first != null) doSignal(first); } ``` @@ -7372,14 +7619,14 @@ signal 流程: // 唤醒 - 将没取消的第一个节点转移至 AQS 队列尾部 private void doSignal(Node first) { do { - // 当前节点是尾节点 + // 当前节点是尾节点,所以队列中只有当前一个节点了 if ((firstWaiter = first.nextWaiter) == null) lastWaiter = null; first.nextWaiter = null; // 将等待队列中的 Node 转移至 AQS 队列, 不成功且还有节点则继续循环 - } while (!transferForSignal(first) && - (first = firstWaiter) != null); + } while (!transferForSignal(first) && (first = firstWaiter) != null); } + // signalAll() 会调用这个函数,唤醒所有的节点 private void doSignalAll(Node first) { lastWaiter = firstWaiter = null; do { @@ -7391,20 +7638,22 @@ signal 流程: } ``` -* 执行 transferForSignal 流程,将该 Node 加入 AQS 队列尾部,将 Thread-0 的 waitStatus 改为 0,Thread-3 的waitStatus 改为 -1 +* 执行 transferForSignal,将该 Node 加入 AQS 阻塞队列尾部,将 Thread-0 的 waitStatus 改为 0,Thread-3 的 waitStatus 改为 -1 ```java // 如果节点状态是取消, 返回 false 表示转移失败, 否则转移成功 final boolean transferForSignal(Node node) { - // 如果状态已经不是 Node.CONDITION, 说明被取消了 + // CAS 修改当前节点的状态,修改为 0,因为当前节点马上要迁移到阻塞队列了 + // 如果状态已经不是 Node.CONDITION, 说明线程被取消(await 释放全部锁失败)或者被中断(可打断 cancelAcquire) if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) return false; - // 加入 AQS 队列尾部 + // 将当前 node 入阻塞队列,p 是当前节点在阻塞队列的前驱节点 Node p = enq(node); int ws = p.waitStatus; - // 上一个节点被取消 上一个节点不能设置状态为 Node.SIGNAL + + // 前驱节点被取消或者不能设置状态为 Node.SIGNAL if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) - // unpark 取消阻塞, 让线程重新同步状态 + // unpark 取消阻塞, 让线程竞争锁,重新同步状态 LockSupport.unpark(node.thread); return true; } @@ -7426,10 +7675,11 @@ signal 流程: #### 读写锁 -独占锁:指该锁一次只能被一个线程所持有,对ReentrantLock和Synchronized而言都是独占锁 +独占锁:指该锁一次只能被一个线程所持有,对 ReentrantLock 和 Synchronized 而言都是独占锁 + 共享锁:指该锁可以被多个线程锁持有 -ReentrantReadWriteLock 其读锁是共享,其写锁是独占 +ReentrantReadWriteLock 其读锁是共享锁,其写锁是独占锁 作用:多个线程同时读一个资源类没有任何问题,为了满足并发量,读取共享资源应该同时进行,但是如果一个线程想去写共享资源,就不应该再有其它线程可以对该资源进行读或写 @@ -7469,15 +7719,17 @@ ReentrantReadWriteLock 其读锁是共享,其写锁是独占 ``` 构造方法: - `public ReentrantReadWriteLock()`:默认构造方法,非公平锁 - `public ReentrantReadWriteLock(boolean fair)`:true 为公平锁 + +* `public ReentrantReadWriteLock()`:默认构造方法,非公平锁 +* `public ReentrantReadWriteLock(boolean fair)`:true 为公平锁 常用API: - `public ReentrantReadWriteLock.ReadLock readLock()`:返回读锁 - `public ReentrantReadWriteLock.WriteLock writeLock()`:返回写锁 - `public void lock()`:加锁 - `public void unlock()`:解锁 - `public boolean tryLock()`:尝试获取锁 + +* `public ReentrantReadWriteLock.ReadLock readLock()`:返回读锁 +* `public ReentrantReadWriteLock.WriteLock writeLock()`:返回写锁 +* `public void lock()`:加锁 +* `public void unlock()`:解锁 +* `public boolean tryLock()`:尝试获取锁 读读并发: From b7ea86d2a68ea3f1ffce70566e596ed5586e6fc6 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 15 Aug 2021 15:13:03 +0800 Subject: [PATCH 021/168] Update Java Notes --- Java.md | 72 +++++++++++++++++++++++++++------------------------------ 1 file changed, 34 insertions(+), 38 deletions(-) diff --git a/Java.md b/Java.md index 7aa57d1..ded431c 100644 --- a/Java.md +++ b/Java.md @@ -668,14 +668,11 @@ public static void sum(int... nums){ 方法(method)是将具有独立功能的代码块组织成为一个整体,使其具有特殊功能的代码集 -注意:方法必须先创建才可以使用,该过程成为方法定义 - 方法创建后并不是直接可以运行的,需要手动使用后,才执行,该过程成为方法调用 - -在方法内部定义的叫局部变量,局部变量不能加static,包括protected, private, public这些也不能加 - -原因:局部变量是保存在栈中的,而静态变量保存于方法区,局部变量出了方法就被栈回收了,而静态变量不会,所以在局部变量前不能加static关键字,静态变量是定义在类中,又叫类变量 +注意:方法必须先创建才可以使用,该过程成为方法定义,方法创建后并不是直接可以运行的,需要手动使用后才执行,该过程成为方法调用 +在方法内部定义的叫局部变量,局部变量不能加 static,包括 protected、private、public 这些也不能加 +原因:局部变量是保存在栈中的,而静态变量保存于方法区(JDK8 在堆中),局部变量出了方法就被栈回收了,而静态变量不会,所以在局部变量前不能加 static 关键字,静态变量是定义在类中,又叫类变量 @@ -708,8 +705,8 @@ public static 返回值类型 方法名(参数) { 如果方法操作完毕 -* void类型的方法,直接调用即可,而且方法体中一般不写return -* 非void类型的方法,推荐用变量接收调用 +* void 类型的方法,直接调用即可,而且方法体中一般不写return +* 非 void 类型的方法,推荐用变量接收调用 原理:每个方法在被调用执行的时候,都会进入栈内存,并且拥有自己独立的内存空间,方法内部代码调用完毕之后,会从栈内存中弹栈消失。 @@ -735,7 +732,7 @@ public static 返回值类型 方法名(参数) { } ``` -* void表示无返回值,可以省略return,也可以单独的书写return,后面不加数据 +* void 表示无返回值,可以省略 return,也可以单独的书写 return,后面不加数据 ```java public static void methodTwo() { @@ -840,6 +837,7 @@ public class MethodDemo { 值传递和引用传递的区别在于传递后会不会影响实参的值:值传递会创建副本,引用传递不会创建副本 * 基本数据类型:形式参数的改变,不影响实际参数 + 每个方法在栈内存中,都会有独立的栈空间,方法运行结束后就会弹栈消失 ```java @@ -857,8 +855,9 @@ public class MethodDemo { ``` * 引用类型:形式参数的改变,影响实际参数的值 + **引用数据类型的传参,本质上是将对象的地址以值的方式传递到形参中**,内存中会造成两个引用指向同一个内存的效果,所以即使方法弹栈,堆内存中的数据也已经是改变后的结果 - + ```java public class PassByValueExample { public static void main(String[] args) { @@ -874,7 +873,7 @@ public class MethodDemo { String name;//..... } ``` - + @@ -885,7 +884,7 @@ public class MethodDemo { ### 枚举 -枚举是Java中的一种特殊类型,为了做信息的标志和信息的分类 +枚举是 Java 中的一种特殊类型,为了做信息的标志和信息的分类 定义枚举的格式: @@ -964,9 +963,9 @@ public class MethodDemo { ### Debug -Debug是供程序员使用的程序调试工具,它可以用于查看程序的执行流程,也可以用于追踪程序执行过程来调试程序。 +Debug 是供程序员使用的程序调试工具,它可以用于查看程序的执行流程,也可以用于追踪程序执行过程来调试程序。 -加断点->Debug运行->单步运行->看Debugger窗口->看Console窗口 +加断点 → Debug 运行 → 单步运行 → 看 Debugger 窗口 → 看 Console 窗口 ![](https://gitee.com/seazean/images/raw/master/Java/Debug按键说明.png) @@ -992,11 +991,11 @@ Debug是供程序员使用的程序调试工具,它可以用于查看程序的 **三大特征:封装,继承,多态** -面向对象最重要的两个概念:类和对象。 +面向对象最重要的两个概念:类和对象 -* 类:相同事物共同特征的描述。类只是学术上的一个概念并非真实存在的,只能描述一类事物。 -* 对象:是真实存在的实例, 实例==对象。**对象是类的实例化**! -* 结论:有了类和对象就可以描述万千世界所有的事物。 必须先有类才能有对象。 +* 类:相同事物共同特征的描述。类只是学术上的一个概念并非真实存在的,只能描述一类事物 +* 对象:是真实存在的实例, 实例==对象,**对象是类的实例化** +* 结论:有了类和对象就可以描述万千世界所有的事物。 必须先有类才能有对象 @@ -1090,8 +1089,8 @@ public class ClassDemo { 封装的步骤: -1. **成员变量应该私有,用private修饰,只能在本类中直接访问** -2. **提供成套的getter和setter方法暴露成员变量的取值和赋值** +1. **成员变量应该私有,用 private 修饰,只能在本类中直接访问** +2. **提供成套的 getter 和 setter 方法暴露成员变量的取值和赋值** 使用 private 修饰成员变量的原因:实现数据封装,不想让别人使用修改你的数据,比较安全 @@ -1106,7 +1105,7 @@ public class ClassDemo { this 关键字的作用: * this 关键字代表了当前对象的引用 -* this 出现在方法中:**哪个对象调用这个方法this就代表谁** +* this 出现在方法中:**哪个对象调用这个方法 this 就代表谁** * this 可以出现在构造器中:代表构造器正在初始化的那个对象 * this 可以区分变量是访问的成员变量还是局部变量 @@ -1120,23 +1119,19 @@ this 关键字的作用: #### 基本介绍 -Java是通过成员变量是否有static修饰来区分是类的还是属于对象的。 +Java 是通过成员变量是否有 static 修饰来区分是类的还是属于对象的。 static 静态修饰的成员(方法和成员变量)属于类本身的。 -按照有无static修饰,成员变量和方法可以分为: +按照有无 static 修饰,成员变量和方法可以分为: * 成员变量: - * 静态成员变量(类变量): - 有static修饰的成员变量称为静态成员变量也叫类变量,属于类本身,**与类一起加载一次,只有一个**,直接用类名访问即可。 - * 实例成员变量: - 无static修饰的成员变量称为实例成员变量,属于类的每个对象的。**与类的对象一起加载**,对象有多少个,实例成员变量就加载多少个,必须用类的对象来访问。 - + * 静态成员变量(类变量):static 修饰的成员变量,属于类本身,**与类一起加载一次,只有一个**,直接用类名访问即可 + * 实例成员变量:无 static 修饰的成员变量,属于类的每个对象的,**与类的对象一起加载**,对象有多少个,实例成员变量就加载多少个,必须用类的对象来访问 + * 成员方法: - * 静态方法: - 有static修饰的成员方法称为静态方法也叫类方法,属于类本身的,直接用类名访问即可。 - * 实例方法: - 无static修饰的成员方法称为实例方法,属于类的每个对象的,必须用类的对象来访问。 + * 静态方法:有 static 修饰的成员方法称为静态方法也叫类方法,属于类本身的,直接用类名访问即可 + * 实例方法:无 static 修饰的成员方法称为实例方法,属于类的每个对象的,必须用类的对象来访问 @@ -1157,12 +1152,12 @@ static 静态修饰的成员(方法和成员变量)属于类本身的。 成员方法的访问语法: -* 静态方法:有static修饰,属于类 +* 静态方法:有 static 修饰,属于类 * 类名.静态方法(同一个类中访问静态成员可以省略类名不写) - * 对象.静态方法(不推荐,参考 JVM类加载--> 字节码 --> 方法调用) + * 对象.静态方法(不推荐,参考 JVM → 运行机制 → 方法调用) -* 实例方法:无static修饰,属于对象 +* 实例方法:无 static 修饰,属于对象 * 对象.实例方法 @@ -1195,11 +1190,11 @@ static 静态修饰的成员(方法和成员变量)属于类本身的。 内存问题: -* **栈内存存放main方法和地址** +* **栈内存存放 main 方法和地址** * **堆内存存放对象和变量** -* **方法区存放class和静态变量(jdk8以后移入堆)** +* **方法区存放 class 和静态变量(jdk8 以后移入堆)** 访问问题: @@ -1366,7 +1361,7 @@ class Animal{ 1. 子类的构造器的第一行默认 super() 调用父类的无参数构造器,写不写都存在 2. 子类继承父类,子类就得到了父类的属性和行为。调用子类构造器初始化子类对象数据时,必须先调用父类构造器初始化继承自父类的属性和行为 - 3. 参考JVM -> 类加载 -> 对象创建 + 3. 参考 JVM → 类加载 → 对象创建 ```java class Animal{ @@ -1387,6 +1382,7 @@ class Animal{ ``` * **为什么 Java 是单继承的?** + 答:反证法,假如 Java 可以多继承,请看如下代码: ```java From 40a6241ad6dfccf76775d9f804ce4fcc638e234d Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 15 Aug 2021 16:52:29 +0800 Subject: [PATCH 022/168] Update Java Notes --- DB.md | 2 +- Prog.md | 819 +++++++++++++++++++++++++++++++++++++++++--------------- SSM.md | 23 +- 3 files changed, 622 insertions(+), 222 deletions(-) diff --git a/DB.md b/DB.md index f0bdfca..bd9170a 100644 --- a/DB.md +++ b/DB.md @@ -10902,7 +10902,7 @@ Cache Aside Pattern 中服务端需要同时维系 DB 和 cache,并且是以 D 时序导致的不一致问题: -* 在写数据的过程中,不能先删除 cache 再更新 DB,因为会造成缓存的不一致。比如请求 1 先写数据 A,请求2随后读数据 A,当请求 1 删除 cache 后,请求 2 直接读取了 DB,此时请求 1 还没写入 DB +* 在写数据的过程中,不能先删除 cache 再更新 DB,因为会造成缓存的不一致。比如请求 1 先写数据 A,请求 2 随后读数据 A,当请求 1 删除 cache 后,请求 2 直接读取了 DB,此时请求 1 还没写入 DB * 在写数据的过程中,先更新 DB 再删除 cache 也会出现问题,但是概率很小,因为缓存的写入速度非常快 diff --git a/Prog.md b/Prog.md index 348d226..2e24687 100644 --- a/Prog.md +++ b/Prog.md @@ -799,7 +799,9 @@ public class demo { ##### 同步方法 -作用:把出现线程安全问题的核心方法锁起来,每次只能一个线程进入访问 +把出现线程安全问题的核心方法锁起来,每次只能一个线程进入访问 + +synchronized 修饰的方法的不具备继承性,所以子类是线程不安全的,如果子类的方法也被 synchronized 修饰,两个锁对象其实是一把锁,而且是**子类对象作为锁** 用法:直接给方法加上一个修饰符 synchronized @@ -2495,7 +2497,7 @@ volatile 是 Java 虚拟机提供的**轻量级**的同步机制(三大特性 - 不保证原子性 - 保证有序性(禁止指令重排) -性能:volatile修饰的变量进行读操作与普通变量几乎没什么差别,但是写操作相对慢一些,因为需要在本地代码中插入很多内存屏障来保证指令不会发生乱序执行,但是开销比锁要小 +性能:volatile 修饰的变量进行读操作与普通变量几乎没什么差别,但是写操作相对慢一些,因为需要在本地代码中插入很多内存屏障来保证指令不会发生乱序执行,但是开销比锁要小 @@ -6736,7 +6738,7 @@ Thread-1 执行: } ``` - * 进入 shouldParkAfterFailedAcquire 逻辑,将前驱 node,即 head 的 waitStatus 改为 -1,返回 false;waitStatus 为 -1 的节点用来唤醒下一个节点 + * 进入 shouldParkAfterFailedAcquire 逻辑,**将前驱 node 的 waitStatus 改为 -1**,返回 false;waitStatus 为 -1 的节点用来唤醒下一个节点 ```java private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { @@ -6751,7 +6753,7 @@ Thread-1 执行: } while (pred.waitStatus > 0); // 获取到非取消的节点,连接上当前节点 pred.next = node; - // 默认情况下 node 的 waitStatus 是 0,进入这里的逻辑 + // 【默认情况下 node 的 waitStatus 是 0,进入这里的逻辑】 } else { // 设置上一个节点状态为 Node.SIGNAL,返回外层循环重试 compareAndSetWaitStatus(pred, ws, Node.SIGNAL); @@ -6997,7 +6999,7 @@ public void getLock() { * 如果没有竞争此方法就会获取 lock 对象锁 * 如果有竞争就进入阻塞队列,可以被其他线程用 interrupt 打断 -注意:如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断 +注意:如果是不可中断模式,那么即使使用了 interrupt 也不会让等待状态中的线程中断 ```java public static void main(String[] args) throws InterruptedException { @@ -7032,7 +7034,7 @@ public static void main(String[] args) throws InterruptedException { ##### 实现原理 -* 不可打断模式:即使它被打断,仍会驻留在 AQS 队列中,一直要等到获得锁后方能得知自己被打断了 +* 不可打断模式:即使它被打断,仍会驻留在 AQS 队列中,一直要**等到获得锁后才能得知自己被打断**了 ```java public final void acquire(int arg) { @@ -7074,7 +7076,7 @@ public static void main(String[] args) throws InterruptedException { } ``` -* 可打断模式:AbstractQueuedSynchronizer#acquireInterruptibly +* 可打断模式:AbstractQueuedSynchronizer#acquireInterruptibly,**被打断后会直接抛出异常** ```java public void lockInterruptibly() throws InterruptedException { @@ -7341,7 +7343,10 @@ Condition 类 API: * **await / signal 前需要获得锁** * await 执行后,会释放锁进入 conditionObject 等待 -* await 的线程被唤醒(打断、超时)去重新竞争 lock 锁 +* await 的线程被唤醒去重新竞争 lock 锁 + +* 线程在条件队列被打断会抛出中断异常 + * 竞争 lock 锁成功后,从 await 后继续执行 ```java @@ -7399,7 +7404,7 @@ public static void main(String[] args) throws InterruptedException { // 完全释放节点持有的锁,因为其他线程唤醒当前线程的前提是 持有锁 int savedState = fullyRelease(node); - //设置打断模式为没有被打断,状态码为 0 + // 设置打断模式为没有被打断,状态码为 0 int interruptMode = 0; // 如果该节点还没有转移至 AQS 阻塞队列, park 阻塞 @@ -7435,7 +7440,7 @@ public static void main(String[] args) throws InterruptedException { ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ReentrantLock-条件变量1.png) -* 创建新的 Node 状态为 -2(Node.CONDITION),关联 Thread-0,加入等待队列尾部 +* **创建新的 Node 状态为 -2(Node.CONDITION)**,关联 Thread-0,加入等待队列尾部 ```java private Node addConditionWaiter() { @@ -7460,7 +7465,7 @@ public static void main(String[] args) throws InterruptedException { ``` ```java - // 清理条件队列内所有已取消(不是 CONDITION)的 node + // 清理条件队列内所有已取消(不是CONDITION)的 node private void unlinkCancelledWaiters() { // 从头节点开始遍历【FIFO】 Node t = firstWaiter; @@ -7526,7 +7531,7 @@ public static void main(String[] args) throws InterruptedException { ```java final boolean isOnSyncQueue(Node node) { - // node 的状态是 CONDITION,signal方法是先修改状态再迁移,所以前驱节点为空证明还没有迁移 + // node 的状态是 CONDITION,signal 方法是先修改状态再迁移,所以前驱节点为空证明还没有迁移 if (node.waitStatus == Node.CONDITION || node.prev == null) return false; // 说明当前节点已经成功入队到阻塞队列,条件队列的 next 指针为 null,且当前节点后面已经有其它 node @@ -7541,7 +7546,7 @@ public static void main(String[] args) throws InterruptedException { ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ReentrantLock-条件变量2.png) -* 线程 park 后如果被打断或者 unpark,会进入 checkInterruptWhileWaiting 判断线程是否被打断: +* 线程 park 后如果被 unpark 或者被打断,都会进入 checkInterruptWhileWaiting 判断线程是否被打断: ```java private int checkInterruptWhileWaiting(Node node) { @@ -7558,7 +7563,7 @@ public static void main(String[] args) throws InterruptedException { if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) { // 【中断唤醒的 node 也会被加入到阻塞队列中】 enq(node); - // 表示是在条件队列内被中断了 + // 表示是在条件队列内被中断了,设置为 THROW_IE -1 return true; } @@ -7570,7 +7575,7 @@ public static void main(String[] args) throws InterruptedException { while (!isOnSyncQueue(node)) Thread.yield(); - // 表示当前节点被中断唤醒时不在条件队列了 + // 表示当前节点被中断唤醒时不在条件队列了,设置为 REINTERRUPT 1 return false; } ``` @@ -7579,11 +7584,11 @@ public static void main(String[] args) throws InterruptedException { ```java private void reportInterruptAfterWait(int interruptMode) throws InterruptedException { - //条件成立说明在条件队列内发生过中断,此时 await 方法抛出中断异常 + // 条件成立说明在条件队列内发生过中断,此时 await 方法抛出中断异常 if (interruptMode == THROW_IE) throw new InterruptedException(); - //条件成立说明在条件队列外发生的中断,此时设置当前线程的中断标记位为 true + // 条件成立说明在条件队列外发生的中断,此时设置当前线程的中断标记位为 true else if (interruptMode == REINTERRUPT) // 进行一次自己打断,产生中断的效果 selfInterrupt(); @@ -7651,7 +7656,7 @@ public static void main(String[] args) throws InterruptedException { Node p = enq(node); int ws = p.waitStatus; - // 前驱节点被取消或者不能设置状态为 Node.SIGNAL + // 如果前驱节点被取消或者不能设置状态为 Node.SIGNAL if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) // unpark 取消阻塞, 让线程竞争锁,重新同步状态 LockSupport.unpark(node.thread); @@ -7679,7 +7684,7 @@ public static void main(String[] args) throws InterruptedException { 共享锁:指该锁可以被多个线程锁持有 -ReentrantReadWriteLock 其读锁是共享锁,其写锁是独占锁 +ReentrantReadWriteLock 其**读锁是共享锁,写锁是独占锁** 作用:多个线程同时读一个资源类没有任何问题,为了满足并发量,读取共享资源应该同时进行,但是如果一个线程想去写共享资源,就不应该再有其它线程可以对该资源进行读或写 @@ -7696,7 +7701,7 @@ ReentrantReadWriteLock 其读锁是共享锁,其写锁是独占锁 } ``` -* 读-读 能共存、读-写 不能共存、写-写 不能共存 +* 读-读能共存、读-写不能共存、写-写不能共存 * 读锁不支持条件变量 @@ -7790,21 +7795,19 @@ public static void main(String[] args) { ##### 加锁原理 -读写锁用的是同一个 Sycn 同步器,因此等待队列、state 等也是同一个,原理与 ReentrantLock 加锁相比没有特殊之处,不同是写锁状态占了 state 的低 16 位,而读锁使用的是 state 的高 16 位 +读写锁用的是同一个 Sycn 同步器,因此等待队列、state 等也是同一个,原理与 ReentrantLock 加锁相比没有特殊之处,不同是**写锁状态占了 state 的低 16 位,而读锁使用的是 state 的高 16 位** * t1 w.lock(写锁),成功上锁 state = 0_1 ```java //lock() -> sync.acquire(1); public final void acquire(int arg) { - // 尝试获得写锁 - if (!tryAcquire(arg) && - // 获得写锁失败,将当前线程关联到一个 Node 对象上, 模式为独占模式 - acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) + // 尝试获得写锁,获得写锁失败,将当前线程关联到一个 Node 对象上, 模式为独占模式 + if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } ``` - + ```java protected final boolean tryAcquire(int acquires) { Thread current = Thread.currentThread(); @@ -7812,7 +7815,8 @@ public static void main(String[] args) { // 获得低 16 位, 代表写锁的 state 计数 int w = exclusiveCount(c); if (c != 0) { - // c != 0 and w == 0 表示r != 0,有读锁,并且写锁的拥有者不是自己,获取失败 + // c != 0 and w == 0 表示 r != 0,有读锁,读锁不能升级,直接返回false + // w != 0 说明有写锁,写锁的拥有者不是自己,获取失败 if (w == 0 || current != getExclusiveOwnerThread()) return false; // 锁重入计数超过低 16 位, 报异常 @@ -7822,11 +7826,11 @@ public static void main(String[] args) { setState(c + acquires); return true; } - // c == 0,没有任何锁,判断写锁是否该阻塞 - if (writerShouldBlock() || - !compareAndSetState(c, c + acquires)) + + // c == 0,没有任何锁,判断写锁是否该阻塞,不阻塞尝试获取锁,获取失败返回 false + if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) return false; - // 获得锁成功 + // 获得锁成功,设置锁的持有线程为当前线程 setExclusiveOwnerThread(current); return true; } @@ -7839,7 +7843,7 @@ public static void main(String[] args) { return hasQueuedPredecessors(); } ``` - + * t2 r.lock(读锁),进入 tryAcquireShared 流程,如果有写锁占据,那么 tryAcquireShared * 返回 -1 表示失败 @@ -7862,10 +7866,11 @@ public static void main(String[] args) { protected final int tryAcquireShared(int unused) { Thread current = Thread.currentThread(); int c = getState(); - // 如果是其它线程持有写锁, 并且写锁的持有者不是当前线程,获取读锁失败 - if (exclusiveCount(c) != 0 && //低 16 位, 代表写锁的 state - getExclusiveOwnerThread() != current) + // 低 16 位, 代表写锁的 state + // 如果是其它线程持有写锁, 并且写锁的持有者不是当前线程,获取读锁失败,写锁允许降级 + if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return -1; + // 高 16 位,代表读锁的 state int r = sharedCount(c); if (!readerShouldBlock() && // 读锁不该阻塞 @@ -7897,6 +7902,7 @@ public static void main(String[] args) { for (;;) { // 获取前驱节点 final Node p = node.predecessor(); + // 如果前驱节点就头节点就去尝试获取锁 if (p == head) { // 再一次尝试获取读锁 int r = tryAcquireShared(arg); @@ -7910,10 +7916,8 @@ public static void main(String[] args) { return; } } - // 是否在获取读锁失败时阻塞 - if (shouldParkAfterFailedAcquire(p, node) && - // park 当前线程 - parkAndCheckInterrupt()) + // 是否在获取读锁失败时阻塞 park 当前线程 + if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { @@ -7967,11 +7971,11 @@ public static void main(String[] args) { } ``` -* 如果没有成功,在 doAcquireShared 内 for (;;) 循环一次,shouldParkAfterFailedAcquire 内把前驱节点的 waitStatus 改为 -1,再 for (;;) 循环一次尝试 tryAcquireShared,不成功在parkAndCheckInterrupt() 处 park +* 如果没有成功,在 doAcquireShared 内 for (;;) 循环一次,shouldParkAfterFailedAcquire 内把前驱节点的 waitStatus 改为 -1,再 for (;;) 循环一次尝试 tryAcquireShared,不成功在 parkAndCheckInterrupt() 处 park -* 这种状态下,假设又有t3 r.lock,t4 w.lock,这期间 t1 仍然持有锁,就变成了下面的样子 +* 这种状态下,假设又有 t3 r.lock,t4 w.lock,这期间 t1 仍然持有锁,就变成了下面的样子 ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ReentrantReadWriteLock加锁2.png) @@ -8008,9 +8012,8 @@ public static void main(String[] args) { * 下一个节点不是 shared 了,因此不会继续唤醒 t4 所在节点 -* t2 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,但计数还不为零 - t3 同样让计数减一,计数为零,进入doReleaseShared() 将头节点从 -1 改为 0 并唤醒下一个节点 - +* t2 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,但计数还不为零,t3 同样让计数减一,计数为零,进入doReleaseShared() 将头节点从 -1 改为 0 并唤醒下一个节点 + ```java public void unlock() { sync.releaseShared(1); @@ -8023,7 +8026,7 @@ public static void main(String[] args) { return false; } ``` - + ```java protected final boolean tryReleaseShared(int unused) { // @@ -8037,7 +8040,7 @@ public static void main(String[] args) { } } ``` - + * t4 在 acquireQueued 中 parkAndCheckInterrupt 处恢复运行,再次 for (;;) 这次自己是头节点的临节点,并且没有其他节点竞争,tryAcquire(1) 成功,修改头结点,流程结束 @@ -8075,7 +8078,7 @@ StampedLock:读写锁,该类自 JDK 8 加入,是为了进一步优化读 lock.unlockWrite(stamp); ``` -* 乐观读,StampedLock 支持`tryOptimisticRead()`方法,读取完毕后做一次**戳校验**,如果校验通过,表示这期间没有其他线程的写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全 +* 乐观读,StampedLock 支持 `tryOptimisticRead()` 方法,读取完毕后做一次**戳校验**,如果校验通过,表示这期间没有其他线程的写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全 ```java long stamp = lock.tryOptimisticRead(); @@ -8087,8 +8090,8 @@ StampedLock:读写锁,该类自 JDK 8 加入,是为了进一步优化读 提供一个数据容器类内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法: -* 读-读 可以优化 -* 读-写 优化读,补加读锁 +* 读-读可以优化 +* 读-写优化读,补加读锁 ```java public static void main(String[] args) throws InterruptedException { @@ -8154,9 +8157,474 @@ class DataContainerStamped { +### CountDown + +#### 基本使用 + +CountDownLatch:计数器,用来进行线程同步协作,等待所有线程完成 + +构造器: + +* `public CountDownLatch(int count)`:初始化唤醒需要的 down 几步 + +常用API: + +* `public void await() `:让当前线程等待,必须 down 完初始化的数字才可以被唤醒,否则进入无限等待 +* `public void countDown()`:计数器进行减1(down 1) + +应用:同步等待多个 Rest 远程调用结束 + +```java +// LOL 10人进入游戏倒计时 +public static void main(String[] args) throws InterruptedException { + CountDownLatch latch = new CountDownLatch(10); + ExecutorService service = Executors.newFixedThreadPool(10); + String[] all = new String[10]; + Random random = new Random(); + + for (int j = 0; j < 10; j++) { + int finalJ = j;//常量 + service.submit(() -> { + for (int i = 0; i <= 100; i++) { + Thread.sleep(random.nextInt(100)); //随机休眠 + all[finalJ] = i + "%"; + System.out.print("\r" + Arrays.toString(all)); // \r代表覆盖 + } + latch.countDown(); + }); + } + latch.await(); + System.out.println("\n游戏开始"); + service.shutdown(); +} +/* +[100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%] +游戏开始 +``` + + + +*** + + + +#### 实现原理 + +阻塞等待: + +* 线程调用 await() 等待其他线程完成任务: + + ```java + public void await() throws InterruptedException { + sync.acquireSharedInterruptibly(1); + } + // AbstractQueuedSynchronizer#acquireSharedInterruptibly + public final void acquireSharedInterruptibly(int arg) throws InterruptedException { + // 判断线程是否被打断,抛出打断异常 + if (Thread.interrupted()) + throw new InterruptedException(); + // 尝试获取共享锁,条件成立说明 state > 0,此时线程入队阻塞等待 + // 条件不成立说明 state = 0,此时不需要阻塞线程,直接结束函数调用 + if (tryAcquireShared(arg) < 0) + doAcquireSharedInterruptibly(arg); + } + // CountDownLatch.Sync#tryAcquireShared + protected int tryAcquireShared(int acquires) { + return (getState() == 0) ? 1 : -1; + } + ``` + +* 线程进入 AbstractQueuedSynchronizer#doAcquireSharedInterruptibly 函数阻塞挂起,等待 latch 变为 0: + + ```java + private void doAcquireSharedInterruptibly(int arg) throws InterruptedException { + // 将调用latch.await()方法的线程 包装成 SHARED 类型的 node 加入到 AQS 的阻塞队列中 + final Node node = addWaiter(Node.SHARED); + boolean failed = true; + try { + for (;;) { + // 获取当前节点的前驱节点 + final Node p = node.predecessor(); + // 前驱节点时头节点就可以尝试获取锁 + if (p == head) { + // 再次尝试获取锁,获取成功返回 1 + int r = tryAcquireShared(arg); + if (r >= 0) { + // 获取锁成功,设置当前节点为 head 节点,并且向后传播 + setHeadAndPropagate(node, r); + p.next = null; // help GC + failed = false; + return; + } + } + // 阻塞在这里 + if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) + throw new InterruptedException(); + } + } finally { + // 阻塞线程被中断后抛出异常,进入取消节点的逻辑 + if (failed) + cancelAcquire(node); + } + } + ``` + +* 获取共享锁成功,进入唤醒阻塞队列中与头节点相连的 SHARED 模式的节点: + + ```java + private void setHeadAndPropagate(Node node, int propagate) { + Node h = head; + // 将当前节点设置为新的 head 节点,前驱节点和持有线程置为 null + setHead(node); + // propagate = 1,条件一成立 + if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { + // 获取当前节点的后继节点 + Node s = node.next; + // 当前节点是尾节点时 next 为 null,或者后继节点是 SHARED共享模式 + if (s == null || s.isShared()) + doReleaseShared(); + } + } + ``` + + ```java + // 唤醒后继节点 + private void doReleaseShared() { + for (;;) { + Node h = head; + // 判断队列是否是空队列 + if (h != null && h != tail) { + int ws = h.waitStatus; + // 头节点的状态为 signal,说明后继节点没有被唤醒过 + if (ws == Node.SIGNAL) { + // cas 设置头节点的状态为 0,设置失败继续自旋 + if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) + continue; + // 唤醒后继节点 + unparkSuccessor(h); + } + // 如果有其他线程已经设置了头节点的状态,重新设置为 PROPAGATE 传播属性 + else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) + continue; + } + // 条件不成立说明被唤醒的节点非常积极,直接将自己设置为了新的head, + // 此时唤醒它的节点(前驱)执行 h == head 不成立,所以不会跳出循环,会继续唤醒新 head 节点的后继 + if (h == head) + break; + } + } + ``` + + + +计数减一: + +* 线程进入 countDown() 完成计数器减一(释放锁)的操作 + + ```java + public void countDown() { + sync.releaseShared(1); + } + public final boolean releaseShared(int arg) { + // 尝试释放共享锁 + if (tryReleaseShared(arg)) { + // 释放锁成功开始唤醒阻塞节点 + doReleaseShared(); + return true; + } + return false; + } + ``` + +* 更新 state 值,每调用一次,state 值减一,当 state -1 正好为 0 时,返回 true + + ```java + protected boolean tryReleaseShared(int releases) { + for (;;) { + int c = getState(); + // 条件成立说明前面已经有线程触发唤醒操作了,这里返回 false + if (c == 0) + return false; + // 计数器减一 + int nextc = c-1; + if (compareAndSetState(c, nextc)) + // 计数器为 0 时返回 true + return nextc == 0; + } + } + ``` + + + + + +*** + + + +### CyclicBarrier + +#### 基本使用 + +CyclicBarrier:循环屏障,用来进行线程协作,等待线程满足某个计数,才能触发自己执行 + +常用方法: + +* `public CyclicBarrier(int parties, Runnable barrierAction)`:用于在线程到达屏障 parties 时,执行 barrierAction + * parties:代表多少个线程到达屏障开始触发线程任务 + * barrierAction:线程任务 +* `public int await()`:线程调用 await 方法通知 CyclicBarrier 本线程已经到达屏障 + +与 CountDownLatch 的区别:CyclicBarrier 是可以重用的 + +应用:可以实现多线程中,某个任务在等待其他线程执行完毕以后触发 + +```java +public static void main(String[] args) { + ExecutorService service = Executors.newFixedThreadPool(2); + CyclicBarrier barrier = new CyclicBarrier(2, () -> { + System.out.println("task1 task2 finish..."); + }); + + for (int i = 0; i < 3; i++) {// 循环重用 + service.submit(() -> { + System.out.println("task1 begin..."); + try { + Thread.sleep(1000); + barrier.await(); // 2 - 1 = 1 + } catch (InterruptedException | BrokenBarrierException e) { + e.printStackTrace(); + } + }); + + service.submit(() -> { + System.out.println("task2 begin..."); + try { + Thread.sleep(2000); + barrier.await(); // 1 - 1 = 0 + } catch (InterruptedException | BrokenBarrierException e) { + e.printStackTrace(); + } + }); + } + service.shutdown(); +} +``` + + + +*** + + + +#### 实现原理 + +##### 成员属性 + +* 全局锁: + + ```java + // barrier 实现是依赖于Condition条件队列,condition 条件队列必须依赖lock才能使用 + private final ReentrantLock lock = new ReentrantLock(); + // 线程挂起实现使用的 condition 队列,当前代所有线程到位,这个条件队列内的线程才会被唤醒 + private final Condition trip = lock.newCondition(); + ``` + +* 线程数量: + + ```java + private final int parties; // 代表多少个线程到达屏障开始触发线程任务 + private int count; // 表示当前“代”还有多少个线程未到位,初始值为 parties + ``` + +* 当前代中最后一个线程到位后要执行的事件: + + ```java + private final Runnable barrierCommand; + ``` + +* 代: + + ```java + // 表示 barrier 对象当前 代 + private Generation generation = new Generation(); + private static class Generation { + // 表示当前“代”是否被打破,如果被打破再来到这一代的线程 就会直接抛出 BrokenException 异常 + // 且在这一代挂起的线程 都会被唤醒,然后抛出 BrokerException 异常。 + boolean broken = false; + } + ``` + +* 构造方法: + + ```java + public CyclicBarrie(int parties, Runnable barrierAction) { + // 因为小于等于 0 的 barrier 没有任何意义 + if (parties <= 0) throw new IllegalArgumentException(); + + this.parties = parties; + this.count = parties; + // 可以为 null + this.barrierCommand = barrierAction; + } + ``` + + + + + +*** + + + +##### 成员方法 + +* await():阻塞等待所有线程到位 + + ```java + public int await() throws InterruptedException, BrokenBarrierException { + try { + return dowait(false, 0L); + } catch (TimeoutException toe) { + throw new Error(toe); // cannot happen + } + } + ``` + + ```java + // timed:表示当前调用await方法的线程是否指定了超时时长,如果 true 表示线程是响应超时的 + // nanos:线程等待超时时长,单位是纳秒 + private int dowait(boolean timed, long nanos) { + final ReentrantLock lock = this.lock; + // 加锁 + lock.lock(); + try { + // 获取当前代 + final Generation g = generation; + + // 如果当前代是已经被打破状态,则当前调用await方法的线程,直接抛出Broken异常 + if (g.broken) + throw new BrokenBarrierException(); + // 如果当前线程的中断标记位为 true,则打破当前代,然后当前线程抛出中断异常 + if (Thread.interrupted()) { + // 设置当前代的状态为 broken 状态,唤醒在 trip 条件队列内的线程 + breakBarrier(); + throw new InterruptedException(); + } + + // 逻辑到这说明,当前线程中断状态是 false, 当前代的 broken 为 false(未打破状态) + + // 假设 parties 给的是 5,那么index对应的值为 4,3,2,1,0 + int index = --count; + // 条件成立说明当前线程是最后一个到达 barrier 的线程 + if (index == 0) { + // 栅栏任务启动标记 + boolean ranAction = false; + try { + final Runnable command = barrierCommand; + if (command != null) + // 启动任务 + command.run(); + // run()未抛出异常的话,启动标记设置为 true + ranAction = true; + // 开启新的一代 + nextGeneration(); + // 返回 0 因为当前线程是此代最后一个到达的线程,index == 0 + return 0; + } finally { + // 如果 command.run() 执行抛出异常的话,会进入到这里 + if (!ranAction) + breakBarrier(); + } + } + + // 自旋,一直到条件满足、当前代被打破、线程被中断,等待超时 + for (;;) { + try { + // 根据是否需要超时等待选择阻塞方法 + if (!timed) + // 当前线程释放掉 lock,进入到 trip 条件队列的尾部挂起自己,等待被唤醒 + trip.await(); + else if (nanos > 0L) + nanos = trip.awaitNanos(nanos); + } catch (InterruptedException ie) { + // 被中断后来到这里的逻辑 + + // 当前代没有变化并且没有被打破 + if (g == generation && !g.broken) { + // 打破屏障 + breakBarrier(); + // node节点在【条件队列】内收到中断信号时 会抛出中断异常 + throw ie; + } else { + // 等待过程中代变化了,完成一次自我打断 + Thread.currentThread().interrupt(); + } + } + // 被唤醒执行到这里 + // 当前代已经被打破,线程唤醒后依次抛出 BrokenBarrier 异常 + if (g.broken) + throw new BrokenBarrierException(); + + //当前线程挂起期间,最后一个线程到位了,然后触发了开启新的一代的逻辑,此时唤醒 trip 条件队列内的线程 + if (g != generation) + return index; + // 当前线程 trip 中等待超时,然后主动转移到阻塞队列 + if (timed && nanos <= 0L) { + breakBarrier(); + // 抛出超时异常 + throw new TimeoutException(); + } + } + } finally { + // 解锁 + lock.unlock(); + } + } + ``` + +* breakBarrier():打破 Barrier 屏障 + + ```java + private void breakBarrier() { + //将代中的 broken 设置为 true,表示这一代是被打破了,再来到这一代的线程,直接抛出异常 + generation.broken = true; + //重置 count 为 parties + count = parties; + // 将在trip条件队列内挂起的线程全部唤醒,唤醒后的线程会检查当前是否是打破的 + trip.signalAll(); + } + ``` + +* nextGeneration():开启新的下一代 + + ```java + private void nextGeneration() { + // 将在 trip 条件队列内挂起的线程全部唤醒 + trip.signalAll(); + // 重置 count 为 parties + count = parties; + + // 开启新的一代..使用一个新的generation对象,表示新的一代,新的一代和上一代【没有任何关系】 + generation = new Generation(); + } + ``` + + + +参考视频:https://space.bilibili.com/457326371/ + + + + + +**** + + + ### Semaphore -#### 信号量 +#### 基本使用 synchronized 可以起到锁的作用,但某个时间段内,只能有一个线程允许执行 @@ -8170,7 +8638,7 @@ Semaphore(信号量)用来限制能同时访问共享资源的线程上限 常用API: * `public void acquire()`:表示获取许可 -* `public void release()`:表示释放许可,acquire()和release()方法之间的代码为同步代码 +* `public void release()`:表示释放许可,acquire() 和 release() 方法之间的代码为同步代码 ```java public static void main(String[] args) { @@ -8222,19 +8690,23 @@ public static void main(String[] args) { public final void acquireSharedInterruptibly(int arg) { if (Thread.interrupted()) throw new InterruptedException(); + // 尝试获取通行证,获取成功返回 >= 0的值 if (tryAcquireShared(arg) < 0) + // 获取许可证失败,进入阻塞 doAcquireSharedInterruptibly(arg); } // tryAcquireShared() -> nonfairTryAcquireShared() - // 非公平,公平锁会在循环内 hasQueuedPredecessors()方法判断是否有临头节点(第二个节点) + // 非公平,公平锁会在循环内 hasQueuedPredecessors()方法判断阻塞队列是否有临头节点(第二个节点) final int nonfairTryAcquireShared(int acquires) { for (;;) { + // 获取 state ,state 这里表示通行证 int available = getState(); + // 计算当前线程获取通行证完成之后,通行证还剩余数量 int remaining = available - acquires; // 如果许可已经用完, 返回负数, 表示获取失败, if (remaining < 0 || - // 如果 cas 重试成功, 返回正数, 表示获取成功 + // 许可证足够分配的,如果 cas 重试成功, 返回正数, 表示获取成功 compareAndSetState(available, remaining)) return remaining; } @@ -8243,55 +8715,78 @@ public static void main(String[] args) { ```java private void doAcquireSharedInterruptibly(int arg) { + // 将调用 Semaphore.aquire 方法的线程,包装成 node 加入到 AQS 的阻塞队列中 final Node node = addWaiter(Node.SHARED); + // 获取标记 boolean failed = true; try { for (;;) { final Node p = node.predecessor(); + // 前驱节点是头节点可以再次获取许可 if (p == head) { // 再次尝试获取许可 int r = tryAcquireShared(arg); if (r >= 0) { // 成功后本线程出队(AQS), 所在 Node设置为 head // r 表示可用资源数, 为 0 则不会继续传播 - setHeadAndPropagate(node, r); //参考 PROPAGATE + setHeadAndPropagate(node, r); p.next = null; // help GC failed = false; return; } } // 不成功, 设置上一个节点 waitStatus = Node.SIGNAL, 下轮进入 park 阻塞 - if (shouldParkAfterFailedAcquire(p, node) && - parkAndCheckInterrupt()) + if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) throw new InterruptedException(); } } finally { + // 被打断后进入该逻辑 if (failed) cancelAcquire(node); } } ``` + ```java + private void setHeadAndPropagate(Node node, int propagate) { + Node h = head; + // 设置自己为 head 节点 + setHead(node); + // propagate 表示有共享资源(例如共享读锁或信号量) + // head waitStatus == Node.SIGNAL 或 Node.PROPAGATE,doReleaseShared 函数中设置的 + if (propagate > 0 || h == null || h.waitStatus < 0 || + (h = head) == null || h.waitStatus < 0) { + Node s = node.next; + // 如果是最后一个节点或者是等待共享读锁的节点,做一次唤醒 + if (s == null || s.isShared()) + doReleaseShared(); + } + } + ``` + ![](https://gitee.com/seazean/images/raw/master/Java/JUC-Semaphore工作流程1.png) * 这时 Thread-4 释放了 permits,状态如下 ```java // release() -> releaseShared() - public final boolean releaseShared(int arg) { - if (tryReleaseShared(arg)) { - doReleaseShared(); - return true; + public final boolean releaseShared(int arg) { + // 尝试释放锁 + if (tryReleaseShared(arg)) { + doReleaseShared(); + return true; } return false; } protected final boolean tryReleaseShared(int releases) { - for (;;) { - int current = getState(); - int next = current + releases; + for (;;) { + // 获取当前锁资源的可用许可证数量 + int current = getState(); + int next = current + releases; + // 索引越界判断 if (next < current) throw new Error("Maximum permit count exceeded"); - // 释放锁 + // 释放锁 if (compareAndSetState(current, next)) return true; } @@ -8315,8 +8810,7 @@ public static void main(String[] args) { #### PROPAGATE -假设存在某次循环中队列里排队的结点情况为 `head(-1)->t1(-1)->t2(0)` -假设存在将要信号量释放的 T3 和 T4,释放顺序为先 T3 后 T4 +假设存在某次循环中队列里排队的结点情况为 `head(-1) → t1(-1) → t2(0)`,存在将要释放信号量的 T3 和 T4,释放顺序为先 T3 后 T4 ```java // 老版本代码 @@ -8335,13 +8829,13 @@ private void setHeadAndPropagate(Node node, int propagate) { 正常流程: * T3 调用 releaseShared(1),直接调用了 unparkSuccessor(head),head.waitStatus 从 -1 变为 0 -* T1 由于 T3 释放信号量被唤醒,然后T4 释放,唤醒 T2 +* T1 由于 T3 释放信号量被唤醒,然后 T4 释放,唤醒 T2 -BUG流程: +BUG 流程: * T3 调用 releaseShared(1),直接调用了 unparkSuccessor(head),head.waitStatus 从 -1 变为 0 * T1 由于 T3 释放信号量被唤醒,调用 tryAcquireShared,返回值为 0(获取锁成功,但没有剩余资源量) -* T1 还没调用setHeadAndPropagate方法,T4 调用 releaseShared(1),此时 head.waitStatus 为 0(此时读到的 head 和 1 中为同一个head),不满足条件,因此不调用 unparkSuccessor(head) +* T1 还没调用 setHeadAndPropagate 方法,T4 调用 releaseShared(1),此时 head.waitStatus 为 0(此时读到的 head 和 1 中为同一个 head),不满足条件,因此不调用 unparkSuccessor(head) * T1 获取信号量成功,调用 setHeadAndPropagate(t1.node, 0) 时,因为不满足 propagate > 0(剩余资源量 == 0),从而不会唤醒后继结点, **T2 线程得不到唤醒** @@ -8351,155 +8845,54 @@ BUG流程: * T3 调用 releaseShared(1),直接调用了 unparkSuccessor(head),head.waitStatus 从 -1 变为 0 * T1 由于 T3 释放信号量被唤醒,调用 tryAcquireShared,返回值为 0(获取锁成功,但没有剩余资源量) -* T1 还没调用setHeadAndPropagate方法,T4 调用 releaseShared(),此时 head.waitStatus 为 0(此时读到的 head 和 1 中为同一个 head),调用 doReleaseShared() 将等待状态置为**PROPAGATE(-3)** +* T1 还没调用 setHeadAndPropagate 方法,T4 调用 releaseShared(),此时 head.waitStatus 为 0(此时读到的 head 和 1 中为同一个 head),调用 doReleaseShared() 将等待状态置为 **PROPAGATE(-3)** * T1 获取信号量成功,调用 setHeadAndPropagate 时,读到 h.waitStatus < 0,从而调用 doReleaseShared() 唤醒 T2 ```java private void setHeadAndPropagate(Node node, int propagate) { - Node h = head; - // 设置自己为 head 节点 - setHead(node); - // propagate 表示有共享资源(例如共享读锁或信号量) - // head waitStatus == Node.SIGNAL 或 Node.PROPAGATE + Node h = head; + // 设置自己为 head 节点 + setHead(node); + // propagate 表示有共享资源(例如共享读锁或信号量) + // head waitStatus == Node.SIGNAL 或 Node.PROPAGATE if (propagate > 0 || h == null || h.waitStatus < 0 || - (h = head) == null || h.waitStatus < 0) { - Node s = node.next; - // 如果是最后一个节点或者是等待共享读锁的节点,做一次唤醒 - if (s == null || s.isShared()) - doReleaseShared(); - - }} + (h = head) == null || h.waitStatus < 0) { + Node s = node.next; + // 如果是最后一个节点或者是等待共享读锁的节点,做一次唤醒 + if (s == null || s.isShared()) + doReleaseShared(); + } +} ``` ```java // 唤醒 -private void doReleaseShared() { +private void doReleaseShared() { // 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark // 如果 head.waitStatus == 0 ==> Node.PROPAGATE - for (;;) { - Node h = head; - if (h != null && h != tail) { - int ws = h.waitStatus; - if (ws == Node.SIGNAL) { - // 防止 unparkSuccessor 被多次执行 - if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) - continue; - // 唤醒后继节点 - unparkSuccessor(h); - } - // 如果已经是 0 了,改为 -3,用来解决传播性 - else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; - } - if (h == head) - break; - } -} -``` - - - - - -*** - - - -### CountDown - -CountDownLatch:计数器,用来进行线程同步协作,等待所有线程完成 - -构造器: - -* `public CountDownLatch(int count)`:初始化唤醒需要的 down 几步 - -常用API: - -* `public void await() `:让当前线程等待,必须 down 完初始化的数字才可以被唤醒,否则进入无限等待 -* `public void countDown()`:计数器进行减1(down 1) - -应用:同步等待多个 Rest 远程调用结束 - -```java -// LOL 10人进入游戏倒计时 -public static void main(String[] args) throws InterruptedException { - CountDownLatch latch = new CountDownLatch(10); - ExecutorService service = Executors.newFixedThreadPool(10); - String[] all = new String[10]; - Random random = new Random(); - - for (int j = 0; j < 10; j++) { - int finalJ = j;//常量 - service.submit(() -> { - for (int i = 0; i <= 100; i++) { - Thread.sleep(random.nextInt(100));//随机休眠 - all[finalJ] = i + "%"; - System.out.print("\r" + Arrays.toString(all));// \r代表覆盖 + for (;;) { + Node h = head; + if (h != null && h != tail) { + int ws = h.waitStatus; + if (ws == Node.SIGNAL) { + // 防止 unparkSuccessor 被多次执行 + if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) + continue; + // 唤醒后继节点 + unparkSuccessor(h); } - latch.countDown(); - }); + // 如果已经是 0 了,改为 -3,用来解决传播性 + else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) + continue; + } + if (h == head) + break; } - latch.await(); - System.out.println("\n游戏开始"); - service.shutdown(); } -/* -[100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%] -游戏开始 ``` -*** - - - -### CyclicBarrier - -CyclicBarrier作用:循环屏障,用来进行线程协作,等待线程满足某个计数,才能触发自己执行 - -常用方法: - -* `public CyclicBarrier(int parties, Runnable barrierAction)`:用于在线程到达屏障parties时,执行barrierAction - * parties:代表多少个线程到达屏障开始触发线程任务 - * barrierAction:线程任务 -* `public int await()`:线程调用 await 方法通知 CyclicBarrier 本线程已经到达屏障 - -与 CountDownLatch 的区别:CyclicBarrier 是可以重用的 - -应用:可以实现多线程中,某个任务在等待其他线程执行完毕以后触发 - -```java -public static void main(String[] args) { - ExecutorService service = Executors.newFixedThreadPool(2); - CyclicBarrier barrier = new CyclicBarrier(2, () -> { - System.out.println("task1 task2 finish..."); - }); - - for (int i = 0; i < 3; i++) {// 循环重用 - service.submit(() -> { - System.out.println("task1 begin..."); - try { - Thread.sleep(1000); - barrier.await(); // 2 - 1 = 1 - } catch (InterruptedException | BrokenBarrierException e) { - e.printStackTrace(); - } - }); - - service.submit(() -> { - System.out.println("task2 begin..."); - try { - Thread.sleep(2000); - barrier.await(); // 1 - 1 = 0 - } catch (InterruptedException | BrokenBarrierException e) { - e.printStackTrace(); - } - }); - } - service.shutdown(); -} -``` - *** @@ -9837,6 +10230,7 @@ public CopyOnWriteArraySet() { ```java public boolean add(E e) { final ReentrantLock lock = this.lock; + // 加锁,保证线程安全 lock.lock(); try { // 获取旧的数组 @@ -9860,12 +10254,13 @@ public CopyOnWriteArraySet() { ```java public void forEach(Consumer action) { if (action == null) throw new NullPointerException(); - // 获取数据集合,放入 - Object[] elements = getArray();// 返回当前存储数据的数组 + // 获取当前存储数据的数组 + Object[] elements = getArray(); int len = elements.length; for (int i = 0; i < len; ++i) { //遍历 - @SuppressWarnings("unchecked") E e = (E) elements[i]; + @SuppressWarnings("unchecked") + E e = (E) elements[i]; // 对给定的参数执行此操作 action.accept(e); } @@ -9876,7 +10271,7 @@ public CopyOnWriteArraySet() { * 迭代器: - CopyOnWriteArrayList 在返回一个迭代器时,会基于创建这个迭代器时内部数组所拥有的数据,创建一个该内部数组当前的快照,然后迭代器遍历的是该快照,而不是内部的数组,所以这种实现方式也存在一定的数据延迟性,即对其他线程并行添加的数据不可见 + CopyOnWriteArrayList 在返回一个迭代器时,会基于创建这个迭代器时内部数组所拥有的数据,创建一个该内部数组当前的快照,即使其他替换了原始数组,迭代器遍历的是该快照依然引用的是创建快照时的数组,所以这种实现方式也存在一定的数据延迟性,即对其他线程并行添加的数据不可见 ```java public Iterator iterator() { @@ -9921,7 +10316,16 @@ public CopyOnWriteArraySet() { | 3 | Thread-1 setArray(arrayCopy) | | 4 | Thread-0 array[index] | -Thread-0读到了脏数据 +Thread-0 读到了脏数据 + +不一定弱一致性就不好 + +* 数据库的**事务隔离级别**就是弱一致性的表现 +* 并发高和一致性是矛盾的,需要权衡 + + + +*** @@ -9946,11 +10350,6 @@ public static void main(String[] args) throws InterruptedException { } ``` -不一定弱一致性就不好 - -* 数据库的事务隔离级别都是弱一致性的表现 -* 并发高和一致性是矛盾的,需要权衡 - *** diff --git a/SSM.md b/SSM.md index b389fb8..7575a2a 100644 --- a/SSM.md +++ b/SSM.md @@ -7426,7 +7426,7 @@ public int loadBeanDefinitions(Resource resource) { -**说明:源码部分的笔记做的不一定适合所有人观看,作者采用流水线式的解析重要的代码,解析的结构类似于树状,如果视觉疲劳可以去网上参考一些博客和流程图学习源码。** +**说明:源码部分的笔记不一定适合所有人阅读,作者采用流水线式去解析重要的代码,解析的结构类似于树状,如果视觉疲劳可以去网上参考一些博客和流程图学习源码。** @@ -7518,25 +7518,26 @@ AbstractApplicationContext.refresh(): `registryProcessor.postProcessBeanDefinitionRegistry(registry)`:向 bf 中注册一些 bd - `registryProcessors.add(registryProcessor)`:添加到 registryProcessors 集合 + `registryProcessors.add(registryProcessor)`:添加到 BeanDefinitionRegistryPostProcessor 集合 - * `regularPostProcessors.add(postProcessor)`:添加到普通集合 + * `regularPostProcessors.add(postProcessor)`:添加到 BeanFactoryPostProcessor 集合 - * 获取到所有 BeanDefinitionRegistryPostProcessor 和 BeanFactoryPostProcessor 接口类型了,首先回调 bdrpp 类 + * 逻辑到这里已经获取到所有 BeanDefinitionRegistryPostProcessor 和 BeanFactoryPostProcessor 接口类型的后置处理器 - * **执行实现了 PriorityOrdered(主排序接口)接口的 bdrpp,再执行实现了 Ordered(次排序接口)接口的 bdrpp** - - * **最后执行没有实现任何优先级或者是顺序接口 bdrpp** - - `boolean reiterate = true`:控制 while 是否需要再次循环,循环内是查找并执行 bdrpp 后处理器的 registry 相关的接口方法,接口方法执行以后会向 bf 内注册 bd,注册的 bd 也有可能是 bdrpp 类型,所以需要该变量控制循环 + * **首先回调 BeanDefinitionRegistryPostProcessor 类的后置处理方法** + * 获取实现了 PriorityOrdered(主排序接口)接口的 bdrpp,进行 sort 排序,然后全部执行并放入已经处理过的集合 + * 再执行实现了 Ordered(次排序接口)接口的 bdrpp + * 最后执行没有实现任何优先级或者是顺序接口 bdrpp,`boolean reiterate = true` 控制 while 是否需要再次循环,循环内是查找并执行 bdrpp 后处理器的 registry 相关的接口方法,接口方法执行以后会向 bf 内注册 bd,注册的 bd 也有可能是 bdrpp 类型,所以需要该变量控制循环 + * `processedBeans.add(ppName)`:已经执行过的后置处理器存储到该集合中 * ` invokeBeanFactoryPostProcessors()`:BeanDefinitionRegistryPostProcessor 也继承了 BeanFactoryPostProcessor,也有 postProcessBeanFactory 方法,所以需要调用 - * 执行普通 BeanFactoryPostProcessor 的相关 postProcessBeanFactory 方法,同 bdrpp,按照主次无次序执行 + * **执行普通 BeanFactoryPostProcessor 的相关 postProcessBeanFactory 方法,按照主次无次序执行** + * `if (processedBeans.contains(ppName))`:会过滤掉已经执行过的后置处理器 + * `beanFactory.clearMetadataCache()`:清除缓存中合并的 bean 定义,因为后置处理器可能更改了元数据 - **以上是 BeanFactory 的创建及预准备工作,接下来进入 Bean 的流程** * registerBeanPostProcessors(beanFactory):**注册 Bean 的后置处理器**,为了干预 Spring 初始化 bean 的流程,这里仅仅是向容器中**注入而非使用** From 00a58dcce7437b8b69ae430ba7c39f9cbae71e9d Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 16 Aug 2021 11:01:39 +0800 Subject: [PATCH 023/168] Update README.md --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 47a94a7..58cd281 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ **Java** 学习笔记,记录着作者从编程入门到深入学习的所有知识,每次学有所获就会更新笔记,所有的知识都有借鉴其他前辈的博客和文章,排版布局**美观整洁**,如果对各位朋友有所帮助,希望可以给个 star。 +个人邮箱:imseazean@gmail.com + 内容说明: * DB:MySQL、Redis @@ -12,8 +14,11 @@ 其他说明: -* 推荐使用 Typora 阅读笔记,观看效果更佳。 * 所有的知识不保证权威性,如果各位朋友发现错误,非常欢迎与我讨论。 + * Java.md 更新后大于 1M,导致网页无法显示,所以划分为 Java 和 Program 两个文档。 -个人邮箱:imseazean@gmail.com +* 推荐使用 Typora 阅读笔记,打开目录栏效果更佳,示例展示: + + ![](https://gitee.com/seazean/images/raw/master/Java/Java-图片.png) + From ebdbdff1b85544cc964fe2482051604026bb477c Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 16 Aug 2021 11:01:58 +0800 Subject: [PATCH 024/168] Update Java Notes --- Prog.md | 583 +------------------------------------------------------- 1 file changed, 6 insertions(+), 577 deletions(-) diff --git a/Prog.md b/Prog.md index 2e24687..28ef918 100644 --- a/Prog.md +++ b/Prog.md @@ -3465,13 +3465,13 @@ ABA 问题:当进行获取主内存值时,该内存值在写入主内存时 其他线程先把 A 改成 B 又改回 A,主线程仅能判断出共享变量的值与最初值 A 是否相同,不能感知到这种从 A 改为 B 又 改回 A 的情况,这时 CAS 虽然成功,但是过程存在问题 * 构造方法: - `public AtomicStampedReference(V initialRef, int initialStamp)`:初始值和初始版本号 + * `public AtomicStampedReference(V initialRef, int initialStamp)`:初始值和初始版本号 * 常用API: - ` public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)`:期望引用和期望版本号都一致才进行 CAS 修改数据 - `public void set(V newReference, int newStamp)`:设置值和版本号 - `public V getReference()`:返回引用的值 - `public int getStamp()`:返回当前版本号 + * ` public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)`:期望引用和期望版本号都一致才进行 CAS 修改数据 + * `public void set(V newReference, int newStamp)`:设置值和版本号 + * `public V getReference()`:返回引用的值 + * `public int getStamp()`:返回当前版本号 ```java public static void main(String[] args) { @@ -3643,578 +3643,6 @@ Servlet 为了保证其线程安全,一般不为 Servlet 设置成员变量, ### Local -#### 基本介绍 - -ThreadLocal 类用来提供线程内部的局部变量,这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量,分配在 TLAB - -ThreadLocal 实例通常来说都是 `private static` 类型的,属于一个线程的本地变量,用于关联线程和线程上下文。每个线程都会在 ThreadLocal 中保存一份该线程独有的数据,所以是线程安全的 - -ThreadLocal 作用: - -* 线程并发:应用在多线程并发的场景下 - -* 传递数据:通过ThreadLocal实现在同一线程不同函数或组件中传递公共变量,减少传递复杂度 - -* 线程隔离:每个线程的变量都是独立的,不会互相影响 - -对比 synchronized: - -| | synchronized | ThreadLocal | -| ------ | ------------------------------------------------------------ | ------------------------------------------------------------ | -| 原理 | 同步机制采用**以时间换空间**的方式,只提供了一份变量,让不同的线程排队访问 | ThreadLocal采用**以空间换时间**的方式,为每个线程都提供了一份变量的副本,从而实现同时访问而相不干扰 | -| 侧重点 | 多个线程之间访问资源的同步 | 多线程中让每个线程之间的数据相互隔离 | - - - -*** - - - -#### 基本使用 - -##### 常用方法 - -| 方法 | 描述 | -| -------------------------- | ---------------------------- | -| ThreadLocal<>() | 创建 ThreadLocal 对象 | -| protected T initialValue() | 返回当前线程局部变量的初始值 | -| public void set( T value) | 设置当前线程绑定的局部变量 | -| public T get() | 获取当前线程绑定的局部变量 | -| public void remove() | 移除当前线程绑定的局部变量 | - -```java -public class MyDemo { - - private static ThreadLocal tl = new ThreadLocal<>(); - - private String content; - - private String getContent() { - // 获取当前线程绑定的变量 - return tl.get(); - } - - private void setContent(String content) { - // 变量content绑定到当前线程 - tl.set(content); - } - - public static void main(String[] args) { - MyDemo demo = new MyDemo(); - for (int i = 0; i < 5; i++) { - Thread thread = new Thread(new Runnable() { - @Override - public void run() { - // 设置数据 - demo.setContent(Thread.currentThread().getName() + "的数据"); - System.out.println("-----------------------"); - System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent()); - } - }); - thread.setName("线程" + i); - thread.start(); - } - } -} -``` - - - -*** - - - -##### 应用场景 - -ThreadLocal 适用于如下两种场景 - -- 每个线程需要有自己单独的实例 -- 实例需要在多个方法中共享,但不希望被多线程共享 - -**事务管理**,ThreadLocal方案有两个突出的优势: - -1. 传递数据:保存每个线程绑定的数据,在需要的地方可以直接获取,避免参数直接传递带来的代码耦合问题 - -2. 线程隔离:各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失 - -```java -public class JdbcUtils { - // ThreadLocal对象,将connection绑定在当前线程中 - private static final ThreadLocal tl = new ThreadLocal(); - // c3p0 数据库连接池对象属性 - private static final ComboPooledDataSource ds = new ComboPooledDataSource(); - // 获取连接 - public static Connection getConnection() throws SQLException { - //取出当前线程绑定的connection对象 - Connection conn = tl.get(); - if (conn == null) { - //如果没有,则从连接池中取出 - conn = ds.getConnection(); - //再将connection对象绑定到当前线程中,非常重要的操作 - tl.set(conn); - } - return conn; - } - // ... -} -``` - -用 ThreadLocal 使 SimpleDateFormat 从独享变量变成单个线程变量: - -```java -public class ThreadLocalDateUtil { - private static ThreadLocal threadLocal = new ThreadLocal() { - @Override - protected DateFormat initialValue() { - return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - } - }; - - public static Date parse(String dateStr) throws ParseException { - return threadLocal.get().parse(dateStr); - } - - public static String format(Date date) { - return threadLocal.get().format(date); - } -} -``` - - - -**** - - - -#### 底层结构 - -JDK8 以前:每个 ThreadLocal 都创建一个 Map,然后用线程作为 Map 的 key,要存储的局部变量作为 Map 的 value,这样就能达到各个线程的局部变量隔离的效果 - -![](https://gitee.com/seazean/images/raw/master/Java/JUC-ThreadLocal数据结构JDK8前.png) - -JDK8 以后:每个 Thread 维护一个 ThreadLocalMap,这个 Map 的 key 是 ThreadLocal 实例本身,value 才是真正要存储的值 Object - -* 每个 Thread 线程内部都有一个 Map (ThreadLocalMap) -* Map 里面存储 ThreadLocal 对象(key)和线程的变量副本(value) -* Thread 内部的 Map 是由 ThreadLocal 维护的,由 ThreadLocal 负责向 map 获取和设置线程的变量值。 -* 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成副本的隔离,互不干扰 - -![](https://gitee.com/seazean/images/raw/master/Java/JUC-ThreadLocal数据结构JDK8后.png) - -JDK8 前后对比: - -* 每个 Map 存储的 Entry 数量会变少,因为之前的存储数量由 Thread 的数量决定,现在由 ThreadLocal 的数量决定,在实际编程当中,往往 ThreadLocal 的数量要少于 Thread 的数量 -* 当 Thread 销毁之后,对应的 ThreadLocalMap 也会随之销毁,能减少内存的使用 - - - -*** - - - -#### 成员方法 - -* set() - - * 获取当前线程,并根据当前线程获取一个Map - * 获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key) - * 如果Map为空,则给该线程创建 Map,并设置初始值 - - ```java - // 设置当前线程对应的ThreadLocal的值 - public void set(T value) { - // 获取当前线程对象 - Thread t = Thread.currentThread(); - // 获取此线程对象中维护的ThreadLocalMap对象 - ThreadLocalMap map = getMap(t); - // 判断map是否存在 - if (map != null) - // 存在则调用map.set设置此实体entry - map.set(this, value); - else - // 调用createMap进行ThreadLocalMap对象的初始化 - createMap(t, value); - } - - // 获取当前线程Thread对应维护的ThreadLocalMap - ThreadLocalMap getMap(Thread t) { - return t.threadLocals; - } - // 创建当前线程Thread对应维护的ThreadLocalMap - void createMap(Thread t, T firstValue) { - //这里的this是调用此方法的threadLocal - t.threadLocals = new ThreadLocalMap(this, firstValue); - } - ``` - -* get() - - ```java - // 获取当前线程的 ThreadLocalMap 变量,如果存在则返回值,不存在则创建并返回初始值 - public T get() { - // 获取当前线程对象 - Thread t = Thread.currentThread(); - // 获取此线程对象中维护的ThreadLocalMap对象 - ThreadLocalMap map = getMap(t); - // 如果此map存在 - if (map != null) { - // 以当前的ThreadLocal 为 key,调用getEntry获取对应的存储实体e - ThreadLocalMap.Entry e = map.getEntry(this); - // 对e进行判空 - if (e != null) { - @SuppressWarnings("unchecked") - // 获取存储实体 e 对应的 value值 - T result = (T)e.value; - return result; - } - } - /*初始化 : 有两种情况有执行当前代码 - 第一种情况: map不存在,表示此线程没有维护的ThreadLocalMap对象 - 第二种情况: map存在, 但是没有与当前ThreadLocal关联的entry*/ - return setInitialValue(); - } - - // 初始化 - private T setInitialValue() { - // 调用initialValue获取初始化的值,此方法可以被子类重写, 如果不重写默认返回null - T value = initialValue(); - Thread t = Thread.currentThread(); - ThreadLocalMap map = getMap(t); - // 判断map是否存在 - if (map != null) - // 存在则调用map.set设置此实体entry - map.set(this, value); - else - // 调用createMap进行ThreadLocalMap对象的初始化中 - createMap(t, value); - // 返回设置的值value - return value; - } - ``` - -* remove() - - ```java - // 删除当前线程中保存的ThreadLocal对应的实体entry - public void remove() { - // 获取当前线程对象中维护的ThreadLocalMap对象 - ThreadLocalMap m = getMap(Thread.currentThread()); - // 如果此map存在 - if (m != null) - // 存在则调用map.remove,this时当前ThreadLocal,以this为key删除对应的实体 - m.remove(this); - } - ``` - -* initialValue() - - 作用:返回该线程局部变量的初始值。 - - * 延迟调用的方法,在set方法还未调用而先调用了get方法时才执行,并且仅执行1次 - - * 该方法缺省(默认)实现直接返回一个``null`` - - * 如果想要一个初始值,可以重写此方法, 该方法是一个``protected``的方法,为了让子类覆盖而设计的 - - ```java - protected T initialValue() { - return null; - } - ``` - - - -*** - - - -#### LocalMap - -##### 成员属性 - -ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部Entry也是独立实现 - -```java -// 初始容量 —— 2的整次幂 -private static final int INITIAL_CAPACITY = 16; - -// 存放数据的table,Entry类的定义在下面分析,同样,数组长度必须是2的整次幂。 -private Entry[] table; - -//数组里面entrys的个数,可以用于判断table当前使用量是否超过阈值 -private int size = 0; - -// 进行扩容的阈值,表使用量大于它的时候进行扩容。 -private int threshold; // Default to 0 -``` - -存储结构 Entry: - -* Entry继承WeakReference,key是弱引用,目的是将ThreadLocal对象的生命周期和线程生命周期解绑 -* Entry限制只能用ThreadLocal作为key,key为null (entry.get() == null) 意味着key不再被引用,entry也可以从table中清除 - -```java -static class Entry extends WeakReference> { - Object value; - Entry(ThreadLocal k, Object v) { - super(k); - value = v; - } -} -``` - - - -*** - - - -##### 成员方法 - -* 构造方法 - - ```java - ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { - // 初始化table,创建一个长度为16的Entry数组 - table = new Entry[INITIAL_CAPACITY]; - // 计算索引 - int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); - // 设置值 - table[i] = new Entry(firstKey, firstValue); - size = 1; - // 设置阈值 - setThreshold(INITIAL_CAPACITY); - } - ``` - -* hashcode - - ```java - private final int threadLocalHashCode = nextHashCode(); - // 通过线程安全的方式操作加减,适合多线程情况下的使用 - private static AtomicInteger nextHashCode = new AtomicInteger(); - //特殊的hash值 - private static final int HASH_INCREMENT = 0x61c88647; - - private static int nextHashCode() { - return nextHashCode.getAndAdd(HASH_INCREMENT); - } - ``` - - ThreadLocal 的散列方式称之为 **斐波那契散列**。这里定义了一个AtomicInteger,每次获取当前值并加上HASH_INCREMENT,这个值跟斐波那契数列(黄金分割数)有关,这样做可以尽量避免hash冲突,让哈希码能均匀的分布在2的n次方的数组里 - -* set() - - ```java - private void set(ThreadLocal key, Object value) { - ThreadLocal.ThreadLocalMap.Entry[] tab = table; - int len = tab.length; - // 计算索引 - int i = key.threadLocalHashCode & (len-1); - // 使用线性探测法查找元素 - for (ThreadLocal.ThreadLocalMap.Entry e = tab[i]; - e != null; - e = tab[i = nextIndex(i, len)]) { - ThreadLocal k = e.get(); - // ThreadLocal 对应的 key 存在,直接覆盖之前的值 - if (k == key) { - e.value = value; - return; - } - // key为 null,但是值不为 null,说明之前的 ThreadLocal 对象已经被回收了, - // 当前数组中的 Entry 是一个陈旧(stale)的元素 - if (k == null) { - //用新元素替换陈旧的元素,这个方法进行了不少的垃圾清理动作,防止内存泄漏 - replaceStaleEntry(key, value, i); - return; - } - } - - //ThreadLocal对应的key不存在并且没有找到陈旧的元素,则在空元素的位置创建一个新的Entry。 - tab[i] = new Entry(key, value); - int sz = ++size; - - // 清除e.get()==null的元素, - // 如果没有清除任何entry并且当前使用量达到了负载因子所定义,那么进行 rehash - if (!cleanSomeSlots(i, sz) && sz >= threshold) - rehash(); - } - - // 获取环形数组的下一个索引 - private static int nextIndex(int i, int len) { - return ((i + 1 < len) ? i + 1 : 0); - } - - ``` - - ThreadLocalMap 使用**线性探测法来解决哈希冲突**: - - * 该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出 - * 在探测过程中 ThreadLocal 会释放 key 为 NULL,value 不为 NULL 的脏 Entry对象,防止内存泄漏 - * 假设当前table长度为16,计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,如果还是冲突会回到0,取table[0],以此类推,直到可以插入,可以把Entry[] table看成一个**环形数组** - - 线性探测法会出现**堆积问题**,一般采取平方探测法解决 - -* 扩容: - - rehash 会触发一次全量清理,如果数组长度大于等于长度的(2/3 * 3/4 = 1/2),则进行 resize - - ```java - // rehash 条件 - private void setThreshold(int len) { - threshold = len * 2 / 3; - } - // 扩容条件 - private void rehash() { - expungeStaleEntries(); - if (size >= threshold - threshold / 4) - resize(); - } - ``` - - Entry 数组为扩容为 原来的2倍 ,重新计算 key 的散列值,如果遇到 key 为 NULL 的情况,会将其 value 也置为 NULL,帮助虚拟机进行GC - - ```java - // 具体的扩容函数 - private void resize() { - } - ``` - - - -*** - - - -#### 内存泄漏 - -Memory leak:内存泄漏是指程序中动态分配的堆内存由于某种原因未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果,内存泄漏的堆积终将导致内存溢出 - -* 如果 key 使用强引用: - - 使用完 ThreadLocal ,threadLocal Ref 被回收,但是 threadLocalMap 的 Entry 强引用了 threadLocal,造成 threadLocal 无法被回收,无法完全避免内存泄漏 - - - -* 如果 key 使用弱引用: - - 使用完 ThreadLocal ,threadLocal Ref 被回收,ThreadLocalMap 只持有 ThreadLocal 的弱引用,所以threadlocal 也可以被回收,此时 Entry 中的 key=null。但没有手动删除这个 Entry 或者 CurrentThread 依然运行,依然存在强引用链,value 不会被回收,而这块 value 永远不会被访问到,导致 value 内存泄漏 - - - -* 两个主要原因: - - * 没有手动删除这个 Entry - * CurrentThread 依然运行 - -根本原因:ThreadLocalMap 是 Thread的一个属性,生命周期跟 Thread 一样长,如果没有手动删除对应 Entry 就会导致内存泄漏 - -解决方法:使用完 ThreadLocal 中存储的内容后将它 **remove** 掉就可以 - -ThreadLocal 内部解决方法:在 ThreadLocalMap 中的 set/getEntry 方法中,会对 key 进行判断,如果为 null(ThreadLocal 为 null)的话,会对 Entry 进行垃圾回收。所以**使用弱引用比强引用多一层保障**,就算不调用 remove,也有机会进行 GC - - - -*** - - - -#### 变量传递 - -##### 基本使用 - -父子线程:**创建子线程的线程是父线程**,比如实例中的 main 线程就是父线程 - -ThreadLocal 中存储的是线程的局部变量,如果想实现线程间局部变量传递可以使用 InheritableThreadLocal 类 - -```java -public static void main(String[] args) { - ThreadLocal threadLocal = new InheritableThreadLocal<>(); - threadLocal.set("父线程设置的值"); - - new Thread(() -> System.out.println("子线程输出:" + threadLocal.get())).start(); -} -// 子线程输出:父线程设置的值 -``` - - - -*** - - - -##### 实现原理 - -InheritableThreadLocal 源码: - -```java -public class InheritableThreadLocal extends ThreadLocal { - protected T childValue(T parentValue) { - return parentValue; - } - ThreadLocalMap getMap(Thread t) { - return t.inheritableThreadLocals; - } - void createMap(Thread t, T firstValue) { - t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); - } -} -``` - -实现父子线程间的局部变量共享需要追溯到 Thread 对象的构造方法: - -```java -private void init(ThreadGroup g, Runnable target, String name, - long stackSize, AccessControlContext acc, - // 该参数默认是 true - boolean inheritThreadLocals) { - // ... - Thread parent = currentThread(); - - //判断父线程(创建子线程的线程)的 inheritableThreadLocals 属性不为 NULL - if (inheritThreadLocals && parent.inheritableThreadLocals != null) { - //复制父线程的 inheritableThreadLocals 属性,实现父子线程局部变量共享 - this.inheritableThreadLocals = - ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); - } - // .. -} -static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) { - return new ThreadLocalMap(parentMap); -} -``` - -```java -private ThreadLocalMap(ThreadLocalMap parentMap) { - Entry[] parentTable = parentMap.table; - int len = parentTable.length; - setThreshold(len); - table = new Entry[len]; - // 逐个复制父线程 ThreadLocalMap 中的数据 - for (int j = 0; j < len; j++) { - Entry e = parentTable[j]; - if (e != null) { - @SuppressWarnings("unchecked") - ThreadLocal key = (ThreadLocal) e.get(); - if (key != null) { - // 调用的是 InheritableThreadLocal#childValue(T parentValue) - Object value = key.childValue(e.value); - Entry c = new Entry(key, value); - int h = key.threadLocalHashCode & (len - 1); - while (table[h] != null) - h = nextIndex(h, len); - table[h] = c; - size++; - } - } - } -} -``` - - - -参考文章:https://blog.csdn.net/feichitianxia/article/details/110495764 - @@ -8849,6 +8277,7 @@ BUG 流程: * T1 获取信号量成功,调用 setHeadAndPropagate 时,读到 h.waitStatus < 0,从而调用 doReleaseShared() 唤醒 T2 ```java +// 新版本代码 private void setHeadAndPropagate(Node node, int propagate) { Node h = head; // 设置自己为 head 节点 From 6565c593aa3fd42d07c8a51ee3a79977f4c7f147 Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 16 Aug 2021 11:14:27 +0800 Subject: [PATCH 025/168] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 58cd281..43ee4e1 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ * DB:MySQL、Redis * Issue:Interview Questions * Java:JavaSE、JVM、Algorithm、Design Pattern -* Prog:JUC、NET +* Prog:Concurrent、Network Programming * SSM:MyBatis、Spring、SpringMVC、SpringBoot * Tool:Git、Linux、Docker * Web:HTML、CSS、HTTP、Servlet、JavaScript From e14c70a35c746e43062aad228270c64178b87eb4 Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 16 Aug 2021 17:14:28 +0800 Subject: [PATCH 026/168] Update Java Notes --- DB.md | 2 +- Java.md | 151 +++++----- Prog.md | 867 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- SSM.md | 4 +- 4 files changed, 930 insertions(+), 94 deletions(-) diff --git a/DB.md b/DB.md index bd9170a..e9ac36c 100644 --- a/DB.md +++ b/DB.md @@ -6628,7 +6628,7 @@ Connection:数据库连接对象 - 获取普通执行者对象:`Statement createStatement()` - 获取预编译执行者对象:`PreparedStatement prepareStatement(String sql)` - 管理事务 - - 开启事务:`setAutoCommit(boolean autoCommit)`,false 开启事务,true 自动提交模式(默认) + - 开启事务:`setAutoCommit(boolean autoCommit)`,false 开启事务,true 自动提交模式(默认) - 提交事务:`void commit()` - 回滚事务:`void rollback()` - 释放资源 diff --git a/Java.md b/Java.md index ded431c..536f72d 100644 --- a/Java.md +++ b/Java.md @@ -2527,12 +2527,12 @@ s.replace("-","");//12378 ##### 基本介绍 -**字符串常量池(String Pool / StringTable / 串池)**保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定,常量池类似于Java系统级别提供的**缓存**,存放对象和引用 +字符串常量池(String Pool / StringTable / 串池)保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定,常量池类似于 Java 系统级别提供的**缓存**,存放对象和引用 -* StringTable,hashtable (哈希表 + 链表)结构,不能扩容,默认值大小长度是 1009 +* StringTable,类似 HashTable 结构,通过 `-XX:StringTableSize` 设置大小,JDK 1.8 中默认 60013 * 常量池中的字符串仅是符号,第一次使用时才变为对象,可以避免重复创建字符串对象 -* 字符串**变量**的拼接的原理是 StringBuilder(jdk1.8),append 效率要比字符串拼接高很多 -* 字符串**常量**拼接的原理是编译期优化,结果在常量池 +* 字符串**变量**的拼接的原理是 StringBuilder#append,append 方法比字符串拼接效率高(JDK 1.8) +* 字符串**常量**拼接的原理是编译期优化,拼接结果放入常量池 * 可以使用 String 的 intern() 方法在运行过程将字符串添加到 String Pool 中 @@ -2543,15 +2543,15 @@ s.replace("-","");//12378 ##### intern() -jdk1.8:当一个字符串调用 intern() 方法时,如果 String Pool 中: +JDK 1.8:当一个字符串调用 intern() 方法时,如果 String Pool 中: * 存在一个字符串和该字符串值相等,就会返回 String Pool 中字符串的引用(需要变量接收) * 不存在,会把对象的**引用地址**复制一份放入串池,并返回串池中的引用地址,前提是堆内存有该对象,因为 Pool 在堆中,为了节省内存不再创建新对象 -jdk1.6:将这个字符串对象尝试放入串池,如果有就不放入,返回已有的串池中的对象的地址;如果没有会把此对象复制一份,放入串池,把串池中的对象返回 +JDK 1.6:将这个字符串对象尝试放入串池,如果有就不放入,返回已有的串池中的对象的地址;如果没有会把此对象复制一份,放入串池,把串池中的对象返回 ```java public class Demo { -//常量池中的信息都加载到运行时常量池,这时a b ab是常量池中的符号,还不是java字符串对象,是懒惰的 + // 常量池中的信息都加载到运行时常量池,这时a b ab是常量池中的符号,还不是java字符串对象,是懒惰的 // ldc #2 会把 a 符号变为 "a" 字符串对象 ldc:反编译后的指令 // ldc #3 会把 b 符号变为 "b" 字符串对象 // ldc #4 会把 ab 符号变为 "ab" 字符串对象 @@ -3787,10 +3787,9 @@ public class RegexDemo { #### 概述 -> Java中集合的代表是:Collection. -> Collection集合是Java中集合的祖宗类。 +Java 中集合的代表是Collection,Collection 集合是 Java 中集合的祖宗类 -Collection集合底层为数组:`[value1, value2, ....]` +Collection 集合底层为数组:`[value1, value2, ....]` ```java Collection集合的体系: @@ -3805,11 +3804,11 @@ LinkedHashSet<>(实现类) **集合的特点:** -* Set系列集合:添加的元素是无序,不重复,无索引的 +* Set 系列集合:添加的元素是无序,不重复,无索引的 * HashSet:添加的元素是无序,不重复,无索引的 * LinkedHashSet:添加的元素是有序,不重复,无索引的 * TreeSet:不重复,无索引,按照大小默认升序排序 -* List系列集合:添加的元素是有序,可重复,有索引 +* List 系列集合:添加的元素是有序,可重复,有索引 * ArrayList:添加的元素是有序,可重复,有索引 * LinekdList:添加的元素是有序,可重复,有索引 @@ -3824,18 +3823,21 @@ LinkedHashSet<>(实现类) Collection 是集合的祖宗类,它的功能是全部集合都可以继承使用的,所以要学习它。 Collection 子类的构造器都有可以包装其他子类的构造方法,如: - `public ArrayList(Collection c)` : 构造新集合,元素按照由集合的迭代器返回的顺序 - `public HashSet(Collection c)` : 构造一个包含指定集合中的元素的新集合 -Collection API如下: - `public boolean add(E e)` : 把给定的对象添加到当前集合中 。 - `public void clear()` : 清空集合中所有的元素。 - `public boolean remove(E e)` : 把给定的对象在当前集合中删除。 - `public boolean contains(Object obj)` : 判断当前集合中是否包含给定的对象。 - `public boolean isEmpty()` : 判断当前集合是否为空。 - `public int size()` : 返回集合中元素的个数。 - `public Object[] toArray()` : 把集合中的元素,存储到数组中 - `public boolean addAll(Collection c)` : 将指定集合中的所有元素添加到此集合 +* `public ArrayList(Collection c)`:构造新集合,元素按照由集合的迭代器返回的顺序 + +* `public HashSet(Collection c)`:构造一个包含指定集合中的元素的新集合 + +Collection API 如下: + +* `public boolean add(E e)`:把给定的对象添加到当前集合中 。 +* `public void clear()`:清空集合中所有的元素。 +* `public boolean remove(E e)`:把给定的对象在当前集合中删除。 +* `public boolean contains(Object obj)`:判断当前集合中是否包含给定的对象。 +* `public boolean isEmpty()`:判断当前集合是否为空。 +* `public int size()`:返回集合中元素的个数。 +* `public Object[] toArray()`:把集合中的元素,存储到数组中 +* `public boolean addAll(Collection c)`:将指定集合中的所有元素添加到此集合 ```java public class CollectionDemo { @@ -3868,27 +3870,27 @@ public class CollectionDemo { #### 遍历 -Collection集合的遍历方式有三种: +Collection 集合的遍历方式有三种: -集合可以直接输出内容,因为底层重写了toString()方法。 +集合可以直接输出内容,因为底层重写了 toString() 方法 1. 迭代器 - `public Iterator iterator()` : 获取集合对应的迭代器,用来遍历集合中的元素的 - `E next()` : 获取下一个元素值! - `boolean hasNext()` : 判断是否有下一个元素,有返回true ,反之 - `default void remove()` : 从底层集合中删除此迭代器返回的最后一个元素,这种方法只能在每次调用next() 时调用一次 + `public Iterator iterator()`:获取集合对应的迭代器,用来遍历集合中的元素的 + `E next()`:获取下一个元素值 + `boolean hasNext()`:判断是否有下一个元素,有返回true ,反之 + `default void remove()`:从底层集合中删除此迭代器返回的最后一个元素,这种方法只能在每次调用next() 时调用一次 -2. 增强for循环 - 增强for循环是一种遍历形式,可以遍历集合或者数组,遍历集合实际上是迭代器遍历的简化写法 -```java +2. 增强 for 循环:可以遍历集合或者数组,遍历集合实际上是迭代器遍历的简化写法 + + ```java for(被遍历集合或者数组中元素的类型 变量名称 : 被遍历集合或者数组){ } -``` + ``` -缺点:遍历无法知道遍历到了哪个元素了,因为没有索引。 + 缺点:遍历无法知道遍历到了哪个元素了,因为没有索引 -3. JDK 1.8开始之后的新技术Lambda表达式 +3. JDK 1.8 开始之后的新技术 Lambda 表达式 ```java public class CollectionDemo { @@ -4531,15 +4533,15 @@ Map集合的体系: Map集合的特点: -1. Map集合的特点都是由键决定的。 -2. Map集合的键是无序,不重复的,无索引的。(Set) -3. Map集合的值无要求。(List) -4. Map集合的键值对都可以为null。 -5. Map集合后面重复的键对应元素会覆盖前面的元素 +1. Map 集合的特点都是由键决定的 +2. Map 集合的键是无序,不重复的,无索引的(Set) +3. Map 集合的值无要求(List) +4. Map 集合的键值对都可以为 null +5. Map 集合后面重复的键对应元素会覆盖前面的元素 -HashMap:元素按照键是无序,不重复,无索引,值不做要求。 +HashMap:元素按照键是无序,不重复,无索引,值不做要求 -LinkedHashMap:元素按照键是有序,不重复,无索引,值不做要求。 +LinkedHashMap:元素按照键是有序,不重复,无索引,值不做要求 ```java //经典代码 @@ -4556,7 +4558,7 @@ System.out.println(maps); #### 常用API -Map集合的常用API +Map 集合的常用 API * `public V put(K key, V value)`:把指定的键与值添加到 Map 集合中,**重复的键会覆盖前面的值元素** * `public V remove(Object key)`:把指定的键对应的键值对元素在集合中删除,返回被删除元素的值 @@ -4641,7 +4643,7 @@ HashMap 基于哈希表的 Map 接口实现,是以 key-value 存储形式存 特点: -* HashMap的实现不是同步的,这意味着它不是线程安全的 +* HashMap 的实现不是同步的,这意味着它不是线程安全的 * key 是唯一不重复的,底层的哈希表结构,依赖 hashCode 方法和 equals 方法保证键的唯一 * key、value 都可以为null,但是 key 位置只能是一个null * HashMap 中的映射不是有序的,即存取是无序的 @@ -4662,12 +4664,12 @@ JDK7 对比 JDK8: * 数组是 HashMap 的主体 * 链表则是为了解决哈希冲突而存在的(**拉链法解决冲突**),拉链法就是头插法 - 两个对象调用的hashCode方法计算的哈希码值(键的哈希)一致导致计算的数组索引值相同 + 两个对象调用的 hashCode 方法计算的哈希码值(键的哈希)一致导致计算的数组索引值相同 * JDK1.8 以后 HashMap 由 **数组+链表 +红黑树**数据结构组成 * 解决哈希冲突时有了较大的变化 - * **当链表长度超过(大于)阈值(或者红黑树的边界值,默认为 8)并且当前数组的长度大于等于64时,此索引位置上的所有数据改为红黑树存储** + * 当链表长度**超过(大于)阈值**(或者红黑树的边界值,默认为 8)并且当前数组的**长度大于等于 64 时**,此索引位置上的所有数据改为红黑树存储 * 即使哈希函数取得再好,也很难达到元素百分百均匀分布。当 HashMap 中有大量的元素都存放到同一个桶中时,就相当于一个长的单链表,假如单链表有 n 个元素,遍历的**时间复杂度是 O(n)**,所以 JDK1.8 中引入了 红黑树(查找**时间复杂度为 O(logn)**)来优化这个问题,使得查找效率更高 ![](https://gitee.com/seazean/images/raw/master/Java/HashMap底层结构.png) @@ -9769,6 +9771,10 @@ JNI:Java Native Interface,通过使用 Java 本地接口书写程序,可 * 可以直接使用本地处理器中的寄存器 + + + +图片来源:https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20%E8%99%9A%E6%8B%9F%E6%9C%BA.md @@ -9872,7 +9878,7 @@ public static void main(String[] args) { 方法区的 GC:针对常量池的回收及对类型的卸载,比较难实现 -为了**避免方法区出现OOM**,在 JDK8 中将堆内的方法区(永久代)移动到了本地内存上,重新开辟了一块空间,叫做元空间,元空间存储类的元信息,**静态变量和字符串常量池等放入堆中** +为了**避免方法区出现 OOM**,在 JDK8 中将堆内的方法区(永久代)移动到了本地内存上,重新开辟了一块空间,叫做元空间,元空间存储类的元信息,**静态变量和字符串常量池等放入堆中** 类元信息:在类编译期间放入方法区,存放了类的基本信息,包括类的方法、参数、接口以及常量池表 @@ -10040,10 +10046,8 @@ public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制 为对象分配内存:首先计算对象占用空间大小,接着在堆中划分一块内存给新对象 -* 如果内存规整,使用指针碰撞(BumpThePointer) - 所有用过的内存在一边,空闲的内存在另外一边,中间有一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离 -* 如果内存不规整,虚拟机需要维护一个空闲列表(Free List)分配 - 已使用的内存和未使用的内存相互交错,虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容 +* 如果内存规整,使用指针碰撞(BumpThePointer)。所有用过的内存在一边,空闲的内存在另外一边,中间有一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离 +* 如果内存不规整,虚拟机需要维护一个空闲列表(Free List)分配。已使用的内存和未使用的内存相互交错,虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容 @@ -10371,7 +10375,7 @@ public void localvarGC4() { 可达性分析算法:也可以称为根搜索算法、追踪性垃圾收集 -**GC Roots对象**: +GC Roots 对象: - 虚拟机栈中局部变量表中引用的对象:各个线程被调用的方法中使用到的参数、局部变量等 - 本地方法栈中引用的对象 @@ -10382,7 +10386,7 @@ public void localvarGC4() { -**GC Roots是一组活跃的引用,不是对象**,放在 GC Roots Set 集合 +**GC Roots 是一组活跃的引用,不是对象**,放在 GC Roots Set 集合 @@ -10531,10 +10535,10 @@ Java语言提供了对象终止(finalization)机制来允许开发人员提 无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关,Java 提供了四种强度不同的引用类型 -1. 强引用:被强引用关联的对象不会被回收,只有所有GCRoots都不通过强引用引用该对象,才能被垃圾回收 +1. 强引用:被强引用关联的对象不会被回收,只有所有 GCRoots 都不通过强引用引用该对象,才能被垃圾回收 * 强引用可以直接访问目标对象 - * 虚拟机宁愿抛出OOM异常,也不会回收强引用所指向对象 + * 虚拟机宁愿抛出 OOM 异常,也不会回收强引用所指向对象 * 强引用可能导致**内存泄漏** ```java @@ -10557,7 +10561,7 @@ Java语言提供了对象终止(finalization)机制来允许开发人员提 * 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象 * 配合引用队列来释放弱引用自身 - * WeakHashMap用来存储图片信息,可以在内存不足的时候及时回收,避免了OOM + * WeakHashMap 用来存储图片信息,可以在内存不足的时候及时回收,避免了 OOM ```java Object obj = new Object(); @@ -10565,7 +10569,7 @@ Java语言提供了对象终止(finalization)机制来允许开发人员提 obj = null; ``` -4. 虚引用(PhantomReference):也称为“幽灵引用”或者“幻影引用”,是所有引用类型中最弱的一个 +4. 虚引用(PhantomReference):也称为幽灵引用或者幻影引”,是所有引用类型中最弱的一个 * 一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象 * 为对象设置虚引用的唯一目的是在于跟踪垃圾回收过程,能在这个对象被回收时收到一个系统通知 @@ -10579,15 +10583,6 @@ Java语言提供了对象终止(finalization)机制来允许开发人员提 5. 终结器引用(finalization) - 引用的四种状态: - -* Active:激活,创建 ref 对象时就是激活状态 -* Pending:等待入队,所对应的强引用被GC,就要进入引用队列 -* Enqueued:入队了 - * 如果指定了 refQueue,pending 移动到 enqueued 状态,refQueue.poll 时进入失效状态 - * 如果没有指定 refQueue,直接到失效状态 -* Inactive:失效 - *** @@ -10598,11 +10593,11 @@ Java语言提供了对象终止(finalization)机制来允许开发人员提 方法区主要回收的是无用的类 -判定一个类是否是无用的类,需要同时满足下面 3 个条件 : +判定一个类是否是无用的类,需要同时满足下面 3 个条件: - 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例 -- 加载该类的`ClassLoader`已经被回收 -- 该类对应的`java.lang.Class`对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法 +- 加载该类的 `ClassLoader` 已经被回收 +- 该类对应的 `java.lang.Class` 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法 虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收 @@ -11167,7 +11162,7 @@ Serial GC、Parallel GC、Concurrent Mark Sweep GC 这三个 GC 不同: 内存溢出(out of memory)指的是申请内存时,没有足够的内存可以使用 -内存泄漏和内存溢出的关系:内存泄漏的增多,最终会导致内存溢出 +内存泄漏和内存溢出的关系:内存泄漏的越来越多,最终会导致内存溢出 @@ -11184,8 +11179,8 @@ Serial GC、Parallel GC、Concurrent Mark Sweep GC 这三个 GC 不同: ```java public class MemoryLeak { static List list = new ArrayList(); - public void oomTests(){ - Object obj=new Object();//局部变量 + public void oomTest(){ + Object obj = new Object();//局部变量 list.add(obj); } } @@ -11219,7 +11214,7 @@ public class MemoryLeak { ##### 连接相关 -数据库连接、网络连接和IO连接等,当不再使用时,需要显式调用 close 方法来释放与连接,垃圾回收器才会回收对应的对象,否则将会造成大量的对象无法被回收,从而引起内存泄漏 +数据库连接、网络连接和 IO 连接等,当不再使用时,需要显式调用 close 方法来释放与连接,垃圾回收器才会回收对应的对象,否则将会造成大量的对象无法被回收,从而引起内存泄漏 @@ -11235,8 +11230,8 @@ public class MemoryLeak { public class UsingRandom { private String msg; public void receiveMsg(){ - msg = readFromNet();//从网络中接受数据保存到msg中 - saveDB(msg);//把msg保存到数据库中 + msg = readFromNet();// 从网络中接受数据保存到 msg 中 + saveDB(msg); // 把 msg 保存到数据库中 } } ``` @@ -15336,14 +15331,14 @@ public class Hanoi { hanoi('X', 'Y', 'Z', 3); } - //将n个块分治的从x移动到z,y为辅助柱 + // 将n个块分治的从x移动到z,y为辅助柱 private static void hanoi(char x, char y, char z, int n) { if (n == 1) { System.out.println(x + "→" + z); //直接将x的块移动到z } else { - hanoi(x, z, y, n - 1); //分治处理n-1个块,先将n-1个块借助z移到y + hanoi(x, z, y, n - 1); //分治处理n-1个块,先将n-1个块借助z移到y System.out.println(x + "→" + z); //然后将x最下面的块(最大的)移动到z - hanoi(y, x, z, n - 1); //最后将n-1个块从y移动到z,x为辅助柱 + hanoi(y, x, z, n - 1); //最后将n-1个块从y移动到z,x为辅助柱 } } } diff --git a/Prog.md b/Prog.md index 28ef918..20ab52b 100644 --- a/Prog.md +++ b/Prog.md @@ -169,7 +169,7 @@ public class Thread implements Runnable { } ``` - +Runnable 方式的优缺点: * 缺点:代码复杂一点。 @@ -2403,7 +2403,7 @@ CPU 处理器速度远远大于在主内存中的,为了解决速度差异, #### 伪共享 -**缓存以缓存行 cache line 为单位,每个缓存行对应着一块内存**,一般是 64 byte(8 个 long),在 CPU 从主存获取数据时,以 cache line 为单位加载,于是相邻的数据会一并加载到缓存中 +**缓存以缓存行 cache line 为单位**,每个缓存行对应着一块内存,一般是 64 byte(8 个 long),在 CPU 从主存获取数据时,以 cache line 为单位加载,于是相邻的数据会一并加载到缓存中 缓存会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中,CPU 要保证数据的一致性,需要做到某个 CPU 核心更改了数据,其它 CPU 核心对应的**整个缓存行必须失效**,这就是伪共享 @@ -2430,17 +2430,17 @@ Linux 查看 CPU 缓存行: 缓存一致性:当多个处理器运算任务都涉及到同一块主内存区域的时候,将可能导致各自的缓存数据不一样 -**MESI**(Modified Exclusive Shared Or Invalid)是一种广泛使用的支持写回策略的缓存一致性协议,CPU 中每个缓存行(caceh line)使用4种状态进行标记(使用额外的两位 (bit) 表示): +**MESI**(Modified Exclusive Shared Or Invalid)是一种广泛使用的支持写回策略的缓存一致性协议,CPU 中每个缓存行(caceh line)使用 4 种状态进行标记(使用额外的两位 (bit) 表示): * M:被修改(Modified) - 该缓存行只被缓存在该 CPU 的缓存中,并且是被修改过的,与主存中的数据不一致 (dirty),该缓存行中的内存需要在未来的某个时间点(其它 CPU 读取主存中相应数据之前)写回( write back )主存 + 该缓存行只被缓存在该 CPU 的缓存中,并且是被修改过的,与主存中的数据不一致 (dirty),该缓存行中的内存需要在未来的某个时间点(其它 CPU 读取主存中相应数据之前)写回 (write back) 主存 当被写回主存之后,该缓存行的状态会变成独享 (exclusive) 状态。 * E:独享的(Exclusive) - 该缓存行只被缓存在该 CPU 的缓存中,它是未被修改过的 (clear),与主存中数据一致,该状态可以在任何时刻有其它 CPU 读取该内存时变成共享状态 (shared) + 该缓存行只被缓存在该 CPU 的缓存中,是未被修改过的 (clear),与主存中数据一致,该状态可以在任何时刻有其它 CPU 读取该内存时变成共享状态 (shared) 当 CPU 修改该缓存行中内容时,该状态可以变成 Modified 状态 @@ -2477,7 +2477,7 @@ Linux 查看 CPU 缓存行: 总线机制: -* 总线嗅探:每个处理器通过嗅探在总线上传播的数据来检查自己缓存值是否过期了,当处理器发现自己的缓存对应的内存地址数据被修改,就**将当前处理器的缓存行设置为无效状态**,当处理器对这个数据进行操作时,会重新从内存中把数据读取到处理器缓存中 +* 总线嗅探:每个处理器通过嗅探在总线上传播的数据来检查自己缓存值是否过期了,当处理器发现自己的缓存对应的内存地址的数据被修改,就**将当前处理器的缓存行设置为无效状态**,当处理器对这个数据进行操作时,会重新从内存中把数据读取到处理器缓存中 * 总线风暴:由于 volatile 的 MESI 缓存一致性协议,需要不断的从主内存嗅探和 CAS 循环,无效的交互会导致总线带宽达到峰值;因此不要大量使用 volatile 关键字,使用 volatile、syschonized 都需要根据实际场景 @@ -3465,13 +3465,13 @@ ABA 问题:当进行获取主内存值时,该内存值在写入主内存时 其他线程先把 A 改成 B 又改回 A,主线程仅能判断出共享变量的值与最初值 A 是否相同,不能感知到这种从 A 改为 B 又 改回 A 的情况,这时 CAS 虽然成功,但是过程存在问题 * 构造方法: - * `public AtomicStampedReference(V initialRef, int initialStamp)`:初始值和初始版本号 + `public AtomicStampedReference(V initialRef, int initialStamp)`:初始值和初始版本号 * 常用API: - * ` public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)`:期望引用和期望版本号都一致才进行 CAS 修改数据 - * `public void set(V newReference, int newStamp)`:设置值和版本号 - * `public V getReference()`:返回引用的值 - * `public int getStamp()`:返回当前版本号 + ` public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)`:期望引用和期望版本号都一致才进行 CAS 修改数据 + `public void set(V newReference, int newStamp)`:设置值和版本号 + `public V getReference()`:返回引用的值 + `public int getStamp()`:返回当前版本号 ```java public static void main(String[] args) { @@ -3643,6 +3643,848 @@ Servlet 为了保证其线程安全,一般不为 Servlet 设置成员变量, ### Local +#### 基本介绍 + +ThreadLocal 类用来提供线程内部的局部变量,这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量,分配在 TLAB + +ThreadLocal 实例通常来说都是 `private static` 类型的,属于一个线程的本地变量,用于关联线程和线程上下文。每个线程都会在 ThreadLocal 中保存一份该线程独有的数据,所以是线程安全的 + +ThreadLocal 作用: + +* 线程并发:应用在多线程并发的场景下 + +* 传递数据:通过 ThreadLocal 实现在同一线程不同函数或组件中传递公共变量,减少传递复杂度 + +* 线程隔离:每个线程的变量都是独立的,不会互相影响 + +对比 synchronized: + +| | synchronized | ThreadLocal | +| ------ | ------------------------------------------------------------ | ------------------------------------------------------------ | +| 原理 | 同步机制采用**以时间换空间**的方式,只提供了一份变量,让不同的线程排队访问 | ThreadLocal采用**以空间换时间**的方式,为每个线程都提供了一份变量的副本,从而实现同时访问而相不干扰 | +| 侧重点 | 多个线程之间访问资源的同步 | 多线程中让每个线程之间的数据相互隔离 | + + + +*** + + + +#### 基本使用 + +##### 常用方法 + +| 方法 | 描述 | +| -------------------------- | ---------------------------- | +| ThreadLocal<>() | 创建 ThreadLocal 对象 | +| protected T initialValue() | 返回当前线程局部变量的初始值 | +| public void set( T value) | 设置当前线程绑定的局部变量 | +| public T get() | 获取当前线程绑定的局部变量 | +| public void remove() | 移除当前线程绑定的局部变量 | + +```java +public class MyDemo { + + private static ThreadLocal tl = new ThreadLocal<>(); + + private String content; + + private String getContent() { + // 获取当前线程绑定的变量 + return tl.get(); + } + + private void setContent(String content) { + // 变量content绑定到当前线程 + tl.set(content); + } + + public static void main(String[] args) { + MyDemo demo = new MyDemo(); + for (int i = 0; i < 5; i++) { + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + // 设置数据 + demo.setContent(Thread.currentThread().getName() + "的数据"); + System.out.println("-----------------------"); + System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent()); + } + }); + thread.setName("线程" + i); + thread.start(); + } + } +} +``` + + + +*** + + + +##### 应用场景 + +ThreadLocal 适用于如下两种场景 + +- 每个线程需要有自己单独的实例 +- 实例需要在多个方法中共享,但不希望被多线程共享 + +**事务管理**,ThreadLocal 方案有两个突出的优势: + +1. 传递数据:保存每个线程绑定的数据,在需要的地方可以直接获取,避免参数直接传递带来的代码耦合问题 + +2. 线程隔离:各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失 + +```java +public class JdbcUtils { + // ThreadLocal对象,将connection绑定在当前线程中 + private static final ThreadLocal tl = new ThreadLocal(); + // c3p0 数据库连接池对象属性 + private static final ComboPooledDataSource ds = new ComboPooledDataSource(); + // 获取连接 + public static Connection getConnection() throws SQLException { + //取出当前线程绑定的connection对象 + Connection conn = tl.get(); + if (conn == null) { + //如果没有,则从连接池中取出 + conn = ds.getConnection(); + //再将connection对象绑定到当前线程中,非常重要的操作 + tl.set(conn); + } + return conn; + } + // ... +} +``` + +用 ThreadLocal 使 SimpleDateFormat 从独享变量变成单个线程变量: + +```java +public class ThreadLocalDateUtil { + private static ThreadLocal threadLocal = new ThreadLocal() { + @Override + protected DateFormat initialValue() { + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + } + }; + + public static Date parse(String dateStr) throws ParseException { + return threadLocal.get().parse(dateStr); + } + + public static String format(Date date) { + return threadLocal.get().format(date); + } +} +``` + + + + + +**** + + + +#### 实现原理 + +##### 底层结构 + +JDK8 以前:每个 ThreadLocal 都创建一个 Map,然后用线程作为 Map 的 key,要存储的局部变量作为 Map 的 value,达到各个线程的局部变量隔离的效果。这种结构会造成 Map 结构过大和内存泄露,因为 Thread 停止后无法通过 key 删除对应的数据 + +![](https://gitee.com/seazean/images/raw/master/Java/JUC-ThreadLocal数据结构JDK8前.png) + +JDK8 以后:每个 Thread 维护一个 ThreadLocalMap,这个 Map 的 key 是 ThreadLocal 实例本身,value 是真正要存储的值 + +* 每个 Thread 线程内部都有一个 Map (ThreadLocalMap) +* Map 里面存储 ThreadLocal 对象(key)和线程的变量副本(value) +* Thread 内部的 Map 是由 ThreadLocal 维护的,由 ThreadLocal 负责向 map 获取和设置线程的变量值。 +* 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成副本的隔离,互不干扰 + +![](https://gitee.com/seazean/images/raw/master/Java/JUC-ThreadLocal数据结构JDK8后.png) + +JDK8 前后对比: + +* 每个 Map 存储的 Entry 数量会变少,因为之前的存储数量由 Thread 的数量决定,现在由 ThreadLocal 的数量决定,在实际编程当中,往往 ThreadLocal 的数量要少于 Thread 的数量 +* 当 Thread 销毁之后,对应的 ThreadLocalMap 也会随之销毁,能减少内存的使用,**防止内存泄露** + + + +*** + + + +##### 成员变量 + +* Thread 类的相关属性:每一个线程持有一个 ThreadLocalMap 对象,存放由 ThreadLocal 和数据组成的 Entry 键值对 + + ```java + ThreadLocal.ThreadLocalMap threadLocals = null + ``` + +* 计算 ThreadLocal 对象的哈希值: + + ```java + private final int threadLocalHashCode = nextHashCode() + ``` + + 使用 `threadLocalHashCode & (table.length - 1)` 计算当前 entry 需要存放的位置 + +* 每创建一个 ThreadLocal 对象就会使用 nextHashCode 分配一个 hash 值给这个对象: + + ```java + private static AtomicInteger nextHashCode = new AtomicInteger() + ``` + +* **斐波那契数**也叫黄金分割数,hash 的增量就是这个数字,带来的好处是 hash 分布非常均匀: + + ```java + private static final int HASH_INCREMENT = 0x61c88647 + ``` + + + +*** + + + +##### 成员方法 + +方法都是线程安全的,因为 ThreadLocal 只属于一个线程 + +* initialValue():返回该线程局部变量的初始值 + + * 延迟调用的方法,在 set 方法还未调用而先调用了 get 方法时才执行,并且仅执行 1 次 + * 该方法缺省(默认)实现直接返回一个 null + * 如果想要一个初始值,可以重写此方法, 该方法是一个 `protected` 的方法,为了让子类覆盖而设计的 + + ```java + protected T initialValue() { + return null; + } + ``` + +* nextHashCode():计算哈希值,ThreadLocal 的散列方式称之为 **斐波那契散列**,每次获取哈希值都会加上 HASH_INCREMENT,这样做可以尽量避免 hash 冲突,让哈希值能均匀的分布在 2 的 n 次方的数组中 + + ```java + private static int nextHashCode() { + // 哈希值自增一个 HASH_INCREMENT 数值 + return nextHashCode.getAndAdd(HASH_INCREMENT); + } + ``` + +* set():修改当前线程与当前 threadLocal 对象相关联的线程局部变量 + + ```java + public void set(T value) { + // 获取当前线程对象 + Thread t = Thread.currentThread(); + // 获取此线程对象中维护的ThreadLocalMap对象 + ThreadLocalMap map = getMap(t); + // 判断 map 是否存在 + if (map != null) + // 调用 threadLocalMap.set 方法进行重写或者添加 + map.set(this, value); + else + // map 为空,调用 createMap 进行 ThreadLocalMap 对象的初始化。参数1是当前线程,参数2是局部变量 + createMap(t, value); + } + ``` + + ```java + // 获取当前线程 Thread 对应维护的ThreadLocalMap + ThreadLocalMap getMap(Thread t) { + return t.threadLocals; + } + // 创建当前线程Thread对应维护的ThreadLocalMap + void createMap(Thread t, T firstValue) { + //这里的 this 是调用此方法的 threadLocal,创建一个新的 Map 并设置第一个数据 + t.threadLocals = new ThreadLocalMap(this, firstValue); + } + ``` + +* get():获取当前线程与当前 ThreadLocal 对象相关联的线程局部变量 + + ```java + public T get() { + Thread t = Thread.currentThread(); + ThreadLocalMap map = getMap(t); + // 如果此map存在 + if (map != null) { + // 以当前的 ThreadLocal 为 key,调用 getEntry 获取对应的存储实体 e + ThreadLocalMap.Entry e = map.getEntry(this); + // 对 e 进行判空 + if (e != null) { + // 获取存储实体 e 对应的 value值 + T result = (T)e.value; + return result; + } + } + /*有两种情况有执行当前代码 + 第一种情况: map 不存在,表示此线程没有维护的 ThreadLocalMap 对象 + 第二种情况: map 存在, 但是没有与当前 ThreadLocal 关联的 entry*/ + // 初始化当前线程与当前 threadLocal 对象相关联的 value + return setInitialValue(); + } + ``` + + ```java + private T setInitialValue() { + // 调用initialValue获取初始化的值,此方法可以被子类重写, 如果不重写默认返回 null + T value = initialValue(); + Thread t = Thread.currentThread(); + ThreadLocalMap map = getMap(t); + // 判断 map 是否初始化过 + if (map != null) + // 存在则调用 map.set 设置此实体 entry + map.set(this, value); + else + // 调用 createMap 进行 ThreadLocalMap 对象的初始化中 + createMap(t, value); + // 返回线程与当前 threadLocal 关联的局部变量 + return value; + } + ``` + +* remove():移除当前线程与当前 threadLocal 对象相关联的线程局部变量 + + ```java + public void remove() { + // 获取当前线程对象中维护的 ThreadLocalMap 对象 + ThreadLocalMap m = getMap(Thread.currentThread()); + if (m != null) + // map 存在则调用 map.remove,this时当前ThreadLocal,以this为key删除对应的实体 + m.remove(this); + } + ``` + + + + + +*** + + + +#### LocalMap + +##### 成员属性 + +ThreadLocalMap 是 ThreadLocal 的内部类,没有实现 Map 接口,用独立的方式实现了 Map 的功能,其内部 Entry 也是独立实现 + +```java +// 初始化当前 map 内部散列表数组的初始长度 16 +private static final int INITIAL_CAPACITY = 16; + +// 存放数据的table,数组长度必须是2的整次幂。 +private Entry[] table; + +// 数组里面 entrys 的个数,可以用于判断 table 当前使用量是否超过阈值 +private int size = 0; + +// 进行扩容的阈值,表使用量大于它的时候进行扩容。 +private int threshold; +``` + +存储结构 Entry: + +* Entry 继承 WeakReference,key 是弱引用,目的是将 ThreadLocal 对象的生命周期和线程生命周期解绑 +* Entry 限制只能用 ThreadLocal 作为 key,key 为 null (entry.get() == null) 意味着 key 不再被引用,entry 也可以从 table 中清除 + +```java +static class Entry extends WeakReference> { + Object value; + Entry(ThreadLocal k, Object v) { + super(k); + value = v; + } +} +``` + +构造方法:延迟初始化的,线程第一次存储 threadLocal - value 时才会创建 threadLocalMap 对象 + +```java +ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { + // 初始化table,创建一个长度为16的Entry数组 + table = new Entry[INITIAL_CAPACITY]; + // 寻址算法计算索引 + int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); + // 创建 entry 对象 存放到指定位置的 slot 中 + table[i] = new Entry(firstKey, firstValue); + // 数据总量是 1 + size = 1; + // 将阈值设置为 (当前数组长度 * 2)/ 3。 + setThreshold(INITIAL_CAPACITY); +} +``` + + + + + +*** + + + +##### 成员方法 + +* set():添加数据,ThreadLocalMap 使用**线性探测法来解决哈希冲突** + + * 该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出 + * 在探测过程中 ThreadLocal 会占用 key 为 null,value 不为 null 的脏 Entry 对象,防止内存泄漏 + * 假设当前 table 长度为16,计算出来 key 的 hash 值为 14,如果 table[14] 上已经有值,并且其 key 与当前 key 不一致,那么就发生了 hash 冲突,这个时候将 14 加 1 得到 15,取 table[15] 进行判断,如果还是冲突会回到 0,取 table[0],以此类推,直到可以插入,可以把 Entry[] table 看成一个**环形数组** + + * 线性探测法会出现**堆积问题**,可以采取平方探测法解决 + + ```java + private void set(ThreadLocal key, Object value) { + // 获取散列表 + ThreadLocal.ThreadLocalMap.Entry[] tab = table; + int len = tab.length; + // 计算当前 key 在散列表中的对应的位置 + int i = key.threadLocalHashCode & (len-1); + // 使用线性探测法向后查找元素,碰到 entry 为空时停止探测 + for (ThreadLocal.ThreadLocalMap.Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { + // 获取当前元素 key + ThreadLocal k = e.get(); + // ThreadLocal 对应的 key 存在,直接覆盖之前的值 + if (k == key) { + e.value = value; + return; + } + // 【这两个条件谁先成立不一定,所以 replaceStaleEntry 中还需要判断 k == key 的情况】 + + // key 为 null,但是值不为 null,说明之前的 ThreadLocal 对象已经被回收了,当前是过期数据 + if (k == null) { + // 碰到一个过期的 slot,当前数据占用该槽位,替换过期数据 + // 这个方法还进行了垃圾清理动作,防止内存泄漏 + replaceStaleEntry(key, value, i); + return; + } + } + // 逻辑到这说明碰到 slot == null 的位置,则在空元素的位置创建一个新的 Entry + tab[i] = new Entry(key, value); + //新添加,++size 后赋值给 sz + int sz = ++size; + + // 做一次启发式清理 + // 如果没有清除任何entry并且当前使用量达到了负载因子所定义,那么进行 rehash + if (!cleanSomeSlots(i, sz) && sz >= threshold) + // 扩容 + rehash(); + } + ``` + + ```java + // 获取环形数组的下一个索引 + private static int nextIndex(int i, int len) { + // 索引越界后从 0 开始继续获取 + return ((i + 1 < len) ? i + 1 : 0); + } + ``` + + ```java + // 在指定位置插入指定的数据 + private void replaceStaleEntry(ThreadLocal key, Object value, int staleSlot) { + // 获取散列表 + Entry[] tab = table; + int len = tab.length; + Entry e; + // 探测式清理过期数据的开始下标,默认从当前 staleSlot 开始 + int slotToExpunge = staleSlot; + // 以当前 staleSlot 开始【向前迭代查找】,找到索引靠前过期数据,找到以后替换 slotToExpunge 值 + for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) + if (e.get() == null) + slotToExpunge = i; + + // 以 staleSlot 向后去查找,直到碰到 null 为止 + for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { + // 获取当前节点的 key + ThreadLocal k = e.get(); + // 条件成立说明是【替换逻辑】 + if (k == key) { + e.value = value; + // 因为本来要在 staleSlot 索引处插入该数据,现在找到了i索引处的key与数据一致,所以需要交换位置 + // 将 table[staleSlot] 这个过期数据放到当前循环到的 table[i] 这个位置 + tab[i] = tab[staleSlot]; + tab[staleSlot] = e; + + // 条件成立说明向前查找过期数据并未找到过期的 entry,并且 staleSlot 位置不是过期数据了,i 位置才是 + if (slotToExpunge == staleSlot) + slotToExpunge = i; + + // 清理过期数据,expungeStaleEntry 探测式清理,cleanSomeSlots 启发式清理 + cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); + return; + } + // 条件成立说明当前遍历的 entry 是一个过期数据,并且该位置前面也没有过期数据 + if (k == null && slotToExpunge == staleSlot) + // 探测式清理过期数据的开始下标修改为当前循环的 index,因为 staleSlot 会放入要添加的数据 + slotToExpunge = i; + } + // 向后查找过程中并未发现 k == key 的 entry,说明当前是一个【添加逻辑】 + // 删除原有的数据引用,防止内存泄露 + tab[staleSlot].value = null; + // staleSlot 位置添加数据,上面的所有逻辑都不会更改 staleSlot 的值 + tab[staleSlot] = new Entry(key, value); + + // 条件成立说明除了 staleSlot 以外,还发现其它的过期 slot,所以要开启清理数据的逻辑 + if (slotToExpunge != staleSlot) + cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); + } + ``` + + ![](https://gitee.com/seazean/images/raw/master/Java/JUC-replaceStaleEntry流程.png) + + ```java + private static int prevIndex(int i, int len) { + // 形成一个环绕式的访问,头索引越界后置为尾索引 + return ((i - 1 >= 0) ? i - 1 : len - 1); + } + ``` + +* getEntry():ThreadLocal 的 get 方法以当前的 ThreadLocal 为 key,调用 getEntry 获取对应的存储实体 e + + ```java + private Entry getEntry(ThreadLocal key) { + // 哈希寻址 + int i = key.threadLocalHashCode & (table.length - 1); + // 访问散列表中指定指定位置的 slot + Entry e = table[i]; + // 条件成立,说明 slot 有值并且 key 就是要寻找的 key,直接返回 + if (e != null && e.get() == key) + return e; + else + // 进行线性探测 + return getEntryAfterMiss(key, i, e); + } + + private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) { + // 获取散列表 + Entry[] tab = table; + int len = tab.length; + + // 开始遍历,碰到 slot == null 的情况,搜索结束 + while (e != null) { + // 获取当前 slot 中 entry 对象的 key + ThreadLocal k = e.get(); + // 条件成立说明找到了,直接返回 + if (k == key) + return e; + // 过期数据 + if (k == null) + // 探测式过期数据回收 + expungeStaleEntry(i); + else + // 更新 index 继续向后走 + i = nextIndex(i, len); + // 获取下一个槽位中的 entry + e = tab[i]; + } + // 说明当前区段没有找到相应数据,因为存放数据是线性的向后寻找槽位,所以不可能越过一个 空槽位 在后面存放 + return null; + } + ``` + +* rehash():触发一次全量清理,如果数组长度大于等于长度的 `2/3 * 3/4 = 1/2`,则进行 resize + + ```java + private void rehash() { + // 清楚当前散列表内的所有过期的数据 + expungeStaleEntries(); + + //threshold = len * 2 / 3; + if (size >= threshold - threshold / 4) + resize(); + } + ``` + + ```java + private void expungeStaleEntries() { + Entry[] tab = table; + int len = tab.length; + // 遍历所有的槽位,清理过期数据 + for (int j = 0; j < len; j++) { + Entry e = tab[j]; + if (e != null && e.get() == null) + expungeStaleEntry(j); + } + } + ``` + + Entry 数组为扩容为原来的 2 倍 ,重新计算 key 的散列值,如果遇到 key 为 null 的情况,会将其 value 也置为 null,帮助 JVM GC + + ```java + private void resize() { + Entry[] oldTab = table; + int oldLen = oldTab.length; + // 新数组的长度是老数组的二倍 + int newLen = oldLen * 2; + Entry[] newTab = new Entry[newLen]; + // 统计新table中的entry数量 + int count = 0; + // 遍历老表,进行数据迁移 + for (int j = 0; j < oldLen; ++j) { + // 访问老表的指定位置的 entry + Entry e = oldTab[j]; + // 条件成立说明老表中该位置有数据,可能是过期数据也可能不是 + if (e != null) { + ThreadLocal k = e.get(); + // 过期数据 + if (k == null) { + e.value = null; // Help the GC + } else { + // 非过期数据,在新表中进行哈希寻址 + int h = k.threadLocalHashCode & (newLen - 1); + // 线程探测 + while (newTab[h] != null) + h = nextIndex(h, newLen); + // 将数据存放到新表合适的 slot 中 + newTab[h] = e; + count++; + } + } + } + //设置下一次触发扩容的指标:threshold = len * 2 / 3; + setThreshold(newLen); + size = count; + // 将扩容后的新表赋值给 threadLocalMap 内部散列表数组引用 + table = newTab; + } + ``` + + + +*** + + + +##### 清理方法 + +* 探测式清理:沿着开始位置向后探测清理过期数据,沿途中碰到未过期数据则将此数据 rehash 在 table 数组中的定位,重定位后的元素理论上更接近 `i = entry.key & (table.length - 1)`,会优化整个散列表查询性能 + + ```java + // table[staleSlot] 是一个过期数据,以这个位置开始继续向后查找过期数据 + private int expungeStaleEntry(int staleSlot) { + // 获取散列表和数组长度 + Entry[] tab = table; + int len = tab.length; + + // help gc,先把 entry 置空,在取消对 entry 的引用 + tab[staleSlot].value = null; + tab[staleSlot] = null; + // 数量-1 + size--; + + Entry e; + int i; + // 从 staleSlot 开始向后遍历,直到碰到 slot == null 结束 + for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { + ThreadLocal k = e.get(); + // 当前 entry 是过期数据 + if (k == null) { + // help gc + e.value = null; + tab[i] = null; + size--; + } else { + // 重新计算当前 entry 对应的 index + int h = k.threadLocalHashCode & (len - 1); + // 条件成立说明当前 entry 存储时发生过 hash 冲突,向后偏移过了 + if (h != i) { + // 当前位置置空 + tab[i] = null; + // 以正确位置 h 开始,向后查找第一个可以存放 entry 的位置 + while (tab[h] != null) + h = nextIndex(h, len); + // 将当前元素放入到【距离正确位置更近的位置,有可能就是正确位置】 + tab[h] = e; + } + } + } + // 返回 slot = null 的槽位索引,图例是 7 + return i; + } + ``` + + + + + +* 启发式清理: + + ```java + // i 表示启发式清理工作开始位置,n 一般传递的是 table.length + private boolean cleanSomeSlots(int i, int n) { + // 表示启发式清理工作是否清除了过期数据 + boolean removed = false; + // 获取当前 map 的散列表引用 + Entry[] tab = table; + int len = tab.length; + do { + // i 是 null,探测式返回的 slot 为 null 的位置 + // 获取下一个索引 + i = nextIndex(i, len); + Entry e = tab[i]; + // 条件成立说明是过期的数据,key 被 gc 了 + if (e != null && e.get() == null) { + // 发现过期数据重置 n 为数组的长度 + n = len; + // 表示清理过过期数据 + removed = true; + // 以当前过期的 slot 为开始节点 做一次探测式清理工作 + i = expungeStaleEntry(i); + } + // 假设 table 长度为 16 + // 16 >>> 1 ==> 8,8 >>> 1 ==> 4,4 >>> 1 ==> 2,2 >>> 1 ==> 1,1 >>> 1 ==> 0 + // 连续经过这么多次循环【没有扫描到过期数据】,就停止循环 + } while ((n >>>= 1) != 0); + + // 返回清除标记 + return removed; + } + ``` + + + +参考视频:https://space.bilibili.com/457326371/ + + + +*** + + + +#### 内存泄漏 + +Memory leak:内存泄漏是指程序中动态分配的堆内存由于某种原因未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果,内存泄漏的堆积终将导致内存溢出 + +* 如果 key 使用强引用: + + 使用完 ThreadLocal ,threadLocal Ref 被回收,但是 threadLocalMap 的 Entry 强引用了 threadLocal,造成 threadLocal 无法被回收,无法完全避免内存泄漏 + + + +* 如果 key 使用弱引用: + + 使用完 ThreadLocal ,threadLocal Ref 被回收,ThreadLocalMap 只持有 ThreadLocal 的弱引用,所以threadlocal 也可以被回收,此时 Entry 中的 key=null。但没有手动删除这个 Entry 或者 CurrentThread 依然运行,依然存在强引用链,value 不会被回收,而这块 value 永远不会被访问到,导致 value 内存泄漏 + + + +* 两个主要原因: + + * 没有手动删除这个 Entry + * CurrentThread 依然运行 + +根本原因:ThreadLocalMap 是 Thread的一个属性,生命周期跟 Thread 一样长,如果没有手动删除对应 Entry 就会导致内存泄漏 + +解决方法:使用完 ThreadLocal 中存储的内容后将它 **remove** 掉就可以 + +ThreadLocal 内部解决方法:在 ThreadLocalMap 中的 set/getEntry 方法中,会对 key 进行判断,如果为 null(ThreadLocal 为 null)的话,会对 Entry 进行垃圾回收。所以**使用弱引用比强引用多一层保障**,就算不调用 remove,也有机会进行 GC + + + +*** + + + +#### 变量传递 + +##### 基本使用 + +父子线程:**创建子线程的线程是父线程**,比如实例中的 main 线程就是父线程 + +ThreadLocal 中存储的是线程的局部变量,如果想实现线程间局部变量传递可以使用 InheritableThreadLocal 类 + +```java +public static void main(String[] args) { + ThreadLocal threadLocal = new InheritableThreadLocal<>(); + threadLocal.set("父线程设置的值"); + + new Thread(() -> System.out.println("子线程输出:" + threadLocal.get())).start(); +} +// 子线程输出:父线程设置的值 +``` + + + +*** + + + +##### 实现原理 + +InheritableThreadLocal 源码: + +```java +public class InheritableThreadLocal extends ThreadLocal { + protected T childValue(T parentValue) { + return parentValue; + } + ThreadLocalMap getMap(Thread t) { + return t.inheritableThreadLocals; + } + void createMap(Thread t, T firstValue) { + t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); + } +} +``` + +实现父子线程间的局部变量共享需要追溯到 Thread 对象的构造方法: + +```java +private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, + // 该参数默认是 true + boolean inheritThreadLocals) { + // ... + Thread parent = currentThread(); + + // 判断父线程(创建子线程的线程)的 inheritableThreadLocals 属性不为 null + if (inheritThreadLocals && parent.inheritableThreadLocals != null) { + // 复制父线程的 inheritableThreadLocals 属性,实现父子线程局部变量共享 + this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); + } + // .. +} +static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) { + return new ThreadLocalMap(parentMap); +} +``` + +```java +private ThreadLocalMap(ThreadLocalMap parentMap) { + // 获取父线程的哈希表 + Entry[] parentTable = parentMap.table; + int len = parentTable.length; + setThreshold(len); + table = new Entry[len]; + // 逐个复制父线程 ThreadLocalMap 中的数据 + for (int j = 0; j < len; j++) { + Entry e = parentTable[j]; + if (e != null) { + ThreadLocal key = (ThreadLocal) e.get(); + if (key != null) { + // 调用的是 InheritableThreadLocal#childValue(T parentValue) + Object value = key.childValue(e.value); + Entry c = new Entry(key, value); + int h = key.threadLocalHashCode & (len - 1); + // 线性探测 + while (table[h] != null) + h = nextIndex(h, len); + table[h] = c; + size++; + } + } + } +} +``` + + + +参考文章:https://blog.csdn.net/feichitianxia/article/details/110495764 + @@ -7506,7 +8348,7 @@ StampedLock:读写锁,该类自 JDK 8 加入,是为了进一步优化读 lock.unlockWrite(stamp); ``` -* 乐观读,StampedLock 支持 `tryOptimisticRead()` 方法,读取完毕后做一次**戳校验**,如果校验通过,表示这期间没有其他线程的写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全 +* 乐观读,StampedLock 支持 `tryOptimisticRead()` 方法,读取完毕后做一次**戳校验**,如果校验通过,表示这期间没有其他线程的写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据一致性 ```java long stamp = lock.tryOptimisticRead(); @@ -8277,7 +9119,6 @@ BUG 流程: * T1 获取信号量成功,调用 setHeadAndPropagate 时,读到 h.waitStatus < 0,从而调用 doReleaseShared() 唤醒 T2 ```java -// 新版本代码 private void setHeadAndPropagate(Node node, int propagate) { Node h = head; // 设置自己为 head 节点 diff --git a/SSM.md b/SSM.md index 7575a2a..dfbe8d3 100644 --- a/SSM.md +++ b/SSM.md @@ -14550,9 +14550,9 @@ SpringBoot 定义了一套接口规范,这套规范规定 SpringBoot 在启动 * `List configurations = SpringFactoriesLoader.loadFactoryNames()`:加载自动配置类 - 参数一:`getSpringFactoriesLoaderFactoryClass()` 获取 @EnableAutoConfiguration 注解类 + 参数一:`getSpringFactoriesLoaderFactoryClass()`:获取 @EnableAutoConfiguration 注解类 - 参数二:`getBeanClassLoader()` 获取类加载器 + 参数二:`getBeanClassLoader()`:获取类加载器 * `factoryTypeName = factoryType.getName()`:@EnableAutoConfiguration 注解的全类名 * `return loadSpringFactories(classLoaderToUse).getOrDefault()`:加载资源 From 1628cb1f7f8e030a39bb7b8f51c099a44d614805 Mon Sep 17 00:00:00 2001 From: Seazean <“imseazean@gmail.com”> Date: Wed, 18 Aug 2021 12:53:06 +0800 Subject: [PATCH 027/168] Update Java Notes --- Java.md | 67 ++--- Prog.md | 849 ++++++++++++++++++++++++++++++++++++++++++++++++-------- SSM.md | 475 +++++++++++++++++-------------- Tool.md | 10 +- 4 files changed, 1034 insertions(+), 367 deletions(-) diff --git a/Java.md b/Java.md index 536f72d..a4a36f6 100644 --- a/Java.md +++ b/Java.md @@ -4470,12 +4470,13 @@ PriorityQueue 是优先级队列,底层存储结构为 Object[],默认实现 java.utils.Collections:集合**工具类**,Collections并不属于集合,是用来操作集合的工具类 Collections有几个常用的API: -`public static boolean addAll(Collection c, T... e)`:给集合对象批量添加元素 -`public static void shuffle(List list)`:打乱集合顺序。 -`public static void sort(List list)`:将集合中元素按照默认规则排序。 -`public static void sort(List list,Comparator )`:集合中元素按照指定规则排序 -`public static List synchronizedList(List list)`:返回由指定 list 支持的线程安全 list -`public static Set singleton(T o)`:返回一个只包含指定对象的不可变组 + +* `public static boolean addAll(Collection c, T... e)`:给集合对象批量添加元素 +* `public static void shuffle(List list)`:打乱集合顺序 +* `public static void sort(List list)`:将集合中元素按照默认规则排序 +* `public static void sort(List list,Comparator )`:集合中元素按照指定规则排序 +* `public static List synchronizedList(List list)`:返回由指定 list 支持的线程安全 list +* `public static Set singleton(T o)`:返回一个只包含指定对象的不可变组 ```java public class CollectionsDemo { @@ -4515,12 +4516,11 @@ public class Student{ #### 概述 ->Collection是单值集合体系。 ->Map集合是一种双列集合,每个元素包含两个值。 +Collection 是单值集合体系,Map集合是一种双列集合,每个元素包含两个值。 -Map集合的每个元素的格式:key=value(键值对元素),Map集合也被称为“键值对集合” +Map集合的每个元素的格式:key=value(键值对元素),Map集合也被称为键值对集合 -Map集合的完整格式:`{key1=value1 , key2=value2 , key3=value3 , ...}` +Map集合的完整格式:`{key1=value1, key2=value2, key3=value3, ...}` ``` Map集合的体系: @@ -6632,7 +6632,7 @@ Stream 流其实就是一根传送带,元素在上面可以被 Stream 流操 作用: -* 可以解决已有集合类库或者数组API的弊端。 +* 可以解决已有集合类库或者数组 API 的弊端 * Stream 流简化集合和数组的操作 * 链式编程 @@ -6655,7 +6655,7 @@ list.stream().filter(s -> s.startsWith("张")); #### 获取流 -集合获取 Stream 流用:default Stream stream() +集合获取 Stream 流用:`default Stream stream()` 数组:Arrays.stream(数组) / Stream.of(数组); @@ -6686,16 +6686,16 @@ Stream arrStream2 = Stream.of(arr); #### 常用API -| 方法名 | 说明 | -| --------------------------------------------------------- | -------------------------------------------------------- | -| void forEach(Consumer action) | 逐一处理(遍历) | -| long count | 返回流中的元素数 | -| Stream filterPredicate predicate) | 用于对流中的数据进行过滤 | -| Stream limit(long maxSize) | 返回此流中的元素组成的流,截取前指定参数个数的数据 | -| Stream skip(long n) | 跳过指定参数个数的数据,返回由该流的剩余元素组成的流 | -| Stream map(Function mapper) | 加工方法,将当前流中的T类型数据转换为另一种R类型的流 | -| static Stream concat(Stream a, Stream b) | 合并a和b两个流为一个. 调用: `Stream.concat(s1,s2);` | -| Stream distinct() | 返回由该流的不同元素(根据Object.equals(Object) )组成的流 | +| 方法名 | 说明 | +| --------------------------------------------------------- | ---------------------------------------------------- | +| void forEach(Consumer action) | 逐一处理(遍历) | +| long count | 返回流中的元素数 | +| Stream filterPredicate predicate) | 用于对流中的数据进行过滤 | +| Stream limit(long maxSize) | 返回此流中的元素组成的流,截取前指定参数个数的数据 | +| Stream skip(long n) | 跳过指定参数个数的数据,返回由该流的剩余元素组成的流 | +| Stream map(Function mapper) | 加工方法,将当前流中的T类型数据转换为另一种R类型的流 | +| static Stream concat(Stream a, Stream b) | 合并a和b两个流为一个,调用 `Stream.concat(s1,s2)` | +| Stream distinct() | 返回由该流的不同元素组成的流 | ```java public class StreamDemo { @@ -6745,7 +6745,7 @@ class Student{ 终结方法:Stream 调用了终结方法,流的操作就全部终结,不能继续使用,如 foreach,count 方法等 -非终结方法:每次调用完成以后返回一个新的流对象,可以继续使用,支持**链式编程**! +非终结方法:每次调用完成以后返回一个新的流对象,可以继续使用,支持**链式编程** ```java // foreach终结方法 @@ -6766,28 +6766,29 @@ list.stream().filter(s -> s.startsWith("张")) * Stream流:工具 * 集合:目的 -| 方法名 | 说明 | -| ------------------------------------------------------------ | ---------------------- | -| R collect(Collector collector) | 把结果收集到集合中 | -| public static Collector toList() | 把元素收集到List集合中 | -| public static Collector toSet() | 把元素收集到Set集合中 | -| public static Collector toMap(Function keyMapper,Function valueMapper) | 把元素收集到Map集合中 | -| Object[] toArray() | 把元素收集数组中 | +Stream 收集方法:`R collect(Collector collector)` 把结果收集到集合中 + +Collectors 方法: + +* `public static Collector toList()`:把元素收集到 List 集合中 +* `public static Collector toSet()`:把元素收集到 Set 集合中 +* `public static Collector toMap(Function keyMapper,Function valueMapper)`:把元素收集到 Map 集合中 +* `Object[] toArray()`:把元素收集数组中 ```java public static void main(String[] args) { List list = new ArrayList<>(); - Stream stream=list.stream().filter(s -> s.startsWith("张")); + Stream stream = list.stream().filter(s -> s.startsWith("张")); //把stream流转换成Set集合。 Set set = stream.collect(Collectors.toSet()); //把stream流转换成List集合。 //重新定义,因为资源已经被关闭了 - Stream stream1=list.stream().filter(s -> s.startsWith("张")); + Stream stream1 = list.stream().filter(s -> s.startsWith("张")); List list = stream.collect(Collectors.toList()); //把stream流转换成数组。 - Stream stream2 =list.stream().filter(s -> s.startsWith("张")); + Stream stream2 = list.stream().filter(s -> s.startsWith("张")); Object[] arr = stream2.toArray(); // 可以借用构造器引用申明转换成的数组类型!!! String[] arr1 = stream2.toArray(String[]::new); diff --git a/Prog.md b/Prog.md index 20ab52b..613605f 100644 --- a/Prog.md +++ b/Prog.md @@ -477,7 +477,7 @@ public static void main(String[] args) throws Exception { System.out.println("park..."); LockSupport.park(); System.out.println("unpark..."); - Sout("打断状态:" + Thread.currentThread().isInterrupted()); //打断状态:true + System.out.println("打断状态:" + Thread.currentThread().isInterrupted());//打断状态:true }, "t1"); t1.start(); Thread.sleep(2000); @@ -608,11 +608,11 @@ t.start(); 不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁: -| 方法 | 功能 | -| --------------------------- | -------------------- | -| public final void stop() | 停止线程运行 | -| public final void suspend() | 挂起(暂停)线程运行 | -| public final void resume() | 恢复线程运行 | +| 方法 | 功能 | +| --------------------------- | ------------------------ | +| public final void stop() | 停止线程运行 | +| public final void suspend() | **挂起(暂停)线程运行** | +| public final void resume() | 恢复线程运行 | @@ -1178,16 +1178,16 @@ public static void method2() { ##### 自旋锁 -**重量级锁竞争**时,尝试获取锁的线程不会立即阻塞,可以使用**自旋**来进行优化,采用循环的方式去尝试获取锁 +重量级锁竞争时,尝试获取锁的线程不会立即阻塞,可以使用**自旋**来进行优化,采用循环的方式去尝试获取锁 注意: -* 自旋占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势 +* 自旋占用 CPU 时间,单核 CPU 自旋就是浪费时间,因为同一时刻只能运行一个线程,多核 CPU 自旋才能发挥优势 * 自旋失败的线程会进入阻塞状态 -优点:不会进入阻塞状态,减少线程上下文切换的消耗 +优点:不会进入阻塞状态,**减少线程上下文切换的消耗** -缺点:当自旋的线程越来越多时,会不断的消耗CPU资源 +缺点:当自旋的线程越来越多时,会不断的消耗 CPU 资源 自旋锁情况: @@ -2917,7 +2917,7 @@ CAS 的全称是 Compare-And-Swap,是 **CPU 并发原语** CAS 特点: -* CAS 体现的是**无锁并发、无阻塞并发**,没有使用 synchronized,所以线程不会陷入阻塞,线程不需要频繁切换状态(上下文切换,系统调用) +* CAS 体现的是**无锁并发、无阻塞并发**,线程不会陷入阻塞,线程不需要频繁切换状态(上下文切换,系统调用) * CAS 是基于乐观锁的思想 CAS 缺点: @@ -4525,7 +4525,7 @@ private ThreadLocalMap(ThreadLocalMap parentMap) { * 有界队列:有固定大小的队列,比如设定了固定大小的 LinkedBlockingQueue,又或者大小为 0 -* 无界队列:没有设置固定大小的队列,这些队列可以直接入队,直到溢出,一般不会有到这么大的容量(超过 Integer.MAX_VALUE),所以相当于 “无界” +* 无界队列:没有设置固定大小的队列,这些队列可以直接入队,直到溢出,一般不会有到这么大的容量(超过 Integer.MAX_VALUE),所以相当于无界 java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:**FIFO 队列** @@ -4533,11 +4533,11 @@ java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:**FIFO - LinkedBlockingQueue:由链表结构组成的无界(默认大小 Integer.MAX_VALUE)的阻塞队列 - PriorityBlockQueue:支持优先级排序的无界阻塞队列 - DelayQueue:使用优先级队列实现的延迟无界阻塞队列 -- SynchronousQueue:不存储元素的阻塞队列,每一个生产线程会阻塞到有一个put的线程放入元素为止 +- SynchronousQueue:不存储元素的阻塞队列,每一个生产线程会阻塞到有一个 put 的线程放入元素为止 - LinkedTransferQueue:由链表结构组成的无界阻塞队列 - LinkedBlockingDeque:由链表结构组成的**双向**阻塞队列 -与普通队列(LinkedList、ArrayList等)的不同点在于阻塞队列中阻塞添加和阻塞删除方法,以及线程安全: +与普通队列(LinkedList、ArrayList等)的不同点在于阻塞队列中阻塞添加和阻塞删除方法,以及线程安全: * 阻塞添加 take():当阻塞队列元素已满时,添加队列元素的线程会被阻塞,直到队列元素不满时才重新唤醒线程执行 * 阻塞删除 put():在队列元素为空时,删除队列元素的线程将被阻塞,直到队列不为空再执行删除操作(一般都会返回被删除的元素) @@ -4588,7 +4588,7 @@ public class LinkedBlockingQueue extends AbstractQueue * 下列三种情况之一 * - 真正的后继节点 * - 自己, 发生在出队时 - * - null, 表示是没有后继节点, 是最后了 + * - null, 表示是没有后继节点, 是尾节点了 */ Node next; @@ -4600,40 +4600,62 @@ public class LinkedBlockingQueue extends AbstractQueue 入队: * 初始化链表 `last = head = new Node(null)`,Dummy 节点用来占位,item 为 null - -* 当一个节点入队 `last = last.next = node` + + ```java + public LinkedBlockingQueue(int capacity) { + // 默认是 Integer.MAX_VALUE + if (capacity <= 0) throw new IllegalArgumentException(); + this.capacity = capacity; + last = head = new Node(null); + } + ``` + +* 当一个节点入队: + + ```java + private void enqueue(Node node) { + // 从右向左计算 + last = last.next = node; + } + ``` + ![](https://gitee.com/seazean/images/raw/master/Java/JUC-LinkedBlockingQueue入队流程.png) -* 再来一个节点入队`last = last.next = node` +* 再来一个节点入队 `last = last.next = node` 出队:出队首节点,先入先出 -* 源码: +* 出队源码: ```java - Node h = head; - Node first = h.next; - h.next = h; // help GC - head = first; - E x = first.item;// 保存数据 - first.item = null; - return x; + private E dequeue() { + Node h = head; + // 获取临头节点 + Node first = h.next; + // 自己指向自己,help GC + h.next = h; + head = first; + // 出队的元素 + E x = first.item; + // 当前节点置为 Dummy 节点 + first.item = null; + return x; + } ``` -* `h = head` -> `first = h.next` +* `h = head` → `first = h.next` ![](https://gitee.com/seazean/images/raw/master/Java/JUC-LinkedBlockingQueue出队流程1.png) -* `h.next = h` -> `head = first` +* `h.next = h` → `head = first` ![](https://gitee.com/seazean/images/raw/master/Java/JUC-LinkedBlockingQueue出队流程2.png) -* `E x = first.item` -> `first.item = null`(**head.item = null**) - - + + *** @@ -4649,8 +4671,7 @@ public class LinkedBlockingQueue extends AbstractQueue 线程安全分析: -* 当节点总数大于 2 时(包括 dummy 节点),putLock 保证的是 last 节点的线程安全,takeLock 保证的是 - head 节点的线程安全,两把锁保证了入队和出队没有竞争 +* 当节点总数大于 2 时(包括 dummy 节点),**putLock 保证的是 last 节点的线程安全,takeLock 保证的是 head 节点的线程安全**,两把锁保证了入队和出队没有竞争 * 当节点总数等于 2 时(即一个 dummy 节点,一个正常节点)这时候,仍然是两把锁锁两个对象,不会竞争 @@ -4659,8 +4680,11 @@ public class LinkedBlockingQueue extends AbstractQueue ```java // 用于 put(阻塞) offer(非阻塞) private final ReentrantLock putLock = new ReentrantLock(); + private final Condition notFull = putLock.newCondition(); // 阻塞等待不满,说明已经满了 + // 用户 take(阻塞) poll(非阻塞) private final ReentrantLock takeLock = new ReentrantLock(); + private final Condition notEmpty = takeLock.newCondition(); // 阻塞等待不空,说明已经是空的 ``` 入队出队: @@ -4669,39 +4693,46 @@ public class LinkedBlockingQueue extends AbstractQueue ```java public void put(E e) throws InterruptedException { + // 空指针异常 if (e == null) throw new NullPointerException(); int c = -1; + // 把待添加的元素封装为 node 节点 Node node = new Node(e); + // 获取全局生产锁 final ReentrantLock putLock = this.putLock; // count 用来维护元素计数 final AtomicInteger count = this.count; + // 获取可打断锁,会抛出异常 putLock.lockInterruptibly(); try { // 队列满了等待 while (count.get() == capacity) { - // 等待不满,就可以生产数据 + // 等待队列不满时,就可以生产数据 notFull.await(); } // 有空位, 入队且计数加一,尾插法 enqueue(node); // 返回自增前的数字 c = count.getAndIncrement(); - // 除了自己 put 以外, 队列还有空位, 唤醒其他生产put线程 + // put 完队列还有空位, 唤醒其他生产 put 线程,唤醒一个减少竞争 if (c + 1 < capacity) notFull.signal(); } finally { + // 解锁 putLock.unlock(); } - // 如果还有一个元素,唤醒 take 线程 + // c自增前是0,说明生产了一个元素,唤醒 take 线程 if (c == 0) signalNotEmpty(); } + ``` + + ```java private void signalNotEmpty() { final ReentrantLock takeLock = this.takeLock; takeLock.lock(); try { - // 调用的是 notEmpty.signal(),而不是 notEmpty.signalAll(),是为了减少竞争 - // 因为只剩下一个元素 + // 调用 notEmpty.signal(),而不是 notEmpty.signalAll() 是为了减少竞争,因为只剩下一个元素 notEmpty.signal(); } finally { takeLock.unlock(); @@ -4717,27 +4748,28 @@ public class LinkedBlockingQueue extends AbstractQueue int c = -1; // 元素个数 final AtomicInteger count = this.count; + // 获取全局消费锁 final ReentrantLock takeLock = this.takeLock; + // 可打断锁 takeLock.lockInterruptibly(); try { - //没有元素可以出队 + // 没有元素可以出队 while (count.get() == 0) { - // 等待不空,就可以消费数据 + // 阻塞等待队列不空,就可以消费数据 notEmpty.await(); } - // 出队,计数减一,Removes a node from head of queue,FIFO + // 出队,计数减一,FIFO,出队头节点 x = dequeue(); - // 返回自减前的数子 + // 返回自减前的数字 c = count.getAndDecrement(); // 队列还有元素 if (c > 1) - // 唤醒其他消费take线程 + // 唤醒一个消费take线程 notEmpty.signal(); } finally { takeLock.unlock(); } - // 如果队列中只有一个空位时, 叫醒 put 线程 - // 如果有多个线程进行出队, 第一个线程满足 c == capacity, 但后续线程 c < capacity + // c 是消费前的数据,消费前满了,消费一个后还剩一个空位,唤醒生产线程 if (c == capacity) // 调用的是 notFull.signal() 而不是 notFull.signalAll() 是为了减少竞争 signalNotFull(); @@ -4771,9 +4803,593 @@ public class LinkedBlockingQueue extends AbstractQueue #### 同步队列 -与其他 BlockingQueue 不同,SynchronousQueue 是一个不存储元素的 BlockingQueue +##### 成员属性 + +与其他 BlockingQueue 不同,SynchronousQueue 是一个不存储元素的 BlockingQueue,每一个生产者必须阻塞匹配到一个消费者 + +成员变量: + +* 运行当前程序的平台拥有 CPU 的数量: + + ```java + static final int NCPUS = Runtime.getRuntime().availableProcessors() + ``` + +* 指定超时时间后,当前线程最大自旋次数: + + ```java + // 只有一个 CPU 时自旋次数为 0,所有程序都是串行执行,多核 CPU 时自旋 32 次是一个经验值 + static final int maxTimedSpins = (NCPUS < 2) ? 0 : 32; + ``` + + 自旋的原因:线程挂起唤醒需要进行上下文切换,涉及到用户态和内核态的转变,是非常消耗资源的。自旋期间线程会一直检查自己的状态是否被匹配到,如果自旋期间被匹配到,那么直接就返回了,如果自旋次数达到某个指标后,还是会将当前线程挂起的。 + +* 未指定超时时间,当前线程最大自旋次数: + + ```java + static final int maxUntimedSpins = maxTimedSpins * 16; // maxTimedSpins 的 16 倍 + ``` + +* 指定超时限制的阈值,小于该值的线程不会被挂起: + + ```java + static final long spinForTimeoutThreshold = 1000L; // 纳秒 + ``` + + 超时时间设置的小于该值,就会被禁止挂起,阻塞在唤醒的成本太高,不如选择自旋空转 + +* 转换器: + + ```java + private transient volatile Transferer transferer; + abstract static class Transferer { + /** + * 参数一:可以为 null,null 时表示这个请求是一个 REQUEST 类型的请求,反之是一个 DATA 类型的请求 + * 参数二:如果为 true 表示指定了超时时间,如果为 false 表示不支持超时,会一直阻塞到匹配或者倍打断 + * 参数三:超时时间限制,单位是纳秒 + + * 返回值:返回值如果不为 null 表示匹配成功,DATA 类型的请求返回当前线程 put 的数据 + * 如果返回 null,表示请求超时或被中断 + */ + abstract E transfer(E e, boolean timed, long nanos); + } + ``` + +* 构造方法: + + ```java + public SynchronousQueue(boolean fair) { + // fair 默认 false + // 非公平模式实现的同步队列,内部数据结构是 栈,公平的是的队列 + transferer = fair ? new TransferQueue() : new TransferStack(); + } + ``` + +* 成员方法: + + ```java + public boolean offer(E e) { + if (e == null) throw new NullPointerException(); + return transferer.transfer(e, true, 0) != null; + } + public E poll() { + return transferer.transfer(null, true, 0); + } + ``` + + + +**** + + + +##### 非公实现 + +TransferStack 是非公平的同步队列,因为所有的请求都被压入栈中,栈顶的元素会最先得到匹配,造成栈底的等待线程饥饿 + +TransferStack 类成员变量: + +* 请求类型: + + ```java + // 表示 Node 类型为请求类型 + static final int REQUEST = 0; + // 表示 Node类 型为数据类型 + static final int DATA = 1; + // 表示 Node 类型为匹配中类型 + // 假设栈顶元素为 REQUEST-NODE,当前请求类型为 DATA,入栈会修改类型为 FULFILLING 【栈顶 & 栈顶之下的一个node】 + // 假设栈顶元素为 DATA-NODE,当前请求类型为 REQUEST,入栈会修改类型为 FULFILLING 【栈顶 & 栈顶之下的一个node】 + static final int FULFILLING = 2; + ``` + +* 栈顶元素: + + ```java + volatile SNode head; + ``` + +内部类 SNode: + +* 成员变量: + + ```java + static final class SNode { + // 指向下一个栈帧 + volatile SNode next; + // 与当前 node 匹配的节点 + volatile SNode match; + // 假设当前node对应的线程自旋期间未被匹配成功,那么node对应的线程需要挂起, + // 挂起前 waiter 保存对应的线程引用,方便匹配成功后,被唤醒。 + volatile Thread waiter; + + // 数据域,不为空表示当前 Node 对应的请求类型为 DATA 类型,反之则表示 Node 为 REQUEST 类型 + Object item; + // 表示当前Node的模式 【DATA/REQUEST/FULFILLING】 + int mode; + } + ``` + +* 构造方法: + + ```java + SNode(Object item) { + this.item = item; + } + ``` + +* 设置方法:设置 Node 对象的 next 字段,此处**对 CAS 进行了优化**,提升了 CAS 的效率 + + ```java + boolean casNext(SNode cmp, SNode val) { + //【优化:cmp == next】,可以提升一部分性能。 cmp == next 不相等,就没必要走 cas指令。 + return cmp == next && UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val); + } + ``` + +* 匹配方法: + + ```java + boolean tryMatch(SNode s) { + // 当前 node 尚未与任何节点发生过匹配,CAS 设置 match 字段为 s 节点,表示当前 node 已经被匹配 + if (match == null && UNSAFE.compareAndSwapObject(this, matchOffset, null, s)) { + // 当前 node 如果自旋结束,会 park 阻塞,阻塞前将 node 对应的 Thread 保留到 waiter 字段 + Thread w = waiter; + //条件成立说明 node 对应的 Thread 已经阻塞 + if (w != null) { + waiter = null; + //使用 unpark 方式唤醒线程 + LockSupport.unpark(w); + } + return true; + } + // 匹配成功返回 true + return match == s; + } + ``` + +* 取消方法: + + ```java + // 取消节点的方法 + void tryCancel() { + // match 字段保留当前 node 对象本身,表示这个 node 是取消状态,取消状态的 node,最终会被强制移除出栈 + UNSAFE.compareAndSwapObject(this, matchOffset, null, this); + } + + boolean isCancelled() { + return match == this; + } + ``` + +TransferStack 类成员方法: + +* snode():填充节点方法 + + ```java + static SNode snode(SNode s, Object e, SNode next, int mode) { + // 引用指向空时,snode 方法会创建一个 SNode 对象 + if (s == null) s = new SNode(e); + s.mode = mode; + s.next = next; + return s; + } + ``` + +* transfer():核心方法,请求匹配出栈,不匹配阻塞 + + ```java + E transfer(E e, boolean timed, long nanos) { + // 包装当前线程的 node + SNode s = null; + // 根据元素判断当前的请求类型 + int mode = (e == null) ? REQUEST : DATA; + // 自旋 + for (;;) { + // 获取栈顶指针 + SNode h = head; + // 【CASE1】:当前栈为空或者栈顶 node 模式与当前请求模式一致无法匹配,做入栈操作 + if (h == null || h.mode == mode) { + // 当前请求是支持超时的,但是 nanos <= 0 说明这个请求不支持 “阻塞等待” + if (timed && nanos <= 0) { + // 栈顶元素是取消状态 + if (h != null && h.isCancelled()) + // 栈顶出栈,设置新的栈顶 + casHead(h, h.next); + else + // 表示【匹配失败】 + return null; + // 入栈 + } else if (casHead(h, s = snode(s, e, h, mode))) { + // 等待被匹配的逻辑,正常情况返回匹配的节点;取消情况返回当前节点,就是 s + SNode m = awaitFulfill(s, timed, nanos); + // 说明当前 node 是【取消状态】 + if (m == s) { + // 将取消节点出栈 + clean(s); + return null; + } + // 执行到这说明【匹配成功】了 + // 栈顶有节点并且 匹配节点还未出栈,需要协助出栈 + if ((h = head) != null && h.next == s) + casHead(h, s.next); + // 当前 node 模式为 REQUEST 类型,返回匹配节点的 m.item 数据域 + // 当前 node 模式为 DATA 类型:返回 Node.item 数据域,当前请求提交的数据 e + return (E) ((mode == REQUEST) ? m.item : s.item); + } + // 【CASE2】:当前栈顶模式与请求模式不一致,且栈顶不是 FULFILLING说明没被其他节点匹配,说明可以匹配 + } else if (!isFulfilling(h.mode)) { + // 头节点是取消节点,协助出栈 + if (h.isCancelled()) + casHead(h, h.next); + // 入栈当前请求的节点 + else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) { + for (;;) { + // m 是 s 的匹配的节点 + SNode m = s.next; + // m 节点在 awaitFulfill 方法中被中断,clean 了自己 + if (m == null) { + // 清空栈 + casHead(s, null); + s = null; + // 返回到外层自旋中 + break; + } + // 获取匹配节点的下一个节点 + SNode mn = m.next; + // 尝试匹配,匹配成功,则将 fulfilling 和 m 一起出栈 + if (m.tryMatch(s)) { + casHead(s, mn); // pop both s and m + return (E) ((mode == REQUEST) ? m.item : s.item); + } else + // 匹配失败,出栈 m + s.casNext(m, mn); + } + } + // 【CASE3】:栈顶模式为 FULFILLING 模式,表示栈顶和栈顶下面的栈帧正在发生匹配,当前请求需要做协助工作 + } else { + // h 表示的是 fulfilling 节点,m 表示 fulfilling 匹配的节点 + SNode m = h.next; + if (m == null) + // 清空栈 + casHead(h, null); + else { + SNode mn = m.next; + if (m.tryMatch(h)) + casHead(h, mn); + else + h.casNext(m, mn); + } + } + } + } + ``` + +* awaitFulfill():阻塞当前线程等待被匹配,返回匹配的节点,或者被取消的节点 + + ```java + SNode awaitFulfill(SNode s, boolean timed, long nanos) { + // 等待的截止时间 + final long deadline = timed ? System.nanoTime() + nanos : 0L; + // 当前线程 + Thread w = Thread.currentThread(); + // 表示当前请求线程在下面的 for(;;) 自旋检查的次数 + int spins = (shouldSpin(s) ? (timed ? maxTimedSpins : maxUntimedSpins) : 0); + // 自旋检查逻辑:是否匹配、是否超时、是否被中断 + for (;;) { + // 当前线程收到中断信号,需要设置 node 状态为取消状态 + if (w.isInterrupted()) + s.tryCancel(); + // 获取与当前 s 匹配的节点 + SNode m = s.match; + if (m != null) + // 可能是正常的匹配的,也可能是取消的 + return m; + // 执行了超时限制就判断是否超时 + if (timed) { + nanos = deadline - System.nanoTime(); + // 超时了,取消节点 + if (nanos <= 0L) { + s.tryCancel(); + continue; + } + } + // 说明当前线程还可以进行自旋检查 + if (spins > 0) + // 自旋一次 递减 1 + spins = shouldSpin(s) ? (spins - 1) : 0; + // 说明没有自旋次数了 + else if (s.waiter == null) + // 把当前 node 对应的 Thread 保存到 node.waiter 字段中 + s.waiter = w; + // 没有超时限制直接阻塞 + else if (!timed) + LockSupport.park(this); + // nanos > 1000 纳秒的情况下,才允许挂起当前线程 + else if (nanos > spinForTimeoutThreshold) + LockSupport.parkNanos(this, nanos); + } + } + ``` + + ```java + boolean shouldSpin(SNode s) { + // 获取栈顶 + SNode h = head; + // 条件一成立说明当前 s 就是栈顶,允许自旋检查 + // 条件二成立说明当前 s 节点自旋检查期间,又来了一个与当前 s 节点匹配的请求,双双出栈后条件会成立 + // 条件三成立前提当前 s 不是栈顶元素,并且当前栈顶正在匹配中,这种状态栈顶下面的元素,都允许自旋检查 + return (h == s || h == null || isFulfilling(h.mode)); + } + ``` + +* clear():指定节点出栈 + + ```java + void clean(SNode s) { + // 清空数据域和关联线程 + s.item = null; + s.waiter = null; + + // 获取取消节点的下一个节点 + SNode past = s.next; + // 判断后继节点是不是取消节点,是就更新 past + if (past != null && past.isCancelled()) + past = past.next; + + SNode p; + // 从栈顶开始向下检查,将栈顶开始向下的 取消状态 的节点全部清理出去,直到碰到 past 或者不是取消状态为止 + while ((p = head) != null && p != past && p.isCancelled()) + // 修改的是内存地址对应的值,p 指向该内存地址所以数据一直在变化 + casHead(p, p.next); + // 说明中间遇到了不是取消状态的节点,继续迭代下去 + while (p != null && p != past) { + SNode n = p.next; + if (n != null && n.isCancelled()) + p.casNext(n, n.next); + else + p = n; + } + } + ``` + + + +*** + + + +##### 公平实现 + +TransferQueue 是公平的同步队列,采用 FIFO 的队列实现 + +TransferQueue 类成员变量: + +* 指向队列的 dummy 节点: + + ```java + transient volatile QNode head; + ``` + +* 指向队列的尾节点: + + ```java + transient volatile QNode tail; + ``` + +* 被清理节点的前驱节点: + + ```java + transient volatile QNode cleanMe; + ``` + + 入队操作是两步完成的,第一步是 t.next = newNode,第二步是 tail = newNode,所以队尾节点出队,是一种非常特殊的情况 + +TransferQueue 内部类: + +* QNode: + + ```java + static final class QNode { + // 指向当前节点的下一个节点 + volatile QNode next; + // 数据域,Node 代表的是 DATA 类型 item 表示数据,否则 Node 代表的 REQUEST 类型,item == null + volatile Object item; + // 假设当前 node 对应的线程自旋期间未被匹配成功,那么 node 对应的线程需要挂起, + // 挂起前 waiter 保存对应的线程引用,方便匹配成功后被唤醒。 + volatile Thread waiter; + // true 当前 Node 是一个 DATA 类型,false 表示当前 Node 是一个 REQUEST 类型 + final boolean isData; + + // 构建方法 + QNode(Object item, boolean isData) { + this.item = item; + this.isData = isData; + } + + // 尝试取消当前 node,取消状态的 node 的 item 域指向自己 + void tryCancel(Object cmp) { + UNSAFE.compareAndSwapObject(this, itemOffset, cmp, this); + } + + // 判断当前 node 是否为取消状态 + boolean isCancelled() { + return item == this; + } + + // 判断当前节点是否 “不在” 队列内,当 next 指向自己时,说明节点已经出队。 + boolean isOffList() { + return next == this; + } + } + ``` + +TransferQueue 类成员方法: + +* 设置头尾节点: + + ```java + void advanceHead(QNode h, QNode nh) { + // 设置头指针指向新的节点, + if (h == head && UNSAFE.compareAndSwapObject(this, headOffset, h, nh)) + // 老的头节点出队 + h.next = h; + } + void advanceTail(QNode t, QNode nt) { + if (tail == t) + // 更新队尾节点为新的队尾 + UNSAFE.compareAndSwapObject(this, tailOffset, t, nt); + } + ``` + +* transfer():核心方法 -(待更新) + ```java + E transfer(E e, boolean timed, long nanos) { + // s 指向当前请求对应的 node + QNode s = null; + // 是否是 DATA 类型的请求 + boolean isData = (e != null); + // 自旋 + for (;;) { + QNode t = tail; + QNode h = head; + if (t == null || h == null) + continue; + // head 和 tail 同时指向 dummy 节点,说明是空队列,或者队尾节点与当前请求类型是一致的情况,无法匹配 + if (h == t || t.isData == isData) { + // 获取队尾 t 的 next 节点 + QNode tn = t.next; + // 多线程环境中其他线程可能修改尾节点 + if (t != tail) + continue; + // 已经有线程入队了,更新 tail + if (tn != null) { + advanceTail(t, tn); + continue; + } + // 允许超时,超时时间小于 0,这种方法不支持阻塞等待 + if (timed && nanos <= 0) + return null; + // 创建 node 的逻辑 + if (s == null) + s = new QNode(e, isData); + // 将 node 添加到队尾 + if (!t.casNext(null, s)) + continue; + // 更新队尾指针 + advanceTail(t, s); + + // 当前节点 等待匹配.... + Object x = awaitFulfill(s, e, timed, nanos); + + // 说明当前 node 状态为 取消状态,需要做出队逻辑 + if (x == s) { + clean(t, s); + return null; + } + // 说明当前 node 仍然在队列内,需要做匹配成功后 出队逻辑 + if (!s.isOffList()) { + // t 是当前 s 节点的前驱节点,判断 t 是不是头节点,是就更新 dummy 节点为 s 节点 + advanceHead(t, s); + // s 节点已经出队,所以需要把它的 item 域设置为它自己,表示它是个取消状态 + if (x != null) + s.item = s; + s.waiter = null; + } + return (x != null) ? (E)x : e; + // 队尾节点与当前请求节点互补 + } else { + // h.next 节点,请求节点与队尾模式不同,需要与队头发生匹配,TransferQueue 是一个【公平模式】 + QNode m = h.next; + // 并发导致其他线程修改了队尾节点,或者已经把 head.next 匹配走了 + if (t != tail || m == null || h != head) + continue; + // 获取匹配节点的数据域保存到 x + Object x = m.item; + // 判断是否匹配成功 + if (isData == (x != null) || + x == m || + !m.casItem(x, e)) { + advanceHead(h, m); + continue; + } + // 【匹配完成】,将头节点出队,让这个真正的头结点成为 dummy 节点 + advanceHead(h, m); + // 唤醒该匹配节点的线程 + LockSupport.unpark(m.waiter); + return (x != null) ? (E)x : e; + } + } + } + ``` + +* awaitFulfill():阻塞当前线程等待被匹配 + + ```java + Object awaitFulfill(QNode s, E e, boolean timed, long nanos) { + // 表示等待截止时间 + final long deadline = timed ? System.nanoTime() + nanos : 0L; + Thread w = Thread.currentThread(); + // 自选检查的次数 + int spins = ((head.next == s) ? (timed ? maxTimedSpins : maxUntimedSpins) : 0); + for (;;) { + // 被打断就取消节点 + if (w.isInterrupted()) + s.tryCancel(e); + // 获取当前 Node 数据域 + Object x = s.item; + + // 当前请求为 DATA 模式时:e 请求带来的数据 + // s.item 修改为 this,说明当前 QNode 对应的线程 取消状态 + // s.item 修改为 null 表示已经有匹配节点了,并且匹配节点拿走了 item 数据 + + // 当前请求为 REQUEST 模式时:e == null + // s.item 修改为 this,说明当前 QNode 对应的线程 取消状态 + // s.item != null 且 item != this 表示当前 REQUEST 类型的 Node 已经匹配到 DATA 了 + if (x != e) + return x; + // 超时检查 + if (timed) { + nanos = deadline - System.nanoTime(); + if (nanos <= 0L) { + s.tryCancel(e); + continue; + } + } + // 自旋次数减一 + if (spins > 0) + --spins; + // 没有自旋次数了,把当前线程封装进去 waiter + else if (s.waiter == null) + s.waiter = w; + // 阻塞 + else if (!timed) + LockSupport.park(this); + else if (nanos > spinForTimeoutThreshold) + LockSupport.parkNanos(this, nanos); + } + } + ``` @@ -4783,13 +5399,13 @@ public class LinkedBlockingQueue extends AbstractQueue #### 延迟队列 -DelayQueue 是一个支持延时获取元素的阻塞队列, 内部采用优先队列 PriorityQueue 存储元素,同时元素必须实现 Delayed 接口;在创建元素时可以指定多久才可以从队列中获取当前元素,只有在延迟期满时才能从队列中提取元素 +DelayQueue 是一个支持延时获取元素的阻塞队列,内部采用优先队列 PriorityQueue 存储元素,同时元素必须实现 Delayed 接口;在创建元素时可以指定多久才可以从队列中获取当前元素,只有在延迟期满时才能从队列中提取元素 DelayQueue 只能添加(offer/put/add)实现了 Delayed 接口的对象,不能添加 int、String API: -* `getDelay()`:获取元素在队列中的剩余时间,只有当剩余时间为0时元素才可以出队列。 +* `getDelay()`:获取元素在队列中的剩余时间,只有当剩余时间为 0 时元素才可以出队列。 * `compareTo()`:用于排序,确定元素出队列的顺序 ```java @@ -6764,6 +7380,7 @@ ReentrantLock 相对于 synchronized 它具备如下特点: 4. 可中断:ReentrantLock 可中断,而 synchronized 不行 5. **公平锁**:公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁 * ReentrantLock 可以设置公平锁,synchronized 中的锁是非公平的 + * 不公平锁的含义是阻塞队列内公平,队列外非公平 6. 锁超时:尝试获取锁,超时获取不到直接放弃,不进入阻塞队列 * ReentrantLock 可以设置超时时间,synchronized 会一直等待 7. 锁绑定多个条件:一个 ReentrantLock 可以同时绑定多个 Condition 对象 @@ -6851,7 +7468,7 @@ NonfairSync 继承自 AQS ```java // ReentrantLock.NonfairSync#lock final void lock() { - // 用 cas 尝试(仅尝试一次)将 state 从 0 改为 1, 如果成功表示获得了独占锁 + // 用 cas 尝试(仅尝试一次)将 state 从 0 改为 1, 如果成功表示【获得了独占锁】 if (compareAndSetState(0, 1)) // 设置当前线程为独占线程 setExclusiveOwnerThread(Thread.currentThread()); @@ -7384,6 +8001,7 @@ public static void main(String[] args) throws InterruptedException { ``` ```java + // 所有的节取消节点出队的逻辑 private void cancelAcquire(Node node) { // 判空 if (node == null) @@ -7421,7 +8039,7 @@ public static void main(String[] args) throws InterruptedException { Node next = node.next; // 当前节点的后继节点是正常节点 if (next != null && next.waitStatus <= 0) - // 把前驱节点的后继节点设置为 当前节点的后继节点,从队列中删除了当前节点 + // 把 前驱节点的后继节点 设置为 当前节点的后继节点,从队列中删除了当前节点 compareAndSetNext(pred, predNext, next); } else { // 当前节点是 head.next 节点,唤醒当前节点的后继节点 @@ -7431,7 +8049,7 @@ public static void main(String[] args) throws InterruptedException { } } ``` - + @@ -8550,41 +9168,14 @@ public static void main(String[] args) throws InterruptedException { if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { // 获取当前节点的后继节点 Node s = node.next; - // 当前节点是尾节点时 next 为 null,或者后继节点是 SHARED共享模式 + // 当前节点是尾节点时 next 为 null,或者后继节点是 SHARED 共享模式 if (s == null || s.isShared()) + // 唤醒所有的等待共享锁的节点 doReleaseShared(); } } ``` - - ```java - // 唤醒后继节点 - private void doReleaseShared() { - for (;;) { - Node h = head; - // 判断队列是否是空队列 - if (h != null && h != tail) { - int ws = h.waitStatus; - // 头节点的状态为 signal,说明后继节点没有被唤醒过 - if (ws == Node.SIGNAL) { - // cas 设置头节点的状态为 0,设置失败继续自旋 - if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) - continue; - // 唤醒后继节点 - unparkSuccessor(h); - } - // 如果有其他线程已经设置了头节点的状态,重新设置为 PROPAGATE 传播属性 - else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) - continue; - } - // 条件不成立说明被唤醒的节点非常积极,直接将自己设置为了新的head, - // 此时唤醒它的节点(前驱)执行 h == head 不成立,所以不会跳出循环,会继续唤醒新 head 节点的后继 - if (h == head) - break; - } - } - ``` - + 计数减一: @@ -8624,7 +9215,36 @@ public static void main(String[] args) throws InterruptedException { } ``` +* state = 0 时,当前线程需要执行唤醒阻塞节点的任务 + ```java + private void doReleaseShared() { + for (;;) { + Node h = head; + // 判断队列是否是空队列 + if (h != null && h != tail) { + int ws = h.waitStatus; + // 头节点的状态为 signal,说明后继节点没有被唤醒过 + if (ws == Node.SIGNAL) { + // cas 设置头节点的状态为 0,设置失败继续自旋 + if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) + continue; + // 唤醒后继节点 + unparkSuccessor(h); + } + // 如果有其他线程已经设置了头节点的状态,重新设置为 PROPAGATE 传播属性 + else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) + continue; + } + // 条件不成立说明被唤醒的节点非常积极,直接将自己设置为了新的head, + // 此时唤醒它的节点(前驱)执行 h == head 不成立,所以不会跳出循环,会继续唤醒新的 head 节点的后继节点 + if (h == head) + break; + } + } + ``` + + @@ -8772,7 +9392,7 @@ public static void main(String[] args) { // 获取当前代 final Generation g = generation; - // 如果当前代是已经被打破状态,则当前调用await方法的线程,直接抛出Broken异常 + // 【如果当前代是已经被打破状态,则当前调用await方法的线程,直接抛出Broken异常】 if (g.broken) throw new BrokenBarrierException(); // 如果当前线程的中断标记位为 true,则打破当前代,然后当前线程抛出中断异常 @@ -8786,7 +9406,7 @@ public static void main(String[] args) { // 假设 parties 给的是 5,那么index对应的值为 4,3,2,1,0 int index = --count; - // 条件成立说明当前线程是最后一个到达 barrier 的线程 + // 条件成立说明当前线程是最后一个到达 barrier 的线程,【需要开启新代,唤醒阻塞线程】 if (index == 0) { // 栅栏任务启动标记 boolean ranAction = false; @@ -8797,7 +9417,7 @@ public static void main(String[] args) { command.run(); // run()未抛出异常的话,启动标记设置为 true ranAction = true; - // 开启新的一代 + // 开启新的一代,这里会【唤醒所有的阻塞队列】 nextGeneration(); // 返回 0 因为当前线程是此代最后一个到达的线程,index == 0 return 0; @@ -8831,12 +9451,11 @@ public static void main(String[] args) { Thread.currentThread().interrupt(); } } - // 被唤醒执行到这里 - // 当前代已经被打破,线程唤醒后依次抛出 BrokenBarrier 异常 + // 唤醒后的线程,【判断当前代已经被打破,线程唤醒后依次抛出 BrokenBarrier 异常】 if (g.broken) throw new BrokenBarrierException(); - //当前线程挂起期间,最后一个线程到位了,然后触发了开启新的一代的逻辑,此时唤醒 trip 条件队列内的线程 + // 当前线程挂起期间,最后一个线程到位了,然后触发了开启新的一代的逻辑 if (g != generation) return index; // 当前线程 trip 中等待超时,然后主动转移到阻塞队列 @@ -8852,7 +9471,7 @@ public static void main(String[] args) { } } ``` - + * breakBarrier():打破 Barrier 屏障 ```java @@ -8861,7 +9480,7 @@ public static void main(String[] args) { generation.broken = true; //重置 count 为 parties count = parties; - // 将在trip条件队列内挂起的线程全部唤醒,唤醒后的线程会检查当前是否是打破的 + // 将在trip条件队列内挂起的线程全部唤醒,唤醒后的线程会检查当前是否是打破的,然后抛出异常 trip.signalAll(); } ``` @@ -8875,7 +9494,7 @@ public static void main(String[] args) { // 重置 count 为 parties count = parties; - // 开启新的一代..使用一个新的generation对象,表示新的一代,新的一代和上一代【没有任何关系】 + // 开启新的一代,使用一个新的generation对象,表示新的一代,新的一代和上一代【没有任何关系】 generation = new Generation(); } ``` @@ -8998,7 +9617,7 @@ public static void main(String[] args) { int r = tryAcquireShared(arg); if (r >= 0) { // 成功后本线程出队(AQS), 所在 Node设置为 head - // r 表示可用资源数, 为 0 则不会继续传播 + // r 表示【可用资源数】, 为 0 则不会继续传播 setHeadAndPropagate(node, r); p.next = null; // help GC failed = false; @@ -9022,7 +9641,7 @@ public static void main(String[] args) { Node h = head; // 设置自己为 head 节点 setHead(node); - // propagate 表示有共享资源(例如共享读锁或信号量) + // propagate 表示有【共享资源】(例如共享读锁或信号量) // head waitStatus == Node.SIGNAL 或 Node.PROPAGATE,doReleaseShared 函数中设置的 if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { @@ -9070,7 +9689,7 @@ public static void main(String[] args) { ![](https://gitee.com/seazean/images/raw/master/Java/JUC-Semaphore工作流程2.png) -* 接下来 Thread-0 竞争成功,permits 再次设置为 0,设置自己为 head 节点,unpark 接下来的 Thread-3 节点,但由于 permits 是 0,因此 Thread-3 在尝试不成功后再次进入 park 状态 +* 接下来 Thread-0 竞争成功,permits 再次设置为 0,设置自己为 head 节点,并且 unpark 接下来的共享状态的 Thread-3 节点,但由于 permits 是 0,因此 Thread-3 在尝试不成功后再次进入 park 状态 @@ -9545,7 +10164,7 @@ B站视频解析:https://www.bilibili.com/video/BV1n541177Ea // 4 → 100 numberOfLeadingZeros(4) = 29 int 值就是占4个字节 // ASHIFT = 31 - 29 = 2 ,int 的大小就是 2 的 2 次方 - // ABASE + (5 << ASHIFT) 用位移运算替代了乘法,看 tabAt 寻址方法 + // ABASE + (5 << ASHIFT) 用位移运算替代了乘法,获取 arr[5] 的值 ASHIFT = 31 - Integer.numberOfLeadingZeros(scale); ``` @@ -9792,7 +10411,7 @@ public V put(K key, V value) { * spread():扰动函数 - 将 hashCode 无符号右移 16 位,高 16bit 和低 16bit 做异或,最后与 HASH_BITS 相与变成正数,把高低位都利用起来减少哈希冲突,保证散列的均匀性 + 将 hashCode 无符号右移 16 位,高 16bit 和低 16bit 做异或,最后与 HASH_BITS 相与变成正数,与树化节点和转移节点区分,把高低位都利用起来减少哈希冲突,保证散列的均匀性 ```java static final int spread(int h) { @@ -9935,6 +10554,7 @@ public V put(K key, V value) { if (check >= 0) { // tab 表示map.table,nt 表示map.nextTable,n 表示map.table数组的长度,sc 表示sizeCtl的临时值 Node[] tab, nt; int n, sc; + // 条件一:true 说明当前 sizeCtl 为一个负数,表示正在扩容中,或者 sizeCtl 是一个正数,表示扩容阈值 // false 表示哈希表的数据的数量没达到扩容条件 // 条件二:条件一为 true 进入条件二,判断当前 table 数组是否初始化了 @@ -9961,7 +10581,7 @@ public V put(K key, V value) { //【协助扩容线程】,持有nextTable参数 transfer(tab, nt); } - // 条件成立表示当前线程是触发扩容的第一个线程 + // 条件成立表示当前线程是触发扩容的第一个线程,【+ 2】 // 1000 0000 0001 1011 0000 0000 0000 0000 +2 => 1000 0000 0001 1011 0000 0000 0000 0010 else if (U.compareAndSwapInt(this, SIZECTL, sc,(rs << RESIZE_STAMP_SHIFT) + 2)) //【触发扩容条件的线程】,不持有 nextTable,自己新建 nextTable @@ -10019,7 +10639,7 @@ public V put(K key, V value) { //stride 表示分配给线程任务的步长,认为是 16 if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) stride = MIN_TRANSFER_STRIDE; - // 条件成立表示当前线程为触发本次扩容的线程,需要做一些扩容准备工作,【协助线程不做这一步】 + // 如果当前线程为触发本次扩容的线程,需要做一些扩容准备工作,【协助线程不做这一步】 if (nextTab == null) { try { // 创建一个容量为之前二倍的 table 数组 @@ -10049,12 +10669,12 @@ public V put(K key, V value) { // f 桶位的头结点,fh 是头节点的哈希值 Node f; int fh; - //给当前线程【分配任务区间】 + // 给当前线程【分配任务区间】 while (advance) { - //分配任务的开始下标,分配任务的结束下标 + // 分配任务的开始下标,分配任务的结束下标 int nextIndex, nextBound; - // 条件一:true 说明当前的迁移任务尚未完成,--i 就让当前线程处理下一个 桶位 - // false 说明线程已经完成或者还未分配 + + // --i 就让当前线程处理下一个,true 说明当前的迁移任务尚未完成,false 说明线程已经完成或者还未分配 if (--i >= bound || finishing) advance = false; // 迁移的开始下标,小于0说明没有区间需要迁移了,设置当前线程的 i 变量为 -1 跳出循环 @@ -10067,13 +10687,12 @@ public V put(K key, V value) { else if (U.compareAndSwapInt (this, TRANSFERINDEX, nextIndex, //判断剩余的区间是否还够 一个步长的范围,,不够就全部分配 - nextBound = (nextIndex > stride ? - nextIndex - stride : 0))) { + nextBound = (nextIndex > stride ? nextIndex - stride : 0))) { // 当前线程的结束下标 bound = nextBound; // 当前线程的开始下标 i = nextIndex - 1; - //任务分配结束,跳出循环执行迁移操作 + // 任务分配结束,跳出循环执行迁移操作 advance = false; } } @@ -10114,6 +10733,8 @@ public V put(K key, V value) { synchronized (f) { // 二次检查,防止头节点已经被修改了,因为这里才是线程安全的访问 if (tabAt(tab, i) == f) { + // 【迁移数据的逻辑,和 HashMap 相似】 + // ln 表示低位链表引用 // hn 表示高位链表引用 Node ln, hn; @@ -10208,7 +10829,7 @@ public V put(K key, V value) { } ``` - 链表处理的 LastRun 机制: + 链表处理的 LastRun 机制,**可以减少节点的创建**: ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentHashMap-LastRun机制.png) @@ -10255,21 +10876,20 @@ ConcurrentHashMap 使用 get() 方法获取指定 key 的数据 ```java public V get(Object key) { - // tab 引用哈希表的数组,e 是当前元素,p 是目标节点,n 是数组长度,eh 是当前元素哈希,k 是当前节点的值 Node[] tab; Node e, p; int n, eh; K ek; - // 扰动运算 + // 扰动运算,获取 key 的哈希值 int h = spread(key.hashCode()); // 判断当前哈希表的数组是否初始化 if ((tab = table) != null && (n = tab.length) > 0 && // 如果 table 已经初始化,进行【哈希寻址】,映射到数组对应索引处,获取该索引处的头节点 (e = tabAt(tab, (n - 1) & h)) != null) { - //对比头结点 hash 与查询 key 的 hash 是否一致 + // 对比头结点 hash 与查询 key 的 hash 是否一致 if ((eh = e.hash) == h) { // 进行值的判断,如果成功就说明当前节点就是要查询的节点,直接返回 if ((ek = e.key) == key || (ek != null && key.equals(ek))) return e.val; } - // 哈希值小于0说明是红黑树节点或者是正在扩容的 fwd 节点 + // 当前槽位的哈希值小于0说明是红黑树节点或者是正在扩容的 fwd 节点 else if (eh < 0) return (p = e.find(h, key)) != null ? p.val : null; // 当前桶位已经形成链表,迭代查找 @@ -10282,7 +10902,7 @@ ConcurrentHashMap 使用 get() 方法获取指定 key 的数据 return null; } ``` - + * ForwardingNode#find ```java @@ -10347,7 +10967,7 @@ ConcurrentHashMap 使用 get() 方法获取指定 key 的数据 } ``` -* replaceNode():替代指定的元素 +* replaceNode():替代指定的元素,会协助扩容,增删改都会协助扩容,只有查询操作不会 ```java final V replaceNode(Object key, V value, Object cv) { @@ -10357,6 +10977,7 @@ ConcurrentHashMap 使用 get() 方法获取指定 key 的数据 for (Node[] tab = table;;) { // f 表示桶位的头节点,n 表示当前 table 数组长度,i 表示 hash 映射的数组下标,fh 表示头节点的哈希值 Node f; int n, i, fh; + //【CASE1】:table 还未初始化或者哈希寻址的数组索引处为 null,直接结束自旋,返回 null if (tab == null || (n = tab.length) == 0 || (f = tabAt(tab, i = (n - 1) & hash)) == null) @@ -10430,7 +11051,7 @@ ConcurrentHashMap 使用 get() 方法获取指定 key 的数据 //其他线程修改过桶位头结点时,当前线程 sync 头结点锁错对象,validated 为 false,会进入下次 for 自旋 if (validated) { if (oldVal != null) { - //替换的值为 null,说明当前是一次删除操作,更新当前元素个数计数器 + // 替换的值为 null,说明当前是一次删除操作,更新当前元素个数计数器 if (value == null) addCount(-1L, -1); return oldVal; @@ -10442,7 +11063,7 @@ ConcurrentHashMap 使用 get() 方法获取指定 key 的数据 return null; } ``` - + 参考视频:https://space.bilibili.com/457326371/ diff --git a/SSM.md b/SSM.md index dfbe8d3..b37ed61 100644 --- a/SSM.md +++ b/SSM.md @@ -139,7 +139,7 @@ org.apache.ibatis.session.SqlSession:构建者对象接口,用于执行 SQL ### 核心配置 -核心配置文件包含了 MyBatis 最核心的设置和属性信息,如数据库的连接、事务、连接池信息等。放在src目录下 +核心配置文件包含了 MyBatis 最核心的设置和属性信息,如数据库的连接、事务、连接池信息等 命名:MyBatisConfig.xml @@ -3039,12 +3039,12 @@ UserService userService = (UserService)ctx.getBean("userService3"); ApplicationContext 子类相关API: -| 方法 | 说明 | -| ------------------------------------------------- | -------------------------------------------- | -| String[] getBeanDefinitionNames() | 获取 Spring容器中定义的所有 JavaBean 的名称 | -| BeanDefinition getBeanDefinition(String beanName) | 返回给定bean名称的BeanDefinition | -| String[] getBeanNamesForType(Class type) | 获取Spring容器中指定类型的所有JavaBean的名称 | -| Environment getEnvironment() | 获取与此组件关联的环境 | +| 方法 | 说明 | +| ------------------------------------------------- | ------------------------------------------------ | +| String[] getBeanDefinitionNames() | 获取 Spring 容器中定义的所有 JavaBean 的名称 | +| BeanDefinition getBeanDefinition(String beanName) | 返回给定 bean 名称的 BeanDefinition | +| String[] getBeanNamesForType(Class type) | 获取 Spring 容器中指定类型的所有 JavaBean 的名称 | +| Environment getEnvironment() | 获取与此组件关联的环境 | @@ -4927,7 +4927,7 @@ public interface ApplicationListener * 写一个监听器(ApplicationListener实现类)来监听某个事件(ApplicationEvent及其子类) * 把监听器加入到容器 @Component * 只要容器中有相关事件的发布,就能监听到这个事件; - * ContextRefreshedEvent:容器刷新完成(所有bean都完全创建)会发布这个事件 + * ContextRefreshedEvent:容器刷新完成(所有 bean 都完全创建)会发布这个事件 * ContextClosedEvent:关闭容器会发布这个事件 * 发布一个事件:`applicationContext.publishEvent()` @@ -4951,39 +4951,31 @@ public class MyApplicationListener implements ApplicationListener - - - - - - - - - - - - - - - - - - ``` +```xml + + + + + + + + + + + + + + + + + + +``` * aop:advice 与 aop:advisor 区别 * aop:advice 配置的通知类可以是普通 java 对象,不实现接口,也不使用继承关系 @@ -7001,8 +6995,17 @@ TransactionStatus 此接口定义了事务在执行过程中某个时间点上 - ThrowsAdvice +pom.xml 文件引入依赖: + +```xml + + org.springframework + spring-tx + 5.1.9.RELEASE + +``` + -方法调用:`AbstractAspectJAdvice#invokeAdviceMethod(org.aspectj.weaver.tools.JoinPointMatch, java.lang.Object, java.lang.Throwable)` @@ -7107,6 +7110,10 @@ TransactionStatus 此接口定义了事务在执行过程中某个时间点上 +*** + + + ###### 纯注解 名称:@EnableTransactionManagement @@ -7186,11 +7193,12 @@ public void addAccount{} * 情况 1:确认创建的 mysql 数据库表引擎是 InnoDB,MyISAM 不支持事务 * 情况 2:注解到 protected,private 方法上事务不生效,但不会报错 + 原因:理论上而言,不用 public 修饰,也可以用 aop 实现事务的功能,但是方法私有化让其他业务无法调用 - + AopUtils.canApply:`methodMatcher.matches(method, targetClass) --true--> return true` `TransactionAttributeSourcePointcut.matches()` ,AbstractFallbackTransactionAttributeSource 中 getTransactionAttribute 方法调用了其本身的 computeTransactionAttribute 方法,当加了事务注解的方法不是 public 时,该方法直接返回 null,所以造成增强不匹配 - + ```java private TransactionAttribute computeTransactionAttribute(Method method, Class targetClass) { // Don't allow no-public methods as required. @@ -7199,7 +7207,7 @@ public void addAccount{} } } ``` - + * 情况 3:注解所在的类没有被加载成 Bean * 情况 4:在业务层捕捉异常后未向上抛出,事务不生效 @@ -7212,7 +7220,7 @@ public void addAccount{} * 情况 6:Spring 的事务传播策略在**内部方法**调用时将不起作用,在一个 Service 内部,事务方法之间的嵌套调用,普通方法和事务方法之间的嵌套调用,都不会开启新的事务,事务注解要加到调用方法上才生效 - 原因:Spring 的事务都是使用 AOP 代理的模式,动态代理最终是要调用原始对象,而原始对象在去调用方法时是不会再触发代理,就是一个方法调用**本对象**的另一个方法,没有通过代理类直接调用,所以事务也就无法生效 + 原因:Spring 的事务都是使用 AOP 代理的模式,动态代理最终是要调用原始对象,而原始对象在去调用方法时是不会再触发代理,就是**一个方法调用本对象的另一个方法**,没有通过代理类直接调用,所以事务也就无法生效 ```java @Transactional @@ -7503,6 +7511,7 @@ AbstractApplicationContext.refresh(): * `ignoreDependencyInterface()`:设置忽略自动装配的接口,bean 内部的这些类型的字段 不参与依赖注入 * `registerResolvableDependency()`:注册一些类型依赖关系 * `addBeanPostProcessor()`:将配置的监听者注册到容器中,当前 bean 实现 ApplicationListener 接口就是**监听器事件** + * `beanFactory.registerSingleton()`:添加一些系统信息 * postProcessBeanFactory(beanFactory):BeanFactory 准备工作完成后进行的后置处理工作,通过重写这个方法来在 BeanFactory 创建并预准备完成以后做进一步的设置 @@ -7694,7 +7703,7 @@ AbstractBeanFactory.doGetBean():获取 Bean,context.getBean() 追踪到此 * `if(isPrototypeCurrentlyInCreation(beanName))`:检查 bean 是否在原型(Prototype)正在被创建的集合中,如果是就报错,说明产生了循环依赖,**原型模式解决不了循环依赖** - 原因:先加载 A,把 A 加入集合,A 依赖 B 去加载 B,B 又依赖 A,发现 A 在正在创建集合中,产生循环依赖 + 原因:先加载 A,把 A 加入集合,A 依赖 B 去加载 B,B 又依赖 A,去加载 A,发现 A 在正在创建集合中,产生循环依赖 * `markBeanAsCreated(beanName)`:把 bean 标记为已经创建 @@ -8029,7 +8038,7 @@ AbstractAutowireCapableBeanFactory.**doCreateBean**(beanName, RootBeanDefinition -* `if (earlySingletonExposure)`:是否循序提前引用 +* `if (earlySingletonExposure)`:是否允许提前引用 `earlySingletonReference = getSingleton(beanName, false)`:**从二级缓存获取实例**,放入一级缓存是在 doGetBean 中的sharedInstance = getSingleton() 方法中,此时在 createBean 的逻辑还没有返回。 @@ -8103,7 +8112,7 @@ AbstractAutowireCapableBeanFactory.createBeanInstance(beanName, RootBeanDefiniti * `return instantiateBean(beanName, mbd)`:**无参构造方法通过反射创建实例** -* `ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName)`:**@Autowired 注解**配置在构造方法上,对应的后置处理器AutowiredAnnotationBeanPostProcessor 逻辑 +* `ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName)`:**@Autowired 注解**配置在构造方法上,对应的后置处理器 AutowiredAnnotationBeanPostProcessor 逻辑 * 配置了 lookup 的相关逻辑 @@ -8245,7 +8254,7 @@ AbstractAutowireCapableBeanFactory.createBeanInstance(beanName, RootBeanDefiniti `BeanUtils.instantiateClass(constructorToUse)`:调用 `java.lang.reflect.Constructor.newInstance()` 实例化 - * `instantiateWithMethodInjection(bd, beanName, owner)`:有方法重写采用 CGLIB 实例化 + * `instantiateWithMethodInjection(bd, beanName, owner)`:**有方法重写采用 CGLIB 实例化** * `BeanWrapper bw = new BeanWrapperImpl(beanInstance)`:包装成 BeanWrapper 类型的对象 @@ -8265,9 +8274,9 @@ AbstractAutowireCapableBeanFactory.createBeanInstance(beanName, RootBeanDefiniti Spring 循环依赖有四种: -* DependsOn 依赖加载【无法解决】 -* 原型模式循环依赖【无法解决】 -* 单例 Bean 循环依赖:构造参数产生依赖【无法解决】 +* DependsOn 依赖加载【无法解决】(两种 Map) +* 原型模式循环依赖【无法解决】(正在创建集合) +* 单例 Bean 循环依赖:构造参数产生依赖【无法解决】(正在创建集合) * 单例 Bean 循环依赖:setter 产生依赖【可以解决】 解决循环依赖:提前引用,提前暴露创建中的 Bean @@ -8297,13 +8306,13 @@ private final Map> singletonFactories = new HashMap<>(1 * 三级缓存一定会创建提前引用吗? - * 出现循环依赖就会去三级缓存获取提前引用,不出现就不会 - * 如果当前有增强方法,就创建代理对象放入二级缓存,如果没有代理对象就返回 createBeanInstance 创建的实例 + * 出现循环依赖就会去三级缓存获取提前引用,不出现就不会,走正常的逻辑,创建完成直接放入一级缓存 + * 存在循环依赖,就创建代理对象放入二级缓存,如果没有增强方法就返回 createBeanInstance 创建的实例,因为 addSingletonFactory 参数中传入了实例化的 Bean,在 singletonFactory.getObject() 中返回给 singletonObject * wrapIfNecessary 一定创建代理对象吗?(AOP 动态代理部分有源码解析) * 存在增强器会创建动态代理,不需要增强就不需要创建动态代理对象 - * 不创建就会把最原始的实例化的Bean放到二级缓存,因为 addSingletonFactory 参数中传入了实例化的Bean,在singletonFactory.getObject() 中返回给 singletonObject,放入二级缓存 + * 存在循环依赖会提前增强,初始化后不需要增强 * 什么时候将 Bean 的引用提前暴露给第三级缓存的 ObjectFactory 持有? @@ -8385,10 +8394,10 @@ private final Map> singletonFactories = new HashMap<>(1 ```java public Object getEarlyBeanReference(Object bean, String beanName) { Object cacheKey = getCacheKey(bean.getClass(), beanName); - this.earlyProxyReferences.put(cacheKey, bean); //向提前引用代理池 earlyProxyReferences 中添加该Bean,防止对象被重新代理 + this.earlyProxyReferences.put(cacheKey, bean); + //创建代理对象,createProxy return wrapIfNecessary(bean, beanName, cacheKey); - //创建代理对象,createProxy } ``` @@ -8470,7 +8479,7 @@ protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) return bean; } - // 查找适合当前 bean 实例 Class 的通知(下一节详解) + // 查找适合当前 bean 实例的增强方法(下一节详解) Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null); //条件成立说明上面方法查询到适合当前class的通知 if (specificInterceptors != DO_NOT_PROXY) { @@ -8501,7 +8510,7 @@ protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) ##### 获取通知 -AbstractAdvisorAutoProxyCreator.getAdvicesAndAdvisorsForBean():查找适合当前实例的增强 +AbstractAdvisorAutoProxyCreator.getAdvicesAndAdvisorsForBean():查找适合当前实例的增强,并进行排序 ```java protected Object[] getAdvicesAndAdvisorsForBean(Class beanClass, String beanName, @Nullable TargetSource targetSource) { @@ -8520,9 +8529,9 @@ AbstractAdvisorAutoProxyCreator.findEligibleAdvisors(): * `candidateAdvisors = findCandidateAdvisors()`:**获取当前容器内可以使用(所有)的 advisor**,调用的是 AnnotationAwareAspectJAutoProxyCreator 类的方法 - * `advisors = super.findCandidateAdvisors()`:查询出所有 Advisor 类型 + * `advisors = super.findCandidateAdvisors()`:查询出 XML 配置的所有 Advisor 类型 - * `advisorNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors()`:通过 BF 查询出来 BD 配置的 class 中 是 **Advisor 子类的 BeanName** + * `advisorNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors()`:通过 BF 查询出来 BD 配置的 class 中 是 Advisor 子类的 BeanName * `advisors.add()`:使用 Spring 容器获取当前这个 Advisor 类型的实例 * `advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors())`:获取添加 @Aspect 注解类中的 Advisor @@ -8557,11 +8566,11 @@ AbstractAdvisorAutoProxyCreator.findEligibleAdvisors(): * `return advisors`:返回 Advisor 列表 -* `eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName)`:**选出适合当前类型的增强** +* `eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName)`:**选出适合当前类的增强** * `if (candidateAdvisors.isEmpty())`:条件成立说明当前 Spring 没有可以操作的 Advisor - * `List eligibleAdvisors = new ArrayList<>()`:匹配当前 clazz 的 Advisors 信息 + * `List eligibleAdvisors = new ArrayList<>()`:存放匹配当前 beanClass 的 Advisors 信息 * `for (Advisor candidate : candidateAdvisors)`:遍历所有的 AdvisorIntroduction @@ -8577,9 +8586,9 @@ AbstractAdvisorAutoProxyCreator.findEligibleAdvisors(): * `methodMatcher = pc.getMethodMatcher()`:**获取方法匹配器** * `Set> classes`:保存目标对象 class 和目标对象父类超类的接口和自身实现的接口 * `if (!Proxy.isProxyClass(targetClass))`:判断当前实例是不是代理类,确保 class 内存储的数据包括目标对象的class 而不是代理类的 class - * `for (Class clazz : classes)`:检查目标 class 和上级接口的所有方法,查看是否会被方法匹配器匹配,如果有一个方法匹配成功,就说明目标对象 AOP 代理需要增强 + * `for (Class clazz : classes)`:**检查目标 class 和上级接口的所有方法,查看是否会被方法匹配器匹配**,如果有一个方法匹配成功,就说明目标对象 AOP 代理需要增强 * `specificMethod = AopUtils.getMostSpecificMethod(method, targetClass)`:方法可能是接口的,判断当前类有没有该方法 - * `return (specificMethod != method && matchesMethod(specificMethod))`:类和方法的匹配,不包括参数(静态匹配) + * `return (specificMethod != method && matchesMethod(specificMethod))`:**类和方法的匹配**,不包括参数,就是静态匹配 * `extendAdvisors(eligibleAdvisors)`:在 eligibleAdvisors 列表的索引 0 的位置添加 DefaultPointcutAdvisor,**封装了 ExposeInvocationInterceptor 拦截器** @@ -8636,9 +8645,12 @@ AbstractAutoProxyCreator.createProxy():根据增强方法创建代理对象 * `proxyFactory.copyFrom(this)`:填充一些信息到 proxyFactory -* `if (!proxyFactory.isProxyTargetClass())`:条件成立说明没有配置修改过 proxyTargetClass 为 true +* `if (!proxyFactory.isProxyTargetClass())`:条件成立说明没有配置修改过 **proxyTargetClass** 为 true,两种配置方法: + + * ` ` + * `@EnableAspectJAutoProxy(proxyTargetClass = true)` - `if (shouldProxyTargetClass(beanClass, beanName))`:如果 **bd 内有 preserveTargetClass = true** ,那么这个 bd 对应的 class 创建代理时必须使用 CGLIB,条件成立设置 proxyTargetClass 为 true + `if (shouldProxyTargetClass(beanClass, beanName))`:如果 bd 内有 preserveTargetClass = true ,那么这个 bd 对应的 class 创建代理时必须使用 CGLIB,条件成立设置 proxyTargetClass 为 true `evaluateProxyInterfaces(beanClass, proxyFactory)`:**根据目标类判定是否可以使用 JDK 动态代理** @@ -8667,11 +8679,7 @@ AbstractAutoProxyCreator.createProxy():根据增强方法创建代理对象 ```java public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { - //条件一:积极的优化 - //条件二:为 true 代表强制使用 CGLIB 动态代理, - //两种配置方法: - // - // @EnableAspectJAutoProxy(proxyTargetClass = true) + //条件二为 true 代表强制使用 CGLIB 动态代理, if (config.isOptimize() || config.isProxyTargetClass() || //条件三:被代理对象没有实现任何接口或者只实现了 SpringProxy 接口,只能使用 CGLIB 动态代理 hasNoUserSuppliedProxyInterfaces(config)) { @@ -8679,7 +8687,7 @@ AbstractAutoProxyCreator.createProxy():根据增强方法创建代理对象 if (targetClass == null) { throw new AopConfigException(""); } - // 条件成立说明 target 是接口或者是已经被代理过的类型,只能使用 JDK 动态代理 + // 条件成立说明 target 【是接口或者是已经被代理过的类型】,只能使用 JDK 动态代理 if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { return new JdkDynamicAopProxy(config); // 使用 JDK 动态代理 } @@ -8690,48 +8698,50 @@ AbstractAutoProxyCreator.createProxy():根据增强方法创建代理对象 } } ``` - + JdkDynamicAopProxy.getProxy(java.lang.ClassLoader):获取 JDK 的代理对象 - + ```java - public JdkDynamicAopProxy(AdvisedSupport config) throws AopConfigException { - // 配置类封装到 JdkDynamicAopProxy 属性中 - this.advised = config; - } - public Object getProxy(@Nullable ClassLoader classLoader) { - // 获取需要代理的接口数组 - Class[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true); - // 查找当前所有的需要代理的接口,看是否有 equals 方法和 hashcode 方法,如果有就做一个标记 - findDefinedEqualsAndHashCodeMethods(proxiedInterfaces); - // classLoader:类加载器 proxiedInterfaces:生成的代理类,需要实现的接口集合 - // this JdkDynamicAopProxy 实现了 InvocationHandler - // 该方法最终返回一个代理类对象 - return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this); - } + public JdkDynamicAopProxy(AdvisedSupport config) throws AopConfigException { + // 配置类封装到 JdkDynamicAopProxy.advised 属性中 + this.advised = config; + } + public Object getProxy(@Nullable ClassLoader classLoader) { + // 获取需要代理的接口数组 + Class[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true); + + // 查找当前所有的需要代理的接口,看是否有 equals 方法和 hashcode 方法,如果有就做一个标记 + findDefinedEqualsAndHashCodeMethods(proxiedInterfaces); + + // 该方法最终返回一个代理类对象 + return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this); + // classLoader:类加载器 proxiedInterfaces:生成的代理类,需要实现的接口集合 + // this JdkDynamicAopProxy 实现了 InvocationHandler + } ``` - - AopProxyUtils.completeProxiedInterfaces(this.advised, true):获取代理的接口数组 - + + AopProxyUtils.completeProxiedInterfaces(this.advised, true):获取代理的接口数组,并添加 SpringProxy 接口 + * `specifiedInterfaces = advised.getProxiedInterfaces()`:从 ProxyFactory 中拿到所有的 target 提取出来的接口 - * `if (specifiedInterfaces.length == 0)`:如果没有实现接口,检查当前 target 是不是接口或者已经是代理类,封装到 ProxyFactory 的 interfaces 集合中 - - * ` addSpringProxy = !advised.isInterfaceProxied(SpringProxy.class)`:判断目标对象所有接口中是否有 **SpringProxy** 接口,没有的话需要添加,这个接口**标识这个代理类型是 Spring 管理的** - * `addAdvised = !advised.isOpaque() && !advised.isInterfaceProxied(Advised.class)`:判断目标对象的所有接口,是否已经有 Advised 接口 - * ` addDecoratingProxy = (decoratingProxy && !advised.isInterfaceProxied(DecoratingProxy.class))`:判断目标对象的所有接口,是否已经有 DecoratingProxy 接口 - * `int nonUserIfcCount = 0`:非用户自己定义的接口数量,接下来要添加上面的三个接口了 - * `proxiedInterfaces = new Class[specifiedInterfaces.length + nonUserIfcCount]`:创建一个新的 class 数组,长度是原目标对象提取出来的接口数量和 Spring 追加的数量,然后进行 **System.arraycopy 拷贝到新数组中** - * `int index = specifiedInterfaces.length`:获取原目标对象提取出来的接口数量,当作 index - * `if(addSpringProxy)`:根据上面三个布尔值把接口添加到新数组中 - * `return proxiedInterfaces`:返回追加后的接口集合 - + * `if (specifiedInterfaces.length == 0)`:如果没有实现接口,检查当前 target 是不是接口或者已经是代理类,封装到 ProxyFactory 的 interfaces 集合中 + + * ` addSpringProxy = !advised.isInterfaceProxied(SpringProxy.class)`:判断目标对象所有接口中是否有 SpringProxy 接口,没有的话需要添加,这个接口**标识这个代理类型是 Spring 管理的** + * `addAdvised = !advised.isOpaque() && !advised.isInterfaceProxied(Advised.class)`:判断目标对象的所有接口,是否已经有 Advised 接口 + * ` addDecoratingProxy = (decoratingProxy && !advised.isInterfaceProxied(DecoratingProxy.class))`:判断目标对象的所有接口,是否已经有 DecoratingProxy 接口 + * `int nonUserIfcCount = 0`:非用户自己定义的接口数量,接下来要添加上面的三个接口了 + * `proxiedInterfaces = new Class[specifiedInterfaces.length + nonUserIfcCount]`:创建一个新的 class 数组,长度是原目标对象提取出来的接口数量和 Spring 追加的数量,然后进行 **System.arraycopy 拷贝到新数组中** + * `int index = specifiedInterfaces.length`:获取原目标对象提取出来的接口数量,当作 index + * `if(addSpringProxy)`:根据上面三个布尔值把接口添加到新数组中 + * `return proxiedInterfaces`:返回追加后的接口集合 + JdkDynamicAopProxy.findDefinedEqualsAndHashCodeMethods():查找在任何定义在接口中的 equals 和 hashCode 方法 - + * `for (Class proxiedInterface : proxiedInterfaces)`:遍历所有的接口 - * ` Method[] methods = proxiedInterface.getDeclaredMethods()`:获取接口中的所有方法 - * `for (Method method : methods)`:遍历所有的方法 - * `if (AopUtils.isEqualsMethod(method))`:当前方法是 equals 方法,把 equalsDefined 置为 true - * `if (AopUtils.isHashCodeMethod(method))`:当前方法是 hashCode 方法,把 hashCodeDefined 置为 true - + * ` Method[] methods = proxiedInterface.getDeclaredMethods()`:获取接口中的所有方法 + * `for (Method method : methods)`:遍历所有的方法 + * `if (AopUtils.isEqualsMethod(method))`:当前方法是 equals 方法,把 equalsDefined 置为 true + * `if (AopUtils.isHashCodeMethod(method))`:当前方法是 hashCode 方法,把 hashCodeDefined 置为 true + * `if (this.equalsDefined && this.hashCodeDefined)`:如果有一个接口中有这两种方法,直接返回 @@ -8747,105 +8757,141 @@ main() 函数中调用用户方法,会进入该逻辑 JdkDynamicAopProxy 类中的 invoke 方法是真正执行代理方法 ```java -public Object invoke(Object proxy, Method method, Object[] args) -//proxy:代理对象 -//method:目标对象的方法 -//args:目标对象方法对应的参数 -``` - -* `targetSource = this.advised.targetSource`:advised 就是初始化 JdkDynamicAopProxy 对象时传入的变量 - -* `if (!this.equalsDefined && AopUtils.isEqualsMethod(method))`:条件成立说明代理类实现的接口没有定义 equals 方法,并且当前 method 调用 equals 方法,就调用 JdkDynamicAopProxy 提供的 equals 方法 - -* `if (this.advised.exposeProxy)`:需不需要暴露当前代理对象到 AOP 上下文内,true 暴露 - - `oldProxy = AopContext.setCurrentProxy(proxy)`:把代理对象设置到上下文环境 +//proxy:代理对象,method:目标对象的方法,args:目标对象方法对应的参数 +public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + Object oldProxy = null; + boolean setProxyContext = false; - `setProxyContext = true`:允许提前引用 + // advised 就是初始化 JdkDynamicAopProxy 对象时传入的变量 + TargetSource targetSource = this.advised.targetSource; + Object target = null; -* `target = targetSource.getTarget()`:根据 targetSource 获取真正的代理对象 + try { + // 条件成立说明代理类实现的接口没有定义 equals 方法,并且当前 method 调用 equals 方法, + // 就调用 JdkDynamicAopProxy 提供的 equals 方法 + if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) { + return equals(args[0]); + } //..... + + Object retVal; + // 需不需要暴露当前代理对象到 AOP 上下文内 + if (this.advised.exposeProxy) { + // 把代理对象设置到上下文环境 + oldProxy = AopContext.setCurrentProxy(proxy); + setProxyContext = true; + } -* `chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass)`:**查找适合该方法的增强**,首先从缓存中查找,查找不到进入主方法 + // 根据 targetSource 获取真正的代理对象 + target = targetSource.getTarget(); + Class targetClass = (target != null ? target.getClass() : null); - * `AdvisorAdapterRegistry registry = GlobalAdvisorAdapterRegistry.getInstance()`:向容器注册适配器,**可以将非 Advisor 类型的增强,包装成为 Advisor,将 Advisor 类型的增强提取出来对应的 MethodInterceptor** + // 查找【适合该方法的增强】,首先从缓存中查找,查找不到进入主方法【下文详解】 + List chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); - * `instance = new DefaultAdvisorAdapterRegistry()`:**该对象向容器中注册了** MethodBeforeAdviceAdapter、AfterReturningAdviceAdapter、ThrowsAdviceAdapter **三个适配器** + // 拦截器链数量是 0 说明当前 method 不需要被增强 + if (chain.isEmpty()) { + Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); + retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse); + } + else { + // 有匹配当前 method 的方法拦截器,要做增强处理,把方法信息封装到方法调用器里 + MethodInvocation invocation = + new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain); + // 【拦截器链驱动方法,下文详解】 + retVal = invocation.proceed(); + } - * `advisors = config.getAdvisors()`:获取 ProxyFactory 内部持有的增强信息 + Class returnType = method.getReturnType(); + if (retVal != null && retVal == target && + returnType != Object.class && returnType.isInstance(proxy) && + !RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) { + // 如果目标方法返回目标对象,这里做个普通替换返回代理对象 + retVal = proxy; + } + + // 返回执行的结果 + return retVal; + } + finally { + if (target != null && !targetSource.isStatic()) { + targetSource.releaseTarget(target); + } + // 如果允许了提前暴露,这里需要设置为初始状态 + if (setProxyContext) { + // 当前代理对象已经完成工作,把原始对象设置回上下文 + AopContext.setCurrentProxy(oldProxy); + } + } +} +``` - * `interceptorList = new ArrayList<>(advisors.length)`:拦截器列表有 5 个,一个 ExposeInvocationInterceptor 和 4 个增强器 +this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass):查找适合该方法的增强,首先从缓存中查找,获取通知时是从全部增强中获取适合当前类的,这里是**从当前类的中获取适合当前方法的增强** - * `actualClass = (targetClass != null ? targetClass : method.getDeclaringClass())`:真实的目标对象类型 +* `AdvisorAdapterRegistry registry = GlobalAdvisorAdapterRegistry.getInstance()`:向容器注册适配器,**可以将非 Advisor 类型的增强,包装成为 Advisor,将 Advisor 类型的增强提取出来对应的 MethodInterceptor** - * `Boolean hasIntroductions = null`:引介增强,不关心 + * `instance = new DefaultAdvisorAdapterRegistry()`:该对象向容器中注册了 MethodBeforeAdviceAdapter、AfterReturningAdviceAdapter、ThrowsAdviceAdapter 三个适配器 - * `for (Advisor advisor : advisors)`:**遍历所有的增强** +* `advisors = config.getAdvisors()`:获取 ProxyFactory 内部持有的增强信息 - * `if (advisor instanceof PointcutAdvisor)`:条件成立说明当前 Advisor 是包含切点信息的,进入匹配逻辑 +* `interceptorList = new ArrayList<>(advisors.length)`:拦截器列表有 5 个,一个 ExposeInvocationInterceptor 和 4 个增强器 - `pointcutAdvisor = (PointcutAdvisor) advisor`:转成可以获取到切点信息的接口 +* `actualClass = (targetClass != null ? targetClass : method.getDeclaringClass())`:真实的目标对象类型 - `if(config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass))`:当前代理被预处理,或者当前被代理的 class 对象匹配当前 Advisor 成功,只是 **class 匹配成功** +* `Boolean hasIntroductions = null`:引介增强,不关心 - * `mm = pointcutAdvisor.getPointcut().getMethodMatcher()`:获取切点的方法匹配器,不考虑引介增强 +* `for (Advisor advisor : advisors)`:**遍历所有的增强** - * `match = mm.matches(method, actualClass)`:**静态匹配成功返回 true,只关注于处理类及其方法,不考虑参数** +* `if (advisor instanceof PointcutAdvisor)`:条件成立说明当前 Advisor 是包含切点信息的,进入匹配逻辑 - * `if (match)`:如果静态切点检查是匹配的,在运行的时候才进行**动态切点检查,会考虑参数匹配**(代表传入了参数)。如果静态匹配失败,直接不需要进行参数匹配,提高了工作效率 + `pointcutAdvisor = (PointcutAdvisor) advisor`:转成可以获取到切点信息的接口 - `interceptors = registry.getInterceptors(advisor)`:提取出 advisor 内持有的拦截器信息 + `if(config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass))`:当前代理被预处理,或者当前被代理的 class 对象匹配当前 Advisor 成功,只是 **class 匹配成功** - * `Advice advice = advisor.getAdvice()`:获取增强方法 + * `mm = pointcutAdvisor.getPointcut().getMethodMatcher()`:获取切点的方法匹配器,不考虑引介增强 - * `if (advice instanceof MethodInterceptor)`:当前 advice 是 MethodInterceptor 直接加入集合 + * `match = mm.matches(method, actualClass)`:**静态匹配成功返回 true,只关注于处理类及其方法,不考虑参数** - * `for (AdvisorAdapter adapter : this.adapters)`:**遍历三个适配器进行匹配**(初始化时创建的),以 MethodBeforeAdviceAdapter 为例 + * `if (match)`:如果静态切点检查是匹配的,在运行的时候才进行**动态切点检查,会考虑参数匹配**(代表传入了参数)。如果静态匹配失败,直接不需要进行参数匹配,提高了工作效率 - `if (adapter.supportsAdvice(advice))`:判断当前 advice 是否是对应的 MethodBeforeAdvice + `interceptors = registry.getInterceptors(advisor)`:提取出 advisor 内持有的拦截器信息 - `interceptors.add(adapter.getInterceptor(advisor))`:是就往拦截器链中添加 advisor - - * `advice = (MethodBeforeAdvice) advisor.getAdvice()`:**获取增强方法** - * `return new MethodBeforeAdviceInterceptor(advice)`:**封装成 MethodInterceptor 方法拦截器返回** + * `Advice advice = advisor.getAdvice()`:获取增强方法 - `interceptorList.add(new InterceptorAndDynamicMethodMatcher(interceptor, mm))`:向拦截器链添加动态匹配器 - - `interceptorList.addAll(Arrays.asList(interceptors))`:将当前 advisor 内部的方法拦截器追加到 interceptorList + * `if (advice instanceof MethodInterceptor)`:当前 advice 是 MethodInterceptor 直接加入集合 - * `interceptors = registry.getInterceptors(advisor)`:进入 else 的逻辑,说明当前 Advisor 匹配全部 class 的全部 method,全部加入到 interceptorList + * `for (AdvisorAdapter adapter : this.adapters)`:**遍历三个适配器进行匹配**(初始化时创建的),以 MethodBeforeAdviceAdapter 为例 - * `return interceptorList`:返回 method 方法的拦截器链 + `if (adapter.supportsAdvice(advice))`:判断当前 advice 是否是对应的 MethodBeforeAdvice -* `if (chain.isEmpty())`:查询出来匹配当前方法的拦截器,**数量是 0 说明当前 method 不需要被增强**,直接调用目标方法 + `interceptors.add(adapter.getInterceptor(advisor))`:条件成立就往拦截器链中添加 advisor - `retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse)`:调用目标对象的目标方法 + * `advice = (MethodBeforeAdvice) advisor.getAdvice()`:**获取增强方法** + * `return new MethodBeforeAdviceInterceptor(advice)`:**封装成 MethodInterceptor 方法拦截器返回** -* `invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain)`:**有匹配当前 method 的方法拦截器,要做增强处理**,把方法信息封装到方法调用器里 + `interceptorList.add(new InterceptorAndDynamicMethodMatcher(interceptor, mm))`:向拦截器链添加动态匹配器 - `retVal = invocation.proceed()`:**核心拦截器链驱动方法** + `interceptorList.addAll(Arrays.asList(interceptors))`:将当前 advisor 内部的方法拦截器追加到 interceptorList - * `if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1)`:条件成立说明方法拦截器全部都已经调用过了(0 - 1 = -1),接下来需要执行目标对象的目标方法 +* `interceptors = registry.getInterceptors(advisor)`:进入 else 的逻辑,说明当前 Advisor 匹配全部 class 的全部 method,全部加入到 interceptorList - `return invokeJoinpoint()`:调用连接点 +* `return interceptorList`:返回 method 方法的拦截器链 - * `this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex)`:**获取下一个方法拦截器** +retVal = invocation.proceed():**拦截器链驱动方法** - * `if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher)`:**需要运行时匹配** +* `if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1)`:条件成立说明方法拦截器全部都已经调用过了(0 - 1 = -1),接下来需要执行目标对象的目标方法 - `if (dm.methodMatcher.matches(this.method, targetClass, this.arguments))`:判断是否匹配成功 + `return invokeJoinpoint()`:调用连接点 - * `return dm.interceptor.invoke(this)`:匹配成功,执行方法 - * `return proceed()`:匹配失败跳过当前拦截器 +* `this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex)`:**获取下一个方法拦截器** - * `return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this)`:**所有的方法拦截器都会执行到该方法,此方法内继续执行 proceed() 完成责任链的驱动,直到最后一个 MethodBeforeAdviceInterceptor 调用前置通知,然后调用 mi.proceed(),发现是最后一个拦截器就直接执行目标方法,return 到上一个拦截器的 mi.proceed() 处,依次返回到责任链的上一个拦截器执行通知方法** +* `if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher)`:需要运行时匹配 -* `retVal = proxy`:如果目标方法返回目标对象,这里做个普通替换返回代理对象 + `if (dm.methodMatcher.matches(this.method, targetClass, this.arguments))`:判断是否匹配成功 -* `if (setProxyContext)`:如果允许了提前暴露,这里需要设置为初始状态 + * `return dm.interceptor.invoke(this)`:匹配成功,执行方法 + * `return proceed()`:匹配失败跳过当前拦截器 - `AopContext.setCurrentProxy(oldProxy)`:当前代理对象已经完成工作,把原始对象设置回上下文 - -* `return retVal`:返回执行的结果 +* `return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this)`:**所有的方法拦截器都会执行到该方法,此方法内继续执行 proceed() 完成责任链的驱动,直到最后一个 MethodBeforeAdviceInterceptor 调用前置通知,然后调用 mi.proceed(),发现是最后一个拦截器就直接执行目标方法,return 到上一个拦截器的 mi.proceed() 处,依次返回到责任链的上一个拦截器执行通知方法** 图示先从上往下建立链,然后从下往上依次执行,责任链模式 @@ -8883,6 +8929,8 @@ public Object invoke(Object proxy, Method method, Object[] args) +参考视频:https://www.bilibili.com/video/BV1gW411W7wy + @@ -8991,13 +9039,15 @@ AutowiredAnnotationBeanPostProcessor 间接实现 InstantiationAwareBeanPostProc * AdviceMode 为 PROXY:导入 AutoProxyRegistrar 和 ProxyTransactionManagementConfiguration(默认) * AdviceMode 为 ASPECTJ:导入 AspectJTransactionManagementConfiguration(与声明式事务无关) -AutoProxyRegistrar:给容器中注册 InfrastructureAdvisorAutoProxyCreator,该类实现了 InstantiationAwareBeanPostProcessor 接口,可以拦截 Spring 的 bean 初始化和实例化前后。利用后置处理器机制拦截 bean 以后包装该 bean 并返回一个代理对象,代理对象中保存所有的拦截器,代理对象执行目标方法,利用拦截器的链式机制依次进入每一个拦截器中进行执行(AOP 原理) +AutoProxyRegistrar:给容器中注册 InfrastructureAdvisorAutoProxyCreator,该类实现了 InstantiationAwareBeanPostProcessor 接口,可以拦截 Spring 的 bean 初始化和实例化前后。**利用后置处理器机制拦截 bean 以后包装该 bean 并返回一个代理对象**,代理对象中保存所有的拦截器,代理对象执行目标方法,利用拦截器的链式机制依次进入每一个拦截器中进行执行(就是 AOP 原理) -ProxyTransactionManagementConfiguration:是一个 Spring 的配置类,注册 BeanFactoryTransactionAttributeSourceAdvisor 事务增强器,利用注解 @Bean 把该类注入到容器中,该增强器有两个字段: +ProxyTransactionManagementConfiguration:是一个 Spring 的事务配置类,注册了三个 Bean: + +* BeanFactoryTransactionAttributeSourceAdvisor:事务增强器,利用注解 @Bean 把该类注入到容器中,该增强器有两个字段: * TransactionAttributeSource:用于解析事务注解的相关信息,比如 @Transactional 注解,该类的真实类型是 AnnotationTransactionAttributeSource,初始化方法中注册了三个**注解解析器**,解析三种类型的事务注解 Spring、JTA、Ejb3 -* TransactionInterceptor:**事务拦截器**,代理对象执行拦截器方法时,会调用 TransactionInterceptor 的 invoke 方法,底层调用TransactionAspectSupport.invokeWithinTransaction(),通过 PlatformTransactionManager **控制着事务的提交和回滚**,所以事务的底层原理就是通过 AOP 动态织入,进行事务开启和提交 +* TransactionInterceptor:**事务拦截器**,代理对象执行拦截器方法时,会调用 TransactionInterceptor 的 invoke 方法,底层调用TransactionAspectSupport.invokeWithinTransaction(),通过 PlatformTransactionManager 控制着事务的提交和回滚,所以事务的底层原理就是通过 AOP 动态织入,进行事务开启和提交 ```java // 创建平台事务管理器对象 @@ -9014,15 +9064,11 @@ ProxyTransactionManagementConfiguration:是一个 Spring 的配置类,注册 * `status = tm.getTransaction(txAttr)`:获取事务状态,方法内通过 doBegin **调用 Connection 的 setAutoCommit 开启事务**,就是 JDBC 原生的方式 - * `prepareTransactionInfo(tm, txAttr, joinpointIdentification, status)`:方法内调用 bindToThread() 方法,利用 ThreadLocal 把当前事务绑定到当前线程 + * `prepareTransactionInfo(tm, txAttr, joinpointIdentification, status)`:方法内调用 bindToThread() 方法,利用 ThreadLocal 把当前事务绑定到当前线程(一个线程对应一个事务) 补充策略模式(Strategy Pattern):**使用不同策略的对象实现不同的行为方式**,策略对象的变化导致行为的变化,事务也是这种模式,每个事务对应一个新的 connection 对象 - -![](https://gitee.com/seazean/images/raw/master/Frame/Spring-图解事务执行流程.jpg) - - - -图片来源:https://blog.csdn.net/weixin_45596022/article/details/113749478 + + @@ -14372,9 +14418,10 @@ SpringApplication#run(String... args): * `this.bootstrapRegistryInitializers.forEach()`:遍历所有的引导器调用 initialize 方法完成初始化设置 * `configureHeadlessProperty()`:让当前应用进入 headless 模式 -* `listeners = getRunListeners(args)`:获取所有 RunListener(运行监听器) +* `listeners = getRunListeners(args)`:**获取所有 RunListener(运行监听器)** + * 去 `META-INF/spring.factories` 文件中找 org.springframework.boot.SpringApplicationRunListener -* `listeners.starting(bootstrapContext, this.mainApplicationClass)`:**遍历所有的运行监听器调用 starting 方法** +* `listeners.starting(bootstrapContext, this.mainApplicationClass)`:遍历所有的运行监听器调用 starting 方法 * `applicationArguments = new DefaultApplicationArguments(args)`:获取所有的命令行参数 @@ -14401,7 +14448,7 @@ SpringApplication#run(String... args): * `sources.addFirst(ATTACHED_PROPERTY_SOURCE_NAME,..)`:把 configurationProperties 重新放入环境信息 -* `configureIgnoreBeanInfo(environment)`:配置忽略的 bean +* `configureIgnoreBeanInfo(environment)`:**配置忽略的 bean** * `printedBanner = printBanner(environment)`:打印 SpringBoot 标志 @@ -14420,7 +14467,7 @@ SpringApplication#run(String... args): * `postProcessApplicationContext(context)`:后置处理流程 * `applyInitializers(context)`:获取所有的**初始化器调用 initialize() 方法**进行初始化 - * `listeners.contextPrepared(context)`:所有的**运行监听器调用 environmentPrepared() 方法**,EventPublishingRunListener 发布事件通知 IOC 容器准备完成 + * `listeners.contextPrepared(context)`:所有的运行监听器调用 environmentPrepared() 方法,EventPublishingRunListener 发布事件通知 IOC 容器准备完成 * `listeners.contextLoaded(context)`:所有的**运行监听器调用 contextLoaded() 方法**,通知 IOC 加载完成 * `refreshContext(context)`:**刷新 IOC 容器** @@ -14517,7 +14564,7 @@ SpringBoot 定义了一套接口规范,这套规范规定 SpringBoot 在启动 * `registry.registerBeanDefinition(BEAN, new BasePackagesBeanDefinition(packageNames))`:存放到容器中 * `new BasePackagesBeanDefinition(packageNames)`:把当前主类所在的包名封装到该对象中 - * @Import(AutoConfigurationImportSelector.class):**首先自动装配的核心类** + * @Import(AutoConfigurationImportSelector.class):**自动装配的核心类** 容器刷新时执行:**invokeBeanFactoryPostProcessors()** → invokeBeanDefinitionRegistryPostProcessors() → postProcessBeanDefinitionRegistry() → processConfigBeanDefinitions() → parse() → process() → processGroupImports() → getImports() → process() → **AutoConfigurationImportSelector#getAutoConfigurationEntry()** @@ -14575,7 +14622,7 @@ SpringBoot 定义了一套接口规范,这套规范规定 SpringBoot 在启动 #### 装配流程 -Spring Boot 通过 `@EnableAutoConfiguration` 开启自动装配,通过 SpringFactoriesLoader 加载 `META-INF/spring.factories` 中的自动配置类实现自动装配,自动配置类其实就是通过 `@Conditional` 注解按需加载的配置类(JVM 类加载机制),想要其生效必须引入 `spring-boot-starter-xxx` 包实现起步依赖 +Spring Boot 通过 `@EnableAutoConfiguration` 开启自动装配,通过 SpringFactoriesLoader 加载 `META-INF/spring.factories` 中的自动配置类实现自动装配,自动配置类其实就是通过 `@Conditional` 注解按需加载的配置类,想要其生效必须引入 `spring-boot-starter-xxx` 包实现起步依赖 * SpringBoot 先加载所有的自动配置类 xxxxxAutoConfiguration * 每个自动配置类进行**条件装配**,默认都会绑定配置文件指定的值(xxxProperties 和配置文件进行了绑定) @@ -14633,7 +14680,7 @@ public class DispatcherServletAutoConfiguration { ``` ```java -//将配置文件中的 spring.mvc 前缀的属性与该类绑定 +// 将配置文件中的 spring.mvc 前缀的属性与该类绑定 @ConfigurationProperties(prefix = "spring.mvc") public class WebMvcProperties { } ``` @@ -15355,7 +15402,7 @@ SpringBoot 嵌入式 Servlet 容器,默认支持的 webServe:Tomcat、Jetty ``` -创建 Web 容器: +Web 应用启动,SpringBoot 导入 Web 场景包 tomcat,创建一个 Web 版的 IOC 容器: * `SpringApplication.run(BootApplication.class, args)`:应用启动 @@ -15387,13 +15434,11 @@ SpringBoot 嵌入式 Servlet 容器,默认支持的 webServe:Tomcat、Jetty * `applicationContextFactory.create(this.webApplicationType)`:根据应用类型创建容器 - * `refreshContext(context)`:容器启动 + * `refreshContext(context)`:容器启动刷新 内嵌容器工作流程: -* Web 应用启动,SpringBoot 导入 Web 场景包 tomcat,创建一个 Web 版的 IOC 容器 ServletWebServerApplicationContext - -- ServletWebServerApplicationContext 容器启动时进入 refresh() 逻辑,Spring 容器启动逻辑中,在实例化非懒加载的单例 Bean 之前有一个方法 **onRefresh()**,留给子类去扩展,该**容器就是重写这个方法创建 WebServer** +- Spring 容器启动逻辑中,在实例化非懒加载的单例 Bean 之前有一个方法 **onRefresh()**,留给子类去扩展,Web 容器就是重写这个方法创建 WebServer ```java protected void onRefresh() { diff --git a/Tool.md b/Tool.md index a82cb45..054384d 100644 --- a/Tool.md +++ b/Tool.md @@ -12,8 +12,8 @@ Git 是分布式版本控制系统(Distributed Version Control System,简称 本地仓库和远程仓库: -* 本地仓库:是在开发人员自己电脑上的Git仓库 -* 远程仓库:是在远程服务器上的Git仓库 +* 本地仓库:是在开发人员自己电脑上的 Git 仓库 +* 远程仓库:是在远程服务器上的 Git 仓库 @@ -103,13 +103,13 @@ GitLab(地址: https://about.gitlab.com/ )是一个用于仓库管理系 * 生成 SSH 公钥步骤 * 设置账户 - * cd ~/.ssh(查看是否生成过SSH公钥)//user目录下 - * 生成 SSH 公钥:`ssh-keygen –t rsa –C "email"` + * cd ~/.ssh(查看是否生成过 SSH 公钥)user 目录下 + * 生成 SSH 公钥:`ssh-keygen -t rsa -C "email"` * -t 指定密钥类型,默认是 rsa ,可以省略 * -C 设置注释文字,比如邮箱 * -f 指定密钥文件存储文件名 * 查看命令: cat ~/.ssh/id_rsa.pub - * 公钥测试命令: ssh -T git@gitee.com + * 公钥测试命令: ssh -T git@github.com From 717556c4352b082633128ecfd6180dd11909aa4a Mon Sep 17 00:00:00 2001 From: Seazean <“imseazean@gmail.com”> Date: Wed, 18 Aug 2021 20:51:46 +0800 Subject: [PATCH 028/168] Update Java Notes --- Java.md | 102 +++++++++++----------- Prog.md | 258 ++++++++++++++++++++++++++++++-------------------------- 2 files changed, 191 insertions(+), 169 deletions(-) diff --git a/Java.md b/Java.md index a4a36f6..d2836ff 100644 --- a/Java.md +++ b/Java.md @@ -2273,14 +2273,13 @@ public class CodeDemo { #### 基本介绍 -Object类是Java中的祖宗类,一个类或者默认继承Object类,或者间接继承Object类,Object类的方法是一切子类都可以直接使用 +Object 类是 Java 中的祖宗类,一个类或者默认继承 Object 类,或者间接继承 Object 类,Object 类的方法是一切子类都可以直接使用 -Object类常用方法: +Object 类常用方法: -* `public String toString()`: - 默认是返回当前对象在堆内存中的地址信息:类的全限名@内存地址,例:Student@735b478; - 直接输出对象名称,默认会调用toString()方法,所以省略toString()不写; - 如果输出对象的内容,需要重写toString()方法,toString方法存在的意义是为了被子类重写 +* `public String toString()`:默认是返回当前对象在堆内存中的地址信息:类的全限名@内存地址,例:Student@735b478; + * 直接输出对象名称,默认会调用 toString() 方法,所以省略 toString() 不写; + * 如果输出对象的内容,需要重写 toString() 方法,toString 方法存在的意义是为了被子类重写 * `public boolean equals(Object o)`:默认是比较两个对象的引用是否相同 * `protected Object clone()`:创建并返回此对象的副本 @@ -2302,7 +2301,7 @@ public boolean equals(Object o) { **面试题**:== 和 equals 的区别 -* == 比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的**地址**是否相同,即是否是指相同一个对象,比较的是真正意义上的指针操作。 +* == 比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的**地址**是否相同,即是否是指相同一个对象,比较的是真正意义上的指针操作 * 重写 equals 方法比较的是两个对象的**内容**是否相等,所有的类都是继承自 java.lang.Object 类,所以适用于所有对象,如果**没有对该方法进行覆盖的话,调用的仍然是 Object 类中的方法,比较两个对象的引用** hashCode 的作用: @@ -2738,12 +2737,9 @@ public class Demo1_25 { #### 不可变好处 -* 可以缓存 hash 值 - String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,只要进行一次计算 -* String Pool 的需要 - 如果一个String对象已经被创建过了,就会从 String Pool 中取得引用,只有 String是不可变的,才可能使用 String Pool -* 安全性 - String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 的那一方以为现在连接的是其它主机,而实际情况却不一定是 +* 可以缓存 hash 值,例如 String 用做 HashMap 的 key,不可变的特性可以使得 hash 值也不可变,只要进行一次计算 +* String Pool 的需要,如果一个 String 对象已经被创建过了,就会从 String Pool 中取得引用,只有 String 是不可变的,才可能使用 String Pool +* 安全性,String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 的那一方以为现在连接的是其它主机,而实际情况却不一定是 * String 不可变性天生具备线程安全,可以在多个线程中安全地使用 * 防止子类继承,破坏 String 的 API 的使用 @@ -5130,7 +5126,7 @@ HashMap继承关系如下图所示: * resize() - 当 HashMap 中的元素个数超过`(数组长度)*loadFactor(负载因子)`或者链表过长时(链表长度 > 8,数组长度 < 64),就会进行数组扩容,创建新的数组,伴随一次重新 hash 分配,并且遍历 hash 表中所有的元素非常耗时,所以要尽量避免 resize + 当 HashMap 中的元素个数超过 `(数组长度)*loadFactor(负载因子)` 或者链表过长时(链表长度 > 8,数组长度 < 64),就会进行数组扩容,创建新的数组,伴随一次重新 hash 分配,并且遍历 hash 表中所有的元素非常耗时,所以要尽量避免 resize 扩容机制为扩容为原来容量的 2 倍: @@ -5147,7 +5143,7 @@ HashMap继承关系如下图所示: } else if (oldThr > 0) // 初始化的threshold赋值给newCap newCap = oldThr; - else { // zero initial threshold signifies using defaults + else { newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } @@ -5161,7 +5157,7 @@ HashMap继承关系如下图所示: ![](https://gitee.com/seazean/images/raw/master/Java/HashMap-resize扩容.png) - 普通节点:把所有节点分成两个链表, + 普通节点:把所有节点分成高低位两个链表,转移到数组 ```java // 遍历所有的节点 @@ -5213,13 +5209,14 @@ HashMap继承关系如下图所示: ​ * remove() - 删除是首先先找到元素的位置,如果是链表就遍历链表找到元素之后删除。如果是用红黑树就遍历树然后找到之后做删除,树小于6的时候要转链表 + 删除是首先先找到元素的位置,如果是链表就遍历链表找到元素之后删除。如果是用红黑树就遍历树然后找到之后做删除,树小于 6 的时候退化为链表 ```java final Node removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node[] tab; Node p; int n, index; - //节点数组tab不为空、数组长度n大于0、根据hash定位到的节点对象p,该节点为树的根节点或链表的首节点)不为空,从该节点p向下遍历,找到那个和key匹配的节点对象 + // 节点数组tab不为空、数组长度n大于0、根据hash定位到的节点对象p, + // 该节点为树的根节点或链表的首节点)不为空,从该节点p向下遍历,找到那个和key匹配的节点对象 if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { Node node = null, e; K k; V v;//临时变量,储存要返回的节点信息 @@ -5267,9 +5264,9 @@ HashMap继承关系如下图所示: return null; } ``` - - + + * get() 1. 通过hash值获取该key映射到的桶 @@ -9626,7 +9623,7 @@ Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所 * 每个方法被执行时,都会在虚拟机栈中创建一个栈帧 stack frame(**一个方法一个栈帧**) -* java虚拟机规范允许 **Java 栈的大小是动态的或者是固定不变的** +* Java 虚拟机规范允许 **Java 栈的大小是动态的或者是固定不变的** * 虚拟机栈是**每个线程私有的**,每个线程只能有一个活动栈帧,对应方法调用到执行完成的整个过程 @@ -9650,7 +9647,7 @@ Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所 * 栈内存分配越大越大,可用的线程数越少(内存越大,每个线程拥有的内存越大) * 方法内的局部变量是否**线程安全**: - * 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的 + * 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的(逃逸分析) * 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全 异常: @@ -10147,10 +10144,10 @@ JVM 是将 TLAB 作为内存分配的首选,但不是所有的对象实例都 #### 逃逸分析 -即时编译(Just-in-time Compilation,JIT)是一种通过在运行时将字节码翻译为机器码,从而改善字节码编译语言性能的技术,在HotSpot实现中有多种选择:C1、C2 和 C1+C2,分别对应 client、server 和分层编译 +即时编译(Just-in-time Compilation,JIT)是一种通过在运行时将字节码翻译为机器码,从而改善性能的技术,在 HotSpot 实现中有多种选择:C1、C2 和 C1+C2,分别对应 client、server 和分层编译 -* C1编译速度快,优化方式比较保守;C2编译速度慢,优化方式比较激进 -* C1+C2在开始阶段采用C1编译,当代码运行到一定热度之后采用G2重新编译 +* C1 编译速度快,优化方式比较保守;C2 编译速度慢,优化方式比较激进 +* C1+C2 在开始阶段采用 C1 编译,当代码运行到一定热度之后采用 C2 重新编译 **逃逸分析**:并不是直接的优化手段,而是一个代码分析,通过动态分析对象的作用域,为其它优化手段如栈上分配、标量替换和同步消除等提供依据,发生逃逸行为的情况有两种:方法逃逸和线程逃逸 @@ -10161,25 +10158,26 @@ JVM 是将 TLAB 作为内存分配的首选,但不是所有的对象实例都 如果不存在逃逸行为,则可以对该对象进行如下优化:同步消除、标量替换和栈上分配 -* **同步消除** +* 同步消除 - 线程同步本身比较耗时,如果确定一个对象不会逃逸出线程,不被其它线程访问到,那对象的读写就不会存在竞争,则可以消除对该对象的**同步锁**,通过`-XX:+EliminateLocks`可以开启同步消除 ( - 号关闭) + 线程同步本身比较耗时,如果确定一个对象不会逃逸出线程,不被其它线程访问到,那对象的读写就不会存在竞争,则可以消除对该对象的**同步锁**,通过 `-XX:+EliminateLocks` 可以开启同步消除 ( - 号关闭) -* **标量替换** +* 标量替换 * 标量替换:如果把一个对象拆散,将其成员变量恢复到基本类型来访问 - * 标量 (scalar) :不可分割的量,如基本数据类型和reference类型 + * 标量 (scalar) :不可分割的量,如基本数据类型和 reference 类型 + 聚合量 (Aggregate):一个数据可以继续分解,对象一般是聚合量 * 如果逃逸分析发现一个对象不会被外部访问,并且该对象可以被拆散,那么经过优化之后,并不直接生成该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替 * 参数设置: `-XX:+EliminateAllocations`:开启标量替换 `-XX:+PrintEliminateAllocations`:查看标量替换情况 -* **栈上分配** +* 栈上分配 - JIT编译器在编译期间根据逃逸分析的结果,如果一个对象没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收,这样就无需GC + JIT 编译器在编译期间根据逃逸分析的结果,如果一个对象没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收,这样就无需 GC - User对象的作用域局限在方法fn中,可以使用标量替换的优化手段在栈上分配对象的成员变量,这样就不会生成User对象,大大减轻GC的压力 + User 对象的作用域局限在方法 fn 中,可以使用标量替换的优化手段在栈上分配对象的成员变量,这样就不会生成 User 对象,大大减轻 GC 的压力 ```java public class JVM { @@ -12166,21 +12164,21 @@ ClassLoader 类常用方法: protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { - //调用当前类加载器的 findLoadedClass(name),检查当前类加载器是否已加载过指定 name 的类 + // 调用当前类加载器的 findLoadedClass(name),检查当前类加载器是否已加载过指定 name 的类 Class c = findLoadedClass(name); - //当前类加载器如果没有加载过 + // 当前类加载器如果没有加载过 if (c == null) { long t0 = System.nanoTime(); try { - //判断当前类加载器是否有父类加载器 + // 判断当前类加载器是否有父类加载器 if (parent != null) { - //如果当前类加载器有父类加载器,则调用父类加载器的 loadClass(name,false) -          //父类加载器的 loadClass 方法,又会检查自己是否已经加载过 + // 如果当前类加载器有父类加载器,则调用父类加载器的 loadClass(name,false) +          // 父类加载器的 loadClass 方法,又会检查自己是否已经加载过 c = parent.loadClass(name, false); } else { - //当前类加载器没有父类加载器,说明当前类加载器是 BootStrapClassLoader -           //则调用 BootStrap ClassLoader 的方法加载类 + // 当前类加载器没有父类加载器,说明当前类加载器是 BootStrapClassLoader +           // 则调用 BootStrap ClassLoader 的方法加载类 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { } @@ -13476,7 +13474,7 @@ javap -v Demo.class:省略 Java 是**半编译半解释型语言**,将解释执行与编译执行二者结合起来进行: -* 解释器:根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行 +* 解释器:根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容翻译为对应平台的本地机器指令执行 * 即时编译器(JIT : Just In Time Compiler):虚拟机运行时将源代码直接编译成**和本地机器平台相关的机器码**后再执行,并存入 Code Cache,下次遇到相同的代码直接执行,效率高 @@ -13489,7 +13487,7 @@ Java 是**半编译半解释型语言**,将解释执行与编译执行二者 HotSpot VM 采用**解释器与即时编译器并存的架构**,解释器和即时编译器能够相互协作,去选择最合适的方式来权衡编译本地代码和直接解释执行代码的时间 -HostSpot JVM的默认执行方式: +HostSpot JVM 的默认执行方式: * 当程序启动后,解释器可以马上发挥作用立即执行,省去编译器编译的时间(解释器存在的**必要性**) * 随着程序运行时间的推移,即时编译器逐渐发挥作用,根据热点探测功能,将有价值的字节码编译为本地机器指令,以换取更高的程序执行效率 @@ -13510,16 +13508,14 @@ HotSpot VM 可以通过 VM 参数设置程序执行方式: #### 热点探测 -热点代码:被 JIT 编译器编译的字节码,根据代码被调用执行的频率而定 - -* 一个被多次调用的方法或者一个循环次数较多的循环体都可以被称之为热点代码 -* 这种编译方式发生在方法的执行过程中,也称为栈上替换,简称 **OSR (On StackReplacement) 编译** +热点代码:被 JIT 编译器编译的字节码,根据代码被调用执行的频率而定,一个被多次调用的方法或者一个循环次数较多的循环体都可以被称之为热点代码 -OSR 替换循环代码体的入口,C1、C2 替换的是方法调用的入口,OSR 编译后会出现方法的整段代码被编译了,但是只有循环体部分才执行编译后的机器码,其他部分仍是解释执行 +热点探测:JIT 编译器在运行时会针热点代码做出深度优化,将其直接编译为对应平台的本地机器指令进行缓存,以提升程序执行性能 -热点探测:JIT 编译器在运行时会针热点代码做出深度优化,将其直接编译为对应平台的本地机器指令进行缓存,以提升 Java 程序的执行性能 +JIT 编译在默认情况是异步进行的,当触发某方法或某代码块的优化时,先将其放入编译队列,然后由编译线程进行编译,编译之后的代码放在 CodeCache 中,通过 `-XX:-BackgroundCompilation` 参数可以关闭异步编译 -CodeCache 用于缓存编译后的机器码、动态生成的代码和本地方法代码 JNI,如果 CodeCache 区域被占满,编译器被停用,字节码将不会编译为机器码,应用程序继续运行,但运行速度会降低一个数量级,严重影响系统性能 +* CodeCache 用于缓存编译后的机器码、动态生成的代码和本地方法代码 JNI +* 如果 CodeCache 区域被占满,编译器被停用,字节码将不会编译为机器码,应用程序继续运行,但运行性能会降低很多 HotSpot VM 采用的热点探测方式是基于计数器的热点探测,为每一个方法都建立 2 个不同类型的计数器:方法调用计数器(Invocation Counter)和回边计数器(BackEdge Counter) @@ -13529,6 +13525,8 @@ HotSpot VM 采用的热点探测方式是基于计数器的热点探测,为每 * 回边计数器:统计一个方法中循环体代码执行的次数,在字节码中控制流向后跳转的指令称为回边 + 如果一个方法中的循环体需要执行多次,可以优化为为栈上替换,简称 OSR (On StackReplacement) 编译,**OSR 替换循环代码体的入口,C1、C2 替换的是方法调用的入口**,OSR 编译后会出现方法的整段代码被编译了,但是只有循环体部分才执行编译后的机器码,其他部分仍是解释执行 + *** @@ -13568,7 +13566,9 @@ C1 编译器会对字节码进行简单可靠的优化,耗时短,以达到 * 内联缓存:是一种加快动态绑定的优化技术(方法调用部分详解) -C2 编译器进行耗时较长的优化以及激进优化,优化的代码执行效率更高,当激进优化的假设不成立时,再退回使用 C1 编译。C2 的优化主要是在全局层面,逃逸分析是优化的基础:标量替换、栈上分配、同步消除 +C2 编译器进行耗时较长的优化以及激进优化,优化的代码执行效率更高,当激进优化的假设不成立时,再退回使用 C1 编译,这也是分层编译比直接使用 C2 逃逸分析进行编译的性能低,也会使用分层编译的原因 + +C2 的优化主要是在全局层面,逃逸分析是优化的基础:标量替换、栈上分配、同步消除 VM 参数设置: @@ -13592,6 +13592,10 @@ VM 参数设置: +参考文章:https://www.jianshu.com/p/20bd2e9b1f03 + + + *** diff --git a/Prog.md b/Prog.md index 613605f..eb3c05a 100644 --- a/Prog.md +++ b/Prog.md @@ -1263,7 +1263,7 @@ public class SpinLock { 锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除 -锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除(同步消除:JVM内存分配) +锁消除主要是通过**逃逸分析**来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除(同步消除:JVM 内存分配) @@ -3610,6 +3610,8 @@ String 类也是不可变的,该类和类中所有属性都是 final 的 * 类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性保 +* 无写入方法(set)确保外部不能对内部属性进行修改 + * 属性用 final 修饰保证了该属性是只读的,不能修改 ```java @@ -3620,8 +3622,10 @@ String 类也是不可变的,该类和类中所有属性都是 final 的 //.... } ``` + +* 更改 String 类数据时,会构造新字符串对象,生成新的 char[] value,通过创建副本对象来避免共享的方式称之为**保护性拷贝** -更改 String 类数据时,会构造新字符串对象,生成新的 char[] value,这种通过创建副本对象来避免共享的方式称之为**保护性拷贝(defensive copy)** + @@ -4525,7 +4529,7 @@ private ThreadLocalMap(ThreadLocalMap parentMap) { * 有界队列:有固定大小的队列,比如设定了固定大小的 LinkedBlockingQueue,又或者大小为 0 -* 无界队列:没有设置固定大小的队列,这些队列可以直接入队,直到溢出,一般不会有到这么大的容量(超过 Integer.MAX_VALUE),所以相当于无界 +* 无界队列:没有设置固定大小的队列,这些队列可以直接入队,直到溢出(超过 Integer.MAX_VALUE),所以相当于无界 java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:**FIFO 队列** @@ -4954,10 +4958,10 @@ TransferStack 类成员变量: if (match == null && UNSAFE.compareAndSwapObject(this, matchOffset, null, s)) { // 当前 node 如果自旋结束,会 park 阻塞,阻塞前将 node 对应的 Thread 保留到 waiter 字段 Thread w = waiter; - //条件成立说明 node 对应的 Thread 已经阻塞 + // 条件成立说明 node 对应的 Thread 正在阻塞 if (w != null) { waiter = null; - //使用 unpark 方式唤醒线程 + // 使用 unpark 方式唤醒线程 LockSupport.unpark(w); } return true; @@ -5036,7 +5040,7 @@ TransferStack 类成员方法: // 当前 node 模式为 DATA 类型:返回 Node.item 数据域,当前请求提交的数据 e return (E) ((mode == REQUEST) ? m.item : s.item); } - // 【CASE2】:当前栈顶模式与请求模式不一致,且栈顶不是 FULFILLING说明没被其他节点匹配,说明可以匹配 + // 【CASE2】:逻辑到这说明请求模式不一致,如果栈顶不是 FULFILLING 说明没被其他节点匹配,说明可以匹配 } else if (!isFulfilling(h.mode)) { // 头节点是取消节点,协助出栈 if (h.isCancelled()) @@ -5056,16 +5060,16 @@ TransferStack 类成员方法: } // 获取匹配节点的下一个节点 SNode mn = m.next; - // 尝试匹配,匹配成功,则将 fulfilling 和 m 一起出栈 + // 尝试匹配,【匹配成功】,则将 fulfilling 和 m 一起出栈,并且唤醒被匹配的节点的线程 if (m.tryMatch(s)) { - casHead(s, mn); // pop both s and m + casHead(s, mn); return (E) ((mode == REQUEST) ? m.item : s.item); } else // 匹配失败,出栈 m s.casNext(m, mn); } } - // 【CASE3】:栈顶模式为 FULFILLING 模式,表示栈顶和栈顶下面的栈帧正在发生匹配,当前请求需要做协助工作 + // 【CASE3】:栈顶模式为 FULFILLING 模式,表示【栈顶和栈顶下面的节点正在发生匹配】,当前请求需要做协助工作 } else { // h 表示的是 fulfilling 节点,m 表示 fulfilling 匹配的节点 SNode m = h.next; @@ -5119,7 +5123,7 @@ TransferStack 类成员方法: spins = shouldSpin(s) ? (spins - 1) : 0; // 说明没有自旋次数了 else if (s.waiter == null) - // 把当前 node 对应的 Thread 保存到 node.waiter 字段中 + // 把当前 node 对应的 Thread 保存到 node.waiter 字段中,要阻塞了 s.waiter = w; // 没有超时限制直接阻塞 else if (!timed) @@ -5275,7 +5279,8 @@ TransferQueue 类成员方法: QNode h = head; if (t == null || h == null) continue; - // head 和 tail 同时指向 dummy 节点,说明是空队列,或者队尾节点与当前请求类型是一致的情况,无法匹配 + // head 和 tail 同时指向 dummy 节点,说明是【空队列,或者是不匹配的情况】 + // 队尾节点与当前请求类型是一致的情况,说明阻塞队列中都无法匹配,无法匹配 if (h == t || t.isData == isData) { // 获取队尾 t 的 next 节点 QNode tn = t.next; @@ -5317,9 +5322,9 @@ TransferQueue 类成员方法: s.waiter = null; } return (x != null) ? (E)x : e; - // 队尾节点与当前请求节点互补 + // 队尾节点与当前请求节点【互补匹配】 } else { - // h.next 节点,请求节点与队尾模式不同,需要与队头发生匹配,TransferQueue 是一个【公平模式】 + // h.next 节点,请求节点与队尾模式不同,需要与队头发生匹配,TransferQueue 是一个【公平模式】 QNode m = h.next; // 并发导致其他线程修改了队尾节点,或者已经把 head.next 匹配走了 if (t != tail || m == null || h != head) @@ -5543,7 +5548,7 @@ Executors提供了四种线程池的创建:newCachedThreadPool、newFixedThrea } ``` - * 核心线程数是 0, 最大线程数是 Integer.MAX_VALUE,全部都是救急线程(60s 后可以回收),可能会创建大量线程,从而导致 **OOM** + * 核心线程数是 0, 最大线程数是 29 个 1,全部都是救急线程(60s 后可以回收),可能会创建大量线程,从而导致 **OOM** * SynchronousQueue 作为阻塞队列,没有容量,对于每一个 take 的线程会阻塞直到有一个 put 的线程放入元素为止(类似一手交钱、一手交货) * 适合任务数比较密集,但每个任务执行时间较短的情况 @@ -5708,7 +5713,7 @@ System.out.println(future.get()); #### 状态信息 -ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量。这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 CAS 原子操作进行赋值 +ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 位表示线程数量**。这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 CAS 原子操作进行赋值 * 状态表示: @@ -5769,7 +5774,7 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 * 重置当前线程池状态 ctl: ```java - // rs 表示线程池状态 wc 表示当前线程池中 worker(线程)数量,类似相加操作 + // rs 表示线程池状态,wc 表示当前线程池中 worker(线程)数量,类似相加操作 private static int ctlOf(int rs, int wc) { return rs | wc; } ``` @@ -5788,11 +5793,11 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 * 设置线程池 ctl: ```java - // 使用CAS方式 让 ctl 值 +1 ,成功返回 true, 失败返回 false + // 使用 CAS 方式 让 ctl 值 +1 ,成功返回 true, 失败返回 false private boolean compareAndIncrementWorkerCount(int expect) { return ctl.compareAndSet(expect, expect + 1); } - // 使用CAS 方式 让 ctl 值 -1 ,成功返回 true, 失败返回 false + // 使用 CAS 方式 让 ctl 值 -1 ,成功返回 true, 失败返回 false private boolean compareAndDecrementWorkerCount(int expect) { return ctl.compareAndSet(expect, expect - 1); } @@ -5851,13 +5856,13 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 ```java private int largestPoolSize; // 记录线程池生命周期内线程数最大值 - private long completedTaskCount; // 记录线程池所完成任务总数,当某个 worker 退出时将完成的任务累积到该属性 + private long completedTaskCount; // 记录线程池所完成任务总数,当某个 worker 退出时将完成的任务累加到该属性 ``` * 控制核心线程数量内的线程是否可以被回收: ```java - // false 代表不可以,为 true 时核心数量内的线程空闲超过 keepAliveTime 也会被回收 + // false 代表不可以,为 true 时核心线程空闲超过 keepAliveTime 也会被回收 private volatile boolean allowCoreThreadTimeOut; ``` @@ -5871,7 +5876,7 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 Runnable firstTask; // worker 第一个执行的任务,普通的 Runnable 实现类或者是 FutureTask volatile long completedTasks; // 记录当前 worker 所完成任务数量 - //构造方法 + // 构造方法 Worker(Runnable firstTask) { // 设置AQS独占模式为初始化中状态,这个状态不能被抢占锁 setState(-1); @@ -5897,7 +5902,7 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 ##### 提交方法 -* AbstractExecutorService#submit():提交任务,**把任务封装成 FutureTask 执行**,可以通过返回的任务对象调用 get 阻塞获取任务执行的结果,源码分析在笔记的 Future 部分 +* AbstractExecutorService#submit():提交任务,**把 Runnable 或 Callable 任务封装成 FutureTask 执行**,可以通过方法返回的任务对象调用 get 阻塞获取任务执行的结果或者异常,源码分析在笔记的 Future 部分 ```java public Future submit(Runnable task) { @@ -5931,33 +5936,32 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 } ``` -* execute():执行任务,但是没有返回值,没办法获取任务执行结果 +* execute():执行任务,但是没有返回值,没办法获取任务执行结果,出现异常会直接抛出任务执行时的异常 ```java - // command 可以是普通的 Runnable 实现类,也可以是 FutureTask + // command 可以是普通的 Runnable 实现类,也可以是 FutureTask,不能是 Callable public void execute(Runnable command) { // 非空判断 if (command == null) throw new NullPointerException(); // 获取 ctl 最新值赋值给 c,ctl 高3位表示线程池状态,低位表示当前线程池线程数量。 int c = ctl.get(); - // 【1】条件成立表示当前线程数量小于核心线程数,此次提交任务直接创建一个新的 worker,线程池中多了一个新的线程 + // 【1】当前线程数量小于核心线程数,此次提交任务直接创建一个新的 worker,线程池中多了一个新的线程 if (workerCountOf(c) < corePoolSize) { // addWorker 为创建线程的过程,会创建 worker 对象并且将 command 作为 firstTask,优先执行 if (addWorker(command, true)) - // 创建成功直接返回 return; - // 执行到这条语句,说明 addWorker 一定是失败的,存在并发现象或者线程池状态被改变, + + // 执行到这条语句,说明 addWorker 一定是失败的,存在并发现象或者线程池状态被改变,重新获取状态 // SHUTDOWN 状态下也有可能创建成功,前提 firstTask == null 而且当前 queue 不为空(特殊情况) c = ctl.get(); } // 执行到这说明当前线程数量已经达到核心线程数量 或者 addWorker 失败 - // 【2】条件成立说明当前线程池处于running状态,则尝试将 task 放入到 workQueue 中 + // 【2】条件成立说明当前线程池处于running状态,则尝试将 task 放入到 workQueue 中,核心满了 if (isRunning(c) && workQueue.offer(command)) { - // 获取线程池状态 ctl 保存到 recheck int recheck = ctl.get(); - // 条件一成立说明线程池状态被外部线程给修改了,可能是执行了 shutdown() 方法,需要把刚提交的任务删除 - // 删除成功说明提交之后,线程池中的线程还未消费该任务(处理) + // 条件一成立说明线程池状态被外部线程给修改了,可能是执行了 shutdown() 方法,该状态不能接收新提交的任务 + // 所以要把刚提交的任务删除,删除成功说明提交之后线程池中的线程还未消费该任务(处理) if (!isRunning(recheck) && remove(command)) // 任务出队成功,走拒绝策略 reject(command); @@ -5967,7 +5971,7 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 addWorker(null, false); } // 【3】offer失败说明queue满了 - // 如果线程数量尚未达到 maximumPoolSize,会创建非核心 worker 线程执行 command,不公平的原因 + // 如果线程数量尚未达到 maximumPoolSize,会创建非核心 worker 线程执行 command,这也是不公平的原因 // 如果当前线程数量达到 maximumPoolSiz,这里 addWorker 也会失败,走拒绝策略 else if (!addWorker(command, false)) reject(command); @@ -5996,9 +6000,11 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 int c = ctl.get(); // 获取当前线程池运行状态 int rs = runStateOf(c); + // 判断当前线程池状态【是否允许添加线程】 - // 判断当前线程池是 SHUTDOWN 状态,但是队列里面还有任务尚未处理完, - // 这时需要处理完 queue 中的任务,但是【不允许再提交新的 task】,所以 addWorker 返回 false + + // 当前线程池是 SHUTDOWN 状态,但是队列里面还有任务尚未处理完, + // 需要处理完 queue 中的任务,但是【不允许再提交新的 task】,所以 addWorker 返回 false if (rs >= SHUTDOWN && !(rs == SHUTDOWN && firstTask == null && !workQueue.isEmpty())) return false; for (;;) { @@ -6038,12 +6044,13 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 // 加互斥锁 mainLock.lock(); try { - // 获取最新线程池运行状态保存到rs中 + // 获取最新线程池运行状态保存到 rs int rs = runStateOf(ctl.get()); // 判断线程池是否为RUNNING状态,不是再判断当前是否为SHUTDOWN状态且firstTask为空(特殊情况) if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) { - // 当线程start后,线程isAlive会返回true,否则报错 + // 当线程 start 后,线程 isAlive 会返回 true,否则报错 if (t.isAlive()) throw new IllegalThreadStateException(); + //【将新建的 Worker 添加到线程池中】 workers.add(w); int s = workers.size(); @@ -6120,7 +6127,7 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 Thread wt = Thread.currentThread(); // 获取 worker 的 firstTask Runnable task = w.firstTask; - // 引用置空,防止复用该线程时重复执行该任务 + // 引用置空,防止复用该线程时【重复执行】该任务 w.firstTask = null; // 初始化 worker 时设置 state = -1,这里需要设置 state = 0 和 exclusiveOwnerThread = null w.unlock(); @@ -6128,10 +6135,11 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 boolean completedAbruptly = true; try { // firstTask 不是 null 就直接运行,否则去 queue 中获取任务 - // 【getTask如果是阻塞获取任务,会一直阻塞在take方法,获取后继续循环,不会走返回null的逻辑】 + // 【getTask 如果是阻塞获取任务,会一直阻塞在take方法,获取后继续循环,不会走返回null的逻辑】 while (task != null || (task = getTask()) != null) { - // worker加锁,shutdown 时会判断当前worker状态,根据独占锁是否【空闲】 + // worker 加锁,shutdown 时会判断当前 worker 状态,根据独占锁是否【空闲】 w.lock(); + // 说明线程池状态大于 STOP,目前处于 STOP/TIDYING/TERMINATION,此时给线程一个中断信号 if ((runStateAtLeast(ctl.get(), STOP) || // 说明线程处于 RUNNING 或者 SHUTDOWN 状态,清除打断标记 @@ -6139,7 +6147,7 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 // 中断线程,设置线程的中断标志位为 true wt.interrupt(); try { - // 钩子方法,开发者自定义实现 + // 钩子方法,任务执行的前置处理 beforeExecute(wt, task); Throwable thrown = null; try { @@ -6152,7 +6160,7 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 } catch (Throwable x) { thrown = x; throw new Error(x); } finally { - // 钩子方法,开发者自定义实现 + // 钩子方法,任务执行的后置处理 afterExecute(task, thrown); } } finally { @@ -6161,7 +6169,7 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 w.unlock(); // 解锁 } } - // getTask()方法返回null时会执行这里,表示queue为空并且线程空闲超过保活时间,当前【线程应该执行退出逻辑】 + // getTask()方法返回null时会走到这里,表示queue为空并且线程空闲超过保活时间,【当前线程执行退出逻辑】 completedAbruptly = false; } finally { // 正常退出 completedAbruptly = false @@ -6187,40 +6195,38 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 ```java private Runnable getTask() { - //超时标记,表示当前线程获取任务是否超时,默认 false,true 表示已超时 + // 超时标记,表示当前线程获取任务是否超时,默认 false,true 表示已超时 boolean timedOut = false; for (;;) { - // 获取最新ctl值保存到c中 int c = ctl.get(); // 获取线程池当前运行状态 int rs = runStateOf(c); // 【tryTerminate】打断线程后执行到这,此时线程池状态为STOP或者线程池状态为SHUTDOWN并且队列已经是空 - // 所以下面的 if 条件一定是成立的,可以直接返回 null - - // 当前线程池是非 RUNNING 状态,并且线程池状态 >= STOP 或者 queue 为 null,线程就应该退出了 + // 所以下面的 if 条件一定是成立的,可以直接返回 null,线程就应该退出了 if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) { // 使用 CAS 自旋的方式让 ctl 值 -1 decrementWorkerCount(); - // 返回null,runWorker 方法就会将返回 null 的线程执行线程退出线程池的逻辑 return null; } + // 获取线程池中的线程数量 int wc = workerCountOf(c); + // 线程没有明确的区分谁是核心或者非核心,是根据当前池中的线程数量判断 + // timed = false 表示当前这个线程 获取task时不支持超时机制的,当前线程会使用 queue.take() 阻塞获取 // timed = true 表示当前这个线程 获取task时支持超时机制,使用 queue.poll(xxx,xxx) 超时获取 // 条件一代表允许回收核心线程,那就无所谓了,全部线程都执行超时回收 - // 条件二成立说明线程数量大于核心线程数,当前线程认为是非核心线程, - // 空闲一定时间就需要退出,去超时获取任务,获取不到返回null + // 条件二成立说明线程数量大于核心线程数,当前线程认为是非核心线程,空闲一定时间就需要退出,去超时获取任务 boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; - // 条件一判断线程数量是否超过最大线程数,直接回收 + // 如果线程数量是否超过最大线程数,直接回收 // 如果当前线程允许超时回收并且已经超时了,就应该被回收了,但是由于【担保机制】还要做判断: // wc > 1 说明线程池还用其他线程,当前线程可以直接回收 // workQueue.isEmpty() 前置条件是 wc = 1,如果当前任务队列也是空了,最后一个线程就可以安全的退出 if ((wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty())) { - // 使用CAS机制将 ctl 值 -1 ,减 1 成功的线程,返回 null,可以退出 + // 使用 CAS 机制将 ctl 值 -1 ,减 1 成功的线程,返回 null,代表可以退出 if (compareAndDecrementWorkerCount(c)) return null; continue; @@ -6236,13 +6242,13 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 // 获取任务为 null 说明超时了,将超时标记设置为 true,下次自旋时返 null timedOut = true; } catch (InterruptedException retry) { - // 阻塞线程被打断后超时标记置为 false, + // 阻塞线程被打断后超时标记置为 false,说明被打断不算超时,要继续获取,直到超时或者获取到任务 timedOut = false; } } } ``` - + * processWorkerExit():**线程退出线程池** ```java @@ -6271,7 +6277,7 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 if (runStateLessThan(c, STOP)) { // 正常退出的逻辑,是空闲线程回收 if (!completedAbruptly) { - // 根据是否回收核心线程确定线程池中的最小值 + // 根据是否回收核心线程确定【线程池中的线程数量最小值】 int min = allowCoreThreadTimeOut ? 0 : corePoolSize; // 最小值为 0,但是线程队列不为空,需要一个线程来完成任务【担保机制】 if (min == 0 && !workQueue.isEmpty()) @@ -6280,7 +6286,7 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 if (workerCountOf(c) >= min) return; } - // 执行task时发生异常,这里要创建一个新 worker 加进线程池 + // 执行 task 时发生异常,这里要创建一个新 worker 加进线程池,有个线程因为异常终止了 addWorker(null, false); } } @@ -6299,18 +6305,18 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 ```java public void shutdown() { final ReentrantLock mainLock = this.mainLock; - //获取线程池全局锁 + // 获取线程池全局锁 mainLock.lock(); try { checkShutdownAccess(); - //设置线程池状态为 SHUTDOWN + // 设置线程池状态为 SHUTDOWN advanceRunState(SHUTDOWN); - //中断空闲线程 + // 中断空闲线程 interruptIdleWorkers(); - //空方法,子类可以扩展 + // 空方法,子类可以扩展 onShutdown(); } finally { - //释放线程池全局锁 + // 释放线程池全局锁 mainLock.unlock(); } tryTerminate(); @@ -6331,11 +6337,11 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 // 获取当前 worker 的线程 Thread t = w.thread; //条件一成立:说明当前迭代的这个线程尚未中断 - //条件二成立:说明当前worker处于空闲状态,阻塞在poll或者take,因为worker执行task时是加锁的 - // 每个worker有一个独占锁,w.tryLock()尝试加锁,如果锁已经加过了会返回 false + //条件二成立:说明当前worker处于空闲状态,阻塞在poll或者take,因为worker执行task时是要加锁的 + // 每个worker有一个独占锁,w.tryLock()尝试加锁,加锁成功返回 true if (!t.isInterrupted() && w.tryLock()) { try { - // 中断线程,处于 queue 阻塞的线程会被唤醒,进入下一次自旋,返回null,执行退出相逻辑 + // 中断线程,处于 queue 阻塞的线程会被唤醒,进入下一次自旋,返回 null,执行退出相逻辑 t.interrupt(); } catch (SecurityException ignore) { } finally { @@ -6359,7 +6365,7 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 ```java public List shutdownNow() { - //返回值引用 + // 返回值引用 List tasks; final ReentrantLock mainLock = this.mainLock; //获取线程池全局锁 @@ -6389,7 +6395,7 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 for (;;) { // 获取 ctl 的值 int c = ctl.get(); - // 条件一说明线程池正常,条件二说明有其他线程执行了状态转换的方法,当前线程直接返回 + // 线程池正常,或者有其他线程执行了状态转换的方法,当前线程直接返回 if (isRunning(c) || runStateAtLeast(c, TIDYING) || // 线程池是 SHUTDOWN 并且任务队列不是空,需要去处理队列中的任务 (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty())) @@ -6408,7 +6414,7 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 // 加全局锁 mainLock.lock(); try { - // 设置线程池状态为 TIDYING 状态 + // 设置线程池状态为 TIDYING 状态,线程数量为 0 if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) { try { // 结束线程池 @@ -6425,7 +6431,6 @@ ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位 // 释放线程池全局锁 mainLock.unlock(); } - // else retry on failed CAS } } ``` @@ -6561,15 +6566,16 @@ FutureTask 类的成员方法: ```java public void run() { - //条件一:成立说明当前 task 已经被执行过了或者被 cancel 了,非 NEW 状态的任务,线程就不处理了 + //条件一:成立说明当前 task 已经被执行过了或者被 cancel 了,非 NEW 状态的任务,线程就不需要处理了 //条件二:线程是 NEW 状态,尝试设置当前任务对象的线程是当前线程,设置失败说明其他线程抢占了该任务,直接返回 if (state != NEW || !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread())) - return; //直接返回 + return; try { // 执行到这里,当前 task 一定是 NEW 状态,而且当前线程也抢占 task 成功! Callable c = callable; - // 判断任务是否为空,防止空指针异常,判断 state 防止外部线程在此期间 cancel 掉当前任务。 + // 判断任务是否为空,防止空指针异常;判断 state 状态,防止外部线程在此期间 cancel 掉当前任务 + // 因为 task 的执行者已经设置为当前线程,所以这里是线程安全的, if (c != null && state == NEW) { // 结果引用 V result; @@ -6729,8 +6735,7 @@ FutureTask 类的成员方法: q = new WaitNode(); // 条件成立:【第二次自旋】,当前线程已经创建 WaitNode 对象了,但是node对象还未入队 else if (!queued) - // waiters 指向队首,让当前 WaitNode 成为新的队首,【头插法】 - // 失败说明再次期间有了新的队首 + // waiters 指向队首,让当前 WaitNode 成为新的队首,【头插法】,失败说明其他线程修改了新的队首 queued = UNSAFE.compareAndSwapObject(this, waitersOffset, q.next = waiters, q); // 条件成立:【第三次自旋】,会到这里。 else if (timed) { @@ -6744,17 +6749,17 @@ FutureTask 类的成员方法: } // 条件成立:说明需要休眠 else - // 当前 get 操作的线程就会被 park 了,除非有其它线程将唤醒或者将当前线程中断 + // 【当前 get 操作的线程就会被 park 阻塞了】,除非有其它线程将唤醒或者将当前线程中断 LockSupport.park(this); } } ``` - + FutureTask#report:封装运行结果 - + ```java private V report(int s) throws ExecutionException { - // 获取执行结果 + // 获取执行结果,都在一个 futuretask 对象中的属性,可以直接获取 Object x = outcome; // 当前任务状态正常结束 if (s == NORMAL) @@ -6766,13 +6771,13 @@ FutureTask 类的成员方法: throw new ExecutionException((Throwable)x); } ``` - + * FutureTask#cancel:任务取消 ```java public boolean cancel(boolean mayInterruptIfRunning) { // 条件一:表示当前任务处于运行中或者处于线程池任务队列中 - // 条件二:表示修改状态,成功可以去执行下面逻辑,否则返回 false 表示 cancel 失败。 + // 条件二:表示修改状态,成功可以去执行下面逻辑,否则返回 false 表示 cancel 失败 if (!(state == NEW && UNSAFE.compareAndSwapInt(this, stateOffset, NEW, mayInterruptIfRunning ? INTERRUPTING : CANCELLED))) @@ -7187,7 +7192,7 @@ AbstractQueuedSynchronizer 中 state 设计: * state **使用 volatile 修饰配合 cas** 保证其修改时的原子性 -* state 表示**线程重入的次数或者许可进入的线程数** +* state 表示**线程重入的次数(独占模式)或者剩余许可数(共享模式)** * state API: @@ -7952,7 +7957,10 @@ public static void main(String[] args) throws InterruptedException { // 条件二中判断当前线程是否被打断,被打断返回true,设置中断标记为 true,获取锁后返回 interrupted = true; } - } + } + } finally { + if (failed) + cancelAcquire(node); } } private final boolean parkAndCheckInterrupt() { @@ -10499,7 +10507,7 @@ public V put(K key, V value) { int sc; while ((sc = sizeCtl) >= 0) { Node[] tab = table; int n; - //数组还未初始化,一般是调用集合构造方法才会成立,put 后调用该方法都是不成立的 + // 数组还未初始化,一般是调用集合构造方法才会成立,put 后调用该方法都是不成立的 if (tab == null || (n = tab.length) == 0) { n = (sc > c) ? sc : c; if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { @@ -10517,8 +10525,21 @@ public V put(K key, V value) { // 未达到扩容阈值或者数组长度已经大于最大长度 else if (c <= sc || n >= MAXIMUM_CAPACITY) break; - else if (tab == table) // 与 addCount 逻辑相同 - + else if (tab == table) {// 与 addCount 逻辑相同 + int rs = resizeStamp(n); + if (sc < 0) { + Node[] nt; + if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || + sc == rs + MAX_RESIZERS || (nt = nextTable) == null || + transferIndex <= 0) + break; + if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) + transfer(tab, nt); + } + else if (U.compareAndSwapInt(this, SIZECTL, sc, + (rs << RESIZE_STAMP_SHIFT) + 2)) + transfer(tab, null); + } } } ``` @@ -11923,9 +11944,9 @@ ConcurrentLinkedQueue 使用约定: 1. 不允许 null 入列 2. 队列中所有未删除的节点的 item 都不能为 null 且都能从 head 节点遍历到 3. 删除节点是将 item 设置为 null,队列迭代时跳过 item 为 null 节点 -4. head 节点跟 tail 不一定指向头节点或尾节点,可能存在滞后性 +4. head 节点跟 tail 不一定指向头节点或尾节点,可能**存在滞后性** -ConcurrentLinkedQueue 由 head 节点和 tail 节点组成,每个节点(Node)由节点元素(item)和指向下一个节点的引用(next)组成,组成一张链表结构的队列 +ConcurrentLinkedQueue 由 head 节点和 tail 节点组成,每个节点由节点元素和指向下一个节点的引用组成,组成一张链表结构的队列 ```java private transient volatile Node head; @@ -11950,7 +11971,7 @@ private static class Node { ```java public ConcurrentLinkedQueue() { - // 默认情况下head节点存储的元素为空,tail节点等于head节点 + // 默认情况下 head 节点存储的元素为空,dummy 节点,tail 节点等于 head 节点 head = tail = new Node(null); } ``` @@ -11998,29 +12019,28 @@ public boolean offer(E e) { // 创建入队节点 final Node newNode = new Node(e); - // 循环CAS直到入队成功 + // 循环 CAS 直到入队成功 for (Node t = tail, p = t;;) { - // p用来表示队列的尾节点,初始情况下等于tail节点,q 是p的next节点 + // p 用来表示队列的尾节点,初始情况下等于 tail 节点,q 是 p 的 next 节点 Node q = p.next; - // 判断p是不是尾节点 + // 条件成立说明 p 是尾节点 if (q == null) { - // p是尾节点,设置p节点的下一个节点为新节点 - // 设置成功则casNext返回true,否则返回false,说明有其他线程更新过尾节点 - // 继续寻找尾节点,继续CAS + // p 是尾节点,设置 p 节点的下一个节点为新节点 + // 设置成功则 casNext 返回 true,否则返回 false,说明有其他线程更新过尾节点,继续寻找尾节点,继续 CAS if (p.casNext(null, newNode)) { - // 首次添加时,p等于t,不进行尾节点更新,所以所尾节点存在滞后性 + // 首次添加时,p 等于 t,不进行尾节点更新,所以尾节点存在滞后性 if (p != t) - // 将tail设置为新入队的节点,设置失败表示其他线程更新了tail节点 + // 将 tail 设置成新入队的节点,设置失败表示其他线程更新了 tail 节点 casTail(t, newNode); return true; } } else if (p == q) - // 当tail不指向最后节点时,如果执行出列操作,可能将tail也移除,tail不在链表中 - // 此时需要对tail节点进行复位,复位到head节点 + // 当 tail 不指向最后节点时,如果执行出列操作,可能将 tail 也移除,tail 不在链表中 + // 此时需要对 tail 节点进行复位,复位到 head 节点 p = (t != (t = tail)) ? t : head; else - // 推动tail尾节点往队尾移动 + // 推动 tail 尾节点往队尾移动 p = (p != t && t != (t = tail)) ? t : q; } } @@ -12061,18 +12081,18 @@ public boolean offer(E e) { public E poll() { restartFromHead: for (;;) { - // p节点表示首节点,即需要出队的节点 + // p 节点表示首节点,即需要出队的节点,FIFO for (Node h = head, p = h, q;;) { E item = p.item; - // 如果p节点的元素不为null,则通过CAS来设置p节点引用元素为null,成功返回item + // 如果 p 节点的元素不为 null,则通过 CAS 来设置 p 节点引用元素为 null,成功返回 item if (item != null && p.casItem(item, null)) { - if (p != h) - // 对head进行移动 + if (p != h) + // 对 head 进行移动 updateHead(h, ((q = p.next) != null) ? q : p); return item; } - // 如果头节点的元素为空或头节点发生了变化,这说明头节点被另外一个线程修改了 - // 那么获取p节点的下一个节点,如果p节点的下一节点为null,则表明队列已经空了 + // 逻辑到这说明头节点的元素为空或头节点发生了变化,头节点被另外一个线程修改了 + // 那么获取 p 节点的下一个节点,如果 p 节点的下一节点也为 null,则表明队列已经空了 else if ((q = p.next) == null) { updateHead(h, p); return null; @@ -12088,7 +12108,7 @@ public E poll() { } final void updateHead(Node h, Node p) { if (h != p && casHead(h, p)) - // 将旧结点h的next域指向为h + // 将旧结点 h 的 next 域指向为 h,help gc h.lazySetNext(h); } ``` @@ -12097,11 +12117,13 @@ final void updateHead(Node h, Node p) { ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentLinkedQueue出队操作1.png) - +![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentLinkedQueue出队操作2.png) + +![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentLinkedQueue出队操作3.png) + +如果这时,有一个线程来添加元素,通过 tail 获取的 next 节点则仍然是它本身,这就出现了p == q 的情况,出现该种情况之后,则会触发执行 head 的更新,将 p 节点重新指向为 head - -如果这时,有一个线程来添加元素,通过 tail 获取的 next 节点则仍然是它本身,这就出现了**p == q** 的情况,出现该种情况之后,则会触发执行 head 的更新,将 p 节点重新指向为 head 参考文章:https://www.jianshu.com/p/231caf90f30b @@ -12113,9 +12135,7 @@ final void updateHead(Node h, Node p) { #### 成员方法 -* peek() - - peek 操作会改变 head 指向,执行 peek() 方法后 head 会指向第一个具有非空元素的节点 +* peek():会改变 head 指向,执行 peek() 方法后 head 会指向第一个具有非空元素的节点 ```java // 获取链表的首部元素,只读取而不移除 @@ -12137,16 +12157,14 @@ final void updateHead(Node h, Node p) { } } ``` - -* size() - - 用来获取当前队列的元素个数,因为整个过程都没有加锁,在并发环境中从调用 size 方法到返回结果期间有可能增删元素,导致统计的元素个数不精确 + +* size():用来获取当前队列的元素个数,因为整个过程都没有加锁,在并发环境中从调用 size 方法到返回结果期间有可能增删元素,导致统计的元素个数不精确 ```java public int size() { int count = 0; - // first()获取第一个具有非空元素的节点,若不存在,返回null - // succ(p)方法获取p的后继节点,若p == p的后继节点,则返回head + // first() 获取第一个具有非空元素的节点,若不存在,返回 null + // succ(p) 方法获取 p 的后继节点,若 p == p.next,则返回 head // 类似遍历链表 for (Node p = first(); p != null; p = succ(p)) if (p.item != null) @@ -12156,8 +12174,8 @@ final void updateHead(Node h, Node p) { return count; } ``` - -* remove() + +* remove():移除元素 ```java public boolean remove(Object o) { @@ -12174,7 +12192,7 @@ final void updateHead(Node h, Node p) { next = succ(p); continue; } - // 若匹配,则通过CAS操作将对应节点元素置为null + // 若匹配,则通过 CAS 操作将对应节点元素置为 null removed = p.casItem(item, null); } // 获取删除节点的后继节点 From 5b8154ba4e5baf0588c1df91cad24417e8dac114 Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 18 Aug 2021 21:03:52 +0800 Subject: [PATCH 029/168] Update Java Notes --- Prog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Prog.md b/Prog.md index eb3c05a..26842b6 100644 --- a/Prog.md +++ b/Prog.md @@ -28,7 +28,7 @@ 参考视频:https://www.bilibili.com/video/BV16J411h7Rd(推荐观看) -笔记的整体内容依据视频编写,并且随着不断的学习补充了很多新知识 +笔记的整体内容依据视频编写,并且补充了很多新知识 From 18823ced2cf3da690997a9ac33c3995fa59a2342 Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 18 Aug 2021 22:24:16 +0800 Subject: [PATCH 030/168] Update README --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index 43ee4e1..2882e8b 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,7 @@ 其他说明: +* 推荐使用 Typora 阅读笔记,打开目录栏效果更佳。 * 所有的知识不保证权威性,如果各位朋友发现错误,非常欢迎与我讨论。 - * Java.md 更新后大于 1M,导致网页无法显示,所以划分为 Java 和 Program 两个文档。 -* 推荐使用 Typora 阅读笔记,打开目录栏效果更佳,示例展示: - - ![](https://gitee.com/seazean/images/raw/master/Java/Java-图片.png) - From 8bd2754dd51ca55e8b239ca1207d979d52c2b055 Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 19 Aug 2021 19:49:14 +0800 Subject: [PATCH 031/168] Update README --- Java.md | 115 +++++++++++++++++++++++++------------------------------- Prog.md | 16 ++++---- SSM.md | 42 +++++++-------------- 3 files changed, 72 insertions(+), 101 deletions(-) diff --git a/Java.md b/Java.md index d2836ff..a6eff40 100644 --- a/Java.md +++ b/Java.md @@ -47,7 +47,7 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, - short 数据类型是 16 位、有符号的以二进制补码表示的整数 - 最小值是 **-32768(-2^15)** - 最大值是 **32767(2^15 - 1)** -- short 数据类型也可以像 byte 那样节省空间,一个short变量是int型变量所占空间的二分之一 +- short 数据类型也可以像 byte 那样节省空间,一个 short 变量是 int 型变量所占空间的二分之一 - 默认值是 **`0`** - 例子:`short s = 1000,short r = -20000` @@ -57,7 +57,7 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, - 最小值是 **-2,147,483,648(-2^31)** - 最大值是 **2,147,483,647(2^31 - 1)** - 一般地整型变量默认为 int 类型 -- 默认值是 **`0`** ; +- 默认值是 **`0`** - 例子:`int a = 100000, int b = -200000` **long:** @@ -100,19 +100,7 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, - 最小值是 **`\u0000`**(即为 0) - 最大值是 **`\uffff`**(即为 65535) - char 数据类型可以储存任何字符 -- 例子:`char c = 'A';` `char c = '张';` - -```mermaid -graph LR -数据范围从小到大图 - -A[byte]-->B[short] -C[char]-->D[int] -B-->D -D-->F[long] -G[float] -G-->H[double] -``` +- 例子:`char c = 'A';` `char c = '张'` 上下转型 @@ -762,7 +750,7 @@ public static 返回值类型 方法名(参数) { 重载仅针对**同一个类**中方法的名称与参数进行识别,**与返回值无关**,不能通过返回值来判定两个方法是否构成重载 -原理:JVM → 运行机制 → 字节码 → 方法表 +原理:JVM → 运行机制 → 方法调用 → 多态原理 ```java public class MethodDemo { @@ -1244,7 +1232,7 @@ static 静态修饰的成员(方法和成员变量)属于类本身的。 } ``` -子类继承父类的东西: +子类不能继承父类的东西: * 子类不能继承父类的构造器,子类有自己的构造器 * 子类是不能可以继承父类的私有成员的,可以反射暴力去访问继承自父类的私有成员 @@ -3755,7 +3743,7 @@ public class RegexDemo { * 链表:元素不是内存中的连续区域存储,元素是游离存储的,每个元素会记录下个元素的地址 特点:**查询元素慢,增删元素快**(针对于首尾元素,速度极快,一般是双链表) -* 树 +* 树: * 二叉树:binary tree 永远只有一个根节点,是每个结点不超过2个节点的树(tree) 特点:二叉排序树:小的左边,大的右边,但是可能树很高,性能变差 @@ -4464,8 +4452,9 @@ PriorityQueue 是优先级队列,底层存储结构为 Object[],默认实现 #### Collections -java.utils.Collections:集合**工具类**,Collections并不属于集合,是用来操作集合的工具类 -Collections有几个常用的API: +java.utils.Collections:集合**工具类**,Collections 并不属于集合,是用来操作集合的工具类 + +Collections 有几个常用的API: * `public static boolean addAll(Collection c, T... e)`:给集合对象批量添加元素 * `public static void shuffle(List list)`:打乱集合顺序 @@ -6683,16 +6672,16 @@ Stream arrStream2 = Stream.of(arr); #### 常用API -| 方法名 | 说明 | -| --------------------------------------------------------- | ---------------------------------------------------- | -| void forEach(Consumer action) | 逐一处理(遍历) | -| long count | 返回流中的元素数 | -| Stream filterPredicate predicate) | 用于对流中的数据进行过滤 | -| Stream limit(long maxSize) | 返回此流中的元素组成的流,截取前指定参数个数的数据 | -| Stream skip(long n) | 跳过指定参数个数的数据,返回由该流的剩余元素组成的流 | -| Stream map(Function mapper) | 加工方法,将当前流中的T类型数据转换为另一种R类型的流 | -| static Stream concat(Stream a, Stream b) | 合并a和b两个流为一个,调用 `Stream.concat(s1,s2)` | -| Stream distinct() | 返回由该流的不同元素组成的流 | +| 方法名 | 说明 | +| --------------------------------------------------------- | -------------------------------------------------------- | +| void forEach(Consumer action) | 逐一处理(遍历) | +| long count | 返回流中的元素数 | +| Stream filter(Predicate predicate) | 用于对流中的数据进行过滤 | +| Stream limit(long maxSize) | 返回此流中的元素组成的流,截取前指定参数个数的数据 | +| Stream skip(long n) | 跳过指定参数个数的数据,返回由该流的剩余元素组成的流 | +| Stream map(Function mapper) | 加工方法,将当前流中的 T 类型数据转换为另一种 R 类型的流 | +| static Stream concat(Stream a, Stream b) | 合并 a 和 b 两个流为一个,调用 `Stream.concat(s1,s2)` | +| Stream distinct() | 返回由该流的不同元素组成的流 | ```java public class StreamDemo { @@ -6771,6 +6760,7 @@ Collectors 方法: * `public static Collector toSet()`:把元素收集到 Set 集合中 * `public static Collector toMap(Function keyMapper,Function valueMapper)`:把元素收集到 Map 集合中 * `Object[] toArray()`:把元素收集数组中 +* `public static Collector groupingBy(Function classifier)`:分组 ```java public static void main(String[] args) { @@ -10318,7 +10308,7 @@ public void localvarGC4() { 垃圾收集主要是针对堆和方法区进行,程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收 -在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段,判断对象存活一般有两种方式:**引用计数算法**和**可达性分析算法** +在堆里存放着几乎所有的Java对象实例,在 GC 执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC 才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段,判断对象存活一般有两种方式:**引用计数算法**和**可达性分析算法** @@ -10783,7 +10773,7 @@ GC性能指标: #### Parallel -Parallel Scavenge 收集器是应用于新生代的并行垃圾回收器,**采用复制算法**、并行回收和"Stop the World"机制 +Parallel Scavenge 收集器是应用于新生代的并行垃圾回收器,**采用复制算法**、并行回收和 Stop the World 机制 Parallel Old 收集器:是一个应用于老年代的并行垃圾回收器,**采用标记-整理算法** @@ -10800,7 +10790,7 @@ Parallel Old 收集器:是一个应用于老年代的并行垃圾回收器,* 停顿时间和吞吐量的关系:新生代空间变小 → 缩短停顿时间 → 垃圾回收变得频繁 → 导致吞吐量下降 -在注重吞吐量及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge+Parallel Old 收集器,在 Server 模式下的内存回收性能很好,**Java8默认是此垃圾收集器组合** +在注重吞吐量及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge+Parallel Old 收集器,在 Server 模式下的内存回收性能很好,**Java8 默认是此垃圾收集器组合** ![](https://gitee.com/seazean/images/raw/master/Java/JVM-ParallelScavenge收集器.png) @@ -10845,7 +10835,7 @@ Par 是 Parallel 并行的缩写,New:只能处理的是新生代 ParNew 是很多 JVM 运行在 Server 模式下新生代的默认垃圾收集器 - 对于新生代,回收次数频繁,使用并行方式高效 -- 对于老年代,回收次数少,使用串行方式节省资源(CPU并行需要切换线程,串行可以省去切换线程的资源) +- 对于老年代,回收次数少,使用串行方式节省资源(CPU 并行需要切换线程,串行可以省去切换线程的资源) @@ -12196,6 +12186,7 @@ protected Class loadClass(String name, boolean resolve) } } if (resolve) { + // 链接指定的 Java 类,可以使类的 Class 对象创建完成的同时也被解析 resolveClass(c); } return c; @@ -14505,7 +14496,7 @@ public class GeneratedMethodAccessor1 extends MethodAccessorImpl { -## JVM调优 +## 系统优化 ### 性能调优 @@ -18581,15 +18572,16 @@ JDK 动态代理方式的优缺点: - 优点:可以为任意的接口实现类对象做代理,也可以为被代理对象的所有接口的所有方法做代理,动态代理可以在不改变方法源码的情况下,实现对方法功能的增强,提高了软件的可扩展性,Java 反射机制可以生成任意类型的动态代理类 - 缺点:**只能针对接口或者接口的实现类对象做代理对象**,普通类是不能做代理对象的 -- 原因:**生成的代理类继承了 Proxy**,java 是单继承的,所以 JDK 动态代理只能代理接口 +- 原因:**生成的代理类继承了 Proxy**,Java 是单继承的,所以 JDK 动态代理只能代理接口 ProxyFactory 不是代理模式中的代理类,而代理类是程序在运行过程中动态的在内存中生成的类,可以通过 Arthas 工具查看代理类结构: * 代理类($Proxy0)实现了 SellTickets 接口,真实类和代理类实现同样的接口 * 代理类($Proxy0)将提供了的匿名内部类对象传递给了父类 +* 代理类($Proxy0)的修饰符是 public final ```java -//程序运行过程中动态生成的代理类 +// 程序运行过程中动态生成的代理类 public final class $Proxy0 extends Proxy implements SellTickets { private static Method m3; @@ -18646,7 +18638,7 @@ public static Object newProxyInstance(ClassLoader loader, checkProxyAccess(Reflection.getCallerClass(), loader, intfs); } - //从缓存中查找 class 类型的代理对象,参数二是代理需要实现的接口 + // 从缓存中查找 class 类型的代理对象,参数二是代理需要实现的接口 Class cl = getProxyClass0(loader, intfs); //proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory()) @@ -18655,20 +18647,20 @@ public static Object newProxyInstance(ClassLoader loader, checkNewProxyPermission(Reflection.getCallerClass(), cl); } - //获取代理类的构造方法,根据参数 InvocationHandler 匹配获取某个构造器 + // 获取代理类的构造方法,根据参数 InvocationHandler 匹配获取某个构造器 final Constructor cons = cl.getConstructor(constructorParams); final InvocationHandler ih = h; - //构造方法不是 pubic 的需要启用权限 + // 构造方法不是 pubic 的需要启用权限 if (!Modifier.isPublic(cl.getModifiers())) { AccessController.doPrivileged(new PrivilegedAction() { public Void run() { - //设置可访问的权限 + // 设置可访问的权限 cons.setAccessible(true); return null; } }); } - //cons 是构造方法,并且内部持有 InvocationHandler,在 InvocationHandler 中持有 target 目标对象 + // cons 是构造方法,并且内部持有 InvocationHandler,在 InvocationHandler 中持有 target 目标对象 return cons.newInstance(new Object[]{h}); } catch (IllegalAccessException|InstantiationException e) {} } @@ -18681,7 +18673,7 @@ private static final class ProxyClassFactory { // 代理类型的名称前缀 private static final String proxyClassNamePrefix = "$Proxy"; - //生成唯一数字使用,结合上面的代理类型名称前缀一起生成 + // 生成唯一数字使用,结合上面的代理类型名称前缀一起生成 private static final AtomicLong nextUniqueNumber = new AtomicLong(); //参数一:Proxy.newInstance 时传递的 @@ -18714,22 +18706,22 @@ private static final class ProxyClassFactory { } } - //生成的代理类的包名 + // 生成的代理类的包名 String proxyPkg = null; - //生成的代理类访问修饰符 pulic final + // 【生成的代理类访问修饰符 public final】 int accessFlags = Modifier.PUBLIC | Modifier.FINAL; // 检查接口集合内的接口,看看有没有某个接口的访问修饰符不是 public 的 如果不是 public 的接口, - //生成的代理类 class 就必须和它在一个包下,否则访问出现问题 + // 生成的代理类 class 就必须和它在一个包下,否则访问出现问题 for (Class intf : interfaces) { // 获取访问修饰符 int flags = intf.getModifiers(); if (!Modifier.isPublic(flags)) { accessFlags = Modifier.FINAL; - //获取当前接口的全限定名 包名.类名 + // 获取当前接口的全限定名 包名.类名 String name = intf.getName(); int n = name.lastIndexOf('.'); - //获取包名 + // 获取包名 String pkg = ((n == -1) ? "" : name.substring(0, n + 1)); if (proxyPkg == null) { proxyPkg = pkg; @@ -18750,11 +18742,11 @@ private static final class ProxyClassFactory { // 包名+ $proxy + 数字,比如 $proxy1 String proxyName = proxyPkg + proxyClassNamePrefix + num; - // 生成二进制字节码,这个字节码写入到文件内,就是编译好的 class 文件 + // 【生成二进制字节码,这个字节码写入到文件内】,就是编译好的 class 文件 byte[] proxyClassFile = ProxyGenerator.generateProxyClass( proxyName, interfaces, accessFlags); try { - //使用加载器加载二进制到 jvm,并且返回 class + // 【使用加载器加载二进制到 jvm】,并且返回 class return defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length); } catch (ClassFormatError e) { } @@ -18831,16 +18823,16 @@ CGLIB 的优缺点 三种方式对比: -* JDK 代理和 CGLIB 代理 - - 使用 CGLIB 实现动态代理,CGLIB 底层采用 ASM 字节码生成框架,使用字节码技术生成代理类,在 JDK1.6之前比使用 Java 反射效率要高,到 JDK1.8 的时候,JDK 代理效率高于 CGLIB 代理。所以如果有接口使用 JDK 动态代理,如果没有接口使用 CGLIB 代理 - -* 动态代理和静态代理 +* 动态代理和静态代理: * 动态代理将接口中声明的所有方法都被转移到一个集中的方法中处理(InvocationHandler.invoke),在接口方法数量比较多的时候,可以进行灵活处理,不需要像静态代理那样每一个方法进行中转 * 静态代理是在编译时就已经将接口、代理类、被代理类的字节码文件确定下来 - * 动态代理是程序在运行后通过反射创建字节码文件交由 JVM 加载 + * 动态代理是程序**在运行后通过反射创建字节码文件**交由 JVM 加载 + +* JDK 代理和 CGLIB 代理: + + JDK 动态代理采用 ProxyGenerator.generateProxyClass() 方法在运行时生成字节码;CGLIB 底层采用 ASM 字节码生成框架,使用字节码技术生成代理类。在 JDK1.6之前比使用 Java 反射效率要高,到 JDK1.8 的时候,JDK 代理效率高于 CGLIB 代理。所以如果有接口或者当前类就是接口使用 JDK 动态代理,如果没有接口使用 CGLIB 代理 代理模式的优缺点: @@ -18853,17 +18845,12 @@ CGLIB 的优缺点 代理模式的使用场景: -* 远程(Remote)代理 - - 本地服务通过网络请求远程服务,需要实现网络通信,处理其中可能的异常。为了良好的代码设计和可维护性,将网络通信部分隐藏起来,只暴露给本地服务一个接口,通过该接口即可访问远程服务提供的功能 - -* 防火墙(Firewall)代理 +* 远程(Remote)代理:本地服务通过网络请求远程服务,需要实现网络通信,处理其中可能的异常。为了良好的代码设计和可维护性,将网络通信部分隐藏起来,只暴露给本地服务一个接口,通过该接口即可访问远程服务提供的功能 - 当你将浏览器配置成使用代理功能时,防火墙就将你的浏览器的请求转给互联网,当互联网返回响应时,代理服务器再把它转给你的浏览器 +* 防火墙(Firewall)代理:当你将浏览器配置成使用代理功能时,防火墙就将你的浏览器的请求转给互联网,当互联网返回响应时,代理服务器再把它转给你的浏览器 -* 保护(Protect or Access)代理 +* 保护(Protect or Access)代理:控制对一个对象的访问,如果需要,可以给不同的用户提供不同级别的使用权限 - 控制对一个对象的访问,如果需要,可以给不同的用户提供不同级别的使用权限 diff --git a/Prog.md b/Prog.md index 26842b6..2e1c29d 100644 --- a/Prog.md +++ b/Prog.md @@ -11581,6 +11581,7 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引指向 //如果随机数的二进制与10000000000000000000000000000001进行与运算为0 //即随机数的二进制最高位与最末尾必须为0,其他位无所谓,就进入该循环 //如果随机数的二进制最高位与最末位不为0,不增加新节点的层数 + //11.判断是否需要添加level if ((rnd & 0x80000001) == 0) { //索引层level,从1开始 @@ -13917,12 +13918,10 @@ MappedByteBuffer 较之 ByteBuffer新增的三个方法 ```java public class MappedByteBufferTest { public static void main(String[] args) throws Exception { - //RandomAccessFile ra = (RandomAccess) new RandomAccessFile("1.txt", "rw"); - //FileChannel channel = ra.getChannel(); - - FileInputStream is = new FileInputStream("data01.txt"); + // 读写模式 + RandomAccessFile ra = (RandomAccess) new RandomAccessFile("1.txt", "rw"); //获取对应的通道 - FileChannel channel = is.getChannel(); + FileChannel channel = ra.getChannel(); /** * 参数1 FileChannel.MapMode.READ_WRITE 使用的读写模式 @@ -14154,7 +14153,7 @@ public class ChannelTest { #### 分散聚集 -分散读取(Scatter ):是指把Channel通道的数据读入到多个缓冲区中去 +分散读取(Scatter ):是指把 Channel 通道的数据读入到多个缓冲区中去 聚集写入(Gathering ):是指将多个 Buffer 中的数据“聚集”到 Channel。 @@ -14202,7 +14201,7 @@ public class ChannelTest { ![](https://gitee.com/seazean/images/raw/master/Java/NIO-Selector.png) -* Selector 能够检测多个注册的通道上是否有事件发生(多个 Channel 以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理,就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求 +* Selector 能够检测多个注册的通道上是否有事件发生(多个 Channel 以事件的方式可以注册到同一个 Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理,就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求 * 只有在连接/通道真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程 * 避免了多线程之间的上下文切换导致的开销 @@ -14224,8 +14223,7 @@ public class ChannelTest { * 写 : SelectionKey.OP_WRITE (4) * 连接 : SelectionKey.OP_CONNECT (8) * 接收 : SelectionKey.OP_ACCEPT (16) -* 若注册时不止监听一个事件,则可以使用“位或”操作符连接: - `int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE ` +* 若不止监听一个事件,可以使用“位或”操作符连接:`int interest = SelectionKey.OP_READ | SelectionKey.OP_WRITE` diff --git a/SSM.md b/SSM.md index b37ed61..490ee7f 100644 --- a/SSM.md +++ b/SSM.md @@ -3893,20 +3893,6 @@ Mybatis 核心配置文件消失 #### 注解驱动 -注解:启动时使用注解的形式替代 xml 配置,将 spring 配置文件从工程中消除,简化书写 - -缺点:为了达成注解驱动的目的,可能会将原先很简单的书写,变的更加复杂。XML中配置第三方开发的资源是很方便的,但使用注解驱动无法在第三方开发的资源中进行编辑,因此会增大开发工作量 - -![](https://gitee.com/seazean/images/raw/master/Frame/注解驱动示例.png) - - - -**** - - - -#### 启动注解 - ##### XML 启动注解扫描,加载类中配置的注解项: @@ -3918,19 +3904,17 @@ Mybatis 核心配置文件消失 说明: - 在进行包扫描时,会对配置的包及其子包中所有文件进行扫描,多个包采用`,`隔开 - - 扫描过程是以文件夹递归迭代的形式进行的 - - 扫描过程仅读取合法的 java 文件 - - 扫描时仅读取 spring 可识别的注解 - - 扫描结束后会将可识别的有效注解转化为 spring 对应的资源加入 IoC 容器 +- 从加载效率上来说注解优于 XML 配置文件 -注意: +注解:启动时使用注解的形式替代 xml 配置,将 spring 配置文件从工程中消除,简化书写 -- 无论是注解格式还是 XML 配置格式,最终都是将资源加载到 IoC 容器中,差别仅仅是数据读取方式不同 -- 从加载效率上来说注解优于 XML 配置文件 +缺点:为了达成注解驱动的目的,可能会将原先很简单的书写,变的更加复杂。XML 中配置第三方开发的资源是很方便的,但使用注解驱动无法在第三方开发的资源中进行编辑,因此会增大开发工作量 + +![](https://gitee.com/seazean/images/raw/master/Frame/注解驱动示例.png) @@ -3960,9 +3944,9 @@ public class SpringConfigClassName{ 说明: - 核心配合类用于替换 Spring 核心配置文件,此类可以设置空的,不设置变量与属性 -- bean 扫描工作使用注解 @ComponentScan 替代,多个包用`{}和,`隔开 +- bean 扫描工作使用注解 @ComponentScan 替代,多个包用 `{} 和 ,` 隔开 -加载纯注解格式上下文对象,需要使用**AnnotationConfigApplicationContext** +加载纯注解格式上下文对象,需要使用 **AnnotationConfigApplicationContext** ```java @Configuration @@ -4046,7 +4030,7 @@ public class MainTest { 类型:类注解,写在类定义上方 -作用:设置该类为Spring 管理的 bean +作用:设置该类为 Spring 管理的 bean 格式: @@ -8942,9 +8926,7 @@ retVal = invocation.proceed():**拦截器链驱动方法** #### Component -解析 @Component 和 @Service 都是常用的注解 - -**@Component 解析流程:** +@Component 解析流程: * 注解类启动容器的时,注册 ClassPathBeanDefinitionScanner 到容器,用来扫描 Bean 的相关信息 @@ -8981,7 +8963,7 @@ retVal = invocation.proceed():**拦截器链驱动方法** private Set scanCandidateComponents(String basePackage) {} ``` - * `String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX resolveBasePackage(basePackage) + '/' + this.resourcePattern` :将 package 转化为 ClassLoader 类资源搜索路径 packageSearchPath,例如:`com.wl.spring.boot` 转化为 `classpath*:com/wl/spring/boot/**/*.class` + * `String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX resolveBasePackage(basePackage) + '/' + this.resourcePattern` :将 package 转化为 ClassLoader 类资源搜索路径 packageSearchPath,例如:`com.sea.spring.boot` 转化为 `classpath*:com/sea/spring/boot/**/*.class` * `resources = getResourcePatternResolver().getResources(packageSearchPath)`:加载搜素路径下的资源 @@ -9009,6 +8991,10 @@ retVal = invocation.proceed():**拦截器链驱动方法** +参考文章:https://my.oschina.net/floor/blog/4325651 + + + *** From ad2135df38e997b87247177f866bcaa298004947 Mon Sep 17 00:00:00 2001 From: Seazean Date: Fri, 20 Aug 2021 20:49:52 +0800 Subject: [PATCH 032/168] Update Java Notes --- DB.md | 108 ++++++++++---------- Issue.md | 295 ------------------------------------------------------- Java.md | 87 +++++++++------- Prog.md | 62 ++++++------ SSM.md | 141 +++++++++++++------------- Web.md | 8 ++ 6 files changed, 221 insertions(+), 480 deletions(-) delete mode 100644 Issue.md diff --git a/DB.md b/DB.md index e9ac36c..5ddfabb 100644 --- a/DB.md +++ b/DB.md @@ -2173,7 +2173,7 @@ rollback segment 称为回滚段,每个回滚段中有 1024 个 undo log segme 隔离性让并发情形下的事务之间互不干扰: - 一个事务的写操作对另一个事务的写操作(写写):锁机制保证隔离性 -- 一个事务的写操作对另一个事务的读操作(读写):MVCC保证隔离性 +- 一个事务的写操作对另一个事务的读操作(读写):MVCC 保证隔离性 锁机制:事务在修改数据之前,需要先获得相应的锁,获得锁之后,事务便可以修改数据;该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁(详解见锁机制) @@ -7955,14 +7955,14 @@ Redis 基于 Reactor 模式开发了网络事件处理器,这个处理器被 * 文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器 -* 当被监听的套接字准备好执行连接应答 (accept)、读取 (read)、写入 (write)、关闭 (close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器会调用套接字之前关联好的事件处理器来处理事件 +* 当被监听的套接字准备好执行连接应答 (accept)、读取 (read)、写入 (write)、关闭 (close) 等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器会调用套接字之前关联好的事件处理器来处理事件 Redis 单线程也能高效的原因: * 纯内存操作 * 核心是基于非阻塞的 IO 多路复用机制 * 底层使用 C 语言实现,C 语言实现的程序距离操作系统更近,执行速度相对会更快 -* 单线程同时也避免了多线程的上下文频繁切换问题,预防了多线程可能产生的竞争问题 +* 单线程同时也**避免了多线程的上下文频繁切换问题**,预防了多线程可能产生的竞争问题 @@ -7986,9 +7986,11 @@ io-threads-do-reads yesCopy to clipboardErrorCopied io-threads 4 #官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程 ``` + -参考文章:https://blog.csdn.net/xp_xpxp/article/details/100999825 + +参考文章:https://mp.weixin.qq.com/s/dqmiR0ECf4lB6Y2OyK-dyA @@ -8638,7 +8640,7 @@ quicklist 实际上是 ziplist 和 linkedlist 的混合体,将 linkedlist 按 数据存储结构:能够保存大量的数据,高效的内部存储机制,便于查询 -set 类型:与 hash 存储结构哈希表完全相同,只是仅存储键不存储值(nil),所以添加,删除,查找的复杂度都是O(1)**,并且值是**不允许重复且无序**的, +set 类型:与 hash 存储结构哈希表完全相同,只是仅存储键不存储值(nil),所以添加,删除,查找的复杂度都是 O(1),并且**值是不允许重复且无序的** @@ -8688,7 +8690,7 @@ set 类型:与 hash 存储结构哈希表完全相同,只是仅存储键不 * 复制 ```sh - smove source destination member #将指定数据从原始集合中移动到目标集合中 + smove source destination member #将指定数据从原始集合中移动到目标集合中 ``` @@ -8806,9 +8808,9 @@ sorted_set类型:在 set 的存储结构基础上添加可排序字段,类 注意事项: -1. score保存的数据存储空间是64位,如果是整数范围是-9007199254740992~9007199254740992 -2. score保存的数据也可以是一个双精度的double值,基于双精度浮点数的特征可能会丢失精度,慎重使用 -3. sorted_set 底层存储还是基于 set 结构的,因此数据不能重复,如果重复添加相同的数据,score值将被反复覆盖,保留最后一次修改的结果 +1. score 保存的数据存储空间是 64 位,如果是整数范围是 -9007199254740992~9007199254740992 +2. score 保存的数据也可以是一个双精度的 double 值,基于双精度浮点数的特征可能会丢失精度,慎重使用 +3. sorted_set 底层存储还是基于 set 结构的,因此数据不能重复,如果重复添加相同的数据,score 值将被反复覆盖,保留最后一次修改的结果 @@ -8861,7 +8863,7 @@ Redis 使用跳跃表作为有序集合键的底层实现之一,如果一个 -个人笔记:JUC → 并发包 → ConcurrentSkipListMap详解跳跃表 +个人笔记:JUC → 并发包 → ConcurrentSkipListMap 详解跳跃表 参考文章:https://www.cnblogs.com/hunternet/p/11248192.html @@ -9199,9 +9201,9 @@ save 指令的执行会阻塞当前 Redis 服务器,直到当前 RDB 过程完 #### bgsave -指令:bgsave(bg是background,后台执行的意思) +指令:bgsave(bg 是 background,后台执行的意思) -配置redis.conf +配置 redis.conf ```sh stop-writes-on-bgsave-error yes|no #后台存储过程中如果出现错误,是否停止保存操作,默认yes @@ -9211,13 +9213,13 @@ rdbcompression yes|no rdbchecksum yes|no ``` -bgsave指令工作原理: +bgsave 指令工作原理: ![](https://gitee.com/seazean/images/raw/master/DB/Redis-bgsave工作原理.png) 流程:当执行 bgsave 的时候,客户端发出 bgsave 指令给到 redis 服务器,服务器返回后台已经开始执行的信息给客户端,同时使用 fork 函数创建一个子进程,让子进程去执行 save 相关的操作。持久化过程是先将数据写入到一个临时文件中,持久化操作结束再用这个临时文件**替换**上次持久化的文件,在这个过程中主进程是不进行任何 IO 操作的,这确保了极高的性能 -bgsave 分成两个过程:第一个是服务端收到指令直接告诉客户端开始执行;另外一个过程是一个子进程在完成后台的保存操作,操作完以后返回消息。**两个进程不相互影响**,所以在持久化期间 Redis 可以正常工作 +bgsave 分成两个过程:第一个是服务端收到指令直接告诉客户端开始执行;另外一个过程是 fork 的子进程在完成后台的保存操作,操作完以后返回消息。**两个进程不相互影响**,所以在持久化期间 Redis 可以正常工作 注意:bgsave 命令是针对 save 阻塞问题做的优化,Redis 内部所有涉及到 RDB 操作都采用 bgsave 的方式,save 命令可以放弃使用 @@ -9319,7 +9321,7 @@ AOF(append only file)持久化:以独立日志的方式记录每次写命 AOF 主要作用是解决了数据持久化的实时性,目前已经是 Redis 持久化的主流方式 AOF 写数据过程: - +![](https://gitee.com/seazean/images/raw/master/DB/Redis-AOF工作原理.png) @@ -9343,7 +9345,7 @@ dir #AOF持久化文件保存路径,与RDB持久化文件路径保持一 appendfsync always|everysec|no #AOF写数据策略:默认为everysec ``` -AOF 写数据三种策略(appendfsync): +AOF 持久化数据的三种策略(appendfsync): - always(每次):每次写入操作均同步到 AOF 文件中,**数据零误差,性能较低**,不建议使用。 @@ -9513,25 +9515,25 @@ AOF 和 RDB 同时开启,系统默认取 AOF 的数据(数据不会存在丢 fork() 函数创建一个子进程,子进程与父进程几乎是完全相同的进程,系统先给子进程分配资源,然后把原来的进程的所有数据都复制到子进程中,只有少数值与父进程的值不同,相当于克隆了一个进程 -在完成对其调用之后,会产生2个进程,且每个进程都会**从 fork() 的返回处开始执行**,这两个进程将执行相同的程序段,但是拥有各自不同的堆段,栈段,数据段,每个子进程都可修改各自的数据段,堆段,和栈段 +在完成对其调用之后,会产生 2 个进程,且每个进程都会**从 fork() 的返回处开始执行**,这两个进程将执行相同的程序段,但是拥有各自不同的堆段,栈段,数据段,每个子进程都可修改各自的数据段,堆段,和栈段 ```c #include pid_t fork(void); -// 父进程返回子进程的pid,子进程返回0,错误返回负值 +// 父进程返回子进程的pid,子进程返回0,错误返回负值,根据返回值的不同进行对应的逻辑处理 ``` fork 调用一次,却能够**返回两次**,可能有三种不同的返回值: -* 在父进程中,fork返回新创建子进程的进程ID -* 在子进程中,fork返回0 -* 如果出现错误,fork返回一个负值,错误原因: - * 当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN - * 系统内存不足,这时errno的值被设置为ENOMEM +* 在父进程中,fork 返回新创建子进程的进程 ID +* 在子进程中,fork 返回 0 +* 如果出现错误,fork 返回一个负值,错误原因: + * 当前的进程数已经达到了系统规定的上限,这时 errno 的值被设置为 EAGAIN + * 系统内存不足,这时 errno 的值被设置为 ENOMEM fpid 的值在父子进程中不同:进程形成了链表,父进程的 fpid 指向子进程的进程 id,因为子进程没有子进程,所以其 fpid 为0 -创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略 +创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的调度策略 每个进程都有一个独特(互不相同)的进程标识符 process ID,可以通过 getpid() 函数获得;还有一个记录父进程 pid 的变量,可以通过 getppid() 函数获得变量的值 @@ -9607,7 +9609,7 @@ int main(void) -在 p3224 和 p3225 执行完第二个循环后,main函数退出,进程死亡。所以 p3226,p3227 就没有父进程了,成为孤儿进程,所以 p3226 和 p3227 的父进程就被置为 ID 为 1的 init 进程(笔记 Tool -> Linux -> 进程管理详解) +在 p3224 和 p3225 执行完第二个循环后,main 函数退出,进程死亡。所以 p3226,p3227 就没有父进程了,成为孤儿进程,所以 p3226 和 p3227 的父进程就被置为 ID 为 1的 init 进程(笔记 Tool → Linux → 进程管理详解) 参考文章:https://blog.csdn.net/love_gaohz/article/details/41727415 @@ -9627,11 +9629,11 @@ fork() 调用之后父子进程的内存关系 -* 对于父进程的数据段,堆段,栈段中的各页,由于父子进程要相互独立,采用**写时复制**的技术,来最大化的提高内存以及内核的利用率 +* 对于父进程的数据段,堆段,栈段中的各页,由于父子进程要相互独立,采用**写时复制**的技术,来提高内存以及内核的利用率 在 fork 之后两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,**两者的虚拟空间不同,但其对应的物理空间是同一个**。当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间,如果两者的代码完全相同,代码段继续共享父进程的物理空间;而如果两者执行的代码不同,子进程的代码段也会分配单独的物理空间。 - fork之后内核会将子进程放在队列的前面,让子进程先执行,以免父进程执行导致写时复制,而后子进程再执行,因无意义的复制而造成效率的下降 + fork 之后内核会将子进程放在队列的前面,让子进程先执行,以免父进程执行导致写时复制,而后子进程再执行,因无意义的复制而造成效率的下降 @@ -9950,7 +9952,7 @@ TTL 返回的值有三种情况:正数,-1,-2 #### 逐出算法 -数据淘汰策略:当新数据进入redis时,在执行每一个命令前,会调用 **freeMemoryIfNeeded()** 检测内存是否充足。如果内存不满足新加入数据的最低存储要求,redis要临时删除一些数据为当前指令清理存储空间,清理数据的策略称为**逐出算法** +数据淘汰策略:当新数据进入 redis 时,在执行每一个命令前,会调用 **freeMemoryIfNeeded()** 检测内存是否充足。如果内存不满足新加入数据的最低存储要求,redis 要临时删除一些数据为当前指令清理存储空间,清理数据的策略称为**逐出算法** 注意:逐出数据的过程不是 100% 能够清理出足够的可使用的内存空间,如果不成功则反复执行,当对所有数据尝试完毕,如不能达到内存清理的要求,将出现错误信息如下: @@ -9968,7 +9970,7 @@ TTL 返回的值有三种情况:正数,-1,-2 影响数据淘汰的相关配置如下,配置 conf 文件: -* 最大可使用内存,即占用物理内存的比例,默认值为0,表示不限制。生产环境中根据需求设定,通常设置在50%以上 +* 最大可使用内存,即占用物理内存的比例,默认值为 0,表示不限制。生产环境中根据需求设定,通常设置在 50% 以上 ```sh maxmemory ?mb @@ -9986,9 +9988,9 @@ TTL 返回的值有三种情况:正数,-1,-2 maxmemory-policy policy ``` - 数据删除的策略policy:3类8种 + 数据删除的策略 policy:3 类 8 种 - 第一类:检测易失数据(可能会过期的数据集server.db[i].expires ): + 第一类:检测易失数据(可能会过期的数据集 server.db[i].expires ): ```sh volatile-lru #挑选最近最久未使用使用的数据淘汰 @@ -9997,7 +9999,7 @@ TTL 返回的值有三种情况:正数,-1,-2 volatile-random #任意选择数据淘汰 ``` - 第二类:检测全库数据(所有数据集server.db[i].dict ): + 第二类:检测全库数据(所有数据集 server.db[i].dict ): ```sh allkeys-lru #挑选最近最少使用的数据淘汰 @@ -10286,9 +10288,9 @@ TTL 返回的值有三种情况:正数,-1,-2 2. 数据同步阶段,master 发给 slave 信息可以理解 master是 slave 的一个客户端,主动向 slave 发送命令 - 3. 多个 slave 同时对 master 请求数据同步,master 发送的 RDB 文件增多,会对带宽造成巨大冲击,如果master 带宽不足,因此数据同步需要根据业务需求,适量错峰 + 3. 多个 slave 同时对 master 请求数据同步,master 发送的 RDB 文件增多,会对带宽造成巨大冲击,如果 master 带宽不足,因此数据同步需要根据业务需求,适量错峰 - 4. slave 过多时,建议调整拓扑结构,由一主多从结构变为树状结构,中间的节点既是 master,也是slave。注意使用树状结构时,由于层级深度,导致深度越高的 slave 与最顶层 master 间数据同步延迟较大,数据一致性变差,应谨慎选择 + 4. slave 过多时,建议调整拓扑结构,由一主多从结构变为树状结构,中间的节点既是 master,也是 slave。注意使用树状结构时,由于层级深度,导致深度越高的 slave 与最顶层 master 间数据同步延迟较大,数据一致性变差,应谨慎选择 @@ -10316,7 +10318,7 @@ TTL 返回的值有三种情况:正数,-1,-2 作用:用于在服务器间进行传输识别身份,如果想两次操作均对同一台服务器进行,必须每次操作携带对应的运行 ID,用于对方识别 - 实现:运行 ID 在每台服务器启动时自动生成,master 在首次连接 slave 时,将运行 ID 发送给 slave,slave保存此ID,通过 info Server 命令,可以查看节点的 runid + 实现:运行 ID 在每台服务器启动时自动生成,master 在首次连接 slave 时,将运行 ID 发送给 slave,slave 保存此 ID,通过 info Server 命令,可以查看节点的 runid * 复制缓冲区:复制积压缓冲区,是一个先进先出(FIFO)的队列,用于存储服务器执行过的命令 @@ -10373,19 +10375,19 @@ master 心跳任务: - 内部指令:PING - 周期:由 `repl-ping-slave-period` 决定,默认10秒 - 作用:判断 slave 是否在线 -- 查询:INFO replication 获取 slave 最后一次连接时间间隔,lag 项维持在0或1视为正常 +- 查询:INFO replication 获取 slave 最后一次连接时间间隔,lag 项维持在 0 或 1 视为正常 slave 心跳任务 - 内部指令:REPLCONF ACK {offset} - 周期:1秒 -- 作用:汇报 slave 自己的复制偏移量,获取最新的数据变更指令;判断master是否在线 +- 作用:汇报 slave 自己的复制偏移量,获取最新的数据变更指令;判断 master 是否在线 心跳阶段注意事项: * 当 slave 多数掉线,或延迟过高时,master 为保障数据稳定性,将拒绝所有信息同步 - slave 数量少于2个,或者所有 slave 的延迟都大于等于8秒时,强制关闭 master 写功能,停止数据同步 + slave 数量少于 2 个,或者所有 slave 的延迟都大于等于 8 秒时,强制关闭 master 写功能,停止数据同步 ```sh min-slaves-to-write 2 @@ -10902,7 +10904,7 @@ Cache Aside Pattern 中服务端需要同时维系 DB 和 cache,并且是以 D 时序导致的不一致问题: -* 在写数据的过程中,不能先删除 cache 再更新 DB,因为会造成缓存的不一致。比如请求 1 先写数据 A,请求 2 随后读数据 A,当请求 1 删除 cache 后,请求 2 直接读取了 DB,此时请求 1 还没写入 DB +* 在写数据的过程中,不能先删除 cache 再更新 DB,因为会造成缓存的不一致。比如请求 1 先写数据 A,请求 2 随后读数据 A,当请求 1 删除 cache 后,请求 2 直接读取了 DB,此时请求 1 还没写入 DB(延迟双删) * 在写数据的过程中,先更新 DB 再删除 cache 也会出现问题,但是概率很小,因为缓存的写入速度非常快 @@ -10949,10 +10951,6 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 -参考文章:https://snailclimb.gitee.io/javaguide - - - **** @@ -10965,7 +10963,7 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 * 数据库和缓存数据**强一致**场景 : * 更新 DB 时同样更新 cache,加一个锁来保证更新 cache 时不存在线程安全问题,这样可以增加命中率 - * 延迟双删:先淘汰缓存再写数据库,休眠1秒再次淘汰缓存,可以将1秒内造成的缓存脏数据再次删除 + * 延迟双删:先淘汰缓存再写数据库,休眠1秒再次淘汰缓存,可以将 1 秒内造成的缓存脏数据再次删除 * CDC 同步:通过 canal 订阅 MySQL binlog 的变更上报给 Kafka,系统监听 Kafka 消息触发缓存失效 * 可以短暂地允许数据库和缓存数据**不一致**场景:更新 DB 的时候同样更新 cache,但是给缓存加一个比较短的过期时间,这样就可以保证即使数据不一致影响也比较小 @@ -10987,7 +10985,7 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 问题排查: -1. 请求数量较高,大量的请求过来之后都需要去从缓存中获取数据,但是缓存中又没有,此时从数据库中查找数据然后将数据再存入缓存,造成了短期内对redis的高强度操作从而导致问题 +1. 请求数量较高,大量的请求过来之后都需要去从缓存中获取数据,但是缓存中又没有,此时从数据库中查找数据然后将数据再存入缓存,造成了短期内对 redis 的高强度操作从而导致问题 2. 主从之间数据吞吐量较大,数据同步操作频度较高 @@ -10997,11 +10995,11 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 1. 日常例行统计数据访问记录,统计访问频度较高的热点数据 - 2. 利用 LRU 数据删除策略,构建数据留存队列例如:storm与kafka配合 + 2. 利用 LRU 数据删除策略,构建数据留存队列例如:storm 与 kafka 配合 - 准备工作: - 1. 将统计结果中的数据分类,根据级别,redis优先加载级别较高的热点数据 + 1. 将统计结果中的数据分类,根据级别,redis 优先加载级别较高的热点数据 2. 利用分布式多服务器同时进行数据读取,提速数据加载过程 @@ -11011,7 +11009,7 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 4. 使用脚本程序固定触发数据预热过程 - 5. 如果条件允许,使用了CDN(内容分发网络),效果会更好 + 5. 如果条件允许,使用了 CDN(内容分发网络),效果会更好 总的来说:缓存预热就是系统启动前,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据! @@ -11025,7 +11023,7 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 场景:数据库服务器崩溃,一连串的问题会随之而来。系统平稳运行过程中,忽然数据库连接量激增,应用服务器无法及时处理请求,大量 408,500 错误页面出现,客户反复刷新页面获取数据,造成数据库崩溃、应用服务器崩溃、重启应用服务器无效、Redis 服务器崩溃、Redis 集群崩溃、重启数据库后再次被瞬间流量放倒 -问题排查:在一个较短的时间内,缓存中**较多的 key 集中过期**,此周期内请求访问过期的数据 Redis 未命中,Redis 向数据库获取数据,数据库同时收到大量的请求无法及时处理。 +问题排查:在一个较短的时间内,**缓存中较多的 key 集中过期**,此周期内请求访问过期的数据 Redis 未命中,Redis 向数据库获取数据,数据库同时收到大量的请求无法及时处理。 解决方案: @@ -11068,7 +11066,7 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 问题排查: -1. Redis 中某个 key 过期,该 key 访问量巨大 +1. **Redis 中某个 key 过期,该 key 访问量巨大** 2. 多个数据请求从服务器直接压到 Redis 后,均未命中 @@ -11080,11 +11078,11 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 1. 预先设定:以电商为例,每个商家根据店铺等级,指定若干款主打商品,在购物节期间,加大此类信息 key 的过期时长 注意:购物节不仅仅指当天,以及后续若干天,访问峰值呈现逐渐降低的趋势 -2. 现场调整:监控访问量,对自然流量激增的数据延长过期时间或设置为永久性 key +2. 现场调整:监控访问量,对自然流量激增的数据**延长过期时间或设置为永久性 key** 3. 后台刷新数据:启动定时任务,高峰期来临之前,刷新数据有效期,确保不丢失 -4. 二级缓存:设置不同的失效时间,保障不会被同时淘汰就行 +4. **二级缓存**:设置不同的失效时间,保障不会被同时淘汰就行 5. 加锁:分布式锁,防止被击穿,但是要注意也是性能瓶颈,慎重! @@ -11115,11 +11113,11 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 解决方案: -1. 缓存 null:对查询结果为null的数据进行缓存 (长期使用,定期清理) ,设定短时限,例如30-60秒,最高5分钟 +1. 缓存 null:对查询结果为 null 的数据进行缓存 (长期使用,定期清理) ,设定短时限,例如 30-60 秒,最高 5 分钟 2. 白名单策略:提前预热各种分类数据 id 对应的 **bitmaps**,id 作为 bitmaps 的 offset,相当于设置了数据白名单。当加载正常数据时放行,加载异常数据时直接拦截(效率偏低),也可以使用布隆过滤器(有关布隆过滤器的命中问题对当前状况可以忽略) -3. 实时监控:实时监控 redis 命中率(业务正常范围时,通常会有一个波动值)与null数据的占比 +3. 实时监控:实时监控 redis 命中率(业务正常范围时,通常会有一个波动值)与 null 数据的占比 * 非活动时段波动:通常检测3-5倍,超过5倍纳入重点排查对象 * 活动时段波动:通常检测10-50倍,超过50倍纳入重点排查对象 @@ -11130,6 +11128,8 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 总的来说:缓存击穿是指访问了不存在的数据,跳过了合法数据的 redis 数据缓存阶段,每次访问数据库,导致对数据库服务器造成压力。通常此类数据的出现量是一个较低的值,当出现此类情况以毒攻毒,并及时报警。无论是黑名单还是白名单,都是对整体系统的压力,警报解除后尽快移除 + + 参考视频:https://www.bilibili.com/video/BV15y4y1r7X3 diff --git a/Issue.md b/Issue.md deleted file mode 100644 index 16e3d57..0000000 --- a/Issue.md +++ /dev/null @@ -1,295 +0,0 @@ -# Base - -## Algorithm - -排序类问题: - -* 海量数据排序: - * 外部排序:归并 + 败者树 - * 基数排序:https://time.geekbang.org/column/article/42038 -* 海量数据查询: - * 布隆过滤器判断是否存在 - * 构建索引:B+ 树、跳表 - - - - - - - -*** - - - -## Network - -### 传输层 - - - -四次挥手 - - - -* **TCP和UDP的区别?** - - * 用户数据报协议 UDP(User Datagram Protocol)是无连接的,尽最大可能交付,没有拥塞控制,面向报文(对于应用程序传下来的报文不合并也不拆分,只是添加 UDP 首部),支持一对一、一对多、多对一和多对多的交互通信 - - 传输控制协议 TCP(Transmission Control Protocol)是面向连接的,提供可靠交付,有流量控制,拥塞控制,提供全双工通信,面向字节流(把应用层传下来的报文看成字节流,把字节流组织成大小不等的数据块),每一条 TCP 连接只能是点对点的(一对一) - - 高手解答: - - * 从tcp udp报文格式就看出差别:tcp头部比udp头部字节更多,说明相同情况下控制开销更多,在一定时间内,传输数据的时延更大,所以tcp不适合用于即时场景 - - * 从TCP报文格式还可以看出,tcp存在多个控制位,意味着会交互更多的控制信息;从控制位字段可以看出,比如syn fin ack,tcp会发送控制消息进行握手,这样一来传输信息更加可靠,是面向连接的;窗口位,意味着tcp有拥塞控制优势 - * udp报文头部有数据长度字段,而tcp没有,只有头部偏移字段,意味着udp是一包一包数据传输,发端和收端不会分片或者重组,能很快识别这包数据;而tcp是流,每次是个数据块,也就是沾包,这是tcp独有的特性 - * 任何一个协议的机制有优点肯定有缺点,就像tcp,发送数据前增加了握手,虽然保证了可靠性,但是同样带来了更多的控制开销和数据时延,怎样平衡它们的优缺点就是看应用场景;任何一个协议,它的特点或者想实现什么功能,最终都会体现在报文格式或者底层叫做帧格式上面 - - - - -* **描述三次握手的过程** - - 假设 A 为客户端,B 为服务器端。 - - - 首先 B 处于 LISTEN(监听)状态,等待客户的连接请求。 - - A 向 B 发送连接请求报文,SYN=1,ACK=0,选择一个初始的序号 x。 - - B 收到连接请求报文,如果同意建立连接,则向 A 发送连接确认报文,SYN=1,ACK=1,确认号为 x+1,同时也选择一个初始的序号 y。 - - A 收到 B 的连接确认报文后,还要向 B 发出确认,确认号为 y+1,序号为 x+1。 - - B 收到 A 的确认后,连接建立 - -* **为什么要进行三次握手?** - - **原因一**:TCP 是全双工可靠的传输协议,全双工意味着双方能够同时向对方发送数据,可靠意味着我发送的数据必须确认对方完整收到。TCP 通过序列号来保证这两种性质的,**三次握手就是互换序列号**的一次过程,可以确保双方的发信和收信能力都是正常的 - - **原因二**:第三次握手是为了防止失效的连接请求到达服务器,让服务器错误打开连接。客户端发送的连接请求如果在网络中滞留,那么就会隔很长一段时间才能收到服务器端发回的连接确认。客户端等待一个超时重传时间之后,就会重新请求连接。但是这个滞留的连接请求最后还是会到达服务器,如果不进行三次握手,那么服务器就会打开两个连接。如果有第三次握手,客户端会忽略服务器之后发送的对滞留连接请求的连接确认,不进行第三次握手,因此就不会再次打开连接 - - - -* **三次握手的第三次握手发送ACK能携带数据吗?** - - 答:可以。第三次握手,在客户端发送完ACK报文后,就进入 ESTABLISHED 状态,当服务器收到这个,服务器变为ESTABLISHED状态,可以直接处理携带的数据。 - -* **不携带数据的 ACK 不会超时重传** - - - -* **为什么 TCP4 次挥手时等待为 2MSL?** - - **原因一:**A 发送完释放连接的应答并不知道 B 是否接到自己的 ACK,所以有两种情况 - 1)如果 B 没有收到自己的 ACK,会超时重传 FIN,那么 A 再次接到重传的 FIN,会再次发送 ACK - 2)如果 B 收到自己的 ACK,也不会再发任何消息,包括 ACK - 无论是 1 还是 2,A都需要等待,要取这两种情况等待时间的最大值,以应对最坏的情况发生,这个最坏情况是:去向ACK消息最大存活时间(MSL) + 来向 FIN 消息的最大存活时间(MSL), - 这就是**2MSL( Maximum Segment Life)**。等待 2MSL 时间,A 就可以放心地释放 TCP 占用的资源、端口号,此时可以使用该端口号连接任何服务器。 - - **原因二:**等待一段时间是为了让本次连接持续时间内所产生的报文都从网络中消失,否则存活在网络里的老的TCP报文可能与新TCP连接报文产生冲突(比如连接同一个端口),为避免此种情况,需要耐心等待网络老的 TCP 连接的活跃报文全部消失,2MSL 时间可以满足这个需求(尽管非常保守) - - - -* **为什么连接的时候是三次握手,关闭的时候却是四次握手?** - - 答:因为当 Server 端收到 Client 端的 SYN 连接请求报文后,可以直接发送 SYN+ACK 报文。其中 ACK 报文是用来应答的,SYN 报文是用来同步的。但是关闭连接时,当 Server 端收到 FIN 报文时,很可能数据还没有处理完成,所以只能先回复一个 ACK 报文,告诉 Client 端,"你发的 FIN 报文我收到了"。只有等到 Server 端所有的报文都发送完了,才能发送 FIN 报文,因此不能一起发送。故需要四步握手 - - - -* TCP 协议如何保证可靠传输? - - - - - -*** - - - -### 应用层 - -* 从浏览器地址栏输入 URL 到请求返回发生了什么? - * 进行 URL 解析,进行编码 - * DNS 解析,顺序是先查 hosts 文件是否有记录,有的话就会把相对应映射的 IP 返回,然后去本地 DNS 缓存中寻找,然后去找计算机上配置的 DNS 服务器上有或者有缓存,最后去找全球的根 DNS 服务器,直到查到为止 - * 查找到 IP 之后,进行 TCP 协议的三次握手,发出 HTTP 请求 - * 服务器处理请求,返回响应 - * 浏览器解析渲染页面 - - - -*** - - - - - -## System - -### 操作系统 - -* 操作系统? - - 控制和管理计算机硬件与软件资源的,并合理的组织和调度计算机工作的程序 - - 特征:并发、异步、共享、虚拟 - -* 什么是系统调度? - - 在用户程序中调用操作系统提供的核心态级别的子功能,结合用户态和核心态区别回答,一般使用陷入(trap),按调用功能分为:设备管理、文件管理、进程控制、进程通信、内存管理 - - - -*** - - - -### 进程线程 - -* 进程线程? - - 进程:程序是静止的,进程是程序的一次执行过程,是系统资源分配的基本单位 - - 线程:轻量级进程,是CPU的执行单元,是独立调度的最小单位,只拥有一点必不可少的资源 - - 关系:一个进程中包含多个线程,线程之间共享进程的资源,进程之间是相互独立 - - 区别:资源、并发、切换、通信、 - - 进程特征:并发、异步、动态、独立 - -* 进程通信的方式? - - 同一台计算机的进程通信称为 IPC(Inter-process communication) - - * 信号量:信号量是一个计数器,用于多进程对共享数据的访问,解决同步相关的问题并避免竞争条件 - * 共享存储:多个进程可以访问同一块内存空间,需要使用信号量用来同步对共享存储的访问 - * 管道通信:管道是用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,pipe文件 - * 匿名管道(Pipes) :用于具有亲缘关系的父子进程间或者兄弟进程之间的通信,只支持半双工通信 - * 命名管道(Names Pipes):以磁盘文件的方式存在,可以实现本机任意两个进程通信,遵循FIFO - * 消息队列:内核中存储消息的链表,由消息队列标识符标识,能在不同进程之间提供全双工通信,对比管道: - * 匿名管道存在于内存中的文件;命名管道存在于实际的磁盘介质或者文件系统;消息队列存放在内核中,只有在内核重启(操作系统重启)或者显示地删除一个消息队列时,该消息队列才被真正删除 - * 读进程可以根据消息类型有选择地接收消息,而不像 FIFO 那样只能默认地接收 - - 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP - - * 套接字:与其它通信机制不同的是,它可用于不同机器间的进程通信 - -* 临界资源? - - 临界资源:一次允许一个进程使用的资源 - - 临界区:访问临界资源的代码,必须互斥的进行 - - * 同步:多个进程先后执行关系 - * 互斥:多个进程在同一时刻只有一个进程能进入临界区 - -* 线程间的同步的方式有哪些呢? - - 信号量(Semphares) :是一个整型变量,可以对其执行 down 和 up 操作,也就是常见的 P 和 V 操作, - - * down 和 up 操作需要被设计成原语,不可分割,通常的做法是在执行这些操作的时候屏蔽中断 - * 如果信号量的取值只能为 0 或者 1,那么就成为了 互斥量(Mutex) - - 管程:Java 中的 synchronized - -* 进程状态转换: - ![](https://gitee.com/seazean/images/raw/master/Issue/OS-进程状态转换.jpg) - -* 死锁问题: - - 预防死锁: - - * 破坏互斥条件:有些资源必须互斥使用,无法破环互斥条件 - * 破坏不剥夺条件:增加系统开销,降低吞吐量 - * 破坏请求和保持条件:严重浪费系统资源,还可能导致饥饿现象 - * 破坏循环等待条件:浪费系统资源,并造成编程不便 - - 避免死锁: - - * 安全状态:能找到一个分配资源的序列能让所有进程都顺序完成 - * 银行家算法:采用预分配策略检查分配完成时系统是否处在安全状态 - - 检测死锁:利用死锁定理化简资源分配图以检测死锁的存在 - - 解除死锁: - - * 资源剥夺法:挂起某些死锁进程并抢夺它的资源,以便让其他进程继续推进 - * 撤销进程法:强制撤销部分、甚至全部死锁进程并剥夺这些进程的资源 - * 进程回退法:让一个或多个进程回退到足以回避死锁的地步 - - - - - - - -**** - - - -### 内存管理 - -* 操作系统的内存管理主要是做什么? - - 操作系统的内存管理主要负责内存的分配与回收,地址转换也就是将逻辑地址转换成相应的物理地址 - -* 内存管理有哪几种方式? - - 连续分配管理方式:块式管理,将内存分为几个固定大小的块,每个块中只包含一个进程 - - 非连续分配管理方式:分页存储,分段存储,段页式管理 - -* 分页机制和分段机制有哪些共同点和区别呢? - - 共同点 : - - - 分页机制和分段机制都是为了提高内存利用率,较少内存碎片 - - 分页、段页式:内部碎片 - - - 以两者都是离散分配内存的方式。但是,每个页和段中的内存是连续的 - - 不同点: - - * 分页对程序员是透明的,但是分段需要程序员显式划分每个段 - * 分页是一维地址空间,分段是二维地址空间 - * 页的大小是固定的,由操作系统决定;而段的大小不固定,取决于我们当前运行的程序 - * 分页主要用于实现虚拟内存,从而获得更大的地址空间;分段主要是为了使程序和数据可以被划分为逻辑上独立的地址空间并且有助于共享和保护 - -* 快表和多级页表 - - 快表:虚拟地址到物理地址的转换要快 - - * CPU给出逻辑地址,地址转换后先去快表(高速缓存寄存器)中查询,如果有就直接读取物理地址 - * 如果没有就去访问主存中的页表,读出以后同时存入快表 - * 当快表填满,就按照淘汰策略淘汰旧的页表项 - - 多级页表:为了避免把全部页表一直放在内存中占用过多空间,特别是那些根本就不需要的页表就不需要保留在内存中 - -* 什么是CPU寻址? - - 现代处理器使用的是一种称为虚拟寻址(Virtual Addressing)的寻址方式。使用虚拟寻址 CPU 将虚拟(逻辑)地址翻译成物理地址,这样才能访问到真实的物理内存。 实际上完成虚拟地址转换为物理地址转换的硬件是 CPU 中含有一个被称为内存管理单元(Memory Management Unit, MMU)的硬件 - - 虚拟地址空间好处:防止用户程序可以访问任意内存,寻址内存的每个字节,这样很容易破坏操作系统,造成操作系统崩溃 - -* **局部性原理**? - - 时间局部性 :如果程序中的某条指令一旦执行,不久以后该指令可能再次执行;如果某数据被访问过,不久以后该数据可能再次被访问。产生时间局部性的典型原因,是由于在程序中存在着大量的循环操作 - - 空间局部性 :一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也将被访问,即程序在一段时间内所访问的地址,可能集中在一定的范围之内,这是因为指令通常是顺序存放、顺序执行的,数据也一般是以向量、数组、表等形式簇聚存储的 - -* 虚拟存储器? - - 虚拟存储器又叫做虚拟内存,都是 Virtual Memory 的翻译,属于同一个概念 - - 基于局部性原理,在程序装入时,可以将程序的一部分装入内存,而将其他部分留在外存,就可以启动程序执行;由于外存往往比内存大很多,所以我们运行的软件的内存大小实际上是可以比计算机系统实际的内存大小大的。在程序执行过程中,当所访问的信息不在内存时,由操作系统将所需要的部分调入内存,然后继续执行程序;另一方面,操作系统将内存中暂时不使用的内容换到外存上,从而腾出空间存放将要调入内存的信息。这样,计算机好像为用户提供了一个比实际内存大的多的存储器就是**虚拟存储器** - - 因为这中存储器实际上不存在,只是系统提供了部分载入、请求调入和置换功能后,是对用户透明的 - -* 虚拟内存技术的实现呢? - - 请求分页存储管理、请求分段存储管理、请求段页式存储管理 - - 请求分页与分页存储管理的不同点:根本区别是是否将程序全部所需的全部地址空间都装入主存 - - * 在载入程序的时候,只需要将程序的一部分装入内存,而将其他部分留在外存,然后程序就可以执行了 - - * **缺页中断**:如果需执行的指令或访问的数据尚未在内存(称为缺页或缺段),则由处理器通知操作系统将相应的页面或段调入到内存,然后继续执行程序; - - * 虚拟地址空间:逻辑地址到物理地址的变换 \ No newline at end of file diff --git a/Java.md b/Java.md index a6eff40..5f53850 100644 --- a/Java.md +++ b/Java.md @@ -7284,8 +7284,8 @@ FileReader:文件字符输入流 `public FileReader(File file)` : 创建一个字符输入流与源文件对象接通。 `public FileReader(String filePath)` : 创建一个字符输入流与源文件路径接通。 * 方法: - `public int read()` : 读取一个字符的编号返回! 读取完毕返回-1 - `public int read(char[] buffer)` : 读取一个字符数组,读取多少个就返回多少个,读取完毕返回-1 + `public int read()` : 读取一个字符的编号返回! 读取完毕返回 -1 + `public int read(char[] buffer)` : 读取一个字符数组,读取多少个就返回多少个,读取完毕返回 -1 * 结论: 字符流一个一个字符的读取文本内容输出,可以解决中文读取输出乱码的问题,适合操作文本文件。 但是:一个一个字符的读取文本内容性能较差!! @@ -7335,10 +7335,10 @@ FileWriter:文件字符输出流 * 作用:以内存为基准,把内存中的数据按照字符的形式写出到磁盘文件中去 * 构造器: - `public FileWriter(File file)` : 创建一个字符输出流管道通向目标文件对象。 - `public FileWriter(String filePath)` : 创建一个字符输出流管道通向目标文件路径。 + `public FileWriter(File file)` : 创建一个字符输出流管道通向目标文件对象 + `public FileWriter(String filePath)` : 创建一个字符输出流管道通向目标文件路径 `public FileWriter(File file,boolean append)` : 创建一个追加数据的字符输出流管道通向文件对象 - `public FileWriter(String filePath,boolean append)` : 创建一个追加数据的字符输出流管道通向目标文件路径。 + `public FileWriter(String filePath,boolean append)` : 创建一个追加数据的字符输出流管道通向目标文件路径 * 方法: `public void write(int c)` : 写一个字符出去 `public void write(String c)` : 写一个字符串出去 @@ -7346,10 +7346,10 @@ FileWriter:文件字符输出流 `public void write(String c ,int pos ,int len)` : 写字符串的一部分出去 `public void write(char[] buffer ,int pos ,int len)` : 写字符数组的一部分出去 * 说明: - 覆盖数据管道:`Writer fw = new FileWriter("Day10Demo/src/dlei03.txt"); ` - 追加数据管道:`Writer fw = new FileWriter("Day10Demo/src/dlei03.txt",true);` - 换行:fw.write("\r\n"); // 换行 - 读写字符文件数据建议使用字符流。 + 覆盖数据管道:`Writer fw = new FileWriter("Day10Demo/src/dlei03.txt")` + 追加数据管道:`Writer fw = new FileWriter("Day10Demo/src/dlei03.txt",true)` + 换行:fw.write("\r\n"); // 换行 + 读写字符文件数据建议使用字符流 ```java Writer fw = new FileWriter("Day10Demo/src/dlei03.txt"); @@ -7373,10 +7373,11 @@ fw.close; 作用:缓冲流可以提高字节流和字符流的读写数据的性能。 缓冲流分为四类: - (1)BufferedInputStream:字节缓冲输入流,可以提高字节输入流读数据的性能。 - (2)BufferedOutStream: 字节缓冲输出流,可以提高字节输出流写数据的性能。 - (3)BufferedReader: 字符缓冲输入流,可以提高字符输入流读数据的性能。 - (4)BufferedWriter: 字符缓冲输出流,可以提高字符输出流写数据的性能。 + +* BufferedInputStream:字节缓冲输入流,可以提高字节输入流读数据的性能。 +* BufferedOutStream: 字节缓冲输出流,可以提高字节输出流写数据的性能。 +* BufferedReader: 字符缓冲输入流,可以提高字符输入流读数据的性能。 +* BufferedWriter: 字符缓冲输出流,可以提高字符输出流写数据的性能。 @@ -7444,10 +7445,11 @@ public class BufferedOutputStreamDemo02 { 利用字节流的复制统计各种写法形式下缓冲流的性能执行情况。 复制流: - (1)使用低级的字节流按照一个一个字节的形式复制文件。 - (2)使用低级的字节流按照一个一个字节数组的形式复制文件。 - (3)使用高级的缓冲字节流按照一个一个字节的形式复制文件。 - (4)使用高级的缓冲字节流按照一个一个字节数组的形式复制文件。 + +* 使用低级的字节流按照一个一个字节的形式复制文件。 +* 使用低级的字节流按照一个一个字节数组的形式复制文件。 +* 使用高级的缓冲字节流按照一个一个字节的形式复制文件。 +* 使用高级的缓冲字节流按照一个一个字节数组的形式复制文件。 高级的缓冲字节流按照一个一个字节数组的形式复制文件,性能最高,建议使用 @@ -14502,7 +14504,7 @@ public class GeneratedMethodAccessor1 extends MethodAccessorImpl { #### 性能指标 -性能指标主要是吞吐量、响应时间、QPS、TPS 等、并发用户数等,而这些性能指标又依赖于系统服务器的资源,如 CPU、内存、磁盘IO、网络IO 等,对于这些指标数据的收集,通常可以根据Java本身的工具或指令进行查询 +性能指标主要是吞吐量、响应时间、QPS、TPS 等、并发用户数等,而这些性能指标又依赖于系统服务器的资源,如 CPU、内存、磁盘 IO、网络 IO 等,对于这些指标数据的收集,通常可以根据Java本身的工具或指令进行查询 几个重要的指标: @@ -14529,11 +14531,11 @@ public class GeneratedMethodAccessor1 extends MethodAccessorImpl { * 打印 GC 日志,通过 GCviewe r或者 http://gceasy.io 来分析异常信息 - - 运用命令行工具、jstack、jmap、jinfo等 + - 运用命令行工具、jstack、jmap、jinfo 等 - dump 出堆文件,使用内存分析工具分析文件 - - 使用阿里 Arthas、jconsole、JVisualVM 来实时查看 JVM 状态 + - 使用阿里 Arthas、jconsole、JVisualVM 来**实时查看 JVM 状态** - jstack 查看堆栈信息 @@ -14837,7 +14839,7 @@ jstatd 是一个 RMI 服务端程序,相当于代理服务器,建立本地 ### GUI工具 -工具的使用此处不再多言,推荐一个写的非常好的文章,JVM 调优部分的笔记全部参考此文章编写。 +工具的使用此处不再多言,推荐一个写的非常好的文章,JVM 调优部分的笔记全部参考此文章编写 视频链接:https://www.bilibili.com/video/BV1PJ411n7xZ?p=304 @@ -15129,15 +15131,15 @@ Full GC 日志: - System:调用了 System.gc() 方法 -通过日志看 GC 前后情况:GC 前内存占用 -> GC 后内存占用(该区域内存总大小) +通过日志看 GC 前后情况:GC 前内存占用 → GC 后内存占用(该区域内存总大小) ```sh [PSYoungGen: 5986K->696K (8704K)] 5986K->704K (9216K) ``` -- 中括号内:GC 回收前年轻代堆大小 -> 回收后大小(年轻代堆总大小) +- 中括号内:GC 回收前年轻代堆大小 → 回收后大小(年轻代堆总大小) -- 括号外:GC 回收前年轻代和老年代大小 -> 回收后大小(年轻代和老年代总大小) +- 括号外:GC 回收前年轻代和老年代大小 → 回收后大小(年轻代和老年代总大小) * Minor GC 堆内存总容量 = 9/10 年轻代 + 老年代,Survivor 区只计算 from 部分,而 JVM 默认年轻代中 Eden 区和 Survivor 区的比例关系:Eden:S0:S1=8:1:1 @@ -15159,7 +15161,7 @@ Full GC 日志: #### 分析工具 -GCEasy 是一款在线的GC日志分析器,可以通过 GC 日志分析进行内存泄露检测、GC 暂停原因分析、JVM 配置建议优化等功能,大多数功能是免费的 +GCEasy 是一款在线的 GC 日志分析器,可以通过 GC 日志分析进行内存泄露检测、GC 暂停原因分析、JVM 配置建议优化等功能,大多数功能是免费的 * 官网地址:https://gceasy.io/ @@ -15933,7 +15935,9 @@ public class BucketSort { -### 稳定性 +### 算法总结 + +#### 稳定性 稳定性:在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中 `r[i]=r[j]`,且 r[i] 在 r[j] 之前,而在排序后的序列中,r[i] 仍在 r[j] 之前,则称这种排序算法是稳定的,否则称为不稳定的 @@ -15962,12 +15966,27 @@ public class BucketSort { -### 算法对比 +#### 算法对比 ![](https://gitee.com/seazean/images/raw/master/Java/Sort-排序算法对比.png) +*** + + + +#### 补充问题 + +海量数据问题: + +* 海量数据排序: + * 外部排序:归并 + 败者树 + * 基数排序:https://time.geekbang.org/column/article/42038 +* 海量数据查询: + * 布隆过滤器判断是否存在 + * 构建索引:B+ 树、跳表 + *** @@ -16402,10 +16421,10 @@ public class Kmp { 红黑树与 AVL 树的比较: -* AVL 树是更加严格的平衡,可以提供更快的查找速度,适用于读取查找密集型任务 -* 红黑树只是做到了近似平衡,并不是严格的平衡,红黑树的插入删除比 AVL 树更便于控制操作,红黑树更适合于插入修改密集型任务 +* AVL 树是更加严格的平衡,可以提供更快的查找速度,适用于读取**查找密集型任务** +* 红黑树只是做到近似平衡,并不是严格的平衡,红黑树的插入删除比 AVL 树更便于控制,红黑树更适合于**插入修改密集型任务** -- 红黑树整体性能略优于 AVL 树,AVL 树的旋转比红黑树的旋转多,更加难以平衡和调试,插入和删除的效率比红黑树慢。 +- 红黑树整体性能略优于 AVL 树,AVL 树的旋转比红黑树的旋转多,更加难以平衡和调试,插入和删除的效率比红黑树慢 ![红黑树](https://gitee.com/seazean/images/raw/master/Java/红黑树结构图.png) @@ -17500,7 +17519,7 @@ UML 从目标系统的不同角度出发,定义了用例图、类图、对象 #### 破坏单例 -##### 序列化 +##### 反序列化 将单例对象序列化再反序列化,对象从内存反序列化到程序中会重新创建一个对象,通过反序列化得到的对象是不同的对象,而且得到的对象不是通过构造器得到的,**反序列化得到的对象不执行构造器** @@ -17537,7 +17556,7 @@ UML 从目标系统的不同角度出发,定义了用例图、类图、对象 private static Singleton readObjectFromFile() throws Exception { //创建对象输入流对象 - ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C:\a.txt")); + ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C://a.txt")); //第一个读取Singleton对象 Singleton instance = (Singleton) ois.readObject(); return instance; @@ -17547,7 +17566,7 @@ UML 从目标系统的不同角度出发,定义了用例图、类图、对象 //获取Singleton类的对象 Singleton instance = Singleton.getInstance(); //创建对象输出流 - ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\a.txt")); + ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C://a.txt")); //将instance对象写出到文件中 oos.writeObject(instance); } @@ -17600,7 +17619,7 @@ UML 从目标系统的不同角度出发,定义了用例图、类图、对象 -##### 反射 +##### 反射破解 * 反射 diff --git a/Prog.md b/Prog.md index 2e1c29d..c4aea9d 100644 --- a/Prog.md +++ b/Prog.md @@ -6890,7 +6890,7 @@ public ScheduledThreadPoolExecutor(int corePoolSize) { } ``` -* 定时任务 scheduleAtFixedRate:**一个任务的启动到下一个任务的启动**之间只要大于间隔时间,抢占到CPU就会立即执行 +* 定时任务 scheduleAtFixedRate:**一个任务的启动到下一个任务的启动**之间只要大于间隔时间,抢占到 CPU 就会立即执行 ```java public static void main(String[] args) { @@ -6909,7 +6909,7 @@ public ScheduledThreadPoolExecutor(int corePoolSize) { running...Sat Apr 24 18:08:17 CST 2021 ``` -* 定时任务 scheduleWithFixedDelay:**一个任务的结束到下一个任务的启动之间**等于间隔时间,抢占到CPU就会立即执行,这个方法才是真正的设置两个任务之间的间隔 +* 定时任务 scheduleWithFixedDelay:**一个任务的结束到下一个任务的启动之间**等于间隔时间,抢占到 CPU 就会立即执行,这个方法才是真正的设置两个任务之间的间隔 ```java public static void main(String[] args){ @@ -12256,7 +12256,7 @@ final void updateHead(Node h, Node p) { 3. 端口:端口号就可以唯一标识设备中的进程(应用程序) 端口号:用两个字节表示的整数,的取值范围是 0-65535,0-1023 之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用 1024 以上的端口号。如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败,报出端口被占用异常 -利用**协议+IP地址+端口号** 三元组合,就可以标识网络中的进程了,那么进程间的通信就可以利用这个标识与其它进程进行交互。 +利用**协议+IP 地址+端口号** 三元组合,就可以标识网络中的进程了,那么进程间的通信就可以利用这个标识与其它进程进行交互。 @@ -12280,14 +12280,16 @@ final void updateHead(Node h, Node p) { > > 数据链路层 : 进入到硬件(网) +通信**是进程与进程之间的通信**,不是主机与主机之间的通信 + TCP/IP协议:传输控制协议 (Transmission Control Protocol) -TCP:面向连接的安全的可靠的传输通信协议 +传输控制协议 TCP(Transmission Control Protocol)是面向连接的,提供可靠交付,有流量控制,拥塞控制,提供全双工通信,面向字节流(把应用层传下来的报文看成字节流,把字节流组织成大小不等的数据块),每一条 TCP 连接只能是点对点的(一对一) * 在通信之前必须确定对方在线并且连接成功才可以通信 -* 例如下载文件、浏览网页等(要求可靠传输) +* 例如下载文件、浏览网页等(要求可靠传输) -UDP:用户数据报协议(User Datagram Protocol),是一个面向无连接的不可靠传输的协议 +用户数据报协议 UDP(User Datagram Protocol)是无连接的,尽最大可能交付,没有拥塞控制,面向报文(对于应用程序传下来的报文不合并也不拆分,只是添加 UDP 首部),支持一对一、一对多、多对一和多对多的交互通信 * 直接发消息给对方,不管对方是否在线,发消息后也不需要确认 * 无线(视频会议,通话),性能好,可能丢失一些数据 @@ -12305,7 +12307,7 @@ UDP:用户数据报协议(User Datagram Protocol),是一个面向无连接 * 同步:当前线程要自己进行数据的读写操作(自己去银行取钱) * 异步:当前线程可以去做其他事情(委托别人拿银行卡到银行取钱,然后给你) * 阻塞:在数据没有的情况下,还是要继续等待着读(排队等待) -* 非阻塞:在数据没有的情况下,会去做其他事情,一旦有了数据再来获取(柜台取款,取个号,然后坐在椅子上做其它事,等号广播会通知你办理) +* 非阻塞:在数据没有的情况下,会去做其他事情,一旦有了数据再来获取(柜台取款,取个号,然后坐在椅子上做其它事,等号广播会通知你办理) Java 中的通信模型: @@ -12955,7 +12957,7 @@ UDP(User Datagram Protocol)协议的特点: * 面向无连接的协议 * 发送端只管发送,不确认对方是否能收到 * 基于数据包进行数据传输 -* 发送数据的包的大小限制**64KB**以内 +* 发送数据的包的大小限制 **64KB** 以内 * 因为面向无连接,速度快,但是不可靠,会丢失数据 UDP协议的使用场景:在线视频、网络语音、电话 @@ -12975,7 +12977,7 @@ UDP 协议相关的两个类 **DatagramPacket**: -* DatagramPacket类 +* DatagramPacket 类 `public new DatagramPacket(byte[] buf, int length, InetAddress address, int port)` : 创建发送端数据包对象,参数: @@ -12995,10 +12997,10 @@ UDP 协议相关的两个类 **DatagramSocket**: -* DatagramSocket类构造方法 - `protected DatagramSocket()` : 创建发送端的Socket对象,系统会随机分配一个端口号 - `protected DatagramSocket(int port)` : 创建接收端的Socket对象并指定端口号 -* DatagramSocket类成员方法 +* DatagramSocket 类构造方法 + `protected DatagramSocket()` : 创建发送端的 Socket 对象,系统会随机分配一个端口号 + `protected DatagramSocket(int port)` : 创建接收端的 Socket 对象并指定端口号 +* DatagramSocket 类成员方法 `public void send(DatagramPacket dp)` : 发送数据包 `public void receive(DatagramPacket p)` : 接收数据包 `public void close()` : 关闭数据报套接字 @@ -13076,7 +13078,7 @@ UDP 通信方式: #### 基本介绍 -TCP/IP 协议 ==> Transfer Control Protocol ==> 传输控制协议 +TCP/IP (Transfer Control Protocol) 协议,传输控制协议 TCP/IP 协议的特点: @@ -13110,20 +13112,24 @@ TCP 协议相关的类: * Socket:一个该类的对象就代表一个客户端程序。 * ServerSocket:一个该类的对象就代表一个服务器端程序。 -Socket类 +Socket 类 * 构造方法: - `Socket(InetAddress address,int port)`:创建流套接字并将其连接到指定 IP 指定端口号 - `Socket(String host, int port)`:根据ip地址字符串和端口号创建客户端 Socket 对象 - 注意事项:执行该方法,就会立即连接指定的服务器,连接成功,则表示三次握手通过,反之抛出异常 -* 常用API: - `OutputStream getOutputStream()`:获得字节输出流对象 - `InputStream getInputStream()`:获得字节输入流对象 - `void shutdownInput()`:停止接受 - `void shutdownOutput()`:停止发送数据,终止通信 - `SocketAddress getRemoteSocketAddress() `:返回套接字连接到的端点的地址,未连接返回null + + * `Socket(InetAddress address,int port)`:创建流套接字并将其连接到指定 IP 指定端口号 + + * `Socket(String host, int port)`:根据ip地址字符串和端口号创建客户端 Socket 对象 + + 注意事项:执行该方法,就会立即连接指定的服务器,连接成功,则表示三次握手通过,反之抛出异常 +* 常用 API: + + * `OutputStream getOutputStream()`:获得字节输出流对象 + * `InputStream getInputStream()`:获得字节输入流对象 + * `void shutdownInput()`:停止接受 + * `void shutdownOutput()`:停止发送数据,终止通信 + * `SocketAddress getRemoteSocketAddress() `:返回套接字连接到的端点的地址,未连接返回 null -ServerSocket类: +ServerSocket 类: * 构造方法:`public ServerSocket(int port)` * 常用API:`public Socket accept()`,**阻塞等待**接收一个客户端的 Socket 管道连接请求,连接成功返回一个 Socket 对象 @@ -13216,7 +13222,7 @@ public class ClientDemo { // 1.客户端要请求于服务端的socket管道连接。 Socket socket = new Socket("127.0.0.1",8080); // 2.从socket通信管道中得到一个字节输出流 - OutputStream os = new socket.getOutputStream(); + OutputStream os = socket.getOutputStream(); // 3.把低级的字节输出流包装成高级的打印流。 PrintStream ps = new PrintStream(os); // 4.开始发消息出去 @@ -14197,7 +14203,7 @@ public class ChannelTest { #### 基本介绍 -选择器(Selector) 是 SelectableChannle 对象的**多路复用器**,Selector 可以同时监控多个通道的状况,利用 Selector 可使一个单独的线程管理多个 Channel。**Selector 是非阻塞 IO 的核心**。 +选择器(Selector) 是 SelectableChannle 对象的**多路复用器**,Selector 可以同时监控多个通道的状况,利用 Selector 可使一个单独的线程管理多个 Channel,**Selector 是非阻塞 IO 的核心**。 ![](https://gitee.com/seazean/images/raw/master/Java/NIO-Selector.png) @@ -14378,7 +14384,7 @@ public class Server { buffer.clear();// 清除之前的数据 } } - //删除当前的 selectionKey,防止重复操作 + // 删除当前的 selectionKey,防止重复操作 it.remove(); } } diff --git a/SSM.md b/SSM.md index 490ee7f..d655f49 100644 --- a/SSM.md +++ b/SSM.md @@ -10607,9 +10607,9 @@ SpringMVC 提供访问原始 Servlet 接口的功能 * View Resolver:视图解析器, 将 Handler 中返回的逻辑视图(ModelAndView)解析为一个具体的视图(View)对象 -* View:视图, View 最后对页面进行渲染将结果返回给用户。Springmvc 框架提供了很多的 View 视图类型,包括:jstlView、freemarkerView、pdfView 等 +* View:视图, View 最后对页面进行渲染将结果返回给用户。SpringMvc 框架提供了很多的 View 视图类型,包括:jstlView、freemarkerView、pdfView 等 - ![](https://gitee.com/seazean/images/raw/master/Frame/SpingMVC技术架构.png) + ![](https://gitee.com/seazean/images/raw/master/Frame/SpingMVC-技术架构.png) @@ -10674,7 +10674,7 @@ protected void doDispatch(HttpServletRequest request, HttpServletResponse respon return; } - //根据映射器获取当前 handler 处理器适配器,用来处理当前的请求 + // 根据映射器获取当前 handler 处理器适配器,用来处理当前的请求 HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); // 获取发出此次请求的方法 String method = request.getMethod(); @@ -10753,9 +10753,9 @@ HandlerMapping 处理器映射器,保存了所有 `@RequestMapping` 和 `hand ```java protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { if (this.handlerMappings != null) { - //遍历所有的 HandlerMapping + // 遍历所有的 HandlerMapping for (HandlerMapping mapping : this.handlerMappings) { - //尝试去每个 HandlerMapping 中匹配当前请求的处理 + // 尝试去每个 HandlerMapping 中匹配当前请求的处理 HandlerExecutionChain handler = mapping.getHandler(request); if (handler != null) { return handler; @@ -10816,13 +10816,13 @@ doDispatch() 中 调用 `HandlerAdapter ha = getHandlerAdapter(mappedHandler.get ```java protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException { if (this.handlerAdapters != null) { - //遍历所有的 HandlerAdapter + // 遍历所有的 HandlerAdapter for (HandlerAdapter adapter : this.handlerAdapters) { - //判断当前适配器是否支持当前 handle - //return (handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler)) - //这里返回的是True, + // 判断当前适配器是否支持当前 handle + // return (handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler)) + // 这里返回的是True, if (adapter.supports(handler)) { - //返回的是 RequestMappingHandlerAdapter + // 返回的是 RequestMappingHandlerAdapter return adapter; } } @@ -10867,38 +10867,38 @@ protected ModelAndView invokeHandlerMethod(HttpServletRequest request, //封装成 SpringMVC 的接口,用于通用 Web 请求拦截器,使能够访问通用请求元数据,而不是用于实际处理请求 ServletWebRequest webRequest = new ServletWebRequest(request, response); try { - //WebDataBinder 用于从 Web 请求参数到 JavaBean 对象的数据绑定,获取创建该实例的工厂 + // WebDataBinder 用于从 Web 请求参数到 JavaBean 对象的数据绑定,获取创建该实例的工厂 WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod); - //创建 Model 实例,用于向模型添加属性 + // 创建 Model 实例,用于向模型添加属性 ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory); - //方法执行器 + // 方法执行器 ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod); - //参数解析器,有很多 + // 参数解析器,有很多 if (this.argumentResolvers != null) { invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); } - //返回值处理器,也有很多 + // 返回值处理器,也有很多 if (this.returnValueHandlers != null) { invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); } - //设置数据绑定器 + // 设置数据绑定器 invocableMethod.setDataBinderFactory(binderFactory); - //设置参数检查器 + // 设置参数检查器 invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer); - //新建一个 ModelAndViewContainer 并进行初始化和一些属性的填充 + // 新建一个 ModelAndViewContainer 并进行初始化和一些属性的填充 ModelAndViewContainer mavContainer = new ModelAndViewContainer(); - //设置一些属性 + // 设置一些属性 - //执行目标方法 + // 【执行目标方法】 invocableMethod.invokeAndHandle(webRequest, mavContainer); - //异步请求 + // 异步请求 if (asyncManager.isConcurrentHandlingStarted()) { return null; } - // 获取 ModelAndView 对象,封装了 ModelAndViewContainer + // 【获取 ModelAndView 对象,封装了 ModelAndViewContainer】 return getModelAndView(mavContainer, modelFactory, webRequest); } finally { @@ -10907,7 +10907,7 @@ protected ModelAndView invokeHandlerMethod(HttpServletRequest request, } ``` -**ServletInvocableHandlerMethod#invokeAndHandle**:执行目标方法 +ServletInvocableHandlerMethod#invokeAndHandle:执行目标方法 * `returnValue = invokeForRequest(webRequest, mavContainer, providedArgs)`:**执行自己写的 controller 方法,返回的就是自定义方法中 return 的值** @@ -10957,7 +10957,9 @@ protected ModelAndView invokeHandlerMethod(HttpServletRequest request, * **进行返回值的处理,响应部分详解**,处理完成进入下面的逻辑 -**RequestMappingHandlerAdapter#getModelAndView**:获取 ModelAndView 对象 + + +RequestMappingHandlerAdapter#getModelAndView:获取 ModelAndView 对象 * `modelFactory.updateModel(webRequest, mavContainer)`:Model 数据升级到会话域(**请求域中的数据在重定向时丢失**) @@ -11136,7 +11138,7 @@ public Person getPerson(){ ```java public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { - // 执行目标方法,return person 对象 + // 【执行目标方法】,return person 对象 Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); // 设置状态码 setResponseStatus(webRequest); @@ -11148,16 +11150,16 @@ public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer.setRequestHandled(true); return; } - } //返回值是字符串 + } // 返回值是字符串 else if (StringUtils.hasText(getResponseStatusReason())) { - //设置请求处理完成 + // 设置请求处理完成 mavContainer.setRequestHandled(true); return; // 设置请求没有处理完成,还需要进行返回值的逻辑 mavContainer.setRequestHandled(false); Assert.state(this.returnValueHandlers != null, "No return value handlers"); try { - // 返回值的处理 + // 【返回值的处理】 this.returnValueHandlers.handleReturnValue( returnValue, getReturnValueType(returnValue), mavContainer, webRequest); } @@ -11165,24 +11167,23 @@ public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer } ``` -* 没有加 @ResponseBody 注解的返回数据按照视图(页面)处理的逻辑,ViewNameMethodReturnValueHandler(视图详解) - +* **没有加 @ResponseBody 注解的返回数据按照视图(页面)处理的逻辑**,ViewNameMethodReturnValueHandler(视图详解) * 此例是加了注解的,返回的数据不是视图,HandlerMethodReturnValueHandlerComposite#handleReturnValue: - ```java - public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, - ModelAndViewContainer mavContainer, NativeWebRequest webRequest) { - //获取合适的返回值处理器 - HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType); - if (handler == null) { - throw new IllegalArgumentException(); - } - //使用处理器处理返回值(详解源码中的这两个函数) - handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest); - } - ``` +```java +public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, + ModelAndViewContainer mavContainer, NativeWebRequest webRequest) { + // 获取合适的返回值处理器 + HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType); + if (handler == null) { + throw new IllegalArgumentException(); + } + // 使用处理器处理返回值(详解源码中的这两个函数) + handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest); +} +``` -**HandlerMethodReturnValueHandlerComposite#selectHandler**: +HandlerMethodReturnValueHandlerComposite#selectHandler:获取合适的返回值处理器 * `boolean isAsyncValue = isAsyncReturnValue(value, returnType)`:是否是异步请求 @@ -11191,7 +11192,9 @@ public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer * `ModelAndViewMethodReturnValueHandler#supportsReturnType`:处理返回值类型是 ModelAndView 的处理器 * `ModelAndViewResolverMethodReturnValueHandler#supportsReturnType`:直接返回 true,处理所有数据 -**RequestResponseBodyMethodProcessor#handleReturnValue**:处理返回值 + + +RequestResponseBodyMethodProcessor#handleReturnValue:处理返回值,要进行内容协商 * `mavContainer.setRequestHandled(true)`:设置请求处理完成 @@ -11219,7 +11222,7 @@ public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer `this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request))`:调用该方法 - * `for(ContentNegotiationStrategy strategy:this.strategies)`:默认策略是提取请求头的字段的内容,策略类为**HeaderContentNegotiationStrategy**,可以配置添加其他类型的策略 + * `for(ContentNegotiationStrategy strategy:this.strategies)`:**默认策略是提取请求头的字段的内容**,策略类为HeaderContentNegotiationStrategy,可以配置添加其他类型的策略 * `List mediaTypes = strategy.resolveMediaTypes(request)`:解析 Accept 字段存储为 List * `headerValueArray = request.getHeaderValues(HttpHeaders.ACCEPT)`:获取请求头中 Accept 字段 * `List mediaTypes = MediaType.parseMediaTypes(headerValues)`:解析成 List 集合 @@ -11227,7 +11230,7 @@ public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer ![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-浏览器支持接收的数据类型.png) - * `producibleTypes = getProducibleMediaTypes(request, valueType, targetType)`:服务器能生成的媒体类型 + * `producibleTypes = getProducibleMediaTypes(request, valueType, targetType)`:**服务器能生成的媒体类型** * `request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE)`:从请求域获取默认的媒体类型 * ` for (HttpMessageConverter converter : this.messageConverters)`:遍历所有的消息转换器 @@ -11239,10 +11242,10 @@ public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer * **内容协商:** ```java - for (MediaType requestedType : acceptableTypes) { //遍历所有的浏览器能接受的媒体类型 - for (MediaType producibleType : producibleTypes) { //遍历所有服务器能产出的 - if (requestedType.isCompatibleWith(producibleType)) { //判断类型是否匹配,最佳匹配 - //数据协商匹配成功 + for (MediaType requestedType : acceptableTypes) { // 遍历所有的浏览器能接受的媒体类型 + for (MediaType producibleType : producibleTypes) { // 遍历所有服务器能产出的 + if (requestedType.isCompatibleWith(producibleType)) { // 判断类型是否匹配,最佳匹配 + // 数据协商匹配成功,一般有多种 mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType)); } } @@ -11253,7 +11256,7 @@ public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer * `for (MediaType mediaType : mediaTypesToUse)`:**遍历所有的最佳匹配** - `selectedMediaType = mediaType`:赋值给选择的类型 + `selectedMediaType = mediaType`:选择一种赋值给选择的类型 * `selectedMediaType = selectedMediaType.removeQualityValue()`:媒体类型去除相对品质因数 @@ -11261,7 +11264,7 @@ public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer * `GenericHttpMessageConverter genericConverter`:**MappingJackson2HttpMessageConverter 可以将对象写为 JSON** - * `((GenericHttpMessageConverter) converter).canWrite()`:转换器是否可以写出给定的类型 + * `((GenericHttpMessageConverter) converter).canWrite()`:判断转换器是否可以写出给定的类型 `AbstractJackson2HttpMessageConverter#canWrit` @@ -11282,15 +11285,15 @@ public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer `AbstractGenericHttpMessageConverter#write`:该类的方法 - * `addDefaultHeaders(headers, t, contentType)`:设置响应头中的数据类型 + * `addDefaultHeaders(headers, t, contentType)`:**设置响应头中的数据类型** ![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-服务器设置数据类型.png) - * `writeInternal(t, type, outputMessage)`:**真正的写出数据的函数** + * `writeInternal(t, type, outputMessage)`:**数据写出为 JSON 格式** * `Object value = object`:value 引用 Person 对象 * `ObjectWriter objectWriter = objectMapper.writer()`:获取 ObjectWriter 对象 - * `objectWriter.writeValue(generator, value)`:**使用 ObjectWriter 写出数据为 JSON** + * `objectWriter.writeValue(generator, value)`:使用 ObjectWriter 写出数据为 JSON @@ -11395,17 +11398,17 @@ public void handleReturnValue(@Nullable Object returnValue, MethodParameter retu } ``` -* ViewNameMethodReturnValueHandler#supportsReturnType +* ViewNameMethodReturnValueHandler#supportsReturnType: ```java public boolean supportsReturnType(MethodParameter returnType) { Class paramType = returnType.getParameterType(); - //返回值是否是void 或者 是 CharSequence 字符序列 + // 返回值是否是void 或者 是 CharSequence 字符序列 return (void.class == paramType || CharSequence.class.isAssignableFrom(paramType)); } ``` -* ViewNameMethodReturnValueHandler#handleReturnValue +* ViewNameMethodReturnValueHandler#handleReturnValue: ```java public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, @@ -11414,11 +11417,11 @@ public void handleReturnValue(@Nullable Object returnValue, MethodParameter retu // 返回值是字符串,是 return "forward:/success" if (returnValue instanceof CharSequence) { String viewName = returnValue.toString(); - //把视图名称设置进入 ModelAndViewContainer 中 + // 把视图名称设置进入 ModelAndViewContainer 中 mavContainer.setViewName(viewName); - //判断是否是重定向数据 `viewName.startsWith("redirect:")` + // 判断是否是重定向数据 `viewName.startsWith("redirect:")` if (isRedirectViewName(viewName)) { - //如果是重定向,设置是重定向指令 + // 如果是重定向,设置是重定向指令 mavContainer.setRedirectModelScenario(true); } } @@ -11475,22 +11478,22 @@ DispatcherServlet#render: * `attrs = RequestContextHolder.getRequestAttributes()`:获取请求的相关属性信息 - * `requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest())`:**获取最佳匹配的媒体类型**,函数内进行了匹配的逻辑 + * `requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest())`:获取最佳匹配的媒体类型,函数内进行了匹配的逻辑 * `candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes)`:获取候选的视图对象 * `for (ViewResolver viewResolver : this.viewResolvers)`:遍历所有的视图解析器 - * `View view = viewResolver.resolveViewName(viewName, locale)`:解析视图 + * `View view = viewResolver.resolveViewName(viewName, locale)`:**解析视图** - `AbstractCachingViewResolver#resolveViewName`:调用此方法 + `AbstractCachingViewResolver#resolveViewName`: * `returnview = createView(viewName, locale)`:UrlBasedViewResolver#createView **请求转发**:实例为 InternalResourceView * `if (viewName.startsWith(FORWARD_URL_PREFIX))`:视图名字是否是 **`forward:`** 的前缀 - * `forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length())`:**名字截取前缀** + * `forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length())`:名字截取前缀 * `view = new InternalResourceView(forwardUrl)`:新建 InternalResourceView 对象并返回 * `return applyLifecycleMethods(FORWARD_URL_PREFIX, view)`:Spring 中的初始化操作 @@ -11505,7 +11508,7 @@ DispatcherServlet#render: * `view.render(mv.getModelInternal(), request, response)`:**页面渲染** - * `mergedModel = createMergedOutputModel(model, request, response)`:把请求域中的数据封装到 Map + * `mergedModel = createMergedOutputModel(model, request, response)`:把请求域中的数据封装到 model * `prepareResponse(request, response)`:响应前的准备工作,设置一些响应头 @@ -11513,17 +11516,17 @@ DispatcherServlet#render: `getRequestToExpose(request)`:获取 Servlet 原生的方式 - **请求转发** InternalResourceView 的逻辑: + **请求转发 InternalResourceView 的逻辑:请求域中的数据不丢失** * `exposeModelAsRequestAttributes(model, request)`:暴露 model 作为请求域的属性 * `model.forEach()`:遍历 Model 中的数据 - * `request.setAttribute(name, value)`:设置到请求域中 + * `request.setAttribute(name, value)`:**设置到请求域中** * `exposeHelpers(request)`:自定义接口 * `dispatcherPath = prepareForRendering(request, response)`:确定调度分派的路径,此例是 /success * `rd = getRequestDispatcher(request, dispatcherPath)`:**获取 Servlet 原生的 RequestDispatcher 实现转发** * `rd.forward(request, response)`:实现请求转发 - **重定向** RedirectView 的逻辑: + **重定向 RedirectView 的逻辑:请求域中的数据会丢失** * `targetUrl = createTargetUrl(model, request)`:获取目标 URL * `enc = request.getCharacterEncoding()`:设置编码 UTF-8 diff --git a/Web.md b/Web.md index f06f46b..5321f0f 100644 --- a/Web.md +++ b/Web.md @@ -2116,6 +2116,14 @@ URL 和 URI * HTTP 协议的长连接和短连接,实质上是 TCP 协议的长连接和短连接 +**从浏览器地址栏输入 URL 到请求返回发生了什么?** + +* 进行 URL 解析,进行编码 +* DNS 解析,顺序是先查 hosts 文件是否有记录,有的话就会把相对应映射的 IP 返回,然后去本地 DNS 缓存中寻找,然后去找计算机上配置的 DNS 服务器上有或者有缓存,最后去找全球的根 DNS 服务器,直到查到为止 +* 查找到 IP 之后,进行 TCP 协议的三次握手,发出 HTTP 请求 +* 服务器处理请求,返回响应 +* 浏览器解析渲染页面 + *** From 211f1c832bcb80d9eb4d90cc959bb4f195277913 Mon Sep 17 00:00:00 2001 From: Seazean Date: Fri, 20 Aug 2021 22:10:45 +0800 Subject: [PATCH 033/168] Update README --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 2882e8b..9691b30 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,6 @@ 内容说明: * DB:MySQL、Redis -* Issue:Interview Questions * Java:JavaSE、JVM、Algorithm、Design Pattern * Prog:Concurrent、Network Programming * SSM:MyBatis、Spring、SpringMVC、SpringBoot From 0a99b094242eec6e50e807d4a44d544a202d0c61 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 21 Aug 2021 21:25:47 +0800 Subject: [PATCH 034/168] Update Java Notes --- DB.md | 14 +- Java.md | 23 +- Prog.md | 994 +++++++++++++++++++++++++++++++++++++++++++++----------- 3 files changed, 819 insertions(+), 212 deletions(-) diff --git a/DB.md b/DB.md index 5ddfabb..b931c4b 100644 --- a/DB.md +++ b/DB.md @@ -8192,7 +8192,7 @@ Redis ACL 是 Access Control List(访问控制列表)的缩写,该功能 #### 简介 -存储的数据:单个数据,最简单的数据存储类型,也是最常用的数据存储类型,实质上是存一个字符串,String 类型是二进制安全的,意味着 Redis 的 string 可以包含任何数据,比如图片或者序列化的对象 +存储的数据:单个数据,最简单的数据存储类型,也是最常用的数据存储类型,实质上是存一个字符串,string 类型是二进制安全的,意味着 Redis 的 string 可以包含任何数据,比如图片或者序列化的对象 存储数据的格式:一个存储空间保存一个数据,每一个空间中只能保存一个字符串信息 @@ -8275,8 +8275,8 @@ Redis 所有操作都是**原子性**的,采用**单线程**机制,命令是 单数据和多数据的选择: -* 单数据执行3条指令的过程:3 次发送 + 3 次处理 + 3次返回 -* 多数据执行1条指令的过程:1 次发送 + 3 次处理 + 1次返回(发送和返回的事件略高于单数据) +* 单数据执行 3 条指令的过程:3 次发送 + 3 次处理 + 3次返回 +* 多数据执行 1 条指令的过程:1 次发送 + 3 次处理 + 1次返回(发送和返回的事件略高于单数据) @@ -8611,8 +8611,8 @@ typedef struct listNode ![](https://gitee.com/seazean/images/raw/master/DB/Redis-链表数据结构.png) -- 双向:链表节点带有前驱、后继指针,获取某个节点的前驱、后继节点的时间复杂度为O(1) -- 无环:链表为非循环链表,表头节点的前驱指针和表尾节点的后继指针都指向NULL,对链表的访问以 NULL 为终点 +- 双向:链表节点带有前驱、后继指针,获取某个节点的前驱、后继节点的时间复杂度为 O(1) +- 无环:链表为非循环链表,表头节点的前驱指针和表尾节点的后继指针都指向 NULL,对链表的访问以 NULL 为终点 @@ -8719,7 +8719,7 @@ set 类型:与 hash 存储结构哈希表完全相同,只是仅存储键不 解决方案: * 设定用户鉴别规则,周期性更新满足规则的黑名单加入 set 集合,用户行为信息达到后与黑名单进行对比 -* 黑名单过滤IP地址:应用于开放游客访问权限的信息源 +* 黑名单过滤 IP 地址:应用于开放游客访问权限的信息源 * 黑名单过滤设备信息:应用于限定访问设备的信息源 * 黑名单过滤用户:应用于基于访问权限的信息源 @@ -8733,7 +8733,7 @@ set 类型:与 hash 存储结构哈希表完全相同,只是仅存储键不 集合类型的内部编码有两种: -* intset(整数集合):当集合中的元素都是整数且元素个数小于 set-maxintset-entries配置(默认512个)时,Redis 会选用 intset 来作为集合的内部实现,从而减少内存的使用 +* intset(整数集合):当集合中的元素都是整数且元素个数小于 set-maxintset-entries配置(默认 512 个)时,Redis 会选用 intset 来作为集合的内部实现,从而减少内存的使用 * hashtable(哈希表,字典):当无法满足 intset 条件时,Redis 会使用 hashtable 作为集合的内部实现 diff --git a/Java.md b/Java.md index 5f53850..f3195ea 100644 --- a/Java.md +++ b/Java.md @@ -748,7 +748,7 @@ public static 返回值类型 方法名(参数) { 重载仅对应方法的定义,与方法的调用无关,调用方式参照标准格式 -重载仅针对**同一个类**中方法的名称与参数进行识别,**与返回值无关**,不能通过返回值来判定两个方法是否构成重载 +重载仅针对同一个类中方法的名称与参数进行识别,与返回值无关,**不能通过返回值来判定两个方法是否构成重载** 原理:JVM → 运行机制 → 方法调用 → 多态原理 @@ -4426,7 +4426,7 @@ public class Student implements Comparable{ Queue:队列,先进先出的特性 -PriorityQueue 是优先级队列,底层存储结构为 Object[],默认实现为小顶堆 +PriorityQueue 是优先级队列,底层存储结构为 Object[],默认实现为小顶堆,每次出队最小的元素 构造方法: @@ -8057,7 +8057,7 @@ public class UserServiceTest { 反射提供了一个Class类型:HelloWorld.java → javac → HelloWorld.class -* `Class c = HelloWorld.class;` +* `Class c = HelloWorld.class` 注意:反射是工作在**运行时**的技术,只有运行之后才会有 class 类对象 @@ -9544,7 +9544,7 @@ Java 编译器输入的指令流是一种基于栈的指令集架构。因为跨 ### 生命周期 -JVM的生命周期分为三个阶段,分别为:启动、运行、死亡。 +JVM 的生命周期分为三个阶段,分别为:启动、运行、死亡。 - **启动**:当启动一个 Java 程序时,通过引导类加载器(bootstrap class loader)创建一个初始类(initial class),对于拥有 main 函数的类就是 JVM 实例运行的起点 - **运行**: @@ -11645,7 +11645,7 @@ Java 对象创建时机: * 实例初始化不一定要在类初始化结束之后才开始 - * 在同一个类加载器下,一个类型只会被初始化一次。所以一旦开始初始化一个类,无论是否完成后续都不会再重新触发该类型的初始化阶段了(只考虑在同一个类加载器下的情形。因此,在实例化上述程序中的st变量时,**实际上是把实例初始化嵌入到了静态初始化流程中,并且在上面的程序中,嵌入到了静态初始化的起始位置**,这就导致了实例初始化完全发生在静态初始化之前,这也是导致a为110 b为0的原因 + * 在同一个类加载器下,一个类型只会被初始化一次。所以一旦开始初始化一个类,无论是否完成后续都不会再重新触发该类型的初始化阶段了(只考虑在同一个类加载器下的情形。因此,在实例化上述程序中的 st 变量时,**实际上是把实例初始化嵌入到了静态初始化流程中,并且在上面的程序中,嵌入到了静态初始化的起始位置**,这就导致了实例初始化完全发生在静态初始化之前,这也是导致 a 为 110,b 为 0 的原因 代码等价于: @@ -11783,7 +11783,6 @@ Java 对象创建时机: 类变量初始化: -* static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾 * static 变量分配空间和赋值是两个步骤:**分配空间在准备阶段完成,赋值在初始化阶段完成** * 如果 static 变量是 final 的基本类型以及字符串常量,那么编译阶段值就确定了,准备阶段会显式初始化 * 如果 static 变量是 final 的,但属于引用类型或者构造器方法的字符串,赋值在初始化阶段完成 @@ -11814,14 +11813,14 @@ Java 对象创建时机: 将常量池中类、接口、字段、方法的**符号引用替换为直接引用**(内存地址)的过程: -* 符号引用:一组符号来描述目标,可以是任何字面量,属于编译原理方面的概念如:包括类和接口的全限名、字段的名称和描述符、方法的名称和**方法描述符** +* 符号引用:一组符号来描述目标,可以是任何字面量,属于编译原理方面的概念,如:包括类和接口的全限名、字段的名称和描述符、方法的名称和**方法描述符** * 直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄,如果有了直接引用,那说明引用的目标必定已经存在于内存之中 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等 * 在类加载阶段解析的是非虚方法,静态绑定 * 也可以在初始化阶段之后再开始解析,这是为了支持 Java 的**动态绑定** -* 通过解析操作,符号引用就可以转变为目标方法在类中虚方法表中的位置,从而使得方法被成功调用 +* 通过解析操作,符号引用就可以转变为目标方法在类的虚方法表中的位置,从而使得方法被成功调用 ```java public class Load2 { @@ -11853,7 +11852,7 @@ class D { 初始化阶段才真正开始执行类中定义的 Java 程序代码,在准备阶段,类变量已经赋过一次系统要求的初始值;在初始化阶段,通过程序制定的计划去初始化类变量和其它资源,执行 -在编译生成 class 文件时,编译器会产生两个方法加于 class 文件中,一个是类的初始化方法 clinit, 另一个是实例的初始化方法 init +在编译生成 class 文件时,编译器会产生两个方法加于 class 文件中,一个是类的初始化方法 clinit,另一个是实例的初始化方法 init 类构造器 () 与实例构造器 () 不同,它不需要程序员进行显式调用,在一个类的生命周期中,类构造器最多被虚拟机**调用一次**,而实例构造器则会被虚拟机调用多次,只要程序员创建对象 @@ -12010,7 +12009,7 @@ init 指的是实例构造器,主要作用是在类实例化过程中执行, 从 Java 虚拟机规范来讲,只存在以下两种不同的类加载器: - 启动类加载器(Bootstrap ClassLoader):使用 C++ 实现,是虚拟机自身的一部分 -- 自定义类加载器(User-Defined ClassLoader):Java虚拟机规范**将所有派生于抽象类 ClassLoader 的类加载器都划分为自定义类加载器**,使用 Java 语言实现,独立于虚拟机 +- 自定义类加载器(User-Defined ClassLoader):Java 虚拟机规范**将所有派生于抽象类 ClassLoader 的类加载器都划分为自定义类加载器**,使用 Java 语言实现,独立于虚拟机 从 Java 开发人员的角度看: @@ -13599,7 +13598,7 @@ VM 参数设置: Java 虚拟机识别方法的关键在于类名、方法名以及方法描述符(method descriptor) -* 方法描述符是由方法的参数类型以及返回类型所构成,也叫方法特征签名 +* **方法描述符是由方法的参数类型以及返回类型所构成**,Java 层面叫方法特征签名 * 在同一个类中,如果同时出现多个名字相同且描述符也相同的方法,那么 Java 虚拟机会在类的验证阶段报错 JVM 根据名字和描述符来判断的,只要返回值不一样,其它完全一样,在 JVM 中是允许的,但 Java 语言不允许 @@ -15514,7 +15513,7 @@ public class SelectSort { 实现思路: -1. 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,并通过上浮对堆进行调整,此堆为初始的无序区,堆顶为最大数 +1. 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,并通过上浮对堆进行调整,此堆为初始的无序区,**堆顶为最大数** 2. 将堆顶元素 R[1] 与最后一个元素 R[n] 交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区 Rn,且满足 R[1,2…n-1]<=R[n] diff --git a/Prog.md b/Prog.md index c4aea9d..580d323 100644 --- a/Prog.md +++ b/Prog.md @@ -204,11 +204,16 @@ Runnable 方式的优缺点: `public FutureTask(Callable callable)`:未来任务对象,在线程执行完后**得到线程的执行结果** -* FutureTask 就是 Runnable 对象,因为 Thread 类只能执行 Runnable 实例的任务对象,所以把 Callable 包装一下 +* FutureTask 就是 Runnable 对象,因为 Thread 类只能执行 Runnable 实例的任务对象,所以把 Callable 包装成未来任务对象 * 线程池部分详解了 FutureTask 的源码 `public V get()`:同步等待 task 执行完毕的结果,如果在线程中获取另一个线程执行结果,会阻塞等待,用于线程同步 +* get() 线程会阻塞等待任务执行完成 +* run() 执行完后会把结果设置到任务中的一个成员变量,get() 线程可以获取到该变量的值 + +优缺点: + * 优点:同 Runnable,并且能得到线程执行的结果 * 缺点:编码复杂 @@ -2248,9 +2253,9 @@ Java 内存模型是 Java MemoryModel(JMM),本身是一种**抽象的概 -**jvm和jmm之间的关系**: +**JVM 和 JMM 之间的关系**: -* jmm 中的主内存、工作内存与 jvm 中的 Java 堆、栈、方法区等并不是同一个层次的内存划分,这两者基本上是没有关系的,如果两者一定要勉强对应起来: +* JMM 中的主内存、工作内存与 JVM 中的 Java 堆、栈、方法区等并不是同一个层次的内存划分,这两者基本上是没有关系的,如果两者一定要勉强对应起来: * 主内存主要对应于 Java 堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域 * 从更低层次上说,主内存直接对应于物理硬件的内存,工作内存对应寄存器和高速缓存 @@ -3529,6 +3534,7 @@ class MyAtomicInteger { static { try { + //Unsafe unsafe = Unsafe.getUnsafe()这样会报错,需要反射获取 Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); theUnsafe.setAccessible(true); UNSAFE = (Unsafe) theUnsafe.get(null); @@ -4074,7 +4080,7 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { int sz = ++size; // 做一次启发式清理 - // 如果没有清除任何entry并且当前使用量达到了负载因子所定义,那么进行 rehash + // 如果没有清除任何 entry 并且当前使用量达到了负载因子所定义,那么进行 rehash if (!cleanSomeSlots(i, sz) && sz >= threshold) // 扩容 rehash(); @@ -4536,7 +4542,7 @@ java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:**FIFO - ArrayBlockQueue:由数组结构组成的有界阻塞队列 - LinkedBlockingQueue:由链表结构组成的无界(默认大小 Integer.MAX_VALUE)的阻塞队列 - PriorityBlockQueue:支持优先级排序的无界阻塞队列 -- DelayQueue:使用优先级队列实现的延迟无界阻塞队列 +- DelayedWorkQueue:使用优先级队列实现的延迟无界阻塞队列 - SynchronousQueue:不存储元素的阻塞队列,每一个生产线程会阻塞到有一个 put 的线程放入元素为止 - LinkedTransferQueue:由链表结构组成的无界阻塞队列 - LinkedBlockingDeque:由链表结构组成的**双向**阻塞队列 @@ -5398,45 +5404,6 @@ TransferQueue 类成员方法: -*** - - - -#### 延迟队列 - -DelayQueue 是一个支持延时获取元素的阻塞队列,内部采用优先队列 PriorityQueue 存储元素,同时元素必须实现 Delayed 接口;在创建元素时可以指定多久才可以从队列中获取当前元素,只有在延迟期满时才能从队列中提取元素 - -DelayQueue 只能添加(offer/put/add)实现了 Delayed 接口的对象,不能添加 int、String - -API: - -* `getDelay()`:获取元素在队列中的剩余时间,只有当剩余时间为 0 时元素才可以出队列。 -* `compareTo()`:用于排序,确定元素出队列的顺序 - -```java -class DelayTask implements Delayed { - private String name; - private long time; - private long start = System.currentTimeMillis(); - // construct set get - - // 需要实现的接口,获得延迟时间 用过期时间-当前时间 - @Override - public long getDelay(TimeUnit unit) { - return unit.convert((start + time) - System.currentTimeMillis(), TimeUnit.MILLISECONDS); - } - - // 用于延迟队列内部比较排序 当前时间的延迟时间 - 被比较对象的延迟时间 - @Override - public int compareTo(Delayed o) { - DelayTask obj = (DelayTask) o; - return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS)); - } -} -``` - - - *** @@ -5844,7 +5811,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 private volatile int maximumPoolSize; // 线程池最大线程数量 private volatile long keepAliveTime; // 空闲线程存活时间 private volatile ThreadFactory threadFactory; // 创建线程时使用的线程工厂,默认是 DefaultThreadFactory - private final BlockingQueue workQueue;// 超过核心线程提交任务就放入【阻塞队列】 + private final BlockingQueue workQueue;// 【超过核心线程提交任务就放入 阻塞队列】 ``` ```java @@ -5863,6 +5830,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 ```java // false 代表不可以,为 true 时核心线程空闲超过 keepAliveTime 也会被回收 + // allowCoreThreadTimeOut 可以设置该值 private volatile boolean allowCoreThreadTimeOut; ``` @@ -5944,7 +5912,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 // 非空判断 if (command == null) throw new NullPointerException(); - // 获取 ctl 最新值赋值给 c,ctl 高3位表示线程池状态,低位表示当前线程池线程数量。 + // 获取 ctl 最新值赋值给 c,ctl 高 3 位表示线程池状态,低位表示当前线程池线程数量。 int c = ctl.get(); // 【1】当前线程数量小于核心线程数,此次提交任务直接创建一个新的 worker,线程池中多了一个新的线程 if (workerCountOf(c) < corePoolSize) { @@ -5991,7 +5959,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 * addWorker():**添加线程到线程池**,返回 true 表示创建 Worker 成功,且线程启动 ```java - // core == true 表示采用核心线程数量限制,false表示采用 maximumPoolSize + // core == true 表示采用核心线程数量限制,false 表示采用 maximumPoolSize private boolean addWorker(Runnable firstTask, boolean core) { // 自旋判断当前线程池状态是否允许创建线程的,允许就设置线程数量 + 1 retry: @@ -6026,6 +5994,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 } } + //【令牌申请成功,开始创建线程】 // 运行标记,表示创建的 worker 是否已经启动,false未启动 true启动 @@ -6048,8 +6017,9 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 int rs = runStateOf(ctl.get()); // 判断线程池是否为RUNNING状态,不是再判断当前是否为SHUTDOWN状态且firstTask为空(特殊情况) if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) { - // 当线程 start 后,线程 isAlive 会返回 true,否则报错 - if (t.isAlive()) throw new IllegalThreadStateException(); + // 当线程 start 后,线程 isAlive 会返回 true,这里还没启动线程 + if (t.isAlive()) + throw new IllegalThreadStateException(); //【将新建的 Worker 添加到线程池中】 workers.add(w); @@ -6066,7 +6036,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 } // 添加成功就启动线程【执行任务】 if (workerAdded) { - // Thread 类中持有 Runnable 任务对象,调用的是 Runnable 的 run 方法 + // Thread 类中持有 Runnable 任务对象,调用的是 Runnable 的 run ,也就是 FutureTask t.start(); // 运行标记置为 true workerStarted = true; @@ -6183,7 +6153,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 ```java public void unlock() { release(1); } - //外部不会直接调用这个方法 这个方法是 AQS 内调用的,外部调用 unlock 时触发此方法 + // 外部不会直接调用这个方法 这个方法是 AQS 内调用的,外部调用 unlock 时触发此方法 protected boolean tryRelease(int unused) { setExclusiveOwnerThread(null); // 设置持有者为 null setState(0); // 设置 state = 0 @@ -6191,7 +6161,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 } ``` -* getTask():获取任务,线程空闲时间超过 keepAliveTime 就会被回收,判断的依据是**当前线程超过保活时间没有获取到任务**,方法返回 null 就代表当前线程要被回收了,返回到 runWorker 执行线程退出逻辑 +* getTask():获取任务,线程空闲时间超过 keepAliveTime 就会被回收,判断的依据是**当前线程阻塞超过保活时间没有获取到任务**,方法返回 null 就代表当前线程要被回收了,返回到 runWorker 执行线程退出逻辑 ```java private Runnable getTask() { @@ -6323,7 +6293,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 } ``` -* interruptIdleWorkers():shutdown 方法会**中断空闲线程**,根据是否可以获取 AQS 独占锁锁判断是否处于工作状态 +* interruptIdleWorkers():shutdown 方法会**中断空闲线程**,根据是否可以获取 AQS 独占锁判断是否处于工作状态 ```java // onlyOne == true 说明只中断一个线程 ,false 则中断所有线程 @@ -6355,7 +6325,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 } } finally { - //释放全局锁。 + // 释放全局锁 mainLock.unlock(); } } @@ -6472,13 +6442,36 @@ public FutureTask(Callable callable){ } public FutureTask(Runnable runnable, V result) { - // 使用装饰者模式将 runnable 转换成 callable 接口,外部线程通过 get 获取 - // 当前任务执行结果时,结果可能为 null 也可能为【传进来】的值,传进来什么返回什么 + // 装饰 this.callable = Executors.callable(runnable, result); this.state = NEW; } ``` +```java +public static Callable callable(Runnable task, T result) { + if (task == null) throw new NullPointerException(); + // 使用装饰者模式将 runnable 转换成 callable 接口,外部线程通过 get 获取 + // 当前任务执行结果时,结果可能为 null 也可能为【传进来】的值,传进来什么返回什么 + return new RunnableAdapter(task, result); +} +static final class RunnableAdapter implements Callable { + final Runnable task; + final T result; + // 构造方法 + RunnableAdapter(Runnable task, T result) { + this.task = task; + this.result = result; + } + public T call() { + // 实则调用 Runnable#run 方法 + task.run(); + // 返回值为构造 FutureTask 对象时传入的返回值或者是 null + return result; + } +} +``` + @@ -6518,7 +6511,7 @@ FutureTask 类的成员属性: private Callable callable; // Runnable 使用装饰者模式伪装成 Callable ``` -* 返回结果: +* 存储任务执行的结果,这是 run 方法返回值是 void 也可以获取到执行结果的原因: ```java // 正常情况下:任务正常执行结束,outcome 保存执行结果,callable 返回值。 @@ -6532,7 +6525,7 @@ FutureTask 类的成员属性: private volatile Thread runner; // 当前任务被线程执行期间,保存当前执行任务的线程对象引用 ``` -* 阻塞线程的队列: +* 线程阻塞队列的头节点: ```java // 会有很多线程去 get 当前任务的结果,这里使用了一种数据结构头插头取(类似栈)的一个队列来保存所有的 get 线程 @@ -6749,13 +6742,13 @@ FutureTask 类的成员方法: } // 条件成立:说明需要休眠 else - // 【当前 get 操作的线程就会被 park 阻塞了】,除非有其它线程将唤醒或者将当前线程中断 + // 【当前 get 操作的线程被 park 阻塞】,除非有其它线程将唤醒或者将当前线程中断 LockSupport.park(this); } } ``` - FutureTask#report:封装运行结果 + FutureTask#report:封装运行结果,可以获取 run() 方法中设置的成员变量 outcome,**这是 run 方法的返回值是 void 也可以获取到任务执行的结果的原因** ```java private V report(int s) throws ExecutionException { @@ -6782,7 +6775,8 @@ FutureTask 类的成员方法: UNSAFE.compareAndSwapInt(this, stateOffset, NEW, mayInterruptIfRunning ? INTERRUPTING : CANCELLED))) return false; - try { + try { + // 如果任务已经被执行,是否允许打断 if (mayInterruptIfRunning) { try { // 获取执行当前 FutureTask 的线程 @@ -6854,11 +6848,18 @@ private static void method1() { 任务调度线程池 ScheduledThreadPoolExecutor 继承 ThreadPoolExecutor: +* 使用内部类 ScheduledFutureTask 封装任务 +* 使用内部类 DelayedWorkQueue 作为线程池队列 +* 重写 onShutdown 方法去处理 shutdown 后的任务 +* 提供 decorateTask 方法作为 ScheduledFutureTask 的修饰方法,以便开发者进行扩展 + 构造方法:`Executors.newScheduledThreadPool(int corePoolSize)` ```java public ScheduledThreadPoolExecutor(int corePoolSize) { + // 最大线程数固定为 Integer.MAX_VALUE,最大活跃时间 keepAliveTime 固定为 super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, + // 阻塞队列是 DelayedWorkQueue new DelayedWorkQueue()); } ``` @@ -6890,7 +6891,7 @@ public ScheduledThreadPoolExecutor(int corePoolSize) { } ``` -* 定时任务 scheduleAtFixedRate:**一个任务的启动到下一个任务的启动**之间只要大于间隔时间,抢占到 CPU 就会立即执行 +* 定时任务 scheduleAtFixedRate:**一次任务的启动到下一次任务的启动**之间只要大于间隔时间,抢占到 CPU 就会立即执行 ```java public static void main(String[] args) { @@ -6909,7 +6910,7 @@ public ScheduledThreadPoolExecutor(int corePoolSize) { running...Sat Apr 24 18:08:17 CST 2021 ``` -* 定时任务 scheduleWithFixedDelay:**一个任务的结束到下一个任务的启动之间**等于间隔时间,抢占到 CPU 就会立即执行,这个方法才是真正的设置两个任务之间的间隔 +* 定时任务 scheduleWithFixedDelay:**一次任务的结束到下一次任务的启动之间**等于间隔时间,抢占到 CPU 就会立即执行,这个方法才是真正的设置两个任务之间的间隔 ```java public static void main(String[] args){ @@ -6929,6 +6930,617 @@ public ScheduledThreadPoolExecutor(int corePoolSize) { +*** + + + +#### 成员属性 + +##### 成员变量 + +* shutdown 后是否继续执行定时任务: + + ```java + private volatile boolean continueExistingPeriodicTasksAfterShutdown; + ``` + +* shutdown 后是否继续执行延迟任务: + + ```java + private volatile boolean executeExistingDelayedTasksAfterShutdown = true; + ``` + +* 取消方法是否将该任务从队列中移除: + + ```java + private volatile boolean removeOnCancel = false; + ``` + +* 任务的序列号: + + ```java + private static final AtomicLong sequencer = new AtomicLong(); + ``` + + + +*** + + + +##### 延迟任务 + +ScheduledFutureTask 继承 FutureTask,实现 RunnableScheduledFuture 接口,具有延迟执行的特点,覆盖 FutureTask 的 run 方法来实现对延时执行、周期执行的支持。对于延时任务调用 FutureTask#run 而对于周期性任务则调用 FutureTask#runAndReset 并且在成功之后根据 fixed-delay/fixed-rate 模式来设置下次执行时间并重新将任务塞到工作队列。 + +在调度线程池中无论是 runnable 还是 callable,无论是否需要延迟和定时,所有的任务都会被封装成 ScheduledFutureTask + +成员变量: + +* 任务序列号: + + ```java + private final long sequenceNumber; + ``` + +* 执行时间: + + ```java + private long time; // 任务可以被执行的时间,以纳秒表示 + private final long period; // 0 表示非周期任务,正数表示 fixed-rate 模式,负数表示 fixed-delay 模式的周期 + ``` + +* 实际的任务: + + ```java + RunnableScheduledFuture outerTask = this; + ``` + +* 任务在队列数组中的索引下标: + + ```java + int heapIndex; // -1 代表删除 + ``` + +成员方法: + +* 构造方法: + + ```java + ScheduledFutureTask(Runnable r, V result, long ns, long period) { + super(r, result); + // 任务的触发时间 + this.time = ns; + // 任务的周期,多长时间执行一次 + this.period = period; + this.sequenceNumber = sequencer.getAndIncrement(); + } + ``` + +* compareTo():ScheduledFutureTask 根据执行时间 time 正序排列,如果执行时间相同,在按照序列号 sequenceNumber 正序排列,任务需要放入 DelayedWorkQueue,延迟队列中使用该方法按照从小到大进行排序 + +* run():执行任务,**周期任务执行完后会重新放入线程池的阻塞队列** + + ```java + public void run() { + // 是否周期性,就是判断 period 是否为 0 + boolean periodic = isPeriodic(); + // 检查当前状态能否执行任务 + if (!canRunInCurrentRunState(periodic)) + // 取消任务 + cancel(false); + // 非周期任务直接执行 + else if (!periodic) + ScheduledFutureTask.super.run(); + // 周期任务的执行,正常完成后任务的状态不会变化,依旧是 NEW,且返回值为成功或失败,不会设置result属性。 + // 需要注意,如果本次任务执行出现异常,返回 false,后续的该任务不会再执行 + else if (ScheduledFutureTask.super.runAndReset()) { + // 设置周期任务的下一次执行时间 + setNextRunTime(); + // 任务的下一次执行安排,如果当前线程池状态可以执行周期任务,加入队列,并开启新线程 + reExecutePeriodic(outerTask); + } + } + ``` + + ```java + protected boolean runAndReset() { + // 任务不是新建的状态了,或者被别的线程执行了,直接返回 false + if (state != NEW || + !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread())) + return false; + boolean ran = false; + int s = state; + try { + Callable c = callable; + if (c != null && s == NEW) { + try { + // 执行方法,没有返回值 + c.call(); + ran = true; + } catch (Throwable ex) { + // 出现异常,把任务设置为异常状态,唤醒所有的 get 阻塞线程 + setException(ex); + } + } + } finally { + // 执行完成把执行线程引用置为 null + runner = null; + s = state; + // 如果线程被中断进行中断处理 + if (s >= INTERRUPTING) + handlePossibleCancellationInterrupt(s); + } + // 如果正常执行,返回 true + return ran && s == NEW; + } + ``` + + ```java + // 任务下一次的触发时间 + private void setNextRunTime() { + long p = period; + if (p > 0) + // fixed-rate 模式,时间设置为上一次时间 +p,两次任务执行的时间差 + time += p; + else + // fixed-delay 模式,下一次执行时间是当前这次任务结束的时间(就是现在) +delay 值 + time = triggerTime(-p); + } + ``` + +* reExecutePeriodic()**:准备任务的下一次执行,重新放入阻塞任务队列** + + ```java + // ScheduledThreadPoolExecutor#reExecutePeriodic + void reExecutePeriodic(RunnableScheduledFuture task) { + if (canRunInCurrentRunState(true)) { + // 放入任务队列 + super.getQueue().add(task); + // 再次检查是否可以执行,如果不能执行且任务还在队列中未被取走,则取消任务 + if (!canRunInCurrentRunState(true) && remove(task)) + task.cancel(false); + else + // 当前线程池状态可以执行周期任务,加入队列,并根据线程数量是否大于 核心线程数确定是否开启新线程 + ensurePrestart(); + } + } + ``` + +* cancel():取消任务 + + ```java + public boolean cancel(boolean mayInterruptIfRunning) { + // 调用父类 FutureTask#cancel 来取消任务 + boolean cancelled = super.cancel(mayInterruptIfRunning); + // removeOnCancel 用于控制任务取消后是否应该从队列中移除 + if (cancelled && removeOnCancel && heapIndex >= 0) + // 从等待队列中删除该任务,并调用 tryTerminate() 判断是否需要停止线程池 + remove(this); + return cancelled; + } + ``` + + + + + +*** + + + +##### 延迟队列 + +DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先队列 PriorityQueue (小根堆)存储元素 + +其他阻塞队列存储节点的数据结构大都是链表,延迟队列是数组,所以延迟队列出队头元素后需要让其他元素(尾)替换到头节点,防止空指针异常 + +成员变量: + +* 容量: + + ```java + private static final int INITIAL_CAPACITY = 16; // 初始容量 + private int size = 0; // 节点数量 + private RunnableScheduledFuture[] queue = + new RunnableScheduledFuture[INITIAL_CAPACITY]; // 存放节点 + ``` + +* 锁: + + ```java + private final ReentrantLock lock = new ReentrantLock(); // 控制并发 + private final Condition available = lock.newCondition();// + ``` + +* 阻塞等待头节点的线程: + + ```java + // 通过阻塞方式去获取头结点,那么 leader 线程的等待时间为头结点的延迟时间,其它线程则会陷入阻塞状态 + // leader 线程获取到头结点后需要发送信号唤醒其它线程 available.asignAll() + // 使用了 Leader/Follower 来避免不必要的等待,只让leader来等待需要等待的时间,其余线程无限等待直至被唤醒即可 + private Thread leader = null; + ``` + +成员方法 + +* offer():插入节点 + + ```java + public boolean offer(Runnable x) { + // 判空 + if (x == null) + throw new NullPointerException(); + RunnableScheduledFuture e = (RunnableScheduledFuture)x; + // 队列锁 + final ReentrantLock lock = this.lock; + lock.lock(); + try { + int i = size; + // 队列数量大于存放节点的数组长度,需要扩容 + if (i >= queue.length) + // 扩容为原来长度的 1.5 倍 + grow(); + size = i + 1; + // 插入的元素是第一个节点 + if (i == 0) { + queue[0] = e; + // 修改 ScheduledFutureTask 的 heapIndex 属性,表示该对象在队列里的下标 + setIndex(e, 0); + } else { + // 向上调整元素的位置 + siftUp(i, e); + } + // 【插入的元素是头节点,原先的 leader 等待的是原先的头节点,所以 leader 已经无效】 + if (queue[0] == e) { + // 将 leader 设置为 null + leader = null; + // 直接随便唤醒等待头结点的阻塞线程 + available.signal(); + } + } finally { + lock.unlock(); + } + return true; + } + ``` + + ```java + // 插入新节点后对堆进行调整,进行节点上移,保持其特性【节点的值小于子节点的值】,小顶堆 + private void siftUp(int k, RunnableScheduledFuture key) { + while (k > 0) { + // 父节点,就是堆排序 + int parent = (k - 1) >>> 1; + RunnableScheduledFuture e = queue[parent]; + // key 和父节点比,如果大于父节点可以直接返回,否则就继续上浮 + if (key.compareTo(e) >= 0) + break; + queue[k] = e; + setIndex(e, k); + k = parent; + } + queue[k] = key; + setIndex(key, k); + } + ``` + +* poll():非阻塞获取头结点,执行时间最近的 + + ```java + // 非阻塞获取 + public RunnableScheduledFuture poll() { + final ReentrantLock lock = this.lock; + lock.lock(); + try { + // 获取队头节点 + RunnableScheduledFuture first = queue[0]; + // 头结点为空或者的延迟时间没到返回 null + if (first == null || first.getDelay(NANOSECONDS) > 0) + return null; + else + // 头结点达到延迟时间,【尾节点成为替代节点下移调整堆结构】,返回头结点 + return finishPoll(first); + } finally { + lock.unlock(); + } + } + ``` + + ```java + private RunnableScheduledFuture finishPoll(RunnableScheduledFuture f) { + int s = --size; + // 获取尾节点 + RunnableScheduledFuture x = queue[s]; + // 置空 + queue[s] = null; + if (s != 0) + // 从索引处0开始向下调整 + siftDown(0, x); + // 出队的元素索引设置为 -1 + setIndex(f, -1); + return f; + } + ``` + +* take():阻塞获取头节点,读取当前堆中最小的也就是执行开始时间最近的任务 + + ```java + public RunnableScheduledFuture take() throws InterruptedException { + final ReentrantLock lock = this.lock; + lock.lockInterruptibly(); + try { + for (;;) { + // 头节点 + RunnableScheduledFuture first = queue[0]; + if (first == null) + // 等待队列不空,直至有任务通过 offer 入队并唤醒 + available.await(); + else { + // 获取头节点的剩延迟时间是否到时 + long delay = first.getDelay(NANOSECONDS); + if (delay <= 0) + // 获取头节点并调整堆,选择延迟时间最小的节点放入头部 + return finishPoll(first); + + // 逻辑到这说明头节点的延迟时间还没到 + first = null; + // 说明有 leader 线程在等待获取头节点,需要阻塞等待 + if (leader != null) + available.await(); + else { + // 没有 leader 线程,【当前线程作为leader线程,并设置头结点的延迟时间作为阻塞时间】 + Thread thisThread = Thread.currentThread(); + leader = thisThread; + try { + available.awaitNanos(delay); + } finally { + // 条件成立的情况: + // 1. 原先 thisThread == leader, 然后堆顶更新了,leader 被置为 null + // 2. 堆顶更新,offer 方法释放锁后,有其它线程通过 take/poll 拿到锁, + // 读到 leader == null,然后将自身更新为leader。 + if (leader == thisThread) + // leader 置为 null 用以接下来判断是否需要唤醒后继线程 + leader = null; + } + } + } + } + } finally { + // 没有 leader 线程没有,头结点不为 null,唤醒阻塞获取头节点的线程 + if (leader == null && queue[0] != null) + available.signal(); + lock.unlock(); + } + } + ``` + +* remove():删除节点,堆移除一个元素的时间复杂度是 O(log n),延迟任务维护了 heapIndex,直接访问的时间复杂度是 O(1),从而可以更快的移除元素,任务在队列中被取消后会进入该逻辑 + + ```java + public boolean remove(Object x) { + final ReentrantLock lock = this.lock; + lock.lock(); + try { + // 查找对象在队列数组中的下标 + int i = indexOf(x); + // 节点不存在,返回 false + if (i < 0) + return false; + // 修改元素的 heapIndex,-1 代表删除 + setIndex(queue[i], -1); + // 尾索引是长度-1 + int s = --size; + // 尾节点作为替代节点 + RunnableScheduledFuture replacement = queue[s]; + queue[s] = null; + // s == i 说明头节点就是尾节点,队列空了 + if (s != i) { + // 向下调整 + siftDown(i, replacement); + // 说明没发生调整 + if (queue[i] == replacement) + // 上移和下移不可能同时发生,替代节点大于子节点时下移,否则上移 + siftUp(i, replacement); + } + return true; + } finally { + lock.unlock(); + } + } + ``` + + + +**** + + + +#### 成员方法 + +##### 提交任务 + +* schedule():延迟执行方法,并指定执行的时间,默认是当前时间 + + ```java + public void execute(Runnable command) { + // 以零延时任务的形式实现 + schedule(command, 0, NANOSECONDS); + } + ``` + + ```java + public ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit) { + // 判空 + if (command == null || unit == null) throw new NullPointerException(); + // 没有做任何操作,直接将 task 返回,该方法主要目的是用于子类扩展 + RunnableScheduledFuture t = decorateTask(command, new ScheduledFutureTask( + command, null, triggerTime(delay, unit))); + // 延迟执行 + delayedExecute(t); + return t; + } + ``` + + ```java + // 返回【当前时间 + 延迟时间】,就是触发当前任务执行的时间 + private long triggerTime(long delay, TimeUnit unit) { + // 设置触发的时间 + return triggerTime(unit.toNanos((delay < 0) ? 0 : delay)); + } + long triggerTime(long delay) { + // 如果 delay < Long.Max_VALUE/2,则下次执行时间为当前时间 +delay + // 否则为了避免队列中出现由于溢出导致的排序紊乱,需要调用overflowFree来修正一下delay + return now() + ((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay)); + } + ``` + + overflowFree 的原因:如果某个任务的 delay 为负数,说明当前可以执行(其实早该执行了)。阻塞队列中维护任务顺序是基于 compareTo 比较的,比较两个任务的顺序会用 time 相减。那么可能出现一个 delay 为正数减去另一个为负数的 delay,结果上溢为负数,则会导致 compareTo 产生错误的结果 + + ```java + private long overflowFree(long delay) { + Delayed head = (Delayed) super.getQueue().peek(); + if (head != null) { + long headDelay = head.getDelay(NANOSECONDS); + // 判断一下队首的delay是不是负数,如果是正数就不用管,怎么减都不会溢出 + // 否则拿当前 delay 减去队首的 delay 来比较看,如果不出现上溢,排序不会乱 + // 不然就把当前 delay 值给调整为Long.MAX_VALUE + 队首 delay + if (headDelay < 0 && (delay - headDelay < 0)) + delay = Long.MAX_VALUE + headDelay; + } + return delay; + } + ``` + +* scheduleAtFixedRate():定时执行,一次任务的启动到下一次任务的启动的间隔 + + ```java + public ScheduledFuture scheduleAtFixedRate(Runnable command, long initialDelay, long period, + TimeUnit unit) { + if (command == null || unit == null) + throw new NullPointerException(); + if (period <= 0) + throw new IllegalArgumentException(); + // 任务封装,【指定初始的延迟时间和周期时间】 + ScheduledFutureTask sft =new ScheduledFutureTask(command, null, + triggerTime(initialDelay, unit), unit.toNanos(period)); + // 默认返回本身 + RunnableScheduledFuture t = decorateTask(command, sft); + sft.outerTask = t; + // 开始执行这个任务 + delayedExecute(t); + return t; + } + ``` + +* scheduleWithFixedDelay():定时执行,一次任务的结束到下一次任务的启动的间隔 + + ```java + public ScheduledFuture scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, + TimeUnit unit) { + if (command == null || unit == null) + throw new NullPointerException(); + if (delay <= 0) + throw new IllegalArgumentException(); + // 任务封装,【指定初始的延迟时间和周期时间】,周期时间为 - 表示是 fixed-delay 模式 + ScheduledFutureTask sft = new ScheduledFutureTask(command, null, + triggerTime(initialDelay, unit), unit.toNanos(-delay)); + RunnableScheduledFuture t = decorateTask(command, sft); + sft.outerTask = t; + delayedExecute(t); + return t; + } + ``` + + + +*** + + + +##### 运行任务 + +* delayedExecute():校验状态,延迟或周期性任务的主要执行方法 + + ```java + private void delayedExecute(RunnableScheduledFuture task) { + // 线程池是 SHUTDOWN 状态,需要执行拒绝策略 + if (isShutdown()) + reject(task); + else { + // 把当前任务放入阻塞队列,因为需要重新获取执行时间最近的 + super.getQueue().add(task); + // 线程池状态为 SHUTDOWN 并且不允许执行任务了,就从队列删除该任务,并设置任务的状态为取消状态 + if (isShutdown() && !canRunInCurrentRunState(task.isPeriodic()) && remove(task)) + task.cancel(false); + else + // 可以执行 + ensurePrestart(); + } + } + ``` + +* ensurePrestart():开启线程执行任务 + + ```java + // ThreadPoolExecutor#ensurePrestart + void ensurePrestart() { + int wc = workerCountOf(ctl.get()); + // worker数目小于corePoolSize,则添加一个worker。 + if (wc < corePoolSize) + // 第二个参数 true 表示采用核心线程数量限制,false 表示采用 maximumPoolSize + addWorker(null, true); + // corePoolSize = 0的情况,至少开启一个线程 + else if (wc == 0) + addWorker(null, false); + } + ``` + +* canRunInCurrentRunState():任务运行时都会被调用以校验当前状态是否可以运行任务 + + ```java + boolean canRunInCurrentRunState(boolean periodic) { + // isRunningOrShutdown 的参数为布尔值,true 则表示shutdown状态也返回true,否则只有running状态返回ture + // 根据是否时周期任务来判断是否shutdown了仍然可以执行。 + return isRunningOrShutdown(periodic ? continueExistingPeriodicTasksAfterShutdown : + executeExistingDelayedTasksAfterShutdown); + } + ``` + +* onShutdown():删除并取消工作队列中的不需要再执行的任务 + + ```java + void onShutdown() { + BlockingQueue q = super.getQueue(); + // shutdown 后是否仍然执行延时任务 + boolean keepDelayed = getExecuteExistingDelayedTasksAfterShutdownPolicy(); + // shutdown 后是否仍然执行周期任务 + boolean keepPeriodic = getContinueExistingPeriodicTasksAfterShutdownPolicy(); + // 如果两者皆不可则对队列中所有 任务 调用 cancel 取消并清空队列 + if (!keepDelayed && !keepPeriodic) { + for (Object e : q.toArray()) + if (e instanceof RunnableScheduledFuture) + ((RunnableScheduledFuture) e).cancel(false); + q.clear(); + } + else { + for (Object e : q.toArray()) { + if (e instanceof RunnableScheduledFuture) { + RunnableScheduledFuture t = (RunnableScheduledFuture)e; + // 不需要执行的任务删除并取消,已经取消的任务也需要从队列中删除 + if ((t.isPeriodic() ? !keepPeriodic : !keepDelayed) || + t.isCancelled()) { + if (q.remove(t)) + t.cancel(false); + } + } + } + } + // 因为任务被从队列中清理掉,所以需要调用 tryTerminate 尝试改变 executor 的状态 + tryTerminate(); + } + ``` + + + **** @@ -11327,7 +11939,7 @@ ConcurrentSkipListMap 提供了一种线程安全的并发访问的排序映射 ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentSkipListMap数据结构.png) -BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引指向链表最下面的节点** +BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引都指向链表最下面的节点** @@ -11359,28 +11971,29 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引指向 ```java static final class Node{ - final K key; // key 是 final 的, 说明节点一旦定下来, 除了删除, 不然不会改动 key - volatile Object value; // 对应的 value - volatile Node next; // 下一个节点 + final K key; // key 是 final 的, 说明节点一旦定下来, 除了删除, 一般不会改动 key + volatile Object value; // 对应的 value + volatile Node next; // 下一个节点 } ``` -* 索引节点 Index +* 索引节点 Index,只有向下和向右的指针 ```java static class Index{ - final Node node; // 索引指向的节点, - final Index down; // 下边level层的Index,分层索引 + final Node node; // 索引指向的节点, + final Index down; // 下边level层的Index,分层索引 volatile Index right; // 右边的Index - // 在index本身和succ之间插入一个新的节点newSucc + // 在 index 本身和 succ 之间插入一个新的节点 newSucc final boolean link(Index succ, Index newSucc){ Node n = node; newSucc.right = succ; + // 把当前节点的右指针从 succ 改为 newSucc return n.value != null && casRight(succ, newSucc); } - // 将当前的节点 index 设置其的 right 为 succ.right 等于删除 succ 节点 + // 断开当前节点和 succ 节点,将当前的节点 index 设置其的 right 为 succ.right,就是把 succ 删除 final boolean unlink(Index succ){ return node.value != null && casRight(succ, succ.right); } @@ -11391,7 +12004,7 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引指向 ```java static final class HeadIndex extends Index { - final int level;// 标示索引层级,所有的HeadIndex都指向同一个Base_header节点 + final int level; // 表示索引层级,所有的 HeadIndex 都指向同一个 Base_header 节点 HeadIndex(Node node, Index down, Index right, int level) { super(node, down, right); this.level = level; @@ -11424,8 +12037,8 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引指向 entrySet = null; values = null; descendingMap = null; - //初始化索引头节点,Node的Key为null,value为BASE_HEADER对象,下一个节点为null - //head的分层索引down为null,链表的后续索引right为null,层级level为第一层。 + // 初始化索引头节点,Node 的 Key 为 null,value 为 BASE_HEADER 对象,下一个节点为 null + // head 的分层索引 down 为 null,链表的后续索引 right 为 null,层级 level 为第一层 head = new HeadIndex(new Node(null, BASE_HEADER, null), null, null, 1); } @@ -11434,7 +12047,7 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引指向 * cpr:排序 ```java - // x是比较者,y是被比较者,比较者大于被比较者 返回正数,小于返回负数,相等返回0 + // x 是比较者,y 是被比较者,比较者大于被比较者 返回正数,小于返回负数,相等返回 0 static final int cpr(Comparator c, Object x, Object y) { return (c != null) ? c.compare(x, y) : ((Comparable)x).compareTo(y); } @@ -11448,43 +12061,45 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引指向 ##### 添加方法 -* findPredecessor():寻找前驱节点 +* findPredecessor():寻找前置节点 - 从最上层的头索引开始向右查找(链表的后续索引),如果后续索引的节点的Key大于要查找的Key,则头索引移到下层链表,在下层链表查找,以此反复,一直查找到没有下层的分层索引为止,返回该索引的节点。如果后续索引的节点的Key小于要查找的Key,则在该层链表中向后查找。由于查找的key可能永远大于索引节点的 key,所以只能找到目标的前置索引节点。如果遇到空值索引的存在,通过CAS来断开索引 + 从最上层的头索引开始向右查找(链表的后续索引),如果后续索引的节点的 key 大于要查找的 key,则头索引移到下层链表,在下层链表查找,以此反复,一直查找到没有下层的分层索引为止,返回该索引的节点。如果后续索引的节点的 key 小于要查找的 key,则在该层链表中向后查找。由于查找的 key 可能永远大于索引节点的 key,所以只能找到目标的前置索引节点。如果遇到空值索引的存在,通过 CAS 来断开索引 ```java private Node findPredecessor(Object key, Comparator cmp) { if (key == null) throw new NullPointerException(); // don't postpone errors for (;;) { - // 1.初始化数据q是head,r是最顶层h的右Index节点 + // 1.初始数据 q 是 head,r 是最顶层 h 的右 Index 节点 for (Index q = head, r = q.right, d;;) { - //2.右索引节点不为空,则进行向下查找 + // 2.右索引节点不为空,则进行向下查找 if (r != null) { Node n = r.node; K k = n.key; - //3.n.value为null说明节点n正在删除的过程中 + // 3.n.value 为 null 说明节点 n 正在删除的过程中,此时【当前线程帮其删除索引】 if (n.value == null) { - //在index层直接删除r索引节点,用在删除节点中 + // 在 index 层直接删除 r 索引节点 if (!q.unlink(r)) - break;//重新从 head 节点开始查找,break到步骤1 - //删除节点r成功,获取新的r节点, 回到步骤 2 - //还是从这层索引开始向右遍历, 直到 r == null + // 删除失败重新从 head 节点开始查找,break 一个 for 到步骤 1,又从初始值开始 + break; + + // 删除节点 r 成功,获取新的 r 节点, r = q.right; + // 回到步骤 2,还是从这层索引开始向右遍历 continue; } - //4.若参数key > r.node.key,则继续向右遍历, continue到步骤2处 - // 若参数key < r.node.key,直接跳到步骤5 + // 4.若参数 key > r.node.key,则继续向右遍历, continue 到步骤 2 处获取右节点 + // 若参数 key < r.node.key,说明需要进入下层索引,到步骤 5 if (cpr(cmp, key, k) > 0) { q = r; r = r.right; continue; } } - //5.先让d指向q的下一层,判断是否是null,是则说明已经到了数据层,也就是第一层 + // 5.先让 d 指向 q 的下一层,判断是否是 null,是则说明已经到了数据层,也就是第一层 if ((d = q.down) == null) return q.node; - //6.未到数据层, 进行重新赋值向下扫描 + // 6.未到数据层, 进行重新赋值向下扫描 q = d; //q指向d r = d.right;//r指向q的后续索引节点 } @@ -11492,16 +12107,9 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引指向 } ``` - ```java - final boolean unlink(Index succ) { - return node.value != null && casRight(succ, succ.right); - // this.node = q - } - ``` - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentSkipListMap-Put流程.png) -* put() +* put():添加数据 ```java public V put(K key, V value) { @@ -11515,88 +12123,90 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引指向 ```java private V doPut(K key, V value, boolean onlyIfAbsent) { Node z; - if (key == null)// 非空判断,key不能为空 + // 非空判断,key不能为空 + if (key == null) throw new NullPointerException(); Comparator cmp = comparator; - // outer循环,处理并发冲突等其他需要重试的情况 + // outer 循环,【把待插入数据插入到数据层的合适的位置,并在扫描过程中处理 已删除(value = null) 的数据】 outer: for (;;) { //0.for (;;) - //1.将 key 对应的前继节点找到, b为前继节点, n是前继节点的next, - // 若没发生条件竞争,最终key在b与n之间 (找到的b在base_level上) + //1.将 key 对应的前继节点找到, b 为前继节点,是数据层的, n 是前继节点的 next, + // 若没发生条件竞争,最终 key 在 b 与 n 之间 (找到的 b 在 base_level 上) for (Node b = findPredecessor(key, cmp), n = b.next;;) { - // 2.n不为null时b不是链表的最后一个节点 + // 2.n 不为 null 说明 b 不是链表的最后一个节点 if (n != null) { Object v; int c; - //3.获取 n 的右节点 + // 3.获取 n 的右节点 Node f = n.next; - //4.条件竞争 - // 并发下其他线程在b之后插入节点或直接删除节点n, break到步骤0 + // 4.条件竞争,并发下其他线程在 b 之后插入节点或直接删除节点 n, break 到步骤 0 if (n != b.next) break; - // 若节点n已经删除, 则调用helpDelete进行帮助删除 + // 若节点 n 已经删除, 则调用 helpDelete 进行帮助删除节点 if ((v = n.value) == null) { n.helpDelete(b, f); break; } - //5.节点b被删除中,则break到步骤0, - // 调用findPredecessor帮助删除index层的数据, - // node层的数据会通过helpDelete方法进行删除 + // 5.节点 b 被删除中,则 break 到步骤 0, + // 【调用findPredecessor帮助删除index层的数据, node层的数据会通过helpDelete方法进行删除】 if (b.value == null || v == n) break; - //6.若key > n.key,则进行向后扫描 - // 若key < n.key,则证明key应该存储在b和n之间 + // 6.若 key > n.key,则进行向后扫描 + // 若 key < n.key,则证明 key 应该存储在 b 和 n 之间 if ((c = cpr(cmp, key, n.key)) > 0) { b = n; n = f; continue; } - //7.key的值和n.key相等,则可以直接覆盖赋值 + // 7.key 的值和 n.key 相等,则可以直接覆盖赋值 if (c == 0) { - // onlyIfAbsent默认false, + // onlyIfAbsent 默认 false, if (onlyIfAbsent || n.casValue(v, value)) { @SuppressWarnings("unchecked") V vv = (V)v; - return vv;//返回被覆盖的值 + // 返回被覆盖的值 + return vv; } - // cas失败,返回0,重试 + // cas失败,break 一层循环,返回 0 重试 break; } // else c < 0; fall through } - //8.此时的情况n.key > key > b.key,对应流程图1中的7 - // 创建z节点指向n + // 8.此时的情况 n.key > key > b.key,对应流程图1中的7,创建z节点指向n z = new Node(key, value, n); - //9.尝试把b.next从n设置成z + // 9.尝试把 b.next 从 n 设置成 z if (!b.casNext(n, z)) // cas失败,返回到步骤0,重试 break; - //10.break outer后, 上面的for循环不会再执行, 而后执行下面的代码 + // 10.break outer 后, 上面的 for 循环不会再执行, 而后执行下面的代码 break outer; } } - // 以上插入节点已经完成,剩下的任务要根据随机数的值来表示是否向上增加层数与上层索引 + // 【以上插入节点已经完成,剩下的任务要根据随机数的值来表示是否向上增加层数与上层索引】 // 随机数 int rnd = ThreadLocalRandom.nextSecondarySeed(); - //如果随机数的二进制与10000000000000000000000000000001进行与运算为0 - //即随机数的二进制最高位与最末尾必须为0,其他位无所谓,就进入该循环 - //如果随机数的二进制最高位与最末位不为0,不增加新节点的层数 + // 如果随机数的二进制与 10000000000000000000000000000001 进行与运算为 0 + // 即随机数的二进制最高位与最末尾必须为 0,其他位无所谓,就进入该循环 + // 如果随机数的二进制最高位与最末位不为 0,不增加新节点的层数 - //11.判断是否需要添加level + // 11.判断是否需要添加 level,32 位 if ((rnd & 0x80000001) == 0) { - //索引层level,从1开始 + // 索引层 level,从 1 开始,就是最底层 int level = 1, max; - //12.判断最低位前面有几个1,有几个leve就加几:0..0 0001 1110,这是4个,则1+4=5 - // 最大有30个就是 1 + 30 + // 12.判断最低位前面有几个 1,有几个leve就加几:0..0 0001 1110,这是4个,则1+4=5 + // 最大有30个就是 1 + 30 = 31 while (((rnd >>>= 1) & 1) != 0) ++level; - Index idx = null;//最终指向z节点,就是添加的节点 - HeadIndex h = head;//指向头索引节点 - //13.判断level是否比当前最高索引小,图中max为3 + // 最终指向 z 节点,就是添加的节点 + Index idx = null; + // 指向头索引节点 + HeadIndex h = head; + + // 13.判断level是否比当前最高索引小,图中 max 为 3 if (level <= (max = h.level)) { for (int i = 1; i <= level; ++i) - //根据层数level不断创建新增节点的上层索引,索引的后继索引留空 - //第一次idx为null,也就是下层索引为空,第二次把上次的索引作为下层索引 + // 根据层数level不断创建新增节点的上层索引,索引的后继索引留空 + // 第一次idx为null,也就是下层索引为空,第二次把上次的索引作为下层索引,【类似头插法】 idx = new Index(z, idx, null); // 循环以后的索引结构 // index-3 ← idx @@ -11607,13 +12217,12 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引指向 // ↓ // z-node } - //14.若level > max,则只增加一层index索引层,3+1=4 + // 14.若 level > max,则只增加一层 index 索引层,3 + 1 = 4 else { level = max + 1; - //创建一个index数组,长度是level+1,假设level是4,创建的数组长度为5 - @SuppressWarnings("unchecked")Index[] idxs = - (Index[])new Index[level+1]; - //index[0]的数组slot 并没有使用,只使用 [1,level]这些数组slot了 + //创建一个 index 数组,长度是 level+1,假设 level 是4,创建的数组长度为 5 + Index[] idxs = (Index[])new Index[level+1]; + //index[0]的数组 slot 并没有使用,只使用 [1,level] 这些数组的 slot for (int i = 1; i <= level; ++i) idxs[i] = idx = new Index(z, idx, null); // index-4 ← idx @@ -11630,17 +12239,17 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引指向 h = head; //获取头索引的层数 int oldLevel = h.level; - // 如果level <= oldLevel,说明其他线程进行了index层增加操作,退出循环 + // 如果 level <= oldLevel,说明其他线程进行了 index 层增加操作,退出循环 if (level <= oldLevel) break; - //定义一个新的头索引节点 + // 定义一个新的头索引节点 HeadIndex newh = h; - //获取头索引的节点,就是BASE_HEADER + // 获取头索引的节点,就是 BASE_HEADER Node oldbase = h.node; - // 升级baseHeader索引,升高一级,并发下可能升高多级 - for (int j = oldLevel+1; j <= level; ++j) + // 升级 baseHeader 索引,升高一级,并发下可能升高多级 + for (int j = oldLevel + 1; j <= level; ++j) newh = new HeadIndex(oldbase, newh, idxs[j], j); - // 执行完for循环之后,baseHeader 索引长这个样子.. + // 执行完for循环之后,baseHeader 索引长这个样子,这里只升高一级 // index-4 → index-4 ← idx // ↓ ↓ // index-3 index-3 @@ -11651,53 +12260,56 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引指向 // ↓ ↓ // baseHeader → .... → z-node - //cas成功后,map.head字段指向最新的headIndex,baseHeader的index-4s + // cas 成功后,map.head 字段指向最新的 headIndex,baseHeader 的 index-4 if (casHead(h, newh)) { - //h指向最新的 index-4 节点 + // h 指向最新的 index-4 节点 h = newh; - //idx指向z-node的index-3节点, - //因为从index-3-index-1的这些z-node索引节点 都没有插入到索引链表 + // idx 指向 z-node 的 index-3 节点, + // 因为从 index-3 - index-1 的这些 z-node 索引节点 都没有插入到索引链表 idx = idxs[level = oldLevel]; break; } } } - //15.把新加的索引插入索引链表中,有上述两种情况,一种索引高度不变,另一种是高度加1 + // 15.【把新加的索引插入索引链表中】,有上述两种情况,一种索引高度不变,另一种是高度加 1 + // 要插入的是第几层的索引 splice: for (int insertionLevel = level;;) { - //获取头索引的层数, 情况1是3,情况2是4 + // 获取头索引的层数,情况 1 是 3,情况 2 是 4 int j = h.level; + // 【遍历 insertionLevel 层的索引,找到合适的插入位置】 for (Index q = h, r = q.right, t = idx;;) { - //如果头索引为null或者新增节点索引为null,退出插入索引的总循环 + // 如果头索引为 null 或者新增节点索引为 null,退出插入索引的总循环 if (q == null || t == null) - //此处表示有其他线程删除了头索引或者新增节点的索引 + // 此处表示有其他线程删除了头索引或者新增节点的索引 break splice; - //头索引的链表后续索引存在,如果是新层则为新节点索引,如果是老层则为原索引 + // 头索引的链表后续索引存在,如果是新层则为新节点索引,如果是老层则为原索引 if (r != null) { - //获取r的节点 + // 获取r的节点 Node n = r.node; - //插入的key和n.key的比较值 + // 插入的key和n.key的比较值 int c = cpr(cmp, key, n.key); - //删除空值索引 + // 【删除空值索引】 if (n.value == null) { if (!q.unlink(r)) break; r = q.right; continue; } - //key > n.key,向右扫描 + // key > n.key,向右扫描 if (c > 0) { q = r; r = r.right; continue; } } - // 执行到这里,说明key < n.key,判断是否第j层插入新增节点的前置索引 + // 执行到这里,说明 key < n.key,判断是否是第 j 层插入新增节点的前置索引 if (j == insertionLevel) { - // 将新索引节点t插入q r之间 + // 【将新索引节点 t 插入 q r 之间】 if (!q.link(r, t)) break; - //如果新增节点的值为null,表示该节点已经被其他线程删除 + // 如果新增节点的值为 null,表示该节点已经被其他线程删除 if (t.node.value == null) { + // 找到该节点 findNode(key); break splice; } @@ -11705,7 +12317,7 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引指向 if (--insertionLevel == 0) break splice; } - //其他节点随着插入节点的层数下移而下移 + // 其他节点随着插入节点的层数下移而下移 if (--j >= insertionLevel && j < level) t = t.down; q = q.down; @@ -11721,7 +12333,7 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引指向 ```java private Node findNode(Object key) { - //原理与doGet相同,无非是findNode返回节点,doGet返回value + // 原理与doGet相同,无非是 findNode 返回节点,doGet 返回 value if ((c = cpr(cmp, key, n.key)) == 0) return n; } @@ -11736,19 +12348,15 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引指向 ##### 获取方法 -* get(key) - - 寻找 key 的前继节点 b (这时b.next = null || b.next > key, 则说明不存key对应的 Node) - - 接着就判断 b, b.next 与 key之间的关系(其中有些 helpDelete操作) +* get(key):获取对应的数据 ```java public V get(Object key) { return doGet(key); } ``` - -* doGet() + +* doGet():扫描过程会对已 value == null 的元素进行删除处理 ```java private V doGet(Object key) { @@ -11756,31 +12364,31 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引指向 throw new NullPointerException(); Comparator cmp = comparator; outer: for (;;) { - //1.找到最底层节点的前置节点 + // 1.找到最底层节点的前置节点 for (Node b = findPredecessor(key, cmp), n = b.next;;) { Object v; int c; - //2.如果该前置节点的链表后续节点为null,说明不存在该节点 + // 2.【如果该前置节点的链表后续节点为 null,说明不存在该节点】 if (n == null) break outer; - //b → n → f + // b → n → f Node f = n.next; - //3.如果n不为前置节点的后续节点,表示已经有其他线程删除了该节点 + // 3.如果n不为前置节点的后续节点,表示已经有其他线程删除了该节点 if (n != b.next) break; - //4.如果后续节点的值为null,删除该节点 + // 4.如果后续节点的值为null,删除该节点 if ((v = n.value) == null) { n.helpDelete(b, f); break; } - //5.如果前置节点已被其他线程删除,重新循环 + // 5.如果前置节点已被其他线程删除,重新循环 if (b.value == null || v == n) break; - //6.如果要获取的key与后续节点的key相等,返回节点的value + // 6.如果要获取的key与后续节点的key相等,返回节点的value if ((c = cpr(cmp, key, n.key)) == 0) { @SuppressWarnings("unchecked") V vv = (V)v; return vv; } - //7.key < n.key,说明被其他线程删除了,或者不存在该节点 + // 7.key < n.key,因位 key > b.key,b 和 n 相连,说明不存在该节点或者被其他线程删除了 if (c < 0) break outer; b = n; @@ -11810,13 +12418,13 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引指向 throw new NullPointerException(); Comparator cmp = comparator; outer: for (;;) { - //1.找到最底层目标节点的前置节点,b.key < key + // 1.找到最底层目标节点的前置节点,b.key < key for (Node b = findPredecessor(key, cmp), n = b.next;;) { Object v; int c; - //2.如果该前置节点的链表后续节点为null,退出循环 + // 2.如果该前置节点的链表后续节点为 null,退出循环,说明不存在这个元素 if (n == null) break outer; - //b → n → f + // b → n → f Node f = n.next; if (n != b.next) // inconsistent read break; @@ -11835,21 +12443,21 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引指向 n = f; continue; } - //5.到这里是 key = n.key,value是n.value + //5.到这里是 key = n.key,value 不为空的情况下判断 value 和 n.value 是否相等 if (value != null && !value.equals(v)) break outer; - //6.把n节点的value置空 + //6.把 n 节点的 value 置空 if (!n.casValue(v, null)) break; - //7.给n添加一个删除标志mark,mark.next=f,然后把b.next设置为f,成功后n出队 + //7.给 n 添加一个删除标志 mark,mark.next=f,然后把 b.next 设置为 f,成功后 n 出队 if (!n.appendMarker(f) || !b.casNext(n, f)) - //对key对应的index进行删除 + // 对 key 对应的 index 进行删除,调用了 findPredecessor 方法 findNode(key); else { - //进行操作失败后通过findPredecessor中进行index的删除 + // 进行操作失败后通过 findPredecessor 中进行 index 的删除 findPredecessor(key, cmp); if (head.right == null) - //进行headIndex 对应的index 层的删除 + // 进行headIndex 对应的index 层的删除 tryReduceLevel(); } @SuppressWarnings("unchecked") V vv = (V)v; @@ -11867,9 +12475,9 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引指向 * appendMarker() ```java - //添加删除标记节点 + // 添加删除标记节点 boolean appendMarker(Node f) { - //通过CAS生成一个key为null,value为this,next为f的标记节点 + // 通过 CAS 让 n.next 指向一个 key 为 null,value 为 this,next 为 f 的标记节点 return casNext(f, new Node(f)); } ``` @@ -11877,15 +12485,15 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引指向 * helpDelete() ```java - //将添加了删除标记的节点清除 + // 将添加了删除标记的节点清除,参数是该节点的前驱和后继节点 void helpDelete(Node b, Node f) { - //this节点的后续节点为f,且本身为b的后续节点,一般都是正确的,除非被别的线程删除 + // this 节点的后续节点为 f,且本身为 b 的后续节点,一般都是正确的,除非被别的线程删除 if (f == next && this == b.next) { - //如果n还还没有被标记 + // 如果 n 还还没有被标记 if (f == null || f.value != f) casNext(f, new Node(f)); else - //通过CAS,将b的下一个节点n变成f.next,即成为图中的样式 + // 通过 CAS,将 b 的下一个节点 n 变成 f.next,即成为图中的样式 b.casNext(this, f.next); } } @@ -11906,9 +12514,9 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引指向 h.right == null && //设置头索引 casHead(h, d) && - //重新检查 + // 重新检查 h.right != null) - //重新检查返回true,说明其他线程增加了索引层级,把索引头节点设置回来 + // 重新检查返回true,说明其他线程增加了索引层级,把索引头节点设置回来 casHead(d, h); } ``` From 303d50de471b765a1eb00707b2954b9311c06ea6 Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 23 Aug 2021 15:17:30 +0800 Subject: [PATCH 035/168] Update Java Notes --- DB.md | 95 +++++---- Java.md | 595 ++++++++++++++++++++++++++++---------------------------- Prog.md | 218 ++++++++++++--------- SSM.md | 2 +- 4 files changed, 480 insertions(+), 430 deletions(-) diff --git a/DB.md b/DB.md index b931c4b..df0861e 100644 --- a/DB.md +++ b/DB.md @@ -3654,7 +3654,7 @@ MERGE存储引擎: #### 基本介绍 -MySQL官方对索引的定义为:索引(index)是帮助 MySQL 高效获取数据的一种数据结构,本质是排好序的快速查找数据结构。在表数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式指向数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。 +MySQL 官方对索引的定义为:索引(index)是帮助 MySQL 高效获取数据的一种数据结构,本质是排好序的快速查找数据结构。在表数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式指向数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。 索引使用:一张数据表,用于保存数据;一个索引配置文件,用于保存索引;每个索引都指向了某一个数据 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-索引的介绍.png) @@ -8290,9 +8290,9 @@ Redis 所有操作都是**原子性**的,采用**单线程**机制,命令是 #### 应用 -主页高频访问信息显示控制,例如新浪微博大V主页显示粉丝数与微博数量 +主页高频访问信息显示控制,例如新浪微博大 V 主页显示粉丝数与微博数量 -* 在 Redis 中为大V用户设定用户信息,以用户主键和属性值作为 key,后台设定定时刷新策略 +* 在 Redis 中为大 V 用户设定用户信息,以用户主键和属性值作为 key,后台设定定时刷新策略 ```sh set user:id:3506728370:fans 12210947 @@ -8358,11 +8358,11 @@ struct sdshdr{ 数据存储结构:一个存储空间保存多个键值对数据 -hash类型:底层使用**哈希表**结构实现数据存储 +hash 类型:底层使用**哈希表**结构实现数据存储 -类似Map结构,左边是key,右边是值,中间叫field字段,本质上**hash存了一个key-value的存储空间** +Redis 中的 hash 类似于 Java 中的 `Map>`,左边是 key,右边是值,中间叫 field 字段,本质上 **hash 存了一个 key-value 的存储空间** hash 是指的一个数据类型,并不是一个数据 @@ -8438,6 +8438,8 @@ user:id:3506728370 → {"name":"春晚","fans":12210862,"blogs":83} +可以实现购物车的功能,key 对应着每个用户,存储空间存储购物车的信息 + *** @@ -8452,8 +8454,8 @@ user:id:3506728370 → {"name":"春晚","fans":12210862,"blogs":83} 当存储的数据量比较小的情况下,Redis 才使用压缩列表来实现字典类型,具体需要满足两个条件: -- 当键值对个数小于 hash-max-ziplist-entries 配置(默认512个) -- 所有键值都小于 hash-max-ziplist-value 配置(默认64字节) +- 当键值对个数小于 hash-max-ziplist-entries 配置(默认 512 个) +- 所有键值都小于 hash-max-ziplist-value 配置(默认 64 字节) ziplist 使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比 hashtable 更加优秀,当 ziplist 无法满足哈希类型时,Redis 会使用 hashtable 作为哈希的内部实现,因为此时 ziplist 的读写效率会下降,而 hashtable 的读写时间复杂度为 O(1) @@ -8568,6 +8570,10 @@ list 类型:保存多个数据,底层使用**双向链表**存储结构实 * 使用队列模型解决多路信息汇总合并的问题 * 使用栈模型解决最新消息的问题 +微信文章订阅公众号: + +* 比如订阅了两个公众号,它们发布了两篇文章,文章 ID 分别为 666 和 888,可以通过执行 `LPUSH key 666 888` 命令推送给我 + **** @@ -8715,13 +8721,9 @@ set 类型:与 hash 存储结构哈希表完全相同,只是仅存储键不 2. 白名单:对于安全性更高的应用访问,仅仅靠黑名单是不能解决安全问题的,此时需要设定可访问的用户群体, 依赖白名单做更为苛刻的访问验证 +3. 随机操作可以实现抽奖功能 -解决方案: - -* 设定用户鉴别规则,周期性更新满足规则的黑名单加入 set 集合,用户行为信息达到后与黑名单进行对比 -* 黑名单过滤 IP 地址:应用于开放游客访问权限的信息源 -* 黑名单过滤设备信息:应用于限定访问设备的信息源 -* 黑名单过滤用户:应用于基于访问权限的信息源 +4. 集合的交并补可以实现微博共同关注的查看,可以根据共同关注或者共同喜欢推荐相关内容 @@ -8737,7 +8739,7 @@ set 类型:与 hash 存储结构哈希表完全相同,只是仅存储键不 * hashtable(哈希表,字典):当无法满足 intset 条件时,Redis 会使用 hashtable 作为集合的内部实现 -整数集合(intset)是 Redis 用于保存整数值的集合抽象数据结构,可以保存类型为 int16_t、int32_t 或者 int64_t的整数值,并且保证集合中的元素是**有序不重复**的 +整数集合(intset)是 Redis 用于保存整数值的集合抽象数据结构,可以保存类型为 int16_t、int32_t 或者 int64_t 的整数值,并且保证集合中的元素是**有序不重复**的 @@ -8774,7 +8776,7 @@ sorted_set类型:在 set 的存储结构基础上添加可排序字段,类 ```sh zadd key score1 member1 [score2 member2] #添加数据 zrem key member [member ...] #删除数据 - zremrangebyrank key start stop #删除指定索引的数据 + zremrangebyrank key start stop #删除指定索引范围的数据 zremrangebyscore key min max #删除指定分数区间内的数据 zscore key member #获取指定值的分数 zincrby key increment member #指定值的分数增加increment @@ -8783,8 +8785,8 @@ sorted_set类型:在 set 的存储结构基础上添加可排序字段,类 * 查询操作 ```sh - zrange key start stop [WITHSCORES] #获取全部数据,升序,WITHSCORES代表显示分数 - zrevrange key start stop [WITHSCORES] #获取全部数据,降序 + zrange key start stop [WITHSCORES] #获取指定范围的数据,升序,WITHSCORES 代表显示分数 + zrevrange key start stop [WITHSCORES] #获取指定范围的数据,降序 zrangebyscore key min max [WITHSCORES] [LIMIT offset count] #按条件获取数据,从小到大 zrevrangebyscore key max min [WITHSCORES] [...] #从大到小 @@ -9768,9 +9770,15 @@ Redis 分布式锁的基本使用,悲观锁 * 对于返回设置成功的,拥有控制权,进行下一步的具体业务操作 * 对于返回设置失败的,不具有控制权,排队或等待 - NX:只在键不存在时,才对键进行设置操作,`SET key value NX` 效果等同于 `SETNX key value` + `NX`:只在键不存在时,才对键进行设置操作,`SET key value NX` 效果等同于 `SETNX key value` - XX :只在键已经存在时,才对键进行设置操作 + `XX` :只在键已经存在时,才对键进行设置操作 + + `EX`:设置键 key 的过期时间,单位时秒 + + `PX`:设置键 key 的过期时间,单位时毫秒 + + 说明:由于 `SET` 命令加上选项已经可以完全取代 SETNX、SETEX、PSETEX 的功能,Redis 不推荐使用这几个命令 * 操作完毕通过 del 操作释放锁 @@ -9864,7 +9872,7 @@ TTL 返回的值有三种情况:正数,-1,-2 创建一个定时器,当 key 设置有过期时间,且过期时间到达时,由定时器任务立即执行对键的删除操作 - 优点:节约内存,到时就删除,快速释放掉不必要的内存占用 -- 缺点:无论 CPU 此时负载量多高,均占用 CPU,会影响 redis 服务器响应时间和指令吞吐量 +- 缺点:无论 CPU 此时负载量多高,均占用 CPU,会影响 Redis 服务器响应时间和指令吞吐量 - 总结:用处理器性能换取存储空间(拿时间换空间) @@ -9875,7 +9883,7 @@ TTL 返回的值有三种情况:正数,-1,-2 #### 惰性删除 -数据到达过期时间,不做处理,等下次访问该数据时,我们需要判断: +数据到达过期时间,不做处理,等下次访问该数据时,需要判断: * 如果未过期,返回数据 * 如果已过期,删除,返回不存在 @@ -9884,7 +9892,7 @@ TTL 返回的值有三种情况:正数,-1,-2 特点: -* 优点:节约CPU性能,发现必须删除的时候才删除 +* 优点:节约 CPU 性能,发现必须删除的时候才删除 * 缺点:内存压力很大,出现长期占用内存的数据 * 总结:用存储空间换取处理器性能(拿空间换时间) @@ -9898,11 +9906,11 @@ TTL 返回的值有三种情况:正数,-1,-2 定时删除和惰性删除这两种方案都是走的极端,定期删除就是折中方案 -定期删除是周期性轮询 redis 库中的时效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删除频度 +定期删除是周期性轮询 Redis 库中的时效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删除频度 定期删除方案: -- Redis启动服务器初始化时,读取配置 server.hz 的值,默认为10。执行指令可以查看:info server +- Redis 启动服务器初始化时,读取配置 server.hz 的值,默认为 10,执行指令 info server 可以查看 - 每秒钟执行 server.hz 次 serverCron() → databasesCron() → activeExpireCycle() @@ -9954,9 +9962,9 @@ TTL 返回的值有三种情况:正数,-1,-2 数据淘汰策略:当新数据进入 redis 时,在执行每一个命令前,会调用 **freeMemoryIfNeeded()** 检测内存是否充足。如果内存不满足新加入数据的最低存储要求,redis 要临时删除一些数据为当前指令清理存储空间,清理数据的策略称为**逐出算法** -注意:逐出数据的过程不是 100% 能够清理出足够的可使用的内存空间,如果不成功则反复执行,当对所有数据尝试完毕,如不能达到内存清理的要求,将出现错误信息如下: +逐出数据的过程不是 100% 能够清理出足够的可使用的内存空间,如果不成功则反复执行,当对所有数据尝试完毕,如不能达到内存清理的要求,**出现 Redis 内存打满异常**: -```shell +```sh (error) OOM command not allowed when used memory >'maxmemory' ``` @@ -9968,13 +9976,20 @@ TTL 返回的值有三种情况:正数,-1,-2 #### 策略配置 -影响数据淘汰的相关配置如下,配置 conf 文件: +Redis 如果不设置最大内存大小或者设置最大内存大小为 0,在 64 位操作系统下不限制内存大小,在 32 位操作系统默认为 3GB 内存,一般推荐设置 Redis 内存为最大物理内存的四分之三 -* 最大可使用内存,即占用物理内存的比例,默认值为 0,表示不限制。生产环境中根据需求设定,通常设置在 50% 以上 +内存配置方式: - ```sh - maxmemory ?mb - ``` +* 通过修改文件配置(永久生效):修改配置文件 maxmemory 字段,单位为字节 + +* 通过命令修改(重启失效): + + * `config set maxmemory 104857600`:设置 Redis 最大占用内存为 100MB + * `config get maxmemory`:获取 Redis 最大占用内存 + + * `info` :可以查看 Redis 内存使用情况,`used_memory_human` 字段表示实际已经占用的内存,`maxmemory` 表示最大占用内存 + +影响数据淘汰的相关配置如下,配置 conf 文件: * 每次选取待删除数据的个数,采用随机获取数据的方式作为待检测删除数据,防止全库扫描,导致严重的性能消耗,降低读写性能 @@ -9990,21 +10005,21 @@ TTL 返回的值有三种情况:正数,-1,-2 数据删除的策略 policy:3 类 8 种 - 第一类:检测易失数据(可能会过期的数据集 server.db[i].expires ): + 第一类:检测易失数据(可能会过期的数据集 server.db[i].expires): ```sh - volatile-lru #挑选最近最久未使用使用的数据淘汰 - volatile-lfu #挑选最近使用次数最少的数据淘汰 - volatile-ttl #挑选将要过期的数据淘汰 - volatile-random #任意选择数据淘汰 + volatile-lru # 对设置了过期时间的 key 选择最近最久未使用使用的数据淘汰 + volatile-lfu # 对设置了过期时间的 key 选择最近使用次数最少的数据淘汰 + volatile-ttl # 对设置了过期时间的 key 选择将要过期的数据淘汰 + volatile-random # 对设置了过期时间的 key 选择任意数据淘汰 ``` 第二类:检测全库数据(所有数据集 server.db[i].dict ): ```sh - allkeys-lru #挑选最近最少使用的数据淘汰 - allkeLyRs-lfu #挑选最近使用次数最少的数据淘汰 - allkeys-random #任意选择数据淘汰,相当于随机 + allkeys-lru # 对所有 key 选择最近最少使用的数据淘汰 + allkeLyRs-lfu # 对所有 key 选择最近使用次数最少的数据淘汰 + allkeys-random # 对所有 key 选择任意数据淘汰,相当于随机 ``` 第三类:放弃数据驱逐 @@ -10013,7 +10028,7 @@ TTL 返回的值有三种情况:正数,-1,-2 no-enviction #禁止驱逐数据(redis4.0中默认策略),会引发OOM(Out Of Memory) ``` -数据淘汰策略配置依据:使用INFO命令输出监控信息,查询缓存 hit 和 miss 的次数,根据需求调优 Redis 配置 +数据淘汰策略配置依据:使用 INFO 命令输出监控信息,查询缓存 hit 和 miss 的次数,根据需求调优 Redis 配置 diff --git a/Java.md b/Java.md index f3195ea..2bc5b09 100644 --- a/Java.md +++ b/Java.md @@ -1303,14 +1303,14 @@ class Animal{ 子类继承了父类就得到了父类的方法,**可以直接调用**,受权限修饰符的限制,也可以重写方法 -**方法重写**:子类重写一个与父类申明一样的方法来**覆盖**父类的该方法 +方法重写:子类重写一个与父类申明一样的方法来**覆盖**父类的该方法 方法重写的校验注解:@Override * 方法加了这个注解,那就必须是成功重写父类的方法,否则报错 * @Override 优势:可读性好,安全,优雅 -子类可以扩展父类的功能,但不能改变父类原有的功能,重写有以下**三个限制**: +**子类可以扩展父类的功能,但不能改变父类原有的功能**,重写有以下三个限制: - 子类方法的访问权限必须大于等于父类方法 - 子类方法的返回类型必须是父类方法返回类型或为其子类型 @@ -3937,14 +3937,14 @@ List系列集合:添加的元素是有序,可重复,有索引。 ###### 介绍 -ArrayList 添加的元素,是有序,可重复,有索引的。 +ArrayList 添加的元素,是有序,可重复,有索引的 -`public boolean add(E e)` : 将指定的元素追加到此集合的末尾 -`public void add(int index, E element)` : 将指定的元素,添加到该集合中的指定位置上。 -`public E get(int index)` : 返回集合中指定位置的元素。 -`public E remove(int index)` : 移除列表中指定位置的元素, 返回的是被移除的元素。 -`public E set(int index, E element)` : 用指定元素替换集合中指定位置的元素,返回更新前的元素值。 -`int indexOf(Object o)` : 返回列表中指定元素第一次出现的索引,如果不包含此元素,则返回-1。 +* `public boolean add(E e)`:将指定的元素追加到此集合的末尾 +* `public void add(int index, E element)`:将指定的元素,添加到该集合中的指定位置上 +* `public E get(int index)`:返回集合中指定位置的元素 +* `public E remove(int index)`:移除列表中指定位置的元素, 返回的是被移除的元素 +* `public E set(int index, E element)`:用指定元素替换集合中指定位置的元素,返回更新前的元素值 +* `int indexOf(Object o)`:返回列表中指定元素第一次出现的索引,如果不包含此元素,则返回 -1 ```java public static void main(String[] args){ @@ -3987,7 +3987,7 @@ public class ArrayList extends AbstractList * 添加元素: ```java - //e 插入的元素 elementData底层数组 size 插入的位置 + // e 插入的元素 elementData底层数组 size 插入的位置 public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; // 插入size位置,然后加一 @@ -4005,9 +4005,9 @@ public class ArrayList extends AbstractList ```java private static int calculateCapacity(Object[] elementData, int minCapacity) { - //判断elementData是不是空数组 + // 判断elementData是不是空数组 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { - //返回默认值和最小需求容量最大的一个 + // 返回默认值和最小需求容量最大的一个 return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; @@ -4017,12 +4017,12 @@ public class ArrayList extends AbstractList 如果需要的容量大于数组长度,进行扩容: ```java - //判断是否需要扩容 + // 判断是否需要扩容 private void ensureExplicitCapacity(int minCapacity) { modCount++; - //索引越界 + // 索引越界 if (minCapacity - elementData.length > 0) - //调用grow方法进行扩容,调用此方法代表已经开始扩容了 + // 调用grow方法进行扩容,调用此方法代表已经开始扩容了 grow(minCapacity); } ``` @@ -4137,23 +4137,24 @@ public class ArrayList extends AbstractList ###### 介绍 -LinkedList也是List的实现类:基于**双向链表**实现,使用 Node 存储链表节点信息,增删比较快,查询慢 - -LinkedList除了拥有List集合的全部功能还多了很多操作首尾元素的特殊功能: - `public boolean add(E e)` : 将指定元素添加到此列表的结尾 - `public E poll()` : 检索并删除此列表的头(第一个元素) - `public void addFirst(E e)` : 将指定元素插入此列表的开头 - `public void addLast(E e)` : 将指定元素添加到此列表的结尾 - `public E getFirst()` : 返回此列表的第一个元素 - `public E getLast()` : 返回此列表的最后一个元素 - `public E removeFirst()` : 移除并返回此列表的第一个元素 - `public E removeLast()` : 移除并返回此列表的最后一个元素 - `public E pop()` : 从此列表所表示的堆栈处弹出一个元素 - `public void push(E e)` : 将元素推入此列表所表示的堆栈 - `public int indexOf(Object o)` : 返回此列表中指定元素的第一次出现的索引,如果不包含返回-1 - `public int lastIndexOf(Object o)` : 从尾遍历找 - ` public boolean remove(Object o)` : 一次只删除一个匹配的对象,如果删除了匹配对象返回true - `public E remove(int index)` : 删除指定位置的元素 +LinkedList 也是 List 的实现类:基于**双向链表**实现,使用 Node 存储链表节点信息,增删比较快,查询慢 + +LinkedList 除了拥有 List 集合的全部功能还多了很多操作首尾元素的特殊功能: + +* `public boolean add(E e)`:将指定元素添加到此列表的结尾 +* `public E poll()`:检索并删除此列表的头(第一个元素) +* `public void addFirst(E e)`:将指定元素插入此列表的开头 +* `public void addLast(E e)`:将指定元素添加到此列表的结尾 +* `public E getFirst()`:返回此列表的第一个元素 +* `public E getLast()`:返回此列表的最后一个元素 +* `public E removeFirst()`:移除并返回此列表的第一个元素 +* `public E removeLast()`:移除并返回此列表的最后一个元素 +* `public E pop()`:从此列表所表示的堆栈处弹出一个元素 +* `public void push(E e)`:将元素推入此列表所表示的堆栈 +* `public int indexOf(Object o)`:返回此列表中指定元素的第一次出现的索引,如果不包含返回 -1 +* `public int lastIndexOf(Object o)`:从尾遍历找 +* ` public boolean remove(Object o)`:一次只删除一个匹配的对象,如果删除了匹配对象返回true +* `public E remove(int index)`:删除指定位置的元素 ```java public class ListDemo { @@ -4193,7 +4194,7 @@ public class ListDemo { ###### 源码 -LinkedList 是一个实现了 List 接口的**双端链表**,支持高效的插入和删除操作,另外也实现了 Deque 接口,使得LinkedList 类也具有队列的特性 +LinkedList 是一个实现了 List 接口的**双端链表**,支持高效的插入和删除操作,另外也实现了 Deque 接口,使得 LinkedList 类也具有队列的特性 ![](https://gitee.com/seazean/images/raw/master/Java/LinkedList底层结构.png) @@ -4268,7 +4269,7 @@ LinkedList 是一个实现了 List 接口的**双端链表**,支持高效的 ##### 概述 -Set系列集合:添加的元素是无序,不重复,无索引的 +Set 系列集合:添加的元素是无序,不重复,无索引的 * HashSet:添加的元素是无序,不重复,无索引的 * LinkedHashSet:添加的元素是有序,不重复,无索引的 @@ -4697,7 +4698,7 @@ HashMap继承关系如下图所示: private static final long serialVersionUID = 362498820763181265L; ``` -2. 集合的初始化容量(**必须是二的n次幂** ) +2. 集合的初始化容量(**必须是二的 n 次幂** ) ```java //默认的初始容量是16 -- 1<<4相当于1*2的4次方---1*16 @@ -5289,7 +5290,7 @@ HashMap继承关系如下图所示: ##### 原理分析 -LinkedHashMap是 HashMap 的子类 +LinkedHashMap 是 HashMap 的子类 * 优点:添加的元素按照键有序不重复的,有序的原因是底层维护了一个双向链表 @@ -5310,7 +5311,7 @@ LinkedHashMap是 HashMap 的子类 transient LinkedHashMap.Entry tail; ``` -* accessOrder 决定了顺序,默认为 false 维护的是插入顺序,true为访问顺序(LRU顺序) +* accessOrder 决定了顺序,默认为 false 维护的是插入顺序(先进先出),true 为访问顺序(**LRU 顺序**) ```java final boolean accessOrder; @@ -5879,11 +5880,11 @@ public class MyVariableParameter4 { ### 基本介绍 -异常:程序在"编译"或者"执行"的过程中可能出现的问题,Java为常见的代码异常都设计一个类来代表。 +异常:程序在"编译"或者"执行"的过程中可能出现的问题,Java 为常见的代码异常都设计一个类来代表。 -错误:Error ,程序员无法处理的错误,只能重启系统,比如内存奔溃,JVM本身的奔溃 +错误:Error ,程序员无法处理的错误,只能重启系统,比如内存奔溃,JVM 本身的奔溃 -Java中异常继承的根类是:Throwable +Java 中异常继承的根类是:Throwable ``` 异常的体系: @@ -5897,8 +5898,8 @@ Java中异常继承的根类是:Throwable Exception 异常的分类: -* 编译时异常:继承自Exception的异常或者其子类,编译阶段就会报错 -* 运行时异常: 继承自RuntimeException的异常或者其子类,编译阶段是不会出错的,在运行时阶段可能出现,编译阶段是不会出错的,但是运行阶段可能出现,建议提前处理 +* 编译时异常:继承自 Exception 的异常或者其子类,编译阶段就会报错 +* 运行时异常: 继承自 RuntimeException 的异常或者其子类,编译阶段是不会出错的,在运行时阶段可能出现,编译阶段是不会出错的,但是运行阶段可能出现,建议提前处理 @@ -9548,6 +9549,7 @@ JVM 的生命周期分为三个阶段,分别为:启动、运行、死亡。 - **启动**:当启动一个 Java 程序时,通过引导类加载器(bootstrap class loader)创建一个初始类(initial class),对于拥有 main 函数的类就是 JVM 实例运行的起点 - **运行**: + - main() 方法是一个程序的初始起点,任何线程均可由在此处启动 - 在 JVM 内部有两种线程类型,分别为:用户线程和守护线程,**JVM 使用的是守护线程,main() 和其他线程使用的是用户线程**,守护线程会随着用户线程的结束而结束 - 执行一个 Java 程序时,真真正正在执行的是一个 Java 虚拟机的进程 @@ -9574,7 +9576,7 @@ JVM 的生命周期分为三个阶段,分别为:启动、运行、死亡。 ### 内存概述 -内存结构是JVM中非常重要的一部分,是非常重要的系统资源,是硬盘和 CPU 的中间仓库及桥梁,承载着操作系统和应用程序的实时运行,又叫运行时数据区 +内存结构是 JVM 中非常重要的一部分,是非常重要的系统资源,是硬盘和 CPU 的中间仓库及桥梁,承载着操作系统和应用程序的实时运行,又叫运行时数据区 JVM 内存结构规定了 Java 在运行过程中内存申请、分配、管理的策略,保证了 JVM 的高效稳定运行 @@ -9662,11 +9664,11 @@ Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所 * 表中的变量只在当前方法调用中有效,方法结束栈帧销毁,局部变量表也会随之销毁 * 表中的变量也是重要的垃圾回收根节点,只要被表中数据直接或间接引用的对象都不会被回收 -局部变量表最基本的存储单元是 **slot(变量槽)**: +局部变量表最基本的存储单元是 **slot(变量槽)**: * 参数值的存放总是在局部变量数组的 index0 开始,到数组长度 -1 的索引结束,JVM 为每一个 slot 都分配一个访问索引,通过索引即可访问到槽中的数据 * 存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress 类型的变量 -* 32 位以内的类型只占一个 slot(包括returnAddress类型),64 位的类型(long 和 double)占两个 slot +* 32 位以内的类型只占一个 slot(包括 returnAddress 类型),64 位的类型(long 和 double)占两个 slot * 局部变量表中的槽位是可以**重复利用**的,如果一个局部变量过了其作用域,那么之后申明的新的局部变量就可能会复用过期局部变量的槽位,从而达到节省资源的目的 @@ -9684,7 +9686,7 @@ Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所 * 保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间,是执行引擎的一个工作区 * Java 虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈 -* 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中 +* 如果被调用的方法带有返回值的话,其**返回值将会被压入当前栈帧的操作数栈中** 栈顶缓存技术 ToS(Top-of-Stack Cashing):将栈顶元素全部缓存在 CPU 的寄存器中,以此降低对内存的读/写次数,提升执行的效率 @@ -9698,15 +9700,16 @@ Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所 ##### 动态链接 -动态链接是指向运行时常量池的方法引用,涉及到栈操作已经是类加载完成,这个阶段的解析是动态绑定 +动态链接是指向运行时常量池的方法引用,涉及到栈操作已经是类加载完成,这个阶段的解析是**动态绑定** * 为了支持当前方法的代码能够实现动态链接,每一个栈帧内部都包含一个指向运行时常量池或该栈帧所属方法的引用 ![](https://gitee.com/seazean/images/raw/master/Java/JVM-动态链接符号引用.png) * 在 Java 源文件被编译成字节码文件中,所有的变量和方法引用都作为符号引用保存在 class 的常量池中 + 常量池的作用:提供一些符号和常量,便于指令的识别 - + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-动态链接运行时常量池.png) @@ -9717,7 +9720,7 @@ Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所 ##### 返回地址 -Return Address:存放调用该方法的PC寄存器的值 +Return Address:存放调用该方法的 PC 寄存器的值 方法的结束有两种方式:正常执行完成、出现未处理的异常,在方法退出后都返回到该方法被调用的位置 @@ -9744,7 +9747,7 @@ Return Address:存放调用该方法的PC寄存器的值 #### 本地方法栈 -本地方法栈是为虚拟机**执行本地方法时提供服务的** +本地方法栈是为虚拟机执行本地方法时提供服务的 JNI:Java Native Interface,通过使用 Java 本地接口书写程序,可以确保代码在不同的平台上方便移植 @@ -9785,12 +9788,12 @@ Program Counter Register 程序计数器(寄存器) 特点: -* **是线程私有的** -* 不会存在内存溢出,是 JVM 规范中唯一一个不出现 OOM 的区域,所以这个空间不会进行 GC +* 是线程私有的 +* **不会存在内存溢出**,是 JVM 规范中唯一一个不出现 OOM 的区域,所以这个空间不会进行 GC -Java**反编译**指令:`javap -v Test.class` +Java 反编译指令:`javap -v Test.class` -#20:去Constant pool查看该地址的指令 +#20:代表去 Constant pool 查看该地址的指令 ```java 0: getstatic #20 // PrintStream out = System.out; @@ -9811,13 +9814,13 @@ Java**反编译**指令:`javap -v Test.class` #### 堆 -Heap 堆:是JVM内存中最大的一块,由所有线程共享,由垃圾回收器管理的主要区域("GC 堆"),堆中对象都需要考虑线程安全的问题 +Heap 堆:是 JVM 内存中最大的一块,由所有线程共享,由垃圾回收器管理的主要区域,堆中对象大部分都需要考虑线程安全的问题 存放哪些资源: * 对象实例:类初始化生成的对象,**基本数据类型的数组也是对象实例**,new 创建对象都使用堆内存 * 字符串常量池: - * 字符串常量池原本存放于方法区,jdk7开始放置于堆中 + * 字符串常量池原本存放于方法区,jdk7 开始放置于堆中 * 字符串常量池**存储的是 String 对象的直接引用或者对象**,是一张 string table * 静态变量:静态变量是有 static 修饰的变量,jdk7 时从方法区迁移至堆中 * 线程分配缓冲区 Thread Local Allocation Buffer:线程私有但不影响堆的共性,可以提升对象分配的效率 @@ -9832,11 +9835,11 @@ Heap 堆:是JVM内存中最大的一块,由所有线程共享,由垃圾回 2. jmap:查看堆内存占用情况 `jhsdb jmap --heap --pid 进程id` 3. jconsole:图形界面的,多功能的监测工具,可以连续监测 -在 Java7 中堆内会存在**年轻代、老年代和方法区(永久代)**: +在 Java7 中堆内会存在**年轻代、老年代和方法区(永久代)**: -* Young 区被划分为三部分,Eden 区和两个大小严格相同的 Survivor 区。Survivo r区间,某一时刻只有其中一个是被使用的,另外一个留做垃圾回收时复制对象。在 Eden 区变满的时候, GC 就会将存活的对象移到空闲的 Survivor 区间中,根据JVM的策略,在经过几次垃圾回收后,仍然存活于 Survivor 的对象将被移动到 Tenured 区间 +* Young 区被划分为三部分,Eden 区和两个大小严格相同的 Survivor 区。Survivor 区某一时刻只有其中一个是被使用的,另外一个留做垃圾回收时复制对象。在 Eden 区变满的时候, GC 就会将存活的对象移到空闲的 Survivor 区间中,根据 JVM 的策略,在经过几次垃圾回收后,仍然存活于 Survivor 的对象将被移动到 Tenured 区间 * Tenured 区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在 Young 复制转移一定的次数以后,对象就会被转移到 Tenured 区 -* Perm 代主要保存 **Class、ClassLoader、静态变量、常量、编译后的代码**,在 java7 中堆内方法区会受到 GC 的管理 +* Perm 代主要保存 Class、ClassLoader、静态变量、常量、编译后的代码,在 Java7 中堆内方法区会受到 GC 的管理 分代原因:不同对象的生命周期不同,70%-99% 的对象都是临时对象,优化 GC 性能 @@ -9860,7 +9863,7 @@ public static void main(String[] args) { #### 方法区 -方法区:是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是也叫 Non-Heap(非堆) +方法区:是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、即时编译器编译后的代码等数据,虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是也叫 Non-Heap(非堆) 方法区是一个 JVM 规范,**永久代与元空间都是其一种实现方式** @@ -9877,10 +9880,10 @@ public static void main(String[] args) { - 字面量:基本数据类型、字符串类型常量、声明为 final 的常量值等 - 符号引用:类、字段、方法、接口等的符号引用 -**运行时常量池**是方法区的一部分 +运行时常量池是方法区的一部分 * 常量池(编译器生成的字面量和符号引用)中的数据会在类加载的加载阶段放入运行时常量池 -* 类在解析阶段将符号引用替换成直接引用 +* 类在解析阶段将这些符号引用替换成直接引用 * 除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern() @@ -9915,13 +9918,13 @@ PermGen 被元空间代替,永久代的**类信息、方法、常量池**等 方法区内存溢出: -* JDK1.8以前会导致永久代内存溢出:java.lang.OutOfMemoryError: PerGen space +* JDK1.8 以前会导致永久代内存溢出:java.lang.OutOfMemoryError: PerGen space ```sh -XX:MaxPermSize=8m #参数设置 ``` -* JDK1.8以后会导致元空间内存溢出:java.lang.OutOfMemoryError: Metaspace +* JDK1.8 以后会导致元空间内存溢出:java.lang.OutOfMemoryError: Metaspace ```sh -XX:MaxMetaspaceSize=8m #参数设置 @@ -9960,7 +9963,7 @@ public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制 #### 直接内存 -直接内存是 Java 堆外、直接向系统申请的内存区间,不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域 +直接内存是 Java 堆外、直接向系统申请的内存区间,不是虚拟机运行时数据区的一部分,也不是《Java 虚拟机规范》中定义的内存区域 @@ -9974,23 +9977,23 @@ public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制 ### 变量位置 -**变量的位置不取决于它是基本数据类型还是引用数据类型,取决于它的声明位置** +变量的位置不取决于它是基本数据类型还是引用数据类型,取决于它的**声明位置** 静态内部类和其他内部类: * **一个 class 文件只能对应一个 public 类型的类**,这个类可以有内部类,但不会生成新的 class 文件 -* 静态内部类属于类本身,加载到方法区,其他内部类属于内部类的属性,加载到栈(待考证) +* 静态内部类属于类本身,加载到方法区,其他内部类属于内部类的属性,加载到堆(待考证) 类变量: * 类变量是用 static 修饰符修饰,定义在方法外的变量,随着 java 进程产生和销毁 -* 在 java8 之前把静态变量存放于方法区,在 java8 时存放在**堆中的静态变量区** +* 在 java8 之前把静态变量存放于方法区,在 java8 时存放在堆中的静态变量区 实例变量: -* 实例(成员)变量是定义在类中,没有static修饰的变量,随着类的实例产生和销毁,是类实例的一部分 +* 实例(成员)变量是定义在类中,没有 static 修饰的变量,随着类的实例产生和销毁,是类实例的一部分 * 在类初始化的时候,从运行时常量池取出直接引用或者值,**与初始化的对象一起放入堆中** 局部变量: @@ -10002,23 +10005,20 @@ public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制 * 类常量池与运行时常量池都存储在方法区,而字符串常量池在 jdk7 时就已经从方法区迁移到了 java 堆中 * 在类编译过程中,会把类元信息放到方法区,类元信息的其中一部分便是类常量池,主要存放字面量和符号引用,而字面量的一部分便是文本字符 -* 在类加载时将字面量和符号引用解析为直接引用存储在运行时常量池 -* 对于文本字符来说,会在解析时查找字符串常量池,查出这个文本字符对应的字符串对象的直接引用,将直接引用存储在运行时常量池 +* **在类加载时将字面量和符号引用解析为直接引用存储在运行时常量池** +* 对于文本字符,会在解析时查找字符串常量池,查出这个文本字符对应的字符串对象的直接引用,将直接引用存储在运行时常量池 什么是字面量?什么是符号引用? -* 字面量:java代码在编译过程中是无法构建引用的,字面量就是在编译时对于数据的一种表示 +* 字面量:java 代码在编译过程中是无法构建引用的,字面量就是在编译时对于数据的一种表示 ```java - int a=1;//这个1便是字面量 - String b="iloveu";//iloveu便是字面量 + int a = 1; //这个1便是字面量 + String b = "iloveu"; //iloveu便是字面量 ``` * 符号引用:在编译过程中并不知道每个类的地址,因为可能这个类还没有加载,如果在一个类中引用了另一个类,无法知道它的内存地址,只能用他的类名作为符号引用,在类加载完后用这个符号引用去获取内存地址 - 例如:在 com.demo.Solution 类中引用了 com.test.Quest,把 `com.test.Quest` 作为符号引用存进类常量池,在类加载完后,用这个符号引用去方法区找这个类的内存地址 - - @@ -10049,7 +10049,7 @@ public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制 ##### 分代介绍 -Java8 时,堆被分为了两份:新生代和老年代(1:2),在java7时,还存在一个永久代 +Java8 时,堆被分为了两份:新生代和老年代(1:2),在 Java7 时,还存在一个永久代 - 新生代使用:复制算法 - 老年代使用:标记 - 清除 或者 标记 - 整理 算法 @@ -10076,7 +10076,7 @@ Java8 时,堆被分为了两份:新生代和老年代(1:2),在java7 工作机制: * **对象优先在 Eden 分配**:当创建一个对象的时候,对象会被分配在新生代的 Eden 区,当Eden 区要满了时候,触发 YoungGC -* 当进行 YoungGC 后,此时在 Eden 区存活的对象被移动到 to 区,并且**当前对象的年龄会加1**,清空 Eden 区 +* 当进行 YoungGC 后,此时在 Eden 区存活的对象被移动到 to 区,并且当前对象的年龄会加 1,清空 Eden 区 * 当再一次触发 YoungGC 的时候,会把 Eden 区中存活下来的对象和 to 中的对象,移动到 from 区中,这些对象的年龄会加 1,清空 Eden 区和 to 区 * To 区永远是空 Survivor 区,From 区是有数据的,每次 MinorGC 后两个区域互换 * From 区和 To 区 也可以叫做 S0 区和 S1 区 @@ -10084,10 +10084,12 @@ Java8 时,堆被分为了两份:新生代和老年代(1:2),在java7 晋升到老年代: * **长期存活的对象进入老年代**:为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中 - `-XX:MaxTenuringThreshold`:定义年龄的阈值,对象头中用4个bit存储,所以最大值是15,默认也是15 -* **大对象直接进入老年代**:需要连续内存空间的对象,最典型的大对象是很长的字符串以及数组;避免在 Eden 和 Survivor 之间的大量复制;经常出现大对象会提前触发GC以获取足够的连续空间分配给大对象 + + `-XX:MaxTenuringThreshold`:定义年龄的阈值,对象头中用 4 个 bit 存储,所以最大值是 15,默认也是 15 +* **大对象直接进入老年代**:需要连续内存空间的对象,最典型的大对象是很长的字符串以及数组;避免在 Eden 和 Survivor 之间的大量复制;经常出现大对象会提前触发 GC 以获取足够的连续空间分配给大对象 + `-XX:PretenureSizeThreshold`:大于此值的对象直接在老年代分配 -* **动态对象年龄判定**:如果在Survivor区中相同年龄的对象的所有大小之和超过Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代 +* **动态对象年龄判定**:如果在 Survivor 区中相同年龄的对象的所有大小之和超过 Survivor 空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代 空间分配担保: @@ -10102,28 +10104,26 @@ Java8 时,堆被分为了两份:新生代和老年代(1:2),在java7 #### TLAB -虚拟机采用了两种方式在创建对象时解决并发问题:CAS、TLAB - -TLAB:Thread Local Allocation Buffer,为每个线程在堆内单独分配了一个缓冲区,多线程分配内存时,使用TLAB 可以避免线程安全问题,同时还能够提升内存分配的吞吐量,这种内存分配方式叫做**快速分配策略** +TLAB:Thread Local Allocation Buffer,为每个线程在堆内单独分配了一个缓冲区,多线程分配内存时,使用 TLAB 可以避免线程安全问题,同时还能够提升内存分配的吞吐量,这种内存分配方式叫做**快速分配策略** - 栈上分配使用的是栈来进行对象内存的分配 - TLAB 分配使用的是 Eden 区域进行内存分配,属于堆内存 -背景:堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据,由于对象实例的创建在JVM中非常频繁,因此在并发环境下为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度 +堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据,由于对象实例的创建在 JVM 中非常频繁,因此在并发环境下为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度 -问题:堆空间都是共享的么? 不一定,因为还有TLAB,在堆中划分出一块区域,为每个线程所独占 +问题:堆空间都是共享的么? 不一定,因为还有 TLAB,在堆中划分出一块区域,为每个线程所独占 ![](https://gitee.com/seazean/images/raw/master/Java/JVM-TLAB内存分配策略.jpg) -JVM 是将 TLAB 作为内存分配的首选,但不是所有的对象实例都能够在 TLAB 中成功分配内存,一旦对象在TLAB空间分配内存失败时,JVM 就会通过**使用加锁机制确保数据操作的原子性**,从而直接在 Eden 空间中分配内存 +JVM 是将 TLAB 作为内存分配的首选,但不是所有的对象实例都能够在 TLAB 中成功分配内存,一旦对象在 TLAB 空间分配内存失败时,JVM 就会通过**使用加锁机制确保数据操作的原子性**,从而直接在堆中分配内存 栈上分配优先于 TLAB 分配进行,逃逸分析中若可进行栈上分配优化,会优先进行对象栈上直接分配内存 参数设置: -* `-XX:UseTLAB`:设置是否开启TLAB空间 +* `-XX:UseTLAB`:设置是否开启 TLAB 空间 -* `-XX:TLABWasteTargetPercent`:设置TLAB空间所占用Eden空间的百分比大小,默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1,即1% +* `-XX:TLABWasteTargetPercent`:设置 TLAB 空间所占用 Eden 空间的百分比大小,默认情况下 TLAB 空间的内存非常小,仅占有整个 Eden 空间的1% * `-XX:TLABRefillWasteFraction`:指当 TLAB 空间不足,请求分配的对象内存大小超过此阈值时不会进行 TLAB 分配,直接进行堆内存分配,否则还是会优先进行 TLAB 分配 ![](https://gitee.com/seazean/images/raw/master/Java/JVM-TLAB内存分配过程.jpg) @@ -10141,7 +10141,7 @@ JVM 是将 TLAB 作为内存分配的首选,但不是所有的对象实例都 * C1 编译速度快,优化方式比较保守;C2 编译速度慢,优化方式比较激进 * C1+C2 在开始阶段采用 C1 编译,当代码运行到一定热度之后采用 C2 重新编译 -**逃逸分析**:并不是直接的优化手段,而是一个代码分析,通过动态分析对象的作用域,为其它优化手段如栈上分配、标量替换和同步消除等提供依据,发生逃逸行为的情况有两种:方法逃逸和线程逃逸 +逃逸分析并不是直接的优化手段,而是一个代码分析方式,通过动态分析对象的作用域,为其它优化手段如栈上分配、标量替换和同步消除等提供依据,发生逃逸行为的情况有两种:方法逃逸和线程逃逸 * 方法逃逸:当一个对象在方法中定义之后,被外部方法引用 * 全局逃逸:一个对象的作用范围逃出了当前方法或者当前线程,比如对象是一个静态变量、全局变量赋值、已经发生逃逸的对象、作为当前方法的返回值 @@ -10162,8 +10162,9 @@ JVM 是将 TLAB 作为内存分配的首选,但不是所有的对象实例都 聚合量 (Aggregate):一个数据可以继续分解,对象一般是聚合量 * 如果逃逸分析发现一个对象不会被外部访问,并且该对象可以被拆散,那么经过优化之后,并不直接生成该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替 * 参数设置: - `-XX:+EliminateAllocations`:开启标量替换 - `-XX:+PrintEliminateAllocations`:查看标量替换情况 + + * `-XX:+EliminateAllocations`:开启标量替换 + * `-XX:+PrintEliminateAllocations`:查看标量替换情况 * 栈上分配 @@ -10217,25 +10218,24 @@ JVM 是将 TLAB 作为内存分配的首选,但不是所有的对象实例都 Minor GC 触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC -FullGC 同时回收新生代、老年代和方法区,只会存在一个FullGC的线程进行执行,其他的线程全部会被**挂起**,有以下触发条件: +FullGC 同时回收新生代、老年代和方法区,只会存在一个 FullGC 的线程进行执行,其他的线程全部会被**挂起**,有以下触发条件: * 调用 System.gc(): - * 在默认情况下,通过 System.gc() 或 Runtime.getRuntime().gc() 的调用,会显式触发 FullGC,同时对老年代和新生代进行回收,但是虚拟机不一定真正去执行,无法保证对垃圾收集器的调用 (马上触发GC) + * 在默认情况下,通过 System.gc() 或 Runtime.getRuntime().gc() 的调用,会显式触发 FullGC,同时对老年代和新生代进行回收,但是虚拟机不一定真正去执行,无法保证对垃圾收集器的调用 * 不建议使用这种方式,应该让虚拟机管理内存。一般情况下,垃圾回收应该是自动进行的,无须手动触发;在一些特殊情况下,如正在编写一个性能基准,可以在运行之间调用 System.gc() * 老年代空间不足: * 为了避免引起的 Full GC,应当尽量不要创建过大的对象以及数组 - * 通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间 + * 通过 -Xmn 参数调整新生代的大小,让对象尽量在新生代被回收掉不进入老年代,可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间 * 空间分配担保失败 * JDK 1.7 及以前的永久代(方法区)空间不足 -* Concurrent Mode Failure: +* Concurrent Mode Failure:执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC - 执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC 手动 GC 测试,VM参数:`-XX:+PrintGcDetails` @@ -10286,13 +10286,13 @@ public void localvarGC4() { 问题:Safepoint 保证程序执行时,在不太长的时间内就会遇到可进入 GC 的 Safepoint,但是当线程处于 Waiting 状态或 Blocked 状态,线程无法响应 JVM 的中断请求,运行到安全点去中断挂起,JVM 也不可能等待线程被唤醒,对于这种情况,需要安全区域来解决 -安全区域 (Safe Region):指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始 GC 都是安全的 +安全区域 (Safe Region):指在一段代码片段中,**对象的引用关系不会发生变化**,在这个区域中的任何位置开始 GC 都是安全的 运行流程: - 当线程运行到 Safe Region 的代码时,首先标识已经进入了 Safe Region,如果这段时间内发生GC,JVM 会忽略标识为 Safe Region 状态的线程 -- 当线程即将离开 Safe Region 时,会检查 JVM 是否已经完成 GC,如果完成了则继续运行,否则线程必须等待GC 完成,收到可以安全离开 SafeRegion 的信号 +- 当线程即将离开 Safe Region 时,会检查 JVM 是否已经完成 GC,如果完成了则继续运行,否则线程必须等待 GC 完成,收到可以安全离开 SafeRegion 的信号 @@ -10306,11 +10306,11 @@ public void localvarGC4() { 垃圾:**如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾** -作用:释放没用的对象,清除内存里的记录碎片,碎片整理将所占用的堆内存移到堆的一端,以便JVM 将整理出的内存分配给新的对象 +作用:释放没用的对象,清除内存里的记录碎片,碎片整理将所占用的堆内存移到堆的一端,以便 JVM 将整理出的内存分配给新的对象 垃圾收集主要是针对堆和方法区进行,程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收 -在堆里存放着几乎所有的Java对象实例,在 GC 执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC 才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段,判断对象存活一般有两种方式:**引用计数算法**和**可达性分析算法** +在堆里存放着几乎所有的 Java 对象实例,在 GC 执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC 才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程可以称为垃圾标记阶段,判断对象存活一般有两种方式:**引用计数算法**和**可达性分析算法** @@ -10320,24 +10320,22 @@ public void localvarGC4() { #### 引用计数法 -引用计数算法(Reference Counting):对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1;当对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收(Java没有采用) +引用计数算法(Reference Counting):对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。对于一个对象 A,只要有任何一个对象引用了 A,则 A 的引用计数器就加 1;当引用失效时,引用计数器就减 1;当对象 A 的引用计数器的值为 0,即表示对象A不可能再被使用,可进行回收(Java 没有采用) 优点: -- 回收没有延迟性,无需等到内存不够的时候才开始回收,运行时根据对象计数器是否为0,可以直接回收 -- 在垃圾回收过程中,应用无需挂起;如果申请内存时,内存不足,则立刻报OOM错误。 +- 回收没有延迟性,无需等到内存不够的时候才开始回收,运行时根据对象计数器是否为 0,可以直接回收 +- 在垃圾回收过程中,应用无需挂起;如果申请内存时,内存不足,则立刻报 OOM 错误 - 区域性,更新对象的计数器时,只是影响到该对象,不会扫描全部对象 缺点: -- 每次对象被引用时,都需要去更新计数器,有一点时间开销。 +- 每次对象被引用时,都需要去更新计数器,有一点时间开销 -- 浪费CPU资源,即使内存够用,仍然在运行时进行计数器的统计。 +- 浪费 CPU 资源,即使内存够用,仍然在运行时进行计数器的统计。 - **无法解决循环引用问题,会引发内存泄露**(最大的缺点) - 内存泄漏(Memory Leak):是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果 - ```java public class Test { public Object instance = null; @@ -10375,8 +10373,6 @@ GC Roots 对象: - 字符串常量池(string Table)里的引用 - 同步锁 synchronized 持有的对象 - - **GC Roots 是一组活跃的引用,不是对象**,放在 GC Roots Set 集合 @@ -10387,9 +10383,9 @@ GC Roots 对象: ##### 工作原理 -可达性分析算法以**根对象集合(GCRoots)**为起始点,从上至下的方式搜索被根对象集合所连接的目标对象 +可达性分析算法以根对象集合(GCRoots)为起始点,从上至下的方式搜索被根对象集合所连接的目标对象 -分析工作必须在一个能保障**一致性的快照**中进行,否则结果的准确性无法保证,这点也是导致 GC 进行时必须 Stop The World 的一个重要原因 +分析工作必须在一个保障**一致性的快照**中进行,否则结果的准确性无法保证,这也是导致 GC 进行时必须 Stop The World 的一个原因 基本原理: @@ -10411,7 +10407,7 @@ GC Roots 对象: ###### 标记算法 -三色标记法把遍历对象图过程中遇到的对象,按“是否访问过”这个条件标记成以下三种颜色: +三色标记法把遍历对象图过程中遇到的对象,标记成以下三种颜色: - 白色:尚未访问过 - 灰色:本对象已访问过,但是本对象引用到的其他对象尚未全部访问 @@ -10443,7 +10439,7 @@ GC Roots 对象: 并发标记时,对象间的引用可能发生变化**,**多标和漏标的情况就有可能发生 -**多标情况:**当E变为灰色或黑色时,其他线程断开的 D 对 E 的引用,导致这部分对象仍会被标记为存活,本轮 GC 不会回收这部分内存,这部分本应该回收但是没有回收到的内存,被称之为**浮动垃圾** +**多标情况:**当 E 变为灰色或黑色时,其他线程断开的 D 对 E 的引用,导致这部分对象仍会被标记为存活,本轮 GC 不会回收这部分内存,这部分本应该回收但是没有回收到的内存,被称之为**浮动垃圾** * 针对并发标记开始后的**新对象**,通常的做法是直接全部当成黑色,也算浮动垃圾 * 浮动垃圾并不会影响应用程序的正确性,只是需要等到下一轮垃圾回收中才被清除 @@ -10454,7 +10450,7 @@ GC Roots 对象: * 条件一:灰色对象断开了对一个白色对象的引用(直接或间接),即灰色对象原成员变量的引用发生了变化 * 条件二:其他线程中修改了黑色对象,插入了一条或多条对该白色对象的新引用 -* 结果:导致该白色对象当作垃圾被GC,影响到了应用程序的正确性 +* 结果:导致该白色对象当作垃圾被 GC,影响到了应用程序的正确性 @@ -10468,7 +10464,7 @@ objD.fieldG = G; // 写 为了解决问题,可以操作上面三步,**将对象 G 记录起来,然后作为灰色对象再进行遍历**,比如放到一个特定的集合,等初始的 GC Roots 遍历完(并发标记),再遍历该集合(重新标记) -> 所以重新标记需要 STW,应用程序一直在运行,该集合可能会一直增加新的对象,导致永远都运行不完 +> 所以**重新标记需要 STW**,应用程序一直在运行,该集合可能会一直增加新的对象,导致永远都运行不完 解决方法:添加读写屏障,读屏障拦截第一步,写屏障拦截第二三步,在读写前后进行一些后置处理: @@ -10480,7 +10476,7 @@ objD.fieldG = G; // 写 * **写屏障 (Store Barrier) + SATB**:当原来成员变量的引用发生变化之前,记录下原来的引用对象 - 保留 GC 开始时的对象图,即原始快照 SATB,当 GC Roots 确定后,对象图就已经确定,那后续的标记也应该是按照这个时刻的对象图走,如果期间对白色对象有了新的引用会记录下来,并且将白色对象变灰,重新扫描该对象 + 保留 GC 开始时的对象图,即原始快照 SATB,当 GC Roots 确定后,对象图就已经确定,那后续的标记也应该是按照这个时刻的对象图走,如果期间对白色对象有了新的引用会记录下来,并且将白色对象变灰(说明可达了),重新扫描该对象 SATB (Snapshot At The Beginning) 破坏了条件一,从而保证了不会漏标 @@ -10500,21 +10496,21 @@ objD.fieldG = G; // 写 #### finalization -Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑 +Java 语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑 -垃圾回收此对象之前,会先调用这个对象的finalize()方法,finalize() 方法允许在子类中被重写,用于在对象被回收时进行后置处理,通常在这个方法中进行一些资源释放和清理,比如关闭文件、套接字和数据库连接等 +垃圾回收此对象之前,会先调用这个对象的 finalize() 方法,finalize() 方法允许在子类中被重写,用于在对象被回收时进行后置处理,通常在这个方法中进行一些资源释放和清理,比如关闭文件、套接字和数据库连接等 -生存OR死亡:如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用,此对象需要被回收。但事实上这时候它们暂时处于”缓刑“阶段。**一个无法触及的对象有可能在某一个条件下“复活”自己**,如果这样对它的回收就是不合理的,所以虚拟机中的对象可能的三种状态: +生存 OR 死亡:如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用,此对象需要被回收。但事实上这时候它们暂时处于缓刑阶段。**一个无法触及的对象有可能在某个条件下复活自己**,这样对它的回收就是不合理的,所以虚拟机中的对象可能的三种状态: - 可触及的:从根节点开始,可以到达这个对象。 - 可复活的:对象的所有引用都被释放,但是对象有可能在finalize() 中复活 -- 不可触及的:对象的 finalize() 被调用,并且没有复活,那么就会进入不可触及状态,不可触及的对象不可能被复活。因为**finalize()只会被调用一次**,等到这个对象再被标记为可回收时就必须回收 +- 不可触及的:对象的 finalize() 被调用并且没有复活,那么就会进入不可触及状态,不可触及的对象不可能被复活,因为 **finalize() 只会被调用一次**,等到这个对象再被标记为可回收时就必须回收 -永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用,原因: +永远不要主动调用某个对象的 finalize() 方法,应该交给垃圾回收机制调用,原因: * finalize() 时可能会导致对象复活 -* finalize() 方法的执行时间是没有保障的,完全由GC线程决定,极端情况下,若不发生GC,则finalize()方法将没有执行机会,因为优先级比较低,即使主动调用该方法,也不会因此就直接进行回收 -* 一个糟糕的finalize() 会严重影响GC的性能 +* finalize() 方法的执行时间是没有保障的,完全由 GC 线程决定,极端情况下,若不发生 GC,则 finalize() 方法将没有执行机会,因为优先级比较低,即使主动调用该方法,也不会因此就直接进行回收 +* 一个糟糕的 finalize() 会严重影响 GC 的性能 @@ -10560,7 +10556,7 @@ Java语言提供了对象终止(finalization)机制来允许开发人员提 obj = null; ``` -4. 虚引用(PhantomReference):也称为幽灵引用或者幻影引”,是所有引用类型中最弱的一个 +4. 虚引用(PhantomReference):也称为幽灵引用或者幻影引用,是所有引用类型中最弱的一个 * 一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象 * 为对象设置虚引用的唯一目的是在于跟踪垃圾回收过程,能在这个对象被回收时收到一个系统通知 @@ -10590,7 +10586,7 @@ Java语言提供了对象终止(finalization)机制来允许开发人员提 - 加载该类的 `ClassLoader` 已经被回收 - 该类对应的 `java.lang.Class` 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法 -虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收 +虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的**仅仅是可以**,而并不是和对象一样不使用了就会必然被回收 @@ -10602,18 +10598,18 @@ Java语言提供了对象终止(finalization)机制来允许开发人员提 #### 标记清除 -当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。目前在JVM中比较常见的三种垃圾收集算法是 +当成功区分出内存中存活对象和死亡对象后,GC 接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。目前在 JVM 中比较常见的三种垃圾收集算法是 - 标记清除算法(Mark-Sweep) - 复制算法(copying) - 标记压缩算法(Mark-Compact) -标记清除算法,是将垃圾回收分为2个阶段,分别是**标记和清除** +标记清除算法,是将垃圾回收分为两个个阶段,分别是**标记和清除** -- **标记**:Collector从引用根节点开始遍历,**标记所有被引用的对象**,一般是在对象的Header中记录为可达对象,**标记的是引用的对象,不是垃圾** -- **清除**:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收,把分块连接到 “**空闲列表**” 的单向链表,判断回收后的分块与前一个空闲分块是否连续,若连续会合并这两个分块,之后进行分配时只需要遍历这个空闲列表,就可以找到分块 +- **标记**:Collector 从引用根节点开始遍历,标记所有被引用的对象,一般是在对象的 Header 中记录为可达对象,**标记的是引用的对象,不是垃圾** +- **清除**:Collector 对堆内存从头到尾进行线性的遍历,如果发现某个对象在其 Header 中没有标记为可达对象,则将其回收,把分块连接到 **空闲列表**”的单向链表,判断回收后的分块与前一个空闲分块是否连续,若连续会合并这两个分块,之后进行分配时只需要遍历这个空闲列表,就可以找到分块 -- **分配阶段**:程序会搜索空闲链表寻找空间大于等于新对象大小 size 的块 block,如果找到的块等于 size,会直接返回这个分块;如果找到的块大于 size,会将块分割成大小为 size 与 (block - size) 的两部分,返回大小为 size 的分块,并把大小为 (block - size) 的块返回给空闲列表 +- **分配阶段**:程序会搜索空闲链表寻找空间大于等于新对象大小 size 的块 block,如果找到的块等于 size,会直接返回这个分块;如果找到的块大于 size,会将块分割成大小为 size 与 block - size 的两部分,返回大小为 size 的分块,并把大小为 block - size 的块返回给空闲列表 算法缺点: @@ -10632,7 +10628,7 @@ Java语言提供了对象终止(finalization)机制来允许开发人员提 复制算法的核心就是,**将原有的内存空间一分为二,每次只用其中的一块**,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清理,交换两个内存的角色,完成垃圾的回收 -应用场景:如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之,则不适合 +应用场景:如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之则不适合 ![](https://gitee.com/seazean/images/raw/master/Java/JVM-复制算法.png) @@ -10644,7 +10640,7 @@ Java语言提供了对象终止(finalization)机制来允许开发人员提 算法缺点: - 主要不足是**只使用了内存的一半** -- 对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销都不小 +- 对于 G1 这种分拆成为大量 region 的 GC,复制而不是移动,意味着 GC 需要维护 region 之间对象引用关系,不管是内存占用或者时间开销都不小 现在的商业虚拟机都采用这种收集算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间 @@ -10713,13 +10709,13 @@ Java语言提供了对象终止(finalization)机制来允许开发人员提 * 非压缩式的垃圾回收器不进行这步操作,再分配对象空间使用空闲列表 * 按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器 -GC性能指标: +GC 性能指标: - **吞吐量**:程序的运行时间占总运行时间的比例(总运行时间 = 程序的运行时间 + 内存回收的时间) - 垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例 -- **暂停时间**:执行垃圾收集时,程序的工作线程被暂停的时间 -- **收集频率**:相对于应用程序的执行,收集操作发生的频率 -- **内存占用**:Java 堆区所占的内存大小 +- 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间 +- 收集频率:相对于应用程序的执行,收集操作发生的频率 +- 内存占用:Java 堆区所占的内存大小 - 快速:一个对象从诞生到被回收所经历的时间 **垃圾收集器的组合关系**: @@ -10732,13 +10728,13 @@ GC性能指标: 整堆收集器:G1 -* 红色虚线在JDK9移除、绿色虚线在JDK14弃用该组合、青色虚线在JDK14删除CMS垃圾回收器 +* 红色虚线在 JDK9 移除、绿色虚线在 JDK14 弃用该组合、青色虚线在 JDK14 删除 CMS 垃圾回收器 查看默认的垃圾收回收器: * `-XX:+PrintcommandLineFlags`:查看命令行相关参数(包含使用的垃圾收集器) -* 使用命令行指令:jinfo -flag 相关垃圾回收器参数 进程ID +* 使用命令行指令:jinfo -flag 相关垃圾回收器参数 进程 ID @@ -10748,13 +10744,13 @@ GC性能指标: #### Serial -**Serial**:串行垃圾收集器,作用于新生代,是指使用单线程进行垃圾回收,采用**复制算法** +Serial:串行垃圾收集器,作用于新生代,是指使用单线程进行垃圾回收,采用**复制算法** -**STW(Stop-The-World)**:垃圾回收时,只有一个线程在工作,并且java应用中的所有线程都要暂停,等待垃圾回收的完成 +**STW(Stop-The-World)**:垃圾回收时,只有一个线程在工作,并且 java 应用中的所有线程都要暂停,等待垃圾回收的完成 -**Serial old**:执行老年代垃圾回收的串行收集器,内存回收算法使用的是**标记-整理算法**,同样也采用了串行回收和"Stop the World"机制, +**Serial old**:执行老年代垃圾回收的串行收集器,内存回收算法使用的是**标记-整理算法**,同样也采用了串行回收和 STW 机制 -- Serial old 是运行在 Client 模式下默认的老年代的垃圾回收器 +- Serial old 是 Client 模式下默认的老年代的垃圾回收器 - Serial old 在 Server 模式下主要有两个用途: - 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用 - 作为老年代 CMS 收集器的**后备垃圾回收方案**,在并发收集发生 Concurrent Mode Failure 时使用 @@ -10763,9 +10759,9 @@ GC性能指标: ![](https://gitee.com/seazean/images/raw/master/Java/JVM-Serial收集器.png) -优点:简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,可以获得最高的单线程收集效率 +优点:简单而高效(与其他收集器的单线程比),对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,可以获得最高的单线程收集效率 -缺点:对于交互性较强的应用而言,这种垃圾收集器是不能够接受的,比如Javaweb应用 +缺点:对于交互性较强的应用而言,这种垃圾收集器是不能够接受的,比如 JavaWeb 应用 @@ -10792,7 +10788,7 @@ Parallel Old 收集器:是一个应用于老年代的并行垃圾回收器,* 停顿时间和吞吐量的关系:新生代空间变小 → 缩短停顿时间 → 垃圾回收变得频繁 → 导致吞吐量下降 -在注重吞吐量及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge+Parallel Old 收集器,在 Server 模式下的内存回收性能很好,**Java8 默认是此垃圾收集器组合** +在注重吞吐量及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge + Parallel Old 收集器,在 Server 模式下的内存回收性能很好,**Java8 默认是此垃圾收集器组合** ![](https://gitee.com/seazean/images/raw/master/Java/JVM-ParallelScavenge收集器.png) @@ -10800,17 +10796,17 @@ Parallel Old 收集器:是一个应用于老年代的并行垃圾回收器,* * `-XX:+UseParallelGC`:手动指定年轻代使用Paralle并行收集器执行内存回收任务 * `-XX:+UseParalleloldcc`:手动指定老年代使用并行回收收集器执行内存回收任务 - * 上面两个参数,默认开启一个,另一个也会被开启(互相激活),默认jdk8是开启的 -* `-XX:+UseAdaptivesizepplicy`:设置 Parallel scavenge 收集器具有**自适应调节策略**,在这种模式下,年轻代的大小、Eden 和 Survivor 的比例、晋升老年代的对象年龄等参数会被自动调整,**虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量** -* `-XX:ParallelGcrhreads`:设置年轻代并行收集器的线程数。一般最好与 CPU 数量相等,以避免过多的线程数影响垃圾收集性能 - * 在默认情况下,当 CPU 数量小于8个,ParallelGcThreads 的值等于 CPU 数量 + * 上面两个参数,默认开启一个,另一个也会被开启(互相激活),默认 jdk8 是开启的 +* `-XX:+UseAdaptivesizepplicy`:设置 Parallel scavenge 收集器具有**自适应调节策略**,在这种模式下,年轻代的大小、Eden 和 Survivor 的比例、晋升老年代的对象年龄等参数会被自动调整,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量 +* `-XX:ParallelGcrhreads`:设置年轻代并行收集器的线程数,一般最好与 CPU 数量相等,以避免过多的线程数影响垃圾收集性能 + * 在默认情况下,当 CPU 数量小于 8 个,ParallelGcThreads 的值等于 CPU 数量 * 当 CPU 数量大于 8 个,ParallelGCThreads 的值等于 3+[5*CPU Count]/8] * `-XX:MaxGCPauseMillis`:设置垃圾收集器最大停顿时间(即 STW 的时间),单位是毫秒 * 对于用户来讲,停顿时间越短体验越好;在服务器端,注重高并发,整体的吞吐量 * 为了把停顿时间控制在 MaxGCPauseMillis 以内,收集器在工作时会调整 Java 堆大小或其他一些参数 * `-XX:GCTimeRatio`:垃圾收集时间占总时间的比例 =1/(N+1),用于衡量吞吐量的大小 * 取值范围(0,100)。默认值 99,也就是垃圾回收时间不超过1 - * 与`-xx:MaxGCPauseMillis`参数有一定矛盾性,暂停时间越长,Radio参数就容易超过设定的比例 + * 与 `-xx:MaxGCPauseMillis` 参数有一定矛盾性,暂停时间越长,Radio 参数就容易超过设定的比例 @@ -10820,17 +10816,17 @@ Parallel Old 收集器:是一个应用于老年代的并行垃圾回收器,* #### ParNew -Par 是 Parallel 并行的缩写,New:只能处理的是新生代 +Par 是 Parallel 并行的缩写,New 是只能处理的是新生代 -**并行垃圾收集器**在串行垃圾收集器的基础之上做了改进,**采用复制算法**,将单线程改为了多线程进行垃圾回收,这样可以缩短垃圾回收的时间 +并行垃圾收集器在串行垃圾收集器的基础之上做了改进,**采用复制算法**,将单线程改为了多线程进行垃圾回收,可以缩短垃圾回收的时间 -对于其他的行为(收集算法、stop the world、对象分配规则、回收策略等)同Serial收集器一样,应用在年轻代,除Serial外,只有**ParNew GC能与CMS收集器配合工作** +对于其他的行为(收集算法、stop the world、对象分配规则、回收策略等)同 Serial 收集器一样,应用在年轻代,除 Serial 外,只有**ParNew GC 能与 CMS 收集器配合工作** 相关参数: -* `-XX:+UseParNewGC`,表示年轻代使用并行收集器,不影响老年代 +* `-XX:+UseParNewGC`:表示年轻代使用并行收集器,不影响老年代 -* `-XX:ParallelGCThreads`,默认开启和 CPU 数量相同的线程数 +* `-XX:ParallelGCThreads`:默认开启和 CPU 数量相同的线程数 ![](https://gitee.com/seazean/images/raw/master/Java/JVM-ParNew收集器.png) @@ -10847,18 +10843,18 @@ ParNew 是很多 JVM 运行在 Server 模式下新生代的默认垃圾收集器 #### CMS -CMS全称 Concurrent Mark Sweep,是一款**并发的、使用标记-清除**算法、针对老年代的垃圾回收器,其最大特点是**让垃圾收集线程与用户线程同时工作** +CMS 全称 Concurrent Mark Sweep,是一款**并发的、使用标记-清除**算法、针对老年代的垃圾回收器,其最大特点是**让垃圾收集线程与用户线程同时工作** -CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,停顿时间越短(**低延迟**)越适合与用户交互的程序,良好的响应速度能提升用户体验 +CMS 收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,停顿时间越短(**低延迟**)越适合与用户交互的程序,良好的响应速度能提升用户体验 分为以下四个流程: -- 初始标记:使用STW出现短暂停顿,仅标记一下 GC Roots 能直接关联到的对象,速度很快 -- 并发标记:进行 GC Roots的直接关联对象开始遍历整个对象图,在整个回收过程中耗时最长,不需要STW,可以与用户线程一起并发运行 -- 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,比初始标记时间长但远比并发标记时间短,需要 STW +- 初始标记:使用 STW 出现短暂停顿,仅标记一下 GC Roots 能直接关联到的对象,速度很快 +- 并发标记:进行 GC Roots 开始遍历整个对象图,在整个回收过程中耗时最长,不需要 STW,可以与用户线程并发运行 +- 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,比初始标记时间长但远比并发标记时间短,需要 STW(不停顿就会一直变化,采用写屏障 + 增量更新来避免漏标情况) - 并发清除:清除标记为可以回收对象,不需要移动存活对象,所以这个阶段可以与用户线程同时并发的 -Mark Sweep 会造成内存碎片,还不把算法换成 Mark Compact 的原因: +Mark Sweep 会造成内存碎片,不把算法换成 Mark Compact 的原因: * Mark Compact 算法会整理内存,导致用户线程使用的对象的地址改变,影响用户线程继续执行 @@ -10873,15 +10869,16 @@ Mark Sweep 会造成内存碎片,还不把算法换成 Mark Compact 的原因 缺点: - 吞吐量降低:在并发阶段虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,CPU 利用率不够高 -- CMS收集器无法处理浮动垃圾,可能出现 Concurrent Mode Failure导致另一次Full GC的产生 - 浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,CMS 收集需要预留出一部分内存,不能等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS,导致很长的停顿时间 +- CMS 收集器**无法处理浮动垃圾**,可能出现 Concurrent Mode Failure 导致另一次 Full GC 的产生 + + 浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾(产生了新对象),这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,CMS 收集需要预留出一部分内存,不能等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS,导致很长的停顿时间 - 标记 - 清除算法导致的空间碎片,往往出现老年代空间无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC;为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer)技术,而只能够选择空闲列表(Free List)执行内存分配 参数设置: -* `-XX:+UseConcMarkSweepGC`:手动指定使用CMS收集器执行内存回收任务 +* `-XX:+UseConcMarkSweepGC`:手动指定使用 CMS 收集器执行内存回收任务 - 开启该参数后会自动将`-XX:+UseParNewGC`打开,即:ParNew+CMS+Serial old的组合 + 开启该参数后会自动将 `-XX:+UseParNewGC` 打开,即:ParNew + CMS + Serial old的组合 * `-XX:CMSInitiatingoccupanyFraction`:设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收 @@ -10890,7 +10887,7 @@ Mark Sweep 会造成内存碎片,还不把算法换成 Mark Compact 的原因 * `-XX:+UseCMSCompactAtFullCollection`:用于指定在执行完 Full GC 后对内存空间进行压缩整理,以此避免内存碎片的产生,由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长 -* `-XX:CMSFullGCsBeforecompaction`:设置在执行多少次 Full GC 后对内存空间进行压缩整理 +* `-XX:CMSFullGCsBeforecompaction`:**设置在执行多少次 Full GC 后对内存空间进行压缩整理** * `-XX:ParallelCMSThreads`:**设置CMS的线程数量** @@ -10907,44 +10904,44 @@ Mark Sweep 会造成内存碎片,还不把算法换成 Mark Compact 的原因 ##### G1特点 -G1(Garbage-First)是一款面向服务端应用的垃圾收集器,**应用于新生代和老年代**、采用标记-整理算法、软实时、低延迟、可设定目标(最大STW停顿时间)的垃圾回收器,用于代替CMS,适用于较大的堆(>4~6G),在JDK9之后默认使用G1 +G1(Garbage-First)是一款面向服务端应用的垃圾收集器,**应用于新生代和老年代**、采用标记-整理算法、软实时、低延迟、可设定目标(最大STW停顿时间)的垃圾回收器,用于代替 CMS,适用于较大的堆(>4 ~ 6G),在 JDK9 之后默认使用 G1 G1 对比其他处理器的优点: * **并发与并行:** - * 并行性:G1在回收期间,可以有多个 GC 线程同时工作,有效利用多核计算能力,此时用户线程 STW - * 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此不会在整个回收阶段发生完全阻塞应用程序的情况 + * 并行性:G1 在回收期间,可以有多个 GC 线程同时工作,有效利用多核计算能力,此时用户线程 STW + * 并发性:G1 拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此不会在整个回收阶段发生完全阻塞应用程序的情况 * 其他的垃圾收集器使用内置的 JVM 线程执行 GC 的多线程操作,而 G1 GC 可以采用应用线程承担后台运行的 GC 工作,JVM 的 GC 线程处理速度慢时,系统会**调用应用程序线程加速垃圾回收**过程 * **分区算法:** - * 从分代上看,G1 属于分代型垃圾回收器,区分年轻代和老年代,年轻代依然有 Eden 区和 Survivor 区 - 从堆结构上看,**新生代和老年代不再物理隔离**,不用担心每个代内存是否足够,这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC - * 将整个堆划分成约 2048 个大小相同的独立 Region 块,每个 Region 块大小根据堆空间的实际大小而定,整体被控制在1MB到32MB之间且为2**的N次幂**,所有 Region 大小相同,在 JVM 生命周期内不会被改变。G1 把堆划分成多个大小相等的独立区域,使得每个小空间可以单独进行垃圾回收 - * **新的区域 Humongous**:本身属于老年代区,当出现了一个巨大的对象,超出了分区容量的一半,则这个对象会进入到该区域。如果一个 H 区装不下一个巨型对象,那么 G1 会寻找连续的 H 分区来存储,为了能找到连续的H区,有时候不得不启动 Full GC - * G1 不会对巨型对象进行拷贝,回收时被优先考虑,G1 会跟踪老年代所有 incoming 引用,这样老年代incoming 引用为 0 的巨型对象就可以在新生代垃圾回收时处理掉 + * 从分代上看,G1 属于分代型垃圾回收器,区分年轻代和老年代,年轻代依然有 Eden 区和 Survivor 区。从堆结构上看,**新生代和老年代不再物理隔离**,不用担心每个代内存是否足够,这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC + * 将整个堆划分成约 2048 个大小相同的独立 Region 块,每个 Region 块大小根据堆空间的实际大小而定,整体被控制在 1MB 到 32 MB之间且为 2 的 N 次幂,所有 Region 大小相同,在 JVM 生命周期内不会被改变。G1 把堆划分成多个大小相等的独立区域,使得每个小空间可以单独进行垃圾回收 + * **新的区域 Humongous**:本身属于老年代区,当出现了一个巨型对象超出了分区容量的一半,该对象就会进入到该区域。如果一个 H 区装不下一个巨型对象,那么 G1 会寻找连续的 H 分区来存储,为了能找到连续的H区,有时候不得不启动 Full GC + * G1 不会对巨型对象进行拷贝,回收时被优先考虑,G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为 0 的巨型对象就可以在新生代垃圾回收时处理掉 + + * Region 结构图: - * Region结构图: - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-G1-Region区域.png) +![](https://gitee.com/seazean/images/raw/master/Java/JVM-G1-Region区域.png) - **空间整合:** - - CMS:“标记-清除”算法、内存碎片、若干次 GC 后进行一次碎片整理 - - G1:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(Region 之间)上来看是基于“复制”算法实现的,两种算法都可以避免内存碎片 + - CMS:标记-清除算法、内存碎片、若干次 GC 后进行一次碎片整理 + - G1:整体来看是基于标记 - 整理算法实现的收集器,从局部(Region 之间)上来看是基于复制算法实现的,两种算法都可以避免内存碎片 -- **可预测的停顿时间模型(软实时soft real-time):** +- **可预测的停顿时间模型(软实时 soft real-time):** - 可以指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒 - 由于分块的原因,G1 可以只选取部分区域进行内存回收,这样缩小了回收的范围,对于全局停顿情况也能得到较好的控制 - - G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间,通过过去回收的经验获得),在后台维护一个**优先列表**,每次根据允许的收集时间优先回收价值最大的 Region,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率 + - G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间,通过过去回收的经验获得),在后台维护一个**优先列表**,每次根据允许的收集时间优先回收价值最大的 Region,保证了 G1 收集器在有限的时间内可以获取尽可能高的收集效率 * 相比于 CMS GC,G1 未必能做到 CMS 在最好情况下的延时停顿,但是最差情况要好很多 G1垃圾收集器的缺点: * 相较于 CMS,G1 还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1 无论是为了垃圾收集产生的内存占用还是程序运行时的额外执行负载都要比 CMS 要高 -* 从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则发挥其优势。平衡点在6-8GB之间 +* 从经验上来说,在小内存应用上 CMS 的表现大概率会优于 G1,而 G1 在大内存应用上则发挥其优势。平衡点在 6-8GB 之间 应用场景: @@ -10963,7 +10960,7 @@ G1垃圾收集器的缺点: -* 程序对 Reference 类型数据写操作时,产生一个 Write Barrier 暂时中断操作,检查该对象和 Reference 类型数据是否在不同的 Region(其他收集器:检查老年代对象是否引用了新生代对象),不同便通过 CardTable 把相关引用信息记录到 Reference 类型所属的 Region 的 Remembered Set 之中 +* 程序对 Reference 类型数据写操作时,产生一个 Write Barrier 暂时中断操作,检查该对象和 Reference 类型数据是否在不同的 Region(跨代引用),不同就将相关引用信息记录到 Reference 类型所属的 Region 的 Remembered Set 之中 * 进行内存回收时,在 GC 根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏 垃圾收集器在新生代中建立了记忆集这样的数据结构,可以理解为它是一个抽象类,具体实现记忆集的三种方式: @@ -10972,7 +10969,7 @@ G1垃圾收集器的缺点: * 对象精度 * 卡精度(卡表) -卡表(Card Table)在老年代中,是一种对记忆集的具体实现,主要定义了记忆集的记录精度、与堆内存的映射关系等,卡表中的每一个元素都对应着一块特定大小的内存块。这个内存块称之为卡页(card page),当存在跨代引用时,会将卡页标记为 dirty,JVM 对于卡页的维护也是通过写屏障的方式 +卡表(Card Table)在老年代中,是一种对记忆集的具体实现,主要定义了记忆集的记录精度、与堆内存的映射关系等,卡表中的每一个元素都对应着一块特定大小的内存块,这个内存块称之为卡页(card page),当存在跨代引用时,会将卡页标记为 dirty,JVM 对于卡页的维护也是通过写屏障的方式 收集集合 CSet 代表每次 GC 暂停时回收的一系列目标分区,在任意一次收集暂停中,CSet 所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。年轻代收集 CSet 只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到 CSet 中 @@ -10987,9 +10984,9 @@ G1垃圾收集器的缺点: ##### 工作原理 -G1中提供了三种垃圾回收模式:YoungGC、Mixed GC 和 FullGC,在不同的条件下被触发 +G1 中提供了三种垃圾回收模式:YoungGC、Mixed GC 和 FullGC,在不同的条件下被触发 -* 当堆内存使用达到一定值(默认45%)时,开始老年代并发标记过程 +* 当堆内存使用达到一定值(默认 45%)时,开始老年代并发标记过程 * 标记完成马上开始混合回收过程 @@ -11004,23 +11001,23 @@ G1中提供了三种垃圾回收模式:YoungGC、Mixed GC 和 FullGC,在不 2. 更新 RSet:处理 dirty card queue 更新 RS,此后 RSet 准确的反映对象的引用关系 * dirty card queue:类似缓存,产生了引用先记录在这里,然后更新到 RSet * 作用:产生引用直接更新 RSet 需要线程同步开销很大,使用队列性能好 - 3. 处理 RSet:识别被老年代对象指向的 Eden 中的对象,这些被指向的对象被认为是存活的对象,把需要回收的放入 YoungCSet 中进行回收 + 3. 处理 RSet:识别被老年代对象指向的 Eden 中的对象,这些被指向的对象被认为是存活的对象,把需要回收的分区放入 YoungCSet 中进行回收 4. 复制对象:Eden 区内存段中存活的对象会被复制到 survivor 区,survivor 区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到 old 区中空的内存分段,如果 survivor 空间不够,Eden 空间的部分数据会直接晋升到老年代空间 5. 处理引用:处理 Soft,Weak,Phantom,JNI Weak 等引用,最终 Eden 空间的数据为空,GC 停止工作 * **Concurrent Mark **: * 初始标记:标记从根节点直接可达的对象,这个阶段是 STW 的,并且会触发一次年轻代 GC - * 根区域扫描 (Root Region Scanning):G1 扫描 survivor 区直接可达的老年代区域对象,并标记被引用的对象,这一过程必须在 Young GC 之前完成 - * 并发标记 (Concurrent Marking):在整个堆中进行并发标记(应用程序并发执行),可能被 YoungGC 中断。会计算每个区域的对象活性,即区域中存活对象的比例,**若区域中的所有对象都是垃圾,则这个区域会被立即回收(实时回收)**,给浮动垃圾准备出更多的空间,把需要收集的 Region 放入 CSet 当中 - * 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这阶段需要停顿线程,但是可并行执行 + * 根区域扫描 (Root Region Scanning):扫描 Survivor 区中指向老年代的,被初始标记标记了的引用及引用的对象,这一个过程是并发进行的,但是必须在 Young GC 之前完成 + * 并发标记 (Concurrent Marking):在整个堆中进行并发标记(应用程序并发执行),可能被 YoungGC 中断。会计算每个区域的对象活性,即区域中存活对象的比例,若区域中的所有对象都是垃圾,则这个区域会被立即回收(实时回收),给浮动垃圾准备出更多的空间,把需要收集的 Region 放入 CSet 当中 + * 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这阶段需要停顿线程,但是可并行执行(防止漏标) * 筛选回收:并发清理阶段,首先对 CSet 中各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率 ![](https://gitee.com/seazean/images/raw/master/Java/JVM-G1收集器.jpg) -* **Mixed GC**:当很多对象晋升到老年代 old region 时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即 Mixed GC,除了回收整个 young region,还会**回收一部分**的 old region,过程同 YGC +* **Mixed GC**:当很多对象晋升到老年代时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即 Mixed GC,除了回收整个 young region,还会**回收一部分**的 old region,过程同 YGC - 注意:**是一部分老年代,而不是全部老年代**,可以选择哪些 old region 收集,对垃圾回收的时间进行控制 + 注意:**是一部分老年代,而不是全部老年代**,可以选择哪些老年代 region 收集,对垃圾回收的时间进行控制 在 G1 中,Mixed GC 可以通过 `-XX:InitiatingHeapOccupancyPercent` 设置阈值 @@ -11094,7 +11091,7 @@ ZGC 收集器是一个可伸缩的、低延迟的垃圾收集器,基于 Region * 可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据 * 内存多重映射:多个虚拟地址指向同一个物理地址 -可并发的标记压缩算法:染色指针标识对象是否被标记或移动,读屏障保证在每次应用程序或 GC 程序访问对象时先根据染色指针的标识判断是否被移动,如果被移动就根据转发表访问新的复制对象,并更新引用,不会像 G1 一样必须等待垃圾回收完成才能访问 +可并发的标记压缩算法:染色指针标识对象是否被标记或移动,读屏障保证在每次应用程序或 GC 程序访问对象时先根据染色指针的标识判断是否被移动,如果被移动就根据转发表访问新的移动对象,并更新引用,不会像 G1 一样必须等待垃圾回收完成才能访问 ZGC 目标: @@ -11104,9 +11101,9 @@ ZGC 目标: ZGC 的工作过程可以分为 4 个阶段: -* 并发标记(Concurrent Mark): **遍历对象图做可达性分析**的阶段,也要经过类似于 G1的初始标记、最终标记的短暂停顿 +* 并发标记(Concurrent Mark): **遍历对象图做可达性分析**的阶段,也要经过初始标记和最终标记,需要短暂停顿 * 并发预备重分配( Concurrent Prepare for Relocate): 根据特定的查询条件统计得出本次收集过程要清理哪些 Region,将这些 Region 组成重分配集(Relocation Set) -* 并发重分配(Concurrent Relocate): 重分配是 ZGC 执行过程中的核心阶段,这个过程要把重分配集中的**存活对象**复制到新的 Region 上,并为重分配集中的每个 Region 维护一个**转发表(Forward Table)**,记录从旧地址到新地址的转向关系 +* 并发重分配(Concurrent Relocate): 重分配是 ZGC 执行过程中的核心阶段,这个过程要把重分配集中的**存活对象**复制到新的 Region 上,并为重分配集中的**每个 Region 维护一个转发表**(Forward Table),记录从旧地址到新地址的转向关系 * 并发重映射(Concurrent Remap):修正整个堆中指向重分配集中旧对象的所有引用,ZGC 的并发映射并不是一个必须要立即完成的任务,ZGC 很巧妙地把并发重映射阶段要做的工作,合并到下一次垃圾收集循环中的并发标记阶段里去完成,因为都是要遍历所有对象的,这样合并节省了一次遍历的开销 ZGC 几乎在所有地方并发执行的,除了初始标记的是 STW 的,但这部分的实际时间是非常少的,所以响应速度快,在尽可能对吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟 @@ -11131,7 +11128,7 @@ Serial GC、Parallel GC、Concurrent Mark Sweep GC 这三个 GC 不同: - 最小化地使用内存和并行开销,选 Serial GC - 最大化应用程序的吞吐量,选 Parallel GC -- 最小化GC的中断或停顿时间,选CMS GC +- 最小化 GC 的中断或停顿时间,选 CMS GC ![](https://gitee.com/seazean/images/raw/master/Java/JVM-垃圾回收器总结.png) @@ -11147,7 +11144,7 @@ Serial GC、Parallel GC、Concurrent Mark Sweep GC 这三个 GC 不同: #### 泄露溢出 -内存泄露(memory leak)指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果 +内存泄漏(Memory Leak):是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果 可达性分析算法来判断对象是否是不再使用的对象,本质都是判断一个对象是否还被引用。由于代码的实现不同就会出现很多种内存泄漏问题,让 JVM 误以为此对象还在引用中,无法回收,造成内存泄漏 @@ -11345,7 +11342,7 @@ public Object pop() { unused(25+1) + hash(31) + age(4) + lock(3) = 64bit #64位系统 ``` - * **Klass Word**:类型指针,指向该对象的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;在64位系统中,开启指针压缩(-XX:+UseCompressedOops)或者 JVM 堆的最大值小于 32G,这个指针也是 4byte,否则是 8byte(就是 **Java 中的一个引用的大小**) + * **Klass Word**:类型指针,指向该对象的类对象的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;在 64 位系统中,开启指针压缩(-XX:+UseCompressedOops)或者 JVM 堆的最大值小于 32G,这个指针也是 4byte,否则是 8byte(就是 **Java 中的一个引用的大小**) ```ruby |-----------------------------------------------------| @@ -11367,9 +11364,9 @@ public Object pop() { 实例数据:实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的,都需要记录起来 -对齐填充:Padding 起占位符的作用。64 位系统,由于 HotSpot VM 的自动内存管理系统要求**对象起始地址必须是 8 字节的整数倍**,就是对象的大小必须是 8 字节的整数倍,而对象头部分正好是 8 字节的倍数(1倍或者2倍),因此当对象实例数据部分没有对齐时,就需要通过对齐填充来补全 +对齐填充:Padding 起占位符的作用。64 位系统,由于 HotSpot VM 的自动内存管理系统要求**对象起始地址必须是 8 字节的整数倍**,就是对象的大小必须是 8 字节的整数倍,而对象头部分正好是 8 字节的倍数(1 倍或者 2 倍),因此当对象实例数据部分没有对齐时,就需要通过对齐填充来补全 -32位系统: +32 位系统: * 一个 int 在 java 中占据 4byte,所以 Integer 的大小为: @@ -11470,7 +11467,7 @@ JVM 是通过**栈帧中的对象引用**访问到其内部的对象实例: * 句柄访问:Java 堆中会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息 - 优点:reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集)时只会改变句柄中的实例数据指针,而 reference 本身不需要被修改。 + 优点:reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集)时只会改变句柄中的实例数据指针,而 reference 本身不需要被修改 ![](https://gitee.com/seazean/images/raw/master/Java/JVM-对象访问-句柄访问.png) @@ -11502,8 +11499,8 @@ JVM 是通过**栈帧中的对象引用**访问到其内部的对象实例: 2. 应用阶段 (In Use):对象至少被一个强引用持有着 3. 不可见阶段 (Invisible):程序的执行已经超出了该对象的作用域,不再持有该对象的任何强引用 4. 不可达阶段 (Unreachable):该对象不再被任何强引用所持有,包括 GC Root 的强引用 -5. 收集阶段 (Collected):垃圾回收器已经对该对象的内存空间重新分配做好准备,该对象如果重写了finalize()方法,则会去执行该方法 -6. 终结阶段 (Finalized):等待垃圾回收器对该对象空间进行回收,当对象执行完finalize()方法后仍然处于不可达状态时进入该阶段 +5. 收集阶段 (Collected):垃圾回收器对该对象的内存空间重新分配做好准备,该对象如果重写了 finalize() 方法,则会去执行该方法 +6. 终结阶段 (Finalized):等待垃圾回收器对该对象空间进行回收,当对象执行完 finalize()方 法后仍然处于不可达状态时进入该阶段 7. 对象空间重分配阶段 (De-allocated):垃圾回收器对该对象的所占用的内存空间进行回收或者再分配 @@ -11576,7 +11573,7 @@ Java 对象创建时机: * 实例变量初始化与实例代码块初始化: - 对实例变量直接赋值或者使用实例代码块赋值,编译器会将其中的代码放到类的构造函数中去,并且这些代码会被放在对超类构造函数的调用语句之后 (**Java 要求构造函数的第一条语句必须是超类构造函数的调用语句**),构造函数本身的代码之前 + 对实例变量直接赋值或者使用实例代码块赋值,**编译器会将其中的代码放到类的构造函数中去**,并且这些代码会被放在对超类构造函数的调用语句之后 (Java 要求构造函数的第一条语句必须是超类构造函数的调用语句),构造函数本身的代码之前 * 构造函数初始化: @@ -11596,12 +11593,12 @@ Java 对象创建时机: 在声明实例变量的同时对其进行了赋值操作,那么这个实例变量就被第二次赋值 在实例代码块中又对变量做了初始化操作,那么这个实例变量就被第三次赋值 在构造函数中也对变量做了初始化操作,那么这个实例变量就被第四次赋值 - 在 Java 的对象初始化过程中,一个实例变量最多可以被初始化4次 + 在 Java 的对象初始化过程中,一个实例变量最多可以被初始化 4 次 2. 类的初始化过程与类的实例化过程的异同? 类的初始化是指类加载过程中的初始化阶段对类变量按照代码进行赋值的过程 - 类的实例化是指在类完全加载到内存中后创建对象的过程(类的实例化触发了类的初始化) + 类的实例化是指在类完全加载到内存中后创建对象的过程(类的实例化触发了类的初始化,先初始化才能实例化) 3. 假如一个类还未加载到内存中,那么在创建一个该类的实例时,具体过程是怎样的?(**经典案例**) @@ -11643,9 +11640,9 @@ Java 对象创建时机: `static StaticTest st = new StaticTest();`: - * 实例初始化不一定要在类初始化结束之后才开始 + * 实例实例化不一定要在类初始化结束之后才开始 - * 在同一个类加载器下,一个类型只会被初始化一次。所以一旦开始初始化一个类,无论是否完成后续都不会再重新触发该类型的初始化阶段了(只考虑在同一个类加载器下的情形。因此,在实例化上述程序中的 st 变量时,**实际上是把实例初始化嵌入到了静态初始化流程中,并且在上面的程序中,嵌入到了静态初始化的起始位置**,这就导致了实例初始化完全发生在静态初始化之前,这也是导致 a 为 110,b 为 0 的原因 + * 在同一个类加载器下,一个类型只会被初始化一次。所以一旦开始初始化一个类,无论是否完成后续都不会再重新触发该类型的初始化阶段了(只考虑在同一个类加载器下的情形)。因此在实例化上述程序中的 st 变量时,**实际上是把实例化嵌入到了静态初始化流程中,并且在上面的程序中,嵌入到了静态初始化的起始位置**,这就导致了实例初始化完全发生在静态初始化之前,这也是导致 a 为 110,b 为 0 的原因 代码等价于: @@ -11695,24 +11692,24 @@ Java 对象创建时机: #### 加载阶段 -加载是类加载的一个阶段,注意不要混淆 +加载是类加载的其中一个阶段,注意不要混淆 加载过程完成以下三件事: - 通过类的完全限定名称获取定义该类的二进制字节流(二进制字节码) - 将该字节流表示的**静态存储结构转换为方法区的运行时存储结构**(Java 类模型) -- 在内存中生成一个代表该类的 Class 对象,作为方法区中该类各种数据的访问入口 +- 在内存中生成一个代表该类的 Class 对象,作为该类在方法区中的各种数据的访问入口 其中二进制字节流可以从以下方式中获取: - 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础 - 从网络中获取,最典型的应用是 Applet - 由其他文件生成,例如由 JSP 文件生成对应的 Class 类 -- 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass +- 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 生成字节码 将字节码文件加载至方法区后,会**在堆中**创建一个 java.lang.Class 对象,用来引用位于方法区内的数据结构,该 Class 对象是在加载类的过程中创建的,每个类都对应有一个 Class 类型的对象 -方法区内部采用 C++ 的 instanceKlass 描述 java 类的数据结构: +方法区内部采用 C++ 的 instanceKlass 描述 Java 类的数据结构: * `_java_mirror` 即 java 的类镜像,例如对 String 来说就是 String.class,作用是把 class 暴露给 java 使用 * `_super` 即父类、`_fields` 即成员变量、`_methods` 即方法、`_constants` 即常量池、`_class_loader` 即类加载器、`_vtable` **虚方法表**、`_itable` 接口方法表 @@ -11779,7 +11776,7 @@ Java 对象创建时机: * 类变量也叫静态变量,就是是被 static 修饰的变量 * 实例变量也叫对象变量,即没加 static 的变量 -实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中。类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次 +说明:实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次 类变量初始化: @@ -11816,6 +11813,8 @@ Java 对象创建时机: * 符号引用:一组符号来描述目标,可以是任何字面量,属于编译原理方面的概念,如:包括类和接口的全限名、字段的名称和描述符、方法的名称和**方法描述符** * 直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄,如果有了直接引用,那说明引用的目标必定已经存在于内存之中 +例如:在 `com.demo.Solution` 类中引用了 `com.test.Quest`,把 `com.test.Quest` 作为符号引用存进类常量池,在类加载完后,**用这个符号引用去方法区找这个类的内存地址** + 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等 * 在类加载阶段解析的是非虚方法,静态绑定 @@ -11886,7 +11885,7 @@ class D { public class Test { static { //i = 0; // 给变量赋值可以正常编译通过 - System.out.print(i); // 这句编译器会提示“非法向前引用” + System.out.print(i); // 这句编译器会提示“非法向前引用” } static int i = 1; } @@ -11908,9 +11907,9 @@ public class Test { 类的初始化是懒惰的,只有在首次使用时才会被装载,JVM 不会无条件地装载 Class 类型,Java 虚拟机规定,一个类或接口在初次使用前,必须要进行初始化 -**主动引用**:虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列情况必须对类进行初始化(加载、验证、准备都会随之发生): +**主动引用**:虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列情况必须对类进行初始化(加载、验证、准备都会发生): -* 当创建一个类的实例时,使用 new 关键字,或者通过反射、克隆、反序列化 +* 当创建一个类的实例时,使用 new 关键字,或者通过反射、克隆、反序列化(前文讲述的对象的创建时机) * 当调用类的静态方法或访问静态字段时,遇到 getstatic、putstatic、invokestatic 这三条字节码指令,如果类没有进行过初始化,则必须先触发其初始化 * getstatic:程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池) * putstatic:程序给类的静态变量赋值 @@ -11977,11 +11976,11 @@ init 指的是实例构造器,主要作用是在类实例化过程中执行, * 在 JVM 启动时,通过三大类加载器加载 class * 显式加载: * ClassLoader.loadClass(className):只加载和连接,**不会进行初始化** - * Class.forName(String name, boolean initialize,ClassLoader loader):使用 loader 进行加载和连接,根据参数 initialize 决定是否初始化 + * Class.forName(String name, boolean initialize, ClassLoader loader):使用 loader 进行加载和连接,根据参数 initialize 决定是否初始化 类的唯一性: -* 在 JVM 中表示两个 class 对象是否为同一个类存在的两个必要条件: +* 在 JVM 中表示两个 class 对象判断为同一个类存在的两个必要条件: - 类的完整类名必须一致,包括包名 - 加载这个类的 ClassLoader(指 ClassLoader 实例对象)必须相同 * 这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true @@ -12017,7 +12016,7 @@ init 指的是实例构造器,主要作用是在类实例化过程中执行, * 处于安全考虑,Bootstrap 启动类加载器只加载包名为 java、javax、sun 等开头的类 * 类加载器负责加载在 `JAVA_HOME/jre/lib `或 `sun.boot.class.path` 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的类,并且是虚拟机识别的类库加载到虚拟机内存中 * 仅按照文件名识别,如 rt.jar 名字不符合的类库即使放在 lib 目录中也不会被加载 - * 启动类加载器无法被 Java 程序直接引用,在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替 + * 启动类加载器无法被 Java 程序直接引用,编写自定义类加载器时,如果要把加载请求委派给启动类加载器,直接使用 null 代替 * 扩展类加载器(Extension ClassLoader): * 由 ExtClassLoader (sun.misc.Launcher$ExtClassLoader) 实现,上级为 Bootstrap,显示为 null * 将 `JAVA_HOME/jre/lib/ext `或者被 `java.ext.dir` 系统变量所指定路径中的所有类库加载到内存中 @@ -12079,10 +12078,10 @@ ClassLoader 类,是一个抽象类,其后所有的类加载器都继承自 C ClassLoader 类常用方法: * `getParent()`:返回该类加载器的超类加载器 -* `loadclass(String name)`:加载名为 name 的类,返回结果为 Class 类的实例,该方法就是双亲委派模式 +* `loadclass(String name)`:加载名为 name 的类,返回结果为 Class 类的实例,**该方法就是双亲委派模式** * `findclass(String name)`:查找二进制名称为 name 的类,返回结果为 Class 类的实例,该方法会在检查完父类加载器之后被 loadClass() 方法调用 * `findLoadedClass(String name)`:查找名称为 name 的已经被加载过的类,final 修饰无法重写 -* `defineClass(String name,byte[] b,int off,int len)`:将字节流解析成 JVM 能够识别的类对象 +* `defineClass(String name, byte[] b, int off, int len)`:将字节流解析成 JVM 能够识别的类对象 * `resolveclass(Class c)`:链接指定的 Java 类,可以使类的 Class 对象创建完成的同时也被解析 * `InputStream getResourceAsStream(String name)`:指定资源名称获取输入流 @@ -12139,7 +12138,7 @@ ClassLoader 类常用方法: 此时执行 main 函数,会出现异常,在类 java.lang.String 中找不到 main 方法,防止恶意篡改核心 API 库。出现该信息是因为双亲委派的机制,java.lang.String 的在启动类加载器(Bootstrap)得到加载,启动类加载器优先级更高,在核心 jre 库中有其相同名字的类文件,但该类中并没有 main 方法 -双亲委派机制的缺点:检查类是否加载的委托过程是单向的,这个方式虽然从结构上看比较清晰,使各个 ClassLoader 的职责非常明确,但**顶层的 ClassLoader 无法访问底层的 ClassLoader 所加载的类** +双亲委派机制的缺点:检查类是否加载的委托过程是单向的,这个方式虽然从结构上看比较清晰,使各个 ClassLoader 的职责非常明确,但**顶层的 ClassLoader 无法访问底层的 ClassLoader 所加载的类**(可见性) @@ -12290,7 +12289,7 @@ public class MyClassLoader extends ClassLoader{ ByteArrayOutputStream baos = null; try { //获取字节码文件的完整路径 - String fileName = byteCodePath + className + ".class"; + String fileName = classPath + className + ".class"; //获取一个输入流 bis = new BufferedInputStream(new FileInputStream(fileName)); //获取一个输出流 @@ -12303,7 +12302,7 @@ public class MyClassLoader extends ClassLoader{ } //获取内存中的完整的字节数组的数据 byte[] byteCodes = baos.toByteArray(); - //调 用defineClass(),将字节数组的数据转换为 Class的实例。 + //调用 defineClass(),将字节数组的数据转换为 Class 的实例。 Class clazz = defineClass(null, byteCodes, 0, byteCodes.length); return clazz; } catch (IOException e) { @@ -12329,7 +12328,7 @@ public class MyClassLoader extends ClassLoader{ ```java public static void main(String[] args) { - MyClassLoader loader = new MyClassLoader("D:\Workspace\Project/JVM_study/src/java1/"); + MyClassLoader loader = new MyClassLoader("D:\Workspace\Project\JVM_study\src\java1\"); try { Class clazz = loader.loadClass("Demo1"); @@ -12354,7 +12353,7 @@ public static void main(String[] args) { * 扩展机制被移除,扩展类加载器由于**向后兼容性**的原因被保留,不过被重命名为平台类加载器(platform classloader),可以通过 ClassLoader 的新方法 getPlatformClassLoader() 来获取 -* JDK9 基于模块化进行构建(原来的 rt.jar 和 tools.jar 被拆分成数十个JMOD文件),其中的 Java 类库就满足了可扩展的需求,那就无须再保留 `\lib\ext` 目录,此前使用这个目录或者 `java.ext.dirs` 系统变量来扩展 JDK 功能的机制就不需要继续存在 +* JDK9 基于模块化进行构建(原来的 rt.jar 和 tools.jar 被拆分成数个 JMOD 文件),其中 Java 类库就满足了可扩展的需求,那就无须再保留 `\lib\ext` 目录,此前使用这个目录或者 `java.ext.dirs` 系统变量来扩展 JDK 功能的机制就不需要再存在 * 启动类加载器、平台类加载器、应用程序类加载器全都继承于 `jdk.internal.loader.BuiltinClassLoader` @@ -12430,7 +12429,7 @@ Java 语言:跨平台的语言(write once ,run anywhere) 机器码:各种用二进制编码方式表示的指令,与 CPU 紧密相关,所以不同种类的 CPU 对应的机器指令不同 -指令:指令就是把机器码中特定的0和1序列,简化成对应的指令,例如 mov,inc 等,可读性稍好,但是不同的硬件平台的同一种指令(比如mov),对应的机器码也可能不同 +指令:指令就是把机器码中特定的 0 和 1 序列,简化成对应的指令,例如 mov,inc 等,可读性稍好,但是不同的硬件平台的同一种指令(比如 mov),对应的机器码也可能不同 指令集:不同的硬件平台支持的指令是有区别的,每个平台所支持的指令,称之为对应平台的指令集 @@ -13060,7 +13059,7 @@ public class Demo { 4: invokespecial #3 // Method "":()V ``` - dup 是复制操作数栈栈顶的内容,需要两份引用原因: + **dup 是复制操作数栈栈顶的内容**,需要两份引用原因: - 一个要配合 invokespecial 调用该对象的构造方法 :()V (会消耗掉栈顶一个引用) - 一个要配合 astore_1 赋值给局部变量 @@ -13199,30 +13198,31 @@ JVM 处理异常(catch 语句)不是由字节码指令来实现的,而是* * 字节码: - * 多出一个 **Exception table** 的结构,[from, to) 是**前闭后开**的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号 - * 8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置,因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用 + * 多出一个 **Exception table** 的结构,**[from, to) 是前闭后开的检测范围**,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号 + * 11 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置,因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用 ```java 0: iconst_0 1: istore_1 // 0 -> i ->赋值 2: bipush 10 // try 10 放入操作数栈顶 4: istore_1 // 10 -> i 将操作数栈顶数据弹出,存入局部变量表的 slot1 - 5: bipush 30 // finally + 5: bipush 30 // 【finally】 7: istore_1 // 30 -> i 8: goto 27 // return ----------------------------------- 11: astore_2 // catch Exceptin -> e ---------------------- 12: bipush 20 // 14: istore_1 // 20 -> i - 15: bipush 30 // finally + 15: bipush 30 // 【finally】 17: istore_1 // 30 -> i 18: goto 27 // return ----------------------------------- 21: astore_3 // catch any -> slot 3 ---------------------- - 22: bipush 30 // finally + 22: bipush 30 // 【finally】 24: istore_1 // 30 -> i 25: aload_3 // 将局部变量表的slot 3数据弹出,放入操作数栈栈顶 26: athrow // throw 抛出异常 27: return Exception table: + // 任何阶段出现任务异常都会执行 finally from to target type 2 5 11 Class java/lang/Exception 2 5 21 any // 剩余的异常类型,比如 Error @@ -13261,10 +13261,10 @@ finally 中的代码被**复制了 3 份**,分别放入 try 流程,catch 流 ```java 0: bipush 10 // 10 放入栈顶 - 2: istore_0 // 10 -> slot 0 (从栈顶移除了) + 2: istore_0 // 10 -> slot 0 【从栈顶移除了】 3: bipush 20 // 20 放入栈顶 5: ireturn // 返回栈顶 int(20) - 6: astore_1 // catch any -> slot 1 存入局部变量表的 slot1 + 6: astore_1 // catch any 存入局部变量表的 slot1 7: bipush 20 // 20 放入栈顶 9: ireturn // 返回栈顶 int(20) Exception table: @@ -13294,10 +13294,10 @@ finally 中的代码被**复制了 3 份**,分别放入 try 流程,catch 流 ```java 0: bipush 10 // 10 放入栈顶 - 2: istore_0 // 10 -> slot 0 (从栈顶移除了) + 2: istore_0 // 10 -> slot 0 【从栈顶移除了】 3: bipush 20 // 20 放入栈顶 5: ireturn // 返回栈顶 int(20) - 6: astore_1 // catch any -> slot 1 存入局部变量表的 slot1 + 6: astore_1 // catch any 存入局部变量表的 slot1 7: bipush 20 // 20 放入栈顶 9: ireturn // 返回栈顶 int(20) Exception table: @@ -13305,7 +13305,7 @@ finally 中的代码被**复制了 3 份**,分别放入 try 流程,catch 流 0 3 6 any ``` - * 由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果肯 finally 的为准 + * 由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果以 finally 的为准 * 字节码中没有 **athrow** ,表明如果在 finally 中出现了 return,会**吞掉异常** * 不吞异常 @@ -13329,11 +13329,11 @@ finally 中的代码被**复制了 3 份**,分别放入 try 流程,catch 流 ```java 0: bipush 10 // 10 放入栈顶 - 2: istore_0 // 10 -> i,赋值给i,放入slot 0 + 2: istore_0 // 10 赋值给i,放入slot 0 3: iload_0 // i(10)加载至操作数栈 - 4: istore_1 // 10 -> slot 1,暂存至 slot 1,目的是为了固定返回值 + 4: istore_1 // 10 -> slot 1,【暂存至 slot 1,目的是为了固定返回值】 5: bipush 20 // 20 放入栈顶 - 7: istore_0 // 20 -> i + 7: istore_0 // 20 slot 0 8: iload_1 // slot 1(10) 载入 slot 1 暂存的值 9: ireturn // 返回栈顶的 int(10) 10: astore_2 // catch any -> slot 2 存入局部变量表的 slot2 @@ -13457,7 +13457,7 @@ javap -v Demo.class:省略 #### 基本介绍 -执行引擎:Java虚拟机的核心组成部分之一,JVM的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,需要执行引擎将字节码指令解释/编译为对应平台上的本地机器指令 +执行引擎:Java 虚拟机的核心组成部分之一,类加载主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,需要执行引擎将字节码指令解释/编译为对应平台上的本地机器指令 虚拟机是一个相对于物理机的概念,这两种机器都有代码执行能力: @@ -13511,9 +13511,9 @@ JIT 编译在默认情况是异步进行的,当触发某方法或某代码块 HotSpot VM 采用的热点探测方式是基于计数器的热点探测,为每一个方法都建立 2 个不同类型的计数器:方法调用计数器(Invocation Counter)和回边计数器(BackEdge Counter) -* 方法调用计数器:用于统计方法被调用的次数,默认阈值在 Client 模式 下是 1500 次,在 Server 模式下是10000 次,超过这个阈值,就会触发 JIT 编译,阈值可以通过虚拟机参数 `-XX:CompileThreshold` 设置 +* 方法调用计数器:用于统计方法被调用的次数,默认阈值在 Client 模式 下是 1500 次,在 Server 模式下是 10000 次(需要进行激进的优化),超过这个阈值,就会触发 JIT 编译,阈值可以通过虚拟机参数 `-XX:CompileThreshold` 设置 - 工作流程:当一个方法被调用时, 会先检查该方法是否存在被 JIT 编译过的版本,存在则使用编译后的本地代码来执行;如果不存在则将此方法的调用计数器值加 1,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阈值,如果超过阈值会向即时编译器提交一个该方法的代码编译请求 + 工作流程:当一个方法被调用时, 会先检查该方法是否存在被 JIT 编译过的版本,存在则使用编译后的本地代码来执行;如果不存在则将此方法的调用计数器值加 1,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阈值,如果超过阈值会向即时编译器**提交一个该方法的代码编译请求** * 回边计数器:统计一个方法中循环体代码执行的次数,在字节码中控制流向后跳转的指令称为回边 @@ -13531,7 +13531,7 @@ HotSpot VM 内嵌两个 JIT 编译器,分别为 Client Compiler 和 Server Com C1 编译器会对字节码进行简单可靠的优化,耗时短,以达到更快的编译速度,C1 编译器的优化方法: -* 方法内联:**将引用的函数代码编译到引用点处**,这样可以减少栈帧的生成,减少参数传递以及跳转过程 +* 方法内联:**将调用的函数代码编译到调用点处**,这样可以减少栈帧的生成,减少参数传递以及跳转过程 方法内联能够消除方法调用的固定开销,任何方法除非被内联,否则调用都会有固定开销,来源于保存程序在该方法中的执行位置,以及新建、压入和弹出新方法所使用的栈帧。 @@ -13558,7 +13558,7 @@ C1 编译器会对字节码进行简单可靠的优化,耗时短,以达到 * 内联缓存:是一种加快动态绑定的优化技术(方法调用部分详解) -C2 编译器进行耗时较长的优化以及激进优化,优化的代码执行效率更高,当激进优化的假设不成立时,再退回使用 C1 编译,这也是分层编译比直接使用 C2 逃逸分析进行编译的性能低,也会使用分层编译的原因 +C2 编译器进行耗时较长的优化以及激进优化,优化的代码执行效率更高,当激进优化的假设不成立时,再退回使用 C1 编译,这也是使用分层编译的原因 C2 的优化主要是在全局层面,逃逸分析是优化的基础:标量替换、栈上分配、同步消除 @@ -13601,7 +13601,7 @@ Java 虚拟机识别方法的关键在于类名、方法名以及方法描述符 * **方法描述符是由方法的参数类型以及返回类型所构成**,Java 层面叫方法特征签名 * 在同一个类中,如果同时出现多个名字相同且描述符也相同的方法,那么 Java 虚拟机会在类的验证阶段报错 -JVM 根据名字和描述符来判断的,只要返回值不一样,其它完全一样,在 JVM 中是允许的,但 Java 语言不允许 +JVM 根据名字和描述符来判断的,只要返回值不一样(方法描述符不一样),其它完全一样,在 JVM 中是允许的,但 Java 语言不允许 ```java // 返回值类型不同,编译阶段直接报错 @@ -13628,7 +13628,7 @@ public static int invoke(Object... args) { - 静态链接:当一个字节码文件被装载进 JVM 内部时,如果被调用的目标方法在编译期可知,且运行期保持不变,将调用方法的符号引用转换为直接引用的过程称之为静态链接(类加载的解析阶段) - 动态链接:如果被调用的方法在编译期无法被确定下来,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此被称为动态链接(初始化后的解析阶段) -对应的方法的绑定(分配)机制为:静态绑定和动态绑定。绑定是一个字段、方法或者类从符号引用被替换为直接引用的过程,仅发生一次: +对应方法的绑定(分配)机制:静态绑定和动态绑定。绑定是一个字段、方法或者类从符号引用被替换为直接引用的过程,仅发生一次: - 静态绑定:被调用的目标方法在编译期可知,且运行期保持不变,将这个方法与所属的类型进行绑定 - 动态绑定:被调用的目标方法在编译期无法确定,只能在程序运行期根据实际的类型绑定相关的方法 @@ -13673,7 +13673,7 @@ public static int invoke(Object... args) { 动态调用指令: -- invokedynamic:动态解析出需要调用的方法, +- invokedynamic:动态解析出需要调用的方法 - Java7 为了实现动态类型语言支持而引入了该指令,但是并没有提供直接生成 invokedynamic 指令的方法,需要借助 ASM 这种底层字节码工具来产生 invokedynamic 指令 - Java8 的 lambda 表达式的出现,invokedynamic 指令在 Java 中才有了直接生成方式 @@ -13686,7 +13686,7 @@ public static int invoke(Object... args) { 指令说明: -- 如果虚拟机能够确定目标方法有且仅有一个,比如说目标方法被标记为 final,那么可以不通过动态类型,直接确定目标方法 +- 如果虚拟机能够确定目标方法有且仅有一个,比如说目标方法被标记为 final,那么可以不通过动态绑定,直接确定目标方法 - 普通成员方法是由 invokevirtual 调用,属于**动态绑定**,即支持多态 @@ -13791,7 +13791,7 @@ public class Demo { Java 虚拟机中关于方法重写的判定基于方法描述符,如果子类定义了与父类中非私有、非静态方法同名的方法,只有当这两个方法的参数类型以及返回类型一致,Java 虚拟机才会判定为重写。 -**理解多态**: +理解多态: - 多态有编译时多态和运行时多态,即静态绑定和动态绑定 - 前者是通过方法重载实现,后者是通过重写实现(子类覆盖父类方法,虚方法表) @@ -13799,9 +13799,9 @@ Java 虚拟机中关于方法重写的判定基于方法描述符,如果子类 方法重写的本质: -1. 找到操作数栈的第一个元素所执行的对象的实际类型,记作 C +1. 找到操作数栈的第一个元素**所执行的对象的实际类型**,记作 C -2. 如果在类型 C 中找到与常量中的描述符和名称都相符的方法,则进行访问**权限校验**(私有的),如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回 java.lang.IllegalAccessError 异常 +2. 如果在类型 C 中找到与描述符和名称都相符的方法,则进行访问**权限校验**(私有的),如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回 java.lang.IllegalAccessError 异常 IllegalAccessError:表示程序试图访问或修改一个属性或调用一个方法,这个属性或方法没有权限访问,一般会引起编译器异常。如果这个错误发生在运行时,就说明一个类发生了不兼容的改变 @@ -13817,7 +13817,7 @@ Java 虚拟机中关于方法重写的判定基于方法描述符,如果子类 ##### 虚方法表 -在虚拟机工作过程中会频繁使用到动态分配,每次动态分配的过程中都要重新在类的元数据中搜索合适目标,影响到执行效率。为了提高性能,JVM 采取了一种用**空间换取时间**的策略来实现动态绑定,在类的方法区建立一个虚方法表(virtual method table),实现使用索引表来代替查找,可以快速定位目标方法 +在虚拟机工作过程中会频繁使用到动态绑定,每次动态绑定的过程中都要重新在类的元数据中搜索合适目标,影响到执行效率。为了提高性能,JVM 采取了一种用**空间换取时间**的策略来实现动态绑定,在每个类的方法区建立一个虚方法表(virtual method table),实现使用索引表来代替查找,可以快速定位目标方法 * invokevirtual 所使用的虚方法表(virtual method table,vtable),执行流程 1. 先通过栈帧中的对象引用找到对象,分析对象头,找到对象的实际 Class @@ -13836,7 +13836,7 @@ Java 虚拟机中关于方法重写的判定基于方法描述符,如果子类 方法表满足以下的特质: * 其一,子类方法表中包含父类方法表中的**所有方法**,并且在方法表中的索引值与父类方法表种的索引值相同 -* 其二,**非重写的方法指向父类的方法表项**,与父类共享一个方法表项,**重写的方法指向本身自己的实现** +* 其二,**非重写的方法指向父类的方法表项,与父类共享一个方法表项,重写的方法指向本身自己的实现**。所以这就是为什么多态情况下可以访问父类的方法。 @@ -13887,7 +13887,7 @@ class Girl extends Person { ##### 内联缓存 -内联缓存:是一种加快动态绑定的优化技术,它能够缓存虚方法调用中**调用者的动态类型以及该类型所对应的目标方法**。在之后的执行过程中,如果碰到已缓存的类型,内联缓存便会直接调用该类型所对应的目标方法;如果没有碰到已缓存的类型,内联缓存则会退化至使用基于方法表的动态绑定 +内联缓存:是一种加快动态绑定的优化技术,能够缓存虚方法调用中**调用者的动态类型以及该类型所对应的目标方法**。在之后的执行过程中,如果碰到已缓存的类型,便会直接调用该类型所对应的目标方法;反之内联缓存则会退化至使用基于方法表的动态绑定 多态的三个术语: @@ -14069,7 +14069,7 @@ while(iter.hasNext()) { } ``` -注意:foreach 循环写法,能够配合数组以及所有实现了 Iterable 接口的集合类一起使用,其中 Iterable 用来获取集合的迭代器( Iterator ) +注意:foreach 循环写法,能够配合数组以及所有实现了 Iterable 接口的集合类一起使用,其中 Iterable 用来获取集合的迭代器 @@ -14096,7 +14096,7 @@ switch (str) { } ``` -注意:switch 配合 String 和枚举使用时,变量不能为null +注意:**switch 配合 String 和枚举使用时,变量不能为null** 会被编译器转换为: @@ -14145,12 +14145,12 @@ enum Sex { public class Candy7 { public static void foo(Sex sex) { switch (sex) { - case MALE: - System.out.println("男"); - break; - case FEMALE: - System.out.println("女"); - break; + case MALE: + System.out.println("男"); + break; + case FEMALE: + System.out.println("女"); + break; } } } @@ -14166,7 +14166,7 @@ public class Candy7 { * 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1 */ static class $MAP { - // 数组大小即为枚举元素个数,里面存储case用来对比的数字 + // 数组大小即为枚举元素个数,里面存储 case 用来对比的数字 static int[] map = new int[2]; static { map[Sex.MALE.ordinal()] = 1; @@ -14228,9 +14228,15 @@ public final class Sex extends Enum { + + +*** + + + #### try-w-r -JDK 7 开始新增了对需要关闭的资源处理的特殊语法`try-with-resources`,格式: +JDK 7 开始新增了对需要关闭的资源处理的特殊语法 `try-with-resources`,格式: ```java try(资源变量 = 创建资源对象){ @@ -14250,7 +14256,7 @@ try(InputStream is = new FileInputStream("d:\\1.txt")) { 转换成: -`addSuppressed(Throwable e)`:添加被压制异常,是为了防止异常信息的丢失(try-with-resources 生成的 fianlly 中如果抛出了异常) +`addSuppressed(Throwable e)`:添加被压制异常,是为了防止异常信息的丢失(**fianlly 中如果抛出了异常**) ```java try { @@ -14374,6 +14380,10 @@ public class Candy11 { +*** + + + ##### 带参优化 引用局部变量的匿名内部类,源代码: @@ -14412,9 +14422,9 @@ public class Candy11 { 局部变量在底层创建为内部类的成员变量,必须是 final 的原因: -* 在Java中方法调用是值传递的,在匿名内部类中对变量的操作都是基于原变量的副本,不会影响到原变量的值,所以原变量的值的改变也无法同步到副本中 +* 在 Java 中方法调用是值传递的,在匿名内部类中对变量的操作都是基于原变量的副本,不会影响到原变量的值,所以原变量的值的改变也无法同步到副本中 -* 外部变量为final是在编译期以强制手段确保用户不会在内部类中做修改原变量值的操作,也是防止外部操作修改了变量而内部类无法随之变化出现的影响 +* 外部变量为 final 是在编译期以强制手段确保用户不会在内部类中做修改原变量值的操作,也是**防止外部操作修改了变量而内部类无法随之变化**出现的影响 在创建 `Candy11$1 ` 对象时,将 x 的值赋值给了 `Candy11$1` 对象的 val 属性,x 不应该再发生变化了,因为发生变化,this.val$x 属性没有机会再跟着变化 @@ -14459,7 +14469,7 @@ public Object invoke(Object obj, Object[] args)throws Exception { method.getModifiers()); parent.setDelegate(acc); } - //调用本地方法实现 + // 【调用本地方法实现】 return invoke0(method, obj, args); } private static native Object invoke0(Method m, Object obj, Object[] args); @@ -14473,7 +14483,7 @@ public class GeneratedMethodAccessor1 extends MethodAccessorImpl { throw new IllegalArgumentException(); } try { - // 可以看到,已经是直接调用方法 + // 【可以看到,已经是直接调用方法】 Reflect1.foo(); // 因为没有返回值 return null; @@ -14512,7 +14522,7 @@ public class GeneratedMethodAccessor1 extends MethodAccessorImpl { 3. 并发数:同一时刻,对服务器有实际交互的请求数 4. QPS:Queries Per Second,每秒处理的查询量 5. TPS:Transactions Per Second,每秒产生的事务数 -6. 内存占用:Java堆区所占的内存大小 +6. 内存占用:Java 堆区所占的内存大小 @@ -14954,12 +14964,11 @@ jinfo -flag = # 堆 -Xms2048m <==> -XX:InitialHeapSize=2048m 设置JVM初始堆内存为2048M(默认为物理内存的1/64) -Xmx2048m <==> -XX:MaxHeapSize=2048m 设置JVM最大堆内存为2048M(默认为物理内存的1/4) --Xmn2g <==> -XX:NewSize=2g -XX:MaxNewSize=2g 设置年轻代大小为2G +-Xmn2g <==> -XX:NewSize=2g 设置年轻代大小为2G -XX:SurvivorRatio=8 设置Eden区与Survivor区的比值,默认为8 -XX:NewRatio=2 设置老年代与年轻代的比例,默认为2 -XX:+UseAdaptiveSizePolicy 设置大小比例自适应,默认开启 --XX:PretenureSizeThreadshold=1024 设置让大于此阈值的对象直接分配在老年代, - 只对Serial、ParNew收集器有效 +-XX:PretenureSizeThreadshold=1024 设置让大于此阈值的对象直接分配在老年代,只对Serial、ParNew收集器有效 -XX:MaxTenuringThreshold=15 设置新生代晋升老年代的年龄限制,默认为15 -XX:TargetSurvivorRatio 设置MinorGC结束后Survivor区占用空间的期望比例 @@ -17344,16 +17353,16 @@ UML 从目标系统的不同角度出发,定义了用例图、类图、对象 ```java public class Singleton { - //私有构造方法 + // 私有构造方法 private Singleton() {} - //在成员位置创建该类的对象 + // 在成员位置创建该类的对象 private static final Singleton instance = new Singleton(); - //对外提供静态方法获取该对象 + // 对外提供静态方法获取该对象 public static Singleton getInstance() { return instance; } - //解决序列化问题 + // 解决序列化问题 protected Object readResolve() { return INSTANCE; } @@ -17367,15 +17376,15 @@ UML 从目标系统的不同角度出发,定义了用例图、类图、对象 * 对单例声明 transient,然后实现 readObject(ObjectInputStream in) 方法,复用原来的单例 - 条件:访问权限为private/protected、返回值必须是Object、异常可以不抛 + 条件:访问权限为 private/protected、返回值必须是 Object、异常可以不抛 - * 实现readResolve()方法,当 JVM 从内存中反序列化地"组装"一个新对象,就会自动调用readResolve方法返回原来单例 + * 实现 readResolve() 方法,当 JVM 从内存中反序列化地"组装"一个新对象,就会自动调用 readResolve 方法返回原来单例 * 问题3:为什么构造方法设置为私有? 是否能防止反射创建新的实例? 防止其他类无限创建对象;不能防止反射破坏 * 问题4:这种方式是否能保证单例对象创建时的线程安全? - 能,静态变量初始化在类加载时完成,由JVM保证线程安全 + 能,静态变量初始化在类加载时完成,由 JVM 保证线程安全 * 问题5:为什么提供静态方法而不是直接将 INSTANCE 设置为 public? 更好的封装性、提供泛型支持、可以改进成懒汉单例设计 @@ -17466,16 +17475,16 @@ UML 从目标系统的不同角度出发,定义了用例图、类图、对象 ```java public class Singleton { - //私有构造方法 + // 私有构造方法 private Singleton() {} private static volatile Singleton instance; - //对外提供静态方法获取该对象 + // 对外提供静态方法获取该对象 public static Singleton getInstance() { - //第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例 + // 第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例 if(instance == null) { synchronized (Singleton.class) { - //抢到锁之后再次判断是否为null + // 抢到锁之后再次判断是否为null if(instance == null) { instance = new Singleton(); } @@ -17490,14 +17499,14 @@ UML 从目标系统的不同角度出发,定义了用例图、类图、对象 ```java public class Singleton { - //私有构造方法 + // 私有构造方法 private Singleton() {} private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } - //对外提供静态方法获取该对象 + // 对外提供静态方法获取该对象 public static Singleton getInstance() { return SingletonHolder.INSTANCE; } @@ -17526,13 +17535,13 @@ UML 从目标系统的不同角度出发,定义了用例图、类图、对象 ```java public class Singleton implements Serializable { //实现序列化接口 - //私有构造方法 + // 私有构造方法 private Singleton() {} private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } - //对外提供静态方法获取该对象 + // 对外提供静态方法获取该对象 public static Singleton getInstance() { return SingletonHolder.INSTANCE; } @@ -17574,7 +17583,7 @@ UML 从目标系统的不同角度出发,定义了用例图、类图、对象 * 解决方法: - 在 Singleton 类中添加`readResolve()`方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新创建的对象 + 在 Singleton 类中添加 `readResolve()` 方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新创建的对象 ```java private Object readResolve() { @@ -17582,7 +17591,7 @@ UML 从目标系统的不同角度出发,定义了用例图、类图、对象 } ``` - ObjectInputStream类源码分析: + ObjectInputStream 类源码分析: ```java public final Object readObject() throws IOException, ClassNotFoundException{ diff --git a/Prog.md b/Prog.md index 580d323..0e2eba0 100644 --- a/Prog.md +++ b/Prog.md @@ -3655,7 +3655,7 @@ Servlet 为了保证其线程安全,一般不为 Servlet 设置成员变量, #### 基本介绍 -ThreadLocal 类用来提供线程内部的局部变量,这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量,分配在 TLAB +ThreadLocal 类用来提供线程内部的局部变量,这种变量在多线程环境下访问(通过 get 和 set 方法访问)时能保证各个线程的变量相对独立于其他线程内的变量,分配在 TLAB ThreadLocal 实例通常来说都是 `private static` 类型的,属于一个线程的本地变量,用于关联线程和线程上下文。每个线程都会在 ThreadLocal 中保存一份该线程独有的数据,所以是线程安全的 @@ -3876,7 +3876,7 @@ JDK8 前后对比: } ``` -* nextHashCode():计算哈希值,ThreadLocal 的散列方式称之为 **斐波那契散列**,每次获取哈希值都会加上 HASH_INCREMENT,这样做可以尽量避免 hash 冲突,让哈希值能均匀的分布在 2 的 n 次方的数组中 +* nextHashCode():计算哈希值,ThreadLocal 的散列方式称之为**斐波那契散列**,每次获取哈希值都会加上 HASH_INCREMENT,这样做可以尽量避免 hash 冲突,让哈希值能均匀的分布在 2 的 n 次方的数组中 ```java private static int nextHashCode() { @@ -4019,7 +4019,7 @@ static class Entry extends WeakReference> { ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { // 初始化table,创建一个长度为16的Entry数组 table = new Entry[INITIAL_CAPACITY]; - // 寻址算法计算索引 + // 【寻址算法】计算索引 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); // 创建 entry 对象 存放到指定位置的 slot 中 table[i] = new Entry(firstKey, firstValue); @@ -4042,7 +4042,7 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { * set():添加数据,ThreadLocalMap 使用**线性探测法来解决哈希冲突** - * 该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出 + * 该方法会一直探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出 * 在探测过程中 ThreadLocal 会占用 key 为 null,value 不为 null 的脏 Entry 对象,防止内存泄漏 * 假设当前 table 长度为16,计算出来 key 的 hash 值为 14,如果 table[14] 上已经有值,并且其 key 与当前 key 不一致,那么就发生了 hash 冲突,这个时候将 14 加 1 得到 15,取 table[15] 进行判断,如果还是冲突会回到 0,取 table[0],以此类推,直到可以插入,可以把 Entry[] table 看成一个**环形数组** @@ -4068,7 +4068,7 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { // key 为 null,但是值不为 null,说明之前的 ThreadLocal 对象已经被回收了,当前是过期数据 if (k == null) { - // 碰到一个过期的 slot,当前数据占用该槽位,替换过期数据 + // 【碰到一个过期的 slot,当前数据占用该槽位,替换过期数据】 // 这个方法还进行了垃圾清理动作,防止内存泄漏 replaceStaleEntry(key, value, i); return; @@ -4125,7 +4125,7 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { if (slotToExpunge == staleSlot) slotToExpunge = i; - // 清理过期数据,expungeStaleEntry 探测式清理,cleanSomeSlots 启发式清理 + // 【清理过期数据,expungeStaleEntry 探测式清理,cleanSomeSlots 启发式清理】 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } @@ -4140,7 +4140,7 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { // staleSlot 位置添加数据,上面的所有逻辑都不会更改 staleSlot 的值 tab[staleSlot] = new Entry(key, value); - // 条件成立说明除了 staleSlot 以外,还发现其它的过期 slot,所以要开启清理数据的逻辑 + // 条件成立说明除了 staleSlot 以外,还发现其它的过期 slot,所以要【开启清理数据的逻辑】 if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); } @@ -4170,7 +4170,7 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { // 进行线性探测 return getEntryAfterMiss(key, i, e); } - + // 线性探测寻址 private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) { // 获取散列表 Entry[] tab = table; @@ -4193,7 +4193,8 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { // 获取下一个槽位中的 entry e = tab[i]; } - // 说明当前区段没有找到相应数据,因为存放数据是线性的向后寻找槽位,所以不可能越过一个 空槽位 在后面存放 + // 说明当前区段没有找到相应数据 + // 【因为存放数据是线性的向后寻找槽位,都是紧挨着的,不可能越过一个 空槽位 在后面放】 return null; } ``` @@ -4202,10 +4203,10 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { ```java private void rehash() { - // 清楚当前散列表内的所有过期的数据 + // 清楚当前散列表内的【所有】过期的数据 expungeStaleEntries(); - //threshold = len * 2 / 3; + // threshold = len * 2 / 3,就是 2/3*(1 - 1/4) if (size >= threshold - threshold / 4) resize(); } @@ -4224,7 +4225,7 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { } ``` - Entry 数组为扩容为原来的 2 倍 ,重新计算 key 的散列值,如果遇到 key 为 null 的情况,会将其 value 也置为 null,帮助 JVM GC + Entry **数组为扩容为原来的 2 倍** ,重新计算 key 的散列值,如果遇到 key 为 null 的情况,会将其 value 也置为 null,帮助 GC ```java private void resize() { @@ -4248,7 +4249,7 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { } else { // 非过期数据,在新表中进行哈希寻址 int h = k.threadLocalHashCode & (newLen - 1); - // 线程探测 + // 【线程探测】 while (newTab[h] != null) h = nextIndex(h, newLen); // 将数据存放到新表合适的 slot 中 @@ -4273,7 +4274,7 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { ##### 清理方法 -* 探测式清理:沿着开始位置向后探测清理过期数据,沿途中碰到未过期数据则将此数据 rehash 在 table 数组中的定位,重定位后的元素理论上更接近 `i = entry.key & (table.length - 1)`,会优化整个散列表查询性能 +* 探测式清理:沿着开始位置向后探测清理过期数据,沿途中碰到未过期数据则将此数据 rehash 在 table 数组中的定位,重定位后的元素理论上更接近 `i = entry.key & (table.length - 1)`,让数据的排列更紧凑,会优化整个散列表查询性能 ```java // table[staleSlot] 是一个过期数据,以这个位置开始继续向后查找过期数据 @@ -4326,7 +4327,7 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { * 启发式清理: ```java - // i 表示启发式清理工作开始位置,n 一般传递的是 table.length + // i 表示启发式清理工作开始位置,一般是空 slot,n 一般传递的是 table.length private boolean cleanSomeSlots(int i, int n) { // 表示启发式清理工作是否清除了过期数据 boolean removed = false; @@ -4334,13 +4335,12 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { Entry[] tab = table; int len = tab.length; do { - // i 是 null,探测式返回的 slot 为 null 的位置 - // 获取下一个索引 + // 获取下一个索引,因为探测式返回的 slot 为 null i = nextIndex(i, len); Entry e = tab[i]; // 条件成立说明是过期的数据,key 被 gc 了 if (e != null && e.get() == null) { - // 发现过期数据重置 n 为数组的长度 + // 【发现过期数据重置 n 为数组的长度】 n = len; // 表示清理过过期数据 removed = true; @@ -4349,16 +4349,16 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { } // 假设 table 长度为 16 // 16 >>> 1 ==> 8,8 >>> 1 ==> 4,4 >>> 1 ==> 2,2 >>> 1 ==> 1,1 >>> 1 ==> 0 - // 连续经过这么多次循环【没有扫描到过期数据】,就停止循环 + // 连续经过这么多次循环【没有扫描到过期数据】,就停止循环,扫描到 空slot 不算,因为不是过期数据 } while ((n >>>= 1) != 0); // 返回清除标记 return removed; } ``` - + 参考视频:https://space.bilibili.com/457326371/ @@ -4390,7 +4390,7 @@ Memory leak:内存泄漏是指程序中动态分配的堆内存由于某种原 根本原因:ThreadLocalMap 是 Thread的一个属性,生命周期跟 Thread 一样长,如果没有手动删除对应 Entry 就会导致内存泄漏 -解决方法:使用完 ThreadLocal 中存储的内容后将它 **remove** 掉就可以 +解决方法:使用完 ThreadLocal 中存储的内容后将它 remove 掉就可以 ThreadLocal 内部解决方法:在 ThreadLocalMap 中的 set/getEntry 方法中,会对 key 进行判断,如果为 null(ThreadLocal 为 null)的话,会对 Entry 进行垃圾回收。所以**使用弱引用比强引用多一层保障**,就算不调用 remove,也有机会进行 GC @@ -4458,6 +4458,7 @@ private void init(ThreadGroup g, Runnable target, String name, long stackSize, A } // .. } +// 【本质上还是创建 ThreadLocalMap,只是把父类中的可继承数据设置进去了】 static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) { return new ThreadLocalMap(parentMap); } @@ -7852,7 +7853,7 @@ AbstractQueuedSynchronizer 中 state 设计: static final Node SHARED = new Node(); // 枚举:独占模式 static final Node EXCLUSIVE = null; - // node需要构建成 FIFO 队列,prev 指向前继节点 + // node 需要构建成 FIFO 队列,prev 指向前继节点 volatile Node prev; // next 指向后继节点 volatile Node next; @@ -8356,17 +8357,17 @@ Thread-0 释放锁,进入 release 流程 compareAndSetWaitStatus(node, ws, 0); // 找到需要 unpark 的节点,当前节点的下一个 Node s = node.next; - // 不考虑已取消的节点 + // 已取消的节点不能唤醒,需要找到距离头节点最近的非取消的节点 if (s == null || s.waitStatus > 0) { s = null; - // AQS 队列从后至前找需要 unpark 的节点,直到 t == 当前的 node 为止 + // AQS 队列从后至前找需要 unpark 的节点,直到 t == 当前的 node 为止,找不到就不唤醒了 for (Node t = tail; t != null && t != node; t = t.prev) // 说明当前线程状态需要被唤醒 if (t.waitStatus <= 0) // 置换引用 s = t; } - // 找到合适的可以被唤醒的 node,则唤醒线程 + // 【找到合适的可以被唤醒的 node,则唤醒线程】 if (s != null) LockSupport.unpark(s.thread); } @@ -8767,6 +8768,7 @@ public static void main(String[] args) { if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold) LockSupport.parkNanos(this, nanosTimeout); + // 被打断会报异常 if (Thread.interrupted()) throw new InterruptedException(); } @@ -8853,7 +8855,7 @@ Condition 类 API: * await 执行后,会释放锁进入 conditionObject 等待 * await 的线程被唤醒去重新竞争 lock 锁 -* 线程在条件队列被打断会抛出中断异常 +* **线程在条件队列被打断会抛出中断异常** * 竞争 lock 锁成功后,从 await 后继续执行 @@ -8909,7 +8911,7 @@ public static void main(String[] args) throws InterruptedException { throw new InterruptedException(); // 将调用 await 的线程包装成 Node 添加到条件队列并返回 Node node = addConditionWaiter(); - // 完全释放节点持有的锁,因为其他线程唤醒当前线程的前提是 持有锁 + // 完全释放节点持有的锁,因为其他线程唤醒当前线程的前提是【持有锁】 int savedState = fullyRelease(node); // 设置打断模式为没有被打断,状态码为 0 @@ -8928,7 +8930,7 @@ public static void main(String[] args) throws InterruptedException { if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT; - // node在条件队列时 如果被外部线程中断唤醒,会加入到阻塞队列,但是并未设nextWaiter = null + // node 在条件队列时 如果被外部线程中断唤醒,会加入到阻塞队列,但是并未设 nextWaiter = null if (node.nextWaiter != null) // 清理条件队列内所有已取消的 Node unlinkCancelledWaiters(); @@ -8954,7 +8956,7 @@ public static void main(String[] args) throws InterruptedException { private Node addConditionWaiter() { // 获取当前条件队列的尾节点的引用,保存到局部变量 t 中 Node t = lastWaiter; - // 当前队列中不是空,并且节点的状态不是CONDITION(-2),说明当前节点发生了中断 + // 当前队列中不是空,并且节点的状态不是 CONDITION(-2),说明当前节点发生了中断 if (t != null && t.waitStatus != Node.CONDITION) { // 清理条件队列内所有已取消的 Node unlinkCancelledWaiters(); @@ -8973,7 +8975,7 @@ public static void main(String[] args) throws InterruptedException { ``` ```java - // 清理条件队列内所有已取消(不是CONDITION)的 node + // 清理条件队列内所有已取消(不是CONDITION)的 node,【链表删除的逻辑】 private void unlinkCancelledWaiters() { // 从头节点开始遍历【FIFO】 Node t = firstWaiter; @@ -8992,7 +8994,7 @@ public static void main(String[] args) throws InterruptedException { // 更新 firstWaiter 指针为下个节点 firstWaiter = next; else - // 让上一个正常节点指向 当前取消节点的 下一个节点,删除非正常的节点 + // 让上一个正常节点指向 当前取消节点的 下一个节点,【删除非正常的节点】 trail.nextWaiter = next; // t 是尾节点了,更新 lastWaiter 指向最后一个正常节点 if (next == null) @@ -9001,7 +9003,7 @@ public static void main(String[] args) throws InterruptedException { // 正常节点赋值给 trail trail = t; } - // 把 t.next 赋值给 t + // 把 t.next 赋值给 t,循环遍历 t = next; } } @@ -9035,26 +9037,27 @@ public static void main(String[] args) throws InterruptedException { } ``` +* fullyRelease 中会 unpark AQS 队列中的下一个节点竞争锁,假设 Thread-1 竞争成功,park 阻塞 Thread-0 + + ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ReentrantLock-条件变量2.png) + * 进入 isOnSyncQueue 逻辑判断节点是否移动到阻塞队列 ```java final boolean isOnSyncQueue(Node node) { - // node 的状态是 CONDITION,signal 方法是先修改状态再迁移,所以前驱节点为空证明还没有迁移 + // node 的状态是 CONDITION,signal 方法是先修改状态再迁移,所以前驱节点为空证明还【没有完成迁移】 if (node.waitStatus == Node.CONDITION || node.prev == null) return false; - // 说明当前节点已经成功入队到阻塞队列,条件队列的 next 指针为 null,且当前节点后面已经有其它 node + // 说明当前节点已经成功入队到阻塞队列,且当前节点后面已经有其它 node,因为条件队列的 next 指针为 null if (node.next != null) return true; - // 阻塞队列的尾巴开始向前遍历查找 node,如果查找到返回 true,查找不到返回 false + // 说明可能在阻塞队列,但是是尾节点 + // 从阻塞队列的尾节点开始向前遍历查找 node,如果查找到返回 true,查找不到返回 false return findNodeFromTail(node); } ``` -* unpark AQS 队列中的下一个节点竞争锁,假设没那么 Thread-1 竞争成功,park 阻塞 Thread-0 - - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ReentrantLock-条件变量2.png) - -* 线程 park 后如果被 unpark 或者被打断,都会进入 checkInterruptWhileWaiting 判断线程是否被打断: +* await 线程 park 后如果被 unpark 或者被打断,都会进入 checkInterruptWhileWaiting 判断线程是否被打断: ```java private int checkInterruptWhileWaiting(Node node) { @@ -9069,7 +9072,7 @@ public static void main(String[] args) throws InterruptedException { final boolean transferAfterCancelledWait(Node node) { // 条件成立说明当前node一定是在条件队列内,因为 signal 迁移节点到阻塞队列时,会将节点的状态修改为 0 if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) { - // 【中断唤醒的 node 也会被加入到阻塞队列中】 + // 把【中断唤醒的 node 加入到阻塞队列中】 enq(node); // 表示是在条件队列内被中断了,设置为 THROW_IE -1 return true; @@ -9117,7 +9120,7 @@ public static void main(String[] args) throws InterruptedException { ```java public final void signal() { - // 判断调用signal方法的线程是否是独占锁持有线程 + // 判断调用 signal 方法的线程是否是独占锁持有线程 if (!isHeldExclusively()) throw new IllegalMonitorStateException(); // 获取条件队列中第一个 Node @@ -9132,13 +9135,14 @@ public static void main(String[] args) throws InterruptedException { // 唤醒 - 将没取消的第一个节点转移至 AQS 队列尾部 private void doSignal(Node first) { do { - // 当前节点是尾节点,所以队列中只有当前一个节点了 + // 成立说明当前节点是尾节点,所以队列中只有当前一个节点了 if ((firstWaiter = first.nextWaiter) == null) lastWaiter = null; first.nextWaiter = null; // 将等待队列中的 Node 转移至 AQS 队列, 不成功且还有节点则继续循环 } while (!transferForSignal(first) && (first = firstWaiter) != null); } + // signalAll() 会调用这个函数,唤醒所有的节点 private void doSignalAll(Node first) { lastWaiter = firstWaiter = null; @@ -9147,6 +9151,7 @@ public static void main(String[] args) throws InterruptedException { first.nextWaiter = null; transferForSignal(first); first = next; + // 唤醒所有的节点,都放到阻塞队列中 } while (first != null); } ``` @@ -9157,16 +9162,18 @@ public static void main(String[] args) throws InterruptedException { // 如果节点状态是取消, 返回 false 表示转移失败, 否则转移成功 final boolean transferForSignal(Node node) { // CAS 修改当前节点的状态,修改为 0,因为当前节点马上要迁移到阻塞队列了 - // 如果状态已经不是 Node.CONDITION, 说明线程被取消(await 释放全部锁失败)或者被中断(可打断 cancelAcquire) + // 如果状态已经不是 CONDITION, 说明线程被取消(await 释放全部锁失败)或者被中断(可打断 cancelAcquire) + // 返回函数调用处继续寻找下一个节点 if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) return false; // 将当前 node 入阻塞队列,p 是当前节点在阻塞队列的前驱节点 + // 所以是 【先改状态,再进行迁移】 Node p = enq(node); int ws = p.waitStatus; // 如果前驱节点被取消或者不能设置状态为 Node.SIGNAL if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) - // unpark 取消阻塞, 让线程竞争锁,重新同步状态 + // unpark 取消阻塞, 让 thread-0 线程竞争锁,重新同步状态 LockSupport.unpark(node.thread); return true; } @@ -9309,6 +9316,9 @@ public static void main(String[] args) { ```java //lock() -> sync.acquire(1); + public void lock() { + sync.acquire(1); + } public final void acquire(int arg) { // 尝试获得写锁,获得写锁失败,将当前线程关联到一个 Node 对象上, 模式为独占模式 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) @@ -9398,7 +9408,7 @@ public static void main(String[] args) { } ``` -* 进入 sync.doAcquireShared(1) 流程,首先也是调用 addWaiter 添加节点,不同之处在于节点被设置为Node.SHARED 模式而非 Node.EXCLUSIVE 模式,注意此时 t2 仍处于活跃状态 +* 进入 sync.doAcquireShared(1) 流程,首先也是调用 addWaiter 添加节点,不同之处在于节点被设置为 Node.SHARED 模式而非 Node.EXCLUSIVE 模式,注意此时 t2 仍处于活跃状态 ```java private void doAcquireShared(int arg) { @@ -9416,6 +9426,7 @@ public static void main(String[] args) { int r = tryAcquireShared(arg); // r >= 0 表示获取成功 if (r >= 0) { + //【这里会设置自己为头节点,唤醒相连的后序的共享节点】 setHeadAndPropagate(node, r); p.next = null; // help GC if (interrupted) @@ -9435,6 +9446,57 @@ public static void main(String[] args) { } ``` + 如果没有成功,在 doAcquireShared 内 for (;;) 循环一次,shouldParkAfterFailedAcquire 内把前驱节点的 waitStatus 改为 -1,再 for (;;) 循环一次尝试 tryAcquireShared,不成功在 parkAndCheckInterrupt() 处 park + + + +* 这种状态下,假设又有 t3 r.lock,t4 w.lock,这期间 t1 仍然持有锁,就变成了下面的样子 + + ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ReentrantReadWriteLock加锁2.png) + + + +*** + + + +##### 解锁原理 + +* t1 w.unlock, 写锁解锁 + + ```java + public void unlock() { + // 释放锁 + sync.release(1); + } + public final boolean release(int arg) { + // 尝试释放锁 + if (tryRelease(arg)) { + Node h = head; + // 头节点不为空并且不是等待状态不是 0,唤醒后继的非取消节点 + if (h != null && h.waitStatus != 0) + unparkSuccessor(h); + return true; + } + return false; + } + protected final boolean tryRelease(int releases) { + if (!isHeldExclusively()) + throw new IllegalMonitorStateException(); + int nextc = getState() - releases; + // 因为可重入的原因, 写锁计数为 0, 才算释放成功 + boolean free = exclusiveCount(nextc) == 0; + if (free) + setExclusiveOwnerThread(null); + setState(nextc); + return free; + } + ``` + +* 唤醒流程 sync.unparkSuccessor,这时 t2 在 doAcquireShared 的 parkAndCheckInterrupt() 处恢复运行,继续循环,执行 tryAcquireShared 成功则让读锁计数加一 + +* 接下来 t2 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点;还会检查下一个节点是否是 shared,如果是则调用 doReleaseShared() 将 head 的状态从 -1 改为 0 并唤醒下一个节点,这时 t3 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行 + ```java private void setHeadAndPropagate(Node node, int propagate) { Node h = head; @@ -9444,9 +9506,9 @@ public static void main(String[] args) { if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { Node s = node.next; - // 如果是最后一个节点或者是等待共享读锁的节点 + // 如果是最后一个节点或者是【等待共享读锁的节点】 if (s == null || s.isShared()) - // 用来唤醒后继节点 + // 唤醒后继节点 doReleaseShared(); } } @@ -9460,6 +9522,7 @@ public static void main(String[] args) { Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; + // SIGNAL 唤醒后继 if (ws == Node.SIGNAL) { // 因为读锁共享,如果其它线程也在释放读锁,那么需要将 waitStatus 先改为 0 // 防止 unparkSuccessor 被多次执行 @@ -9479,43 +9542,6 @@ public static void main(String[] args) { } ``` -* 如果没有成功,在 doAcquireShared 内 for (;;) 循环一次,shouldParkAfterFailedAcquire 内把前驱节点的 waitStatus 改为 -1,再 for (;;) 循环一次尝试 tryAcquireShared,不成功在 parkAndCheckInterrupt() 处 park - - - -* 这种状态下,假设又有 t3 r.lock,t4 w.lock,这期间 t1 仍然持有锁,就变成了下面的样子 - - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ReentrantReadWriteLock加锁2.png) - - - -*** - - - -##### 解锁原理 - -* t1 w.unlock, 调用 sync.tryRelease(1) 成功 - - ```java - // sync.release(1) -> tryRelease(1) - protected final boolean tryRelease(int releases) { - if (!isHeldExclusively()) - throw new IllegalMonitorStateException(); - int nextc = getState() - releases; - // 因为可重入的原因, 写锁计数为 0, 才算释放成功 - boolean free = exclusiveCount(nextc) == 0; - if (free) - setExclusiveOwnerThread(null); - setState(nextc); - return free; - } - ``` - -* 唤醒流程 sync.unparkSuccessor,这时 t2 在 doAcquireShared 的 parkAndCheckInterrupt() 处恢复运行,继续循环,执行 tryAcquireShared 成功则让读锁计数加一 - -* 接下来 t2 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点;还会检查下一个节点是否是 shared,如果是则调用 doReleaseShared() 将 head 的状态从 -1 改为 0 并唤醒下一个节点,这时 t3 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行 - * 下一个节点不是 shared 了,因此不会继续唤醒 t4 所在节点 @@ -10663,7 +10689,7 @@ B站视频解析:https://www.bilibili.com/video/BV1n541177Ea * -1 表示当前 table 正在初始化(有线程在创建 table 数组),当前线程需要自旋等待 - * 其他负数表示当前 map 的 table 数组正在进行扩容,高 16 位表示扩容的标识戳;低 16 位表示 (1 + nThread) 当前参与并发扩容的线程数量 + * 其他负数表示当前 map 的 table 数组正在进行扩容,高 16 位表示扩容的标识戳;低 16 位表示 (1 + nThread) 当前参与并发扩容的线程数量 + 1 sizeCtl = 0,表示创建 table 数组时使用 DEFAULT_CAPACITY 为数组大小 @@ -11156,7 +11182,7 @@ public V put(K key, V value) { } ``` -* addCount():添加计数,代表哈希表中的数据总量 +* addCount():添加计数,**代表哈希表中的数据总量** ```java private final void addCount(long x, int check) { @@ -13149,9 +13175,9 @@ select 调用流程图: 1. 使用 copy_from_user 从用户空间拷贝 fd_set 到内核空间,进程阻塞 2. 注册回调函数 _pollwait 3. 遍历所有 fd,调用其对应的 poll 方法判断当前请求是否准备就绪,对于 socket,这个 poll 方法是 sock_poll,sock_poll 根据情况会调用到 tcp_poll、udp_poll 或者 datagram_poll,以 tcp_poll 为例,其核心实现就是 _pollwait -4. _pollwait 把 **current(调用 select 的进程)**挂到设备的等待队列,不同设备有不同的等待队列,对于 tcp_poll ,其等待队列是 sk → sk_sleep(把进程挂到等待队列中并不代表进程已经睡眠),在设备收到消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时 current 便被唤醒 +4. _pollwait 把 **current(调用 select 的进程)**挂到设备的等待队列,不同设备有不同的等待队列,对于 tcp_poll ,其等待队列是 sk → sk_sleep(把进程挂到等待队列中并不代表进程已经睡眠),在设备收到消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时 current 便被唤醒,进入就绪队列 5. poll 方法返回时会返回一个描述读写操作是否就绪的 mask 掩码,根据这个 mask 掩码给 fd_set 赋值 -6. 如果遍历完所有的 fd,还没有返回**一个**可读写的 mask 掩码,则会调用 schedule_timeout 让 current 进程进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程,如果超过一定的超时时间(schedule_timeout指定),没有其他线程唤醒,则调用 select 的进程会重新被唤醒获得 CPU,进而重新遍历 fd,判断有没有就绪的 fd +6. 如果遍历完所有的 fd,还没有返回一个可读写的 mask 掩码,则会调用 schedule_timeout 让 current 进程进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程,如果超过一定的超时时间(schedule_timeout)没有其他线程唤醒,则调用 select 的进程会重新被唤醒获得 CPU,进而重新遍历 fd,判断有没有就绪的 fd 7. 把 fd_set 从内核空间拷贝到用户空间,阻塞进程继续执行 @@ -14392,7 +14418,7 @@ JVM 直接内存图解: 堆外内存不受 JVM GC 控制,可以使用堆外内存进行通信,防止 GC 后缓冲区位置发生变化的情况 -NIO 使用的 SocketChannel 的源码解析: +NIO 使用的 SocketChannel 也是使用的堆外内存,源码解析: * SocketChannel#write(java.nio.ByteBuffer) → SocketChannelImpl#write(java.nio.ByteBuffer) @@ -14408,16 +14434,16 @@ NIO 使用的 SocketChannel 的源码解析: ```java static int write(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) { - //判断是否是直接内存,是则直接写出,不是则封装到直接内存 + // 判断是否是直接内存,是则直接写出,不是则封装到直接内存 if (var1 instanceof DirectBuffer) { return writeFromNativeBuffer(var0, var1, var2, var4); } else { //.... - //从堆内buffer拷贝到堆外buffer + // 从堆内buffer拷贝到堆外buffer ByteBuffer var8 = Util.getTemporaryDirectBuffer(var7); var8.put(var1); //... - //从堆外写到内核缓冲区 + // 从堆外写到内核缓冲区 int var9 = writeFromNativeBuffer(var0, var8, var2, var4); } } @@ -14507,14 +14533,14 @@ public class Demo1_27 { #### 共享内存 -FileChannel 提供 map 方法返回 MappedByteBuffer 对象,把文件映射到内存,通常情况可以映射整个文件,如果文件比较大,可以进行分段映射,完成映射后对物理内存的操作会被同步到硬盘上 +FileChannel 提供 map 方法返回 MappedByteBuffer 对象,把文件映射到内存,通常情况可以映射整个文件,如果文件比较大,可以进行分段映射,完成映射后对物理内存的操作会被**同步**到硬盘上 FileChannel 中的成员属性: * MapMode.mode:内存映像文件访问的方式,共三种: * `MapMode.READ_ONLY`:只读,试图修改得到的缓冲区将导致抛出异常。 * `MapMode.READ_WRITE`:读/写,对得到的缓冲区的更改最终将写入文件;但该更改对映射到同一文件的其他程序不一定是可见的 - * `MapMode.PRIVATE`:私用,可读可写,但是修改的内容不会写入文件,只是 buffer 自身的改变,称之为 copy on write 写时复制 + * `MapMode.PRIVATE`:私用,可读可写,但是修改的内容不会写入文件,只是 buffer 自身的改变,称之为写时复制 * `public final FileLock lock()`:获取此文件通道的排他锁 @@ -14533,7 +14559,7 @@ MappedByteBuffer 较之 ByteBuffer新增的三个方法 public class MappedByteBufferTest { public static void main(String[] args) throws Exception { // 读写模式 - RandomAccessFile ra = (RandomAccess) new RandomAccessFile("1.txt", "rw"); + RandomAccessFile ra = new RandomAccessFile("1.txt", "rw"); //获取对应的通道 FileChannel channel = ra.getChannel(); diff --git a/SSM.md b/SSM.md index d655f49..55767d7 100644 --- a/SSM.md +++ b/SSM.md @@ -7163,7 +7163,7 @@ public void addAccount{} * 不推荐在接口上使用 `@Transactional` 注解 - 原因:在接口上使用注解,只有**在使用基于接口的代理时才会生效**,因为**注解是不能继承的**,这就意味着如果正在使用基于类的代理时,那么事务的设置将不能被基于类的代理所识别 + 原因:在接口上使用注解,**只有在使用基于接口的代理时才会生效,因为注解是不能继承的**,这就意味着如果正在使用基于类的代理时,那么事务的设置将不能被基于类的代理所识别 * 正确的设置 `@Transactional` 的 rollbackFor 和 propagation 属性,否则事务可能会回滚失败 From 196cdf7d3d22ceccbf6183f32b692e05f3270fca Mon Sep 17 00:00:00 2001 From: Seazean Date: Tue, 24 Aug 2021 23:01:39 +0800 Subject: [PATCH 036/168] Update Java Notes --- DB.md | 759 +++++++++++++++++++++++++++++++++----------------------- Java.md | 24 +- 2 files changed, 454 insertions(+), 329 deletions(-) diff --git a/DB.md b/DB.md index df0861e..5701429 100644 --- a/DB.md +++ b/DB.md @@ -34,7 +34,9 @@ -参考视频:https://www.bilibili.com/video/BV1zJ411M7TB(推荐观看) +参考视频:https://www.bilibili.com/video/BV1zJ411M7TB + +参考文章:https://time.geekbang.org/column/intro/139 @@ -336,27 +338,29 @@ mysqlshow -uroot -p1234 test book --count ## 体系结构 +### 整体架构 + 体系结构详解: * 第一层:网络连接层 * 一些客户端和链接服务,包含本地 socket 通信和大多数基于客户端/服务端工具实现的 TCP/IP 通信,主要完成一些类似于连接处理、授权认证、及相关的安全方案 - * 在该层上引入了线程池 Connection Pool 的概念,管理缓冲用户连接,线程处理等需要缓存的需求 + * 在该层上引入了连接池 Connection Pool 的概念,管理缓冲用户连接,线程处理等需要缓存的需求 * 在该层上实现基于 SSL 的安全链接,服务器也会为安全接入的每个客户端验证它所具有的操作权限 - 第二层:核心服务层 - * 完成大多数核心服务功能,如 SQL接口,并完成缓存的查询,SQL的分析和优化: + * 查询缓存、分析器、优化器、执行器等,涵盖 MySQL 的大多数核心服务功能,所有的内置函数(日期、数学、加密函数等) * Management Serveices & Utilities:系统管理和控制工具,备份、安全、复制、集群等 * SQL Interface:接受用户的 SQL 命令,并且返回用户需要查询的结果 - * Parser:SQL 语句解析器 - * Optimizer:查询优化器,SQL 语句在查询之前会使用查询优化器进行优化,优化客户端查找请求,根据客户端请求的 query 语句和数据库中的一些统计信息进行分析,得出一个最优策略 - * Caches & Buffers:查询缓存,服务器会查询内部的缓存,如果缓存空间足够大,可以在大量读操作的环境中提升系统的性能 + * Parser:SQL 语句分析器 + * Optimizer:查询优化器 + * Caches & Buffers:查询缓存,服务器会查询内部的缓存,如果缓存空间足够大,可以在大量读操作的环境中提升系统性能 * 所有**跨存储引擎**的功能在这一层实现,如存储过程、触发器、视图等 * 在该层服务器会解析查询并创建相应的内部解析树,并对其完成相应的优化如确定表的查询顺序,是否利用索引等, 最后生成相应的执行操作 * MySQL 中服务器层不管理事务,**事务是由存储引擎实现的** - 第三层:存储引擎层 - - Pluggable Storage Engines:存储引擎接口,MySQL 区别于其他数据库的最重要的特点就是其插件式的表存储引擎(**存储引擎是基于表的,而不是数据库**) - - 存储引擎真正的负责了 MySQL 中数据的存储和提取,服务器通过 API 和存储引擎进行通信 - - 不同的存储引擎具有不同的功能,可以根据开发的需要,来选取合适的存储引擎 + - Pluggable Storage Engines:存储引擎接口,MySQL 区别于其他数据库的重要特点就是其存储引擎的架构模式是插件式的(存储引擎是基于表的,而不是数据库) + - 存储引擎**真正的负责了 MySQL 中数据的存储和提取**,服务器通过 API 和存储引擎进行通信 + - 不同的存储引擎具有不同的功能,共用一个 Server 层,可以根据开发的需要,来选取合适的存储引擎 - 第四层:系统文件层 - 数据存储层,主要是将数据存储在文件系统之上,并完成与存储引擎的交互 - File System:文件系统,保存配置文件、数据文件、日志文件、错误文件、二进制文件等 @@ -365,6 +369,218 @@ mysqlshow -uroot -p1234 test book --count +*** + + + +### 执行流程 + +#### 连接器 + +池化技术:对于访问数据库来说,建立连接的代价是比较昂贵的,频繁的创建关闭连接比较耗费资源,有必要建立数据库连接池,以提高访问的性能 + +首先连接到数据库上,这时连接器发挥作用,连接完成后如果没有后续的动作,这个连接就处于空闲状态,通过指令查看连接状态: + +SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 SQL 的执行情况,其中的 Command 列显示为 Sleep 的这一行,就表示现在系统里面有一个空闲连接 + +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SHOW PROCESSLIST命令.png) + +| 参数 | 含义 | +| ------- | ------------------------------------------------------------ | +| ID | 用户登录 mysql 时系统分配的 connection_id,可以使用函数 connection_id() 查看 | +| User | 显示当前用户,如果不是 root,这个命令就只显示用户权限范围的 sql 语句 | +| Host | 显示这个语句是从哪个 ip 的哪个端口上发的,可以用来跟踪出现问题语句的用户 | +| db | 显示这个进程目前连接的是哪个数据库 | +| Command | 显示当前连接的执行的命令,一般取值为休眠 Sleep、查询 Query、连接 Connect 等 | +| Time | 显示这个状态持续的时间,单位是秒 | +| State | 显示使用当前连接的 sql 语句的状态,以查询为例,需要经过 copying to tmp table、sorting result、sending data等状态才可以完成 | +| Info | 显示执行的 sql 语句,是判断问题语句的一个重要依据 | + +连接建立 TCP 以后需要做权限验证,验证成功后可以进行执行 SQL。如果这时管理员账号对这个用户的权限做了修改,也不会影响已经存在连接的权限,只有再新建的连接才会使用新的权限设置 + +客户端如果太长时间没动静,连接器就会自动断开,这个时间是由参数 wait_timeout 控制的,默认值是 8 小时。如果在连接被断开之后,客户端再次发送请求的话,就会收到一个错误提醒:`Lost connection to MySQL server during query` + +数据库里面,长连接是指连接成功后,如果客户端持续有请求,则一直使用同一个连接;短连接则是指每次执行完很少的几次查询就断开连接,下次查询再重新建立一个。为了减少连接的创建,推荐使用长连接,但是过多的长连接会造成 OOM,解决方案: + +* 定期断开长连接,使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开连接,之后要查询再重连。 +* MySQL 5.7 版本,可以在每次执行一个比较大的操作后,通过执行 mysql_reset_connection 来重新初始化连接资源,这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态 + + + + + +*** + + + +#### 查询缓存 + +##### 工作流程 + +当执行完全相同的 SQL 语句的时候,服务器就会直接从缓存中读取结果,当数据被修改,之前的缓存会失效,修改比较频繁的表不适合做查询缓存 + +查询过程: + +1. 客户端发送一条查询给服务器 +2. 服务器先会检查查询缓存,如果命中了缓存,则立即返回存储在缓存中的结果(一般是 K-V 键值对),否则进入下一阶段 +3. 服务器端进行 SQL 分析,再由优化器生成对应的执行计划 +4. MySQL 根据优化器生成的执行计划,调用存储引擎的 API 来执行查询 +5. 将结果返回给客户端 + +大多数情况下不建议使用查询缓存,因为查询缓存往往弊大于利 + +* 查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。因此很可能费力地把结果存起来,还没使用就被一个更新全清空了,对于更新压力大的数据库来说,查询缓存的命中率会非常低 +* 除非业务就是有一张静态表,很长时间才会更新一次,比如一个系统配置表,那这张表上的查询才适合使用查询缓存 + + + +*** + + + +##### 缓存配置 + +1. 查看当前的 MySQL 数据库是否支持查询缓存: + + ```mysql + SHOW VARIABLES LIKE 'have_query_cache'; -- YES + ``` + +2. 查看当前MySQL是否开启了查询缓存: + + ```mysql + SHOW VARIABLES LIKE 'query_cache_type'; -- OFF + ``` + + 参数说明: + + * OFF 或 0:查询缓存功能关闭 + + * ON 或 1:查询缓存功能打开,查询结果符合缓存条件即会缓存,否则不予缓存;可以显式指定 SQL_NO_CACHE 不予缓存 + + * DEMAND 或 2:查询缓存功能按需进行,显式指定 SQL_CACHE 的 SELECT 语句才缓存,其它不予缓存 + + ```mysql + SELECT SQL_CACHE id, name FROM customer; -- SQL_CACHE:查询结果可缓存 + SELECT SQL_NO_CACHE id, name FROM customer;-- SQL_NO_CACHE:不使用查询缓存 + ``` + +3. 查看查询缓存的占用大小: + + ```mysql + SHOW VARIABLES LIKE 'query_cache_size';-- 单位是字节 1048576 / 1024 = 1024 = 1KB + ``` + +4. 查看查询缓存的状态变量: + + ```mysql + SHOW STATUS LIKE 'Qcache%'; + ``` + + + + | 参数 | 含义 | + | ----------------------- | ------------------------------------------------------------ | + | Qcache_free_blocks | 查询缓存中的可用内存块数 | + | Qcache_free_memory | 查询缓存的可用内存量 | + | Qcache_hits | 查询缓存命中数 | + | Qcache_inserts | 添加到查询缓存的查询数 | + | Qcache_lowmen_prunes | 由于内存不足而从查询缓存中删除的查询数 | + | Qcache_not_cached | 非缓存查询的数量(由于 query_cache_type 设置而无法缓存或未缓存) | + | Qcache_queries_in_cache | 查询缓存中注册的查询数 | + | Qcache_total_blocks | 查询缓存中的块总数 | + +5. 配置 my.cnf: + + ```sh + sudo chmod 666 /etc/mysql/my.cnf + vim my.cnf + # mysqld中配置缓存 + query_cache_type=1 + ``` + + 重启服务既可生效,执行 SQL 语句进行验证 ,执行一条比较耗时的 SQL 语句,然后再多执行几次,查看后面几次的执行时间;获取通过查看查询缓存的缓存命中数,来判定是否走查询缓存 + + + +*** + + + +##### 缓存失效 + +查询缓存失效的情况: + +* SQL 语句不一致的情况,要想命中查询缓存,查询的 SQL 语句必须一致 + + ```mysql + select count(*) from tb_item; + Select count(*) from tb_item; -- 不走缓存,首字母不一致 + ``` + +* 当查询语句中有一些不确定查询时,则不会缓存,比如:now()、current_date()、curdate()、curtime()、rand()、uuid()、user()、database() + + ```mysql + SELECT * FROM tb_item WHERE updatetime < NOW() LIMIT 1; + SELECT USER(); + SELECT DATABASE(); + ``` + +* 不使用任何表查询语句: + + ```mysql + SELECT 'A'; + ``` + +* 查询 mysql、information_schema、performance_schema 等系统表时,不走查询缓存: + + ```mysql + SELECT * FROM information_schema.engines; + ``` + +* 在存储过程、触发器或存储函数的主体内执行的查询,缓存失效 + +* 如果表更改,则使用该表的所有高速缓存查询都将变为无效并从高速缓存中删除,包括使用 MERGE 映射到已更改表的表的查询,比如:INSERT、UPDATE、DELETE、ALTER TABLE、DROP TABLE、DROP DATABASE + + + +*** + + + +#### 分析器 + +没有命中查询缓存,就开始了 SQL 的真正执行,分析器会对 SQL 语句做解析 + +```sql +select * from t where id = 1; +``` + +* 先做**词法分析**,输入的是由多个字符串和空格组成的一条 SQL 语句,MySQL 需要识别出里面的字符串分别是什么,代表什么。从输入的 select 这个关键字识别出来这是一个查询语句;把字符串 t 识别成 表名 T,把字符串 id 识别成列 id +* 然后做**语法分析**,根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法。如果你的语句不对,就会收到 `You have an error in your SQL syntax` 的错误提醒 + + + +*** + + + +#### 优化器 + +优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。 + +优化器阶段完成后,这个语句的执行方案就确定下来了,然后进入执行器阶段。 + + + +*** + + + +#### 执行器 + +开始执行的时候,要先判断一下当前连接对表有没有执行查询的权限,如果没有就会返回没有权限的错误,在工程实现上,如果命中查询缓存,会在查询缓存返回结果的时候,做权限验证。如果有权限,就打开表继续执行,执行器就会根据表的引擎定义,去使用这个引擎提供的接口 + *** @@ -407,7 +623,7 @@ mysqlshow -uroot -p1234 test book --count - 用来定义数据库的访问权限和安全级别,及创建用户。关键字:grant, revoke等 - ![](https://gitee.com/seazean/images/raw/master/Java/SQL分类.png) + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SQL分类.png) @@ -2123,7 +2339,7 @@ InnoDB 存储引擎提供了两种事务日志:redo log(重做日志)和 u undo log 属于逻辑日志,根据每行操作进行记录,记录了 SQL 执行相关的信息,用来回滚行记录到某个版本 -当事务对数据库进行修改时,InnoDB 会生成对应的 undo log,如果事务执行失败或调用了 rollback 导致事务回滚,InnoDB 会根据 undo log 的内容**做与之前相反的操作**: +当事务对数据库进行修改时,InnoDB 会先记录对应的 undo log,如果事务执行失败或调用了 rollback 导致事务回滚,InnoDB 会根据 undo log 的内容**做与之前相反的操作**: * 对于每个 insert,回滚时会执行 delete @@ -2140,6 +2356,10 @@ rollback segment 称为回滚段,每个回滚段中有 1024 个 undo log segme +参考文章:https://www.cnblogs.com/kismetv/p/10331633.html + + + *** @@ -2189,8 +2409,6 @@ rollback segment 称为回滚段,每个回滚段中有 1024 个 undo log segme 持久性是指一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,接下来的其他操作或故障不应该对其有任何影响。 -redo log,记录**数据页的物理修改**,而不是某一行或某几行的修改,用来恢复提交后的物理数据页,且只能恢复到最后一次提交的位置 - InnoDB 作为存储引擎,数据是存放在磁盘中,每次读写数据都需要磁盘 IO,效率会很低。InnoDB 提供了缓存 Buffer Pool,Buffer Pool 中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲: * 从数据库读取数据时,会首先从缓存中读取,如果缓存中没有,则从磁盘读取后放入 Buffer Pool @@ -2199,44 +2417,62 @@ InnoDB 作为存储引擎,数据是存放在磁盘中,每次读写数据都 Buffer Pool 的使用提高了读写数据的效率,但是也带了新的问题:如果 MySQL 宕机,此时 Buffer Pool 中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证,所以引入 redo log * 当数据修改时,除了修改 Buffer Pool 中的数据,还会在 redo log 记录这次操作 -* 当事务提交时,会调用 fsync 接口对 redo log 进行刷盘 - * 如果 MySQL 宕机,重启时可以读取 redo log 中的数据,对数据库进行恢复 -redo log 采用的是 WAL(Write-ahead logging,**预写式日志**),所有修改要先写入日志,再更新到 Buffer Pool,保证了数据不会因 MySQL 宕机而丢失,从而满足了持久性要求 +redo log **记录数据页的物理修改**,而不是某一行或某几行的修改,用来恢复提交后的物理数据页,且只能恢复到最后一次提交的位置 + +redo log 采用的是 WAL(Write-ahead logging,**预写式日志**),所有修改要先写入日志,再更新到磁盘,保证了数据不会因 MySQL 宕机而丢失,从而满足了持久性要求,InnoDB 引擎会在适当的时候,将这个操作记录更新到磁盘里面 -redo log 也需要**在事务提交时**将日志写入磁盘,但是比将 Buffer Pool 中修改的数据写入磁盘快: +redo log 也需要在事务提交时将日志写入磁盘,但是比将 Buffer Pool 中修改的数据写入磁盘快: * 刷脏是随机 IO,因为每次修改的数据位置随机,但写 redo log 是尾部追加操作,属于顺序 IO * 刷脏是以数据页 (Page) 为单位的,MySQL 默认页大小是 16KB,一个页上的一个小修改都要整页写入,而 redo log 中只包含真正需要写入的部分,无效 IO 大大减少 -刷盘策略,通过修改参数 `innodb_flush_log_at_trx_commit` 设置: +刷盘策略,就是把内存缓冲区中 redo log 日志持久化到磁盘,通过修改参数 `innodb_flush_log_at_trx_commit` 设置: * 0:表示当提交事务时,并不将缓冲区的 redo 日志写入磁盘,而是等待主线程每秒刷新一次 * 1:在事务提交时将缓冲区的 redo 日志**同步写入**到磁盘,保证一定会写入成功 * 2:在事务提交时将缓冲区的 redo 日志异步写入到磁盘,不能保证提交时肯定会写入,只是有这个动作 +InnoDB 的 redo log 是固定大小的,如果写满了就要擦除以前的记录,在擦除之前需要把旧记录更新到数据文件中 + *** -##### 日志对比 +##### 工作流程 -MySQL中还存在 binlog(二进制日志) 也可以记录写操作并用于数据的恢复,二者的区别是: +MySQL中还存在 binlog(二进制日志)也可以记录写操作并用于数据的恢复,二者的区别是: -* 作用不同:redo log 是用于 crash recovery (故障恢复),保证MySQL宕机也不会影响持久性;binlog 是用于 point-in-time recovery 的,保证服务器可以基于时间点恢复数据,此外 binlog 还用于主从复制 +* 作用不同:redo log 是用于 crash recovery (故障恢复),保证 MySQL 宕机也不会影响持久性;binlog 是用于 point-in-time recovery 的,保证服务器可以基于时间点恢复数据,此外 binlog 还用于主从复制 -* 层次不同:redo log 是 InnoDB 存储引擎实现的,而 binlog 是MySQL的服务器层实现的,同时支持 InnoDB 和其他存储引擎,并且二进制日志先于 redo log 被记录。 +* 层次不同:redo log 是 InnoDB 存储引擎实现的,而 binlog 是MySQL的服务器层实现的,同时支持 InnoDB 和其他存储引擎 * 内容不同:redo log 是物理日志,内容基于磁盘的 Page;binlog 的内容是二进制的,根据 binlog_format 参数的不同,可能基于SQL 语句、基于数据本身或者二者的混合(日志部分详解) * 写入时机不同:binlog 在事务提交时一次写入;redo log 的写入时机相对多元 +两种日志在 update 更新数据的**作用时机**: +```sql +update T set c=c+1 where ID=2; +``` -参考文章:https://www.cnblogs.com/kismetv/p/10331633.html + + +流程说明:执行引擎将这行新数据更新到内存中(Buffer Pool)后,然后会将这个更新操作记录到 redo log buffer 里,此时 redo log 处于 prepare 状态,代表执行完成随时可以提交事务,然后执行器生成这个操作的 binlog,并**把 binlog 写入磁盘**,在提交事务后 **redo log 也持久化到磁盘** + +redo log 和 binlog 都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致,在恢复数据时 + +* 如果 redolog 状态为 commit,则说明 binlog 也成功,直接恢复数据 + +* 如果 redolog 是 prepare,则需要查询对应的 binlog 事务是否成功,决定是回滚还是提交 + + + +参考文章:https://time.geekbang.org/column/article/68633 @@ -2250,12 +2486,12 @@ MySQL中还存在 binlog(二进制日志) 也可以记录写操作并用于数 隔离级别分类: -| 隔离级别 | 名称 | 会引发的问题 | 数据库默认隔离级别 | -| ---------------- | -------- | ---------------------- | ------------------- | -| read uncommitted | 读未提交 | 脏读、不可重复读、幻读 | | -| read committed | 读已提交 | 不可重复读、幻读 | Oracle / SQL Server | -| repeatable read | 可重复读 | 幻读 | MySQL | -| serializable | 串行化 | 无 | | +| 隔离级别 | 名称 | 会引发的问题 | 数据库默认隔离级别 | +| ---------------- | -------- | -------------------------------- | ------------------- | +| read uncommitted | 读未提交 | 脏读、不可重复读、幻读 | | +| read committed | 读已提交 | 不可重复读、幻读 | Oracle / SQL Server | +| repeatable read | 可重复读 | 幻读 | MySQL | +| serializable | 串行化 | 无(因为写会加写锁,读会加读锁) | | 一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差 @@ -2359,7 +2595,7 @@ MVCC 的优点: ##### undo -undo log 是逻辑日志,保存修改行的数据的拷贝副本 +undo log 是逻辑日志,记录的是每个事务对数据执行的操作,而不是记录的全部数据,需要根据 undo log 逆推出以往事务的数据 undo log 的作用: @@ -2378,12 +2614,13 @@ undo log 主要分为两种: * 有个事务插入 persion 表一条新记录,name 为 Jerry,age 为 24 -* 事务 1 修改该行数据时,数据库会先对该行加排他锁,然后先把该行数据拷贝到 undo log 中作为旧记录,拷贝完毕后修改该行 name 为 Tom,并且修改隐藏字段的事务 ID 为当前事务 1 的 ID(默认为 1 之后递增),回滚指针指向拷贝到 undo log 的副本记录,事务提交后,释放锁 +* 事务 1 修改该行数据时,数据库会先对该行加排他锁,然后先记录 undo log,然后修改该行 name 为 Tom,并且修改隐藏字段的事务 ID 为当前事务 1 的 ID(默认为 1 之后递增),回滚指针指向拷贝到 undo log 的副本记录,事务提交后,释放锁 * 以此类推 -purge 线程: +补充知识:purge 线程 -为了实现 InnoDB 的 MVCC 机制,更新或者删除操作都只是设置一下老记录的 deleted_bit,并不真正将过时的记录删除,为了节省磁盘空间,InnoDB 有专门的 purge 线程来清理 deleted_bit 为 true 的记录,purge 线程维护了一个 Read view(这个 Read view 相当于系统中最老活跃事务的 Read view),如果某个记录的 deleted_bit 为 true,并且 DB_TRX_ID 相对于 purge 线程的 Read view 可见,那么这条记录一定是可以被安全清除的 +* 为了实现 InnoDB 的 MVCC 机制,更新或者删除操作都只是设置一下老记录的 deleted_bit,并不真正将过时的记录删除,为了节省磁盘空间,InnoDB 有专门的 purge 线程来清理 deleted_bit 为 true 的记录 +* purge 线程维护了一个 Read view(这个 Read view 相当于系统中最老活跃事务的 Read view),如果某个记录的 deleted_bit 为 true,并且 DB_TRX_ID 相对于 purge 线程的 Read view 可见,那么这条记录一定是可以被安全清除的 @@ -2393,26 +2630,28 @@ purge 线程: ##### 读视图 -Read View 是事务进行快照读操作时产生的读视图,该事务执行快照读的那一刻会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的 ID,用来做可见性判断,根据视图判断当前事务能够看到哪个版本的数据 +Read View 是事务进行**快照读**操作时产生的读视图,该事务执行快照读的那一刻会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的 ID,用来做可见性判断,根据视图判断当前事务能够看到哪个版本的数据 + +注意:这里的快照并不是把所有的数据拷贝一份副本,而是由 undo log 记录的逻辑日志 -工作流程:将版本链的头节点的事务ID(最新数据事务ID)DB_TRX_ID 取出来,与系统当前活跃事务的 ID 对比,如果 DB_TRX_ID 不符合可见性,通过 DB_ROLL_PTR 回滚指针去取出 undo log 中的下一个 DB_TRX_ID 比较,直到找到最近的满足特定条件的 DB_TRX_ID,该事务 ID 所在的旧记录就是当前事务能看见的最新的记录 +工作流程:将版本链的头节点的事务 ID(最新数据事务 ID)DB_TRX_ID 取出来,与系统当前活跃事务的 ID 对比,如果 DB_TRX_ID 不符合可见性,通过 DB_ROLL_PTR 回滚指针去取出 undo log 中的下一个 DB_TRX_ID 比较,直到找到最近的满足特定条件的 DB_TRX_ID,该事务 ID 所在的旧记录就是当前事务能看见的最新的记录 Read View 几个属性: -- m_ids:生成 Read View 时当前系统中活跃的事务 id 列表 -- up_limit_id:生成 Read View 时当前系统中活跃的最小的事务 id,也就是 m_ids 中的最小值 -- low_limit_id:生成 Read View 时系统应该分配给下一个事务的 id 值,m_ids 中的最大值加 1 +- m_ids:生成 Read View 时当前系统中活跃的事务 id 列表(未提交的事务集合,当前事务也在其中) +- up_limit_id:生成 Read View 时当前系统中活跃的最小的事务 id,也就是 m_ids 中的最小值(小于该值的都是已提交的事务) +- low_limit_id:生成 Read View 时系统应该分配给下一个事务的 id 值,m_ids 中的最大值加 1(未开始事务) - creator_trx_id:生成该 Read View 的事务的事务 id,就是判断该 id 的事务能读到什么数据 creator 创建一个 Read View,进行可见性算法分析:(解决了读未提交) * db_trx_id == creator_trx_id:表示这个数据就是当前事务自己生成的,自己生成的数据自己肯定能看见,所以这种情况下此数据对 creator 是可见的 -* db_trx_id < up_limit_id:该版本对应的事务 ID 小于 Read view 中的最小活跃事务 ID,则这个事务在当前事务之前就已经被 COMMIT 了,对 creator 可见 +* db_trx_id < up_limit_id:该版本对应的事务 ID 小于 Read view 中的最小活跃事务 ID,则这个事务在当前事务之前就已经被提交了,对 creator 可见 * db_trx_id >= low_limit_id:该版本对应的事务 ID 大于 Read view 中当前系统的最大事务 ID,则说明该数据是在当前 Read view 创建之后才产生的,对 creator 不可见 * up_limit_id <= db_trx_id < low_limit_id:判断 db_trx_id 是否在活跃事务列表 m_ids 中 * 在列表中,说明该版本对应的事务正在运行,数据不能显示(**不能读到未提交的数据**) - * 不在列表中,说明该版本对应的事务已经被提交,数据可以显示 + * 不在列表中,说明该版本对应的事务已经被提交,数据可以显示(**可以读到已经提交的数据**) @@ -2476,11 +2715,6 @@ RR、RC 生成时机: - RC 隔离级别下,每个快照读都会生成并获取最新的 Read View - RR 隔离级别下,则是同一个事务中的第一个快照读才会创建 Read View -解决幻读问题: - -- 快照读:通过 MVCC 来进行控制的,不用加锁,当前事务只生成一次 Read View,外部操作没有影响 -- 当前读:通过 next-key 锁(行锁 + 间隙锁)来解决问题 - RC、RR 级别下的 InnoDB 快照读区别 - RC 级别下的,事务中每次快照读都会新生成一个 Read View,这就是在 RC 级别下的事务中可以看到别的事务提交的更新的原因 @@ -2489,6 +2723,11 @@ RC、RR 级别下的 InnoDB 快照读区别 当前事务在其他事务提交之前使用过快照读,那么以后其他事务对数据的修改都是不可见的,就算以后其他事务提交了数据也不可见;早于 Read View 创建的事务所做的修改并提交的均是可见的 +解决幻读问题: + +- 快照读:通过 MVCC 来进行控制的,不用加锁,当前事务只生成一次 Read View,外部操作没有影响 +- 当前读:通过 next-key 锁(行锁 + 间隙锁)来解决问题 + @@ -3656,6 +3895,8 @@ MERGE存储引擎: MySQL 官方对索引的定义为:索引(index)是帮助 MySQL 高效获取数据的一种数据结构,本质是排好序的快速查找数据结构。在表数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式指向数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。 +**索引是在存储引擎层实现的**,所以并没有统一的索引标准,即不同存储引擎的索引的工作方式并不一样 + 索引使用:一张数据表,用于保存数据;一个索引配置文件,用于保存索引;每个索引都指向了某一个数据 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-索引的介绍.png) @@ -3738,9 +3979,9 @@ MySQL 官方对索引的定义为:索引(index)是帮助 MySQL 高效获 在 Innodb 存储引擎,B+ 树索引可以分为聚簇索引(也称聚集索引、clustered index)和辅助索引(也称非聚簇索引或二级索引、secondary index、non-clustered index) -InnoDB 中,聚簇索引是按照每张表的主键构造一颗 B+ 树,同时叶子节点中存放的就是整张表的行记录数据,也将聚集索引的叶子节点称为数据页 +InnoDB 中,聚簇索引是按照每张表的主键构造一颗 B+ 树,同时叶子节点中存放的就是整张表的行记录数据,也将聚簇索引的叶子节点称为数据页 -* 这个特性决定了数据也是索引的一部分,所以一张表只能有一个聚簇索引 +* 这个特性决定了**数据也是索引的一部分**,所以一张表只能有一个聚簇索引 * 辅助索引的存在不影响聚簇索引中数据的组织,所以一张表可以有多个辅助索引 聚簇索引的优点: @@ -3752,7 +3993,7 @@ InnoDB 中,聚簇索引是按照每张表的主键构造一颗 B+ 树,同时 * 插入速度严重依赖于插入顺序,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能,所以对于 InnoDB 表,一般都会定义一个自增的 ID 列为主键 -* 更新主键的代价很高,将会导致被更新的行移动,所以对于InnoDB表,一般定义主键为不可更新 +* 更新主键的代价很高,将会导致被更新的行移动,所以对于 InnoDB 表,一般定义主键为不可更新 * 二级索引访问需要两次索引查找,第一次找到主键值,第二次根据主键值找到行数据 @@ -3766,7 +4007,7 @@ InnoDB 中,聚簇索引是按照每张表的主键构造一颗 B+ 树,同时 在聚簇索引之上创建的索引称之为辅助索引,非聚簇索引都是辅助索引,像复合索引、前缀索引、唯一索引等 -辅助索引叶子节点存储的是主键值,而不是行的物理地址,所以访问数据需要二次查找,这也是推荐使用覆盖索引的原因,可以减少回表查询 +辅助索引叶子节点存储的是主键值,而不是行的物理地址,所以访问数据需要二次查找,推荐使用覆盖索引,可以减少回表查询 检索过程:辅助索引找到主键值,再通过聚簇索引找到数据页,最后通过数据页中的 Page Directory 找到数据行 @@ -3780,17 +4021,17 @@ InnoDB 中,聚簇索引是按照每张表的主键构造一颗 B+ 树,同时 InnoDB 使用 B+Tree 作为索引结构 -**主键索引:** +主键索引: * 在 InnoDB 中,表数据文件本身就是按 B+Tree 组织的一个索引结构,这个索引的 key 是数据表的主键,叶子节点 data 域保存了完整的数据记录 * Innodb 的表数据文件**通过主键聚集数据**,如果没有定义主键,会选择非空唯一索引代替,如果也没有这样的列,MySQL 会自动为 InnoDB 表生成一个**隐含字段**作为主键,这个字段长度为 6 个字节,类型为长整形(MVCC 部分的笔记提及) -**辅助索引:** +辅助索引: -InnoDB 的所有辅助索引(二级索引)都引用主键作为 data 域 +* InnoDB 的所有辅助索引(二级索引)都引用主键作为 data 域 -InnoDB 表是基于聚簇索引建立的,因此 InnoDB 的索引能提供一种非常快速的主键查找性能。不过辅助索引也会包含主键列,所以不建议使用过长的字段作为主键,过长的主索引会令辅助索引变得过大 +* InnoDB 表是基于聚簇索引建立的,因此 InnoDB 的索引能提供一种非常快速的主键查找性能。不过辅助索引也会包含主键列,所以不建议使用过长的字段作为主键,**过长的主索引会令辅助索引变得过大** ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB聚簇和辅助索引结构.png) @@ -3804,7 +4045,7 @@ InnoDB 表是基于聚簇索引建立的,因此 InnoDB 的索引能提供一 ##### 非聚簇 -MyISAM 的主键索引使用的是非聚簇索引,索引文件和数据文件是分离的,索引文件仅保存数据记录的**地址** +MyISAM 的主键索引使用的是非聚簇索引,索引文件和数据文件是分离的,**索引文件仅保存数据记录的地址** * 主键索引 B+ 树的节点存储了主键,辅助键索引 B+ 树存储了辅助键,表数据存储在独立的地方,这两颗 B+ 树的叶子节点都使用一个地址指向真正的表数据,对于表数据来说,这两个键没有任何差别 * 由于索引树是独立的,通过辅助索引检索无需访问主键的索引树回表查询 @@ -3821,9 +4062,9 @@ MyISAM 的主键索引使用的是非聚簇索引,索引文件和数据文件 MyISAM 的索引方式也叫做非聚集的,之所以这么称呼是为了与 InnoDB 的聚集索引区分 -**主键索引:**MyISAM 引擎使用 B+Tree 作为索引结构,叶节点的data域存放的是数据记录的地址 +主键索引:MyISAM 引擎使用 B+Tree 作为索引结构,叶节点的 data 域存放的是数据记录的地址 -**辅助索引:**MyISAM 中主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求 key 是唯一的,而辅助索引的 key 可以重复 +辅助索引:MyISAM 中主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求 key 是唯一的,而辅助索引的 key 可以重复 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM主键和辅助索引结构.png) @@ -3841,26 +4082,16 @@ MyISAM 的索引方式也叫做非聚集的,之所以这么称呼是为了与 ### 索引结构 -#### 原理 - -索引是在 MySQL 的存储引擎中实现的,不同的存储引擎的所支持的索引不一定完全相同 - -BTree 的索引类型是基于 B+Tree 树型数据结构的,B+Tree 又是 BTree 数据结构的变种,用在数据库和操作系统中的文件系统,特点是能够保持数据稳定有序 +#### BTree 磁盘存储: -* 系统从磁盘读取数据到内存时是以磁盘块(block)为基本单位的,位于同一个磁盘块中的数据会被一次性读取出来,而不是需要什么取什么。 - -- InnoDB 存储引擎中有页(Page)的概念,页是其磁盘管理的最小单位,InnoDB 存储引擎中默认每个页的大小为 16KB。 -- InnoDB 引擎将若干个地址连接磁盘块,以此来达到页的大小16KB,在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘I/O次数,提高查询效率。 - +* 系统从磁盘读取数据到内存时是以磁盘块(block)为基本单位的,位于同一个磁盘块中的数据会被一次性读取出来,而不是需要什么取什么 +- InnoDB 存储引擎中有页(Page)的概念,页是其磁盘管理的最小单位,InnoDB 存储引擎中默认每个页的大小为 16KB +- InnoDB 引擎将若干个地址连接磁盘块,以此来达到页的大小 16KB,在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘 I/O 次数,提高查询效率 -*** - - - -#### BTree +BTree 的索引类型是基于 B+Tree 树型数据结构的,B+Tree 又是 BTree 数据结构的变种,用在数据库和操作系统中的文件系统,特点是能够保持数据稳定有序 BTree 又叫多路平衡搜索树,一颗 m 叉的 BTree 特性如下: @@ -3870,7 +4101,7 @@ BTree 又叫多路平衡搜索树,一颗 m 叉的 BTree 特性如下: - 所有的叶子节点都在同一层 - 每个非叶子节点由 n 个key与 n+1 个指针组成,其中 [ceil(m/2)-1] <= n <= m-1 -5叉,key 的数量 [ceil(m/2)-1] <= n <= m-1 为 2 <= n <=4 ,当 n>4 时中间节点分裂到父节点,两边节点分裂 +5 叉,key 的数量 [ceil(m/2)-1] <= n <= m-1 为 2 <= n <=4 ,当 n>4 时中间节点分裂到父节点,两边节点分裂 插入 C N G A H E K Q M F W L T Z D P R X Y S 数据的工作流程: @@ -3906,7 +4137,7 @@ BTree 又叫多路平衡搜索树,一颗 m 叉的 BTree 特性如下: ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程8.png) -BTREE 树就已经构建完成了,BTREE 树和二叉树相比, 查询数据的效率更高, 因为对于相同的数据量来说,**BTREE 的层级结构比二叉树小**,所以搜索速度快 +BTree 树就已经构建完成了,BTree 树和二叉树相比, 查询数据的效率更高, 因为对于相同的数据量来说,**BTree 的层级结构比二叉树小**,所以搜索速度快 BTree 结构的数据可以让系统高效的找到数据所在的磁盘块,定义一条记录为一个二元组 [key, data] ,key 为记录的键值,对应表中的主键值,data 为一行记录中除主键外的数据。对于不同的记录,key 值互不相同,BTree 中的每个节点根据实际情况可以包含大量的关键字信息和分支 ![](https://gitee.com/seazean/images/raw/master/DB/索引的原理-BTree.png) @@ -3923,13 +4154,15 @@ BTree 结构的数据可以让系统高效的找到数据所在的磁盘块, ##### 数据结构 +BTree 数据结构中每个节点中不仅包含数据的 key 值,还有 data 值。磁盘中每一页的存储空间是有限的,如果 data 数据较大时将会导致每个节点(即一个页)能存储的 key 的数量很小,当存储的数据量很大时同样会导致 B-Tree 的深度较大,增大查询时的磁盘 I/O 次数,进而影响查询效率,所以引入 B+Tree + B+Tree 为 BTree 的变种,B+Tree 与 BTree 的区别为: * n 叉 B+Tree 最多含有 n 个 key(哈希值),而 BTree 最多含有 n-1 个 key -- 所有**非叶子节点只存储键值 key**信息,只进行数据索引,使每个非叶子节点所能保存的关键字大大增加 +- 所有**非叶子节点只存储键值 key** 信息,只进行数据索引,使每个非叶子节点所能保存的关键字大大增加 - 所有**数据都存储在叶子节点**,所以每次数据查询的次数都一样 -- 叶子节点按照 key 大小顺序排列,左边结尾数据都会保存右边节点开始数据的**指针**,形成一个链表 +- **叶子节点按照 key 大小顺序排列,左边结尾数据都会保存右边节点开始数据的指针,形成一个链表** - 所有节点中的 key 在叶子节点中也存在(比如 5),key 允许重复,B 树不同节点不存在重复的 key @@ -3944,8 +4177,6 @@ B* 树:是 B+ 树的变体,在 B+ 树的非根和非叶子结点再增加指 ##### 优化结构 -BTree 数据结构中每个节点中不仅包含数据的 key 值,还有 data 值。每一页的存储空间是有限的,如果 data 数据较大时将会导致每个节点(即一个页)能存储的key的数量很小,当存储的数据量很大时同样会导致 B-Tree 的深度较大,增大查询时的磁盘 I/O 次数,进而影响查询效率 - MySQL 索引数据结构对经典的 B+Tree 进行了优化,在原 B+Tree 的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的 B+Tree,**提高区间访问的性能,防止回旋查找** 区间访问的意思是访问索引为 5 - 15 的数据,可以直接根据相邻节点的指针遍历 @@ -3957,9 +4188,9 @@ MySQL 索引数据结构对经典的 B+Tree 进行了优化,在原 B+Tree 的 - 有范围:对于主键的范围查找和分页查找 - 有顺序:从根节点开始,进行随机查找,顺序查找 -InnoDB 存储引擎中页的大小为 16KB,一般表的主键类型为 INT(4字节)或 BIGINT(8字节),指针类型也一般为4或8个字节,也就是说一个页(B+Tree中的**一个节点**)中大概存储 16KB/(8B+8B)=1K 个键值(估值)。则一个深度为3的 B+Tree 索引可以维护 `10^3 * 10^3 * 10^3 = 10亿` 条记录 +InnoDB 存储引擎中页的大小为 16KB,一般表的主键类型为 INT(4字节)或 BIGINT(8字节),指针类型也一般为 4 或 8 个字节,也就是说一个页(B+Tree 中的**一个节点**)中大概存储 16KB/(8B+8B)=1K 个键值(估值)。则一个深度为 3 的 B+Tree 索引可以维护 `10^3 * 10^3 * 10^3 = 10亿` 条记录 -实际情况中每个节点可能不能填充满,因此在数据库中,B+Tree的高度一般都在2-4层。MySQL 的 InnoDB 存储引擎在设计时是将根节点常驻内存的,也就是说查找某一键值的行记录时最多只需要1~3次磁盘 I/O 操作 +实际情况中每个节点可能不能填充满,因此在数据库中,B+Tree 的高度一般都在 2-4 层。MySQL 的 InnoDB 存储引擎在设计时是**将根节点常驻内存的**,也就是说查找某一键值的行记录时最多只需要 1~3 次磁盘 I/O 操作 B+Tree 优点:提高查询速度,减少磁盘的 IO 次数,树形结构较小 @@ -3969,6 +4200,24 @@ B+Tree 优点:提高查询速度,减少磁盘的 IO 次数,树形结构较 +##### 索引维护 + +B+ 树为了维护索引有序性,在插入新值的时候需要做必要的维护 + +每个索引中每个块存储在磁盘页中,可能会出现以下两种情况: + +* 如果所在的数据页已经满了,根据 B+ 树的算法,这时候需要申请一个新的数据页,然后挪动部分数据过去,这个过程称为页分裂 + +* 当相邻两个页由于删除了数据,利用率很低之后,会将数据页做合并,合并的过程可以认为是分裂过程的逆过程 + +一般选用数据小的字段做索引,字段长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小 + + + +*** + + + ### 索引操作 索引在创建表的时候可以同时创建, 也可以随时增加新的索引 @@ -4048,7 +4297,7 @@ B+Tree 优点:提高查询速度,减少磁盘的 IO 次数,树形结构较 创建索引时的原则: - 对查询频次较高,且数据量比较大的表建立索引 - 使用唯一索引,区分度越高,使用索引的效率越高 -- 索引字段的选择,最佳候选列应当从 where 子句的条件中提取,如果 where 子句中的组合比较多,那么应当挑选最常用、过滤效果最好的列的组合 +- 索引字段的选择,最佳候选列应当从 where 子句的条件中提取,使用覆盖索引 - 使用短索引,索引创建之后也是使用硬盘来存储的,因此提升索引访问的 I/O 效率,也可以提升总体的访问效率。假如构成索引的字段总长度比较短,那么在给定大小的存储块内可以存储更多的索引值,相应的可以有效的提升 MySQL 访问索引的 I/O 效率 - 索引可以有效的提升查询数据的效率,但索引数量不是多多益善,索引越多,维护索引的代价越高。对于插入、更新、删除等 DML 操作比较频繁的表来说,索引过多,会引入相当高的维护代价,降低 DML 操作的效率,增加相应操作的时间消耗;另外索引过多的话,MySQL 也会犯选择困难病,虽然最终仍然会找到一个可用的索引,但提高了选择的代价 @@ -4102,7 +4351,7 @@ B+Tree 优点:提高查询速度,减少磁盘的 IO 次数,树形结构较 SELECT * FROM user WHERE age = 30; ``` - 查询过程:先通过普通索引 age=30 定位到主键值 id=1,再通过聚集索引 id=1 定位到行记录数据,需要两次扫描 B+ 树,这就是回表查询 + 查询过程:先通过普通索引 age=30 定位到主键值 id=1,再通过聚集索引 id=1 定位到行记录数据,需要两次扫描 B+ 树 * 使用覆盖索引: @@ -4124,32 +4373,32 @@ B+Tree 优点:提高查询速度,减少磁盘的 IO 次数,树形结构较 #### 索引下推 -索引条件下推优化(Index Condition Pushdown)是 MySQL5.6 添加,用于优化数据查询,减少回表操作 +索引条件下推优化(Index Condition Pushdown)是 MySQL5.6 添加,可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数 索引下推充分利用了索引中的数据,在查询出整行数据之前过滤掉无效的数据,再去主键索引树上查找 * 不使用索引下推优化时存储引擎通过索引检索到数据返回给 MySQL 服务器,服务器判断数据是否符合条件 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-不使用索引下推.png) -* 使用索引下推优化时,如果存在某些被索引的列的判断条件时,MySQL 服务器将这一部分**判断条件传递给存储引擎**,然后由存储引擎在索引内部判断索引是否符合传递的条件,只有当索引符合条件时才会将数据检索出来返回给 MySQL 服务器,由此减少 IO次数 +* 使用索引下推优化时,如果存在某些被索引的列的判断条件时,MySQL 服务器将这一部分**判断条件传递给存储引擎**,然后由存储引擎在索引内部判断索引是否符合传递的条件,只有当索引符合条件时才会将数据检索出来返回给服务器,由此减少 IO次数 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-使用索引下推.png) -适用条件: +**适用条件**: * 需要存储引擎将索引中的数据与条件进行判断,所以优化是基于存储引擎的,只有特定引擎可以使用,适用于 InnoDB 和 MyISAM * 存储引擎没有调用跨存储引擎的能力,跨存储引擎的功能有存储过程、触发器、视图,所以调用这些功能的不可以进行索引下推优化 * 对于 InnoDB 引擎只适用于二级索引,InnoDB 的聚簇索引会将整行数据读到缓冲区,因为数据已经在内存中了,不再需要去读取了,索引下推的目的减少 IO 次数也就失去了意义 -工作过程:用户表 user,(name,sex) 是联合索引 +工作过程:用户表 user,(name, age) 是联合索引 ```mysql -SELECT * FROM user WHERE name LIKE '王%' AND sex=1; -- 头部模糊匹配会造成索引失效 +SELECT * FROM user WHERE name LIKE '张%' AND age = 10; -- 头部模糊匹配会造成索引失效 ``` * 优化前:在非主键索引树上找到满足第一个条件的行,然后通过叶子节点记录的主键值再回到主键索引树上查找到对应的行数据,再对比 AND 后的条件是否符合,符合返回数据,需要 4 次回表 - + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-索引下推优化1.png) * 优化后:检查索引中存储的列信息是否符合索引条件,然后交由存储引擎用剩余的判断条件判断此行数据是否符合要求,**不满足条件的不去读取表中的数据**,满足下推条件的就根据主键值进行回表查询,2 次回表 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-索引下推优化2.png) @@ -4159,7 +4408,8 @@ SELECT * FROM user WHERE name LIKE '王%' AND sex=1; -- 头部模糊匹配会 参考文章:https://blog.csdn.net/sinat_29774479/article/details/103470244 -参考文章:https://blog.csdn.net/linuxguitu/article/details/113649245 + +参考文章:https://time.geekbang.org/column/article/69636 @@ -4171,7 +4421,7 @@ SELECT * FROM user WHERE name LIKE '王%' AND sex=1; -- 头部模糊匹配会 当要索引的列字符很多时,索引会变大变慢,可以只索引列开始的部分字符串,节约索引空间,提高索引效率 -优化原则:降低重复的索引值 +优化原则:**降低重复的索引值** 比如地区表: @@ -4262,7 +4512,7 @@ Innodb_xxxx:这几个参数只是针对InnoDB 存储引擎的,累加的算 * 慢日志查询: 慢查询日志在查询结束以后才记录,执行效率出现问题时查询日志并不能定位问题 - 配置文件修改:修改.cnf文件`vim /etc/mysql/my.cnf`,重启MySQL服务器 + 配置文件修改:修改 .cnf 文件`vim /etc/mysql/my.cnf`,重启 MySQL 服务器 ```sh slow_query_log=ON @@ -4288,16 +4538,6 @@ Innodb_xxxx:这几个参数只是针对InnoDB 存储引擎的,累加的算 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SHOW PROCESSLIST命令.png) - | 参数 | 含义 | - | ------- | ------------------------------------------------------------ | - | ID | 用户登录mysql时系统分配的"connection_id",可以使用函数 connection_id() 查看 | - | User | 显示当前用户,如果不是 root,这个命令就只显示用户权限范围的 sql 语句 | - | Host | 显示这个语句是从哪个 ip 的哪个端口上发的,可以用来跟踪出现问题语句的用户 | - | db | 显示这个进程目前连接的是哪个数据库 | - | Command | 显示当前连接的执行的命令,一般取值为休眠 Sleep、查询 Query、连接 Connect 等 | - | Time | 显示这个状态持续的时间,单位是秒 | - | State | 显示使用当前连接的 sql 语句的状态,以查询为例,需要经过 copying to tmp table、sorting result、sending data等状态才可以完成 | - | Info | 显示执行的 sql 语句,是判断问题语句的一个重要依据 | @@ -4500,12 +4740,12 @@ key_len: SHOW PROFILES 能够在做 SQL 优化时分析当前会话中语句执行的资源消耗情况 -* 通过 have_profiling 参数,能够看到当前 MySQL 是否支持profile: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL- have_profiling.png) +* 通过 have_profiling 参数,能够看到当前 MySQL 是否支持 profile: + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-have_profiling.png) -* 默认 profiling 是关闭的,可以通过 set 语句在 Session 级别开启profiling: +* 默认 profiling 是关闭的,可以通过 set 语句在 Session 级别开启 profiling: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL- profiling.png) + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-profiling.png) ```mysql SET profiling=1; #开启profiling 开关; @@ -4517,7 +4757,7 @@ SHOW PROFILES 能够在做 SQL 优化时分析当前会话中语句执行的资 SHOW PROFILES; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL- 查看SQL语句执行耗时.png) + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-查看SQL语句执行耗时.png) * 查看到该 SQL 执行过程中每个线程的状态和消耗的时间: @@ -5151,18 +5391,6 @@ SQL 提示,是优化数据库的一个重要手段,就是在SQL语句中加 ### 应用优化 -#### 连接池 - -在实际生产环境中,由于数据库本身的性能局限,就必须要对前台的应用进行一些优化,来降低数据库的访问压力 - -池化技术:对于访问数据库来说,建立连接的代价是比较昂贵的,频繁的创建关闭连接比较耗费资源,有必要建立数据库连接池,以提高访问的性能 - - - -*** - - - #### 减少访问 避免对数据进行重复检索:能够一次连接就获取到结果的,就不用两次连接,这样可以大大减少对数据库无用的重复请求 @@ -5210,138 +5438,6 @@ SQL 提示,是优化数据库的一个重要手段,就是在SQL语句中加 -### 缓存优化 - -#### 工作流程 - -开启 Mysql 的查询缓存,当执行完全相同的 SQL 语句的时候,服务器就会直接从缓存中读取结果,当数据被修改,之前的缓存会失效,修改比较频繁的表不适合做查询缓存 - -查询过程: - -1. 客户端发送一条查询给服务器 -2. 服务器先会检查查询缓存,如果命中了缓存,则立即返回存储在缓存中的结果,否则进入下一阶段 -3. 服务器端进行 SQL 解析、预处理,再由优化器生成对应的执行计划 -4. MySQL 根据优化器生成的执行计划,调用存储引擎的 API 来执行查询 -5. 将结果返回给客户端 - - - - - -*** - - - -#### 缓存配置 - -1. 查看当前的 MySQL 数据库是否支持查询缓存: - - ```mysql - SHOW VARIABLES LIKE 'have_query_cache'; -- YES - ``` - -2. 查看当前MySQL是否开启了查询缓存: - - ```mysql - SHOW VARIABLES LIKE 'query_cache_type'; -- OFF - ``` - - 参数说明: - - * OFF 或 0:查询缓存功能关闭 - - * ON 或 1:查询缓存功能打开,查询结果符合缓存条件即会缓存,否则不予缓存;可以显式指定 SQL_NO_CACHE 不予缓存 - - * DEMAND 或 2:查询缓存功能按需进行,显式指定 SQL_CACHE 的 SELECT 语句才缓存,其它不予缓存 - - ```mysql - SELECT SQL_CACHE id, name FROM customer; -- SQL_CACHE:查询结果可缓存 - SELECT SQL_NO_CACHE id, name FROM customer;-- SQL_NO_CACHE:不使用查询缓存 - ``` - -3. 查看查询缓存的占用大小: - - ```mysql - SHOW VARIABLES LIKE 'query_cache_size';-- 单位是字节 1048576 / 1024 = 1024 = 1KB - ``` - -4. 查看查询缓存的状态变量: - - ```mysql - SHOW STATUS LIKE 'Qcache%'; - ``` - - - - | 参数 | 含义 | - | ----------------------- | ------------------------------------------------------------ | - | Qcache_free_blocks | 查询缓存中的可用内存块数 | - | Qcache_free_memory | 查询缓存的可用内存量 | - | Qcache_hits | 查询缓存命中数 | - | Qcache_inserts | 添加到查询缓存的查询数 | - | Qcache_lowmen_prunes | 由于内存不足而从查询缓存中删除的查询数 | - | Qcache_not_cached | 非缓存查询的数量(由于 query_cache_type 设置而无法缓存或未缓存) | - | Qcache_queries_in_cache | 查询缓存中注册的查询数 | - | Qcache_total_blocks | 查询缓存中的块总数 | - -5. 配置 my.cnf: - - ```sh - sudo chmod 666 /etc/mysql/my.cnf - vim my.cnf - # mysqld中配置缓存 - query_cache_type=1 - ``` - - 重启服务既可生效,执行SQL语句进行验证 ,执行一条比较耗时的SQL语句,然后再多执行几次,查看后面几次的执行时间;获取通过查看查询缓存的缓存命中数,来判定是否走查询缓存 - - - -*** - - - -#### 缓存失效 - -查询缓存失效的情况: - -* SQL 语句不一致的情况,要想命中查询缓存,查询的 SQL 语句必须一致 - - ```mysql - select count(*) from tb_item; - Select count(*) from tb_item; -- 不走缓存,首字母不一致 - ``` - -* 当查询语句中有一些不确定查询时,则不会缓存,比如:now()、current_date()、curdate()、curtime()、rand()、uuid()、user()、database() - - ```mysql - SELECT * FROM tb_item WHERE updatetime < NOW() LIMIT 1; - SELECT USER(); - SELECT DATABASE(); - ``` - -* 不使用任何表查询语句: - - ```mysql - SELECT 'A'; - ``` - -* 查询 mysql、information_schema、performance_schema 等系统表时,不走查询缓存: - - ```mysql - SELECT * FROM information_schema.engines; - ``` - -* 在存储过程、触发器或存储函数的主体内执行的查询,缓存失效 - -* 如果表更改,则使用该表的所有高速缓存查询都将变为无效并从高速缓存中删除,包括使用 MERGE 映射到已更改表的表的查询,比如:INSERT、UPDATE、DELETE、ALTER TABLE、DROP TABLE、DROP DATABASE - - - -*** - - - ### 内存优化 #### 优化原则 @@ -5415,7 +5511,7 @@ Innodb 用一块内存区做 IO 缓存池,该缓存池不仅用来缓存 Innod -### 并发参数 +### 并发优化 MySQL Server 是多线程结构,包括后台线程和客户服务线程。多线程可以有效利用服务器资源,提高数据库的并发性能。在 MySQL 中,控制并发连接和线程的主要参数: @@ -5692,8 +5788,8 @@ MySQL 的主从复制原理图: - 共享锁:也叫读锁。对同一份数据,多个事务读操作可以同时加锁而不互相影响 ,但不能修改数据 - 排他锁:也叫写锁。当前的操作没有完成前,会阻断其他操作的读取和写入 - 按粒度分类: - - 表级锁:会锁定整个表,开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低,偏向MyISAM 存储引擎 - - 行级锁:会锁定当前操作行,开销大,加锁慢;会出现死锁;锁定力度小,发生锁冲突概率低,并发度高,偏向 InnoDB 存储引擎 + - 表级锁:会锁定整个表,开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低,偏向 MyISAM + - 行级锁:会锁定当前操作行,开销大,加锁慢;会出现死锁;锁定力度小,发生锁冲突概率低,并发度高,偏向 InnoDB - 页级锁:锁的力度、发生冲突的概率和加锁开销介于表锁和行锁之间,会出现死锁,并发性能一般 - 按使用方式分类: - 悲观锁:每次查询数据时都认为别人会修改,很悲观,所以查询时加锁 @@ -5722,13 +5818,13 @@ MySQL 的主从复制原理图: MyISAM 存储引擎只支持表锁,这也是 MySQL 开始几个版本中唯一支持的锁类型 -MyISAM 引擎在执行查询语句之前,会**自动**给涉及到的所有表加读锁,在执行增删改之前,会自动给涉及的表加写锁,这个过程并不需要用户干预,所以用户一般不需要直接用 LOCK TABLE 命令给 MyISAM 表显式加锁 +MyISAM 引擎在执行查询语句之前,会**自动**给涉及到的所有表加读锁,在执行增删改之前,会**自动**给涉及的表加写锁,这个过程并不需要用户干预,所以用户一般不需要直接用 LOCK TABLE 命令给 MyISAM 表显式加锁 * 加锁命令: - 读锁:**所有**连接只能读取数据,不能修改 + 读锁:所有连接只能读取数据,不能修改 - 写锁:**其他**连接不能查询和修改数据 + 写锁:其他连接不能查询和修改数据 ```mysql -- 读锁 @@ -5895,7 +5991,7 @@ InnoDB 实现了以下两种类型的行锁: - 共享锁 (S):又称为读锁,简称 S 锁,就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改 - 排他锁 (X):又称为写锁,简称 X 锁,就是不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,只有获取排他锁的事务是可以对数据读取和修改 -对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会**自动给涉及数据集加排他锁**(行锁);对于普通 SELECT 语句,不会加任何锁 +对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会**自动给涉及数据集加排他锁**(行锁),在 commit 的时候会自动释放;对于普通 SELECT 语句,不会加任何锁 锁的兼容性: @@ -5913,6 +6009,8 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 + + *** @@ -6006,46 +6104,15 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 -*** - - - -#### 锁升级 - -无索引行锁升级为表锁:不通过索引检索数据,InnoDB 会将对表中的所有记录加锁,实际效果和**表锁**一样 - -索引失效会造成锁升级,实际开发过程应避免出现索引失效的状况 - -* 查看当前表的索引: - - ```mysql - SHOW INDEX FROM test_innodb_lock; - ``` - -* 关闭自动提交功能: - - ```mysql - SET AUTOCOMMIT=0; -- C1、C2 - ``` - -* 执行更新语句: - - ```mysql - UPDATE test_innodb_lock SET sex='2' WHERE name=10; -- C1 - UPDATE test_innodb_lock SET sex='2' WHERE id=3; -- C2 - ``` - - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁升级.png) - - 索引失效:执行更新时 name 字段为 varchar 类型,造成索引失效,最终行锁变为表锁 - ​ *** -#### 间隙锁 +#### 锁分类 + +##### 间隙锁 当使用范围条件检索数据,并请求共享或排他锁时,InnoDB 会给符合条件的已有数据进行加锁,对于键值在条件范围内但并不存在的记录,叫做间隙(GAP), InnoDB 会对间隙进行加锁,就是间隙锁 @@ -6092,7 +6159,7 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 -#### 意向锁 +##### 意向锁 InnoDB 为了支持多粒度的加锁,允许行锁和表锁同时存在,支持在不同粒度上的加锁操作,InnoDB 增加了意向锁(Intention Lock ) @@ -6106,7 +6173,24 @@ InnoDB 存储引擎支持的是行级别的锁,因此意向锁不会阻塞除 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-意向锁兼容性.png) -插入意向锁是在插入一行记录操作之前设置的一种间隙锁,这个锁释放了一种插入方式的信号,即多个事务在相同的索引间隙插入时如果不是插入间隙中相同的位置就不需要互相等待。假设某列有索引值2,6,只要两个事务插入位置不同,如事务 A 插入3,事务 B 插入4,那么就可以同时插入 +插入意向锁是在插入一行记录操作之前设置的一种间隙锁,这个锁释放了一种插入方式的信号,即多个事务在相同的索引间隙插入时如果不是插入间隙中相同的位置就不需要互相等待。假设某列有索引值2,6,只要两个事务插入位置不同,如事务 A 插入 3,事务 B 插入 4,那么就可以同时插入 + + + +*** + + + +##### 死锁 + +当并发系统中不同线程出现循环资源依赖,线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁 + +死锁情况:线程 A 修改了 id = 1 的数据,请求修改 id = 2 的数据,线程 B 修改了 id = 2 的数据,请求修改 id = 1 的数据,产生死锁 + +解决策略: + +* 直接进入等待直到超时,超时时间可以通过参数 innodb_lock_wait_timeout 来设置,但是时间的设置不好控制,超时可能不是因为死锁,而是因为事务处理比较慢,所以一般不采取该方式 +* 主动死锁检测,发现死锁后**主动回滚死锁链条中的某一个事务**,让其他事务得以继续执行,将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑 @@ -6114,6 +6198,65 @@ InnoDB 存储引擎支持的是行级别的锁,因此意向锁不会阻塞除 +#### 锁优化 + +##### 锁升级 + +无索引行锁升级为表锁:不通过索引检索数据,InnoDB 会将对表中的所有记录加锁,实际效果和**表锁**一样 + +索引失效会造成锁升级,实际开发过程应避免出现索引失效的状况 + +* 查看当前表的索引: + + ```mysql + SHOW INDEX FROM test_innodb_lock; + ``` + +* 关闭自动提交功能: + + ```mysql + SET AUTOCOMMIT=0; -- C1、C2 + ``` + +* 执行更新语句: + + ```mysql + UPDATE test_innodb_lock SET sex='2' WHERE name=10; -- C1 + UPDATE test_innodb_lock SET sex='2' WHERE id=3; -- C2 + ``` + + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁升级.png) + + 索引失效:执行更新时 name 字段为 varchar 类型,造成索引失效,最终行锁变为表锁 + + + +*** + + + +##### 优化锁 + +InnoDB 存储引擎实现了行级锁定,虽然在锁定机制的实现方面带来了性能损耗可能比表锁会更高,但是在整体并发处理能力方面要远远优于 MyISAM 的表锁,当系统并发量较高的时候,InnoDB 的整体性能远远好于 MyISAM + +但是使用不当可能会让InnoDB 的整体性能表现不仅不能比 MyISAM 高,甚至可能会更差 + +优化建议: + +- 尽可能让所有数据检索都能通过索引来完成,避免无索引行锁升级为表锁 +- 合理设计索引,尽量缩小锁的范围 +- 尽可能减少索引条件及索引范围,避免间隙锁 +- 尽量控制事务大小,减少锁定资源量和时间长度 +- 尽可使用低级别事务隔离(需要业务层面满足需求) + + + + + +*** + + + #### 锁状态 ```mysql @@ -6149,24 +6292,6 @@ lock_id 是锁 id;lock_trx_id 为事务 id;lock_mode 为 X 代表排它锁 -*** - - - -#### 锁优化 - -InnoDB 存储引擎实现了行级锁定,虽然在锁定机制的实现方面带来了性能损耗可能比表锁会更高,但是在整体并发处理能力方面要远远优于 MyISAM 的表锁,当系统并发量较高的时候,InnoDB 的整体性能远远好于 MyISAM - -InnoDB 的行级锁,如果使用不当可能会让InnoDB 的整体性能表现不仅不能比 MyISAM 高,甚至可能会更差 - -优化建议: - -- 尽可能让所有数据检索都能通过索引来完成,避免无索引行锁升级为表锁 -- 合理设计索引,尽量缩小锁的范围 -- 尽可能减少索引条件及索引范围,避免间隙锁 -- 尽量控制事务大小,减少锁定资源量和时间长度 -- 尽可使用低级别事务隔离(需要业务层面满足需求) - *** @@ -6184,7 +6309,7 @@ InnoDB 的行级锁,如果使用不当可能会让InnoDB 的整体性能表现 * 版本号 - 1. 给数据表中添加一个version列,每次更新后都将这个列的值加1 + 1. 给数据表中添加一个 version 列,每次更新后都将这个列的值加 1 2. 读取数据时,将版本号读取出来,在执行更新的时候,比较版本号 @@ -6212,7 +6337,7 @@ InnoDB 的行级锁,如果使用不当可能会让InnoDB 的整体性能表现 * 时间戳 - - 和版本号方式基本一样,给数据表中添加一个列,名称无所谓,数据类型需要是**timestamp** + - 和版本号方式基本一样,给数据表中添加一个列,名称无所谓,数据类型需要是 **timestamp** - 每次更新后都将最新时间插入到此列 - 读取数据时,将时间读取出来,在执行更新的时候,比较时间 - 如果相同则执行更新,如果不相同,说明此条数据已经发生了变化 @@ -6275,11 +6400,11 @@ tail -f /var/log/mysql/error.log #### 基本介绍 -归档日志(BINLOG)记录了所有的 DDL(数据定义语言)语句和 DML(数据操作语言)语句,但**不包括数据查询语句**,在事务提交时写入。归档日志也叫二进制日志,是因为采用二进制进行存储 +归档日志(BINLOG)也叫二进制日志,是因为采用二进制进行存储,记录了所有的 DDL(数据定义语言)语句和 DML(数据操作语言)语句,但**不包括数据查询语句,在事务提交前的最后阶段写入** 作用:**灾难时的数据恢复和 MySQL 的主从复制** -归档日志默认情况下是没有开启的,需要在 MySQL 配置文件中开启,并配置MySQL日志的格式: +归档日志默认情况下是没有开启的,需要在 MySQL 配置文件中开启,并配置 MySQL 日志的格式: ```sh cd /etc/mysql @@ -6295,10 +6420,10 @@ binlog_format=STATEMENT 日志格式: -* STATEMENT:该日志格式在日志文件中记录的都是 SQL 语句 (statement),每一条对数据进行修改的 SQL都会记录在日志文件中,通过 mysqlbinlog 工具,可以查看到每条语句的文本。主从复制时,从库 (slave) 会将日志解析为原语句,并在从库重新执行一 -* ROW:该日志格式在日志文件中记录的是每一行的数据变更,而不是记录 SQL 语句。比如执行 SQL 语句`update tb_book set status='1'`,如果是 STATEMENT,在日志中会记录一行 SQL 语句; 如果是 ROW,由于是对全表进行更新,就是每一行记录都会发生变更,ROW 格式的日志中会记录每一行的数据变更 +* STATEMENT:该日志格式在日志文件中记录的都是 SQL 语句,每一条对数据进行修改的 SQL都会记录在日志文件中,通过 mysqlbinlog 工具,可以查看到每条语句的文本。主从复制时,从库会将日志解析为原语句,并在从库重新执行一 +* ROW:该日志格式在日志文件中记录的是每一行的数据变更,而不是记录 SQL 语句。比如执行 SQL 语句 `update tb_book set status='1'`,如果是 STATEMENT,在日志中会记录一行 SQL 语句; 如果是 ROW,由于是对全表进行更新,就是每一行记录都会发生变更,ROW 格式的日志中会记录每一行的数据变更 -* MIXED:这是 MySQL 默认的日志格式,混合了STATEMENT 和 ROW两种格式。MIXED 格式能尽量利用两种模式的优点,而避开他们的缺点 +* MIXED:这是 MySQL 默认的日志格式,混合了STATEMENT 和 ROW两种格式。MIXED 格式能尽量利用两种模式的优点,而避开它们的缺点 diff --git a/Java.md b/Java.md index 2bc5b09..1cb838c 100644 --- a/Java.md +++ b/Java.md @@ -2845,13 +2845,13 @@ public class MyArraysDemo { 用于生成伪随机数。 使用步骤: -1. 导入包:`import java.util.Random;` -2. 创建对象:`Random r = new Random();` -3. 随机整数:`int num = r.nextInt(10);` -解释:10代表的是一个范围,如果括号写10,产生的随机数就是0-9,括号写20的随机数则是0-19 -获取0-10:`int num = r.nextInt(10) + 1` +1. 导入包:`import java.util.Random` +2. 创建对象:`Random r = new Random()` +3. 随机整数:`int num = r.nextInt(10)` +* 解释:10 代表的是一个范围,如果括号写 10,产生的随机数就是 0-9,括号写 20 的随机数则是 0-19 +* 获取 0-10:`int num = r.nextInt(10 + 1)` -4. 随机小数:`public double nextDouble()`从范围`0.0d`(含)至`1.0d` (不包括),伪随机地生成并返回 +4. 随机小数:`public double nextDouble()` 从范围 `0.0d` 至 `1.0d` (左闭右开),伪随机地生成并返回 @@ -15727,14 +15727,14 @@ public class MergeSort { mergeSort(arr, 0, arr.length - 1); System.out.println(Arrays.toString(arr)); } - //low 为arr最小索引,high为最大索引 + // low 为arr最小索引,high为最大索引 public static void mergeSort(int[] arr, int low, int high) { // low == high 时说明只有一个元素了,直接返回 if (low < high) { int mid = (low + high) / 2; - mergeSort(arr, low, mid);//归并排序前半段 - mergeSort(arr, mid + 1, high);//归并排序后半段 - merge(arr, low, mid, high);//将两段有序段合成一段有序段 + mergeSort(arr, low, mid); //归并排序前半段 + mergeSort(arr, mid + 1, high); //归并排序后半段 + merge(arr, low, mid, high); //将两段有序段合成一段有序段 } } @@ -16027,9 +16027,9 @@ public class binarySearch { int start = 0; int end = arr.length - 1; - //确保不会出现重复查找,越界 + // 确保不会出现重复查找,越界 while (start <= end) { - //计算出中间索引值 + // 计算出中间索引值 int mid = (start + end) / 2; if (des == arr[mid]) { return mid; From 78a598698e1de08613924d227ee33e41e90d12c5 Mon Sep 17 00:00:00 2001 From: Seazean Date: Tue, 24 Aug 2021 23:07:08 +0800 Subject: [PATCH 037/168] Update Java Notes --- Java.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Java.md b/Java.md index 1cb838c..0721a91 100644 --- a/Java.md +++ b/Java.md @@ -905,7 +905,7 @@ public class MethodDemo { enum Season { SPRING , SUMMER , AUTUMN , WINTER; } - //枚举类的编译以后源代码: + // 枚举类的编译以后源代码: public final class Season extends java.lang.Enum { public static final Season SPRING = new Season(); public static final Season SUMMER = new Season(); @@ -917,15 +917,17 @@ public class MethodDemo { } ``` -* API使用 +* API 使用 ```java public class EnumDemo { public static void main(String[] args){ - //获取索引 - Season s = Season.SPRING; // s = SPRING - System.out.println(s.ordinal()); - //获取全部枚举 + // 获取索引 + Season s = Season.SPRING;/ + System.out.println(s); //SPRING + System.out.println(s.ordinal()); // 0,代表索引,summer 就是 1 + s.s.doSomething(); + // 获取全部枚举 Season[] ss = Season.values(); for(int i = 0; i < ss.length; i++){ System.out.println(ss[i]); @@ -937,6 +939,10 @@ public class MethodDemo { } enum Season { SPRING , SUMMER , AUTUMN , WINTER; + + public void doSomething() { + System.out.println("hello "); + } } ``` From 1234f479ad9c3547fd4a3f65acd7d169530a94b7 Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 26 Aug 2021 00:07:48 +0800 Subject: [PATCH 038/168] Update Java Notes --- DB.md | 534 +++++++++++++++++++++++++++++++++++++++----------------- Prog.md | 2 +- SSM.md | 16 +- 3 files changed, 382 insertions(+), 170 deletions(-) diff --git a/DB.md b/DB.md index 5701429..5fb58b1 100644 --- a/DB.md +++ b/DB.md @@ -336,6 +336,8 @@ mysqlshow -uroot -p1234 test book --count + + ## 体系结构 ### 整体架构 @@ -409,6 +411,8 @@ SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 + + *** @@ -567,9 +571,44 @@ select * from t where id = 1; #### 优化器 -优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。 +##### 扫描行数 + +优化器是在表里面有多个索引的时候,决定使用哪个索引,找到一个最优的执行方案,用最小的代价去执行语句;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序 + +在数据库里面,扫描行数是影响执行代价的因素之一,扫描的行数越少意味着访问磁盘的次数越少,消耗的 CPU 资源越少,优化器还会结合是否使用临时表、是否排序等因素进行综合判断 -优化器阶段完成后,这个语句的执行方案就确定下来了,然后进入执行器阶段。 +MySQL 在真正执行语句之前,并不能精确地知道满足条件的记录有多少条,只能根据统计信息来估算记录,统计信息就是索引的区分度,一个索引上不同的值的个数(比如性别只能是男女,就是 2 ),称之为基数(cardinality),基数越大说明区分度越好 + +* 通过采样统计来获取基数,InnoDB 默认会选择 N 个数据页,统计这些页面上的不同值得到一个平均值,然后乘以这个索引的页面数,就得到了这个索引的基数 +* 数据表是会持续更新的,索引统计信息也不会固定不变,当变更的数据行数超过 1/ M 的时候,会自动触发重新做一次索引统计 + +* 在 MySQL 中,有两种存储索引统计的方式,可以通过设置参数 innodb_stats_persistent 的值来选择: + * 设置为 on 时,表示统计信息会持久化存储,这时默认的 N 是 20,M 是 10 + * 设置为 off 时,表示统计信息只存储在内存,这时默认的 N 是 8,M 是 16 + +EXPLAIN 执行计划在优化器阶段生成,如果发现 explain 的结果预估的 rows 值跟实际情况差距比较大,可以执行 `analyze table t ` 重新修正信息 + + + +*** + + + +##### 错选索引 + +扫描行数本身是估算数据,或者 SQL 语句中的字段选择有问题时,可能导致 MySQL 没有选择正确的执行索引 + +解决方法: + +* 采用 force index 强行选择一个索引 + + ```sql + SELECT * FROM user FORCE INDEX(name) WHERE NAME='seazean'; + ``` + +* 可以考虑修改 SQL 语句,引导 MySQL 使用期望的索引 + +* 新建一个更合适的索引,来提供给优化器做选择,或删掉误用的索引 @@ -583,10 +622,55 @@ select * from t where id = 1; +**** + + + +### 数据空间 + +#### 数据存储 + +表数据既可以存在共享表空间里,也可以是单独的文件,这个行为是由参数 innodb_file_per_table 控制的: + +* OFF:表示表的数据放在系统共享表空间,也就是跟数据字典放在一起 +* ON :表示每个 InnoDB 表数据存储在一个以 .ibd 为后缀的文件中(默认) + +一个表单独存储为一个文件更容易管理,在不需要这个表时通过 drop table 命令,系统就会直接删除这个文件;如果是放在共享表空间中,即使表删掉了,空间也是不会回收的 + + + +*** + + + +#### 数据删除 + +MySQL 的数据删除就是移除掉某个记录后,该位置就被标记为可复用,如果有符合范围条件的数据可以插入到这里。符合范围条件的意思是假设删除记录 R4,之后要再插入一个 ID 在 300 和 600 之间的记录时,就会复用这个位置 + + + +InnoDB 的数据是按页存储的如果删掉了一个数据页上的所有记录,整个数据页就可以被复用了,如果相邻的两个数据页利用率都很小,系统就会把这两个页上的数据合到其中一个页上,另外一个数据页就被标记为可复用 + +删除命令其实只是把记录的位置,或者数据页标记为了**可复用**,但磁盘文件的大小是不会变的,这些可以复用还没有被使用的空间,看起来就像是空洞,造成数据库的稀疏,因此需要进行紧凑处理 + + + +*** + + + +### 空间收缩 + + + + + *** + + ## 单表操作 ### SQL @@ -740,7 +824,12 @@ select * from t where id = 1; USE db4; -- 使用db4数据库 ``` - + + + +*** + + #### 数据表 @@ -929,6 +1018,8 @@ select * from t where id = 1; +*** + #### UPDATE @@ -955,6 +1046,10 @@ select * from t where id = 1; +*** + + + #### DELETE * 删除表数据语法 @@ -1181,13 +1276,13 @@ LIMIT * 聚合函数分类 - | 函数名 | 功能 | - | ----------- | -------------------------------- | - | COUNT(列名) | 统计数量(一般选用不为null的列) | - | MAX(列名) | 最大值 | - | MIN(列名) | 最小值 | - | SUM(列名) | 求和 | - | AVG(列名) | 平均值(AVG() 会忽略 NULL 行) | + | 函数名 | 功能 | + | ----------- | ---------------------------------- | + | COUNT(列名) | 统计数量(一般选用不为 null 的列) | + | MAX(列名) | 最大值 | + | MIN(列名) | 最小值 | + | SUM(列名) | 求和 | + | AVG(列名) | 平均值(会忽略 null 行) | * 例如 @@ -1444,6 +1539,8 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 + + ## 约束操作 ### 约束分类 @@ -1723,6 +1820,8 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 + + ## 多表操作 ### 多表设计 @@ -2193,7 +2292,10 @@ CREATE TABLE us_pro( -*** + +**** + + @@ -2221,7 +2323,7 @@ CREATE TABLE us_pro( 1. 开启事务:记录回滚点,并通知服务器,将要执行一组操作,要么同时成功、要么同时失败 -2. 执行sql语句:执行具体的一条或多条sql语句 +2. 执行 SQL 语句:执行具体的一条或多条 SQL语句 3. 结束事务(提交|回滚) @@ -2243,12 +2345,34 @@ CREATE TABLE us_pro( ROLLBACK; ``` -* 提交事务 +* 提交事务,显示执行是手动提交,MySQL 默认为自动提交 ```mysql COMMIT; ``` + 工作原理: + + * 在自动提交模式下,如果没有 start transaction 显式地开始一个事务,那么每个 SQL 语句都会被当做一个事务执行提交操作 + * 在手动提交模式下,所有的 SQL 语句都在一个事务中,直到执行了 commit 或 rollback,该事务结束,同时开始了另外一个事务 + + * 存在一些特殊的命令,在事务中执行了这些命令会马上强制执行 COMMIT 提交事务,如 DDL 语句 (create/drop/alter/table)、lock tables 语句等 + + 提交方式语法: + + - 查看事务提交方式 + + ```mysql + SELECT @@AUTOCOMMIT; -- 1代表自动提交 0代表手动提交 + ``` + + - 修改事务提交方式 + + ```mysql + SET @@AUTOCOMMIT=数字; -- 系统 + SET AUTOCOMMIT=数字; -- 会话 + ``` + * 管理实务演示 @@ -2272,39 +2396,6 @@ CREATE TABLE us_pro( -*** - - - -### 提交方式 - -提交方式: - -- 自动提交(MySQL默认为自动提交) -- 手动提交: - -工作原理: - -* 在自动提交模式下,如果没有 start transaction 显式地开始一个事务,那么每个sql语句都会被当做一个事务执行提交操作 -* 在手动提交模式下,所有的 sql 语句都在一个事务中,直到执行了commit 或 rollback,该事务结束,同时开始了另外一个事务 - -* 存在一些特殊的命令,在事务中执行了这些命令会马上强制执行 COMMIT 提交事务,如DDL语句 (create table/drop table/alter/table)、lock tables 语句等 - -提交方式语法: - -- 查看事务提交方式 - - ```mysql - SELECT @@AUTOCOMMIT; -- 1代表自动提交 0代表手动提交 - ``` - -- 修改事务提交方式 - - ```mysql - SET @@AUTOCOMMIT=数字; -- 系统 - SET AUTOCOMMIT=数字; -- 会话 - ``` - *** @@ -2317,10 +2408,10 @@ CREATE TABLE us_pro( 事务的四大特征:ACID -- 原子性(atomicity) -- 一致性(consistency) -- 隔离性(isolaction) -- 持久性(durability) +- 原子性 (atomicity) +- 一致性 (consistency) +- 隔离性 (isolaction) +- 持久性 (durability) @@ -2347,7 +2438,7 @@ undo log 属于逻辑日志,根据每行操作进行记录,记录了 SQL 执 * 对于每个 update,回滚时会执行一个相反的 update,把数据修改回去 -undo log 是采用段 (segment) 的方式来记录,每个 undo 操作在记录的时候占用一个 undo log segment +undo log 是采用段(segment)的方式来记录,每个 undo 操作在记录的时候占用一个 undo log segment rollback segment 称为回滚段,每个回滚段中有 1024 个 undo log segment @@ -2409,32 +2500,53 @@ rollback segment 称为回滚段,每个回滚段中有 1024 个 undo log segme 持久性是指一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,接下来的其他操作或故障不应该对其有任何影响。 -InnoDB 作为存储引擎,数据是存放在磁盘中,每次读写数据都需要磁盘 IO,效率会很低。InnoDB 提供了缓存 Buffer Pool,Buffer Pool 中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲: +Buffer Pool 是一片内存空间,可以通过 innodb_buffer_pool_size 来控制 Buffer Pool 的大小(内存优化部分会详解) + +* Change Buffer 是 Buffer Pool 里的内存,不能无限增大,用来对增删改操作提供缓存 +* Change Buffer 的大小可以通过参数 innodb_change_buffer_max_size 来动态设置,设置为 50 时表示 Change Buffer 的大小最多只能占用 Buffer Pool 的 50% +* 补充知识:唯一索引的更新不能使用 Buffer,一般只有普通索引可以使用,直接写入 Buffer 就结束 + +InnoDB 的数据是按数据页为单位来读写,每个数据页的大小默认是 16KB。数据是存放在磁盘中,每次读写数据都需要磁盘 IO,效率会很低。InnoDB 提供了缓存 Change Buffer,Buffer 中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲: * 从数据库读取数据时,会首先从缓存中读取,如果缓存中没有,则从磁盘读取后放入 Buffer Pool * 向数据库写入数据时,会首先写入缓存,缓存中修改的数据会**定期刷新**到磁盘,这一过程称为刷脏 + + +*** + + + +##### 数据恢复 + Buffer Pool 的使用提高了读写数据的效率,但是也带了新的问题:如果 MySQL 宕机,此时 Buffer Pool 中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证,所以引入 redo log -* 当数据修改时,除了修改 Buffer Pool 中的数据,还会在 redo log 记录这次操作 -* 如果 MySQL 宕机,重启时可以读取 redo log 中的数据,对数据库进行恢复 +* 当数据修改时,先修改 Change Buffer 中的数据,然后在 redo log buffer 记录这次操作 +* 如果 MySQL 宕机,InnoDB 判断一个数据页在崩溃恢复时丢失了更新,就会将它读到内存,然后根据 redo log 内容更新内存,更新完成后,内存页变成脏页,然后进行刷脏(buffer pool 的任务) redo log **记录数据页的物理修改**,而不是某一行或某几行的修改,用来恢复提交后的物理数据页,且只能恢复到最后一次提交的位置 redo log 采用的是 WAL(Write-ahead logging,**预写式日志**),所有修改要先写入日志,再更新到磁盘,保证了数据不会因 MySQL 宕机而丢失,从而满足了持久性要求,InnoDB 引擎会在适当的时候,将这个操作记录更新到磁盘里面 -redo log 也需要在事务提交时将日志写入磁盘,但是比将 Buffer Pool 中修改的数据写入磁盘快: +redo log 也需要在事务提交时将日志写入磁盘,但是比将内存中的 Buffer Pool 修改的数据写入磁盘的速度快: * 刷脏是随机 IO,因为每次修改的数据位置随机,但写 redo log 是尾部追加操作,属于顺序 IO -* 刷脏是以数据页 (Page) 为单位的,MySQL 默认页大小是 16KB,一个页上的一个小修改都要整页写入,而 redo log 中只包含真正需要写入的部分,无效 IO 大大减少 +* 刷脏是以数据页(Page)为单位的,一个页上的一个小修改都要整页写入,而 redo log 中只包含真正需要写入的部分,减少无效 IO -刷盘策略,就是把内存缓冲区中 redo log 日志持久化到磁盘,通过修改参数 `innodb_flush_log_at_trx_commit` 设置: +刷盘策略,就是把内存中 redo log buffer 持久化到磁盘: -* 0:表示当提交事务时,并不将缓冲区的 redo 日志写入磁盘,而是等待主线程每秒刷新一次 -* 1:在事务提交时将缓冲区的 redo 日志**同步写入**到磁盘,保证一定会写入成功 -* 2:在事务提交时将缓冲区的 redo 日志异步写入到磁盘,不能保证提交时肯定会写入,只是有这个动作 +* 通过修改参数 `innodb_flush_log_at_trx_commit` 设置: + * 0:表示当提交事务时,并不将缓冲区的 redo 日志写入磁盘,而是等待主线程每秒刷新一次 + * 1:在事务提交时将缓冲区的 redo 日志**同步写入**到磁盘,保证一定会写入成功 + * 2:在事务提交时将缓冲区的 redo 日志异步写入到磁盘,不能保证提交时肯定会写入,只是有这个动作 +* 如果写入 redo log buffer 的日志已经占据了 redo log buffer 总容量的一半了,此时就会刷入到磁盘文件 -InnoDB 的 redo log 是固定大小的,如果写满了就要擦除以前的记录,在擦除之前需要把旧记录更新到数据文件中 +刷脏策略: + +* 系统内存不足,需要淘汰部分数据页,如果淘汰的是脏页,就要先将脏页写到磁盘 +* 系统空闲时,后台线程会自动进行刷脏 +* MySQL 正常关闭时,会把内存的脏页都 flush 到磁盘上 +* InnoDB 的 redo log 文件是固定大小的,如果写满了就要擦除以前的记录,在擦除之前需要把旧记录更新到磁盘中的数据文件中 @@ -2464,15 +2576,55 @@ update T set c=c+1 where ID=2; 流程说明:执行引擎将这行新数据更新到内存中(Buffer Pool)后,然后会将这个更新操作记录到 redo log buffer 里,此时 redo log 处于 prepare 状态,代表执行完成随时可以提交事务,然后执行器生成这个操作的 binlog,并**把 binlog 写入磁盘**,在提交事务后 **redo log 也持久化到磁盘** -redo log 和 binlog 都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致,在恢复数据时 +redo log 和 binlog 都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致,也有利于主从复制,更好的保持主从数据的一致性 + +故障恢复数据: + +* 如果在时刻 A 发生了崩溃(crash),由于此时 binlog 还没写,redo log 也没提交,所以数据恢复的时候这个事务会回滚 +* 如果在时刻 B 发生了崩溃,redo log 和 binlog 有一个共同的数据字段叫 XID,崩溃恢复的时候,会按顺序扫描 redo log: + * 如果 redo log 里面的事务是完整的,也就是已经有了 commit 标识,说明 binlog 也已经记录完整,直接从 redo log 恢复数据 + * 如果 redo log 里面的事务只有 prepare,就根据 XID 去 binlog 中判断对应的事务是否存在并完整,如果完整可以从 binlog 恢复 redo log 的信息,进而恢复数据,提交事务 + + +判断一个事务的 binlog 是否完整的方法: + +* statement 格式的 binlog,最后会有 COMMIT +* row 格式的 binlog,最后会有一个 XID event +* MySQL 5.6.2 版本以后,引入了 binlog-checksum 参数用来验证 binlog 内容的正确性 + + + +参考文章:https://time.geekbang.org/column/article/73161 + + + +*** + + + +##### 系统优化 + +系统在进行刷脏时会占用一部分系统资源,会影响系统的性能,产生系统抖动 + +* 一个查询要淘汰的脏页个数太多,会导致查询的响应时间明显变长 +* 日志写满,更新全部堵住,写性能跌为 0,这种情况对敏感业务来说,是不能接受的 + +InnoDB 刷脏页的控制策略: + +* `innodb_io_capacity` 参数代表磁盘的读写能力,建议设置成磁盘的 IOPS(每秒的 IO 次数) + +* 刷脏速度参考两个因素:脏页比例和 redo log 写盘速度 + * 参数`innodb_max_dirty_pages_pct` 是脏页比例上限,默认值是 75%,InnoDB 会根据当前的脏页比例,算出一个范围在 0 到 100 之间的数字 + * InnoDB 每次写入的日志都有一个序号,当前写入的序号跟 checkpoint 对应的序号之间的差值,InnoDB 根据差值算出一个范围在 0 到 100 之间的数字 + * 两者较大的值记为 R,执行引擎按照 innodb_io_capacity 定义的能力乘以 R% 来控制刷脏页的速度 + +* `innodb_flush_neighbors` 参数置为 1 代表控制刷脏时检查相邻的数据页,如果也是脏页就一起刷脏,并检查邻居的邻居,这个行为会一直蔓延直到不是脏页,在 MySQL 8.0 中该值的默认值是 0,不建议开启此功能 + -* 如果 redolog 状态为 commit,则说明 binlog 也成功,直接恢复数据 -* 如果 redolog 是 prepare,则需要查询对应的 binlog 事务是否成功,决定是回滚还是提交 -参考文章:https://time.geekbang.org/column/article/68633 @@ -2503,7 +2655,7 @@ redo log 和 binlog 都可以用于表示事务的提交状态,而两阶段提 > 可重复读的意思是不管读几次,结果都一样,可以重复的读,可以理解为快照读,要读的数据集不会发生变化 -* 幻读 (Phantom Reads):在事务中按某个条件先后两次查询数据库,两次查询结果的数量不同,**数据条目**发生了变化。比如查询某数据不存在,准备插入此记录,但执行插入时发现此记录已存在,无法插入;或查询某数据不存在,执行delete删除,却发现删除成功 +* 幻读 (Phantom Reads):在事务中按某个条件先后两次查询数据库,两次查询结果的数量不同,**数据条目**发生了变化。比如查询某数据不存在,准备插入此记录,但执行插入时发现此记录已存在,无法插入;或查询某数据不存在,执行删除操作却发现删除成功 **隔离级别操作语法:** @@ -2736,6 +2888,8 @@ RC、RR 级别下的 InnoDB 快照读区别 + + ## 存储结构 ### 视图 @@ -3734,6 +3888,8 @@ LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定 + + ## 存储引擎 ### 基本介绍 @@ -3879,7 +4035,8 @@ MERGE存储引擎: ALTER TABLE 表名 ENGINE = 引擎名称; ``` - + + @@ -3887,6 +4044,8 @@ MERGE存储引擎: + + ## 索引机制 ### 索引介绍 @@ -3931,10 +4090,10 @@ MySQL 官方对索引的定义为:索引(index)是帮助 MySQL 高效获 - 外键索引:只有 InnoDB 引擎支持外键索引,用来保证数据的一致性、完整性和实现级联操作 - 结构分类 - - BTree索引:MySQL使用最频繁的一个索引数据结构,是 InnoDB 和 MyISAM 存储引擎默认的索引类型,底层基于 B+Tree 数据结构 - - Hash索引:MySQL中 Memory 存储引擎默认支持的索引类型 + - BTree 索引:MySQL 使用最频繁的一个索引数据结构,是 InnoDB 和 MyISAM 存储引擎默认的索引类型,底层基于 B+Tree 数据结构 + - Hash 索引:MySQL中 Memory 存储引擎默认支持的索引类型 - R-tree 索引(空间索引):空间索引是 MyISAM 引擎的一个特殊索引类型,主要用于地理空间数据类型 - - Full-text (全文索引) :快速匹配全部文档的方式。MyISAM支持, InnoDB不支持FULLTEXT类型的索引,但是 InnoDB 可以使用 sphinx 插件支持全文索引,MEMORY引擎不支持 + - Full-text 索引(全文索引):快速匹配全部文档的方式。MyISAM 支持, InnoDB 不支持 FULLTEXT 类型的索引,但是 InnoDB 可以使用 sphinx 插件支持全文索引,MEMORY 引擎不支持 | 索引 | InnoDB引擎 | MyISAM引擎 | Memory引擎 | | ----------- | --------------- | ---------- | ---------- | @@ -4179,6 +4338,8 @@ B* 树:是 B+ 树的变体,在 B+ 树的非根和非叶子结点再增加指 MySQL 索引数据结构对经典的 B+Tree 进行了优化,在原 B+Tree 的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的 B+Tree,**提高区间访问的性能,防止回旋查找** +B+ 树的叶子节点是数据页(page),一个页里面可以存多个数据行 + 区间访问的意思是访问索引为 5 - 15 的数据,可以直接根据相邻节点的指针遍历 ![](https://gitee.com/seazean/images/raw/master/DB/索引的原理-B+Tree.png) @@ -4188,7 +4349,7 @@ MySQL 索引数据结构对经典的 B+Tree 进行了优化,在原 B+Tree 的 - 有范围:对于主键的范围查找和分页查找 - 有顺序:从根节点开始,进行随机查找,顺序查找 -InnoDB 存储引擎中页的大小为 16KB,一般表的主键类型为 INT(4字节)或 BIGINT(8字节),指针类型也一般为 4 或 8 个字节,也就是说一个页(B+Tree 中的**一个节点**)中大概存储 16KB/(8B+8B)=1K 个键值(估值)。则一个深度为 3 的 B+Tree 索引可以维护 `10^3 * 10^3 * 10^3 = 10亿` 条记录 +InnoDB 中每个数据页的大小默认是 16KB,一般表的主键类型为 INT(4字节)或 BIGINT(8字节),指针类型也一般为 4 或 8 个字节,也就是说一个页(B+Tree 中的**一个节点**)中大概存储 16KB/(8B+8B)=1K 个键值(估值)。则一个深度为 3 的 B+Tree 索引可以维护 `10^3 * 10^3 * 10^3 = 10亿` 条记录 实际情况中每个节点可能不能填充满,因此在数据库中,B+Tree 的高度一般都在 2-4 层。MySQL 的 InnoDB 存储引擎在设计时是**将根节点常驻内存的**,也就是说查找某一键值的行记录时最多只需要 1~3 次磁盘 I/O 操作 @@ -4207,8 +4368,8 @@ B+ 树为了维护索引有序性,在插入新值的时候需要做必要的 每个索引中每个块存储在磁盘页中,可能会出现以下两种情况: * 如果所在的数据页已经满了,根据 B+ 树的算法,这时候需要申请一个新的数据页,然后挪动部分数据过去,这个过程称为页分裂 - * 当相邻两个页由于删除了数据,利用率很低之后,会将数据页做合并,合并的过程可以认为是分裂过程的逆过程 +* 这两个情况都是由 B+ 树的结构决定的 一般选用数据小的字段做索引,字段长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小 @@ -4421,6 +4582,8 @@ SELECT * FROM user WHERE name LIKE '张%' AND age = 10; -- 头部模糊匹配 当要索引的列字符很多时,索引会变大变慢,可以只索引列开始的部分字符串,节约索引空间,提高索引效率 +注意:使用前缀索引就系统就忽略覆盖索引对查询性能的优化了 + 优化原则:**降低重复的索引值** 比如地区表: @@ -4440,6 +4603,15 @@ chinaBeijing 500 eee CREATE INDEX idx_area ON table_name(area(7)); ``` +场景:存储身份证 + +* 直接创建完整索引,这样可能比较占用空间 +* 创建前缀索引,节省空间,但会增加查询扫描次数,并且不能使用覆盖索引 +* 倒序存储,再创建前缀索引,用于绕过字符串本身前缀的区分度不够的问题(前 6 位相同的很多) +* 创建 hash 字段索引,查询性能稳定,有额外的存储和计算消耗,跟第三种方式一样,都不支持范围扫描 + + + @@ -4448,7 +4620,9 @@ CREATE INDEX idx_area ON table_name(area(7)); -## 语句优化 + + +## 系统优化 ### 优化步骤 @@ -4717,7 +4891,7 @@ key_len: * Using index:该值表示相应的 SELECT 操作中使用了**覆盖索引**(Covering Index) * Using index condition:第一种情况是搜索条件中虽然出现了索引列,但是有部分条件无法使用索引,会根据能用索引的条件先搜索一遍再匹配无法使用索引的条件,回表查询数据;第二种是使用了索引下推 -* Using where:表示存储引擎收到记录后进行“后过滤”(Post-filter),如果查询未能使用索引,Using where 的作用是提醒我们 MySQL 将用 WHERE 子句来过滤结果集,即需要回表查询 +* Using where:表示存储引擎收到记录后进行后过滤(Post-filter),如果查询未能使用索引,Using where 的作用是提醒我们 MySQL 将用 WHERE 子句来过滤结果集,即需要回表查询 * Using temporary:表示 MySQL 需要使用临时表来存储结果集,常见于排序和分组查询 * Using filesort:MySQL 会对数据使用一个外部的索引排序,而不是按照表内的索引顺序进行读取,无法利用索引完成的排序操作称为文件排序 * Using join buffer:说明在获取连接条件时没有使用索引,并且需要连接缓冲区来存储中间结果 @@ -4912,7 +5086,7 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name,status,address); ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引7.png) -* 用 **OR** 分割条件,索引失效,导致全表查询: +* **用 OR 分割条件,索引失效**,导致全表查询: OR 前的条件中的列有索引而后面的列中没有索引或 OR 前后两个列是同一个复合索引,都造成索引失效 @@ -4923,7 +5097,7 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name,status,address); ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引10.png) - AND 分割的条件不影响: + **AND 分割的条件不影响**: ```mysql EXPLAIN SELECT * FROM tb_seller WHERE name='阿里巴巴' AND createtime = '2088-01-01 12:00:00'; @@ -4931,7 +5105,7 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name,status,address); ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引11.png) -* 以 % 开头的 LIKE **模糊查询**,索引失效: +* **以 % 开头的 LIKE 模糊查询**,索引失效: 如果是尾部模糊匹配,索引不会失效;如果是头部模糊匹配,索引失效。 @@ -4951,6 +5125,8 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name,status,address); 原因:在覆盖索引的这棵 B+ 数上只需要进行 like 的匹配,或者是基于覆盖索引查询再进行 WHERE 的判断就可以获得结果 +系统优化为全表扫描: + * 如果 MySQL 评估使用索引比全表更慢,则不使用索引,索引失效: ```mysql @@ -5035,7 +5211,7 @@ SHOW GLOBAL STATUS LIKE 'Handler_read%'; -### 优化SQL +### SQL优化 #### 覆盖索引 @@ -5059,6 +5235,35 @@ EXPLAIN SELECT name,status,address,password FROM tb_seller WHERE name='小米科 +**** + + + +#### 减少访问 + +避免对数据进行重复检索:能够一次连接就获取到结果的,就不用两次连接,这样可以大大减少对数据库无用的重复请求 + +* 查询数据: + + ```mysql + SELECT id,name FROM tb_book; + SELECT id,status FROM tb_book; -- 向数据库提交两次请求,数据库就要做两次查询操作 + -- > 优化为: + SELECT id,name,statu FROM tb_book; + ``` + +* 插入数据: + + ```mysql + INSERT INTO tb_test VALUES(1,'Tom'); + INSERT INTO tb_test VALUES(2,'Cat'); + INSERT INTO tb_test VALUES(3,'Jerry'); -- 连接三次数据库 + -- >优化为 + INSERT INTO tb_test VALUES(1,'Tom'),(2,'Cat'),(3,'Jerry'); -- 连接一次 + ``` + +增加 cache 层:在应用中增加缓存层来达到减轻数据库负担的目的。可以部分数据从数据库中抽取出来放到应用端以文本方式存储, 或者使用框架(Mybatis)提供的一级缓存 / 二级缓存,或者使用 Redis 数据库来缓存数据 + *** @@ -5246,33 +5451,35 @@ GROUP BY 也会进行排序操作,与 ORDER BY 相比,GROUP BY 主要只是 -#### 嵌套查询 - -MySQL 4.1版本之后,开始支持 SQL 的子查询 - -* 可以使用 SELECT 语句来创建一个单列的查询结果,然后把结果作为过滤条件用在另一个查询中 -* 使用子查询可以一次性的完成逻辑上需要多个步骤才能完成的 SQL 操作,同时也可以避免事务或者表锁死 -* 在有些情况下,子查询是可以被更高效的连接(JOIN)替代 +#### OR -例如查找有角色的所有的用户信息: +对于包含 OR 的查询子句,如果要利用索引,则 OR 之间的每个条件列都必须用到索引,而且不能使用到复合索引,如果没有索引,则应该考虑增加索引 -* 执行计划: +* 执行查询语句: ```mysql - EXPLAIN SELECT * FROM t_user WHERE id IN (SELECT user_id FROM user_role); + EXPLAIN SELECT * FROM emp WHERE id = 1 OR age = 30; -- 两个索引,并且不是复合索引 ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL嵌套查询1.png) + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL OR条件查询1.png) -* 优化后: + ```sh + Extra: Using sort_union(idx_emp_age_salary,PRIMARY); Using where + ``` + +* 使用 UNION 替换 OR,求并集: + 注意:该优化只针对多个索引列有效,如果有 column 没有被索引,查询效率可能会因为没有选择 OR 而降低 ```mysql - EXPLAIN SELECT * FROM t_user u , user_role ur WHERE u.id = ur.user_id; + EXPLAIN SELECT * FROM emp WHERE id = 1 UNION SELECT * FROM emp WHERE age = 30; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL嵌套查询2.png) + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL OR条件查询2.png) - 连接查询之所以效率更高 ,是因为不需要在内存中创建临时表来完成逻辑上需要两个步骤的查询工作 +* UNION 要优于 OR 的原因: + + * UNION 语句的 type 值为 ref,OR 语句的 type 值为 range + * UNION 语句的 ref 值为 const,OR 语句的 ref 值为 null,const 表示是常量值引用,非常快 @@ -5280,35 +5487,35 @@ MySQL 4.1版本之后,开始支持 SQL 的子查询 -#### OR +#### 嵌套查询 -对于包含 OR 的查询子句,如果要利用索引,则 OR 之间的每个条件列都必须用到索引,而且不能使用到复合索引,如果没有索引,则应该考虑增加索引 +MySQL 4.1 版本之后,开始支持 SQL 的子查询 -* 执行查询语句: +* 可以使用 SELECT 语句来创建一个单列的查询结果,然后把结果作为过滤条件用在另一个查询中 +* 使用子查询可以一次性的完成逻辑上需要多个步骤才能完成的 SQL 操作,同时也可以避免事务或者表锁死 +* 在有些情况下,子查询是可以被更高效的连接(JOIN)替代 - ```mysql - EXPLAIN SELECT * FROM emp WHERE id = 1 OR age = 30; -- 两个索引,并且不是复合索引 - ``` +例如查找有角色的所有的用户信息: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL OR条件查询1.png) +* 执行计划: - ```sh - Extra: Using sort_union(idx_emp_age_salary,PRIMARY); Using where + ```mysql + EXPLAIN SELECT * FROM t_user WHERE id IN (SELECT user_id FROM user_role); ``` -* 使用 UNION 替换 OR,求并集: - 注意:该优化只针对多个索引列有效,如果有 column 没有被索引,查询效率可能会因为没有选择 OR 而降低 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL嵌套查询1.png) + +* 优化后: ```mysql - EXPLAIN SELECT * FROM emp WHERE id = 1 UNION SELECT * FROM emp WHERE age = 30; + EXPLAIN SELECT * FROM t_user u , user_role ur WHERE u.id = ur.user_id; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL OR条件查询2.png) + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL嵌套查询2.png) + + 连接查询之所以效率更高 ,是因为不需要在内存中创建临时表来完成逻辑上需要两个步骤的查询工作 -* UNION 要优于 OR 的原因: - * UNION 语句的 type 值为 ref,OR 语句的 type 值为 range - * UNION 语句的 ref 值为 const,OR 语句的 ref 值为 null,const 表示是常量值引用,非常快 @@ -5316,11 +5523,11 @@ MySQL 4.1版本之后,开始支持 SQL 的子查询 -#### 分页 +#### 分页查询 一般分页查询时,通过创建覆盖索引能够比较好地提高性能 -一个常见的问题是 `LIMIT 200000,10`,此时需要MySQL扫描前 200010 记录,仅仅返回200000 - 200010 之间的记录,其他记录丢弃,查询排序的代价非常大 +一个常见的问题是 `LIMIT 200000,10`,此时需要 MySQL 扫描前 200010 记录,仅仅返回 200000 - 200010 之间的记录,其他记录丢弃,查询排序的代价非常大 * 分页查询: @@ -5387,55 +5594,6 @@ SQL 提示,是优化数据库的一个重要手段,就是在SQL语句中加 -## 系统优化 - -### 应用优化 - -#### 减少访问 - -避免对数据进行重复检索:能够一次连接就获取到结果的,就不用两次连接,这样可以大大减少对数据库无用的重复请求 - -* 查询数据: - - ```mysql - SELECT id,name FROM tb_book; - SELECT id,status FROM tb_book; -- 向数据库提交两次请求,数据库就要做两次查询操作 - -- > 优化为: - SELECT id,name,statu FROM tb_book; - ``` - -* 插入数据: - - ```mysql - INSERT INTO tb_test VALUES(1,'Tom'); - INSERT INTO tb_test VALUES(2,'Cat'); - INSERT INTO tb_test VALUES(3,'Jerry'); -- 连接三次数据库 - -- >优化为 - INSERT INTO tb_test VALUES(1,'Tom'),(2,'Cat'),(3,'Jerry'); -- 连接一次 - ``` - -增加 cache 层:在应用中增加缓存层来达到减轻数据库负担的目的。可以部分数据从数据库中抽取出来放到应用端以文本方式存储, 或者使用框架(Mybatis)提供的一级缓存 / 二级缓存,或者使用 Redis 数据库来缓存数据 - - - -*** - - - -#### 负载均衡 - -负载均衡是应用中使用非常普遍的一种优化方法,机制就是利用某种均衡算法,将固定的负载量分布到不同的服务器上, 以此来降低单台服务器的负载,达到优化的效果 - -* 分流查询:通过 MySQL 的主从复制,实现读写分离,使增删改操作走主节点,查询操作走从节点,从而可以降低单台服务器的读写压力 - - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-负载均衡主从复制.jpg) - -* 分布式数据库架构:适合大数据量、负载高的情况,具有良好的拓展性和高可用性。通过在多台服务器之间分布数据,可以实现在多台服务器之间的负载均衡,提高访问效率 - - - -*** - ### 内存优化 @@ -5491,7 +5649,7 @@ Innodb 用一块内存区做 IO 缓存池,该缓存池不仅用来缓存 Innod SHOW VARIABLES LIKE 'innodb_buffer_pool_size'; ``` - 在保证操作系统及其他程序有足够内存可用的情况下,innodb_buffer_pool_size 的值越大,缓存命中率越高,访问InnoDB表需要的磁盘I/O 就越少,性能也就越高。通过配置文件修改: + 在保证操作系统及其他程序有足够内存可用的情况下,innodb_buffer_pool_size 的值越大,缓存命中率越高 ```sh innodb_buffer_pool_size=512M @@ -5507,6 +5665,8 @@ Innodb 用一块内存区做 IO 缓存池,该缓存池不仅用来缓存 Innod + + *** @@ -5541,10 +5701,14 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 + + *** + + ## 主从复制 ### 基本介绍 @@ -5632,6 +5796,22 @@ MySQL 的主从复制原理图: +*** + + + +### 负载均衡 + +负载均衡是应用中使用非常普遍的一种优化方法,机制就是利用某种均衡算法,将固定的负载量分布到不同的服务器上, 以此来降低单台服务器的负载,达到优化的效果 + +* 分流查询:通过 MySQL 的主从复制,实现读写分离,使增删改操作走主节点,查询操作走从节点,从而可以降低单台服务器的读写压力 + + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-负载均衡主从复制.jpg) + +* 分布式数据库架构:适合大数据量、负载高的情况,具有良好的拓展性和高可用性。通过在多台服务器之间分布数据,可以实现在多台服务器之间的负载均衡,提高访问效率 + + + **** @@ -5774,6 +5954,8 @@ MySQL 的主从复制原理图: + + ## 锁机制 ### 基本介绍 @@ -5808,6 +5990,34 @@ MySQL 的主从复制原理图: +*** + + + +### Server + +FLUSH TABLES WITH READ LOCK 简称(FTWRL),全局读锁,让整个库处于只读状态,工作流程: + +1. 上全局读锁(lock_global_read_lock) +2. 清理表缓存(close_cached_tables) +3. 上全局 COMMIT 锁(make_global_read_lock_block_commit) + +该命令主要用于备份工具做一致性备份,由于 FTWRL 需要持有两把全局的 MDL 锁,并且还要关闭所有表对象,因此杀伤性很大 + +MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL) + +MDL 叫元数据锁,主要用来保护 Mysql 内部对象的元数据,保证数据读写的正确性,通过 MDL 机制保证 DDL、DML、SELECT 操作的并发,当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁 + +* MDL 锁不需要显式使用,在访问一个表的时候会被自动加上,事务中的 MDL 锁,在语句执行开始时申请,在整个事务提交后释放 + +* MDL 锁是在 Server 中实现,不是 InnoDB 存储引擎层不能直接实现的锁 + +* MDL 锁还能实现其他粒度级别的锁,比如全局锁、库级别的锁、表空间级别的锁 + + + + + *** @@ -6350,6 +6560,8 @@ lock_id 是锁 id;lock_trx_id 为事务 id;lock_mode 为 X 代表排它锁 + + ## 日志 ### 日志分类 diff --git a/Prog.md b/Prog.md index 0e2eba0..9a788df 100644 --- a/Prog.md +++ b/Prog.md @@ -11375,7 +11375,7 @@ public V put(K key, V value) { return; // 所以最后一个线程退出的时候,sizeCtl 的低 16 位为 1 finishing = advance = true; - // 这里表示最后一个线程需要重新检查一遍是否有漏掉的 + // 【这里表示最后一个线程需要重新检查一遍是否有漏掉的 i = n; } } diff --git a/SSM.md b/SSM.md index 55767d7..5c21bbe 100644 --- a/SSM.md +++ b/SSM.md @@ -3180,9 +3180,9 @@ IoC 和 DI 的关系:IoC 与 DI 是同一件事站在不同角度看待问题 ##### 构造注入 -标签:标签,的子标签 +标签: 标签, 的子标签 -作用:使用构造方法的形式为bean提供资源,兼容早期遗留系统的升级工作 +作用:使用构造方法的形式为 bean 提供资源,兼容早期遗留系统的升级工作 格式: @@ -3196,10 +3196,10 @@ IoC 和 DI 的关系:IoC 与 DI 是同一件事站在不同角度看待问题 属性: * name:对应bean中的构造方法所携带的参数名 -* value:设定非引用类型构造方法参数对应的值,不能与ref同时使用 -* ref:设定引用类型构造方法参数对应bean的id ,不能与value同时使用 +* value:设定非引用类型构造方法参数对应的值,不能与 ref 同时使用 +* ref:设定引用类型构造方法参数对应 bean 的 id ,不能与 value 同时使用 * type :设定构造方法参数的类型,用于按类型匹配参数或进行类型校验 -* index :设定构造方法参数的位置,用于按位置匹配参数,参数index值从0开始计数 +* index :设定构造方法参数的位置,用于按位置匹配参数,参数 index 值从 0 开始计数 ```xml @@ -7713,7 +7713,7 @@ AbstractBeanFactory.doGetBean():获取 Bean,context.getBean() 追踪到此 * `mbd.getDependsOn()`:获取 bean 标签 depends-on -* `if(dependsOn != null)`:**遍历所有的依赖加载** +* `if(dependsOn != null)`:**遍历所有的依赖加载,解决不了循环依赖** `isDependent(beanName, dep)`:判断循环依赖,出现循环依赖问题报错 @@ -7756,7 +7756,7 @@ AbstractBeanFactory.doGetBean():获取 Bean,context.getBean() 追踪到此 * `this.singletonsCurrentlyInDestruction`:容器销毁时会设置这个属性为 true,这时就不能再创建 bean 实例了 - * `beforeSingletonCreation(beanName)`:检查构造参数的依赖,**构造参数产生的循环依赖无法解决** + * `beforeSingletonCreation(beanName)`:检查构造注入的依赖,**构造参数注入产生的循环依赖无法解决** `!this.singletonsCurrentlyInCreation.add(beanName)`:将当前 beanName 放入到正在创建中单实例集合,放入成功说明没有产生循环依赖,失败则产生循环依赖,进入判断条件内的逻辑抛出异常 @@ -12964,7 +12964,7 @@ public class HelloController { ```xml - < + org.springframework spring-context 5.1.9.RELEASE From 01ac470502cf604bf4113bbe48fb7b3a10300074 Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 26 Aug 2021 21:52:17 +0800 Subject: [PATCH 039/168] Update Java Notes --- DB.md | 372 +++++++++++++++++++++++++++++++++++++++++++++----------- Prog.md | 13 +- 2 files changed, 308 insertions(+), 77 deletions(-) diff --git a/DB.md b/DB.md index 5fb58b1..46781fc 100644 --- a/DB.md +++ b/DB.md @@ -573,7 +573,7 @@ select * from t where id = 1; ##### 扫描行数 -优化器是在表里面有多个索引的时候,决定使用哪个索引,找到一个最优的执行方案,用最小的代价去执行语句;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序 +优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。找到一个最优的执行方案,用最小的代价去执行语句 在数据库里面,扫描行数是影响执行代价的因素之一,扫描的行数越少意味着访问磁盘的次数越少,消耗的 CPU 资源越少,优化器还会结合是否使用临时表、是否排序等因素进行综合判断 @@ -586,7 +586,7 @@ MySQL 在真正执行语句之前,并不能精确地知道满足条件的记 * 设置为 on 时,表示统计信息会持久化存储,这时默认的 N 是 20,M 是 10 * 设置为 off 时,表示统计信息只存储在内存,这时默认的 N 是 8,M 是 16 -EXPLAIN 执行计划在优化器阶段生成,如果发现 explain 的结果预估的 rows 值跟实际情况差距比较大,可以执行 `analyze table t ` 重新修正信息 +EXPLAIN 执行计划在优化器阶段生成,如果发现 explain 的结果预估的 rows 值跟实际情况差距比较大,可以执行 `analyze table t ` 重新修正信息,只是对表的索引信息做重新统计(不是重建表),没有修改数据,这个过程中加了 MDL 读锁 @@ -630,6 +630,8 @@ EXPLAIN 执行计划在优化器阶段生成,如果发现 explain 的结果预 #### 数据存储 +系统表空间是用来放系统信息的,比如数据字典什么的,对应的磁盘文件是ibdata1,数据表空间是一个个的表数据文件,对应的磁盘文件就是表名.ibd + 表数据既可以存在共享表空间里,也可以是单独的文件,这个行为是由参数 innodb_file_per_table 控制的: * OFF:表示表的数据放在系统共享表空间,也就是跟数据字典放在一起 @@ -639,6 +641,8 @@ EXPLAIN 执行计划在优化器阶段生成,如果发现 explain 的结果预 + + *** @@ -659,11 +663,58 @@ InnoDB 的数据是按页存储的如果删掉了一个数据页上的所有记 -### 空间收缩 +#### 空间收缩 + +重建表就是按照主键 ID 递增的顺序,把数据一行一行地从旧表中读出来再插入到新表中,重建表时 MySQL 会自动完成转存数据、交换表名、删除旧表的操作,重建命令: + +```sql +ALTER TABLE A ENGINE=InnoDB +``` + +工作流程:新建临时表 tmp_table B(在 Server 层创建的),把表 A 中的数据导入到表 B 中,操作完成后用表 B 替换 表A,完成重建 + +重建表的步骤需要 DDL 不是 Online 的,因为在导入数据的过程有新的数据要写入到表 A 的话,就会造成数据丢失 + +MySQL 5.6 版本开始引入的 Online DDL,重建表的命令默认执行此步骤: + +* 建立一个临时文件 tmp_file(InnoDB 创建),扫描表 A 主键的所有数据页 +* 用数据页中表 A 的记录生成 B+ 树,存储到临时文件中 +* 生成临时文件的过程中,将所有对 A 的操作记录在一个日志文件(row log)中,对应的是图中 state2 的状态 +* 临时文件生成后,将日志文件中的操作应用到临时文件,得到一个逻辑数据上与表 A 相同的数据文件,对应的就是图中 state3 +* 用临时文件替换表 A 的数据文件 + + + +Online DDL 操作会先获取 MDL 写锁,再退化成 MDL 读锁。但 MDL 写锁持有时间比较短,所以可以称为 Online; 而 MDL 读锁,不阻止数据增删查改,但会阻止其它线程修改表结构 + +问题:想要收缩表空间,执行指令后整体占用空间增大 + +原因:在重建表后 InnoDB 不会把整张表占满,每个页留了 1/16 给后续的更新使用。表在未整理之前页已经占用 15/16 以上,收缩之后需要保持数据占用空间在 15/16 以下,所以文件占用空间更大才能保持 + +注意:临时文件也要占用空间,如果空间不足会重建失败 + + + +**** + + + +#### inplace + +DDL 中的临时表 tmp_table 是在 Server 层创建的,Online DDL 中的临时文件tmp_file 是 InnoDB 在内部创建出来的,整个 DDL 过程都在 InnoDB 内部完成,对于 Server 层来说,没有把数据挪动到临时表,是一个原地操作,这就是 inplace + +两者的关系: + +* DDL 过程如果是 Online 的,就一定是 inplace 的 +* inplace 的 DDL,有可能不是 Online 的,截止到 MySQL 8.0,全文索引(FULLTEXT)和空间索引 (SPATIAL ) 属于这种情况 + +参考文章:https://time.geekbang.org/column/article/72388 + + *** @@ -2556,7 +2607,7 @@ redo log 也需要在事务提交时将日志写入磁盘,但是比将内存 ##### 工作流程 -MySQL中还存在 binlog(二进制日志)也可以记录写操作并用于数据的恢复,二者的区别是: +MySQL中还存在 binlog(二进制日志)也可以记录写操作并用于数据的恢复,**保证数据不丢失**,二者的区别是: * 作用不同:redo log 是用于 crash recovery (故障恢复),保证 MySQL 宕机也不会影响持久性;binlog 是用于 point-in-time recovery 的,保证服务器可以基于时间点恢复数据,此外 binlog 还用于主从复制 @@ -2624,10 +2675,6 @@ InnoDB 刷脏页的控制策略: - - - - *** @@ -4682,6 +4729,8 @@ Innodb_xxxx:这几个参数只是针对InnoDB 存储引擎的,累加的算 #### 定位低效 +慢 SQL 由三种原因造成:索引没有设计好、SQL 语句没写好、MySQL 选错了索引 + 通过以下两种方式定位执行效率较低的 SQL 语句 * 慢日志查询: 慢查询日志在查询结束以后才记录,执行效率出现问题时查询日志并不能定位问题 @@ -4745,7 +4794,7 @@ EXPLAIN SELECT * FROM table_1 WHERE id = 1; | key | 表示实际使用的索引 | | key_len | 索引字段的长度 | | ref | 列与索引的比较,表示表的连接匹配条件,即哪些列或常量被用于查找索引列上的值 | -| rows | 扫描出的行数,表示MySQL根据表统计信息及索引选用情况,估算的找到所需的记录所需要读取的行数 | +| rows | 扫描出的行数,表示 MySQL 根据表统计信息及索引选用情况,**估算**的找到所需的记录扫描的行数 | | filtered | 按表条件过滤的行百分比 | | extra | 执行情况的说明和描述 | @@ -4965,7 +5014,7 @@ MySQL 提供了对 SQL 的跟踪, 通过 trace 文件能够进一步了解执 * 打开 trace,设置格式为 JSON,并设置 trace 最大能够使用的内存大小,避免解析过程中因为默认内存过小而不能够完整展示 ```mysql - SET optimizer_trace="enabled=on",end_markers_in_json=ON; + SET optimizer_trace="enabled=on",end_markers_in_json=ON; -- 会话内有效 SET optimizer_trace_max_mem_size=1000000; ``` @@ -5261,6 +5310,24 @@ EXPLAIN SELECT name,status,address,password FROM tb_seller WHERE name='小米科 -- >优化为 INSERT INTO tb_test VALUES(1,'Tom'),(2,'Cat'),(3,'Jerry'); -- 连接一次 ``` + +* 在事务中进行数据插入: + + ```mysql + start transaction; + INSERT INTO tb_test VALUES(1,'Tom'); + INSERT INTO tb_test VALUES(2,'Cat'); + INSERT INTO tb_test VALUES(3,'Jerry'); + commit; -- 手动提交,分段提交 + ``` + +* 数据有序插入: + + ```mysql + INSERT INTO tb_test VALUES(1,'Tom'); + INSERT INTO tb_test VALUES(2,'Cat'); + INSERT INTO tb_test VALUES(3,'Jerry'); + ``` 增加 cache 层:在应用中增加缓存层来达到减轻数据库负担的目的。可以部分数据从数据库中抽取出来放到应用端以文本方式存储, 或者使用框架(Mybatis)提供的一级缓存 / 二级缓存,或者使用 Redis 数据库来缓存数据 @@ -5270,7 +5337,7 @@ EXPLAIN SELECT name,status,address,password FROM tb_seller WHERE name='小米科 -#### 批量插入 +#### 数据插入 当使用 load 命令导入数据的时候,适当的设置可以提高导入的效率: @@ -5284,6 +5351,8 @@ LOAD DATA LOCAL INFILE = '/home/seazean/sql1.log' INTO TABLE `tb_user_1` FIELD T 1. 主键顺序插入:因为 InnoDB 类型的表是按照主键的顺序保存的,所以将导入的数据按照主键的顺序排列,可以有效的提高导入数据的效率,如果 InnoDB 表没有主键,那么系统会自动默认创建一个内部列作为主键。 + **主键是否连续对性能影响不大,只要是递增的就可以**,比如雪花算法产生的 ID 不是连续的,但是是递增的 + * 插入 ID 顺序排列数据: ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL插入ID顺序排列数据.png) @@ -5304,44 +5373,6 @@ LOAD DATA LOCAL INFILE = '/home/seazean/sql1.log' INTO TABLE `tb_user_1` FIELD T -*** - - - -#### INSERT - -当进行数据的 INSERT 操作的时候,可以考虑采用以下几种优化方案: - -* 如果需要同时对一张表插入很多行数据时,优化为一条插入语句,这种方式将大大的缩减客户端与数据库之间的连接、关闭等消耗 - - ```mysql - INSERT INTO tb_test VALUES(1,'Tom'); - INSERT INTO tb_test VALUES(2,'Cat'); - INSERT INTO tb_test VALUES(3,'Jerry'); -- 连接三次数据库 - -- 优化为 - INSERT INTO tb_test VALUES(1,'Tom'),(2,'Cat'),(3,'Jerry'); -- 连接一次 - ``` - -* 在事务中进行数据插入: - - ```mysql - start transaction; - INSERT INTO tb_test VALUES(1,'Tom'); - INSERT INTO tb_test VALUES(2,'Cat'); - INSERT INTO tb_test VALUES(3,'Jerry'); - commit; -- 手动提交,分段提交 - ``` - -* 数据有序插入: - - ```mysql - INSERT INTO tb_test VALUES(1,'Tom'); - INSERT INTO tb_test VALUES(2,'Cat'); - INSERT INTO tb_test VALUES(3,'Jerry'); - ``` - - - **** @@ -5362,7 +5393,7 @@ INSERT INTO `emp` (`id`, `name`, `age`, `salary`) VALUES('1','Tom','25','2300'); CREATE INDEX idx_emp_age_salary ON emp(age,salary); ``` -* 第一种是通过对返回数据进行排序,所有不是通过索引直接返回排序结果的排序都叫 FileSort 排序 +* 第一种是通过对返回数据进行排序,所有不是通过索引直接返回排序结果的排序都叫 FileSort 排序,会在内存中重新排序 ```mysql EXPLAIN SELECT * FROM emp ORDER BY age DESC; -- 年龄降序 @@ -5394,7 +5425,7 @@ Filesort 的优化:通过创建合适的索引,能够减少 Filesort 的出 对于 Filesort , MySQL 有两种排序算法: -* 两次扫描算法:MySQL4.1 之前,使用该方式排序。首先根据条件取出排序字段和行指针信息,然后在排序区 sort buffer 中排序,如果 sort buffer 不够,则在临时表 temporary table 中存储排序结果。完成排序后,再根据行指针回表读取记录,该操作可能会导致大量随机I/O操作 +* 两次扫描算法:MySQL4.1 之前,使用该方式排序。首先根据条件取出排序字段和行指针信息,然后在排序区 sort buffer 中排序,如果 sort buffer 不够,则在临时表 temporary table 中存储排序结果。完成排序后,再根据行指针**回表读取记录**,该操作可能会导致大量随机 I/O 操作 * 一次扫描算法:一次性取出满足条件的所有字段,然后在排序区 sort buffer 中排序后直接输出结果集。排序时内存开销较大,但是排序效率比两次扫描算法高 MySQL 通过比较系统变量 max_length_for_sort_data 的大小和 Query 语句取出的字段的大小,来判定使用哪种排序算法。如果前者大,则说明 sort buffer 空间足够,使用第二种优化之后的算法,否则使用第一种。 @@ -5483,7 +5514,7 @@ GROUP BY 也会进行排序操作,与 ORDER BY 相比,GROUP BY 主要只是 -*** +**** @@ -5590,6 +5621,45 @@ SQL 提示,是优化数据库的一个重要手段,就是在SQL语句中加 + + +*** + + + +#### 统计计数 + +在不同的 MySQL 引擎中,count(*) 有不同的实现方式: + +* MyISAM 引擎把一个表的总行数存在了磁盘上,因此执行 count(*) 的时候会直接返回这个数,效率很高,但不支持事务 +* show table status 命令通过采样估算可以快速获取,但是不准确 +* InnoDB 表执行 count(*) 会遍历全表,虽然结果准确,但会导致性能问题 + +解决方案: + +* 计数保存在 Redis 中,因为更新 MySQL 和 Redis 的操作不是原子的,会存在数据一致性的问题 + +* 计数直接放到数据库里单独的一张计数表中,利用事务解决计数精确问题: + + + + 会话 B 的读操作在 T3 执行的,这时更新事务还没有提交,所以计数值加 1 这个操作对会话 B 还不可见,因此会话 B 看到的结果里, 查计数值和最近 100 条记录看到的结果,逻辑上就是一致的 + + 并发系统性能的角度考虑,应该先插入操作记录再更新计数表,因为更新计数表涉及到行锁的竞争,**先插入再更新能最大程度地减少事务之间的锁等待,提升并发度** + +count 函数的按照效率排序:`count(字段) < count(主键id) < count(1) ≈ count(*)`,所以建议尽量使用 count(*) + +* count(主键 id):InnoDB 引擎会遍历整张表,把每一行的 id 值都取出来返回给 Server 层,Server 判断 id 不为空就按行累加 +* count(1):InnoDB 引擎遍历整张表但不取值,Server 层对于返回的每一行,放一个数字 1 进去,判断不为空就按行累加 + +* count(字段):如果这个字段是定义为 not null 的话,一行行地从记录里面读出这个字段,判断不能为 null,按行累加;如果这个字段定义允许为 null,那么执行的时候,判断到有可能是 null,还要把值取出来再判断一下,不是 null 才累加 + + + +参考文章:https://time.geekbang.org/column/article/72775 + + + *** @@ -5738,40 +5808,68 @@ MySQL 复制的优点主要包含以下三个方面: ### 复制原理 -MySQL 的主从复制原理图: +#### 主从结构 + +MySQL 的主从之间维持了一个长连接。主库内部有一个线程,专门用于服务从库的长连接,连接过程: + +* 从库执行 change master 命令,设置主库 A 的 IP、端口、用户名、密码,以及要从哪个位置开始请求 binlog,这个位置包含文件名和日志偏移量 +* 从库执行 start slave 命令,这时从库会启动两个线程,就是图中的 io_thread 和 sql_thread,其中 io_thread 负责与主库建立连接 +* 主库校验完用户名、密码后,开始按照从传过来的位置,从本地读取 binlog 发给从库,开始主从复制 + +主从复制原理图: ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-主从复制原理图.jpg) -主从复制主要依赖的是 binlog,MySQL 默认是异步复制,需要三个线程:**master(binlog dump thread)、slave(I/O thread 、SQL thread)** +主从复制主要依赖的是 binlog,MySQL 默认是异步复制,需要三个线程: -- binlog dump thread:在主库事务提交时,负责把数据变更作为事件 Events 记录在二进制日志文件 binlog 中,并通知 slave 有数据更新 -- I/O thread:负责从主服务器上拉取二进制日志,并将 binlog 日志内容依次写到 relay log 文件的最末端,并将新的 binlog 文件名和 offset 记录到 master-info 文件中,以便下一次读取日志时能告诉 master 服务器从指定 binlog 日志文件及位置开始读取新的 binlog 日志内容 +- binlog dump thread:在主库事务提交时,负责把数据变更记录在二进制日志文件 binlog 中,并通知 slave 有数据更新 +- I/O thread:负责从主服务器上拉取二进制日志,并将 binlog 日志内容依次写到 relay log 中转日志的最末端,并将新的 binlog 文件名和 offset 记录到 master-info 文件中,以便下一次读取日志时能告诉 master 服务器从指定 binlog 日志文件及位置开始读取新的 binlog 日志内容 - SQL thread:监测本地 relay log 中新增了日志内容,读取中继日志并重做其中的 SQL 语句 - 从库在 relay-log.info 中记录当前应用中继日志的文件名和位置点以便下一次执行 同步与异步: * 异步复制有数据丢失风险,例如数据还未同步到从库,主库就给客户端响应,然后主库挂了,此时从库晋升为主库的话数据是缺失的 -* 同步复制,主库需要将 binlog 复制到所有从库,等所有从库响应了之后才会给客户端响应,这样的话性能很差,一般不会选择同步复制 +* 同步复制,主库需要将 binlog 复制到所有从库,等所有从库响应了之后才会给客户端响应,这样的话性能很差,一般不会选择 * MySQL 5.7 之出现了半同步复制,有参数可以选择成功同步几个从库就返回响应 -并行复制:MySQL 5.6 版本增加了并行复制功能,为了改善复制延迟问题,在从库中有两个线程 IO Thread 和 SQL Thread,以采用多线程机制来促进执行,减少从库复制延迟 - **** +#### 主主结构 + +主主结构就是两个数据库之间总是互为主备关系,这样在切换的时候就不用再修改主备关系 + +循环复制:在库 A 上更新了一条语句,然后把生成的 binlog 发给库 B,库 B 执行完这条更新语句后也会生成 binlog,会再发给 A + +解决方法: + +* 两个库的 server id 必须不同,如果相同则它们之间不能设定为主备关系 +* 一个库接到 binlog 并在重放的过程中,生成与原 binlog 的 server id 相同的新的 binlog +* 每个库在收到从主库发过来的日志后,先判断 server id,如果跟自己的相同,表示这个日志是自己生成的,就直接丢弃这个日志 + + + +*** + + + ### 主从延迟 -主从延迟就是主从之间是存在一定时间的数据不一致: +#### 延迟原因 + +正常情况主库执行更新生成的所有 binlog,都可以传到备库并被正确地执行,备库就能达到跟主库一致的状态,这就是最终一致性 + +主从延迟是主从之间是存在一定时间的数据不一致,就是同一个事务在从库执行完成的时间和主库执行完成的时间的差值,即 T3-T1 - 主库 A 执行完成一个事务,写入 binlog,该时刻记为 T1 - 传给从库 B,从库接受完这个 binlog 的时刻记为 T2 - 从库 B 执行完这个事务,该时刻记为 T3 -同一个事务,从库执行完成的时间和主库执行完成的时间之间的差值,即 T3-T,通过在从库执行 `show slave status` 命令,返回结果会显示 seconds_behind_master 表示当前从库延迟了多少秒 +通过在从库执行 `show slave status` 命令,返回结果会显示 seconds_behind_master 表示当前从库延迟了多少秒 - 每一个事务的 binlog 都有一个时间字段,用于记录主库上写入的时间 - 从库取出当前正在执行的事务的时间字段,跟系统的时间进行相减,得到的就是 seconds_behind_master @@ -5786,14 +5884,105 @@ MySQL 的主从复制原理图: 主从同步问题永远都是**一致性和性能的权衡**,需要根据实际的应用场景,可以采取下面的办法: -* **二次查询**,如果从库查不到数据,则再去主库查一遍,由 API 封装,比较简单,但导致主库压力大 -* 降低多线程大事务并发的概率,优化业务逻辑 * 优化 SQL,避免慢 SQL,减少批量操作 -* 提高从库机器的配置,减少主库写 binlog 和从库读 binlog 的效率差 +* 降低多线程大事务并发的概率,优化业务逻辑 +* 业务中大多数情况查询操作要比更新操作更多,搭建一主多从结构,让这些从库来分担读的压力 + * 尽量采用短的链路,主库和从库服务器的距离尽量要短,提升端口带宽,减少 binlog 传输的网络延时 * 实时性要求高的业务读强制走主库,从库只做灾备,备份 + + + +*** + + + +#### 并行复制 + +##### MySQL5.6 + +高并发情况下,主库的会产生大量的 binlog,在从库中有两个线程 IO Thread 和 SQL Thread单线程执行,会导致主库延迟变大。为了改善复制延迟问题,MySQL 5.6 版本增加了并行复制功能,以采用多线程机制来促进执行 + +coordinator 就是原来的 sql_thread,并行复制中它不再直接更新数据,只负责读取中转日志和分发事务: + +* 线程分配完成并不是立即执行,为了防止造成更新覆盖,更新同一行的两个事务必须被分发到同一个工作线程 +* 同一个事务不能被拆开,必须放到同一个工作线程 + +MySQL 5.6 版本的策略:每个线程对应一个 hash 表,用于保存当前这个线程的执行队列里的事务所涉及的表,hash 表的 key 是数据库 DB 名,value 是一个数字,表示队列中有多少个事务修改这个库,适用于主库上有多个 DB 的情况 + +每个事务在分发的时候,跟线程的冲突(事务操作的是同一个 DB)关系包括以下三种情况: + +* 如果跟所有线程都不冲突,coordinator 线程就会把这个事务分配给最空闲的线程 +* 如果跟多于一个线程冲突,coordinator 线程就进入等待状态,直到和这个事务存在冲突关系的线程只剩下 1 个 +* 如果只跟一个线程冲突,coordinator 线程就会把这个事务分配给这个存在冲突关系的线程 + +优缺点: + +* 构造 hash 值的时候很快,只需要库名,而且一个实例上 DB 数也不会很多,不会出现需要构造很多个项的情况 +* 不要求 binlog 的格式,statement 格式的 binlog 也可以很容易拿到库名 +* 主库上的表都放在同一个 DB 里面,这个策略就没有效果了;或者不同 DB 的热点不同,比如一个是业务逻辑库,一个是系统配置库,那也起不到并行的效果,需要把相同热度的表均匀分到这些不同的 DB 中,才可以使用这个策略 + + + +*** + + + +##### MySQL5.7 + +MySQL 5.7 并行复制策略的思想是:所有处于 commit 状态的事务可以并行执行 + +* 同时处于 prepare 状态的事务,在备库执行时是可以并行的 +* 处于 prepare 状态的事务,与处于 commit 状态的事务之间,在备库执行时也是可以并行的 + +MySQL 5.7 由参数 slave-parallel-type 来控制并行复制策略: + +* 配置为 DATABASE,表示使用 MySQL 5.6 版本的**按库并行策略** +* 配置为 LOGICAL_CLOCK,表示的**按提交状态并行**执行 + +MySQL 5.7.22 版本里,MySQL 增加了一个新的并行复制策略,基于 WRITESET 的并行复制。新增了一个参数 binlog-transaction-dependency-tracking,用来控制是否启用这个新策略: + +* COMMIT_ORDER:表示根据同时进入 prepare 和 commit 来判断是否可以并行的策略 + +* WRITESET:表示的是对于事务涉及更新的每一行,计算出这一行的 hash 值,组成集合 writeset,如果两个事务没有操作相同的行,也就是说它们的 writeset 没有交集,就可以并行(**按行并行**) + +* WRITESET_SESSION:是在 WRITESET 的基础上多了一个约束,即在主库上同一个线程先后执行的两个事务,在备库执行的时候,要保证相同的先后顺序 + + 为了唯一标识,这个 hash 表的值是通过 `库名 + 表名 + 索引名 + 值` 计算出来的 + +MySQL 5.7.22 按行并发的优势: + +* writeset 是在主库生成后直接写入到 binlog 里面的,这样在备库执行的时候,不需要解析 binlog 内容,节省了计算量 +* 不需要把整个事务的 binlog 都扫一遍才能决定分发到哪个线程,更省内存 +* 从库的分发策略不依赖于 binlog 内容,所以 binlog 是 statement 格式也可以,更节约内存(因为 row 才记录更改的行) + +MySQL 5.7.22 的并行复制策略在通用性上是有保证的,但是对于表上没主键和外键约束的场景,WRITESET 策略也是没法并行的,也会暂时退化为单线程模型 + + + +参考文章:https://time.geekbang.org/column/article/77083 + + + +*** + + + +#### 读写分离 + +读写分离的主要目标就是分摊主库的压力,这也产生了读写延迟,造成数据的不一致性 + +假如客户端执行完一个更新事务后马上发起查询,如果查询选择的是从库的话,可能读到的还是以前的数据,叫过期读,解决方案: + * 强制将写之后**立刻读的操作转移到主库**,比如刚注册的用户,直接登录从库查询可能查询不到,先走主库登录 +* **二次查询**,如果从库查不到数据,则再去主库查一遍,由 API 封装,比较简单,但导致主库压力大 + +* 主库更新后,读从库之前先 sleep 一下,类似于执行一条 `select sleep(1)` 命令 +* 确保主备无延迟的方法,每次从库执行查询请求前,先判断 seconds_behind_master 是否已经等于 0,如果不等于那就等到这个参数变为 0 才能执行查询请求 + + + *** @@ -5816,9 +6005,11 @@ MySQL 的主从复制原理图: -### 搭建流程 +### 主从搭建 -#### master +#### 搭建流程 + +##### master 1. 在master 的配置文件(/etc/mysql/my.cnf)中,配置如下内容: @@ -5878,7 +6069,7 @@ MySQL 的主从复制原理图: -#### slave +##### slave 1. 在 slave 端配置文件中,配置如下内容: @@ -5890,7 +6081,7 @@ MySQL 的主从复制原理图: log-bin=/var/lib/mysql/mysqlbin ``` -2. 执行完毕之后,需要重启MySQL +2. 执行完毕之后,需要重启 MySQL 3. 指定当前从库对应的主库的IP地址、用户名、密码,从哪个日志文件开始的那个位置开始同步推送日志 @@ -5917,7 +6108,7 @@ MySQL 的主从复制原理图: -#### 验证 +##### 验证 1. 在主库中创建数据库,创建表并插入数据: @@ -5948,6 +6139,35 @@ MySQL 的主从复制原理图: +*** + + + +#### 主从切换 + +正常切换步骤: + +* 在开始切换之前先对主库进行锁表 `flush tables with read lock`,然后等待所有语句执行完成,切换完成后可以释放锁 + +* 检查 slave 同步状态,在 slave 执行 `show processlist` + +* 停止 slave io 线程,执行命令 `STOP SLAVE IO_THREAD` + +* 提升 slave 为 master + + ```sql + Stop slave; + Reset master; + Reset slave all; + set global read_only=off; -- 设置为可更新状态 + ``` + +* 将原来 master 变为 slave(参考搭建流程中的 slave 方法) + +主库发生故障,从库会进行上位,其他从库指向新的主库 + + + **** @@ -6006,7 +6226,7 @@ FLUSH TABLES WITH READ LOCK 简称(FTWRL),全局读锁,让整个库处 MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL) -MDL 叫元数据锁,主要用来保护 Mysql 内部对象的元数据,保证数据读写的正确性,通过 MDL 机制保证 DDL、DML、SELECT 操作的并发,当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁 +MDL 叫元数据锁,主要用来保护 MySQL内部对象的元数据,保证数据读写的正确性,通过 MDL 机制保证 DDL、DML、SELECT 操作的并发,**当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁** * MDL 锁不需要显式使用,在访问一个表的时候会被自动加上,事务中的 MDL 锁,在语句执行开始时申请,在整个事务提交后释放 @@ -6632,9 +6852,13 @@ binlog_format=STATEMENT 日志格式: -* STATEMENT:该日志格式在日志文件中记录的都是 SQL 语句,每一条对数据进行修改的 SQL都会记录在日志文件中,通过 mysqlbinlog 工具,可以查看到每条语句的文本。主从复制时,从库会将日志解析为原语句,并在从库重新执行一 +* STATEMENT:该日志格式在日志文件中记录的都是 SQL 语句,每一条对数据进行修改的 SQL都会记录在日志文件中,通过 mysqlbinlog 工具,可以查看到每条语句的文本。主从复制时,从库会将日志解析为原语句,并在从库重新执行一遍 + + 缺点:可能会导致主备不一致,因为记录的 SQL 在不同的环境中可能选择的索引不同,导致结果不同 * ROW:该日志格式在日志文件中记录的是每一行的数据变更,而不是记录 SQL 语句。比如执行 SQL 语句 `update tb_book set status='1'`,如果是 STATEMENT,在日志中会记录一行 SQL 语句; 如果是 ROW,由于是对全表进行更新,就是每一行记录都会发生变更,ROW 格式的日志中会记录每一行的数据变更 + 缺点:记录的数据比较多,占用很多的存储空间 + * MIXED:这是 MySQL 默认的日志格式,混合了STATEMENT 和 ROW两种格式。MIXED 格式能尽量利用两种模式的优点,而避开它们的缺点 @@ -6679,6 +6903,8 @@ mysqlbinlog log-file; ``` ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-日志读取1.png) + + 日志结尾有 COMMIT 查看 ROW 格式日志: @@ -6705,6 +6931,8 @@ mysqlbinlog log-file; + + *** @@ -6870,7 +7098,7 @@ long_query_time=10 **3NF:**在满足第二范式的基础上,表中的任何属性不依赖于其它非主属性,消除传递依赖。简而言之,**非主键都直接依赖于主键,而不是通过其它的键来间接依赖于主键**。 -作用:可以通过主键id区分相同数据,修改数据的时候只需要修改一张表(方便修改),反之需要修改多表。 +作用:可以通过主键 id 区分相同数据,修改数据的时候只需要修改一张表(方便修改),反之需要修改多表。 ![](https://gitee.com/seazean/images/raw/master/DB/第三范式.png) diff --git a/Prog.md b/Prog.md index 9a788df..3f1db03 100644 --- a/Prog.md +++ b/Prog.md @@ -2336,7 +2336,7 @@ public static void main(String[] args) throws InterruptedException { 1. 不允许一个线程无原因地(没有发生过任何 assign 操作)把数据从工作内存同步会主内存中 2. 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(assign 或者 load)的变量,即对一个变量实施 use 和 store 操作之前,必须先自行 assign 和 load 操作 -3. 一个变量在同一时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一线程重复执行多次,多次执行 lock 后,只有**执行相同次数的unlock**操作,变量才会被解锁,**lock和unlock必须成对出现** +3. 一个变量在同一时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一线程重复执行多次,多次执行 lock 后,只有**执行相同次数的 unlock** 操作,变量才会被解锁,**lock 和 unlock 必须成对出现** 4. 如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量之前需要重新执行 load 或 assign 操作初始化变量的值 5. 如果一个变量事先没有被 lock 操作锁定,则不允许对它执行 unlock 操作,也不允许去 unlock 一个被其他线程锁定的变量 6. 对一个变量执行 unlock 操作之前,必须先把此变量同步到主内存中(执行 store 和 write 操作) @@ -2469,9 +2469,9 @@ Linux 查看 CPU 缓存行: 单核 CPU 处理器会自动保证基本内存操作的原子性 -多核 CPU 处理器,每个 CPU 处理器内维护了一块内存,每个内核内部维护着一块缓存,当多线程并发读写时,就会出现缓存数据不一致的情况。处理器提供(平台级别): +多核 CPU 处理器,每个 CPU 处理器内维护了一块内存,每个内核内部维护着一块缓存,当多线程并发读写时,就会出现缓存数据不一致的情况。处理器提供: -* 总线锁定:当处理器要操作共享变量时,在 BUS 总线上发出一个 LOCK 信号,其他处理器就无法操作这个共享变量,该操作会导致大量阻塞,从而增加系统的性能开销 +* 总线锁定:当处理器要操作共享变量时,在 BUS 总线上发出一个 LOCK 信号,其他处理器就无法操作这个共享变量,该操作会导致大量阻塞,从而增加系统的性能开销(**平台级别的加锁**) * 缓存锁定:当处理器对缓存中的共享变量进行了操作,其他处理器有嗅探机制,将该共享变量的缓存失效,其他线程读取时会重新从主内存中读取最新的数据,基于 MESI 缓存一致性协议来实现 有如下两种情况处理器不会使用缓存锁定: @@ -2532,6 +2532,7 @@ volatile 是 Java 虚拟机提供的**轻量级**的同步机制(三大特性 ``` 执行顺序是:1 2 3 4、2 1 3 4、1 3 2 4 + 指令重排也有限制不会出现:4321,语句4需要依赖于 y 以及 x 的申明,因为存在数据依赖,无法首先执行 * example 2: @@ -2555,10 +2556,12 @@ volatile 是 Java 虚拟机提供的**轻量级**的同步机制(三大特性 ``` 情况一:线程1 先执行,ready = false,结果为 r.r1 = 1 + 情况二:线程2 先执行 num = 2,但还没执行 ready = true,线程1 执行,结果为 r.r1 = 1 + 情况三:线程2 先执行 ready = true,线程1 执行,进入 if 分支结果为 r.r1 = 4 - 情况四:线程2 执行 ready = true,切换到线程1,进入 if 分支为 r.r1 = 0,再切回线程2 执行 num = 2 - 发生指令重排 + + 情况四:线程2 执行 ready = true,切换到线程1,进入 if 分支为 r.r1 = 0,再切回线程2 执行 num = 2,发生指令重排 From 8dfec2c38747cfbe4f99b7f927edd8cd83af1f33 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 28 Aug 2021 11:37:28 +0800 Subject: [PATCH 040/168] Update Java Notes --- DB.md | 724 +++++++++++++++++++++++++++----------------------------- Java.md | 50 ++-- Prog.md | 6 +- 3 files changed, 382 insertions(+), 398 deletions(-) diff --git a/DB.md b/DB.md index 46781fc..61b911f 100644 --- a/DB.md +++ b/DB.md @@ -46,15 +46,15 @@ ### MySQL -MySQL数据库是一个最流行的关系型数据库管理系统之一 +MySQL 数据库是一个最流行的关系型数据库管理系统之一 关系型数据库是将数据保存在不同的数据表中,而且表与表之间可以有关联关系,提高了灵活性。 缺点:数据存储在磁盘中,导致读写性能差,而且数据关系复杂,扩展性差 -MySQL所使用的SQL语句是用于访问数据库最常用的标准化语言。 +MySQL 所使用的SQL语句是用于访问数据库最常用的标准化语言。 -MySQL配置: +MySQL 配置: * MySQL 安装:https://www.jianshu.com/p/ba48f1e386f0 @@ -356,7 +356,7 @@ mysqlshow -uroot -p1234 test book --count * Parser:SQL 语句分析器 * Optimizer:查询优化器 * Caches & Buffers:查询缓存,服务器会查询内部的缓存,如果缓存空间足够大,可以在大量读操作的环境中提升系统性能 - * 所有**跨存储引擎**的功能在这一层实现,如存储过程、触发器、视图等 + * 所有**跨存储引擎的功能**在这一层实现,如存储过程、触发器、视图等 * 在该层服务器会解析查询并创建相应的内部解析树,并对其完成相应的优化如确定表的查询顺序,是否利用索引等, 最后生成相应的执行操作 * MySQL 中服务器层不管理事务,**事务是由存储引擎实现的** - 第三层:存储引擎层 @@ -398,7 +398,7 @@ SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 | State | 显示使用当前连接的 sql 语句的状态,以查询为例,需要经过 copying to tmp table、sorting result、sending data等状态才可以完成 | | Info | 显示执行的 sql 语句,是判断问题语句的一个重要依据 | -连接建立 TCP 以后需要做权限验证,验证成功后可以进行执行 SQL。如果这时管理员账号对这个用户的权限做了修改,也不会影响已经存在连接的权限,只有再新建的连接才会使用新的权限设置 +连接建立 TCP 以后需要做**权限验证**,验证成功后可以进行执行 SQL。如果这时管理员账号对这个用户的权限做了修改,也不会影响已经存在连接的权限,只有再新建的连接才会使用新的权限设置 客户端如果太长时间没动静,连接器就会自动断开,这个时间是由参数 wait_timeout 控制的,默认值是 8 小时。如果在连接被断开之后,客户端再次发送请求的话,就会收到一个错误提醒:`Lost connection to MySQL server during query` @@ -427,13 +427,13 @@ SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 1. 客户端发送一条查询给服务器 2. 服务器先会检查查询缓存,如果命中了缓存,则立即返回存储在缓存中的结果(一般是 K-V 键值对),否则进入下一阶段 -3. 服务器端进行 SQL 分析,再由优化器生成对应的执行计划 +3. 分析器进行 SQL 分析,再由优化器生成对应的执行计划 4. MySQL 根据优化器生成的执行计划,调用存储引擎的 API 来执行查询 5. 将结果返回给客户端 大多数情况下不建议使用查询缓存,因为查询缓存往往弊大于利 -* 查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。因此很可能费力地把结果存起来,还没使用就被一个更新全清空了,对于更新压力大的数据库来说,查询缓存的命中率会非常低 +* 查询缓存的**失效非常频繁**,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。因此很可能费力地把结果存起来,还没使用就被一个更新全清空了,对于更新压力大的数据库来说,查询缓存的命中率会非常低 * 除非业务就是有一张静态表,很长时间才会更新一次,比如一个系统配置表,那这张表上的查询才适合使用查询缓存 @@ -542,7 +542,7 @@ SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 SELECT * FROM information_schema.engines; ``` -* 在存储过程、触发器或存储函数的主体内执行的查询,缓存失效 +* 在跨存储引擎的存储过程、触发器或存储函数的主体内执行的查询,缓存失效 * 如果表更改,则使用该表的所有高速缓存查询都将变为无效并从高速缓存中删除,包括使用 MERGE 映射到已更改表的表的查询,比如:INSERT、UPDATE、DELETE、ALTER TABLE、DROP TABLE、DROP DATABASE @@ -557,10 +557,10 @@ SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 没有命中查询缓存,就开始了 SQL 的真正执行,分析器会对 SQL 语句做解析 ```sql -select * from t where id = 1; +SELECT * FROM t WHERE id = 1; ``` -* 先做**词法分析**,输入的是由多个字符串和空格组成的一条 SQL 语句,MySQL 需要识别出里面的字符串分别是什么,代表什么。从输入的 select 这个关键字识别出来这是一个查询语句;把字符串 t 识别成 表名 T,把字符串 id 识别成列 id +* 先做**词法分析**,输入的是由多个字符串和空格组成的一条 SQL 语句,MySQL 需要识别出里面的字符串分别是什么,代表什么。从输入的 select 这个关键字识别出来这是一个查询语句;把字符串 t 识别成 表名 t,把字符串 id 识别成列 id * 然后做**语法分析**,根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法。如果你的语句不对,就会收到 `You have an error in your SQL syntax` 的错误提醒 @@ -579,7 +579,7 @@ select * from t where id = 1; MySQL 在真正执行语句之前,并不能精确地知道满足条件的记录有多少条,只能根据统计信息来估算记录,统计信息就是索引的区分度,一个索引上不同的值的个数(比如性别只能是男女,就是 2 ),称之为基数(cardinality),基数越大说明区分度越好 -* 通过采样统计来获取基数,InnoDB 默认会选择 N 个数据页,统计这些页面上的不同值得到一个平均值,然后乘以这个索引的页面数,就得到了这个索引的基数 +* 通过**采样统计**来获取基数,InnoDB 默认会选择 N 个数据页,统计这些页面上的不同值得到一个平均值,然后乘以这个索引的页面数,就得到了这个索引的基数 * 数据表是会持续更新的,索引统计信息也不会固定不变,当变更的数据行数超过 1/ M 的时候,会自动触发重新做一次索引统计 * 在 MySQL 中,有两种存储索引统计的方式,可以通过设置参数 innodb_stats_persistent 的值来选择: @@ -630,7 +630,7 @@ EXPLAIN 执行计划在优化器阶段生成,如果发现 explain 的结果预 #### 数据存储 -系统表空间是用来放系统信息的,比如数据字典什么的,对应的磁盘文件是ibdata1,数据表空间是一个个的表数据文件,对应的磁盘文件就是表名.ibd +系统表空间是用来放系统信息的,比如数据字典什么的,对应的磁盘文件是 ibdata,数据表空间是一个个的表数据文件,对应的磁盘文件就是表名.ibd 表数据既可以存在共享表空间里,也可以是单独的文件,这个行为是由参数 innodb_file_per_table 控制的: @@ -641,8 +641,6 @@ EXPLAIN 执行计划在优化器阶段生成,如果发现 explain 的结果预 - - *** @@ -655,7 +653,7 @@ MySQL 的数据删除就是移除掉某个记录后,该位置就被标记为 InnoDB 的数据是按页存储的如果删掉了一个数据页上的所有记录,整个数据页就可以被复用了,如果相邻的两个数据页利用率都很小,系统就会把这两个页上的数据合到其中一个页上,另外一个数据页就被标记为可复用 -删除命令其实只是把记录的位置,或者数据页标记为了**可复用**,但磁盘文件的大小是不会变的,这些可以复用还没有被使用的空间,看起来就像是空洞,造成数据库的稀疏,因此需要进行紧凑处理 +删除命令其实只是把记录的位置,或者**数据页标记为了可复用,但磁盘文件的大小是不会变的**,这些可以复用还没有被使用的空间,看起来就像是空洞,造成数据库的稀疏,因此需要进行紧凑处理 @@ -671,7 +669,7 @@ InnoDB 的数据是按页存储的如果删掉了一个数据页上的所有记 ALTER TABLE A ENGINE=InnoDB ``` -工作流程:新建临时表 tmp_table B(在 Server 层创建的),把表 A 中的数据导入到表 B 中,操作完成后用表 B 替换 表A,完成重建 +工作流程:新建临时表 tmp_table B(在 Server 层创建的),把表 A 中的数据导入到表 B 中,操作完成后用表 B 替换表 A,完成重建 重建表的步骤需要 DDL 不是 Online 的,因为在导入数据的过程有新的数据要写入到表 A 的话,就会造成数据丢失 @@ -689,7 +687,7 @@ Online DDL 操作会先获取 MDL 写锁,再退化成 MDL 读锁。但 MDL 写 问题:想要收缩表空间,执行指令后整体占用空间增大 -原因:在重建表后 InnoDB 不会把整张表占满,每个页留了 1/16 给后续的更新使用。表在未整理之前页已经占用 15/16 以上,收缩之后需要保持数据占用空间在 15/16 以下,所以文件占用空间更大才能保持 +原因:在重建表后 InnoDB 不会把整张表占满,每个页留了 1/16 给后续的更新使用。表在未整理之前页已经占用 15/16 以上,收缩之后需要保持数据占用空间在 15/16,所以文件占用空间更大才能保持 注意:临时文件也要占用空间,如果空间不足会重建失败 @@ -701,7 +699,7 @@ Online DDL 操作会先获取 MDL 写锁,再退化成 MDL 读锁。但 MDL 写 #### inplace -DDL 中的临时表 tmp_table 是在 Server 层创建的,Online DDL 中的临时文件tmp_file 是 InnoDB 在内部创建出来的,整个 DDL 过程都在 InnoDB 内部完成,对于 Server 层来说,没有把数据挪动到临时表,是一个原地操作,这就是 inplace +DDL 中的临时表 tmp_table 是在 Server 层创建的,Online DDL 中的临时文件 tmp_file 是 InnoDB 在内部创建出来的,整个 DDL 过程都在 InnoDB 内部完成,对于 Server 层来说,没有把数据挪动到临时表,是一个原地操作,这就是 inplace 两者的关系: @@ -731,7 +729,7 @@ DDL 中的临时表 tmp_table 是在 Server 层创建的,Online DDL 中的临 - Structured Query Language:结构化查询语言 - 定义了操作所有关系型数据库的规则,每种数据库操作的方式可能会存在不一样的地方,称为“方言” -- SQL通用语法 +- SQL 通用语法 - SQL 语句可以单行或多行书写,以**分号结尾**。 - 可使用空格和缩进来增强语句的可读性。 @@ -740,7 +738,7 @@ DDL 中的临时表 tmp_table 是在 Server 层创建的,Online DDL 中的临 - 单行注释:-- 注释内容 #注释内容(mysql特有) - 多行注释:/* 注释内容 */ -- SQL分类 +- SQL 分类 - DDL(Data Definition Language)数据定义语言 @@ -783,7 +781,7 @@ DDL 中的临时表 tmp_table 是在 Server 层创建的,Online DDL 中的临 ```sql SHOW CREATE DATABASE 数据库名称; -- 标准语法 - SHOW CREATE DATABASE mysql; --查看mysql数据库的创建格式 + SHOW CREATE DATABASE mysql; -- 查看mysql数据库的创建格式 ``` @@ -1877,13 +1875,11 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 ### 多表设计 -#### 多表介绍 +#### 一对一 -多表:有多张数据表,而表与表之间有一定的关联关系,通过外键约束实现 +多表:有多张数据表,而表与表之间有一定的关联关系,通过外键约束实现,分为一对一、一对多、多对多三类 -多表分类:一对一、一对多、多对多 -#### 一对一 举例:人和身份证 @@ -2100,7 +2096,7 @@ WHERE #### 自关联 -自关联查询:同一张表中有数据关联。可以多次查询这同一个表! +自关联查询:同一张表中有数据关联,可以多次查询这同一个表 * 数据准备 @@ -2256,7 +2252,7 @@ CREATE TABLE us_pro( u.id = o.uid; ``` -3. 查询用户年龄大于23岁的信息,显示用户的编号、姓名、年龄、订单编号。 +3. 查询用户年龄大于 23 岁的信息,显示用户的编号、姓名、年龄、订单编号。 ```mysql SELECT @@ -2298,10 +2294,9 @@ CREATE TABLE us_pro( ```` 5. 查询所有的用户和该用户能查看的所有的商品,显示用户的编号、姓名、年龄、商品名称。 - 分析: - 数据:用户的编号、姓名、年龄在user表,商品名称在product表,中间表 us_pro - 条件:us_pro.uid = user.id AND us_pro.pid = product.id - + 数据:用户的编号、姓名、年龄在user表,商品名称在product表,中间表 us_pro + 条件:us_pro.uid = user.id AND us_pro.pid = product.id + ```mysql SELECT u.id, @@ -2317,7 +2312,7 @@ CREATE TABLE us_pro( AND up.pid=p.id; ``` - + 6. 查询张三和李四这两个用户可以看到的商品,显示用户的编号、姓名、年龄、商品名称。 ```mysql @@ -2354,7 +2349,7 @@ CREATE TABLE us_pro( ### 事务介绍 -事务(Transaction)是访问和更新数据库的程序执行单元;事务中可能包含一个或多个sql语句,这些语句要么都执行,要么都不执行。作为一个关系型数据库,MySQL支持事务。 +事务(Transaction)是访问和更新数据库的程序执行单元;事务中可能包含一个或多个 sql 语句,这些语句要么都执行,要么都不执行。作为一个关系型数据库,MySQL 支持事务。 单元中的每条 SQL 语句都相互依赖,形成一个整体 @@ -2374,9 +2369,9 @@ CREATE TABLE us_pro( 1. 开启事务:记录回滚点,并通知服务器,将要执行一组操作,要么同时成功、要么同时失败 -2. 执行 SQL 语句:执行具体的一条或多条 SQL语句 +2. 执行 SQL 语句:执行具体的一条或多条 SQL 语句 -3. 结束事务(提交|回滚) +3. 结束事务(提交|回滚) - 提交:没出现问题,数据进行更新 - 回滚:出现问题,数据恢复到开启事务时的状态 @@ -2404,8 +2399,8 @@ CREATE TABLE us_pro( 工作原理: - * 在自动提交模式下,如果没有 start transaction 显式地开始一个事务,那么每个 SQL 语句都会被当做一个事务执行提交操作 - * 在手动提交模式下,所有的 SQL 语句都在一个事务中,直到执行了 commit 或 rollback,该事务结束,同时开始了另外一个事务 + * 自动提交模式下,如果没有 start transaction 显式地开始一个事务,那么每个 SQL 语句都会被当做一个事务执行提交操作 + * 手动提交模式下,所有的 SQL 语句都在一个事务中,直到执行了 commit 或 rollback,该事务结束的同时开启另外一个事务 * 存在一些特殊的命令,在事务中执行了这些命令会马上强制执行 COMMIT 提交事务,如 DDL 语句 (create/drop/alter/table)、lock tables 语句等 @@ -2414,7 +2409,7 @@ CREATE TABLE us_pro( - 查看事务提交方式 ```mysql - SELECT @@AUTOCOMMIT; -- 1代表自动提交 0代表手动提交 + SELECT @@AUTOCOMMIT; -- 1 代表自动提交 0 代表手动提交 ``` - 修改事务提交方式 @@ -2551,7 +2546,7 @@ rollback segment 称为回滚段,每个回滚段中有 1024 个 undo log segme 持久性是指一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,接下来的其他操作或故障不应该对其有任何影响。 -Buffer Pool 是一片内存空间,可以通过 innodb_buffer_pool_size 来控制 Buffer Pool 的大小(内存优化部分会详解) +Buffer Pool 是一片内存空间,可以通过 innodb_buffer_pool_size 来控制 Buffer Pool 的大小(内存优化部分会详解参数) * Change Buffer 是 Buffer Pool 里的内存,不能无限增大,用来对增删改操作提供缓存 * Change Buffer 的大小可以通过参数 innodb_change_buffer_max_size 来动态设置,设置为 50 时表示 Change Buffer 的大小最多只能占用 Buffer Pool 的 50% @@ -2577,14 +2572,14 @@ Buffer Pool 的使用提高了读写数据的效率,但是也带了新的问 redo log **记录数据页的物理修改**,而不是某一行或某几行的修改,用来恢复提交后的物理数据页,且只能恢复到最后一次提交的位置 -redo log 采用的是 WAL(Write-ahead logging,**预写式日志**),所有修改要先写入日志,再更新到磁盘,保证了数据不会因 MySQL 宕机而丢失,从而满足了持久性要求,InnoDB 引擎会在适当的时候,将这个操作记录更新到磁盘里面 +redo log 采用的是 WAL(Write-ahead logging,**预写式日志**),所有修改要先写入日志,再更新到磁盘,保证了数据不会因 MySQL 宕机而丢失,从而满足了持久性要求 redo log 也需要在事务提交时将日志写入磁盘,但是比将内存中的 Buffer Pool 修改的数据写入磁盘的速度快: * 刷脏是随机 IO,因为每次修改的数据位置随机,但写 redo log 是尾部追加操作,属于顺序 IO * 刷脏是以数据页(Page)为单位的,一个页上的一个小修改都要整页写入,而 redo log 中只包含真正需要写入的部分,减少无效 IO -刷盘策略,就是把内存中 redo log buffer 持久化到磁盘: +InnoDB 引擎会在适当的时候,把内存中 redo log buffer 持久化到磁盘,具体的刷盘策略: * 通过修改参数 `innodb_flush_log_at_trx_commit` 设置: * 0:表示当提交事务时,并不将缓冲区的 redo 日志写入磁盘,而是等待主线程每秒刷新一次 @@ -2594,10 +2589,10 @@ redo log 也需要在事务提交时将日志写入磁盘,但是比将内存 刷脏策略: +* redo log 文件是固定大小的,如果写满了就要擦除以前的记录,在擦除之前需要把旧记录更新到磁盘中的数据文件中 * 系统内存不足,需要淘汰部分数据页,如果淘汰的是脏页,就要先将脏页写到磁盘 * 系统空闲时,后台线程会自动进行刷脏 * MySQL 正常关闭时,会把内存的脏页都 flush 到磁盘上 -* InnoDB 的 redo log 文件是固定大小的,如果写满了就要擦除以前的记录,在擦除之前需要把旧记录更新到磁盘中的数据文件中 @@ -2665,7 +2660,7 @@ InnoDB 刷脏页的控制策略: * `innodb_io_capacity` 参数代表磁盘的读写能力,建议设置成磁盘的 IOPS(每秒的 IO 次数) * 刷脏速度参考两个因素:脏页比例和 redo log 写盘速度 - * 参数`innodb_max_dirty_pages_pct` 是脏页比例上限,默认值是 75%,InnoDB 会根据当前的脏页比例,算出一个范围在 0 到 100 之间的数字 + * 参数 `innodb_max_dirty_pages_pct` 是脏页比例上限,默认值是 75%,InnoDB 会根据当前的脏页比例,算出一个范围在 0 到 100 之间的数字 * InnoDB 每次写入的日志都有一个序号,当前写入的序号跟 checkpoint 对应的序号之间的差值,InnoDB 根据差值算出一个范围在 0 到 100 之间的数字 * 两者较大的值记为 R,执行引擎按照 innodb_io_capacity 定义的能力乘以 R% 来控制刷脏页的速度 @@ -2702,7 +2697,7 @@ InnoDB 刷脏页的控制策略: > 可重复读的意思是不管读几次,结果都一样,可以重复的读,可以理解为快照读,要读的数据集不会发生变化 -* 幻读 (Phantom Reads):在事务中按某个条件先后两次查询数据库,两次查询结果的数量不同,**数据条目**发生了变化。比如查询某数据不存在,准备插入此记录,但执行插入时发现此记录已存在,无法插入;或查询某数据不存在,执行删除操作却发现删除成功 +* 幻读 (Phantom Reads):在事务中按某个条件先后两次查询数据库,后一次查询查到了前一次查询没有查到的行,**数据条目**发生了变化。比如查询某数据不存在,准备插入此记录,但执行插入时发现此记录已存在,无法插入 **隔离级别操作语法:** @@ -2729,12 +2724,12 @@ InnoDB 刷脏页的控制策略: #### MVCC -MVCC 全称 Multi-Version Concurrency Control,即多版本并发控制,用来解决**读写冲突**的无锁并发控制 +MVCC 全称 Multi-Version Concurrency Control,即多版本并发控制,用来**解决读写冲突的无锁并发控制** MVCC 处理读写请求,可以做到在发生读写请求冲突时不用加锁,这个读是指的快照读,而不是当前读 * 快照读:实现基于 MVCC,因为是多版本并发,所以快照读读到的数据不一定是当前最新的数据,有可能是历史版本的数据 -* 当前读:读取数据库记录是当前最新的版本,会对读取的数据进行加锁,防止其他事务修改数据,是悲观锁的一种操作,读写操作加共享锁或者排他锁和串行化事务的隔离级别都是当前读 +* 当前读:读取数据库记录是当前最新的版本(产生幻读、不可重复读),可以对读取的数据进行加锁,防止其他事务修改数据,是悲观锁的一种操作,读写操作加共享锁或者排他锁和串行化事务的隔离级别都是当前读 数据库并发场景: @@ -2772,11 +2767,11 @@ MVCC 的优点: 数据库中的每行数据,除了自定义的字段,还有数据库隐式定义的字段: -* DB_TRX_ID:6byte,最近修改事务ID,记录创建该数据或最后一次修改(修改/插入)该数据的事务ID。当每个事务开启时,都会被分配一个ID,这个ID是递增的 +* DB_TRX_ID:6byte,最近修改事务ID,记录创建该数据或最后一次修改(修改/插入)该数据的事务ID。当每个事务开启时,都会被分配一个ID,这个 ID 是递增的 * DB_ROLL_PTR:7byte,回滚指针,配合 undo 日志,指向上一个旧版本(存储在 rollback segment) -* DB_ROW_ID:6byte,隐含的自增ID(**隐藏主键**),如果数据表没有主键,InnoDB 会自动以 DB_ROW_ID 产生一个聚簇索引 +* DB_ROW_ID:6byte,隐含的自增ID(**隐藏主键**),如果数据表没有主键,InnoDB 会自动以 DB_ROW_ID 作为聚簇索引 * DELETED_BIT:删除标志的隐藏字段,记录被更新或删除并不代表真的删除,而是删除位变了 @@ -2799,7 +2794,7 @@ undo log 是逻辑日志,记录的是每个事务对数据执行的操作, undo log 的作用: * 保证事务进行 rollback 时的原子性和一致性,当事务进行回滚的时候可以用 undo log 的数据进行恢复 -* 用于 MVCC 快照读的数据,在 MVCC 多版本控制中,通过读取 undo log 的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据版本 +* 用于 MVCC 快照读,通过读取 undo log 的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据版本 undo log 主要分为两种: @@ -2831,14 +2826,14 @@ undo log 主要分为两种: Read View 是事务进行**快照读**操作时产生的读视图,该事务执行快照读的那一刻会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的 ID,用来做可见性判断,根据视图判断当前事务能够看到哪个版本的数据 -注意:这里的快照并不是把所有的数据拷贝一份副本,而是由 undo log 记录的逻辑日志 +注意:这里的快照并不是把所有的数据拷贝一份副本,而是由 undo log 记录的逻辑日志,根据库中的数据进行计算出历史数据 工作流程:将版本链的头节点的事务 ID(最新数据事务 ID)DB_TRX_ID 取出来,与系统当前活跃事务的 ID 对比,如果 DB_TRX_ID 不符合可见性,通过 DB_ROLL_PTR 回滚指针去取出 undo log 中的下一个 DB_TRX_ID 比较,直到找到最近的满足特定条件的 DB_TRX_ID,该事务 ID 所在的旧记录就是当前事务能看见的最新的记录 Read View 几个属性: - m_ids:生成 Read View 时当前系统中活跃的事务 id 列表(未提交的事务集合,当前事务也在其中) -- up_limit_id:生成 Read View 时当前系统中活跃的最小的事务 id,也就是 m_ids 中的最小值(小于该值的都是已提交的事务) +- up_limit_id:生成 Read View 时当前系统中活跃的最小的事务 id,也就是 m_ids 中的最小值(已提交的事务集合) - low_limit_id:生成 Read View 时系统应该分配给下一个事务的 id 值,m_ids 中的最大值加 1(未开始事务) - creator_trx_id:生成该 Read View 的事务的事务 id,就是判断该 id 的事务能读到什么数据 @@ -2924,7 +2919,7 @@ RC、RR 级别下的 InnoDB 快照读区别 解决幻读问题: -- 快照读:通过 MVCC 来进行控制的,不用加锁,当前事务只生成一次 Read View,外部操作没有影响 +- 快照读:通过 MVCC 来进行控制的,在可重复读隔离级别下,普通查询是快照读,是不会看到别的事务插入的数据的,幻读只在当前读下才会出现 - 当前读:通过 next-key 锁(行锁 + 间隙锁)来解决问题 @@ -3134,9 +3129,9 @@ RC、RR 级别下的 InnoDB 快照读区别 DELIMITER: -* DELIMITER关键字用来声明sql语句的分隔符,告诉MySQL该段命令已经结束 +* DELIMITER 关键字用来声明 sql 语句的分隔符,告诉 MySQL 该段命令已经结束 -* MySQL语句默认的分隔符是分号,但是有时需要一条功能sql语句中包含分号,但是并不作为结束标识,这时使用DELIMITER来指定分隔符: +* MySQL 语句默认的分隔符是分号,但是有时需要一条功能 sql 语句中包含分号,但是并不作为结束标识,这时使用 DELIMITER 来指定分隔符: ```mysql DELIMITER 分隔符 @@ -3190,7 +3185,7 @@ DELIMITER: 4 赵六 26 女 90 ``` -* 创建stu_group()存储过程,封装分组查询总成绩,并按照总成绩升序排序的功能 +* 创建 stu_group() 存储过程,封装分组查询总成绩,并按照总成绩升序排序的功能 ```mysql DELIMITER $ @@ -3220,7 +3215,7 @@ DELIMITER: 存储过程是可以进行编程的,意味着可以使用变量、表达式、条件控制语句等,来完成比较复杂的功能 -* 定义变量:DECLARE定义的是局部变量,只能用在BEGIN END范围之内 +* 定义变量:DECLARE 定义的是局部变量,只能用在 BEGIN END 范围之内 ```mysql DECLARE 变量名 数据类型 [DEFAULT 默认值]; @@ -3233,7 +3228,7 @@ DELIMITER: SELECT 列名 INTO 变量名 FROM 表名 [WHERE 条件]; ``` -* 数据准备:表student +* 数据准备:表 student ```mysql id NAME age gender score @@ -3243,7 +3238,7 @@ DELIMITER: 4 赵六 26 女 90 ``` -* 定义两个int变量,用于存储男女同学的总分数 +* 定义两个 int 变量,用于存储男女同学的总分数 ```mysql DELIMITER $ @@ -3271,7 +3266,7 @@ DELIMITER: ##### IF语句 -* if语句标准语法 +* if 语句标准语法 ```mysql IF 判断条件1 THEN 执行的sql语句1; @@ -3281,7 +3276,7 @@ DELIMITER: END IF; ``` -* 数据准备:表student +* 数据准备:表 student ```mysql id NAME age gender score @@ -3291,7 +3286,7 @@ DELIMITER: 4 赵六 26 女 90 ``` -* 根据总成绩判断:全班380分及以上学习优秀、320 ~ 380学习良好、320以下学习一般 +* 根据总成绩判断:全班 380 分及以上学习优秀、320 ~ 380 学习良好、320 以下学习一般 ```mysql DELIMITER $ @@ -3325,7 +3320,7 @@ DELIMITER: * 参数传递的语法 - IN:代表输入参数,需要由调用者传递实际数据。默认的 + IN:代表输入参数,需要由调用者传递实际数据,默认的 OUT:代表输出参数,该参数可以作为返回值 INOUT:代表既可以作为输入参数,也可以作为输出参数 @@ -3368,8 +3363,8 @@ DELIMITER: * 查看参数方法 - * @变量名 : 这种变量要在变量名称前面加上“@”符号,叫做用户会话变量,代表整个会话过程他都是有作用的,类似于全局变量 - * @@变量名 : 这种在变量前加上 "@@" 符号, 叫做系统变量 + * @变量名 : **用户会话变量**,代表整个会话过程他都是有作用的,类似于全局变量 + * @@变量名 : **系统变量** @@ -3379,7 +3374,7 @@ DELIMITER: ##### CASE -* 标准语法1 +* 标准语法 1 ```mysql CASE 表达式 @@ -3390,7 +3385,7 @@ DELIMITER: END CASE; ``` -* 标准语法2 +* 标准语法 2 ```mysql sCASE @@ -3436,7 +3431,7 @@ DELIMITER: ##### WHILE -* while循环语法 +* while 循环语法 ```mysql WHILE 条件判断语句 DO @@ -3445,7 +3440,7 @@ DELIMITER: END WHILE; ``` -* 计算1~100之间的偶数和 +* 计算 1~100 之间的偶数和 ```mysql DELIMITER $ @@ -3479,7 +3474,7 @@ DELIMITER: ##### REPEAT -* repeat循环标准语法 +* repeat 循环标准语法 ```mysql 初始化语句; @@ -3490,7 +3485,7 @@ DELIMITER: END REPEAT; ``` -* 计算1~10之间的和 +* 计算 1~10 之间的和 ```mysql DELIMITER $ @@ -3529,7 +3524,7 @@ DELIMITER: LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定义,通常可以使用 LEAVE 语句实现,如果不加退出循环的语句,那么就变成了死循环 -* loop循环标准语法 +* loop 循环标准语法 ```mysql [循环名称:] LOOP @@ -3540,7 +3535,7 @@ LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定 END LOOP 循环名称; ``` -* 计算1~10之间的和 +* 计算 1~10 之间的和 ```mysql DELIMITER $ @@ -3580,7 +3575,7 @@ LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定 游标是用来存储查询结果集的数据类型,在存储过程和函数中可以使用光标对结果集进行循环的处理 * 游标可以遍历返回的多行结果,每次拿到一整行数据 * 简单来说游标就类似于集合的迭代器遍历 -* MySQL中的游标只能用在存储过程和函数中 +* MySQL 中的游标只能用在存储过程和函数中 游标的语法 @@ -3686,9 +3681,9 @@ LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定 #### 存储函数 -存储函数和存储过程是非常相似的。存储函数可以做的事情,存储过程也可以做到! +存储函数和存储过程是非常相似的,存储函数可以做的事情,存储过程也可以做到 -存储函数有返回值,存储过程没有返回值(参数的out其实也相当于是返回数据了) +存储函数有返回值,存储过程没有返回值(参数的 out 其实也相当于是返回数据了) * 创建存储函数 @@ -3748,7 +3743,7 @@ LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定 #### 基本介绍 -触发器是与表有关的数据库对象,在insert/update/delete 之前或之后触发并执行触发器中定义的SQL语句 +触发器是与表有关的数据库对象,在 insert/update/delete 之前或之后触发并执行触发器中定义的 SQL 语句 * 触发器的这种特性可以协助应用在数据库端确保数据的完整性 、日志记录 、数据校验等操作 @@ -3838,7 +3833,7 @@ LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定 ); ``` -* 创建INSERT型触发器 +* 创建 INSERT 型触发器 ```mysql DELIMITER $ @@ -3868,7 +3863,7 @@ LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定 -* 创建UPDATE型触发器 +* 创建 UPDATE 型触发器 ```mysql DELIMITER $ @@ -3899,7 +3894,7 @@ LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定 -* 创建DELETE型触发器 +* 创建 DELETE 型触发器 ```mysql DELIMITER $ @@ -3945,12 +3940,12 @@ LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定 存储引擎的介绍: -- MySQL 数据库使用不同的机制存取表文件 , 机制的差别在于不同的存储方式、索引技巧、锁定水平等不同的功能和能力,在 MySQL 中,将这些不同的技术及配套的功能称为**存储引擎** -- Oracle , SqlServer 等数据库只有一种存储引擎,MySQL 提供了插件式的存储引擎架构,所以 MySQL 存在多种存储引擎 , 就会让数据库采取了不同的处理数据的方式和扩展功能 +- MySQL 数据库使用不同的机制存取表文件 , 机制的差别在于不同的存储方式、索引技巧、锁定水平等不同的功能和能力,在 MySQL 中,将这些不同的技术及配套的功能称为存储引擎 +- Oracle、SqlServer 等数据库只有一种存储引擎,MySQL **提供了插件式的存储引擎架构**,所以 MySQL 存在多种存储引擎 , 就会让数据库采取了不同的处理数据的方式和扩展功能 - 在关系型数据库中数据的存储是以表的形式存进行,所以存储引擎也称为表类型(存储和操作此表的类型) - 通过选择不同的引擎,能够获取最佳的方案, 也能够获得额外的速度或者功能,提高程序的整体效果。 -MySQL支持的存储引擎: +MySQL 支持的存储引擎: - MySQL 支持的引擎包括:InnoDB、MyISAM、MEMORY、Archive、Federate、CSV、BLACKHOLE 等 - MySQL5.5 之前的默认存储引擎是 MyISAM,5.5 之后就改为了 InnoDB @@ -3971,7 +3966,7 @@ MyISAM 存储引擎: * 每个 MyISAM 在磁盘上存储成 3 个文件,其文件名都和表名相同,拓展名不同 * 表的定义保存在 .frm 文件,表数据保存在 .MYD (MYData) 文件中,索引保存在 .MYI (MYIndex) 文件中 -InnoDB 存储引擎:(MySQL5.5版本后默认的存储引擎) +InnoDB 存储引擎:(MySQL5.5 版本后默认的存储引擎) - 特点:**支持事务**和外键操作,支持并发控制。对比 MyISAM 的存储引擎,InnoDB 写的处理效率差一些,并且会占用更多的磁盘空间以保留数据和索引 - 应用场景:对事务的完整性有比较高的要求,在并发条件下要求数据的一致性,读写频繁的操作 @@ -3997,7 +3992,7 @@ MERGE存储引擎: * 操作方式: * 插入操作是通过 INSERT_METHOD 子句定义插入的表,使用 FIRST 或 LAST 值使得插入操作被相应地作用在第一或者最后一个表上;不定义这个子句或者定义为 NO,表示不能对 MERGE 表执行插入操作 - * 对 MERGE 表进行 DROP 操作,但是这个操作只是删除MERGE表的定义,对内部的表是没有任何影响的 + * 对 MERGE 表进行 DROP 操作,但是这个操作只是删除 MERGE 表的定义,对内部的表是没有任何影响的 ```mysql CREATE TABLE order_1( @@ -4099,7 +4094,7 @@ MERGE存储引擎: #### 基本介绍 -MySQL 官方对索引的定义为:索引(index)是帮助 MySQL 高效获取数据的一种数据结构,本质是排好序的快速查找数据结构。在表数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式指向数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。 +MySQL 官方对索引的定义为:索引(index)是帮助 MySQL 高效获取数据的一种数据结构,**本质是排好序的快速查找数据结构。**在表数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式指向数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。 **索引是在存储引擎层实现的**,所以并没有统一的索引标准,即不同存储引擎的索引的工作方式并不一样 @@ -4137,19 +4132,19 @@ MySQL 官方对索引的定义为:索引(index)是帮助 MySQL 高效获 - 外键索引:只有 InnoDB 引擎支持外键索引,用来保证数据的一致性、完整性和实现级联操作 - 结构分类 - - BTree 索引:MySQL 使用最频繁的一个索引数据结构,是 InnoDB 和 MyISAM 存储引擎默认的索引类型,底层基于 B+Tree 数据结构 + - BTree 索引:MySQL 使用最频繁的一个索引数据结构,是 InnoDB 和 MyISAM 存储引擎默认的索引类型,底层基于 B+Tree - Hash 索引:MySQL中 Memory 存储引擎默认支持的索引类型 - R-tree 索引(空间索引):空间索引是 MyISAM 引擎的一个特殊索引类型,主要用于地理空间数据类型 - Full-text 索引(全文索引):快速匹配全部文档的方式。MyISAM 支持, InnoDB 不支持 FULLTEXT 类型的索引,但是 InnoDB 可以使用 sphinx 插件支持全文索引,MEMORY 引擎不支持 - | 索引 | InnoDB引擎 | MyISAM引擎 | Memory引擎 | - | ----------- | --------------- | ---------- | ---------- | - | BTREE索引 | 支持 | 支持 | 支持 | - | HASH 索引 | 不支持 | 不支持 | 支持 | - | R-tree 索引 | 不支持 | 支持 | 不支持 | - | Full-text | 5.6版本之后支持 | 支持 | 不支持 | + | 索引 | InnoDB引擎 | MyISAM引擎 | Memory引擎 | + | --------- | --------------- | ---------- | ---------- | + | BTREE | 支持 | 支持 | 支持 | + | HASH | 不支持 | 不支持 | 支持 | + | R-tree | 不支持 | 支持 | 不支持 | + | Full-text | 5.6版本之后支持 | 支持 | 不支持 | -组合索引图示:根据身高年龄建立的组合索引(height,age) +联合索引图示:根据身高年龄建立的组合索引(height,age) ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-组合索引图.png) @@ -4169,9 +4164,9 @@ MySQL 官方对索引的定义为:索引(index)是帮助 MySQL 高效获 * 聚簇索引的叶子节点存放的是主键值和数据行,支持覆盖索引 -* 非聚簇索引的叶子节点存放的是主键值或指向数据行的指针 +* 非聚簇索引的叶子节点存放的是主键值或指向数据行的指针(由存储引擎决定) -在 Innodb 下主键索引是聚簇索引,在 Myisam 下主键索引是非聚簇索引 +在 Innodb 下主键索引是聚簇索引,在 MyISAM 下主键索引是非聚簇索引 @@ -4185,7 +4180,7 @@ MySQL 官方对索引的定义为:索引(index)是帮助 MySQL 高效获 在 Innodb 存储引擎,B+ 树索引可以分为聚簇索引(也称聚集索引、clustered index)和辅助索引(也称非聚簇索引或二级索引、secondary index、non-clustered index) -InnoDB 中,聚簇索引是按照每张表的主键构造一颗 B+ 树,同时叶子节点中存放的就是整张表的行记录数据,也将聚簇索引的叶子节点称为数据页 +InnoDB 中,聚簇索引是按照每张表的主键构造一颗 B+ 树,叶子节点中存放的就是整张表的数据,将聚簇索引的叶子节点称为数据页 * 这个特性决定了**数据也是索引的一部分**,所以一张表只能有一个聚簇索引 * 辅助索引的存在不影响聚簇索引中数据的组织,所以一张表可以有多个辅助索引 @@ -4197,7 +4192,7 @@ InnoDB 中,聚簇索引是按照每张表的主键构造一颗 B+ 树,同时 聚簇索引的缺点: -* 插入速度严重依赖于插入顺序,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能,所以对于 InnoDB 表,一般都会定义一个自增的 ID 列为主键 +* 插入速度严重依赖于插入顺序,按照主键的顺序(递增)插入是最快的方式,否则将会出现页分裂,严重影响性能,所以对于 InnoDB 表,一般都会定义一个自增的 ID 列为主键 * 更新主键的代价很高,将会导致被更新的行移动,所以对于 InnoDB 表,一般定义主键为不可更新 @@ -4213,7 +4208,7 @@ InnoDB 中,聚簇索引是按照每张表的主键构造一颗 B+ 树,同时 在聚簇索引之上创建的索引称之为辅助索引,非聚簇索引都是辅助索引,像复合索引、前缀索引、唯一索引等 -辅助索引叶子节点存储的是主键值,而不是行的物理地址,所以访问数据需要二次查找,推荐使用覆盖索引,可以减少回表查询 +辅助索引叶子节点存储的是主键值,而不是数据的物理地址,所以访问数据需要二次查找,推荐使用覆盖索引,可以减少回表查询 检索过程:辅助索引找到主键值,再通过聚簇索引找到数据页,最后通过数据页中的 Page Directory 找到数据行 @@ -4231,7 +4226,7 @@ InnoDB 使用 B+Tree 作为索引结构 * 在 InnoDB 中,表数据文件本身就是按 B+Tree 组织的一个索引结构,这个索引的 key 是数据表的主键,叶子节点 data 域保存了完整的数据记录 -* Innodb 的表数据文件**通过主键聚集数据**,如果没有定义主键,会选择非空唯一索引代替,如果也没有这样的列,MySQL 会自动为 InnoDB 表生成一个**隐含字段**作为主键,这个字段长度为 6 个字节,类型为长整形(MVCC 部分的笔记提及) +* InnoDB 的表数据文件**通过主键聚集数据**,如果没有定义主键,会选择非空唯一索引代替,如果也没有这样的列,MySQL 会自动为 InnoDB 表生成一个**隐含字段**作为主键,这个字段长度为 6 个字节,类型为长整形(MVCC 部分的笔记提及) 辅助索引: @@ -4251,7 +4246,7 @@ InnoDB 使用 B+Tree 作为索引结构 ##### 非聚簇 -MyISAM 的主键索引使用的是非聚簇索引,索引文件和数据文件是分离的,**索引文件仅保存数据记录的地址** +MyISAM 的主键索引使用的是非聚簇索引,索引文件和数据文件是分离的,**索引文件仅保存数据的地址** * 主键索引 B+ 树的节点存储了主键,辅助键索引 B+ 树存储了辅助键,表数据存储在独立的地方,这两颗 B+ 树的叶子节点都使用一个地址指向真正的表数据,对于表数据来说,这两个键没有任何差别 * 由于索引树是独立的,通过辅助索引检索无需访问主键的索引树回表查询 @@ -4301,7 +4296,7 @@ BTree 的索引类型是基于 B+Tree 树型数据结构的,B+Tree 又是 BTre BTree 又叫多路平衡搜索树,一颗 m 叉的 BTree 特性如下: -- 树中每个节点最多包含m个孩子 +- 树中每个节点最多包含 m 个孩子 - 除根节点与叶子节点外,每个节点至少有 [ceil(m/2)] 个孩子 - 若根节点不是叶子节点,则至少有两个孩子 - 所有的叶子节点都在同一层 @@ -4385,13 +4380,13 @@ B* 树:是 B+ 树的变体,在 B+ 树的非根和非叶子结点再增加指 MySQL 索引数据结构对经典的 B+Tree 进行了优化,在原 B+Tree 的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的 B+Tree,**提高区间访问的性能,防止回旋查找** -B+ 树的叶子节点是数据页(page),一个页里面可以存多个数据行 - 区间访问的意思是访问索引为 5 - 15 的数据,可以直接根据相邻节点的指针遍历 +B+ 树的叶子节点是数据页(page),一个页里面可以存多个数据行 + ![](https://gitee.com/seazean/images/raw/master/DB/索引的原理-B+Tree.png) -通常在 B+Tree 上有两个头指针,一个指向根节点,另一个指向关键字最小的叶子节点,而且所有叶子节点(即数据节点)之间是一种链式环结构。可以对 B+Tree 进行两种查找运算: +通常在 B+Tree 上有两个头指针,**一个指向根节点,另一个指向关键字最小的叶子节点**,而且所有叶子节点(即数据节点)之间是一种链式环结构。可以对 B+Tree 进行两种查找运算: - 有范围:对于主键的范围查找和分页查找 - 有顺序:从根节点开始,进行随机查找,顺序查找 @@ -4414,8 +4409,8 @@ B+ 树为了维护索引有序性,在插入新值的时候需要做必要的 每个索引中每个块存储在磁盘页中,可能会出现以下两种情况: -* 如果所在的数据页已经满了,根据 B+ 树的算法,这时候需要申请一个新的数据页,然后挪动部分数据过去,这个过程称为页分裂 -* 当相邻两个页由于删除了数据,利用率很低之后,会将数据页做合并,合并的过程可以认为是分裂过程的逆过程 +* 如果所在的数据页已经满了,这时候需要申请一个新的数据页,然后挪动部分数据过去,这个过程称为**页分裂** +* 当相邻两个页由于删除了数据,利用率很低之后,会将数据页做**页合并**,合并的过程可以认为是分裂过程的逆过程 * 这两个情况都是由 B+ 树的结构决定的 一般选用数据小的字段做索引,字段长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小 @@ -4472,8 +4467,9 @@ B+ 树为了维护索引有序性,在插入新值的时候需要做必要的 ``` * 案例练习 + 数据准备:student - + ```mysql id NAME age score 1 张三 23 99 @@ -4481,9 +4477,9 @@ B+ 树为了维护索引有序性,在插入新值的时候需要做必要的 3 王五 25 98 4 赵六 26 97 ``` - + 索引操作: - + ```mysql -- 为student表中姓名列创建一个普通索引 CREATE INDEX idx_name ON student(NAME); @@ -4491,7 +4487,7 @@ B+ 树为了维护索引有序性,在插入新值的时候需要做必要的 -- 为student表中年龄列创建一个唯一索引 CREATE UNIQUE INDEX idx_age ON student(age); ``` - + *** @@ -4511,7 +4507,7 @@ B+ 树为了维护索引有序性,在插入新值的时候需要做必要的 * MySQL 建立联合索引时会遵守**最左前缀匹配原则**,即最左优先,在检索数据时从联合索引的最左边开始匹配 - N个列组合而成的组合索引,相当于创建了N个索引,如果查询时 where 句中使用了组成该索引的**前**几个字段,那么这条查询SQL可以利用组合索引来提升查询效率 + N 个列组合而成的组合索引,相当于创建了 N 个索引,如果查询时 where 句中使用了组成该索引的**前**几个字段,那么这条查询 SQL 可以利用组合索引来提升查询效率 ```mysql -- 对name、address、phone列建一个联合索引 @@ -4519,6 +4515,7 @@ B+ 树为了维护索引有序性,在插入新值的时候需要做必要的 -- 查询语句执行时会依照最左前缀匹配原则,检索时分别会使用索引进行数据匹配。 (name,address,phone) (name,address) + (name,phone) -- 只有name字段走了索引 (name) -- 索引的字段可以是任意顺序的,优化器会帮助我们调整顺序,下面的SQL语句可以命中索引 @@ -4571,7 +4568,7 @@ B+ 树为了维护索引有序性,在插入新值的时候需要做必要的 在一棵索引树上就能获取查询所需的数据,无需回表速度更快 -使用覆盖索引,要注意 SELECT 列表中只取出需要的列,不可用 SELECT *,因为如果将所有字段一起做索引会导致索引文件过大,查询性能下降 +使用覆盖索引,要注意 SELECT 列表中只取出需要的列,不可用 SELECT *,所有字段一起做索引会导致索引文件过大,查询性能下降 @@ -4585,10 +4582,10 @@ B+ 树为了维护索引有序性,在插入新值的时候需要做必要的 索引下推充分利用了索引中的数据,在查询出整行数据之前过滤掉无效的数据,再去主键索引树上查找 -* 不使用索引下推优化时存储引擎通过索引检索到数据返回给 MySQL 服务器,服务器判断数据是否符合条件 +* 不使用索引下推优化时存储引擎通过索引检索到数据返回给 MySQL 服务器,服务器判断数据是否符合条件,符合条件的数据去聚簇索引回表查询,获取完整的数据 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-不使用索引下推.png) -* 使用索引下推优化时,如果存在某些被索引的列的判断条件时,MySQL 服务器将这一部分**判断条件传递给存储引擎**,然后由存储引擎在索引内部判断索引是否符合传递的条件,只有当索引符合条件时才会将数据检索出来返回给服务器,由此减少 IO次数 +* 使用索引下推优化时,如果**存在某些被索引的列的判断条件**时,MySQL 服务器将这一部分判断条件传递给存储引擎,由存储引擎在索引遍历的过程中判断数据是否符合传递的条件,将符合条件的数据进行回表,检索出来返回给服务器,由此减少 IO 次数 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-使用索引下推.png) @@ -4596,7 +4593,7 @@ B+ 树为了维护索引有序性,在插入新值的时候需要做必要的 * 需要存储引擎将索引中的数据与条件进行判断,所以优化是基于存储引擎的,只有特定引擎可以使用,适用于 InnoDB 和 MyISAM * 存储引擎没有调用跨存储引擎的能力,跨存储引擎的功能有存储过程、触发器、视图,所以调用这些功能的不可以进行索引下推优化 -* 对于 InnoDB 引擎只适用于二级索引,InnoDB 的聚簇索引会将整行数据读到缓冲区,因为数据已经在内存中了,不再需要去读取了,索引下推的目的减少 IO 次数也就失去了意义 +* 对于 InnoDB 引擎只适用于二级索引,InnoDB 的聚簇索引会将整行数据读到缓冲区,不再需要去回表查询了,索引下推的目的减少 IO 次数也就失去了意义 工作过程:用户表 user,(name, age) 是联合索引 @@ -4644,7 +4641,7 @@ chinaFuxin 400 ddd chinaBeijing 500 eee ``` -发现 area 字段很多都是以 china 开头的,那么如果以前1-5位字符做前缀索引就会出现大量索引值重复的情况,索引值重复性越低,查询效率也就越高,所以需要建立前 6 位字符的索引: +发现 area 字段很多都是以 china 开头的,那么如果以前 1-5 位字符做前缀索引就会出现大量索引值重复的情况,索引值重复性越低,查询效率也就越高,所以需要建立前 6 位字符的索引: ```mysql CREATE INDEX idx_area ON table_name(area(7)); @@ -4735,7 +4732,7 @@ Innodb_xxxx:这几个参数只是针对InnoDB 存储引擎的,累加的算 * 慢日志查询: 慢查询日志在查询结束以后才记录,执行效率出现问题时查询日志并不能定位问题 - 配置文件修改:修改 .cnf 文件`vim /etc/mysql/my.cnf`,重启 MySQL 服务器 + 配置文件修改:修改 .cnf 文件 `vim /etc/mysql/my.cnf`,重启 MySQL 服务器 ```sh slow_query_log=ON @@ -4757,7 +4754,7 @@ Innodb_xxxx:这几个参数只是针对InnoDB 存储引擎的,累加的算 SHOW VARIABLES LIKE '%query%' ``` -* SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,包括线程的状态、是否锁表等,可以实时地查看 SQL 的执行情况,同时对一些锁表操作进行优化 +* SHOW PROCESSLIST:查看当前 MySQL 在进行的连接线程,包括线程的状态、是否锁表等,可以实时地查看 SQL 的执行情况,同时对一些锁表操作进行优化 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SHOW PROCESSLIST命令.png) @@ -4774,9 +4771,9 @@ Innodb_xxxx:这几个参数只是针对InnoDB 存储引擎的,累加的算 ##### 执行计划 -通过 EXPLAIN 命令获取执行 SQL 语句的信息,包括在 SELECT 语句执行过程中如何连接和连接的顺序 +通过 EXPLAIN 命令获取执行 SQL 语句的信息,包括在 SELECT 语句执行过程中如何连接和连接的顺序,执行计划在优化器优化完成后、执行器之前生成,然后执行器会调用存储引擎检索数据 -查询 SQL语句的执行计划: +查询 SQL 语句的执行计划: ```mysql EXPLAIN SELECT * FROM table_1 WHERE id = 1; @@ -4788,7 +4785,7 @@ EXPLAIN SELECT * FROM table_1 WHERE id = 1; | ------------- | ------------------------------------------------------------ | | id | select查询的序列号,表示查询中执行select子句或操作表的顺序 | | select_type | 表示 SELECT 的类型 | -| table | 输出结果集的表 | +| table | 输出结果集的表,显示这一步所访问数据库中表名称,有时不是真实的表名字,可能是简称 | | type | 表示表的连接类型 | | possible_keys | 表示查询时,可能使用的索引 | | key | 表示实际使用的索引 | @@ -4800,15 +4797,13 @@ EXPLAIN SELECT * FROM table_1 WHERE id = 1; MySQL 执行计划的局限: -* 只是计划,不是执行 SQL 语句 - -* EXPLAIN 不会告诉显示关于触发器、存储过程的信息或用户自定义函数对查询的影响情况 +* 只是计划,不是执行 SQL 语句,可以随着底层优化器输入的更改而更改 +* EXPLAIN 不会告诉显示关于触发器、存储过程的信息对查询的影响情况 * EXPLAIN 不考虑各种 Cache -* EXPLAIN 不能显示 MySQL 在执行查询时所作的优化工作,因为执行计划在执行查询之前生成 +* EXPLAIN 不能显示 MySQL 在执行查询时的动态,因为执行计划在执行查询之前生成 * EXPALIN 部分统计信息是估算的,并非精确值 * EXPALIN 只能解释 SELECT 操作,其他操作要重写为 SELECT 后查看执行计划 -* 执行计划在优化器之后、执行器之前生成,然后执行器调用存储引擎检索数据 -* 执行计划可以随着底层优化器输入的更改而更改。EXPLAIN PLAN 显示的是在解释语句时数据库将如何运行 SQL 语句,由于执行环境和 EXPLAIN PLAN 环境的不同,此计划可能与 SQL 语句实际的执行计划不同 +* EXPLAIN PLAN 显示的是在解释语句时数据库将如何运行 SQL 语句,由于执行环境和 EXPLAIN PLAN 环境的不同,此计划可能与 SQL 语句实际的执行计划不同 环境准备: @@ -4834,7 +4829,7 @@ SQL 执行的顺序的标识,SQL 从大到小的执行 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-explain之id相同.png) -* id 不同时,id值越大优先级越高,越先被执行 +* id 不同时,id 值越大优先级越高,越先被执行 ```mysql EXPLAIN SELECT * FROM t_role WHERE id = (SELECT role_id FROM user_role WHERE user_id = (SELECT id FROM t_user WHERE username = 'stu1')) @@ -4842,7 +4837,7 @@ SQL 执行的顺序的标识,SQL 从大到小的执行 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-explain之id不同.png) -* id 有相同也有不同时,id相同的可以认为是一组,从上往下顺序执行;在所有的组中,id的值越大,优先级越高,越先执行 +* id 有相同也有不同时,id 相同的可以认为是一组,从上往下顺序执行;在所有的组中,id 的值越大的组,优先级越高,越先执行 ```mysql EXPLAIN SELECT * FROM t_role r , (SELECT * FROM user_role ur WHERE ur.`user_id` = '2') a WHERE r.id = a.role_id ; @@ -4877,19 +4872,9 @@ SQL 执行的顺序的标识,SQL 从大到小的执行 -##### table - -输出结果集的表,显示这一步所访问数据库中表名称,有时不是真实的表名字,可能是简称,也可能是第几步执行的结果的简称 - - - -*** - - - ##### type -对表的访问方式,表示MySQL在表中找到所需行的方式,又称访问类型 +对表的访问方式,表示 MySQL 在表中找到所需行的方式,又称访问类型 | type | 含义 | | ------ | ------------------------------------------------------------ | @@ -4899,8 +4884,8 @@ SQL 执行的顺序的标识,SQL 从大到小的执行 | ref | 非唯一性索引扫描,返回匹配某个单独值的所有,本质上也是一种索引访问 | | eq_ref | 唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配,常见于主键或唯一索引扫描 | | const | 当 MySQL 对查询某部分进行优化,并转换为一个常量时,使用该类型访问,如将主键置于where列表中,MySQL就能将该查询转换为一个常量 | -| system | system 是 const 类型的特例,当查询的表只有一行的情况下,使用system | -| NULL | MySQL在优化过程中分解语句,执行时甚至不用访问表或索引 | +| system | system 是 const 类型的特例,当查询的表只有一行的情况下,使用 system | +| NULL | MySQL 在优化过程中分解语句,执行时甚至不用访问表或索引 | 从上到下,性能从差到好,一般来说需要保证查询至少达到 range 级别, 最好达到 ref @@ -4920,7 +4905,7 @@ possible_keys: key: * 显示MySQL在查询中实际使用的索引,若没有使用索引,显示为 NULL -* 查询中若使用了**覆盖索引**,则该索引仅出现在 key 列表,不出现在 possible_keys +* 查询中若使用了**覆盖索引**,则该索引可能出现在 key 列表,不出现在 possible_keys key_len: @@ -4940,11 +4925,11 @@ key_len: * Using index:该值表示相应的 SELECT 操作中使用了**覆盖索引**(Covering Index) * Using index condition:第一种情况是搜索条件中虽然出现了索引列,但是有部分条件无法使用索引,会根据能用索引的条件先搜索一遍再匹配无法使用索引的条件,回表查询数据;第二种是使用了索引下推 -* Using where:表示存储引擎收到记录后进行后过滤(Post-filter),如果查询未能使用索引,Using where 的作用是提醒我们 MySQL 将用 WHERE 子句来过滤结果集,即需要回表查询 +* Using where:表示存储引擎收到记录后进行后过滤(Post-filter),如果查询操作未能使用索引,Using where 的作用是提醒我们 MySQL 将用 where 子句来过滤结果集,即需要回表查询 * Using temporary:表示 MySQL 需要使用临时表来存储结果集,常见于排序和分组查询 -* Using filesort:MySQL 会对数据使用一个外部的索引排序,而不是按照表内的索引顺序进行读取,无法利用索引完成的排序操作称为文件排序 +* Using filesort:对数据使用外部排序算法,将取得的数据在内存中进行排序,这种无法利用索引完成的排序操作称为文件排序 * Using join buffer:说明在获取连接条件时没有使用索引,并且需要连接缓冲区来存储中间结果 -* Impossible where:说明 WHERE 语句会导致没有符合条件的行,通过收集统计信息不可能存在结果 +* Impossible where:说明 where 语句会导致没有符合条件的行,通过收集统计信息不可能存在结果 * Select tables optimized away:说明仅通过使用索引,优化器可能仅从聚合函数结果中返回一行 * No tables used:Query 语句中使用 from dual 或不含任何 from 子句 @@ -4990,16 +4975,16 @@ SHOW PROFILES 能够在做 SQL 优化时分析当前会话中语句执行的资 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SQL执行每个状态消耗的时间.png) - Sending data 状态表示 MySQL 线程开始访问数据行并把结果返回给客户端,而不仅仅是返回给客户端。由于在 Sending data 状态下,MySQL 线程需要做大量磁盘读取操作,所以是整个查询中耗时最长的状态。 + **Sending data 状态**表示 MySQL 线程开始访问数据行并把结果返回给客户端,而不仅仅是返回给客户端。由于在 Sending data 状态下,MySQL 线程需要做大量磁盘读取操作,所以是整个查询中耗时最长的状态。 * 在获取到最消耗时间的线程状态后,MySQL 支持选择 all、cpu、block io 、context switch、page faults 等类型查看 MySQL 在使用什么资源上耗费了过高的时间。例如,选择查看 CPU 的耗费时间: ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SQL执行每个状态消耗的CPU.png) - Status:SQL 语句执行的状态 - Durationsql:执行过程中每一个步骤的耗时 - CPU_user:当前用户占有的 CPU - CPU_system:系统占有的 CPU + * Status:SQL 语句执行的状态 + * Durationsql:执行过程中每一个步骤的耗时 + * CPU_user:当前用户占有的 CPU + * CPU_system:系统占有的 CPU @@ -5056,7 +5041,7 @@ CREATE TABLE `tb_seller` ( PRIMARY KEY(`sellerid`) )ENGINE=INNODB DEFAULT CHARSET=utf8mb4; INSERT INTO `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('xiaomi','小米科技','小米官方旗舰店','e10adc3949ba59abbe56e057f20f883e','1','西安市','2088-01-01 12:00:00'); -CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name,status,address); +CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); ``` ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引环境准备.png) @@ -5184,7 +5169,7 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name,status,address); EXPLAIN SELECT * FROM tb_seller WHERE address='北京市'; ``` - 北京市的键值占 9/10,所以优化为全表扫描,type = ALL + 北京市的键值占 9/10(区分度低),所以优化为全表扫描,type = ALL ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引14.png) @@ -5218,9 +5203,9 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name,status,address); -* 最左前缀法则:当不匹配前面的字段的时候,后面的字段都是无序的。这种无序不仅体现在叶子节点,也会**导致查询时的非叶子节点也是无序的**,因为索引树相当于忽略的第一个字段,就无法使用二分查找 +* 最左前缀法则:当不匹配前面的字段的时候,后面的字段都是无序的。这种无序不仅体现在叶子节点,也会**导致查询时扫描的非叶子节点也是无序的**,因为索引树相当于忽略的第一个字段,就无法使用二分查找 -* 范围查询右边的列,不能使用索引,比如语句: `WHERE a > 1 AND b = 1 `,在 a 大于1的时候,b是无序的 +* 范围查询右边的列,不能使用索引,比如语句: `WHERE a > 1 AND b = 1 `,在 a 大于 1 的时候,b 是无序的,a > 1 是扫描时有序的,但是找到以后进行寻找 b 时,索引树就不是有序的了 @@ -5242,17 +5227,17 @@ SHOW GLOBAL STATUS LIKE 'Handler_read%'; ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL查看索引使用情况.png) -* Handler_read_first:索引中第一条被读的次数。如果较高,表示服务器正执行大量全索引扫描(这个值越低越好) +* Handler_read_first:索引中第一条被读的次数,如果较高,表示服务器正执行大量全索引扫描(这个值越低越好) -* Handler_read_key:如果索引正在工作,这个值代表一个行被索引值读的次数,如果值越低,表示索引不经常使用,性能改善不好(这个值越高越好) +* Handler_read_key:如果索引正在工作,这个值代表一个行被索引值读的次数,值越低表示索引不经常使用(这个值越高越好) * Handler_read_next:按照键顺序读下一行的请求数,如果范围约束或执行索引扫描来查询索引列,值增加 -* Handler_read_prev:按照键顺序读前一行的请求数,该读方法主要用于优化ORDER BY ... DESC +* Handler_read_prev:按照键顺序读前一行的请求数,该读方法主要用于优化 ORDER BY ... DESC -* Handler_read_rnd:根据固定位置读一行的请求数,如果执行大量查询并对结果进行排序则该值较高,可能是使用了大量需要MySQL扫描整个表的查询或连接,这个值较高意味着运行效率低,应该建立索引来解决 +* Handler_read_rnd:根据固定位置读一行的请求数,如果执行大量查询并对结果进行排序则该值较高,可能是使用了大量需要 MySQL 扫描整个表的查询或连接,这个值较高意味着运行效率低,应该建立索引来解决 -* Handler_read_rnd_next:在数据文件中读下一行的请求数。如果你正进行大量的表扫描,该值较高。通常说明你的表索引不正确或写入的查询没有利用索引。 +* Handler_read_rnd_next:在数据文件中读下一行的请求数,如果正进行大量的表扫描,该值较高,说明表索引不正确或写入的查询没有利用索引 @@ -5329,7 +5314,7 @@ EXPLAIN SELECT name,status,address,password FROM tb_seller WHERE name='小米科 INSERT INTO tb_test VALUES(3,'Jerry'); ``` -增加 cache 层:在应用中增加缓存层来达到减轻数据库负担的目的。可以部分数据从数据库中抽取出来放到应用端以文本方式存储, 或者使用框架(Mybatis)提供的一级缓存 / 二级缓存,或者使用 Redis 数据库来缓存数据 +增加 cache 层:在应用中增加缓存层来达到减轻数据库负担的目的。可以部分数据从数据库中抽取出来放到应用端以文本方式存储,或者使用框架(Mybatis)提供的一级缓存 / 二级缓存,或者使用 Redis 数据库来缓存数据 @@ -5349,7 +5334,7 @@ LOAD DATA LOCAL INFILE = '/home/seazean/sql1.log' INTO TABLE `tb_user_1` FIELD T 对于 InnoDB 类型的表,有以下几种方式可以提高导入的效率: -1. 主键顺序插入:因为 InnoDB 类型的表是按照主键的顺序保存的,所以将导入的数据按照主键的顺序排列,可以有效的提高导入数据的效率,如果 InnoDB 表没有主键,那么系统会自动默认创建一个内部列作为主键。 +1. **主键顺序插入**:因为 InnoDB 类型的表是按照主键的顺序保存的,所以将导入的数据按照主键的顺序排列,可以有效的提高导入数据的效率,如果 InnoDB 表没有主键,那么系统会自动默认创建一个内部列作为主键。 **主键是否连续对性能影响不大,只要是递增的就可以**,比如雪花算法产生的 ID 不是连续的,但是是递增的 @@ -5361,13 +5346,13 @@ LOAD DATA LOCAL INFILE = '/home/seazean/sql1.log' INTO TABLE `tb_user_1` FIELD T ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL插入ID无序排列数据.png) -2. 关闭唯一性校验:在导入数据前执行 `SET UNIQUE_CHECKS=0`,关闭唯一性校验;导入结束后执行 `SET UNIQUE_CHECKS=1`,恢复唯一性校验,可以提高导入的效率。 +2. **关闭唯一性校验**:在导入数据前执行 `SET UNIQUE_CHECKS=0`,关闭唯一性校验;导入结束后执行 `SET UNIQUE_CHECKS=1`,恢复唯一性校验,可以提高导入的效率。 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL插入数据关闭唯一性校验.png) -3. 手动提交事务:如果应用使用自动提交的方式,建议在导入前执行`SET AUTOCOMMIT=0`,关闭自动提交;导入结束后再执行 SET AUTOCOMMIT=1,打开自动提交,可以提高导入的效率。 +3. **手动提交事务**:如果应用使用自动提交的方式,建议在导入前执行`SET AUTOCOMMIT=0`,关闭自动提交;导入结束后再打开自动提交,可以提高导入的效率。 - 事务需要控制大小,事务太大可能会影响执行的效率。MySQL 有 innodb_log_buffer_size 配置项,超过这个值的日志会写入磁盘数据,效率会下降。所以在事务大小达到配置项数据级前进行事务提交可以提高效率 + 事务需要控制大小,事务太大可能会影响执行的效率。MySQL 有 innodb_log_buffer_size 配置项,超过这个值的日志会写入磁盘数据,效率会下降,所以在事务大小达到配置项数据级前进行事务提交可以提高效率 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL插入数据手动提交事务.png) @@ -5419,14 +5404,14 @@ CREATE INDEX idx_emp_age_salary ON emp(age,salary); ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL ORDER BY排序3.png) - 尽量减少额外的排序,通过索引直接返回有序数据。需要**满足 Order by 使用相同的索引、Order By 的顺序和索引顺序相同、Order by 的字段都是升序或都是降序**,否则需要额外的操作,就会出现 FileSort + 尽量减少额外的排序,通过索引直接返回有序数据。**需要满足 Order by 使用相同的索引、Order By 的顺序和索引顺序相同、Order by 的字段都是升序或都是降序**,否则需要额外的操作,就会出现 FileSort -Filesort 的优化:通过创建合适的索引,能够减少 Filesort 的出现,但是在某些情况,条件限制不能让 Filesort 消失,就需要加快 Filesort 的排序操作。 +优化:通过创建合适的索引能够减少 Filesort 的出现,但是某些情况下条件限制不能让 Filesort 消失,就要加快 Filesort 的排序操作 对于 Filesort , MySQL 有两种排序算法: -* 两次扫描算法:MySQL4.1 之前,使用该方式排序。首先根据条件取出排序字段和行指针信息,然后在排序区 sort buffer 中排序,如果 sort buffer 不够,则在临时表 temporary table 中存储排序结果。完成排序后,再根据行指针**回表读取记录**,该操作可能会导致大量随机 I/O 操作 -* 一次扫描算法:一次性取出满足条件的所有字段,然后在排序区 sort buffer 中排序后直接输出结果集。排序时内存开销较大,但是排序效率比两次扫描算法高 +* 两次扫描算法:MySQL4.1 之前,使用该方式排序。首先根据条件取出排序字段和行指针信息,然后在排序区 sort buffer 中排序,如果 sort buffer 不够,则在临时表 temporary table 中存储排序结果。完成排序后再根据行指针**回表读取记录**,该操作可能会导致大量随机 I/O 操作 +* 一次扫描算法:一次性取出满足条件的所有数据,需要回表,然后在排序区 sort buffer 中排序后直接输出结果集。排序时内存开销较大,但是排序效率比两次扫描算法高 MySQL 通过比较系统变量 max_length_for_sort_data 的大小和 Query 语句取出的字段的大小,来判定使用哪种排序算法。如果前者大,则说明 sort buffer 空间足够,使用第二种优化之后的算法,否则使用第一种。 @@ -5484,7 +5469,7 @@ GROUP BY 也会进行排序操作,与 ORDER BY 相比,GROUP BY 主要只是 #### OR -对于包含 OR 的查询子句,如果要利用索引,则 OR 之间的每个条件列都必须用到索引,而且不能使用到复合索引,如果没有索引,则应该考虑增加索引 +对于包含 OR 的查询子句,如果要利用索引,则 OR 之间的**每个条件列都必须用到索引,而且不能使用到条件之间的复合索引**,如果没有索引,则应该考虑增加索引 * 执行查询语句: @@ -5499,14 +5484,15 @@ GROUP BY 也会进行排序操作,与 ORDER BY 相比,GROUP BY 主要只是 ``` * 使用 UNION 替换 OR,求并集: - 注意:该优化只针对多个索引列有效,如果有 column 没有被索引,查询效率可能会因为没有选择 OR 而降低 - + + 注意:该优化只针对多个索引列有效,如果有列没有被索引,查询效率可能会因为没有选择 OR 而降低 + ```mysql EXPLAIN SELECT * FROM emp WHERE id = 1 UNION SELECT * FROM emp WHERE age = 30; ``` - + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL OR条件查询2.png) - + * UNION 要优于 OR 的原因: * UNION 语句的 type 值为 ref,OR 语句的 type 值为 range @@ -5568,7 +5554,7 @@ MySQL 4.1 版本之后,开始支持 SQL 的子查询 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL分页查询1.png) -* 优化方式一:子查询,在索引上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容 +* 优化方式一:子查询,在索引列 id 上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容 ```mysql EXPLAIN SELECT * FROM tb_user_1 t,(SELECT id FROM tb_user_1 ORDER BY id LIMIT 200000,10) a WHERE t.id = a.id; @@ -5592,9 +5578,9 @@ MySQL 4.1 版本之后,开始支持 SQL 的子查询 #### 使用提示 -SQL 提示,是优化数据库的一个重要手段,就是在SQL语句中加入一些提示来达到优化操作的目的 +SQL 提示,是优化数据库的一个重要手段,就是在 SQL 语句中加入一些提示来达到优化操作的目的 -* USE INDEX:在查询语句中表名的后面,添加 USE INDEX 来提供 MySQL 去参考的索引列表,可以让 MySQL 不再考虑其他可用的索引 +* USE INDEX:在查询语句中表名的后面添加 USE INDEX 来提供 MySQL 去参考的索引列表,可以让 MySQL 不再考虑其他可用的索引 ```mysql CREATE INDEX idx_seller_name ON tb_seller(name); @@ -5611,7 +5597,7 @@ SQL 提示,是优化数据库的一个重要手段,就是在SQL语句中加 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用提示2.png) -* FORCE INDEX:为强制 MySQL 使用一个特定的索引,可在查询中使用 FORCE INDEX 作为提示 +* FORCE INDEX:强制 MySQL 使用一个特定的索引 ```mysql EXPLAIN SELECT * FROM tb_seller FORCE INDEX(idx_seller_name_sta_addr) WHERE NAME='小米科技'; @@ -5637,13 +5623,13 @@ SQL 提示,是优化数据库的一个重要手段,就是在SQL语句中加 解决方案: -* 计数保存在 Redis 中,因为更新 MySQL 和 Redis 的操作不是原子的,会存在数据一致性的问题 +* 计数保存在 Redis 中,但是更新 MySQL 和 Redis 的操作不是原子的,会存在数据一致性的问题 * 计数直接放到数据库里单独的一张计数表中,利用事务解决计数精确问题: - 会话 B 的读操作在 T3 执行的,这时更新事务还没有提交,所以计数值加 1 这个操作对会话 B 还不可见,因此会话 B 看到的结果里, 查计数值和最近 100 条记录看到的结果,逻辑上就是一致的 + 会话 B 的读操作在 T3 执行的,这时更新事务还没有提交,所以计数值加 1 这个操作对会话 B 还不可见,因此会话 B 查询的计数值和最近 100 条记录,返回的结果逻辑上就是一致的 并发系统性能的角度考虑,应该先插入操作记录再更新计数表,因为更新计数表涉及到行锁的竞争,**先插入再更新能最大程度地减少事务之间的锁等待,提升并发度** @@ -5684,7 +5670,7 @@ count 函数的按照效率排序:`count(字段) < count(主键id) < count(1) #### MyISAM -MyISAM 存储引擎使用 key_buffer 缓存索引块,加速 MyISAM 索引的读写速度。对于 MyISAM 表的数据块没有特别的缓存机制,完全依赖于操作系统的 IO 缓存。 +MyISAM 存储引擎使用 key_buffer 缓存索引块,加速 MyISAM 索引的读写速度。对于 MyISAM 表的数据块没有特别的缓存机制,完全依赖于操作系统的 IO 缓存 * key_buffer_size:该变量决定 MyISAM 索引块缓存区的大小,直接影响到 MyISAM 表的存取效率 @@ -5701,7 +5687,7 @@ MyISAM 存储引擎使用 key_buffer 缓存索引块,加速 MyISAM 索引的 * read_buffer_size:如果需要经常顺序扫描 MyISAM 表,可以通过增大 read_buffer_size 的值来改善性能。但 read_buffer_size 是每个 Session 独占的,如果默认值设置太大,并发环境就会造成内存浪费 -* read_rnd_buffer_size:对于需要做排序的 MyISAM 表的查询,如带有 ORDER BY 子句的语句,适当增加该的值,可以改善此类的 SQL 的性能。但是 read_rnd_buffer_size 是每个 Session 独占的,如果默认值设置太大,就会造成内存浪费 +* read_rnd_buffer_size:对于需要做排序的 MyISAM 表的查询,如带有 ORDER BY 子句的语句,适当增加该的值,可以改善此类的 SQL 的性能,但是 read_rnd_buffer_size 是每个 Session 独占的,如果默认值设置太大,就会造成内存浪费 @@ -5745,7 +5731,7 @@ Innodb 用一块内存区做 IO 缓存池,该缓存池不仅用来缓存 Innod MySQL Server 是多线程结构,包括后台线程和客户服务线程。多线程可以有效利用服务器资源,提高数据库的并发性能。在 MySQL 中,控制并发连接和线程的主要参数: -* max_connections:控制允许连接到MySQL数据库的最大连接数,默认值是 151 +* max_connections:控制允许连接到 MySQL 数据库的最大连接数,默认值是 151 如果状态变量 connection_errors_max_connections 不为零,并且一直增长,则说明不断有连接请求因数据库连接数已达到允许最大值而失败,这时可以考虑增大max_connections 的值 @@ -5795,11 +5781,6 @@ MySQL 复制的优点主要包含以下三个方面: - 可以在从库中执行备份,以避免备份期间影响主库的服务 -**读写分离**: - -* 读写分离可以降低主库的访问压力,提高系统的并发能力 -* 主库不建查询的索引,从库建查询的索引。因为索引需要维护的,比如插入一条数据,不仅要在聚簇索引上面插入,对应的二级索引也得插入,修改也是一样的。所以将读操作分到从库了之后,可以在主库把查询要用的索引删了,减少写操作对主库的影响 - *** @@ -5812,7 +5793,7 @@ MySQL 复制的优点主要包含以下三个方面: MySQL 的主从之间维持了一个长连接。主库内部有一个线程,专门用于服务从库的长连接,连接过程: -* 从库执行 change master 命令,设置主库 A 的 IP、端口、用户名、密码,以及要从哪个位置开始请求 binlog,这个位置包含文件名和日志偏移量 +* 从库执行 change master 命令,设置主库的 IP、端口、用户名、密码以及要从哪个位置开始请求 binlog,这个位置包含文件名和日志偏移量 * 从库执行 start slave 命令,这时从库会启动两个线程,就是图中的 io_thread 和 sql_thread,其中 io_thread 负责与主库建立连接 * 主库校验完用户名、密码后,开始按照从传过来的位置,从本地读取 binlog 发给从库,开始主从复制 @@ -5823,14 +5804,13 @@ MySQL 的主从之间维持了一个长连接。主库内部有一个线程, 主从复制主要依赖的是 binlog,MySQL 默认是异步复制,需要三个线程: - binlog dump thread:在主库事务提交时,负责把数据变更记录在二进制日志文件 binlog 中,并通知 slave 有数据更新 -- I/O thread:负责从主服务器上拉取二进制日志,并将 binlog 日志内容依次写到 relay log 中转日志的最末端,并将新的 binlog 文件名和 offset 记录到 master-info 文件中,以便下一次读取日志时能告诉 master 服务器从指定 binlog 日志文件及位置开始读取新的 binlog 日志内容 -- SQL thread:监测本地 relay log 中新增了日志内容,读取中继日志并重做其中的 SQL 语句 -- 从库在 relay-log.info 中记录当前应用中继日志的文件名和位置点以便下一次执行 +- I/O thread:负责从主服务器上拉取二进制日志,并将 binlog 日志内容依次写到 relay log 中转日志的最末端,并将新的 binlog 文件名和 offset 记录到 master-info 文件中,以便下一次读取日志时从指定 binlog 日志文件及位置开始读取新的 binlog 日志内容 +- SQL thread:监测本地 relay log 中新增了日志内容,读取中继日志并重做其中的 SQL 语句,从库在 relay-log.info 中记录当前应用中继日志的文件名和位置点以便下一次执行 同步与异步: * 异步复制有数据丢失风险,例如数据还未同步到从库,主库就给客户端响应,然后主库挂了,此时从库晋升为主库的话数据是缺失的 -* 同步复制,主库需要将 binlog 复制到所有从库,等所有从库响应了之后才会给客户端响应,这样的话性能很差,一般不会选择 +* 同步复制,主库需要将 binlog 复制到所有从库,等所有从库响应了之后主库才进行其他逻辑,这样的话性能很差,一般不会选择 * MySQL 5.7 之出现了半同步复制,有参数可以选择成功同步几个从库就返回响应 @@ -5841,13 +5821,13 @@ MySQL 的主从之间维持了一个长连接。主库内部有一个线程, #### 主主结构 -主主结构就是两个数据库之间总是互为主备关系,这样在切换的时候就不用再修改主备关系 +主主结构就是两个数据库之间总是互为主从关系,这样在切换的时候就不用再修改主从关系 循环复制:在库 A 上更新了一条语句,然后把生成的 binlog 发给库 B,库 B 执行完这条更新语句后也会生成 binlog,会再发给 A 解决方法: -* 两个库的 server id 必须不同,如果相同则它们之间不能设定为主备关系 +* 两个库的 server id 必须不同,如果相同则它们之间不能设定为主主关系 * 一个库接到 binlog 并在重放的过程中,生成与原 binlog 的 server id 相同的新的 binlog * 每个库在收到从主库发过来的日志后,先判断 server id,如果跟自己的相同,表示这个日志是自己生成的,就直接丢弃这个日志 @@ -5861,18 +5841,17 @@ MySQL 的主从之间维持了一个长连接。主库内部有一个线程, #### 延迟原因 -正常情况主库执行更新生成的所有 binlog,都可以传到备库并被正确地执行,备库就能达到跟主库一致的状态,这就是最终一致性 +正常情况主库执行更新生成的所有 binlog,都可以传到从库并被正确地执行,从库就能达到跟主库一致的状态,这就是最终一致性 -主从延迟是主从之间是存在一定时间的数据不一致,就是同一个事务在从库执行完成的时间和主库执行完成的时间的差值,即 T3-T1 +主从延迟是主从之间是存在一定时间的数据不一致,就是同一个事务在从库执行完成的时间和主库执行完成的时间的差值,即 T2-T1 - 主库 A 执行完成一个事务,写入 binlog,该时刻记为 T1 -- 传给从库 B,从库接受完这个 binlog 的时刻记为 T2 -- 从库 B 执行完这个事务,该时刻记为 T3 +- 日志传给从库 B,从库 B 执行完这个事务,该时刻记为 T2 通过在从库执行 `show slave status` 命令,返回结果会显示 seconds_behind_master 表示当前从库延迟了多少秒 -- 每一个事务的 binlog 都有一个时间字段,用于记录主库上写入的时间 -- 从库取出当前正在执行的事务的时间字段,跟系统的时间进行相减,得到的就是 seconds_behind_master +- 每一个事务的 binlog 都有一个时间字段,用于记录主库上**写入**的时间 +- 从库取出当前正在**执行**的事务的时间字段,跟系统的时间进行相减,得到的就是 seconds_behind_master 主从延迟的原因: @@ -5889,7 +5868,7 @@ MySQL 的主从之间维持了一个长连接。主库内部有一个线程, * 业务中大多数情况查询操作要比更新操作更多,搭建一主多从结构,让这些从库来分担读的压力 * 尽量采用短的链路,主库和从库服务器的距离尽量要短,提升端口带宽,减少 binlog 传输的网络延时 -* 实时性要求高的业务读强制走主库,从库只做灾备,备份 +* 实时性要求高的业务读强制走主库,从库只做备份 @@ -5901,14 +5880,14 @@ MySQL 的主从之间维持了一个长连接。主库内部有一个线程, ##### MySQL5.6 -高并发情况下,主库的会产生大量的 binlog,在从库中有两个线程 IO Thread 和 SQL Thread单线程执行,会导致主库延迟变大。为了改善复制延迟问题,MySQL 5.6 版本增加了并行复制功能,以采用多线程机制来促进执行 +高并发情况下,主库的会产生大量的 binlog,在从库中有两个线程 IO Thread 和 SQL Thread 单线程执行,会导致主库延迟变大。为了改善复制延迟问题,MySQL 5.6 版本增加了并行复制功能,以采用多线程机制来促进执行 -coordinator 就是原来的 sql_thread,并行复制中它不再直接更新数据,只负责读取中转日志和分发事务: +coordinator 就是原来的 sql_thread,并行复制中它不再直接更新数据,只**负责读取中转日志和分发事务**: * 线程分配完成并不是立即执行,为了防止造成更新覆盖,更新同一行的两个事务必须被分发到同一个工作线程 * 同一个事务不能被拆开,必须放到同一个工作线程 -MySQL 5.6 版本的策略:每个线程对应一个 hash 表,用于保存当前这个线程的执行队列里的事务所涉及的表,hash 表的 key 是数据库 DB 名,value 是一个数字,表示队列中有多少个事务修改这个库,适用于主库上有多个 DB 的情况 +MySQL 5.6 版本的策略:每个线程对应一个 hash 表,用于保存当前这个线程的执行队列里的事务所涉及的表,hash 表的 key 是数据库 名,value 是一个数字,表示队列中有多少个事务修改这个库,适用于主库上有多个 DB 的情况 每个事务在分发的时候,跟线程的冲突(事务操作的是同一个 DB)关系包括以下三种情况: @@ -5920,7 +5899,7 @@ MySQL 5.6 版本的策略:每个线程对应一个 hash 表,用于保存当 * 构造 hash 值的时候很快,只需要库名,而且一个实例上 DB 数也不会很多,不会出现需要构造很多个项的情况 * 不要求 binlog 的格式,statement 格式的 binlog 也可以很容易拿到库名 -* 主库上的表都放在同一个 DB 里面,这个策略就没有效果了;或者不同 DB 的热点不同,比如一个是业务逻辑库,一个是系统配置库,那也起不到并行的效果,需要把相同热度的表均匀分到这些不同的 DB 中,才可以使用这个策略 +* 主库上的表都放在同一个 DB 里面,这个策略就没有效果了;或者不同 DB 的热点不同,比如一个是业务逻辑库,一个是系统配置库,那也起不到并行的效果,需要**把相同热度的表均匀分到这些不同的 DB 中**,才可以使用这个策略 @@ -5930,25 +5909,26 @@ MySQL 5.6 版本的策略:每个线程对应一个 hash 表,用于保存当 ##### MySQL5.7 -MySQL 5.7 并行复制策略的思想是:所有处于 commit 状态的事务可以并行执行 +MySQL 5.7 并行复制策略的思想是: -* 同时处于 prepare 状态的事务,在备库执行时是可以并行的 -* 处于 prepare 状态的事务,与处于 commit 状态的事务之间,在备库执行时也是可以并行的 +* 所有处于 commit 状态的事务可以并行执行 +* 同时处于 prepare 状态的事务,在从库执行时是可以并行的 +* 处于 prepare 状态的事务,与处于 commit 状态的事务之间,在从库执行时也是可以并行的 MySQL 5.7 由参数 slave-parallel-type 来控制并行复制策略: -* 配置为 DATABASE,表示使用 MySQL 5.6 版本的**按库并行策略** +* 配置为 DATABASE,表示使用 MySQL 5.6 版本的**按库(DB)并行策略** * 配置为 LOGICAL_CLOCK,表示的**按提交状态并行**执行 MySQL 5.7.22 版本里,MySQL 增加了一个新的并行复制策略,基于 WRITESET 的并行复制。新增了一个参数 binlog-transaction-dependency-tracking,用来控制是否启用这个新策略: * COMMIT_ORDER:表示根据同时进入 prepare 和 commit 来判断是否可以并行的策略 -* WRITESET:表示的是对于事务涉及更新的每一行,计算出这一行的 hash 值,组成集合 writeset,如果两个事务没有操作相同的行,也就是说它们的 writeset 没有交集,就可以并行(**按行并行**) +* WRITESET:表示的是对于每个事务涉及更新的每一行,计算出这一行的 hash 值,组成该事务的 writeset 集合,如果两个事务没有操作相同的行,也就是说它们的 writeset 没有交集,就可以并行(**按行并行**) * WRITESET_SESSION:是在 WRITESET 的基础上多了一个约束,即在主库上同一个线程先后执行的两个事务,在备库执行的时候,要保证相同的先后顺序 - 为了唯一标识,这个 hash 表的值是通过 `库名 + 表名 + 索引名 + 值` 计算出来的 + 为了唯一标识,这个 hash 表的值是通过 `库名 + 表名 + 索引名 + 值` (表示的是某一行)计算出来的 MySQL 5.7.22 按行并发的优势: @@ -5956,7 +5936,7 @@ MySQL 5.7.22 按行并发的优势: * 不需要把整个事务的 binlog 都扫一遍才能决定分发到哪个线程,更省内存 * 从库的分发策略不依赖于 binlog 内容,所以 binlog 是 statement 格式也可以,更节约内存(因为 row 才记录更改的行) -MySQL 5.7.22 的并行复制策略在通用性上是有保证的,但是对于表上没主键和外键约束的场景,WRITESET 策略也是没法并行的,也会暂时退化为单线程模型 +MySQL 5.7.22 的并行复制策略在通用性上是有保证的,但是对于表上没主键、唯一和外键约束的场景,WRITESET 策略也是没法并行的,也会暂时退化为单线程模型 @@ -5970,15 +5950,20 @@ MySQL 5.7.22 的并行复制策略在通用性上是有保证的,但是对于 #### 读写分离 -读写分离的主要目标就是分摊主库的压力,这也产生了读写延迟,造成数据的不一致性 +读写分离:可以降低主库的访问压力,提高系统的并发能力 -假如客户端执行完一个更新事务后马上发起查询,如果查询选择的是从库的话,可能读到的还是以前的数据,叫过期读,解决方案: +* 主库不建查询的索引,从库建查询的索引。因为索引需要维护的,比如插入一条数据,不仅要在聚簇索引上面插入,对应的二级索引也得插入 +* 将读操作分到从库了之后,可以在主库把查询要用的索引删了,减少写操作对主库的影响 + +读写分离产生了读写延迟,造成数据的不一致性。假如客户端执行完一个更新事务后马上发起查询,如果查询选择的是从库的话,可能读到的还是以前的数据,叫过期读, + +解决方案: * 强制将写之后**立刻读的操作转移到主库**,比如刚注册的用户,直接登录从库查询可能查询不到,先走主库登录 * **二次查询**,如果从库查不到数据,则再去主库查一遍,由 API 封装,比较简单,但导致主库压力大 -* 主库更新后,读从库之前先 sleep 一下,类似于执行一条 `select sleep(1)` 命令 +* 更新主库后,读从库之前先 sleep 一下,类似于执行一条 `select sleep(1)` 命令 * 确保主备无延迟的方法,每次从库执行查询请求前,先判断 seconds_behind_master 是否已经等于 0,如果不等于那就等到这个参数变为 0 才能执行查询请求 @@ -5991,7 +5976,7 @@ MySQL 5.7.22 的并行复制策略在通用性上是有保证的,但是对于 ### 负载均衡 -负载均衡是应用中使用非常普遍的一种优化方法,机制就是利用某种均衡算法,将固定的负载量分布到不同的服务器上, 以此来降低单台服务器的负载,达到优化的效果 +负载均衡是应用中使用非常普遍的一种优化方法,机制就是利用某种均衡算法,将固定的负载量分布到不同的服务器上,以此来降低单台服务器的负载,达到优化的效果 * 分流查询:通过 MySQL 的主从复制,实现读写分离,使增删改操作走主节点,查询操作走从节点,从而可以降低单台服务器的读写压力 @@ -6632,9 +6617,7 @@ InnoDB 存储引擎支持的是行级别的锁,因此意向锁不会阻塞除 ##### 锁升级 -无索引行锁升级为表锁:不通过索引检索数据,InnoDB 会将对表中的所有记录加锁,实际效果和**表锁**一样 - -索引失效会造成锁升级,实际开发过程应避免出现索引失效的状况 +索引失效造成行锁升级为表锁,不通过索引检索数据,InnoDB 会将对表中的所有记录加锁,实际效果和**表锁**一样实际开发过程应避免出现索引失效的状况 * 查看当前表的索引: @@ -6848,11 +6831,11 @@ log_bin=mysqlbin binlog_format=STATEMENT ``` -日志存放位置:配置时给定了文件名但是没有指定路径,日志默认写入Mysql的数据目录 +日志存放位置:配置时给定了文件名但是没有指定路径,日志默认写入MySQL 的数据目录 日志格式: -* STATEMENT:该日志格式在日志文件中记录的都是 SQL 语句,每一条对数据进行修改的 SQL都会记录在日志文件中,通过 mysqlbinlog 工具,可以查看到每条语句的文本。主从复制时,从库会将日志解析为原语句,并在从库重新执行一遍 +* STATEMENT:该日志格式在日志文件中记录的都是 SQL 语句,每一条对数据进行修改的 SQL 都会记录在日志文件中,通过 mysqlbinlog 工具,可以查看到每条语句的文本。主从复制时,从库会将日志解析为原语句,并在从库重新执行一遍 缺点:可能会导致主备不一致,因为记录的 SQL 在不同的环境中可能选择的索引不同,导致结果不同 * ROW:该日志格式在日志文件中记录的是每一行的数据变更,而不是记录 SQL 语句。比如执行 SQL 语句 `update tb_book set status='1'`,如果是 STATEMENT,在日志中会记录一行 SQL 语句; 如果是 ROW,由于是对全表进行更新,就是每一行记录都会发生变更,ROW 格式的日志中会记录每一行的数据变更 @@ -6949,7 +6932,7 @@ mysqlbinlog log-file; * 执行指令 `PURGE MASTER LOGS TO 'mysqlbin.***`,该命令将删除 ` ***` 编号之前的所有日志 -* 执行指令 `PURGE MASTER LOGS BEFORE 'yyyy-mm-dd hh:mm:ss'` ,该命令将删除日志为 `yyyy-mm-dd hh:mm:ss` 之前产生的所有日志 +* 执行指令 `PURGE MASTER LOGS BEFORE 'yyyy-mm-dd hh:mm:ss'` ,该命令将删除日志为 `yyyy-mm-dd hh:mm:ss` 之前产生的日志 * 设置参数 `--expire_logs_days=#`,此参数的含义是设置日志的过期天数,过了指定的天数后日志将会被自动删除,这样做有利于减少管理日志的工作量,配置 my.cnf 文件: @@ -6969,7 +6952,7 @@ mysqlbinlog log-file; 查询日志中记录了客户端的所有操作语句,而二进制日志不包含查询数据的 SQL 语句 -默认情况下, 查询日志是未开启的。如果需要开启查询日志,配置 my.cnf: +默认情况下,查询日志是未开启的。如果需要开启查询日志,配置 my.cnf: ```sh # 该选项用来开启查询日志,可选值0或者1,0代表关闭,1代表开启 @@ -7132,9 +7115,9 @@ long_query_time=10 ## 概述 -JDBC(Java DataBase Connectivity,java数据库连接)是一种用于执行SQL语句的Java API,可以为多种关系型数据库提供统一访问,是由一组用Java语言编写的类和接口组成的。 +JDBC(Java DataBase Connectivity,java数据库连接)是一种用于执行 SQL 语句的 Java API,可以为多种关系型数据库提供统一访问,是由一组用 Java 语言编写的类和接口组成的。 -JDBC 是 java 官方提供的一套规范(接口),用于帮助开发人员快速实现不同关系型数据库的连接 +JDBC 是 Java 官方提供的一套规范(接口),用于帮助开发人员快速实现不同关系型数据库的连接 使用 JDBC 需要导包 @@ -7212,7 +7195,7 @@ Statement:执行 sql 语句的对象 - 执行 DML 语句:`int executeUpdate(String sql)` - 返回值 int:返回影响的行数 - 参数 sql:可以执行 insert、update、delete 语句 -- 执行DQL语句:`ResultSet executeQuery(String sql)` +- 执行 DQL 语句:`ResultSet executeQuery(String sql)` - 返回值 ResultSet:封装查询的结果 - 参数 sql:可以执行 select 语句 - 释放资源 @@ -7562,7 +7545,7 @@ PreparedStatement:预编译 sql 语句的执行者对象,继承 `PreparedSta 为 ? 占位符赋值的方法:`setXxx(int parameterIndex, xxx data)` -- 参数1:? 的位置编号(编号从1开始) +- 参数1:? 的位置编号(编号从 1 开始) - 参数2:? 的实际参数 @@ -8177,7 +8160,7 @@ public class DataSourceUtils { ### 概述 -NoSQL (Not-Only SQL) : 泛指非关系型的数据库,作为关系型数据库的补充。 +NoSQL(Not-Only SQL):泛指非关系型的数据库,作为关系型数据库的补充。 MySQL 支持 ACID 特性,保证可靠性和持久性,读取性能不高,因此需要缓存的来减缓数据库的访问压力。 @@ -8405,7 +8388,7 @@ Redis (REmote DIctionary Server) :用 C 语言开发的一个开源的高性 * 多服务器快捷配置: - 导入并加载指定配置文件信息,用于快速创建 redis 公共配置较多的 redis实例配置文件,便于维护 + 导入并加载指定配置文件信息,用于快速创建 redis 公共配置较多的 redis 实例配置文件,便于维护 ```sh include /path/conf_name.conf @@ -8451,7 +8434,7 @@ Redis (REmote DIctionary Server) :用 C 语言开发的一个开源的高性 logfile filename ``` -注意:日志级别开发期设置为verbose即可,生产环境中配置为notice,简化日志输出量,降低写日志IO的频度 +注意:日志级别开发期设置为 verbose 即可,生产环境中配置为 notice,简化日志输出量,降低写日志IO的频度 @@ -8537,7 +8520,7 @@ Redis 单线程也能高效的原因: ### 多线程 -Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,因为这是 Redis 中的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络),Redis 的多线程只是用来**处理网络数据的读写和协议解析**, 执行命令仍然是单线程顺序执行,因此不需要担心线程安全问题。 +Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,因为这是 Redis 的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络),多线程只是用来**处理网络数据的读写和协议解析**, 执行命令仍然是单线程顺序执行,因此不需要担心线程安全问题。 Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启需要修改 redis 配置文件 `redis.conf` : @@ -8545,7 +8528,7 @@ Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启需 io-threads-do-reads yesCopy to clipboardErrorCopied ``` -开启多线程后,还需要设置线程数,否则是不生效的。同样需要修改 redis 配置文件 : +开启多线程后,还需要设置线程数,否则是不生效的,同样需要修改 redis 配置文件 : ```sh io-threads 4 #官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程 @@ -8624,19 +8607,19 @@ io-threads 4 #官网建议4核的机器建议设置为2或3个线程,8核的 ### key指令 -key是一个字符串,通过key获取redis中保存的数据 +key 是一个字符串,通过 key 获取 redis 中保存的数据 * 基本操作 ```sh - del key #删除指定key - unlink key #非阻塞删除key,真正的删除会在后续异步操作 - exists key #获取key是否存在 - type key #获取key的类型 - sort key [ASC/DESC] #对key中数据排序,默认对数字排序,并不更改集合中的数据位置,只是查询 - sort key alpha #对key中字母排序 - rename key newkey #改名 - renamenx key newkey #改名 + del key #删除指定key + unlink key #非阻塞删除key,真正的删除会在后续异步操作 + exists key #获取key是否存在 + type key #获取key的类型 + sort key [ASC/DESC] #对key中数据排序,默认对数字排序,并不更改集合中的数据位置,只是查询 + sort key alpha #对key中字母排序 + rename key newkey #改名 + renamenx key newkey #改名 ``` * 时效性控制 @@ -8656,18 +8639,18 @@ key是一个字符串,通过key获取redis中保存的数据 * 查询模式 ```sh - keys pattern #查询key + keys pattern #查询key ``` 查询模式规则:*匹配任意数量的任意符号;?配合一个任意符号;[]匹配一个指定符号 ```sh - keys * #查询所有key - keys aa* #查询所有以aa开头 - keys *bb #查询所有以bb结尾 - keys ??cc #查询所有前面两个字符任意,后面以cc结尾 - keys user:? #查询所有以user:开头,最后一个字符任意 - keys u[st]er:1 #查询所有以u开头,以er:1结尾,中间包含一个字母,s或t + keys * #查询所有key + keys aa* #查询所有以aa开头 + keys *bb #查询所有以bb结尾 + keys ??cc #查询所有前面两个字符任意,后面以cc结尾 + keys user:? #查询所有以user:开头,最后一个字符任意 + keys u[st]er:1 #查询所有以u开头,以er:1结尾,中间包含一个字母,s或t ``` @@ -8680,9 +8663,7 @@ key是一个字符串,通过key获取redis中保存的数据 ### DB指令 -Redis 在使用过程中,伴随着操作数据量的增加,会出现大量的数据以及对应的 key,数据不区分种类、类别混在一起,容易引起重复或者冲突 - -Redis 为每个服务提供 16 个数据库,编码 0-15,每个数据库之间相互独立,**共用 **Redis 内存,不区分大小 +Redis 在使用过程中,随着操作数据量的增加,会出现大量的数据以及对应的 key,数据不区分种类、类别混在一起,容易引起重复或者冲突,所以 Redis 为每个服务提供 16 个数据库,编码 0-15,每个数据库之间相互独立,**共用 **Redis 内存,不区分大小 * 基本操作 @@ -8724,7 +8705,7 @@ Redis 客户端可以订阅任意数量的频道 -注意:发布的消息没有持久化,订阅的客户端只能收到订阅后发布的消息 +注意:发布的消息没有持久化,所以订阅的客户端只能收到订阅后发布的消息 @@ -8887,7 +8868,7 @@ Redis 所有操作都是**原子性**的,采用**单线程**机制,命令是 #### 实现 -Redis 字符串对象底层的数据结构实现主要是 int 和简单动态字符串 SDS,是可以修改的字符串,内部结构实现上类似于 Java 的 ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配 +Redis 字符串对象底层的数据结构实现主要是 int 和简单动态字符串 SDS,是可以修改的字符串,内部结构实现上类似于 Java 的 ArrayList,采用**预分配冗余空间**的方式来减少内存的频繁分配 ```c struct sdshdr{ @@ -8903,7 +8884,7 @@ struct sdshdr{ ![](https://gitee.com/seazean/images/raw/master/DB/Redis-string数据结构.png) -内部为当前字符串实际分配的空间 capacity 一般要高于实际字符串长度 len,当字符串长度小于 1M 时,扩容都是双倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间。需要注意的是字符串最大长度为 512M +内部为当前字符串实际分配的空间 capacity 一般要高于实际字符串长度 len,当字符串长度小于 1M 时,扩容都是双倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间,需要注意的是字符串最大长度为 512M @@ -8980,10 +8961,10 @@ hash 是指的一个数据类型,并不是一个数据 注意事项 -1. hash类型中value只能存储字符串,不允许存储其他数据类型,不存在嵌套现象,如果数据未获取到,对应的值为(nil) -2. 每个hash可以存储2^32 - 1个键值对 -3. hash类型和对象的数据存储形式相似,并且可以灵活添加删除对象属性。但hash设计初衷不是为了存储大量对象而设计的,不可滥用,不可将hash作为对象列表使用 -4. hgetall 操作可以获取全部属性,如果内部field过多,遍历整体数据效率就很会低,有可能成为数据访问瓶颈 +1. hash 类型中 value 只能存储字符串,不允许存储其他数据类型,不存在嵌套现象,如果数据未获取到,对应的值为(nil) +2. 每个 hash 可以存储 2^32 - 1 个键值对 +3. hash 类型和对象的数据存储形式相似,并且可以灵活添加删除对象属性。但 hash 设计初衷不是为了存储大量对象而设计的,不可滥用,不可将 hash 作为对象列表使用 +4. hgetall 操作可以获取全部属性,如果内部 field 过多,遍历整体数据效率就很会低,有可能成为数据访问瓶颈 @@ -9049,7 +9030,7 @@ ziplist 使用更加紧凑的结构实现多个元素的连续存储,所以在 Redis 字典使用散列表为底层实现,一个散列表里面有多个散列表节点,每个散列表节点就保存了字典中的一个键值对,发生哈希冲突采用链表法解决,存储无序 * 为了避免散列表性能的下降,当装载因子大于 1 的时候,Redis 会触发扩容,将散列表扩大为原来大小的 2 倍左右 -* 当数据动态减少之后,为了节省内存,当装载因子小于 0.1 的时候,Redis 就会触发缩容,缩小为字典中数据个数的大约 2 倍大小 +* 当数据动态减少之后,为了节省内存,当装载因子小于 0.1 的时候,Redis 就会触发缩容,缩小为字典中数据个数的 50 % 左右 @@ -9094,9 +9075,9 @@ list 类型:保存多个数据,底层使用**双向链表**存储结构实 * 查询操作 ```sh - lrange key start stop #从左边遍历数据并指定开始和结束索引,0是第一个索引,-1是终索引 - lindex key index #获取指定索引数据,没有则为nil,没有索引越界 - llen key #list中数据长度/个数 + lrange key start stop #从左边遍历数据并指定开始和结束索引,0是第一个索引,-1是终索引 + lindex key index #获取指定索引数据,没有则为nil,没有索引越界 + llen key #list中数据长度/个数 ``` * 规定时间内获取并移除数据 @@ -9450,7 +9431,7 @@ Bitmaps 本身不是一种数据类型, 实际上就是字符串(key-value 指令操作: -* 获取指定 key 对应偏移量上的 bit 值 +* 获取指定 key 对应**偏移量**上的 bit 值 ```sh getbit key offset @@ -9744,9 +9725,9 @@ Redis Desktop Manager #### save -save指令:手动执行一次保存操作 +save 指令:手动执行一次保存操作 -配置redis.conf: +配置 redis.conf: ```sh dir path #设置存储.rdb文件的路径,通常设置成存储空间较大的目录中,目录名称data @@ -9756,7 +9737,7 @@ rdbchecksum yes|no #设置读写文件过程是否进行RDB格式校验,默 #消耗,但存在数据损坏的风险 ``` -工作原理:redis 是个单线程的工作模式,会创建一个任务队列,所有的命令都会进到这个队列排队执行。当某个指令在执行的时候,队列后面的指令都要等待,所以这种执行方式会非常耗时。 +工作原理:redis 是个**单线程的工作模式**,会创建一个任务队列,所有的命令都会进到这个队列排队执行。当某个指令在执行的时候,队列后面的指令都要等待,所以这种执行方式会非常耗时。 save 指令的执行会阻塞当前 Redis 服务器,直到当前 RDB 过程完成为止,有可能会造成长时间阻塞,线上环境不建议使用 @@ -9809,7 +9790,7 @@ save second changes #设置自动持久化条件,满足限定时间范围内ke 参数: * second:监控时间范围 -* changes:监控key的变化量 +* changes:监控 key 的变化量 说明: save 配置中对于 second 与 changes 设置通常具有互补对应关系,尽量不要设置成包含性关系 @@ -9857,20 +9838,20 @@ RDB三种启动方式对比: shutdown save ``` - 默认情况下执行 shutdown 命令时,自动执行 bgsave(如果没有开启AOF持久化功能) + 默认情况下执行 shutdown 命令时,自动执行 bgsave(如果没有开启 AOF 持久化功能) * 全量复制:主从复制部分详解 * RDB优点: - RDB 是一个紧凑压缩的二进制文件,存储效率较高,但存储数据量较大时,存储效率较低 - - RDB 内部存储的是 redis 在某个时间点的数据快照,非常适合用于数据备份,全量复制等场景 + - RDB 内部存储的是 redis 在某个时间点的数据快照,非常**适合用于数据备份,全量复制**等场景 - RDB 恢复数据的速度要比 AOF 快很多,因为是快照,直接恢复 - 应用:服务器中每 X 小时执行 bgsave 备份,并将 RDB 文件拷贝到远程机器中,用于灾难恢复 * RDB缺点: - bgsave 指令每次运行要执行 fork 操作创建子进程,会牺牲一些性能 - - RDB 方式无论是执行指令还是利用配置,无法做到实时持久化,具有较大的可能性丢失数据,**最后一次持久化后的数据可能丢失** + - RDB 方式无论是执行指令还是利用配置,无法做到实时持久化,具有丢失数据的可能性,最后一次持久化后的数据可能丢失 - Redis 的众多版本中未进行 RDB 文件格式的版本统一,可能出现各版本之间数据格式无法兼容 @@ -9903,9 +9884,9 @@ AOF 写数据过程: 启动 AOF 基本配置: ```sh -appendonly yes|no #开启AOF持久化功能,默认no,即不开启状态 -appendfilename filename #AOF持久化文件名,默认appendonly.aof,建议设置appendonly-端口号.aof -dir #AOF持久化文件保存路径,与RDB持久化文件路径保持一致即可 +appendonly yes|no #开启AOF持久化功能,默认no,即不开启状态 +appendfilename filename #AOF持久化文件名,默认appendonly.aof,建议设置appendonly-端口号.aof +dir #AOF持久化文件保存路径,与RDB持久化文件路径保持一致即可 ``` ```sh @@ -9917,7 +9898,7 @@ AOF 持久化数据的三种策略(appendfsync): - always(每次):每次写入操作均同步到 AOF 文件中,**数据零误差,性能较低**,不建议使用。 -- everysec(每秒):每秒将缓冲区中的指令同步到 AOF 文件中,在系统突然宕机的情况下丢失1秒内的数据 数据**准确性较高,性能较高**,建议使用,也是默认配置 +- everysec(每秒):每秒将缓冲区中的指令同步到 AOF 文件中,在系统突然宕机的情况下丢失 1 秒内的数据 数据**准确性较高,性能较高**,建议使用,也是默认配置 - no(系统控制):由操作系统控制每次同步到 AOF 文件的周期,整体过程**不可控** @@ -9928,7 +9909,7 @@ AOF 持久化数据的三种策略(appendfsync): * 同步硬盘操作依赖于系统调度机制,比如缓冲区页空间写满或达到特定时间周期。同步文件之前,如果此时系统故障宕机,缓冲区内数据将丢失 * fsync 针对单个文件操作(比如 AOF 文件)做强制硬盘同步,fsync 将阻塞到写入硬盘完成后返回,保证了数据持久化 -异常恢复:AOF 文件损坏,通过 **redis-check-aof--fix appendonly.aof** 进行恢复,重启 redis,然后重新加载 +异常恢复:AOF 文件损坏,通过 redis-check-aof--fix appendonly.aof 进行恢复,重启 redis,然后重新加载 @@ -9963,7 +9944,7 @@ AOF 重写规则: 如 lpushlist1 a、lpush list1 b、lpush list1 c 可以转化为:lpush list1 a b c。 - 为防止数据量过大造成客户端缓冲区溢出,对 list、set、hash、zset 等类型,每条指令最多写入64个元素 + 为防止数据量过大造成客户端缓冲区溢出,对 list、set、hash、zset 等类型,每条指令最多写入 64 个元素 @@ -9997,8 +9978,8 @@ AOF 重写规则: 自动重写触发比对参数( 运行指令 `info Persistence` 获取具体信息 ): ```sh - aof_current_size #AOF文件当前尺寸大小(单位:字节) - aof_base_size #AOF文件上次启动和重写时的尺寸大小(单位:字节) + aof_current_size #AOF文件当前尺寸大小(单位:字节) + aof_base_size #AOF文件上次启动和重写时的尺寸大小(单位:字节) ``` 自动重写触发条件公式: @@ -10022,7 +10003,7 @@ AOF 重写规则: 重写流程: - +![](https://gitee.com/seazean/images/raw/master/DB/Redis-AOF重写流程2.png) 使用**新的 AOF 文件覆盖旧的 AOF 文件**,完成 AOF 重写 @@ -10051,9 +10032,9 @@ AOF 和 RDB 同时开启,系统默认取 AOF 的数据(数据不会存在丢 - 对数据**非常敏感**,建议使用默认的 AOF 持久化方案 - AOF 持久化策略使用 everysecond,每秒钟 fsync 一次,该策略 redis 仍可以保持很好的处理性能,当出现问题时,最多丢失0-1秒内的数据。 + AOF 持久化策略使用 everysecond,每秒钟 fsync 一次,该策略 redis 仍可以保持很好的处理性能,当出现问题时,最多丢失 1 秒内的数据。 - 注意:AOF文件存储体积较大,恢复速度较慢,因为要执行每条指令 + 注意:AOF 文件存储体积较大,恢复速度较慢,因为要执行每条指令 - 数据呈现**阶段有效性**,建议使用 RDB 持久化方案 @@ -10190,7 +10171,7 @@ int main(void) fork() 调用之后父子进程的内存关系 -早期 Linux 的 fork() 实现时,就是全部复制,这种方法效率太低,而且造成了很大的内存浪费,现在 Linux 实现采用了两种方法来规避这种浪费: +早期 Linux 的 fork() 实现时,就是全部复制,这种方法效率太低,而且造成了很大的内存浪费,现在 Linux 实现采用了两种方法: * 父子进程的代码段是相同的,所以代码段是没必要复制的,只需内核将代码段标记为只读,父子进程就共享此代码段。fork() 之后在进程创建代码段时,子进程的进程级页表项都指向和父进程相同的物理页帧 @@ -10252,7 +10233,7 @@ Redis 事务的三大特性: * Redis 事务是一个单独的隔离操作,将一系列预定义命令包装成一个整体(一个队列),当执行时按照添加顺序依次执行,中间不会被打断或者干扰 * Redis 事务**没有隔离级别**的概念,队列中的命令在事务没有提交之前都不会实际被执行 -* Redis 单条命令式保存原子性的,但是事务不保证原子性,事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚 +* Redis 单条命令式保存原子性的,但是事务**不保证原子性**,事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚 @@ -10404,7 +10385,7 @@ TTL 返回的值有三种情况:正数,-1,-2 删除策略:**删除策略就是针对已过期数据的处理策略**,已过期的数据不一定被立即删除,在不同的场景下使用不同的删除方式会有不同效果,这就是删除策略的问题 -过期数据是一块独立的存储空间,Hash结构,field是内存地址,value是过期时间,保存了所有key的过期描述,在最终进行过期处理的时候,对该空间的数据进行检测, 当时间到期之后通过field找到内存该地址处的数据,然后进行相关操作 +过期数据是一块独立的存储空间,Hash 结构,field 是内存地址,value 是过期时间,保存了所有 key 的过期描述,在最终进行过期处理的时候,对该空间的数据进行检测, 当时间到期之后通过 field 找到内存该地址处的数据,然后进行相关操作 @@ -10418,7 +10399,7 @@ TTL 返回的值有三种情况:正数,-1,-2 #### 删除策略 -在内存占用与 CPU 占用之间寻找一种平衡,顾此失彼都会造成整体 redis 性能的下降,甚至引发服务器宕机或内存泄露 +在内存占用与 CPU 占用之间寻找一种平衡,顾此失彼都会造成整体 Redis 性能的下降,甚至引发服务器宕机或内存泄露 针对过期数据有三种删除策略: @@ -10479,7 +10460,7 @@ TTL 返回的值有三种情况:正数,-1,-2 - 每秒钟执行 server.hz 次 serverCron() → databasesCron() → activeExpireCycle() -- databasesCron() 操作是轮询每个数据库 +- databasesCron() 操作是**轮询每个数据库** - activeExpireCycle() 对某个数据库中的每个 expires 进行检测,每次执行耗时:250ms/server.hz @@ -10487,8 +10468,8 @@ TTL 返回的值有三种情况:正数,-1,-2 - 如果 key 超时,删除 key - 如果一轮中删除的 key 的数量 > W*25%,循环该过程 - - 如果一轮中删除的 key 的数量 ≤ W*25%,检查下一个expires[],0-15循环 - - W取值 = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 属性值,自定义值 + - 如果一轮中删除的 key 的数量 ≤ W*25%,检查下一个expires[],0-15 循环 + - W 取值 = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 属性值,自定义值 * 参数 current_db 用于记录 activeExpireCycle() 进入哪个expires[*] 执行 * 如果 activeExpireCycle() 执行时间到期,下次从 current_db 继续向下执行 @@ -10525,7 +10506,7 @@ TTL 返回的值有三种情况:正数,-1,-2 #### 逐出算法 -数据淘汰策略:当新数据进入 redis 时,在执行每一个命令前,会调用 **freeMemoryIfNeeded()** 检测内存是否充足。如果内存不满足新加入数据的最低存储要求,redis 要临时删除一些数据为当前指令清理存储空间,清理数据的策略称为**逐出算法** +数据淘汰策略:当新数据进入 Redis 时,在执行每一个命令前,会调用 **freeMemoryIfNeeded()** 检测内存是否充足。如果内存不满足新加入数据的最低存储要求,Redis 要临时删除一些数据为当前指令清理存储空间,清理数据的策略称为**逐出算法** 逐出数据的过程不是 100% 能够清理出足够的可使用的内存空间,如果不成功则反复执行,当对所有数据尝试完毕,如不能达到内存清理的要求,**出现 Redis 内存打满异常**: @@ -10615,12 +10596,12 @@ Redis 如果不设置最大内存大小或者设置最大内存大小为 0,在 - 高可用: - 可用性:应用服务在全年宕机的时间加在一起就是全年应用服务不可用的时间 - - 业界可用性目标5个9,即99.999%,即服务器年宕机时长低于315秒,约5.25分钟 + - 业界可用性目标 5 个 9,即 99.999%,即服务器年宕机时长低于 315 秒,约 5.25 分钟 主从复制: * 概念:将 master 中的数据即时、有效的复制到 slave 中 -* 特征:一个 master 可以拥有多个 slave,一个slave 只对应一个 master +* 特征:一个 master 可以拥有多个 slave,一个 slave 只对应一个 master * 职责:master 和 slave 各自的职责不一样 master: @@ -10645,8 +10626,8 @@ Redis 如果不设置最大内存大小或者设置最大内存大小为 0,在 - **读写分离**:master 写、slave 读,提高服务器的读写负载能力 - **负载均衡**:基于主从结构,配合读写分离,由 slave 分担 master 负载,并根据需求的变化,改变 slave 的数量,通过多个从节点分担数据读取负载,大大提高 Redis 服务器并发量与数据吞吐量 -- **故障恢复**:当 master 出现问题时,由 slave 提供服务,实现快速的故障恢复 -- **数据冗余**:实现数据热备份,是持久化之外的一种数据冗余方式 +- 故障恢复:当 master 出现问题时,由 slave 提供服务,实现快速的故障恢复 +- 数据冗余:实现数据热备份,是持久化之外的一种数据冗余方式 - 高可用基石:基于主从复制,构建哨兵模式与集群,实现 Redis 的高可用方案 主从复制的应用场景: @@ -10753,51 +10734,47 @@ Redis 如果不设置最大内存大小或者设置最大内存大小为 0,在 info replication ``` -* 主从断开连接 - - 断开 slave 与 master 的连接,slave 断开连接后,不会删除已有数据,只是不再接受 master 发送的数据 +* 主从断开连接:断开 slave 与 master 的连接,slave 断开连接后,不会删除已有数据,只是不再接受 master 发送的数据 slave客户端执行命令: ```sh slaveof no one ``` - -* 授权访问 - - master 有服务端和客户端,slave 也有服务端和客户端,不仅服务端之间可以发命令,客户端也可以 + +* 授权访问:master 有服务端和客户端,slave 也有服务端和客户端,不仅服务端之间可以发命令,客户端也可以 master 客户端发送命令设置密码: ```sh requirepass password ``` - + master 配置文件设置密码: ```sh config set requirepass password config get requirepass ``` - + slave 客户端发送命令设置密码: ```sh auth password ``` - + slave 配置文件设置密码: ```sh masterauth password ``` - + slave 启动服务器设置密码: ```sh redis-server –a password ``` - + *** @@ -10817,7 +10794,7 @@ Redis 如果不设置最大内存大小或者设置最大内存大小为 0,在 1. 请求同步数据 2. 创建 RDB 同步数据 -3. 恢复 RDB 同步数据(清空原有数据) +3. 恢复 RDB 同步数据(从服务器会**清空原有数据**) 4. 请求部分同步数据 5. 恢复部分同步数据 6. 数据同步工作完成 @@ -11058,9 +11035,9 @@ slave 与 master 连接断开 解决方案: -* 优化主从间的网络环境,通常放置在同一个机房部署,如使用阿里云等云服务器时要注意此现象 +* **优化主从间的网络环境**,通常放置在同一个机房部署,如使用阿里云等云服务器时要注意此现象 -* 监控主从节点延迟(通过offset)判断,如果 slave 延迟过大,暂时屏蔽程序对该 slave 的数据访问 +* 监控主从节点延迟(通过offset)判断,如果 slave 延迟过大,**暂时屏蔽程序对该 slave 的数据访问** ```sh slave-serve-stale-data yes|no @@ -11080,22 +11057,22 @@ slave 与 master 连接断开 ### 哨兵概述 -如果 redis 的 master 宕机了,需要从 slave 中重新选出一个 master,要实现这些功能就需要 redis 的哨兵 +如果 Redis 的 master 宕机了,需要从 slave 中重新选出一个 master,要实现这些功能就需要 Redis 的哨兵 -哨兵 (sentinel) 是一个分布式系统,用于对主从结构中的每台服务器进行**监控**,当出现故障时通过**投票机制选择**新的 master 并将所有 slave 连接到新的 master +哨兵(sentinel)是一个分布式系统,用于对主从结构中的每台服务器进行**监控**,当出现故障时通过**投票机制选择**新的 master 并将所有 slave 连接到新的 master 哨兵的作用: -- 监控:监控master和slave,不断的检查master和slave是否正常运行,master存活检测、master与slave运行情况检测 +- 监控:监控 master 和 slave,不断的检查 master 和 slave 是否正常运行,master 存活检测、master 与 slave 运行情况检测 -- 通知 (提醒):当被监控的服务器出现问题时,向其他(哨兵间,客户端)发送通知 +- 通知:当被监控的服务器出现问题时,向其他(哨兵间,客户端)发送通知 - 自动故障转移:断开 master 与 slave 连接,选取一个 slave 作为 master,将其他 slave 连接新的 master,并告知客户端新的服务器地址 -注意:哨兵也是一台redis服务器,只是不提供数据相关服务,通常哨兵的数量配置为单数(投票) +注意:哨兵也是一台 Redis 服务器,只是不提供数据相关服务,通常哨兵的数量配置为单数(投票) @@ -11129,7 +11106,7 @@ slave 与 master 连接断开 sentinel monitor master_name master_host master_port sentinel_number ``` - * 指定哨兵在监控Redis服务时,设置判定服务器宕机的时长,该设置控制是否进行主从切换 + * 指定哨兵在监控 Redis 服务时,设置判定服务器宕机的时长,该设置控制是否进行主从切换 ```sh sentinel down-after-milliseconds master_name million_seconds @@ -11141,7 +11118,7 @@ slave 与 master 连接断开 sentinel failover-timeout master_name million_seconds ``` - * 指定同时进行主从的slave数量,数值越大,要求网络资源越高,要求约小,同步时间约长 + * 指定同时进行主从的 slave 数量,数值越大,要求网络资源越高,要求约小,同步时间约长 ```sh sentinel parallel-syncs master_name sync_slave_number @@ -11149,7 +11126,7 @@ slave 与 master 连接断开 启动哨兵: -* 服务端命令(Linux命令): +* 服务端命令(Linux 命令): ```sh redis-sentinel filename @@ -11163,7 +11140,7 @@ slave 与 master 连接断开 ### 工作原理 -#### 三个阶段 +#### 监控阶段 哨兵在进行主从切换过程中经历三个阶段 @@ -11171,16 +11148,12 @@ slave 与 master 连接断开 - 通知 - 故障转移 +监控阶段作用:同步各个节点的状态信息 - -#### 监控阶段 - -作用:同步各个节点的状态信息 - -* 获取各个sentinel的状态(是否在线) +* 获取各个 sentinel 的状态(是否在线) -- 获取master的状态 +- 获取 master 的状态 ```markdown master属性 @@ -11189,7 +11162,7 @@ slave 与 master 连接断开 各个slave的详细信息 ``` -- 获取所有slave的状态(根据master中的slave信息) +- 获取所有 slave 的状态(根据 master 中的 slave 信息) ```markdown slave属性 @@ -11201,9 +11174,9 @@ slave 与 master 连接断开 内部的工作原理: -sentinel 1首先连接 master,建立 cmd 通道,根据主节点访问从节点,连接完成 +sentinel 1 首先连接 master,建立 cmd 通道,根据主节点访问从节点,连接完成 -sentinel 2 首先连接 master,然后通过 master 中的 sentinels 发现其他哨兵,然后寻找哨兵建立连接,同步数据 +sentinel 2 首先连接 master,然后通过 master 中的 sentinels 发现其他哨兵,然后寻找哨兵建立连接,哨兵之间同步数据 @@ -11245,11 +11218,10 @@ sentinel 在通知阶段不断的去获取 master/slave 的信息,然后在各 - 不在线的 OUT - - 响应慢的 OUT +- 响应慢的 OUT +- 与原 master 断开时间久的 OUT - - 与原 master 断开时间久的 OUT - - - 优先原则:优先级 → offset → runid +- 优先原则:先根据优先级 → offset → runid 选出新的 master之后,发送指令(sentinel )给其他的 slave @@ -11288,17 +11260,17 @@ sentinel 在通知阶段不断的去获取 master/slave 的信息,然后在各 **数据存储设计:** -1. 通过算法设计,计算出key应该保存的位置 +1. 通过算法设计,计算出 key 应该保存的位置 ```markdown key -> CRC16(key) -> 值 -> %16384 -> 存储位置 ``` -2. 将所有的存储空间计划切割成16384份,每台主机保存一部分 +2. 将所有的存储空间计划切割成 16384 份,每台主机保存一部分 - 注意:每份代表的是一个存储空间,不是一个key的保存空间,可以存储多个key + 注意:每份代表的是一个存储空间,不是一个 key 的保存空间,可以存储多个 key -3. 将key按照计算出的结果放到对应的存储空间 +3. 将 key 按照计算出的结果放到对应的存储空间 @@ -11327,7 +11299,7 @@ sentinel 在通知阶段不断的去获取 master/slave 的信息,然后在各 - 分槽(Slot) - 搭建主从(master-slave) -创建集群conf配置文件: +创建集群 conf 配置文件: * redis-6501.conf @@ -11354,15 +11326,15 @@ sentinel 在通知阶段不断的去获取 master/slave 的信息,然后在各 redis-cli -p 6504 -c ``` -**cluster配置:** +**cluster 配置:** -- 是否启用cluster,加入cluster节点 +- 是否启用 cluster,加入 cluster 节点 ```properties cluster-enabled yes|no ``` -- cluster配置文件名,该文件属于自动生成,仅用于快速查找文件并查询文件内容 +- cluster 配置文件名,该文件属于自动生成,仅用于快速查找文件并查询文件内容 ```properties cluster-config-file filename @@ -11374,7 +11346,7 @@ sentinel 在通知阶段不断的去获取 master/slave 的信息,然后在各 cluster-node-timeout milliseconds ``` -- master连接的slave最小数量 +- master 连接的 slave 最小数量 ```properties cluster-migration-barrier min_slave_number @@ -11382,8 +11354,7 @@ sentinel 在通知阶段不断的去获取 master/slave 的信息,然后在各 客户端启动命令: - -**cluster节点操作命令(客户端命令):** +**cluster 节点操作命令(客户端命令):** - 查看集群节点信息 @@ -11391,19 +11362,19 @@ sentinel 在通知阶段不断的去获取 master/slave 的信息,然后在各 cluster nodes ``` -- 更改slave指向新的master +- 更改 slave 指向新的 master ```properties cluster replicate master-id ``` -- 发现一个新节点,新增master +- 发现一个新节点,新增 master ```properties cluster meet ip:port ``` -- 忽略一个没有solt的节点 +- 忽略一个没有 solt 的节点 ```properties cluster forget server_id @@ -11423,37 +11394,37 @@ sentinel 在通知阶段不断的去获取 master/slave 的信息,然后在各 redis-cli –-cluster create masterhost1:masterport1 masterhost2:masterport2 masterhost3:masterport3 [masterhostn:masterportn …] slavehost1:slaveport1 slavehost2:slaveport2 slavehost3:slaveport3 -–cluster-replicas n ``` - 注意:master与slave的数量要匹配,一个master对应n个slave,由最后的参数n决定。master与slave的匹配顺序为第一个master与前n个slave分为一组,形成主从结构 + 注意:master 与 slave 的数量要匹配,一个 master 对应 n 个 slave,由最后的参数 n 决定。master 与 slave 的匹配顺序为第一个 master 与前 n 个 slave 分为一组,形成主从结构 -* 添加master到当前集群中,连接时可以指定任意现有节点地址与端口 +* 添加 master 到当前集群中,连接时可以指定任意现有节点地址与端口 ```properties redis-cli --cluster add-node new-master-host:new-master-port now-host:now-port ``` -* 添加slave +* 添加 slave ```properties redis-cli --cluster add-node new-slave-host:new-slave-port master-host:master-port --cluster-slave --cluster-master-id masterid ``` -* 删除节点,如果删除的节点是master,必须保障其中没有槽slot +* 删除节点,如果删除的节点是 master,必须保障其中没有槽 slot ```properties redis-cli --cluster del-node del-slave-host:del-slave-port del-slave-id ``` -* 重新分槽,分槽是从具有槽的master中划分一部分给其他master,过程中不创建新的槽 +* 重新分槽,分槽是从具有槽的 master 中划分一部分给其他 master,过程中不创建新的槽 ```properties redis-cli --cluster reshard new-master-host:new-master:port --cluster-from src- master-id1, src-master-id2, src-master-idn --cluster-to target-master-id -- cluster-slots slots ``` - 注意:将需要参与分槽的所有masterid不分先后顺序添加到参数中,使用`,`分隔,指定目标得到的槽的数量,所有的槽将平均从每个来源的master处获取 + 注意:将需要参与分槽的所有 masterid 不分先后顺序添加到参数中,使用 `,` 分隔,指定目标得到的槽的数量,所有的槽将平均从每个来源的 master 处获取 -* 重新分配槽,从具有槽的master中分配指定数量的槽到另一个master中,常用于清空指定master中的槽 +* 重新分配槽,从具有槽的 master 中分配指定数量的槽到另一个 master 中,常用于清空指定 master 中的槽 ```properties redis-cli --cluster reshard src-master-host:src-master-port --cluster-from src- master-id --cluster-to target-master-id --cluster-slots slots --cluster-yes @@ -11541,11 +11512,11 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 缓存不一致的方法: -* 数据库和缓存数据**强一致**场景 : +* 数据库和缓存数据强一致场景 : * 更新 DB 时同样更新 cache,加一个锁来保证更新 cache 时不存在线程安全问题,这样可以增加命中率 - * 延迟双删:先淘汰缓存再写数据库,休眠1秒再次淘汰缓存,可以将 1 秒内造成的缓存脏数据再次删除 + * 延迟双删:先淘汰缓存再写数据库,休眠 1 秒再次淘汰缓存,可以将 1 秒内造成的缓存脏数据再次删除 * CDC 同步:通过 canal 订阅 MySQL binlog 的变更上报给 Kafka,系统监听 Kafka 消息触发缓存失效 -* 可以短暂地允许数据库和缓存数据**不一致**场景:更新 DB 的时候同样更新 cache,但是给缓存加一个比较短的过期时间,这样就可以保证即使数据不一致影响也比较小 +* 可以短暂允许数据库和缓存数据不一致场景:更新 DB 的时候同样更新 cache,但是给缓存加一个比较短的过期时间,这样就可以保证即使数据不一致影响也比较小 @@ -11591,7 +11562,7 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 5. 如果条件允许,使用了 CDN(内容分发网络),效果会更好 -总的来说:缓存预热就是系统启动前,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据! +总的来说:缓存预热就是系统启动前,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题,用户直接查询事先被预热的缓存数据! @@ -11601,7 +11572,7 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 #### 缓存雪崩 -场景:数据库服务器崩溃,一连串的问题会随之而来。系统平稳运行过程中,忽然数据库连接量激增,应用服务器无法及时处理请求,大量 408,500 错误页面出现,客户反复刷新页面获取数据,造成数据库崩溃、应用服务器崩溃、重启应用服务器无效、Redis 服务器崩溃、Redis 集群崩溃、重启数据库后再次被瞬间流量放倒 +场景:数据库服务器崩溃,一连串的问题会随之而来 问题排查:在一个较短的时间内,**缓存中较多的 key 集中过期**,此周期内请求访问过期的数据 Redis 未命中,Redis 向数据库获取数据,数据库同时收到大量的请求无法及时处理。 @@ -11611,11 +11582,11 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 1. 更多的页面静态化处理 - 2. 构建**多级缓存**架构:Nginx 缓存 + redis 缓存 + ehcache 缓存 + 2. 构建**多级缓存**架构:Nginx 缓存 + Redis 缓存 + ehcache 缓存 - 3. 检测 Mysql 严重耗时业务进行优化:对数据库的瓶颈排查,例如超时查询、耗时较高事务等 + 3. 检测 MySQL 严重耗时业务进行优化:对数据库的瓶颈排查,例如超时查询、耗时较高事务等 - 4. 灾难预警机制:监控 redis 服务器性能指标,CPU 使用率、内存容量、平均响应时间、线程数 + 4. 灾难预警机制:监控 Redis 服务器性能指标,CPU 使用率、内存容量、平均响应时间、线程数 5. 限流、降级:短时间范围内牺牲一些客户体验,限制一部分请求访问,降低应用服务器压力,待业务低速运转后再逐步放开访问 @@ -11642,7 +11613,7 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 #### 缓存击穿 -场景:系统平稳运行过程中,数据库连接量瞬间激增,Redis 服务器无大量 key 过期,Redis 内存平稳,无波动,Redis 服务器 CPU 正常,但是数据库崩溃 +场景:系统平稳运行过程中,数据库连接量瞬间激增,Redis 服务器无大量 key 过期,Redis 内存平稳无波动,Redis 服务器 CPU 正常,但是数据库崩溃 问题排查: @@ -11664,7 +11635,7 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 4. **二级缓存**:设置不同的失效时间,保障不会被同时淘汰就行 -5. 加锁:分布式锁,防止被击穿,但是要注意也是性能瓶颈,慎重! +5. 加锁:分布式锁,防止被击穿,但是要注意也是性能瓶颈,慎重 总的来说:缓存击穿就是单个高热数据过期的瞬间,数据访问量较大,未命中 redis 后,发起了大量对同一数据的数据库访问,导致对数据库服务器造成压力。应对策略应该在业务数据分析与预防方面进行,配合运行监控测试与即时调整策略,毕竟单个 key 的过期监控难度较高,配合雪崩处理策略即可 @@ -11686,27 +11657,26 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 问题分析: -- 获取的数据在数据库中也不存在,数据库查询未得到对应数据 +- 访问了不存在的数据,跳过了 Redis 缓存,数据库页查询不到对应数据 - Redis 获取到 null 数据未进行持久化,直接返回 -- 下次此类数据到达重复上述过程 - 出现黑客攻击服务器 解决方案: -1. 缓存 null:对查询结果为 null 的数据进行缓存 (长期使用,定期清理) ,设定短时限,例如 30-60 秒,最高 5 分钟 +1. 缓存 null:对查询结果为 null 的数据进行缓存,设定短时限,例如 30-60 秒,最高 5 分钟 2. 白名单策略:提前预热各种分类数据 id 对应的 **bitmaps**,id 作为 bitmaps 的 offset,相当于设置了数据白名单。当加载正常数据时放行,加载异常数据时直接拦截(效率偏低),也可以使用布隆过滤器(有关布隆过滤器的命中问题对当前状况可以忽略) -3. 实时监控:实时监控 redis 命中率(业务正常范围时,通常会有一个波动值)与 null 数据的占比 +3. 实时监控:实时监控 Redis 命中率(业务正常范围时,通常会有一个波动值)与 null 数据的占比 - * 非活动时段波动:通常检测3-5倍,超过5倍纳入重点排查对象 - * 活动时段波动:通常检测10-50倍,超过50倍纳入重点排查对象 + * 非活动时段波动:通常检测 3-5 倍,超过 5 倍纳入重点排查对象 + * 活动时段波动:通常检测10-50 倍,超过 50 倍纳入重点排查对象 ​ 根据倍数不同,启动不同的排查流程。然后使用黑名单进行防控(运营) -4. key 加密:临时启动防灾业务 key,对 key 进行业务层传输加密服务,设定校验程序,过来的 key 校验;例如每天随机分配60个加密串,挑选2到3个,混淆到页面数据 id 中,发现访问 key 不满足规则,驳回数据访问 +4. key 加密:临时启动防灾业务 key,对 key 进行业务层传输加密服务,设定校验程序,过来的 key 校验;例如每天随机分配 60 个加密串,挑选 2 到 3 个,混淆到页面数据 id 中,发现访问 key 不满足规则,驳回数据访问 -总的来说:缓存击穿是指访问了不存在的数据,跳过了合法数据的 redis 数据缓存阶段,每次访问数据库,导致对数据库服务器造成压力。通常此类数据的出现量是一个较低的值,当出现此类情况以毒攻毒,并及时报警。无论是黑名单还是白名单,都是对整体系统的压力,警报解除后尽快移除 +总的来说:缓存击穿是指访问了不存在的数据,跳过了合法数据的 Redis 数据缓存阶段,每次访问数据库,导致对数据库服务器造成压力。通常此类数据的出现量是一个较低的值,当出现此类情况以毒攻毒,并及时报警。无论是黑名单还是白名单,都是对整体系统的压力,警报解除后尽快移除 @@ -11722,7 +11692,7 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 ### 性能指标 -redis中的监控指标如下: +Redis 中的监控指标如下: * 性能指标:Performance @@ -11778,7 +11748,7 @@ redis中的监控指标如下: connected_clients ``` - 当前连接slave总数: + 当前连接 slave 总数: ```sh connected_slaves @@ -11790,7 +11760,7 @@ redis中的监控指标如下: master_last_io_seconds_ago ``` - key的总数: + key 的总数: ```sh keyspace @@ -11798,13 +11768,13 @@ redis中的监控指标如下: * 持久性指标:Persistence - 当前服务器其最后一次RDB持久化的时间: + 当前服务器其最后一次 RDB 持久化的时间: ```sh rdb_last_save_time ``` - 当前服务器最后一次RDB持久化后数据变化总量: + 当前服务器最后一次 RDB 持久化后数据变化总量: ```sh rdb_changes_since_last_save @@ -11849,7 +11819,7 @@ redis中的监控指标如下: redis-benchmark [-h ] [-p ] [-c ] [-n [-k ] ``` - 范例:100个连接,5000次请求对应的性能 + 范例:100 个连接,5000 次请求对应的性能 ```sh redis-benchmark -c 100 -n 5000 diff --git a/Java.md b/Java.md index 0721a91..0725e7e 100644 --- a/Java.md +++ b/Java.md @@ -7663,7 +7663,7 @@ public class SerializeDemo01 { User user = new User("seazean","980823","七十一"); // 2.创建低级的字节输出流通向目标文件 OutputStream os = new FileOutputStream("Day10Demo/src/obj.dat"); - // 3.把低级的字节输出流包装成高级的对象字节输出流ObjectOutputStream + // 3.把低级的字节输出流包装成高级的对象字节输出流 ObjectOutputStream ObjectOutputStream oos = new ObjectOutputStream(os); // 4.通过对象字节输出流序列化对象: oos.writeObject(user); @@ -7684,6 +7684,17 @@ class User implements Serializable { } ``` +```java +// 序列化为二进制数据 +ByteArrayOutputStream bos = new ByteArrayOutputStream(); +ObjectOutputStream oos = new ObjectOutputStream(bos); +oos.writeObject(obj); // 将该对象序列化为二进制数据 +oos.flush(); +byte[] bytes = bos.toByteArray(); +``` + + + **** @@ -8104,7 +8115,7 @@ Class 类下的方法: | ---------------------- | ------------------------------------------------------------ | | String getSimpleName() | 获得类名字符串:类名 | | String getName() | 获得类全名:包名+类名 | -| T newInstance() | 创建Class对象关联类的对象,底层是调用无参数构造器,已经被淘汰 | +| T newInstance() | 创建 Class 对象关联类的对象,底层是调用无参数构造器,已经被淘汰 | ```java public class ReflectDemo{ @@ -8247,13 +8258,13 @@ public class TestStudent02 { Field 的方法:给成员变量赋值和取值 -| 方法 | 作用 | -| ---------------------------------- | --------------------------------------------------------- | -| void set(Object obj, Object value) | 给对象注入某个成员变量数据,**obj是对象**,value是值 | -| Object get(Object obj) | 获取指定对象的成员变量的值,**obj是对象**,没有对象为null | -| void setAccessible(true) | 暴力反射,设置为可以直接访问私有类型的属性 | -| Class getType() | 获取属性的类型,返回Class对象 | -| String getName() | 获取属性的名称 | +| 方法 | 作用 | +| ---------------------------------- | ----------------------------------------------------------- | +| void set(Object obj, Object value) | 给对象注入某个成员变量数据,**obj 是对象**,value 是值 | +| Object get(Object obj) | 获取指定对象的成员变量的值,**obj 是对象**,没有对象为 null | +| void setAccessible(true) | 暴力反射,设置为可以直接访问私有类型的属性 | +| Class getType() | 获取属性的类型,返回 Class 对象 | +| String getName() | 获取属性的名称 | ```Java public class FieldDemo { @@ -8332,7 +8343,8 @@ public class FieldDemo02 { * Method[] getDeclaredMethods():获得类中的所有成员方法对象,返回数组,只获得本类申明的方法 Method 常用 API: -`public Object invoke(Object obj, Object... args) `:使用指定的参数调用由此方法对象,obj 对象名 + +* public Object invoke(Object obj, Object... args):使用指定的参数调用由此方法对象,obj 对象名 ```java public class MethodDemo{ @@ -11090,7 +11102,7 @@ G1 的设计原则就是简化 JVM 性能调优,只需要简单的三步即可 ZGC 收集器是一个可伸缩的、低延迟的垃圾收集器,基于 Region 内存布局的,不设分代,使用了读屏障、染色指针和内存多重映射等技术来实现**可并发的标记压缩算法** * 在 CMS 和 G1 中都用到了写屏障,而 ZGC 用到了读屏障 -* 染色指针:直接将少量额外的信息存储在指针上的技术,从 64 位的指针中拿高 4 位来标识对象此时的状态 +* 染色指针:直接**将少量额外的信息存储在指针上的技术**,从 64 位的指针中拿高 4 位来标识对象此时的状态 * 染色指针可以使某个 Region 的存活对象被移走之后,这个 Region 立即就能够被释放和重用 * 可以直接从指针中看到引用对象的三色标记状态(Marked0、Marked1)、是否进入了重分配集、是否被移动过(Remapped)、是否只能通过 finalize() 方法才能被访问到(Finalizable) * 可以大幅减少在垃圾收集过程中内存屏障的使用数量,写屏障的目的通常是为了记录对象引用的变动情况,如果将这些信息直接维护在指针中,显然就可以省去一些专门的记录操作 @@ -11107,9 +11119,9 @@ ZGC 目标: ZGC 的工作过程可以分为 4 个阶段: -* 并发标记(Concurrent Mark): **遍历对象图做可达性分析**的阶段,也要经过初始标记和最终标记,需要短暂停顿 +* 并发标记(Concurrent Mark): 遍历对象图做可达性分析的阶段,也要经过初始标记和最终标记,需要短暂停顿 * 并发预备重分配( Concurrent Prepare for Relocate): 根据特定的查询条件统计得出本次收集过程要清理哪些 Region,将这些 Region 组成重分配集(Relocation Set) -* 并发重分配(Concurrent Relocate): 重分配是 ZGC 执行过程中的核心阶段,这个过程要把重分配集中的**存活对象**复制到新的 Region 上,并为重分配集中的**每个 Region 维护一个转发表**(Forward Table),记录从旧地址到新地址的转向关系 +* 并发重分配(Concurrent Relocate): 重分配是 ZGC 执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的 Region 上,并为重分配集中的**每个 Region 维护一个转发表**(Forward Table),记录从旧地址到新地址的转向关系 * 并发重映射(Concurrent Remap):修正整个堆中指向重分配集中旧对象的所有引用,ZGC 的并发映射并不是一个必须要立即完成的任务,ZGC 很巧妙地把并发重映射阶段要做的工作,合并到下一次垃圾收集循环中的并发标记阶段里去完成,因为都是要遍历所有对象的,这样合并节省了一次遍历的开销 ZGC 几乎在所有地方并发执行的,除了初始标记的是 STW 的,但这部分的实际时间是非常少的,所以响应速度快,在尽可能对吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟 @@ -16009,11 +16021,11 @@ public class BucketSort { ## 查找 -正常查找:从第一个元素开始遍历,一个一个的往后找,综合查找比较耗时。 +正常查找:从第一个元素开始遍历,一个一个的往后找,综合查找比较耗时 二分查找也称折半查找(Binary Search)是一种效率较高的查找方法,数组必须是有序数组 -过程:每次先与中间的元素进行比较,如果大于往右边找,如果小于往左边找,如果等于就返回该元素索引位置!如果没有该元素,返回-1 +过程:每次先与中间的元素进行比较,如果大于往右边找,如果小于往左边找,如果等于就返回该元素索引位置,如果没有该元素,返回 -1 时间复杂度:O(logn) @@ -16925,11 +16937,11 @@ public class MGraph { ### 基本介绍 -布隆过滤器:一种数据结构,是一个很长的二进制向量(位数组)和一系列随机映射函数(哈希函数),既然是二进制,每个空间存放的不是0就是1,但是初始默认值都是0,所以布隆过滤器不存数据只存状态 +布隆过滤器:一种数据结构,是一个很长的二进制向量(位数组)和一系列随机映射函数(哈希函数),既然是二进制,每个空间存放的不是 0 就是 1,但是初始默认值都是 0,所以布隆过滤器不存数据只存状态 -这种数据结构是高效且性能很好的,但缺点是具有一定的错误识别率和删除难度。并且,理论情况下,添加到集合中的元素越多,误报的可能性就越大 +这种数据结构是高效且性能很好的,但缺点是具有一定的错误识别率和删除难度。并且理论情况下,添加到集合中的元素越多,误报的可能性就越大 @@ -16947,7 +16959,7 @@ public class MGraph { - 通过 K 个哈希函数计算该数据,对应计算出的 K 个hash值 - 通过 hash 值找到对应的二进制的数组下标 -- 判断方法:如果存在一处位置的二进制数据是0,那么该数据不存在。如果都是1,该数据存在集合中 +- 判断方法:如果存在一处位置的二进制数据是 0,那么该数据一定不存在。如果都是 1,则认为数据存在集合中(会误判) 布隆过滤器优缺点: @@ -16955,7 +16967,7 @@ public class MGraph { * 二进制组成的数组,占用内存极少,并且插入和查询速度都足够快 * 去重方便:当字符串第一次存储时对应的位数组下标设置为 1,当第二次存储相同字符串时,因为对应位置已设置为 1,所以很容易知道此值已经存在 * 缺点: - * 随着数据的增加,误判率会增加:添加数据是通过计算数据的hash值,不同的字符串可能哈希出来的位置相同,导致无法确定到底是哪个数据存在,**这种情况可以适当增加位数组大小或者调整哈希函数** + * 随着数据的增加,误判率会增加:添加数据是通过计算数据的 hash 值,不同的字符串可能哈希出来的位置相同,导致无法确定到底是哪个数据存在,**这种情况可以适当增加位数组大小或者调整哈希函数** * 无法删除数据:可能存在几个数据占据相同的位置,所以删除一位会导致很多数据失效 * 总结:**布隆过滤器判断某个元素存在,小概率会误判。如果判断某个元素不在,那这个元素一定不在** diff --git a/Prog.md b/Prog.md index 3f1db03..ea75a83 100644 --- a/Prog.md +++ b/Prog.md @@ -594,7 +594,7 @@ t.start(); 用户线程:平常创建的普通线程 -守护线程:服务于用户线程,只要其它非守护线程运行结束了,即使守护线程代码没有执行完,也会强制结束 +守护线程:服务于用户线程,只要其它非守护线程运行结束了,即使守护线程代码没有执行完,也会强制结束。守护进程是脱离于终端并且在后台运行的进程,脱离终端是为了避免在执行的过程中的信息在终端上显示 说明:当运行的线程都是守护线程,Java 虚拟机将退出,因为普通线程执行完后,JVM 是守护线程,不会继续运行下去 @@ -605,6 +605,8 @@ t.start(); + + *** @@ -12223,7 +12225,7 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引都指 // 索引层 level,从 1 开始,就是最底层 int level = 1, max; // 12.判断最低位前面有几个 1,有几个leve就加几:0..0 0001 1110,这是4个,则1+4=5 - // 最大有30个就是 1 + 30 = 31 + // 【最大有30个就是 1 + 30 = 31 while (((rnd >>>= 1) & 1) != 0) ++level; // 最终指向 z 节点,就是添加的节点 From 236289912f513ccbd2cfdbdba0d1b3384e4f78d3 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 28 Aug 2021 12:58:18 +0800 Subject: [PATCH 041/168] Update Java Notes --- DB.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/DB.md b/DB.md index 61b911f..d298c47 100644 --- a/DB.md +++ b/DB.md @@ -2919,7 +2919,9 @@ RC、RR 级别下的 InnoDB 快照读区别 解决幻读问题: -- 快照读:通过 MVCC 来进行控制的,在可重复读隔离级别下,普通查询是快照读,是不会看到别的事务插入的数据的,幻读只在当前读下才会出现 +- 快照读:通过 MVCC 来进行控制的,在可重复读隔离级别下,普通查询是快照读,是不会看到别的事务插入的数据的,但是**并不能完全避免幻读** + + 场景:RR 级别,T1 事务开启,创建 Read View,此时 T2 去 INSERT 新的一行然后提交,然后 T1去 UPDATE 该行会发现更新成功,因为 Read View 并不能阻止事务去更新数据,并且把这条新纪录的 trx_id 给变为当前的事务 id,对当前事务就是可见的了 - 当前读:通过 next-key 锁(行锁 + 间隙锁)来解决问题 From 048a5a17bfc0be89eb4c21a06667671adef6bafd Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 28 Aug 2021 12:59:47 +0800 Subject: [PATCH 042/168] Update Java Notes --- DB.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DB.md b/DB.md index d298c47..260d07f 100644 --- a/DB.md +++ b/DB.md @@ -2921,7 +2921,7 @@ RC、RR 级别下的 InnoDB 快照读区别 - 快照读:通过 MVCC 来进行控制的,在可重复读隔离级别下,普通查询是快照读,是不会看到别的事务插入的数据的,但是**并不能完全避免幻读** - 场景:RR 级别,T1 事务开启,创建 Read View,此时 T2 去 INSERT 新的一行然后提交,然后 T1去 UPDATE 该行会发现更新成功,因为 Read View 并不能阻止事务去更新数据,并且把这条新纪录的 trx_id 给变为当前的事务 id,对当前事务就是可见的了 + 场景:RR 级别,T1 事务开启,创建 Read View,此时 T2 去 INSERT 新的一行然后提交,然后 T1去 UPDATE 该行会发现更新成功,因为 Read View 并不能阻止事务去更新数据,并且把这条新记录的 trx_id 给变为当前的事务 id,对当前事务就是可见的了 - 当前读:通过 next-key 锁(行锁 + 间隙锁)来解决问题 From 50a6817a29fd8b2a76003b84dd29216c1d430ad8 Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 30 Aug 2021 18:57:26 +0800 Subject: [PATCH 043/168] Update Java Notes --- DB.md | 4 +- Java.md | 26 +- Prog.md | 160 +++++----- SSM.md | 941 +++++++++++++++++++++++++++++--------------------------- 4 files changed, 575 insertions(+), 556 deletions(-) diff --git a/DB.md b/DB.md index 260d07f..22e143a 100644 --- a/DB.md +++ b/DB.md @@ -8507,10 +8507,10 @@ Redis 基于 Reactor 模式开发了网络事件处理器,这个处理器被 * 当被监听的套接字准备好执行连接应答 (accept)、读取 (read)、写入 (write)、关闭 (close) 等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器会调用套接字之前关联好的事件处理器来处理事件 -Redis 单线程也能高效的原因: +**Redis 单线程也能高效的原因**: * 纯内存操作 -* 核心是基于非阻塞的 IO 多路复用机制 +* 核心是基于非阻塞的 IO 多路复用机制,单线程可以高效处理多个请求 * 底层使用 C 语言实现,C 语言实现的程序距离操作系统更近,执行速度相对会更快 * 单线程同时也**避免了多线程的上下文频繁切换问题**,预防了多线程可能产生的竞争问题 diff --git a/Java.md b/Java.md index 0725e7e..571792b 100644 --- a/Java.md +++ b/Java.md @@ -9415,23 +9415,23 @@ public class Dom4JDemo { Dom4J 可以用于解析整个 XML 的数据,但是如果要检索 XML 中的某些信息,建议使用 XPath -XPath常用API: +XPath 常用API: * List selectNodes(String var1) : 检索出一批节点集合 * Node selectSingleNode(String var1) : 检索出一个节点返回 -XPath提供的四种检索数据的写法: +XPath 提供的四种检索数据的写法: -1. 绝对路径:/根元素/子元素/子元素。 -2. 相对路径:./子元素/子元素。 (.代表了当前元素) +1. 绝对路径:/根元素/子元素/子元素 +2. 相对路径:./子元素/子元素 (.代表了当前元素) 3. 全文搜索: - * //元素 在全文找这个元素 - * //元素1/元素2 在全文找元素1下面的一级元素2 - * //元素1//元素2 在全文找元素1下面的全部元素2 -4. 属性查找。 - * //@属性名称 在全文检索属性对象。 - * //元素[@属性名称] 在全文检索包含该属性的元素对象。 - * //元素[@属性名称=值] 在全文检索包含该属性的元素且属性值为该值的元素对象。 + * //元素:在全文找这个元素 + * //元素1/元素2:在全文找元素1下面的一级元素 2 + * //元素1//元素2:在全文找元素1下面的全部元素 2 +4. 属性查找: + * //@属性名称:在全文检索属性对象 + * //元素[@属性名称]:在全文检索包含该属性的元素对象 + * //元素[@属性名称=值]:在全文检索包含该属性的元素且属性值为该值的元素对象 ```java public class XPathDemo { @@ -11360,7 +11360,7 @@ public Object pop() { unused(25+1) + hash(31) + age(4) + lock(3) = 64bit #64位系统 ``` - * **Klass Word**:类型指针,指向该对象的类对象的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;在 64 位系统中,开启指针压缩(-XX:+UseCompressedOops)或者 JVM 堆的最大值小于 32G,这个指针也是 4byte,否则是 8byte(就是 **Java 中的一个引用的大小**) + * **Klass Word**:类型指针,**指向该对象的 Class 类对象的指针**,虚拟机通过这个指针来确定这个对象是哪个类的实例;在 64 位系统中,开启指针压缩(-XX:+UseCompressedOops)或者 JVM 堆的最大值小于 32G,这个指针也是 4byte,否则是 8byte(就是 **Java 中的一个引用的大小**) ```ruby |-----------------------------------------------------| @@ -18829,7 +18829,7 @@ CGLIB 是一个功能强大,高性能的代码生成包,为没有实现接 public TrainStation getProxyObject() { //创建Enhancer对象,类似于JDK动态代理的Proxy类,下一步就是设置几个参数 - Enhancer enhancer =new Enhancer(); + Enhancer enhancer = new Enhancer(); //设置父类的字节码对象 enhancer.setSuperclass(target.getClass()); //设置回调函数 diff --git a/Prog.md b/Prog.md index ea75a83..ed62ae7 100644 --- a/Prog.md +++ b/Prog.md @@ -49,7 +49,7 @@ 同一台计算机的进程通信称为 IPC(Inter-process communication) * 信号量:信号量是一个计数器,用于多进程对共享数据的访问,解决同步相关的问题并避免竞争条件 - * 共享存储:多个进程可以访问同一块内存空间,需要使用信号量用来同步对共享存储的访问 + * 共享存储:多个进程可以访问同一块内存空间,需要使用信号量用来同步对共享存储的访问(MappedByteBuffer) * 管道通信:管道是用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,pipe 文件 * 匿名管道(Pipes):用于具有亲缘关系的父子进程间或者兄弟进程之间的通信,只支持半双工通信 * 命名管道(Names Pipes):以磁盘文件的方式存在,可以实现本机任意两个进程通信,遵循 FIFO @@ -955,25 +955,22 @@ public static void main(String[] args) { Monitor 被翻译为监视器或管程 -每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 -Mark Word 中就被设置指向 Monitor 对象的指针,这就是重量级锁 +每个 Java 对象都可以关联一个 Monitor 对象,Monitor 也是 class,其实例会存储在堆中,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针,这就是重量级锁 -* Mark Word结构: +* Mark Word 结构: ![](https://gitee.com/seazean/images/raw/master/Java/JUC-Monitor-MarkWord结构32位.png) -* 64位虚拟机Mark Word: +* 64位虚拟机 Mark Word: ![](https://gitee.com/seazean/images/raw/master/Java/JUC-Monitor-MarkWord结构64位.png) 工作流程: * 开始时 Monitor 中 Owner 为 null -* 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一 - 个 Owner,**obj 对象的 Mark Word 指向 Monitor** +* 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor 中只能有一个 Owner,**obj 对象的 Mark Word 指向 Monitor** -* 在 Thread-2 上锁的过程中,Thread-3、Thread-4、Thread-5 也来执行 synchronized(obj),就会进入 - EntryList BLOCKED(双向链表) +* 在 Thread-2 上锁的过程,Thread-3、Thread-4、Thread-5 也执行 synchronized(obj),就会进入 EntryList BLOCKED(双向链表) * Thread-2 执行完同步代码块的内容,根据对象头中 Monitor 地址寻找,设置 Owner 为空,唤醒 EntryList 中等待的线程来竞争锁,竞争是**非公平的** * WaitSet 中的 Thread-0,是以前获得过锁,但条件不满足进入 WAITING 状态的线程(wait-notify 机制) @@ -2668,7 +2665,8 @@ public final class Singleton { if(INSTANCE == null) { // t2,这里的判断不是线程安全的 // 首次访问会同步,而之后的使用没有 synchronized synchronized(Singleton.class) { - if (INSTANCE == null) { // t1,这里是线程安全的,判断防止其他线程在当前线程等待锁的期间完成了初始化 + // 这里是线程安全的判断,防止其他线程在当前线程等待锁的期间完成了初始化 + if (INSTANCE == null) { INSTANCE = new Singleton(); } } @@ -2731,7 +2729,7 @@ getInstance 方法对应的字节码为: 步骤 21 和步骤 24 之间不存在数据依赖关系,而且无论重排前后,程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的 -* 关键在于 0:getstatic 这行代码在 monitor 控制之外,可以越过 monitor 读取INSTANCE 变量的值 +* 关键在于 0:getstatic 这行代码在 monitor 控制之外,可以越过 monitor 读取 INSTANCE 变量的值 * 当其他线程访问 instance 不为 null 时,由于 instance 实例未必已初始化,那么 t2 拿到的是将是一个未初 始化完毕的单例返回,这就造成了线程安全的问题 @@ -2747,7 +2745,7 @@ getInstance 方法对应的字节码为: 指令重排只会保证串行语义的执行一致性(单线程),但并不会关系多线程间的语义一致性 -引入volatile,来保证出现指令重排的问题,从而保证单例模式的线程安全性: +引入 volatile,来保证出现指令重排的问题,从而保证单例模式的线程安全性: ```java private static volatile SingletonDemo INSTANCE = null; @@ -12881,7 +12879,7 @@ final void updateHead(Node h, Node p) { 1. 协议:计算机网络客户端与服务端通信必须约定和彼此遵守的通信规则,HTTP、FTP、TCP、UDP、SMTP -2. IP地址:互联网协议地址(Internet Protocol Address),用来给一个网络中的计算机设备做唯一的编号 +2. IP地址:互联网协议地址(Internet Protocol Address),用来给一个网络中的计算机设备做唯一的编号 * IPv4 :4个字节,32位组成,192.168.1.1 * Pv6:可以实现为所有设备分配 IP 128 位 @@ -12911,11 +12909,11 @@ final void updateHead(Node h, Node p) { 网络通信协议:对计算机必须遵守的规则,只有遵守这些规则,计算机之间才能进行通信 -> 应用层:应用程序(QQ,微信,浏览器),可能用到的协议(HTTP,FTP,SMTP) +> 应用层:应用程序(QQ、微信、浏览器),可能用到的协议(HTTP、FTP、SMTP) > -> 传输层:TCP/IP协议 - UDP协议 +> 传输层:TCP/IP 协议 - UDP 协议 > -> 网络层 :IP协议,封装自己的IP和对方的IP和端口 +> 网络层 :IP 协议,封装自己的 IP 和对方的 IP 和端口 > > 数据链路层 : 进入到硬件(网) @@ -12923,12 +12921,12 @@ final void updateHead(Node h, Node p) { TCP/IP协议:传输控制协议 (Transmission Control Protocol) -传输控制协议 TCP(Transmission Control Protocol)是面向连接的,提供可靠交付,有流量控制,拥塞控制,提供全双工通信,面向字节流(把应用层传下来的报文看成字节流,把字节流组织成大小不等的数据块),每一条 TCP 连接只能是点对点的(一对一) +传输控制协议 TCP(Transmission Control Protocol)是面向连接的,提供可靠交付,有流量控制,拥塞控制,提供全双工通信,面向字节流,每一条 TCP 连接只能是点对点的(一对一) * 在通信之前必须确定对方在线并且连接成功才可以通信 * 例如下载文件、浏览网页等(要求可靠传输) -用户数据报协议 UDP(User Datagram Protocol)是无连接的,尽最大可能交付,没有拥塞控制,面向报文(对于应用程序传下来的报文不合并也不拆分,只是添加 UDP 首部),支持一对一、一对多、多对一和多对多的交互通信 +用户数据报协议 UDP(User Datagram Protocol)是无连接的,尽最大可能交付,不可靠,没有拥塞控制,面向报文,支持一对一、一对多、多对一和多对多的交互通信 * 直接发消息给对方,不管对方是否在线,发消息后也不需要确认 * 无线(视频会议,通话),性能好,可能丢失一些数据 @@ -12951,26 +12949,32 @@ TCP/IP协议:传输控制协议 (Transmission Control Protocol) Java 中的通信模型: 1. BIO 表示同步阻塞式通信,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善。 + 同步阻塞式性能极差:大量线程,大量阻塞 - + 2. 伪异步通信:引入线程池,不需要一个客户端一个线程,实现线程复用来处理很多个客户端,线程可控。 - 高并发下性能还是很差:线程数量少,数据依然是阻塞的;数据没有来线程还是要等待 - + + 高并发下性能还是很差:线程数量少,数据依然是阻塞的,数据没有来线程还是要等待 + 3. NIO 表示**同步非阻塞 IO**,服务器实现模式为请求对应一个线程,客户端发送的连接会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求时才启动一个线程进行处理 - 工作原理:1个主线程专门负责接收客户端,1个线程轮询所有的客户端,发来了数据才会开启线程处理 + 工作原理:1 个主线程专门负责接收客户端,1 个线程轮询所有的客户端,发来了数据才会开启线程处理 + 同步:线程还要不断的接收客户端连接,以及处理数据 + 非阻塞:如果一个管道没有数据,不需要等待,可以轮询下一个管道是否有数据 4. AIO 表示异步非阻塞 IO,AIO 引入异步通道的概念,采用了 Proactor 模式,有效的请求才启动线程,特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用 + 异步:服务端线程接收到了客户端管道以后就交给底层处理 IO 通信,线程可以做其他事情 + 非阻塞:底层也是客户端有数据才会处理,有了数据以后处理好通知服务器应用来启动线程进行处理 各种模型应用场景: * BIO 适用于连接数目比较小且固定的架构,该方式对服务器资源要求比较高,并发局限于应用中,程序简单 -* NIO 适用于连接数目多且连接比较短(轻操作)的架构,如聊天服务器,并发局限于应用中,编程复杂,JDK 1.4开始支持 -* AIO 适用于连接数目多且连接比较长(重操作)的架构,如相册服务器,充分调用操作系统参与并发操作,编程复杂,JDK 1.7开始支持 +* NIO 适用于连接数目多且连接比较短(轻操作)的架构,如聊天服务器,并发局限于应用中,编程复杂,JDK 1.4 开始支持 +* AIO 适用于连接数目多且连接比较长(重操作)的架构,如相册服务器,充分调用操作系统参与并发操作,JDK 1.7 开始支持 @@ -13105,8 +13109,8 @@ int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct t ```c struct timeval{ - long tv_sec; //秒 - long tv_usec;//微秒 + long tv_sec; //秒 + long tv_usec; //微秒 } ``` @@ -13331,12 +13335,8 @@ else 流程图:https://gitee.com/seazean/images/blob/master/Java/IO-epoll%E5%8E%9F%E7%90%86%E5%9B%BE.jpg -图片来源:https://www.processon.com/view/link/5f62f98f5653bb28eb434add - 参考视频:https://www.bilibili.com/video/BV19D4y1o797 -参考文章:https://github.com/CyC2018/CS-Notes/blob/master/notes/Socket.md - *** @@ -13353,8 +13353,8 @@ epoll 的特点: * epoll 的时间复杂度 O(1),epoll 理解为 event poll,不同于忙轮询和无差别轮询,调用 epoll_wait **只是轮询就绪链表**。当监听列表有设备就绪时调用回调函数,把就绪 fd 放入就绪链表中,并唤醒在 epoll_wait 中阻塞的进程,所以 epoll 实际上是**事件驱动**(每个事件关联上fd)的,降低了 system call 的时间复杂度 * epoll 内核中根据每个 fd 上的 callback 函数来实现,只有活跃的 socket 才会主动调用 callback,所以使用 epoll 没有前面两者的线性下降的性能问题,效率提高 -* epoll 每次注册新的事件到 epoll 句柄中时,会把新的 fd 拷贝进内核,但不是每次 epoll_wait 的重复拷贝,对比前面两种,epoll 只需要将描述符从进程缓冲区向内核缓冲区拷贝一次。 epoll 也可以利用 **mmap() 文件映射内存**加速与内核空间的消息传递,减少复制开销 -* 前面两者要把 current 往设备等待队列中挂一次,epoll 也只把 current 往等待队列上挂一次,但是这里的等待队列并不是设备等待队列,只是一个 epoll 内部定义的等待队列,这样节省不少的开销 +* epoll 注册新的事件都是注册到到内核中 epoll 句柄中,不需要每次调用 epoll_wait 时重复拷贝,对比前面两种,epoll 只需要将描述符从进程缓冲区向内核缓冲区拷贝一次。 epoll 也可以利用 **mmap() 文件映射内存**加速与内核空间的消息传递,减少复制开销 +* 前面两者要把 current 往设备等待队列中挂一次,epoll 也只把 current 往等待队列上挂一次,但是这里的等待队列并不是设备等待队列,只是一个 epoll 内部定义的等待队列,这样可以节省开销 * epoll 对多线程编程更有友好,一个线程调用了 epoll_wait() 另一个线程关闭了同一个描述符,也不会产生像 select 和 poll 的不确定情况 @@ -13374,15 +13374,15 @@ epoll 的特点: 应用场景: * select 应用场景: - * select 的 timeout 参数精度为微秒,poll 和 epoll 为毫秒,因此 select 适用于实时性要求比较高的场景,比如核反应堆的控制 + * select 的 timeout 参数精度为微秒,poll 和 epoll 为毫秒,因此 select 适用**实时性要求比较高**的场景,比如核反应堆的控制 * select 可移植性更好,几乎被所有主流平台所支持 * poll 应用场景:poll 没有最大描述符数量的限制,适用于平台支持并且对实时性要求不高的情况 * epoll 应用场景: - * 运行在 Linux 平台上,有大量的描述符需要同时轮询,并且这些连接最好是长连接 + * 运行在 Linux 平台上,有大量的描述符需要同时轮询,并且这些连接最好是**长连接** * 需要同时监控小于 1000 个描述符,没必要使用 epoll,因为这个应用场景下并不能体现 epoll 的优势 - * 需要监控的描述符状态变化多,而且都是非常短暂的,也没有必要使用 epoll。因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用,频繁系统调用降低效率,并且 epoll 的描述符存储在内核,不容易调试 + * 需要监控的描述符状态变化多,而且是非常短暂的,就没有必要使用 epoll。因为 epoll 中的所有描述符都存储在内核中,每次对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用,频繁系统调用降低效率,并且 epoll 的描述符存储在内核,不容易调试 @@ -13457,13 +13457,13 @@ DMA (Direct Memory Access) :直接存储器访问,让外部设备不通过 C 把内存数据传输到网卡然后发送: * 没有 DMA:CPU 读内存数据到 CPU 高速缓存,再写到网卡,这样就把 CPU 的速度拉低到和网卡一个速度 -* 使用 DMA:把数据读到 Socket 内核缓存区(CPU复制),CPU 分配给 DMA 开始**异步**操作,DMA 读取 Socket 缓冲区到 DMA 缓冲区,然后写到网卡。DMA 执行完后中断(就是通知) CPU,这时 Socket 内核缓冲区为空,CPU 从用户态切换到内核态,执行中断处理程序,将需要使用 Socket 缓冲区的阻塞进程移到就绪队列 +* 使用 DMA:把数据读到 Socket 内核缓存区(CPU 复制),CPU 分配给 DMA 开始**异步**操作,DMA 读取 Socket 缓冲区到 DMA 缓冲区,然后写到网卡。DMA 执行完后中断(就是通知) CPU,这时 Socket 内核缓冲区为空,CPU 从用户态切换到内核态,执行中断处理程序,将需要使用 Socket 缓冲区的阻塞进程移到就绪队列 一个完整的 DMA 传输过程必须经历 DMA 请求、DMA 响应、DMA 传输、DMA 结束四个步骤: -DMA 方式是一种完全由硬件进行组信息传送的控制方式,通常系统总线由 CPU 管理,在 DMA 方式中,CPU 的主存控制信号被禁止使用,CPU 把总线(地址总线、数据总线、控制总线)让出来由 DMA 控制器接管,用来控制传送的字节数、判断 DMA 是否结束、以及发出 DMA 结束信号,所以 DMA 控制器必须有以下功能: +DMA 方式是一种完全由硬件进行信息传送的控制方式,通常系统总线由 CPU 管理,在 DMA 方式中,CPU 的主存控制信号被禁止使用,CPU 把总线(地址总线、数据总线、控制总线)让出来由 DMA 控制器接管,用来控制传送的字节数、判断 DMA 是否结束、以及发出 DMA 结束信号,所以 DMA 控制器必须有以下功能: * 接受外设发出的 DMA 请求,并向 CPU 发出总线接管请求 * 当 CPU 发出允许接管信号后,进入 DMA 操作周期 @@ -13481,8 +13481,8 @@ DMA 方式是一种完全由硬件进行组信息传送的控制方式,通常 传统的 I/O 操作进行了 4 次用户空间与内核空间的上下文切换,以及 4 次数据拷贝: -* JVM 发出 read() 系统调用,OS 上下文切换到内核模式(切换1)并将数据从网卡或硬盘等通过 DMA 读取到内核空间缓冲区(拷贝1) -* OS 内核将数据复制到用户空间缓冲区(拷贝2),然后 read 系统调用返回,又会导致一次内核空间到用户空间的上下文切换(切换2) +* JVM 发出 read 系统调用,OS 上下文切换到内核模式(切换 1)并将数据从网卡或硬盘等设备通过 DMA 读取到内核空间缓冲区(拷贝 1) +* OS 内核将数据复制到用户空间缓冲区(拷贝 2),然后 read 系统调用返回,又会导致一次内核空间到用户空间的上下文切换(切换 2) * JVM 处理代码逻辑并发送 write() 系统调用,OS 上下文切换到内核模式(切换3)并从用户空间缓冲区复制数据到内核空间缓冲区(拷贝3) * 将内核空间缓冲区中的数据写到 hardware(拷贝4),write 系统调用返回,导致内核空间到用户空间的再次上下文切换(切换4) @@ -13508,7 +13508,7 @@ mmap(Memory Mapped Files)加 write 实现零拷贝,**零拷贝就是没有 进行了 4 次用户空间与内核空间的上下文切换,以及 3 次数据拷贝(2 次 DMA,一次 CPU 复制): -* 发出 mmap 系统调用,DMA 拷贝到内核缓冲区;mmap系统调用返回,无需拷贝 +* 发出 mmap 系统调用,DMA 拷贝到内核缓冲区,映射到共享缓冲区;mmap 系统调用返回,无需拷贝 * 发出 write 系统调用,将数据从内核缓冲区拷贝到内核 Socket 缓冲区;write系统调用返回,DMA 将内核空间 Socket 缓冲区中的数据传递到协议引擎 ![](https://gitee.com/seazean/images/raw/master/Java/IO-mmap工作流程.png) @@ -13551,13 +13551,14 @@ Java NIO 对 sendfile 的支持是 `FileChannel.transferTo()/transferFrom()`, ### Inet -一个该 InetAddress 类的对象就代表一个IP地址对象 +一个 InetAddress 类的对象就代表一个 IP 地址对象 成员方法: -`static InetAddress getLocalHost()` : 获得本地主机IP地址对象 -`static InetAddress getByName(String host)` : 根据IP地址字符串或主机名获得对应的IP地址对象 -`String getHostName()` : 获取主机名 -`String getHostAddress()` : 获得IP地址字符串 + +* `static InetAddress getLocalHost()`:获得本地主机 IP 地址对象 +* `static InetAddress getByName(String host)`:根据 IP 地址字符串或主机名获得对应的IP地址对象 +* `String getHostName()`:获取主机名 +* `String getHostAddress()`:获得 IP 地址字符串 ```java public class InetAddressDemo { @@ -13599,7 +13600,7 @@ UDP(User Datagram Protocol)协议的特点: * 发送数据的包的大小限制 **64KB** 以内 * 因为面向无连接,速度快,但是不可靠,会丢失数据 -UDP协议的使用场景:在线视频、网络语音、电话 +UDP 协议的使用场景:在线视频、网络语音、电话 @@ -13618,31 +13619,32 @@ UDP 协议相关的两个类 * DatagramPacket 类 - `public new DatagramPacket(byte[] buf, int length, InetAddress address, int port)` : 创建发送端数据包对象,参数: + `public new DatagramPacket(byte[] buf, int length, InetAddress address, int port)`:创建发送端数据包对象 * buf:要发送的内容,字节数组 * length:要发送内容的长度,单位是字节 * address:接收端的IP地址对象 * port:接收端的端口号 - `public new DatagramPacket(byte[] buf, int length)` : 创建接收端的数据包对象,参数: + `public new DatagramPacket(byte[] buf, int length)`:创建接收端的数据包对象 * buf:用来存储接收到内容 * length:能够接收内容的长度 * DatagramPacket 类常用方法 - `public int getLength()` : 获得实际接收到的字节个数 - `public byte[] getData()` : 返回数据缓冲区 + + * `public int getLength()`:获得实际接收到的字节个数 + * `public byte[] getData()`:返回数据缓冲区 **DatagramSocket**: -* DatagramSocket 类构造方法 - `protected DatagramSocket()` : 创建发送端的 Socket 对象,系统会随机分配一个端口号 - `protected DatagramSocket(int port)` : 创建接收端的 Socket 对象并指定端口号 +* DatagramSocket 类构造方法: + * `protected DatagramSocket()`:创建发送端的 Socket 对象,系统会随机分配一个端口号 + * `protected DatagramSocket(int port)`:创建接收端的 Socket 对象并指定端口号 * DatagramSocket 类成员方法 - `public void send(DatagramPacket dp)` : 发送数据包 - `public void receive(DatagramPacket p)` : 接收数据包 - `public void close()` : 关闭数据报套接字 + * `public void send(DatagramPacket dp)`:发送数据包 + * `public void receive(DatagramPacket p)`:接收数据包 + * `public void close()`:关闭数据报套接字 ```java public class UDPClientDemo { @@ -13664,7 +13666,7 @@ public class UDPServerDemo{ System.out.println("==启动服务端程序=="); // 1.创建一个接收客户都端的数据包对象(集装箱) byte[] buffer = new byte[1024*64]; - DatagramPacket packet = new DatagramPacket(buffer,bubffer.length); + DatagramPacket packet = new DatagramPacket(buffer, bubffer.length); // 2.创建一个接收端的码头对象 DatagramSocket socket = new DatagramSocket(8000); // 3.开始接收 @@ -13724,13 +13726,13 @@ TCP/IP 协议的特点: * 面向连接的协议 * 只能由客户端主动发送数据给服务器端,服务器端接收到数据之后,可以给客户端响应数据 * 通过**三次握手**建立连接,连接成功形成数据传输通道;通过**四次挥手**断开连接 -* 基于IO流进行数据传输 +* 基于字节流进行数据传输 * 传输数据大小没有限制 * 因为面向连接的协议,速度慢,但是是可靠的协议。 TCP 协议的使用场景:文件上传和下载、邮件发送和接收、远程登录 -注意:**TCP不会为没有数据的ACK超时重传** +注意:**TCP 不会为没有数据的 ACK 超时重传** 三次握手 @@ -14177,10 +14179,10 @@ public class Server { **NIO的介绍**: -Java NIO(New IO、Java non-blocking IO),从 Java 1.4 版本开始引入的一个新的 IO API,可以替代标准的 Java IO API,NIO 支持面**向缓冲区**的、基于**通道**的 IO 操作,以更加高效的方式进行文件的读写操作。 +Java NIO(New IO、Java non-blocking IO),从 Java 1.4 版本开始引入的一个新的 IO API,可以替代标准的 Java IO API,NIO 支持面向缓冲区的、基于通道的 IO 操作,以更加高效的方式进行文件的读写操作。 * NIO 有三大核心部分:**Channel( 通道) ,Buffer( 缓冲区),Selector( 选择器)** -* NIO 是非阻塞IO,传统 IO 的 read 和 write 只能阻塞执行,线程在读写 IO 期间不能干其他事情,比如调用socket.read(),如果服务器没有数据传输过来,线程就一直阻塞,而 NIO 中可以配置 Socket 为非阻塞模式 +* NIO 是非阻塞IO,传统 IO 的 read 和 write 只能阻塞执行,线程在读写 IO 期间不能干其他事情,比如调用 socket.accept(),如果服务器没有数据传输过来,线程就一直阻塞,而 NIO 中可以配置 Socket 为非阻塞模式 * NIO 可以做到用一个线程来处理多个操作的。假设有 1000 个请求过来,根据实际情况可以分配20 或者 80个线程来处理,不像之前的阻塞 IO 那样分配 1000 个 NIO 和 BIO 的比较: @@ -14205,7 +14207,7 @@ NIO 和 BIO 的比较: ### 实现原理 -NIO 三大核心部分:**Channel( 通道) ,Buffer( 缓冲区), Selector( 选择器)** +NIO 三大核心部分:Channel( 通道) ,Buffer( 缓冲区), Selector( 选择器) * Buffer 缓冲区 @@ -14225,12 +14227,12 @@ NIO 的实现框架: * 每个 Channel 对应一个 Buffer * 一个线程对应 Selector , 一个 Selector 对应多个 Channel(连接) -* 程序切换到哪个Channel 是由事件决定的,Event 是一个重要的概念 +* 程序切换到哪个 Channel 是由事件决定的,Event 是一个重要的概念 * Selector 会根据不同的事件,在各个通道上切换 * Buffer 是一个内存块 , 底层是一个数组 * 数据的读取写入是通过 Buffer 完成的 , BIO 中要么是输入流,或者是输出流,不能双向,NIO 的 Buffer 是可以读也可以写, flip() 切换 Buffer 的工作模式 -Java NIO 系统的核心在于:通道和缓冲区,通道表示打开到 IO 设备(例如:文件、 套接字)的连接。若需要使用 NIO 系统,获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区,然后操作缓冲区,对数据进行处理。简而言之,Channel 负责传输, Buffer 负责存取数据 +Java NIO 系统的核心在于:通道和缓冲区,通道表示打开的 IO 设备(例如:文件、 套接字)的连接。若要使用 NIO 系统,获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区,然后操作缓冲区,对数据进行处理。简而言之,Channel 负责传输, Buffer 负责存取数据 @@ -14246,7 +14248,7 @@ Java NIO 系统的核心在于:通道和缓冲区,通道表示打开到 IO ![](https://gitee.com/seazean/images/raw/master/Java/NIO-Buffer.png) -Buffer 底层是一个数组,可以保存多个相同类型的数据,根据数据类型不同 ,有以下 Buffer 常用子类:ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer +**Buffer 底层是一个数组**,可以保存多个相同类型的数据,根据数据类型不同 ,有以下 Buffer 常用子类:ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer @@ -14258,7 +14260,7 @@ Buffer 底层是一个数组,可以保存多个相同类型的数据,根据 * 容量(capacity):作为一个内存块,Buffer 具有固定大小,缓冲区容量不能为负,并且创建后不能更改 -* 限制 (limit):表示缓冲区中可以操作数据的大小(limit 后数据不能进行读写),缓冲区的限制不能为负,并且不能大于其容量。 写入模式,限制等于 buffer 的容量;读取模式下,limit 等于写入的数据量 +* 限制 (limit):表示缓冲区中可以操作数据的大小(limit 后数据不能进行读写),缓冲区的限制不能为负,并且不能大于其容量。写入模式,limit 等于 buffer 的容量;读取模式下,limit 等于写入的数据量 * 位置(position):**下一个要读取或写入的数据的索引**,缓冲区的位置不能为负,并且不能大于其限制 @@ -14276,7 +14278,7 @@ Buffer 底层是一个数组,可以保存多个相同类型的数据,根据 #### 常用API -`static XxxBuffer allocate(int capacity)` : 创建一个容量为 capacity 的 XxxBuffer 对象 +`static XxxBuffer allocate(int capacity)`:创建一个容量为 capacity 的 XxxBuffer 对象 Buffer 基本操作: @@ -14318,12 +14320,12 @@ Buffer 数据操作: #### 读写数据 -使用Buffer读写数据一般遵循以下四个步骤: +使用 Buffer 读写数据一般遵循以下四个步骤: * 写入数据到 Buffer * 调用 flip()方法,转换为读取模式 * 从 Buffer 中读取数据 -* 调用 buffer.clear() 方法清除缓冲区 +* 调用 buffer.clear() 方法清除缓冲区(不是清空了数据,只是重置指针) ```java public class TestBuffer { @@ -14402,7 +14404,7 @@ Direct Memory 优点: 数据流的角度: -* 非直接内存的作用链:本地IO → 内核缓冲区→ 用户缓冲区 →内核缓冲区 → 本地IO +* 非直接内存的作用链:本地IO → 内核缓冲区→ 用户(JVM)缓冲区 →内核缓冲区 → 本地IO * 直接内存是:本地IO → 直接内存 → 本地IO JVM 直接内存图解: @@ -14439,7 +14441,7 @@ NIO 使用的 SocketChannel 也是使用的堆外内存,源码解析: ```java static int write(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) { - // 判断是否是直接内存,是则直接写出,不是则封装到直接内存 + // 【判断是否是直接内存,是则直接写出,不是则封装到直接内存】 if (var1 instanceof DirectBuffer) { return writeFromNativeBuffer(var0, var1, var2, var4); } else { @@ -14552,7 +14554,7 @@ FileChannel 中的成员属性: MappedByteBuffer,可以让文件在直接内存(堆外内存)中进行修改,这种方式叫做**内存映射**,可以直接调用系统底层的缓存,没有 JVM 和 OS 之间的复制操作,提高了传输效率,作用: * **用在进程间的通信,能达到共享内存页的作用**,但在高并发下要对文件内存进行加锁,防止出现读写内容混乱和不一致性,Java 提供了文件锁 FileLock,但在父/子进程中锁定后另一进程会一直等待,效率不高 -* 读写那些太大而不能放进内存中的文件 +* 读写那些太大而不能放进内存中的文件,分段映射 MappedByteBuffer 较之 ByteBuffer新增的三个方法 @@ -14671,7 +14673,7 @@ public class ChannelTest { public void write() throws Exception{ // 1、字节输出流通向目标文件 FileOutputStream fos = new FileOutputStream("data01.txt"); - // 2、得到字节输出流对应的通道Channel + // 2、得到字节输出流对应的通道 【FileChannel】 FileChannel channel = fos.getChannel(); // 3、分配缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); @@ -14708,7 +14710,7 @@ public class ChannelTest { #### 文件复制 -Channel 的两个方法: +Channel 的方法:sendfile 实现零拷贝 * `abstract long transferFrom(ReadableByteChannel src, long position, long count)`:从给定的可读字节通道将字节传输到该通道的文件中 * src:源通道 @@ -14800,7 +14802,7 @@ public class ChannelTest { 分散读取(Scatter ):是指把 Channel 通道的数据读入到多个缓冲区中去 -聚集写入(Gathering ):是指将多个 Buffer 中的数据“聚集”到 Channel。 +聚集写入(Gathering ):是指将多个 Buffer 中的数据聚集到 Channel ```java public class ChannelTest { @@ -14868,9 +14870,7 @@ public class ChannelTest { * 写 : SelectionKey.OP_WRITE (4) * 连接 : SelectionKey.OP_CONNECT (8) * 接收 : SelectionKey.OP_ACCEPT (16) -* 若不止监听一个事件,可以使用“位或”操作符连接:`int interest = SelectionKey.OP_READ | SelectionKey.OP_WRITE` - - +* 若不止监听一个事件,可以使用位或操作符连接:`int interest = SelectionKey.OP_READ | SelectionKey.OP_WRITE` **Selector API**: @@ -14967,9 +14967,9 @@ ssChannel.register(selector, SelectionKey.OP_ACCEPT); 4. 获取选择器 -5. 将通道注册到选择器上, 并且指定“监听接收事件” +5. 将通道注册到选择器上,并且指定监听接收事件 -6. 轮询式的获取选择器上已经“准备就绪”的事件 +6. **轮询式**的获取选择器上已经准备就绪的事件 客户端: diff --git a/SSM.md b/SSM.md index 5c21bbe..732a113 100644 --- a/SSM.md +++ b/SSM.md @@ -8,13 +8,13 @@ ORM(Object Relational Mapping): 对象关系映射,指的是持久化数 **MyBatis**: -* MyBatis 是一个优秀的基于 java 的持久层框架,它内部封装了 JDBC,使开发者只需关注 SQL 语句本身,而不需要花费精力去处理加载驱动、创建连接、创建 Statement 等过程。 +* MyBatis 是一个优秀的基于 Java 的持久层框架,它内部封装了 JDBC,使开发者只需关注 SQL 语句本身,而不需要花费精力去处理加载驱动、创建连接、创建 Statement 等过程。 -* MyBatis通过 xml 或注解的方式将要执行的各种 Statement 配置起来,并通过 Java 对象和 Statement 中 SQL 的动态参数进行映射生成最终执行的 sql 语句。 +* MyBatis通过 xml 或注解的方式将要执行的各种 Statement 配置起来,并通过 Java 对象和 Statement 中 SQL 的动态参数进行映射生成最终执行的 SQL 语句。 * MyBatis 框架执行 SQL 并将结果映射为 Java 对象并返回。采用 ORM 思想解决了实体和数据库映射的问题,对 JDBC 进行了封装,屏蔽了 JDBC 底层 API 的调用细节,使我们不用操作 JDBC API,就可以完成对数据库的持久化操作。 -MyBatis官网地址:http://www.mybatis.org/mybatis-3/ +MyBatis 官网地址:http://www.mybatis.org/mybatis-3/ @@ -28,40 +28,24 @@ MyBatis官网地址:http://www.mybatis.org/mybatis-3/ ### 相关API -#### Resources +Resources:加载资源的工具类 -org.apache.ibatis.io.Resources:加载资源的工具类 +* `InputStream getResourceAsStream(String fileName)`:通过类加载器返回指定资源的字节流 + * 参数 fileName 是放在 src 的核心配置文件名:MyBatisConfig.xml -`InputStream getResourceAsStream(String fileName)`:通过类加载器返回指定资源的字节流 +SqlSessionFactoryBuilder:构建器,用来获取 SqlSessionFactory 工厂对象 -* 参数 fileName 是放在 src 的核心配置文件名:MyBatisConfig.xml +* `SqlSessionFactory build(InputStream is)`:通过指定资源的字节输入流获取 SqlSession 工厂对象 +SqlSessionFactory:获取 SqlSession 构建者对象的工厂接口 +* `SqlSession openSession()`:获取 SqlSession 构建者对象,并开启手动提交事务 +* `SqlSession openSession(boolean)`:获取 SqlSession 构建者对象,参数为 true 开启自动提交事务 -#### SqlSessionFactoryBuilder - -org.apache.ibatis.session.SqlSessionFactoryBuilder:构建器,用来获取 SqlSessionFactory 工厂对象 - -`SqlSessionFactory build(InputStream is)`:通过指定资源的字节输入流获取 SqlSession 工厂对象 - - - -#### SqlSessionFactory - -org.apache.ibatis.session.SqlSessionFactory:获取 SqlSession 构建者对象的工厂接口 - -`SqlSession openSession()`:获取 SqlSession 构建者对象,并开启手动提交事务 - -`SqlSession openSession(boolean)`:获取 SqlSession 构建者对象,参数为 true 开启自动提交事务 - - - -#### SqlSession - -org.apache.ibatis.session.SqlSession:构建者对象接口,用于执行 SQL、管理事务、接口代理 +SqlSession:构建者对象接口,用于执行 SQL、管理事务、接口代理 * SqlSession 代表和数据库的一次会话,用完必须关闭 -* SqlSession 和 connection 一样都是非线程安全,每次使用都应该去获取新的对象 +* SqlSession 和 Connection 一样都是非线程安全,每次使用都应该去获取新的对象 注:**update 数据需要提交事务,或开启默认提交** @@ -85,7 +69,7 @@ org.apache.ibatis.session.SqlSession:构建者对象接口,用于执行 SQL ### 映射配置 -映射配置文件包含了数据和对象之间的映射关系以及要执行的 SQL 语句,放在src目录下, +映射配置文件包含了数据和对象之间的映射关系以及要执行的 SQL 语句,放在 src 目录下, 命名:StudentMapper.xml @@ -109,10 +93,10 @@ org.apache.ibatis.session.SqlSession:构建者对象接口,用于执行 SQL * :修改功能标签 * :删除功能标签 * id:属性,唯一标识,配合名称空间使用 - * resultType:指定结果映射对象类型,和对应的方法的返回值类型(全限定名)保持一致,但是如果返回值是List则和其泛型保持一致 + * resultType:指定结果映射对象类型,和对应的方法的返回值类型(全限定名)保持一致,但是如果返回值是 List 则和其泛型保持一致 * parameterType:指定参数映射对象类型,必须和对应的方法的参数类型(全限定名)保持一致 * **statementType**:可选 STATEMENT,PREPARED 或 CALLABLE,默认值:PREPARED - * STATEMENT:直接操作 sql,使用 Statement 不进行预编译,获取数据:$ + * STATEMENT:直接操作 SQL,使用 Statement 不进行预编译,获取数据:$ * PREPARED:预处理参数,使用 PreparedStatement 进行预编译,获取数据:# * CALLABLE:执行存储过程,CallableStatement @@ -336,7 +320,7 @@ org.apache.ibatis.session.SqlSession:构建者对象接口,用于执行 SQL ```properties driver=com.mysql.jdbc.Driver - url=jdbc:mysql://192.168.2.184:3306/db11 + url=jdbc:mysql://192.168.2.184:3306/db1 username=root password=123456 ``` @@ -457,7 +441,7 @@ org.apache.ibatis.session.SqlSession:构建者对象接口,用于执行 SQL SqlSession sqlSession = sqlSessionFactory.openSession(true); //4.执行映射配置文件中的sql语句,并接收结果 - Student stu = new Student(5,"周七",27); + Student stu = new Student(5, "周七", 27); int result = sqlSession.insert("StudentMapper.insert", stu); //5.提交事务 @@ -503,7 +487,7 @@ org.apache.ibatis.session.SqlSession:构建者对象接口,用于执行 SQL public void testBatch() throws IOException{ SqlSessionFactory sqlSessionFactory = getSqlSessionFactory(); - //可以执行批量操作的sqlSession + // 可以执行批量操作的sqlSession SqlSession openSession = sqlSessionFactory.openSession(ExecutorType.BATCH); long start = System.currentTimeMillis(); try{ @@ -513,9 +497,9 @@ org.apache.ibatis.session.SqlSession:构建者对象接口,用于执行 SQL } openSession.commit(); long end = System.currentTimeMillis(); - //批量:(预编译sql一次==>设置参数===>10000次===>执行1次(类似管道)) - //非批量:(预编译sql=设置参数=执行)==》10000 耗时更多 - System.out.println("执行时长:"+(end-start)); + // 批量:(预编译sql一次==>设置参数===>10000次===>执行1次(类似管道)) + // 非批量:(预编译sql=设置参数=执行)==》10000 耗时更多 + System.out.println("执行时长:" + (end - start)); }finally{ openSession.close(); } @@ -582,7 +566,7 @@ Mapper 接口开发需要遵循以下规范: ### 实现原理 -通过动态代理开发模式,我们只编写一个接口,不写实现类,通过 **getMapper()** 方法最终获取到 org.apache.ibatis.binding.MapperProxy 代理对象,而这个代理对象是 MyBatis 使用了 JDK 的动态代理技术 +通过动态代理开发模式,只编写一个接口不写实现类,通过 **getMapper()** 方法最终获取到 org.apache.ibatis.binding.MapperProxy 代理对象,而这个代理对象是 MyBatis 使用了 JDK 的动态代理技术 动态代理实现类对象在执行方法时最终调用了 **MapperMethod.execute()** 方法,这个方法中通过 switch case 语句根据操作类型来判断是新增、修改、删除、查询操作,最后一步回到了 MyBatis 最原生的 **SqlSession 方式来执行增删改查**。 @@ -999,7 +983,7 @@ Mapper 接口开发需要遵循以下规范: 坏处:只有当需要用到数据时,才会进行数据库查询,这样在大批量数据查询时,查询工作也要消耗时间,所以可能造成用户等待时间变长,造成用户体验下降 -核心配置文件 +核心配置文件: | 标签名 | 描述 | 默认值 | | --------------------- | ------------------------------------------------------------ | ------ | @@ -1166,7 +1150,7 @@ Mapper 接口开发需要遵循以下规范: 参数注解: -* @Param:当 SQL 语句需要多个(大于1)参数时,用来指定参数的对应规则 +* @Param:当 SQL 语句需要**多个(大于1)参数**时,用来指定参数的对应规则 核心配置文件配置映射关系: @@ -1182,7 +1166,7 @@ Mapper 接口开发需要遵循以下规范: 基本增删改查: -* 创建Mapper接口 +* 创建 Mapper 接口 ```java package mapper; @@ -1206,7 +1190,7 @@ Mapper 接口开发需要遵循以下规范: } ``` -* 修改MyBatis的核心配置文件 +* 修改 MyBatis 的核心配置文件 ```xml @@ -1463,12 +1447,12 @@ Mapper 接口开发需要遵循以下规范: 缓存类别: -* 一级缓存:sqlSession 级别的缓存,又叫本地会话缓存,自带的(不需要配置),一级缓存的生命周期与 sqlSession 一致。在操作数据库时需要构造 sqlSession 对象,在对象中有一个数据结构(HashMap)用于存储缓存数据,不同的 sqlSession 之间的缓存数据区域是互相不影响的 -* 二级缓存:mapper(namespace)级别的缓存,二级缓存的使用,需要手动开启(需要配置)。多个 SqlSession 去操作同一个 Mapper 的 sql,可以共用二级缓存,二级缓存是跨 SqlSession 的 +* 一级缓存:SqlSession 级别的缓存,又叫本地会话缓存,自带的(不需要配置),一级缓存的生命周期与 SqlSession 一致。在操作数据库时需要构造 SqlSession 对象,在对象中有一个数据结构(HashMap)用于存储缓存数据,不同的 SqlSession 之间的缓存数据区域是互相不影响的 +* 二级缓存:mapper(namespace)级别的缓存,二级缓存的使用,需要手动开启(需要配置)。多个 SqlSession 去操作同一个 Mapper 的 SQL 可以共用二级缓存,二级缓存是跨 SqlSession 的 -开启缓存:配置核心配置文件中标签 +开启缓存:配置核心配置文件中 标签 -* cacheEnabled:true 表示全局性地开启所有映射器配置文件中已配置的任何缓存 +* cacheEnabled:true 表示全局性地开启所有映射器配置文件中已配置的任何缓存,默认 true ![](https://gitee.com/seazean/images/raw/master/Frame/MyBatis-缓存的实现原理.png) @@ -1495,7 +1479,7 @@ Mapper 接口开发需要遵循以下规范: * SqlSession 不同 * SqlSession 相同,查询条件不同时(还未缓存该数据) * SqlSession 相同,手动清除了一级缓存,调用 `openSession.clearCache()` -* SqlSession 相同,执行 commit 操作(执行插入、更新、删除),清空 SqlSession 中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。 +* SqlSession 相同,执行 commit 操作(执行插入、更新、删除),清空 SqlSession 中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,**避免脏读** 测试一级缓存存在 @@ -1535,7 +1519,7 @@ public void testFirstLevelCache(){ 二级缓存是 mapper 的缓存,只要是同一个 mapper 的 SqlSession 就共享二级缓存的内容,并且可以操作二级缓存 -工作流程:一个会话查询一条数据,这个数据就会被存放在当前会话的一级缓存中,如果**会话关闭**,一级缓存中的数据会被保存到二级缓存 +工作流程:一个会话查询一条数据,这个数据就会被放在当前会话的一级缓存中,如果**会话关闭**一级缓存中的数据会保存到二级缓存 二级缓存的基本使用: @@ -1588,9 +1572,9 @@ public void testFirstLevelCache(){ 1. select 标签的 useCache 属性 - 映射文件中的 `` 标签中设置 `useCache="true"` 代表当前 statement 要使用二级缓存(默认) - 注意:针对每次查询都需要最新的数据 sql,要设置成 useCache=false,禁用二级缓存 + 注意:如果每次查询都需要最新的数据 sql,要设置成 useCache=false,禁用二级缓存 ```xml ``` -2. 每个增删改标签都有 flushCache 属性,默认为 true,代表在执行增删改之后就会清除一、二级缓存,而查询标签默认值为 false,所以查询不会清空缓存 +2. 每个增删改标签都有 flushCache 属性,默认为 true,代表在**执行增删改之后就会清除一、二级缓存**,而查询标签默认值为 false,所以查询不会清空缓存 -3. localCacheScope:本地缓存作用域,默认值为 SESSION,当前会话的所有数据保存在会话缓存中,设置为 STATEMENT 禁用一级缓存 +3. localCacheScope:本地缓存作用域, 中的配置项,默认值为 SESSION,当前会话的所有数据保存在会话缓存中,设置为 STATEMENT 禁用一级缓存 @@ -2295,30 +2279,28 @@ MyBatis 提供了 org.apache.ibatis.jdbc.SQL 功能类,专门用于构建 SQL ![](https://gitee.com/seazean/images/raw/master/Frame/MyBatis-执行流程.png) -MyBatis 运行原理: - -1. 通过加载 mybatis 全局配置文件以及 mapper 映射文件初始化 configuration 对象和 Executor 对象(通过全局配置文件中的 defaultExecutorType 初始化) - -2. 创建一个 defaultSqlSession 对象,将 configuration 和 Executor 对象注入到 defaulSqlSession 对象 +MyBatis 运行过程: -3. defaulSqlSession 通过 getMapper() 获取 mapper 接口的代理对象 mapperProxy +1. 加载 MyBatis 全局配置文件,通过 XPath 方式解析 XML 配置文件,首先解析核心配置文件, 标签中配置属性项有 defaultExecutorType,用来配置指定 Executor 类型,将配置文件的信息填充到 Configuration对象。最后解析映射器配置的映射文件,并**构建 MappedStatement 对象填充至 Configuration**,将解析后的映射器添加到 mapperRegistry 中,用于获取代理 -4. 执行增删改查: +2. 创建一个 DefaultSqlSession 对象,**根据参数创建指定类型的 Executor**,二级缓存默认开启,把 Executor 包装成缓存执行器 - * 通过 defaulSqlSession 中的属性 Executor 创建 statementHandler 对象 - * 创建 statementHandler 对象的同时也创建 parameterHandler 和 resultSetHandler - * 通过 parameterHandler 设置预编译参数及参数值 +3. DefaulSqlSession 调用 getMapper(),通过 JDK 动态代理获取 Mapper 接口的代理对象 MapperProxy - * 调用 statementHandler 执行增删改查 +4. 执行 SQL 语句: - * 通过 resultsetHandler 封装查询结果 + * MapperProxy.invoke() 执行代理方法,通过 MapperMethod#execute 判断执行的是增删改查中的哪个方法 + * 查询方法调用 sqlSession.selectOne(),从 Configuration 中获取执行者对象 MappedStatement,然后 Executor 调用 executor.query 开始执行查询方法 + * 首先通过 CachingExecutor 去二级缓存查询,查询不到去一级缓存查询,**最后去数据库查询并放入一级缓存** + * Configuration 对象根据 标签的 statementType 属性,根据属性选择创建哪种对象 + * 判断 BoundSql 是否被创建,没有创建会重新封装参数信息到 BoundSql + * **StatementHandler 的构造方法中,创建了 ParameterHandler 和 ResultSetHandler 对象** + * `interceptorChain.pluginAll(statementHandler)`:拦截器链 + * `prepareStatement()`:通过 StatementHandler 创建 JDBC 原生的 Statement 对象 + * `getConnection()`:获取 JDBC 的 Connection 对象 + * `handler.prepare()`:初始化 Statement 对象 + * `instantiateStatement(Connection connection)`:Connection 中的方法实例化对象 + * 获取普通执行者对象:`Connection.createStatement()` + * 获取预编译执行者对象:`Connection.prepareStatement()` + * `handler.parameterize()`:进行参数的设置 + * `ParameterHandler.setParameters()`:**通过 ParameterHandler 设置参数** + * `typeHandler.setParameter()`:底层通过 TypeHandler 实现 + * `StatementHandler.query()`:**封装成 JDBC 的 PreparedStatement 执行 SQL** + * `resultSetHandler.handleResultSets(ps)`:**通过 ResultSetHandler 对象封装结果集** + * `localCache.putObject(key, list)`:放入本地缓存 `return list.get(0)`:返回结果集的第一个数据 @@ -2502,7 +2504,7 @@ Executor#query(): * 每个创建出来的对象不是直接返回的,而是 `interceptorChain.pluginAll(parameterHandler)` * 获取到所有 Interceptor(插件需要实现的接口),调用 `interceptor.plugin(target)`返回 target 包装后的对象 - * 插件机制可以使用插件为目标对象创建一个代理对象(AOP),代理对象可以拦截到四大对象的每一个执行 + * 插件机制可以使用插件为目标对象创建一个代理对象,代理对象可以**拦截到四大对象的每一个执行** ```java @Intercepts( @@ -2511,18 +2513,18 @@ Executor#query(): }) public class MyFirstPlugin implements Interceptor{ - //intercept:拦截目标对象的目标方法的执行; + //intercept:拦截目标对象的目标方法的执行 @Override public Object intercept(Invocation invocation) throws Throwable { - System.out.println("MyFirstPlugin...intercept:"+invocation.getMethod()); + System.out.println("MyFirstPlugin...intercept:" + invocation.getMethod()); //动态的改变一下sql运行的参数:以前1号员工,实际从数据库查询11号员工 Object target = invocation.getTarget(); - System.out.println("当前拦截到的对象:"+target); + System.out.println("当前拦截到的对象:" + target); //拿到:StatementHandler==>ParameterHandler===>parameterObject //拿到target的元数据 MetaObject metaObject = SystemMetaObject.forObject(target); Object value = metaObject.getValue("parameterHandler.parameterObject"); - System.out.println("sql语句用的参数是:"+value); + System.out.println("sql语句用的参数是:" + value); //修改完sql语句要用的参数 metaObject.setValue("parameterHandler.parameterObject", 11); //执行目标方法 @@ -2868,13 +2870,13 @@ ctx.getBean("beanId") == ctx.getBean("beanName1") == ctx.getBean("beanName2") - prototype:设定创建出的对象保存在 Spring 容器中,是一个非单例(原型)的对象 - request、session、application、 websocket :设定创建出的对象放置在 web 容器对应的位置 -Spring 容器中 Bean 的线程安全问题: +Spring 容器中 Bean 的**线程安全**问题: -* 原型 Bean,对于原型 Bean,每次创建一个新对象,线程之间并不存在 Bean 共享,所以不会有线程安全的问题 +* 原型 Bean,每次创建一个新对象,线程之间并不存在 Bean 共享,所以不会有线程安全的问题 * 单例Bean,所有线程共享一个单例实例 Bean,因此是存在资源的竞争,如果单例 Bean是一个**无状态 Bean**,也就是线程中的操作不会对 Bean 的成员执行查询以外的操作,那么这个单例 Bean 是线程安全的 - 解决方法:开发人员自来进行线程安全的保证,最简单的办法就是把 Bean 的作用域 singleton 改为 protopyte + 解决方法:开发人员来进行线程安全的保证,最简单的办法就是把 Bean 的作用域 singleton 改为 protopyte @@ -2906,7 +2908,7 @@ Spring 容器中 Bean 的线程安全问题: - 当 scope=“singleton” 时,Spring 容器中有且仅有一个对象,init 方法在创建容器时仅执行一次 - 当 scope=“prototype” 时,Spring 容器要创建同一类型的多个对象,init 方法在每个对象创建时均执行一次 - 当 scope=“singleton” 时,关闭容器(.close())会导致bean实例的销毁,调用 destroy 方法一次 -- 当 scope=“prototype” 时,对象的销毁由垃圾回收机制 gc() 控制,destroy 方法将不会被执行 +- 当 scope=“prototype” 时,对象的销毁由垃圾回收机制 gc 控制,destroy 方法将不会被执行 bean 配置: @@ -2970,7 +2972,7 @@ UserService userService = (UserService)ctx.getBean("userService3"); bean配置: ```xml - + ``` @@ -3012,7 +3014,7 @@ UserService userService = (UserService)ctx.getBean("userService3"); bean 配置: ```xml - + ``` @@ -4206,7 +4208,7 @@ private UserDao userDao; 相关属性: -- required:为 true (默认)表示注入 bean 时该 bean 必须存在,不然就会注入失败;为 false 表示注入时该 bean 存在就注入,不存在就忽略跳过 +- required:**为 true (默认)表示注入 bean 时该 bean 必须存在**,不然就会注入失败抛出异常;为 false 表示注入时该 bean 存在就注入,不存在就忽略跳过 注意:在使用 @Autowired 时,首先在容器中查询对应类型的 bean,如果查询结果刚好为一个,就将该 bean 装配给 @Autowired 指定的数据,如果查询的结果不止一个,那么 @Autowired 会根据名称来查找。如果查询的结果为空,那么会抛出异常。解决方法:使用 required = false @@ -4248,17 +4250,17 @@ public class ClassName{} - @Inject 与 @Named 是 JSR330 规范中的注解,功能与 @Autowired 和 @Qualifier 完全相同,适用于不同架构场景 - @Resource 是 JSR250 规范中的注解,可以简化书写格式 -@Resource相关属性 +@Resource 相关属性 - name:设置注入的 bean 的 id - type:设置注入的 bean 的类型,接收的参数为 Class 类型 -**@Autowired和@Resource之间的区别**: +**@Autowired 和 @Resource之间的区别**: -* @Autowired默认是按照类型装配注入的,默认情况下它要求依赖对象必须存在(可以设置它required属性为false) +* @Autowired 默认是按照类型装配注入的,默认情况下它要求依赖对象必须存在(可以设置它required属性为false) -* @Resource默认按照名称来装配注入,只有当找不到与名称匹配的bean才会按照类型来装配注入 +* @Resource 默认按照名称来装配注入,只有当找不到与名称匹配的bean才会按照类型来装配注入 @@ -4286,13 +4288,13 @@ public class ClassName { 说明: -- 不支持 * 通配符,加载后,所有spring控制的bean中均可使用对应属性值,加载多个需要用`{}和,`隔开 +- 不支持 * 通配符,加载后,所有 Spring 控制的 bean 中均可使用对应属性值,加载多个需要用 `{} 和 ,` 隔开 相关属性 -- value(默认):设置加载的properties文件名 +- value(默认):设置加载的 properties 文件名 -- ignoreResourceNotFound:如果资源未找到,是否忽略,默认为false +- ignoreResourceNotFound:如果资源未找到,是否忽略,默认为 false @@ -4328,7 +4330,7 @@ public class ClassName { - 配置在类上,使 @DependsOn 指定的 bean 优先于当前类中所有 @Bean 配置的 bean 进行加载 - - 配置在类上,使 @DependsOn 指定的 bean 优先于 @Component 等配置的bean进行加载 + - 配置在类上,使 @DependsOn 指定的 bean 优先于 @Component 等配置的 bean 进行加载 - 相关属性 @@ -4356,7 +4358,7 @@ public class ClassName { - 类型:类注解、方法注解 -- 作用:控制bean的加载时机,使其延迟加载,获取的时候加载 +- 作用:控制 bean 的加载时机,使其延迟加载,获取的时候加载 - 格式: @@ -5012,7 +5014,7 @@ AOP 弥补了 OOP 的不足,基于 OOP 基础之上进行横向开发: - uAOP 程序开发主要关注基于 OOP 开发中的共性功能,一切围绕共性功能进行,完成某个任务先构建可能遇到的所有共性功能(当所有功能都开发出来也就没有共性与非共性之分),将软件开发由手工制作走向半自动化/全自动化阶段,实现“插拔式组件体系结构”搭建 -AOP作用: +AOP 作用: * 提高代码的可重用性 * 业务代码编码更简洁 @@ -5021,7 +5023,7 @@ AOP作用: * 业务功能扩展更便捷 -AOP开发思想: +AOP 开发思想: ![](https://gitee.com/seazean/images/raw/master/Frame/AOP开发思想.png) @@ -5386,7 +5388,7 @@ execution(List com.seazean.service.*Service+.findAll(..)) ##### 配置方式 -XML配置规则: +XML 配置规则: - 企业开发命名规范严格遵循规范文档进行 @@ -5429,7 +5431,7 @@ XML配置规则: ##### 通知类型 -AOP的通知类型共5种:前置通知,后置通知、返回后通知、抛出异常后通知、环绕通知 +AOP 的通知类型共5种:前置通知,后置通知、返回后通知、抛出异常后通知、环绕通知 ###### before @@ -5652,7 +5654,7 @@ AOP的通知类型共5种:前置通知,后置通知、返回后通知、抛 第一种方式: -* 设定通知方法第一个参数为JoinPoint,通过该对象调用getArgs()方法,获取原始方法运行的参数数组 +* 设定通知方法第一个参数为 JoinPoint,通过该对象调用 getArgs() 方法,获取原始方法运行的参数数组 ```java public void before(JoinPoint jp) throws Throwable { @@ -5669,7 +5671,7 @@ AOP的通知类型共5种:前置通知,后置通知、返回后通知、抛 * 流程图:![](https://gitee.com/seazean/images/raw/master/Frame/AOP通知获取参数方式二.png) * 解释: - * `&`代表并且& + * `&` 代表并且 & * 输出结果:a = param1 b = param2 第三种方式: @@ -6007,7 +6009,7 @@ AOP的通知类型共5种:前置通知,后置通知、返回后通知、抛 #### AOP注解 -AOP 注解简化 xml: +AOP 注解简化 XML: ![](https://gitee.com/seazean/images/raw/master/Frame/AOP注解开发.png) @@ -6225,7 +6227,7 @@ public Object around(ProceedingJoinPoint pjp) throws Throwable { #### 执行顺序 -AOP使用XML配置情况下,通知的执行顺序由配置顺序决定,在注解情况下由于不存在配置顺序的概念,参照通知所配置的**方法名字符串对应的编码值顺序**,可以简单理解为字母排序 +AOP 使用 XML 配置情况下,通知的执行顺序由配置顺序决定,在注解情况下由于不存在配置顺序的概念,参照通知所配置的**方法名字符串对应的编码值顺序**,可以简单理解为字母排序 - 同一个通知类中,相同通知类型以方法名排序为准 @@ -6239,7 +6241,7 @@ AOP使用XML配置情况下,通知的执行顺序由配置顺序决定,在 - 不同通知类中,以类名排序为准 -- 使用@Order注解通过变更bean的加载顺序改变通知的加载顺序 +- 使用 @Order 注解通过变更 bean 的加载顺序改变通知的加载顺序 ```java @Component @@ -6257,25 +6259,8 @@ AOP使用XML配置情况下,通知的执行顺序由配置顺序决定,在 } ``` - - -企业开发经验: -- 通知方法名由3部分组成,分别是前缀、顺序编码、功能描述 -- 前缀为固定字符串,例如baidu、seazean等,无实际意义 - -- 顺序编码为6位以内的整数,通常3位即可,不足位补0 - -- 功能描述为该方法对应的实际通知功能,例如exception、strLenCheck - - - 制通知执行顺序使用顺序编码控制,使用时做一定空间预留 - - - 003使用,006使用,预留001、002、004、005、007、008 - - - 使用时从中段开始使用,方便后期做前置追加或后置追加 - - - 最终顺序以运行顺序为准,以测试结果为准,不以设定规则为准 @@ -6832,13 +6817,13 @@ TransactionStatus 此接口定义了事务在执行过程中某个时间点上 ```java public void transfer(String outName,String inName,Double money){ - //1.创建事务管理器,开启事务 + //1.创建事务管理器, DataSourceTransactionManager dstm = new DataSourceTransactionManager(); //2.为事务管理器设置与数据层相同的数据源 dstm.setDataSource(dataSource); //3.创建事务定义对象 TransactionDefinition td = new DefaultTransactionDefinition(); - //4.创建事务状态对象,用于控制事务执行 + //4.创建事务状态对象,用于控制事务执行,【开启事务】 TransactionStatus ts = dstm.getTransaction(td); accountDao.inMoney(inName,money); int i = 1/0; //模拟业务层事务过程中出现错误 @@ -7163,7 +7148,7 @@ public void addAccount{} * 不推荐在接口上使用 `@Transactional` 注解 - 原因:在接口上使用注解,**只有在使用基于接口的代理时才会生效,因为注解是不能继承的**,这就意味着如果正在使用基于类的代理时,那么事务的设置将不能被基于类的代理所识别 + 原因:在接口上使用注解,**只有在使用基于接口的代理(JDK)时才会生效,因为注解是不能继承的**,这就意味着如果正在使用基于类的代理(CGLIB)时,那么事务的设置将不能被基于类的代理所识别 * 正确的设置 `@Transactional` 的 rollbackFor 和 propagation 属性,否则事务可能会回滚失败 @@ -7174,7 +7159,7 @@ public void addAccount{} **事务不生效的问题** -* 情况 1:确认创建的 mysql 数据库表引擎是 InnoDB,MyISAM 不支持事务 +* 情况 1:确认创建的 MySQL 数据库表引擎是 InnoDB,MyISAM 不支持事务 * 情况 2:注解到 protected,private 方法上事务不生效,但不会报错 @@ -7198,13 +7183,13 @@ public void addAccount{} 原因:在业务层捕捉并处理了异常(try..catch)等于把异常处理掉了,Spring 就不知道这里有错,也不会主动去回滚数据,推荐做法是在业务层统一抛出异常,然后在控制层统一处理 -* 情况 5:遇到检测异常时,事务不开启,也无法回滚 +* 情况 5:遇到检测异常时,也无法回滚 原因:Spring 的默认的事务规则是遇到运行异常(RuntimeException)和程序错误(Error)才会回滚。想针对检测异常进行事务回滚,可以在 @Transactional 注解里使用 rollbackFor 属性明确指定异常 * 情况 6:Spring 的事务传播策略在**内部方法**调用时将不起作用,在一个 Service 内部,事务方法之间的嵌套调用,普通方法和事务方法之间的嵌套调用,都不会开启新的事务,事务注解要加到调用方法上才生效 - 原因:Spring 的事务都是使用 AOP 代理的模式,动态代理最终是要调用原始对象,而原始对象在去调用方法时是不会再触发代理,就是**一个方法调用本对象的另一个方法**,没有通过代理类直接调用,所以事务也就无法生效 + 原因:Spring 的事务都是使用 AOP 代理的模式,动态代理最终是要调用原始对象,而原始对象在去调用方法时是不会触发拦截器,就是**一个方法调用本对象的另一个方法**,所以事务也就无法生效 ```java @Transactional @@ -7325,7 +7310,7 @@ Spring 模板对象:TransactionTemplate、JdbcTemplate、RedisTemplate、Rabbi * **BeanDefinationRegistry**:存放 BeanDefination 的容器,是一种键值对的形式,通过特定的 Bean 定义的 id,映射到相应的 BeanDefination,**BeanFactory 的实现类同样继承 BeanDefinationRegistry 接口**,拥有保存 BD 的能力 -* **BeanDefinitionReader**:读取配置文件,比如 XML 用 dom4j 解析,配置文件用 IO 流 +* **BeanDefinitionReader**:读取配置文件,**XML 用 Dom4j 解析**,**注解用 IO 流加载解析** 程序: @@ -7407,14 +7392,14 @@ public int loadBeanDefinitions(Resource resource) { * `beanName = this.readerContext.generateBeanName(beanDefinition)`:生成 className + # + 序号的名称赋值给 beanName - * `return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray)`:包装成 BeanDefinitionHolder + * `return new BeanDefinitionHolder(beanDefinition, beanName, aliases)`:**包装成 BeanDefinitionHolder** * `registerBeanDefinition(bdHolder, getReaderContext().getRegistry())`:**注册到容器** * `beanName = definitionHolder.getBeanName()`:获取beanName * `this.beanDefinitionMap.put(beanName, beanDefinition)`:添加到注册中心 - * `getReaderContext().fireComponentRegistered`:发送注册完成事件 + * `getReaderContext().fireComponentRegistered()`:发送注册完成事件 @@ -7460,7 +7445,7 @@ AbstractApplicationContext.refresh(): * `getEnvironment().validateRequiredProperties()`:检查环境变量 * `earlyApplicationEvents= new LinkedHashSet()`:保存容器中早期的事件 -* obtainFreshBeanFactory():获取一个**全新的 BeanFactory 接口实例** +* obtainFreshBeanFactory():获取一个**全新的 BeanFactory 接口实例**,如果容器中存在工厂实例直接销毁 `refreshBeanFactory()`:创建 BeanFactory,设置序列化 ID、读取 BeanDefinition 并加载到工厂 @@ -7474,14 +7459,13 @@ AbstractApplicationContext.refresh(): * `destroyBean(beanName, disposableBean)`:销毁 bean * dependentBeanMap 记录了依赖当前 bean 的其他 bean 信息,因为依赖的对象要被回收了,所以依赖当前 bean 的其他对象都要执行 destroySingleton,遍历 dependentBeanMap 执行销毁 * `bean.destroy()`:解决完成依赖后,执行 DisposableBean 的 destroy 方法 - * `this.containedBeanMap.remove(beanName)`:处理映射 * ` this.dependenciesForBeanMap.remove(beanName)`:保存当前 bean 依赖了谁,直接清除 * 进行一些集合和缓存的清理工作 * `closeBeanFactory()`:将容器内部的 beanFactory 设置为空,重新创建 * `beanFactory = createBeanFactory()`:创建新的 DefaultListableBeanFactory 对象 * `beanFactory.setSerializationId(getId())`:进行 ID 的设置,可以根据 ID 获取 BeanFactory 对象 * `customizeBeanFactory(beanFactory)`:设置是否允许覆盖和循环引用 - * `loadBeanDefinitions(beanFactory)`:**加载 BeanDefinition 信息,注册到 BeanFactory 中** + * `loadBeanDefinitions(beanFactory)`:**加载 BeanDefinition 信息,注册 BD注册到 BeanFactory 中** * `this.beanFactory = beanFactory`:把 beanFactory 填充至容器中 `getBeanFactory()`:返回创建的 DefaultListableBeanFactory 对象,该对象继承 BeanDefinitionRegistry @@ -7494,10 +7478,10 @@ AbstractApplicationContext.refresh(): * `addBeanPostProcessor()`:添加后处理器,主要用于向 bean 内部注入一些框架级别的实例 * `ignoreDependencyInterface()`:设置忽略自动装配的接口,bean 内部的这些类型的字段 不参与依赖注入 * `registerResolvableDependency()`:注册一些类型依赖关系 - * `addBeanPostProcessor()`:将配置的监听者注册到容器中,当前 bean 实现 ApplicationListener 接口就是**监听器事件** + * `addBeanPostProcessor()`:将配置的监听者注册到容器中,当前 bean 实现 ApplicationListener 接口就是监听器事件 * `beanFactory.registerSingleton()`:添加一些系统信息 -* postProcessBeanFactory(beanFactory):BeanFactory 准备工作完成后进行的后置处理工作,通过重写这个方法来在 BeanFactory 创建并预准备完成以后做进一步的设置 +* postProcessBeanFactory(beanFactory):BeanFactory 准备工作完成后进行的后置处理工作,扩展方法 * invokeBeanFactoryPostProcessors(beanFactory):**执行 BeanFactoryPostProcessor 的方法** @@ -7517,19 +7501,20 @@ AbstractApplicationContext.refresh(): * 逻辑到这里已经获取到所有 BeanDefinitionRegistryPostProcessor 和 BeanFactoryPostProcessor 接口类型的后置处理器 - * **首先回调 BeanDefinitionRegistryPostProcessor 类的后置处理方法** + * **首先回调 BeanDefinitionRegistryPostProcessor 类的后置处理方法 postProcessBeanDefinitionRegistry()** * 获取实现了 PriorityOrdered(主排序接口)接口的 bdrpp,进行 sort 排序,然后全部执行并放入已经处理过的集合 - * 再执行实现了 Ordered(次排序接口)接口的 bdrpp + * 再执行实现了 Ordered(次排序接口)接口的 bdrpp + * 最后执行没有实现任何优先级或者是顺序接口 bdrpp,`boolean reiterate = true` 控制 while 是否需要再次循环,循环内是查找并执行 bdrpp 后处理器的 registry 相关的接口方法,接口方法执行以后会向 bf 内注册 bd,注册的 bd 也有可能是 bdrpp 类型,所以需要该变量控制循环 - * `processedBeans.add(ppName)`:已经执行过的后置处理器存储到该集合中 - * ` invokeBeanFactoryPostProcessors()`:BeanDefinitionRegistryPostProcessor 也继承了 BeanFactoryPostProcessor,也有 postProcessBeanFactory 方法,所以需要调用 - + * `processedBeans.add(ppName)`:已经执行过的后置处理器存储到该集合中,防止重复执行 + * ` invokeBeanFactoryPostProcessors()`:bdrpp 继承了 BeanFactoryPostProcessor,有 postProcessBeanFactory 方法 + * **执行普通 BeanFactoryPostProcessor 的相关 postProcessBeanFactory 方法,按照主次无次序执行** - + * `if (processedBeans.contains(ppName))`:会过滤掉已经执行过的后置处理器 - * `beanFactory.clearMetadataCache()`:清除缓存中合并的 bean 定义,因为后置处理器可能更改了元数据 + * `beanFactory.clearMetadataCache()`:清除缓存中合并的 Bean 定义,因为后置处理器可能更改了元数据 **以上是 BeanFactory 的创建及预准备工作,接下来进入 Bean 的流程** @@ -7572,9 +7557,9 @@ AbstractApplicationContext.refresh(): * onRefresh():留给用户去实现,可以硬编码提供一些组件,比如提供一些监听器 -* registerListeners():注册通过配置提供的 Listener,这些监听器最终注册到 ApplicationEventMulticaster 内 +* registerListeners():注册通过配置提供的 Listener,这些**监听器**最终注册到 ApplicationEventMulticaster 内 - * `for (ApplicationListener listener : getApplicationListeners()) `:注册硬编码实现的监听器 + * `for (ApplicationListener listener : getApplicationListeners()) `:注册编码实现的监听器 * `getBeanNamesForType(ApplicationListener.class, true, false)`:注册通过配置提供的 Listener @@ -7607,13 +7592,13 @@ AbstractApplicationContext.refresh(): * `finishRefresh()`:完成刷新后做的一些事情,主要是启动生命周期 * `clearResourceCaches()`:清空上下文缓存 - * `initLifecycleProcessor()`:**初始化和生命周期有关的后置处理器** + * `initLifecycleProcessor()`:**初始化和生命周期有关的后置处理器**,容器的生命周期 * `if (beanFactory.containsLocalBean(LIFECYCLE_PROCESSOR_BEAN_NAME))`:成立说明自定义了生命周期处理器 * `defaultProcessor = new DefaultLifecycleProcessor()`:Spring 默认提供的生命周期处理器 * ` beanFactory.registerSingleton()`:将生命周期处理器注册到 bf 的一级缓存和注册单例集合中 * `getLifecycleProcessor().onRefresh()`:获取该**生命周期后置处理器回调 onRefresh()**,调用 `startBeans(true)` * `lifecycleBeans = getLifecycleBeans()`:获取到所有实现了 Lifecycle 接口的对象包装到 Map 内,key 是beanName, value 是 Lifecycle 对象 - * `int phase = getPhase(bean)`:获取当前 Lifecycle 的 phase 值,当前生命周期对象可能依赖其他生命周期对象的执行结果,所以需要 **phase 决定执行顺序,数值越低的优先执行** + * `int phase = getPhase(bean)`:获取当前 Lifecycle 的 phase 值,当前生命周期对象可能依赖其他生命周期对象的执行结果,所以需要 phase 决定执行顺序,数值越低的优先执行 * `LifecycleGroup group = phases.get(phase)`:把 phsae 相同的 Lifecycle 存入 LifecycleGroup * `if (group == null)`:group 为空则创建,初始情况下是空的 * `group.add(beanName, bean)`:将当前 Lifecycle 添加到当前 phase 值一样的 group 内 @@ -7752,7 +7737,7 @@ AbstractBeanFactory.doGetBean():获取 Bean,context.getBean() 追踪到此 }); ``` - * `singletonObjects.get(beanName)`:从一级缓存检查是否已经被加载,单例模式复用已经创建的bean + * `singletonObjects.get(beanName)`:从一级缓存检查是否已经被加载,单例模式复用已经创建的 bean * `this.singletonsCurrentlyInDestruction`:容器销毁时会设置这个属性为 true,这时就不能再创建 bean 实例了 @@ -7762,7 +7747,7 @@ AbstractBeanFactory.doGetBean():获取 Bean,context.getBean() 追踪到此 原因:加载 A,向正在创建集合中添加了 {A},根据 A 的构造方法实例化 A 对象,发现 A 的构造方法依赖 B,然后加载 B,B 构造方法的参数依赖于 A,又去加载 A 时来到当前方法,因为创建中集合已经存在 A,所以添加失败 - * `singletonObject = singletonFactory.getObject()`:**实例化 bean**(生命周期部分详解) + * `singletonObject = singletonFactory.getObject()`:**创建 bean**(生命周期部分详解) * **创建完成以后,Bean 已经初始化好,是一个完整的可使用的 Bean** @@ -7774,9 +7759,9 @@ AbstractBeanFactory.doGetBean():获取 Bean,context.getBean() 追踪到此 * 参数 bean 是未处理 & 的 name,beanName 是处理过 & 和别名后的 name - * `if(BeanFactoryUtils.isFactoryDereference(name))`:判断 name 前是否带 & + * `if(BeanFactoryUtils.isFactoryDereference(name))`:判断 doGetBean 中参数 name 前是否带 &,不是处理后的 - * `if(!(beanInstance instanceof FactoryBean) || BeanFactoryUtils.isFactoryDereference(name))`:Bean 是普通单实例或者是 FactoryBean 就可以直接返回,否则进入下面的获取一个 **FactoryBean 内部管理的实例**的逻辑 + * `if(!(beanInstance instanceof FactoryBean) || BeanFactoryUtils.isFactoryDereference(name))`:Bean 是普通单实例或者是 FactoryBean 就可以直接返回,否则进入下面的获取 **FactoryBean 内部管理的实例**的逻辑 * `getCachedObjectForFactoryBean(beanName)`:尝试到缓存获取,获取到直接返回,获取不到进行下面逻辑 @@ -7799,7 +7784,7 @@ AbstractBeanFactory.doGetBean():获取 Bean,context.getBean() 追踪到此 * `this.prototypesCurrentlyInCreation.set(beanName)`:集合为空就把当前 beanName 加入 * `if (curVal instanceof String)`:已经有线程相关原型类创建了,把当前的创建的加进去 - `createBean(beanName, mbd, args)`:创建原型类对象 + `createBean(beanName, mbd, args)`:创建原型类对象,不需要三级缓存 `afterPrototypeCreation(beanName)`:从正在创建中的集合中移除该 beanName, **与 beforePrototypeCreation逻辑相反** @@ -7843,14 +7828,14 @@ AbstractAutowireCapableBeanFactory.**doCreateBean**(beanName, RootBeanDefinition * `if (!mbd.postProcessed)`:每个 bean 只进行一次该逻辑 - `applyMergedBeanDefinitionPostProcessors()`:后置处理器,合并 bd 信息,接下来要属性填充了 + `applyMergedBeanDefinitionPostProcessors()`:**后置处理器,合并 bd 信息**,接下来要属性填充了 `AutowiredAnnotationBeanPostProcessor.postProcessMergedBeanDefinition()`:后置处理逻辑**(@Autowired)** * `metadata = findAutowiringMetadata(beanName, beanType, null)`:提取当前 bean 整个继承体系内的 **@Autowired、@Value、@Inject** 信息,存入一个 InjectionMetadata 对象,保存着当前 bean 信息和要自动注入的字段信息 ```java - private final Class targetClass; //当前 bean + private final Class targetClass; //当前 bean private final Collection injectedElements; //要注入的信息集合 ``` @@ -7872,9 +7857,9 @@ AbstractAutowireCapableBeanFactory.**doCreateBean**(beanName, RootBeanDefinition ```java if (earlySingletonExposure) { - //放入三级缓存 + // 放入三级缓存一个工厂对象,用来获取提前引用 addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); - //lamda 表达式,用来获取提前引用,循环依赖部分详解该逻辑 + // lamda 表达式,用来获取提前引用,循环依赖部分详解该逻辑 } ``` @@ -7883,21 +7868,21 @@ AbstractAutowireCapableBeanFactory.**doCreateBean**(beanName, RootBeanDefinition * ` populateBean(beanName, mbd, instanceWrapper)`:**属性填充,依赖注入,整体逻辑是先处理标签再处理注解,填充至 pvs 中,最后通过 apply 方法最后完成属性依赖注入到 BeanWrapper ** - * `if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName))`:实例化后的后置处理器,默认返回 true,自定义继承 InstantiationAwareBeanPostProcessor 修改返回值为 false,使 continueWithPropertyPopulation 为 false + * `if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName))`:实例化后的后置处理器,默认返回 true,可以自定义类继承 InstantiationAwareBeanPostProcessor 修改后置处理方法的返回值为 false,使 continueWithPropertyPopulation 为 false,会导致直接返回,不进行属性的注入 - * `if (!continueWithPropertyPopulation)`:自定义方法返回值会造成该条件成立,逻辑为直接返回,不能进行依赖注入 + * `if (!continueWithPropertyPopulation)`:自定义方法返回值会造成该条件成立,逻辑为直接返回,**不进行依赖注入** * `PropertyValues pvs = (mbd.hasPropertyValues() ? mbd.getPropertyValues() : null)`:处理依赖注入逻辑开始 * `mbd.getResolvedAutowireMode() == ?`:**根据 bean 标签配置的 autowire** 判断是 BY_NAME 或者 BY_TYPE - `autowireByName(beanName, mbd, bw, newPvs)`:根据字段名称去查找依赖的 bean + `autowireByName(beanName, mbd, bw, newPvs)`:根据字段名称去获取依赖的 bean,还没注入,只是添加到 pvs * `propertyNames = unsatisfiedNonSimpleProperties(mbd, bw)`:bean 实例中有该字段和该字段的 setter 方法,但是在 bd 中没有 property 属性 * 拿到配置的 property 信息和 bean 的所有字段信息 - * `pd.getWriteMethod() != null`:**当前字段是否有 setter 方法,配置类注入的方式需要 set 方法** + * `pd.getWriteMethod() != null`:**当前字段是否有 set 方法,配置类注入的方式需要 set 方法** `!isExcludedFromDependencyCheck(pd)`:当前字段类型是否在忽略自动注入的列表中 @@ -7920,13 +7905,13 @@ AbstractAutowireCapableBeanFactory.**doCreateBean**(beanName, RootBeanDefinition `pvs = newPvs`:newPvs 是处理了依赖数据后的 pvs,所以赋值给 pvs - * `hasInstAwareBpps`:表示当前是否有 InstantiationAwareBeanPostProcessors 的后置处理器 + * `hasInstAwareBpps`:表示当前是否有 InstantiationAwareBeanPostProcessors 的后置处理器(Autowired) - * `pvsToUse = ibp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName)`:**@Autowired 注解的注入** + * `pvsToUse = ibp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName)`:**@Autowired 注解的注入**,这个传入的 pvs 对象,最后原封不动的返回,不会添加东西 * `findAutowiringMetadata()`:包装着当前 bd 需要注入的注解信息集合,**三种注解的元数据**,直接缓存获取 - * `InjectionMetadata.InjectedElement.inject()`:遍历注解信息解析后注入到 Bean,方法和字段的注入的实现不同 + * `InjectionMetadata.InjectedElement.inject()`:**遍历**注解信息解析后注入到 Bean,方法和字段的注入实现不同 以字段注入为例: @@ -7952,7 +7937,7 @@ AbstractAutowireCapableBeanFactory.**doCreateBean**(beanName, RootBeanDefinition } ``` - * `ReflectionUtils.makeAccessible()`:修改访问权限,true 代表暴力破解 + * `ReflectionUtils.makeAccessible(field)`:修改访问权限 * `field.set(bean, value)`:获取属性访问器为此 field 对象赋值 * `applyPropertyValues()`:**将所有解析的 PropertyValues 的注入至 BeanWrapper 实例中**(深拷贝) @@ -8010,8 +7995,8 @@ AbstractAutowireCapableBeanFactory.**doCreateBean**(beanName, RootBeanDefinition // cacheKey 是 beanName 或者加上 & Object cacheKey = getCacheKey(bean.getClass(), beanName);y if (this.earlyProxyReferences.remove(cacheKey) != bean) { - //去提前代理引用池中寻找该key,不存在则创建代理 - //如果存在则证明被代理过,则判断是否是当前的 bean,不是则创建代理 + // 去提前代理引用池中寻找该key,不存在则创建代理 + // 如果存在则证明被代理过,则判断是否是当前的 bean,不是则创建代理 return wrapIfNecessary(bean, bN, cacheKey); } } @@ -8024,13 +8009,13 @@ AbstractAutowireCapableBeanFactory.**doCreateBean**(beanName, RootBeanDefinition * `if (earlySingletonExposure)`:是否允许提前引用 - `earlySingletonReference = getSingleton(beanName, false)`:**从二级缓存获取实例**,放入一级缓存是在 doGetBean 中的sharedInstance = getSingleton() 方法中,此时在 createBean 的逻辑还没有返回。 + `earlySingletonReference = getSingleton(beanName, false)`:**从二级缓存获取实例**,放入一级缓存是在 doGetBean 中的sharedInstance = getSingleton() 逻辑中,此时在 createBean 的逻辑还没有返回,所以一级缓存没有 `if (earlySingletonReference != null)`:当前 bean 实例从二级缓存中获取到了,说明产生了循环依赖,在属性填充阶段会提前调用三级缓存中的工厂生成 Bean 对象的动态代理,放入二级缓存中,然后使用原始 bean 继续执行初始化 * ` if (exposedObject == bean)`:初始化后的 bean == 创建的原始实例,条件成立的两种情况:当前的真实实例不需要被代理;当前实例已经被代理过了,初始化时的后置处理器直接返回 bean 原实例 - `exposedObject = earlySingletonReference`:**把代理后的 Bean 传给 exposedObject 用来返回,因为只有代理对象才封装了增强的拦截器链,main 方法中用代理对象调用方法时,会进行增强,代理是对原始对象的包装,所以这里返回的代理对象中含有完整的原实例(属性填充和初始化后的),是一个完整的代理对象** + `exposedObject = earlySingletonReference`:**把代理后的 Bean 传给 exposedObject 用来返回,因为只有代理对象才封装了拦截器链,main 方法中用代理对象调用方法时会进行增强,代理是对原始对象的包装,所以这里返回的代理对象中含有完整的原实例(属性填充和初始化后的),是一个完整的代理对象** * `else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName))`:是否有其他 bean 依赖当前 bean,执行到这里说明是不存在循环依赖、存在增强代理的逻辑,也就是正常的逻辑 @@ -8045,21 +8030,20 @@ AbstractAutowireCapableBeanFactory.**doCreateBean**(beanName, RootBeanDefinition * `if (!actualDependentBeans.isEmpty())`:条件成立说明有依赖于当前 bean 的 bean 实例创建完成,但是当前的 bean 还没创建完成返回,依赖当前 bean 的外部 bean 持有的是不完整的 bean,所以需要报错 -* `registerDisposableBeanIfNecessary`:判断当前 bean 是否需要注册析构回调,当容器销毁时进行回调 +* `registerDisposableBeanIfNecessary`:判断当前 bean 是否需要**注册析构函数回调**,当容器销毁时进行回调 * `if (!mbd.isPrototype() && requiresDestruction(bean, mbd))` * 如果是原型 prototype 不会注册析构回调,不会回调该函数,对象的回收由 JVM 的 GC 机制完成 - * requiresDestruction: + * requiresDestruction(): - `DisposableBeanAdapter.hasDestroyMethod(bean, mbd)`:bd 中定义了 DestroyMethod 返回 true + * `DisposableBeanAdapter.hasDestroyMethod(bean, mbd)`:bd 中定义了 DestroyMethod 返回 true - `hasDestructionAwareBeanPostProcessors()`:后处理器框架决定是否进行析构回调 + * `hasDestructionAwareBeanPostProcessors()`:后处理器框架决定是否进行析构回调 * `registerDisposableBean()`:条件成立进入该方法,给当前单实例注册回调适配器,适配器内根据当前 bean 实例是继承接口(DisposableBean)还是自定义标签来判定具体调用哪个方法实现 - - * `this.disposableBeans.put(beanName, bean)`:向销毁集合添加实例 +* `this.disposableBeans.put(beanName, bean)`:向销毁集合添加实例 @@ -8096,7 +8080,19 @@ AbstractAutowireCapableBeanFactory.createBeanInstance(beanName, RootBeanDefiniti * `return instantiateBean(beanName, mbd)`:**无参构造方法通过反射创建实例** -* `ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName)`:**@Autowired 注解**配置在构造方法上,对应的后置处理器 AutowiredAnnotationBeanPostProcessor 逻辑 + * `SimpleInstantiationStrategy.instantiate()`:**真正用来实例化的函数**(无论如何都会走到这一步) + + * `if (!bd.hasMethodOverrides())`:没有方法重写覆盖 + + `BeanUtils.instantiateClass(constructorToUse)`:调用 `java.lang.reflect.Constructor.newInstance()` 实例化 + + * `instantiateWithMethodInjection(bd, beanName, owner)`:**有方法重写采用 CGLIB 实例化** + + * `BeanWrapper bw = new BeanWrapperImpl(beanInstance)`:包装成 BeanWrapper 类型的对象 + + * `return bw`:返回实例 + +* `ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName)`:**@Autowired 注解**,对应的后置处理器 AutowiredAnnotationBeanPostProcessor 逻辑 * 配置了 lookup 的相关逻辑 @@ -8139,7 +8135,7 @@ AbstractAutowireCapableBeanFactory.createBeanInstance(beanName, RootBeanDefiniti `requiredConstructor = candidate`:把构造器赋值给 requiredConstructor - * `candidates.add(candidate)`:**把当前构造方法添加至 candidates 集合** + * `candidates.add(candidate)`:把当前构造方法添加至 candidates 集合 ` if(candidate.getParameterCount() == 0)`:当前遍历的构造器的参数为 0 代表没有参数,是**默认构造器**,赋值给 defaultConstructor @@ -8232,18 +8228,6 @@ AbstractAutowireCapableBeanFactory.createBeanInstance(beanName, RootBeanDefiniti * `return instantiateBean(beanName, mbd)`:默认走到这里 - * `SimpleInstantiationStrategy.instantiate()`:**真正用来实例化的函数**(无论如何都会走到这一步) - - * `if (!bd.hasMethodOverrides())`:没有方法重写覆盖 - - `BeanUtils.instantiateClass(constructorToUse)`:调用 `java.lang.reflect.Constructor.newInstance()` 实例化 - - * `instantiateWithMethodInjection(bd, beanName, owner)`:**有方法重写采用 CGLIB 实例化** - - * `BeanWrapper bw = new BeanWrapperImpl(beanInstance)`:包装成 BeanWrapper 类型的对象 - - * `return bw`:返回实例 - **** @@ -8259,24 +8243,24 @@ AbstractAutowireCapableBeanFactory.createBeanInstance(beanName, RootBeanDefiniti Spring 循环依赖有四种: * DependsOn 依赖加载【无法解决】(两种 Map) -* 原型模式循环依赖【无法解决】(正在创建集合) -* 单例 Bean 循环依赖:构造参数产生依赖【无法解决】(正在创建集合) +* 原型模式 Prototype 循环依赖【无法解决】(正在创建集合) +* 单例 Bean 循环依赖:构造参数产生依赖【无法解决】(正在创建集合,getSingleton() 逻辑中) * 单例 Bean 循环依赖:setter 产生依赖【可以解决】 解决循环依赖:提前引用,提前暴露创建中的 Bean * Spring 先实例化 A,拿到 A 的构造方法反射创建出来 A 的早期实例对象,这个对象被包装成 ObjectFactory 对象,放入三级缓存 -* 处理 A 的依赖数据,检查发现 A 依赖 B 对象,所以 Spring 就会去根据 B 类型到容器中去 getBean(B.class),这里产生递归 +* 处理 A 的依赖数据,检查发现 A 依赖 B 对象,所以 Spring 就会去根据 B 类型到容器中去 getBean(B),这里产生递归 * 拿到 B 的构造方法,进行反射创建出来 B 的早期实例对象,也会把 B 包装成 ObjectFactory 对象,放到三级缓存,处理 B 的依赖数据,检查发现 B 依赖了 A 对象,然后 Spring 就会去根据 A 类型到容器中去 getBean(A.class) -* 这时获取到 A 的早期对象进入属性填充 +* 这时从三级缓存中获取到 A 的早期对象进入属性填充 循环依赖的三级缓存: ```java -//一级缓存:存放所有初始化完成单实例bean,单例池,key是beanName,value是对应的单实例对象引用 +//一级缓存:存放所有初始化完成单实例 bean,单例池,key是beanName,value是对应的单实例对象引用 private final Map singletonObjects = new ConcurrentHashMap<>(256); -//二级缓存:存放实例化未进行初始化的Bean,提前引用池 +//二级缓存:存放实例化未进行初始化的 Bean,提前引用池 private final Map earlySingletonObjects = new HashMap<>(16); /** Cache of singleton factories: bean name to ObjectFactory. 3*/ @@ -8285,13 +8269,14 @@ private final Map> singletonFactories = new HashMap<>(1 * 为什么需要三级缓存? - * 循环依赖解决需要提前引用动态代理对象,AOP 动态代理是在 Bean 初始化后的后置处理中进行,这时的 bean 已经是成品对象,需要提前进行动态代理,三级缓存的 ObjectFactory 提前产生需要代理的对象 + * 循环依赖解决需要提前引用动态代理对象,AOP 动态代理是在 Bean 初始化后的后置处理中进行,这时的 bean 已经是成品对象。因为需要提前进行动态代理,三级缓存的 ObjectFactory 提前产生需要代理的对象,把提前引用放入二级缓存 + * 如果只有二级缓存,提前引用就直接放入了一级缓存,然后初始化 Bean 完成后会将 Bean 放入一级缓存,这时就发生冲突了 * 若存在循环依赖,**后置处理不创建代理对象,真正创建代理对象的过程是在 getBean(B) 的阶段中** * 三级缓存一定会创建提前引用吗? * 出现循环依赖就会去三级缓存获取提前引用,不出现就不会,走正常的逻辑,创建完成直接放入一级缓存 - * 存在循环依赖,就创建代理对象放入二级缓存,如果没有增强方法就返回 createBeanInstance 创建的实例,因为 addSingletonFactory 参数中传入了实例化的 Bean,在 singletonFactory.getObject() 中返回给 singletonObject + * 存在循环依赖,就创建代理对象放入二级缓存,如果没有增强方法就返回 createBeanInstance 创建的实例,因为 addSingletonFactory 参数中传入了实例化的 Bean,在 singletonFactory.getObject() 中返回给 singletonObject,所以**存在循环依赖就一定会使用工厂**,但是不一定创建的是代理对象 * wrapIfNecessary 一定创建代理对象吗?(AOP 动态代理部分有源码解析) @@ -8303,7 +8288,7 @@ private final Map> singletonFactories = new HashMap<>(1 * 实例化之后,依赖注入之前 ```java - createBeanInstance --> addSingletonFactory --> populateBean + createBeanInstance -> addSingletonFactory -> populateBean ``` @@ -8329,11 +8314,12 @@ private final Map> singletonFactories = new HashMap<>(1 protected void addSingletonFactory(String beanName, ObjectFactory singletonFactory) { Assert.notNull(singletonFactory, "Singleton factory must not be null"); synchronized (this.singletonObjects) { - //单例池包含该Bean说明已经创建完成,不需要循环依赖 + // 单例池包含该Bean说明已经创建完成,不需要循环依赖 if (!this.singletonObjects.containsKey(beanName)) { - this.singletonFactories.put(beanName,singletonFactory);//加入三级缓存 + //加入三级缓存 + this.singletonFactories.put(beanName,singletonFactory); this.earlySingletonObjects.remove(beanName); - //从二级缓存移除,因为三个Map中都是一个对象,不能同时存在! + // 从二级缓存移除,因为三个Map中都是一个对象,不能同时存在! this.registeredSingletons.add(beanName); } } @@ -8349,21 +8335,20 @@ private final Map> singletonFactories = new HashMap<>(1 protected Object getSingleton(String beanName, boolean allowEarlyReference) { // 在一级缓存中获取 beanName 对应的单实例对象。 Object singletonObject = this.singletonObjects.get(beanName); - //条件一成立:单实例确实尚未创建;单实例正在创建,发生了循环依赖 - //条件二成立:代表单实例正在创建 + // 单实例确实尚未创建;单实例正在创建,发生了循环依赖 if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { synchronized (this.singletonObjects) { - //从二级缓存获取 + // 从二级缓存获取 singletonObject = this.earlySingletonObjects.get(beanName); - //二级缓存不存在,并且允许获取早期实例对象,去三级缓存查看 + // 二级缓存不存在,并且允许获取早期实例对象,去三级缓存查看 if (singletonObject == null && allowEarlyReference) { ObjectFactory singletonFactory = this.singletonFactories.get(beanName); if (singletonFactory != null) { - //从三级缓存获取工厂对象,并得到bean的提前引用 + // 从三级缓存获取工厂对象,并得到 bean 的提前引用 singletonObject = singletonFactory.getObject(); - //缓存升级,放入二级缓存,提前引用池 + // 【缓存升级】,放入二级缓存,提前引用池 this.earlySingletonObjects.put(beanName, singletonObject); - //从三级缓存移除该对象 + // 从三级缓存移除该对象 this.singletonFactories.remove(beanName); } } @@ -8372,15 +8357,15 @@ private final Map> singletonFactories = new HashMap<>(1 return singletonObject; } ``` - -* 从三级缓存获取 A 的 Bean:`singletonFactory.getObject()`,调用了 Lambda 表达式的 getEarlyBeanReference 方法: + +* 从三级缓存获取 A 的 Bean:`singletonFactory.getObject()`,调用了 lambda 表达式的 getEarlyBeanReference 方法: ```java public Object getEarlyBeanReference(Object bean, String beanName) { Object cacheKey = getCacheKey(bean.getClass(), beanName); - //向提前引用代理池 earlyProxyReferences 中添加该Bean,防止对象被重新代理 + // 【向提前引用代理池 earlyProxyReferences 中添加该 Bean,防止对象被重新代理】 this.earlyProxyReferences.put(cacheKey, bean); - //创建代理对象,createProxy + // 创建代理对象,createProxy return wrapIfNecessary(bean, beanName, cacheKey); } ``` @@ -8422,19 +8407,17 @@ AspectJAutoProxyRegistrar 在用来向容器中注册 **AnnotationAwareAspectJAu -#### 动态代理 - -##### 后置处理 +#### 后置处理 Bean 初始化完成的执行后置处理器的方法: ```java public Object postProcessAfterInitialization(@Nullable Object bean,String bN){ if (bean != null) { - // cacheKey 是 beanName 或者加上 & + // cacheKey 是 【beanName 或者加上 & 的 beanName】 Object cacheKey = getCacheKey(bean.getClass(), beanName); if (this.earlyProxyReferences.remove(cacheKey) != bean) { - // 去提前代理引用池中寻找该key,不存在则创建代理 + // 去提前代理引用池中寻找该 key,不存在则创建代理 // 如果存在则证明被代理过,则判断是否是当前的 bean,不是则创建代理 return wrapIfNecessary(bean, bN, cacheKey); } @@ -8447,7 +8430,7 @@ AbstractAutoProxyCreator.wrapIfNecessary():根据通知创建动态代理, ```java protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { - //条件一般不成立,很少使用 TargetSourceCreator 去创建对象 BeforeInstantiation 阶段,doCreateBean 之前的阶段 + // 条件一般不成立,很少使用 TargetSourceCreator 去创建对象 BeforeInstantiation 阶段,doCreateBean 之前的阶段 if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) { return bean; } @@ -8456,32 +8439,32 @@ protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) { return bean; } - //条件一:判断当前 bean 类型是否是基础框架类型,这个类的实例不能被增强 - //条件二:shouldSkip 判断当前 beanName 是否是 .ORIGINAL 结尾,如果是就跳过增强逻辑,直接返回 + // 条件一:判断当前 bean 类型是否是基础框架类型,这个类的实例不能被增强 + // 条件二:shouldSkip 判断当前 beanName 是否是 .ORIGINAL 结尾,如果是就跳过增强逻辑,直接返回 if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) { this.advisedBeans.put(cacheKey, Boolean.FALSE); return bean; } - // 查找适合当前 bean 实例的增强方法(下一节详解) + // 【查找适合当前 bean 实例的增强方法】(下一节详解) Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null); - //条件成立说明上面方法查询到适合当前class的通知 + // 条件成立说明上面方法查询到适合当前class的通知 if (specificInterceptors != DO_NOT_PROXY) { this.advisedBeans.put(cacheKey, Boolean.TRUE); - //根据查询到的增强创建代理对象(下一节详解) - //参数一:目标对象 - //参数二:beanName - //参数三:匹配当前目标对象 clazz 的 Advisor 数据 + // 根据查询到的增强创建代理对象(下一节详解) + // 参数一:目标对象 + // 参数二:beanName + // 参数三:匹配当前目标对象 clazz 的 Advisor 数据 Object proxy = createProxy( bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean)); - //保存代理对象类型 + // 保存代理对象类型 this.proxyTypes.put(cacheKey, proxy.getClass()); - //返回代理对象 + // 返回代理对象 return proxy; } // 执行到这里说明没有查到通知,当前 bean 不需要增强 this.advisedBeans.put(cacheKey, Boolean.FALSE); - //返回原始的 bean 实例 + // 【返回原始的 bean 实例】 return bean; } ``` @@ -8492,16 +8475,16 @@ protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) -##### 获取通知 +#### 获取通知 -AbstractAdvisorAutoProxyCreator.getAdvicesAndAdvisorsForBean():查找适合当前实例的增强,并进行排序 +AbstractAdvisorAutoProxyCreator.getAdvicesAndAdvisorsForBean():查找适合当前类实例的增强,并进行排序 ```java protected Object[] getAdvicesAndAdvisorsForBean(Class beanClass, String beanName, @Nullable TargetSource targetSource) { // 查询适合当前类型的增强通知 List advisors = findEligibleAdvisors(beanClass, beanName); if (advisors.isEmpty()) { - //增强为空直接返回 null,不需要创建代理 + // 增强为空直接返回 null,不需要创建代理 return DO_NOT_PROXY; } // 不是空,转成数组返回 @@ -8513,36 +8496,34 @@ AbstractAdvisorAutoProxyCreator.findEligibleAdvisors(): * `candidateAdvisors = findCandidateAdvisors()`:**获取当前容器内可以使用(所有)的 advisor**,调用的是 AnnotationAwareAspectJAutoProxyCreator 类的方法 - * `advisors = super.findCandidateAdvisors()`:查询出 XML 配置的所有 Advisor 类型 + * `advisors = super.findCandidateAdvisors()`:**查询出 XML 配置的所有 Advisor 类型** * `advisorNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors()`:通过 BF 查询出来 BD 配置的 class 中 是 Advisor 子类的 BeanName * `advisors.add()`:使用 Spring 容器获取当前这个 Advisor 类型的实例 - * `advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors())`:获取添加 @Aspect 注解类中的 Advisor + * `advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors())`:**获取添加 @Aspect 注解类中的 Advisor** - `buildAspectJAdvisors()`:构建的方法,**把 Advice 封装成 Advisor**(逻辑很绕,不建议深究) + `buildAspectJAdvisors()`:构建的方法,**把 Advice 封装成 Advisor** * ` beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, Object.class, true, false)`:获取出容器内 Object 所有的 beanName,就是全部的 - * ` for (String beanName : beanNames)`:遍历所有的 beanName,判断每个 beanName 对应的 class 是否是 Aspect 类型,就是**加了 @Aspect 注解的类** + * ` for (String beanName : beanNames)`:遍历所有的 beanName,判断每个 beanName 对应的 Class 是否是 Aspect 类型,就是加了 @Aspect 注解的类 * `factory = new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName)`:使用工厂模式管理 Aspect 的元数据,关联的真实 @Aspect 注解的实例对象 - * `classAdvisors = this.advisorFactory.getAdvisors(factory)`:获取当前添加了 @Aspect 注解的 class 的 Advisor 相关信息 + * `classAdvisors = this.advisorFactory.getAdvisors(factory)`:添加了 @Aspect 注解的类的通知信息 * aspectClass:@Aspect 标签的类的 class - * `for (Method method : getAdvisorMethods(aspectClass))`:遍历不包括 @Pointcut 注解的方法 + * `for (Method method : getAdvisorMethods(aspectClass))`:遍历**不包括 @Pointcut 注解的方法** - * `Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, advisors.size(), aspectName)`:将当前 method 包装成 Advisor 数据 + `Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, advisors.size(), aspectName)`:**将当前 method 包装成 Advisor 数据** * `AspectJExpressionPointcut expressionPointcut = getPointcut()`:获取切点表达式 * `return new InstantiationModelAwarePointcutAdvisorImpl()`:把 method 中 Advice 包装成 Advisor,Spring 中每个 Advisor 内部一定是持有一个 Advice 的,Advice 内部最重要的数据是当前 method 和aspectInstanceFactory,工厂用来获取实例 - `this.instantiatedAdvice = instantiateAdvice(this.declaredPointcut)`:实例化 Advice 对象 - - * `this.aspectJAdvisorFactory.getAdvice()`:获取 Advice,逻辑是获取注解信息,根据注解的不同生成对应的 Advice 对象 + `this.instantiatedAdvice = instantiateAdvice(this.declaredPointcut)`:实例化 Advice 对象,逻辑是获取注解信息,根据注解的不同生成对应的 Advice 对象 * `advisors.addAll(classAdvisors)`:保存通过 @Aspect 注解定义的 Advisor 数据 @@ -8550,7 +8531,7 @@ AbstractAdvisorAutoProxyCreator.findEligibleAdvisors(): * `return advisors`:返回 Advisor 列表 -* `eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName)`:**选出适合当前类的增强** +* `eligibleAdvisors=findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName)`:**选出匹配当前类的增强** * `if (candidateAdvisors.isEmpty())`:条件成立说明当前 Spring 没有可以操作的 Advisor @@ -8576,7 +8557,7 @@ AbstractAdvisorAutoProxyCreator.findEligibleAdvisors(): * `extendAdvisors(eligibleAdvisors)`:在 eligibleAdvisors 列表的索引 0 的位置添加 DefaultPointcutAdvisor,**封装了 ExposeInvocationInterceptor 拦截器** -* ` eligibleAdvisors = sortAdvisors(eligibleAdvisors)`:**对拦截器链进行排序**,数值越小优先级越高,高的排在前面 +* ` eligibleAdvisors = sortAdvisors(eligibleAdvisors)`:**对拦截器进行排序**,数值越小优先级越高,高的排在前面 * 实现 Ordered 或 PriorityOrdered 接口,PriorityOrdered 的级别要优先于 Ordered,使用 OrderComparator 比较器 * 使用 @Order(Spring 规范)或 @Priority(JDK 规范)注解,使用 AnnotationAwareOrderComparator 比较器 @@ -8589,11 +8570,11 @@ AbstractAdvisorAutoProxyCreator.findEligibleAdvisors(): -##### 创建代理 +#### 创建代理 AbstractAutoProxyCreator.createProxy():根据增强方法创建代理对象 -* `ProxyFactory proxyFactory = new ProxyFactory()`:**无参构造 ProxyFactory**,讲解一下两种有参构造方法: +* `ProxyFactory proxyFactory = new ProxyFactory()`:**无参构造 ProxyFactory**,此处讲解一下两种有参构造方法: * public ProxyFactory(Object target): @@ -8629,12 +8610,12 @@ AbstractAutoProxyCreator.createProxy():根据增强方法创建代理对象 * `proxyFactory.copyFrom(this)`:填充一些信息到 proxyFactory -* `if (!proxyFactory.isProxyTargetClass())`:条件成立说明没有配置修改过 **proxyTargetClass** 为 true,两种配置方法: +* `if (!proxyFactory.isProxyTargetClass())`:条件成立说明 proxyTargetClass 为 false(默认),两种配置方法: - * ` ` + * ` `:强制使用 CGLIB * `@EnableAspectJAutoProxy(proxyTargetClass = true)` - `if (shouldProxyTargetClass(beanClass, beanName))`:如果 bd 内有 preserveTargetClass = true ,那么这个 bd 对应的 class 创建代理时必须使用 CGLIB,条件成立设置 proxyTargetClass 为 true + `if (shouldProxyTargetClass(beanClass, beanName))`:如果 bd 内有 preserveTargetClass = true ,那么这个 bd 对应的 class **创建代理时必须使用 CGLIB**,条件成立设置 proxyTargetClass 为 true `evaluateProxyInterfaces(beanClass, proxyFactory)`:**根据目标类判定是否可以使用 JDK 动态代理** @@ -8663,9 +8644,9 @@ AbstractAutoProxyCreator.createProxy():根据增强方法创建代理对象 ```java public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { - //条件二为 true 代表强制使用 CGLIB 动态代理, + // 条件二为 true 代表强制使用 CGLIB 动态代理 if (config.isOptimize() || config.isProxyTargetClass() || - //条件三:被代理对象没有实现任何接口或者只实现了 SpringProxy 接口,只能使用 CGLIB 动态代理 + // 条件三:被代理对象没有实现任何接口或者只实现了 SpringProxy 接口,只能使用 CGLIB 动态代理 hasNoUserSuppliedProxyInterfaces(config)) { Class targetClass = config.getTargetClass(); if (targetClass == null) { @@ -8678,7 +8659,7 @@ AbstractAutoProxyCreator.createProxy():根据增强方法创建代理对象 return new ObjenesisCglibAopProxy(config); // 使用 CGLIB 动态代理 } else { - return new JdkDynamicAopProxy(config); // 有接口的情况下只能使用 JDK 动态代理 + return new JdkDynamicAopProxy(config); // 【有接口的情况下只能使用 JDK 动态代理】 } } ``` @@ -8712,7 +8693,7 @@ AbstractAutoProxyCreator.createProxy():根据增强方法创建代理对象 * ` addSpringProxy = !advised.isInterfaceProxied(SpringProxy.class)`:判断目标对象所有接口中是否有 SpringProxy 接口,没有的话需要添加,这个接口**标识这个代理类型是 Spring 管理的** * `addAdvised = !advised.isOpaque() && !advised.isInterfaceProxied(Advised.class)`:判断目标对象的所有接口,是否已经有 Advised 接口 * ` addDecoratingProxy = (decoratingProxy && !advised.isInterfaceProxied(DecoratingProxy.class))`:判断目标对象的所有接口,是否已经有 DecoratingProxy 接口 - * `int nonUserIfcCount = 0`:非用户自己定义的接口数量,接下来要添加上面的三个接口了 + * `int nonUserIfcCount = 0`:非用户自定义的接口数量,接下来要添加上面的三个接口了 * `proxiedInterfaces = new Class[specifiedInterfaces.length + nonUserIfcCount]`:创建一个新的 class 数组,长度是原目标对象提取出来的接口数量和 Spring 追加的数量,然后进行 **System.arraycopy 拷贝到新数组中** * `int index = specifiedInterfaces.length`:获取原目标对象提取出来的接口数量,当作 index * `if(addSpringProxy)`:根据上面三个布尔值把接口添加到新数组中 @@ -8736,12 +8717,12 @@ AbstractAutoProxyCreator.createProxy():根据增强方法创建代理对象 #### 方法增强 -main() 函数中调用用户方法,会进入该逻辑 +main() 函数中调用用户方法,会进入代理对象的 invoke 方法 JdkDynamicAopProxy 类中的 invoke 方法是真正执行代理方法 ```java -//proxy:代理对象,method:目标对象的方法,args:目标对象方法对应的参数 +// proxy:代理对象,method:目标对象的方法,args:目标对象方法对应的参数 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object oldProxy = null; boolean setProxyContext = false; @@ -8760,19 +8741,19 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl Object retVal; // 需不需要暴露当前代理对象到 AOP 上下文内 if (this.advised.exposeProxy) { - // 把代理对象设置到上下文环境 + // 【把代理对象设置到上下文环境】 oldProxy = AopContext.setCurrentProxy(proxy); setProxyContext = true; } - // 根据 targetSource 获取真正的代理对象 + // 根据 targetSource 获取真正的代理对象 target = targetSource.getTarget(); Class targetClass = (target != null ? target.getClass() : null); // 查找【适合该方法的增强】,首先从缓存中查找,查找不到进入主方法【下文详解】 List chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); - // 拦截器链数量是 0 说明当前 method 不需要被增强 + // 拦截器链时空,说明当前 method 不需要被增强 if (chain.isEmpty()) { Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse); @@ -8781,7 +8762,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl // 有匹配当前 method 的方法拦截器,要做增强处理,把方法信息封装到方法调用器里 MethodInvocation invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain); - // 【拦截器链驱动方法,下文详解】 + // 【拦截器链驱动方法,核心】 retVal = invocation.proceed(); } @@ -8802,7 +8783,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl } // 如果允许了提前暴露,这里需要设置为初始状态 if (setProxyContext) { - // 当前代理对象已经完成工作,把原始对象设置回上下文 + // 当前代理对象已经完成工作,【把原始对象设置回上下文】 AopContext.setCurrentProxy(oldProxy); } } @@ -8815,21 +8796,29 @@ this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass): * `instance = new DefaultAdvisorAdapterRegistry()`:该对象向容器中注册了 MethodBeforeAdviceAdapter、AfterReturningAdviceAdapter、ThrowsAdviceAdapter 三个适配器 + * Advisor 中持有 Advice 对象 + + ```java + public interface Advisor { + Advice getAdvice(); + } + ``` + * `advisors = config.getAdvisors()`:获取 ProxyFactory 内部持有的增强信息 -* `interceptorList = new ArrayList<>(advisors.length)`:拦截器列表有 5 个,一个 ExposeInvocationInterceptor 和 4 个增强器 +* `interceptorList = new ArrayList<>(advisors.length)`:拦截器列表有 5 个,1 个 ExposeInvocation和 4 个增强器 * `actualClass = (targetClass != null ? targetClass : method.getDeclaringClass())`:真实的目标对象类型 * `Boolean hasIntroductions = null`:引介增强,不关心 -* `for (Advisor advisor : advisors)`:**遍历所有的增强** +* `for (Advisor advisor : advisors)`:**遍历所有的 advisor 增强** * `if (advisor instanceof PointcutAdvisor)`:条件成立说明当前 Advisor 是包含切点信息的,进入匹配逻辑 `pointcutAdvisor = (PointcutAdvisor) advisor`:转成可以获取到切点信息的接口 - `if(config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass))`:当前代理被预处理,或者当前被代理的 class 对象匹配当前 Advisor 成功,只是 **class 匹配成功** + `if(config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass))`:当前代理被预处理,或者当前被代理的 class 对象匹配当前 Advisor 成功,只是 class 匹配成功 * `mm = pointcutAdvisor.getPointcut().getMethodMatcher()`:获取切点的方法匹配器,不考虑引介增强 @@ -8837,20 +8826,20 @@ this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass): * `if (match)`:如果静态切点检查是匹配的,在运行的时候才进行**动态切点检查,会考虑参数匹配**(代表传入了参数)。如果静态匹配失败,直接不需要进行参数匹配,提高了工作效率 - `interceptors = registry.getInterceptors(advisor)`:提取出 advisor 内持有的拦截器信息 + `interceptors = registry.getInterceptors(advisor)`:提取出当前 advisor 内持有的 advice 信息 * `Advice advice = advisor.getAdvice()`:获取增强方法 * `if (advice instanceof MethodInterceptor)`:当前 advice 是 MethodInterceptor 直接加入集合 - * `for (AdvisorAdapter adapter : this.adapters)`:**遍历三个适配器进行匹配**(初始化时创建的),以 MethodBeforeAdviceAdapter 为例 + * `for (AdvisorAdapter adapter : this.adapters)`:**遍历三个适配器进行匹配**(初始化时创建的),匹配成功创建对应的拦截器返回,以 MethodBeforeAdviceAdapter 为例 `if (adapter.supportsAdvice(advice))`:判断当前 advice 是否是对应的 MethodBeforeAdvice `interceptors.add(adapter.getInterceptor(advisor))`:条件成立就往拦截器链中添加 advisor * `advice = (MethodBeforeAdvice) advisor.getAdvice()`:**获取增强方法** - * `return new MethodBeforeAdviceInterceptor(advice)`:**封装成 MethodInterceptor 方法拦截器返回** + * `return new MethodBeforeAdviceInterceptor(advice)`:**封装成 MethodBeforeAdviceInterceptor 返回** `interceptorList.add(new InterceptorAndDynamicMethodMatcher(interceptor, mm))`:向拦截器链添加动态匹配器 @@ -8862,9 +8851,9 @@ this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass): retVal = invocation.proceed():**拦截器链驱动方法** -* `if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1)`:条件成立说明方法拦截器全部都已经调用过了(0 - 1 = -1),接下来需要执行目标对象的目标方法 +* `if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1)`:条件成立说明方法拦截器全部都已经调用过了(index 从 - 1 开始累加),接下来需要执行目标对象的目标方法 - `return invokeJoinpoint()`:调用连接点 + `return invokeJoinpoint()`:**调用连接点方法** * `this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex)`:**获取下一个方法拦截器** @@ -8875,7 +8864,7 @@ retVal = invocation.proceed():**拦截器链驱动方法** * `return dm.interceptor.invoke(this)`:匹配成功,执行方法 * `return proceed()`:匹配失败跳过当前拦截器 -* `return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this)`:**所有的方法拦截器都会执行到该方法,此方法内继续执行 proceed() 完成责任链的驱动,直到最后一个 MethodBeforeAdviceInterceptor 调用前置通知,然后调用 mi.proceed(),发现是最后一个拦截器就直接执行目标方法,return 到上一个拦截器的 mi.proceed() 处,依次返回到责任链的上一个拦截器执行通知方法** +* `return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this)`:**一般方法拦截器都会执行到该方法,此方法内继续执行 proceed() 完成责任链的驱动,直到最后一个 MethodBeforeAdviceInterceptor 调用前置通知,然后调用 mi.proceed(),发现是最后一个拦截器就直接执行连接点(目标方法),return 到上一个拦截器的 mi.proceed() 处,依次返回到责任链的上一个拦截器执行通知方法** 图示先从上往下建立链,然后从下往上依次执行,责任链模式 @@ -8883,24 +8872,38 @@ retVal = invocation.proceed():**拦截器链驱动方法** * 出现异常:(环绕通知)→ 前置通知 → 目标方法 → 后置通知 → 异常通知 - * AfterReturningAdviceInterceptor 源码:没有任何异常处理机制,直接抛给上层 + * MethodBeforeAdviceInterceptor 源码: ```java public Object invoke(MethodInvocation mi) throws Throwable { + // 先执行通知方法,再驱动责任链 + this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis()); + // 开始驱动目标方法执行,执行完后返回到这,然后继续向上层返回 + return mi.proceed(); + } + ``` + + AfterReturningAdviceInterceptor 源码:没有任何异常处理机制,直接抛给上层 + + ```java + public Object invoke(MethodInvocation mi) throws Throwable { + // 先驱动责任链,再执行通知方法 Object retVal = mi.proceed(); this.advice.afterReturning(retVal, mi.getMethod(), mi.getArguments(), mi.getThis()); return retVal; } ``` - + AspectJAfterThrowingAdvice 执行异常处理: - + ```java public Object invoke(MethodInvocation mi) throws Throwable { try { + // 默认直接驱动责任链 return mi.proceed(); } catch (Throwable ex) { + // 出现错误才执行该方法 if (shouldInvokeOnThrowing(ex)) { invokeAdviceMethod(getJoinPointMatch(), null, ex); } @@ -8933,19 +8936,18 @@ retVal = invocation.proceed():**拦截器链驱动方法** ```java protected Set doScan(String... basePackages) { Set beanDefinitions = new LinkedHashSet<>(); - // 遍历指定的所有的包 + // 遍历指定的所有的包,【这就相当于扫描了】 for (String basePackage : basePackages) { // 读取当前包下的资源装换为 BeanDefinition,字节流的方式 Set candidates = findCandidateComponents(basePackage); for (BeanDefinition candidate : candidates) { - //遍历,封装,类似于 XML 的解析方式 - // 注册到容器中 + // 遍历,封装,类似于 XML 的解析方式,注册到容器中 registerBeanDefinition(definitionHolder, this.registry) } return beanDefinitions; } ``` - + * ClassPathScanningCandidateComponentProvider.findCandidateComponents() ```java @@ -8965,7 +8967,7 @@ retVal = invocation.proceed():**拦截器链驱动方法** * `String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX resolveBasePackage(basePackage) + '/' + this.resourcePattern` :将 package 转化为 ClassLoader 类资源搜索路径 packageSearchPath,例如:`com.sea.spring.boot` 转化为 `classpath*:com/sea/spring/boot/**/*.class` - * `resources = getResourcePatternResolver().getResources(packageSearchPath)`:加载搜素路径下的资源 + * `resources = getResourcePatternResolver().getResources(packageSearchPath)`:加载路径下的资源 * `for (Resource resource : resources) `:遍历所有的资源 @@ -8983,7 +8985,7 @@ retVal = invocation.proceed():**拦截器链驱动方法** @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented - @Component + @Component // 拥有了 Component 功能 public @interface Service {} ``` @@ -9009,7 +9011,7 @@ AutowiredAnnotationBeanPostProcessor 间接实现 InstantiationAwareBeanPostProc 作用时机: -* Spring 在每个 Bean 实例化之后,调用 AutowiredAnnotationBeanPostProcessor 的 `postProcessMergedBeanDefinition()` 方法,查找该 Bean 是否有 @Autowired 注解,进行相关数据的获取 +* Spring 在每个 Bean 实例化之后,调用 AutowiredAnnotationBeanPostProcessor 的 `postProcessMergedBeanDefinition()` 方法,查找该 Bean 是否有 @Autowired 注解,进行相关元数据的获取 * Spring 在每个 Bean 调用 `populateBean()` 进行属性注入的时候,即调用 `postProcessProperties()` 方法,查找该 Bean 属性是否有 @Autowired 注解,进行相关数据的填充 @@ -9025,13 +9027,13 @@ AutowiredAnnotationBeanPostProcessor 间接实现 InstantiationAwareBeanPostProc * AdviceMode 为 PROXY:导入 AutoProxyRegistrar 和 ProxyTransactionManagementConfiguration(默认) * AdviceMode 为 ASPECTJ:导入 AspectJTransactionManagementConfiguration(与声明式事务无关) -AutoProxyRegistrar:给容器中注册 InfrastructureAdvisorAutoProxyCreator,该类实现了 InstantiationAwareBeanPostProcessor 接口,可以拦截 Spring 的 bean 初始化和实例化前后。**利用后置处理器机制拦截 bean 以后包装该 bean 并返回一个代理对象**,代理对象中保存所有的拦截器,代理对象执行目标方法,利用拦截器的链式机制依次进入每一个拦截器中进行执行(就是 AOP 原理) +AutoProxyRegistrar:给容器中注册 InfrastructureAdvisorAutoProxyCreator,**利用后置处理器机制拦截 bean 以后包装并返回一个代理对象**,代理对象中保存所有的拦截器,利用拦截器的链式机制依次进入每一个拦截器中进行拦截执行(就是 AOP 原理) ProxyTransactionManagementConfiguration:是一个 Spring 的事务配置类,注册了三个 Bean: * BeanFactoryTransactionAttributeSourceAdvisor:事务增强器,利用注解 @Bean 把该类注入到容器中,该增强器有两个字段: -* TransactionAttributeSource:用于解析事务注解的相关信息,比如 @Transactional 注解,该类的真实类型是 AnnotationTransactionAttributeSource,初始化方法中注册了三个**注解解析器**,解析三种类型的事务注解 Spring、JTA、Ejb3 +* TransactionAttributeSource:解析事务注解的相关信息,比如 @Transactional 注解,该类的真实类型是 AnnotationTransactionAttributeSource,构造方法中注册了三个**注解解析器**,解析三种类型的事务注解 Spring、JTA、Ejb3 * TransactionInterceptor:**事务拦截器**,代理对象执行拦截器方法时,会调用 TransactionInterceptor 的 invoke 方法,底层调用TransactionAspectSupport.invokeWithinTransaction(),通过 PlatformTransactionManager 控制着事务的提交和回滚,所以事务的底层原理就是通过 AOP 动态织入,进行事务开启和提交 @@ -9040,23 +9042,36 @@ ProxyTransactionManagementConfiguration:是一个 Spring 的事务配置类, final PlatformTransactionManager tm = determineTransactionManager(txAttr); // 开启事务 TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification); - // 执行目标方法 + // 执行目标方法(方法引用方式,invocation::proceed,还是调用 proceed) retVal = invocation.proceedWithInvocation(); - // 调用 java.sql.Connection 提交或者回滚事务 + // 提交或者回滚事务 commitTransactionAfterReturning(txInfo); ``` `createTransactionIfNecessary(tm, txAttr, joinpointIdentification)`: - * `status = tm.getTransaction(txAttr)`:获取事务状态,方法内通过 doBegin **调用 Connection 的 setAutoCommit 开启事务**,就是 JDBC 原生的方式 + * `status = tm.getTransaction(txAttr)`:获取事务状态,开启事务 - * `prepareTransactionInfo(tm, txAttr, joinpointIdentification, status)`:方法内调用 bindToThread() 方法,利用 ThreadLocal 把当前事务绑定到当前线程(一个线程对应一个事务) + * `doBegin`: **调用 Connection 的 setAutoCommit(false) 开启事务**,就是 JDBC 原生的方式 - 补充策略模式(Strategy Pattern):**使用不同策略的对象实现不同的行为方式**,策略对象的变化导致行为的变化,事务也是这种模式,每个事务对应一个新的 connection 对象 - + * `prepareTransactionInfo(tm, txAttr, joinpointIdentification, status)`:准备事务信息 + + * `bindToThread() `:利用 ThreadLocal **把当前事务绑定到当前线程**(一个线程对应一个事务) - - + 策略模式(Strategy Pattern):使用不同策略的对象实现不同的行为方式,策略对象的变化导致行为的变化,事务也是这种模式,每个事务对应一个新的 connection 对象 + + `commitTransactionAfterReturning(txInfo)`: + + * `txInfo.getTransactionManager().commit(txInfo.getTransactionStatus())`:通过平台事务管理器操作事务 + + * `processRollback(defStatus, false)`:回滚事务,和提交逻辑一样 + + * `processCommit(defStatus)`:提交事务,调用 doCommit(status) + + * `Connection con = txObject.getConnectionHolder().getConnection()`:获取当前线程的连接对象 + * `con.commit()`:事务提交,JDBC 原生的方式 + + @@ -10240,14 +10255,14 @@ Restful 是按照 Rest 风格访问网络资源 Restful 请求路径简化配置方式:`@RestController = @Controller + @ResponseBody` -相关注解: +相关注解:@GetMapping 注解是 @RequestMapping 注解的衍生,所以效果是一样的,建议使用 @GetMapping * `@GetMapping("/poll")` = `@RequestMapping(value = "/poll",method = RequestMethod.GET)` ```java - @RequestMapping(method = RequestMethod.GET) + @RequestMapping(method = RequestMethod.GET) // @GetMapping 就拥有了 @RequestMapping 的功能 public @interface GetMapping { - @AliasFor(annotation = RequestMapping.class) //与 RequestMapping 相通 + @AliasFor(annotation = RequestMapping.class) // 与 RequestMapping 相通 String name() default ""; } ``` @@ -10404,11 +10419,11 @@ org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(): ```java public class HiddenHttpMethodFilter extends OncePerRequestFilter { - //兼容的请求 PUT、DELETE、PATCH + // 兼容的请求 PUT、DELETE、PATCH private static final List ALLOWED_METHODS = Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(), HttpMethod.DELETE.name(), HttpMethod.PATCH.name())); - //隐藏域的名字 + // 隐藏域的名字 public static final String DEFAULT_METHOD_PARAM = "_method"; private String methodParam = DEFAULT_METHOD_PARAM; @@ -10417,21 +10432,21 @@ public class HiddenHttpMethodFilter extends OncePerRequestFilter { FilterChain filterChain) throws ServletException, IOException { HttpServletRequest requestToUse = request; - //请求必须是 POST, + // 请求必须是 POST, if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) { - //获取标签中 name="_method" 的 value 值 + // 获取标签中 name="_method" 的 value 值 String paramValue = request.getParameter(this.methodParam); if (StringUtils.hasLength(paramValue)) { - //转成大写 + // 转成大写 String method = paramValue.toUpperCase(Locale.ENGLISH); - //兼容的请求方式 + // 兼容的请求方式 if (ALLOWED_METHODS.contains(method)) { - //包装请求 + // 包装请求 requestToUse = new HttpMethodRequestWrapper(request, method); } } } - //过滤器链放行的时候用wrapper。以后的方法调用getMethod是调用requesWrapper的 + // 过滤器链放行的时候用wrapper。以后的方法调用getMethod是调用requesWrapper的 filterChain.doFilter(requestToUse, response); } } @@ -10599,7 +10614,7 @@ SpringMVC 提供访问原始 Servlet 接口的功能 * DispatcherServlet:核心控制器, 是 SpringMVC 的核心,整体流程控制的中心,所有的请求第一步都先到达这里,由其调用其它组件处理用户的请求,它就是在 web.xml 配置的核心 Servlet,有效的降低了组件间的耦合性 -* HandlerMapping:处理器映射器, 负责根据用户请求找到对应具体的 Handler 处理器,SpringMVC 中针对配置文件方式、注解方式等提供了不同的映射器来处理 +* HandlerMapping:处理器映射器, 负责根据请求找到对应具体的 Handler 处理器,SpringMVC 中针对配置文件方式、注解方式等提供了不同的映射器来处理 * Handler:处理器,其实就是 Controller,业务处理的核心类,通常由开发者编写,并且必须遵守 Controller 开发的规则,这样适配器才能正确的执行。例如实现 Controller 接口,将 Controller 注册到 IOC 容器中等 @@ -10607,7 +10622,7 @@ SpringMVC 提供访问原始 Servlet 接口的功能 * View Resolver:视图解析器, 将 Handler 中返回的逻辑视图(ModelAndView)解析为一个具体的视图(View)对象 -* View:视图, View 最后对页面进行渲染将结果返回给用户。SpringMvc 框架提供了很多的 View 视图类型,包括:jstlView、freemarkerView、pdfView 等 +* View:视图, View 最后对页面进行渲染将结果返回给用户。SpringMVC 框架提供了很多的 View 视图类型,包括:jstlView、freemarkerView、pdfView 等 ![](https://gitee.com/seazean/images/raw/master/Frame/SpingMVC-技术架构.png) @@ -10619,24 +10634,26 @@ SpringMVC 提供访问原始 Servlet 接口的功能 #### 工作原理 -在 Spring 容器初始化时会建立所有的 URL 和 Controller 的对应关系,保存到 Map 中,这样 request 就能快速根据 URL 定位到 Controller: +在 Spring 容器初始化时会建立所有的 URL 和 Controller 的对应关系,保存到 Map 中,这样 request 就能快速根据 URL 定位到 Controller: * 在 Spring IOC 容器初始化完所有单例 bean 后 * SpringMVC 会遍历所有的 bean,获取 controller 中对应的 URL(这里获取 URL 的实现类有多个,用于处理不同形式配置的 Controller) -* 将每一个 URL 对应一个 controller 存入 Map 中 +* 将每一个 URL 对应一个 controller 存入 Map 中 -注意:将 Controller 类的注解换成 @Component,启动时不会报错,但是在浏览器中输入路径时会出现 404,说明 Spring 没有对所有的 bean 进行 URL 映射 +注意:将 @Controller 注解换成 @Component,启动时不会报错,但是在浏览器中输入路径时会出现 404,说明 Spring 没有对所有的 bean 进行 URL 映射 **一个 Request 来了:** -* 监听端口,获得请求:Tomcat 监听 8080 端口的请求,进行接收、解析、封装,根据路径调用了 web.xml 中配置的核心控制器 DispatcherServlet -* 获取 Handler:进入 DispatcherServlet,核心控制器调用 HandlerMapping 去根据请求的 URL 获取对应的 Handler,如果获取的 Handler 为 null 则返回 404 -* 调用适配器执行 Handler: - * 适配器根据 request 的 URL 去 Handler 中寻找对应的处理方法(Controller 的 URL 与方法的 URL 拼接后对比) - * 获取到对应方法后,需要将 request 中的参数与方法参数上的数据进行绑定,根据反射获取方法的参数名和注解,再根据注解或者根据参数名对照进行绑定(找到对应的参数,然后在反射调用方法时传入) - * 绑定完参数后,反射调用方法获取 ModelAndView(如果 Handler 中返回的是 String、View 等对象,SpringMVC 也会将它们重新封装成一个 ModelAndView) -* 调用视图解析器解析:将 ModelAndView 解析成 View 对象 -* 渲染视图:将 View 对象中的返回地址、参数信息等放入 RequestDispatcher,最后进行转发 +* 监听端口,获得请求:Tomcat 监听 8080 端口的请求处理,根据路径调用了 web.xml 中配置的核心控制器 DispatcherServlet,`DispatcherServlet#doDispatch` 是**核心调度方法** +* **首先根据 URI 获取 HandlerMapping 处理器映射器**,RequestMappingHandlerMapping 用来处理 @RequestMapping 注解的映射规则,其中保存了所有 handler 的映射规则,最后包装成一个拦截器链返回,拦截器链对象持有 HandlerMapping。如果没有合适的处理请求的 HandlerMapping,说明请求处理失败,设置响应码 404 返回 +* 根据映射器获取当前 handler,**处理器适配器执行处理方法**,适配器根据请求的 URL 去 handler 中寻找对应的处理方法: + * 创建 ModelAndViewContainer (mav) 对象,用来填充数据,然后通过不同的**参数**解析器去解析 URL 中的参数,完成数据解析绑定,然后执行真正的 Controller 方法,完成 handle 处理 + * 方法执行完对**返回值**进行处理,没添加 @ResponseBody 注解的返回值使用视图处理器处理,把视图名称设置进入 mav 中 + * 对添加了 @ResponseBody 注解的 Controller 的按照普通的返回值进行处理,首先进行内容协商,找到一种浏览器可以接受(请求头 Accept)的并且服务器可以生成的数据类型,选择合适数据转换器,设置响应头中的数据类型,然后写出数据 + * 最后把 ModelAndViewContainer 和 ModelMap 中的数据**封装到 ModelAndView 对象**返回 +* **视图解析**,根据返回值创建视图,请求转发 View 实例为 InternalResourceView,重定向 View 实例为 RedirectView。最后调用 view.render 进行页面渲染,结果派发 + * 请求转发时请求域中的数据不丢失,会把 ModelAndView 的数据设置到请求域中,获取 Servlet 原生的 RequestDispatcher,调用 `RequestDispatcher#forward` 实现转发 + * 重定向会造成请求域中的数据丢失,使用 Servlet 原生方式实现重定向 `HttpServletResponse#sendRedirect` @@ -10651,32 +10668,35 @@ SpringMVC 提供访问原始 Servlet 接口的功能 ![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-请求相应的原理.png) ```java -//request 和 response 为 Java 原生的类 +// request 和 response 为 Java 原生的类 protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null; - boolean multipartRequestParsed = false; //文件上传请求 - WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);// 异步管理器 + // 文件上传请求 + boolean multipartRequestParsed = false; + // 异步管理器 + WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try { ModelAndView mv = null; Exception dispatchException = null; try { - processedRequest = checkMultipart(request); //文件上传请求 + // 文件上传相关请求 + processedRequest = checkMultipart(request); multipartRequestParsed = (processedRequest != request); - // 找到当前请求使用哪个 HandlerMapping(Controller的方法)处理,返回执行链 + // 找到当前请求使用哪个 HandlerMapping (Controller 的方法)处理,返回执行链 mappedHandler = getHandler(processedRequest); - // 没有合适的处理请求的方式 HandlerMapping 直接返回 + // 没有合适的处理请求的方式 HandlerMapping,请求失败,直接返回 404 if (mappedHandler == null) { noHandlerFound(processedRequest, response); return; } - // 根据映射器获取当前 handler 处理器适配器,用来处理当前的请求 + // 根据映射器获取当前 handler 处理器适配器,用来【处理当前的请求】 HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); - // 获取发出此次请求的方法 + // 获取发出此次请求的方式 String method = request.getMethod(); // 判断请求是不是 GET 方法 boolean isGet = HttpMethod.GET.matches(method); @@ -10704,7 +10724,7 @@ protected void doDispatch(HttpServletRequest request, HttpServletResponse respon dispatchException = ex; } - // 处理 程序调用的结果,进行结果派发 + // 处理程序调用的结果,进行结果派发 processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); } //.... @@ -10748,7 +10768,7 @@ public String postUser(){ //。。。。。 ``` -HandlerMapping 处理器映射器,保存了所有 `@RequestMapping` 和 `handler` 的映射规则 +HandlerMapping 处理器映射器,**保存了所有 `@RequestMapping` 和 `handler` 的映射规则** ```java protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { @@ -10774,7 +10794,7 @@ protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Ex * `String lookupPath = initLookupPath(request)`:地址栏的 uri,这里的 lookupPath 为 /user - * `this.mappingRegistry.acquireReadLock()`:防止并发 + * `this.mappingRegistry.acquireReadLock()`:加读锁防止并发 * `handlerMethod = lookupHandlerMethod(lookupPath, request)`:获取当前 HandlerMapping 中的映射规则 @@ -10790,18 +10810,20 @@ protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Ex * `match = getMatchingMapping(mapping, request)`:去匹配每一个映射规则,匹配失败返回 null * `matches.add(new Match())`:匹配成功后封装成匹配器添加到匹配集合中 + * `matches.sort(comparator)`:匹配集合排序 + * `Match bestMatch = matches.get(0)`:匹配完成只剩一个,直接获取返回对应的处理方法 * `if (matches.size() > 1)`:当有多个映射规则符合请求时,报错 - + * `return bestMatch.getHandlerMethod()`:返回匹配器中的处理方法 * `executionChain = getHandlerExecutionChain(handler, request)`:**为当前请求和映射器的构建一个拦截器链** - + * `for (HandlerInterceptor interceptor : this.adaptedInterceptors)`:遍历所有的拦截器 * `chain.addInterceptor(interceptor)`:把所有的拦截器添加到 HandlerExecutionChain 中,形成拦截器链 - * `return executionChain`:**返回拦截器链,包含 HandlerMapping 和拦截方法** + * `return executionChain`:**返回拦截器链,HandlerMapping 是链的成员属性** @@ -10811,7 +10833,7 @@ protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Ex #### 适配器 -doDispatch() 中 调用 `HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler())` +doDispatch() 中调用 `HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler())` ```java protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException { @@ -10819,10 +10841,9 @@ protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletExcepti // 遍历所有的 HandlerAdapter for (HandlerAdapter adapter : this.handlerAdapters) { // 判断当前适配器是否支持当前 handle - // return (handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler)) // 这里返回的是True, if (adapter.supports(handler)) { - // 返回的是 RequestMappingHandlerAdapter + // 返回的是 【RequestMappingHandlerAdapter】 return adapter; } } @@ -10846,8 +10867,8 @@ protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletExcepti ```java @GetMapping("/params") public String param(Map map, Model model, HttpServletRequest request) { - map.put("k1", "v1"); //都可以向请求域中添加数据 - model.addAttribute("k2", "v2"); //它们两个都在数据封装在 BindingAwareModelMap + map.put("k1", "v1"); // 都可以向请求域中添加数据 + model.addAttribute("k2", "v2"); // 它们两个都在数据封装在 【BindingAwareModelMap】,继承自 LinkedHashMap request.setAttribute("m", "HelloWorld"); return "forward:/success"; } @@ -10855,19 +10876,18 @@ public String param(Map map, Model model, HttpServletRequest req ![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-Model和Map的数据解析.png) -doDispatch() 中调用 `mv = ha.handle(processedRequest, response, mappedHandler.getHandler())` 执行目标方法 +doDispatch() 中调用 `mv = ha.handle(processedRequest, response, mappedHandler.getHandler())` **使用适配器执行方法** `AbstractHandlerMethodAdapter#handle` → `RequestMappingHandlerAdapter#handleInternal` → `invokeHandlerMethod`: ```java -//使用适配器执行方法 protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { - //封装成 SpringMVC 的接口,用于通用 Web 请求拦截器,使能够访问通用请求元数据,而不是用于实际处理请求 + // 封装成 SpringMVC 的接口,用于通用 Web 请求拦截器,使能够访问通用请求元数据,而不是用于实际处理请求 ServletWebRequest webRequest = new ServletWebRequest(request, response); try { - // WebDataBinder 用于从 Web 请求参数到 JavaBean 对象的数据绑定,获取创建该实例的工厂 + // WebDataBinder 用于【从 Web 请求参数到 JavaBean 对象的数据绑定】,获取创建该实例的工厂 WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod); // 创建 Model 实例,用于向模型添加属性 ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory); @@ -10930,13 +10950,12 @@ ServletInvocableHandlerMethod#invokeAndHandle:执行目标方法 `if (resolver.supportsParameter(parameter))`:是否支持当前参数 * `PathVariableMethodArgumentResolver#supportsParameter`:**解析标注 @PathVariable 注解的参数** - * `ModelMethodProcessor#supportsParameter`:解析 Map 类型的参数 - * `ModelMethodProcessor#supportsParameter`:解析 Model 类型的参数,Model 和 Map 的作用一样 + * `ModelMethodProcessor#supportsParameter`:解析 Map 和 Model 类型的参数,Model 和 Map 的作用一样 * `ExpressionValueMethodArgumentResolver#supportsParameter`:解析标注 @Value 注解的参数 * `RequestParamMapMethodArgumentResolver#supportsParameter`:**解析标注 @RequestParam 注解** * `RequestPartMethodArgumentResolver#supportsParameter`:解析文件上传的信息 * `ModelAttributeMethodProcessor#supportsParameter`:解析标注 @ModelAttribute 注解或者不是简单类型 - * 子类 ServletModelAttributeMethodProcessor 是**解析自定义类型 JavaBean 的解析器** + * 子类 ServletModelAttributeMethodProcessor 是解析自定义类型 JavaBean 的解析器 * 简单类型有 Void、Enum、Number、CharSequence、Date、URI、URL、Locale、Class * `args[i] = this.resolvers.resolveArgument()`:**开始解析参数,每个参数使用的解析器不同** @@ -10949,11 +10968,11 @@ ServletInvocableHandlerMethod#invokeAndHandle:执行目标方法 * `MapMethodProcessor#resolveArgument`:调用 `mavContainer.getModel()` 返回默认 BindingAwareModelMap 对象 * `ModelAttributeMethodProcessor#resolveArgument`:**自定义的 JavaBean 的绑定封装**,下一小节详解 - `return doInvoke(args)`:真正的执行方法 + `return doInvoke(args)`:**真正的执行 Controller 方法** * `Method method = getBridgedMethod()`:从 HandlerMethod 获取要反射执行的方法 * `ReflectionUtils.makeAccessible(method)`:破解权限 - * `method.invoke(getBean(), args)`:**执行方法**,getBean 获取的是标记 @Controller 的 Bean 类,其中包含执行方法 + * `method.invoke(getBean(), args)`:执行方法,getBean 获取的是标记 @Controller 的 Bean 类,其中包含执行方法 * **进行返回值的处理,响应部分详解**,处理完成进入下面的逻辑 @@ -10962,16 +10981,15 @@ ServletInvocableHandlerMethod#invokeAndHandle:执行目标方法 RequestMappingHandlerAdapter#getModelAndView:获取 ModelAndView 对象 * `modelFactory.updateModel(webRequest, mavContainer)`:Model 数据升级到会话域(**请求域中的数据在重定向时丢失**) - - `updateBindingResult(request, defaultModel)`:把绑定的数据添加到 Model 中 - + * `updateBindingResult(request, defaultModel)`:把绑定的数据添加到 BindingAwareModelMap 中 + * `if (mavContainer.isRequestHandled())`:判断请求是否已经处理完成了 -* `ModelMap model = mavContainer.getModel()`:获取**包含 Controller 方法参数**的 BindingAwareModelMap 对象(本节开头) +* `ModelMap model = mavContainer.getModel()`:获取**包含 Controller 方法参数**的 BindingAwareModelMap(本节开头) * `mav = new ModelAndView()`:**把 ModelAndViewContainer 和 ModelMap 中的数据封装到 ModelAndView** -* `if (!mavContainer.isViewReference())`:视图是否是通过名称指定视图引用 +* `if (!mavContainer.isViewReference())`:是否是通过名称指定视图引用 * `if (model instanceof RedirectAttributes)`:判断 model 是否是重定向数据,如果是进行重定向逻辑 @@ -10985,7 +11003,7 @@ RequestMappingHandlerAdapter#getModelAndView:获取 ModelAndView 对象 ##### 参数解析 -解析自定义的 JavaBean 为例 +解析自定义的 JavaBean 为例,调用 ModelAttributeMethodProcessor#resolveArgument 处理参数的方法,通过合适的类型转换器把 URL 中的参数转换以后,利用反射获取 set 方法,注入到 JavaBean * Person.java: @@ -11026,13 +11044,13 @@ RequestMappingHandlerAdapter#getModelAndView:获取 ModelAndView 对象 * `binder = binderFactory.createBinder(webRequest, attribute, name)`:Web 数据绑定器,可以利用 Converters 将请求数据转成指定的数据类型,绑定到 JavaBean 中 -* `bindRequestParameters(binder, webRequest)`:利用反射向目标对象填充数据 +* `bindRequestParameters(binder, webRequest)`:**利用反射向目标对象填充数据** `servletBinder = (ServletRequestDataBinder) binder`:类型强转 `servletBinder.bind(servletRequest)`:绑定数据 - * `mpvs = new MutablePropertyValues(request.getParameterMap())`:获取请求 URI 参数中的 KV 键值对 + * `mpvs = new MutablePropertyValues(request.getParameterMap())`:获取请求 URI 参数中的 k-v 键值对 * `addBindValues(mpvs, request)`:子类可以用来为请求添加额外绑定值 @@ -11066,7 +11084,7 @@ RequestMappingHandlerAdapter#getModelAndView:获取 ModelAndView 对象 * `if (conversionService.canConvert(sourceTypeDesc, typeDescriptor))`:判断能不能转换 - `GenericConverter converter = getConverter(sourceType, targetType)`:获取**类型转换器** + `GenericConverter converter = getConverter(sourceType, targetType)`:**获取类型转换器** * `converter = this.converters.find(sourceType, targetType)`:寻找合适的转换器 @@ -11122,7 +11140,7 @@ RequestMappingHandlerAdapter#getModelAndView:获取 ModelAndView 对象 以 Person 为例: ```java -@ResponseBody //利用返回值处理器里面的消息转换器进行处理 +@ResponseBody // 利用返回值处理器里面的消息转换器进行处理,而不是视图 @GetMapping(value = "/person") public Person getPerson(){ Person person = new Person(); @@ -11216,84 +11234,82 @@ RequestResponseBodyMethodProcessor#handleReturnValue:处理返回值,要进 * `if (contentType != null && contentType.isConcrete())`:判断当前响应头中是否已经有确定的媒体类型 - `selectedMediaType = contentType`:说明前置处理已经使用了媒体类型,直接继续使用该类型 + `selectedMediaType = contentType`:前置处理已经使用了媒体类型,直接继续使用该类型 * `acceptableTypes = getAcceptableMediaTypes(request)`:**获取浏览器支持的媒体类型,请求头字段** - `this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request))`:调用该方法 - + * `this.contentNegotiationManager.resolveMediaTypes()`:调用该方法 * `for(ContentNegotiationStrategy strategy:this.strategies)`:**默认策略是提取请求头的字段的内容**,策略类为HeaderContentNegotiationStrategy,可以配置添加其他类型的策略 - * `List mediaTypes = strategy.resolveMediaTypes(request)`:解析 Accept 字段存储为 List - * `headerValueArray = request.getHeaderValues(HttpHeaders.ACCEPT)`:获取请求头中 Accept 字段 - * `List mediaTypes = MediaType.parseMediaTypes(headerValues)`:解析成 List 集合 - * `MediaType.sortBySpecificityAndQuality(mediaTypes)`:按照相对品质因数 q 降序排序 - - ![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-浏览器支持接收的数据类型.png) - - * `producibleTypes = getProducibleMediaTypes(request, valueType, targetType)`:**服务器能生成的媒体类型** - - * `request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE)`:从请求域获取默认的媒体类型 + * `List mediaTypes = strategy.resolveMediaTypes(request)`:解析 Accept 字段存储为 List + * `headerValueArray = request.getHeaderValues(HttpHeaders.ACCEPT)`:获取请求头中 Accept 字段 + * `List mediaTypes = MediaType.parseMediaTypes(headerValues)`:解析成 List 集合 + * `MediaType.sortBySpecificityAndQuality(mediaTypes)`:按照相对品质因数 q 降序排序 + + + ![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-浏览器支持接收的数据类型.png) + +* `producibleTypes = getProducibleMediaTypes(request, valueType, targetType)`:**服务器能生成的媒体类型** + + * `request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE)`:从请求域获取默认的媒体类型 * ` for (HttpMessageConverter converter : this.messageConverters)`:遍历所有的消息转换器 * `converter.canWrite(valueClass, null)`:是否支持当前的类型 * ` result.addAll(converter.getSupportedMediaTypes())`:把当前 MessageConverter 支持的所有类型放入 result - - * `List mediaTypesToUse = new ArrayList<>()`:存储最佳匹配 - - * **内容协商:** - - ```java - for (MediaType requestedType : acceptableTypes) { // 遍历所有的浏览器能接受的媒体类型 - for (MediaType producibleType : producibleTypes) { // 遍历所有服务器能产出的 + +* `List mediaTypesToUse = new ArrayList<>()`:存储最佳匹配的集合 + +* **内容协商:** + + ```java + for (MediaType requestedType : acceptableTypes) { // 遍历所有浏览器能接受的媒体类型 + for (MediaType producibleType : producibleTypes) { // 遍历所有服务器能产出的 if (requestedType.isCompatibleWith(producibleType)) { // 判断类型是否匹配,最佳匹配 // 数据协商匹配成功,一般有多种 mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType)); } } } - ``` - - * `MediaType.sortBySpecificityAndQuality(mediaTypesToUse)`:按照相对品质因数 q 排序,降序排序,越大的越好 - - * `for (MediaType mediaType : mediaTypesToUse)`:**遍历所有的最佳匹配** - - `selectedMediaType = mediaType`:选择一种赋值给选择的类型 - - * `selectedMediaType = selectedMediaType.removeQualityValue()`:媒体类型去除相对品质因数 - - * `for (HttpMessageConverter converter : this.messageConverters)`:遍历所有的 HTTP 数据转换器 - - * `GenericHttpMessageConverter genericConverter`:**MappingJackson2HttpMessageConverter 可以将对象写为 JSON** - - * `((GenericHttpMessageConverter) converter).canWrite()`:判断转换器是否可以写出给定的类型 - - `AbstractJackson2HttpMessageConverter#canWrit` - - * `if (!canWrite(mediaType))`:是否可以写出指定类型 + ``` + +* `MediaType.sortBySpecificityAndQuality(mediaTypesToUse)`:按照相对品质因数 q 排序,降序排序,越大的越好 + +* `for (MediaType mediaType : mediaTypesToUse)`:**遍历所有的最佳匹配**,选择一种赋值给选择的类型 + +* `selectedMediaType = selectedMediaType.removeQualityValue()`:媒体类型去除相对品质因数 + +* `for (HttpMessageConverter converter : this.messageConverters)`:遍历所有的 HTTP 数据转换器 + +* `GenericHttpMessageConverter genericConverter`:**MappingJackson2HttpMessageConverter 可以将对象写为 JSON** + +* `((GenericHttpMessageConverter) converter).canWrite()`:判断转换器是否可以写出给定的类型 + + `AbstractJackson2HttpMessageConverter#canWrit` + + * `if (!canWrite(mediaType))`:是否可以写出指定类型 * `MediaType.ALL.equalsTypeAndSubtype(mediaType)`:是不是 `*/*` 类型 - * `getSupportedMediaTypes()`:支持 `application/json` 和 `application/*+json` 两种类型 + * `getSupportedMediaTypes()`:支持 `application/json` 和 `application/*+json` 两种类型 * `return true`:返回 true * `objectMapper = selectObjectMapper(clazz, mediaType)`:选择可以使用的 objectMapper * `causeRef = new AtomicReference<>()`:获取并发安全的引用 * `if (objectMapper.canSerialize(clazz, causeRef))`:objectMapper 可以序列化当前类 * `return true`:返回 true - - * ` body = getAdvice().beforeBodyWrite()`:**要响应的所有数据,Person 对象** - - * `addContentDispositionHeader(inputMessage, outputMessage)`:检查路径 - - * `genericConverter.write(body, targetType, selectedMediaType, outputMessage)`:调用消息转换器的 write 方法 - - `AbstractGenericHttpMessageConverter#write`:该类的方法 - - * `addDefaultHeaders(headers, t, contentType)`:**设置响应头中的数据类型** - - ![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-服务器设置数据类型.png) - - * `writeInternal(t, type, outputMessage)`:**数据写出为 JSON 格式** - - * `Object value = object`:value 引用 Person 对象 + + * ` body = getAdvice().beforeBodyWrite()`:**获取要响应的所有数据,就是 Person 对象** + +* `addContentDispositionHeader(inputMessage, outputMessage)`:检查路径 + +* `genericConverter.write(body, targetType, selectedMediaType, outputMessage)`:调用消息转换器的 write 方法 + + `AbstractGenericHttpMessageConverter#write`:该类的方法 + + * `addDefaultHeaders(headers, t, contentType)`:**设置响应头中的数据类型** + + ![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-服务器设置数据类型.png) + + * `writeInternal(t, type, outputMessage)`:**数据写出为 JSON 格式** + + * `Object value = object`:value 引用 Person 对象 * `ObjectWriter objectWriter = objectMapper.writer()`:获取 ObjectWriter 对象 - * `objectWriter.writeValue(generator, value)`:使用 ObjectWriter 写出数据为 JSON + * `objectWriter.writeValue(generator, value)`:使用 ObjectWriter 写出数据为 JSON @@ -11308,7 +11324,7 @@ RequestResponseBodyMethodProcessor#handleReturnValue:处理返回值,要进 开启基于请求参数的内容协商模式:(SpringBoot 方式) ```yaml -spring.mvc.contentnegotiation:favor-parameter: true #开启请求参数内容协商模式 +spring.mvc.contentnegotiation:favor-parameter: true # 开启请求参数内容协商模式 ``` 发请求: http://localhost:8080/person?format=json,解析 format @@ -11342,17 +11358,17 @@ public class WebConfig implements WebMvcConfigurer { mediaTypes.put("json", MediaType.APPLICATION_JSON); mediaTypes.put("xml",MediaType.APPLICATION_XML); mediaTypes.put("person",MediaType.parseMediaType("application/x-person")); - //指定支持解析哪些参数对应的哪些媒体类型 + // 指定支持解析哪些参数对应的哪些媒体类型 ParameterContentNegotiationStrategy parameterStrategy = new ParameterContentNegotiationStrategy(mediaTypes); - //请求头解析 + // 请求头解析 HeaderContentNegotiationStrategy headStrategy = new HeaderContentNegotiationStrategy(); - //添加到容器中,即可以解析请求头 又可以解析请求参数 + // 添加到容器中,即可以解析请求头 又可以解析请求参数 configurer.strategies(Arrays.asList(parameterStrategy,headStrategy)); } - @Override //自定义消息转换器 + @Override // 自定义消息转换器 public void extendMessageConverters(List> converters) { converters.add(new GuiguMessageConverter()); } @@ -11388,12 +11404,12 @@ public String param(){ ```java public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) { - //获取合适的返回值处理器:调用 if (handler.supportsReturnType(returnType))判断是否支持 + // 获取合适的返回值处理器:调用 if (handler.supportsReturnType(returnType))判断是否支持 HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType); if (handler == null) { throw new IllegalArgumentException(); } - //使用处理器处理返回值 + // 使用处理器处理返回值 handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest); } ``` @@ -11403,7 +11419,7 @@ public void handleReturnValue(@Nullable Object returnValue, MethodParameter retu ```java public boolean supportsReturnType(MethodParameter returnType) { Class paramType = returnType.getParameterType(); - // 返回值是否是void 或者 是 CharSequence 字符序列 + // 返回值是否是 void 或者是 CharSequence 字符序列,这里是字符序列 return (void.class == paramType || CharSequence.class.isAssignableFrom(paramType)); } ``` @@ -11417,7 +11433,7 @@ public void handleReturnValue(@Nullable Object returnValue, MethodParameter retu // 返回值是字符串,是 return "forward:/success" if (returnValue instanceof CharSequence) { String viewName = returnValue.toString(); - // 把视图名称设置进入 ModelAndViewContainer 中 + // 【把视图名称设置进入 ModelAndViewContainer 中】 mavContainer.setViewName(viewName); // 判断是否是重定向数据 `viewName.startsWith("redirect:")` if (isRedirectViewName(viewName)) { @@ -11468,7 +11484,7 @@ DispatcherServlet#render: * `Locale locale = this.localeResolver.resolveLocale(request)`:国际化相关 -* `String viewName = mv.getViewName()`:视图名字,是请求转发 forward:/success(响应数据部分解析了该名字存入 ModelAndView 是**通过 ViewNameMethodReturnValueHandler**) +* `String viewName = mv.getViewName()`:视图名字,是请求转发 forward:/success(响应数据解析并存入 ModelAndView) * `view = resolveViewName(viewName, mv.getModelInternal(), locale, request)`:解析视图 @@ -11490,7 +11506,7 @@ DispatcherServlet#render: * `returnview = createView(viewName, locale)`:UrlBasedViewResolver#createView - **请求转发**:实例为 InternalResourceView + **请求转发**:实例为 InternalResourceView * `if (viewName.startsWith(FORWARD_URL_PREFIX))`:视图名字是否是 **`forward:`** 的前缀 * `forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length())`:名字截取前缀 @@ -11532,8 +11548,7 @@ DispatcherServlet#render: * `enc = request.getCharacterEncoding()`:设置编码 UTF-8 * `appendQueryProperties(targetUrl, model, enc)`:添加一些属性,比如 `url + ?name=123&&age=324` * `sendRedirect(request, response, targetUrl, this.http10Compatible)`:重定向 - - * `response.sendRedirect(encodedURL)`:**使用 Servlet 原生方法实现重定向** +* `response.sendRedirect(encodedURL)`:**使用 Servlet 原生方法实现重定向** @@ -11552,8 +11567,11 @@ DispatcherServlet#render: ### 请求参数 名称:@RequestBody + 类型:形参注解 + 位置:处理器类中的方法形参前方 + 作用:将异步提交数据**转换**成标准请求参数格式,并赋值给形参 范例: @@ -11568,7 +11586,7 @@ public class AjaxController { } ``` -* 注解添加到 Pojo 参数前方时,封装的异步提交数据按照 Pojo 的属性格式进行关系映射 +* 注解添加到 POJO 参数前方时,封装的异步提交数据按照 POJO 的属性格式进行关系映射 * POJO 中的属性如果请求数据中没有,属性值为 null * POJO 中没有的属性如果请求数据中有,不进行映射 * 注解添加到集合参数前方时,封装的异步提交数据按照集合的存储结构进行关系映射 @@ -11660,6 +11678,7 @@ spring-mvc.xml: ### 响应数据 注解:@ResponseBody + 作用:将 java 对象转为 json 格式的数据 方法返回值为 POJO 时,自动封装数据成 Json 对象数据: @@ -12224,7 +12243,7 @@ public class UserController { 使用注解实现异常分类管理,开发异常处理器 -ControllerAdvice 注解: +@ControllerAdvice 注解: * 类型:类注解 @@ -12242,7 +12261,7 @@ ControllerAdvice 注解: } ``` -ExceptionHandler 注解: +@ExceptionHandler 注解: * 类型:方法注解 @@ -14396,7 +14415,7 @@ SpringApplication 构造方法: * `this.mainApplicationClass = deduceMainApplicationClass()`:获取出 main 程序类 -SpringApplication#run(String... args): +SpringApplication#run(String... args):创建 IOC 容器并实现了自动装配 * `StopWatch stopWatch = new StopWatch()`:停止监听器,**监控整个应用的启停** * `stopWatch.start()`:记录应用的启动时间 @@ -14425,7 +14444,7 @@ SpringApplication#run(String... args): * `ConfigurationPropertySources.attach(environment)`:属性值绑定环境信息 * `sources.addFirst(ATTACHED_PROPERTY_SOURCE_NAME,..)`:把 configurationProperties 放入环境的属性信息头部 - * `listeners.environmentPrepared(bootstrapContext, environment)`:**运行监听器调用 environmentPrepared()**,EventPublishingRunListener 发布事件通知所有的监听器当前环境准备完成 + * `listeners.environmentPrepared(bootstrapContext, environment)`:运行监听器调用 environmentPrepared(),EventPublishingRunListener 发布事件通知所有的监听器当前环境准备完成 * `DefaultPropertiesPropertySource.moveToEnd(environment)`:移动 defaultProperties 属性源到环境中的最后一个源 @@ -14457,7 +14476,7 @@ SpringApplication#run(String... args): * `applyInitializers(context)`:获取所有的**初始化器调用 initialize() 方法**进行初始化 * `listeners.contextPrepared(context)`:所有的运行监听器调用 environmentPrepared() 方法,EventPublishingRunListener 发布事件通知 IOC 容器准备完成 - * `listeners.contextLoaded(context)`:所有的**运行监听器调用 contextLoaded() 方法**,通知 IOC 加载完成 + * `listeners.contextLoaded(context)`:所有的运行监听器调用 contextLoaded() 方法,通知 IOC 加载完成 * `refreshContext(context)`:**刷新 IOC 容器** @@ -14496,7 +14515,7 @@ SpringApplication#run(String... args): SpringBoot 定义了一套接口规范,这套规范规定 SpringBoot 在启动时会扫描外部引用 jar 包中的 `META-INF/spring.factories` 文件,将文件中配置的类型信息加载到 Spring 容器,并执行类中定义的各种操作,对于外部的 jar 包,直接引入一个 starter 即可 -@SpringBootApplication 注解是 `@Configuration`、`@EnableAutoConfiguration`、`@ComponentScan` 注解的集合 +@SpringBootApplication 注解是 `@SpringBootConfiguration`、`@EnableAutoConfiguration`、`@ComponentScan` 注解的集合 * @SpringBootApplication 注解 @@ -14523,7 +14542,7 @@ SpringBoot 定义了一套接口规范,这套规范规定 SpringBoot 在启动 @AliasFor 注解:表示别名,可以注解到自定义注解的两个属性上表示这两个互为别名,两个属性其实是同一个含义相互替代 -* @ComponentScan 注解:默认扫描当前包及其子级包下的所有文件 +* @ComponentScan 注解:默认扫描当前类所在包及其子级包下的所有文件 * **@EnableAutoConfiguration 注解:启用 SpringBoot 的自动配置机制** @@ -14597,7 +14616,7 @@ SpringBoot 定义了一套接口规范,这套规范规定 SpringBoot 在启动 * `return configurations`:返回所有自动装配类的候选项 - * 从 spring-boot-autoconfigure-2.5.3.jar/META-INF/spring.factories 文件中获取自动装配类,**进行条件装配,按需装配** + * 从 spring-boot-autoconfigure-2.5.3.jar/META-INF/spring.factories 文件中寻找 EnableAutoConfiguration 字段,获取自动装配类,**进行条件装配,按需装配** ![](https://gitee.com/seazean/images/raw/master/Frame/SpringBoot-自动装配配置文件.png) @@ -15740,7 +15759,7 @@ spring: * 编写 dao 和 mapper 文件/纯注解开发 - dao:**@Mapper 注解一定要加,否则在启动类指定 @MapperScan() 扫描路径(不建议)** + dao:**@Mapper 注解必须加,使用自动装配的 package,否则在启动类指定 @MapperScan() 扫描路径(不建议)** ```java @Mapper //必须加Mapper From 17f387ba869520857fa3a31454430ff8bda3fd0e Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 1 Sep 2021 22:47:00 +0800 Subject: [PATCH 044/168] Update Java Notes --- DB.md | 8 +- Java.md | 248 ++++---- Prog.md | 1783 ++++++++++++++++++++++++++++--------------------------- 3 files changed, 1049 insertions(+), 990 deletions(-) diff --git a/DB.md b/DB.md index 22e143a..f576b26 100644 --- a/DB.md +++ b/DB.md @@ -2585,12 +2585,12 @@ InnoDB 引擎会在适当的时候,把内存中 redo log buffer 持久化到 * 0:表示当提交事务时,并不将缓冲区的 redo 日志写入磁盘,而是等待主线程每秒刷新一次 * 1:在事务提交时将缓冲区的 redo 日志**同步写入**到磁盘,保证一定会写入成功 * 2:在事务提交时将缓冲区的 redo 日志异步写入到磁盘,不能保证提交时肯定会写入,只是有这个动作 -* 如果写入 redo log buffer 的日志已经占据了 redo log buffer 总容量的一半了,此时就会刷入到磁盘文件 +* 如果写入 redo log buffer 的日志已经占据了 redo log buffer 总容量的一半了,此时就会刷入到磁盘文件,这时会影响执行效率,所以开发中应该**避免大事务** 刷脏策略: * redo log 文件是固定大小的,如果写满了就要擦除以前的记录,在擦除之前需要把旧记录更新到磁盘中的数据文件中 -* 系统内存不足,需要淘汰部分数据页,如果淘汰的是脏页,就要先将脏页写到磁盘 +* 系统内存不足,需要淘汰部分数据页,如果淘汰的是脏页,就要先将脏页写到磁盘(大事务) * 系统空闲时,后台线程会自动进行刷脏 * MySQL 正常关闭时,会把内存的脏页都 flush 到磁盘上 @@ -4756,7 +4756,7 @@ Innodb_xxxx:这几个参数只是针对InnoDB 存储引擎的,累加的算 SHOW VARIABLES LIKE '%query%' ``` -* SHOW PROCESSLIST:查看当前 MySQL 在进行的连接线程,包括线程的状态、是否锁表等,可以实时地查看 SQL 的执行情况,同时对一些锁表操作进行优化 +* SHOW PROCESSLIST:**实时查看**当前 MySQL 在进行的连接线程,包括线程的状态、是否锁表、SQL 的执行情况,同时对一些锁表操作进行优化 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SHOW PROCESSLIST命令.png) @@ -4948,7 +4948,7 @@ key_len: #### PROFILES -SHOW PROFILES 能够在做 SQL 优化时分析当前会话中语句执行的资源消耗情况 +SHOW PROFILES 能够在做 SQL 优化时分析当前会话中语句执行的**资源消耗**情况 * 通过 have_profiling 参数,能够看到当前 MySQL 是否支持 profile: ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-have_profiling.png) diff --git a/Java.md b/Java.md index 571792b..e936cb3 100644 --- a/Java.md +++ b/Java.md @@ -820,7 +820,7 @@ public class MethodDemo { #### 参数传递 -**Java 的参数是以值传递的形式传入方法中** +Java 的参数是以**值传递**的形式传入方法中 值传递和引用传递的区别在于传递后会不会影响实参的值:值传递会创建副本,引用传递不会创建副本 @@ -2296,7 +2296,7 @@ public boolean equals(Object o) { **面试题**:== 和 equals 的区别 * == 比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的**地址**是否相同,即是否是指相同一个对象,比较的是真正意义上的指针操作 -* 重写 equals 方法比较的是两个对象的**内容**是否相等,所有的类都是继承自 java.lang.Object 类,所以适用于所有对象,如果**没有对该方法进行覆盖的话,调用的仍然是 Object 类中的方法,比较两个对象的引用** +* Object 类中的方法,默认比较两个对象的引用,重写 equals 方法比较的是两个对象的**内容**是否相等,所有的类都是继承自 java.lang.Object 类,所以适用于所有对象 hashCode 的作用: @@ -2314,7 +2314,7 @@ hashCode 的作用: 深浅拷贝(克隆)的概念: -* 浅拷贝 (shallowCopy):对基本数据类型进行值传递,对引用数据类型只是复制了引用,被复制对象属性的所有的引用仍然指向原来的对象,简而言之就是增加了一个指针指向原来对象的内存地址 +* 浅拷贝 (shallowCopy):**对基本数据类型进行值传递,对引用数据类型只是复制了引用**,被复制对象属性的所有的引用仍然指向原来的对象,简而言之就是增加了一个指针指向原来对象的内存地址 Java 中的复制方法基本都是浅克隆:Object.clone()、System.arraycopy()、Arrays.copyOf() @@ -2478,24 +2478,24 @@ s.replace("-","");//12378 * `public String(char[] chs)` : 根据字符数组的内容,来创建字符串对象 * `public String(String original)` : 根据传入的字符串内容,来创建字符串对象 -直接赋值:`String s = “abc”` 直接赋值的方式创建字符串对象,内容就是 abc +直接赋值:`String s = "abc"` 直接赋值的方式创建字符串对象,内容就是 abc - 通过构造方法创建:通过 new 创建的字符串对象,每一次 new 都会申请一个内存空间,虽然内容相同,但是地址值不同,**返回堆内存中对象的引用** - 直接赋值方式创建:以“ ”方式给出的字符串,只要字符序列相同(顺序和大小写),无论在程序代码中出现几次,JVM 都只会**在 String Pool 中创建一个字符串对象**,并在字符串池中维护 -`String str = new String("abc")`创建字符串对象: +`String str = new String("abc")` 创建字符串对象: -* 创建一个对象:字符串池中已经存在"abc"对象,那么直接在创建一个对象放入堆中,返回堆内引用 -* 创建两个对象:字符串池中未找到"abc"对象,那么分别在堆中和字符串池中创建一个对象,字符串池中的比较都是采用 equals() +* 创建一个对象:字符串池中已经存在 abc 对象,那么直接在创建一个对象放入堆中,返回堆内引用 +* 创建两个对象:字符串池中未找到 abc 对象,那么分别在堆中和字符串池中创建一个对象,字符串池中的比较都是采用 equals() -`new String("a") + new String("b")`创建字符串对象: +`new String("a") + new String("b")` 创建字符串对象: -* 对象1:new StringBuilder() +* 对象 1:new StringBuilder() -* 对象2:new String("a")、对象3:常量池中的"a" +* 对象 2:new String("a")、对象 3:常量池中的 a -* 对象4:new String("b")、对象5:常量池中的"b" +* 对象 4:new String("b")、对象 5:常量池中的 b * StringBuilder 的 toString(): @@ -2507,8 +2507,8 @@ s.replace("-","");//12378 } ``` - * 对象6:new String("ab") - * StringBuilder 的 toString() 调用,在字符串常量池中没有生成"ab",new String("ab") 会创建两个对象因为传参数的时候使用字面量创建了对象 “ab ”,当使用数组构造 String 对象时,没有加入常量池的操作 + * 对象 6:new String("ab") + * StringBuilder 的 toString() 调用,**在字符串常量池中没有生成 ab**,new String("ab") 会创建两个对象因为传参数的时候使用字面量创建了对象 ab,当使用数组构造 String 对象时,没有加入常量池的操作 @@ -2549,18 +2549,20 @@ public class Demo { // ldc #3 会把 b 符号变为 "b" 字符串对象 // ldc #4 会把 ab 符号变为 "ab" 字符串对象 public static void main(String[] args) { - String s1 = "a"; // 懒惰的 + String s1 = "a"; // 懒惰的 String s2 = "b"; - String s3 = "ab";//串池 - // new StringBuilder().append("a").append("b").toString() new String("ab") + String s3 = "ab"; // 串池 + String s4 = s1 + s2; // 返回的是堆内地址 + // 原理:new StringBuilder().append("a").append("b").toString() new String("ab") + String s5 = "a" + "b"; // javac 在编译期间的优化,结果已经在编译期确定为ab System.out.println(s3 == s4); // false System.out.println(s3 == s5); // true String x2 = new String("c") + new String("d"); // new String("cd") - // 虽然new,但是在字符串常量池没有 cd 对象,toString()方法 + // 虽然 new,但是在字符串常量池没有 cd 对象,toString() 方法 x2.intern(); String x1 = "cd"; @@ -2575,9 +2577,9 @@ public class Demo { 结论: ```java -String s1 = "ab"; //放入串池 -String s2 = new String("a") + new String("b"); //放入堆 -//上面两条指令的结果和下面的 效果 相同 +String s1 = "ab"; // 仅放入串池 +String s2 = new String("a") + new String("b"); // 仅放入堆 +// 上面两条指令的结果和下面的 效果 相同 String s = new String("ab"); ``` @@ -4650,14 +4652,13 @@ JDK7 对比 JDK8: 底层数据结构: -* 哈希表(Hash table,也叫散列表),根据关键码值(Key value)而直接访问的数据结构。通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度,这个映射函数叫做散列函数,存放记录的数组叫做散列表 +* 哈希表(Hash table,也叫散列表),根据关键码值而直接访问的数据结构。通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度,这个映射函数叫做散列函数,存放记录的数组叫做散列表 * JDK1.8 之前 HashMap 由 数组+链表 组成 * 数组是 HashMap 的主体 - * 链表则是为了解决哈希冲突而存在的(**拉链法解决冲突**),拉链法就是头插法 - 两个对象调用的 hashCode 方法计算的哈希码值(键的哈希)一致导致计算的数组索引值相同 - + * 链表则是为了解决哈希冲突而存在的(**拉链法解决冲突**),拉链法就是头插法,两个对象调用的 hashCode 方法计算的哈希码值(键的哈希)一致导致计算的数组索引值相同 + * JDK1.8 以后 HashMap 由 **数组+链表 +红黑树**数据结构组成 * 解决哈希冲突时有了较大的变化 @@ -4707,14 +4708,14 @@ HashMap继承关系如下图所示: 2. 集合的初始化容量(**必须是二的 n 次幂** ) ```java - //默认的初始容量是16 -- 1<<4相当于1*2的4次方---1*16 + // 默认的初始容量是16 -- 1<<4相当于1*2的4次方---1*16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; ``` HashMap 构造方法指定集合的初始化容量大小: ```java - HashMap(int initialCapacity)//构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap + HashMap(int initialCapacity)// 构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap ``` * 为什么必须是 2 的 n 次幂? @@ -4741,7 +4742,7 @@ HashMap继承关系如下图所示: 4. 集合最大容量 ```java - //集合最大容量的上限是:2的30次幂 + // 集合最大容量的上限是:2的30次幂 static final int MAXIMUM_CAPACITY = 1 << 30;// 0100 0000 0000 0000 0000 0000 0000 0000 = 2 ^ 30 ``` @@ -4753,8 +4754,8 @@ HashMap继承关系如下图所示: 5. 当链表的值超过 8 则会转红黑树(JDK1.8 新增) ```java - //当桶(bucket)上的结点数大于这个值时会转成红黑树 - static final int TREEIFY_THRESHOLD = 8; + // 当桶(bucket)上的结点数大于这个值时会转成红黑树 + static final int TREEIFY_THRESHOLD = 8; ``` 为什么 Map 桶中节点个数大于 8 才转为红黑树? @@ -4782,14 +4783,14 @@ HashMap继承关系如下图所示: 6. 当链表的值小 于 6 则会从红黑树转回链表 ```java - //当桶(bucket)上的结点数小于这个值时树转链表 + // 当桶(bucket)上的结点数小于这个值时树转链表 static final int UNTREEIFY_THRESHOLD = 6; ``` 7. 当 Map 里面的数量**大于等于**这个阈值时,表中的桶才能进行树形化 ,否则桶内元素超过 8 时会扩容,而不是树形化。为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD (8) ```java - //桶中结构转化为红黑树对应的数组长度最小的值 + // 桶中结构转化为红黑树对应的数组长度最小的值 static final int MIN_TREEIFY_CAPACITY = 64; ``` @@ -4798,7 +4799,7 @@ HashMap继承关系如下图所示: 8. table 用来初始化(必须是二的 n 次幂) ```java - //存储元素的数组 + // 存储元素的数组 transient Node[] table; ``` @@ -4807,21 +4808,21 @@ HashMap继承关系如下图所示: 9. HashMap 中存放元素的个数(**重点**) ```java - //存放元素的个数,HashMap中K-V的实时数量,不是table数组的长度 + // 存放元素的个数,HashMap中K-V的实时数量,不是table数组的长度 transient int size; ``` 10. 记录 HashMap 的修改次数 ```java - //每次扩容和更改map结构的计数器 + // 每次扩容和更改map结构的计数器 transient int modCount; ``` 11. 调整大小下一个容量的值计算方式为(容量 * 负载因子) ```java - //临界值,当实际大小(容量*负载因子)超过临界值时,会进行扩容 + // 临界值,当实际大小(容量*负载因子)超过临界值时,会进行扩容 int threshold; ``` @@ -4945,22 +4946,20 @@ HashMap继承关系如下图所示: ##### 成员方法 -* hash() - - HashMap 是支持 Key 为空的;HashTable 是直接用 Key 来获取 HashCode,key 为空会抛异常 +* hash():HashMap 是支持 Key 为空的;HashTable 是直接用 Key 来获取 HashCode,key 为空会抛异常 * &(按位与运算):相同的二进制数位上,都是1的时候,结果为1,否则为零 - * ^(按位异或运算):相同的二进制数位上,数字相同,结果为0,不同为1,**不进位加法** - +* ^(按位异或运算):相同的二进制数位上,数字相同,结果为0,不同为1,**不进位加法** + ```java - static final int hash(Object key) { +static final int hash(Object key) { int h; // 1)如果key等于null:可以看到当key等于null的时候也是有哈希值的,返回的是0. // 2)如果key不等于null:首先计算出key的hashCode赋值给h,然后与h无符号右移16位后的二进制进行按位异或得到最后的hash值 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } ``` - + 计算 hash 的方法:将 hashCode 无符号右移 16 位,高 16bit 和低 16bit 做异或,扰动运算 原因:当数组长度很小,假设是 16,那么 n-1即为 1111 ,这样的值和 hashCode() 直接做按位与操作,实际上只使用了哈希值的后4位。如果当哈希值的高位变化很大,低位变化很小,就很容易造成哈希冲突了,所以这里**把高低位都利用起来,让高16 位也参与运算**,从而解决了这个问题 @@ -4968,13 +4967,11 @@ HashMap继承关系如下图所示: 哈希冲突的处理方式: * 开放定址法:线性探查法(ThreadLocalMap 使用),平方探查法(i + 1^2、i - 1^2、i + 2^2……)、双重散列(多个哈希函数) - * 链地址法:拉链法 - +* 链地址法:拉链法 + -* put() - - jdk1.8 前是头插法 (拉链法),多线程下扩容出现循环链表,jdk1.8 以后引入红黑树,插入方法变成尾插法 +* put():jdk1.8 前是头插法 (拉链法),多线程下扩容出现循环链表,jdk1.8 以后引入红黑树,插入方法变成尾插法 第一次调用 put 方法时创建数组 Node[] table,因为散列表耗费内存,为了防止内存浪费,所以**延迟初始化** @@ -4987,18 +4984,18 @@ HashMap继承关系如下图所示: 3. 如果出现碰撞冲突:如果该桶使用红黑树处理冲突,则调用红黑树的方法插入数据;否则采用传统的链式方法插入,如果链的长度达到临界值,则把链转变为红黑树 4. 如果数组位置相同,通过 equals 比较内容是否相同:相同则新的 value 覆盖旧 value,不相同则将新的键值对添加到哈希表中 - 5. 如果 size 大于阈值 threshold,则进行扩容 - +5. 如果 size 大于阈值 threshold,则进行扩容 + ```java - public V put(K key, V value) { +public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } ``` - + putVal() 方法中 key 在这里执行了一下 hash(),在 putVal 函数中使用到了上述 hash 函数计算的哈希值: ```java - final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { +final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //。。。。。。。。。。。。。。 if ((p = tab[i = (n - 1) & hash]) == null){//这里的n表示数组长度16 //..... @@ -5014,7 +5011,7 @@ HashMap继承关系如下图所示: } } ``` - + * `(n - 1) & hash`:计算下标位置 @@ -5039,9 +5036,8 @@ HashMap继承关系如下图所示: -* tableSizeFor() - 创建 HashMap 指定容量时,HashMap 通过位移运算和或运算得到比指定初始化容量大的最小的2的 n 次幂 - +* tableSizeFor():创建 HashMap 指定容量时,HashMap 通过位移运算和或运算得到比指定初始化容量大的最小的 2 的 n 次幂 + ```java static final int tableSizeFor(int cap) {//int cap = 10 int n = cap - 1; @@ -5053,18 +5049,18 @@ HashMap继承关系如下图所示: return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; } ``` - + 分析算法: - + 1. `int n = cap - 1`:防止 cap 已经是 2 的幂。如果 cap 已经是 2 的幂, 不执行减 1 操作,则执行完后面的无符号右移操作之后,返回的 capacity 将是这个 cap 的 2 倍 2. n=0 (cap-1 之后),则经过后面的几次无符号右移依然是 0,返回的 capacity 是 1,最后有 n+1 3. |(按位或运算):相同的二进制数位上,都是 0 的时候,结果为 0,否则为 1 4. 核心思想:**把最高位是 1 的位以及右边的位全部置 1**,结果加 1 后就是大于指定容量的最小的 2 的 n 次幂 - + 例如初始化的值为 10: - + * 第一次右移 - + ```java int n = cap - 1;//cap=10 n=9 n |= n >>> 1; @@ -5074,9 +5070,9 @@ HashMap继承关系如下图所示: 00000000 00000000 00000000 00001101 //按位或之后是13 //使得n的二进制表示中与最高位的1紧邻的右边一位为1 ``` - + * 第二次右移 - + ```java n |= n >>> 2;//n通过第一次右移变为了:n=13 00000000 00000000 00000000 00001101 // 13 @@ -5085,17 +5081,17 @@ HashMap继承关系如下图所示: 00000000 00000000 00000000 00001111 //按位或之后是15 //无符号右移两位,会将最高位两个连续的1右移两位,然后再与原来的n做或操作,这样n的二进制表示的高位中会有4个连续的1 ``` - + 注意:容量最大是 32bit 的正数,因此最后 `n |= n >>> 16`,最多是 32 个 1(但是这已经是负数了)。在执行 tableSizeFor 之前,对 initialCapacity 做了判断,如果大于 MAXIMUM_CAPACITY(2 ^ 30),则取 MAXIMUM_CAPACITY;如果小于 MAXIMUM_CAPACITY(2 ^ 30),会执行移位操作,所以移位操作之后,最大 30 个 1,加 1 之后得 2 ^ 30 - + * 得到的 capacity 被赋值给了 threshold - + ```java this.threshold = tableSizeFor(initialCapacity);//initialCapacity=10 ``` - + * JDK 11 - + ```java static final int tableSizeFor(int cap) { //无符号右移,高位补0 @@ -5117,10 +5113,10 @@ HashMap继承关系如下图所示: return n - (i >>> 1); } ``` - + - -* resize() + +* resize(): 当 HashMap 中的元素个数超过 `(数组长度)*loadFactor(负载因子)` 或者链表过长时(链表长度 > 8,数组长度 < 64),就会进行数组扩容,创建新的数组,伴随一次重新 hash 分配,并且遍历 hash 表中所有的元素非常耗时,所以要尽量避免 resize @@ -5204,9 +5200,8 @@ HashMap继承关系如下图所示: ​ -* remove() - 删除是首先先找到元素的位置,如果是链表就遍历链表找到元素之后删除。如果是用红黑树就遍历树然后找到之后做删除,树小于 6 的时候退化为链表 - +* remove():删除是首先先找到元素的位置,如果是链表就遍历链表找到元素之后删除。如果是用红黑树就遍历树然后找到之后做删除,树小于 6 的时候退化为链表 + ```java final Node removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { @@ -5265,20 +5260,20 @@ HashMap继承关系如下图所示: * get() - 1. 通过hash值获取该key映射到的桶 + 1. 通过 hash 值获取该 key 映射到的桶 - 2. 桶上的key就是要查找的key,则直接找到并返回 + 2. 桶上的 key 就是要查找的 key,则直接找到并返回 - 3. 桶上的key不是要找的key,则查看后续的节点: + 3. 桶上的 key 不是要找的 key,则查看后续的节点: - * 如果后续节点是红黑树节点,通过调用红黑树的方法根据key获取value + * 如果后续节点是红黑树节点,通过调用红黑树的方法根据 key 获取v alue - * 如果后续节点是链表节点,则通过循环遍历链表根据key获取value + * 如果后续节点是链表节点,则通过循环遍历链表根据 key 获取 value - 4. 红黑树节点调用的是getTreeNode方法通过树形节点的find方法进行查 + 4. 红黑树节点调用的是 getTreeNode 方法通过树形节点的 find 方法进行查 * 查找红黑树,之前添加时已经保证这个树是有序的,因此查找时就是折半查找,效率更高。 - * 这里和插入时一样,如果对比节点的哈希值相等并且通过equals判断值也相等,就会判断key相等,直接返回,不相等就从子树中递归查找 + * 这里和插入时一样,如果对比节点的哈希值相等并且通过 equals 判断值也相等,就会判断 key 相等,直接返回,不相等就从子树中递归查找 5. 时间复杂度 O(1) @@ -9893,7 +9888,7 @@ public static void main(String[] args) { 类元信息:在类编译期间放入方法区,存放了类的基本信息,包括类的方法、参数、接口以及常量池表 -常量池表(Constant Pool Table)是 Class 文件的一部分,存储了类在编译期间生成的**字面量、符号引用**,JVM 为每个已加载的类维护一个常量池 +常量池表(Constant Pool Table)是 Class 文件的一部分,存储了**类在编译期间生成的字面量、符号引用**,JVM 为每个已加载的类维护一个常量池 - 字面量:基本数据类型、字符串类型常量、声明为 final 的常量值等 - 符号引用:类、字段、方法、接口等的符号引用 @@ -10926,13 +10921,12 @@ G1(Garbage-First)是一款面向服务端应用的垃圾收集器,**应用 G1 对比其他处理器的优点: -* **并发与并行:** +* **并发与并行**: * 并行性:G1 在回收期间,可以有多个 GC 线程同时工作,有效利用多核计算能力,此时用户线程 STW * 并发性:G1 拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此不会在整个回收阶段发生完全阻塞应用程序的情况 * 其他的垃圾收集器使用内置的 JVM 线程执行 GC 的多线程操作,而 G1 GC 可以采用应用线程承担后台运行的 GC 工作,JVM 的 GC 线程处理速度慢时,系统会**调用应用程序线程加速垃圾回收**过程 -* **分区算法:** - +* **分区算法**: * 从分代上看,G1 属于分代型垃圾回收器,区分年轻代和老年代,年轻代依然有 Eden 区和 Survivor 区。从堆结构上看,**新生代和老年代不再物理隔离**,不用担心每个代内存是否足够,这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC * 将整个堆划分成约 2048 个大小相同的独立 Region 块,每个 Region 块大小根据堆空间的实际大小而定,整体被控制在 1MB 到 32 MB之间且为 2 的 N 次幂,所有 Region 大小相同,在 JVM 生命周期内不会被改变。G1 把堆划分成多个大小相等的独立区域,使得每个小空间可以单独进行垃圾回收 * **新的区域 Humongous**:本身属于老年代区,当出现了一个巨型对象超出了分区容量的一半,该对象就会进入到该区域。如果一个 H 区装不下一个巨型对象,那么 G1 会寻找连续的 H 分区来存储,为了能找到连续的H区,有时候不得不启动 Full GC @@ -10943,14 +10937,13 @@ G1 对比其他处理器的优点: ![](https://gitee.com/seazean/images/raw/master/Java/JVM-G1-Region区域.png) -- **空间整合:** +- **空间整合**: - CMS:标记-清除算法、内存碎片、若干次 GC 后进行一次碎片整理 - G1:整体来看是基于标记 - 整理算法实现的收集器,从局部(Region 之间)上来看是基于复制算法实现的,两种算法都可以避免内存碎片 -- **可预测的停顿时间模型(软实时 soft real-time):** +- **可预测的停顿时间模型(软实时 soft real-time)**:可以指定在 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒 - - 可以指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒 - 由于分块的原因,G1 可以只选取部分区域进行内存回收,这样缩小了回收的范围,对于全局停顿情况也能得到较好的控制 - G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间,通过过去回收的经验获得),在后台维护一个**优先列表**,每次根据允许的收集时间优先回收价值最大的 Region,保证了 G1 收集器在有限的时间内可以获取尽可能高的收集效率 @@ -11019,7 +11012,7 @@ G1 中提供了三种垃圾回收模式:YoungGC、Mixed GC 和 FullGC,在不 2. 更新 RSet:处理 dirty card queue 更新 RS,此后 RSet 准确的反映对象的引用关系 * dirty card queue:类似缓存,产生了引用先记录在这里,然后更新到 RSet * 作用:产生引用直接更新 RSet 需要线程同步开销很大,使用队列性能好 - 3. 处理 RSet:识别被老年代对象指向的 Eden 中的对象,这些被指向的对象被认为是存活的对象,把需要回收的分区放入 YoungCSet 中进行回收 + 3. 处理 RSet:识别被老年代对象指向的 Eden 中的对象,这些被指向的对象被认为是存活的对象,把需要回收的分区放入 Young CSet 中进行回收 4. 复制对象:Eden 区内存段中存活的对象会被复制到 survivor 区,survivor 区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到 old 区中空的内存分段,如果 survivor 空间不够,Eden 空间的部分数据会直接晋升到老年代空间 5. 处理引用:处理 Soft,Weak,Phantom,JNI Weak 等引用,最终 Eden 空间的数据为空,GC 停止工作 @@ -11120,9 +11113,9 @@ ZGC 目标: ZGC 的工作过程可以分为 4 个阶段: * 并发标记(Concurrent Mark): 遍历对象图做可达性分析的阶段,也要经过初始标记和最终标记,需要短暂停顿 -* 并发预备重分配( Concurrent Prepare for Relocate): 根据特定的查询条件统计得出本次收集过程要清理哪些 Region,将这些 Region 组成重分配集(Relocation Set) +* 并发预备重分配( Concurrent Prepare for Relocate):根据特定的查询条件统计得出本次收集过程要清理哪些 Region,将这些 Region 组成重分配集(Relocation Set) * 并发重分配(Concurrent Relocate): 重分配是 ZGC 执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的 Region 上,并为重分配集中的**每个 Region 维护一个转发表**(Forward Table),记录从旧地址到新地址的转向关系 -* 并发重映射(Concurrent Remap):修正整个堆中指向重分配集中旧对象的所有引用,ZGC 的并发映射并不是一个必须要立即完成的任务,ZGC 很巧妙地把并发重映射阶段要做的工作,合并到下一次垃圾收集循环中的并发标记阶段里去完成,因为都是要遍历所有对象的,这样合并节省了一次遍历的开销 +* 并发重映射(Concurrent Remap):修正整个堆中指向重分配集中旧对象的所有引用,ZGC 的并发映射并不是一个必须要立即完成的任务,ZGC 很巧妙地把并发重映射阶段要做的工作,合并到下一次垃圾收集循环中的并发标记阶段里去完成,因为都是要遍历所有对象,这样合并节省了一次遍历的开销 ZGC 几乎在所有地方并发执行的,除了初始标记的是 STW 的,但这部分的实际时间是非常少的,所以响应速度快,在尽可能对吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟 @@ -11645,7 +11638,7 @@ Java 对象创建时机: System.out.println("4"); } - int a = 110; // 实例变量 + int a = 110; // 实例变量 static int b = 112; // 静态变量 }/* Output: 2 @@ -11799,7 +11792,7 @@ Java 对象创建时机: 类变量初始化: * static 变量分配空间和赋值是两个步骤:**分配空间在准备阶段完成,赋值在初始化阶段完成** -* 如果 static 变量是 final 的基本类型以及字符串常量,那么编译阶段值就确定了,准备阶段会显式初始化 +* 如果 static 变量是 final 的基本类型以及字符串常量,那么编译阶段值(方法区)就确定了,准备阶段会显式初始化 * 如果 static 变量是 final 的,但属于引用类型或者构造器方法的字符串,赋值在初始化阶段完成 实例: @@ -14052,7 +14045,7 @@ public static void main(String[] args) { **数组的循环:** ```java -int[] array = {1, 2, 3, 4, 5}; // 数组赋初值的简化写法也是语法糖哦 +int[] array = {1, 2, 3, 4, 5}; // 数组赋初值的简化写法也是语法糖 for (int e : array) { System.out.println(e); } @@ -15230,8 +15223,6 @@ GCViewer 是一款离线的 GC 日志分析器,用于可视化 Java VM 选项 -推荐阅读:https://time.geekbang.org/column/article/41440 - 参考书籍:《数据结构高分笔记》 @@ -15251,10 +15242,10 @@ GCViewer 是一款离线的 GC 日志分析器,用于可视化 Java VM 选项 3. 递归的方向:必须走向终结点 ```java -//f(x)=f(x-1)+1; f(1)=1; f(10)=? -//1.递归的终结点: f(1) = 1 -//2.递归的公式:f(x) = f(x - 1) + 1 -//3.递归的方向:必须走向终结点 +// f(x)=f(x-1)+1; f(1)=1; f(10)=? +// 1.递归的终结点: f(1) = 1 +// 2.递归的公式:f(x) = f(x - 1) + 1 +// 3.递归的方向:必须走向终结点 public static int f(int x){ if(x == 1){ return 1; @@ -15273,9 +15264,9 @@ public static int f(int x){ #### 公式转换 ```java -//已知: f(x) = f(x + 1) + 2, f(1) = 1。求:f(10) = ? -//公式转换 -//f(x-1)=f(x-1+1)+2 => f(x)=f(x-1)+2 +// 已知: f(x) = f(x + 1) + 2, f(1) = 1。求:f(10) = ? +// 公式转换 +// f(x-1)=f(x-1+1)+2 => f(x)=f(x-1)+2 //(1)递归的公式: f(n) = f(n-1)- 2 ; //(2)递归的终结点: f(1) = 1 //(3)递归的方向:必须走向终结点。 @@ -15358,11 +15349,11 @@ public class Hanoi { // 将n个块分治的从x移动到z,y为辅助柱 private static void hanoi(char x, char y, char z, int n) { if (n == 1) { - System.out.println(x + "→" + z); //直接将x的块移动到z + System.out.println(x + "→" + z); // 直接将x的块移动到z } else { - hanoi(x, z, y, n - 1); //分治处理n-1个块,先将n-1个块借助z移到y - System.out.println(x + "→" + z); //然后将x最下面的块(最大的)移动到z - hanoi(y, x, z, n - 1); //最后将n-1个块从y移动到z,x为辅助柱 + hanoi(x, z, y, n - 1); // 分治处理n-1个块,先将n-1个块借助z移到y + System.out.println(x + "→" + z); // 然后将x最下面的块(最大的)移动到z + hanoi(y, x, z, n - 1); // 最后将n-1个块从y移动到z,x为辅助柱 } } } @@ -15378,9 +15369,7 @@ public class Hanoi { #### 啤酒问题 -非规律化递归问题。 - -啤酒2元一瓶,4个盖子可以换一瓶,2个空瓶可以换一瓶。 +非规律化递归问题,啤酒 2 元 1 瓶,4 个盖子可以换 1 瓶,2 个空瓶可以换 1 瓶 ```java public class BeerDemo{ @@ -15402,8 +15391,8 @@ public class BeerDemo{ int currentCoverNum = lastCoverNum + number ; // 把他们换算成金额 int totalMoney = 0 ; - totalMoney += (currentBottleNum/2)*2;//除2代表可以换几个瓶子,乘2代表换算成钱,秒! - lastBottleNum = currentBottleNum % 2 ;//取余//算出剩余的瓶子 + totalMoney += (currentBottleNum/2)*2; // 除2代表可以换几个瓶子,乘2代表换算成钱,秒! + lastBottleNum = currentBottleNum % 2 ;// 取余//算出剩余的瓶子 totalMoney += (currentCoverNum / 4) * 2; lastCoverNum = currentCoverNum % 4 ; @@ -17365,7 +17354,7 @@ UML 从目标系统的不同角度出发,定义了用例图、类图、对象 #### 饿汉式 -饿汉式在类加载的过程导致该单实例对象被创建,虚拟机会保证类加载的线程安全,但是如果只是为了加载该类不需要实例,则会造成内存的浪费 +饿汉式在类加载的过程导致该单实例对象被创建,**虚拟机会保证类加载的线程安全**,但是如果只是为了加载该类不需要实例,则会造成内存的浪费 * 静态变量的方式: @@ -17388,8 +17377,9 @@ UML 从目标系统的不同角度出发,定义了用例图、类图、对象 ``` * 问题1:为什么类加 final 修饰? - 不被子类继承,防止子类中不适当的行为覆盖父类的方法,破坏了单例 - + + * 不被子类继承,防止子类中不适当的行为覆盖父类的方法,破坏了单例 + * 问题2:如果实现了序列化接口,怎么防止防止反序列化破坏单例? * 对单例声明 transient,然后实现 readObject(ObjectInputStream in) 方法,复用原来的单例 @@ -17399,13 +17389,16 @@ UML 从目标系统的不同角度出发,定义了用例图、类图、对象 * 实现 readResolve() 方法,当 JVM 从内存中反序列化地"组装"一个新对象,就会自动调用 readResolve 方法返回原来单例 * 问题3:为什么构造方法设置为私有? 是否能防止反射创建新的实例? - 防止其他类无限创建对象;不能防止反射破坏 - + + * 防止其他类无限创建对象;不能防止反射破坏 + * 问题4:这种方式是否能保证单例对象创建时的线程安全? - 能,静态变量初始化在类加载时完成,由 JVM 保证线程安全 - + + * 能,静态变量初始化在类加载时完成,由 JVM 保证线程安全 + * 问题5:为什么提供静态方法而不是直接将 INSTANCE 设置为 public? - 更好的封装性、提供泛型支持、可以改进成懒汉单例设计 + + * 更好的封装性、提供泛型支持、可以改进成懒汉单例设计 * 静态代码块的方式: @@ -17622,17 +17615,16 @@ UML 从目标系统的不同角度出发,定义了用例图、类图、对象 switch (tc) { case TC_OBJECT: return checkResolve(readOrdinaryObject(unshared)); - //重点查看readOrdinaryObject方法 } } } private Object readOrdinaryObject(boolean unshared) throws IOException { - //isInstantiable 返回true,执行 desc.newInstance(),通过反射创建新的单例类 + // isInstantiable 返回true,执行 desc.newInstance(),通过反射创建新的单例类 obj = desc.isInstantiable() ? desc.newInstance() : null; - //添加 readResolve 方法后 desc.hasReadResolveMethod() 方法执行结果为true + // 添加 readResolve 方法后 desc.hasReadResolveMethod() 方法执行结果为true if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) { // 通过反射调用 Singleton 类中的 readResolve 方法,将返回值赋值给rep变量 - // 多次调用ObjectInputStream类中的readObject方法,本质调用定义的readResolve方法,所以返回的是同一个对象。 + // 多次调用ObjectInputStream类中的readObject方法,本质调用定义的readResolve方法,返回的是同一个对象。 Object rep = desc.invokeReadResolve(obj); } return obj; @@ -18562,7 +18554,11 @@ Java 中提供了一个动态代理类 Proxy,Proxy 并不是代理对象的类 `static Object newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h) ` * 参数一:类加载器,负责加载代理类 + + 传入类加载器,代理和被代理对象要用一个类加载器才是父子关系,不同类加载器加载相同的类在 JVM 中都不是同一个类对象 + * 参数二:被代理业务对象的**全部实现的接口**,代理对象与真实对象实现相同接口,知道为哪些方法做代理 + * 参数三:代理真正的执行方法,也就是代理的处理逻辑 代码实现: @@ -18644,7 +18640,7 @@ public final class $Proxy0 extends Proxy implements SellTickets { } } -//Java提供的动态代理相关类 +// Java提供的动态代理相关类 public class Proxy implements java.io.Serializable { protected InvocationHandler h; @@ -18683,7 +18679,7 @@ public static Object newProxyInstance(ClassLoader loader, checkProxyAccess(Reflection.getCallerClass(), loader, intfs); } - // 从缓存中查找 class 类型的代理对象,参数二是代理需要实现的接口 + // 从缓存中查找 class 类型的代理对象,会调用 ProxyClassFactory#apply 方法 Class cl = getProxyClass0(loader, intfs); //proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory()) diff --git a/Prog.md b/Prog.md index ed62ae7..d9be48f 100644 --- a/Prog.md +++ b/Prog.md @@ -4,22 +4,22 @@ ### 概述 -进程:程序是静止的,进程实体的运行过程就是进程,是系统进行资源分配的基本单位 +进程:程序是静止的,进程实体的运行过程就是进程,是系统进行**资源分配的基本单位** 进程的特征:并发性、异步性、动态性、独立性、结构性 -**线程**:线程是属于进程的,是一个基本的 CPU 执行单元,是程序执行流的最小单元。线程是进程中的一个实体,是被系统独立调度和分配的基本单位,线程本身不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其他线程共享进程所拥有的全部资源。 +**线程**:线程是属于进程的,是一个基本的 CPU 执行单元,是程序执行流的最小单元。线程是进程中的一个实体,是系统**独立调度的基本单位**,线程本身不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其他线程共享进程所拥有的全部资源 关系:一个进程可以包含多个线程,这就是多线程,比如看视频是进程,图画、声音、广告等就是多个线程 线程的作用:使多道程序更好的并发执行,提高资源利用率和系统吞吐量,增强操作系统的并发性能 -**并发并行**: +并发并行: * 并行:在同一时刻,有多个指令在多个 CPU 上同时执行 * 并发:在同一时刻,有多个指令在单个 CPU 上交替执行 -**同步异步**: +同步异步: * 需要等待结果返回,才能继续运行就是同步 * 不需要等待结果返回,就能继续运行就是异步 @@ -28,7 +28,7 @@ 参考视频:https://www.bilibili.com/video/BV16J411h7Rd(推荐观看) -笔记的整体内容依据视频编写,并且补充了很多新知识 +笔记的整体结构依据视频编写,并随着学习的深入补充了很多知识 @@ -49,21 +49,21 @@ 同一台计算机的进程通信称为 IPC(Inter-process communication) * 信号量:信号量是一个计数器,用于多进程对共享数据的访问,解决同步相关的问题并避免竞争条件 - * 共享存储:多个进程可以访问同一块内存空间,需要使用信号量用来同步对共享存储的访问(MappedByteBuffer) + * 共享存储:多个进程可以访问同一块内存空间,需要使用信号量用来同步对共享存储的访问 * 管道通信:管道是用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,pipe 文件 - * 匿名管道(Pipes):用于具有亲缘关系的父子进程间或者兄弟进程之间的通信,只支持半双工通信 + * 匿名管道(Pipes):用于具有亲缘关系的父子进程间或者兄弟进程之间的通信,只支持**半双工通信** * 命名管道(Names Pipes):以磁盘文件的方式存在,可以实现本机任意两个进程通信,遵循 FIFO - * 消息队列:内核中存储消息的链表,由消息队列标识符标识,能在不同进程之间提供全双工通信,对比管道: + * 消息队列:内核中存储消息的链表,由消息队列标识符标识,能在不同进程之间提供**全双工通信**,对比管道: * 匿名管道存在于内存中的文件;命名管道存在于实际的磁盘介质或者文件系统;消息队列存放在内核中,只有在内核重启(操作系统重启)或者显示地删除一个消息队列时,该消息队列才被真正删除 * 读进程可以根据消息类型有选择地接收消息,而不像 FIFO 那样只能默认地接收 - 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP + 不同计算机之间的**进程通信**,需要通过网络,并遵守共同的协议,例如 HTTP - * 套接字:与其它通信机制不同的是,它可用于不同机器间的进程通信 + * 套接字:与其它通信机制不同的是,它可用于不同机器间的互相通信 * 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量 - Java 中的通信机制:volatile、等待/通知机制、join方式、InheritableThreadLocal、MappedByteBuffer + **Java 中的通信机制**:volatile、等待/通知机制、join 方式、InheritableThreadLocal、MappedByteBuffer * 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低 @@ -86,7 +86,7 @@ Thread 创建线程方式:创建线程类,匿名内部类方式 * **start() 方法底层其实是给 CPU 注册当前线程,并且触发 run() 方法执行** -* 线程的启动必须调用 start() 方法,如果线程直接调用 run() 方法,相当于变成了普通类的执行,此时将只有主线程在执行该线程 +* 线程的启动必须调用 start() 方法,如果线程直接调用 run() 方法,相当于变成了普通类的执行,此时主线程将只有执行该线程 * 建议线程先创建子线程,主线程的任务放在之后,否则主线程(main)永远是先执行完 Thread 构造器: @@ -95,14 +95,14 @@ Thread 构造器: * `public Thread(String name)` ```java -puclic class ThreadDemo{ +public class ThreadDemo{ public static void main(String[] args) { Thread t = new MyThread(); t.start(); for(int i = 0 ; i < 100 ; i++ ){ - System.out.println("main线程"+i) + System.out.println("main线程" + i) } - // main线程输出放在上面 就变成有先后顺序了 + // main线程输出放在上面 就变成有先后顺序了,因为是 main 线程驱动的子线程运行 } } class MyThread extends Thread{ @@ -149,20 +149,21 @@ public class MyRunnable implements Runnable{ @Override public void run() { for(int i = 0 ; i < 10 ; i++ ){ - System.out.println(Thread.currentThread().getName()+"->"+i); + System.out.println(Thread.currentThread().getName() + "->" + i); } } } ``` -**Thread 类本身也是实现了 Runnable 接口**,Thread 类中持有 Runnable 的属性,用来执行 run 方法: +**Thread 类本身也是实现了 Runnable 接口**,Thread 类中持有 Runnable 的属性,执行线程 run 方法底层是调用 Runnable#run: ```java public class Thread implements Runnable { private Runnable target; - // 底层调用的是 Runnable 的 run 方法 + public void run() { if (target != null) { + // 底层调用的是 Runnable 的 run 方法 target.run(); } } @@ -198,19 +199,19 @@ Runnable 方式的优缺点: 1. 定义一个线程任务类实现 Callable 接口,申明线程执行的结果类型 2. 重写线程任务类的 call 方法,这个方法可以直接返回执行的结果 3. 创建一个 Callable 的线程任务对象 -4. 把 Callable 的线程任务对象包装成一个未来任务对象 +4. 把 Callable 的线程任务对象**包装成一个未来任务对象** 5. 把未来任务对象包装成线程对象 6. 调用线程的 start() 方法启动线程 -`public FutureTask(Callable callable)`:未来任务对象,在线程执行完后**得到线程的执行结果** +`public FutureTask(Callable callable)`:未来任务对象,在线程执行完后得到线程的执行结果 -* FutureTask 就是 Runnable 对象,因为 Thread 类只能执行 Runnable 实例的任务对象,所以把 Callable 包装成未来任务对象 +* FutureTask 就是 Runnable 对象,因为 **Thread 类只能执行 Runnable 实例的任务对象**,所以把 Callable 包装成未来任务对象 * 线程池部分详解了 FutureTask 的源码 `public V get()`:同步等待 task 执行完毕的结果,如果在线程中获取另一个线程执行结果,会阻塞等待,用于线程同步 * get() 线程会阻塞等待任务执行完成 -* run() 执行完后会把结果设置到任务中的一个成员变量,get() 线程可以获取到该变量的值 +* run() 执行完后会把结果设置到 FutureTask 的一个成员变量,get() 线程可以获取到该变量的值 优缺点: @@ -318,7 +319,7 @@ start:使用 start 是启动新的线程,此线程处于就绪(可运行 **面试问题**:run() 方法中的异常不能抛出,只能 try/catch * 因为父类中没有抛出任何异常,子类不能比父类抛出更多的异常 -* 异常不能跨线程传播回 main() 中,因此必须在本地进行处理 +* **异常不能跨线程传播回 main() 中**,因此必须在本地进行处理 @@ -330,10 +331,10 @@ start:使用 start 是启动新的线程,此线程处于就绪(可运行 sleep: -* 调用 sleep 会让当前线程从 Running 进入 `Timed Waiting` 状态(阻塞) +* 调用 sleep 会让当前线程从 `Running` 进入 `Timed Waiting` 状态(阻塞) * sleep() 方法的过程中,线程不会释放对象锁 * 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException -* 睡眠结束后的线程未必会立刻得到执行 +* 睡眠结束后的线程未必会立刻得到执行,需要抢占 CPU * 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性 yield: @@ -352,7 +353,7 @@ yield: 线程优先级会提示(hint)调度器优先调度该线程,但这仅仅是一个提示,调度器可以忽略它 -如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用 +如果 CPU 比较忙,那么优先级高的线程会获得更多的时间片,但 CPU 闲时,优先级几乎没作用 @@ -367,17 +368,17 @@ public final void join():等待这个线程结束 原理:调用者轮询检查线程 alive 状态,t1.join() 等价于: ```java -synchronized (t1) { - // 调用者线程进入 t1 的 waitSet 等待, 直到 t1 运行结束 - while (t1.isAlive()) { - t1.wait(0); +public final synchronized void join(long millis) throws InterruptedException { + // 调用者线程进入 thread 的 waitSet 等待, 直到当前线程运行结束 + while (isAlive()) { + wait(0); } } ``` -* join 方法是被 synchronized 修饰的,本质上是一个对象锁,其内部的 wait 方法调用也是释放锁的,但是**释放的是当前线程的对象锁,而不是外面的锁** +* join 方法是被 synchronized 修饰的,本质上是一个对象锁,其内部的 wait 方法调用也是释放锁的,但是**释放的是当前的线程对象锁,而不是外面的锁** -* t1 会强占 CPU 资源,当调用某个线程的 join 方法后,该线程抢占到 CPU 资源,就不再释放,直到线程执行完毕 +* 当调用某个线程(t1)的 join 方法后,该线程(t1)抢占到 CPU 资源,就不再释放,直到线程执行完毕 线程同步: @@ -426,7 +427,7 @@ public class Test { `public boolean isInterrupted()`:判断当前线程是否被打断,不清除打断标记 -打断的线程会发生上下文切换,操作系统会保存线程信息,抢占到 CPU 后会从中断的地方接着运行 +打断的线程会发生上下文切换,操作系统会保存线程信息,抢占到 CPU 后会从中断的地方接着运行(打断不是停止) * sleep、wait、join 方法都会让线程进入阻塞状态,打断进程**会清空打断状态**(false) @@ -513,7 +514,7 @@ LockSupport 类在 同步 → park-un 详解 终止模式之两阶段终止模式:Two Phase Termination -目标:在一个线程 T1 中如何“优雅”终止线程 T2?”优雅“指的是给 T2 一个后置处理器 +目标:在一个线程 T1 中如何优雅终止线程 T2?优雅指的是给 T2 一个后置处理器 错误思想: @@ -537,7 +538,7 @@ public class Test { } class TwoPhaseTermination { private Thread monitor; - //启动监控线程 + // 启动监控线程 public void start() { monitor = new Thread(new Runnable() { @Override @@ -549,11 +550,11 @@ class TwoPhaseTermination { break; } try { - Thread.sleep(1000);//睡眠 - System.out.println("执行监控记录");//在此被打断不会异常 - } catch (InterruptedException e) {//在睡眠期间被打断,进入异常处理的逻辑 + Thread.sleep(1000); // 睡眠 + System.out.println("执行监控记录"); // 在此被打断不会异常 + } catch (InterruptedException e) { // 在睡眠期间被打断,进入异常处理的逻辑 e.printStackTrace(); - //重新设置打断标记 + // 重新设置打断标记 thread.interrupt(); } } @@ -561,7 +562,7 @@ class TwoPhaseTermination { }); monitor.start(); } - //停止监控线程 + // 停止监控线程 public void stop() { monitor.interrupt(); } @@ -621,6 +622,8 @@ t.start(); | public final void suspend() | **挂起(暂停)线程运行** | | public final void resume() | 恢复线程运行 | +所以 Java 中线程的状态是阻塞,很少使用挂起 + *** @@ -631,16 +634,16 @@ t.start(); 进程的状态参考操作系统:创建态、就绪态、运行态、阻塞态、终止态 -线程由生到死的完整过程(生命周期):当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态,在 API 中 `java.lang.Thread.State` 这个枚举中给出了六种线程状态: +线程由生到死的完整过程(生命周期):当线程被创建并启动以后,既不是一启动就进入了执行状态,也不是一直处于执行状态,在 API 中 `java.lang.Thread.State` 这个枚举中给出了六种线程状态: -| 线程状态 | 导致状态发生条件 | -| ------------------------ | ------------------------------------------------------------ | -| NEW (新建) | 线程刚被创建,但是并未启动,还没调用 start 方法,只有线程对象,没有线程特征 | -| Runnable (可运行) | 线程可以在 java 虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器,调用了 t.start() 方法:就绪(经典叫法) | -| Blocked (锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入 Blocked 状态;当该线程持有锁时,该线程将变成 Runnable 状态 | -| Waiting (无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入 Waiting 状态,进入这个状态后不能自动唤醒,必须等待另一个线程调用 notify 或者 notifyAll 方法才能唤醒 | -| Timed Waiting (计时等待) | 有几个方法有超时参数,调用将进入 Timed Waiting 状态,这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有 Thread.sleep 、Object.wait | -| Teminated (被终止) | run 方法正常退出而死亡,或者因为没有捕获的异常终止了 run 方法而死亡 | +| 线程状态 | 导致状态发生条件 | +| -------------------------- | ------------------------------------------------------------ | +| NEW(新建) | 线程刚被创建,但是并未启动,还没调用 start 方法,只有线程对象,没有线程特征 | +| Runnable(可运行) | 线程可以在 Java 虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器,调用了 t.start() 方法:就绪(经典叫法) | +| Blocked(锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入 Blocked 状态;当该线程持有锁时,该线程将变成 Runnable 状态 | +| Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入 Waiting 状态,进入这个状态后不能自动唤醒,必须等待另一个线程调用 notify 或者 notifyAll 方法才能唤醒 | +| Timed Waiting (计时等待) | 有几个方法有超时参数,调用将进入 Timed Waiting 状态,这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有 Thread.sleep 、Object.wait | +| Teminated(被终止) | run 方法正常退出而死亡,或者因为没有捕获的异常终止了 run 方法而死亡 | ![](https://gitee.com/seazean/images/raw/master/Java/JUC-线程6种状态.png) @@ -711,14 +714,11 @@ Java: 竞态条件:多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件 -一个程序运行多个线程本身是没有问题的,多个线程访问共享资源会出现问题: - -* 多个线程读共享资源也没有问题 -* 在多个线程对共享资源读写操作时发生指令交错,就会出现问题 +一个程序运行多个线程本身是没有问题的,多个线程访问共享资源会出现问题。多个线程读共享资源也没有问题,在多个线程对共享资源读写操作时发生指令交错,就会出现问题 为了避免临界区的竞态条件发生(解决线程安全问题): -* 阻塞式的解决方案:synchronized,Lock +* 阻塞式的解决方案:synchronized,lock * 非阻塞式的解决方案:原子变量 管程(monitor):由局部于自己的若干公共变量和所有访问这些公共变量的过程所组成的软件模块,保证同一时刻只有一个进程在管程内活动,即管程内定义的操作在同一时刻只被一个进程调用(由编译器实现) @@ -747,7 +747,7 @@ Java: ##### 同步代码块 -锁对象:理论上可以是任意的唯一对象 +锁对象:理论上可以是**任意的唯一对象** synchronized 是可重入、不公平的重量级锁 @@ -755,9 +755,7 @@ synchronized 是可重入、不公平的重量级锁 * 锁对象建议使用共享资源 * 在实例方法中使用 this 作为锁对象,锁住的 this 正好是共享资源 -* 在静态方法中使用类名 .class 字节码作为锁对象 - * 因为静态成员属于类,被所有实例对象共享,所以需要锁住类 - * 锁住类以后,类的所有实例都相当于同一把锁,参考线程八锁 +* 在静态方法中使用类名 .class 字节码作为锁对象,因为静态成员属于类,被所有实例对象共享,所以需要锁住类 格式: @@ -894,11 +892,11 @@ class Room { ##### 线程八锁 -所谓的“线程八锁”,其实就是考察 synchronized 锁住的是哪个对象,直接百度搜索相关的实例 +线程八锁就是考察 synchronized 锁住的是哪个对象,直接百度搜索相关的实例 说明:主要关注锁住的对象是不是同一个 -* 锁住类对象,所有类的实例的方法都是安全的 +* 锁住类对象,所有类的实例的方法都是安全的,类的所有实例都相当于同一把锁 * 锁住 this 对象,只有在当前实例对象的线程内是安全的,如果有多个实例就不安全 线程不安全:因为锁住的不是同一个对象,线程 1 调用 a 方法锁住的类对象,线程 2 调用 b 方法锁住的 n2 对象,不是同一个对象 @@ -955,7 +953,7 @@ public static void main(String[] args) { Monitor 被翻译为监视器或管程 -每个 Java 对象都可以关联一个 Monitor 对象,Monitor 也是 class,其实例会存储在堆中,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针,这就是重量级锁 +每个 Java 对象都可以关联一个 Monitor 对象,Monitor 也是 class,其**实例存储在堆中**,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针,这就是重量级锁 * Mark Word 结构: @@ -968,10 +966,11 @@ Monitor 被翻译为监视器或管程 工作流程: * 开始时 Monitor 中 Owner 为 null -* 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor 中只能有一个 Owner,**obj 对象的 Mark Word 指向 Monitor** +* 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor 中只能有一个 Owner,**obj 对象的 Mark Word 指向 Monitor**,把对象原有的 MarkWord 存入线程栈中的锁记录中(轻量级锁部分详解) * 在 Thread-2 上锁的过程,Thread-3、Thread-4、Thread-5 也执行 synchronized(obj),就会进入 EntryList BLOCKED(双向链表) -* Thread-2 执行完同步代码块的内容,根据对象头中 Monitor 地址寻找,设置 Owner 为空,唤醒 EntryList 中等待的线程来竞争锁,竞争是**非公平的** +* Thread-2 执行完同步代码块的内容,根据 obj 对象头中 Monitor 地址寻找,设置 Owner 为空,把线程栈的锁记录中的对象头的值设置到 MarkWord +* 唤醒 EntryList 中等待的线程来竞争锁,竞争是**非公平的**,如果这时有新的线程想要获取锁,可能直接就抢占到了,阻塞队列的线程就会继续阻塞 * WaitSet 中的 Thread-0,是以前获得过锁,但条件不满足进入 WAITING 状态的线程(wait-notify 机制) ![](https://gitee.com/seazean/images/raw/master/Java/JUC-Monitor工作原理2.png) @@ -1008,16 +1007,16 @@ public static void main(String[] args) { 8: aload_1 // lock (synchronized开始) 9: dup // 一份用来初始化,一份用来引用 10: astore_2 // lock引用 -> slot 2 -11: monitorenter // 将 lock对象 MarkWord 置为 Monitor 指针 +11: monitorenter // 【将 lock对象 MarkWord 置为 Monitor 指针】 12: getstatic #3 // System.out 15: ldc #4 // "ok" 17: invokevirtual #5 // invokevirtual println:(Ljava/lang/String;)V 20: aload_2 // slot 2(lock引用) -21: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList +21: monitorexit // 【将 lock对象 MarkWord 重置, 唤醒 EntryList】 22: goto 30 25: astore_3 // any -> slot 3 26: aload_2 // slot 2(lock引用) -27: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList +27: monitorexit // 【将 lock对象 MarkWord 重置, 唤醒 EntryList】 28: aload_3 29: athrow 30: return @@ -1050,7 +1049,7 @@ LocalVariableTable: **synchronized 是可重入、不公平的重量级锁**,所以可以对其进行优化 ```java -无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁 //随着竞争的增加,只能锁升级,不能降级 +无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁 // 随着竞争的增加,只能锁升级,不能降级 ``` ![](https://gitee.com/seazean/images/raw/master/Java/JUC-锁升级过程.png) @@ -1065,9 +1064,9 @@ LocalVariableTable: 偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程之后重新获取该锁不再需要同步操作: -* 当锁对象第一次被线程获得的时候进入偏向状态,标记为101,同时使用 CAS 操作将线程 ID 记录到Mark Word。如果 CAS 操作成功,这个线程以后进入这个锁相关的同步块,查看这个线程 ID 是自己的就表示没有竞争,就不需要再进行任何同步操作 +* 当锁对象第一次被线程获得的时候进入偏向状态,标记为 101,同时使用 CAS 操作将线程 ID 记录到 Mark Word。如果 CAS 操作成功,这个线程以后进入这个锁相关的同步块,查看这个线程 ID 是自己的就表示没有竞争,就不需要再进行任何同步操作 -* 当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定状态或轻量级锁状态 +* 当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定或轻量级锁状态 @@ -1087,13 +1086,13 @@ LocalVariableTable: * 轻量级锁会在锁记录中记录 hashCode * 重量级锁会在 Monitor 中记录 hashCode * 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁 -* 调用 wait/notify +* 调用 wait/notify,需要申请 Monitor,进入 WaitSet **批量撤销**:如果对象被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID -* 批量重偏向:当撤销偏向锁阈值超过 20 次后,jvm 会觉得是不是偏向错了,于是在给这些对象加锁时重新偏向至加锁线程 +* 批量重偏向:当撤销偏向锁阈值超过 20 次后,JVM 会觉得是不是偏向错了,于是在给这些对象加锁时重新偏向至加锁线程 -* 批量撤销:当撤销偏向锁阈值超过 40 次后,jvm 会觉得自己确实偏向错了,根本就不该偏向,于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的 +* 批量撤销:当撤销偏向锁阈值超过 40 次后,JVM 会觉得自己确实偏向错了,根本就不该偏向,于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的 @@ -1103,9 +1102,9 @@ LocalVariableTable: ##### 轻量级锁 -一个对象有多个线程要加锁,但加锁的时间是错开的(没有竞争),那么可以使用轻量级锁来优化,轻量级锁对使用者是透明的(不可见) +一个对象有多个线程要加锁,但加锁的时间是错开的(没有竞争),可以使用轻量级锁来优化,轻量级锁对使用者是透明的(不可见) -可重入锁:**线程可以进入任何一个它已经拥有的锁所同步着的代码块,可重入锁最大的作用是避免死锁** +可重入锁:线程可以进入任何一个它已经拥有的锁所同步着的代码块,可重入锁最大的作用是**避免死锁** 轻量级锁在没有竞争时(锁重入时),每次重入仍然需要执行 CAS 操作,Java 6 才引入的偏向锁来优化 @@ -1126,13 +1125,12 @@ public static void method2() { } ``` -* 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,存储锁定对象的 Mark Word +* 创建锁记录(Lock Record)对象,每个线程的**栈帧**都会包含一个锁记录的结构,存储锁定对象的 Mark Word ![](https://gitee.com/seazean/images/raw/master/Java/JUC-轻量级锁原理1.png) -* 让锁记录中 Object reference 指向锁对象,并尝试用 CAS 替换 Object 的 Mark Word,将 Mark Word 的值存 - 入锁记录 - +* 让锁记录中 Object reference 指向锁住的对象,并尝试用 CAS 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录 + * 如果 CAS 替换成功,对象头中存储了锁记录地址和状态 00(轻量级锁) ,表示由该线程给对象加锁 ![](https://gitee.com/seazean/images/raw/master/Java/JUC-轻量级锁原理2.png) @@ -1182,7 +1180,7 @@ public static void method2() { ##### 自旋锁 -重量级锁竞争时,尝试获取锁的线程不会立即阻塞,可以使用**自旋**来进行优化,采用循环的方式去尝试获取锁 +重量级锁竞争时,尝试获取锁的线程不会立即阻塞,可以使用**自旋**(默认 10 次)来进行优化,采用循环的方式去尝试获取锁 注意: @@ -1279,7 +1277,7 @@ public class SpinLock { 对相同对象多次加锁,导致线程发生多次重入,频繁的加锁操作就会导致性能损耗,可以使用锁粗化方式优化 -如果虚拟机探测到一串零碎的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部 +如果虚拟机探测到一串的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部 * 一些看起来没有加锁的代码,其实隐式的加了很多锁: @@ -1542,7 +1540,7 @@ class TestLiveLock { #### 基本使用 -需要获取对象锁后才可以调用`锁对象.wait()`,notify 随机唤醒一个线程,notifyAll 唤醒所有线程去竞争 CPU +需要获取对象锁后才可以调用 `锁对象.wait()`,notify 随机唤醒一个线程,notifyAll 唤醒所有线程去竞争 CPU Object 类 API: @@ -1553,6 +1551,8 @@ public final void wait():导致当前线程等待,直到另一个线程调用 public final native void wait(long timeout):有时限的等待, 到n毫秒后结束等待,或是被唤醒 ``` +说明:**wait 是挂起线程,需要唤醒的都是挂起操作**,阻塞线程可以自己去争抢锁,挂起的线程需要唤醒后去争抢锁 + 对比 sleep(): * 原理不同:sleep() 方法是属于 Thread 类,是线程用来控制自身流程的,使此线程暂停执行一段时间而把执行机会让给其他线程;wait() 方法属于 Object 类,用于线程间通信 @@ -1658,26 +1658,26 @@ public class demo { ### park-un -LockSupport 是用来创建锁和其他同步类的线程阻塞原语 +LockSupport 是用来创建锁和其他同步类的**线程原语** LockSupport 类方法: -* `LockSupport.park()`:暂停当前线程,原语 +* `LockSupport.park()`:暂停当前线程,挂起原语 * `LockSupport.unpark(暂停的线程对象)`:恢复某个线程的运行 ```java public static void main(String[] args) { Thread t1 = new Thread(() -> { - System.out.println("start...");//1 - Thread.sleep(1000);//Thread.sleep(3000) - //先park再unpark 和 先unpark再park效果一样,都会直接恢复线程的运行 - System.out.println("park...");//2 + System.out.println("start..."); //1 + Thread.sleep(1000);// Thread.sleep(3000) + // 先 park 再 unpark 和先 unpark 再 park 效果一样,都会直接恢复线程的运行 + System.out.println("park..."); //2 LockSupport.park(); System.out.println("resume...");//4 },"t1"); t1.start(); Thread.sleep(2000); - System.out.println("unpark...");//3 + System.out.println("unpark..."); //3 LockSupport.unpark(t1); } ``` @@ -1685,16 +1685,16 @@ public static void main(String[] args) { LockSupport 出现就是为了增强 wait & notify 的功能: * wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park、unpark 不需要 -* park & unpark 以线程为单位来阻塞和唤醒线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程 -* **park & unpark 可以先 unpark**,而 wait & notify 不能先 notify。类比生产消费,先消费发现有产品就消费,没有就等待;先生产就直接产生商品,然后线程直接消费 -* wait 会释放锁资源进入等待队列,park 不会释放锁资源,只负责阻塞当前线程,会释放 CPU +* park & unpark **以线程为单位**来阻塞和唤醒线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程 +* park & unpark 可以先 unpark,而 wait & notify 不能先 notify。类比生产消费,先消费发现有产品就消费,没有就等待;先生产就直接产生商品,然后线程直接消费 +* wait 会释放锁资源进入等待队列,**park 不会释放锁资源**,只负责阻塞当前线程,会释放 CPU -原理: +原理:类似生产者消费者 * 先 park: 1. 当前线程调用 Unsafe.park() 方法 2. 检查 _counter ,本情况为 0,这时获得 _mutex 互斥锁 - 3. 线程进入 _cond 条件变量阻塞 + 3. 线程进入 _cond 条件变量挂起 4. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1 5. 唤醒 _cond 条件变量中的 Thread_0,Thread_0 恢复运行,设置 _counter 为 0 @@ -1704,7 +1704,7 @@ LockSupport 出现就是为了增强 wait & notify 的功能: 1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1 2. 当前线程调用 Unsafe.park() 方法 - 3. 检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行,设置 _counter 为 0 + 3. 检查 _counter ,本情况为 1,这时线程无需挂起,继续运行,设置 _counter 为 0 ![](https://gitee.com/seazean/images/raw/master/Java/JUC-park原理2.png) @@ -1746,22 +1746,22 @@ LockSupport 出现就是为了增强 wait & notify 的功能: } ``` -不可变类线程安全:String、Integer 等都是不可变类,因为内部的状态不可以改变,因此方法是线程安全 +无状态类线程安全,就是没有成员变量的类 -* replace等方法底层是新建一个对象,复制过去 +不可变类线程安全:String、Integer 等都是不可变类,**内部的状态不可以改变**,所以方法是线程安全 + +* replace 等方法底层是新建一个对象,复制过去 ```java - Map map = new HashMap<>(); //线程不安全 - String S1 = "..."; //线程安全 - final String S2 = "..."; //线程安全 - Date D1 = new Date(); //线程不安全 - final Date D2 = new Date(); //线程不安全,final让D2引用的对象不能变,但对象的内容可以变 + Map map = new HashMap<>(); // 线程不安全 + String S1 = "..."; // 线程安全 + final String S2 = "..."; // 线程安全 + Date D1 = new Date(); // 线程不安全 + final Date D2 = new Date(); // 线程不安全,final让D2引用的对象不能变,但对象的内容可以变 ``` 抽象方法如果有参数,被重写后行为不确定可能造成线程不安全,被称之为外星方法:`public abstract foo(Student s);` -无状态类线程安全 - *** @@ -2234,9 +2234,9 @@ public static void main(String[] args) { #### 内存模型 -Java 内存模型是 Java MemoryModel(JMM),本身是一种**抽象的概念**,实际上并不存在,描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式 +Java 内存模型是 Java Memory Model(JMM),本身是一种**抽象的概念**,实际上并不存在,描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式 -作用: +JMM 作用: * 屏蔽各种硬件和操作系统的内存访问差异,实现让 Java 程序在各种平台下都能达到一致的内存访问效果 * 规定了线程和内存之间的一些关系 @@ -2250,13 +2250,10 @@ Java 内存模型是 Java MemoryModel(JMM),本身是一种**抽象的概 * 主内存:计算机的内存,也就是经常提到的 8G 内存,16G 内存,存储所有共享变量的值 * 工作内存:存储该线程使用到的共享变量在主内存的的值的副本拷贝 +**JVM 和 JMM 之间的关系**:JMM 中的主内存、工作内存与 JVM 中的 Java 堆、栈、方法区等并不是同一个层次的内存划分,这两者基本上是没有关系的,如果两者一定要勉强对应起来: - -**JVM 和 JMM 之间的关系**: - -* JMM 中的主内存、工作内存与 JVM 中的 Java 堆、栈、方法区等并不是同一个层次的内存划分,这两者基本上是没有关系的,如果两者一定要勉强对应起来: - * 主内存主要对应于 Java 堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域 - * 从更低层次上说,主内存直接对应于物理硬件的内存,工作内存对应寄存器和高速缓存 +* 主内存主要对应于 Java 堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域 +* 从更低层次上说,主内存直接对应于物理硬件的内存,工作内存对应寄存器和高速缓存 @@ -2270,7 +2267,7 @@ Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互 -* lock:将一个变量标识为被一个线程独占状态 +* lock:将一个变量标识为被一个线程**独占状态** * unclock:将一个变量从独占状态释放出来,释放后的变量才可以被其他线程锁定 * read:把一个变量的值从主内存传输到工作内存中 * load:在 read 之后执行,把 read 得到的值放入工作内存的变量副本中 @@ -2295,7 +2292,7 @@ Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互 可见性:是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值 -存在可见性问题的根本原因是由于缓存的存在,线程持有的是共享变量的副本,无法感知其他线程对于共享变量的更改,导致读取的值不是最新的 +存在不可见问题的根本原因是由于缓存的存在,线程持有的是共享变量的副本,无法感知其他线程对于共享变量的更改,导致读取的值不是最新的 main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止: @@ -2356,12 +2353,12 @@ CPU 的基本工作是执行存储的指令序列,即程序,程序的执行 源代码 -> 编译器优化的重排 -> 指令并行的重排 -> 内存系统的重排 -> 最终执行指令 ``` -现代 CPU 支持多级指令流水线,几乎所有的冯•诺伊曼型计算机的 CPU,其工作都可以分为 5 个阶段:取指令、指令译码、执行指令、访存取数和结果写回,可以称之为**五级指令流水线**。这时 CPU 可以在一个时钟周期内,同时运行五条指令的**不同阶段**(每个线程不同的阶段),本质上流水线技术并不能缩短单条指令的执行时间,但变相地提高了指令地吞吐率 +现代 CPU 支持多级指令流水线,几乎所有的冯•诺伊曼型计算机的 CPU,其工作都可以分为 5 个阶段:取指令、指令译码、执行指令、访存取数和结果写回,可以称之为**五级指令流水线**。CPU 可以在一个时钟周期内,同时运行五条指令的**不同阶段**(每个线程不同的阶段),本质上流水线技术并不能缩短单条指令的执行时间,但变相地提高了指令地吞吐率 处理器在进行重排序时,必须要考虑**指令之间的数据依赖性** -* 单线程环境也存在指令重排,由于存在依赖,最终执行结果和代码顺序的结果一致 -* 多线程环境中线程交替执行,由于编译器优化重排,两个线程中使用的变量能否保证一致性是无法确定的 +* 单线程环境也存在指令重排,由于存在依赖性,最终执行结果和代码顺序的结果一致 +* 多线程环境中线程交替执行,由于编译器优化重排,会获取其他线程处在不同阶段的指令同时执行 @@ -2383,19 +2380,19 @@ CPU 处理器速度远远大于在主内存中的,为了解决速度差异, -| 从 cpu 到 | 大约需要的时钟周期 | -| --------- | -------------------------------- | -| 寄存器 | 1 cycle (4GHz 的 CPU 约为0.25ns) | -| L1 | 3~4 cycle | -| L2 | 10~20 cycle | -| L3 | 40~45 cycle | -| 内存 | 120~240 cycle | +| 从 cpu 到 | 大约需要的时钟周期 | +| --------- | --------------------------------- | +| 寄存器 | 1 cycle (4GHz 的 CPU 约为 0.25ns) | +| L1 | 3~4 cycle | +| L2 | 10~20 cycle | +| L3 | 40~45 cycle | +| 内存 | 120~240 cycle | ##### 缓存使用 -当处理器发出内存访问请求时,会先查看缓存内是否有请求数据,如果存在(命中),则不经访问内存直接返回该数据;如果不存在(失效),则要先把内存中的相应数据载入缓存,再将其返回处理器 +当处理器发出内存访问请求时,会先查看缓存内是否有请求数据,如果存在(命中),则不用访问内存直接返回该数据;如果不存在(失效),则要先把内存中的相应数据载入缓存,再将其返回处理器 缓存之所以有效,主要因为程序运行时对内存的访问呈现局部性(Locality)特征。既包括空间局部性(Spatial Locality),也包括时间局部性(Temporal Locality),有效利用这种局部性,缓存可以达到极高的命中率。 @@ -2417,7 +2414,7 @@ CPU 处理器速度远远大于在主内存中的,为了解决速度差异, * padding:通过填充,让数据落在不同的 cache line 中 -* @Contended:原理参考 无锁 → Addr → 优化机制 → 伪共享 +* @Contended:原理参考 无锁 → Adder → 优化机制 → 伪共享 Linux 查看 CPU 缓存行: @@ -2434,7 +2431,7 @@ Linux 查看 CPU 缓存行: 缓存一致性:当多个处理器运算任务都涉及到同一块主内存区域的时候,将可能导致各自的缓存数据不一样 -**MESI**(Modified Exclusive Shared Or Invalid)是一种广泛使用的支持写回策略的缓存一致性协议,CPU 中每个缓存行(caceh line)使用 4 种状态进行标记(使用额外的两位 (bit) 表示): +MESI(Modified Exclusive Shared Or Invalid)是一种广泛使用的**支持写回策略的缓存一致性协议**,CPU 中每个缓存行(caceh line)使用 4 种状态进行标记(使用额外的两位 bit 表示): * M:被修改(Modified) @@ -2450,7 +2447,7 @@ Linux 查看 CPU 缓存行: * S:共享的(Shared) - 该状态意味着该缓存行可能被多个 CPU 缓存,并且各个缓存中的数据与主存数据一致 (clear),当有一个 CPU 修改该缓存行中,其它 CPU 中该缓存行变成无效状态 (Invalid) + 该状态意味着该缓存行可能被多个 CPU 缓存,并且各个缓存中的数据与主存数据一致,当有一个 CPU 修改该缓存行中,其它 CPU 中该缓存行变成无效状态 (Invalid) * I:无效的(Invalid) @@ -2514,8 +2511,10 @@ volatile 是 Java 虚拟机提供的**轻量级**的同步机制(三大特性 **volatile 修饰的变量,可以禁用指令重排** -**synchronized 无法禁止指令重排和处理器优化**,为什么可以保证有序性? -加了锁之后,只能有一个线程获得到了锁,获得不到锁的线程就要阻塞,所以同一时间只有一个线程执行,相当于单线程,而单线程的指令重排是没有问题的 +**synchronized 无法禁止指令重排和处理器优化**,为什么可以保证有序性可见性 + +* 加了锁之后,只能有一个线程获得到了锁,获得不到锁的线程就要阻塞,所以同一时间只有一个线程执行,相当于单线程,由于数据依赖性的存在,单线程的指令重排是没有问题的 +* 线程解锁前,必须把共享变量的最新值刷新到主内存中。线程加锁前,将清空工作内存中共享变量的值,使用共享变量时需要从主内存中重新读取最新的值 指令重排实例: @@ -2532,7 +2531,7 @@ volatile 是 Java 虚拟机提供的**轻量级**的同步机制(三大特性 执行顺序是:1 2 3 4、2 1 3 4、1 3 2 4 - 指令重排也有限制不会出现:4321,语句4需要依赖于 y 以及 x 的申明,因为存在数据依赖,无法首先执行 + 指令重排也有限制不会出现:4321,语句 4 需要依赖于 y 以及 x 的申明,因为存在数据依赖,无法首先执行 * example 2: @@ -2572,7 +2571,7 @@ volatile 是 Java 虚拟机提供的**轻量级**的同步机制(三大特性 使用 volatile 修饰的共享变量,总线会开启 **CPU 总线嗅探机制**来解决 JMM 缓存一致性问题,也就是共享变量在多线程中可见性的问题,实现 MESI 缓存一致性协议 -底层是通过汇编 lock 前缀指令,共享变量加了 lock 前缀指令,在线程修改完共享变量后,会马上执行 store 和 write 操作写回主存。在执行 store 操作前,会先执行**缓存锁定**的操作,其他的 CPU 上运行的线程根据 CPU 总线嗅探机制会修改其共享变量为失效状态,读取时会重新从主内存中读取最新的数据 +底层是通过汇编 lock 前缀指令,共享变量加了 lock 前缀指令,在线程修改完共享变量后,会马上执行 store 和 write 操作。在执行 store 操作前,会先执行**缓存锁定**的操作然后写回主存,其他的 CPU 上运行的线程根据 CPU 总线嗅探机制会修改其共享变量为失效状态,读取时会重新从主内存中读取最新的数据 lock 前缀指令就相当于内存屏障,Memory Barrier(Memory Fence) @@ -2635,12 +2634,12 @@ lock 前缀指令就相当于内存屏障,Memory Barrier(Memory Fence) i++ 反编译后的指令: ```java - 0: iconst_1 //当int取值 -1~5 时,JVM采用iconst指令将常量压入栈中 - 1: istore_1 //将操作数栈顶数据弹出,存入局部变量表的 slot 1 + 0: iconst_1 // 当int取值 -1~5 时,JVM采用iconst指令将常量压入栈中 + 1: istore_1 // 将操作数栈顶数据弹出,存入局部变量表的 slot 1 2: iinc 1, 1 ``` - + @@ -2654,7 +2653,7 @@ lock 前缀指令就相当于内存屏障,Memory Barrier(Memory Fence) Double-Checked Locking:双端检锁机制 -DCL(双端检锁)机制不一定是线程安全的,原因是有指令重排的存在,加入volatile可以禁止指令重排 +DCL(双端检锁)机制不一定是线程安全的,原因是有指令重排的存在,加入 volatile 可以禁止指令重排 ```java public final class Singleton { @@ -2679,7 +2678,7 @@ public final class Singleton { 不锁 INSTANCE 的原因: * INSTANCE 要重新赋值 -* INSTANCE 是 null,线程加锁之前需要获取对象的引用,null 没有引用 +* INSTANCE 是 null,线程加锁之前需要获取对象的引用,设置对象头,null 没有引用 实现特点: @@ -2727,11 +2726,10 @@ getInstance 方法对应的字节码为: * 21 表示利用一个对象引用,调用构造方法初始化对象 * 24 表示利用一个对象引用,赋值给 static INSTANCE -步骤 21 和步骤 24 之间不存在数据依赖关系,而且无论重排前后,程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的 +步骤 21 和 24 之间不存在数据依赖关系,而且无论重排前后,程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的 * 关键在于 0:getstatic 这行代码在 monitor 控制之外,可以越过 monitor 读取 INSTANCE 变量的值 -* 当其他线程访问 instance 不为 null 时,由于 instance 实例未必已初始化,那么 t2 拿到的是将是一个未初 - 始化完毕的单例返回,这就造成了线程安全的问题 +* 当其他线程访问 instance 不为 null 时,由于 instance 实例未必已初始化,那么 t2 拿到的是将是一个未初始化完毕的单例返回,这就造成了线程安全的问题 ![](https://gitee.com/seazean/images/raw/master/Java/JMM-DCL出现的问题.png) @@ -2761,30 +2759,30 @@ private static volatile SingletonDemo INSTANCE = null; happens-before 先行发生 -Java 内存模型具备一些先天的“有序性”,即不需要通过任何同步手段(volatile、synchronized等)就能够得到保证的安全,这个通常也称为 happens-before 原则,它是可见性与有序性的一套规则总结 +Java 内存模型具备一些先天的“有序性”,即不需要通过任何同步手段(volatile、synchronized 等)就能够得到保证的安全,这个通常也称为 happens-before 原则,它是可见性与有序性的一套规则总结 不符合 happens-before 规则,JMM 并不能保证一个线程的可见性和有序性 1. 程序次序规则 (Program Order Rule):一个线程内,逻辑上书写在前面的操作先行发生于书写在后面的操作 ,因为多个操作之间有先后依赖关系,则不允许对这些操作进行重排序 -2. 锁定规则 (Monitor Lock Rule):一个 unLock 操作先行发生于后面(时间的先后)对同一个锁的 lock 操作,所以线程解锁 m 之前对变量的写(锁内的写),对于接下来对 m 加锁的其它线程对该变量的读可见 +2. 锁定规则 (Monitor Lock Rule):一个 unlock 操作先行发生于后面(时间的先后)对同一个锁的 lock 操作,所以线程解锁 m 之前对变量的写(解锁前会刷新到主内存中),对于接下来对 m 加锁的其它线程对该变量的读可见 -3. **volatile变量规则** (Volatile Variable Rule):对 volatile 变量的写操作先行发生于后面对这个变量的读 +3. **volatile 变量规则** (Volatile Variable Rule):对 volatile 变量的写操作先行发生于后面对这个变量的读 -4. 传递规则 (Transitivity):具有传递性,如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C +4. 传递规则 (Transitivity):具有传递性,如果操作 A 先行发生于操作 B,而操作 B 又先行发生于操作 C,则可以得出操作 A 先行发生于操作 C -5. 线程启动规则 (Thread Start Rule):Thread对象的start()方法先行发生于此线程中的每一个操作 +5. 线程启动规则 (Thread Start Rule):Thread 对象的 start()方 法先行发生于此线程中的每一个操作 ```java static int x = 10;//线程 start 前对变量的写,对该线程开始后对该变量的读可见 new Thread(()->{ System.out.println(x); },"t1").start(); ``` -6. 线程中断规则 (Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生 +6. 线程中断规则 (Thread Interruption Rule):对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生 -7. 线程终止规则 (Thread Termination Rule):线程中所有的操作都先行发生于线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行 +7. 线程终止规则 (Thread Termination Rule):线程中所有的操作都先行发生于线程的终止检测,可以通过 Thread.join() 方法结束、Thread.isAlive() 的返回值手段检测到线程已经终止执行 -8. 对象终结规则(Finaizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始 +8. 对象终结规则(Finaizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始 @@ -2800,12 +2798,12 @@ Java 内存模型具备一些先天的“有序性”,即不需要通过任何 ```java class TwoPhaseTermination { - //监控线程 + // 监控线程 private Thread monitor; - //停止标记 + // 停止标记 private volatile boolean stop = false;; - //启动监控线程 + // 启动监控线程 public void start() { monitor = new Thread(() -> { while (true) { @@ -2815,7 +2813,7 @@ class TwoPhaseTermination { break; } try { - Thread.sleep(1000);//睡眠 + Thread.sleep(1000);// 睡眠 System.out.println(thread.getName() + "执行监控记录"); } catch (InterruptedException e) { System.out.println("被打断,退出睡眠"); @@ -2825,13 +2823,13 @@ class TwoPhaseTermination { monitor.start(); } - //停止监控线程 + // 停止监控线程 public void stop() { stop = true; - monitor.interrupt();//让线程尽快退出Timed Waiting + monitor.interrupt();// 让线程尽快退出Timed Waiting } } -//测试 +// 测试 public static void main(String[] args) throws InterruptedException { TwoPhaseTermination tpt = new TwoPhaseTermination(); tpt.start(); @@ -2849,8 +2847,7 @@ public static void main(String[] args) throws InterruptedException { #### Balking -Balking (犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做 -了,直接结束返回 +Balking (犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回 ```java public class MonitorService { @@ -2913,13 +2910,13 @@ public class TestVolatile { CAS 的全称是 Compare-And-Swap,是 **CPU 并发原语** * CAS 并发原语体现在 Java 语言中就是 sun.misc.Unsafe 类的各个方法,调用 UnSafe 类中的 CAS 方法,JVM 会实现出 CAS 汇编指令,这是一种完全依赖于硬件的功能,实现了原子操作 -* CAS 是一种系统原语,原语属于操作系统范畴,是由若干条指令组成 ,用于完成某个功能的一个过程,并且**原语的执行必须是连续的,执行过程中不允许被中断**,也就是说 CAS 是一条 CPU 的原子指令,不会造成数据不一致的问题,所以 CAS 是线程安全的 +* CAS 是一种系统原语,原语属于操作系统范畴,是由若干条指令组成 ,用于完成某个功能的一个过程,并且原语的执行必须是连续的,执行过程中不允许被中断,所以 CAS 是一条 CPU 的原子指令,不会造成数据不一致的问题,是线程安全的 底层原理:CAS 的底层是 `lock cmpxchg` 指令(X86 架构),在单核和多核 CPU 下都能够保证比较交换的原子性 * 程序是在单核处理器上运行,会省略 lock 前缀,单处理器自身会维护处理器内的顺序一致性,不需要 lock 前缀的内存屏障效果 -* 程序是在多核处理器上运行,会为 cmpxchg 指令加上 lock 前缀(lock cmpxchg)。当某个核执行到带 lock 的指令时,CPU 会执行**总线锁定或缓存锁定**,当这个核把此指令执行完毕,这个过程不会被线程的调度机制所打断,保证了多个线程对内存操作的原子性 +* 程序是在多核处理器上运行,会为 cmpxchg 指令加上 lock 前缀。当某个核执行到带 lock 的指令时,CPU 会执行**总线锁定或缓存锁定**,将修改的变量写入到主存,这个过程不会被线程的调度机制所打断,保证了多个线程对内存操作的原子性 作用:比较当前工作内存中的值和主物理内存中的值,如果相同则执行规定操作,否者继续比较直到主内存和工作内存的值一致为止 @@ -2933,7 +2930,7 @@ CAS 缺点: - 循环时间长,开销大,因为执行的是循环操作,如果比较不成功一直在循环,最差的情况某个线程一直取到的值和预期值都不一样,就会无限循环导致饥饿,**使用 CAS 线程数不要超过 CPU的 核心数** - 只能保证一个共享变量的原子操作 - 对于一个共享变量执行操作时,可以通过循环 CAS 的方式来保证原子操作 - - 对于多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候只能用锁来保证原子性 + - 对于多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候**只能用锁来保证原子性** - 引出来 ABA 问题 @@ -2946,12 +2943,10 @@ CAS 缺点: #### 乐观锁 -CAS 与 Synchronized 总结: +CAS 与 synchronized 总结: -* Synchronized 是从悲观的角度出发: - 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。(**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**),因此 Synchronized 我们也将其称之为悲观锁。jdk中的ReentrantLock也是一种悲观锁,**性能较差** -* CAS 是从乐观的角度出发: - 总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。**如果别人修改过,则获取现在最新的值。如果别人没修改过,直接修改共享数据的值**,CAS 这种机制我们也可以将其称之为乐观锁。**综合性能较好** +* synchronized 是从悲观的角度出发:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞(**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**),因此 synchronized 也称之为悲观锁,ReentrantLock 也是一种悲观锁,**性能较差** +* CAS 是从乐观的角度出发:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。**如果别人修改过,则获取现在最新的值,如果别人没修改过,直接修改共享数据的值**,CAS 这种机制也称之为乐观锁,**综合性能较好** @@ -2990,7 +2985,7 @@ CAS 与 Synchronized 总结: #### 原理分析 -**AtomicInteger原理**:自旋锁 + CAS 算法 +**AtomicInteger 原理**:自旋锁 + CAS 算法 CAS 算法:有 3 个操作数(内存值 V, 旧的预期值 A,要修改的值 B) @@ -3023,11 +3018,11 @@ CAS 算法:有 3 个操作数(内存值 V, 旧的预期值 A,要修改 * unsafe类: ```java - //val1: AtomicInteger对象本身 var2: 该对象值得引用地址 var4: 需要变动的数 + // val1: AtomicInteger对象本身,var2: 该对象值得引用地址,var4: 需要变动的数 public final int getAndSetInt(Object var1, long var2, int var4) { int var5; do { - //var5: 用var1和var2找到的内存中的真实值 + // var5: 用 var1 和 var2 找到的内存中的真实值 var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var4)); @@ -3037,7 +3032,7 @@ CAS 算法:有 3 个操作数(内存值 V, 旧的预期值 A,要修改 var5:从主内存中拷贝到工作内存中的值(每次都要从主内存拿到最新的值到本地内存),然后执行 `compareAndSwapInt()` 再和主内存的值进行比较,假设方法返回 false,那么就一直执行 while 方法,直到期望的值和真实值一样,修改数据 -* 变量 value 用 volatile 修饰,保证了多线程之间的内存可见性,避免线程从自己的工作缓存中查找变量 +* 变量 value 用 volatile 修饰,保证了多线程之间的内存可见性,避免线程从工作缓存中获取失效的变量 ```java private volatile int value @@ -3099,23 +3094,23 @@ AtomicReference 类: * 构造方法:`AtomicReference atomicReference = new AtomicReference()` * 常用 API: - `public final boolean compareAndSet(V expectedValue, V newValue)`:CAS 操作 - `public final void set(V newValue)`:将值设置为 newValue - `public final V get()`:返回当前值 + * `public final boolean compareAndSet(V expectedValue, V newValue)`:CAS 操作 + * `public final void set(V newValue)`:将值设置为 newValue + * `public final V get()`:返回当前值 ```java public class AtomicReferenceDemo { public static void main(String[] args) { - Student s1 = new Student(33,"z3"); + Student s1 = new Student(33, "z3"); - //创建原子引用包装类 + // 创建原子引用包装类 AtomicReference atomicReference = new AtomicReference<>(); - //设置主内存共享变量为s1 + // 设置主内存共享变量为s1 atomicReference.set(s1); - //比较并交换,如果现在主物理内存的值为z3,那么交换成l4 + // 比较并交换,如果现在主物理内存的值为 z3,那么交换成 l4 while (true) { - Student s2 = new Student(44,"l4"); + Student s2 = new Student(44, "l4"); if (atomicReference.compareAndSet(s1, s2)) { break; } @@ -3166,7 +3161,7 @@ public final boolean compareAndSet(int i, int expect, int update) { 利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常 `IllegalArgumentException: Must be volatile type` -常用API: +常用 API: * `static AtomicIntegerFieldUpdater newUpdater(Class c, String fieldName)`:构造方法 * `abstract boolean compareAndSet(T obj, int expect, int update)`:CAS @@ -3193,7 +3188,7 @@ public class UpdateDemo { #### 原子累加器 -原子累加器类:LongAdder、DoubleAdder、LongAccumulator、DoubleAccumulator +原子累加器类:LongAdder、DoubleAdder、LongAccumulator、DoubleAccumulator LongAdder 和 LongAccumulator 区别: @@ -3225,7 +3220,7 @@ LongAdder 是 Java8 提供的类,跟 AtomicLong 有相同的效果,但对 CA CAS 底层实现是在一个循环中不断地尝试修改目标值,直到修改成功。如果竞争不激烈修改成功率很高,否则失败率很高,失败后这些重复的原子性操作会耗费性能(导致大量线程**空循环,自旋转**) -优化核心思想:数据分离,将 AtomicLong 的**单点的更新压力分担到各个节点**,空间换时间,在低并发的时候直接更新,可以保障和 AtomicLong 的性能基本一致,而在高并发的时候通过分散提高了性能 +优化核心思想:数据分离,将 AtomicLong 的**单点的更新压力分担到各个节点,空间换时间**,在低并发的时候直接更新,可以保障和 AtomicLong 的性能基本一致,而在高并发的时候通过分散减少竞争,提高了性能 **分段 CAS 机制**: @@ -3246,10 +3241,11 @@ CAS 底层实现是在一个循环中不断地尝试修改目标值,直到修 Cell 为累加单元:数组访问索引是通过 Thread 里的 threadLocalRandomProbe 域取模实现的,这个域是 ThreadLocalRandom 更新的 ```java +// Striped64.Cell @sun.misc.Contended static final class Cell { volatile long value; Cell(long x) { value = x; } - // 最重要的方法, 用来 cas 方式进行累加, prev 表示旧值, next 表示新值 + // 用 cas 方式进行累加, prev 表示旧值, next 表示新值 final boolean cas(long prev, long next) { return UNSAFE.compareAndSwapLong(this, valueOffset, prev, next); } @@ -3257,7 +3253,7 @@ Cell 为累加单元:数组访问索引是通过 Thread 里的 threadLocalRand } ``` -Cell 是数组形式,**在内存中是连续存储的**,64 位系统中,一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value),每一个 cache line 为 64 字节,因此缓存行可以存下 2 个的 Cell 对象,当 Core-0 要修改 Cell[0]、Core-1 要修改 Cell[1],无论谁修改成功都会导致当前缓存行失效,导致对方的数据失效,需要重新去主存获取 +Cell 是数组形式,**在内存中是连续存储的**,64 位系统中,一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value),每一个 cache line 为 64 字节,因此缓存行可以存下 2 个的 Cell 对象,当 Core-0 要修改 Cell[0]、Core-1 要修改 Cell[1],无论谁修改成功都会导致当前缓存行失效,从而导致对方的数据失效,需要重新去主存获取,影响效率 ![](https://gitee.com/seazean/images/raw/master/Java/JUC-伪共享1.png) @@ -3288,7 +3284,15 @@ transient volatile long base; transient volatile int cellsBusy; ``` -Cells 占用内存是相对比较大的,是惰性加载的,在无竞争的情况下直接更新 base 域,在第一次发生竞争的时候(CAS 失败)就会创建一个大小为 2 的 cells 数组,进行分段累加。如果是更新当前线程对应的 cell 槽位时出现的竞争,就会重新计算线程对应的槽位,继续自旋尝试修改。分段迁移后还出现竞争就会扩容 cells 数组长度为原来的两倍,然后重新 rehash,**数组长度总是 2 的 n 次幂** +工作流程: + +* cells 占用内存是相对比较大的,是惰性加载的,在无竞争或者其他线程正在初始化 cells 数组的情况下,直接更新 base 域 + +* 在第一次发生竞争时(casBase 失败)会创建一个大小为 2 的 cells 数组,将当前累加的值包装为 Cell 对象,放入映射的槽位上 +* 分段累加的过程中,如果当前线程对应的 cells 槽位为空,就会新建 Cell 填充,如果出现竞争,就会重新计算线程对应的槽位,继续自旋尝试修改 +* 分段迁移后还出现竞争就会扩容 cells 数组长度为原来的两倍,然后 rehash,**数组长度总是 2 的 n 次幂**,默认最大为 CPU 核数,但是可以超过,如果核数是 6 核,数组最长是 8 + +方法分析: * LongAdder#add:累加方法 @@ -3299,23 +3303,22 @@ Cells 占用内存是相对比较大的,是惰性加载的,在无竞争的 Cell[] as; long b, v; int m; Cell a; // cells 不为空说明 cells 已经被初始化,线程发生了竞争,去更新对应的 cell 槽位 - // 为空说明没有初始化,条件为 fasle,进入 || 后的逻辑去更新 base 域,更新失败表示发生竞争进入条件 + // 进入 || 后的逻辑去更新 base 域,更新失败表示发生竞争进入条件 if ((as = cells) != null || !casBase(b = base, b + x)) { - // uncontended 为 true 表示 cell 没有竞争,false 表示发生竞争 + // uncontended 为 true 表示 cell 没有竞争 boolean uncontended = true; // 条件一: true 说明 cells 未初始化,多线程写 base 发生竞争需要进行初始化 cells 数组 // fasle 说明 cells 已经初始化,进行下一个条件寻找自己的 cell 去累加 // 条件二: getProbe() 获取 hash 值,& m 的逻辑和 HashMap 的逻辑相同,保证散列的均匀性 // true 说明当前线程对应下标的 cell 为空,需要创建 cell - // false 说明当前线程对应的 cell 不为空,进行下一个条件想要将 x 值累加到 cell 中 - // 条件三: true 说明 cas 失败,当前线程对应的 cell 有竞争 - // false 说明 cas 成功,可以直接返回 + // false 说明当前线程对应的 cell 不为空,进行下一个条件【将 x 值累加到对应的 cell 中】 + // 条件三: 有取反符号,false 说明 cas 成功,直接返回,true 说明失败,当前线程对应的 cell 有竞争 if (as == null || (m = as.length - 1) < 0 || (a = as[getProbe() & m]) == null || !(uncontended = a.cas(v = a.value, v + x))) longAccumulate(x, null, uncontended); - // uncontended 在对应的 cell 上累加失败的时候才为 false,其余情况均为 true + // 【uncontended 在对应的 cell 上累加失败的时候才为 false,其余情况均为 true】 } } ``` @@ -3334,7 +3337,7 @@ Cells 占用内存是相对比较大的,是惰性加载的,在无竞争的 // 默认情况下 当前线程肯定是写入到了 cells[0] 位置,不把它当做一次真正的竞争 wasUncontended = true; } - // 表示扩容意向,false 一定不会扩容,true 可能会扩容 + // 表示【扩容意向】,false 一定不会扩容,true 可能会扩容 boolean collide = false; //自旋 for (;;) { @@ -3342,7 +3345,7 @@ Cells 占用内存是相对比较大的,是惰性加载的,在无竞争的 Cell[] as; Cell a; int n; long v; // 【CASE1】: 表示 cells 已经初始化了,当前线程应该将数据写入到对应的 cell 中 if ((as = cells) != null && (n = as.length) > 0) { - // CASE1.1: true 表示当前线程对应的下标位置的 cell 为 null,需要创建 new Cell + // CASE1.1: true 表示当前线程对应的索引下标的 Cell 为 null,需要创建 new Cell if ((a = as[(n - 1) & h]) == null) { // 判断 cellsBusy 是否被锁 if (cellsBusy == 0) { @@ -3350,48 +3353,47 @@ Cells 占用内存是相对比较大的,是惰性加载的,在无竞争的 Cell r = new Cell(x); // 加锁 if (cellsBusy == 0 && casCellsBusy()) { - // 是否创建成功的标记,进入【创建 cell 逻辑】 + // 创建成功标记,进入【创建 cell 逻辑】 boolean created = false; try { Cell[] rs; int m, j; - // 把当前 cells 数组赋值给 rs,并且不为n ull + // 把当前 cells 数组赋值给 rs,并且不为 null if ((rs = cells) != null && (m = rs.length) > 0 && // 再次判断防止其它线程初始化过该位置,当前线程再次初始化该位置会造成数据丢失 - // 因为这里是线程安全的,这里判断后进行的逻辑不会被其他线程影响 + // 因为这里是线程安全的判断,进行的逻辑不会被其他线程影响 rs[j = (m - 1) & h] == null) { // 把新创建的 cell 填充至当前位置 rs[j] = r; created = true; // 表示创建完成 } } finally { - cellsBusy = 0;//解锁 + cellsBusy = 0; // 解锁 } - if (created) // true 表示创建完成,可以推出循环了 - break; // 成功则 break, 否则继续 continue 循环 + if (created) // true 表示创建完成,可以推出循环了 + break; continue; } } collide = false; } - // CASE1.2: 线程对应的 cell 有竞争, 改变线程对应的 cell 来重试 cas + // CASE1.2: 条件成立说明线程对应的 cell 有竞争, 改变线程对应的 cell 来重试 cas else if (!wasUncontended) wasUncontended = true; - // CASE 1.3: 当前线程 rehash 过,尝试新命中的 cell 不为空去累加 - // true 表示写成功,退出循环,false 表示 rehash 之后命中的新 cell 也有竞争 + // CASE 1.3: 当前线程 rehash 过,如果新命中的 cell 不为空,就尝试累加,false 说明新命中也有竞争 else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) break; // CASE 1.4: cells 长度已经超过了最大长度 CPU 内核的数量或者已经扩容 else if (n >= NCPU || cells != as) - collide = false; // 扩容意向改为false,表示不扩容了 - // CASE 1.5: 更改扩容意向 + collide = false; // 扩容意向改为false,【表示不能扩容了】 + // CASE 1.5: 更改扩容意向,如果 n >= NCPU,这里就永远不会执行到,case1.4 永远先于 1.5 执行 else if (!collide) collide = true; // CASE 1.6: 【扩容逻辑】,进行加锁 else if (cellsBusy == 0 && casCellsBusy()) { try { - // 再次检查,防止期间被其他线程扩容了 + // 线程安全的检查,防止期间被其他线程扩容了 if (cells == as) { // 扩容为以前的 2 倍 Cell[] rs = new Cell[n << 1]; @@ -3402,53 +3404,52 @@ Cells 占用内存是相对比较大的,是惰性加载的,在无竞争的 cells = rs; } } finally { - cellsBusy = 0; //解锁 + cellsBusy = 0; // 解锁 } - collide = false; // 扩容意向改为false,表示不扩容了 + collide = false; // 扩容意向改为 false,表示不扩容了 continue; } - // 重置当前线程 Hash 值,这就是【分段迁移机制】,case1.3 + // 重置当前线程 Hash 值,这就是【分段迁移机制】 h = advanceProbe(h); } // 【CASE2】: 运行到这说明 cells 还未初始化,as 为null - // 条件一: true 表示当前未加锁 - // 条件二: 其它线程可能会在当前线程给 as 赋值之后修改了 cells,这里需要判断(这里不是线程安全的判断) - // 条件三: true 表示加锁成功 + // 判断是否没有加锁,没有加锁就用 CAS 加锁 + // 条件二判断是否其它线程在当前线程给 as 赋值之后修改了 cells,这里不是线程安全的判断 else if (cellsBusy == 0 && cells == as && casCellsBusy()) { - // 初始化标志,这里就进行 【初始化 cells 数组】 + // 初始化标志,开始 【初始化 cells 数组】 boolean init = false; try { - // 再次判断 cells == as 防止其它线程已经初始化了,当前线程再次初始化导致丢失数据 - // 因为这里是线程安全的,所以重新检查,经典DCL + // 再次判断 cells == as 防止其它线程已经提前初始化了,当前线程再次初始化导致丢失数据 + // 因为这里是【线程安全的,重新检查,经典 DCL】 if (cells == as) { - Cell[] rs = new Cell[2];//初始化数组大小为2 - rs[h & 1] = new Cell(x);//填充线程对应的cell + Cell[] rs = new Cell[2]; // 初始化数组大小为2 + rs[h & 1] = new Cell(x); // 填充线程对应的cell cells = rs; - init = true; //初始化成功 + init = true; // 初始化成功,标记置为 true } } finally { - cellsBusy = 0; //解锁啊 + cellsBusy = 0; // 解锁啊 } if (init) - break; //初始化成功直接返回 + break; // 初始化成功直接跳出自旋 } - // 【CASE3】: 运行到这说明其他线程在初始化 cells,所以当前线程将值累加到 base,累加成功直接结束自旋 + // 【CASE3】: 运行到这说明其他线程在初始化 cells,当前线程将值累加到 base,累加成功直接结束自旋 else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) break; } } ``` - -* sum:获取最终结果通过 sum 整合,保持最终一致性,不保证强一致性 + +* sum:获取最终结果通过 sum 整合,**保证最终一致性,不保证强一致性** ```java public long sum() { Cell[] as = cells; Cell a; long sum = base; if (as != null) { - //遍历 累加 + // 遍历 累加 for (int i = 0; i < as.length; ++i) { if ((a = as[i]) != null) sum += a.value; @@ -3470,16 +3471,16 @@ Cells 占用内存是相对比较大的,是惰性加载的,在无竞争的 ABA 问题:当进行获取主内存值时,该内存值在写入主内存时已经被修改了 N 次,但是最终又改成原来的值 -其他线程先把 A 改成 B 又改回 A,主线程仅能判断出共享变量的值与最初值 A 是否相同,不能感知到这种从 A 改为 B 又 改回 A 的情况,这时 CAS 虽然成功,但是过程存在问题 +其他线程先把 A 改成 B 又改回 A,主线程**仅能判断出共享变量的值与最初值 A 是否相同**,不能感知到这种从 A 改为 B 又 改回 A 的情况,这时 CAS 虽然成功,但是过程存在问题 * 构造方法: - `public AtomicStampedReference(V initialRef, int initialStamp)`:初始值和初始版本号 + * `public AtomicStampedReference(V initialRef, int initialStamp)`:初始值和初始版本号 * 常用API: - ` public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)`:期望引用和期望版本号都一致才进行 CAS 修改数据 - `public void set(V newReference, int newStamp)`:设置值和版本号 - `public V getReference()`:返回引用的值 - `public int getStamp()`:返回当前版本号 + * ` public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)`:期望引用和期望版本号都一致才进行 CAS 修改数据 + * `public void set(V newReference, int newStamp)`:设置值和版本号 + * `public V getReference()`:返回引用的值 + * `public int getStamp()`:返回当前版本号 ```java public static void main(String[] args) { @@ -3593,15 +3594,15 @@ public class TestFinal { 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: aload_0 -5: bipush 20 //将值直接放入栈中 -7: putfield #2 // Field a:I +5: bipush 20 // 将值直接放入栈中 +7: putfield #2 // Field a:I <-- 写屏障 10: return ``` final 变量的赋值通过 putfield 指令来完成,在这条指令之后也会加入写屏障,保证在其它线程读到它的值时不会出现为 0 的情况 -其他线程访问 final 修饰的变量会复制一份放入栈中,效率更高 +其他线程访问 final 修饰的变量**会复制一份放入栈中**,效率更高 @@ -3617,7 +3618,7 @@ final 变量的赋值通过 putfield 指令来完成,在这条指令之后也 String 类也是不可变的,该类和类中所有属性都是 final 的 -* 类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性保 +* 类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性 * 无写入方法(set)确保外部不能对内部属性进行修改 @@ -3632,7 +3633,7 @@ String 类也是不可变的,该类和类中所有属性都是 final 的 } ``` -* 更改 String 类数据时,会构造新字符串对象,生成新的 char[] value,通过创建副本对象来避免共享的方式称之为**保护性拷贝** +* 更改 String 类数据时,会构造新字符串对象,生成新的 char[] value,通过**创建副本对象来避免共享的方式称之为保护性拷贝** @@ -3658,7 +3659,7 @@ Servlet 为了保证其线程安全,一般不为 Servlet 设置成员变量, #### 基本介绍 -ThreadLocal 类用来提供线程内部的局部变量,这种变量在多线程环境下访问(通过 get 和 set 方法访问)时能保证各个线程的变量相对独立于其他线程内的变量,分配在 TLAB +ThreadLocal 类用来提供线程内部的局部变量,这种变量在多线程环境下访问(通过 get 和 set 方法访问)时能保证各个线程的变量相对独立于其他线程内的变量,分配在堆内的 TLAB 中 ThreadLocal 实例通常来说都是 `private static` 类型的,属于一个线程的本地变量,用于关联线程和线程上下文。每个线程都会在 ThreadLocal 中保存一份该线程独有的数据,所以是线程安全的 @@ -3674,7 +3675,7 @@ ThreadLocal 作用: | | synchronized | ThreadLocal | | ------ | ------------------------------------------------------------ | ------------------------------------------------------------ | -| 原理 | 同步机制采用**以时间换空间**的方式,只提供了一份变量,让不同的线程排队访问 | ThreadLocal采用**以空间换时间**的方式,为每个线程都提供了一份变量的副本,从而实现同时访问而相不干扰 | +| 原理 | 同步机制采用**以时间换空间**的方式,只提供了一份变量,让不同的线程排队访问 | ThreadLocal 采用**以空间换时间**的方式,为每个线程都提供了一份变量的副本,从而实现同时访问而相不干扰 | | 侧重点 | 多个线程之间访问资源的同步 | 多线程中让每个线程之间的数据相互隔离 | @@ -3739,17 +3740,18 @@ public class MyDemo { ##### 应用场景 -ThreadLocal 适用于如下两种场景 +ThreadLocal 适用于下面两种场景: - 每个线程需要有自己单独的实例 - 实例需要在多个方法中共享,但不希望被多线程共享 -**事务管理**,ThreadLocal 方案有两个突出的优势: +ThreadLocal 方案有两个突出的优势: 1. 传递数据:保存每个线程绑定的数据,在需要的地方可以直接获取,避免参数直接传递带来的代码耦合问题 - 2. 线程隔离:各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失 +ThreadLocal 用于数据连接的事务管理: + ```java public class JdbcUtils { // ThreadLocal对象,将connection绑定在当前线程中 @@ -3813,7 +3815,7 @@ JDK8 以后:每个 Thread 维护一个 ThreadLocalMap,这个 Map 的 key 是 * 每个 Thread 线程内部都有一个 Map (ThreadLocalMap) * Map 里面存储 ThreadLocal 对象(key)和线程的变量副本(value) -* Thread 内部的 Map 是由 ThreadLocal 维护的,由 ThreadLocal 负责向 map 获取和设置线程的变量值。 +* Thread 内部的 Map 是由 ThreadLocal 维护的,由 ThreadLocal 负责向 map 获取和设置线程的变量值 * 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成副本的隔离,互不干扰 ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ThreadLocal数据结构JDK8后.png) @@ -3831,7 +3833,7 @@ JDK8 前后对比: ##### 成员变量 -* Thread 类的相关属性:每一个线程持有一个 ThreadLocalMap 对象,存放由 ThreadLocal 和数据组成的 Entry 键值对 +* Thread 类的相关属性:**每一个线程持有一个 ThreadLocalMap 对象**,存放由 ThreadLocal 和数据组成的 Entry 键值对 ```java ThreadLocal.ThreadLocalMap threadLocals = null @@ -3851,7 +3853,7 @@ JDK8 前后对比: private static AtomicInteger nextHashCode = new AtomicInteger() ``` -* **斐波那契数**也叫黄金分割数,hash 的增量就是这个数字,带来的好处是 hash 分布非常均匀: +* 斐波那契数也叫黄金分割数,hash 的**增量**就是这个数字,带来的好处是 hash 分布非常均匀: ```java private static final int HASH_INCREMENT = 0x61c88647 @@ -3865,11 +3867,11 @@ JDK8 前后对比: ##### 成员方法 -方法都是线程安全的,因为 ThreadLocal 只属于一个线程 +方法都是线程安全的,因为 ThreadLocal 属于一个线程的,ThreadLocal 中的方法,逻辑都是获取当前线程维护的 ThreadLocalMap 对象,然后进行数据的增删改查,没有指定初始值的 threadlcoal 对象默认赋值为 null * initialValue():返回该线程局部变量的初始值 - * 延迟调用的方法,在 set 方法还未调用而先调用了 get 方法时才执行,并且仅执行 1 次 + * 延迟调用的方法,在执行 get 方法时才执行 * 该方法缺省(默认)实现直接返回一个 null * 如果想要一个初始值,可以重写此方法, 该方法是一个 `protected` 的方法,为了让子类覆盖而设计的 @@ -3888,13 +3890,13 @@ JDK8 前后对比: } ``` -* set():修改当前线程与当前 threadLocal 对象相关联的线程局部变量 +* set():修改当前线程与当前 threadlocal 对象相关联的线程局部变量 ```java public void set(T value) { // 获取当前线程对象 Thread t = Thread.currentThread(); - // 获取此线程对象中维护的ThreadLocalMap对象 + // 获取此线程对象中维护的 ThreadLocalMap 对象 ThreadLocalMap map = getMap(t); // 判断 map 是否存在 if (map != null) @@ -3907,13 +3909,13 @@ JDK8 前后对比: ``` ```java - // 获取当前线程 Thread 对应维护的ThreadLocalMap + // 获取当前线程 Thread 对应维护的 ThreadLocalMap ThreadLocalMap getMap(Thread t) { return t.threadLocals; } // 创建当前线程Thread对应维护的ThreadLocalMap void createMap(Thread t, T firstValue) { - //这里的 this 是调用此方法的 threadLocal,创建一个新的 Map 并设置第一个数据 + // 【这里的 this 是调用此方法的 threadLocal】,创建一个新的 Map 并设置第一个数据 t.threadLocals = new ThreadLocalMap(this, firstValue); } ``` @@ -3937,7 +3939,7 @@ JDK8 前后对比: } /*有两种情况有执行当前代码 第一种情况: map 不存在,表示此线程没有维护的 ThreadLocalMap 对象 - 第二种情况: map 存在, 但是没有与当前 ThreadLocal 关联的 entry*/ + 第二种情况: map 存在, 但是【没有与当前 ThreadLocal 关联的 entry】,就会设置为默认值 */ // 初始化当前线程与当前 threadLocal 对象相关联的 value return setInitialValue(); } @@ -3951,7 +3953,7 @@ JDK8 前后对比: ThreadLocalMap map = getMap(t); // 判断 map 是否初始化过 if (map != null) - // 存在则调用 map.set 设置此实体 entry + // 存在则调用 map.set 设置此实体 entry,value 是默认的值 map.set(this, value); else // 调用 createMap 进行 ThreadLocalMap 对象的初始化中 @@ -4010,6 +4012,7 @@ private int threshold; static class Entry extends WeakReference> { Object value; Entry(ThreadLocal k, Object v) { + // this.referent = referent = key; super(k); value = v; } @@ -4024,7 +4027,7 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; // 【寻址算法】计算索引 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); - // 创建 entry 对象 存放到指定位置的 slot 中 + // 创建 entry 对象,存放到指定位置的 slot 中 table[i] = new Entry(firstKey, firstValue); // 数据总量是 1 size = 1; @@ -4045,33 +4048,35 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { * set():添加数据,ThreadLocalMap 使用**线性探测法来解决哈希冲突** - * 该方法会一直探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出 - * 在探测过程中 ThreadLocal 会占用 key 为 null,value 不为 null 的脏 Entry 对象,防止内存泄漏 - * 假设当前 table 长度为16,计算出来 key 的 hash 值为 14,如果 table[14] 上已经有值,并且其 key 与当前 key 不一致,那么就发生了 hash 冲突,这个时候将 14 加 1 得到 15,取 table[15] 进行判断,如果还是冲突会回到 0,取 table[0],以此类推,直到可以插入,可以把 Entry[] table 看成一个**环形数组** + * 该方法会一直探测下一个地址,直到有空的地址后插入,若插入后 Map 数量超过阈值,数组会扩容为原来的 2 倍 + + 假设当前 table 长度为16,计算出来 key 的 hash 值为 14,如果 table[14] 上已经有值,并且其 key 与当前 key 不一致,那么就发生了 hash 冲突,这个时候将 14 加 1 得到 15,取 table[15] 进行判断,如果还是冲突会回到 0,取 table[0],以此类推,直到可以插入,可以把 Entry[] table 看成一个**环形数组** * 线性探测法会出现**堆积问题**,可以采取平方探测法解决 + * 在探测过程中 ThreadLocal 会复用 key 为 null 的脏 Entry 对象,并进行垃圾清理,防止出现内存泄漏 + ```java private void set(ThreadLocal key, Object value) { // 获取散列表 ThreadLocal.ThreadLocalMap.Entry[] tab = table; int len = tab.length; - // 计算当前 key 在散列表中的对应的位置 + // 哈希寻址 int i = key.threadLocalHashCode & (len-1); // 使用线性探测法向后查找元素,碰到 entry 为空时停止探测 for (ThreadLocal.ThreadLocalMap.Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { // 获取当前元素 key ThreadLocal k = e.get(); - // ThreadLocal 对应的 key 存在,直接覆盖之前的值 + // ThreadLocal 对应的 key 存在,【直接覆盖之前的值】 if (k == key) { e.value = value; return; } // 【这两个条件谁先成立不一定,所以 replaceStaleEntry 中还需要判断 k == key 的情况】 - // key 为 null,但是值不为 null,说明之前的 ThreadLocal 对象已经被回收了,当前是过期数据 + // key 为 null,但是值不为 null,说明之前的 ThreadLocal 对象已经被回收了,当前是【过期数据】 if (k == null) { - // 【碰到一个过期的 slot,当前数据占用该槽位,替换过期数据】 + // 【碰到一个过期的 slot,当前数据复用该槽位,替换过期数据】 // 这个方法还进行了垃圾清理动作,防止内存泄漏 replaceStaleEntry(key, value, i); return; @@ -4079,11 +4084,10 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { } // 逻辑到这说明碰到 slot == null 的位置,则在空元素的位置创建一个新的 Entry tab[i] = new Entry(key, value); - //新添加,++size 后赋值给 sz + // 数量 + 1 int sz = ++size; - // 做一次启发式清理 - // 如果没有清除任何 entry 并且当前使用量达到了负载因子所定义,那么进行 rehash + // 【做一次启发式清理】,如果没有清除任何 entry 并且当前使用量达到了负载因子所定义,那么进行 rehash if (!cleanSomeSlots(i, sz) && sz >= threshold) // 扩容 rehash(); @@ -4091,7 +4095,7 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { ``` ```java - // 获取环形数组的下一个索引 + // 获取【环形数组】的下一个索引 private static int nextIndex(int i, int len) { // 索引越界后从 0 开始继续获取 return ((i + 1 < len) ? i + 1 : 0); @@ -4105,26 +4109,28 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { Entry[] tab = table; int len = tab.length; Entry e; - // 探测式清理过期数据的开始下标,默认从当前 staleSlot 开始 + // 探测式清理的开始下标,默认从当前 staleSlot 开始 int slotToExpunge = staleSlot; // 以当前 staleSlot 开始【向前迭代查找】,找到索引靠前过期数据,找到以后替换 slotToExpunge 值 + // 【保证在一个区间段内,从最前面的过期数据开始清理】 for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) if (e.get() == null) slotToExpunge = i; - // 以 staleSlot 向后去查找,直到碰到 null 为止 + // 以 staleSlot 【向后去查找】,直到碰到 null 为止,还是线性探测 for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { // 获取当前节点的 key ThreadLocal k = e.get(); // 条件成立说明是【替换逻辑】 if (k == key) { e.value = value; - // 因为本来要在 staleSlot 索引处插入该数据,现在找到了i索引处的key与数据一致,所以需要交换位置 - // 将 table[staleSlot] 这个过期数据放到当前循环到的 table[i] 这个位置 + // 因为本来要在 staleSlot 索引处插入该数据,现在找到了i索引处的key与数据一致 + // 但是 i 位置距离正确的位置更远,因为是向后查找,所以还是要在 staleSlot 位置插入当前 entry + // 然后将 table[staleSlot] 这个过期数据放到当前循环到的 table[i] 这个位置, tab[i] = tab[staleSlot]; tab[staleSlot] = e; - // 条件成立说明向前查找过期数据并未找到过期的 entry,并且 staleSlot 位置不是过期数据了,i 位置才是 + // 条件成立说明向前查找过期数据并未找到过期的 entry,但 staleSlot 位置已经不是过期数据了,i 位置才是 if (slotToExpunge == staleSlot) slotToExpunge = i; @@ -4137,10 +4143,10 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { // 探测式清理过期数据的开始下标修改为当前循环的 index,因为 staleSlot 会放入要添加的数据 slotToExpunge = i; } - // 向后查找过程中并未发现 k == key 的 entry,说明当前是一个【添加逻辑】 + // 向后查找过程中并未发现 k == key 的 entry,说明当前是一个【取代过期数据逻辑】 // 删除原有的数据引用,防止内存泄露 tab[staleSlot].value = null; - // staleSlot 位置添加数据,上面的所有逻辑都不会更改 staleSlot 的值 + // staleSlot 位置添加数据,【上面的所有逻辑都不会更改 staleSlot 的值】 tab[staleSlot] = new Entry(key, value); // 条件成立说明除了 staleSlot 以外,还发现其它的过期 slot,所以要【开启清理数据的逻辑】 @@ -4186,9 +4192,8 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { // 条件成立说明找到了,直接返回 if (k == key) return e; - // 过期数据 if (k == null) - // 探测式过期数据回收 + // 过期数据,【探测式过期数据回收】 expungeStaleEntry(i); else // 更新 index 继续向后走 @@ -4197,11 +4202,11 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { e = tab[i]; } // 说明当前区段没有找到相应数据 - // 【因为存放数据是线性的向后寻找槽位,都是紧挨着的,不可能越过一个 空槽位 在后面放】 + // 【因为存放数据是线性的向后寻找槽位,都是紧挨着的,不可能越过一个 空槽位 在后面放】,可以减少遍历的次数 return null; } ``` - + * rehash():触发一次全量清理,如果数组长度大于等于长度的 `2/3 * 3/4 = 1/2`,则进行 resize ```java @@ -4209,7 +4214,7 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { // 清楚当前散列表内的【所有】过期的数据 expungeStaleEntries(); - // threshold = len * 2 / 3,就是 2/3*(1 - 1/4) + // threshold = len * 2 / 3,就是 2/3 * (1 - 1/4) if (size >= threshold - threshold / 4) resize(); } @@ -4219,7 +4224,7 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { private void expungeStaleEntries() { Entry[] tab = table; int len = tab.length; - // 遍历所有的槽位,清理过期数据 + // 【遍历所有的槽位,清理过期数据】 for (int j = 0; j < len; j++) { Entry e = tab[j]; if (e != null && e.get() == null) @@ -4239,7 +4244,7 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { Entry[] newTab = new Entry[newLen]; // 统计新table中的entry数量 int count = 0; - // 遍历老表,进行数据迁移 + // 遍历老表,进行【数据迁移】 for (int j = 0; j < oldLen; ++j) { // 访问老表的指定位置的 entry Entry e = oldTab[j]; @@ -4261,7 +4266,7 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { } } } - //设置下一次触发扩容的指标:threshold = len * 2 / 3; + // 设置下一次触发扩容的指标:threshold = len * 2 / 3; setThreshold(newLen); size = count; // 将扩容后的新表赋值给 threadLocalMap 内部散列表数组引用 @@ -4269,6 +4274,27 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { } ``` +* remove():删除 Entry + + ```java + private void remove(ThreadLocal key) { + Entry[] tab = table; + int len = tab.length; + // 哈希寻址 + int i = key.threadLocalHashCode & (len-1); + for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { + // 找到了对应的 key + if (e.get() == key) { + // 设置 key 为 null + e.clear(); + // 探测式清理 + expungeStaleEntry(i); + return; + } + } + } + ``` + *** @@ -4277,7 +4303,7 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { ##### 清理方法 -* 探测式清理:沿着开始位置向后探测清理过期数据,沿途中碰到未过期数据则将此数据 rehash 在 table 数组中的定位,重定位后的元素理论上更接近 `i = entry.key & (table.length - 1)`,让数据的排列更紧凑,会优化整个散列表查询性能 +* 探测式清理:沿着开始位置向后探测清理过期数据,沿途中碰到未过期数据则将此数据 rehash 在 table 数组中的定位,重定位后的元素理论上更接近 `i = entry.key & (table.length - 1)`,让**数据的排列更紧凑**,会优化整个散列表查询性能 ```java // table[staleSlot] 是一个过期数据,以这个位置开始继续向后查找过期数据 @@ -4286,7 +4312,7 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { Entry[] tab = table; int len = tab.length; - // help gc,先把 entry 置空,在取消对 entry 的引用 + // help gc,先把当前过期的 entry 置空,在取消对 entry 的引用 tab[staleSlot].value = null; tab[staleSlot] = null; // 数量-1 @@ -4294,7 +4320,7 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { Entry e; int i; - // 从 staleSlot 开始向后遍历,直到碰到 slot == null 结束 + // 从 staleSlot 开始向后遍历,直到碰到 slot == null 结束,【区间内清理过期数据】 for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal k = e.get(); // 当前 entry 是过期数据 @@ -4304,6 +4330,7 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { tab[i] = null; size--; } else { + // 当前 entry 不是过期数据的逻辑 // 重新计算当前 entry 对应的 index int h = k.threadLocalHashCode & (len - 1); // 条件成立说明当前 entry 存储时发生过 hash 冲突,向后偏移过了 @@ -4318,7 +4345,7 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { } } } - // 返回 slot = null 的槽位索引,图例是 7 + // 返回 slot = null 的槽位索引,图例是 7,这个索引代表【索引前面的区间已经清理完成垃圾了】 return i; } ``` @@ -4327,7 +4354,7 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { -* 启发式清理: +* 启发式清理:向后循环扫描过期数据,发现过期数据调用探测式清理方法,如果连续几次的循环都没有发现过期数据,就停止扫描 ```java // i 表示启发式清理工作开始位置,一般是空 slot,n 一般传递的是 table.length @@ -4352,7 +4379,7 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { } // 假设 table 长度为 16 // 16 >>> 1 ==> 8,8 >>> 1 ==> 4,4 >>> 1 ==> 2,2 >>> 1 ==> 1,1 >>> 1 ==> 0 - // 连续经过这么多次循环【没有扫描到过期数据】,就停止循环,扫描到 空slot 不算,因为不是过期数据 + // 连续经过这么多次循环【没有扫描到过期数据】,就停止循环,扫描到空 slot 不算,因为不是过期数据 } while ((n >>>= 1) != 0); // 返回清除标记 @@ -4360,6 +4387,7 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { } ``` + 参考视频:https://space.bilibili.com/457326371/ @@ -4374,15 +4402,11 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { Memory leak:内存泄漏是指程序中动态分配的堆内存由于某种原因未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果,内存泄漏的堆积终将导致内存溢出 -* 如果 key 使用强引用: - - 使用完 ThreadLocal ,threadLocal Ref 被回收,但是 threadLocalMap 的 Entry 强引用了 threadLocal,造成 threadLocal 无法被回收,无法完全避免内存泄漏 +* 如果 key 使用强引用:使用完 ThreadLocal ,threadLocal Ref 被回收,但是 threadLocalMap 的 Entry 强引用了 threadLocal,造成 threadLocal 无法被回收,无法完全避免内存泄漏 -* 如果 key 使用弱引用: - - 使用完 ThreadLocal ,threadLocal Ref 被回收,ThreadLocalMap 只持有 ThreadLocal 的弱引用,所以threadlocal 也可以被回收,此时 Entry 中的 key=null。但没有手动删除这个 Entry 或者 CurrentThread 依然运行,依然存在强引用链,value 不会被回收,而这块 value 永远不会被访问到,导致 value 内存泄漏 +* 如果 key 使用弱引用:使用完 ThreadLocal ,threadLocal Ref 被回收,ThreadLocalMap 只持有 ThreadLocal 的弱引用,所以threadlocal 也可以被回收,此时 Entry 中的 key = null。但没有手动删除这个 Entry 或者 CurrentThread 依然运行,依然存在强引用链,value 不会被回收,而这块 value 永远不会被访问到,也会导致 value 内存泄漏 @@ -4395,7 +4419,7 @@ Memory leak:内存泄漏是指程序中动态分配的堆内存由于某种原 解决方法:使用完 ThreadLocal 中存储的内容后将它 remove 掉就可以 -ThreadLocal 内部解决方法:在 ThreadLocalMap 中的 set/getEntry 方法中,会对 key 进行判断,如果为 null(ThreadLocal 为 null)的话,会对 Entry 进行垃圾回收。所以**使用弱引用比强引用多一层保障**,就算不调用 remove,也有机会进行 GC +ThreadLocal 内部解决方法:在 ThreadLocalMap 中的 set/getEntry 方法中,通过线性探测法对 key 进行判断,如果 key 为 null(ThreadLocal 为 null)会对 Entry 进行垃圾回收。所以**使用弱引用比强引用多一层保障**,就算不调用 remove,也有机会进行 GC @@ -4474,7 +4498,7 @@ private ThreadLocalMap(ThreadLocalMap parentMap) { int len = parentTable.length; setThreshold(len); table = new Entry[len]; - // 逐个复制父线程 ThreadLocalMap 中的数据 + // 【逐个复制父线程 ThreadLocalMap 中的数据】 for (int j = 0; j < len; j++) { Entry e = parentTable[j]; if (e != null) { @@ -4578,7 +4602,7 @@ java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:**FIFO * 移除方法:成功返回出队列元素,队列没有就返回 null * 阻塞组: * 当阻塞队列满时,生产者继续往队列里 put 元素,队列会一直阻塞生产线程直到 put 数据或响应中断退出 - * 当阻塞队列空时,消费者线程试图从队列里 take 元素,队列会一直阻塞消费者线程直到队列可用 + * 当阻塞队列空时,消费者线程试图从队列里 take 元素,队列会一直阻塞消费者线程直到队列中有可用元素 * 超时退出:当阻塞队列满时,队里会阻塞生产者线程一定时间,超过限时后生产者线程会退出 @@ -4611,9 +4635,9 @@ public class LinkedBlockingQueue extends AbstractQueue } ``` -入队: +入队:尾插法 -* 初始化链表 `last = head = new Node(null)`,Dummy 节点用来占位,item 为 null +* 初始化链表 `last = head = new Node(null)`,**Dummy 节点用来占位**,item 为 null ```java public LinkedBlockingQueue(int capacity) { @@ -4637,7 +4661,7 @@ public class LinkedBlockingQueue extends AbstractQueue * 再来一个节点入队 `last = last.next = node` -出队:出队首节点,先入先出 +出队:出队首节点,FIFO * 出队源码: @@ -4651,7 +4675,7 @@ public class LinkedBlockingQueue extends AbstractQueue head = first; // 出队的元素 E x = first.item; - // 当前节点置为 Dummy 节点 + // 【当前节点置为 Dummy 节点】 first.item = null; return x; } @@ -4696,7 +4720,7 @@ public class LinkedBlockingQueue extends AbstractQueue private final ReentrantLock putLock = new ReentrantLock(); private final Condition notFull = putLock.newCondition(); // 阻塞等待不满,说明已经满了 - // 用户 take(阻塞) poll(非阻塞) + // 用于 take(阻塞) poll(非阻塞) private final ReentrantLock takeLock = new ReentrantLock(); private final Condition notEmpty = takeLock.newCondition(); // 阻塞等待不空,说明已经是空的 ``` @@ -4721,7 +4745,7 @@ public class LinkedBlockingQueue extends AbstractQueue try { // 队列满了等待 while (count.get() == capacity) { - // 等待队列不满时,就可以生产数据 + // 【等待队列不满时,就可以生产数据】,线程处于 Waiting notFull.await(); } // 有空位, 入队且计数加一,尾插法 @@ -4735,7 +4759,7 @@ public class LinkedBlockingQueue extends AbstractQueue // 解锁 putLock.unlock(); } - // c自增前是0,说明生产了一个元素,唤醒 take 线程 + // c自增前是0,说明生产了一个元素,唤醒一个 take 线程 if (c == 0) signalNotEmpty(); } @@ -4769,7 +4793,7 @@ public class LinkedBlockingQueue extends AbstractQueue try { // 没有元素可以出队 while (count.get() == 0) { - // 阻塞等待队列不空,就可以消费数据 + // 【阻塞等待队列不空,就可以消费数据】,线程处于 Waiting notEmpty.await(); } // 出队,计数减一,FIFO,出队头节点 @@ -4836,7 +4860,7 @@ public class LinkedBlockingQueue extends AbstractQueue static final int maxTimedSpins = (NCPUS < 2) ? 0 : 32; ``` - 自旋的原因:线程挂起唤醒需要进行上下文切换,涉及到用户态和内核态的转变,是非常消耗资源的。自旋期间线程会一直检查自己的状态是否被匹配到,如果自旋期间被匹配到,那么直接就返回了,如果自旋次数达到某个指标后,还是会将当前线程挂起的。 + 自旋的原因:线程挂起唤醒需要进行上下文切换,涉及到用户态和内核态的转变,是非常消耗资源的。自旋期间线程会一直检查自己的状态是否被匹配到,如果自旋期间被匹配到,那么直接就返回了,如果自旋次数达到某个指标后,还是会将当前线程挂起 * 未指定超时时间,当前线程最大自旋次数: @@ -4859,7 +4883,7 @@ public class LinkedBlockingQueue extends AbstractQueue abstract static class Transferer { /** * 参数一:可以为 null,null 时表示这个请求是一个 REQUEST 类型的请求,反之是一个 DATA 类型的请求 - * 参数二:如果为 true 表示指定了超时时间,如果为 false 表示不支持超时,会一直阻塞到匹配或者倍打断 + * 参数二:如果为 true 表示指定了超时时间,如果为 false 表示不支持超时,会一直阻塞到匹配或者被打断 * 参数三:超时时间限制,单位是纳秒 * 返回值:返回值如果不为 null 表示匹配成功,DATA 类型的请求返回当前线程 put 的数据 @@ -4874,7 +4898,7 @@ public class LinkedBlockingQueue extends AbstractQueue ```java public SynchronousQueue(boolean fair) { // fair 默认 false - // 非公平模式实现的同步队列,内部数据结构是 栈,公平的是的队列 + // 非公平模式实现的数据结构是栈,公平模式的数据结构是队列 transferer = fair ? new TransferQueue() : new TransferStack(); } ``` @@ -4967,6 +4991,7 @@ TransferStack 类成员变量: // 当前 node 尚未与任何节点发生过匹配,CAS 设置 match 字段为 s 节点,表示当前 node 已经被匹配 if (match == null && UNSAFE.compareAndSwapObject(this, matchOffset, null, s)) { // 当前 node 如果自旋结束,会 park 阻塞,阻塞前将 node 对应的 Thread 保留到 waiter 字段 + // 获取当前 node 对应的阻塞线程 Thread w = waiter; // 条件成立说明 node 对应的 Thread 正在阻塞 if (w != null) { @@ -4986,7 +5011,7 @@ TransferStack 类成员变量: ```java // 取消节点的方法 void tryCancel() { - // match 字段保留当前 node 对象本身,表示这个 node 是取消状态,取消状态的 node,最终会被强制移除出栈 + // match 字段指向自己,表示这个 node 是取消状态,取消状态的 node,最终会被强制移除出栈 UNSAFE.compareAndSwapObject(this, matchOffset, null, this); } @@ -5003,6 +5028,7 @@ TransferStack 类成员方法: static SNode snode(SNode s, Object e, SNode next, int mode) { // 引用指向空时,snode 方法会创建一个 SNode 对象 if (s == null) s = new SNode(e); + // 填充数据 s.mode = mode; s.next = next; return s; @@ -5047,12 +5073,12 @@ TransferStack 类成员方法: if ((h = head) != null && h.next == s) casHead(h, s.next); // 当前 node 模式为 REQUEST 类型,返回匹配节点的 m.item 数据域 - // 当前 node 模式为 DATA 类型:返回 Node.item 数据域,当前请求提交的数据 e + // 当前 node 模式为 DATA 类型:返回 node.item 数据域,当前请求提交的数据 e return (E) ((mode == REQUEST) ? m.item : s.item); } - // 【CASE2】:逻辑到这说明请求模式不一致,如果栈顶不是 FULFILLING 说明没被其他节点匹配,说明可以匹配 + // 【CASE2】:逻辑到这说明请求模式不一致,如果栈顶不是 FULFILLING 说明没被其他节点匹配,【当前可以匹配】 } else if (!isFulfilling(h.mode)) { - // 头节点是取消节点,协助出栈 + // 头节点是取消节点,match 指向自己,协助出栈 if (h.isCancelled()) casHead(h, h.next); // 入栈当前请求的节点 @@ -5088,6 +5114,7 @@ TransferStack 类成员方法: casHead(h, null); else { SNode mn = m.next; + // m 和 h 匹配,唤醒 m 中的线程 if (m.tryMatch(h)) casHead(h, mn); else @@ -5121,7 +5148,7 @@ TransferStack 类成员方法: // 执行了超时限制就判断是否超时 if (timed) { nanos = deadline - System.nanoTime(); - // 超时了,取消节点 + // 【超时了,取消节点】 if (nanos <= 0L) { s.tryCancel(); continue; @@ -5133,7 +5160,7 @@ TransferStack 类成员方法: spins = shouldSpin(s) ? (spins - 1) : 0; // 说明没有自旋次数了 else if (s.waiter == null) - // 把当前 node 对应的 Thread 保存到 node.waiter 字段中,要阻塞了 + //【把当前 node 对应的 Thread 保存到 node.waiter 字段中,要阻塞了】 s.waiter = w; // 没有超时限制直接阻塞 else if (!timed) @@ -5171,7 +5198,7 @@ TransferStack 类成员方法: past = past.next; SNode p; - // 从栈顶开始向下检查,将栈顶开始向下的 取消状态 的节点全部清理出去,直到碰到 past 或者不是取消状态为止 + // 从栈顶开始向下检查,【将栈顶开始向下的 取消状态 的节点全部清理出去】,直到碰到 past 或者不是取消状态为止 while ((p = head) != null && p != past && p.isCancelled()) // 修改的是内存地址对应的值,p 指向该内存地址所以数据一直在变化 casHead(p, p.next); @@ -5289,8 +5316,8 @@ TransferQueue 类成员方法: QNode h = head; if (t == null || h == null) continue; - // head 和 tail 同时指向 dummy 节点,说明是【空队列,或者是不匹配的情况】 - // 队尾节点与当前请求类型是一致的情况,说明阻塞队列中都无法匹配,无法匹配 + // head 和 tail 同时指向 dummy 节点,说明是空队列 + // 队尾节点与当前请求类型是一致的情况,说明阻塞队列中都无法匹配, if (h == t || t.isData == isData) { // 获取队尾 t 的 next 节点 QNode tn = t.next; @@ -5317,12 +5344,12 @@ TransferQueue 类成员方法: // 当前节点 等待匹配.... Object x = awaitFulfill(s, e, timed, nanos); - // 说明当前 node 状态为 取消状态,需要做出队逻辑 + // 说明【当前 node 状态为 取消状态】,需要做出队逻辑 if (x == s) { clean(t, s); return null; } - // 说明当前 node 仍然在队列内,需要做匹配成功后 出队逻辑 + // 说明当前 node 仍然在队列内,匹配成功,需要做出队逻辑 if (!s.isOffList()) { // t 是当前 s 节点的前驱节点,判断 t 是不是头节点,是就更新 dummy 节点为 s 节点 advanceHead(t, s); @@ -5348,7 +5375,7 @@ TransferQueue 类成员方法: advanceHead(h, m); continue; } - // 【匹配完成】,将头节点出队,让这个真正的头结点成为 dummy 节点 + // 【匹配完成】,将头节点出队,让这个新的头结点成为 dummy 节点 advanceHead(h, m); // 唤醒该匹配节点的线程 LockSupport.unpark(m.waiter); @@ -5448,7 +5475,7 @@ public ThreadPoolExecutor(int corePoolSize, * unit:`keepAliveTime` 参数的时间单位 -* workQueue:阻塞队列,被提交但尚未被执行的任务 +* workQueue:阻塞队列,存放被提交但尚未被执行的任务 * threadFactory:线程工厂,创建新线程时用到,可以为线程创建时起名字 @@ -5481,11 +5508,11 @@ public ThreadPoolExecutor(int corePoolSize, * 如果队列满了且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会启动饱和**拒绝策略**来执行 3. 当一个线程完成任务时,会从队列中取下一个任务来执行 -4. 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断:如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉,所以线程池的所有任务完成后最终会收缩到 corePoolSize 大小 +4. 当一个线程空闲超过一定的时间(keepAliveTime)时,线程池会判断:如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉,所以线程池的所有任务完成后最终会收缩到 corePoolSize 大小 -图片来源:https://space.bilibili.com/457326371/ +参考视频:https://space.bilibili.com/457326371/ @@ -5495,7 +5522,7 @@ public ThreadPoolExecutor(int corePoolSize, ##### Executors -Executors提供了四种线程池的创建:newCachedThreadPool、newFixedThreadPool、newSingleThreadExecutor、newScheduledThreadPool +Executors 提供了四种线程池的创建:newCachedThreadPool、newFixedThreadPool、newSingleThreadExecutor、newScheduledThreadPool * newFixedThreadPool:创建一个拥有 n 个线程的线程池 @@ -5538,16 +5565,16 @@ Executors提供了四种线程池的创建:newCachedThreadPool、newFixedThrea 对比: - * 创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一 - 个线程,保证池的正常工作 - - * Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改。F...D..ExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法 - - 原因:父类不能直接调用子类中的方法,需要反射或者创建对象的方式,可以调用子类静态方法 - - * Executors.newFixedThreadPool(1) 初始时为1,可以修改。对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改 + * 创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,线程池会新建一个线程,保证池的正常工作 + +* Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改。FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法 + + 原因:父类不能直接调用子类中的方法,需要反射或者创建对象的方式,可以调用子类静态方法 + +* Executors.newFixedThreadPool(1) 初始时为1,可以修改。对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改 + - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-newSingleThreadExecutor.png) +![](https://gitee.com/seazean/images/raw/master/Java/JUC-newSingleThreadExecutor.png) @@ -5562,7 +5589,7 @@ Executors提供了四种线程池的创建:newCachedThreadPool、newFixedThrea - **线程资源必须通过线程池提供,不允许在应用中自行显式创建线程** - 使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题 - - 如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题 + - 如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者过度切换的问题 - 线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式更加明确线程池的运行规则,规避资源耗尽的风险 @@ -5583,11 +5610,11 @@ Executors提供了四种线程池的创建:newCachedThreadPool、newFixedThrea 核心线程数常用公式: -- **CPU 密集型任务(N+1):** 这种任务消耗的是 CPU 资源,可以将核心线程数设置为 N (CPU 核心数) + 1,比 CPU 核心数多出来的一个线程是为了防止线程发生缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间 +- **CPU 密集型任务(N+1):** 这种任务消耗的是 CPU 资源,可以将核心线程数设置为 N (CPU 核心数) + 1,比 CPU 核心数多出来的一个线程是为了防止线程发生缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 某个核心就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间 - CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行分析 + CPU 密集型简单理解就是利用 CPU 计算能力的任务比如在内存中对大量数据进行分析 -- **I/O 密集型任务:** 这种系统 CPU 处于阻塞状态,用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用,因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N 或 CPU核数/ (1-阻塞系数),阻塞系数在0.8~0.9之间 +- **I/O 密集型任务:** 这种系统 CPU 处于阻塞状态,用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用,因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N 或 CPU 核数/ (1-阻塞系数),阻塞系数在 0.8~0.9 之间 IO 密集型就是涉及到网络读取,文件读取此类任务 ,特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上 @@ -5614,7 +5641,7 @@ ExecutorService 类 API: execute 和 submit 都属于线程池的方法,对比: -* execute 只能提交 Runnable 类型的任务,没有返回值; submit 既能提交 Runnable 类型任务也能提交 Callable 类型任务,底层是封装成 FutureTask 调用 execute 执行 +* execute 只能执行 Runnable 类型的任务,没有返回值; submit 既能提交 Runnable 类型任务也能提交 Callable 类型任务,底层是封装成 FutureTask,然后调用 execute 执行 * execute 会直接抛出任务执行时的异常,submit 会吞掉异常,可通过 Future 的 get 方法将任务执行时的异常重新抛出 @@ -5630,7 +5657,7 @@ ExecutorService 类 API: | 方法 | 说明 | | ----------------------------------------------------- | ------------------------------------------------------------ | -| void shutdown() | 线程池状态变为 SHUTDOWN,等待任务执行完后关闭线程池,不会接收新任务,但已提交任务会执行完 | +| void shutdown() | 线程池状态变为 SHUTDOWN,等待任务执行完后关闭线程池,不会接收新任务,但已提交任务会执行完,而且也可以添加线程 | | List shutdownNow() | 线程池状态变为 STOP,用 interrupt 中断正在执行的任务,直接关闭线程池,不会接收新任务,会将队列中的任务返回 | | boolean isShutdown() | 不在 RUNNING 状态的线程池,此执行者已被关闭,方法返回 true | | boolean isTerminated() | 线程池状态是否是 TERMINATED,如果所有任务在关闭后完成,返回 true | @@ -5702,7 +5729,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 * 四种状态: ```java - // 111 000000000000000000,转换成整数后其实就是一个负数 + // 111 000000000000000000,转换成整数后其实就是一个【负数】 private static final int RUNNING = -1 << COUNT_BITS; // 000 000000000000000000 private static final int SHUTDOWN = 0 << COUNT_BITS; @@ -5745,7 +5772,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 * 重置当前线程池状态 ctl: ```java - // rs 表示线程池状态,wc 表示当前线程池中 worker(线程)数量,类似相加操作 + // rs 表示线程池状态,wc 表示当前线程池中 worker(线程)数量,相与以后就是合并后的状态 private static int ctlOf(int rs, int wc) { return rs | wc; } ``` @@ -5833,14 +5860,14 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 * 控制核心线程数量内的线程是否可以被回收: ```java - // false 代表不可以,为 true 时核心线程空闲超过 keepAliveTime 也会被回收 - // allowCoreThreadTimeOut 可以设置该值 + // false(默认)代表不可以,为 true 时核心线程空闲超过 keepAliveTime 也会被回收 + // allowCoreThreadTimeOut(boolean value) 方法可以设置该值 private volatile boolean allowCoreThreadTimeOut; ``` 内部类: -* Worker 类:**每个 Worker 对象会绑定一个初始任务**,启动 Worker 时优先执行,这也是造成线程池不公平的原因。Worker 继承自 AQS,采用了独占锁的模式,state = 0 表示未被占用,> 0 表示被占用,< 0 表示初始状态,这种情况下不能被抢锁 +* Worker 类:**每个 Worker 对象会绑定一个初始任务**,启动 Worker 时优先执行,这也是造成线程池不公平的原因。Worker 继承自 AQS,本身具有锁的特性,采用独占锁模式,state = 0 表示未被占用,> 0 表示被占用,< 0 表示初始状态不能被抢锁 ```java private final class Worker extends AbstractQueuedSynchronizer implements Runnable { @@ -5854,12 +5881,26 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 setState(-1); // firstTask不为空时,当worker启动后,内部线程会优先执行firstTask,执行完后会到queue中去获取下个任务 this.firstTask = firstTask; - // 使用【线程工厂创建一个线程】,并且将当前worker指定为Runnable,所以thread启动时会调用 worker.run() + // 使用线程工厂创建一个线程,并且【将当前worker指定为Runnable】,所以thread启动时会调用 worker.run() this.thread = getThreadFactory().newThread(this); } } ``` + ```java + public Thread newThread(Runnable r) { + // 将当前 worker 指定为 thread 的执行方法,线程调用 start 会调用 r.run() + Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0); + if (t.isDaemon()) + t.setDaemon(false); + if (t.getPriority() != Thread.NORM_PRIORITY) + t.setPriority(Thread.NORM_PRIORITY); + return t; + } + ``` + + + * 拒绝策略相关的内部类 @@ -5899,7 +5940,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 ```java protected RunnableFuture newTaskFor(Runnable runnable, T value) { - // Runnable 封装成 FutureTask,指定返回值 + // Runnable 封装成 FutureTask,【指定返回值】 return new FutureTask(runnable, value); } protected RunnableFuture newTaskFor(Callable callable) { @@ -5908,7 +5949,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 } ``` -* execute():执行任务,但是没有返回值,没办法获取任务执行结果,出现异常会直接抛出任务执行时的异常 +* execute():执行任务,但是没有返回值,没办法获取任务执行结果,出现异常会直接抛出任务执行时的异常。根据线程池中的线程数,选择添加任务时的处理方式 ```java // command 可以是普通的 Runnable 实现类,也可以是 FutureTask,不能是 Callable @@ -5928,12 +5969,12 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 // SHUTDOWN 状态下也有可能创建成功,前提 firstTask == null 而且当前 queue 不为空(特殊情况) c = ctl.get(); } - // 执行到这说明当前线程数量已经达到核心线程数量 或者 addWorker 失败 - // 【2】条件成立说明当前线程池处于running状态,则尝试将 task 放入到 workQueue 中,核心满了 + // 【2】执行到这说明当前线程数量已经达到核心线程数量 或者 addWorker 失败 + // 判断当前线程池是否处于running状态,成立就尝试将 task 放入到 workQueue 中 if (isRunning(c) && workQueue.offer(command)) { int recheck = ctl.get(); // 条件一成立说明线程池状态被外部线程给修改了,可能是执行了 shutdown() 方法,该状态不能接收新提交的任务 - // 所以要把刚提交的任务删除,删除成功说明提交之后线程池中的线程还未消费该任务(处理) + // 所以要把刚提交的任务删除,删除成功说明提交之后线程池中的线程还未消费(处理)该任务 if (!isRunning(recheck) && remove(command)) // 任务出队成功,走拒绝策略 reject(command); @@ -5943,7 +5984,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 addWorker(null, false); } // 【3】offer失败说明queue满了 - // 如果线程数量尚未达到 maximumPoolSize,会创建非核心 worker 线程执行 command,这也是不公平的原因 + // 如果线程数量尚未达到 maximumPoolSize,会创建非核心 worker 线程直接执行 command,【这也是不公平的原因】 // 如果当前线程数量达到 maximumPoolSiz,这里 addWorker 也会失败,走拒绝策略 else if (!addWorker(command, false)) reject(command); @@ -5960,12 +6001,14 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 ##### 添加线程 -* addWorker():**添加线程到线程池**,返回 true 表示创建 Worker 成功,且线程启动 +* addWorker():**添加线程到线程池**,返回 true 表示创建 Worker 成功,且线程启动。首先判断线程池是否允许添加线程,允许就让线程数量 + 1,然后去创建 Worker 加入线程池 + + 注意:SHUTDOWN 状态也能添加线程,但是要求新加的 Woker 没有 firstTask ```java // core == true 表示采用核心线程数量限制,false 表示采用 maximumPoolSize private boolean addWorker(Runnable firstTask, boolean core) { - // 自旋判断当前线程池状态是否允许创建线程的,允许就设置线程数量 + 1 + // 自旋【判断当前线程池状态是否允许创建线程】,允许就设置线程数量 + 1 retry: for (;;) { // 获取 ctl 的值 @@ -5975,12 +6018,12 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 // 判断当前线程池状态【是否允许添加线程】 - // 当前线程池是 SHUTDOWN 状态,但是队列里面还有任务尚未处理完, - // 需要处理完 queue 中的任务,但是【不允许再提交新的 task】,所以 addWorker 返回 false + // 当前线程池是 SHUTDOWN 状态,但是队列里面还有任务尚未处理完,需要处理完 queue 中的任务 + // 【不允许再提交新的 task,所以 firstTask 为空,但是可以继续添加 worker】 if (rs >= SHUTDOWN && !(rs == SHUTDOWN && firstTask == null && !workQueue.isEmpty())) return false; for (;;) { - // 获取当前线程池中线程数量 + // 获取线程池中线程数量 int wc = workerCountOf(c); // 条件一一般不成立,CAPACITY是5亿多,根据 core 判断使用哪个大小限制线程数量,超过了返回 false if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize)) @@ -5991,7 +6034,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 break retry; // CAS 失败,没有成功的申请到令牌 c = ctl.get(); - // 判断当前线程池状态是否发生过变化,被其他线程修改了,可能调用了 shutdown() 方法 + // 判断当前线程池状态是否发生过变化,被其他线程修改了,可能其他线程调用了 shutdown() 方法 if (runStateOf(c) != rs) // 返回外层循环检查是否能创建线程,在 if 语句中返回 false continue retry; @@ -6007,28 +6050,28 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 boolean workerAdded = false; Worker w = null; try { - // 创建 Worker,底层通过线程工厂创建执行线程,指定了先执行的任务 + // 【创建 Worker,底层通过线程工厂 newThread 方法创建执行线程,指定了首先执行的任务】 w = new Worker(firstTask); - // 将新创建的 worker 节点的线程赋值给 t + // 将新创建的 worker 节点中的线程赋值给 t final Thread t = w.thread; - // 为了防止 ThreadFactory 程序员自定义的实现类有 bug,创造不出线程 + // 这里的判断为了防止 程序员自定义的 ThreadFactory 实现类有 bug,创造不出线程 if (t != null) { final ReentrantLock mainLock = this.mainLock; - // 加互斥锁 + // 加互斥锁,要添加 worker 了 mainLock.lock(); try { // 获取最新线程池运行状态保存到 rs int rs = runStateOf(ctl.get()); - // 判断线程池是否为RUNNING状态,不是再判断当前是否为SHUTDOWN状态且firstTask为空(特殊情况) + // 判断线程池是否为RUNNING状态,不是再【判断当前是否为SHUTDOWN状态且firstTask为空,特殊情况】 if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) { - // 当线程 start 后,线程 isAlive 会返回 true,这里还没启动线程 + // 当线程start后,线程isAlive会返回true,这里还没开始启动线程,如果被启动了就需要报错 if (t.isAlive()) throw new IllegalThreadStateException(); //【将新建的 Worker 添加到线程池中】 workers.add(w); int s = workers.size(); - // 条件成立说明当前线程数量是一个新高,更新 largestPoolSize + // 当前池中的线程数量是一个新高,更新 largestPoolSize if (s > largestPoolSize) largestPoolSize = s; // 添加标记置为 true @@ -6038,7 +6081,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 // 解锁啊 mainLock.unlock(); } - // 添加成功就启动线程【执行任务】 + // 添加成功就【启动线程执行任务】 if (workerAdded) { // Thread 类中持有 Runnable 任务对象,调用的是 Runnable 的 run ,也就是 FutureTask t.start(); @@ -6055,7 +6098,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 return workerStarted; } ``` - + * addWorkerFailed():清理任务 ```java @@ -6069,6 +6112,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 workers.remove(w); // 将线程池计数 -1,相当于归还令牌。 decrementWorkerCount(); + // 尝试停止线程池 tryTerminate(); } finally { //释放线程池全局锁。 @@ -6085,7 +6129,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 ##### 运行方法 -* Worker#run:Worker 实现了 Runnable 接口,当某个 worker 启动时,会执行 run() +* Worker#run:Worker 实现了 Runnable 接口,当线程启动时,会调用 Worker 的 run() 方法 ```java public void run() { @@ -6101,17 +6145,18 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 Thread wt = Thread.currentThread(); // 获取 worker 的 firstTask Runnable task = w.firstTask; - // 引用置空,防止复用该线程时【重复执行】该任务 + // 引用置空,【防止复用该线程时重复执行该任务】 w.firstTask = null; - // 初始化 worker 时设置 state = -1,这里需要设置 state = 0 和 exclusiveOwnerThread = null + // 初始化 worker 时设置 state = -1,表示不允许抢占锁 + // 这里需要设置 state = 0 和 exclusiveOwnerThread = null,开始独占模式抢锁 w.unlock(); // true 表示发生异常退出,false 表示正常退出。 boolean completedAbruptly = true; try { // firstTask 不是 null 就直接运行,否则去 queue 中获取任务 - // 【getTask 如果是阻塞获取任务,会一直阻塞在take方法,获取后继续循环,不会走返回null的逻辑】 + // 【getTask 如果是阻塞获取任务,会一直阻塞在take方法,直到获取任务,不会走返回null的逻辑】 while (task != null || (task = getTask()) != null) { - // worker 加锁,shutdown 时会判断当前 worker 状态,根据独占锁是否【空闲】 + // worker 加锁,shutdown 时会判断当前 worker 状态,【根据独占锁状态判断是否空闲】 w.lock(); // 说明线程池状态大于 STOP,目前处于 STOP/TIDYING/TERMINATION,此时给线程一个中断信号 @@ -6121,24 +6166,20 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 // 中断线程,设置线程的中断标志位为 true wt.interrupt(); try { - // 钩子方法,任务执行的前置处理 + // 钩子方法,【任务执行的前置处理】 beforeExecute(wt, task); Throwable thrown = null; try { // 【执行任务】 task.run(); - } catch (RuntimeException x) { - thrown = x; throw x; - } catch (Error x) { - thrown = x; throw x; - } catch (Throwable x) { - thrown = x; throw new Error(x); + } catch (Exception x) { + //..... } finally { - // 钩子方法,任务执行的后置处理 + // 钩子方法,【任务执行的后置处理】 afterExecute(task, thrown); } } finally { - task = null; // 将局部变量task置为null + task = null; // 将局部变量task置为null,代表任务执行完成 w.completedTasks++; // 更新worker完成任务数量 w.unlock(); // 解锁 } @@ -6147,7 +6188,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 completedAbruptly = false; } finally { // 正常退出 completedAbruptly = false - // 异常退出 completedAbruptly = true,从 task.run() 内部抛出异常时,跳到这一行 + // 异常退出 completedAbruptly = true,【从 task.run() 内部抛出异常】时,跳到这一行 processWorkerExit(w, completedAbruptly); } } @@ -6165,11 +6206,11 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 } ``` -* getTask():获取任务,线程空闲时间超过 keepAliveTime 就会被回收,判断的依据是**当前线程阻塞超过保活时间没有获取到任务**,方法返回 null 就代表当前线程要被回收了,返回到 runWorker 执行线程退出逻辑 +* getTask():获取任务,线程空闲时间超过 keepAliveTime 就会被回收,判断的依据是**当前线程阻塞获取任务超过保活时间**,方法返回 null 就代表当前线程要被回收了,返回到 runWorker 执行线程退出逻辑。线程池具有担保机制,对于 RUNNING 状态下的超时回收,要保证线程池中最少有一个线程运行,或者任务阻塞队列已经是空 ```java private Runnable getTask() { - // 超时标记,表示当前线程获取任务是否超时,默认 false,true 表示已超时 + // 超时标记,表示当前线程获取任务是否超时,true 表示已超时 boolean timedOut = false; for (;;) { int c = ctl.get(); @@ -6187,16 +6228,16 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 // 获取线程池中的线程数量 int wc = workerCountOf(c); - // 线程没有明确的区分谁是核心或者非核心,是根据当前池中的线程数量判断 + // 线程没有明确的区分谁是核心或者非核心线程,是根据当前池中的线程数量判断 // timed = false 表示当前这个线程 获取task时不支持超时机制的,当前线程会使用 queue.take() 阻塞获取 // timed = true 表示当前这个线程 获取task时支持超时机制,使用 queue.poll(xxx,xxx) 超时获取 // 条件一代表允许回收核心线程,那就无所谓了,全部线程都执行超时回收 - // 条件二成立说明线程数量大于核心线程数,当前线程认为是非核心线程,空闲一定时间就需要退出,去超时获取任务 + // 条件二成立说明线程数量大于核心线程数,当前线程认为是非核心线程,有保活时间,去超时获取任务 boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; // 如果线程数量是否超过最大线程数,直接回收 - // 如果当前线程允许超时回收并且已经超时了,就应该被回收了,但是由于【担保机制】还要做判断: + // 如果当前线程【允许超时回收并且已经超时了】,就应该被回收了,由于【担保机制】还要做判断: // wc > 1 说明线程池还用其他线程,当前线程可以直接回收 // workQueue.isEmpty() 前置条件是 wc = 1,如果当前任务队列也是空了,最后一个线程就可以安全的退出 if ((wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty())) { @@ -6216,7 +6257,8 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 // 获取任务为 null 说明超时了,将超时标记设置为 true,下次自旋时返 null timedOut = true; } catch (InterruptedException retry) { - // 阻塞线程被打断后超时标记置为 false,说明被打断不算超时,要继续获取,直到超时或者获取到任务 + // 阻塞线程被打断后超时标记置为 false,【说明被打断不算超时】,要继续获取,直到超时或者获取到任务 + // 如果线程池 SHUTDOWN 状态下的打断,会在循环获取任务前判断,返回 null timedOut = false; } } @@ -6228,13 +6270,14 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 ```java // 正常退出 completedAbruptly = false,异常退出为 true private void processWorkerExit(Worker w, boolean completedAbruptly) { - // 条件成立代表当前 worker 是发生异常退出的,task 任务执行过程中向上抛出异常了, + // 条件成立代表当前 worker 是发生异常退出的,task 任务执行过程中向上抛出异常了 if (completedAbruptly) // 从异常时到这里 ctl 一直没有 -1,需要在这里 -1 decrementWorkerCount(); final ReentrantLock mainLock = this.mainLock; - mainLock.lock(); // 加锁 + // 加锁 + mainLock.lock(); try { // 将当前 worker 完成的 task 数量,汇总到线程池的 completedTaskCount completedTaskCount += w.completedTasks; @@ -6247,20 +6290,21 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 tryTerminate(); int c = ctl.get(); - // 线程池不是停止状态就应该有线程运行 + // 线程池不是停止状态就应该有线程运行【担保机制】 if (runStateLessThan(c, STOP)) { - // 正常退出的逻辑,是空闲线程回收 + // 正常退出的逻辑,是对空闲线程回收,不是执行出错 if (!completedAbruptly) { // 根据是否回收核心线程确定【线程池中的线程数量最小值】 int min = allowCoreThreadTimeOut ? 0 : corePoolSize; - // 最小值为 0,但是线程队列不为空,需要一个线程来完成任务【担保机制】 + // 最小值为 0,但是线程队列不为空,需要一个线程来完成任务担保机制 if (min == 0 && !workQueue.isEmpty()) min = 1; // 线程池中的线程数量大于最小值可以直接返回 if (workerCountOf(c) >= min) return; } - // 执行 task 时发生异常,这里要创建一个新 worker 加进线程池,有个线程因为异常终止了 + // 执行 task 时发生异常,有个线程因为异常终止了,需要添加 + // 或者线程池中的数量小于最小值,这里要创建一个新 worker 加进线程池 addWorker(null, false); } } @@ -6274,7 +6318,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 ##### 停止方法 -* shutdown(): +* shutdown():停止线程池 ```java public void shutdown() { @@ -6283,7 +6327,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 mainLock.lock(); try { checkShutdownAccess(); - // 设置线程池状态为 SHUTDOWN + // 设置线程池状态为 SHUTDOWN,如果线程池状态大于 SHUTDOWN,就不会设置直接返回 advanceRunState(SHUTDOWN); // 中断空闲线程 interruptIdleWorkers(); @@ -6297,22 +6341,22 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 } ``` -* interruptIdleWorkers():shutdown 方法会**中断空闲线程**,根据是否可以获取 AQS 独占锁判断是否处于工作状态 +* interruptIdleWorkers():shutdown 方法会**中断所有空闲线程**,根据是否可以获取 AQS 独占锁判断是否处于工作状态。线程之所以空闲是因为阻塞队列没有任务,不会中断正在运行的线程,所以 shutdown 方法会让所有的任务执行完毕。 ```java // onlyOne == true 说明只中断一个线程 ,false 则中断所有线程 private void interruptIdleWorkers(boolean onlyOne) { final ReentrantLock mainLock = this.mainLock; - //持有全局锁 + / /持有全局锁 mainLock.lock(); try { - // 遍历所有worker + // 遍历所有 worker for (Worker w : workers) { // 获取当前 worker 的线程 Thread t = w.thread; - //条件一成立:说明当前迭代的这个线程尚未中断 - //条件二成立:说明当前worker处于空闲状态,阻塞在poll或者take,因为worker执行task时是要加锁的 - // 每个worker有一个独占锁,w.tryLock()尝试加锁,加锁成功返回 true + // 条件一成立:说明当前迭代的这个线程尚未中断 + // 条件二成立:说明当前worker处于空闲状态,阻塞在poll或者take,因为worker执行task时是要加锁的 + // 每个worker有一个独占锁,w.tryLock()尝试加锁,加锁成功返回 true if (!t.isInterrupted() && w.tryLock()) { try { // 中断线程,处于 queue 阻塞的线程会被唤醒,进入下一次自旋,返回 null,执行退出相逻辑 @@ -6335,29 +6379,29 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 } ``` -* shutdownNow(): +* shutdownNow():直接关闭线程池,不会等待任务执行完成 ```java public List shutdownNow() { // 返回值引用 List tasks; final ReentrantLock mainLock = this.mainLock; - //获取线程池全局锁 + // 获取线程池全局锁 mainLock.lock(); try { checkShutdownAccess(); - //设置线程池状态为STOP + // 设置线程池状态为STOP advanceRunState(STOP); - //中断线程池中所有线程 + // 中断线程池中【所有线程】 interruptWorkers(); - //导出未处理的task + // 从阻塞队列中导出未处理的task tasks = drainQueue(); } finally { mainLock.unlock(); } tryTerminate(); - //返回当前任务队列中 未处理的任务。 + // 返回当前任务队列中 未处理的任务。 return tasks; } ``` @@ -6378,8 +6422,9 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 // 执行到这里说明线程池状态为 STOP 或者线程池状态为 SHUTDOWN 并且队列已经是空 // 判断线程池中线程的数量 if (workerCountOf(c) != 0) { - // 中断一个【空闲线程】,在 queue.take() | queue.poll() 阻塞空闲 - // 唤醒后的线程会在getTask()方法返回null,执行退出逻辑时会再次调用tryTerminate()唤醒下一个空闲线程 + // 【中断一个空闲线程】,在 queue.take() | queue.poll() 阻塞空闲 + // 唤醒后的线程会在getTask()方法返回null, + // 执行 processWorkerExit 退出逻辑时会再次调用 tryTerminate() 唤醒下一个空闲线程 interruptIdleWorkers(ONLY_ONE); return; } @@ -6396,7 +6441,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 } finally { // 设置线程池状态为TERMINATED状态。 ctl.set(ctlOf(TERMINATED, 0)); - // 唤醒所有调用 awaitTermination() 方法的线程 + // 【唤醒所有调用 awaitTermination() 方法的线程】 termination.signalAll(); } return; @@ -6444,15 +6489,14 @@ public FutureTask(Callable callable){ this.callable = callable; // 属性注入 this.state = NEW; // 任务状态设置为 new } +``` +```java public FutureTask(Runnable runnable, V result) { - // 装饰 + // 适配器模式 this.callable = Executors.callable(runnable, result); this.state = NEW; } -``` - -```java public static Callable callable(Runnable task, T result) { if (task == null) throw new NullPointerException(); // 使用装饰者模式将 runnable 转换成 callable 接口,外部线程通过 get 获取 @@ -6499,7 +6543,7 @@ FutureTask 类的成员属性: private static final int COMPLETING = 1; // 当前任务正常结束 private static final int NORMAL = 2; - // 当前任务执行过程中发生了异常。 内部封装的 callable.run() 向上抛出异常了 + // 当前任务执行过程中发生了异常,内部封装的 callable.run() 向上抛出异常了 private static final int EXCEPTIONAL = 3; // 当前任务被取消 private static final int CANCELLED = 4; @@ -6515,10 +6559,10 @@ FutureTask 类的成员属性: private Callable callable; // Runnable 使用装饰者模式伪装成 Callable ``` -* 存储任务执行的结果,这是 run 方法返回值是 void 也可以获取到执行结果的原因: +* **存储任务执行的结果**,这是 run 方法返回值是 void 也可以获取到执行结果的原因: ```java - // 正常情况下:任务正常执行结束,outcome 保存执行结果,callable 返回值。 + // 正常情况下:任务正常执行结束,outcome 保存执行结果,callable 返回值 // 非正常情况:callable 向上抛出异常,outcome 保存异常 private Object outcome; ``` @@ -6540,7 +6584,7 @@ FutureTask 类的成员属性: ```java static final class WaitNode { - //单向链表 + // 单向链表 volatile Thread thread; volatile WaitNode next; WaitNode() { thread = Thread.currentThread(); } @@ -6569,18 +6613,17 @@ FutureTask 类的成员方法: !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread())) return; try { - // 执行到这里,当前 task 一定是 NEW 状态,而且当前线程也抢占 task 成功! + // 执行到这里,当前 task 一定是 NEW 状态,而且【当前线程也抢占 task 成功】 Callable c = callable; // 判断任务是否为空,防止空指针异常;判断 state 状态,防止外部线程在此期间 cancel 掉当前任务 - // 因为 task 的执行者已经设置为当前线程,所以这里是线程安全的, + // 【因为 task 的执行者已经设置为当前线程,所以这里是线程安全的】 if (c != null && state == NEW) { - // 结果引用 V result; // true 表示 callable.run 代码块执行成功 未抛出异常 // false 表示 callable.run 代码块执行失败 抛出异常 boolean ran; try { - // 调用自定义的方法 + // 【调用自定义的方法,执行结果赋值给 result】 result = c.call(); // 没有出现异常 ran = true; @@ -6608,13 +6651,13 @@ FutureTask 类的成员方法: } ``` - FutureTask#set:设置正常返回值 + FutureTask#set:设置正常返回值,首先将任务状态设置为 COMPLETING 状态代表完成中,逻辑执行完设置为 NORMAL 状态代表任务正常执行完成,最后唤醒 get() 阻塞线程 ```java protected void set(V v) { // CAS 方式设置当前任务状态为完成中,设置失败说明其他线程取消了该任务 if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) { - // 将结果赋值给 outcome + // 【将结果赋值给 outcome】 outcome = v; // 将当前任务状态修改为 NORMAL 正常结束状态。 UNSAFE.putOrderedInt(this, stateOffset, NORMAL); @@ -6643,7 +6686,7 @@ FutureTask 类的成员方法: private void finishCompletion() { // 遍历所有的等待的节点,q 指向头节点 for (WaitNode q; (q = waiters) != null;) { - // 使用cas设置 waiters 为 null,防止外部线程使用 cancel 取消当前任务,也会触发finishCompletion方法 + // 使用cas设置 waiters 为 null,防止外部线程使用cancel取消当前任务,触发finishCompletion方法重复执行 if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) { // 自旋 for (;;) { @@ -6659,6 +6702,7 @@ FutureTask 类的成员方法: // 当前节点是最后一个节点了 if (next == null) break; + // 断开链表 q.next = null; // help gc q = next; } @@ -6682,7 +6726,7 @@ FutureTask 类的成员方法: } ``` -* **FutureTask#get**:获取任务执行的返回值,执行 run 和 get 的不是同一个线程,可能有多个线程 get,只有一个线程 run +* **FutureTask#get**:获取任务执行的返回值,执行 run 和 get 的不是同一个线程,一般有多个线程 get,只有一个线程 run ```java public V get() throws InterruptedException, ExecutionException { @@ -6696,7 +6740,7 @@ FutureTask 类的成员方法: } ``` - FutureTask#awaitDone:**get 线程阻塞等待**,封装成 WaitNode 对象进入阻塞队列 + FutureTask#awaitDone:**get 线程封装成 WaitNode 对象进入阻塞队列阻塞等待** ```java private int awaitDone(boolean timed, long nanos) throws InterruptedException { @@ -6704,9 +6748,9 @@ FutureTask 类的成员方法: final long deadline = timed ? System.nanoTime() + nanos : 0L; // 引用当前线程,封装成 WaitNode 对象 WaitNode q = null; - // 表示当前线程 waitNode 对象 有没有入队/压栈 + // 表示当前线程 waitNode 对象,是否进入阻塞队列 boolean queued = false; - // 自旋,三次自旋开始休眠 + // 【三次自旋开始休眠】 for (;;) { // 判断当前 get() 线程是否被打断,打断返回 true,清除打断标记 if (Thread.interrupted()) { @@ -6716,35 +6760,35 @@ FutureTask 类的成员方法: } // 获取任务状态 int s = state; - // 条件成立:说明当前任务执行完成已经有结果了 + // 条件成立说明当前任务执行完成已经有结果了 if (s > COMPLETING) { - // 条件成立说明已经为当前线程创建了 WaitNode,置空帮助 GC + // 条件成立说明已经为当前线程创建了 WaitNode,置空 help GC if (q != null) q.thread = null; // 返回当前的状态 return s; } - // 条件成立:说明当前任务接近完成状态,这里让当前线程释放 cpu ,进行下一次抢占 cpu + // 条件成立说明当前任务接近完成状态,这里让当前线程释放一下 cpu ,等待进行下一次抢占 cpu else if (s == COMPLETING) Thread.yield(); - // 条件成立:【第一次自旋】,当前线程还未创建 WaitNode 对象,此时为当前线程创建 WaitNode对象 + // 【第一次自旋】,当前线程还未创建 WaitNode 对象,此时为当前线程创建 WaitNode对象 else if (q == null) q = new WaitNode(); - // 条件成立:【第二次自旋】,当前线程已经创建 WaitNode 对象了,但是node对象还未入队 + // 【第二次自旋】,当前线程已经创建 WaitNode 对象了,但是node对象还未入队 else if (!queued) // waiters 指向队首,让当前 WaitNode 成为新的队首,【头插法】,失败说明其他线程修改了新的队首 queued = UNSAFE.compareAndSwapObject(this, waitersOffset, q.next = waiters, q); - // 条件成立:【第三次自旋】,会到这里。 + // 【第三次自旋】,会到这里,或者 else 内 else if (timed) { nanos = deadline - System.nanoTime(); if (nanos <= 0L) { removeWaiter(q); return state; } - // 休眠指定的时间 + // 阻塞指定的时间 LockSupport.parkNanos(this, nanos); } - // 条件成立:说明需要休眠 + // 条件成立:说明需要阻塞 else // 【当前 get 操作的线程被 park 阻塞】,除非有其它线程将唤醒或者将当前线程中断 LockSupport.park(this); @@ -6756,14 +6800,14 @@ FutureTask 类的成员方法: ```java private V report(int s) throws ExecutionException { - // 获取执行结果,都在一个 futuretask 对象中的属性,可以直接获取 + // 获取执行结果,是在一个 futuretask 对象中的属性,可以直接获取 Object x = outcome; // 当前任务状态正常结束 if (s == NORMAL) return (V)x; // 直接返回 callable 的逻辑结果 // 当前任务被取消或者中断 if (s >= CANCELLED) - throw new CancellationException(); //抛出异常 + throw new CancellationException(); // 抛出异常 // 执行到这里说明自定义的 callable 中的方法有异常,使用 outcome 上层抛出异常 throw new ExecutionException((Throwable)x); } @@ -6861,14 +6905,14 @@ private static void method1() { ```java public ScheduledThreadPoolExecutor(int corePoolSize) { - // 最大线程数固定为 Integer.MAX_VALUE,最大活跃时间 keepAliveTime 固定为 + // 最大线程数固定为 Integer.MAX_VALUE,保活时间 keepAliveTime 固定为 0 super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, // 阻塞队列是 DelayedWorkQueue new DelayedWorkQueue()); } ``` -常用API: +常用 API: * `ScheduledFuture schedule(Runnable/Callable, long delay, TimeUnit u)`:延迟执行任务 * `ScheduledFuture scheduleAtFixedRate(Runnable/Callable, long initialDelay, long period, TimeUnit unit)`:定时执行任务,参数为初始延迟时间、间隔时间、单位 @@ -6895,7 +6939,7 @@ public ScheduledThreadPoolExecutor(int corePoolSize) { } ``` -* 定时任务 scheduleAtFixedRate:**一次任务的启动到下一次任务的启动**之间只要大于间隔时间,抢占到 CPU 就会立即执行 +* 定时任务 scheduleAtFixedRate:**一次任务的启动到下一次任务的启动**之间只要大于等于间隔时间,抢占到 CPU 就会立即执行 ```java public static void main(String[] args) { @@ -6974,7 +7018,7 @@ public ScheduledThreadPoolExecutor(int corePoolSize) { ##### 延迟任务 -ScheduledFutureTask 继承 FutureTask,实现 RunnableScheduledFuture 接口,具有延迟执行的特点,覆盖 FutureTask 的 run 方法来实现对延时执行、周期执行的支持。对于延时任务调用 FutureTask#run 而对于周期性任务则调用 FutureTask#runAndReset 并且在成功之后根据 fixed-delay/fixed-rate 模式来设置下次执行时间并重新将任务塞到工作队列。 +ScheduledFutureTask 继承 FutureTask,实现 RunnableScheduledFuture 接口,具有延迟执行的特点,覆盖 FutureTask 的 run 方法来实现对**延时执行、周期执行**的支持。对于延时任务调用 FutureTask#run 而对于周期性任务则调用 FutureTask#runAndReset 并且在成功之后根据 fixed-delay/fixed-rate 模式来设置下次执行时间并重新将任务塞到工作队列。 在调度线程池中无论是 runnable 还是 callable,无论是否需要延迟和定时,所有的任务都会被封装成 ScheduledFutureTask @@ -6990,10 +7034,12 @@ ScheduledFutureTask 继承 FutureTask,实现 RunnableScheduledFuture 接口, ```java private long time; // 任务可以被执行的时间,以纳秒表示 - private final long period; // 0 表示非周期任务,正数表示 fixed-rate 模式,负数表示 fixed-delay 模式的周期 + private final long period; // 0 表示非周期任务,正数表示 fixed-rate 模式的周期,负数表示 fixed-delay 模式 ``` -* 实际的任务: + fixed-rate:两次开始启动的间隔,fixed-delay:一次执行结束到下一次开始启动 + +* 实际的任务对象: ```java RunnableScheduledFuture outerTask = this; @@ -7002,7 +7048,7 @@ ScheduledFutureTask 继承 FutureTask,实现 RunnableScheduledFuture 接口, * 任务在队列数组中的索引下标: ```java - int heapIndex; // -1 代表删除 + int heapIndex; // -1 代表删除 ``` 成员方法: @@ -7016,27 +7062,26 @@ ScheduledFutureTask 继承 FutureTask,实现 RunnableScheduledFuture 接口, this.time = ns; // 任务的周期,多长时间执行一次 this.period = period; + // 任务的序号 this.sequenceNumber = sequencer.getAndIncrement(); } ``` * compareTo():ScheduledFutureTask 根据执行时间 time 正序排列,如果执行时间相同,在按照序列号 sequenceNumber 正序排列,任务需要放入 DelayedWorkQueue,延迟队列中使用该方法按照从小到大进行排序 -* run():执行任务,**周期任务执行完后会重新放入线程池的阻塞队列** +* run():执行任务,非周期任务直接完成直接结束,**周期任务执行完后会设置下一次的执行时间,重新放入线程池的阻塞队列**,如果线程池中的线程数量少于核心线程,就会添加 Worker 开启新线程 ```java public void run() { // 是否周期性,就是判断 period 是否为 0 boolean periodic = isPeriodic(); - // 检查当前状态能否执行任务 + // 检查当前状态能否执行任务,不能执行就取消任务 if (!canRunInCurrentRunState(periodic)) - // 取消任务 cancel(false); - // 非周期任务直接执行 + // 非周期任务,直接调用 FutureTask#run 执行 else if (!periodic) ScheduledFutureTask.super.run(); - // 周期任务的执行,正常完成后任务的状态不会变化,依旧是 NEW,且返回值为成功或失败,不会设置result属性。 - // 需要注意,如果本次任务执行出现异常,返回 false,后续的该任务不会再执行 + // 周期任务的执行,返回 true 表示执行成功 else if (ScheduledFutureTask.super.runAndReset()) { // 设置周期任务的下一次执行时间 setNextRunTime(); @@ -7046,6 +7091,8 @@ ScheduledFutureTask 继承 FutureTask,实现 RunnableScheduledFuture 接口, } ``` + 周期任务正常完成后任务的状态不会变化,依旧是 NEW,不会设置 outcome 属性。但是如果本次任务执行出现异常,会进入 setException 方法将任务状态置为异常,把异常保存在 outcome 中,方法返回 false,后续的该任务将不会再周期的执行 + ```java protected boolean runAndReset() { // 任务不是新建的状态了,或者被别的线程执行了,直接返回 false @@ -7074,7 +7121,7 @@ ScheduledFutureTask 继承 FutureTask,实现 RunnableScheduledFuture 接口, if (s >= INTERRUPTING) handlePossibleCancellationInterrupt(s); } - // 如果正常执行,返回 true + // 如果正常执行,返回 true,并且任务状态没有被取消 return ran && s == NEW; } ``` @@ -7084,10 +7131,10 @@ ScheduledFutureTask 继承 FutureTask,实现 RunnableScheduledFuture 接口, private void setNextRunTime() { long p = period; if (p > 0) - // fixed-rate 模式,时间设置为上一次时间 +p,两次任务执行的时间差 + // fixed-rate 模式,【时间设置为上一次执行任务的时间 +p】,两次任务执行的时间差 time += p; else - // fixed-delay 模式,下一次执行时间是当前这次任务结束的时间(就是现在) +delay 值 + // fixed-delay 模式,下一次执行时间是当【前这次任务结束的时间(就是现在) +delay 值】 time = triggerTime(-p); } ``` @@ -7098,13 +7145,13 @@ ScheduledFutureTask 继承 FutureTask,实现 RunnableScheduledFuture 接口, // ScheduledThreadPoolExecutor#reExecutePeriodic void reExecutePeriodic(RunnableScheduledFuture task) { if (canRunInCurrentRunState(true)) { - // 放入任务队列 + // 【放入任务队列】 super.getQueue().add(task); // 再次检查是否可以执行,如果不能执行且任务还在队列中未被取走,则取消任务 if (!canRunInCurrentRunState(true) && remove(task)) task.cancel(false); else - // 当前线程池状态可以执行周期任务,加入队列,并根据线程数量是否大于 核心线程数确定是否开启新线程 + // 当前线程池状态可以执行周期任务,加入队列,并【根据线程数量是否大于核心线程数确定是否开启新线程】 ensurePrestart(); } } @@ -7134,9 +7181,9 @@ ScheduledFutureTask 继承 FutureTask,实现 RunnableScheduledFuture 接口, ##### 延迟队列 -DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先队列 PriorityQueue (小根堆)存储元素 +DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先队列 PriorityQueue(小根堆)存储元素 -其他阻塞队列存储节点的数据结构大都是链表,延迟队列是数组,所以延迟队列出队头元素后需要让其他元素(尾)替换到头节点,防止空指针异常 +其他阻塞队列存储节点的数据结构大都是链表,**延迟队列是数组**,所以延迟队列出队头元素后需要让其他元素(尾)替换到头节点,防止空指针异常 成员变量: @@ -7185,13 +7232,13 @@ DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先 // 扩容为原来长度的 1.5 倍 grow(); size = i + 1; - // 插入的元素是第一个节点 + // 当前是第一个要插入的节点 if (i == 0) { queue[0] = e; // 修改 ScheduledFutureTask 的 heapIndex 属性,表示该对象在队列里的下标 setIndex(e, 0); } else { - // 向上调整元素的位置 + // 向上调整元素的位置,并更新 heapIndex siftUp(i, e); } // 【插入的元素是头节点,原先的 leader 等待的是原先的头节点,所以 leader 已经无效】 @@ -7227,7 +7274,7 @@ DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先 } ``` -* poll():非阻塞获取头结点,执行时间最近的 +* poll():非阻塞获取头结点,**获取执行时间最近的** ```java // 非阻塞获取 @@ -7235,7 +7282,7 @@ DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先 final ReentrantLock lock = this.lock; lock.lock(); try { - // 获取队头节点 + // 获取队头节点,因为是小顶堆 RunnableScheduledFuture first = queue[0]; // 头结点为空或者的延迟时间没到返回 null if (first == null || first.getDelay(NANOSECONDS) > 0) @@ -7257,7 +7304,7 @@ DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先 // 置空 queue[s] = null; if (s != 0) - // 从索引处0开始向下调整 + // 从索引处 0 开始向下调整 siftDown(0, x); // 出队的元素索引设置为 -1 setIndex(f, -1); @@ -7282,7 +7329,7 @@ DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先 // 获取头节点的剩延迟时间是否到时 long delay = first.getDelay(NANOSECONDS); if (delay <= 0) - // 获取头节点并调整堆,选择延迟时间最小的节点放入头部 + // 到时了,获取头节点并调整堆,重新选择延迟时间最小的节点放入头部 return finishPoll(first); // 逻辑到这说明头节点的延迟时间还没到 @@ -7317,7 +7364,7 @@ DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先 } ``` -* remove():删除节点,堆移除一个元素的时间复杂度是 O(log n),延迟任务维护了 heapIndex,直接访问的时间复杂度是 O(1),从而可以更快的移除元素,任务在队列中被取消后会进入该逻辑 +* remove():删除节点,堆移除一个元素的时间复杂度是 O(log n),**延迟任务维护了 heapIndex**,直接访问的时间复杂度是 O(1),从而可以更快的移除元素,任务在队列中被取消后会进入该逻辑 ```java public boolean remove(Object x) { @@ -7375,7 +7422,7 @@ DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先 public ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit) { // 判空 if (command == null || unit == null) throw new NullPointerException(); - // 没有做任何操作,直接将 task 返回,该方法主要目的是用于子类扩展 + // 没有做任何操作,直接将 task 返回,该方法主要目的是用于子类扩展,并且【根据延迟时间设置任务触发的时间点】 RunnableScheduledFuture t = decorateTask(command, new ScheduledFutureTask( command, null, triggerTime(delay, unit))); // 延迟执行 @@ -7406,7 +7453,7 @@ DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先 long headDelay = head.getDelay(NANOSECONDS); // 判断一下队首的delay是不是负数,如果是正数就不用管,怎么减都不会溢出 // 否则拿当前 delay 减去队首的 delay 来比较看,如果不出现上溢,排序不会乱 - // 不然就把当前 delay 值给调整为Long.MAX_VALUE + 队首 delay + // 不然就把当前 delay 值给调整为 Long.MAX_VALUE + 队首 delay if (headDelay < 0 && (delay - headDelay < 0)) delay = Long.MAX_VALUE + headDelay; } @@ -7462,7 +7509,7 @@ DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先 ##### 运行任务 -* delayedExecute():校验状态,延迟或周期性任务的主要执行方法 +* delayedExecute():**校验线程池状态**,延迟或周期性任务的主要执行方法 ```java private void delayedExecute(RunnableScheduledFuture task) { @@ -7470,7 +7517,7 @@ DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先 if (isShutdown()) reject(task); else { - // 把当前任务放入阻塞队列,因为需要重新获取执行时间最近的 + // 把当前任务放入阻塞队列,因为需要【获取执行时间最近的】 super.getQueue().add(task); // 线程池状态为 SHUTDOWN 并且不允许执行任务了,就从队列删除该任务,并设置任务的状态为取消状态 if (isShutdown() && !canRunInCurrentRunState(task.isPeriodic()) && remove(task)) @@ -7482,7 +7529,7 @@ DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先 } ``` -* ensurePrestart():开启线程执行任务 +* ensurePrestart():**开启线程执行任务** ```java // ThreadPoolExecutor#ensurePrestart @@ -7492,7 +7539,7 @@ DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先 if (wc < corePoolSize) // 第二个参数 true 表示采用核心线程数量限制,false 表示采用 maximumPoolSize addWorker(null, true); - // corePoolSize = 0的情况,至少开启一个线程 + // corePoolSize = 0的情况,至少开启一个线程,【担保机制】 else if (wc == 0) addWorker(null, false); } @@ -7502,8 +7549,7 @@ DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先 ```java boolean canRunInCurrentRunState(boolean periodic) { - // isRunningOrShutdown 的参数为布尔值,true 则表示shutdown状态也返回true,否则只有running状态返回ture - // 根据是否时周期任务来判断是否shutdown了仍然可以执行。 + // 根据是否是周期任务判断,在线程池 shutdown 后是否继续执行该任务,默认非周期任务是继续执行的 return isRunningOrShutdown(periodic ? continueExistingPeriodicTasksAfterShutdown : executeExistingDelayedTasksAfterShutdown); } @@ -7518,7 +7564,7 @@ DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先 boolean keepDelayed = getExecuteExistingDelayedTasksAfterShutdownPolicy(); // shutdown 后是否仍然执行周期任务 boolean keepPeriodic = getContinueExistingPeriodicTasksAfterShutdownPolicy(); - // 如果两者皆不可则对队列中所有 任务 调用 cancel 取消并清空队列 + // 如果两者皆不可,则对队列中【所有任务】调用 cancel 取消并清空队列 if (!keepDelayed && !keepPeriodic) { for (Object e : q.toArray()) if (e instanceof RunnableScheduledFuture) @@ -7538,7 +7584,7 @@ DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先 } } } - // 因为任务被从队列中清理掉,所以需要调用 tryTerminate 尝试改变 executor 的状态 + // 因为任务被从队列中清理掉,所以需要调用 tryTerminate 尝试【改变线程池的状态】 tryTerminate(); } ``` @@ -7555,9 +7601,9 @@ DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先 Fork/Join:线程池的实现,体现是分治思想,适用于能够进行任务拆分的 CPU 密集型运算,用于**并行计算** -任务拆分:是将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的一些计算,如归并排序、斐波那契数列都可以用分治思想进行求解 +任务拆分:将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的一些计算,如归并排序、斐波那契数列都可以用分治思想进行求解 -* Fork/Join 在分治的基础上加入了多线程,把每个任务的分解和合并交给不同的线程来完成,提升了运算效率 +* Fork/Join 在**分治的基础上加入了多线程**,把每个任务的分解和合并交给不同的线程来完成,提升了运算效率 * ForkJoin 使用 ForkJoinPool 来启动,是一个特殊的线程池,默认会创建与 CPU 核心数大小相同的线程池 * 任务有返回值继承 RecursiveTask,没有返回值继承 RecursiveAction @@ -7763,7 +7809,7 @@ AQS 核心思想: * 如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置锁定状态 -* 被请求的共享资源被占用,AQS 用 CLH 队列实现线程阻塞等待以及被唤醒时锁分配的(park)机制,将暂时获取不到锁的线程加入到队列中 +* 请求的共享资源被占用,AQS 用 CLH 队列实现线程阻塞等待以及被唤醒时锁分配的机制,将暂时获取不到锁的线程加入到队列中 CLH 是一种基于单向链表的**高性能、公平的自旋锁**,AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配 @@ -7782,7 +7828,7 @@ AQS 核心思想: * 获取锁: ```java - while(state 状态不允许获取) { //tryAcquire(arg) + while(state 状态不允许获取) { // tryAcquire(arg) if(队列中还没有此线程) { 入队并阻塞 park } @@ -7793,7 +7839,7 @@ AQS 核心思想: * 释放锁: ```java - if(state 状态允许了) { //tryRelease(arg) + if(state 状态允许了) { // tryRelease(arg) 恢复阻塞的线程(s) unpark } ``` @@ -7814,7 +7860,7 @@ AbstractQueuedSynchronizer 中 state 设计: * `protected final int getState()`:获取 state 状态 * `protected final void setState(int newState)`:设置 state 状态 - * `protected final boolean compareAndSetState(int expect,int update)`:**cas** 安全设置 state + * `protected final boolean compareAndSetState(int expect,int update)`:**CAS** 安全设置 state 封装线程的 Node 节点中 waitstate 设计: @@ -7827,7 +7873,7 @@ AbstractQueuedSynchronizer 中 state 设计: volatile int waitStatus; // 由于超时或中断,此节点被取消,不会再改变状态 static final int CANCELLED = 1; - // 此节点后面的节点已(或即将)被阻止(通过park),当前节点在释放或取消时必须唤醒后面的节点 + // 此节点后面的节点已(或即将)被阻止(通过park),【当前节点在释放或取消时必须唤醒后面的节点】 static final int SIGNAL = -1; // 此节点当前在条件队列中 static final int CONDITION = -2; @@ -7860,7 +7906,7 @@ AbstractQueuedSynchronizer 中 state 设计: volatile Node prev; // next 指向后继节点 volatile Node next; - // 当前node封装的线程 + // 当前 node 封装的线程 volatile Thread thread; // 条件队列是单向链表,只有后继指针 Node nextWaiter; @@ -7993,7 +8039,7 @@ class MyLock implements Lock { #### 锁对比 -ReentrantLock 相对于 synchronized 它具备如下特点: +ReentrantLock 相对于 synchronized 具备如下特点: 1. 锁的实现:synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的 2. 性能:新版本 Java 对 synchronized 进行了很多优化,synchronized 与 ReentrantLock 大致相同 @@ -8084,40 +8130,46 @@ public ReentrantLock() { NonfairSync 继承自 AQS -没有竞争:ExclusiveOwnerThread 属于 Thread-0,state 设置为 1 - ```java -// ReentrantLock.NonfairSync#lock -final void lock() { - // 用 cas 尝试(仅尝试一次)将 state 从 0 改为 1, 如果成功表示【获得了独占锁】 - if (compareAndSetState(0, 1)) - // 设置当前线程为独占线程 - setExclusiveOwnerThread(Thread.currentThread()); - else - acquire(1);//失败进入 +public void lock() { + sync.lock(); } + ``` -第一个竞争出现: +* 没有竞争:ExclusiveOwnerThread 属于 Thread-0,state 设置为 1 -```java -// AbstractQueuedSynchronizer#acquire -public final void acquire(int arg) { - // tryAcquire 尝试获取锁失败返回为 false 时, 会调用 addWaiter 将当前线程封装成node入队, - // 然后 acquireQueued 挂起当前线程,返回 true 表示挂起过程中线程被中断唤醒过,false 表示未被中断过 - if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) - // 如果线程被中断了逻辑来到这,完成一次真正的打断效果 - selfInterrupt(); -} -``` + ```java + // ReentrantLock.NonfairSync#lock + final void lock() { + // 用 cas 尝试(仅尝试一次)将 state 从 0 改为 1, 如果成功表示【获得了独占锁】 + if (compareAndSetState(0, 1)) + // 设置当前线程为独占线程 + setExclusiveOwnerThread(Thread.currentThread()); + else + acquire(1);//失败进入 + } + ``` - +* 第一个竞争出现:Thread-1 执行,CAS 尝试将 state 由 0 改为 1,结果失败(第一次),进入 acquire 逻辑 -Thread-1 执行: + ```java + // AbstractQueuedSynchronizer#acquire + public final void acquire(int arg) { + // tryAcquire 尝试获取锁失败时, 会调用 addWaiter 将当前线程封装成node入队,acquireQueued 阻塞当前线程, + // acquireQueued 返回 true 表示挂起过程中线程被中断唤醒过,false 表示未被中断过 + if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) + // 如果线程被中断了逻辑来到这,完成一次真正的打断效果 + selfInterrupt(); + } + ``` -* CAS 尝试将 state 由 0 改为 1,结果失败(第一次) + + +* 进入 tryAcquire 尝试获取锁逻辑,这时 state 已经是1,结果仍然失败(第二次),加锁成功有两种情况: -* 进入 tryAcquire 尝试获取锁逻辑,这时 state 已经是1,结果仍然失败(第二次) + * 当前 AQS 处于无锁状态 + * 加锁线程就是当前线程,说明发生了锁重入 ```java // ReentrantLock.NonfairSync#tryAcquire @@ -8129,7 +8181,7 @@ Thread-1 执行: final Thread current = Thread.currentThread(); // state 值 int c = getState(); - // 条件成立说明当前处于无锁状态 + // 条件成立说明当前处于【无锁状态】 if (c == 0) { //如果还没有获得锁,尝试用cas获得,这里体现非公平性: 不去检查 AQS 队列是否有阻塞线程直接获取锁 if (compareAndSetState(0, acquires)) { @@ -8138,7 +8190,7 @@ Thread-1 执行: return true; } } - // 如果已经有线程获得了锁, 独占锁线程还是当前线程, 表示发生了锁重入 + // 如果已经有线程获得了锁, 独占锁线程还是当前线程, 表示【发生了锁重入】 else if (current == getExclusiveOwnerThread()) { // 更新锁重入的值 int nextc = c + acquires; @@ -8157,8 +8209,7 @@ Thread-1 执行: * 接下来进入 addWaiter 逻辑,构造 Node 队列,前置条件是当前线程获取锁失败,说明有线程占用了锁 * 图中黄色三角表示该 Node 的 waitStatus 状态,其中 0 为默认**正常状态** - * Node 的创建是懒惰的 - * 其中第一个 Node 称为 **Dummy(哑元)或哨兵**,用来占位,并不关联线程 + * Node 的创建是懒惰的,其中第一个 Node 称为 **Dummy(哑元)或哨兵**,用来占位,并不关联线程 ```java // AbstractQueuedSynchronizer#addWaiter,返回当前线程的 node 节点 @@ -8170,13 +8221,13 @@ Thread-1 执行: if (pred != null) { // 将当前节点的前驱节点指向 尾节点 node.prev = pred; - // 通过 cas 将 Node 对象加入 AQS 队列,成为尾节点 + // 通过 cas 将 Node 对象加入 AQS 队列,成为尾节点,【尾插法】 if (compareAndSetTail(pred, node)) { pred.next = node;// 双向链表 return node; } } - // 等待队列为空或者 CAS 失败进入逻辑 + // 初始时没有队列为空,或者 CAS 失败进入这里 enq(node); return node; } @@ -8188,17 +8239,17 @@ Thread-1 执行: // 自旋入队,必须入队成功才结束循环 for (;;) { Node t = tail; - // 说明当前锁被占用,且当前线程可能是【第一个获取锁失败】的线程,还没有建立队列 + // 说明当前锁被占用,且当前线程可能是【第一个获取锁失败】的线程,【还没有建立队列】 if (t == null) { // 设置一个【哑元节点】,头尾指针都指向该节点 if (compareAndSetHead(new Node())) tail = head; } else { - //自旋到这,普通入队方式 + // 自旋到这,普通入队方式,【尾插法】 node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; - return t; //返回当前 node 的前驱节点 + return t; // 返回当前 node 的前驱节点 } } } @@ -8209,21 +8260,21 @@ Thread-1 执行: * 线程节点加入阻塞队列成功,进入 AbstractQueuedSynchronizer#acquireQueued 逻辑阻塞线程 - * acquireQueued 会在一个死循环中不断尝试获得锁,失败后进入 park 阻塞 + * acquireQueued 会在一个自旋中不断尝试获得锁,失败后进入 park 阻塞 * 如果当前线程是在 head 节点后,会再次 tryAcquire 尝试获取锁,state 仍为 1 则失败(第三次) ```java final boolean acquireQueued(final Node node, int arg) { - //true 表示当前线程抢占锁失败,false 表示成功 + // true 表示当前线程抢占锁失败,false 表示成功 boolean failed = true; try { - // 表示当前线程是否被中断 + // 中断标记,表示当前线程是否被中断 boolean interrupted = false; for (;;) { // 获得当前线程节点的前驱节点 final Node p = node.predecessor(); - // 前驱节点 head, FIFO 队列的特性表示轮到当前线程可以去获取锁 + // 前驱节点是 head, FIFO 队列的特性表示轮到当前线程可以去获取锁 if (p == head && tryAcquire(arg)) { // 获取成功, 设置当前线程自己的 node 为 head setHead(node); @@ -8239,7 +8290,7 @@ Thread-1 执行: interrupted = true; } } finally { - // 可打断模式下才会进入该逻辑 + // 【可打断模式下才会进入该逻辑】 if (failed) cancelAcquire(node); } @@ -8254,28 +8305,26 @@ Thread-1 执行: // 表示前置节点是个可以唤醒当前节点的节点,返回 true if (ws == Node.SIGNAL) return true; - // 前置节点的状态处于取消状态,需要删除前面所有取消的节点, 返回到外层循环重试 + // 前置节点的状态处于取消状态,需要【删除前面所有取消的节点】, 返回到外层循环重试 if (ws > 0) { do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); // 获取到非取消的节点,连接上当前节点 pred.next = node; - // 【默认情况下 node 的 waitStatus 是 0,进入这里的逻辑】 + // 默认情况下 node 的 waitStatus 是 0,进入这里的逻辑 } else { - // 设置上一个节点状态为 Node.SIGNAL,返回外层循环重试 + // 【设置上一个节点状态为 Node.SIGNAL】,返回外层循环重试 compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } - // 返回不应该 park + // 返回不应该 park,再次尝试一次 return false; } ``` * shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued ,再次 tryAcquire 尝试获取锁,这时 state 仍为 1 获取失败(第四次) - * 当再次进入 shouldParkAfterFailedAcquire 时,这时其前驱 node 的 waitStatus 已经是 -1,返回 true - * 进入 parkAndCheckInterrupt, Thread-1 park(灰色表示),再有多个线程经历竞争失败后: - - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ReentrantLock-非公平锁3.png) + * 当再次进入 shouldParkAfterFailedAcquire 时,这时其前驱 node 的 waitStatus 已经是 -1 了,返回 true + * 进入 parkAndCheckInterrupt, Thread-1 park(灰色表示) ```java private final boolean parkAndCheckInterrupt() { @@ -8286,7 +8335,11 @@ Thread-1 执行: } ``` - +* 再有多个线程经历竞争失败后: + + ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ReentrantLock-非公平锁3.png) + + *** @@ -8306,16 +8359,16 @@ Thread-0 释放锁,进入 release 流程 * 进入 tryRelease,设置 exclusiveOwnerThread 为 null,state = 0 -* 当前队列不为 null,并且 head 的 waitStatus = -1,进入 unparkSuccessor +* 当前队列不为 null,并且 head 的 waitStatus = -1,进入 unparkSuccessor ```java // AbstractQueuedSynchronizer#release public final boolean release(int arg) { - // 尝试释放锁,tryRelease 返回 true 表示当前线程已经完全释放锁 + // 尝试释放锁,tryRelease 返回 true 表示当前线程已经【完全释放锁,重入的释放了】 if (tryRelease(arg)) { // 队列头节点 Node h = head; - // 头节点什么时候是空?没有发生锁竞争,没有竞争线程帮忙创建哑元节点 + // 头节点什么时候是空?没有发生锁竞争,没有竞争线程创建哑元节点 // 条件成立说明阻塞队列有等待线程,需要唤醒 head 节点后面的线程 if (h != null && h.waitStatus != 0) unparkSuccessor(h); @@ -8348,7 +8401,7 @@ Thread-0 释放锁,进入 release 流程 * 进入 AbstractQueuedSynchronizer#unparkSuccessor 方法,唤醒当前节点的后继节点 - * 找到队列中离 head 最近的一个 Node(没取消的),unpark 恢复其运行,本例中即为 Thread-1 + * 找到队列中距离 head 最近的一个没取消的 Node,unpark 恢复其运行,本例中即为 Thread-1 * 回到 Thread-1 的 acquireQueued 流程 ```java @@ -8356,14 +8409,14 @@ Thread-0 释放锁,进入 release 流程 // 当前节点的状态 int ws = node.waitStatus; if (ws < 0) - // 尝试重置状态为 0,因为当前节点要完成对后续节点的唤醒任务了,不需要 -1 了 + // 【尝试重置状态为 0】,因为当前节点要完成对后续节点的唤醒任务了,不需要 -1 了 compareAndSetWaitStatus(node, ws, 0); // 找到需要 unpark 的节点,当前节点的下一个 Node s = node.next; // 已取消的节点不能唤醒,需要找到距离头节点最近的非取消的节点 if (s == null || s.waitStatus > 0) { s = null; - // AQS 队列从后至前找需要 unpark 的节点,直到 t == 当前的 node 为止,找不到就不唤醒了 + // AQS 队列【从后至前】找需要 unpark 的节点,直到 t == 当前的 node 为止,找不到就不唤醒了 for (Node t = tail; t != null && t != node; t = t.prev) // 说明当前线程状态需要被唤醒 if (t.waitStatus <= 0) @@ -8379,12 +8432,12 @@ Thread-0 释放锁,进入 release 流程 * 唤醒的线程会从 park 位置开始执行,如果加锁成功(没有竞争),会设置 * exclusiveOwnerThread 为 Thread-1,state = 1 - * head 指向刚刚 Thread-1 所在的 Node,该 Node 清空 Thread - * 原本的 head 因为从链表断开,而可被垃圾回收(途中有错误,原来的头节点的 waitStatus 为 0) + * head 指向刚刚 Thread-1 所在的 Node,该 Node 会清空 Thread + * 原本的 head 因为从链表断开,而可被垃圾回收(图中有错误,原来的头节点的 waitStatus 被改为 0 了) ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ReentrantLock-非公平锁4.png) -* 如果这时候有其它线程来竞争**(非公平)**,例如这时有 Thread-4 来了并抢占了锁 +* 如果这时有其它线程来竞争**(非公平)**,例如这时有 Thread-4 来了并抢占了锁 * Thread-4 被设置为 exclusiveOwnerThread,state = 1 * Thread-1 再次进入 acquireQueued 流程,获取锁失败,重新进入 park 阻塞 @@ -8427,14 +8480,15 @@ static final class FairSync extends Sync { ```java public final boolean hasQueuedPredecessors() { - Node t = tail; - Node h = head; + Node t = tail; + Node h = head; Node s; // 头尾指向一个节点,链表为空,返回false - return h != t && - // 头尾之间有节点,判断头节点的下一个是不是空 - // 不是空进入最后的判断,第二个节点的线程是否是本线程 - ((s = h.next) == null || s.thread != Thread.currentThread());} + return h != t && + // 头尾之间有节点,判断头节点的下一个是不是空 + // 不是空进入最后的判断,第二个节点的线程是否是本线程,不是返回 true,表示当前节点有前驱节点,洛矶歪 + ((s = h.next) == null || s.thread != Thread.currentThread()); +} ``` @@ -8542,7 +8596,7 @@ public static void main(String[] args) throws InterruptedException { ##### 实现原理 -* 不可打断模式:即使它被打断,仍会驻留在 AQS 队列中,一直要**等到获得锁后才能得知自己被打断**了 +* 不可打断模式:即使它被打断,仍会驻留在 AQS 阻塞队列中,一直要**等到获得锁后才能得知自己被打断**了 ```java public final void acquire(int arg) { @@ -8580,7 +8634,7 @@ public static void main(String[] args) throws InterruptedException { } } private final boolean parkAndCheckInterrupt() { - // 阻塞当前线程,如果打断标记已经是 true, 则 park 会失效 + // 阻塞当前线程,如果打断标记已经是 true, 则 park 会失效 LockSupport.park(this); // 判断当前线程是否被打断,清除打断标记,被打断返回true return Thread.interrupted(); @@ -8612,11 +8666,11 @@ public static void main(String[] args) throws InterruptedException { for (;;) { //... if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) - // 在 park 过程中如果被 interrupt 会抛出异常, 而不会再次进入循环获取锁后才完成打断效果 + // 【在 park 过程中如果被 interrupt 会抛出异常】, 而不会再次进入循环获取锁后才完成打断效果 throw new InterruptedException(); } } finally { - // 抛出异常前x进入这里 + // 抛出异常前会进入这里 if (failed) // 取消当前线程的节点 cancelAcquire(node); @@ -8625,7 +8679,7 @@ public static void main(String[] args) throws InterruptedException { ``` ```java - // 所有的节取消节点出队的逻辑 + // 取消节点出队的逻辑 private void cancelAcquire(Node node) { // 判空 if (node == null) @@ -8634,7 +8688,7 @@ public static void main(String[] args) throws InterruptedException { node.thread = null; // 获取当前取消的 node 的前驱节点 Node pred = node.prev; - // 前驱节点也被取消了,循环找到前面最近的不是取消节点的节点 + // 前驱节点也被取消了,循环找到前面最近的没被取消的节点 while (pred.waitStatus > 0) node.prev = pred = pred.prev; @@ -8663,7 +8717,7 @@ public static void main(String[] args) throws InterruptedException { Node next = node.next; // 当前节点的后继节点是正常节点 if (next != null && next.waitStatus <= 0) - // 把 前驱节点的后继节点 设置为 当前节点的后继节点,从队列中删除了当前节点 + // 把 前驱节点的后继节点 设置为 当前节点的后继节点,【从队列中删除了当前节点】 compareAndSetNext(pred, predNext, next); } else { // 当前节点是 head.next 节点,唤醒当前节点的后继节点 @@ -8732,11 +8786,20 @@ public static void main(String[] args) { ##### 实现原理 +* 成员变量:指定超时限制的阈值,小于该值的线程不会被挂起 + + ```java + static final long spinForTimeoutThreshold = 1000L; + ``` + + 超时时间设置的小于该值,就会被禁止挂起,因为阻塞在唤醒的成本太高,不如选择自旋空转 + * tryLock() ```java - public boolean tryLock() { - return sync.nonfairTryAcquire(1);//只尝试一次 + public boolean tryLock() { + // 只尝试一次 + return sync.nonfairTryAcquire(1); } ``` @@ -8746,7 +8809,7 @@ public static void main(String[] args) { public final boolean tryAcquireNanos(int arg, long nanosTimeout) { if (Thread.interrupted()) throw new InterruptedException(); - //tryAcquire 尝试一次 + // tryAcquire 尝试一次 return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout); } protected final boolean tryAcquire(int acquires) { @@ -8769,9 +8832,10 @@ public static void main(String[] args) { if (nanosTimeout <= 0L) //时间已到 return false; if (shouldParkAfterFailedAcquire(p, node) && + // 如果 nanosTimeout 大于该值,才有阻塞的意义,否则直接自旋会好点 nanosTimeout > spinForTimeoutThreshold) LockSupport.parkNanos(this, nanosTimeout); - // 被打断会报异常 + // 【被打断会报异常】 if (Thread.interrupted()) throw new InterruptedException(); } @@ -8843,7 +8907,7 @@ class Chopstick extends ReentrantLock { ##### 基本使用 -synchronized 的条件变量,是当条件不满足时进入 waitSet 等待;ReentrantLock 的条件变量比 synchronized 强大之处在于,它支持多个条件变量 +synchronized 的条件变量,是当条件不满足时进入 WaitSet 等待;ReentrantLock 的条件变量比 synchronized 强大之处在于,它支持多个条件变量 ReentrantLock 类获取 Condition 对象:`public Condition newCondition()` @@ -8855,7 +8919,7 @@ Condition 类 API: 使用流程: * **await / signal 前需要获得锁** -* await 执行后,会释放锁进入 conditionObject 等待 +* await 执行后,会释放锁进入 ConditionObject 等待 * await 的线程被唤醒去重新竞争 lock 锁 * **线程在条件队列被打断会抛出中断异常** @@ -8905,6 +8969,8 @@ public static void main(String[] args) throws InterruptedException { ###### await +总体流程是将 await 线程包装成 node 节点放入 ConditionObject 的阻条件塞队列,如果被唤醒就将 node 转移到 AQS 的执行阻塞队列,等待获取锁 + * 开始 Thread-0 持有锁,调用 await,线程进入 ConditionObject 等待,直到被唤醒或打断,调用 await 方法的线程都是持锁状态的,所以说逻辑里**不存在并发** ```java @@ -8912,7 +8978,7 @@ public static void main(String[] args) throws InterruptedException { // 判断当前线程是否是中断状态,是就直接给个中断异常 if (Thread.interrupted()) throw new InterruptedException(); - // 将调用 await 的线程包装成 Node 添加到条件队列并返回 + // 将调用 await 的线程包装成 Node,添加到条件队列并返回 Node node = addConditionWaiter(); // 完全释放节点持有的锁,因为其他线程唤醒当前线程的前提是【持有锁】 int savedState = fullyRelease(node); @@ -8929,7 +8995,7 @@ public static void main(String[] args) throws InterruptedException { } // 逻辑到这说明当前线程退出等待队列,进入【阻塞队列】 - // 释放了多少锁就重新获取多少锁,获取锁成功判断打断模式 + // 释放了多少锁就【重新获取多少锁】,获取锁成功判断打断模式 if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT; @@ -9003,7 +9069,7 @@ public static void main(String[] args) throws InterruptedException { if (next == null) lastWaiter = trail; } else { - // 正常节点赋值给 trail + // trail 指向的是正常节点 trail = t; } // 把 t.next 赋值给 t,循环遍历 @@ -9040,11 +9106,11 @@ public static void main(String[] args) throws InterruptedException { } ``` -* fullyRelease 中会 unpark AQS 队列中的下一个节点竞争锁,假设 Thread-1 竞争成功,park 阻塞 Thread-0 +* fullyRelease 中会 unpark AQS 队列中的下一个节点竞争锁,假设 Thread-1 竞争成功 ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ReentrantLock-条件变量2.png) -* 进入 isOnSyncQueue 逻辑判断节点是否移动到阻塞队列 +* 进入 isOnSyncQueue 逻辑判断节点**是否移动到阻塞队列**,没有就 park 阻塞 Thread-0 ```java final boolean isOnSyncQueue(Node node) { @@ -9054,7 +9120,7 @@ public static void main(String[] args) throws InterruptedException { // 说明当前节点已经成功入队到阻塞队列,且当前节点后面已经有其它 node,因为条件队列的 next 指针为 null if (node.next != null) return true; - // 说明可能在阻塞队列,但是是尾节点 + // 说明【可能在阻塞队列,但是是尾节点】 // 从阻塞队列的尾节点开始向前遍历查找 node,如果查找到返回 true,查找不到返回 false return findNodeFromTail(node); } @@ -9071,13 +9137,13 @@ public static void main(String[] args) throws InterruptedException { ``` ```java - // 这个方法只有在线程是被中断唤醒时才会调用 + // 这个方法只有在线程是被打断唤醒时才会调用 final boolean transferAfterCancelledWait(Node node) { // 条件成立说明当前node一定是在条件队列内,因为 signal 迁移节点到阻塞队列时,会将节点的状态修改为 0 if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) { // 把【中断唤醒的 node 加入到阻塞队列中】 enq(node); - // 表示是在条件队列内被中断了,设置为 THROW_IE -1 + // 表示是在条件队列内被中断了,设置为 THROW_IE 为 -1 return true; } @@ -9089,7 +9155,7 @@ public static void main(String[] args) throws InterruptedException { while (!isOnSyncQueue(node)) Thread.yield(); - // 表示当前节点被中断唤醒时不在条件队列了,设置为 REINTERRUPT 1 + // 表示当前节点被中断唤醒时不在条件队列了,设置为 REINTERRUPT 为 1 return false; } ``` @@ -9098,11 +9164,11 @@ public static void main(String[] args) throws InterruptedException { ```java private void reportInterruptAfterWait(int interruptMode) throws InterruptedException { - // 条件成立说明在条件队列内发生过中断,此时 await 方法抛出中断异常 + // 条件成立说明【在条件队列内发生过中断,此时 await 方法抛出中断异常】 if (interruptMode == THROW_IE) throw new InterruptedException(); - // 条件成立说明在条件队列外发生的中断,此时设置当前线程的中断标记位为 true + // 条件成立说明【在条件队列外发生的中断,此时设置当前线程的中断标记位为 true】 else if (interruptMode == REINTERRUPT) // 进行一次自己打断,产生中断的效果 selfInterrupt(); @@ -9119,7 +9185,7 @@ public static void main(String[] args) throws InterruptedException { ###### signal -* 假设 Thread-1 要来唤醒 Thread-0,进入 ConditionObject 的 doSignal 流程,取得等待队列中第一个 Node,即 Thread-0 所在 Node,必须持有锁才能唤醒, 因此 doSignal 内**线程安全** +* 假设 Thread-1 要来唤醒 Thread-0,进入 ConditionObject 的 doSignal 流程,**取得等待队列中第一个 Node**,即 Thread-0 所在 Node,必须持有锁才能唤醒, 因此 doSignal 内线程安全 ```java public final void signal() { @@ -9135,14 +9201,14 @@ public static void main(String[] args) throws InterruptedException { ``` ```java - // 唤醒 - 将没取消的第一个节点转移至 AQS 队列尾部 + // 唤醒 - 【将没取消的第一个节点转移至 AQS 队列尾部】 private void doSignal(Node first) { do { - // 成立说明当前节点是尾节点,所以队列中只有当前一个节点了 + // 成立说明当前节点的下一个节点是 null,当前节点是尾节点了,队列中只有当前一个节点了 if ((firstWaiter = first.nextWaiter) == null) lastWaiter = null; first.nextWaiter = null; - // 将等待队列中的 Node 转移至 AQS 队列, 不成功且还有节点则继续循环 + // 将等待队列中的 Node 转移至 AQS 队列,不成功且还有节点则继续循环 } while (!transferForSignal(first) && (first = firstWaiter) != null); } @@ -9159,24 +9225,25 @@ public static void main(String[] args) throws InterruptedException { } ``` -* 执行 transferForSignal,将该 Node 加入 AQS 阻塞队列尾部,将 Thread-0 的 waitStatus 改为 0,Thread-3 的 waitStatus 改为 -1 +* 执行 transferForSignal,**先将节点的 waitStatus 改为 0,然后加入 AQS 阻塞队列尾部**,将 Thread-3 的 waitStatus 改为 -1 ```java // 如果节点状态是取消, 返回 false 表示转移失败, 否则转移成功 final boolean transferForSignal(Node node) { // CAS 修改当前节点的状态,修改为 0,因为当前节点马上要迁移到阻塞队列了 // 如果状态已经不是 CONDITION, 说明线程被取消(await 释放全部锁失败)或者被中断(可打断 cancelAcquire) - // 返回函数调用处继续寻找下一个节点 if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) + // 返回函数调用处继续寻找下一个节点 return false; - // 将当前 node 入阻塞队列,p 是当前节点在阻塞队列的前驱节点 - // 所以是 【先改状态,再进行迁移】 + + // 【先改状态,再进行迁移】 + // 将当前 node 入阻塞队列,p 是当前节点在阻塞队列的【前驱节点】 Node p = enq(node); int ws = p.waitStatus; - // 如果前驱节点被取消或者不能设置状态为 Node.SIGNAL + // 如果前驱节点被取消或者不能设置状态为 Node.SIGNAL,就 unpark 取消当前节点线程的阻塞状态, + // 让 thread-0 线程竞争锁,重新同步状态 if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) - // unpark 取消阻塞, 让 thread-0 线程竞争锁,重新同步状态 LockSupport.unpark(node.thread); return true; } @@ -9343,12 +9410,12 @@ public static void main(String[] args) { // 锁重入计数超过低 16 位, 报异常 if (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error("Maximum lock count exceeded"); - // 写锁重入, 获得锁成功 + // 【写锁重入, 获得锁成功】 setState(c + acquires); return true; } - // c == 0,没有任何锁,判断写锁是否该阻塞,不阻塞尝试获取锁,获取失败返回 false + // c == 0,没有任何锁,判断写锁是否该阻塞,是 false 就尝试获取锁,失败返回 false if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) return false; // 获得锁成功,设置锁的持有线程为当前线程 @@ -9388,7 +9455,7 @@ public static void main(String[] args) { Thread current = Thread.currentThread(); int c = getState(); // 低 16 位, 代表写锁的 state - // 如果是其它线程持有写锁, 并且写锁的持有者不是当前线程,获取读锁失败,写锁允许降级 + // 如果是其它线程持有写锁, 并且写锁的持有者不是当前线程,获取读锁失败,【写锁允许降级】 if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return -1; @@ -9411,7 +9478,7 @@ public static void main(String[] args) { } ``` -* 进入 sync.doAcquireShared(1) 流程,首先也是调用 addWaiter 添加节点,不同之处在于节点被设置为 Node.SHARED 模式而非 Node.EXCLUSIVE 模式,注意此时 t2 仍处于活跃状态 +* 获取读锁失败,进入 sync.doAcquireShared(1) 流程开始阻塞,首先也是调用 addWaiter 添加节点,不同之处在于节点被设置为 Node.SHARED 模式而非 Node.EXCLUSIVE 模式,注意此时 t2 仍处于活跃状态 ```java private void doAcquireShared(int arg) { @@ -9498,7 +9565,7 @@ public static void main(String[] args) { * 唤醒流程 sync.unparkSuccessor,这时 t2 在 doAcquireShared 的 parkAndCheckInterrupt() 处恢复运行,继续循环,执行 tryAcquireShared 成功则让读锁计数加一 -* 接下来 t2 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点;还会检查下一个节点是否是 shared,如果是则调用 doReleaseShared() 将 head 的状态从 -1 改为 0 并唤醒下一个节点,这时 t3 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行 +* 接下来 t2 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点;还会检查下一个节点是否是 shared,如果是则调用 doReleaseShared() 将 head 的状态从 -1 改为 0 并唤醒下一个节点,这时 t3 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行,**唤醒连续的所有的共享节点** ```java private void setHeadAndPropagate(Node node, int propagate) { @@ -9508,8 +9575,9 @@ public static void main(String[] args) { // propagate 表示有共享资源(例如共享读锁或信号量),为 0 就没有资源 if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { + // 获取下一个节点 Node s = node.next; - // 如果是最后一个节点或者是【等待共享读锁的节点】 + // 如果当前是最后一个节点,或者下一个节点是【等待共享读锁的节点】 if (s == null || s.isShared()) // 唤醒后继节点 doReleaseShared(); @@ -9535,10 +9603,11 @@ public static void main(String[] args) { unparkSuccessor(h); } // 如果已经是 0 了,改为 -3,用来解决传播性 - else if (ws == 0 && - !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) + else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; } + // 条件不成立说明被唤醒的节点非常积极,直接将自己设置为了新的head, + // 此时唤醒它的节点(前驱)执行 h == head 不成立,所以不会跳出循环,会继续唤醒新的 head 节点的后继节点 if (h == head) break; } @@ -9549,7 +9618,7 @@ public static void main(String[] args) { * 下一个节点不是 shared 了,因此不会继续唤醒 t4 所在节点 -* t2 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,但计数还不为零,t3 同样让计数减一,计数为零,进入doReleaseShared() 将头节点从 -1 改为 0 并唤醒下一个节点 +* t2 读锁解锁,进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,但计数还不为零,t3 同样让计数减一,计数为零,进入doReleaseShared() 将头节点从 -1 改为 0 并唤醒下一个节点 ```java public void unlock() { @@ -9570,9 +9639,9 @@ public static void main(String[] args) { for (;;) { int c = getState(); int nextc = c - SHARED_UNIT; - // 读锁的计数不会影响其它获取读锁线程, 但会影响其它获取写锁线程 - // 计数为 0 才是真正释放 + // 读锁的计数不会影响其它获取读锁线程, 但会影响其它获取写锁线程,计数为 0 才是真正释放 if (compareAndSetState(c, nextc)) + // 返回是否已经完全释放了 return nextc == 0; } } @@ -9605,7 +9674,7 @@ StampedLock:读写锁,该类自 JDK 8 加入,是为了进一步优化读 ```java long stamp = lock.readLock(); - lock.unlockRead(stamp); + lock.unlockRead(stamp); // 类似于 unpark,解指定的锁 ``` * 加解写锁: @@ -9651,12 +9720,13 @@ class DataContainerStamped { long stamp = lock.tryOptimisticRead(); System.out.println(new Date() + " optimistic read locking" + stamp); Thread.sleep(readTime); + // 戳有效,直接返回数据 if (lock.validate(stamp)) { Sout(new Date() + " optimistic read finish..." + stamp); return data; } - //锁升级 + // 说明其他线程更改了戳,需要锁升级了,从乐观读升级到读锁 System.out.println(new Date() + " updating to read lock" + stamp); try { stamp = lock.readLock(); @@ -9698,7 +9768,7 @@ class DataContainerStamped { #### 基本使用 -CountDownLatch:计数器,用来进行线程同步协作,等待所有线程完成 +CountDownLatch:计数器,用来进行线程同步协作,**等待所有线程完成** 构造器: @@ -9749,7 +9819,7 @@ public static void main(String[] args) throws InterruptedException { 阻塞等待: -* 线程调用 await() 等待其他线程完成任务: +* 线程调用 await() 等待其他线程完成任务:支持打断 ```java public void await() throws InterruptedException { @@ -9760,7 +9830,7 @@ public static void main(String[] args) throws InterruptedException { // 判断线程是否被打断,抛出打断异常 if (Thread.interrupted()) throw new InterruptedException(); - // 尝试获取共享锁,条件成立说明 state > 0,此时线程入队阻塞等待 + // 尝试获取共享锁,条件成立说明 state > 0,此时线程入队阻塞等待,等待其他线程获取共享资源 // 条件不成立说明 state = 0,此时不需要阻塞线程,直接结束函数调用 if (tryAcquireShared(arg) < 0) doAcquireSharedInterruptibly(arg); @@ -9852,7 +9922,7 @@ public static void main(String[] args) throws InterruptedException { protected boolean tryReleaseShared(int releases) { for (;;) { int c = getState(); - // 条件成立说明前面已经有线程触发唤醒操作了,这里返回 false + // 条件成立说明前面【已经有线程触发唤醒操作】了,这里返回 false if (c == 0) return false; // 计数器减一 @@ -9864,7 +9934,7 @@ public static void main(String[] args) throws InterruptedException { } ``` -* state = 0 时,当前线程需要执行唤醒阻塞节点的任务 +* state = 0 时,当前线程需要执行**唤醒阻塞节点的任务** ```java private void doReleaseShared() { @@ -9925,7 +9995,7 @@ public static void main(String[] args) { System.out.println("task1 task2 finish..."); }); - for (int i = 0; i < 3; i++) {// 循环重用 + for (int i = 0; i < 3; i++) { // 循环重用 service.submit(() -> { System.out.println("task1 begin..."); try { @@ -9960,7 +10030,7 @@ public static void main(String[] args) { ##### 成员属性 -* 全局锁: +* 全局锁:利用可重入锁实现的工具类 ```java // barrier 实现是依赖于Condition条件队列,condition 条件队列必须依赖lock才能使用 @@ -9989,7 +10059,7 @@ public static void main(String[] args) { private Generation generation = new Generation(); private static class Generation { // 表示当前“代”是否被打破,如果被打破再来到这一代的线程 就会直接抛出 BrokenException 异常 - // 且在这一代挂起的线程 都会被唤醒,然后抛出 BrokerException 异常。 + // 且在这一代挂起的线程都会被唤醒,然后抛出 BrokerException 异常。 boolean broken = false; } ``` @@ -10044,7 +10114,7 @@ public static void main(String[] args) { // 【如果当前代是已经被打破状态,则当前调用await方法的线程,直接抛出Broken异常】 if (g.broken) throw new BrokenBarrierException(); - // 如果当前线程的中断标记位为 true,则打破当前代,然后当前线程抛出中断异常 + // 如果当前线程被中断了,则打破当前代,然后当前线程抛出中断异常 if (Thread.interrupted()) { // 设置当前代的状态为 broken 状态,唤醒在 trip 条件队列内的线程 breakBarrier(); @@ -10062,7 +10132,7 @@ public static void main(String[] args) { try { final Runnable command = barrierCommand; if (command != null) - // 启动任务 + // 启动触发的任务 command.run(); // run()未抛出异常的话,启动标记设置为 true ranAction = true; @@ -10093,7 +10163,7 @@ public static void main(String[] args) { if (g == generation && !g.broken) { // 打破屏障 breakBarrier(); - // node节点在【条件队列】内收到中断信号时 会抛出中断异常 + // node 节点在【条件队列】内收到中断信号时 会抛出中断异常 throw ie; } else { // 等待过程中代变化了,完成一次自我打断 @@ -10125,9 +10195,9 @@ public static void main(String[] args) { ```java private void breakBarrier() { - //将代中的 broken 设置为 true,表示这一代是被打破了,再来到这一代的线程,直接抛出异常 + // 将代中的 broken 设置为 true,表示这一代是被打破了,再来到这一代的线程,直接抛出异常 generation.broken = true; - //重置 count 为 parties + // 重置 count 为 parties count = parties; // 将在trip条件队列内挂起的线程全部唤醒,唤醒后的线程会检查当前是否是打破的,然后抛出异常 trip.signalAll(); @@ -10224,7 +10294,7 @@ public static void main(String[] args) { 假设其中 Thread-1,Thread-2,Thread-4 CAS 竞争成功,permits 变为 0,而 Thread-0 和 Thread-3 竞争失败,进入 AQS 队列park 阻塞 ```java - // acquire() -> sync.acquireSharedInterruptibly(1); + // acquire() -> sync.acquireSharedInterruptibly(1),可中断 public final void acquireSharedInterruptibly(int arg) { if (Thread.interrupted()) throw new InterruptedException(); @@ -10238,7 +10308,7 @@ public static void main(String[] args) { // 非公平,公平锁会在循环内 hasQueuedPredecessors()方法判断阻塞队列是否有临头节点(第二个节点) final int nonfairTryAcquireShared(int acquires) { for (;;) { - // 获取 state ,state 这里表示通行证 + // 获取 state ,state 这里【表示通行证】 int available = getState(); // 计算当前线程获取通行证完成之后,通行证还剩余数量 int remaining = available - acquires; @@ -10262,7 +10332,7 @@ public static void main(String[] args) { final Node p = node.predecessor(); // 前驱节点是头节点可以再次获取许可 if (p == head) { - // 再次尝试获取许可 + // 再次尝试获取许可,【返回剩余的许可证数量】 int r = tryAcquireShared(arg); if (r >= 0) { // 成功后本线程出队(AQS), 所在 Node设置为 head @@ -10519,16 +10589,16 @@ class ThreadB extends Thread{ 三种集合: -* HashMap是线程不安全的,性能好 -* Hashtable线程安全基于synchronized,综合性能差,已经被淘汰 -* ConcurrentHashMap保证了线程安全,综合性能较好,不止线程安全,而且效率高,性能好 +* HashMap 是线程不安全的,性能好 +* Hashtable 线程安全基于 synchronized,综合性能差,已经被淘汰 +* ConcurrentHashMap 保证了线程安全,综合性能较好,不止线程安全,而且效率高,性能好 集合对比: 1. Hashtable 继承 Dictionary 类,HashMap、ConcurrentHashMap 继承 AbstractMap,均实现 Map 接口 2. Hashtable 底层是数组 + 链表,JDK8 以后 HashMap 和 ConcurrentHashMap 底层是数组 + 链表 + 红黑树 3. HashMap 线程非安全,Hashtable 线程安全,Hashtable 的方法都加了 synchronized 关来确保线程同步 -4. ConcurrentHashMap、Hashtable 不允许 null 值,HashMap 允许 null 值 +4. ConcurrentHashMap、Hashtable **不允许 null 值**,HashMap 允许 null 值 5. ConcurrentHashMap、HashMap 的初始容量为 16,Hashtable 初始容量为11,填充因子默认都是 0.75,两种 Map 扩容是当前容量翻倍:capacity * 2,Hashtable 扩容时是容量翻倍 + 1:capacity*2 + 1 ![ConcurrentHashMap数据结构](https://gitee.com/seazean/images/raw/master/Java/ConcurrentHashMap数据结构.png) @@ -10536,10 +10606,8 @@ class ThreadB extends Thread{ 工作步骤: 1. 初始化,使用 cas 来保证并发安全,懒惰初始化 table -2. 树化,当 table.length < 64 时,先尝试扩容,超过 64 时,并且 bin.length > 8 时,会将**链表树化**,树化过程 - 会用 synchronized 锁住链表头 -3. put,如果该 bin 尚未创建,只需要使用 cas 创建 bin;如果已经有了,锁住链表头进行后续 put 操作,元素 - 添加至 bin 的尾部 +2. 树化,当 table.length < 64 时,先尝试扩容,超过 64 时,并且 bin.length > 8 时,会将**链表树化**,树化过程会用 synchronized 锁住链表头 +3. put,如果该 bin 尚未创建,只需要使用 cas 创建 bin;如果已经有了,锁住链表头进行后续 put 操作,元素添加至 bin 的尾部 4. get,无锁操作仅需要保证可见性,扩容过程中 get 操作拿到的是 ForwardingNode 会让 get 操作在新 table 进行搜索 5. 扩容,扩容时以 bin 为单位进行,需要对 bin 进行 synchronized,但这时其它竞争线程也不是无事可做,它们会帮助把其它 bin 进行扩容 6. size,元素个数保存在 baseCount 中,并发时的个数变动保存在 CounterCell[] 当中,最后统计数量时累加 @@ -10583,22 +10651,22 @@ resize() 中节点(Entry)转移的源代码: ```java void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length;//得到新数组的长度 - //遍历整个数组对应下标下的链表,e代表一个节点 + // 遍历整个数组对应下标下的链表,e代表一个节点 for (Entry e : table) { - //当e == null时,则该链表遍历完了,继续遍历下一数组下标的链表 + // 当e == null时,则该链表遍历完了,继续遍历下一数组下标的链表 while(null != e) { - //先把e节点的下一节点存起来 + // 先把e节点的下一节点存起来 Entry next = e.next; if (rehash) { //得到新的hash值 e.hash = null == e.key ? 0 : hash(e.key); } - //在新数组下得到新的数组下标 + // 在新数组下得到新的数组下标 int i = indexFor(e.hash, newCapacity); - //将e的next指针指向新数组下标的位置 + // 将e的next指针指向新数组下标的位置 e.next = newTable[i]; - //将该数组下标的节点变为e节点 + // 将该数组下标的节点变为e节点 newTable[i] = e; - //遍历链表的下一节点 + // 遍历链表的下一节点 e = next; } } @@ -10624,8 +10692,8 @@ B站视频解析:https://www.bilibili.com/video/BV1n541177Ea * 散列表的长度: ```java - private static final int MAXIMUM_CAPACITY = 1 << 30; //最大长度 - private static final int DEFAULT_CAPACITY = 16; //默认长度 + private static final int MAXIMUM_CAPACITY = 1 << 30; // 最大长度 + private static final int DEFAULT_CAPACITY = 16; // 默认长度 ``` * 并发级别,JDK7 遗留下来,1.8 中不代表并发级别: @@ -10643,27 +10711,27 @@ B站视频解析:https://www.bilibili.com/video/BV1n541177Ea * 阈值: ```java - static final int TREEIFY_THRESHOLD = 8; //链表树化的阈值 - static final int UNTREEIFY_THRESHOLD = 6; //红黑树转化为链表的阈值 - static final int MIN_TREEIFY_CAPACITY = 64; //当数组长度达到 64 且某个桶位中的链表长度超过 8,才会真正树化 + static final int TREEIFY_THRESHOLD = 8; // 链表树化的阈值 + static final int UNTREEIFY_THRESHOLD = 6; // 红黑树转化为链表的阈值 + static final int MIN_TREEIFY_CAPACITY = 64; // 当数组长度达到64且某个桶位中的链表长度超过8,才会真正树化 ``` * 扩容相关: ```java - private static final int MIN_TRANSFER_STRIDE = 16; //线程迁移数据最小步长,控制线程迁移任务的最小区间 - private static int RESIZE_STAMP_BITS = 16; //用来计算扩容时生成的标识戳 - private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;//65535-1 并发扩容最多线程数 - private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS; //扩容时使用 + private static final int MIN_TRANSFER_STRIDE = 16; // 线程迁移数据【最小步长】,控制线程迁移任务的最小区间 + private static int RESIZE_STAMP_BITS = 16; // 用来计算扩容时生成的【标识戳】 + private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;// 65535-1并发扩容最多线程数 + private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS; // 扩容时使用 ``` * 节点哈希值: ```java - static final int MOVED = -1; //表示当前节点是 FWD 节点 - static final int TREEBIN = -2; //表示当前节点已经树化,且当前节点为 TreeBin 对象 - static final int RESERVED = -3; //表示节点时临时节点 - static final int HASH_BITS = 0x7fffffff; //正常节点的哈希值的可用的位数 + static final int MOVED = -1; // 表示当前节点是 FWD 节点 + static final int TREEBIN = -2; // 表示当前节点已经树化,且当前节点为 TreeBin 对象 + static final int RESERVED = -3; // 表示节点时临时节点 + static final int HASH_BITS = 0x7fffffff; // 正常节点的哈希值的可用的位数 ``` * 扩容过程: @@ -10671,7 +10739,7 @@ B站视频解析:https://www.bilibili.com/video/BV1n541177Ea ```java // 扩容过程中,会将扩容中的新 table 赋值给 nextTable 保持引用,扩容结束之后,这里会被设置为 null private transient volatile Node[] nextTable; - // 扩容过程中,记录当前进度。所有线程都要从 transferIndex 中分配区间任务,简单说就是老表转移到哪了,索引从高到低转移 + // 记录扩容进度,所有线程都要从 0 - transferIndex 中分配区间任务,简单说就是老表转移到哪了,索引从高到低转移 private transient volatile int transferIndex; ``` @@ -10702,7 +10770,7 @@ B站视频解析:https://www.bilibili.com/video/BV1n541177Ea * 如果 table 已经初始化,表示下次扩容时的触发条件(阈值) ```java - private transient volatile int sizeCtl; + private transient volatile int sizeCtl; // volatile 保持可见性 ``` @@ -10717,10 +10785,11 @@ B站视频解析:https://www.bilibili.com/video/BV1n541177Ea ```java static class Node implements Entry { + // 节点哈希值 final int hash; final K key; volatile V val; - //单向链表 + // 单向链表 volatile Node next; } ``` @@ -10741,7 +10810,7 @@ B站视频解析:https://www.bilibili.com/video/BV1n541177Ea static final int WRITER = 1; // 等待者状态(写线程在等待),当 TreeBin 中有读线程目前正在读取数据时,写线程无法修改数据 static final int WAITER = 2; - // 读锁状态是共享,同一时刻可以有多个线程 同时进入到 TreeBi 对象中获取数据,每一个线程都给 lockStat + 4 + // 读锁状态是共享,同一时刻可以有多个线程 同时进入到 TreeBi 对象中获取数据,每一个线程都给 lockState + 4 static final int READER = 4; } ``` @@ -10758,7 +10827,7 @@ B站视频解析:https://www.bilibili.com/video/BV1n541177Ea } ``` -* ForwardingNode 节点: +* ForwardingNode 节点:转移节点 ```java static final class ForwardingNode extends Node { @@ -10811,10 +10880,10 @@ B站视频解析:https://www.bilibili.com/video/BV1n541177Ea // numberOfLeadingZeros(n):返回当前数值转换为二进制后,从高位到低位开始统计,看有多少个0连续在一起 // 8 → 1000 numberOfLeadingZeros(8) = 28 // 4 → 100 numberOfLeadingZeros(4) = 29 int 值就是占4个字节 + ASHIFT = 31 - Integer.numberOfLeadingZeros(scale); // ASHIFT = 31 - 29 = 2 ,int 的大小就是 2 的 2 次方 // ABASE + (5 << ASHIFT) 用位移运算替代了乘法,获取 arr[5] 的值 - ASHIFT = 31 - Integer.numberOfLeadingZeros(scale); ``` @@ -10843,6 +10912,7 @@ B站视频解析:https://www.bilibili.com/video/BV1n541177Ea int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : // 假如传入的参数是 16,16 + 8 + 1 ,最后得到 32 + // 传入 12, 12 + 6 + 1 = 19,最后得到 32,尽可能的大,与 HashMap不一样 tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1)); // sizeCtl > 0,当目前 table 未初始化时,sizeCtl 表示初始化容量 this.sizeCtl = cap; @@ -10869,7 +10939,6 @@ B站视频解析:https://www.bilibili.com/video/BV1n541177Ea public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) { if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0) throw new IllegalArgumentException(); - // 初始容量小于并发级别 if (initialCapacity < concurrencyLevel) // 把并发级别赋值给初始容量 @@ -10882,23 +10951,71 @@ B站视频解析:https://www.bilibili.com/video/BV1n541177Ea this.sizeCtl = cap; } ``` - -* 集合构造方法 + +* 集合构造方法: ```java public ConcurrentHashMap(Map m) { - this.sizeCtl = DEFAULT_CAPACITY; //默认16 + this.sizeCtl = DEFAULT_CAPACITY; // 默认16 putAll(m); } public void putAll(Map m) { - //扩容为 2 倍 + // 尝试触发扩容 tryPresize(m.size()); for (Entry e : m.entrySet()) putVal(e.getKey(), e.getValue(), false); } ``` + ```java + private final void tryPresize(int size) { + // 扩容为大于 2 倍的最小的 2 的 n 次幂 + int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : + tableSizeFor(size + (size >>> 1) + 1); + int sc; + while ((sc = sizeCtl) >= 0) { + Node[] tab = table; int n; + // 数组还未初始化,【一般是调用集合构造方法才会成立,put 后调用该方法都是不成立的】 + if (tab == null || (n = tab.length) == 0) { + n = (sc > c) ? sc : c; + if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { + try { + if (table == tab) { + Node[] nt = (Node[])new Node[n]; + table = nt; + sc = n - (n >>> 2);// 扩容阈值:n - 1/4 n + } + } finally { + sizeCtl = sc; // 扩容阈值赋值给sizeCtl + } + } + } + // 未达到扩容阈值或者数组长度已经大于最大长度 + else if (c <= sc || n >= MAXIMUM_CAPACITY) + break; + // 与 addCount 逻辑相同 + else if (tab == table) { + int rs = resizeStamp(n); + if (sc < 0) { + Node[] nt; + if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || + sc == rs + MAX_RESIZERS || (nt = nextTable) == null || + transferIndex <= 0) + break; + if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) + transfer(tab, nt); + } + else if (U.compareAndSwapInt(this, SIZECTL, sc, + (rs << RESIZE_STAMP_SHIFT) + 2)) + transfer(tab, null); + } + } + } + ``` + + + *** @@ -10946,7 +11063,7 @@ B站视频解析:https://www.bilibili.com/video/BV1n541177Ea ```java public V put(K key, V value) { - //第三个参数 onlyIfAbsent 为 false 表示哈希表中存在相同的 key 时用当前数据覆盖旧数据 + // 第三个参数 onlyIfAbsent 为 false 表示哈希表中存在相同的 key 时【用当前数据覆盖旧数据】 return putVal(key, value, false); } ``` @@ -10955,12 +11072,11 @@ public V put(K key, V value) { ```java final V putVal(K key, V value, boolean onlyIfAbsent) { - // ConcurrentHashMap 不能存放 null 值 + // 【ConcurrentHashMap 不能存放 null 值】 if (key == null || value == null) throw new NullPointerException(); - // 扰动运算,高低位都参与寻址运算。 + // 扰动运算,高低位都参与寻址运算 int hash = spread(key.hashCode()); // 表示当前 k-v 封装成 node 后插入到指定桶位后,在桶位中的所属链表的下标位置 - // 0 表示当前桶位为 null,node 可以直接放入,2 表示当前桶位已经是红黑树 int binCount = 0; // tab 引用当前 map 的数组 table,开始自旋 for (Node[] tab = table;;) { @@ -10968,36 +11084,36 @@ public V put(K key, V value) { // i 表示 key 通过寻址计算后得到的桶位下标,fh 表示桶位头结点的 hash 值 Node f; int n, i, fh; - // 【CASE1】:条件成立表示当前 map 中的 table 尚未初始化 + // 【CASE1】:表示当前 map 中的 table 尚未初始化 if (tab == null || (n = tab.length) == 0) //【延迟初始化】 tab = initTable(); - // 【CASE2】:i 表示 key 使用寻址算法得到 key 对应数组的下标位置,tabAt 获取指定桶位的头结点 f + // 【CASE2】:i 表示 key 使用【寻址算法】得到 key 对应数组的下标位置,tabAt 获取指定桶位的头结点f else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { - // 对应的数组为 null 说明没有哈希冲突,直接添加到表中 + // 对应的数组为 null 说明没有哈希冲突,直接新建节点添加到表中 if (casTabAt(tab, i, null, new Node(hash, key, value, null))) break; } - // 【CASE3】:走到这里说明数组已经被初始化,并且当前 key 对应的位置不为null + // 【CASE3】:逻辑说明数组已经被初始化,并且当前 key 对应的位置不为 null // 条件成立表示当前桶位的头结点为 FWD 结点,表示目前 map 正处于扩容过程中 else if ((fh = f.hash) == MOVED) - // 当前线程需要去帮助哈希表完成扩容 + // 当前线程【需要去帮助哈希表完成扩容】 tab = helpTransfer(tab, f); // 【CASE4】:哈希表没有在扩容,当前桶位可能是链表也可能是红黑树 else { // 当插入 key 存在时,会将旧值赋值给 oldVal 返回 V oldVal = null; - // 锁住当前 key 寻址的桶位的头节点,当前逻辑是前面的 if else 语句判断后到达的,所以这里的 f 有值 + // 【锁住当前 key 寻址的桶位的头节点】 synchronized (f) { - // 这里重新获取一下桶的头节点有没有被修改,因为期间可能被其他线程修改过 + // 这里重新获取一下桶的头节点有没有被修改,因为可能被其他线程修改过,这里是线程安全的获取 if (tabAt(tab, i) == f) { // 头节点的哈希值大于 0 说明当前桶位是普通的链表节点 if (fh >= 0) { // 当前的插入操作没出现重复的 key,追加到链表的末尾,binCount表示链表长度 -1 - // 插入的key与链表中的某个元素的 key 一致,变成替换操作,binCount表示第几个节点冲突 + // 插入的key与链表中的某个元素的 key 一致,变成替换操作,binCount 表示第几个节点冲突 binCount = 1; // 迭代循环当前桶位的链表,e 是每次循环处理节点,e 初始是头节点 for (Node e = f;; ++binCount) { @@ -11017,7 +11133,7 @@ public V put(K key, V value) { break; } Node pred = e; - // 如果下一个节点为空,把数据封装成节点插入链表尾部,binCount 代表长度 - 1 + // 如果下一个节点为空,把数据封装成节点插入链表尾部,【binCount 代表长度 - 1】 if ((e = e.next) == null) { pred.next = new Node(hash, key, value, null); @@ -11041,7 +11157,7 @@ public V put(K key, V value) { // 条件成立说明当前是链表或者红黑树 if (binCount != 0) { - // 如果 binCount>=8 表示处理的桶位一定是链表,说明长度是9 + // 如果 binCount >= 8 表示处理的桶位一定是链表,说明长度是 9 if (binCount >= TREEIFY_THRESHOLD) // 树化 treeifyBin(tab, i); @@ -11051,16 +11167,16 @@ public V put(K key, V value) { } } } - // 统计当前 table 一共有多少数据 - // 判断是否达到扩容阈值标准,触发扩容 + // 统计当前 table 一共有多少数据,判断是否达到扩容阈值标准,触发扩容 + // binCount = 0 表示当前桶位为 null,node 可以直接放入,2 表示当前桶位已经是红黑树 addCount(1L, binCount); return null; } ``` - + * spread():扰动函数 - 将 hashCode 无符号右移 16 位,高 16bit 和低 16bit 做异或,最后与 HASH_BITS 相与变成正数,与树化节点和转移节点区分,把高低位都利用起来减少哈希冲突,保证散列的均匀性 + 将 hashCode 无符号右移 16 位,高 16bit 和低 16bit 做异或,最后与 HASH_BITS 相与变成正数,**与树化节点和转移节点区分**,把高低位都利用起来减少哈希冲突,保证散列的均匀性 ```java static final int spread(int h) { @@ -11076,20 +11192,21 @@ public V put(K key, V value) { Node[] tab; int sc; // table 尚未初始化,开始自旋 while ((tab = table) == null || tab.length == 0) { - // sc < 0 说明 table 正在初始化或者正在扩容,当前线程释放 CPU 资源 + // sc < 0 说明 table 正在初始化或者正在扩容,当前线程可以释放 CPU 资源 if ((sc = sizeCtl) < 0) Thread.yield(); - // sizeCtl 设置为-1,相当于加锁,设置的是 SIZECTL 位置的数据,不是 sc 的数据,sc 只起到对比作用 + // sizeCtl 设置为 -1,相当于加锁,【设置的是 SIZECTL 位置的数据】, + // 因为是 sizeCtl 是基本类型,不是引用类型,所以 sc 保存的是数据的副本 else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { try { // 线程安全的逻辑,再进行一次判断 if ((tab = table) == null || tab.length == 0) { - // sc > 0 创建 table 时使用 sc 为指定大小,否则使用 16 默认值. + // sc > 0 创建 table 时使用 sc 为指定大小,否则使用 16 默认值 int n = (sc > 0) ? sc : DEFAULT_CAPACITY; // 创建哈希表数组 Node[] nt = (Node[])new Node[n]; table = tab = nt; - // n >>> 2 => 等于 1/4 n n - (1/4)n = 3/4 n => 0.75 * n + // 扩容阈值,n >>> 2 => 等于 1/4 n ,n - (1/4)n = 3/4 n => 0.75 * n sc = n - (n >>> 2); } } finally { @@ -11109,21 +11226,20 @@ public V put(K key, V value) { private final void treeifyBin(Node[] tab, int index) { Node b; int n, sc; if (tab != null) { - //条件成立:说明当前table数组长度 未达到 64,此时不进行树化操作,进行扩容操作。 + // 条件成立:【说明当前 table 数组长度未达到 64,此时不进行树化操作,进行扩容操作】 if ((n = tab.length) < MIN_TREEIFY_CAPACITY) // 当前容量的 2 倍 tryPresize(n << 1); - //条件成立:说明当前桶位有数据,且是普通 node 数据。 + // 条件成立:说明当前桶位有数据,且是普通 node 数据。 else if ((b = tabAt(tab, index)) != null && b.hash >= 0) { + // 【树化加锁】 synchronized (b) { - //条件成立:表示加锁没问题。 + // 条件成立:表示加锁没问题。 if (tabAt(tab, index) == b) { TreeNode hd = null, tl = null; for (Node e = b; e != null; e = e.next) { - TreeNode p = - new TreeNode(e.hash, e.key, e.val, - null, null); + TreeNode p = new TreeNode(e.hash, e.key, e.val,null, null); if ((p.prev = tl) == null) hd = p; else @@ -11137,70 +11253,22 @@ public V put(K key, V value) { } } ``` - - tryPresize(int size):尝试触发扩容 - - ```java - private final void tryPresize(int size) { - // 又扩大一次,4倍 - int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : - tableSizeFor(size + (size >>> 1) + 1); - int sc; - while ((sc = sizeCtl) >= 0) { - Node[] tab = table; int n; - // 数组还未初始化,一般是调用集合构造方法才会成立,put 后调用该方法都是不成立的 - if (tab == null || (n = tab.length) == 0) { - n = (sc > c) ? sc : c; - if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { - try { - if (table == tab) { - Node[] nt = (Node[])new Node[n]; - table = nt; - sc = n - (n >>> 2);//扩容阈值:n - 1/4 n - } - } finally { - sizeCtl = sc; //扩容阈值赋值给sizeCtl - } - } - } - // 未达到扩容阈值或者数组长度已经大于最大长度 - else if (c <= sc || n >= MAXIMUM_CAPACITY) - break; - else if (tab == table) {// 与 addCount 逻辑相同 - int rs = resizeStamp(n); - if (sc < 0) { - Node[] nt; - if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || - sc == rs + MAX_RESIZERS || (nt = nextTable) == null || - transferIndex <= 0) - break; - if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) - transfer(tab, nt); - } - else if (U.compareAndSwapInt(this, SIZECTL, sc, - (rs << RESIZE_STAMP_SHIFT) + 2)) - transfer(tab, null); - } - } - } - ``` - + * addCount():添加计数,**代表哈希表中的数据总量** ```java private final void addCount(long x, int check) { - // as 表示 LongAdder.cells,b 表示 LongAdder.base,s 表示当前 map.table 中元素的数量 + // 【上面这部分的逻辑就是 LongAdder 的累加逻辑】 CounterCell[] as; long b, s; // 判断累加数组 cells 是否初始化,没有就去累加 base 域,累加失败进入条件内逻辑 if ((as = counterCells) != null || !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) { - // a 表示当前线程 hash 寻址命中的cell,v 表示当前线程写cell时的期望值,m 表示当前cells数组的长度 CounterCell a; long v; int m; - // true -> 未竞争,false -> 发生竞争 + // true 未竞争,false 发生竞争 boolean uncontended = true; // 判断 cells 是否被其他线程初始化 if (as == null || (m = as.length - 1) < 0 || - // 前面的条件为 fasle 说明cells 被其他线程初始化,通过 hash 寻址对应的槽位 + // 前面的条件为 fasle 说明 cells 被其他线程初始化,通过 hash 寻址对应的槽位 (a = as[ThreadLocalRandom.getProbe() & m]) == null || // 尝试去对应的槽位累加,累加失败进入 fullAddCount 进行重试或者扩容 !(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) { @@ -11208,45 +11276,48 @@ public V put(K key, V value) { fullAddCount(x, uncontended); return; } - if (check <= 1) // 表示当前桶位是 null,或者一个链表节点 + // 表示当前桶位是 null,或者一个链表节点 + if (check <= 1) return; - s = sumCount(); // 获取当前散列表元素个数,这是一个期望值 + // 【获取当前散列表元素个数】,这是一个期望值 + s = sumCount(); } - //表示一定 【是一个 put 操作调用的 addCount】 + + // 表示一定 【是一个 put 操作调用的 addCount】 if (check >= 0) { - // tab 表示map.table,nt 表示map.nextTable,n 表示map.table数组的长度,sc 表示sizeCtl的临时值 Node[] tab, nt; int n, sc; - // 条件一:true 说明当前 sizeCtl 为一个负数,表示正在扩容中,或者 sizeCtl 是一个正数,表示扩容阈值 + // 条件一:true 说明当前 sizeCtl 可能为一个负数表示正在扩容中,或者 sizeCtl 是一个正数,表示扩容阈值 // false 表示哈希表的数据的数量没达到扩容条件 - // 条件二:条件一为 true 进入条件二,判断当前 table 数组是否初始化了 - // 条件三:true->当前 table 长度小于最大值限制,则可以进行扩容 + // 然后判断当前 table 数组是否初始化了,当前 table 长度是否小于最大值限制,就可以进行扩容 while (s >= (long)(sc = sizeCtl) && (tab = table) != null && (n = tab.length) < MAXIMUM_CAPACITY) { - // 扩容批次唯一标识戳 - // 16 -> 32 扩容 标识为:1000 0000 0001 1011,负数 + // 16 -> 32 扩容 标识为:1000 0000 0001 1011,【负数,扩容批次唯一标识戳】 int rs = resizeStamp(n); - // 条件成立说明表示当前 table,【正在扩容】,sc 高 16 位是扩容标识戳,低 16 位是线程数 + 1 + + // 表示当前 table,【正在扩容】,sc 高 16 位是扩容标识戳,低 16 位是线程数 + 1 if (sc < 0) { // 条件一:判断扩容标识戳是否一样,fasle 代表一样 - // 条件二是:sc == (rs << 16 ) + 1,true 代表扩容完成,因为低 16 位是1代表没有线程扩容了 + // 勘误两个条件: + // 条件二是:sc == (rs << 16 ) + 1,true 代表扩容完成,因为低16位是1代表没有线程扩容了 // 条件三是:sc == (rs << 16) + MAX_RESIZERS,判断是否已经超过最大允许的并发扩容线程数 - // 条件四:判断新表是否是 null,代表扩容完成 - // 条件五:扩容是从高位到低位扩容,transferIndex < 0 说明没有区间需要扩容了 + // 条件四:判断新表的引用是否是 null,代表扩容完成 + // 条件五:【扩容是从高位到低位转移】,transferIndex < 0 说明没有区间需要扩容了 if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || (nt = nextTable) == null || transferIndex <= 0) break; - // 条件成立表示当前线程成功参与到扩容任务中,并且将 sc 低 16 位值加 1,表示多一个线程参与扩容 - // 条件失败说明其他线程或者 transfer 内部修改了 sizeCtl 值 + + // 设置当前线程参与到扩容任务中,将 sc 低 16 位值加 1,表示多一个线程参与扩容 + // 条设置失败其他线程或者 transfer 内部修改了 sizeCtl 值 if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) //【协助扩容线程】,持有nextTable参数 transfer(tab, nt); } - // 条件成立表示当前线程是触发扩容的第一个线程,【+ 2】 + // 逻辑到这说明当前线程是触发扩容的第一个线程,线程数量 + 2 // 1000 0000 0001 1011 0000 0000 0000 0000 +2 => 1000 0000 0001 1011 0000 0000 0000 0010 else if (U.compareAndSwapInt(this, SIZECTL, sc,(rs << RESIZE_STAMP_SHIFT) + 2)) - //【触发扩容条件的线程】,不持有 nextTable,自己新建 nextTable + //【触发扩容条件的线程】,不持有 nextTable,初始线程会新建 nextTable transfer(tab, null); s = sumCount(); } @@ -11254,13 +11325,13 @@ public V put(K key, V value) { } ``` -* resizeStamp():扩容标识符,每次扩容都会产生一个,不是每个线程都产生,16 扩容到 32 产生一个,32 扩容到 64 产生一个 +* resizeStamp():扩容标识符,**每次扩容都会产生一个,不是每个线程都产生**,16 扩容到 32 产生一个,32 扩容到 64 产生一个 ```java /** * 扩容的标识符 * 16 -> 32 从16扩容到32 - * numberOfLeadingZeros(16) => 1 0000 =>27 =>0000 0000 0001 1011 + * numberOfLeadingZeros(16) => 1 0000 => 32 - 5 = 27 => 0000 0000 0001 1011 * (1 << (RESIZE_STAMP_BITS - 1)) => 1000 0000 0000 0000 => 32768 * --------------------------------------------------------------- * 0000 0000 0001 1011 @@ -11270,7 +11341,7 @@ public V put(K key, V value) { */ static final int resizeStamp(int n) { // 或运算 - return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));//(16 -1 = 15) + return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1)); // (16 -1 = 15) } ``` @@ -11298,22 +11369,22 @@ public V put(K key, V value) { private final void transfer(Node[] tab, Node[] nextTab) { // n 表示扩容之前 table 数组的长度 int n = tab.length, stride; - //stride 表示分配给线程任务的步长,认为是 16 + // stride 表示分配给线程任务的步长,默认就是 16 if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) stride = MIN_TRANSFER_STRIDE; // 如果当前线程为触发本次扩容的线程,需要做一些扩容准备工作,【协助线程不做这一步】 if (nextTab == null) { try { - // 创建一个容量为之前二倍的 table 数组 + // 创建一个容量是之前【二倍的 table 数组】 Node[] nt = (Node[])new Node[n << 1]; nextTab = nt; } catch (Throwable ex) { sizeCtl = Integer.MAX_VALUE; return; } - // 赋值给对象属性 nextTable,方便其他线程获取新表 + // 把新表赋值给对象属性 nextTable,方便其他线程获取新表 nextTable = nextTab; - // 记录迁移数据整体位置的一个标记,transferIndex计数从1开始不是 0,所以这里是长度,不是长度-1 + // 记录迁移数据整体位置的一个标记,transferIndex 计数从1开始不是 0,所以这里是长度,不是长度-1 transferIndex = n; } // 新数组的长度 @@ -11328,7 +11399,6 @@ public V put(K key, V value) { // i 表示分配给当前线程任务,执行到的桶位 // bound 表示分配给当前线程任务的下界限制,因为是倒序迁移,16 迁移完 迁移 15,15完成去迁移14 for (int i = 0, bound = 0;;) { - // f 桶位的头结点,fh 是头节点的哈希值 Node f; int fh; // 给当前线程【分配任务区间】 @@ -11336,7 +11406,7 @@ public V put(K key, V value) { // 分配任务的开始下标,分配任务的结束下标 int nextIndex, nextBound; - // --i 就让当前线程处理下一个,true 说明当前的迁移任务尚未完成,false 说明线程已经完成或者还未分配 + // --i 让当前线程处理下一个索引,true说明当前的迁移任务尚未完成,false说明线程已经完成或者还未分配 if (--i >= bound || finishing) advance = false; // 迁移的开始下标,小于0说明没有区间需要迁移了,设置当前线程的 i 变量为 -1 跳出循环 @@ -11344,25 +11414,22 @@ public V put(K key, V value) { i = -1; advance = false; } - // 走到这里说明还有区间需要分配 - // 条件成立表示给当前线程分配任务成功,上一个线程结束的下标就是这个线程开始的下标 - else if (U.compareAndSwapInt - (this, TRANSFERINDEX, nextIndex, - //判断剩余的区间是否还够 一个步长的范围,,不够就全部分配 + // 逻辑到这说明还有区间需要分配,然后给当前线程分配任务, + else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex, + // 判断区间是否还够一个步长,不够就全部分配 nextBound = (nextIndex > stride ? nextIndex - stride : 0))) { // 当前线程的结束下标 bound = nextBound; - // 当前线程的开始下标 + // 当前线程的开始下标,上一个线程结束的下标的下一个索引就是这个线程开始的下标 i = nextIndex - 1; // 任务分配结束,跳出循环执行迁移操作 advance = false; } } - // 【数据迁移操作】 - // 【CASE1】:i < 0 成立表示当前线程未分配到任务 + // 【分配完成,开始数据迁移操作】 + // 【CASE1】:i < 0 成立表示当前线程未分配到任务,或者任务执行完了 if (i < 0 || i >= n || i + n >= nextn) { - // 保存 sizeCtl 的变量 int sc; // 如果迁移完成 if (finishing) { @@ -11378,12 +11445,12 @@ public V put(K key, V value) { return; // 所以最后一个线程退出的时候,sizeCtl 的低 16 位为 1 finishing = advance = true; - // 【这里表示最后一个线程需要重新检查一遍是否有漏掉的 + // 【这里表示最后一个线程需要重新检查一遍是否有漏掉的区间】 i = n; } } - // 【CASE2】:说明当前桶位未存放数据,只需要将此处设置为 fwd 节点即可。 + // 【CASE2】:当前桶位未存放数据,只需要将此处设置为 fwd 节点即可。 else if ((f = tabAt(tab, i)) == null) advance = casTabAt(tab, i, null, fwd); // 【CASE3】:说明当前桶位已经迁移过了,当前线程不用再处理了,直接处理下一个桶位即可 @@ -11391,7 +11458,7 @@ public V put(K key, V value) { advance = true; // 【CASE4】:当前桶位有数据,而且 node 节点不是 fwd 节点,说明这些数据需要迁移 else { - // 锁住头节点 + // 【锁住头节点】 synchronized (f) { // 二次检查,防止头节点已经被修改了,因为这里才是线程安全的访问 if (tabAt(tab, i) == f) { @@ -11400,13 +11467,13 @@ public V put(K key, V value) { // ln 表示低位链表引用 // hn 表示高位链表引用 Node ln, hn; - // 条件成立:表示当前桶位是链表桶位 + // 哈希 > 0 表示当前桶位是链表桶位 if (fh >= 0) { // 和 HashMap 的处理方式一致,与老数组长度相与,16 是 10000 // 判断对应的 1 的位置上是 0 或 1 分成高低位链表 int runBit = fh & n; Node lastRun = f; - //遍历链表,获取逆序看最长的对应位相同的链表,看下面的图更好的理解 + // 遍历链表,寻找逆序看最长的对应位相同的链表,看下面的图更好的理解 for (Node p = f.next; p != null; p = p.next) { // 将当前节点的哈希 与 n int b = p.hash & n; @@ -11416,12 +11483,12 @@ public V put(K key, V value) { lastRun = p; } } - // 成立说明筛选出的链表是低位的 + // 判断筛选出的链表是低位的还是高位的 if (runBit == 0) { ln = lastRun; // ln 指向该链表 hn = null; // hn 为 null } - // 否则,说明 lastRun 引用的链表为高位链表,就让 hn 指向 高位链表 + // 说明 lastRun 引用的链表为高位链表,就让 hn 指向高位链表头节点 else { hn = lastRun; ln = null; @@ -11430,7 +11497,8 @@ public V put(K key, V value) { for (Node p = f; p != lastRun; p = p.next) { int ph = p.hash; K pk = p.key; V pv = p.val; if ((ph & n) == 0) - // ln 是上一个节点的下一个节点的引用【头插法】 + // 【头插法】,从右往左看,首先 ln 指向的是上一个节点, + // 所以这次新建的节点的 next 指向上一个节点,然后更新 ln 的引用 ln = new Node(ph, pk, pv, ln); else hn = new Node(ph, pk, pv, hn); @@ -11474,7 +11542,7 @@ public V put(K key, V value) { ++hc; } } - // 拆成的高位低位两个链,判断是否需要需要转化为链表,反之保持树化 + // 拆成的高位低位两个链,【判断是否需要需要转化为链表】,反之保持树化 ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) : (hc != 0) ? new TreeBin(lo) : t; hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) : @@ -11490,12 +11558,12 @@ public V put(K key, V value) { } } ``` - - 链表处理的 LastRun 机制,**可以减少节点的创建**: - + + 链表处理的 LastRun 机制,**可以减少节点的创建** + ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentHashMap-LastRun机制.png) - -* helpTransfer():帮助扩容 + +* helpTransfer():帮助扩容机制 ```java final Node[] helpTransfer(Node[] tab, Node f) { @@ -11503,13 +11571,14 @@ public V put(K key, V value) { // 数组不为空,节点是转发节点,获取转发节点指向的新表开始协助主线程扩容 if (tab != null && (f instanceof ForwardingNode) && (nextTab = ((ForwardingNode)f).nextTable) != null) { + // 扩容标识戳 int rs = resizeStamp(tab.length); // 判断数据迁移是否完成,迁移完成会把 新表赋值给 nextTable 属性 - while (nextTab == nextTable && table == tab && - (sc = sizeCtl) < 0) { + while (nextTab == nextTable && table == tab && (sc = sizeCtl) < 0) { if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || transferIndex <= 0) break; + // 设置扩容线程数量 + 1 if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) { // 协助扩容 transfer(tab, nextTab); @@ -11521,7 +11590,7 @@ public V put(K key, V value) { return table; } ``` - + @@ -11551,10 +11620,10 @@ ConcurrentHashMap 使用 get() 方法获取指定 key 的数据 if ((ek = e.key) == key || (ek != null && key.equals(ek))) return e.val; } - // 当前槽位的哈希值小于0说明是红黑树节点或者是正在扩容的 fwd 节点 + // 当前槽位的【哈希值小于0】说明是红黑树节点或者是正在扩容的 fwd 节点 else if (eh < 0) return (p = e.find(h, key)) != null ? p.val : null; - // 当前桶位已经形成链表,迭代查找 + // 当前桶位是【链表】,循环遍历查找 while ((e = e.next) != null) { if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek)))) @@ -11565,52 +11634,48 @@ ConcurrentHashMap 使用 get() 方法获取指定 key 的数据 } ``` -* ForwardingNode#find +* ForwardingNode#find:转移节点的查找方法 ```java Node find(int h, Object k) { // 获取新表的引用 outer: for (Node[] tab = nextTable;;) { - //e 表示在扩容而创建新表使用寻址算法得到的桶位头结点,n 表示为扩容而创建的新表的长度 + // e 表示在扩容而创建新表使用寻址算法得到的桶位头结点,n 表示为扩容而创建的新表的长度 Node e; int n; - - //条件四:在新扩容表中 重新定位 hash 对应的头结点 - // 条件成立说明在 oldTable 中对应的桶位在迁移之前就是 null + if (k == null || tab == null || (n = tab.length) == 0 || + // 在新表中重新定位 hash 对应的头结点,表示在 oldTable 中对应的桶位在迁移之前就是 null (e = tabAt(tab, (n - 1) & h)) == null) return null; for (;;) { - //eh 新扩容后表指定桶位的当前节点的hash,ek 新扩容后表指定桶位的当前节点的key int eh; K ek; - //条件成立说明新表当前命中桶位中的数据,即为查询想要数据。 - if ((eh = e.hash) == h && - ((ek = e.key) == k || (ek != null && k.equals(ek)))) + // 【哈希相同值也相同】,表示新表当前命中桶位中的数据,即为查询想要数据 + if ((eh = e.hash) == h && ((ek = e.key) == k || (ek != null && k.equals(ek)))) return e; - // eh < 0 说明当前头节点 TreeBin 类型,或者是 FWD 类型, - // 在并发很大的情况下新扩容的表还没完成可能【再次扩容】,在此方法处再次拿到 FWD 类型 + // eh < 0 说明当前新表中该索引的头节点是 TreeBin 类型,或者是 FWD 类型 if (eh < 0) { + // 在并发很大的情况下新扩容的表还没完成可能【再次扩容】,在此方法处再次拿到 FWD 类型 if (e instanceof ForwardingNode) { - // 继续获取新的 fwd 指向的新数组的地址 + // 继续获取新的 fwd 指向的新数组的地址,递归了 tab = ((ForwardingNode)e).nextTable; continue outer; } else - //说明此桶位 为 TreeBin 节点,使用TreeBin.find 查找红黑树中相应节点。 + // 说明此桶位为 TreeBin 节点,使用TreeBin.find 查找红黑树中相应节点。 return e.find(h, k); } - // 逻辑到这说明当前桶位头结点 并没有命中查询,说明此桶位是链表 - // 将当前元素指向链表的下一个元素,判断当前元素的下一个位置是否为空 + // 逻辑到这说明当前桶位是链表,将当前元素指向链表的下一个元素,判断当前元素的下一个位置是否为空 if ((e = e.next) == null) - //条件成立说明迭代到链表末尾,未找到对应的数据,返回 你ull + // 条件成立说明迭代到链表末尾,【未找到对应的数据,返回 null】 return null; } } } ``` - + @@ -11629,22 +11694,20 @@ ConcurrentHashMap 使用 get() 方法获取指定 key 的数据 } ``` -* replaceNode():替代指定的元素,会协助扩容,增删改都会协助扩容,只有查询操作不会 +* replaceNode():替代指定的元素,会协助扩容,**增删改(写)都会协助扩容,只有查询(读)操作不会** ```java final V replaceNode(Object key, V value, Object cv) { // 计算 key 扰动运算后的 hash int hash = spread(key.hashCode()); - // 自旋 + // 开始自旋 for (Node[] tab = table;;) { - // f 表示桶位的头节点,n 表示当前 table 数组长度,i 表示 hash 映射的数组下标,fh 表示头节点的哈希值 Node f; int n, i, fh; - //【CASE1】:table 还未初始化或者哈希寻址的数组索引处为 null,直接结束自旋,返回 null - if (tab == null || (n = tab.length) == 0 || - (f = tabAt(tab, i = (n - 1) & hash)) == null) + // 【CASE1】:table 还未初始化或者哈希寻址的数组索引处为 null,直接结束自旋,返回 null + if (tab == null || (n = tab.length) == 0 || (f = tabAt(tab, i = (n - 1) & hash)) == null) break; - // 【CASE2】:条件成立说明当前 table 正在扩容,当前是个写操作,所以当前线程需要协助 table 完成扩容 + // 【CASE2】:条件成立说明当前 table 正在扩容,【当前是个写操作,所以当前线程需要协助 table 完成扩容】 else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); // 【CASE3】:当前桶位可能是 链表 也可能是 红黑树 @@ -11653,7 +11716,7 @@ ConcurrentHashMap 使用 get() 方法获取指定 key 的数据 V oldVal = null; // 校验标记 boolean validated = false; - // 加锁当前桶位头结点,加锁成功之后会进入代码块 + // 【加锁当前桶位头结点】,加锁成功之后会进入代码块 synchronized (f) { // 双重检查 if (tabAt(tab, i) == f) { @@ -11673,12 +11736,12 @@ ConcurrentHashMap 使用 get() 方法获取指定 key 的数据 (ev != null && cv.equals(ev))) { // 将当前节点的值 赋值给 oldVal 后续返回会用到 oldVal = ev; - if (value != null) //条件成立说明是替换操作 + if (value != null) // 条件成立说明是替换操作 e.val = value; - else if (pred != null) //非头节点删除操作,断开链表 + else if (pred != null) // 非头节点删除操作,断开链表 pred.next = e.next; else - //说明当前节点即为头结点,将桶位头节点设置为以前头节点的下一个节点 + // 说明当前节点即为头结点,将桶位头节点设置为以前头节点的下一个节点 setTabAt(tab, i, e.next); } break; @@ -11710,10 +11773,10 @@ ConcurrentHashMap 使用 get() 方法获取指定 key 的数据 } } } - //其他线程修改过桶位头结点时,当前线程 sync 头结点锁错对象,validated 为 false,会进入下次 for 自旋 + // 其他线程修改过桶位头结点时,当前线程 sync 头结点锁错对象,validated 为 false,会进入下次 for 自旋 if (validated) { if (oldVal != null) { - // 替换的值为 null,说明当前是一次删除操作,更新当前元素个数计数器 + // 替换的值为 null,【说明当前是一次删除操作,更新当前元素个数计数器】 if (value == null) addCount(-1L, -1); return oldVal; @@ -11742,7 +11805,7 @@ ConcurrentHashMap 对锁粒度进行了优化,**分段锁技术**,将整张 底层结构: **Segment 数组 + HashEntry 数组 + 链表**(数组 + 链表是 HashMap 的结构) -* 优点:如果多个线程访问不同的 segment,实际是没有冲突的,这与 jdk8 中是类似的 +* 优点:如果多个线程访问不同的 segment,实际是没有冲突的,这与 JDK8 中是类似的 * 缺点:Segments 数组默认大小为16,这个容量初始化指定后就不能改变了,并且不是懒惰初始化 @@ -11762,7 +11825,7 @@ ConcurrentHashMap 对锁粒度进行了优化,**分段锁技术**,将整张 #### 原理分析 -CopyOnWriteArrayList 采用了**写入时拷贝**的思想,增删改操作会将底层数组拷贝一份,更改操作在新数组上执行,不影响其它线程的**并发读,读写分离** +CopyOnWriteArrayList 采用了**写入时拷贝**的思想,增删改操作会将底层数组拷贝一份,在新数组上执行操作,不影响其它线程的**并发读,读写分离** CopyOnWriteArraySet 底层对 CopyOnWriteArrayList 进行了包装,装饰器模式 @@ -11775,10 +11838,16 @@ public CopyOnWriteArraySet() { * 存储结构: ```java - private transient volatile Object[] array;//保证了读写线程之间的可见性 + private transient volatile Object[] array; // 保证了读写线程之间的可见性 ``` -* 新增数据: +* 全局锁:保证线程的执行安全 + + ```java + final transient ReentrantLock lock = new ReentrantLock(); + ``` + +* 新增数据:需要加锁,**创建新的数组操作** ```java public boolean add(E e) { @@ -11789,11 +11858,11 @@ public CopyOnWriteArraySet() { // 获取旧的数组 Object[] elements = getArray(); int len = elements.length; - // 拷贝新的数组(这里是比较耗时的操作,但不影响其它读线程) + // 【拷贝新的数组(这里是比较耗时的操作,但不影响其它读线程)】 Object[] newElements = Arrays.copyOf(elements, len + 1); // 添加新元素 newElements[len] = e; - // 替换旧的数组 + // 替换旧的数组,【这个操作以后,其他线程获取数组就是获取的新数组了】 setArray(newElements); return true; } finally { @@ -11802,32 +11871,24 @@ public CopyOnWriteArraySet() { } ``` -* 读操作: +* 读操作:不加锁,**在原数组上操作** ```java - public void forEach(Consumer action) { - if (action == null) throw new NullPointerException(); - // 获取当前存储数据的数组 - Object[] elements = getArray(); - int len = elements.length; - for (int i = 0; i < len; ++i) { - //遍历 - @SuppressWarnings("unchecked") - E e = (E) elements[i]; - // 对给定的参数执行此操作 - action.accept(e); - } + public E get(int index) { + return get(getArray(), index); + } + private E get(Object[] a, int index) { + return (E) a[index]; } ``` 适合读多写少的应用场景 -* 迭代器: - - CopyOnWriteArrayList 在返回一个迭代器时,会基于创建这个迭代器时内部数组所拥有的数据,创建一个该内部数组当前的快照,即使其他替换了原始数组,迭代器遍历的是该快照依然引用的是创建快照时的数组,所以这种实现方式也存在一定的数据延迟性,即对其他线程并行添加的数据不可见 +* 迭代器:CopyOnWriteArrayList 在返回一个迭代器时,创建一个该内部数组当前的快照,即使其他线程替换了原始数组,迭代器遍历的是该快照依然引用的是创建快照时的数组,所以这种实现方式也存在一定的数据延迟性,对其他线程并行添加的数据不可见 ```java public Iterator iterator() { + // 获取到数组引用,整个遍历的过程该数组都不会变,一直引用的都是老数组, return new COWIterator(getArray(), 0); } @@ -11836,14 +11897,18 @@ public CopyOnWriteArraySet() { // 内部数组快照 private final Object[] snapshot; - //... + private COWIterator(Object[] elements, int initialCursor) { + cursor = initialCursor; + // 数组的引用在迭代过程不会改变 + snapshot = elements; + } // 不支持写操作 public void remove() { throw new UnsupportedOperationException(); } } ``` - + *** @@ -11959,11 +12024,11 @@ public boolean add(E e) { 对于单链表,即使链表是有序的,如果查找数据也只能从头到尾遍历链表,所以采用链表上建索引的方式提高效率,跳表的查询时间复杂度是 **O(logn)**,空间复杂度 O(n) -ConcurrentSkipListMap 提供了一种线程安全的并发访问的排序映射表,内部是跳表结构实现,通过 CAS + volatile 保证线程安全 +ConcurrentSkipListMap 提供了一种线程安全的并发访问的排序映射表,内部是跳表结构实现,通过 CAS + volatile 保证线程安全 平衡树和跳表的区别: -* 对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整;而对跳表的插入和删除,只需要对整个数据结构的局部进行操作 +* 对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整;而对跳表的插入和删除,**只需要对整个结构的局部进行操作** * 在高并发的情况下,保证整个平衡树的线程安全需要一个全局锁;对于跳表则只需要部分锁,拥有更好的性能 ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentSkipListMap数据结构.png) @@ -12002,7 +12067,7 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引都指 static final class Node{ final K key; // key 是 final 的, 说明节点一旦定下来, 除了删除, 一般不会改动 key volatile Object value; // 对应的 value - volatile Node next; // 下一个节点 + volatile Node next; // 下一个节点,单向链表 } ``` @@ -12010,7 +12075,7 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引都指 ```java static class Index{ - final Node node; // 索引指向的节点, + final Node node; // 索引指向的节点,每个都会指向数据节点 final Index down; // 下边level层的Index,分层索引 volatile Index right; // 右边的Index @@ -12055,7 +12120,7 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引都指 ```java public ConcurrentSkipListMap() { - this.comparator = null; // comparator为null,使用key的自然序,如字典序 + this.comparator = null; // comparator 为 null,使用 key 的自然序,如字典序 initialize(); } ``` @@ -12066,13 +12131,12 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引都指 entrySet = null; values = null; descendingMap = null; - // 初始化索引头节点,Node 的 Key 为 null,value 为 BASE_HEADER 对象,下一个节点为 null - // head 的分层索引 down 为 null,链表的后续索引 right 为 null,层级 level 为第一层 - head = new HeadIndex(new Node(null, BASE_HEADER, null), - null, null, 1); + // 初始化索引头节点,Node 的 key 为 null,value 为 BASE_HEADER 对象,下一个节点为 null + // head 的分层索引 down 为 null,链表的后续索引 right 为 null,层级 level 为第 1 层 + head = new HeadIndex(new Node(null, BASE_HEADER, null), null, null, 1); } ``` - + * cpr:排序 ```java @@ -12129,8 +12193,8 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引都指 if ((d = q.down) == null) return q.node; // 6.未到数据层, 进行重新赋值向下扫描 - q = d; //q指向d - r = d.right;//r指向q的后续索引节点 + q = d; // q 指向 d + r = d.right;// r 指向 q 的后续索引节点,此时(q.key < key < r.key) } } } @@ -12152,11 +12216,11 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引都指 ```java private V doPut(K key, V value, boolean onlyIfAbsent) { Node z; - // 非空判断,key不能为空 + // 非空判断,key 不能为空 if (key == null) throw new NullPointerException(); Comparator cmp = comparator; - // outer 循环,【把待插入数据插入到数据层的合适的位置,并在扫描过程中处理 已删除(value = null) 的数据】 + // outer 循环,【把待插入数据插入到数据层的合适的位置,并在扫描过程中处理已删除(value = null)的数据】 outer: for (;;) { //0.for (;;) //1.将 key 对应的前继节点找到, b 为前继节点,是数据层的, n 是前继节点的 next, @@ -12170,7 +12234,7 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引都指 // 4.条件竞争,并发下其他线程在 b 之后插入节点或直接删除节点 n, break 到步骤 0 if (n != b.next) break; - // 若节点 n 已经删除, 则调用 helpDelete 进行帮助删除节点 + // 若节点 n 已经删除, 则调用 helpDelete 进行【帮助删除节点】 if ((v = n.value) == null) { n.helpDelete(b, f); break; @@ -12226,7 +12290,7 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引都指 // 【最大有30个就是 1 + 30 = 31 while (((rnd >>>= 1) & 1) != 0) ++level; - // 最终指向 z 节点,就是添加的节点 + // 最终会指向 z 节点,就是添加的节点 Index idx = null; // 指向头索引节点 HeadIndex h = head; @@ -12246,19 +12310,17 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引都指 // ↓ // z-node } - // 14.若 level > max,则只增加一层 index 索引层,3 + 1 = 4 + // 14.若 level > max,则【只增加一层 index 索引层】,3 + 1 = 5 else { level = max + 1; - //创建一个 index 数组,长度是 level+1,假设 level 是4,创建的数组长度为 5 + //创建一个 index 数组,长度是 level+1,假设 level 是 4,创建的数组长度为 5 Index[] idxs = (Index[])new Index[level+1]; - //index[0]的数组 slot 并没有使用,只使用 [1,level] 这些数组的 slot + // index[0]的数组 slot 并没有使用,只使用 [1,level] 这些数组的 slot for (int i = 1; i <= level; ++i) idxs[i] = idx = new Index(z, idx, null); // index-4 ← idx // ↓ - // index-3 - // ↓ - // index-2 + // ...... // ↓ // index-1 // ↓ @@ -12266,7 +12328,7 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引都指 for (;;) { h = head; - //获取头索引的层数 + // 获取头索引的层数 int oldLevel = h.level; // 如果 level <= oldLevel,说明其他线程进行了 index 层增加操作,退出循环 if (level <= oldLevel) @@ -12293,7 +12355,7 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引都指 if (casHead(h, newh)) { // h 指向最新的 index-4 节点 h = newh; - // idx 指向 z-node 的 index-3 节点, + // 让 idx 指向 z-node 的 index-3 节点, // 因为从 index-3 - index-1 的这些 z-node 索引节点 都没有插入到索引链表 idx = idxs[level = oldLevel]; break; @@ -12324,14 +12386,14 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引都指 r = q.right; continue; } - // key > n.key,向右扫描 + // key > r.node.key,向右扫描 if (c > 0) { q = r; r = r.right; continue; } } - // 执行到这里,说明 key < n.key,判断是否是第 j 层插入新增节点的前置索引 + // 执行到这里,说明 key < r.node.key,判断是否是第 j 层插入新增节点的前置索引 if (j == insertionLevel) { // 【将新索引节点 t 插入 q r 之间】 if (!q.link(r, t)) @@ -12385,7 +12447,7 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引都指 } ``` -* doGet():扫描过程会对已 value == null 的元素进行删除处理 +* doGet():扫描过程会对已 value = null 的元素进行删除处理 ```java private V doGet(Object key) { @@ -12404,7 +12466,7 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引都指 // 3.如果n不为前置节点的后续节点,表示已经有其他线程删除了该节点 if (n != b.next) break; - // 4.如果后续节点的值为null,删除该节点 + // 4.如果后续节点的值为null,【需要帮助删除该节点】 if ((v = n.value) == null) { n.helpDelete(b, f); break; @@ -12475,10 +12537,10 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引都指 //5.到这里是 key = n.key,value 不为空的情况下判断 value 和 n.value 是否相等 if (value != null && !value.equals(v)) break outer; - //6.把 n 节点的 value 置空 + //6.【把 n 节点的 value 置空】 if (!n.casValue(v, null)) break; - //7.给 n 添加一个删除标志 mark,mark.next=f,然后把 b.next 设置为 f,成功后 n 出队 + //7.【给 n 添加一个删除标志 mark】,mark.next = f,然后把 b.next 设置为 f,成功后n出队 if (!n.appendMarker(f) || !b.casNext(n, f)) // 对 key 对应的 index 进行删除,调用了 findPredecessor 方法 findNode(key); @@ -12517,6 +12579,7 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引都指 // 将添加了删除标记的节点清除,参数是该节点的前驱和后继节点 void helpDelete(Node b, Node f) { // this 节点的后续节点为 f,且本身为 b 的后续节点,一般都是正确的,除非被别的线程删除 + // b if (f == next && this == b.next) { // 如果 n 还还没有被标记 if (f == null || f.value != f) From 24186f10ea1fbfbe4b9b3207856f16e697734d72 Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 2 Sep 2021 09:28:18 +0800 Subject: [PATCH 045/168] Update Java Notes --- DB.md | 4 ++-- Tool.md | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DB.md b/DB.md index f576b26..968e2a2 100644 --- a/DB.md +++ b/DB.md @@ -11262,7 +11262,7 @@ sentinel 在通知阶段不断的去获取 master/slave 的信息,然后在各 **数据存储设计:** -1. 通过算法设计,计算出 key 应该保存的位置 +1. 通过算法设计,计算出 key 应该保存的位置(类似哈希寻址) ```markdown key -> CRC16(key) -> 值 -> %16384 -> 存储位置 @@ -11296,7 +11296,7 @@ sentinel 在通知阶段不断的去获取 master/slave 的信息,然后在各 整体框架: -- 配置服务器(3主3从) +- 配置服务器(3 主 3 从) - 建立通信(Meet) - 分槽(Slot) - 搭建主从(master-slave) diff --git a/Tool.md b/Tool.md index 054384d..7d53ef2 100644 --- a/Tool.md +++ b/Tool.md @@ -1026,16 +1026,16 @@ Linux kill命令用于删除执行中的程序或工作(可强制中断) - -l <信息编号>  若不加<信息编号>选项,则-l参数会列出全部的信息名称。 - -s <信息名称或编号>  指定要送出的信息。 - -KILL 强制杀死进程 -- **-9 彻底杀死进程(常用)** -- [程序] 程序的PID、PGID、工作编号。 +- **-9 彻底杀死进程(常用)** +- [程序] 程序的 PID、PGID、工作编号。 `kill 15642 `. `kill -KILL 15642`. **`kill -9 15642`** 杀死指定用户所有进程: -1.过滤出user用户进程 :`kill -9 $(ps -ef | grep user) ` +1. 过滤出 user 用户进程 :`kill -9 $(ps -ef | grep user) ` -2.直接杀死:`kill -u user` +2. 直接杀死:`kill -u user` From 46e371424edc16d3f9f9b84fcd2bdeb73df05086 Mon Sep 17 00:00:00 2001 From: Seazean Date: Fri, 3 Sep 2021 23:09:32 +0800 Subject: [PATCH 046/168] Update Java Notes --- Java.md | 261 +++++++++++++++++++++++++++++++++++++++++--------------- Prog.md | 88 ++++++++++--------- SSM.md | 37 ++++++-- 3 files changed, 275 insertions(+), 111 deletions(-) diff --git a/Java.md b/Java.md index e936cb3..0225e4e 100644 --- a/Java.md +++ b/Java.md @@ -4112,6 +4112,47 @@ public class ArrayList extends AbstractList * **Fail-Fast**:快速失败,modCount 用来记录 ArrayList **结构发生变化**的次数,结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化 在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,改变了抛出 ConcurrentModificationException 异常 + + ```java + public Iterator iterator() { + return new Itr(); + } + ``` + + ```java + private class Itr implements Iterator { + int cursor; // index of next element to return + int lastRet = -1; // index of last element returned; -1 if no such + int expectedModCount = modCount; + + Itr() {} + + public boolean hasNext() { + return cursor != size; + } + + // 获取下一个元素时首先判断结构是否发生变化 + public E next() { + checkForComodification(); + // ..... + } + // modCount 被其他线程改变抛出并发修改异常 + final void checkForComodification() { + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + } + // 允许删除操作 + public void remove() { + // ... + checkForComodification(); + // ... + // 删除后重置 expectedModCount + expectedModCount = modCount; + } + } + ``` + + @@ -4805,7 +4846,7 @@ HashMap继承关系如下图所示: jdk8 之前数组类型是 Entry类型,之后是 Node 类型。只是换了个名字,都实现了一样的接口 Map.Entry,负责存储键值对数据的 - 9. HashMap 中存放元素的个数(**重点**) + 9. HashMap 中**存放元素的个数**(**重点**) ```java // 存放元素的个数,HashMap中K-V的实时数量,不是table数组的长度 @@ -4861,7 +4902,7 @@ HashMap继承关系如下图所示: ```java public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; - //将默认的加载因子0.75赋值给loadFactor,并没有创建数组 + // 将默认的加载因子0.75赋值给loadFactor,并没有创建数组 } ``` @@ -4878,26 +4919,24 @@ HashMap继承关系如下图所示: ```java public HashMap(int initialCapacity, float loadFactor) { - //进行判断 - //将指定的加载因子赋值给HashMap成员变量的负载因子loadFactor + // 进行判断 + // 将指定的加载因子赋值给HashMap成员变量的负载因子loadFactor this.loadFactor = loadFactor; - //最后调用了tableSizeFor + // 最后调用了tableSizeFor this.threshold = tableSizeFor(initialCapacity); } ``` - * 对于`this.threshold = tableSizeFor(initialCapacity);` + * 对于 `this.threshold = tableSizeFor(initialCapacity)` - 有些人会觉得这里是一个bug应该这样书写: - `this.threshold = tableSizeFor(initialCapacity) * this.loadFactor;` - 这样才符合 threshold 的概念,但是在 jdk8 以后的构造方法中,并没有对 table 这个成员变量进行初始化,table 的初始化被推迟到了 put 方法中,在 put 方法中会对 threshold 重新计算 - -* 包含另一个`Map`的构造函数 + JDK8 以后的构造方法中,并没有对 table 这个成员变量进行初始化,table 的初始化被推迟到了 put 方法中,在 put 方法中会对 threshold 重新计算 + +* 包含另一个 `Map` 的构造函数 ```java - //构造一个映射关系与指定 Map 相同的新 HashMap + // 构造一个映射关系与指定 Map 相同的新 HashMap public HashMap(Map m) { - //负载因子loadFactor变为默认的负载因子0.75 + // 负载因子loadFactor变为默认的负载因子0.75 this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); } @@ -4949,10 +4988,10 @@ HashMap继承关系如下图所示: * hash():HashMap 是支持 Key 为空的;HashTable 是直接用 Key 来获取 HashCode,key 为空会抛异常 * &(按位与运算):相同的二进制数位上,都是1的时候,结果为1,否则为零 -* ^(按位异或运算):相同的二进制数位上,数字相同,结果为0,不同为1,**不进位加法** + * ^(按位异或运算):相同的二进制数位上,数字相同,结果为0,不同为1,**不进位加法** ```java -static final int hash(Object key) { + static final int hash(Object key) { int h; // 1)如果key等于null:可以看到当key等于null的时候也是有哈希值的,返回的是0. // 2)如果key不等于null:首先计算出key的hashCode赋值给h,然后与h无符号右移16位后的二进制进行按位异或得到最后的hash值 @@ -4961,16 +5000,14 @@ static final int hash(Object key) { ``` 计算 hash 的方法:将 hashCode 无符号右移 16 位,高 16bit 和低 16bit 做异或,扰动运算 - + 原因:当数组长度很小,假设是 16,那么 n-1即为 1111 ,这样的值和 hashCode() 直接做按位与操作,实际上只使用了哈希值的后4位。如果当哈希值的高位变化很大,低位变化很小,就很容易造成哈希冲突了,所以这里**把高低位都利用起来,让高16 位也参与运算**,从而解决了这个问题 - + 哈希冲突的处理方式: - - * 开放定址法:线性探查法(ThreadLocalMap 使用),平方探查法(i + 1^2、i - 1^2、i + 2^2……)、双重散列(多个哈希函数) -* 链地址法:拉链法 + * 开放定址法:线性探查法(ThreadLocalMap 使用),平方探查法(i + 1^2、i - 1^2、i + 2^2……)、双重散列(多个哈希函数) + * 链地址法:拉链法 - * put():jdk1.8 前是头插法 (拉链法),多线程下扩容出现循环链表,jdk1.8 以后引入红黑树,插入方法变成尾插法 第一次调用 put 方法时创建数组 Node[] table,因为散列表耗费内存,为了防止内存浪费,所以**延迟初始化** @@ -4978,45 +5015,50 @@ static final int hash(Object key) { 存储数据步骤(存储过程): 1. 先通过 hash 值计算出 key 映射到哪个桶,哈希寻址 - 2. 如果桶上没有碰撞冲突,则直接插入 - 3. 如果出现碰撞冲突:如果该桶使用红黑树处理冲突,则调用红黑树的方法插入数据;否则采用传统的链式方法插入,如果链的长度达到临界值,则把链转变为红黑树 - 4. 如果数组位置相同,通过 equals 比较内容是否相同:相同则新的 value 覆盖旧 value,不相同则将新的键值对添加到哈希表中 -5. 如果 size 大于阈值 threshold,则进行扩容 - + 5. 最后判断 size 是否大于阈值 threshold,则进行扩容 + ```java -public V put(K key, V value) { + public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } ``` putVal() 方法中 key 在这里执行了一下 hash(),在 putVal 函数中使用到了上述 hash 函数计算的哈希值: - + ```java -final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { - //。。。。。。。。。。。。。。 - if ((p = tab[i = (n - 1) & hash]) == null){//这里的n表示数组长度16 - //..... - } else { - if (e != null) { // existing mapping for key - V oldValue = e.value; - //onlyIfAbsent默认为false,所以可以覆盖已经存在的数据,如果为true说明不能覆盖 - if (!onlyIfAbsent || oldValue == null) - e.value = value; - afterNodeAccess(e); - return oldValue; - } - } + final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { + //。。。。。。。。。。。。。。 + if ((p = tab[i = (n - 1) & hash]) == null){//这里的n表示数组长度16 + //..... + } else { + if (e != null) { // existing mapping for key + V oldValue = e.value; + //onlyIfAbsent默认为false,所以可以覆盖已经存在的数据,如果为true说明不能覆盖 + if (!onlyIfAbsent || oldValue == null) + e.value = value; + afterNodeAccess(e); + // 如果这里允许覆盖,就直接返回了 + return oldValue; + } + } + // 如果是添加操作,modCount ++,如果不是替换,不会走这里的逻辑,modCount用来记录逻辑的变化 + ++modCount; + // 数量大于扩容阈值 + if (++size > threshold) + resize(); + afterNodeInsertion(evict); + return null; } ``` - * `(n - 1) & hash`:计算下标位置 - - - - * 余数本质是不断做除法,把剩余的数减去,运算效率要比位运算低 + * `(n - 1) & hash`:计算下标位置 + + + + * 余数本质是不断做除法,把剩余的数减去,运算效率要比位运算低 @@ -5118,7 +5160,7 @@ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { * resize(): - 当 HashMap 中的元素个数超过 `(数组长度)*loadFactor(负载因子)` 或者链表过长时(链表长度 > 8,数组长度 < 64),就会进行数组扩容,创建新的数组,伴随一次重新 hash 分配,并且遍历 hash 表中所有的元素非常耗时,所以要尽量避免 resize + 当 HashMap 中的**元素个数**超过 `(数组长度)*loadFactor(负载因子)` 或者链表过长时(链表长度 > 8,数组长度 < 64),就会进行数组扩容,创建新的数组,伴随一次重新 hash 分配,并且遍历 hash 表中所有的元素非常耗时,所以要尽量避免 resize 扩容机制为扩容为原来容量的 2 倍: @@ -5278,11 +5320,94 @@ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { 5. 时间复杂度 O(1) * 若为树,则在树中通过key.equals(k)查找,**O(logn)** - * 若为链表,则在链表中通过key.equals(k)查找,**O(n)** +**** + + + +##### 并发异常 + +HashMap 和 ArrayList 一样,内部采用 modCount 用来记录集合结构发生变化的次数,结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化 + +在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果**其他线程此时修改了集合内部的结构**,就会直接抛出 ConcurrentModificationException 异常 + +```java +HashMap map = new HashMap(); +Iterator iterator = map.keySet().iterator(); +``` + +```java +final class KeySet extends AbstractSet { + // 底层获取的是 KeyIterator + public final Iterator iterator() { + return new KeyIterator(); + } +} +final class KeyIterator extends HashIterator implements Iterator { + // 回调 HashMap.HashIterator#nextNode + public final K next() { + return nextNode().key; + } +} +``` + +```java +abstract class HashIterator { + Node next; // next entry to return + Node current; // current entry + int expectedModCount; // for 【fast-fail】,快速失败 + int index; // current slot + + HashIterator() { + // 把当前 map 的数量赋值给 expectedModCount,迭代时判断 + expectedModCount = modCount; + Node[] t = table; + current = next = null; + index = 0; + if (t != null && size > 0) { // advance to first entry + do {} while (index < t.length && (next = t[index++]) == null); + } + } + + public final boolean hasNext() { + return next != null; + } + // iterator.next() 会调用这个函数 + final Node nextNode() { + Node[] t; + Node e = next; + // 这里会判断 集合的结构是否发生了变化,变化后 modCount 会改变,直接抛出并发异常 + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + if (e == null) + throw new NoSuchElementException(); + if ((next = (current = e).next) == null && (t = table) != null) { + do {} while (index < t.length && (next = t[index++]) == null); + } + return e; + } + // 迭代器允许删除集合的元素,【删除后会重置 expectedModCount = modCount】 + public final void remove() { + Node p = current; + if (p == null) + throw new IllegalStateException(); + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + current = null; + K key = p.key; + removeNode(hash(key), key, null, false, false); + // 同步expectedModCount + expectedModCount = modCount; + } +``` + + + + + *** @@ -15480,7 +15605,7 @@ public class BubbleSort { 实现思路: -1. 控制选择几轮:数组的长度-1 +1. 控制选择几轮:数组的长度 - 1 2. 控制每轮从当前位置开始比较几次 @@ -15613,9 +15738,9 @@ public class InsertSort { int[] arr = {55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88}; for (int i = 1; i < arr.length; i++) { for (int j = i; j > 0; j--) { - //比较索引j处的值和索引j-1处的值, - //如果索引j-1处的值比索引j处的值大,则交换数据, - //如果不大,那么就找到合适的位置了,退出循环即可; + // 比较索引j处的值和索引j-1处的值, + // 如果索引j-1处的值比索引j处的值大,则交换数据, + // 如果不大,那么就找到合适的位置了,退出循环即可; if (arr[j - 1] > arr[j]) { int temp = arr[j]; arr[j] = arr[j - 1]; @@ -15660,18 +15785,18 @@ public class InsertSort { public class ShellSort { public static void main(String[] args) { int[] arr = {55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88}; - //1. 确定增长量h的初始值 + // 确定增长量h的初始值 int h = 1; while (h < arr.length / 2) { h = 2 * h + 1; } - //2. 希尔排序 + // 希尔排序 while (h >= 1) { - //2.1 找到待插入的元素 + // 找到待插入的元素 for (int i = h; i < arr.length; i++) { - //2.2 把待插入的元素插到有序数列中 + // 把待插入的元素插到有序数列中 for (int j = i; j >= h; j -= h) { - //待插入的元素是arr[j],比较arr[j]和arr[j-h] + // 待插入的元素是arr[j],比较arr[j]和arr[j-h] if (arr[j] < arr[j - h]) { int temp = arr[j]; arr[j] = arr[j - h]; @@ -15679,7 +15804,7 @@ public class ShellSort { } } } - //3. 减小h的值,减小规则为: + // 减小h的值,减小规则为: h = h / 2; } System.out.println(Arrays.toString(arr)); @@ -15734,20 +15859,20 @@ public class MergeSort { mergeSort(arr, 0, arr.length - 1); System.out.println(Arrays.toString(arr)); } - // low 为arr最小索引,high为最大索引 + // low为arr最小索引,high为最大索引 public static void mergeSort(int[] arr, int low, int high) { // low == high 时说明只有一个元素了,直接返回 if (low < high) { int mid = (low + high) / 2; - mergeSort(arr, low, mid); //归并排序前半段 - mergeSort(arr, mid + 1, high); //归并排序后半段 - merge(arr, low, mid, high); //将两段有序段合成一段有序段 + mergeSort(arr, low, mid); // 归并排序前半段 + mergeSort(arr, mid + 1, high); // 归并排序后半段 + merge(arr, low, mid, high); // 将两段有序段合成一段有序段 } } private static void merge(int[] arr, int low, int mid, int high) { int index = 0; - //定义左右指针 + // 定义左右指针 int left = low, right = mid + 1; int[] assist = new int[high - low + 1]; @@ -15803,15 +15928,15 @@ public class QuickSort { } public static void quickSort(int[] arr, int low, int high) { - //递归结束的条件 + // 递归结束的条件 if (low >= high) { return; } int left = low; int right = high; - - int temp = arr[left];//基准数 + // 基准数 + int temp = arr[left]; while (left < right) { // 用 >= 可以防止多余的交换 while (arr[right] >= temp && right > left) { @@ -15820,7 +15945,7 @@ public class QuickSort { // 做判断防止相等 if (right > left) { // 到这里说明 arr[right] < temp - arr[left] = arr[right];//此时把arr[right]元素视为空 + arr[left] = arr[right];// 此时把arr[right]元素视为空 left++; } while (arr[left] <= temp && left < right) { diff --git a/Prog.md b/Prog.md index d9be48f..913726c 100644 --- a/Prog.md +++ b/Prog.md @@ -1736,7 +1736,7 @@ LockSupport 出现就是为了增强 wait & notify 的功能: * 线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的 -* 每个方法是原子的,但多个方法的组合不是原子的,只能保证调用的方法内部安全: +* **每个方法是原子的,但多个方法的组合不是原子的**,只能保证调用的方法内部安全: ```java Hashtable table = new Hashtable(); @@ -3299,7 +3299,7 @@ transient volatile int cellsBusy; ```java public void add(long x) { // as 为累加单元数组的引用,b 为基础值,v 表示期望值 - // m 表示 cells 数组的长度,a 表示当前线程命中的 cell 单元格 + // m 表示 cells 数组的长度 - 1,a 表示当前线程命中的 cell 单元格 Cell[] as; long b, v; int m; Cell a; // cells 不为空说明 cells 已经被初始化,线程发生了竞争,去更新对应的 cell 槽位 @@ -3381,8 +3381,7 @@ transient volatile int cellsBusy; else if (!wasUncontended) wasUncontended = true; // CASE 1.3: 当前线程 rehash 过,如果新命中的 cell 不为空,就尝试累加,false 说明新命中也有竞争 - else if (a.cas(v = a.value, ((fn == null) ? v + x : - fn.applyAsLong(v, x)))) + else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) break; // CASE 1.4: cells 长度已经超过了最大长度 CPU 内核的数量或者已经扩容 else if (n >= NCPU || cells != as) @@ -10689,6 +10688,12 @@ B站视频解析:https://www.bilibili.com/video/BV1n541177Ea ##### 变量 +* 存储数组: + + ```java + transient volatile Node[] table; + ``` + * 散列表的长度: ```java @@ -10767,7 +10772,7 @@ B站视频解析:https://www.bilibili.com/video/BV1n541177Ea sizeCtl > 0: * 如果 table 未初始化,表示初始化大小 - * 如果 table 已经初始化,表示下次扩容时的触发条件(阈值) + * 如果 table 已经初始化,表示下次扩容时的触发条件(阈值,元素个数,不是数组的长度) ```java private transient volatile int sizeCtl; // volatile 保持可见性 @@ -11359,7 +11364,7 @@ public V put(K key, V value) { * 当链表中元素个数超过 8 个,数组的大小还未超过 64 时,此时进行数组的扩容,如果超过则将链表转化成红黑树 * put 数据后调用 addCount() 方法,判断当前哈希表的容量超过阈值 sizeCtl,超过进行扩容 -* 发现其他线程正在扩容,帮其扩容 +* 增删改线程发现其他线程正在扩容,帮其扩容 常见方法: @@ -11884,7 +11889,7 @@ public CopyOnWriteArraySet() { 适合读多写少的应用场景 -* 迭代器:CopyOnWriteArrayList 在返回一个迭代器时,创建一个该内部数组当前的快照,即使其他线程替换了原始数组,迭代器遍历的是该快照依然引用的是创建快照时的数组,所以这种实现方式也存在一定的数据延迟性,对其他线程并行添加的数据不可见 +* 迭代器:CopyOnWriteArrayList 在返回迭代器时,**创建一个该内部数组当前的快照(引用)**,即使其他线程替换了原始数组,迭代器遍历的快照依然引用的是创建快照时的数组,所以这种实现方式也存在一定的数据延迟性,对其他线程并行添加的数据不可见 ```java public Iterator iterator() { @@ -11902,7 +11907,7 @@ public CopyOnWriteArraySet() { // 数组的引用在迭代过程不会改变 snapshot = elements; } - // 不支持写操作 + // 【不支持写操作】,因为是在快照上操作,无法同步回去 public void remove() { throw new UnsupportedOperationException(); } @@ -11917,8 +11922,6 @@ public CopyOnWriteArraySet() { #### 弱一致性 -##### get方法 - 数据一致性就是读到最新更新的数据: * 强一致性:当更新操作完成之后,任何多个后续进程或者线程的访问都会返回最新的更新过的值 @@ -11947,33 +11950,6 @@ Thread-0 读到了脏数据 -##### 迭代器 - -```java -public static void main(String[] args) throws InterruptedException { - CopyOnWriteArrayList list = new CopyOnWriteArrayList<>(); - list.add(1); - list.add(2); - list.add(3); - Iterator iter = list.iterator(); - new Thread(() -> { - list.remove(0); - System.out.println(list);[2,3] - }).start(); - - Thread.sleep(1000); - while (iter.hasNext()) { - System.out.println(iter.next());// 1 2 3 - } -} -``` - - - -*** - - - #### 安全失败 在 java.util 包的集合类就都是快速失败的,而 java.util.concurrent 包下的类都是安全失败 @@ -11981,7 +11957,43 @@ public static void main(String[] args) throws InterruptedException { * 快速失败:在 A 线程使用**迭代器**对集合进行遍历的过程中,此时 B 线程对集合进行修改(增删改),或者 A 线程在遍历过程中对集合进行修改,都会导致 A 线程抛出 ConcurrentModificationException 异常 * AbstractList 类中的成员变量 modCount,用来记录 List 结构发生变化的次数,**结构发生变化**是指添加或者删除至少一个元素的操作,或者是调整内部数组的大小,仅仅设置元素的值不算结构发生变化 * 在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了抛出 CME 异常 -* 安全失败:采用安全失败机制的集合容器,在**迭代器**遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在复制集合上进行遍历。由于迭代时不是对原集合进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,故不会抛 ConcurrentModificationException 异常 + +* 安全失败:采用安全失败机制的集合容器,在**迭代器**遍历时直接在原集合数组内容上访问,但其他线程的增删改都会新建数组进行修改,就算修改了集合底层的数组容器,迭代器依然引用着以前的数组(快照思想),所以不会出现异常 + + ConcurrentHashMap 不会出现并发时的迭代异常,因为在迭代过程中 CHM 的迭代器并没有判断结构的变化,迭代器还可以根据迭代的节点状态去寻找并发扩容时的新表进行迭代 + + ```java + ConcurrentHashMap map = new ConcurrentHashMap(); + // KeyIterator + Iterator iterator = map.keySet().iterator(); + ``` + + ```java + Traverser(Node[] tab, int size, int index, int limit) { + // 引用还是原来集合的 Node 数组,所以其他线程对数据的修改是可见的 + this.tab = tab; + this.baseSize = size; + this.baseIndex = this.index = index; + this.baseLimit = limit; + this.next = null; + } + ``` + + ```java + public final boolean hasNext() { return next != null; } + public final K next() { + Node p; + if ((p = next) == null) + throw new NoSuchElementException(); + K k = p.key; + lastReturned = p; + // 在方法中进行下一个节点的获取,会进行槽位头节点的状态判断 + advance(); + return k; + } + ``` + + diff --git a/SSM.md b/SSM.md index 732a113..8c1b353 100644 --- a/SSM.md +++ b/SSM.md @@ -10,7 +10,7 @@ ORM(Object Relational Mapping): 对象关系映射,指的是持久化数 * MyBatis 是一个优秀的基于 Java 的持久层框架,它内部封装了 JDBC,使开发者只需关注 SQL 语句本身,而不需要花费精力去处理加载驱动、创建连接、创建 Statement 等过程。 -* MyBatis通过 xml 或注解的方式将要执行的各种 Statement 配置起来,并通过 Java 对象和 Statement 中 SQL 的动态参数进行映射生成最终执行的 SQL 语句。 +* MyBatis通过 XML 或注解的方式将要执行的各种 Statement 配置起来,并通过 Java 对象和 Statement 中 SQL 的动态参数进行映射生成最终执行的 SQL 语句。 * MyBatis 框架执行 SQL 并将结果映射为 Java 对象并返回。采用 ORM 思想解决了实体和数据库映射的问题,对 JDBC 进行了封装,屏蔽了 JDBC 底层 API 的调用细节,使我们不用操作 JDBC API,就可以完成对数据库的持久化操作。 @@ -2293,7 +2293,7 @@ MyBatis 运行过程: * 查询方法调用 sqlSession.selectOne(),从 Configuration 中获取执行者对象 MappedStatement,然后 Executor 调用 executor.query 开始执行查询方法 * 首先通过 CachingExecutor 去二级缓存查询,查询不到去一级缓存查询,**最后去数据库查询并放入一级缓存** * Configuration 对象根据 + SELECT * FROM BLOG WHERE ID = #{id} + + + +``` + +循环引用:通过缓存解决 + +```xml + + + + + + - @@ -2129,7 +2286,7 @@ MyBatis 提供了 org.apache.ibatis.jdbc.SQL 功能类,专门用于构建 SQL #### 基本操作 -* MyBatisConfig.xml配置 +* MyBatisConfig.xml 配置 ```xml @@ -2138,7 +2295,7 @@ MyBatis 提供了 org.apache.ibatis.jdbc.SQL 功能类,专门用于构建 SQL ``` -* Mapper类 +* Mapper 类 ```java public interface StudentMapper { @@ -2161,7 +2318,7 @@ MyBatis 提供了 org.apache.ibatis.jdbc.SQL 功能类,专门用于构建 SQL } ``` -* ReturnSql类 +* ReturnSQL 类 ```java public class ReturnSql { @@ -2454,6 +2611,12 @@ Executor#query(): * `CachingExecutor.query()`:先执行 CachingExecutor 去二级缓存获取数据 + ```java + public class CachingExecutor implements Executor { + private final Executor delegate; // 包装了 BaseExecutor,二级缓存不存在数据调用 BaseExecutor 查询 + } + ``` + * `MappedStatement.getBoundSql(parameterObject)`:**把 parameterObject 封装成 BoundSql** 构造函数中有:`this.parameterObject = parameterObject` @@ -2466,13 +2629,14 @@ Executor#query(): * `tcm.getObject(cache, key)`:尝试从**二级缓存**中获取数据 -* `BaseExecutor.query()`:获取不到缓存继续执行该方法 +* `BaseExecutor.query()`:二级缓存不存在该数据,调用该方法 * `localCache.getObject(key) `:尝试从**本地缓存(一级缓存**)获取数据 * `BaseExecutor.queryFromDatabase()`:缓存获取数据失败,**开始从数据库获取数据,并放入本地缓存** * `SimpleExecutor.doQuery()`:执行 query + * `configuration.newStatementHandler()`:创建 StatementHandler 对象 * 根据 + + + + + + + ``` + + + + + +*** + + + +### 参数调优 + +#### CONNECT + +参数配置方式: + +* 客户端通过 .option() 方法配置参数,给 SocketChannel 配置参数 +* 服务器端: + * new ServerBootstrap().option(): 给 ServerSocketChannel 配置参数 + * new ServerBootstrap().childOption():给 SocketChannel 配置参数 + +CONNECT_TIMEOUT_MILLIS 参数: + +* 属于 SocketChannal 参数 +* 在客户端建立连接时,如果在指定毫秒内无法连接,会抛出 timeout 异常 + +* SO_TIMEOUT 主要用在阻塞 IO,阻塞 IO 中 accept,read 等都是无限等待的,如果不希望永远阻塞,可以调整超时时间 ```java -// 发送内容随机的数据包 -Random r = new Random(); -char c = '0'; -ByteBuf buf = ctx.alloc().buffer(); -for (int i = 0; i < 10; i++) { - byte[] bytes = new byte[10]; - for (int j = 0; j < r.nextInt(10); j++) { - bytes[j] = (byte) c; +public class ConnectionTimeoutTest { + public static void main(String[] args) { + NioEventLoopGroup group = new NioEventLoopGroup(); + try { + Bootstrap bootstrap = new Bootstrap() + .group(group) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) + .channel(NioSocketChannel.class) + .handler(new LoggingHandler()); + ChannelFuture future = bootstrap.connect("127.0.0.1", 8080); + future.sync().channel().closeFuture().sync(); + } catch (Exception e) { + e.printStackTrace(); + log.debug("timeout"); + } finally { + group.shutdownGracefully(); + } } - c++; - buf.writeBytes(bytes); } -ctx.writeAndFlush(buf); ``` +**** + + + +#### SO_BACKLOG + +属于 ServerSocketChannal 参数,通过 `option(ChannelOption.SO_BACKLOG, value)` 来设置大小 + +在 Linux 2.2 之前,backlog 大小包括了两个队列的大小,在 2.2 之后,分别用下面两个参数来控制 + +* sync queue:半连接队列,大小通过 `/proc/sys/net/ipv4/tcp_max_syn_backlog` 指定,在 `syncookies` 启用的情况下,逻辑上没有最大值限制 +* accept queue:全连接队列,大小通过 `/proc/sys/net/core/somaxconn` 指定,在使用 listen 函数时,内核会根据传入的 backlog 参数与系统参数,取二者的较小值。如果 accpet queue 队列满了,server 将**发送一个拒绝连接的错误信息**到 client + +![](https://gitee.com/seazean/images/raw/master/Frame/Netty-TCP三次握手.png) + + + +**** + + + +#### 其他参数 + +ALLOCATOR:属于 SocketChannal 参数,用来分配 ByteBuf, ctx.alloc() + +RCVBUF_ALLOCATOR:属于 SocketChannal 参数 + +* 控制 Netty 接收缓冲区大小 +* 负责入站数据的分配,决定入站缓冲区的大小(并可动态调整),统一采用 direct 直接内存,具体池化还是非池化由 allocator 决定 + + + + + +*** + + + + + + + +# Tail + + + + + + + + + + + + + diff --git a/Prog.md b/Prog.md index 9c05c3b..b461c65 100644 --- a/Prog.md +++ b/Prog.md @@ -5946,7 +5946,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 } ``` -* execute():执行任务,但是没有返回值,没办法获取任务执行结果,出现异常会直接抛出任务执行时的异常。根据线程池中的线程数,选择添加任务时的处理方式 +* execute():执行任务,**但是没有返回值,没办法获取任务执行结果**,出现异常会直接抛出任务执行时的异常。根据线程池中的线程数,选择添加任务时的处理方式 ```java // command 可以是普通的 Runnable 实现类,也可以是 FutureTask,不能是 Callable @@ -13851,7 +13851,9 @@ ServerSocket 类: * 构造方法:`public ServerSocket(int port)` * 常用API:`public Socket accept()`,**阻塞等待**接收一个客户端的 Socket 管道连接请求,连接成功返回一个 Socket 对象 - 三次握手后 TCP 连接建立成功,服务器内核会把连接从 SYN 半连接队列中移出,移入 accept (全连接)队列,等待进程调用 accept 函数时把连接取出。如果进程不能及时调用 accept 函数,就会造成 accept 队列溢出,最终导致建立好的 TCP 连接被丢弃 + 三次握手后 TCP 连接建立成功,服务器内核会把连接从 SYN 半连接队列中移出,移入 accept 全连接队列,等待进程调用 accept 函数时把连接取出。如果进程不能及时调用 accept 函数,就会造成 accept 队列溢出,最终导致建立好的 TCP 连接被丢弃 + + 相当于客户端和服务器建立一个数据管道,管道一般不用 close @@ -14998,13 +15000,14 @@ public class ChannelTest { 向选择器注册通道:`SelectableChannel.register(Selector sel, int ops, Object att)` -选择器对通道的监听事件,需要通过第二个参数 ops 指定。监听的事件类型用四个常量表示: - -* 读 : SelectionKey.OP_READ (1) -* 写 : SelectionKey.OP_WRITE (4) -* 连接 : SelectionKey.OP_CONNECT (8) -* 接收 : SelectionKey.OP_ACCEPT (16) -* 若不止监听一个事件,可以使用位或操作符连接:`int interest = SelectionKey.OP_READ | SelectionKey.OP_WRITE` +* 参数一:选择器,指定当前 Channel 注册到的选择器 +* 参数二:选择器对通道的监听事件,监听的事件类型用四个常量表示 + * 读 : SelectionKey.OP_READ (1) + * 写 : SelectionKey.OP_WRITE (4) + * 连接 : SelectionKey.OP_CONNECT (8) + * 接收 : SelectionKey.OP_ACCEPT (16) + * 若不止监听一个事件,使用位或操作符连接:`int interest = SelectionKey.OP_READ | SelectionKey.OP_WRITE` +* 参数三:**关联一个附件**,可以是任何对象 **Selector API**: @@ -15024,7 +15027,7 @@ SelectionKey API: | ------------------------------------------- | -------------------------------------------------- | | public abstract void cancel() | 取消该键的通道与其选择器的注册 | | public abstract SelectableChannel channel() | 返回创建此键的通道,该方法在取消键之后仍将返回通道 | -| public final Object attachment() | 返回当前 key 关联的缓冲 | +| public final Object attachment() | 返回当前 key 关联的附件 | | public final boolean isAcceptable() | 检测此密钥的通道是否已准备好接受新的套接字连接 | | public final boolean isConnectable() | 检测此密钥的通道是否已完成或未完成其套接字连接操作 | | public final boolean isReadable() | 检测此密钥的频道是否可以阅读 | From 930c577f1b6c67fba915613e2be065641aafd89c Mon Sep 17 00:00:00 2001 From: Seazean Date: Tue, 28 Sep 2021 23:27:29 +0800 Subject: [PATCH 061/168] Update Java Notes --- Tool.md | 56 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/Tool.md b/Tool.md index 9b5642f..4849926 100644 --- a/Tool.md +++ b/Tool.md @@ -871,7 +871,7 @@ gpasswd 是 Linux 工作组文件 /etc/group 和 /etc/gshadow 管理工具,用 可以看到命令的帮助文档 **man** [指令名称] 查看帮助文档 -比如 man ls。退出方式 q +比如 man ls,退出方式 q @@ -888,11 +888,11 @@ date 可以用来显示或设定系统的日期与时间 * -d<字符串>:显示字符串所指的日期与时间。字符串前后必须加上双引号; * -s<字符串>:根据字符串来设置日期与时间。字符串前后必须加上双引号 -* -u:显示GMT; +* -u:显示 GMT; * --version:显示版本信息 -查看时间:**date** ------->2020年 11月 30日 星期一 17:10:54 CST -查看指定格式时间:**date "+%Y-%m-%d %H:%M:%S"** -----> 2020-11-30 17:11:44 +查看时间:**date** → 2020年 11月 30日 星期一 17:10:54 CST +查看指定格式时间:**date "+%Y-%m-%d %H:%M:%S"** → 2020-11-30 17:11:44 设置日期指令:**date -s “2019-12-23 19:21:00”** @@ -903,17 +903,17 @@ date 可以用来显示或设定系统的日期与时间 ### id -id会显示用户以及所属群组的实际与有效ID。若两个ID相同,则仅显示实际ID。若仅指定用户名称,则显示目前用户的ID。 +id 会显示用户以及所属群组的实际与有效 ID。若两个 ID 相同,则仅显示实际 ID;若仅指定用户名称,则显示目前用户的 ID。 命令:id [-gGnru] [--help] [--version] [用户名称] //参数的顺序 -- -g或--group  显示用户所属群组的ID。 -- -G或--groups  显示用户所属附加群组的ID。 -- -n或--name  显示用户,所属群组或附加群组的名称。 -- -r或--real  显示实际ID。 -- -u或--user  显示用户ID。 +- -g 或--group  显示用户所属群组的 ID +- -G 或--groups  显示用户所属附加群组的 ID +- -n 或--name  显示用户,所属群组或附加群组的名称。 +- -r 或--real  显示实际 ID +- -u 或--user  显示用户 ID -> id命令参数虽然很多,但是常用的是不带参数的id命令,主要看他的uid和组信息 +> id 命令参数虽然很多,但是常用的是不带参数的id命令,主要看他的uid和组信息 @@ -923,7 +923,7 @@ id会显示用户以及所属群组的实际与有效ID。若两个ID相同, ### sudo -sudo:控制用户对系统命令的使用权限,root允许的操作。通过sudo可以提高普通用户的操作权限 +sudo:控制用户对系统命令的使用权限,root 允许的操作,通过 sudo 可以提高普通用户的操作权限 - -V 显示版本编号 - -h 会显示版本编号及指令的使用方式说明 @@ -934,9 +934,9 @@ sudo:控制用户对系统命令的使用权限,root允许的操作。通过su - -p prompt 可以更改问密码的提示语,其中 %u 会代换为使用者的帐号名称, %h 会显示主机名称 - -u username/#uid 不加此参数,代表要以 root 的身份执行指令,而加了此参数,可以以 username 的身份执行指令(#uid 为该 username 的使用者号码) - -s 执行环境变数中的 SHELL 所指定的 shell ,或是 /etc/passwd 里所指定的 shell -- -H 将环境变数中的 HOME 指定为要变更身份的使用者HOME目录(如不加 -u 参数就是系统管理者 root ) +- -H 将环境变数中的 HOME 指定为要变更身份的使用者 HOME 目录(如不加 -u 参数就是系统管理者 root ) - -command 要以系统管理者身份(或以 -u 更改为其他人)执行的指令 - **sudo -u root command -l**:指定root用户执行指令command + **sudo -u root command -l**:指定 root 用户执行指令 command @@ -948,30 +948,30 @@ sudo:控制用户对系统命令的使用权限,root允许的操作。通过su top:用于实时显示 process 的动态 -* -c:command属性进行了命令补全 +* -c:command 属性进行了命令补全 -* -p 进程号:显示指定pid的进程信息 +* -p 进程号:显示指定 pid 的进程信息 * -d 秒数:表示进程界面更新时间(每几秒刷新一次) * -H 表示线程模式 -`top -Hp 进程id`:分析该进程内各线程的cpu使用情况 +`top -Hp 进程 id`:分析该进程内各线程的cpu使用情况 ![](https://gitee.com/seazean/images/raw/master/Tool/top命令.png) **各进程(任务)的状态监控属性解释说明:** - PID — 进程id - TID — 线程id + PID — 进程 id + TID — 线程 id USER — 进程所有者 PR — 进程优先级 - NI — nice值。负值表示高优先级,正值表示低优先级 - VIRT — 进程使用的虚拟内存总量,单位kb。VIRT=SWAP+RES - RES — 进程使用的、未被换出的物理内存大小,单位kb。RES=CODE+DATA - SHR — 共享内存大小,单位kb - S — 进程状态。D=不可中断的睡眠状态 R=运行 S=睡眠 T=跟踪/停止 Z=僵尸进程 - %CPU — 上次更新到现在的CPU时间占用百分比 + NI — nice 值,负值表示高优先级,正值表示低优先级 + VIRT — 进程使用的虚拟内存总量,单位 kb,VIRT=SWAP+RES + RES — 进程使用的、未被换出的物理内存大小,单位 kb,RES=CODE+DATA + SHR — 共享内存大小,单位 kb + S — 进程状态,D=不可中断的睡眠状态 R=运行 S=睡眠 T=跟踪/停止 Z=僵尸进程 + %CPU — 上次更新到现在的 CPU 时间占用百分比 %MEM — 进程使用的物理内存百分比 - TIME+ — 进程使用的CPU时间总计,单位1/100秒 + TIME+ — 进程使用的 CPU 时间总计,单位 1/100 秒 COMMAND — 进程名称(命令名/命令行) @@ -1005,9 +1005,9 @@ Linux 系统中查看进程使用情况的命令是 ps 指令 **ps和top区别:** -* ps命令:可以查看进程的瞬间信息,是系统在过去执行的进程的静态快照 +* ps 命令:可以查看进程的瞬间信息,是系统在过去执行的进程的静态快照 -* top命令:可以持续的监视进程的动态信息 +* top 命令:可以持续的监视进程的动态信息 From 5059e39a06dabb0578f739df43df715c2b4b2080 Mon Sep 17 00:00:00 2001 From: Seazean Date: Fri, 8 Oct 2021 22:16:48 +0800 Subject: [PATCH 062/168] Update Java Notes --- DB.md | 8 ++++---- Frame.md | 13 +++++-------- Java.md | 7 ++++--- Prog.md | 2 +- SSM.md | 2 +- 5 files changed, 15 insertions(+), 17 deletions(-) diff --git a/DB.md b/DB.md index 380a105..1c56ae1 100644 --- a/DB.md +++ b/DB.md @@ -4418,7 +4418,7 @@ B+Tree 优点:提高查询速度,减少磁盘的 IO 次数,树形结构较 ##### 索引维护 -B+ 树为了维护索引有序性,在插入新值的时候需要做必要的维护 +B+ 树为了保持索引的有序性,在插入新值的时候需要做相应的维护 每个索引中每个块存储在磁盘页中,可能会出现以下两种情况: @@ -5400,7 +5400,7 @@ INSERT INTO `emp` (`id`, `name`, `age`, `salary`) VALUES('1','Tom','25','2300'); CREATE INDEX idx_emp_age_salary ON emp(age,salary); ``` -* 第一种是通过对返回数据进行排序,所有不是通过索引直接返回排序结果的排序都叫 FileSort 排序,会在内存中重新排序 +* 第一种是通过对返回数据进行排序,所有不通过索引直接返回结果的排序都叫 FileSort 排序,会在内存中重新排序 ```mysql EXPLAIN SELECT * FROM emp ORDER BY age DESC; -- 年龄降序 @@ -5408,7 +5408,7 @@ CREATE INDEX idx_emp_age_salary ON emp(age,salary); ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL ORDER BY排序1.png) -* 第二种通过有序索引顺序扫描直接返回有序数据,这种情况为 Using index,不需要额外排序,操作效率高 +* 第二种通过有序索引顺序扫描直接返回**有序数据**,这种情况为 Using index,不需要额外排序,操作效率高 ```mysql EXPLAIN SELECT id, age, salary FROM emp ORDER BY age DESC; @@ -5588,7 +5588,7 @@ MySQL 4.1 版本之后,开始支持 SQL 的子查询 ```mysql EXPLAIN SELECT * FROM tb_user_1 WHERE id > 200000 LIMIT 10; -- 写法 1 - EXPLAIN SELECT * FROM tb_user_1 WHERE id BETWEEN 200000 and 200010; -- 写法 2 + EXPLAIN SELECT * FROM tb_user_1 WHERE id BETWEEN 200000 and 200010; -- 写法 2 ``` ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL分页查询3.png) diff --git a/Frame.md b/Frame.md index 0795ddb..55fbb3c 100644 --- a/Frame.md +++ b/Frame.md @@ -3241,7 +3241,7 @@ HTTP 协议是无状态的,浏览器和服务器间的请求响应一次,下 }); //启动服务器 - ChannelFuture channelFuture = serverBootstrap.bind(7000).sync(); + ChannelFuture channelFuture = serverBootstrap.bind(8080).sync(); channelFuture.channel().closeFuture().sync(); } finally { @@ -3300,7 +3300,7 @@ HTTP 协议是无状态的,浏览器和服务器间的请求响应一次,下 // 判断当前浏览器是否支持websocket if(window.WebSocket) { //go on - socket = new WebSocket("ws://localhost:7000/hello2"); + socket = new WebSocket("ws://localhost:8080/hello"); //相当于channelReado, ev 收到服务器端回送的消息 socket.onmessage = function (ev) { var rt = document.getElementById("responseText"); @@ -3325,11 +3325,12 @@ HTTP 协议是无状态的,浏览器和服务器间的请求响应一次,下 // 发送消息到服务器 function send(message) { - if(!window.socket) { //先判断socket是否创建好 + // 先判断socket是否创建好 + if(!window.socket) { return; } if(socket.readyState == WebSocket.OPEN) { - //通过socket 发送消息 + // 通过socket 发送消息 socket.send(message) } else { alert("连接没有开启"); @@ -3438,10 +3439,6 @@ RCVBUF_ALLOCATOR:属于 SocketChannal 参数 -# Tail - - - diff --git a/Java.md b/Java.md index 9d8f2b2..0fb3182 100644 --- a/Java.md +++ b/Java.md @@ -6780,7 +6780,7 @@ list.stream().filter(s -> s.startsWith("张")); Collection c = new ArrayList<>(); Stream listStream = c.stream(); -//Map集合获取流 +// Map集合获取流 // 先获取键的Stream流。 Stream keysStream = map.keySet().stream(); // 在获取值的Stream流 @@ -8579,6 +8579,7 @@ public class ReflectDemo { * 注解是 JDK1.5 的新特性 * 注解是给编译器或 JVM 看的,编译器或 JVM 可以根据注解来完成对应的功能 * 注解类似修饰符,应用于包、类型、构造方法、方法、成员变量、参数及本地变量的声明语句中 +* 父类中的注解是不能被子类继承的 注解作用: @@ -8757,9 +8758,9 @@ Class 类 API : * `boolean isAnnotationPresent(Class class)`:判断对象是否使用了指定的注解 * `boolean isAnnotation()`:此 Class 对象是否表示注释类型 -注解原理:注解本质是一个继承了 `Annotation` 的特殊接口,其具体实现类是 Java 运行时生成的**动态代理类**,通过反射获取注解时,返回的是运行时生成的动态代理对象 `$Proxy1`,通过代理对象调用自定义注解(接口)的方法,回调 `AnnotationInvocationHandler` 的 `invoke` 方法,该方法会从 `memberValues` 这个Map 中找出对应的值,而 `memberValues` 的来源是 Java 常量池 +注解原理:注解本质是**特殊接口**,继承了 `Annotation` ,其具体实现类是 Java 运行时生成的**动态代理类**,通过反射获取注解时,返回的是运行时生成的动态代理对象 `$Proxy1`,通过代理对象调用自定义注解(接口)的方法,回调 `AnnotationInvocationHandler` 的 `invoke` 方法,该方法会从 `memberValues` 这个 Map 中找出对应的值,而 `memberValues` 的来源是 Java 常量池 -解析注解数据的原理:注解在哪个成分上,就先拿哪个成分对象,比如注解作用在类上,则要该类的Class对象,再来拿上面的注解 +解析注解数据的原理:注解在哪个成分上,就先拿哪个成分对象,比如注解作用在类上,则要该类的 Class 对象,再来拿上面的注解 ```java public class AnnotationDemo{ diff --git a/Prog.md b/Prog.md index b461c65..6a1c1d1 100644 --- a/Prog.md +++ b/Prog.md @@ -13422,7 +13422,7 @@ epoll 的特点: * epoll 仅适用于 Linux 系统 * epoll 使用**一个文件描述符管理多个描述符**,将用户关系的文件描述符的事件存放到内核的一个事件表(个人理解成哑元节点) -* 没有最大描述符数量(并发连接)的限制,打开 fd 的上限远大于1024(1G 内存能监听约10万个端口) +* 没有最大描述符数量(并发连接)的限制,打开 fd 的上限远大于1024(1G 内存能监听约 10 万个端口) * epoll 的时间复杂度 O(1),epoll 理解为 event poll,不同于忙轮询和无差别轮询,调用 epoll_wait **只是轮询就绪链表**。当监听列表有设备就绪时调用回调函数,把就绪 fd 放入就绪链表中,并唤醒在 epoll_wait 中阻塞的进程,所以 epoll 实际上是**事件驱动**(每个事件关联上fd)的,降低了 system call 的时间复杂度 * epoll 内核中根据每个 fd 上的 callback 函数来实现,只有活跃的 socket 才会主动调用 callback,所以使用 epoll 没有前面两者的线性下降的性能问题,效率提高 diff --git a/SSM.md b/SSM.md index ec3e827..b6edc17 100644 --- a/SSM.md +++ b/SSM.md @@ -7412,7 +7412,7 @@ public void addAccount{} * 情况 6:Spring 的事务传播策略在**内部方法**调用时将不起作用,在一个 Service 内部,事务方法之间的嵌套调用,普通方法和事务方法之间的嵌套调用,都不会开启新的事务,事务注解要加到调用方法上才生效 - 原因:Spring 的事务都是使用 AOP 代理的模式,动态代理最终是要调用原始对象,而原始对象在去调用方法时是不会触发拦截器,就是**一个方法调用本对象的另一个方法**,所以事务也就无法生效 + 原因:Spring 的事务都是使用 AOP 代理的模式,动态代理 invoke 后会调用原始对象,而原始对象在去调用方法时是不会触发拦截器,就是**一个方法调用本对象的另一个方法**,所以事务也就无法生效 ```java @Transactional From 52c22bb29296add4fd3be699a70a2345105219d8 Mon Sep 17 00:00:00 2001 From: Seazean Date: Tue, 12 Oct 2021 22:54:15 +0800 Subject: [PATCH 063/168] Update Java Notes --- DB.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DB.md b/DB.md index 1c56ae1..c9b9ea3 100644 --- a/DB.md +++ b/DB.md @@ -2631,9 +2631,9 @@ update T set c=c+1 where ID=2; -流程说明:执行引擎将这行新数据更新到内存中(Buffer Pool)后,然后会将这个更新操作记录到 redo log buffer 里,此时 redo log 处于 prepare 状态,代表执行完成随时可以提交事务,然后执行器生成这个操作的 binlog,并**把 binlog 写入磁盘**,在提交事务后 **redo log 也持久化到磁盘** +流程说明:执行引擎将这行新数据更新到内存中(Buffer Pool)后,然后会将这个更新操作记录到 redo log buffer 里,此时 redo log 处于 prepare 状态,代表执行完成随时可以提交事务,然后执行器生成这个操作的 binlog 并**把 binlog 写入磁盘**,在提交事务后 **redo log 也持久化到磁盘** -redo log 和 binlog 都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致,也有利于主从复制,更好的保持主从数据的一致性 +redo log 和 binlog 都可以用于表示事务的提交状态,而**两阶段提交就是让这两个状态保持逻辑上的一致**,也有利于主从复制,更好的保持主从数据的一致性 故障恢复数据: @@ -11497,7 +11497,7 @@ Cache Aside Pattern 中服务端需要同时维系 DB 和 cache,并且是以 D #### 读写穿透 -读写穿透模式 Read/Write Through Pattern:服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中,cache 负责将此数据读取和写入 DB,从而减轻了应用程序的职责 +读写穿透模式 Read/Write Through Pattern:服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中,cache 负责将此数据同步写入 DB,从而减轻了应用程序的职责 * 写操作:先查 cache,cache 中不存在,直接更新 DB;cache 中存在则先更新 cache,然后 cache 服务更新 DB(同步更新 cache 和 DB) From c6affc81fa8faf08c32d076606207a6eeea2f614 Mon Sep 17 00:00:00 2001 From: Seazean Date: Fri, 15 Oct 2021 00:13:05 +0800 Subject: [PATCH 064/168] Update Java Notes --- Tool.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tool.md b/Tool.md index 4849926..d5e0f99 100644 --- a/Tool.md +++ b/Tool.md @@ -328,7 +328,7 @@ git push origin branch-name:推送到远程仓库,origin 是引用名 ### 切换分支 -git checkout branch-name:切换到branch-name分支 +git checkout branch-name:切换到 branch-name 分支 From 2cf310aab9e0f5e65dcf5fe53964b95e3bc60e89 Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 18 Oct 2021 22:40:37 +0800 Subject: [PATCH 065/168] Update Java Notes --- Prog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Prog.md b/Prog.md index 6a1c1d1..6e602cb 100644 --- a/Prog.md +++ b/Prog.md @@ -8966,7 +8966,7 @@ public static void main(String[] args) throws InterruptedException { ###### await -总体流程是将 await 线程包装成 node 节点放入 ConditionObject 的阻条件塞队列,如果被唤醒就将 node 转移到 AQS 的执行阻塞队列,等待获取锁 +总体流程是将 await 线程包装成 node 节点放入 ConditionObject 的条件队列,如果被唤醒就将 node 转移到 AQS 的执行阻塞队列,等待获取锁 * 开始 Thread-0 持有锁,调用 await,线程进入 ConditionObject 等待,直到被唤醒或打断,调用 await 方法的线程都是持锁状态的,所以说逻辑里**不存在并发** From 198c9ed8d10dbac9a9426b657897f026d5aa9de3 Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 1 Nov 2021 23:29:11 +0800 Subject: [PATCH 066/168] Update Java Notes --- DB.md | 14 +++++++----- Frame.md | 44 +++++++++++++++++++----------------- Java.md | 6 ++--- Tool.md | 68 ++++++++++++++++++++++++++++---------------------------- 4 files changed, 69 insertions(+), 63 deletions(-) diff --git a/DB.md b/DB.md index c9b9ea3..3bdd92b 100644 --- a/DB.md +++ b/DB.md @@ -4225,6 +4225,8 @@ InnoDB 中,聚簇索引是按照每张表的主键构造一颗 B+ 树,叶子 **检索过程**:辅助索引找到主键值,再通过聚簇索引(二分)找到数据页,最后通过数据页中的 Page Directory(二分)找到对应的数据分组,遍历组内所所有的数据找到数据行 +补充:无索引走全表查询,查到数据页后和上述步骤一致 + *** @@ -4233,7 +4235,7 @@ InnoDB 中,聚簇索引是按照每张表的主键构造一颗 B+ 树,叶子 ##### 索引实现 -InnoDB 使用 B+Tree 作为索引结构 +InnoDB 使用 B+Tree 作为索引结构,并且 InnoDB 一定有索引 主键索引: @@ -4739,7 +4741,7 @@ Innodb_xxxx:这几个参数只是针对InnoDB 存储引擎的,累加的算 #### 定位低效 -慢 SQL 由三种原因造成: +SQL 执行慢有两种情况: * 偶尔慢:DB 在刷新脏页 * redo log 写满了 @@ -4898,12 +4900,12 @@ SQL 执行的顺序的标识,SQL 从大到小的执行 | type | 含义 | | ------ | ------------------------------------------------------------ | -| ALL | Full Table Scan,MySQL将遍历全表以找到匹配的行,全表扫描 | +| ALL | Full Table Scan,MySQL 将遍历全表以找到匹配的行,全表扫描 | | index | Full Index Scan,index 与 ALL 区别为 index 类型只遍历索引树 | -| range | 索引范围扫描,常见于between、<、>等的查询 | -| ref | 非唯一性索引扫描,返回匹配某个单独值的所有,本质上也是一种索引访问 | +| range | 索引范围扫描,常见于 between、<、> 等的查询 | +| ref | 非唯一性索引扫描,返回匹配某个单独值的所有记录,本质上也是一种索引访问 | | eq_ref | 唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配,常见于主键或唯一索引扫描 | -| const | 当 MySQL 对查询某部分进行优化,并转换为一个常量时,使用该类型访问,如将主键置于where列表中,MySQL就能将该查询转换为一个常量 | +| const | 当 MySQL 对查询某部分进行优化,并转换为一个常量时,使用该类型访问,如将主键置于 where 列表中,MySQL 就能将该查询转换为一个常量 | | system | system 是 const 类型的特例,当查询的表只有一行的情况下,使用 system | | NULL | MySQL 在优化过程中分解语句,执行时甚至不用访问表或索引 | diff --git a/Frame.md b/Frame.md index 55fbb3c..af8d30c 100644 --- a/Frame.md +++ b/Frame.md @@ -2858,26 +2858,26 @@ public class LoginRequestMessage extends Message { * 如果能确保编解码器不会保存状态,可以继承 MessageToMessageCodec 父类 -```java -@Slf4j -@ChannelHandler.Sharable -// 必须和 LengthFieldBasedFrameDecoder 一起使用,确保接到的 ByteBuf 消息是完整的 -public class MessageCodecSharable extends MessageToMessageCodec { - @Override - protected void encode(ChannelHandlerContext ctx, Message msg, List outList) throws Exception { - ByteBuf out = ctx.alloc().buffer(); - // 4 字节的魔数 - out.writeBytes(new byte[]{1, 2, 3, 4}); - // .... - outList.add(out); - } - - @Override - protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { - //.... - } -} -``` + ````java + @Slf4j + @ChannelHandler.Sharable + // 必须和 LengthFieldBasedFrameDecoder 一起使用,确保接到的 ByteBuf 消息是完整的 + public class MessageCodecSharable extends MessageToMessageCodec { + @Override + protected void encode(ChannelHandlerContext ctx, Message msg, List outList) throws Exception { + ByteBuf out = ctx.alloc().buffer(); + // 4 字节的魔数 + out.writeBytes(new byte[]{1, 2, 3, 4}); + // .... + outList.add(out); + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + //.... + } + } + ```` @@ -3439,6 +3439,10 @@ RCVBUF_ALLOCATOR:属于 SocketChannal 参数 +# RocketMQ + + + diff --git a/Java.md b/Java.md index 0fb3182..ae24482 100644 --- a/Java.md +++ b/Java.md @@ -12975,9 +12975,9 @@ attributes[](属性表):属性表的每个项的值必须是 attribute_inf | Exceptions | 方法表 | 方法抛出的异常 | | EnclosingMethod | 类文件 | 仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法 | | InnerClass | 类文件 | 内部类列表 | - | LineNumberTable | Code属性 | Java 源码的行号与字节码指令的对应关系 | - | LocalVariableTable | Code属性 | 方法的局部变量描述 | - | StackMapTable | Code属性 | JDK1.6中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配 | + | LineNumberTable | Code 属性 | Java 源码的行号与字节码指令的对应关系 | + | LocalVariableTable | Code 属性 | 方法的局部变量描述 | + | StackMapTable | Code 属性 | JDK1.6 中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配 | | Signature | 类,方法表,字段表 | 用于支持泛型情况下的方法签名 | | SourceFile | 类文件 | 记录源文件名称 | | SourceDebugExtension | 类文件 | 用于存储额外的调试信息 | diff --git a/Tool.md b/Tool.md index d5e0f99..d81b3ce 100644 --- a/Tool.md +++ b/Tool.md @@ -1003,7 +1003,7 @@ Linux 系统中查看进程使用情况的命令是 ps 指令 -**ps和top区别:** +**ps 和 top 区别:** * ps 命令:可以查看进程的瞬间信息,是系统在过去执行的进程的静态快照 @@ -1576,9 +1576,9 @@ tail 命令可用于查看文件的内容,有一个常用的参数 **-f** 常 `tail -f filename`:动态显示最尾部的内容 -`tail -n +2 txtfile.txt`:显示文件txtfile.txt 的内容,从第 2 行至文件末尾 +`tail -n +2 txtfile.txt`:显示文件 txtfile.txt 的内容,从第 2 行至文件末尾 -`tail -n 2 txtfile.txt`:显示文件txtfile.txt 的内容,最后2行 +`tail -n 2 txtfile.txt`:显示文件 txtfile.txt 的内容,最后 2 行 @@ -1605,28 +1605,28 @@ grep 指令用于查找内容包含指定的范本样式的文件,若不指定 grep [-abcEFGhHilLnqrsvVwxy][-A<显示列数>][-B<显示列数>][-C<显示列数>][-d<进行动作>][-e<范本样式>][-f<范本文件>][--help][范本样式][文件或目录...] ``` -* -c:只输出匹配行的计数。 -* -i:不区分大小写。 -* -h:查询多文件时不显示文件名。 -* -l:查询多文件时只输出包含匹配字符的文件名。 -* -n:显示匹配行及行号。 -* -s:不显示不存在或无匹配文本的错误信息。 -* -v:显示不包含匹配文本的所有行。 -* --color=auto :可以将找到的关键词部分加上颜色的显示。 +* -c:只输出匹配行的计数 +* -i:不区分大小写 +* -h:查询多文件时不显示文件名 +* -l:查询多文件时只输出包含匹配字符的文件名 +* -n:显示匹配行及行号 +* -s:不显示不存在或无匹配文本的错误信息 +* -v:显示不包含匹配文本的所有行 +* --color=auto :可以将找到的关键词部分加上颜色的显示 **管道符 |**:表示将前一个命令处理的结果传递给后面的命令处理。 -`grep aaaa Filename `:显示存在关键字aaaa的行 +`grep aaaa Filename `:显示存在关键字 aaaa 的行 -`grep -n aaaa Filename`:显示存在关键字aaaa的行,且显示行号 +`grep -n aaaa Filename`:显示存在关键字 aaaa 的行,且显示行号 -`grep -i aaaa Filename`:忽略大小写,显示存在关键字aaaa的行 +`grep -i aaaa Filename`:忽略大小写,显示存在关键字 aaaa 的行 `grep -v aaaa Filename`:显示存在关键字aaaa的所有行 -`ps -ef | grep sshd`:查找包含sshd进程的进程信息 +`ps -ef | grep sshd`:查找包含 sshd 进程的进程信息 -` ps -ef|grep -c sshd`:查找sshd相关的进程个数 +` ps -ef | grep -c sshd`:查找 sshd 相关的进程个数 @@ -1642,32 +1642,32 @@ grep [-abcEFGhHilLnqrsvVwxy][-A<显示列数>][-B<显示列数>][-C<显示列数 - 通过 `命令 >> 文件` 将**命令的成功结果** **追加** 指定文件的后面 - 通过 `命令 &>> 文件` 将 **命令的失败结果** **追加** 指定文件的后面 -`echo "程序员" >> a.txt`:将程序员追加到a.txt后面 +`echo "程序员" >> a.txt`:将程序员追加到 a.txt 后面 -`cat 不存在的目录 &>> error.log`:将错误信息追加到error.log文件 +`cat 不存在的目录 &>> error.log`:将错误信息追加到 error.log 文件 #### awk -AWK是一种处理文本文件的语言,是一个强大的文本分析工具。 +AWK 是一种处理文本文件的语言,是一个强大的文本分析工具。 ```shell awk [options] 'script' var=value file(s) awk [options] -f scriptfile var=value file(s) ``` -* -F fs 指定输入文件折分隔符,fs是一个字符串或者是一个正则表达式 -* -v var=value赋值一个用户定义变量 -* -f 从脚本文件中读取awk命令 -* $n(数字) 获取**第几段**内容 -* $0 获取 **当前行** 内容 -* NF 表示当前行共有多少个字段 -* $NF 代表 最后一个字段 +* -F fs:指定输入文件折分隔符,fs 是一个字符串或者是一个正则表达式 +* -v:var=value 赋值一个用户定义变量 +* -f:从脚本文件中读取 awk 命令 +* $n(数字):获取**第几段**内容 +* $0:获取**当前行** 内容 +* NF:表示当前行共有多少个字段 +* $NF:代表最后一个字段 -* $(NF-1) 代表 倒数第二个字段 +* $(NF-1):代表倒数第二个字段 -* NR 代表 处理的是第几行 +* NR:代表处理的是第几行 * ```shell 命令:awk 'BEGIN{初始化操作}{每行都执行} END{结束时操作}' @@ -1728,19 +1728,19 @@ zhouba 98 44 46 #### find -find命令用来在指定目录下查找文件。任何位于参数之前的字符串都将被视为欲查找的目录名。如果使用该命令时,不设置任何参数,则find命令将在当前目录下查找子目录与文件。并且将查找到的子目录和文件全部进行显示 +find 命令用来在指定目录下查找文件。任何位于参数之前的字符串都将被视为查找的目录名。如果使用该命令不设置任何参数,将在当前目录下查找子目录与文件,并且将查找到的子目录和文件全部进行显示 命令:find <指定目录> <指定条件> <指定内容> * `find . -name "*.gz"`:将目前目录及其子目录下所有延伸档名是 gz 的文件查询出来 -* `find . -ctime -1`:将目前目录及其子目录下所有最近 1天内更新过的文件查询出来 -* ` find / -name 'seazean'`:全局搜索seazean +* `find . -ctime -1`:将目前目录及其子目录下所有最近 1 天内更新过的文件查询出来 +* ` find / -name 'seazean'`:全局搜索 seazean #### read -read命令用于从标准输入读取数值。 +read 命令用于从标准输入读取数值。 read 内部命令被用来从标准输入读取单行数据。这个命令可以用来读取键盘输入,当使用重定向的时候,可以读取文件中的一行数据。 @@ -1759,10 +1759,10 @@ sort [-bcdfimMnr][文件] ``` * -n 依照数值的大小排序 -* -r 以相反的顺序来排序(sort默认的排序方式是**升序**,改成降序,加-r) +* -r 以相反的顺序来排序(sort 默认的排序方式是**升序**,改成降序,加 -r) * -u 去掉重复 -面试题:一列数字,输出最大的4个不重复的数 +面试题:一列数字,输出最大的 4 个不重复的数 ```sh sort -ur a.txt | head -n 4 From 1084367d4d843471dac22a0a5b14fdf21e03c9ae Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 3 Nov 2021 01:25:48 +0800 Subject: [PATCH 067/168] Update README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 9952e97..6433260 100644 --- a/README.md +++ b/README.md @@ -18,3 +18,5 @@ * 所有的知识不保证权威性,如果各位朋友发现错误,非常欢迎与我讨论。 * Java.md 更新后大于 1M,导致网页无法显示,所以划分为 Java 和 Program 两个文档。 +* 笔记的编写是基于 Windows 平台,可能会因为平台的不同而造成空格、制表符的显示效果不同。 + From 3f38376e44437614802b7f3e4e73d3a0b0e875cd Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 3 Nov 2021 23:46:19 +0800 Subject: [PATCH 068/168] Update Java Notes --- DB.md | 8 ++--- Frame.md | 97 ++++++++++++++++++++++++++++---------------------------- 2 files changed, 52 insertions(+), 53 deletions(-) diff --git a/DB.md b/DB.md index 3bdd92b..6c28da1 100644 --- a/DB.md +++ b/DB.md @@ -4606,7 +4606,7 @@ B+ 树为了保持索引的有序性,在插入新值的时候需要做相应 **适用条件**: -* 需要存储引擎将索引中的数据与条件进行判断,所以优化是基于存储引擎的,只有特定引擎可以使用,适用于 InnoDB 和 MyISAM +* 需要存储引擎将索引中的数据与条件进行判断(所以条件列必须都在同一个索引中),所以优化是基于存储引擎的,只有特定引擎可以使用,适用于 InnoDB 和 MyISAM * 存储引擎没有调用跨存储引擎的能力,跨存储引擎的功能有存储过程、触发器、视图,所以调用这些功能的不可以进行索引下推优化 * 对于 InnoDB 引擎只适用于二级索引,InnoDB 的聚簇索引会将整行数据读到缓冲区,不再需要去回表查询了,索引下推的目的减少 IO 次数也就失去了意义 @@ -4673,8 +4673,6 @@ CREATE INDEX idx_area ON table_name(area(7)); - - *** @@ -4900,12 +4898,12 @@ SQL 执行的顺序的标识,SQL 从大到小的执行 | type | 含义 | | ------ | ------------------------------------------------------------ | -| ALL | Full Table Scan,MySQL 将遍历全表以找到匹配的行,全表扫描 | +| ALL | Full Table Scan,MySQL 将遍历全表以找到匹配的行,全表扫描,如果是 InnoDB 引擎是扫描聚簇索引 | | index | Full Index Scan,index 与 ALL 区别为 index 类型只遍历索引树 | | range | 索引范围扫描,常见于 between、<、> 等的查询 | | ref | 非唯一性索引扫描,返回匹配某个单独值的所有记录,本质上也是一种索引访问 | | eq_ref | 唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配,常见于主键或唯一索引扫描 | -| const | 当 MySQL 对查询某部分进行优化,并转换为一个常量时,使用该类型访问,如将主键置于 where 列表中,MySQL 就能将该查询转换为一个常量 | +| const | 通过主键或者唯一索引来定位一条记录 | | system | system 是 const 类型的特例,当查询的表只有一行的情况下,使用 system | | NULL | MySQL 在优化过程中分解语句,执行时甚至不用访问表或索引 | diff --git a/Frame.md b/Frame.md index af8d30c..8a731be 100644 --- a/Frame.md +++ b/Frame.md @@ -177,7 +177,7 @@ Path 下配置:`%MAVEN_HOME%\bin` ### 手动搭建 -1. 在 E 盘下创建目录 `mvnproject 进入该目录,作为我们的操作目录 +1. 在 E 盘下创建目录 mvnproject 进入该目录,作为我们的操作目录 2. 创建我们的 Maven 项目,创建一个目录 `project-java` 作为我们的项目文件夹,并进入到该目录 @@ -283,7 +283,7 @@ Path 下配置:`%MAVEN_HOME%\bin` 1. 在 IDEA 中配置 Maven,选择 maven3.6.1 防止依赖问题 IDEA配置Maven -2. 创建 Maven,New Module → Maven→ 不选中 Create from archetype +2. 创建 Maven,New Module → Maven → 不选中 Create from archetype 3. 填写项目的坐标 @@ -320,15 +320,15 @@ Path 下配置:`%MAVEN_HOME%\bin` -web 工程: +Web 工程: -1. 选择 web 对应的原型骨架(选择 Maven 开头的是简化的) +1. 选择 Web 对应的原型骨架(选择 Maven 开头的是简化的) ![](https://gitee.com/seazean/images/raw/master/Frame/IDEA创建Maven-webapp.png) -2. 通过原型创建 web 项目得到的目录结构是不全的,因此需要我们自行补全,同时要标记正确 +2. 通过原型创建 Web 项目得到的目录结构是不全的,因此需要我们自行补全,同时要标记正确 -3. web 工程创建之后需要启动运行,使用 tomcat 插件来运行项目,在 `pom.xml` 中添加插件的坐标: +3. Web 工程创建之后需要启动运行,使用 tomcat 插件来运行项目,在 `pom.xml` 中添加插件的坐标: ```xml @@ -413,7 +413,6 @@ web 工程: 注意:直接依赖和间接依赖其实也是一个相对关系 - 依赖传递的冲突问题:在依赖传递过程中产生了冲突,有三种优先法则 @@ -423,9 +422,7 @@ web 工程: * 特殊优先:当同级配置了相同资源的不同版本时,后配置的覆盖先配置的 - - -**可选依赖:**对外隐藏当前所依赖的资源,不透明 +**可选依赖**:对外隐藏当前所依赖的资源,不透明 ```xml @@ -437,7 +434,7 @@ web 工程: ``` -**排除依赖:主动**断开依赖的资源,被排除的资源无需指定版本 +**排除依赖**:主动断开依赖的资源,被排除的资源无需指定版本 ```xml @@ -663,30 +660,31 @@ Maven 的插件用来执行生命周期中的相关事件 - 间接依赖 ssm_dao、ssm_pojo - ```xml - - - - demo - ssm_service - 1.0-SNAPSHOT - - - - - - - - - - - - - - ``` - + + ```xml + + + + demo + ssm_service + 1.0-SNAPSHOT + + + + + + + + + + + + + + ``` + - 修改 web.xml 配置文件中加载 Spring 环境的配置文件名称,使用*通配,加载所有 applicationContext- 开始的配置文件: - + ```xml @@ -694,13 +692,13 @@ Maven 的插件用来执行生命周期中的相关事件 classpath*:applicationContext-*.xml ``` - + - spring-mvc - + ```xml ``` - + *** @@ -856,7 +854,11 @@ Maven 的插件用来执行生命周期中的相关事件 * 属性类别: - 1.自定义属性 2.内置属性 3.Setting属性 4.Java系统属性 5.环境变量属性 + 1. 自定义属性 + 2. 内置属性 + 3. setting 属性 + 4. Java 系统属性 + 5. 环境变量属性 * 自定义属性: @@ -898,13 +900,14 @@ Maven 的插件用来执行生命周期中的相关事件 * vresion 是 1.0-SNAPSHOT - ```xml - demo - ssm - 1.0-SNAPSHOT - ``` -* Setting 属性 + ```xml + demo + ssm + 1.0-SNAPSHOT + ``` + +* setting 属性 - 使用 Maven 配置文件 setting.xml 中的标签属性,用于动态配置 @@ -920,7 +923,7 @@ Maven 的插件用来执行生命周期中的相关事件 调用格式: - ``` + ```xml ${user.home} ``` @@ -936,7 +939,7 @@ Maven 的插件用来执行生命周期中的相关事件 调用格式: - ``` + ```xml ${env.JAVA_HOME} ``` @@ -977,9 +980,7 @@ RELEASE(发布版本) - 里程碑版本:表明一个版本的里程碑(版本内部)。这样的版本同下一个正式版本相比,相对来说不是很稳定,有待更多的测试 -范例: -- 5.1.9.RELEASE From 4a7852b72fb38999353067289027312ec3f0f40e Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 6 Nov 2021 01:01:13 +0800 Subject: [PATCH 069/168] Update Java Notes --- DB.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/DB.md b/DB.md index 6c28da1..579e96a 100644 --- a/DB.md +++ b/DB.md @@ -573,7 +573,12 @@ SELECT * FROM t WHERE id = 1; ##### 扫描行数 -优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。找到一个最优的执行方案,用最小的代价去执行语句 +优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。 + +* 根据搜索条件找出所有可能的使用的索引 +* 计算全表扫描的代价 +* 计算使用不同索引执行 SQL 的的代价 +* 找到一个最优的执行方案,用最小的代价去执行语句 在数据库里面,扫描行数是影响执行代价的因素之一,扫描的行数越少意味着访问磁盘的次数越少,消耗的 CPU 资源越少,优化器还会结合是否使用临时表、是否排序等因素进行综合判断 @@ -1190,13 +1195,12 @@ LIMIT SELECT 列名1,列名2,... FROM 表名; ``` -* 去除重复查询 - 注意:只有值全部重复的才可以去除 - +* 去除重复查询:只有值全部重复的才可以去除,需要创建临时表辅助查询 + ```mysql SELECT DISTINCT 列名1,列名2,... FROM 表名; ``` - + * 计算列的值(四则运算) ```mysql @@ -2012,7 +2016,7 @@ WHERE #### 内连接 -查询原理:内连接查询的是两张表有交集的部分数据 +查询原理:内连接查询的是两张表有交集的部分数据,分为驱动表和被驱动表,首先查询驱动表得到结果集,然后根据结果集中的每一条记录都分别到被驱动表中查找匹配 * 显式内连接 @@ -4671,6 +4675,42 @@ CREATE INDEX idx_area ON table_name(area(7)); +**** + + + +#### 索引合并 + +使用多个索引来完成一次查询的执行方法叫做索引合并 index merge + +* Intersection 索引合并: + + ```sql + SELECT * FROM table_test WHERE key1 = 'a' AND key3 = 'b'; # key1 和 key3 列都是单列索引、二级索引 + ``` + + 从不同索引中扫描到的记录的 id 值取交集(相同 id),然后执行回表操作,要求从每个二级索引获取到的记录都是按照主键值排序 + +* Union 索引合并: + + ```sql + SELECT * FROM table_test WHERE key1 = 'a' OR key3 = 'b'; + ``` + + 从不同索引中扫描到的记录的 id 值取并集,然后执行回表操作,要求从每个二级索引获取到的记录都是按照主键值排序 + +* Sort-Union 索引合并 + + ```sql + SELECT * FROM table_test WHERE key1 < 'a' OR key3 > 'b'; + ``` + + 先将从不同索引中扫描到的记录的主键值进行排序,再按照 Union 索引合并的方式进行查询 + + + + + *** From 9cb437d3b2c92d71fd9db1f6f986f939d792c4ee Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 8 Nov 2021 01:33:28 +0800 Subject: [PATCH 070/168] Update Java Notes --- Frame.md | 1037 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ SSM.md | 19 +- 2 files changed, 1053 insertions(+), 3 deletions(-) diff --git a/Frame.md b/Frame.md index 8a731be..587bb0c 100644 --- a/Frame.md +++ b/Frame.md @@ -3432,6 +3432,8 @@ RCVBUF_ALLOCATOR:属于 SocketChannal 参数 + + *** @@ -3442,15 +3444,1050 @@ RCVBUF_ALLOCATOR:属于 SocketChannal 参数 # RocketMQ +## 消息队列 + +消息队列是一种先进先出的数据结构,常见的应用场景: + +* 应用解耦:系统的耦合性越高,容错性就越低 + + 实例:用户创建订单后,耦合调用库存系统、物流系统、支付系统,任何一个子系统出了故障都会造成下单异常,影响用户使用体验。使用消息队列解耦合,比如物流系统发生故障,需要几分钟恢复,将物流系统要处理的数据缓存到消息队列中,用户的下单操作正常完成。等待物流系统正常后处理存在消息队列中的订单消息即可,终端系统感知不到物流系统发生过几分钟故障 + + ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-解耦.png) + +* 流量削峰:应用系统如果遇到系统请求流量的瞬间猛增,有可能会将系统压垮,使用消息队列可以将大量请求缓存起来,分散到很长一段时间处理,这样可以提高系统的稳定性和用户体验。 + + ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-流量削峰.png) + +* 数据分发:让数据在多个系统更加之间进行流通,数据的产生方不需要关心谁来使用数据,只需要将数据发送到消息队列,数据使用方直接在消息队列中直接获取数据 + + ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-数据分发.png) + +主要缺点包含以下几点: + +* 系统可用性降低:系统引入的外部依赖越多,系统稳定性越差,一旦 MQ 宕机,就会对业务造成影响 + + 引申问题:如何保证 MQ 的高可用? + +* 系统复杂度提高:MQ 的加入大大增加了系统的复杂度,以前系统间是同步的远程调用,现在是通过 MQ 进行异步调用 + + 引申问题:如何保证消息没有被重复消费?怎么处理消息丢失情况?那么保证消息传递的顺序性? + +* 一致性问题:A 系统处理完业务,通过 MQ 给 B、C、D 三个系统发消息数据,如果 B 系统、C 系统处理成功,D 系统处理失败 + + 引申问题:如何保证消息数据处理的一致性? + + + + + +**** + + + + + +## 概念模型 + +### 安装测试 + +安装需要 Java 环境,下载解压后进入安装目录,进行启动: + +* 启动 NameServer + + ```sh + # 1.启动 NameServer + nohup sh bin/mqnamesrv & + # 2.查看启动日志 + tail -f ~/logs/rocketmqlogs/namesrv.log + ``` + + RocketMQ 默认的虚拟机内存较大,需要编辑如下两个配置文件,修改 JVM 内存大小 + + ```shell + # 编辑runbroker.sh和runserver.sh修改默认JVM大小 + vi runbroker.sh + vi runserver.sh + ``` + + 参考配置:JAVA_OPT="${JAVA_OPT} -server -Xms256m -Xmx256m -Xmn128m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m" + +* 启动 Broker + + ```sh + # 1.启动 Broker + nohup sh bin/mqbroker -n localhost:9876 autoCreateTopicEnable=true & + # 2.查看启动日志 + tail -f ~/logs/rocketmqlogs/broker.log + ``` + +* 发送消息: + + ```sh + # 1.设置环境变量 + export NAMESRV_ADDR=localhost:9876 + # 2.使用安装包的 Demo 发送消息 + sh bin/tools.sh org.apache.rocketmq.example.quickstart.Producer + ``` + +* 接受消息: + + ```sh + # 1.设置环境变量 + export NAMESRV_ADDR=localhost:9876 + # 2.接收消息 + sh bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer + +* 关闭 RocketMQ: + + ```sh + # 1.关闭 NameServer + sh bin/mqshutdown namesrv + # 2.关闭 Broker + sh bin/mqshutdown broker + + + +**** + + + +### 基本概念 + +#### 服务相关 + +RocketMQ 主要由 Producer、Broker、Consumer 三部分组成,其中 Producer 负责生产消息,Consumer 负责消费消息,Broker 负责存储消息,NameServer 负责管理 Broker + +* 代理服务器(Broker Server):消息中转角色,负责**存储消息、转发消息**。在 RocketMQ 系统中负责接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备,也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等 + +* 名字服务(Name Server):充当**路由消息**的提供者。生产者或消费者能够通过名字服务查找各主题相应的 Broker IP 列表 + +* 消息生产者(Producer):负责**生产消息**,把业务应用系统里产生的消息发送到 Broker 服务器。RocketMQ 提供多种发送方式,同步发送、异步发送、顺序发送、单向发送,同步和异步方式均需要 Broker 返回确认信息,单向发送不需要;可以通过 MQ 的负载均衡模块选择相应的 Broker 集群队列进行消息投递,投递的过程支持快速失败并且低延迟 +* 消息消费者(Consumer):负责**消费消息**,一般是后台系统负责异步消费,一个消息消费者会从 Broker 服务器拉取消息、并将其提供给应用程序。从用户应用的角度而提供了两种消费形式: + * 拉取式消费(Pull Consumer):应用通主动调用 Consumer 的拉消息方法从 Broker 服务器拉消息,主动权由应用控制,一旦获取了批量消息,应用就会启动消费过程 + * 推动式消费(Push Consumer):该模式下 Broker 收到数据后会主动推送给消费端,实时性较高 + +* 生产者组(Producer Group):同一类 Producer 的集合,都发送同一类消息且发送逻辑一致。如果发送的是事务消息且原始生产者在发送之后崩溃,**则 Broker 服务器会联系同一生产者组的其他生产者实例以提交或回溯消费** + +* 消费者组(Consumer Group):同一类 Consumer 的集合,消费者实例必须订阅完全相同的 Topic,消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面更容易的实现负载均衡和容错。RocketMQ 支持两种消息模式: + * 集群消费(Clustering):相同 Consumer Group 的每个 Consumer 实例平均分摊消息 + * 广播消费(Broadcasting):相同 Consumer Group 的每个 Consumer 实例都接收全量的消息 + + + +*** + + + +#### 消息相关 + +每个 Broker 可以存储多个 Topic 的消息,每个 Topic 的消息也可以分片存储于不同的 Broker,Message Queue(消息队列)是用于存储消息的物理地址,每个 Topic 中的消息地址存储于多个 Message Queue 中 + +* 主题(Topic):表示一类消息的集合,每个主题包含若干条消息,每条消息只属于一个主题,是 RocketMQ 消息订阅的基本单位 + +* 消息(Message):消息系统所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题。RocketMQ 中每个消息拥有唯一的 Message ID,且可以携带具有业务标识的 Key,系统提供了通过 Message ID 和 Key 查询消息的功能 + +* 标签(Tag):为消息设置的标志,用于同一主题下区分不同类型的消息。标签能够有效地保持代码的清晰度和连贯性,并优化 RocketMQ 提供的查询系统,消费者可以根据 Tag 实现对不同子主题的不同消费逻辑,实现更好的扩展性 + +* 普通顺序消息(Normal Ordered Message):消费者通过同一个消息队列(Topic 分区)收到的消息是有顺序的,不同消息队列收到的消息则可能是无顺序的 + +* 严格顺序消息(Strictly Ordered Message):消费者收到的所有消息均是有顺序的 + + + +官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/concept.md + + + + + +*** + + + + + +### 集群设计 + +#### 集群模式 + +常用的以下几种模式: + +* 单 Master 模式:这种方式风险较大,一旦 Broker 重启或者宕机,会导致整个服务不可用 + +* 多 Master 模式:一个集群无 Slave,全是 Master + + - 优点:配置简单,单个 Master 宕机或重启维护对应用无影响,在磁盘配置为 RAID10 时,即使机器宕机不可恢复情况下,由于 RAID10 磁盘非常可靠,消息也不会丢(异步刷盘丢失少量消息,同步刷盘一条不丢),性能最高 + + - 缺点:单台机器宕机期间,这台机器上未被消费的消息在机器恢复之前不可订阅,消息实时性会受到影响 + +* 多 Master 多 Slave 模式(同步):每个 Master 配置一个 Slave,有多对 Master-Slave,HA 采用同步双写方式,即只有主备都写成功,才向应用返回成功 + + * 优点:数据与服务都无单点故障,Master 宕机情况下,消息无延迟,服务可用性与数据可用性都非常高 + * 缺点:性能比异步复制略低(大约低 10% 左右),发送单个消息的 RT 略高,目前不能实现主节点宕机,备机自动切换为主机 + +* 多 Master 多 Slave 模式(异步):HA 采用异步复制的方式,会造成主备有短暂的消息延迟(毫秒级别) + + - 优点:即使磁盘损坏,消息丢失的非常少,且消息实时性不会受影响,同时 Master 宕机后,消费者仍然可以从 Slave 消费,而且此过程对应用透明,不需要人工干预,性能同多 Master 模式几乎一样 + + - 缺点:Master 宕机,磁盘损坏情况下会丢失少量消息 + + + + +*** + + + +#### 系统架构 + +NameServer 是一个简单的 Topic 路由注册中心,支持 Broker 的动态注册与发现。NameServer 通常是集群的方式部署,各实例间相互不进行信息通讯。Broker 向每一台 NameServer 注册自己的路由信息,所以每个 NameServer 实例上面**都保存一份完整的路由信息**。当某个 NameServer 因某种原因下线了,Broker 仍可以向其它 NameServer 同步其路由信息 + +NameServer 主要包括两个功能: + +* Broker 管理,NameServer 接受 Broker 集群的注册信息并保存下来作为路由信息的基本数据,提供**心跳检测**检查 Broker 活性 +* 路由信息管理,每个 NameServer 将保存关于 Broker 集群的整个路由信息和用于客户端查询的队列信息,然后 Producer 和 Conumser 通过 NameServer 就可以知道整个 Broker 集群的路由信息,从而进行消息的投递和消费 + +BrokerServer 主要负责消息的存储、投递和查询以及服务高可用保证,为了实现这些功能,Broker 包含了以下几个重要子模块: +* Remoting Module:整个 Broker 的实体,负责处理来自 clients 端的请求 +* Client Manager:负责管理客户端(Producer/Consumer)和维护 Consumer 的 Topic 订阅信息 +* Store Service:提供方便简单的 API 接口处理消息存储到物理硬盘和查询功能 + +* HA Service:高可用服务,提供 Master Broker 和 Slave Broker 之间的数据同步功能 + +* Index Service:根据特定的 Message key 对投递到 Broker 的消息进行索引服务,以提供消息的快速查询 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-Broker工作流程.png) + + + +*** + + + +#### 集群架构 + +RocketMQ 网络部署特点: + +- NameServer 是一个几乎**无状态节点**,节点之间相互独立,无任何信息同步 + +- Broker 部署相对复杂,Broker 分为 Master 与 Slave,Master 可以部署多个,一个 Master 可以对应多个 Slave,但是一个 Slave 只能对应一个 Master,Master 与 Slave 的对应关系通过指定相同 BrokerName、不同 BrokerId 来定义,BrokerId 为 0 是 Master,非 0 表示 Slave。**每个 Broker 与 NameServer 集群中的所有节点建立长连接**,定时注册 Topic 信息到所有 NameServer + + 注意:部署架构上也支持一 Master 多 Slave,但只有 BrokerId=1 的从服务器才会参与消息的读负载(读写分离) + +- Producer 与 NameServer 集群中的其中**一个节点(随机选择)建立长连接**,定期从 NameServer 获取 Topic 路由信息,并向提供 Topic 服务的 Master 建立长连接,且定时向 Master **发送心跳**。Producer 完全无状态,可集群部署 + +- Consumer 与 NameServer 集群中的其中一个节点(随机选择)建立长连接,定期从 NameServer 获取 Topic 路由信息,并向提供 Topic 服务的 Master、Slave 建立长连接,且定时向 Master、Slave 发送心跳 + + Consumer 既可以从 Master 订阅消息,也可以从 Slave 订阅消息,在向 Master 拉取消息时,Master 服务器会根据拉取偏移量与最大偏移量的距离(判断是否读老消息,产生读 I/O),以及从服务器是否可读等因素建议下一次是从 Master 还是 Slave 拉取 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-集群架构.png) + +集群工作流程: + +- 启动 NameServer 监听端口,等待 Broker、Producer、Consumer 连上来,相当于一个路由控制中心 +- Broker 启动,跟所有的 NameServer 保持长连接,定时发送心跳包。心跳包中包含当前 Broker 信息(IP、端口等)以及存储所有 Topic 信息。注册成功后,NameServer 集群中就有 Topic 跟 Broker 的映射关系 +- 收发消息前,先创建 Topic,创建 Topic 时需要指定该 Topic 要存储在哪些 Broker 上,也可以在发送消息时自动创建 Topic +- Producer 发送消息,启动时先跟 NameServer 集群中的其中一台建立长连接,并从 NameServer 中获取当前发送的 Topic 存在哪些 Broker 上,轮询从队列列表中选择一个队列,然后与队列所在的 Broker 建立长连接从而向 Broker 发消息。 +- Consumer 跟 Producer 类似,跟其中一台 NameServer 建立长连接,获取当前订阅 Topic 存在哪些 Broker 上,然后直接跟 Broker 建立连接通道,开始消费消息 + + + +官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/architecture.md + + + + + +**** + + + + + +## 基本操作 + +### 基本样例 + +#### 工作流程 + +导入 MQ 客户端依赖 + +```xml + + org.apache.rocketmq + rocketmq-client + 4.4.0 + +``` + +消息发送者步骤分析: + +1. 创建消息生产者 producer,并制定生产者组名 +2. 指定 Nameserver 地址 +3. 启动 producer +4. 创建消息对象,指定主题 Topic、Tag 和消息体 +5. 发送消息 +6. 关闭生产者 producer + +消息消费者步骤分析: + +1. 创建消费者 Consumer,制定消费者组名 +2. 指定 Nameserver 地址 +3. 订阅主题 Topic 和 Tag +4. 设置回调函数,处理消息 +5. 启动消费者 consumer + + + +官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/RocketMQ_Example.md + + + +*** + + + +#### 发送消息 + +##### 同步发送 + +使用 RocketMQ 发送三种类型的消息:同步消息、异步消息和单向消息,其中前两种消息是可靠的,因为会有发送是否成功的应答 + +这种可靠性同步地发送方式使用的比较广泛,比如:重要的消息通知,短信通知 + +```java +public class SyncProducer { + public static void main(String[] args) throws Exception { + // 实例化消息生产者Producer + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + // 设置NameServer的地址 + producer.setNamesrvAddr("localhost:9876"); + // 启动Producer实例 + producer.start(); + for (int i = 0; i < 100; i++) { + // 创建消息,并指定Topic,Tag和消息体 + Message msg = new Message( + "TopicTest" /* Topic */, + "TagA" /* Tag */, + ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */); + + // 发送消息到一个Broker + SendResult sendResult = producer.send(msg); + // 通过sendResult返回消息是否成功送达 + System.out.printf("%s%n", sendResult); + } + // 如果不再发送消息,关闭Producer实例。 + producer.shutdown(); + } +} +``` + + + +*** + + + +##### 异步发送 + +异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间地等待 Broker 的响应 + +```java +public class AsyncProducer { + public static void main(String[] args) throws Exception { + // 实例化消息生产者Producer + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + // 设置NameServer的地址 + producer.setNamesrvAddr("localhost:9876"); + // 启动Producer实例 + producer.start(); + producer.setRetryTimesWhenSendAsyncFailed(0); + + int messageCount = 100; + // 根据消息数量实例化倒计时计算器 + final CountDownLatch2 countDownLatch = new CountDownLatch2(messageCount); + for (int i = 0; i < messageCount; i++) { + final int index = i; + // 创建消息,并指定Topic,Tag和消息体 + Message msg = new Message("TopicTest", "TagA", "OrderID188", + "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); + + // SendCallback接收异步返回结果的回调 + producer.send(msg, new SendCallback() { + // 发送成功回调函数 + @Override + public void onSuccess(SendResult sendResult) { + countDownLatch.countDown(); + System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId()); + } + + @Override + public void onException(Throwable e) { + countDownLatch.countDown(); + System.out.printf("%-10d Exception %s %n", index, e); + e.printStackTrace(); + } + }); + } + // 等待5s + countDownLatch.await(5, TimeUnit.SECONDS); + // 如果不再发送消息,关闭Producer实例。 + producer.shutdown(); + } +} +``` + + + +*** + + + +##### 单向发送 + +单向发送主要用在不特别关心发送结果的场景,例如日志发送 + +```java +public class OnewayProducer { + public static void main(String[] args) throws Exception{ + // 实例化消息生产者Producer + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + // 设置NameServer的地址 + producer.setNamesrvAddr("localhost:9876"); + // 启动Producer实例 + producer.start(); + for (int i = 0; i < 100; i++) { + // 创建消息,并指定Topic,Tag和消息体 + Message msg = new Message("TopicTest","TagA", + ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)); + // 发送单向消息,没有任何返回结果 + producer.sendOneway(msg); + } + // 如果不再发送消息,关闭Producer实例。 + producer.shutdown(); + } +} +``` + + + +**** + + + +#### 消费消息 + +```java +public class Consumer { + public static void main(String[] args) throws InterruptedException, MQClientException { + // 实例化消费者 + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name"); + // 设置NameServer的地址 + consumer.setNamesrvAddr("localhost:9876"); + + // 订阅一个或者多个Topic,以及Tag来过滤需要消费的消息 + consumer.subscribe("TopicTest", "*"); + // 注册消息监听器,回调实现类来处理从broker拉取回来的消息 + consumer.registerMessageListener(new MessageListenerConcurrently() { + // 接受消息内容 + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { + System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs); + // 标记该消息已经被成功消费 + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + // 启动消费者实例 + consumer.start(); + System.out.printf("Consumer Started.%n"); + } +} +``` + + + + + +**** +### 顺序消息 +#### 原理解析 + +消息有序指的是一类消息消费时,能按照发送的顺序来消费。例如:一个订单产生了三条消息分别是订单创建、订单付款、订单完成。消费时要按照这个顺序消费才能有意义,但是同时订单之间是可以并行消费的,RocketMQ 可以严格的保证消息有序。 + +顺序消息分为全局顺序消息与分区顺序消息, + +- 全局顺序:对于指定的一个 Topic,所有消息按照严格的先入先出(FIFO)的顺序进行发布和消费。 适用于性能要求不高,所有的消息严格按照 FIFO 原则进行消息发布和消费的场景 +- 分区顺序:对于指定的一个 Topic,所有消息根据 sharding key 进行分区,同一个分组内的消息按照严格的 FIFO 顺序进行发布和消费。Sharding key 是顺序消息中用来区分不同分区的关键字段,和普通消息的 Key 是完全不同的概念。 适用于性能要求高,以 sharding key 作为分区字段,在同一个区中严格的按照 FIFO 原则进行消息发布和消费的场景 + +在默认的情况下消息发送会采取 Round Robin 轮询方式把消息发送到不同的 queue(分区队列),而消费消息是从多个 queue 上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个 queue 中,消费的时候只从这个 queue 上依次拉取,则就保证了顺序。当发送和消费参与的 queue 只有一个,则是全局有序;如果多个queue参与,则为分区有序,即相对每个 queue,消息都是有序的 + + + +*** + + + +#### 代码实例 + +一个订单的顺序流程是:创建、付款、推送、完成,订单号相同的消息会被先后发送到同一个队列中,消费时同一个 OrderId 获取到的肯定是同一个队列 + +```java +public class Producer { + public static void main(String[] args) throws Exception { + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + producer.setNamesrvAddr("127.0.0.1:9876"); + producer.start(); + // 标签集合 + String[] tags = new String[]{"TagA", "TagC", "TagD"}; + + // 订单列表 + List orderList = new Producer().buildOrders(); + + Date date = new Date(); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String dateStr = sdf.format(date); + for (int i = 0; i < 10; i++) { + // 加个时间前缀 + String body = dateStr + " Hello RocketMQ " + orderList.get(i); + Message msg = new Message("OrderTopic", tags[i % tags.length], "KEY" + i, body.getBytes()); + /** + * 参数一:消息对象 + * 参数二:消息队列的选择器 + * 参数三:选择队列的业务标识(订单 ID) + */ + SendResult sendResult = producer.send(msg, new MessageQueueSelector() { + @Override + /** + * mqs:队列集合 + * msg:消息对象 + * arg:业务标识的参数 + */ + public MessageQueue select(List mqs, Message msg, Object arg) { + Long id = (Long) arg; + long index = id % mqs.size(); // 根据订单id选择发送queue + return mqs.get((int) index); + } + }, orderList.get(i).getOrderId());//订单id + + System.out.println(String.format("SendResult status:%s, queueId:%d, body:%s", + sendResult.getSendStatus(), + sendResult.getMessageQueue().getQueueId(), + body)); + } + + producer.shutdown(); + } + + // 订单的步骤 + private static class OrderStep { + private long orderId; + private String desc; + // set + get + } + + // 生成模拟订单数据 + private List buildOrders() { + List orderList = new ArrayList(); + + OrderStep orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111039L); + orderDemo.setDesc("创建"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111065L); + orderDemo.setDesc("创建"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111039L); + orderDemo.setDesc("付款"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103117235L); + orderDemo.setDesc("创建"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111065L); + orderDemo.setDesc("付款"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103117235L); + orderDemo.setDesc("付款"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111065L); + orderDemo.setDesc("完成"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111039L); + orderDemo.setDesc("推送"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103117235L); + orderDemo.setDesc("完成"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111039L); + orderDemo.setDesc("完成"); + orderList.add(orderDemo); + + return orderList; + } +} +``` + +```java +// 顺序消息消费,带事务方式(应用可控制Offset什么时候提交) +public class ConsumerInOrder { + public static void main(String[] args) throws Exception { + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_3"); + consumer.setNamesrvAddr("127.0.0.1:9876"); + // 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费 + // 如果非第一次启动,那么按照上次消费的位置继续消费 + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + // 订阅三个tag + consumer.subscribe("OrderTopic", "TagA || TagC || TagD"); + consumer.registerMessageListener(new MessageListenerOrderly() { + Random random = new Random(); + @Override + public ConsumeOrderlyStatus consumeMessage(List msgs, ConsumeOrderlyContext context) { + context.setAutoCommit(true); + for (MessageExt msg : msgs) { + // 可以看到每个queue有唯一的consume线程来消费, 订单对每个queue(分区)有序 + System.out.println("consumeThread=" + Thread.currentThread().getName() + "queueId=" + msg.getQueueId() + ", content:" + new String(msg.getBody())); + } + return ConsumeOrderlyStatus.SUCCESS; + } + }); + consumer.start(); + System.out.println("Consumer Started."); + } +} +``` + + + + + +***** + + + +### 延时消息 + +提交了一个订单就可以发送一个延时消息,1h 后去检查这个订单的状态,如果还是未付款就取消订单释放库存 + +RocketMQ 并不支持任意时间的延时,需要设置几个固定的延时等级,从 1s 到 2h 分别对应着等级 1 到 18,消息消费失败会进入延时消息队列,消息发送时间与设置的延时等级和重试次数有关,详见代码 `SendMessageProcessor.java` + +```java +private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h"; +``` + +```java +public class ScheduledMessageProducer { + public static void main(String[] args) throws Exception { + // 实例化一个生产者来产生延时消息 + DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup"); + producer.setNamesrvAddr("127.0.0.1:9876"); + // 启动生产者 + producer.start(); + int totalMessagesToSend = 100; + for (int i = 0; i < totalMessagesToSend; i++) { + Message message = new Message("DelayTopic", ("Hello scheduled message " + i).getBytes()); + // 设置延时等级3,这个消息将在10s之后发送(现在只支持固定的几个时间,详看delayTimeLevel) + message.setDelayTimeLevel(3); + // 发送消息 + producer.send(message); + } + // 关闭生产者 + producer.shutdown(); + } +} +``` + +```java +public class ScheduledMessageConsumer { + public static void main(String[] args) throws Exception { + // 实例化消费者 + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ExampleConsumer"); + consumer.setNamesrvAddr("127.0.0.1:9876"); + // 订阅Topics + consumer.subscribe("DelayTopic", "*"); + // 注册消息监听者 + consumer.registerMessageListener(new MessageListenerConcurrently() { + @Override + public ConsumeConcurrentlyStatus consumeMessage(List messages, ConsumeConcurrentlyContext context) { + for (MessageExt message : messages) { + // 打印延迟的时间段 + System.out.println("Receive message[msgId=" + message.getMsgId() + "] " + (System.currentTimeMillis() - message.getBornTimestamp()) + "ms later");} + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + // 启动消费者 + consumer.start(); + } +} +``` + + + +**** + + + +### 批量消息 + +批量发送消息能显著提高传递小消息的性能,限制是这些批量消息应该有相同的 topic,相同的 waitStoreMsgOK,而且不能是延时消息,并且这一批消息的总大小不应超过 4MB + +```java +public class Producer { + + public static void main(String[] args) throws Exception { + DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup") + producer.setNamesrvAddr("127.0.0.1:9876"); + //启动producer + producer.start(); + + List msgs = new ArrayList(); + // 创建消息对象,指定主题Topic、Tag和消息体 + Message msg1 = new Message("BatchTopic", "Tag1", ("Hello World" + 1).getBytes()); + Message msg2 = new Message("BatchTopic", "Tag1", ("Hello World" + 2).getBytes()); + Message msg3 = new Message("BatchTopic", "Tag1", ("Hello World" + 3).getBytes()); + + msgs.add(msg1); + msgs.add(msg2); + msgs.add(msg3); + + // 发送消息 + SendResult result = producer.send(msgs); + System.out.println("发送结果:" + result); + // 关闭生产者producer + producer.shutdown(); + } +} +``` + +当发送大批量数据时,可能不确定消息是否超过了大小限制(4MB),所以需要将消息列表分割一下 + +```java +public class ListSplitter implements Iterator> { + private final int SIZE_LIMIT = 1024 * 1024 * 4; + private final List messages; + private int currIndex; + + public ListSplitter(List messages) { + this.messages = messages; + } + + @Override + public boolean hasNext() { + return currIndex < messages.size(); + } + + @Override + public List next() { + int startIndex = getStartIndex(); + int nextIndex = startIndex; + int totalSize = 0; + for (; nextIndex < messages.size(); nextIndex++) { + Message message = messages.get(nextIndex); + int tmpSize = calcMessageSize(message); + // 单个消息超过了最大的限制 + if (tmpSize + totalSize > SIZE_LIMIT) { + break; + } else { + totalSize += tmpSize; + } + } + List subList = messages.subList(startIndex, nextIndex); + currIndex = nextIndex; + return subList; + } + + private int getStartIndex() { + Message currMessage = messages.get(currIndex); + int tmpSize = calcMessageSize(currMessage); + while (tmpSize > SIZE_LIMIT) { + currIndex += 1; + Message message = messages.get(curIndex); + tmpSize = calcMessageSize(message); + } + return currIndex; + } + + private int calcMessageSize(Message message) { + int tmpSize = message.getTopic().length() + message.getBody().length; + Map properties = message.getProperties(); + for (Map.Entry entry : properties.entrySet()) { + tmpSize += entry.getKey().length() + entry.getValue().length(); + } + tmpSize = tmpSize + 20; // 增加⽇日志的开销20字节 + return tmpSize; + } + + public static void main(String[] args) { + //把大的消息分裂成若干个小的消息 + ListSplitter splitter = new ListSplitter(messages); + while (splitter.hasNext()) { + try { + List listItem = splitter.next(); + producer.send(listItem); + } catch (Exception e) { + e.printStackTrace(); + //处理error + } + } + } +} +``` + + + + + +*** + + + +### 过滤消息 + +#### 基本语法 + +RocketMQ 定义了一些基本语法来支持过滤特性,可以很容易地扩展: + +- 数值比较,比如:>,>=,<,<=,BETWEEN,= +- 字符比较,比如:=,<>,IN +- IS NULL 或者 IS NOT NULL +- 逻辑符号 AND,OR,NOT + +常量支持类型为: + +- 数值,比如 123,3.1415 +- 字符,比如 'abc',必须用单引号包裹起来 +- NULL,特殊的常量 +- 布尔值,TRUE 或 FALSE + +只有使用 push 模式的消费者才能用使用SQL92标准的sql语句,接口如下: + +```java +public void subscribe(final String topic, final MessageSelector messageSelector) +``` + +例如:消费者接收包含 TAGA 或 TAGB 或 TAGC 的消息 + +```java +DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("CID_EXAMPLE"); +consumer.subscribe("TOPIC", "TAGA || TAGB || TAGC"); +``` + + + +**** + + + +#### 代码实例 + +发送消息时,通过 putUserProperty 来设置消息的属性 + +```java +public class Producer { + public static void main(String[] args) throws Exception { + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + producer.setNamesrvAddr("127.0.0.1:9876"); + producer.start(); + for (int i = 0; i < 10; i++) { + Message msg = new Message("FilterTopic", "tag", + ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)); + // 设置一些属性 + msg.putUserProperty("i", String.valueOf(i)); + SendResult sendResult = producer.send(msg); + } + producer.shutdown(); + } +} +``` + +使用 SQL 筛选过滤消息: + +```java +public class Consumer { + public static void main(String[] args) throws Exception { + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name"); + consumer.setNamesrvAddr("127.0.0.1:9876"); + // 过滤属性大于 5 的消息 + consumer.subscribe("FilterTopic", MessageSelector.bySql("i>5")); + + // 设置回调函数,处理消息 + consumer.registerMessageListener(new MessageListenerConcurrently() { + //接受消息内容 + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { + for (MessageExt msg : msgs) { + System.out.println("consumeThread=" + Thread.currentThread().getName() + "," + new String(msg.getBody())); + } + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + // 启动消费者consumer + consumer.start(); + } +} +``` + + + + + +*** + + + +### 事务消息 + +#### 事务机制 + +事务消息共有三种状态,提交状态、回滚状态、中间状态: + +- TransactionStatus.CommitTransaction:提交事务,允许消费者消费此消息。 +- TransactionStatus.RollbackTransaction:回滚事务,代表该消息将被删除,不允许被消费 +- TransactionStatus.Unknown:中间状态,代表需要检查消息队列来确定状态 + +使用限制: + +1. 事务消息不支持延时消息和批量消息 +2. Broker 配置文件中的参数 `transactionTimeout` 为特定时间,事务消息将在特定时间长度之后被检查。当发送事务消息时,还可以通过设置用户属性 `CHECK_IMMUNITY_TIME_IN_SECONDS` 来改变这个限制,该参数优先于 `transactionTimeout` 参数 +3. 为了避免单个消息被检查太多次而导致半队列消息累积,默认将单个消息的检查次数限制为 15 次,开发者可以通过 Broker 配置文件的 `transactionCheckMax` 参数来修改此限制。如果已经检查某条消息超过 N 次(N = `transactionCheckMax`), 则 Broker 将丢弃此消息,在默认情况下会打印错误日志。可以通过重写 `AbstractTransactionalMessageCheckListener` 类来修改这个行为 +4. 事务性消息可能不止一次被检查或消费 +5. 提交给用户的目标主题消息可能会失败,可以查看日志的记录。事务的高可用性通过 RocketMQ 本身的高可用性机制来保证,如果希望事务消息不丢失、并且事务完整性得到保证,可以使用同步的双重写入机制 +6. 事务消息的生产者 ID 不能与其他类型消息的生产者 ID 共享。与其他类型的消息不同,事务消息允许反向查询,MQ 服务器能通过消息的生产者 ID 查询到消费者 + + + +**** + + + +#### 代码实例 + +使用 **TransactionMQProducer** 类创建事务性生产者,并指定唯一的 `ProducerGroup`,就可以设置自定义线程池来处理这些检查请求,执行本地事务后、需要根据执行结果对消息队列进行回复 + +```java +public class Producer { + public static void main(String[] args) throws MQClientException, InterruptedException { + // 创建事务监听器 + TransactionListener transactionListener = new TransactionListenerImpl(); + // 创建消息生产者 + TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name"); + ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS); + producer.setExecutorService(executorService); + // 生产者的监听器 + producer.setTransactionListener(transactionListener); + producer.start(); + String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"}; + for (int i = 0; i < 10; i++) { + try { + Message msg = new Message("TransactionTopic", tags[i % tags.length], "KEY" + i, + ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)); + SendResult sendResult = producer.sendMessageInTransaction(msg, null); + System.out.printf("%s%n", sendResult); + Thread.sleep(10); + } catch (MQClientException | UnsupportedEncodingException e) { + e.printStackTrace(); + } + } + //Thread.sleep(1000000); + //producer.shutdown();暂时不关闭 + } +} +``` + +消费者代码和前面的实例相同的 + +实现事务的监听接口,当发送半消息成功时 + +* `executeLocalTransaction` 方法来执行本地事务,返回三个事务状态之一 +* `checkLocalTransaction` 方法检查本地事务状态,响应消息队列的检查请求,返回三个事务状态之一 + +```java +public class TransactionListenerImpl implements TransactionListener { + private AtomicInteger transactionIndex = new AtomicInteger(0); + private ConcurrentHashMap localTrans = new ConcurrentHashMap<>(); + + @Override + public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { + int value = transactionIndex.getAndIncrement(); + int status = value % 3; + // 将事务ID和状态存入 map 集合 + localTrans.put(msg.getTransactionId(), status); + return LocalTransactionState.UNKNOW; + } + + @Override + public LocalTransactionState checkLocalTransaction(MessageExt msg) { + // 从 map 集合读出当前事务对应的状态 + Integer status = localTrans.get(msg.getTransactionId()); + if (null != status) { + switch (status) { + case 0: + return LocalTransactionState.UNKNOW; + case 1: + return LocalTransactionState.COMMIT_MESSAGE; + case 2: + return LocalTransactionState.ROLLBACK_MESSAGE; + } + } + return LocalTransactionState.COMMIT_MESSAGE; + } +} +``` + + + + + + + + + + + +**** + + + + + +## 高级特性 + +待补充笔记: + +* https://github.com/apache/rocketmq/blob/master/docs/cn/design.md#3-%E6%B6%88%E6%81%AF%E8%BF%87%E6%BB%A4 +* https://github.com/apache/rocketmq/blob/master/docs/cn/design.md#5-%E4%BA%8B%E5%8A%A1%E6%B6%88%E6%81%AF + + + + + +**** +## 源码分析 diff --git a/SSM.md b/SSM.md index b6edc17..740fc8f 100644 --- a/SSM.md +++ b/SSM.md @@ -12869,7 +12869,7 @@ DispatcherServlet#checkMultipart: 校验分类:客户端校验和服务端校验 * 格式校验 - * 客户端:使用Js技术,利用正则表达式校验 + * 客户端:使用 js 技术,利用正则表达式校验 * 服务端:使用校验框架 * 逻辑校验 * 客户端:使用ajax发送要校验的数据,在服务端完成逻辑校验,返回校验结果 @@ -12903,8 +12903,9 @@ DispatcherServlet#checkMultipart: ``` **注意:** -tomcat7:搭配hibernate-validator版本5.*.*.Final -tomcat8.5↑:搭配hibernate-validator版本6.*.*.Final + +* tomcat7:搭配 hibernate-validator 版本 5.*.*.Final +* tomcat8.5:搭配 hibernate-validator 版本 6.*.*.Final @@ -12917,9 +12918,13 @@ tomcat8.5↑:搭配hibernate-validator版本6.*.*.Final ##### 开启校验 名称:@Valid、@Validated + 类型:形参注解 + 位置:处理器类中的实体类类型的方法形参前方 + 作用:设定对当前实体类类型参数进行校验 + 范例: ```java @@ -12934,9 +12939,13 @@ public String addEmployee(@Valid Employee employee) { ##### 设置校验规则 名称:@NotNull + 类型:属性注解等 + 位置:实体类属性上方 + 作用:设定当前属性校验规则 + 范例:每个校验规则所携带的参数不同,根据校验规则进行相应的调整,具体的校验规则查看对应的校验框架进行获取 ```java @@ -13012,9 +13021,13 @@ public String addEmployee(@Valid Employee employee, Errors errors, Model model){ #### 嵌套校验 名称:@Valid + 类型:属性注解 + 位置:实体类中的引用类型属性上方 + 作用:设定当前应用类型属性中的属性开启校验 + 范例: ```java From 83738e501b849e3ed2aac19ddca971cef4c39333 Mon Sep 17 00:00:00 2001 From: Seazean Date: Tue, 9 Nov 2021 01:32:40 +0800 Subject: [PATCH 071/168] Update Java Notes --- Frame.md | 137 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 119 insertions(+), 18 deletions(-) diff --git a/Frame.md b/Frame.md index 587bb0c..0c18f72 100644 --- a/Frame.md +++ b/Frame.md @@ -3594,7 +3594,7 @@ RocketMQ 主要由 Producer、Broker、Consumer 三部分组成,其中 Produce -官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/concept.md +官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/concept.md(基础知识部分的笔记参考官方文档编写) @@ -3934,7 +3934,7 @@ public class Consumer { -#### 代码实例 +#### 代码实现 一个订单的顺序流程是:创建、付款、推送、完成,订单号相同的消息会被先后发送到同一个队列中,消费时同一个 OrderId 获取到的肯定是同一个队列 @@ -4282,7 +4282,7 @@ RocketMQ 定义了一些基本语法来支持过滤特性,可以很容易地 - NULL,特殊的常量 - 布尔值,TRUE 或 FALSE -只有使用 push 模式的消费者才能用使用SQL92标准的sql语句,接口如下: +只有使用 push 模式的消费者才能用使用 SQL92 标准的 sql 语句,接口如下: ```java public void subscribe(final String topic, final MessageSelector messageSelector) @@ -4297,13 +4297,33 @@ consumer.subscribe("TOPIC", "TAGA || TAGB || TAGC"); +*** + + + +#### 原理解析 + +RocketMQ 分布式消息队列的消息过滤方式是在 Consumer 端订阅消息时再做消息过滤的。因为 RocketMQ 在 Producer 端写入消息和在 Consumer 端订阅消息采用**分离存储**的机制实现,Consumer 端订阅消息是需要通过 ConsumeQueue 这个消息消费的逻辑队列拿到一个索引,然后再从 CommitLog 里面读取真正的消息实体内容,所以绕不开其存储结构。 + +ConsumeQueue 的存储结构如下,有 8 个字节存储的 Message Tag 的哈希值,基于 Tag 的消息过滤就是基于这个字段 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消费队列结构.png) + +* Tag 过滤:Consumer 端订阅消息时指定 Topic 和 TAG,然后将订阅请求构建成一个 SubscriptionData,发送一个 Pull 消息的请求给 Broker 端。Broker 端用这些数据先构建一个 MessageFilter,然后传给文件存储层 Store。Store 从 ConsumeQueue 读取到一条记录后,会用它记录的消息 tag hash 值去做过滤。因为在服务端只是根据 hashcode 进行判断,无法精确对 tag 原始字符串进行过滤,所以消费端拉取到消息后,还需要对消息的原始 tag 字符串进行比对,如果不同,则丢弃该消息,不进行消息消费 + +* SQL92 过滤:工作流程和 Tag 过滤大致一样,只是在 Store 层的具体过滤方式不一样。真正的 SQL expression 的构建和执行由 rocketmq-filter 模块负责,每次过滤都去执行 SQL 表达式会影响效率,所以 RocketMQ 使用了 BloomFilter 来避免了每次都去执行 + + + + + **** -#### 代码实例 +#### 代码实现 -发送消息时,通过 putUserProperty 来设置消息的属性 +发送消息时,通过 putUserProperty 来设置消息的属性,SQL92 的表达式上下文为消息的属性 ```java public class Producer { @@ -4360,7 +4380,97 @@ public class Consumer { ### 事务消息 -#### 事务机制 +#### 工作流程 + +RocketMQ 支持分布式事务消息,采用了 2PC 的思想来实现了提交事务消息,同时增加一个补偿逻辑来处理二阶段超时或者失败的消息,如下图所示: + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-事务消息.png) + +事务消息的大致方案分为两个流程:正常事务消息的发送及提交、事务消息的补偿流程 + +1. 事务消息发送及提交: + + * 发送消息(Half 消息) + + * 服务端响应消息写入结果 + + * 根据发送结果执行本地事务(如果写入失败,此时 Half 消息对业务不可见,本地逻辑不执行) + * 根据本地事务状态执行 Commit 或者 Rollback(Commit 操作生成消息索引,消息对消费者可见) + +2. 补偿流程: + + * 对没有 Commit/Rollback 的事务消息(pending 状态的消息),从服务端发起一次回查 + * Producer 收到回查消息,检查回查消息对应的本地事务的状态 + + * 根据本地事务状态,重新 Commit 或者 Rollback + + 补偿阶段用于解决消息 Commit 或者 Rollback 发生超时或者失败的情况 + + + +**** + + + +#### 原理解析 + +##### 不可见性 + +事务消息相对普通消息最大的特点就是**一阶段发送的消息对用户是不可见的**,因为对于 Half 消息,会备份原消息的主题与消息消费队列,然后改变主题为 RMQ_SYS_TRANS_HALF_TOPIC,由于消费组未订阅该主题,故消费端无法消费 Half 类型的消息 + +RocketMQ 会开启一个定时任务,从 Topic 为 RMQ_SYS_TRANS_HALF_TOPIC 中拉取消息进行消费,根据生产者组获取一个服务提供者发送回查事务状态请求,根据事务状态来决定是提交或回滚消息 + +在 RocketMQ 中,每条消息都会有对应的索引信息,Consumer 通过 ConsumeQueue 这个二级索引来读取消息实体内容,如图: + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-事务工作流程.png) + +RocketMQ 的具体实现策略:如果写入的是事务消息,对消息的 Topic 和 Queue 等属性进行替换,同时将原来的 Topic 和 Queue 信息存储到消息的属性中,因为消息主题被替换,消息并不会转发到该原主题的消息消费队列,消费者无法感知消息的存在,不会消费 + + + +**** + + + +##### OP消息 + +一阶段写入不可见的消息后,二阶段操作: + +* 如果执行 Commit 操作,则需要让消息对用户可见,构建出 Half 消息的索引。一阶段的 Half 消息写到一个特殊的 Topic,所以构建索引时需要读取出 Half 消息,并将 Topic 和 Queue 替换成真正的目标的 Topic 和 Queue,然后通过一次普通消息的写入操作来生成一条对用户可见的消息 + +* 如果是 Rollback 则需要撤销一阶段的消息,因为消息本就不可见,所以并不需要真正撤销消息(实际上 RocketMQ 也无法去删除一条消息,因为是顺序写文件的),为了区分这条消息没有确定的状态(Pending 状态),RocketMQ 用 Op 消息标识事务消息已经确定的状态(Commit 或者 Rollback) + +事务消息无论是 Commit 或者 Rollback 都会记录一个 Op 操作,两者的区别是 Commit 相对于 Rollback 在写入 Op 消息前创建 Half 消息的索引。如果一条事务消息没有对应的 Op 消息,说明这个事务的状态还无法确定(可能是二阶段失败了) + +RocketMQ 将 Op 消息写入到全局一个特定的 Topic 中,通过源码中的方法 `TransactionalMessageUtil.buildOpTopic()`,这个主题是一个内部的 Topic(像 Half 消息的 Topic 一样),不会被用户消费。Op 消息的内容为对应的 Half 消息的存储的 Offset,这样通过 Op 消息能索引到 Half 消息进行后续的回查操作 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-OP消息.png) + + + +**** + + + +##### 补偿机制 + +如果在 RocketMQ 事务消息的二阶段过程中失败了,例如在做 Commit 操作时,出现网络问题导致 Commit 失败,那么需要通过一定的策略使这条消息最终被 Commit,RocketMQ采用了一种补偿机制,称为回查 + +Broker 端通过对比 Half 消息和 Op 消息,对未确定状态的消息发起回查并且推进CheckPoint(记录那些事务消息的状态是确定的),将消息发送到对应的 Producer 端(同一个 Group 的 Producer),由 Producer 根据消息来检查本地事务的状态,然后执行提交或回滚 + +注意:RocketMQ 并不会无休止的进行事务状态回查,默认回查 15 次,如果 15 次回查还是无法得知事务状态,则默认回滚该消息 + + + + + +**** + + + +#### 基本使用 + +##### 使用方式 事务消息共有三种状态,提交状态、回滚状态、中间状态: @@ -4379,11 +4489,11 @@ public class Consumer { -**** +*** -#### 代码实例 +##### 代码实现 使用 **TransactionMQProducer** 类创建事务性生产者,并指定唯一的 `ProducerGroup`,就可以设置自定义线程池来处理这些检查请求,执行本地事务后、需要根据执行结果对消息队列进行回复 @@ -4419,7 +4529,7 @@ public class Producer { 消费者代码和前面的实例相同的 -实现事务的监听接口,当发送半消息成功时 +实现事务的监听接口,当发送半消息成功时: * `executeLocalTransaction` 方法来执行本地事务,返回三个事务状态之一 * `checkLocalTransaction` 方法检查本地事务状态,响应消息队列的检查请求,返回三个事务状态之一 @@ -4461,12 +4571,6 @@ public class TransactionListenerImpl implements TransactionListener { - - - - - - **** @@ -4475,10 +4579,7 @@ public class TransactionListenerImpl implements TransactionListener { ## 高级特性 -待补充笔记: -* https://github.com/apache/rocketmq/blob/master/docs/cn/design.md#3-%E6%B6%88%E6%81%AF%E8%BF%87%E6%BB%A4 -* https://github.com/apache/rocketmq/blob/master/docs/cn/design.md#5-%E4%BA%8B%E5%8A%A1%E6%B6%88%E6%81%AF From 0ab4708b5dd076fbafe35d18588380b329c54027 Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 11 Nov 2021 01:30:36 +0800 Subject: [PATCH 072/168] Update Java Notes --- Frame.md | 292 ++++++++++++++++++++++++++----------------------------- Java.md | 4 +- Prog.md | 6 +- 3 files changed, 142 insertions(+), 160 deletions(-) diff --git a/Frame.md b/Frame.md index 0c18f72..fb9ae14 100644 --- a/Frame.md +++ b/Frame.md @@ -3444,7 +3444,9 @@ RCVBUF_ALLOCATOR:属于 SocketChannal 参数 # RocketMQ -## 消息队列 +## 基本介绍 + +### 消息队列 消息队列是一种先进先出的数据结构,常见的应用场景: @@ -3484,10 +3486,6 @@ RCVBUF_ALLOCATOR:属于 SocketChannal 参数 - - -## 概念模型 - ### 安装测试 安装需要 Java 环境,下载解压后进入安装目录,进行启动: @@ -3547,107 +3545,28 @@ RCVBUF_ALLOCATOR:属于 SocketChannal 参数 -**** - - - -### 基本概念 - -#### 服务相关 - -RocketMQ 主要由 Producer、Broker、Consumer 三部分组成,其中 Producer 负责生产消息,Consumer 负责消费消息,Broker 负责存储消息,NameServer 负责管理 Broker - -* 代理服务器(Broker Server):消息中转角色,负责**存储消息、转发消息**。在 RocketMQ 系统中负责接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备,也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等 - -* 名字服务(Name Server):充当**路由消息**的提供者。生产者或消费者能够通过名字服务查找各主题相应的 Broker IP 列表 - -* 消息生产者(Producer):负责**生产消息**,把业务应用系统里产生的消息发送到 Broker 服务器。RocketMQ 提供多种发送方式,同步发送、异步发送、顺序发送、单向发送,同步和异步方式均需要 Broker 返回确认信息,单向发送不需要;可以通过 MQ 的负载均衡模块选择相应的 Broker 集群队列进行消息投递,投递的过程支持快速失败并且低延迟 -* 消息消费者(Consumer):负责**消费消息**,一般是后台系统负责异步消费,一个消息消费者会从 Broker 服务器拉取消息、并将其提供给应用程序。从用户应用的角度而提供了两种消费形式: - * 拉取式消费(Pull Consumer):应用通主动调用 Consumer 的拉消息方法从 Broker 服务器拉消息,主动权由应用控制,一旦获取了批量消息,应用就会启动消费过程 - * 推动式消费(Push Consumer):该模式下 Broker 收到数据后会主动推送给消费端,实时性较高 - -* 生产者组(Producer Group):同一类 Producer 的集合,都发送同一类消息且发送逻辑一致。如果发送的是事务消息且原始生产者在发送之后崩溃,**则 Broker 服务器会联系同一生产者组的其他生产者实例以提交或回溯消费** - -* 消费者组(Consumer Group):同一类 Consumer 的集合,消费者实例必须订阅完全相同的 Topic,消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面更容易的实现负载均衡和容错。RocketMQ 支持两种消息模式: - * 集群消费(Clustering):相同 Consumer Group 的每个 Consumer 实例平均分摊消息 - * 广播消费(Broadcasting):相同 Consumer Group 的每个 Consumer 实例都接收全量的消息 - - - -*** - - - -#### 消息相关 - -每个 Broker 可以存储多个 Topic 的消息,每个 Topic 的消息也可以分片存储于不同的 Broker,Message Queue(消息队列)是用于存储消息的物理地址,每个 Topic 中的消息地址存储于多个 Message Queue 中 - -* 主题(Topic):表示一类消息的集合,每个主题包含若干条消息,每条消息只属于一个主题,是 RocketMQ 消息订阅的基本单位 - -* 消息(Message):消息系统所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题。RocketMQ 中每个消息拥有唯一的 Message ID,且可以携带具有业务标识的 Key,系统提供了通过 Message ID 和 Key 查询消息的功能 - -* 标签(Tag):为消息设置的标志,用于同一主题下区分不同类型的消息。标签能够有效地保持代码的清晰度和连贯性,并优化 RocketMQ 提供的查询系统,消费者可以根据 Tag 实现对不同子主题的不同消费逻辑,实现更好的扩展性 - -* 普通顺序消息(Normal Ordered Message):消费者通过同一个消息队列(Topic 分区)收到的消息是有顺序的,不同消息队列收到的消息则可能是无顺序的 - -* 严格顺序消息(Strictly Ordered Message):消费者收到的所有消息均是有顺序的 - - - -官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/concept.md(基础知识部分的笔记参考官方文档编写) - - - - - -*** - - - - - -### 集群设计 - -#### 集群模式 - -常用的以下几种模式: - -* 单 Master 模式:这种方式风险较大,一旦 Broker 重启或者宕机,会导致整个服务不可用 - -* 多 Master 模式:一个集群无 Slave,全是 Master - - - 优点:配置简单,单个 Master 宕机或重启维护对应用无影响,在磁盘配置为 RAID10 时,即使机器宕机不可恢复情况下,由于 RAID10 磁盘非常可靠,消息也不会丢(异步刷盘丢失少量消息,同步刷盘一条不丢),性能最高 - - - 缺点:单台机器宕机期间,这台机器上未被消费的消息在机器恢复之前不可订阅,消息实时性会受到影响 - -* 多 Master 多 Slave 模式(同步):每个 Master 配置一个 Slave,有多对 Master-Slave,HA 采用同步双写方式,即只有主备都写成功,才向应用返回成功 - - * 优点:数据与服务都无单点故障,Master 宕机情况下,消息无延迟,服务可用性与数据可用性都非常高 - * 缺点:性能比异步复制略低(大约低 10% 左右),发送单个消息的 RT 略高,目前不能实现主节点宕机,备机自动切换为主机 - -* 多 Master 多 Slave 模式(异步):HA 采用异步复制的方式,会造成主备有短暂的消息延迟(毫秒级别) - - - 优点:即使磁盘损坏,消息丢失的非常少,且消息实时性不会受影响,同时 Master 宕机后,消费者仍然可以从 Slave 消费,而且此过程对应用透明,不需要人工干预,性能同多 Master 模式几乎一样 - - - 缺点:Master 宕机,磁盘损坏情况下会丢失少量消息 - - - - *** -#### 系统架构 +### 工作流程 -NameServer 是一个简单的 Topic 路由注册中心,支持 Broker 的动态注册与发现。NameServer 通常是集群的方式部署,各实例间相互不进行信息通讯。Broker 向每一台 NameServer 注册自己的路由信息,所以每个 NameServer 实例上面**都保存一份完整的路由信息**。当某个 NameServer 因某种原因下线了,Broker 仍可以向其它 NameServer 同步其路由信息 +NameServer 是一个简单的 Topic 路由注册中心,支持 Broker 的动态注册与发现,生产者或消费者能够通过名字服务查找各主题相应的 Broker IP 列表 NameServer 主要包括两个功能: * Broker 管理,NameServer 接受 Broker 集群的注册信息并保存下来作为路由信息的基本数据,提供**心跳检测**检查 Broker 活性 * 路由信息管理,每个 NameServer 将保存关于 Broker 集群的整个路由信息和用于客户端查询的队列信息,然后 Producer 和 Conumser 通过 NameServer 就可以知道整个 Broker 集群的路由信息,从而进行消息的投递和消费 -BrokerServer 主要负责消息的存储、投递和查询以及服务高可用保证,为了实现这些功能,Broker 包含了以下几个重要子模块: +NameServer 特点: + +* NameServer 通常是集群的方式部署,各实例间相互不进行信息通讯 +* Broker 向每一台 NameServer 注册自己的路由信息,所以每个 NameServer 实例上面**都保存一份完整的路由信息** +* 当某个 NameServer 因某种原因下线了,Broker 仍可以向其它 NameServer 同步其路由信息 + +BrokerServer 主要负责消息的存储、投递和查询以及服务高可用保证,在 RocketMQ 系统中负责接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备,也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等 + +Broker 包含了以下几个重要子模块: * Remoting Module:整个 Broker 的实体,负责处理来自 clients 端的请求 @@ -3667,35 +3586,36 @@ BrokerServer 主要负责消息的存储、投递和查询以及服务高可用 -#### 集群架构 +### 相关概念 -RocketMQ 网络部署特点: +RocketMQ 主要由 Producer、Broker、Consumer 三部分组成,其中 Producer 负责生产消息,Consumer 负责消费消息,Broker 负责存储消息,NameServer 负责管理 Broker -- NameServer 是一个几乎**无状态节点**,节点之间相互独立,无任何信息同步 +* 消息生产者(Producer):负责**生产消息**,把业务应用系统里产生的消息发送到 Broker 服务器。RocketMQ 提供多种发送方式,同步发送、异步发送、顺序发送、单向发送,同步和异步方式均需要 Broker 返回确认信息,单向发送不需要;可以通过 MQ 的负载均衡模块选择相应的 Broker 集群队列进行消息投递,投递的过程支持快速失败并且低延迟 +* 消息消费者(Consumer):负责**消费消息**,一般是后台系统负责异步消费,一个消息消费者会从 Broker 服务器拉取消息、并将其提供给应用程序。从用户应用的角度而提供了两种消费形式: + * 拉取式消费(Pull Consumer):应用通主动调用 Consumer 的拉消息方法从 Broker 服务器拉消息,主动权由应用控制,一旦获取了批量消息,应用就会启动消费过程 + * 推动式消费(Push Consumer):该模式下 Broker 收到数据后会主动推送给消费端,实时性较高 -- Broker 部署相对复杂,Broker 分为 Master 与 Slave,Master 可以部署多个,一个 Master 可以对应多个 Slave,但是一个 Slave 只能对应一个 Master,Master 与 Slave 的对应关系通过指定相同 BrokerName、不同 BrokerId 来定义,BrokerId 为 0 是 Master,非 0 表示 Slave。**每个 Broker 与 NameServer 集群中的所有节点建立长连接**,定时注册 Topic 信息到所有 NameServer +* 生产者组(Producer Group):同一类 Producer 的集合,都发送同一类消息且发送逻辑一致。如果发送的是事务消息且原始生产者在发送之后崩溃,**则 Broker 服务器会联系同一生产者组的其他生产者实例以提交或回溯消费** - 注意:部署架构上也支持一 Master 多 Slave,但只有 BrokerId=1 的从服务器才会参与消息的读负载(读写分离) +* 消费者组(Consumer Group):同一类 Consumer 的集合,消费者实例必须订阅完全相同的 Topic,消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面更容易的实现负载均衡和容错。RocketMQ 支持两种消息模式: + * 集群消费(Clustering):相同 Consumer Group 的每个 Consumer 实例平均分摊消息 + * 广播消费(Broadcasting):相同 Consumer Group 的每个 Consumer 实例都接收全量的消息 -- Producer 与 NameServer 集群中的其中**一个节点(随机选择)建立长连接**,定期从 NameServer 获取 Topic 路由信息,并向提供 Topic 服务的 Master 建立长连接,且定时向 Master **发送心跳**。Producer 完全无状态,可集群部署 +每个 Broker 可以存储多个 Topic 的消息,每个 Topic 的消息也可以分片存储于不同的 Broker,Message Queue(消息队列)是用于存储消息的物理地址,每个 Topic 中的消息地址存储于多个 Message Queue 中 -- Consumer 与 NameServer 集群中的其中一个节点(随机选择)建立长连接,定期从 NameServer 获取 Topic 路由信息,并向提供 Topic 服务的 Master、Slave 建立长连接,且定时向 Master、Slave 发送心跳 +* 主题(Topic):表示一类消息的集合,每个主题包含若干条消息,每条消息只属于一个主题,是 RocketMQ 消息订阅的基本单位 - Consumer 既可以从 Master 订阅消息,也可以从 Slave 订阅消息,在向 Master 拉取消息时,Master 服务器会根据拉取偏移量与最大偏移量的距离(判断是否读老消息,产生读 I/O),以及从服务器是否可读等因素建议下一次是从 Master 还是 Slave 拉取 +* 消息(Message):消息系统所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题。RocketMQ 中每个消息拥有唯一的 Message ID,且可以携带具有业务标识的 Key,系统提供了通过 Message ID 和 Key 查询消息的功能 -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-集群架构.png) +* 标签(Tag):为消息设置的标志,用于同一主题下区分不同类型的消息。标签能够有效地保持代码的清晰度和连贯性,并优化 RocketMQ 提供的查询系统,消费者可以根据 Tag 实现对不同子主题的不同消费逻辑,实现更好的扩展性 -集群工作流程: +* 普通顺序消息(Normal Ordered Message):消费者通过同一个消息队列(Topic 分区)收到的消息是有顺序的,不同消息队列收到的消息则可能是无顺序的 -- 启动 NameServer 监听端口,等待 Broker、Producer、Consumer 连上来,相当于一个路由控制中心 -- Broker 启动,跟所有的 NameServer 保持长连接,定时发送心跳包。心跳包中包含当前 Broker 信息(IP、端口等)以及存储所有 Topic 信息。注册成功后,NameServer 集群中就有 Topic 跟 Broker 的映射关系 -- 收发消息前,先创建 Topic,创建 Topic 时需要指定该 Topic 要存储在哪些 Broker 上,也可以在发送消息时自动创建 Topic -- Producer 发送消息,启动时先跟 NameServer 集群中的其中一台建立长连接,并从 NameServer 中获取当前发送的 Topic 存在哪些 Broker 上,轮询从队列列表中选择一个队列,然后与队列所在的 Broker 建立长连接从而向 Broker 发消息。 -- Consumer 跟 Producer 类似,跟其中一台 NameServer 建立长连接,获取当前订阅 Topic 存在哪些 Broker 上,然后直接跟 Broker 建立连接通道,开始消费消息 +* 严格顺序消息(Strictly Ordered Message):消费者收到的所有消息均是有顺序的 -官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/architecture.md +官方文档:https://github.com/apache/rocketmq/tree/master/docs/cn(基础知识部分的笔记参考官方文档编写) @@ -3707,11 +3627,13 @@ RocketMQ 网络部署特点: -## 基本操作 +## 消息操作 ### 基本样例 -#### 工作流程 +#### 订阅发布 + +消息的发布是指某个生产者向某个 Topic 发送消息,消息的订阅是指某个消费者关注了某个 Topic 中带有某些 Tag 的消息,进而从该 Topic 消费数据 导入 MQ 客户端依赖 @@ -4420,11 +4342,11 @@ RocketMQ 支持分布式事务消息,采用了 2PC 的思想来实现了提交 RocketMQ 会开启一个定时任务,从 Topic 为 RMQ_SYS_TRANS_HALF_TOPIC 中拉取消息进行消费,根据生产者组获取一个服务提供者发送回查事务状态请求,根据事务状态来决定是提交或回滚消息 -在 RocketMQ 中,每条消息都会有对应的索引信息,Consumer 通过 ConsumeQueue 这个二级索引来读取消息实体内容,如图: +在 RocketMQ 中,每条消息都会有对应的索引信息,Consumer 通过 ConsumeQueue(在 Broker 端)这个类似二级索引的结构来读取消息实体内容 ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-事务工作流程.png) -RocketMQ 的具体实现策略:如果写入的是事务消息,对消息的 Topic 和 Queue 等属性进行替换,同时将原来的 Topic 和 Queue 信息存储到消息的属性中,因为消息主题被替换,消息并不会转发到该原主题的消息消费队列,消费者无法感知消息的存在,不会消费 +RocketMQ 的具体实现策略:如果写入的是事务消息,对消息的 Topic 和 Queue 等属性进行替换,同时将原来的 Topic 和 Queue 信息存储到**消息的属性**中,因为消息的主题被替换,所以消息不会转发到该原主题的消息消费队列,消费者无法感知消息的存在,不会消费 @@ -4436,13 +4358,13 @@ RocketMQ 的具体实现策略:如果写入的是事务消息,对消息的 T 一阶段写入不可见的消息后,二阶段操作: -* 如果执行 Commit 操作,则需要让消息对用户可见,构建出 Half 消息的索引。一阶段的 Half 消息写到一个特殊的 Topic,所以构建索引时需要读取出 Half 消息,并将 Topic 和 Queue 替换成真正的目标的 Topic 和 Queue,然后通过一次普通消息的写入操作来生成一条对用户可见的消息 +* 如果执行 Commit 操作,则需要让消息对用户可见,构建出 Half 消息的索引。一阶段的 Half 消息写到一个特殊的 Topic,构建索引时需要读取出 Half 消息,然后通过一次普通消息的写入操作将 Topic 和 Queue 替换成真正的目标 Topic 和 Queue,生成一条对用户可见的消息。其实就是利用了一阶段存储的消息的内容,在二阶段时恢复出一条完整的普通消息,然后走一遍消息写入流程 -* 如果是 Rollback 则需要撤销一阶段的消息,因为消息本就不可见,所以并不需要真正撤销消息(实际上 RocketMQ 也无法去删除一条消息,因为是顺序写文件的),为了区分这条消息没有确定的状态(Pending 状态),RocketMQ 用 Op 消息标识事务消息已经确定的状态(Commit 或者 Rollback) +* 如果是 Rollback 则需要撤销一阶段的消息,因为消息本就不可见,所以并不需要真正撤销消息(实际上 RocketMQ 也无法去删除一条消息,因为是顺序写文件的)。RocketMQ 为了区分这条消息没有确定状态的消息(Pending 状态),采用 Op 消息标识已经确定状态的事务消息(Commit 或者 Rollback) 事务消息无论是 Commit 或者 Rollback 都会记录一个 Op 操作,两者的区别是 Commit 相对于 Rollback 在写入 Op 消息前创建 Half 消息的索引。如果一条事务消息没有对应的 Op 消息,说明这个事务的状态还无法确定(可能是二阶段失败了) -RocketMQ 将 Op 消息写入到全局一个特定的 Topic 中,通过源码中的方法 `TransactionalMessageUtil.buildOpTopic()`,这个主题是一个内部的 Topic(像 Half 消息的 Topic 一样),不会被用户消费。Op 消息的内容为对应的 Half 消息的存储的 Offset,这样通过 Op 消息能索引到 Half 消息进行后续的回查操作 +RocketMQ 将 Op 消息写入到全局一个特定的 Topic 中,通过源码中的方法 `TransactionalMessageUtil.buildOpTopic()`,这个主题是一个内部的 Topic(像 Half 消息的 Topic 一样),不会被用户消费。Op 消息的内容为对应的 Half 消息的存储的 Offset,这样**通过 Op 消息能索引到 Half 消息**进行后续的回查操作 ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-OP消息.png) @@ -4454,9 +4376,9 @@ RocketMQ 将 Op 消息写入到全局一个特定的 Topic 中,通过源码中 ##### 补偿机制 -如果在 RocketMQ 事务消息的二阶段过程中失败了,例如在做 Commit 操作时,出现网络问题导致 Commit 失败,那么需要通过一定的策略使这条消息最终被 Commit,RocketMQ采用了一种补偿机制,称为回查 +如果在 RocketMQ 事务消息的二阶段过程中失败了,例如在做 Commit 操作时,出现网络问题导致 Commit 失败,那么需要通过一定的策略使这条消息最终被 Commit,RocketMQ 采用了一种补偿机制,称为回查 -Broker 端通过对比 Half 消息和 Op 消息,对未确定状态的消息发起回查并且推进CheckPoint(记录那些事务消息的状态是确定的),将消息发送到对应的 Producer 端(同一个 Group 的 Producer),由 Producer 根据消息来检查本地事务的状态,然后执行提交或回滚 +Broker 端通过对比 Half 消息和 Op 消息,对未确定状态的消息发起回查并且推进 CheckPoint(记录哪些事务消息的状态是确定的),将消息发送到对应的 Producer 端(同一个 Group 的 Producer),由 Producer 根据消息来检查本地事务的状态,然后执行提交或回滚 注意:RocketMQ 并不会无休止的进行事务状态回查,默认回查 15 次,如果 15 次回查还是无法得知事务状态,则默认回滚该消息 @@ -4495,40 +4417,6 @@ Broker 端通过对比 Half 消息和 Op 消息,对未确定状态的消息发 ##### 代码实现 -使用 **TransactionMQProducer** 类创建事务性生产者,并指定唯一的 `ProducerGroup`,就可以设置自定义线程池来处理这些检查请求,执行本地事务后、需要根据执行结果对消息队列进行回复 - -```java -public class Producer { - public static void main(String[] args) throws MQClientException, InterruptedException { - // 创建事务监听器 - TransactionListener transactionListener = new TransactionListenerImpl(); - // 创建消息生产者 - TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name"); - ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS); - producer.setExecutorService(executorService); - // 生产者的监听器 - producer.setTransactionListener(transactionListener); - producer.start(); - String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"}; - for (int i = 0; i < 10; i++) { - try { - Message msg = new Message("TransactionTopic", tags[i % tags.length], "KEY" + i, - ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)); - SendResult sendResult = producer.sendMessageInTransaction(msg, null); - System.out.printf("%s%n", sendResult); - Thread.sleep(10); - } catch (MQClientException | UnsupportedEncodingException e) { - e.printStackTrace(); - } - } - //Thread.sleep(1000000); - //producer.shutdown();暂时不关闭 - } -} -``` - -消费者代码和前面的实例相同的 - 实现事务的监听接口,当发送半消息成功时: * `executeLocalTransaction` 方法来执行本地事务,返回三个事务状态之一 @@ -4567,6 +4455,43 @@ public class TransactionListenerImpl implements TransactionListener { } ``` +使用 **TransactionMQProducer** 类创建事务性生产者,并指定唯一的 `ProducerGroup`,就可以设置自定义线程池来处理这些检查请求,执行本地事务后、需要根据执行结果对消息队列进行回复 + +```java +public class Producer { + public static void main(String[] args) throws MQClientException, InterruptedException { + // 创建消息生产者 + TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name"); + ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS); + producer.setExecutorService(executorService); + + // 创建事务监听器 + TransactionListener transactionListener = new TransactionListenerImpl(); + // 生产者的监听器 + producer.setTransactionListener(transactionListener); + // 启动生产者 + producer.start(); + String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"}; + for (int i = 0; i < 10; i++) { + try { + Message msg = new Message("TransactionTopic", tags[i % tags.length], "KEY" + i, + ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)); + // 发送消息 + SendResult sendResult = producer.sendMessageInTransaction(msg, null); + System.out.printf("%s%n", sendResult); + Thread.sleep(10); + } catch (MQClientException | UnsupportedEncodingException e) { + e.printStackTrace(); + } + } + //Thread.sleep(1000000); + //producer.shutdown();暂时不关闭 + } +} +``` + +消费者代码和前面的实例相同的 + @@ -4577,7 +4502,64 @@ public class TransactionListenerImpl implements TransactionListener { -## 高级特性 +## 系统机制 + +### 集群设计 + +#### 集群模式 + +常用的以下几种模式: + +* 单 Master 模式:这种方式风险较大,一旦 Broker 重启或者宕机,会导致整个服务不可用 +* 多 Master 模式:一个集群无 Slave,全是 Master + + - 优点:配置简单,单个 Master 宕机或重启维护对应用无影响,在磁盘配置为 RAID10 时,即使机器宕机不可恢复情况下,由于 RAID10 磁盘非常可靠,消息也不会丢(异步刷盘丢失少量消息,同步刷盘一条不丢),性能最高 + + - 缺点:单台机器宕机期间,这台机器上未被消费的消息在机器恢复之前不可订阅,消息实时性会受到影响 +* 多 Master 多 Slave 模式(同步):每个 Master 配置一个 Slave,有多对 Master-Slave,HA 采用同步双写方式,即只有主备都写成功,才向应用返回成功 + + * 优点:数据与服务都无单点故障,Master 宕机情况下,消息无延迟,服务可用性与数据可用性都非常高 + * 缺点:性能比异步复制略低(大约低 10% 左右),发送单个消息的 RT 略高,目前不能实现主节点宕机,备机自动切换为主机 +* 多 Master 多 Slave 模式(异步):HA 采用异步复制的方式,会造成主备有短暂的消息延迟(毫秒级别) + + - 优点:即使磁盘损坏,消息丢失的非常少,且消息实时性不会受影响,同时 Master 宕机后,消费者仍然可以从 Slave 消费,而且此过程对应用透明,不需要人工干预,性能同多 Master 模式几乎一样 + - 缺点:Master 宕机,磁盘损坏情况下会丢失少量消息 + + + +*** + + + +#### 集群架构 + +RocketMQ 网络部署特点: + +- NameServer 是一个几乎**无状态节点**,节点之间相互独立,无任何信息同步 + +- Broker 部署相对复杂,Broker 分为 Master 与 Slave,Master 可以部署多个,一个 Master 可以对应多个 Slave,但是一个 Slave 只能对应一个 Master,Master 与 Slave 的对应关系通过指定相同 BrokerName、不同 BrokerId 来定义,BrokerId 为 0 是 Master,非 0 表示 Slave。**每个 Broker 与 NameServer 集群中的所有节点建立长连接**,定时注册 Topic 信息到所有 NameServer + + 注意:部署架构上也支持一 Master 多 Slave,但只有 BrokerId=1 的从服务器才会参与消息的读负载(读写分离) + +- Producer 与 NameServer 集群中的其中**一个节点(随机选择)建立长连接**,定期从 NameServer 获取 Topic 路由信息,并向提供 Topic 服务的 Master 建立长连接,且定时向 Master **发送心跳**。Producer 完全无状态,可集群部署 + +- Consumer 与 NameServer 集群中的其中一个节点(随机选择)建立长连接,定期从 NameServer 获取 Topic 路由信息,并向提供 Topic 服务的 Master、Slave 建立长连接,且定时向 Master、Slave 发送心跳 + + Consumer 既可以从 Master 订阅消息,也可以从 Slave 订阅消息,在向 Master 拉取消息时,Master 服务器会根据拉取偏移量与最大偏移量的距离(判断是否读老消息,产生读 I/O),以及从服务器是否可读等因素建议下一次是从 Master 还是 Slave 拉取 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-集群架构.png) + +集群工作流程: + +- 启动 NameServer 监听端口,等待 Broker、Producer、Consumer 连上来,相当于一个路由控制中心 +- Broker 启动,跟所有的 NameServer 保持长连接,定时发送心跳包。心跳包中包含当前 Broker 信息(IP、端口等)以及存储所有 Topic 信息。注册成功后,NameServer 集群中就有 Topic 跟 Broker 的映射关系 +- 收发消息前,先创建 Topic,创建 Topic 时需要指定该 Topic 要存储在哪些 Broker 上,也可以在发送消息时自动创建 Topic +- Producer 发送消息,启动时先跟 NameServer 集群中的其中一台建立长连接,并从 NameServer 中获取当前发送的 Topic 存在哪些 Broker 上,轮询从队列列表中选择一个队列,然后与队列所在的 Broker 建立长连接从而向 Broker 发消息。 +- Consumer 跟 Producer 类似,跟其中一台 NameServer 建立长连接,获取当前订阅 Topic 存在哪些 Broker 上,然后直接跟 Broker 建立连接通道,开始消费消息 + + + +官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/architecture.md diff --git a/Java.md b/Java.md index ae24482..389fc78 100644 --- a/Java.md +++ b/Java.md @@ -9677,7 +9677,7 @@ JVM 结构: -JVM、JRE、JDK对比: +JVM、JRE、JDK 对比: @@ -12568,7 +12568,7 @@ Java 语言:跨平台的语言(write once ,run anywhere) 编译过程中的编译器: -* 前端编译器: Sun 的全量式编译器 javac、 Eclipse 的增量式编译器 ECJ,把源代码编译为字节码文件.class +* 前端编译器: Sun 的全量式编译器 javac、 Eclipse 的增量式编译器 ECJ,把源代码编译为字节码文件 .class * IntelliJ IDEA 使用 javac 编译器 * Eclipse 中,当开发人员编写完代码后保存时,ECJ 编译器就会把未编译部分的源码逐行进行编译,而非每次都全量编译,因此 ECJ 的编译效率会比 javac 更加迅速和高效 diff --git a/Prog.md b/Prog.md index 6e602cb..e0ff4ea 100644 --- a/Prog.md +++ b/Prog.md @@ -14691,7 +14691,7 @@ MappedByteBuffer,可以让文件在直接内存(堆外内存)中进行修 * **用在进程间的通信,能达到共享内存页的作用**,但在高并发下要对文件内存进行加锁,防止出现读写内容混乱和不一致性,Java 提供了文件锁 FileLock,但在父/子进程中锁定后另一进程会一直等待,效率不高 * 读写那些太大而不能放进内存中的文件,分段映射 -MappedByteBuffer 较之 ByteBuffer新增的三个方法 +MappedByteBuffer 较之 ByteBuffer 新增的三个方法: - `final MappedByteBuffer force()`:缓冲区是 READ_WRITE 模式下,对缓冲区内容的修改强行写入文件 - `final MappedByteBuffer load()`:将缓冲区的内容载入物理内存,并返回该缓冲区的引用 @@ -14752,13 +14752,13 @@ public class MappedByteBufferTest { * 通道可以实现异步读写数据 * 通道可以从缓冲读数据,也可以写数据到缓冲 -2. BIO 中的 stream 是单向的,NIO中的 Channel 是双向的,可以读操作,也可以写操作 +2. BIO 中的 Stream 是单向的,NIO中的 Channel 是双向的,可以读操作,也可以写操作 3. Channel 在 NIO 中是一个接口:`public interface Channel extends Closeable{}` Channel 实现类: -* FileChannel:用于读取、写入、映射和操作文件的通道,只能工作在阻塞模式下 +* FileChannel:用于读取、写入、映射和操作文件的通道,**只能工作在阻塞模式下** * 通过 FileInputStream 获取的 Channel 只能读 * 通过 FileOutputStream 获取的 Channel 只能写 * 通过 RandomAccessFile 是否能读写根据构造 RandomAccessFile 时的读写模式决定 From 85b0df459548f0e06e0ea00c0c1c361dcd04156d Mon Sep 17 00:00:00 2001 From: Seazean Date: Fri, 12 Nov 2021 01:11:46 +0800 Subject: [PATCH 073/168] Update Java Notes --- DB.md | 3 +- Frame.md | 206 +++++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 163 insertions(+), 46 deletions(-) diff --git a/DB.md b/DB.md index 579e96a..60ca2e1 100644 --- a/DB.md +++ b/DB.md @@ -576,8 +576,7 @@ SELECT * FROM t WHERE id = 1; 优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。 * 根据搜索条件找出所有可能的使用的索引 -* 计算全表扫描的代价 -* 计算使用不同索引执行 SQL 的的代价 +* 成本分析,执行成本由 I/O 成本和 CPU 成本组成,计算全表扫描和使用不同索引执行 SQL 的代价 * 找到一个最优的执行方案,用最小的代价去执行语句 在数据库里面,扫描行数是影响执行代价的因素之一,扫描的行数越少意味着访问磁盘的次数越少,消耗的 CPU 资源越少,优化器还会结合是否使用临时表、是否排序等因素进行综合判断 diff --git a/Frame.md b/Frame.md index fb9ae14..7152a16 100644 --- a/Frame.md +++ b/Frame.md @@ -3549,54 +3549,17 @@ RCVBUF_ALLOCATOR:属于 SocketChannal 参数 -### 工作流程 - -NameServer 是一个简单的 Topic 路由注册中心,支持 Broker 的动态注册与发现,生产者或消费者能够通过名字服务查找各主题相应的 Broker IP 列表 - -NameServer 主要包括两个功能: - -* Broker 管理,NameServer 接受 Broker 集群的注册信息并保存下来作为路由信息的基本数据,提供**心跳检测**检查 Broker 活性 -* 路由信息管理,每个 NameServer 将保存关于 Broker 集群的整个路由信息和用于客户端查询的队列信息,然后 Producer 和 Conumser 通过 NameServer 就可以知道整个 Broker 集群的路由信息,从而进行消息的投递和消费 - -NameServer 特点: - -* NameServer 通常是集群的方式部署,各实例间相互不进行信息通讯 -* Broker 向每一台 NameServer 注册自己的路由信息,所以每个 NameServer 实例上面**都保存一份完整的路由信息** -* 当某个 NameServer 因某种原因下线了,Broker 仍可以向其它 NameServer 同步其路由信息 - -BrokerServer 主要负责消息的存储、投递和查询以及服务高可用保证,在 RocketMQ 系统中负责接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备,也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等 - -Broker 包含了以下几个重要子模块: - -* Remoting Module:整个 Broker 的实体,负责处理来自 clients 端的请求 - -* Client Manager:负责管理客户端(Producer/Consumer)和维护 Consumer 的 Topic 订阅信息 - -* Store Service:提供方便简单的 API 接口处理消息存储到物理硬盘和查询功能 - -* HA Service:高可用服务,提供 Master Broker 和 Slave Broker 之间的数据同步功能 - -* Index Service:根据特定的 Message key 对投递到 Broker 的消息进行索引服务,以提供消息的快速查询 - -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-Broker工作流程.png) - - - -*** - - - ### 相关概念 RocketMQ 主要由 Producer、Broker、Consumer 三部分组成,其中 Producer 负责生产消息,Consumer 负责消费消息,Broker 负责存储消息,NameServer 负责管理 Broker +* 代理服务器(Broker Server):消息中转角色,负责**存储消息、转发消息**。在 RocketMQ 系统中负责接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备,也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等 +* 名字服务(Name Server):充当**路由消息**的提供者。生产者或消费者能够通过名字服务查找各主题相应的 Broker IP 列表 * 消息生产者(Producer):负责**生产消息**,把业务应用系统里产生的消息发送到 Broker 服务器。RocketMQ 提供多种发送方式,同步发送、异步发送、顺序发送、单向发送,同步和异步方式均需要 Broker 返回确认信息,单向发送不需要;可以通过 MQ 的负载均衡模块选择相应的 Broker 集群队列进行消息投递,投递的过程支持快速失败并且低延迟 * 消息消费者(Consumer):负责**消费消息**,一般是后台系统负责异步消费,一个消息消费者会从 Broker 服务器拉取消息、并将其提供给应用程序。从用户应用的角度而提供了两种消费形式: * 拉取式消费(Pull Consumer):应用通主动调用 Consumer 的拉消息方法从 Broker 服务器拉消息,主动权由应用控制,一旦获取了批量消息,应用就会启动消费过程 * 推动式消费(Push Consumer):该模式下 Broker 收到数据后会主动推送给消费端,实时性较高 - * 生产者组(Producer Group):同一类 Producer 的集合,都发送同一类消息且发送逻辑一致。如果发送的是事务消息且原始生产者在发送之后崩溃,**则 Broker 服务器会联系同一生产者组的其他生产者实例以提交或回溯消费** - * 消费者组(Consumer Group):同一类 Consumer 的集合,消费者实例必须订阅完全相同的 Topic,消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面更容易的实现负载均衡和容错。RocketMQ 支持两种消息模式: * 集群消费(Clustering):相同 Consumer Group 的每个 Consumer 实例平均分摊消息 * 广播消费(Broadcasting):相同 Consumer Group 的每个 Consumer 实例都接收全量的消息 @@ -4225,7 +4188,9 @@ consumer.subscribe("TOPIC", "TAGA || TAGB || TAGC"); #### 原理解析 -RocketMQ 分布式消息队列的消息过滤方式是在 Consumer 端订阅消息时再做消息过滤的。因为 RocketMQ 在 Producer 端写入消息和在 Consumer 端订阅消息采用**分离存储**的机制实现,Consumer 端订阅消息是需要通过 ConsumeQueue 这个消息消费的逻辑队列拿到一个索引,然后再从 CommitLog 里面读取真正的消息实体内容,所以绕不开其存储结构。 +RocketMQ 分布式消息队列的消息过滤方式是在 Consumer 端订阅消息时再做消息过滤的,所以是在 Broker 端实现的,优点是减少了对于 Consumer 无用消息的网络传输,缺点是增加了 Broker 的负担、而且实现相对复杂 + +RocketMQ 在 Producer 端写入消息和在 Consumer 端订阅消息采用**分离存储**的机制实现,Consumer 端订阅消息是需要通过 ConsumeQueue 这个消息消费的逻辑队列拿到一个索引,然后再从 CommitLog 里面读取真正的消息实体内容 ConsumeQueue 的存储结构如下,有 8 个字节存储的 Message Tag 的哈希值,基于 Tag 的消息过滤就是基于这个字段 @@ -4342,10 +4307,6 @@ RocketMQ 支持分布式事务消息,采用了 2PC 的思想来实现了提交 RocketMQ 会开启一个定时任务,从 Topic 为 RMQ_SYS_TRANS_HALF_TOPIC 中拉取消息进行消费,根据生产者组获取一个服务提供者发送回查事务状态请求,根据事务状态来决定是提交或回滚消息 -在 RocketMQ 中,每条消息都会有对应的索引信息,Consumer 通过 ConsumeQueue(在 Broker 端)这个类似二级索引的结构来读取消息实体内容 - -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-事务工作流程.png) - RocketMQ 的具体实现策略:如果写入的是事务消息,对消息的 Topic 和 Queue 等属性进行替换,同时将原来的 Topic 和 Queue 信息存储到**消息的属性**中,因为消息的主题被替换,所以消息不会转发到该原主题的消息消费队列,消费者无法感知消息的存在,不会消费 @@ -4504,6 +4465,163 @@ public class Producer { ## 系统机制 +### 消息存储 + +#### 工作流程 + +分布式队列因为有高可靠性的要求,所以数据要进行持久化存储 + +1. 消息生产者发送消息 +2. MQ 收到消息,将消息进行持久化,在存储中新增一条记录 +3. 返回 ACK 给生产者 +4. MQ push 消息给对应的消费者,然后等待消费者返回 ACK +5. 如果消息消费者在指定时间内成功返回 ACK,那么 MQ 认为消息消费成功,在存储中删除消息;如果 MQ 在指定时间内没有收到 ACK,则认为消息消费失败,会尝试重新 push 消息,重复执行 4、5、6 步骤 +6. MQ删除消息 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息存取.png) + + + + + +*** + + + +#### 存储结构 + +RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成 的,消息真正的物理存储文件是 CommitLog,ConsumeQueue 是消息的逻辑队列,类似数据库的索引节点,存储的是指向物理存储的地址。每个 Topic 下的每个 Message Queue 都有一个对应的 ConsumeQueue 文件 + +每条消息都会有对应的索引信息,Consumer 通过 ConsumeQueue(在 Broker 端)这个结构来读取消息实体内容 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息存储结构.png) + +* CommitLog:消息主体以及元数据的存储主体,存储 Producer 端写入的消息内容,消息内容不是定长的。消息主要是顺序写入日志文件,单个文件大小默认1G,偏移量代表下一次写入的位置,当文件写满了就继续写入下一个文件 +* ConsumerQueue:消息消费队列,存储消息在 CommitLog 的索引。RocketMQ 消息消费时要遍历 CommitLog 文件,并根据主题 Topic 检索消息,这是非常低效的。引入 ConsumeQueue 作为消费消息的索引,保存了指定 Topic 下的队列消息在 CommitLog 中的起始物理偏移量 offset,消息大小 size 和消息 Tag 的 HashCode 值,每个 ConsumeQueue 文件大小约 5.72M +* IndexFile:为了消息查询提供了一种通过 Key 或时间区间来查询消息的方法,通过 IndexFile 来查找消息的方法不影响发送与消费消息的主流程。IndexFile 的底层存储为在文件系统中实现的 HashMap 结构,故 RocketMQ 的索引文件其底层实现为 hash 索引 + +RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所有的队列共用一个日志数据文件(CommitLog)来存储。混合型存储结构(多个 Topic 的消息实体内容都存储于一个 CommitLog 中)**针对 Producer 和 Consumer 分别采用了数据和索引部分相分离的存储结构**,Producer 发送消息至 Broker 端,然后 Broker 端使用同步或者异步的方式对消息刷盘持久化,保存至 CommitLog 中。只要消息被持久化至磁盘文件 CommitLog 中,Producer 发送的消息就不会丢失,Consumer 也就肯定有机会去消费这条消息 + +服务端支持长轮询模式,当消费者无法拉取到消息后,可以等下一次消息拉取,Broker 允许等待 30s 的时间,只要这段时间内有新消息到达,将直接返回给消费端。RocketMQ 的具体做法是,使用 Broker 端的后台服务线程 ReputMessageService 不停地分发请求并异步构建 ConsumeQueue(逻辑消费队列)和 IndexFile(索引文件)数据 + + + + + +**** + + + +#### 存储优化 + +##### 存储媒介 + +两种持久化的方案: + +* 关系型数据库 DB:IO 读写性能比较差,如果 DB 出现故障,则 MQ 的消息就无法落盘存储导致线上故障,可靠性不高 + +* 文件系统:消息刷盘至所部署虚拟机/物理机的文件系统来做持久化,分为异步刷盘和同步刷盘两种模式。消息刷盘为消息存储提供了一种高效率、高可靠性和高性能的数据持久化方式,除非部署 MQ 机器本身或是本地磁盘挂了,一般不会出现无法持久化的问题 + + 注意:磁盘的顺序读写要比随机读写快很多,可以匹配上网络的速度,RocketMQ 的消息采用的顺序写 + +页缓存(PageCache)是 OS 对文件的缓存,用于加速对文件的读写。程序对文件进行顺序读写的速度几乎接近于内存的读写速度,就是因为 OS 将一部分的内存用作 PageCache,对读写访问操作进行了性能优化, + +* 对于数据的写入,OS 会先写入至 Cache 内,随后通过异步的方式由 pdflush 内核线程将 Cache 内的数据刷盘至物理磁盘上 +* 对于数据的读取,如果一次读取文件时出现未命中 PageCache 的情况,OS 从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行预读取(局部性原理) + +在 RocketMQ 中,ConsumeQueue 逻辑消费队列存储的数据较少,并且是顺序读取,在 PageCache 机制的预读取作用下,Consume Queue 文件的读性能几乎接近读内存,即使在有消息堆积情况下也不会影响性能。CommitLog 消息存储的日志数据文件读取内容时会产生较多的随机访问读取,严重影响性能。选择合适的系统 IO 调度算法和固态硬盘,比如设置调度算法为 Deadline,随机读的性能也会有所提升 + + + +*** + + + +##### 内存映射 + +操作系统分为用户态和内核态,文件操作、网络操作需要涉及这两种形态的切换,需要进行数据复制。一台服务器把本机磁盘文件的内容发送到客户端,分为两个步骤: + +* read:读取本地文件内容 + +* write:将读取的内容通过网络发送出去 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-文件与网络操作.png) + +补充:Prog → NET → I/O → 零拷贝部分的笔记详解相关内容 + +通过使用 mmap 的方式,可以省去向用户态的内存复制,RocketMQ 充分利用零拷贝技术,提高消息存盘和网络发送的速度。 + +RocketMQ 主要通过 MappedByteBuffer 对文件进行读写操作,利用了 NIO 中的 FileChannel 模型将磁盘上的物理文件直接映射到用户态的内存地址中,将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率 + +MappedByteBuffer 内存映射的方式限制一次只能映射 1.5~2G 的文件至用户态的虚拟内存,所以 RocketMQ 默认设置单个 CommitLog 日志数据文件为 1G。RocketMQ 的文件存储使用定长结构来存储,方便一次将整个文件映射至内存 + + + +*** + + + +#### 刷盘机制 + +同步刷盘:只有在消息真正持久化至磁盘后 RocketMQ 的 Broker 端才会真正返回给 Producer 端一个成功的 ACK 响应,保障 MQ消息的可靠性,但是性能上会有较大影响,一般适用于金融业务应用该模式较多 + +异步刷盘:利用 OS 的 PageCache 的优势,只要消息写入内存 PageCache 即可将成功的 ACK 返回给 Producer 端,降低了读写延迟,提高了 MQ 的性能和吞吐量。消息刷盘采用**后台异步线程**提交的方式进行,当内存里的消息量积累到一定程度时,触发写磁盘动作 + +通过 Broker 配置文件里的 flushDiskType 参数设置采用什么方式,可以配置成 SYNC_FLUSH、ASYNC_FLUSH 中的一个 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-刷盘机制.png) + + + +官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md + + + + + +**** + + + +### 通信机制 + +NameServer 是一个简单的 Topic 路由注册中心,支持 Broker 的动态注册与发现,生产者或消费者能够通过名字服务查找各主题相应的 Broker IP 列表 + +NameServer 主要包括两个功能: + +* Broker 管理,NameServer 接受 Broker 集群的注册信息并保存下来作为路由信息的基本数据,提供**心跳检测**检查 Broker 活性 +* 路由信息管理,每个 NameServer 将保存关于 Broker 集群的整个路由信息和用于客户端查询的队列信息,然后 Producer 和 Conumser 通过 NameServer 就可以知道整个 Broker 集群的路由信息,从而进行消息的投递和消费 + +NameServer 特点: + +* NameServer 通常是集群的方式部署,各实例间相互不进行信息通讯 +* Broker 向每一台 NameServer 注册自己的路由信息,所以每个 NameServer 实例上面**都保存一份完整的路由信息** +* 当某个 NameServer 因某种原因下线了,Broker 仍可以向其它 NameServer 同步其路由信息 + +BrokerServer 主要负责消息的存储、投递和查询以及服务高可用保证,在 RocketMQ 系统中负责接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备,也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等 + +Broker 包含了以下几个重要子模块: + +* Remoting Module:整个 Broker 的实体,负责处理来自 clients 端的请求 + +* Client Manager:负责管理客户端(Producer/Consumer)和维护 Consumer 的 Topic 订阅信息 + +* Store Service:提供方便简单的 API 接口处理消息存储到物理硬盘和查询功能 + +* HA Service:高可用服务,提供 Master Broker 和 Slave Broker 之间的数据同步功能 + +* Index Service:根据特定的 Message key 对投递到 Broker 的消息进行索引服务,以提供消息的快速查询 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-Broker工作流程.png) + + + + + +*** + + + ### 集群设计 #### 集群模式 From e59dcdb9bc3cb19901695dabc3df2dd1628d99ab Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 13 Nov 2021 00:03:17 +0800 Subject: [PATCH 074/168] Update Java Notes --- DB.md | 34 ++++-- Frame.md | 327 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- Java.md | 9 +- 3 files changed, 352 insertions(+), 18 deletions(-) diff --git a/DB.md b/DB.md index 60ca2e1..cd7f505 100644 --- a/DB.md +++ b/DB.md @@ -571,7 +571,7 @@ SELECT * FROM t WHERE id = 1; #### 优化器 -##### 扫描行数 +##### 成本分析 优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。 @@ -581,16 +581,34 @@ SELECT * FROM t WHERE id = 1; 在数据库里面,扫描行数是影响执行代价的因素之一,扫描的行数越少意味着访问磁盘的次数越少,消耗的 CPU 资源越少,优化器还会结合是否使用临时表、是否排序等因素进行综合判断 + + +*** + + + +##### 统计数据 + +MySQL 中保存着两种统计数据: + +* innodb_table_stats 存储了表的统计数据,每一条记录对应着一个表的统计数据 +* innodb_index_stats 存储了索引的统计数据,每一条记录对应着一个索引的一个统计项的数据 + MySQL 在真正执行语句之前,并不能精确地知道满足条件的记录有多少条,只能根据统计信息来估算记录,统计信息就是索引的区分度,一个索引上不同的值的个数(比如性别只能是男女,就是 2 ),称之为基数(cardinality),基数越大说明区分度越好 -* 通过**采样统计**来获取基数,InnoDB 默认会选择 N 个数据页,统计这些页面上的不同值得到一个平均值,然后乘以这个索引的页面数,就得到了这个索引的基数 -* 数据表是会持续更新的,索引统计信息也不会固定不变,当变更的数据行数超过 1/ M 的时候,会自动触发重新做一次索引统计 +通过**采样统计**来获取基数,InnoDB 默认会选择 N 个数据页,统计这些页面上的不同值得到一个平均值,然后乘以这个索引的页面数,就得到了这个索引的基数 + +在 MySQL 中,有两种存储统计数据的方式,可以通过设置参数 `innodb_stats_persistent` 的值来选择: + +* ON:表示统计信息会持久化存储(默认),采样页数 N 默认为 20,可以通过 `innodb_stats_persistent_sample_pages `指定,页数越多统计的数据越准确,但消耗的资源更大 +* 设置为 off 时,表示统计信息只存储在内存,采样页数 N 默认为 8,也可以通过系统变量设置(不推荐,每次重新计算浪费资源) + +数据表是会持续更新的,两种更新方式: -* 在 MySQL 中,有两种存储索引统计的方式,可以通过设置参数 innodb_stats_persistent 的值来选择: - * 设置为 on 时,表示统计信息会持久化存储,这时默认的 N 是 20,M 是 10 - * 设置为 off 时,表示统计信息只存储在内存,这时默认的 N 是 8,M 是 16 +* 设置 `innodb_stats_auto_recalc` 为 1,当发生变动的记录数量超过表大小的 10% 时,自动触发重新计算,不过是**异步进行** +* 调用 `ANALYZE TABLE t` 手动更新统计信息,只对信息做重新统计(不是重建表),没有修改数据,这个过程中加了 MDL 读锁并且是同步进行,所以会暂时阻塞系统 -EXPLAIN 执行计划在优化器阶段生成,如果发现 explain 的结果预估的 rows 值跟实际情况差距比较大,可以执行 `analyze table t ` 重新修正信息,只是对表的索引信息做重新统计(不是重建表),没有修改数据,这个过程中加了 MDL 读锁 +EXPLAIN 执行计划在优化器阶段生成,如果 explain 的结果预估的 rows 值跟实际情况差距比较大,可以执行 analyze 命令重新修正信息 @@ -600,7 +618,7 @@ EXPLAIN 执行计划在优化器阶段生成,如果发现 explain 的结果预 ##### 错选索引 -扫描行数本身是估算数据,或者 SQL 语句中的字段选择有问题时,可能导致 MySQL 没有选择正确的执行索引 +采样统计本身是估算数据,或者 SQL 语句中的字段选择有问题时,可能导致 MySQL 没有选择正确的执行索引 解决方法: diff --git a/Frame.md b/Frame.md index 7152a16..d1f54c3 100644 --- a/Frame.md +++ b/Frame.md @@ -3975,7 +3975,9 @@ public class ConsumerInOrder { ### 延时消息 -提交了一个订单就可以发送一个延时消息,1h 后去检查这个订单的状态,如果还是未付款就取消订单释放库存 +#### 原理解析 + +定时消息(延迟队列)是指消息发送到 Broker 后,不会立即被消费,等待特定时间投递给真正的 Topic RocketMQ 并不支持任意时间的延时,需要设置几个固定的延时等级,从 1s 到 2h 分别对应着等级 1 到 18,消息消费失败会进入延时消息队列,消息发送时间与设置的延时等级和重试次数有关,详见代码 `SendMessageProcessor.java` @@ -3983,6 +3985,28 @@ RocketMQ 并不支持任意时间的延时,需要设置几个固定的延时 private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h"; ``` +Broker 可以配置 messageDelayLevel,该属性是 Broker 的属性,不属于某个 Topic + +发消息时,可以设置延迟等级 `msg.setDelayLevel(level)`,level 有以下三种情况: + +- level == 0:消息为非延迟消息 +- 1<=level<=maxLevel:消息延迟特定时间,例如 level==1,延迟 1s +- level > maxLevel:则 level== maxLevel,例如 level==20,延迟 2h + +定时消息会暂存在名为 SCHEDULE_TOPIC_XXXX 的 Topic 中,并根据 delayTimeLevel 存入特定的 queue,队列的标识`queueId = delayTimeLevel – 1`,即一个 queue 只存相同延迟的消息,保证具有相同发送延迟的消息能够顺序消费。Broker 会调度地消费 SCHEDULE_TOPIC_XXXX,将消息写入真实的 Topic + +注意:定时消息在第一次写入和调度写入真实 Topic 时都会计数,因此发送数量、tps 都会变高。 + + + +*** + + + +#### 代码实现 + +提交了一个订单就可以发送一个延时消息,1h 后去检查这个订单的状态,如果还是未付款就取消订单释放库存 + ```java public class ScheduledMessageProducer { public static void main(String[] args) throws Exception { @@ -4467,7 +4491,11 @@ public class Producer { ### 消息存储 -#### 工作流程 +#### 生产消费 + +At least Once:至少一次,指每个消息必须投递一次,Consumer 先 Pull 消息到本地,消费完成后才向服务器返回 ACK,如果没有消费一定不会 ACK 消息 + +回溯消费:指 Consumer 已经消费成功的消息,由于业务上需求需要重新消费,Broker 在向 Consumer 投递成功消息后,消息仍然需要保留。并且重新消费一般是按照时间维度,例如由于 Consumer 系统故障,恢复后需要重新消费 1 小时前的数据,RocketMQ 支持按照时间回溯消费,时间维度精确到毫秒 分布式队列因为有高可靠性的要求,所以数据要进行持久化存储 @@ -4476,7 +4504,7 @@ public class Producer { 3. 返回 ACK 给生产者 4. MQ push 消息给对应的消费者,然后等待消费者返回 ACK 5. 如果消息消费者在指定时间内成功返回 ACK,那么 MQ 认为消息消费成功,在存储中删除消息;如果 MQ 在指定时间内没有收到 ACK,则认为消息消费失败,会尝试重新 push 消息,重复执行 4、5、6 步骤 -6. MQ删除消息 +6. MQ 删除消息 ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息存取.png) @@ -4506,8 +4534,6 @@ RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所 - - **** @@ -4681,7 +4707,21 @@ RocketMQ 网络部署特点: +**** + + + +#### 高可用 + +在 Consumer 的配置文件中,并不需要设置是从 Master 读还是从 Slave 读,当 Master 不可用或者繁忙的时候,Consumer 会被自动切换到从 Slave 读。有了自动切换的机制,当一个 Master 机器出现故障后,Consumer 仍然可以从 Slave 读取消息,不影响 Consumer 程序,达到了消费端的高可用性 + +在创建 Topic 的时候,把 Topic 的多个 Message Queue 创建在多个 Broker 组上(相同 Broker 名称,不同 brokerId 的机器组成一个 Broker 组),当一个 Broker 组的 Master 不可用后,其他组的 Master 仍然可用,Producer 仍然可以发送消息。 +RocketMQ 目前还不支持把 Slave 自动转成 Master,需要手动停止 Slave 角色的 Broker,更改配置文件,用新的配置文件启动 Broker + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-高可用.png) + +5)、6)属于单点故障,且无法恢复,一旦发生,在此单点上的消息全部丢失。RocketMQ在这两种情况下,通过异步复制,可保证99%的消息不丢,但是仍然会有极少量的消息可能丢失。通过同步双写技术可以完全避免单点,同步双写势必会影响性能,适合对消息可靠性要求极高的场合,例如与Money相关的应用。注:RocketMQ从3.0版本开始支持同步双写。 @@ -4689,6 +4729,283 @@ RocketMQ 网络部署特点: +#### 主从复制 + +如果一个 Broker 组有 Master 和 Slave,消息需要从 Master 复制到 Slave 上,有同步和异步两种复制方式: + +* 同步复制方式:Master 和 Slave 均写成功后才反馈给客户端写成功状态。在同步复制方式下,如果 Master 出故障, Slave 上有全部的备份数据,容易恢复,但是同步复制会增大数据写入延迟,降低系统吞吐量 + +* 异步复制方式:只要 Master 写成功,即可反馈给客户端写成功状态,系统拥有较低的延迟和较高的吞吐量,但是如果 Master 出了故障,有些数据因为没有被写入 Slave,有可能会丢失 + +同步复制和异步复制是通过 Broker 配置文件里的 brokerRole 参数进行设置的,可以设置成 ASYNC_MASTE、RSYNC_MASTER、SLAVE 三个值中的一个 + +一般把刷盘机制配置成 ASYNC_FLUSH,主从复制为 SYNC_MASTER,这样即使有一台机器出故障,仍然能保证数据不丢 + +RocketMQ 支持消息的高可靠,影响消息可靠性的几种情况: + +1. Broker 非正常关闭 +2. Broker 异常 Crash +3. OS Crash +4. 机器掉电,但是能立即恢复供电情况 +5. 机器无法开机(可能是 CPU、主板、内存等关键设备损坏) +6. 磁盘设备损坏 + +前四种情况都属于硬件资源可立即恢复情况,RocketMQ 在这四种情况下能保证消息不丢,或者丢失少量数据(依赖刷盘方式) + +后两种属于单点故障,且无法恢复,一旦发生,在此单点上的消息全部丢失。RocketMQ 在这两种情况下,通过主从异步复制,可保证 99% 的消息不丢,但是仍然会有极少量的消息可能丢失。通过**同步双写技术**可以完全避免单点,但是会影响性能,适合对消息可靠性要求极高的场合,RocketMQ 从 3.0 版本开始支持同步双写 + + + +**** + + + +### 负载均衡 + +#### 生产端 + +Producer 端,每个实例在发消息的时候,默认会轮询所有的 Message Queue 发送,以让消息平均落在不同的 queue 上。而由于 queue可以散落在不同的 Broker,所以消息就发送到不同的 Broker 下 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-producer负载均衡.png) + +图中箭头线条上的标号代表顺序,发布方会把第一条消息发送至 Queue 0,然后第二条消息发送至 Queue 1,以此类推 + + + +*** + + + +#### 消费端 + +广播模式下要求一条消息需要投递到一个消费组下面所有的消费者实例,所以不存在负载均衡,在实现上,Consumer 分配 queue 时,所有 Consumer 都分到所有的queue。 + +在集群消费模式下,每条消息只需要投递到订阅这个 Topic 的 Consumer Group 下的一个实例即可,RocketMQ 采用主动拉取的方式拉取并消费消息,在拉取的时候需要明确指定拉取哪一条 Message Queue + +而每当实例的数量有变更,都会触发一次所有实例的负载均衡,这时候会按照 queue 的数量和实例的数量平均分配 queue 给每个实例。默认的分配算法是 AllocateMessageQueueAveragely: + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-consumer负载均衡1.png) + + 还有一种平均的算法是 AllocateMessageQueueAveragelyByCircle,以环状轮流均分 queue 的形式: + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-consumer负载均衡2.png) + +集群模式下,queue 都是只允许分配只一个实例,如果多个实例同时消费一个 queue 的消息,由于拉取哪些消息是 Consumer 主动控制的,会导致同一个消息在不同的实例下被消费多次 + +通过增加 Consumer 实例去分摊 queue 的消费,可以起到水平扩展的消费能力的作用。而当有实例下线时,会重新触发负载均衡,这时候原来分配到的 queue 将分配到其他实例上继续消费 + +但是如果 Consumer 实例的数量比 Message Queue 的总数量还多的话,多出来的 Consumer 实例将无法分到 queue,也就无法消费到消息,也就无法起到分摊负载的作用了,所以需要控制让 queue 的总数量大于等于 Consumer 的数量 + + + +**** + + + +### 消息重试 + +todo:以下还需要修改,明日完成 + +顺序消息的重试 + +对于顺序消息,当消费者消费消息失败后,消息队列 RocketMQ 会自动不断进行消息重试(每次间隔时间为 1 秒),这时,应用会出现消息消费被阻塞的情况。因此,在使用顺序消息时,务必保证应用能够及时监控并处理消费失败的情况,避免阻塞现象的发生。 + +1.4.2 无序消息的重试 + +对于无序消息(普通、定时、延时、事务消息),当消费者消费消息失败时,您可以通过设置返回状态达到消息重试的结果。 + +无序消息的重试只针对集群消费方式生效;广播方式不提供失败重试特性,即消费失败后,失败消息不再重试,继续消费新的消息。 + +1)重试次数 + +消息队列 RocketMQ 默认允许每条消息最多重试 16 次,每次重试的间隔时间如下: + +| 第几次重试 | 与上次重试的间隔时间 | 第几次重试 | 与上次重试的间隔时间 | +| :--------: | :------------------: | :--------: | :------------------: | +| 1 | 10 秒 | 9 | 7 分钟 | +| 2 | 30 秒 | 10 | 8 分钟 | +| 3 | 1 分钟 | 11 | 9 分钟 | +| 4 | 2 分钟 | 12 | 10 分钟 | +| 5 | 3 分钟 | 13 | 20 分钟 | +| 6 | 4 分钟 | 14 | 30 分钟 | +| 7 | 5 分钟 | 15 | 1 小时 | +| 8 | 6 分钟 | 16 | 2 小时 | + +如果消息重试 16 次后仍然失败,消息将不再投递。如果严格按照上述重试时间间隔计算,某条消息在一直消费失败的前提下,将会在接下来的 4 小时 46 分钟之内进行 16 次重试,超过这个时间范围消息将不再重试投递。 + +**注意:** 一条消息无论重试多少次,这些重试消息的 Message ID 不会改变。 + +2)配置方式 + +**消费失败后,重试配置方式** + +集群消费方式下,消息消费失败后期望消息重试,需要在消息监听器接口的实现中明确进行配置(三种方式任选一种): + +- 返回 Action.ReconsumeLater (推荐) +- 返回 Null +- 抛出异常 + +```java +public class MessageListenerImpl implements MessageListener { + @Override + public Action consume(Message message, ConsumeContext context) { + //处理消息 + doConsumeMessage(message); + //方式1:返回 Action.ReconsumeLater,消息将重试 + return Action.ReconsumeLater; + //方式2:返回 null,消息将重试 + return null; + //方式3:直接抛出异常, 消息将重试 + throw new RuntimeException("Consumer Message exceotion"); + } +} +``` + +**消费失败后,不重试配置方式** + +集群消费方式下,消息失败后期望消息不重试,需要捕获消费逻辑中可能抛出的异常,最终返回 Action.CommitMessage,此后这条消息将不会再重试。 + +```java +public class MessageListenerImpl implements MessageListener { + @Override + public Action consume(Message message, ConsumeContext context) { + try { + doConsumeMessage(message); + } catch (Throwable e) { + //捕获消费逻辑中的所有异常,并返回 Action.CommitMessage; + return Action.CommitMessage; + } + //消息处理正常,直接返回 Action.CommitMessage; + return Action.CommitMessage; + } +} +``` + +**自定义消息最大重试次数** + +消息队列 RocketMQ 允许 Consumer 启动的时候设置最大重试次数,重试时间间隔将按照如下策略: + +- 最大重试次数小于等于 16 次,则重试时间间隔同上表描述。 +- 最大重试次数大于 16 次,超过 16 次的重试时间间隔均为每次 2 小时。 + +```java +Properties properties = new Properties(); +//配置对应 Group ID 的最大消息重试次数为 20 次 +properties.put(PropertyKeyConst.MaxReconsumeTimes,"20"); +Consumer consumer =ONSFactory.createConsumer(properties); +``` + +> 注意: + +- 消息最大重试次数的设置对相同 Group ID 下的所有 Consumer 实例有效。 +- 如果只对相同 Group ID 下两个 Consumer 实例中的其中一个设置了 MaxReconsumeTimes,那么该配置对两个 Consumer 实例均生效。 +- 配置采用覆盖的方式生效,即最后启动的 Consumer 实例会覆盖之前的启动实例的配置 + +**获取消息重试次数** + +消费者收到消息后,可按照如下方式获取消息的重试次数: + +```java +public class MessageListenerImpl implements MessageListener { + @Override + public Action consume(Message message, ConsumeContext context) { + //获取消息的重试次数 + System.out.println(message.getReconsumeTimes()); + return Action.CommitMessage; + } +} +``` + + + + + +*** + + + +### 死信队列 + +当一条消息初次消费失败,消息队列 RocketMQ 会自动进行消息重试;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列 RocketMQ 不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。 + +在消息队列 RocketMQ 中,这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message),存储死信消息的特殊队列称为死信队列(Dead-Letter Queue)。 + +死信消息具有以下特性 + +- 不会再被消费者正常消费。 +- 有效期与正常消息相同,均为 3 天,3 天后会被自动删除。因此,请在死信消息产生后的 3 天内及时处理。 + +死信队列具有以下特性: + +- 一个死信队列对应一个 Group ID, 而不是对应单个消费者实例。 +- 如果一个 Group ID 未产生死信消息,消息队列 RocketMQ 不会为其创建相应的死信队列。 +- 一个死信队列包含了对应 Group ID 产生的所有死信消息,不论该消息属于哪个 Topic。 + +一条消息进入死信队列,意味着某些因素导致消费者无法正常消费该消息,因此,通常需要您对其进行特殊处理。排查可疑因素并解决问题后,可以在消息队列 RocketMQ 控制台重新发送该消息,让消费者重新消费一次。 + + + +**** + + + +### 幂等消费 + +消息队列 RocketMQ 消费者在接收到消息以后,有必要根据业务上的唯一 Key 对消息做幂等处理的必要性。 + +1.6.1 消费幂等的必要性 + +在互联网应用中,尤其在网络不稳定的情况下,消息队列 RocketMQ 的消息有可能会出现重复,这个重复简单可以概括为以下情况: + +- 发送时消息重复 + + 当一条消息已被成功发送到服务端并完成持久化,此时出现了网络闪断或者客户端宕机,导致服务端对客户端应答失败。 如果此时生产者意识到消息发送失败并尝试再次发送消息,消费者后续会收到两条内容相同并且 Message ID 也相同的消息。 + +- 投递时消息重复 + + 消息消费的场景下,消息已投递到消费者并完成业务处理,当客户端给服务端反馈应答的时候网络闪断。 为了保证消息至少被消费一次,消息队列 RocketMQ 的服务端将在网络恢复后再次尝试投递之前已被处理过的消息,消费者后续会收到两条内容相同并且 Message ID 也相同的消息。 + +- 负载均衡时消息重复(包括但不限于网络抖动、Broker 重启以及订阅方应用重启) + + 当消息队列 RocketMQ 的 Broker 或客户端重启、扩容或缩容时,会触发 Rebalance,此时消费者可能会收到重复消息。 + +1.6.2 处理方式 + +因为 Message ID 有可能出现冲突(重复)的情况,所以真正安全的幂等处理,不建议以 Message ID 作为处理依据。 最好的方式是以业务唯一标识作为幂等处理的关键依据,而业务的唯一标识可以通过消息 Key 进行设置: + +```java +Message message = new Message(); +message.setKey("ORDERID_100"); +SendResult sendResult = producer.send(message); +``` + +订阅方收到消息时可以根据消息的 Key 进行幂等处理: + +```java +consumer.subscribe("ons_test", "*", new MessageListener() { + public Action consume(Message message, ConsumeContext context) { + String key = message.getKey() + // 根据业务唯一标识的 key 做幂等处理 + } +}); +``` + + + + + + + + + + + + + +*** + + + ## 源码分析 diff --git a/Java.md b/Java.md index 389fc78..e918610 100644 --- a/Java.md +++ b/Java.md @@ -350,15 +350,14 @@ public static void main(String[] args) { * 有了基本数据类型,为什么还要引用数据类型? - > 1、引用数据类型封装了数据和处理该数据的方法,比如 Integer.parseInt(String) 就是将 String 字符类型数据转换为 Integer 整型数据 + > 引用数据类型封装了数据和处理该数据的方法,比如 Integer.parseInt(String) 就是将 String 字符类型数据转换为 Integer 整型 > - > 2、Java 中大部分类和方法都是针对引用数据类型,包括泛型和集合 + > Java 中大部分类和方法都是针对引用数据类型,包括泛型和集合 * 引用数据类型那么好,为什么还用基本数据类型? - > 引用类型的对象要多储存对象头,对基本数据类型来说空间浪费率太高 - > 逻辑上来讲,java 只有包装类就够了,为了运行速度,需要用到基本数据类型;优先考虑运行效率的问题,所以二者同时存在是合乎情理的 - + > 引用类型的对象要多储存对象头,对基本数据类型来说空间浪费率太高。逻辑上来讲,Java 只有包装类就够了,为了运行速度,需要用到基本数据类型;优先考虑运行效率的问题,所以二者同时存在是合乎情理的 + * Java 集合不能存放基本数据类型,只存放对象的引用? > 不能放基本数据类型是因为不是 Object 的子类。泛型思想,如果不用泛型要写很多参数类型不同的但功能相同的函数(方法重载) From 95d59a122cc5ad1ae210899c7d36fa579806dfdc Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 14 Nov 2021 20:16:14 +0800 Subject: [PATCH 075/168] Update Java Notes --- Frame.md | 422 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 314 insertions(+), 108 deletions(-) diff --git a/Frame.md b/Frame.md index d1f54c3..3536a79 100644 --- a/Frame.md +++ b/Frame.md @@ -1468,7 +1468,7 @@ Netty 的功能特性: #### 设计思想 -Reactor 模式,通过一个或多个输入同时传递给服务处理器的事件驱动处理模式。 服务端程序处理传入的多路请求,并将它们同步分派给对应的处理线程,Reactor 模式也叫 Dispatcher 模式,即 I/O 多路复用统一监听事件,收到事件后分发(Dispatch 给某线程) +Reactor 模式,通过一个或多个输入同时传递给服务处理器的**事件驱动处理模式**。 服务端程序处理传入的多路请求,并将它们同步分派给对应的处理线程,Reactor 模式也叫 Dispatcher 模式,即 I/O 多路复用统一监听事件,收到事件后分发(Dispatch 给某线程) **I/O 复用结合线程池**,就是 Reactor 模式基本设计思想: @@ -1477,7 +1477,7 @@ Reactor 模式,通过一个或多个输入同时传递给服务处理器的事 Reactor 模式关键组成: - Reactor:在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对 I/O 事件做出反应 -- Handlers:处理程序执行 I/O 事件要完成的实际事件,Reactor 通过调度适当的处理程序来响应 I/O 事件,处理程序执行非阻塞操作 +- Handler:处理程序执行 I/O 要完成的实际事件,Reactor 通过调度适当的处理程序来响应 I/O 事件,处理程序执行**非阻塞操作** Reactor 模式具有如下的优点: @@ -1486,7 +1486,7 @@ Reactor 模式具有如下的优点: - 可扩展性,可以方便的通过增加 Reactor 实例个数来充分利用 CPU 资源 - 可复用性,Reactor 模型本身与具体事件处理逻辑无关,具有很高的复用性 -根据Reactor的数量和处理资源池线程的数量不同,有3种典型的实现: +根据 Reactor 的数量和处理资源池线程的数量不同,有三种典型的实现: - 单 Reactor 单线程 - 单 Reactor 多线程 @@ -1506,6 +1506,8 @@ Reactor 对象通过 select 监控客户端请求事件,收到事件后通过 * 如果不是建立连接事件,则 Reactor 会分发给连接对应的 Handler 来响应,Handler 会完成 read、业务处理、send 的完整流程 + 说明:Handler 和 Acceptor 属于同一个线程 + 模型优点:模型简单,没有多线程、进程通信、竞争的问题,全部都在一个线程中完成 @@ -1533,7 +1535,7 @@ Reactor 对象通过 select 监控客户端请求事件,收到事件后通过 -模型优点:可以充分利用多核CPU的处理能力 +模型优点:可以充分利用多核 CPU 的处理能力 模型缺点: @@ -1575,7 +1577,7 @@ Reactor 对象通过 select 监控客户端请求事件,收到事件后通过 ### Proactor -Reactor 模式中,Reactor 等待某个事件的操作状态发生变化(文件描述符可读写,socket 可读写),然后把事件传递给事先注册的 Handler 来做实际的读写操作,其中的读写操作都需要应用程序同步操作,所以 Reactor 是非阻塞同步网络模型(NIO) +Reactor 模式中,Reactor 等待某个事件的操作状态发生变化(文件描述符可读写,socket 可读写),然后把事件传递给事先注册的 Handler 来做实际的读写操作,其中的读写操作都需要应用程序同步操作,所以 **Reactor 是非阻塞同步网络模型(NIO)** 把 I/O 操作改为异步,交给操作系统来完成就能进一步提升性能,这就是异步网络模型 Proactor(AIO): @@ -1587,7 +1589,7 @@ Reactor 模式中,Reactor 等待某个事件的操作状态发生变化(文 * AsyOptProcessor 处理注册请求,并处理 I/O 操作,完成I/O后通知 Proactor * Proactor 根据不同的事件类型回调不同的 Handler 进行业务处理,最后由 Handler 完成业务处理 -对比 Reactor:Reactor 在事件发生时就通知事先注册的处理器(读写在应用程序线程中处理完成);Proactor是在事件发生时基于异步 I/O 完成读写操作(内核完成),I/O 完成后才回调应用程序的处理器进行业务处理 +对比:Reactor 在事件发生时就通知事先注册的处理器(读写在应用程序线程中处理完成);Proactor 是在事件发生时基于异步 I/O 完成读写操作(内核完成),I/O 完成后才回调应用程序的处理器进行业务处理 模式优点:异步 I/O 更加充分发挥 DMA(Direct Memory Access 直接内存存取)的优势 @@ -3559,7 +3561,7 @@ RocketMQ 主要由 Producer、Broker、Consumer 三部分组成,其中 Produce * 消息消费者(Consumer):负责**消费消息**,一般是后台系统负责异步消费,一个消息消费者会从 Broker 服务器拉取消息、并将其提供给应用程序。从用户应用的角度而提供了两种消费形式: * 拉取式消费(Pull Consumer):应用通主动调用 Consumer 的拉消息方法从 Broker 服务器拉消息,主动权由应用控制,一旦获取了批量消息,应用就会启动消费过程 * 推动式消费(Push Consumer):该模式下 Broker 收到数据后会主动推送给消费端,实时性较高 -* 生产者组(Producer Group):同一类 Producer 的集合,都发送同一类消息且发送逻辑一致。如果发送的是事务消息且原始生产者在发送之后崩溃,**则 Broker 服务器会联系同一生产者组的其他生产者实例以提交或回溯消费** +* 生产者组(Producer Group):同一类 Producer 的集合,发送同一类消息且发送逻辑一致。如果发送的是事务消息且原始生产者在发送之后崩溃,**则 Broker 服务器会联系同一生产者组的其他生产者实例以提交或回溯消费** * 消费者组(Consumer Group):同一类 Consumer 的集合,消费者实例必须订阅完全相同的 Topic,消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面更容易的实现负载均衡和容错。RocketMQ 支持两种消息模式: * 集群消费(Clustering):相同 Consumer Group 的每个 Consumer 实例平均分摊消息 * 广播消费(Broadcasting):相同 Consumer Group 的每个 Consumer 实例都接收全量的消息 @@ -4487,7 +4489,136 @@ public class Producer { -## 系统机制 +## 系统特性 + +### 工作机制 + +#### 模块介绍 + +NameServer 是一个简单的 Topic 路由注册中心,支持 Broker 的动态注册与发现,生产者或消费者能够通过名字服务查找各主题相应的 Broker IP 列表 + +NameServer 主要包括两个功能: + +* Broker 管理,NameServer 接受 Broker 集群的注册信息并保存下来作为路由信息的基本数据,提供**心跳检测**检查 Broker 活性 +* 路由信息管理,每个 NameServer 将保存关于 Broker 集群的整个路由信息和用于客户端查询的队列信息,然后 Producer 和 Conumser 通过 NameServer 就可以知道整个 Broker 集群的路由信息,从而进行消息的投递和消费 + +NameServer 特点: + +* NameServer 通常是集群的方式部署,各实例间相互不进行信息通讯 +* Broker 向每一台 NameServer 注册自己的路由信息,所以每个 NameServer 实例上面**都保存一份完整的路由信息** +* 当某个 NameServer 因某种原因下线了,Broker 仍可以向其它 NameServer 同步其路由信息 + +BrokerServer 主要负责消息的存储、投递和查询以及服务高可用保证,在 RocketMQ 系统中接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备,也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等 + +Broker 包含了以下几个重要子模块: + +* Remoting Module:整个 Broker 的实体,负责处理来自 clients 端的请求 + +* Client Manager:负责管理客户端(Producer/Consumer)和维护 Consumer 的 Topic 订阅信息 + +* Store Service:提供方便简单的 API 接口处理消息存储到物理硬盘和查询功能 + +* HA Service:高可用服务,提供 Master Broker 和 Slave Broker 之间的数据同步功能 + +* Index Service:根据特定的 Message key 对投递到 Broker 的消息进行索引服务,以提供消息的快速查询 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-Broker工作流程.png) + + + +*** + + + +#### 工作流程 + +RocketMQ 的工作流程: + +- 启动 NameServer 监听端口,等待 Broker、Producer、Consumer 连上来,相当于一个路由控制中心 +- Broker 启动,跟所有的 NameServer 保持长连接,每隔 30s 时间向 NameServer 上报 Topic 路由信息(心跳包)。心跳包中包含当前 Broker 信息(IP、端口等)以及存储所有 Topic 信息。注册成功后,NameServer 集群中就有 Topic 跟 Broker 的映射关系 +- 收发消息前,先创建 Topic,创建 Topic 时需要指定该 Topic 要存储在哪些 Broker 上,也可以在发送消息时自动创建 Topic +- Producer 启动时先跟 NameServer 集群中的**其中一台**建立长连接,并从 NameServer 中获取当前发送的 Topic 存在哪些 Broker 上,同时 Producer 会默认每隔 30s 向 NameServer 拉取一次路由信息 +- Producer 发送消息时,根据消息的 Topic 从本地缓存的 TopicPublishInfoTable 获取路由信息,如果没有则会从 NameServer 上重新拉取并更新,轮询队列列表并选择一个队列 MessageQueue,然后与队列所在的 Broker 建立长连接,向 Broker 发消息 +- Consumer 跟 Producer 类似,跟其中一台 NameServer 建立长连接获取路由信息,根据当前订阅 Topic 存在哪些 Broker 上,直接跟 Broker 建立连接通道,在完成客户端的负载均衡后,选择其中的某一个或者某几个 MessageQueue 来拉取消息并进行消费 + + + +**** + + + +#### 协议设计 + +在 Client 和 Server 之间完成一次消息发送时,需要对发送的消息进行一个协议约定,所以自定义 RocketMQ 的消息协议。为了高效地在网络中传输消息和对收到的消息读取,就需要对消息进行编解码。在 RocketMQ 中,RemotingCommand 这个类在消息传输过程中对所有数据内容的封装,不但包含了所有的数据结构,还包含了编码解码操作 + +| Header字段 | 类型 | Request 说明 | Response 说明 | +| ---------- | ----------------------- | ------------------------------------------------------------ | ------------------------------------------- | +| code | int | 请求操作码,应答方根据不同的请求码进行不同的处理 | 应答响应码,0 表示成功,非 0 则表示各种错误 | +| language | LanguageCode | 请求方实现的语言 | 应答方实现的语言 | +| version | int | 请求方程序的版本 | 应答方程序的版本 | +| opaque | int | 相当于 requestId,在同一个连接上的不同请求标识码,与响应消息中的相对应 | 应答不做修改直接返回 | +| flag | int | 区分是普通 RPC 还是 onewayRPC 的标志 | 区分是普通 RPC 还是 onewayRPC的标志 | +| remark | String | 传输自定义文本信息 | 传输自定义文本信息 | +| extFields | HashMap | 请求自定义扩展信息 | 响应自定义扩展信息 | + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息协议.png) + +传输内容主要可以分为以下四部分: + +* 消息长度:总长度,四个字节存储,占用一个 int 类型 + +* 序列化类型&消息头长度:同样占用一个 int 类型,第一个字节表示序列化类型,后面三个字节表示消息头长度 + +* 消息头数据:经过序列化后的消息头数据 + +* 消息主体数据:消息主体的二进制字节数据内容 + + + +***** + + + +#### 通信原理 + +==todo:后期学习了源码会进行扩充,现在暂时 copy 官方文档== + +在 RocketMQ 消息队列中支持通信的方式主要有同步(sync)、异步(async)、单向(oneway)三种,其中单向通信模式相对简单,一般用在发送心跳包场景下,无需关注其 Response + +RocketMQ 的异步通信流程: + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-异步通信流程.png) + +RocketMQ 的 RPC 通信采用 Netty 组件作为底层通信库,同样也遵循了 Reactor 多线程模型,同时又在这之上做了一些扩展和优化 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-Reactor设计.png) + +RocketMQ 基于 NettyRemotingServer 的 Reactor 多线程模型: + +* 一个 Reactor 主线程(eventLoopGroupBoss)负责监听 TCP 网络连接请求,建立好连接,创建 SocketChannel,并注册到 selector 上。RocketMQ 会自动根据 OS 的类型选择 NIO 和 Epoll,也可以通过参数配置),然后监听真正的网络数据 + +* 拿到网络数据交给 Worker 线程池(eventLoopGroupSelector,默认设置为 3),在真正执行业务逻辑之前需要进行 SSL 验证、编解码、空闲检查、网络连接管理,这些工作交给 defaultEventExecutorGroup(默认设置为 8)去做 +* 处理业务操作放在业务线程池中执行,根据 RomotingCommand 的业务请求码 code 去 processorTable 这个本地缓存变量中找到对应的 processor,封装成 task 任务提交给对应的业务 processor 处理线程池来执行(sendMessageExecutor,以发送消息为例) +* 从入口到业务逻辑的几个步骤中线程池一直再增加,这跟每一步逻辑复杂性相关,越复杂,需要的并发通道越宽 + +| 线程数 | 线程名 | 线程具体说明 | +| ------ | ------------------------------ | ------------------------- | +| 1 | NettyBoss_%d | Reactor 主线程 | +| N | NettyServerEPOLLSelector_%d_%d | Reactor 线程池 | +| M1 | NettyServerCodecThread_%d | Worker 线程池 | +| M2 | RemotingExecutorThread_%d | 业务 processor 处理线程池 | + + + +官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md#2-%E9%80%9A%E4%BF%A1%E6%9C%BA%E5%88%B6 + + + + + +*** + + ### 消息存储 @@ -4518,9 +4649,11 @@ At least Once:至少一次,指每个消息必须投递一次,Consumer 先 #### 存储结构 -RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成 的,消息真正的物理存储文件是 CommitLog,ConsumeQueue 是消息的逻辑队列,类似数据库的索引节点,存储的是指向物理存储的地址。每个 Topic 下的每个 Message Queue 都有一个对应的 ConsumeQueue 文件 +Broker 负责存储消息转发消息,所以以下的结构是存储在 Broker Server 上的 + +RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成 的,消息真正的物理存储文件是 CommitLog,ConsumeQueue 是消息的逻辑队列,类似数据库的索引节点,存储的是指向物理存储的地址。**每个 Topic 下的每个 Message Queue 都有一个对应的 ConsumeQueue 文件** -每条消息都会有对应的索引信息,Consumer 通过 ConsumeQueue(在 Broker 端)这个结构来读取消息实体内容 +每条消息都会有对应的索引信息,Consumer 通过 ConsumeQueue 这个结构来读取消息实体内容 ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息存储结构.png) @@ -4550,7 +4683,7 @@ RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所 注意:磁盘的顺序读写要比随机读写快很多,可以匹配上网络的速度,RocketMQ 的消息采用的顺序写 -页缓存(PageCache)是 OS 对文件的缓存,用于加速对文件的读写。程序对文件进行顺序读写的速度几乎接近于内存的读写速度,就是因为 OS 将一部分的内存用作 PageCache,对读写访问操作进行了性能优化, +页缓存(PageCache)是 OS 对文件的缓存,用于加速对文件的读写。程序对文件进行顺序读写的速度几乎接近于内存的读写速度,就是因为 OS 将一部分的内存用作 PageCache,对读写访问操作进行了性能优化 * 对于数据的写入,OS 会先写入至 Cache 内,随后通过异步的方式由 pdflush 内核线程将 Cache 内的数据刷盘至物理磁盘上 * 对于数据的读取,如果一次读取文件时出现未命中 PageCache 的情况,OS 从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行预读取(局部性原理) @@ -4609,36 +4742,38 @@ MappedByteBuffer 内存映射的方式限制一次只能映射 1.5~2G 的文件 -### 通信机制 +### 消息查询 -NameServer 是一个简单的 Topic 路由注册中心,支持 Broker 的动态注册与发现,生产者或消费者能够通过名字服务查找各主题相应的 Broker IP 列表 +#### Message ID -NameServer 主要包括两个功能: +RocketMQ 支持按照两种维度进行消息查询:按照 Message ID 查询消息、按照 Message Key 查询消息 -* Broker 管理,NameServer 接受 Broker 集群的注册信息并保存下来作为路由信息的基本数据,提供**心跳检测**检查 Broker 活性 -* 路由信息管理,每个 NameServer 将保存关于 Broker 集群的整个路由信息和用于客户端查询的队列信息,然后 Producer 和 Conumser 通过 NameServer 就可以知道整个 Broker 集群的路由信息,从而进行消息的投递和消费 +RocketMQ 中的 MessageId 的长度总共有 16 字节,其中包含了消息存储主机地址(IP 地址和端口),消息 Commit Log offset -NameServer 特点: +实现方式:Client 端从 MessageId 中解析出 Broker 的地址(IP 地址和端口)和 Commit Log 的偏移地址,封装成一个 RPC 请求后通过 Remoting 通信层发送(业务请求码 VIEW_MESSAGE_BY_ID)。Broker 端走的是 QueryMessageProcessor,读取消息的过程用其中的 CommitLog 的 offset 和 size 去 CommitLog 中找到真正的记录并解析成一个完整的消息返回 -* NameServer 通常是集群的方式部署,各实例间相互不进行信息通讯 -* Broker 向每一台 NameServer 注册自己的路由信息,所以每个 NameServer 实例上面**都保存一份完整的路由信息** -* 当某个 NameServer 因某种原因下线了,Broker 仍可以向其它 NameServer 同步其路由信息 -BrokerServer 主要负责消息的存储、投递和查询以及服务高可用保证,在 RocketMQ 系统中负责接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备,也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等 -Broker 包含了以下几个重要子模块: +*** -* Remoting Module:整个 Broker 的实体,负责处理来自 clients 端的请求 -* Client Manager:负责管理客户端(Producer/Consumer)和维护 Consumer 的 Topic 订阅信息 -* Store Service:提供方便简单的 API 接口处理消息存储到物理硬盘和查询功能 +#### Message Key -* HA Service:高可用服务,提供 Master Broker 和 Slave Broker 之间的数据同步功能 +按照 Message Key 查询消息,主要是基于 RocketMQ 的 IndexFile 索引文件来实现的,RocketMQ 的索引文件逻辑结构,类似 JDK 中 HashMap 的实现,具体结构如下: -* Index Service:根据特定的 Message key 对投递到 Broker 的消息进行索引服务,以提供消息的快速查询 +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-IndexFile索引文件.png) -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-Broker工作流程.png) +IndexFile 索引文件为提供了通过 Message Key 查询消息的服务,IndexFile 文件的存储在 `$HOME\store\index${fileName}`,文件名 fileName 是以创建时的时间戳命名,文件大小是固定的,等于 `40+500W*4+2000W*20= 420000040` 个字节大小。如果消息的 properties 中设置了 UNIQ_KEY 这个属性,就用 `topic + “#” + UNIQ_KEY` 作为 key 来做写入操作;如果消息设置了 KEYS 属性(多个 KEY 以空格分隔),也会用 `topic + “#” + KEY` 来做索引 + +整个 Index File 的结构如图,40 Byte 的 Header 用于保存一些总的统计信息,`4*500W` 的 Slot Table 并不保存真正的索引数据,而是保存每个槽位对应的单向链表的头,即一个 Index File 可以保存 2000W 个索引,`20*2000W` 是真正的索引数据 + +索引数据包含了 Key Hash/CommitLog Offset/Timestamp/NextIndex offset 这四个字段,一共 20 Byte + +* NextIndex offset 即前面读出来的 slotValue,如果有 hash 冲突,就可以用这个字段将所有冲突的索引用链表的方式串起来 +* Timestamp 记录的是消息 storeTimestamp 之间的差,并不是一个绝对的时间 + +实现方式:通过 Broker 端的 QueryMessageProcessor 业务处理器来查询,读取消息的过程用 Topic 和 Key 找到 IndexFile 索引文件中的一条记录,根据其中的 CommitLog Offset 从 CommitLog 文件中读取消息的实体内容 @@ -4693,13 +4828,7 @@ RocketMQ 网络部署特点: ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-集群架构.png) -集群工作流程: - -- 启动 NameServer 监听端口,等待 Broker、Producer、Consumer 连上来,相当于一个路由控制中心 -- Broker 启动,跟所有的 NameServer 保持长连接,定时发送心跳包。心跳包中包含当前 Broker 信息(IP、端口等)以及存储所有 Topic 信息。注册成功后,NameServer 集群中就有 Topic 跟 Broker 的映射关系 -- 收发消息前,先创建 Topic,创建 Topic 时需要指定该 Topic 要存储在哪些 Broker 上,也可以在发送消息时自动创建 Topic -- Producer 发送消息,启动时先跟 NameServer 集群中的其中一台建立长连接,并从 NameServer 中获取当前发送的 Topic 存在哪些 Broker 上,轮询从队列列表中选择一个队列,然后与队列所在的 Broker 建立长连接从而向 Broker 发消息。 -- Consumer 跟 Producer 类似,跟其中一台 NameServer 建立长连接,获取当前订阅 Topic 存在哪些 Broker 上,然后直接跟 Broker 建立连接通道,开始消费消息 +集群工作流程:参考通信机制 → 工作流程 @@ -4721,8 +4850,6 @@ RocketMQ 目前还不支持把 Slave 自动转成 Master,需要手动停止 Sl ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-高可用.png) -5)、6)属于单点故障,且无法恢复,一旦发生,在此单点上的消息全部丢失。RocketMQ在这两种情况下,通过异步复制,可保证99%的消息不丢,但是仍然会有极少量的消息可能丢失。通过同步双写技术可以完全避免单点,同步双写势必会影响性能,适合对消息可靠性要求极高的场合,例如与Money相关的应用。注:RocketMQ从3.0版本开始支持同步双写。 - **** @@ -4764,11 +4891,20 @@ RocketMQ 支持消息的高可靠,影响消息可靠性的几种情况: #### 生产端 -Producer 端,每个实例在发消息的时候,默认会轮询所有的 Message Queue 发送,以让消息平均落在不同的 queue 上。而由于 queue可以散落在不同的 Broker,所以消息就发送到不同的 Broker 下 +RocketMQ 中的负载均衡可以分为 Producer 端发送消息时候的负载均衡和 Consumer 端订阅消息的负载均衡 + +Producer 端在发送消息时,会先根据 Topic 找到指定的 TopicPublishInfo,在获取了 TopicPublishInfo 路由信息后,RocketMQ 的客户端在默认方式调用 selectOneMessageQueue() 方法从 TopicPublishInfo 中的 messageQueueList 中选择一个队列 MessageQueue 进行发送消息 + +默认会轮询所有的 Message Queue 发送,以让消息平均落在不同的 queue 上,而由于 queue可以散落在不同的 Broker,所以消息就发送到不同的 Broker 下,图中箭头线条上的标号代表顺序,发布方会把第一条消息发送至 Queue 0,然后第二条消息发送至 Queue 1,以此类推: ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-producer负载均衡.png) -图中箭头线条上的标号代表顺序,发布方会把第一条消息发送至 Queue 0,然后第二条消息发送至 Queue 1,以此类推 +容错策略均在 MQFaultStrategy 这个类中定义,有一个 sendLatencyFaultEnable 开关变量: + +* 如果开启,会在随机递增取模的基础上,再过滤掉 not available 的 Broker 代理 +* 如果关闭,采用随机递增取模的方式选择一个队列(MessageQueue)来发送消息 + +latencyFaultTolerance 机制是实现消息发送高可用的核心关键所在,对之前失败的,按一定的时间做退避。例如上次请求的 latency 超过 550Lms,就退避 3000Lms;超过 1000L,就退避 60000L @@ -4778,11 +4914,15 @@ Producer 端,每个实例在发消息的时候,默认会轮询所有的 Mess #### 消费端 -广播模式下要求一条消息需要投递到一个消费组下面所有的消费者实例,所以不存在负载均衡,在实现上,Consumer 分配 queue 时,所有 Consumer 都分到所有的queue。 +在 RocketMQ 中,Consumer 端的两种消费模式(Push/Pull)都是基于拉模式来获取消息的,而在 Push 模式只是对 Pull 模式的一种封装,其本质实现为消息拉取线程在从服务器拉取到一批消息,提交到消息消费线程池后,又继续向服务器再次尝试拉取消息,如果未拉取到消息,则延迟一下又继续拉取 + +在两种基于拉模式的消费方式(Push/Pull)中,均需要 Consumer 端在知道从 Broker 端的哪一个消息队列—队列中去获取消息,所以在 Consumer 端来做负载均衡,即 Broker 端中多个 MessageQueue 分配给同一个 ConsumerGroup 中的哪些 Consumer 消费 + +* 广播模式下要求一条消息需要投递到一个消费组下面所有的消费者实例,所以不存在负载均衡,在实现上,Consumer 分配 queue 时,所有 Consumer 都分到所有的queue。 -在集群消费模式下,每条消息只需要投递到订阅这个 Topic 的 Consumer Group 下的一个实例即可,RocketMQ 采用主动拉取的方式拉取并消费消息,在拉取的时候需要明确指定拉取哪一条 Message Queue +* 在集群消费模式下,每条消息只需要投递到订阅这个 Topic 的 Consumer Group 下的一个实例即可,RocketMQ 采用主动拉取的方式拉取并消费消息,在拉取的时候需要明确指定拉取哪一条 Message Queue -而每当实例的数量有变更,都会触发一次所有实例的负载均衡,这时候会按照 queue 的数量和实例的数量平均分配 queue 给每个实例。默认的分配算法是 AllocateMessageQueueAveragely: +集群模式下,每当实例的数量有变更,都会触发一次所有实例的负载均衡,这时候会按照 queue 的数量和实例的数量平均分配 queue 给每个实例。默认的分配算法是 AllocateMessageQueueAveragely: ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-consumer负载均衡1.png) @@ -4792,9 +4932,45 @@ Producer 端,每个实例在发消息的时候,默认会轮询所有的 Mess 集群模式下,queue 都是只允许分配只一个实例,如果多个实例同时消费一个 queue 的消息,由于拉取哪些消息是 Consumer 主动控制的,会导致同一个消息在不同的实例下被消费多次 -通过增加 Consumer 实例去分摊 queue 的消费,可以起到水平扩展的消费能力的作用。而当有实例下线时,会重新触发负载均衡,这时候原来分配到的 queue 将分配到其他实例上继续消费 +通过增加 Consumer 实例去分摊 queue 的消费,可以起到水平扩展的消费能力的作用。而当有实例下线时,会重新触发负载均衡,这时候原来分配到的 queue 将分配到其他实例上继续消费。但是如果 Consumer 实例的数量比 Message Queue 的总数量还多的话,多出来的 Consumer 实例将无法分到 queue,也就无法消费到消息,也就无法起到分摊负载的作用了,所以需要控制让 queue 的总数量大于等于 Consumer 的数量 + + + +*** + + + +#### 原理解析 + +==todo:暂时 copy 官方文档,学习源码后更新,建议粗略看一下,真想搞懂过程还需要研究一下源码== + +在 Consumer 启动后,会通过定时任务不断地向 RocketMQ 集群中的所有 Broker 实例发送心跳包。Broke r端在收到 Consumer 的心跳消息后,会将它维护 在ConsumerManager 的本地缓存变量 consumerTable,同时并将封装后的客户端网络通道信息保存在本地缓存变量 channelInfoTable 中,为 Consumer 端的负载均衡提供可以依据的元数据信息 + +Consumer 端实现负载均衡的核心类 **RebalanceImpl** + +在 Consumer 实例的启动流程中的启动 MQClientInstance 实例部分,会完成负载均衡服务线程 RebalanceService 的启动(每隔 20s 执行一次),RebalanceService 线程的 run() 方法最终调用的是 RebalanceImpl 类的 rebalanceByTopic() 方法,该方法是实现 Consumer 端负载均衡的核心。rebalanceByTopic() 方法会根据消费者通信类型为广播模式还是集群模式做不同的逻辑处理。这里主要看下集群模式下的处理流程: + +* 从 rebalanceImpl 实例的本地缓存变量 topicSubscribeInfoTable 中,获取该 Topic 主题下的消息消费队列集合 mqSet + +* 根据 Topic 和 consumerGroup 为参数调用 `mQClientFactory.findConsumerIdList()` 方法向 Broker 端发送获取该消费组下消费者 ID 列表的 RPC 通信请求(Broker 端基于前面 Consumer 端上报的心跳包数据而构建的 consumerTable 做出响应返回,业务请求码 `GET_CONSUMER_LIST_BY_GROUP`) + +* 先对 Topic 下的消息消费队列、消费者 ID 排序,然后用消息队列分配策略算法(默认是消息队列的平均分配算法),计算出待拉取的消息队列。平均分配算法类似于分页的算法,将所有 MessageQueue 排好序类似于记录,将所有消费端 Consumer 排好序类似页数,并求出每一页需要包含的平均 size 和每个页面记录的范围 range,最后遍历整个 range 而计算出当前 Consumer 端应该分配到的记录(这里即为 MessageQueue) + + ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-负载均衡平均分配算法.png) -但是如果 Consumer 实例的数量比 Message Queue 的总数量还多的话,多出来的 Consumer 实例将无法分到 queue,也就无法消费到消息,也就无法起到分摊负载的作用了,所以需要控制让 queue 的总数量大于等于 Consumer 的数量 +* 调用 updateProcessQueueTableInRebalance() 方法,先将分配到的消息队列集合 mqSet 与 processQueueTable 做一个过滤比对 + + ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-负载均衡重新平衡算法.png) + +* processQueueTable 标注的红色部分,表示与分配到的消息队列集合 mqSet 互不包含,将这些队列设置 Dropped 属性为 true,然后查看这些队列是否可以移除出 processQueueTable 缓存变量。具体执行 removeUnnecessaryMessageQueue() 方法,即每隔 1s 查看是否可以获取当前消费处理队列的锁,拿到的话返回 true;如果等待 1s 后,仍然拿不到当前消费处理队列的锁则返回 false。如果返回 true,则从 processQueueTable 缓存变量中移除对应的 Entry + +* processQueueTable 的绿色部分,表示与分配到的消息队列集合 mqSet 的交集,判断该 ProcessQueue 是否已经过期了,在 Pull 模式的不用管,如果是 Push 模式的,设置 Dropped 属性为 true,并且调用 removeUnnecessaryMessageQueue() 方法,像上面一样尝试移除 Entry + +* 为过滤后的消息队列集合 mqSet 中每个 MessageQueue 创建 ProcessQueue 对象存入 RebalanceImpl 的 processQueueTable 队列中(其中调用 RebalanceImpl 实例的 `computePullFromWhere(MessageQueue mq)` 方法获取该 MessageQueue 对象的下一个进度消费值 offset,随后填充至接下来要创建的 pullRequest 对象属性中),并创建拉取请求对象 pullRequest 添加到拉取列表 pullRequestList 中,最后执行 dispatchPullRequest() 方法,将 Pull 消息的请求对象 PullRequest 依次放入 PullMessageService 服务线程的阻塞队列 pullRequestQueue 中,待该服务线程取出后向 Broker 端发起 Pull 消息的请求。 + + 对比下 RebalancePushImpl 和 RebalancePullImpl 两个实现类的 dispatchPullRequest() 方法,RebalancePullImpl 类里面的该方法为空 + +消息消费队列在同一消费组不同消费者之间的负载均衡,其核心设计理念是在一个消息消费队列在同一时间只允许被同一消费组内的一个消费者消费,一个消息消费者能同时消费多个消息队列 @@ -4804,19 +4980,20 @@ Producer 端,每个实例在发消息的时候,默认会轮询所有的 Mess ### 消息重试 -todo:以下还需要修改,明日完成 +#### 重试机制 -顺序消息的重试 +Consumer 消费消息失败后,提供了一种重试机制,令消息再消费一次。Consumer 消费消息失败可以认为有以下几种情况: -对于顺序消息,当消费者消费消息失败后,消息队列 RocketMQ 会自动不断进行消息重试(每次间隔时间为 1 秒),这时,应用会出现消息消费被阻塞的情况。因此,在使用顺序消息时,务必保证应用能够及时监控并处理消费失败的情况,避免阻塞现象的发生。 +- 由于消息本身的原因,例如反序列化失败,消息数据本身无法处理等。这种错误通常需要跳过这条消息,再消费其它消息,而这条失败的消息即使立刻重试消费,99% 也不成功,所以需要提供一种定时重试机制,即过 10秒 后再重试 +- 由于依赖的下游应用服务不可用,例如 DB 连接不可用,外系统网络不可达等。这种情况即使跳过当前失败的消息,消费其他消息同样也会报错,这种情况建议应用 sleep 30s,再消费下一条消息,这样可以减轻 Broker 重试消息的压力 -1.4.2 无序消息的重试 +RocketMQ 会为每个消费组都设置一个 Topic 名称为 `%RETRY%+consumerGroup` 的重试队列(这个 Topic 的重试队列是针对消费组,而不是针对每个 Topic 设置的),用于暂时保存因为各种异常而导致 Consumer 端无法消费的消息 -对于无序消息(普通、定时、延时、事务消息),当消费者消费消息失败时,您可以通过设置返回状态达到消息重试的结果。 +* 顺序消息的重试,当消费者消费消息失败后,消息队列 RocketMQ 会自动不断进行消息重试(每次间隔时间为 1 秒),这时应用会出现消息消费被阻塞的情况。所以在使用顺序消息时,必须保证应用能够及时监控并处理消费失败的情况,避免阻塞现象的发生 -无序消息的重试只针对集群消费方式生效;广播方式不提供失败重试特性,即消费失败后,失败消息不再重试,继续消费新的消息。 +* 无序消息(普通、定时、延时、事务消息)的重试,可以通过设置返回状态达到消息重试的结果。无序消息的重试只针对集群消费方式生效,广播方式不提供失败重试特性,即消费失败后,失败消息不再重试,继续消费新的消息 -1)重试次数 +**无序消息情况下**,因为异常恢复需要一些时间,会为重试队列设置多个重试级别,每个重试级别都有对应的重新投递延时,重试次数越多投递延时就越大。RocketMQ 对于重试消息的处理是先保存至 Topic 名称为 `SCHEDULE_TOPIC_XXXX` 的延迟队列中,后台定时任务按照对应的时间进行 Delay 后重新保存至 `%RETRY%+consumerGroup` 的重试队列中 消息队列 RocketMQ 默认允许每条消息最多重试 16 次,每次重试的间隔时间如下: @@ -4831,25 +5008,29 @@ todo:以下还需要修改,明日完成 | 7 | 5 分钟 | 15 | 1 小时 | | 8 | 6 分钟 | 16 | 2 小时 | -如果消息重试 16 次后仍然失败,消息将不再投递。如果严格按照上述重试时间间隔计算,某条消息在一直消费失败的前提下,将会在接下来的 4 小时 46 分钟之内进行 16 次重试,超过这个时间范围消息将不再重试投递。 +如果消息重试 16 次后仍然失败,消息将**不再投递**,如果严格按照上述重试时间间隔计算,某条消息在一直消费失败的前提下,将会在接下来的 4 小时 46 分钟之内进行 16 次重试,超过这个时间范围消息将不再重试投递 + +说明:一条消息无论重试多少次,消息的 Message ID 是不会改变的 + + + +*** -**注意:** 一条消息无论重试多少次,这些重试消息的 Message ID 不会改变。 -2)配置方式 -**消费失败后,重试配置方式** +#### 重试操作 集群消费方式下,消息消费失败后期望消息重试,需要在消息监听器接口的实现中明确进行配置(三种方式任选一种): - 返回 Action.ReconsumeLater (推荐) -- 返回 Null +- 返回 null - 抛出异常 ```java public class MessageListenerImpl implements MessageListener { @Override public Action consume(Message message, ConsumeContext context) { - //处理消息 + // 处理消息 doConsumeMessage(message); //方式1:返回 Action.ReconsumeLater,消息将重试 return Action.ReconsumeLater; @@ -4861,9 +5042,7 @@ public class MessageListenerImpl implements MessageListener { } ``` -**消费失败后,不重试配置方式** - -集群消费方式下,消息失败后期望消息不重试,需要捕获消费逻辑中可能抛出的异常,最终返回 Action.CommitMessage,此后这条消息将不会再重试。 +集群消费方式下,消息失败后期望消息不重试,需要捕获消费逻辑中可能抛出的异常,最终返回 Action.CommitMessage,此后这条消息将不会再重试 ```java public class MessageListenerImpl implements MessageListener { @@ -4872,7 +5051,7 @@ public class MessageListenerImpl implements MessageListener { try { doConsumeMessage(message); } catch (Throwable e) { - //捕获消费逻辑中的所有异常,并返回 Action.CommitMessage; + // 捕获消费逻辑中的所有异常,并返回 Action.CommitMessage; return Action.CommitMessage; } //消息处理正常,直接返回 Action.CommitMessage; @@ -4881,35 +5060,30 @@ public class MessageListenerImpl implements MessageListener { } ``` -**自定义消息最大重试次数** - -消息队列 RocketMQ 允许 Consumer 启动的时候设置最大重试次数,重试时间间隔将按照如下策略: +自定义消息最大重试次数,RocketMQ 允许 Consumer 启动的时候设置最大重试次数,重试时间间隔将按照如下策略: -- 最大重试次数小于等于 16 次,则重试时间间隔同上表描述。 -- 最大重试次数大于 16 次,超过 16 次的重试时间间隔均为每次 2 小时。 +- 最大重试次数小于等于 16 次,则重试时间间隔同上表描述 +- 最大重试次数大于 16 次,超过 16 次的重试时间间隔均为每次 2 小时 ```java Properties properties = new Properties(); -//配置对应 Group ID 的最大消息重试次数为 20 次 +// 配置对应 Group ID 的最大消息重试次数为 20 次 properties.put(PropertyKeyConst.MaxReconsumeTimes,"20"); -Consumer consumer =ONSFactory.createConsumer(properties); +Consumer consumer = ONSFactory.createConsumer(properties); ``` -> 注意: +注意: -- 消息最大重试次数的设置对相同 Group ID 下的所有 Consumer 实例有效。 -- 如果只对相同 Group ID 下两个 Consumer 实例中的其中一个设置了 MaxReconsumeTimes,那么该配置对两个 Consumer 实例均生效。 +- 消息最大重试次数的设置对相同 Group ID 下的所有 Consumer 实例有效。例如只对相同 Group ID 下两个 Consumer 实例中的其中一个设置了 MaxReconsumeTimes,那么该配置对两个 Consumer 实例均生效 - 配置采用覆盖的方式生效,即最后启动的 Consumer 实例会覆盖之前的启动实例的配置 -**获取消息重试次数** - 消费者收到消息后,可按照如下方式获取消息的重试次数: ```java public class MessageListenerImpl implements MessageListener { @Override public Action consume(Message message, ConsumeContext context) { - //获取消息的重试次数 + // 获取消息的重试次数 System.out.println(message.getReconsumeTimes()); return Action.CommitMessage; } @@ -4918,6 +5092,22 @@ public class MessageListenerImpl implements MessageListener { +*** + + + +#### 重投机制 + +生产者在发送消息时,同步消息失败会重投,异步消息有重试,oneway 没有任何保证。消息重投保证消息尽可能发送成功、不丢失,但当出现消息量大、网络抖动时,可能会造成消息重复;生产者主动重发、Consumer 负载变化也会导致重复消息。 + +消息重复在 RocketMQ 中是无法避免的问题,如下方法可以设置消息重试策略: + +- retryTimesWhenSendFailed:同步发送失败重投次数,默认为 2,因此生产者会最多尝试发送 retryTimesWhenSendFailed + 1 次。不会选择上次失败的 Broker,尝试向其他 Broker 发送,最大程度保证消息不丢。超过重投次数抛出异常,由客户端保证消息不丢。当出现 RemotingException、MQClientException 和部分 MQBrokerException 时会重投 +- retryTimesWhenSendAsyncFailed:异步发送失败重试次数,异步重试不会选择其他 Broker,仅在同一个 Broker 上做重试,不保证消息不丢 +- retryAnotherBrokerWhenNotStoreOK:消息刷盘(主或备)超时或 slave 不可用(返回状态非 SEND_OK),是否尝试发送到其他 Broker,默认 false,十分重要消息可以开启 + + + *** @@ -4926,22 +5116,24 @@ public class MessageListenerImpl implements MessageListener { ### 死信队列 -当一条消息初次消费失败,消息队列 RocketMQ 会自动进行消息重试;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列 RocketMQ 不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。 +正常情况下无法被消费的消息称为死信消息(Dead-Letter Message),存储死信消息的特殊队列称为死信队列(Dead-Letter Queue) -在消息队列 RocketMQ 中,这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message),存储死信消息的特殊队列称为死信队列(Dead-Letter Queue)。 +当一条消息初次消费失败,消息队列 RocketMQ 会自动进行消息重试,达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时 RocketMQ 不会立刻将消息丢弃,而是将其发送到该消费者对应的死信队列中 -死信消息具有以下特性 +死信消息具有以下特性: -- 不会再被消费者正常消费。 -- 有效期与正常消息相同,均为 3 天,3 天后会被自动删除。因此,请在死信消息产生后的 3 天内及时处理。 +- 不会再被消费者正常消费 +- 有效期与正常消息相同,均为 3 天,3 天后会被自动删除,所以请在死信消息产生后的 3 天内及时处理 死信队列具有以下特性: -- 一个死信队列对应一个 Group ID, 而不是对应单个消费者实例。 -- 如果一个 Group ID 未产生死信消息,消息队列 RocketMQ 不会为其创建相应的死信队列。 -- 一个死信队列包含了对应 Group ID 产生的所有死信消息,不论该消息属于哪个 Topic。 +- 一个死信队列对应一个 Group ID, 而不是对应单个消费者实例 +- 如果一个 Group ID 未产生死信消息,消息队列 RocketMQ 不会为其创建相应的死信队列 +- 一个死信队列包含了对应 Group ID 产生的所有死信消息,不论该消息属于哪个 Topic + +一条消息进入死信队列,需要排查可疑因素并解决问题后,可以在消息队列 RocketMQ 控制台重新发送该消息,让消费者重新消费一次 + -一条消息进入死信队列,意味着某些因素导致消费者无法正常消费该消息,因此,通常需要您对其进行特殊处理。排查可疑因素并解决问题后,可以在消息队列 RocketMQ 控制台重新发送该消息,让消费者重新消费一次。 @@ -4951,52 +5143,66 @@ public class MessageListenerImpl implements MessageListener { ### 幂等消费 -消息队列 RocketMQ 消费者在接收到消息以后,有必要根据业务上的唯一 Key 对消息做幂等处理的必要性。 +消息队列 RocketMQ 消费者在接收到消息以后,需要根据业务上的唯一 Key 对消息做幂等处理 -1.6.1 消费幂等的必要性 +在互联网应用中,尤其在网络不稳定的情况下,消息队列 RocketMQ 的消息有可能会出现重复,几种情况: -在互联网应用中,尤其在网络不稳定的情况下,消息队列 RocketMQ 的消息有可能会出现重复,这个重复简单可以概括为以下情况: +- 发送时消息重复:当一条消息已被成功发送到服务端并完成持久化,此时出现了网络闪断或客户端宕机,导致服务端对客户端应答失败。此时生产者意识到消息发送失败并尝试再次发送消息,消费者后续会收到两条内容相同并且 Message ID 也相同的消息 -- 发送时消息重复 +- 投递时消息重复:消息消费的场景下,消息已投递到消费者并完成业务处理,当客户端给服务端反馈应答的时候网络闪断。为了保证消息至少被消费一次,消息队列 RocketMQ 的服务端将在网络恢复后再次尝试投递之前已被处理过的消息,消费者后续会收到两条内容相同并且 Message ID 也相同的消息 - 当一条消息已被成功发送到服务端并完成持久化,此时出现了网络闪断或者客户端宕机,导致服务端对客户端应答失败。 如果此时生产者意识到消息发送失败并尝试再次发送消息,消费者后续会收到两条内容相同并且 Message ID 也相同的消息。 +- 负载均衡时消息重复:当消息队列 RocketMQ 的 Broker 或客户端重启、扩容或缩容时,会触发 Rebalance,此时消费者可能会收到重复消息 -- 投递时消息重复 - 消息消费的场景下,消息已投递到消费者并完成业务处理,当客户端给服务端反馈应答的时候网络闪断。 为了保证消息至少被消费一次,消息队列 RocketMQ 的服务端将在网络恢复后再次尝试投递之前已被处理过的消息,消费者后续会收到两条内容相同并且 Message ID 也相同的消息。 +处理方式: -- 负载均衡时消息重复(包括但不限于网络抖动、Broker 重启以及订阅方应用重启) +* 因为 Message ID 有可能出现冲突(重复)的情况,所以真正安全的幂等处理,不建议以 Message ID 作为处理依据,最好的方式是以业务唯一标识作为幂等处理的关键依据,而业务的唯一标识可以通过消息 Key 进行设置: - 当消息队列 RocketMQ 的 Broker 或客户端重启、扩容或缩容时,会触发 Rebalance,此时消费者可能会收到重复消息。 + ```java + Message message = new Message(); + message.setKey("ORDERID_100"); + SendResult sendResult = producer.send(message); + ``` -1.6.2 处理方式 +* 订阅方收到消息时可以根据消息的 Key 进行幂等处理: -因为 Message ID 有可能出现冲突(重复)的情况,所以真正安全的幂等处理,不建议以 Message ID 作为处理依据。 最好的方式是以业务唯一标识作为幂等处理的关键依据,而业务的唯一标识可以通过消息 Key 进行设置: + ```java + consumer.subscribe("ons_test", "*", new MessageListener() { + public Action consume(Message message, ConsumeContext context) { + String key = message.getKey() + // 根据业务唯一标识的 key 做幂等处理 + } + }); + ``` -```java -Message message = new Message(); -message.setKey("ORDERID_100"); -SendResult sendResult = producer.send(message); -``` + + + + +*** -订阅方收到消息时可以根据消息的 Key 进行幂等处理: -```java -consumer.subscribe("ons_test", "*", new MessageListener() { - public Action consume(Message message, ConsumeContext context) { - String key = message.getKey() - // 根据业务唯一标识的 key 做幂等处理 - } -}); -``` +### 流量控制 +生产者流控,因为 Broker 处理能力达到瓶颈;消费者流控,因为消费能力达到瓶颈 +生产者流控: +- CommitLog 文件被锁时间超过 osPageCacheBusyTimeOutMills 时,参数默认为 1000ms,返回流控 +- 如果开启 transientStorePoolEnable == true,且 Broker 为异步刷盘的主机,且 transientStorePool 中资源不足,拒绝当前 send 请求,返回流控 +- Broker 每隔 10ms 检查 send 请求队列头部请求的等待时间,如果超过 waitTimeMillsInSendQueue,默认 200ms,拒绝当前 send 请求,返回流控。 +- Broker 通过拒绝 send 请求方式实现流量控制 +注意:生产者流控,不会尝试消息重投 +消费者流控: +- 消费者本地缓存消息数超过 pullThresholdForQueue 时,默认 1000 +- 消费者本地缓存消息大小超过 pullThresholdSizeForQueue 时,默认 100MB +- 消费者本地缓存消息跨度超过 consumeConcurrentlyMaxSpan 时,默认 2000 +消费者流控的结果是降低拉取频率 From df4af1f76c9d889b5470173aac5e25d7ff1c92a0 Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 17 Nov 2021 00:37:29 +0800 Subject: [PATCH 076/168] Update Java Notes --- Frame.md | 2 +- Java.md | 66 ++++++++++++++++++++++++++++---------------------------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/Frame.md b/Frame.md index 3536a79..c8e1908 100644 --- a/Frame.md +++ b/Frame.md @@ -4942,7 +4942,7 @@ latencyFaultTolerance 机制是实现消息发送高可用的核心关键所在 #### 原理解析 -==todo:暂时 copy 官方文档,学习源码后更新,建议粗略看一下,真想搞懂过程还需要研究一下源码== +==todo:暂时 copy 官方文档,学习源码后更新,真想搞懂过程还需要研究一下源码== 在 Consumer 启动后,会通过定时任务不断地向 RocketMQ 集群中的所有 Broker 实例发送心跳包。Broke r端在收到 Consumer 的心跳消息后,会将它维护 在ConsumerManager 的本地缓存变量 consumerTable,同时并将封装后的客户端网络通道信息保存在本地缓存变量 channelInfoTable 中,为 Consumer 端的负载均衡提供可以依据的元数据信息 diff --git a/Java.md b/Java.md index e918610..4f49e7c 100644 --- a/Java.md +++ b/Java.md @@ -2442,22 +2442,22 @@ s = s + "cd"; //s = abccd 新对象 常用 API: -* `public boolean equals(String s)` : 比较两个字符串内容是否相同、区分大小写 - -* `public boolean equalsIgnoreCase(String anotherString)` : 比较字符串的内容,忽略大小写 -* `public int length()` : 返回此字符串的长度 -* `public String trim()` : 返回一个字符串,其值为此字符串,并删除任何前导和尾随空格 -* `public String[] split(String regex)` : 将字符串按给定的正则表达式分割成字符串数组 -* `public char charAt(int index)` : 取索引处的值 -* `public char[] toCharArray()` : 将字符串拆分为字符数组后返回 -* `public boolean startsWith(String prefix)` : 测试此字符串是否以指定的前缀开头 -* `public int indexOf(String str)` : 返回指定子字符串第一次出现的字符串内的索引,没有返回 -1 -* `public int lastIndexOf(String str)` : 返回字符串最后一次出现str的索引,没有返回 -1 -* `public String substring(int beginIndex)` : 返回子字符串,以原字符串指定索引处到结尾 -* `public String substring(int i, int j)` : 指定索引处扩展到 j - 1 的位置,字符串长度为 j - i -* `public String toLowerCase()` : 将此 String 所有字符转换为小写,使用默认语言环境的规则 -* `public String toUpperCase()` : 使用默认语言环境的规则将此 String 所有字符转换为大写 -* `public String replace(CharSequence target, CharSequence replacement)` : 使用新值,将字符串中的旧值替换,得到新的字符串 +* `public boolean equals(String s)`:比较两个字符串内容是否相同、区分大小写 + +* `public boolean equalsIgnoreCase(String anotherString)`:比较字符串的内容,忽略大小写 +* `public int length()`:返回此字符串的长度 +* `public String trim()`:返回一个字符串,其值为此字符串,并删除任何前导和尾随空格 +* `public String[] split(String regex)`:将字符串按给定的正则表达式分割成字符串数组 +* `public char charAt(int index)`:取索引处的值 +* `public char[] toCharArray()`:将字符串拆分为字符数组后返回 +* `public boolean startsWith(String prefix)`:测试此字符串是否以指定的前缀开头 +* `public int indexOf(String str)`:返回指定子字符串第一次出现的字符串内的索引,没有返回 -1 +* `public int lastIndexOf(String str)`:返回字符串最后一次出现str的索引,没有返回 -1 +* `public String substring(int beginIndex)`:返回子字符串,以原字符串指定索引处到结尾 +* `public String substring(int i, int j)`:指定索引处扩展到 j - 1 的位置,字符串长度为 j - i +* `public String toLowerCase()`:将此 String 所有字符转换为小写,使用默认语言环境的规则 +* `public String toUpperCase()`:使用默认语言环境的规则将此 String 所有字符转换为大写 +* `public String replace(CharSequence target, CharSequence replacement)`:使用新值,将字符串中的旧值替换,得到新的字符串 ```java String s = 123-78; @@ -2474,9 +2474,9 @@ s.replace("-","");//12378 构造方法: -* `public String()` : 创建一个空白字符串对象,不含有任何内容 -* `public String(char[] chs)` : 根据字符数组的内容,来创建字符串对象 -* `public String(String original)` : 根据传入的字符串内容,来创建字符串对象 +* `public String()`:创建一个空白字符串对象,不含有任何内容 +* `public String(char[] chs)`:根据字符数组的内容,来创建字符串对象 +* `public String(String original)`:根据传入的字符串内容,来创建字符串对象 直接赋值:`String s = "abc"` 直接赋值的方式创建字符串对象,内容就是 abc @@ -3009,20 +3009,20 @@ JDK1.8 新增,线程安全 常用API: -| 方法名 | 说明 | -| --------------------------------------------------------- | ----------------------------------------------------------- | -| public int getYear() | 获取年 | -| public int getMonthValue() | 获取月份(1-12) | -| public int getDayOfMonth() | 获取月份中的第几天(1-31) | -| public int getDayOfYear() | 获取一年中的第几天(1-366) | -| public DayOfWeek getDayOfWeek() | 获取星期 | -| public int getMinute() | 获取分钟 | -| public int getHour() | 获取小时 | -| public LocalDate toLocalDate() | 转换成为一个LocalDate对象(年月日) | -| public LocalTime toLocalTime() | 转换成为一个LocalTime对象(时分秒) | -| public String format(指定格式) | 把一个LocalDateTime格式化成为一个字符串 | -| public LocalDateTime parse(准备解析的字符串, 解析格式) | 把一个日期字符串解析成为一个LocalDateTime对象 | -| public static DateTimeFormatter ofPattern(String pattern) | 使用指定的日期模板获取一个日期格式化器DateTimeFormatter对象 | +| 方法名 | 说明 | +| --------------------------------------------------------- | ------------------------------------------------------------ | +| public int getYear() | 获取年 | +| public int getMonthValue() | 获取月份(1-12) | +| public int getDayOfMonth() | 获取月份中的第几天(1-31) | +| public int getDayOfYear() | 获取一年中的第几天(1-366) | +| public DayOfWeek getDayOfWeek() | 获取星期 | +| public int getMinute() | 获取分钟 | +| public int getHour() | 获取小时 | +| public LocalDate toLocalDate() | 转换成为一个 LocalDate 对象(年月日) | +| public LocalTime toLocalTime() | 转换成为一个 LocalTime 对象(时分秒) | +| public String format(指定格式) | 把一个 LocalDateTime 格式化成为一个字符串 | +| public LocalDateTime parse(准备解析的字符串, 解析格式) | 把一个日期字符串解析成为一个 LocalDateTime 对象 | +| public static DateTimeFormatter ofPattern(String pattern) | 使用指定的日期模板获取一个日期格式化器 DateTimeFormatter 对象 | ```java public class JDK8DateDemo2 { From 5d4885103b4598a7d5f6056955d9acdd90bff9a2 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 20 Nov 2021 15:05:06 +0800 Subject: [PATCH 077/168] Update Java Notes --- Prog.md | 229 ++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 206 insertions(+), 23 deletions(-) diff --git a/Prog.md b/Prog.md index e0ff4ea..41bb420 100644 --- a/Prog.md +++ b/Prog.md @@ -622,7 +622,7 @@ t.start(); | public final void suspend() | **挂起(暂停)线程运行** | | public final void resume() | 恢复线程运行 | -所以 Java 中线程的状态是阻塞,很少使用挂起 + @@ -9289,7 +9289,7 @@ ReentrantReadWriteLock 其**读锁是共享锁,写锁是独占锁** * **重入时升级不支持**:持有读锁的情况下去获取写锁会导致获取写锁永久等待,需要先释放读,再去获得写 -* **重入时降级支持**:持有写锁的情况下去获取读锁 +* **重入时降级支持**:持有写锁的情况下去获取读锁,造成只有当前线程会持有读锁,因为写锁会互斥其他的锁 ```java w.lock(); @@ -9357,7 +9357,7 @@ public static void main(String[] args) { 缓存更新时,是先清缓存还是先更新数据库 -* 先清缓存:可能造成刚清理缓存还没有更新数据库,线程直接查询了数据库更新缓存 +* 先清缓存:可能造成刚清理缓存还没有更新数据库,线程直接查询了数据库更新过期数据到缓存 * 先更新据库:可能造成刚更新数据库,还没清空缓存就有线程从缓存拿到了旧数据 @@ -9375,14 +9375,110 @@ public static void main(String[] args) { #### 实现原理 -##### 加锁原理 +##### 成员属性 读写锁用的是同一个 Sycn 同步器,因此等待队列、state 等也是同一个,原理与 ReentrantLock 加锁相比没有特殊之处,不同是**写锁状态占了 state 的低 16 位,而读锁使用的是 state 的高 16 位** -* t1 w.lock(写锁),成功上锁 state = 0_1 +* 读写锁: + + ```java + private final ReentrantReadWriteLock.ReadLock readerLock; + private final ReentrantReadWriteLock.WriteLock writerLock; + ``` + +* 构造方法:默认是非公平锁,可以指定参数创建公平锁 + + ```java + public ReentrantReadWriteLock(boolean fair) { + // true 为公平锁 + sync = fair ? new FairSync() : new NonfairSync(); + // 这两个 lock 共享同一个 sync 实例,都是由 ReentrantReadWriteLock 的 sync 提供同步实现 + readerLock = new ReadLock(this); + writerLock = new WriteLock(this); + } + ``` + +Sync 类的属性: + +* 统计变量: + + ```java + // 用来移位 + static final int SHARED_SHIFT = 16; + // 高16位的1 + static final int SHARED_UNIT = (1 << SHARED_SHIFT); + // 65535,16个1,代表写锁的最大重入次数 + static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; + // 低16位掩码:0b 1111 1111 1111 1111,用来获取写锁重入的次数 + static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; + ``` + +* 获取读写锁的次数: + + ```java + // 获取读写锁的读锁分配的总次数 + static int sharedCount(int c) { return c >>> SHARED_SHIFT; } + // 写锁(独占)锁的重入次数 + static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; } + ``` + +* 内部类: + + ```java + // 记录读锁线程自己的持有读锁的数量(重入次数),因为 state 高16位记录的是全局范围内所有的读线程获取读锁的总量 + static final class HoldCounter { + int count = 0; + // Use id, not reference, to avoid garbage retention + final long tid = getThreadId(Thread.currentThread()); + } + // 线程安全的存放线程各自的 HoldCounter 对象 + static final class ThreadLocalHoldCounter extends ThreadLocal { + public HoldCounter initialValue() { + return new HoldCounter(); + } + } + ``` + +* 内部类实例: + + ```java + // 当前线程持有的可重入读锁的数量,计数为 0 时删除 + private transient ThreadLocalHoldCounter readHolds; + // 记录最后一个获取【读锁】线程的 HoldCounter 对象 + private transient HoldCounter cachedHoldCounter; + ``` + +* 首次获取锁: + + ```java + // 第一个获取读锁的线程 + private transient Thread firstReader = null; + // 记录该线程持有的读锁次数(读锁重入次数) + private transient int firstReaderHoldCount; + ``` + +* Sync 构造方法: ```java - //lock() -> sync.acquire(1); + Sync() { + readHolds = new ThreadLocalHoldCounter(); + // 确保其他线程的数据可见性,state 是 volatile 修饰的变量,重写该值会将线程本地缓存数据【同步至主存】 + setState(getState()); + } + ``` + + + +*** + + + +##### 加锁原理 + +* t1 线程:w.lock(**写锁**),成功上锁 state = 0_1 + + ```java + // lock() -> sync.acquire(1); public void lock() { sync.acquire(1); } @@ -9399,20 +9495,22 @@ public static void main(String[] args) { int c = getState(); // 获得低 16 位, 代表写锁的 state 计数 int w = exclusiveCount(c); + // 说明有读锁或者写锁 if (c != 0) { - // c != 0 and w == 0 表示 r != 0,有读锁,读锁不能升级,直接返回false + // c != 0 and w == 0 表示有读锁,【读锁不能升级】,直接返回 false // w != 0 说明有写锁,写锁的拥有者不是自己,获取失败 if (w == 0 || current != getExclusiveOwnerThread()) return false; - // 锁重入计数超过低 16 位, 报异常 + + // 执行到这里只有一种情况:【写锁重入】,所以下面几行代码不存在并发 if (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error("Maximum lock count exceeded"); - // 【写锁重入, 获得锁成功】 + // 写锁重入, 获得锁成功,没有并发,所以不使用 CAS setState(c + acquires); return true; } - // c == 0,没有任何锁,判断写锁是否该阻塞,是 false 就尝试获取锁,失败返回 false + // c == 0,说明没有任何锁,判断写锁是否该阻塞,是 false 就尝试获取锁,失败返回 false if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) return false; // 获得锁成功,设置锁的持有线程为当前线程 @@ -9429,11 +9527,11 @@ public static void main(String[] args) { } ``` -* t2 r.lock(读锁),进入 tryAcquireShared 流程,如果有写锁占据,那么 tryAcquireShared +* t2 r.lock(**读锁**),进入 tryAcquireShared 流程: * 返回 -1 表示失败 * 如果返回 0 表示成功 - * 返回正数表示还有多少后继节点支持共享模式,读写锁返回1 + * 返回正数表示还有多少后继节点支持共享模式,读写锁返回 1 ```java public void lock() { @@ -9451,28 +9549,113 @@ public static void main(String[] args) { protected final int tryAcquireShared(int unused) { Thread current = Thread.currentThread(); int c = getState(); - // 低 16 位, 代表写锁的 state - // 如果是其它线程持有写锁, 并且写锁的持有者不是当前线程,获取读锁失败,【写锁允许降级】 + // exclusiveCount(c) 代表低 16 位, 写锁的 state,成立说明有线程持有写锁 + // 写锁的持有者不是当前线程,则获取读锁失败,【写锁允许降级】 if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return -1; - // 高 16 位,代表读锁的 state + // 高 16 位,代表读锁的 state,共享锁分配出去的总次数 int r = sharedCount(c); - if (!readerShouldBlock() && // 读锁不该阻塞 - r < MAX_COUNT && // 小于读锁计数 - compareAndSetState(c, c + SHARED_UNIT)) {// 尝试增加计数成功 - // .... + // 读锁是否应该阻塞 + if (!readerShouldBlock() && r < MAX_COUNT && + compareAndSetState(c, c + SHARED_UNIT)) { // 尝试增加读锁计数 + // 加锁成功 + // 加锁之前读锁为 0,说明当前线程是第一个读锁线程 + if (r == 0) { + firstReader = current; + firstReaderHoldCount = 1; + // 第一个读锁线程是自己就发生了读锁重入 + } else if (firstReader == current) { + firstReaderHoldCount++; + } else { + // cachedHoldCounter 设置为当前线程的 holdCounter 对象,即最后一个获取读锁的线程 + HoldCounter rh = cachedHoldCounter; + // 说明还没设置 rh + if (rh == null || rh.tid != getThreadId(current)) + // 获取当前线程的锁重入的对象,赋值给 cachedHoldCounter + cachedHoldCounter = rh = readHolds.get(); + // 还没重入 + else if (rh.count == 0) + readHolds.set(rh); + // 重入 + 1 + rh.count++; + } // 读锁加锁成功 return 1; } - // 与 tryAcquireShared 功能类似, 但会不断尝试 for (;;) 获取读锁, 执行过程中无阻塞 + // 逻辑到这 应该阻塞,或者 cas 加锁失败 + // 会不断尝试 for (;;) 获取读锁, 执行过程中无阻塞 return fullTryAcquireShared(current); } - // 非公平锁 readerShouldBlock 看 AQS 队列中第一个节点是否是写锁,是则阻塞,反之不阻塞 + // 非公平锁 readerShouldBlock 偏向写锁一些,看 AQS 阻塞队列中第一个节点是否是写锁,是则阻塞,反之不阻塞 + // 防止一直有读锁线程,导致写锁线程饥饿 // true 则该阻塞, false 则不阻塞 final boolean readerShouldBlock() { return apparentlyFirstQueuedIsExclusive(); } + final boolean readerShouldBlock() { + return hasQueuedPredecessors(); + } + ``` + + ```java + final int fullTryAcquireShared(Thread current) { + // 当前读锁线程持有的读锁次数对象 + HoldCounter rh = null; + for (;;) { + int c = getState(); + // 说明有线程持有写锁 + if (exclusiveCount(c) != 0) { + // 写锁不是自己则获取锁失败 + if (getExclusiveOwnerThread() != current) + return -1; + } else if (readerShouldBlock()) { + // 条件成立说明当前线程是 firstReader,当前锁是读忙碌状态,而且当前线程也是读锁重入 + if (firstReader == current) { + // assert firstReaderHoldCount > 0; + } else { + if (rh == null) { + // 最后一个读锁的 HoldCounter + rh = cachedHoldCounter; + // 说明当前线程也不是最后一个读锁 + if (rh == null || rh.tid != getThreadId(current)) { + // 获取当前线程的 HoldCounter + rh = readHolds.get(); + // 条件成立说明 HoldCounter 对象是上一步代码新建的 + // 当前线程不是锁重入,在 readerShouldBlock() 返回 true 时需要去排队 + if (rh.count == 0) + // 防止内存泄漏 + readHolds.remove(); + } + } + if (rh.count == 0) + return -1; + } + } + // 越界判断 + if (sharedCount(c) == MAX_COUNT) + throw new Error("Maximum lock count exceeded"); + // 读锁加锁,条件内的逻辑与 tryAcquireShared 相同 + if (compareAndSetState(c, c + SHARED_UNIT)) { + if (sharedCount(c) == 0) { + firstReader = current; + firstReaderHoldCount = 1; + } else if (firstReader == current) { + firstReaderHoldCount++; + } else { + if (rh == null) + rh = cachedHoldCounter; + if (rh == null || rh.tid != getThreadId(current)) + rh = readHolds.get(); + else if (rh.count == 0) + readHolds.set(rh); + rh.count++; + cachedHoldCounter = rh; // cache for release + } + return 1; + } + } + } ``` * 获取读锁失败,进入 sync.doAcquireShared(1) 流程开始阻塞,首先也是调用 addWaiter 添加节点,不同之处在于节点被设置为 Node.SHARED 模式而非 Node.EXCLUSIVE 模式,注意此时 t2 仍处于活跃状态 @@ -9603,7 +9786,7 @@ public static void main(String[] args) { else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; } - // 条件不成立说明被唤醒的节点非常积极,直接将自己设置为了新的head, + // 条件不成立说明被唤醒的节点非常积极,直接将自己设置为了新的 head, // 此时唤醒它的节点(前驱)执行 h == head 不成立,所以不会跳出循环,会继续唤醒新的 head 节点的后继节点 if (h == head) break; @@ -9632,7 +9815,7 @@ public static void main(String[] args) { ```java protected final boolean tryReleaseShared(int unused) { - // + for (;;) { int c = getState(); int nextc = c - SHARED_UNIT; From 50c329b304ca222a20f25b00a04a2bf6c01c4845 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 21 Nov 2021 16:56:13 +0800 Subject: [PATCH 078/168] Update Java Notes --- Java.md | 6 ++-- Prog.md | 97 +++++++++++++++++++++++++++++++++++++++------------------ 2 files changed, 69 insertions(+), 34 deletions(-) diff --git a/Java.md b/Java.md index 4f49e7c..2ac7b25 100644 --- a/Java.md +++ b/Java.md @@ -4414,7 +4414,7 @@ TreeSet 集合自排序的方式: * 直接为**对象的类**实现比较器规则接口 Comparable,重写比较方法: - 方法:`public int compareTo(Employee o): this 是比较者, o 是被比较者` + 方法:`public int compareTo(Employee o): this 是比较者, o 是被比较者` * 比较者大于被比较者,返回正数(升序) * 比较者小于被比较者,返回负数 @@ -5948,8 +5948,8 @@ class Dog{} + 这个集合不能添加,不能删除,不能修改 + 但是可以结合集合的带参构造,实现集合的批量添加 -在Map接口中,还有一个ofEntries方法可以提高代码的阅读性 -+ 首先会把键值对封装成一个Entry对象,再把这个Entry对象添加到集合当中 +在 Map 接口中,还有一个 ofEntries 方法可以提高代码的阅读性 ++ 首先会把键值对封装成一个 Entry 对象,再把这个 Entry 对象添加到集合当中 ````java public class MyVariableParameter4 { diff --git a/Prog.md b/Prog.md index 41bb420..82e14e6 100644 --- a/Prog.md +++ b/Prog.md @@ -6845,9 +6845,6 @@ FutureTask 类的成员方法: -参考视频:https://www.bilibili.com/video/BV13E411N7pp - - **** @@ -6912,8 +6909,8 @@ public ScheduledThreadPoolExecutor(int corePoolSize) { 常用 API: * `ScheduledFuture schedule(Runnable/Callable, long delay, TimeUnit u)`:延迟执行任务 -* `ScheduledFuture scheduleAtFixedRate(Runnable/Callable, long initialDelay, long period, TimeUnit unit)`:定时执行任务,参数为初始延迟时间、间隔时间、单位 -* `ScheduledFuture scheduleWithFixedDelay(Runnable/Callable, long initialDelay, long delay, TimeUnit unit)`:定时执行任务,参数为初始延迟时间、间隔时间、单位 +* `ScheduledFuture scheduleAtFixedRate(Runnable/Callable, long initialDelay, long period, TimeUnit unit)`:定时执行周期任务,不考虑执行的耗时,参数为初始延迟时间、间隔时间、单位 +* `ScheduledFuture scheduleWithFixedDelay(Runnable/Callable, long initialDelay, long delay, TimeUnit unit)`:定时执行周期任务,考虑执行的耗时,参数为初始延迟时间、间隔时间、单位 基本使用: @@ -6983,7 +6980,7 @@ public ScheduledThreadPoolExecutor(int corePoolSize) { ##### 成员变量 -* shutdown 后是否继续执行定时任务: +* shutdown 后是否继续执行周期任务: ```java private volatile boolean continueExistingPeriodicTasksAfterShutdown; @@ -6998,10 +6995,11 @@ public ScheduledThreadPoolExecutor(int corePoolSize) { * 取消方法是否将该任务从队列中移除: ```java + // 默认 false,不移除,等到线程拿到任务之后抛弃 private volatile boolean removeOnCancel = false; ``` -* 任务的序列号: +* 任务的序列号,可以用来比较优先级: ```java private static final AtomicLong sequencer = new AtomicLong(); @@ -7015,7 +7013,7 @@ public ScheduledThreadPoolExecutor(int corePoolSize) { ##### 延迟任务 -ScheduledFutureTask 继承 FutureTask,实现 RunnableScheduledFuture 接口,具有延迟执行的特点,覆盖 FutureTask 的 run 方法来实现对**延时执行、周期执行**的支持。对于延时任务调用 FutureTask#run,而对于周期性任务则调用 FutureTask#runAndReset 并且在成功之后根据 fixed-delay/fixed-rate 模式来设置下次执行时间并重新将任务塞到工作队列。 +ScheduledFutureTask 继承 FutureTask,实现 RunnableScheduledFuture 接口,具有延迟执行的特点,覆盖 FutureTask 的 run 方法来实现对**延时执行、周期执行**的支持。对于延时任务调用 FutureTask#run,而对于周期性任务则调用 FutureTask#runAndReset 并且在成功之后根据 fixed-delay/fixed-rate 模式来设置下次执行时间并重新将任务塞到工作队列 在调度线程池中无论是 runnable 还是 callable,无论是否需要延迟和定时,所有的任务都会被封装成 ScheduledFutureTask @@ -7030,7 +7028,7 @@ ScheduledFutureTask 继承 FutureTask,实现 RunnableScheduledFuture 接口, * 执行时间: ```java - private long time; // 任务可以被执行的时间,以纳秒表示 + private long time; // 任务可以被执行的时间,交付时间,以纳秒表示 private final long period; // 0 表示非周期任务,正数表示 fixed-rate 模式的周期,负数表示 fixed-delay 模式 ``` @@ -7045,7 +7043,8 @@ ScheduledFutureTask 继承 FutureTask,实现 RunnableScheduledFuture 接口, * 任务在队列数组中的索引下标: ```java - int heapIndex; // -1 代表删除 + // DelayedWorkQueue 底层使用的数据结构是最小堆,记录当前任务在堆中的索引,-1 代表删除 + int heapIndex; ``` 成员方法: @@ -7066,13 +7065,40 @@ ScheduledFutureTask 继承 FutureTask,实现 RunnableScheduledFuture 接口, * compareTo():ScheduledFutureTask 根据执行时间 time 正序排列,如果执行时间相同,在按照序列号 sequenceNumber 正序排列,任务需要放入 DelayedWorkQueue,延迟队列中使用该方法按照从小到大进行排序 + ```java + public int compareTo(Delayed other) { + if (other == this) // compare zero if same object + return 0; + if (other instanceof ScheduledFutureTask) { + // 类型强转 + ScheduledFutureTask x = (ScheduledFutureTask)other; + // 比较者 - 被比较者的执行时间 + long diff = time - x.time; + // 比较者先执行 + if (diff < 0) + return -1; + // 被比较者先执行 + else if (diff > 0) + return 1; + // 比较者的序列号小 + else if (sequenceNumber < x.sequenceNumber) + return -1; + else + return 1; + } + // 不是 ScheduledFutureTask 类型时,根据延迟时间排序 + long diff = getDelay(NANOSECONDS) - other.getDelay(NANOSECONDS); + return (diff < 0) ? -1 : (diff > 0) ? 1 : 0; + } + ``` + * run():执行任务,非周期任务直接完成直接结束,**周期任务执行完后会设置下一次的执行时间,重新放入线程池的阻塞队列**,如果线程池中的线程数量少于核心线程,就会添加 Worker 开启新线程 ```java public void run() { // 是否周期性,就是判断 period 是否为 0 boolean periodic = isPeriodic(); - // 检查当前状态能否执行任务,不能执行就取消任务 + // 根据是否是周期任务检查当前状态能否执行任务,不能执行就取消任务 if (!canRunInCurrentRunState(periodic)) cancel(false); // 非周期任务,直接调用 FutureTask#run 执行 @@ -7088,7 +7114,7 @@ ScheduledFutureTask 继承 FutureTask,实现 RunnableScheduledFuture 接口, } ``` - 周期任务正常完成后任务的状态不会变化,依旧是 NEW,不会设置 outcome 属性。但是如果本次任务执行出现异常,会进入 setException 方法将任务状态置为异常,把异常保存在 outcome 中,方法返回 false,后续的该任务将不会再周期的执行 + 周期任务正常完成后**任务的状态不会变化**,依旧是 NEW,不会设置 outcome 属性。但是如果本次任务执行出现异常,会进入 setException 方法将任务状态置为异常,把异常保存在 outcome 中,方法返回 false,后续的该任务将不会再周期的执行 ```java protected boolean runAndReset() { @@ -7128,10 +7154,10 @@ ScheduledFutureTask 继承 FutureTask,实现 RunnableScheduledFuture 接口, private void setNextRunTime() { long p = period; if (p > 0) - // fixed-rate 模式,【时间设置为上一次执行任务的时间 +p】,两次任务执行的时间差 + // fixed-rate 模式,【时间设置为上一次执行任务的时间 + p】,两次任务执行的时间差 time += p; else - // fixed-delay 模式,下一次执行时间是当【前这次任务结束的时间(就是现在) +delay 值】 + // fixed-delay 模式,下一次执行时间是【当前这次任务结束的时间(就是现在) + delay 值】 time = triggerTime(-p); } ``` @@ -7144,7 +7170,8 @@ ScheduledFutureTask 继承 FutureTask,实现 RunnableScheduledFuture 接口, if (canRunInCurrentRunState(true)) { // 【放入任务队列】 super.getQueue().add(task); - // 再次检查是否可以执行,如果不能执行且任务还在队列中未被取走,则取消任务 + // 如果提交完任务之后,线程池状态变为了 shutdown 状态,需要再次检查是否可以执行, + // 如果不能执行且任务还在队列中未被取走,则取消任务 if (!canRunInCurrentRunState(true) && remove(task)) task.cancel(false); else @@ -7178,9 +7205,9 @@ ScheduledFutureTask 继承 FutureTask,实现 RunnableScheduledFuture 接口, ##### 延迟队列 -DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先队列 PriorityQueue(小根堆)存储元素 +DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先队列 PriorityQueue(小根堆、满二叉树)存储元素 -其他阻塞队列存储节点的数据结构大都是链表,**延迟队列是数组**,所以延迟队列出队头元素后需要让其他元素(尾)替换到头节点,防止空指针异常 +其他阻塞队列存储节点的数据结构大都是链表,**延迟队列是数组**,所以延迟队列出队头元素后需要**让其他元素(尾)替换到头节点**,防止空指针异常 成员变量: @@ -7203,9 +7230,10 @@ DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先 * 阻塞等待头节点的线程: ```java - // 通过阻塞方式去获取头结点,那么 leader 线程的等待时间为头结点的延迟时间,其它线程则会陷入阻塞状态 - // leader 线程获取到头结点后需要发送信号唤醒其它线程 available.asignAll() - // 使用了 Leader/Follower 来避免不必要的等待,只让leader来等待需要等待的时间,其余线程无限等待直至被唤醒即可 + // 线程池内的某个线程去 take() 获取任务时,如果延迟队列顶层节点不为null(队列内有任务),但是节点任务还不到触发时间,线程就去检查【队列的 leader】字段是否被占用 + // * 如果未被占用,则当前线程占用该字段,然后当前线程到 available 条件队列指定超时时间(堆顶任务.time - now())挂起 + // * 如果被占用,当前线程直接到 available 条件队列“不指定”超时时间的挂起 + // leader 在 available 条件队列内是首元素,它超时之后会醒过来,然后再次将堆顶元素获取走,获取走之后,take()结束之前,会调用是 available.signal() 唤醒下一个条件队列内的等待者,然后释放 lock,下一个等待者被唤醒后去到 AQS 队列,做 acquireQueue(node) 逻辑 private Thread leader = null; ``` @@ -7219,7 +7247,7 @@ DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先 if (x == null) throw new NullPointerException(); RunnableScheduledFuture e = (RunnableScheduledFuture)x; - // 队列锁 + // 队列锁,增加删除数据时都要加锁 final ReentrantLock lock = this.lock; lock.lock(); try { @@ -7238,7 +7266,11 @@ DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先 // 向上调整元素的位置,并更新 heapIndex siftUp(i, e); } - // 【插入的元素是头节点,原先的 leader 等待的是原先的头节点,所以 leader 已经无效】 + // 情况1:当前任务是第一个加入到 queue 内的任务,所以在当前任务加入到 queue 之前,take() 线程会直接 + // 到 available 队列不设置超时的挂起,并不会去占用 leader 字段,这时需会唤醒一个线程 让它去消费 + // 情况2:当前任务优先级最高,原堆顶任务可能还未到触发时间,leader 线程设置超时的在 available 挂起 + // 原先的 leader 等待的是原先的头节点,所以 leader 已经无效,需要将 leader 线程唤醒, + // 唤醒之后它会检查堆顶,如果堆顶任务可以被消费,则直接获取走,否则继续成为 leader 等待新堆顶任务 if (queue[0] == e) { // 将 leader 设置为 null leader = null; @@ -7295,11 +7327,13 @@ DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先 ```java private RunnableScheduledFuture finishPoll(RunnableScheduledFuture f) { + // 获取尾索引 int s = --size; // 获取尾节点 RunnableScheduledFuture x = queue[s]; - // 置空 + // 将堆结构最后一个节点占用的 slot 设置为 null,因为该节点要尝试升级成堆顶,会根据特性下调 queue[s] = null; + // s == 0 说明 当前堆结构只有堆顶一个节点,此时不需要做任何的事情 if (s != 0) // 从索引处 0 开始向下调整 siftDown(0, x); @@ -7309,11 +7343,12 @@ DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先 } ``` -* take():阻塞获取头节点,读取当前堆中最小的也就是执行开始时间最近的任务 +* take():阻塞获取头节点,读取当前堆中最小的也就是触发时间最近的任务 ```java public RunnableScheduledFuture take() throws InterruptedException { final ReentrantLock lock = this.lock; + // 保证线程安全 lock.lockInterruptibly(); try { for (;;) { @@ -7323,15 +7358,15 @@ DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先 // 等待队列不空,直至有任务通过 offer 入队并唤醒 available.await(); else { - // 获取头节点的剩延迟时间是否到时 + // 获取头节点的延迟时间是否到时 long delay = first.getDelay(NANOSECONDS); if (delay <= 0) - // 到时了,获取头节点并调整堆,重新选择延迟时间最小的节点放入头部 + // 到达触发时间,获取头节点并调整堆,重新选择延迟时间最小的节点放入头部 return finishPoll(first); // 逻辑到这说明头节点的延迟时间还没到 first = null; - // 说明有 leader 线程在等待获取头节点,需要阻塞等待 + // 说明有 leader 线程在等待获取头节点,当前线程直接去阻塞等待 if (leader != null) available.await(); else { @@ -7339,12 +7374,12 @@ DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先 Thread thisThread = Thread.currentThread(); leader = thisThread; try { + // 在条件队列 available 使用带超时的挂起(堆顶任务.time - now() 纳秒值..) available.awaitNanos(delay); + // 到达阻塞时间时,当前线程会从来 } finally { - // 条件成立的情况: - // 1. 原先 thisThread == leader, 然后堆顶更新了,leader 被置为 null - // 2. 堆顶更新,offer 方法释放锁后,有其它线程通过 take/poll 拿到锁, - // 读到 leader == null,然后将自身更新为leader。 + // t堆顶更新,leader 置为 null,offer 方法释放锁后, + // 有其它线程通过 take/poll 拿到锁,读到 leader == null,然后将自身更新为leader。 if (leader == thisThread) // leader 置为 null 用以接下来判断是否需要唤醒后继线程 leader = null; From 84fad7fbef6e6c15ee438bd3bbe9b70f0fb234c0 Mon Sep 17 00:00:00 2001 From: Seazean Date: Tue, 23 Nov 2021 01:04:54 +0800 Subject: [PATCH 079/168] Update Java Notes --- DB.md | 165 +++++++++++++++++++++++++++++++--------------------------- 1 file changed, 88 insertions(+), 77 deletions(-) diff --git a/DB.md b/DB.md index cd7f505..3333bb5 100644 --- a/DB.md +++ b/DB.md @@ -1153,7 +1153,7 @@ SELECT DISTINCT FROM JOIN - ON + ON -- 连接查询在多表查询部分详解 WHERE GROUP BY @@ -1603,18 +1603,18 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 - - *** -## 约束操作 +## 多表操作 ### 约束分类 +#### 约束介绍 + 约束:对表中的数据进行限定,保证数据的正确性、有效性、完整性! 约束的分类: @@ -1635,7 +1635,7 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 -### 主键约束 +#### 主键约束 * 主键约束特点: @@ -1687,7 +1687,7 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 -### 主键自增 +#### 主键自增 主键自增约束可以为空,并自动增长。删除某条数据不影响自增的下一个数值,依然按照前一个值自增。 @@ -1733,7 +1733,7 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 -### 唯一约束 +#### 唯一约束 唯一约束:约束不能有重复的数据 @@ -1765,7 +1765,7 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 -### 非空约束 +#### 非空约束 * 建表时添加非空约束 @@ -1795,7 +1795,7 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 -### 外键约束 +#### 外键约束 外键约束:让表和表之间产生关系,从而保证数据的准确性! @@ -1850,9 +1850,14 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 DELETE FROM USER WHERE NAME='王五'; ``` - -### 外键级联 + + +*** + + + +#### 外键级联 级联操作:当把主表中的数据进行删除或更新时,从表中有关联的数据的相应操作,包括 RESTRICT、CASCADE、SET NULL 和 NO ACTION @@ -1892,16 +1897,12 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 -## 多表操作 - ### 多表设计 #### 一对一 多表:有多张数据表,而表与表之间有一定的关联关系,通过外键约束实现,分为一对一、一对多、多对多三类 - - 举例:人和身份证 实现原则:在任意一个表建立外键,去关联另外一个表的主键 @@ -2003,27 +2004,29 @@ INSERT INTO stu_course VALUES (NULL,1,1),(NULL,1,2),(NULL,2,1),(NULL,2,2); -### 多表查询 +### 连接查询 -#### 查询格式 +#### 连接原理 -多表查询分类: +连接查询的是两张表有交集的部分数据,如果结果集中的每条记录都是两个表相互匹配的组合,则称这样的结果集为笛卡尔积 -* 内连接查询 -* 外连接查询 -* 子查询 -* 自关联查询 +查询原理:两张表分为驱动表和被驱动表,首先查询驱动表得到数据集,然后根据数据集中的每一条记录再分别到被驱动表中查找匹配,所以驱动表只需要访问一次,被驱动表要访问多次 -多表查询格式:(笛卡儿积) +MySQL 将查询驱动表后得到的记录成为驱动表的扇出,连接查询的成本:单次访问驱动表的成本 + 扇出值 * 单次访问被驱动表的成本,优化器会选择成本最小的表连接顺序(确定谁是驱动表,谁是被驱动表)生成执行计划,进行连接查询,优化方式: -```mysql -SELECT - 列名列表 -FROM - 表名列表 -WHERE - 条件... -``` +* 减少驱动表的扇出 +* 降低访问被驱动表的成本 + +MySQL 提出了一种空间换时间的优化方式,基于块的循环连接,执行连接查询前申请一块固定大小的内存作为连接缓冲区 Join Buffer,先把若干条驱动表中的扇出暂存在缓冲区,每一条被驱动表中的记录一次性的与 Buffer 中多条记录进行匹配(可能是一对多),因为是在内存中完成,所以速度快,并且降低了 I/O 成本。 + +Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 256 KB + +在成本分析时,对于很多张表的连接查询,连接顺序有非常多,MySQL 如果挨着进行遍历计算成本,会消耗很多资源 + +* 提前结束某种连接顺序的成本评估:维护一个全局变量记录当前成本最小的连接方式,如果一种顺序只计算了一部分就已经超过了最小成本,可以提前结束计算 +* 系统变量 optimizer_search_depth:如果连接表的个数小于该变量,就继续穷举分析每一种连接数量,反之只对数量与 depth 值相同的表进行分析,该值越大成本分析的越精确 + +* 系统变量 optimizer_prune_level:控制启发式规则的启用,这些规则就是根据以往经验指定的,不满足规则的连接顺序不分析成本 @@ -2031,9 +2034,11 @@ WHERE -#### 内连接 +#### 内外连接 + +##### 内连接 -查询原理:内连接查询的是两张表有交集的部分数据,分为驱动表和被驱动表,首先查询驱动表得到结果集,然后根据结果集中的每一条记录都分别到被驱动表中查找匹配 +内连接查询,若驱动表中的记录在被驱动表中找不到匹配的记录时,则该记录不会加到最后的结果集 * 显式内连接 @@ -2054,15 +2059,19 @@ WHERE -#### 外连接 +##### 外连接 -* 左外连接:查询左表的全部数据,和左右两张表有交集部分的数据 +外连接查询,若驱动表中的记录在被驱动表中找不到匹配的记录时,则该记录也会加到最后的结果集,只是对于被驱动表中**不匹配过滤条件**的记录,各个字段使用 NULL 填充 + +应用实例:差学生成绩,也想查出缺考的人的成绩 + +* 左外连接:选择左侧的表为驱动表,查询左表的全部数据,和左右两张表有交集部分的数据 ```mysql SELECT 列名 FROM 表名1 LEFT [OUTER] JOIN 表名2 ON 条件; ``` -* 右外连接:查询右表的全部数据,和左右两张表有交集部分的数据 +* 右外连接:选择右侧的表为驱动表,查询右表的全部数据,和左右两张表有交集部分的数据 ```mysql SELECT 列名 FROM 表名1 RIGHT [OUTER] JOIN 表名2 ON 条件; @@ -2077,45 +2086,9 @@ WHERE -#### 子查询 - -子查询概念:查询语句中嵌套了查询语句,**将嵌套查询称为子查询** - -* 结果是单行单列:可以将查询的结果作为另一条语句的查询条件,使用运算符判断 - - ```mysql - SELECT 列名 FROM 表名 WHERE 列名=(SELECT 列名/聚合函数(列名) FROM 表名 [WHERE 条件]); - ``` - -* 结果是多行单列:可以作为条件,使用运算符in或not in进行判断 - - ```mysql - SELECT 列名 FROM 表名 WHERE 列名 [NOT] IN (SELECT 列名 FROM 表名 [WHERE 条件]); - ``` - -* 结果是多行多列:查询的结果可以作为一张虚拟表参与查询 - - ```mysql - SELECT 列名 FROM 表名 [别名],(SELECT 列名 FROM 表名 [WHERE 条件]) [别名] [WHERE 条件]; - - -- 查询订单表orderlist中id大于4的订单信息和所属用户USER信息 - SELECT - * - FROM - USER u, - (SELECT * FROM orderlist WHERE id>4) o - WHERE - u.id=o.uid; - ``` - - - - -*** - -#### 自关联 +#### 关联查询 自关联查询:同一张表中有数据关联,可以多次查询这同一个表 @@ -2182,15 +2155,53 @@ WHERE 1009 宋江 NULL NULL NULL ``` + + + +*** + + + +### 嵌套查询 + +子查询概念:查询语句中嵌套了查询语句,**将嵌套查询称为子查询** + +* 结果是单行单列:可以将查询的结果作为另一条语句的查询条件,使用运算符判断 + + ```mysql + SELECT 列名 FROM 表名 WHERE 列名=(SELECT 列名/聚合函数(列名) FROM 表名 [WHERE 条件]); + ``` + +* 结果是多行单列:可以作为条件,使用运算符in或not in进行判断 + + ```mysql + SELECT 列名 FROM 表名 WHERE 列名 [NOT] IN (SELECT 列名 FROM 表名 [WHERE 条件]); + ``` + +* 结果是多行多列:查询的结果可以作为一张虚拟表参与查询 + + ```mysql + SELECT 列名 FROM 表名 [别名],(SELECT 列名 FROM 表名 [WHERE 条件]) [别名] [WHERE 条件]; + -- 查询订单表orderlist中id大于4的订单信息和所属用户USER信息 + SELECT + * + FROM + USER u, + (SELECT * FROM orderlist WHERE id>4) o + WHERE + u.id=o.uid; + ``` + + *** -### 多表练习 +### 查询练习 -#### 数据准备 +数据准备: ```mysql -- 创建db4数据库 @@ -2241,11 +2252,11 @@ CREATE TABLE us_pro( -#### 数据查询 +**数据查询:** 1. 查询用户的编号、姓名、年龄、订单编号。 分析: - 数据:用户的编号、姓名、年龄在user表,订单编号在orderlist表 + 数据:用户的编号、姓名、年龄在 user 表,订单编号在 orderlist 表 条件:user.id = orderlist.uid ```mysql From 210c98f4859f7821119fe2982b8479cc32bffb19 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 28 Nov 2021 23:20:58 +0800 Subject: [PATCH 080/168] Update Java Notes --- DB.md | 2 +- Java.md | 8 +++--- Prog.md | 8 +++--- SSM.md | 76 +++++++++++++++------------------------------------------ 4 files changed, 28 insertions(+), 66 deletions(-) diff --git a/DB.md b/DB.md index 3333bb5..7569aaf 100644 --- a/DB.md +++ b/DB.md @@ -2432,7 +2432,7 @@ CREATE TABLE us_pro( 工作原理: * 自动提交模式下,如果没有 start transaction 显式地开始一个事务,那么**每个 SQL 语句都会被当做一个事务执行提交操作** - * 手动提交模式下,所有的 SQL 语句都在一个事务中,直到执行了 commit 或 rollback,该事务结束的同时开启另外一个事务 + * 手动提交模式下,所有的 SQL 语句都在一个事务中,直到执行了 commit 或 rollback * 存在一些特殊的命令,在事务中执行了这些命令会马上强制执行 COMMIT 提交事务,如 DDL 语句 (create/drop/alter/table)、lock tables 语句等 diff --git a/Java.md b/Java.md index 2ac7b25..7d9523a 100644 --- a/Java.md +++ b/Java.md @@ -4415,11 +4415,11 @@ TreeSet 集合自排序的方式: * 直接为**对象的类**实现比较器规则接口 Comparable,重写比较方法: 方法:`public int compareTo(Employee o): this 是比较者, o 是被比较者` - + * 比较者大于被比较者,返回正数(升序) * 比较者小于被比较者,返回负数 * 比较者等于被比较者,返回 0 - + * 直接为**集合**设置比较器 Comparator 对象,重写比较方法: 方法:`public int compare(Employee o1, Employee o2): o1 比较者, o2 被比较者` @@ -4489,7 +4489,7 @@ PriorityQueue 是优先级队列,底层存储结构为 Object[],默认实现 常用 API: -* `public boolean offer(E e)`:将指定的元素插入到此优先级队列中**尾部** +* `public boolean offer(E e)`:将指定的元素插入到此优先级队列的**尾部** * `public E poll() `:检索并删除此队列的**头元素**,如果此队列为空,则返回 null * `public E peek()`:检索但不删除此队列的头,如果此队列为空,则返回 null * `public boolean remove(Object o)`:从该队列中删除指定元素(如果存在),删除元素 e 使用 o.equals(e) 比较,如果队列包含多个这样的元素,删除第一个 @@ -6671,7 +6671,7 @@ public class MethodDemo{ public static void main(String[] args) { String[] strs = new String[]{"James", "AA", "John", "Patricia","Dlei" , "Robert","Boom", "Cao" ,"black" , - "Michael", "Linda","cao","after","sBBB"}; + "Michael", "Linda","cao","after","sa"}; // public static void sort(T[] a, Comparator c) // 需求:按照元素的首字符(忽略大小写)升序排序!!! diff --git a/Prog.md b/Prog.md index 82e14e6..0109c33 100644 --- a/Prog.md +++ b/Prog.md @@ -3812,8 +3812,8 @@ JDK8 以前:每个 ThreadLocal 都创建一个 Map,然后用线程作为 Map JDK8 以后:每个 Thread 维护一个 ThreadLocalMap,这个 Map 的 key 是 ThreadLocal 实例本身,value 是真正要存储的值 -* 每个 Thread 线程内部都有一个 Map (ThreadLocalMap) -* Map 里面存储 ThreadLocal 对象(key)和线程的变量副本(value) +* **每个 Thread 线程内部都有一个 Map (ThreadLocalMap)** +* Map 里面存储 ThreadLocal 对象(key)和线程的私有变量(value) * Thread 内部的 Map 是由 ThreadLocal 维护的,由 ThreadLocal 负责向 map 获取和设置线程的变量值 * 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成副本的隔离,互不干扰 @@ -4589,8 +4589,8 @@ java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:**FIFO | 方法类型 | 抛出异常 | 特殊值 | 阻塞 | 超时 | | ---------------- | --------- | -------- | ------ | ------------------ | -| 插入 | add(e) | offer(e) | put(e) | offer(e,time,unit) | -| 移除 | remove() | poll() | take() | poll(time,unit) | +| 插入(尾) | add(e) | offer(e) | put(e) | offer(e,time,unit) | +| 移除(头) | remove() | poll() | take() | poll(time,unit) | | 检查(队首元素) | element() | peek() | 不可用 | 不可用 | * 抛出异常组: diff --git a/SSM.md b/SSM.md index 740fc8f..8702630 100644 --- a/SSM.md +++ b/SSM.md @@ -6755,8 +6755,13 @@ MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ( **支持当前事务**的情况: * TransactionDefinition.PROPAGATION_REQUIRED: 如果当前存在事务,则**加入该事务**;如果当前没有事务,则创建一个新的事务 - * 内外层是相同的事务 - * 在 aMethod 或者在 bMethod 内的任何地方出现异常,事务都会被回滚 + * 内外层是相同的事务,在 aMethod 或者在 bMethod 内的任何地方出现异常,事务都会被回滚 + * 工作流程: + * 线程执行到 serviceA.aMethod() 时,其实是执行的代理 serviceA 对象的 aMethod + * 首先执行事务增强器逻辑(环绕增强),提取事务标签属性,检查当前线程是否绑定 connection 数据库连接资源,没有就调用 datasource.getConnection(),设置事务提交为手动提交 autocommit(false) + * 执行其他增强器的逻辑,然后调用 target 的目标方法 aMethod() 方法,进入 serviceB 的逻辑 + * serviceB 也是先执行事务增强器的逻辑,提取事务标签属性,但此时会检查到线程绑定了 connection,检查注解的传播属性,所以调用 DataSourceUtils.getConnection(datasource) 共享该连接资源,执行完相关的增强和 SQL 后,发现事务并不是当前方法开启的,可以直接返回上层 + * serviceA.aMethod() 继续执行,执行完增强后进行提交事务或回滚事务 * TransactionDefinition.PROPAGATION_SUPPORTS: 如果当前存在事务,则**加入该事务**;如果当前没有事务,则以非事务的方式继续运行 * TransactionDefinition.PROPAGATION_MANDATORY: 如果当前存在事务,则**加入该事务**;如果当前没有事务,则抛出异常 @@ -9144,6 +9149,18 @@ retVal = invocation.proceed():**拦截器链驱动方法** +*** + + + +### 事务 + + + + + + + *** @@ -9239,62 +9256,7 @@ AutowiredAnnotationBeanPostProcessor 间接实现 InstantiationAwareBeanPostProc -*** - - - -#### Transaction - -@EnableTransactionManagement 导入 TransactionManagementConfigurationSelector,该类给 Spring 容器中两个组件: - -* AdviceMode 为 PROXY:导入 AutoProxyRegistrar 和 ProxyTransactionManagementConfiguration(默认) -* AdviceMode 为 ASPECTJ:导入 AspectJTransactionManagementConfiguration(与声明式事务无关) - -AutoProxyRegistrar:给容器中注册 InfrastructureAdvisorAutoProxyCreator,**利用后置处理器机制拦截 bean 以后包装并返回一个代理对象**,代理对象中保存所有的拦截器,利用拦截器的链式机制依次进入每一个拦截器中进行拦截执行(就是 AOP 原理) - -ProxyTransactionManagementConfiguration:是一个 Spring 的事务配置类,注册了三个 Bean: -* BeanFactoryTransactionAttributeSourceAdvisor:事务增强器,利用注解 @Bean 把该类注入到容器中,该增强器有两个字段: - -* TransactionAttributeSource:解析事务注解的相关信息,比如 @Transactional 注解,该类的真实类型是 AnnotationTransactionAttributeSource,构造方法中注册了三个**注解解析器**,解析三种类型的事务注解 Spring、JTA、Ejb3 - -* TransactionInterceptor:**事务拦截器**,代理对象执行拦截器方法时,会调用 TransactionInterceptor 的 invoke 方法,底层调用TransactionAspectSupport.invokeWithinTransaction(),通过 PlatformTransactionManager 控制着事务的提交和回滚,所以事务的底层原理就是通过 AOP 动态织入,进行事务开启和提交 - - ```java - // 创建平台事务管理器对象 - final PlatformTransactionManager tm = determineTransactionManager(txAttr); - // 开启事务 - TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification); - // 执行目标方法(方法引用方式,invocation::proceed,还是调用 proceed) - retVal = invocation.proceedWithInvocation(); - // 提交或者回滚事务 - commitTransactionAfterReturning(txInfo); - ``` - - `createTransactionIfNecessary(tm, txAttr, joinpointIdentification)`: - - * `status = tm.getTransaction(txAttr)`:获取事务状态,开启事务 - - * `doBegin`: **调用 Connection 的 setAutoCommit(false) 开启事务**,就是 JDBC 原生的方式 - - * `prepareTransactionInfo(tm, txAttr, joinpointIdentification, status)`:准备事务信息 - - * `bindToThread() `:利用 ThreadLocal **把当前事务绑定到当前线程**(一个线程对应一个事务) - - 策略模式(Strategy Pattern):使用不同策略的对象实现不同的行为方式,策略对象的变化导致行为的变化,事务也是这种模式,每个事务对应一个新的 connection 对象 - - `commitTransactionAfterReturning(txInfo)`: - - * `txInfo.getTransactionManager().commit(txInfo.getTransactionStatus())`:通过平台事务管理器操作事务 - - * `processRollback(defStatus, false)`:回滚事务,和提交逻辑一样 - - * `processCommit(defStatus)`:提交事务,调用 doCommit(status) - - * `Connection con = txObject.getConnectionHolder().getConnection()`:获取当前线程的连接对象 - * `con.commit()`:事务提交,JDBC 原生的方式 - - From 8fe98eefc6ea30168c75156a9bf6b69223458375 Mon Sep 17 00:00:00 2001 From: Seazean Date: Tue, 30 Nov 2021 00:38:47 +0800 Subject: [PATCH 081/168] Update Java Notes --- SSM.md | 706 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 706 insertions(+) diff --git a/SSM.md b/SSM.md index 8702630..01da621 100644 --- a/SSM.md +++ b/SSM.md @@ -6778,6 +6778,7 @@ MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ( * TransactionDefinition.PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 PROPAGATION_REQUIRED * 如果 ServiceB 异常回滚,可以通过 try-catch 机制执行 ServiceC * 如果 ServiceB 提交, ServiceA 可以根据具体的配置决定是 commit 还是 rollback + * **应用场景**:在查询数据的时候要向数据库中存储一些日志,系统不希望存日志的行为影响到主逻辑,可以使用该传播 requied:必须的、supports:支持的、mandatory:强制的、nested:嵌套的 @@ -9155,7 +9156,712 @@ retVal = invocation.proceed():**拦截器链驱动方法** ### 事务 +#### 解析方法 +##### 标签解析 + +```xml + +``` + +容器启动时会根据注解注册对应的解析器: + +```java +public class TxNamespaceHandler extends NamespaceHandlerSupport { + public void init() { + registerBeanDefinitionParser("advice", new TxAdviceBeanDefinitionParser()); + // 注册解析器 + registerBeanDefinitionParser("annotation-driven", new AnnotationDrivenBeanDefinitionParser()); + registerBeanDefinitionParser("jta-transaction-manager", new JtaTransactionManagerBeanDefinitionParser()); + } +} +protected final void registerBeanDefinitionParser(String elementName, BeanDefinitionParser parser) { + this.parsers.put(elementName, parser); +} +``` + +获取对应的解析器 NamespaceHandlerSupport#findParserForElement: + +```java +private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) { + String localName = parserContext.getDelegate().getLocalName(element); + // 获取对应的解析器 + BeanDefinitionParser parser = this.parsers.get(localName); + // ... + return parser; +} +``` + +调用解析器的方法对 XML 文件进行解析: + +```java +public BeanDefinition parse(Element element, ParserContext parserContext) { + // 向Spring容器注册了一个 BD -> TransactionalEventListenerFactory.class + registerTransactionalEventListenerFactory(parserContext); + String mode = element.getAttribute("mode"); + if ("aspectj".equals(mode)) { + // mode="aspectj" + registerTransactionAspect(element, parserContext); + if (ClassUtils.isPresent("javax.transaction.Transactional", getClass().getClassLoader())) { + registerJtaTransactionAspect(element, parserContext); + } + } + else { + // mode="proxy",默认逻辑,不配置 mode 时 + // 用来向容器中注入一些 BeanDefinition,包括事务增强器、事务拦截器、注解解析器 + AopAutoProxyConfigurer.configureAutoProxyCreator(element, parserContext); + } + return null; +} +``` + + + + + +**** + + + +##### 注解解析 + +@EnableTransactionManagement 导入 TransactionManagementConfigurationSelector,该类给 Spring 容器中两个组件: + +```java +protected String[] selectImports(AdviceMode adviceMode) { + switch (adviceMode) { + // 导入 AutoProxyRegistrar 和 ProxyTransactionManagementConfiguration(默认) + case PROXY: + return new String[] {AutoProxyRegistrar.class.getName(), + ProxyTransactionManagementConfiguration.class.getName()}; + // 导入 AspectJTransactionManagementConfiguration(与声明式事务无关) + case ASPECTJ: + return new String[] {determineTransactionAspectClass()}; + default: + return null; + } +} +``` + +AutoProxyRegistrar:给容器中注册 InfrastructureAdvisorAutoProxyCreator,**利用后置处理器机制拦截 bean 以后包装并返回一个代理对象**,代理对象中保存所有的拦截器,利用拦截器的链式机制依次进入每一个拦截器中进行拦截执行(就是 AOP 原理) + +ProxyTransactionManagementConfiguration:是一个 Spring 的事务配置类,注册了三个 Bean: + +* BeanFactoryTransactionAttributeSourceAdvisor:事务驱动,利用注解 @Bean 把该类注入到容器中,该增强器有两个字段: +* TransactionAttributeSource:解析事务注解的相关信息,真实类型是 AnnotationTransactionAttributeSource,构造方法中注册了三个**注解解析器**,解析 Spring、JTA、Ejb3 三种类型的事务注解 +* TransactionInterceptor:**事务拦截器**,代理对象执行拦截器方法时,调用 TransactionInterceptor 的 invoke 方法,底层调用TransactionAspectSupport.invokeWithinTransaction(),通过 PlatformTransactionManager 控制着事务的提交和回滚,所以事务的底层原理就是通过 AOP 动态织入,进行事务开启和提交 + +注解解析器 SpringTransactionAnnotationParser **解析 @Transactional 注解**: + +```java +protected TransactionAttribute parseTransactionAnnotation(AnnotationAttributes attributes) { + RuleBasedTransactionAttribute rbta = new RuleBasedTransactionAttribute(); + // 从注解信息中获取传播行为 + Propagation propagation = attributes.getEnum("propagation"); + rbta.setPropagationBehavior(propagation.value()); + // 获取隔离界别 + Isolation isolation = attributes.getEnum("isolation"); + rbta.setIsolationLevel(isolation.value()); + rbta.setTimeout(attributes.getNumber("timeout").intValue()); + // 从注解信息中获取 readOnly 参数 + rbta.setReadOnly(attributes.getBoolean("readOnly")); + // 从注解信息中获取 value 信息并且设置 qualifier,表示当前事务指定使用的【事务管理器】 + rbta.setQualifier(attributes.getString("value")); + // 【存放的是 rollback 条件】,回滚规则放在这个集合 + List rollbackRules = new ArrayList<>(); + // 表示事务碰到哪些指定的异常才进行回滚,不指定的话默认是 RuntimeException/Error 非检查型异常菜回滚 + for (Class rbRule : attributes.getClassArray("rollbackFor")) { + rollbackRules.add(new RollbackRuleAttribute(rbRule)); + } + // 与 rollbackFor 功能相同 + for (String rbRule : attributes.getStringArray("rollbackForClassName")) { + rollbackRules.add(new RollbackRuleAttribute(rbRule)); + } + // 表示事务碰到指定的 exception 实现对象不进行回滚,否则碰到其他的class就进行回滚 + for (Class rbRule : attributes.getClassArray("noRollbackFor")) { + rollbackRules.add(new NoRollbackRuleAttribute(rbRule)); + } + for (String rbRule : attributes.getStringArray("noRollbackForClassName")) { + rollbackRules.add(new NoRollbackRuleAttribute(rbRule)); + } + // 设置回滚规则 + rbta.setRollbackRules(rollbackRules); + + return rbta; +} +``` + + + + + +**** + + + + + +#### 驱动方法 + +TransactionInterceptor 事务拦截器的核心驱动方法: + +```java +public Object invoke(MethodInvocation invocation) throws Throwable { + // targetClass 是需要被事务增强器增强的目标类,invocation.getThis() → 目标对象 → 目标类 + Class targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null); + // 参数一是目标方法,参数二是目标类,参数三是方法引用,用来触发驱动方法 + return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed); +} + +protected Object invokeWithinTransaction(Method method, @Nullable Class targetClass, + final InvocationCallback invocation) throws Throwable { + + // 事务属性源信息 + TransactionAttributeSource tas = getTransactionAttributeSource(); + // 提取 @Transactional 注解信息,txAttr 是注解信息的承载对象 + final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null); + // 获取 Spring 配置的事务管理器 + // 首先会检查是否通过XML或注解配置 qualifier,没有就尝试去容器获取,一般情况下为 DatasourceTransactionManager + final PlatformTransactionManager tm = determineTransactionManager(txAttr); + // 权限定类名.方法名,该值用来当做事务名称使用 + final String joinpointIdentification = methodIdentification(method, targetClass, txAttr); + + // 条件成立说明是【声明式事务】 + if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) { + // 用来【开启事务】 + TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification); + + Object retVal; + try { + // This is an 【around advice】: Invoke the next interceptor in the chain. + // 环绕通知,执行目标方法(方法引用方式,invocation::proceed,还是调用 proceed) + retVal = invocation.proceedWithInvocation(); + } + catch (Throwable ex) { + // 执行业务代码时抛出异常,执行回滚逻辑 + completeTransactionAfterThrowing(txInfo, ex); + throw ex; + } + finally { + // 清理事务的信息 + cleanupTransactionInfo(txInfo); + } + // 提交事务的入口 + commitTransactionAfterReturning(txInfo); + return retVal; + } + else { + // 编程式事务,省略 + } +} +``` + + + +*** + + + +#### 开启事务 + +##### 事务绑定 + +创建事务的方法: + +```java +protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm, + @Nullable TransactionAttribute txAttr, + final String joinpointIdentification) { + + // If no name specified, apply method identification as transaction name. + if (txAttr != null && txAttr.getName() == null) { + // 事务的名称: 类的权限定名.方法名 + txAttr = new DelegatingTransactionAttribute(txAttr) { + @Override + public String getName() { + return joinpointIdentification; + } + }; + } + TransactionStatus status = null; + if (txAttr != null) { + if (tm != null) { + // 通过事务管理器根据事务属性创建事务状态对象,事务状态对象一般情况下包装着 事务对象,当然也有可能是null + // 方法上的注解为 @Transactional(propagation = NOT_SUPPORTED || propagation = NEVER) 时 + // 【下一小节详解】 + status = tm.getTransaction(txAttr); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Skipping transactional joinpoint [" + joinpointIdentification + + "] because no transaction manager has been configured"); + } + } + } + // 包装成一个上层的事务上下文对象 + return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status); +} +``` + +TransactionAspectSupport#prepareTransactionInfo:为事务的属性和状态准备一个事务信息对象 + +* `TransactionInfo txInfo = new TransactionInfo(tm, txAttr, joinpointIdentification)`:创建事务信息对象 +* `txInfo.newTransactionStatus(status)`:填充事务的状态信息 +* `txInfo.bindToThread()`:利用 ThreadLocal **把当前事务信息绑定到当前线程**,不同的事务信息会形成一个栈的结构 + * `this.oldTransactionInfo = transactionInfoHolder.get()`:获取其他事务的信息存入 oldTransactionInfo + * `transactionInfoHolder.set(this)`:将当前的事务信息设置到 ThreadLocalMap 中 + + + +*** + + + +##### 事务创建 + +```java +public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException { + // 获取事务的对象 + Object transaction = doGetTransaction(); + boolean debugEnabled = logger.isDebugEnabled(); + + if (definition == null) { + // Use defaults if no transaction definition given. + definition = new DefaultTransactionDefinition(); + } + // 条件成立说明当前是事务重入的情况,事务中有 ConnectionHolder 对象 + if (isExistingTransaction(transaction)) { + // a方法开启事务,a方法内调用b方法,b方法仍然加了 @Transactional 注解,需要检查传播行为 + return handleExistingTransaction(definition, transaction, debugEnabled); + } + + // 逻辑到这说明当前线程没有连接资源,一个连接对应一个事务,没有连接就相当于没有开启事务 + // 检查事务的延迟属性 + if (definition.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) { + throw new InvalidTimeoutException("Invalid transaction timeout", definition.getTimeout()); + } + + // 传播行为是 MANDATORY,没有事务就抛出异常 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) { + throw new IllegalTransactionStateException(); + } + // 需要开启事务的传播行为 + else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED || + definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW || + definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) { + // 什么也没挂起,因为线程并没有绑定事务 + SuspendedResourcesHolder suspendedResources = suspend(null); + try { + // 是否支持同步线程事务,一般是 true + boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); + // 新建一个事务状态信息 + DefaultTransactionStatus status = newTransactionStatus( + definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); + // 【启动事务】 + doBegin(transaction, definition); + // 设置线程上下文变量,方便程序运行期间获取当前事务的一些核心的属性,initSynchronization() 启动同步 + prepareSynchronization(status, definition); + return status; + } + catch (RuntimeException | Error ex) { + // 恢复现场 + resume(null, suspendedResources); + throw ex; + } + } + // 不支持事务的传播行为 + else { + // Create "empty" transaction: no actual transaction, but potentially synchronization. + boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS); + // 创建事务状态对象 + // 参数2 transaction 是 null 说明当前事务状态是未手动开启事,线程上未绑定任何的连接资源,业务程序执行时需要先去 datasource 获取的 conn,是自动提交事务的,不需要 Spring 再提交事务 + // 参数6 suspendedResources 是 null 说明当前事务状态未挂起任何事,当前这个事务执行到后置处理时不需要恢复现场 + return prepareTransactionStatus(definition, null, true, newSynchronization, debugEnabled, null); + } +} +``` + +DataSourceTransactionManager#doGetTransaction:真正获取事务的方法 + +* `DataSourceTransactionObject txObject = new DataSourceTransactionObject()`:**创建事务对象** + +* `txObject.setSavepointAllowed(isNestedAllowed())`:设置事务对象是否支持保存点,由事务管理器控制(默认不支持) + +* `ConnectionHolder conHolder = TransactionSynchronizationManager.getResource(obtainDataSource())`: + + * 从 ThreadLocal 中获取 conHolder 资源,可能拿到 null 或者不是 null + + * 是 null:举例 + + ```java + @Transaction + public void a() {...b.b()....} + ``` + + * 不是 null:执行 b 方法事务增强的前置逻辑时,可以拿到 a 放进去的 conHolder 资源 + + ```java + @Transaction + public void b() {....} + ``` + +* `txObject.setConnectionHolder(conHolder, false)`:将 ConnectionHolder 保存到事务对象内,参数二是 false 代表连接资源是上层事务共享的,不是新建的连接资源 + +* `return txObject`:返回事务的对象 + +DataSourceTransactionManager#doBegin:事务开启的逻辑 + +* `txObject = (DataSourceTransactionObject) transaction`:强转为事务对象 + +* 事务中没有数据库连接资源就要分配: + + `Connection newCon = obtainDataSource().getConnection()`:**获取 JDBC 原生的数据库连接对象** + + `txObject.setConnectionHolder(new ConnectionHolder(newCon), true)`:代表是新开启的事务,新建的连接对象 + +* `previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition)`:修改连接属性 + + * `if (definition != null && definition.isReadOnly())`:注解(或 XML)配置了只读属性,需要设置 + + * `if (..definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT)`:注解配置了隔离级别 + + `int currentIsolation = con.getTransactionIsolation()`:获取连接的隔离界别 + + `previousIsolationLevel = currentIsolation`:保存之前的隔离界别,返回该值 + + ` con.setTransactionIsolation(definition.getIsolationLevel())`:**将当前连接设置为配置的隔离界别** + +* `txObject.setPreviousIsolationLevel(previousIsolationLevel)`:将 Conn 原来的隔离级别保存到事务对象,为了释放 Conn 时重置回原状态 + +* `if (con.getAutoCommit())`:默认会成立,说明还没开启事务 + + `txObject.setMustRestoreAutoCommit(true)`:保存 Conn 原来的事务状态 + + `con.setAutoCommit(false)`:**开启事务,JDBC 原生的方式** + +* `txObject.getConnectionHolder().setTransactionActive(true)`:表示 Holder 持有的 Conn 已经手动开启事务了 + +* `TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder())`:将 ConnectionHolder 对象绑定到 ThreadLocal 内,数据源为 key,为了方便获取手动开启事务的连接对象去执行 SQL + + + +*** + + + +##### 事务重入 + +事务重入的核心处理逻辑: + +```java +private TransactionStatus handleExistingTransaction( TransactionDefinition definition, + Object transaction, boolean debugEnabled){ + // 传播行为是 PROPAGATION_NEVER,需要以非事务方式执行操作,如果当前事务存在则【抛出异常】 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) { + throw new IllegalTransactionStateException(); + } + // 传播行为是 PROPAGATION_NOT_SUPPORTED,以非事务方式运行,如果当前存在事务,则【把当前事务挂起】 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) { + // 挂起事务 + Object suspendedResources = suspend(transaction); + boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS); + // 创建一个非事务的事务状态对象返回 + return prepareTransactionStatus(definition, null, false, newSynchronization, debugEnabled, suspendedResources); + } + // 开启新事物的逻辑 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) { + // 【挂起当前事务】 + SuspendedResourcesHolder suspendedResources = suspend(transaction); + // 【开启新事物】 + } + // 传播行为是 PROPAGATION_NESTED,嵌套事务 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) { + // Spring 默认不支持内嵌事务 + // 【开启方式】: + if (!isNestedTransactionAllowed()) { + throw new NestedTransactionNotSupportedException(); + } + + if (useSavepointForNestedTransaction()) { + // 为当前方法创建一个 TransactionStatus 对象, + DefaultTransactionStatus status = + prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null); + // 创建一个 JDBC 的保存点 + status.createAndHoldSavepoint(); + // 不需要使用同步,直接返回 + return status; + } + else { + // Usually only for JTA transaction,开启一个新事务 + } + } + + // Assumably PROPAGATION_SUPPORTS or PROPAGATION_REQUIRED,【使用当前的事务】 + boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); + return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null); +} +``` + + + +*** + + + +##### 挂起恢复 + +AbstractPlatformTransactionManager#suspend:**挂起事务**,并获得一个上下文信息对象 + +```java +protected final SuspendedResourcesHolder suspend(@Nullable Object transaction) { + // 事务是同步状态的 + if (TransactionSynchronizationManager.isSynchronizationActive()) { + List suspendedSynchronizations = doSuspendSynchronization(); + try { + Object suspendedResources = null; + if (transaction != null) { + // do it + suspendedResources = doSuspend(transaction); + } + //将上层事务绑定在线程上下文的变量全部取出来 + //... + // 通过被挂起的资源和上层事务的上下文变量,创建一个【SuspendedResourcesHolder】返回 + return new SuspendedResourcesHolder(suspendedResources, suspendedSynchronizations, + name, readOnly, isolationLevel, wasActive); + } //... +} +protected Object doSuspend(Object transaction) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; + // 将当前方法的事务对象 connectionHolder 属性置为 null,不和上层共享资源 + // 当前方法有可能是不开启事务或者要开启一个独立的事务 + txObject.setConnectionHolder(null); + // 解绑在线程上的事务 + return TransactionSynchronizationManager.unbindResource(obtainDataSource()); +} +``` + +AbstractPlatformTransactionManager#resume:**恢复现场**,根据挂起资源去恢复线程上下文信息 + +```java +protected final void resume(Object transaction, SuspendedResourcesHolder resourcesHolder) { + if (resourcesHolder != null) { + // 获取被挂起的事务资源 + Object suspendedResources = resourcesHolder.suspendedResources; + if (suspendedResources != null) { + //绑定上一个事务的 ConnectionHolder 到线程上下文 + doResume(transaction, suspendedResources); + } + List suspendedSynchronizations = resourcesHolder.suspendedSynchronizations; + if (suspendedSynchronizations != null) { + //.... + // 将线程上下文变量恢复为上一个事务的挂起现场 + doResumeSynchronization(suspendedSynchronizations); + } + } +} +protected void doResume(@Nullable Object transaction, Object suspendedResources) { + // doSuspend 的逆动作 + TransactionSynchronizationManager.bindResource(obtainDataSource(), suspendedResources); +} +``` + + + + + +*** + + + +#### 提交回滚 + +##### 回滚方式 + +```java +protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) { + // 事务状态信息不为空进入逻辑 + if (txInfo != null && txInfo.getTransactionStatus() != null) { + // 条件二成立 说明目标方法抛出的异常需要回滚事务 + if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) { + try { + // 事务管理器的回滚方法 + txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus()); + } + catch (TransactionSystemException ex2) {} + } + else { + // 执行到这里,说明当前事务虽然抛出了异常,但是该异常并不会导致整个事务回滚 + try { + // 提交事务 + txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); + } + catch (TransactionSystemException ex2) {} + } + } +} +public boolean rollbackOn(Throwable ex) { + // 继承自 RuntimeException 或 error 的是【非检查型异常】,才会归滚事务 + // 如果配置了其他回滚错误,会获取到回滚规则 rollbackRules 进行判断 + return (ex instanceof RuntimeException || ex instanceof Error); +} +``` + +```java +public final void rollback(TransactionStatus status) throws TransactionException { + // 事务已经完成不需要回滚 + if (status.isCompleted()) { + throw new IllegalTransactionStateException(); + } + DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status; + // 开始回滚事务 + processRollback(defStatus, false); +} +``` + +AbstractPlatformTransactionManager#processRollback:事务回滚 + +* `triggerBeforeCompletion(status)`:用来做扩展逻辑,回滚前的前置处理 + +* `if (status.hasSavepoint())`:条件成立说明当前事务是一个**内嵌事务**,当前方法只是复用了上层事务的一个内嵌事务 + + `status.rollbackToHeldSavepoint()`:内嵌事务加入事务时会创建一个保存点,此时恢复至保存点 + +* `if (status.isNewTransaction())`:说明事务是当前连接开启的,需要去回滚事务 + + `doRollback(status)`:真正的的回滚函数 + + * `DataSourceTransactionObject txObject = status.getTransaction()`:获取事务对象 + * `Connection con = txObject.getConnectionHolder().getConnection()`:获取连接对象 + * `con.rollback()`:**JDBC 的方式回滚事务** + +* `else`:当前方法是共享的上层的事务,和上层使用同一个 Conn 资源,**共享的事务不能直接回滚,应该交给上层处理** + + `doSetRollbackOnly(status)`:设置 con.rollbackOnly = true,线程回到上层事务 commit 时会检查该字段,然后执行回滚操作 + +* `triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK)`:回滚的后置处理 + +* `cleanupAfterCompletion(status)`:清理和恢复现场 + + + +*** + + + +##### 提交方式 + +```java +protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo) { + if (txInfo != null && txInfo.getTransactionStatus() != null) { + // 事务管理器的提交方法 + txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); + } +} +``` + +```java +public final void commit(TransactionStatus status) throws TransactionException { + // 已经完成的事务不需要提交了 + if (status.isCompleted()) { + throw new IllegalTransactionStateException(); + } + DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status; + // 条件成立说明是当前的业务强制回滚 + if (defStatus.isLocalRollbackOnly()) { + // 回滚逻辑, + processRollback(defStatus, false); + return; + } + // 成立说明共享当前事务的【下层事务逻辑出错,需要回滚】 + if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) { + // 如果当前事务还是事务重入,会继续抛给上层,最上层事务会进行真实的事务回滚操作 + processRollback(defStatus, true); + return; + } + // 执行提交 + processCommit(defStatus); +} +``` + +AbstractPlatformTransactionManager#processCommit:事务提交 + +* `prepareForCommit(status)`:前置处理 + +* `if (status.hasSavepoint())`:条件成立说明当前事务是一个**内嵌事务**,只是复用了上层事务 + + `status.releaseHeldSavepoint()`:清理保存点,因为没有发生任何异常,所以保存点没有存在的意义了 + +* `if (status.isNewTransaction())`:说明事务是归属于当前连接的,需要去提交事务 + + `doCommit(status)`:真正的提交函数 + + * `Connection con = txObject.getConnectionHolder().getConnection()`:获取连接对象 + * `con.commit()`:**JDBC 的方式提交事务** + +* `doRollbackOnCommitException(status, ex)`:**提交事务出错后进行回滚** + +* ` cleanupAfterCompletion(status)`:清理和恢复现场 + + + +*** + + + +##### 清理现场 + +恢复上层事务: + +```java +protected void cleanupTransactionInfo(@Nullable TransactionInfo txInfo) { + if (txInfo != null) { + // 从当前线程的 ThreadLocal 获取上层的事务信息,将当前事务出栈,继续执行上层事务 + txInfo.restoreThreadLocalStatus(); + } +} +private void restoreThreadLocalStatus() { + // Use stack to restore old transaction TransactionInfo. + transactionInfoHolder.set(this.oldTransactionInfo); +} +``` + +当前层级事务结束时的清理: + +```java +private void cleanupAfterCompletion(DefaultTransactionStatus status) { + // 设置当前方法的事务状态为完成状态 + status.setCompleted(); + if (status.isNewSynchronization()) { + // 清理线程上下文变量以及扩展点注册的 sync + TransactionSynchronizationManager.clear(); + } + // 事务是当前线程开启的 + if (status.isNewTransaction()) { + // 解绑资源 + doCleanupAfterCompletion(status.getTransaction()); + } + // 条件成立说明当前事务执行的时候,【挂起了一个上层的事务】 + if (status.getSuspendedResources() != null) { + Object transaction = (status.hasTransaction() ? status.getTransaction() : null); + // 恢复上层事务现场 + resume(transaction, (SuspendedResourcesHolder) status.getSuspendedResources()); + } +} +``` + +DataSourceTransactionManager#doCleanupAfterCompletion:清理工作 + +* `TransactionSynchronizationManager.unbindResource(obtainDataSource())`:解绑数据库资源 + +* `if (txObject.isMustRestoreAutoCommit())`:是否恢复连接,Conn 归还到 DataSource**,归还前需要恢复到申请时的状态** + + `con.setAutoCommit(true)`:恢复链接为自动提交 + +* `DataSourceUtils.resetConnectionAfterTransaction(con, txObject.getPreviousIsolationLevel())`:恢复隔离级别 + +* `DataSourceUtils.releaseConnection(con, this.dataSource)`:将连接归还给数据库连接池 + +* `txObject.getConnectionHolder().clear()`:清理 ConnectionHolder 资源 From a1aa654a2be69b9ba8c51edcb12b1aa5ee467753 Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 1 Dec 2021 23:43:03 +0800 Subject: [PATCH 082/168] Update Java Notes --- DB.md | 104 +++++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 70 insertions(+), 34 deletions(-) diff --git a/DB.md b/DB.md index 7569aaf..df7af85 100644 --- a/DB.md +++ b/DB.md @@ -958,11 +958,12 @@ DDL 中的临时表 tmp_table 是在 Server 层创建的,Online DDL 中的临 | DOUBLE | 小数类型 | | DATE | 日期,只包含年月日:yyyy-MM-dd | | DATETIME | 日期,包含年月日时分秒:yyyy-MM-dd HH:mm:ss | - | TIMESTAMP | 时间戳类型,包含年月日时分秒:yyyy-MM-dd HH:mm:ss
如果不给这个字段赋值或赋值为null,则默认使用当前的系统时间 | - | VARCHAR | 字符串
name varchar(20):姓名最大20个字符:zhangsan8个字符,张三2个字符 | - + | TIMESTAMP | 时间戳类型,包含年月日时分秒:yyyy-MM-dd HH:mm:ss
如果不给这个字段赋值或赋值为 NULL,则默认使用当前的系统时间 | + | CHAR | 字符串,定长类型 | + | VARCHAR | 字符串,**变长类型**
name varchar(20) 代表姓名最大 20 个字符:zhangsan 8 个字符,张三 2 个字符 | + `INT(n)`:n 代表位数 - + * 3:int(9)显示结果为 000000010 * 3:int(3)显示结果为 010 @@ -1286,8 +1287,8 @@ LIMIT | AND 或 && | 并且 | | OR 或 \|\| | 或者 | | NOT 或 ! | 非,不是 | - | UNION | 对两个结果集进行并集操作,不包括重复行,同时进行默认规则的排序 | - | UNION ALL | 对两个结果集进行并集操作,包括重复行,不进行排序 | + | UNION | 对两个结果集进行并集操作并进行去重,同时进行默认规则的排序 | + | UNION ALL | 对两个结果集进行并集操作不进行去重,不进行排序 | * 例如: @@ -2164,7 +2165,11 @@ Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 ### 嵌套查询 -子查询概念:查询语句中嵌套了查询语句,**将嵌套查询称为子查询** +#### 查询分类 + +查询语句中嵌套了查询语句,**将嵌套查询称为子查询**,FROM 子句后面的子查询的结果集称为派生表 + +根据结果分类: * 结果是单行单列:可以将查询的结果作为另一条语句的查询条件,使用运算符判断 @@ -2172,7 +2177,7 @@ Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 SELECT 列名 FROM 表名 WHERE 列名=(SELECT 列名/聚合函数(列名) FROM 表名 [WHERE 条件]); ``` -* 结果是多行单列:可以作为条件,使用运算符in或not in进行判断 +* 结果是多行单列:可以作为条件,使用运算符 IN 或 NOT IN 进行判断 ```mysql SELECT 列名 FROM 表名 WHERE 列名 [NOT] IN (SELECT 列名 FROM 表名 [WHERE 条件]); @@ -2193,6 +2198,26 @@ Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 u.id=o.uid; ``` +相关性分类: + +* 不相关子查询:子查询不依赖外层查询的值,可以单独运行出结果 +* 相关子查询:子查询的执行需要依赖外层查询的值 + + + +**** + + + +#### 查询优化 + +不相关子查询的结果集会被写入一个临时表,并且在写入时去重,该过程称为物化,存储结果集的临时表称为物化表 + +系统变量 tmp_table_size 或者 max_heap_table_size 为表的最值 + +* 小于系统变量时,内存中可以保存,会为建立基于内存的 MEMORY 存储引擎的临时表,并建立哈希索引 +* 大于任意一个系统变量时,物化表会使用基于磁盘的存储引擎来保存结果集中的记录,索引类型为 B+ 树, + *** @@ -4717,7 +4742,7 @@ CREATE INDEX idx_area ON table_name(area(7)); SELECT * FROM table_test WHERE key1 = 'a' AND key3 = 'b'; # key1 和 key3 列都是单列索引、二级索引 ``` - 从不同索引中扫描到的记录的 id 值取交集(相同 id),然后执行回表操作,要求从每个二级索引获取到的记录都是按照主键值排序 + 从不同索引中扫描到的记录的 id 值取**交集**(相同 id),然后执行回表操作,要求从每个二级索引获取到的记录都是按照主键值排序 * Union 索引合并: @@ -4725,7 +4750,7 @@ CREATE INDEX idx_area ON table_name(area(7)); SELECT * FROM table_test WHERE key1 = 'a' OR key3 = 'b'; ``` - 从不同索引中扫描到的记录的 id 值取并集,然后执行回表操作,要求从每个二级索引获取到的记录都是按照主键值排序 + 从不同索引中扫描到的记录的 id 值取**并集**,然后执行回表操作,要求从每个二级索引获取到的记录都是按照主键值排序 * Sort-Union 索引合并 @@ -4871,16 +4896,16 @@ EXPLAIN SELECT * FROM table_1 WHERE id = 1; | 字段 | 含义 | | ------------- | ------------------------------------------------------------ | -| id | select查询的序列号,表示查询中执行select子句或操作表的顺序 | +| id | SELECT 的序列号 | | select_type | 表示 SELECT 的类型 | -| table | 输出结果集的表,显示这一步所访问数据库中表名称,有时不是真实的表名字,可能是简称 | +| table | 访问数据库中表名称,有时可能是简称或者临时表名称() | | type | 表示表的连接类型 | | possible_keys | 表示查询时,可能使用的索引 | | key | 表示实际使用的索引 | | key_len | 索引字段的长度 | -| ref | 列与索引的比较,表示表的连接匹配条件,即哪些列或常量被用于查找索引列上的值 | +| ref | 表示与索引列进行等值匹配的对象,常数、某个列、函数等,type 必须在(range, const] 之间,左闭右开 | | rows | 扫描出的行数,表示 MySQL 根据表统计信息及索引选用情况,**估算**的找到所需的记录扫描的行数 | -| filtered | 按表条件过滤的行百分比 | +| filtered | 条件过滤的行百分比,单表查询没意义,用于连接查询中对驱动表的扇出进行过滤,查询优化器预测所有扇出值满足剩余查询条件的百分比,相乘以后表示多表查询中还要对被驱动执行查询的次数 | | extra | 执行情况的说明和描述 | MySQL 执行计划的局限: @@ -4907,9 +4932,9 @@ MySQL 执行计划的局限: ##### id -SQL 执行的顺序的标识,SQL 从大到小的执行 +id 代表 SQL 执行的顺序的标识,每个 SELECT 关键字对应一个唯一 id,所以在同一个 SELECT 关键字中的表的 id 都是相同的。SELECT 后的 FROM 可以跟随多个表,每个表都会对应一条记录,这些记录的 id 都是相同的, -* id 相同时,执行顺序由上至下 +* id 相同时,执行顺序由上至下。连接查询的执行计划,记录的 id 值都是相同的,出现在前面的表为驱动表,后面为被驱动表 ```mysql EXPLAIN SELECT * FROM t_role r, t_user u, user_role ur WHERE r.id = ur.role_id AND u.id = ur.user_id ; @@ -4933,6 +4958,8 @@ SQL 执行的顺序的标识,SQL 从大到小的执行 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-explain之id相同和不同.png) +* id 为 NULL 时代表的是临时表 + *** @@ -4946,13 +4973,18 @@ SQL 执行的顺序的标识,SQL 从大到小的执行 | select_type | 含义 | | ------------------ | ------------------------------------------------------------ | | SIMPLE | 简单的 SELECT 查询,查询中不包含子查询或者 UNION | -| PRIMARY | 查询中若包含任何复杂的子查询,最外层查询标记为该标识 | -| SUBQUERY | 在 SELECT 或 WHERE 中包含子查询,该子查询被标记为:SUBQUERY | -| DEPENDENT SUBQUERY | 在 SUBQUERY 基础上,子查询中的第一个SELECT,取决于外部的查询 | -| DERIVED | 在 FROM 列表中包含的子查询,被标记为 DERIVED(衍生),MYSQL会递归执行这些子查询,把结果放在临时表中 | -| UNION | UNION 中的第二个或后面的 SELECT 语句,则标记为UNION ; 若 UNION 包含在 FROM 子句的子查询中,外层 SELECT 将被标记为:DERIVED | -| DEPENDENT UNION | UNION 中的第二个或后面的SELECT语句,取决于外面的查询 | -| UNION RESULT | UNION 的结果,UNION 语句中第二个 SELECT 开始后面所有 SELECT | +| PRIMARY | 查询中若包含任何复杂的子查询,最外层(也就是最左侧)查询标记为该标识 | +| UNION | 对于 UNION 或者 UNION ALL 的复杂查询,除了最左侧的查询,其余的小查询都是 UNION | +| UNION RESULT | UNION 需要使用临时表进行去重,临时表的是 UNION RESULT | +| DEPENDENT UNION | 对于 UNION 或者 UNION ALL 的复杂查询,如果各个小查询都依赖外层查询,是相关子查询,除了最左侧的小查询为 DEPENDENT SUBQUERY,其余都是 DEPENDENT UNION | +| SUBQUERY | 子查询不是相关子查询,该子查询第一个 SELECT 代表的查询就是这种类型,会进行物化(该子查询只需要执行一次) | +| DEPENDENT SUBQUERY | 子查询是相关子查询,该子查询第一个 SELECT 代表的查询就是这种类型,不会物化(该子查询需要执行多次) | +| DERIVED | 在 FROM 列表中包含的子查询,被标记为 DERIVED(衍生),也就是生成物化派生表的这个子查询 | +| MATERIALIZED | 将子查询物化后与与外层进行连接查询,生成物化表的子查询 | + +子查询为 DERIVED:`SELECT * FROM (SELECT key1 FROM t1) AS derived_1 WHERE key1 > 10` + +子查询为 MATERIALIZED:`SELECT * FROM t1 WHERE key1 IN (SELECT key1 FROM t2)` @@ -4964,16 +4996,20 @@ SQL 执行的顺序的标识,SQL 从大到小的执行 对表的访问方式,表示 MySQL 在表中找到所需行的方式,又称访问类型 -| type | 含义 | -| ------ | ------------------------------------------------------------ | -| ALL | Full Table Scan,MySQL 将遍历全表以找到匹配的行,全表扫描,如果是 InnoDB 引擎是扫描聚簇索引 | -| index | Full Index Scan,index 与 ALL 区别为 index 类型只遍历索引树 | -| range | 索引范围扫描,常见于 between、<、> 等的查询 | -| ref | 非唯一性索引扫描,返回匹配某个单独值的所有记录,本质上也是一种索引访问 | -| eq_ref | 唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配,常见于主键或唯一索引扫描 | -| const | 通过主键或者唯一索引来定位一条记录 | -| system | system 是 const 类型的特例,当查询的表只有一行的情况下,使用 system | -| NULL | MySQL 在优化过程中分解语句,执行时甚至不用访问表或索引 | +| type | 含义 | +| --------------- | ------------------------------------------------------------ | +| ALL | 全表扫描,如果是 InnoDB 引擎是扫描聚簇索引 | +| index | 可以使用覆盖索引,但需要扫描全部索引 | +| range | 索引范围扫描,常见于 between、<、> 等的查询 | +| index_subquery | 子查询可以普通索引,则子查询的 type 为 index_subquery | +| unique_subquery | 子查询可以使用主键或唯一二级索引,则子查询的 type 为 index_subquery | +| index_merge | 索引合并 | +| ref_or_null | 非唯一性索引(普通二级索引)并且可以存储 NULL,进行等值匹配 | +| ref | 非唯一性索引与常量等值匹配 | +| eq_ref | 唯一性索引(主键或不存储 NULL 的唯一二级索引)进行等值匹配,如果二级索引是联合索引,那么所有联合的列都要进行等值匹配 | +| const | 通过主键或者唯一二级索引与常量进行等值匹配 | +| system | system 是 const 类型的特例,当查询的表只有一条记录的情况下,使用 system | +| NULL | MySQL 在优化过程中分解语句,执行时甚至不用访问表或索引 | 从上到下,性能从差到好,一般来说需要保证查询至少达到 range 级别, 最好达到 ref @@ -4992,7 +5028,7 @@ possible_keys: key: -* 显示MySQL在查询中实际使用的索引,若没有使用索引,显示为 NULL +* 显示 MySQL 在查询中实际使用的索引,若没有使用索引,显示为 NULL * 查询中若使用了**覆盖索引**,则该索引可能出现在 key 列表,不出现在 possible_keys key_len: From c03d5e02383c20de8a997257f4eb293297154c3b Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 2 Dec 2021 23:33:07 +0800 Subject: [PATCH 083/168] Update Java Notes --- Java.md | 32 ++++++++++++++++-- SSM.md | 100 +++++++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 115 insertions(+), 17 deletions(-) diff --git a/Java.md b/Java.md index 7d9523a..0ba5164 100644 --- a/Java.md +++ b/Java.md @@ -10400,7 +10400,7 @@ FullGC 同时回收新生代、老年代和方法区,只会存在一个 FullGC * 老年代空间不足: * 为了避免引起的 Full GC,应当尽量不要创建过大的对象以及数组 - * 通过 -Xmn 参数调整新生代的大小,让对象尽量在新生代被回收掉不进入老年代,可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间 + * 通过 -Xmn 参数调整新生代的大小,让对象尽量在新生代被回收掉不进入老年代,可以通过 `-XX:MaxTenuringThreshold` 调大对象进入老年代的年龄,让对象在新生代多存活一段时间 * 空间分配担保失败 @@ -10748,7 +10748,9 @@ Java 语言提供了对象终止(finalization)机制来允许开发人员提 -#### 无用类 +#### 无用属性 + +##### 无用类 方法区主要回收的是无用的类 @@ -10766,6 +10768,32 @@ Java 语言提供了对象终止(finalization)机制来允许开发人员提 +##### 废弃常量 + +在常量池中存在字符串 "abc",如果当前没有任何 String 对象引用该常量,说明常量 "abc" 是废弃常量,如果这时发生内存回收的话**而且有必要的话**(内存不够用),"abc" 就会被系统清理出常量池 + + + +*** + + + +##### 静态变量 + +类加载时(第一次访问),这个类中所有静态成员就会被加载到静态变量区,该区域的成员一旦创建,直到程序退出才会被回收 + +如果是静态引用类型的变量,静态变量区只存储一份对象的引用地址,真正的对象在堆内,如果要回收该对象可以设置引用为 null + + + +参考文章:https://blog.csdn.net/zhengzhb/article/details/7331354 + + + +*** + + + ### 回收算法 #### 标记清除 diff --git a/SSM.md b/SSM.md index 01da621..8bad561 100644 --- a/SSM.md +++ b/SSM.md @@ -2930,8 +2930,8 @@ Spring 优点: ### 基本概述 -- IoC(Inversion Of Control)控制反转,Spring反向控制应用程序所需要使用的外部资源 -- **Spring 控制的资源全部放置在 Spring 容器中,该容器称为 IoC 容器** +- IoC(Inversion Of Control)控制反转,Spring 反向控制应用程序所需要使用的外部资源 +- **Spring 控制的资源全部放置在 Spring 容器中,该容器称为 IoC 容器**(存放实例对象) - 官方网站:https://spring.io/ → Projects → spring-framework → LEARN → Reference Doc ![](https://gitee.com/seazean/images/raw/master/Frame/Spring-IOC介绍.png) @@ -3311,7 +3311,7 @@ IoC 和 DI 的关系:IoC 与 DI 是同一件事站在不同角度看待问题 代码实现: -* DAO层:要注入的资源 +* DAO 层:要注入的资源 ```java public interface UserDao { @@ -3327,7 +3327,7 @@ IoC 和 DI 的关系:IoC 与 DI 是同一件事站在不同角度看待问题 } ``` -* Service业务层 +* Service 业务层 ```java public interface UserService { @@ -3357,7 +3357,7 @@ IoC 和 DI 的关系:IoC 与 DI 是同一件事站在不同角度看待问题 } ``` -* 配置applicationContext.xml +* 配置 applicationContext.xml ```xml @@ -3418,7 +3418,7 @@ IoC 和 DI 的关系:IoC 与 DI 是同一件事站在不同角度看待问题 代码实现: -* DAO层:要注入的资源 +* DAO 层:要注入的资源 ```java public class UserDaoImpl implements UserDao{ @@ -4315,6 +4315,8 @@ public class UserServiceImpl implements UserService { } ``` +一个对象的执行顺序:Constructor >> @Autowired(注入属性) >> @PostConstruct(初始化逻辑) + *** @@ -4394,7 +4396,9 @@ private String username; -##### 属性填充 +##### 自动装配 + +###### 属性注入 名称:@Autowired、@Qualifier @@ -4426,7 +4430,7 @@ private UserDao userDao; -##### 属性设置 +###### 优先注入 名称:@Primary @@ -4451,7 +4455,7 @@ public class ClassName{} -##### 注解对比 +###### 注解对比 名称:@Inject、@Named、@Resource @@ -4464,11 +4468,77 @@ public class ClassName{} - type:设置注入的 bean 的类型,接收的参数为 Class 类型 -**@Autowired 和 @Resource之间的区别**: +@Autowired 和 @Resource之间的区别: + +* @Autowired 默认是**按照类型装配**注入,默认情况下它要求依赖对象必须存在(可以设置它 required 属性为 false) + +* @Resource 默认**按照名称装配**注入,只有当找不到与名称匹配的 bean 才会按照类型来装配注入 + + + +**** + + + +##### 静态注入 + +Spring 容器管理的都是实例对象,**@Autowired 依赖注入的都是容器内的对象实例**,在 Java 中 static 修饰的静态属性(变量和方法)是属于类的,而非属于实例对象 + +当类加载器加载静态变量时,Spring 上下文尚未加载,所以类加载器不会在 Bean 中正确注入静态类 + +```java +@Component +public class TestClass { + @Autowired + private static Component component; + + // 调用静态组件的方法 + public static void testMethod() { + component.callTestMethod(); + } +} +// 编译正常,但运行时报java.lang.NullPointerException,所以在调用testMethod()方法时,component变量还没被初始化 +``` + +解决方法: + +* @Autowired 注解到类的构造函数上,Spring 扫描到 Component 的 Bean,然后赋给静态变量 component + + ```java + @Component + public class TestClass { + private static Component component; + + @Autowired + public TestClass(Component component) { + TestClass.component = component; + } + + public static void testMethod() { + component.callTestMethod(); + } + } + ``` + +* @Autowired 注解到静态属性的 setter 方法上 + +* 使用 @PostConstruct 注解一个方法,在方法内为 static 静态成员赋值 + +* 使用 Spring 框架工具类获取 bean,定义成局部变量使用 + + ```java + public class TestClass { + // 调用静态组件的方法 + public static void testMethod() { + Component component = SpringApplicationContextUtil.getBean("component"); + component.callTestMethod(); + } + } + ``` + -* @Autowired 默认是按照类型装配注入的,默认情况下它要求依赖对象必须存在(可以设置它required属性为false) -* @Resource 默认按照名称来装配注入,只有当找不到与名称匹配的bean才会按照类型来装配注入 +参考文章:http://jessehzx.top/2018/03/18/spring-autowired-static-field/ @@ -4482,7 +4552,7 @@ public class ClassName{} 类型:类注解 -作用:加载properties文件中的属性值 +作用:加载 properties 文件中的属性值 格式: @@ -4586,9 +4656,9 @@ public class ClassName { @DependsOn -- 微信订阅号,发布消息和订阅消息的bean的加载顺序控制(先开订阅,再发布) +- 微信订阅号,发布消息和订阅消息的 bean 的加载顺序控制(先开订阅,再发布) -- 双11活动期间,零点前是结算策略A,零点后是结算策略B,策略B操作的数据为促销数据。策略B加载顺序与促销数据的加载顺序 +- 双 11 活动,零点前是结算策略 A,零点后是结算策略 B,策略 B 操作的数据为促销数据,策略 B 加载顺序与促销数据的加载顺序 @Lazy From 006f9552a9a1bebdcf337712bcb9c2cf4bf0ad9d Mon Sep 17 00:00:00 2001 From: Seazean Date: Fri, 3 Dec 2021 01:10:25 +0800 Subject: [PATCH 084/168] Update Java Notes --- SSM.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/SSM.md b/SSM.md index 8bad561..9c01b44 100644 --- a/SSM.md +++ b/SSM.md @@ -3946,9 +3946,16 @@ Mybatis 核心配置文件消失 - 类型别名交由 spring 处理 -业务发起使用spring上下文对象获取对应的bean +DAO 接口不需要创建实现类,MyBatis-Spring 提供了一个动态代理的实现 **MapperFactoryBean**,这个类可以让直接注入数据映射器接口到 service 层 bean 中,底层将会动态代理创建类 -**原理**:DAO 接口不需要创建实现类,MyBatis-Spring 提供了一个动态代理的实现 **MapperFactoryBean**,这个类可以让直接注入数据映射器接口到 service 层 bean 中,底层将会动态代理创建类 +整合原理:利用 Spring 框架的 SPI 机制,在 META-INF 目录的 spring.handlers 中给 Spring 容器中导入 NamespaceHandler 类 + +* NamespaceHandler 的 init 方法注册 bean 信息的解析器 MapperScannerBeanDefinitionParser +* 解析器在 Spring 容器创建过程中去解析 mapperScanner 标签,解析出的属性填充到 MapperScannerConfigurer 中 + +* MapperScannerConfigurer 实现了 BeanDefinitionRegistryPostProcessor 接口,重写 postProcessBeanDefinitionRegistry() 方法,可以扫描到 MyBatis 的 Mapper + +整合代码: * pom.xml,导入坐标 @@ -4911,7 +4918,7 @@ FactoryBean与 BeanFactory 区别: } ``` -* MapperScannerConfigurer 实现了 BeanDefinitionRegistryPostProcessor 接口,重写 postProcessBeanDefinitionRegistry() 方法,可以扫描到 MyBatis 的 Mapper + From 784aab5c004ab3deda10bf85ddf3cdfb6c991136 Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 6 Dec 2021 00:02:46 +0800 Subject: [PATCH 085/168] Update Java Notes --- Prog.md | 32 +++++--- SSM.md | 9 ++- Web.md | 234 ++++++++++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 223 insertions(+), 52 deletions(-) diff --git a/Prog.md b/Prog.md index 0109c33..5fcf710 100644 --- a/Prog.md +++ b/Prog.md @@ -595,7 +595,7 @@ t.start(); 用户线程:平常创建的普通线程 -守护线程:服务于用户线程,只要其它非守护线程运行结束了,即使守护线程代码没有执行完,也会强制结束。守护进程是脱离于终端并且在后台运行的进程,脱离终端是为了避免在执行的过程中的信息在终端上显示 +守护线程:服务于用户线程,只要其它非守护线程运行结束了,即使守护线程代码没有执行完,也会强制结束。守护进程是**脱离于终端并且在后台运行的进程**,脱离终端是为了避免在执行的过程中的信息在终端上显示 说明:当运行的线程都是守护线程,Java 虚拟机将退出,因为普通线程执行完后,JVM 是守护线程,不会继续运行下去 @@ -4842,7 +4842,7 @@ public class LinkedBlockingQueue extends AbstractQueue ##### 成员属性 -与其他 BlockingQueue 不同,SynchronousQueue 是一个不存储元素的 BlockingQueue,每一个生产者必须阻塞匹配到一个消费者 +SynchronousQueue 是一个不存储元素的 BlockingQueue,**每一个生产者必须阻塞匹配到一个消费者** 成员变量: @@ -13644,7 +13644,7 @@ epoll 的特点: * epoll 的时间复杂度 O(1),epoll 理解为 event poll,不同于忙轮询和无差别轮询,调用 epoll_wait **只是轮询就绪链表**。当监听列表有设备就绪时调用回调函数,把就绪 fd 放入就绪链表中,并唤醒在 epoll_wait 中阻塞的进程,所以 epoll 实际上是**事件驱动**(每个事件关联上fd)的,降低了 system call 的时间复杂度 * epoll 内核中根据每个 fd 上的 callback 函数来实现,只有活跃的 socket 才会主动调用 callback,所以使用 epoll 没有前面两者的线性下降的性能问题,效率提高 -* epoll 注册新的事件都是注册到到内核中 epoll 句柄中,不需要每次调用 epoll_wait 时重复拷贝,对比前面两种,epoll 只需要将描述符从进程缓冲区向内核缓冲区拷贝一次。 epoll 也可以利用 **mmap() 文件映射内存**加速与内核空间的消息传递,减少复制开销 +* epoll 注册新的事件都是注册到到内核中 epoll 句柄中,不需要每次调用 epoll_wait 时重复拷贝,对比前面两种,epoll 只需要将描述符从进程缓冲区向内核缓冲区**拷贝一次**,epoll 也可以利用 **mmap() 文件映射内存**加速与内核空间的消息传递(只是可以用) * 前面两者要把 current 往设备等待队列中挂一次,epoll 也只把 current 往等待队列上挂一次,但是这里的等待队列并不是设备等待队列,只是一个 epoll 内部定义的等待队列,这样可以节省开销 * epoll 对多线程编程更有友好,一个线程调用了 epoll_wait() 另一个线程关闭了同一个描述符,也不会产生像 select 和 poll 的不确定情况 @@ -14069,7 +14069,7 @@ ServerSocket 类: * 构造方法:`public ServerSocket(int port)` * 常用API:`public Socket accept()`,**阻塞等待**接收一个客户端的 Socket 管道连接请求,连接成功返回一个 Socket 对象 - 三次握手后 TCP 连接建立成功,服务器内核会把连接从 SYN 半连接队列中移出,移入 accept 全连接队列,等待进程调用 accept 函数时把连接取出。如果进程不能及时调用 accept 函数,就会造成 accept 队列溢出,最终导致建立好的 TCP 连接被丢弃 + 三次握手后 TCP 连接建立成功,服务器内核会把连接从 SYN 半连接队列(一次握手时在服务端建立的队列)中移出,移入 accept 全连接队列,等待进程调用 accept 函数时把连接取出。如果进程不能及时调用 accept 函数,就会造成 accept 队列溢出,最终导致建立好的 TCP 连接被丢弃 @@ -14122,7 +14122,7 @@ public class ClientDemo { // 1.客户端要请求于服务端的socket管道连接。 Socket socket = new Socket("127.0.0.1", 8080); // 2.从socket通信管道中得到一个字节输出流 - OutputStream os = new socket.getOutputStream(); + OutputStream os = socket.getOutputStream(); // 3.把低级的字节输出流包装成高级的打印流。 PrintStream ps = new PrintStream(os); // 4.开始发消息出去 @@ -14980,10 +14980,16 @@ Channel 实现类: * 通过 FileInputStream 获取的 Channel 只能读 * 通过 FileOutputStream 获取的 Channel 只能写 * 通过 RandomAccessFile 是否能读写根据构造 RandomAccessFile 时的读写模式决定 + * DatagramChannel:通过 UDP 读写网络中的数据通道 + * SocketChannel:通过 TCP 读写网络中的数据 -* ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个SocketChannel。 - 提示:ServerSocketChanne 类似 ServerSocket , SocketChannel 类似 Socket + +* ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel + + 提示:ServerSocketChanne 类似 ServerSocket、SocketChannel 类似 Socket + + @@ -14999,7 +15005,7 @@ Channel 实现类: * 通过通道的静态方法 `open()` 打开并返回指定通道 * 使用 Files 类的静态方法 `newByteChannel()` 获取字节通道 -Channel 基本操作: +Channel 基本操作:**读写都是相对于内存来看,也就是缓冲区** | 方法 | 说明 | | ------------------------------------------ | -------------------------------------------------------- | @@ -15011,7 +15017,13 @@ Channel 基本操作: | FileChannel position(long newPosition) | 设置此通道的文件位置 | | public abstract long size() | 返回此通道的文件的当前大小 | -**读写都是相对于内存来看,也就是缓冲区** +**SelectableChannel 的操作 API**: + +| 方法 | 说明 | +| -------------------------------------------------------- | ------------------------------------------------------------ | +| SocketChannel accept() | 如果通道处于非阻塞模式,没有请求连接时此方法将立即返回 NULL,否则将阻塞直到有新的连接或发生 I/O 错误,**通过该方法返回的套接字通道将处于阻塞模式** | +| SelectionKey register(Selector sel, int ops) | 将通道注册到选择器上,并指定监听事件 | +| SelectionKey register(Selector sel, int ops, Object att) | 将通道注册到选择器上,并在当前通道绑定一个附件对象,Object 代表可以是任何类型 | @@ -15225,7 +15237,7 @@ public class ChannelTest { * 连接 : SelectionKey.OP_CONNECT (8) * 接收 : SelectionKey.OP_ACCEPT (16) * 若不止监听一个事件,使用位或操作符连接:`int interest = SelectionKey.OP_READ | SelectionKey.OP_WRITE` -* 参数三:**关联一个附件**,可以是任何对象 +* 参数三:可以关联一个附件,可以是任何对象 **Selector API**: diff --git a/SSM.md b/SSM.md index 9c01b44..b045da3 100644 --- a/SSM.md +++ b/SSM.md @@ -44,7 +44,7 @@ SqlSessionFactory:获取 SqlSession 构建者对象的工厂接口 SqlSession:构建者对象接口,用于执行 SQL、管理事务、接口代理 -* SqlSession 代表和数据库的一次会话,用完必须关闭 +* SqlSession **代表和数据库的一次会话**,用完必须关闭 * SqlSession 和 Connection 一样都是非线程安全,每次使用都应该去获取新的对象 注:**update 数据需要提交事务,或开启默认提交** @@ -1529,6 +1529,11 @@ public class Blog { * SqlSession 相同,手动清除了一级缓存,调用 `sqlSession.clearCache()` * SqlSession 相同,执行 commit 操作或者执行插入、更新、删除,清空 SqlSession 中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,**避免脏读** +Spring 整合 MyBatis 后,一级缓存作用: + +* 未开启事务的情况,每次查询 Spring 都会创建新的 SqlSession,因此一级缓存失效 +* 开启事务的情况,Spring 使用 ThreadLocal 获取当前资源绑定同一个 SqlSession,因此此时一级缓存是有效的 + 测试一级缓存存在 ```java @@ -2644,7 +2649,7 @@ Executor#query(): * `interceptorChain.pluginAll(statementHandler)`:拦截器链 * `prepareStatement()`:通过 StatementHandler 创建 JDBC 原生的 Statement 对象 - * `getConnection()`:获取 JDBC 的 Connection 对象 + * `getConnection()`:**获取 JDBC 的 Connection 对象** * `handler.prepare()`:初始化 Statement 对象 * `instantiateStatement(Connection connection)`:Connection 中的方法实例化对象 * 获取普通执行者对象:`Connection.createStatement()` diff --git a/Web.md b/Web.md index 3bf591f..f12c7a5 100644 --- a/Web.md +++ b/Web.md @@ -4,7 +4,7 @@ ### 概述 -HTML(超文本标记语言—HyperText Markup Language)是构成 Web 世界的一砖一瓦。它是一种用来告知浏览器如何组织页面的标记语言。 +HTML(超文本标记语言—HyperText Markup Language)是构成 Web 世界的基础,是一种用来告知浏览器如何组织页面的标记语言 * 超文本 Hypertext,是指连接单个或者多个网站间的网页的链接。通过链接,就能访问互联网中的内容 @@ -2479,10 +2479,10 @@ Web,在计算机领域指网络。像我们接触的`WWW`,它是由3个单 | 服务器名称 | 说明 | | ----------- | ----------------------------------------------------- | -| weblogic | 实现了javaEE规范,重量级服务器,又称为javaEE容器 | -| websphereAS | 实现了javaEE规范,重量级服务器。 | -| JBOSSAS | 实现了JavaEE规范,重量级服务器。免费的。 | -| Tomcat | 实现了jsp/servlet规范,是一个轻量级服务器,开源免费。 | +| weblogic | 实现了 JavaEE 规范,重量级服务器,又称为 JavaEE 容器 | +| websphereAS | 实现了 JavaEE 规范,重量级服务器。 | +| JBOSSAS | 实现了 JavaEE 规范,重量级服务器,免费 | +| Tomcat | 实现了 jsp/servlet 规范,是一个轻量级服务器,开源免费 | @@ -2646,6 +2646,160 @@ Run -> Edit Configurations -> Templates -> Tomcat Server -> Local +**** + + + +### 执行原理 + +#### 整体架构 + +Tomcat 核心组件架构图如下所示: + +![](https://gitee.com/seazean/images/raw/master/Web/Tomcat-核心组件架构图.png) + +组件介绍: + +- GlobalNamingResources:实现 JNDI,指定一些资源的配置信息 +- Server:Tomcat 是一个 Servlet 容器,一个 Tomcat 对应一个 Server,一个 Server 可以包含多个 Service +- Service:核心服务是 Catalina,用来对请求进行处理,一个 Service 包含多个 Connector 和一个 Container +- Connector:连接器,负责处理客户端请求,解析不同协议及 I/O 方式 +- Executor:线程池 +- Container:容易包含 Engine,Host,Context,Wrapper 等组件 +- Engine:服务交给引擎处理请求,Container 容器中顶层的容器对象,一个 Engine 可以包含多个 Host 主机 +- Host:Engine 容器的子容器,一个 Host 对应一个网络域名,一个 Host 包含多个 Context +- Context:Host 容器的子容器,表示一个 Web 应用 +- Wrapper:Tomcat 中的最小容器单元,表示 Web 应用中的 Servlet + +核心类库: + +* Coyote:Tomcat 连接器的名称,封装了底层的网络通信,为 Catalina 容器提供了统一的接口,使容器与具体的协议以及 I/O 解耦 +* EndPoint:Coyote 通信端点,即通信监听的接口,是 Socket 接收和发送处理器,是对传输层的抽象,用来实现 TCP/IP 协议 +* Processor : Coyote 协议处理接口,用来实现 HTTP 协议,Processor 接收来自 EndPoint 的 Socket,读取字节流解析成 Tomcat 的 Request 和 Response 对象,并通过 Adapter 将其提交到容器处理,Processor 是对应用层协议的抽象 +* CoyoteAdapter:适配器,连接器调用 CoyoteAdapter 的 sevice 方法,传入的是 TomcatRequest 对象,CoyoteAdapter 负责将TomcatRequest 转成 ServletRequest,再调用容器的 service 方法 + + + +参考文章:https://www.jianshu.com/p/7c9401b85704 + +参考文章:https://www.yuque.com/yinhuidong/yu877c/ktq82e + + + +*** + + + +#### 启动过程 + +Tomcat 的启动入口是 Bootstrap#main 函数,首先通过调用 `bootstrap.init()` 初始化相关组件: + +* `initClassLoaders()`:初始化三个类加载器,commonLoader 的父类加载器是启动类加载器 +* `Thread.currentThread().setContextClassLoader(catalinaLoader)`:自定义类加载器加载 Catalina 类,**打破双亲委派** +* `Object startupInstance = startupClass.getConstructor().newInstance()`:反射创建 Catalina 对象 +* `method.invoke(startupInstance, paramValues)`:反射调用方法,设置父类加载器是 sharedLoader +* `catalinaDaemon = startupInstance`:引用 Catalina 对象 + +`daemon.load(args)` 方法反射调用 Catalina 对象的 load 方法,对**服务器的组件进行初始化**,并绑定了 ServerSocket 的端口: + +* `parseServerXml(true)`:解析 XML 配置文件 + +* `getServer().init()`:服务器执行初始化,采用责任链的执行方式 + + * `LifecycleBase.init()`:生命周期接口的初始化方法,开始链式调用 + + * `StandardServer.initInternal()`:Server 的初始化,遍历所有的 Service 进行初始化 + + * `StandardService.initInternal()`:Service 的初始化,对 Engine、Executor、listener、Connector 进行初始化 + + * `StandardEngine.initInternal()`:Engine 的初始化 + + * `getRealm()`:创建一个 Realm 对象 + * `ContainerBase.initInternal()`:容器的初始化,设置处理容器内组件的启动和停止事件的线程池 + + * `Connector.initInternal()`:Connector 的初始化 + + ```java + public Connector() { + this("HTTP/1.1"); //默认无参构造方法,会创建出 Http11NioProtocol 的协议处理器 + } + ``` + + * `adapter = new CoyoteAdapter(this)`:实例化 CoyoteAdapter 对象 + + * `protocolHandler.setAdapter(adapter)`:设置到 ProtocolHandler 协议处理器中 + + * `ProtocolHandler.init()`:协议处理器的初始化,底层调用 `AbstractProtocol#init` 方法 + + `endpoint.init()`:端口的初始化,底层调用 `AbstractEndpoint#init` 方法 + + `NioEndpoint.bind()`:绑定方法 + + * `initServerSocket()`:初始化 ServerSocket,以 NIO 的方式监听端口 + * `serverSock = ServerSocketChannel.open()`:**NIO 的方式打开通道** + * `serverSock.bind(addr, getAcceptCount())`:通道绑定连接端口 + * `serverSock.configureBlocking(true)`:切换为阻塞模式(没懂,为什么阻塞) + * `initialiseSsl()`:初始化 SSL 连接 + * `selectorPool.open(getName())`:打开选择器,类似 NIO 的多路复用器 + +初始化完所有的组件,调用 `daemon.start()` 进行**组件的启动**,底层反射调用 Catalina 对象的 start 方法: + +* `getServer().start()`:启动组件,也是责任链的模式 + + * `LifecycleBase.start()`:生命周期接口的初始化方法,开始链式调用 + + * `StandardServer.startInternal()`:Server 服务的启动 + + * `globalNamingResources.start()`:启动 JNDI 服务 + * `for (Service service : services)`:遍历所有的 Service 进行启动 + + * `StandardService.startInternal()`:Service 的启动,对所有 Executor、listener、Connector 进行启 + + * `StandardEngine.startInternal()`:启动引擎,部署项目 + + * `ContainerBase.startInternal()`:容器的启动 + * 启动集群、Realm 组件,并且创建子容器,提交给线程池 + * `((Lifecycle) pipeline).start()`:遍历所有的管道进行启动 + * `Valve current = first`:获取第一个阀门 + * `((Lifecycle) current).start()`:启动阀门,底层 `ValveBase#startInternal` 中设置启动的状态 + * `current = current.getNext()`:获取下一个阀门 + + * `Connector.startInternal()`:Connector 的初始化 + + * `protocolHandler.start()`:协议处理器的启动 + + `endpoint.start()`:端点启动 + + `NioEndpoint.startInternal()`:启动 NIO 的端点 + + * `createExecutor()`:创建 Worker 线程组,10 个线程,用来进行任务处理 + * `initializeConnectionLatch()`:用来进行连接限流,**最大 8*1024 条连接** + * `poller = new Poller()`:**创建 Poller 对象**,开启了一个多路复用器 Selector + * `Thread pollerThread = new Thread(poller, getName() + "-ClientPoller")`:创建并启动 Poller 线程,Poller 实现了 Runnable 接口,是一个任务对象,**线程 start 后进入 Poller#run 方法** + * `pollerThread.setDaemon(true)`:设置为守护线程 + * `startAcceptorThread()`:启动接收者线程 + * `acceptor = new Acceptor<>(this)`:**创建 Acceptor 对象** + * `Thread t = new Thread(acceptor, threadName)`:创建并启动 Acceptor 接受者线程 + + + +*** + + + +#### 处理过程 + +1) Acceptor 监听客户端套接字,每 50ms 调用一次 `serverSocket.accept`,获取 Socket 后把封装成 NioSocketWrapper(是 SocketWrapperBase 的子类),并设置为非阻塞模式,把 NioSocketWrapper 封装成 PollerEvent 放入同步队列中 +2) Poller 循环判断同步队列中是否有就绪的事件,如果有则通过 `selector.selectedKeys()` 获取就绪事件,获取 SocketChannel 中携带的 attachment(NioSocketWrapper),在 processKey 方法中根据事件类型进行 processSocket,将 Wrapper 对象封装成 SocketProcessor 对象,该对象是一个任务对象,提交到 Worker 线程池进行执行 +3) `SocketProcessorBase.run()` 加锁调用 `SocketProcessor#doRun`,保证线程安全,从协议处理器 ProtocolHandler 中获取 AbstractProtocol,然后**创建 Http11Processor 对象处理请求** +4) `Http11Processor#service` 中调用 `CoyoteAdapter#service` ,把生成的 Tomcat 下的 Request 和 Response 对象通过方法 postParseRequest 匹配到对应的 Servlet 的请求响应,将请求传递到对应的 Engine 容器中调用 Pipeline,管道中包含若干个 Valve,执行完所有的 Valve 最后执行 StandardEngineValve,继续调用 Host 容器的 Pipeline,执行 Host 的 Valve,再传递给 Context 的 Pipeline,最后传递到 Wrapper 容器 +5) `StandardWrapperValve#invoke` 中创建了 Servlet 对象并执行初始化,并为当前请求准备一个 FilterChain 过滤器链执行 doFilter 方法,`ApplicationFilterChain#doFilter` 是一个**责任链的驱动方法**,通过调用 internalDoFilter 来获取过滤器链的下一个过滤器执行 doFilter,执行完所有的过滤器后执行 `servlet.service` 的方法 +6) 最后调用 HttpServlet#service(),根据请求的方法来调用 doGet、doPost 等,执行到自定义的业务方法 + + + + + *** @@ -2659,9 +2813,7 @@ Socket 是使用 TCP/IP 或者 UDP 协议在服务器与客户端之间进行传 - **Servlet 是使用 HTTP 协议在服务器与客户端之间通信的技术,是 Socket 的一种应用** - **HTTP 协议:是在 TCP/IP 协议之上进一步封装的一层协议,关注数据传输的格式是否规范,底层的数据传输还是运用了 Socket 和 TCP/IP** -Tomcat 和 Servlet 的关系: - -Servlet 的运行环境叫做 Web 容器或 Servlet 服务器,**Tomcat 是 Web 应用服务器,是一个 Servlet/JSP 容器**。Tomcat 作为 Servlet 容器,负责处理客户请求,把请求传送给 Servlet,并将 Servlet 的响应传送回给客户。而 Servlet 是一种运行在支持Java语言的服务器上的组件,Servlet 最常见的用途是扩展 Java Web 服务器功能,提供非常安全的、可移植的、易于使用的 CGI 替代品 +Tomcat 和 Servlet 的关系:Servlet 的运行环境叫做 Web 容器或 Servlet 服务器,**Tomcat 是 Web 应用服务器,是一个 Servlet/JSP 容器**。Tomcat 作为 Servlet 容器,负责处理客户请求,把请求传送给 Servlet,并将 Servlet 的响应传送回给客户。而 Servlet 是一种运行在支持 Java 语言的服务器上的组件,Servlet 用来扩展 Java Web 服务器功能,提供非常安全的、可移植的、易于使用的 CGI 替代品 ![](https://gitee.com/seazean/images/raw/master/Web/Tomcat与Servlet的关系.png) @@ -5337,11 +5489,11 @@ JSTL:Java Server Pages Standarded Tag Library,JSP中标准标签库。 ### 过滤器 -Filter:过滤器,是JavaWeb三大组件之一,另外两个是Servlet和Listener。 +Filter:过滤器,是 JavaWeb 三大组件之一,另外两个是 Servlet 和 Listener -工作流程:在程序访问服务器资源时,当一个请求到来,服务器首先判断是否有过滤器与去请求资源相关联,如果有,过滤器可以将请求拦截下来,完成一些特定的功能,再由过滤器决定是否交给请求资源。如果没有就直接请求资源,响应同理。 +工作流程:在程序访问服务器资源时,当一个请求到来,服务器首先判断是否有过滤器与去请求资源相关联,如果有过滤器可以将请求拦截下来,完成一些特定的功能,再由过滤器决定是否交给请求资源,如果没有就直接请求资源,响应同理 -作用:过滤器一般用于完成通用的操作,例如:登录验证、统一编码处理、敏感字符过滤等。 +作用:过滤器一般用于完成通用的操作,例如:登录验证、统一编码处理、敏感字符过滤等 @@ -5353,7 +5505,7 @@ Filter:过滤器,是JavaWeb三大组件之一,另外两个是Servlet和Lis #### Filter -**Filter是一个接口,如果想实现过滤器的功能,必须实现该接口** +Filter是一个接口,如果想实现过滤器的功能,必须实现该接口 * 核心方法 @@ -5365,27 +5517,31 @@ Filter:过滤器,是JavaWeb三大组件之一,另外两个是Servlet和Lis * 配置方式 - * 注解方式 + 注解方式 - ```java - @WebFilter("/*") - ()内填拦截路径,/*代表全部路径 - ``` + ```java + @WebFilter("/*") + ()内填拦截路径,/*代表全部路径 + ``` - * 配置文件 + 配置文件 + + ```xml + + filterDemo01 + filter.FilterDemo01 + + + filterDemo01 + /* + + ``` + + + +*** - ```xml - - filterDemo01 - filter.FilterDemo01 - - - filterDemo01 - /* - - ``` - #### FilterChain @@ -5404,16 +5560,14 @@ Filter:过滤器,是JavaWeb三大组件之一,另外两个是Servlet和Lis FilterConfig 是一个接口,代表过滤器的配置对象,可以加载一些初始化参数 -* 核心方法: +| 方法 | 作用 | +| ------------------------------------------- | -------------------------------------------- | +| String getFilterName() | 获取过滤器对象名称 | +| String getInitParameter(String name) | 获取指定名称的初始化参数的值,不存在返回null | +| Enumeration getInitParameterNames() | 获取所有参数的名称 | +| ServletContext getServletContext() | 获取应用上下文对象 | - | 方法 | 作用 | - | ------------------------------------------- | -------------------------------------------- | - | String getFilterName() | 获取过滤器对象名称 | - | String getInitParameter(String name) | 获取指定名称的初始化参数的值,不存在返回null | - | Enumeration getInitParameterNames() | 获取所有参数的名称 | - | ServletContext getServletContext() | 获取应用上下文对象 | - @@ -5429,7 +5583,7 @@ FilterConfig 是一个接口,代表过滤器的配置对象,可以加载一 过滤器放行之后执行完目标资源,仍会回到过滤器中 -* Filter代码: +* Filter 代码: ```java @WebFilter("/*") @@ -5446,7 +5600,7 @@ FilterConfig 是一个接口,代表过滤器的配置对象,可以加载一 } ``` -* Servlet代码: +* Servlet 代码: ```java @WebServlet("/servletDemo01") @@ -5584,7 +5738,7 @@ FilterConfig 是一个接口,代表过滤器的配置对象,可以加载一 ``` -* Servlet代码:`System.out.println("servletDemo03执行了...");` +* Servlet 代码:`System.out.println("servletDemo03执行了...");` * 控制台输出: From 361a77583c9e9645da58d5822a81e0af50f261c6 Mon Sep 17 00:00:00 2001 From: Seazean Date: Tue, 7 Dec 2021 01:34:29 +0800 Subject: [PATCH 086/168] Update Java Notes --- DB.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/DB.md b/DB.md index df7af85..05fc858 100644 --- a/DB.md +++ b/DB.md @@ -1287,7 +1287,7 @@ LIMIT | AND 或 && | 并且 | | OR 或 \|\| | 或者 | | NOT 或 ! | 非,不是 | - | UNION | 对两个结果集进行并集操作并进行去重,同时进行默认规则的排序 | + | UNION | 对两个结果集进行**并集操作并进行去重,同时进行默认规则的排序** | | UNION ALL | 对两个结果集进行并集操作不进行去重,不进行排序 | * 例如: @@ -2211,12 +2211,21 @@ Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 #### 查询优化 -不相关子查询的结果集会被写入一个临时表,并且在写入时去重,该过程称为物化,存储结果集的临时表称为物化表 - -系统变量 tmp_table_size 或者 max_heap_table_size 为表的最值 +不相关子查询的结果集会被写入一个临时表,并且在写入时**去重**,该过程称为**物化**,存储结果集的临时表称为物化表。系统变量 tmp_table_size 或者 max_heap_table_size 为表的最值 * 小于系统变量时,内存中可以保存,会为建立基于内存的 MEMORY 存储引擎的临时表,并建立哈希索引 -* 大于任意一个系统变量时,物化表会使用基于磁盘的存储引擎来保存结果集中的记录,索引类型为 B+ 树, +* 大于任意一个系统变量时,物化表会使用基于磁盘的存储引擎来保存结果集中的记录,索引类型为 B+ 树 + +物化后,嵌套查询就相当于外层查询的表和物化表进行内连接查询,然后经过优化器选择成本最小的表连接顺序执行查询 + +将子查询物化会产生建立临时表的成本,但是将子查询转化为连接查询可以充分发挥优化器的作用,所以引入:半连接 + +* t1 和 t2 表进行半连接,对于 t1 表中的某条记录,只需要关心在 s2 表中是否存在,而不需要关心有多少条记录与之匹配,最终结果集只保留 t1 的记录 +* 半连接只是执行子查询的一种方式,MySQL 并没有提供面向用户的半连接语法 + + + +详细内容可以参考:《MySQL 是怎样运行的》 From c3ce60fcd20347579d24c589b9c31065e415d34e Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 8 Dec 2021 00:47:17 +0800 Subject: [PATCH 087/168] Update Java Notes --- DB.md | 487 ++++++++++++++++++++++++++++++---------------------------- 1 file changed, 254 insertions(+), 233 deletions(-) diff --git a/DB.md b/DB.md index 05fc858..4663c01 100644 --- a/DB.md +++ b/DB.md @@ -2018,7 +2018,7 @@ MySQL 将查询驱动表后得到的记录成为驱动表的扇出,连接查 * 减少驱动表的扇出 * 降低访问被驱动表的成本 -MySQL 提出了一种空间换时间的优化方式,基于块的循环连接,执行连接查询前申请一块固定大小的内存作为连接缓冲区 Join Buffer,先把若干条驱动表中的扇出暂存在缓冲区,每一条被驱动表中的记录一次性的与 Buffer 中多条记录进行匹配(可能是一对多),因为是在内存中完成,所以速度快,并且降低了 I/O 成本。 +MySQL 提出了一种**空间换时间**的优化方式,基于块的循环连接,执行连接查询前申请一块固定大小的内存作为连接缓冲区 Join Buffer,先把若干条驱动表中的扇出暂存在缓冲区,每一条被驱动表中的记录一次性的与 Buffer 中多条记录进行匹配(可能是一对多),因为是在内存中完成,所以速度快,并且降低了 I/O 成本。 Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 256 KB @@ -2413,7 +2413,7 @@ CREATE TABLE us_pro( ## 事务机制 -### 事务介绍 +### 管理事务 事务(Transaction)是访问和更新数据库的程序执行单元;事务中可能包含一个或多个 sql 语句,这些语句要么都执行,要么都不执行。作为一个关系型数据库,MySQL 支持事务。 @@ -2423,14 +2423,6 @@ CREATE TABLE us_pro( * 如果单元中所有的 SQL 语句都执行成功,则事务就顺利执行 - - -*** - - - -### 管理事务 - 管理事务的三个步骤 1. 开启事务:记录回滚点,并通知服务器,将要执行一组操作,要么同时成功、要么同时失败 @@ -2485,7 +2477,7 @@ CREATE TABLE us_pro( SET AUTOCOMMIT=数字; -- 会话 ``` - - 系统变量的操作: + - **系统变量的操作**: ```sql SET [GLOBAL|SESSION] 变量名 = 值; -- 默认是会话 @@ -2496,9 +2488,7 @@ CREATE TABLE us_pro( SHOW [GLOBAL|SESSION] VARIABLES [LIKE '变量%']; -- 默认查看会话内系统变量值 ``` - - -* 管理实务演示 +* 操作演示 ```mysql -- 开启事务 @@ -2525,7 +2515,7 @@ CREATE TABLE us_pro( -### 四大特征 +### 事务特性 #### ACID @@ -2542,7 +2532,7 @@ CREATE TABLE us_pro( -#### 原子性 +#### 原子特性 原子性是指事务是一个不可分割的工作单位,事务的操作如果成功就必须要完全应用到数据库,失败则不能对数据库有任何影响。比如事务中一个 SQL 语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态 @@ -2578,7 +2568,7 @@ rollback segment 称为回滚段,每个回滚段中有 1024 个 undo log segme -#### 一致性 +#### 一致特性 一致性是指事务执行前后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。 @@ -2596,7 +2586,9 @@ rollback segment 称为回滚段,每个回滚段中有 1024 个 undo log segme -#### 隔离性 +### 隔离特性 + +#### 实现方式 隔离性是指,事务内部的操作与其他事务是隔离的,多个并发事务之间要相互隔离,不能互相干扰 @@ -2617,188 +2609,6 @@ rollback segment 称为回滚段,每个回滚段中有 1024 个 undo log segme -#### 持久性 - -##### 实现原理 - -持久性是指一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,接下来的其他操作或故障不应该对其有任何影响。 - -Buffer Pool 是一片内存空间,可以通过 innodb_buffer_pool_size 来控制 Buffer Pool 的大小(内存优化部分会详解参数) - -* Change Buffer 是 Buffer Pool 里的内存,不能无限增大,用来对增删改操作提供缓存 -* Change Buffer 的大小可以通过参数 innodb_change_buffer_max_size 来动态设置,设置为 50 时表示 Change Buffer 的大小最多只能占用 Buffer Pool 的 50% -* 补充知识:**唯一索引的更新不能使用 Buffer**,一般只有普通索引可以使用,直接写入 Buffer 就结束 - -InnoDB 的数据是按数据页为单位来读写,每个数据页的大小默认是 16KB。数据是存放在磁盘中,每次读写数据都需要磁盘 IO,效率会很低。InnoDB 提供了缓存 Change Buffer,Buffer 中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲: - -* 从数据库读取数据时,会首先从缓存中读取,如果缓存中没有,则从磁盘读取后放入 Buffer Pool -* 向数据库写入数据时,会首先写入缓存,缓存中修改的数据会**定期刷新**到磁盘,这一过程称为刷脏 - - - -*** - - - -##### 数据恢复 - -Buffer Pool 的使用提高了读写数据的效率,但是也带了新的问题:如果 MySQL 宕机,此时 Buffer Pool 中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证,所以引入 redo log - -* 当数据修改时,先修改 Change Buffer 中的数据,然后在 redo log buffer 记录这次操作 -* 如果 MySQL 宕机,InnoDB 判断一个数据页在崩溃恢复时丢失了更新,就会将它读到内存,然后根据 redo log 内容更新内存,更新完成后,内存页变成脏页,然后进行刷脏(buffer pool 的任务) - -redo log **记录数据页的物理修改**,而不是某一行或某几行的修改,用来恢复提交后的物理数据页,且只能恢复到最后一次提交的位置 - -redo log 采用的是 WAL(Write-ahead logging,**预写式日志**),所有修改要先写入日志,再更新到磁盘,保证了数据不会因 MySQL 宕机而丢失,从而满足了持久性要求 - -redo log 也需要在事务提交时将日志写入磁盘,但是比将内存中的 Buffer Pool 修改的数据写入磁盘的速度快: - -* 刷脏是随机 IO,因为每次修改的数据位置随机,但写 redo log 是尾部追加操作,属于顺序 IO -* 刷脏是以数据页(Page)为单位的,一个页上的一个小修改都要整页写入,而 redo log 中只包含真正需要写入的部分,减少无效 IO - -InnoDB 引擎会在适当的时候,把内存中 redo log buffer 持久化到磁盘,具体的刷盘策略: - -* 通过修改参数 `innodb_flush_log_at_trx_commit` 设置: - * 0:表示当提交事务时,并不将缓冲区的 redo 日志写入磁盘,而是等待主线程每秒刷新一次 - * 1:在事务提交时将缓冲区的 redo 日志**同步写入**到磁盘,保证一定会写入成功 - * 2:在事务提交时将缓冲区的 redo 日志异步写入到磁盘,不能保证提交时肯定会写入,只是有这个动作 -* 如果写入 redo log buffer 的日志已经占据了 redo log buffer 总容量的一半了,此时就会刷入到磁盘文件,这时会影响执行效率,所以开发中应该**避免大事务** - -刷脏策略: - -* redo log 文件是固定大小的,如果写满了就要擦除以前的记录,在擦除之前需要把旧记录更新到磁盘中的数据文件中 -* Buffer Pool 内存不足,需要淘汰部分数据页,如果淘汰的是脏页,就要先将脏页写到磁盘(大事务) -* 系统空闲时,后台线程会自动进行刷脏 -* MySQL 正常关闭时,会把内存的脏页都 flush 到磁盘上 - - - -*** - - - -##### 工作流程 - -MySQL中还存在 binlog(二进制日志)也可以记录写操作并用于数据的恢复,**保证数据不丢失**,二者的区别是: - -* 作用不同:redo log 是用于 crash recovery (故障恢复),保证 MySQL 宕机也不会影响持久性;binlog 是用于 point-in-time recovery 的,保证服务器可以基于时间点恢复数据,此外 binlog 还用于主从复制 - -* 层次不同:redo log 是 InnoDB 存储引擎实现的,而 binlog 是MySQL的服务器层实现的,同时支持 InnoDB 和其他存储引擎 - -* 内容不同:redo log 是物理日志,内容基于磁盘的 Page;binlog 的内容是二进制的,根据 binlog_format 参数的不同,可能基于SQL 语句、基于数据本身或者二者的混合(日志部分详解) - -* 写入时机不同:binlog 在事务提交时一次写入;redo log 的写入时机相对多元 - -两种日志在 update 更新数据的**作用时机**: - -```sql -update T set c=c+1 where ID=2; -``` - - - -流程说明:执行引擎将这行新数据更新到内存中(Buffer Pool)后,然后会将这个更新操作记录到 redo log buffer 里,此时 redo log 处于 prepare 状态,代表执行完成随时可以提交事务,然后执行器生成这个操作的 binlog 并**把 binlog 写入磁盘**,在提交事务后 **redo log 也持久化到磁盘** - -redo log 和 binlog 都可以用于表示事务的提交状态,而**两阶段提交就是让这两个状态保持逻辑上的一致**,也有利于主从复制,更好的保持主从数据的一致性 - -故障恢复数据: - -* 如果在时刻 A 发生了崩溃(crash),由于此时 binlog 还没写,redo log 也没提交,所以数据恢复的时候这个事务会回滚 -* 如果在时刻 B 发生了崩溃,redo log 和 binlog 有一个共同的数据字段叫 XID,崩溃恢复的时候,会按顺序扫描 redo log: - * 如果 redo log 里面的事务是完整的,也就是已经有了 commit 标识,说明 binlog 也已经记录完整,直接从 redo log 恢复数据 - * 如果 redo log 里面的事务只有 prepare,就根据 XID 去 binlog 中判断对应的事务是否存在并完整,如果完整可以从 binlog 恢复 redo log 的信息,进而恢复数据,提交事务 - - -判断一个事务的 binlog 是否完整的方法: - -* statement 格式的 binlog,最后会有 COMMIT -* row 格式的 binlog,最后会有一个 XID event -* MySQL 5.6.2 版本以后,引入了 binlog-checksum 参数用来验证 binlog 内容的正确性 - - - -参考文章:https://time.geekbang.org/column/article/73161 - - - -*** - - - -##### 系统优化 - -系统在进行刷脏时会占用一部分系统资源,会影响系统的性能,产生系统抖动 - -* 一个查询要淘汰的脏页个数太多,会导致查询的响应时间明显变长 -* 日志写满,更新全部堵住,写性能跌为 0,这种情况对敏感业务来说,是不能接受的 - -InnoDB 刷脏页的控制策略: - -* `innodb_io_capacity` 参数代表磁盘的读写能力,建议设置成磁盘的 IOPS(每秒的 IO 次数) - -* 刷脏速度参考两个因素:脏页比例和 redo log 写盘速度 - * 参数 `innodb_max_dirty_pages_pct` 是脏页比例上限,默认值是 75%,InnoDB 会根据当前的脏页比例,算出一个范围在 0 到 100 之间的数字 - * InnoDB 每次写入的日志都有一个序号,当前写入的序号跟 checkpoint 对应的序号之间的差值,InnoDB 根据差值算出一个范围在 0 到 100 之间的数字 - * 两者较大的值记为 R,执行引擎按照 innodb_io_capacity 定义的能力乘以 R% 来控制刷脏页的速度 - -* `innodb_flush_neighbors` 参数置为 1 代表控制刷脏时检查相邻的数据页,如果也是脏页就一起刷脏,并检查邻居的邻居,这个行为会一直蔓延直到不是脏页,在 MySQL 8.0 中该值的默认值是 0,不建议开启此功能 - - - - - -*** - - - -### 隔离级别 - -事务的隔离级别:多个客户端操作时,各个客户端的事务之间应该是隔离的,**不同的事务之间不该互相影响**,而如果多个事务操作同一批数据时,则需要设置不同的隔离级别,否则就会产生问题。 - -隔离级别分类: - -| 隔离级别 | 名称 | 会引发的问题 | 数据库默认隔离级别 | -| ---------------- | -------- | -------------------------------- | ------------------- | -| read uncommitted | 读未提交 | 脏读、不可重复读、幻读 | | -| read committed | 读已提交 | 不可重复读、幻读 | Oracle / SQL Server | -| repeatable read | 可重复读 | 幻读 | MySQL | -| serializable | 串行化 | 无(因为写会加写锁,读会加读锁) | | - -一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差 - -* 丢失更新 (Lost Update):当两个或多个事务选择同一行,最初的事务修改的值,被后面事务修改的值覆盖,所有的隔离级别都可以避免丢失更新(行锁) - -* 脏读 (Dirty Reads):在事务中先后两次读取同一个数据,两次读取的结果不一样,原因是在一个事务处理过程中读取了另一个**未提交**的事务中的数据 - -* 不可重复读 (Non-Repeatable Reads):在事务中先后两次读取同一个数据,两次读取的结果不一样,原因是在一个事务处理过程中读取了另一个事务中修改并**已提交**的数据 - - > 可重复读的意思是不管读几次,结果都一样,可以重复的读,可以理解为快照读,要读的数据集不会发生变化 - -* 幻读 (Phantom Reads):在事务中按某个条件先后两次查询数据库,后一次查询查到了前一次查询没有查到的行,**数据条目**发生了变化。比如查询某数据不存在,准备插入此记录,但执行插入时发现此记录已存在,无法插入 - -**隔离级别操作语法:** - -* 查询数据库隔离级别 - - ```mysql - SELECT @@TX_ISOLATION; - SHOW VARIABLES LIKE 'tx_isolation'; - ``` - -* 修改数据库隔离级别 - - ```mysql - SET GLOBAL TRANSACTION ISOLATION LEVEL 级别字符串; - ``` - - - -*** - - - -### 并发控制 - #### MVCC MVCC 全称 Multi-Version Concurrency Control,即多版本并发控制,用来**解决读写冲突的无锁并发控制** @@ -2836,7 +2646,7 @@ MVCC 的优点: -#### 原理 +#### 实现原理 ##### 隐藏字段 @@ -2917,12 +2727,12 @@ Read View 几个属性: creator 创建一个 Read View,进行可见性算法分析:(解决了读未提交) * db_trx_id == creator_trx_id:表示这个数据就是当前事务自己生成的,自己生成的数据自己肯定能看见,所以这种情况下此数据对 creator 是可见的 -* db_trx_id < up_limit_id:该版本对应的事务 ID 小于 Read view 中的最小活跃事务 ID,则这个事务在当前事务之前就已经被提交了,对 creator 可见 +* db_trx_id < up_limit_id:该版本对应的事务 ID 小于 Read view 中的最小活跃事务 ID,则这个事务在当前事务之前就已经被提交了,对 creator 可见 -* db_trx_id >= low_limit_id:该版本对应的事务 ID 大于 Read view 中当前系统的最大事务 ID,则说明该数据是在当前 Read view 创建之后才产生的,对 creator 不可见 -* up_limit_id <= db_trx_id < low_limit_id:判断 db_trx_id 是否在活跃事务列表 m_ids 中 - * 在列表中,说明该版本对应的事务正在运行,数据不能显示(**不能读到未提交的数据**) - * 不在列表中,说明该版本对应的事务已经被提交,数据可以显示(**可以读到已经提交的数据**) +* db_trx_id >= low_limit_id:该版本对应的事务 ID 大于 Read view 中当前系统的最大事务 ID,则说明该数据是在当前 Read view 创建之后才产生的,对 creator 不可见 +* up_limit_id <= db_trx_id < low_limit_id:判断 db_trx_id 是否在活跃事务列表 m_ids 中 + * 在列表中,说明该版本对应的事务正在运行,数据不能显示(**不能读到未提交的数据**) + * 不在列表中,说明该版本对应的事务已经被提交,数据可以显示(**可以读到已经提交的数据**) @@ -2999,12 +2809,197 @@ RC、RR 级别下的 InnoDB 快照读区别 - 快照读:通过 MVCC 来进行控制的,在可重复读隔离级别下,普通查询是快照读,是不会看到别的事务插入的数据的,但是**并不能完全避免幻读** 场景:RR 级别,T1 事务开启,创建 Read View,此时 T2 去 INSERT 新的一行然后提交,然后 T1去 UPDATE 该行会发现更新成功,因为 Read View 并不能阻止事务去更新数据,并且把这条新记录的 trx_id 给变为当前的事务 id,对当前事务就是可见的了 + - 当前读:通过 next-key 锁(行锁 + 间隙锁)来解决问题 + + +*** + + + +### 持久特性 + +#### 实现原理 + +持久性是指一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,接下来的其他操作或故障不应该对其有任何影响。 + +Buffer Pool 是一片内存空间,可以通过 innodb_buffer_pool_size 来控制 Buffer Pool 的大小(内存优化部分会详解参数) + +* Change Buffer 是 Buffer Pool 里的内存,不能无限增大,用来对增删改操作提供缓存 +* Change Buffer 的大小可以通过参数 innodb_change_buffer_max_size 来动态设置,设置为 50 时表示 Change Buffer 的大小最多只能占用 Buffer Pool 的 50% +* 补充知识:**唯一索引的更新不能使用 Buffer**,一般只有普通索引可以使用,直接写入 Buffer 就结束 + +InnoDB 的数据是按数据页为单位来读写,每个数据页的大小默认是 16KB。数据是存放在磁盘中,每次读写数据都需要磁盘 IO,效率会很低。InnoDB 提供了缓存 Change Buffer,Buffer 中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲: + +* 从数据库读取数据时,会首先从缓存中读取,如果缓存中没有,则从磁盘读取后放入 Buffer Pool +* 向数据库写入数据时,会首先写入缓存,缓存中修改的数据会**定期刷新**到磁盘,这一过程称为刷脏 + + + +*** + + + +#### 数据恢复 + +Buffer Pool 的使用提高了读写数据的效率,但是也带了新的问题:如果 MySQL 宕机,此时 Buffer Pool 中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证,所以引入 redo log + +* 当数据修改时,先修改 Change Buffer 中的数据,然后在 redo log buffer 记录这次操作 +* 如果 MySQL 宕机,InnoDB 判断一个数据页在崩溃恢复时丢失了更新,就会将它读到内存,然后根据 redo log 内容更新内存,更新完成后,内存页变成脏页,然后进行刷脏(buffer pool 的任务) + +redo log **记录数据页的物理修改**,而不是某一行或某几行的修改,用来恢复提交后的物理数据页,且只能恢复到最后一次提交的位置 + +redo log 采用的是 WAL(Write-ahead logging,**预写式日志**),所有修改要先写入日志,再更新到磁盘,保证了数据不会因 MySQL 宕机而丢失,从而满足了持久性要求 + +redo log 也需要在事务提交时将日志写入磁盘,但是比将内存中的 Buffer Pool 修改的数据写入磁盘的速度快: + +* 刷脏是随机 IO,因为每次修改的数据位置随机,但写 redo log 是尾部追加操作,属于顺序 IO +* 刷脏是以数据页(Page)为单位的,一个页上的一个小修改都要整页写入,而 redo log 中只包含真正需要写入的部分,减少无效 IO + +InnoDB 引擎会在适当的时候,把内存中 redo log buffer 持久化到磁盘,具体的刷盘策略: + +* 通过修改参数 `innodb_flush_log_at_trx_commit` 设置: + * 0:表示当提交事务时,并不将缓冲区的 redo 日志写入磁盘,而是等待主线程每秒刷新一次 + * 1:在事务提交时将缓冲区的 redo 日志**同步写入**到磁盘,保证一定会写入成功 + * 2:在事务提交时将缓冲区的 redo 日志异步写入到磁盘,不能保证提交时肯定会写入,只是有这个动作 +* 如果写入 redo log buffer 的日志已经占据了 redo log buffer 总容量的一半了,此时就会刷入到磁盘文件,这时会影响执行效率,所以开发中应该**避免大事务** + +刷脏策略: + +* redo log 文件是固定大小的,如果写满了就要擦除以前的记录,在擦除之前需要把旧记录更新到磁盘中的数据文件中 +* Buffer Pool 内存不足,需要淘汰部分数据页,如果淘汰的是脏页,就要先将脏页写到磁盘(大事务) +* 系统空闲时,后台线程会自动进行刷脏 +* MySQL 正常关闭时,会把内存的脏页都 flush 到磁盘上 + + + +*** + + + +#### 工作流程 + +MySQL中还存在 binlog(二进制日志)也可以记录写操作并用于数据的恢复,**保证数据不丢失**,二者的区别是: + +* 作用不同:redo log 是用于 crash recovery (故障恢复),保证 MySQL 宕机也不会影响持久性;binlog 是用于 point-in-time recovery 的,保证服务器可以基于时间点恢复数据,此外 binlog 还用于主从复制 + +* 层次不同:redo log 是 InnoDB 存储引擎实现的,而 binlog 是MySQL的服务器层实现的,同时支持 InnoDB 和其他存储引擎 + +* 内容不同:redo log 是物理日志,内容基于磁盘的 Page;binlog 的内容是二进制的,根据 binlog_format 参数的不同,可能基于SQL 语句、基于数据本身或者二者的混合(日志部分详解) + +* 写入时机不同:binlog 在事务提交时一次写入;redo log 的写入时机相对多元 + +两种日志在 update 更新数据的**作用时机**: + +```sql +update T set c=c+1 where ID=2; +``` + + + +流程说明:执行引擎将这行新数据更新到内存中(Buffer Pool)后,然后会将这个更新操作记录到 redo log buffer 里,此时 redo log 处于 prepare 状态,代表执行完成随时可以提交事务,然后执行器生成这个操作的 binlog 并**把 binlog 写入磁盘**,在提交事务后 **redo log 也持久化到磁盘** + +redo log 和 binlog 都可以用于表示事务的提交状态,而**两阶段提交就是让这两个状态保持逻辑上的一致**,也有利于主从复制,更好的保持主从数据的一致性 + +故障恢复数据: + +* 如果在时刻 A 发生了崩溃(crash),由于此时 binlog 还没写,redo log 也没提交,所以数据恢复的时候这个事务会回滚 +* 如果在时刻 B 发生了崩溃,redo log 和 binlog 有一个共同的数据字段叫 XID,崩溃恢复的时候,会按顺序扫描 redo log: + * 如果 redo log 里面的事务是完整的,也就是已经有了 commit 标识,说明 binlog 也已经记录完整,直接从 redo log 恢复数据 + * 如果 redo log 里面的事务只有 prepare,就根据 XID 去 binlog 中判断对应的事务是否存在并完整,如果完整可以从 binlog 恢复 redo log 的信息,进而恢复数据,提交事务 + + +判断一个事务的 binlog 是否完整的方法: + +* statement 格式的 binlog,最后会有 COMMIT +* row 格式的 binlog,最后会有一个 XID event +* MySQL 5.6.2 版本以后,引入了 binlog-checksum 参数用来验证 binlog 内容的正确性 + + + +参考文章:https://time.geekbang.org/column/article/73161 + + + +*** + + + +#### 系统优化 + +系统在进行刷脏时会占用一部分系统资源,会影响系统的性能,产生系统抖动 + +* 一个查询要淘汰的脏页个数太多,会导致查询的响应时间明显变长 +* 日志写满,更新全部堵住,写性能跌为 0,这种情况对敏感业务来说,是不能接受的 + +InnoDB 刷脏页的控制策略: + +* `innodb_io_capacity` 参数代表磁盘的读写能力,建议设置成磁盘的 IOPS(每秒的 IO 次数) + +* 刷脏速度参考两个因素:脏页比例和 redo log 写盘速度 + * 参数 `innodb_max_dirty_pages_pct` 是脏页比例上限,默认值是 75%,InnoDB 会根据当前的脏页比例,算出一个范围在 0 到 100 之间的数字 + * InnoDB 每次写入的日志都有一个序号,当前写入的序号跟 checkpoint 对应的序号之间的差值,InnoDB 根据差值算出一个范围在 0 到 100 之间的数字 + * 两者较大的值记为 R,执行引擎按照 innodb_io_capacity 定义的能力乘以 R% 来控制刷脏页的速度 + +* `innodb_flush_neighbors` 参数置为 1 代表控制刷脏时检查相邻的数据页,如果也是脏页就一起刷脏,并检查邻居的邻居,这个行为会一直蔓延直到不是脏页,在 MySQL 8.0 中该值的默认值是 0,不建议开启此功能 + + + + + +*** + + + +### 隔离级别 + +事务的隔离级别:多个客户端操作时,各个客户端的事务之间应该是隔离的,**不同的事务之间不该互相影响**,而如果多个事务操作同一批数据时,则需要设置不同的隔离级别,否则就会产生问题。 + +隔离级别分类: + +| 隔离级别 | 名称 | 会引发的问题 | 数据库默认隔离级别 | +| ---------------- | -------- | -------------------------------- | ------------------- | +| read uncommitted | 读未提交 | 脏读、不可重复读、幻读 | | +| read committed | 读已提交 | 不可重复读、幻读 | Oracle / SQL Server | +| repeatable read | 可重复读 | 幻读 | MySQL | +| serializable | 串行化 | 无(因为写会加写锁,读会加读锁) | | + +一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差 + +* 丢失更新 (Lost Update):当两个或多个事务选择同一行,最初的事务修改的值,被后面事务修改的值覆盖,所有的隔离级别都可以避免丢失更新(行锁) + +* 脏读 (Dirty Reads):在事务中先后两次读取同一个数据,两次读取的结果不一样,原因是在一个事务处理过程中读取了另一个**未提交**的事务中的数据 + +* 不可重复读 (Non-Repeatable Reads):在事务中先后两次读取同一个数据,两次读取的结果不一样,原因是在一个事务处理过程中读取了另一个事务中修改并**已提交**的数据 + + > 可重复读的意思是不管读几次,结果都一样,可以重复的读,可以理解为快照读,要读的数据集不会发生变化 + +* 幻读 (Phantom Reads):在事务中按某个条件先后两次查询数据库,后一次查询查到了前一次查询没有查到的行,**数据条目**发生了变化。比如查询某数据不存在,准备插入此记录,但执行插入时发现此记录已存在,无法插入 + +**隔离级别操作语法:** + +* 查询数据库隔离级别 + + ```mysql + SELECT @@TX_ISOLATION; + SHOW VARIABLES LIKE 'tx_isolation'; + ``` + +* 修改数据库隔离级别 + + ```mysql + SET GLOBAL TRANSACTION ISOLATION LEVEL 级别字符串; + ``` + + + + + *** @@ -4364,14 +4359,25 @@ MyISAM 的索引方式也叫做非聚集的,之所以这么称呼是为了与 ### 索引结构 -#### BTree +#### 数据页 + +文件系统的最小单元是块(block),一个块的大小是 4K,系统从磁盘读取数据到内存时是以磁盘块为基本单位的,位于同一个磁盘块中的数据会被一次性读取出来,而不是需要什么取什么 + +InnoDB 存储引擎中有页(Page)的概念,页是 MySQL 磁盘管理的最小单位 + +* **InnoDB 存储引擎中默认每个页的大小为 16KB,索引中一个节点就是一个数据页**,所以会一次性读取 16KB 的数据到内存 +* InnoDB 引擎将若干个地址连接磁盘块,以此来达到页的大小 16KB +* 在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘 I/O 次数,提高查询效率 + -磁盘存储: -* 系统从磁盘读取数据到内存时是以磁盘块(block)为基本单位的,位于同一个磁盘块中的数据会被一次性读取出来,而不是需要什么取什么 -- InnoDB 存储引擎中有页(Page)的概念,页是其磁盘管理的最小单位,InnoDB 存储引擎中默认每个页的大小为 16KB -- InnoDB 引擎将若干个地址连接磁盘块,以此来达到页的大小 16KB,在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘 I/O 次数,提高查询效率 + +*** + + + +#### BTree BTree 的索引类型是基于 B+Tree 树型数据结构的,B+Tree 又是 BTree 数据结构的变种,用在数据库和操作系统中的文件系统,特点是能够保持数据稳定有序 @@ -4391,31 +4397,31 @@ BTree 又叫多路平衡搜索树,一颗 m 叉的 BTree 特性如下: ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程1.png) -* 插入H,n>4,中间元素G字母向上分裂到新的节点 +* 插入 H,n>4,中间元素 G 字母向上分裂到新的节点 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程2.png) -* 插入E,K,Q不需要分裂 +* 插入 E、K、Q 不需要分裂 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程3.png) -* 插入M,中间元素M字母向上分裂到父节点G +* 插入 M,中间元素 M 字母向上分裂到父节点 G ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程4.png) -* 插入F,W,L,T 不需要分裂 +* 插入 F,W,L,T 不需要分裂 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程5.png) -* 插入Z,中间元素T向上分裂到父节点中 +* 插入 Z,中间元素 T 向上分裂到父节点中 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程6.png) -* 插入D,中间元素D向上分裂到父节点中,然后插入P,R,X,Y不需要分裂 +* 插入 D,中间元素 D 向上分裂到父节点中,然后插入 P,R,X,Y 不需要分裂 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程7.png) -* 最后插入S,NPQR节点n>5,中间节点Q向上分裂,但分裂后父节点DGMT的n>5,中间节点M向上分裂 +* 最后插入 S,NPQR 节点 n>5,中间节点 Q 向上分裂,但分裂后父节点 DGMT 的 n>5,中间节点 M 向上分裂 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程8.png) @@ -4463,7 +4469,7 @@ MySQL 索引数据结构对经典的 B+Tree 进行了优化,在原 B+Tree 的 区间访问的意思是访问索引为 5 - 15 的数据,可以直接根据相邻节点的指针遍历 -B+ 树的叶子节点是数据页(page),一个页里面可以存多个数据行 +B+ 树的**叶子节点是数据页**(page),一个页里面可以存多个数据行 ![](https://gitee.com/seazean/images/raw/master/DB/索引的原理-B+Tree.png) @@ -4472,7 +4478,10 @@ B+ 树的叶子节点是数据页(page),一个页里面可以存多个数 - 有范围:对于主键的范围查找和分页查找 - 有顺序:从根节点开始,进行随机查找,顺序查找 -InnoDB 中每个数据页的大小默认是 16KB,一般表的主键类型为 INT(4 字节)或 BIGINT(8 字节),指针类型也一般为 4 或 8 个字节,也就是说一个页(B+Tree 中的**一个节点**)中大概存储 16KB/(8B+8B)=1K 个键值(估值)。则一个深度为 3 的 B+Tree 索引可以维护 `10^3 * 10^3 * 10^3 = 10亿` 条记录 +InnoDB 中每个数据页的大小默认是 16KB, + +* 索引行:一般表的主键类型为 INT(4 字节)或 BIGINT(8 字节),指针大小在 InnoDB 中设置为 6 字节节,也就是说一个页大概存储 16KB/(8B+6B)=1K 个键值(估值)。则一个深度为 3 的 B+Tree 索引可以维护 `10^3 * 10^3 * 10^3 = 10亿` 条记录 +* 数据行:一行数据的大小可能是 1k,一个数据页可以存储 16 行 实际情况中每个节点可能不能填充满,因此在数据库中,B+Tree 的高度一般都在 2-4 层。MySQL 的 InnoDB 存储引擎在设计时是**将根节点常驻内存的**,也就是说查找某一键值的行记录时最多只需要 1~3 次磁盘 I/O 操作 @@ -4672,7 +4681,7 @@ B+ 树为了保持索引的有序性,在插入新值的时候需要做相应 **适用条件**: -* 需要存储引擎将索引中的数据与条件进行判断(所以条件列必须都在同一个索引中),所以优化是基于存储引擎的,只有特定引擎可以使用,适用于 InnoDB 和 MyISAM +* 需要存储引擎将索引中的数据与条件进行判断(所以**条件列必须都在同一个索引中**),所以优化是基于存储引擎的,只有特定引擎可以使用,适用于 InnoDB 和 MyISAM * 存储引擎没有调用跨存储引擎的能力,跨存储引擎的功能有存储过程、触发器、视图,所以调用这些功能的不可以进行索引下推优化 * 对于 InnoDB 引擎只适用于二级索引,InnoDB 的聚簇索引会将整行数据读到缓冲区,不再需要去回表查询了,索引下推的目的减少 IO 次数也就失去了意义 @@ -4917,7 +4926,7 @@ EXPLAIN SELECT * FROM table_1 WHERE id = 1; | filtered | 条件过滤的行百分比,单表查询没意义,用于连接查询中对驱动表的扇出进行过滤,查询优化器预测所有扇出值满足剩余查询条件的百分比,相乘以后表示多表查询中还要对被驱动执行查询的次数 | | extra | 执行情况的说明和描述 | -MySQL 执行计划的局限: +MySQL **执行计划的局限**: * 只是计划,不是执行 SQL 语句,可以随着底层优化器输入的更改而更改 * EXPLAIN 不会告诉显示关于触发器、存储过程的信息对查询的影响情况 @@ -4927,6 +4936,8 @@ MySQL 执行计划的局限: * EXPALIN 只能解释 SELECT 操作,其他操作要重写为 SELECT 后查看执行计划 * EXPLAIN PLAN 显示的是在解释语句时数据库将如何运行 SQL 语句,由于执行环境和 EXPLAIN PLAN 环境的不同,此计划可能与 SQL 语句实际的执行计划不同 +SHOW WARINGS:在使用 EXPALIN 命令后执行该语句,可以查询与执行计划相关的拓展信息,展示出 Level、Code、Message 三个字段,当 Code 为 1003 时,Message 字段展示的信息类似于将查询语句重写后的信息,但是不是等价,不能执行复制过来运行 + 环境准备: ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-执行计划环境准备.png) @@ -5056,15 +5067,15 @@ key_len: 其他的额外的执行计划信息,在该列展示: +* No tables used:查询语句中使用 FROM dual 或者没有 FROM 语句 +* Impossible WHERE:查询语句中的 WHERE 子句条件永远为 FALSE,会导致没有符合条件的行 * Using index:该值表示相应的 SELECT 操作中使用了**覆盖索引**(Covering Index) -* Using index condition:第一种情况是搜索条件中虽然出现了索引列,但是有部分条件无法使用索引,会根据能用索引的条件先搜索一遍再匹配无法使用索引的条件,回表查询数据;第二种是使用了索引下推 -* Using where:表示存储引擎收到记录后进行后过滤(Post-filter),如果查询操作未能使用索引,Using where 的作用是提醒我们 MySQL 将用 where 子句来过滤结果集,即需要回表查询 -* Using temporary:表示 MySQL 需要使用临时表来存储结果集,常见于排序和分组查询 -* Using filesort:对数据使用外部排序算法,将取得的数据在内存中进行排序,这种无法利用索引完成的排序操作称为文件排序 -* Using join buffer:说明在获取连接条件时没有使用索引,并且需要连接缓冲区来存储中间结果 -* Impossible where:说明 where 语句会导致没有符合条件的行,通过收集统计信息不可能存在结果 +* Using index condition:第一种情况是搜索条件中虽然出现了索引列,但是部分条件无法形成扫描区间(**索引失效**),会根据可用索引的条件先搜索一遍再匹配无法使用索引的条件,回表查询数据;第二种是使用了**索引条件下推**优化 +* Using where:搜索条件需要在 Server 层判断,判断后执行回表操作查询,无法使用索引下推 +* Using join buffer:连接查询被驱动表无法利用索引,需要连接缓冲区来存储中间结果 +* Using filesort:无法利用索引完成排序(优化方向),需要对数据使用外部排序算法,将取得的数据在内存或磁盘中进行排序 +* Using temporary:表示 MySQL 需要使用临时表来存储结果集,常见于排序、去重、分组等场景 * Select tables optimized away:说明仅通过使用索引,优化器可能仅从聚合函数结果中返回一行 - * No tables used:Query 语句中使用 from dual 或不含任何 from 子句 @@ -5125,11 +5136,11 @@ SHOW PROFILES 能够在做 SQL 优化时分析当前会话中语句执行的** -#### trace +#### TRACE -MySQL 提供了对 SQL 的跟踪, 通过 trace 文件能够进一步了解执行过程。 +MySQL 提供了对 SQL 的跟踪, 通过 trace 文件可以查看优化器生成执行计划的过程 -* 打开 trace,设置格式为 JSON,并设置 trace 最大能够使用的内存大小,避免解析过程中因为默认内存过小而不能够完整展示 +* 打开 trace 功能,设置格式为 JSON,并设置 trace 的最大使用内存,避免解析过程中因默认内存过小而不能够完整展示 ```mysql SET optimizer_trace="enabled=on",end_markers_in_json=ON; -- 会话内有效 @@ -5147,6 +5158,8 @@ MySQL 提供了对 SQL 的跟踪, 通过 trace 文件能够进一步了解执 ```mysql SELECT * FROM information_schema.optimizer_trace \G; -- \G代表竖列展示 ``` + + 执行信息主要有三个阶段:prepare 阶段、optimize 阶段(成本分析)、execute 阶段(执行) @@ -5187,7 +5200,7 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); #### 避免失效 -索引失效的情况: +##### 语句错误 * 全值匹配:对索引中所有列都指定具体值,这种情况索引生效,执行效率高 @@ -5294,6 +5307,14 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); 原因:在覆盖索引的这棵 B+ 数上只需要进行 like 的匹配,或者是基于覆盖索引查询再进行 WHERE 的判断就可以获得结果 + + +*** + + + +##### 系统优化 + 系统优化为全表扫描: * 如果 MySQL 评估使用索引比全表更慢,则不使用索引,索引失效: From a5061ed3f2b7436c2c02e873f55e20a2255b2244 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 11 Dec 2021 01:53:28 +0800 Subject: [PATCH 088/168] Update Java Notes --- DB.md | 5373 ++++++++++++++++++++++++++++--------------------------- Java.md | 29 +- Prog.md | 4 + SSM.md | 4 +- Web.md | 48 +- 5 files changed, 2803 insertions(+), 2655 deletions(-) diff --git a/DB.md b/DB.md index 4663c01..7235c9b 100644 --- a/DB.md +++ b/DB.md @@ -52,7 +52,7 @@ MySQL 数据库是一个最流行的关系型数据库管理系统之一 缺点:数据存储在磁盘中,导致读写性能差,而且数据关系复杂,扩展性差 -MySQL 所使用的SQL语句是用于访问数据库最常用的标准化语言。 +MySQL 所使用的 SQL 语句是用于访问数据库最常用的标准化语言。 MySQL 配置: @@ -450,7 +450,7 @@ SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 SHOW VARIABLES LIKE 'have_query_cache'; -- YES ``` -2. 查看当前MySQL是否开启了查询缓存: +2. 查看当前 MySQL 是否开启了查询缓存: ```mysql SHOW VARIABLES LIKE 'query_cache_type'; -- OFF @@ -2405,180 +2405,198 @@ CREATE TABLE us_pro( -**** +*** -## 事务机制 +## 存储结构 -### 管理事务 +### 视图 -事务(Transaction)是访问和更新数据库的程序执行单元;事务中可能包含一个或多个 sql 语句,这些语句要么都执行,要么都不执行。作为一个关系型数据库,MySQL 支持事务。 +#### 基本介绍 -单元中的每条 SQL 语句都相互依赖,形成一个整体 +视图概念:视图是一种虚拟存在的数据表,这个虚拟的表并不在数据库中实际存在 -* 如果某条 SQL 语句执行失败或者出现错误,那么整个单元就会回滚,撤回到事务最初的状态 +本质:将一条 SELECT 查询语句的结果封装到了一个虚拟表中,所以在创建视图的时候,工作重心要放在这条 SELECT 查询语句上 -* 如果单元中所有的 SQL 语句都执行成功,则事务就顺利执行 +作用:将一些比较复杂的查询语句的结果,封装到一个虚拟表中,再有相同查询需求时,直接查询该虚拟表 -管理事务的三个步骤 +优点: -1. 开启事务:记录回滚点,并通知服务器,将要执行一组操作,要么同时成功、要么同时失败 +* 简单:使用视图的用户不需要关心表的结构、关联条件和筛选条件,因为虚拟表中已经是过滤好的结果集 +* 安全:使用视图的用户只能访问查询的结果集,对表的权限管理并不能限制到某个行某个列 -2. 执行 SQL 语句:执行具体的一条或多条 SQL 语句 +* 数据独立,一旦视图的结构确定,可以屏蔽表结构变化对用户的影响,源表增加列对视图没有影响;源表修改列名,则可以通过修改视图来解决,不会造成对访问者的影响 -3. 结束事务(提交|回滚) - - 提交:没出现问题,数据进行更新 - - 回滚:出现问题,数据恢复到开启事务时的状态 +*** -事务操作: -* 开启事务 - ```mysql - START TRANSACTION; - ``` +#### 视图创建 -* 回滚事务 +* 创建视图 ```mysql - ROLLBACK; + CREATE [OR REPLACE] + VIEW 视图名称 [(列名列表)] + AS 查询语句 + [WITH [CASCADED | LOCAL] CHECK OPTION]; ``` -* 提交事务,显示执行是手动提交,MySQL 默认为自动提交 + `WITH [CASCADED | LOCAL] CHECK OPTION` 决定了是否允许更新数据使记录不再满足视图的条件: + + * LOCAL:只要满足本视图的条件就可以更新 + * CASCADED:必须满足所有针对该视图的所有视图的条件才可以更新, 默认值 + +* 例如 ```mysql - COMMIT; + -- 数据准备 city + id NAME cid + 1 深圳 1 + 2 上海 1 + 3 纽约 2 + 4 莫斯科 3 + + -- 数据准备 country + id NAME + 1 中国 + 2 美国 + 3 俄罗斯 + + -- 创建city_country视图,保存城市和国家的信息(使用指定列名) + CREATE + VIEW + city_country (city_id,city_name,country_name) + AS + SELECT + c1.id, + c1.name, + c2.name + FROM + city c1, + country c2 + WHERE + c1.cid=c2.id; ``` - 工作原理: - - * 自动提交模式下,如果没有 start transaction 显式地开始一个事务,那么**每个 SQL 语句都会被当做一个事务执行提交操作** - * 手动提交模式下,所有的 SQL 语句都在一个事务中,直到执行了 commit 或 rollback + - * 存在一些特殊的命令,在事务中执行了这些命令会马上强制执行 COMMIT 提交事务,如 DDL 语句 (create/drop/alter/table)、lock tables 语句等 +*** - 提交方式语法: - - 查看事务提交方式 - ```mysql - SELECT @@AUTOCOMMIT; -- 1 代表自动提交 0 代表手动提交 - ``` +#### 视图查询 - - 修改事务提交方式 +* 查询所有数据表,视图也会查询出来 - ```mysql - SET @@AUTOCOMMIT=数字; -- 系统 - SET AUTOCOMMIT=数字; -- 会话 - ``` - - - **系统变量的操作**: + ```mysql + SHOW TABLES; + SHOW TABLE STATUS [\G]; + ``` - ```sql - SET [GLOBAL|SESSION] 变量名 = 值; -- 默认是会话 - SET @@[(GLOBAL|SESSION).]变量名 = 值; -- 默认是系统 - ``` +* 查询视图 - ```sql - SHOW [GLOBAL|SESSION] VARIABLES [LIKE '变量%']; -- 默认查看会话内系统变量值 - ``` + ```mysql + SELECT * FROM 视图名称; + ``` -* 操作演示 +* 查询某个视图创建 ```mysql - -- 开启事务 - START TRANSACTION; - - -- 张三给李四转账500元 - -- 1.张三账户-500 - UPDATE account SET money=money-500 WHERE NAME='张三'; - -- 2.李四账户+500 - UPDATE account SET money=money+500 WHERE NAME='李四'; - - -- 回滚事务(出现问题) - ROLLBACK; - - -- 提交事务(没出现问题) - COMMIT; + SHOW CREATE VIEW 视图名称; ``` - - *** -### 事务特性 - -#### ACID - -事务的四大特征:ACID - -- 原子性 (atomicity) -- 一致性 (consistency) -- 隔离性 (isolaction) -- 持久性 (durability) +#### 视图修改 +视图表数据修改,会**自动修改源表中的数据**,因为更新的是视图中的基表中的数据 +* 修改视图表中的数据 -*** + ```mysql + UPDATE 视图名称 SET 列名 = 值 WHERE 条件; + ``` +* 修改视图的结构 + ```mysql + ALTER [ALGORITHM = {UNDEFINED | MERGE | TEMPTABLE}] + VIEW 视图名称 [(列名列表)] + AS 查询语句 + [WITH [CASCADED | LOCAL] CHECK OPTION] + + -- 将视图中的country_name修改为name + ALTER + VIEW + city_country (city_id,city_name,name) + AS + SELECT + c1.id, + c1.name, + c2.name + FROM + city c1, + country c2 + WHERE + c1.cid=c2.id; + ``` -#### 原子特性 -原子性是指事务是一个不可分割的工作单位,事务的操作如果成功就必须要完全应用到数据库,失败则不能对数据库有任何影响。比如事务中一个 SQL 语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态 -InnoDB 存储引擎提供了两种事务日志:redo log(重做日志)和 undo log(回滚日志) +*** -* redo log 用于保证事务持久性 -* undo log 用于保证事务原子性和隔离性 -undo log 属于逻辑日志,根据每行操作进行记录,记录了 SQL 执行相关的信息,用来回滚行记录到某个版本 -当事务对数据库进行修改时,InnoDB 会先记录对应的 undo log,如果事务执行失败或调用了 rollback 导致事务回滚,InnoDB 会根据 undo log 的内容**做与之前相反的操作**: +#### 视图删除 -* 对于每个 insert,回滚时会执行 delete +* 删除视图 -* 对于每个 delete,回滚时会执行 insert + ```mysql + DROP VIEW 视图名称; + ``` -* 对于每个 update,回滚时会执行一个相反的 update,把数据修改回去 +* 如果存在则删除 -undo log 是采用段(segment)的方式来记录,每个 undo 操作在记录的时候占用一个 undo log segment + ```mysql + DROP VIEW IF EXISTS 视图名称; + ``` -rollback segment 称为回滚段,每个回滚段中有 1024 个 undo log segment -* 在以前老版本,只支持 1 个 rollback segment,只能记录 1024 个 undo log segment -* MySQL5.5 开始支持 128 个 rollback segment,支持 128*1024 个 undo 操作 -参考文章:https://www.cnblogs.com/kismetv/p/10331633.html +*** -*** +### 存储过程 +#### 基本介绍 -#### 一致特性 +存储过程和函数:存储过程和函数是事先经过编译并存储在数据库中的一段 SQL 语句的集合 -一致性是指事务执行前后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。 +存储过程和函数的好处: -数据库的完整性约束包括但不限于:实体完整性(如行的主键存在且唯一)、列完整性(如字段的类型、大小、长度要符合要求)、外键约束、用户自定义完整性(如转账前后,两个账户余额的和应该不变) +* 提高代码的复用性 +* 减少数据在数据库和应用服务器之间的传输,提高传输效率 +* 减少代码层面的业务处理 +* **一次编译永久有效** -实现一致性的措施: +存储过程和函数的区别: -- 保证原子性、持久性和隔离性,如果这些特性无法保证,事务的一致性也无法保证 -- 数据库本身提供保障,例如不允许向整形列插入字符串值、字符串长度不能超过列的限制等 -- 应用层面进行保障,例如如果转账操作只扣除转账者的余额,而没有增加接收者的余额,无论数据库实现的多么完美,也无法保证状态的一致 +* 存储函数必须有返回值 +* 存储过程可以没有返回值 @@ -2586,86 +2604,189 @@ rollback segment 称为回滚段,每个回滚段中有 1024 个 undo log segme -### 隔离特性 +#### 基本操作 -#### 实现方式 +DELIMITER: -隔离性是指,事务内部的操作与其他事务是隔离的,多个并发事务之间要相互隔离,不能互相干扰 +* DELIMITER 关键字用来声明 sql 语句的分隔符,告诉 MySQL 该段命令已经结束 -* 严格的隔离性,对应了事务隔离级别中的 serializable,实际应用中对性能考虑很少使用可串行化 +* MySQL 语句默认的分隔符是分号,但是有时需要一条功能 sql 语句中包含分号,但是并不作为结束标识,这时使用 DELIMITER 来指定分隔符: -* 与原子性、持久性侧重于研究事务本身不同,隔离性研究的是**不同事务**之间的相互影响 + ```mysql + DELIMITER 分隔符 + ``` -隔离性让并发情形下的事务之间互不干扰: +存储过程的创建调用查看和删除: -- 一个事务的写操作对另一个事务的写操作(写写):锁机制保证隔离性 -- 一个事务的写操作对另一个事务的读操作(读写):MVCC 保证隔离性 +* 创建存储过程 -锁机制:事务在修改数据之前,需要先获得相应的锁,获得锁之后,事务便可以修改数据;该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁(详解见锁机制) + ```mysql + -- 修改分隔符为$ + DELIMITER $ + + -- 标准语法 + CREATE PROCEDURE 存储过程名称(参数...) + BEGIN + sql语句; + END$ + + -- 修改分隔符为分号 + DELIMITER ; + ``` +* 调用存储过程 + ```mysql + CALL 存储过程名称(实际参数); + ``` -*** +* 查看存储过程 + ```mysql + SELECT * FROM mysql.proc WHERE db='数据库名称'; + ``` +* 删除存储过程 -#### MVCC + ```mysql + DROP PROCEDURE [IF EXISTS] 存储过程名称; + ``` -MVCC 全称 Multi-Version Concurrency Control,即多版本并发控制,用来**解决读写冲突的无锁并发控制** +练习: -MVCC 处理读写请求,可以做到在发生读写请求冲突时不用加锁,这个读是指的快照读,而不是当前读 +* 数据准备 -* 快照读:实现基于 MVCC,因为是多版本并发,所以快照读读到的数据不一定是当前最新的数据,有可能是历史版本的数据 -* 当前读:读取数据库记录是当前最新的版本(产生幻读、不可重复读),可以对读取的数据进行加锁,防止其他事务修改数据,是悲观锁的一种操作,读写操作加共享锁或者排他锁和串行化事务的隔离级别都是当前读 - -数据库并发场景: - -* 读-读:不存在任何问题,也不需要并发控制 + ```mysql + id NAME age gender score + 1 张三 23 男 95 + 2 李四 24 男 98 + 3 王五 25 女 100 + 4 赵六 26 女 90 + ``` -* 读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读 +* 创建 stu_group() 存储过程,封装分组查询总成绩,并按照总成绩升序排序的功能 -* 写-写:有线程安全问题,可能会存在丢失更新问题 + ```mysql + DELIMITER $ + + CREATE PROCEDURE stu_group() + BEGIN + SELECT gender,SUM(score) getSum FROM student GROUP BY gender ORDER BY getSum ASC; + END$ + + DELIMITER ; + + -- 调用存储过程 + CALL stu_group(); + -- 删除存储过程 + DROP PROCEDURE IF EXISTS stu_group; + ``` -MVCC 的优点: + -* 在并发读写数据库时,做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了并发读写的性能 -* 可以解决脏读,不可重复读等事务隔离问题(加锁也能解决),但不能解决更新丢失问题 +*** -提高读写和写写的并发性能: -* MVCC + 悲观锁:MVCC 解决读写冲突,悲观锁解决写写冲突 -* MVCC + 乐观锁:MVCC 解决读写冲突,乐观锁解决写写冲突 +#### 存储语法 +##### 变量使用 -参考文章:https://www.jianshu.com/p/8845ddca3b23 +存储过程是可以进行编程的,意味着可以使用变量、表达式、条件控制语句等,来完成比较复杂的功能 +* 定义变量:DECLARE 定义的是局部变量,只能用在 BEGIN END 范围之内 + + ```mysql + DECLARE 变量名 数据类型 [DEFAULT 默认值]; + ``` + +* 变量的赋值 + ```mysql + SET 变量名 = 变量值; + SELECT 列名 INTO 变量名 FROM 表名 [WHERE 条件]; + ``` -*** +* 数据准备:表 student + ```mysql + id NAME age gender score + 1 张三 23 男 95 + 2 李四 24 男 98 + 3 王五 25 女 100 + 4 赵六 26 女 90 + ``` +* 定义两个 int 变量,用于存储男女同学的总分数 -#### 实现原理 + ```mysql + DELIMITER $ + CREATE PROCEDURE pro_test3() + BEGIN + -- 定义两个变量 + DECLARE men,women INT; + -- 查询男同学的总分数,为men赋值 + SELECT SUM(score) INTO men FROM student WHERE gender='男'; + -- 查询女同学的总分数,为women赋值 + SELECT SUM(score) INTO women FROM student WHERE gender='女'; + -- 使用变量 + SELECT men,women; + END$ + DELIMITER ; + -- 调用存储过程 + CALL pro_test3(); + ``` -##### 隐藏字段 -实现原理主要是隐藏字段,undo日志,Read View 来实现的 -数据库中的每行数据,除了自定义的字段,还有数据库隐式定义的字段: +*** -* DB_TRX_ID:6byte,最近修改事务ID,记录创建该数据或最后一次修改(修改/插入)该数据的事务ID。当每个事务开启时,都会被分配一个ID,这个 ID 是递增的 -* DB_ROLL_PTR:7byte,回滚指针,配合 undo 日志,指向上一个旧版本(存储在 rollback segment) -* DB_ROW_ID:6byte,隐含的自增ID(**隐藏主键**),如果数据表没有主键,InnoDB 会自动以 DB_ROW_ID 作为聚簇索引 +##### IF语句 -* DELETED_BIT:删除标志的隐藏字段,记录被更新或删除并不代表真的删除,而是删除位变了 +* if 语句标准语法 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MVCC版本链隐藏字段.png) + ```mysql + IF 判断条件1 THEN 执行的sql语句1; + [ELSEIF 判断条件2 THEN 执行的sql语句2;] + ... + [ELSE 执行的sql语句n;] + END IF; + ``` +* 数据准备:表 student + ```mysql + id NAME age gender score + 1 张三 23 男 95 + 2 李四 24 男 98 + 3 王五 25 女 100 + 4 赵六 26 女 90 + ``` + +* 根据总成绩判断:全班 380 分及以上学习优秀、320 ~ 380 学习良好、320 以下学习一般 + ```mysql + DELIMITER $ + CREATE PROCEDURE pro_test4() + BEGIN + DECLARE total INT; -- 定义总分数变量 + DECLARE description VARCHAR(10); -- 定义分数描述变量 + SELECT SUM(score) INTO total FROM student; -- 为总分数变量赋值 + -- 判断总分数 + IF total >= 380 THEN + SET description = '学习优秀'; + ELSEIF total >=320 AND total < 380 THEN + SET description = '学习良好'; + ELSE + SET description = '学习一般'; + END IF; + END$ + DELIMITER ; + -- 调用pro_test4存储过程 + CALL pro_test4(); + ``` @@ -2674,65 +2795,155 @@ MVCC 的优点: -##### undo - -undo log 是逻辑日志,记录的是每个事务对数据执行的操作,而不是记录的全部数据,需要根据 undo log 逆推出以往事务的数据 - -undo log 的作用: +##### 参数传递 -* 保证事务进行 rollback 时的原子性和一致性,当事务进行回滚的时候可以用 undo log 的数据进行恢复 -* 用于 MVCC 快照读,通过读取 undo log 的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据版本 +* 参数传递的语法 -undo log 主要分为两种: + IN:代表输入参数,需要由调用者传递实际数据,默认的 + OUT:代表输出参数,该参数可以作为返回值 + INOUT:代表既可以作为输入参数,也可以作为输出参数 -* insert undo log:事务在 insert 新记录时产生的 undo log,只在事务回滚时需要,并且在事务提交后可以被立即丢弃 + ```mysql + DELIMITER $ + + -- 标准语法 + CREATE PROCEDURE 存储过程名称([IN|OUT|INOUT] 参数名 数据类型) + BEGIN + 执行的sql语句; + END$ + + DELIMITER ; + ``` -* update undo log:事务在进行 update 或 delete 时产生的 undo log,在事务回滚时需要,在快照读时也需要。不能随意删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被 purge 线程统一清除 +* 输入总成绩变量,代表学生总成绩,输出分数描述变量,代表学生总成绩的描述 -每次对数据库记录进行改动,都会将旧值放到一条 undo 日志中,算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,把这个链表称之为**版本链**,版本链的头节点就是当前记录最新的值,链尾就是最早的旧记录 + ```mysql + DELIMITER $ + + CREATE PROCEDURE pro_test6(IN total INT, OUT description VARCHAR(10)) + BEGIN + -- 判断总分数 + IF total >= 380 THEN + SET description = '学习优秀'; + ELSEIF total >= 320 AND total < 380 THEN + SET description = '学习不错'; + ELSE + SET description = '学习一般'; + END IF; + END$ + + DELIMITER ; + -- 调用pro_test6存储过程 + CALL pro_test6(310,@description); + CALL pro_test6((SELECT SUM(score) FROM student), @description); + -- 查询总成绩描述 + SELECT @description; + ``` - +* 查看参数方法 -* 有个事务插入 persion 表一条新记录,name 为 Jerry,age 为 24 + * @变量名 : **用户会话变量**,代表整个会话过程他都是有作用的,类似于全局变量 + * @@变量名 : **系统变量** -* 事务 1 修改该行数据时,数据库会先对该行加排他锁,然后先记录 undo log,然后修改该行 name 为 Tom,并且修改隐藏字段的事务 ID 为当前事务 1 的 ID(默认为 1 之后递增),回滚指针指向拷贝到 undo log 的副本记录,事务提交后,释放锁 -* 以此类推 + -补充知识:purge 线程 +*** -* 为了实现 InnoDB 的 MVCC 机制,更新或者删除操作都只是设置一下老记录的 deleted_bit,并不真正将过时的记录删除,为了节省磁盘空间,InnoDB 有专门的 purge 线程来清理 deleted_bit 为 true 的记录 -* purge 线程维护了一个 Read view(这个 Read view 相当于系统中最老活跃事务的 Read view),如果某个记录的 deleted_bit 为 true,并且 DB_TRX_ID 相对于 purge 线程的 Read view 可见,那么这条记录一定是可以被安全清除的 +##### CASE -*** +* 标准语法 1 + ```mysql + CASE 表达式 + WHEN 值1 THEN 执行sql语句1; + [WHEN 值2 THEN 执行sql语句2;] + ... + [ELSE 执行sql语句n;] + END CASE; + ``` +* 标准语法 2 -##### 读视图 + ```mysql + sCASE + WHEN 判断条件1 THEN 执行sql语句1; + [WHEN 判断条件2 THEN 执行sql语句2;] + ... + [ELSE 执行sql语句n;] + END CASE; + ``` -Read View 是事务进行**快照读**操作时产生的读视图,该事务执行快照读的那一刻会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的 ID,用来做可见性判断,根据视图判断当前事务能够看到哪个版本的数据 +* 演示 -注意:这里的快照并不是把所有的数据拷贝一份副本,而是由 undo log 记录的逻辑日志,根据库中的数据进行计算出历史数据 + ```mysql + DELIMITER $ + CREATE PROCEDURE pro_test7(IN total INT) + BEGIN + -- 定义变量 + DECLARE description VARCHAR(10); + -- 使用case判断 + CASE + WHEN total >= 380 THEN + SET description = '学习优秀'; + WHEN total >= 320 AND total < 380 THEN + SET description = '学习不错'; + ELSE + SET description = '学习一般'; + END CASE; + + -- 查询分数描述信息 + SELECT description; + END$ + DELIMITER ; + -- 调用pro_test7存储过程 + CALL pro_test7(390); + CALL pro_test7((SELECT SUM(score) FROM student)); + ``` -工作流程:将版本链的头节点的事务 ID(最新数据事务 ID)DB_TRX_ID 取出来,与系统当前活跃事务的 ID 对比,如果 DB_TRX_ID 不符合可见性,通过 DB_ROLL_PTR 回滚指针去取出 undo log 中的下一个 DB_TRX_ID 比较,直到找到最近的满足特定条件的 DB_TRX_ID,该事务 ID 所在的旧记录就是当前事务能看见的最新的记录 -Read View 几个属性: -- m_ids:生成 Read View 时当前系统中活跃的事务 id 列表(未提交的事务集合,当前事务也在其中) -- up_limit_id:生成 Read View 时当前系统中活跃的最小的事务 id,也就是 m_ids 中的最小值(已提交的事务集合) -- low_limit_id:生成 Read View 时系统应该分配给下一个事务的 id 值,m_ids 中的最大值加 1(未开始事务) -- creator_trx_id:生成该 Read View 的事务的事务 id,就是判断该 id 的事务能读到什么数据 +*** -creator 创建一个 Read View,进行可见性算法分析:(解决了读未提交) -* db_trx_id == creator_trx_id:表示这个数据就是当前事务自己生成的,自己生成的数据自己肯定能看见,所以这种情况下此数据对 creator 是可见的 -* db_trx_id < up_limit_id:该版本对应的事务 ID 小于 Read view 中的最小活跃事务 ID,则这个事务在当前事务之前就已经被提交了,对 creator 可见 -* db_trx_id >= low_limit_id:该版本对应的事务 ID 大于 Read view 中当前系统的最大事务 ID,则说明该数据是在当前 Read view 创建之后才产生的,对 creator 不可见 -* up_limit_id <= db_trx_id < low_limit_id:判断 db_trx_id 是否在活跃事务列表 m_ids 中 - * 在列表中,说明该版本对应的事务正在运行,数据不能显示(**不能读到未提交的数据**) - * 不在列表中,说明该版本对应的事务已经被提交,数据可以显示(**可以读到已经提交的数据**) +##### WHILE + +* while 循环语法 + + ```mysql + WHILE 条件判断语句 DO + 循环体语句; + 条件控制语句; + END WHILE; + ``` + +* 计算 1~100 之间的偶数和 + + ```mysql + DELIMITER $ + CREATE PROCEDURE pro_test6() + BEGIN + -- 定义求和变量 + DECLARE result INT DEFAULT 0; + -- 定义初始化变量 + DECLARE num INT DEFAULT 1; + -- while循环 + WHILE num <= 100 DO + IF num % 2 = 0 THEN + SET result = result + num; + END IF; + SET num = num + 1; + END WHILE; + -- 查询求和结果 + SELECT result; + END$ + DELIMITER ; + + -- 调用pro_test6存储过程 + CALL pro_test6(); + ``` @@ -2740,46 +2951,97 @@ creator 创建一个 Read View,进行可见性算法分析:(解决了读 -##### 工作流程 +##### REPEAT -表 user 数据 +* repeat 循环标准语法 -```sh -id name age -1 张三 18 -``` + ```mysql + 初始化语句; + REPEAT + 循环体语句; + 条件控制语句; + UNTIL 条件判断语句 + END REPEAT; + ``` -Transaction 20: +* 计算 1~10 之间的和 -```mysql -START TRANSACTION; -- 开启事务 -UPDATE user SET name = '李四' WHERE id = 1; -UPDATE user SET name = '王五' WHERE id = 1; -``` + ```mysql + DELIMITER $ + CREATE PROCEDURE pro_test9() + BEGIN + -- 定义求和变量 + DECLARE result INT DEFAULT 0; + -- 定义初始化变量 + DECLARE num INT DEFAULT 1; + -- repeat循环 + REPEAT + -- 累加 + SET result = result + num; + -- 让num+1 + SET num = num + 1; + -- 停止循环 + UNTIL num > 10 + END REPEAT; + -- 查询求和结果 + SELECT result; + END$ + + DELIMITER ; + -- 调用pro_test9存储过程 + CALL pro_test9(); + ``` -Transaction 60: -```mysql -START TRANSACTION; -- 开启事务 --- 操作表的其他数据 -``` -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MVCC工作流程1.png) -ID 为 0 的事务创建 Read View: +*** -* m_ids:20、60 -* up_limit_id:20 -* low_limit_id:61 -* creator_trx_id:0 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MVCC工作流程2.png) -只有红框部分才复合条件,所以只有张三对应的版本的数据可以被看到 +##### LOOP +LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定义,通常可以使用 LEAVE 语句实现,如果不加退出循环的语句,那么就变成了死循环 +* loop 循环标准语法 -参考视频:https://www.bilibili.com/video/BV1t5411u7Fg + ```mysql + [循环名称:] LOOP + 条件判断语句 + [LEAVE 循环名称;] + 循环体语句; + 条件控制语句; + END LOOP 循环名称; + ``` + +* 计算 1~10 之间的和 + + ```mysql + DELIMITER $ + CREATE PROCEDURE pro_test10() + BEGIN + -- 定义求和变量 + DECLARE result INT DEFAULT 0; + -- 定义初始化变量 + DECLARE num INT DEFAULT 1; + -- loop循环 + l:LOOP + -- 条件成立,停止循环 + IF num > 10 THEN + LEAVE l; + END IF; + -- 累加 + SET result = result + num; + -- 让num+1 + SET num = num + 1; + END LOOP l; + -- 查询求和结果 + SELECT result; + END$ + DELIMITER ; + -- 调用pro_test10存储过程 + CALL pro_test10(); + ``` @@ -2787,57 +3049,361 @@ ID 为 0 的事务创建 Read View: -#### RC RR +##### 游标 -Read View 用于支持 RC(Read Committed,读已提交)和 RR(Repeatable Read,可重复读)隔离级别的实现 +游标是用来存储查询结果集的数据类型,在存储过程和函数中可以使用光标对结果集进行循环的处理 +* 游标可以遍历返回的多行结果,每次拿到一整行数据 +* 简单来说游标就类似于集合的迭代器遍历 +* MySQL 中的游标只能用在存储过程和函数中 -RR、RC 生成时机: +游标的语法 -- RC 隔离级别下,每个快照读都会生成并获取最新的 Read View -- RR 隔离级别下,则是同一个事务中的第一个快照读才会创建 Read View +* 创建游标 -RC、RR 级别下的 InnoDB 快照读区别 + ```mysql + DECLARE 游标名称 CURSOR FOR 查询sql语句; + ``` -- RC 级别下的,事务中每次快照读都会新生成一个 Read View,这就是在 RC 级别下的事务中可以看到别的事务提交的更新的原因 +* 打开游标 -- RR 级别下的某个事务的对某条记录的第一次快照读会创建一个 Read View, 将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,使用的是同一个Read View,所以一个事务的查询结果每次都是相同的 + ```mysql + OPEN 游标名称; + ``` - 当前事务在其他事务提交之前使用过快照读,那么以后其他事务对数据的修改都是不可见的,就算以后其他事务提交了数据也不可见;早于 Read View 创建的事务所做的修改并提交的均是可见的 +* 使用游标获取数据 -解决幻读问题: + ```mysql + FETCH 游标名称 INTO 变量名1,变量名2,...; + ``` -- 快照读:通过 MVCC 来进行控制的,在可重复读隔离级别下,普通查询是快照读,是不会看到别的事务插入的数据的,但是**并不能完全避免幻读** +* 关闭游标 - 场景:RR 级别,T1 事务开启,创建 Read View,此时 T2 去 INSERT 新的一行然后提交,然后 T1去 UPDATE 该行会发现更新成功,因为 Read View 并不能阻止事务去更新数据,并且把这条新记录的 trx_id 给变为当前的事务 id,对当前事务就是可见的了 + ```mysql + CLOSE 游标名称; + ``` -- 当前读:通过 next-key 锁(行锁 + 间隙锁)来解决问题 +* Mysql 通过一个 Error handler 声明来判断指针是否到尾部,并且必须和创建游标的 SQL 语句声明在一起: + + ```mysql + DECLARE EXIT HANDLER FOR NOT FOUND (do some action,一般是设置标志变量) + ``` + + + +游标的基本使用 + +* 数据准备:表 student + ```mysql + id NAME age gender score + 1 张三 23 男 95 + 2 李四 24 男 98 + 3 王五 25 女 100 + 4 赵六 26 女 90 + ``` + +* 创建 stu_score 表 + + ```mysql + CREATE TABLE stu_score( + id INT PRIMARY KEY AUTO_INCREMENT, + score INT + ); + ``` + +* 将student表中所有的成绩保存到stu_score表中 + ```mysql + DELIMITER $ + + CREATE PROCEDURE pro_test12() + BEGIN + -- 定义成绩变量 + DECLARE s_score INT; + -- 定义标记变量 + DECLARE flag INT DEFAULT 0; + + -- 创建游标,查询所有学生成绩数据 + DECLARE stu_result CURSOR FOR SELECT score FROM student; + -- 游标结束后,将标记变量改为1 这两个必须声明在一起 + DECLARE EXIT HANDLER FOR NOT FOUND SET flag = 1; + + -- 开启游标 + OPEN stu_result; + -- 循环使用游标 + REPEAT + -- 使用游标,遍历结果,拿到数据 + FETCH stu_result INTO s_score; + -- 将数据保存到stu_score表中 + INSERT INTO stu_score VALUES (NULL,s_score); + UNTIL flag=1 + END REPEAT; + -- 关闭游标 + CLOSE stu_result; + END$ + + DELIMITER ; + + -- 调用pro_test12存储过程 + CALL pro_test12(); + -- 查询stu_score表 + SELECT * FROM stu_score; + ``` + + + + + +*** + + + +#### 存储函数 + +存储函数和存储过程是非常相似的,存储函数可以做的事情,存储过程也可以做到 + +存储函数有返回值,存储过程没有返回值(参数的 out 其实也相当于是返回数据了) + +* 创建存储函数 + + ```mysql + DELIMITER $ + -- 标准语法 + CREATE FUNCTION 函数名称(参数 数据类型) + RETURNS 返回值类型 + BEGIN + 执行的sql语句; + RETURN 结果; + END$ + + DELIMITER ; + ``` + +* 调用存储函数,因为有返回值,所以使用 SELECT 调用 + + ```mysql + SELECT 函数名称(实际参数); + ``` + +* 删除存储函数 + + ```mysql + DROP FUNCTION 函数名称; + ``` + +* 定义存储函数,获取学生表中成绩大于95分的学生数量 + + ```mysql + DELIMITER $ + CREATE FUNCTION fun_test() + RETURN INT + BEGIN + -- 定义统计变量 + DECLARE result INT; + -- 查询成绩大于95分的学生数量,给统计变量赋值 + SELECT COUNT(score) INTO result FROM student WHERE score > 95; + -- 返回统计结果 + SELECT result; + END + DELIMITER ; + -- 调用fun_test存储函数 + SELECT fun_test(); + ``` + + + + + +*** + + + +### 触发器 + +#### 基本介绍 + +触发器是与表有关的数据库对象,在 insert/update/delete 之前或之后触发并执行触发器中定义的 SQL 语句 + +* 触发器的这种特性可以协助应用在数据库端确保数据的完整性 、日志记录 、数据校验等操作 + +- 使用别名 NEW 和 OLD 来引用触发器中发生变化的记录内容,这与其他的数据库是相似的 +- 现在触发器还只支持行级触发,不支持语句级触发 + +| 触发器类型 | OLD的含义 | NEW的含义 | +| --------------- | ------------------------------ | ------------------------------ | +| INSERT 型触发器 | 无 (因为插入前状态无数据) | NEW 表示将要或者已经新增的数据 | +| UPDATE 型触发器 | OLD 表示修改之前的数据 | NEW 表示将要或已经修改后的数据 | +| DELETE 型触发器 | OLD 表示将要或者已经删除的数据 | 无 (因为删除后状态无数据) | + + + +*** + + + +#### 基本操作 + +* 创建触发器 + + ```mysql + DELIMITER $ + + CREATE TRIGGER 触发器名称 + BEFORE|AFTER INSERT|UPDATE|DELETE + ON 表名 + [FOR EACH ROW] -- 行级触发器 + BEGIN + 触发器要执行的功能; + END$ + + DELIMITER ; + ``` + +* 查看触发器的状态、语法等信息 + + ```mysql + SHOW TRIGGERS; + ``` + +* 删除触发器,如果没有指定 schema_name,默认为当前数据库 + + ```mysql + DROP TRIGGER [schema_name.]trigger_name; + ``` + + + +*** + + + +#### 触发演示 + +通过触发器记录账户表的数据变更日志。包含:增加、修改、删除 + +* 数据准备 + + ```mysql + -- 创建db9数据库 + CREATE DATABASE db9; + -- 使用db9数据库 + USE db9; + ``` + + ```mysql + -- 创建账户表account + CREATE TABLE account( + id INT PRIMARY KEY AUTO_INCREMENT, -- 账户id + NAME VARCHAR(20), -- 姓名 + money DOUBLE -- 余额 + ); + -- 添加数据 + INSERT INTO account VALUES (NULL,'张三',1000),(NULL,'李四',2000); + ``` + + ```mysql + -- 创建日志表account_log + CREATE TABLE account_log( + id INT PRIMARY KEY AUTO_INCREMENT, -- 日志id + operation VARCHAR(20), -- 操作类型 (insert update delete) + operation_time DATETIME, -- 操作时间 + operation_id INT, -- 操作表的id + operation_params VARCHAR(200) -- 操作参数 + ); + ``` + +* 创建 INSERT 型触发器 + ```mysql + DELIMITER $ + + CREATE TRIGGER account_insert + AFTER INSERT + ON account + FOR EACH ROW + BEGIN + INSERT INTO account_log VALUES (NULL,'INSERT',NOW(),new.id,CONCAT('插入后{id=',new.id,',name=',new.name,',money=',new.money,'}')); + END$ + + DELIMITER ; + ``` + ```mysql + -- 向account表添加记录 + INSERT INTO account VALUES (NULL,'王五',3000); + + -- 查询日志表 + SELECT * FROM account_log; + /* + id operation operation_time operation_id operation_params + 1 INSERT 2021-01-26 19:51:11 3 插入后{id=3,name=王五money=2000} + */ + ``` + +* 创建 UPDATE 型触发器 -*** + ```mysql + DELIMITER $ + + CREATE TRIGGER account_update + AFTER UPDATE + ON account + FOR EACH ROW + BEGIN + INSERT INTO account_log VALUES (NULL,'UPDATE',NOW(),new.id,CONCAT('修改前{id=',old.id,',name=',old.name,',money=',old.money,'}','修改后{id=',new.id,',name=',new.name,',money=',new.money,'}')); + END$ + + DELIMITER ; + ``` + ```mysql + -- 修改account表 + UPDATE account SET money=3500 WHERE id=3; + + -- 查询日志表 + SELECT * FROM account_log; + /* + id operation operation_time operation_id operation_params + 2 UPDATE 2021-01-26 19:58:54 2 更新前{id=2,name=李四money=1000} + 更新后{id=2,name=李四money=200} + */ + ``` + -### 持久特性 +* 创建 DELETE 型触发器 -#### 实现原理 + ```mysql + DELIMITER $ + + CREATE TRIGGER account_delete + AFTER DELETE + ON account + FOR EACH ROW + BEGIN + INSERT INTO account_log VALUES (NULL,'DELETE',NOW(),old.id,CONCAT('删除前{id=',old.id,',name=',old.name,',money=',old.money,'}')); + END$ + + DELIMITER ; + ``` -持久性是指一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,接下来的其他操作或故障不应该对其有任何影响。 + ```mysql + -- 删除account表数据 + DELETE FROM account WHERE id=3; + + -- 查询日志表 + SELECT * FROM account_log; + /* + id operation operation_time operation_id operation_params + 3 DELETE 2021-01-26 20:02:48 3 删除前{id=3,name=王五money=2000} + */ + ``` -Buffer Pool 是一片内存空间,可以通过 innodb_buffer_pool_size 来控制 Buffer Pool 的大小(内存优化部分会详解参数) -* Change Buffer 是 Buffer Pool 里的内存,不能无限增大,用来对增删改操作提供缓存 -* Change Buffer 的大小可以通过参数 innodb_change_buffer_max_size 来动态设置,设置为 50 时表示 Change Buffer 的大小最多只能占用 Buffer Pool 的 50% -* 补充知识:**唯一索引的更新不能使用 Buffer**,一般只有普通索引可以使用,直接写入 Buffer 就结束 -InnoDB 的数据是按数据页为单位来读写,每个数据页的大小默认是 16KB。数据是存放在磁盘中,每次读写数据都需要磁盘 IO,效率会很低。InnoDB 提供了缓存 Change Buffer,Buffer 中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲: -* 从数据库读取数据时,会首先从缓存中读取,如果缓存中没有,则从磁盘读取后放入 Buffer Pool -* 向数据库写入数据时,会首先写入缓存,缓存中修改的数据会**定期刷新**到磁盘,这一过程称为刷脏 @@ -2845,84 +3411,110 @@ InnoDB 的数据是按数据页为单位来读写,每个数据页的大小默 -#### 数据恢复 - -Buffer Pool 的使用提高了读写数据的效率,但是也带了新的问题:如果 MySQL 宕机,此时 Buffer Pool 中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证,所以引入 redo log - -* 当数据修改时,先修改 Change Buffer 中的数据,然后在 redo log buffer 记录这次操作 -* 如果 MySQL 宕机,InnoDB 判断一个数据页在崩溃恢复时丢失了更新,就会将它读到内存,然后根据 redo log 内容更新内存,更新完成后,内存页变成脏页,然后进行刷脏(buffer pool 的任务) -redo log **记录数据页的物理修改**,而不是某一行或某几行的修改,用来恢复提交后的物理数据页,且只能恢复到最后一次提交的位置 -redo log 采用的是 WAL(Write-ahead logging,**预写式日志**),所有修改要先写入日志,再更新到磁盘,保证了数据不会因 MySQL 宕机而丢失,从而满足了持久性要求 +## 存储引擎 -redo log 也需要在事务提交时将日志写入磁盘,但是比将内存中的 Buffer Pool 修改的数据写入磁盘的速度快: +### 基本介绍 -* 刷脏是随机 IO,因为每次修改的数据位置随机,但写 redo log 是尾部追加操作,属于顺序 IO -* 刷脏是以数据页(Page)为单位的,一个页上的一个小修改都要整页写入,而 redo log 中只包含真正需要写入的部分,减少无效 IO +对比其他数据库,MySQL 的架构可以在不同场景应用并发挥良好作用,主要体现在存储引擎,插件式的存储引擎架构将查询处理和其他的系统任务以及数据的存储提取分离,可以针对不同的存储需求可以选择最优的存储引擎 -InnoDB 引擎会在适当的时候,把内存中 redo log buffer 持久化到磁盘,具体的刷盘策略: +存储引擎的介绍: -* 通过修改参数 `innodb_flush_log_at_trx_commit` 设置: - * 0:表示当提交事务时,并不将缓冲区的 redo 日志写入磁盘,而是等待主线程每秒刷新一次 - * 1:在事务提交时将缓冲区的 redo 日志**同步写入**到磁盘,保证一定会写入成功 - * 2:在事务提交时将缓冲区的 redo 日志异步写入到磁盘,不能保证提交时肯定会写入,只是有这个动作 -* 如果写入 redo log buffer 的日志已经占据了 redo log buffer 总容量的一半了,此时就会刷入到磁盘文件,这时会影响执行效率,所以开发中应该**避免大事务** +- MySQL 数据库使用不同的机制存取表文件 , 机制的差别在于不同的存储方式、索引技巧、锁定水平等不同的功能和能力,在 MySQL 中,将这些不同的技术及配套的功能称为存储引擎 +- Oracle、SqlServer 等数据库只有一种存储引擎,MySQL **提供了插件式的存储引擎架构**,所以 MySQL 存在多种存储引擎 , 就会让数据库采取了不同的处理数据的方式和扩展功能 +- 在关系型数据库中数据的存储是以表的形式存进行,所以存储引擎也称为表类型(存储和操作此表的类型) +- 通过选择不同的引擎,能够获取最佳的方案, 也能够获得额外的速度或者功能,提高程序的整体效果。 -刷脏策略: +MySQL 支持的存储引擎: -* redo log 文件是固定大小的,如果写满了就要擦除以前的记录,在擦除之前需要把旧记录更新到磁盘中的数据文件中 -* Buffer Pool 内存不足,需要淘汰部分数据页,如果淘汰的是脏页,就要先将脏页写到磁盘(大事务) -* 系统空闲时,后台线程会自动进行刷脏 -* MySQL 正常关闭时,会把内存的脏页都 flush 到磁盘上 +- MySQL 支持的引擎包括:InnoDB、MyISAM、MEMORY、Archive、Federate、CSV、BLACKHOLE 等 +- MySQL5.5 之前的默认存储引擎是 MyISAM,5.5 之后就改为了 InnoDB -*** +**** -#### 工作流程 +### 引擎对比 -MySQL中还存在 binlog(二进制日志)也可以记录写操作并用于数据的恢复,**保证数据不丢失**,二者的区别是: +MyISAM 存储引擎: -* 作用不同:redo log 是用于 crash recovery (故障恢复),保证 MySQL 宕机也不会影响持久性;binlog 是用于 point-in-time recovery 的,保证服务器可以基于时间点恢复数据,此外 binlog 还用于主从复制 +* 特点:不支持事务和外键,读取速度快,节约资源 +* 应用场景:查询和插入操作为主,只有很少更新和删除操作,并对事务的完整性、并发性要求不高 +* 存储方式: + * 每个 MyISAM 在磁盘上存储成 3 个文件,其文件名都和表名相同,拓展名不同 + * 表的定义保存在 .frm 文件,表数据保存在 .MYD (MYData) 文件中,索引保存在 .MYI (MYIndex) 文件中 -* 层次不同:redo log 是 InnoDB 存储引擎实现的,而 binlog 是MySQL的服务器层实现的,同时支持 InnoDB 和其他存储引擎 +InnoDB 存储引擎:(MySQL5.5 版本后默认的存储引擎) -* 内容不同:redo log 是物理日志,内容基于磁盘的 Page;binlog 的内容是二进制的,根据 binlog_format 参数的不同,可能基于SQL 语句、基于数据本身或者二者的混合(日志部分详解) +- 特点:**支持事务**和外键操作,支持并发控制。对比 MyISAM 的存储引擎,InnoDB 写的处理效率差一些,并且会占用更多的磁盘空间以保留数据和索引 +- 应用场景:对事务的完整性有比较高的要求,在并发条件下要求数据的一致性,读写频繁的操作 +- 存储方式: + - 使用共享表空间存储, 这种方式创建的表的表结构保存在 .frm 文件中, 数据和索引保存在 innodb_data_home_dir 和 innodb_data_file_path 定义的表空间中,可以是多个文件 + - 使用多表空间存储,创建的表的表结构存在 .frm 文件中,每个表的数据和索引单独保存在 .ibd 中 -* 写入时机不同:binlog 在事务提交时一次写入;redo log 的写入时机相对多元 +MEMORY 存储引擎: -两种日志在 update 更新数据的**作用时机**: +- 特点:每个 MEMORY 表实际对应一个磁盘文件 ,该文件中只存储表的结构,表数据保存在内存中,且默认使用 HASH 索引,这样有利于数据的快速处理,在需要快速定位记录可以提供更快的访问,但是服务一旦关闭,表中的数据就会丢失,数据存储不安全 +- 应用场景:通常用于更新不太频繁的小表,用以快速得到访问结果,类似缓存 +- 存储方式:表结构保存在 .frm 中 -```sql -update T set c=c+1 where ID=2; -``` +MERGE存储引擎: - +* 特点: -流程说明:执行引擎将这行新数据更新到内存中(Buffer Pool)后,然后会将这个更新操作记录到 redo log buffer 里,此时 redo log 处于 prepare 状态,代表执行完成随时可以提交事务,然后执行器生成这个操作的 binlog 并**把 binlog 写入磁盘**,在提交事务后 **redo log 也持久化到磁盘** + * 是一组 MyISAM 表的组合,这些 MyISAM 表必须结构完全相同,通过将不同的表分布在多个磁盘上 + * MERGE 表本身并没有存储数据,对 MERGE 类型的表可以进行查询、更新、删除操作,这些操作实际上是对内部的 MyISAM 表进行的 -redo log 和 binlog 都可以用于表示事务的提交状态,而**两阶段提交就是让这两个状态保持逻辑上的一致**,也有利于主从复制,更好的保持主从数据的一致性 +* 应用场景:将一系列等同的 MyISAM 表以逻辑方式组合在一起,并作为一个对象引用他们,适合做数据仓库 -故障恢复数据: +* 操作方式: -* 如果在时刻 A 发生了崩溃(crash),由于此时 binlog 还没写,redo log 也没提交,所以数据恢复的时候这个事务会回滚 -* 如果在时刻 B 发生了崩溃,redo log 和 binlog 有一个共同的数据字段叫 XID,崩溃恢复的时候,会按顺序扫描 redo log: - * 如果 redo log 里面的事务是完整的,也就是已经有了 commit 标识,说明 binlog 也已经记录完整,直接从 redo log 恢复数据 - * 如果 redo log 里面的事务只有 prepare,就根据 XID 去 binlog 中判断对应的事务是否存在并完整,如果完整可以从 binlog 恢复 redo log 的信息,进而恢复数据,提交事务 + * 插入操作是通过 INSERT_METHOD 子句定义插入的表,使用 FIRST 或 LAST 值使得插入操作被相应地作用在第一或者最后一个表上;不定义这个子句或者定义为 NO,表示不能对 MERGE 表执行插入操作 + * 对 MERGE 表进行 DROP 操作,但是这个操作只是删除 MERGE 表的定义,对内部的表是没有任何影响的 + ```mysql + CREATE TABLE order_1( + )ENGINE = MyISAM DEFAULT CHARSET=utf8; + + CREATE TABLE order_2( + )ENGINE = MyISAM DEFAULT CHARSET=utf8; + + CREATE TABLE order_all( + -- 结构与MyISAM表相同 + )ENGINE = MERGE UNION = (order_1,order_2) INSERT_METHOD=LAST DEFAULT CHARSET=utf8; + ``` -判断一个事务的 binlog 是否完整的方法: + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MERGE.png) -* statement 格式的 binlog,最后会有 COMMIT -* row 格式的 binlog,最后会有一个 XID event -* MySQL 5.6.2 版本以后,引入了 binlog-checksum 参数用来验证 binlog 内容的正确性 +| 特性 | MyISAM | InnoDB | MEMORY | +| ------------ | ---------------------------- | ------------- | ------------------ | +| 存储限制 | 有(平台对文件系统大小的限制) | 64TB | 有(平台的内存限制) | +| **事务安全** | **不支持** | **支持** | **不支持** | +| **锁机制** | **表锁** | **表锁/行锁** | **表锁** | +| B+Tree索引 | 支持 | 支持 | 支持 | +| 哈希索引 | 不支持 | 不支持 | 支持 | +| 全文索引 | 支持 | 支持 | 不支持 | +| 集群索引 | 不支持 | 支持 | 不支持 | +| 数据索引 | 不支持 | 支持 | 支持 | +| 数据缓存 | 不支持 | 支持 | N/A | +| 索引缓存 | 支持 | 支持 | N/A | +| 数据可压缩 | 支持 | 不支持 | 不支持 | +| 空间使用 | 低 | 高 | N/A | +| 内存使用 | 低 | 高 | 中等 | +| 批量插入速度 | 高 | 低 | 高 | +| **外键** | **不支持** | **支持** | **不支持** | +面试问题:MyIsam 和 InnoDB 的区别? +* 事务:InnoDB 支持事务,MyISAM 不支持事务 +* 外键:InnoDB 支持外键,MyISAM 不支持外键 +* 索引:InnoDB 是聚集(聚簇)索引,MyISAM 是非聚集(非聚簇)索引 -参考文章:https://time.geekbang.org/column/article/73161 +* 锁粒度:InnoDB 最小的锁粒度是行锁,MyISAM 最小的锁粒度是表锁 +* 存储结构:参考本节上半部分 @@ -2930,100 +3522,118 @@ redo log 和 binlog 都可以用于表示事务的提交状态,而**两阶段 -#### 系统优化 - -系统在进行刷脏时会占用一部分系统资源,会影响系统的性能,产生系统抖动 +### 引擎操作 -* 一个查询要淘汰的脏页个数太多,会导致查询的响应时间明显变长 -* 日志写满,更新全部堵住,写性能跌为 0,这种情况对敏感业务来说,是不能接受的 +* 查询数据库支持的存储引擎 -InnoDB 刷脏页的控制策略: + ```mysql + SHOW ENGINES; + SHOW VARIABLES LIKE '%storage_engine%'; -- 查看Mysql数据库默认的存储引擎 + ``` -* `innodb_io_capacity` 参数代表磁盘的读写能力,建议设置成磁盘的 IOPS(每秒的 IO 次数) +* 查询某个数据库中所有数据表的存储引擎 -* 刷脏速度参考两个因素:脏页比例和 redo log 写盘速度 - * 参数 `innodb_max_dirty_pages_pct` 是脏页比例上限,默认值是 75%,InnoDB 会根据当前的脏页比例,算出一个范围在 0 到 100 之间的数字 - * InnoDB 每次写入的日志都有一个序号,当前写入的序号跟 checkpoint 对应的序号之间的差值,InnoDB 根据差值算出一个范围在 0 到 100 之间的数字 - * 两者较大的值记为 R,执行引擎按照 innodb_io_capacity 定义的能力乘以 R% 来控制刷脏页的速度 + ```mysql + SHOW TABLE STATUS FROM 数据库名称; + ``` -* `innodb_flush_neighbors` 参数置为 1 代表控制刷脏时检查相邻的数据页,如果也是脏页就一起刷脏,并检查邻居的邻居,这个行为会一直蔓延直到不是脏页,在 MySQL 8.0 中该值的默认值是 0,不建议开启此功能 +* 查询某个数据库中某个数据表的存储引擎 + ```mysql + SHOW TABLE STATUS FROM 数据库名称 WHERE NAME = '数据表名称'; + ``` +* 创建数据表,指定存储引擎 + ```mysql + CREATE TABLE 表名( + 列名,数据类型, + ... + )ENGINE = 引擎名称; + ``` +* 修改数据表的存储引擎 -*** + ```mysql + ALTER TABLE 表名 ENGINE = 引擎名称; + ``` -### 隔离级别 -事务的隔离级别:多个客户端操作时,各个客户端的事务之间应该是隔离的,**不同的事务之间不该互相影响**,而如果多个事务操作同一批数据时,则需要设置不同的隔离级别,否则就会产生问题。 -隔离级别分类: -| 隔离级别 | 名称 | 会引发的问题 | 数据库默认隔离级别 | -| ---------------- | -------- | -------------------------------- | ------------------- | -| read uncommitted | 读未提交 | 脏读、不可重复读、幻读 | | -| read committed | 读已提交 | 不可重复读、幻读 | Oracle / SQL Server | -| repeatable read | 可重复读 | 幻读 | MySQL | -| serializable | 串行化 | 无(因为写会加写锁,读会加读锁) | | -一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差 -* 丢失更新 (Lost Update):当两个或多个事务选择同一行,最初的事务修改的值,被后面事务修改的值覆盖,所有的隔离级别都可以避免丢失更新(行锁) +*** -* 脏读 (Dirty Reads):在事务中先后两次读取同一个数据,两次读取的结果不一样,原因是在一个事务处理过程中读取了另一个**未提交**的事务中的数据 -* 不可重复读 (Non-Repeatable Reads):在事务中先后两次读取同一个数据,两次读取的结果不一样,原因是在一个事务处理过程中读取了另一个事务中修改并**已提交**的数据 - > 可重复读的意思是不管读几次,结果都一样,可以重复的读,可以理解为快照读,要读的数据集不会发生变化 -* 幻读 (Phantom Reads):在事务中按某个条件先后两次查询数据库,后一次查询查到了前一次查询没有查到的行,**数据条目**发生了变化。比如查询某数据不存在,准备插入此记录,但执行插入时发现此记录已存在,无法插入 -**隔离级别操作语法:** +## 索引机制 -* 查询数据库隔离级别 +### 索引介绍 - ```mysql - SELECT @@TX_ISOLATION; - SHOW VARIABLES LIKE 'tx_isolation'; - ``` +#### 基本介绍 -* 修改数据库隔离级别 +MySQL 官方对索引的定义为:索引(index)是帮助 MySQL 高效获取数据的一种数据结构,**本质是排好序的快速查找数据结构。**在表数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式指向数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。 - ```mysql - SET GLOBAL TRANSACTION ISOLATION LEVEL 级别字符串; - ``` +**索引是在存储引擎层实现的**,所以并没有统一的索引标准,即不同存储引擎的索引的工作方式并不一样 +索引使用:一张数据表,用于保存数据;一个索引配置文件,用于保存索引;每个索引都指向了某一个数据 +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-索引的介绍.png) +左边是数据表,一共有两列七条记录,最左边的是数据记录的物理地址(注意逻辑上相邻的记录在磁盘上也并不是一定物理相邻的)。为了加快 Col2 的查找,可以维护一个右边所示的二叉查找树,每个节点分别包含索引键值和一个指向对应数据的物理地址的指针,这样就可以运用二叉查找快速获取到相应数据 +索引的优点: +* 类似于书籍的目录索引,提高数据检索的效率,降低数据库的 IO 成本 +* 通过索引列对数据进行排序,降低数据排序的成本,降低 CPU 的消耗 -*** +索引的缺点: +* 一般来说索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式**存储在磁盘**上 +* 虽然索引大大提高了查询效率,同时却也降低更新表的速度。对表进行 INSERT、UPDATE、DELETE 操作,MySQL 不仅要保存数据,还要保存一下索引文件每次更新添加了索引列的字段,还会调整因为更新所带来的键值变化后的索引信息,**但是更新数据也需要先从数据库中获取**,索引加快了获取速度,所以可以相互抵消一下。 +* 索引会影响到 WHERE 的查询条件和排序 ORDER BY 两大功能 +*** -## 存储结构 -### 视图 -#### 基本介绍 +#### 索引分类 -视图概念:视图是一种虚拟存在的数据表,这个虚拟的表并不在数据库中实际存在 +索引一般的分类如下: -本质:将一条 SELECT 查询语句的结果封装到了一个虚拟表中,所以在创建视图的时候,工作重心要放在这条 SELECT 查询语句上 +- 功能分类 + - 主键索引:一种特殊的唯一索引,不允许有空值,一般在建表时同时创建主键索引 + - 单列索引:一个索引只包含单个列,一个表可以有多个单列索引(普通索引) + - 联合索引:顾名思义,就是将单列索引进行组合 + - 唯一索引:索引列的值必须唯一,**允许有空值**,如果是联合索引,则列值组合必须唯一 + * NULL 值必须只出现一次 + * 可以声明不允许存储 NULL 值的非空唯一索引 + - 外键索引:只有 InnoDB 引擎支持外键索引,用来保证数据的一致性、完整性和实现级联操作 + +- 结构分类 + - BTree 索引:MySQL 使用最频繁的一个索引数据结构,是 InnoDB 和 MyISAM 存储引擎默认的索引类型,底层基于 B+Tree + - Hash 索引:MySQL中 Memory 存储引擎默认支持的索引类型 + - R-tree 索引(空间索引):空间索引是 MyISAM 引擎的一个特殊索引类型,主要用于地理空间数据类型 + - Full-text 索引(全文索引):快速匹配全部文档的方式。MyISAM 支持, InnoDB 不支持 FULLTEXT 类型的索引,但是 InnoDB 可以使用 sphinx 插件支持全文索引,MEMORY 引擎不支持 + + | 索引 | InnoDB | MyISAM | Memory | + | --------- | ---------------- | ------ | ------ | + | BTREE | 支持 | 支持 | 支持 | + | HASH | 不支持 | 不支持 | 支持 | + | R-tree | 不支持 | 支持 | 不支持 | + | Full-text | 5.6 版本之后支持 | 支持 | 不支持 | -作用:将一些比较复杂的查询语句的结果,封装到一个虚拟表中,再有相同查询需求时,直接查询该虚拟表 +联合索引图示:根据身高年龄建立的组合索引(height,age) -优点: +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-组合索引图.png) -* 简单:使用视图的用户不需要关心表的结构、关联条件和筛选条件,因为虚拟表中已经是过滤好的结果集 -* 安全:使用视图的用户只能访问查询的结果集,对表的权限管理并不能限制到某个行某个列 -* 数据独立,一旦视图的结构确定,可以屏蔽表结构变化对用户的影响,源表增加列对视图没有影响;源表修改列名,则可以通过修改视图来解决,不会造成对访问者的影响 @@ -3031,80 +3641,75 @@ InnoDB 刷脏页的控制策略: -#### 视图创建 +### 索引操作 -* 创建视图 +索引在创建表的时候可以同时创建, 也可以随时增加新的索引 + +* 创建索引:如果一个表中有一列是主键,那么会**默认为其创建主键索引**(主键列不需要单独创建索引) ```mysql - CREATE [OR REPLACE] - VIEW 视图名称 [(列名列表)] - AS 查询语句 - [WITH [CASCADED | LOCAL] CHECK OPTION]; + CREATE [UNIQUE|FULLTEXT] INDEX 索引名称 [USING 索引类型] ON 表名(列名...); + -- 索引类型默认是 B+TREE ``` - `WITH [CASCADED | LOCAL] CHECK OPTION` 决定了是否允许更新数据使记录不再满足视图的条件: +* 查看索引 - * LOCAL:只要满足本视图的条件就可以更新 - * CASCADED:必须满足所有针对该视图的所有视图的条件才可以更新, 默认值 + ```mysql + SHOW INDEX FROM 表名; + ``` -* 例如 +* 添加索引 ```mysql - -- 数据准备 city - id NAME cid - 1 深圳 1 - 2 上海 1 - 3 纽约 2 - 4 莫斯科 3 + -- 单列索引 + ALTER TABLE 表名 ADD INDEX 索引名称(列名); - -- 数据准备 country - id NAME - 1 中国 - 2 美国 - 3 俄罗斯 + -- 组合索引 + ALTER TABLE 表名 ADD INDEX 索引名称(列名1,列名2,...); - -- 创建city_country视图,保存城市和国家的信息(使用指定列名) - CREATE - VIEW - city_country (city_id,city_name,country_name) - AS - SELECT - c1.id, - c1.name, - c2.name - FROM - city c1, - country c2 - WHERE - c1.cid=c2.id; - ``` - + -- 主键索引 + ALTER TABLE 表名 ADD PRIMARY KEY(主键列名); + -- 外键索引(添加外键约束,就是外键索引) + ALTER TABLE 表名 ADD CONSTRAINT 外键名 FOREIGN KEY (本表外键列名) REFERENCES 主表名(主键列名); + + -- 唯一索引 + ALTER TABLE 表名 ADD UNIQUE 索引名称(列名); + + -- 全文索引(mysql只支持文本类型) + ALTER TABLE 表名 ADD FULLTEXT 索引名称(列名); + ``` -*** - +* 删除索引 + ```mysql + DROP INDEX 索引名称 ON 表名; + ``` -#### 视图查询 +* 案例练习 -* 查询所有数据表,视图也会查询出来 + 数据准备:student ```mysql - SHOW TABLES; - SHOW TABLE STATUS [\G]; + id NAME age score + 1 张三 23 99 + 2 李四 24 95 + 3 王五 25 98 + 4 赵六 26 97 ``` -* 查询视图 + 索引操作: ```mysql - SELECT * FROM 视图名称; + -- 为student表中姓名列创建一个普通索引 + CREATE INDEX idx_name ON student(NAME); + + -- 为student表中年龄列创建一个唯一索引 + CREATE UNIQUE INDEX idx_age ON student(age); ``` -* 查询某个视图创建 - ```mysql - SHOW CREATE VIEW 视图名称; - ``` + @@ -3112,39 +3717,17 @@ InnoDB 刷脏页的控制策略: -#### 视图修改 +### 聚簇索引 -视图表数据修改,会**自动修改源表中的数据**,因为更新的是视图中的基表中的数据 +#### 索引对比 -* 修改视图表中的数据 +聚簇索引是一种数据存储方式,并不是一种单独的索引类型 - ```mysql - UPDATE 视图名称 SET 列名 = 值 WHERE 条件; - ``` +* 聚簇索引的叶子节点存放的是主键值和数据行,支持覆盖索引 -* 修改视图的结构 +* 非聚簇索引的叶子节点存放的是主键值或指向数据行的指针(由存储引擎决定) - ```mysql - ALTER [ALGORITHM = {UNDEFINED | MERGE | TEMPTABLE}] - VIEW 视图名称 [(列名列表)] - AS 查询语句 - [WITH [CASCADED | LOCAL] CHECK OPTION] - - -- 将视图中的country_name修改为name - ALTER - VIEW - city_country (city_id,city_name,name) - AS - SELECT - c1.id, - c1.name, - c2.name - FROM - city c1, - country c2 - WHERE - c1.cid=c2.id; - ``` +在 Innodb 下主键索引是聚簇索引,在 MyISAM 下主键索引是非聚簇索引 @@ -3152,46 +3735,45 @@ InnoDB 刷脏页的控制策略: -#### 视图删除 +#### Innodb -* 删除视图 +##### 聚簇索引 - ```mysql - DROP VIEW 视图名称; - ``` +在 Innodb 存储引擎,B+ 树索引可以分为聚簇索引(也称聚集索引、clustered index)和辅助索引(也称非聚簇索引或二级索引、secondary index、non-clustered index) -* 如果存在则删除 +InnoDB 中,聚簇索引是按照每张表的主键构造一颗 B+ 树,叶子节点中存放的就是整张表的数据,将聚簇索引的叶子节点称为数据页 - ```mysql - DROP VIEW IF EXISTS 视图名称; - ``` +* 这个特性决定了**数据也是索引的一部分**,所以一张表只能有一个聚簇索引 +* 辅助索引的存在不影响聚簇索引中数据的组织,所以一张表可以有多个辅助索引 +聚簇索引的优点: +* 数据访问更快,聚簇索引将索引和数据保存在同一个 B+ 树中,因此从聚簇索引中获取数据比非聚簇索引更快 +* 聚簇索引对于主键的排序查找和范围查找速度非常快 +聚簇索引的缺点: +* 插入速度严重依赖于插入顺序,按照主键的顺序(递增)插入是最快的方式,否则将会出现页分裂,严重影响性能,所以对于 InnoDB 表,一般都会定义一个自增的 ID 列为主键 +* 更新主键的代价很高,将会导致被更新的行移动,所以对于 InnoDB 表,一般定义主键为不可更新 -*** +* 二级索引访问需要两次索引查找,第一次找到主键值,第二次根据主键值找到行数据 -### 存储过程 +*** -#### 基本介绍 -存储过程和函数:存储过程和函数是事先经过编译并存储在数据库中的一段 SQL 语句的集合 -存储过程和函数的好处: +##### 辅助索引 -* 提高代码的复用性 -* 减少数据在数据库和应用服务器之间的传输,提高传输效率 -* 减少代码层面的业务处理 -* **一次编译永久有效** +在聚簇索引之上创建的索引称之为辅助索引,非聚簇索引都是辅助索引,像复合索引、前缀索引、唯一索引等 -存储过程和函数的区别: +辅助索引叶子节点存储的是主键值,而不是数据的物理地址,所以访问数据需要二次查找,推荐使用覆盖索引,可以减少回表查询 -* 存储函数必须有返回值 -* 存储过程可以没有返回值 +**检索过程**:辅助索引找到主键值,再通过聚簇索引(二分)找到数据页,最后通过数据页中的 Page Directory(二分)找到对应的数据分组,遍历组内所所有的数据找到数据行 + +补充:无索引走全表查询,查到数据页后和上述步骤一致 @@ -3199,138 +3781,62 @@ InnoDB 刷脏页的控制策略: -#### 基本操作 +##### 索引实现 -DELIMITER: +InnoDB 使用 B+Tree 作为索引结构,并且 InnoDB 一定有索引 -* DELIMITER 关键字用来声明 sql 语句的分隔符,告诉 MySQL 该段命令已经结束 +主键索引: -* MySQL 语句默认的分隔符是分号,但是有时需要一条功能 sql 语句中包含分号,但是并不作为结束标识,这时使用 DELIMITER 来指定分隔符: +* 在 InnoDB 中,表数据文件本身就是按 B+Tree 组织的一个索引结构,这个索引的 key 是数据表的主键,叶子节点 data 域保存了完整的数据记录 - ```mysql - DELIMITER 分隔符 - ``` +* InnoDB 的表数据文件**通过主键聚集数据**,如果没有定义主键,会选择非空唯一索引代替,如果也没有这样的列,MySQL 会自动为 InnoDB 表生成一个**隐含字段**作为主键,这个字段长度为 6 个字节,类型为长整形(MVCC 部分的笔记提及) -存储过程的创建调用查看和删除: +辅助索引: -* 创建存储过程 +* InnoDB 的所有辅助索引(二级索引)都引用主键作为 data 域 - ```mysql - -- 修改分隔符为$ - DELIMITER $ - - -- 标准语法 - CREATE PROCEDURE 存储过程名称(参数...) - BEGIN - sql语句; - END$ - - -- 修改分隔符为分号 - DELIMITER ; - ``` +* InnoDB 表是基于聚簇索引建立的,因此 InnoDB 的索引能提供一种非常快速的主键查找性能。不过辅助索引也会包含主键列,所以不建议使用过长的字段作为主键,**过长的主索引会令辅助索引变得过大** -* 调用存储过程 +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB聚簇和辅助索引结构.png) - ```mysql - CALL 存储过程名称(实际参数); - ``` -* 查看存储过程 - ```mysql - SELECT * FROM mysql.proc WHERE db='数据库名称'; - ``` +*** -* 删除存储过程 - ```mysql - DROP PROCEDURE [IF EXISTS] 存储过程名称; - ``` -练习: +#### MyISAM -* 数据准备 +##### 非聚簇 - ```mysql - id NAME age gender score - 1 张三 23 男 95 - 2 李四 24 男 98 - 3 王五 25 女 100 - 4 赵六 26 女 90 - ``` +MyISAM 的主键索引使用的是非聚簇索引,索引文件和数据文件是分离的,**索引文件仅保存数据的地址** -* 创建 stu_group() 存储过程,封装分组查询总成绩,并按照总成绩升序排序的功能 +* 主键索引 B+ 树的节点存储了主键,辅助键索引 B+ 树存储了辅助键,表数据存储在独立的地方,这两颗 B+ 树的叶子节点都使用一个地址指向真正的表数据,对于表数据来说,这两个键没有任何差别 +* 由于索引树是独立的,通过辅助索引检索无需访问主键的索引树回表查询 + +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-聚簇索引和辅助索引检锁数据图.jpg) - ```mysql - DELIMITER $ - - CREATE PROCEDURE stu_group() - BEGIN - SELECT gender,SUM(score) getSum FROM student GROUP BY gender ORDER BY getSum ASC; - END$ - - DELIMITER ; - - -- 调用存储过程 - CALL stu_group(); - -- 删除存储过程 - DROP PROCEDURE IF EXISTS stu_group; - ``` - *** -#### 存储语法 +##### 索引实现 -##### 变量使用 +MyISAM 的索引方式也叫做非聚集的,之所以这么称呼是为了与 InnoDB 的聚集索引区分 -存储过程是可以进行编程的,意味着可以使用变量、表达式、条件控制语句等,来完成比较复杂的功能 +主键索引:MyISAM 引擎使用 B+Tree 作为索引结构,叶节点的 data 域存放的是数据记录的地址 -* 定义变量:DECLARE 定义的是局部变量,只能用在 BEGIN END 范围之内 - - ```mysql - DECLARE 变量名 数据类型 [DEFAULT 默认值]; - ``` - -* 变量的赋值 +辅助索引:MyISAM 中主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求 key 是唯一的,而辅助索引的 key 可以重复 - ```mysql - SET 变量名 = 变量值; - SELECT 列名 INTO 变量名 FROM 表名 [WHERE 条件]; - ``` +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM主键和辅助索引结构.png) -* 数据准备:表 student - ```mysql - id NAME age gender score - 1 张三 23 男 95 - 2 李四 24 男 98 - 3 王五 25 女 100 - 4 赵六 26 女 90 - ``` -* 定义两个 int 变量,用于存储男女同学的总分数 - ```mysql - DELIMITER $ - CREATE PROCEDURE pro_test3() - BEGIN - -- 定义两个变量 - DECLARE men,women INT; - -- 查询男同学的总分数,为men赋值 - SELECT SUM(score) INTO men FROM student WHERE gender='男'; - -- 查询女同学的总分数,为women赋值 - SELECT SUM(score) INTO women FROM student WHERE gender='女'; - -- 使用变量 - SELECT men,women; - END$ - DELIMITER ; - -- 调用存储过程 - CALL pro_test3(); - ``` + +参考文章:https://blog.csdn.net/lm1060891265/article/details/81482136 @@ -3338,51 +3844,27 @@ DELIMITER: -##### IF语句 +### 索引结构 -* if 语句标准语法 +#### 数据页 - ```mysql - IF 判断条件1 THEN 执行的sql语句1; - [ELSEIF 判断条件2 THEN 执行的sql语句2;] - ... - [ELSE 执行的sql语句n;] - END IF; - ``` +文件系统的最小单元是块(block),一个块的大小是 4K,系统从磁盘读取数据到内存时是以磁盘块为基本单位的,位于同一个磁盘块中的数据会被一次性读取出来,而不是需要什么取什么 -* 数据准备:表 student +InnoDB 存储引擎中有页(Page)的概念,页是 MySQL 磁盘管理的最小单位 - ```mysql - id NAME age gender score - 1 张三 23 男 95 - 2 李四 24 男 98 - 3 王五 25 女 100 - 4 赵六 26 女 90 - ``` - -* 根据总成绩判断:全班 380 分及以上学习优秀、320 ~ 380 学习良好、320 以下学习一般 +* **InnoDB 存储引擎中默认每个页的大小为 16KB,索引中一个节点就是一个数据页**,所以会一次性读取 16KB 的数据到内存 +* InnoDB 引擎将若干个地址连接磁盘块,以此来达到页的大小 16KB +* 在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘 I/O 次数,提高查询效率 - ```mysql - DELIMITER $ - CREATE PROCEDURE pro_test4() - BEGIN - DECLARE total INT; -- 定义总分数变量 - DECLARE description VARCHAR(10); -- 定义分数描述变量 - SELECT SUM(score) INTO total FROM student; -- 为总分数变量赋值 - -- 判断总分数 - IF total >= 380 THEN - SET description = '学习优秀'; - ELSEIF total >=320 AND total < 380 THEN - SET description = '学习良好'; - ELSE - SET description = '学习一般'; - END IF; - END$ - DELIMITER ; - -- 调用pro_test4存储过程 - CALL pro_test4(); - ``` +数据页物理结构,从上到下: +* File Header:上一页和下一页的指针、该页的类型(索引页、数据页、日志页等)、**校验和**等信息 +* Page Header:记录状态信息 +* Infimum + Supremum:当前页的最小记录和最大记录(头尾指针),Infimum 所在分组只有一条记录,Supremum 所在分组可以有 1 ~ 8 条记录,剩余的分组可以有 4 ~ 8 条记录 +* User Records:存储数据的记录 +* Free Space:尚未使用的存储空间 +* Page Directory:分组的目录,可以通过目录快速定位(二分法)数据的分组 +* File Trailer:检验和字段,在刷脏过程中,页首和页尾的校验和一致才能说明页面刷新成功,二者不同说明刷新期间发生了错误;LSN 字段,也是用来校验页面的完整性 @@ -3390,155 +3872,60 @@ DELIMITER: -##### 参数传递 - -* 参数传递的语法 - - IN:代表输入参数,需要由调用者传递实际数据,默认的 - OUT:代表输出参数,该参数可以作为返回值 - INOUT:代表既可以作为输入参数,也可以作为输出参数 - - ```mysql - DELIMITER $ - - -- 标准语法 - CREATE PROCEDURE 存储过程名称([IN|OUT|INOUT] 参数名 数据类型) - BEGIN - 执行的sql语句; - END$ - - DELIMITER ; - ``` +#### BTree -* 输入总成绩变量,代表学生总成绩,输出分数描述变量,代表学生总成绩的描述 +BTree 的索引类型是基于 B+Tree 树型数据结构的,B+Tree 又是 BTree 数据结构的变种,用在数据库和操作系统中的文件系统,特点是能够保持数据稳定有序 - ```mysql - DELIMITER $ - - CREATE PROCEDURE pro_test6(IN total INT, OUT description VARCHAR(10)) - BEGIN - -- 判断总分数 - IF total >= 380 THEN - SET description = '学习优秀'; - ELSEIF total >= 320 AND total < 380 THEN - SET description = '学习不错'; - ELSE - SET description = '学习一般'; - END IF; - END$ - - DELIMITER ; - -- 调用pro_test6存储过程 - CALL pro_test6(310,@description); - CALL pro_test6((SELECT SUM(score) FROM student), @description); - -- 查询总成绩描述 - SELECT @description; - ``` +BTree 又叫多路平衡搜索树,一颗 m 叉的 BTree 特性如下: -* 查看参数方法 +- 树中每个节点最多包含 m 个孩子 +- 除根节点与叶子节点外,每个节点至少有 [ceil(m/2)] 个孩子 +- 若根节点不是叶子节点,则至少有两个孩子 +- 所有的叶子节点都在同一层 +- 每个非叶子节点由 n 个key与 n+1 个指针组成,其中 [ceil(m/2)-1] <= n <= m-1 - * @变量名 : **用户会话变量**,代表整个会话过程他都是有作用的,类似于全局变量 - * @@变量名 : **系统变量** +5 叉,key 的数量 [ceil(m/2)-1] <= n <= m-1 为 2 <= n <=4 ,当 n>4 时中间节点分裂到父节点,两边节点分裂 - +插入 C N G A H E K Q M F W L T Z D P R X Y S 数据的工作流程: -*** +* 插入前4个字母 C N G A + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程1.png) +* 插入 H,n>4,中间元素 G 字母向上分裂到新的节点 -##### CASE + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程2.png) -* 标准语法 1 +* 插入 E、K、Q 不需要分裂 - ```mysql - CASE 表达式 - WHEN 值1 THEN 执行sql语句1; - [WHEN 值2 THEN 执行sql语句2;] - ... - [ELSE 执行sql语句n;] - END CASE; - ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程3.png) -* 标准语法 2 +* 插入 M,中间元素 M 字母向上分裂到父节点 G - ```mysql - sCASE - WHEN 判断条件1 THEN 执行sql语句1; - [WHEN 判断条件2 THEN 执行sql语句2;] - ... - [ELSE 执行sql语句n;] - END CASE; - ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程4.png) -* 演示 +* 插入 F,W,L,T 不需要分裂 - ```mysql - DELIMITER $ - CREATE PROCEDURE pro_test7(IN total INT) - BEGIN - -- 定义变量 - DECLARE description VARCHAR(10); - -- 使用case判断 - CASE - WHEN total >= 380 THEN - SET description = '学习优秀'; - WHEN total >= 320 AND total < 380 THEN - SET description = '学习不错'; - ELSE - SET description = '学习一般'; - END CASE; - - -- 查询分数描述信息 - SELECT description; - END$ - DELIMITER ; - -- 调用pro_test7存储过程 - CALL pro_test7(390); - CALL pro_test7((SELECT SUM(score) FROM student)); - ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程5.png) +* 插入 Z,中间元素 T 向上分裂到父节点中 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程6.png) -*** +* 插入 D,中间元素 D 向上分裂到父节点中,然后插入 P,R,X,Y 不需要分裂 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程7.png) +* 最后插入 S,NPQR 节点 n>5,中间节点 Q 向上分裂,但分裂后父节点 DGMT 的 n>5,中间节点 M 向上分裂 -##### WHILE + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程8.png) -* while 循环语法 +BTree 树就已经构建完成了,BTree 树和二叉树相比, 查询数据的效率更高, 因为对于相同的数据量来说,**BTree 的层级结构比二叉树小**,所以搜索速度快 - ```mysql - WHILE 条件判断语句 DO - 循环体语句; - 条件控制语句; - END WHILE; - ``` - -* 计算 1~100 之间的偶数和 +BTree 结构的数据可以让系统高效的找到数据所在的磁盘块,定义一条记录为一个二元组 [key, data] ,key 为记录的键值,对应表中的主键值,data 为一行记录中除主键外的数据。对于不同的记录,key 值互不相同,BTree 中的每个节点根据实际情况可以包含大量的关键字信息和分支 +![](https://gitee.com/seazean/images/raw/master/DB/索引的原理-BTree.png) - ```mysql - DELIMITER $ - CREATE PROCEDURE pro_test6() - BEGIN - -- 定义求和变量 - DECLARE result INT DEFAULT 0; - -- 定义初始化变量 - DECLARE num INT DEFAULT 1; - -- while循环 - WHILE num <= 100 DO - IF num % 2 = 0 THEN - SET result = result + num; - END IF; - SET num = num + 1; - END WHILE; - -- 查询求和结果 - SELECT result; - END$ - DELIMITER ; - - -- 调用pro_test6存储过程 - CALL pro_test6(); - ``` +缺点:当进行范围查找时会出现回旋查找 @@ -3546,206 +3933,118 @@ DELIMITER: -##### REPEAT +#### B+Tree -* repeat 循环标准语法 +##### 数据结构 - ```mysql - 初始化语句; - REPEAT - 循环体语句; - 条件控制语句; - UNTIL 条件判断语句 - END REPEAT; - ``` +BTree 数据结构中每个节点中不仅包含数据的 key 值,还有 data 值。磁盘中每一页的存储空间是有限的,如果 data 数据较大时将会导致每个节点(即一个页)能存储的 key 的数量很小,当存储的数据量很大时同样会导致 B-Tree 的深度较大,增大查询时的磁盘 I/O 次数,进而影响查询效率,所以引入 B+Tree -* 计算 1~10 之间的和 +B+Tree 为 BTree 的变种,B+Tree 与 BTree 的区别为: - ```mysql - DELIMITER $ - CREATE PROCEDURE pro_test9() - BEGIN - -- 定义求和变量 - DECLARE result INT DEFAULT 0; - -- 定义初始化变量 - DECLARE num INT DEFAULT 1; - -- repeat循环 - REPEAT - -- 累加 - SET result = result + num; - -- 让num+1 - SET num = num + 1; - -- 停止循环 - UNTIL num > 10 - END REPEAT; - -- 查询求和结果 - SELECT result; - END$ - - DELIMITER ; - -- 调用pro_test9存储过程 - CALL pro_test9(); - ``` +* n 叉 B+Tree 最多含有 n 个 key(哈希值),而 BTree 最多含有 n-1 个 key +- 所有**非叶子节点只存储键值 key** 信息,只进行数据索引,使每个非叶子节点所能保存的关键字大大增加 +- 所有**数据都存储在叶子节点**,所以每次数据查询的次数都一样 +- **叶子节点按照 key 大小顺序排列,左边结尾数据都会保存右边节点开始数据的指针,形成一个链表** +- 所有节点中的 key 在叶子节点中也存在(比如 5),key 允许重复,B 树不同节点不存在重复的 key + +B* 树:是 B+ 树的变体,在 B+ 树的非根和非叶子结点再增加指向兄弟的指针 -*** +*** -##### LOOP -LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定义,通常可以使用 LEAVE 语句实现,如果不加退出循环的语句,那么就变成了死循环 -* loop 循环标准语法 +##### 优化结构 - ```mysql - [循环名称:] LOOP - 条件判断语句 - [LEAVE 循环名称;] - 循环体语句; - 条件控制语句; - END LOOP 循环名称; - ``` - -* 计算 1~10 之间的和 +MySQL 索引数据结构对经典的 B+Tree 进行了优化,在原 B+Tree 的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的 B+Tree,**提高区间访问的性能,防止回旋查找** - ```mysql - DELIMITER $ - CREATE PROCEDURE pro_test10() - BEGIN - -- 定义求和变量 - DECLARE result INT DEFAULT 0; - -- 定义初始化变量 - DECLARE num INT DEFAULT 1; - -- loop循环 - l:LOOP - -- 条件成立,停止循环 - IF num > 10 THEN - LEAVE l; - END IF; - -- 累加 - SET result = result + num; - -- 让num+1 - SET num = num + 1; - END LOOP l; - -- 查询求和结果 - SELECT result; - END$ - DELIMITER ; - -- 调用pro_test10存储过程 - CALL pro_test10(); - ``` +区间访问的意思是访问索引为 5 - 15 的数据,可以直接根据相邻节点的指针遍历 +B+ 树的**叶子节点是数据页**(page),一个页里面可以存多个数据行 +![](https://gitee.com/seazean/images/raw/master/DB/索引的原理-B+Tree.png) -*** +通常在 B+Tree 上有两个头指针,**一个指向根节点,另一个指向关键字最小的叶子节点**,而且所有叶子节点(即数据节点)之间是一种链式环结构。可以对 B+Tree 进行两种查找运算: +- 有范围:对于主键的范围查找和分页查找 +- 有顺序:从根节点开始,进行随机查找,顺序查找 +InnoDB 中每个数据页的大小默认是 16KB, -##### 游标 +* 索引行:一般表的主键类型为 INT(4 字节)或 BIGINT(8 字节),指针大小在 InnoDB 中设置为 6 字节节,也就是说一个页大概存储 16KB/(8B+6B)=1K 个键值(估值)。则一个深度为 3 的 B+Tree 索引可以维护 `10^3 * 10^3 * 10^3 = 10亿` 条记录 +* 数据行:一行数据的大小可能是 1k,一个数据页可以存储 16 行 -游标是用来存储查询结果集的数据类型,在存储过程和函数中可以使用光标对结果集进行循环的处理 -* 游标可以遍历返回的多行结果,每次拿到一整行数据 -* 简单来说游标就类似于集合的迭代器遍历 -* MySQL 中的游标只能用在存储过程和函数中 +实际情况中每个节点可能不能填充满,因此在数据库中,B+Tree 的高度一般都在 2-4 层。MySQL 的 InnoDB 存储引擎在设计时是**将根节点常驻内存的**,也就是说查找某一键值的行记录时最多只需要 1~3 次磁盘 I/O 操作 -游标的语法 +B+Tree 优点:提高查询速度,减少磁盘的 IO 次数,树形结构较小 -* 创建游标 - ```mysql - DECLARE 游标名称 CURSOR FOR 查询sql语句; - ``` -* 打开游标 +*** - ```mysql - OPEN 游标名称; - ``` -* 使用游标获取数据 - ```mysql - FETCH 游标名称 INTO 变量名1,变量名2,...; - ``` +##### 索引维护 -* 关闭游标 +B+ 树为了保持索引的有序性,在插入新值的时候需要做相应的维护 - ```mysql - CLOSE 游标名称; - ``` +每个索引中每个块存储在磁盘页中,可能会出现以下两种情况: -* Mysql 通过一个 Error handler 声明来判断指针是否到尾部,并且必须和创建游标的 SQL 语句声明在一起: +* 如果所在的数据页已经满了,这时候需要申请一个新的数据页,然后挪动部分数据过去,这个过程称为**页分裂** +* 当相邻两个页由于删除了数据,利用率很低之后,会将数据页做**页合并**,合并的过程可以认为是分裂过程的逆过程 +* 这两个情况都是由 B+ 树的结构决定的 - ```mysql - DECLARE EXIT HANDLER FOR NOT FOUND (do some action,一般是设置标志变量) - ``` +一般选用数据小的字段做索引,字段长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小 - -游标的基本使用 -* 数据准备:表 student +*** - ```mysql - id NAME age gender score - 1 张三 23 男 95 - 2 李四 24 男 98 - 3 王五 25 女 100 - 4 赵六 26 女 90 - ``` -* 创建 stu_score 表 - ```mysql - CREATE TABLE stu_score( - id INT PRIMARY KEY AUTO_INCREMENT, - score INT - ); - ``` +### 设计原则 -* 将student表中所有的成绩保存到stu_score表中 +索引的设计可以遵循一些已有的原则,创建索引的时候请尽量考虑符合这些原则,便于提升索引的使用效率 - ```mysql - DELIMITER $ +创建索引时的原则: +- 对查询频次较高,且数据量比较大的表建立索引 +- 使用唯一索引,区分度越高,使用索引的效率越高 +- 索引字段的选择,最佳候选列应当从 where 子句的条件中提取,使用覆盖索引 +- 使用短索引,索引创建之后也是使用硬盘来存储的,因此提升索引访问的 I/O 效率,也可以提升总体的访问效率。假如构成索引的字段总长度比较短,那么在给定大小的存储块内可以存储更多的索引值,相应的可以有效的提升 MySQL 访问索引的 I/O 效率 +- 索引可以有效的提升查询数据的效率,但索引数量不是多多益善,索引越多,维护索引的代价越高。对于插入、更新、删除等 DML 操作比较频繁的表来说,索引过多,会引入相当高的维护代价,降低 DML 操作的效率,增加相应操作的时间消耗;另外索引过多的话,MySQL 也会犯选择困难病,虽然最终仍然会找到一个可用的索引,但提高了选择的代价 + +* MySQL 建立联合索引时会遵守**最左前缀匹配原则**,即最左优先,在检索数据时从联合索引的最左边开始匹配 - CREATE PROCEDURE pro_test12() - BEGIN - -- 定义成绩变量 - DECLARE s_score INT; - -- 定义标记变量 - DECLARE flag INT DEFAULT 0; - - -- 创建游标,查询所有学生成绩数据 - DECLARE stu_result CURSOR FOR SELECT score FROM student; - -- 游标结束后,将标记变量改为1 这两个必须声明在一起 - DECLARE EXIT HANDLER FOR NOT FOUND SET flag = 1; - - -- 开启游标 - OPEN stu_result; - -- 循环使用游标 - REPEAT - -- 使用游标,遍历结果,拿到数据 - FETCH stu_result INTO s_score; - -- 将数据保存到stu_score表中 - INSERT INTO stu_score VALUES (NULL,s_score); - UNTIL flag=1 - END REPEAT; - -- 关闭游标 - CLOSE stu_result; - END$ + N 个列组合而成的组合索引,相当于创建了 N 个索引,如果查询时 where 句中使用了组成该索引的**前**几个字段,那么这条查询 SQL 可以利用组合索引来提升查询效率 - DELIMITER ; + ```mysql + -- 对name、address、phone列建一个联合索引 + ALTER TABLE user ADD INDEX index_three(name,address,phone); + -- 查询语句执行时会依照最左前缀匹配原则,检索时分别会使用索引进行数据匹配。 + (name,address,phone) + (name,address) + (name,phone) -- 只有name字段走了索引 + (name) - -- 调用pro_test12存储过程 - CALL pro_test12(); - -- 查询stu_score表 - SELECT * FROM stu_score; + -- 索引的字段可以是任意顺序的,优化器会帮助我们调整顺序,下面的SQL语句可以命中索引 + SELECT * FROM user WHERE address = '北京' AND phone = '12345' AND name = '张三'; ``` - + ```mysql + -- 如果联合索引中最左边的列不包含在条件查询中,SQL语句就不会命中索引,比如: + SELECT * FROM user WHERE address = '北京' AND phone = '12345'; + ``` + +哪些情况不要建立索引: + +* 记录太少的表 +* 经常增删改的表 +* 频繁更新的字段不适合创建索引 +* where 条件里用不到的字段不创建索引 @@ -3753,59 +4052,35 @@ LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定 -#### 存储函数 +### 索引优化 -存储函数和存储过程是非常相似的,存储函数可以做的事情,存储过程也可以做到 +#### 覆盖索引 -存储函数有返回值,存储过程没有返回值(参数的 out 其实也相当于是返回数据了) +覆盖索引:包含所有满足查询需要的数据的索引(SELECT 后面的字段刚好是索引字段),可以利用该索引返回 SELECT 列表的字段,而不必根据索引去聚簇索引上读取数据文件 -* 创建存储函数 +回表查询:要查找的字段不在非主键索引树上时,需要通过叶子节点的主键值去主键索引上获取对应的行数据 - ```mysql - DELIMITER $ - -- 标准语法 - CREATE FUNCTION 函数名称(参数 数据类型) - RETURNS 返回值类型 - BEGIN - 执行的sql语句; - RETURN 结果; - END$ - - DELIMITER ; - ``` +使用覆盖索引,防止回表查询: -* 调用存储函数,因为有返回值,所以使用 SELECT 调用 +* 表 user 主键为 id,普通索引为 age,查询语句: ```mysql - SELECT 函数名称(实际参数); + SELECT * FROM user WHERE age = 30; ``` -* 删除存储函数 - - ```mysql - DROP FUNCTION 函数名称; - ``` + 查询过程:先通过普通索引 age=30 定位到主键值 id=1,再通过聚集索引 id=1 定位到行记录数据,需要两次扫描 B+ 树 -* 定义存储函数,获取学生表中成绩大于95分的学生数量 +* 使用覆盖索引: ```mysql - DELIMITER $ - CREATE FUNCTION fun_test() - RETURN INT - BEGIN - -- 定义统计变量 - DECLARE result INT; - -- 查询成绩大于95分的学生数量,给统计变量赋值 - SELECT COUNT(score) INTO result FROM student WHERE score > 95; - -- 返回统计结果 - SELECT result; - END - DELIMITER ; - -- 调用fun_test存储函数 - SELECT fun_test(); + DROP INDEX idx_age ON user; + CREATE INDEX idx_age_name ON user(age,name); + SELECT id,age FROM user WHERE age = 30; ``` + 在一棵索引树上就能获取查询所需的数据,无需回表速度更快 +使用覆盖索引,要注意 SELECT 列表中只取出需要的列,不可用 SELECT *,所有字段一起做索引会导致索引文件过大,查询性能下降 @@ -3813,346 +4088,319 @@ LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定 -### 触发器 +#### 索引下推 -#### 基本介绍 +索引条件下推优化(Index Condition Pushdown)是 MySQL5.6 添加,可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数 -触发器是与表有关的数据库对象,在 insert/update/delete 之前或之后触发并执行触发器中定义的 SQL 语句 +索引下推充分利用了索引中的数据,在查询出整行数据之前过滤掉无效的数据,再去主键索引树上查找 -* 触发器的这种特性可以协助应用在数据库端确保数据的完整性 、日志记录 、数据校验等操作 +* 不使用索引下推优化时存储引擎通过索引检索到数据返回给 MySQL 服务器,服务器判断数据是否符合条件,符合条件的数据去聚簇索引回表查询,获取完整的数据 -- 使用别名 NEW 和 OLD 来引用触发器中发生变化的记录内容,这与其他的数据库是相似的 -- 现在触发器还只支持行级触发,不支持语句级触发 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-不使用索引下推.png) +* 使用索引下推优化时,如果**存在某些被索引的列的判断条件**时,MySQL 服务器将这一部分判断条件传递给存储引擎,由存储引擎在索引遍历的过程中判断数据是否符合传递的条件,将符合条件的数据进行回表,检索出来返回给服务器,由此减少 IO 次数 -| 触发器类型 | OLD的含义 | NEW的含义 | -| --------------- | ------------------------------ | ------------------------------ | -| INSERT 型触发器 | 无 (因为插入前状态无数据) | NEW 表示将要或者已经新增的数据 | -| UPDATE 型触发器 | OLD 表示修改之前的数据 | NEW 表示将要或已经修改后的数据 | -| DELETE 型触发器 | OLD 表示将要或者已经删除的数据 | 无 (因为删除后状态无数据) | + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-使用索引下推.png) +**适用条件**: +* 需要存储引擎将索引中的数据与条件进行判断(所以**条件列必须都在同一个索引中**),所以优化是基于存储引擎的,只有特定引擎可以使用,适用于 InnoDB 和 MyISAM +* 存储引擎没有调用跨存储引擎的能力,跨存储引擎的功能有存储过程、触发器、视图,所以调用这些功能的不可以进行索引下推优化 +* 对于 InnoDB 引擎只适用于二级索引,InnoDB 的聚簇索引会将整行数据读到缓冲区,不再需要去回表查询了,索引下推的目的减少 IO 次数也就失去了意义 -*** +工作过程:用户表 user,(name, age) 是联合索引 +```mysql +SELECT * FROM user WHERE name LIKE '张%' AND age = 10; -- 头部模糊匹配会造成索引失效 +``` +* 优化前:在非主键索引树上找到满足第一个条件的行,然后通过叶子节点记录的主键值再回到主键索引树上查找到对应的行数据,再对比 AND 后的条件是否符合,符合返回数据,需要 4 次回表 -#### 基本操作 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-索引下推优化1.png) -* 创建触发器 +* 优化后:检查索引中存储的列信息是否符合索引条件,然后交由存储引擎用剩余的判断条件判断此行数据是否符合要求,**不满足条件的不去读取表中的数据**,满足下推条件的就根据主键值进行回表查询,2 次回表 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-索引下推优化2.png) - ```mysql - DELIMITER $ - - CREATE TRIGGER 触发器名称 - BEFORE|AFTER INSERT|UPDATE|DELETE - ON 表名 - [FOR EACH ROW] -- 行级触发器 - BEGIN - 触发器要执行的功能; - END$ - - DELIMITER ; - ``` +当使用 EXPLAIN 进行分析时,如果使用了索引条件下推,Extra 会显示 Using index condition -* 查看触发器的状态、语法等信息 - ```mysql - SHOW TRIGGERS; - ``` -* 删除触发器,如果没有指定 schema_name,默认为当前数据库 +参考文章:https://blog.csdn.net/sinat_29774479/article/details/103470244 + +参考文章:https://time.geekbang.org/column/article/69636 - ```mysql - DROP TRIGGER [schema_name.]trigger_name; - ``` - *** -#### 触发演示 +#### 前缀索引 -通过触发器记录账户表的数据变更日志。包含:增加、修改、删除 +当要索引的列字符很多时,索引会变大变慢,可以只索引列开始的部分字符串,节约索引空间,提高索引效率 -* 数据准备 +注意:使用前缀索引就系统就忽略覆盖索引对查询性能的优化了 - ```mysql - -- 创建db9数据库 - CREATE DATABASE db9; - -- 使用db9数据库 - USE db9; - ``` +优化原则:**降低重复的索引值** - ```mysql - -- 创建账户表account - CREATE TABLE account( - id INT PRIMARY KEY AUTO_INCREMENT, -- 账户id - NAME VARCHAR(20), -- 姓名 - money DOUBLE -- 余额 - ); - -- 添加数据 - INSERT INTO account VALUES (NULL,'张三',1000),(NULL,'李四',2000); - ``` +比如地区表: - ```mysql - -- 创建日志表account_log - CREATE TABLE account_log( - id INT PRIMARY KEY AUTO_INCREMENT, -- 日志id - operation VARCHAR(20), -- 操作类型 (insert update delete) - operation_time DATETIME, -- 操作时间 - operation_id INT, -- 操作表的id - operation_params VARCHAR(200) -- 操作参数 - ); - ``` +```mysql +area gdp code +chinaShanghai 100 aaa +chinaDalian 200 bbb +usaNewYork 300 ccc +chinaFuxin 400 ddd +chinaBeijing 500 eee +``` -* 创建 INSERT 型触发器 +发现 area 字段很多都是以 china 开头的,那么如果以前 1-5 位字符做前缀索引就会出现大量索引值重复的情况,索引值重复性越低,查询效率也就越高,所以需要建立前 6 位字符的索引: - ```mysql - DELIMITER $ - - CREATE TRIGGER account_insert - AFTER INSERT - ON account - FOR EACH ROW - BEGIN - INSERT INTO account_log VALUES (NULL,'INSERT',NOW(),new.id,CONCAT('插入后{id=',new.id,',name=',new.name,',money=',new.money,'}')); - END$ - - DELIMITER ; +```mysql +CREATE INDEX idx_area ON table_name(area(7)); +``` + +场景:存储身份证 + +* 直接创建完整索引,这样可能比较占用空间 +* 创建前缀索引,节省空间,但会增加查询扫描次数,并且不能使用覆盖索引 +* 倒序存储,再创建前缀索引,用于绕过字符串本身前缀的区分度不够的问题(前 6 位相同的很多) +* 创建 hash 字段索引,查询性能稳定,有额外的存储和计算消耗,跟第三种方式一样,都不支持范围扫描 + + + +**** + + + +#### 索引合并 + +使用多个索引来完成一次查询的执行方法叫做索引合并 index merge + +* Intersection 索引合并: + + ```sql + SELECT * FROM table_test WHERE key1 = 'a' AND key3 = 'b'; # key1 和 key3 列都是单列索引、二级索引 ``` - ```mysql - -- 向account表添加记录 - INSERT INTO account VALUES (NULL,'王五',3000); - - -- 查询日志表 - SELECT * FROM account_log; - /* - id operation operation_time operation_id operation_params - 1 INSERT 2021-01-26 19:51:11 3 插入后{id=3,name=王五money=2000} - */ + 从不同索引中扫描到的记录的 id 值取**交集**(相同 id),然后执行回表操作,要求从每个二级索引获取到的记录都是按照主键值排序 + +* Union 索引合并: + + ```sql + SELECT * FROM table_test WHERE key1 = 'a' OR key3 = 'b'; ``` - + 从不同索引中扫描到的记录的 id 值取**并集**,然后执行回表操作,要求从每个二级索引获取到的记录都是按照主键值排序 -* 创建 UPDATE 型触发器 +* Sort-Union 索引合并 - ```mysql - DELIMITER $ - - CREATE TRIGGER account_update - AFTER UPDATE - ON account - FOR EACH ROW - BEGIN - INSERT INTO account_log VALUES (NULL,'UPDATE',NOW(),new.id,CONCAT('修改前{id=',old.id,',name=',old.name,',money=',old.money,'}','修改后{id=',new.id,',name=',new.name,',money=',new.money,'}')); - END$ - - DELIMITER ; + ```sql + SELECT * FROM table_test WHERE key1 < 'a' OR key3 > 'b'; ``` + 先将从不同索引中扫描到的记录的主键值进行排序,再按照 Union 索引合并的方式进行查询 + + + + + + + +*** + + + + + +## 系统优化 + +### 优化步骤 + +#### 执行频率 + +随着生产数据量的急剧增长,很多 SQL 语句逐渐显露出性能问题,对生产的影响也越来越大,此时有问题的 SQL 语句就成为整个系统性能的瓶颈,因此必须要进行优化 + +MySQL 客户端连接成功后,查询服务器状态信息: + +```mysql +SHOW [SESSION|GLOBAL] STATUS LIKE ''; +-- SESSION: 显示当前会话连接的统计结果,默认参数 +-- GLOBAL: 显示自数据库上次启动至今的统计结果 +``` + +* 查看SQL执行频率: + ```mysql - -- 修改account表 - UPDATE account SET money=3500 WHERE id=3; - - -- 查询日志表 - SELECT * FROM account_log; - /* - id operation operation_time operation_id operation_params - 2 UPDATE 2021-01-26 19:58:54 2 更新前{id=2,name=李四money=1000} - 更新后{id=2,name=李四money=200} - */ + SHOW STATUS LIKE 'Com_____'; ``` - + Com_xxx 表示每种语句执行的次数 -* 创建 DELETE 型触发器 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SQL语句执行频率.png) - ```mysql - DELIMITER $ - - CREATE TRIGGER account_delete - AFTER DELETE - ON account - FOR EACH ROW - BEGIN - INSERT INTO account_log VALUES (NULL,'DELETE',NOW(),old.id,CONCAT('删除前{id=',old.id,',name=',old.name,',money=',old.money,'}')); - END$ - - DELIMITER ; - ``` +* 查询 SQL 语句影响的行数: ```mysql - -- 删除account表数据 - DELETE FROM account WHERE id=3; - - -- 查询日志表 - SELECT * FROM account_log; - /* - id operation operation_time operation_id operation_params - 3 DELETE 2021-01-26 20:02:48 3 删除前{id=3,name=王五money=2000} - */ + SHOW STATUS LIKE 'Innodb_rows_%'; ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SQL语句影响的行数.png) +Com_xxxx:这些参数对于所有存储引擎的表操作都会进行累计 +Innodb_xxxx:这几个参数只是针对InnoDB 存储引擎的,累加的算法也略有不同 +| 参数 | 含义 | +| :------------------- | ------------------------------------------------------------ | +| Com_select | 执行 SELECT 操作的次数,一次查询只累加 1 | +| Com_insert | 执行 INSERT 操作的次数,对于批量插入的 INSERT 操作,只累加一次 | +| Com_update | 执行 UPDATE 操作的次数 | +| Com_delete | 执行 DELETE 操作的次数 | +| Innodb_rows_read | 执行 SELECT 查询返回的行数 | +| Innodb_rows_inserted | 执行 INSERT 操作插入的行数 | +| Innodb_rows_updated | 执行 UPDATE 操作更新的行数 | +| Innodb_rows_deleted | 执行 DELETE 操作删除的行数 | +| Connections | 试图连接 MySQL 服务器的次数 | +| Uptime | 服务器工作时间 | +| Slow_queries | 慢查询的次数 | -*** +**** -## 存储引擎 +#### 定位低效 -### 基本介绍 +SQL 执行慢有两种情况: -对比其他数据库,MySQL 的架构可以在不同场景应用并发挥良好作用,主要体现在存储引擎,插件式的存储引擎架构将查询处理和其他的系统任务以及数据的存储提取分离,可以针对不同的存储需求可以选择最优的存储引擎 +* 偶尔慢:DB 在刷新脏页 + * redo log 写满了 + * 内存不够用,要从 LRU 链表中淘汰 + * MySQL 认为系统空闲的时候 + * MySQL 关闭时 +* 一直慢的原因:索引没有设计好、SQL 语句没写好、MySQL 选错了索引 -存储引擎的介绍: +通过以下两种方式定位执行效率较低的 SQL 语句 -- MySQL 数据库使用不同的机制存取表文件 , 机制的差别在于不同的存储方式、索引技巧、锁定水平等不同的功能和能力,在 MySQL 中,将这些不同的技术及配套的功能称为存储引擎 -- Oracle、SqlServer 等数据库只有一种存储引擎,MySQL **提供了插件式的存储引擎架构**,所以 MySQL 存在多种存储引擎 , 就会让数据库采取了不同的处理数据的方式和扩展功能 -- 在关系型数据库中数据的存储是以表的形式存进行,所以存储引擎也称为表类型(存储和操作此表的类型) -- 通过选择不同的引擎,能够获取最佳的方案, 也能够获得额外的速度或者功能,提高程序的整体效果。 +* 慢日志查询: 慢查询日志在查询结束以后才记录,执行效率出现问题时查询日志并不能定位问题 -MySQL 支持的存储引擎: + 配置文件修改:修改 .cnf 文件 `vim /etc/mysql/my.cnf`,重启 MySQL 服务器 -- MySQL 支持的引擎包括:InnoDB、MyISAM、MEMORY、Archive、Federate、CSV、BLACKHOLE 等 -- MySQL5.5 之前的默认存储引擎是 MyISAM,5.5 之后就改为了 InnoDB + ```sh + slow_query_log=ON + slow_query_log_file=/usr/local/mysql/var/localhost-slow.log + long_query_time=1 #记录超过long_query_time秒的SQL语句的日志 + log-queries-not-using-indexes = 1 + ``` + 使用命令配置: + ```mysql + mysql> SET slow_query_log=ON; + mysql> SET GLOBAL slow_query_log=ON; + ``` -**** + 查看是否配置成功: + ```mysql + SHOW VARIABLES LIKE '%query%' + ``` +* SHOW PROCESSLIST:**实时查看**当前 MySQL 在进行的连接线程,包括线程的状态、是否锁表、SQL 的执行情况,同时对一些锁表操作进行优化 -### 引擎对比 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SHOW PROCESSLIST命令.png) -MyISAM 存储引擎: -* 特点:不支持事务和外键,读取速度快,节约资源 -* 应用场景:查询和插入操作为主,只有很少更新和删除操作,并对事务的完整性、并发性要求不高 -* 存储方式: - * 每个 MyISAM 在磁盘上存储成 3 个文件,其文件名都和表名相同,拓展名不同 - * 表的定义保存在 .frm 文件,表数据保存在 .MYD (MYData) 文件中,索引保存在 .MYI (MYIndex) 文件中 -InnoDB 存储引擎:(MySQL5.5 版本后默认的存储引擎) -- 特点:**支持事务**和外键操作,支持并发控制。对比 MyISAM 的存储引擎,InnoDB 写的处理效率差一些,并且会占用更多的磁盘空间以保留数据和索引 -- 应用场景:对事务的完整性有比较高的要求,在并发条件下要求数据的一致性,读写频繁的操作 -- 存储方式: - - 使用共享表空间存储, 这种方式创建的表的表结构保存在 .frm 文件中, 数据和索引保存在 innodb_data_home_dir 和 innodb_data_file_path 定义的表空间中,可以是多个文件 - - 使用多表空间存储,创建的表的表结构存在 .frm 文件中,每个表的数据和索引单独保存在 .ibd 中 -MEMORY 存储引擎: -- 特点:每个 MEMORY 表实际对应一个磁盘文件 ,该文件中只存储表的结构,表数据保存在内存中,且默认使用 HASH 索引,这样有利于数据的快速处理,在需要快速定位记录可以提供更快的访问,但是服务一旦关闭,表中的数据就会丢失,数据存储不安全 -- 应用场景:通常用于更新不太频繁的小表,用以快速得到访问结果,类似缓存 -- 存储方式:表结构保存在 .frm 中 +*** -MERGE存储引擎: -* 特点: - * 是一组 MyISAM 表的组合,这些 MyISAM 表必须结构完全相同,通过将不同的表分布在多个磁盘上 - * MERGE 表本身并没有存储数据,对 MERGE 类型的表可以进行查询、更新、删除操作,这些操作实际上是对内部的 MyISAM 表进行的 +#### EXPLAIN -* 应用场景:将一系列等同的 MyISAM 表以逻辑方式组合在一起,并作为一个对象引用他们,适合做数据仓库 +##### 执行计划 -* 操作方式: +通过 EXPLAIN 命令获取执行 SQL 语句的信息,包括在 SELECT 语句执行过程中如何连接和连接的顺序,执行计划在优化器优化完成后、执行器之前生成,然后执行器会调用存储引擎检索数据 - * 插入操作是通过 INSERT_METHOD 子句定义插入的表,使用 FIRST 或 LAST 值使得插入操作被相应地作用在第一或者最后一个表上;不定义这个子句或者定义为 NO,表示不能对 MERGE 表执行插入操作 - * 对 MERGE 表进行 DROP 操作,但是这个操作只是删除 MERGE 表的定义,对内部的表是没有任何影响的 +查询 SQL 语句的执行计划: - ```mysql - CREATE TABLE order_1( - )ENGINE = MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE order_2( - )ENGINE = MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE order_all( - -- 结构与MyISAM表相同 - )ENGINE = MERGE UNION = (order_1,order_2) INSERT_METHOD=LAST DEFAULT CHARSET=utf8; - ``` +```mysql +EXPLAIN SELECT * FROM table_1 WHERE id = 1; +``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MERGE.png) +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-explain查询SQL语句的执行计划.png) -| 特性 | MyISAM | InnoDB | MEMORY | -| ------------ | ---------------------------- | ------------- | ------------------ | -| 存储限制 | 有(平台对文件系统大小的限制) | 64TB | 有(平台的内存限制) | -| **事务安全** | **不支持** | **支持** | **不支持** | -| **锁机制** | **表锁** | **表锁/行锁** | **表锁** | -| B+Tree索引 | 支持 | 支持 | 支持 | -| 哈希索引 | 不支持 | 不支持 | 支持 | -| 全文索引 | 支持 | 支持 | 不支持 | -| 集群索引 | 不支持 | 支持 | 不支持 | -| 数据索引 | 不支持 | 支持 | 支持 | -| 数据缓存 | 不支持 | 支持 | N/A | -| 索引缓存 | 支持 | 支持 | N/A | -| 数据可压缩 | 支持 | 不支持 | 不支持 | -| 空间使用 | 低 | 高 | N/A | -| 内存使用 | 低 | 高 | 中等 | -| 批量插入速度 | 高 | 低 | 高 | -| **外键** | **不支持** | **支持** | **不支持** | +| 字段 | 含义 | +| ------------- | ------------------------------------------------------------ | +| id | SELECT 的序列号 | +| select_type | 表示 SELECT 的类型 | +| table | 访问数据库中表名称,有时可能是简称或者临时表名称() | +| type | 表示表的连接类型 | +| possible_keys | 表示查询时,可能使用的索引 | +| key | 表示实际使用的索引 | +| key_len | 索引字段的长度 | +| ref | 表示与索引列进行等值匹配的对象,常数、某个列、函数等,type 必须在(range, const] 之间,左闭右开 | +| rows | 扫描出的行数,表示 MySQL 根据表统计信息及索引选用情况,**估算**的找到所需的记录扫描的行数 | +| filtered | 条件过滤的行百分比,单表查询没意义,用于连接查询中对驱动表的扇出进行过滤,查询优化器预测所有扇出值满足剩余查询条件的百分比,相乘以后表示多表查询中还要对被驱动执行查询的次数 | +| extra | 执行情况的说明和描述 | -面试问题:MyIsam 和 InnoDB 的区别? +MySQL **执行计划的局限**: -* 事务:InnoDB 支持事务,MyISAM 不支持事务 -* 外键:InnoDB 支持外键,MyISAM 不支持外键 -* 索引:InnoDB 是聚集(聚簇)索引,MyISAM 是非聚集(非聚簇)索引 +* 只是计划,不是执行 SQL 语句,可以随着底层优化器输入的更改而更改 +* EXPLAIN 不会告诉显示关于触发器、存储过程的信息对查询的影响情况 +* EXPLAIN 不考虑各种 Cache +* EXPLAIN 不能显示 MySQL 在执行查询时的动态,因为执行计划在执行查询之前生成 +* EXPALIN 部分统计信息是估算的,并非精确值 +* EXPALIN 只能解释 SELECT 操作,其他操作要重写为 SELECT 后查看执行计划 +* EXPLAIN PLAN 显示的是在解释语句时数据库将如何运行 SQL 语句,由于执行环境和 EXPLAIN PLAN 环境的不同,此计划可能与 SQL 语句实际的执行计划不同 -* 锁粒度:InnoDB 最小的锁粒度是行锁,MyISAM 最小的锁粒度是表锁 -* 存储结构:参考本节上半部分 +SHOW WARINGS:在使用 EXPALIN 命令后执行该语句,可以查询与执行计划相关的拓展信息,展示出 Level、Code、Message 三个字段,当 Code 为 1003 时,Message 字段展示的信息类似于将查询语句重写后的信息,但是不是等价,不能执行复制过来运行 +环境准备: +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-执行计划环境准备.png) -*** -### 引擎操作 -* 查询数据库支持的存储引擎 +*** - ```mysql - SHOW ENGINES; - SHOW VARIABLES LIKE '%storage_engine%'; -- 查看Mysql数据库默认的存储引擎 - ``` -* 查询某个数据库中所有数据表的存储引擎 - ```mysql - SHOW TABLE STATUS FROM 数据库名称; - ``` +##### id -* 查询某个数据库中某个数据表的存储引擎 +id 代表 SQL 执行的顺序的标识,每个 SELECT 关键字对应一个唯一 id,所以在同一个 SELECT 关键字中的表的 id 都是相同的。SELECT 后的 FROM 可以跟随多个表,每个表都会对应一条记录,这些记录的 id 都是相同的, + +* id 相同时,执行顺序由上至下。连接查询的执行计划,记录的 id 值都是相同的,出现在前面的表为驱动表,后面为被驱动表 ```mysql - SHOW TABLE STATUS FROM 数据库名称 WHERE NAME = '数据表名称'; + EXPLAIN SELECT * FROM t_role r, t_user u, user_role ur WHERE r.id = ur.role_id AND u.id = ur.user_id ; ``` -* 创建数据表,指定存储引擎 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-explain之id相同.png) + +* id 不同时,id 值越大优先级越高,越先被执行 ```mysql - CREATE TABLE 表名( - 列名,数据类型, - ... - )ENGINE = 引擎名称; + EXPLAIN SELECT * FROM t_role WHERE id = (SELECT role_id FROM user_role WHERE user_id = (SELECT id FROM t_user WHERE username = 'stu1')) ``` -* 修改数据表的存储引擎 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-explain之id不同.png) + +* id 有相同也有不同时,id 相同的可以认为是一组,从上往下顺序执行;在所有的组中,id 的值越大的组,优先级越高,越先执行 ```mysql - ALTER TABLE 表名 ENGINE = 引擎名称; + EXPLAIN SELECT * FROM t_role r , (SELECT * FROM user_role ur WHERE ur.`user_id` = '2') a WHERE r.id = a.role_id ; ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-explain之id相同和不同.png) - +* id 为 NULL 时代表的是临时表 @@ -4160,69 +4408,76 @@ MERGE存储引擎: +##### select +表示查询中每个 select 子句的类型(简单 OR 复杂) -## 索引机制 +| select_type | 含义 | +| ------------------ | ------------------------------------------------------------ | +| SIMPLE | 简单的 SELECT 查询,查询中不包含子查询或者 UNION | +| PRIMARY | 查询中若包含任何复杂的子查询,最外层(也就是最左侧)查询标记为该标识 | +| UNION | 对于 UNION 或者 UNION ALL 的复杂查询,除了最左侧的查询,其余的小查询都是 UNION | +| UNION RESULT | UNION 需要使用临时表进行去重,临时表的是 UNION RESULT | +| DEPENDENT UNION | 对于 UNION 或者 UNION ALL 的复杂查询,如果各个小查询都依赖外层查询,是相关子查询,除了最左侧的小查询为 DEPENDENT SUBQUERY,其余都是 DEPENDENT UNION | +| SUBQUERY | 子查询不是相关子查询,该子查询第一个 SELECT 代表的查询就是这种类型,会进行物化(该子查询只需要执行一次) | +| DEPENDENT SUBQUERY | 子查询是相关子查询,该子查询第一个 SELECT 代表的查询就是这种类型,不会物化(该子查询需要执行多次) | +| DERIVED | 在 FROM 列表中包含的子查询,被标记为 DERIVED(衍生),也就是生成物化派生表的这个子查询 | +| MATERIALIZED | 将子查询物化后与与外层进行连接查询,生成物化表的子查询 | -### 索引介绍 +子查询为 DERIVED:`SELECT * FROM (SELECT key1 FROM t1) AS derived_1 WHERE key1 > 10` -#### 基本介绍 +子查询为 MATERIALIZED:`SELECT * FROM t1 WHERE key1 IN (SELECT key1 FROM t2)` -MySQL 官方对索引的定义为:索引(index)是帮助 MySQL 高效获取数据的一种数据结构,**本质是排好序的快速查找数据结构。**在表数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式指向数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。 -**索引是在存储引擎层实现的**,所以并没有统一的索引标准,即不同存储引擎的索引的工作方式并不一样 -索引使用:一张数据表,用于保存数据;一个索引配置文件,用于保存索引;每个索引都指向了某一个数据 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-索引的介绍.png) +**** -左边是数据表,一共有两列七条记录,最左边的是数据记录的物理地址(注意逻辑上相邻的记录在磁盘上也并不是一定物理相邻的)。为了加快 Col2 的查找,可以维护一个右边所示的二叉查找树,每个节点分别包含索引键值和一个指向对应数据的物理地址的指针,这样就可以运用二叉查找快速获取到相应数据 -索引的优点: -* 类似于书籍的目录索引,提高数据检索的效率,降低数据库的 IO 成本 -* 通过索引列对数据进行排序,降低数据排序的成本,降低 CPU 的消耗 +##### type -索引的缺点: +对表的访问方式,表示 MySQL 在表中找到所需行的方式,又称访问类型 -* 一般来说索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式**存储在磁盘**上 -* 虽然索引大大提高了查询效率,同时却也降低更新表的速度。对表进行 INSERT、UPDATE、DELETE 操作,MySQL 不仅要保存数据,还要保存一下索引文件每次更新添加了索引列的字段,还会调整因为更新所带来的键值变化后的索引信息,**但是更新数据也需要先从数据库中获取**,索引加快了获取速度,所以可以相互抵消一下。 -* 索引会影响到 WHERE 的查询条件和排序 ORDER BY 两大功能 +| type | 含义 | +| --------------- | ------------------------------------------------------------ | +| ALL | 全表扫描,如果是 InnoDB 引擎是扫描聚簇索引 | +| index | 可以使用覆盖索引,但需要扫描全部索引 | +| range | 索引范围扫描,常见于 between、<、> 等的查询 | +| index_subquery | 子查询可以普通索引,则子查询的 type 为 index_subquery | +| unique_subquery | 子查询可以使用主键或唯一二级索引,则子查询的 type 为 index_subquery | +| index_merge | 索引合并 | +| ref_or_null | 非唯一性索引(普通二级索引)并且可以存储 NULL,进行等值匹配 | +| ref | 非唯一性索引与常量等值匹配 | +| eq_ref | 唯一性索引(主键或不存储 NULL 的唯一二级索引)进行等值匹配,如果二级索引是联合索引,那么所有联合的列都要进行等值匹配 | +| const | 通过主键或者唯一二级索引与常量进行等值匹配 | +| system | system 是 const 类型的特例,当查询的表只有一条记录的情况下,使用 system | +| NULL | MySQL 在优化过程中分解语句,执行时甚至不用访问表或索引 | +从上到下,性能从差到好,一般来说需要保证查询至少达到 range 级别, 最好达到 ref -*** +*** -#### 索引分类 -索引一般的分类如下: +##### key -- 功能分类 - - 单列索引:一个索引只包含单个列,一个表可以有多个单列索引(普通索引) - - 联合索引:顾名思义,就是将单列索引进行组合 - - 唯一索引:索引列的值必须唯一,允许有空值。如果是联合索引,则列值组合必须唯一 - - 主键索引:一种特殊的唯一索引,不允许有空值,一般在建表时同时创建主键索引 - - 外键索引:只有 InnoDB 引擎支持外键索引,用来保证数据的一致性、完整性和实现级联操作 +possible_keys: -- 结构分类 - - BTree 索引:MySQL 使用最频繁的一个索引数据结构,是 InnoDB 和 MyISAM 存储引擎默认的索引类型,底层基于 B+Tree - - Hash 索引:MySQL中 Memory 存储引擎默认支持的索引类型 - - R-tree 索引(空间索引):空间索引是 MyISAM 引擎的一个特殊索引类型,主要用于地理空间数据类型 - - Full-text 索引(全文索引):快速匹配全部文档的方式。MyISAM 支持, InnoDB 不支持 FULLTEXT 类型的索引,但是 InnoDB 可以使用 sphinx 插件支持全文索引,MEMORY 引擎不支持 - - | 索引 | InnoDB | MyISAM | Memory | - | --------- | ---------------- | ------ | ------ | - | BTREE | 支持 | 支持 | 支持 | - | HASH | 不支持 | 不支持 | 支持 | - | R-tree | 不支持 | 支持 | 不支持 | - | Full-text | 5.6 版本之后支持 | 支持 | 不支持 | +* 指出 MySQL 能使用哪个索引在表中找到记录,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用 +* 如果该列是 NULL,则没有相关的索引 -联合索引图示:根据身高年龄建立的组合索引(height,age) +key: -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-组合索引图.png) +* 显示 MySQL 在查询中实际使用的索引,若没有使用索引,显示为 NULL +* 查询中若使用了**覆盖索引**,则该索引可能出现在 key 列表,不出现在 possible_keys +key_len: +* 表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度 +* key_len 显示的值为索引字段的最大可能长度,并非实际使用长度,即 key_len 是根据表定义计算而得,不是通过表内检索出的 +* 在不损失精确性的前提下,长度越短越好 @@ -4230,207 +4485,289 @@ MySQL 官方对索引的定义为:索引(index)是帮助 MySQL 高效获 -### 聚簇索引 +##### Extra -#### 索引对比 +其他的额外的执行计划信息,在该列展示: -聚簇索引是一种数据存储方式,并不是一种单独的索引类型 +* No tables used:查询语句中使用 FROM dual 或者没有 FROM 语句 +* Impossible WHERE:查询语句中的 WHERE 子句条件永远为 FALSE,会导致没有符合条件的行 +* Using index:该值表示相应的 SELECT 操作中使用了**覆盖索引**(Covering Index) +* Using index condition:第一种情况是搜索条件中虽然出现了索引列,但是部分条件无法形成扫描区间(**索引失效**),会根据可用索引的条件先搜索一遍再匹配无法使用索引的条件,回表查询数据;第二种是使用了**索引条件下推**优化 +* Using where:搜索条件需要在 Server 层判断,判断后执行回表操作查询,无法使用索引下推 +* Using join buffer:连接查询被驱动表无法利用索引,需要连接缓冲区来存储中间结果 +* Using filesort:无法利用索引完成排序(优化方向),需要对数据使用外部排序算法,将取得的数据在内存或磁盘中进行排序 +* Using temporary:表示 MySQL 需要使用临时表来存储结果集,常见于排序、去重、分组等场景 +* Select tables optimized away:说明仅通过使用索引,优化器可能仅从聚合函数结果中返回一行 +* No tables used:Query 语句中使用 from dual 或不含任何 from 子句 -* 聚簇索引的叶子节点存放的是主键值和数据行,支持覆盖索引 -* 非聚簇索引的叶子节点存放的是主键值或指向数据行的指针(由存储引擎决定) -在 Innodb 下主键索引是聚簇索引,在 MyISAM 下主键索引是非聚簇索引 +参考文章:https://www.cnblogs.com/ggjucheng/archive/2012/11/11/2765237.html -*** +**** -#### Innodb +#### PROFILES -##### 聚簇索引 +SHOW PROFILES 能够在做 SQL 优化时分析当前会话中语句执行的**资源消耗**情况 -在 Innodb 存储引擎,B+ 树索引可以分为聚簇索引(也称聚集索引、clustered index)和辅助索引(也称非聚簇索引或二级索引、secondary index、non-clustered index) +* 通过 have_profiling 参数,能够看到当前 MySQL 是否支持 profile: + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-have_profiling.png) -InnoDB 中,聚簇索引是按照每张表的主键构造一颗 B+ 树,叶子节点中存放的就是整张表的数据,将聚簇索引的叶子节点称为数据页 +* 默认 profiling 是关闭的,可以通过 set 语句在 Session 级别开启 profiling: -* 这个特性决定了**数据也是索引的一部分**,所以一张表只能有一个聚簇索引 -* 辅助索引的存在不影响聚簇索引中数据的组织,所以一张表可以有多个辅助索引 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-profiling.png) -聚簇索引的优点: + ```mysql + SET profiling=1; #开启profiling 开关; + ``` -* 数据访问更快,聚簇索引将索引和数据保存在同一个 B+ 树中,因此从聚簇索引中获取数据比非聚簇索引更快 -* 聚簇索引对于主键的排序查找和范围查找速度非常快 +* 执行 SHOW PROFILES 指令, 来查看 SQL 语句执行的耗时: -聚簇索引的缺点: + ```mysql + SHOW PROFILES; + ``` -* 插入速度严重依赖于插入顺序,按照主键的顺序(递增)插入是最快的方式,否则将会出现页分裂,严重影响性能,所以对于 InnoDB 表,一般都会定义一个自增的 ID 列为主键 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-查看SQL语句执行耗时.png) -* 更新主键的代价很高,将会导致被更新的行移动,所以对于 InnoDB 表,一般定义主键为不可更新 +* 查看到该 SQL 执行过程中每个线程的状态和消耗的时间: -* 二级索引访问需要两次索引查找,第一次找到主键值,第二次根据主键值找到行数据 + ```mysql + SHOW PROFILE FOR QUERY query_id; + ``` + + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SQL执行每个状态消耗的时间.png) + **Sending data 状态**表示 MySQL 线程开始访问数据行并把结果返回给客户端,而不仅仅是返回给客户端。由于在 Sending data 状态下,MySQL 线程需要做大量磁盘读取操作,所以是整个查询中耗时最长的状态。 +* 在获取到最消耗时间的线程状态后,MySQL 支持选择 all、cpu、block io 、context switch、page faults 等类型查看 MySQL 在使用什么资源上耗费了过高的时间。例如,选择查看 CPU 的耗费时间: -*** + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SQL执行每个状态消耗的CPU.png) + * Status:SQL 语句执行的状态 + * Durationsql:执行过程中每一个步骤的耗时 + * CPU_user:当前用户占有的 CPU + * CPU_system:系统占有的 CPU -##### 辅助索引 -在聚簇索引之上创建的索引称之为辅助索引,非聚簇索引都是辅助索引,像复合索引、前缀索引、唯一索引等 +*** -辅助索引叶子节点存储的是主键值,而不是数据的物理地址,所以访问数据需要二次查找,推荐使用覆盖索引,可以减少回表查询 -**检索过程**:辅助索引找到主键值,再通过聚簇索引(二分)找到数据页,最后通过数据页中的 Page Directory(二分)找到对应的数据分组,遍历组内所所有的数据找到数据行 -补充:无索引走全表查询,查到数据页后和上述步骤一致 +#### TRACE +MySQL 提供了对 SQL 的跟踪, 通过 trace 文件可以查看优化器生成执行计划的过程 +* 打开 trace 功能,设置格式为 JSON,并设置 trace 的最大使用内存,避免解析过程中因默认内存过小而不能够完整展示 -*** + ```mysql + SET optimizer_trace="enabled=on",end_markers_in_json=ON; -- 会话内有效 + SET optimizer_trace_max_mem_size=1000000; + ``` +* 执行 SQL 语句: + ```mysql + SELECT * FROM tb_item WHERE id < 4; + ``` -##### 索引实现 +* 检查 information_schema.optimizer_trace: -InnoDB 使用 B+Tree 作为索引结构,并且 InnoDB 一定有索引 + ```mysql + SELECT * FROM information_schema.optimizer_trace \G; -- \G代表竖列展示 + ``` + + 执行信息主要有三个阶段:prepare 阶段、optimize 阶段(成本分析)、execute 阶段(执行) -主键索引: -* 在 InnoDB 中,表数据文件本身就是按 B+Tree 组织的一个索引结构,这个索引的 key 是数据表的主键,叶子节点 data 域保存了完整的数据记录 -* InnoDB 的表数据文件**通过主键聚集数据**,如果没有定义主键,会选择非空唯一索引代替,如果也没有这样的列,MySQL 会自动为 InnoDB 表生成一个**隐含字段**作为主键,这个字段长度为 6 个字节,类型为长整形(MVCC 部分的笔记提及) -辅助索引: -* InnoDB 的所有辅助索引(二级索引)都引用主键作为 data 域 +**** -* InnoDB 表是基于聚簇索引建立的,因此 InnoDB 的索引能提供一种非常快速的主键查找性能。不过辅助索引也会包含主键列,所以不建议使用过长的字段作为主键,**过长的主索引会令辅助索引变得过大** -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB聚簇和辅助索引结构.png) +### 索引失效 +#### 创建索引 -*** +索引是数据库优化最重要的手段之一,通过索引通常可以帮助用户解决大多数的 MySQL 的性能优化问题 +```mysql +CREATE TABLE `tb_seller` ( + `sellerid` varchar (100), + `name` varchar (100), + `nickname` varchar (50), + `password` varchar (60), + `status` varchar (1), + `address` varchar (100), + `createtime` datetime, + PRIMARY KEY(`sellerid`) +)ENGINE=INNODB DEFAULT CHARSET=utf8mb4; +INSERT INTO `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('xiaomi','小米科技','小米官方旗舰店','e10adc3949ba59abbe56e057f20f883e','1','西安市','2088-01-01 12:00:00'); +CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); +``` +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引环境准备.png) -#### MyISAM -##### 非聚簇 -MyISAM 的主键索引使用的是非聚簇索引,索引文件和数据文件是分离的,**索引文件仅保存数据的地址** +**** -* 主键索引 B+ 树的节点存储了主键,辅助键索引 B+ 树存储了辅助键,表数据存储在独立的地方,这两颗 B+ 树的叶子节点都使用一个地址指向真正的表数据,对于表数据来说,这两个键没有任何差别 -* 由于索引树是独立的,通过辅助索引检索无需访问主键的索引树回表查询 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-聚簇索引和辅助索引检锁数据图.jpg) +#### 避免失效 +##### 语句错误 -*** +* 全值匹配:对索引中所有列都指定具体值,这种情况索引生效,执行效率高 + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status='1' AND address='西安市'; + ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引1.png) -##### 索引实现 +* **最左前缀法则**:联合索引遵守最左前缀法则 -MyISAM 的索引方式也叫做非聚集的,之所以这么称呼是为了与 InnoDB 的聚集索引区分 + 匹配最左前缀法则,走索引: -主键索引:MyISAM 引擎使用 B+Tree 作为索引结构,叶节点的 data 域存放的是数据记录的地址 + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技'; + EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status='1'; + ``` -辅助索引:MyISAM 中主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求 key 是唯一的,而辅助索引的 key 可以重复 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引2.png) -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM主键和辅助索引结构.png) + 违法最左前缀法则 , 索引失效: + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE status='1'; + EXPLAIN SELECT * FROM tb_seller WHERE status='1' AND address='西安市'; + ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引3.png) + 如果符合最左法则,但是出现跳跃某一列,只有最左列索引生效: + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND address='西安市'; + ``` -参考文章:https://blog.csdn.net/lm1060891265/article/details/81482136 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引4.png) + 虽然索引列失效,但是系统**使用了索引下推进行了优化** +* **范围查询**右边的列,不能使用索引: -*** + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status>'1' AND address='西安市'; + ``` + 根据前面的两个字段 name , status 查询是走索引的, 但是最后一个条件 address 没有用到索引,使用了索引下推 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引5.png) -### 索引结构 +* 在索引列上进行**运算操作**, 索引将失效: -#### 数据页 + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE SUBSTRING(name,3,2) = '科技'; + ``` -文件系统的最小单元是块(block),一个块的大小是 4K,系统从磁盘读取数据到内存时是以磁盘块为基本单位的,位于同一个磁盘块中的数据会被一次性读取出来,而不是需要什么取什么 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引6.png) -InnoDB 存储引擎中有页(Page)的概念,页是 MySQL 磁盘管理的最小单位 +* **字符串不加单引号**,造成索引失效: -* **InnoDB 存储引擎中默认每个页的大小为 16KB,索引中一个节点就是一个数据页**,所以会一次性读取 16KB 的数据到内存 -* InnoDB 引擎将若干个地址连接磁盘块,以此来达到页的大小 16KB -* 在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘 I/O 次数,提高查询效率 + 在查询时,没有对字符串加单引号,MySQL 的查询优化器,会自动的进行类型转换,造成索引失效 + + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status=1; + ``` + + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引7.png) +* **用 OR 分割条件,索引失效**,导致全表查询: + OR 前的条件中的列有索引而后面的列中没有索引或 OR 前后两个列是同一个复合索引,都造成索引失效 + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name='阿里巴巴' OR createtime = '2088-01-01 12:00:00'; + EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' OR status='1'; + ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引10.png) -*** + **AND 分割的条件不影响**: + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name='阿里巴巴' AND createtime = '2088-01-01 12:00:00'; + ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引11.png) -#### BTree +* **以 % 开头的 LIKE 模糊查询**,索引失效: -BTree 的索引类型是基于 B+Tree 树型数据结构的,B+Tree 又是 BTree 数据结构的变种,用在数据库和操作系统中的文件系统,特点是能够保持数据稳定有序 + 如果是尾部模糊匹配,索引不会失效;如果是头部模糊匹配,索引失效。 -BTree 又叫多路平衡搜索树,一颗 m 叉的 BTree 特性如下: + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name like '%科技%'; + ``` -- 树中每个节点最多包含 m 个孩子 -- 除根节点与叶子节点外,每个节点至少有 [ceil(m/2)] 个孩子 -- 若根节点不是叶子节点,则至少有两个孩子 -- 所有的叶子节点都在同一层 -- 每个非叶子节点由 n 个key与 n+1 个指针组成,其中 [ceil(m/2)-1] <= n <= m-1 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引12.png) -5 叉,key 的数量 [ceil(m/2)-1] <= n <= m-1 为 2 <= n <=4 ,当 n>4 时中间节点分裂到父节点,两边节点分裂 + 解决方案:通过覆盖索引来解决 -插入 C N G A H E K Q M F W L T Z D P R X Y S 数据的工作流程: + ```mysql + EXPLAIN SELECT sellerid,name,status FROM tb_seller WHERE name like '%科技%'; + ``` -* 插入前4个字母 C N G A + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引13.png) - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程1.png) + 原因:在覆盖索引的这棵 B+ 数上只需要进行 like 的匹配,或者是基于覆盖索引查询再进行 WHERE 的判断就可以获得结果 -* 插入 H,n>4,中间元素 G 字母向上分裂到新的节点 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程2.png) -* 插入 E、K、Q 不需要分裂 +*** - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程3.png) -* 插入 M,中间元素 M 字母向上分裂到父节点 G - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程4.png) +##### 系统优化 -* 插入 F,W,L,T 不需要分裂 +系统优化为全表扫描: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程5.png) +* 如果 MySQL 评估使用索引比全表更慢,则不使用索引,索引失效: -* 插入 Z,中间元素 T 向上分裂到父节点中 + ```mysql + CREATE INDEX idx_address ON tb_seller(address); + EXPLAIN SELECT * FROM tb_seller WHERE address='西安市'; + EXPLAIN SELECT * FROM tb_seller WHERE address='北京市'; + ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程6.png) + 北京市的键值占 9/10(区分度低),所以优化为全表扫描,type = ALL -* 插入 D,中间元素 D 向上分裂到父节点中,然后插入 P,R,X,Y 不需要分裂 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引14.png) - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程7.png) +* IS NULL、IS NOT NULL **有时**索引失效: -* 最后插入 S,NPQR 节点 n>5,中间节点 Q 向上分裂,但分裂后父节点 DGMT 的 n>5,中间节点 M 向上分裂 + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name IS NULL; + EXPLAIN SELECT * FROM tb_seller WHERE name IS NOT NULL; + ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程8.png) + NOT NULL 失效的原因是 name 列全部不是 null,优化为全表扫描,当 NULL 过多时,IS NULL 失效 -BTree 树就已经构建完成了,BTree 树和二叉树相比, 查询数据的效率更高, 因为对于相同的数据量来说,**BTree 的层级结构比二叉树小**,所以搜索速度快 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引15.png) -BTree 结构的数据可以让系统高效的找到数据所在的磁盘块,定义一条记录为一个二元组 [key, data] ,key 为记录的键值,对应表中的主键值,data 为一行记录中除主键外的数据。对于不同的记录,key 值互不相同,BTree 中的每个节点根据实际情况可以包含大量的关键字信息和分支 -![](https://gitee.com/seazean/images/raw/master/DB/索引的原理-BTree.png) +* IN 肯定会走索引,但是当 IN 的取值范围较大时会导致索引失效,走全表扫描: -缺点:当进行范围查找时会出现回旋查找 + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE sellerId IN ('alibaba','huawei');-- 都走索引 + EXPLAIN SELECT * FROM tb_seller WHERE sellerId NOT IN ('alibaba','huawei'); + ``` @@ -4438,24 +4775,20 @@ BTree 结构的数据可以让系统高效的找到数据所在的磁盘块, -#### B+Tree - -##### 数据结构 +#### 底层原理 -BTree 数据结构中每个节点中不仅包含数据的 key 值,还有 data 值。磁盘中每一页的存储空间是有限的,如果 data 数据较大时将会导致每个节点(即一个页)能存储的 key 的数量很小,当存储的数据量很大时同样会导致 B-Tree 的深度较大,增大查询时的磁盘 I/O 次数,进而影响查询效率,所以引入 B+Tree +索引失效一般是针对联合索引,联合索引一般由几个字段组成,排序方式是先按照第一个字段进行排序,然后排序第二个,依此类推,图示(a, b)索引,**a 相等的情况下 b 是有序的** -B+Tree 为 BTree 的变种,B+Tree 与 BTree 的区别为: + -* n 叉 B+Tree 最多含有 n 个 key(哈希值),而 BTree 最多含有 n-1 个 key +* 最左前缀法则:当不匹配前面的字段的时候,后面的字段都是无序的。这种无序不仅体现在叶子节点,也会**导致查询时扫描的非叶子节点也是无序的**,因为索引树相当于忽略的第一个字段,就无法使用二分查找 -- 所有**非叶子节点只存储键值 key** 信息,只进行数据索引,使每个非叶子节点所能保存的关键字大大增加 -- 所有**数据都存储在叶子节点**,所以每次数据查询的次数都一样 -- **叶子节点按照 key 大小顺序排列,左边结尾数据都会保存右边节点开始数据的指针,形成一个链表** -- 所有节点中的 key 在叶子节点中也存在(比如 5),key 允许重复,B 树不同节点不存在重复的 key +* 范围查询右边的列,不能使用索引,比如语句: `WHERE a > 1 AND b = 1 `,在 a 大于 1 的时候,b 是无序的,a > 1 是扫描时有序的,但是找到以后进行寻找 b 时,索引树就不是有序的了 - + -B* 树:是 B+ 树的变体,在 B+ 树的非根和非叶子结点再增加指向兄弟的指针 +* 以 % 开头的 LIKE 模糊查询,索引失效,比如语句:`WHERE a LIKE '%d'`,前面的不确定,导致不符合最左匹配,直接去索引中搜索以 d 结尾的节点,所以没有顺序 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-索引失效底层原理3.png) @@ -4463,29 +4796,28 @@ B* 树:是 B+ 树的变体,在 B+ 树的非根和非叶子结点再增加指 -##### 优化结构 +#### 查看索引 -MySQL 索引数据结构对经典的 B+Tree 进行了优化,在原 B+Tree 的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的 B+Tree,**提高区间访问的性能,防止回旋查找** +```mysql +SHOW STATUS LIKE 'Handler_read%'; +SHOW GLOBAL STATUS LIKE 'Handler_read%'; +``` -区间访问的意思是访问索引为 5 - 15 的数据,可以直接根据相邻节点的指针遍历 +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL查看索引使用情况.png) -B+ 树的**叶子节点是数据页**(page),一个页里面可以存多个数据行 +* Handler_read_first:索引中第一条被读的次数,如果较高,表示服务器正执行大量全索引扫描(这个值越低越好) -![](https://gitee.com/seazean/images/raw/master/DB/索引的原理-B+Tree.png) +* Handler_read_key:如果索引正在工作,这个值代表一个行被索引值读的次数,值越低表示索引不经常使用(这个值越高越好) -通常在 B+Tree 上有两个头指针,**一个指向根节点,另一个指向关键字最小的叶子节点**,而且所有叶子节点(即数据节点)之间是一种链式环结构。可以对 B+Tree 进行两种查找运算: +* Handler_read_next:按照键顺序读下一行的请求数,如果范围约束或执行索引扫描来查询索引列,值增加 -- 有范围:对于主键的范围查找和分页查找 -- 有顺序:从根节点开始,进行随机查找,顺序查找 +* Handler_read_prev:按照键顺序读前一行的请求数,该读方法主要用于优化 ORDER BY ... DESC -InnoDB 中每个数据页的大小默认是 16KB, +* Handler_read_rnd:根据固定位置读一行的请求数,如果执行大量查询并对结果进行排序则该值较高,可能是使用了大量需要 MySQL 扫描整个表的查询或连接,这个值较高意味着运行效率低,应该建立索引来解决 -* 索引行:一般表的主键类型为 INT(4 字节)或 BIGINT(8 字节),指针大小在 InnoDB 中设置为 6 字节节,也就是说一个页大概存储 16KB/(8B+6B)=1K 个键值(估值)。则一个深度为 3 的 B+Tree 索引可以维护 `10^3 * 10^3 * 10^3 = 10亿` 条记录 -* 数据行:一行数据的大小可能是 1k,一个数据页可以存储 16 行 +* Handler_read_rnd_next:在数据文件中读下一行的请求数,如果正进行大量的表扫描,该值较高,说明表索引不正确或写入的查询没有利用索引 -实际情况中每个节点可能不能填充满,因此在数据库中,B+Tree 的高度一般都在 2-4 层。MySQL 的 InnoDB 存储引擎在设计时是**将根节点常驻内存的**,也就是说查找某一键值的行记录时最多只需要 1~3 次磁盘 I/O 操作 -B+Tree 优点:提高查询速度,减少磁盘的 IO 次数,树形结构较小 @@ -4493,136 +4825,76 @@ B+Tree 优点:提高查询速度,减少磁盘的 IO 次数,树形结构较 -##### 索引维护 +### SQL 优化 -B+ 树为了保持索引的有序性,在插入新值的时候需要做相应的维护 +#### 覆盖索引 -每个索引中每个块存储在磁盘页中,可能会出现以下两种情况: +复合索引叶子节点不仅保存了复合索引的值,还有主键索引,所以使用覆盖索引的时候,加上主键也会用到索引 -* 如果所在的数据页已经满了,这时候需要申请一个新的数据页,然后挪动部分数据过去,这个过程称为**页分裂** -* 当相邻两个页由于删除了数据,利用率很低之后,会将数据页做**页合并**,合并的过程可以认为是分裂过程的逆过程 -* 这两个情况都是由 B+ 树的结构决定的 +尽量使用覆盖索引,避免 SELECT *: -一般选用数据小的字段做索引,字段长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小 +```mysql +EXPLAIN SELECT name,status,address FROM tb_seller WHERE name='小米科技' AND status='1' AND address='西安市'; +``` +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引8.png) +如果查询列,超出索引列,也会降低性能: -*** +```mysql +EXPLAIN SELECT name,status,address,password FROM tb_seller WHERE name='小米科技' AND status='1' AND address='西安市'; +``` +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引9.png) -### 索引操作 -索引在创建表的时候可以同时创建, 也可以随时增加新的索引 +**** -* 创建索引:如果一个表中有一列是主键,那么会**默认为其创建主键索引**(主键列不需要单独创建索引) - - ```mysql - CREATE [UNIQUE|FULLTEXT] INDEX 索引名称 [USING 索引类型] ON 表名(列名...); - -- 索引类型默认是 B+TREE - ``` - -* 查看索引 - ```mysql - SHOW INDEX FROM 表名; - ``` -* 添加索引 +#### 减少访问 - ```mysql - -- 单列索引 - ALTER TABLE 表名 ADD INDEX 索引名称(列名); - - -- 组合索引 - ALTER TABLE 表名 ADD INDEX 索引名称(列名1,列名2,...); - - -- 主键索引 - ALTER TABLE 表名 ADD PRIMARY KEY(主键列名); - - -- 外键索引(添加外键约束,就是外键索引) - ALTER TABLE 表名 ADD CONSTRAINT 外键名 FOREIGN KEY (本表外键列名) REFERENCES 主表名(主键列名); - - -- 唯一索引 - ALTER TABLE 表名 ADD UNIQUE 索引名称(列名); - - -- 全文索引(mysql只支持文本类型) - ALTER TABLE 表名 ADD FULLTEXT 索引名称(列名); - ``` +避免对数据进行重复检索:能够一次连接就获取到结果的,就不用两次连接,这样可以大大减少对数据库无用的重复请求 -* 删除索引 +* 查询数据: ```mysql - DROP INDEX 索引名称 ON 表名; + SELECT id,name FROM tb_book; + SELECT id,status FROM tb_book; -- 向数据库提交两次请求,数据库就要做两次查询操作 + -- > 优化为: + SELECT id,name,statu FROM tb_book; ``` -* 案例练习 - - 数据准备:student - +* 插入数据: + ```mysql - id NAME age score - 1 张三 23 99 - 2 李四 24 95 - 3 王五 25 98 - 4 赵六 26 97 + INSERT INTO tb_test VALUES(1,'Tom'); + INSERT INTO tb_test VALUES(2,'Cat'); + INSERT INTO tb_test VALUES(3,'Jerry'); -- 连接三次数据库 + -- >优化为 + INSERT INTO tb_test VALUES(1,'Tom'),(2,'Cat'),(3,'Jerry'); -- 连接一次 ``` - 索引操作: - +* 在事务中进行数据插入: + ```mysql - -- 为student表中姓名列创建一个普通索引 - CREATE INDEX idx_name ON student(NAME); - - -- 为student表中年龄列创建一个唯一索引 - CREATE UNIQUE INDEX idx_age ON student(age); + start transaction; + INSERT INTO tb_test VALUES(1,'Tom'); + INSERT INTO tb_test VALUES(2,'Cat'); + INSERT INTO tb_test VALUES(3,'Jerry'); + commit; -- 手动提交,分段提交 ``` - - - -*** - - - -### 设计原则 - -索引的设计可以遵循一些已有的原则,创建索引的时候请尽量考虑符合这些原则,便于提升索引的使用效率 -创建索引时的原则: -- 对查询频次较高,且数据量比较大的表建立索引 -- 使用唯一索引,区分度越高,使用索引的效率越高 -- 索引字段的选择,最佳候选列应当从 where 子句的条件中提取,使用覆盖索引 -- 使用短索引,索引创建之后也是使用硬盘来存储的,因此提升索引访问的 I/O 效率,也可以提升总体的访问效率。假如构成索引的字段总长度比较短,那么在给定大小的存储块内可以存储更多的索引值,相应的可以有效的提升 MySQL 访问索引的 I/O 效率 -- 索引可以有效的提升查询数据的效率,但索引数量不是多多益善,索引越多,维护索引的代价越高。对于插入、更新、删除等 DML 操作比较频繁的表来说,索引过多,会引入相当高的维护代价,降低 DML 操作的效率,增加相应操作的时间消耗;另外索引过多的话,MySQL 也会犯选择困难病,虽然最终仍然会找到一个可用的索引,但提高了选择的代价 +* 数据有序插入: -* MySQL 建立联合索引时会遵守**最左前缀匹配原则**,即最左优先,在检索数据时从联合索引的最左边开始匹配 - - N 个列组合而成的组合索引,相当于创建了 N 个索引,如果查询时 where 句中使用了组成该索引的**前**几个字段,那么这条查询 SQL 可以利用组合索引来提升查询效率 - - ```mysql - -- 对name、address、phone列建一个联合索引 - ALTER TABLE user ADD INDEX index_three(name,address,phone); - -- 查询语句执行时会依照最左前缀匹配原则,检索时分别会使用索引进行数据匹配。 - (name,address,phone) - (name,address) - (name,phone) -- 只有name字段走了索引 - (name) - - -- 索引的字段可以是任意顺序的,优化器会帮助我们调整顺序,下面的SQL语句可以命中索引 - SELECT * FROM user WHERE address = '北京' AND phone = '12345' AND name = '张三'; - ``` - ```mysql - -- 如果联合索引中最左边的列不包含在条件查询中,SQL语句就不会命中索引,比如: - SELECT * FROM user WHERE address = '北京' AND phone = '12345'; + INSERT INTO tb_test VALUES(1,'Tom'); + INSERT INTO tb_test VALUES(2,'Cat'); + INSERT INTO tb_test VALUES(3,'Jerry'); ``` -哪些情况不要建立索引: - -* 记录太少的表 -* 经常增删改的表 -* 频繁更新的字段不适合创建索引 -* where 条件里用不到的字段不创建索引 +增加 cache 层:在应用中增加缓存层来达到减轻数据库负担的目的。可以部分数据从数据库中抽取出来放到应用端以文本方式存储,或者使用框架(Mybatis)提供的一级缓存 / 二级缓存,或者使用 Redis 数据库来缓存数据 @@ -4630,355 +4902,330 @@ B+ 树为了保持索引的有序性,在插入新值的时候需要做相应 -### 索引优化 - -#### 覆盖索引 - -覆盖索引:包含所有满足查询需要的数据的索引(SELECT 后面的字段刚好是索引字段),可以利用该索引返回 SELECT 列表的字段,而不必根据索引去聚簇索引上读取数据文件 - -回表查询:要查找的字段不在非主键索引树上时,需要通过叶子节点的主键值去主键索引上获取对应的行数据 +#### 数据插入 -使用覆盖索引,防止回表查询: +当使用 load 命令导入数据的时候,适当的设置可以提高导入的效率: -* 表 user 主键为 id,普通索引为 age,查询语句: +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL load data.png) - ```mysql - SELECT * FROM user WHERE age = 30; - ``` +```mysql +LOAD DATA LOCAL INFILE = '/home/seazean/sql1.log' INTO TABLE `tb_user_1` FIELD TERMINATED BY ',' LINES TERMINATED BY '\n'; -- 文件格式如上图 +``` - 查询过程:先通过普通索引 age=30 定位到主键值 id=1,再通过聚集索引 id=1 定位到行记录数据,需要两次扫描 B+ 树 +对于 InnoDB 类型的表,有以下几种方式可以提高导入的效率: -* 使用覆盖索引: +1. **主键顺序插入**:因为 InnoDB 类型的表是按照主键的顺序保存的,所以将导入的数据按照主键的顺序排列,可以有效的提高导入数据的效率,如果 InnoDB 表没有主键,那么系统会自动默认创建一个内部列作为主键。 - ```mysql - DROP INDEX idx_age ON user; - CREATE INDEX idx_age_name ON user(age,name); - SELECT id,age FROM user WHERE age = 30; - ``` + **主键是否连续对性能影响不大,只要是递增的就可以**,比如雪花算法产生的 ID 不是连续的,但是是递增的 - 在一棵索引树上就能获取查询所需的数据,无需回表速度更快 + * 插入 ID 顺序排列数据: -使用覆盖索引,要注意 SELECT 列表中只取出需要的列,不可用 SELECT *,所有字段一起做索引会导致索引文件过大,查询性能下降 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL插入ID顺序排列数据.png) + * 插入 ID 无序排列数据: + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL插入ID无序排列数据.png) -*** +2. **关闭唯一性校验**:在导入数据前执行 `SET UNIQUE_CHECKS=0`,关闭唯一性校验;导入结束后执行 `SET UNIQUE_CHECKS=1`,恢复唯一性校验,可以提高导入的效率。 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL插入数据关闭唯一性校验.png) +3. **手动提交事务**:如果应用使用自动提交的方式,建议在导入前执行`SET AUTOCOMMIT=0`,关闭自动提交;导入结束后再打开自动提交,可以提高导入的效率。 -#### 索引下推 + 事务需要控制大小,事务太大可能会影响执行的效率。MySQL 有 innodb_log_buffer_size 配置项,超过这个值的日志会写入磁盘数据,效率会下降,所以在事务大小达到配置项数据级前进行事务提交可以提高效率 -索引条件下推优化(Index Condition Pushdown)是 MySQL5.6 添加,可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL插入数据手动提交事务.png) -索引下推充分利用了索引中的数据,在查询出整行数据之前过滤掉无效的数据,再去主键索引树上查找 -* 不使用索引下推优化时存储引擎通过索引检索到数据返回给 MySQL 服务器,服务器判断数据是否符合条件,符合条件的数据去聚簇索引回表查询,获取完整的数据 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-不使用索引下推.png) -* 使用索引下推优化时,如果**存在某些被索引的列的判断条件**时,MySQL 服务器将这一部分判断条件传递给存储引擎,由存储引擎在索引遍历的过程中判断数据是否符合传递的条件,将符合条件的数据进行回表,检索出来返回给服务器,由此减少 IO 次数 +**** - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-使用索引下推.png) -**适用条件**: -* 需要存储引擎将索引中的数据与条件进行判断(所以**条件列必须都在同一个索引中**),所以优化是基于存储引擎的,只有特定引擎可以使用,适用于 InnoDB 和 MyISAM -* 存储引擎没有调用跨存储引擎的能力,跨存储引擎的功能有存储过程、触发器、视图,所以调用这些功能的不可以进行索引下推优化 -* 对于 InnoDB 引擎只适用于二级索引,InnoDB 的聚簇索引会将整行数据读到缓冲区,不再需要去回表查询了,索引下推的目的减少 IO 次数也就失去了意义 +#### ORDER BY -工作过程:用户表 user,(name, age) 是联合索引 +数据准备: ```mysql -SELECT * FROM user WHERE name LIKE '张%' AND age = 10; -- 头部模糊匹配会造成索引失效 +CREATE TABLE `emp` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `name` VARCHAR(100) NOT NULL, + `age` INT(3) NOT NULL, + `salary` INT(11) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=INNODB DEFAULT CHARSET=utf8mb4; +INSERT INTO `emp` (`id`, `name`, `age`, `salary`) VALUES('1','Tom','25','2300');-- ... +CREATE INDEX idx_emp_age_salary ON emp(age,salary); ``` -* 优化前:在非主键索引树上找到满足第一个条件的行,然后通过叶子节点记录的主键值再回到主键索引树上查找到对应的行数据,再对比 AND 后的条件是否符合,符合返回数据,需要 4 次回表 - - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-索引下推优化1.png) - -* 优化后:检查索引中存储的列信息是否符合索引条件,然后交由存储引擎用剩余的判断条件判断此行数据是否符合要求,**不满足条件的不去读取表中的数据**,满足下推条件的就根据主键值进行回表查询,2 次回表 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-索引下推优化2.png) - -当使用 EXPLAIN 进行分析时,如果使用了索引条件下推,Extra 会显示 Using index condition +* 第一种是通过对返回数据进行排序,所有不通过索引直接返回结果的排序都叫 FileSort 排序,会在内存中重新排序 + ```mysql + EXPLAIN SELECT * FROM emp ORDER BY age DESC; -- 年龄降序 + ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL ORDER BY排序1.png) -参考文章:https://blog.csdn.net/sinat_29774479/article/details/103470244 +* 第二种通过有序索引顺序扫描直接返回**有序数据**,这种情况为 Using index,不需要额外排序,操作效率高 -参考文章:https://time.geekbang.org/column/article/69636 + ```mysql + EXPLAIN SELECT id, age, salary FROM emp ORDER BY age DESC; + ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL ORDER BY排序2.png) +* 多字段排序: -*** + ```mysql + EXPLAIN SELECT id,age,salary FROM emp ORDER BY age DESC, salary DESC; + EXPLAIN SELECT id,age,salary FROM emp ORDER BY salary DESC, age DESC; + EXPLAIN SELECT id,age,salary FROM emp ORDER BY age DESC, salary ASC; + ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL ORDER BY排序3.png) + 尽量减少额外的排序,通过索引直接返回有序数据。**需要满足 Order by 使用相同的索引、Order By 的顺序和索引顺序相同、Order by 的字段都是升序或都是降序**,否则需要额外的操作,就会出现 FileSort -#### 前缀索引 +优化:通过创建合适的索引能够减少 Filesort 的出现,但是某些情况下条件限制不能让 Filesort 消失,就要加快 Filesort 的排序操作 -当要索引的列字符很多时,索引会变大变慢,可以只索引列开始的部分字符串,节约索引空间,提高索引效率 +对于 Filesort , MySQL 有两种排序算法: -注意:使用前缀索引就系统就忽略覆盖索引对查询性能的优化了 +* 两次扫描算法:MySQL4.1 之前,使用该方式排序。首先根据条件取出排序字段和行指针信息,然后在排序区 sort buffer 中排序,如果 sort buffer 不够,则在临时表 temporary table 中存储排序结果。完成排序后再根据行指针**回表读取记录**,该操作可能会导致大量随机 I/O 操作 +* 一次扫描算法:一次性取出满足条件的所有数据,需要回表,然后在排序区 sort buffer 中排序后直接输出结果集。排序时内存开销较大,但是排序效率比两次扫描算法高 -优化原则:**降低重复的索引值** +MySQL 通过比较系统变量 max_length_for_sort_data 的大小和 Query 语句取出的字段的大小,来判定使用哪种排序算法。如果前者大,则说明 sort buffer 空间足够,使用第二种优化之后的算法,否则使用第一种。 -比如地区表: +可以适当提高 sort_buffer_size 和 max_length_for_sort_data 系统变量,来增大排序区的大小,提高排序的效率 ```mysql -area gdp code -chinaShanghai 100 aaa -chinaDalian 200 bbb -usaNewYork 300 ccc -chinaFuxin 400 ddd -chinaBeijing 500 eee +SET @@max_length_for_sort_data = 10000; -- 设置全局变量 +SET max_length_for_sort_data = 10240; -- 设置会话变量 +SHOW VARIABLES LIKE 'max_length_for_sort_data'; -- 默认1024 +SHOW VARIABLES LIKE 'sort_buffer_size'; -- 默认262114 ``` -发现 area 字段很多都是以 china 开头的,那么如果以前 1-5 位字符做前缀索引就会出现大量索引值重复的情况,索引值重复性越低,查询效率也就越高,所以需要建立前 6 位字符的索引: -```mysql -CREATE INDEX idx_area ON table_name(area(7)); -``` -场景:存储身份证 +*** -* 直接创建完整索引,这样可能比较占用空间 -* 创建前缀索引,节省空间,但会增加查询扫描次数,并且不能使用覆盖索引 -* 倒序存储,再创建前缀索引,用于绕过字符串本身前缀的区分度不够的问题(前 6 位相同的很多) -* 创建 hash 字段索引,查询性能稳定,有额外的存储和计算消耗,跟第三种方式一样,都不支持范围扫描 +#### GROUP BY -**** +GROUP BY 也会进行排序操作,与 ORDER BY 相比,GROUP BY 主要只是多了排序之后的分组操作,所以在 GROUP BY 的实现过程中,与 ORDER BY 一样也可以利用到索引 +* 分组查询: + ```mysql + DROP INDEX idx_emp_age_salary ON emp; + EXPLAIN SELECT age,COUNT(*) FROM emp GROUP BY age; + ``` -#### 索引合并 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL GROUP BY排序1.png) -使用多个索引来完成一次查询的执行方法叫做索引合并 index merge + Using temporary:表示 MySQL 需要使用临时表来存储结果集,常见于排序和分组查询 -* Intersection 索引合并: +* 查询包含 GROUP BY 但是用户想要避免排序结果的消耗, 则可以执行 ORDER BY NULL 禁止排序: - ```sql - SELECT * FROM table_test WHERE key1 = 'a' AND key3 = 'b'; # key1 和 key3 列都是单列索引、二级索引 + ```mysql + EXPLAIN SELECT age,COUNT(*) FROM emp GROUP BY age ORDER BY NULL; ``` - 从不同索引中扫描到的记录的 id 值取**交集**(相同 id),然后执行回表操作,要求从每个二级索引获取到的记录都是按照主键值排序 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL GROUP BY排序2.png) -* Union 索引合并: +* 创建索引: - ```sql - SELECT * FROM table_test WHERE key1 = 'a' OR key3 = 'b'; + ```mysql + CREATE INDEX idx_emp_age_salary ON emp(age,salary); ``` - 从不同索引中扫描到的记录的 id 值取**并集**,然后执行回表操作,要求从每个二级索引获取到的记录都是按照主键值排序 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL GROUP BY排序3.png) -* Sort-Union 索引合并 - ```sql - SELECT * FROM table_test WHERE key1 < 'a' OR key3 > 'b'; - ``` - 先将从不同索引中扫描到的记录的主键值进行排序,再按照 Union 索引合并的方式进行查询 +*** +#### OR + +对于包含 OR 的查询子句,如果要利用索引,则 OR 之间的**每个条件列都必须用到索引,而且不能使用到条件之间的复合索引**,如果没有索引,则应该考虑增加索引 +* 执行查询语句: + ```mysql + EXPLAIN SELECT * FROM emp WHERE id = 1 OR age = 30; -- 两个索引,并且不是复合索引 + ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL OR条件查询1.png) -*** + ```sh + Extra: Using sort_union(idx_emp_age_salary,PRIMARY); Using where + ``` +* 使用 UNION 替换 OR,求并集: + + 注意:该优化只针对多个索引列有效,如果有列没有被索引,查询效率可能会因为没有选择 OR 而降低 + + ```mysql + EXPLAIN SELECT * FROM emp WHERE id = 1 UNION SELECT * FROM emp WHERE age = 30; + ``` + + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL OR条件查询2.png) + +* UNION 要优于 OR 的原因: + * UNION 语句的 type 值为 ref,OR 语句的 type 值为 range + * UNION 语句的 ref 值为 const,OR 语句的 ref 值为 null,const 表示是常量值引用,非常快 -## 系统优化 +**** -### 优化步骤 -#### 执行频率 -随着生产数据量的急剧增长,很多 SQL 语句逐渐显露出性能问题,对生产的影响也越来越大,此时有问题的 SQL 语句就成为整个系统性能的瓶颈,因此必须要进行优化 +#### 嵌套查询 -MySQL 客户端连接成功后,查询服务器状态信息: +MySQL 4.1 版本之后,开始支持 SQL 的子查询 -```mysql -SHOW [SESSION|GLOBAL] STATUS LIKE ''; --- SESSION: 显示当前会话连接的统计结果,默认参数 --- GLOBAL: 显示自数据库上次启动至今的统计结果 -``` +* 可以使用 SELECT 语句来创建一个单列的查询结果,然后把结果作为过滤条件用在另一个查询中 +* 使用子查询可以一次性的完成逻辑上需要多个步骤才能完成的 SQL 操作,同时也可以避免事务或者表锁死 +* 在有些情况下,子查询是可以被更高效的连接(JOIN)替代 -* 查看SQL执行频率: +例如查找有角色的所有的用户信息: + +* 执行计划: ```mysql - SHOW STATUS LIKE 'Com_____'; + EXPLAIN SELECT * FROM t_user WHERE id IN (SELECT user_id FROM user_role); ``` - Com_xxx 表示每种语句执行的次数 - - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SQL语句执行频率.png) + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL嵌套查询1.png) -* 查询 SQL 语句影响的行数: +* 优化后: ```mysql - SHOW STATUS LIKE 'Innodb_rows_%'; + EXPLAIN SELECT * FROM t_user u , user_role ur WHERE u.id = ur.user_id; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SQL语句影响的行数.png) - -Com_xxxx:这些参数对于所有存储引擎的表操作都会进行累计 - -Innodb_xxxx:这几个参数只是针对InnoDB 存储引擎的,累加的算法也略有不同 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL嵌套查询2.png) -| 参数 | 含义 | -| :------------------- | ------------------------------------------------------------ | -| Com_select | 执行 SELECT 操作的次数,一次查询只累加 1 | -| Com_insert | 执行 INSERT 操作的次数,对于批量插入的 INSERT 操作,只累加一次 | -| Com_update | 执行 UPDATE 操作的次数 | -| Com_delete | 执行 DELETE 操作的次数 | -| Innodb_rows_read | 执行 SELECT 查询返回的行数 | -| Innodb_rows_inserted | 执行 INSERT 操作插入的行数 | -| Innodb_rows_updated | 执行 UPDATE 操作更新的行数 | -| Innodb_rows_deleted | 执行 DELETE 操作删除的行数 | -| Connections | 试图连接 MySQL 服务器的次数 | -| Uptime | 服务器工作时间 | -| Slow_queries | 慢查询的次数 | + 连接查询之所以效率更高 ,是因为不需要在内存中创建临时表来完成逻辑上需要两个步骤的查询工作 -**** +*** -#### 定位低效 -SQL 执行慢有两种情况: -* 偶尔慢:DB 在刷新脏页 - * redo log 写满了 - * 内存不够用,要从 LRU 链表中淘汰 - * MySQL 认为系统空闲的时候 - * MySQL 关闭时 -* 一直慢的原因:索引没有设计好、SQL 语句没写好、MySQL 选错了索引 +#### 分页查询 -通过以下两种方式定位执行效率较低的 SQL 语句 +一般分页查询时,通过创建覆盖索引能够比较好地提高性能 -* 慢日志查询: 慢查询日志在查询结束以后才记录,执行效率出现问题时查询日志并不能定位问题 +一个常见的问题是 `LIMIT 200000,10`,此时需要 MySQL 扫描前 200010 记录,仅仅返回 200000 - 200010 之间的记录,其他记录丢弃,查询排序的代价非常大 - 配置文件修改:修改 .cnf 文件 `vim /etc/mysql/my.cnf`,重启 MySQL 服务器 +* 分页查询: - ```sh - slow_query_log=ON - slow_query_log_file=/usr/local/mysql/var/localhost-slow.log - long_query_time=1 #记录超过long_query_time秒的SQL语句的日志 - log-queries-not-using-indexes = 1 + ```mysql + EXPLAIN SELECT * FROM tb_user_1 LIMIT 200000,10; ``` - 使用命令配置: + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL分页查询1.png) - ```mysql - mysql> SET slow_query_log=ON; - mysql> SET GLOBAL slow_query_log=ON; +* 优化方式一:子查询,在索引列 id 上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容 + + ```mysql + EXPLAIN SELECT * FROM tb_user_1 t,(SELECT id FROM tb_user_1 ORDER BY id LIMIT 200000,10) a WHERE t.id = a.id; ``` - 查看是否配置成功: + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL分页查询2.png) + +* 优化方式二:方案适用于主键自增的表,可以把 LIMIT 查询转换成某个位置的查询 ```mysql - SHOW VARIABLES LIKE '%query%' + EXPLAIN SELECT * FROM tb_user_1 WHERE id > 200000 LIMIT 10; -- 写法 1 + EXPLAIN SELECT * FROM tb_user_1 WHERE id BETWEEN 200000 and 200010; -- 写法 2 ``` + + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL分页查询3.png) -* SHOW PROCESSLIST:**实时查看**当前 MySQL 在进行的连接线程,包括线程的状态、是否锁表、SQL 的执行情况,同时对一些锁表操作进行优化 - - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SHOW PROCESSLIST命令.png) +**** +#### 使用提示 -*** +SQL 提示,是优化数据库的一个重要手段,就是在 SQL 语句中加入一些提示来达到优化操作的目的 +* USE INDEX:在查询语句中表名的后面添加 USE INDEX 来提供 MySQL 去参考的索引列表,可以让 MySQL 不再考虑其他可用的索引 + ```mysql + CREATE INDEX idx_seller_name ON tb_seller(name); + EXPLAIN SELECT * FROM tb_seller USE INDEX(idx_seller_name) WHERE name='小米科技'; + ``` -#### EXPLAIN + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用提示1.png) -##### 执行计划 +* IGNORE INDEX:让 MySQL 忽略一个或者多个索引,则可以使用 IGNORE INDEX 作为提示 -通过 EXPLAIN 命令获取执行 SQL 语句的信息,包括在 SELECT 语句执行过程中如何连接和连接的顺序,执行计划在优化器优化完成后、执行器之前生成,然后执行器会调用存储引擎检索数据 + ```mysql + EXPLAIN SELECT * FROM tb_seller IGNORE INDEX(idx_seller_name) WHERE name = '小米科技'; + ``` -查询 SQL 语句的执行计划: + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用提示2.png) -```mysql -EXPLAIN SELECT * FROM table_1 WHERE id = 1; -``` +* FORCE INDEX:强制 MySQL 使用一个特定的索引 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-explain查询SQL语句的执行计划.png) + ```mysql + EXPLAIN SELECT * FROM tb_seller FORCE INDEX(idx_seller_name_sta_addr) WHERE NAME='小米科技'; + ``` -| 字段 | 含义 | -| ------------- | ------------------------------------------------------------ | -| id | SELECT 的序列号 | -| select_type | 表示 SELECT 的类型 | -| table | 访问数据库中表名称,有时可能是简称或者临时表名称() | -| type | 表示表的连接类型 | -| possible_keys | 表示查询时,可能使用的索引 | -| key | 表示实际使用的索引 | -| key_len | 索引字段的长度 | -| ref | 表示与索引列进行等值匹配的对象,常数、某个列、函数等,type 必须在(range, const] 之间,左闭右开 | -| rows | 扫描出的行数,表示 MySQL 根据表统计信息及索引选用情况,**估算**的找到所需的记录扫描的行数 | -| filtered | 条件过滤的行百分比,单表查询没意义,用于连接查询中对驱动表的扇出进行过滤,查询优化器预测所有扇出值满足剩余查询条件的百分比,相乘以后表示多表查询中还要对被驱动执行查询的次数 | -| extra | 执行情况的说明和描述 | + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用提示3.png) -MySQL **执行计划的局限**: -* 只是计划,不是执行 SQL 语句,可以随着底层优化器输入的更改而更改 -* EXPLAIN 不会告诉显示关于触发器、存储过程的信息对查询的影响情况 -* EXPLAIN 不考虑各种 Cache -* EXPLAIN 不能显示 MySQL 在执行查询时的动态,因为执行计划在执行查询之前生成 -* EXPALIN 部分统计信息是估算的,并非精确值 -* EXPALIN 只能解释 SELECT 操作,其他操作要重写为 SELECT 后查看执行计划 -* EXPLAIN PLAN 显示的是在解释语句时数据库将如何运行 SQL 语句,由于执行环境和 EXPLAIN PLAN 环境的不同,此计划可能与 SQL 语句实际的执行计划不同 -SHOW WARINGS:在使用 EXPALIN 命令后执行该语句,可以查询与执行计划相关的拓展信息,展示出 Level、Code、Message 三个字段,当 Code 为 1003 时,Message 字段展示的信息类似于将查询语句重写后的信息,但是不是等价,不能执行复制过来运行 -环境准备: -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-执行计划环境准备.png) +*** +#### 统计计数 +在不同的 MySQL 引擎中,count(*) 有不同的实现方式: -*** +* MyISAM 引擎把一个表的总行数存在了磁盘上,因此执行 count(*) 的时候会直接返回这个数,效率很高,但不支持事务 +* show table status 命令通过采样估算可以快速获取,但是不准确 +* InnoDB 表执行 count(*) 会遍历全表,虽然结果准确,但会导致性能问题 +解决方案: +* 计数保存在 Redis 中,但是更新 MySQL 和 Redis 的操作不是原子的,会存在数据一致性的问题 -##### id +* 计数直接放到数据库里单独的一张计数表中,利用事务解决计数精确问题: -id 代表 SQL 执行的顺序的标识,每个 SELECT 关键字对应一个唯一 id,所以在同一个 SELECT 关键字中的表的 id 都是相同的。SELECT 后的 FROM 可以跟随多个表,每个表都会对应一条记录,这些记录的 id 都是相同的, + -* id 相同时,执行顺序由上至下。连接查询的执行计划,记录的 id 值都是相同的,出现在前面的表为驱动表,后面为被驱动表 + 会话 B 的读操作在 T3 执行的,这时更新事务还没有提交,所以计数值加 1 这个操作对会话 B 还不可见,因此会话 B 查询的计数值和最近 100 条记录,返回的结果逻辑上就是一致的 - ```mysql - EXPLAIN SELECT * FROM t_role r, t_user u, user_role ur WHERE r.id = ur.role_id AND u.id = ur.user_id ; - ``` + 并发系统性能的角度考虑,应该先插入操作记录再更新计数表,因为更新计数表涉及到行锁的竞争,**先插入再更新能最大程度地减少事务之间的锁等待,提升并发度** - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-explain之id相同.png) +count 函数的按照效率排序:`count(字段) < count(主键id) < count(1) ≈ count(*)`,所以建议尽量使用 count(*) -* id 不同时,id 值越大优先级越高,越先被执行 +* count(主键 id):InnoDB 引擎会遍历整张表,把每一行的 id 值都取出来返回给 Server 层,Server 判断 id 不为空就按行累加 +* count(1):InnoDB 引擎遍历整张表但不取值,Server 层对于返回的每一行,放一个数字 1 进去,判断不为空就按行累加 - ```mysql - EXPLAIN SELECT * FROM t_role WHERE id = (SELECT role_id FROM user_role WHERE user_id = (SELECT id FROM t_user WHERE username = 'stu1')) - ``` +* count(字段):如果这个字段是定义为 not null 的话,一行行地从记录里面读出这个字段,判断不能为 null,按行累加;如果这个字段定义允许为 null,那么执行的时候,判断到有可能是 null,还要把值取出来再判断一下,不是 null 才累加 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-explain之id不同.png) -* id 有相同也有不同时,id 相同的可以认为是一组,从上往下顺序执行;在所有的组中,id 的值越大的组,优先级越高,越先执行 - ```mysql - EXPLAIN SELECT * FROM t_role r , (SELECT * FROM user_role ur WHERE ur.`user_id` = '2') a WHERE r.id = a.role_id ; - ``` +参考文章:https://time.geekbang.org/column/article/72775 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-explain之id相同和不同.png) -* id 为 NULL 时代表的是临时表 @@ -4986,101 +5233,82 @@ id 代表 SQL 执行的顺序的标识,每个 SELECT 关键字对应一个唯 -##### select +### 内存优化 -表示查询中每个 select 子句的类型(简单 OR 复杂) +#### 优化原则 -| select_type | 含义 | -| ------------------ | ------------------------------------------------------------ | -| SIMPLE | 简单的 SELECT 查询,查询中不包含子查询或者 UNION | -| PRIMARY | 查询中若包含任何复杂的子查询,最外层(也就是最左侧)查询标记为该标识 | -| UNION | 对于 UNION 或者 UNION ALL 的复杂查询,除了最左侧的查询,其余的小查询都是 UNION | -| UNION RESULT | UNION 需要使用临时表进行去重,临时表的是 UNION RESULT | -| DEPENDENT UNION | 对于 UNION 或者 UNION ALL 的复杂查询,如果各个小查询都依赖外层查询,是相关子查询,除了最左侧的小查询为 DEPENDENT SUBQUERY,其余都是 DEPENDENT UNION | -| SUBQUERY | 子查询不是相关子查询,该子查询第一个 SELECT 代表的查询就是这种类型,会进行物化(该子查询只需要执行一次) | -| DEPENDENT SUBQUERY | 子查询是相关子查询,该子查询第一个 SELECT 代表的查询就是这种类型,不会物化(该子查询需要执行多次) | -| DERIVED | 在 FROM 列表中包含的子查询,被标记为 DERIVED(衍生),也就是生成物化派生表的这个子查询 | -| MATERIALIZED | 将子查询物化后与与外层进行连接查询,生成物化表的子查询 | +三个原则: -子查询为 DERIVED:`SELECT * FROM (SELECT key1 FROM t1) AS derived_1 WHERE key1 > 10` +* 将尽量多的内存分配给 MySQL 做缓存,但也要给操作系统和其他程序预留足够内存 +* MyISAM 存储引擎的数据文件读取依赖于操作系统自身的 IO 缓存,如果有 MyISAM 表,就要预留更多的内存给操作系统做 IO 缓存 +* 排序区、连接区等缓存是分配给每个数据库会话(Session)专用的,值的设置要根据最大连接数合理分配,如果设置太大,不但浪费资源,而且在并发数较高时会导致物理内存耗尽 -子查询为 MATERIALIZED:`SELECT * FROM t1 WHERE key1 IN (SELECT key1 FROM t2)` +MyISAM 存储引擎使用 key_buffer 缓存索引块,加速 MyISAM 索引的读写速度。对于 MyISAM 表的数据块没有特别的缓存机制,完全依赖于操作系统的 IO 缓存 +* key_buffer_size:该变量决定 MyISAM 索引块缓存区的大小,直接影响到 MyISAM 表的存取效率 + + ```mysql + SHOW VARIABLES LIKE 'key_buffer_size'; -- 单位是字节 + ``` + 在 MySQL 配置文件中设置该值,建议至少将1/4可用内存分配给 key_buffer_size: -**** + ```sh + vim /etc/mysql/my.cnf + key_buffer_size=1024M + ``` +* read_buffer_size:如果需要经常顺序扫描 MyISAM 表,可以通过增大 read_buffer_size 的值来改善性能。但 read_buffer_size 是每个 Session 独占的,如果默认值设置太大,并发环境就会造成内存浪费 +* read_rnd_buffer_size:对于需要做排序的 MyISAM 表的查询,如带有 ORDER BY 子句的语句,适当增加该的值,可以改善此类的 SQL 的性能,但是 read_rnd_buffer_size 是每个 Session 独占的,如果默认值设置太大,就会造成内存浪费 -##### type -对表的访问方式,表示 MySQL 在表中找到所需行的方式,又称访问类型 -| type | 含义 | -| --------------- | ------------------------------------------------------------ | -| ALL | 全表扫描,如果是 InnoDB 引擎是扫描聚簇索引 | -| index | 可以使用覆盖索引,但需要扫描全部索引 | -| range | 索引范围扫描,常见于 between、<、> 等的查询 | -| index_subquery | 子查询可以普通索引,则子查询的 type 为 index_subquery | -| unique_subquery | 子查询可以使用主键或唯一二级索引,则子查询的 type 为 index_subquery | -| index_merge | 索引合并 | -| ref_or_null | 非唯一性索引(普通二级索引)并且可以存储 NULL,进行等值匹配 | -| ref | 非唯一性索引与常量等值匹配 | -| eq_ref | 唯一性索引(主键或不存储 NULL 的唯一二级索引)进行等值匹配,如果二级索引是联合索引,那么所有联合的列都要进行等值匹配 | -| const | 通过主键或者唯一二级索引与常量进行等值匹配 | -| system | system 是 const 类型的特例,当查询的表只有一条记录的情况下,使用 system | -| NULL | MySQL 在优化过程中分解语句,执行时甚至不用访问表或索引 | +*** -从上到下,性能从差到好,一般来说需要保证查询至少达到 range 级别, 最好达到 ref +#### 缓冲内存 -*** +Buffer Pool 本质上是 InnoDB 向操作系统申请的一段连续的内存空间。InnoDB 的数据是按数据页为单位来读写,每个数据页的大小默认是 16KB。数据是存放在磁盘中,每次读写数据都需要进行磁盘 IO 将数据读入内存进行操作,效率会很低,所以提供了 Buffer Pool 来暂存这些数据页,缓存中的这些页又叫缓冲页 +工作流程: +* 从数据库读取数据时,会首先从缓存中读取,如果缓存中没有,则从磁盘读取后放入 Buffer Pool +* 向数据库写入数据时,会首先写入缓存,缓存中修改的数据会**定期刷新**到磁盘,这一过程称为刷脏 -##### key +Buffer Pool 中每个缓冲页都有对应的控制信息,包括表空间编号、页号、偏移量、链表信息等,控制信息存放在占用的内存称为控制块,控制块与缓冲页是一一对应的,但并不是物理上相连的,都在缓冲池中 -possible_keys: +MySQL 提供了缓冲页的快速查找方式:**哈希表**,使用表空间号和页号作为 Key,缓冲页控制块的地址作为 Value 创建一个哈希表,获取数据页时根据 Key 进行哈希寻址: -* 指出 MySQL 能使用哪个索引在表中找到记录,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用 -* 如果该列是 NULL,则没有相关的索引 +* 如果不存在对应的缓存页,就从 free 链表中选一个空闲缓冲页,把磁盘中的对应页加载到该位置 +* 如果存在对应的缓存页,直接获取使用 -key: -* 显示 MySQL 在查询中实际使用的索引,若没有使用索引,显示为 NULL -* 查询中若使用了**覆盖索引**,则该索引可能出现在 key 列表,不出现在 possible_keys -key_len: +*** -* 表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度 -* key_len 显示的值为索引字段的最大可能长度,并非实际使用长度,即 key_len 是根据表定义计算而得,不是通过表内检索出的 -* 在不损失精确性的前提下,长度越短越好 +#### 内存管理 -*** +##### Free 链表 +MySQL 启动时完成对 Buffer Pool 的初始化,先向操作系统申请连续的内存空间,然后将内存划分为若干对控制块和缓冲页。为了区分空闲和已占用的数据页,将所有缓冲页对应的控制块作为一个节点放入一个链表中,就是 Free 链表(**空闲链表**) + -##### Extra +基节点:是一块单独申请的内存空间(占 40 字节),并不在Buffer Pool的那一大片连续内存空间里 -其他的额外的执行计划信息,在该列展示: +磁盘加载页的流程: -* No tables used:查询语句中使用 FROM dual 或者没有 FROM 语句 -* Impossible WHERE:查询语句中的 WHERE 子句条件永远为 FALSE,会导致没有符合条件的行 -* Using index:该值表示相应的 SELECT 操作中使用了**覆盖索引**(Covering Index) -* Using index condition:第一种情况是搜索条件中虽然出现了索引列,但是部分条件无法形成扫描区间(**索引失效**),会根据可用索引的条件先搜索一遍再匹配无法使用索引的条件,回表查询数据;第二种是使用了**索引条件下推**优化 -* Using where:搜索条件需要在 Server 层判断,判断后执行回表操作查询,无法使用索引下推 -* Using join buffer:连接查询被驱动表无法利用索引,需要连接缓冲区来存储中间结果 -* Using filesort:无法利用索引完成排序(优化方向),需要对数据使用外部排序算法,将取得的数据在内存或磁盘中进行排序 -* Using temporary:表示 MySQL 需要使用临时表来存储结果集,常见于排序、去重、分组等场景 -* Select tables optimized away:说明仅通过使用索引,优化器可能仅从聚合函数结果中返回一行 -* No tables used:Query 语句中使用 from dual 或不含任何 from 子句 +* 从 Free 链表中取出一个空闲的缓冲页 +* 把缓冲页对应的控制块的信息填上(页所在的表空间、页号之类的信息) +* 把缓冲页对应的 Free 链表节点(控制块)从链表中移除,表示该缓冲页已经被使用 -参考文章:https://www.cnblogs.com/ggjucheng/archive/2012/11/11/2765237.html +参考文章:https://blog.csdn.net/li1325169021/article/details/121124440 @@ -5088,497 +5316,431 @@ key_len: -#### PROFILES +##### Flush 链表 -SHOW PROFILES 能够在做 SQL 优化时分析当前会话中语句执行的**资源消耗**情况 +Flush 链表是一个用来**存储脏页**的链表,对于已经修改过的缓冲脏页,出于性能考虑并不是直接更新到磁盘,而是在未来的某个时间进行刷脏,所以需要暂时存储所有的脏页 -* 通过 have_profiling 参数,能够看到当前 MySQL 是否支持 profile: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-have_profiling.png) + -* 默认 profiling 是关闭的,可以通过 set 语句在 Session 级别开启 profiling: +后台有专门的线程每隔一段时间把脏页刷新到磁盘: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-profiling.png) +* 从 Flush 链表中刷新一部分页面到磁盘: + * 后台线程定时从 Flush 链表刷脏,根据系统的繁忙程度来决定刷新速率,这种方式称为 BUF_FLUSH_LIST + * 线程刷脏的比较慢,导致用户线程加载一个新的数据页时发现没有空闲缓冲页,此时会尝试从 LRU 链表尾部寻找未修改的缓冲页直接释放,如果没有就会将 LRU 链表尾部的一个脏页**同步刷新**到磁盘,速度较慢,这种方式称为 BUF_FLUSH_SINGLE_PAGE +* 从 LRU 链表的冷数据中刷新一部分页面到磁盘,即:BUF_FLUSH_LRU + * 后台线程会定时从 LRU 链表的尾部开始扫描一些页面,扫描的页面数量可以通过系统变量 `innodb_lru_scan_depth` 指定,如果在 LRU 链表中发现脏页,则把它们刷新到磁盘,这种方式称为 BUF_FLUSH_LRU + * 控制块里会存储该缓冲页是否被修改的信息,所以可以很容易的获取到某个缓冲页是否是脏页 - ```mysql - SET profiling=1; #开启profiling 开关; - ``` -* 执行 SHOW PROFILES 指令, 来查看 SQL 语句执行的耗时: - ```mysql - SHOW PROFILES; - ``` +参考文章:https://blog.csdn.net/li1325169021/article/details/121125765 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-查看SQL语句执行耗时.png) -* 查看到该 SQL 执行过程中每个线程的状态和消耗的时间: - ```mysql - SHOW PROFILE FOR QUERY query_id; - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SQL执行每个状态消耗的时间.png) - **Sending data 状态**表示 MySQL 线程开始访问数据行并把结果返回给客户端,而不仅仅是返回给客户端。由于在 Sending data 状态下,MySQL 线程需要做大量磁盘读取操作,所以是整个查询中耗时最长的状态。 +*** -* 在获取到最消耗时间的线程状态后,MySQL 支持选择 all、cpu、block io 、context switch、page faults 等类型查看 MySQL 在使用什么资源上耗费了过高的时间。例如,选择查看 CPU 的耗费时间: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SQL执行每个状态消耗的CPU.png) - * Status:SQL 语句执行的状态 - * Durationsql:执行过程中每一个步骤的耗时 - * CPU_user:当前用户占有的 CPU - * CPU_system:系统占有的 CPU +##### LRU 链表 +当 Buffer Pool 中没有空闲缓冲页时就需要淘汰掉最近最少使用的部分缓冲页,为了实现这个功能,MySQL 创建了一个 LRU 链表,当访问某个页时: +* 如果该页不在 Buffer Pool 中,把该页从磁盘加载进来后会将该缓冲页对应的控制块作为节点放入 **LRU 链表的头部** +* 如果该页在 Buffer Pool 中,则直接把该页对应的控制块移动到 LRU 链表的头部 -*** +这样操作后 LRU 链表的尾部就是最近最少使用的缓冲页 +MySQL 基于局部性原理提供了预读功能: +* 线性预读:系统变量 `innodb_read_ahead_threshold`,如果顺序访问某个区(extent:16 KB 的页,连续 64 个形成一个区,一个区默认 1MB 大小)的页面数超过了该系统变量值,就会触发一次**异步读取**下一个区中全部的页面到 Buffer Pool 中 +* 随机预读:如果某个区 13 个连续的页面都被加载到 Buffer Pool,无论这些页面是否是顺序读取,都会触发一次**异步读取**本区所有的其他页面到 Buffer Pool 中 -#### TRACE +预读会造成加载太多用不到的数据页,造成那些使用频率很高的数据页被挤到 LRU 链表尾部,所以 InnoDB 将 LRU 链表分成两段: -MySQL 提供了对 SQL 的跟踪, 通过 trace 文件可以查看优化器生成执行计划的过程 +* 一部分存储使用频率很高的数据页,这部分链表也叫热数据,young 区 +* 一部分存储使用频率不高的冷数据,old 区,默认占 37%,可以通过系统变量 `innodb_old_blocks_pct` 指定 -* 打开 trace 功能,设置格式为 JSON,并设置 trace 的最大使用内存,避免解析过程中因默认内存过小而不能够完整展示 +当磁盘上的某数据页被初次加载到 Buffer Pool 中会被放入 old 区,淘汰时优先淘汰 old 区 - ```mysql - SET optimizer_trace="enabled=on",end_markers_in_json=ON; -- 会话内有效 - SET optimizer_trace_max_mem_size=1000000; - ``` +* 当对 old 区的数据进行访问时,会在控制块记录下访问时间,等待后续的访问时间与第一次访问的时间是否在某个时间间隔内,通过系统变量 `innodb_old_blocks_time` 指定时间间隔,默认 1000ms,成立就移动到 young 区的链表头部 +* `innodb_old_blocks_time` 为 0 时,每次访问一个页面都会放入 young 区的头部 -* 执行 SQL 语句: - ```mysql - SELECT * FROM tb_item WHERE id < 4; - ``` -* 检查 information_schema.optimizer_trace: - ```mysql - SELECT * FROM information_schema.optimizer_trace \G; -- \G代表竖列展示 - ``` - - 执行信息主要有三个阶段:prepare 阶段、optimize 阶段(成本分析)、execute 阶段(执行) +*** +#### 参数优化 -**** +Innodb 用一块内存区做 IO 缓存池,该缓存池不仅用来缓存 Innodb 的索引块,也用来缓存 Innodb 的数据块,可以通过下面的指令查看 Buffer Pool 的状态信息: +```mysql +SHOW ENGINE INNODB STATUS\G +``` +核心参数: -### 索引失效 +* `innodb_buffer_pool_size`:该变量决定了 Innodb 存储引擎表数据和索引数据的最大缓存区大小,默认 128M -#### 创建索引 + ```mysql + SHOW VARIABLES LIKE 'innodb_buffer_pool_size'; + ``` -索引是数据库优化最重要的手段之一,通过索引通常可以帮助用户解决大多数的 MySQL 的性能优化问题 + 在保证操作系统及其他程序有足够内存可用的情况下,`innodb_buffer_pool_size` 的值越大,缓存命中率越高 -```mysql -CREATE TABLE `tb_seller` ( - `sellerid` varchar (100), - `name` varchar (100), - `nickname` varchar (50), - `password` varchar (60), - `status` varchar (1), - `address` varchar (100), - `createtime` datetime, - PRIMARY KEY(`sellerid`) -)ENGINE=INNODB DEFAULT CHARSET=utf8mb4; -INSERT INTO `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('xiaomi','小米科技','小米官方旗舰店','e10adc3949ba59abbe56e057f20f883e','1','西安市','2088-01-01 12:00:00'); -CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); -``` + ```sh + innodb_buffer_pool_size=512M + ``` -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引环境准备.png) +* `innodb_log_buffer_size`:该值决定了 Innodb 日志缓冲区的大小,保存要写入磁盘上的日志文件数据 + 对于可能产生大量更新记录的大事务,增加该值的大小,可以避免 Innodb 在事务提交前就执行不必要的日志写入磁盘操作,影响执行效率,通过配置文件修改: + ```sh + innodb_log_buffer_size=10M + ``` -**** +在多线程下,访问 Buffer Pool 中的各种链表都需要加锁,所以将 Buffer Pool 拆成若干个小实例,每个实例独立管理内存空间和各种链表(类似 ThreadLocal),多线程访问各实例互不影响,提高了并发能力 +* 在系统启动时设置系统变量 `innodb_buffer_pool_instance` 可以指定 Buffer Pool 实例的个数,但是当 Buffer Pool 小于 1GB 时,设置多个实例时无效的 +MySQL 5.7.5 之前 `innodb_buffer_pool_size` 只支持在系统启动时修改,现在已经支持运行时修改 Buffer Pool 的大小,但是每次调整参数都会重新向操作系统申请一块连续的内存空间,将旧的缓冲池的内容拷贝到新空间非常耗时,所以 MySQL 开始以一个 chunk 为单位向操作系统申请内存,所以一个 Buffer Pool 实例由多个 chunk 组成 -#### 避免失效 +* 指定系统变量 `innodb_buffer_pool_chunk_size` 来改变 chunk 的大小,只能在启动时修改,运行中不能修改,而且该变量并不包含缓冲页的控制块的内存大小 +* `innodb_buffer_pool_size` 必须是 `innodb_buffer_pool_chunk_size × innodb_buffer_pool_instance` 的倍数,默认值是 `128M × 16 = 2G`,Buffer Pool 必须是 2G 的整数倍,如果指定 5G,会自动调整成 6G -##### 语句错误 +* 如果启动时 `chunk × instances` > `pool_size`,那么 chunk 的值会自动设置为 `pool_size ÷ instances` -* 全值匹配:对索引中所有列都指定具体值,这种情况索引生效,执行效率高 - ```mysql - EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status='1' AND address='西安市'; - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引1.png) -* **最左前缀法则**:联合索引遵守最左前缀法则 - 匹配最左前缀法则,走索引: +*** - ```mysql - EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技'; - EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status='1'; - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引2.png) - 违法最左前缀法则 , 索引失效: +### 并发优化 - ```mysql - EXPLAIN SELECT * FROM tb_seller WHERE status='1'; - EXPLAIN SELECT * FROM tb_seller WHERE status='1' AND address='西安市'; - ``` +MySQL Server 是多线程结构,包括后台线程和客户服务线程。多线程可以有效利用服务器资源,提高数据库的并发性能。在 MySQL 中,控制并发连接和线程的主要参数: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引3.png) +* max_connections:控制允许连接到 MySQL 数据库的最大连接数,默认值是 151 - 如果符合最左法则,但是出现跳跃某一列,只有最左列索引生效: + 如果状态变量 connection_errors_max_connections 不为零,并且一直增长,则说明不断有连接请求因数据库连接数已达到允许最大值而失败,这时可以考虑增大max_connections 的值 - ```mysql - EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND address='西安市'; - ``` + Mysql 最大可支持的连接数取决于很多因素,包括操作系统平台的线程库的质量、内存大小、每个连接的负荷、CPU的处理速度、期望的响应时间等。在 Linux 平台下,性能好的服务器,可以支持 500-1000 个连接,需要根据服务器性能进行评估设定 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引4.png) +* back_log:控制 MySQL 监听 TCP 端口时的积压请求栈的大小 - 虽然索引列失效,但是系统**使用了索引下推进行了优化** + 如果 Mysql 的连接数达到 max_connections 时,新来的请求将会被存在堆栈中,以等待某一连接释放资源,该堆栈的数量即 back_log。如果等待连接的数量超过 back_log,将不被授予连接资源直接报错 -* **范围查询**右边的列,不能使用索引: + 5.6.6 版本之前默认值为 50,之后的版本默认为 `50 + (max_connections/5)`,但最大不超过900,如果需要数据库在较短的时间内处理大量连接请求, 可以考虑适当增大 back_log 的值 - ```mysql - EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status>'1' AND address='西安市'; - ``` +* table_open_cache:控制所有 SQL 语句执行线程可打开表缓存的数量 - 根据前面的两个字段 name , status 查询是走索引的, 但是最后一个条件 address 没有用到索引,使用了索引下推 + 在执行 SQL 语句时,每个执行线程至少要打开1个表缓存,该参数的值应该根据设置的最大连接数以及每个连接执行关联查询中涉及的表的最大数量来设定:`max_connections * N` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引5.png) +* thread_cache_size:可控制 MySQL 缓存客户服务线程的数量 -* 在索引列上进行**运算操作**, 索引将失效: + 为了加快连接数据库的速度,MySQL 会缓存一定数量的客户服务线程以备重用,池化思想 - ```mysql - EXPLAIN SELECT * FROM tb_seller WHERE SUBSTRING(name,3,2) = '科技'; - ``` +* innodb_lock_wait_timeout:设置 InnoDB 事务等待行锁的时间,默认值是 50ms - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引6.png) + 对于需要快速反馈的业务系统,可以将行锁的等待时间调小,以避免事务被长时间挂起; 对于后台运行的批量处理程序来说,可以将行锁的等待时间调大,以避免发生大的回滚操作 -* **字符串不加单引号**,造成索引失效: - 在查询时,没有对字符串加单引号,MySQL 的查询优化器,会自动的进行类型转换,造成索引失效 - ```mysql - EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status=1; - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引7.png) -* **用 OR 分割条件,索引失效**,导致全表查询: - OR 前的条件中的列有索引而后面的列中没有索引或 OR 前后两个列是同一个复合索引,都造成索引失效 - ```mysql - EXPLAIN SELECT * FROM tb_seller WHERE name='阿里巴巴' OR createtime = '2088-01-01 12:00:00'; - EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' OR status='1'; - ``` +*** - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引10.png) - **AND 分割的条件不影响**: - ```mysql - EXPLAIN SELECT * FROM tb_seller WHERE name='阿里巴巴' AND createtime = '2088-01-01 12:00:00'; - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引11.png) -* **以 % 开头的 LIKE 模糊查询**,索引失效: +## 事务机制 - 如果是尾部模糊匹配,索引不会失效;如果是头部模糊匹配,索引失效。 +### 管理事务 - ```mysql - EXPLAIN SELECT * FROM tb_seller WHERE name like '%科技%'; - ``` +事务(Transaction)是访问和更新数据库的程序执行单元;事务中可能包含一个或多个 sql 语句,这些语句要么都执行,要么都不执行。作为一个关系型数据库,MySQL 支持事务。 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引12.png) +事务的四大特征:ACID - 解决方案:通过覆盖索引来解决 +- 原子性 (atomicity) +- 一致性 (consistency) +- 隔离性 (isolaction) +- 持久性 (durability) - ```mysql - EXPLAIN SELECT sellerid,name,status FROM tb_seller WHERE name like '%科技%'; - ``` +单元中的每条 SQL 语句都相互依赖,形成一个整体 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引13.png) +* 如果某条 SQL 语句执行失败或者出现错误,那么整个单元就会回滚,撤回到事务最初的状态 - 原因:在覆盖索引的这棵 B+ 数上只需要进行 like 的匹配,或者是基于覆盖索引查询再进行 WHERE 的判断就可以获得结果 +* 如果单元中所有的 SQL 语句都执行成功,则事务就顺利执行 +管理事务的三个步骤 +1. 开启事务:记录回滚点,并通知服务器,将要执行一组操作,要么同时成功、要么同时失败 -*** +2. 执行 SQL 语句:执行具体的一条或多条 SQL 语句 +3. 结束事务(提交|回滚) + - 提交:没出现问题,数据进行更新 + - 回滚:出现问题,数据恢复到开启事务时的状态 -##### 系统优化 -系统优化为全表扫描: +事务操作: -* 如果 MySQL 评估使用索引比全表更慢,则不使用索引,索引失效: +* 开启事务 ```mysql - CREATE INDEX idx_address ON tb_seller(address); - EXPLAIN SELECT * FROM tb_seller WHERE address='西安市'; - EXPLAIN SELECT * FROM tb_seller WHERE address='北京市'; + START TRANSACTION; ``` - 北京市的键值占 9/10(区分度低),所以优化为全表扫描,type = ALL +* 回滚事务 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引14.png) + ```mysql + ROLLBACK; + ``` -* IS NULL、IS NOT NULL **有时**索引失效: +* 提交事务,显示执行是手动提交,MySQL 默认为自动提交 ```mysql - EXPLAIN SELECT * FROM tb_seller WHERE name IS NULL; - EXPLAIN SELECT * FROM tb_seller WHERE name IS NOT NULL; + COMMIT; ``` - NOT NULL 失效的原因是 name 列全部不是 null,优化为全表扫描,当 NULL 过多时,IS NULL 失效 + 工作原理: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引15.png) + * 自动提交模式下,如果没有 start transaction 显式地开始一个事务,那么**每个 SQL 语句都会被当做一个事务执行提交操作** + * 手动提交模式下,所有的 SQL 语句都在一个事务中,直到执行了 commit 或 rollback -* IN 肯定会走索引,但是当 IN 的取值范围较大时会导致索引失效,走全表扫描: + * 存在一些特殊的命令,在事务中执行了这些命令会马上强制执行 COMMIT 提交事务,如 DDL 语句 (create/drop/alter/table)、lock tables 语句等 - ```mysql - EXPLAIN SELECT * FROM tb_seller WHERE sellerId IN ('alibaba','huawei');-- 都走索引 - EXPLAIN SELECT * FROM tb_seller WHERE sellerId NOT IN ('alibaba','huawei'); - ``` + 提交方式语法: + - 查看事务提交方式 + ```mysql + SELECT @@AUTOCOMMIT; -- 1 代表自动提交 0 代表手动提交 + ``` -*** + - 修改事务提交方式 + ```mysql + SET @@AUTOCOMMIT=数字; -- 系统 + SET AUTOCOMMIT=数字; -- 会话 + ``` + - **系统变量的操作**: -#### 底层原理 + ```sql + SET [GLOBAL|SESSION] 变量名 = 值; -- 默认是会话 + SET @@[(GLOBAL|SESSION).]变量名 = 值; -- 默认是系统 + ``` -索引失效一般是针对联合索引,联合索引一般由几个字段组成,排序方式是先按照第一个字段进行排序,然后排序第二个,依此类推,图示(a, b)索引,**a 相等的情况下 b 是有序的** + ```sql + SHOW [GLOBAL|SESSION] VARIABLES [LIKE '变量%']; -- 默认查看会话内系统变量值 + ``` - +* 操作演示 -* 最左前缀法则:当不匹配前面的字段的时候,后面的字段都是无序的。这种无序不仅体现在叶子节点,也会**导致查询时扫描的非叶子节点也是无序的**,因为索引树相当于忽略的第一个字段,就无法使用二分查找 + ```mysql + -- 开启事务 + START TRANSACTION; + + -- 张三给李四转账500元 + -- 1.张三账户-500 + UPDATE account SET money=money-500 WHERE NAME='张三'; + -- 2.李四账户+500 + UPDATE account SET money=money+500 WHERE NAME='李四'; + + -- 回滚事务(出现问题) + ROLLBACK; + + -- 提交事务(没出现问题) + COMMIT; + ``` -* 范围查询右边的列,不能使用索引,比如语句: `WHERE a > 1 AND b = 1 `,在 a 大于 1 的时候,b 是无序的,a > 1 是扫描时有序的,但是找到以后进行寻找 b 时,索引树就不是有序的了 - -* 以 % 开头的 LIKE 模糊查询,索引失效,比如语句:`WHERE a LIKE '%d'`,前面的不确定,导致不符合最左匹配,直接去索引中搜索以 d 结尾的节点,所以没有顺序 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-索引失效底层原理3.png) +*** -*** +### 隔离级别 +事务的隔离级别:多个客户端操作时,各个客户端的事务之间应该是隔离的,**不同的事务之间不该互相影响**,而如果多个事务操作同一批数据时,则需要设置不同的隔离级别,否则就会产生问题。 -#### 查看索引 +隔离级别分类: -```mysql -SHOW STATUS LIKE 'Handler_read%'; -SHOW GLOBAL STATUS LIKE 'Handler_read%'; -``` +| 隔离级别 | 名称 | 会引发的问题 | 数据库默认隔离级别 | +| ---------------- | -------- | -------------------------------- | ------------------- | +| read uncommitted | 读未提交 | 脏读、不可重复读、幻读 | | +| read committed | 读已提交 | 不可重复读、幻读 | Oracle / SQL Server | +| repeatable read | 可重复读 | 幻读 | MySQL | +| serializable | 串行化 | 无(因为写会加写锁,读会加读锁) | | -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL查看索引使用情况.png) +一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差 -* Handler_read_first:索引中第一条被读的次数,如果较高,表示服务器正执行大量全索引扫描(这个值越低越好) +* 丢失更新 (Lost Update):当两个或多个事务选择同一行,最初的事务修改的值,被后面事务修改的值覆盖,所有的隔离级别都可以避免丢失更新(行锁) -* Handler_read_key:如果索引正在工作,这个值代表一个行被索引值读的次数,值越低表示索引不经常使用(这个值越高越好) +* 脏读 (Dirty Reads):在事务中先后两次读取同一个数据,两次读取的结果不一样,原因是在一个事务处理过程中读取了另一个**未提交**的事务中的数据 -* Handler_read_next:按照键顺序读下一行的请求数,如果范围约束或执行索引扫描来查询索引列,值增加 +* 不可重复读 (Non-Repeatable Reads):在事务中先后两次读取同一个数据,两次读取的结果不一样,原因是在一个事务处理过程中读取了另一个事务中修改并**已提交**的数据 -* Handler_read_prev:按照键顺序读前一行的请求数,该读方法主要用于优化 ORDER BY ... DESC + > 可重复读的意思是不管读几次,结果都一样,可以重复的读,可以理解为快照读,要读的数据集不会发生变化 -* Handler_read_rnd:根据固定位置读一行的请求数,如果执行大量查询并对结果进行排序则该值较高,可能是使用了大量需要 MySQL 扫描整个表的查询或连接,这个值较高意味着运行效率低,应该建立索引来解决 +* 幻读 (Phantom Reads):在事务中按某个条件先后两次查询数据库,后一次查询查到了前一次查询没有查到的行,**数据条目**发生了变化。比如查询某数据不存在,准备插入此记录,但执行插入时发现此记录已存在,无法插入 -* Handler_read_rnd_next:在数据文件中读下一行的请求数,如果正进行大量的表扫描,该值较高,说明表索引不正确或写入的查询没有利用索引 +**隔离级别操作语法:** +* 查询数据库隔离级别 + ```mysql + SELECT @@TX_ISOLATION; + SHOW VARIABLES LIKE 'tx_isolation'; + ``` -*** +* 修改数据库隔离级别 + ```mysql + SET GLOBAL TRANSACTION ISOLATION LEVEL 级别字符串; + ``` -### SQL优化 -#### 覆盖索引 -复合索引叶子节点不仅保存了复合索引的值,还有主键索引,所以使用覆盖索引的时候,加上主键也会用到索引 -尽量使用覆盖索引,避免 SELECT *: +*** -```mysql -EXPLAIN SELECT name,status,address FROM tb_seller WHERE name='小米科技' AND status='1' AND address='西安市'; -``` -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引8.png) -如果查询列,超出索引列,也会降低性能: +### 原子特性 -```mysql -EXPLAIN SELECT name,status,address,password FROM tb_seller WHERE name='小米科技' AND status='1' AND address='西安市'; -``` +原子性是指事务是一个不可分割的工作单位,事务的操作如果成功就必须要完全应用到数据库,失败则不能对数据库有任何影响。比如事务中一个 SQL 语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引9.png) +InnoDB 存储引擎提供了两种事务日志:redo log(重做日志)和 undo log(回滚日志) +* redo log 用于保证事务持久性 +* undo log 用于保证事务原子性和隔离性 +undo log 属于逻辑日志,根据每行操作进行记录,记录了 SQL 执行相关的信息,用来回滚行记录到某个版本 -**** +当事务对数据库进行修改时,InnoDB 会先记录对应的 undo log,如果事务执行失败或调用了 rollback 导致事务回滚,InnoDB 会根据 undo log 的内容**做与之前相反的操作**: +* 对于每个 insert,回滚时会执行 delete +* 对于每个 delete,回滚时会执行 insert -#### 减少访问 +* 对于每个 update,回滚时会执行一个相反的 update,把数据修改回去 -避免对数据进行重复检索:能够一次连接就获取到结果的,就不用两次连接,这样可以大大减少对数据库无用的重复请求 +undo log 是采用段(segment)的方式来记录,每个 undo 操作在记录的时候占用一个 undo log segment + +rollback segment 称为回滚段,每个回滚段中有 1024 个 undo log segment + +* 在以前老版本,只支持 1 个 rollback segment,只能记录 1024 个 undo log segment +* MySQL5.5 开始支持 128 个 rollback segment,支持 128*1024 个 undo 操作 + + + +参考文章:https://www.cnblogs.com/kismetv/p/10331633.html -* 查询数据: - ```mysql - SELECT id,name FROM tb_book; - SELECT id,status FROM tb_book; -- 向数据库提交两次请求,数据库就要做两次查询操作 - -- > 优化为: - SELECT id,name,statu FROM tb_book; - ``` -* 插入数据: - ```mysql - INSERT INTO tb_test VALUES(1,'Tom'); - INSERT INTO tb_test VALUES(2,'Cat'); - INSERT INTO tb_test VALUES(3,'Jerry'); -- 连接三次数据库 - -- >优化为 - INSERT INTO tb_test VALUES(1,'Tom'),(2,'Cat'),(3,'Jerry'); -- 连接一次 - ``` - -* 在事务中进行数据插入: - ```mysql - start transaction; - INSERT INTO tb_test VALUES(1,'Tom'); - INSERT INTO tb_test VALUES(2,'Cat'); - INSERT INTO tb_test VALUES(3,'Jerry'); - commit; -- 手动提交,分段提交 - ``` +*** -* 数据有序插入: - ```mysql - INSERT INTO tb_test VALUES(1,'Tom'); - INSERT INTO tb_test VALUES(2,'Cat'); - INSERT INTO tb_test VALUES(3,'Jerry'); - ``` -增加 cache 层:在应用中增加缓存层来达到减轻数据库负担的目的。可以部分数据从数据库中抽取出来放到应用端以文本方式存储,或者使用框架(Mybatis)提供的一级缓存 / 二级缓存,或者使用 Redis 数据库来缓存数据 +### 隔离特性 +#### 实现方式 +隔离性是指,事务内部的操作与其他事务是隔离的,多个并发事务之间要相互隔离,不能互相干扰 -*** +* 严格的隔离性,对应了事务隔离级别中的 serializable,实际应用中对性能考虑很少使用可串行化 +* 与原子性、持久性侧重于研究事务本身不同,隔离性研究的是**不同事务**之间的相互影响 +隔离性让并发情形下的事务之间互不干扰: -#### 数据插入 +- 一个事务的写操作对另一个事务的写操作(写写):锁机制保证隔离性 +- 一个事务的写操作对另一个事务的读操作(读写):MVCC 保证隔离性 -当使用 load 命令导入数据的时候,适当的设置可以提高导入的效率: +锁机制:事务在修改数据之前,需要先获得相应的锁,获得锁之后,事务便可以修改数据;该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁(详解见锁机制) -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL load data.png) -```mysql -LOAD DATA LOCAL INFILE = '/home/seazean/sql1.log' INTO TABLE `tb_user_1` FIELD TERMINATED BY ',' LINES TERMINATED BY '\n'; -- 文件格式如上图 -``` -对于 InnoDB 类型的表,有以下几种方式可以提高导入的效率: +*** -1. **主键顺序插入**:因为 InnoDB 类型的表是按照主键的顺序保存的,所以将导入的数据按照主键的顺序排列,可以有效的提高导入数据的效率,如果 InnoDB 表没有主键,那么系统会自动默认创建一个内部列作为主键。 - **主键是否连续对性能影响不大,只要是递增的就可以**,比如雪花算法产生的 ID 不是连续的,但是是递增的 - * 插入 ID 顺序排列数据: +#### 并发控制 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL插入ID顺序排列数据.png) +MVCC 全称 Multi-Version Concurrency Control,即多版本并发控制,用来**解决读写冲突的无锁并发控制** - * 插入 ID 无序排列数据: +MVCC 处理读写请求,可以做到在发生读写请求冲突时不用加锁,这个读是指的快照读,而不是当前读 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL插入ID无序排列数据.png) +* 快照读:实现基于 MVCC,因为是多版本并发,所以快照读读到的数据不一定是当前最新的数据,有可能是历史版本的数据 +* 当前读:读取数据库记录是当前最新的版本(产生幻读、不可重复读),可以对读取的数据进行加锁,防止其他事务修改数据,是悲观锁的一种操作,读写操作加共享锁或者排他锁和串行化事务的隔离级别都是当前读 -2. **关闭唯一性校验**:在导入数据前执行 `SET UNIQUE_CHECKS=0`,关闭唯一性校验;导入结束后执行 `SET UNIQUE_CHECKS=1`,恢复唯一性校验,可以提高导入的效率。 +数据库并发场景: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL插入数据关闭唯一性校验.png) +* 读-读:不存在任何问题,也不需要并发控制 -3. **手动提交事务**:如果应用使用自动提交的方式,建议在导入前执行`SET AUTOCOMMIT=0`,关闭自动提交;导入结束后再打开自动提交,可以提高导入的效率。 +* 读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读 - 事务需要控制大小,事务太大可能会影响执行的效率。MySQL 有 innodb_log_buffer_size 配置项,超过这个值的日志会写入磁盘数据,效率会下降,所以在事务大小达到配置项数据级前进行事务提交可以提高效率 +* 写-写:有线程安全问题,可能会存在丢失更新问题 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL插入数据手动提交事务.png) +MVCC 的优点: +* 在并发读写数据库时,做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了并发读写的性能 +* 可以解决脏读,不可重复读等事务隔离问题(加锁也能解决),但不能解决更新丢失问题 +提高读写和写写的并发性能: -**** +* MVCC + 悲观锁:MVCC 解决读写冲突,悲观锁解决写写冲突 +* MVCC + 乐观锁:MVCC 解决读写冲突,乐观锁解决写写冲突 -#### ORDER BY +参考文章:https://www.jianshu.com/p/8845ddca3b23 -数据准备: -```mysql -CREATE TABLE `emp` ( - `id` INT(11) NOT NULL AUTO_INCREMENT, - `name` VARCHAR(100) NOT NULL, - `age` INT(3) NOT NULL, - `salary` INT(11) DEFAULT NULL, - PRIMARY KEY (`id`) -) ENGINE=INNODB DEFAULT CHARSET=utf8mb4; -INSERT INTO `emp` (`id`, `name`, `age`, `salary`) VALUES('1','Tom','25','2300');-- ... -CREATE INDEX idx_emp_age_salary ON emp(age,salary); -``` -* 第一种是通过对返回数据进行排序,所有不通过索引直接返回结果的排序都叫 FileSort 排序,会在内存中重新排序 +*** - ```mysql - EXPLAIN SELECT * FROM emp ORDER BY age DESC; -- 年龄降序 - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL ORDER BY排序1.png) -* 第二种通过有序索引顺序扫描直接返回**有序数据**,这种情况为 Using index,不需要额外排序,操作效率高 +#### 实现原理 - ```mysql - EXPLAIN SELECT id, age, salary FROM emp ORDER BY age DESC; - ``` +##### 隐藏字段 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL ORDER BY排序2.png) +实现原理主要是隐藏字段,undo日志,Read View 来实现的 -* 多字段排序: +数据库中的每行数据,除了自定义的字段,还有数据库隐式定义的字段: - ```mysql - EXPLAIN SELECT id,age,salary FROM emp ORDER BY age DESC, salary DESC; - EXPLAIN SELECT id,age,salary FROM emp ORDER BY salary DESC, age DESC; - EXPLAIN SELECT id,age,salary FROM emp ORDER BY age DESC, salary ASC; - ``` +* DB_TRX_ID:6byte,最近修改事务ID,记录创建该数据或最后一次修改(修改/插入)该数据的事务ID。当每个事务开启时,都会被分配一个ID,这个 ID 是递增的 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL ORDER BY排序3.png) +* DB_ROLL_PTR:7byte,回滚指针,配合 undo 日志,指向上一个旧版本(存储在 rollback segment) - 尽量减少额外的排序,通过索引直接返回有序数据。**需要满足 Order by 使用相同的索引、Order By 的顺序和索引顺序相同、Order by 的字段都是升序或都是降序**,否则需要额外的操作,就会出现 FileSort +* DB_ROW_ID:6byte,隐含的自增ID(**隐藏主键**),如果数据表没有主键,InnoDB 会自动以 DB_ROW_ID 作为聚簇索引 -优化:通过创建合适的索引能够减少 Filesort 的出现,但是某些情况下条件限制不能让 Filesort 消失,就要加快 Filesort 的排序操作 +* DELETED_BIT:删除标志的隐藏字段,记录被更新或删除并不代表真的删除,而是删除位变了 -对于 Filesort , MySQL 有两种排序算法: +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MVCC版本链隐藏字段.png) -* 两次扫描算法:MySQL4.1 之前,使用该方式排序。首先根据条件取出排序字段和行指针信息,然后在排序区 sort buffer 中排序,如果 sort buffer 不够,则在临时表 temporary table 中存储排序结果。完成排序后再根据行指针**回表读取记录**,该操作可能会导致大量随机 I/O 操作 -* 一次扫描算法:一次性取出满足条件的所有数据,需要回表,然后在排序区 sort buffer 中排序后直接输出结果集。排序时内存开销较大,但是排序效率比两次扫描算法高 -MySQL 通过比较系统变量 max_length_for_sort_data 的大小和 Query 语句取出的字段的大小,来判定使用哪种排序算法。如果前者大,则说明 sort buffer 空间足够,使用第二种优化之后的算法,否则使用第一种。 -可以适当提高 sort_buffer_size 和 max_length_for_sort_data 系统变量,来增大排序区的大小,提高排序的效率 -```mysql -SET @@max_length_for_sort_data = 10000; -- 设置全局变量 -SET max_length_for_sort_data = 10240; -- 设置会话变量 -SHOW VARIABLES LIKE 'max_length_for_sort_data'; -- 默认1024 -SHOW VARIABLES LIKE 'sort_buffer_size'; -- 默认262114 -``` @@ -5586,36 +5748,34 @@ SHOW VARIABLES LIKE 'sort_buffer_size'; -- 默认262114 -#### GROUP BY +##### undo -GROUP BY 也会进行排序操作,与 ORDER BY 相比,GROUP BY 主要只是多了排序之后的分组操作,所以在 GROUP BY 的实现过程中,与 ORDER BY 一样也可以利用到索引 +undo log 是逻辑日志,记录的是每个事务对数据执行的操作,而不是记录的全部数据,需要根据 undo log 逆推出以往事务的数据 -* 分组查询: +undo log 的作用: - ```mysql - DROP INDEX idx_emp_age_salary ON emp; - EXPLAIN SELECT age,COUNT(*) FROM emp GROUP BY age; - ``` +* 保证事务进行 rollback 时的原子性和一致性,当事务进行回滚的时候可以用 undo log 的数据进行恢复 +* 用于 MVCC 快照读,通过读取 undo log 的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据版本 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL GROUP BY排序1.png) +undo log 主要分为两种: - Using temporary:表示 MySQL 需要使用临时表来存储结果集,常见于排序和分组查询 +* insert undo log:事务在 insert 新记录时产生的 undo log,只在事务回滚时需要,并且在事务提交后可以被立即丢弃 -* 查询包含 GROUP BY 但是用户想要避免排序结果的消耗, 则可以执行 ORDER BY NULL 禁止排序: +* update undo log:事务在进行 update 或 delete 时产生的 undo log,在事务回滚时需要,在快照读时也需要。不能随意删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被 purge 线程统一清除 - ```mysql - EXPLAIN SELECT age,COUNT(*) FROM emp GROUP BY age ORDER BY NULL; - ``` +每次对数据库记录进行改动,都会将旧值放到一条 undo 日志中,算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,把这个链表称之为**版本链**,版本链的头节点就是当前记录最新的值,链尾就是最早的旧记录 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL GROUP BY排序2.png) + -* 创建索引: +* 有个事务插入 persion 表一条新记录,name 为 Jerry,age 为 24 - ```mysql - CREATE INDEX idx_emp_age_salary ON emp(age,salary); - ``` +* 事务 1 修改该行数据时,数据库会先对该行加排他锁,然后先记录 undo log,然后修改该行 name 为 Tom,并且修改隐藏字段的事务 ID 为当前事务 1 的 ID(默认为 1 之后递增),回滚指针指向拷贝到 undo log 的副本记录,事务提交后,释放锁 +* 以此类推 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL GROUP BY排序3.png) +补充知识:purge 线程 + +* 为了实现 InnoDB 的 MVCC 机制,更新或者删除操作都只是设置一下老记录的 deleted_bit,并不真正将过时的记录删除,为了节省磁盘空间,InnoDB 有专门的 purge 线程来清理 deleted_bit 为 true 的记录 +* purge 线程维护了一个 Read view(这个 Read view 相当于系统中最老活跃事务的 Read view),如果某个记录的 deleted_bit 为 true,并且 DB_TRX_ID 相对于 purge 线程的 Read view 可见,那么这条记录一定是可以被安全清除的 @@ -5623,144 +5783,124 @@ GROUP BY 也会进行排序操作,与 ORDER BY 相比,GROUP BY 主要只是 -#### OR +##### 读视图 -对于包含 OR 的查询子句,如果要利用索引,则 OR 之间的**每个条件列都必须用到索引,而且不能使用到条件之间的复合索引**,如果没有索引,则应该考虑增加索引 +Read View 是事务进行**快照读**操作时产生的读视图,该事务执行快照读的那一刻会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的 ID,用来做可见性判断,根据视图判断当前事务能够看到哪个版本的数据 -* 执行查询语句: +注意:这里的快照并不是把所有的数据拷贝一份副本,而是由 undo log 记录的逻辑日志,根据库中的数据进行计算出历史数据 - ```mysql - EXPLAIN SELECT * FROM emp WHERE id = 1 OR age = 30; -- 两个索引,并且不是复合索引 - ``` +工作流程:将版本链的头节点的事务 ID(最新数据事务 ID)DB_TRX_ID 取出来,与系统当前活跃事务的 ID 对比,如果 DB_TRX_ID 不符合可见性,通过 DB_ROLL_PTR 回滚指针去取出 undo log 中的下一个 DB_TRX_ID 比较,直到找到最近的满足特定条件的 DB_TRX_ID,该事务 ID 所在的旧记录就是当前事务能看见的最新的记录 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL OR条件查询1.png) +Read View 几个属性: - ```sh - Extra: Using sort_union(idx_emp_age_salary,PRIMARY); Using where - ``` +- m_ids:生成 Read View 时当前系统中活跃的事务 id 列表(未提交的事务集合,当前事务也在其中) +- up_limit_id:生成 Read View 时当前系统中活跃的最小的事务 id,也就是 m_ids 中的最小值(已提交的事务集合) +- low_limit_id:生成 Read View 时系统应该分配给下一个事务的 id 值,m_ids 中的最大值加 1(未开始事务) +- creator_trx_id:生成该 Read View 的事务的事务 id,就是判断该 id 的事务能读到什么数据 -* 使用 UNION 替换 OR,求并集: - - 注意:该优化只针对多个索引列有效,如果有列没有被索引,查询效率可能会因为没有选择 OR 而降低 - - ```mysql - EXPLAIN SELECT * FROM emp WHERE id = 1 UNION SELECT * FROM emp WHERE age = 30; - ``` - - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL OR条件查询2.png) - -* UNION 要优于 OR 的原因: +creator 创建一个 Read View,进行可见性算法分析:(解决了读未提交) - * UNION 语句的 type 值为 ref,OR 语句的 type 值为 range - * UNION 语句的 ref 值为 const,OR 语句的 ref 值为 null,const 表示是常量值引用,非常快 +* db_trx_id == creator_trx_id:表示这个数据就是当前事务自己生成的,自己生成的数据自己肯定能看见,所以这种情况下此数据对 creator 是可见的 +* db_trx_id < up_limit_id:该版本对应的事务 ID 小于 Read view 中的最小活跃事务 ID,则这个事务在当前事务之前就已经被提交了,对 creator 可见 +* db_trx_id >= low_limit_id:该版本对应的事务 ID 大于 Read view 中当前系统的最大事务 ID,则说明该数据是在当前 Read view 创建之后才产生的,对 creator 不可见 +* up_limit_id <= db_trx_id < low_limit_id:判断 db_trx_id 是否在活跃事务列表 m_ids 中 + * 在列表中,说明该版本对应的事务正在运行,数据不能显示(**不能读到未提交的数据**) + * 不在列表中,说明该版本对应的事务已经被提交,数据可以显示(**可以读到已经提交的数据**) -**** +*** -#### 嵌套查询 -MySQL 4.1 版本之后,开始支持 SQL 的子查询 +##### 工作流程 -* 可以使用 SELECT 语句来创建一个单列的查询结果,然后把结果作为过滤条件用在另一个查询中 -* 使用子查询可以一次性的完成逻辑上需要多个步骤才能完成的 SQL 操作,同时也可以避免事务或者表锁死 -* 在有些情况下,子查询是可以被更高效的连接(JOIN)替代 +表 user 数据 -例如查找有角色的所有的用户信息: +```sh +id name age +1 张三 18 +``` -* 执行计划: +Transaction 20: - ```mysql - EXPLAIN SELECT * FROM t_user WHERE id IN (SELECT user_id FROM user_role); - ``` +```mysql +START TRANSACTION; -- 开启事务 +UPDATE user SET name = '李四' WHERE id = 1; +UPDATE user SET name = '王五' WHERE id = 1; +``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL嵌套查询1.png) +Transaction 60: -* 优化后: +```mysql +START TRANSACTION; -- 开启事务 +-- 操作表的其他数据 +``` - ```mysql - EXPLAIN SELECT * FROM t_user u , user_role ur WHERE u.id = ur.user_id; - ``` +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MVCC工作流程1.png) - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL嵌套查询2.png) +ID 为 0 的事务创建 Read View: - 连接查询之所以效率更高 ,是因为不需要在内存中创建临时表来完成逻辑上需要两个步骤的查询工作 +* m_ids:20、60 +* up_limit_id:20 +* low_limit_id:61 +* creator_trx_id:0 + +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MVCC工作流程2.png) +只有红框部分才复合条件,所以只有张三对应的版本的数据可以被看到 +参考视频:https://www.bilibili.com/video/BV1t5411u7Fg -*** +*** -#### 分页查询 -一般分页查询时,通过创建覆盖索引能够比较好地提高性能 -一个常见的问题是 `LIMIT 200000,10`,此时需要 MySQL 扫描前 200010 记录,仅仅返回 200000 - 200010 之间的记录,其他记录丢弃,查询排序的代价非常大 +#### RC RR -* 分页查询: +Read View 用于支持 RC(Read Committed,读已提交)和 RR(Repeatable Read,可重复读)隔离级别的实现 - ```mysql - EXPLAIN SELECT * FROM tb_user_1 LIMIT 200000,10; - ``` +RR、RC 生成时机: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL分页查询1.png) +- RC 隔离级别下,每个快照读都会生成并获取最新的 Read View +- RR 隔离级别下,则是同一个事务中的第一个快照读才会创建 Read View -* 优化方式一:子查询,在索引列 id 上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容 +RC、RR 级别下的 InnoDB 快照读区别 - ```mysql - EXPLAIN SELECT * FROM tb_user_1 t,(SELECT id FROM tb_user_1 ORDER BY id LIMIT 200000,10) a WHERE t.id = a.id; - ``` +- RC 级别下的,事务中每次快照读都会新生成一个 Read View,这就是在 RC 级别下的事务中可以看到别的事务提交的更新的原因 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL分页查询2.png) +- RR 级别下的某个事务的对某条记录的第一次快照读会创建一个 Read View, 将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,使用的是同一个Read View,所以一个事务的查询结果每次都是相同的 -* 优化方式二:方案适用于主键自增的表,可以把 LIMIT 查询转换成某个位置的查询 + 当前事务在其他事务提交之前使用过快照读,那么以后其他事务对数据的修改都是不可见的,就算以后其他事务提交了数据也不可见;早于 Read View 创建的事务所做的修改并提交的均是可见的 - ```mysql - EXPLAIN SELECT * FROM tb_user_1 WHERE id > 200000 LIMIT 10; -- 写法 1 - EXPLAIN SELECT * FROM tb_user_1 WHERE id BETWEEN 200000 and 200010; -- 写法 2 - ``` - - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL分页查询3.png) +解决幻读问题: +- 快照读:通过 MVCC 来进行控制的,在可重复读隔离级别下,普通查询是快照读,是不会看到别的事务插入的数据的,但是**并不能完全避免幻读** + 场景:RR 级别,T1 事务开启,创建 Read View,此时 T2 去 INSERT 新的一行然后提交,然后 T1去 UPDATE 该行会发现更新成功,因为 Read View 并不能阻止事务去更新数据,并且把这条新记录的 trx_id 给变为当前的事务 id,对当前事务就是可见的了 -**** +- 当前读:通过 next-key 锁(行锁 + 间隙锁)来解决问题 -#### 使用提示 -SQL 提示,是优化数据库的一个重要手段,就是在 SQL 语句中加入一些提示来达到优化操作的目的 -* USE INDEX:在查询语句中表名的后面添加 USE INDEX 来提供 MySQL 去参考的索引列表,可以让 MySQL 不再考虑其他可用的索引 - ```mysql - CREATE INDEX idx_seller_name ON tb_seller(name); - EXPLAIN SELECT * FROM tb_seller USE INDEX(idx_seller_name) WHERE name='小米科技'; - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用提示1.png) +*** -* IGNORE INDEX:让 MySQL 忽略一个或者多个索引,则可以使用 IGNORE INDEX 作为提示 - ```mysql - EXPLAIN SELECT * FROM tb_seller IGNORE INDEX(idx_seller_name) WHERE name = '小米科技'; - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用提示2.png) +### 持久特性 -* FORCE INDEX:强制 MySQL 使用一个特定的索引 +#### 重做日志 - ```mysql - EXPLAIN SELECT * FROM tb_seller FORCE INDEX(idx_seller_name_sta_addr) WHERE NAME='小米科技'; - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用提示3.png) @@ -5770,36 +5910,46 @@ SQL 提示,是优化数据库的一个重要手段,就是在 SQL 语句中 -#### 统计计数 +#### 实现原理 -在不同的 MySQL 引擎中,count(*) 有不同的实现方式: +##### 数据恢复 -* MyISAM 引擎把一个表的总行数存在了磁盘上,因此执行 count(*) 的时候会直接返回这个数,效率很高,但不支持事务 -* show table status 命令通过采样估算可以快速获取,但是不准确 -* InnoDB 表执行 count(*) 会遍历全表,虽然结果准确,但会导致性能问题 +持久性是指一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,接下来的其他操作或故障不应该对其有任何影响。 -解决方案: +Buffer Pool 是一片内存空间,可以通过 innodb_buffer_pool_size 来控制 Buffer Pool 的大小(内存优化部分会详解参数) -* 计数保存在 Redis 中,但是更新 MySQL 和 Redis 的操作不是原子的,会存在数据一致性的问题 +* Change Buffer 是 Buffer Pool 里的内存,不能无限增大,用来对增删改操作提供缓存 +* Change Buffer 的大小可以通过参数 innodb_change_buffer_max_size 来动态设置,设置为 50 时表示 Change Buffer 的大小最多只能占用 Buffer Pool 的 50% +* 补充知识:**唯一索引的更新不能使用 Buffer**,一般只有普通索引可以使用,直接写入 Buffer 就结束 -* 计数直接放到数据库里单独的一张计数表中,利用事务解决计数精确问题: +Buffer Pool 的使用提高了读写数据的效率,但是也带了新的问题:如果 MySQL 宕机,此时 Buffer Pool 中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证,所以引入 redo log - +* 当数据修改时,先修改 Change Buffer 中的数据,然后在 redo log buffer 记录这次操作 +* 如果 MySQL 宕机,InnoDB 判断一个数据页在崩溃恢复时丢失了更新,就会将它读到内存,然后根据 redo log 内容更新内存,更新完成后,内存页变成脏页,然后进行刷脏(buffer pool 的任务) - 会话 B 的读操作在 T3 执行的,这时更新事务还没有提交,所以计数值加 1 这个操作对会话 B 还不可见,因此会话 B 查询的计数值和最近 100 条记录,返回的结果逻辑上就是一致的 +redo log **记录数据页的物理修改**,而不是某一行或某几行的修改,用来恢复提交后的物理数据页,且只能恢复到最后一次提交的位置 - 并发系统性能的角度考虑,应该先插入操作记录再更新计数表,因为更新计数表涉及到行锁的竞争,**先插入再更新能最大程度地减少事务之间的锁等待,提升并发度** +redo log 采用的是 WAL(Write-ahead logging,**预写式日志**),所有修改要先写入日志,再更新到磁盘,保证了数据不会因 MySQL 宕机而丢失,从而满足了持久性要求 -count 函数的按照效率排序:`count(字段) < count(主键id) < count(1) ≈ count(*)`,所以建议尽量使用 count(*) +redo log 也需要在事务提交时将日志写入磁盘,但是比将内存中的 Buffer Pool 修改的数据写入磁盘的速度快: -* count(主键 id):InnoDB 引擎会遍历整张表,把每一行的 id 值都取出来返回给 Server 层,Server 判断 id 不为空就按行累加 -* count(1):InnoDB 引擎遍历整张表但不取值,Server 层对于返回的每一行,放一个数字 1 进去,判断不为空就按行累加 +* 刷脏是随机 IO,因为每次修改的数据位置随机,但写 redo log 是尾部追加操作,属于顺序 IO +* 刷脏是以数据页(Page)为单位的,一个页上的一个小修改都要整页写入,而 redo log 中只包含真正需要写入的部分,减少无效 IO -* count(字段):如果这个字段是定义为 not null 的话,一行行地从记录里面读出这个字段,判断不能为 null,按行累加;如果这个字段定义允许为 null,那么执行的时候,判断到有可能是 null,还要把值取出来再判断一下,不是 null 才累加 +InnoDB 引擎会在适当的时候,把内存中 redo log buffer 持久化到磁盘,具体的刷盘策略: +* 通过修改参数 `innodb_flush_log_at_trx_commit` 设置: + * 0:表示当提交事务时,并不将缓冲区的 redo 日志写入磁盘,而是等待主线程每秒刷新一次 + * 1:在事务提交时将缓冲区的 redo 日志**同步写入**到磁盘,保证一定会写入成功 + * 2:在事务提交时将缓冲区的 redo 日志异步写入到磁盘,不能保证提交时肯定会写入,只是有这个动作 +* 如果写入 redo log buffer 的日志已经占据了 redo log buffer 总容量的一半了,此时就会刷入到磁盘文件,这时会影响执行效率,所以开发中应该**避免大事务** +刷脏策略: -参考文章:https://time.geekbang.org/column/article/72775 +* redo log 文件是固定大小的,如果写满了就要擦除以前的记录,在擦除之前需要把旧记录更新到磁盘中的数据文件中 +* Buffer Pool 内存不足,需要淘汰部分数据页,如果淘汰的是脏页,就要先将脏页写到磁盘(大事务) +* 系统空闲时,后台线程会自动进行刷脏 +* MySQL 正常关闭时,会把内存的脏页都 flush 到磁盘上 @@ -5807,44 +5957,47 @@ count 函数的按照效率排序:`count(字段) < count(主键id) < count(1) +##### 工作流程 +MySQL 中还存在 binlog(二进制日志)也可以记录写操作并用于数据的恢复,**保证数据不丢失**,二者的区别是: -### 内存优化 +* 作用不同:redo log 是用于 crash recovery (故障恢复),保证 MySQL 宕机也不会影响持久性;binlog 是用于 point-in-time recovery 的,保证服务器可以基于时间点恢复数据,此外 binlog 还用于主从复制 -#### 优化原则 +* 层次不同:redo log 是 InnoDB 存储引擎实现的,而 binlog 是MySQL的服务器层实现的,同时支持 InnoDB 和其他存储引擎 -三个原则: +* 内容不同:redo log 是物理日志,内容基于磁盘的 Page;binlog 的内容是二进制的,根据 binlog_format 参数的不同,可能基于SQL 语句、基于数据本身或者二者的混合(日志部分详解) -* 将尽量多的内存分配给 MySQL 做缓存,但也要给操作系统和其他程序预留足够内存 -* MyISAM 存储引擎的数据文件读取依赖于操作系统自身的 IO 缓存,如果有 MyISAM 表,就要预留更多的内存给操作系统做 IO 缓存 -* 排序区、连接区等缓存是分配给每个数据库会话(Session)专用的,值的设置要根据最大连接数合理分配,如果设置太大,不但浪费资源,而且在并发数较高时会导致物理内存耗尽 +* 写入时机不同:binlog 在事务提交时一次写入;redo log 的写入时机相对多元 +两种日志在 update 更新数据的**作用时机**: +```sql +update T set c=c+1 where ID=2; +``` -*** + +流程说明:执行引擎将这行新数据更新到内存中(Buffer Pool)后,然后会将这个更新操作记录到 redo log buffer 里,此时 redo log 处于 prepare 状态,代表执行完成随时可以提交事务,然后执行器生成这个操作的 binlog 并**把 binlog 写入磁盘**,在提交事务后 **redo log 也持久化到磁盘** +redo log 和 binlog 都可以用于表示事务的提交状态,而**两阶段提交就是让这两个状态保持逻辑上的一致**,也有利于主从复制,更好的保持主从数据的一致性 -#### MyISAM +故障恢复数据: -MyISAM 存储引擎使用 key_buffer 缓存索引块,加速 MyISAM 索引的读写速度。对于 MyISAM 表的数据块没有特别的缓存机制,完全依赖于操作系统的 IO 缓存 +* 如果在时刻 A 发生了崩溃(crash),由于此时 binlog 还没写,redo log 也没提交,所以数据恢复的时候这个事务会回滚 +* 如果在时刻 B 发生了崩溃,redo log 和 binlog 有一个共同的数据字段叫 XID,崩溃恢复的时候,会按顺序扫描 redo log: + * 如果 redo log 里面的事务是完整的,也就是已经有了 commit 标识,说明 binlog 也已经记录完整,直接从 redo log 恢复数据 + * 如果 redo log 里面的事务只有 prepare,就根据 XID 去 binlog 中判断对应的事务是否存在并完整,如果完整可以从 binlog 恢复 redo log 的信息,进而恢复数据,提交事务 -* key_buffer_size:该变量决定 MyISAM 索引块缓存区的大小,直接影响到 MyISAM 表的存取效率 - ```mysql - SHOW VARIABLES LIKE 'key_buffer_size'; -- 单位是字节 - ``` +判断一个事务的 binlog 是否完整的方法: - 在 MySQL 配置文件中设置该值,建议至少将1/4可用内存分配给 key_buffer_size: +* statement 格式的 binlog,最后会有 COMMIT +* row 格式的 binlog,最后会有一个 XID event +* MySQL 5.6.2 版本以后,引入了 binlog-checksum 参数用来验证 binlog 内容的正确性 - ```sh - vim /etc/mysql/my.cnf - key_buffer_size=1024M - ``` -* read_buffer_size:如果需要经常顺序扫描 MyISAM 表,可以通过增大 read_buffer_size 的值来改善性能。但 read_buffer_size 是每个 Session 独占的,如果默认值设置太大,并发环境就会造成内存浪费 -* read_rnd_buffer_size:对于需要做排序的 MyISAM 表的查询,如带有 ORDER BY 子句的语句,适当增加该的值,可以改善此类的 SQL 的性能,但是 read_rnd_buffer_size 是每个 Session 独占的,如果默认值设置太大,就会造成内存浪费 +参考文章:https://time.geekbang.org/column/article/73161 @@ -5852,141 +6005,159 @@ MyISAM 存储引擎使用 key_buffer 缓存索引块,加速 MyISAM 索引的 -#### InnoDB +#### 系统优化 -Innodb 用一块内存区做 IO 缓存池,该缓存池不仅用来缓存 Innodb 的索引块,也用来缓存 Innodb 的数据块 +系统在进行刷脏时会占用一部分系统资源,会影响系统的性能,产生系统抖动 -* innodb_buffer_pool_size:该变量决定了 Innodb 存储引擎表数据和索引数据的最大缓存区大小 +* 一个查询要淘汰的脏页个数太多,会导致查询的响应时间明显变长 +* 日志写满,更新全部堵住,写性能跌为 0,这种情况对敏感业务来说,是不能接受的 - ```mysql - SHOW VARIABLES LIKE 'innodb_buffer_pool_size'; - ``` +InnoDB 刷脏页的控制策略: - 在保证操作系统及其他程序有足够内存可用的情况下,innodb_buffer_pool_size 的值越大,缓存命中率越高 +* `innodb_io_capacity` 参数代表磁盘的读写能力,建议设置成磁盘的 IOPS(每秒的 IO 次数) - ```sh - innodb_buffer_pool_size=512M - ``` +* 刷脏速度参考两个因素:脏页比例和 redo log 写盘速度 + * 参数 `innodb_max_dirty_pages_pct` 是脏页比例上限,默认值是 75%,InnoDB 会根据当前的脏页比例,算出一个范围在 0 到 100 之间的数字 + * InnoDB 每次写入的日志都有一个序号,当前写入的序号跟 checkpoint 对应的序号之间的差值,InnoDB 根据差值算出一个范围在 0 到 100 之间的数字 + * 两者较大的值记为 R,执行引擎按照 innodb_io_capacity 定义的能力乘以 R% 来控制刷脏页的速度 -* innodb_log_buffer_size:该值决定了 Innodb 日志缓冲区的大小,保存要写入磁盘上的日志文件数据 +* `innodb_flush_neighbors` 参数置为 1 代表控制刷脏时检查相邻的数据页,如果也是脏页就一起刷脏,并检查邻居的邻居,这个行为会一直蔓延直到不是脏页,在 MySQL 8.0 中该值的默认值是 0,不建议开启此功能 - 对于可能产生大量更新记录的大事务,增加 innodb_log_buffer_size 的大小,可以避免 Innodb 在事务提交前就执行不必要的日志写入磁盘操作,影响执行效率。通过配置文件修改: - ```sh - innodb_log_buffer_size=10M - ``` +**** -*** +### 一致特性 +一致性是指事务执行前后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。 -### 并发优化 +数据库的完整性约束包括但不限于:实体完整性(如行的主键存在且唯一)、列完整性(如字段的类型、大小、长度要符合要求)、外键约束、用户自定义完整性(如转账前后,两个账户余额的和应该不变) -MySQL Server 是多线程结构,包括后台线程和客户服务线程。多线程可以有效利用服务器资源,提高数据库的并发性能。在 MySQL 中,控制并发连接和线程的主要参数: +实现一致性的措施: -* max_connections:控制允许连接到 MySQL 数据库的最大连接数,默认值是 151 +- 保证原子性、持久性和隔离性,如果这些特性无法保证,事务的一致性也无法保证 +- 数据库本身提供保障,例如不允许向整形列插入字符串值、字符串长度不能超过列的限制等 +- 应用层面进行保障,例如如果转账操作只扣除转账者的余额,而没有增加接收者的余额,无论数据库实现的多么完美,也无法保证状态的一致 - 如果状态变量 connection_errors_max_connections 不为零,并且一直增长,则说明不断有连接请求因数据库连接数已达到允许最大值而失败,这时可以考虑增大max_connections 的值 - Mysql 最大可支持的连接数取决于很多因素,包括操作系统平台的线程库的质量、内存大小、每个连接的负荷、CPU的处理速度、期望的响应时间等。在 Linux 平台下,性能好的服务器,可以支持 500-1000 个连接,需要根据服务器性能进行评估设定 -* back_log:控制 MySQL 监听 TCP 端口时的积压请求栈的大小 - 如果 Mysql 的连接数达到 max_connections 时,新来的请求将会被存在堆栈中,以等待某一连接释放资源,该堆栈的数量即 back_log。如果等待连接的数量超过 back_log,将不被授予连接资源直接报错 - 5.6.6 版本之前默认值为 50,之后的版本默认为 `50 + (max_connections/5)`,但最大不超过900,如果需要数据库在较短的时间内处理大量连接请求, 可以考虑适当增大 back_log 的值 -* table_open_cache:控制所有 SQL 语句执行线程可打开表缓存的数量 - 在执行 SQL 语句时,每个执行线程至少要打开1个表缓存,该参数的值应该根据设置的最大连接数以及每个连接执行关联查询中涉及的表的最大数量来设定:`max_connections * N` +**** -* thread_cache_size:可控制 MySQL 缓存客户服务线程的数量 - 为了加快连接数据库的速度,MySQL 会缓存一定数量的客户服务线程以备重用,池化思想 -* innodb_lock_wait_timeout:设置 InnoDB 事务等待行锁的时间,默认值是 50ms - 对于需要快速反馈的业务系统,可以将行锁的等待时间调小,以避免事务被长时间挂起; 对于后台运行的批量处理程序来说,可以将行锁的等待时间调大,以避免发生大的回滚操作 +## 锁机制 + +### 基本介绍 +锁机制:数据库为了保证数据的一致性,在共享的资源被并发访问时变得安全有序所设计的一种规则 +作用:锁机制类似于多线程中的同步,可以保证数据的一致性和安全性 +锁的分类: -*** +- 按操作分类: + - 共享锁:也叫读锁。对同一份数据,多个事务读操作可以同时加锁而不互相影响 ,但不能修改数据 + - 排他锁:也叫写锁。当前的操作没有完成前,会阻断其他操作的读取和写入 +- 按粒度分类: + - 表级锁:会锁定整个表,开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低,偏向 MyISAM + - 行级锁:会锁定当前操作行,开销大,加锁慢;会出现死锁;锁定力度小,发生锁冲突概率低,并发度高,偏向 InnoDB + - 页级锁:锁的力度、发生冲突的概率和加锁开销介于表锁和行锁之间,会出现死锁,并发性能一般 +- 按使用方式分类: + - 悲观锁:每次查询数据时都认为别人会修改,很悲观,所以查询时加锁 + - 乐观锁:每次查询数据时都认为别人不会修改,很乐观,但是更新时会判断一下在此期间别人有没有去更新这个数据 +* 不同存储引擎支持的锁 + | 存储引擎 | 表级锁 | 行级锁 | 页级锁 | + | -------- | -------- | -------- | ------ | + | MyISAM | 支持 | 不支持 | 不支持 | + | InnoDB | **支持** | **支持** | 不支持 | + | MEMORY | 支持 | 不支持 | 不支持 | + | BDB | 支持 | 不支持 | 支持 | +从锁的角度来说:表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如 Web 应用;而行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并查询的应用,如一些在线事务处理系统 -## 主从复制 -### 基本介绍 +*** -复制是指将主数据库的 DDL 和 DML 操作通过二进制日志传到从库服务器中,然后在从库上对这些日志重新执行(也叫重做),从而使得从库和主库的数据保持同步 -MySQL 支持一台主库同时向多台从库进行复制,从库同时也可以作为其他从服务器的主库,实现链状复制 -MySQL 复制的优点主要包含以下三个方面: +### Server -- 主库出现问题,可以快速切换到从库提供服务 +FLUSH TABLES WITH READ LOCK 简称(FTWRL),全局读锁,让整个库处于只读状态,工作流程: -- 可以在从库上执行查询操作,从主库中更新,实现读写分离 +1. 上全局读锁(lock_global_read_lock) +2. 清理表缓存(close_cached_tables) +3. 上全局 COMMIT 锁(make_global_read_lock_block_commit) -- 可以在从库中执行备份,以避免备份期间影响主库的服务 +该命令主要用于备份工具做一致性备份,由于 FTWRL 需要持有两把全局的 MDL 锁,并且还要关闭所有表对象,因此杀伤性很大 +MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL) +MDL 叫元数据锁,主要用来保护 MySQL内部对象的元数据,保证数据读写的正确性,通过 MDL 机制保证 DDL、DML、DQL 操作的并发,**当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁** -*** +* MDL 锁不需要显式使用,在访问一个表的时候会被自动加上,事务中的 MDL 锁,在语句执行开始时申请,在整个事务提交后释放 +* MDL 锁是在 Server 中实现,不是 InnoDB 存储引擎层不能直接实现的锁 +* MDL 锁还能实现其他粒度级别的锁,比如全局锁、库级别的锁、表空间级别的锁 -### 复制原理 -#### 主从结构 -MySQL 的主从之间维持了一个长连接。主库内部有一个线程,专门用于服务从库的长连接,连接过程: -* 从库执行 change master 命令,设置主库的 IP、端口、用户名、密码以及要从哪个位置开始请求 binlog,这个位置包含文件名和日志偏移量 -* 从库执行 start slave 命令,这时从库会启动两个线程,就是图中的 io_thread 和 sql_thread,其中 io_thread 负责与主库建立连接 -* 主库校验完用户名、密码后,开始按照从传过来的位置,从本地读取 binlog 发给从库,开始主从复制 -主从复制原理图: +*** -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-主从复制原理图.jpg) -主从复制主要依赖的是 binlog,MySQL 默认是异步复制,需要三个线程: -- binlog dump thread:在主库事务提交时,负责把数据变更记录在二进制日志文件 binlog 中,并通知 slave 有数据更新 -- I/O thread:负责从主服务器上拉取二进制日志,并将 binlog 日志内容依次写到 relay log 中转日志的最末端,并将新的 binlog 文件名和 offset 记录到 master-info 文件中,以便下一次读取日志时从指定 binlog 日志文件及位置开始读取新的 binlog 日志内容 -- SQL thread:监测本地 relay log 中新增了日志内容,读取中继日志并重做其中的 SQL 语句,从库在 relay-log.info 中记录当前应用中继日志的文件名和位置点以便下一次执行 +### MyISAM -同步与异步: +#### 表级锁 -* 异步复制有数据丢失风险,例如数据还未同步到从库,主库就给客户端响应,然后主库挂了,此时从库晋升为主库的话数据是缺失的 -* 同步复制,主库需要将 binlog 复制到所有从库,等所有从库响应了之后主库才进行其他逻辑,这样的话性能很差,一般不会选择 -* MySQL 5.7 之出现了半同步复制,有参数可以选择成功同步几个从库就返回响应 +MyISAM 存储引擎只支持表锁,这也是 MySQL 开始几个版本中唯一支持的锁类型 + +MyISAM 引擎在执行查询语句之前,会**自动**给涉及到的所有表加读锁,在执行增删改之前,会**自动**给涉及的表加写锁,这个过程并不需要用户干预,所以用户一般不需要直接用 LOCK TABLE 命令给 MyISAM 表显式加锁 +* 加锁命令: + 读锁:所有连接只能读取数据,不能修改 -**** + 写锁:其他连接不能查询和修改数据 + ```mysql + -- 读锁 + LOCK TABLE table_name READ; + + -- 写锁 + LOCK TABLE table_name WRITE; + ``` +* 解锁命令: -#### 主主结构 + ```mysql + -- 将当前会话所有的表进行解锁 + UNLOCK TABLES; + ``` -主主结构就是两个数据库之间总是互为主从关系,这样在切换的时候就不用再修改主从关系 +锁的兼容性: -循环复制:在库 A 上更新了一条语句,然后把生成的 binlog 发给库 B,库 B 执行完这条更新语句后也会生成 binlog,会再发给 A +* 对 MyISAM 表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求 +* 对 MyISAM 表的写操作,则会阻塞其他用户对同一表的读和写操作 -解决方法: +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 锁的兼容性.png) -* 两个库的 server id 必须不同,如果相同则它们之间不能设定为主主关系 -* 一个库接到 binlog 并在重放的过程中,生成与原 binlog 的 server id 相同的新的 binlog -* 每个库在收到从主库发过来的日志后,先判断 server id,如果跟自己的相同,表示这个日志是自己生成的,就直接丢弃这个日志 +锁调度:MyISAM 的读写锁调度是写优先,因为写锁后其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永远阻塞,所以 MyISAM 不适合做写为主的表的存储引擎 @@ -5994,110 +6165,123 @@ MySQL 的主从之间维持了一个长连接。主库内部有一个线程, -### 主从延迟 +#### 锁操作 -#### 延迟原因 +##### 读锁 -正常情况主库执行更新生成的所有 binlog,都可以传到从库并被正确地执行,从库就能达到跟主库一致的状态,这就是最终一致性 +两个客户端操作 Client 1和 Client 2,简化为 C1、C2 -主从延迟是主从之间是存在一定时间的数据不一致,就是同一个事务在从库执行完成的时间和主库执行完成的时间的差值,即 T2-T1 +* 数据准备: -- 主库 A 执行完成一个事务,写入 binlog,该时刻记为 T1 -- 日志传给从库 B,从库 B 执行完这个事务,该时刻记为 T2 + ```mysql + CREATE TABLE `tb_book` ( + `id` INT(11) AUTO_INCREMENT, + `name` VARCHAR(50) DEFAULT NULL, + `publish_time` DATE DEFAULT NULL, + `status` CHAR(1) DEFAULT NULL, + PRIMARY KEY (`id`) + ) ENGINE=MYISAM DEFAULT CHARSET=utf8 ; + + INSERT INTO tb_book (id, NAME, publish_time, STATUS) VALUES(NULL,'java编程思想','2088-08-01','1'); + INSERT INTO tb_book (id, NAME, publish_time, STATUS) VALUES(NULL,'mysql编程思想','2088-08-08','0'); + ``` -通过在从库执行 `show slave status` 命令,返回结果会显示 seconds_behind_master 表示当前从库延迟了多少秒 +* C1、C2 加读锁,同时查询可以正常查询出数据 -- 每一个事务的 binlog 都有一个时间字段,用于记录主库上**写入**的时间 -- 从库取出当前正在**执行**的事务的时间字段,跟系统的时间进行相减,得到的就是 seconds_behind_master + ```mysql + LOCK TABLE tb_book READ; -- C1、C2 + SELECT * FROM tb_book; -- C1、C2 + ``` -主从延迟的原因: + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 读锁1.png) -* 从库的查询压力大 -* 大事务的执行,主库必须要等到事务完成之后才会写入 binlog,导致从节点出现应用 binlog 延迟 -* 主库的 DDL,从库与主库的 DDL 同步是串行进行,DDL 在主库执行时间很长,那么从库也会消耗同样的时间 -* 锁冲突问题也可能导致从节点的 SQL 线程执行慢 -* 从库的机器性能比主库的差,导致从库的复制能力弱 +* C1 加读锁,C1、C2查询未锁定的表,C1 报错,C2 正常查询 -主从同步问题永远都是**一致性和性能的权衡**,需要根据实际的应用场景,可以采取下面的办法: + ```mysql + LOCK TABLE tb_book READ; -- C1 + SELECT * FROM tb_user; -- C1、C2 + ``` -* 优化 SQL,避免慢 SQL,减少批量操作 -* 降低多线程大事务并发的概率,优化业务逻辑 -* 业务中大多数情况查询操作要比更新操作更多,搭建一主多从结构,让这些从库来分担读的压力 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 读锁2.png) -* 尽量采用短的链路,主库和从库服务器的距离尽量要短,提升端口带宽,减少 binlog 传输的网络延时 -* 实时性要求高的业务读强制走主库,从库只做备份 + C1、C2 执行插入操作,C1 报错,C2 等待获取 + ```mysql + INSERT INTO tb_book VALUES(NULL,'Spring高级','2088-01-01','1'); -- C1、C2 + ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 读锁3.png) -*** + 当在 C1 中释放锁指令 UNLOCK TABLES,C2 中的 INSERT 语句立即执行 -#### 并行复制 +*** -##### MySQL5.6 -高并发情况下,主库的会产生大量的 binlog,在从库中有两个线程 IO Thread 和 SQL Thread 单线程执行,会导致主库延迟变大。为了改善复制延迟问题,MySQL 5.6 版本增加了并行复制功能,以采用多线程机制来促进执行 -coordinator 就是原来的 sql_thread,并行复制中它不再直接更新数据,只**负责读取中转日志和分发事务**: +##### 写锁 -* 线程分配完成并不是立即执行,为了防止造成更新覆盖,更新同一 DB 的两个事务必须被分发到同一个工作线程 -* 同一个事务不能被拆开,必须放到同一个工作线程 +两个客户端操作 Client 1和 Client 2,简化为 C1、C2 -MySQL 5.6 版本的策略:每个线程对应一个 hash 表,用于保存当前这个线程的执行队列里的事务所涉及的表,hash 表的 key 是数据库 名,value 是一个数字,表示队列中有多少个事务修改这个库,适用于主库上有多个 DB 的情况 +* C1 加写锁,C1、C2查询表,C1 正常查询,C2 需要等待 -每个事务在分发的时候,跟线程的冲突(事务操作的是同一个 DB)关系包括以下三种情况: + ```mysql + LOCK TABLE tb_book WRITE; -- C1 + SELECT * FROM tb_book; -- C1、C2 + ``` -* 如果跟所有线程都不冲突,coordinator 线程就会把这个事务分配给最空闲的线程 -* 如果只跟一个线程冲突,coordinator 线程就会把这个事务分配给这个存在冲突关系的线程 -* 如果跟多于一个线程冲突,coordinator 线程就进入等待状态,直到和这个事务存在冲突关系的线程只剩下 1 个 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 写锁1.png) -优缺点: + 当在 C1 中释放锁指令 UNLOCK TABLES,C2 中的 SELECT 语句立即执行 -* 构造 hash 值的时候很快,只需要库名,而且一个实例上 DB 数也不会很多,不会出现需要构造很多个项的情况 -* 不要求 binlog 的格式,statement 格式的 binlog 也可以很容易拿到库名(日志章节详解了 binlog) -* 主库上的表都放在同一个 DB 里面,这个策略就没有效果了;或者不同 DB 的热点不同,比如一个是业务逻辑库,一个是系统配置库,那也起不到并行的效果,需要**把相同热度的表均匀分到这些不同的 DB 中**,才可以使用这个策略 +* C1、C2 同时加写锁 + ```mysql + LOCK TABLE tb_book WRITE; + ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 写锁2.png) -*** +* C1 加写锁,C1、C2查询未锁定的表,C1 报错,C2 正常查询 -##### MySQL5.7 +*** -MySQL 5.7 并行复制策略的思想是: -* 所有处于 commit 状态的事务可以并行执行 -* 同时处于 prepare 状态的事务,在从库执行时是可以并行的 -* 处于 prepare 状态的事务,与处于 commit 状态的事务之间,在从库执行时也是可以并行的 -MySQL 5.7 由参数 slave-parallel-type 来控制并行复制策略: +#### 锁状态 -* 配置为 DATABASE,表示使用 MySQL 5.6 版本的**按库(DB)并行策略** -* 配置为 LOGICAL_CLOCK,表示的**按提交状态并行**执行 +* 查看锁竞争: -MySQL 5.7.22 版本里,MySQL 增加了一个新的并行复制策略,基于 WRITESET 的并行复制。新增了一个参数 binlog-transaction-dependency-tracking,用来控制是否启用这个新策略: + ```mysql + SHOW OPEN TABLES; + ``` -* COMMIT_ORDER:表示根据同时进入 prepare 和 commit 来判断是否可以并行的策略 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-锁争用情况查看1.png) -* WRITESET:表示的是对于每个事务涉及更新的每一行,计算出这一行的 hash 值,组成该事务的 writeset 集合,如果两个事务没有操作相同的行,也就是说它们的 writeset 没有交集,就可以并行(**按行并行**) + In_user:表当前被查询使用的次数,如果该数为零,则表是打开的,但是当前没有被使用 -* WRITESET_SESSION:是在 WRITESET 的基础上多了一个约束,即在主库上同一个线程先后执行的两个事务,在备库执行的时候,要保证相同的先后顺序 + Name_locked:表名称是否被锁定,名称锁定用于取消表或对表进行重命名等操作 - 为了唯一标识,这个 hash 表的值是通过 `库名 + 表名 + 索引名 + 值` (表示的是某一行)计算出来的 + ```mysql + LOCK TABLE tb_book READ; -- 执行命令 + ``` -MySQL 5.7.22 按行并发的优势: + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-锁争用情况查看2.png) -* writeset 是在主库生成后直接写入到 binlog 里面的,这样在备库执行的时候,不需要解析 binlog 内容,节省了计算量 -* 不需要把整个事务的 binlog 都扫一遍才能决定分发到哪个线程,更省内存 -* 从库的分发策略不依赖于 binlog 内容,所以 binlog 是 statement 格式也可以,更节约内存(因为 row 才记录更改的行) +* 查看锁状态: -MySQL 5.7.22 的并行复制策略在通用性上是有保证的,但是对于表上没主键、唯一和外键约束的场景,WRITESET 策略也是没法并行的,也会暂时退化为单线程模型 + ```mysql + SHOW STATUS LIKE 'Table_locks%'; + ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 锁状态.png) + Table_locks_immediate:指的是能立即获得表级锁的次数,每立即获取锁,值加 1 -参考文章:https://time.geekbang.org/column/article/77083 + Table_locks_waited:指的是不能立即获取表级锁而需要等待的次数,每等待一次,该值加 1,此值高说明存在着较为严重的表级锁争用情况 @@ -6105,278 +6289,252 @@ MySQL 5.7.22 的并行复制策略在通用性上是有保证的,但是对于 -#### 读写分离 - -读写分离:可以降低主库的访问压力,提高系统的并发能力 - -* 主库不建查询的索引,从库建查询的索引。因为索引需要维护的,比如插入一条数据,不仅要在聚簇索引上面插入,对应的二级索引也得插入 -* 将读操作分到从库了之后,可以在主库把查询要用的索引删了,减少写操作对主库的影响 - -读写分离产生了读写延迟,造成数据的不一致性。假如客户端执行完一个更新事务后马上发起查询,如果查询选择的是从库的话,可能读到的还是以前的数据,叫过期读 - -解决方案: - -* 强制将写之后**立刻读的操作转移到主库**,比如刚注册的用户,直接登录从库查询可能查询不到,先走主库登录 - -* **二次查询**,如果从库查不到数据,则再去主库查一遍,由 API 封装,比较简单,但导致主库压力大 +### InnoDB -* 更新主库后,读从库之前先 sleep 一下,类似于执行一条 `select sleep(1)` 命令 -* 确保主备无延迟的方法,每次从库执行查询请求前,先判断 seconds_behind_master 是否已经等于 0,如果不等于那就等到这个参数变为 0 才能执行查询请求 +#### 行级锁 +InnoDB 与 MyISAM 的**最大不同**有两点:一是支持事务;二是采用了行级锁,InnoDB 同时支持表锁和行锁 +InnoDB 实现了以下两种类型的行锁: +- 共享锁 (S):又称为读锁,简称 S 锁,就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改 +- 排他锁 (X):又称为写锁,简称 X 锁,就是不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,只有获取排他锁的事务是可以对数据读取和修改 +对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会**自动给涉及数据集加排他锁**(行锁),在 commit 的时候会自动释放;对于普通 SELECT 语句,不会加任何锁 -*** +锁的兼容性: +- 共享锁和共享锁 兼容 +- 共享锁和排他锁 冲突 +- 排他锁和排他锁 冲突 +- 排他锁和共享锁 冲突 +可以通过以下语句显式给数据集加共享锁或排他锁: -### 负载均衡 +```mysql +SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE -- 共享锁 +SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 +``` -负载均衡是应用中使用非常普遍的一种优化方法,机制就是利用某种均衡算法,将固定的负载量分布到不同的服务器上,以此来降低单台服务器的负载,达到优化的效果 -* 分流查询:通过 MySQL 的主从复制,实现读写分离,使增删改操作走主节点,查询操作走从节点,从而可以降低单台服务器的读写压力 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-负载均衡主从复制.jpg) -* 分布式数据库架构:适合大数据量、负载高的情况,具有良好的拓展性和高可用性。通过在多台服务器之间分布数据,可以实现在多台服务器之间的负载均衡,提高访问效率 +*** -**** +#### 锁操作 +两个客户端操作 Client 1和 Client 2,简化为 C1、C2 -### 主从搭建 +* 环境准备 -#### 搭建流程 + ```mysql + CREATE TABLE test_innodb_lock( + id INT(11), + name VARCHAR(16), + sex VARCHAR(1) + )ENGINE = INNODB DEFAULT CHARSET=utf8; + + INSERT INTO test_innodb_lock VALUES(1,'100','1'); + -- .......... + + CREATE INDEX idx_test_innodb_lock_id ON test_innodb_lock(id); + CREATE INDEX idx_test_innodb_lock_name ON test_innodb_lock(name); + ``` -##### master +* 关闭自动提交功能: -1. 在master 的配置文件(/etc/mysql/my.cnf)中,配置如下内容: + ```mysql + SET AUTOCOMMIT=0; -- C1、C2 + ``` - ```sh - #mysql 服务ID,保证整个集群环境中唯一 - server-id=1 - - #mysql binlog 日志的存储路径和文件名 - log-bin=/var/lib/mysql/mysqlbin - - #错误日志,默认已经开启 - #log-err - - #mysql的安装目录 - #basedir - - #mysql的临时目录 - #tmpdir - - #mysql的数据存放目录 - #datadir - - #是否只读,1 代表只读, 0 代表读写 - read-only=0 - - #忽略的数据, 指不需要同步的数据库 - binlog-ignore-db=mysql - - #指定同步的数据库 - #binlog-do-db=db01 - ``` + 正常查询数据: -2. 执行完毕之后,需要重启 MySQL + ```mysql + SELECT * FROM test_innodb_lock; -- C1、C2 + ``` -3. 创建同步数据的账户,并且进行授权操作: +* 查询 id 为 3 的数据,正常查询: - ```mysql - GRANT REPLICATION SLAVE ON *.* TO 'seazean'@'192.168.0.137' IDENTIFIED BY '123456'; - FLUSH PRIVILEGES; - ``` + ```mysql + SELECT * FROM test_innodb_lock WHERE id=3; -- C1、C2 + ``` -4. 查看 master 状态: + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作1.png) - ```mysql - SHOW MASTER STATUS; - ``` +* C1 更新 id 为 3 的数据,但不提交: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-查看master状态.jpg) + ```mysql + UPDATE test_innodb_lock SET name='300' WHERE id=3; -- C1 + ``` - * File:从哪个日志文件开始推送日志文件 - * Position:从哪个位置开始推送日志 - * Binlog_Ignore_DB:指定不需要同步的数据库 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作2.png) + C2 查询不到 C1 修改的数据,因为隔离界别为 REPEATABLE READ,C1 提交事务,C2 查询: + ```mysql + COMMIT; -- C1 + ``` -*** + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作3.png) + 提交后仍然查询不到 C1 修改的数据,因为隔离级别可以防止脏读、不可重复读,所以 C2 需要提交才可以查询到其他事务对数据的修改: + ```mysql + COMMIT; -- C2 + SELECT * FROM test_innodb_lock WHERE id=3; -- C2 + ``` -##### slave + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作4.png) -1. 在 slave 端配置文件中,配置如下内容: +* C1 更新 id 为 3 的数据,但不提交,C2 也更新 id 为 3 的数据: - ```sh - #mysql服务端ID,唯一 - server-id=2 - - #指定binlog日志 - log-bin=/var/lib/mysql/mysqlbin - ``` + ```mysql + UPDATE test_innodb_lock SET name='3' WHERE id=3; -- C1 + UPDATE test_innodb_lock SET name='30' WHERE id=3; -- C2 + ``` -2. 执行完毕之后,需要重启 MySQL + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作5.png) -3. 指定当前从库对应的主库的IP地址、用户名、密码,从哪个日志文件开始的那个位置开始同步推送日志 + 当 C1 提交,C2 直接解除阻塞,直接更新 - ```mysql - CHANGE MASTER TO MASTER_HOST= '192.168.0.138', MASTER_USER='seazean', MASTER_PASSWORD='seazean', MASTER_LOG_FILE='mysqlbin.000001', MASTER_LOG_POS=413; - ``` +* 操作不同行的数据: -4. 开启同步操作: + ```mysql + UPDATE test_innodb_lock SET name='10' WHERE id=1; -- C1 + UPDATE test_innodb_lock SET name='30' WHERE id=3; -- C2 + ``` - ```mysql - START SLAVE; - SHOW SLAVE STATUS; - ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作6.png) -5. 停止同步操作: + 由于C1、C2 操作的不同行,获取不同的行锁,所以都可以正常获取行锁 - ```mysql - STOP SLAVE; - ``` +​ *** -##### 验证 +#### 锁分类 -1. 在主库中创建数据库,创建表并插入数据: +##### 间隙锁 - ```mysql - CREATE DATABASE db01; - USE db01; - CREATE TABLE user( - id INT(11) NOT NULL AUTO_INCREMENT, - name VARCHAR(50) NOT NULL, - sex VARCHAR(1), - PRIMARY KEY (id) - )ENGINE=INNODB DEFAULT CHARSET=utf8; - - INSERT INTO user(id,NAME,sex) VALUES(NULL,'Tom','1'); - INSERT INTO user(id,NAME,sex) VALUES(NULL,'Trigger','0'); - INSERT INTO user(id,NAME,sex) VALUES(NULL,'Dawn','1'); - ``` +当使用范围条件检索数据,并请求共享或排他锁时,InnoDB 会给符合条件的已有数据进行加锁,对于键值在条件范围内但并不存在的记录,叫做间隙(GAP), InnoDB 会对间隙进行加锁,就是间隙锁 -2. 在从库中查询数据,进行验证: +* 唯一索引加锁只有在值存在时才是行锁,值不存在会变成间隙锁,所以范围查询时容易出现间隙锁 +* 对于联合索引且是唯一索引,如果 where 条件只包括联合索引的一部分,那么会加间隙锁 - 在从库中,可以查看到刚才创建的数据库: +加锁的基本单位是 next-key lock,该锁是行锁和这条记录前面的 gap lock 的组合,就是行锁加间隙锁 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-主从复制验证1.jpg) +* 加锁遵循前开后闭原则 +* 假设有索引值 10、11、13,那么可能的间隙锁包括:(负无穷,10]、(10,11]、(11,13]、(13,20,正无穷),锁住索引 11 会同时对间隙 (10,11]、(11,13] 加锁 - 在该数据库中,查询表中的数据: +间隙锁优点:RR 级别下间隙锁可以解决事务的**幻读问题**,通过对间隙加锁,防止读取过程中数据条目发生变化 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-主从复制验证2.jpg) +间隙锁危害:当锁定一个范围的键值后,即使某些不存在的键值也会被无辜的锁定,造成在锁定的时候无法插入锁定键值范围内的任何数据,在某些场景下这可能会对性能造成很大的危害 +* 关闭自动提交功能: + ```mysql + SET AUTOCOMMIT=0; -- C1、C2 + ``` -*** +* 查询数据表: + ```mysql + SELECT * FROM test_innodb_lock; + ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 间隙锁1.png) -#### 主从切换 +* C1 根据 id 范围更新数据,C2 插入数据: -正常切换步骤: + ```mysql + UPDATE test_innodb_lock SET name='8888' WHERE id < 4; -- C1 + INSERT INTO test_innodb_lock VALUES(2,'200','2'); -- C2 + ``` -* 在开始切换之前先对主库进行锁表 `flush tables with read lock`,然后等待所有语句执行完成,切换完成后可以释放锁 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 间隙锁2.png) -* 检查 slave 同步状态,在 slave 执行 `show processlist` + 出现间隙锁,C2 被阻塞,等待 C1 提交事务后才能更新 -* 停止 slave io 线程,执行命令 `STOP SLAVE IO_THREAD` -* 提升 slave 为 master - ```sql - Stop slave; - Reset master; - Reset slave all; - set global read_only=off; -- 设置为可更新状态 - ``` +*** -* 将原来 master 变为 slave(参考搭建流程中的 slave 方法) -主库发生故障,从库会进行上位,其他从库指向新的主库 +##### 意向锁 +InnoDB 为了支持多粒度的加锁,允许行锁和表锁同时存在,支持在不同粒度上的加锁操作,InnoDB 增加了意向锁(Intention Lock ) +意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度上进行加锁,意向锁分为两种: +* 意向共享锁(IS):事务有意向对表中的某些行加共享锁 -**** +* 意向排他锁(IX):事务有意向对表中的某些行加排他锁 +InnoDB 存储引擎支持的是行级别的锁,因此意向锁不会阻塞除全表扫描以外的任何请求,表级意向锁与行级锁的兼容性如下所示: +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-意向锁兼容性.png) +插入意向锁是在插入一行记录操作之前设置的一种间隙锁,这个锁释放了一种插入方式的信号,即多个事务在相同的索引间隙插入时如果不是插入间隙中相同的位置就不需要互相等待。假设某列有索引值2,6,只要两个事务插入位置不同,如事务 A 插入 3,事务 B 插入 4,那么就可以同时插入 -## 锁机制 -### 基本介绍 +*** -锁机制:数据库为了保证数据的一致性,在共享的资源被并发访问时变得安全有序所设计的一种规则 -作用:锁机制类似于多线程中的同步,可以保证数据的一致性和安全性 -锁的分类: +##### 死锁 -- 按操作分类: - - 共享锁:也叫读锁。对同一份数据,多个事务读操作可以同时加锁而不互相影响 ,但不能修改数据 - - 排他锁:也叫写锁。当前的操作没有完成前,会阻断其他操作的读取和写入 -- 按粒度分类: - - 表级锁:会锁定整个表,开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低,偏向 MyISAM - - 行级锁:会锁定当前操作行,开销大,加锁慢;会出现死锁;锁定力度小,发生锁冲突概率低,并发度高,偏向 InnoDB - - 页级锁:锁的力度、发生冲突的概率和加锁开销介于表锁和行锁之间,会出现死锁,并发性能一般 -- 按使用方式分类: - - 悲观锁:每次查询数据时都认为别人会修改,很悲观,所以查询时加锁 - - 乐观锁:每次查询数据时都认为别人不会修改,很乐观,但是更新时会判断一下在此期间别人有没有去更新这个数据 +当并发系统中不同线程出现循环资源依赖,线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁 -* 不同存储引擎支持的锁 +死锁情况:线程 A 修改了 id = 1 的数据,请求修改 id = 2 的数据,线程 B 修改了 id = 2 的数据,请求修改 id = 1 的数据,产生死锁 - | 存储引擎 | 表级锁 | 行级锁 | 页级锁 | - | -------- | -------- | -------- | ------ | - | MyISAM | 支持 | 不支持 | 不支持 | - | InnoDB | **支持** | **支持** | 不支持 | - | MEMORY | 支持 | 不支持 | 不支持 | - | BDB | 支持 | 不支持 | 支持 | +解决策略: -从锁的角度来说:表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如 Web 应用;而行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并查询的应用,如一些在线事务处理系统 +* 直接进入等待直到超时,超时时间可以通过参数 innodb_lock_wait_timeout 来设置,但是时间的设置不好控制,超时可能不是因为死锁,而是因为事务处理比较慢,所以一般不采取该方式 +* 主动死锁检测,发现死锁后**主动回滚死锁链条中的某一个事务**,让其他事务得以继续执行,将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑 -*** +**** -### Server +#### 锁优化 -FLUSH TABLES WITH READ LOCK 简称(FTWRL),全局读锁,让整个库处于只读状态,工作流程: +##### 锁升级 -1. 上全局读锁(lock_global_read_lock) -2. 清理表缓存(close_cached_tables) -3. 上全局 COMMIT 锁(make_global_read_lock_block_commit) +索引失效造成行锁升级为表锁,不通过索引检索数据,InnoDB 会将对表中的所有记录加锁,实际效果和**表锁**一样实际开发过程应避免出现索引失效的状况 -该命令主要用于备份工具做一致性备份,由于 FTWRL 需要持有两把全局的 MDL 锁,并且还要关闭所有表对象,因此杀伤性很大 +* 查看当前表的索引: -MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL) + ```mysql + SHOW INDEX FROM test_innodb_lock; + ``` -MDL 叫元数据锁,主要用来保护 MySQL内部对象的元数据,保证数据读写的正确性,通过 MDL 机制保证 DDL、DML、DQL 操作的并发,**当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁** +* 关闭自动提交功能: -* MDL 锁不需要显式使用,在访问一个表的时候会被自动加上,事务中的 MDL 锁,在语句执行开始时申请,在整个事务提交后释放 + ```mysql + SET AUTOCOMMIT=0; -- C1、C2 + ``` -* MDL 锁是在 Server 中实现,不是 InnoDB 存储引擎层不能直接实现的锁 +* 执行更新语句: -* MDL 锁还能实现其他粒度级别的锁,比如全局锁、库级别的锁、表空间级别的锁 + ```mysql + UPDATE test_innodb_lock SET sex='2' WHERE name=10; -- C1 + UPDATE test_innodb_lock SET sex='2' WHERE id=3; -- C2 + ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁升级.png) + 索引失效:执行更新时 name 字段为 varchar 类型,造成索引失效,最终行锁变为表锁 @@ -6384,129 +6542,116 @@ MDL 叫元数据锁,主要用来保护 MySQL内部对象的元数据,保证 -### MyISAM - -#### 表级锁 +##### 优化锁 -MyISAM 存储引擎只支持表锁,这也是 MySQL 开始几个版本中唯一支持的锁类型 +InnoDB 存储引擎实现了行级锁定,虽然在锁定机制的实现方面带来了性能损耗可能比表锁会更高,但是在整体并发处理能力方面要远远优于 MyISAM 的表锁,当系统并发量较高的时候,InnoDB 的整体性能远远好于 MyISAM -MyISAM 引擎在执行查询语句之前,会**自动**给涉及到的所有表加读锁,在执行增删改之前,会**自动**给涉及的表加写锁,这个过程并不需要用户干预,所以用户一般不需要直接用 LOCK TABLE 命令给 MyISAM 表显式加锁 +但是使用不当可能会让InnoDB 的整体性能表现不仅不能比 MyISAM 高,甚至可能会更差 -* 加锁命令: +优化建议: - 读锁:所有连接只能读取数据,不能修改 +- 尽可能让所有数据检索都能通过索引来完成,避免无索引行锁升级为表锁 +- 合理设计索引,尽量缩小锁的范围 +- 尽可能减少索引条件及索引范围,避免间隙锁 +- 尽量控制事务大小,减少锁定资源量和时间长度 +- 尽可使用低级别事务隔离(需要业务层面满足需求) - 写锁:其他连接不能查询和修改数据 - ```mysql - -- 读锁 - LOCK TABLE table_name READ; - - -- 写锁 - LOCK TABLE table_name WRITE; - ``` -* 解锁命令: - ```mysql - -- 将当前会话所有的表进行解锁 - UNLOCK TABLES; - ``` -锁的兼容性: +*** -* 对 MyISAM 表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求 -* 对 MyISAM 表的写操作,则会阻塞其他用户对同一表的读和写操作 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 锁的兼容性.png) -锁调度:MyISAM 的读写锁调度是写优先,因为写锁后其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永远阻塞,所以 MyISAM 不适合做写为主的表的存储引擎 +#### 锁状态 +```mysql +SHOW STATUS LIKE 'innodb_row_lock%'; +``` + -*** +参数说明: +* Innodb_row_lock_current_waits:当前正在等待锁定的数量 +* Innodb_row_lock_time:从系统启动到现在锁定总时间长度 -#### 锁操作 +* Innodb_row_lock_time_avg:每次等待所花平均时长 -##### 读锁 +* Innodb_row_lock_time_max:从系统启动到现在等待最长的一次所花的时间 -两个客户端操作 Client 1和 Client 2,简化为 C1、C2 +* Innodb_row_lock_waits:系统启动后到现在总共等待的次数 -* 数据准备: +当等待的次数很高,而且每次等待的时长也不短的时候,就需要分析系统中为什么会有如此多的等待,然后根据分析结果制定优化计划 - ```mysql - CREATE TABLE `tb_book` ( - `id` INT(11) AUTO_INCREMENT, - `name` VARCHAR(50) DEFAULT NULL, - `publish_time` DATE DEFAULT NULL, - `status` CHAR(1) DEFAULT NULL, - PRIMARY KEY (`id`) - ) ENGINE=MYISAM DEFAULT CHARSET=utf8 ; - - INSERT INTO tb_book (id, NAME, publish_time, STATUS) VALUES(NULL,'java编程思想','2088-08-01','1'); - INSERT INTO tb_book (id, NAME, publish_time, STATUS) VALUES(NULL,'mysql编程思想','2088-08-08','0'); - ``` +查看锁状态: -* C1、C2 加读锁,同时查询可以正常查询出数据 +```mysql +SELECT * FROM information_schema.innodb_locks; #锁的概况 +SHOW ENGINE INNODB STATUS; #InnoDB整体状态,其中包括锁的情况 +``` - ```mysql - LOCK TABLE tb_book READ; -- C1、C2 - SELECT * FROM tb_book; -- C1、C2 - ``` +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB查看锁状态.png) - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 读锁1.png) +lock_id 是锁 id;lock_trx_id 为事务 id;lock_mode 为 X 代表排它锁(写锁);lock_type 为 RECORD 代表锁为行锁(记录锁) -* C1 加读锁,C1、C2查询未锁定的表,C1 报错,C2 正常查询 - ```mysql - LOCK TABLE tb_book READ; -- C1 - SELECT * FROM tb_user; -- C1、C2 - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 读锁2.png) - C1、C2 执行插入操作,C1 报错,C2 等待获取 - ```mysql - INSERT INTO tb_book VALUES(NULL,'Spring高级','2088-01-01','1'); -- C1、C2 - ``` +*** - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 读锁3.png) - 当在 C1 中释放锁指令 UNLOCK TABLES,C2 中的 INSERT 语句立即执行 +### 乐观锁 +悲观锁和乐观锁使用前提: -*** +- 对于读的操作远多于写的操作的时候,一个更新操作加锁会阻塞所有的读取操作,降低了吞吐量,最后需要释放锁,锁是需要一些开销的,这时候可以选择乐观锁 +- 如果是读写比例差距不是非常大或者系统没有响应不及时,吞吐量瓶颈的问题,那就不要去使用乐观锁,它增加了复杂度,也带来了业务额外的风险,这时候可以选择悲观锁 +乐观锁的现方式: +* 版本号 -##### 写锁 + 1. 给数据表中添加一个 version 列,每次更新后都将这个列的值加 1 -两个客户端操作 Client 1和 Client 2,简化为 C1、C2 + 2. 读取数据时,将版本号读取出来,在执行更新的时候,比较版本号 -* C1 加写锁,C1、C2查询表,C1 正常查询,C2 需要等待 + 3. 如果相同则执行更新,如果不相同,说明此条数据已经发生了变化 - ```mysql - LOCK TABLE tb_book WRITE; -- C1 - SELECT * FROM tb_book; -- C1、C2 - ``` + 4. 用户自行根据这个通知来决定怎么处理,比如重新开始一遍,或者放弃本次更新 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 写锁1.png) + ```mysql + -- 创建city表 + CREATE TABLE city( + id INT PRIMARY KEY AUTO_INCREMENT, -- 城市id + NAME VARCHAR(20), -- 城市名称 + VERSION INT -- 版本号 + ); + + -- 添加数据 + INSERT INTO city VALUES (NULL,'北京',1),(NULL,'上海',1),(NULL,'广州',1),(NULL,'深圳',1); + + -- 修改北京为北京市 + -- 1.查询北京的version + SELECT VERSION FROM city WHERE NAME='北京'; + -- 2.修改北京为北京市,版本号+1。并对比版本号 + UPDATE city SET NAME='北京市',VERSION=VERSION+1 WHERE NAME='北京' AND VERSION=1; + ``` - 当在 C1 中释放锁指令 UNLOCK TABLES,C2 中的 SELECT 语句立即执行 +* 时间戳 + + - 和版本号方式基本一样,给数据表中添加一个列,名称无所谓,数据类型需要是 **timestamp** + - 每次更新后都将最新时间插入到此列 + - 读取数据时,将时间读取出来,在执行更新的时候,比较时间 + - 如果相同则执行更新,如果不相同,说明此条数据已经发生了变化 -* C1、C2 同时加写锁 - ```mysql - LOCK TABLE tb_book WRITE; - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 写锁2.png) -* C1 加写锁,C1、C2查询未锁定的表,C1 报错,C2 正常查询 @@ -6514,216 +6659,184 @@ MyISAM 引擎在执行查询语句之前,会**自动**给涉及到的所有表 -#### 锁状态 -* 查看锁竞争: - ```mysql - SHOW OPEN TABLES; - ``` +## 主从 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-锁争用情况查看1.png) +### 基本介绍 - In_user:表当前被查询使用的次数,如果该数为零,则表是打开的,但是当前没有被使用 +复制是指将主数据库的 DDL 和 DML 操作通过二进制日志传到从库服务器中,然后在从库上对这些日志重新执行(也叫重做),从而使得从库和主库的数据保持同步 - Name_locked:表名称是否被锁定,名称锁定用于取消表或对表进行重命名等操作 +MySQL 支持一台主库同时向多台从库进行复制,从库同时也可以作为其他从服务器的主库,实现链状复制 - ```mysql - LOCK TABLE tb_book READ; -- 执行命令 - ``` +MySQL 复制的优点主要包含以下三个方面: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-锁争用情况查看2.png) +- 主库出现问题,可以快速切换到从库提供服务 -* 查看锁状态: +- 可以在从库上执行查询操作,从主库中更新,实现读写分离 - ```mysql - SHOW STATUS LIKE 'Table_locks%'; - ``` +- 可以在从库中执行备份,以避免备份期间影响主库的服务 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 锁状态.png) - Table_locks_immediate:指的是能立即获得表级锁的次数,每立即获取锁,值加 1 - Table_locks_waited:指的是不能立即获取表级锁而需要等待的次数,每等待一次,该值加 1,此值高说明存在着较为严重的表级锁争用情况 +*** -*** +### 复制原理 +#### 主从结构 +MySQL 的主从之间维持了一个长连接。主库内部有一个线程,专门用于服务从库的长连接,连接过程: -### InnoDB +* 从库执行 change master 命令,设置主库的 IP、端口、用户名、密码以及要从哪个位置开始请求 binlog,这个位置包含文件名和日志偏移量 +* 从库执行 start slave 命令,这时从库会启动两个线程,就是图中的 io_thread 和 sql_thread,其中 io_thread 负责与主库建立连接 +* 主库校验完用户名、密码后,开始按照从传过来的位置,从本地读取 binlog 发给从库,开始主从复制 -#### 行级锁 +主从复制原理图: -InnoDB 与 MyISAM 的**最大不同**有两点:一是支持事务;二是采用了行级锁,InnoDB 同时支持表锁和行锁 +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-主从复制原理图.jpg) -InnoDB 实现了以下两种类型的行锁: +主从复制主要依赖的是 binlog,MySQL 默认是异步复制,需要三个线程: -- 共享锁 (S):又称为读锁,简称 S 锁,就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改 -- 排他锁 (X):又称为写锁,简称 X 锁,就是不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,只有获取排他锁的事务是可以对数据读取和修改 +- binlog dump thread:在主库事务提交时,负责把数据变更记录在二进制日志文件 binlog 中,并通知 slave 有数据更新 +- I/O thread:负责从主服务器上拉取二进制日志,并将 binlog 日志内容依次写到 relay log 中转日志的最末端,并将新的 binlog 文件名和 offset 记录到 master-info 文件中,以便下一次读取日志时从指定 binlog 日志文件及位置开始读取新的 binlog 日志内容 +- SQL thread:监测本地 relay log 中新增了日志内容,读取中继日志并重做其中的 SQL 语句,从库在 relay-log.info 中记录当前应用中继日志的文件名和位置点以便下一次执行 -对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会**自动给涉及数据集加排他锁**(行锁),在 commit 的时候会自动释放;对于普通 SELECT 语句,不会加任何锁 +同步与异步: -锁的兼容性: +* 异步复制有数据丢失风险,例如数据还未同步到从库,主库就给客户端响应,然后主库挂了,此时从库晋升为主库的话数据是缺失的 +* 同步复制,主库需要将 binlog 复制到所有从库,等所有从库响应了之后主库才进行其他逻辑,这样的话性能很差,一般不会选择 +* MySQL 5.7 之出现了半同步复制,有参数可以选择成功同步几个从库就返回响应 -- 共享锁和共享锁 兼容 -- 共享锁和排他锁 冲突 -- 排他锁和排他锁 冲突 -- 排他锁和共享锁 冲突 -可以通过以下语句显式给数据集加共享锁或排他锁: -```mysql -SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE -- 共享锁 -SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 -``` +**** +#### 主主结构 +主主结构就是两个数据库之间总是互为主从关系,这样在切换的时候就不用再修改主从关系 -*** +循环复制:在库 A 上更新了一条语句,然后把生成的 binlog 发给库 B,库 B 执行完这条更新语句后也会生成 binlog,会再发给 A +解决方法: +* 两个库的 server id 必须不同,如果相同则它们之间不能设定为主主关系 +* 一个库接到 binlog 并在重放的过程中,生成与原 binlog 的 server id 相同的新的 binlog +* 每个库在收到从主库发过来的日志后,先判断 server id,如果跟自己的相同,表示这个日志是自己生成的,就直接丢弃这个日志 -#### 锁操作 -两个客户端操作 Client 1和 Client 2,简化为 C1、C2 -* 环境准备 +*** - ```mysql - CREATE TABLE test_innodb_lock( - id INT(11), - name VARCHAR(16), - sex VARCHAR(1) - )ENGINE = INNODB DEFAULT CHARSET=utf8; - - INSERT INTO test_innodb_lock VALUES(1,'100','1'); - -- .......... - - CREATE INDEX idx_test_innodb_lock_id ON test_innodb_lock(id); - CREATE INDEX idx_test_innodb_lock_name ON test_innodb_lock(name); - ``` -* 关闭自动提交功能: - ```mysql - SET AUTOCOMMIT=0; -- C1、C2 - ``` +### 主从延迟 - 正常查询数据: +#### 延迟原因 - ```mysql - SELECT * FROM test_innodb_lock; -- C1、C2 - ``` +正常情况主库执行更新生成的所有 binlog,都可以传到从库并被正确地执行,从库就能达到跟主库一致的状态,这就是最终一致性 -* 查询 id 为 3 的数据,正常查询: +主从延迟是主从之间是存在一定时间的数据不一致,就是同一个事务在从库执行完成的时间和主库执行完成的时间的差值,即 T2-T1 - ```mysql - SELECT * FROM test_innodb_lock WHERE id=3; -- C1、C2 - ``` +- 主库 A 执行完成一个事务,写入 binlog,该时刻记为 T1 +- 日志传给从库 B,从库 B 执行完这个事务,该时刻记为 T2 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作1.png) +通过在从库执行 `show slave status` 命令,返回结果会显示 seconds_behind_master 表示当前从库延迟了多少秒 -* C1 更新 id 为 3 的数据,但不提交: +- 每一个事务的 binlog 都有一个时间字段,用于记录主库上**写入**的时间 +- 从库取出当前正在**执行**的事务的时间字段,跟系统的时间进行相减,得到的就是 seconds_behind_master - ```mysql - UPDATE test_innodb_lock SET name='300' WHERE id=3; -- C1 - ``` +主从延迟的原因: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作2.png) +* 从库的查询压力大 +* 大事务的执行,主库必须要等到事务完成之后才会写入 binlog,导致从节点出现应用 binlog 延迟 +* 主库的 DDL,从库与主库的 DDL 同步是串行进行,DDL 在主库执行时间很长,那么从库也会消耗同样的时间 +* 锁冲突问题也可能导致从节点的 SQL 线程执行慢 +* 从库的机器性能比主库的差,导致从库的复制能力弱 - C2 查询不到 C1 修改的数据,因为隔离界别为 REPEATABLE READ,C1 提交事务,C2 查询: +主从同步问题永远都是**一致性和性能的权衡**,需要根据实际的应用场景,可以采取下面的办法: - ```mysql - COMMIT; -- C1 - ``` +* 优化 SQL,避免慢 SQL,减少批量操作 +* 降低多线程大事务并发的概率,优化业务逻辑 +* 业务中大多数情况查询操作要比更新操作更多,搭建一主多从结构,让这些从库来分担读的压力 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作3.png) +* 尽量采用短的链路,主库和从库服务器的距离尽量要短,提升端口带宽,减少 binlog 传输的网络延时 +* 实时性要求高的业务读强制走主库,从库只做备份 - 提交后仍然查询不到 C1 修改的数据,因为隔离级别可以防止脏读、不可重复读,所以 C2 需要提交才可以查询到其他事务对数据的修改: - ```mysql - COMMIT; -- C2 - SELECT * FROM test_innodb_lock WHERE id=3; -- C2 - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作4.png) +*** -* C1 更新 id 为 3 的数据,但不提交,C2 也更新 id 为 3 的数据: - ```mysql - UPDATE test_innodb_lock SET name='3' WHERE id=3; -- C1 - UPDATE test_innodb_lock SET name='30' WHERE id=3; -- C2 - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作5.png) +#### 并行复制 - 当 C1 提交,C2 直接解除阻塞,直接更新 +##### MySQL5.6 -* 操作不同行的数据: +高并发情况下,主库的会产生大量的 binlog,在从库中有两个线程 IO Thread 和 SQL Thread 单线程执行,会导致主库延迟变大。为了改善复制延迟问题,MySQL 5.6 版本增加了并行复制功能,以采用多线程机制来促进执行 - ```mysql - UPDATE test_innodb_lock SET name='10' WHERE id=1; -- C1 - UPDATE test_innodb_lock SET name='30' WHERE id=3; -- C2 - ``` +coordinator 就是原来的 sql_thread,并行复制中它不再直接更新数据,只**负责读取中转日志和分发事务**: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作6.png) +* 线程分配完成并不是立即执行,为了防止造成更新覆盖,更新同一 DB 的两个事务必须被分发到同一个工作线程 +* 同一个事务不能被拆开,必须放到同一个工作线程 - 由于C1、C2 操作的不同行,获取不同的行锁,所以都可以正常获取行锁 +MySQL 5.6 版本的策略:每个线程对应一个 hash 表,用于保存当前这个线程的执行队列里的事务所涉及的表,hash 表的 key 是数据库 名,value 是一个数字,表示队列中有多少个事务修改这个库,适用于主库上有多个 DB 的情况 +每个事务在分发的时候,跟线程的冲突(事务操作的是同一个 DB)关系包括以下三种情况: +* 如果跟所有线程都不冲突,coordinator 线程就会把这个事务分配给最空闲的线程 +* 如果只跟一个线程冲突,coordinator 线程就会把这个事务分配给这个存在冲突关系的线程 +* 如果跟多于一个线程冲突,coordinator 线程就进入等待状态,直到和这个事务存在冲突关系的线程只剩下 1 个 -​ +优缺点: -*** +* 构造 hash 值的时候很快,只需要库名,而且一个实例上 DB 数也不会很多,不会出现需要构造很多个项的情况 +* 不要求 binlog 的格式,statement 格式的 binlog 也可以很容易拿到库名(日志章节详解了 binlog) +* 主库上的表都放在同一个 DB 里面,这个策略就没有效果了;或者不同 DB 的热点不同,比如一个是业务逻辑库,一个是系统配置库,那也起不到并行的效果,需要**把相同热度的表均匀分到这些不同的 DB 中**,才可以使用这个策略 -#### 锁分类 +*** -##### 间隙锁 -当使用范围条件检索数据,并请求共享或排他锁时,InnoDB 会给符合条件的已有数据进行加锁,对于键值在条件范围内但并不存在的记录,叫做间隙(GAP), InnoDB 会对间隙进行加锁,就是间隙锁 -* 唯一索引加锁只有在值存在时才是行锁,值不存在会变成间隙锁,所以范围查询时容易出现间隙锁 -* 对于联合索引且是唯一索引,如果 where 条件只包括联合索引的一部分,那么会加间隙锁 +##### MySQL5.7 + +MySQL 5.7 并行复制策略的思想是: + +* 所有处于 commit 状态的事务可以并行执行 +* 同时处于 prepare 状态的事务,在从库执行时是可以并行的 +* 处于 prepare 状态的事务,与处于 commit 状态的事务之间,在从库执行时也是可以并行的 -加锁的基本单位是 next-key lock,该锁是行锁和这条记录前面的 gap lock 的组合,就是行锁加间隙锁 +MySQL 5.7 由参数 slave-parallel-type 来控制并行复制策略: -* 加锁遵循前开后闭原则 -* 假设有索引值 10、11、13,那么可能的间隙锁包括:(负无穷,10]、(10,11]、(11,13]、(13,20,正无穷),锁住索引 11 会同时对间隙 (10,11]、(11,13] 加锁 +* 配置为 DATABASE,表示使用 MySQL 5.6 版本的**按库(DB)并行策略** +* 配置为 LOGICAL_CLOCK,表示的**按提交状态并行**执行 -间隙锁优点:RR 级别下间隙锁可以解决事务的**幻读问题**,通过对间隙加锁,防止读取过程中数据条目发生变化 +MySQL 5.7.22 版本里,MySQL 增加了一个新的并行复制策略,基于 WRITESET 的并行复制。新增了一个参数 binlog-transaction-dependency-tracking,用来控制是否启用这个新策略: -间隙锁危害:当锁定一个范围的键值后,即使某些不存在的键值也会被无辜的锁定,造成在锁定的时候无法插入锁定键值范围内的任何数据,在某些场景下这可能会对性能造成很大的危害 +* COMMIT_ORDER:表示根据同时进入 prepare 和 commit 来判断是否可以并行的策略 -* 关闭自动提交功能: +* WRITESET:表示的是对于每个事务涉及更新的每一行,计算出这一行的 hash 值,组成该事务的 writeset 集合,如果两个事务没有操作相同的行,也就是说它们的 writeset 没有交集,就可以并行(**按行并行**) - ```mysql - SET AUTOCOMMIT=0; -- C1、C2 - ``` +* WRITESET_SESSION:是在 WRITESET 的基础上多了一个约束,即在主库上同一个线程先后执行的两个事务,在备库执行的时候,要保证相同的先后顺序 -* 查询数据表: + 为了唯一标识,这个 hash 表的值是通过 `库名 + 表名 + 索引名 + 值`(表示的是某一行)计算出来的 - ```mysql - SELECT * FROM test_innodb_lock; - ``` +MySQL 5.7.22 按行并发的优势: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 间隙锁1.png) +* writeset 是在主库生成后直接写入到 binlog 里面的,这样在备库执行的时候,不需要解析 binlog 内容,节省了计算量 +* 不需要把整个事务的 binlog 都扫一遍才能决定分发到哪个线程,更省内存 +* 从库的分发策略不依赖于 binlog 内容,所以 binlog 是 statement 格式也可以,更节约内存(因为 row 才记录更改的行) -* C1 根据 id 范围更新数据,C2 插入数据: +MySQL 5.7.22 的并行复制策略在通用性上是有保证的,但是对于表上没主键、唯一和外键约束的场景,WRITESET 策略也是没法并行的,也会暂时退化为单线程模型 - ```mysql - UPDATE test_innodb_lock SET name='8888' WHERE id < 4; -- C1 - INSERT INTO test_innodb_lock VALUES(2,'200','2'); -- C2 - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 间隙锁2.png) - 出现间隙锁,C2 被阻塞,等待 C1 提交事务后才能更新 +参考文章:https://time.geekbang.org/column/article/77083 @@ -6731,136 +6844,179 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 -##### 意向锁 - -InnoDB 为了支持多粒度的加锁,允许行锁和表锁同时存在,支持在不同粒度上的加锁操作,InnoDB 增加了意向锁(Intention Lock ) - -意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度上进行加锁,意向锁分为两种: +#### 读写分离 -* 意向共享锁(IS):事务有意向对表中的某些行加共享锁 +读写分离:可以降低主库的访问压力,提高系统的并发能力 -* 意向排他锁(IX):事务有意向对表中的某些行加排他锁 +* 主库不建查询的索引,从库建查询的索引。因为索引需要维护的,比如插入一条数据,不仅要在聚簇索引上面插入,对应的二级索引也得插入 +* 将读操作分到从库了之后,可以在主库把查询要用的索引删了,减少写操作对主库的影响 -InnoDB 存储引擎支持的是行级别的锁,因此意向锁不会阻塞除全表扫描以外的任何请求,表级意向锁与行级锁的兼容性如下所示: +读写分离产生了读写延迟,造成数据的不一致性。假如客户端执行完一个更新事务后马上发起查询,如果查询选择的是从库的话,可能读到的还是以前的数据,叫过期读 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-意向锁兼容性.png) +解决方案: -插入意向锁是在插入一行记录操作之前设置的一种间隙锁,这个锁释放了一种插入方式的信号,即多个事务在相同的索引间隙插入时如果不是插入间隙中相同的位置就不需要互相等待。假设某列有索引值2,6,只要两个事务插入位置不同,如事务 A 插入 3,事务 B 插入 4,那么就可以同时插入 +* 强制将写之后**立刻读的操作转移到主库**,比如刚注册的用户,直接登录从库查询可能查询不到,先走主库登录 +* **二次查询**,如果从库查不到数据,则再去主库查一遍,由 API 封装,比较简单,但导致主库压力大 +* 更新主库后,读从库之前先 sleep 一下,类似于执行一条 `select sleep(1)` 命令 +* 确保主备无延迟的方法,每次从库执行查询请求前,先判断 seconds_behind_master 是否已经等于 0,如果不等于那就等到这个参数变为 0 才能执行查询请求 -*** -##### 死锁 -当并发系统中不同线程出现循环资源依赖,线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁 +*** -死锁情况:线程 A 修改了 id = 1 的数据,请求修改 id = 2 的数据,线程 B 修改了 id = 2 的数据,请求修改 id = 1 的数据,产生死锁 -解决策略: -* 直接进入等待直到超时,超时时间可以通过参数 innodb_lock_wait_timeout 来设置,但是时间的设置不好控制,超时可能不是因为死锁,而是因为事务处理比较慢,所以一般不采取该方式 -* 主动死锁检测,发现死锁后**主动回滚死锁链条中的某一个事务**,让其他事务得以继续执行,将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑 +### 负载均衡 +负载均衡是应用中使用非常普遍的一种优化方法,机制就是利用某种均衡算法,将固定的负载量分布到不同的服务器上,以此来降低单台服务器的负载,达到优化的效果 +* 分流查询:通过 MySQL 的主从复制,实现读写分离,使增删改操作走主节点,查询操作走从节点,从而可以降低单台服务器的读写压力 -**** + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-负载均衡主从复制.jpg) +* 分布式数据库架构:适合大数据量、负载高的情况,具有良好的拓展性和高可用性。通过在多台服务器之间分布数据,可以实现在多台服务器之间的负载均衡,提高访问效率 -#### 锁优化 -##### 锁升级 +**** -索引失效造成行锁升级为表锁,不通过索引检索数据,InnoDB 会将对表中的所有记录加锁,实际效果和**表锁**一样实际开发过程应避免出现索引失效的状况 -* 查看当前表的索引: - ```mysql - SHOW INDEX FROM test_innodb_lock; - ``` +### 主从搭建 -* 关闭自动提交功能: +#### 搭建流程 - ```mysql - SET AUTOCOMMIT=0; -- C1、C2 - ``` +##### master -* 执行更新语句: +1. 在master 的配置文件(/etc/mysql/my.cnf)中,配置如下内容: - ```mysql - UPDATE test_innodb_lock SET sex='2' WHERE name=10; -- C1 - UPDATE test_innodb_lock SET sex='2' WHERE id=3; -- C2 - ``` + ```sh + #mysql 服务ID,保证整个集群环境中唯一 + server-id=1 + + #mysql binlog 日志的存储路径和文件名 + log-bin=/var/lib/mysql/mysqlbin + + #错误日志,默认已经开启 + #log-err + + #mysql的安装目录 + #basedir + + #mysql的临时目录 + #tmpdir + + #mysql的数据存放目录 + #datadir + + #是否只读,1 代表只读, 0 代表读写 + read-only=0 + + #忽略的数据, 指不需要同步的数据库 + binlog-ignore-db=mysql + + #指定同步的数据库 + #binlog-do-db=db01 + ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁升级.png) +2. 执行完毕之后,需要重启 MySQL - 索引失效:执行更新时 name 字段为 varchar 类型,造成索引失效,最终行锁变为表锁 +3. 创建同步数据的账户,并且进行授权操作: + ```mysql + GRANT REPLICATION SLAVE ON *.* TO 'seazean'@'192.168.0.137' IDENTIFIED BY '123456'; + FLUSH PRIVILEGES; + ``` +4. 查看 master 状态: -*** + ```mysql + SHOW MASTER STATUS; + ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-查看master状态.jpg) + * File:从哪个日志文件开始推送日志文件 + * Position:从哪个位置开始推送日志 + * Binlog_Ignore_DB:指定不需要同步的数据库 -##### 优化锁 -InnoDB 存储引擎实现了行级锁定,虽然在锁定机制的实现方面带来了性能损耗可能比表锁会更高,但是在整体并发处理能力方面要远远优于 MyISAM 的表锁,当系统并发量较高的时候,InnoDB 的整体性能远远好于 MyISAM -但是使用不当可能会让InnoDB 的整体性能表现不仅不能比 MyISAM 高,甚至可能会更差 +*** -优化建议: -- 尽可能让所有数据检索都能通过索引来完成,避免无索引行锁升级为表锁 -- 合理设计索引,尽量缩小锁的范围 -- 尽可能减少索引条件及索引范围,避免间隙锁 -- 尽量控制事务大小,减少锁定资源量和时间长度 -- 尽可使用低级别事务隔离(需要业务层面满足需求) +##### slave +1. 在 slave 端配置文件中,配置如下内容: + ```sh + #mysql服务端ID,唯一 + server-id=2 + + #指定binlog日志 + log-bin=/var/lib/mysql/mysqlbin + ``` +2. 执行完毕之后,需要重启 MySQL -*** +3. 指定当前从库对应的主库的IP地址、用户名、密码,从哪个日志文件开始的那个位置开始同步推送日志 + ```mysql + CHANGE MASTER TO MASTER_HOST= '192.168.0.138', MASTER_USER='seazean', MASTER_PASSWORD='seazean', MASTER_LOG_FILE='mysqlbin.000001', MASTER_LOG_POS=413; + ``` +4. 开启同步操作: -#### 锁状态 + ```mysql + START SLAVE; + SHOW SLAVE STATUS; + ``` -```mysql -SHOW STATUS LIKE 'innodb_row_lock%'; -``` +5. 停止同步操作: - + ```mysql + STOP SLAVE; + ``` -参数说明: -* Innodb_row_lock_current_waits:当前正在等待锁定的数量 -* Innodb_row_lock_time:从系统启动到现在锁定总时间长度 +*** -* Innodb_row_lock_time_avg:每次等待所花平均时长 -* Innodb_row_lock_time_max:从系统启动到现在等待最长的一次所花的时间 -* Innodb_row_lock_waits:系统启动后到现在总共等待的次数 +##### 验证 -当等待的次数很高,而且每次等待的时长也不短的时候,就需要分析系统中为什么会有如此多的等待,然后根据分析结果制定优化计划 +1. 在主库中创建数据库,创建表并插入数据: -查看锁状态: + ```mysql + CREATE DATABASE db01; + USE db01; + CREATE TABLE user( + id INT(11) NOT NULL AUTO_INCREMENT, + name VARCHAR(50) NOT NULL, + sex VARCHAR(1), + PRIMARY KEY (id) + )ENGINE=INNODB DEFAULT CHARSET=utf8; + + INSERT INTO user(id,NAME,sex) VALUES(NULL,'Tom','1'); + INSERT INTO user(id,NAME,sex) VALUES(NULL,'Trigger','0'); + INSERT INTO user(id,NAME,sex) VALUES(NULL,'Dawn','1'); + ``` -```mysql -SELECT * FROM information_schema.innodb_locks; #锁的概况 -SHOW ENGINE INNODB STATUS; #InnoDB整体状态,其中包括锁的情况 -``` +2. 在从库中查询数据,进行验证: -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB查看锁状态.png) + 在从库中,可以查看到刚才创建的数据库: -lock_id 是锁 id;lock_trx_id 为事务 id;lock_mode 为 X 代表排它锁(写锁);lock_type 为 RECORD 代表锁为行锁(记录锁) + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-主从复制验证1.jpg) + 在该数据库中,查询表中的数据: + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-主从复制验证2.jpg) @@ -6868,49 +7024,30 @@ lock_id 是锁 id;lock_trx_id 为事务 id;lock_mode 为 X 代表排它锁 -### 乐观锁 - -悲观锁和乐观锁使用前提: +#### 主从切换 -- 对于读的操作远多于写的操作的时候,一个更新操作加锁会阻塞所有的读取操作,降低了吞吐量,最后需要释放锁,锁是需要一些开销的,这时候可以选择乐观锁 -- 如果是读写比例差距不是非常大或者系统没有响应不及时,吞吐量瓶颈的问题,那就不要去使用乐观锁,它增加了复杂度,也带来了业务额外的风险,这时候可以选择悲观锁 +正常切换步骤: -乐观锁的现方式: +* 在开始切换之前先对主库进行锁表 `flush tables with read lock`,然后等待所有语句执行完成,切换完成后可以释放锁 -* 版本号 +* 检查 slave 同步状态,在 slave 执行 `show processlist` - 1. 给数据表中添加一个 version 列,每次更新后都将这个列的值加 1 +* 停止 slave io 线程,执行命令 `STOP SLAVE IO_THREAD` - 2. 读取数据时,将版本号读取出来,在执行更新的时候,比较版本号 +* 提升 slave 为 master - 3. 如果相同则执行更新,如果不相同,说明此条数据已经发生了变化 + ```sql + Stop slave; + Reset master; + Reset slave all; + set global read_only=off; -- 设置为可更新状态 + ``` - 4. 用户自行根据这个通知来决定怎么处理,比如重新开始一遍,或者放弃本次更新 +* 将原来 master 变为 slave(参考搭建流程中的 slave 方法) - ```mysql - -- 创建city表 - CREATE TABLE city( - id INT PRIMARY KEY AUTO_INCREMENT, -- 城市id - NAME VARCHAR(20), -- 城市名称 - VERSION INT -- 版本号 - ); - - -- 添加数据 - INSERT INTO city VALUES (NULL,'北京',1),(NULL,'上海',1),(NULL,'广州',1),(NULL,'深圳',1); - - -- 修改北京为北京市 - -- 1.查询北京的version - SELECT VERSION FROM city WHERE NAME='北京'; - -- 2.修改北京为北京市,版本号+1。并对比版本号 - UPDATE city SET NAME='北京市',VERSION=VERSION+1 WHERE NAME='北京' AND VERSION=1; - ``` +主库发生故障,从库会进行上位,其他从库指向新的主库 -* 时间戳 - - 和版本号方式基本一样,给数据表中添加一个列,名称无所谓,数据类型需要是 **timestamp** - - 每次更新后都将最新时间插入到此列 - - 读取数据时,将时间读取出来,在执行更新的时候,比较时间 - - 如果相同则执行更新,如果不相同,说明此条数据已经发生了变化 @@ -7674,13 +7811,13 @@ public class JDBCDemo01 { ### 攻击演示 -SQL注入攻击演示 +SQL 注入攻击演示 * 在登录界面,输入一个错误的用户名或密码,也可以登录成功 ![](https://gitee.com/seazean/images/raw/master/DB/SQL注入攻击演示.png) -* 原理:我们在密码处输入的所有内容,都应该认为是密码的组成,但是 Statement 对象在执行 sql 语句时,将一部分内容当做查询条件来执行 +* 原理:我们在密码处输入的所有内容,都应该认为是密码的组成,但是 Statement 对象在执行 SQL 语句时,将一部分内容当做查询条件来执行 ```mysql SELECT * FROM user WHERE loginname='aaa' AND password='aaa' OR '1'='1'; @@ -8762,7 +8899,7 @@ io-threads 4 #官网建议4核的机器建议设置为2或3个线程,8核的 -### key指令 +### key 指令 key 是一个字符串,通过 key 获取 redis 中保存的数据 @@ -8818,7 +8955,7 @@ key 是一个字符串,通过 key 获取 redis 中保存的数据 -### DB指令 +### DB 指令 Redis 在使用过程中,随着操作数据量的增加,会出现大量的数据以及对应的 key,数据不区分种类、类别混在一起,容易引起重复或者冲突,所以 Redis 为每个服务提供 16 个数据库,编码 0-15,每个数据库之间相互独立,**共用 **Redis 内存,不区分大小 @@ -8870,7 +9007,7 @@ Redis 客户端可以订阅任意数量的频道 -### ACL指令 +### ACL 指令 Redis ACL 是 Access Control List(访问控制列表)的缩写,该功能允许根据可以执行的命令和可以访问的键来限制某些连接 diff --git a/Java.md b/Java.md index 0ba5164..c1d2f77 100644 --- a/Java.md +++ b/Java.md @@ -3867,11 +3867,12 @@ Collection 集合的遍历方式有三种: 集合可以直接输出内容,因为底层重写了 toString() 方法 1. 迭代器 - `public Iterator iterator()`:获取集合对应的迭代器,用来遍历集合中的元素的 - `E next()`:获取下一个元素值 - `boolean hasNext()`:判断是否有下一个元素,有返回true ,反之 - `default void remove()`:从底层集合中删除此迭代器返回的最后一个元素,这种方法只能在每次调用next() 时调用一次 - + + * `public Iterator iterator()`:获取集合对应的迭代器,用来遍历集合中的元素的 + * `E next()`:获取下一个元素值 + * `boolean hasNext()`:判断是否有下一个元素,有返回 true ,反之返回 false + * `default void remove()`:从底层集合中删除此迭代器返回的最后一个元素,这种方法只能在每次调用 next() 时调用一次 + 2. 增强 for 循环:可以遍历集合或者数组,遍历集合实际上是迭代器遍历的简化写法 ```java @@ -3925,11 +3926,11 @@ Collection 集合的遍历方式有三种: ##### 概述 -List集合继承了Collection集合全部的功能。 +List 集合继承了 Collection 集合全部的功能。 -List系列集合有索引,所以多了很多按照索引操作元素的功能:for循环遍历(4种遍历) +List 系列集合有索引,所以多了很多按照索引操作元素的功能:for 循环遍历(4 种遍历) -List系列集合:添加的元素是有序,可重复,有索引。 +List 系列集合:添加的元素是有序,可重复,有索引。 * ArrayList:添加的元素是有序,可重复,有索引。 @@ -4141,7 +4142,7 @@ public class ArrayList extends AbstractList if (modCount != expectedModCount) throw new ConcurrentModificationException(); } - // 允许删除操作 + // 【允许删除操作】 public void remove() { // ... checkForComodification(); @@ -4162,7 +4163,7 @@ public class ArrayList extends AbstractList ##### Vector -同步:Vector的实现与 ArrayList 类似,但是方法上使用了 synchronized 进行同步 +同步:Vector 的实现与 ArrayList 类似,但是方法上使用了 synchronized 进行同步 构造:默认长度为 10 的数组 @@ -4284,8 +4285,8 @@ LinkedList 是一个实现了 List 接口的**双端链表**,支持高效的 * 获取元素:`get(int index)` 根据指定索引返回数据 - * 获取头节点 (index=0):getFirst()、element()、peek()、peekFirst() 这四个获取头结点方法的区别在于对链表为空时的处理,是抛出异常还是返回null,其中**getFirst() 和element()** 方法将会在链表为空时,抛出异常 - * 获取尾节点 (index=-1):getLast() 方法在链表为空时,会抛出NoSuchElementException,而peekLast() 则不会,只会返回 null + * 获取头节点 (index=0):getFirst()、element()、peek()、peekFirst() 这四个获取头结点方法的区别在于对链表为空时的处理,是抛出异常还是返回null,其中 **getFirst() 和 element()** 方法将会在链表为空时,抛出异常 + * 获取尾节点 (index=-1):getLast() 方法在链表为空时,抛出 NoSuchElementException,而 peekLast() 不会,只会返回 null * 删除元素: @@ -4303,7 +4304,7 @@ LinkedList 是一个实现了 List 接口的**双端链表**,支持高效的 * LinkedList采 用链表存储,所以对于`add(E e)`方法的插入,删除元素不受元素位置的影响 4. 是否支持快速随机访问: * LinkedList 不支持高效的随机元素访问,ArrayList 支持 - * 快速随机访问就是通过元素的序号快速获取元素对象(对应于`get(int index)`方法)。 + * 快速随机访问就是通过元素的序号快速获取元素对象(对应于 `get(int index)` 方法) 5. 内存空间占用: * ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间 * LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据) @@ -4347,7 +4348,7 @@ Set 系列集合:添加的元素是无序,不重复,无索引的 **HashSet 底层就是基于 HashMap 实现,值是 PRESENT = new Object()** -Set集合添加的元素是无序,不重复的。 +Set 集合添加的元素是无序,不重复的。 * 是如何去重复的? diff --git a/Prog.md b/Prog.md index 5fcf710..36c0ed0 100644 --- a/Prog.md +++ b/Prog.md @@ -8279,7 +8279,9 @@ public void lock() { } else { // 自旋到这,普通入队方式,【尾插法】 node.prev = t; + // 【在设置完尾节点后,才更新的原始尾节点的后继节点,所以此时从前往后遍历会丢失尾节点】 if (compareAndSetTail(t, node)) { + //【此时 t.next = null,并且这里已经 CAS 结束,线程并不是安全的】 t.next = node; return t; // 返回当前 node 的前驱节点 } @@ -8461,6 +8463,8 @@ Thread-0 释放锁,进入 release 流程 } ``` + 从后向前的原因:enq 方法中,节点是尾插法,首先赋值的是尾节点的前驱节点,此时前驱节点的 next 并没有指向尾节点,从前遍历会丢失节点 + * 唤醒的线程会从 park 位置开始执行,如果加锁成功(没有竞争),会设置 * exclusiveOwnerThread 为 Thread-1,state = 1 diff --git a/SSM.md b/SSM.md index b045da3..085ba19 100644 --- a/SSM.md +++ b/SSM.md @@ -16,6 +16,8 @@ ORM(Object Relational Mapping): 对象关系映射,指的是持久化数 MyBatis 官网地址:http://www.mybatis.org/mybatis-3/ +参考视频:https://space.bilibili.com/37974444/ + *** @@ -10091,7 +10093,7 @@ MVC(Model View Controller),一种用于设计创建Web应用程序表现 ![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-MVC功能图示.png) - +参考视频:https://space.bilibili.com/37974444/ diff --git a/Web.md b/Web.md index f12c7a5..7c70150 100644 --- a/Web.md +++ b/Web.md @@ -9026,9 +9026,9 @@ Element:网站快速成型工具,是饿了么公司前端开发团队提供 ## 安装软件 -Nginx(engine x) 是一个高性能的HTTP和[反向代理](https://baike.baidu.com/item/反向代理/7793488)web服务器,同时也提供了IMAP/POP3/SMTP服务。 +Nginx 是一个高性能的 HTTP 和[反向代理 ](https://baike.baidu.com/item/反向代理/7793488)Web 服务器,同时也提供了 IMAP/POP3/SMTP 服务 -Nginx两个最核心的功能:高性能的静态web服务器,反向代理 +Nginx 两个最核心的功能:高性能的静态 Web 服务器,反向代理 * 安装指令:sudo apt-get install nginx @@ -9036,6 +9036,7 @@ Nginx两个最核心的功能:高性能的静态web服务器,反向代理 * 系统指令:systemctl / service start/restart/stop/status nginx 配置文件安装目录:/etc/nginx + 日志文件:/var/log/nginx @@ -9046,20 +9047,20 @@ Nginx两个最核心的功能:高性能的静态web服务器,反向代理 ## 配置文件 -nginx.conf 文件时nginx的主配置文件 +nginx.conf 文件时 Nginx 的主配置文件 -* main部分 +* main 部分 -* events部分 +* events 部分 -* server部分 +* server 部分 - root设置的路径会拼接上location的路径,然后去最终路径寻找对应的文件 + root 设置的路径会拼接上 location 的路径,然后去最终路径寻找对应的文件 @@ -9069,15 +9070,18 @@ nginx.conf 文件时nginx的主配置文件 ## 发布项目 -1. 创建一个toutiao目录 - cd /home - mkdir toutiao - -2. 将项目上传到toutiao目录 +1. 创建一个 toutiao 目录 + + ```sh + cd /home + mkdir toutiao + ``` + +2. 将项目上传到 toutiao 目录 3. 解压项目 unzip web.zip -4. 编辑Nginx配置文件nginx.conf +4. 编辑 Nginx 配置文件 nginx.conf ```shell server { @@ -9090,9 +9094,9 @@ nginx.conf 文件时nginx的主配置文件 } ``` -5. 重启nginx服务:systemctl restart nginx +5. 重启 Nginx 服务:systemctl restart nginx -6. 浏览器打开网址 http://127.0.0.1:80 +6. 浏览器打开网址:http://127.0.0.1:80 @@ -9102,15 +9106,15 @@ nginx.conf 文件时nginx的主配置文件 ## 反向代理 -> 无法访问Google,可以配置一个代理服务器,发送请求到代理服务器,代理服务器经过转发,再将请求转发给Google,返回结果之后,再次转发给用户。这个叫做正向代理,正向代理对于用户来说,是有感知的 +> 无法访问 Google,可以配置一个代理服务器,发送请求到代理服务器,代理服务器经过转发,再将请求转发给 Google,返回结果之后,再次转发给用户,这个叫做正向代理,正向代理对于用户来说,是有感知的 -**正向代理(forward proxy)**:是一个位于客户端和目标服务器之间的服务器(代理服务器),为了从目标服务器取得内容,客户端向代理服务器发送一个请求并指定目标,然后代理服务器向目标服务器转交请求并将获得的内容返回给客户端,**正向代理,其实是"代理服务器"代理了"客户端",去和"目标服务器"进行交互** +**正向代理(forward proxy)**:是一个位于客户端和目标服务器之间的代理服务器,为了从目标服务器取得内容,客户端向代理服务器发送一个请求并指定目标,然后代理服务器向目标服务器转交请求并将获得的内容返回给客户端,**正向代理,其实是"代理服务器"代理了当前"客户端",去和"目标服务器"进行交互** 作用: -* 突破访问限制:通过代理服务器,可以突破自身IP访问限制,访问国外网站,教育网等 +* 突破访问限制:通过代理服务器,可以突破自身 IP 访问限制,访问国外网站,教育网等 * 提高访问速度:代理服务器都设置一个较大的硬盘缓冲区,会将部分请求的响应保存到缓冲区中,当其他用户再访问相同的信息时, 则直接由缓冲区中取出信息,传给用户,以提高访问速度 -* 隐藏客户端真实IP:隐藏自己的IP,免受攻击 +* 隐藏客户端真实 IP:隐藏自己的 IP,免受攻击 @@ -9118,14 +9122,14 @@ nginx.conf 文件时nginx的主配置文件 -**反向代理(reverse proxy)**:是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器,**反向代理,其实是"代理服务器"代理了"目标服务器",去和"客户端"进行交互** +**反向代理(reverse proxy)**:是指以代理服务器来接受 Internet 上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给 Internet 上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器,**反向代理,其实是"代理服务器"代理了"目标服务器",去和当前"客户端"进行交互** 作用: -* 隐藏服务器真实IP:使用反向代理,可以对客户端隐藏服务器的IP地址 +* 隐藏服务器真实 IP:使用反向代理,可以对客户端隐藏服务器的 IP 地址 * 负载均衡:根据所有真实服务器的负载情况,将客户端请求分发到不同的真实服务器上 * 提高访问速度:反向代理服务器可以对于静态内容及短时间内有大量访问请求的动态内容提供缓存服务 -* 提供安全保障:反向代理服务器可以作为应用层防火墙,为网站提供对基于Web的攻击行为(例如DoS/DDoS)的防护,更容易排查恶意软件等 +* 提供安全保障:反向代理服务器可以作为应用层防火墙,为网站提供对基于 Web 的攻击行为(例如 DoS/DDoS)的防护,更容易排查恶意软件等 From c58a00285ded7dbaa786d038b5d40e6d0e3cdad4 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 12 Dec 2021 20:52:27 +0800 Subject: [PATCH 089/168] Update Java Notes --- DB.md | 251 ++++++++++++++++++++++++++++++++++++++------------------ Java.md | 4 +- 2 files changed, 175 insertions(+), 80 deletions(-) diff --git a/DB.md b/DB.md index 7235c9b..2710a29 100644 --- a/DB.md +++ b/DB.md @@ -730,9 +730,9 @@ DDL 中的临时表 tmp_table 是在 Server 层创建的,Online DDL 中的临 +参考文章:https://time.geekbang.org/column/article/72388 -参考文章:https://time.geekbang.org/column/article/72388 @@ -3858,7 +3858,7 @@ InnoDB 存储引擎中有页(Page)的概念,页是 MySQL 磁盘管理的 数据页物理结构,从上到下: -* File Header:上一页和下一页的指针、该页的类型(索引页、数据页、日志页等)、**校验和**等信息 +* File Header:上一页和下一页的指针、该页的类型(索引页、数据页、日志页等)、**校验和**、LSN(最近一次修改当前页面时的系统 lsn 值,事务持久性部分详解)等信息 * Page Header:记录状态信息 * Infimum + Supremum:当前页的最小记录和最大记录(头尾指针),Infimum 所在分组只有一条记录,Supremum 所在分组可以有 1 ~ 8 条记录,剩余的分组可以有 4 ~ 8 条记录 * User Records:存储数据的记录 @@ -4274,7 +4274,7 @@ Innodb_xxxx:这几个参数只是针对InnoDB 存储引擎的,累加的算 SQL 执行慢有两种情况: -* 偶尔慢:DB 在刷新脏页 +* 偶尔慢:DB 在刷新脏页(学完事务就懂了) * redo log 写满了 * 内存不够用,要从 LRU 链表中淘汰 * MySQL 认为系统空闲的时候 @@ -4309,7 +4309,7 @@ SQL 执行慢有两种情况: * SHOW PROCESSLIST:**实时查看**当前 MySQL 在进行的连接线程,包括线程的状态、是否锁表、SQL 的执行情况,同时对一些锁表操作进行优化 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SHOW PROCESSLIST命令.png) + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SHOW_PROCESSLIST命令.png) @@ -5272,11 +5272,14 @@ MyISAM 存储引擎使用 key_buffer 缓存索引块,加速 MyISAM 索引的 Buffer Pool 本质上是 InnoDB 向操作系统申请的一段连续的内存空间。InnoDB 的数据是按数据页为单位来读写,每个数据页的大小默认是 16KB。数据是存放在磁盘中,每次读写数据都需要进行磁盘 IO 将数据读入内存进行操作,效率会很低,所以提供了 Buffer Pool 来暂存这些数据页,缓存中的这些页又叫缓冲页 -工作流程: +工作原理: * 从数据库读取数据时,会首先从缓存中读取,如果缓存中没有,则从磁盘读取后放入 Buffer Pool + * 向数据库写入数据时,会首先写入缓存,缓存中修改的数据会**定期刷新**到磁盘,这一过程称为刷脏 + **唯一索引的更新不能使用 Buffer**,一般只有普通索引可以使用,直接写入 Buffer 就结束 + Buffer Pool 中每个缓冲页都有对应的控制信息,包括表空间编号、页号、偏移量、链表信息等,控制信息存放在占用的内存称为控制块,控制块与缓冲页是一一对应的,但并不是物理上相连的,都在缓冲池中 MySQL 提供了缓冲页的快速查找方式:**哈希表**,使用表空间号和页号作为 Key,缓冲页控制块的地址作为 Value 创建一个哈希表,获取数据页时根据 Key 进行哈希寻址: @@ -5286,6 +5289,8 @@ MySQL 提供了缓冲页的快速查找方式:**哈希表**,使用表空间 + + *** @@ -5298,7 +5303,7 @@ MySQL 启动时完成对 Buffer Pool 的初始化,先向操作系统申请连 -基节点:是一块单独申请的内存空间(占 40 字节),并不在Buffer Pool的那一大片连续内存空间里 +基节点:是一块单独申请的内存空间(占 40 字节),并不在 Buffer Pool的那一大片连续内存空间里 磁盘加载页的流程: @@ -5318,7 +5323,7 @@ MySQL 启动时完成对 Buffer Pool 的初始化,先向操作系统申请连 ##### Flush 链表 -Flush 链表是一个用来**存储脏页**的链表,对于已经修改过的缓冲脏页,出于性能考虑并不是直接更新到磁盘,而是在未来的某个时间进行刷脏,所以需要暂时存储所有的脏页 +Flush 链表是一个用来**存储脏页**的链表,对于已经修改过的缓冲脏页,第一次修改后加入到**链表头部**,以后每次修改都不会重新加入,只修改部分控制信息,出于性能考虑并不是直接更新到磁盘,而是在未来的某个时间进行刷脏 @@ -5348,9 +5353,7 @@ Flush 链表是一个用来**存储脏页**的链表,对于已经修改过的 当 Buffer Pool 中没有空闲缓冲页时就需要淘汰掉最近最少使用的部分缓冲页,为了实现这个功能,MySQL 创建了一个 LRU 链表,当访问某个页时: * 如果该页不在 Buffer Pool 中,把该页从磁盘加载进来后会将该缓冲页对应的控制块作为节点放入 **LRU 链表的头部** -* 如果该页在 Buffer Pool 中,则直接把该页对应的控制块移动到 LRU 链表的头部 - -这样操作后 LRU 链表的尾部就是最近最少使用的缓冲页 +* 如果该页在 Buffer Pool 中,则直接把该页对应的控制块移动到 LRU 链表的头部,所以 LRU 链表尾部就是最近最少使用的缓冲页 MySQL 基于局部性原理提供了预读功能: @@ -5369,8 +5372,6 @@ MySQL 基于局部性原理提供了预读功能: - - *** @@ -5405,11 +5406,13 @@ SHOW ENGINE INNODB STATUS\G innodb_log_buffer_size=10M ``` +Buffer Pool 中有一块内存叫 Change Buffer 用来对增删改操作提供缓存,可以通过参数 innodb_change_buffer_max_size 来动态设置,设置为 50 时表示 Change Buffer 的大小最多只能占用 Buffer Pool 的 50% + 在多线程下,访问 Buffer Pool 中的各种链表都需要加锁,所以将 Buffer Pool 拆成若干个小实例,每个实例独立管理内存空间和各种链表(类似 ThreadLocal),多线程访问各实例互不影响,提高了并发能力 * 在系统启动时设置系统变量 `innodb_buffer_pool_instance` 可以指定 Buffer Pool 实例的个数,但是当 Buffer Pool 小于 1GB 时,设置多个实例时无效的 -MySQL 5.7.5 之前 `innodb_buffer_pool_size` 只支持在系统启动时修改,现在已经支持运行时修改 Buffer Pool 的大小,但是每次调整参数都会重新向操作系统申请一块连续的内存空间,将旧的缓冲池的内容拷贝到新空间非常耗时,所以 MySQL 开始以一个 chunk 为单位向操作系统申请内存,所以一个 Buffer Pool 实例由多个 chunk 组成 +MySQL 5.7.5 之前 `innodb_buffer_pool_size` 只支持在系统启动时修改,现在已经支持运行时修改 Buffer Pool 的大小,但是每次调整参数都会重新向操作系统申请一块连续的内存空间,**将旧的缓冲池的内容拷贝到新空间**非常耗时,所以 MySQL 开始以一个 chunk 为单位向操作系统申请内存,所以一个 Buffer Pool 实例由多个 chunk 组成 * 指定系统变量 `innodb_buffer_pool_chunk_size` 来改变 chunk 的大小,只能在启动时修改,运行中不能修改,而且该变量并不包含缓冲页的控制块的内存大小 * `innodb_buffer_pool_size` 必须是 `innodb_buffer_pool_chunk_size × innodb_buffer_pool_instance` 的倍数,默认值是 `128M × 16 = 2G`,Buffer Pool 必须是 2G 的整数倍,如果指定 5G,会自动调整成 6G @@ -5466,9 +5469,15 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 ## 事务机制 -### 管理事务 +### 基本介绍 + +事务(Transaction)是访问和更新数据库的程序执行单元;事务中可能包含一个或多个 SQL 语句,这些语句要么都执行,要么都不执行,作为一个关系型数据库,MySQL 支持事务。 + +单元中的每条 SQL 语句都相互依赖,形成一个整体 -事务(Transaction)是访问和更新数据库的程序执行单元;事务中可能包含一个或多个 sql 语句,这些语句要么都执行,要么都不执行。作为一个关系型数据库,MySQL 支持事务。 +* 如果某条 SQL 语句执行失败或者出现错误,那么整个单元就会回滚,撤回到事务最初的状态 + +* 如果单元中所有的 SQL 语句都执行成功,则事务就顺利执行 事务的四大特征:ACID @@ -5477,13 +5486,27 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 - 隔离性 (isolaction) - 持久性 (durability) -单元中的每条 SQL 语句都相互依赖,形成一个整体 +事务的几种状态: -* 如果某条 SQL 语句执行失败或者出现错误,那么整个单元就会回滚,撤回到事务最初的状态 +* 活动的(active):事务对应的数据库操作正在执行中 +* 部分提交的(partially committed):事务的最后一个操作执行完,但是内存还没刷新至磁盘 +* 失败的(failed):当事务处于活动状态或部分提交状态时,如果数据库遇到了错误或刷脏失败,或者用户主动停止当前的事务 +* 中止的(aborted):失败状态的事务回滚完成后的状态 +* 提交的(committed):当处于部分提交状态的事务刷脏成功,就处于提交状态 -* 如果单元中所有的 SQL 语句都执行成功,则事务就顺利执行 -管理事务的三个步骤 + + + +*** + + + +### 事务管理 + +#### 基本操作 + +事务管理的三个步骤 1. 开启事务:记录回滚点,并通知服务器,将要执行一组操作,要么同时成功、要么同时失败 @@ -5497,13 +5520,14 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 事务操作: -* 开启事务 +* 显式开启事务 ```mysql - START TRANSACTION; + START TRANSACTION [READ ONLY|READ WRITE|WITH CONSISTENT SNAPSHOT]; #可以跟一个或多个状态,最后的是一致性读 + BEGIN [WORK]; ``` -* 回滚事务 +* 回滚事务,用来手动中止事务 ```mysql ROLLBACK; @@ -5515,38 +5539,13 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 COMMIT; ``` - 工作原理: - - * 自动提交模式下,如果没有 start transaction 显式地开始一个事务,那么**每个 SQL 语句都会被当做一个事务执行提交操作** - * 手动提交模式下,所有的 SQL 语句都在一个事务中,直到执行了 commit 或 rollback - - * 存在一些特殊的命令,在事务中执行了这些命令会马上强制执行 COMMIT 提交事务,如 DDL 语句 (create/drop/alter/table)、lock tables 语句等 - - 提交方式语法: - - - 查看事务提交方式 - - ```mysql - SELECT @@AUTOCOMMIT; -- 1 代表自动提交 0 代表手动提交 - ``` - - - 修改事务提交方式 - - ```mysql - SET @@AUTOCOMMIT=数字; -- 系统 - SET AUTOCOMMIT=数字; -- 会话 - ``` - - - **系统变量的操作**: - - ```sql - SET [GLOBAL|SESSION] 变量名 = 值; -- 默认是会话 - SET @@[(GLOBAL|SESSION).]变量名 = 值; -- 默认是系统 - ``` +* 保存点:在事务的执行过程中设置的还原点,调用 ROLLBACK 时可以指定回滚到哪个点 - ```sql - SHOW [GLOBAL|SESSION] VARIABLES [LIKE '变量%']; -- 默认查看会话内系统变量值 - ``` + ```mysql + SAVEPOINT point_name; #设置保存点 + RELEASE point_name #删除保存点 + ROLLBACK [WORK] TO [SAVEPOINT] point_name #回滚至某个保存点,不填默认回滚到事务执行之前的状态 + ``` * 操作演示 @@ -5569,6 +5568,48 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 +*** + + + +#### 提交方式 + +提交方式的相关语法: + +- 查看事务提交方式 + + ```mysql + SELECT @@AUTOCOMMIT; -- 1 代表自动提交 0 代表手动提交 + ``` + +- 修改事务提交方式 + + ```mysql + SET @@AUTOCOMMIT=数字; -- 系统 + SET AUTOCOMMIT=数字; -- 会话 + ``` + +- **系统变量的操作**: + + ```sql + SET [GLOBAL|SESSION] 变量名 = 值; -- 默认是会话 + SET @@[(GLOBAL|SESSION).]变量名 = 值; -- 默认是系统 + ``` + + ```sql + SHOW [GLOBAL|SESSION] VARIABLES [LIKE '变量%']; -- 默认查看会话内系统变量值 + ``` + +工作原理: + +* 自动提交:如果没有 START TRANSACTION 显式地开始一个事务,那么**每条 SQL 语句都会被当做一个事务执行提交操作**;显式开启事务后,会在本次事务结束(提交或回滚)前暂时关闭自动提交 +* 手动提交:不需要显式的开启事务,所有的 SQL 语句都在一个事务中,直到执行了提交或回滚,然后进入下一个事务 +* 隐式提交:存在一些特殊的命令,在事务中执行了这些命令会马上强制执行 COMMIT 提交事务 + * DDL 语句 (CREATE/DROP/ALTER)、LOCK TABLES 语句、LOAD DATA 导入数据语句、主从复制语句等 + * 当一个事务还没提交或回滚,显式的开启一个事务会隐式的提交上一个事务 + + + *** @@ -5898,58 +5939,112 @@ RC、RR 级别下的 InnoDB 快照读区别 ### 持久特性 -#### 重做日志 +#### 持久方式 +持久性是指一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,接下来的其他操作或故障不应该对其有任何影响。 +Buffer Pool 的使用提高了读写数据的效率,但是如果 MySQL 宕机,此时 Buffer Pool 中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证,所以引入了 redo log 日志: +* redo log **记录数据页的物理修改**,而不是某一行或某几行的修改,用来恢复**提交后**的数据页,只能恢复到最后一次提交的位置 +* redo log 采用的是 WAL(Write-ahead logging,**预写式日志**),所有修改要先写入日志,再更新到磁盘,保证了数据不会因 MySQL 宕机而丢失,从而满足了持久性要求 +工作过程:MySQL 发生了宕机,InnoDB 会判断一个数据页在崩溃恢复时丢失了更新,就会将它读到内存,然后根据 redo log 内容更新内存,更新完成后,内存页变成脏页,然后进行刷脏 +缓冲池的**刷脏策略**: -*** +* redo log 文件是固定大小的,如果写满了就要擦除以前的记录,在擦除之前需要把旧记录更新到磁盘中的数据文件中 +* Buffer Pool 内存不足,需要淘汰部分数据页,如果淘汰的是脏页,就要先将脏页写到磁盘(要避免大事务) +* 系统空闲时,后台线程会自动进行刷脏(Flush 链表部分已经详解) +* MySQL 正常关闭时,会把内存的脏页都刷新到磁盘上 -#### 实现原理 +**** -##### 数据恢复 -持久性是指一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,接下来的其他操作或故障不应该对其有任何影响。 -Buffer Pool 是一片内存空间,可以通过 innodb_buffer_pool_size 来控制 Buffer Pool 的大小(内存优化部分会详解参数) +#### 重做日志 -* Change Buffer 是 Buffer Pool 里的内存,不能无限增大,用来对增删改操作提供缓存 -* Change Buffer 的大小可以通过参数 innodb_change_buffer_max_size 来动态设置,设置为 50 时表示 Change Buffer 的大小最多只能占用 Buffer Pool 的 50% -* 补充知识:**唯一索引的更新不能使用 Buffer**,一般只有普通索引可以使用,直接写入 Buffer 就结束 +##### 缓冲区 -Buffer Pool 的使用提高了读写数据的效率,但是也带了新的问题:如果 MySQL 宕机,此时 Buffer Pool 中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证,所以引入 redo log +服务器启动时会向操作系统申请一片连续内存空间作为 redo log buffer(重做日志缓冲区),可以通过 `innodb_log_buffer_size` 系统变量指定 log buffer 的大小,默认是 16MB -* 当数据修改时,先修改 Change Buffer 中的数据,然后在 redo log buffer 记录这次操作 -* 如果 MySQL 宕机,InnoDB 判断一个数据页在崩溃恢复时丢失了更新,就会将它读到内存,然后根据 redo log 内容更新内存,更新完成后,内存页变成脏页,然后进行刷脏(buffer pool 的任务) +log buffer 被划分为若干 redo log block(块,类似数据页的概念),每个默认大小 512 字节,每个 block 由 12 字节的 log block head、496 字节的 log block body、4 字节的 log block trailer 组成 -redo log **记录数据页的物理修改**,而不是某一行或某几行的修改,用来恢复提交后的物理数据页,且只能恢复到最后一次提交的位置 +补充知识:MySQL 规定对底层页面的一次原子访问称为一个 Mini-Transaction(MTR),比如在 B+ 树上插入一条数据就算一个 MTR -redo log 采用的是 WAL(Write-ahead logging,**预写式日志**),所有修改要先写入日志,再更新到磁盘,保证了数据不会因 MySQL 宕机而丢失,从而满足了持久性要求 +* 当数据修改时,先修改 Change Buffer 中的数据,然后在 redo log buffer 记录这次操作,写入 log buffer 的过程是顺序写入的(先写入前面的 block,写满后继续写下一个) +* log buffer 中有一个指针 buf_free,来标识该位置之前都是填满的 block,该位置之后都是空闲区域(碰撞指针) +* 一个事务包含若干个 MTR,一个 MTR 对应一组若干条 redo log,一组 redo log 是不可分割的,所以并不是每生成一条 redo 日志就将其插入到 log buffer 中,而是一个 MTR 结束后**将一组 redo 日志写入 log buffer**,在进行数据恢复时把这一组 redo log 当作一个不可分割的整体处理 redo log 也需要在事务提交时将日志写入磁盘,但是比将内存中的 Buffer Pool 修改的数据写入磁盘的速度快: * 刷脏是随机 IO,因为每次修改的数据位置随机,但写 redo log 是尾部追加操作,属于顺序 IO * 刷脏是以数据页(Page)为单位的,一个页上的一个小修改都要整页写入,而 redo log 中只包含真正需要写入的部分,减少无效 IO -InnoDB 引擎会在适当的时候,把内存中 redo log buffer 持久化到磁盘,具体的刷盘策略: +InnoDB 引擎会在适当的时候,把内存中 redo log buffer 持久化到磁盘,具体的**刷盘策略**: * 通过修改参数 `innodb_flush_log_at_trx_commit` 设置: - * 0:表示当提交事务时,并不将缓冲区的 redo 日志写入磁盘,而是等待主线程每秒刷新一次 - * 1:在事务提交时将缓冲区的 redo 日志**同步写入**到磁盘,保证一定会写入成功 - * 2:在事务提交时将缓冲区的 redo 日志异步写入到磁盘,不能保证提交时肯定会写入,只是有这个动作 -* 如果写入 redo log buffer 的日志已经占据了 redo log buffer 总容量的一半了,此时就会刷入到磁盘文件,这时会影响执行效率,所以开发中应该**避免大事务** + * 0:表示当提交事务时,并不将缓冲区的 redo 日志写入磁盘,而是等待后台线程每秒刷新一次 + * 1:在事务提交时将缓冲区的 redo 日志**同步写入**到磁盘,保证一定会写入成功(默认值) + * 2:在事务提交时将缓冲区的 redo 日志异步写入到磁盘,不能保证提交时肯定会写入,只是有这个动作。已经写入到操作系统的缓存,如果操作系统没有宕机而 MySQL 宕机,也是可以恢复数据的 +* 写入 redo log buffer 的日志超过了总容量的一半,就会将日志刷入到磁盘文件,这会影响执行效率,所以开发中应**避免大事务** +* 服务器关闭时 +* checkpoint 时(下小节详解) + + + +*** -刷脏策略: -* redo log 文件是固定大小的,如果写满了就要擦除以前的记录,在擦除之前需要把旧记录更新到磁盘中的数据文件中 -* Buffer Pool 内存不足,需要淘汰部分数据页,如果淘汰的是脏页,就要先将脏页写到磁盘(大事务) -* 系统空闲时,后台线程会自动进行刷脏 -* MySQL 正常关闭时,会把内存的脏页都 flush 到磁盘上 + +##### 磁盘文件 + +redo 存储在磁盘中的日志文件是被**循环使用**的,redo 日志文件组中每个文件大小一样格式一样,组成结构:前 2048 个字节(前 4 个 block)用来存储一些管理信息,以后的存储 log buffer 中的 block 镜像 + +注意:block 并不代表一组 redo log,一组日志可能占用不到一个 block 或者几个 block,依赖 MTR 的大小 + +磁盘存储 redo log 的文件目录通过 `innodb_log_group_home_dir` 指定,默认是当前数据目录,文件大小: + +* 通过两个参数调节:`innodb_log_file_size` 文件大小默认 48M,`innodb_log_files_in_group` 文件个数默认 2 最大 100 +* 服务器启动后磁盘空间不变,所以采用循环写数据的方式,写完尾部重新写头部,所以要确保头部 log 对应的修改已经持久化到磁盘 + +lsn (log sequence number) 代表已经写入的 redo 日志量、flushed_to_disk_lsn 指刷新到磁盘中的 redo 日志量,两者都是**全局变量**,如果两者的值相同,说明 log buffer 中所有的 redo 日志都已经持久化到磁盘 + +MTR 的执行过程中修改过的页对应的控制块会加到 Buffer Pool 的 flush 链表中,链表中脏页是按照第一次修改的时间进行排序的(头插),控制块中有两个指针用来记录脏页被修改的时间: + +* oldest_modification:第一次修改 Buffer Pool 中某个缓冲页时,将修改该页的 MTR **开始时**对应的 lsn 值写入这个属性,所以链表页是以该值进行排序的 +* newest_modification:每次修改页面,都将 MTR **结束时**对应的 lsn 值写入这个属性,所以是该页面最后一次修改后对应的 lsn 值 + +全局变量 checkpoint_lsn 表示当前系统中可以被覆盖的 redo 日志量,当 redo 日志对应的脏页已经被刷新到磁盘后就可以被覆盖重用,此时执行一次 checkpoint 来更新 checkpoint_lsn 的值存入管理信息,刷脏和执行一次 checkpoint并不是同一个线程 + +使用命令可以查看当前 InnoDB 存储引擎各种 lsn 的值: + +```mysql +SHOW ENGINE INNODB STATUS\G +``` + + + +**** + + + +##### 崩溃恢复 + +恢复的起点:在从 redo 日志文件组的管理信息中获取最近发生 checkpoint 的信息,从 checkpoint_lsn 对应的日志文件开始恢复 + +恢复的终点:扫描日志文件的 block,block 的头部记录着当前 block 使用了多少字节,填满的 block 总是 512 字节, 如果某个 block 不是 512 字节,说明该 block 就是需要恢复的最后一个 block + +恢复的过程:按照 redo log 依次执行恢复数据,优化方式 + +* 使用哈希表:根据 redo log 的 space ID 和 page number 属性计算出哈希值,将对同一页面的修改放入同一个槽里,可以一次性完成对某页的恢复,**避免了随机 IO** +* 跳过已经刷新到磁盘中的页面:数据页的 File Header 中的 FILE_PAGE_LSN 属性(类似 newest_modification)表示最近一次修改页面时的 lsn 值,如果在 checkpoint 后,数据页被刷新到磁盘中,那么该页 lsn 属性肯定大于 checkpoint_lsn + + + +参考书籍:https://book.douban.com/subject/35231266/ @@ -5957,7 +6052,7 @@ InnoDB 引擎会在适当的时候,把内存中 redo log buffer 持久化到 -##### 工作流程 +#### 工作流程 MySQL 中还存在 binlog(二进制日志)也可以记录写操作并用于数据的恢复,**保证数据不丢失**,二者的区别是: diff --git a/Java.md b/Java.md index c1d2f77..dc01f5b 100644 --- a/Java.md +++ b/Java.md @@ -4285,7 +4285,7 @@ LinkedList 是一个实现了 List 接口的**双端链表**,支持高效的 * 获取元素:`get(int index)` 根据指定索引返回数据 - * 获取头节点 (index=0):getFirst()、element()、peek()、peekFirst() 这四个获取头结点方法的区别在于对链表为空时的处理,是抛出异常还是返回null,其中 **getFirst() 和 element()** 方法将会在链表为空时,抛出异常 + * 获取头节点 (index=0):`getFirst()、element()、peek()、peekFirst()` 这四个获取头结点方法的区别在于对链表为空时的处理方式,是抛出异常还是返回NULL,其中 `getFirst() element()` 方法将会在链表为空时,抛出异常 * 获取尾节点 (index=-1):getLast() 方法在链表为空时,抛出 NoSuchElementException,而 peekLast() 不会,只会返回 null * 删除元素: @@ -10209,7 +10209,7 @@ public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制 为对象分配内存:首先计算对象占用空间大小,接着在堆中划分一块内存给新对象 -* 如果内存规整,使用指针碰撞(BumpThePointer)。所有用过的内存在一边,空闲的内存在另外一边,中间有一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离 +* 如果内存规整,使用指针碰撞(Bump The Pointer)。所有用过的内存在一边,空闲的内存在另外一边,中间有一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离 * 如果内存不规整,虚拟机需要维护一个空闲列表(Free List)分配。已使用的内存和未使用的内存相互交错,虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容 From 635916b60153be1a0418e03eaba44ba303346658 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 12 Dec 2021 23:25:14 +0800 Subject: [PATCH 090/168] Update Java Notes --- DB.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/DB.md b/DB.md index 2710a29..e3ef7c8 100644 --- a/DB.md +++ b/DB.md @@ -650,6 +650,8 @@ EXPLAIN 执行计划在优化器阶段生成,如果 explain 的结果预估的 ### 数据空间 +==TODO:本节知识是抄录自《MySQL 实战 45 讲》不作为重点学习目标,暂时记录方便后续有了新的理解后更新知识== + #### 数据存储 系统表空间是用来放系统信息的,比如数据字典什么的,对应的磁盘文件是 ibdata,数据表空间是一个个的表数据文件,对应的磁盘文件就是表名.ibd @@ -5527,6 +5529,8 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 BEGIN [WORK]; ``` + 说明:只读事务不能对普通的表进行增删改操作,但是可以对临时表增删改 + * 回滚事务,用来手动中止事务 ```mysql @@ -5946,8 +5950,8 @@ RC、RR 级别下的 InnoDB 快照读区别 Buffer Pool 的使用提高了读写数据的效率,但是如果 MySQL 宕机,此时 Buffer Pool 中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证,所以引入了 redo log 日志: * redo log **记录数据页的物理修改**,而不是某一行或某几行的修改,用来恢复**提交后**的数据页,只能恢复到最后一次提交的位置 - * redo log 采用的是 WAL(Write-ahead logging,**预写式日志**),所有修改要先写入日志,再更新到磁盘,保证了数据不会因 MySQL 宕机而丢失,从而满足了持久性要求 +* 简单的 redo log 是纯粹的物理日志,负责的 redo log 会存在物理日志和逻辑日志 工作过程:MySQL 发生了宕机,InnoDB 会判断一个数据页在崩溃恢复时丢失了更新,就会将它读到内存,然后根据 redo log 内容更新内存,更新完成后,内存页变成脏页,然后进行刷脏 @@ -5966,17 +5970,17 @@ Buffer Pool 的使用提高了读写数据的效率,但是如果 MySQL 宕机 #### 重做日志 -##### 缓冲区 +##### 日志缓冲 服务器启动时会向操作系统申请一片连续内存空间作为 redo log buffer(重做日志缓冲区),可以通过 `innodb_log_buffer_size` 系统变量指定 log buffer 的大小,默认是 16MB -log buffer 被划分为若干 redo log block(块,类似数据页的概念),每个默认大小 512 字节,每个 block 由 12 字节的 log block head、496 字节的 log block body、4 字节的 log block trailer 组成 - 补充知识:MySQL 规定对底层页面的一次原子访问称为一个 Mini-Transaction(MTR),比如在 B+ 树上插入一条数据就算一个 MTR +log buffer 被划分为若干 redo log block(块,类似数据页的概念),每个默认大小 512 字节,每个 block 由 12 字节的 log block head、496 字节的 log block body、4 字节的 log block trailer 组成 + * 当数据修改时,先修改 Change Buffer 中的数据,然后在 redo log buffer 记录这次操作,写入 log buffer 的过程是顺序写入的(先写入前面的 block,写满后继续写下一个) * log buffer 中有一个指针 buf_free,来标识该位置之前都是填满的 block,该位置之后都是空闲区域(碰撞指针) -* 一个事务包含若干个 MTR,一个 MTR 对应一组若干条 redo log,一组 redo log 是不可分割的,所以并不是每生成一条 redo 日志就将其插入到 log buffer 中,而是一个 MTR 结束后**将一组 redo 日志写入 log buffer**,在进行数据恢复时把这一组 redo log 当作一个不可分割的整体处理 +* 一个事务包含若干个 MTR,一个 MTR 对应一组若干条 redo log,一组 redo log 是不可分割的,所以并不是每生成一条 redo 日志就将其插入到 log buffer 中,而是一个 MTR 结束后**将一组 redo 日志写入 log buffer**,在进行数据恢复时也把这一组 redo log 当作一个不可分割的整体处理 redo log 也需要在事务提交时将日志写入磁盘,但是比将内存中的 Buffer Pool 修改的数据写入磁盘的速度快: @@ -5985,7 +5989,7 @@ redo log 也需要在事务提交时将日志写入磁盘,但是比将内存 InnoDB 引擎会在适当的时候,把内存中 redo log buffer 持久化到磁盘,具体的**刷盘策略**: -* 通过修改参数 `innodb_flush_log_at_trx_commit` 设置: +* 在事务提交时需要进行刷盘,通过修改参数 `innodb_flush_log_at_trx_commit` 设置: * 0:表示当提交事务时,并不将缓冲区的 redo 日志写入磁盘,而是等待后台线程每秒刷新一次 * 1:在事务提交时将缓冲区的 redo 日志**同步写入**到磁盘,保证一定会写入成功(默认值) * 2:在事务提交时将缓冲区的 redo 日志异步写入到磁盘,不能保证提交时肯定会写入,只是有这个动作。已经写入到操作系统的缓存,如果操作系统没有宕机而 MySQL 宕机,也是可以恢复数据的 From c98186e0d4b99f8d80ebbecd06f05e30af95c8f6 Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 15 Dec 2021 00:51:39 +0800 Subject: [PATCH 091/168] Update Java Notes --- DB.md | 147 +++++++++++++++++++++++++++++++++++++++++++++++++------- Java.md | 15 ++++-- 2 files changed, 141 insertions(+), 21 deletions(-) diff --git a/DB.md b/DB.md index e3ef7c8..48cb370 100644 --- a/DB.md +++ b/DB.md @@ -650,8 +650,6 @@ EXPLAIN 执行计划在优化器阶段生成,如果 explain 的结果预估的 ### 数据空间 -==TODO:本节知识是抄录自《MySQL 实战 45 讲》不作为重点学习目标,暂时记录方便后续有了新的理解后更新知识== - #### 数据存储 系统表空间是用来放系统信息的,比如数据字典什么的,对应的磁盘文件是 ibdata,数据表空间是一个个的表数据文件,对应的磁盘文件就是表名.ibd @@ -685,7 +683,7 @@ InnoDB 的数据是按页存储的如果删掉了一个数据页上的所有记 -#### 空间收缩 +#### 重建数据 重建表就是按照主键 ID 递增的顺序,把数据一行一行地从旧表中读出来再插入到新表中,重建表时 MySQL 会自动完成转存数据、交换表名、删除旧表的操作,重建命令: @@ -709,7 +707,7 @@ MySQL 5.6 版本开始引入的 Online DDL,重建表的命令默认执行此 Online DDL 操作会先获取 MDL 写锁,再退化成 MDL 读锁。但 MDL 写锁持有时间比较短,所以可以称为 Online; 而 MDL 读锁,不阻止数据增删查改,但会阻止其它线程修改表结构 -问题:想要收缩表空间,执行指令后整体占用空间增大 +问题:重建表可以收缩表空间,但是执行指令后整体占用空间增大 原因:在重建表后 InnoDB 不会把整张表占满,每个页留了 1/16 给后续的更新使用。表在未整理之前页已经占用 15/16 以上,收缩之后需要保持数据占用空间在 15/16,所以文件占用空间更大才能保持 @@ -732,6 +730,8 @@ DDL 中的临时表 tmp_table 是在 Server 层创建的,Online DDL 中的临 +==本节知识是抄录自《MySQL 实战 45 讲》,作者目前没有更深的理解,暂时记录,后续有了新的认知后会更新知识== + 参考文章:https://time.geekbang.org/column/article/72388 @@ -3791,7 +3791,7 @@ InnoDB 使用 B+Tree 作为索引结构,并且 InnoDB 一定有索引 * 在 InnoDB 中,表数据文件本身就是按 B+Tree 组织的一个索引结构,这个索引的 key 是数据表的主键,叶子节点 data 域保存了完整的数据记录 -* InnoDB 的表数据文件**通过主键聚集数据**,如果没有定义主键,会选择非空唯一索引代替,如果也没有这样的列,MySQL 会自动为 InnoDB 表生成一个**隐含字段**作为主键,这个字段长度为 6 个字节,类型为长整形(MVCC 部分的笔记提及) +* InnoDB 的表数据文件**通过主键聚集数据**,如果没有定义主键,会选择非空唯一索引代替,如果也没有这样的列,MySQL 会自动为 InnoDB 表生成一个**隐含字段 row_id** 作为主键,这个字段长度为 6 个字节,类型为长整形 辅助索引: @@ -3868,6 +3868,10 @@ InnoDB 存储引擎中有页(Page)的概念,页是 MySQL 磁盘管理的 * Page Directory:分组的目录,可以通过目录快速定位(二分法)数据的分组 * File Trailer:检验和字段,在刷脏过程中,页首和页尾的校验和一致才能说明页面刷新成功,二者不同说明刷新期间发生了错误;LSN 字段,也是用来校验页面的完整性 +数据页中包含数据行,数据的存储是基于数据行的,数据行有 next_record 属性指向下一个行数据,所以是可以遍历的,但是一组数据至多 8 个行,通过 Page Directory 先定位到组,然后遍历获取所需的数据行即可 + +数据行中有三个隐藏字段:trx_id、roll_pointer、row_id(在事务章节会详细介绍它们的作用) + *** @@ -5529,7 +5533,7 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 BEGIN [WORK]; ``` - 说明:只读事务不能对普通的表进行增删改操作,但是可以对临时表增删改 + 说明:不填状态默认是读写事务 * 回滚事务,用来手动中止事务 @@ -5614,6 +5618,26 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 +**** + + + +#### 事务 ID + +只读事务不能对普通的表进行增删改操作,但是可以对临时表增删改,读写事务可以对数据表执行增删改查操作 + +事务在执行过程中对某个表执行了**增删改操作或者创建表**,就会为当前事务分配一个独一无二的事务 ID(对临时表并不会分配 ID),如果当前事务没有被分配 ID,默认是 0 + +事务 ID 本质上就是一个数字,服务器在内存中维护一个全局变量: + +* 每当需要为某个事务分配 ID,就会把全局变量的值赋值给事务 ID,然后变量自增 1 +* 每当变量值为 256 的倍数时,就将该变量的值刷新到系统表空间的 Max Trx ID 属性中,该属性占 8 字节 +* 系统再次启动后,会读取表空间的 Max Trx ID 属性到内存,加上 256 后赋值给全局变量,因为关机时的事务 ID 可能并不是 256 的倍数,会比 Max Trx ID 大,所以需要加上 256 保持事务 ID 是一个递增的数字 + +**聚簇索引**的行记录除了完整的数据,还会自动添加 trx_id、roll_pointer 隐藏列,如果表中没有主键并且没有非空唯一索引,也会添加一个 row_id 的隐藏列作为聚簇索引 + + + *** @@ -5669,6 +5693,8 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 ### 原子特性 +#### 实现方式 + 原子性是指事务是一个不可分割的工作单位,事务的操作如果成功就必须要完全应用到数据库,失败则不能对数据库有任何影响。比如事务中一个 SQL 语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态 InnoDB 存储引擎提供了两种事务日志:redo log(重做日志)和 undo log(回滚日志) @@ -5686,16 +5712,100 @@ undo log 属于逻辑日志,根据每行操作进行记录,记录了 SQL 执 * 对于每个 update,回滚时会执行一个相反的 update,把数据修改回去 -undo log 是采用段(segment)的方式来记录,每个 undo 操作在记录的时候占用一个 undo log segment -rollback segment 称为回滚段,每个回滚段中有 1024 个 undo log segment -* 在以前老版本,只支持 1 个 rollback segment,只能记录 1024 个 undo log segment -* MySQL5.5 开始支持 128 个 rollback segment,支持 128*1024 个 undo 操作 +参考文章:https://www.cnblogs.com/kismetv/p/10331633.html -参考文章:https://www.cnblogs.com/kismetv/p/10331633.html +*** + + + +#### DML 解析 + +##### INSERT + +乐观插入:当前数据页的剩余空间充足,直接将数据进行插入 + +悲观插入:当前数据页的剩余空间不足,需要进行页分裂,申请一个新的页面来插入数据,会造成更多的 redo log,undo log 影响不大 + +当向某个表插入一条记录,实际上需要向聚簇索引和所有二级索引都插入一条记录,但是 undo log 只需要针对聚簇索引记录,在回滚时会根据聚簇索引去所有的二级索引进行回滚操作 + +roll_pointer 是一个指针,**指向记录对应的 undo log 日志**,一条记录就是一个数据行,行格式中的 roll_pointer 就指向 undo log + + + +*** + + + +##### DELETE + +插入到页面中的记录会根据 next_record 属性组成一个单向链表,这个链表称为正常链表,被删除的记录也会通过 next_record 组成一个垃圾链表,该链表中所占用的存储空间可以被重新利用,并不会直接清除数据 + +在页面 Page Header 中,PAGE_FREE 属性指向垃圾链表的头节点,删除的工作过程: + +* 将要删除的记录的 delete_flag 位置为 1,其他不做修改,这个过程叫 **delete mark** +* 在事务提交前,delete_flag = 1 的记录一直都会处于中间状态 +* 事务提交后,有专门的线程将 delete_flag = 1 的记录从正常链表移除并加入垃圾链表,这个过程叫 **purge** + +在对一条记录 delete mark 前会将记录的隐藏列 trx_id 和 roll_pointer 的旧值记录到 undo log 对应的属性中,这样就会产生记录的 roll_pointer 指向当前 undo log 记录,当前 undo log 记录的 roll_pointer 指向旧的 undo log 记录,**形成一个版本链** + +当有新插入的记录时,首先判断 PAGE_FREE 指向的头节点是否足够容纳新纪录: + +* 如果可以容纳新纪录,就会直接重用已删除的记录的存储空间,然后让 PAGE_FREE 指向垃圾链表的下一个节点 +* 如果不能容纳新纪录,就直接向页面申请新的空间存储,并不会遍历垃圾链表 + +重用已删除的记录空间,可能会造成空间碎片,当数据页容纳不了一条记录时,会判断将碎片空间加起来是否可以容纳,判断为真就会重新组织页内的记录: + +* 开辟一个临时页面,将页内记录一次插入到临时页面,此时临时页面时没有碎片的 +* 把临时页面的内容复制到本页,这样就解放出了内存碎片,但是会耗费很大的性能资源 + + + +**** + + + +##### UPDATE + +执行 UPDATE 语句,对于更新主键和不更新主键有两种不同的处理方式 + +不更新主键的情况: + +* 就地更新(in-place update),如果更新后的列和更新前的列占用的存储空间一样大,就可以直接在原记录上修改 + +* 先删除旧纪录,再插入新纪录,这里的删除不是 delete mark,而是直接将记录加入垃圾链表,并且修改页面的相应的控制信息,执行删除的线程不是 purge,是执行更新的用户线程 + + 插入新记录时可能造成页空间不足,从而导致页分裂 + +更新主键的情况: + +* 将旧纪录进行 delete mark,在更新语句提交后由 purge 线程移入垃圾链表 +* 根据更新的各列的值创建一条新纪录,插入到聚簇索引中 + + + +*** + + + +#### 回滚日志 + +undo log 是采用段(segment)的方式来记录,每个 undo 操作在记录的时候占用一个 undo log segment + +Rollback Segement 称为回滚段,每个回滚段中有 1024 个 undo slot + +* 在以前老版本,只支持 1 个 Rollback Segement,只能记录 1024 个 undo log segment +* MySQL5.5 开始支持 128 个 Rollback Segement,支持 128*1024 个 undo 操作 + +工作流程: + +* 事务执行前需要到系统表空间第 5 号页面中分配一个回滚段(页),获取一个 Rollback Segement Header 页面的地址 +* 回滚段页面有 1024 个 undo slot,每个 slot 存放 undo 链表页面的头节点页号。首先去回滚段的两个 cached 链表看是否有缓存的 slot,缓存中没有就在回滚段中找一个可用的 slot +* 缓存中获取的 slot 对应的 Undo Log Segment 已经分配了,需要重新分配,然后从 Undo Log Segment 中申请一个页面作为日志链表的头节点,并填入对应的 slot 中 +* 开始记录 @@ -5817,11 +5927,6 @@ undo log 主要分为两种: * 事务 1 修改该行数据时,数据库会先对该行加排他锁,然后先记录 undo log,然后修改该行 name 为 Tom,并且修改隐藏字段的事务 ID 为当前事务 1 的 ID(默认为 1 之后递增),回滚指针指向拷贝到 undo log 的副本记录,事务提交后,释放锁 * 以此类推 -补充知识:purge 线程 - -* 为了实现 InnoDB 的 MVCC 机制,更新或者删除操作都只是设置一下老记录的 deleted_bit,并不真正将过时的记录删除,为了节省磁盘空间,InnoDB 有专门的 purge 线程来清理 deleted_bit 为 true 的记录 -* purge 线程维护了一个 Read view(这个 Read view 相当于系统中最老活跃事务的 Read view),如果某个记录的 deleted_bit 为 true,并且 DB_TRX_ID 相对于 purge 线程的 Read view 可见,那么这条记录一定是可以被安全清除的 - *** @@ -5943,7 +6048,7 @@ RC、RR 级别下的 InnoDB 快照读区别 ### 持久特性 -#### 持久方式 +#### 实现方式 持久性是指一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,接下来的其他操作或故障不应该对其有任何影响。 @@ -6021,7 +6126,9 @@ MTR 的执行过程中修改过的页对应的控制块会加到 Buffer Pool 的 * oldest_modification:第一次修改 Buffer Pool 中某个缓冲页时,将修改该页的 MTR **开始时**对应的 lsn 值写入这个属性,所以链表页是以该值进行排序的 * newest_modification:每次修改页面,都将 MTR **结束时**对应的 lsn 值写入这个属性,所以是该页面最后一次修改后对应的 lsn 值 -全局变量 checkpoint_lsn 表示当前系统中可以被覆盖的 redo 日志量,当 redo 日志对应的脏页已经被刷新到磁盘后就可以被覆盖重用,此时执行一次 checkpoint 来更新 checkpoint_lsn 的值存入管理信息,刷脏和执行一次 checkpoint并不是同一个线程 +全局变量 checkpoint_lsn 表示当前系统中可以被覆盖的 redo 日志量,当 redo 日志对应的脏页已经被刷新到磁盘后就可以被覆盖重用,此时执行一次 checkpoint 来更新 checkpoint_lsn 的值存入管理信息,刷脏和执行一次 checkpoint 并不是同一个线程 + +在系统忙碌时,后台线程的刷脏操作不能将脏页快速刷出,导致系统无法及时执行 checkpoint,这时需要用户线程从 flush 链表中把最早修改的脏页刷新到磁盘中,然后执行 checkpoint 使用命令可以查看当前 InnoDB 存储引擎各种 lsn 的值: @@ -6046,6 +6153,10 @@ SHOW ENGINE INNODB STATUS\G * 使用哈希表:根据 redo log 的 space ID 和 page number 属性计算出哈希值,将对同一页面的修改放入同一个槽里,可以一次性完成对某页的恢复,**避免了随机 IO** * 跳过已经刷新到磁盘中的页面:数据页的 File Header 中的 FILE_PAGE_LSN 属性(类似 newest_modification)表示最近一次修改页面时的 lsn 值,如果在 checkpoint 后,数据页被刷新到磁盘中,那么该页 lsn 属性肯定大于 checkpoint_lsn +问题:系统崩溃前没有提交的事务的 redo log 可能已经刷盘,这些数据可能在重启后也会恢复 + +解决:通过 undo log 在服务器重启时将未提交的事务回滚掉,定位到 128 个回滚段,遍历 slot,获取 undo 链表首节点页面的 Undo Segement Header 中的 TRX_UNDO_STATE 属性,表示当前链表的事务属性,如果是活跃的就全部回滚 + 参考书籍:https://book.douban.com/subject/35231266/ diff --git a/Java.md b/Java.md index dc01f5b..e3ddbb2 100644 --- a/Java.md +++ b/Java.md @@ -19,6 +19,15 @@ +初学时笔记内容参考视频:https://www.bilibili.com/video/BV1TE41177mP,后随着学习的深入逐渐增加了很多知识 + +给初学者的一些个人建议: + +* 初学者对编程的认知比较浅显,一些专有词汇和概念难以理解,所以建议观看视频进行入门,大部分公开课视频讲的比较基础 +* 在有了一定的编程基础后,需要看一些经典书籍和技术博客,来扩容自己的知识广度和深度,可以长期保持记录笔记的好习惯 + + + *** @@ -140,8 +149,8 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, ``` - - + + *** @@ -4464,7 +4473,7 @@ public class Student implements Comparable{ } ``` -比较器原理:底层是以第一个元素为基准,加一个新元素,就会和第一个元素比,如果大于,就继续和大于的元素进行比较,直到遇到比新元素大的元素为止,放在该位置的左边。(树) +比较器原理:底层是以第一个元素为基准,加一个新元素,就会和第一个元素比,如果大于,就继续和大于的元素进行比较,直到遇到比新元素大的元素为止,放在该位置的左边。(红黑树) From 8ad135fd9c29437e0aac3aa128b0b0e293220ec1 Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 15 Dec 2021 23:24:20 +0800 Subject: [PATCH 092/168] Update Java Notes --- DB.md | 147 +++++++++++++++++++++++++++++++++----------------------- Java.md | 8 +-- 2 files changed, 92 insertions(+), 63 deletions(-) diff --git a/DB.md b/DB.md index 48cb370..45d46c7 100644 --- a/DB.md +++ b/DB.md @@ -379,7 +379,32 @@ mysqlshow -uroot -p1234 test book --count #### 连接器 -池化技术:对于访问数据库来说,建立连接的代价是比较昂贵的,频繁的创建关闭连接比较耗费资源,有必要建立数据库连接池,以提高访问的性能 +##### 连接原理 + +池化技术:对于访问数据库来说,建立连接的代价是比较昂贵的,因为每个连接对应一个用来交互的线程,频繁的创建关闭连接比较耗费资源,有必要建立数据库连接池,以提高访问的性能 + +连接建立 TCP 以后需要做**权限验证**,验证成功后可以进行执行 SQL。如果这时管理员账号对这个用户的权限做了修改,也不会影响已经存在连接的权限,只有再新建的连接才会使用新的权限设置 + +客户端如果长时间没有操作,连接器就会自动断开,时间是由参数 wait_timeout 控制的,默认值是 8 小时。如果在连接被断开之后,客户端再次发送请求的话,就会收到一个错误提醒:`Lost connection to MySQL server during query` + +数据库里面,长连接是指连接成功后,如果客户端持续有请求,则一直使用同一个连接;短连接则是指每次执行完很少的几次查询就断开连接,下次查询再重新建立一个。为了减少连接的创建,推荐使用长连接,但是过多的长连接会造成 OOM,解决方案: + +* 定期断开长连接,使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开连接,之后要查询再重连。 +* MySQL 5.7 版本,可以在每次执行一个比较大的操作后,通过执行 mysql_reset_connection 来重新初始化连接资源,这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态 + +MySQL 服务器可以同时和多个客户端进行交互,所以要保证每个连接会话的隔离性(事务机制部分详解) + +整体的执行流程: + + + + + +**** + + + +##### 连接状态 首先连接到数据库上,这时连接器发挥作用,连接完成后如果没有后续的动作,这个连接就处于空闲状态,通过指令查看连接状态 @@ -398,19 +423,6 @@ SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 | State | 显示使用当前连接的 sql 语句的状态,以查询为例,需要经过 copying to tmp table、sorting result、sending data等状态才可以完成 | | Info | 显示执行的 sql 语句,是判断问题语句的一个重要依据 | -连接建立 TCP 以后需要做**权限验证**,验证成功后可以进行执行 SQL。如果这时管理员账号对这个用户的权限做了修改,也不会影响已经存在连接的权限,只有再新建的连接才会使用新的权限设置 - -客户端如果太长时间没动静,连接器就会自动断开,这个时间是由参数 wait_timeout 控制的,默认值是 8 小时。如果在连接被断开之后,客户端再次发送请求的话,就会收到一个错误提醒:`Lost connection to MySQL server during query` - -数据库里面,长连接是指连接成功后,如果客户端持续有请求,则一直使用同一个连接;短连接则是指每次执行完很少的几次查询就断开连接,下次查询再重新建立一个。为了减少连接的创建,推荐使用长连接,但是过多的长连接会造成 OOM,解决方案: - -* 定期断开长连接,使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开连接,之后要查询再重连。 -* MySQL 5.7 版本,可以在每次执行一个比较大的操作后,通过执行 mysql_reset_connection 来重新初始化连接资源,这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态 - - - - - *** @@ -5281,10 +5293,10 @@ Buffer Pool 本质上是 InnoDB 向操作系统申请的一段连续的内存空 工作原理: * 从数据库读取数据时,会首先从缓存中读取,如果缓存中没有,则从磁盘读取后放入 Buffer Pool - * 向数据库写入数据时,会首先写入缓存,缓存中修改的数据会**定期刷新**到磁盘,这一过程称为刷脏 - **唯一索引的更新不能使用 Buffer**,一般只有普通索引可以使用,直接写入 Buffer 就结束 + +**唯一索引的更新不能使用 Buffer,只有普通索引可以使用,直接写入 Buffer 就结束,不用校验唯一性** Buffer Pool 中每个缓冲页都有对应的控制信息,包括表空间编号、页号、偏移量、链表信息等,控制信息存放在占用的内存称为控制块,控制块与缓冲页是一一对应的,但并不是物理上相连的,都在缓冲池中 @@ -5649,26 +5661,29 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 隔离级别分类: -| 隔离级别 | 名称 | 会引发的问题 | 数据库默认隔离级别 | -| ---------------- | -------- | -------------------------------- | ------------------- | -| read uncommitted | 读未提交 | 脏读、不可重复读、幻读 | | -| read committed | 读已提交 | 不可重复读、幻读 | Oracle / SQL Server | -| repeatable read | 可重复读 | 幻读 | MySQL | -| serializable | 串行化 | 无(因为写会加写锁,读会加读锁) | | +| 隔离级别 | 名称 | 会引发的问题 | 数据库默认隔离级别 | +| ---------------- | -------- | ---------------------- | ------------------- | +| read uncommitted | 读未提交 | 脏读、不可重复读、幻读 | | +| read committed | 读已提交 | 不可重复读、幻读 | Oracle / SQL Server | +| repeatable read | 可重复读 | 幻读 | MySQL | +| serializable | 可串行化 | 无 | | + +* 串行化:让所有事务按顺序单独执行,写操作会加写锁,读操作会加读锁 +* 可串行化:让所有操作相同数据的事务顺序执行,通过加锁实现 一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差 -* 丢失更新 (Lost Update):当两个或多个事务选择同一行,最初的事务修改的值,被后面事务修改的值覆盖,所有的隔离级别都可以避免丢失更新(行锁) +* 脏写 (Dirty Write):当两个或多个事务选择同一行,最初的事务修改的值被后面事务修改的值覆盖,所有的隔离级别都可以避免脏写(又叫丢失更新),因为有行锁 -* 脏读 (Dirty Reads):在事务中先后两次读取同一个数据,两次读取的结果不一样,原因是在一个事务处理过程中读取了另一个**未提交**的事务中的数据 +* 脏读 (Dirty Reads):在一个事务处理过程中读取了另一个**未提交**的事务中修改过的数据 -* 不可重复读 (Non-Repeatable Reads):在事务中先后两次读取同一个数据,两次读取的结果不一样,原因是在一个事务处理过程中读取了另一个事务中修改并**已提交**的数据 +* 不可重复读 (Non-Repeatable Reads):在一个事务处理过程中读取了另一个事务中修改并**已提交**的数据 > 可重复读的意思是不管读几次,结果都一样,可以重复的读,可以理解为快照读,要读的数据集不会发生变化 * 幻读 (Phantom Reads):在事务中按某个条件先后两次查询数据库,后一次查询查到了前一次查询没有查到的行,**数据条目**发生了变化。比如查询某数据不存在,准备插入此记录,但执行插入时发现此记录已存在,无法插入 -**隔离级别操作语法:** +隔离级别操作语法: * 查询数据库隔离级别 @@ -5750,7 +5765,7 @@ roll_pointer 是一个指针,**指向记录对应的 undo log 日志**,一 * 在事务提交前,delete_flag = 1 的记录一直都会处于中间状态 * 事务提交后,有专门的线程将 delete_flag = 1 的记录从正常链表移除并加入垃圾链表,这个过程叫 **purge** -在对一条记录 delete mark 前会将记录的隐藏列 trx_id 和 roll_pointer 的旧值记录到 undo log 对应的属性中,这样就会产生记录的 roll_pointer 指向当前 undo log 记录,当前 undo log 记录的 roll_pointer 指向旧的 undo log 记录,**形成一个版本链** + purge 线程在执行删除操作时会创建一个 ReadView,根据事务的可见性移除数据(隔离特性部分详解) 当有新插入的记录时,首先判断 PAGE_FREE 指向的头节点是否足够容纳新纪录: @@ -5785,6 +5800,10 @@ roll_pointer 是一个指针,**指向记录对应的 undo log 日志**,一 * 将旧纪录进行 delete mark,在更新语句提交后由 purge 线程移入垃圾链表 * 根据更新的各列的值创建一条新纪录,插入到聚簇索引中 +在对一条记录修改前会**将记录的隐藏列 trx_id 和 roll_pointer 的旧值记录到 undo log 对应的属性中**,这样就记录的 roll_pointer 指向当前 undo log 记录,当前 undo log 记录的 roll_pointer 指向旧的 undo log 记录,**形成一个版本链** + + + *** @@ -5853,7 +5872,7 @@ MVCC 处理读写请求,可以做到在发生读写请求冲突时不用加锁 * 读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读 -* 写-写:有线程安全问题,可能会存在丢失更新问题 +* 写-写:有线程安全问题,可能会存在脏写(丢失更新)问题 MVCC 的优点: @@ -5881,15 +5900,11 @@ MVCC 的优点: 实现原理主要是隐藏字段,undo日志,Read View 来实现的 -数据库中的每行数据,除了自定义的字段,还有数据库隐式定义的字段: - -* DB_TRX_ID:6byte,最近修改事务ID,记录创建该数据或最后一次修改(修改/插入)该数据的事务ID。当每个事务开启时,都会被分配一个ID,这个 ID 是递增的 - -* DB_ROLL_PTR:7byte,回滚指针,配合 undo 日志,指向上一个旧版本(存储在 rollback segment) - -* DB_ROW_ID:6byte,隐含的自增ID(**隐藏主键**),如果数据表没有主键,InnoDB 会自动以 DB_ROW_ID 作为聚簇索引 +数据库中的**聚簇索引**每行数据,除了自定义的字段,还有数据库隐式定义的字段: -* DELETED_BIT:删除标志的隐藏字段,记录被更新或删除并不代表真的删除,而是删除位变了 +* DB_TRX_ID:最近修改事务 ID,记录创建该数据或最后一次修改该数据的事务 ID +* DB_ROLL_PTR:回滚指针,**指向记录对应的 undo log 日志**,undo log 中又指向上一个旧版本的 undo log +* DB_ROW_ID:隐含的自增 ID(**隐藏主键**),如果数据表没有主键,InnoDB 会自动以 DB_ROW_ID 作为聚簇索引 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MVCC版本链隐藏字段.png) @@ -5897,15 +5912,13 @@ MVCC 的优点: - - *** -##### undo +##### 版本链 -undo log 是逻辑日志,记录的是每个事务对数据执行的操作,而不是记录的全部数据,需要根据 undo log 逆推出以往事务的数据 +undo log 是逻辑日志,记录的是每个事务对数据执行的操作,而不是记录的全部数据,要**根据 undo log 逆推出以往事务的数据** undo log 的作用: @@ -5918,12 +5931,15 @@ undo log 主要分为两种: * update undo log:事务在进行 update 或 delete 时产生的 undo log,在事务回滚时需要,在快照读时也需要。不能随意删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被 purge 线程统一清除 -每次对数据库记录进行改动,都会将旧值放到一条 undo 日志中,算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,把这个链表称之为**版本链**,版本链的头节点就是当前记录最新的值,链尾就是最早的旧记录 +每次对数据库记录进行改动,都会产生的新版本的 undo log,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,把这个链表称之为**版本链**,版本链的头节点就是当前的最新的 undo log,链尾就是最早的旧 undo log -* 有个事务插入 persion 表一条新记录,name 为 Jerry,age 为 24 +补充:undo 是逻辑日志,这里只是直观的展示出来 +工作流程: + +* 有个事务插入 persion 表一条新记录,name 为 Jerry,age 为 24 * 事务 1 修改该行数据时,数据库会先对该行加排他锁,然后先记录 undo log,然后修改该行 name 为 Tom,并且修改隐藏字段的事务 ID 为当前事务 1 的 ID(默认为 1 之后递增),回滚指针指向拷贝到 undo log 的副本记录,事务提交后,释放锁 * 以此类推 @@ -5935,26 +5951,26 @@ undo log 主要分为两种: ##### 读视图 -Read View 是事务进行**快照读**操作时产生的读视图,该事务执行快照读的那一刻会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的 ID,用来做可见性判断,根据视图判断当前事务能够看到哪个版本的数据 +Read View 是事务进行读数据操作时产生的读视图,该事务执行快照读的那一刻会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的 ID,用来做可见性判断,根据视图判断当前事务能够看到哪个版本的数据 注意:这里的快照并不是把所有的数据拷贝一份副本,而是由 undo log 记录的逻辑日志,根据库中的数据进行计算出历史数据 -工作流程:将版本链的头节点的事务 ID(最新数据事务 ID)DB_TRX_ID 取出来,与系统当前活跃事务的 ID 对比,如果 DB_TRX_ID 不符合可见性,通过 DB_ROLL_PTR 回滚指针去取出 undo log 中的下一个 DB_TRX_ID 比较,直到找到最近的满足特定条件的 DB_TRX_ID,该事务 ID 所在的旧记录就是当前事务能看见的最新的记录 +工作流程:将版本链的头节点的事务 ID(最新数据事务 ID)DB_TRX_ID 取出来,与系统当前活跃事务的 ID 对比进行可见性分析,不可见就通过 DB_ROLL_PTR 回滚指针去取出 undo log 中的下一个 DB_TRX_ID 比较,直到找到最近的满足可见性的 DB_TRX_ID,该事务 ID 所在的旧记录就是当前事务能看见的最新的记录 Read View 几个属性: - m_ids:生成 Read View 时当前系统中活跃的事务 id 列表(未提交的事务集合,当前事务也在其中) -- up_limit_id:生成 Read View 时当前系统中活跃的最小的事务 id,也就是 m_ids 中的最小值(已提交的事务集合) -- low_limit_id:生成 Read View 时系统应该分配给下一个事务的 id 值,m_ids 中的最大值加 1(未开始事务) +- min_trx_id:生成 Read View 时当前系统中活跃的最小的事务 id,也就是 m_ids 中的最小值(已提交的事务集合) +- max_trx_id:生成 Read View 时当前系统应该分配给下一个事务的 id 值,m_ids 中的最大值加 1(未开始事务) - creator_trx_id:生成该 Read View 的事务的事务 id,就是判断该 id 的事务能读到什么数据 creator 创建一个 Read View,进行可见性算法分析:(解决了读未提交) -* db_trx_id == creator_trx_id:表示这个数据就是当前事务自己生成的,自己生成的数据自己肯定能看见,所以这种情况下此数据对 creator 是可见的 -* db_trx_id < up_limit_id:该版本对应的事务 ID 小于 Read view 中的最小活跃事务 ID,则这个事务在当前事务之前就已经被提交了,对 creator 可见 +* db_trx_id == creator_trx_id:表示这个数据就是当前事务自己生成的,自己生成的数据自己肯定能看见,所以此数据对 creator 是可见的 +* db_trx_id < min_trx_id:该版本对应的事务 ID 小于 Read view 中的最小活跃事务 ID,则这个事务在当前事务之前就已经被提交了,对 creator 可见 -* db_trx_id >= low_limit_id:该版本对应的事务 ID 大于 Read view 中当前系统的最大事务 ID,则说明该数据是在当前 Read view 创建之后才产生的,对 creator 不可见 -* up_limit_id <= db_trx_id < low_limit_id:判断 db_trx_id 是否在活跃事务列表 m_ids 中 +* db_trx_id >= max_trx_id:该版本对应的事务 ID 大于 Read view 中当前系统的最大事务 ID,则说明该数据是在当前 Read view 创建之后才产生的,对 creator 不可见 +* min_trx_id<= db_trx_id < max_trx_id:判断 db_trx_id 是否在活跃事务列表 m_ids 中 * 在列表中,说明该版本对应的事务正在运行,数据不能显示(**不能读到未提交的数据**) * 不在列表中,说明该版本对应的事务已经被提交,数据可以显示(**可以读到已经提交的数据**) @@ -5993,8 +6009,8 @@ START TRANSACTION; -- 开启事务 ID 为 0 的事务创建 Read View: * m_ids:20、60 -* up_limit_id:20 -* low_limit_id:61 +* min_trx_id:20 +* max_trx_id:61 * creator_trx_id:0 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MVCC工作流程2.png) @@ -6011,28 +6027,41 @@ ID 为 0 的事务创建 Read View: +##### 二级索引 + +只有在聚簇索引中才有 trx_id 和 roll_pointer 的隐藏列,对于二级索引判断可见性的方式: + +* 二级索引页面的 Page Header 中有一个 PAGE_MAX_TRX_ID 属性,代表修改当前页面的最大的事务 ID,SELECT 语句访问某个二级索引时会判断 ReadView 的 min_trx_id 是否大于该属性,大于说明该页面的所有属性对 ReadView 可见 +* 如果属性判断不可见,就需要利用二级索引获取主键值,进行回表操作,得到聚簇索引后再按照聚簇索引的可见性判断的方法操作 + + + +*** + + + #### RC RR Read View 用于支持 RC(Read Committed,读已提交)和 RR(Repeatable Read,可重复读)隔离级别的实现 RR、RC 生成时机: -- RC 隔离级别下,每个快照读都会生成并获取最新的 Read View -- RR 隔离级别下,则是同一个事务中的第一个快照读才会创建 Read View +- RC 隔离级别下,每次读取数据前都会生成最新的 Read View(当前读) +- RR 隔离级别下,在第一次数据读取时才会创建 Read View(快照读) RC、RR 级别下的 InnoDB 快照读区别 -- RC 级别下的,事务中每次快照读都会新生成一个 Read View,这就是在 RC 级别下的事务中可以看到别的事务提交的更新的原因 +- RC 级别下,事务中每次快照读都会新生成一个 Read View,这就是在 RC 级别下的事务中可以看到别的事务提交的更新的原因 -- RR 级别下的某个事务的对某条记录的第一次快照读会创建一个 Read View, 将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,使用的是同一个Read View,所以一个事务的查询结果每次都是相同的 +- RR 级别下,某个事务的对某条记录的第一次快照读会创建一个 Read View, 将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,使用的是同一个 Read View,所以一个事务的查询结果每次都是相同的 - 当前事务在其他事务提交之前使用过快照读,那么以后其他事务对数据的修改都是不可见的,就算以后其他事务提交了数据也不可见;早于 Read View 创建的事务所做的修改并提交的均是可见的 + RR 级别下,通过 `START TRANSACTION WITH CONSISTENT SNAPSHOT` 开启事务,会在执行该语句后立刻生成一个 Read View,不是在执行第一条 SELECT 语句时生成 解决幻读问题: - 快照读:通过 MVCC 来进行控制的,在可重复读隔离级别下,普通查询是快照读,是不会看到别的事务插入的数据的,但是**并不能完全避免幻读** - 场景:RR 级别,T1 事务开启,创建 Read View,此时 T2 去 INSERT 新的一行然后提交,然后 T1去 UPDATE 该行会发现更新成功,因为 Read View 并不能阻止事务去更新数据,并且把这条新记录的 trx_id 给变为当前的事务 id,对当前事务就是可见的了 + 场景:RR 级别,T1 事务开启,创建 Read View,此时 T2 去 INSERT 新的一行然后提交,然后 T1去 UPDATE 该行会发现更新成功,因为 **Read View 并不能阻止事务去更新数据**,并且把这条新记录的 trx_id 给变为当前的事务 id,所以对当前事务就是可见的 - 当前读:通过 next-key 锁(行锁 + 间隙锁)来解决问题 @@ -6040,8 +6069,6 @@ RC、RR 级别下的 InnoDB 快照读区别 - - *** diff --git a/Java.md b/Java.md index e3ddbb2..e0f0de8 100644 --- a/Java.md +++ b/Java.md @@ -531,11 +531,11 @@ public class Test1 { * || 和 |,&& 和& 的区别,逻辑运算符 - **&和| 称为布尔运算符,位运算符。&&和|| 称为条件布尔运算符,也叫短路运算符**。 + **&和| 称为布尔运算符,位运算符。&&和|| 称为条件布尔运算符,也叫短路运算符**。 - 两种运算符得到的结果完全相同,但得到结果的方式又一个重要区别:条件布尔运算符性能比较好。他检查第一个操作数的值,再根据该操作数的值进行操作,可能根本就不处理第二个操作数。 + 如果 && 运算符的第一个操作数是 false,就不需要考虑第二个操作数的值了,因为无论第二个操作数的值是什么,其结果都是 false;同样,如果第一个操作数是 true,|| 运算符就返回 true,无需考虑第二个操作数的值;但 & 和 | 却不是这样,它们总是要计算两个操作数。为了提高性能,**尽可能使用 && 和 || 运算符** - 结论:如果 && 运算符的第一个操作数是 false,就不需要考虑第二个操作数的值了,因为无论第二个操作数的值是什么,其结果都是 false;同样,如果第一个操作数是 true,|| 运算符就返回 true,无需考虑第二个操作数的值。但 & 和 | 却不是这样,它们总是要计算两个操作数。为了提高性能,**尽可能使用 && 和 || 运算符** +* ^ 异或:两位相异为 1,相同为 0,又叫不进位加法。同或:两位相同为 1,相异为 0 * switch @@ -591,11 +591,13 @@ public class Test1 { 运算规则: * 正数的左移与右移,空位补 0 + * 负数原码的左移与右移,空位补 0 负数反码的左移与右移,空位补 1 负数补码,左移低位补 0(会导致负数变为正数的问题,因为移动了符号位),右移高位补 1 + * 无符号移位,空位补 0 From 744e3006467240d278b6cce5a3cffd337ddffbad Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 18 Dec 2021 00:05:29 +0800 Subject: [PATCH 093/168] Update Java Notes --- DB.md | 14 ++++++++------ Java.md | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/DB.md b/DB.md index 45d46c7..558ea03 100644 --- a/DB.md +++ b/DB.md @@ -3626,7 +3626,7 @@ MySQL 官方对索引的定义为:索引(index)是帮助 MySQL 高效获 - 单列索引:一个索引只包含单个列,一个表可以有多个单列索引(普通索引) - 联合索引:顾名思义,就是将单列索引进行组合 - 唯一索引:索引列的值必须唯一,**允许有空值**,如果是联合索引,则列值组合必须唯一 - * NULL 值必须只出现一次 + * NULL 值可以出现多次,因为两个 NULL 比较的结果既不相等,也不不等,结果仍然是未知 * 可以声明不允许存储 NULL 值的非空唯一索引 - 外键索引:只有 InnoDB 引擎支持外键索引,用来保证数据的一致性、完整性和实现级联操作 @@ -4108,7 +4108,7 @@ B+ 树为了保持索引的有序性,在插入新值的时候需要做相应 #### 索引下推 -索引条件下推优化(Index Condition Pushdown)是 MySQL5.6 添加,可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数 +索引条件下推优化(Index Condition Pushdown,ICP)是 MySQL5.6 添加,可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数 索引下推充分利用了索引中的数据,在查询出整行数据之前过滤掉无效的数据,再去主键索引树上查找 @@ -5762,7 +5762,9 @@ roll_pointer 是一个指针,**指向记录对应的 undo log 日志**,一 在页面 Page Header 中,PAGE_FREE 属性指向垃圾链表的头节点,删除的工作过程: * 将要删除的记录的 delete_flag 位置为 1,其他不做修改,这个过程叫 **delete mark** + * 在事务提交前,delete_flag = 1 的记录一直都会处于中间状态 + * 事务提交后,有专门的线程将 delete_flag = 1 的记录从正常链表移除并加入垃圾链表,这个过程叫 **purge** purge 线程在执行删除操作时会创建一个 ReadView,根据事务的可见性移除数据(隔离特性部分详解) @@ -5859,9 +5861,7 @@ Rollback Segement 称为回滚段,每个回滚段中有 1024 个 undo slot #### 并发控制 -MVCC 全称 Multi-Version Concurrency Control,即多版本并发控制,用来**解决读写冲突的无锁并发控制** - -MVCC 处理读写请求,可以做到在发生读写请求冲突时不用加锁,这个读是指的快照读,而不是当前读 +MVCC 全称 Multi-Version Concurrency Control,即多版本并发控制,用来**解决读写冲突的无锁并发控制**,可以在发生读写请求冲突时不用加锁解决,这个读是指的快照读(也叫一致性读或一致性无锁读),而不是当前读: * 快照读:实现基于 MVCC,因为是多版本并发,所以快照读读到的数据不一定是当前最新的数据,有可能是历史版本的数据 * 当前读:读取数据库记录是当前最新的版本(产生幻读、不可重复读),可以对读取的数据进行加锁,防止其他事务修改数据,是悲观锁的一种操作,读写操作加共享锁或者排他锁和串行化事务的隔离级别都是当前读 @@ -5933,6 +5933,8 @@ undo log 主要分为两种: 每次对数据库记录进行改动,都会产生的新版本的 undo log,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,把这个链表称之为**版本链**,版本链的头节点就是当前的最新的 undo log,链尾就是最早的旧 undo log +说明:因为 DELETE 删除记录,都是移动到垃圾链表中,不是真正的删除,所以才可以通过版本链访问原始数据 + 补充:undo 是逻辑日志,这里只是直观的展示出来 @@ -6042,7 +6044,7 @@ ID 为 0 的事务创建 Read View: #### RC RR -Read View 用于支持 RC(Read Committed,读已提交)和 RR(Repeatable Read,可重复读)隔离级别的实现 +Read View 用于支持 RC(Read Committed,读已提交)和 RR(Repeatable Read,可重复读)隔离级别的实现,所以 SELECT 在 RC 和 RR 隔离级别才使用 MVCC 读取记录 RR、RC 生成时机: diff --git a/Java.md b/Java.md index e0f0de8..35e4ddd 100644 --- a/Java.md +++ b/Java.md @@ -19,7 +19,7 @@ -初学时笔记内容参考视频:https://www.bilibili.com/video/BV1TE41177mP,后随着学习的深入逐渐增加了很多知识 +初学时笔记内容参考视频:https://www.bilibili.com/video/BV1TE41177mP,随着学习的深入又增加了很多知识 给初学者的一些个人建议: From 84186472aa520ef9699b16a28becfb8c7e20d1f6 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 18 Dec 2021 22:32:45 +0800 Subject: [PATCH 094/168] Update Java Notes --- DB.md | 197 +++++++++++++++++++++++++++++++++++++++++--------------- Java.md | 8 +-- 2 files changed, 150 insertions(+), 55 deletions(-) diff --git a/DB.md b/DB.md index 558ea03..894b004 100644 --- a/DB.md +++ b/DB.md @@ -1946,6 +1946,10 @@ INSERT INTO card VALUES (NULL,'12345',1),(NULL,'56789',2); +*** + + + #### 一对多 举例:用户和订单、商品分类和商品 @@ -1976,6 +1980,10 @@ INSERT INTO orderlist VALUES (NULL,'hm001',1),(NULL,'hm002',1),(NULL,'hm003',2), +*** + + + #### 多对多 举例:学生和课程。一个学生可以选择多个课程,一个课程也可以被多个学生选择 @@ -6044,7 +6052,7 @@ ID 为 0 的事务创建 Read View: #### RC RR -Read View 用于支持 RC(Read Committed,读已提交)和 RR(Repeatable Read,可重复读)隔离级别的实现,所以 SELECT 在 RC 和 RR 隔离级别才使用 MVCC 读取记录 +Read View 用于支持 RC(Read Committed,读已提交)和 RR(Repeatable Read,可重复读)隔离级别的实现,所以 **SELECT 在 RC 和 RR 隔离级别才使用 MVCC 读取记录** RR、RC 生成时机: @@ -6300,7 +6308,7 @@ InnoDB 刷脏页的控制策略: 锁机制:数据库为了保证数据的一致性,在共享的资源被并发访问时变得安全有序所设计的一种规则 -作用:锁机制类似于多线程中的同步,可以保证数据的一致性和安全性 +利用 MVCC 性质进行读取的操作叫**一致性读**,读取数据前加锁的操作叫**锁定读** 锁的分类: @@ -6332,27 +6340,56 @@ InnoDB 刷脏页的控制策略: -### Server +### 内存结构 -FLUSH TABLES WITH READ LOCK 简称(FTWRL),全局读锁,让整个库处于只读状态,工作流程: +对一条记录加锁的本质就是在内存中创建一个锁结构与之关联,结构包括 -1. 上全局读锁(lock_global_read_lock) -2. 清理表缓存(close_cached_tables) -3. 上全局 COMMIT 锁(make_global_read_lock_block_commit) +* 事务信息:锁对应的事务信息,一个锁属于一个事务 +* 索引信息:对于行级锁,需要记录加锁的记录属于哪个索引 +* 表锁和行锁信息:表锁记录着锁定的表,行锁记录了 Space ID 所在表空间、Page Number 所在的页号、n_bits 使用了多少比特 +* type_mode:一个 32 比特的数,被分成 lock_mode、lock_type、rec_lock_type 三个部分 + * lock_mode:锁模式,记录是共享锁、排他锁、意向锁之类 + * lock_type:代表表级锁还是行级锁 + * rec_lock_type:代表行锁的具体类型和 is_waiting 属性,is_waiting = true 时表示当前事务尚未获取到锁,处于等待状态。事务获取锁后的锁结构是 is_waiting 为 false,释放锁时会检查是否与当前记录关联的锁结构,如果有就唤醒对应事务的线程 -该命令主要用于备份工具做一致性备份,由于 FTWRL 需要持有两把全局的 MDL 锁,并且还要关闭所有表对象,因此杀伤性很大 +一个事务可能操作多条记录,为了节省内存,满足下面条件的锁使用同一个锁结构: + +* 在同一个事务中的加锁操作 +* 被加锁的记录在同一个页面中 +* 加锁的类型时一样的 +* 加锁的状态时一样的 + + + + + +**** + + + +### Server MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL) -MDL 叫元数据锁,主要用来保护 MySQL内部对象的元数据,保证数据读写的正确性,通过 MDL 机制保证 DDL、DML、DQL 操作的并发,**当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁** +MDL 叫元数据锁,主要用来保护 MySQL内部对象的元数据,保证数据读写的正确性,**当对一个表做增删改查的时候,加 MDL 读锁;当要对表做结构变更操作 DDL 的时候,加 MDL 写锁**,两种锁不相互兼容,所以可以保证 DDL、DML、DQL 操作的安全 -* MDL 锁不需要显式使用,在访问一个表的时候会被自动加上,事务中的 MDL 锁,在语句执行开始时申请,在整个事务提交后释放 +说明:DDL 操作执行前会隐式提交当前会话的事务,因为 DDL 一般会在若干个特殊事务中完成,开启特殊事务前需要提交到其他事务 -* MDL 锁是在 Server 中实现,不是 InnoDB 存储引擎层不能直接实现的锁 +MDL 锁的特性: + +* MDL 锁不需要显式使用,在访问一个表的时候会被自动加上,在事务开始执行时申请,在整个事务提交后释放 + +* MDL 锁是在 Server 中实现,不是 InnoDB 存储引擎层能直接实现的锁 * MDL 锁还能实现其他粒度级别的锁,比如全局锁、库级别的锁、表空间级别的锁 +FLUSH TABLES WITH READ LOCK 简称(FTWRL),全局读锁,让整个库处于只读状态,工作流程: +1. 上全局读锁(lock_global_read_lock) +2. 清理表缓存(close_cached_tables) +3. 上全局 COMMIT 锁(make_global_read_lock_block_commit) + +该命令主要用于备份工具做一致性备份,由于 FTWRL 需要持有两把全局的 MDL 锁,并且还要关闭所有表对象,因此杀伤性很大 @@ -6368,7 +6405,7 @@ MyISAM 存储引擎只支持表锁,这也是 MySQL 开始几个版本中唯一 MyISAM 引擎在执行查询语句之前,会**自动**给涉及到的所有表加读锁,在执行增删改之前,会**自动**给涉及的表加写锁,这个过程并不需要用户干预,所以用户一般不需要直接用 LOCK TABLE 命令给 MyISAM 表显式加锁 -* 加锁命令: +* 加锁命令:(对 InnoDB 存储引擎也适用) 读锁:所有连接只能读取数据,不能修改 @@ -6396,7 +6433,7 @@ MyISAM 引擎在执行查询语句之前,会**自动**给涉及到的所有表 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 锁的兼容性.png) -锁调度:MyISAM 的读写锁调度是写优先,因为写锁后其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永远阻塞,所以 MyISAM 不适合做写为主的表的存储引擎 +锁调度:**MyISAM 的读写锁调度是写优先**,因为写锁后其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永远阻塞,所以 MyISAM 不适合做写为主的表的存储引擎 @@ -6532,14 +6569,16 @@ MyISAM 引擎在执行查询语句之前,会**自动**给涉及到的所有表 #### 行级锁 -InnoDB 与 MyISAM 的**最大不同**有两点:一是支持事务;二是采用了行级锁,InnoDB 同时支持表锁和行锁 +##### 记录锁 + +InnoDB 与 MyISAM 的最大不同有两点:一是支持事务;二是采用了行级锁,**InnoDB 同时支持表锁和行锁** -InnoDB 实现了以下两种类型的行锁: +行级锁,也成为记录锁(Record Lock),InnoDB 实现了以下两种类型的行锁: -- 共享锁 (S):又称为读锁,简称 S 锁,就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改 -- 排他锁 (X):又称为写锁,简称 X 锁,就是不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,只有获取排他锁的事务是可以对数据读取和修改 +- 共享锁 (S):又称为读锁,简称 S 锁,多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改 +- 排他锁 (X):又称为写锁,简称 X 锁,不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,只有获取排他锁的事务是可以对数据读取和修改 -对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会**自动给涉及数据集加排他锁**(行锁),在 commit 的时候会自动释放;对于普通 SELECT 语句,不会加任何锁 +对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会**自动给涉及数据集加排他锁**(行锁),在 commit 的时候会自动释放(在事务中加的锁,会**在事务中止或提交时自动释放**);对于普通 SELECT 语句,不会加任何锁 锁的兼容性: @@ -6557,13 +6596,11 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 +**** -*** - - -#### 锁操作 +##### 锁操作 两个客户端操作 Client 1和 Client 2,简化为 C1、C2 @@ -6648,13 +6685,13 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作6.png) - 由于C1、C2 操作的不同行,获取不同的行锁,所以都可以正常获取行锁 + 由于 C1、C2 操作的不同行,获取不同的行锁,所以都可以正常获取行锁 -​ -*** + +**** @@ -6662,17 +6699,17 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 ##### 间隙锁 -当使用范围条件检索数据,并请求共享或排他锁时,InnoDB 会给符合条件的已有数据进行加锁,对于键值在条件范围内但并不存在的记录,叫做间隙(GAP), InnoDB 会对间隙进行加锁,就是间隙锁 +当使用范围条件检索数据,并请求共享或排他锁时,InnoDB 会给符合条件的已有数据进行加锁,对于键值在条件范围内但并不存在的记录,叫做间隙(GAP), InnoDB 会对间隙进行加锁,就是间隙锁 * 唯一索引加锁只有在值存在时才是行锁,值不存在会变成间隙锁,所以范围查询时容易出现间隙锁 * 对于联合索引且是唯一索引,如果 where 条件只包括联合索引的一部分,那么会加间隙锁 -加锁的基本单位是 next-key lock,该锁是行锁和这条记录前面的 gap lock 的组合,就是行锁加间隙锁 +加锁的基本单位是 next-key lock,该锁是行锁和 gap lock 的组合,可以保护当前记录和前面的间隙 -* 加锁遵循前开后闭原则 -* 假设有索引值 10、11、13,那么可能的间隙锁包括:(负无穷,10]、(10,11]、(11,13]、(13,20,正无穷),锁住索引 11 会同时对间隙 (10,11]、(11,13] 加锁 +* 加锁遵循左开右闭原则 +* 假设有 10、11、13,那么可能的间隙锁包括:(负无穷,10]、(10,11]、(11,13]、(13,20,正无穷),锁住索引 11 会对 (10,11] 加锁 -间隙锁优点:RR 级别下间隙锁可以解决事务的**幻读问题**,通过对间隙加锁,防止读取过程中数据条目发生变化 +间隙锁优点:RR 级别下间隙锁可以解决事务的一部分的**幻读问题**,通过对间隙加锁,可以防止读取过程中数据条目发生变化 间隙锁危害:当锁定一个范围的键值后,即使某些不存在的键值也会被无辜的锁定,造成在锁定的时候无法插入锁定键值范围内的任何数据,在某些场景下这可能会对性能造成很大的危害 @@ -6703,25 +6740,31 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 -*** +**** ##### 意向锁 -InnoDB 为了支持多粒度的加锁,允许行锁和表锁同时存在,支持在不同粒度上的加锁操作,InnoDB 增加了意向锁(Intention Lock ) +InnoDB 为了支持多粒度的加锁,允许行锁和表锁同时存在,支持在不同粒度上的加锁操作,InnoDB 增加了意向锁(Intention Lock) 意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度上进行加锁,意向锁分为两种: * 意向共享锁(IS):事务有意向对表中的某些行加共享锁 - * 意向排他锁(IX):事务有意向对表中的某些行加排他锁 -InnoDB 存储引擎支持的是行级别的锁,因此意向锁不会阻塞除全表扫描以外的任何请求,表级意向锁与行级锁的兼容性如下所示: +**IX,IS 是表级锁**,不会和行级的 X,S 锁发生冲突,意向锁是在加表级锁之前添加,为了在加表级锁时可以快速判断表中是否有记录被上锁,比如向一个表添加表级 X 锁的时: + +- 没有意向锁,则需要遍历整个表判断是否有锁定的记录 +- 有了意向锁,首先判断是否存在意向锁,然后判断该意向锁与即将添加的表级锁是否兼容即可,因为意向锁的存在代表有表级锁的存在或者即将有表级锁的存在 + +兼容性如下所示: ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-意向锁兼容性.png) -插入意向锁是在插入一行记录操作之前设置的一种间隙锁,这个锁释放了一种插入方式的信号,即多个事务在相同的索引间隙插入时如果不是插入间隙中相同的位置就不需要互相等待。假设某列有索引值2,6,只要两个事务插入位置不同,如事务 A 插入 3,事务 B 插入 4,那么就可以同时插入 +**插入意向锁** Insert Intention Lock 是在插入一行记录操作之前设置的一种间隙锁,是行级锁 + +插入意向锁释放了一种插入信号,即多个事务在相同的索引间隙插入时如果不是插入间隙中相同的位置就不需要互相等待。假设某列有索引值 2,6,只要两个事务插入位置不同,如事务 A 插入 3,事务 B 插入 4,那么就可以同时插入 @@ -6729,16 +6772,18 @@ InnoDB 存储引擎支持的是行级别的锁,因此意向锁不会阻塞除 -##### 死锁 +##### 自增锁 -当并发系统中不同线程出现循环资源依赖,线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁 +系统会自动给 AUTO_INCREMENT 修饰的列进行递增赋值,实现方式: -死锁情况:线程 A 修改了 id = 1 的数据,请求修改 id = 2 的数据,线程 B 修改了 id = 2 的数据,请求修改 id = 1 的数据,产生死锁 +* AUTO_INC 锁:表级锁,执行插入语句时会自动添加,在该语句执行完成后释放,并不是事务结束 +* 轻量级锁:为插入语句生成 AUTO_INCREMENT 修饰的列时获取该锁,生成以后释放掉,不需要等到插入语句执行完后释放 -解决策略: +系统变量 `innodb_autoinc_lock_mode` 控制采取哪种方式: -* 直接进入等待直到超时,超时时间可以通过参数 innodb_lock_wait_timeout 来设置,但是时间的设置不好控制,超时可能不是因为死锁,而是因为事务处理比较慢,所以一般不采取该方式 -* 主动死锁检测,发现死锁后**主动回滚死锁链条中的某一个事务**,让其他事务得以继续执行,将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑 +* 0:全部采用 AUTO_INC 锁 +* 1:全部采用轻量级锁 +* 2:混合使用,在插入记录的数量确定是采用轻量级锁,不确定时采用 AUTO_INC 锁 @@ -6746,8 +6791,57 @@ InnoDB 存储引擎支持的是行级别的锁,因此意向锁不会阻塞除 +##### 隐式锁 + +一般情况下 INSERT 语句是不需要在内存中生成锁结构的,会进行隐式的加锁,保护的是插入后的安全 + +注意:如果插入的间隙被其他事务加了间隙锁,此次插入会被阻塞,并在该间隙插入一个插入意向锁 + +* 聚簇索引:索引记录有 trx_id 隐藏列,表示最后改动该记录的事务 id,插入数据后事务 id 就是当前事务。其他事务想获取该记录的锁时会判断当前记录的事务 id 是否是活跃的,如果不是就可以正常加锁;如果是就创建一个 X 的锁结构,该锁的 is_waiting 是 false,为自己的事务创建一个锁结构,is_waiting 是 true(类似 Java 中的锁升级) +* 二级索引:获取数据页 Page Header 中的 PAGE_MAX_TRX_ID 属性,代表修改当前页面的最大的事务 ID,如果小于当前活跃的最小事务 id,就证明插入该数据的事务已经提交,否则就需要获取到主键值进行回表操作 + +隐式锁起到了延迟生成锁的效果,如果其他事务与隐式锁没有冲突,就可以避免锁结构的生成,节省了内存资源 + +INSERT 在两种情况下会生成锁结构: + +* 重复键:在插入主键或唯一二级索引时遇到重复的键值会报错,在报错前需要对对应的聚簇索引进行加锁 + * 隔离级别 <= Read Uncommitted,加 S 型 Record Lock + * 隔离级别 >= Repeatable Read,加 S 型 next_key 锁 + +* 外键检查:如果待插入的记录在父表中可以找到,会对父表的记录加 S 型 Record Lock。如果待插入的记录在父表中找不到 + * 隔离级别 <= Read Committed,不加锁 + * 隔离级别 >= Repeatable Read,加间隙锁 + + + + + +*** + + + #### 锁优化 +##### 优化锁 + +InnoDB 存储引擎实现了行级锁定,虽然在锁定机制的实现方面带来了性能损耗可能比表锁会更高,但是在整体并发处理能力方面要远远优于 MyISAM 的表锁,当系统并发量较高的时候,InnoDB 的整体性能远远好于 MyISAM + +但是使用不当可能会让 InnoDB 的整体性能表现不仅不能比 MyISAM 高,甚至可能会更差 + +优化建议: + +- 尽可能让所有数据检索都能通过索引来完成,避免无索引行锁升级为表锁 +- 合理设计索引,尽量缩小锁的范围 +- 尽可能减少索引条件及索引范围,避免间隙锁 +- 尽量控制事务大小,减少锁定资源量和时间长度 +- 尽可使用低级别事务隔离(需要业务层面满足需求) + + + +**** + + + ##### 锁升级 索引失效造成行锁升级为表锁,不通过索引检索数据,InnoDB 会将对表中的所有记录加锁,实际效果和**表锁**一样实际开发过程应避免出现索引失效的状况 @@ -6781,21 +6875,20 @@ InnoDB 存储引擎支持的是行级别的锁,因此意向锁不会阻塞除 -##### 优化锁 +##### 死锁 -InnoDB 存储引擎实现了行级锁定,虽然在锁定机制的实现方面带来了性能损耗可能比表锁会更高,但是在整体并发处理能力方面要远远优于 MyISAM 的表锁,当系统并发量较高的时候,InnoDB 的整体性能远远好于 MyISAM +不同事务由于互相持有对方需要的锁而导致事务都无法继续执行的情况称为死锁 -但是使用不当可能会让InnoDB 的整体性能表现不仅不能比 MyISAM 高,甚至可能会更差 +死锁情况:线程 A 修改了 id = 1 的数据,请求修改 id = 2 的数据,线程 B 修改了 id = 2 的数据,请求修改 id = 1 的数据,产生死锁 -优化建议: +解决策略: -- 尽可能让所有数据检索都能通过索引来完成,避免无索引行锁升级为表锁 -- 合理设计索引,尽量缩小锁的范围 -- 尽可能减少索引条件及索引范围,避免间隙锁 -- 尽量控制事务大小,减少锁定资源量和时间长度 -- 尽可使用低级别事务隔离(需要业务层面满足需求) +* 直接进入等待直到超时,超时时间可以通过参数 innodb_lock_wait_timeout 来设置,但是时间的设置不好控制,超时可能不是因为死锁,而是因为事务处理比较慢,所以一般不采取该方式 +* 主动死锁检测,发现死锁后**主动回滚死锁链条中较小的一个事务**,让其他事务得以继续执行,将参数 `innodb_deadlock_detect` 设置为 on,表示开启该功能。较小的意思就是事务执行过程中插入、删除、更新的记录条数 +通过执行 `SHOW ENGINE INNODB STATUS` 可以查看最近发生的一次死循环,全局系统变量 `innodb_print_all_deadlocks` 设置为 on,就可以将每个死锁信息都记录在 MySQL 错误日志中 +死锁一般是行级锁,当表锁发生死锁时,会在事务中访问其他表时直接报错,破坏了持有并等待的死锁条件 @@ -6805,6 +6898,8 @@ InnoDB 存储引擎实现了行级锁定,虽然在锁定机制的实现方面 #### 锁状态 +查看锁信息 + ```mysql SHOW STATUS LIKE 'innodb_row_lock%'; ``` @@ -6829,7 +6924,7 @@ SHOW STATUS LIKE 'innodb_row_lock%'; ```mysql SELECT * FROM information_schema.innodb_locks; #锁的概况 -SHOW ENGINE INNODB STATUS; #InnoDB整体状态,其中包括锁的情况 +SHOW ENGINE INNODB STATUS\G; #InnoDB整体状态,其中包括锁的情况 ``` ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB查看锁状态.png) diff --git a/Java.md b/Java.md index 35e4ddd..0d8a455 100644 --- a/Java.md +++ b/Java.md @@ -2324,7 +2324,7 @@ hashCode 的作用: * 浅拷贝 (shallowCopy):**对基本数据类型进行值传递,对引用数据类型只是复制了引用**,被复制对象属性的所有的引用仍然指向原来的对象,简而言之就是增加了一个指针指向原来对象的内存地址 - Java 中的复制方法基本都是浅克隆:Object.clone()、System.arraycopy()、Arrays.copyOf() + **Java 中的复制方法基本都是浅克隆**:Object.clone()、System.arraycopy()、Arrays.copyOf() * 深拷贝 (deepCopy):对基本数据类型进行值传递,对引用数据类型是一个整个独立的对象拷贝,会拷贝所有的属性并指向的动态分配的内存,简而言之就是把所有属性复制到一个新的内存,增加一个指针指向新内存。所以使用深拷贝的情况下,释放内存的时候不会出现使用浅拷贝时释放同一块内存的错误 @@ -2334,9 +2334,9 @@ Cloneable 接口是一个标识性接口,即该接口不包含任何方法( * Clone & Copy:`Student s = new Student` - `Student s1 = s`:只是 copy 了一下 reference,s 和 s1 指向内存中同一个 object,对对象的修改会影响对方 + `Student s1 = s`:只是 copy 了一下 reference,s 和 s1 指向内存中同一个 Object,对对象的修改会影响对方 - `Student s2 = s.clone()`:会生成一个新的Student对象,并且和s具有相同的属性值和方法 + `Student s2 = s.clone()`:会生成一个新的 Student 对象,并且和s具有相同的属性值和方法 * Shallow Clone & Deep Clone: @@ -18365,7 +18365,7 @@ Java中 的 Object 类中提供了 `clone()` 方法来实现浅克隆,实现 C } ``` - stu1 对象和 stu2 对象是同一个对象,将 stu2 对象中 name 属性改为李四,两个Citation对象中都是李四,这就是浅克隆的效果 + stu1 对象和 stu2 对象是同一个对象,将 stu2 对象中 name 属性改为李四,两个 Citation 对象中都是李四,这就是浅克隆的效果 * 序列化实现深克隆,或者重写克隆方法: From e60e7ffdef0f89a3dc85dde902fbefca7bb1ad6f Mon Sep 17 00:00:00 2001 From: Seazean Date: Tue, 21 Dec 2021 00:26:39 +0800 Subject: [PATCH 095/168] Update Java Notes --- DB.md | 85 +++++++++++++++++++++++++++++++++++--------------------- Frame.md | 14 +++++----- 2 files changed, 60 insertions(+), 39 deletions(-) diff --git a/DB.md b/DB.md index 894b004..c835ac6 100644 --- a/DB.md +++ b/DB.md @@ -5822,9 +5822,9 @@ roll_pointer 是一个指针,**指向记录对应的 undo log 日志**,一 #### 回滚日志 -undo log 是采用段(segment)的方式来记录,每个 undo 操作在记录的时候占用一个 undo log segment +undo log 是采用段的方式来记录,Rollback Segement 称为回滚段,本质上就是一个类型是 Rollback Segement Header 的页面 -Rollback Segement 称为回滚段,每个回滚段中有 1024 个 undo slot +每个回滚段中有 1024 个 undo slot,每个 slot 存放 undo 链表页面的头节点页号,每个链表对应一个叫 undo log segment 的段 * 在以前老版本,只支持 1 个 Rollback Segement,只能记录 1024 个 undo log segment * MySQL5.5 开始支持 128 个 Rollback Segement,支持 128*1024 个 undo 操作 @@ -5832,9 +5832,11 @@ Rollback Segement 称为回滚段,每个回滚段中有 1024 个 undo slot 工作流程: * 事务执行前需要到系统表空间第 5 号页面中分配一个回滚段(页),获取一个 Rollback Segement Header 页面的地址 -* 回滚段页面有 1024 个 undo slot,每个 slot 存放 undo 链表页面的头节点页号。首先去回滚段的两个 cached 链表看是否有缓存的 slot,缓存中没有就在回滚段中找一个可用的 slot -* 缓存中获取的 slot 对应的 Undo Log Segment 已经分配了,需要重新分配,然后从 Undo Log Segment 中申请一个页面作为日志链表的头节点,并填入对应的 slot 中 -* 开始记录 +* 回滚段页面有 1024 个 undo slot,首先去回滚段的两个 cached 链表获取缓存的 slot,缓存中没有就在回滚段页面中找一个可用的 undo slot 分配给当前事务 +* 如果是缓存中获取的 slot,则该 slot 对应的 undo log segment 已经分配了,需要重新分配,然后从 undo log segment 中申请一个页面作为日志链表的头节点,并填入对应的 slot 中 +* 每个事务 undo 日志在记录的时候占用两个 undo 页面的组成链表,分别为 insert undo 链表和 update undo 链表,链表的头节点页面为 first undo page 会包含一些管理信息,其他页面为 normal undo page + + 说明:事务执行过程的临时表也需要两个 undo 链表,不和普通表共用,这些链表并不是事务开始就分配,而是按需分配 @@ -6114,17 +6116,28 @@ Buffer Pool 的使用提高了读写数据的效率,但是如果 MySQL 宕机 ##### 日志缓冲 -服务器启动时会向操作系统申请一片连续内存空间作为 redo log buffer(重做日志缓冲区),可以通过 `innodb_log_buffer_size` 系统变量指定 log buffer 的大小,默认是 16MB - -补充知识:MySQL 规定对底层页面的一次原子访问称为一个 Mini-Transaction(MTR),比如在 B+ 树上插入一条数据就算一个 MTR +服务器启动时会向操作系统申请一片连续内存空间作为 redo log buffer(重做日志缓冲区),可以通过 `innodb_log_buffer_size` 系统变量指定 redo log buffer 的大小,默认是 16MB log buffer 被划分为若干 redo log block(块,类似数据页的概念),每个默认大小 512 字节,每个 block 由 12 字节的 log block head、496 字节的 log block body、4 字节的 log block trailer 组成 * 当数据修改时,先修改 Change Buffer 中的数据,然后在 redo log buffer 记录这次操作,写入 log buffer 的过程是顺序写入的(先写入前面的 block,写满后继续写下一个) -* log buffer 中有一个指针 buf_free,来标识该位置之前都是填满的 block,该位置之后都是空闲区域(碰撞指针) -* 一个事务包含若干个 MTR,一个 MTR 对应一组若干条 redo log,一组 redo log 是不可分割的,所以并不是每生成一条 redo 日志就将其插入到 log buffer 中,而是一个 MTR 结束后**将一组 redo 日志写入 log buffer**,在进行数据恢复时也把这一组 redo log 当作一个不可分割的整体处理 +* log buffer 中有一个指针 buf_free,来标识该位置之前都是填满的 block,该位置之后都是空闲区域(**碰撞指针**) + +MySQL 规定对底层页面的一次原子访问称为一个 Mini-Transaction(MTR),比如在 B+ 树上插入一条数据就算一个 MTR + +* 一个事务包含若干个 MTR,一个 MTR 对应一组若干条 redo log,一组 redo log 是不可分割的,在进行数据恢复时也把一组 redo log 当作一个不可分割的整体处理 + +* 所以不是每生成一条 redo 日志就将其插入到 log buffer 中,而是一个 MTR 结束后**将一组 redo 日志写入 log buffer** + + + +*** -redo log 也需要在事务提交时将日志写入磁盘,但是比将内存中的 Buffer Pool 修改的数据写入磁盘的速度快: + + +##### 日志刷盘 + +redo log 需要在事务提交时将日志写入磁盘,但是比将内存中的 Buffer Pool 修改的数据写入磁盘的速度快,原因: * 刷脏是随机 IO,因为每次修改的数据位置随机,但写 redo log 是尾部追加操作,属于顺序 IO * 刷脏是以数据页(Page)为单位的,一个页上的一个小修改都要整页写入,而 redo log 中只包含真正需要写入的部分,减少无效 IO @@ -6139,31 +6152,39 @@ InnoDB 引擎会在适当的时候,把内存中 redo log buffer 持久化到 * 服务器关闭时 * checkpoint 时(下小节详解) +redo 日志在磁盘中以文件组的形式存储,同一组中的每个文件大小一样格式一样, +* `innodb_log_group_home_dir` 代表磁盘存储 redo log 的文件目录,默认是当前数据目录 +* `innodb_log_file_size` 代表文件大小,默认 48M,`innodb_log_files_in_group` 代表文件个数,默认 2 最大 100,所以日志的文件大小为 `innodb_log_file_size * innodb_log_files_in_group` -*** +redo 日志文件也是由若干个 512 字节的 block 组成,日志文件的前 2048 个字节(前 4 个 block)用来存储一些管理信息,以后的用来存储 log buffer 中的 block 镜像 +注意:block 并不代表一组 redo log,一组日志可能占用不到一个 block 或者几个 block,依赖于 MTR 的大小 +服务器启动后 redo 磁盘空间不变,所以 redo 磁盘中的日志文件是被**循环使用**的,采用循环写数据的方式,写完尾部重新写头部,所以要确保头部 log 对应的修改已经持久化到磁盘 -##### 磁盘文件 -redo 存储在磁盘中的日志文件是被**循环使用**的,redo 日志文件组中每个文件大小一样格式一样,组成结构:前 2048 个字节(前 4 个 block)用来存储一些管理信息,以后的存储 log buffer 中的 block 镜像 -注意:block 并不代表一组 redo log,一组日志可能占用不到一个 block 或者几个 block,依赖 MTR 的大小 +*** -磁盘存储 redo log 的文件目录通过 `innodb_log_group_home_dir` 指定,默认是当前数据目录,文件大小: -* 通过两个参数调节:`innodb_log_file_size` 文件大小默认 48M,`innodb_log_files_in_group` 文件个数默认 2 最大 100 -* 服务器启动后磁盘空间不变,所以采用循环写数据的方式,写完尾部重新写头部,所以要确保头部 log 对应的修改已经持久化到磁盘 + +##### 日志序号 lsn (log sequence number) 代表已经写入的 redo 日志量、flushed_to_disk_lsn 指刷新到磁盘中的 redo 日志量,两者都是**全局变量**,如果两者的值相同,说明 log buffer 中所有的 redo 日志都已经持久化到磁盘 +工作过程:写入 log buffer 数据时,buf_free 会进行偏移,偏移量就会加到 lsn 上 + MTR 的执行过程中修改过的页对应的控制块会加到 Buffer Pool 的 flush 链表中,链表中脏页是按照第一次修改的时间进行排序的(头插),控制块中有两个指针用来记录脏页被修改的时间: * oldest_modification:第一次修改 Buffer Pool 中某个缓冲页时,将修改该页的 MTR **开始时**对应的 lsn 值写入这个属性,所以链表页是以该值进行排序的 * newest_modification:每次修改页面,都将 MTR **结束时**对应的 lsn 值写入这个属性,所以是该页面最后一次修改后对应的 lsn 值 -全局变量 checkpoint_lsn 表示当前系统中可以被覆盖的 redo 日志量,当 redo 日志对应的脏页已经被刷新到磁盘后就可以被覆盖重用,此时执行一次 checkpoint 来更新 checkpoint_lsn 的值存入管理信息,刷脏和执行一次 checkpoint 并不是同一个线程 +全局变量 checkpoint_lsn 表示当前系统可以被覆盖的 redo 日志总量,当 redo 日志对应的脏页已经被刷新到磁盘后,该文件空间就可以被覆盖重用,此时执行一次 checkpoint 来更新 checkpoint_lsn 的值存入管理信息,刷脏和执行一次 checkpoint 并不是同一个线程 + +**checkpoint**:从 flush 链表尾部中找出还未刷脏的页面,该页面是当前系统中最早被修改的脏页,该页面之前产生的脏页都已经刷脏,然后将该页 oldest_modification 值赋值给 checkpoint_lsn,因为 lsn 小于该值时产生的 redo 日志都可以被覆盖了 + +checkpoint_lsn 是一个总量,随着 lsn 写入的增加,刷脏的继续进行,所以 checkpoint_lsn 值就会一直变大,该值的增量就代表磁盘文件中当前位置向后可以被覆盖的文件的量 在系统忙碌时,后台线程的刷脏操作不能将脏页快速刷出,导致系统无法及时执行 checkpoint,这时需要用户线程从 flush 链表中把最早修改的脏页刷新到磁盘中,然后执行 checkpoint @@ -6192,7 +6213,7 @@ SHOW ENGINE INNODB STATUS\G 问题:系统崩溃前没有提交的事务的 redo log 可能已经刷盘,这些数据可能在重启后也会恢复 -解决:通过 undo log 在服务器重启时将未提交的事务回滚掉,定位到 128 个回滚段,遍历 slot,获取 undo 链表首节点页面的 Undo Segement Header 中的 TRX_UNDO_STATE 属性,表示当前链表的事务属性,如果是活跃的就全部回滚 +解决:通过 undo log 在服务器重启时将未提交的事务回滚掉,定位到 128 个回滚段,遍历 slot,获取 undo 链表首节点页面的 undo segement header 中的 TRX_UNDO_STATE 属性,表示当前链表的事务属性,如果是活跃的就全部回滚 @@ -10454,7 +10475,7 @@ RDB三种启动方式对比: #### 总结 -* RDB特殊启动形式的指令(客户端输入) +* RDB 特殊启动形式的指令(客户端输入) * 服务器运行过程中重启 @@ -10472,17 +10493,17 @@ RDB三种启动方式对比: * 全量复制:主从复制部分详解 -* RDB优点: +* RDB 优点: - RDB 是一个紧凑压缩的二进制文件,存储效率较高,但存储数据量较大时,存储效率较低 - RDB 内部存储的是 redis 在某个时间点的数据快照,非常**适合用于数据备份,全量复制**等场景 - RDB 恢复数据的速度要比 AOF 快很多,因为是快照,直接恢复 - - 应用:服务器中每 X 小时执行 bgsave 备份,并将 RDB 文件拷贝到远程机器中,用于灾难恢复 -* RDB缺点: +* RDB 缺点: - bgsave 指令每次运行要执行 fork 操作创建子进程,会牺牲一些性能 - RDB 方式无论是执行指令还是利用配置,无法做到实时持久化,具有丢失数据的可能性,最后一次持久化后的数据可能丢失 - Redis 的众多版本中未进行 RDB 文件格式的版本统一,可能出现各版本之间数据格式无法兼容 +* 应用:服务器中每 X 小时执行 bgsave 备份,并将 RDB 文件拷贝到远程机器中,用于灾难恢复 @@ -10539,7 +10560,7 @@ AOF 持久化数据的三种策略(appendfsync): * 同步硬盘操作依赖于系统调度机制,比如缓冲区页空间写满或达到特定时间周期。同步文件之前,如果此时系统故障宕机,缓冲区内数据将丢失 * fsync 针对单个文件操作(比如 AOF 文件)做强制硬盘同步,fsync 将阻塞到写入硬盘完成后返回,保证了数据持久化 -异常恢复:AOF 文件损坏,通过 redis-check-aof--fix appendonly.aof 进行恢复,重启 redis,然后重新加载 +异常恢复:AOF 文件损坏,通过 redis-check-aof--fix appendonly.aof 进行恢复,重启 Redis,然后重新加载 @@ -10678,7 +10699,7 @@ AOF 和 RDB 同时开启,系统默认取 AOF 的数据(数据不会存在丢 - 如不能承受数分钟以内的数据丢失,对业务数据非常敏感,选用 AOF - 如能承受数分钟以内的数据丢失,且追求大数据集的恢复速度,选用 RDB - 灾难恢复选用 RDB -- 双保险策略,同时开启 RDB和 AOF,重启后 Redis 优先使用 AOF 来恢复数据,降低丢失数据的量 +- 双保险策略,同时开启 RDB 和 AOF,重启后 Redis 优先使用 AOF 来恢复数据,降低丢失数据的量 - 不建议单独用 AOF,因为可能会出现 Bug,如果只是做纯内存缓存,可以都不用 @@ -10731,8 +10752,8 @@ fpid 的值在父子进程中不同:进程形成了链表,父进程的 fpid int main () { pid_t fpid; // fpid表示fork函数返回的值 - int count=0; - fpid=fork(); + int count = 0; + fpid = fork(); if (fpid < 0) printf("error in fork!"); else if (fpid == 0) { @@ -10761,16 +10782,16 @@ int main () #include int main(void) { - int i=0; + int i = 0; // ppid 指当前进程的父进程pid // pid 指当前进程的pid, // fpid 指fork返回给当前进程的值,在这可以表示子进程 - for(i=0; i<2; i++){ + for(i = 0; i < 2; i++){ pid_t fpid = fork(); if(fpid == 0) - printf("%d child %4d %4d %4d/n",i,getppid(),getpid(),fpid); + printf("%d child %4d %4d %4d/n",i, getppid(), getpid(), fpid); else - printf("%d parent %4d %4d %4d/n",i,getppid(),getpid(),fpid); + printf("%d parent %4d %4d %4d/n",i, getppid(), getpid(),fpid); } return 0; } diff --git a/Frame.md b/Frame.md index c8e1908..8947b3a 100644 --- a/Frame.md +++ b/Frame.md @@ -2505,7 +2505,7 @@ Pipeline 的存在,需要将 ByteBuf 传递给下一个 ChannelHandler,如 +--------+-------------------------------------------------+----------------+ ``` -解决方法:通过调整系统的接受缓冲区的滑动窗口和 Netty 的接受缓冲区保证每条包只含有一条数据,滑动窗口大小,仅决定了 Netty 读取的**最小单位**,实际每次读取的一般是它的整数倍 +解决方法:通过调整系统的接受缓冲区的滑动窗口和 Netty 的接受缓冲区保证每条包只含有一条数据,滑动窗口的大小仅决定了 Netty 读取的**最小单位**,实际每次读取的一般是它的整数倍 @@ -2540,13 +2540,13 @@ public class HelloWorldClient { #### 固定长度 -服务器端加入定长解码器,每一条消息采用固定长度,缺点浪费空间 +服务器端加入定长解码器,每一条消息采用固定长度。如果是半包消息,会缓存半包消息并等待下个包到达之后进行拼包合并,直到读取一个完整的消息包;如果是粘包消息,空余的位置会进行补 0,会浪费空间 ```java serverBootstrap.childHandler(new ChannelInitializer() { @Override protected void initChannel(SocketChannel ch) throws Exception { - ch.pipeline().addLast(new FixedLengthFrameDecoder(8)); + ch.pipeline().addLast(new FixedLengthFrameDecoder(10)); // LoggingHandler 用来打印消息 ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG)); } @@ -2692,7 +2692,7 @@ public class LengthFieldDecoderDemo { #### HTTP协议 -访问URL:http://localhost:8080/ +访问 URL:http://localhost:8080/ ```java public class HttpDemo { @@ -4537,9 +4537,9 @@ RocketMQ 的工作流程: - 启动 NameServer 监听端口,等待 Broker、Producer、Consumer 连上来,相当于一个路由控制中心 - Broker 启动,跟所有的 NameServer 保持长连接,每隔 30s 时间向 NameServer 上报 Topic 路由信息(心跳包)。心跳包中包含当前 Broker 信息(IP、端口等)以及存储所有 Topic 信息。注册成功后,NameServer 集群中就有 Topic 跟 Broker 的映射关系 - 收发消息前,先创建 Topic,创建 Topic 时需要指定该 Topic 要存储在哪些 Broker 上,也可以在发送消息时自动创建 Topic -- Producer 启动时先跟 NameServer 集群中的**其中一台**建立长连接,并从 NameServer 中获取当前发送的 Topic 存在哪些 Broker 上,同时 Producer 会默认每隔 30s 向 NameServer 拉取一次路由信息 +- Producer 启动时先跟 NameServer 集群中的**其中一台**建立长连接,并从 NameServer 中获取当前发送的 Topic 存在哪些 Broker 上,同时 Producer 会默认每隔 30s 向 NameServer **定时拉取**一次路由信息 - Producer 发送消息时,根据消息的 Topic 从本地缓存的 TopicPublishInfoTable 获取路由信息,如果没有则会从 NameServer 上重新拉取并更新,轮询队列列表并选择一个队列 MessageQueue,然后与队列所在的 Broker 建立长连接,向 Broker 发消息 -- Consumer 跟 Producer 类似,跟其中一台 NameServer 建立长连接获取路由信息,根据当前订阅 Topic 存在哪些 Broker 上,直接跟 Broker 建立连接通道,在完成客户端的负载均衡后,选择其中的某一个或者某几个 MessageQueue 来拉取消息并进行消费 +- Consumer 跟 Producer 类似,跟其中一台 NameServer 建立长连接,定时获取路由信息,根据当前订阅 Topic 存在哪些 Broker 上,直接跟 Broker 建立连接通道,在完成客户端的负载均衡后,选择其中的某一个或者某几个 MessageQueue 来拉取消息并进行消费 @@ -4581,7 +4581,7 @@ RocketMQ 的工作流程: #### 通信原理 -==todo:后期学习了源码会进行扩充,现在暂时 copy 官方文档== +==todo:后期对 Netty 有了更深的认知后会进行扩充,现在暂时 copy 官方文档== 在 RocketMQ 消息队列中支持通信的方式主要有同步(sync)、异步(async)、单向(oneway)三种,其中单向通信模式相对简单,一般用在发送心跳包场景下,无需关注其 Response From 2bfeaa8c14fe64a2c4ca17d4f6cf3bfcd7c982b5 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 25 Dec 2021 01:06:00 +0800 Subject: [PATCH 096/168] Update Java Notes --- DB.md | 5 +- Frame.md | 639 ++++++++++++++++++++++++++++++++++++++++++++++++------- Java.md | 8 +- 3 files changed, 568 insertions(+), 84 deletions(-) diff --git a/DB.md b/DB.md index c835ac6..ebf0128 100644 --- a/DB.md +++ b/DB.md @@ -4,7 +4,7 @@ ### 数据库 -数据库:DataBase,简称 DB,用于存储和管理数据的仓库,它的存储空间很大,可以存放百万上亿条数据。 +数据库:DataBase,简称 DB,存储和管理数据的仓库 数据库的优势: @@ -22,13 +22,12 @@ - 数据表 - 数据库最重要的组成部分之一 - - 它由纵向的列和横向的行组成(类似 excel 表格) + - 由纵向的列和横向的行组成(类似 excel 表格) - 可以指定列名、数据类型、约束等 - 一个表中可以存储多条数据 - 数据:想要永久化存储的数据 - diff --git a/Frame.md b/Frame.md index 8947b3a..4dc7ebc 100644 --- a/Frame.md +++ b/Frame.md @@ -42,6 +42,10 @@ pom.xml:Maven 需要一个 pom.xml 文件,Maven 通过加载这个配置文 +参考视频:https://www.bilibili.com/video/BV1Ah411S7ZE + + + *** @@ -1757,7 +1761,7 @@ Netty 主要基于主从 Reactors 多线程模型做了一定的改进,Netty #### 基本介绍 -事件循环对象 EventLoop,本质是一个单线程执行器(同时维护了一个 selector),里面有 run 方法处理 Channel 上源源不断的 IO 事件 +事件循环对象 EventLoop,本质是一个单线程执行器(同时维护了一个 selector),有 run 方法处理 Channel 上源源不断的 IO 事件 事件循环组 EventLoopGroup 是一组 EventLoop,Channel 会调用 Boss EventLoopGroup 的 register 方法来绑定其中一个 Worker 的 EventLoop,后续这个 Channel 上的 IO 事件都由此 EventLoop 来处理,保证了事件处理时的线程安全 @@ -1847,7 +1851,7 @@ static void invokeChannelRead(final AbstractChannelHandlerContext next, Object m ### Channel -#### 基本介绍 +#### 连接操作 Channel 类 API: @@ -2112,7 +2116,6 @@ public static void main(String[] args) { }) .bind(8080); } - ``` 服务器端依次打印:1 2 4 3 ,所以**入站是按照 addLast 的顺序执行的,出站是按照 addLast 的逆序执行** @@ -2690,7 +2693,7 @@ public class LengthFieldDecoderDemo { ### 协议设计 -#### HTTP协议 +#### HTTP 访问 URL:http://localhost:8080/ @@ -2750,7 +2753,7 @@ public class HttpDemo { -#### 自定义协议 +#### 自定义 处理器代码: @@ -4499,7 +4502,7 @@ NameServer 是一个简单的 Topic 路由注册中心,支持 Broker 的动态 NameServer 主要包括两个功能: -* Broker 管理,NameServer 接受 Broker 集群的注册信息并保存下来作为路由信息的基本数据,提供**心跳检测**检查 Broker 活性 +* Broker 路由管理,NameServer 接受 Broker 集群的注册信息,并保存下来作为路由信息的基本数据,提供**心跳检测机制**检查 Broker 活性(每 10 秒) * 路由信息管理,每个 NameServer 将保存关于 Broker 集群的整个路由信息和用于客户端查询的队列信息,然后 Producer 和 Conumser 通过 NameServer 就可以知道整个 Broker 集群的路由信息,从而进行消息的投递和消费 NameServer 特点: @@ -4543,77 +4546,6 @@ RocketMQ 的工作流程: -**** - - - -#### 协议设计 - -在 Client 和 Server 之间完成一次消息发送时,需要对发送的消息进行一个协议约定,所以自定义 RocketMQ 的消息协议。为了高效地在网络中传输消息和对收到的消息读取,就需要对消息进行编解码。在 RocketMQ 中,RemotingCommand 这个类在消息传输过程中对所有数据内容的封装,不但包含了所有的数据结构,还包含了编码解码操作 - -| Header字段 | 类型 | Request 说明 | Response 说明 | -| ---------- | ----------------------- | ------------------------------------------------------------ | ------------------------------------------- | -| code | int | 请求操作码,应答方根据不同的请求码进行不同的处理 | 应答响应码,0 表示成功,非 0 则表示各种错误 | -| language | LanguageCode | 请求方实现的语言 | 应答方实现的语言 | -| version | int | 请求方程序的版本 | 应答方程序的版本 | -| opaque | int | 相当于 requestId,在同一个连接上的不同请求标识码,与响应消息中的相对应 | 应答不做修改直接返回 | -| flag | int | 区分是普通 RPC 还是 onewayRPC 的标志 | 区分是普通 RPC 还是 onewayRPC的标志 | -| remark | String | 传输自定义文本信息 | 传输自定义文本信息 | -| extFields | HashMap | 请求自定义扩展信息 | 响应自定义扩展信息 | - -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息协议.png) - -传输内容主要可以分为以下四部分: - -* 消息长度:总长度,四个字节存储,占用一个 int 类型 - -* 序列化类型&消息头长度:同样占用一个 int 类型,第一个字节表示序列化类型,后面三个字节表示消息头长度 - -* 消息头数据:经过序列化后的消息头数据 - -* 消息主体数据:消息主体的二进制字节数据内容 - - - -***** - - - -#### 通信原理 - -==todo:后期对 Netty 有了更深的认知后会进行扩充,现在暂时 copy 官方文档== - -在 RocketMQ 消息队列中支持通信的方式主要有同步(sync)、异步(async)、单向(oneway)三种,其中单向通信模式相对简单,一般用在发送心跳包场景下,无需关注其 Response - -RocketMQ 的异步通信流程: - -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-异步通信流程.png) - -RocketMQ 的 RPC 通信采用 Netty 组件作为底层通信库,同样也遵循了 Reactor 多线程模型,同时又在这之上做了一些扩展和优化 - -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-Reactor设计.png) - -RocketMQ 基于 NettyRemotingServer 的 Reactor 多线程模型: - -* 一个 Reactor 主线程(eventLoopGroupBoss)负责监听 TCP 网络连接请求,建立好连接,创建 SocketChannel,并注册到 selector 上。RocketMQ 会自动根据 OS 的类型选择 NIO 和 Epoll,也可以通过参数配置),然后监听真正的网络数据 - -* 拿到网络数据交给 Worker 线程池(eventLoopGroupSelector,默认设置为 3),在真正执行业务逻辑之前需要进行 SSL 验证、编解码、空闲检查、网络连接管理,这些工作交给 defaultEventExecutorGroup(默认设置为 8)去做 -* 处理业务操作放在业务线程池中执行,根据 RomotingCommand 的业务请求码 code 去 processorTable 这个本地缓存变量中找到对应的 processor,封装成 task 任务提交给对应的业务 processor 处理线程池来执行(sendMessageExecutor,以发送消息为例) -* 从入口到业务逻辑的几个步骤中线程池一直再增加,这跟每一步逻辑复杂性相关,越复杂,需要的并发通道越宽 - -| 线程数 | 线程名 | 线程具体说明 | -| ------ | ------------------------------ | ------------------------- | -| 1 | NettyBoss_%d | Reactor 主线程 | -| N | NettyServerEPOLLSelector_%d_%d | Reactor 线程池 | -| M1 | NettyServerCodecThread_%d | Worker 线程池 | -| M2 | RemotingExecutorThread_%d | 业务 processor 处理线程池 | - - - -官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md#2-%E9%80%9A%E4%BF%A1%E6%9C%BA%E5%88%B6 - - - *** @@ -5215,3 +5147,556 @@ public class MessageListenerImpl implements MessageListener { ## 源码分析 + +### 服务启动 + +#### 启动方法 + +NamesrvStartup 类中有 Namesrv 服务的启动方法: + +```java +public static void main(String[] args) { + // 如果启动时 使用 -c -p 设置参数了,这些参数存储在 args 中 + main0(args); +} + +public static NamesrvController main0(String[] args) { + try { + // 创建 namesrv 控制器,用来初始化 namesrv 启动 namesrv 关闭 namesrv + NamesrvController controller = createNamesrvController(args); + // 启动 controller + start(controller); + return controller; + } catch (Throwable e) { + // 出现异常,停止系统 + System.exit(-1); + } + return null; +} +``` + +NamesrvStartup#createNamesrvController:读取配置信息,初始化 Namesrv 控制器 + +* `ServerUtil.parseCmdLine("mqnamesrv", args, buildCommandlineOptions(options),..)`:解析启动时的参数信息 + +* `namesrvConfig = new NamesrvConfig()`:创建 Namesrv 配置对象 + + * `private String rocketmqHome`:获取 ROCKETMQ_HOME 值 + * `private boolean orderMessageEnable = false`:顺序消息功能是否开启 + +* `nettyServerConfig = new NettyServerConfig()`:Netty 的服务器配置对象 + + ```java + public class NettyServerConfig implements Cloneable { + // 服务端启动时监听的端口号 + private int listenPort = 8888; + // 【业务线程池】 线程数量 + private int serverWorkerThreads = 8; + // 根据该值创建 remotingServer 内部的一个 publicExecutor + private int serverCallbackExecutorThreads = 0; + // netty 【worker】线程数 + private int serverSelectorThreads = 3; + // 【单向访问】时的并发限制 + private int serverOnewaySemaphoreValue = 256; + // 【异步访问】时的并发限制 + private int serverAsyncSemaphoreValue = 64; + // channel 最大的空闲存活时间 默认是 2min + private int serverChannelMaxIdleTimeSeconds = 120; + // 发送缓冲区大小 65535 + private int serverSocketSndBufSize = NettySystemConfig.socketSndbufSize; + // 接收缓冲区大小 65535 + private int serverSocketRcvBufSize = NettySystemConfig.socketRcvbufSize; + // 是否启用 netty 内存池 默认开启 + private boolean serverPooledByteBufAllocatorEnable = true; + + // 默认 linux 会启用 【epoll】 + private boolean useEpollNativeSelector = false; + } + ``` + +* `nettyServerConfig.setListenPort(9876)`:Namesrv 服务器的监听端口设置为 9876 + +* `if (commandLine.hasOption('c'))`:读取命令行 -c 的参数值 + + `in = new BufferedInputStream(new FileInputStream(file))`:读取指定目录的配置文件 + + `properties.load(in)`:将配置文件信息加载到 properties 对象,相关属性会复写到 Namesrv 配置和 Netty 配置对象 + + `namesrvConfig.setConfigStorePath(file)`:将配置文件的路径保存到配置保存字段 + +* `if (null == namesrvConfig.getRocketmqHome())`:检查 ROCKETMQ_HOME 配置是否是空,是空就报错 + +* `lc = (LoggerContext) LoggerFactory.getILoggerFactory()`:创建日志对象 + +* `controller = new NamesrvController(namesrvConfig, nettyServerConfig)`:**创建 Namesrv 控制器** + +NamesrvStartup#start:启动 Namesrv 控制器 + +* `boolean initResult = controller.initialize()`:初始化方法 + +* ` Runtime.getRuntime().addShutdownHook(new ShutdownHookThread())`:JVM HOOK 平滑关闭的逻辑, 当 JVM 被关闭时,主动调用 controller.shutdown() 方法,让服务器平滑关机 +* `controller.start()`:启动服务器 + + + +**** + + + + + +#### 控制器类 + +NamesrvController 用来初始化和启动 Namesrv 服务器 + +* 成员变量: + + ```java + private final ScheduledExecutorService scheduledExecutorService; // 调度线程池,用来执行定时任务 + private final RouteInfoManager routeInfoManager; // 管理【路由信息】的对象 + private RemotingServer remotingServer; // 【网络层】封装对象 + private ExecutorService remotingExecutor; // 业务线程池,用来 work + private BrokerHousekeepingService brokerHousekeepingService; // 用于监听 channel 状态 + private ExecutorService remotingExecutor; // 业务线程池 + ``` + +* 初始化: + + ```java + public boolean initialize() { + // 加载本地kv配置(我还不明白 kv 配置是啥) + this.kvConfigManager.load(); + // 创建网络服务器对象,【将 netty 的配置和监听器传入】 + // 监听器监听 channel 状态的改变,会向事件队列发起事件,最后交由 service 处理 + this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService); + // 【创建业务线程池,默认线程数 8】 + // netty 线程解析报文成 RemotingCommand 对象,然后将该对象交给业务线程池再继续处理。 + this.remotingExecutor = Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads().); + + // 注册协议处理器(缺省协议处理器),处理器是 DefaultRequestProcessor,线程使用的是刚创建的业务的线程池 + this.registerProcessor(); + + // 定时任务1:每 10 秒钟检查 broker 存活状态,将 IDLE 状态的 broker 移除。【心跳机制】 + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + // 将两小时没有活动的 broker 关闭,通过 next.getKey() 获取 broker 的地址 + // 然后【关闭服务器与broker物理节点的 channel】 + NamesrvController.this.routeInfoManager.scanNotActiveBroker(); + } + }, 5, 10, TimeUnit.SECONDS); + + // 定时任务2:每 10 分钟打印一遍 kv 配置。 + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + NamesrvController.this.kvConfigManager.printAllPeriodically(); + } + }, 1, 10, TimeUnit.MINUTES); + + return true; + } + ``` + +* 启动方法: + + ```java + public void start() throws Exception { + // 服务器网络层启动。 + this.remotingServer.start(); + + if (this.fileWatchService != null) { + this.fileWatchService.start(); + } + } + ``` + + + + + +*** + + + +#### 网络服务 + +##### 通信原理 + +RocketMQ 的 RPC 通信采用 Netty 组件作为底层通信库,同样也遵循了 Reactor 多线程模型,NettyRemotingServer 类负责框架的通信服务,同时又在这之上做了一些扩展和优化 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-Reactor设计.png) + +RocketMQ 基于 NettyRemotingServer 的 Reactor 多线程模型: + +* 一个 Reactor 主线程(eventLoopGroupBoss)负责监听 TCP 网络连接请求,建立好连接创建 SocketChannel(RocketMQ 会自动根据 OS 的类型选择 NIO 和 Epoll,也可以通过参数配置),并注册到 Selector 上,然后监听真正的网络数据 + +* 拿到网络数据交给 Worker 线程池(eventLoopGroupSelector,默认设置为 3),在真正执行业务逻辑之前需要进行 SSL 验证、编解码、空闲检查、网络连接管理,这些工作交给 defaultEventExecutorGroup(默认设置为 8)去做 +* 处理业务操作放在业务线程池中执行,根据 RomotingCommand 的业务请求码 code 去 processorTable 这个本地缓存变量中找到对应的 processor,封装成 task 任务提交给对应的业务 processor 处理线程池来执行(sendMessageExecutor,以发送消息为例) +* 从入口到业务逻辑的几个步骤中线程池一直再增加,这跟每一步逻辑复杂性相关,越复杂,需要的并发通道越宽 + +| 线程数 | 线程名 | 线程具体说明 | +| ------ | ------------------------------ | ------------------------- | +| 1 | NettyBoss_%d | Reactor 主线程 | +| N | NettyServerEPOLLSelector_%d_%d | Reactor 线程池 | +| M1 | NettyServerCodecThread_%d | Worker 线程池 | +| M2 | RemotingExecutorThread_%d | 业务 processor 处理线程池 | + +RocketMQ 的异步通信流程: + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-异步通信流程.png) + + + +==todo:后期对 Netty 有了更深的认知后会进行扩充,现在暂时 copy 官方文档== + +官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md#2-%E9%80%9A%E4%BF%A1%E6%9C%BA%E5%88%B6 + + + +*** + + + +##### 成员属性 + +成员变量: + +* 服务器相关属性: + + ```java + private final ServerBootstrap serverBootstrap; // netty 服务端启动对象 + private final EventLoopGroup eventLoopGroupSelector; // netty worker 组线程池,【默认 3 个线程】 + private final EventLoopGroup eventLoopGroupBoss; // netty boss 组线程池,【一般是 1 个线程】 + private final NettyServerConfig nettyServerConfig; // netty 服务端网络配置 + private int port = 0; // 服务器绑定的端口 + ``` + +* 公共线程池:注册处理器时如果未指定线程池,则业务处理使用公共线程池,线程数量默认是 4 + + ```java + private final ExecutorService publicExecutor; + ``` + +* 事件监听器:Nameserver 使用 BrokerHouseKeepingService,Broker 使用 ClientHouseKeepingService + + ```java + private final ChannelEventListener channelEventListener; + ``` + +* 事件处理线程池:默认是 8 + + ```java + private DefaultEventExecutorGroup defaultEventExecutorGroup; + ``` + +* 定时器:执行循环任务,并且将定时器线程设置为守护线程 + + ```java + private final Timer timer = new Timer("ServerHouseKeepingService", true); + ``` + +* 处理器:多个 Channel 共享的处理器 Handler,多个通道使用同一个对象 + + +构造方法: + +* 无监听器构造: + + ```java + public NettyRemotingServer(final NettyServerConfig nettyServerConfig) { + this(nettyServerConfig, null); + } + ``` + +* 有参构造方法: + + ```java + public NettyRemotingServer(final NettyServerConfig nettyServerConfig, + final ChannelEventListener channelEventListener) { + // 服务器对客户端主动发起请求时并发限制。【单向请求和异步请求】的并发限制 + super(nettyServerConfig.getServerOnewaySemaphoreValue(), nettyServerConfig.getServerAsyncSemaphoreValue()); + // Netty 的启动器,负责组装 netty 组件 + this.serverBootstrap = new ServerBootstrap(); + // 成员变量的赋值 + this.nettyServerConfig = nettyServerConfig; + this.channelEventListener = channelEventListener; + + // 公共线程池的线程数量,默认给的0,这里最终修改为4. + int publicThreadNums = nettyServerConfig.getServerCallbackExecutorThreads(); + if (publicThreadNums <= 0) { + publicThreadNums = 4; + } + // 创建公共线程池,指定线程工厂,设置线程名称前缀:NettyServerPublicExecutor_[数字] + this.publicExecutor = Executors.newFixedThreadPool(publicThreadNums, new ThreadFactory(){.}); + + // 创建两个 netty 的线程组,一个是boss组,一个是worker组,【linux 系统默认启用 epoll】 + if (useEpoll()) {...} else {...} + // SSL 相关 + loadSslContext(); + } + ``` + + + + + +*** + + + +##### 启动方法 + +核心方法的解析: + +* start():启动方法 + + ```java + public void start() { + // 向 channel pipeline 添加 handler,网络事件传播到当前 handler 时,【线程分配给 handler 处理事件】 + this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(...); + + // 创建通用共享的处理器 handler,【非常重要的 NettyServerHandler】 + prepareSharableHandlers(); + + ServerBootstrap childHandler = + // 配置工作组 boss(数量1) 和 worker(数量3) 组 + this.serverBootstrap.group(this.eventLoopGroupBoss, this.eventLoopGroupSelector) + // 设置服务端 ServerSocketChannel 类型, Linux 用 epoll + .channel(useEpoll() ? EpollServerSocketChannel.class : NioServerSocketChannel.class) + // 设置服务端 channel 选项 + .option(ChannelOption.SO_BACKLOG, 1024) + // 客户端 channel 选项 + .childOption(ChannelOption.TCP_NODELAY, true) + // 设置服务器端口 + .localAddress(new InetSocketAddress(this.nettyServerConfig.getListenPort())) + // 向 channel pipeline 添加了很多 handler,包括 NettyServerHandler + .childHandler(new ChannelInitializer() {}); + + // 客户端开启 内存池,使用的内存池是 PooledByteBufAllocator.DEFAULT + if (nettyServerConfig.isServerPooledByteBufAllocatorEnable()) { + childHandler.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT); + } + + try { + // 同步等待建立连接,并绑定端口。 + ChannelFuture sync = this.serverBootstrap.bind().sync(); + InetSocketAddress addr = (InetSocketAddress) sync.channel().localAddress(); + // 将服务器成功绑定的端口号赋值给字段 port。 + this.port = addr.getPort(); + } catch (InterruptedException e1) {} + + // housekeepingService 不为空,则创建【网络异常事件处理器】 + if (this.channelEventListener != null) { + // 线程一直轮询 nettyEventExecutor 状态,根据 CONNECT,CLOSE,IDLE,EXCEPTION 四种事件类型 + // CONNECT 不做操作,其余都是回调 onChannelDestroy 关闭服务器与 Broker 物理节点的 Channel + this.nettyEventExecutor.start(); + } + + // 提交定时任务,每一秒 执行一次。扫描 responseTable 表,将过期的 请求 移除。 + this.timer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + NettyRemotingServer.this.scanResponseTable(); + } + }, 1000 * 3, 1000); + } + ``` + +* registerProcessor():注册业务处理器 + + ```java + public void registerProcessor(int requestCode, NettyRequestProcessor processor, ExecutorService executor) { + ExecutorService executorThis = executor; + if (null == executor) { + // 未指定线程池资源,将公共线程池赋值 + executorThis = this.publicExecutor; + } + // pair 对象,第一个参数代表的是处理器, 第二个参数是线程池,默认是公共的线程池 + Pair pair = new Pair(processor, executorThis); + + // key 是请求码,value 是 Pair 对象 + this.processorTable.put(requestCode, pair); + } + ``` + +* getProcessorPair():**根据请求码获取对应的处理器和线程池资源** + + ```java + public Pair getProcessorPair(int requestCode) { + return processorTable.get(requestCode); + } + ``` + + + +*** + + + +##### 请求方法 + +在 RocketMQ 消息队列中支持通信的方式主要有同步(sync)、异步(async)、单向(oneway)三种,其中单向通信模式相对简单,一般用在发送心跳包场景下,无需关注其 Response + +服务器主动向客户端发起请求时,使用三种方法 + +* invokeSync(): 同步调用,服务器需要阻塞等待调用的返回结果 + * `int opaque = request.getOpaque()`:获取请求 ID(与请求码不同) + * `responseFuture = new ResponseFuture(...)`:创建响应对象,将请求 ID、通道、超时时间传入,没有回调函数和 Once + * `this.responseTable.put(opaque, responseFuture)`:**加入到响应映射表中**,key 为请求 ID + * `SocketAddress addr = channel.remoteAddress()`:获取客户端的地址信息 + * `channel.writeAndFlush(request).addListener(...)`:将**业务 Command 信息**写入通道,业务线程将数据交给 Netty ,Netty 的 IO 线程接管写刷数据的操作,**监听器由 IO 线程在写刷后回调** + * `if (f.isSuccess())`:写入成功会将响应对象设置为成功状态直接 return,写入失败设置为失败状态 + * `responseTable.remove(opaque)`:将当前请求的 responseFuture **从映射表移除** + * `responseFuture.setCause(f.cause())`:设置错误的信息 + * `responseFuture.putResponse(null)`:请求的业务码设置为 null + * `responseCommand = responseFuture.waitResponse(timeoutMillis)`:当前线程设置超时时间挂起,**同步等待响应** + * `if (null == responseCommand)`:超时或者出现异常,直接报错 + * `return responseCommand`:返回响应 Command 信息 +* invokeAsync():异步调用,有回调对象,无返回值 + * `boolean acquired = this.semaphoreAsync.tryAcquire(timeoutMillis, TimeUnit.MILLISECONDS)`:获取信号量的许可证,信号量用来**限制异步请求**的数量 + * `if (acquired)`:许可证获取失败说明并发较高,会抛出异常 + * `once = new SemaphoreReleaseOnlyOnce(this.semaphoreAsync)`:Once 对象封装了释放信号量的操作 + * `costTime = System.currentTimeMillis() - beginStartTime`:计算一下耗费的时间,超时不再发起请求 + * `responseFuture = new ResponseFuture()`:创建响应对象,包装了回调函数和 Once 对象 + * `this.responseTable.put(opaque, responseFuture)`:加入到响应映射表中,key 为请求 ID + * `channel.writeAndFlush(request).addListener(...)`:写刷数据 + * `if (f.isSuccess())`:写刷成功,设置 responseFuture 发生状态为 true + * `requestFail(opaque)`:写入失败,使用 publicExecutor **公共线程池异步执行回调对象的函数** + * `responseFuture.release()`:出现异常会释放信号量 + +* invokeOneway():单向调用,不关注响应结果 + * `request.markOnewayRPC()`:设置单向标记,对端检查标记可知该请是单向请求 + * `boolean acquired = this.semaphoreOneway.tryAcquire(timeoutMillis, TimeUnit.MILLISECONDS)`:获取信号量的许可证,信号量用来**限制单向请求**的数量 + + + + + +*** + + + +#### 处理器类 + +##### 协议设计 + +在 Client 和 Server 之间完成一次消息发送时,需要对发送的消息进行一个协议约定,所以自定义 RocketMQ 的消息协议。在 RocketMQ 中,为了高效地在网络中传输消息和对收到的消息读取,就需要对消息进行编解码,RemotingCommand 这个类在消息传输过程中对所有数据内容的封装,不但包含了所有的数据结构,还包含了编码解码操作 + +| Header字段 | 类型 | Request 说明 | Response 说明 | +| ---------- | ----------------------- | ------------------------------------------------------------ | ------------------------------------------- | +| code | int | 请求操作码,应答方根据不同的请求码进行不同的处理 | 应答响应码,0 表示成功,非 0 则表示各种错误 | +| language | LanguageCode | 请求方实现的语言 | 应答方实现的语言 | +| version | int | 请求方程序的版本 | 应答方程序的版本 | +| opaque | int | 相当于 requestId,在同一个连接上的不同请求标识码,与响应消息中的相对应 | 应答不做修改直接返回 | +| flag | int | 区分是普通 RPC 还是 onewayRPC 的标志 | 区分是普通 RPC 还是 onewayRPC的标志 | +| remark | String | 传输自定义文本信息 | 传输自定义文本信息 | +| extFields | HashMap | 请求自定义扩展信息 | 响应自定义扩展信息 | + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息协议.png) + +传输内容主要可以分为以下四部分: + +* 消息长度:总长度,四个字节存储,占用一个 int 类型 + +* 序列化类型&消息头长度:同样占用一个 int 类型,第一个字节表示序列化类型,后面三个字节表示消息头长度 + +* 消息头数据:经过序列化后的消息头数据 + +* 消息主体数据:消息主体的二进制字节数据内容 + + + +官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md#22-%E5%8D%8F%E8%AE%AE%E8%AE%BE%E8%AE%A1%E4%B8%8E%E7%BC%96%E8%A7%A3%E7%A0%81 + + + +**** + + + +##### 处理方法 + +NettyServerHandler 类用来处理 RemotingCommand 相关的数据,针对某一种类型的**请求处理** + +```java +class NettyServerHandler extends SimpleChannelInboundHandler { + @Override + protected void channelRead0(ChannelHandlerContext ctx, RemotingCommand msg) throws Exception { + // 服务器处理接受到的请求信息 + processMessageReceived(ctx, msg); + } +} +public void processMessageReceived(ChannelHandlerContext ctx, RemotingCommand msg) throws Exception { + final RemotingCommand cmd = msg; + if (cmd != null) { + // 根据请求的类型进行处理 + switch (cmd.getType()) { + case REQUEST_COMMAND:// 客户端发起的请求,走这里 + processRequestCommand(ctx, cmd); + break; + case RESPONSE_COMMAND:// 客户端响应的数据,走这里【当前类本身是服务器类也是客户端类】 + processResponseCommand(ctx, cmd); + break; + default: + break; + } + } +} +``` + +NettyRemotingAbstract#processRequestCommand:处理请求的数据 + +* `matched = this.processorTable.get(cmd.getCode())`:根据业务请求码获取 Pair 对象,包含**处理器和线程池资源** + +* `pair = null == matched ? this.defaultRequestProcessor : matched`:未找到处理器则使用缺省处理器 + +* `int opaque = cmd.getOpaque()`:获取请求 ID + +* `Runnable run = new Runnable()`:创建任务对象 + + * `doBeforeRpcHooks()`:RPC HOOK 前置处理 + + * `callback = new RemotingResponseCallback()`:封装响应客户端逻辑 + + * `doAfterRpcHooks()`:RPC HOOK 后置处理 + * `if (!cmd.isOnewayRPC())`:条件成立说明不是单向请求,需要结果 + * `response.setOpaque(opaque)`:将请求 ID 设置到 response + * `response.markResponseType()`:设置当前的处理是响应处理 + * `ctx.writeAndFlush(response)`: 将数据交给 Netty IO 线程,完成数据写和刷 + + * `if (pair.getObject1() instanceof AsyncNettyRequestProcessor)`:Nameserver 默认使用 DefaultRequestProcessor 处理器,是一个 AsyncNettyRequestProcessor 子类 + + * `processor = (AsyncNettyRequestProcessor)pair.getObject1()`:获取处理器 + + * `processor.asyncProcessRequest(ctx, cmd, callback)`:异步调用,首先 processRequest,然后 callback 响应客户端 + + DefaultRequestProcessor.processRequest **根据业务码处理请求,执行对应的操作** + +* `requestTask = new RequestTask(run, ctx.channel(), cmd)`:将任务对象、通道、请求封装成 RequestTask 对象 + +* `pair.getObject2().submit(requestTask)`:获取处理器对应的线程池,将 task 提交,**从 IO 线程切换到业务线程** + +NettyRemotingAbstract#processResponseCommand:处理响应的数据 + +* `int opaque = cmd.getOpaque()`:获取请求 ID +* `responseFuture = responseTable.get(opaque)`:**从响应映射表中获取对应的对象** +* `responseFuture.setResponseCommand(cmd)`:设置响应的 Command 对象 +* `responseTable.remove(opaque)`:从映射表中移除对象,代表处理完成 +* `if (responseFuture.getInvokeCallback() != null)`:包含回调对象,异步执行回调对象 +* `responseFuture.putResponse(cmd)`:不好含回调对象,**同步调用时,需要唤醒等待的业务线程** + + + +流程:客户端 invokeSync → 服务器的 processRequestCommand → 客户端的 processResponseCommand → 结束 + + + + + + + + + + + diff --git a/Java.md b/Java.md index 0d8a455..d1f0b60 100644 --- a/Java.md +++ b/Java.md @@ -2320,6 +2320,8 @@ hashCode 的作用: #### 深浅克隆 +Object 的 clone() 是 protected 方法,一个类不显式去重写 clone(),就不能直接去调用该类实例的 clone() 方法 + 深浅拷贝(克隆)的概念: * 浅拷贝 (shallowCopy):**对基本数据类型进行值传递,对引用数据类型只是复制了引用**,被复制对象属性的所有的引用仍然指向原来的对象,简而言之就是增加了一个指针指向原来对象的内存地址 @@ -2328,15 +2330,13 @@ hashCode 的作用: * 深拷贝 (deepCopy):对基本数据类型进行值传递,对引用数据类型是一个整个独立的对象拷贝,会拷贝所有的属性并指向的动态分配的内存,简而言之就是把所有属性复制到一个新的内存,增加一个指针指向新内存。所以使用深拷贝的情况下,释放内存的时候不会出现使用浅拷贝时释放同一块内存的错误 -Object 的 clone() 是 protected 方法,一个类不显式去重写 clone(),就不能直接去调用该类实例的 clone() 方法 - Cloneable 接口是一个标识性接口,即该接口不包含任何方法(包括clone()),但是如果一个类想合法的进行克隆,那么就必须实现这个接口,在使用 clone() 方法时,若该类未实现 Cloneable 接口,则抛出异常 * Clone & Copy:`Student s = new Student` `Student s1 = s`:只是 copy 了一下 reference,s 和 s1 指向内存中同一个 Object,对对象的修改会影响对方 - `Student s2 = s.clone()`:会生成一个新的 Student 对象,并且和s具有相同的属性值和方法 + `Student s2 = s.clone()`:会生成一个新的 Student 对象,并且和 s 具有相同的属性值和方法 * Shallow Clone & Deep Clone: @@ -16085,7 +16085,7 @@ public class QuickSort { 实现思路: -- 获得最大数的位数,可以通过将最大数变为String类型,再求长度 +- 获得最大数的位数,可以通过将最大数变为 String 类型,再求长度 - 将所有待比较数值(正整数)统一为同样的数位长度,**位数较短的数前面补零** - 从最低位开始,依次进行一次排序 - 从最低位排序一直到最高位(个位 → 十位 → 百位 → … →最高位)排序完成以后,数列就变成一个有序序列 From 74f98a667c84532feb5a859d7c9f5d3be5e2451f Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 29 Dec 2021 23:29:46 +0800 Subject: [PATCH 097/168] Update README --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6433260..6f633dd 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -**Java** 学习笔记,记录着作者从编程入门到深入学习的所有知识,每次学有所获就会更新笔记,所有的知识都有借鉴其他前辈的博客和文章,排版布局**美观整洁**,如果对各位朋友有所帮助,希望可以给个 star。 +**Java** 学习笔记,记录作者从编程入门到深入学习的所有知识,每次学有所获都会更新笔记,排版布局**美观整洁**,希望对读者朋友有所帮助。 个人邮箱:imseazean@gmail.com @@ -15,8 +15,7 @@ 其他说明: * 推荐使用 Typora 阅读笔记,打开目录栏效果更佳。 -* 所有的知识不保证权威性,如果各位朋友发现错误,非常欢迎与我讨论。 -* Java.md 更新后大于 1M,导致网页无法显示,所以划分为 Java 和 Program 两个文档。 +* 所有的知识不保证权威性,如果各位朋友发现错误,欢迎与我讨论。 +* 笔记的编写基于 Windows 平台,可能会因为平台的不同而造成空格、制表符的显示效果不同。 -* 笔记的编写是基于 Windows 平台,可能会因为平台的不同而造成空格、制表符的显示效果不同。 From 21717ee194664be70adc062ccaa7d5198db3012e Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 29 Dec 2021 23:31:29 +0800 Subject: [PATCH 098/168] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6f633dd..5b83132 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -**Java** 学习笔记,记录作者从编程入门到深入学习的所有知识,每次学有所获都会更新笔记,排版布局**美观整洁**,希望对读者朋友有所帮助。 +**Java** 学习笔记,记录作者从编程入门到深入学习的所有知识,每次学有所获都会更新笔记,排版布局**美观整洁**,希望对各位读者朋友有所帮助。 个人邮箱:imseazean@gmail.com From a555f81dd9942c557efeac208928b6af0df5f7e5 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 1 Jan 2022 22:54:15 +0800 Subject: [PATCH 099/168] Update Java Notes --- DB.md | 18 +- Frame.md | 1002 +++++++++++++++++++++++++++++++++++++++++++++++++++--- Java.md | 2 +- Web.md | 24 +- 4 files changed, 983 insertions(+), 63 deletions(-) diff --git a/DB.md b/DB.md index ebf0128..2564c46 100644 --- a/DB.md +++ b/DB.md @@ -1226,7 +1226,7 @@ LIMIT SELECT 列名1,列名2,... FROM 表名; ``` -* 去除重复查询:只有值全部重复的才可以去除,需要创建临时表辅助查询 +* **去除重复查询**:只有值全部重复的才可以去除,需要创建临时表辅助查询 ```mysql SELECT DISTINCT 列名1,列名2,... FROM 表名; @@ -1556,6 +1556,8 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 #### 分组查询 +分组查询会进行去重 + * 分组查询语法 ````mysql @@ -2052,6 +2054,8 @@ Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 + + *** @@ -2074,6 +2078,8 @@ Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 SELECT 列名 FROM 表名1,表名2 WHERE 条件; ``` +内连接中 WHERE 子句和 ON 子句是等价的 + @@ -2085,7 +2091,9 @@ Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 外连接查询,若驱动表中的记录在被驱动表中找不到匹配的记录时,则该记录也会加到最后的结果集,只是对于被驱动表中**不匹配过滤条件**的记录,各个字段使用 NULL 填充 -应用实例:差学生成绩,也想查出缺考的人的成绩 +应用实例:查学生成绩,也想查出缺考的人的成绩 + +(内连接快还是外连接快?) * 左外连接:选择左侧的表为驱动表,查询左表的全部数据,和左右两张表有交集部分的数据 @@ -2232,7 +2240,9 @@ Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 #### 查询优化 -不相关子查询的结果集会被写入一个临时表,并且在写入时**去重**,该过程称为**物化**,存储结果集的临时表称为物化表。系统变量 tmp_table_size 或者 max_heap_table_size 为表的最值 +不相关子查询的结果集会被写入一个临时表,并且在写入时**去重**,该过程称为**物化**,存储结果集的临时表称为物化表。 + +系统变量 tmp_table_size 或者 max_heap_table_size 为表的最值 * 小于系统变量时,内存中可以保存,会为建立基于内存的 MEMORY 存储引擎的临时表,并建立哈希索引 * 大于任意一个系统变量时,物化表会使用基于磁盘的存储引擎来保存结果集中的记录,索引类型为 B+ 树 @@ -2246,7 +2256,7 @@ Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 -详细内容可以参考:《MySQL 是怎样运行的》 +参考书籍:https://book.douban.com/subject/35231266/ diff --git a/Frame.md b/Frame.md index 4dc7ebc..b5c4d88 100644 --- a/Frame.md +++ b/Frame.md @@ -4502,7 +4502,7 @@ NameServer 是一个简单的 Topic 路由注册中心,支持 Broker 的动态 NameServer 主要包括两个功能: -* Broker 路由管理,NameServer 接受 Broker 集群的注册信息,并保存下来作为路由信息的基本数据,提供**心跳检测机制**检查 Broker 活性(每 10 秒) +* Broker 路由管理,NameServer 接受 Broker 集群的注册信息,并保存下来作为路由信息的基本数据,提供**心跳检测机制**检查 Broker 活性,每 10 秒清除一次两小时没有活跃的 Broker * 路由信息管理,每个 NameServer 将保存关于 Broker 集群的整个路由信息和用于客户端查询的队列信息,然后 Producer 和 Conumser 通过 NameServer 就可以知道整个 Broker 集群的路由信息,从而进行消息的投递和消费 NameServer 特点: @@ -4581,7 +4581,7 @@ At least Once:至少一次,指每个消息必须投递一次,Consumer 先 #### 存储结构 -Broker 负责存储消息转发消息,所以以下的结构是存储在 Broker Server 上的 +RocketMQ 中 Broker 负责存储消息转发消息,所以以下的结构是存储在 Broker Server 上的,生产者和消费者与 Broker 进行消息的收发是通过主题对应的 Message Queue 完成,类似于通道 RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成 的,消息真正的物理存储文件是 CommitLog,ConsumeQueue 是消息的逻辑队列,类似数据库的索引节点,存储的是指向物理存储的地址。**每个 Topic 下的每个 Message Queue 都有一个对应的 ConsumeQueue 文件** @@ -5030,15 +5030,19 @@ public class MessageListenerImpl implements MessageListener { #### 重投机制 -生产者在发送消息时,同步消息失败会重投,异步消息有重试,oneway 没有任何保证。消息重投保证消息尽可能发送成功、不丢失,但当出现消息量大、网络抖动时,可能会造成消息重复;生产者主动重发、Consumer 负载变化也会导致重复消息。 +生产者在发送消息时,同步消息和异步消息失败会重投,oneway 没有任何保证。消息重投保证消息尽可能发送成功、不丢失,但当出现消息量大、网络抖动时,可能会造成消息重复;生产者主动重发、Consumer 负载变化也会导致重复消息。 -消息重复在 RocketMQ 中是无法避免的问题,如下方法可以设置消息重试策略: +消息重复在 RocketMQ 中是无法避免的问题,如下方法可以设置消息重投策略: - retryTimesWhenSendFailed:同步发送失败重投次数,默认为 2,因此生产者会最多尝试发送 retryTimesWhenSendFailed + 1 次。不会选择上次失败的 Broker,尝试向其他 Broker 发送,最大程度保证消息不丢。超过重投次数抛出异常,由客户端保证消息不丢。当出现 RemotingException、MQClientException 和部分 MQBrokerException 时会重投 - retryTimesWhenSendAsyncFailed:异步发送失败重试次数,异步重试不会选择其他 Broker,仅在同一个 Broker 上做重试,不保证消息不丢 - retryAnotherBrokerWhenNotStoreOK:消息刷盘(主或备)超时或 slave 不可用(返回状态非 SEND_OK),是否尝试发送到其他 Broker,默认 false,十分重要消息可以开启 +注意点: +* 如果同步模式发送失败,则选择到下一个 Broker,如果异步模式发送失败,则**只会在当前 Broker 进行重试** + +* 发送消息超时时间默认3000毫秒,就不会再尝试重试 @@ -5148,7 +5152,7 @@ public class MessageListenerImpl implements MessageListener { ## 源码分析 -### 服务启动 +### 服务端 #### 启动方法 @@ -5186,34 +5190,6 @@ NamesrvStartup#createNamesrvController:读取配置信息,初始化 Namesrv * `nettyServerConfig = new NettyServerConfig()`:Netty 的服务器配置对象 - ```java - public class NettyServerConfig implements Cloneable { - // 服务端启动时监听的端口号 - private int listenPort = 8888; - // 【业务线程池】 线程数量 - private int serverWorkerThreads = 8; - // 根据该值创建 remotingServer 内部的一个 publicExecutor - private int serverCallbackExecutorThreads = 0; - // netty 【worker】线程数 - private int serverSelectorThreads = 3; - // 【单向访问】时的并发限制 - private int serverOnewaySemaphoreValue = 256; - // 【异步访问】时的并发限制 - private int serverAsyncSemaphoreValue = 64; - // channel 最大的空闲存活时间 默认是 2min - private int serverChannelMaxIdleTimeSeconds = 120; - // 发送缓冲区大小 65535 - private int serverSocketSndBufSize = NettySystemConfig.socketSndbufSize; - // 接收缓冲区大小 65535 - private int serverSocketRcvBufSize = NettySystemConfig.socketRcvbufSize; - // 是否启用 netty 内存池 默认开启 - private boolean serverPooledByteBufAllocatorEnable = true; - - // 默认 linux 会启用 【epoll】 - private boolean useEpollNativeSelector = false; - } - ``` - * `nettyServerConfig.setListenPort(9876)`:Namesrv 服务器的监听端口设置为 9876 * `if (commandLine.hasOption('c'))`:读取命令行 -c 的参数值 @@ -5276,12 +5252,12 @@ NamesrvController 用来初始化和启动 Namesrv 服务器 // 注册协议处理器(缺省协议处理器),处理器是 DefaultRequestProcessor,线程使用的是刚创建的业务的线程池 this.registerProcessor(); - // 定时任务1:每 10 秒钟检查 broker 存活状态,将 IDLE 状态的 broker 移除。【心跳机制】 + // 定时任务1:每 10 秒钟检查 broker 存活状态,将 IDLE 状态的 broker 移除【扫描机制】 this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { @Override public void run() { - // 将两小时没有活动的 broker 关闭,通过 next.getKey() 获取 broker 的地址 - // 然后【关闭服务器与broker物理节点的 channel】 + // 扫描 brokerLiveTable 表,将两小时没有活动的 broker 关闭, + //通过 next.getKey() 获取 broker 的地址,然后【关闭服务器与broker物理节点的 channel】 NamesrvController.this.routeInfoManager.scanNotActiveBroker(); } }, 5, 10, TimeUnit.SECONDS); @@ -5319,7 +5295,7 @@ NamesrvController 用来初始化和启动 Namesrv 服务器 -#### 网络服务 +#### 网络通信 ##### 通信原理 @@ -5360,7 +5336,7 @@ RocketMQ 的异步通信流程: ##### 成员属性 -成员变量: +NettyRemotingServer 类成员变量: * 服务器相关属性: @@ -5398,6 +5374,36 @@ RocketMQ 的异步通信流程: * 处理器:多个 Channel 共享的处理器 Handler,多个通道使用同一个对象 +* Netty 配置对象: + + ```java + public class NettyServerConfig implements Cloneable { + // 服务端启动时监听的端口号 + private int listenPort = 8888; + // 【业务线程池】 线程数量 + private int serverWorkerThreads = 8; + // 根据该值创建 remotingServer 内部的一个 publicExecutor + private int serverCallbackExecutorThreads = 0; + // netty 【worker】线程数 + private int serverSelectorThreads = 3; + // 【单向访问】时的并发限制 + private int serverOnewaySemaphoreValue = 256; + // 【异步访问】时的并发限制 + private int serverAsyncSemaphoreValue = 64; + // channel 最大的空闲存活时间 默认是 2min + private int serverChannelMaxIdleTimeSeconds = 120; + // 发送缓冲区大小 65535 + private int serverSocketSndBufSize = NettySystemConfig.socketSndbufSize; + // 接收缓冲区大小 65535 + private int serverSocketRcvBufSize = NettySystemConfig.socketRcvbufSize; + // 是否启用 netty 内存池 默认开启 + private boolean serverPooledByteBufAllocatorEnable = true; + + // 默认 linux 会启用 【epoll】 + private boolean useEpollNativeSelector = false; + } + ``` + 构造方法: @@ -5453,7 +5459,7 @@ RocketMQ 的异步通信流程: ```java public void start() { - // 向 channel pipeline 添加 handler,网络事件传播到当前 handler 时,【线程分配给 handler 处理事件】 + // Channel Pipeline 内的 handler 使用的线程资源,【线程分配给 handler 处理事件】 this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(...); // 创建通用共享的处理器 handler,【非常重要的 NettyServerHandler】 @@ -5493,7 +5499,7 @@ RocketMQ 的异步通信流程: this.nettyEventExecutor.start(); } - // 提交定时任务,每一秒 执行一次。扫描 responseTable 表,将过期的 请求 移除。 + // 提交定时任务,每一秒 执行一次。扫描 responseTable 表,将过期的数据移除。 this.timer.scheduleAtFixedRate(new TimerTask() { @Override public void run() { @@ -5653,17 +5659,17 @@ NettyRemotingAbstract#processRequestCommand:处理请求的数据 * `int opaque = cmd.getOpaque()`:获取请求 ID -* `Runnable run = new Runnable()`:创建任务对象 +* `Runnable run = new Runnable()`:创建任务对象,任务在提交到线程池后开始执行 * `doBeforeRpcHooks()`:RPC HOOK 前置处理 - * `callback = new RemotingResponseCallback()`:封装响应客户端逻辑 + * `callback = new RemotingResponseCallback()`:**封装响应客户端的逻辑** * `doAfterRpcHooks()`:RPC HOOK 后置处理 * `if (!cmd.isOnewayRPC())`:条件成立说明不是单向请求,需要结果 * `response.setOpaque(opaque)`:将请求 ID 设置到 response * `response.markResponseType()`:设置当前的处理是响应处理 - * `ctx.writeAndFlush(response)`: 将数据交给 Netty IO 线程,完成数据写和刷 + * `ctx.writeAndFlush(response)`: **将响应数据交给 Netty IO 线程,完成数据写和刷** * `if (pair.getObject1() instanceof AsyncNettyRequestProcessor)`:Nameserver 默认使用 DefaultRequestProcessor 处理器,是一个 AsyncNettyRequestProcessor 子类 @@ -5671,7 +5677,9 @@ NettyRemotingAbstract#processRequestCommand:处理请求的数据 * `processor.asyncProcessRequest(ctx, cmd, callback)`:异步调用,首先 processRequest,然后 callback 响应客户端 - DefaultRequestProcessor.processRequest **根据业务码处理请求,执行对应的操作** + `DefaultRequestProcessor.processRequest`:**根据业务码处理请求,执行对应的操作** + + `ClientRemotingProcessor.processRequest`:处理回退消息,需要消费者回执一条消息给生产者 * `requestTask = new RequestTask(run, ctx.channel(), cmd)`:将任务对象、通道、请求封装成 RequestTask 对象 @@ -5684,15 +5692,917 @@ NettyRemotingAbstract#processResponseCommand:处理响应的数据 * `responseFuture.setResponseCommand(cmd)`:设置响应的 Command 对象 * `responseTable.remove(opaque)`:从映射表中移除对象,代表处理完成 * `if (responseFuture.getInvokeCallback() != null)`:包含回调对象,异步执行回调对象 -* `responseFuture.putResponse(cmd)`:不好含回调对象,**同步调用时,需要唤醒等待的业务线程** +* `responseFuture.putResponse(cmd)`:不包含回调对象,**同步调用时,唤醒等待的业务线程** +流程:客户端 invokeSync → 服务器的 processRequestCommand → 客户端的 processResponseCommand → 结束 -流程:客户端 invokeSync → 服务器的 processRequestCommand → 客户端的 processResponseCommand → 结束 +*** + + + +##### 路由注册 + +DefaultRequestProcessor REGISTER_BROKER 方法解析: + +```java +public RemotingCommand registerBroker(ChannelHandlerContext ctx, RemotingCommand request) { + // 创建响应请求的对象,设置为响应类型,【先设置响应的状态码时系统错误码】 + // 反射创建 RegisterBrokerResponseHeader 对象设置到 response.customHeader 属性中 + final RemotingCommand response = RemotingCommand.createResponseCommand(RegisterBrokerResponseHeader.class); + + // 获取出反射创建的 RegisterBrokerResponseHeader 用户自定义header对象。 + final RegisterBrokerResponseHeader responseHeader = (RegisterBrokerResponseHeader) response.readCustomHeader(); + + // 反射创建 RegisterBrokerRequestHeader 对象,并且将 request.extFields 中的数据写入到该对象中 + final RegisterBrokerRequestHeader requestHeader = request.decodeCommandCustomHeader(RegisterBrokerRequestHeader.class); + + // CRC 校验,计算请求中的 CRC 值和请求头中包含的是否一致 + if (!checksum(ctx, request, requestHeader)) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("crc32 not match"); + return response; + } + + TopicConfigSerializeWrapper topicConfigWrapper; + if (request.getBody() != null) { + // 【解析请求体 body】,解码出来的数据就是当前机器的主题信息 + topicConfigWrapper = TopicConfigSerializeWrapper.decode(request.getBody(), TopicConfigSerializeWrapper.class); + } else { + topicConfigWrapper = new TopicConfigSerializeWrapper(); + topicConfigWrapper.getDataVersion().setCounter(new AtomicLong(0)); + topicConfigWrapper.getDataVersion().setTimestamp(0); + } + + // 注册方法 + // 参数1 集群、参数2:节点ip地址、参数3:brokerName、参数4:brokerId 注意brokerId=0的节点为主节点 + // 参数5:ha节点ip地址、参数6当前节点主题信息、参数7:过滤服务器列表、参数8:当前服务器和客户端通信的channel + RegisterBrokerResult result = this.namesrvController.getRouteInfoManager().registerBroker(..); + + // 将结果信息 写到 responseHeader 中 + responseHeader.setHaServerAddr(result.getHaServerAddr()); + responseHeader.setMasterAddr(result.getMasterAddr()); + // 获取 kv配置,写入 response body 中,【kv 配置是顺序消息相关的】 + byte[] jsonValue = this.namesrvController.getKvConfigManager().getKVListByNamespace(NamesrvUtil.NAMESPACE_ORDER_TOPIC); + response.setBody(jsonValue); + + // code 设置为 SUCCESS + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + // 返回 response ,【返回的 response 由 callback 对象处理】 + return response; +} +``` + +RouteInfoManager#registerBroker:注册 Broker 的信息 + +* `RegisterBrokerResult result = new RegisterBrokerResult()`:返回结果的封装对象 + +* `this.lock.writeLock().lockInterruptibly()`:加写锁后**同步执行** + +* `brokerNames = this.clusterAddrTable.get(clusterName)`:获取当前集群上的 Broker 名称列表,是空就新建列表 + +* `brokerNames.add(brokerName)`:将当前 Broker 名字加入到集群列表 + +* `brokerData = this.brokerAddrTable.get(brokerName)`:获取当前 Broker 的 brokerData,是空就新建放入映射表 + +* `brokerAddrsMap = brokerData.getBrokerAddrs()`:获取当前 Broker 的物理节点 map 表,进行遍历,如果物理节点角色发生变化(slave → master),先将旧数据从物理节点 map 中移除,然后重写放入,**保证节点的唯一性** + +* `if (null != topicConfigWrapper && MixAll.MASTER_ID == brokerId)`:Broker 上的 Topic 不为 null,并且当前物理节点是 Broker 上的 master 节点 + + `tcTable = topicConfigWrapper.getTopicConfigTable()`:获取当前 Broker 信息中的主题映射表 + + `if (tcTable != null)`:映射表不空就加入或者更新到 Namesrv 内 + +* ` prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr)`:添加**当前节点的 BrokerLiveInfo** ,返回上一次心跳时当前 Broker 节点的存活对象数据。**NamesrvController 中的定时任务会扫描映射表 brokerLiveTable** + + ```java + BrokerLiveInfo prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr, new BrokerLiveInfo( + System.currentTimeMillis(),topicConfigWrapper.getDataVersion(), channel,haServerAddr)); + ``` + +* `if (MixAll.MASTER_ID != brokerId)`:当前 Broker 不是 master 节点,**获取主节点的信息**设置到结果对象 + +* `this.lock.writeLock().unlock()`:释放写锁 + + + +**** + + + +### 生产者 + +#### 生产者类 + +DefaultMQProducer 是生产者的默认实现类 + +成员变量: + +* 生产者实现类: + + ```java + protected final transient DefaultMQProducerImpl defaultMQProducerImpl + ``` + +* 生产者组:发送事务消息,Broker 端进行事务回查(补偿机制)时,选择当前生产者组的下一个生产者进行事务回查 + + ```java + private String producerGroup; + ``` + +* 默认主题:isAutoCreateTopicEnable 开启时,当发送消息指定的 Topic 在 Namesrv 未找到路由信息,使用该值创建 Topic 信息 + + ```java + private String createTopicKey = TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC; + // 值为【TBW102】,Just for testing or demo program + ``` + +* 消息重投:系统特性消息重试部分详解了三个参数的作用 + + ```java + private int retryTimesWhenSendFailed = 2; // 同步发送失败后重试的发送次数,加上第一次发送,一共三次 + private int retryTimesWhenSendAsyncFailed = 2; // 异步 + private boolean retryAnotherBrokerWhenNotStoreOK = false; // 消息未存储成功,选择其他 Broker 重试 + ``` + +* 消息队列: + + ```java + private volatile int defaultTopicQueueNums = 4; // 默认 Broker 创建的队列数 + ``` + +* 消息属性: + + ```java + private int sendMsgTimeout = 3000; // 发送消息的超时限制 + private int compressMsgBodyOverHowmuch = 1024 * 4; // 压缩阈值,当 msg body 超过 4k 后使用压缩 + private int maxMessageSize = 1024 * 1024 * 4; // 消息体的最大限制,默认 4M + private TraceDispatcher traceDispatcher = null; // 消息轨迹 + +构造方法: + +* 构造方法: + + ```java + public DefaultMQProducer(final String namespace, final String producerGroup, RPCHook rpcHook) { + this.namespace = namespace; + this.producerGroup = producerGroup; + // 创建生产者实现对象 + defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook); + } + ``` + +成员方法: + +* start():启动方法 + + ```java + public void start() throws MQClientException { + // 重置生产者组名,如果传递了命名空间,则 【namespace%group】 + this.setProducerGroup(withNamespace(this.producerGroup)); + // 生产者实现对象启动 + this.defaultMQProducerImpl.start(); + if (null != traceDispatcher) { + // 消息轨迹的逻辑 + traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel()); + } + } + ``` + +* send():**发送消息**: + + ```java + public SendResult send(Message msg){ + // 校验消息 + Validators.checkMessage(msg, this); + // 设置消息 Topic + msg.setTopic(withNamespace(msg.getTopic())); + return this.defaultMQProducerImpl.send(msg); + } + ``` + +* request():请求方法,**需要消费者回执消息**,又叫回退消息 + + ```java + public Message request(final Message msg, final MessageQueue mq, final long timeout) { + msg.setTopic(withNamespace(msg.getTopic())); + return this.defaultMQProducerImpl.request(msg, mq, timeout); + } + ``` + + + + +*** + + + +#### 实现者类 + +##### 成员属性 + +DefaultMQProducerImpl 类是默认的生产者实现类 + +成员变量: + +* 实例对象: + + ```java + private final DefaultMQProducer defaultMQProducer; // 持有默认生产者对象,用来获取对象中的配置信息 + private MQClientInstance mQClientFactory; // 客户端实例对象,生产者启动后需要注册到该客户端对象内 + ``` + +* 主题发布信息映射表:key 是 Topic,value 是发布信息 + + ```java + private final ConcurrentMap topicPublishInfoTable = new ConcurrentHashMap(); + ``` + + ```java + public class TopicPublishInfo { + private boolean orderTopic = false; + private boolean haveTopicRouterInfo = false; + // 主题的全部队列 + private List messageQueueList = new ArrayList(); + // 使用 ThreaLocal 保存发送消息使用的队列 + private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex(); + // 主题的路由数据 + private TopicRouteData topicRouteData; + } + ``` + +* 异步发送消息:相关信息 + + ```java + private final BlockingQueue asyncSenderThreadPoolQueue;// 异步发送消息,异步线程池使用的队列 + private final ExecutorService defaultAsyncSenderExecutor; // 异步发送消息默认使用的线程池 + private ExecutorService asyncSenderExecutor; // 异步消息发送线程池,指定后就不使用默认线程池了 + ``` + +* 定时器:执行定时任务 + + ```java + private final Timer timer = new Timer("RequestHouseKeepingService", true); // 守护线程 + ``` + +* 状态信息:服务的状态,默认创建状态 + + ```java + private ServiceState serviceState = ServiceState.CREATE_JUST; + ``` + +* 压缩等级:ZIP 压缩算法的等级,默认是 5,越高压缩效果好,但是压缩的更慢 + + ```java + private int zipCompressLevel = Integer.parseInt(System.getProperty..., "5")); + ``` + +* 容错策略:选择队列的容错策略 + + ```java + private MQFaultStrategy mqFaultStrategy = new MQFaultStrategy(); + ``` + +* 钩子:用来进行前置或者后置处理 + + ```java + ArrayList sendMessageHookList; // 发送消息的钩子,留给用户扩展使用 + ArrayList checkForbiddenHookList; // 对比上面的钩子,可以抛异常,控制消息是否可以发送 + private final RPCHook rpcHook; // 传递给 NettyRemotingClient + ``` + +构造方法: + +* 默认构造: + + ```java + public DefaultMQProducerImpl(final DefaultMQProducer defaultMQProducer) { + // 默认 RPC HOOK 是空 + this(defaultMQProducer, null); + } + ``` + +* 有参构造: + + ```java + public DefaultMQProducerImpl(final DefaultMQProducer defaultMQProducer, RPCHook rpcHook) { + // 属性赋值 + this.defaultMQProducer = defaultMQProducer; + this.rpcHook = rpcHook; + + // 创建【异步消息线程池任务队列】,长度是 5w + this.asyncSenderThreadPoolQueue = new LinkedBlockingQueue(50000); + // 创建默认的异步消息任务线程池 + this.defaultAsyncSenderExecutor = new ThreadPoolExecutor( + // 核心线程数和最大线程数都是 系统可用的计算资源(8核16线程的系统就是 16)... + } + ``` + + + +**** + + + +##### 成员方法 + +* start():启动方法,参数默认是 true,代表正常的启动路径 + + * `this.serviceState = ServiceState.START_FAILED`:先修改为启动失败,成功后再修改,这种思想很常见 + + * `this.checkConfig()`:判断生产者组名不能是空,也不能是 default_PRODUCER + + * `if (!getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP))`:条件成立说明当前生产者不是内部产生者,内部生产者是**处理消息回退**的这种情况使用的生产者 + + `this.defaultMQProducer.changeInstanceNameToPID()`:正常的生产者,修改生产者实例名称为当前进程的 PID + + * ` this.mQClientFactory = ...`:获取当前进程的 MQ 客户端实例对象,从 factoryTable 中获取 key 为 客户端 ID,格式是`ip@pid`,**一个 JVM 进程只有一个 PID,也只有一个 MQClientInstance** + + * `boolean registerOK = mQClientFactory.registerProducer(...)`:将生产者注册到 RocketMQ 客户端实例内 + + * `this.topicPublishInfoTable.put(...)`:添加一个主题发布信息,key 是 **TBW102** ,value 是一个空对象 + + * `if (startFactory) `:正常启动路径 + + `mQClientFactory.start()`:启动 RocketMQ 客户端实例对象 + + * `this.serviceState = ServiceState.RUNNING`:修改生产者实例的状态 + + * `this.mQClientFactory.sendHeartbeatToAllBrokerWithLock()`:RocketMQ 客户端实例向已知的 Broker 节点发送一次心跳(也是定时任务) + * `this.timer.scheduleAtFixedRate()`: request 发送的回执信息,启动定时任务每秒一次删除超时请求 + + * 生产者 msg 添加信息关联 ID 发送到 Broker + * 消费者从 Broker 拿到消息后会检查 msg 类型是一个需要回执的消息,处理完消息后会根据 msg 关联 ID 和客户端 ID 生成一条响应结果消息发送到 Broker,Broker 判断为回执消息,会根据客户端ID 找到 channel 推送给生产者 + * 生产者拿到回执消息后,读取出来关联 ID 找到对应的 RequestFuture,将阻塞线程唤醒 + +* sendDefaultImpl():发送消息 + + ```java + //参数1:消息;参数2:发送模式(同步异步单向);参数3:回调函数,异步发送时需要;参数4:发送超时时间, 默认 3 秒 + private SendResult sendDefaultImpl(msg, communicationMode, sendCallback,timeout) {} + ``` + + * `this.makeSureStateOK()`:校验生产者状态是运行中,否则抛出异常 + + * `Validators.checkMessage(msg, this.defaultMQProducer)`:校验消息规格 + + * `long beginTimestampPrev, endTimestamp`:本轮发送的开始时间和本轮的结束时间 + + * `topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic())`:**获取当前消息主题的发布信息** + + * `this.topicPublishInfoTable.get(topic)`:尝试从本地主题发布信息映射表获取信息,不空直接返回 + + * `if (null == topicPublishInfo || !topicPublishInfo.ok())`:本地没有需要去 MQ 客户端获取 + + `this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo())`:保存一份空数据 + + `this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic)`:从 Namesrv 更新该 Topic 的路由数据 + + `topicPublishInfo = this.topicPublishInfoTable.get(topic)`:重新从本地获取发布信息 + + * `this.mQClientFactory.updateTopicRouteInfoFromNameServer(..)`:**路由数据是空,获取默认 TBW102 的数据** + + * `return topicPublishInfo`:返回 TBW102 主题的发布信息 + + * `int timesTotal, times `:发送的总尝试次数和当前是第几次发送 + * `String[] brokersSent = new String[timesTotal]`:下标索引代表第几次发送,值代表这次发送选择 Broker name + * `for (; times < timesTotal; times++)`:循环发送,发送成功或者发送尝试次数达到上限,结束循环 + * `String lastBrokerName = null == mq ? null : mq.getBrokerName()`:获取上次发送失败的 BrokerName + + * `mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName)`:**从发布信息中选择一个队列** + + * `if (this.sendLatencyFaultEnable)`:默认不开启,可以通过配置开启 + * `return tpInfo.selectOneMessageQueue(lastBrokerName)`:默认选择队列的方式,就是循环主题全部的队列 + * `int index = this.sendWhichQueue.getAndIncrement()`:选择队列的索引 + * `int pos = Math.abs(index) % this.messageQueueList.size()`:获取该索引对应的队列位置 + * `return this.messageQueueList.get(pos)`:返回消息队列 + + * `brokersSent[times] = mq.getBrokerName()`:将本次选择的 BrokerName 存入数组 + * `msg.setTopic(this.defaultMQProducer.withNamespace(msg.getTopic()))`:**重投的消息需要加上标记** + * `sendResult = this.sendKernelImpl`:核心发送方法 + * `this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false)`:更新一下时间 + + * `switch (communicationMode)`:异步或者单向消息直接返回 null,同步发送进入逻辑判断 + + `if (sendResult.getSendStatus() != SendStatus.SEND_OK)`:**服务端 Broker 存储失败**,需要重试其他 Broker + + * `throw new MQClientException()`:未找到当前主题的路由数据,无法发送消息,抛出异常 + +* sendKernelImpl():**核心发送方法** + + ```java + //参数1:消息;参数2:选择的队列;参数3:发送模式(同步异步单向);参数4:回调函数,异步发送时需要;参数5:主题发布信息;参数6:剩余超时时间限制 + private SendResult sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout) + ``` + + * `brokerAddr = this.mQClientFactory(...)`:获取指定 BrokerName 对应的 mater 节点的地址,master 节点的 ID 为 0 + + * `brokerAddr = MixAll.brokerVIPChannel()`:Broker 启动时会绑定两个服务器端口,一个是普通端口,一个是 VIP 端口,服务器端根据不同端口创建不同的的 NioSocketChannel + + * `byte[] prevBody = msg.getBody()`:获取消息体 + + * `if (!(msg instanceof MessageBatch))`:非批量消息,需要重新设置消息 ID + + `MessageClientIDSetter.setUniqID(msg)`:msg id 由两部分组成,一部分是 ip 地址、进程号、ClassLoader 的 hashcode,另一部分是时间差(当前时间减去当月一号的时间)和计数器的值 + + * `if (this.tryToCompressMessage(msg))`:判断消息是否压缩,压缩需要设置压缩标记 + + * `hasCheckForbiddenHook、hasSendMessageHook`:执行钩子方法 + + * `requestHeader = new SendMessageRequestHeader()`:设置发送消息的消息头 + + * `if (requestHeader.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX))`:重投的发送消息 + + * `switch (communicationMode)`:异步发送一种处理方式,单向和同步同样的处理逻辑 + + `sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage()`:**发送消息** + + * `request = RemotingCommand.createRequestCommand()`:创建一个 RequestCommand 对象 + * `request.setBody(msg.getBody())`:**将消息放入请求体** + * `switch (communicationMode)`:根据不同的模式 invoke 不同的方法 + +* request():请求方法,消费者回执消息,这种消息是异步消息 + + * `requestResponseFuture = new RequestResponseFuture(correlationId, timeout, null)`:创建请求响应对象 + + * `getRequestFutureTable().put(correlationId, requestResponseFuture)`:放入RequestFutureTable 映射表中 + + * `this.sendDefaultImpl(msg, CommunicationMode.ASYNC, new SendCallback())`:**发送异步消息** + + * `return waitResponse(msg, timeout, requestResponseFuture, cost)`:用来挂起请求的方法 + + ```java + public Message waitResponseMessage(final long timeout) throws InterruptedException { + // 请求挂起 + this.countDownLatch.await(timeout, TimeUnit.MILLISECONDS); + return this.responseMsg; + } + + * 当消息被消费后,会获取消息的关联 ID,从映射表中获取消息的 RequestResponseFuture,执行下面的方法唤醒挂起线程 + + ```java + public void putResponseMessage(final Message responseMsg) { + this.responseMsg = responseMsg; + this.countDownLatch.countDown(); + } + ``` + + + +**** + + + +### 客户端 + +#### 实例对象 + +##### 成员属性 + +MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一个客户端实例,**既服务于生产者,也服务于消费者** + +成员变量: + +* 配置信息: + + ```java + private final int instanceIndex; // 索引一般是 0,因为客户端实例一般都是一个进程只有一个 + private final String clientId; // 客户端 ID ip@pid + private final long bootTimestamp; // 客户端的启动时间 + private ServiceState serviceState; // 客户端状态 + ``` + +* 生产者消费者的映射表:key 是组名 + + ```java + private final ConcurrentMap producerTable + private final ConcurrentMap consumerTable + private final ConcurrentMap adminExtTable + ``` + +* 网络层配置: + + ```java + private final NettyClientConfig nettyClientConfig; + ``` + +* 核心功能的实现:负责将 MQ 业务层的数据转换为网络层的 RemotingCommand 对象,使用内部持有的 NettyRemotingClient 对象的 invoke 系列方法,完成网络 IO(同步、异步、单向) + + ```java + private final MQClientAPIImpl mQClientAPIImpl; + ``` + +* 本地路由数据:key 是主题名称,value 路由信息 + + ```java + private final ConcurrentMap topicRouteTable = new ConcurrentHashMap<>(); + +* 锁信息:两把锁,锁不同的数据 + + ```java + private final Lock lockNamesrv = new ReentrantLock(); + private final Lock lockHeartbeat = new ReentrantLock(); + ``` + +* 调度线程池:单线程,执行定时任务 + + ```java + private final ScheduledExecutorService scheduledExecutorService; + ``` + +* Broker 映射表:key 是 BrokerName + + ```java + // 物理节点映射表,value:Long 是 brokerID,【ID=0 的是主节点,其他是从节点】,String 是地址 ip:port + private final ConcurrentMap> brokerAddrTable; + // 物理节点版本映射表,String 是地址 ip:port,Integer 是版本 + ConcurrentMap> brokerVersionTable; + ``` + +* **客户端的协议处理器**:用于处理 IO 事件 + + ```java + private final ClientRemotingProcessor clientRemotingProcessor; + ``` + +* 消息服务: + + ```java + private final PullMessageService pullMessageService; // 拉消息服务 + private final RebalanceService rebalanceService; // 消费者负载均衡服务 + private final ConsumerStatsManager consumerStatsManager; // 消费者状态管理 + ``` + +* 内部生产者实例:处理消费端**消息回退**,用该生产者发送回执消息 + + ```java + private final DefaultMQProducer defaultMQProducer; + ``` + +* 心跳次数统计: + + ```java + private final AtomicLong sendHeartbeatTimesTotal = new AtomicLong(0) + ``` + +* 公共配置类: + + ```java + public class ClientConfig { + // Namesrv 地址配置 + private String namesrvAddr = NameServerAddressUtils.getNameServerAddresses(); + // 客户端的 IP 地址 + private String clientIP = RemotingUtil.getLocalAddress(); + // 客户端实例名称 + private String instanceName = System.getProperty("rocketmq.client.name", "DEFAULT"); + // 客户端回调线程池的数量,平台核心数,8核16线程的电脑返回16 + private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); + // 命名空间 + protected String namespace; + protected AccessChannel accessChannel = AccessChannel.LOCAL; + + // 获取路由信息的间隔时间 30s + private int pollNameServerInterval = 1000 * 30; + // 客户端与 broker 之间的心跳周期 30s + private int heartbeatBrokerInterval = 1000 * 30; + // 消费者持久化消费的周期 5s + private int persistConsumerOffsetInterval = 1000 * 5; + private long pullTimeDelayMillsWhenException = 1000; + private boolean unitMode = false; + private String unitName; + // vip 通道,broker 启动时绑定两个端口,其中一个是 vip 通道 + private boolean vipChannelEnabled = Boolean.parseBoolean(); + // 语言,默认是 Java + private LanguageCode language = LanguageCode.JAVA; + } + ``` + +构造方法: + +* MQClientInstance 有参构造: + + ```java + public MQClientInstance(ClientConfig clientConfig, int instanceIndex, String clientId, RPCHook rpcHook) { + this.clientConfig = clientConfig; + this.instanceIndex = instanceIndex; + // Netty 相关的配置信息 + this.nettyClientConfig = new NettyClientConfig(); + // 平台核心数 + this.nettyClientConfig.setClientCallbackExecutorThreads(...); + this.nettyClientConfig.setUseTLS(clientConfig.isUseTLS()); + // 【创建客户端协议处理器】 + this.clientRemotingProcessor = new ClientRemotingProcessor(this); + // 创建 API 实现对象 + // 参数一:客户端网络配置 + // 参数二:客户端协议处理器,注册到客户端网络层 + // 参数三:rpcHook,注册到客户端网络层 + // 参数四:客户端配置 + this.mQClientAPIImpl = new MQClientAPIImpl(this.nettyClientConfig, this.clientRemotingProcessor, rpcHook, clientConfig); + + //... + // 内部生产者,指定内部生产者的组 + this.defaultMQProducer = new DefaultMQProducer(MixAll.CLIENT_INNER_PRODUCER_GROUP); + } + ``` + +* MQClientAPIImpl 有参构造: + + ```java + public MQClientAPIImpl(nettyClientConfig, clientRemotingProcessor, rpcHook, clientConfig) { + this.clientConfig = clientConfig; + topAddressing = new TopAddressing(MixAll.getWSAddr(), clientConfig.getUnitName()); + // 创建网络层对象,参数二为 null 说明客户端并不关心 channel event + this.remotingClient = new NettyRemotingClient(nettyClientConfig, null); + // 业务处理器 + this.clientRemotingProcessor = clientRemotingProcessor; + // 注册 RpcHook + this.remotingClient.registerRPCHook(rpcHook); + // ... + // 注册回退消息的请求码 + this.remotingClient.registerProcessor(RequestCode.PUSH_REPLY_MESSAGE_TO_CLIENT, this.clientRemotingProcessor, null); + } + ``` + + + +*** + + + +##### 成员方法 + +* start():启动方法 + + * `synchronized (this)`:加锁保证线程安全,保证只有一个实例对象启动 + * `this.mQClientAPIImpl.start()`:启动客户端网络层,底层调用 RemotingClient 类 + * `this.startScheduledTask()`:启动定时任务 + * `this.pullMessageService.start()`:启动拉取消息服务 + * `this.rebalanceService.start()`:启动负载均衡服务 + * `this.defaultMQProducer.getDefaultMQProducerImpl().start(false)`:启动内部生产者,参数为 false 代表不启动实例 + +* startScheduledTask():**启动定时任务**,调度线程池是单线程 + + * `if (null == this.clientConfig.getNamesrvAddr())`:Namesrv 地址是空,需要两分钟拉取一次 Namesrv 地址 + + * 定时任务 1:从 Namesrv 更新客户端本地的路由数据,周期 30 秒一次 + + ```java + // 获取生产者和消费者订阅的主题集合,遍历集合,对比从 namesrv 拉取最新的主题路由数据和本地数据,是否需要更新 + MQClientInstance.this.updateTopicRouteInfoFromNameServer(); + ``` + + * 定时任务 2:周期 30 秒一次,两个任务 + + * 清理下线的 Broker 节点,遍历客户端的 Broker 物理节点映射表,将所有主题数据都不包含的 Broker 物理节点清理掉,如果被清理的 Broker 下所有的物理节点都没有了,就将该 Broker 的映射数据删除掉 + * 向在线的所有的 Broker 发送心跳数据,**同步发送的方式**,返回值是 Broker 物理节点的版本号,更新版本映射表 + + ```java + MQClientInstance.this.cleanOfflineBroker(); + MQClientInstance.this.sendHeartbeatToAllBrokerWithLock(); + ``` + + ```java + // 心跳数据 + public class HeartbeatData extends RemotingSerializable { + // 客户端 ID ip@pid + private String clientID; + // 存储客户端所有生产者数据 + private Set producerDataSet = new HashSet(); + // 存储客户端所有消费者数据 + private Set consumerDataSet = new HashSet(); + } + ``` + + * 定时任务 3:消费者持久化消费数据,周期 5 秒一次 + + ```java + MQClientInstance.this.persistAllConsumerOffset(); + ``` + + * 定时任务 4:动态调整消费者线程池,周期 1 分钟一次 + + ```java + MQClientInstance.this.adjustThreadPool(); + ``` + +* updateTopicRouteInfoFromNameServer():**更新路由数据** + + * `if (isDefault && defaultMQProducer != null)`:需要默认数据 + + `topicRouteData = ...getDefaultTopicRouteInfoFromNameServer()`:从 Namesrv 获取默认的 TBW102 的路由数据 + + `int queueNums`:遍历所有队列,为每个读写队列设置较小的队列数 + + * `topicRouteData = ...getTopicRouteInfoFromNameServer(topic)`:需要**从 Namesrv 获取**路由数据(同步) + + * `old = this.topicRouteTable.get(topic)`:获取客户端实例本地的该主题的路由数据 + + * `boolean changed = topicRouteDataIsChange(old, topicRouteData)`:对比本地和最新下拉的数据是否一致 + + * `if (changed)`:不一致进入更新逻辑 + + `cloneTopicRouteData = topicRouteData.cloneTopicRouteData()`:克隆一份最新数据 + + `Update Pub info`:更新生产者信息 + + * `publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData)`:**将主题路由数据转化为发布数据** + * `impl.updateTopicPublishInfo(topic, publishInfo)`:生产者将主题的发布数据保存到它本地,方便发送消息使用 + + `Update sub info`:更新消费者信息 + + `this.topicRouteTable.put(topic, cloneTopicRouteData)`:将数据放入本地路由表 + + + +**** + + + +#### 网络通信 + +##### 成员属性 + +NettyRemotingClient 类负责客户端的网络通信 + +成员变量: + +* Netty 服务相关属性: + + ```java + private final NettyClientConfig nettyClientConfig; // 客户端的网络层配置 + private final Bootstrap bootstrap = new Bootstrap(); // 客户端网络层启动对象 + private final EventLoopGroup eventLoopGroupWorker; // 客户端网络层 Netty IO 线程组 + ``` + +* Channel 映射表: + + ```java + private final ConcurrentMap channelTables;// key 是服务器的地址,value 是通道对象 + private final Lock lockChannelTables = new ReentrantLock(); // 锁,控制并发安全 + ``` + +* 定时器:启动定时任务 + + ```java + private final Timer timer = new Timer("ClientHouseKeepingService", true) + ``` + +* 线程池: + + ```java + private ExecutorService publicExecutor; // 公共线程池 + private ExecutorService callbackExecutor; // 回调线程池,客户端发起异步请求,服务器的响应数据由回调线程池处理 + ``` + +* 事件监听器:客户端这里是 null + + ```java + private final ChannelEventListener channelEventListener; + ``` + +* Netty 配置对象: + + ```java + public class NettyClientConfig { + // 客户端工作线程数 + private int clientWorkerThreads = 4; + // 回调处理线程池 线程数:平台核心数 + private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); + // 单向请求并发数,默认 65535 + private int clientOnewaySemaphoreValue = NettySystemConfig.CLIENT_ONEWAY_SEMAPHORE_VALUE; + // 异步请求并发数,默认 65535 + private int clientAsyncSemaphoreValue = NettySystemConfig.CLIENT_ASYNC_SEMAPHORE_VALUE; + // 客户端连接服务器的超时时间限制 3秒 + private int connectTimeoutMillis = 3000; + // 客户端未激活周期,60s(指定时间内 ch 未激活,需要关闭) + private long channelNotActiveInterval = 1000 * 60; + // 客户端与服务器 ch 最大空闲时间 2分钟 + private int clientChannelMaxIdleTimeSeconds = 120; + + // 底层 Socket 写和收 缓冲区的大小 65535 64k + private int clientSocketSndBufSize = NettySystemConfig.socketSndbufSize; + private int clientSocketRcvBufSize = NettySystemConfig.socketRcvbufSize; + // 客户端 netty 是否启动内存池 + private boolean clientPooledByteBufAllocatorEnable = false; + // 客户端是否超时关闭 Socket 连接 + private boolean clientCloseSocketIfTimeout = false; + } + ``` + +构造方法 + +* 无参构造: + + ```java + public NettyRemotingClient(final NettyClientConfig nettyClientConfig) { + this(nettyClientConfig, null); + } + ``` + +* 有参构造: + + ```java + public NettyRemotingClient(nettyClientConfig, channelEventListener) { + // 父类创建了2个信号量,1、控制单向请求的并发度,2、控制异步请求的并发度 + super(nettyClientConfig.getClientOnewaySemaphoreValue(), nettyClientConfig.getClientAsyncSemaphoreValue()); + this.nettyClientConfig = nettyClientConfig; + this.channelEventListener = channelEventListener; + + // 创建公共线程池 + int publicThreadNums = nettyClientConfig.getClientCallbackExecutorThreads(); + if (publicThreadNums <= 0) { + publicThreadNums = 4; + } + this.publicExecutor = Executors.newFixedThreadPool(publicThreadNums,); + + // 创建 Netty IO 线程,1个线程 + this.eventLoopGroupWorker = new NioEventLoopGroup(1, ); + + if (nettyClientConfig.isUseTLS()) { + sslContext = TlsHelper.buildSslContext(true); + } + } + ``` + + + +**** + + + +##### 成员方法 + +* start():启动方法 + + ```java + public void start() { + // channel pipeline 内的 handler 使用的线程资源,默认 4 个 + this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(); + // 配置 netty 客户端启动类对象 + Bootstrap handler = this.bootstrap.group(this.eventLoopGroupWorker).channel(NioSocketChannel.class) + //... + .handler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + // 加几个handler + pipeline.addLast( + // 服务端的数据,都会来到这个 + new NettyClientHandler()); + } + }); + // 注意 Bootstrap 只是配置好客户端的元数据了,【在这里并没有创建任何 channel 对象】 + // 定时任务 扫描 responseTable 中超时的 ResponseFuture,避免客户端线程长时间阻塞 + this.timer.scheduleAtFixedRate(() -> { + NettyRemotingClient.this.scanResponseTable(); + }, 1000 * 3, 1000); + // 这里是 null,不启动 + if (this.channelEventListener != null) { + this.nettyEventExecutor.start(); + } + } + ``` + +* 单向通信: + + ```java + public RemotingCommand invokeSync(String addr, final RemotingCommand request, long timeoutMillis) { + // 开始时间 + long beginStartTime = System.currentTimeMillis(); + // 获取或者创建客户端与服务端(addr)的通道 channel + final Channel channel = this.getAndCreateChannel(addr); + // 条件成立说明客户端与服务端 channel 通道正常,可以通信 + if (channel != null && channel.isActive()) { + try { + // 执行 rpcHook 拓展点 + doBeforeRpcHooks(addr, request); + // 计算耗时,如果当前耗时已经超过 timeoutMillis 限制,则直接抛出异常,不再进行系统通信 + long costTime = System.currentTimeMillis() - beginStartTime; + if (timeoutMillis < costTime) { + throw new RemotingTimeoutException("invokeSync call timeout"); + } + // 参数1:客户端-服务端通道channel + // 参数二:网络层传输对象,封装着请求数据 + // 参数三:剩余的超时限制 + RemotingCommand response = this.invokeSyncImpl(channel, request, ...); + // 后置处理 + doAfterRpcHooks(RemotingHelper.parseChannelRemoteAddr(channel), request, response); + // 返回响应数据 + return response; + } catch (RemotingSendRequestException e) {} + } else { + this.closeChannel(addr, channel); + throw new RemotingConnectException(addr); + } + } + ``` + + diff --git a/Java.md b/Java.md index d1f0b60..edc5ccd 100644 --- a/Java.md +++ b/Java.md @@ -4436,7 +4436,7 @@ TreeSet 集合自排序的方式: 方法:`public int compare(Employee o1, Employee o2): o1 比较者, o2 被比较者` - * 比较者大于被比较者,返回正数 + * 比较者大于被比较者,返回正数(升序) * 比较者小于被比较者,返回负数 * 比较者等于被比较者,返回 0 diff --git a/Web.md b/Web.md index 7c70150..5262ce7 100644 --- a/Web.md +++ b/Web.md @@ -2164,7 +2164,7 @@ HTTP 和 HTTPS 的区别: * 优点:运算速度快 * 缺点:无法安全的将密钥传输给通信方 -* 非对称加密:加密和解密使用不同的秘钥,一把作为公开的公钥,另一把作为私钥,公钥公开给任何人(类似于把锁和箱子给别人,对方打开箱子放入数据,上锁后发送),典型的非对称加密算法有RSA、DSA等 +* 非对称加密:加密和解密使用不同的秘钥,一把作为公开的公钥,另一把作为私钥,**公钥公开给任何人**(类似于把锁和箱子给别人,对方打开箱子放入数据,上锁后发送),典型的非对称加密算法有 RSA、DSA 等 * 优点:可以更安全地将公开密钥传输给通信发送方 * 缺点:运算速度慢 @@ -2174,28 +2174,28 @@ HTTP 和 HTTPS 的区别: * 获取到 Secret Key 后,再使用对称密钥加密方式进行通信,从而保证效率 思想:锁上加锁 + +* 数字签名:附加在报文上的特殊加密校验码,可以防止报文被篡改,一般是通过哈希算法 + +* 数字证书:由权威机构给某网站颁发的一种认可凭证 HTTPS 工作流程:服务器端的公钥和私钥,用来进行非对称加密,客户端生成的随机密钥,用来进行对称加密 ![](https://gitee.com/seazean/images/raw/master/Web/HTTP-HTTPS加密过程.png) -1. 客户端向服务器发起 HTTPS 请求,连接到服务器的 443 端口 - -2. 服务器端有一个密钥对,即公钥和私钥,用来进行非对称加密,服务器端保存着私钥不能泄露,公钥可以发给任何客户端 - -3. 服务器将公钥发送给客户端 - -4. 客户端收到服务器端的数字证书之后,会对证书进行检查,验证其合法性,如果发现发现证书有问题,那么 HTTPS 传输就无法继续。如果公钥合格,那么客户端会生成一个随机值,这个随机值就是用于进行对称加密的密钥,将该密钥称之为 client key,即客户端密钥。然后用服务器的公钥对客户端密钥进行非对称加密,这样客户端密钥就变成密文了,HTTPS 中的第一次 HTTP 请求结束 - +1. 客户端向服务器发起 HTTPS 请求,连接到服务器的 443 端口,请求携带了浏览器支持的加密算法和哈希算法 +2. 服务器端会向数字证书认证机构提出公开密钥的申请,认证机构对公开密钥做数字签名后进行分配,会将公钥绑定在数字证书(又叫公钥证书,内容有公钥,网站地址,证书颁发机构,失效日期等) +3. 服务器将数字证书发送给客户端,私钥由服务器持有 +4. 客户端收到服务器端的数字证书后对证书进行检查,验证其合法性,如果发现发现证书有问题,那么 HTTPS 传输就无法继续。如果公钥合格,那么客户端会生成一个随机值,**这个随机值就是用于进行对称加密的密钥**,将该密钥称之为 client key(客户端密钥、会话密钥)。用服务器的公钥对客户端密钥进行非对称加密,这样客户端密钥就变成密文,HTTPS 中的第一次 HTTP 请求结束 5. 客户端会发起 HTTPS 中的第二个 HTTP 请求,将加密之后的客户端密钥发送给服务器 - 6. 服务器接收到客户端发来的密文之后,会用自己的私钥对其进行非对称解密,解密之后的明文就是客户端密钥,然后用客户端密钥对数据进行对称加密,这样数据就变成了密文 - 7. 服务器将加密后的密文发送给客户端 - 8. 客户端收到服务器发送来的密文,用客户端密钥对其进行对称解密,得到服务器发送的数据,这样 HTTPS 中的第二个 HTTP 请求结束,整个 HTTPS 传输完成 + +参考文章:https://www.cnblogs.com/linianhui/p/security-https-workflow.html + 参考文章:https://www.jianshu.com/p/14cd2c9d2cd2 From be312e31a807598dc13bd0f33e98ddd676e0e6e4 Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 3 Jan 2022 15:04:59 +0800 Subject: [PATCH 100/168] Update README --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 5b83132..4ac1828 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ 内容说明: * DB:MySQL、Redis -* Frame:Maven、Netty +* Frame:Maven、Netty、RocketMQ * Java:JavaSE、JVM、Algorithm、Design Pattern * Prog:Concurrent、Network Programming * SSM:MyBatis、Spring、SpringMVC、SpringBoot @@ -18,4 +18,3 @@ * 所有的知识不保证权威性,如果各位朋友发现错误,欢迎与我讨论。 * 笔记的编写基于 Windows 平台,可能会因为平台的不同而造成空格、制表符的显示效果不同。 - From 4debfde6642506a8e4b4f2e88189efa8038f7745 Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 6 Jan 2022 00:48:25 +0800 Subject: [PATCH 101/168] Update Java Notes --- DB.md | 30 ++- Frame.md | 651 +++++++++++++++++++++++++++++++++++++++++++------------ Java.md | 2 +- Prog.md | 16 +- 4 files changed, 544 insertions(+), 155 deletions(-) diff --git a/DB.md b/DB.md index 2564c46..c815155 100644 --- a/DB.md +++ b/DB.md @@ -5320,7 +5320,7 @@ Buffer Pool 中每个缓冲页都有对应的控制信息,包括表空间编 MySQL 提供了缓冲页的快速查找方式:**哈希表**,使用表空间号和页号作为 Key,缓冲页控制块的地址作为 Value 创建一个哈希表,获取数据页时根据 Key 进行哈希寻址: * 如果不存在对应的缓存页,就从 free 链表中选一个空闲缓冲页,把磁盘中的对应页加载到该位置 -* 如果存在对应的缓存页,直接获取使用 +* 如果存在对应的缓存页,直接获取使用,提高查询数据的效率 @@ -5419,6 +5419,8 @@ Innodb 用一块内存区做 IO 缓存池,该缓存池不仅用来缓存 Innod SHOW ENGINE INNODB STATUS\G ``` +`Buffer pool hit rate` 字段代表**内存命中率**,表示 Buffer Pool 对查询的加速效果 + 核心参数: * `innodb_buffer_pool_size`:该变量决定了 Innodb 存储引擎表数据和索引数据的最大缓存区大小,默认 128M @@ -5441,8 +5443,6 @@ SHOW ENGINE INNODB STATUS\G innodb_log_buffer_size=10M ``` -Buffer Pool 中有一块内存叫 Change Buffer 用来对增删改操作提供缓存,可以通过参数 innodb_change_buffer_max_size 来动态设置,设置为 50 时表示 Change Buffer 的大小最多只能占用 Buffer Pool 的 50% - 在多线程下,访问 Buffer Pool 中的各种链表都需要加锁,所以将 Buffer Pool 拆成若干个小实例,每个实例独立管理内存空间和各种链表(类似 ThreadLocal),多线程访问各实例互不影响,提高了并发能力 * 在系统启动时设置系统变量 `innodb_buffer_pool_instance` 可以指定 Buffer Pool 实例的个数,但是当 Buffer Pool 小于 1GB 时,设置多个实例时无效的 @@ -5456,6 +5456,30 @@ MySQL 5.7.5 之前 `innodb_buffer_pool_size` 只支持在系统启动时修改 +*** + + + +#### 其他内存 + +InnoDB 管理的 Buffer Pool 中有一块内存叫 Change Buffer 用来对**增删改**操作提供缓存,参数 `innodb_change_buffer_max_size ` 来动态设置,设置为 50 时表示 Change Buffer 的大小最多只能占用 Buffer Pool 的 50% + +Server 层针对优化**查询**的内存为 Net Buffer,内存的大小是由参数 `net_buffer_length`定义,默认 16k,实现流程: + +* 获取一行数据写入 Net Buffer,重复获取直到 Net Buffer 写满,调用网络接口发出去 +* 若发送成功就清空 Net Buffer,然后继续取下一行;若发送函数返回 `EAGAIN` 或 `WSAEWOULDBLOCK`,表示本地网络栈 `socket send buffer` 写满了,进入等待,直到网络栈重新可写再继续发送 + +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-查询内存优化.png) + +MySQL 采用的是边算边发的逻辑,因此对于数据量很大的查询来说,不会在 Server 端保存完整的结果集,如果客户端读结果不及时,会堵住 MySQL 的查询过程,但是不会把内存打爆导致 OOM + + + + +参考文章:https://blog.csdn.net/qq_33589510/article/details/117673449 + + + *** diff --git a/Frame.md b/Frame.md index b5c4d88..b809388 100644 --- a/Frame.md +++ b/Frame.md @@ -3816,7 +3816,7 @@ public class Consumer { - 全局顺序:对于指定的一个 Topic,所有消息按照严格的先入先出(FIFO)的顺序进行发布和消费。 适用于性能要求不高,所有的消息严格按照 FIFO 原则进行消息发布和消费的场景 - 分区顺序:对于指定的一个 Topic,所有消息根据 sharding key 进行分区,同一个分组内的消息按照严格的 FIFO 顺序进行发布和消费。Sharding key 是顺序消息中用来区分不同分区的关键字段,和普通消息的 Key 是完全不同的概念。 适用于性能要求高,以 sharding key 作为分区字段,在同一个区中严格的按照 FIFO 原则进行消息发布和消费的场景 -在默认的情况下消息发送会采取 Round Robin 轮询方式把消息发送到不同的 queue(分区队列),而消费消息是从多个 queue 上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个 queue 中,消费的时候只从这个 queue 上依次拉取,则就保证了顺序。当发送和消费参与的 queue 只有一个,则是全局有序;如果多个queue参与,则为分区有序,即相对每个 queue,消息都是有序的 +在默认的情况下消息发送会采取 Round Robin 轮询方式把消息发送到不同的 queue(分区队列),而消费消息是从多个 queue 上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个 queue 中,消费的时候只从这个 queue 上依次拉取,则就保证了顺序。当发送和消费参与的 queue 只有一个,则是全局有序;如果多个queue 参与,则为分区有序,即相对每个 queue,消息都是有序的 @@ -4552,8 +4552,6 @@ RocketMQ 的工作流程: -### 消息存储 - #### 生产消费 At least Once:至少一次,指每个消息必须投递一次,Consumer 先 Pull 消息到本地,消费完成后才向服务器返回 ACK,如果没有消费一定不会 ACK 消息 @@ -4575,101 +4573,6 @@ At least Once:至少一次,指每个消息必须投递一次,Consumer 先 -*** - - - -#### 存储结构 - -RocketMQ 中 Broker 负责存储消息转发消息,所以以下的结构是存储在 Broker Server 上的,生产者和消费者与 Broker 进行消息的收发是通过主题对应的 Message Queue 完成,类似于通道 - -RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成 的,消息真正的物理存储文件是 CommitLog,ConsumeQueue 是消息的逻辑队列,类似数据库的索引节点,存储的是指向物理存储的地址。**每个 Topic 下的每个 Message Queue 都有一个对应的 ConsumeQueue 文件** - -每条消息都会有对应的索引信息,Consumer 通过 ConsumeQueue 这个结构来读取消息实体内容 - -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息存储结构.png) - -* CommitLog:消息主体以及元数据的存储主体,存储 Producer 端写入的消息内容,消息内容不是定长的。消息主要是顺序写入日志文件,单个文件大小默认1G,偏移量代表下一次写入的位置,当文件写满了就继续写入下一个文件 -* ConsumerQueue:消息消费队列,存储消息在 CommitLog 的索引。RocketMQ 消息消费时要遍历 CommitLog 文件,并根据主题 Topic 检索消息,这是非常低效的。引入 ConsumeQueue 作为消费消息的索引,保存了指定 Topic 下的队列消息在 CommitLog 中的起始物理偏移量 offset,消息大小 size 和消息 Tag 的 HashCode 值,每个 ConsumeQueue 文件大小约 5.72M -* IndexFile:为了消息查询提供了一种通过 Key 或时间区间来查询消息的方法,通过 IndexFile 来查找消息的方法不影响发送与消费消息的主流程。IndexFile 的底层存储为在文件系统中实现的 HashMap 结构,故 RocketMQ 的索引文件其底层实现为 hash 索引 - -RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所有的队列共用一个日志数据文件(CommitLog)来存储。混合型存储结构(多个 Topic 的消息实体内容都存储于一个 CommitLog 中)**针对 Producer 和 Consumer 分别采用了数据和索引部分相分离的存储结构**,Producer 发送消息至 Broker 端,然后 Broker 端使用同步或者异步的方式对消息刷盘持久化,保存至 CommitLog 中。只要消息被持久化至磁盘文件 CommitLog 中,Producer 发送的消息就不会丢失,Consumer 也就肯定有机会去消费这条消息 - -服务端支持长轮询模式,当消费者无法拉取到消息后,可以等下一次消息拉取,Broker 允许等待 30s 的时间,只要这段时间内有新消息到达,将直接返回给消费端。RocketMQ 的具体做法是,使用 Broker 端的后台服务线程 ReputMessageService 不停地分发请求并异步构建 ConsumeQueue(逻辑消费队列)和 IndexFile(索引文件)数据 - - - -**** - - - -#### 存储优化 - -##### 存储媒介 - -两种持久化的方案: - -* 关系型数据库 DB:IO 读写性能比较差,如果 DB 出现故障,则 MQ 的消息就无法落盘存储导致线上故障,可靠性不高 - -* 文件系统:消息刷盘至所部署虚拟机/物理机的文件系统来做持久化,分为异步刷盘和同步刷盘两种模式。消息刷盘为消息存储提供了一种高效率、高可靠性和高性能的数据持久化方式,除非部署 MQ 机器本身或是本地磁盘挂了,一般不会出现无法持久化的问题 - - 注意:磁盘的顺序读写要比随机读写快很多,可以匹配上网络的速度,RocketMQ 的消息采用的顺序写 - -页缓存(PageCache)是 OS 对文件的缓存,用于加速对文件的读写。程序对文件进行顺序读写的速度几乎接近于内存的读写速度,就是因为 OS 将一部分的内存用作 PageCache,对读写访问操作进行了性能优化 - -* 对于数据的写入,OS 会先写入至 Cache 内,随后通过异步的方式由 pdflush 内核线程将 Cache 内的数据刷盘至物理磁盘上 -* 对于数据的读取,如果一次读取文件时出现未命中 PageCache 的情况,OS 从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行预读取(局部性原理) - -在 RocketMQ 中,ConsumeQueue 逻辑消费队列存储的数据较少,并且是顺序读取,在 PageCache 机制的预读取作用下,Consume Queue 文件的读性能几乎接近读内存,即使在有消息堆积情况下也不会影响性能。CommitLog 消息存储的日志数据文件读取内容时会产生较多的随机访问读取,严重影响性能。选择合适的系统 IO 调度算法和固态硬盘,比如设置调度算法为 Deadline,随机读的性能也会有所提升 - - - -*** - - - -##### 内存映射 - -操作系统分为用户态和内核态,文件操作、网络操作需要涉及这两种形态的切换,需要进行数据复制。一台服务器把本机磁盘文件的内容发送到客户端,分为两个步骤: - -* read:读取本地文件内容 - -* write:将读取的内容通过网络发送出去 - -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-文件与网络操作.png) - -补充:Prog → NET → I/O → 零拷贝部分的笔记详解相关内容 - -通过使用 mmap 的方式,可以省去向用户态的内存复制,RocketMQ 充分利用零拷贝技术,提高消息存盘和网络发送的速度。 - -RocketMQ 主要通过 MappedByteBuffer 对文件进行读写操作,利用了 NIO 中的 FileChannel 模型将磁盘上的物理文件直接映射到用户态的内存地址中,将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率 - -MappedByteBuffer 内存映射的方式限制一次只能映射 1.5~2G 的文件至用户态的虚拟内存,所以 RocketMQ 默认设置单个 CommitLog 日志数据文件为 1G。RocketMQ 的文件存储使用定长结构来存储,方便一次将整个文件映射至内存 - - - -*** - - - -#### 刷盘机制 - -同步刷盘:只有在消息真正持久化至磁盘后 RocketMQ 的 Broker 端才会真正返回给 Producer 端一个成功的 ACK 响应,保障 MQ消息的可靠性,但是性能上会有较大影响,一般适用于金融业务应用该模式较多 - -异步刷盘:利用 OS 的 PageCache 的优势,只要消息写入内存 PageCache 即可将成功的 ACK 返回给 Producer 端,降低了读写延迟,提高了 MQ 的性能和吞吐量。消息刷盘采用**后台异步线程**提交的方式进行,当内存里的消息量积累到一定程度时,触发写磁盘动作 - -通过 Broker 配置文件里的 flushDiskType 参数设置采用什么方式,可以配置成 SYNC_FLUSH、ASYNC_FLUSH 中的一个 - -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-刷盘机制.png) - - - -官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md - - - - - **** @@ -4912,6 +4815,27 @@ Consumer 端实现负载均衡的核心类 **RebalanceImpl** ### 消息重试 +#### 重投机制 + +生产者在发送消息时,同步消息和异步消息失败会重投,oneway 没有任何保证。消息重投保证消息尽可能发送成功、不丢失,但当出现消息量大、网络抖动时,可能会造成消息重复;生产者主动重发、Consumer 负载变化也会导致重复消息。 + +如下方法可以设置消息重投策略: + +- retryTimesWhenSendFailed:同步发送失败重投次数,默认为 2,因此生产者会最多尝试发送 retryTimesWhenSendFailed + 1 次。不会选择上次失败的 Broker,尝试向其他 Broker 发送,最大程度保证消息不丢。超过重投次数抛出异常,由客户端保证消息不丢。当出现 RemotingException、MQClientException 和部分 MQBrokerException 时会重投 +- retryTimesWhenSendAsyncFailed:异步发送失败重试次数,异步重试不会选择其他 Broker,仅在同一个 Broker 上做重试,不保证消息不丢 +- retryAnotherBrokerWhenNotStoreOK:消息刷盘(主或备)超时或 slave 不可用(返回状态非 SEND_OK),是否尝试发送到其他 Broker,默认 false,十分重要消息可以开启 + +注意点: + +* 如果同步模式发送失败,则选择到下一个 Broker,如果异步模式发送失败,则**只会在当前 Broker 进行重试** +* 发送消息超时时间默认3000毫秒,就不会再尝试重试 + + + +*** + + + #### 重试机制 Consumer 消费消息失败后,提供了一种重试机制,令消息再消费一次。Consumer 消费消息失败可以认为有以下几种情况: @@ -4927,7 +4851,7 @@ RocketMQ 会为每个消费组都设置一个 Topic 名称为 `%RETRY%+consumerG **无序消息情况下**,因为异常恢复需要一些时间,会为重试队列设置多个重试级别,每个重试级别都有对应的重新投递延时,重试次数越多投递延时就越大。RocketMQ 对于重试消息的处理是先保存至 Topic 名称为 `SCHEDULE_TOPIC_XXXX` 的延迟队列中,后台定时任务按照对应的时间进行 Delay 后重新保存至 `%RETRY%+consumerGroup` 的重试队列中 -消息队列 RocketMQ 默认允许每条消息最多重试 16 次,每次重试的间隔时间如下: +消息队列 RocketMQ 默认允许每条消息最多重试 16 次,每次重试的间隔时间如下表示: | 第几次重试 | 与上次重试的间隔时间 | 第几次重试 | 与上次重试的间隔时间 | | :--------: | :------------------: | :--------: | :------------------: | @@ -4942,7 +4866,9 @@ RocketMQ 会为每个消费组都设置一个 Topic 名称为 `%RETRY%+consumerG 如果消息重试 16 次后仍然失败,消息将**不再投递**,如果严格按照上述重试时间间隔计算,某条消息在一直消费失败的前提下,将会在接下来的 4 小时 46 分钟之内进行 16 次重试,超过这个时间范围消息将不再重试投递 -说明:一条消息无论重试多少次,消息的 Message ID 是不会改变的 +时间间隔不支持自定义配置,最大重试次数可通过自定义参数 `MaxReconsumeTimes` 取值进行配置,若配置超过 16 次,则超过的间隔时间均为 2 小时 + +说明:一条消息无论重试多少次,**消息的 Message ID 是不会改变的** @@ -5024,26 +4950,6 @@ public class MessageListenerImpl implements MessageListener { -*** - - - -#### 重投机制 - -生产者在发送消息时,同步消息和异步消息失败会重投,oneway 没有任何保证。消息重投保证消息尽可能发送成功、不丢失,但当出现消息量大、网络抖动时,可能会造成消息重复;生产者主动重发、Consumer 负载变化也会导致重复消息。 - -消息重复在 RocketMQ 中是无法避免的问题,如下方法可以设置消息重投策略: - -- retryTimesWhenSendFailed:同步发送失败重投次数,默认为 2,因此生产者会最多尝试发送 retryTimesWhenSendFailed + 1 次。不会选择上次失败的 Broker,尝试向其他 Broker 发送,最大程度保证消息不丢。超过重投次数抛出异常,由客户端保证消息不丢。当出现 RemotingException、MQClientException 和部分 MQBrokerException 时会重投 -- retryTimesWhenSendAsyncFailed:异步发送失败重试次数,异步重试不会选择其他 Broker,仅在同一个 Broker 上做重试,不保证消息不丢 -- retryAnotherBrokerWhenNotStoreOK:消息刷盘(主或备)超时或 slave 不可用(返回状态非 SEND_OK),是否尝试发送到其他 Broker,默认 false,十分重要消息可以开启 - -注意点: - -* 如果同步模式发送失败,则选择到下一个 Broker,如果异步模式发送失败,则**只会在当前 Broker 进行重试** - -* 发送消息超时时间默认3000毫秒,就不会再尝试重试 - *** @@ -5081,7 +4987,7 @@ public class MessageListenerImpl implements MessageListener { 消息队列 RocketMQ 消费者在接收到消息以后,需要根据业务上的唯一 Key 对消息做幂等处理 -在互联网应用中,尤其在网络不稳定的情况下,消息队列 RocketMQ 的消息有可能会出现重复,几种情况: +At least Once 机制保证消息不丢失,但是可能会造成消息重复,RocketMQ 中无法避免消息重复(Exactly-Once),在互联网应用中,尤其在网络不稳定的情况下,几种情况: - 发送时消息重复:当一条消息已被成功发送到服务端并完成持久化,此时出现了网络闪断或客户端宕机,导致服务端对客户端应答失败。此时生产者意识到消息发送失败并尝试再次发送消息,消费者后续会收到两条内容相同并且 Message ID 也相同的消息 @@ -5150,11 +5056,13 @@ public class MessageListenerImpl implements MessageListener { -## 源码分析 +## 原理解析 ### 服务端 -#### 启动方法 +#### 服务启动 + +##### 启动方法 NamesrvStartup 类中有 Namesrv 服务的启动方法: @@ -5221,7 +5129,7 @@ NamesrvStartup#start:启动 Namesrv 控制器 -#### 控制器类 +##### 控制器类 NamesrvController 用来初始化和启动 Namesrv 服务器 @@ -5899,7 +5807,7 @@ DefaultMQProducer 是生产者的默认实现类 -#### 实现者类 +#### 默认实现 ##### 成员属性 @@ -5920,19 +5828,6 @@ DefaultMQProducerImpl 类是默认的生产者实现类 private final ConcurrentMap topicPublishInfoTable = new ConcurrentHashMap(); ``` - ```java - public class TopicPublishInfo { - private boolean orderTopic = false; - private boolean haveTopicRouterInfo = false; - // 主题的全部队列 - private List messageQueueList = new ArrayList(); - // 使用 ThreaLocal 保存发送消息使用的队列 - private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex(); - // 主题的路由数据 - private TopicRouteData topicRouteData; - } - ``` - * 异步发送消息:相关信息 ```java @@ -6078,13 +5973,13 @@ DefaultMQProducerImpl 类是默认的生产者实现类 * `if (this.sendLatencyFaultEnable)`:默认不开启,可以通过配置开启 * `return tpInfo.selectOneMessageQueue(lastBrokerName)`:默认选择队列的方式,就是循环主题全部的队列 - * `int index = this.sendWhichQueue.getAndIncrement()`:选择队列的索引 - * `int pos = Math.abs(index) % this.messageQueueList.size()`:获取该索引对应的队列位置 - * `return this.messageQueueList.get(pos)`:返回消息队列 - + * `brokersSent[times] = mq.getBrokerName()`:将本次选择的 BrokerName 存入数组 + * `msg.setTopic(this.defaultMQProducer.withNamespace(msg.getTopic()))`:**重投的消息需要加上标记** + * `sendResult = this.sendKernelImpl`:核心发送方法 + * `this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false)`:更新一下时间 * `switch (communicationMode)`:异步或者单向消息直接返回 null,同步发送进入逻辑判断 @@ -6152,7 +6047,107 @@ DefaultMQProducerImpl 类是默认的生产者实现类 } ``` - + + + +*** + + + +#### 路由信息 + +TopicPublishInfo 类用来存储路由信息 + +成员变量: + +* 顺序消息: + + ```java + private boolean orderTopic = false; + ``` + +* 消息队列: + + ```java + private List messageQueueList = new ArrayList<>(); // 主题全部的消息队列 + private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex(); // 消息队列索引 + ``` + + ```java + // 【消息队列类】 + public class MessageQueue implements Comparable, Serializable { + private String topic; + private String brokerName; + private int queueId;// 队列 ID + } + ``` + +* 路由数据:主题对应的路由数据 + + ```java + private TopicRouteData topicRouteData; + ``` + + ```java + public class TopicRouteData extends RemotingSerializable { + private String orderTopicConf; + private List queueDatas; // 队列数据 + private List brokerDatas; // Broker 数据 + private HashMap/* Filter Server */> filterServerTable; + } + ``` + + ```java + public class QueueData implements Comparable { + private String brokerName; // 节点名称 + private int readQueueNums; // 读队列数 + private int writeQueueNums; // 写队列数 + private int perm; // 权限 + private int topicSynFlag; + } + ``` + + ```java + public class BrokerData implements Comparable { + private String cluster; // 集群名 + private String brokerName; // Broker节点名称 + private HashMap brokerAddrs; + } + ``` + +核心方法: + +* selectOneMessageQueue():**选择消息队列**使用 + + ```java + // 参数是上次失败时的 brokerName,可以为 null + public MessageQueue selectOneMessageQueue(final String lastBrokerName) { + if (lastBrokerName == null) { + return selectOneMessageQueue(); + } else { + // 遍历消息队列 + for (int i = 0; i < this.messageQueueList.size(); i++) { + // 获取队列的索引 + int index = this.sendWhichQueue.getAndIncrement(); + // 获取队列的下标位置 + int pos = Math.abs(index) % this.messageQueueList.size(); + if (pos < 0) + pos = 0; + // 获取消息队列 + MessageQueue mq = this.messageQueueList.get(pos); + // 与上次选择的不同就可以返回 + if (!mq.getBrokerName().equals(lastBrokerName)) { + return mq; + } + } + return selectOneMessageQueue(); + } + } + ``` + + + + **** @@ -6604,9 +6599,379 @@ NettyRemotingClient 类负责客户端的网络通信 +*** + + + +### 存储端 + +#### 存储机制 + +##### 存储结构 + +RocketMQ 中 Broker 负责存储消息转发消息,所以以下的结构是存储在 Broker Server 上的,生产者和消费者与 Broker 进行消息的收发是通过主题对应的 Message Queue 完成,类似于通道 + +RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成 的,CommitLog 是消息真正的**物理存储**文件,ConsumeQueue 是消息的逻辑队列,类似数据库的**索引节点**,存储的是指向物理存储的地址。**每个 Topic 下的每个 Message Queue 都有一个对应的 ConsumeQueue 文件** + +每条消息都会有对应的索引信息,Consumer 通过 ConsumeQueue 这个结构来读取消息实体内容 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息存储结构.png) + +* CommitLog:消息主体以及元数据的存储主体,存储 Producer 端写入的消息内容,消息内容不是定长的。消息主要是顺序写入日志文件,单个文件大小默认 1G,偏移量代表下一次写入的位置,当文件写满了就继续写入下一个文件 +* ConsumerQueue:消息消费队列,存储消息在 CommitLog 的索引。RocketMQ 消息消费时要遍历 CommitLog 文件,并根据主题 Topic 检索消息,这是非常低效的。引入 ConsumeQueue 作为消费消息的索引,保存了指定 Topic 下的队列消息在 CommitLog 中的起始物理偏移量 offset,消息大小 size 和消息 Tag 的 HashCode 值,每个 ConsumeQueue 文件大小约 5.72M +* IndexFile:为了消息查询提供了一种通过 Key 或时间区间来查询消息的方法,通过 IndexFile 来查找消息的方法不影响发送与消费消息的主流程。IndexFile 的底层存储为在文件系统中实现的 HashMap 结构,故 RocketMQ 的索引文件其底层实现为 hash 索引 + +RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所有的队列共用一个日志数据文件(CommitLog)来存储。混合型存储结构(多个 Topic 的消息实体内容都存储于一个 CommitLog 中)**针对 Producer 和 Consumer 分别采用了数据和索引部分相分离的存储结构**,Producer 发送消息至 Broker 端,然后 Broker 端使用同步或者异步的方式对消息刷盘持久化,保存至 CommitLog 中。只要消息被持久化至磁盘文件 CommitLog 中,Producer 发送的消息就不会丢失,Consumer 也就肯定有机会去消费这条消息 + +服务端支持长轮询模式,当消费者无法拉取到消息后,可以等下一次消息拉取,Broker 允许等待 30s 的时间,只要这段时间内有新消息到达,将直接返回给消费端。RocketMQ 的具体做法是,使用 Broker 端的后台服务线程 ReputMessageService 不停地分发请求并异步构建 ConsumeQueue(逻辑消费队列)和 IndexFile(索引文件)数据 + + + +**** + + + +##### 存储优化 + +###### 内存映射 + +操作系统分为用户态和内核态,文件操作、网络操作需要涉及这两种形态的切换,需要进行数据复制。一台服务器把本机磁盘文件的内容发送到客户端,分为两个步骤: + +* read:读取本地文件内容 + +* write:将读取的内容通过网络发送出去 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-文件与网络操作.png) + +补充:Prog → NET → I/O → 零拷贝部分的笔记详解相关内容 + +通过使用 mmap 的方式,可以省去向用户态的内存复制,RocketMQ 充分利用**零拷贝技术**,提高消息存盘和网络发送的速度。 + +RocketMQ 通过 MappedByteBuffer 对文件进行读写操作,利用了 NIO 中的 FileChannel 模型将磁盘上的物理文件直接映射到用户态的内存地址中,将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率 + +MappedByteBuffer 内存映射的方式**限制**一次只能映射 1.5~2G 的文件至用户态的虚拟内存,所以 RocketMQ 默认设置单个 CommitLog 日志数据文件为 1G。RocketMQ 的文件存储使用定长结构来存储,方便一次将整个文件映射至内存 + + + +*** + + + +###### 页缓存 + +页缓存(PageCache)是 OS 对文件的缓存,每一页的大小通常是 4K,用于加速对文件的读写。程序对文件进行顺序读写的速度几乎接近于内存的读写速度,就是因为 OS 将一部分的内存用作 PageCache,**对读写访问操作进行了性能优化** + +* 对于数据的写入,OS 会先写入至 Cache 内,随后通过异步的方式由 pdflush 内核线程将 Cache 内的数据刷盘至物理磁盘上 +* 对于数据的读取,如果一次读取文件时出现未命中 PageCache 的情况,OS 从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行预读取(局部性原理,最大 128K) + +在 RocketMQ 中,ConsumeQueue 逻辑消费队列存储的数据较少,并且是顺序读取,在 PageCache 机制的预读取作用下,Consume Queue 文件的读性能几乎接近读内存,即使在有消息堆积情况下也不会影响性能。但是 CommitLog 消息存储的日志数据文件读取内容时会产生较多的随机访问读取,严重影响性能。选择合适的系统 IO 调度算法和固态硬盘,比如设置调度算法为 Deadline,随机读的性能也会有所提升 + + + +*** + + + +##### 刷盘机制 + +两种持久化的方案: + +* 关系型数据库 DB:IO 读写性能比较差,如果 DB 出现故障,则 MQ 的消息就无法落盘存储导致线上故障,可靠性不高 +* 文件系统:消息刷盘至所部署虚拟机/物理机的文件系统来做持久化,分为异步刷盘和同步刷盘两种模式。消息刷盘为消息存储提供了一种高效率、高可靠性和高性能的数据持久化方式,除非部署 MQ 机器本身或是本地磁盘挂了,一般不会出现无法持久化的问题 + +RocketMQ 采用文件系统的方式,无论同步还是异步刷盘,都使用**顺序 IO**,因为磁盘的顺序读写要比随机读写快很多 + +* 同步刷盘:只有在消息真正持久化至磁盘后 RocketMQ 的 Broker 端才会真正返回给 Producer 端一个成功的 ACK 响应,保障 MQ消息的可靠性,但是性能上会有较大影响,一般适用于金融业务应用该模式较多 + +* 异步刷盘:利用 OS 的 PageCache,只要消息写入内存 PageCache 即可将成功的 ACK 返回给 Producer 端,降低了读写延迟,提高了 MQ 的性能和吞吐量。消息刷盘采用**后台异步线程**提交的方式进行,当内存里的消息量积累到一定程度时,触发写磁盘动作 + +通过 Broker 配置文件里的 flushDiskType 参数设置采用什么方式,可以配置成 SYNC_FLUSH、ASYNC_FLUSH 中的一个 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-刷盘机制.png) + + + +官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md + + + +*** + + + +#### MappedFile + +##### 成员属性 + +MappedFile 类是最基础的存储类,继承自 ReferenceResource 类,用来**保证线程安全** + +MappedFile 类成员变量: + +* 内存相关: + + ```java + public static final int OS_PAGE_SIZE = 1024 * 4;// 内存页大小:默认是 4k + private AtomicLong TOTAL_MAPPED_VIRTUAL_MEMORY; // 当前进程下所有的 mappedFile 占用的总虚拟内存大小 + private AtomicInteger TOTAL_MAPPED_FILES; // 当前进程下所有的 mappedFile 个数 + ``` + +* 数据位点: + + ```java + protected final AtomicInteger wrotePosition; // 当前 mappedFile 的数据写入点 + protected final AtomicInteger committedPosition;// 当前 mappedFile 的数据提交点 + private final AtomicInteger flushedPosition; // 数据落盘位点,在这之前的数据是持久化的安全数据 + // flushedPosition-wrotePosition 之间的数据属于脏页 + ``` + +* 文件相关:CL 是 CommitLog,CQ 是 ConsumeQueue + + ```java + private String fileName; // 文件名称,CL和CQ文件名是第一条消息的物理偏移量,索引文件是年月日时分秒 + private long fileFromOffset;// 文件名转long,代表该对象的【起始偏移量】 + private File file; // 文件对象 + ``` + +* 内存映射: + + ```java + protected FileChannel fileChannel; // 文件通道 + private MappedByteBuffer mappedByteBuffer; // 内存映射缓冲区,访问虚拟内存 + ``` + +ReferenceResource 类成员变量: + +* 引用数量:当 `refCount <= 0` 时,表示该资源可以释放了,没有任何其他程序依赖它了,用原子类保证线程安全 + + ```java + protected final AtomicLong refCount = new AtomicLong(1); // 初始值为 1 + ``` + +* 存活状态:表示资源的存活状态 + + ```java + protected volatile boolean available = true; + ``` + +* 是否清理:默认值 false,当执行完子类对象的 cleanup() 清理方法后,该值置为 true ,表示资源已经全部释放 + + ```java + protected volatile boolean cleanupOver = false; + ``` + +* 第一次关闭资源的时间:用来记录超时时间 + + ```java + private volatile long firstShutdownTimestamp = 0; + ``` + + + +*** + + + +##### 成员方法 + +MappedFile 类核心方法: + +* appendMessage():提供上层向内存映射中追加消息的方法,消息如何追加由 AppendMessageCallback 控制 + + ```java + // 参数一:消息 参数二:追加消息回调 + public AppendMessageResult appendMessage(MessageExtBrokerInner msg, AppendMessageCallback cb) + ``` + + ```java + // 将字节数组写入到文件通道 + public boolean appendMessage(final byte[] data) + ``` + +* flush():刷盘接口,参数 flushLeastPages 代表刷盘的最小页数 ,等于 0 时属于强制刷盘;> 0 时需要脏页(计算方法在数据位点)达到该值才进行物理刷盘;文件写满时强制刷盘 + + ```java + public int flush(final int flushLeastPages) + ``` + +* selectMappedBuffer():该方法以 pos 为开始位点 ,到有效数据为止,创建一个切片 ByteBuffer 作为数据副本,供业务访问数据 + + ```java + public SelectMappedBufferResult selectMappedBuffer(int pos) + ``` + +* destroy():销毁映射文件对象,并删除关联的系统文件,参数是强制关闭资源的时间 + + ```java + public boolean destroy(final long intervalForcibly) + ``` + +* cleanup():释放堆外内存,更新总虚拟内存和总内存映射文件数 + + ```java + public boolean cleanup(final long currentRef) + ``` + +* warmMappedFile():内存预热,当要新建的 MappedFile 对象大于 1g 时,执行该方法对该 MappedFile 的每个 Page Cache 进行写入一个字节进行分配内存,**将映射文件都加载到内存** + + ```java + public void warmMappedFile(FlushDiskType type, int pages) + ``` + +ReferenceResource 类核心方法: + +* hold():增加引用记数 refCount,方法加锁 + + ```java + public synchronized boolean hold() + ``` + +* shutdown():关闭资源,参数代表强制关闭资源的时间间隔 + + ```java + // 系统当前时间 - firstShutdownTimestamp 时间 > intervalForcibly 进行【强制关闭】 + public void shutdown(final long intervalForcibly) + ``` + +* release():引用计数减 1,当 refCount 为 0 时,调用子类的 cleanup 方法 + + ```java + public void release() + ``` + + + + + + + +*** + + + +#### MapQueue + +MappedFileQueue 用来管理 MappedFile 文件 + +成员变量: + +* 管理目录:CommitLog 是 `../store/commitlog`, ConsumeQueue 是 `../store/xxx_topic/0` + + ```java + private final String storePath; + ``` + +* 文件属性: + + ```java + private final int mappedFileSize; // 目录下每个文件大小,CL文件默认 1g,CQ文件 默认 600w字节 + private final CopyOnWriteArrayList mappedFiles; //目录下的每个 mappedFile 都加入该集合 + ``` + +* 数据位点: + + ```java + private long flushedWhere = 0; // 目录的刷盘位点,值为 mf.fileName + mf.wrotePosition + private long committedWhere = 0; // 目录的提交位点 + ``` + +* 消息存储: + + ```java + private volatile long storeTimestamp = 0; // 当前目录下最后一条 msg 的存储时间 + ``` + +* 创建服务:新建 MappedFile 实例,继承自 ServiceThread 是一个任务对象,run 方法用来创建实例 + + ```java + private final AllocateMappedFileService allocateMappedFileService; + ``` + +核心方法: + +* load():Broker 启动时,加载本地磁盘数据,该方法读取 storePath 目录下的文件,创建 MappedFile 对象放入集合内 + + ```java + public boolean load() + ``` + +* getLastMappedFile():获取当前正在顺序写入的 MappedFile 对象,如果最后一个 MappedFile 写满了,或者不存在 MappedFile 对象,则创建新的 MappedFile + + ```java + // 参数一:文件起始偏移量;参数二:当list为空时,是否新建 MappedFile + public MappedFile getLastMappedFile(final long startOffset, boolean needCreate) + ``` + +* flush():根据 flushedWhere 属性查找合适的 MappedFile,调用该 MappedFile 的落盘方法,并更新全局的 flushedWhere + + ```java + //参数:0 表示强制刷新, > 0 脏页数据必须达到 flushLeastPages 才刷新 + public boolean flush(final int flushLeastPages) + ``` + +* findMappedFileByOffset():根据偏移量查询对象 + + ```java + public MappedFile findMappedFileByOffset(final long offset, final boolean returnFirstOnNotFound) + ``` + +* deleteExpiredFileByTime():CL 删除过期文件,根据文件的保留时长决定是否删除 + + ```java + // 参数一:过期时间; 参数二:删除两个文件之间的时间间隔; 参数三:mf.destory传递的参数; 参数四:true 强制删除 + public int deleteExpiredFileByTime(final long expiredTime,final int deleteFilesInterval, final long intervalForcibly, final boolean cleanImmediately) + ``` + +* deleteExpiredFileByOffset():CQ 删除过期文件,遍历每个 MF 文件,获取当前文件最后一个数据单元的物理偏移量,小于 offset 说明当前 MF 文件内都是过期数据 + + ```java + // 参数一:consumeLog 目录下最小物理偏移量,就是第一条消息的 offset; + // 参数二:ConsumerQueue 文件内每个数据单元固定大小 + public int deleteExpiredFileByOffset(long offset, int unitSize) + ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +*** +## TEST diff --git a/Java.md b/Java.md index edc5ccd..c9daa70 100644 --- a/Java.md +++ b/Java.md @@ -10218,7 +10218,7 @@ public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制 #### 两种方式 -为对象分配内存:首先计算对象占用空间大小,接着在堆中划分一块内存给新对象 +不分配内存的对象无法进行其他操作,JVM 为对象分配内存的过程:首先计算对象占用空间大小,接着在堆中划分一块内存给新对象 * 如果内存规整,使用指针碰撞(Bump The Pointer)。所有用过的内存在一边,空闲的内存在另外一边,中间有一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离 * 如果内存不规整,虚拟机需要维护一个空闲列表(Free List)分配。已使用的内存和未使用的内存相互交错,虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容 diff --git a/Prog.md b/Prog.md index 36c0ed0..dc6f5e3 100644 --- a/Prog.md +++ b/Prog.md @@ -2292,7 +2292,7 @@ Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互 可见性:是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值 -存在不可见问题的根本原因是由于缓存的存在,线程持有的是共享变量的副本,无法感知其他线程对于共享变量的更改,导致读取的值不是最新的 +存在不可见问题的根本原因是由于缓存的存在,线程持有的是共享变量的副本,无法感知其他线程对于共享变量的更改,导致读取的值不是最新的。但是 final 修饰的变量是**不可变**的,就算有缓存,也不会存在不可见的问题 main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止: @@ -3613,7 +3613,7 @@ final 变量的赋值通过 putfield 指令来完成,在这条指令之后也 不可变:如果一个对象不能够修改其内部状态(属性),那么就是不可变对象 -不可变对象线程安全的,因为不存在并发修改,是另一种避免竞争的方式 +不可变对象线程安全的,不存在并发修改和可见性问题,是另一种避免竞争的方式 String 类也是不可变的,该类和类中所有属性都是 final 的 @@ -13752,7 +13752,7 @@ DMA (Direct Memory Access) :直接存储器访问,让外部设备不通过 C 把内存数据传输到网卡然后发送: * 没有 DMA:CPU 读内存数据到 CPU 高速缓存,再写到网卡,这样就把 CPU 的速度拉低到和网卡一个速度 -* 使用 DMA:把数据读到 Socket 内核缓存区(CPU 复制),CPU 分配给 DMA 开始**异步**操作,DMA 读取 Socket 缓冲区到 DMA 缓冲区,然后写到网卡。DMA 执行完后中断(就是通知) CPU,这时 Socket 内核缓冲区为空,CPU 从用户态切换到内核态,执行中断处理程序,将需要使用 Socket 缓冲区的阻塞进程移到就绪队列 +* 使用 DMA:把数据读到 Socket 内核缓存区(CPU 复制),CPU 分配给 DMA 开始**异步**操作,DMA 读取 Socket 缓冲区到 DMA 缓冲区,然后写到网卡。DMA 执行完后**中断**(就是通知) CPU,这时 Socket 内核缓冲区为空,CPU 从用户态切换到内核态,执行中断处理程序,将需要使用 Socket 缓冲区的阻塞进程移到就绪队列 一个完整的 DMA 传输过程必须经历 DMA 请求、DMA 响应、DMA 传输、DMA 结束四个步骤: @@ -13797,7 +13797,7 @@ read 调用图示:read、write 都是系统调用指令 #### mmap -mmap(Memory Mapped Files)加 write 实现零拷贝,**零拷贝就是没有数据从内核空间复制到用户空间** +mmap(Memory Mapped Files)内存映射加 write 实现零拷贝,**零拷贝就是没有数据从内核空间复制到用户空间** 用户空间和内核空间都使用内存,所以可以共享同一块物理内存地址,省去用户态和内核态之间的拷贝。写网卡时,共享空间的内容拷贝到 Socket 缓冲区,然后交给 DMA 发送到网卡,只需要 3 次复制 @@ -14902,8 +14902,8 @@ FileChannel 提供 map 方法返回 MappedByteBuffer 对象,把文件映射到 FileChannel 中的成员属性: * MapMode.mode:内存映像文件访问的方式,共三种: - * `MapMode.READ_ONLY`:只读,试图修改得到的缓冲区将导致抛出异常。 - * `MapMode.READ_WRITE`:读/写,对得到的缓冲区的更改最终将写入文件;但该更改对映射到同一文件的其他程序不一定是可见的 + * `MapMode.READ_ONLY`:只读,修改得到的缓冲区将导致抛出异常 + * `MapMode.READ_WRITE`:读/写,对缓冲区的更改最终将写入文件,但此次修改对映射到同一文件的其他程序不一定是可见的 * `MapMode.PRIVATE`:私用,可读可写,但是修改的内容不会写入文件,只是 buffer 自身的改变,称之为写时复制 * `public final FileLock lock()`:获取此文件通道的排他锁 @@ -14915,7 +14915,7 @@ MappedByteBuffer,可以让文件在直接内存(堆外内存)中进行修 MappedByteBuffer 较之 ByteBuffer 新增的三个方法: -- `final MappedByteBuffer force()`:缓冲区是 READ_WRITE 模式下,对缓冲区内容的修改强行写入文件 +- `final MappedByteBuffer force()`:缓冲区是 READ_WRITE 模式下,对缓冲区内容的修改**强制写入文件** - `final MappedByteBuffer load()`:将缓冲区的内容载入物理内存,并返回该缓冲区的引用 - `final boolean isLoaded()`:如果缓冲区的内容在物理内存中,则返回真,否则返回假 @@ -14924,7 +14924,7 @@ public class MappedByteBufferTest { public static void main(String[] args) throws Exception { // 读写模式 RandomAccessFile ra = new RandomAccessFile("1.txt", "rw"); - //获取对应的通道 + // 获取对应的通道 FileChannel channel = ra.getChannel(); /** From 68e76ef7463339f7198bc668aa7b0d017c95e119 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 9 Jan 2022 22:14:49 +0800 Subject: [PATCH 102/168] Update Java Notes --- Frame.md | 2354 ++++++++++++++++++++++++++++++++++++------------------ Java.md | 2 +- 2 files changed, 1589 insertions(+), 767 deletions(-) diff --git a/Frame.md b/Frame.md index b809388..6f84a04 100644 --- a/Frame.md +++ b/Frame.md @@ -3485,6 +3485,8 @@ RCVBUF_ALLOCATOR:属于 SocketChannal 参数 +参考视频:https://www.bilibili.com/video/BV1L4411y7mn + **** @@ -4494,7 +4496,7 @@ public class Producer { ## 系统特性 -### 工作机制 +### 工作流程 #### 模块介绍 @@ -4533,7 +4535,7 @@ Broker 包含了以下几个重要子模块: -#### 工作流程 +#### 总体流程 RocketMQ 的工作流程: @@ -4573,47 +4575,6 @@ At least Once:至少一次,指每个消息必须投递一次,Consumer 先 -**** - - - -### 消息查询 - -#### Message ID - -RocketMQ 支持按照两种维度进行消息查询:按照 Message ID 查询消息、按照 Message Key 查询消息 - -RocketMQ 中的 MessageId 的长度总共有 16 字节,其中包含了消息存储主机地址(IP 地址和端口),消息 Commit Log offset - -实现方式:Client 端从 MessageId 中解析出 Broker 的地址(IP 地址和端口)和 Commit Log 的偏移地址,封装成一个 RPC 请求后通过 Remoting 通信层发送(业务请求码 VIEW_MESSAGE_BY_ID)。Broker 端走的是 QueryMessageProcessor,读取消息的过程用其中的 CommitLog 的 offset 和 size 去 CommitLog 中找到真正的记录并解析成一个完整的消息返回 - - - -*** - - - -#### Message Key - -按照 Message Key 查询消息,主要是基于 RocketMQ 的 IndexFile 索引文件来实现的,RocketMQ 的索引文件逻辑结构,类似 JDK 中 HashMap 的实现,具体结构如下: - -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-IndexFile索引文件.png) - -IndexFile 索引文件为提供了通过 Message Key 查询消息的服务,IndexFile 文件的存储在 `$HOME\store\index${fileName}`,文件名 fileName 是以创建时的时间戳命名,文件大小是固定的,等于 `40+500W*4+2000W*20= 420000040` 个字节大小。如果消息的 properties 中设置了 UNIQ_KEY 这个属性,就用 `topic + “#” + UNIQ_KEY` 作为 key 来做写入操作;如果消息设置了 KEYS 属性(多个 KEY 以空格分隔),也会用 `topic + “#” + KEY` 来做索引 - -整个 Index File 的结构如图,40 Byte 的 Header 用于保存一些总的统计信息,`4*500W` 的 Slot Table 并不保存真正的索引数据,而是保存每个槽位对应的单向链表的头,即一个 Index File 可以保存 2000W 个索引,`20*2000W` 是真正的索引数据 - -索引数据包含了 Key Hash/CommitLog Offset/Timestamp/NextIndex offset 这四个字段,一共 20 Byte - -* NextIndex offset 即前面读出来的 slotValue,如果有 hash 冲突,就可以用这个字段将所有冲突的索引用链表的方式串起来 -* Timestamp 记录的是消息 storeTimestamp 之间的差,并不是一个绝对的时间 - -实现方式:通过 Broker 端的 QueryMessageProcessor 业务处理器来查询,读取消息的过程用 Topic 和 Key 找到 IndexFile 索引文件中的一条记录,根据其中的 CommitLog Offset 从 CommitLog 文件中读取消息的实体内容 - - - - - *** @@ -4813,9 +4774,27 @@ Consumer 端实现负载均衡的核心类 **RebalanceImpl** -### 消息重试 +### 消息机制 + +#### 消息查询 + +RocketMQ 支持按照两种维度进行消息查询:按照 Message ID 查询消息、按照 Message Key 查询消息 + +* RocketMQ 中的 MessageID 的长度总共有 16 字节,其中包含了消息存储主机地址(IP 地址和端口),消息 Commit Log offset + + 实现方式:Client 端从 MessageID 中解析出 Broker 的地址(IP 地址和端口)和 Commit Log 的偏移地址,封装成一个 RPC 请求后通过 Remoting 通信层发送(业务请求码 VIEW_MESSAGE_BY_ID)。Broker 端走的是 QueryMessageProcessor,读取消息的过程用其中的 CommitLog 的 offset 和 size 去 CommitLog 中找到真正的记录并解析成一个完整的消息返回 + +* 按照 Message Key 查询消息,IndexFile 索引文件为提供了通过 Message Key 查询消息的服务 -#### 重投机制 + 实现方式:通过 Broker 端的 QueryMessageProcessor 业务处理器来查询,读取消息的过程用 Topic 和 Key 找到 IndexFile 索引文件中的一条记录,根据其中的 CommitLog Offset 从 CommitLog 文件中读取消息的实体内容 + + + +*** + + + +#### 消息重投 生产者在发送消息时,同步消息和异步消息失败会重投,oneway 没有任何保证。消息重投保证消息尽可能发送成功、不丢失,但当出现消息量大、网络抖动时,可能会造成消息重复;生产者主动重发、Consumer 负载变化也会导致重复消息。 @@ -4836,7 +4815,7 @@ Consumer 端实现负载均衡的核心类 **RebalanceImpl** -#### 重试机制 +#### 消息重试 Consumer 消费消息失败后,提供了一种重试机制,令消息再消费一次。Consumer 消费消息失败可以认为有以下几种情况: @@ -5165,7 +5144,7 @@ NamesrvController 用来初始化和启动 Namesrv 服务器 @Override public void run() { // 扫描 brokerLiveTable 表,将两小时没有活动的 broker 关闭, - //通过 next.getKey() 获取 broker 的地址,然后【关闭服务器与broker物理节点的 channel】 + // 通过 next.getKey() 获取 broker 的地址,然后【关闭服务器与broker物理节点的 channel】 NamesrvController.this.routeInfoManager.scanNotActiveBroker(); } }, 5, 10, TimeUnit.SECONDS); @@ -5696,356 +5675,359 @@ RouteInfoManager#registerBroker:注册 Broker 的信息 + + **** -### 生产者 +### 存储端 -#### 生产者类 +#### 存储机制 -DefaultMQProducer 是生产者的默认实现类 +##### 存储结构 -成员变量: +RocketMQ 中 Broker 负责存储消息转发消息,所以以下的结构是存储在 Broker Server 上的,生产者和消费者与 Broker 进行消息的收发是通过主题对应的 Message Queue 完成,类似于通道 -* 生产者实现类: +RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成 的,CommitLog 是消息真正的**物理存储**文件,ConsumeQueue 是消息的逻辑队列,类似数据库的**索引节点**,存储的是指向物理存储的地址。**每个 Topic 下的每个 Message Queue 都有一个对应的 ConsumeQueue 文件** - ```java - protected final transient DefaultMQProducerImpl defaultMQProducerImpl - ``` +每条消息都会有对应的索引信息,Consumer 通过 ConsumeQueue 这个结构来读取消息实体内容 -* 生产者组:发送事务消息,Broker 端进行事务回查(补偿机制)时,选择当前生产者组的下一个生产者进行事务回查 +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息存储结构.png) - ```java - private String producerGroup; - ``` +* CommitLog:消息主体以及元数据的存储主体,存储 Producer 端写入的消息内容,消息内容不是定长的。消息主要是顺序写入日志文件,单个文件大小默认 1G,偏移量代表下一次写入的位置,当文件写满了就继续写入下一个文件 +* ConsumerQueue:消息消费队列,存储消息在 CommitLog 的索引。RocketMQ 消息消费时要遍历 CommitLog 文件,并根据主题 Topic 检索消息,这是非常低效的。引入 ConsumeQueue 作为消费消息的索引,保存了指定 Topic 下的队列消息在 CommitLog 中的起始物理偏移量 offset,消息大小 size 和消息 Tag 的 HashCode 值,每个 ConsumeQueue 文件大小约 5.72M +* IndexFile:为了消息查询提供了一种通过 Key 或时间区间来查询消息的方法,通过 IndexFile 来查找消息的方法不影响发送与消费消息的主流程。IndexFile 的底层存储为在文件系统中实现的 HashMap 结构,故 RocketMQ 的索引文件其底层实现为 **hash 索引** -* 默认主题:isAutoCreateTopicEnable 开启时,当发送消息指定的 Topic 在 Namesrv 未找到路由信息,使用该值创建 Topic 信息 +RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所有的队列共用一个日志数据文件(CommitLog)来存储。混合型存储结构(多个 Topic 的消息实体内容都存储于一个 CommitLog 中)**针对 Producer 和 Consumer 分别采用了数据和索引部分相分离的存储结构**,Producer 发送消息至 Broker 端,然后 Broker 端使用同步或者异步的方式对消息刷盘持久化,保存至 CommitLog 中。只要消息被持久化至磁盘文件 CommitLog 中,Producer 发送的消息就不会丢失,Consumer 也就肯定有机会去消费这条消息 - ```java - private String createTopicKey = TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC; - // 值为【TBW102】,Just for testing or demo program - ``` +服务端支持长轮询模式,当消费者无法拉取到消息后,可以等下一次消息拉取,Broker 允许等待 30s 的时间,只要这段时间内有新消息到达,将直接返回给消费端。RocketMQ 的具体做法是,使用 Broker 端的后台服务线程 ReputMessageService 不停地分发请求并异步构建 ConsumeQueue(逻辑消费队列)和 IndexFile(索引文件)数据 -* 消息重投:系统特性消息重试部分详解了三个参数的作用 - ```java - private int retryTimesWhenSendFailed = 2; // 同步发送失败后重试的发送次数,加上第一次发送,一共三次 - private int retryTimesWhenSendAsyncFailed = 2; // 异步 - private boolean retryAnotherBrokerWhenNotStoreOK = false; // 消息未存储成功,选择其他 Broker 重试 - ``` -* 消息队列: +**** - ```java - private volatile int defaultTopicQueueNums = 4; // 默认 Broker 创建的队列数 - ``` -* 消息属性: - ```java - private int sendMsgTimeout = 3000; // 发送消息的超时限制 - private int compressMsgBodyOverHowmuch = 1024 * 4; // 压缩阈值,当 msg body 超过 4k 后使用压缩 - private int maxMessageSize = 1024 * 1024 * 4; // 消息体的最大限制,默认 4M - private TraceDispatcher traceDispatcher = null; // 消息轨迹 +##### 存储优化 -构造方法: +###### 内存映射 -* 构造方法: +操作系统分为用户态和内核态,文件操作、网络操作需要涉及这两种形态的切换,需要进行数据复制。一台服务器把本机磁盘文件的内容发送到客户端,分为两个步骤: - ```java - public DefaultMQProducer(final String namespace, final String producerGroup, RPCHook rpcHook) { - this.namespace = namespace; - this.producerGroup = producerGroup; - // 创建生产者实现对象 - defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook); - } - ``` +* read:读取本地文件内容 -成员方法: +* write:将读取的内容通过网络发送出去 -* start():启动方法 +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-文件与网络操作.png) - ```java - public void start() throws MQClientException { - // 重置生产者组名,如果传递了命名空间,则 【namespace%group】 - this.setProducerGroup(withNamespace(this.producerGroup)); - // 生产者实现对象启动 - this.defaultMQProducerImpl.start(); - if (null != traceDispatcher) { - // 消息轨迹的逻辑 - traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel()); - } - } - ``` +补充:Prog → NET → I/O → 零拷贝部分的笔记详解相关内容 -* send():**发送消息**: +通过使用 mmap 的方式,可以省去向用户态的内存复制,RocketMQ 充分利用**零拷贝技术**,提高消息存盘和网络发送的速度。 - ```java - public SendResult send(Message msg){ - // 校验消息 - Validators.checkMessage(msg, this); - // 设置消息 Topic - msg.setTopic(withNamespace(msg.getTopic())); - return this.defaultMQProducerImpl.send(msg); - } - ``` +RocketMQ 通过 MappedByteBuffer 对文件进行读写操作,利用了 NIO 中的 FileChannel 模型将磁盘上的物理文件直接映射到用户态的内存地址中,将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率 -* request():请求方法,**需要消费者回执消息**,又叫回退消息 +MappedByteBuffer 内存映射的方式**限制**一次只能映射 1.5~2G 的文件至用户态的虚拟内存,所以 RocketMQ 默认设置单个 CommitLog 日志数据文件为 1G。RocketMQ 的文件存储使用定长结构来存储,方便一次将整个文件映射至内存 - ```java - public Message request(final Message msg, final MessageQueue mq, final long timeout) { - msg.setTopic(withNamespace(msg.getTopic())); - return this.defaultMQProducerImpl.request(msg, mq, timeout); - } - ``` +*** + + + +###### 页缓存 + +页缓存(PageCache)是 OS 对文件的缓存,每一页的大小通常是 4K,用于加速对文件的读写。程序对文件进行顺序读写的速度几乎接近于内存的读写速度,就是因为 OS 将一部分的内存用作 PageCache,**对读写访问操作进行了性能优化** + +* 对于数据的写入,OS 会先写入至 Cache 内,随后通过异步的方式由 pdflush 内核线程将 Cache 内的数据刷盘至物理磁盘上 +* 对于数据的读取,如果一次读取文件时出现未命中 PageCache 的情况,OS 从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行预读取(局部性原理,最大 128K) + +在 RocketMQ 中,ConsumeQueue 逻辑消费队列存储的数据较少,并且是顺序读取,在 PageCache 机制的预读取作用下,Consume Queue 文件的读性能几乎接近读内存,即使在有消息堆积情况下也不会影响性能。但是 CommitLog 消息存储的日志数据文件读取内容时会产生较多的随机访问读取,严重影响性能。选择合适的系统 IO 调度算法和固态硬盘,比如设置调度算法为 Deadline,随机读的性能也会有所提升 + *** -#### 默认实现 +##### 刷盘机制 + +两种持久化的方案: + +* 关系型数据库 DB:IO 读写性能比较差,如果 DB 出现故障,则 MQ 的消息就无法落盘存储导致线上故障,可靠性不高 +* 文件系统:消息刷盘至所部署虚拟机/物理机的文件系统来做持久化,分为异步刷盘和同步刷盘两种模式。消息刷盘为消息存储提供了一种高效率、高可靠性和高性能的数据持久化方式,除非部署 MQ 机器本身或是本地磁盘挂了,一般不会出现无法持久化的问题 + +RocketMQ 采用文件系统的方式,无论同步还是异步刷盘,都使用**顺序 IO**,因为磁盘的顺序读写要比随机读写快很多 + +* 同步刷盘:只有在消息真正持久化至磁盘后 RocketMQ 的 Broker 端才会真正返回给 Producer 端一个成功的 ACK 响应,保障 MQ消息的可靠性,但是性能上会有较大影响,一般适用于金融业务应用该模式较多 + +* 异步刷盘:利用 OS 的 PageCache,只要消息写入内存 PageCache 即可将成功的 ACK 返回给 Producer 端,降低了读写延迟,提高了 MQ 的性能和吞吐量。消息刷盘采用**后台异步线程**提交的方式进行,当内存里的消息量积累到一定程度时,触发写磁盘动作 + +通过 Broker 配置文件里的 flushDiskType 参数设置采用什么方式,可以配置成 SYNC_FLUSH、ASYNC_FLUSH 中的一个 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-刷盘机制.png) + + + +官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md + + + +*** + + + +#### MappedFile ##### 成员属性 -DefaultMQProducerImpl 类是默认的生产者实现类 +MappedFile 类是最基础的存储类,继承自 ReferenceResource 类,用来**保证线程安全** -成员变量: +MappedFile 类成员变量: -* 实例对象: +* 内存相关: ```java - private final DefaultMQProducer defaultMQProducer; // 持有默认生产者对象,用来获取对象中的配置信息 - private MQClientInstance mQClientFactory; // 客户端实例对象,生产者启动后需要注册到该客户端对象内 + public static final int OS_PAGE_SIZE = 1024 * 4;// 内存页大小:默认是 4k + private AtomicLong TOTAL_MAPPED_VIRTUAL_MEMORY; // 当前进程下所有的 mappedFile 占用的总虚拟内存大小 + private AtomicInteger TOTAL_MAPPED_FILES; // 当前进程下所有的 mappedFile 个数 ``` -* 主题发布信息映射表:key 是 Topic,value 是发布信息 +* 数据位点: ```java - private final ConcurrentMap topicPublishInfoTable = new ConcurrentHashMap(); + protected final AtomicInteger wrotePosition; // 当前 mappedFile 的数据写入点 + protected final AtomicInteger committedPosition;// 当前 mappedFile 的数据提交点 + private final AtomicInteger flushedPosition; // 数据落盘位点,在这之前的数据是持久化的安全数据 + // flushedPosition-wrotePosition 之间的数据属于脏页 ``` -* 异步发送消息:相关信息 +* 文件相关:CL 是 CommitLog,CQ 是 ConsumeQueue ```java - private final BlockingQueue asyncSenderThreadPoolQueue;// 异步发送消息,异步线程池使用的队列 - private final ExecutorService defaultAsyncSenderExecutor; // 异步发送消息默认使用的线程池 - private ExecutorService asyncSenderExecutor; // 异步消息发送线程池,指定后就不使用默认线程池了 + private String fileName; // 文件名称,CL和CQ文件名是第一条消息的物理偏移量,索引文件是年月日时分秒 + private long fileFromOffset;// 文件名转long,代表该对象的【起始偏移量】 + private File file; // 文件对象 ``` -* 定时器:执行定时任务 + **MF 中以物理偏移量作为文件名,可以更好的寻址和进行判断** + +* 内存映射: ```java - private final Timer timer = new Timer("RequestHouseKeepingService", true); // 守护线程 + protected FileChannel fileChannel; // 文件通道 + private MappedByteBuffer mappedByteBuffer; // 内存映射缓冲区,访问虚拟内存 ``` -* 状态信息:服务的状态,默认创建状态 +ReferenceResource 类成员变量: + +* 引用数量:当 `refCount <= 0` 时,表示该资源可以释放了,没有任何其他程序依赖它了,用原子类保证线程安全 ```java - private ServiceState serviceState = ServiceState.CREATE_JUST; + protected final AtomicLong refCount = new AtomicLong(1); // 初始值为 1 ``` -* 压缩等级:ZIP 压缩算法的等级,默认是 5,越高压缩效果好,但是压缩的更慢 +* 存活状态:表示资源的存活状态 ```java - private int zipCompressLevel = Integer.parseInt(System.getProperty..., "5")); + protected volatile boolean available = true; ``` -* 容错策略:选择队列的容错策略 +* 是否清理:默认值 false,当执行完子类对象的 cleanup() 清理方法后,该值置为 true ,表示资源已经全部释放 ```java - private MQFaultStrategy mqFaultStrategy = new MQFaultStrategy(); + protected volatile boolean cleanupOver = false; ``` -* 钩子:用来进行前置或者后置处理 +* 第一次关闭资源的时间:用来记录超时时间 ```java - ArrayList sendMessageHookList; // 发送消息的钩子,留给用户扩展使用 - ArrayList checkForbiddenHookList; // 对比上面的钩子,可以抛异常,控制消息是否可以发送 - private final RPCHook rpcHook; // 传递给 NettyRemotingClient + private volatile long firstShutdownTimestamp = 0; ``` -构造方法: + -* 默认构造: +*** - ```java - public DefaultMQProducerImpl(final DefaultMQProducer defaultMQProducer) { - // 默认 RPC HOOK 是空 - this(defaultMQProducer, null); - } - ``` -* 有参构造: + +##### 成员方法 + +MappedFile 类核心方法: + +* appendMessage():提供上层向内存映射中追加消息的方法,消息如何追加由 AppendMessageCallback 控制 ```java - public DefaultMQProducerImpl(final DefaultMQProducer defaultMQProducer, RPCHook rpcHook) { - // 属性赋值 - this.defaultMQProducer = defaultMQProducer; - this.rpcHook = rpcHook; - - // 创建【异步消息线程池任务队列】,长度是 5w - this.asyncSenderThreadPoolQueue = new LinkedBlockingQueue(50000); - // 创建默认的异步消息任务线程池 - this.defaultAsyncSenderExecutor = new ThreadPoolExecutor( - // 核心线程数和最大线程数都是 系统可用的计算资源(8核16线程的系统就是 16)... - } + // 参数一:消息 参数二:追加消息回调 + public AppendMessageResult appendMessage(MessageExtBrokerInner msg, AppendMessageCallback cb) ``` - + ```java + // 将字节数组写入到文件通道 + public boolean appendMessage(final byte[] data) + ``` -**** +* flush():刷盘接口,参数 flushLeastPages 代表刷盘的最小页数 ,等于 0 时属于强制刷盘;> 0 时需要脏页(计算方法在数据位点)达到该值才进行物理刷盘;文件写满时强制刷盘 + ```java + public int flush(final int flushLeastPages) + ``` +* selectMappedBuffer():该方法以 pos 为开始位点 ,到有效数据为止,创建一个切片 ByteBuffer 作为数据副本,供业务访问数据 -##### 成员方法 + ```java + public SelectMappedBufferResult selectMappedBuffer(int pos) + ``` -* start():启动方法,参数默认是 true,代表正常的启动路径 +* destroy():销毁映射文件对象,并删除关联的系统文件,参数是强制关闭资源的时间 - * `this.serviceState = ServiceState.START_FAILED`:先修改为启动失败,成功后再修改,这种思想很常见 + ```java + public boolean destroy(final long intervalForcibly) + ``` - * `this.checkConfig()`:判断生产者组名不能是空,也不能是 default_PRODUCER +* cleanup():释放堆外内存,更新总虚拟内存和总内存映射文件数 - * `if (!getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP))`:条件成立说明当前生产者不是内部产生者,内部生产者是**处理消息回退**的这种情况使用的生产者 + ```java + public boolean cleanup(final long currentRef) + ``` - `this.defaultMQProducer.changeInstanceNameToPID()`:正常的生产者,修改生产者实例名称为当前进程的 PID +* warmMappedFile():内存预热,当要新建的 MappedFile 对象大于 1g 时,执行该方法对该 MappedFile 的每个 Page Cache 进行写入一个字节进行分配内存,**将映射文件全部加载到内存** - * ` this.mQClientFactory = ...`:获取当前进程的 MQ 客户端实例对象,从 factoryTable 中获取 key 为 客户端 ID,格式是`ip@pid`,**一个 JVM 进程只有一个 PID,也只有一个 MQClientInstance** + ```java + public void warmMappedFile(FlushDiskType type, int pages) + ``` - * `boolean registerOK = mQClientFactory.registerProducer(...)`:将生产者注册到 RocketMQ 客户端实例内 +* mlock():锁住指定的内存区域避免被操作系统调到 **swap 空间**,一次性将一段数据读入到映射内存区域,减少了缺页异常的产生 - * `this.topicPublishInfoTable.put(...)`:添加一个主题发布信息,key 是 **TBW102** ,value 是一个空对象 + ```java + public void mlock() + ``` - * `if (startFactory) `:正常启动路径 + swap space 是磁盘上的一块区域,可以是一个分区或者一个文件或者是组合。当系统物理内存不足时,Linux 会将内存中不常访问的数据保存到 swap 区域上,这样系统就可以有更多的物理内存为各个进程服务,而当系统需要访问 swap 上存储的内容时,需要通过**缺页中断**将 swap 上的数据加载到内存中 - `mQClientFactory.start()`:启动 RocketMQ 客户端实例对象 +ReferenceResource 类核心方法: - * `this.serviceState = ServiceState.RUNNING`:修改生产者实例的状态 +* hold():增加引用记数 refCount,方法加锁 - * `this.mQClientFactory.sendHeartbeatToAllBrokerWithLock()`:RocketMQ 客户端实例向已知的 Broker 节点发送一次心跳(也是定时任务) - * `this.timer.scheduleAtFixedRate()`: request 发送的回执信息,启动定时任务每秒一次删除超时请求 - - * 生产者 msg 添加信息关联 ID 发送到 Broker - * 消费者从 Broker 拿到消息后会检查 msg 类型是一个需要回执的消息,处理完消息后会根据 msg 关联 ID 和客户端 ID 生成一条响应结果消息发送到 Broker,Broker 判断为回执消息,会根据客户端ID 找到 channel 推送给生产者 - * 生产者拿到回执消息后,读取出来关联 ID 找到对应的 RequestFuture,将阻塞线程唤醒 + ```java + public synchronized boolean hold() + ``` -* sendDefaultImpl():发送消息 +* shutdown():关闭资源,参数代表强制关闭资源的时间间隔 ```java - //参数1:消息;参数2:发送模式(同步异步单向);参数3:回调函数,异步发送时需要;参数4:发送超时时间, 默认 3 秒 - private SendResult sendDefaultImpl(msg, communicationMode, sendCallback,timeout) {} + // 系统当前时间 - firstShutdownTimestamp 时间 > intervalForcibly 进行【强制关闭】 + public void shutdown(final long intervalForcibly) ``` - * `this.makeSureStateOK()`:校验生产者状态是运行中,否则抛出异常 - - * `Validators.checkMessage(msg, this.defaultMQProducer)`:校验消息规格 +* release():引用计数减 1,当 refCount 为 0 时,调用子类的 cleanup 方法 - * `long beginTimestampPrev, endTimestamp`:本轮发送的开始时间和本轮的结束时间 + ```java + public void release() + ``` - * `topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic())`:**获取当前消息主题的发布信息** + - * `this.topicPublishInfoTable.get(topic)`:尝试从本地主题发布信息映射表获取信息,不空直接返回 - * `if (null == topicPublishInfo || !topicPublishInfo.ok())`:本地没有需要去 MQ 客户端获取 - `this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo())`:保存一份空数据 - `this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic)`:从 Namesrv 更新该 Topic 的路由数据 - `topicPublishInfo = this.topicPublishInfoTable.get(topic)`:重新从本地获取发布信息 +*** - * `this.mQClientFactory.updateTopicRouteInfoFromNameServer(..)`:**路由数据是空,获取默认 TBW102 的数据** - * `return topicPublishInfo`:返回 TBW102 主题的发布信息 - * `int timesTotal, times `:发送的总尝试次数和当前是第几次发送 +#### MapQueue - * `String[] brokersSent = new String[timesTotal]`:下标索引代表第几次发送,值代表这次发送选择 Broker name +##### 成员属性 - * `for (; times < timesTotal; times++)`:循环发送,发送成功或者发送尝试次数达到上限,结束循环 +MappedFileQueue 用来管理 MappedFile 文件 - * `String lastBrokerName = null == mq ? null : mq.getBrokerName()`:获取上次发送失败的 BrokerName +成员变量: - * `mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName)`:**从发布信息中选择一个队列** +* 管理目录:CommitLog 是 `../store/commitlog`, ConsumeQueue 是 `../store/xxx_topic/0` - * `if (this.sendLatencyFaultEnable)`:默认不开启,可以通过配置开启 - * `return tpInfo.selectOneMessageQueue(lastBrokerName)`:默认选择队列的方式,就是循环主题全部的队列 - - * `brokersSent[times] = mq.getBrokerName()`:将本次选择的 BrokerName 存入数组 + ```java + private final String storePath; + ``` - * `msg.setTopic(this.defaultMQProducer.withNamespace(msg.getTopic()))`:**重投的消息需要加上标记** +* 文件属性: - * `sendResult = this.sendKernelImpl`:核心发送方法 + ```java + private final int mappedFileSize; // 目录下每个文件大小,CL文件默认 1g,CQ文件 默认 600w字节 + private final CopyOnWriteArrayList mappedFiles; //目录下的每个 mappedFile 都加入该集合 + ``` - * `this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false)`:更新一下时间 +* 数据位点: - * `switch (communicationMode)`:异步或者单向消息直接返回 null,同步发送进入逻辑判断 + ```java + private long flushedWhere = 0; // 目录的刷盘位点,值为 mf.fileName + mf.wrotePosition + private long committedWhere = 0; // 目录的提交位点 + ``` - `if (sendResult.getSendStatus() != SendStatus.SEND_OK)`:**服务端 Broker 存储失败**,需要重试其他 Broker +* 消息存储: - * `throw new MQClientException()`:未找到当前主题的路由数据,无法发送消息,抛出异常 + ```java + private volatile long storeTimestamp = 0; // 当前目录下最后一条 msg 的存储时间 + ``` -* sendKernelImpl():**核心发送方法** +* 创建服务:新建 MappedFile 实例,继承自 ServiceThread 是一个任务对象,run 方法用来创建实例 ```java - //参数1:消息;参数2:选择的队列;参数3:发送模式(同步异步单向);参数4:回调函数,异步发送时需要;参数5:主题发布信息;参数6:剩余超时时间限制 - private SendResult sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout) + private final AllocateMappedFileService allocateMappedFileService; ``` - * `brokerAddr = this.mQClientFactory(...)`:获取指定 BrokerName 对应的 mater 节点的地址,master 节点的 ID 为 0 - * `brokerAddr = MixAll.brokerVIPChannel()`:Broker 启动时会绑定两个服务器端口,一个是普通端口,一个是 VIP 端口,服务器端根据不同端口创建不同的的 NioSocketChannel - * `byte[] prevBody = msg.getBody()`:获取消息体 +*** - * `if (!(msg instanceof MessageBatch))`:非批量消息,需要重新设置消息 ID - `MessageClientIDSetter.setUniqID(msg)`:msg id 由两部分组成,一部分是 ip 地址、进程号、ClassLoader 的 hashcode,另一部分是时间差(当前时间减去当月一号的时间)和计数器的值 - * `if (this.tryToCompressMessage(msg))`:判断消息是否压缩,压缩需要设置压缩标记 +##### 成员方法 - * `hasCheckForbiddenHook、hasSendMessageHook`:执行钩子方法 +核心方法: - * `requestHeader = new SendMessageRequestHeader()`:设置发送消息的消息头 +* load():Broker 启动时,加载本地磁盘数据,该方法读取 storePath 目录下的文件,创建 MappedFile 对象放入集合内 - * `if (requestHeader.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX))`:重投的发送消息 + ```java + public boolean load() + ``` - * `switch (communicationMode)`:异步发送一种处理方式,单向和同步同样的处理逻辑 +* getLastMappedFile():获取当前正在顺序写入的 MappedFile 对象,如果最后一个 MappedFile 写满了,或者不存在 MappedFile 对象,则创建新的 MappedFile - `sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage()`:**发送消息** + ```java + // 参数一:文件起始偏移量;参数二:当list为空时,是否新建 MappedFile + public MappedFile getLastMappedFile(final long startOffset, boolean needCreate) + ``` - * `request = RemotingCommand.createRequestCommand()`:创建一个 RequestCommand 对象 - * `request.setBody(msg.getBody())`:**将消息放入请求体** - * `switch (communicationMode)`:根据不同的模式 invoke 不同的方法 +* flush():根据 flushedWhere 属性查找合适的 MappedFile,调用该 MappedFile 的落盘方法,并更新全局的 flushedWhere -* request():请求方法,消费者回执消息,这种消息是异步消息 + ```java + //参数:0 表示强制刷新, > 0 脏页数据必须达到 flushLeastPages 才刷新 + public boolean flush(final int flushLeastPages) + ``` - * `requestResponseFuture = new RequestResponseFuture(correlationId, timeout, null)`:创建请求响应对象 +* findMappedFileByOffset():根据偏移量查询对象 - * `getRequestFutureTable().put(correlationId, requestResponseFuture)`:放入RequestFutureTable 映射表中 + ```java + public MappedFile findMappedFileByOffset(final long offset, final boolean returnFirstOnNotFound) + ``` - * `this.sendDefaultImpl(msg, CommunicationMode.ASYNC, new SendCallback())`:**发送异步消息** +* deleteExpiredFileByTime():CL 删除过期文件,根据文件的保留时长决定是否删除 - * `return waitResponse(msg, timeout, requestResponseFuture, cost)`:用来挂起请求的方法 + ```java + // 参数一:过期时间; 参数二:删除两个文件之间的时间间隔; 参数三:mf.destory传递的参数; 参数四:true 强制删除 + public int deleteExpiredFileByTime(final long expiredTime,final int deleteFilesInterval, final long intervalForcibly, final boolean cleanImmediately) + ``` - ```java - public Message waitResponseMessage(final long timeout) throws InterruptedException { - // 请求挂起 - this.countDownLatch.await(timeout, TimeUnit.MILLISECONDS); - return this.responseMsg; - } +* deleteExpiredFileByOffset():CQ 删除过期文件,遍历每个 MF 文件,获取当前文件最后一个数据单元的物理偏移量,小于 offset 说明当前 MF 文件内都是过期数据 + + ```java + // 参数一:consumeLog 目录下最小物理偏移量,就是第一条消息的 offset; + // 参数二:ConsumerQueue 文件内每个数据单元固定大小 + public int deleteExpiredFileByOffset(long offset, int unitSize) + ``` - * 当消息被消费后,会获取消息的关联 ID,从映射表中获取消息的 RequestResponseFuture,执行下面的方法唤醒挂起线程 - ```java - public void putResponseMessage(final Message responseMsg) { - this.responseMsg = responseMsg; - this.countDownLatch.countDown(); - } - ``` @@ -6054,276 +6036,229 @@ DefaultMQProducerImpl 类是默认的生产者实现类 -#### 路由信息 +#### CommitLog -TopicPublishInfo 类用来存储路由信息 +##### 成员属性 成员变量: -* 顺序消息: +* 魔数: ```java - private boolean orderTopic = false; + public final static int MESSAGE_MAGIC_CODE = -626843481; // 消息的第一个字段是大小,第二个字段就是魔数 + protected final static int BLANK_MAGIC_CODE = -875286124; // 文件尾消息的魔法值 ``` -* 消息队列: +* MappedFileQueue:用于管理 `../store/commitlog` 目录下的文件 ```java - private List messageQueueList = new ArrayList<>(); // 主题全部的消息队列 - private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex(); // 消息队列索引 + protected final MappedFileQueue mappedFileQueue; ``` +* 存储服务: + ```java - // 【消息队列类】 - public class MessageQueue implements Comparable, Serializable { - private String topic; - private String brokerName; - private int queueId;// 队列 ID - } + protected final DefaultMessageStore defaultMessageStore; // 存储模块对象,上层服务 + private final FlushCommitLogService flushCommitLogService; // 刷盘服务,默认实现是异步刷盘 ``` -* 路由数据:主题对应的路由数据 +* 回调器:控制消息的哪些字段添加到 MappedFile ```java - private TopicRouteData topicRouteData; + private final AppendMessageCallback appendMessageCallback; ``` +* 队列偏移量字典表:key 是主题队列 id,value 是偏移量 + ```java - public class TopicRouteData extends RemotingSerializable { - private String orderTopicConf; - private List queueDatas; // 队列数据 - private List brokerDatas; // Broker 数据 - private HashMap/* Filter Server */> filterServerTable; - } + protected HashMap topicQueueTable = new HashMap(1024); ``` +* 锁相关: + ```java - public class QueueData implements Comparable { - private String brokerName; // 节点名称 - private int readQueueNums; // 读队列数 - private int writeQueueNums; // 写队列数 - private int perm; // 权限 - private int topicSynFlag; - } + private volatile long beginTimeInLock = 0; // 写数据时加锁的开始时间 + protected final PutMessageLock putMessageLock; // 写锁,两个实现类:自旋锁和重入锁 ``` +构造方法: + +* 有参构造: + ```java - public class BrokerData implements Comparable { - private String cluster; // 集群名 - private String brokerName; // Broker节点名称 - private HashMap brokerAddrs; + public CommitLog(final DefaultMessageStore defaultMessageStore) { + // 创建 MappedFileQueue 对象 + // 参数1:../store/commitlog; 参数2:【1g】; 参数3:allocateMappedFileService + this.mappedFileQueue = new MappedFileQueue(...); + // 默认 异步刷盘,创建这个对象 + this.flushCommitLogService = new FlushRealTimeService(); + // 控制消息哪些字段追加到 mappedFile,【消息最大是 4M】 + this.appendMessageCallback = new DefaultAppendMessageCallback(...); + // 默认使用自旋锁 + this.putMessageLock = ...; } ``` -核心方法: + + +*** + -* selectOneMessageQueue():**选择消息队列**使用 + +##### 成员方法 + +CommitLog 类核心方法: + +* start():会启动刷盘服务 ```java - // 参数是上次失败时的 brokerName,可以为 null - public MessageQueue selectOneMessageQueue(final String lastBrokerName) { - if (lastBrokerName == null) { - return selectOneMessageQueue(); - } else { - // 遍历消息队列 - for (int i = 0; i < this.messageQueueList.size(); i++) { - // 获取队列的索引 - int index = this.sendWhichQueue.getAndIncrement(); - // 获取队列的下标位置 - int pos = Math.abs(index) % this.messageQueueList.size(); - if (pos < 0) - pos = 0; - // 获取消息队列 - MessageQueue mq = this.messageQueueList.get(pos); - // 与上次选择的不同就可以返回 - if (!mq.getBrokerName().equals(lastBrokerName)) { - return mq; - } - } - return selectOneMessageQueue(); - } - } + public void start() ``` - +* shutdown():关闭刷盘服务 + ```java + public void shutdown() + ``` +* load():加载 CommitLog 目录下的文件 -**** + ```java + public boolean load() + ``` +* getMessage():根据 offset 查询单条信息,返回的结果对象内部封装了一个 ByteBuffer,该 Buffer 表示 `[offset, offset + size]` 区间的 MappedFile 的数据 + ```java + public SelectMappedBufferResult getMessage(final long offset, final int size) + ``` -### 客户端 +* deleteExpiredFile():删除过期文件,方法由 DefaultMessageStore 的定时任务调用 -#### 实例对象 + ```java + public int deleteExpiredFile() + ``` -##### 成员属性 +* asyncPutMessage():**存储消息** -MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一个客户端实例,**既服务于生产者,也服务于消费者** + ```java + public CompletableFuture asyncPutMessage(final MessageExtBrokerInner msg) + ``` -成员变量: + * `msg.setStoreTimestamp(System.currentTimeMillis())`:设置存储时间,后面获取到写锁后这个事件会重写 + * `String topic = msg.getTopic()`:获取主题和队列 ID + * `mappedFile = this.mappedFileQueue.getLastMappedFile()`:获取当前顺序写的 MappedFile 对象 -* 配置信息: + * `putMessageLock.lock()`:获取**写锁** + * `msg.setStoreTimestamp(beginLockTimestamp)`:设置消息的存储时间为获取锁的时间 + * `if (null == mappedFile || mappedFile.isFull())`:文件写满了创建新的 MF 对象 + * `result = mappedFile.appendMessage(msg, this.appendMessageCallback)`:**消息追加**,核心逻辑在回调器类 + * `putMessageLock.unlock()`:释放写锁 + * `this.defaultMessageStore.unlockMappedFile(..)`:将 MappedByteBuffer 从 lock 切换为 unlock 状态 + * `putMessageResult = new PutMessageResult(PutMessageStatus.PUT_OK, result)`:结果封装 + * `flushResultFuture = submitFlushRequest(result, msg)`:**唤醒刷盘线程** + * `replicaResultFuture = submitReplicaRequest(result, msg)`:HA 消息同步 + +* recoverNormally():正常关机时的恢复方法,存储模块启动时**先恢复所有的 ConsumeQueue 数据,再恢复 CommitLog 数据** ```java - private final int instanceIndex; // 索引一般是 0,因为客户端实例一般都是一个进程只有一个 - private final String clientId; // 客户端 ID ip@pid - private final long bootTimestamp; // 客户端的启动时间 - private ServiceState serviceState; // 客户端状态 + // 参数表示恢复阶段 ConsumeQueue 中已知的最大的消息 offset + public void recoverNormally(long maxPhyOffsetOfConsumeQueue) ``` -* 生产者消费者的映射表:key 是组名 + * `int index = mappedFiles.size() - 3`:从倒数第三个 file 开始向后恢复 - ```java - private final ConcurrentMap producerTable - private final ConcurrentMap consumerTable - private final ConcurrentMap adminExtTable - ``` + * `dispatchRequest = this.checkMessageAndReturnSize()`:每次从切片内解析出一条 msg 封装成 DispatchRequest 对象 -* 网络层配置: + * `size = dispatchRequest.getMsgSize()`:获取消息的大小,检查 DispatchRequest 对象的状态 - ```java - private final NettyClientConfig nettyClientConfig; - ``` + 情况 1:正常数据,则 `mappedFileOffset += size` -* 核心功能的实现:负责将 MQ 业务层的数据转换为网络层的 RemotingCommand 对象,使用内部持有的 NettyRemotingClient 对象的 invoke 系列方法,完成网络 IO(同步、异步、单向) + 情况 2:文件尾数据,处理下一个文件,mappedFileOffset 置为 0,magic_code 表示文件尾 - ```java - private final MQClientAPIImpl mQClientAPIImpl; - ``` + * `processOffset += mappedFileOffset`:计算出正确的数据存储位点,并设置 MappedFileQueue 的目录刷盘位点 -* 本地路由数据:key 是主题名称,value 路由信息 + * `this.mappedFileQueue.truncateDirtyFiles(processOffset)`:调整 MFQ 中文件的刷盘位点 - ```java - private final ConcurrentMap topicRouteTable = new ConcurrentHashMap<>(); + * `if (maxPhyOffsetOfConsumeQueue >= processOffset)`:删除冗余数据,将超过全局位点的 CQ 下的文件删除,将包含全局位点的 CQ 下的文件重新定位 -* 锁信息:两把锁,锁不同的数据 +* recoverAbnormally():异常关机时的恢复方法 ```java - private final Lock lockNamesrv = new ReentrantLock(); - private final Lock lockHeartbeat = new ReentrantLock(); + public void recoverAbnormally(long maxPhyOffsetOfConsumeQueue) ``` -* 调度线程池:单线程,执行定时任务 + * `int index = mappedFiles.size() - 1`:从尾部开始遍历 MFQ,验证 MF 的第一条消息,找到第一个验证通过的文件对象 + * `dispatchRequest = this.checkMessageAndReturnSize()`:每次解析出一条 msg 封装成 DispatchRequest 对象 + * `this.defaultMessageStore.doDispatch(dispatchRequest)`:重建 ConsumerQueue 和 Index,避免上次异常停机导致 CQ 和 Index 与 CommitLog 不对齐 + * 剩余逻辑与正常关机的恢复方法相似 - ```java - private final ScheduledExecutorService scheduledExecutorService; - ``` +消息追加服务 DefaultAppendMessageCallback -* Broker 映射表:key 是 BrokerName +* doAppend() + * `long wroteOffset = fileFromOffset + byteBuffer.position()`:消息写入的位置,物理偏移量 phyOffset + * `String msgId`:消息 ID,规则是客户端 IP + 消息偏移量 phyOffset + * `byte[] topicData`:序列化消息,将消息的字段压入到 msgStoreItemMemory 这个 Buffer 中 + * `byteBuffer.put(this.msgStoreItemMemory.array(), 0, msgLen)`:将 msgStoreItemMemory 中的数据写入 MF 对象的内存映射的 Buffer 中,数据还没落盘 + * `AppendMessageResult result`:构造结果对象,包括存储位点、是否成功、队列偏移量等信息 + * `CommitLog.this.topicQueueTable.put(key, ++queueOffset)`:更新队列偏移量 - ```java - // 物理节点映射表,value:Long 是 brokerID,【ID=0 的是主节点,其他是从节点】,String 是地址 ip:port - private final ConcurrentMap> brokerAddrTable; - // 物理节点版本映射表,String 是地址 ip:port,Integer 是版本 - ConcurrentMap> brokerVersionTable; - ``` -* **客户端的协议处理器**:用于处理 IO 事件 - ```java - private final ClientRemotingProcessor clientRemotingProcessor; - ``` +**** -* 消息服务: - ```java - private final PullMessageService pullMessageService; // 拉消息服务 - private final RebalanceService rebalanceService; // 消费者负载均衡服务 - private final ConsumerStatsManager consumerStatsManager; // 消费者状态管理 - ``` -* 内部生产者实例:处理消费端**消息回退**,用该生产者发送回执消息 +#### ConsQueue + +##### 成员属性 + +ConsumerQueue 是消息消费队列,存储消息在 CommitLog 的索引,便于快速定位消息 + +成员变量: + +* 数据单元:ConsumerQueueData 数据单元的固定大小是 20 字节,默认申请 20 字节的缓冲区 ```java - private final DefaultMQProducer defaultMQProducer; + public static final int CQ_STORE_UNIT_SIZE = 20; ``` -* 心跳次数统计: +* 文件管理: ```java - private final AtomicLong sendHeartbeatTimesTotal = new AtomicLong(0) + private final MappedFileQueue mappedFileQueue; // 文件管理器,管理 CQ 目录下的文件 + private final String storePath; // 目录,比如../store/consumequeue/xxx_topic/0 + private final int mappedFileSize; // 每一个 CCQ 存储文件大小,默认 20 * 30w = 600w byte ``` -* 公共配置类: +* 存储主模块:上层的对象 ```java - public class ClientConfig { - // Namesrv 地址配置 - private String namesrvAddr = NameServerAddressUtils.getNameServerAddresses(); - // 客户端的 IP 地址 - private String clientIP = RemotingUtil.getLocalAddress(); - // 客户端实例名称 - private String instanceName = System.getProperty("rocketmq.client.name", "DEFAULT"); - // 客户端回调线程池的数量,平台核心数,8核16线程的电脑返回16 - private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); - // 命名空间 - protected String namespace; - protected AccessChannel accessChannel = AccessChannel.LOCAL; - - // 获取路由信息的间隔时间 30s - private int pollNameServerInterval = 1000 * 30; - // 客户端与 broker 之间的心跳周期 30s - private int heartbeatBrokerInterval = 1000 * 30; - // 消费者持久化消费的周期 5s - private int persistConsumerOffsetInterval = 1000 * 5; - private long pullTimeDelayMillsWhenException = 1000; - private boolean unitMode = false; - private String unitName; - // vip 通道,broker 启动时绑定两个端口,其中一个是 vip 通道 - private boolean vipChannelEnabled = Boolean.parseBoolean(); - // 语言,默认是 Java - private LanguageCode language = LanguageCode.JAVA; - } + private final DefaultMessageStore defaultMessageStore; ``` -构造方法: - -* MQClientInstance 有参构造: +* 消息属性: ```java - public MQClientInstance(ClientConfig clientConfig, int instanceIndex, String clientId, RPCHook rpcHook) { - this.clientConfig = clientConfig; - this.instanceIndex = instanceIndex; - // Netty 相关的配置信息 - this.nettyClientConfig = new NettyClientConfig(); - // 平台核心数 - this.nettyClientConfig.setClientCallbackExecutorThreads(...); - this.nettyClientConfig.setUseTLS(clientConfig.isUseTLS()); - // 【创建客户端协议处理器】 - this.clientRemotingProcessor = new ClientRemotingProcessor(this); - // 创建 API 实现对象 - // 参数一:客户端网络配置 - // 参数二:客户端协议处理器,注册到客户端网络层 - // 参数三:rpcHook,注册到客户端网络层 - // 参数四:客户端配置 - this.mQClientAPIImpl = new MQClientAPIImpl(this.nettyClientConfig, this.clientRemotingProcessor, rpcHook, clientConfig); - - //... - // 内部生产者,指定内部生产者的组 - this.defaultMQProducer = new DefaultMQProducer(MixAll.CLIENT_INNER_PRODUCER_GROUP); - } + private final String topic; // CQ 主题 + private final int queueId; // CQ 队列,每一个队列都有一个 ConsumeQueue 对象进行管理 + private final ByteBuffer byteBufferIndex; // 临时缓冲区,插新的 CQData 时使用 + private long maxPhysicOffset = -1; // 当前ConsumeQueue内存储的最大消息物理偏移量 + private volatile long minLogicOffset = 0; // 当前ConsumeQueue内存储的最小消息物理偏移量 ``` -* MQClientAPIImpl 有参构造: +构造方法: + +* 有参构造: ```java - public MQClientAPIImpl(nettyClientConfig, clientRemotingProcessor, rpcHook, clientConfig) { - this.clientConfig = clientConfig; - topAddressing = new TopAddressing(MixAll.getWSAddr(), clientConfig.getUnitName()); - // 创建网络层对象,参数二为 null 说明客户端并不关心 channel event - this.remotingClient = new NettyRemotingClient(nettyClientConfig, null); - // 业务处理器 - this.clientRemotingProcessor = clientRemotingProcessor; - // 注册 RpcHook - this.remotingClient.registerRPCHook(rpcHook); - // ... - // 注册回退消息的请求码 - this.remotingClient.registerProcessor(RequestCode.PUSH_REPLY_MESSAGE_TO_CLIENT, this.clientRemotingProcessor, null); + public ConsumeQueue() { + // 申请了一个 20 字节大小的 临时缓冲区 + this.byteBufferIndex = ByteBuffer.allocate(CQ_STORE_UNIT_SIZE); } ``` - + *** @@ -6331,340 +6266,281 @@ MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一 ##### 成员方法 -* start():启动方法 +ConsumeQueue 启动阶段方法: - * `synchronized (this)`:加锁保证线程安全,保证只有一个实例对象启动 - * `this.mQClientAPIImpl.start()`:启动客户端网络层,底层调用 RemotingClient 类 - * `this.startScheduledTask()`:启动定时任务 - * `this.pullMessageService.start()`:启动拉取消息服务 - * `this.rebalanceService.start()`:启动负载均衡服务 - * `this.defaultMQProducer.getDefaultMQProducerImpl().start(false)`:启动内部生产者,参数为 false 代表不启动实例 +* load():第一步,加载 storePath 目录下的文件,初始化 MappedFileQueue +* recover():第二步,恢复 ConsumeQueue 数据 + * 从倒数第三个 MF 文件开始向后遍历,依次读取 MF 中 20 个字节的 CQData 数据,检查 offset 和 size 是否是有效数据 + * 找到无效的 CQData 的位点,该位点就是 CQ 的刷盘点和数据顺序写入点 + * 删除无效的 MF 文件,调整当前顺序写的 MF 文件的数据位点 -* startScheduledTask():**启动定时任务**,调度线程池是单线程 +其他方法: - * `if (null == this.clientConfig.getNamesrvAddr())`:Namesrv 地址是空,需要两分钟拉取一次 Namesrv 地址 +* truncateDirtyLogicFiles():CommitLog 恢复阶段调用,将 ConsumeQueue 有效数据文件与 CommitLog 对齐,将超出部分的数据文删除掉,并调整当前文件的数据位点。Broker 启动阶段先恢复 CQ 的数据,再恢复 CL 数据,但是**数据要以 CL 为基准** - * 定时任务 1:从 Namesrv 更新客户端本地的路由数据,周期 30 秒一次 + ```java + // 参数是最大消息物理偏移量 + public void truncateDirtyLogicFiles(long phyOffet) + ``` - ```java - // 获取生产者和消费者订阅的主题集合,遍历集合,对比从 namesrv 拉取最新的主题路由数据和本地数据,是否需要更新 - MQClientInstance.this.updateTopicRouteInfoFromNameServer(); - ``` +* flush():刷盘,调用 MFQ 的刷盘方法 - * 定时任务 2:周期 30 秒一次,两个任务 + ```java + public boolean flush(final int flushLeastPages) + ``` - * 清理下线的 Broker 节点,遍历客户端的 Broker 物理节点映射表,将所有主题数据都不包含的 Broker 物理节点清理掉,如果被清理的 Broker 下所有的物理节点都没有了,就将该 Broker 的映射数据删除掉 - * 向在线的所有的 Broker 发送心跳数据,**同步发送的方式**,返回值是 Broker 物理节点的版本号,更新版本映射表 +* deleteExpiredFile():删除过期文件,将小于 offset 的所有 MF 文件删除,offset 是 CommitLog 目录下最小的物理偏移量,小于该值的 CL 文件已经没有了,所以 CQ 也没有存在的必要 - ```java - MQClientInstance.this.cleanOfflineBroker(); - MQClientInstance.this.sendHeartbeatToAllBrokerWithLock(); - ``` + ```java + public int deleteExpiredFile(long offset) + ``` - ```java - // 心跳数据 - public class HeartbeatData extends RemotingSerializable { - // 客户端 ID ip@pid - private String clientID; - // 存储客户端所有生产者数据 - private Set producerDataSet = new HashSet(); - // 存储客户端所有消费者数据 - private Set consumerDataSet = new HashSet(); - } - ``` +* putMessagePositionInfoWrapper():**向 CQ 中追加 CQData 数据**,由存储主模块 DefaultMessageStore 内部的异步线程调用,负责构建 ConsumeQueue 文件和 Index 文件的,该线程会持续关注 CommitLog 文件,当 CommitLog 文件内有新数据写入,就读出来封装成 DispatchRequest 对象,转发给 ConsumeQueue 或者 IndexService - * 定时任务 3:消费者持久化消费数据,周期 5 秒一次 + ```java + public void putMessagePositionInfoWrapper(DispatchRequest request) + ``` - ```java - MQClientInstance.this.persistAllConsumerOffset(); - ``` +* getIndexBuffer():转换 startIndex 为 offset,获取包含该 offset 的 MappedFile 文件,读取 `[offset%maxSize, mfPos]` 范围的数据,包装成结果对象返回 - * 定时任务 4:动态调整消费者线程池,周期 1 分钟一次 + ```java + public SelectMappedBufferResult getIndexBuffer(final long startIndex) + ``` - ```java - MQClientInstance.this.adjustThreadPool(); - ``` -* updateTopicRouteInfoFromNameServer():**更新路由数据** - * `if (isDefault && defaultMQProducer != null)`:需要默认数据 +**** - `topicRouteData = ...getDefaultTopicRouteInfoFromNameServer()`:从 Namesrv 获取默认的 TBW102 的路由数据 - `int queueNums`:遍历所有队列,为每个读写队列设置较小的队列数 - * `topicRouteData = ...getTopicRouteInfoFromNameServer(topic)`:需要**从 Namesrv 获取**路由数据(同步) +#### IndexFile - * `old = this.topicRouteTable.get(topic)`:获取客户端实例本地的该主题的路由数据 +##### 索引机制 - * `boolean changed = topicRouteDataIsChange(old, topicRouteData)`:对比本地和最新下拉的数据是否一致 +RocketMQ 的索引文件逻辑结构,类似 JDK 中 HashMap 的实现,具体结构如下: - * `if (changed)`:不一致进入更新逻辑 +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-IndexFile索引文件.png) - `cloneTopicRouteData = topicRouteData.cloneTopicRouteData()`:克隆一份最新数据 +IndexFile 文件的存储在 `$HOME\store\index${fileName}`,文件名 fileName 是以创建时的时间戳命名,文件大小是固定的,等于 `40+500W*4+2000W*20= 420000040` 个字节大小。如果消息的 properties 中设置了 UNIQ_KEY 这个属性,就用 `topic + “#” + UNIQ_KEY` 作为 key 来做写入操作;如果消息设置了 KEYS 属性(多个 KEY 以空格分隔),也会用 `topic + “#” + KEY` 来做索引 - `Update Pub info`:更新生产者信息 +整个 Index File 的结构如图,40 Byte 的 Header 用于保存一些总的统计信息,`4*500W` 的 Slot Table 并不保存真正的索引数据,而是保存每个槽位对应的单向链表的**头指针**,即一个 Index File 可以保存 2000W 个索引,`20*2000W` 是**真正的索引数据** - * `publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData)`:**将主题路由数据转化为发布数据** - * `impl.updateTopicPublishInfo(topic, publishInfo)`:生产者将主题的发布数据保存到它本地,方便发送消息使用 +索引数据包含了 Key Hash/CommitLog Offset/Timestamp/NextIndex offset 这四个字段,一共 20 Byte - `Update sub info`:更新消费者信息 +* NextIndex offset 即前面读出来的 slotValue,如果有 hash 冲突,就可以用这个字段将所有冲突的索引用链表的方式串起来 +* Timestamp 记录的是消息 storeTimestamp 之间的差,并不是一个绝对的时间 - `this.topicRouteTable.put(topic, cloneTopicRouteData)`:将数据放入本地路由表 +参考文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md -**** +*** -#### 网络通信 -##### 成员属性 -NettyRemotingClient 类负责客户端的网络通信 +##### 成员属性 -成员变量: +IndexFile 类成员属性 -* Netty 服务相关属性: +* 哈希: ```java - private final NettyClientConfig nettyClientConfig; // 客户端的网络层配置 - private final Bootstrap bootstrap = new Bootstrap(); // 客户端网络层启动对象 - private final EventLoopGroup eventLoopGroupWorker; // 客户端网络层 Netty IO 线程组 + private static int hashSlotSize = 4; // 每个 hash 桶的大小是 4 字节,【用来存放索引的编号】 + private final int hashSlotNum; // hash 桶的个数,默认 500 万 ``` -* Channel 映射表: +* 索引: ```java - private final ConcurrentMap channelTables;// key 是服务器的地址,value 是通道对象 - private final Lock lockChannelTables = new ReentrantLock(); // 锁,控制并发安全 + private static int indexSize = 20; // 每个 index 条目的大小是 20 字节 + private static int invalidIndex = 0; // 无效索引编号:0 特殊值 + private final int indexNum; // 默认值:2000w + private final IndexHeader indexHeader; // 索引头 ``` -* 定时器:启动定时任务 +* 映射: ```java - private final Timer timer = new Timer("ClientHouseKeepingService", true) + private final MappedFile mappedFile; // 【索引文件使用的 MF 文件】 + private final FileChannel fileChannel; // 文件通道 + private final MappedByteBuffer mappedByteBuffer;// 从 MF 中获取的内存映射缓冲区 ``` -* 线程池: +构造方法: + +* 有参构造 ```java - private ExecutorService publicExecutor; // 公共线程池 - private ExecutorService callbackExecutor; // 回调线程池,客户端发起异步请求,服务器的响应数据由回调线程池处理 + // endPhyOffset 上个索引文件 最后一条消息的 物理偏移量 + // endTimestamp 上个索引文件 最后一条消息的 存储时间 + public IndexFile(final String fileName, final int hashSlotNum, final int indexNum, + final long endPhyOffset, final long endTimestamp) throws IOException { + // 文件大小 40 + 500w * 4 + 2000w * 20 + int fileTotalSize = + IndexHeader.INDEX_HEADER_SIZE + (hashSlotNum * hashSlotSize) + (indexNum * indexSize); + // 创建 mf 对象,会在disk上创建文件 + this.mappedFile = new MappedFile(fileName, fileTotalSize); + // 创建 索引头对象,传递 索引文件mf 的切片数据 + this.indexHeader = new IndexHeader(byteBuffer); + //... + } ``` -* 事件监听器:客户端这里是 null - ```java - private final ChannelEventListener channelEventListener; - ``` -* Netty 配置对象: +**** - ```java - public class NettyClientConfig { - // 客户端工作线程数 - private int clientWorkerThreads = 4; - // 回调处理线程池 线程数:平台核心数 - private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); - // 单向请求并发数,默认 65535 - private int clientOnewaySemaphoreValue = NettySystemConfig.CLIENT_ONEWAY_SEMAPHORE_VALUE; - // 异步请求并发数,默认 65535 - private int clientAsyncSemaphoreValue = NettySystemConfig.CLIENT_ASYNC_SEMAPHORE_VALUE; - // 客户端连接服务器的超时时间限制 3秒 - private int connectTimeoutMillis = 3000; - // 客户端未激活周期,60s(指定时间内 ch 未激活,需要关闭) - private long channelNotActiveInterval = 1000 * 60; - // 客户端与服务器 ch 最大空闲时间 2分钟 - private int clientChannelMaxIdleTimeSeconds = 120; - - // 底层 Socket 写和收 缓冲区的大小 65535 64k - private int clientSocketSndBufSize = NettySystemConfig.socketSndbufSize; - private int clientSocketRcvBufSize = NettySystemConfig.socketRcvbufSize; - // 客户端 netty 是否启动内存池 - private boolean clientPooledByteBufAllocatorEnable = false; - // 客户端是否超时关闭 Socket 连接 - private boolean clientCloseSocketIfTimeout = false; - } - ``` -构造方法 -* 无参构造: +##### 成员方法 - ```java - public NettyRemotingClient(final NettyClientConfig nettyClientConfig) { - this(nettyClientConfig, null); - } - ``` +IndexFile 类方法 -* 有参构造: +* load():加载 IndexHeader ```java - public NettyRemotingClient(nettyClientConfig, channelEventListener) { - // 父类创建了2个信号量,1、控制单向请求的并发度,2、控制异步请求的并发度 - super(nettyClientConfig.getClientOnewaySemaphoreValue(), nettyClientConfig.getClientAsyncSemaphoreValue()); - this.nettyClientConfig = nettyClientConfig; - this.channelEventListener = channelEventListener; - - // 创建公共线程池 - int publicThreadNums = nettyClientConfig.getClientCallbackExecutorThreads(); - if (publicThreadNums <= 0) { - publicThreadNums = 4; - } - this.publicExecutor = Executors.newFixedThreadPool(publicThreadNums,); - - // 创建 Netty IO 线程,1个线程 - this.eventLoopGroupWorker = new NioEventLoopGroup(1, ); - - if (nettyClientConfig.isUseTLS()) { - sslContext = TlsHelper.buildSslContext(true); - } - } + public void load() ``` +* flush():MappedByteBuffer 内的数据强制落盘 + ```java + public void flush() + ``` -**** +* isWriteFull():检查当前的 IndexFile 已写索引数是否 >= indexNum,达到该值则当前 IndexFile 不能继续追加 IndexData 了 + ```java + public boolean isWriteFull() + ``` +* destroy():删除文件时使用的方法 -##### 成员方法 + ```java + public boolean destroy(final long intervalForcibly) + ``` -* start():启动方法 +* putKey():添加索引数据,解决哈希冲突使用**头插法** ```java - public void start() { - // channel pipeline 内的 handler 使用的线程资源,默认 4 个 - this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(); - // 配置 netty 客户端启动类对象 - Bootstrap handler = this.bootstrap.group(this.eventLoopGroupWorker).channel(NioSocketChannel.class) - //... - .handler(new ChannelInitializer() { - @Override - public void initChannel(SocketChannel ch) throws Exception { - ChannelPipeline pipeline = ch.pipeline(); - // 加几个handler - pipeline.addLast( - // 服务端的数据,都会来到这个 - new NettyClientHandler()); - } - }); - // 注意 Bootstrap 只是配置好客户端的元数据了,【在这里并没有创建任何 channel 对象】 - // 定时任务 扫描 responseTable 中超时的 ResponseFuture,避免客户端线程长时间阻塞 - this.timer.scheduleAtFixedRate(() -> { - NettyRemotingClient.this.scanResponseTable(); - }, 1000 * 3, 1000); - // 这里是 null,不启动 - if (this.channelEventListener != null) { - this.nettyEventExecutor.start(); - } - } + // 参数一:消息的 key,uniq_key 或者 keys="aaa bbb ccc" 会分别为 aaa bbb ccc 创建索引 + // 参数二:消息的物理偏移量; 参数三:消息存储时间 + public boolean putKey(final String key, final long phyOffset, final long storeTimestamp) ``` -* 单向通信: + * `int slotPos = keyHash % this.hashSlotNum`:对 key 计算哈希后,取模得到对应的哈希槽 slot 下标,然后计算出哈希槽的存储位置 absSlotPos + * `int slotValue = this.mappedByteBuffer.getInt(absSlotPos)`:获取槽中的值,如果是无效值说明没有哈希冲突 + * `timeDiff = timeDiff / 1000`:计算当前 msg 存储时间减去索引文件内第一条消息存储时间的差值,转化为秒进行存储 + * `int absIndexPos`:计算当前索引数据存储的位置,开始填充索引数据到对应的位置 + * `this.mappedByteBuffer.putInt(absSlotPos, this.indexHeader.getIndexCount())`:在 slot 放入当前索引的索引编号 + * `if (this.indexHeader.getIndexCount() <= 1)`:索引文件插入的第一条数据,需要设置起始偏移量和存储时间 + * `if (invalidIndex == slotValue)`:没有哈希冲突,说明占用了一个新的 hash slot + * `this.indexHeader`:设置索引头的相关属性 + +* selectPhyOffset():从索引文件查询消息的物理偏移量 ```java - public RemotingCommand invokeSync(String addr, final RemotingCommand request, long timeoutMillis) { - // 开始时间 - long beginStartTime = System.currentTimeMillis(); - // 获取或者创建客户端与服务端(addr)的通道 channel - final Channel channel = this.getAndCreateChannel(addr); - // 条件成立说明客户端与服务端 channel 通道正常,可以通信 - if (channel != null && channel.isActive()) { - try { - // 执行 rpcHook 拓展点 - doBeforeRpcHooks(addr, request); - // 计算耗时,如果当前耗时已经超过 timeoutMillis 限制,则直接抛出异常,不再进行系统通信 - long costTime = System.currentTimeMillis() - beginStartTime; - if (timeoutMillis < costTime) { - throw new RemotingTimeoutException("invokeSync call timeout"); - } - // 参数1:客户端-服务端通道channel - // 参数二:网络层传输对象,封装着请求数据 - // 参数三:剩余的超时限制 - RemotingCommand response = this.invokeSyncImpl(channel, request, ...); - // 后置处理 - doAfterRpcHooks(RemotingHelper.parseChannelRemoteAddr(channel), request, response); - // 返回响应数据 - return response; - } catch (RemotingSendRequestException e) {} - } else { - this.closeChannel(addr, channel); - throw new RemotingConnectException(addr); - } - } + // 参数一:查询结果全部放到该list内; 参数二:查询key; 参数三:结果最大数限制; 参数四五:时间范围 + public void selectPhyOffset(final List phyOffsets, final String key, final int maxNum,final long begin, final long end, boolean lock) ``` - + * `if (this.mappedFile.hold())`: MF 的引用记数 +1,查询期间 MF 资源**不能被释放** + * `int slotValue = this.mappedByteBuffer.getInt(absSlotPos)`:获取槽中的值,可能是无效值或者索引编号,如果是无效值说明查询未命中 + * `int absIndexPos`:计算出索引编号对应索引数据的开始位点 + * `this.mappedByteBuffer`:读取索引数据 + * `long timeRead = this.indexHeader.getBeginTimestamp() + timeDiff`:计算出准确的存储时间 + * `boolean timeMatched = (timeRead >= begin) && (timeRead <= end)`:时间范围的匹配 + * `phyOffsets.add(phyOffsetRead)`:将命中的消息索引的消息偏移量加入到 list 集合中 + * `nextIndexToRead = prevIndexRead`:遍历前驱节点 -*** +**** -### 存储端 -#### 存储机制 -##### 存储结构 +#### IndexServ -RocketMQ 中 Broker 负责存储消息转发消息,所以以下的结构是存储在 Broker Server 上的,生产者和消费者与 Broker 进行消息的收发是通过主题对应的 Message Queue 完成,类似于通道 +##### 成员属性 -RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成 的,CommitLog 是消息真正的**物理存储**文件,ConsumeQueue 是消息的逻辑队列,类似数据库的**索引节点**,存储的是指向物理存储的地址。**每个 Topic 下的每个 Message Queue 都有一个对应的 ConsumeQueue 文件** +IndexService 类用来管理 IndexFile 文件 -每条消息都会有对应的索引信息,Consumer 通过 ConsumeQueue 这个结构来读取消息实体内容 +成员变量: -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息存储结构.png) +* 存储主模块: -* CommitLog:消息主体以及元数据的存储主体,存储 Producer 端写入的消息内容,消息内容不是定长的。消息主要是顺序写入日志文件,单个文件大小默认 1G,偏移量代表下一次写入的位置,当文件写满了就继续写入下一个文件 -* ConsumerQueue:消息消费队列,存储消息在 CommitLog 的索引。RocketMQ 消息消费时要遍历 CommitLog 文件,并根据主题 Topic 检索消息,这是非常低效的。引入 ConsumeQueue 作为消费消息的索引,保存了指定 Topic 下的队列消息在 CommitLog 中的起始物理偏移量 offset,消息大小 size 和消息 Tag 的 HashCode 值,每个 ConsumeQueue 文件大小约 5.72M -* IndexFile:为了消息查询提供了一种通过 Key 或时间区间来查询消息的方法,通过 IndexFile 来查找消息的方法不影响发送与消费消息的主流程。IndexFile 的底层存储为在文件系统中实现的 HashMap 结构,故 RocketMQ 的索引文件其底层实现为 hash 索引 + ```java + private final DefaultMessageStore defaultMessageStore; + ``` -RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所有的队列共用一个日志数据文件(CommitLog)来存储。混合型存储结构(多个 Topic 的消息实体内容都存储于一个 CommitLog 中)**针对 Producer 和 Consumer 分别采用了数据和索引部分相分离的存储结构**,Producer 发送消息至 Broker 端,然后 Broker 端使用同步或者异步的方式对消息刷盘持久化,保存至 CommitLog 中。只要消息被持久化至磁盘文件 CommitLog 中,Producer 发送的消息就不会丢失,Consumer 也就肯定有机会去消费这条消息 +* 索引文件存储目录:`../store/index` -服务端支持长轮询模式,当消费者无法拉取到消息后,可以等下一次消息拉取,Broker 允许等待 30s 的时间,只要这段时间内有新消息到达,将直接返回给消费端。RocketMQ 的具体做法是,使用 Broker 端的后台服务线程 ReputMessageService 不停地分发请求并异步构建 ConsumeQueue(逻辑消费队列)和 IndexFile(索引文件)数据 + ```java + private final String storePath; + ``` +* 索引对象集合:目录下的每个文件都有一个 IndexFile 对象 + ```java + private final ArrayList indexFileList = new ArrayList(); + ``` -**** +* 索引文件: + ```java + private final int hashSlotNum; // 每个索引文件包含的 哈希桶数量 :500w + private final int indexNum; // 每个索引文件包含的 索引条目数量 :2000w + ``` -##### 存储优化 -###### 内存映射 +*** -操作系统分为用户态和内核态,文件操作、网络操作需要涉及这两种形态的切换,需要进行数据复制。一台服务器把本机磁盘文件的内容发送到客户端,分为两个步骤: -* read:读取本地文件内容 -* write:将读取的内容通过网络发送出去 +##### 成员方法 -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-文件与网络操作.png) +* load():加载 storePath 目录下的文件,为每个文件创建一个 IndexFile 实例对象,并加载 IndexHeader 信息 -补充:Prog → NET → I/O → 零拷贝部分的笔记详解相关内容 + ```java + public boolean load(final boolean lastExitOK) + ``` -通过使用 mmap 的方式,可以省去向用户态的内存复制,RocketMQ 充分利用**零拷贝技术**,提高消息存盘和网络发送的速度。 +* deleteExpiredFile():删除过期索引文件 -RocketMQ 通过 MappedByteBuffer 对文件进行读写操作,利用了 NIO 中的 FileChannel 模型将磁盘上的物理文件直接映射到用户态的内存地址中,将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率 + ```java + // 参数 offset 表示 CommitLog 内最早的消息的 phyOffset + public void deleteExpiredFile(long offset) + ``` -MappedByteBuffer 内存映射的方式**限制**一次只能映射 1.5~2G 的文件至用户态的虚拟内存,所以 RocketMQ 默认设置单个 CommitLog 日志数据文件为 1G。RocketMQ 的文件存储使用定长结构来存储,方便一次将整个文件映射至内存 + * `this.readWriteLock.readLock().lock()`:加锁判断 + * `long endPhyOffset = this.indexFileList.get(0).getEndPhyOffset()`:获取目录中第一个文件的结束偏移量 + * `if (endPhyOffset < offset)`:索引目录内存在过期的索引文件,并且当前的 IndexFile 都是过期的数据 + * `for (int i = 0; i < (files.length - 1); i++)`:遍历文件列表,删除过期的文件 +* buildIndex():存储主模块 DefaultMessageStore 内部的异步线程调用,构建 Index 数据 + ```java + public void buildIndex(DispatchRequest req) + ``` -*** + * `indexFile = retryGetAndCreateIndexFile()`:获取或者创建顺序写的索引文件对象 + * `buildKey(topic, req.getUniqKey())`:**构建索引 key**,`topic + # + uniqKey` + * `indexFile = putKey()`:插入索引文件 -###### 页缓存 + * `if (keys != null && keys.length() > 0)`:消息存在自定义索引 -页缓存(PageCache)是 OS 对文件的缓存,每一页的大小通常是 4K,用于加速对文件的读写。程序对文件进行顺序读写的速度几乎接近于内存的读写速度,就是因为 OS 将一部分的内存用作 PageCache,**对读写访问操作进行了性能优化** + `for (int i = 0; i < keyset.length; i++)`:遍历每个索引,为每个 key 调用一次 putKey -* 对于数据的写入,OS 会先写入至 Cache 内,随后通过异步的方式由 pdflush 内核线程将 Cache 内的数据刷盘至物理磁盘上 -* 对于数据的读取,如果一次读取文件时出现未命中 PageCache 的情况,OS 从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行预读取(局部性原理,最大 128K) +* getAndCreateLastIndexFile():获取当前顺序写的 IndexFile,没有就创建 -在 RocketMQ 中,ConsumeQueue 逻辑消费队列存储的数据较少,并且是顺序读取,在 PageCache 机制的预读取作用下,Consume Queue 文件的读性能几乎接近读内存,即使在有消息堆积情况下也不会影响性能。但是 CommitLog 消息存储的日志数据文件读取内容时会产生较多的随机访问读取,严重影响性能。选择合适的系统 IO 调度算法和固态硬盘,比如设置调度算法为 Deadline,随机读的性能也会有所提升 + ```java + public IndexFile getAndCreateLastIndexFile() + ``` @@ -6672,175 +6548,236 @@ MappedByteBuffer 内存映射的方式**限制**一次只能映射 1.5~2G 的文 -##### 刷盘机制 +#### MesStore -两种持久化的方案: +##### 生命周期 -* 关系型数据库 DB:IO 读写性能比较差,如果 DB 出现故障,则 MQ 的消息就无法落盘存储导致线上故障,可靠性不高 -* 文件系统:消息刷盘至所部署虚拟机/物理机的文件系统来做持久化,分为异步刷盘和同步刷盘两种模式。消息刷盘为消息存储提供了一种高效率、高可靠性和高性能的数据持久化方式,除非部署 MQ 机器本身或是本地磁盘挂了,一般不会出现无法持久化的问题 +DefaultMessageStore 类核心是整个存储服务的调度类 -RocketMQ 采用文件系统的方式,无论同步还是异步刷盘,都使用**顺序 IO**,因为磁盘的顺序读写要比随机读写快很多 +* 构造方法: -* 同步刷盘:只有在消息真正持久化至磁盘后 RocketMQ 的 Broker 端才会真正返回给 Producer 端一个成功的 ACK 响应,保障 MQ消息的可靠性,但是性能上会有较大影响,一般适用于金融业务应用该模式较多 + ```java + public DefaultMessageStore() + ``` -* 异步刷盘:利用 OS 的 PageCache,只要消息写入内存 PageCache 即可将成功的 ACK 返回给 Producer 端,降低了读写延迟,提高了 MQ 的性能和吞吐量。消息刷盘采用**后台异步线程**提交的方式进行,当内存里的消息量积累到一定程度时,触发写磁盘动作 + * `this.allocateMappedFileService.start()`:启动**创建 MappedFile 文件服务** + * `this.indexService.start()`:启动索引服务 -通过 Broker 配置文件里的 flushDiskType 参数设置采用什么方式,可以配置成 SYNC_FLUSH、ASYNC_FLUSH 中的一个 +* load():加载资源 -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-刷盘机制.png) + ```java + public boolean load() + ``` + * `this.commitLog.load()`:先加载 CommitLog + * `this.loadConsumeQueue()`:再加载 ConsumeQueue + * `this.storeCheckpoint`:检查位点对象 + * `this.indexService.load(lastExitOK)`:加载 IndexFile + * `this.recover(lastExitOK)`:恢复阶段,先恢复 CQ,在恢复 CL +* start():核心启动方法 -官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md + ```java + public void start() + ``` + * `lock = lockFile.getChannel().tryLock(0, 1, false)`:获取文件锁,获取失败说明当前目录已经启动过 Broker + * `long maxPhysicalPosInLogicQueue = commitLog.getMinOffset()`:遍历全部的 CQ 对象,获取 CQ 中消息的最大偏移量 -*** + * `this.reputMessageService.start()`:设置分发服务的分发位点,启动**分发服务**,构建 ConsumerQueue 和 IndexFile + * `if (dispatchBehindBytes() <= 0)`:线程等待分发服务将分发数据全部处理完毕 + * `this.recoverTopicQueueTable()`:因为修改了 CQ 数据,所以再次构建队列偏移量字段表 -#### MappedFile + * `this.haService.start()`:启动 **HA 服务** -##### 成员属性 + * `this.handleScheduleMessageService()`:启动**消息调度服务** -MappedFile 类是最基础的存储类,继承自 ReferenceResource 类,用来**保证线程安全** + * `this.flushConsumeQueueService.start()`:启动 CQ **消费队列刷盘服务** -MappedFile 类成员变量: + * `this.commitLog.start()`:启动 **CL 刷盘服务** -* 内存相关: + * `this.storeStatsService.start()`:启动状态存储服务 - ```java - public static final int OS_PAGE_SIZE = 1024 * 4;// 内存页大小:默认是 4k - private AtomicLong TOTAL_MAPPED_VIRTUAL_MEMORY; // 当前进程下所有的 mappedFile 占用的总虚拟内存大小 - private AtomicInteger TOTAL_MAPPED_FILES; // 当前进程下所有的 mappedFile 个数 - ``` + * `this.createTempFile()`:创建 AbortFile,正常关机时 JVM HOOK 会删除该文件,异常宕机时该文件不会删除,开机数据恢复阶段根据是否存在该文件,执行不同的**恢复策略** -* 数据位点: + * `this.addScheduleTask()`:添加定时任务 - ```java - protected final AtomicInteger wrotePosition; // 当前 mappedFile 的数据写入点 - protected final AtomicInteger committedPosition;// 当前 mappedFile 的数据提交点 - private final AtomicInteger flushedPosition; // 数据落盘位点,在这之前的数据是持久化的安全数据 - // flushedPosition-wrotePosition 之间的数据属于脏页 - ``` + * `DefaultMessageStore.this.cleanFilesPeriodically()`:定时**清理过期文件**,周期是 10 秒 -* 文件相关:CL 是 CommitLog,CQ 是 ConsumeQueue + * `this.cleanCommitLogService.run()`:启动清理过期的 CL 文件服务 + * `this.cleanConsumeQueueService.run()`:启动清理过期的 CQ 文件服务 - ```java - private String fileName; // 文件名称,CL和CQ文件名是第一条消息的物理偏移量,索引文件是年月日时分秒 - private long fileFromOffset;// 文件名转long,代表该对象的【起始偏移量】 - private File file; // 文件对象 - ``` + * `DefaultMessageStore.this.checkSelf()`:每 10 分种进行健康检查 -* 内存映射: + * `DefaultMessageStore.this.cleanCommitLogService.isSpaceFull()`:**磁盘预警**定时任务,每 10 秒一次 - ```java - protected FileChannel fileChannel; // 文件通道 - private MappedByteBuffer mappedByteBuffer; // 内存映射缓冲区,访问虚拟内存 - ``` + * `if (physicRatio > this.diskSpaceWarningLevelRatio)`:检查磁盘是否到达 waring 阈值,默认 90% -ReferenceResource 类成员变量: + `boolean diskok = ...runningFlags.getAndMakeDiskFull()`:设置磁盘写满标记 -* 引用数量:当 `refCount <= 0` 时,表示该资源可以释放了,没有任何其他程序依赖它了,用原子类保证线程安全 + * `boolean diskok = ...this.runningFlags.getAndMakeDiskOK()`:设置磁盘可写标记 - ```java - protected final AtomicLong refCount = new AtomicLong(1); // 初始值为 1 - ``` + * `this.shutdown = false`:刚启动,设置为 false -* 存活状态:表示资源的存活状态 +* shutdown():关闭各种服务和线程资源,设置存储模块状态为关闭状态 ```java - protected volatile boolean available = true; + public void shutdown() ``` -* 是否清理:默认值 false,当执行完子类对象的 cleanup() 清理方法后,该值置为 true ,表示资源已经全部释放 +* destroy():销毁 Broker 的工作目录 ```java - protected volatile boolean cleanupOver = false; + public void destroy() ``` -* 第一次关闭资源的时间:用来记录超时时间 - ```java - private volatile long firstShutdownTimestamp = 0; - ``` - + *** -##### 成员方法 +##### 服务线程 -MappedFile 类核心方法: +ServiceThread 类被很多服务继承,本身是一个 Runnable 任务对象,继承者通过重写 run 方法来实现服务的逻辑 -* appendMessage():提供上层向内存映射中追加消息的方法,消息如何追加由 AppendMessageCallback 控制 +* run():一般实现方式 ```java - // 参数一:消息 参数二:追加消息回调 - public AppendMessageResult appendMessage(MessageExtBrokerInner msg, AppendMessageCallback cb) + public void run() { + while (!this.isStopped()) { + // 业务逻辑 + } + } ``` + 通过参数 stopped 控制服务的停止,使用 volatile 修饰保证可见性 + ```java - // 将字节数组写入到文件通道 - public boolean appendMessage(final byte[] data) + protected volatile boolean stopped = false ``` -* flush():刷盘接口,参数 flushLeastPages 代表刷盘的最小页数 ,等于 0 时属于强制刷盘;> 0 时需要脏页(计算方法在数据位点)达到该值才进行物理刷盘;文件写满时强制刷盘 +* shutdown():停止线程,首先设置 stopped 为 true,然后进行唤醒,默认不直接打断线程 ```java - public int flush(final int flushLeastPages) + public void shutdown() ``` -* selectMappedBuffer():该方法以 pos 为开始位点 ,到有效数据为止,创建一个切片 ByteBuffer 作为数据副本,供业务访问数据 +* waitForRunning():挂起线程,设置唤醒标记 hasNotified 为 false ```java - public SelectMappedBufferResult selectMappedBuffer(int pos) + protected void waitForRunning(long interval) ``` -* destroy():销毁映射文件对象,并删除关联的系统文件,参数是强制关闭资源的时间 +* wakeup():唤醒线程,设置 hasNotified 为 true ```java - public boolean destroy(final long intervalForcibly) + public void wakeup() ``` -* cleanup():释放堆外内存,更新总虚拟内存和总内存映射文件数 - ```java - public boolean cleanup(final long currentRef) - ``` - -* warmMappedFile():内存预热,当要新建的 MappedFile 对象大于 1g 时,执行该方法对该 MappedFile 的每个 Page Cache 进行写入一个字节进行分配内存,**将映射文件都加载到内存** + +*** + + + +##### 构建服务 + +AllocateMappedFileService 创建 MappedFile 服务 + +* mmapOperation():核心服务 ```java - public void warmMappedFile(FlushDiskType type, int pages) + private boolean mmapOperation() ``` -ReferenceResource 类核心方法: + * `req = this.requestQueue.take()`: 从 requestQueue 阻塞队列(优先级)中获取 AllocateRequest 任务 + * `if (...isTransientStorePoolEnable())`:条件成立使用直接内存写入数据, 从直接内存中 commit 到 FileChannel 中 + * `mappedFile = new MappedFile(req.getFilePath(), req.getFileSize())`:根据请求的路径和大小创建对象 + * `mappedFile.warmMappedFile()`:判断 mappedFile 大小,只有 CommitLog 才进行文件预热 + * `req.setMappedFile(mappedFile)`:将创建好的 MF 对象的赋值给请求对象的成员属性 + * `req.getCountDownLatch().countDown()`:**唤醒请求的阻塞线程** -* hold():增加引用记数 refCount,方法加锁 +* putRequestAndReturnMappedFile():MappedFileQueue 中用来创建 MF 对象的方法 ```java - public synchronized boolean hold() + public MappedFile putRequestAndReturnMappedFile(String nextFilePath, String nextNextFilePath, int fileSize) ``` -* shutdown():关闭资源,参数代表强制关闭资源的时间间隔 + * `AllocateRequest nextReq = new AllocateRequest(...)`:创建 nextFilePath 的 AllocateRequest 对象,放入请求列表和阻塞队列,然后创建 nextNextFilePath 的 AllocateRequest 对象,放入请求列表和阻塞队列 + * `AllocateRequest result = this.requestTable.get(nextFilePath)`:从请求列表获取 nextFilePath 的请求对象 + * `result.getCountDownLatch().await(...)`:**线程挂起**,直到超时或者 nextFilePath 对应的 MF 文件创建完成 + * `return result.getMappedFile()`:返回创建好的 MF 文件对象 + +ReputMessageService 消息分发服务,用于构建 ConsumerQueue 和 IndexFile 文件 + +* run():循环执行 doReput 方法,每执行一次线程休眠 1 毫秒 ```java - // 系统当前时间 - firstShutdownTimestamp 时间 > intervalForcibly 进行【强制关闭】 - public void shutdown(final long intervalForcibly) + public void run() ``` -* release():引用计数减 1,当 refCount 为 0 时,调用子类的 cleanup 方法 +* doReput():实现分发的核心逻辑 ```java - public void release() + private void doReput() ``` - + * `for (boolean doNext = true; this.isCommitLogAvailable() && doNext; )`:循环遍历 + * `SelectMappedBufferResult result`: 从 CommitLog 拉取数据,数据范围 `[reputFromOffset, 包含该偏移量的 MF 的最大 Pos]`,封装成结果对象 + * `DispatchRequest dispatchRequest`:从结果对象读取出一条 DispatchRequest 数据 + * `DefaultMessageStore.this.doDispatch(dispatchRequest)`:将数据交给分发器进行分发,用于**构建 CQ 和索引文件** + * `this.reputFromOffset += size`:更新数据范围 + + + +*** + + + +##### 刷盘服务 + +FlushConsumeQueueService 刷盘 CQ 数据 + +* run():每隔 1 秒执行一次刷盘服务,跳出循环后还会执行一次强制刷盘 + + ```java + public void run() + ``` +* doFlush():刷盘 + ```java + private void doFlush(int retryTimes) + ``` + + * `int flushConsumeQueueLeastPages`:脏页阈值,默认是 2 + + * `if (retryTimes == RETRY_TIMES_OVER)`:**重试次数是 3** 时设置强制刷盘,设置脏页阈值为 0 + * `int flushConsumeQueueThoroughInterval`:两次刷新的**时间间隔超过 60 秒**会强制刷盘 + * `for (ConsumeQueue cq : maps.values())`:遍历所有的 CQ,进行刷盘 + * `DefaultMessageStore.this.getStoreCheckpoint().flush()`:强制刷盘时将 StoreCheckpoint 瞬时数据刷盘 + +FlushCommitLogService 刷盘 CL 数据,默认是异步刷盘 + +* run():运行方法 + + ```java + public void run() + ``` + + * `while (!this.isStopped())`:stopped为 true 才跳出循环 + * `boolean flushCommitLogTimed`:控制线程的休眠方式,默认是 false,使用 `CountDownLatch.await()` 休眠,设置为 true 时使用 `Thread.sleep()` 休眠 + * `int interval`:获取配置中的刷盘时间间隔 + * `int flushPhysicQueueLeastPages`:获取最小刷盘页数,默认是 4 页,脏页达到指定页数才刷盘 + * `int flushPhysicQueueThoroughInterval`:获取强制刷盘周期,默认是 10 秒,达到周期后强制刷盘,不考虑脏页 + * `if (flushCommitLogTimed)`:休眠逻辑,避免 CPU 占用太长时间,导致无法执行其他更紧急的任务 + * `CommitLog.this.mappedFileQueue.flush(flushPhysicQueueLeastPages)`:**刷盘** @@ -6848,120 +6785,1005 @@ ReferenceResource 类核心方法: -#### MapQueue +##### 清理服务 -MappedFileQueue 用来管理 MappedFile 文件 +CleanCommitLogService 清理过期的 CL 数据,定时任务 10 秒调用一次,先清理 CL,再清理 CQ,因为 CQ 依赖于 CL 的数据 -成员变量: +* run():运行方法 -* 管理目录:CommitLog 是 `../store/commitlog`, ConsumeQueue 是 `../store/xxx_topic/0` + ```java + public void run() + ``` + +* deleteExpiredFiles():删除过期 CL 文件 ```java - private final String storePath; + private void deleteExpiredFiles() ``` -* 文件属性: + * `long fileReservedTime`:默认 72,代表文件的保留时间 + * `boolean timeup = this.isTimeToDelete()`:当前时间是否是凌晨 4 点 + * `boolean spacefull = this.isSpaceToDelete()`:CL 或者 CQ 的目录磁盘使用率达到阈值标准 85% + * `boolean manualDelete = this.manualDeleteFileSeveralTimes > 0`:手动删除文件 + * `fileReservedTime *= 60 * 60 * 1000`:默认保留 72 小时 + * `deleteCount = DefaultMessageStore.this.commitLog.deleteExpiredFile()`:**调用 MFQ 对象的删除方法** + +CleanConsumeQueueService 清理过期的 CQ 数据 + +* run():运行方法 ```java - private final int mappedFileSize; // 目录下每个文件大小,CL文件默认 1g,CQ文件 默认 600w字节 - private final CopyOnWriteArrayList mappedFiles; //目录下的每个 mappedFile 都加入该集合 + public void run() ``` -* 数据位点: +* deleteExpiredFiles():删除过期 CQ 文件 ```java - private long flushedWhere = 0; // 目录的刷盘位点,值为 mf.fileName + mf.wrotePosition - private long committedWhere = 0; // 目录的提交位点 + private void deleteExpiredFiles() ``` -* 消息存储: + * `int deleteLogicsFilesInterval`:清理 CQ 的时间间隔,默认 100 毫秒 + * `long minOffset = DefaultMessageStore.this.commitLog.getMinOffset()`:获取 CL 文件中最小的物理偏移量 + * `if (minOffset > this.lastPhysicalMinOffset)`:CL 最小的偏移量大于 CQ 最小的,说明有过期数据 + * `this.lastPhysicalMinOffset = minOffset`:更新 CQ 的最小偏移量 + * `for (ConsumeQueue logic : maps.values())`:遍历所有的 CQ 文件 + * `logic.deleteExpiredFile(minOffset)`:调用 MFQ 对象的删除方法 + * `DefaultMessageStore.this.indexService.deleteExpiredFile(minOffset)`:**删除过期的索引文件** + + + +*** + + + +#### Broker + +BrokerStartup 启动方法 + +```java +public static void main(String[] args) { + start(createBrokerController(args)); +} +public static BrokerController start(BrokerController controller) { + controller.start(); // 启动 +} +``` + +BrokerController#start:核心启动方法 + +* `this.messageStore.start()`:**启动存储服务** + +* `this.remotingServer.start()`:启动 Netty 通信服务 + +* `this.fileWatchService.start()`:启动文件监听服务 + +* `this.scheduledExecutorService.scheduleAtFixedRate()`:每隔 30s 向 NameServer 上报 Topic 路由信息,**心跳机制** + + `BrokerController.this.registerBrokerAll(true, false, brokerConfig.isForceRegister())` + + + + + +**** + + + +### 生产者 + +#### 生产者类 + +DefaultMQProducer 是生产者的默认实现类 + +成员变量: + +* 生产者实现类: ```java - private volatile long storeTimestamp = 0; // 当前目录下最后一条 msg 的存储时间 + protected final transient DefaultMQProducerImpl defaultMQProducerImpl ``` -* 创建服务:新建 MappedFile 实例,继承自 ServiceThread 是一个任务对象,run 方法用来创建实例 +* 生产者组:发送事务消息,Broker 端进行事务回查(补偿机制)时,选择当前生产者组的下一个生产者进行事务回查 ```java - private final AllocateMappedFileService allocateMappedFileService; + private String producerGroup; ``` -核心方法: +* 默认主题:isAutoCreateTopicEnable 开启时,当发送消息指定的 Topic 在 Namesrv 未找到路由信息,使用该值创建 Topic 信息 -* load():Broker 启动时,加载本地磁盘数据,该方法读取 storePath 目录下的文件,创建 MappedFile 对象放入集合内 + ```java + private String createTopicKey = TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC; + // 值为【TBW102】,Just for testing or demo program + ``` + +* 消息重投:系统特性消息重试部分详解了三个参数的作用 ```java - public boolean load() + private int retryTimesWhenSendFailed = 2; // 同步发送失败后重试的发送次数,加上第一次发送,一共三次 + private int retryTimesWhenSendAsyncFailed = 2; // 异步 + private boolean retryAnotherBrokerWhenNotStoreOK = false; // 消息未存储成功,选择其他 Broker 重试 ``` -* getLastMappedFile():获取当前正在顺序写入的 MappedFile 对象,如果最后一个 MappedFile 写满了,或者不存在 MappedFile 对象,则创建新的 MappedFile +* 消息队列: ```java - // 参数一:文件起始偏移量;参数二:当list为空时,是否新建 MappedFile - public MappedFile getLastMappedFile(final long startOffset, boolean needCreate) + private volatile int defaultTopicQueueNums = 4; // 默认 Broker 创建的队列数 ``` -* flush():根据 flushedWhere 属性查找合适的 MappedFile,调用该 MappedFile 的落盘方法,并更新全局的 flushedWhere +* 消息属性: ```java - //参数:0 表示强制刷新, > 0 脏页数据必须达到 flushLeastPages 才刷新 - public boolean flush(final int flushLeastPages) + private int sendMsgTimeout = 3000; // 发送消息的超时限制 + private int compressMsgBodyOverHowmuch = 1024 * 4; // 压缩阈值,当 msg body 超过 4k 后使用压缩 + private int maxMessageSize = 1024 * 1024 * 4; // 消息体的最大限制,默认 4M + private TraceDispatcher traceDispatcher = null; // 消息轨迹 + +构造方法: + +* 构造方法: + + ```java + public DefaultMQProducer(final String namespace, final String producerGroup, RPCHook rpcHook) { + this.namespace = namespace; + this.producerGroup = producerGroup; + // 创建生产者实现对象 + defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook); + } ``` -* findMappedFileByOffset():根据偏移量查询对象 +成员方法: + +* start():启动方法 ```java - public MappedFile findMappedFileByOffset(final long offset, final boolean returnFirstOnNotFound) + public void start() throws MQClientException { + // 重置生产者组名,如果传递了命名空间,则 【namespace%group】 + this.setProducerGroup(withNamespace(this.producerGroup)); + // 生产者实现对象启动 + this.defaultMQProducerImpl.start(); + if (null != traceDispatcher) { + // 消息轨迹的逻辑 + traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel()); + } + } ``` -* deleteExpiredFileByTime():CL 删除过期文件,根据文件的保留时长决定是否删除 +* send():**发送消息**: ```java - // 参数一:过期时间; 参数二:删除两个文件之间的时间间隔; 参数三:mf.destory传递的参数; 参数四:true 强制删除 - public int deleteExpiredFileByTime(final long expiredTime,final int deleteFilesInterval, final long intervalForcibly, final boolean cleanImmediately) + public SendResult send(Message msg){ + // 校验消息 + Validators.checkMessage(msg, this); + // 设置消息 Topic + msg.setTopic(withNamespace(msg.getTopic())); + return this.defaultMQProducerImpl.send(msg); + } ``` -* deleteExpiredFileByOffset():CQ 删除过期文件,遍历每个 MF 文件,获取当前文件最后一个数据单元的物理偏移量,小于 offset 说明当前 MF 文件内都是过期数据 +* request():请求方法,**需要消费者回执消息**,又叫回退消息 ```java - // 参数一:consumeLog 目录下最小物理偏移量,就是第一条消息的 offset; - // 参数二:ConsumerQueue 文件内每个数据单元固定大小 - public int deleteExpiredFileByOffset(long offset, int unitSize) + public Message request(final Message msg, final MessageQueue mq, final long timeout) { + msg.setTopic(withNamespace(msg.getTopic())); + return this.defaultMQProducerImpl.request(msg, mq, timeout); + } ``` - +*** + + + +#### 默认实现 + +##### 成员属性 + +DefaultMQProducerImpl 类是默认的生产者实现类 + +成员变量: + +* 实例对象: + + ```java + private final DefaultMQProducer defaultMQProducer; // 持有默认生产者对象,用来获取对象中的配置信息 + private MQClientInstance mQClientFactory; // 客户端实例对象,生产者启动后需要注册到该客户端对象内 + ``` +* 主题发布信息映射表:key 是 Topic,value 是发布信息 + ```java + private final ConcurrentMap topicPublishInfoTable = new ConcurrentHashMap(); + ``` +* 异步发送消息:相关信息 + ```java + private final BlockingQueue asyncSenderThreadPoolQueue;// 异步发送消息,异步线程池使用的队列 + private final ExecutorService defaultAsyncSenderExecutor; // 异步发送消息默认使用的线程池 + private ExecutorService asyncSenderExecutor; // 异步消息发送线程池,指定后就不使用默认线程池了 + ``` +* 定时器:执行定时任务 + ```java + private final Timer timer = new Timer("RequestHouseKeepingService", true); // 守护线程 + ``` +* 状态信息:服务的状态,默认创建状态 + ```java + private ServiceState serviceState = ServiceState.CREATE_JUST; + ``` +* 压缩等级:ZIP 压缩算法的等级,默认是 5,越高压缩效果好,但是压缩的更慢 + ```java + private int zipCompressLevel = Integer.parseInt(System.getProperty..., "5")); + ``` +* 容错策略:选择队列的容错策略 + ```java + private MQFaultStrategy mqFaultStrategy = new MQFaultStrategy(); + ``` +* 钩子:用来进行前置或者后置处理 + ```java + ArrayList sendMessageHookList; // 发送消息的钩子,留给用户扩展使用 + ArrayList checkForbiddenHookList; // 对比上面的钩子,可以抛异常,控制消息是否可以发送 + private final RPCHook rpcHook; // 传递给 NettyRemotingClient + ``` +构造方法: +* 默认构造: + ```java + public DefaultMQProducerImpl(final DefaultMQProducer defaultMQProducer) { + // 默认 RPC HOOK 是空 + this(defaultMQProducer, null); + } + ``` +* 有参构造: + ```java + public DefaultMQProducerImpl(final DefaultMQProducer defaultMQProducer, RPCHook rpcHook) { + // 属性赋值 + this.defaultMQProducer = defaultMQProducer; + this.rpcHook = rpcHook; + + // 创建【异步消息线程池任务队列】,长度是 5w + this.asyncSenderThreadPoolQueue = new LinkedBlockingQueue(50000); + // 创建默认的异步消息任务线程池 + this.defaultAsyncSenderExecutor = new ThreadPoolExecutor( + // 核心线程数和最大线程数都是 系统可用的计算资源(8核16线程的系统就是 16)... + } + ``` + +**** +##### 成员方法 +* start():启动方法,参数默认是 true,代表正常的启动路径 + * `this.serviceState = ServiceState.START_FAILED`:先修改为启动失败,成功后再修改,这种思想很常见 + * `this.checkConfig()`:判断生产者组名不能是空,也不能是 default_PRODUCER + * `if (!getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP))`:条件成立说明当前生产者不是内部产生者,内部生产者是**处理消息回退**的这种情况使用的生产者 + `this.defaultMQProducer.changeInstanceNameToPID()`:正常的生产者,修改生产者实例名称为当前进程的 PID + + * ` this.mQClientFactory = ...`:获取当前进程的 MQ 客户端实例对象,从 factoryTable 中获取 key 为 客户端 ID,格式是`ip@pid`,**一个 JVM 进程只有一个 PID,也只有一个 MQClientInstance** + + * `boolean registerOK = mQClientFactory.registerProducer(...)`:将生产者注册到 RocketMQ 客户端实例内 + + * `this.topicPublishInfoTable.put(...)`:添加一个主题发布信息,key 是 **TBW102** ,value 是一个空对象 + + * `if (startFactory) `:正常启动路径 + + `mQClientFactory.start()`:启动 RocketMQ 客户端实例对象 + + * `this.serviceState = ServiceState.RUNNING`:修改生产者实例的状态 + + * `this.mQClientFactory.sendHeartbeatToAllBrokerWithLock()`:RocketMQ 客户端实例向已知的 Broker 节点发送一次心跳(也是定时任务) + * `this.timer.scheduleAtFixedRate()`: request 发送的回执信息,启动定时任务每秒一次删除超时请求 + + * 生产者 msg 添加信息关联 ID 发送到 Broker + * 消费者从 Broker 拿到消息后会检查 msg 类型是一个需要回执的消息,处理完消息后会根据 msg 关联 ID 和客户端 ID 生成一条响应结果消息发送到 Broker,Broker 判断为回执消息,会根据客户端ID 找到 channel 推送给生产者 + * 生产者拿到回执消息后,读取出来关联 ID 找到对应的 RequestFuture,将阻塞线程唤醒 + +* sendDefaultImpl():发送消息 + + ```java + //参数1:消息;参数2:发送模式(同步异步单向);参数3:回调函数,异步发送时需要;参数4:发送超时时间, 默认 3 秒 + private SendResult sendDefaultImpl(msg, communicationMode, sendCallback,timeout) {} + ``` + + * `this.makeSureStateOK()`:校验生产者状态是运行中,否则抛出异常 + + * `Validators.checkMessage(msg, this.defaultMQProducer)`:校验消息规格 + + * `long beginTimestampPrev, endTimestamp`:本轮发送的开始时间和本轮的结束时间 + + * `topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic())`:**获取当前消息主题的发布信息** + + * `this.topicPublishInfoTable.get(topic)`:尝试从本地主题发布信息映射表获取信息,不空直接返回 + + * `if (null == topicPublishInfo || !topicPublishInfo.ok())`:本地没有需要去 MQ 客户端获取 + + `this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo())`:保存一份空数据 + + `this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic)`:从 Namesrv 更新该 Topic 的路由数据 + + `topicPublishInfo = this.topicPublishInfoTable.get(topic)`:重新从本地获取发布信息 + + * `this.mQClientFactory.updateTopicRouteInfoFromNameServer(..)`:**路由数据是空,获取默认 TBW102 的数据** + + * `return topicPublishInfo`:返回 TBW102 主题的发布信息 + + * `int timesTotal, times `:发送的总尝试次数和当前是第几次发送 + + * `String[] brokersSent = new String[timesTotal]`:下标索引代表第几次发送,值代表这次发送选择 Broker name + + * `for (; times < timesTotal; times++)`:循环发送,发送成功或者发送尝试次数达到上限,结束循环 + + * `String lastBrokerName = null == mq ? null : mq.getBrokerName()`:获取上次发送失败的 BrokerName + + * `mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName)`:**从发布信息中选择一个队列** + + * `if (this.sendLatencyFaultEnable)`:默认不开启,可以通过配置开启 + * `return tpInfo.selectOneMessageQueue(lastBrokerName)`:默认选择队列的方式,就是循环主题全部的队列 + + * `brokersSent[times] = mq.getBrokerName()`:将本次选择的 BrokerName 存入数组 + + * `msg.setTopic(this.defaultMQProducer.withNamespace(msg.getTopic()))`:**重投的消息需要加上标记** + + * `sendResult = this.sendKernelImpl`:核心发送方法 + + * `this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false)`:更新一下时间 + + * `switch (communicationMode)`:异步或者单向消息直接返回 null,同步发送进入逻辑判断 + + `if (sendResult.getSendStatus() != SendStatus.SEND_OK)`:**服务端 Broker 存储失败**,需要重试其他 Broker + + * `throw new MQClientException()`:未找到当前主题的路由数据,无法发送消息,抛出异常 + +* sendKernelImpl():**核心发送方法** + + ```java + //参数1:消息;参数2:选择的队列;参数3:发送模式(同步异步单向);参数4:回调函数,异步发送时需要;参数5:主题发布信息;参数6:剩余超时时间限制 + private SendResult sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout) + ``` + + * `brokerAddr = this.mQClientFactory(...)`:获取指定 BrokerName 对应的 mater 节点的地址,master 节点的 ID 为 0 + + * `brokerAddr = MixAll.brokerVIPChannel()`:Broker 启动时会绑定两个服务器端口,一个是普通端口,一个是 VIP 端口,服务器端根据不同端口创建不同的的 NioSocketChannel + + * `byte[] prevBody = msg.getBody()`:获取消息体 + + * `if (!(msg instanceof MessageBatch))`:非批量消息,需要重新设置消息 ID + + `MessageClientIDSetter.setUniqID(msg)`:msg id 由两部分组成,一部分是 ip 地址、进程号、ClassLoader 的 hashcode,另一部分是时间差(当前时间减去当月一号的时间)和计数器的值 + + * `if (this.tryToCompressMessage(msg))`:判断消息是否压缩,压缩需要设置压缩标记 + + * `hasCheckForbiddenHook、hasSendMessageHook`:执行钩子方法 + + * `requestHeader = new SendMessageRequestHeader()`:设置发送消息的消息头 + + * `if (requestHeader.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX))`:重投的发送消息 + + * `switch (communicationMode)`:异步发送一种处理方式,单向和同步同样的处理逻辑 + + `sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage()`:**发送消息** + + * `request = RemotingCommand.createRequestCommand()`:创建一个 RequestCommand 对象 + * `request.setBody(msg.getBody())`:**将消息放入请求体** + * `switch (communicationMode)`:根据不同的模式 invoke 不同的方法 + +* request():请求方法,消费者回执消息,这种消息是异步消息 + + * `requestResponseFuture = new RequestResponseFuture(correlationId, timeout, null)`:创建请求响应对象 + + * `getRequestFutureTable().put(correlationId, requestResponseFuture)`:放入RequestFutureTable 映射表中 + + * `this.sendDefaultImpl(msg, CommunicationMode.ASYNC, new SendCallback())`:**发送异步消息** + + * `return waitResponse(msg, timeout, requestResponseFuture, cost)`:用来挂起请求的方法 + + ```java + public Message waitResponseMessage(final long timeout) throws InterruptedException { + // 请求挂起 + this.countDownLatch.await(timeout, TimeUnit.MILLISECONDS); + return this.responseMsg; + } + + * 当消息被消费后,会获取消息的关联 ID,从映射表中获取消息的 RequestResponseFuture,执行下面的方法唤醒挂起线程 + + ```java + public void putResponseMessage(final Message responseMsg) { + this.responseMsg = responseMsg; + this.countDownLatch.countDown(); + } + ``` + + + + +*** + + + +#### 路由信息 + +TopicPublishInfo 类用来存储路由信息 + +成员变量: + +* 顺序消息: + + ```java + private boolean orderTopic = false; + ``` + +* 消息队列: + + ```java + private List messageQueueList = new ArrayList<>(); // 主题全部的消息队列 + private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex(); // 消息队列索引 + ``` + + ```java + // 【消息队列类】 + public class MessageQueue implements Comparable, Serializable { + private String topic; + private String brokerName; + private int queueId;// 队列 ID + } + ``` + +* 路由数据:主题对应的路由数据 + + ```java + private TopicRouteData topicRouteData; + ``` + + ```java + public class TopicRouteData extends RemotingSerializable { + private String orderTopicConf; + private List queueDatas; // 队列数据 + private List brokerDatas; // Broker 数据 + private HashMap/* Filter Server */> filterServerTable; + } + ``` + + ```java + public class QueueData implements Comparable { + private String brokerName; // 节点名称 + private int readQueueNums; // 读队列数 + private int writeQueueNums; // 写队列数 + private int perm; // 权限 + private int topicSynFlag; + } + ``` + + ```java + public class BrokerData implements Comparable { + private String cluster; // 集群名 + private String brokerName; // Broker节点名称 + private HashMap brokerAddrs; + } + ``` + +核心方法: + +* selectOneMessageQueue():**选择消息队列**使用 + + ```java + // 参数是上次失败时的 brokerName,可以为 null + public MessageQueue selectOneMessageQueue(final String lastBrokerName) { + if (lastBrokerName == null) { + return selectOneMessageQueue(); + } else { + // 遍历消息队列 + for (int i = 0; i < this.messageQueueList.size(); i++) { + // 获取队列的索引 + int index = this.sendWhichQueue.getAndIncrement(); + // 获取队列的下标位置 + int pos = Math.abs(index) % this.messageQueueList.size(); + if (pos < 0) + pos = 0; + // 获取消息队列 + MessageQueue mq = this.messageQueueList.get(pos); + // 与上次选择的不同就可以返回 + if (!mq.getBrokerName().equals(lastBrokerName)) { + return mq; + } + } + return selectOneMessageQueue(); + } + } + ``` + + + + + +**** + + + +### 消费者 + + + + + + + +*** + + + +### 客户端 + +#### 实例对象 + +##### 成员属性 + +MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一个客户端实例,**既服务于生产者,也服务于消费者** + +成员变量: + +* 配置信息: + + ```java + private final int instanceIndex; // 索引一般是 0,因为客户端实例一般都是一个进程只有一个 + private final String clientId; // 客户端 ID ip@pid + private final long bootTimestamp; // 客户端的启动时间 + private ServiceState serviceState; // 客户端状态 + ``` + +* 生产者消费者的映射表:key 是组名 + + ```java + private final ConcurrentMap producerTable + private final ConcurrentMap consumerTable + private final ConcurrentMap adminExtTable + ``` + +* 网络层配置: + + ```java + private final NettyClientConfig nettyClientConfig; + ``` + +* 核心功能的实现:负责将 MQ 业务层的数据转换为网络层的 RemotingCommand 对象,使用内部持有的 NettyRemotingClient 对象的 invoke 系列方法,完成网络 IO(同步、异步、单向) + + ```java + private final MQClientAPIImpl mQClientAPIImpl; + ``` + +* 本地路由数据:key 是主题名称,value 路由信息 + + ```java + private final ConcurrentMap topicRouteTable = new ConcurrentHashMap<>(); + +* 锁信息:两把锁,锁不同的数据 + + ```java + private final Lock lockNamesrv = new ReentrantLock(); + private final Lock lockHeartbeat = new ReentrantLock(); + ``` + +* 调度线程池:单线程,执行定时任务 + + ```java + private final ScheduledExecutorService scheduledExecutorService; + ``` + +* Broker 映射表:key 是 BrokerName + + ```java + // 物理节点映射表,value:Long 是 brokerID,【ID=0 的是主节点,其他是从节点】,String 是地址 ip:port + private final ConcurrentMap> brokerAddrTable; + // 物理节点版本映射表,String 是地址 ip:port,Integer 是版本 + ConcurrentMap> brokerVersionTable; + ``` + +* **客户端的协议处理器**:用于处理 IO 事件 + + ```java + private final ClientRemotingProcessor clientRemotingProcessor; + ``` + +* 消息服务: + + ```java + private final PullMessageService pullMessageService; // 拉消息服务 + private final RebalanceService rebalanceService; // 消费者负载均衡服务 + private final ConsumerStatsManager consumerStatsManager; // 消费者状态管理 + ``` + +* 内部生产者实例:处理消费端**消息回退**,用该生产者发送回执消息 + + ```java + private final DefaultMQProducer defaultMQProducer; + ``` + +* 心跳次数统计: + + ```java + private final AtomicLong sendHeartbeatTimesTotal = new AtomicLong(0) + ``` + +* 公共配置类: + + ```java + public class ClientConfig { + // Namesrv 地址配置 + private String namesrvAddr = NameServerAddressUtils.getNameServerAddresses(); + // 客户端的 IP 地址 + private String clientIP = RemotingUtil.getLocalAddress(); + // 客户端实例名称 + private String instanceName = System.getProperty("rocketmq.client.name", "DEFAULT"); + // 客户端回调线程池的数量,平台核心数,8核16线程的电脑返回16 + private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); + // 命名空间 + protected String namespace; + protected AccessChannel accessChannel = AccessChannel.LOCAL; + + // 获取路由信息的间隔时间 30s + private int pollNameServerInterval = 1000 * 30; + // 客户端与 broker 之间的心跳周期 30s + private int heartbeatBrokerInterval = 1000 * 30; + // 消费者持久化消费的周期 5s + private int persistConsumerOffsetInterval = 1000 * 5; + private long pullTimeDelayMillsWhenException = 1000; + private boolean unitMode = false; + private String unitName; + // vip 通道,broker 启动时绑定两个端口,其中一个是 vip 通道 + private boolean vipChannelEnabled = Boolean.parseBoolean(); + // 语言,默认是 Java + private LanguageCode language = LanguageCode.JAVA; + } + ``` + +构造方法: + +* MQClientInstance 有参构造: + + ```java + public MQClientInstance(ClientConfig clientConfig, int instanceIndex, String clientId, RPCHook rpcHook) { + this.clientConfig = clientConfig; + this.instanceIndex = instanceIndex; + // Netty 相关的配置信息 + this.nettyClientConfig = new NettyClientConfig(); + // 平台核心数 + this.nettyClientConfig.setClientCallbackExecutorThreads(...); + this.nettyClientConfig.setUseTLS(clientConfig.isUseTLS()); + // 【创建客户端协议处理器】 + this.clientRemotingProcessor = new ClientRemotingProcessor(this); + // 创建 API 实现对象 + // 参数一:客户端网络配置 + // 参数二:客户端协议处理器,注册到客户端网络层 + // 参数三:rpcHook,注册到客户端网络层 + // 参数四:客户端配置 + this.mQClientAPIImpl = new MQClientAPIImpl(this.nettyClientConfig, this.clientRemotingProcessor, rpcHook, clientConfig); + + //... + // 内部生产者,指定内部生产者的组 + this.defaultMQProducer = new DefaultMQProducer(MixAll.CLIENT_INNER_PRODUCER_GROUP); + } + ``` + +* MQClientAPIImpl 有参构造: + + ```java + public MQClientAPIImpl(nettyClientConfig, clientRemotingProcessor, rpcHook, clientConfig) { + this.clientConfig = clientConfig; + topAddressing = new TopAddressing(MixAll.getWSAddr(), clientConfig.getUnitName()); + // 创建网络层对象,参数二为 null 说明客户端并不关心 channel event + this.remotingClient = new NettyRemotingClient(nettyClientConfig, null); + // 业务处理器 + this.clientRemotingProcessor = clientRemotingProcessor; + // 注册 RpcHook + this.remotingClient.registerRPCHook(rpcHook); + // ... + // 注册回退消息的请求码 + this.remotingClient.registerProcessor(RequestCode.PUSH_REPLY_MESSAGE_TO_CLIENT, this.clientRemotingProcessor, null); + } + ``` + + + +*** + + + +##### 成员方法 + +* start():启动方法 + + * `synchronized (this)`:加锁保证线程安全,保证只有一个实例对象启动 + * `this.mQClientAPIImpl.start()`:启动客户端网络层,底层调用 RemotingClient 类 + * `this.startScheduledTask()`:启动定时任务 + * `this.pullMessageService.start()`:启动拉取消息服务 + * `this.rebalanceService.start()`:启动负载均衡服务 + * `this.defaultMQProducer.getDefaultMQProducerImpl().start(false)`:启动内部生产者,参数为 false 代表不启动实例 + +* startScheduledTask():**启动定时任务**,调度线程池是单线程 + + * `if (null == this.clientConfig.getNamesrvAddr())`:Namesrv 地址是空,需要两分钟拉取一次 Namesrv 地址 + + * 定时任务 1:从 Namesrv 更新客户端本地的路由数据,周期 30 秒一次 + + ```java + // 获取生产者和消费者订阅的主题集合,遍历集合,对比从 namesrv 拉取最新的主题路由数据和本地数据,是否需要更新 + MQClientInstance.this.updateTopicRouteInfoFromNameServer(); + ``` + + * 定时任务 2:周期 30 秒一次,两个任务 + + * 清理下线的 Broker 节点,遍历客户端的 Broker 物理节点映射表,将所有主题数据都不包含的 Broker 物理节点清理掉,如果被清理的 Broker 下所有的物理节点都没有了,就将该 Broker 的映射数据删除掉 + * 向在线的所有的 Broker 发送心跳数据,**同步发送的方式**,返回值是 Broker 物理节点的版本号,更新版本映射表 + + ```java + MQClientInstance.this.cleanOfflineBroker(); + MQClientInstance.this.sendHeartbeatToAllBrokerWithLock(); + ``` + + ```java + // 心跳数据 + public class HeartbeatData extends RemotingSerializable { + // 客户端 ID ip@pid + private String clientID; + // 存储客户端所有生产者数据 + private Set producerDataSet = new HashSet(); + // 存储客户端所有消费者数据 + private Set consumerDataSet = new HashSet(); + } + ``` + + * 定时任务 3:消费者持久化消费数据,周期 5 秒一次 + + ```java + MQClientInstance.this.persistAllConsumerOffset(); + ``` + + * 定时任务 4:动态调整消费者线程池,周期 1 分钟一次 + + ```java + MQClientInstance.this.adjustThreadPool(); + ``` + +* updateTopicRouteInfoFromNameServer():**更新路由数据** + + * `if (isDefault && defaultMQProducer != null)`:需要默认数据 + + `topicRouteData = ...getDefaultTopicRouteInfoFromNameServer()`:从 Namesrv 获取默认的 TBW102 的路由数据 + + `int queueNums`:遍历所有队列,为每个读写队列设置较小的队列数 + + * `topicRouteData = ...getTopicRouteInfoFromNameServer(topic)`:需要**从 Namesrv 获取**路由数据(同步) + + * `old = this.topicRouteTable.get(topic)`:获取客户端实例本地的该主题的路由数据 + + * `boolean changed = topicRouteDataIsChange(old, topicRouteData)`:对比本地和最新下拉的数据是否一致 + + * `if (changed)`:不一致进入更新逻辑 + + `cloneTopicRouteData = topicRouteData.cloneTopicRouteData()`:克隆一份最新数据 + + `Update Pub info`:更新生产者信息 + + * `publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData)`:**将主题路由数据转化为发布数据** + * `impl.updateTopicPublishInfo(topic, publishInfo)`:生产者将主题的发布数据保存到它本地,方便发送消息使用 + + `Update sub info`:更新消费者信息 + + `this.topicRouteTable.put(topic, cloneTopicRouteData)`:将数据放入本地路由表 + + + +**** + + + +#### 网络通信 + +##### 成员属性 + +NettyRemotingClient 类负责客户端的网络通信 + +成员变量: + +* Netty 服务相关属性: + + ```java + private final NettyClientConfig nettyClientConfig; // 客户端的网络层配置 + private final Bootstrap bootstrap = new Bootstrap(); // 客户端网络层启动对象 + private final EventLoopGroup eventLoopGroupWorker; // 客户端网络层 Netty IO 线程组 + ``` + +* Channel 映射表: + + ```java + private final ConcurrentMap channelTables;// key 是服务器的地址,value 是通道对象 + private final Lock lockChannelTables = new ReentrantLock(); // 锁,控制并发安全 + ``` + +* 定时器:启动定时任务 + + ```java + private final Timer timer = new Timer("ClientHouseKeepingService", true) + ``` + +* 线程池: + + ```java + private ExecutorService publicExecutor; // 公共线程池 + private ExecutorService callbackExecutor; // 回调线程池,客户端发起异步请求,服务器的响应数据由回调线程池处理 + ``` + +* 事件监听器:客户端这里是 null + + ```java + private final ChannelEventListener channelEventListener; + ``` + +* Netty 配置对象: + + ```java + public class NettyClientConfig { + // 客户端工作线程数 + private int clientWorkerThreads = 4; + // 回调处理线程池 线程数:平台核心数 + private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); + // 单向请求并发数,默认 65535 + private int clientOnewaySemaphoreValue = NettySystemConfig.CLIENT_ONEWAY_SEMAPHORE_VALUE; + // 异步请求并发数,默认 65535 + private int clientAsyncSemaphoreValue = NettySystemConfig.CLIENT_ASYNC_SEMAPHORE_VALUE; + // 客户端连接服务器的超时时间限制 3秒 + private int connectTimeoutMillis = 3000; + // 客户端未激活周期,60s(指定时间内 ch 未激活,需要关闭) + private long channelNotActiveInterval = 1000 * 60; + // 客户端与服务器 ch 最大空闲时间 2分钟 + private int clientChannelMaxIdleTimeSeconds = 120; + + // 底层 Socket 写和收 缓冲区的大小 65535 64k + private int clientSocketSndBufSize = NettySystemConfig.socketSndbufSize; + private int clientSocketRcvBufSize = NettySystemConfig.socketRcvbufSize; + // 客户端 netty 是否启动内存池 + private boolean clientPooledByteBufAllocatorEnable = false; + // 客户端是否超时关闭 Socket 连接 + private boolean clientCloseSocketIfTimeout = false; + } + ``` + +构造方法 + +* 无参构造: + + ```java + public NettyRemotingClient(final NettyClientConfig nettyClientConfig) { + this(nettyClientConfig, null); + } + ``` + +* 有参构造: + + ```java + public NettyRemotingClient(nettyClientConfig, channelEventListener) { + // 父类创建了2个信号量,1、控制单向请求的并发度,2、控制异步请求的并发度 + super(nettyClientConfig.getClientOnewaySemaphoreValue(), nettyClientConfig.getClientAsyncSemaphoreValue()); + this.nettyClientConfig = nettyClientConfig; + this.channelEventListener = channelEventListener; + + // 创建公共线程池 + int publicThreadNums = nettyClientConfig.getClientCallbackExecutorThreads(); + if (publicThreadNums <= 0) { + publicThreadNums = 4; + } + this.publicExecutor = Executors.newFixedThreadPool(publicThreadNums,); + + // 创建 Netty IO 线程,1个线程 + this.eventLoopGroupWorker = new NioEventLoopGroup(1, ); + + if (nettyClientConfig.isUseTLS()) { + sslContext = TlsHelper.buildSslContext(true); + } + } + ``` + + + +**** + + + +##### 成员方法 + +* start():启动方法 + + ```java + public void start() { + // channel pipeline 内的 handler 使用的线程资源,默认 4 个 + this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(); + // 配置 netty 客户端启动类对象 + Bootstrap handler = this.bootstrap.group(this.eventLoopGroupWorker).channel(NioSocketChannel.class) + //... + .handler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + // 加几个handler + pipeline.addLast( + // 服务端的数据,都会来到这个 + new NettyClientHandler()); + } + }); + // 注意 Bootstrap 只是配置好客户端的元数据了,【在这里并没有创建任何 channel 对象】 + // 定时任务 扫描 responseTable 中超时的 ResponseFuture,避免客户端线程长时间阻塞 + this.timer.scheduleAtFixedRate(() -> { + NettyRemotingClient.this.scanResponseTable(); + }, 1000 * 3, 1000); + // 这里是 null,不启动 + if (this.channelEventListener != null) { + this.nettyEventExecutor.start(); + } + } + ``` + +* 单向通信: + + ```java + public RemotingCommand invokeSync(String addr, final RemotingCommand request, long timeoutMillis) { + // 开始时间 + long beginStartTime = System.currentTimeMillis(); + // 获取或者创建客户端与服务端(addr)的通道 channel + final Channel channel = this.getAndCreateChannel(addr); + // 条件成立说明客户端与服务端 channel 通道正常,可以通信 + if (channel != null && channel.isActive()) { + try { + // 执行 rpcHook 拓展点 + doBeforeRpcHooks(addr, request); + // 计算耗时,如果当前耗时已经超过 timeoutMillis 限制,则直接抛出异常,不再进行系统通信 + long costTime = System.currentTimeMillis() - beginStartTime; + if (timeoutMillis < costTime) { + throw new RemotingTimeoutException("invokeSync call timeout"); + } + // 参数1:客户端-服务端通道channel + // 参数二:网络层传输对象,封装着请求数据 + // 参数三:剩余的超时限制 + RemotingCommand response = this.invokeSyncImpl(channel, request, ...); + // 后置处理 + doAfterRpcHooks(RemotingHelper.parseChannelRemoteAddr(channel), request, response); + // 返回响应数据 + return response; + } catch (RemotingSendRequestException e) {} + } else { + this.closeChannel(addr, channel); + throw new RemotingConnectException(addr); + } + } + ``` + + + + + + +*** diff --git a/Java.md b/Java.md index c9daa70..b0a6075 100644 --- a/Java.md +++ b/Java.md @@ -73,7 +73,7 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, - 这种类型主要使用在需要比较大整数的系统上 - 默认值是 **` 0L`** - 例子: `long a = 100000L,Long b = -200000L` - "L"理论上不分大小写,但是若写成"l"容易与数字"1"混淆,不容易分辩,所以最好大写 + L 理论上不分大小写,但是若写成 I 容易与数字 1 混淆,不容易分辩,所以最好大写 **float:** From 3bca1287329a4553f1399ed6d2a3282378100f2c Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 9 Jan 2022 22:50:25 +0800 Subject: [PATCH 103/168] Update Java Notes --- Frame.md | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/Frame.md b/Frame.md index 6f84a04..183cc5f 100644 --- a/Frame.md +++ b/Frame.md @@ -4504,7 +4504,7 @@ NameServer 是一个简单的 Topic 路由注册中心,支持 Broker 的动态 NameServer 主要包括两个功能: -* Broker 路由管理,NameServer 接受 Broker 集群的注册信息,并保存下来作为路由信息的基本数据,提供**心跳检测机制**检查 Broker 活性,每 10 秒清除一次两小时没有活跃的 Broker +* Broker 路由管理,NameServer 接受 Broker 集群的注册信息,保存下来作为路由信息的基本数据,提供**心跳检测机制**检查 Broker 是否还存活,每 10 秒清除一次两小时没有活跃的 Broker * 路由信息管理,每个 NameServer 将保存关于 Broker 集群的整个路由信息和用于客户端查询的队列信息,然后 Producer 和 Conumser 通过 NameServer 就可以知道整个 Broker 集群的路由信息,从而进行消息的投递和消费 NameServer 特点: @@ -5589,6 +5589,37 @@ NettyRemotingAbstract#processResponseCommand:处理响应的数据 +#### 路由信息 + +##### 信息管理 + +RouteInfoManager 类负责管理路由信息,NamesrvController 的构造方法中创建该类的实例对象,管理服务端的路由数据 + +```java +public class RouteInfoManager { + // Broker 两个小时不活跃,视为离线,被定时任务删除 + private final static long BROKER_CHANNEL_EXPIRED_TIME = 1000 * 60 * 2; + // 读写锁,保证线程安全 + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + // 主题队列数据,一个主题对应多个队列 + private final HashMap> topicQueueTable; + // Broker 数据列表 + private final HashMap brokerAddrTable; + // 集群 + private final HashMap> clusterAddrTable; + // Broker 存活信息 + private final HashMap brokerLiveTable; + // 服务过滤 + private final HashMap/* Filter Server */> filterServerTable; +} +``` + + + +*** + + + ##### 路由注册 DefaultRequestProcessor REGISTER_BROKER 方法解析: From 411b33b4315e1edc5a5d77749f6028ac442cc01b Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 15 Jan 2022 18:55:18 +0800 Subject: [PATCH 104/168] Update Java Notes --- DB.md | 4 ++++ Frame.md | 8 ++++---- Java.md | 4 ++-- Web.md | 6 +++--- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/DB.md b/DB.md index c815155..41db319 100644 --- a/DB.md +++ b/DB.md @@ -4827,6 +4827,10 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); +参考文章:https://mp.weixin.qq.com/s/B_M09dzLe9w7cT46rdGIeQ + + + *** diff --git a/Frame.md b/Frame.md index 183cc5f..15ea0fc 100644 --- a/Frame.md +++ b/Frame.md @@ -3617,12 +3617,12 @@ RocketMQ 主要由 Producer、Broker、Consumer 三部分组成,其中 Produce 消息发送者步骤分析: -1. 创建消息生产者 producer,并制定生产者组名 +1. 创建消息生产者 Producer,并制定生产者组名 2. 指定 Nameserver 地址 -3. 启动 producer +3. 启动 Producer 4. 创建消息对象,指定主题 Topic、Tag 和消息体 5. 发送消息 -6. 关闭生产者 producer +6. 关闭生产者 Producer 消息消费者步骤分析: @@ -3630,7 +3630,7 @@ RocketMQ 主要由 Producer、Broker、Consumer 三部分组成,其中 Produce 2. 指定 Nameserver 地址 3. 订阅主题 Topic 和 Tag 4. 设置回调函数,处理消息 -5. 启动消费者 consumer +5. 启动消费者 Consumer diff --git a/Java.md b/Java.md index b0a6075..125e703 100644 --- a/Java.md +++ b/Java.md @@ -2551,7 +2551,7 @@ JDK 1.8:当一个字符串调用 intern() 方法时,如果 String Pool 中 * 存在一个字符串和该字符串值相等,就会返回 String Pool 中字符串的引用(需要变量接收) * 不存在,会把对象的**引用地址**复制一份放入串池,并返回串池中的引用地址,前提是堆内存有该对象,因为 Pool 在堆中,为了节省内存不再创建新对象 -JDK 1.6:将这个字符串对象尝试放入串池,如果有就不放入,返回已有的串池中的对象的地址;如果没有会把此对象复制一份,放入串池,把串池中的对象返回 +JDK 1.6:将这个字符串对象尝试放入串池,如果有就不放入,返回已有的串池中的对象的引用;如果没有会把此对象复制一份,放入串池,把串池中的对象返回 ```java public class Demo { @@ -2600,7 +2600,7 @@ String s = new String("ab"); -##### 面试问题 +##### 常见问题 问题一: diff --git a/Web.md b/Web.md index 5262ce7..3236ec1 100644 --- a/Web.md +++ b/Web.md @@ -2235,13 +2235,13 @@ HTTPS 工作流程:服务器端的公钥和私钥,用来进行非对称加 Cookie: Idea-b77ddca6=4bc282fe-febf-4fd1-b6c9-72e9e0f381e8 ``` - * 面试题:**Get 和POST比较** + * **Get 和 POST 比较** 作用:GET 用于获取资源,而 POST 用于传输实体主体 - 参数:GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中。不能因为 POST 参数存储在实体主体中就认为它的安全性更高,因为照样可以通过一些抓包工具(Fiddler)查看 + 参数:GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中(GET 也有请求体,POST 也可以通过 URL 传输参数)。不能因为 POST 参数存储在实体主体中就认为它的安全性更高,因为照样可以通过一些抓包工具(Fiddler)查看 - 安全:安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的。GET方法是安全的,而POST不是,因为 POST 的目的是传送实体主体内容 + 安全:安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的。GET 方法是安全的,而 POST 不是,因为 POST 的目的是传送实体主体内容 * 安全的方法除了 GET 之外还有:HEAD、OPTIONS * 不安全的方法除了 POST 之外还有 PUT、DELETE From 3bdd11667b7b40fb65d3f531843fa1c2a575b46b Mon Sep 17 00:00:00 2001 From: Seazean Date: Fri, 21 Jan 2022 00:03:53 +0800 Subject: [PATCH 105/168] Update Java Notes --- DB.md | 44 +++- Frame.md | 615 ++++++++++++++++++++++++++++++++++++++----------------- Java.md | 91 ++++---- 3 files changed, 508 insertions(+), 242 deletions(-) diff --git a/DB.md b/DB.md index 41db319..61b9c13 100644 --- a/DB.md +++ b/DB.md @@ -5702,19 +5702,18 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 ### 隔离级别 +#### 四种级别 + 事务的隔离级别:多个客户端操作时,各个客户端的事务之间应该是隔离的,**不同的事务之间不该互相影响**,而如果多个事务操作同一批数据时,则需要设置不同的隔离级别,否则就会产生问题。 隔离级别分类: | 隔离级别 | 名称 | 会引发的问题 | 数据库默认隔离级别 | | ---------------- | -------- | ---------------------- | ------------------- | -| read uncommitted | 读未提交 | 脏读、不可重复读、幻读 | | -| read committed | 读已提交 | 不可重复读、幻读 | Oracle / SQL Server | -| repeatable read | 可重复读 | 幻读 | MySQL | -| serializable | 可串行化 | 无 | | - -* 串行化:让所有事务按顺序单独执行,写操作会加写锁,读操作会加读锁 -* 可串行化:让所有操作相同数据的事务顺序执行,通过加锁实现 +| Read Uncommitted | 读未提交 | 脏读、不可重复读、幻读 | | +| Read Committed | 读已提交 | 不可重复读、幻读 | Oracle / SQL Server | +| Repeatable Read | 可重复读 | 幻读 | MySQL | +| Serializable | 可串行化 | 无 | | 一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差 @@ -5745,6 +5744,31 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 +*** + + + +#### 加锁分析 + +InnoDB 存储引擎支持事务,所以加锁分析是基于该存储引擎 + +* Read Uncommitted 级别,任何操作都不会加锁 + +* Read Committed 级别,增删改操作会加写锁(行锁),读操作不加锁 + + MySQL 做了优化,在 Server 层过滤条件时发现不满足的记录会调用 unlock_row 方法释放该记录的行锁,保证最后只有满足条件的记录加锁,但是扫表过程中每条记录的**加锁操作不能省略**。所以对数据量很大的表做批量修改时,如果无法使用相应的索引,需要在Server 过滤数据时就会特别慢,出现虽然没有修改某些行的数据,但是还是被锁住了的现象,这种情况同样适用于 RR + +* Repeatable Read 级别,增删改操作会加写锁,读操作不加锁。因为读写锁不兼容,加了写锁后其他事务就无法修改数据,影响了并发性能,为了保证隔离性和并发性,MySQL 通过 MVCC 解决了读写冲突。RR 级别下的锁有很多种,锁机制章节详解 + +* Serializable 级别,读加共享锁,写加排他锁,读写互斥,使用的悲观锁的理论,实现简单,数据更加安全,但是并发能力非常差 + + * 串行化:让所有事务按顺序单独执行,写操作会加写锁,读操作会加读锁 + * 可串行化:让所有操作相同数据的事务顺序执行,通过加锁实现 + + + +参考文章:https://tech.meituan.com/2014/08/20/innodb-lock.html + *** @@ -6636,7 +6660,7 @@ InnoDB 与 MyISAM 的最大不同有两点:一是支持事务;二是采用 - 共享锁 (S):又称为读锁,简称 S 锁,多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改 - 排他锁 (X):又称为写锁,简称 X 锁,不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,只有获取排他锁的事务是可以对数据读取和修改 -对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会**自动给涉及数据集加排他锁**(行锁),在 commit 的时候会自动释放(在事务中加的锁,会**在事务中止或提交时自动释放**);对于普通 SELECT 语句,不会加任何锁 +RR 隔离界别下,对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会**自动给涉及数据集加排他锁**(行锁),在 commit 的时候会自动释放(在事务中加的锁,会**在事务中止或提交时自动释放**);对于普通 SELECT 语句,不会加任何锁(只是针对 InnoDB 层来说的,因为在 Server 层会加 MDL 读锁),通过 MVCC 防止冲突 锁的兼容性: @@ -6654,7 +6678,7 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 -**** +*** @@ -6999,6 +7023,8 @@ lock_id 是锁 id;lock_trx_id 为事务 id;lock_mode 为 X 代表排它锁 ### 乐观锁 +悲观锁:在整个数据处理过程中,将数据处于锁定状态,为了保证事务的隔离性,就需要一致性锁定读。读取数据时给加锁,其它事务无法修改这些数据,修改删除数据时也加锁,其它事务同样无法读取这些数据 + 悲观锁和乐观锁使用前提: - 对于读的操作远多于写的操作的时候,一个更新操作加锁会阻塞所有的读取操作,降低了吞吐量,最后需要释放锁,锁是需要一些开销的,这时候可以选择乐观锁 diff --git a/Frame.md b/Frame.md index 15ea0fc..c126bb8 100644 --- a/Frame.md +++ b/Frame.md @@ -3818,7 +3818,7 @@ public class Consumer { - 全局顺序:对于指定的一个 Topic,所有消息按照严格的先入先出(FIFO)的顺序进行发布和消费。 适用于性能要求不高,所有的消息严格按照 FIFO 原则进行消息发布和消费的场景 - 分区顺序:对于指定的一个 Topic,所有消息根据 sharding key 进行分区,同一个分组内的消息按照严格的 FIFO 顺序进行发布和消费。Sharding key 是顺序消息中用来区分不同分区的关键字段,和普通消息的 Key 是完全不同的概念。 适用于性能要求高,以 sharding key 作为分区字段,在同一个区中严格的按照 FIFO 原则进行消息发布和消费的场景 -在默认的情况下消息发送会采取 Round Robin 轮询方式把消息发送到不同的 queue(分区队列),而消费消息是从多个 queue 上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个 queue 中,消费的时候只从这个 queue 上依次拉取,则就保证了顺序。当发送和消费参与的 queue 只有一个,则是全局有序;如果多个queue 参与,则为分区有序,即相对每个 queue,消息都是有序的 +在默认的情况下消息发送会采取 Round Robin 轮询方式把消息发送到不同的 queue(分区队列),而消费消息是从多个 queue 上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个 queue 中,消费的时候只从这个 queue 上依次拉取,则就保证了顺序。当**发送和消费参与的 queue 只有一个**,则是全局有序;如果多个queue 参与,则为分区有序,即相对每个 queue,消息都是有序的 @@ -4346,7 +4346,7 @@ RocketMQ 的具体实现策略:如果写入的是事务消息,对消息的 T -##### OP消息 +##### OP 消息 一阶段写入不可见的消息后,二阶段操作: @@ -4579,6 +4579,106 @@ At least Once:至少一次,指每个消息必须投递一次,Consumer 先 + + +### 存储机制 + +#### 存储结构 + +RocketMQ 中 Broker 负责存储消息转发消息,所以以下的结构是存储在 Broker Server 上的,生产者和消费者与 Broker 进行消息的收发是通过主题对应的 Message Queue 完成,类似于通道 + +RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成 的,CommitLog 是消息真正的**物理存储**文件,ConsumeQueue 是消息的逻辑队列,类似数据库的**索引节点**,存储的是指向物理存储的地址。**每个 Topic 下的每个 Message Queue 都有一个对应的 ConsumeQueue 文件** + +每条消息都会有对应的索引信息,Consumer 通过 ConsumeQueue 这个结构来读取消息实体内容 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息存储结构.png) + +* CommitLog:消息主体以及元数据的存储主体,存储 Producer 端写入的消息内容,消息内容不是定长的。消息主要是顺序写入日志文件,单个文件大小默认 1G,偏移量代表下一次写入的位置,当文件写满了就继续写入下一个文件 +* ConsumerQueue:消息消费队列,存储消息在 CommitLog 的索引。RocketMQ 消息消费时要遍历 CommitLog 文件,并根据主题 Topic 检索消息,这是非常低效的。引入 ConsumeQueue 作为消费消息的索引,保存了指定 Topic 下的队列消息在 CommitLog 中的起始物理偏移量 offset,消息大小 size 和消息 Tag 的 HashCode 值,每个 ConsumeQueue 文件大小约 5.72M +* IndexFile:为了消息查询提供了一种通过 Key 或时间区间来查询消息的方法,通过 IndexFile 来查找消息的方法不影响发送与消费消息的主流程。IndexFile 的底层存储为在文件系统中实现的 HashMap 结构,故 RocketMQ 的索引文件其底层实现为 **hash 索引** + +RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所有的队列共用一个日志数据文件(CommitLog)来存储。混合型存储结构(多个 Topic 的消息实体内容都存储于一个 CommitLog 中)**针对 Producer 和 Consumer 分别采用了数据和索引部分相分离的存储结构**,Producer 发送消息至 Broker 端,然后 Broker 端使用同步或者异步的方式对消息刷盘持久化,保存至 CommitLog 中。只要消息被持久化至磁盘文件 CommitLog 中,Producer 发送的消息就不会丢失,Consumer 也就肯定有机会去消费这条消息 + +服务端支持长轮询模式,当消费者无法拉取到消息后,可以等下一次消息拉取,Broker 允许等待 30s 的时间,只要这段时间内有新消息到达,将直接返回给消费端。RocketMQ 的具体做法是,使用 Broker 端的后台服务线程 ReputMessageService 不停地分发请求并异步构建 ConsumeQueue(逻辑消费队列)和 IndexFile(索引文件)数据 + + + +**** + + + +#### 存储优化 + +##### 内存映射 + +操作系统分为用户态和内核态,文件操作、网络操作需要涉及这两种形态的切换,需要进行数据复制。一台服务器把本机磁盘文件的内容发送到客户端,分为两个步骤: + +* read:读取本地文件内容 + +* write:将读取的内容通过网络发送出去 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-文件与网络操作.png) + +补充:Prog → NET → I/O → 零拷贝部分的笔记详解相关内容 + +通过使用 mmap 的方式,可以省去向用户态的内存复制,RocketMQ 充分利用**零拷贝技术**,提高消息存盘和网络发送的速度。 + +RocketMQ 通过 MappedByteBuffer 对文件进行读写操作,利用了 NIO 中的 FileChannel 模型将磁盘上的物理文件直接映射到用户态的内存地址中,将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率 + +MappedByteBuffer 内存映射的方式**限制**一次只能映射 1.5~2G 的文件至用户态的虚拟内存,所以 RocketMQ 默认设置单个 CommitLog 日志数据文件为 1G。RocketMQ 的文件存储使用定长结构来存储,方便一次将整个文件映射至内存 + + + +*** + + + +##### 页缓存 + +页缓存(PageCache)是 OS 对文件的缓存,每一页的大小通常是 4K,用于加速对文件的读写。程序对文件进行顺序读写的速度几乎接近于内存的读写速度,就是因为 OS 将一部分的内存用作 PageCache,**对读写访问操作进行了性能优化** + +* 对于数据的写入,OS 会先写入至 Cache 内,随后通过异步的方式由 pdflush 内核线程将 Cache 内的数据刷盘至物理磁盘上 +* 对于数据的读取,如果一次读取文件时出现未命中 PageCache 的情况,OS 从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行预读取(局部性原理,最大 128K) + +在 RocketMQ 中,ConsumeQueue 逻辑消费队列存储的数据较少,并且是顺序读取,在 PageCache 机制的预读取作用下,Consume Queue 文件的读性能几乎接近读内存,即使在有消息堆积情况下也不会影响性能。但是 CommitLog 消息存储的日志数据文件读取内容时会产生较多的随机访问读取,严重影响性能。选择合适的系统 IO 调度算法和固态硬盘,比如设置调度算法为 Deadline,随机读的性能也会有所提升 + + + +*** + + + +#### 刷盘机制 + +两种持久化的方案: + +* 关系型数据库 DB:IO 读写性能比较差,如果 DB 出现故障,则 MQ 的消息就无法落盘存储导致线上故障,可靠性不高 +* 文件系统:消息刷盘至所部署虚拟机/物理机的文件系统来做持久化,分为异步刷盘和同步刷盘两种模式。消息刷盘为消息存储提供了一种高效率、高可靠性和高性能的数据持久化方式,除非部署 MQ 机器本身或是本地磁盘挂了,一般不会出现无法持久化的问题 + +RocketMQ 采用文件系统的方式,无论同步还是异步刷盘,都使用**顺序 IO**,因为磁盘的顺序读写要比随机读写快很多 + +* 同步刷盘:只有在消息真正持久化至磁盘后 RocketMQ 的 Broker 端才会真正返回给 Producer 端一个成功的 ACK 响应,保障 MQ消息的可靠性,但是性能上会有较大影响,一般适用于金融业务应用该模式较多 + +* 异步刷盘:利用 OS 的 PageCache,只要消息写入内存 PageCache 即可将成功的 ACK 返回给 Producer 端,降低了读写延迟,提高了 MQ 的性能和吞吐量。消息刷盘采用**后台异步线程**提交的方式进行,当内存里的消息量积累到一定程度时,触发写磁盘动作 + +通过 Broker 配置文件里的 flushDiskType 参数设置采用什么方式,可以配置成 SYNC_FLUSH、ASYNC_FLUSH 中的一个 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-刷盘机制.png) + + + +官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md + + + + + +**** + + + + + ### 集群设计 #### 集群模式 @@ -4738,8 +4838,6 @@ latencyFaultTolerance 机制是实现消息发送高可用的核心关键所在 #### 原理解析 -==todo:暂时 copy 官方文档,学习源码后更新,真想搞懂过程还需要研究一下源码== - 在 Consumer 启动后,会通过定时任务不断地向 RocketMQ 集群中的所有 Broker 实例发送心跳包。Broke r端在收到 Consumer 的心跳消息后,会将它维护 在ConsumerManager 的本地缓存变量 consumerTable,同时并将封装后的客户端网络通道信息保存在本地缓存变量 channelInfoTable 中,为 Consumer 端的负载均衡提供可以依据的元数据信息 Consumer 端实现负载均衡的核心类 **RebalanceImpl** @@ -4770,13 +4868,15 @@ Consumer 端实现负载均衡的核心类 **RebalanceImpl** + + **** -### 消息机制 +### 消息查询 -#### 消息查询 +#### 查询方式 RocketMQ 支持按照两种维度进行消息查询:按照 Message ID 查询消息、按照 Message Key 查询消息 @@ -4794,6 +4894,35 @@ RocketMQ 支持按照两种维度进行消息查询:按照 Message ID 查询 +#### 索引机制 + +RocketMQ 的索引文件逻辑结构,类似 JDK 中 HashMap 的实现,具体结构如下: + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-IndexFile索引文件.png) + +IndexFile 文件的存储在 `$HOME\store\index${fileName}`,文件名 fileName 是以创建时的时间戳命名,文件大小是固定的,等于 `40+500W*4+2000W*20= 420000040` 个字节大小。如果消息的 properties 中设置了 UNIQ_KEY 这个属性,就用 `topic + “#” + UNIQ_KEY` 作为 key 来做写入操作;如果消息设置了 KEYS 属性(多个 KEY 以空格分隔),也会用 `topic + “#” + KEY` 来做索引 + +整个 Index File 的结构如图,40 Byte 的 Header 用于保存一些总的统计信息,`4*500W` 的 Slot Table 并不保存真正的索引数据,而是保存每个槽位对应的单向链表的**头指针**,即一个 Index File 可以保存 2000W 个索引,`20*2000W` 是**真正的索引数据** + +索引数据包含了 Key Hash/CommitLog Offset/Timestamp/NextIndex offset 这四个字段,一共 20 Byte + +* NextIndex offset 即前面读出来的 slotValue,如果有 hash 冲突,就可以用这个字段将所有冲突的索引用链表的方式串起来 +* Timestamp 记录的是消息 storeTimestamp 之间的差,并不是一个绝对的时间 + + + +参考文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md + + + + + +*** + + + +### 消息重试 + #### 消息重投 生产者在发送消息时,同步消息和异步消息失败会重投,oneway 没有任何保证。消息重投保证消息尽可能发送成功、不丢失,但当出现消息量大、网络抖动时,可能会造成消息重复;生产者主动重发、Consumer 负载变化也会导致重复消息。 @@ -4822,7 +4951,7 @@ Consumer 消费消息失败后,提供了一种重试机制,令消息再消 - 由于消息本身的原因,例如反序列化失败,消息数据本身无法处理等。这种错误通常需要跳过这条消息,再消费其它消息,而这条失败的消息即使立刻重试消费,99% 也不成功,所以需要提供一种定时重试机制,即过 10秒 后再重试 - 由于依赖的下游应用服务不可用,例如 DB 连接不可用,外系统网络不可达等。这种情况即使跳过当前失败的消息,消费其他消息同样也会报错,这种情况建议应用 sleep 30s,再消费下一条消息,这样可以减轻 Broker 重试消息的压力 -RocketMQ 会为每个消费组都设置一个 Topic 名称为 `%RETRY%+consumerGroup` 的重试队列(这个 Topic 的重试队列是针对消费组,而不是针对每个 Topic 设置的),用于暂时保存因为各种异常而导致 Consumer 端无法消费的消息 +RocketMQ 会为每个消费组都设置一个 Topic 名称为 `%RETRY%+consumerGroup` 的重试队列(这个 Topic 的重试队列是**针对消费组**,而不是针对每个 Topic 设置的),用于暂时保存因为各种异常而导致 Consumer 端无法消费的消息 * 顺序消息的重试,当消费者消费消息失败后,消息队列 RocketMQ 会自动不断进行消息重试(每次间隔时间为 1 秒),这时应用会出现消息消费被阻塞的情况。所以在使用顺序消息时,必须保证应用能够及时监控并处理消费失败的情况,避免阻塞现象的发生 @@ -5714,100 +5843,6 @@ RouteInfoManager#registerBroker:注册 Broker 的信息 ### 存储端 -#### 存储机制 - -##### 存储结构 - -RocketMQ 中 Broker 负责存储消息转发消息,所以以下的结构是存储在 Broker Server 上的,生产者和消费者与 Broker 进行消息的收发是通过主题对应的 Message Queue 完成,类似于通道 - -RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成 的,CommitLog 是消息真正的**物理存储**文件,ConsumeQueue 是消息的逻辑队列,类似数据库的**索引节点**,存储的是指向物理存储的地址。**每个 Topic 下的每个 Message Queue 都有一个对应的 ConsumeQueue 文件** - -每条消息都会有对应的索引信息,Consumer 通过 ConsumeQueue 这个结构来读取消息实体内容 - -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息存储结构.png) - -* CommitLog:消息主体以及元数据的存储主体,存储 Producer 端写入的消息内容,消息内容不是定长的。消息主要是顺序写入日志文件,单个文件大小默认 1G,偏移量代表下一次写入的位置,当文件写满了就继续写入下一个文件 -* ConsumerQueue:消息消费队列,存储消息在 CommitLog 的索引。RocketMQ 消息消费时要遍历 CommitLog 文件,并根据主题 Topic 检索消息,这是非常低效的。引入 ConsumeQueue 作为消费消息的索引,保存了指定 Topic 下的队列消息在 CommitLog 中的起始物理偏移量 offset,消息大小 size 和消息 Tag 的 HashCode 值,每个 ConsumeQueue 文件大小约 5.72M -* IndexFile:为了消息查询提供了一种通过 Key 或时间区间来查询消息的方法,通过 IndexFile 来查找消息的方法不影响发送与消费消息的主流程。IndexFile 的底层存储为在文件系统中实现的 HashMap 结构,故 RocketMQ 的索引文件其底层实现为 **hash 索引** - -RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所有的队列共用一个日志数据文件(CommitLog)来存储。混合型存储结构(多个 Topic 的消息实体内容都存储于一个 CommitLog 中)**针对 Producer 和 Consumer 分别采用了数据和索引部分相分离的存储结构**,Producer 发送消息至 Broker 端,然后 Broker 端使用同步或者异步的方式对消息刷盘持久化,保存至 CommitLog 中。只要消息被持久化至磁盘文件 CommitLog 中,Producer 发送的消息就不会丢失,Consumer 也就肯定有机会去消费这条消息 - -服务端支持长轮询模式,当消费者无法拉取到消息后,可以等下一次消息拉取,Broker 允许等待 30s 的时间,只要这段时间内有新消息到达,将直接返回给消费端。RocketMQ 的具体做法是,使用 Broker 端的后台服务线程 ReputMessageService 不停地分发请求并异步构建 ConsumeQueue(逻辑消费队列)和 IndexFile(索引文件)数据 - - - -**** - - - -##### 存储优化 - -###### 内存映射 - -操作系统分为用户态和内核态,文件操作、网络操作需要涉及这两种形态的切换,需要进行数据复制。一台服务器把本机磁盘文件的内容发送到客户端,分为两个步骤: - -* read:读取本地文件内容 - -* write:将读取的内容通过网络发送出去 - -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-文件与网络操作.png) - -补充:Prog → NET → I/O → 零拷贝部分的笔记详解相关内容 - -通过使用 mmap 的方式,可以省去向用户态的内存复制,RocketMQ 充分利用**零拷贝技术**,提高消息存盘和网络发送的速度。 - -RocketMQ 通过 MappedByteBuffer 对文件进行读写操作,利用了 NIO 中的 FileChannel 模型将磁盘上的物理文件直接映射到用户态的内存地址中,将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率 - -MappedByteBuffer 内存映射的方式**限制**一次只能映射 1.5~2G 的文件至用户态的虚拟内存,所以 RocketMQ 默认设置单个 CommitLog 日志数据文件为 1G。RocketMQ 的文件存储使用定长结构来存储,方便一次将整个文件映射至内存 - - - -*** - - - -###### 页缓存 - -页缓存(PageCache)是 OS 对文件的缓存,每一页的大小通常是 4K,用于加速对文件的读写。程序对文件进行顺序读写的速度几乎接近于内存的读写速度,就是因为 OS 将一部分的内存用作 PageCache,**对读写访问操作进行了性能优化** - -* 对于数据的写入,OS 会先写入至 Cache 内,随后通过异步的方式由 pdflush 内核线程将 Cache 内的数据刷盘至物理磁盘上 -* 对于数据的读取,如果一次读取文件时出现未命中 PageCache 的情况,OS 从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行预读取(局部性原理,最大 128K) - -在 RocketMQ 中,ConsumeQueue 逻辑消费队列存储的数据较少,并且是顺序读取,在 PageCache 机制的预读取作用下,Consume Queue 文件的读性能几乎接近读内存,即使在有消息堆积情况下也不会影响性能。但是 CommitLog 消息存储的日志数据文件读取内容时会产生较多的随机访问读取,严重影响性能。选择合适的系统 IO 调度算法和固态硬盘,比如设置调度算法为 Deadline,随机读的性能也会有所提升 - - - -*** - - - -##### 刷盘机制 - -两种持久化的方案: - -* 关系型数据库 DB:IO 读写性能比较差,如果 DB 出现故障,则 MQ 的消息就无法落盘存储导致线上故障,可靠性不高 -* 文件系统:消息刷盘至所部署虚拟机/物理机的文件系统来做持久化,分为异步刷盘和同步刷盘两种模式。消息刷盘为消息存储提供了一种高效率、高可靠性和高性能的数据持久化方式,除非部署 MQ 机器本身或是本地磁盘挂了,一般不会出现无法持久化的问题 - -RocketMQ 采用文件系统的方式,无论同步还是异步刷盘,都使用**顺序 IO**,因为磁盘的顺序读写要比随机读写快很多 - -* 同步刷盘:只有在消息真正持久化至磁盘后 RocketMQ 的 Broker 端才会真正返回给 Producer 端一个成功的 ACK 响应,保障 MQ消息的可靠性,但是性能上会有较大影响,一般适用于金融业务应用该模式较多 - -* 异步刷盘:利用 OS 的 PageCache,只要消息写入内存 PageCache 即可将成功的 ACK 返回给 Producer 端,降低了读写延迟,提高了 MQ 的性能和吞吐量。消息刷盘采用**后台异步线程**提交的方式进行,当内存里的消息量积累到一定程度时,触发写磁盘动作 - -通过 Broker 配置文件里的 flushDiskType 参数设置采用什么方式,可以配置成 SYNC_FLUSH、ASYNC_FLUSH 中的一个 - -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-刷盘机制.png) - - - -官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md - - - -*** - - - #### MappedFile ##### 成员属性 @@ -6346,31 +6381,6 @@ ConsumeQueue 启动阶段方法: #### IndexFile -##### 索引机制 - -RocketMQ 的索引文件逻辑结构,类似 JDK 中 HashMap 的实现,具体结构如下: - -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-IndexFile索引文件.png) - -IndexFile 文件的存储在 `$HOME\store\index${fileName}`,文件名 fileName 是以创建时的时间戳命名,文件大小是固定的,等于 `40+500W*4+2000W*20= 420000040` 个字节大小。如果消息的 properties 中设置了 UNIQ_KEY 这个属性,就用 `topic + “#” + UNIQ_KEY` 作为 key 来做写入操作;如果消息设置了 KEYS 属性(多个 KEY 以空格分隔),也会用 `topic + “#” + KEY` 来做索引 - -整个 Index File 的结构如图,40 Byte 的 Header 用于保存一些总的统计信息,`4*500W` 的 Slot Table 并不保存真正的索引数据,而是保存每个槽位对应的单向链表的**头指针**,即一个 Index File 可以保存 2000W 个索引,`20*2000W` 是**真正的索引数据** - -索引数据包含了 Key Hash/CommitLog Offset/Timestamp/NextIndex offset 这四个字段,一共 20 Byte - -* NextIndex offset 即前面读出来的 slotValue,如果有 hash 冲突,就可以用这个字段将所有冲突的索引用链表的方式串起来 -* Timestamp 记录的是消息 storeTimestamp 之间的差,并不是一个绝对的时间 - - - -参考文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md - - - -*** - - - ##### 成员属性 IndexFile 类成员属性 @@ -7355,6 +7365,223 @@ TopicPublishInfo 类用来存储路由信息 ### 消费者 +#### 消费者类 + +##### 默认消费 + +DefaultMQPushConsumer 类是默认的消费者类 + +成员变量: + +* 消费者实现类: + + ```java + protected final transient DefaultMQPushConsumerImpl defaultMQPushConsumerImpl; + ``` + +* 消费属性: + + ```java + private String consumerGroup; // 消费者组 + private MessageModel messageModel = MessageModel.CLUSTERING; // 消费模式,默认集群模式 + ``` + +* 订阅信息:key 是主题,value 是过滤表达式,一般是 tag + + ```java + private Map subscription = new HashMap() + ``` + +* 消息监听器:**消息处理逻辑**,并发消费 MessageListenerConcurrently,顺序(分区)消费 MessageListenerOrderly + + ```java + private MessageListener messageListener; + ``` + +* 消费位点:当从 Broker 获取当前组内该 queue 的 offset 不存在时,consumeFromWhere 才有效,默认值代表从队列的最后 offset 开始消费,当队列内再有一条新的 msg 加入时,消费者才会去消费 + + ```java + private ConsumeFromWhere consumeFromWhere = ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET; + ``` + +* 消费时间戳:当消费位点配置的是 CONSUME_FROM_TIMESTAMP 时,并且服务器 Group 内不存在该 queue 的 offset 时,会使用该时间戳进行消费 + + ```java + private String consumeTimestamp = UtilAll.timeMillisToHumanString3(System.currentTimeMillis() - (1000 * 60 * 30));// 消费者创建时间 - 30秒,转换成 格式: 年月日小时分钟秒,比如 20220203171201 + ``` + +* 队列分配策略:主题下的队列分配策略,RebalanceImpl 对象依赖该算法 + + ```java + private AllocateMessageQueueStrategy allocateMessageQueueStrategy; + ``` + +* 消费进度存储器: + + ```java + private OffsetStore offsetStore; + ``` + +核心方法: + +* start():启动消费者 + + ```java + public void start() + ``` + +* shutdown():关闭消费者 + + ```java + public void shutdown() + ``` + +* registerMessageListener():注册消息监听器 + + ```java + public void registerMessageListener(MessageListener messageListener) + ``` + +* subscribe():添加订阅信息 + + ```java + public void subscribe(String topic, String subExpression) + ``` + +* unsubscribe():删除订阅指定主题的信息 + + ```java + public void unsubscribe(String topic) + ``` + +* suspend():停止消费 + + ```java + public void suspend() + ``` + +* resume():恢复消费 + + ```java + public void resume() + ``` + + + +*** + + + +##### 默认实现 + +DefaultMQPushConsumerImpl 是默认消费者的实现类 + +成员变量: + +* 客户端实例:整个进程内只有一个客户端实例对象 + + ```java + private MQClientInstance mQClientFactory; + ``` + +* 消费者实例:门面对象 + + ```java + private final DefaultMQPushConsumer defaultMQPushConsumer; + ``` + +* **负载均衡**:分配订阅主题的队列给当前消费者,20秒钟一个周期执行 Rebalance 算法(客户端实例触发) + + ```java + private final RebalanceImpl rebalanceImpl = new RebalancePushImpl(this); + ``` + +* 消费者信息: + + ```java + private final long consumerStartTimestamp; // 消费者启动时间 + private volatile ServiceState serviceState; // 消费者状态 + private volatile boolean pause = false; // 是否暂停 + private boolean consumeOrderly = false; // 是否顺序消费 + ``` + +* **拉取消息**:封装拉消息的 API,服务器 Broker 返回结果中包含下次 Pull 时推荐的 BrokerId,根据本次请求数据的冷热程度进行推荐 + + ```java + private PullAPIWrapper pullAPIWrapper; + ``` + +* **消息消费**服务:并发消费和顺序消费 + + ```java + private ConsumeMessageService consumeMessageService; + ``` + +* 流控: + + ```java + private long queueFlowControlTimes = 0; // 队列流控次数,默认每1000次流控,进行一次日志打印 + private long queueMaxSpanFlowControlTimes = 0; // 流控使用,控制打印日志 + ``` + +* HOOK:钩子方法 + + ```java + // 过滤消息 hook + private final ArrayList filterMessageHookList; + // 消息执行hook,在消息处理前和处理后分别执行 hook.before hook.after 系列方法 + private final ArrayList consumeMessageHookList; + ``` + +核心方法: + +* start():加锁保证线程安全 + + ```java + public synchronized void start() + ``` + + * `this.checkConfig()`:检查配置,包括组名、消费模式、订阅信息、消息监听器等 + * `this.copySubscription()`:拷贝订阅信息到 RebalanceImpl 对象 + * `this.rebalanceImpl.getSubscriptionInner().put(topic, subscriptionData)`:将订阅信息加入 rbl 的 map 中 + * `this.messageListenerInner = ...getMessageListener()`:将消息监听器保存到实例对象 + * `switch (this.defaultMQPushConsumer.getMessageModel())`:判断消费模式,广播模式下直接返回 + * `final String retryTopic`:当前**消费者组重试的主题名**,规则 `%RETRY%ConsumerGroup` + * `SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData()`:创建重试主题的订阅数据对象 + * `this.rebalanceImpl.getSubscriptionInner().put(retryTopic, subscriptionData)`:将创建的重试主题加入到 rbl 对象的 map 中,消息重试时会再次加入到该主题,消费者订阅这个主题之后,就有机会再次拿到该消息进行消费处理 + * `this.mQClientFactory = ...getOrCreateMQClientInstance()`:获取客户端实例对象 + * `this.rebalanceImpl.`:初始化负载均衡对象,设置**队列分配策略对象**到属性中 + * `this.pullAPIWrapper = new PullAPIWrapper()`:创建拉消息 API 对象,内部封装了查询推荐主机算法 + * `this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList)`:将 过滤 Hook 列表注册到该对象内,消息拉取下来之后会执行该 Hook,再**进行一次自定义的过滤** + * `this.offsetStore = new RemoteBrokerOffsetStore()`:默认集群模式下创建消息进度存储器 + * `this.consumeMessageService = ...`:根据消息监听器的类型创建消费服务 + * `this.consumeMessageService.start()`:启动消费服务 + * `boolean registerOK = mQClientFactory.registerConsumer()`:**将消费者注册到客户端实例中**,客户端提供的服务: + * 心跳服务:把订阅数据同步到订阅主题的 Broker + * 拉消息服务:内部 PullMessageService 启动线程,基于 PullRequestQueue 工作,消费者负载均衡分配到队列后会向该队列提交 PullRequest + * 队列负载服务:每 20 秒调用一次 `consumer.doRebalance()` 接口 + * 消息进度持久化 + * 动态调整消费者、消费服务线程池 + * `mQClientFactory.start()`:启动客户端实例 + * ` this.updateTopic`:从 nameserver 获取主题路由数据,生成主题集合放入 rbl 对象的 table + * `this.mQClientFactory.checkClientInBroker()`:检查服务器是否支持消息过滤模式,一般使用 tag 过滤,服务器默认支持 + * `this.mQClientFactory.sendHeartbeatToAllBrokerWithLock()`:向所有已知的 Broker 节点,发送心跳数据 + * `this.mQClientFactory.rebalanceImmediately()`:唤醒 rbl 线程,触发负载均衡执行 + + + + + + + + + + + + + + + @@ -7367,6 +7594,77 @@ TopicPublishInfo 类用来存储路由信息 ### 客户端 +#### 公共配置 + +公共的配置信息类 + +* ClientConfig 类 + + ```java + public class ClientConfig { + // Namesrv 地址配置 + private String namesrvAddr = NameServerAddressUtils.getNameServerAddresses(); + // 客户端的 IP 地址 + private String clientIP = RemotingUtil.getLocalAddress(); + // 客户端实例名称 + private String instanceName = System.getProperty("rocketmq.client.name", "DEFAULT"); + // 客户端回调线程池的数量,平台核心数,8核16线程的电脑返回16 + private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); + // 命名空间 + protected String namespace; + protected AccessChannel accessChannel = AccessChannel.LOCAL; + + // 获取路由信息的间隔时间 30s + private int pollNameServerInterval = 1000 * 30; + // 客户端与 broker 之间的心跳周期 30s + private int heartbeatBrokerInterval = 1000 * 30; + // 消费者持久化消费的周期 5s + private int persistConsumerOffsetInterval = 1000 * 5; + private long pullTimeDelayMillsWhenException = 1000; + private boolean unitMode = false; + private String unitName; + // vip 通道,broker 启动时绑定两个端口,其中一个是 vip 通道 + private boolean vipChannelEnabled = Boolean.parseBoolean(); + // 语言,默认是 Java + private LanguageCode language = LanguageCode.JAVA; + } + ``` + +* NettyClientConfig + + ```java + public class NettyClientConfig { + // 客户端工作线程数 + private int clientWorkerThreads = 4; + // 回调处理线程池 线程数:平台核心数 + private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); + // 单向请求并发数,默认 65535 + private int clientOnewaySemaphoreValue = NettySystemConfig.CLIENT_ONEWAY_SEMAPHORE_VALUE; + // 异步请求并发数,默认 65535 + private int clientAsyncSemaphoreValue = NettySystemConfig.CLIENT_ASYNC_SEMAPHORE_VALUE; + // 客户端连接服务器的超时时间限制 3秒 + private int connectTimeoutMillis = 3000; + // 客户端未激活周期,60s(指定时间内 ch 未激活,需要关闭) + private long channelNotActiveInterval = 1000 * 60; + // 客户端与服务器 ch 最大空闲时间 2分钟 + private int clientChannelMaxIdleTimeSeconds = 120; + + // 底层 Socket 写和收 缓冲区的大小 65535 64k + private int clientSocketSndBufSize = NettySystemConfig.socketSndbufSize; + private int clientSocketRcvBufSize = NettySystemConfig.socketRcvbufSize; + // 客户端 netty 是否启动内存池 + private boolean clientPooledByteBufAllocatorEnable = false; + // 客户端是否超时关闭 Socket 连接 + private boolean clientCloseSocketIfTimeout = false; + } + ``` + + + +*** + + + #### 实例对象 ##### 成员属性 @@ -7442,7 +7740,7 @@ MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一 ```java private final PullMessageService pullMessageService; // 拉消息服务 private final RebalanceService rebalanceService; // 消费者负载均衡服务 - private final ConsumerStatsManager consumerStatsManager; // 消费者状态管理 + private final ConsumerStatsManager consumerStatsManager; // 消费者状态管理 ``` * 内部生产者实例:处理消费端**消息回退**,用该生产者发送回执消息 @@ -7457,38 +7755,6 @@ MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一 private final AtomicLong sendHeartbeatTimesTotal = new AtomicLong(0) ``` -* 公共配置类: - - ```java - public class ClientConfig { - // Namesrv 地址配置 - private String namesrvAddr = NameServerAddressUtils.getNameServerAddresses(); - // 客户端的 IP 地址 - private String clientIP = RemotingUtil.getLocalAddress(); - // 客户端实例名称 - private String instanceName = System.getProperty("rocketmq.client.name", "DEFAULT"); - // 客户端回调线程池的数量,平台核心数,8核16线程的电脑返回16 - private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); - // 命名空间 - protected String namespace; - protected AccessChannel accessChannel = AccessChannel.LOCAL; - - // 获取路由信息的间隔时间 30s - private int pollNameServerInterval = 1000 * 30; - // 客户端与 broker 之间的心跳周期 30s - private int heartbeatBrokerInterval = 1000 * 30; - // 消费者持久化消费的周期 5s - private int persistConsumerOffsetInterval = 1000 * 5; - private long pullTimeDelayMillsWhenException = 1000; - private boolean unitMode = false; - private String unitName; - // vip 通道,broker 启动时绑定两个端口,其中一个是 vip 通道 - private boolean vipChannelEnabled = Boolean.parseBoolean(); - // 语言,默认是 Java - private LanguageCode language = LanguageCode.JAVA; - } - ``` - 构造方法: * MQClientInstance 有参构造: @@ -7550,7 +7816,7 @@ MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一 * `this.startScheduledTask()`:启动定时任务 * `this.pullMessageService.start()`:启动拉取消息服务 * `this.rebalanceService.start()`:启动负载均衡服务 - * `this.defaultMQProducer.getDefaultMQProducerImpl().start(false)`:启动内部生产者,参数为 false 代表不启动实例 + * `this.defaultMQProducer...start(false)`:启动内部生产者,参数为 false 代表不启动实例 * startScheduledTask():**启动定时任务**,调度线程池是单线程 @@ -7672,35 +7938,6 @@ NettyRemotingClient 类负责客户端的网络通信 private final ChannelEventListener channelEventListener; ``` -* Netty 配置对象: - - ```java - public class NettyClientConfig { - // 客户端工作线程数 - private int clientWorkerThreads = 4; - // 回调处理线程池 线程数:平台核心数 - private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); - // 单向请求并发数,默认 65535 - private int clientOnewaySemaphoreValue = NettySystemConfig.CLIENT_ONEWAY_SEMAPHORE_VALUE; - // 异步请求并发数,默认 65535 - private int clientAsyncSemaphoreValue = NettySystemConfig.CLIENT_ASYNC_SEMAPHORE_VALUE; - // 客户端连接服务器的超时时间限制 3秒 - private int connectTimeoutMillis = 3000; - // 客户端未激活周期,60s(指定时间内 ch 未激活,需要关闭) - private long channelNotActiveInterval = 1000 * 60; - // 客户端与服务器 ch 最大空闲时间 2分钟 - private int clientChannelMaxIdleTimeSeconds = 120; - - // 底层 Socket 写和收 缓冲区的大小 65535 64k - private int clientSocketSndBufSize = NettySystemConfig.socketSndbufSize; - private int clientSocketRcvBufSize = NettySystemConfig.socketRcvbufSize; - // 客户端 netty 是否启动内存池 - private boolean clientPooledByteBufAllocatorEnable = false; - // 客户端是否超时关闭 Socket 连接 - private boolean clientCloseSocketIfTimeout = false; - } - ``` - 构造方法 * 无参构造: diff --git a/Java.md b/Java.md index 125e703..3004ef2 100644 --- a/Java.md +++ b/Java.md @@ -630,8 +630,9 @@ public class Test1 { 可变参数在方法内部本质上就是一个数组。 可变参数的注意事项: - 1.一个形参列表中可变参数只能有一个! - 2.可变参数必须放在形参列表的**最后面**! + +* 一个形参列表中可变参数只能有一个 +* 可变参数必须放在形参列表的**最后面** ```java public static void main(String[] args) { @@ -929,7 +930,7 @@ Java 的参数是以**值传递**的形式传入方法中 public class EnumDemo { public static void main(String[] args){ // 获取索引 - Season s = Season.SPRING;/ + Season s = Season.SPRING; System.out.println(s); //SPRING System.out.println(s.ordinal()); // 0,代表索引,summer 就是 1 s.s.doSomething(); @@ -3291,7 +3292,7 @@ BigDecimal divide = bd1.divide(参与运算的对象,小数点后精确到多少 正则表达式的作用:是一些特殊字符组成的校验规则,可以校验信息的正确性,校验邮箱、电话号码、金额等。 -比如检验qq号: +比如检验 qq 号: ```java public static boolean checkQQRegex(String qq){ @@ -3333,7 +3334,7 @@ java.util.regex 包主要包括以下三个类: ##### 特殊字符 -\r\n 是Windows中的文本行结束标签,在Unix/Linux则是 \n +\r\n 是 Windows 中的文本行结束标签,在 Unix/Linux 则是 \n | 元字符 | 说明 | | ------ | ------------------------------------------------------------ | @@ -3342,8 +3343,8 @@ java.util.regex 包主要包括以下三个类: | \n | 换行符 | | \r | 回车符 | | \t | 制表符 | -| \\ | 代表\本身 | -| () | 使用( )定义一个子表达式。子表达式的内容可以当成一个独立元素 | +| \\ | 代表 \ 本身 | +| () | 使用 () 定义一个子表达式。子表达式的内容可以当成一个独立元素 | @@ -3353,12 +3354,11 @@ java.util.regex 包主要包括以下三个类: ##### 标准字符 -标准字符集合 -能够与”多种字符“匹配的表达式。注意区分大小写,大写是相反的意思。只能校验**"单"**个字符。 +能够与多种字符匹配的表达式,注意区分大小写,大写是相反的意思,只能校验**单**个字符。 | 元字符 | 说明 | | ------ | ------------------------------------------------------------ | -| . | 匹配任意一个字符(除了换行符),如果要匹配包括“\n”在内的所有字符,一般用[\s\S] | +| . | 匹配任意一个字符(除了换行符),如果要匹配包括 \n 在内的所有字符,一般用 [\s\S] | | \d | 数字字符,0~9 中的任意一个,等价于 [0-9] | | \D | 非数字字符,等价于 [ ^0-9] | | \w | 大小写字母或数字或下划线,等价于[a-zA-Z_0-9_] | @@ -3376,7 +3376,7 @@ java.util.regex 包主要包括以下三个类: ##### 自定义符 -自定义符号集合,[ ]方括号匹配方式,能够匹配方括号中**任意一个**字符 +自定义符号集合,[ ] 方括号匹配方式,能够匹配方括号中**任意一个**字符 | 元字符 | 说明 | | ------------ | ----------------------------------------- | @@ -3388,10 +3388,10 @@ java.util.regex 包主要包括以下三个类: | [a-z&&[m-p]] | 匹配 a 到 z 并且 m 到 p:[a-dm-p](交集) | | [^] | 取反 | -* 正则表达式的特殊符号,被包含到中括号中,则失去特殊意义,除了^,-之外,需要在前面加 \ +* 正则表达式的特殊符号,被包含到中括号中,则失去特殊意义,除了 ^,- 之外,需要在前面加 \ * 标准字符集合,除小数点外,如果被包含于中括号,自定义字符集合将包含该集合。 - 比如:[\d. \ -+]将匹配:数字、小数点、+、- + 比如:[\d. \ -+] 将匹配:数字、小数点、+、- @@ -3403,17 +3403,17 @@ java.util.regex 包主要包括以下三个类: 修饰匹配次数的特殊符号。 -* 匹配次数中的贪婪模式(匹配字符越多越好,默认!),\* 和 + 都是贪婪型元字符。 -* 匹配次数中的非贪婪模式(匹配字符越少越好,修饰匹配次数的特殊符号后再加上一个 "?" 号) +* 匹配次数中的贪婪模式(匹配字符越多越好,默认 !),\* 和 + 都是贪婪型元字符。 +* 匹配次数中的非贪婪模式(匹配字符越少越好,修饰匹配次数的特殊符号后再加上一个 ? 号) -| 元字符 | 说明 | -| ------ | -------------------------------- | -| X? | X一次或一次也没,有相当于 {0,1} | -| X* | X不出现或出现任意次,相当于 {0,} | -| X+ | X至少一次,相当于 {1,} | -| X{n} | X恰好 n 次 | -| {n,} | X至少 n 次 | -| {n,m} | X至少 n 次,但是不超过 m 次 | +| 元字符 | 说明 | +| ------ | --------------------------------- | +| X? | X 一次或一次也没,有相当于 {0,1} | +| X* | X 不出现或出现任意次,相当于 {0,} | +| X+ | X 至少一次,相当于 {1,} | +| X{n} | X 恰好 n 次 | +| {n,} | X 至少 n 次 | +| {n,m} | X 至少 n 次,但是不超过 m 次 | @@ -3443,16 +3443,16 @@ java.util.regex 包主要包括以下三个类: 捕获组是把多个字符当一个单独单元进行处理的方法,它通过对括号内的字符分组来创建。 -在表达式`((A)(B(C)))`,有四个这样的组:((A)(B(C)))、(A)、(B(C))、(C)(按照括号从左到右依次为 group(1)...) +在表达式 `((A)(B(C)))`,有四个这样的组:((A)(B(C)))、(A)、(B(C))、(C)(按照括号从左到右依次为 group(1)...) * 调用 matcher 对象的 groupCount 方法返回一个 int 值,表示 matcher 对象当前有多个捕获组。 -* 特殊的组group(0)、group(),它代表整个表达式,该组不包括在 groupCount 的返回值中。 +* 特殊的组 group(0)、group(),代表整个表达式,该组不包括在 groupCount 的返回值中。 | 表达式 | 说明 | | ------------------------- | ------------------------------------------------------------ | | \| (分支结构) | 左右两边表达式之间 "或" 关系,匹配左边或者右边 | -| () (捕获组) | (1) 在被修饰匹配次数的时候,括号中的表达式可以作为整体被修饰
(2) 取匹配结果的时候,括号中的表达式匹配到的内容可以被单独得到
(3) 每一对括号分配一个编号,()的捕获根据左括号的顺序从1开始自动编号。捕获元素编号为零的第一个捕获是由整个正则表达式模式匹配的文本 | -| (?:Expression) 非捕获组 | 一些表达式中,不得不使用( ),但又不需要保存( )中子表达式匹配的内容,这时可以用非捕获组来抵消使用( )带来的副作用。 | +| () (捕获组) | (1) 在被修饰匹配次数的时候,括号中的表达式可以作为整体被修饰
(2) 取匹配结果的时候,括号中的表达式匹配到的内容可以被单独得到
(3) 每一对括号分配一个编号,()的捕获根据左括号的顺序从 1 开始自动编号。捕获元素编号为零的第一个捕获是由整个正则表达式模式匹配的文本 | +| (?:Expression) 非捕获组 | 一些表达式中,不得不使用( ),但又不需要保存 () 中子表达式匹配的内容,这时可以用非捕获组来抵消使用( )带来的副作用。 | @@ -3470,17 +3470,17 @@ java.util.regex 包主要包括以下三个类: * **把匹配到的字符重复一遍在进行匹配** -* 应用1: +* 应用 1: ```java String regex = "((\d)3)\1[0-9](\w)\2{2}"; ``` - * 首先匹配((\d)3),其次\1匹配((\d)3)已经匹配到的内容,\2匹配(\d), {2}指的是\2的值出现两次 - * 实例:23238n22(匹配到2未来就继续匹配2) + * 首先匹配 ((\d)3),其次 \1 匹配 ((\d)3) 已经匹配到的内容,\2 匹配 (\d), {2} 指的是 \2 的值出现两次 + * 实例:23238n22(匹配到 2 未来就继续匹配 2) * 实例:43438n44 -* 应用2:爬虫 +* 应用 2:爬虫 ```java String regex = "<(h[1-6])>\w*?<\/\1>"; @@ -3506,7 +3506,7 @@ java.util.regex 包主要包括以下三个类: * 只进行子表达式的匹配,匹配内容不计入最终的匹配结果,是零宽度 -* 判断当前位置的前后字符,是否符合指定的条件,但不匹配前后的字符。**是对位置的匹配**。 +* 判断当前位置的前后字符,是否符合指定的条件,但不匹配前后的字符,**是对位置的匹配** * 正则表达式匹配过程中,如果子表达式匹配到的是字符内容,而非位置,并被保存到最终的匹配结果中,那么就认为这个子表达式是占有字符的;如果子表达式匹配的仅仅是位置,或者匹配的内容并不保存到最终的匹配结果中,那么就认为这个子表达式是**零宽度**的。占有字符还是零宽度,是针对匹配的内容是否保存到最终的匹配结果中而言的 @@ -3555,7 +3555,7 @@ Pattern 类: Matcher 类: * `boolean find()`:扫描输入的序列,查找与该模式匹配的下一个子序列 -* `String group()`:返回与上一个匹配的输入子序列。同group(0),匹配整个表达式的子字符串 +* `String group()`:返回与上一个匹配的输入子序列,同 group(0),匹配整个表达式的子字符串 * `String group(int group)`:返回在上一次匹配操作期间由给定组捕获的输入子序列 * `int groupCount()`:返回此匹配器模式中捕获组的数量 @@ -3604,8 +3604,8 @@ public class Demo02 { } ``` -* 正则表达式改为`"(([a-z]+)(?:[0-9]+))"` 没有group(3) 因为是非捕获组 -* 正则表达式改为`"([a-z]+)([0-9]+)"` 没有 group(3) aa232 - aa --232 +* 正则表达式改为 `"(([a-z]+)(?:[0-9]+))"` 没有 group(3) 因为是非捕获组 +* 正则表达式改为 `"([a-z]+)([0-9]+)"` 没有 group(3) aa232 - aa --232 @@ -3695,9 +3695,9 @@ public static void main(String[] args) { -##### 面试问题 +##### 搜索号码 -找出所有189和132开头的手机号 +找出所有 189 和 132 开头的手机号 ```java public class RegexDemo { @@ -5624,15 +5624,15 @@ class LRUCache extends LinkedHashMap { #### TreeMap -TreeMap 实现了 SotredMap 接口,是有序不可重复的键值对集合,基于红黑树(Red-Black tree)实现,每个 key-value 都作为一个红黑树的节点。如果构造 TreeMap 没有指定比较器,则根据 key 执行自然排序(默认升序),如果指定了比较器则按照比较器来进行排序 - -TreeSet 集合的底层是基于TreeMap,只是键的附属值为空对象而已 +TreeMap 实现了 SotredMap 接口,是有序不可重复的键值对集合,基于红黑树(Red-Black tree)实现,每个 key-value 都作为一个红黑树的节点,如果构造 TreeMap 没有指定比较器,则根据 key 执行自然排序(默认升序),如果指定了比较器则按照比较器来进行排序 TreeMap 集合指定大小规则有 2 种方式: -* 直接为对象的类实现比较器规则接口 Comparable,重写比较方法(拓展方式) +* 直接为对象的类实现比较器规则接口 Comparable,重写比较方法 * 直接为集合设置比较器 Comparator 对象,重写比较方法 +说明:TreeSet 集合的底层是基于 TreeMap,只是键的附属值为空对象而已 + 成员属性: * Entry 节点 @@ -5799,8 +5799,7 @@ public class MapDemo{ } ``` -优点:泛型在编译阶段约束了操作的数据类型,从而不会出现类型转换异常 - 体现的是 Java 的严谨性和规范性,数据类型,经常需要进行统一 +优点:泛型在编译阶段约束了操作的数据类型,从而不会出现类型转换异常,体现的是 Java 的严谨性和规范性 @@ -5812,7 +5811,7 @@ public class MapDemo{ ##### 泛型类 -泛型类:使用了泛型定义的类就是泛型类。 +泛型类:使用了泛型定义的类就是泛型类 泛型类格式: @@ -11642,6 +11641,10 @@ private int hash32; +参考文章:https://www.yuque.com/u21195183/jvm/nkq31c + + + *** From 916bc11bd67e092736ee58eb025258c52e45b556 Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 24 Jan 2022 00:23:57 +0800 Subject: [PATCH 106/168] Update Java Notes --- Frame.md | 1475 +++++++++++++++++++++++++++++++++++++++--------------- Java.md | 6 +- 2 files changed, 1086 insertions(+), 395 deletions(-) diff --git a/Frame.md b/Frame.md index c126bb8..bc07592 100644 --- a/Frame.md +++ b/Frame.md @@ -4594,10 +4594,10 @@ RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成 的,Com ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息存储结构.png) * CommitLog:消息主体以及元数据的存储主体,存储 Producer 端写入的消息内容,消息内容不是定长的。消息主要是顺序写入日志文件,单个文件大小默认 1G,偏移量代表下一次写入的位置,当文件写满了就继续写入下一个文件 -* ConsumerQueue:消息消费队列,存储消息在 CommitLog 的索引。RocketMQ 消息消费时要遍历 CommitLog 文件,并根据主题 Topic 检索消息,这是非常低效的。引入 ConsumeQueue 作为消费消息的索引,保存了指定 Topic 下的队列消息在 CommitLog 中的起始物理偏移量 offset,消息大小 size 和消息 Tag 的 HashCode 值,每个 ConsumeQueue 文件大小约 5.72M +* ConsumerQueue:消息消费队列,存储消息在 CommitLog 的索引。RocketMQ 消息消费时要遍历 CommitLog 文件,并根据主题 Topic 检索消息,这是非常低效的。引入 ConsumeQueue 作为消费消息的索引,**保存了指定 Topic 下的队列消息在 CommitLog 中的起始物理偏移量 offset**,消息大小 size 和消息 Tag 的 HashCode 值,每个 ConsumeQueue 文件大小约 5.72M * IndexFile:为了消息查询提供了一种通过 Key 或时间区间来查询消息的方法,通过 IndexFile 来查找消息的方法不影响发送与消费消息的主流程。IndexFile 的底层存储为在文件系统中实现的 HashMap 结构,故 RocketMQ 的索引文件其底层实现为 **hash 索引** -RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所有的队列共用一个日志数据文件(CommitLog)来存储。混合型存储结构(多个 Topic 的消息实体内容都存储于一个 CommitLog 中)**针对 Producer 和 Consumer 分别采用了数据和索引部分相分离的存储结构**,Producer 发送消息至 Broker 端,然后 Broker 端使用同步或者异步的方式对消息刷盘持久化,保存至 CommitLog 中。只要消息被持久化至磁盘文件 CommitLog 中,Producer 发送的消息就不会丢失,Consumer 也就肯定有机会去消费这条消息 +RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所有的队列共用一个日志数据文件(CommitLog)来存储。混合型存储结构(多个 Topic 的消息实体内容都存储于一个 CommitLog 中)针对 Producer 和 Consumer 分别采用了**数据和索引部分相分离**的存储结构,Producer 发送消息至 Broker 端,然后 Broker 端使用同步或者异步的方式对消息刷盘持久化,保存至 CommitLog 中。只要消息被持久化至磁盘文件 CommitLog 中,Producer 发送的消息就不会丢失,Consumer 也就肯定有机会去消费这条消息 服务端支持长轮询模式,当消费者无法拉取到消息后,可以等下一次消息拉取,Broker 允许等待 30s 的时间,只要这段时间内有新消息到达,将直接返回给消费端。RocketMQ 的具体做法是,使用 Broker 端的后台服务线程 ReputMessageService 不停地分发请求并异步构建 ConsumeQueue(逻辑消费队列)和 IndexFile(索引文件)数据 @@ -4607,9 +4607,7 @@ RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所 -#### 存储优化 - -##### 内存映射 +#### 内存映射 操作系统分为用户态和内核态,文件操作、网络操作需要涉及这两种形态的切换,需要进行数据复制。一台服务器把本机磁盘文件的内容发送到客户端,分为两个步骤: @@ -4633,7 +4631,7 @@ MappedByteBuffer 内存映射的方式**限制**一次只能映射 1.5~2G 的文 -##### 页缓存 +#### 页面缓存 页缓存(PageCache)是 OS 对文件的缓存,每一页的大小通常是 4K,用于加速对文件的读写。程序对文件进行顺序读写的速度几乎接近于内存的读写速度,就是因为 OS 将一部分的内存用作 PageCache,**对读写访问操作进行了性能优化** @@ -4812,23 +4810,23 @@ latencyFaultTolerance 机制是实现消息发送高可用的核心关键所在 在 RocketMQ 中,Consumer 端的两种消费模式(Push/Pull)都是基于拉模式来获取消息的,而在 Push 模式只是对 Pull 模式的一种封装,其本质实现为消息拉取线程在从服务器拉取到一批消息,提交到消息消费线程池后,又继续向服务器再次尝试拉取消息,如果未拉取到消息,则延迟一下又继续拉取 -在两种基于拉模式的消费方式(Push/Pull)中,均需要 Consumer 端在知道从 Broker 端的哪一个消息队列—队列中去获取消息,所以在 Consumer 端来做负载均衡,即 Broker 端中多个 MessageQueue 分配给同一个 ConsumerGroup 中的哪些 Consumer 消费 +在两种基于拉模式的消费方式(Push/Pull)中,均需要 Consumer 端在知道从 Broker 端的哪一个消息队列—队列中去获取消息,所以在 Consumer 端来做负载均衡,即 Broker 端中多个 MessageQueue 分配给同一个 Consumer Group 中的哪些 Consumer 消费 -* 广播模式下要求一条消息需要投递到一个消费组下面所有的消费者实例,所以不存在负载均衡,在实现上,Consumer 分配 queue 时,所有 Consumer 都分到所有的queue。 +* 广播模式下要求一条消息需要投递到一个消费组下面所有的消费者实例,所以不存在负载均衡,在实现上,Consumer 分配 queue 时,所有 Consumer 都分到所有的 queue。 * 在集群消费模式下,每条消息只需要投递到订阅这个 Topic 的 Consumer Group 下的一个实例即可,RocketMQ 采用主动拉取的方式拉取并消费消息,在拉取的时候需要明确指定拉取哪一条 Message Queue 集群模式下,每当实例的数量有变更,都会触发一次所有实例的负载均衡,这时候会按照 queue 的数量和实例的数量平均分配 queue 给每个实例。默认的分配算法是 AllocateMessageQueueAveragely: -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-consumer负载均衡1.png) +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-平均队列分配.png) 还有一种平均的算法是 AllocateMessageQueueAveragelyByCircle,以环状轮流均分 queue 的形式: -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-consumer负载均衡2.png) +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-平均队列轮流分配.png) 集群模式下,queue 都是只允许分配只一个实例,如果多个实例同时消费一个 queue 的消息,由于拉取哪些消息是 Consumer 主动控制的,会导致同一个消息在不同的实例下被消费多次 -通过增加 Consumer 实例去分摊 queue 的消费,可以起到水平扩展的消费能力的作用。而当有实例下线时,会重新触发负载均衡,这时候原来分配到的 queue 将分配到其他实例上继续消费。但是如果 Consumer 实例的数量比 Message Queue 的总数量还多的话,多出来的 Consumer 实例将无法分到 queue,也就无法消费到消息,也就无法起到分摊负载的作用了,所以需要控制让 queue 的总数量大于等于 Consumer 的数量 +通过增加 Consumer 实例去分摊 queue 的消费,可以起到水平扩展的消费能力的作用。而当有实例下线时,会重新触发负载均衡,这时候原来分配到的 queue 将分配到其他实例上继续消费。但是如果 Consumer 实例的数量比 Message Queue 的总数量还多的话,多出来的 Consumer 实例将无法分到 queue,也就无法消费到消息,也就无法起到分摊负载的作用了,所以需要**控制让 queue 的总数量大于等于 Consumer 的数量** @@ -4850,8 +4848,6 @@ Consumer 端实现负载均衡的核心类 **RebalanceImpl** * 先对 Topic 下的消息消费队列、消费者 ID 排序,然后用消息队列分配策略算法(默认是消息队列的平均分配算法),计算出待拉取的消息队列。平均分配算法类似于分页的算法,将所有 MessageQueue 排好序类似于记录,将所有消费端 Consumer 排好序类似页数,并求出每一页需要包含的平均 size 和每个页面记录的范围 range,最后遍历整个 range 而计算出当前 Consumer 端应该分配到的记录(这里即为 MessageQueue) - ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-负载均衡平均分配算法.png) - * 调用 updateProcessQueueTableInRebalance() 方法,先将分配到的消息队列集合 mqSet 与 processQueueTable 做一个过滤比对 ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-负载均衡重新平衡算法.png) @@ -4864,7 +4860,7 @@ Consumer 端实现负载均衡的核心类 **RebalanceImpl** 对比下 RebalancePushImpl 和 RebalancePullImpl 两个实现类的 dispatchPullRequest() 方法,RebalancePullImpl 类里面的该方法为空 -消息消费队列在同一消费组不同消费者之间的负载均衡,其核心设计理念是在一个消息消费队列在同一时间只允许被同一消费组内的一个消费者消费,一个消息消费者能同时消费多个消息队列 +消息消费队列在同一消费组不同消费者之间的负载均衡,其核心设计理念是在**一个消息消费队列在同一时间只允许被同一消费组内的一个消费者消费,一个消息消费者能同时消费多个消息队列** @@ -5231,6 +5227,10 @@ NamesrvStartup#start:启动 Namesrv 控制器 +源码解析参考视频:https://space.bilibili.com/457326371 + + + **** @@ -5471,7 +5471,7 @@ NettyRemotingServer 类成员变量: 核心方法的解析: -* start():启动方法 +* start():启动方法,**创建 BootStrap,并添加 NettyServerHandler 处理器** ```java public void start() { @@ -5492,7 +5492,7 @@ NettyRemotingServer 类成员变量: .childOption(ChannelOption.TCP_NODELAY, true) // 设置服务器端口 .localAddress(new InetSocketAddress(this.nettyServerConfig.getListenPort())) - // 向 channel pipeline 添加了很多 handler,包括 NettyServerHandler + // 向 channel pipeline 添加了很多 handler,【包括 NettyServerHandler】 .childHandler(new ChannelInitializer() {}); // 客户端开启 内存池,使用的内存池是 PooledByteBufAllocator.DEFAULT @@ -5639,7 +5639,7 @@ NettyRemotingServer 类成员变量: ##### 处理方法 -NettyServerHandler 类用来处理 RemotingCommand 相关的数据,针对某一种类型的**请求处理** +NettyServerHandler 类用来处理 Channel 上的事件,在 NettyRemotingServer 启动时注册到 Netty 中,可以处理 RemotingCommand 相关的数据,针对某一种类型的**请求处理** ```java class NettyServerHandler extends SimpleChannelInboundHandler { @@ -5667,7 +5667,7 @@ public void processMessageReceived(ChannelHandlerContext ctx, RemotingCommand ms } ``` -NettyRemotingAbstract#processRequestCommand:处理请求的数据 +NettyRemotingAbstract#processRequestCommand:**处理请求的数据** * `matched = this.processorTable.get(cmd.getCode())`:根据业务请求码获取 Pair 对象,包含**处理器和线程池资源** @@ -5701,7 +5701,7 @@ NettyRemotingAbstract#processRequestCommand:处理请求的数据 * `pair.getObject2().submit(requestTask)`:获取处理器对应的线程池,将 task 提交,**从 IO 线程切换到业务线程** -NettyRemotingAbstract#processResponseCommand:处理响应的数据 +NettyRemotingAbstract#processResponseCommand:**处理响应的数据** * `int opaque = cmd.getOpaque()`:获取请求 ID * `responseFuture = responseTable.get(opaque)`:**从响应映射表中获取对应的对象** @@ -6828,7 +6828,7 @@ FlushCommitLogService 刷盘 CL 数据,默认是异步刷盘 ##### 清理服务 -CleanCommitLogService 清理过期的 CL 数据,定时任务 10 秒调用一次,先清理 CL,再清理 CQ,因为 CQ 依赖于 CL 的数据 +CleanCommitLogService 清理过期的 CL 数据,定时任务 10 秒调用一次,**先清理 CL,再清理 CQ**,因为 CQ 依赖于 CL 的数据 * run():运行方法 @@ -6877,6 +6877,105 @@ CleanConsumeQueueService 清理过期的 CQ 数据 +##### 获取消息 + +PullMessageProcessor#processRequest 方法中调用 getMessage 用于获取消息(提示:建议学习消费者源码时再阅读) + +```java +// offset: 客户端拉消息使用位点; maxMsgNums: 32; messageFilter: 一般这里是 tagCode 过滤 +public GetMessageResult getMessage(final String group, final String topic, final int queueId, final long offset, final int maxMsgNums, final MessageFilter messageFilter) +``` + +* `if (this.shutdown)`:检查运行状态 + +* `GetMessageResult getResult`:创建查询结果对象 + +* `final long maxOffsetPy = this.commitLog.getMaxOffset()`:**获取 CommitLog 最大物理偏移量** + +* `ConsumeQueue consumeQueue = findConsumeQueue(topic, queueId)`:根据主题和队列 ID 获取 ConsumeQueue对象 + +* `minOffset, maxOffset`:获取当前 ConsumeQueue 的最小 offset 和 最大 offset,**判断是否满足本次 Pull 的 offset** + + `if (maxOffset == 0)`:说明队列内无数据,设置状态为 NO_MESSAGE_IN_QUEUE,外层进行长轮询 + + `else if (offset < minOffset)`:说明 offset 太小了,设置状态为 OFFSET_TOO_SMALL + + `else if (offset == maxOffset)`:消费进度持平,设置状态为 OFFSET_OVERFLOW_ONE,外层进行长轮询 + + `else if (offset > maxOffset)`:说明 offset 越界了,设置状态为 OFFSET_OVERFLOW_BADLY + +* `SelectMappedBufferResult bufferConsumeQueue`:查询 CQData **获取包含该 offset 的 MappedFile 文件**,如果该文件不是顺序写的文件,就读取 `[offset%maxSize, 文件尾]` 范围的数据,反之读取 `[offset%maxSize, 文件名+wrotePosition尾]` + + 先查 CQ 的原因:因为 CQ 时 CL 的索引,通过 CQ 查询 CL 更加快捷 + +* `if (bufferConsumeQueue != null)`:只有再 CQ 删除过期数据的逻辑执行时,条件才不成立,一般都是成立的 + +* `long nextPhyFileStartOffset = Long.MIN_VALUE`:下一个 commitLog 物理文件名,初始值为最小值 + +* `long maxPhyOffsetPulling = 0`:本次拉消息最后一条消息的物理偏移量 + +* `for ()`:**处理数据**,每次处理 20 字节处理字节数大于 16000 时跳出循环 + +* `offsetPy, sizePy, tagsCode`:读取 20 个字节后,获取消息物理偏移量、消息大小、消息 tagCode + +* `boolean isInDisk = checkInDiskByCommitOffset(...)`:**检查消息是热数据还是冷数据**,false 为热数据 + + * `long memory`:Broker 系统 40% 内存的字节数,写数据时内存不够会使用 LRU 算法淘汰数据,将淘汰数据持久化到磁盘 + * `return (maxOffsetPy - offsetPy) > memory`:返回 true 说明数据已经持久化到磁盘,为冷数据 + +* `if (this.isTheBatchFull())`:**控制是否跳出循环** + + * `if (0 == bufferTotal || 0 == messageTotal)`:本次 pull 消息未拉取到任何东西,需要外层 for 循环继续,返回 false + + * `if (maxMsgNums <= messageTotal)`:结果对象内消息数已经超过了最大消息数量,可以结束循环了 + + * `if (isInDisk)`:冷数据 + + `if ((bufferTotal + sizePy) > ...)`:冷数据一次 pull 请求最大允许获取 64kb 的消息 + + `if (messageTotal > ...)`:冷数据一次 pull 请求最大允许获取8 条消息 + + * `else`:热数据 + + `if ((bufferTotal + sizePy) > ...)`:热数据一次 pull 请求最大允许获取 256kb 的消息 + + `if (messageTotal > ...)`:冷数据一次 pull 请求最大允许获取32 条消息 + +* `if (messageFilter != null)`:按照消息 tagCode 进行过滤 + +* `selectResult = this.commitLog.getMessage(offsetPy, sizePy)`:根据 CQ 消息物理偏移量和消息大小**到 commitLog 中查询这条 msg** + +* `if (null == selectResult)`:条件成立说明 commitLog 执行了删除过期文件的定时任务,因为是先清理的 CL,所以 CQ 还有该索引数据 + +* `nextPhyFileStartOffset = this.commitLog.rollNextFile(offsetPy)`:获取包含该 offsetPy 的下一个数据文件的文件名 + +* `getResult.addMessage(selectResult)`:**将本次循环查询出来的 msg 加入到 getResult 内** + +* `status = GetMessageStatus.FOUND`:查询状态设置为 FOUND + +* `nextPhyFileStartOffset = Long.MIN_VALUE`:设置为最小值,跳过期 CQData 数据的逻辑 + +* `nextBeginOffset = offset + (i / ConsumeQueue.CQ_STORE_UNIT_SIZE)`:计算客户端下一次 pull 时使用的位点信息 + +* `getResult.setSuggestPullingFromSlave(diff > memory)`:**选择主从节点的建议** + + * `diff > memory => true`:表示本轮查询最后一条消息为冷数据,Broker 建议客户端下一次 pull 时到 slave 节点 + * `diff > memory => false`:表示本轮查询最后一条消息为热数据,Broker 建议客户端下一次 pull 时到 master 节点 + +* `getResult.setStatus(status)`:设置结果状态 + +* `getResult.setNextBeginOffset(nextBeginOffset)`:设置客户端下一次 pull 时的 offset + +* `getResult.setMaxOffset(maxOffset)`:设置 queue 的最大 offset 和最小 offset + +* `return getResult`:返回结果对象 + + + +*** + + + #### Broker BrokerStartup 启动方法 @@ -6914,6 +7013,8 @@ BrokerController#start:核心启动方法 #### 生产者类 +##### 生产者类 + DefaultMQProducer 是生产者的默认实现类 成员变量: @@ -7017,9 +7118,7 @@ DefaultMQProducer 是生产者的默认实现类 -#### 默认实现 - -##### 成员属性 +##### 实现者类 DefaultMQProducerImpl 类是默认的生产者实现类 @@ -7111,7 +7210,7 @@ DefaultMQProducerImpl 类是默认的生产者实现类 -##### 成员方法 +##### 实现方法 * start():启动方法,参数默认是 true,代表正常的启动路径 @@ -7337,7 +7436,7 @@ TopicPublishInfo 类用来存储路由信息 } else { // 遍历消息队列 for (int i = 0; i < this.messageQueueList.size(); i++) { - // 获取队列的索引 + // 【获取队列的索引,+1】 int index = this.sendWhichQueue.getAndIncrement(); // 获取队列的下标位置 int pos = Math.abs(index) % this.messageQueueList.size(); @@ -7355,115 +7454,219 @@ TopicPublishInfo 类用来存储路由信息 } ``` - - -**** +*** -### 消费者 -#### 消费者类 +#### 公共配置 -##### 默认消费 +公共的配置信息类 -DefaultMQPushConsumer 类是默认的消费者类 +* ClientConfig 类 -成员变量: + ```java + public class ClientConfig { + // Namesrv 地址配置 + private String namesrvAddr = NameServerAddressUtils.getNameServerAddresses(); + // 客户端的 IP 地址 + private String clientIP = RemotingUtil.getLocalAddress(); + // 客户端实例名称 + private String instanceName = System.getProperty("rocketmq.client.name", "DEFAULT"); + // 客户端回调线程池的数量,平台核心数,8核16线程的电脑返回16 + private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); + // 命名空间 + protected String namespace; + protected AccessChannel accessChannel = AccessChannel.LOCAL; + + // 获取路由信息的间隔时间 30s + private int pollNameServerInterval = 1000 * 30; + // 客户端与 broker 之间的心跳周期 30s + private int heartbeatBrokerInterval = 1000 * 30; + // 消费者持久化消费的周期 5s + private int persistConsumerOffsetInterval = 1000 * 5; + private long pullTimeDelayMillsWhenException = 1000; + private boolean unitMode = false; + private String unitName; + // vip 通道,broker 启动时绑定两个端口,其中一个是 vip 通道 + private boolean vipChannelEnabled = Boolean.parseBoolean(); + // 语言,默认是 Java + private LanguageCode language = LanguageCode.JAVA; + } + ``` -* 消费者实现类: +* NettyClientConfig ```java - protected final transient DefaultMQPushConsumerImpl defaultMQPushConsumerImpl; + public class NettyClientConfig { + // 客户端工作线程数 + private int clientWorkerThreads = 4; + // 回调处理线程池 线程数:平台核心数 + private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); + // 单向请求并发数,默认 65535 + private int clientOnewaySemaphoreValue = NettySystemConfig.CLIENT_ONEWAY_SEMAPHORE_VALUE; + // 异步请求并发数,默认 65535 + private int clientAsyncSemaphoreValue = NettySystemConfig.CLIENT_ASYNC_SEMAPHORE_VALUE; + // 客户端连接服务器的超时时间限制 3秒 + private int connectTimeoutMillis = 3000; + // 客户端未激活周期,60s(指定时间内 ch 未激活,需要关闭) + private long channelNotActiveInterval = 1000 * 60; + // 客户端与服务器 ch 最大空闲时间 2分钟 + private int clientChannelMaxIdleTimeSeconds = 120; + + // 底层 Socket 写和收 缓冲区的大小 65535 64k + private int clientSocketSndBufSize = NettySystemConfig.socketSndbufSize; + private int clientSocketRcvBufSize = NettySystemConfig.socketRcvbufSize; + // 客户端 netty 是否启动内存池 + private boolean clientPooledByteBufAllocatorEnable = false; + // 客户端是否超时关闭 Socket 连接 + private boolean clientCloseSocketIfTimeout = false; + } ``` -* 消费属性: + + +*** + + + +#### 客户端类 + +##### 成员属性 + +MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一个客户端实例,**既服务于生产者,也服务于消费者** + +成员变量: + +* 配置信息: ```java - private String consumerGroup; // 消费者组 - private MessageModel messageModel = MessageModel.CLUSTERING; // 消费模式,默认集群模式 + private final int instanceIndex; // 索引一般是 0,因为客户端实例一般都是一个进程只有一个 + private final String clientId; // 客户端 ID ip@pid + private final long bootTimestamp; // 客户端的启动时间 + private ServiceState serviceState; // 客户端状态 ``` -* 订阅信息:key 是主题,value 是过滤表达式,一般是 tag +* 生产者消费者的映射表:key 是组名 ```java - private Map subscription = new HashMap() + private final ConcurrentMap producerTable + private final ConcurrentMap consumerTable + private final ConcurrentMap adminExtTable ``` -* 消息监听器:**消息处理逻辑**,并发消费 MessageListenerConcurrently,顺序(分区)消费 MessageListenerOrderly +* 网络层配置: ```java - private MessageListener messageListener; + private final NettyClientConfig nettyClientConfig; ``` -* 消费位点:当从 Broker 获取当前组内该 queue 的 offset 不存在时,consumeFromWhere 才有效,默认值代表从队列的最后 offset 开始消费,当队列内再有一条新的 msg 加入时,消费者才会去消费 +* 核心功能的实现:负责将 MQ 业务层的数据转换为网络层的 RemotingCommand 对象,使用内部持有的 NettyRemotingClient 对象的 invoke 系列方法,完成网络 IO(同步、异步、单向) ```java - private ConsumeFromWhere consumeFromWhere = ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET; + private final MQClientAPIImpl mQClientAPIImpl; ``` -* 消费时间戳:当消费位点配置的是 CONSUME_FROM_TIMESTAMP 时,并且服务器 Group 内不存在该 queue 的 offset 时,会使用该时间戳进行消费 +* 本地路由数据:key 是主题名称,value 路由信息 ```java - private String consumeTimestamp = UtilAll.timeMillisToHumanString3(System.currentTimeMillis() - (1000 * 60 * 30));// 消费者创建时间 - 30秒,转换成 格式: 年月日小时分钟秒,比如 20220203171201 + private final ConcurrentMap topicRouteTable = new ConcurrentHashMap<>(); ``` -* 队列分配策略:主题下的队列分配策略,RebalanceImpl 对象依赖该算法 +* 锁信息:两把锁,锁不同的数据 ```java - private AllocateMessageQueueStrategy allocateMessageQueueStrategy; + private final Lock lockNamesrv = new ReentrantLock(); + private final Lock lockHeartbeat = new ReentrantLock(); ``` -* 消费进度存储器: +* 调度线程池:单线程,执行定时任务 ```java - private OffsetStore offsetStore; + private final ScheduledExecutorService scheduledExecutorService; ``` -核心方法: - -* start():启动消费者 +* Broker 映射表:key 是 BrokerName ```java - public void start() + // 物理节点映射表,value:Long 是 brokerID,【ID=0 的是主节点,其他是从节点】,String 是地址 ip:port + private final ConcurrentMap> brokerAddrTable; + // 物理节点版本映射表,String 是地址 ip:port,Integer 是版本 + ConcurrentMap> brokerVersionTable; ``` -* shutdown():关闭消费者 +* **客户端的协议处理器**:用于处理 IO 事件 ```java - public void shutdown() + private final ClientRemotingProcessor clientRemotingProcessor; ``` -* registerMessageListener():注册消息监听器 +* 消息服务: ```java - public void registerMessageListener(MessageListener messageListener) + private final PullMessageService pullMessageService; // 拉消息服务 + private final RebalanceService rebalanceService; // 消费者负载均衡服务 + private final ConsumerStatsManager consumerStatsManager; // 消费者状态管理 ``` -* subscribe():添加订阅信息 +* 内部生产者实例:处理消费端**消息回退**,用该生产者发送回执消息 ```java - public void subscribe(String topic, String subExpression) + private final DefaultMQProducer defaultMQProducer; ``` -* unsubscribe():删除订阅指定主题的信息 +* 心跳次数统计: ```java - public void unsubscribe(String topic) + private final AtomicLong sendHeartbeatTimesTotal = new AtomicLong(0) ``` -* suspend():停止消费 +构造方法: + +* MQClientInstance 有参构造: ```java - public void suspend() + public MQClientInstance(ClientConfig clientConfig, int instanceIndex, String clientId, RPCHook rpcHook) { + this.clientConfig = clientConfig; + this.instanceIndex = instanceIndex; + // Netty 相关的配置信息 + this.nettyClientConfig = new NettyClientConfig(); + // 平台核心数 + this.nettyClientConfig.setClientCallbackExecutorThreads(...); + this.nettyClientConfig.setUseTLS(clientConfig.isUseTLS()); + // 【创建客户端协议处理器】 + this.clientRemotingProcessor = new ClientRemotingProcessor(this); + // 创建 API 实现对象 + // 参数一:客户端网络配置 + // 参数二:客户端协议处理器,注册到客户端网络层 + // 参数三:rpcHook,注册到客户端网络层 + // 参数四:客户端配置 + this.mQClientAPIImpl = new MQClientAPIImpl(this.nettyClientConfig, this.clientRemotingProcessor, rpcHook, clientConfig); + + //... + // 内部生产者,指定内部生产者的组 + this.defaultMQProducer = new DefaultMQProducer(MixAll.CLIENT_INNER_PRODUCER_GROUP); + } ``` -* resume():恢复消费 +* MQClientAPIImpl 有参构造: ```java - public void resume() + public MQClientAPIImpl(nettyClientConfig, clientRemotingProcessor, rpcHook, clientConfig) { + this.clientConfig = clientConfig; + topAddressing = new TopAddressing(MixAll.getWSAddr(), clientConfig.getUnitName()); + // 创建网络层对象,参数二为 null 说明客户端并不关心 channel event + this.remotingClient = new NettyRemotingClient(nettyClientConfig, null); + // 业务处理器 + this.clientRemotingProcessor = clientRemotingProcessor; + // 注册 RpcHook + this.remotingClient.registerRPCHook(rpcHook); + // ... + // 注册回退消息的请求码 + this.remotingClient.registerProcessor(RequestCode.PUSH_REPLY_MESSAGE_TO_CLIENT, this.clientRemotingProcessor, null); + } ``` @@ -7472,15 +7675,371 @@ DefaultMQPushConsumer 类是默认的消费者类 -##### 默认实现 - -DefaultMQPushConsumerImpl 是默认消费者的实现类 - -成员变量: +##### 成员方法 -* 客户端实例:整个进程内只有一个客户端实例对象 +* start():启动方法 - ```java + * `synchronized (this)`:加锁保证线程安全,保证只有一个实例对象启动 + * `this.mQClientAPIImpl.start()`:启动客户端网络层,底层调用 RemotingClient 类 + * `this.startScheduledTask()`:启动定时任务 + * `this.pullMessageService.start()`:启动拉取消息服务 + * `this.rebalanceService.start()`:启动负载均衡服务 + * `this.defaultMQProducer...start(false)`:启动内部生产者,参数为 false 代表不启动实例 + +* startScheduledTask():**启动定时任务**,调度线程池是单线程 + + * `if (null == this.clientConfig.getNamesrvAddr())`:Namesrv 地址是空,需要两分钟拉取一次 Namesrv 地址 + + * 定时任务 1:从 Namesrv 更新客户端本地的路由数据,周期 30 秒一次 + + ```java + // 获取生产者和消费者订阅的主题集合,遍历集合,对比从 namesrv 拉取最新的主题路由数据和本地数据,是否需要更新 + MQClientInstance.this.updateTopicRouteInfoFromNameServer(); + ``` + + * 定时任务 2:周期 30 秒一次,两个任务 + + * 清理下线的 Broker 节点,遍历客户端的 Broker 物理节点映射表,将所有主题数据都不包含的 Broker 物理节点清理掉,如果被清理的 Broker 下所有的物理节点都没有了,就将该 Broker 的映射数据删除掉 + * 向在线的所有的 Broker 发送心跳数据,**同步发送的方式**,返回值是 Broker 物理节点的版本号,更新版本映射表 + + ```java + MQClientInstance.this.cleanOfflineBroker(); + MQClientInstance.this.sendHeartbeatToAllBrokerWithLock(); + ``` + + ```java + // 心跳数据 + public class HeartbeatData extends RemotingSerializable { + // 客户端 ID ip@pid + private String clientID; + // 存储客户端所有生产者数据 + private Set producerDataSet = new HashSet(); + // 存储客户端所有消费者数据 + private Set consumerDataSet = new HashSet(); + } + ``` + + * 定时任务 3:消费者持久化消费数据,周期 5 秒一次 + + ```java + MQClientInstance.this.persistAllConsumerOffset(); + ``` + + * 定时任务 4:动态调整消费者线程池,周期 1 分钟一次 + + ```java + MQClientInstance.this.adjustThreadPool(); + ``` + +* updateTopicRouteInfoFromNameServer():**更新路由数据** + + * `if (isDefault && defaultMQProducer != null)`:需要默认数据 + + `topicRouteData = ...getDefaultTopicRouteInfoFromNameServer()`:从 Namesrv 获取默认的 TBW102 的路由数据 + + `int queueNums`:遍历所有队列,为每个读写队列设置较小的队列数 + + * `topicRouteData = ...getTopicRouteInfoFromNameServer(topic)`:需要**从 Namesrv 获取**路由数据(同步) + + * `old = this.topicRouteTable.get(topic)`:获取客户端实例本地的该主题的路由数据 + + * `boolean changed = topicRouteDataIsChange(old, topicRouteData)`:对比本地和最新下拉的数据是否一致 + + * `if (changed)`:不一致进入更新逻辑 + + `cloneTopicRouteData = topicRouteData.cloneTopicRouteData()`:克隆一份最新数据 + + `Update Pub info`:更新生产者信息 + + * `publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData)`:**将主题路由数据转化为发布数据** + * `impl.updateTopicPublishInfo(topic, publishInfo)`:生产者将主题的发布数据保存到它本地,方便发送消息使用 + + `Update sub info`:更新消费者信息 + + `this.topicRouteTable.put(topic, cloneTopicRouteData)`:将数据放入本地路由表 + + + +**** + + + +#### 网络通信 + +##### 成员属性 + +NettyRemotingClient 类负责客户端的网络通信 + +成员变量: + +* Netty 服务相关属性: + + ```java + private final NettyClientConfig nettyClientConfig; // 客户端的网络层配置 + private final Bootstrap bootstrap = new Bootstrap(); // 客户端网络层启动对象 + private final EventLoopGroup eventLoopGroupWorker; // 客户端网络层 Netty IO 线程组 + ``` + +* Channel 映射表: + + ```java + private final ConcurrentMap channelTables;// key 是服务器的地址,value 是通道对象 + private final Lock lockChannelTables = new ReentrantLock(); // 锁,控制并发安全 + ``` + +* 定时器:启动定时任务 + + ```java + private final Timer timer = new Timer("ClientHouseKeepingService", true) + ``` + +* 线程池: + + ```java + private ExecutorService publicExecutor; // 公共线程池 + private ExecutorService callbackExecutor; // 回调线程池,客户端发起异步请求,服务器的响应数据由回调线程池处理 + ``` + +* 事件监听器:客户端这里是 null + + ```java + private final ChannelEventListener channelEventListener; + ``` + +构造方法 + +* 无参构造: + + ```java + public NettyRemotingClient(final NettyClientConfig nettyClientConfig) { + this(nettyClientConfig, null); + } + ``` + +* 有参构造: + + ```java + public NettyRemotingClient(nettyClientConfig, channelEventListener) { + // 父类创建了2个信号量,1、控制单向请求的并发度,2、控制异步请求的并发度 + super(nettyClientConfig.getClientOnewaySemaphoreValue(), nettyClientConfig.getClientAsyncSemaphoreValue()); + this.nettyClientConfig = nettyClientConfig; + this.channelEventListener = channelEventListener; + + // 创建公共线程池 + int publicThreadNums = nettyClientConfig.getClientCallbackExecutorThreads(); + if (publicThreadNums <= 0) { + publicThreadNums = 4; + } + this.publicExecutor = Executors.newFixedThreadPool(publicThreadNums,); + + // 创建 Netty IO 线程,1个线程 + this.eventLoopGroupWorker = new NioEventLoopGroup(1, ); + + if (nettyClientConfig.isUseTLS()) { + sslContext = TlsHelper.buildSslContext(true); + } + } + ``` + + + +**** + + + +##### 成员方法 + +* start():启动方法 + + ```java + public void start() { + // channel pipeline 内的 handler 使用的线程资源,默认 4 个 + this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(); + // 配置 netty 客户端启动类对象 + Bootstrap handler = this.bootstrap.group(this.eventLoopGroupWorker).channel(NioSocketChannel.class) + //... + .handler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + // 加几个handler + pipeline.addLast( + // 服务端的数据,都会来到这个 + new NettyClientHandler()); + } + }); + // 注意 Bootstrap 只是配置好客户端的元数据了,【在这里并没有创建任何 channel 对象】 + // 定时任务 扫描 responseTable 中超时的 ResponseFuture,避免客户端线程长时间阻塞 + this.timer.scheduleAtFixedRate(() -> { + NettyRemotingClient.this.scanResponseTable(); + }, 1000 * 3, 1000); + // 这里是 null,不启动 + if (this.channelEventListener != null) { + this.nettyEventExecutor.start(); + } + } + ``` + +* 单向通信: + + ```java + public RemotingCommand invokeSync(String addr, final RemotingCommand request, long timeoutMillis) { + // 开始时间 + long beginStartTime = System.currentTimeMillis(); + // 获取或者创建客户端与服务端(addr)的通道 channel + final Channel channel = this.getAndCreateChannel(addr); + // 条件成立说明客户端与服务端 channel 通道正常,可以通信 + if (channel != null && channel.isActive()) { + try { + // 执行 rpcHook 拓展点 + doBeforeRpcHooks(addr, request); + // 计算耗时,如果当前耗时已经超过 timeoutMillis 限制,则直接抛出异常,不再进行系统通信 + long costTime = System.currentTimeMillis() - beginStartTime; + if (timeoutMillis < costTime) { + throw new RemotingTimeoutException("invokeSync call timeout"); + } + // 参数1:客户端-服务端通道channel + // 参数二:网络层传输对象,封装着请求数据 + // 参数三:剩余的超时限制 + RemotingCommand response = this.invokeSyncImpl(channel, request, ...); + // 后置处理 + doAfterRpcHooks(RemotingHelper.parseChannelRemoteAddr(channel), request, response); + // 返回响应数据 + return response; + } catch (RemotingSendRequestException e) {} + } else { + this.closeChannel(addr, channel); + throw new RemotingConnectException(addr); + } + } + ``` + + + + + +**** + + + +### 消费者 + +#### 消费者类 + +##### 默认消费 + +DefaultMQPushConsumer 类是默认的消费者类 + +成员变量: + +* 消费者实现类: + + ```java + protected final transient DefaultMQPushConsumerImpl defaultMQPushConsumerImpl; + ``` + +* 消费属性: + + ```java + private String consumerGroup; // 消费者组 + private MessageModel messageModel = MessageModel.CLUSTERING; // 消费模式,默认集群模式 + ``` + +* 订阅信息:key 是主题,value 是过滤表达式,一般是 tag + + ```java + private Map subscription = new HashMap() + ``` + +* 消息监听器:**消息处理逻辑**,并发消费 MessageListenerConcurrently,顺序(分区)消费 MessageListenerOrderly + + ```java + private MessageListener messageListener; + ``` + +* 消费位点:当从 Broker 获取当前组内该 queue 的 offset 不存在时,consumeFromWhere 才有效,默认值代表从队列的最后 offset 开始消费,当队列内再有一条新的 msg 加入时,消费者才会去消费 + + ```java + private ConsumeFromWhere consumeFromWhere = ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET; + ``` + +* 消费时间戳:当消费位点配置的是 CONSUME_FROM_TIMESTAMP 时,并且服务器 Group 内不存在该 queue 的 offset 时,会使用该时间戳进行消费 + + ```java + private String consumeTimestamp = UtilAll.timeMillisToHumanString3(System.currentTimeMillis() - (1000 * 60 * 30));// 消费者创建时间 - 30秒,转换成 格式: 年月日小时分钟秒,比如 20220203171201 + ``` + +* 队列分配策略:主题下的队列分配策略,RebalanceImpl 对象依赖该算法 + + ```java + private AllocateMessageQueueStrategy allocateMessageQueueStrategy; + ``` + +* 消费进度存储器: + + ```java + private OffsetStore offsetStore; + ``` + +核心方法: + +* start():启动消费者 + + ```java + public void start() + ``` + +* shutdown():关闭消费者 + + ```java + public void shutdown() + ``` + +* registerMessageListener():注册消息监听器 + + ```java + public void registerMessageListener(MessageListener messageListener) + ``` + +* subscribe():添加订阅信息 + + ```java + public void subscribe(String topic, String subExpression) + ``` + +* unsubscribe():删除订阅指定主题的信息 + + ```java + public void unsubscribe(String topic) + ``` + +* suspend():停止消费 + + ```java + public void suspend() + ``` + +* resume():恢复消费 + + ```java + public void resume() + ``` + + + +*** + + + +##### 默认实现 + +DefaultMQPushConsumerImpl 是默认消费者的实现类 + +成员变量: + +* 客户端实例:整个进程内只有一个客户端实例对象 + + ```java private MQClientInstance mQClientFactory; ``` @@ -7570,488 +8129,618 @@ DefaultMQPushConsumerImpl 是默认消费者的实现类 +*** + + + +#### 负载均衡 + +##### 实现方式 + +MQClientInstance#start 中会启动负载均衡服务: + +```java +public void run() { + // 检查停止标记 + while (!this.isStopped()) { + // 休眠 20 秒,防止其他线程饥饿,所以【每 20 秒负载均衡一次】 + this.waitForRunning(waitInterval); + // 调用客户端实例的负载均衡方法,底层【会遍历所有消费者,调用消费者的负载均衡】 + this.mqClientFactory.doRebalance(); + } +} +``` + +RebalanceImpl 类成员变量: + +* 分配给当前消费者的处理队列:处理消息队列集合,ProcessQueue 是 MQ 队列在消费者端的快照 + ```java + protected final ConcurrentMap processQueueTable; + ``` +* 消费者订阅主题的队列信息: + ```java + protected final ConcurrentMap> topicSubscribeInfoTable; + ``` +* 订阅数据: + ```java + protected final ConcurrentMap subscriptionInner; + ``` +* 队列分配策略: + ```java + protected AllocateMessageQueueStrategy allocateMessageQueueStrategy; + ``` +成员方法: +* doRebalance():负载均衡方法 + ```java + public void doRebalance(final boolean isOrder) { + // 获取当前消费者的订阅数据 + Map subTable = this.getSubscriptionInner(); + if (subTable != null) { + // 遍历所有的订阅主题 + for (final Entry entry : subTable.entrySet()) { + // 获取订阅的主题 + final String topic = entry.getKey(); + // 按照主题进行负载均衡 + this.rebalanceByTopic(topic, isOrder); + } + } + // 将分配到当前消费者的队列进行过滤,不属于当前消费者订阅主题的直接移除 + this.truncateMessageQueueNotMyTopic(); + } + ``` + * `Set mqSet = this.topicSubscribeInfoTable.get(topic)`:获取当前主题的全部队列信息 + * `cidAll = this...findConsumerIdList(topic, consumerGroup)`:从服务器获取消费者组下的全部消费者 ID + * `Collections.sort(mqAll)`:主题 MQ 队列和消费者 ID 都进行排序,保证每个消费者的视图一致性 + * `strategy = this.allocateMessageQueueStrategy`:获取队列分配策略对象 + * `allocateResult = strategy.allocate()`: **调用队列分配策略**,给当前消费者进行分配 MessageQueue + * `boolean changed = this.updateProcessQueueTableInRebalance(...)`:负载均衡,更新队列处理集合 + * `boolean changed = false`:当前消费者的消费队列是否有变化 -*** + * `while (it.hasNext())`:遍历当前消费者的所有处理队列 + * `if (!mqSet.contains(mq))`:该 MQ 经过 rbl 计算之后,**被分配到其它 consumer 节点** + `pq.setDropped(true)`:将删除状态设置为 true -### 客户端 + `if (this.removeUnnecessaryMessageQueue(mq, pq))`:在 MQ 归属的 broker 节点持久化消费进度,并删除该 MQ 在本地的消费进度 -#### 公共配置 + `it.remove()`:从 processQueueTable 移除该 MQ -公共的配置信息类 + * `else if (pq.isPullExpired())`:说明当前 MQ 还是被当前 consumer 消费,此时判断一下是否超过 2 分钟未到服务器 拉消息,如果条件成立进行上述相同的逻辑、 -* ClientConfig 类 + * `for (MessageQueue mq : mqSet)`:开始处理当前主题**新分配**到当前节点的队列 - ```java - public class ClientConfig { - // Namesrv 地址配置 - private String namesrvAddr = NameServerAddressUtils.getNameServerAddresses(); - // 客户端的 IP 地址 - private String clientIP = RemotingUtil.getLocalAddress(); - // 客户端实例名称 - private String instanceName = System.getProperty("rocketmq.client.name", "DEFAULT"); - // 客户端回调线程池的数量,平台核心数,8核16线程的电脑返回16 - private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); - // 命名空间 - protected String namespace; - protected AccessChannel accessChannel = AccessChannel.LOCAL; - - // 获取路由信息的间隔时间 30s - private int pollNameServerInterval = 1000 * 30; - // 客户端与 broker 之间的心跳周期 30s - private int heartbeatBrokerInterval = 1000 * 30; - // 消费者持久化消费的周期 5s - private int persistConsumerOffsetInterval = 1000 * 5; - private long pullTimeDelayMillsWhenException = 1000; - private boolean unitMode = false; - private String unitName; - // vip 通道,broker 启动时绑定两个端口,其中一个是 vip 通道 - private boolean vipChannelEnabled = Boolean.parseBoolean(); - // 语言,默认是 Java - private LanguageCode language = LanguageCode.JAVA; - } - ``` + * `if (isOrder && !this.lock(mq))`:**顺序消息为了保证有序性,需要获取分布式锁** -* NettyClientConfig + * `ProcessQueue pq = new ProcessQueue()`:为每个新分配的消息队列创建快照队列 + + * `long nextOffset = this.computePullFromWhere(mq)`:**从服务端获取新分配的 MQ 的消费进度** + + * `ProcessQueue pre = this.processQueueTable.putIfAbsent(mq, pq)`:保存到处理队列集合 + + * `PullRequest pullRequest = new PullRequest()`:创建拉取请求对象 + + * `this.dispatchPullRequest(pullRequestList)`:放入拉消息服务的本地阻塞队列内,**用于拉取消息工作** + + + + +*** + + + +##### 队列分配 + +AllocateMessageQueueStrategy 类是队列的分配策略 + +* 平均分配:AllocateMessageQueueAveragely 类 ```java - public class NettyClientConfig { - // 客户端工作线程数 - private int clientWorkerThreads = 4; - // 回调处理线程池 线程数:平台核心数 - private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); - // 单向请求并发数,默认 65535 - private int clientOnewaySemaphoreValue = NettySystemConfig.CLIENT_ONEWAY_SEMAPHORE_VALUE; - // 异步请求并发数,默认 65535 - private int clientAsyncSemaphoreValue = NettySystemConfig.CLIENT_ASYNC_SEMAPHORE_VALUE; - // 客户端连接服务器的超时时间限制 3秒 - private int connectTimeoutMillis = 3000; - // 客户端未激活周期,60s(指定时间内 ch 未激活,需要关闭) - private long channelNotActiveInterval = 1000 * 60; - // 客户端与服务器 ch 最大空闲时间 2分钟 - private int clientChannelMaxIdleTimeSeconds = 120; - - // 底层 Socket 写和收 缓冲区的大小 65535 64k - private int clientSocketSndBufSize = NettySystemConfig.socketSndbufSize; - private int clientSocketRcvBufSize = NettySystemConfig.socketRcvbufSize; - // 客户端 netty 是否启动内存池 - private boolean clientPooledByteBufAllocatorEnable = false; - // 客户端是否超时关闭 Socket 连接 - private boolean clientCloseSocketIfTimeout = false; + // 参数一:消费者组 参数二:当前消费者id + // 参数三:主题的全部队列,包括所有 broker 上该主题的 mq 参数四:全部消费者id集合 + public List allocate(String consumerGroup, String currentCID, List mqAll, List cidAll) { + // 获取当前消费者在全部消费者中的位置,【全部消费者是已经排序好的,排在前面的优先分配更多的队列】 + int index = cidAll.indexOf(currentCID); + // 平均分配完以后,还剩余的待分配的 mq 的数量 + int mod = mqAll.size() % cidAll.size(); + // 首先判断整体的 mq 的数量是否小于消费者的数量,小于消费者的数量就说明不够分的,先分一个 + int averageSize = mqAll.size() <= cidAll.size() ? 1 : + // 成立需要多分配一个队列,因为更靠前 + (mod > 0 && index < mod ? mqAll.size() / cidAll.size() + 1 : mqAll.size() / cidAll.size()); + // 获取起始的分配位置 + int startIndex = (mod > 0 && index < mod) ? index * averageSize : index * averageSize + mod; + // 防止索引越界 + int range = Math.min(averageSize, mqAll.size() - startIndex); + // 开始分配,【挨着分配,是直接就把当前的 消费者分配完成】 + for (int i = 0; i < range; i++) { + result.add(mqAll.get((startIndex + i) % mqAll.size())); + } + return result; } ``` + 队列排序后:Q1 → Q2 → Q3,消费者排序后 C1 → C2 → C3 + + ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-平均队列分配.png) + +* 轮流分配:AllocateMessageQueueAveragelyByCircle + + ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-平均队列轮流分配.png) + +* 指定机房平均分配:AllocateMessageQueueByMachineRoom,前提是 Broker 的命名规则为 `机房名@BrokerName` + + + *** -#### 实例对象 +#### 消息拉取 -##### 成员属性 +##### 实现方式 -MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一个客户端实例,**既服务于生产者,也服务于消费者** +MQClientInstance#start 中会启动消息拉取服务:PullMessageService + +```java +public void run() { + // 检查停止标记 + while (!this.isStopped()) { + try { + // 从阻塞队列中获取拉消息请求 + PullRequest pullRequest = this.pullRequestQueue.take(); + // 拉取消息,获取请求对应的使用当前消费者组中的哪个消费者,调用消费者的 pullMessage 方法 + this.pullMessage(pullRequest); + } catch (Exception e) { + log.error("Pull Message Service Run Method exception", e); + } + } +} +``` + +DefaultMQPushConsumerImpl#pullMessage: + +* `ProcessQueue processQueue = pullRequest.getProcessQueue()`:获取请求对应的快照队列,并判断是否是删除状态 + +* `this.executePullRequestLater()`:如果当前消费者不是运行状态,则拉消息任务延迟 3 秒后执行,如果是暂停状态延迟 1 秒 + +* **流控的逻辑**: + + `long cachedMessageCount = processQueue.getMsgCount().get()`:获取消费者本地该 queue 快照内缓存的消息数量,如果大于 1000 条,进行流控,延迟 50 毫秒 + + `long cachedMessageSizeInMiB`: 消费者本地该 queue 快照内缓存的消息容量 size,超过 100m 消息未被消费进行流控 + + `if(processQueue.getMaxSpan() > 2000)`:消费者本地缓存消息第一条消息最后一条消息跨度超过 2000 进行流控 + +* `SubscriptionData subscriptionData`:本次拉消息请求订阅的主题数据,如果调用了 `unsubscribe(主题)` 将会获取为 null + +* `PullCallback pullCallback = new PullCallback()`:**拉消息处理回调对象**, + + * `pullResult = ...processPullResult()`:预处理 PullResult 结果 + + * `case FOUND`:正常拉取到消息 + + `pullRequest.setNextOffset(pullResult.getNextBeginOffset())`:更新 pullRequest 对象下一次拉取消息的位点 + + `if (pullResult.getMsgFoundList() == null...)`:消息过滤导致消息全部被过滤掉,需要立马发起下一次拉消息 + + `boolean .. = processQueue.putMessage()`:将服务器拉取的消息集合**加入到消费者本地**的 processQueue 内 + + `DefaultMQPushConsumerImpl...submitConsumeRequest()`:**提交消费任务**,分为顺序消费和并发消费 + + `Defaul..executePullRequestImmediately(pullRequest)`:将更新过 nextOffset 字段的 PullRequest 对象,再次放到 pullMessageService 的阻塞队列中,**形成闭环** + + * `case NO_NEW_MSG ||NO_MATCHED_MSG`:表示本次 pull 没有新的可消费的信息 + + `pullRequest.setNextOffset()`:更新更新 pullRequest 对象下一次拉取消息的位点 + + `Defaul..executePullRequestImmediately(pullRequest)`:再次拉取请求 + + * `case OFFSET_ILLEGAL`:本次 pull 时使用的 offset 是无效的,即 offset > maxOffset || offset < minOffset + + `pullRequest.setNextOffset()`:调整pullRequest nextOffset 为 正确的 offset + + `pullRequest.getProcessQueue().setDropped(true)`:设置该 processQueue 为删除状态,如果有该 queue 的消费任务,该消费任务会马上停止任务 + + `DefaultMQPushConsumerImpl.this.executeTaskLater()`:提交异步任务,10 秒后去执行 + + * `DefaultMQPushConsumerImpl...updateOffset()`:更新 offsetStore 该 MQ 的 offset 为正确值,内部直接替换 + + * `DefaultMQPushConsumerImpl...persist()`:持久化该 messageQueue 的 offset 到 Broker 端 + + * `DefaultMQPushConsumerImpl...removeProcessQueue()`: 删除该消费者该 messageQueue 对应的 processQueue + + * 这里没有再次提交 pullRequest 到 pullMessageService 的队列,那该队列不再拉消息了吗? + + 负载均衡 rbl 程序会重建该队列的 processQueue,重建完之后会为该队列创建新的 PullRequest 对象 + +* `int sysFlag = PullSysFlag.buildSysFlag()`:构建标志对象,sysFlag 高 4 位未使用,低 4 位使用,从左到右为 0000 0011 + + * 第一位:表示是否提交消费者本地该队列的 offset,一般是 1 + * 第二位:表示是否允许服务器端进行长轮询,一般是 1 + * 第三位:表示是否提交消费者本地该主题的订阅数据,一般是 0 + * 第四位:表示是否为类过滤,一般是 0 + +* `this.pullAPIWrapper.pullKernelImpl()`:拉取消息的核心方法 + + + +*** + + + +##### 封装对象 + +PullAPIWrapper 类封装了拉取消息的 API 成员变量: -* 配置信息: +* 推荐拉消息使用的主机 ID: ```java - private final int instanceIndex; // 索引一般是 0,因为客户端实例一般都是一个进程只有一个 - private final String clientId; // 客户端 ID ip@pid - private final long bootTimestamp; // 客户端的启动时间 - private ServiceState serviceState; // 客户端状态 + private ConcurrentMap pullFromWhichNodeTable ``` -* 生产者消费者的映射表:key 是组名 +成员方法: - ```java - private final ConcurrentMap producerTable - private final ConcurrentMap consumerTable - private final ConcurrentMap adminExtTable - ``` +* pullKernelImpl():拉消息 -* 网络层配置: + * `FindBrokerResult findBrokerResult`:查询指定 BrokerName 的地址信息,主节点或者推荐节点 - ```java - private final NettyClientConfig nettyClientConfig; - ``` + * `if (null == findBrokerResult)`:查询不到,就到 Namesrv 获取指定 topic 的路由数据 -* 核心功能的实现:负责将 MQ 业务层的数据转换为网络层的 RemotingCommand 对象,使用内部持有的 NettyRemotingClient 对象的 invoke 系列方法,完成网络 IO(同步、异步、单向) + * `if (findBrokerResult.isSlave())`:成立说明 findBrokerResult 表示的主机为 slave 节点,**slave 不存储 offset 信息** - ```java - private final MQClientAPIImpl mQClientAPIImpl; - ``` + `sysFlagInner = PullSysFlag.clearCommitOffsetFlag(sysFlagInner)`:将 sysFlag 标记位中 CommitOffset 的位置为 0 -* 本地路由数据:key 是主题名称,value 路由信息 + * `PullMessageRequestHeader requestHeader`:创建请求头对象,封装所有的参数 - ```java - private final ConcurrentMap topicRouteTable = new ConcurrentHashMap<>(); + * `PullResult pullResult = this.mQClientFactory.getMQClientAPIImpl().pullMessage()`:调用客户端实例的方法,核心逻辑就是**将业务数据转化为 RemotingCommand 通过 NettyRemotingClient 的 IO 进行通信** -* 锁信息:两把锁,锁不同的数据 + * `RemotingCommand request`:创建网络层传输对象 RemotingCommand 对象,**请求 ID 为 `PULL_MESSAGE = 11`** - ```java - private final Lock lockNamesrv = new ReentrantLock(); - private final Lock lockHeartbeat = new ReentrantLock(); - ``` + * `return this.pullMessageSync(...)`:此处是异步调用,**处理结果放入 ResponseFuture 中**,参考服务端小节的处理器类 `NettyServerHandler#processMessageReceived` 方法 -* 调度线程池:单线程,执行定时任务 + * `RemotingCommand response = responseFuture.getResponseCommand()`:获取服务器端响应数据 response + * `PullResult pullResult`:从 response 内提取出来拉消息结果对象,将响应头 PullMessageResponseHeader 对象中信息**填充到 PullResult 中**,列出两个重要的字段: + * `private Long suggestWhichBrokerId`:服务端建议客户端下次 Pull 时选择的 BrokerID + * `private Long nextBeginOffset`:客户端下次 Pull 时使用的 offset 信息 - ```java - private final ScheduledExecutorService scheduledExecutorService; - ``` + * `pullCallback.onSuccess(pullResult)`:将 PullResult 交给拉消息结果处理回调对象,调用 onSuccess 方法 -* Broker 映射表:key 是 BrokerName +* processPullResult():预处理拉消息结果,**更新推荐 Broker 和过滤消息** - ```java - // 物理节点映射表,value:Long 是 brokerID,【ID=0 的是主节点,其他是从节点】,String 是地址 ip:port - private final ConcurrentMap> brokerAddrTable; - // 物理节点版本映射表,String 是地址 ip:port,Integer 是版本 - ConcurrentMap> brokerVersionTable; - ``` + * `this.updatePullFromWhichNode()`:更新 pullFromWhichNodeTable 内该 MQ 的下次查询推荐 BrokerID + * `if (PullStatus.FOUND == pullResult.getPullStatus())`:条件成立说明拉取成功 + * `List msgList`:**将获取的消息进行解码** + * `if (!subscriptionData... && !subscriptionData.isClassFilterMode())`:客户端按照 tag 值进行过滤 + * `pullResultExt.setMsgFoundList(msgListFilterAgain)`:将再次过滤后的消息集合,保存到 pullResult + * `pullResultExt.setMessageBinary(null)`:设置为 null,帮助 GC + + + +*** + + + +#### 通信处理 + +##### 处理器 -* **客户端的协议处理器**:用于处理 IO 事件 +BrokerStartup#createBrokerController 方法中创建了 BrokerController 并进行初始化,调用 `registerProcessor()` 方法将处理器 PullMessageProcessor 注册到 NettyRemotingServer 中,对应的请求 ID 为 `PULL_MESSAGE = 11`,NettyServerHandler 在处理请求时通过请求 ID 会获取处理器执行 processRequest 方法 - ```java - private final ClientRemotingProcessor clientRemotingProcessor; - ``` +```java +// 参数一:服务器与客户端 netty 通道; 参数二:客户端请求; 参数三:是否允许服务器端长轮询,默认 true +private RemotingCommand processRequest(final Channel channel, RemotingCommand request, boolean brokerAllowSuspend) +``` -* 消息服务: +* `RemotingCommand response`:创建响应对象,设置为响应类型的请求,响应头是 PullMessageResponseHeader - ```java - private final PullMessageService pullMessageService; // 拉消息服务 - private final RebalanceService rebalanceService; // 消费者负载均衡服务 - private final ConsumerStatsManager consumerStatsManager; // 消费者状态管理 - ``` +* `final PullMessageResponseHeader responseHeader`:获取响应对象的 header -* 内部生产者实例:处理消费端**消息回退**,用该生产者发送回执消息 +* `final PullMessageRequestHeader requestHeader`:解析出请求头 PullMessageRequestHeader - ```java - private final DefaultMQProducer defaultMQProducer; - ``` +* `response.setOpaque(request.getOpaque())`:设置 opaque 属性,客户端需要根据该字段**获取 ResponseFuture** -* 心跳次数统计: +* 进行一些鉴权的逻辑:是否允许长轮询、提交 offset、topicConfig 是否是空、队列 ID 是否合理 - ```java - private final AtomicLong sendHeartbeatTimesTotal = new AtomicLong(0) - ``` +* `ConsumerGroupInfo consumerGroupInfo`:获取消费者组信息,包好全部的消费者和订阅数据 -构造方法: +* `subscriptionData = consumerGroupInfo.findSubscriptionData()`:**获取指定主题的订阅数据** -* MQClientInstance 有参构造: +* `if (!ExpressionType.isTagType()`:表达式匹配 - ```java - public MQClientInstance(ClientConfig clientConfig, int instanceIndex, String clientId, RPCHook rpcHook) { - this.clientConfig = clientConfig; - this.instanceIndex = instanceIndex; - // Netty 相关的配置信息 - this.nettyClientConfig = new NettyClientConfig(); - // 平台核心数 - this.nettyClientConfig.setClientCallbackExecutorThreads(...); - this.nettyClientConfig.setUseTLS(clientConfig.isUseTLS()); - // 【创建客户端协议处理器】 - this.clientRemotingProcessor = new ClientRemotingProcessor(this); - // 创建 API 实现对象 - // 参数一:客户端网络配置 - // 参数二:客户端协议处理器,注册到客户端网络层 - // 参数三:rpcHook,注册到客户端网络层 - // 参数四:客户端配置 - this.mQClientAPIImpl = new MQClientAPIImpl(this.nettyClientConfig, this.clientRemotingProcessor, rpcHook, clientConfig); - - //... - // 内部生产者,指定内部生产者的组 - this.defaultMQProducer = new DefaultMQProducer(MixAll.CLIENT_INNER_PRODUCER_GROUP); - } - ``` +* `MessageFilter messageFilter`:创建消息过滤器,一般是通过 tagCode 进行过滤 -* MQClientAPIImpl 有参构造: +* `DefaultMessageStore.getMessage()`:**查询消息的核心逻辑**,在 Broker 端查询消息(存储端笔记详解了该源码) - ```java - public MQClientAPIImpl(nettyClientConfig, clientRemotingProcessor, rpcHook, clientConfig) { - this.clientConfig = clientConfig; - topAddressing = new TopAddressing(MixAll.getWSAddr(), clientConfig.getUnitName()); - // 创建网络层对象,参数二为 null 说明客户端并不关心 channel event - this.remotingClient = new NettyRemotingClient(nettyClientConfig, null); - // 业务处理器 - this.clientRemotingProcessor = clientRemotingProcessor; - // 注册 RpcHook - this.remotingClient.registerRPCHook(rpcHook); - // ... - // 注册回退消息的请求码 - this.remotingClient.registerProcessor(RequestCode.PUSH_REPLY_MESSAGE_TO_CLIENT, this.clientRemotingProcessor, null); - } - ``` +* `response.setRemark()`:设置此次响应的状态 +* `responseHeader.set..`:设置响应头对象的一些字段 +* `switch (this.brokerController.getMessageStoreConfig().getBrokerRole())`:如果当前主机节点角色为 slave 并且**从节点读**并未开启的话,直接给客户端 一个状态 `PULL_RETRY_IMMEDIATELY` -*** +* `if (this.brokerController.getBrokerConfig().isSlaveReadEnable())`:消费太慢,下次从另一台机器拉取 +* `switch (getMessageResult.getStatus())`:根据 getMessageResult 的状态设置 response 的 code + ```java + public enum GetMessageStatus { + FOUND, // 查询成功 + NO_MATCHED_MESSAGE, // 未查询到到消息,服务端过滤 tagCode + MESSAGE_WAS_REMOVING, // 查询时赶上 CommitLog 清理过期文件,导致查询失败,立刻尝试 + OFFSET_FOUND_NULL, // 查询时赶上 ConsumerQueue 清理过期文件,导致查询失败,【进行长轮询】 + OFFSET_OVERFLOW_BADLY, // pullRequest.offset 越界 maxOffset + OFFSET_OVERFLOW_ONE, // pullRequest.offset == CQ.maxOffset,【进行长轮询】 + OFFSET_TOO_SMALL, // pullRequest.offset 越界 minOffset + NO_MATCHED_LOGIC_QUEUE, // 没有匹配到逻辑队列 + NO_MESSAGE_IN_QUEUE, // 空队列,创建队列也是因为查询导致,【进行长轮询】 + } + ``` -##### 成员方法 +* `switch (response.getCode())`:根据 response 状态做对应的业务处理 -* start():启动方法 + `case ResponseCode.SUCCESS`:查询成功 - * `synchronized (this)`:加锁保证线程安全,保证只有一个实例对象启动 - * `this.mQClientAPIImpl.start()`:启动客户端网络层,底层调用 RemotingClient 类 - * `this.startScheduledTask()`:启动定时任务 - * `this.pullMessageService.start()`:启动拉取消息服务 - * `this.rebalanceService.start()`:启动负载均衡服务 - * `this.defaultMQProducer...start(false)`:启动内部生产者,参数为 false 代表不启动实例 + * `final byte[] r = this.readGetMessageResult()`:本次 pull 出来的全部消息导入 byte 数组 + * `response.setBody(r)`:将消息的 byte 数组保存到 response body 字段 -* startScheduledTask():**启动定时任务**,调度线程池是单线程 + `case ResponseCode.PULL_NOT_FOUND`:产生这种情况大部分原因是 `pullRequest.offset == queue.maxOffset`,说明已经没有需要获取的消息,此时如果直接返回给客户端,客户端会立刻重新请求,还是继续返回该状态,频繁拉取服务器导致服务器压力大,所以此处**需要长轮询** - * `if (null == this.clientConfig.getNamesrvAddr())`:Namesrv 地址是空,需要两分钟拉取一次 Namesrv 地址 + * `if (brokerAllowSuspend && hasSuspendFlag)`:brokerAllowSuspend = true,当长轮询结束再次执行 processRequest 时该参数为 false,所以**每次 Pull 请求至多在服务器端长轮询控制一次** + * `PullRequest pullRequest = new PullRequest()`:创建长轮询 PullRequest 对象 + * `this.brokerController...suspendPullRequest(topic, queueId, pullRequest)`:将长轮询请求对象交给长轮询服务 + * `String key = this.buildKey(topic, queueId)`:构建一个 `topic@queueId` 的 key + * `ManyPullRequest mpr = this.pullRequestTable.get(key)`:从拉请求表中获取对象 + * `mpr.addPullRequest(pullRequest)`:将 PullRequest 对象放入到 ManyPullRequest 的请求集合中 + * `response = null`:响应设置为 null 内部的 callBack 就不会给客户端发送任何数据,**不进行通信**,否则就又开始重新请求 - * 定时任务 1:从 Namesrv 更新客户端本地的路由数据,周期 30 秒一次 +* `boolean storeOffsetEnable`:允许长轮询、sysFlag 表示提交消费者本地该队列的offset、当前 broker 节点角色为 master 节点三个条件成立,才**在 Broker 端存储消费者组内该主题的指定 queue 的消费进度** - ```java - // 获取生产者和消费者订阅的主题集合,遍历集合,对比从 namesrv 拉取最新的主题路由数据和本地数据,是否需要更新 - MQClientInstance.this.updateTopicRouteInfoFromNameServer(); - ``` +* `return response`:返回 response,不为 null 时外层 requestTask 的 callback 会将数据写给客户端 - * 定时任务 2:周期 30 秒一次,两个任务 - * 清理下线的 Broker 节点,遍历客户端的 Broker 物理节点映射表,将所有主题数据都不包含的 Broker 物理节点清理掉,如果被清理的 Broker 下所有的物理节点都没有了,就将该 Broker 的映射数据删除掉 - * 向在线的所有的 Broker 发送心跳数据,**同步发送的方式**,返回值是 Broker 物理节点的版本号,更新版本映射表 - ```java - MQClientInstance.this.cleanOfflineBroker(); - MQClientInstance.this.sendHeartbeatToAllBrokerWithLock(); - ``` +*** - ```java - // 心跳数据 - public class HeartbeatData extends RemotingSerializable { - // 客户端 ID ip@pid - private String clientID; - // 存储客户端所有生产者数据 - private Set producerDataSet = new HashSet(); - // 存储客户端所有消费者数据 - private Set consumerDataSet = new HashSet(); - } - ``` - * 定时任务 3:消费者持久化消费数据,周期 5 秒一次 - ```java - MQClientInstance.this.persistAllConsumerOffset(); - ``` +##### 长轮询 - * 定时任务 4:动态调整消费者线程池,周期 1 分钟一次 +PullRequestHoldService 类负责长轮询,BrokerController#start 方法中调用了 `this.pullRequestHoldService.start()` 启动该服务 - ```java - MQClientInstance.this.adjustThreadPool(); - ``` +核心方法: -* updateTopicRouteInfoFromNameServer():**更新路由数据** +* run():核心运行方法 - * `if (isDefault && defaultMQProducer != null)`:需要默认数据 + ```java + public void run() { + // 循环运行 + while (!this.isStopped()) { + if (this.brokerController.getBrokerConfig().isLongPollingEnable()) { + // 服务器开启长轮询开关:每次循环休眠5秒 + this.waitForRunning(5 * 1000); + } else { + // 服务器关闭长轮询开关:每次循环休眠1秒 + this.waitForRunning(...); + } + // 检查持有的请求 + this.checkHoldRequest(); + // ..... + } + } + ``` - `topicRouteData = ...getDefaultTopicRouteInfoFromNameServer()`:从 Namesrv 获取默认的 TBW102 的路由数据 +* checkHoldRequest():检查所有的请求 - `int queueNums`:遍历所有队列,为每个读写队列设置较小的队列数 + * `for (String key : this.pullRequestTable.keySet())`:**处理所有的 topic@queueId 的逻辑** + * `String[] kArray = key.split(TOPIC_QUEUEID_SEPARATOR)`:key 按照 @ 拆分,得到 topic 和 queueId + * `long offset = this...getMaxOffsetInQueue(topic, queueId)`: 到存储模块查询该 ConsumeQueue 的**最大 offset** + * `this.notifyMessageArriving(topic, queueId, offset)`:通知消息到达 - * `topicRouteData = ...getTopicRouteInfoFromNameServer(topic)`:需要**从 Namesrv 获取**路由数据(同步) +* notifyMessageArriving():通知消息到达的逻辑,ReputMessageService 消息分发服务也会调用该方法 - * `old = this.topicRouteTable.get(topic)`:获取客户端实例本地的该主题的路由数据 + * `ManyPullRequest mpr = this.pullRequestTable.get(key)`:获取对应的的manyPullRequest对象 + * `List requestList`:获取该队列下的所有 PullRequest,并进行遍历 + * `List replayList`:当某个 pullRequest 不超时,并且对应的 `CQ.maxOffset <= pullRequest.offset`,就将该 PullRequest 再放入该列表 + * `long newestOffset`:该值为 CQ 的 maxOffset + * `if (newestOffset > request.getPullFromThisOffset())`:说明该请求对应的队列内可以 pull 消息了,**结束长轮询** + * `boolean match`:进行过滤匹配 + * `this.brokerController...executeRequestWhenWakeup()`:将满足条件的 pullRequest 再次提交到线程池内执行 + * `final RemotingCommand response`:执行 processRequest 方法,并且**不会触发长轮询** + * `channel.writeAndFlush(response).addListene()`:**将结果数据发送给客户端** + * `if (System.currentTimeMillis() >= ...)`:判断该 pullRequest 是否超时,超时后的也是重新提交到线程池,并且不进行长轮询 + * `mpr.addPullRequest(replayList)`:将未满足条件的 PullRequest 对象再次添加到 ManyPullRequest 属性中 - * `boolean changed = topicRouteDataIsChange(old, topicRouteData)`:对比本地和最新下拉的数据是否一致 - * `if (changed)`:不一致进入更新逻辑 - `cloneTopicRouteData = topicRouteData.cloneTopicRouteData()`:克隆一份最新数据 +*** - `Update Pub info`:更新生产者信息 - * `publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData)`:**将主题路由数据转化为发布数据** - * `impl.updateTopicPublishInfo(topic, publishInfo)`:生产者将主题的发布数据保存到它本地,方便发送消息使用 - `Update sub info`:更新消费者信息 +##### 结果类 - `this.topicRouteTable.put(topic, cloneTopicRouteData)`:将数据放入本地路由表 +GetMessageResult 类成员信息: + +```java +public class GetMessageResult { + // 查询消息时,最底层都是 mappedFile 支持的查询,查询时返回给外层一个 SelectMappedBufferResult, + // mappedFile 每查询一次都会 refCount++ ,通过SelectMappedBufferResult持有mappedFile,完成资源释放的句柄 + private final List messageMapedList = + new ArrayList(100); + + // 该List内存储消息,每一条消息都被转成 ByteBuffer 表示了 + private final List messageBufferList = new ArrayList(100); + // 查询结果状态 + private GetMessageStatus status; + // 客户端下次再向当前Queue拉消息时,使用的 offset + private long nextBeginOffset; + // 当前queue最小offset + private long minOffset; + // 当前queue最大offset + private long maxOffset; + // 消息总byte大小 + private int bufferTotalSize = 0; + // 服务器建议客户端下次到该 queue 拉消息时是否使用 【从节点】 + private boolean suggestPullingFromSlave = false; +} +``` -**** +*** -#### 网络通信 +#### 队列快照 ##### 成员属性 -NettyRemotingClient 类负责客户端的网络通信 +ProcessQueue 类是消费队列的快照 成员变量: -* Netty 服务相关属性: +* 属性字段: ```java - private final NettyClientConfig nettyClientConfig; // 客户端的网络层配置 - private final Bootstrap bootstrap = new Bootstrap(); // 客户端网络层启动对象 - private final EventLoopGroup eventLoopGroupWorker; // 客户端网络层 Netty IO 线程组 + private final AtomicLong msgCount = new AtomicLong(); // 队列中消息数量 + private final AtomicLong msgSize = new AtomicLong(); // 消息总大小 + private volatile long queueOffsetMax = 0L; // 快照中最大 offset + private volatile boolean dropped = false; // 快照是否移除 + private volatile long lastPullTimestamp = current; // 上一次拉消息的时间 + private volatile long lastConsumeTimestamp = current; // 上一次消费消息的时间 + private volatile long lastLockTimestamp = current; // 上一次获取锁的时间 ``` -* Channel 映射表: +* **消息容器**:key 是消息偏移量,val 是消息 ```java - private final ConcurrentMap channelTables;// key 是服务器的地址,value 是通道对象 - private final Lock lockChannelTables = new ReentrantLock(); // 锁,控制并发安全 + private final TreeMap msgTreeMap = new TreeMap(); ``` -* 定时器:启动定时任务 +* **顺序消费临时容器**: ```java - private final Timer timer = new Timer("ClientHouseKeepingService", true) + private final TreeMap consumingMsgOrderlyTreeMap = new TreeMap(); ``` -* 线程池: +* 锁: ```java - private ExecutorService publicExecutor; // 公共线程池 - private ExecutorService callbackExecutor; // 回调线程池,客户端发起异步请求,服务器的响应数据由回调线程池处理 + private final ReadWriteLock lockTreeMap; // 读写锁 + private final Lock lockConsume; // 重入锁,【顺序消费使用】 ``` -* 事件监听器:客户端这里是 null +* 顺序消费状态: ```java - private final ChannelEventListener channelEventListener; + private volatile boolean locked = false; // 是否是锁定状态 + private volatile boolean consuming = false; // 是否是消费中 ``` -构造方法 + -* 无参构造: +**** - ```java - public NettyRemotingClient(final NettyClientConfig nettyClientConfig) { - this(nettyClientConfig, null); - } - ``` -* 有参构造: + +##### 成员方法 + +核心成员方法 + +* putMessage():将 Broker 拉取下来的 msgs 存储到快照队列内,返回为 true 表示提交顺序消费任务,false 表示不提交 ```java - public NettyRemotingClient(nettyClientConfig, channelEventListener) { - // 父类创建了2个信号量,1、控制单向请求的并发度,2、控制异步请求的并发度 - super(nettyClientConfig.getClientOnewaySemaphoreValue(), nettyClientConfig.getClientAsyncSemaphoreValue()); - this.nettyClientConfig = nettyClientConfig; - this.channelEventListener = channelEventListener; - - // 创建公共线程池 - int publicThreadNums = nettyClientConfig.getClientCallbackExecutorThreads(); - if (publicThreadNums <= 0) { - publicThreadNums = 4; - } - this.publicExecutor = Executors.newFixedThreadPool(publicThreadNums,); - - // 创建 Netty IO 线程,1个线程 - this.eventLoopGroupWorker = new NioEventLoopGroup(1, ); - - if (nettyClientConfig.isUseTLS()) { - sslContext = TlsHelper.buildSslContext(true); - } - } + public boolean putMessage(final List msgs) ``` + * `this.lockTreeMap.writeLock().lockInterruptibly()`:获取写锁 + * `for (MessageExt msg : msgs)`:遍历 msgs 全部加入 msgTreeMap,key 是消息的 queueOffset -**** + * `if (!msgTreeMap.isEmpty() && !this.consuming)`:**消息容器中存在未处理的消息,并且不是消费中的状态** + `dispatchToConsume = true`:代表需要提交顺序消费任务 + `this.consuming = true`:设置为顺序消费执行中的状态 -##### 成员方法 + * `this.lockTreeMap.writeLock().unlock()`:释放写锁 -* start():启动方法 +* removeMessage():移除已经消费的消息,参数是已经消费的消息集合 ```java - public void start() { - // channel pipeline 内的 handler 使用的线程资源,默认 4 个 - this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(); - // 配置 netty 客户端启动类对象 - Bootstrap handler = this.bootstrap.group(this.eventLoopGroupWorker).channel(NioSocketChannel.class) - //... - .handler(new ChannelInitializer() { - @Override - public void initChannel(SocketChannel ch) throws Exception { - ChannelPipeline pipeline = ch.pipeline(); - // 加几个handler - pipeline.addLast( - // 服务端的数据,都会来到这个 - new NettyClientHandler()); - } - }); - // 注意 Bootstrap 只是配置好客户端的元数据了,【在这里并没有创建任何 channel 对象】 - // 定时任务 扫描 responseTable 中超时的 ResponseFuture,避免客户端线程长时间阻塞 - this.timer.scheduleAtFixedRate(() -> { - NettyRemotingClient.this.scanResponseTable(); - }, 1000 * 3, 1000); - // 这里是 null,不启动 - if (this.channelEventListener != null) { - this.nettyEventExecutor.start(); - } - } + public long removeMessage(final List msgs) ``` -* 单向通信: + * `long result = -1`:结果初始化为 -1 + * `this.lockTreeMap.writeLock().lockInterruptibly()`:获取写锁 + * `this.lastConsumeTimestamp = now`:更新上一次消费消息的时间为现在 + * `if (!msgTreeMap.isEmpty())`:判断消息容器是否是空,**是空直接返回 -1** + * `result = this.queueOffsetMax + 1`:设置结果,**删除完后消息容器为空时返回** + * `for (MessageExt msg : msgs)`:将已经消费的消息全部从 msgTreeMap 移除 + * `if (!msgTreeMap.isEmpty())`:移除后容器内还有待消费的消息,**获取第一条消息 offset 返回** + * `this.lockTreeMap.writeLock().unlock()`:释放写锁 + +* takeMessages():获取一批消息 ```java - public RemotingCommand invokeSync(String addr, final RemotingCommand request, long timeoutMillis) { - // 开始时间 - long beginStartTime = System.currentTimeMillis(); - // 获取或者创建客户端与服务端(addr)的通道 channel - final Channel channel = this.getAndCreateChannel(addr); - // 条件成立说明客户端与服务端 channel 通道正常,可以通信 - if (channel != null && channel.isActive()) { - try { - // 执行 rpcHook 拓展点 - doBeforeRpcHooks(addr, request); - // 计算耗时,如果当前耗时已经超过 timeoutMillis 限制,则直接抛出异常,不再进行系统通信 - long costTime = System.currentTimeMillis() - beginStartTime; - if (timeoutMillis < costTime) { - throw new RemotingTimeoutException("invokeSync call timeout"); - } - // 参数1:客户端-服务端通道channel - // 参数二:网络层传输对象,封装着请求数据 - // 参数三:剩余的超时限制 - RemotingCommand response = this.invokeSyncImpl(channel, request, ...); - // 后置处理 - doAfterRpcHooks(RemotingHelper.parseChannelRemoteAddr(channel), request, response); - // 返回响应数据 - return response; - } catch (RemotingSendRequestException e) {} - } else { - this.closeChannel(addr, channel); - throw new RemotingConnectException(addr); - } - } + public List takeMessages(final int batchSize) ``` + * `this.lockTreeMap.writeLock().lockInterruptibly()`:获取写锁 + * `this.lastConsumeTimestamp = now`:更新上一次消费消息的时间为现在 + * `for (int i = 0; i < batchSize; i++)`:从头节点开始获取消息 + * `result.add(entry.getValue())`:将消息放入结果集合 + * `consumingMsgOrderlyTreeMap.put()`:将消息加入顺序消费容器中 + * `if (result.isEmpty())`:条件成立说明顺序消费容器本地快照内的消息全部处理完了,**当前顺序消费任务需要停止** + * `consuming = false`:消费状态置为 false + * `this.lockTreeMap.writeLock().unlock()`:释放写锁 + +* commit():处理完一批消息后调用 + ```java + public long commit() + ``` + * `this.lockTreeMap.writeLock().lockInterruptibly()`:获取写锁 + * `Long offset = this.consumingMsgOrderlyTreeMap.lastKey()`:获取顺序消费临时容器最后一条数据的 key + * `msgCount, msgSize`:更新顺序消费相关的字段 + * `this.consumingMsgOrderlyTreeMap.clear()`:清空顺序消费容器的数据 + * `return offset + 1`:**消费者下一条消费的位点** + * `this.lockTreeMap.writeLock().unlock()`:释放写锁 +* cleanExpiredMsg():清除过期消息 + + ```java + public void cleanExpiredMsg(DefaultMQPushConsumer pushConsumer) + ``` + * `if (pushConsumer.getDefaultMQPushConsumerImpl().isConsumeOrderly()) `:顺序消费不执行过期清理逻辑 + * `int loop = msgTreeMap.size() < 16 ? msgTreeMap.size() : 16`:最多循环 16 次 + * `if (!msgTreeMap.isEmpty() &&)`:如果容器中第一条消息的消费开始时间与当前系统时间差值 > 15min,则取出该消息 + * `else`:直接跳出循环,因为**快照队列内的消息是有顺序的**,第一条消息不过期,其他消息都不过期 + * `pushConsumer.sendMessageBack(msg, 3)`:**消息回退**到服务器,设置该消息的延迟级别为 3 + * `if (!msgTreeMap.isEmpty() && msg.getQueueOffset() == msgTreeMap.firstKey())`:条件成立说明消息回退期间,该目标消息并没有被消费任务成功消费 + * `removeMessage(Collections.singletonList(msg))`:从 treeMap 将该回退成功的 msg 删除 -*** @@ -8061,6 +8750,8 @@ NettyRemotingClient 类负责客户端的网络通信 + + ## TEST diff --git a/Java.md b/Java.md index 3004ef2..b932420 100644 --- a/Java.md +++ b/Java.md @@ -5013,7 +5013,7 @@ HashMap继承关系如下图所示: 计算 hash 的方法:将 hashCode 无符号右移 16 位,高 16bit 和低 16bit 做异或,扰动运算 - 原因:当数组长度很小,假设是 16,那么 n-1即为 1111 ,这样的值和 hashCode() 直接做按位与操作,实际上只使用了哈希值的后4位。如果当哈希值的高位变化很大,低位变化很小,就很容易造成哈希冲突了,所以这里**把高低位都利用起来,让高16 位也参与运算**,从而解决了这个问题 + 原因:当数组长度很小,假设是 16,那么 n-1 即为 1111 ,这样的值和 hashCode() 直接做按位与操作,实际上只使用了哈希值的后4位。如果当哈希值的高位变化很大,低位变化很小,就很容易造成哈希冲突了,所以这里**把高低位都利用起来,让高16 位也参与运算**,从而解决了这个问题 哈希冲突的处理方式: @@ -5076,7 +5076,7 @@ HashMap继承关系如下图所示: * treeifyBin() - 节点添加完成之后判断此时节点个数是否大于TREEIFY_THRESHOLD临界值8,如果大于则将链表转换为红黑树,转换红黑树的方法 treeifyBin,整体代码如下: + 节点添加完成之后判断此时节点个数是否大于 TREEIFY_THRESHOLD 临界值 8,如果大于则将链表转换为红黑树,转换红黑树的方法 treeifyBin,整体代码如下: ```java if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st @@ -5084,7 +5084,7 @@ HashMap继承关系如下图所示: treeifyBin(tab, hash); ``` - 1. 如果当前数组为空或者数组的长度小于进行树形化的阈值(MIN_TREEIFY_CAPACITY = 64)就去扩容,而不是将节点变为红黑树 + 1. 如果当前数组为空或者数组的长度小于进行树形化的阈 MIN_TREEIFY_CAPACITY = 64 就去扩容,而不是将节点变为红黑树 2. 如果是树形化遍历桶中的元素,创建相同个数的树形节点,复制内容,建立起联系,类似单向链表转换为双向链表 3. 让桶中的第一个元素即数组中的元素指向新建的红黑树的节点,以后这个桶里的元素就是红黑树而不是链表数据结构了 From 159ce03e152acfd33a068eebd59f64a3a9614b80 Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 27 Jan 2022 22:35:25 +0800 Subject: [PATCH 107/168] Update Java Notes --- Frame.md | 713 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 691 insertions(+), 22 deletions(-) diff --git a/Frame.md b/Frame.md index bc07592..ee3ce2c 100644 --- a/Frame.md +++ b/Frame.md @@ -3816,7 +3816,7 @@ public class Consumer { 顺序消息分为全局顺序消息与分区顺序消息, - 全局顺序:对于指定的一个 Topic,所有消息按照严格的先入先出(FIFO)的顺序进行发布和消费。 适用于性能要求不高,所有的消息严格按照 FIFO 原则进行消息发布和消费的场景 -- 分区顺序:对于指定的一个 Topic,所有消息根据 sharding key 进行分区,同一个分组内的消息按照严格的 FIFO 顺序进行发布和消费。Sharding key 是顺序消息中用来区分不同分区的关键字段,和普通消息的 Key 是完全不同的概念。 适用于性能要求高,以 sharding key 作为分区字段,在同一个区中严格的按照 FIFO 原则进行消息发布和消费的场景 +- 分区顺序:对于指定的一个 Topic,所有消息根据 sharding key 进行分区,同一个分组内的消息按照严格的 FIFO 顺序进行发布和消费。Sharding key 是顺序消息中用来区分不同分区的关键字段,和普通消息的 Key 是完全不同的概念。 适用于性能要求高,以 Sharding key 作为分区字段,在同一个区中严格的按照 FIFO 原则进行消息发布和消费的场景 在默认的情况下消息发送会采取 Round Robin 轮询方式把消息发送到不同的 queue(分区队列),而消费消息是从多个 queue 上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个 queue 中,消费的时候只从这个 queue 上依次拉取,则就保证了顺序。当**发送和消费参与的 queue 只有一个**,则是全局有序;如果多个queue 参与,则为分区有序,即相对每个 queue,消息都是有序的 @@ -4000,7 +4000,7 @@ Broker 可以配置 messageDelayLevel,该属性是 Broker 的属性,不属 - 1<=level<=maxLevel:消息延迟特定时间,例如 level==1,延迟 1s - level > maxLevel:则 level== maxLevel,例如 level==20,延迟 2h -定时消息会暂存在名为 SCHEDULE_TOPIC_XXXX 的 Topic 中,并根据 delayTimeLevel 存入特定的 queue,队列的标识`queueId = delayTimeLevel – 1`,即一个 queue 只存相同延迟的消息,保证具有相同发送延迟的消息能够顺序消费。Broker 会调度地消费 SCHEDULE_TOPIC_XXXX,将消息写入真实的 Topic +定时消息会暂存在名为 SCHEDULE_TOPIC_XXXX 的 Topic 中,并根据 delayTimeLevel 存入特定的 queue,队列的标识 `queueId = delayTimeLevel – 1`,即一个 queue 只存相同延迟的消息,保证具有相同发送延迟的消息能够顺序消费。Broker 会调度地消费 SCHEDULE_TOPIC_XXXX,将消息写入真实的 Topic 注意:定时消息在第一次写入和调度写入真实 Topic 时都会计数,因此发送数量、tps 都会变高。 @@ -6212,9 +6212,14 @@ CommitLog 类核心方法: ``` * `msg.setStoreTimestamp(System.currentTimeMillis())`:设置存储时间,后面获取到写锁后这个事件会重写 - * `String topic = msg.getTopic()`:获取主题和队列 ID + * `msg.setBodyCRC(UtilAll.crc32(msg.getBody()))`:获取消息的 CRC 值 + * `topic、queueId`:获取主题和队列 ID + * `if (msg.getDelayTimeLevel() > 0) `:获取消息的延迟级别 + * `topic = TopicValidator.RMQ_SYS_SCHEDULE_TOPIC`:**修改消息的主题为 `SCHEDULE_TOPIC_XXXX`** + * `queueId = ScheduleMessageService.delayLevel2QueueId()`:队列 ID 为延迟级别 -1 + * `MessageAccessor.putProperty`:将原来的消息主题和 ID 存入消息的属性 `REAL_TOPIC` 中 + * `msg.setTopic(topic)`:修改主题 * `mappedFile = this.mappedFileQueue.getLastMappedFile()`:获取当前顺序写的 MappedFile 对象 - * `putMessageLock.lock()`:获取**写锁** * `msg.setStoreTimestamp(beginLockTimestamp)`:设置消息的存储时间为获取锁的时间 * `if (null == mappedFile || mappedFile.isFull())`:文件写满了创建新的 MF 对象 @@ -6813,12 +6818,22 @@ FlushCommitLogService 刷盘 CL 数据,默认是异步刷盘 ``` * `while (!this.isStopped())`:stopped为 true 才跳出循环 + * `boolean flushCommitLogTimed`:控制线程的休眠方式,默认是 false,使用 `CountDownLatch.await()` 休眠,设置为 true 时使用 `Thread.sleep()` 休眠 + * `int interval`:获取配置中的刷盘时间间隔 + * `int flushPhysicQueueLeastPages`:获取最小刷盘页数,默认是 4 页,脏页达到指定页数才刷盘 + * `int flushPhysicQueueThoroughInterval`:获取强制刷盘周期,默认是 10 秒,达到周期后强制刷盘,不考虑脏页 + * `if (flushCommitLogTimed)`:休眠逻辑,避免 CPU 占用太长时间,导致无法执行其他更紧急的任务 + * `CommitLog.this.mappedFileQueue.flush(flushPhysicQueueLeastPages)`:**刷盘** + + * `for (int i = 0; i < RETRY_TIMES_OVER && !result; i++)`:stopped 停止标记为 true 时,需要确保所有的数据都已经刷盘,所以此处尝试 10 次强制刷盘, + + `result = CommitLog.this.mappedFileQueue.flush(0)`:**强制刷盘** @@ -6989,6 +7004,12 @@ public static BrokerController start(BrokerController controller) { } ``` +BrokerStartup#createBrokerController:构造控制器,并初始化 + +* `final BrokerController controller()`:创建实例对象 +* `boolean initResult = controller.initialize()`:控制器初始化 + * `this.registerProcessor()`:**注册了处理器,包括发送消息、拉取消息、查询消息等核心处理器** + BrokerController#start:核心启动方法 * `this.messageStore.start()`:**启动存储服务** @@ -7102,7 +7123,7 @@ DefaultMQProducer 是生产者的默认实现类 } ``` -* request():请求方法,**需要消费者回执消息**,又叫回退消息 +* request():请求方法,**需要消费者回执消息** ```java public Message request(final Message msg, final MessageQueue mq, final long timeout) { @@ -7336,7 +7357,7 @@ DefaultMQProducerImpl 类是默认的生产者实现类 * `getRequestFutureTable().put(correlationId, requestResponseFuture)`:放入RequestFutureTable 映射表中 - * `this.sendDefaultImpl(msg, CommunicationMode.ASYNC, new SendCallback())`:**发送异步消息** + * `this.sendDefaultImpl(msg, CommunicationMode.ASYNC, new SendCallback())`:**发送异步消息,有回调函数** * `return waitResponse(msg, timeout, requestResponseFuture, cost)`:用来挂起请求的方法 @@ -7611,7 +7632,7 @@ MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一 private final ConsumerStatsManager consumerStatsManager; // 消费者状态管理 ``` -* 内部生产者实例:处理消费端**消息回退**,用该生产者发送回执消息 +* 内部生产者实例:处理消费端**消息回退**,用该生产者发送回退消息 ```java private final DefaultMQProducer defaultMQProducer; @@ -7918,6 +7939,236 @@ NettyRemotingClient 类负责客户端的网络通信 +*** + + + +#### 延迟消息 + +##### 消息处理 + +BrokerStartup 初始化 BrokerController 调用 `registerProcessor()` 方法将 SendMessageProcessor 注册到 NettyRemotingServer 中,对应的请求 ID 为 `SEND_MESSAGE = 10`,NettyServerHandler 在处理请求时通过请求 ID 会获取处理器执行 processRequest + +```java +// 参数一:处理通道的事件; 参数二:客户端 +public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) { + RemotingCommand response = null; + response = asyncProcessRequest(ctx, request).get(); + return response; +} +``` + +SendMessageProcessor#asyncConsumerSendMsgBack:异步发送消费者的回调消息 + +* `final RemotingCommand response`:创建一个服务器响应对象 + +* `final ConsumerSendMsgBackRequestHeader requestHeader`:解析出客户端请求头信息,几个**核心字段**: + + * `private Long offset`:回退消息的 CommitLog offset + * `private Integer delayLevel`:延迟级别,一般是 0 + * `private String originMsgId, originTopic`:原始的消息 ID,主题 + * `private Integer maxReconsumeTimes`:最大重试次数,默认是 16 次 + +* `if ()`:鉴权,是否找到订阅组配置、Broker 是否支持写请求、订阅组是否支持消息重试 + +* `String newTopic = MixAll.getRetryTopic(...)`:获取**消费者组的重试主题**,规则是 `%RETRY%GroupName` + +* `int queueIdInt = Math.abs()`:充实主题下的队列 ID 是 0 + +* `TopicConfig topicConfig`:获取重试主题的配置信息 + +* `MessageExt msgExt`:根据消息的物理 offset 到存储模块查询,内部先查询出这条消息的 size,然后再根据 offset 和 size 查询出整条 msg + +* `final String retryTopic`:获取消息的原始主题 + +* `if (null == retryTopic)`:条件成立说明**当前消息是第一次被回退**, 添加 `RETRY_TOPIC` 属性 + +* `msgExt.setWaitStoreMsgOK(false)`:异步刷盘 + +* `if (msgExt...() >= maxReconsumeTimes || delayLevel < 0)`:消息重试次数超过最大次数,不支持重试 + + `newTopic = MixAll.getDLQTopic()`:获取消费者的死信队列,规则是 `%DLQ%GroupName` + + `queueIdInt, topicConfig`:死信队列 ID 为 0,创建死信队列的配置 + +* `if (0 == delayLevel)`:说明延迟级别由 Broker 控制 + + `delayLevel = 3 + msgExt.getReconsumeTimes()`:**延迟级别默认从 3 级开始**,每重试一次,延迟级别 +1 + +* `msgExt.setDelayTimeLevel(delayLevel)`:**将延迟级别设置进消息属性**,存储时会检查该属性,该属性值 > 0 会将消息的主题和队列再次修改,修改为调度主题和调度队列 ID + +* `MessageExtBrokerInner msgInner`:创建一条空消息,消息属性从 offset 查询出来的 msg 中拷贝 + +* `msgInner.setReconsumeTimes)`:重试次数设置为原 msg 的次数 +1 + +* `UtilAll.isBlank(originMsgId)`:判断消息是否是初次返回到服务器 + + * true:说明 msgExt 消息是第一次被返回到服务器,此时使用该 msg 的 id 作为 originMessageId + * false:说明原始消息已经被重试不止 1 次,此时使用 offset 查询出来的 msg 中的 originMessageId + +* `CompletableFuture putMessageResult = ..asyncPutMessage(msgInner)`:调用存储模块存储消息 + + `DefaultMessageStore#asyncPutMessage`: + + * `PutMessageResult result = this.commitLog.asyncPutMessage(msg)`:**将新消息存储到 CommitLog 中** + + + +*** + + + +##### 调度服务 + +DefaultMessageStore 中有成员属性 ScheduleMessageService,在 start 方法中会启动该调度服务 + +成员变量: + +* 延迟级别属性表: + + ```java + // 存储延迟级别对应的 延迟时间长度 (单位:毫秒) + private final ConcurrentMap delayLevelTable; + // 存储延迟级别 queue 的消费进度 offset,该 table 每 10 秒钟,会持久化一次,持久化到本地磁盘 + private final ConcurrentMap offsetTable; + ``` + +* 最大延迟级别: + + ```java + private int maxDelayLevel; + ``` + +* 模块启动状态: + + ```java + private final AtomicBoolean started = new AtomicBoolean(false); + ``` + +* 定时器:内部有线程资源,可执行调度任务 + + ```java + private Timer timer; + ``` + +成员方法: + +* load():加载调度消息,初始化 delayLevelTable 和 offsetTable + + ```java + public boolean load() + ``` + +* start():启动消息调度服务 + + ```java + public void start() + ``` + + * `if (started.compareAndSet(false, true))`:将启动状态设为 true + + * `this.timer`:创建定时器对象 + + * `for (... : this.delayLevelTable.entrySet())`:为**每个延迟级别创建一个延迟任务**提交到 timer ,延迟 1 秒后执行 + + * `this.timer.scheduleAtFixedRate()`:提交周期型任务,延迟 10 秒执行,周期为 10 秒,持久化延迟队列消费进度任务 + + `ScheduleMessageService.this.persist()`:持久化消费进度 + + + +*** + + + +##### 调度任务 + +DeliverDelayedMessageTimerTask 是一个任务类 + +成员变量: + +* 延迟级别:延迟队列任务处理的延迟级别 + + ```java + private final int delayLevel; + ``` + +* 消费进度:延迟队列任务处理的延迟队列的消费进度 + + ```java + private final long offset; + ``` + +成员方法: + +* run():执行任务 + + ```java + public void run() { + if (isStarted()) { + this.executeOnTimeup(); + } + ``` + +* executeOnTimeup():执行任务 + + ```java + public void executeOnTimeup() + ``` + + * `ConsumeQueue cq`:获取出该延迟队列任务处理的延迟队列 ConsumeQueue + + * `SelectMappedBufferResult bufferCQ`:根据消费进度查询出 SMBR 对象 + + * `for (; i < bufferCQ.getSize(); i += ConsumeQueue.CQ_STORE_UNIT_SIZE)`:每次读取 20 各字节的数据 + + * `offsetPy, sizePy`:延迟消息的物理偏移量和消息大小 + + * `long tagsCode`:延迟消息的交付时间,在 ReputMessageService 转发时根据消息的 DELAY 属性是否 >0 ,会在 tagsCode 字段存储交付时间 + + * `long deliverTimestamp = this.correctDeliverTimestamp(now, tagsCode)`:**延迟交付时间** + + * `long maxTimestamp`:当前时间 + 延迟级别对应的延迟毫秒值的时间戳 + * `if (deliverTimestamp > maxTimestamp)`:条件成立说明延迟时间过长,调整为当前时间立刻执行 + * `return result`:一般情况 result 就是 deliverTimestamp + + * `long countdown = deliverTimestamp - now`:计算差值 + + * `if (countdown <= 0)`:消息已经到达交付时间了 + + `MessageExt msgExt`:根据物理偏移量和消息大小获取这条消息 + + `MessageExtBrokerInner msgInner`:**构建一条新消息**,将原消息的属性拷贝过来 + + * `long tagsCodeValue`:不再是交付时间了 + * `MessageAccessor.clearProperty(msgInner, DELAY..)`:清理新消息的 DELAY 属性,避免存储时重定向到延迟队列 + * `msgInner.setTopic()`:修改主题为原始的主题 `%RETRY%GroupName` + * `String queueIdStr`:修改队列 ID 为原始的 ID + + `PutMessageResult putMessageResult`:**将新消息存储到 CommitLog**,消费者订阅的是目标主题,会再次消费该消息 + + * `else`:消息还未到达交付时间 + + `ScheduleMessageService.this.timer.schedule()`:创建该延迟级别的任务,延迟 countDown 毫秒之后再执行 + + `ScheduleMessageService.this.updateOffset()`:更新延迟级别队列的消费进度 + + * `PutMessageResult putMessageResult` + + * `bufferCQ == null`:说明通过消费进度没有获取到数据 + + `if (offset < cqMinOffset)`:如果消费进度比最小位点都小,说明是过期数据,重置为最小位点 + + * `ScheduleMessageService.this.timer.schedule()`:重新提交该延迟级别对应的延迟队列任务,延迟 100 毫秒后执行 + + + + + + + + + **** @@ -8209,7 +8460,7 @@ RebalanceImpl 类成员变量: * `allocateResult = strategy.allocate()`: **调用队列分配策略**,给当前消费者进行分配 MessageQueue - * `boolean changed = this.updateProcessQueueTableInRebalance(...)`:负载均衡,更新队列处理集合 + * `boolean changed = this.updateProcessQueueTableInRebalance(...)`:**更新队列处理集合** * `boolean changed = false`:当前消费者的消费队列是否有变化 @@ -8219,25 +8470,70 @@ RebalanceImpl 类成员变量: `pq.setDropped(true)`:将删除状态设置为 true - `if (this.removeUnnecessaryMessageQueue(mq, pq))`:在 MQ 归属的 broker 节点持久化消费进度,并删除该 MQ 在本地的消费进度 + `if (this.removeUnnecessaryMessageQueue(mq, pq))`:删除不需要的 MQ 队列 - `it.remove()`:从 processQueueTable 移除该 MQ + * `this...getOffsetStore().persist(mq)`:在 MQ 归属的 Broker 节点持久化消费进度 - * `else if (pq.isPullExpired())`:说明当前 MQ 还是被当前 consumer 消费,此时判断一下是否超过 2 分钟未到服务器 拉消息,如果条件成立进行上述相同的逻辑、 + * `this...getOffsetStore().removeOffset(mq)`:删除该 MQ 在本地的消费进度 + * `if (this.defaultMQPushConsumerImpl.isConsumeOrderly() &&)`:是否是**顺序消费**和集群模式 + + `if (pq.getLockConsume().tryLock(1000, ..))`: 获取锁成功,说明顺序消费任务已经停止消费工作 + + `return this.unlockDelay(mq, pq)`:**释放锁 Broker 端的队列锁** + + * `if (pq.hasTempMessage())`:队列中有消息,延迟 20 秒释放队列分布式锁,确保全局范围内只有一个消费任务 运行中 + * `else`:当前消费者本地该消费任务已经退出,直接释放锁 + + `else`:顺序消费任务正在消费一批消息,不可打断,增加尝试获取锁的次数 + + `it.remove()`:从 processQueueTable 移除该 MQ + + * `else if (pq.isPullExpired())`:说明当前 MQ 还是被当前 consumer 消费,此时判断一下是否超过 2 分钟未到服务器 拉消息,如果条件成立进行上述相同的逻辑 + * `for (MessageQueue mq : mqSet)`:开始处理当前主题**新分配**到当前节点的队列 + + `if (isOrder && !this.lock(mq))`:**顺序消息为了保证有序性,需要获取分布式锁** + + `ProcessQueue pq = new ProcessQueue()`:为每个新分配的消息队列创建快照队列 + + `long nextOffset = this.computePullFromWhere(mq)`:**从服务端获取新分配的 MQ 的消费进度** + + `ProcessQueue pre = this.processQueueTable.putIfAbsent(mq, pq)`:保存到处理队列集合 + + `PullRequest pullRequest = new PullRequest()`:创建拉取请求对象 + + * `this.dispatchPullRequest(pullRequestList)`:放入拉消息服务的本地阻塞队列内,**用于拉取消息工作** + +* lockAll():续约锁,对消费者的所有队列进行续约 - * `if (isOrder && !this.lock(mq))`:**顺序消息为了保证有序性,需要获取分布式锁** + ```java + public void lockAll() + ``` - * `ProcessQueue pq = new ProcessQueue()`:为每个新分配的消息队列创建快照队列 + * `HashMap> brokerMqs`:将分配给当前消费者的全部 MQ,按照 BrokerName 分组 - * `long nextOffset = this.computePullFromWhere(mq)`:**从服务端获取新分配的 MQ 的消费进度** + * `while (it.hasNext())`:遍历所有的分组 - * `ProcessQueue pre = this.processQueueTable.putIfAbsent(mq, pq)`:保存到处理队列集合 + * `final Set mqs`:获取该 Broker 上分配给当前消费者的 queue 集合 - * `PullRequest pullRequest = new PullRequest()`:创建拉取请求对象 + * `FindBrokerResult findBrokerResult`:查询 Broker 主节点信息 - * `this.dispatchPullRequest(pullRequestList)`:放入拉消息服务的本地阻塞队列内,**用于拉取消息工作** + * `LockBatchRequestBody requestBody`:创建请求对象,填充属性 + + * `Set lockOKMQSet`:**向 Broker 发起批量续约锁的同步请求**,返回成功的队列集合 + + * `for (MessageQueue mq : lockOKMQSet)`:遍历续约锁成功的 MQ + + `processQueue.setLocked(true)`:分布式锁状态设置为 true,**表示允许顺序消费** + + `processQueue.setLastLockTimestamp(System.currentTimeMillis())`:设置上次获取锁的时间为当前时间 + + * `for (MessageQueue mq : mqs)`:遍历当前 Broker 上的所有队列集合 + + `if (!lockOKMQSet.contains(mq))`:条件成立说明续约锁失败 + + `processQueue.setLocked(false)`:分布式锁状态设置为 false,表示不允许顺序消费 @@ -8294,7 +8590,7 @@ AllocateMessageQueueStrategy 类是队列的分配策略 -#### 消息拉取 +#### 拉取服务 ##### 实现方式 @@ -8441,7 +8737,7 @@ PullAPIWrapper 类封装了拉取消息的 API -#### 通信处理 +#### 拉取处理 ##### 处理器 @@ -8684,7 +8980,7 @@ ProcessQueue 类是消费队列的快照 * `this.lockTreeMap.writeLock().unlock()`:释放写锁 -* removeMessage():移除已经消费的消息,参数是已经消费的消息集合 +* removeMessage():移除已经消费的消息,参数是已经消费的消息集合,并发消费使用 ```java public long removeMessage(final List msgs) @@ -8699,7 +8995,7 @@ ProcessQueue 类是消费队列的快照 * `if (!msgTreeMap.isEmpty())`:移除后容器内还有待消费的消息,**获取第一条消息 offset 返回** * `this.lockTreeMap.writeLock().unlock()`:释放写锁 -* takeMessages():获取一批消息 +* takeMessages():获取一批消息,顺序消费使用 ```java public List takeMessages(final int batchSize) @@ -8714,7 +9010,7 @@ ProcessQueue 类是消费队列的快照 * `consuming = false`:消费状态置为 false * `this.lockTreeMap.writeLock().unlock()`:释放写锁 -* commit():处理完一批消息后调用 +* commit():处理完一批消息后调用,顺序消费使用 ```java public long commit() @@ -8743,6 +9039,379 @@ ProcessQueue 类是消费队列的快照 +**** + + + +#### 并发消费 + +##### 成员属性 + +ConsumeMessageConcurrentlyService 负责并发消费服务 + +成员变量: + +* 消息监听器:封装处理消息的逻辑,该监听器由开发者实现,并注册到 defaultMQPushConsumer + + ```java + private final MessageListenerConcurrently messageListener; + ``` + +* 消费属性: + + ```java + private final BlockingQueue consumeRequestQueue; // 消费任务队列 + private final String consumerGroup; // 消费者组 + ``` + +* 线程池: + + ```java + private final ThreadPoolExecutor consumeExecutor; // 消费任务线程池 + private final ScheduledExecutorService scheduledExecutorService;// 调度线程池,延迟提交消费任务 + private final ScheduledExecutorService cleanExpireMsgExecutors; // 清理过期消息任务线程池,15min 一次 + ``` + + + +*** + + + +##### 成员方法 + +ConsumeMessageConcurrentlyService 并发消费核心方法 + +* start():启动消费服务,DefaultMQPushConsumerImpl 启动时会调用该方法 + + ```java + public void start() { + // 提交“清理过期消息任务”任务,延迟15min之后执行,之后每15min执行一次 + this.cleanExpireMsgExecutors.scheduleAtFixedRate(() -> cleanExpireMsg()}, + 15, 15, TimeUnit.MINUTES); + } + ``` + +* cleanExpireMsg():清理过期消息任务 + + ```java + private void cleanExpireMsg() + ``` + + * `Iterator> it `:获取分配给当前消费者的队列 + * `while (it.hasNext())`:遍历所有的队列 + * `pq.cleanExpiredMsg(this.defaultMQPushConsumer)`:调用队列快照 ProcessQueue 清理过期消息的方法 + +* submitConsumeRequest():提交消费请求 + + ```java + // 参数一:从服务器 pull 下来的这批消息 + // 参数二:消息归属 mq 在消费者端的 processQueue,提交消费任务之前,msgs已经加入到该pq内了 + // 参数三:消息归属队列 + // 参数四:并发消息此参数无效 + public void submitConsumeRequest(List msgs, ProcessQueue processQueue, MessageQueue messageQueue, boolean dispatchToConsume) + ``` + + * `final int consumeBatchSize`:**一个消费任务**可消费的消息数量,默认为 1 + + * `if (msgs.size() <= consumeBatchSize)`:判断一个消费任务是否可以提交 + + `ConsumeRequest consumeRequest`:封装为消费请求 + + `this.consumeExecutor.submit(consumeRequest)`:提交消费任务,异步执行消息的处理 + + * `else`:说明消息较多,需要多个消费任务 + + `for (int total = 0; total < msgs.size(); )`:将消息拆分成多个消费任务 + +* processConsumeResult():处理消费结果 + + ```java + // 参数一:消费结果状态; 参数二:消费上下文; 参数三:当前消费任务 + public void processConsumeResult(status, context, consumeRequest) + ``` + + * `switch (status)`:根据消费结果状态进行处理 + + * `case CONSUME_SUCCESS`:消费成功 + + `if (ackIndex >= consumeRequest.getMsgs().size())`:消费成功的话,ackIndex 设置成 `消费消息数 - 1` 的值,比如有 5 条消息,这里就设置为 4 + + `ok, failed`:ok 设置为消息数量,failed 设置为 0 + + * `case RECONSUME_LATER`:消费失败 + + `ackIndex = -1`:设置为 -1 + + * `switch (this.defaultMQPushConsumer.getMessageModel())`:判断消费模式,默认是**集群模式** + + * `for (int i = ackIndex + 1; i < msgs.size(); i++)`:当消费失败时 ackIndex 为 -1,i 的起始值为 0,该消费任务内的全部消息都会尝试回退给服务器 + + * `MessageExt msg`:提取一条消息 + + * `boolean result = this.sendMessageBack(msg, context)`:发送**消息回退** + + * `String brokerAddr`:根据 brokerName 获取 master 节点地址 + * `his.mQClientFactory...consumerSendMessageBack()`:发送回退消息 + * `RemotingCommand request`:创建请求对象 + * `RemotingCommand response = this.remotingClient.invokeSync()`:**同步请求** + + * `if (!result)`:回退失败的消息,将**消息的重试属性加 1**,并加入到回退失败的集合 + + * `if (!msgBackFailed.isEmpty())`:回退失败集合不为空 + + `consumeRequest.getMsgs().removeAll(msgBackFailed)`:将回退失败的消息从当前消费任务的 msgs 集合内移除 + + `this.submitConsumeRequestLater()`:回退失败的消息会再次提交消费任务,延迟 5 秒钟后**再次尝试消费** + + * `long offset = ...removeMessage(msgs)`:从 pq 中删除已经消费成功的消息,返回 offset + + * `this...getOffsetStore().updateOffset()`:更新消费者本地该 mq 的**消费进度** + + + +*** + + + +##### 消费请求 + +ConsumeRequest 是 ConsumeMessageConcurrentlyService 的内部类,是一个 Runnable 任务对象 + +成员变量: + +* 分配到该消费任务的消息: + + ```java + private final List msgs; + ``` + +* 消息队列: + + ```java + private final ProcessQueue processQueue; // 消息处理队列 + private final MessageQueue messageQueue; // 消息队列 + ``` + +核心方法: + +* run():执行任务 + + ```java + public void run() + ``` + + * `if (this.processQueue.isDropped())`:条件成立说明该 queue 经过 rbl 算法分配到其他的 consumer + * `MessageListenerConcurrently listener`:获取消息监听器 + * `ConsumeConcurrentlyContext context`:创建消费上下文对象 + * `defaultMQPushConsumerImpl.resetRetryAndNamespace()`:重置重试标记 + * `final String groupTopic`:获取当前消费者组的重试主题 `%RETRY%GroupName` + * `for (MessageExt msg : msgs)`:遍历所有的消息 + * `String retryTopic = msg.getProperty(...)`:原主题,一般消息没有该属性,只有被重复消费的消息才有 + * `if (retryTopic != null && groupTopic.equals(...))`:条件成立说明该消息是被重复消费的消息 + * `msg.setTopic(retryTopic)`:将被**重复消费的消息主题修改回原主题** + * `if (ConsumeMessageConcurrentlyService...hasHook())`:前置处理 + * `boolean hasException = false`:消费过程中,是否向外抛出异常 + * `MessageAccessor.setConsumeStartTimeStamp()`:给每条消息设置消费开始时间 + * `status = listener.consumeMessage(Collections.unmodifiableList(msgs), context)`:**消费消息** + * `if (ConsumeMessageConcurrentlyService...hasHook())`:后置处理 + * `...processConsumeResult(status, context, this)`:**处理消费结果** + + + +**** + + + +#### 顺序消费 + +##### 成员属性 + +ConsumeMessageOrderlyService 负责顺序消费服务 + +成员变量: + +* 消息监听器:封装处理消息的逻辑,该监听器由开发者实现,并注册到 defaultMQPushConsumer + + ```java + private final MessageListenerOrderly messageListener; + ``` + +* 消费属性: + + ```java + private final BlockingQueue consumeRequestQueue; // 消费任务队列 + private final String consumerGroup; // 消费者组 + private volatile boolean stopped = false; // 消费停止状态 + ``` + +* 线程池: + + ```java + private final ThreadPoolExecutor consumeExecutor; // 消费任务线程池 + private final ScheduledExecutorService scheduledExecutorService;// 调度线程池,延迟提交消费任务 + ``` + +* 队列锁:消费者本地 MQ 锁,确保本地对于需要顺序消费的 MQ 同一时间只有一个任务在执行 + + ```java + private final MessageQueueLock messageQueueLock = new MessageQueueLock(); + ``` + + ```java + public class MessageQueueLock { + private ConcurrentMap mqLockTable = new ConcurrentHashMap(); + // 获取本地队列锁对象 + public Object fetchLockObject(final MessageQueue mq) { + Object objLock = this.mqLockTable.get(mq); + if (null == objLock) { + objLock = new Object(); + Object prevLock = this.mqLockTable.putIfAbsent(mq, objLock); + if (prevLock != null) { + objLock = prevLock; + } + } + return objLock; + } + } + ``` + + 已经获取了 Broker 端该 Queue 的独占锁,为什么还要获取本地队列锁对象?(这里我也没太懂,先记录下来) + + * Broker queue 占用锁的角度是 Client 占用,Client 从 Broker 的某个占用了锁的 queue 拉取下来消息以后,将消息存储到消费者本地的 ProcessQueue 中,快照对象的 consuming 属性置为 true,表示本地的队列正在消费处理中。 + * ProcessQueue 调用 takeMessages 方法时会获取下一批待处理的消息,获取不到会修改 `consuming = false`,本消费任务马上停止。 + * 如果此时 Pull 再次拉取一批当前 ProcessQueue 的 msg,会再次向顺序消费服务提交消费任务,此时需要本地队列锁对象同步本地线程 + + + +*** + + + +##### 成员方法 + +* start():启动消费服务,DefaultMQPushConsumerImpl 启动时会调用该方法 + + ```java + public void start() + ``` + + * `this.scheduledExecutorService.scheduleAtFixedRate()`:提交锁续约任务,延迟 1 秒执行,周期为 20 秒钟 + * `ConsumeMessageOrderlyService.this.lockMQPeriodically()`:**锁续约任务** + * `this.defaultMQPushConsumerImpl.getRebalanceImpl().lockAll()`:对消费者的所有队列进行续约 + +* submitConsumeRequest():**提交消费任务请求** + + ```java + // 参数:true 表示创建消费任务并提交,false不创建消费任务,说明消费者本地已经有消费任务在执行了 + public void submitConsumeRequest(...., final boolean dispathToConsume) { + if (dispathToConsume) { + // 当前进程内不存在 顺序消费任务,创建新的消费任务,【提交到消费任务线程池】 + ConsumeRequest consumeRequest = new ConsumeRequest(processQueue, messageQueue); + this.consumeExecutor.submit(consumeRequest); + } + } + ``` + +* processConsumeResult():消费结果处理 + + ```java + // 参数1:msgs 本轮循环消费的消息集合 参数2:status 消费状态 + // 参数3:context 消费上下文 参数4:消费任务 + // 返回值:boolean 决定是否继续循环处理pq内的消息 + public boolean processConsumeResult(final List msgs, final ConsumeOrderlyStatus status, final ConsumeOrderlyContext context, final ConsumeRequest consumeRequest) + ``` + + * `if (context.isAutoCommit()) `:默认自动提交 + + * `switch (status)`:根据消费状态进行不同的处理 + + * `case SUCCESS`:消费成功 + + `commitOffset = ...commit()`:调用 pq 提交方法,会将本次循环处理的消息从顺序消费 map 删除,并且返回消息进度 + + * `case SUSPEND_CURRENT_QUEUE_A_MOMENT`:挂起当前队列 + + `consumeRequest.getProcessQueue().makeMessageToConsumeAgain(msgs)`:回滚消息 + + * `for (MessageExt msg : msgs)`:遍历所有的消息 + * `this.consumingMsgOrderlyTreeMap.remove(msg.getQueueOffset())`:从顺序消费临时容器中移除 + * `this.msgTreeMap.put(msg.getQueueOffset(), msg)`:添加到消息容器 + + * `this.submitConsumeRequestLater()`:再次提交消费任务,1 秒后执行 + + * `continueConsume = false`:设置为 false,**外层会退出本次的消费任务** + + * `this.defaultMQPushConsumerImpl.getOffsetStore().updateOffset(...)`:更新本地消费进度 + + + +**** + + + +##### 消费请求 + +ConsumeRequest 是 ConsumeMessageOrderlyService 的内部类,是一个 Runnable 任务对象 + +核心方法: + +* run():执行任务 + + ```java + public void run() + ``` + + * `final Object objLock`:获取本地锁对象 + + * `synchronized (objLock)`:本地队列锁,确保每个 MQ 的消费任务只有一个在执行,**确保顺序消费** + + * `if(.. || (this.processQueue.isLocked() && !this.processQueue.isLockExpired())))`:当前队列持有分布式锁,并且锁未过期,持锁时间超过 30 秒算过期 + + * `final long beginTime`:消费开始时间 + + * `for (boolean continueConsume = true; continueConsume; )`:根据是否继续消费的标记判断是否继续 + + * `final int consumeBatchSize`:获取每次循环处理的消息数量,一般是 1 + + * `List msgs = this...takeMessages(consumeBatchSize)`:到**处理队列获取一批消息** + + * `if (!msgs.isEmpty())`:获取到了待消费的消息 + + `final ConsumeOrderlyContext context`:创建消费上下文对象 + + `this.processQueue.getLockConsume().lock()`:**获取 lockConsume 锁**,与 RBL 线程同步使用 + + `status = messageListener.consumeMessage(...)`:监听器处理消息 + + `this.processQueue.getLockConsume().unlock()`:**释放 lockConsume 锁** + + `if (null == status)`:处理消息状态返回 null,设置状态为挂起当前队列 + + `continueConsume = ...processConsumeResult()`:消费结果处理 + + * `else`:获取到的消息是空 + + `continueConsume = false`:结束任务循环 + + * `else`:当前队列未持有分布式锁,或者锁过期 + + `ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume()`:重新提交任务,根据是否获取到队列锁,选择延迟 10 毫秒或者 300 毫秒 + + + + + + + + + + + + + + + From cbfb2fb2c4eea14ac2443a2fbd2a926fdf565062 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 30 Jan 2022 22:34:01 +0800 Subject: [PATCH 108/168] Update Java Notes --- Frame.md | 1236 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 1069 insertions(+), 167 deletions(-) diff --git a/Frame.md b/Frame.md index ee3ce2c..cec0168 100644 --- a/Frame.md +++ b/Frame.md @@ -4227,7 +4227,7 @@ ConsumeQueue 的存储结构如下,有 8 个字节存储的 Message Tag 的哈 ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消费队列结构.png) -* Tag 过滤:Consumer 端订阅消息时指定 Topic 和 TAG,然后将订阅请求构建成一个 SubscriptionData,发送一个 Pull 消息的请求给 Broker 端。Broker 端用这些数据先构建一个 MessageFilter,然后传给文件存储层 Store。Store 从 ConsumeQueue 读取到一条记录后,会用它记录的消息 tag hash 值去做过滤。因为在服务端只是根据 hashcode 进行判断,无法精确对 tag 原始字符串进行过滤,所以消费端拉取到消息后,还需要对消息的原始 tag 字符串进行比对,如果不同,则丢弃该消息,不进行消息消费 +* Tag 过滤:Consumer 端订阅消息时指定 Topic 和 TAG,然后将订阅请求构建成一个 SubscriptionData,发送一个 Pull 消息的请求给 Broker 端。Broker 端用这些数据先构建一个 MessageFilter,然后传给文件存储层 Store。Store 从 ConsumeQueue 读取到一条记录后,会用它记录的消息 tag hash 值去做过滤。因为在服务端只是根据 hashcode 进行判断,无法精确对 tag 原始字符串进行过滤,所以消费端拉取到消息后,还需要对消息的原始 tag 字符串进行比对,如果不同,则丢弃该消息,不进行消息消费 * SQL92 过滤:工作流程和 Tag 过滤大致一样,只是在 Store 层的具体过滤方式不一样。真正的 SQL expression 的构建和执行由 rocketmq-filter 模块负责,每次过滤都去执行 SQL 表达式会影响效率,所以 RocketMQ 使用了 BloomFilter 来避免了每次都去执行 @@ -4309,16 +4309,16 @@ RocketMQ 支持分布式事务消息,采用了 2PC 的思想来实现了提交 1. 事务消息发送及提交: * 发送消息(Half 消息) - - * 服务端响应消息写入结果 - +* 服务端响应消息写入结果 * 根据发送结果执行本地事务(如果写入失败,此时 Half 消息对业务不可见,本地逻辑不执行) - * 根据本地事务状态执行 Commit 或者 Rollback(Commit 操作生成消息索引,消息对消费者可见) +* 根据本地事务状态执行 Commit 或者 Rollback(Commit 操作生成消息索引,消息对消费者可见) + + ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-事务工作流程.png) 2. 补偿流程: - * 对没有 Commit/Rollback 的事务消息(pending 状态的消息),从服务端发起一次回查 - * Producer 收到回查消息,检查回查消息对应的本地事务的状态 + * 对没有 Commit/Rollback 的事务消息(pending 状态的消息),服务端根据根据半消息的生产者组,到 ProducerManager 中获取生产者的会话通道,发起一次回查(**单向请求**) + * Producer 收到回查消息,检查事务消息状态表内对应的本地事务的状态 * 根据本地事务状态,重新 Commit 或者 Rollback @@ -4370,7 +4370,7 @@ RocketMQ 将 Op 消息写入到全局一个特定的 Topic 中,通过源码中 如果在 RocketMQ 事务消息的二阶段过程中失败了,例如在做 Commit 操作时,出现网络问题导致 Commit 失败,那么需要通过一定的策略使这条消息最终被 Commit,RocketMQ 采用了一种补偿机制,称为回查 -Broker 端通过对比 Half 消息和 Op 消息,对未确定状态的消息发起回查并且推进 CheckPoint(记录哪些事务消息的状态是确定的),将消息发送到对应的 Producer 端(同一个 Group 的 Producer),由 Producer 根据消息来检查本地事务的状态,然后执行提交或回滚 +Broker 服务端通过对比 Half 消息和 Op 消息,对未确定状态的消息发起回查并且推进 CheckPoint(记录哪些事务消息的状态是确定的),将消息发送到对应的 Producer 端(同一个 Group 的 Producer),由 Producer 根据消息来检查本地事务的状态,然后执行提交或回滚 注意:RocketMQ 并不会无休止的进行事务状态回查,默认回查 15 次,如果 15 次回查还是无法得知事务状态,则默认回滚该消息 @@ -4447,7 +4447,7 @@ public class TransactionListenerImpl implements TransactionListener { } ``` -使用 **TransactionMQProducer** 类创建事务性生产者,并指定唯一的 `ProducerGroup`,就可以设置自定义线程池来处理这些检查请求,执行本地事务后、需要根据执行结果对消息队列进行回复 +使用 **TransactionMQProducer** 类创建事务性生产者,并指定唯一的 `ProducerGroup`,就可以设置自定义线程池来处理这些检查请求,执行本地事务后,需要根据执行结果对消息队列进行回复 ```java public class Producer { @@ -4787,7 +4787,7 @@ RocketMQ 支持消息的高可靠,影响消息可靠性的几种情况: RocketMQ 中的负载均衡可以分为 Producer 端发送消息时候的负载均衡和 Consumer 端订阅消息的负载均衡 -Producer 端在发送消息时,会先根据 Topic 找到指定的 TopicPublishInfo,在获取了 TopicPublishInfo 路由信息后,RocketMQ 的客户端在默认方式调用 selectOneMessageQueue() 方法从 TopicPublishInfo 中的 messageQueueList 中选择一个队列 MessageQueue 进行发送消息 +Producer 端在发送消息时,会先根据 Topic 找到指定的 TopicPublishInfo,在获取了 TopicPublishInfo 路由信息后,RocketMQ 的客户端在默认方式调用 `selectOneMessageQueue()` 方法从 TopicPublishInfo 中的 messageQueueList 中选择一个队列 MessageQueue 进行发送消息 默认会轮询所有的 Message Queue 发送,以让消息平均落在不同的 queue 上,而由于 queue可以散落在不同的 Broker,所以消息就发送到不同的 Broker 下,图中箭头线条上的标号代表顺序,发布方会把第一条消息发送至 Queue 0,然后第二条消息发送至 Queue 1,以此类推: @@ -4798,7 +4798,7 @@ Producer 端在发送消息时,会先根据 Topic 找到指定的 TopicPublish * 如果开启,会在随机递增取模的基础上,再过滤掉 not available 的 Broker 代理 * 如果关闭,采用随机递增取模的方式选择一个队列(MessageQueue)来发送消息 -latencyFaultTolerance 机制是实现消息发送高可用的核心关键所在,对之前失败的,按一定的时间做退避。例如上次请求的 latency 超过 550Lms,就退避 3000Lms;超过 1000L,就退避 60000L +LatencyFaultTolerance 机制是实现消息发送高可用的核心关键所在,对之前失败的,按一定的时间做退避。例如上次请求的 latency 超过 550Lms,就退避 3000Lms;超过 1000L,就退避 60000L @@ -5695,7 +5695,7 @@ NettyRemotingAbstract#processRequestCommand:**处理请求的数据** `DefaultRequestProcessor.processRequest`:**根据业务码处理请求,执行对应的操作** - `ClientRemotingProcessor.processRequest`:处理回退消息,需要消费者回执一条消息给生产者 + `ClientRemotingProcessor.processRequest`:处理事务回查消息,或者回执消息,需要消费者回执一条消息给生产者 * `requestTask = new RequestTask(run, ctx.channel(), cmd)`:将任务对象、通道、请求封装成 RequestTask 对象 @@ -6264,9 +6264,22 @@ CommitLog 类核心方法: * `this.defaultMessageStore.doDispatch(dispatchRequest)`:重建 ConsumerQueue 和 Index,避免上次异常停机导致 CQ 和 Index 与 CommitLog 不对齐 * 剩余逻辑与正常关机的恢复方法相似 -消息追加服务 DefaultAppendMessageCallback + + +*** + + + +##### 服务线程 + +AppendMessageCallback 消息追加服务实现类为 DefaultAppendMessageCallback * doAppend() + + ```java + public AppendMessageResult doAppend() + ``` + * `long wroteOffset = fileFromOffset + byteBuffer.position()`:消息写入的位置,物理偏移量 phyOffset * `String msgId`:消息 ID,规则是客户端 IP + 消息偏移量 phyOffset * `byte[] topicData`:序列化消息,将消息的字段压入到 msgStoreItemMemory 这个 Buffer 中 @@ -6274,6 +6287,71 @@ CommitLog 类核心方法: * `AppendMessageResult result`:构造结果对象,包括存储位点、是否成功、队列偏移量等信息 * `CommitLog.this.topicQueueTable.put(key, ++queueOffset)`:更新队列偏移量 +FlushRealTimeService 刷盘 CL 数据,默认是异步刷盘类 FlushRealTimeService + +* run():运行方法 + + ```java + public void run() + ``` + + * `while (!this.isStopped())`:stopped为 true 才跳出循环 + + * `boolean flushCommitLogTimed`:控制线程的休眠方式,默认是 false,使用 `CountDownLatch.await()` 休眠,设置为 true 时使用 `Thread.sleep()` 休眠 + + * `int interval`:获取配置中的刷盘时间间隔 + + * `int flushPhysicQueueLeastPages`:获取最小刷盘页数,默认是 4 页,脏页达到指定页数才刷盘 + + * `int flushPhysicQueueThoroughInterval`:获取强制刷盘周期,默认是 10 秒,达到周期后强制刷盘,不考虑脏页 + + * `if (flushCommitLogTimed)`:休眠逻辑,避免 CPU 占用太长时间,导致无法执行其他更紧急的任务 + + * `CommitLog.this.mappedFileQueue.flush(flushPhysicQueueLeastPages)`:**刷盘** + + * `for (int i = 0; i < RETRY_TIMES_OVER && !result; i++)`:stopped 停止标记为 true 时,需要确保所有的数据都已经刷盘,所以此处尝试 10 次强制刷盘, + + `result = CommitLog.this.mappedFileQueue.flush(0)`:**强制刷盘** + +同步刷盘类 GroupCommitService + +* run():运行方法 + + ```java + public void run() + ``` + + * `while (!this.isStopped())`:stopped为 true 才跳出循环 + + `this.waitForRunning(10)`:线程休眠 10 毫秒,最后调用 `onWaitEnd()` 进行请求的交换 `swapRequests()` + + `this.doCommit()`:做提交逻辑 + + * `if (!this.requestsRead.isEmpty()) `:读请求集合不为空 + + `for (GroupCommitRequest req : this.requestsRead)`:遍历所有的读请求,请求中的属性: + + * `private final long nextOffset`:本条消息存储之后,下一条消息开始的 offset + * `private CompletableFuture flushOKFuture`:Future 对象 + + `boolean flushOK = ...`:当前请求关注的数据是否全部落盘,**落盘成功唤醒消费者线程** + + `for (int i = 0; i < 2 && !flushOK; i++)`:尝试进行两次强制刷盘,保证刷盘成功 + + `CommitLog.this.mappedFileQueue.flush(0)`:强制刷盘 + + `req.wakeupCustomer(flushOK ? ...)`:设置 Future 结果,在 Future 阻塞的线程在这里会被唤醒 + + `this.requestsRead.clear()`:清理 reqeustsRead 列表,方便交换时 成为 requestsWrite 使用 + + * `else`:读请求集合为空 + + `CommitLog.this.mappedFileQueue.flush(0)`:强制刷盘 + + * `this.swapRequests()`:交换读写请求 + + * `this.doCommit()`:交换后做一次提交 + **** @@ -6594,136 +6672,189 @@ IndexService 类用来管理 IndexFile 文件 -#### MesStore +#### HAService -##### 生命周期 +##### HAService -DefaultMessageStore 类核心是整个存储服务的调度类 +###### Service -* 构造方法: +HAService 类成员变量: + +* 主节点属性: ```java - public DefaultMessageStore() + // master 节点当前有多少个 slave 节点与其进行数据同步 + private final AtomicInteger connectionCount = new AtomicInteger(0); + // master 节点会给每个发起连接的 slave 节点的通道创建一个 HAConnection,控制 master 端向 slave 端传输数据 + private final List connectionList = new LinkedList<>(); + // master 向 slave 节点推送的最大的 offset,表示数据同步的进度 + private final AtomicLong push2SlaveMaxOffset = new AtomicLong(0) ``` - * `this.allocateMappedFileService.start()`:启动**创建 MappedFile 文件服务** - * `this.indexService.start()`:启动索引服务 - -* load():加载资源 +* 内部类属性: ```java - public boolean load() + // 封装了绑定服务器指定端口,监听 slave 的连接的逻辑,没有使用 Netty,使用了原生态的 NIO 去做 + private final AcceptSocketService acceptSocketService; + // 控制生产者线程阻塞等待的逻辑 + private final GroupTransferService groupTransferService; + // slave 节点的客户端对象,【slave 端才会正常运行该实例】 + private final HAClient haClient; ``` - * `this.commitLog.load()`:先加载 CommitLog - * `this.loadConsumeQueue()`:再加载 ConsumeQueue - * `this.storeCheckpoint`:检查位点对象 - * `this.indexService.load(lastExitOK)`:加载 IndexFile - * `this.recover(lastExitOK)`:恢复阶段,先恢复 CQ,在恢复 CL - -* start():核心启动方法 +* 线程通信对象: ```java - public void start() + private final WaitNotifyObject waitNotifyObject = new WaitNotifyObject() ``` - * `lock = lockFile.getChannel().tryLock(0, 1, false)`:获取文件锁,获取失败说明当前目录已经启动过 Broker - - * `long maxPhysicalPosInLogicQueue = commitLog.getMinOffset()`:遍历全部的 CQ 对象,获取 CQ 中消息的最大偏移量 - - * `this.reputMessageService.start()`:设置分发服务的分发位点,启动**分发服务**,构建 ConsumerQueue 和 IndexFile +成员方法: - * `if (dispatchBehindBytes() <= 0)`:线程等待分发服务将分发数据全部处理完毕 +* start():启动高可用服务 - * `this.recoverTopicQueueTable()`:因为修改了 CQ 数据,所以再次构建队列偏移量字段表 + ```java + public void start() throws Exception { + // 监听从节点 + this.acceptSocketService.beginAccept(); + // 启动监听服务 + this.acceptSocketService.start(); + // 启动转移服务 + this.groupTransferService.start(); + // 启动从节点客户端实例 + this.haClient.start(); + } + ``` - * `this.haService.start()`:启动 **HA 服务** - * `this.handleScheduleMessageService()`:启动**消息调度服务** - * `this.flushConsumeQueueService.start()`:启动 CQ **消费队列刷盘服务** +**** - * `this.commitLog.start()`:启动 **CL 刷盘服务** - * `this.storeStatsService.start()`:启动状态存储服务 - * `this.createTempFile()`:创建 AbortFile,正常关机时 JVM HOOK 会删除该文件,异常宕机时该文件不会删除,开机数据恢复阶段根据是否存在该文件,执行不同的**恢复策略** +###### Accept - * `this.addScheduleTask()`:添加定时任务 +AcceptSocketService 类用于监听从节点的连接,创建 HAConnection 连接对象 - * `DefaultMessageStore.this.cleanFilesPeriodically()`:定时**清理过期文件**,周期是 10 秒 +成员变量: - * `this.cleanCommitLogService.run()`:启动清理过期的 CL 文件服务 - * `this.cleanConsumeQueueService.run()`:启动清理过期的 CQ 文件服务 +* 端口信息:Master 绑定监听的端口信息 - * `DefaultMessageStore.this.checkSelf()`:每 10 分种进行健康检查 + ```java + private final SocketAddress socketAddressListen; + ``` - * `DefaultMessageStore.this.cleanCommitLogService.isSpaceFull()`:**磁盘预警**定时任务,每 10 秒一次 +* 服务端通道: - * `if (physicRatio > this.diskSpaceWarningLevelRatio)`:检查磁盘是否到达 waring 阈值,默认 90% + ```java + private ServerSocketChannel serverSocketChannel; + ``` - `boolean diskok = ...runningFlags.getAndMakeDiskFull()`:设置磁盘写满标记 +* 多路复用器: - * `boolean diskok = ...this.runningFlags.getAndMakeDiskOK()`:设置磁盘可写标记 + ```java + private Selector selector; + ``` - * `this.shutdown = false`:刚启动,设置为 false +成员方法: -* shutdown():关闭各种服务和线程资源,设置存储模块状态为关闭状态 +* beginAccept():开始监听连接,**NIO** 标准模板 ```java - public void shutdown() + public void beginAccept() ``` -* destroy():销毁 Broker 的工作目录 + * `this.serverSocketChannel = ServerSocketChannel.open()`:获取服务端 SocketChannel + * `this.selector = RemotingUtil.openSelector()`:获取多路复用器 + * `this.serverSocketChannel.socket().setReuseAddress(true)`:开启通道可重用 + * `this.serverSocketChannel.socket().bind(this.socketAddressListen)`:绑定连接端口 + * `this.serverSocketChannel.configureBlocking(false)`:设置非阻塞 + * `this.serverSocketChannel.register(this.selector, SelectionKey.OP_ACCEPT)`:将通道注册到多路复用器上,关注 `OP_ACCEPT` 事件 + +* run():服务启动 ```java - public void destroy() + public void run() ``` + * `this.selector.select(1000)`:多路复用器阻塞获取就绪的通道,最多等待 1 秒钟 + * `Set selected = this.selector.selectedKeys()`:获取选择器中所有注册的通道中已经就绪好的事件 + * `for (SelectionKey k : selected)`:遍历所有就绪的事件 + * `if ((k.readyOps() & SelectionKey.OP_ACCEPT) != 0)`:说明 `OP_ACCEPT` 事件就绪 + * `SocketChannel sc = ((ServerSocketChannel) k.channel()).accept()`:**获取到客户端连接的通道** + * `HAConnection conn = new HAConnection(HAService.this, sc)`:**为每个连接 master 服务器的 slave 创建连接对象** + * `conn.start()`:**启动 HAConnection 对象**,内部启动两个服务为读数据服务、写数据服务 + * `HAService.this.addConnection(conn)`:加入到 HAConnection 集合内 +**** -*** +###### Group -##### 服务线程 +GroupTransferService 用来控制数据同步 -ServiceThread 类被很多服务继承,本身是一个 Runnable 任务对象,继承者通过重写 run 方法来实现服务的逻辑 +成员方法: -* run():一般实现方式 +* doWaitTransfer():等待主从数据同步 ```java - public void run() { - while (!this.isStopped()) { - // 业务逻辑 - } - } + private void doWaitTransfer() ``` - 通过参数 stopped 控制服务的停止,使用 volatile 修饰保证可见性 + * `if (!this.requestsRead.isEmpty())`:读请求不为空 + * `boolean transferOK = HAService.this.push2SlaveMaxOffset.get() >= req.getNextOffset()`:主从同步是否完成 + * `req.wakeupCustomer(transferOK ? ...)`:唤醒消费者 + * `this.requestsRead.clear()`:清空读请求 + + + +**** + + + +##### HAClient + +###### 成员属性 + +HAClient 是 slave 端运行的代码,用于和 master 服务器建立长连接,上报本地同步进度,消费服务器发来的 msg 数据 + +成员变量: + +* 缓冲区: ```java - protected volatile boolean stopped = false + private static final int READ_MAX_BUFFER_SIZE = 1024 * 1024 * 4; // 默认大小:4 MB + private ByteBuffer byteBufferRead = ByteBuffer.allocate(READ_MAX_BUFFER_SIZE); + private ByteBuffer byteBufferBackup = ByteBuffer.allocate(READ_MAX_BUFFER_SIZE); ``` -* shutdown():停止线程,首先设置 stopped 为 true,然后进行唤醒,默认不直接打断线程 +* 主节点地址:格式为 `ip:port` ```java - public void shutdown() + private final AtomicReference masterAddress = new AtomicReference<>() ``` -* waitForRunning():挂起线程,设置唤醒标记 hasNotified 为 false +* NIO 属性: ```java - protected void waitForRunning(long interval) + private final ByteBuffer reportOffset; // 通信使用NIO,所以消息使用块传输,上报 slave offset 使用 + private SocketChannel socketChannel; // 客户端与 master 的会话通道 + private Selector selector; // 多路复用器 ``` -* wakeup():唤醒线程,设置 hasNotified 为 true +* 通信时间:上次会话通信时间,用于控制 socketChannel 是否关闭的 + + ````java + private long lastWriteTimestamp = System.currentTimeMillis(); + ```` + +* 进度信息: ```java - public void wakeup() + private long currentReportedOffset = 0; // slave 当前的进度信息 + private int dispatchPosition = 0; // 控制 byteBufferRead position 指针 ``` @@ -6732,151 +6863,710 @@ ServiceThread 类被很多服务继承,本身是一个 Runnable 任务对象 -##### 构建服务 - -AllocateMappedFileService 创建 MappedFile 服务 +###### 成员方法 -* mmapOperation():核心服务 +* run():启动方法 ```java - private boolean mmapOperation() + public void run() ``` - * `req = this.requestQueue.take()`: 从 requestQueue 阻塞队列(优先级)中获取 AllocateRequest 任务 - * `if (...isTransientStorePoolEnable())`:条件成立使用直接内存写入数据, 从直接内存中 commit 到 FileChannel 中 - * `mappedFile = new MappedFile(req.getFilePath(), req.getFileSize())`:根据请求的路径和大小创建对象 - * `mappedFile.warmMappedFile()`:判断 mappedFile 大小,只有 CommitLog 才进行文件预热 - * `req.setMappedFile(mappedFile)`:将创建好的 MF 对象的赋值给请求对象的成员属性 - * `req.getCountDownLatch().countDown()`:**唤醒请求的阻塞线程** + * `if (this.connectMaster())`:连接主节点,连接失败会休眠 5 秒 -* putRequestAndReturnMappedFile():MappedFileQueue 中用来创建 MF 对象的方法 + * `String addr = this.masterAddress.get()`:获取 master 暴露的 HA 地址端口信息 + * `this.socketChannel = RemotingUtil.connect(socketAddress)`:建立连接 + * `this.socketChannel.register(this.selector, SelectionKey.OP_READ)`:注册到多路复用器,**关注读事件** + * `this.currentReportedOffset`: 初始化上报进度字段为 slave 的 maxPhyOffset - ```java - public MappedFile putRequestAndReturnMappedFile(String nextFilePath, String nextNextFilePath, int fileSize) - ``` + * `if (this.isTimeToReportOffset())`:slave 每 5 秒会上报一次 slave 端的同步进度信息给 master - * `AllocateRequest nextReq = new AllocateRequest(...)`:创建 nextFilePath 的 AllocateRequest 对象,放入请求列表和阻塞队列,然后创建 nextNextFilePath 的 AllocateRequest 对象,放入请求列表和阻塞队列 - * `AllocateRequest result = this.requestTable.get(nextFilePath)`:从请求列表获取 nextFilePath 的请求对象 - * `result.getCountDownLatch().await(...)`:**线程挂起**,直到超时或者 nextFilePath 对应的 MF 文件创建完成 - * `return result.getMappedFile()`:返回创建好的 MF 文件对象 + `boolean result = this.reportSlaveMaxOffset()`:上报同步信息,上报失败关闭连接 -ReputMessageService 消息分发服务,用于构建 ConsumerQueue 和 IndexFile 文件 + * `this.selector.select(1000)`:多路复用器阻塞获取就绪的通道,最多等待 1 秒钟,**获取到就绪事件或者超时后结束** -* run():循环执行 doReput 方法,每执行一次线程休眠 1 毫秒 + * `boolean ok = this.processReadEvent()`:处理读事件 + + * `if (!reportSlaveMaxOffsetPlus())`:检查是否重新上报同步进度 + +* reportSlaveMaxOffset():上报 slave 同步进度 ```java - public void run() + private boolean reportSlaveMaxOffset(final long maxOffset) ``` -* doReput():实现分发的核心逻辑 + * 首先向缓冲区写入 slave 端最大偏移量,写完以后切换为指定置为初始状态 + + * `for (int i = 0; i < 3 && this.reportOffset.hasRemaining(); i++)`:尝试三次写数据 + + `this.socketChannel.write(this.reportOffset)`:**写数据** + + * `return !this.reportOffset.hasRemaining()`:写成功之后 pos = limit + +* processReadEvent():处理 master 发送给 slave 数据,返回 true 表示处理成功 false 表示 Socket 处于半关闭状态,需要上层重建 haClient ```java - private void doReput() + private boolean processReadEvent() ``` - * `for (boolean doNext = true; this.isCommitLogAvailable() && doNext; )`:循环遍历 - * `SelectMappedBufferResult result`: 从 CommitLog 拉取数据,数据范围 `[reputFromOffset, 包含该偏移量的 MF 的最大 Pos]`,封装成结果对象 - * `DispatchRequest dispatchRequest`:从结果对象读取出一条 DispatchRequest 数据 - * `DefaultMessageStore.this.doDispatch(dispatchRequest)`:将数据交给分发器进行分发,用于**构建 CQ 和索引文件** - * `this.reputFromOffset += size`:更新数据范围 + * `int readSizeZeroTimes = 0`:控制 while 循环的一个条件变量,当值为 3 时跳出循环 + * `while (this.byteBufferRead.hasRemaining())`:byteBufferRead 有空间可以去 Socket 读缓冲区加载数据 + * `int readSize = this.socketChannel.read(this.byteBufferRead)`:**从通道读数据** -*** + * `if (readSize > 0)`:加载成功,有新数据 + `readSizeZeroTimes = 0`:置为 0 + `boolean result = this.dispatchReadRequest()`:处理数据的核心逻辑 -##### 刷盘服务 + * `else if (readSize == 0) `:无新数据 -FlushConsumeQueueService 刷盘 CQ 数据 + `if (++readSizeZeroTimes >= 3)`:大于 3 时跳出循环 -* run():每隔 1 秒执行一次刷盘服务,跳出循环后还会执行一次强制刷盘 + * `else`:readSize = -1 就表示 Socket 处于半关闭状态,对端已经关闭了 + +* dispatchReadRequest():**处理数据的核心逻辑**,master 与 slave 传输的数据格式 `{[phyOffset][size][data...]}`,phyOffset 表示数据区间的开始偏移量,data 代表数据块,最大 32kb,可能包含多条消息的数据 ```java - public void run() + private boolean dispatchReadRequest() ``` -* doFlush():刷盘 + * `final int msgHeaderSize = 8 + 4`:协议头大小 12 - ```java - private void doFlush(int retryTimes) - ``` + * `int readSocketPos = this.byteBufferRead.position()`:记录缓冲区处理数据前的 pos 位点,用于恢复指针 - * `int flushConsumeQueueLeastPages`:脏页阈值,默认是 2 + * `int diff = ...`:当前 byteBufferRead 还剩多少 byte 未处理,每处理一条帧数据都会更新 dispatchPosition - * `if (retryTimes == RETRY_TIMES_OVER)`:**重试次数是 3** 时设置强制刷盘,设置脏页阈值为 0 - * `int flushConsumeQueueThoroughInterval`:两次刷新的**时间间隔超过 60 秒**会强制刷盘 - * `for (ConsumeQueue cq : maps.values())`:遍历所有的 CQ,进行刷盘 - * `DefaultMessageStore.this.getStoreCheckpoint().flush()`:强制刷盘时将 StoreCheckpoint 瞬时数据刷盘 + * `if (diff >= msgHeaderSize)`:缓冲区还有完整的协议头 header 数据 -FlushCommitLogService 刷盘 CL 数据,默认是异步刷盘 + * `long masterPhyOffset, int bodySize`:读取 header 信息 -* run():运行方法 + * `long slavePhyOffset`:获取 slave 端最大的物理偏移量 - ```java - public void run() - ``` + * `if (slavePhyOffset != masterPhyOffset)`:正常情况两者是相等的,因为是一帧一帧同步的 - * `while (!this.isStopped())`:stopped为 true 才跳出循环 - - * `boolean flushCommitLogTimed`:控制线程的休眠方式,默认是 false,使用 `CountDownLatch.await()` 休眠,设置为 true 时使用 `Thread.sleep()` 休眠 - - * `int interval`:获取配置中的刷盘时间间隔 - - * `int flushPhysicQueueLeastPages`:获取最小刷盘页数,默认是 4 页,脏页达到指定页数才刷盘 - - * `int flushPhysicQueueThoroughInterval`:获取强制刷盘周期,默认是 10 秒,达到周期后强制刷盘,不考虑脏页 - - * `if (flushCommitLogTimed)`:休眠逻辑,避免 CPU 占用太长时间,导致无法执行其他更紧急的任务 - - * `CommitLog.this.mappedFileQueue.flush(flushPhysicQueueLeastPages)`:**刷盘** - - * `for (int i = 0; i < RETRY_TIMES_OVER && !result; i++)`:stopped 停止标记为 true 时,需要确保所有的数据都已经刷盘,所以此处尝试 10 次强制刷盘, - - `result = CommitLog.this.mappedFileQueue.flush(0)`:**强制刷盘** + * `if (diff >= (msgHeaderSize + bodySize))`:说明**缓冲区内是包含当前帧的全部数据的**,开始处理帧数据 + `byte[] bodyData = new byte[bodySize]`:提取帧内的 body 数据 + `this.byteBufferRead.position(this.dispatchPosition + msgHeaderSize)`:**设置 pos 为当前帧的 body 起始位置** -*** + `this.byteBufferRead.get(bodyData)`:读取数据到 bodyData + `HAService...appendToCommitLog(masterPhyOffset, bodyData)`:存储数据到 CommitLog + `this.byteBufferRead.position(readSocketPos)`:恢复 byteBufferRead 的 pos 指针 -##### 清理服务 + `this.dispatchPosition += msgHeaderSize + bodySize`:**加一帧数据长度** -CleanCommitLogService 清理过期的 CL 数据,定时任务 10 秒调用一次,**先清理 CL,再清理 CQ**,因为 CQ 依赖于 CL 的数据 + `if (!reportSlaveMaxOffsetPlus())`:上报 slave 同步信息 -* run():运行方法 + * `if (!this.byteBufferRead.hasRemaining())`:缓冲区写满了 - ```java - public void run() - ``` + `this.reallocateByteBuffer()`:重新分配缓冲区 -* deleteExpiredFiles():删除过期 CL 文件 +* reallocateByteBuffer():重新分配缓冲区 ```java - private void deleteExpiredFiles() + private void reallocateByteBuffer() ``` - * `long fileReservedTime`:默认 72,代表文件的保留时间 - * `boolean timeup = this.isTimeToDelete()`:当前时间是否是凌晨 4 点 - * `boolean spacefull = this.isSpaceToDelete()`:CL 或者 CQ 的目录磁盘使用率达到阈值标准 85% - * `boolean manualDelete = this.manualDeleteFileSeveralTimes > 0`:手动删除文件 - * `fileReservedTime *= 60 * 60 * 1000`:默认保留 72 小时 - * `deleteCount = DefaultMessageStore.this.commitLog.deleteExpiredFile()`:**调用 MFQ 对象的删除方法** + * `int remain = READ_MAX_BUFFER_SIZE - this.dispatchPosition`:表示缓冲区尚未处理过的字节数量 -CleanConsumeQueueService 清理过期的 CQ 数据 + * `if (remain > 0)`:条件成立,说明缓冲区**最后一帧数据是半包数据**,但是不能丢失数据 -* run():运行方法 + `this.byteBufferBackup.put(this.byteBufferRead)`:**将半包数据拷贝到 backup 缓冲区** - ```java - public void run() - ``` + * `this.swapByteBuffer()`:交换 backup 成为 read -* deleteExpiredFiles():删除过期 CQ 文件 + * `this.byteBufferRead.position(remain)`:设置 pos 为 remain ,后续加载数据 pos 从remain 开始向后移动 - ```java - private void deleteExpiredFiles() - ``` + * `this.dispatchPosition = 0`:当前缓冲区交换之后,相当于是一个全新的 byteBuffer,所以分配指针归零 + + + +*** + + + +##### HAConn + +###### Connection + +HAConnection 类成员变量: + +* 会话通道:master 和 slave 之间通信的 SocketChannel + + ```java + private final SocketChannel socketChannel; + ``` + +* 客户端地址: + + ```java + private final String clientAddr; + ``` + +* 服务类: + + ```java + private WriteSocketService writeSocketService; // 写数据服务 + private ReadSocketService readSocketService; // 读数据服务 + ``` + +* 请求位点:在 slave上报本地的进度之后被赋值,该值大于 0 后同步逻辑才会运行,master 如果不知道 slave 节点当前消息的存储进度,就无法给 slave 推送数据 + + ```java + private volatile long slaveRequestOffset = -1; + ``` + +* 应答位点: 保存最新的 slave 上报的 offset 信息,slaveAckOffset 之前的数据都可以认为 slave 已经同步完成 + + ```java + private volatile long slaveAckOffset = -1; + ``` + +核心方法: + +* 构造方法: + + ```java + public HAConnection(final HAService haService, final SocketChannel socketChannel) { + // 初始化一些东西 + // 设置 socket 读写缓冲区为 64kb 大小 + this.socketChannel.socket().setReceiveBufferSize(1024 * 64); + this.socketChannel.socket().setSendBufferSize(1024 * 64); + // 创建读写服务 + this.writeSocketService = new WriteSocketService(this.socketChannel); + this.readSocketService = new ReadSocketService(this.socketChannel); + // 自增 + this.haService.getConnectionCount().incrementAndGet(); + } + ``` + +* 启动方法: + + ```java + public void start() { + this.readSocketService.start(); + this.writeSocketService.start(); + } + ``` + + + +*** + + + +###### ReadSocket + +ReadSocketService 类是一个任务对象,slave 向 master 传输的帧格式为 `[long][long][long]`,上报的是 slave 本地的同步进度,同步进度是一个 long 值 + +成员变量: + +* 读缓冲: + + ```java + private static final int READ_MAX_BUFFER_SIZE = 1024 * 1024; // 默认大小 1MB + private final ByteBuffer byteBufferRead = ByteBuffer.allocate(READ_MAX_BUFFER_SIZE); + ``` + +* NIO 属性: + + ```java + private final Selector selector; // 多路复用器 + private final SocketChannel socketChannel; // master 与 slave 之间的会话 SocketChannel + ``` + +* 处理位点:缓冲区处理位点 + + ```java + private int processPosition = 0; + ``` + +* 上次读操作的时间: + + ```java + private volatile long lastReadTimestamp = System.currentTimeMillis(); + ``` + +核心方法: + +* 构造方法: + + ```java + public ReadSocketService(final SocketChannel socketChannel) + ``` + + * `this.socketChannel.register(this.selector, SelectionKey.OP_READ)`:通道注册到多路复用器,关注读事件 + * `this.setDaemon(true)`:设置为守护线程 + +* 运行方法: + + ```java + public void run() + ``` + + * `this.selector.select(1000)`:多路复用器阻塞获取就绪的通道,最多等待 1 秒钟,获取到就绪事件或者超时后结束 + + * `boolean ok = this.processReadEvent()`:**读数据的核心方法**,返回 true 表示处理成功 false 表示 Socket 处于半关闭状态,需要上层重建 HAConnection 对象 + + * `int readSizeZeroTimes = 0`:控制 while 循环,当连续从 Socket 读取失败 3 次(未加载到数据)跳出循环 + + * `if (!this.byteBufferRead.hasRemaining())`:byteBufferRead 已经全部使用完,需要清理数据并更新位点 + + * `while (this.byteBufferRead.hasRemaining())`:byteBufferRead 有空间可以去 Socket 读缓冲区加载数据 + + * `int readSize = this.socketChannel.read(this.byteBufferRead)`:**从通道读数据** + + * `if (readSize > 0)`:加载成功,有新数据 + + `readSizeZeroTimes = 0`:置为 0 + + `if ((byteBufferRead.position() - processPosition) >= 8)`:缓冲区的可读数据最少包含一个数据帧 + + * `int pos = ...`:**获取可读帧数据中最后一个完整的帧数据的位点**,后面的数据丢弃 + * `long readOffset = ...byteBufferRead.getLong(pos - 8)`:读取最后一帧数据,slave 端当前的同步进度信息 + * `this.processPosition = pos`:更新处理位点 + * `HAConnection.this.slaveAckOffset = readOffset`:更新应答位点 + * `if (HAConnection.this.slaveRequestOffset < 0)`:条件成立**给 slaveRequestOffset 赋值** + * `HAConnection...notifyTransferSome(slaveAckOffset)`:**唤醒阻塞的生产者线程** + + * `else if (readSize == 0) `:无新数据 + + `if (++readSizeZeroTimes >= 3)`:大于 3 时跳出循环 + + * `else`:readSize = -1 就表示 Socket 处于半关闭状态,对端已经关闭了 + + * `if (interval > 20)`:超过 20 秒未发生通信,直接结束循环 + + + +*** + + + +###### WriteSocket + +WriteSocketService 类是一个任务对象,master向 slave 传输的数据帧格式为 `{[phyOffset][size][data...]}{[phyOffset][size][data...]}`,上报的是 slave 本地的同步进度,同步进度是一个 long 值 + +* phyOffset:数据区间的开始偏移量,并不表示某一条具体的消息,表示的数据块开始的偏移量位置 +* size:同步的数据块的大小 +* data:数据块,最大 32kb,可能包含多条消息的数据 + +成员变量: + +* 协议头: + + ```java + private final int headerSize = 8 + 4; // 协议头大小:12 + private final ByteBuffer byteBufferHeader; // 帧头缓冲区 + ``` + +* NIO 属性: + + ```java + private final Selector selector; // 多路复用器 + private final SocketChannel socketChannel; // master 与 slave 之间的会话 SocketChannel + ``` + +* 处理位点:下一次传输同步数据的位置信息,master 给当前 slave 同步的位点 + + ```java + private long nextTransferFromWhere = -1; + ``` + +* 上次写操作: + + ```java + private boolean lastWriteOver = true; // 上一轮数据是否传输完毕 + private long lastWriteTimestamp = System.currentTimeMillis(); // 上次写操作的时间 + ``` + +核心方法: + +* 构造方法: + + ```java + public WriteSocketService(final SocketChannel socketChannel) + ``` + + * `this.socketChannel.register(this.selector, SelectionKey.OP_WRITE)`:通道注册到多路复用器,关注写事件 + * `this.setDaemon(true)`:设置为守护线程 + +* 运行方法: + + ```java + public void run() + ``` + + * `this.selector.select(1000)`:多路复用器阻塞获取就绪的通道,最多等待 1 秒钟,获取到就绪事件或者超时后结束 + + * `if (-1 == HAConnection.this.slaveRequestOffset)`:**等待 slave 同步完数据** + + * `if (-1 == this.nextTransferFromWhere)`:条件成立,需要初始化该变量 + + `if (0 == HAConnection.this.slaveRequestOffset)`:slave 是一个全新节点,从正在顺序写的 MF 开始同步数据 + + `long masterOffset = ...`:获取 master 最大的 offset,并计算归属的 mappedFile 文件的开始 offset + + `this.nextTransferFromWhere = masterOffset`:**赋值给下一次传输同步数据的位置信息** + + `this.nextTransferFromWhere = HAConnection.this.slaveRequestOffset`:大部分情况走这个赋值逻辑 + + * `if (this.lastWriteOver)`:上一次待发送数据全部发送完成 + + `if (interval > 5)`:超过 5 秒未同步数据,发送一个 header 数据包,维持长连接 + + * `else`:上一轮的待发送数据未全部发送,需要同步数据到 slave 节点 + + * `SelectMappedBufferResult selectResult`:到 CommitLog 中查询 nextTransferFromWhere 开始位置的数据 + + * `if (size > 32k)`:**一次最多同步 32k 数据** + + * `this.nextTransferFromWhere += size`:增加 size,下一轮传输跳过本帧数据 + + * `selectResult.getByteBuffer().limit(size)`:设置 byteBuffer 可访问数据区间为 [pos, size] + + * `this.selectMappedBufferResult = selectResult`:**待发送的数据** + + * `this.byteBufferHeader.put`:**构建帧头数据** + + * `this.lastWriteOver = this.transferData()`:处理数据,返回是否处理完成 + +* 同步方法:**同步数据到 slave 节点**,返回 true 表示本轮数据全部同步完成,false 表示本轮同步未完成(Header 和 Body 其中一个未同步完成都会返回 false) + + ```java + private boolean transferData() + ``` + + * `int writeSizeZeroTimes= 0`:控制 while 循环,当写失败连续 3 次时,跳出循环)跳出循环 + + * `while (this.byteBufferHeader.hasRemaining())`:**帧头数据缓冲区有待发送的数据** + + * `int writeSize = this.socketChannel.write(this.byteBufferHeader)`:向通道写帧头数据 + + * `if (writeSize > 0)`:写数据成功 + + `writeSizeZeroTimes = 0`:控制变量置为 0 + + * `else if (readSize == 0)`:写失败 + + `if (++readSizeZeroTimes >= 3)`:大于 3 时跳出循环 + + * `if (null == this.selectMappedBufferResult)`:说明是心跳数据,返回心跳数据是否发送完成 + + * `writeSizeZeroTimes = 0`:控制变量置为 0 + + * `if (!this.byteBufferHeader.hasRemaining())`:**Header写成功之后,才进行写 Body** + + * `while (this.selectMappedBufferResult.getByteBuffer().hasRemaining())`:**数据缓冲区有待发送的数据** + + * `int writeSize = this.socketChannel.write(this.selectMappedBufferResult...)`:向通道写帧头数据 + + * `if (writeSize > 0)`:写数据成功,但是不代表 SMBR 中的数据全部写完成 + + `writeSizeZeroTimes = 0`:控制变量置为 0 + + * `else if (readSize == 0)`:写失败,因为 Socket 写缓冲区写满了 + + `if (++readSizeZeroTimes >= 3)`:大于 3 时跳出循环 + + * `boolean result`:判断是否发送完成,返回该值 + + + + + +**** + + + +#### MesStore + +##### 生命周期 + +DefaultMessageStore 类核心是整个存储服务的调度类 + +* 构造方法: + + ```java + public DefaultMessageStore() + ``` + + * `this.allocateMappedFileService.start()`:启动**创建 MappedFile 文件服务** + * `this.indexService.start()`:启动索引服务 + +* load():加载资源 + + ```java + public boolean load() + ``` + + * `this.commitLog.load()`:先加载 CommitLog + * `this.loadConsumeQueue()`:再加载 ConsumeQueue + * `this.storeCheckpoint`:检查位点对象 + * `this.indexService.load(lastExitOK)`:加载 IndexFile + * `this.recover(lastExitOK)`:恢复阶段,先恢复 CQ,在恢复 CL + +* start():核心启动方法 + + ```java + public void start() + ``` + + * `lock = lockFile.getChannel().tryLock(0, 1, false)`:获取文件锁,获取失败说明当前目录已经启动过 Broker + + * `long maxPhysicalPosInLogicQueue = commitLog.getMinOffset()`:遍历全部的 CQ 对象,获取 CQ 中消息的最大偏移量 + + * `this.reputMessageService.start()`:设置分发服务的分发位点,启动**分发服务**,构建 ConsumerQueue 和 IndexFile + + * `if (dispatchBehindBytes() <= 0)`:线程等待分发服务将分发数据全部处理完毕 + + * `this.recoverTopicQueueTable()`:因为修改了 CQ 数据,所以再次构建队列偏移量字段表 + + * `this.haService.start()`:启动 **HA 服务** + + * `this.handleScheduleMessageService()`:启动**消息调度服务** + + * `this.flushConsumeQueueService.start()`:启动 CQ **消费队列刷盘服务** + + * `this.commitLog.start()`:启动 **CL 刷盘服务** + + * `this.storeStatsService.start()`:启动状态存储服务 + + * `this.createTempFile()`:创建 AbortFile,正常关机时 JVM HOOK 会删除该文件,异常宕机时该文件不会删除,开机数据恢复阶段根据是否存在该文件,执行不同的**恢复策略** + + * `this.addScheduleTask()`:添加定时任务 + + * `DefaultMessageStore.this.cleanFilesPeriodically()`:定时**清理过期文件**,周期是 10 秒 + + * `this.cleanCommitLogService.run()`:启动清理过期的 CL 文件服务 + * `this.cleanConsumeQueueService.run()`:启动清理过期的 CQ 文件服务 + + * `DefaultMessageStore.this.checkSelf()`:每 10 分种进行健康检查 + + * `DefaultMessageStore.this.cleanCommitLogService.isSpaceFull()`:**磁盘预警**定时任务,每 10 秒一次 + + * `if (physicRatio > this.diskSpaceWarningLevelRatio)`:检查磁盘是否到达 waring 阈值,默认 90% + + `boolean diskok = ...runningFlags.getAndMakeDiskFull()`:设置磁盘写满标记 + + * `boolean diskok = ...this.runningFlags.getAndMakeDiskOK()`:设置磁盘可写标记 + + * `this.shutdown = false`:刚启动,设置为 false + +* shutdown():关闭各种服务和线程资源,设置存储模块状态为关闭状态 + + ```java + public void shutdown() + ``` + +* destroy():销毁 Broker 的工作目录 + + ```java + public void destroy() + ``` + + + + + +*** + + + +##### 服务线程 + +ServiceThread 类被很多服务继承,本身是一个 Runnable 任务对象,继承者通过重写 run 方法来实现服务的逻辑 + +* run():一般实现方式 + + ```java + public void run() { + while (!this.isStopped()) { + // 业务逻辑 + } + } + ``` + + 通过参数 stopped 控制服务的停止,使用 volatile 修饰保证可见性 + + ```java + protected volatile boolean stopped = false + ``` + +* shutdown():停止线程,首先设置 stopped 为 true,然后进行唤醒,默认不直接打断线程 + + ```java + public void shutdown() + ``` + +* waitForRunning():挂起线程,设置唤醒标记 hasNotified 为 false + + ```java + protected void waitForRunning(long interval) + ``` + +* wakeup():唤醒线程,设置 hasNotified 为 true + + ```java + public void wakeup() + ``` + + + +*** + + + +##### 构建服务 + +AllocateMappedFileService 创建 MappedFile 服务 + +* mmapOperation():核心服务 + + ```java + private boolean mmapOperation() + ``` + + * `req = this.requestQueue.take()`: 从 requestQueue 阻塞队列(优先级)中获取 AllocateRequest 任务 + * `if (...isTransientStorePoolEnable())`:条件成立使用直接内存写入数据, 从直接内存中 commit 到 FileChannel 中 + * `mappedFile = new MappedFile(req.getFilePath(), req.getFileSize())`:根据请求的路径和大小创建对象 + * `mappedFile.warmMappedFile()`:判断 mappedFile 大小,只有 CommitLog 才进行文件预热 + * `req.setMappedFile(mappedFile)`:将创建好的 MF 对象的赋值给请求对象的成员属性 + * `req.getCountDownLatch().countDown()`:**唤醒请求的阻塞线程** + +* putRequestAndReturnMappedFile():MappedFileQueue 中用来创建 MF 对象的方法 + + ```java + public MappedFile putRequestAndReturnMappedFile(String nextFilePath, String nextNextFilePath, int fileSize) + ``` + + * `AllocateRequest nextReq = new AllocateRequest(...)`:创建 nextFilePath 的 AllocateRequest 对象,放入请求列表和阻塞队列,然后创建 nextNextFilePath 的 AllocateRequest 对象,放入请求列表和阻塞队列 + * `AllocateRequest result = this.requestTable.get(nextFilePath)`:从请求列表获取 nextFilePath 的请求对象 + * `result.getCountDownLatch().await(...)`:**线程挂起**,直到超时或者 nextFilePath 对应的 MF 文件创建完成 + * `return result.getMappedFile()`:返回创建好的 MF 文件对象 + +ReputMessageService 消息分发服务,用于构建 ConsumerQueue 和 IndexFile 文件 + +* run():循环执行 doReput 方法,每执行一次线程休眠 1 毫秒 + + ```java + public void run() + ``` + +* doReput():实现分发的核心逻辑 + + ```java + private void doReput() + ``` + + * `for (boolean doNext = true; this.isCommitLogAvailable() && doNext; )`:循环遍历 + * `SelectMappedBufferResult result`: 从 CommitLog 拉取数据,数据范围 `[reputFromOffset, 包含该偏移量的 MF 的最大 Pos]`,封装成结果对象 + * `DispatchRequest dispatchRequest`:从结果对象读取出一条 DispatchRequest 数据 + * `DefaultMessageStore.this.doDispatch(dispatchRequest)`:将数据交给分发器进行分发,用于**构建 CQ 和索引文件** + * `this.reputFromOffset += size`:更新数据范围 + + + +*** + + + +##### 刷盘服务 + +FlushConsumeQueueService 刷盘 CQ 数据 + +* run():每隔 1 秒执行一次刷盘服务,跳出循环后还会执行一次强制刷盘 + + ```java + public void run() + ``` + +* doFlush():刷盘 + + ```java + private void doFlush(int retryTimes) + ``` + + * `int flushConsumeQueueLeastPages`:脏页阈值,默认是 2 + + * `if (retryTimes == RETRY_TIMES_OVER)`:**重试次数是 3** 时设置强制刷盘,设置脏页阈值为 0 + * `int flushConsumeQueueThoroughInterval`:两次刷新的**时间间隔超过 60 秒**会强制刷盘 + * `for (ConsumeQueue cq : maps.values())`:遍历所有的 CQ,进行刷盘 + * `DefaultMessageStore.this.getStoreCheckpoint().flush()`:强制刷盘时将 StoreCheckpoint 瞬时数据刷盘 + +FlushCommitLogService 刷盘 CL 数据,默认是异步刷盘 + +* run():运行方法 + + ```java + public void run() + ``` + + * `while (!this.isStopped())`:stopped为 true 才跳出循环 + + * `boolean flushCommitLogTimed`:控制线程的休眠方式,默认是 false,使用 `CountDownLatch.await()` 休眠,设置为 true 时使用 `Thread.sleep()` 休眠 + + * `int interval`:获取配置中的刷盘时间间隔 + + * `int flushPhysicQueueLeastPages`:获取最小刷盘页数,默认是 4 页,脏页达到指定页数才刷盘 + + * `int flushPhysicQueueThoroughInterval`:获取强制刷盘周期,默认是 10 秒,达到周期后强制刷盘,不考虑脏页 + + * `if (flushCommitLogTimed)`:休眠逻辑,避免 CPU 占用太长时间,导致无法执行其他更紧急的任务 + + * `CommitLog.this.mappedFileQueue.flush(flushPhysicQueueLeastPages)`:**刷盘** + + * `for (int i = 0; i < RETRY_TIMES_OVER && !result; i++)`:stopped 停止标记为 true 时,需要确保所有的数据都已经刷盘,所以此处尝试 10 次强制刷盘, + + `result = CommitLog.this.mappedFileQueue.flush(0)`:**强制刷盘** + + + +*** + + + +##### 清理服务 + +CleanCommitLogService 清理过期的 CL 数据,定时任务 10 秒调用一次,**先清理 CL,再清理 CQ**,因为 CQ 依赖于 CL 的数据 + +* run():运行方法 + + ```java + public void run() + ``` + +* deleteExpiredFiles():删除过期 CL 文件 + + ```java + private void deleteExpiredFiles() + ``` + + * `long fileReservedTime`:默认 72,代表文件的保留时间 + * `boolean timeup = this.isTimeToDelete()`:当前时间是否是凌晨 4 点 + * `boolean spacefull = this.isSpaceToDelete()`:CL 或者 CQ 的目录磁盘使用率达到阈值标准 85% + * `boolean manualDelete = this.manualDeleteFileSeveralTimes > 0`:手动删除文件 + * `fileReservedTime *= 60 * 60 * 1000`:默认保留 72 小时 + * `deleteCount = DefaultMessageStore.this.commitLog.deleteExpiredFile()`:**调用 MFQ 对象的删除方法** + +CleanConsumeQueueService 清理过期的 CQ 数据 + +* run():运行方法 + + ```java + public void run() + ``` + +* deleteExpiredFiles():删除过期 CQ 文件 + + ```java + private void deleteExpiredFiles() + ``` * `int deleteLogicsFilesInterval`:清理 CQ 的时间间隔,默认 100 毫秒 * `long minOffset = DefaultMessageStore.this.commitLog.getMinOffset()`:获取 CL 文件中最小的物理偏移量 @@ -6894,7 +7584,7 @@ CleanConsumeQueueService 清理过期的 CQ 数据 ##### 获取消息 -PullMessageProcessor#processRequest 方法中调用 getMessage 用于获取消息(提示:建议学习消费者源码时再阅读) +DefaultMessageStore#getMessage 用于获取消息,在 PullMessageProcessor#processRequest 方法中被调用 (提示:建议学习消费者源码时再阅读) ```java // offset: 客户端拉消息使用位点; maxMsgNums: 32; messageFilter: 一般这里是 tagCode 过滤 @@ -7024,8 +7714,6 @@ BrokerController#start:核心启动方法 - - **** @@ -8163,9 +8851,223 @@ DeliverDelayedMessageTimerTask 是一个任务类 +**** + + + +#### 事务消息 + +##### 生产者类 + +TransactionMQProducer 类发送事务消息时使用 + +成员变量: + +* 事务回查线程池资源: + + ```java + private ExecutorService executorService; + +* 事务监听器: + + ```java + private TransactionListener transactionListener; + ``` + +核心方法: + +* start():启动方法 + + ```java + public void start() + ``` + + * `this.defaultMQProducerImpl.initTransactionEnv()`:初始化生产者实例和回查线程池资源 + * `super.start()`:启动生产者实例 + +* sendMessageInTransaction():发送事务消息 + + ```java + public TransactionSendResult sendMessageInTransaction(final Message msg, final Object arg) { + msg.setTopic(NamespaceUtil.wrapNamespace(this.getNamespace(), msg.getTopic())); + // 调用实现类的发送方法 + return this.defaultMQProducerImpl.sendMessageInTransaction(msg, null, arg); + } + ``` + + * `TransactionListener transactionListener = getCheckListener()`:获取监听器 + + * `if (null == localTransactionExecuter && null == transactionListener)`:两者都为 null 抛出异常 + + * `Validators.checkMessage(msg, this.defaultMQProducer)`:检查消息 + + * `MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true")`:**设置事务标志** + + * `sendResult = this.send(msg)`:发送消息 + + * `switch (sendResult.getSendStatus())`:**判断发送消息的结果状态** + + * `case SEND_OK`:消息发送成功 + + `msg.setTransactionId(transactionId)`:设置事务 ID 为消息的 UNIQ_KEY 属性 + + `localTransactionState = ...executeLocalTransactionBranch(msg, arg)`:**执行本地事务** + + * `case SLAVE_NOT_AVAILABLE`:其他情况都需要回滚事务 + + `localTransactionState = LocalTransactionState.ROLLBACK_MESSAGE`:**事务状态设置为回滚** + * `this.endTransaction(sendResult, ...)`:结束事务 + * `EndTransactionRequestHeader requestHeader`:构建事务结束头对象 + * `this.mQClientFactory.getMQClientAPIImpl().endTransactionOneway()`:向 Broker 发起事务结束的单向请求 + + + +*** + + + +##### 回查处理 + +ClientRemotingProcessor 用于处理到客户端的请求,创建 MQClientAPIImpl 时将该处理器注册到 Netty 中,`processRequest()` 方法根据请求的命令码,进行不同的处理,事务回查的处理命令码为 `CHECK_TRANSACTION_STATE` + +成员方法: + +* checkTransactionState():检查事务状态 + + ```java + public RemotingCommand checkTransactionState(ChannelHandlerContext ctx, RemotingCommand request) + ``` + + * `final CheckTransactionStateRequestHeader requestHeader`:解析出请求头对象 + * `final MessageExt messageExt`:从请求 body 中解析出服务器回查的事务消息 + * `String transactionId`:提取 UNIQ_KEY 字段属性值赋值给事务 ID + * `final String group`:提取生产者组名 + * `MQProducerInner producer = this...selectProducer(group)`:根据生产者组获取生产者对象 + * `String addr = RemotingHelper.parseChannelRemoteAddr()`:解析出要回查的 Broker 服务器的地址 + * `producer.checkTransactionState(addr, messageExt, requestHeader)`:生产者的事务回查 + * `Runnable request = new Runnable()`:**创建回查事务状态任务对象** + * 获取生产者的 TransactionCheckListener 和 TransactionListener,选择一个不为 null 的监听器进行事务状态回查 + * `this.processTransactionState()`:处理回查状态 + * `EndTransactionRequestHeader thisHeader`:构建 EndTransactionRequestHeader 对象 + * `DefaultMQProducerImpl...endTransactionOneway()`:向 Broker 发起结束事务单向请求,**二阶段提交** + * `this.checkExecutor.submit(request)`:提交到线程池运行 + + + +参考图:https://www.processon.com/view/link/61c8257e0e3e7474fb9dcbc0 + +参考视频:https://space.bilibili.com/457326371 + + + +*** + + + +##### 接受消息 + +SendMessageProcessor 是服务端处理客户端发送来的消息的处理器,`processRequest()` 方法处理请求 + +核心方法: + +* `asyncProcessRequest()`:处理请求 + + ```java + public CompletableFuture asyncProcessRequest(ChannelHandlerContext ctx, + RemotingCommand request) { + final SendMessageContext mqtraceContext; + switch (request.getCode()) { + // 回调消息回退 + case RequestCode.CONSUMER_SEND_MSG_BACK: + return this.asyncConsumerSendMsgBack(ctx, request); + default: + // 解析出请求头对象 + SendMessageRequestHeader requestHeader = parseRequestHeader(request); + if (requestHeader == null) { + return CompletableFuture.completedFuture(null); + } + // 创建上下文对象 + mqtraceContext = buildMsgContext(ctx, requestHeader); + // 前置处理器 + this.executeSendMessageHookBefore(ctx, request, mqtraceContext); + // 判断是否是批量消息 + if (requestHeader.isBatch()) { + return this.asyncSendBatchMessage(ctx, request, mqtraceContext, requestHeader); + } else { + return this.asyncSendMessage(ctx, request, mqtraceContext, requestHeader); + } + } + } + ``` + +* asyncSendMessage():异步处理发送消息 + + ```java + private CompletableFuture asyncSendMessage(ChannelHandlerContext ctx, RemotingCommand request, SendMessageContext mqtraceContext, SendMessageRequestHeader requestHeader) + ``` + + * `RemotingCommand response`:创建响应对象 + + * `SendMessageResponseHeader responseHeader`:获取响应头,此时为 null + + * `byte[] body = request.getBody()`:获取请求体 + + * `MessageExtBrokerInner msgInner = new MessageExtBrokerInner()`:创建 msgInner 对象,并赋值相关的属性,主题和队列 ID 都是请求头中的 + + * `String transFlag`:获取**事务属性** + + * `if (transFlag != null && Boolean.parseBoolean(transFlag))`:判断事务属性是否是 true,走事务消息的存储流程 + + * `putMessageResult = ...asyncPrepareMessage(msgInner)`:事务消息处理流程 + + ```java + public CompletableFuture asyncPutHalfMessage(MessageExtBrokerInner messageInner) { + // 调用存储模块,将修改后的 msg 存储进 Broker + return store.asyncPutMessage(parseHalfMessageInner(messageInner)); + } + ``` + + TransactionalMessageBridge#parseHalfMessageInner: + + * `MessageAccessor.putProperty(...)`:将消息的原主题和队列 ID 放入消息的属性中 + * `msgInner.setSysFlag(...)`:消息设置为非事务状态 + * `msgInner.setTopic(TransactionalMessageUtil.buildHalfTopic())`:**消息主题设置为半消息主题** + * `msgInner.setQueueId(0)`:**队列 ID 设置为 0** + + * `else`:普通消息存储 + + + +*** + + + +##### 事务提交 + +EndTransactionProcessor 类用来处理客户端发来的提交或者回滚请求 + +* processRequest():处理请求 + + ```java + public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) + ``` + * `EndTransactionRequestHeader requestHeader`:从请求中解析出 EndTransactionRequestHeader + * `result = this.brokerController...commitMessage(requestHeader)`:根据 commitLogOffset 提取出 halfMsg 消息 + * `MessageExtBrokerInner msgInner`:根据 result 克隆出一条新消息 + * `msgInner.setTopic(msgExt.getUserProperty(...))`:**设置回原主题** + * `msgInner.setQueueId(Integer.parseInt(msgExt.getUserProperty(..)))`:**设置回原队列 ID** + * `MessageAccessor.clearProperty()`:清理上面的两个属性 + * `MessageAccessor.clearProperty(msgInner, MessageConst.PROPERTY_TRANSACTION_PREPARED)`:清理事务属性 + * `RemotingCommand sendResult = sendFinalMessage(msgInner)`:调用存储模块存储至 Broker + * `this.brokerController...deletePrepareMessage(result.getPrepareMessage())`:向删除(OP)队列添加消息,消息体的数据是 halfMsg 的 queueOffset,表示半消息队列指定的 offset 的消息已被删除 + * `if (this...putOpMessage(msgExt, TransactionalMessageUtil.REMOVETAG))`:**添加一条 OP 数据** + * `MessageQueue messageQueue`:新建一个消息队列,OP 队列 + * `return addRemoveTagInTransactionOp(messageExt, messageQueue)`:添加数据 + * `Message message`:创建消息 + * `writeOp(message, messageQueue)`:写入消息 From 29d649175caf40b0ec337619219737713ce46cf4 Mon Sep 17 00:00:00 2001 From: Seazean Date: Tue, 1 Feb 2022 22:07:40 +0800 Subject: [PATCH 109/168] Update Java Note --- Frame.md | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/Frame.md b/Frame.md index cec0168..5cf9cd6 100644 --- a/Frame.md +++ b/Frame.md @@ -10314,16 +10314,3 @@ ConsumeRequest 是 ConsumeMessageOrderlyService 的内部类,是一个 Runnabl - - - -*** - - - - - -## TEST - - - From 83dba99c2261bc88b2a549fe9f90a58976b516f6 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 12 Feb 2022 14:23:24 +0800 Subject: [PATCH 110/168] Update Java Note --- Tool.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Tool.md b/Tool.md index d81b3ce..77b4fef 100644 --- a/Tool.md +++ b/Tool.md @@ -2210,7 +2210,7 @@ pstree -A #查看所有进程树 -### 进程ID +### 进程 ID 进程号: @@ -2218,11 +2218,14 @@ pstree -A #查看所有进程树 * 进程号为 1 是 init 进程,是一个守护进程,在自举过程结束时由内核调用,init 进程绝不会终止,是一个普通的用户进程,但是它以超级用户特权运行 -父进程 ID 为 0 的进程通常是内核进程,它们作为系统自举过程的一部分而启动,init 进程是个例外,它的父进程是 0,但它是用户进程 +父进程 ID 为 0 的进程通常是内核进程,作为系统**自举过程**的一部分而启动,init 进程是个例外,它的父进程是 0,但它是用户进程 -主存 = RAM + BIOS 部分的 ROM +* 主存 = RAM + BIOS 部分的 ROM +* DISK:存放 OS 和 Bootloader +* BIOS:基于 I/O 处理系统 +* Bootloader:加载 OS,将 OS 放入内存 -自举程序存储在内存中 ROM(BIOS 芯片),用来加载操作系统。CPU 的程序计数器指向 ROM 中自举程序第一条指令,当计算机**通电**,CPU 开始读取并执行自举程序,将操作系统(不是全部,只是启动计算机的那部分程序)装入 RAM 中,这个过程是自举过程。装入完成后 CPU 的程序计数器就被设置为 RAM 中操作系统的第一条指令所对应的位置,接下来 CPU 将开始执行操作系统的指令 +自举程序存储在内存中 ROM(BIOS 芯片),用来加载操作系统。CPU 的程序计数器指向 ROM 中自举程序第一条指令,当计算机**通电**,CPU 开始读取并执行自举程序,将操作系统(不是全部,只是启动计算机的那部分程序)装入 RAM 中,这个过程是自举过程。装入完成后 CPU 的程序计数器就被设置为 RAM 中操作系统的**第一条指令**所对应的位置,接下来 CPU 将开始执行操作系统的指令 存储在 ROM 中保留很小的自举装入程序,完整功能的自举程序保存在磁盘的启动块上,启动块位于磁盘的固定位,拥有启动分区的磁盘称为启动磁盘或系统磁盘(C盘) @@ -2277,7 +2280,7 @@ pstree -A #查看所有进程树 - 得到 SIGCHLD 信号 - waitpid() 或者 wait() 调用会返回 -子进程发送的 SIGCHLD 信号包含了子进程的信息,比如进程 ID、进程状态、进程使用 CPU 的时间等;在子进程退出时,它的进程描述符不会立即释放,这是为了让父进程得到子进程信息,父进程通过 wait() 和 waitpid() 来获得一个已经退出的子进程的信息 +子进程发送的 SIGCHLD 信号包含了子进程的信息,比如进程 ID、进程状态、进程使用 CPU 的时间等;在子进程退出时进程描述符不会立即释放,父进程通过 wait() 和 waitpid() 来获得一个已经退出的子进程的信息,释放子进程的 PCB From cac33c723439189b778f4a61e81ecce5491e116f Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 13 Feb 2022 18:27:23 +0800 Subject: [PATCH 111/168] Update Java Note --- DB.md | 11 ++++++----- Java.md | 4 ++-- Prog.md | 8 ++++---- Tool.md | 21 ++++++++------------- 4 files changed, 20 insertions(+), 24 deletions(-) diff --git a/DB.md b/DB.md index 61b9c13..f51e5c2 100644 --- a/DB.md +++ b/DB.md @@ -5644,7 +5644,8 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 - 查看事务提交方式 ```mysql - SELECT @@AUTOCOMMIT; -- 1 代表自动提交 0 代表手动提交 + SELECT @@AUTOCOMMIT; -- 会话,1 代表自动提交 0 代表手动提交 + SELECT @@GLOBAL.AUTOCOMMIT; -- 系统 ``` - 修改事务提交方式 @@ -5732,8 +5733,8 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 * 查询数据库隔离级别 ```mysql - SELECT @@TX_ISOLATION; - SHOW VARIABLES LIKE 'tx_isolation'; + SELECT @@TX_ISOLATION; -- 会话 + SELECT @@GLOBAL.TX_ISOLATION; -- 系统 ``` * 修改数据库隔离级别 @@ -5758,7 +5759,7 @@ InnoDB 存储引擎支持事务,所以加锁分析是基于该存储引擎 MySQL 做了优化,在 Server 层过滤条件时发现不满足的记录会调用 unlock_row 方法释放该记录的行锁,保证最后只有满足条件的记录加锁,但是扫表过程中每条记录的**加锁操作不能省略**。所以对数据量很大的表做批量修改时,如果无法使用相应的索引,需要在Server 过滤数据时就会特别慢,出现虽然没有修改某些行的数据,但是还是被锁住了的现象,这种情况同样适用于 RR -* Repeatable Read 级别,增删改操作会加写锁,读操作不加锁。因为读写锁不兼容,加了写锁后其他事务就无法修改数据,影响了并发性能,为了保证隔离性和并发性,MySQL 通过 MVCC 解决了读写冲突。RR 级别下的锁有很多种,锁机制章节详解 +* Repeatable Read 级别,增删改操作会加写锁,读操作不加锁。因为读写锁不兼容,**加了读锁后其他事务就无法修改数据**,影响了并发性能,为了保证隔离性和并发性,MySQL 通过 MVCC 解决了读写冲突。RR 级别下的锁有很多种,锁机制章节详解 * Serializable 级别,读加共享锁,写加排他锁,读写互斥,使用的悲观锁的理论,实现简单,数据更加安全,但是并发能力非常差 @@ -6660,7 +6661,7 @@ InnoDB 与 MyISAM 的最大不同有两点:一是支持事务;二是采用 - 共享锁 (S):又称为读锁,简称 S 锁,多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改 - 排他锁 (X):又称为写锁,简称 X 锁,不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,只有获取排他锁的事务是可以对数据读取和修改 -RR 隔离界别下,对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会**自动给涉及数据集加排他锁**(行锁),在 commit 的时候会自动释放(在事务中加的锁,会**在事务中止或提交时自动释放**);对于普通 SELECT 语句,不会加任何锁(只是针对 InnoDB 层来说的,因为在 Server 层会加 MDL 读锁),通过 MVCC 防止冲突 +RR 隔离界别下,对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会**自动给涉及数据集加排他锁**(行锁),在 commit 的时候会自动释放(在事务中加的锁,会**在事务中止或提交时自动释放**);对于普通 SELECT 语句,不会加任何锁(只是针对 InnoDB 层来说的,因为在 Server 层会**加 MDL 读锁**),通过 MVCC 防止并发冲突 锁的兼容性: diff --git a/Java.md b/Java.md index b932420..91ff8a8 100644 --- a/Java.md +++ b/Java.md @@ -12677,7 +12677,7 @@ Java 语言:跨平台的语言(write once ,run anywhere) 字节码是一种二进制的类文件,是编译之后供虚拟机解释执行的二进制字节码文件,**一个 class 文件对应一个 public 类型的类或接口** -字节码内容是 **JVM 的字节码指令**,不是机器码,C、C++ 经由编译器直接生成机器码,所以 C 执行效率比 Java 高 +字节码内容是 **JVM 的字节码指令**,不是机器码,C、C++ 经由编译器直接生成机器码,所以执行效率比 Java 高 JVM 官方文档:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html @@ -13860,7 +13860,7 @@ public static int invoke(Object... args) { - 静态语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息 -- **Java 是静态类型语言**(尽管 lambda 表达式为其增加了动态特性),js,python是动态类型语言 +- **Java 是静态类型语言**(尽管 Lambda 表达式为其增加了动态特性),JS,Python 是动态类型语言 ```java String s = "abc"; //Java diff --git a/Prog.md b/Prog.md index dc6f5e3..c09c693 100644 --- a/Prog.md +++ b/Prog.md @@ -50,8 +50,8 @@ * 信号量:信号量是一个计数器,用于多进程对共享数据的访问,解决同步相关的问题并避免竞争条件 * 共享存储:多个进程可以访问同一块内存空间,需要使用信号量用来同步对共享存储的访问 - * 管道通信:管道是用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,pipe 文件 - * 匿名管道(Pipes):用于具有亲缘关系的父子进程间或者兄弟进程之间的通信,只支持**半双工通信** + * 管道通信:管道是用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件 pipe 文件,该文件同一时间只允许一个进程访问,所以只支持**半双工通信** + * 匿名管道(Pipes):用于具有亲缘关系的父子进程间或者兄弟进程之间的通信 * 命名管道(Names Pipes):以磁盘文件的方式存在,可以实现本机任意两个进程通信,遵循 FIFO * 消息队列:内核中存储消息的链表,由消息队列标识符标识,能在不同进程之间提供**全双工通信**,对比管道: * 匿名管道存在于内存中的文件;命名管道存在于实际的磁盘介质或者文件系统;消息队列存放在内核中,只有在内核重启(操作系统重启)或者显示地删除一个消息队列时,该消息队列才被真正删除 @@ -59,9 +59,9 @@ 不同计算机之间的**进程通信**,需要通过网络,并遵守共同的协议,例如 HTTP - * 套接字:与其它通信机制不同的是,它可用于不同机器间的互相通信 + * 套接字:与其它通信机制不同的是,可用于不同机器间的互相通信 -* 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量 +* 线程通信相对简单,因为线程之间共享进程内的内存,一个例子是多个线程可以访问同一个共享变量 **Java 中的通信机制**:volatile、等待/通知机制、join 方式、InheritableThreadLocal、MappedByteBuffer diff --git a/Tool.md b/Tool.md index 77b4fef..bf5c965 100644 --- a/Tool.md +++ b/Tool.md @@ -1614,19 +1614,14 @@ grep [-abcEFGhHilLnqrsvVwxy][-A<显示列数>][-B<显示列数>][-C<显示列数 * -v:显示不包含匹配文本的所有行 * --color=auto :可以将找到的关键词部分加上颜色的显示 -**管道符 |**:表示将前一个命令处理的结果传递给后面的命令处理。 +**管道符 |**:表示将前一个命令处理的结果传递给后面的命令处理 -`grep aaaa Filename `:显示存在关键字 aaaa 的行 - -`grep -n aaaa Filename`:显示存在关键字 aaaa 的行,且显示行号 - -`grep -i aaaa Filename`:忽略大小写,显示存在关键字 aaaa 的行 - -`grep -v aaaa Filename`:显示存在关键字aaaa的所有行 - -`ps -ef | grep sshd`:查找包含 sshd 进程的进程信息 - -` ps -ef | grep -c sshd`:查找 sshd 相关的进程个数 +* `grep aaaa Filename `:显示存在关键字 aaaa 的行 +* `grep -n aaaa Filename`:显示存在关键字 aaaa 的行,且显示行号 +* `grep -i aaaa Filename`:忽略大小写,显示存在关键字 aaaa 的行 +* `grep -v aaaa Filename`:显示存在关键字aaaa的所有行 +* `ps -ef | grep sshd`:查找包含 sshd 进程的进程信息 +* ` ps -ef | grep -c sshd`:查找 sshd 相关的进程个数 @@ -2225,7 +2220,7 @@ pstree -A #查看所有进程树 * BIOS:基于 I/O 处理系统 * Bootloader:加载 OS,将 OS 放入内存 -自举程序存储在内存中 ROM(BIOS 芯片),用来加载操作系统。CPU 的程序计数器指向 ROM 中自举程序第一条指令,当计算机**通电**,CPU 开始读取并执行自举程序,将操作系统(不是全部,只是启动计算机的那部分程序)装入 RAM 中,这个过程是自举过程。装入完成后 CPU 的程序计数器就被设置为 RAM 中操作系统的**第一条指令**所对应的位置,接下来 CPU 将开始执行操作系统的指令 +自举程序存储在内存中 ROM(BIOS 芯片),**用来加载操作系统**。CPU 的程序计数器指向 ROM 中自举程序第一条指令,当计算机**通电**,CPU 开始读取并执行自举程序,将操作系统(不是全部,只是启动计算机的那部分程序)装入 RAM 中,这个过程是自举过程。装入完成后 CPU 的程序计数器就被设置为 RAM 中操作系统的**第一条指令**所对应的位置,接下来 CPU 将开始执行操作系统的指令 存储在 ROM 中保留很小的自举装入程序,完整功能的自举程序保存在磁盘的启动块上,启动块位于磁盘的固定位,拥有启动分区的磁盘称为启动磁盘或系统磁盘(C盘) From e8ee01e5d103823e10507f8aa1f91aec42c1f10d Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 20 Feb 2022 22:35:52 +0800 Subject: [PATCH 112/168] Update Java Note --- DB.md | 8 +- Java.md | 468 ++++++++++++++++++++++++++------------------------------ Prog.md | 2 +- Web.md | 4 +- 4 files changed, 222 insertions(+), 260 deletions(-) diff --git a/DB.md b/DB.md index f51e5c2..bdd5d85 100644 --- a/DB.md +++ b/DB.md @@ -5757,7 +5757,7 @@ InnoDB 存储引擎支持事务,所以加锁分析是基于该存储引擎 * Read Committed 级别,增删改操作会加写锁(行锁),读操作不加锁 - MySQL 做了优化,在 Server 层过滤条件时发现不满足的记录会调用 unlock_row 方法释放该记录的行锁,保证最后只有满足条件的记录加锁,但是扫表过程中每条记录的**加锁操作不能省略**。所以对数据量很大的表做批量修改时,如果无法使用相应的索引,需要在Server 过滤数据时就会特别慢,出现虽然没有修改某些行的数据,但是还是被锁住了的现象,这种情况同样适用于 RR + MySQL 做了优化,在 Server 层过滤条件时发现不满足的记录会调用 unlock_row 方法释放该记录的行锁,保证最后只有满足条件的记录加锁,但是扫表过程中每条记录的**加锁操作不能省略**。所以对数据量很大的表做批量修改时,如果无法使用相应的索引(全表扫描),在Server 过滤数据时就会特别慢,出现虽然没有修改某些行的数据,但是还是被锁住了的现象,这种情况同样适用于 RR * Repeatable Read 级别,增删改操作会加写锁,读操作不加锁。因为读写锁不兼容,**加了读锁后其他事务就无法修改数据**,影响了并发性能,为了保证隔离性和并发性,MySQL 通过 MVCC 解决了读写冲突。RR 级别下的锁有很多种,锁机制章节详解 @@ -6135,7 +6135,7 @@ RC、RR 级别下的 InnoDB 快照读区别 - 快照读:通过 MVCC 来进行控制的,在可重复读隔离级别下,普通查询是快照读,是不会看到别的事务插入的数据的,但是**并不能完全避免幻读** - 场景:RR 级别,T1 事务开启,创建 Read View,此时 T2 去 INSERT 新的一行然后提交,然后 T1去 UPDATE 该行会发现更新成功,因为 **Read View 并不能阻止事务去更新数据**,并且把这条新记录的 trx_id 给变为当前的事务 id,所以对当前事务就是可见的 + 场景:RR 级别,T1 事务开启,创建 Read View,此时 T2 去 INSERT 新的一行然后提交,然后 T1 去 UPDATE 该行会发现更新成功,因为 **Read View 并不能阻止事务去更新数据**,并且把这条新记录的 trx_id 变为当前的事务 id,所以对当前事务就是可见的 - 当前读:通过 next-key 锁(行锁 + 间隙锁)来解决问题 @@ -6790,9 +6790,9 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 加锁的基本单位是 next-key lock,该锁是行锁和 gap lock 的组合,可以保护当前记录和前面的间隙 * 加锁遵循左开右闭原则 -* 假设有 10、11、13,那么可能的间隙锁包括:(负无穷,10]、(10,11]、(11,13]、(13,20,正无穷),锁住索引 11 会对 (10,11] 加锁 +* 假设有 10、11、13,那么可能的间隙锁包括:(负无穷,10]、(10,11]、(11,13]、(13,正无穷),锁住索引 11 会对 (10,11] 加锁 -间隙锁优点:RR 级别下间隙锁可以解决事务的一部分的**幻读问题**,通过对间隙加锁,可以防止读取过程中数据条目发生变化 +间隙锁优点:RR 级别下间隙锁可以解决事务的一部分的**幻读问题**,通过对间隙加锁,可以防止读取过程中数据条目发生变化。一部分的意思是不会对全部间隙加锁,只能加锁一部分的间隙。 间隙锁危害:当锁定一个范围的键值后,即使某些不存在的键值也会被无辜的锁定,造成在锁定的时候无法插入锁定键值范围内的任何数据,在某些场景下这可能会对性能造成很大的危害 diff --git a/Java.md b/Java.md index 91ff8a8..be48f40 100644 --- a/Java.md +++ b/Java.md @@ -104,7 +104,7 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, - char 类型是一个单一的 16 位两个字节的 Unicode 字符 - 最小值是 **`\u0000`**(即为 0) - 最大值是 **`\uffff`**(即为 65535) -- char 数据类型可以**存储任何字符** +- char 数据类型可以存储任何字符 - 例子:`char c = 'A'`,`char c = '张'` 上下转型 @@ -187,8 +187,8 @@ Java 为包装类做了一些特殊功能,具体来看特殊功能主要有: * 把字符串类型的数值转换成对应的基本数据类型的值(**重要**) - 1. Xxx.parseXxx("字符串类型的数值") → Integer.parseInt(numStr) - 2. Xxx.valueOf("字符串类型的数值") → Integer.valueOf(numStr) (推荐使用) + 1. Xxx.parseXxx("字符串类型的数值") → `Integer.parseInt(numStr)` + 2. Xxx.valueOf("字符串类型的数值") → `Integer.valueOf(numStr)` (推荐使用) ```java public class PackageClass02 { @@ -219,7 +219,35 @@ Java 为包装类做了一些特殊功能,具体来看特殊功能主要有: } ``` - + + + +*** + + + +##### 类型对比 + +* 有了基本数据类型,为什么还要引用数据类型? + + > 引用数据类型封装了数据和处理该数据的方法,比如 Integer.parseInt(String) 就是将 String 字符类型数据转换为 Integer 整型 + > + > Java 中大部分类和方法都是针对引用数据类型,包括泛型和集合 + +* 引用数据类型那么好,为什么还用基本数据类型? + + > 引用类型的对象要多储存对象头,对基本数据类型来说空间浪费率太高。逻辑上来讲,Java 只有包装类就够了,为了运行速度,需要用到基本数据类型;优先考虑运行效率的问题,所以二者同时存在是合乎情理的 + +* Java 集合不能存放基本数据类型,只存放对象的引用? + + > 不能放基本数据类型是因为不是 Object 的子类。泛型思想,如果不用泛型要写很多参数类型不同的但功能相同的函数(方法重载) + +* == + + > == 比较基本数据类型:比较的是具体的值 + > == 比较引用数据类型:比较的是对象地址值 + + *** @@ -242,8 +270,8 @@ public class PackegeClass { Integer c = 100 ; int c1 = c ; // 自动拆箱 - Integer it = Integer.valueOf(12); // 手工装箱! - // Integer it1 = new Integer(12); // 手工装箱! + Integer it = Integer.valueOf(12); // 手工装箱! + // Integer it1 = new Integer(12); // 手工装箱! Integer it2 = 12; Integer it3 = 111 ; @@ -257,6 +285,7 @@ public class PackegeClass { ```java public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) + // 【缓存池】,本质上是一个数组 return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); } @@ -304,10 +333,10 @@ valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中 - Integer values between -128 and 127 - Character in the range \u0000 to \u007F (0 and 127) -在 jdk 1.8 所有的数值类缓冲池中,**Integer 的缓存池 IntegerCache 很特殊,这个缓冲池的下界是 -128,上界默认是 127**,但是上界是可调的,在启动 jvm 的时候,通过 AutoBoxCacheMax= 来指定这个缓冲池的大小,该选项在 JVM 初始化的时候会设定一个名为 java.lang.IntegerCache.high 系统属性,然后 IntegerCache 初始化的时候就会读取该系统属性来决定上界 +在 jdk 1.8 所有的数值类缓冲池中,**Integer 的缓存池 IntegerCache 很特殊,这个缓冲池的下界是 -128,上界默认是 127**,但是上界是可调的,在启动 JVM 时通过 `AutoBoxCacheMax=` 来指定这个缓冲池的大小,该选项在 JVM 初始化的时候会设定一个名为 java.lang.IntegerCache.high 系统属性,然后 IntegerCache 初始化的时候就会读取该系统属性来决定上界 ```java -Integer x = 100; //自动装箱,底层调用 Integer.valueOf(1) +Integer x = 100; // 自动装箱,底层调用 Integer.valueOf(1) Integer y = 100; System.out.println(x == y); // true @@ -335,8 +364,8 @@ System.out.println(x == y); // true,因为 y 会调用 intValue 自动拆箱 一般使用 `sc.nextInt()` 或者 `sc.nextLine()` 接受整型和字符串,然后转成需要的数据类型 -Scanner:`BufferedReader br = new BufferedReader(new InputStreamReader(System.in))` -print:`PrintStream.write()` +* Scanner:`BufferedReader br = new BufferedReader(new InputStreamReader(System.in))` +* print:`PrintStream.write()` > 使用引用数据类型的API @@ -351,31 +380,6 @@ public static void main(String[] args) { -*** - - - -#### 面试题 - -* 有了基本数据类型,为什么还要引用数据类型? - - > 引用数据类型封装了数据和处理该数据的方法,比如 Integer.parseInt(String) 就是将 String 字符类型数据转换为 Integer 整型 - > - > Java 中大部分类和方法都是针对引用数据类型,包括泛型和集合 - -* 引用数据类型那么好,为什么还用基本数据类型? - - > 引用类型的对象要多储存对象头,对基本数据类型来说空间浪费率太高。逻辑上来讲,Java 只有包装类就够了,为了运行速度,需要用到基本数据类型;优先考虑运行效率的问题,所以二者同时存在是合乎情理的 - -* Java 集合不能存放基本数据类型,只存放对象的引用? - - > 不能放基本数据类型是因为不是 Object 的子类。泛型思想,如果不用泛型要写很多参数类型不同的但功能相同的函数(方法重载) - -* == - - > == 比较基本数据类型:比较的是具体的值 - > == 比较引用数据类型:比较的是对象地址值 - **** @@ -386,12 +390,12 @@ public static void main(String[] args) { #### 初始化 -数组就是存储数据长度固定的容器,存储多个数据的**数据类型要一致**,数组也是一个对象 +数组就是存储数据长度固定的容器,存储多个数据的数据类型要一致,**数组也是一个对象** 创建数组: -* 数据类型[] 数组名:`int[] arr;` (常用) -* 数据类型 数组名[]:`int arr[];` +* 数据类型[] 数组名:`int[] arr` (常用) +* 数据类型 数组名[]:`int arr[]` 静态初始化: @@ -406,9 +410,9 @@ public static void main(String[] args) { #### 元素访问 -* **索引**:每一个存储到数组的元素,都会自动的拥有一个编号,从 **0** 开始。这个自动编号称为数组索引(index),可以通过数组的索引访问到数组中的元素。 +* **索引**:每一个存储到数组的元素,都会自动的拥有一个编号,从 **0** 开始。这个自动编号称为数组索引(index),可以通过数组的索引访问到数组中的元素。 -* **访问格式**:数组名[索引] `arr[0]` +* **访问格式**:数组名[索引],`arr[0]` * **赋值:**`arr[0] = 10` @@ -419,19 +423,19 @@ public static void main(String[] args) { #### 内存分配 -内存是计算机中的重要原件,临时存储区域,作用是运行程序。我们编写的程序是存放在硬盘中的,在硬盘中的程序是不会运行的。必须放进内存中才能运行,运行完毕后会清空内存。 Java虚拟机要运行程序,必须要对内存进行空间的分配和管理。 +内存是计算机中的重要器件,临时存储区域,作用是运行程序。我们编写的程序是存放在硬盘中,在硬盘中的程序是不会运行的,必须放进内存中才能运行,运行完毕后会清空内存。 Java 虚拟机要运行程序,必须要对内存进行空间的分配和管理。 -| 区域名称 | 作用 | -| ---------- | -------------------------------------------------------- | -| 寄存器 | 给CPU使用,和我们开发无关 | -| 本地方法栈 | JVM在使用操作系统功能的时候使用,和我们开发无关 | -| 方法区 | 存储可以运行的class文件 | -| 堆内存 | 存储对象或者数组,new来创建的,都存储在堆内存 | -| 方法栈 | 方法运行时使用的内存,比如main方法运行,进入方法栈中执行 | +| 区域名称 | 作用 | +| ---------- | ---------------------------------------------------------- | +| 寄存器 | 给 CPU 使用 | +| 本地方法栈 | JVM 在使用操作系统功能的时候使用 | +| 方法区 | 存储可以运行的 class 文件 | +| 堆内存 | 存储对象或者数组,new 来创建的,都存储在堆内存 | +| 方法栈 | 方法运行时使用的内存,比如 main 方法运行,进入方法栈中执行 | -**内存分配图**: +**内存分配图**:Java 内存分配 -* Java内存分配-一个数组内存图 +* 一个数组内存图 ![](https://gitee.com/seazean/images/raw/master/Java/数组内存分配-一个数组内存图.png) @@ -464,7 +468,7 @@ public static void main(String[] args) { } ``` - arr = null,表示变量arr将不再保存数组的内存地址,也就不允许再操作数组,因此运行的时候会抛出空指针异常。在开发中,空指针异常是不能出现的,一旦出现了,就必须要修改我们编写的代码。 + arr = null,表示变量 arr 将不再保存数组的内存地址,也就不允许再操作数组,因此运行的时候会抛出空指针异常。在开发中,空指针异常是不能出现的,一旦出现了,就必须要修改编写的代码。 解决方案:给数组一个真正的堆内存空间引用即可! @@ -482,14 +486,14 @@ public static void main(String[] args) { * 动态初始化: - 数据类型[][] 变量名 = new 数据类型[m] [n] : `int[][] arr = new int[3][3];` + 数据类型[][] 变量名 = new 数据类型[m] [n] : `int[][] arr = new int[3][3]` * m 表示这个二维数组,可以存放多少个一维数组,行 * n 表示每一个一维数组,可以存放多少个元素,列 * 静态初始化 * 数据类型[][] 变量名 = new 数据类型[][]{ {元素1, 元素2...} , {元素1, 元素2...} - * 数据类型[][] 变量名 = { {元素1, 元素2...} , {元素1, 元素2...} ...} - * `int[][] arr = {{11,22,33}, {44,55,66}};` + * 数据类型[][] 变量名 = {{元素1, 元素2...}, {元素1, 元素2...}...} + * `int[][] arr = {{11,22,33}, {44,55,66}}` 遍历: @@ -525,13 +529,13 @@ public class Test1 { ### 运算 -* i++ 与++i 的区别? +* i++ 与 ++i 的区别? i++ 表示先将 i 放在表达式中运算,然后再加 1 ++i 表示先将 i 加 1,然后再放在表达式中运算 * || 和 |,&& 和& 的区别,逻辑运算符 - **&和| 称为布尔运算符,位运算符。&&和|| 称为条件布尔运算符,也叫短路运算符**。 + **& 和| 称为布尔运算符,位运算符。&& 和 || 称为条件布尔运算符,也叫短路运算符**。 如果 && 运算符的第一个操作数是 false,就不需要考虑第二个操作数的值了,因为无论第二个操作数的值是什么,其结果都是 false;同样,如果第一个操作数是 true,|| 运算符就返回 true,无需考虑第二个操作数的值;但 & 和 | 却不是这样,它们总是要计算两个操作数。为了提高性能,**尽可能使用 && 和 || 运算符** @@ -570,14 +574,14 @@ public class Test1 { ``` * 负数: - 原码:最高位为1,其余位置和正数相同 + 原码:最高位为 1,其余位置和正数相同 反码:保证符号位不变,其余位置取反 补码:保证符号位不变,其余位置取反加 1,即反码 +1 ```java - -100原码: 10000000 00000000 00000000 01100100 //32位 - -100反码: 11111111 11111111 11111111 10011011 - -100补码: 11111111 11111111 11111111 10011100 + -100 原码: 10000000 00000000 00000000 01100100 //32位 + -100 反码: 11111111 11111111 11111111 10011011 + -100 补码: 11111111 11111111 11111111 10011100 ``` 补码 → 原码:符号位不变,其余位置取反加 1 @@ -621,13 +625,11 @@ public class Test1 { #### 可变参数 -可变参数用在形参中可以接收多个数据。 +可变参数用在形参中可以接收多个数据,在方法内部**本质上就是一个数组** -可变参数的格式:数据类型... 参数名称 +格式:数据类型... 参数名称 -可变参数的作用:传输参数非常灵活,方便。可以不传输参数、传输一个参数、或者传输一个数组。 - -可变参数在方法内部本质上就是一个数组。 +作用:传输参数非常灵活,方便,可以不传输参数、传输一个参数、或者传输一个数组。 可变参数的注意事项: @@ -667,7 +669,7 @@ public static void sum(int... nums){ 在方法内部定义的叫局部变量,局部变量不能加 static,包括 protected、private、public 这些也不能加 -原因:局部变量是保存在栈中的,而静态变量保存于方法区(JDK8 在堆中),局部变量出了方法就被栈回收了,而静态变量不会,所以在局部变量前不能加 static 关键字,静态变量是定义在类中,又叫类变量 +原因:局部变量是保存在栈中的,而静态变量保存于方法区(JDK8 在堆中),局部变量出了方法就被栈回收了,而静态变量不会,所以**在局部变量前不能加 static 关键字**,静态变量是定义在类中,又叫类变量 @@ -677,7 +679,7 @@ public static void sum(int... nums){ #### 定义调用 -定义格式 +定义格式: ```java public static 返回值类型 方法名(参数) { @@ -686,11 +688,10 @@ public static 返回值类型 方法名(参数) { } ``` -调用格式 +调用格式: ```java -数据类型 变量名 = 方法名 ( 参数 ) ; -//注意:方法的返回值通常会使用变量接收,否则该返回值将无意义 +数据类型 变量名 = 方法名 (参数) ; ``` * 方法名:调用方法时候使用的标识 @@ -700,10 +701,10 @@ public static 返回值类型 方法名(参数) { 如果方法操作完毕 -* void 类型的方法,直接调用即可,而且方法体中一般不写return +* void 类型的方法,直接调用即可,而且方法体中一般不写 return * 非 void 类型的方法,推荐用变量接收调用 -原理:每个方法在被调用执行的时候,都会进入栈内存,并且拥有自己独立的内存空间,方法内部代码调用完毕之后,会从栈内存中弹栈消失。 +原理:每个方法在被调用执行的时候,都会进入栈内存,并且拥有自己独立的内存空间,方法内部代码调用完毕之后,会从栈内存中弹栈消失 @@ -785,9 +786,9 @@ public class MethodDemo { 重载的方法在编译过程中即可完成识别,方法调用时 Java 编译器会根据所传入参数的声明类型(注意与实际类型区分)来选取重载方法。选取的过程共分为三个阶段: -* 在不考虑对基本类型自动装拆箱 (auto-boxing,auto-unboxing),以及可变长参数的情况下选取重载方法 -* 如果第 1 个阶段中没有找到适配的方法,那么在允许自动装拆箱,但不允许可变长参数的情况下选取重载方法 -* 如果第 2 个阶段中没有找到适配的方法,那么在允许自动装拆箱以及可变长参数的情况下选取重载方法 +* 一阶段:在不考虑对基本类型自动装拆箱 (auto-boxing,auto-unboxing),以及可变长参数的情况下选取重载方法 +* 二阶段:如果第一阶段中没有找到适配的方法,那么在允许自动装拆箱,但不允许可变长参数的情况下选取重载方法 +* 三阶段:如果第二阶段中没有找到适配的方法,那么在允许自动装拆箱以及可变长参数的情况下选取重载方法 如果 Java 编译器在同一个阶段中找到了多个适配的方法,那么会选择一个最为贴切的,而决定贴切程度的一个关键就是形式参数类型的继承关系,**一般会选择形参为参数类型的子类的方法,因为子类时更具体的实现**: @@ -796,7 +797,7 @@ public class MethodDemo { void invoke(Object obj, Object... args) { ... } void invoke(String s, Object obj, Object... args) { ... } - invoke(null, 1); // 调用第二个invoke方法,选取的第二阶段 + invoke(null, 1); // 调用第二个invoke方法,选取的第二阶段 invoke(null, 1, 2); // 调用第二个invoke方法,匹配第一个和第二个,但String是Object的子类 invoke(null, new Object[]{1}); // 只有手动绕开可变长参数的语法糖,才能调用第一个invoke方法 @@ -932,7 +933,7 @@ Java 的参数是以**值传递**的形式传入方法中 // 获取索引 Season s = Season.SPRING; System.out.println(s); //SPRING - System.out.println(s.ordinal()); // 0,代表索引,summer 就是 1 + System.out.println(s.ordinal()); // 0,该值代表索引,summer 就是 1 s.s.doSomething(); // 获取全部枚举 Season[] ss = Season.values(); @@ -988,15 +989,15 @@ Debug 是供程序员使用的程序调试工具,它可以用于查看程序 ### 概述 -**Java是一种面向对象的高级编程语言。** +**Java 是一种面向对象的高级编程语言。** **三大特征:封装,继承,多态** 面向对象最重要的两个概念:类和对象 -* 类:相同事物共同特征的描述。类只是学术上的一个概念并非真实存在的,只能描述一类事物 -* 对象:是真实存在的实例, 实例==对象,**对象是类的实例化** -* 结论:有了类和对象就可以描述万千世界所有的事物。 必须先有类才能有对象 +* 类:相同事物共同特征的描述,类只是学术上的一个概念并非真实存在的,只能描述一类事物 +* 对象:是真实存在的实例, 实例 == 对象,**对象是类的实例化** +* 结论:有了类和对象就可以描述万千世界所有的事物,必须先有类才能有对象 @@ -1017,7 +1018,7 @@ Debug 是供程序员使用的程序调试工具,它可以用于查看程序 1. 类名的首字母建议大写,满足驼峰模式,比如 StudentNameCode 2. 一个 Java 代码中可以定义多个类,按照规范一个 Java 文件一个类 -3. 一个 Java 代码文件中,只能有一个类是 public 修饰,**public修饰的类名必须成为当前Java代码的文件名称** +3. 一个 Java 代码文件中,只能有一个类是 public 修饰,**public 修饰的类名必须成为当前 Java 代码的文件名称** ```java 类中的成分:有且仅有五大成分 @@ -1086,6 +1087,7 @@ public class ClassDemo { ### 封装 封装的哲学思维:合理隐藏,合理暴露 + 封装最初的目的:提高代码的安全性和复用性,组件化 封装的步骤: @@ -1120,9 +1122,7 @@ this 关键字的作用: #### 基本介绍 -Java 是通过成员变量是否有 static 修饰来区分是类的还是属于对象的。 - -static 静态修饰的成员(方法和成员变量)属于类本身的。 +Java 是通过成员变量是否有 static 修饰来区分是类的还是属于对象的 按照有无 static 修饰,成员变量和方法可以分为: @@ -1140,7 +1140,7 @@ static 静态修饰的成员(方法和成员变量)属于类本身的。 -#### static用法 +#### static 用法 成员变量的访问语法: @@ -1175,8 +1175,8 @@ static 静态修饰的成员(方法和成员变量)属于类本身的。 inAddr(); // b.对象.实例方法 // Student.eat(); // 报错了! - Student zbj = new Student(); - zbj.eat(); + Student sea = new Student(); + sea.eat(); } } ``` @@ -1199,14 +1199,14 @@ static 静态修饰的成员(方法和成员变量)属于类本身的。 访问问题: -* 实例方法是否可以直接访问实例成员变量?可以的,因为它们都属于对象 -* 实例方法是否可以直接访问静态成员变量?可以的,静态成员变量可以被共享访问 -* 实例方法是否可以直接访问实例方法? 可以的,实例方法和实例方法都属于对象 -* 实例方法是否可以直接访问静态方法?可以的,静态方法可以被共享访问 -* 静态方法是否可以直接访问实例变量? 不可以的,实例变量必须用对象访问!! -* 静态方法是否可以直接访问静态变量? 可以的,静态成员变量可以被共享访问。 -* 静态方法是否可以直接访问实例方法? 不可以的,实例方法必须用对象访问!! -* 静态方法是否可以直接访问静态方法?可以的,静态方法可以被共享访问!! +* 实例方法是否可以直接访问实例成员变量?可以,因为它们都属于对象 +* 实例方法是否可以直接访问静态成员变量?可以,静态成员变量可以被共享访问 +* 实例方法是否可以直接访问实例方法? 可以,实例方法和实例方法都属于对象 +* 实例方法是否可以直接访问静态方法?可以,静态方法可以被共享访问 +* 静态方法是否可以直接访问实例变量? 不可以,实例变量必须用对象访问!! +* 静态方法是否可以直接访问静态变量? 可以,静态成员变量可以被共享访问。 +* 静态方法是否可以直接访问实例方法? 不可以,实例方法必须用对象访问!! +* 静态方法是否可以直接访问静态方法?可以,静态方法可以被共享访问!! @@ -1220,8 +1220,8 @@ static 静态修饰的成员(方法和成员变量)属于类本身的。 继承是 Java 中一般到特殊的关系,是一种子类到父类的关系 -* 被继承的类称为:父类/超类。 -* 继承父类的类称为:子类。 +* 被继承的类称为:父类/超类 +* 继承父类的类称为:子类 继承的作用: @@ -1235,7 +1235,7 @@ static 静态修饰的成员(方法和成员变量)属于类本身的。 2. **单继承**:一个类只能继承一个直接父类 3. 多层继承:一个类可以间接继承多个父类(家谱) 4. 一个类可以有多个子类 -5. 一个类要么默认继承了 Object 类,要么间接继承了 Object 类,Object 类是 Java 中的祖宗类 +5. 一个类要么默认继承了 Object 类,要么间接继承了 Object 类,**Object 类是 Java 中的祖宗类** 继承的格式: @@ -1356,7 +1356,7 @@ class Animal{ -#### 面试问题 +#### 常见问题 * 为什么子类构造器会先调用父类构造器? @@ -1365,17 +1365,18 @@ class Animal{ 3. 参考 JVM → 类加载 → 对象创建 ```java - class Animal{ - public Animal(){ + class Animal { + public Animal() { System.out.println("==父类Animal的无参数构造器=="); } } - class Tiger extends Animal{ - public Tiger(){ + + class Tiger extends Animal { + public Tiger() { super(); // 默认存在的,根据参数去匹配调用父类的构造器。 System.out.println("==子类Tiger的无参数构造器=="); } - public Tiger(String name){ + public Tiger(String name) { //super(); 默认存在的,根据参数去匹配调用父类的构造器。 System.out.println("==子类Tiger的有参数构造器=="); } @@ -1421,8 +1422,8 @@ class Animal{ 总结与拓展: -* this 代表了当前对象的引用(继承中指代子类对象):this.子类成员变量、this.子类成员方法、**this(...)**可以根据参数匹配访问本类其他构造器。 -* super 代表了父类对象的引用(继承中指代了父类对象空间):super.父类成员变量、super.父类的成员方法、super(...)可以根据参数匹配访问父类的构造器 +* this 代表了当前对象的引用(继承中指代子类对象):this.子类成员变量、this.子类成员方法、**this(...)** 可以根据参数匹配访问本类其他构造器 +* super 代表了父类对象的引用(继承中指代了父类对象空间):super.父类成员变量、super.父类的成员方法、super(...) 可以根据参数匹配访问父类的构造器 **注意:** @@ -1461,7 +1462,7 @@ class Student{ this.age = age; this.schoolName = schoolName; } -// .......get + set + // .......get + set } ``` @@ -1481,7 +1482,7 @@ final 用于修饰:类,方法,变量 * final 可以修饰方法,方法就不能被重写 * final 修饰变量总规则:变量有且仅能被赋值一次 -**面试题**:final 和 abstract 的关系是互斥关系,不能同时修饰类或者同时修饰方法! +final 和 abstract 的关系是**互斥关系**,不能同时修饰类或者同时修饰方法 @@ -1505,7 +1506,7 @@ final 修饰静态成员变量可以在哪些地方赋值: ```java public class FinalDemo { -//常量:public static final修饰,名称字母全部大写,下划线连接。 + //常量:public static final修饰,名称字母全部大写,下划线连接。 public static final String SCHOOL_NAME = "张三" ; public static final String SCHOOL_NAME1; @@ -1565,9 +1566,9 @@ public class FinalDemo { > 父类知道子类要完成某个功能,但是每个子类实现情况不一样。 -抽象方法:没有方法体,只有方法签名,必须用**abstract**修饰的方法就是抽象方法 +抽象方法:没有方法体,只有方法签名,必须用 abstract 修饰的方法就是抽象方法 -抽象类:拥有抽象方法的类必须定义成抽象类,必须用**abstract**修饰,抽象类是为了被继承 +抽象类:拥有抽象方法的类必须定义成抽象类,必须用 abstract 修饰,**抽象类是为了被继承** 一个类继承抽象类,**必须重写抽象类的全部抽象方法**,否则这个类必须定义成抽象类,因为拥有抽象方法的类必须定义成抽象类 @@ -1597,12 +1598,11 @@ abstract class Animal{ -#### 面试问题 +#### 常见问题 一、抽象类是否有构造器,是否可以创建对象? -答:抽象类作为类一定有构造器,而且必须有构造器,提供给子类继承后调用父类构造器使用的 -* 抽象类有构造器,但是抽象类不能创建对象,类的其他成分它都具备 +* 抽象类有构造器,但是抽象类不能创建对象,类的其他成分它都具备,构造器提供给子类继承后调用父类构造器使用 * 抽象类中存在抽象方法,但不能执行,**抽象类中也可没有抽象方法** > 抽象在学术上本身意味着不能实例化 @@ -1678,9 +1678,9 @@ abstract class Template{ #### 基本介绍 -接口,是 Java 语言中一种引用类型,是方法的集合。 +接口是 Java 语言中一种引用类型,是方法的集合。 -接口是更加彻底的抽象,接口中只有抽象方法和常量,没有其他成分,jdk1.8 前 +接口是更加彻底的抽象,接口中只有抽象方法和常量,没有其他成分 ```java 修饰符 interface 接口名称{ @@ -1691,11 +1691,11 @@ abstract class Template{ } ``` -* 抽象方法:接口中的抽象方法默认会加上public abstract修饰,所以可以省略不写 +* 抽象方法:接口中的抽象方法默认会加上 public abstract 修饰,所以可以省略不写 * 静态方法:静态方法必须有方法体 -* 常量:常量是public static final修饰的成员变量,仅能被赋值一次,值不能改变。常量的名称规范上要求全部大写,多个单词下划线连接。public static final可以省略不写。 +* 常量:是 public static final 修饰的成员变量,仅能被赋值一次,值不能改变。常量的名称规范上要求全部大写,多个单词下划线连接,public static final 可以省略不写 ```java public interface InterfaceDemo{ @@ -1717,14 +1717,11 @@ abstract class Template{ #### 实现接口 -作用:**接口是用来被类实现的。** +**接口是用来被类实现的。** -类与类是继承关系:一个类只能直接继承一个父类,单继承 -类与接口是实现关系:一个类可以实现多个接口,多实现,接口不能继承类 -接口与接口继承关系:**多继承** - ->子类 继承 父类 ->实现类 实现 接口 +* 类与类是继承关系:一个类只能直接继承一个父类,单继承 +* 类与接口是实现关系:一个类可以实现多个接口,多实现,接口不能继承类 +* 接口与接口继承关系:**多继承** ```java 修饰符 class 实现类名称 implements 接口1,接口2,接口3,....{ @@ -1741,9 +1738,9 @@ abstract class Template{ 2. 当一个类实现多个接口时,多个接口中存在同名的默认方法,实现类必须重写这个方法 -3. 当一个类,既继承一个父类,又实现若干个接口时,父类中的成员方法与接口中的默认方法重名,子类**就近选择执行父类**的成员方法 +3. 当一个类既继承一个父类,又实现若干个接口时,父类中成员方法与接口中默认方法重名,子类**就近选择执行父类**的成员方法 -4. 接口中,没有构造器,**不能创建对象**,接口是更彻底的抽象,连构造器都没有,自然不能创建对象!! +4. 接口中,没有构造器,**不能创建对象**,接口是更彻底的抽象,连构造器都没有,自然不能创建对象 ```java public class InterfaceDemo { @@ -1783,10 +1780,12 @@ jdk1.8 以后新增的功能: * 默认方法(就是普通实例方法) * 必须用 default 修饰,默认会 public 修饰 * 必须用接口的实现类的对象来调用 + * 必须有默认实现 * 静态方法 * 默认会 public 修饰 * 接口的静态方法必须用接口的类名本身来调用 * 调用格式:ClassName.method() + * 必须有默认实现 * 私有方法:JDK 1.9 才开始有的,只能在**本类中**被其他的默认方法或者私有方法访问 ```java @@ -1846,7 +1845,7 @@ interface InterfaceJDK8{ | 实现 | 子类使用 **extends** 关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现。 | 子类使用关键字 **implements** 来实现接口。它需要提供接口中所有声明的方法的实现 | | 构造器 | 抽象类可以有构造器 | 接口不能有构造器 | | 与正常Java类的区别 | 除了不能实例化抽象类之外,和普通 Java 类没有任何区别 | 接口是完全不同的类型 | -| 访问修饰符 | 抽象方法有 **public**、**protected** 和 **default** 这些修饰符 | 接口方法默认修饰符是 **public**,别的修饰符需要有方法体 | +| 访问修饰符 | 抽象方法有 **public**、**protected** 和 **default** 这些修饰符 | 接口默认修饰符是 **public**,别的修饰符需要有方法体 | | main方法 | 抽象方法可以有 main 方法并且我们可以运行它 | jdk8 以前接口没有 main 方法,不能运行;jdk8 以后接口可以有 default 和 static 方法,可以运行 main 方法 | | 多继承 | 抽象方法可以继承一个类和实现多个接口 | 接口可以继承一个或多个其它接口,接口不可继承类 | | 速度 | 比接口速度要快 | 接口是稍微有点慢的,因为它需要时间去寻找在类中实现的方法 | @@ -1928,21 +1927,22 @@ class Animal{ #### 上下转型 >基本数据类型的转换: -> 1.小范围类型的变量或者值可以直接赋值给大范围类型的变量。 -> 2.大范围类型的变量或者值必须强制类型转换给小范围类型的变量。 +> +>1. 小范围类型的变量或者值可以直接赋值给大范围类型的变量 +>2. 大范围类型的变量或者值必须强制类型转换给小范围类型的变量 -引用数据类型的**自动**类型转换语法:子类类型的对象或者变量可以自动类型转换赋值给父类类型的变量。 +引用数据类型的**自动**类型转换语法:子类类型的对象或者变量可以自动类型转换赋值给父类类型的变量 **父类引用指向子类对象** -- **向上转型(upcasting)**:通过子类对象(小范围)实例化父类对象(大范围),这种属于自动转换 -- **向下转型(downcasting)**:通过父类对象(大范围)实例化子类对象(小范围),这种属于强制转换 +- **向上转型 (upcasting)**:通过子类对象(小范围)实例化父类对象(大范围),这种属于自动转换 +- **向下转型 (downcasting)**:通过父类对象(大范围)实例化子类对象(小范围),这种属于强制转换 ```java public class PolymorphicDemo { public static void main(String[] args){ - Animal a = new Cat();//向上转型 - Cat c = (Cat)a;//向下转型 + Animal a = new Cat(); // 向上转型 + Cat c = (Cat)a; // 向下转型 } } class Animal{} @@ -2007,7 +2007,7 @@ class Animal{} #### 静态内部类 -定义:有static修饰,属于外部类本身,会加载一次 +定义:有 static 修饰,属于外部类本身,会加载一次 静态内部类中的成分研究: @@ -2017,7 +2017,7 @@ class Animal{} 静态内部类的访问格式:外部类名称.内部类名称 -静态内部类创建对象的格式:外部类名称.内部类名称 对象名称 = new 外部类名称.内部类构造器; +静态内部类创建对象的格式:外部类名称.内部类名称 对象名称 = new 外部类名称.内部类构造器 静态内部类的访问拓展: @@ -2050,22 +2050,20 @@ static class Outter{ #### 实例内部类 -定义:无static修饰的内部类,属于外部类的每个对象,跟着外部类对象一起加载。 +定义:无 static 修饰的内部类,属于外部类的每个对象,跟着外部类对象一起加载。 实例内部类的成分特点:实例内部类中不能定义静态成员,其他都可以定义 实例内部类的访问格式:外部类名称.内部类名称 -创建对象的格式:外部类名称.内部类名称 对象名称 = new 外部类构造器.new 内部构造器; +创建对象的格式:外部类名称.内部类名称 对象名称 = new 外部类构造器.new 内部构造器 -* `Outter.Inner in = new Outter().new Inner();` +* `Outter.Inner in = new Outter().new Inner()` -拓展:**实例内部类可以访问外部类的全部成员** +**实例内部类可以访问外部类的全部成员** -> * 实例内部类中是否可以直接访问外部类的静态成员? -> 可以,外部类的静态成员可以被共享访问! -> * 实例内部类中是否可以访问外部类的实例成员? -> 可以,实例内部类属于外部类对象,可以直接访问外部类对象的实例成员! +* 实例内部类中可以直接访问外部类的静态成员,外部类的静态成员可以被共享访问 +* 实例内部类中可以访问外部类的实例成员,实例内部类属于外部类对象,可以直接访问外部类对象的实例成员 @@ -2075,7 +2073,7 @@ static class Outter{ #### 局部内部类 -局部内部类:定义在方法中,在构造器中,代码块中,for循环中定义的内部类。 +局部内部类:定义在方法中,在构造器中,代码块中,for 循环中定义的内部类 局部内部类中的成分特点:只能定义实例成员,不能定义静态成员 @@ -2101,7 +2099,6 @@ public class InnerClass{ #### 匿名内部类 匿名内部类:没有名字的局部内部类 -作用:简化代码,是开发中常用的形式 匿名内部类的格式: @@ -2114,7 +2111,7 @@ new 类名|抽象类|接口(形参){ * 匿名内部类不能定义静态成员 * 匿名内部类一旦写出来,就会立即创建一个匿名内部类的对象返回 -* **匿名内部类的对象的类型相当于是当前new的那个的类型的子类类型** +* **匿名内部类的对象的类型相当于是当前 new 的那个的类型的子类类型** * 匿名内部类引用局部变量必须是**常量**,底层创建为内部类的成员变量(原因:JVM → 运行机制 → 代码优化) ```java @@ -2149,7 +2146,7 @@ abstract class Animal{ ### 权限符 权限修饰符:有四种**(private -> 缺省 -> protected - > public )** -可以修饰成员变量,修饰方法,修饰构造器,内部类,不同修饰符修饰的成员能够被访问的权限将受到限制! +可以修饰成员变量,修饰方法,修饰构造器,内部类,不同修饰符修饰的成员能够被访问的权限将受到限制 | 四种修饰符访问权限 | private | 缺省 | protected | public | | ------------------ | :-----: | :--: | :-------: | :----: | @@ -2184,12 +2181,12 @@ static { ``` * 静态代码块特点: - * 必须有static修饰 + * 必须有 static 修饰 * 会与类一起优先加载,且自动触发执行一次 * 只能访问静态资源 * 静态代码块作用: * 可以在执行类的方法等操作之前先在静态代码块中进行静态资源的初始化 - * **先执行静态代码块,在执行main函数里的操作** + * **先执行静态代码块,在执行 main 函数里的操作** ```java public class CodeDemo { @@ -2234,7 +2231,7 @@ main方法被执行 ``` * 实例代码块的特点: - * 无static修饰,属于对象 + * 无 static 修饰,属于对象 * 会与类的对象一起加载,每次创建类的对象的时候,实例代码块都会被加载且自动触发执行一次 * 实例代码块的代码在底层实际上是提取到每个构造器中去执行的 @@ -2305,7 +2302,7 @@ public boolean equals(Object o) { **面试题**:== 和 equals 的区别 * == 比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的**地址**是否相同,即是否是指相同一个对象,比较的是真正意义上的指针操作 -* Object 类中的方法,默认比较两个对象的引用,重写 equals 方法比较的是两个对象的**内容**是否相等,所有的类都是继承自 java.lang.Object 类,所以适用于所有对象 +* Object 类中的方法,**默认比较两个对象的引用**,重写 equals 方法比较的是两个对象的**内容**是否相等,所有的类都是继承自 java.lang.Object 类,所以适用于所有对象 hashCode 的作用: @@ -2331,7 +2328,7 @@ Object 的 clone() 是 protected 方法,一个类不显式去重写 clone(), * 深拷贝 (deepCopy):对基本数据类型进行值传递,对引用数据类型是一个整个独立的对象拷贝,会拷贝所有的属性并指向的动态分配的内存,简而言之就是把所有属性复制到一个新的内存,增加一个指针指向新内存。所以使用深拷贝的情况下,释放内存的时候不会出现使用浅拷贝时释放同一块内存的错误 -Cloneable 接口是一个标识性接口,即该接口不包含任何方法(包括clone()),但是如果一个类想合法的进行克隆,那么就必须实现这个接口,在使用 clone() 方法时,若该类未实现 Cloneable 接口,则抛出异常 +Cloneable 接口是一个标识性接口,即该接口不包含任何方法(包括 clone),但是如果一个类想合法的进行克隆,那么就必须实现这个接口,在使用 clone() 方法时,若该类未实现 Cloneable 接口,则抛出异常 * Clone & Copy:`Student s = new Student` @@ -2343,7 +2340,7 @@ Cloneable 接口是一个标识性接口,即该接口不包含任何方法( 浅克隆:Object 中的 clone() 方法在对某个对象克隆时对其仅仅是简单地执行域对域的 copy - * 对基本数据类型和包装类的克隆是没有问题的。String、Integer 等包装类型在内存中是不可以被改变的对象,所以在使用克隆时可以视为基本类型,只需浅克隆引用即可 + * 对基本数据类型和包装类的克隆是没有问题的。String、Integer 等包装类型在内存中是**不可以被改变的对象**,所以在使用克隆时可以视为基本类型,只需浅克隆引用即可 * 如果对一个引用类型进行克隆时只是克隆了它的引用,和原始对象共享对象成员变量 ![](https://gitee.com/seazean/images/raw/master/Java/Object浅克隆.jpg) @@ -2376,12 +2373,12 @@ SDP → 创建型 → 原型模式 ### Objects -Objects 类与 Object 是继承关系。 +Objects 类与 Object 是继承关系 -Objects的方法: +Objects 的方法: -* `public static boolean equals(Object a, Object b)` : 比较两个对象是否相同。 - 底层进行非空判断,从而可以避免空指针异常,更安全!!推荐使用!! +* `public static boolean equals(Object a, Object b)`:比较两个对象是否相同。 + 底层进行非空判断,从而可以避免空指针异常,更安全,推荐使用! ```java public static boolean equals(Object a, Object b) { @@ -2389,11 +2386,11 @@ Objects的方法: } ``` -* `public static boolean isNull(Object obj)` : 判断变量是否为null ,为null返回true ,反之! +* `public static boolean isNull(Object obj)`:判断变量是否为 null ,为 null 返回 true -* `public static String toString(对象)` : 返回参数中对象的字符串表示形式 +* `public static String toString(对象)`:返回参数中对象的字符串表示形式 -* `public static String toString(对象, 默认字符串)` : 返回对象的字符串表示形式。 +* `public static String toString(对象, 默认字符串)`:返回对象的字符串表示形式 ```java public class ObjectsDemo { @@ -2464,7 +2461,7 @@ s = s + "cd"; //s = abccd 新对象 * `public char[] toCharArray()`:将字符串拆分为字符数组后返回 * `public boolean startsWith(String prefix)`:测试此字符串是否以指定的前缀开头 * `public int indexOf(String str)`:返回指定子字符串第一次出现的字符串内的索引,没有返回 -1 -* `public int lastIndexOf(String str)`:返回字符串最后一次出现str的索引,没有返回 -1 +* `public int lastIndexOf(String str)`:返回字符串最后一次出现 str 的索引,没有返回 -1 * `public String substring(int beginIndex)`:返回子字符串,以原字符串指定索引处到结尾 * `public String substring(int i, int j)`:指定索引处扩展到 j - 1 的位置,字符串长度为 j - i * `public String toLowerCase()`:将此 String 所有字符转换为小写,使用默认语言环境的规则 @@ -2493,7 +2490,7 @@ s.replace("-","");//12378 直接赋值:`String s = "abc"` 直接赋值的方式创建字符串对象,内容就是 abc - 通过构造方法创建:通过 new 创建的字符串对象,每一次 new 都会申请一个内存空间,虽然内容相同,但是地址值不同,**返回堆内存中对象的引用** -- 直接赋值方式创建:以“ ”方式给出的字符串,只要字符序列相同(顺序和大小写),无论在程序代码中出现几次,JVM 都只会**在 String Pool 中创建一个字符串对象**,并在字符串池中维护 +- 直接赋值方式创建:以 `" "` 方式给出的字符串,只要字符序列相同(顺序和大小写),无论在程序代码中出现几次,JVM 都只会**在 String Pool 中创建一个字符串对象**,并在字符串池中维护 `String str = new String("abc")` 创建字符串对象: @@ -2574,7 +2571,7 @@ public class Demo { System.out.println(s3 == s5); // true String x2 = new String("c") + new String("d"); // new String("cd") - // 虽然 new,但是在字符串常量池没有 cd 对象,toString() 方法 + // 虽然 new,但是在字符串常量池没有 cd 对象,因为 toString() 方法 x2.intern(); String x1 = "cd"; @@ -2612,7 +2609,7 @@ public static void main(String[] args) { String s2 = s.intern(); //jdk6:串池中创建一个字符串"ab" - //jdk8:串池中没有创建字符串"ab",而是创建一个引用指向new String("ab"),将此引用返回 + //jdk8:串池中没有创建字符串"ab",而是创建一个引用指向 new String("ab"),将此引用返回 System.out.println(s2 == "ab");//jdk6:true jdk8:true System.out.println(s == "ab");//jdk6:false jdk8:true @@ -2641,7 +2638,7 @@ public static void main(String[] args) { } ``` -* Version类初始化时需要对静态常量字段初始化,被 launcher_name 静态常量字段所引用的"java"字符串字面量就被放入的字符串常量池: +* Version 类初始化时需要对静态常量字段初始化,被 launcher_name 静态常量字段所引用的 `"java"` 字符串字面量就被放入的字符串常量池: ```java package sun.misc; @@ -2664,15 +2661,15 @@ public static void main(String[] args) { ##### 内存位置 -Java 7之前,String Pool 被放在运行时常量池中,它属于永久代;Java 7以后,String Pool 被移到堆中,这是因为永久代的空间有限,在大量使用字符串的场景下会导致OutOfMemoryError 错误 +Java 7 之前,String Pool 被放在运行时常量池中,属于永久代;Java 7以后,String Pool 被移到堆中,这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误 演示 StringTable 位置: -* `-Xmx10m`设置堆内存10m +* `-Xmx10m` 设置堆内存 10m -* 在jdk8下设置: `-Xmx10m -XX:-UseGCOverheadLimit`(运行参数在Run Configurations VM options) +* 在 JDK8 下设置: `-Xmx10m -XX:-UseGCOverheadLimit`(运行参数在 Run Configurations VM options) -* 在jdk6下设置: `-XX:MaxPermSize=10m` +* 在 JDK6 下设置: `-XX:MaxPermSize=10m` ```java public static void main(String[] args) throws InterruptedException { @@ -2824,7 +2821,7 @@ public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) { ### Arrays -Array 的工具类 +Array 的工具类 Arrays 常用API: @@ -3755,7 +3752,7 @@ public class RegexDemo { 压栈 == 入栈、弹栈 == 出栈 场景:手枪的弹夹 -* 数组:数组是内存中的连续存储区域,分成若干等分的小区域(每个区域大小是一样的)。元素存在索引 +* 数组:数组是内存中的连续存储区域,分成若干等分的小区域(每个区域大小是一样的)元素存在索引 特点:**查询元素快**(根据索引快速计算出元素的地址,然后立即去定位) **增删元素慢**(创建新数组,迁移元素) @@ -3765,12 +3762,11 @@ public class RegexDemo { * 树: * 二叉树:binary tree 永远只有一个根节点,是每个结点不超过2个节点的树(tree) - 特点:二叉排序树:小的左边,大的右边,但是可能树很高,性能变差 - 为了做排序和搜索会进行左旋和右旋实现平衡查找二叉树,让树的高度差不大于1 - - * 红黑树(基于红黑规则实现自平衡的排序二叉树):树保证到了很矮小,但是又排好序,性能最高的树 - - 特点:**红黑树的增删查改性能都好** + 特点:二叉排序树:小的左边,大的右边,但是可能树很高,性能变差,为了做排序和搜索会进行左旋和右旋实现平衡查找二叉树,让树的高度差不大于1 + +* 红黑树(基于红黑规则实现自平衡的排序二叉树):树保证到了很矮小,但是又排好序,性能最高的树 + + 特点:**红黑树的增删查改性能都好** 各数据结构时间复杂度对比: @@ -3790,7 +3786,7 @@ public class RegexDemo { #### 概述 -Java 中集合的代表是Collection,Collection 集合是 Java 中集合的祖宗类 +Java 中集合的代表是 Collection,Collection 集合是 Java 中集合的祖宗类 Collection 集合底层为数组:`[value1, value2, ....]` @@ -3979,8 +3975,6 @@ public static void main(String[] args){ } ``` -![ArrayList源码分析](https://gitee.com/seazean/images/raw/master/Java/ArrayList添加元素源码解析.png) - *** @@ -3997,8 +3991,8 @@ public class ArrayList extends AbstractList ``` - `RandomAccess` 是一个标志接口,表明实现这个这个接口的 List 集合是支持**快速随机访问**的。在 `ArrayList` 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。 -- `ArrayList` 实现了 `Cloneable` 接口 ,即覆盖了函数`clone()`,能被克隆 -- `ArrayList` 实现了 `Serializable `接口,这意味着`ArrayList`支持序列化,能通过序列化去传输 +- `ArrayList` 实现了 `Cloneable` 接口 ,即覆盖了函数 `clone()`,能被克隆 +- `ArrayList` 实现了 `Serializable ` 接口,这意味着 `ArrayList` 支持序列化,能通过序列化去传输 核心方法: @@ -4082,8 +4076,8 @@ public class ArrayList extends AbstractList MAX_ARRAY_SIZE:要分配的数组的最大大小,分配更大的**可能**会导致 - * OutOfMemoryError:Requested array size exceeds VM limit(请求的数组大小超出VM限制) - * OutOfMemoryError: Java heap space(堆区内存不足,可以通过设置JVM参数 -Xmx 来调节) + * OutOfMemoryError:Requested array size exceeds VM limit(请求的数组大小超出 VM 限制) + * OutOfMemoryError: Java heap space(堆区内存不足,可以通过设置 JVM 参数 -Xmx 来调节) * 删除元素:需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,在旧数组上操作,该操作的时间复杂度为 O(N),可以看到 ArrayList 删除元素的代价是非常高的, @@ -4121,7 +4115,7 @@ public class ArrayList extends AbstractList } ``` -* **Fail-Fast**:快速失败,modCount 用来记录 ArrayList **结构发生变化**的次数,结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化 +* **Fail-Fast**:快速失败,modCount 用来记录 ArrayList **结构发生变化**的次数,结构发生变化是指添加或者删除至少一个元素的操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化 在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,改变了抛出 ConcurrentModificationException 异常 @@ -4186,7 +4180,7 @@ public class ArrayList extends AbstractList 2. Vector 每次扩容请求其大小的 2 倍(也可以通过构造函数设置增长的容量),而 ArrayList 是 1.5 倍 -3. 底层都是 `Object[]`数组存储 +3. 底层都是 `Object[]` 数组存储 @@ -4214,7 +4208,7 @@ LinkedList 除了拥有 List 集合的全部功能还多了很多操作首尾元 * `public void push(E e)`:将元素推入此列表所表示的堆栈 * `public int indexOf(Object o)`:返回此列表中指定元素的第一次出现的索引,如果不包含返回 -1 * `public int lastIndexOf(Object o)`:从尾遍历找 -* ` public boolean remove(Object o)`:一次只删除一个匹配的对象,如果删除了匹配对象返回true +* ` public boolean remove(Object o)`:一次只删除一个匹配的对象,如果删除了匹配对象返回 true * `public E remove(int index)`:删除指定位置的元素 ```java @@ -4247,9 +4241,9 @@ public class ListDemo { } ``` -![](https://gitee.com/seazean/images/raw/master/Java/LinkedList添加元素源码解析.png) +*** @@ -4336,7 +4330,7 @@ Set 系列集合:添加的元素是无序,不重复,无索引的 * LinkedHashSet:添加的元素是有序,不重复,无索引的 * TreeSet:不重复,无索引,按照大小默认升序排序 -**面试问题**:没有索引,不能使用普通 for 循环遍历 +**注意**:没有索引,不能使用普通 for 循环遍历 @@ -4355,7 +4349,7 @@ Set 系列集合:添加的元素是无序,不重复,无索引的 - 哈希值的特点 - 同一个对象多次调用 hashCode() 方法返回的哈希值是相同的 - - 默认情况下,不同对象的哈希值是不同的。而重写 hashCode() 方法,可以实现让不同对象的哈希值相同 + - 默认情况下,不同对象的哈希值是不同的,而重写 hashCode() 方法,可以实现让不同对象的哈希值相同 **HashSet 底层就是基于 HashMap 实现,值是 PRESENT = new Object()** @@ -4382,16 +4376,17 @@ Set 集合添加的元素是无序,不重复的。 不重复 重复了 ``` -* Set系列集合元素无序的根本原因 +* Set 系列集合元素无序的根本原因 - Set系列集合添加元素无序的根本原因是因为**底层采用了哈希表存储元素**。 - JDK 1.8 之前:哈希表 = 数组(初始容量16) + 链表 + (哈希算法) - JDK 1.8 之后:哈希表 = 数组(初始容量16) + 链表 + 红黑树 + (哈希算法) - 当链表长度超过阈值8且当前数组的长度 > 64时,将链表转换为红黑树,减少了查找时间 - 当链表长度超过阈值8且当前数组的长度 < 64时,扩容 + Set 系列集合添加元素无序的根本原因是因为**底层采用了哈希表存储元素**。 + + * JDK 1.8 之前:哈希表 = 数组(初始容量16) + 链表 + (哈希算法) + * JDK 1.8 之后:哈希表 = 数组(初始容量16) + 链表 + 红黑树 + (哈希算法) + * 当链表长度超过阈值 8 且当前数组的长度 > 64时,将链表转换为红黑树,减少了查找时间 + * 当链表长度超过阈值 8 且当前数组的长度 < 64时,扩容 ![](https://gitee.com/seazean/images/raw/master/Java/HashSet底层结构哈希表.png) - + 每个元素的 hashcode() 的值进行响应的算法运算,计算出的值相同的存入一个数组块中,以链表的形式存储,如果链表长度超过8就采取红黑树存储,所以输出的元素是无序的。 * 如何设置只要对象内容一样,就希望集合认为它们重复了:**重写 hashCode 和 equals 方法** @@ -4426,9 +4421,9 @@ TreeSet 集合自排序的方式: * 直接为**对象的类**实现比较器规则接口 Comparable,重写比较方法: - 方法:`public int compareTo(Employee o): this 是比较者, o 是被比较者` + 方法:`public int compareTo(Employee o): this 是比较者, o 是被比较者` - * 比较者大于被比较者,返回正数(升序) + * 比较者大于被比较者,返回正数 * 比较者小于被比较者,返回负数 * 比较者等于被比较者,返回 0 @@ -4436,7 +4431,7 @@ TreeSet 集合自排序的方式: 方法:`public int compare(Employee o1, Employee o2): o1 比较者, o2 被比较者` - * 比较者大于被比较者,返回正数(升序) + * 比较者大于被比较者,返回正数 * 比较者小于被比较者,返回负数 * 比较者等于被比较者,返回 0 @@ -4475,7 +4470,7 @@ public class Student implements Comparable{ } ``` -比较器原理:底层是以第一个元素为基准,加一个新元素,就会和第一个元素比,如果大于,就继续和大于的元素进行比较,直到遇到比新元素大的元素为止,放在该位置的左边。(红黑树) +比较器原理:底层是以第一个元素为基准,加一个新元素,就会和第一个元素比,如果大于,就继续和大于的元素进行比较,直到遇到比新元素大的元素为止,放在该位置的左边(红黑树) @@ -4642,7 +4637,7 @@ Map集合的遍历方式有:3种。 1. “键找值”的方式遍历:先获取 Map 集合全部的键,再根据遍历键找值。 2. “键值对”的方式遍历:难度较大,采用增强 for 或者迭代器 -3. JDK 1.8 开始之后的新技术:foreach,采用 Lambda表 达式 +3. JDK 1.8 开始之后的新技术:foreach,采用 Lambda 表达式 集合可以直接输出内容,因为底层重写了 toString() 方法 @@ -4732,7 +4727,7 @@ JDK7 对比 JDK8: ##### 继承关系 -HashMap继承关系如下图所示: +HashMap 继承关系如下图所示: ![](https://gitee.com/seazean/images/raw/master/Java/HashMap继承关系.bmp) @@ -4740,9 +4735,9 @@ HashMap继承关系如下图所示: 说明: -* Cloneable 空接口,表示可以克隆, 创建并返回HashMap对象的一个副本。 -* Serializable 序列化接口,属于标记性接口,HashMap对象可以被序列化和反序列化。 -* AbstractMap 父类提供了Map实现接口,以最大限度地减少实现此接口所需的工作 +* Cloneable 空接口,表示可以克隆, 创建并返回 HashMap 对象的一个副本。 +* Serializable 序列化接口,属于标记性接口,HashMap 对象可以被序列化和反序列化。 +* AbstractMap 父类提供了 Map 实现接口,以最大限度地减少实现此接口所需的工作 @@ -4773,7 +4768,7 @@ HashMap继承关系如下图所示: * 为什么必须是 2 的 n 次幂? - HashMap 中添加元素时,需要根据 key 的 hash 值,确定在数组中的具体位置。为了存取高效,要尽量较少碰撞,把数据尽可能分配均匀,每个链表长度大致相同,实现该方法的算法就是取模,hash%length,计算机中直接求余效率不如位移运算,所以源码中使用 hash&(length-1),实际上**hash % length == hash & (length-1)的前提是 length 是 2 的n次幂** + HashMap 中添加元素时,需要根据 key 的 hash 值,确定在数组中的具体位置。为了存取高效,要尽量较少碰撞,把数据分配均匀,每个链表长度大致相同,实现该方法的算法就是取模,hash%length,计算机中直接求余效率不如位移运算,所以源码中使用 hash&(length-1),实际上**hash % length == hash & (length-1)的前提是 length 是 2 的n次幂** 散列平均分布:2 的 n 次方是 1 后面 n 个 0,2 的 n 次方 -1 是 n 个 1,可以**保证散列的均匀性**,减少碰撞 @@ -4833,7 +4828,7 @@ HashMap继承关系如下图所示: * 其他说法 红黑树的平均查找长度是 log(n),如果长度为 8,平均查找长度为 log(8)=3,链表的平均查找长度为 n/2,当长度为 8 时,平均查找长度为 8/2=4,这才有转换成树的必要;链表长度如果是小于等于 6,6/2=3,而 log(6)=2.6,虽然速度也很快的,但转化为树结构和生成树的时间并不短 -6. 当链表的值小 于 6 则会从红黑树转回链表 +6. 当链表的值小于 6 则会从红黑树转回链表 ```java // 当桶(bucket)上的结点数小于这个值时树转链表 @@ -4847,7 +4842,7 @@ HashMap继承关系如下图所示: static final int MIN_TREEIFY_CAPACITY = 64; ``` - 原因:数组比较小的情况下变为红黑树结构,反而会降低效率,红黑树需要进行左旋,右旋,变色这些操作来保持平衡。同时数组长度小于 64 时,搜索时间相对快些,所以为了提高性能和减少搜索时间,底层在阈值大于 8 并且数组长度大于 64 时,链表才转换为红黑树,效率也变的更高效 + 原因:数组比较小的情况下变为红黑树结构,反而会降低效率,红黑树需要进行左旋,右旋,变色这些操作来保持平衡。同时数组长度小于 64 时,搜索时间相对快些,所以为了提高性能和减少搜索时间,底层在阈值大于 8 并且数组长度大于等于 64 时,链表才转换为红黑树,效率也变的更高效 8. table 用来初始化(必须是二的 n 次幂) @@ -4858,7 +4853,7 @@ HashMap继承关系如下图所示: jdk8 之前数组类型是 Entry类型,之后是 Node 类型。只是换了个名字,都实现了一样的接口 Map.Entry,负责存储键值对数据的 - 9. HashMap 中**存放元素的个数**(**重点**) + 9. HashMap 中**存放元素的个数** ```java // 存放元素的个数,HashMap中K-V的实时数量,不是table数组的长度 @@ -4899,7 +4894,7 @@ HashMap继承关系如下图所示: loadFactor 太大导致查找元素效率低,存放的数据拥挤,太小导致数组的利用率低,存放的数据会很分散。loadFactor 的默认值为 **0.75f 是官方给出的一个比较好的临界值** - * threshold 计算公式:capacity(数组长度默认16) * loadFactor(负载因子默认 0.75)。这个值是当前已占用数组长度的最大值。**当 size>=threshold** 的时候,那么就要考虑对数组的 resize(扩容),这就是衡量数组是否需要扩增的一个标准, 扩容后的 HashMap 容量是之前容量的**两倍**. + * threshold 计算公式:capacity(数组长度默认16) * loadFactor(负载因子默认 0.75)。这个值是当前已占用数组长度的最大值。**当 size>=threshold** 的时候,那么就要考虑对数组的 resize(扩容),这就是衡量数组是否需要扩增的一个标准, 扩容后的 HashMap 容量是之前容量的**两倍** @@ -5013,7 +5008,7 @@ HashMap继承关系如下图所示: 计算 hash 的方法:将 hashCode 无符号右移 16 位,高 16bit 和低 16bit 做异或,扰动运算 - 原因:当数组长度很小,假设是 16,那么 n-1 即为 1111 ,这样的值和 hashCode() 直接做按位与操作,实际上只使用了哈希值的后4位。如果当哈希值的高位变化很大,低位变化很小,就很容易造成哈希冲突了,所以这里**把高低位都利用起来,让高16 位也参与运算**,从而解决了这个问题 + 原因:当数组长度很小,假设是 16,那么 n-1 即为 1111 ,这样的值和 hashCode() 直接做按位与操作,实际上只使用了哈希值的后 4 位。如果当哈希值的高位变化很大,低位变化很小,就很容易造成哈希冲突了,所以这里**把高低位都利用起来,让高16 位也参与运算**,从而解决了这个问题 哈希冲突的处理方式: @@ -5320,7 +5315,7 @@ HashMap继承关系如下图所示: 3. 桶上的 key 不是要找的 key,则查看后续的节点: - * 如果后续节点是红黑树节点,通过调用红黑树的方法根据 key 获取v alue + * 如果后续节点是红黑树节点,通过调用红黑树的方法根据 key 获取 value * 如果后续节点是链表节点,则通过循环遍历链表根据 key 获取 value @@ -5443,7 +5438,7 @@ LinkedHashMap 是 HashMap 的子类 源码解析: -* 内部维护了一个双向链表,用来维护插入顺序或者 LRU 顺序 +* **内部维护了一个双向链表**,用来维护插入顺序或者 LRU 顺序 ```java transient LinkedHashMap.Entry head; @@ -5672,7 +5667,7 @@ TreeMap 集合指定大小规则有 2 种方式: WeakHashMap 是基于弱引用的 -内部的 Entry 继承 WeakReference,被弱引用关联的对象在下一次垃圾回收时会被回收,并且构造方法传入引用队列,用来在清理对象完成以后清理引用 +内部的 Entry 继承 WeakReference,被弱引用关联的对象在**下一次垃圾回收时会被回收**,并且构造方法传入引用队列,用来在清理对象完成以后清理引用 ```java private static class Entry extends WeakReference implements Map.Entry { @@ -5734,41 +5729,6 @@ Tomcat 中的 ConcurrentCache 使用了 WeakHashMap 来实现缓存功能,Conc -*** - - - -#### 面试题 - -输出一个字符串中每个字符出现的次数。 - -```java -/* - (1)键盘录入一个字符串。aabbccddaa123。 - (2)定义一个Map集合,键是每个字符,值是其出现的次数。 {a=4 , b=2 ,...} - (3)遍历字符串中的每一个字符。 - (4)拿着这个字符去Map集合中看是否有这个字符键,有说明之前统计过,其值+1 - 没有这个字符键,说明该字符是第一次统计,直接存入“该字符=1” -*/ -public class MapDemo{ - public static void main(String[] args){ - String s = "aabbccddaa123"; - Map infos = new HashMap<>(); - for (int i = 0; i < s.length(); i++){ - char ch = datas.charAt(i); - if(infos.containsKey(ch)){ - infos.put(ch,infos.get(ch) + 1); - } else { - infos.put(ch,1); - } - } - System.out.println("结果:"+infos); - } -} -``` - - - *** @@ -14234,7 +14194,7 @@ public static void main(String[] args) { } ``` -注意:如果调用了 foo() 则等价代码为 `foo(new String[]{})` ,创建了一个空的数组,而不会传递 null 进去 +注意:如果调用了 `foo()` 则等价代码为 `foo(new String[]{})` ,创建了一个空的数组,而不会传递 null 进去 diff --git a/Prog.md b/Prog.md index c09c693..8d5634a 100644 --- a/Prog.md +++ b/Prog.md @@ -14077,7 +14077,7 @@ ServerSocket 类: -相当于客户端和服务器建立一个数据管道,管道一般不用 close +**相当于**客户端和服务器建立一个数据管道(虚连接,不是真正的物理连接),管道一般不用 close diff --git a/Web.md b/Web.md index 3236ec1..ea0ed9d 100644 --- a/Web.md +++ b/Web.md @@ -2119,8 +2119,10 @@ URL 和 URI * 进行 URL 解析,进行编码 * DNS 解析,顺序是先查 hosts 文件是否有记录,有的话就会把相对应映射的 IP 返回,然后去本地 DNS 缓存中寻找,然后去找计算机上配置的 DNS 服务器上有或者有缓存,最后去找全球的根 DNS 服务器,直到查到为止 -* 查找到 IP 之后,进行 TCP 协议的三次握手,发出 HTTP 请求 +* 查找到 IP 之后,进行 TCP 协议的三次握手建立连接 +* 发出 HTTP 请求,取文件指令 * 服务器处理请求,返回响应 +* 释放 TCP 连接 * 浏览器解析渲染页面 From c6e1467b4466acb9b6686b8008dc844443b8de01 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 20 Feb 2022 23:20:36 +0800 Subject: [PATCH 113/168] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4ac1828..211ff15 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ * DB:MySQL、Redis * Frame:Maven、Netty、RocketMQ -* Java:JavaSE、JVM、Algorithm、Design Pattern +* Java:JavaSE、JVM、Algorithm * Prog:Concurrent、Network Programming * SSM:MyBatis、Spring、SpringMVC、SpringBoot * Tool:Git、Linux、Docker From 83f54324a1b289a542e899a5b0520710481e806e Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 21 Feb 2022 00:43:50 +0800 Subject: [PATCH 114/168] Update Java Note --- Java.md | 15131 ++++++++++++++++++++---------------------------------- 1 file changed, 5599 insertions(+), 9532 deletions(-) diff --git a/Java.md b/Java.md index be48f40..77f9179 100644 --- a/Java.md +++ b/Java.md @@ -2260,6 +2260,8 @@ public class CodeDemo { + + *** @@ -2872,6 +2874,48 @@ public class MyArraysDemo { +*** + + + +### System + +System 代表当前系统 + +静态方法: + +* `public static void exit(int status)`:终止 JVM 虚拟机,**非 0 是异常终止** + +* `public static long currentTimeMillis()`:获取当前系统此刻时间毫秒值 + +* `static void arraycopy(Object var0, int var1, Object var2, int var3, int var4)`:数组拷贝 + 参数一:原数组 + 参数二:从原数组的哪个位置开始赋值 + 参数三:目标数组 + 参数四:从目标数组的哪个位置开始赋值 + 参数五:赋值几个 + +```java +public class SystemDemo { + public static void main(String[] args) { + //System.exit(0); // 0代表正常终止!! + long startTime = System.currentTimeMillis();//定义sdf 按照格式输出 + for(int i = 0; i < 10000; i++){输出i} + long endTime = new Date().getTime(); + System.out.println( (endTime - startTime)/1000.0 +"s");//程序用时 + + int[] arr1 = new int[]{10 ,20 ,30 ,40 ,50 ,60 ,70}; + int[] arr2 = new int[6]; // [ 0 , 0 , 0 , 0 , 0 , 0] + // 变成arrs2 = [0 , 30 , 40 , 50 , 0 , 0 ] + System.arraycopy(arr1, 2, arr2, 1, 3); + } +} +``` + + + + + *** @@ -2889,8 +2933,8 @@ public class MyArraysDemo { 时间记录的两种方式: -1. Date日期对象 -2. 时间毫秒值:从1970-01-01 00:00:00开始走到此刻的总的毫秒值。 1s = 1000ms +1. Date 日期对象 +2. 时间毫秒值:从 `1970-01-01 00:00:00` 开始走到此刻的总的毫秒值,1s = 1000ms ```java public class DateDemo { @@ -2934,10 +2978,10 @@ DateFormat 是一个抽象类,不能直接使用,使用它的子类:Simple SimpleDateFormat 简单日期格式化类: -* `public SimpleDateFormat(String pattern)` : 指定时间的格式创建简单日期对象 -* `public String format(Date date) ` : 把日期对象格式化成我们喜欢的时间形式,返回字符串 -* `public String format(Object time)` : 把时间毫秒值格式化成设定的时间形式,返回字符串! -* `public Date parse(String date)` : 把字符串的时间解析成日期对象 +* `public SimpleDateFormat(String pattern)`:指定时间的格式创建简单日期对象 +* `public String format(Date date) `:把日期对象格式化成我们喜欢的时间形式,返回字符串 +* `public String format(Object time)`:把时间毫秒值格式化成设定的时间形式,返回字符串! +* `public Date parse(String date)`:把字符串的时间解析成日期对象 >yyyy年MM月dd日 HH:mm:ss EEE a" 周几 上午下午 @@ -2972,12 +3016,12 @@ Calendar 日历类创建日历对象:`Calendar rightNow = Calendar.getInstance Calendar 的方法: -* `public static Calendar getInstance()`: 返回一个日历类的对象 +* `public static Calendar getInstance()`:返回一个日历类的对象 * `public int get(int field)`:取日期中的某个字段信息 * `public void set(int field,int value)`:修改日历的某个字段信息 * `public void add(int field,int amount)`:为某个字段增加/减少指定的值 -* `public final Date getTime()`: 拿到此刻日期对象 -* `public long getTimeInMillis()`: 拿到此刻时间毫秒值 +* `public final Date getTime()`:拿到此刻日期对象 +* `public long getTimeInMillis()`:拿到此刻时间毫秒值 ```java public static void main(String[] args){ @@ -3048,23 +3092,10 @@ public class JDK8DateDemo2 { } ``` -| 方法名 | 说明 | -| --------------------------------------------------- | ------------------------------ | -| public LocalDateTime plusYears (long years) | 添加或者减去年 | -| public LocalDateTime plusMonths(long months) | 添加或者减去月 | -| public LocalDateTime plusDays(long days) | 添加或者减去日 | -| public LocalDateTime plusHours(long hours) | 添加或者减去时 | -| public LocalDateTime plusMinutes(long minutes) | 添加或者减去分 | -| public LocalDateTime plusSeconds(long seconds) | 添加或者减去秒 | -| public LocalDateTime plusWeeks(long weeks) | 添加或者减去周 | -| public LocalDateTime minusYears (long years) | 减去或者添加年 | -| public LocalDateTime withYear(int year) | 直接修改年 | -| public LocalDateTime withMonth(int month) | 直接修改月 | -| public LocalDateTime withDayOfMonth(int dayofmonth) | 直接修改日期(一个月中的第几天) | -| public LocalDateTime withDayOfYear(int dayOfYear) | 直接修改日期(一年中的第几天) | -| public LocalDateTime withHour(int hour) | 直接修改小时 | -| public LocalDateTime withMinute(int minute) | 直接修改分钟 | -| public LocalDateTime withSecond(int second) | 直接修改秒 | +| 方法名 | 说明 | +| ------------------------------------------- | -------------- | +| public LocalDateTime plusYears (long years) | 添加或者减去年 | +| public LocalDateTime withYear(int year) | 直接修改年 | @@ -3142,6 +3173,10 @@ public class MathDemo { +**** + + + ### DecimalFormat 使任何形式的数字解析和格式化 @@ -3175,42 +3210,6 @@ public static void main(String[]args){ -*** - - - -### System - -System代表当前系统。 - -静态方法: - -1. `public static void exit(int status)` : 终止JVM虚拟机,非0是异常终止 -2. `public static long currentTimeMillis()` : 获取当前系统此刻时间毫秒值 -3. `static void arraycopy(Object var0, int var1, Object var2, int var3, int var4)` : 数组拷贝 - 参数一:原数组 - 参数二:从原数组的哪个位置开始赋值。 - 参数三:目标数组 - 参数四:从目标数组的哪个位置开始赋值 - 参数五:赋值几个。 - -```java -public class SystemDemo { - public static void main(String[] args) { - //System.exit(0); // 0代表正常终止!! - long startTime = System.currentTimeMillis();//定义sdf 按照格式输出 - for(int i = 0; i < 10000; i++){输出i} - long endTime = new Date().getTime(); - System.out.println( (endTime - startTime)/1000.0 +"s");//程序用时 - - int[] arr1 = new int[]{10 ,20 ,30 ,40 ,50 ,60 ,70}; - int[] arr2 = new int[6]; // [ 0 , 0 , 0 , 0 , 0 , 0] - // 变成arrs2 = [0 , 30 , 40 , 50 , 0 , 0 ] - System.arraycopy(arr1, 2, arr2, 1, 3); - } -} -``` - *** @@ -3233,8 +3232,8 @@ Java 在 java.math 包中提供的 API 类,用来对超过16位有效位的数 * `public BigDecimal subtract(BigDecimal value)`:减法运算 * `public BigDecimal multiply(BigDecimal value)`:乘法运算 * `public BigDecimal divide(BigDecimal value)`:除法运算 -* `public double doubleValue()`:把BigDecimal转换成double类型。 -* `public int intValue()`:转为int 其他类型相同 +* `public double doubleValue()`:把 BigDecimal 转换成 double 类型 +* `public int intValue()`:转为 int 其他类型相同 * `public BigDecimal divide (BigDecimal value,精确几位,舍入模式)`:除法 ```java @@ -3261,20 +3260,20 @@ public class BigDecimalDemo { } ``` -总结 +总结: 1. BigDecimal 是用来进行精确计算的 -2. 创建 BigDecimal 的对象,构造方法使用参数类型为字符串的。 -3. 四则运算中的除法,如果除不尽请使用 divide 的三个参数的方法。 +2. 创建 BigDecimal 的对象,构造方法使用参数类型为字符串的 +3. 四则运算中的除法,如果除不尽请使用 divide 的三个参数的方法 ```java BigDecimal divide = bd1.divide(参与运算的对象,小数点后精确到多少位,舍入模式); -参数1:表示参与运算的BigDecimal 对象。 -参数2:表示小数点后面精确到多少位 -参数3:舍入模式 - BigDecimal.ROUND_UP 进一法 - BigDecimal.ROUND_FLOOR 去尾法 - BigDecimal.ROUND_HALF_UP 四舍五入 +//参数1:表示参与运算的BigDecimal 对象。 +//参数2:表示小数点后面精确到多少位 +//参数3:舍入模式 +// BigDecimal.ROUND_UP 进一法 +// BigDecimal.ROUND_FLOOR 去尾法 +// BigDecimal.ROUND_HALF_UP 四舍五入 ``` @@ -3718,6 +3717,8 @@ public class RegexDemo { + + ## 集合 ### 集合概述 @@ -3763,7 +3764,7 @@ public class RegexDemo { * 二叉树:binary tree 永远只有一个根节点,是每个结点不超过2个节点的树(tree) 特点:二叉排序树:小的左边,大的右边,但是可能树很高,性能变差,为了做排序和搜索会进行左旋和右旋实现平衡查找二叉树,让树的高度差不大于1 - + * 红黑树(基于红黑规则实现自平衡的排序二叉树):树保证到了很矮小,但是又排好序,性能最高的树 特点:**红黑树的增删查改性能都好** @@ -5748,15 +5749,13 @@ Tomcat 中的 ConcurrentCache 使用了 WeakHashMap 来实现缓存功能,Conc * **泛型和集合都只能支持引用数据类型,不支持基本数据类型。** ```java -{ - ArrayList lists = new ArrayList<>(); - lists.add(99.9); - lists.add('a'); - lists.add("Java"); - ArrayList list = new ArrayList<>(); - lists1.add(10); - lists1.add(20); -} +ArrayList lists = new ArrayList<>(); +lists.add(99.9); +lists.add('a'); +lists.add("Java"); +ArrayList list = new ArrayList<>(); +lists1.add(10); +lists1.add(20); ``` 优点:泛型在编译阶段约束了操作的数据类型,从而不会出现类型转换异常,体现的是 Java 的严谨性和规范性 @@ -5913,73 +5912,13 @@ class Dog{} -### 不可变 - -在 List、Set、Map 接口中都存在 of 方法,可以创建一个不可变的集合 -+ 这个集合不能添加,不能删除,不能修改 -+ 但是可以结合集合的带参构造,实现集合的批量添加 - -在 Map 接口中,还有一个 ofEntries 方法可以提高代码的阅读性 -+ 首先会把键值对封装成一个 Entry 对象,再把这个 Entry 对象添加到集合当中 - -````java -public class MyVariableParameter4 { - public static void main(String[] args) { - // static List of(E…elements) 创建一个具有指定元素的List集合对象 - //static Set of(E…elements) 创建一个具有指定元素的Set集合对象 - //static Map of(E…elements) 创建一个具有指定元素的Map集合对象 - - //method1(); - //method2(); - //method3(); - //method4(); - - } - - private static void method4() { - Map map = Map.ofEntries( - Map.entry("zhangsan", "江苏"), - Map.entry("lisi", "北京")); - System.out.println(map); - } - - private static void method3() { - Map map = Map.of("zhangsan", "江苏", "lisi", "北京"); - System.out.println(map); - } - - private static void method2() { - //传递的参数当中,不能存在重复的元素。 - Set set = Set.of("a", "b", "c", "d","a"); - System.out.println(set); - } - - private static void method1() { - List list = List.of("a", "b", "c", "d"); - System.out.println(list); - - //集合的批量添加。 - //首先是通过调用List.of方法来创建一个不可变的集合,of方法的形参就是一个可变参数。 - //再创建一个ArrayList集合,并把这个不可变的集合中所有的数据,都添加到ArrayList中。 - ArrayList list3 = new ArrayList<>(List.of("a", "b", "c", "d")); - System.out.println(list3); - } -} -```` - - - - - -*** - ## 异常 ### 基本介绍 -异常:程序在"编译"或者"执行"的过程中可能出现的问题,Java 为常见的代码异常都设计一个类来代表。 +异常:程序在编译或者执行的过程中可能出现的问题,Java 为常见的代码异常都设计一个类来代表。 错误:Error ,程序员无法处理的错误,只能重启系统,比如内存奔溃,JVM 本身的奔溃 @@ -5998,7 +5937,7 @@ Java 中异常继承的根类是:Throwable Exception 异常的分类: * 编译时异常:继承自 Exception 的异常或者其子类,编译阶段就会报错 -* 运行时异常: 继承自 RuntimeException 的异常或者其子类,编译阶段是不会出错的,在运行时阶段可能出现,编译阶段是不会出错的,但是运行阶段可能出现,建议提前处理 +* 运行时异常:继承自 RuntimeException 的异常或者其子类,编译阶段是不会出错的,在运行阶段出错 @@ -6011,9 +5950,9 @@ Exception 异常的分类: 异常的产生默认的处理过程解析:(自动处理的过程) 1. 默认会在出现异常的代码那里自动的创建一个异常对象:ArithmeticException(算术异常) -2. 异常会从方法中出现的点这里抛出给调用者,调用者最终抛出给JVM虚拟机 +2. 异常会从方法中出现的点这里抛出给调用者,调用者最终抛出给 JVM 虚拟机 3. 虚拟机接收到异常对象后,先在控制台直接输出**异常栈**信息数据 -4. 直接从当前执行的异常点干掉当前程序 +4. 直接从当前执行的异常点终止当前程序 5. 后续代码没有机会执行了,因为程序已经死亡 ```java @@ -6040,12 +5979,9 @@ public class ExceptionDemo { #### 基本介绍 -编译时异常:继承自Exception的异常或者其子类,没有继承 RuntimeException,编译时异常是编译阶段就会报错,必须程序员编译阶段就处理的。否则代码编译就报错 - -编译时异常的作用是什么: +编译时异常:继承自 Exception 的异常或者其子类,没有继承 RuntimeException,编译时异常是编译阶段就会报错 -* 是担心程序员的技术不行,在编译阶段就爆出一个错误, 目的在于提醒 -* 提醒程序员这里很可能出错,请检查并注意不要出bug +编译时异常的作用是什么:在编译阶段就爆出一个错误,目的在于提醒,请检查并注意不要出 BUG ```java public static void main(String[] args) throws ParseException { @@ -6066,12 +6002,9 @@ public static void main(String[] args) throws ParseException { ##### throws -在出现编译时异常的地方层层把异常抛出去给调用者,调用者最终抛出给JVM虚拟机,JVM 虚拟机输出异常信息,直接干掉程序,这种方式与默认方式是一样的。 +在出现编译时异常的地方层层把异常抛出去给调用者,调用者最终抛出给 JVM 虚拟机,JVM 虚拟机输出异常信息,直接终止掉程序,这种方式与默认方式是一样的 -* 优点:可以解决代码编译时的错误 -* 运行时出现异常,程序还是会立即死亡! - -**Exception是异常最高类型可以抛出一切异常!** +**Exception是异常最高类型可以抛出一切异常** ```java public static void main(String[] args) throws Exception { @@ -6091,7 +6024,7 @@ public static void main(String[] args) throws Exception { ##### try/catch -可以处理异常,并且出现异常后代码也不会死亡。 +可以处理异常,并且出现异常后代码也不会死亡 * 自己捕获异常和处理异常的格式:**捕获处理** @@ -6107,9 +6040,8 @@ public static void main(String[] args) throws Exception { } ``` -* 监视捕获处理异常企业级写法: - Exception可以捕获处理一切异常类型! - +* 监视捕获处理异常写法:Exception 可以捕获处理一切异常类型 + ```java try{ // 可能出现异常的代码! @@ -6119,9 +6051,10 @@ public static void main(String[] args) throws Exception { ``` **Throwable成员方法:** - `public String getMessage()` : 返回此 throwable 的详细消息字符串 - `public String toString()` : 返回此可抛出的简短描述 - `public void printStackTrace()` : 把异常的错误信息输出在控制台 + +* `public String getMessage()`:返回此 throwable 的详细消息字符串 +* `public String toString()`:返回此可抛出的简短描述 +* `public void printStackTrace()`:把异常的错误信息输出在控制台 ```java public static void main(String[] args) { @@ -6146,8 +6079,7 @@ public static void main(String[] args) { ##### 规范做法 -在出现异常的地方把异常一层一层的抛出给最外层调用者,最外层调用者集中捕获处理!(**规范做法**) -这种方案最外层调用者可以知道底层执行的情况,同时程序在出现异常后也不会立即死亡(最好的方案) +在出现异常的地方把异常一层一层的抛出给最外层调用者,最外层调用者集中捕获处理 ```java public class ExceptionDemo{ @@ -6174,16 +6106,16 @@ public class ExceptionDemo{ #### 基本介绍 -继承自RuntimeException的异常或者其子类,编译阶段是不会出错的,它是在运行时阶段可能出现的错误,运行时异常编译阶段可以处理也可以不处理,代码编译都能通过!! +继承自 RuntimeException 的异常或者其子类,编译阶段是不会出错的,是在运行时阶段可能出现的错误,运行时异常编译阶段可以处理也可以不处理,代码编译都能通过 **常见的运行时异常**: -1. 数组索引越界异常: ArrayIndexOutOfBoundsException -2. 空指针异常 : NullPointerException,直接输出没问题,调用空指针的变量的功能就会报错! +1. 数组索引越界异常:ArrayIndexOutOfBoundsException +2. 空指针异常:NullPointerException,直接输出没问题,调用空指针的变量的功能就会报错 3. 类型转换异常:ClassCastException 4. 迭代器遍历没有此元素异常:NoSuchElementException 5. 算术异常(数学操作异常):ArithmeticException -6. 数字转换异常: NumberFormatException +6. 数字转换异常:NumberFormatException ```java public class ExceptionDemo { @@ -6267,11 +6199,9 @@ catch:0-N次 (如果有finally那么catch可以没有!!) finally: 0-1次 ``` +**finally 的作用**:可以在代码执行完毕以后进行资源的释放操作 - -**finally的作用**:可以在代码执行完毕以后进行资源的释放操作 - -资源:资源都是实现了 Closeable 接口的,都自带 close() 关闭方法! +资源:资源都是实现了 Closeable 接口的,都自带 close() 关闭方法 注意:如果在 finally 中出现了 return,会吞掉异常 @@ -6322,10 +6252,10 @@ public class FinallyDemo { 自定义异常: -* 自定义编译时异常:定义一个异常类继承 Exception,重写构造器,在出现异常的地方用throw new 自定义对象抛出 -* 自定义运行时异常:定义一个异常类继承 RuntimeException,重写构造器,在出现异常的地方用 throw new 自定义对象抛出! +* 自定义编译时异常:定义一个异常类继承 Exception,重写构造器,在出现异常的地方用 throw new 自定义对象抛出 +* 自定义运行时异常:定义一个异常类继承 RuntimeException,重写构造器,在出现异常的地方用 throw new 自定义对象抛出 -**throws: 用在方法上,用于抛出方法中的异常** +**throws:用在方法上,用于抛出方法中的异常** **throw: 用在出现异常的地方,创建异常对象且立即从此处抛出** @@ -6412,17 +6342,19 @@ public class Demo{ + + ## λ ### lambda #### 基本介绍 -Lambda表达式是JDK1.8开始之后的新技术,是一种代码的新语法,一种特殊写法 +Lambda 表达式是 JDK1.8 开始之后的新技术,是一种代码的新语法,一种特殊写法 作用:为了简化匿名内部类的代码写法 -Lambda表达式的格式: +Lambda 表达式的格式: ```java (匿名内部类被重写方法的形参列表) -> { @@ -6430,11 +6362,11 @@ Lambda表达式的格式: } ``` -Lambda表达式并不能简化所有匿名内部类的写法,只能简化**函数式接口的匿名内部类** +Lambda 表达式并不能简化所有匿名内部类的写法,只能简化**函数式接口的匿名内部类** 简化条件:首先必须是接口,接口中只能有一个抽象方法 -@FunctionalInterface函数式接口注解:一旦某个接口加上了这个注解,这个接口只能有且仅有一个抽象方法 +@FunctionalInterface 函数式接口注解:一旦某个接口加上了这个注解,这个接口只能有且仅有一个抽象方法 @@ -6444,9 +6376,9 @@ Lambda表达式并不能简化所有匿名内部类的写法,只能简化**函 #### 简化方法 -Lambda表达式的省略写法(进一步在Lambda表达式的基础上继续简化) +Lambda 表达式的省略写法(进一步在 Lambda 表达式的基础上继续简化) -* 如果Lambda表达式的方法体代码只有一行代码,可以省略大括号不写,同时要省略分号;如果这行代码是return语句,必须省略return不写 +* 如果 Lambda 表达式的方法体代码只有一行代码,可以省略大括号不写,同时要省略分号;如果这行代码是 return 语句,必须省略 return 不写 * 参数类型可以省略不写 * 如果只有一个参数,参数类型可以省略,同时()也可以省略 @@ -6486,35 +6418,7 @@ names.forEach(s -> System.out.println(s) ); #### 常用简化 -##### Runnable - -```java -//1. -Thread t = new Thread(new Runnable() { - @Override - public void run() { - System.out.println(Thread.currentThread().getName()+":执行~~~"); - } -}); -t.start(); - -//2. -Thread t1 = new Thread(() -> { - System.out.println(Thread.currentThread().getName()+":执行~~~"); -}); -t1.start(); -//3. -new Thread(() -> { - System.out.println(Thread.currentThread().getName()+":执行~~~"); -}).start(); - -//4.一行代码 -new Thread(() -> System.out.println(Thread.currentThread().getName()+":执行~~~")).start(); -``` - - - -##### Comparator +Comparator ```java public class CollectionsDemo { @@ -6550,7 +6454,7 @@ public class CollectionsDemo { #### 基本介绍 -方法引用:方法引用是为了进一步简化Lambda表达式的写法 +方法引用:方法引用是为了进一步简化 Lambda 表达式的写法 方法引用的格式:类型或者对象::引用的方法 @@ -6708,6 +6612,8 @@ public class ConstructorDemo { + + ## I/O ### Stream @@ -6895,9 +6801,9 @@ public static void main(String[] args) { File 类:代表操作系统的文件对象,是用来操作操作系统的文件对象的,删除文件,获取文件信息,创建文件(文件夹),广义来说操作系统认为文件包含(文件和文件夹) File 类构造器: - `public File(String pathname)`:根据路径获取文件对象 - `public File(String parent , String child)`:根据父路径和文件名称获取文件对象! - `public File(File parent , String child)` + +* `public File(String pathname)`:根据路径获取文件对象 +* `public File(String parent , String child)`:根据父路径和文件名称获取文件对象 File 类创建文件对象的格式: @@ -6945,17 +6851,19 @@ public class FileDemo{ ##### 常用方法 -`public String getAbsolutePath()` : 返回此File的绝对路径名字符串。 -`public String getPath()` : 获取创建文件对象的时候用的路径 -`public String getName()` : 返回由此File表示的文件或目录的名称。 -`public long length()` : 返回由此File表示的文件的长度(大小)。 -`public long length(FileFilter filter)` : 文件过滤器。 +| 方法 | 说明 | +| ------------------------------ | -------------------------------------- | +| String getAbsolutePath() | 返回此 File 的绝对路径名字符串 | +| String getPath() | 获取创建文件对象的时候用的路径 | +| String getName() | 返回由此 File 表示的文件或目录的名称 | +| long length() | 返回由此 File 表示的文件的长度(大小) | +| long length(FileFilter filter) | 文件过滤器 | ```java public class FileDemo { public static void main(String[] args) { // 1.绝对路径创建一个文件对象 - File f1 = new File("E:/图片/meinv.jpg"); + File f1 = new File("E:/图片/test.jpg"); // a.获取它的绝对路径。 System.out.println(f1.getAbsolutePath()); // b.获取文件定义的时候使用的路径。 @@ -6967,7 +6875,7 @@ public class FileDemo { System.out.println("------------------------"); // 2.相对路径 - File f2 = new File("Day09Demo/src/dlei01.txt"); + File f2 = new File("Demo/src/test.txt"); // a.获取它的绝对路径。 System.out.println(f2.getAbsolutePath()); // b.获取文件定义的时候使用的路径。 @@ -6983,14 +6891,20 @@ public class FileDemo { +*** + + + ##### 判断方法 -`public boolean exists()` : 此File表示的文件或目录是否实际存在。 -`public boolean isDirectory()` : 此File表示的是否为目录。 -`public boolean isFile()` : 此File表示的是否为文件 +方法列表: + +* `boolean exists()`:此 File 表示的文件或目录是否实际存在 +* `boolean isDirectory()`:此 File 表示的是否为目录 +* `boolean isFile()`:此 File 表示的是否为文件 ```java -File f = new File("Day09Demo/src/dlei01.txt"); +File f = new File("Demo/src/test.txt"); // a.判断文件路径是否存在 System.out.println(f.exists()); // true // b.判断文件对象是否是文件,是文件返回true ,反之 @@ -7001,17 +6915,23 @@ System.out.println(f.isDirectory()); // false +**** + + + ##### 创建删除 -`public boolean createNewFile()` : 当且仅当具有该名称的文件尚不存在时, 创建一个新的空文件。 -`public boolean delete()` : 删除由此File表示的文件或目录。 (只能删除空目录) -`public boolean mkdir()` : 创建由此File表示的目录。(只能创建一级目录) -`public boolean mkdirs()` : 可以创建多级目录(建议使用的) +方法列表: + +* `boolean createNewFile()`:当且仅当具有该名称的文件尚不存在时, 创建一个新的空文件 +* `boolean delete()`:删除由此 File 表示的文件或目录(只能删除空目录) +* `boolean mkdir()`:创建由此 File 表示的目录(只能创建一级目录) +* `boolean mkdirs()`:可以创建多级目录(建议使用) ```java public class FileDemo { public static void main(String[] args) throws IOException { - File f = new File("Day09Demo/src/dlei02.txt"); + File f = new File("Demo/src/test.txt"); // a.创建新文件,创建成功返回true ,反之 System.out.println(f.createNewFile()); @@ -7041,7 +6961,7 @@ public class FileDemo { #### 遍历目录 - `public String[] list()`:获取当前目录下所有的"一级文件名称"到一个字符串数组中去返回。 -- `public File[] listFiles()(常用)`:获取当前目录下所有的"一级文件对象"到一个**文件对象数组**中去返回(**重点**) +- `public File[] listFiles()`:获取当前目录下所有的"一级文件对象"到一个**文件对象数组**中去返回(**重点**) - `public long lastModified`:返回此抽象路径名表示的文件上次修改的时间。 ```java @@ -7060,7 +6980,7 @@ public class FileDemo { } // c - File f1 = new File("D:\\it\\图片资源\\beautiful.jpg"); + File f1 = new File("D:\\图片资源\\beautiful.jpg"); long time = f1.lastModified(); // 最后修改时间! SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); System.out.println(sdf.format(time)); @@ -7168,30 +7088,23 @@ IO 输入输出流:输入/输出流 * Input:输入 * Output:输出 -引入:File类只能操作文件对象本身,不能读写文件对象的内容,读写数据内容,应该使用IO流 +引入:File 类只能操作文件对象本身,不能读写文件对象的内容,读写数据内容,应该使用 IO 流 IO 流是一个水流模型:IO 理解成水管,把数据理解成水流 IO 流的分类: * 按照流的方向分为:输入流,输出流。 - * 输出流:以内存为基准,把内存中的数据写出到磁盘文件或者网络介质中去的流称为输出流 - 输出流的作用:写数据到文件,或者写数据发送给别人 - * 输入流:以内存为基准,把磁盘文件中的数据或者网络中的数据读入到内存中的流称为输入流 - 输入流的作用:读取数据到内存 + * 输出流:以内存为基准,把内存中的数据**写出到磁盘文件**或者网络介质中去的流称为输出流 + * 输入流:以内存为基准,把磁盘文件中的数据或者网络中的数据**读入到内存**中的流称为输入流 * 按照流的内容分为:字节流,字符流 * 字节流:流中的数据的最小单位是一个一个的字节,这个流就是字节流 * 字符流:流中的数据的最小单位是一个一个的字符,这个流就是字符流(**针对于文本内容**) -流大体分为四大类: - -* 字节输入流:以内存为基准,把磁盘文件中的数据或者网络中的数据以一个一个的字节的形式读入到内存中去的流称为字节输入流 -* 字节输出流:以内存为基准,把内存中的数据以一个一个的字节写出到磁盘文件或者网络介质中去的流称为字节输出流 -* 字符输入流:以内存为基准,把磁盘文件中的数据或者网络中的数据以一个一个的字符的形式读入到内存中去的流称为字符输入流 -* 字符输出流:以内存为基准,把内存中的数据以一个一个的字符写出到磁盘文件或者网络介质中去的流称为字符输出流 +流大体分为四大类:字节输入流、字节输出流、字符输入流、字符输出流 ```java -IO流的体系: +IO 流的体系: 字节流 字符流 字节输入流 字节输出流 字符输入流 字符输出流 InputStream OutputStream Reader Writer (抽象类) @@ -7211,25 +7124,25 @@ ObjectInputStream ObjectOutputStream ##### 字节输入 -FileInputStream 文件字节输入流: +FileInputStream 文件字节输入流:以内存为基准,把磁盘文件中的数据按照字节的形式读入到内存中的流 + +构造方法: -* 作用:以内存为基准,把磁盘文件中的数据按照字节的形式读入到内存中的流 +* `public FileInputStream(File path)`:创建一个字节输入流管道与源文件对象接通 +* `public FileInputStream(String pathName)`:创建一个字节输入流管道与文件路径对接,底层实质上创建 File 对象 -* 构造器: - `public FileInputStream(File path)` : 创建一个字节输入流管道与源文件对象接通 - `public FileInputStream(String pathName)` : 创建一个字节输入流管道与文件路径对接,底层实质上创建了File对象 - -* 方法: - `public int read()` : 每次读取一个字节返回,读取完毕会返回-1 - `public int read(byte[] buffer)` : 从字节输入流中读取字节到字节数组中去,返回读取的字节数量,没有字节可读返回-1,**byte中新读取的数据默认是覆盖原数据**,构造String需要设定长度 - `public String(byte[] bytes,int offset,int length)` : 构造新的String - `public long transferTo(OutputStream out) ` : 从输入流中读取所有字节,并按读取的顺序,将字节写入给定的输出流`is.transferTo(os)` +方法: + +* `public int read()`:每次读取一个字节返回,读取完毕会返回-1 +* `public int read(byte[] buffer)`:从字节输入流中读取字节到字节数组中去,返回读取的字节数量,没有字节可读返回 -1,**byte 中新读取的数据默认是覆盖原数据**,构造 String 需要设定长度 +* `public String(byte[] bytes,int offset,int length)`:构造新的 String +* `public long transferTo(OutputStream out) `:从输入流中读取所有字节,并按读取的顺序,将字节写入给定的输出流 ```java public class FileInputStreamDemo01 { public static void main(String[] args) throws Exception { // 1.创建文件对象定位dlei01.txt - File file = new File("Day09Demo/src/dlei01.txt"); + File file = new File("Demo/src/dlei01.txt"); // 2.创建一个字节输入流管道与源文件接通 InputStream is = new FileInputStream(file); // 3.读取一个字节的编号返回,读取完毕返回-1 @@ -7246,14 +7159,14 @@ public class FileInputStreamDemo01 { } ``` -一个一个字节读取英文和数字没有问题。但是一旦读取中文输出无法避免乱码,因为会截断中文的字节。一个一个字节的读取数据,性能也较差,所以**禁止使用上面的方案** +一个一个字节读取英文和数字没有问题,但是读取中文输出无法避免乱码,因为会截断中文的字节。一个一个字节的读取数据,性能也较差,所以**禁止使用上面的方案** 采取下面的方案: ```java public static void main(String[] args) throws Exception { //简化写法,底层实质上创建了File对象 - InputStream is = new FileInputStream("Day09Demo/src/dlei01.txt"); + InputStream is = new FileInputStream("Demo/src/test.txt"); byte[] buffer = new byte[3];//开发中使用byte[1024] int len; while((len = is.read(buffer)) !=-1){ @@ -7265,17 +7178,9 @@ public static void main(String[] args) throws Exception { ``` ```java -//定义一个字节数组与文件的大小刚刚一样大,然后一桶水读取全部字节数据再输出! -//可以避免中文读取输出乱码,但是如果读取的文件过大,会出现内存溢出!! -//字节流并不适合读取文本文件内容输出,读写文件内容建议使用字符流。 -/* - byte[] buffer = new byte[(int) f.length()]; - int len = is.read(buffer); - String rs = new String(buffer); -*/ - -File f = new File("Day09Demo/src/dlei03.txt"); +File f = new File("Demo/src/test.txt"); InputStream is = new FileInputStream(f); +// 读取全部的 byte[] buffer = is.readAllBytes(); String rs = new String(buffer); System.out.println(rs); @@ -7283,35 +7188,39 @@ System.out.println(rs); +**** + + + ##### 字节输出 -FileOutputStream 文件字节输出流: +FileOutputStream 文件字节输出流:以内存为基准,把内存中的数据,按照字节的形式写出到磁盘文件中去 + +构造方法: + +* `public FileOutputStream(File file)`:创建一个字节输出流管道通向目标文件对象 +* `public FileOutputStream(String file) `:创建一个字节输出流管道通向目标文件路径 +* `public FileOutputStream(File file, boolean append)` : 创建一个追加数据的字节输出流管道到目标文件对象 +* `public FileOutputStream(String file, boolean append)` : 创建一个追加数据的字节输出流管道通向目标文件路径 -* 作用:以内存为基准,把内存中的数据,按照字节的形式写出到磁盘文件中去 +API: -* 构造器: - `public FileOutputStream(File file)` : 创建一个字节输出流管道通向目标文件对象 - `public FileOutputStream(String file) ` : 创建一个字节输出流管道通向目标文件路径 - `public FileOutputStream(File file , boolean append)` : 追加数据的字节输出流管道到目标文件对象 - `public FileOutputStream(String file , boolean append)` : 创建一个追加数据的字节输出流管道通向目标文件路径 -* API: - `public void write(int a)` : 写一个字节出去 - `public void write(byte[] buffer)` :写一个字节数组出去 - `public void write(byte[] buffer , int pos , int len)` : 写一个字节数组的一部分出去 - 参数一,字节数组;参数二:起始字节索引位置,参数三:写多少个字节数出去。 +* `public void write(int a)`:写一个字节出去 +* `public void write(byte[] buffer)`:写一个字节数组出去 +* `public void write(byte[] buffer , int pos , int len)`:写一个字节数组的一部分出去,从 pos 位置,写出 len 长度 -* FileOutputStream字节输出流每次启动写数据的时候都会先清空之前的全部数据,重新写入: - `OutputStream os = new FileOutputStream("Day09Demo/out05")` : 覆盖数据管道 - `OutputStream os = new FileOutputStream("Day09Demo/out05" , true)` : 追加数据的管道 +* FileOutputStream 字节输出流每次启动写数据的时候都会先清空之前的全部数据,重新写入: + * `OutputStream os = new FileOutputStream("Demo/out05")`:覆盖数据管道 + * `OutputStream os = new FileOutputStream("Demo/out05" , true)`:追加数据的管道 说明: -* 字节输出流只能写字节出去,字节输出流默认是**覆盖**数据管道。 -* 换行用: **os.write("\r\n".getBytes());** -* 关闭和刷新:刷新流可以继续使用,关闭包含刷新数据但是流就不能使用了! +* 字节输出流只能写字节出去,字节输出流默认是**覆盖**数据管道 +* 换行用:**os.write("\r\n".getBytes())** +* 关闭和刷新:刷新流可以继续使用,关闭包含刷新数据但是流就不能使用了 ```java -OutputStream os = new FileOutputStream("Day09Demo/out05"); +OutputStream os = new FileOutputStream("Demo/out05"); os.write(97);//a os.write('b'); os.write("\r\n".getBytes()); @@ -7323,14 +7232,7 @@ os.close(); ##### 文件复制 -思想:字节是计算机中一切文件的组成,所以字节流适合做一切文件的复制 - -分析步骤: - (1)创建一个字节输入流管道与源文件接通。 - (2)创建一个字节输出流与目标文件接通。 - (3)创建一个字节数组作为桶 - (4)从字节输入流管道中读取数据,写出到字节输出流管道即可。 - (5)关闭资源! +字节是计算机中一切文件的组成,所以字节流适合做一切文件的复制 ```java public class CopyDemo01 { @@ -7339,9 +7241,9 @@ public class CopyDemo01 { OutputStream os = null ; try{ //(1)创建一个字节输入流管道与源文件接通。 - is = new FileInputStream("D:\\seazean\\图片资源\\meinv.jpg"); + is = new FileInputStream("D:\\seazean\\图片资源\\test.jpg"); //(2)创建一个字节输出流与目标文件接通。 - os = new FileOutputStream("D:\\seazean\\meimei.jpg"); + os = new FileOutputStream("D:\\seazean\\test.jpg"); //(3)创建一个字节数组作为桶 byte buffer = new byte[1024]; //(4)从字节输入流管道中读取数据,写出到字节输出流管道即可 @@ -7375,32 +7277,30 @@ public class CopyDemo01 { ##### 字符输入 -FileReader:文件字符输入流 +FileReader:文件字符输入流,以内存为基准,把磁盘文件的数据以字符的形式读入到内存,读取文本文件内容到内存中去 + +构造器: + +* `public FileReader(File file)`:创建一个字符输入流与源文件对象接通。 +* `public FileReader(String filePath)`:创建一个字符输入流与源文件路径接通。 + +方法: + +* `public int read()`:读取一个字符的编号返回,读取完毕返回 -1 +* `public int read(char[] buffer)`:读取一个字符数组,读取多少个就返回多少个,读取完毕返回 -1 + +结论: + +* 字符流一个一个字符的读取文本内容输出,可以解决中文读取输出乱码的问题,适合操作文本文件,但是一个一个字符的读取文本内容性能较差 +* 字符流按照**字符数组循环读取数据**,可以解决中文读取输出乱码的问题,而且性能也较好 - * 作用:以内存为基准,把磁盘文件的数据以字符的形式读入到内存,读取文本文件内容到内存中去。 - * 构造器: - `public FileReader(File file)` : 创建一个字符输入流与源文件对象接通。 - `public FileReader(String filePath)` : 创建一个字符输入流与源文件路径接通。 - * 方法: - `public int read()` : 读取一个字符的编号返回! 读取完毕返回 -1 - `public int read(char[] buffer)` : 读取一个字符数组,读取多少个就返回多少个,读取完毕返回 -1 - * 结论: - 字符流一个一个字符的读取文本内容输出,可以解决中文读取输出乱码的问题,适合操作文本文件。 - 但是:一个一个字符的读取文本内容性能较差!! - 字符流按照**字符数组循环读取数据**,可以解决中文读取输出乱码的问题,而且性能也较好!! - * **字符流不能复制图片,视频等类型的文件**。字符流在读取完了字节数据后并没有直接往目的地写,而是先查编码表,查到对应的数据就将该数据写入目的地。如果查不到,则码表会将一些未知区域中的数据去map这些字节数据,然后写到目的地,这样的话就造成了源数据和目的数据的不一致。 +**字符流不能复制图片,视频等类型的文件**。字符流在读取完了字节数据后并没有直接往目的地写,而是先查编码表,查到对应的数据就将该数据写入目的地。如果查不到,则码表会将一些未知区域中的数据去 map 这些字节数据,然后写到目的地,这样的话就造成了源数据和目的数据的不一致。 ```java public class FileReaderDemo01{//字符 public static void main(String[] args) throws Exception { - // 1.创建一个文件对象定位源文件 - // File f = new File("Day10Demo/src/dlei01.txt"); - // 2.创建一个字符输入流管道与源文件接通 - // Reader fr = new FileReader(f); - // 3.简化写法:创建一个字符输入流管道与源文件路径接通 - Reader fr = new FileReader("Day10Demo/src/dlei01.txt"); - //int code1 = fr.read(); - //System.out.print((char)code1); + // 创建一个字符输入流管道与源文件路径接通 + Reader fr = new FileReader("Demo/src/test.txt"); int ch; while((ch = fr.read()) != -1){ System.out.print((char)ch); @@ -7409,13 +7309,8 @@ public class FileReaderDemo01{//字符 } public class FileReaderDemo02 {//字符数组 public static void main(String[] args) throws Exception { - Reader fr = new FileReader("Day10Demo/src/dlei01.txt"); + Reader fr = new FileReader("Demo/src/test.txt"); - //char[] buffer = new char[3]; - //int len = fr.read(buffer); - //System.out.println("字符数:"+len); - //String rs = new String(buffer,0,len); - //System.out.println(rs); char[] buffer = new char[1024]; int len; while((len = fr.read(buffer)) != -1) { @@ -7427,30 +7322,33 @@ public class FileReaderDemo02 {//字符数组 +*** + + + ##### 字符输出 -FileWriter:文件字符输出流 - -* 作用:以内存为基准,把内存中的数据按照字符的形式写出到磁盘文件中去 -* 构造器: - `public FileWriter(File file)` : 创建一个字符输出流管道通向目标文件对象 - `public FileWriter(String filePath)` : 创建一个字符输出流管道通向目标文件路径 - `public FileWriter(File file,boolean append)` : 创建一个追加数据的字符输出流管道通向文件对象 - `public FileWriter(String filePath,boolean append)` : 创建一个追加数据的字符输出流管道通向目标文件路径 -* 方法: - `public void write(int c)` : 写一个字符出去 - `public void write(String c)` : 写一个字符串出去 - `public void write(char[] buffer)` : 写一个字符数组出去 - `public void write(String c ,int pos ,int len)` : 写字符串的一部分出去 - `public void write(char[] buffer ,int pos ,int len)` : 写字符数组的一部分出去 -* 说明: - 覆盖数据管道:`Writer fw = new FileWriter("Day10Demo/src/dlei03.txt")` - 追加数据管道:`Writer fw = new FileWriter("Day10Demo/src/dlei03.txt",true)` - 换行:fw.write("\r\n"); // 换行 - 读写字符文件数据建议使用字符流 - -```java -Writer fw = new FileWriter("Day10Demo/src/dlei03.txt"); +FileWriter:文件字符输出流,以内存为基准,把内存中的数据按照字符的形式写出到磁盘文件中去 + +构造器: + +* `public FileWriter(File file)`:创建一个字符输出流管道通向目标文件对象(覆盖数据管道) +* `public FileWriter(String filePath)`:创建一个字符输出流管道通向目标文件路径 +* `public FileWriter(File file, boolean append)`:创建一个追加数据的字符输出流管道通向文件对象(追加数据管道) +* `public FileWriter(String filePath, boolean append)`:创建一个追加数据的字符输出流管道通向目标文件路径 + +方法: + +* `public void write(int c)`:写一个字符出去 +* `public void write(char[] buffer)`:写一个字符数组出去 +* `public void write(String c, int pos, int len)`:写字符串的一部分出去 +* `public void write(char[] buffer, int pos, int len)`:写字符数组的一部分出去 +* `fw.write("\r\n")`:换行 + +读写字符文件数据建议使用字符流 + +```java +Writer fw = new FileWriter("Demo/src/test.txt"); fw.write(97); // 字符a fw.write('b'); // 字符b fw.write("Java是最优美的语言!"); @@ -7468,14 +7366,14 @@ fw.close; ##### 基本介绍 -作用:缓冲流可以提高字节流和字符流的读写数据的性能。 +缓冲流可以提高字节流和字符流的读写数据的性能 缓冲流分为四类: -* BufferedInputStream:字节缓冲输入流,可以提高字节输入流读数据的性能。 -* BufferedOutStream: 字节缓冲输出流,可以提高字节输出流写数据的性能。 -* BufferedReader: 字符缓冲输入流,可以提高字符输入流读数据的性能。 -* BufferedWriter: 字符缓冲输出流,可以提高字符输出流写数据的性能。 +* BufferedInputStream:字节缓冲输入流,可以提高字节输入流读数据的性能 +* BufferedOutStream:字节缓冲输出流,可以提高字节输出流写数据的性能 +* BufferedReader:字符缓冲输入流,可以提高字符输入流读数据的性能 +* BufferedWriter:字符缓冲输出流,可以提高字符输出流写数据的性能 @@ -7487,7 +7385,7 @@ fw.close; 字节缓冲输入流:BufferedInputStream -作用:可以把低级的字节输入流包装成一个高级的缓冲字节输入流管道, 提高字节输入流读数据的性能 +作用:可以把低级的字节输入流包装成一个高级的缓冲字节输入流管道,提高字节输入流读数据的性能 构造器:`public BufferedInputStream(InputStream in)` @@ -7497,7 +7395,7 @@ fw.close; public class BufferedInputStreamDemo01 { public static void main(String[] args) throws Exception { // 1.定义一个低级的字节输入流与源文件接通 - InputStream is = new FileInputStream("Day10Demo/src/dlei04.txt"); + InputStream is = new FileInputStream("Demo/src/test.txt"); // 2.把低级的字节输入流包装成一个高级的缓冲字节输入流。 BufferInputStream bis = new BufferInputStream(is); // 3.定义一个字节数组按照循环读取。 @@ -7525,13 +7423,13 @@ public class BufferedInputStreamDemo01 { 构造器:`public BufferedOutputStream(OutputStream os)` -原理:缓冲字节输出流自带了8KB缓冲池,数据就直接写入到缓冲池中去,性能极高了 +原理:缓冲字节输出流自带了 8KB 缓冲池,数据就直接写入到缓冲池中去,性能提高了 ```java public class BufferedOutputStreamDemo02 { public static void main(String[] args) throws Exception { // 1.写一个原始的字节输出流 - OutputStream os = new FileOutputStream("Day10Demo/src/dlei05.txt"); + OutputStream os = new FileOutputStream("Demo/src/test.txt"); // 2.把低级的字节输出流包装成一个高级的缓冲字节输出流 BufferedOutputStream bos = new BufferedOutputStream(os); // 3.写数据出去 @@ -7548,14 +7446,14 @@ public class BufferedOutputStreamDemo02 { ##### 字节流性能 -利用字节流的复制统计各种写法形式下缓冲流的性能执行情况。 +利用字节流的复制统计各种写法形式下缓冲流的性能执行情况 复制流: -* 使用低级的字节流按照一个一个字节的形式复制文件。 -* 使用低级的字节流按照一个一个字节数组的形式复制文件。 -* 使用高级的缓冲字节流按照一个一个字节的形式复制文件。 -* 使用高级的缓冲字节流按照一个一个字节数组的形式复制文件。 +* 使用低级的字节流按照一个一个字节的形式复制文件 +* 使用低级的字节流按照一个一个字节数组的形式复制文件 +* 使用高级的缓冲字节流按照一个一个字节的形式复制文件 +* 使用高级的缓冲字节流按照一个一个字节数组的形式复制文件 高级的缓冲字节流按照一个一个字节数组的形式复制文件,性能最高,建议使用 @@ -7573,14 +7471,14 @@ public class BufferedOutputStreamDemo02 { 构造器:`public BufferedReader(Reader reader)` -原理:缓冲字符输入流默认会有一个8K的字符缓冲池,可以提高读字符的性能 +原理:缓冲字符输入流默认会有一个 8K 的字符缓冲池,可以提高读字符的性能 -按照行读取数据的功能:`public String readLine()` 读取一行数据返回,读取完毕返回null +按照行读取数据的功能:`public String readLine()` 读取一行数据返回,读取完毕返回 null ```java public static void main(String[] args) throws Exception { // 1.定义一个原始的字符输入流读取源文件 - Reader fr = new FileReader("Day10Demo/src/dlei06.txt"); + Reader fr = new FileReader("Demo/src/test.txt"); // 2.把低级的字符输入流管道包装成一个高级的缓冲字符输入流管道 BufferedReader br = new BufferedReader(fr); // 定义一个字符串变量存储每行数据 @@ -7611,13 +7509,13 @@ public static void main(String[] args) throws Exception { 构造器:`public BufferedWriter(Writer writer)` - 原理:高级的字符缓冲输出流多了一个8k的字符缓冲池,写数据性能极大提高了 + 原理:高级的字符缓冲输出流多了一个 8K 的字符缓冲池,写数据性能极大提高了 字符缓冲输出流多了一个换行的特有功能:`public void newLine()` **新建一行** ```java public static void main(String[] args) throws Exception { - Writer fw = new FileWriter("Day10Demo/src/dlei07.txt",true);//追加 + Writer fw = new FileWriter("Demo/src/test.txt",true);//追加 BufferedWriter bw = new BufferedWriter(fw); bw.write("我爱学习Java"); @@ -7667,8 +7565,8 @@ GBK GBK 不乱码! UTF-8 GBK 乱码! ``` -如果代码编码和读取的文件编码一致,字符流读取的时候不会乱码。 -如果代码编码和读取的文件编码不一致,字符流读取的时候会乱码。 +* 如果代码编码和读取的文件编码一致,字符流读取的时候不会乱码 +* 如果代码编码和读取的文件编码不一致,字符流读取的时候会乱码 @@ -7684,8 +7582,8 @@ UTF-8 GBK 乱码! 构造器: -* `public InputStreamReader(InputStream is)` : 使用当前代码默认编码 UTF-8 转换成字符流 -* `public InputStreamReader(InputStream is, String charset)` : 指定编码把字节流转换成字符流 +* `public InputStreamReader(InputStream is)`:使用当前代码默认编码 UTF-8 转换成字符流 +* `public InputStreamReader(InputStream is, String charset)`:指定编码把字节流转换成字符流 ```java public class InputStreamReaderDemo{ @@ -7719,11 +7617,11 @@ public class InputStreamReaderDemo{ 构造器: -* `public OutputStreamWriter(OutputStream os)` : 用默认编码 UTF-8 把字节输出流转换成字符输出流 -* `public OutputStreamWriter(OutputStream os, String charset)` : 指定编码把字节输出流转换成 +* `public OutputStreamWriter(OutputStream os)`:用默认编码 UTF-8 把字节输出流转换成字符输出流 +* `public OutputStreamWriter(OutputStream os, String charset)`:指定编码把字节输出流转换成 ```Java -OutputStream os = new FileOutputStream("Day10Demo/src/dlei07.txt"); +OutputStream os = new FileOutputStream("Demo/src/test.txt"); OutputStreamWriter osw = new OutputStreamWriter(os,"GBK"); osw.write("我在学习Java"); osw.close(); @@ -7739,11 +7637,11 @@ osw.close(); ##### 基本介绍 -对象序列化:把Java对象转换成字节序列的过程,将对象写入到IO流中。 对象 => 文件中 +对象序列化:把 Java 对象转换成字节序列的过程,将对象写入到 IO 流中。对象 => 文件中 -对象反序列化:把字节序列恢复为Java对象的过程,从IO流中恢复对象。 文件中 => 对象 +对象反序列化:把字节序列恢复为 Java 对象的过程,从 IO 流中恢复对象,文件中 => 对象 -transient 关键字修饰的成员变量,将不参与序列化! +transient 关键字修饰的成员变量,将不参与序列化 @@ -7755,7 +7653,7 @@ transient 关键字修饰的成员变量,将不参与序列化! 对象序列化流(对象字节输出流):ObjectOutputStream -作用:把内存中的Java对象数据保存到文件中去 +作用:把内存中的 Java 对象数据保存到文件中去 构造器:`public ObjectOutputStream(OutputStream out)` @@ -7769,7 +7667,7 @@ public class SerializeDemo01 { // 1.创建User用户对象 User user = new User("seazean","980823","七十一"); // 2.创建低级的字节输出流通向目标文件 - OutputStream os = new FileOutputStream("Day10Demo/src/obj.dat"); + OutputStream os = new FileOutputStream("Demo/src/obj.dat"); // 3.把低级的字节输出流包装成高级的对象字节输出流 ObjectOutputStream ObjectOutputStream oos = new ObjectOutputStream(os); // 4.通过对象字节输出流序列化对象: @@ -7787,7 +7685,7 @@ class User implements Serializable { private String loginName; private transient String passWord; private String userName; - ///get+set + // get+set } ``` @@ -7812,19 +7710,20 @@ byte[] bytes = bos.toByteArray(); 对象反序列化(对象字节输入流):ObjectInputStream -作用:读取序列化的对象文件恢复到Java对象中 +作用:读取序列化的对象文件恢复到 Java 对象中 构造器:`public ObjectInputStream(InputStream is)` 方法:`public final Object readObject()` 序列化版本号:`private static final long serialVersionUID = 2L` -说明:序列化使用的版本号和反序列化使用的版本号一致才可以正常反序列化,否则报错 + +注意:序列化使用的版本号和反序列化使用的版本号一致才可以正常反序列化,否则报错 ```java public class SerializeDemo02 { public static void main(String[] args) throws Exception { - InputStream is = new FileInputStream("Day10Demo/src/obj.dat"); + InputStream is = new FileInputStream("Demo/src/obj.dat"); ObjectInputStream ois = new ObjectInputStream(is); User user = (User)ois.readObject();//反序列化 System.out.println(user); @@ -7859,15 +7758,14 @@ class User implements Serializable { * `public PrintStream(OutputStream os)` * `public PrintStream(String filepath)` -System类: +System 类: * `public static void setOut(PrintStream out)`:让系统的输出流向打印流 ```java public class PrintStreamDemo01 { public static void main(String[] args) throws Exception { - PrintStream ps = new PrintStream("Day10Demo/src/dlei.txt"); - //PrintWriter pw = new PrintWriter("Day10Demo/src/dlei08.txt"); + PrintStream ps = new PrintStream("Demo/src/test.txt"); ps.println(任何类型的数据); ps.print(不换行); ps.write("我爱你".getBytes()); @@ -7877,7 +7775,7 @@ public class PrintStreamDemo01 { public class PrintStreamDemo02 { public static void main(String[] args) throws Exception { System.out.println("==seazean0=="); - PrintStream ps = new PrintStream("Day10Demo/src/log.txt"); + PrintStream ps = new PrintStream("Demo/src/log.txt"); System.setOut(ps); // 让系统的输出流向打印流 //不输出在控制台,输出到文件里 System.out.println("==seazean1=="); @@ -7937,23 +7835,23 @@ try( ### Properties -Properties:属性集对象。就是一个Map集合,一个键值对集合 +Properties:属性集对象。就是一个 Map 集合,一个键值对集合 -核心作用:Properties代表的是一个属性文件,可以把键值对数据存入到一个属性文件 +核心作用:Properties 代表的是一个属性文件,可以把键值对数据存入到一个属性文件 -属性文件:后缀是.properties结尾的文件,里面的内容都是 key=value +属性文件:后缀是 `.properties` 结尾的文件,里面的内容都是 key=value -Properties方法: +Properties 方法: -| 方法名 | 说明 | -| --------------------------------------------------- | ------------------------------------------- | -| public Object setProperty(String key, String value) | 设置集合的键和值,底层调用Hashtable方法 put | -| public String getProperty(String key) | 使用此属性列表中指定的键搜索属性 | -| public Set stringPropertyNames() | 所有键的名称的集合 | -| public synchronized void load(Reader r) | 从输入字符流读取属性列表(键和元素对) | -| public synchronized void load(InputStream inStream) | 加载属性文件的数据到属性集对象中去 | -| public void store(Writer w, String comments) | 将此属性列表(键和元素对)写入 Properties表 | -| public void store(OutputStream os, String comments) | 保存数据到属性文件中去 | +| 方法名 | 说明 | +| -------------------------------------------- | --------------------------------------------- | +| Object setProperty(String key, String value) | 设置集合的键和值,底层调用 Hashtable 方法 put | +| String getProperty(String key) | 使用此属性列表中指定的键搜索属性 | +| Set stringPropertyNames() | 所有键的名称的集合 | +| synchronized void load(Reader r) | 从输入字符流读取属性列表(键和元素对) | +| synchronized void load(InputStream inStream) | 加载属性文件的数据到属性集对象中去 | +| void store(Writer w, String comments) | 将此属性列表(键和元素对)写入 Properties 表 | +| void store(OutputStream os, String comments) | 保存数据到属性文件中去 | ````java public class PropertiesDemo01 { @@ -7962,7 +7860,7 @@ public class PropertiesDemo01 { Properties properties = new Properties();//{} properties.setProperty("admin" , "123456"); // b.把属性集对象的数据存入到属性文件中去(重点) - OutputStream os = new FileOutputStream("Day10Demo/src/users.properties"); + OutputStream os = new FileOutputStream("Demo/src/users.properties"); properties.store(os,"i am very happy!!我保存了用户数据!"); //参数一:被保存数据的输出管道 //参数二:保存心得。就是对象保存的数据进行解释说明! @@ -7974,7 +7872,7 @@ public class PropertiesDemo01 { public class PropertiesDemo02 { public static void main(String[] args) throws Exception { Properties properties = new Properties();//底层基于map集合 - properties.load(new FileInputStream("Day10Demo/src/users.properties")); + properties.load(new FileInputStream("Demo/src/users.properties")); System.out.println(properties); System.out.println(properties.getProperty("admin")); @@ -7998,13 +7896,15 @@ public class PropertiesDemo02 { RandomAccessFile 类:该类的实例支持读取和写入随机访问文件 构造器: -RandomAccessFile(File file, String mode):创建随机访问文件流,从File参数指定的文件读取,可选择写入 -RandomAccessFile(String name, String mode):创建随机访问文件流,从指定名称文件读取,可选择写入文件 + +* `RandomAccessFile(File file, String mode)`:创建随机访问文件流,从 File 参数指定的文件读取,可选择写入 +* `RandomAccessFile(String name, String mode)`:创建随机访问文件流,从指定名称文件读取,可选择写入文件 常用方法: -`public void seek(long pos)` : 设置文件指针偏移,从该文件开头测量,发生下一次读取或写入(插入+覆盖) -`public void write(byte[] b)` : 从指定的字节数组写入 b.length个字节到该文件 -`public int read(byte[] b)` : 从该文件读取最多b.length个字节的数据到字节数组 + +* `public void seek(long pos)`:设置文件指针偏移,从该文件开头测量,发生下一次读取或写入(插入+覆盖) +* `public void write(byte[] b)`:从指定的字节数组写入 b.length 个字节到该文件 +* `public int read(byte[] b)`:从该文件读取最多 b.length 个字节的数据到字节数组 ```java public static void main(String[] args) throws Exception { @@ -8024,16 +7924,16 @@ public static void main(String[] args) throws Exception { ### Commons -commons-io 是apache开源基金组织提供的一组有关IO操作的类库,可以挺提高IO功能开发的效率。 +commons-io 是 apache 提供的一组有关 IO 操作的类库,可以挺提高 IO 功能开发的效率。 commons-io 工具包提供了很多有关 IO 操作的类: -| 包 | 功能描述 | -| ----------------------------------- | :------------------------------------------- | -| org.apache.commons.io | 有关Streams、Readers、Writers、Files的工具类 | -| org.apache.commons.io.input | 输入流相关的实现类,包含Reader和InputStream | -| org.apache.commons.io.output | 输出流相关的实现类,包含Writer和OutputStream | -| org.apache.commons.io.serialization | 序列化相关的类 | +| 包 | 功能描述 | +| ----------------------------------- | :---------------------------------------------- | +| org.apache.commons.io | 有关 Streams、Readers、Writers、Files 的工具类 | +| org.apache.commons.io.input | 输入流相关的实现类,包含 Reader 和 InputStream | +| org.apache.commons.io.output | 输出流相关的实现类,包含 Writer 和 OutputStream | +| org.apache.commons.io.serialization | 序列化相关的类 | IOUtils 和 FileUtils 可以方便的复制文件和文件夹 @@ -8041,18 +7941,18 @@ IOUtils 和 FileUtils 可以方便的复制文件和文件夹 public class CommonsIODemo01 { public static void main(String[] args) throws Exception { // 1.完成文件复制! - IOUtils.copy(new FileInputStream("Day13Demo/src/books.xml"), - new FileOutputStream("Day13Demo/new.xml")); + IOUtils.copy(new FileInputStream("Demo/src/books.xml"), + new FileOutputStream("Demo/new.xml")); // 2.完成文件复制到某个文件夹下! - FileUtils.copyFileToDirectory(new File("Day13Demo/src/books.xml"), + FileUtils.copyFileToDirectory(new File("Demo/src/books.xml"), new File("D:/it")); // 3.完成文件夹复制到某个文件夹下! FileUtils.copyDirectoryToDirectory(new File("D:\\it\\图片服务器") , new File("D:\\")); // Java从1.7开始提供了一些nio, 自己也有一行代码完成复制的技术。 - Files.copy(Paths.get("Day13Demo/src/books.xml") - , new FileOutputStream("Day13Demo/new11.txt")); + Files.copy(Paths.get("Demo/src/books.xml") + , new FileOutputStream("Demo/new11.txt")); } } ``` @@ -8065,19 +7965,17 @@ public class CommonsIODemo01 { + + ## 反射 ### 测试框架 -> 单元测试是指程序员写的测试代码给自己的类中的方法进行预期正确性的验证。 -> 单元测试一旦写好了这些测试代码,就可以一直使用,可以实现一定程度上的自动化测试。 +单元测试的经典框架:Junit,是 Java 语言编写的第三方单元测试框架 -单元测试的经典框架:Junit - -* Junit : 是 Java 语言编写的第三方单元测试框架,可以帮助我们方便快速的测试我们代码的正确性。 -* 单元测试: - * 单元:在 Java 中,一个类就是一个单元 - * 单元测试:Junit 编写的一小段代码,用来对某个类中的某个方法进行功能测试或业务逻辑测试 +单元测试: +* 单元:在 Java 中,一个类就是一个单元 +* 单元测试:Junit 编写的一小段代码,用来对某个类中的某个方法进行功能测试或业务逻辑测试 Junit 单元测试框架的作用: @@ -8086,14 +7984,14 @@ Junit 单元测试框架的作用: 测试方法注意事项:**必须是 public 修饰的,没有返回值,没有参数,使用注解@Test修饰** -Junit常用注解(Junit 4.xxxx版本),@Test 测试方法: +Junit常用注解(Junit 4.xxxx 版本),@Test 测试方法: * @Before:用来修饰实例方法,该方法会在每一个测试方法执行之前执行一次 * @After:用来修饰实例方法,该方法会在每一个测试方法执行之后执行一次 * @BeforeClass:用来静态修饰方法,该方法会在所有测试方法之前**只**执行一次 * @AfterClass:用来静态修饰方法,该方法会在所有测试方法之后**只**执行一次 -Junit常用注解(Junit5.xxxx版本),@Test 测试方法: +Junit 常用注解(Junit5.xxxx 版本),@Test 测试方法: * @BeforeEach:用来修饰实例方法,该方法会在每一个测试方法执行之前执行一次 * @AfterEach:用来修饰实例方法,该方法会在每一个测试方法执行之后执行一次 @@ -8180,7 +8078,7 @@ public class UserServiceTest { 核心思想:在运行时获取类编译后的字节码文件对象,然后解析类中的全部成分 -反射提供了一个Class类型:HelloWorld.java → javac → HelloWorld.class +反射提供了一个 Class 类型:HelloWorld.java → javac → HelloWorld.class * `Class c = HelloWorld.class` @@ -8188,16 +8086,16 @@ public class UserServiceTest { 作用:可以在运行时得到一个类的全部成分然后操作,破坏封装性,也可以破坏泛型的约束性。 -**反射的优点:** +反射的优点: - 可扩展性:应用程序可以利用全限定名创建可扩展对象的实例,来使用来自外部的用户自定义类 - 类浏览器和可视化开发环境:一个类浏览器需要可以枚举类的成员,可视化开发环境(如 IDE)可以从利用反射中可用的类型信息中受益,以帮助程序员编写正确的代码 -- 调试器和测试工具: 调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率 +- 调试器和测试工具: 调试器需要能够检查一个类里的私有成员,测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率 -**反射的缺点:** +反射的缺点: - 性能开销:反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化,反射操作的效率要比那些非射操作低得多,应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。 -- 安全限制:使用反射技术要求程序必须在一个没有安全限制的环境中运行,如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了 +- 安全限制:使用反射技术要求程序必须在一个没有安全限制的环境中运行,如果一个程序必须在有安全限制的环境中运行 - 内部暴露:由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化 @@ -8260,9 +8158,9 @@ class Student{} 获取构造器的 API: * Constructor getConstructor(Class... parameterTypes):根据参数匹配获取某个构造器,只能拿 public 修饰的构造器 -* **Constructor getDeclaredConstructor(Class... parameterTypes)**:根据参数匹配获取某个构造器,只要申明就可以定位,不关心权限修饰符 +* Constructor getDeclaredConstructor(Class... parameterTypes):根据参数匹配获取某个构造器,只要申明就可以定位,不关心权限修饰符 * Constructor[] getConstructors():获取所有的构造器,只能拿 public 修饰的构造器 -* **Constructor[] getDeclaredConstructors()**:获取所有构造器,只要申明就可以定位,不关心权限修饰符 +* Constructor[] getDeclaredConstructors():获取所有构造器,只要申明就可以定位,不关心权限修饰符 Constructor 的常用 API: @@ -8356,12 +8254,12 @@ public class TestStudent02 { #### 获取变量 -获取Field成员变量API: +获取 Field 成员变量 API: -* Field getField(String name) : 根据成员变量名获得对应 Field 对象,只能获得 public 修饰 -* Field getDeclaredField(String name) : 根据成员变量名获得对应 Field 对象,所有申明的变量 -* Field[] getFields() : 获得所有的成员变量对应的Field对象,只能获得 public 的 -* Field[] getDeclaredFields() : 获得所有的成员变量对应的 Field 对象,只要申明了就可以得到 +* Field getField(String name):根据成员变量名获得对应 Field 对象,只能获得 public 修饰 +* Field getDeclaredField(String name):根据成员变量名获得对应 Field 对象,所有申明的变量 +* Field[] getFields():获得所有的成员变量对应的 Field 对象,只能获得 public 的 +* Field[] getDeclaredFields():获得所有的成员变量对应的 Field 对象,只要申明了就可以得到 Field 的方法:给成员变量赋值和取值 @@ -8440,6 +8338,10 @@ public class FieldDemo02 { +*** + + + #### 获取方法 获取 Method 方法 API: @@ -8546,10 +8448,9 @@ public class ReflectDemo { 注解:类的组成部分,可以给类携带一些额外的信息,提供一种安全的类似注释标记的机制,用来将任何信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联 -* 注解是 JDK1.5 的新特性 * 注解是给编译器或 JVM 看的,编译器或 JVM 可以根据注解来完成对应的功能 * 注解类似修饰符,应用于包、类型、构造方法、方法、成员变量、参数及本地变量的声明语句中 -* 父类中的注解是不能被子类继承的 +* **父类中的注解是不能被子类继承的** 注解作用: @@ -8665,29 +8566,25 @@ public class AnnotationDemo01{ 元注解有四个: -* @Target:约束自定义注解可以标记的范围,默认值为任何元素,表示该注解用于什么地方 - - 可使用的值定义在ElementType枚举类中: +* @Target:约束自定义注解可以标记的范围,默认值为任何元素,表示该注解用于什么地方,可用值定义在 ElementType 类中: - `ElementType.CONSTRUCTOR`:用于描述构造器 - - `ElementType.FIELD`:成员变量、对象、属性(包括enum实例) + - `ElementType.FIELD`:成员变量、对象、属性(包括 enum 实例) - `ElementType.LOCAL_VARIABLE`:用于描述局部变量 - `ElementType.METHOD`:用于描述方法 - `ElementType.PACKAGE`:用于描述包 - `ElementType.PARAMETER`:用于描述参数 - - `ElementType.TYPE`:用于描述类、接口(包括注解类型) 或enum声明 - -* @Retention:定义该注解的生命周期,申明注解的作用范围:编译时,运行时 - - 可使用的值定义在RetentionPolicy枚举类中: + - `ElementType.TYPE`:用于描述类、接口(包括注解类型)或 enum 声明 + +* @Retention:定义该注解的生命周期,申明注解的作用范围:编译时,运行时,可使用的值定义在 RetentionPolicy 枚举类中: - - `RetentionPolicy.SOURCE`:在编译阶段丢弃,这些注解在编译结束之后就不再有任何意义,只作用在源码阶段,生成的字节码文件中不存在。`@Override`, `@SuppressWarnings`都属于这类注解 + - `RetentionPolicy.SOURCE`:在编译阶段丢弃,这些注解在编译结束之后就不再有任何意义,只作用在源码阶段,生成的字节码文件中不存在,`@Override`、`@SuppressWarnings` 都属于这类注解 - `RetentionPolicy.CLASS`:在类加载时丢弃,在字节码文件的处理中有用,运行阶段不存在,默认值 - `RetentionPolicy.RUNTIME` : 始终不会丢弃,运行期也保留该注解,因此可以使用反射机制读取该注解的信息,自定义的注解通常使用这种方式 - + * @Inherited:表示修饰的自定义注解可以被子类继承 -* @Documented:表示是否将自定义的注解信息添加在 java 文档中 +* @Documented:表示是否将自定义的注解信息添加在 Java 文档中 ```java public class AnnotationDemo01{ @@ -8816,10 +8713,14 @@ public class TestDemo{ + + **** + + ## XML ### 概述 @@ -8966,11 +8867,11 @@ XML 文件中常见的组成元素有:文档声明、元素、属性、注释、 #### DTD -##### DTD定义 +##### DTD 定义 -DTD 是文档类型定义(Document Type Definition)。DTD 可以定义在 XML 文档中出现的元素、这些元素出现的次序、它们如何相互嵌套以及XML文档结构的其它详细信息。 +DTD 是文档类型定义(Document Type Definition)。DTD 可以定义在 XML 文档中出现的元素、这些元素出现的次序、它们如何相互嵌套以及 XML 文档结构的其它详细信息。 -##### DTD规则 +DTD 规则: * 约束元素的嵌套层级 @@ -9066,7 +8967,7 @@ DTD 是文档类型定义(Document Type Definition)。DTD 可以定义在 XM -##### DTD引入 +##### DTD 引入 * 引入本地 dtd @@ -9142,7 +9043,7 @@ DTD 是文档类型定义(Document Type Definition)。DTD 可以定义在 XM -##### DTD实现 +##### DTD 实现 persondtd.dtd 文件 @@ -9179,13 +9080,13 @@ persondtd.dtd 文件 #### Schema -##### XSD定义 +##### XSD 定义 1. Schema 语言也可作为 XSD(XML Schema Definition) -2. Schema 约束文件本身也是一个 xml 文件,符合 xml 的语法,这个文件的后缀名 .xsd -3. 一个 xml 中可以引用多个 Schema 约束文件,多个 Schema 使用名称空间区分(名称空间类似于 Java 包名) +2. Schema 约束文件本身也是一个 XML 文件,符合 XML 的语法,这个文件的后缀名 .xsd +3. 一个 XML 中可以引用多个 Schema 约束文件,多个 Schema 使用名称空间区分(名称空间类似于 Java 包名) 4. dtd 里面元素类型的取值比较单一常见的是 PCDATA 类型,但是在 Schema 里面可以支持很多个数据类型 -5. **Schema 文件约束 xml 文件的同时也被别的文件约束着** +5. **Schema 文件约束 XML 文件的同时也被别的文件约束着** @@ -9193,7 +9094,7 @@ persondtd.dtd 文件 -##### XSD规则 +##### XSD 规则 1. 创建一个文件,这个文件的后缀名为 .xsd 2. 定义文档声明 @@ -9244,7 +9145,7 @@ person.xsd -##### XSD引入 +##### XSD 引入 1. 在根标签上定义属性 xmlns="http://www.w3.org/2001/XMLSchema-instance" 2. **通过 xmlns 引入约束文件的名称空间** @@ -9273,7 +9174,7 @@ person.xsd -##### XSD属性 +##### XSD 属性 ```scheme @@ -9339,12 +9240,12 @@ DOM(Document Object Model):文档对象模型,把文档的各个组成 Dom4J 实现: * Dom4J 解析器构造方法:`SAXReader saxReader = new SAXReader()` -* SAXReader 常用API: +* SAXReader 常用 API: * `public Document read(File file)`:Reads a Document from the given File * `public Document read(InputStream in)`:Reads a Document from the given stream using SAX -* Java Class 类API: +* Java Class 类 API: * `public InputStream getResourceAsStream(String path)`:加载文件成为一个字节输入流返回 @@ -9401,6 +9302,10 @@ public class Dom4JDemo { +**** + + + #### 子元素 Element 元素的 API: @@ -9620,92 +9525,126 @@ public class XPathDemo { +**** -*** +## SDP +### 单例模式 -# JVM +#### 基本介绍 -## JVM概述 +创建型模式的主要关注点是怎样创建对象,将对象的创建与使用分离,降低系统的耦合度,使用者不需要关注对象的创建细节 -### 基本介绍 +创建型模式分为:单例模式、工厂方法模式、抽象工程模式、原型模式、建造者模式 -JVM:全称 Java Virtual Machine,即 Java 虚拟机,一种规范,本身是一个虚拟计算机,直接和操作系统进行交互,与硬件不直接交互,而操作系统可以帮我们完成和硬件进行交互的工作 +单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一,提供了一种创建对象的最佳方式 -特点: +单例设计模式分类两种: -* Java 虚拟机基于**二进制字节码**执行,由一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆、一个方法区等组成 -* JVM 屏蔽了与操作系统平台相关的信息,从而能够让 Java 程序只需要生成能够在 JVM 上运行的字节码文件,通过该机制实现的**跨平台性** +* 饿汉式:类加载就会导致该单实例对象被创建 -Java 代码执行流程:java程序 --(编译)--> 字节码文件 --(解释执行)--> 操作系统(Win,Linux) +* 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建 -JVM 结构: - -JVM、JRE、JDK 对比: +*** - +#### 饿汉式 -参考书籍:https://book.douban.com/subject/34907497/ +饿汉式在类加载的过程导致该单实例对象被创建,**虚拟机会保证类加载的线程安全**,但是如果只是为了加载该类不需要实例,则会造成内存的浪费 -参考视频:https://www.bilibili.com/video/BV1PJ411n7xZ +* 静态变量的方式: -参考视频:https://www.bilibili.com/video/BV1yE411Z7AP + ```java + public final class Singleton { + // 私有构造方法 + private Singleton() {} + // 在成员位置创建该类的对象 + private static final Singleton instance = new Singleton(); + // 对外提供静态方法获取该对象 + public static Singleton getInstance() { + return instance; + } + + // 解决序列化问题 + protected Object readResolve() { + return INSTANCE; + } + } + ``` + * 加 final 修饰,所以不会被子类继承,防止子类中不适当的行为覆盖父类的方法,破坏了单例 + * 防止反序列化破坏单例的方式: -*** + * 对单例声明 transient,然后实现 readObject(ObjectInputStream in) 方法,复用原来的单例 + 条件:访问权限为 private/protected、返回值必须是 Object、异常可以不抛 + * 实现 readResolve() 方法,当 JVM 从内存中反序列化地组装一个新对象,就会自动调用 readResolve 方法返回原来单例 -### 架构模型 + * 构造方法设置为私有,防止其他类无限创建对象,但是不能防止反射破坏 -Java 编译器输入的指令流是一种基于栈的指令集架构。因为跨平台的设计,Java 的指令都是根据栈来设计的,不同平台 CPU 架构不同,所以不能设计为基于寄存器架构 + * 静态变量初始化在类加载时完成,由 JVM 保证线程安全,能保证单例对象创建时的安全 -* 基于栈式架构的特点: - * 设计和实现简单,适用于资源受限的系统 - * 使用零地址指令方式分配,执行过程依赖操作栈,指令集更小,编译器容易实现 - * 零地址指令:机器指令的一种,是指令系统中的一种不设地址字段的指令,只有操作码而没有地址码。这种指令有两种情况:一是无需操作数,另一种是操作数为默认的(隐含的),默认为操作数在寄存器(ACC)中,指令可直接访问寄存器 - * 一地址指令:一个操作码对应一个地址码,通过地址码寻找操作数 - * 不需要硬件的支持,可移植性更好,更好实现跨平台 -* 基于寄存器架构的特点: - * 需要硬件的支持,可移植性差 - * 性能更好,执行更高效,寄存器比内存快 - * 以一地址指令、二地址指令、三地址指令为主 + * 提供静态方法而不是直接将 INSTANCE 设置为 public,体现了更好的封装性、提供泛型支持、可以改进成懒汉单例设计 +* 静态代码块的方式: + ```java + public class Singleton { + // 私有构造方法 + private Singleton() {} + + // 在成员位置创建该类的对象 + private static Singleton instance; + static { + instance = new Singleton(); + } + + // 对外提供静态方法获取该对象 + public static Singleton getInstance() { + return instance; + } + } + ``` -*** +* 枚举方式:枚举类型是所用单例实现中**唯一一种不会被破坏**的单例实现模式 + ```java + public enum Singleton { + INSTANCE; + public void doSomething() { + System.out.println("doSomething"); + } + } + public static void main(String[] args) { + Singleton.INSTANCE.doSomething(); + } + ``` + * 问题1:枚举单例是如何限制实例个数的?每个枚举项都是一个实例,是一个静态成员变量 + * 问题2:枚举单例在创建时是否有并发问题?否 + * 问题3:枚举单例能否被反射破坏单例?否,反射创建对象时判断是枚举类型就直接抛出异常 + * 问题4:枚举单例能否被反序列化破坏单例?否 + * 问题5:枚举单例属于懒汉式还是饿汉式?**饿汉式** + * 问题6:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做?添加构造方法 -### 生命周期 + 反编译结果: -JVM 的生命周期分为三个阶段,分别为:启动、运行、死亡。 + ```java + public final class Singleton extends java.lang.Enum { // Enum实现序列化接口 + public static final Singleton INSTANCE = new Singleton(); + } + ``` -- **启动**:当启动一个 Java 程序时,通过引导类加载器(bootstrap class loader)创建一个初始类(initial class),对于拥有 main 函数的类就是 JVM 实例运行的起点 -- **运行**: - - - main() 方法是一个程序的初始起点,任何线程均可由在此处启动 - - 在 JVM 内部有两种线程类型,分别为:用户线程和守护线程,**JVM 使用的是守护线程,main() 和其他线程使用的是用户线程**,守护线程会随着用户线程的结束而结束 - - 执行一个 Java 程序时,真真正正在执行的是一个 Java 虚拟机的进程 - - JVM 有两种运行模式 Server 与 Client,两种模式的区别在于:Client 模式启动速度较快,Server 模式启动较慢;但是启动进入稳定期长期运行之后 Server 模式的程序运行速度比 Client 要快很多 - - Server 模式启动的 JVM 采用的是重量级的虚拟机,对程序采用了更多的优化;Client 模式启动的 JVM 采用的是轻量级的虚拟机 -- **死亡**: - - 当程序中的用户线程都中止,JVM 才会退出 - - 程序正常执行结束、程序异常或错误而异常终止、操作系统错误导致终止 - - 线程调用 Runtime 类 halt 方法或 System 类 exit 方法,并且 java 安全管理器允许这次 exit 或 halt 操作 - - @@ -9713,36 +9652,78 @@ JVM 的生命周期分为三个阶段,分别为:启动、运行、死亡。 +#### 懒汉式 +* 线程不安全 -## 内存结构 - -### 内存概述 - -内存结构是 JVM 中非常重要的一部分,是非常重要的系统资源,是硬盘和 CPU 的中间仓库及桥梁,承载着操作系统和应用程序的实时运行,又叫运行时数据区 + ```java + public class Singleton { + // 私有构造方法 + private Singleton() {} + + // 在成员位置创建该类的对象 + private static Singleton instance; + + // 对外提供静态方法获取该对象 + public static Singleton getInstance() { + if(instance == null) { + // 多线程环境,会出现线程安全问题,可能多个线程同时进入这里 + instance = new Singleton(); + } + return instance; + } + } + ``` -JVM 内存结构规定了 Java 在运行过程中内存申请、分配、管理的策略,保证了 JVM 的高效稳定运行 +* 双端检锁机制 -* Java1.8 以前的内存结构图: - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java7内存结构图.png) + 在多线程的情况下,可能会出现空指针问题,出现问题的原因是 JVM 在实例化对象的时候会进行优化和指令重排序操作,所以需要使用 `volatile` 关键字 -* Java1.8 之后的内存结果图: + ```java + public class Singleton { + // 私有构造方法 + private Singleton() {} + private static volatile Singleton instance; + + // 对外提供静态方法获取该对象 + public static Singleton getInstance() { + // 第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例 + if(instance == null) { + synchronized (Singleton.class) { + // 抢到锁之后再次判断是否为null + if(instance == null) { + instance = new Singleton(); + } + } + } + return instance; + } + } + ``` - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java8内存结构图.png) +* 静态内部类方式 -线程运行诊断: + ```java + public class Singleton { + // 私有构造方法 + private Singleton() {} + + private static class SingletonHolder { + private static final Singleton INSTANCE = new Singleton(); + } + + // 对外提供静态方法获取该对象 + public static Singleton getInstance() { + return SingletonHolder.INSTANCE; + } + } + ``` -* 定位:jps 定位进程 id -* jstack 进程 id:用于打印出给定的 java 进程 ID 或 core file 或远程调试服务的 Java 堆栈信息 + * 内部类属于懒汉式,类加载本身就是懒惰的,首次调用时加载,然后对单例进行初始化 -常见OOM错误: + 类加载的时候方法不会被调用,所以不会触发 getInstance 方法调用 invokestatic 指令对内部类进行加载;加载的时候字节码常量池会被加入类的运行时常量池,解析工作是将常量池中的符号引用解析成直接引用,但是解析过程不一定非得在类加载时完成,可以延迟到运行时进行,所以静态内部类实现单例会**延迟加载** -* java.lang.StackOverflowError -* java.lang.OutOfMemoryError:java heap space -* java.lang.OutOfMemoryError:GC overhead limit exceeded -* java.lang.OutOfMemoryError:Direct buffer memory -* java.lang.OutOfMemoryError:unable to create new native thread -* java.lang.OutOfMemoryError:Metaspace + * 没有线程安全问题,静态变量初始化在类加载时完成,由 JVM 保证线程安全 @@ -9750,47 +9731,100 @@ JVM 内存结构规定了 Java 在运行过程中内存申请、分配、管理 -### JVM内存 - -#### 虚拟机栈 - -##### Java栈 - -Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所需要的内存 - -* 每个方法被执行时,都会在虚拟机栈中创建一个栈帧 stack frame(**一个方法一个栈帧**) +#### 破坏单例 -* Java 虚拟机规范允许 **Java 栈的大小是动态的或者是固定不变的** +##### 反序列化 -* 虚拟机栈是**每个线程私有的**,每个线程只能有一个活动栈帧,对应方法调用到执行完成的整个过程 +将单例对象序列化再反序列化,对象从内存反序列化到程序中会重新创建一个对象,通过反序列化得到的对象是不同的对象,而且得到的对象不是通过构造器得到的,**反序列化得到的对象不执行构造器** -* 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存,每个栈帧中存储着: +* Singleton - * 局部变量表:存储方法里的java基本数据类型以及对象的引用 - * 动态链接:也叫指向运行时常量池的方法引用 - * 方法返回地址:方法正常退出或者异常退出的定义 - * 操作数栈或表达式栈和其他一些附加信息 + ```java + public class Singleton implements Serializable { //实现序列化接口 + // 私有构造方法 + private Singleton() {} + private static class SingletonHolder { + private static final Singleton INSTANCE = new Singleton(); + } - - -设置栈内存大小:`-Xss size` `-Xss 1024k` + // 对外提供静态方法获取该对象 + public static Singleton getInstance() { + return SingletonHolder.INSTANCE; + } + } + ``` -* 在 JDK 1.4 中默认为 256K,而在 JDK 1.5+ 默认为 1M +* 序列化 -虚拟机栈特点: + ```java + public class Test { + public static void main(String[] args) throws Exception { + //往文件中写对象 + //writeObject2File(); + //从文件中读取对象 + Singleton s1 = readObjectFromFile(); + Singleton s2 = readObjectFromFile(); + //判断两个反序列化后的对象是否是同一个对象 + System.out.println(s1 == s2); + } + + private static Singleton readObjectFromFile() throws Exception { + //创建对象输入流对象 + ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C://a.txt")); + //第一个读取Singleton对象 + Singleton instance = (Singleton) ois.readObject(); + return instance; + } + + public static void writeObject2File() throws Exception { + //获取Singleton类的对象 + Singleton instance = Singleton.getInstance(); + //创建对象输出流 + ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C://a.txt")); + //将instance对象写出到文件中 + oos.writeObject(instance); + } + } + ``` -* 栈内存**不需要进行GC**,方法开始执行的时候会进栈,方法调用后自动弹栈,相当于清空了数据 +* 解决方法: -* 栈内存分配越大越大,可用的线程数越少(内存越大,每个线程拥有的内存越大) + 在 Singleton 类中添加 `readResolve()` 方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新创建的对象 -* 方法内的局部变量是否**线程安全**: - * 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的(逃逸分析) - * 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全 + ```java + private Object readResolve() { + return SingletonHolder.INSTANCE; + } + ``` -异常: + ObjectInputStream 类源码分析: -* 栈帧过多导致栈内存溢出 (超过了栈的容量),会抛出 OutOfMemoryError 异常 -* 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常 + ```java + public final Object readObject() throws IOException, ClassNotFoundException{ + //... + Object obj = readObject0(false);//重点查看readObject0方法 + } + + private Object readObject0(boolean unshared) throws IOException { + try { + switch (tc) { + case TC_OBJECT: + return checkResolve(readOrdinaryObject(unshared)); + } + } + } + private Object readOrdinaryObject(boolean unshared) throws IOException { + // isInstantiable 返回true,执行 desc.newInstance(),通过反射创建新的单例类 + obj = desc.isInstantiable() ? desc.newInstance() : null; + // 添加 readResolve 方法后 desc.hasReadResolveMethod() 方法执行结果为true + if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) { + // 通过反射调用 Singleton 类中的 readResolve 方法,将返回值赋值给rep变量 + // 多次调用ObjectInputStream类中的readObject方法,本质调用定义的readResolve方法,返回的是同一个对象。 + Object rep = desc.invokeReadResolve(obj); + } + return obj; + } + ``` @@ -9798,21 +9832,62 @@ Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所 -##### 局部变量 +##### 反射破解 -局部变量表也被称之为局部变量数组或本地变量表,本质上定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量 +* 反射 -* 表是建立在线程的栈上,是线程私有的数据,因此不存在数据安全问题 -* 表的容量大小是在编译期确定的,保存在方法的 Code 属性的 maximum local variables 数据项中 -* 表中的变量只在当前方法调用中有效,方法结束栈帧销毁,局部变量表也会随之销毁 -* 表中的变量也是重要的垃圾回收根节点,只要被表中数据直接或间接引用的对象都不会被回收 + ```java + public class Test { + public static void main(String[] args) throws Exception { + //获取Singleton类的字节码对象 + Class clazz = Singleton.class; + //获取Singleton类的私有无参构造方法对象 + Constructor constructor = clazz.getDeclaredConstructor(); + //取消访问检查 + constructor.setAccessible(true); + + //创建Singleton类的对象s1 + Singleton s1 = (Singleton) constructor.newInstance(); + //创建Singleton类的对象s2 + Singleton s2 = (Singleton) constructor.newInstance(); + + //判断通过反射创建的两个Singleton对象是否是同一个对象 + System.out.println(s1 == s2); //false + } + } + ``` + +* 反射方式破解单例的解决方法: + + ```java + public class Singleton { + private static volatile Singleton instance; + + // 私有构造方法 + private Singleton() { + // 反射破解单例模式需要添加的代码 + if(instance != null) { + throw new RuntimeException(); + } + } + + // 对外提供静态方法获取该对象 + public static Singleton getInstance() { + if(instance != null) { + return instance; + } + synchronized (Singleton.class) { + if(instance != null) { + return instance; + } + instance = new Singleton(); + return instance; + } + } + } + ``` -局部变量表最基本的存储单元是 **slot(变量槽)**: -* 参数值的存放总是在局部变量数组的 index0 开始,到数组长度 -1 的索引结束,JVM 为每一个 slot 都分配一个访问索引,通过索引即可访问到槽中的数据 -* 存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress 类型的变量 -* 32 位以内的类型只占一个 slot(包括 returnAddress 类型),64 位的类型(long 和 double)占两个 slot -* 局部变量表中的槽位是可以**重复利用**的,如果一个局部变量过了其作用域,那么之后申明的新的局部变量就可能会复用过期局部变量的槽位,从而达到节省资源的目的 @@ -9820,97 +9895,172 @@ Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所 -##### 操作数栈 +#### Runtime -栈:可以使用数组或者链表来实现 +Runtime 类就是使用的单例设计模式中的饿汉式 -操作数栈:在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)或出栈(pop) +```java +public class Runtime { + private static Runtime currentRuntime = new Runtime(); + public static Runtime getRuntime() { + return currentRuntime; + } + private Runtime() {} + ... +} +``` -* 保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间,是执行引擎的一个工作区 +使用 Runtime -* Java 虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈 -* 如果被调用的方法带有返回值的话,其**返回值将会被压入当前栈帧的操作数栈中** +```java +public class RuntimeDemo { + public static void main(String[] args) throws IOException { + //获取Runtime类对象 + Runtime runtime = Runtime.getRuntime(); -栈顶缓存技术 ToS(Top-of-Stack Cashing):将栈顶元素全部缓存在 CPU 的寄存器中,以此降低对内存的读/写次数,提升执行的效率 + //返回 Java 虚拟机中的内存总量。 + System.out.println(runtime.totalMemory()); + //返回 Java 虚拟机试图使用的最大内存量。 + System.out.println(runtime.maxMemory()); -基于栈式架构的虚拟机使用的零地址指令更加紧凑,完成一项操作需要使用很多入栈和出栈指令,所以需要更多的指令分派(instruction dispatch)次数和内存读/写次数,由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度,所以需要栈顶缓存技术 + //创建一个新的进程执行指定的字符串命令,返回进程对象 + Process process = runtime.exec("ipconfig"); + //获取命令执行后的结果,通过输入流获取 + InputStream inputStream = process.getInputStream(); + byte[] arr = new byte[1024 * 1024* 100]; + int b = inputStream.read(arr); + System.out.println(new String(arr,0,b,"gbk")); + } +} +``` -*** +**** -##### 动态链接 -动态链接是指向运行时常量池的方法引用,涉及到栈操作已经是类加载完成,这个阶段的解析是**动态绑定** -* 为了支持当前方法的代码能够实现动态链接,每一个栈帧内部都包含一个指向运行时常量池或该栈帧所属方法的引用 +### 代理模式 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-动态链接符号引用.png) +#### 静态代理 -* 在 Java 源文件被编译成字节码文件中,所有的变量和方法引用都作为符号引用保存在 class 的常量池中 - - 常量池的作用:提供一些符号和常量,便于指令的识别 - - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-动态链接运行时常量池.png) +代理模式:由于某些原因需要给某对象提供一个代理以控制对该对象的访问,访问对象不适合或者不能直接引用为目标对象,代理对象作为访问对象和目标对象之间的中介 +Java 中的代理按照代理类生成时机不同又分为静态代理和动态代理,静态代理代理类在编译期就生成,而动态代理代理类则是在 Java 运行时动态生成,动态代理又有 JDK 代理和 CGLib 代理两种 +代理(Proxy)模式分为三种角色: -*** +* 抽象主题(Subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法 +* 真实主题(Real Subject)类: 实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象 +* 代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,可以访问、控制或扩展真实主题的功能 +买票案例,火车站是目标对象,代售点是代理对象 +* 卖票接口: -##### 返回地址 + ```java + public interface SellTickets { + void sell(); + } + ``` -Return Address:存放调用该方法的 PC 寄存器的值 +* 火车站,具有卖票功能,需要实现SellTickets接口 -方法的结束有两种方式:正常执行完成、出现未处理的异常,在方法退出后都返回到该方法被调用的位置 + ```java + public class TrainStation implements SellTickets { + public void sell() { + System.out.println("火车站卖票"); + } + } + ``` -* 正常:调用者的 pc 计数器的值作为返回地址,即调用该方法的指令的**下一条指令的地址** -* 异常:返回地址是要通过异常表来确定 +* 代售点: -正常完成出口:执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者 + ```java + public class ProxyPoint implements SellTickets { + private TrainStation station = new TrainStation(); + + public void sell() { + System.out.println("代理点收取一些服务费用"); + station.sell(); + } + } + ``` -异常完成出口:方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内进行处理,本方法的异常表中没有搜素到匹配的异常处理器,导致方法退出 +* 测试类: -两者区别:通过异常完成出口退出的不会给上层调用者产生任何的返回值 + ```java + public class Client { + public static void main(String[] args) { + ProxyPoint pp = new ProxyPoint(); + pp.sell(); + } + } + ``` + 测试类直接访问的是 ProxyPoint 类对象,也就是 ProxyPoint 作为访问对象和目标对象的中介 -##### 附加信息 -栈帧中还允许携带与 Java 虚拟机实现相关的一些附加信息,例如对程序调试提供支持的信息 +**** -*** +#### JDK +##### 使用方式 +Java 中提供了一个动态代理类 Proxy,Proxy 并不是代理对象的类,而是提供了一个创建代理对象的静态方法 newProxyInstance() 来获取代理对象 -#### 本地方法栈 +`static Object newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h) ` -本地方法栈是为虚拟机执行本地方法时提供服务的 +* 参数一:类加载器,负责加载代理类。传入类加载器,代理和被代理对象要用一个类加载器才是父子关系,不同类加载器加载相同的类在 JVM 中都不是同一个类对象 -JNI:Java Native Interface,通过使用 Java 本地接口书写程序,可以确保代码在不同的平台上方便移植 +* 参数二:被代理业务对象的**全部实现的接口**,代理对象与真实对象实现相同接口,知道为哪些方法做代理 -* 不需要进行GC,与虚拟机栈类似,也是线程私有的,有 StackOverFlowError 和 OutOfMemoryError 异常 +* 参数三:代理真正的执行方法,也就是代理的处理逻辑 -* 虚拟机栈执行的是 Java 方法,在 HotSpot JVM 中,直接将本地方法栈和虚拟机栈合二为一 +代码实现: -* 本地方法一般是由其他语言编写,并且被编译为基于本机硬件和操作系统的程序 +* 代理工厂:创建代理对象 -* 当某个线程调用一个本地方法时,就进入了不再受虚拟机限制的世界,和虚拟机拥有同样的权限 + ```java + public class ProxyFactory { + private TrainStation station = new TrainStation(); + //也可以在参数中提供 getProxyObject(TrainStation station) + public SellTickets getProxyObject() { + //使用 Proxy 获取代理对象 + SellTickets sellTickets = (SellTickets) Proxy.newProxyInstance( + station.getClass().getClassLoader(), + station.getClass().getInterfaces(), + new InvocationHandler() { + public Object invoke(Object proxy, Method method, Object[] args) { + System.out.println("代理点(JDK动态代理方式)"); + //执行真实对象 + Object result = method.invoke(station, args); + return result; + } + }); + return sellTickets; + } + } + ``` - * 本地方法可以通过本地方法接口来**访问虚拟机内部的运行时数据区** - * 直接从本地内存的堆中分配任意数量的内存 - * 可以直接使用本地处理器中的寄存器 - - - - +* 测试类: -图片来源:https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20%E8%99%9A%E6%8B%9F%E6%9C%BA.md + ```java + public class Client { + public static void main(String[] args) { + //获取代理对象 + ProxyFactory factory = new ProxyFactory(); + //必须时代理ji + SellTickets proxyObject = factory.getProxyObject(); + proxyObject.sell(); + } + } + ``` @@ -9918,185 +10068,292 @@ JNI:Java Native Interface,通过使用 Java 本地接口书写程序,可 -#### 程序计数器 - -Program Counter Register 程序计数器(寄存器) +##### 实现原理 -作用:内部保存字节码的行号,用于记录正在执行的字节码指令地址(如果正在执行的是本地方法则为空) +JDK 动态代理方式的优缺点: -原理: +- 优点:可以为任意的接口实现类对象做代理,也可以为被代理对象的所有接口的所有方法做代理,动态代理可以在不改变方法源码的情况下,实现对方法功能的增强,提高了软件的可扩展性,Java 反射机制可以生成任意类型的动态代理类 +- 缺点:**只能针对接口或者接口的实现类对象做代理对象**,普通类是不能做代理对象的 +- 原因:**生成的代理类继承了 Proxy**,Java 是单继承的,所以 JDK 动态代理只能代理接口 -* JVM 对于多线程是通过线程轮流切换并且分配线程执行时间,一个处理器只会处理执行一个线程 -* 切换线程需要从程序计数器中来回去到当前的线程上一次执行的行号 +ProxyFactory 不是代理模式中的代理类,而代理类是程序在运行过程中动态的在内存中生成的类,可以通过 Arthas 工具查看代理类结构: -特点: +* 代理类($Proxy0)实现了 SellTickets 接口,真实类和代理类实现同样的接口 +* 代理类($Proxy0)将提供了的匿名内部类对象传递给了父类 +* 代理类($Proxy0)的修饰符是 public final -* 是线程私有的 -* **不会存在内存溢出**,是 JVM 规范中唯一一个不出现 OOM 的区域,所以这个空间不会进行 GC +```java +// 程序运行过程中动态生成的代理类 +public final class $Proxy0 extends Proxy implements SellTickets { + private static Method m3; -Java 反编译指令:`javap -v Test.class` + public $Proxy0(InvocationHandler invocationHandler) { + super(invocationHandler);//InvocationHandler对象传递给父类 + } -#20:代表去 Constant pool 查看该地址的指令 + static { + m3 = Class.forName("proxy.dynamic.jdk.SellTickets").getMethod("sell", new Class[0]); + } -```java -0: getstatic #20 // PrintStream out = System.out; -3: astore_1 // -- -4: aload_1 // out.println(1); -5: iconst_1 // -- -6: invokevirtual #26 // -- -9: aload_1 // out.println(2); -10: iconst_2 // -- -11: invokevirtual #26 // -- + public final void sell() { + // 调用InvocationHandler的invoke方法 + this.h.invoke(this, m3, null); + } +} + +// Java提供的动态代理相关类 +public class Proxy implements java.io.Serializable { + protected InvocationHandler h; + + protected Proxy(InvocationHandler h) { + this.h = h; + } +} ``` +执行流程如下: + +1. 在测试类中通过代理对象调用 sell() 方法 +2. 根据多态的特性,执行的是代理类($Proxy0)中的 sell() 方法 +3. 代理类($Proxy0)中的 sell() 方法中又调用了 InvocationHandler 接口的子实现类对象的 invoke 方法 +4. invoke 方法通过反射执行了真实对象所属类(TrainStation)中的 sell() 方法 + **** -#### 堆 +##### 源码解析 -Heap 堆:是 JVM 内存中最大的一块,由所有线程共享,由垃圾回收器管理的主要区域,堆中对象大部分都需要考虑线程安全的问题 +```java +public static Object newProxyInstance(ClassLoader loader, + Class[] interfaces, + InvocationHandler h){ + // InvocationHandler 为空则抛出异常 + Objects.requireNonNull(h); -存放哪些资源: + // 复制一份 interfaces + final Class[] intfs = interfaces.clone(); + final SecurityManager sm = System.getSecurityManager(); + if (sm != null) { + checkProxyAccess(Reflection.getCallerClass(), loader, intfs); + } -* 对象实例:类初始化生成的对象,**基本数据类型的数组也是对象实例**,new 创建对象都使用堆内存 -* 字符串常量池: - * 字符串常量池原本存放于方法区,jdk7 开始放置于堆中 - * 字符串常量池**存储的是 String 对象的直接引用或者对象**,是一张 string table -* 静态变量:静态变量是有 static 修饰的变量,jdk7 时从方法区迁移至堆中 -* 线程分配缓冲区 Thread Local Allocation Buffer:线程私有但不影响堆的共性,可以提升对象分配的效率 + // 从缓存中查找 class 类型的代理对象,会调用 ProxyClassFactory#apply 方法 + Class cl = getProxyClass0(loader, intfs); + //proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory()) + + try { + if (sm != null) { + checkNewProxyPermission(Reflection.getCallerClass(), cl); + } -设置堆内存指令:`-Xmx Size` + // 获取代理类的构造方法,根据参数 InvocationHandler 匹配获取某个构造器 + final Constructor cons = cl.getConstructor(constructorParams); + final InvocationHandler ih = h; + // 构造方法不是 pubic 的需要启用权限,暴力p + if (!Modifier.isPublic(cl.getModifiers())) { + AccessController.doPrivileged(new PrivilegedAction() { + public Void run() { + // 设置可访问的权限 + cons.setAccessible(true); + return null; + } + }); + } + // cons 是构造方法,并且内部持有 InvocationHandler,在 InvocationHandler 中持有 target 目标对象 + return cons.newInstance(new Object[]{h}); + } catch (IllegalAccessException|InstantiationException e) {} +} +``` -内存溢出:new 出对象,循环添加字符数据,当堆中没有内存空间可分配给实例,也无法再扩展时,就会抛出 OutOfMemoryError 异常 +Proxy 的静态内部类: -堆内存诊断工具:(控制台命令) +```java +private static final class ProxyClassFactory { + // 代理类型的名称前缀 + private static final String proxyClassNamePrefix = "$Proxy"; -1. jps:查看当前系统中有哪些 java 进程 -2. jmap:查看堆内存占用情况 `jhsdb jmap --heap --pid 进程id` -3. jconsole:图形界面的,多功能的监测工具,可以连续监测 + // 生成唯一数字使用,结合上面的代理类型名称前缀一起生成 + private static final AtomicLong nextUniqueNumber = new AtomicLong(); -在 Java7 中堆内会存在**年轻代、老年代和方法区(永久代)**: + //参数一:Proxy.newInstance 时传递的 + //参数二:Proxy.newInstance 时传递的接口集合 + @Override + public Class apply(ClassLoader loader, Class[] interfaces) { + + Map, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length); + // 遍历接口集合 + for (Class intf : interfaces) { + Class interfaceClass = null; + try { + // 加载接口类到 JVM + interfaceClass = Class.forName(intf.getName(), false, loader); + } catch (ClassNotFoundException e) { + } + if (interfaceClass != intf) { + throw new IllegalArgumentException( + intf + " is not visible from class loader"); + } + // 如果 interfaceClass 不是接口 直接报错,保证集合内都是接口 + if (!interfaceClass.isInterface()) { + throw new IllegalArgumentException( + interfaceClass.getName() + " is not an interface"); + } + // 保证接口 interfaces 集合中没有重复的接口 + if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) { + throw new IllegalArgumentException( + "repeated interface: " + interfaceClass.getName()); + } + } -* Young 区被划分为三部分,Eden 区和两个大小严格相同的 Survivor 区。Survivor 区某一时刻只有其中一个是被使用的,另外一个留做垃圾回收时复制对象。在 Eden 区变满的时候, GC 就会将存活的对象移到空闲的 Survivor 区间中,根据 JVM 的策略,在经过几次垃圾回收后,仍然存活于 Survivor 的对象将被移动到 Tenured 区间 -* Tenured 区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在 Young 复制转移一定的次数以后,对象就会被转移到 Tenured 区 -* Perm 代主要保存 Class、ClassLoader、静态变量、常量、编译后的代码,在 Java7 中堆内方法区会受到 GC 的管理 + // 生成的代理类的包名 + String proxyPkg = null; + // 【生成的代理类访问修饰符 public final】 + int accessFlags = Modifier.PUBLIC | Modifier.FINAL; -分代原因:不同对象的生命周期不同,70%-99% 的对象都是临时对象,优化 GC 性能 + // 检查接口集合内的接口,看看有没有某个接口的访问修饰符不是 public 的 如果不是 public 的接口, + // 生成的代理类 class 就必须和它在一个包下,否则访问出现问题 + for (Class intf : interfaces) { + // 获取访问修饰符 + int flags = intf.getModifiers(); + if (!Modifier.isPublic(flags)) { + accessFlags = Modifier.FINAL; + // 获取当前接口的全限定名 包名.类名 + String name = intf.getName(); + int n = name.lastIndexOf('.'); + // 获取包名 + String pkg = ((n == -1) ? "" : name.substring(0, n + 1)); + if (proxyPkg == null) { + proxyPkg = pkg; + } else if (!pkg.equals(proxyPkg)) { + throw new IllegalArgumentException( + "non-public interfaces from different packages"); + } + } + } -```java -public static void main(String[] args) { - //返回Java虚拟机中的堆内存总量 - long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024; - //返回Java虚拟机使用的最大堆内存量 - long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024; - - System.out.println("-Xms : " + initialMemory + "M");//-Xms : 245M - System.out.println("-Xmx : " + maxMemory + "M");//-Xmx : 3641M + if (proxyPkg == null) { + // if no non-public proxy interfaces, use com.sun.proxy package + proxyPkg = ReflectUtil.PROXY_PACKAGE + "."; + } + + // 获取唯一的编号 + long num = nextUniqueNumber.getAndIncrement(); + // 包名+ $proxy + 数字,比如 $proxy1 + String proxyName = proxyPkg + proxyClassNamePrefix + num; + + // 【生成二进制字节码,这个字节码写入到文件内】,就是编译好的 class 文件 + byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags); + try { + // 【使用加载器加载二进制到 jvm】,并且返回 class + return defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length); + } catch (ClassFormatError e) { } + } } ``` -*** +*** -#### 方法区 -方法区:是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、即时编译器编译后的代码等数据,虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是也叫 Non-Heap(非堆) -方法区是一个 JVM 规范,**永久代与元空间都是其一种实现方式** +#### CGLIB -方法区的大小不必是固定的,可以动态扩展,加载的类太多,可能导致永久代内存溢出 (OutOfMemoryError) +CGLIB 是一个功能强大,高性能的代码生成包,为没有实现接口的类提供代理,为 JDK 动态代理提供了补充($$Proxy) -方法区的 GC:针对常量池的回收及对类型的卸载,比较难实现 +* CGLIB 是第三方提供的包,所以需要引入 jar 包的坐标: -为了**避免方法区出现 OOM**,在 JDK8 中将堆内的方法区(永久代)移动到了本地内存上,重新开辟了一块空间,叫做元空间,元空间存储类的元信息,**静态变量和字符串常量池等放入堆中** + ```xml + + cglib + cglib + 2.2.2 + + ``` -类元信息:在类编译期间放入方法区,存放了类的基本信息,包括类的方法、参数、接口以及常量池表 +* 代理工厂类: -常量池表(Constant Pool Table)是 Class 文件的一部分,存储了**类在编译期间生成的字面量、符号引用**,JVM 为每个已加载的类维护一个常量池 + ```java + public class ProxyFactory implements MethodInterceptor { + private TrainStation target = new TrainStation(); + + public TrainStation getProxyObject() { + //创建Enhancer对象,类似于JDK动态代理的Proxy类,下一步就是设置几个参数 + Enhancer enhancer = new Enhancer(); + //设置父类的字节码对象 + enhancer.setSuperclass(target.getClass()); + //设置回调函数 + enhancer.setCallback(new MethodInterceptor() { + @Override + public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + System.out.println("代理点收取一些服务费用(CGLIB动态代理方式)"); + Object o = methodProxy.invokeSuper(obj, args); + return null;//因为返回值为void + } + }); + //创建代理对象 + TrainStation obj = (TrainStation) enhancer.create(); + return obj; + } + } + ``` -- 字面量:基本数据类型、字符串类型常量、声明为 final 的常量值等 -- 符号引用:类、字段、方法、接口等的符号引用 +CGLIB 的优缺点 -运行时常量池是方法区的一部分 +* 优点: + * CGLIB 动态代理**不限定**是否具有接口,可以对任意操作进行增强 + * CGLIB 动态代理无需要原始被代理对象,动态创建出新的代理对象 + * **JDKProxy 仅对接口方法做增强,CGLIB 对所有方法做增强**,包括 Object 类中的方法,toString、hashCode 等 +* 缺点:CGLIB 不能对声明为 final 的类或者方法进行代理,因为 CGLIB 原理是**动态生成被代理类的子类,继承被代理类** -* 常量池(编译器生成的字面量和符号引用)中的数据会在类加载的加载阶段放入运行时常量池 -* 类在解析阶段将这些符号引用替换成直接引用 -* 除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern() -*** +**** -### 本地内存 -#### 基本介绍 +#### 方式对比 -虚拟机内存:Java 虚拟机在执行的时候会把管理的内存分配成不同的区域,受虚拟机内存大小的参数控制,当大小超过参数设置的大小时就会报 OOM +三种方式对比: -本地内存:又叫做**堆外内存**,线程共享的区域,本地内存这块区域是不会受到 JVM 的控制的,不会发生 GC;因此对于整个 java 的执行效率是提升非常大,但是如果内存的占用超出物理内存的大小,同样也会报 OOM +* 动态代理和静态代理: -本地内存概述图: + * 动态代理将接口中声明的所有方法都被转移到一个集中的方法中处理(InvocationHandler.invoke),在接口方法数量比较多的时候,可以进行灵活处理,不需要像静态代理那样每一个方法进行中转 - + * 静态代理是在编译时就已经将接口、代理类、被代理类的字节码文件确定下来 + * 动态代理是程序**在运行后通过反射创建字节码文件**交由 JVM 加载 +* JDK 代理和 CGLIB 代理: + JDK 动态代理采用 ProxyGenerator.generateProxyClass() 方法在运行时生成字节码;CGLIB 底层采用 ASM 字节码生成框架,使用字节码技术生成代理类。在 JDK1.6之前比使用 Java 反射效率要高,到 JDK1.8 的时候,JDK 代理效率高于 CGLIB 代理。所以如果有接口或者当前类就是接口使用 JDK 动态代理,如果没有接口使用 CGLIB 代理 -*** +代理模式的优缺点: +* 优点: + * 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用 + * **代理对象可以增强目标对象的功能,内部持有原始的目标对象** + * 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度 +* 缺点:增加了系统的复杂度 -#### 元空间 +代理模式的使用场景: -PermGen 被元空间代替,永久代的**类信息、方法、常量池**等都移动到元空间区 - -元空间与永久代区别:元空间不在虚拟机中,使用的本地内存,默认情况下,元空间的大小仅受本地内存限制 +* 远程(Remote)代理:本地服务通过网络请求远程服务,需要实现网络通信,处理其中可能的异常。为了良好的代码设计和可维护性,将网络通信部分隐藏起来,只暴露给本地服务一个接口,通过该接口即可访问远程服务提供的功能 -方法区内存溢出: +* 防火墙(Firewall)代理:当你将浏览器配置成使用代理功能时,防火墙就将你的浏览器的请求转给互联网,当互联网返回响应时,代理服务器再把它转给你的浏览器 -* JDK1.8 以前会导致永久代内存溢出:java.lang.OutOfMemoryError: PerGen space +* 保护(Protect or Access)代理:控制对一个对象的访问,如果需要,可以给不同的用户提供不同级别的使用权限 - ```sh - -XX:MaxPermSize=8m #参数设置 - ``` - -* JDK1.8 以后会导致元空间内存溢出:java.lang.OutOfMemoryError: Metaspace - ```sh - -XX:MaxMetaspaceSize=8m #参数设置 - ``` -元空间内存溢出演示: -```java -public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码 - public static void main(String[] args) { - int j = 0; - try { - Demo1_8 test = new Demo1_8(); - for (int i = 0; i < 10000; i++, j++) { - // ClassWriter 作用是生成类的二进制字节码 - ClassWriter cw = new ClassWriter(0); - // 版本号, public, 类名, 包名, 父类, 接口 - cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null); - // 返回 byte[] - byte[] code = cw.toByteArray(); - // 执行了类的加载 - test.defineClass("Class" + i, code, 0, code.length); // Class 对象 - } - } finally { - System.out.println(j); - } - } -} -``` @@ -10104,172 +10361,193 @@ public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制 -#### 直接内存 -直接内存是 Java 堆外、直接向系统申请的内存区间,不是虚拟机运行时数据区的一部分,也不是《Java 虚拟机规范》中定义的内存区域 +# JVM +## JVM概述 -直接内存详解参考:NET → NIO → 直接内存 +### 基本介绍 +JVM:全称 Java Virtual Machine,即 Java 虚拟机,一种规范,本身是一个虚拟计算机,直接和操作系统进行交互,与硬件不直接交互,而操作系统可以帮我们完成和硬件进行交互的工作 +特点: -*** +* Java 虚拟机基于**二进制字节码**执行,由一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆、一个方法区等组成 +* JVM 屏蔽了与操作系统平台相关的信息,从而能够让 Java 程序只需要生成能够在 JVM 上运行的字节码文件,通过该机制实现的**跨平台性** +Java 代码执行流程:java程序 --(编译)--> 字节码文件 --(解释执行)--> 操作系统(Win,Linux) +JVM 结构: -### 变量位置 + -变量的位置不取决于它是基本数据类型还是引用数据类型,取决于它的**声明位置** +JVM、JRE、JDK 对比: -静态内部类和其他内部类: + -* **一个 class 文件只能对应一个 public 类型的类**,这个类可以有内部类,但不会生成新的 class 文件 -* 静态内部类属于类本身,加载到方法区,其他内部类属于内部类的属性,加载到堆(待考证) -类变量: +参考书籍:https://book.douban.com/subject/34907497/ -* 类变量是用 static 修饰符修饰,定义在方法外的变量,随着 java 进程产生和销毁 -* 在 java8 之前把静态变量存放于方法区,在 java8 时存放在堆中的静态变量区 +参考视频:https://www.bilibili.com/video/BV1PJ411n7xZ +参考视频:https://www.bilibili.com/video/BV1yE411Z7AP -实例变量: -* 实例(成员)变量是定义在类中,没有 static 修饰的变量,随着类的实例产生和销毁,是类实例的一部分 -* 在类初始化的时候,从运行时常量池取出直接引用或者值,**与初始化的对象一起放入堆中** -局部变量: +*** -* 局部变量是定义在类的方法中的变量 -* 在所在方法被调用时**放入虚拟机栈的栈帧**中,方法执行结束后从虚拟机栈中弹出, -类常量池、运行时常量池、字符串常量池有什么关系?有什么区别? -* 类常量池与运行时常量池都存储在方法区,而字符串常量池在 jdk7 时就已经从方法区迁移到了 java 堆中 -* 在类编译过程中,会把类元信息放到方法区,类元信息的其中一部分便是类常量池,主要存放字面量和符号引用,而字面量的一部分便是文本字符 -* **在类加载时将字面量和符号引用解析为直接引用存储在运行时常量池** -* 对于文本字符,会在解析时查找字符串常量池,查出这个文本字符对应的字符串对象的直接引用,将直接引用存储在运行时常量池 +### 架构模型 -什么是字面量?什么是符号引用? +Java 编译器输入的指令流是一种基于栈的指令集架构。因为跨平台的设计,Java 的指令都是根据栈来设计的,不同平台 CPU 架构不同,所以不能设计为基于寄存器架构 -* 字面量:java 代码在编译过程中是无法构建引用的,字面量就是在编译时对于数据的一种表示 +* 基于栈式架构的特点: + * 设计和实现简单,适用于资源受限的系统 + * 使用零地址指令方式分配,执行过程依赖操作栈,指令集更小,编译器容易实现 + * 零地址指令:机器指令的一种,是指令系统中的一种不设地址字段的指令,只有操作码而没有地址码。这种指令有两种情况:一是无需操作数,另一种是操作数为默认的(隐含的),默认为操作数在寄存器(ACC)中,指令可直接访问寄存器 + * 一地址指令:一个操作码对应一个地址码,通过地址码寻找操作数 + * 不需要硬件的支持,可移植性更好,更好实现跨平台 +* 基于寄存器架构的特点: + * 需要硬件的支持,可移植性差 + * 性能更好,执行更高效,寄存器比内存快 + * 以一地址指令、二地址指令、三地址指令为主 - ```java - int a = 1; //这个1便是字面量 - String b = "iloveu"; //iloveu便是字面量 - ``` -* 符号引用:在编译过程中并不知道每个类的地址,因为可能这个类还没有加载,如果在一个类中引用了另一个类,无法知道它的内存地址,只能用他的类名作为符号引用,在类加载完后用这个符号引用去获取内存地址 +*** -*** +### 生命周期 +JVM 的生命周期分为三个阶段,分别为:启动、运行、死亡。 +- **启动**:当启动一个 Java 程序时,通过引导类加载器(bootstrap class loader)创建一个初始类(initial class),对于拥有 main 函数的类就是 JVM 实例运行的起点 +- **运行**: + + - main() 方法是一个程序的初始起点,任何线程均可由在此处启动 + - 在 JVM 内部有两种线程类型,分别为:用户线程和守护线程,**JVM 使用的是守护线程,main() 和其他线程使用的是用户线程**,守护线程会随着用户线程的结束而结束 + - 执行一个 Java 程序时,真真正正在执行的是一个 Java 虚拟机的进程 + - JVM 有两种运行模式 Server 与 Client,两种模式的区别在于:Client 模式启动速度较快,Server 模式启动较慢;但是启动进入稳定期长期运行之后 Server 模式的程序运行速度比 Client 要快很多 + + Server 模式启动的 JVM 采用的是重量级的虚拟机,对程序采用了更多的优化;Client 模式启动的 JVM 采用的是轻量级的虚拟机 +- **死亡**: + + - 当程序中的用户线程都中止,JVM 才会退出 + - 程序正常执行结束、程序异常或错误而异常终止、操作系统错误导致终止 + - 线程调用 Runtime 类 halt 方法或 System 类 exit 方法,并且 java 安全管理器允许这次 exit 或 halt 操作 -## 内存管理 -### 内存分配 -#### 两种方式 +*** -不分配内存的对象无法进行其他操作,JVM 为对象分配内存的过程:首先计算对象占用空间大小,接着在堆中划分一块内存给新对象 -* 如果内存规整,使用指针碰撞(Bump The Pointer)。所有用过的内存在一边,空闲的内存在另外一边,中间有一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离 -* 如果内存不规整,虚拟机需要维护一个空闲列表(Free List)分配。已使用的内存和未使用的内存相互交错,虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容 -*** +## 内存结构 +### 内存概述 +内存结构是 JVM 中非常重要的一部分,是非常重要的系统资源,是硬盘和 CPU 的中间仓库及桥梁,承载着操作系统和应用程序的实时运行,又叫运行时数据区 -#### 分代思想 +JVM 内存结构规定了 Java 在运行过程中内存申请、分配、管理的策略,保证了 JVM 的高效稳定运行 -##### 分代介绍 +* Java1.8 以前的内存结构图: + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java7内存结构图.png) -Java8 时,堆被分为了两份:新生代和老年代(1:2),在 Java7 时,还存在一个永久代 +* Java1.8 之后的内存结果图: -- 新生代使用:复制算法 -- 老年代使用:标记 - 清除 或者 标记 - 整理 算法 + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java8内存结构图.png) -**Minor GC 和 Full GC**: +线程运行诊断: -- Minor GC:回收新生代,新生代对象存活时间很短,所以 Minor GC 会频繁执行,执行的速度比较快 -- Full GC:回收老年代和新生代,老年代对象其存活时间长,所以 Full GC 很少执行,执行速度会比 Minor GC 慢很多 +* 定位:jps 定位进程 id +* jstack 进程 id:用于打印出给定的 java 进程 ID 或 core file 或远程调试服务的 Java 堆栈信息 - Eden 和 Survivor 大小比例默认为 8:1:1 +常见OOM错误: - +* java.lang.StackOverflowError +* java.lang.OutOfMemoryError:java heap space +* java.lang.OutOfMemoryError:GC overhead limit exceeded +* java.lang.OutOfMemoryError:Direct buffer memory +* java.lang.OutOfMemoryError:unable to create new native thread +* java.lang.OutOfMemoryError:Metaspace +*** -*** +### JVM内存 +#### 虚拟机栈 -##### 分代分配 +##### Java栈 -工作机制: +Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所需要的内存 -* **对象优先在 Eden 分配**:当创建一个对象的时候,对象会被分配在新生代的 Eden 区,当Eden 区要满了时候,触发 YoungGC -* 当进行 YoungGC 后,此时在 Eden 区存活的对象被移动到 to 区,并且当前对象的年龄会加 1,清空 Eden 区 -* 当再一次触发 YoungGC 的时候,会把 Eden 区中存活下来的对象和 to 中的对象,移动到 from 区中,这些对象的年龄会加 1,清空 Eden 区和 to 区 -* To 区永远是空 Survivor 区,From 区是有数据的,每次 MinorGC 后两个区域互换 -* From 区和 To 区 也可以叫做 S0 区和 S1 区 +* 每个方法被执行时,都会在虚拟机栈中创建一个栈帧 stack frame(**一个方法一个栈帧**) -晋升到老年代: +* Java 虚拟机规范允许 **Java 栈的大小是动态的或者是固定不变的** -* **长期存活的对象进入老年代**:为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中 - - `-XX:MaxTenuringThreshold`:定义年龄的阈值,对象头中用 4 个 bit 存储,所以最大值是 15,默认也是 15 -* **大对象直接进入老年代**:需要连续内存空间的对象,最典型的大对象是很长的字符串以及数组;避免在 Eden 和 Survivor 之间的大量复制;经常出现大对象会提前触发 GC 以获取足够的连续空间分配给大对象 - - `-XX:PretenureSizeThreshold`:大于此值的对象直接在老年代分配 -* **动态对象年龄判定**:如果在 Survivor 区中相同年龄的对象的所有大小之和超过 Survivor 空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代 +* 虚拟机栈是**每个线程私有的**,每个线程只能有一个活动栈帧,对应方法调用到执行完成的整个过程 -空间分配担保: +* 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存,每个栈帧中存储着: -* 在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的 -* 如果不成立,虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于将尝试着进行一次 Minor GC;如果小于或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。 + * 局部变量表:存储方法里的java基本数据类型以及对象的引用 + * 动态链接:也叫指向运行时常量池的方法引用 + * 方法返回地址:方法正常退出或者异常退出的定义 + * 操作数栈或表达式栈和其他一些附加信息 + + +设置栈内存大小:`-Xss size` `-Xss 1024k` +* 在 JDK 1.4 中默认为 256K,而在 JDK 1.5+ 默认为 1M -*** +虚拟机栈特点: +* 栈内存**不需要进行GC**,方法开始执行的时候会进栈,方法调用后自动弹栈,相当于清空了数据 +* 栈内存分配越大越大,可用的线程数越少(内存越大,每个线程拥有的内存越大) -#### TLAB +* 方法内的局部变量是否**线程安全**: + * 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的(逃逸分析) + * 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全 -TLAB:Thread Local Allocation Buffer,为每个线程在堆内单独分配了一个缓冲区,多线程分配内存时,使用 TLAB 可以避免线程安全问题,同时还能够提升内存分配的吞吐量,这种内存分配方式叫做**快速分配策略** +异常: -- 栈上分配使用的是栈来进行对象内存的分配 -- TLAB 分配使用的是 Eden 区域进行内存分配,属于堆内存 +* 栈帧过多导致栈内存溢出 (超过了栈的容量),会抛出 OutOfMemoryError 异常 +* 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常 -堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据,由于对象实例的创建在 JVM 中非常频繁,因此在并发环境下为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度 -问题:堆空间都是共享的么? 不一定,因为还有 TLAB,在堆中划分出一块区域,为每个线程所独占 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-TLAB内存分配策略.jpg) +*** -JVM 是将 TLAB 作为内存分配的首选,但不是所有的对象实例都能够在 TLAB 中成功分配内存,一旦对象在 TLAB 空间分配内存失败时,JVM 就会通过**使用加锁机制确保数据操作的原子性**,从而直接在堆中分配内存 -栈上分配优先于 TLAB 分配进行,逃逸分析中若可进行栈上分配优化,会优先进行对象栈上直接分配内存 -参数设置: +##### 局部变量 -* `-XX:UseTLAB`:设置是否开启 TLAB 空间 +局部变量表也被称之为局部变量数组或本地变量表,本质上定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量 -* `-XX:TLABWasteTargetPercent`:设置 TLAB 空间所占用 Eden 空间的百分比大小,默认情况下 TLAB 空间的内存非常小,仅占有整个 Eden 空间的1% -* `-XX:TLABRefillWasteFraction`:指当 TLAB 空间不足,请求分配的对象内存大小超过此阈值时不会进行 TLAB 分配,直接进行堆内存分配,否则还是会优先进行 TLAB 分配 +* 表是建立在线程的栈上,是线程私有的数据,因此不存在数据安全问题 +* 表的容量大小是在编译期确定的,保存在方法的 Code 属性的 maximum local variables 数据项中 +* 表中的变量只在当前方法调用中有效,方法结束栈帧销毁,局部变量表也会随之销毁 +* 表中的变量也是重要的垃圾回收根节点,只要被表中数据直接或间接引用的对象都不会被回收 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-TLAB内存分配过程.jpg) +局部变量表最基本的存储单元是 **slot(变量槽)**: + +* 参数值的存放总是在局部变量数组的 index0 开始,到数组长度 -1 的索引结束,JVM 为每一个 slot 都分配一个访问索引,通过索引即可访问到槽中的数据 +* 存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress 类型的变量 +* 32 位以内的类型只占一个 slot(包括 returnAddress 类型),64 位的类型(long 和 double)占两个 slot +* 局部变量表中的槽位是可以**重复利用**的,如果一个局部变量过了其作用域,那么之后申明的新的局部变量就可能会复用过期局部变量的槽位,从而达到节省资源的目的 @@ -10277,223 +10555,183 @@ JVM 是将 TLAB 作为内存分配的首选,但不是所有的对象实例都 -#### 逃逸分析 +##### 操作数栈 -即时编译(Just-in-time Compilation,JIT)是一种通过在运行时将字节码翻译为机器码,从而改善性能的技术,在 HotSpot 实现中有多种选择:C1、C2 和 C1+C2,分别对应 client、server 和分层编译 +栈:可以使用数组或者链表来实现 -* C1 编译速度快,优化方式比较保守;C2 编译速度慢,优化方式比较激进 -* C1+C2 在开始阶段采用 C1 编译,当代码运行到一定热度之后采用 C2 重新编译 +操作数栈:在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)或出栈(pop) -逃逸分析并不是直接的优化手段,而是一个代码分析方式,通过动态分析对象的作用域,为其它优化手段如栈上分配、标量替换和同步消除等提供依据,发生逃逸行为的情况有两种:方法逃逸和线程逃逸 +* 保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间,是执行引擎的一个工作区 -* 方法逃逸:当一个对象在方法中定义之后,被外部方法引用 - * 全局逃逸:一个对象的作用范围逃出了当前方法或者当前线程,比如对象是一个静态变量、全局变量赋值、已经发生逃逸的对象、作为当前方法的返回值 - * 参数逃逸:一个对象被作为方法参数传递或者被参数引用 -* 线程逃逸:如类变量或实例变量,可能被其它线程访问到 +* Java 虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈 +* 如果被调用的方法带有返回值的话,其**返回值将会被压入当前栈帧的操作数栈中** -如果不存在逃逸行为,则可以对该对象进行如下优化:同步消除、标量替换和栈上分配 +栈顶缓存技术 ToS(Top-of-Stack Cashing):将栈顶元素全部缓存在 CPU 的寄存器中,以此降低对内存的读/写次数,提升执行的效率 -* 同步消除 +基于栈式架构的虚拟机使用的零地址指令更加紧凑,完成一项操作需要使用很多入栈和出栈指令,所以需要更多的指令分派(instruction dispatch)次数和内存读/写次数,由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度,所以需要栈顶缓存技术 - 线程同步本身比较耗时,如果确定一个对象不会逃逸出线程,不被其它线程访问到,那对象的读写就不会存在竞争,则可以消除对该对象的**同步锁**,通过 `-XX:+EliminateLocks` 可以开启同步消除 ( - 号关闭) -* 标量替换 - * 标量替换:如果把一个对象拆散,将其成员变量恢复到基本类型来访问 - * 标量 (scalar) :不可分割的量,如基本数据类型和 reference 类型 - - 聚合量 (Aggregate):一个数据可以继续分解,对象一般是聚合量 - * 如果逃逸分析发现一个对象不会被外部访问,并且该对象可以被拆散,那么经过优化之后,并不直接生成该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替 - * 参数设置: - - * `-XX:+EliminateAllocations`:开启标量替换 - * `-XX:+PrintEliminateAllocations`:查看标量替换情况 +*** -* 栈上分配 - JIT 编译器在编译期间根据逃逸分析的结果,如果一个对象没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收,这样就无需 GC - User 对象的作用域局限在方法 fn 中,可以使用标量替换的优化手段在栈上分配对象的成员变量,这样就不会生成 User 对象,大大减轻 GC 的压力 +##### 动态链接 - ```java - public class JVM { - public static void main(String[] args) throws Exception { - int sum = 0; - int count = 1000000; - //warm up - for (int i = 0; i < count ; i++) { - sum += fn(i); - } - System.out.println(sum); - System.in.read(); - } - private static int fn(int age) { - User user = new User(age); - int i = user.getAge(); - return i; - } - } - - class User { - private final int age; +动态链接是指向运行时常量池的方法引用,涉及到栈操作已经是类加载完成,这个阶段的解析是**动态绑定** + +* 为了支持当前方法的代码能够实现动态链接,每一个栈帧内部都包含一个指向运行时常量池或该栈帧所属方法的引用 + + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-动态链接符号引用.png) + +* 在 Java 源文件被编译成字节码文件中,所有的变量和方法引用都作为符号引用保存在 class 的常量池中 - public User(int age) { - this.age = age; - } + 常量池的作用:提供一些符号和常量,便于指令的识别 - public int getAge() { - return age; - } - } - ``` + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-动态链接运行时常量池.png) - -*** +*** -### 回收策略 -#### 触发条件 +##### 返回地址 -内存垃圾回收机制主要集中的区域就是线程共享区域:**堆和方法区** +Return Address:存放调用该方法的 PC 寄存器的值 -Minor GC 触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC +方法的结束有两种方式:正常执行完成、出现未处理的异常,在方法退出后都返回到该方法被调用的位置 -FullGC 同时回收新生代、老年代和方法区,只会存在一个 FullGC 的线程进行执行,其他的线程全部会被**挂起**,有以下触发条件: +* 正常:调用者的 pc 计数器的值作为返回地址,即调用该方法的指令的**下一条指令的地址** +* 异常:返回地址是要通过异常表来确定 -* 调用 System.gc(): +正常完成出口:执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者 - * 在默认情况下,通过 System.gc() 或 Runtime.getRuntime().gc() 的调用,会显式触发 FullGC,同时对老年代和新生代进行回收,但是虚拟机不一定真正去执行,无法保证对垃圾收集器的调用 - * 不建议使用这种方式,应该让虚拟机管理内存。一般情况下,垃圾回收应该是自动进行的,无须手动触发;在一些特殊情况下,如正在编写一个性能基准,可以在运行之间调用 System.gc() +异常完成出口:方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内进行处理,本方法的异常表中没有搜素到匹配的异常处理器,导致方法退出 -* 老年代空间不足: +两者区别:通过异常完成出口退出的不会给上层调用者产生任何的返回值 - * 为了避免引起的 Full GC,应当尽量不要创建过大的对象以及数组 - * 通过 -Xmn 参数调整新生代的大小,让对象尽量在新生代被回收掉不进入老年代,可以通过 `-XX:MaxTenuringThreshold` 调大对象进入老年代的年龄,让对象在新生代多存活一段时间 -* 空间分配担保失败 -* JDK 1.7 及以前的永久代(方法区)空间不足 +##### 附加信息 -* Concurrent Mode Failure:执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC +栈帧中还允许携带与 Java 虚拟机实现相关的一些附加信息,例如对程序调试提供支持的信息 -手动 GC 测试,VM参数:`-XX:+PrintGcDetails` -```java -public void localvarGC1() { - byte[] buffer = new byte[10 * 1024 * 1024];//10MB - System.gc(); //输出: 不会被回收, FullGC时被放入老年代 -} +*** -public void localvarGC2() { - byte[] buffer = new byte[10 * 1024 * 1024]; - buffer = null; - System.gc(); //输出: 正常被回收 -} - public void localvarGC3() { - { - byte[] buffer = new byte[10 * 1024 * 1024]; - } - System.gc(); //输出: 不会被回收, FullGC时被放入老年代 - } -public void localvarGC4() { - { - byte[] buffer = new byte[10 * 1024 * 1024]; - } - int value = 10; - System.gc(); //输出: 正常被回收,slot复用,局部变量过了其作用域 buffer置空 -} -``` +#### 本地方法栈 +本地方法栈是为虚拟机执行本地方法时提供服务的 -*** +JNI:Java Native Interface,通过使用 Java 本地接口书写程序,可以确保代码在不同的平台上方便移植 +* 不需要进行GC,与虚拟机栈类似,也是线程私有的,有 StackOverFlowError 和 OutOfMemoryError 异常 +* 虚拟机栈执行的是 Java 方法,在 HotSpot JVM 中,直接将本地方法栈和虚拟机栈合二为一 -#### 安全区域 +* 本地方法一般是由其他语言编写,并且被编译为基于本机硬件和操作系统的程序 -安全点 (Safepoint):程序执行时并非在所有地方都能停顿下来开始 GC,只有在安全点才能停下 +* 当某个线程调用一个本地方法时,就进入了不再受虚拟机限制的世界,和虚拟机拥有同样的权限 -- Safe Point 的选择很重要,如果太少可能导致 GC 等待的时间太长,如果太多可能导致运行时的性能问题 -- 大部分指令的执行时间都非常短,通常会根据是否具有让程序长时间执行的特征为标准,选择些执行时间较长的指令作为 Safe Point, 如方法调用、循环跳转和异常跳转等 + * 本地方法可以通过本地方法接口来**访问虚拟机内部的运行时数据区** + * 直接从本地内存的堆中分配任意数量的内存 + * 可以直接使用本地处理器中的寄存器 + + + + -在 GC 发生时,让所有线程都在最近的安全点停顿下来的方法: +图片来源:https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20%E8%99%9A%E6%8B%9F%E6%9C%BA.md -- 抢先式中断:没有虚拟机采用,首先中断所有线程,如果有线程不在安全点,就恢复线程让线程运行到安全点 -- 主动式中断:设置一个中断标志,各个线程运行到各个 Safe Point 时就轮询这个标志,如果中断标志为真,则将自己进行中断挂起 -问题:Safepoint 保证程序执行时,在不太长的时间内就会遇到可进入 GC 的 Safepoint,但是当线程处于 Waiting 状态或 Blocked 状态,线程无法响应 JVM 的中断请求,运行到安全点去中断挂起,JVM 也不可能等待线程被唤醒,对于这种情况,需要安全区域来解决 -安全区域 (Safe Region):指在一段代码片段中,**对象的引用关系不会发生变化**,在这个区域中的任何位置开始 GC 都是安全的 +*** -运行流程: -- 当线程运行到 Safe Region 的代码时,首先标识已经进入了 Safe Region,如果这段时间内发生GC,JVM 会忽略标识为 Safe Region 状态的线程 -- 当线程即将离开 Safe Region 时,会检查 JVM 是否已经完成 GC,如果完成了则继续运行,否则线程必须等待 GC 完成,收到可以安全离开 SafeRegion 的信号 +#### 程序计数器 +Program Counter Register 程序计数器(寄存器) +作用:内部保存字节码的行号,用于记录正在执行的字节码指令地址(如果正在执行的是本地方法则为空) -*** +原理: +* JVM 对于多线程是通过线程轮流切换并且分配线程执行时间,一个处理器只会处理执行一个线程 +* 切换线程需要从程序计数器中来回去到当前的线程上一次执行的行号 +特点: -### 垃圾判断 +* 是线程私有的 +* **不会存在内存溢出**,是 JVM 规范中唯一一个不出现 OOM 的区域,所以这个空间不会进行 GC -#### 垃圾介绍 +Java 反编译指令:`javap -v Test.class` -垃圾:**如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾** +#20:代表去 Constant pool 查看该地址的指令 -作用:释放没用的对象,清除内存里的记录碎片,碎片整理将所占用的堆内存移到堆的一端,以便 JVM 将整理出的内存分配给新的对象 +```java +0: getstatic #20 // PrintStream out = System.out; +3: astore_1 // -- +4: aload_1 // out.println(1); +5: iconst_1 // -- +6: invokevirtual #26 // -- +9: aload_1 // out.println(2); +10: iconst_2 // -- +11: invokevirtual #26 // -- +``` -垃圾收集主要是针对堆和方法区进行,程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收 -在堆里存放着几乎所有的 Java 对象实例,在 GC 执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC 才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程可以称为垃圾标记阶段,判断对象存活一般有两种方式:**引用计数算法**和**可达性分析算法** +**** -*** +#### 堆 +Heap 堆:是 JVM 内存中最大的一块,由所有线程共享,由垃圾回收器管理的主要区域,堆中对象大部分都需要考虑线程安全的问题 -#### 引用计数法 +存放哪些资源: -引用计数算法(Reference Counting):对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。对于一个对象 A,只要有任何一个对象引用了 A,则 A 的引用计数器就加 1;当引用失效时,引用计数器就减 1;当对象 A 的引用计数器的值为 0,即表示对象A不可能再被使用,可进行回收(Java 没有采用) +* 对象实例:类初始化生成的对象,**基本数据类型的数组也是对象实例**,new 创建对象都使用堆内存 +* 字符串常量池: + * 字符串常量池原本存放于方法区,jdk7 开始放置于堆中 + * 字符串常量池**存储的是 String 对象的直接引用或者对象**,是一张 string table +* 静态变量:静态变量是有 static 修饰的变量,jdk7 时从方法区迁移至堆中 +* 线程分配缓冲区 Thread Local Allocation Buffer:线程私有但不影响堆的共性,可以提升对象分配的效率 -优点: +设置堆内存指令:`-Xmx Size` -- 回收没有延迟性,无需等到内存不够的时候才开始回收,运行时根据对象计数器是否为 0,可以直接回收 -- 在垃圾回收过程中,应用无需挂起;如果申请内存时,内存不足,则立刻报 OOM 错误 -- 区域性,更新对象的计数器时,只是影响到该对象,不会扫描全部对象 +内存溢出:new 出对象,循环添加字符数据,当堆中没有内存空间可分配给实例,也无法再扩展时,就会抛出 OutOfMemoryError 异常 -缺点: +堆内存诊断工具:(控制台命令) -- 每次对象被引用时,都需要去更新计数器,有一点时间开销 +1. jps:查看当前系统中有哪些 java 进程 +2. jmap:查看堆内存占用情况 `jhsdb jmap --heap --pid 进程id` +3. jconsole:图形界面的,多功能的监测工具,可以连续监测 -- 浪费 CPU 资源,即使内存够用,仍然在运行时进行计数器的统计。 +在 Java7 中堆内会存在**年轻代、老年代和方法区(永久代)**: -- **无法解决循环引用问题,会引发内存泄露**(最大的缺点) +* Young 区被划分为三部分,Eden 区和两个大小严格相同的 Survivor 区。Survivor 区某一时刻只有其中一个是被使用的,另外一个留做垃圾回收时复制对象。在 Eden 区变满的时候, GC 就会将存活的对象移到空闲的 Survivor 区间中,根据 JVM 的策略,在经过几次垃圾回收后,仍然存活于 Survivor 的对象将被移动到 Tenured 区间 +* Tenured 区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在 Young 复制转移一定的次数以后,对象就会被转移到 Tenured 区 +* Perm 代主要保存 Class、ClassLoader、静态变量、常量、编译后的代码,在 Java7 中堆内方法区会受到 GC 的管理 - ```java - public class Test { - public Object instance = null; - public static void main(String[] args) { - Test a = new Test();// a = 1 - Test b = new Test();// b = 1 - a.instance = b; // b = 2 - b.instance = a; // a = 2 - a = null; // a = 1 - b = null; // b = 1 - } - } - ``` +分代原因:不同对象的生命周期不同,70%-99% 的对象都是临时对象,优化 GC 性能 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-循环引用.png) +```java +public static void main(String[] args) { + //返回Java虚拟机中的堆内存总量 + long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024; + //返回Java虚拟机使用的最大堆内存量 + long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024; + + System.out.println("-Xms : " + initialMemory + "M");//-Xms : 245M + System.out.println("-Xmx : " + maxMemory + "M");//-Xmx : 3641M +} +``` @@ -10501,159 +10739,164 @@ public void localvarGC4() { -#### 可达性分析 +#### 方法区 -##### GC Roots +方法区:是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、即时编译器编译后的代码等数据,虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是也叫 Non-Heap(非堆) -可达性分析算法:也可以称为根搜索算法、追踪性垃圾收集 +方法区是一个 JVM 规范,**永久代与元空间都是其一种实现方式** -GC Roots 对象: +方法区的大小不必是固定的,可以动态扩展,加载的类太多,可能导致永久代内存溢出 (OutOfMemoryError) -- 虚拟机栈中局部变量表中引用的对象:各个线程被调用的方法中使用到的参数、局部变量等 -- 本地方法栈中引用的对象 -- 堆中类静态属性引用的对象 -- 方法区中的常量引用的对象 -- 字符串常量池(string Table)里的引用 -- 同步锁 synchronized 持有的对象 +方法区的 GC:针对常量池的回收及对类型的卸载,比较难实现 -**GC Roots 是一组活跃的引用,不是对象**,放在 GC Roots Set 集合 +为了**避免方法区出现 OOM**,在 JDK8 中将堆内的方法区(永久代)移动到了本地内存上,重新开辟了一块空间,叫做元空间,元空间存储类的元信息,**静态变量和字符串常量池等放入堆中** +类元信息:在类编译期间放入方法区,存放了类的基本信息,包括类的方法、参数、接口以及常量池表 +常量池表(Constant Pool Table)是 Class 文件的一部分,存储了**类在编译期间生成的字面量、符号引用**,JVM 为每个已加载的类维护一个常量池 -*** +- 字面量:基本数据类型、字符串类型常量、声明为 final 的常量值等 +- 符号引用:类、字段、方法、接口等的符号引用 +运行时常量池是方法区的一部分 +* 常量池(编译器生成的字面量和符号引用)中的数据会在类加载的加载阶段放入运行时常量池 +* 类在解析阶段将这些符号引用替换成直接引用 +* 除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern() -##### 工作原理 -可达性分析算法以根对象集合(GCRoots)为起始点,从上至下的方式搜索被根对象集合所连接的目标对象 -分析工作必须在一个保障**一致性的快照**中进行,否则结果的准确性无法保证,这也是导致 GC 进行时必须 Stop The World 的一个原因 +*** -基本原理: -- 可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索走过的路径称为引用链 -- 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象 +### 本地内存 -- 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象 +#### 基本介绍 - +虚拟机内存:Java 虚拟机在执行的时候会把管理的内存分配成不同的区域,受虚拟机内存大小的参数控制,当大小超过参数设置的大小时就会报 OOM +本地内存:又叫做**堆外内存**,线程共享的区域,本地内存这块区域是不会受到 JVM 的控制的,不会发生 GC;因此对于整个 java 的执行效率是提升非常大,但是如果内存的占用超出物理内存的大小,同样也会报 OOM +本地内存概述图: -*** + -##### 三色标记 +*** -###### 标记算法 -三色标记法把遍历对象图过程中遇到的对象,标记成以下三种颜色: -- 白色:尚未访问过 -- 灰色:本对象已访问过,但是本对象引用到的其他对象尚未全部访问 -- 黑色:本对象已访问过,而且本对象引用到的其他对象也全部访问完成 - -当 Stop The World (STW) 时,对象间的引用是不会发生变化的,可以轻松完成标记,遍历访问过程为: - -1. 初始时,所有对象都在白色集合 -2. 将 GC Roots 直接引用到的对象挪到灰色集合 -3. 从灰色集合中获取对象: - * 将本对象引用到的其他对象全部挪到灰色集合中 - * 将本对象挪到黑色集合里面 -4. 重复步骤 3,直至灰色集合为空时结束 -5. 结束后,仍在白色集合的对象即为 GC Roots 不可达,可以进行回收 - - +#### 元空间 +PermGen 被元空间代替,永久代的**类信息、方法、常量池**等都移动到元空间区 +元空间与永久代区别:元空间不在虚拟机中,使用的本地内存,默认情况下,元空间的大小仅受本地内存限制 -参考文章:https://www.jianshu.com/p/12544c0ad5c1 +方法区内存溢出: +* JDK1.8 以前会导致永久代内存溢出:java.lang.OutOfMemoryError: PerGen space + ```sh + -XX:MaxPermSize=8m #参数设置 + ``` + +* JDK1.8 以后会导致元空间内存溢出:java.lang.OutOfMemoryError: Metaspace -**** + ```sh + -XX:MaxMetaspaceSize=8m #参数设置 + ``` +元空间内存溢出演示: +```java +public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码 + public static void main(String[] args) { + int j = 0; + try { + Demo1_8 test = new Demo1_8(); + for (int i = 0; i < 10000; i++, j++) { + // ClassWriter 作用是生成类的二进制字节码 + ClassWriter cw = new ClassWriter(0); + // 版本号, public, 类名, 包名, 父类, 接口 + cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null); + // 返回 byte[] + byte[] code = cw.toByteArray(); + // 执行了类的加载 + test.defineClass("Class" + i, code, 0, code.length); // Class 对象 + } + } finally { + System.out.println(j); + } + } +} +``` -###### 并发标记 -并发标记时,对象间的引用可能发生变化**,**多标和漏标的情况就有可能发生 -**多标情况:**当 E 变为灰色或黑色时,其他线程断开的 D 对 E 的引用,导致这部分对象仍会被标记为存活,本轮 GC 不会回收这部分内存,这部分本应该回收但是没有回收到的内存,被称之为**浮动垃圾** +*** -* 针对并发标记开始后的**新对象**,通常的做法是直接全部当成黑色,也算浮动垃圾 -* 浮动垃圾并不会影响应用程序的正确性,只是需要等到下一轮垃圾回收中才被清除 - -**漏标情况:** +#### 直接内存 -* 条件一:灰色对象断开了对一个白色对象的引用(直接或间接),即灰色对象原成员变量的引用发生了变化 -* 条件二:其他线程中修改了黑色对象,插入了一条或多条对该白色对象的新引用 -* 结果:导致该白色对象当作垃圾被 GC,影响到了应用程序的正确性 +直接内存是 Java 堆外、直接向系统申请的内存区间,不是虚拟机运行时数据区的一部分,也不是《Java 虚拟机规范》中定义的内存区域 - -代码角度解释漏标: -```java -Object G = objE.fieldG; // 读 -objE.fieldG = null; // 写 -objD.fieldG = G; // 写 -``` +直接内存详解参考:NET → NIO → 直接内存 -为了解决问题,可以操作上面三步,**将对象 G 记录起来,然后作为灰色对象再进行遍历**,比如放到一个特定的集合,等初始的 GC Roots 遍历完(并发标记),再遍历该集合(重新标记) -> 所以**重新标记需要 STW**,应用程序一直在运行,该集合可能会一直增加新的对象,导致永远都运行不完 -解决方法:添加读写屏障,读屏障拦截第一步,写屏障拦截第二三步,在读写前后进行一些后置处理: +*** -* **写屏障 + 增量更新**:黑色对象新增引用,会将黑色对象变成灰色对象,最后对该节节点重新扫描 - 增量更新 (Incremental Update) 破坏了条件二,从而保证了不会漏标 - 缺点:对黑色变灰的对象重新扫描所有引用,比较耗费时间 +### 变量位置 -* **写屏障 (Store Barrier) + SATB**:当原来成员变量的引用发生变化之前,记录下原来的引用对象 +变量的位置不取决于它是基本数据类型还是引用数据类型,取决于它的**声明位置** - 保留 GC 开始时的对象图,即原始快照 SATB,当 GC Roots 确定后,对象图就已经确定,那后续的标记也应该是按照这个时刻的对象图走,如果期间对白色对象有了新的引用会记录下来,并且将白色对象变灰(说明可达了),重新扫描该对象的引用关系 +静态内部类和其他内部类: - SATB (Snapshot At The Beginning) 破坏了条件一,从而保证了不会漏标 +* **一个 class 文件只能对应一个 public 类型的类**,这个类可以有内部类,但不会生成新的 class 文件 -* **读屏障 (Load Barrier)**:破坏条件二,黑色对象引用白色对象的前提是获取到该对象,此时读屏障发挥作用 +* 静态内部类属于类本身,加载到方法区,其他内部类属于内部类的属性,加载到堆(待考证) -以 Java HotSpot VM 为例,其并发标记时对漏标的处理方案如下: +类变量: -- CMS:写屏障 + 增量更新 -- G1:写屏障 + SATB -- ZGC:读屏障 +* 类变量是用 static 修饰符修饰,定义在方法外的变量,随着 java 进程产生和销毁 +* 在 java8 之前把静态变量存放于方法区,在 java8 时存放在堆中的静态变量区 +实例变量: -*** +* 实例(成员)变量是定义在类中,没有 static 修饰的变量,随着类的实例产生和销毁,是类实例的一部分 +* 在类初始化的时候,从运行时常量池取出直接引用或者值,**与初始化的对象一起放入堆中** +局部变量: +* 局部变量是定义在类的方法中的变量 +* 在所在方法被调用时**放入虚拟机栈的栈帧**中,方法执行结束后从虚拟机栈中弹出, -#### finalization +类常量池、运行时常量池、字符串常量池有什么关系?有什么区别? -Java 语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑 +* 类常量池与运行时常量池都存储在方法区,而字符串常量池在 jdk7 时就已经从方法区迁移到了 java 堆中 +* 在类编译过程中,会把类元信息放到方法区,类元信息的其中一部分便是类常量池,主要存放字面量和符号引用,而字面量的一部分便是文本字符 +* **在类加载时将字面量和符号引用解析为直接引用存储在运行时常量池** +* 对于文本字符,会在解析时查找字符串常量池,查出这个文本字符对应的字符串对象的直接引用,将直接引用存储在运行时常量池 -垃圾回收此对象之前,会先调用这个对象的 finalize() 方法,finalize() 方法允许在子类中被重写,用于在对象被回收时进行后置处理,通常在这个方法中进行一些资源释放和清理,比如关闭文件、套接字和数据库连接等 +什么是字面量?什么是符号引用? -生存 OR 死亡:如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用,此对象需要被回收。但事实上这时候它们暂时处于缓刑阶段。**一个无法触及的对象有可能在某个条件下复活自己**,这样对它的回收就是不合理的,所以虚拟机中的对象可能的三种状态: +* 字面量:java 代码在编译过程中是无法构建引用的,字面量就是在编译时对于数据的一种表示 -- 可触及的:从根节点开始,可以到达这个对象。 -- 可复活的:对象的所有引用都被释放,但是对象有可能在finalize() 中复活 -- 不可触及的:对象的 finalize() 被调用并且没有复活,那么就会进入不可触及状态,不可触及的对象不可能被复活,因为 **finalize() 只会被调用一次**,等到这个对象再被标记为可回收时就必须回收 + ```java + int a = 1; //这个1便是字面量 + String b = "iloveu"; //iloveu便是字面量 + ``` -永远不要主动调用某个对象的 finalize() 方法,应该交给垃圾回收机制调用,原因: +* 符号引用:在编译过程中并不知道每个类的地址,因为可能这个类还没有加载,如果在一个类中引用了另一个类,无法知道它的内存地址,只能用他的类名作为符号引用,在类加载完后用这个符号引用去获取内存地址 -* finalize() 时可能会导致对象复活 -* finalize() 方法的执行时间是没有保障的,完全由 GC 线程决定,极端情况下,若不发生 GC,则 finalize() 方法将没有执行机会,因为优先级比较低,即使主动调用该方法,也不会因此就直接进行回收 -* 一个糟糕的 finalize() 会严重影响 GC 的性能 @@ -10661,87 +10904,75 @@ Java 语言提供了对象终止(finalization)机制来允许开发人员提 -#### 引用分析 -无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关,Java 提供了四种强度不同的引用类型 -1. 强引用:被强引用关联的对象不会被回收,只有所有 GCRoots 都不通过强引用引用该对象,才能被垃圾回收 +## 内存管理 - * 强引用可以直接访问目标对象 - * 虚拟机宁愿抛出 OOM 异常,也不会回收强引用所指向对象 - * 强引用可能导致**内存泄漏** +### 内存分配 - ```java - Object obj = new Object();//使用 new 一个新对象的方式来创建强引用 - ``` +#### 两种方式 -2. 软引用(SoftReference):被软引用关联的对象只有在内存不够的情况下才会被回收 +不分配内存的对象无法进行其他操作,JVM 为对象分配内存的过程:首先计算对象占用空间大小,接着在堆中划分一块内存给新对象 - * **仅(可能有强引用,一个对象可以被多个引用)**有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象 - * 配合**引用队列来释放软引用自身**,在构造软引用时,可以指定一个引用队列,当软引用对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况 - * 软引用通常用来实现内存敏感的缓存,比如高速缓存就有用到软引用;如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时不会耗尽内存 +* 如果内存规整,使用指针碰撞(Bump The Pointer)。所有用过的内存在一边,空闲的内存在另外一边,中间有一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离 +* 如果内存不规整,虚拟机需要维护一个空闲列表(Free List)分配。已使用的内存和未使用的内存相互交错,虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容 - ```java - Object obj = new Object(); - SoftReference sf = new SoftReference(obj); - obj = null; // 使对象只被软引用关联 - ``` -3. 弱引用(WeakReference):被弱引用关联的对象一定会被回收,只能存活到下一次垃圾回收发生之前 - * 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象 - * 配合引用队列来释放弱引用自身 - * WeakHashMap 用来存储图片信息,可以在内存不足的时候及时回收,避免了 OOM +*** - ```java - Object obj = new Object(); - WeakReference wf = new WeakReference(obj); - obj = null; - ``` -4. 虚引用(PhantomReference):也称为幽灵引用或者幻影引用,是所有引用类型中最弱的一个 - * 一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象 - * 为对象设置虚引用的唯一目的是在于跟踪垃圾回收过程,能在这个对象被回收时收到一个系统通知 - * 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存 +#### 分代思想 - ```java - Object obj = new Object(); - PhantomReference pf = new PhantomReference(obj, null); - obj = null; - ``` +##### 分代介绍 -5. 终结器引用(finalization) +Java8 时,堆被分为了两份:新生代和老年代(1:2),在 Java7 时,还存在一个永久代 +- 新生代使用:复制算法 +- 老年代使用:标记 - 清除 或者 标记 - 整理 算法 +**Minor GC 和 Full GC**: -*** +- Minor GC:回收新生代,新生代对象存活时间很短,所以 Minor GC 会频繁执行,执行的速度比较快 +- Full GC:回收老年代和新生代,老年代对象其存活时间长,所以 Full GC 很少执行,执行速度会比 Minor GC 慢很多 + Eden 和 Survivor 大小比例默认为 8:1:1 + -#### 无用属性 -##### 无用类 -方法区主要回收的是无用的类 -判定一个类是否是无用的类,需要同时满足下面 3 个条件: -- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例 -- 加载该类的 `ClassLoader` 已经被回收 -- 该类对应的 `java.lang.Class` 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法 +*** -虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的**仅仅是可以**,而并不是和对象一样不使用了就会必然被回收 +##### 分代分配 -*** +工作机制: +* **对象优先在 Eden 分配**:当创建一个对象的时候,对象会被分配在新生代的 Eden 区,当Eden 区要满了时候,触发 YoungGC +* 当进行 YoungGC 后,此时在 Eden 区存活的对象被移动到 to 区,并且当前对象的年龄会加 1,清空 Eden 区 +* 当再一次触发 YoungGC 的时候,会把 Eden 区中存活下来的对象和 to 中的对象,移动到 from 区中,这些对象的年龄会加 1,清空 Eden 区和 to 区 +* To 区永远是空 Survivor 区,From 区是有数据的,每次 MinorGC 后两个区域互换 +* From 区和 To 区 也可以叫做 S0 区和 S1 区 +晋升到老年代: -##### 废弃常量 +* **长期存活的对象进入老年代**:为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中 + + `-XX:MaxTenuringThreshold`:定义年龄的阈值,对象头中用 4 个 bit 存储,所以最大值是 15,默认也是 15 +* **大对象直接进入老年代**:需要连续内存空间的对象,最典型的大对象是很长的字符串以及数组;避免在 Eden 和 Survivor 之间的大量复制;经常出现大对象会提前触发 GC 以获取足够的连续空间分配给大对象 + + `-XX:PretenureSizeThreshold`:大于此值的对象直接在老年代分配 +* **动态对象年龄判定**:如果在 Survivor 区中相同年龄的对象的所有大小之和超过 Survivor 空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代 -在常量池中存在字符串 "abc",如果当前没有任何 String 对象引用该常量,说明常量 "abc" 是废弃常量,如果这时发生内存回收的话**而且有必要的话**(内存不够用),"abc" 就会被系统清理出常量池 +空间分配担保: + +* 在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的 +* 如果不成立,虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于将尝试着进行一次 Minor GC;如果小于或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。 @@ -10749,163 +10980,215 @@ Java 语言提供了对象终止(finalization)机制来允许开发人员提 -##### 静态变量 +#### TLAB -类加载时(第一次访问),这个类中所有静态成员就会被加载到静态变量区,该区域的成员一旦创建,直到程序退出才会被回收 +TLAB:Thread Local Allocation Buffer,为每个线程在堆内单独分配了一个缓冲区,多线程分配内存时,使用 TLAB 可以避免线程安全问题,同时还能够提升内存分配的吞吐量,这种内存分配方式叫做**快速分配策略** -如果是静态引用类型的变量,静态变量区只存储一份对象的引用地址,真正的对象在堆内,如果要回收该对象可以设置引用为 null +- 栈上分配使用的是栈来进行对象内存的分配 +- TLAB 分配使用的是 Eden 区域进行内存分配,属于堆内存 +堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据,由于对象实例的创建在 JVM 中非常频繁,因此在并发环境下为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度 +问题:堆空间都是共享的么? 不一定,因为还有 TLAB,在堆中划分出一块区域,为每个线程所独占 -参考文章:https://blog.csdn.net/zhengzhb/article/details/7331354 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-TLAB内存分配策略.jpg) +JVM 是将 TLAB 作为内存分配的首选,但不是所有的对象实例都能够在 TLAB 中成功分配内存,一旦对象在 TLAB 空间分配内存失败时,JVM 就会通过**使用加锁机制确保数据操作的原子性**,从而直接在堆中分配内存 +栈上分配优先于 TLAB 分配进行,逃逸分析中若可进行栈上分配优化,会优先进行对象栈上直接分配内存 -*** +参数设置: +* `-XX:UseTLAB`:设置是否开启 TLAB 空间 +* `-XX:TLABWasteTargetPercent`:设置 TLAB 空间所占用 Eden 空间的百分比大小,默认情况下 TLAB 空间的内存非常小,仅占有整个 Eden 空间的1% +* `-XX:TLABRefillWasteFraction`:指当 TLAB 空间不足,请求分配的对象内存大小超过此阈值时不会进行 TLAB 分配,直接进行堆内存分配,否则还是会优先进行 TLAB 分配 -### 回收算法 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-TLAB内存分配过程.jpg) -#### 标记清除 -当成功区分出内存中存活对象和死亡对象后,GC 接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。目前在 JVM 中比较常见的三种垃圾收集算法是 -- 标记清除算法(Mark-Sweep) -- 复制算法(copying) -- 标记压缩算法(Mark-Compact) +*** -标记清除算法,是将垃圾回收分为两个个阶段,分别是**标记和清除** -- **标记**:Collector 从引用根节点开始遍历,标记所有被引用的对象,一般是在对象的 Header 中记录为可达对象,**标记的是引用的对象,不是垃圾** -- **清除**:Collector 对堆内存从头到尾进行线性的遍历,如果发现某个对象在其 Header 中没有标记为可达对象,则将其回收,把分块连接到 **空闲列表**”的单向链表,判断回收后的分块与前一个空闲分块是否连续,若连续会合并这两个分块,之后进行分配时只需要遍历这个空闲列表,就可以找到分块 -- **分配阶段**:程序会搜索空闲链表寻找空间大于等于新对象大小 size 的块 block,如果找到的块等于 size,会直接返回这个分块;如果找到的块大于 size,会将块分割成大小为 size 与 block - size 的两部分,返回大小为 size 的分块,并把大小为 block - size 的块返回给空闲列表 +#### 逃逸分析 -算法缺点: +即时编译(Just-in-time Compilation,JIT)是一种通过在运行时将字节码翻译为机器码,从而改善性能的技术,在 HotSpot 实现中有多种选择:C1、C2 和 C1+C2,分别对应 client、server 和分层编译 -- 标记和清除过程效率都不高 -- 会产生大量不连续的内存碎片,导致无法给大对象分配内存,需要维护一个空闲链表 +* C1 编译速度快,优化方式比较保守;C2 编译速度慢,优化方式比较激进 +* C1+C2 在开始阶段采用 C1 编译,当代码运行到一定热度之后采用 C2 重新编译 - +逃逸分析并不是直接的优化手段,而是一个代码分析方式,通过动态分析对象的作用域,为其它优化手段如栈上分配、标量替换和同步消除等提供依据,发生逃逸行为的情况有两种:方法逃逸和线程逃逸 +* 方法逃逸:当一个对象在方法中定义之后,被外部方法引用 + * 全局逃逸:一个对象的作用范围逃出了当前方法或者当前线程,比如对象是一个静态变量、全局变量赋值、已经发生逃逸的对象、作为当前方法的返回值 + * 参数逃逸:一个对象被作为方法参数传递或者被参数引用 +* 线程逃逸:如类变量或实例变量,可能被其它线程访问到 +如果不存在逃逸行为,则可以对该对象进行如下优化:同步消除、标量替换和栈上分配 -*** - +* 同步消除 + 线程同步本身比较耗时,如果确定一个对象不会逃逸出线程,不被其它线程访问到,那对象的读写就不会存在竞争,则可以消除对该对象的**同步锁**,通过 `-XX:+EliminateLocks` 可以开启同步消除 ( - 号关闭) -#### 复制算法 +* 标量替换 -复制算法的核心就是,**将原有的内存空间一分为二,每次只用其中的一块**,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清理,交换两个内存的角色,完成垃圾的回收 + * 标量替换:如果把一个对象拆散,将其成员变量恢复到基本类型来访问 + * 标量 (scalar) :不可分割的量,如基本数据类型和 reference 类型 + + 聚合量 (Aggregate):一个数据可以继续分解,对象一般是聚合量 + * 如果逃逸分析发现一个对象不会被外部访问,并且该对象可以被拆散,那么经过优化之后,并不直接生成该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替 + * 参数设置: + + * `-XX:+EliminateAllocations`:开启标量替换 + * `-XX:+PrintEliminateAllocations`:查看标量替换情况 -应用场景:如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之则不适合 +* 栈上分配 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-复制算法.png) + JIT 编译器在编译期间根据逃逸分析的结果,如果一个对象没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收,这样就无需 GC -算法优点: + User 对象的作用域局限在方法 fn 中,可以使用标量替换的优化手段在栈上分配对象的成员变量,这样就不会生成 User 对象,大大减轻 GC 的压力 -- 没有标记和清除过程,实现简单,运行高效 -- 复制过去以后保证空间的连续性,不会出现“碎片”问题。 + ```java + public class JVM { + public static void main(String[] args) throws Exception { + int sum = 0; + int count = 1000000; + //warm up + for (int i = 0; i < count ; i++) { + sum += fn(i); + } + System.out.println(sum); + System.in.read(); + } + private static int fn(int age) { + User user = new User(age); + int i = user.getAge(); + return i; + } + } + + class User { + private final int age; + + public User(int age) { + this.age = age; + } + + public int getAge() { + return age; + } + } + ``` -算法缺点: + -- 主要不足是**只使用了内存的一半** -- 对于 G1 这种分拆成为大量 region 的 GC,复制而不是移动,意味着 GC 需要维护 region 之间对象引用关系,不管是内存占用或者时间开销都不小 +*** -现在的商业虚拟机都采用这种收集算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间 +### 回收策略 -*** +#### 触发条件 +内存垃圾回收机制主要集中的区域就是线程共享区域:**堆和方法区** +Minor GC 触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC -#### 标记整理 +FullGC 同时回收新生代、老年代和方法区,只会存在一个 FullGC 的线程进行执行,其他的线程全部会被**挂起**,有以下触发条件: -标记整理(压缩)算法是在标记清除算法的基础之上,做了优化改进的算法 +* 调用 System.gc(): -标记阶段和标记清除算法一样,也是从根节点开始,对对象的引用进行标记,在清理阶段,并不是简单的直接清理可回收对象,而是将存活对象都向内存另一端移动,然后清理边界以外的垃圾,从而**解决了碎片化**的问题 + * 在默认情况下,通过 System.gc() 或 Runtime.getRuntime().gc() 的调用,会显式触发 FullGC,同时对老年代和新生代进行回收,但是虚拟机不一定真正去执行,无法保证对垃圾收集器的调用 + * 不建议使用这种方式,应该让虚拟机管理内存。一般情况下,垃圾回收应该是自动进行的,无须手动触发;在一些特殊情况下,如正在编写一个性能基准,可以在运行之间调用 System.gc() -优点:不会产生内存碎片 +* 老年代空间不足: -缺点:需要移动大量对象,处理效率比较低 + * 为了避免引起的 Full GC,应当尽量不要创建过大的对象以及数组 + * 通过 -Xmn 参数调整新生代的大小,让对象尽量在新生代被回收掉不进入老年代,可以通过 `-XX:MaxTenuringThreshold` 调大对象进入老年代的年龄,让对象在新生代多存活一段时间 - +* 空间分配担保失败 -| | Mark-Sweep | Mark-Compact | Copying | -| -------- | ---------------- | -------------- | ----------------------------------- | -| 速度 | 中等 | 最慢 | 最快 | -| 空间开销 | 少(但会堆积碎片) | 少(不堆积碎片) | 通常需要活对象的2倍大小(不堆积碎片) | -| 移动对象 | 否 | 是 | 是 | +* JDK 1.7 及以前的永久代(方法区)空间不足 -- 效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存。 -- 为了尽量兼顾三个指标,标记一整理算法相对来说更平滑一些 +* Concurrent Mode Failure:执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC +手动 GC 测试,VM参数:`-XX:+PrintGcDetails` -*** +```java +public void localvarGC1() { + byte[] buffer = new byte[10 * 1024 * 1024];//10MB + System.gc(); //输出: 不会被回收, FullGC时被放入老年代 +} +public void localvarGC2() { + byte[] buffer = new byte[10 * 1024 * 1024]; + buffer = null; + System.gc(); //输出: 正常被回收 +} + public void localvarGC3() { + { + byte[] buffer = new byte[10 * 1024 * 1024]; + } + System.gc(); //输出: 不会被回收, FullGC时被放入老年代 + } +public void localvarGC4() { + { + byte[] buffer = new byte[10 * 1024 * 1024]; + } + int value = 10; + System.gc(); //输出: 正常被回收,slot复用,局部变量过了其作用域 buffer置空 +} +``` -#### 增量收集 -增量收集算法:通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作,基础仍是标记-清除和复制算法,用于多线程并发环境 -工作原理:如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,影响系统的交互性,所以让垃圾收集线程和应用程序线程交替执行,每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程,依次反复直到垃圾收集完成 +*** -缺点:线程切换和上下文转换消耗资源,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降 +#### 安全区域 +安全点 (Safepoint):程序执行时并非在所有地方都能停顿下来开始 GC,只有在安全点才能停下 +- Safe Point 的选择很重要,如果太少可能导致 GC 等待的时间太长,如果太多可能导致运行时的性能问题 +- 大部分指令的执行时间都非常短,通常会根据是否具有让程序长时间执行的特征为标准,选择些执行时间较长的指令作为 Safe Point, 如方法调用、循环跳转和异常跳转等 -*** +在 GC 发生时,让所有线程都在最近的安全点停顿下来的方法: +- 抢先式中断:没有虚拟机采用,首先中断所有线程,如果有线程不在安全点,就恢复线程让线程运行到安全点 +- 主动式中断:设置一个中断标志,各个线程运行到各个 Safe Point 时就轮询这个标志,如果中断标志为真,则将自己进行中断挂起 +问题:Safepoint 保证程序执行时,在不太长的时间内就会遇到可进入 GC 的 Safepoint,但是当线程处于 Waiting 状态或 Blocked 状态,线程无法响应 JVM 的中断请求,运行到安全点去中断挂起,JVM 也不可能等待线程被唤醒,对于这种情况,需要安全区域来解决 -### 垃圾回收器 +安全区域 (Safe Region):指在一段代码片段中,**对象的引用关系不会发生变化**,在这个区域中的任何位置开始 GC 都是安全的 -#### 概述 +运行流程: -垃圾收集器分类: +- 当线程运行到 Safe Region 的代码时,首先标识已经进入了 Safe Region,如果这段时间内发生GC,JVM 会忽略标识为 Safe Region 状态的线程 -* 按线程数分(垃圾回收线程数),可以分为串行垃圾回收器和并行垃圾回收器 - * 除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行 -* 按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器 - * 并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间 - * 独占式垃圾回收器(Stop the world)一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束 -* 按碎片处理方式分,可分为压缩式垃圾回收器和非压缩式垃圾回收器 - * 压缩式垃圾回收器在回收完成后进行压缩整理,消除回收后的碎片,再分配对象空间使用指针碰撞 - * 非压缩式的垃圾回收器不进行这步操作,再分配对象空间使用空闲列表 -* 按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器 +- 当线程即将离开 Safe Region 时,会检查 JVM 是否已经完成 GC,如果完成了则继续运行,否则线程必须等待 GC 完成,收到可以安全离开 SafeRegion 的信号 -GC 性能指标: -- **吞吐量**:程序的运行时间占总运行时间的比例(总运行时间 = 程序的运行时间 + 内存回收的时间) -- 垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例 -- 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间 -- 收集频率:相对于应用程序的执行,收集操作发生的频率 -- 内存占用:Java 堆区所占的内存大小 -- 快速:一个对象从诞生到被回收所经历的时间 -**垃圾收集器的组合关系**: +*** -![](https://gitee.com/seazean/images/raw/master/Java/JVM-垃圾回收器关系图.png) -新生代收集器:Serial、ParNew、Paralle1 Scavenge; -老年代收集器:Serial old、Parallel old、CMS; +### 垃圾判断 -整堆收集器:G1 +#### 垃圾介绍 -* 红色虚线在 JDK9 移除、绿色虚线在 JDK14 弃用该组合、青色虚线在 JDK14 删除 CMS 垃圾回收器 +垃圾:**如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾** -查看默认的垃圾收回收器: +作用:释放没用的对象,清除内存里的记录碎片,碎片整理将所占用的堆内存移到堆的一端,以便 JVM 将整理出的内存分配给新的对象 -* `-XX:+PrintcommandLineFlags`:查看命令行相关参数(包含使用的垃圾收集器) +垃圾收集主要是针对堆和方法区进行,程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收 -* 使用命令行指令:jinfo -flag 相关垃圾回收器参数 进程 ID +在堆里存放着几乎所有的 Java 对象实例,在 GC 执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC 才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程可以称为垃圾标记阶段,判断对象存活一般有两种方式:**引用计数算法**和**可达性分析算法** @@ -10913,209 +11196,199 @@ GC 性能指标: -#### Serial +#### 引用计数法 -Serial:串行垃圾收集器,作用于新生代,是指使用单线程进行垃圾回收,采用**复制算法** +引用计数算法(Reference Counting):对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。对于一个对象 A,只要有任何一个对象引用了 A,则 A 的引用计数器就加 1;当引用失效时,引用计数器就减 1;当对象 A 的引用计数器的值为 0,即表示对象A不可能再被使用,可进行回收(Java 没有采用) -**STW(Stop-The-World)**:垃圾回收时,只有一个线程在工作,并且 java 应用中的所有线程都要暂停,等待垃圾回收的完成 +优点: -**Serial old**:执行老年代垃圾回收的串行收集器,内存回收算法使用的是**标记-整理算法**,同样也采用了串行回收和 STW 机制 +- 回收没有延迟性,无需等到内存不够的时候才开始回收,运行时根据对象计数器是否为 0,可以直接回收 +- 在垃圾回收过程中,应用无需挂起;如果申请内存时,内存不足,则立刻报 OOM 错误 +- 区域性,更新对象的计数器时,只是影响到该对象,不会扫描全部对象 -- Serial old 是 Client 模式下默认的老年代的垃圾回收器 -- Serial old 在 Server 模式下主要有两个用途: - - 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用 - - 作为老年代 CMS 收集器的**后备垃圾回收方案**,在并发收集发生 Concurrent Mode Failure 时使用 +缺点: -开启参数:`-XX:+UseSerialGC` 等价于新生代用 Serial GC 且老年代用 Serial old GC +- 每次对象被引用时,都需要去更新计数器,有一点时间开销 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-Serial收集器.png) +- 浪费 CPU 资源,即使内存够用,仍然在运行时进行计数器的统计。 -优点:简单而高效(与其他收集器的单线程比),对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,可以获得最高的单线程收集效率 +- **无法解决循环引用问题,会引发内存泄露**(最大的缺点) -缺点:对于交互性较强的应用而言,这种垃圾收集器是不能够接受的,比如 JavaWeb 应用 + ```java + public class Test { + public Object instance = null; + public static void main(String[] args) { + Test a = new Test();// a = 1 + Test b = new Test();// b = 1 + a.instance = b; // b = 2 + b.instance = a; // a = 2 + a = null; // a = 1 + b = null; // b = 1 + } + } + ``` +![](https://gitee.com/seazean/images/raw/master/Java/JVM-循环引用.png) -**** +*** -#### Parallel -Parallel Scavenge 收集器是应用于新生代的并行垃圾回收器,**采用复制算法**、并行回收和 Stop the World 机制 +#### 可达性分析 -Parallel Old 收集器:是一个应用于老年代的并行垃圾回收器,**采用标记-整理算法** +##### GC Roots -对比其他回收器: +可达性分析算法:也可以称为根搜索算法、追踪性垃圾收集 -* 其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间 -* Parallel 目标是达到一个可控制的吞吐量,被称为**吞吐量优先**收集器 -* Parallel Scavenge 对比 ParNew 拥有**自适应调节策略**,可以通过一个开关参数打开 GC Ergonomics +GC Roots 对象: -应用场景: +- 虚拟机栈中局部变量表中引用的对象:各个线程被调用的方法中使用到的参数、局部变量等 +- 本地方法栈中引用的对象 +- 堆中类静态属性引用的对象 +- 方法区中的常量引用的对象 +- 字符串常量池(string Table)里的引用 +- 同步锁 synchronized 持有的对象 -* 停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验 -* 高吞吐量可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互 +**GC Roots 是一组活跃的引用,不是对象**,放在 GC Roots Set 集合 -停顿时间和吞吐量的关系:新生代空间变小 → 缩短停顿时间 → 垃圾回收变得频繁 → 导致吞吐量下降 -在注重吞吐量及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge + Parallel Old 收集器,在 Server 模式下的内存回收性能很好,**Java8 默认是此垃圾收集器组合** -![](https://gitee.com/seazean/images/raw/master/Java/JVM-ParallelScavenge收集器.png) +*** -参数配置: -* `-XX:+UseParallelGC`:手动指定年轻代使用Paralle并行收集器执行内存回收任务 -* `-XX:+UseParalleloldcc`:手动指定老年代使用并行回收收集器执行内存回收任务 - * 上面两个参数,默认开启一个,另一个也会被开启(互相激活),默认 jdk8 是开启的 -* `-XX:+UseAdaptivesizepplicy`:设置 Parallel scavenge 收集器具有**自适应调节策略**,在这种模式下,年轻代的大小、Eden 和 Survivor 的比例、晋升老年代的对象年龄等参数会被自动调整,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量 -* `-XX:ParallelGcrhreads`:设置年轻代并行收集器的线程数,一般最好与 CPU 数量相等,以避免过多的线程数影响垃圾收集性能 - * 在默认情况下,当 CPU 数量小于 8 个,ParallelGcThreads 的值等于 CPU 数量 - * 当 CPU 数量大于 8 个,ParallelGCThreads 的值等于 3+[5*CPU Count]/8] -* `-XX:MaxGCPauseMillis`:设置垃圾收集器最大停顿时间(即 STW 的时间),单位是毫秒 - * 对于用户来讲,停顿时间越短体验越好;在服务器端,注重高并发,整体的吞吐量 - * 为了把停顿时间控制在 MaxGCPauseMillis 以内,收集器在工作时会调整 Java 堆大小或其他一些参数 -* `-XX:GCTimeRatio`:垃圾收集时间占总时间的比例 =1/(N+1),用于衡量吞吐量的大小 - * 取值范围(0,100)。默认值 99,也就是垃圾回收时间不超过1 - * 与 `-xx:MaxGCPauseMillis` 参数有一定矛盾性,暂停时间越长,Radio 参数就容易超过设定的比例 +##### 工作原理 +可达性分析算法以根对象集合(GCRoots)为起始点,从上至下的方式搜索被根对象集合所连接的目标对象 -*** +分析工作必须在一个保障**一致性的快照**中进行,否则结果的准确性无法保证,这也是导致 GC 进行时必须 Stop The World 的一个原因 +基本原理: +- 可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索走过的路径称为引用链 -#### ParNew +- 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象 -Par 是 Parallel 并行的缩写,New 是只能处理的是新生代 +- 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象 -并行垃圾收集器在串行垃圾收集器的基础之上做了改进,**采用复制算法**,将单线程改为了多线程进行垃圾回收,可以缩短垃圾回收的时间 + -对于其他的行为(收集算法、stop the world、对象分配规则、回收策略等)同 Serial 收集器一样,应用在年轻代,除 Serial 外,只有**ParNew GC 能与 CMS 收集器配合工作** - -相关参数: -* `-XX:+UseParNewGC`:表示年轻代使用并行收集器,不影响老年代 -* `-XX:ParallelGCThreads`:默认开启和 CPU 数量相同的线程数 +*** -![](https://gitee.com/seazean/images/raw/master/Java/JVM-ParNew收集器.png) -ParNew 是很多 JVM 运行在 Server 模式下新生代的默认垃圾收集器 -- 对于新生代,回收次数频繁,使用并行方式高效 -- 对于老年代,回收次数少,使用串行方式节省资源(CPU 并行需要切换线程,串行可以省去切换线程的资源) +##### 三色标记 +###### 标记算法 +三色标记法把遍历对象图过程中遇到的对象,标记成以下三种颜色: -**** +- 白色:尚未访问过 +- 灰色:本对象已访问过,但是本对象引用到的其他对象尚未全部访问 +- 黑色:本对象已访问过,而且本对象引用到的其他对象也全部访问完成 +当 Stop The World (STW) 时,对象间的引用是不会发生变化的,可以轻松完成标记,遍历访问过程为: +1. 初始时,所有对象都在白色集合 +2. 将 GC Roots 直接引用到的对象挪到灰色集合 +3. 从灰色集合中获取对象: + * 将本对象引用到的其他对象全部挪到灰色集合中 + * 将本对象挪到黑色集合里面 +4. 重复步骤 3,直至灰色集合为空时结束 +5. 结束后,仍在白色集合的对象即为 GC Roots 不可达,可以进行回收 -#### CMS + -CMS 全称 Concurrent Mark Sweep,是一款**并发的、使用标记-清除**算法、针对老年代的垃圾回收器,其最大特点是**让垃圾收集线程与用户线程同时工作** -CMS 收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,停顿时间越短(**低延迟**)越适合与用户交互的程序,良好的响应速度能提升用户体验 -分为以下四个流程: +参考文章:https://www.jianshu.com/p/12544c0ad5c1 -- 初始标记:使用 STW 出现短暂停顿,仅标记一下 GC Roots 能直接关联到的对象,速度很快 -- 并发标记:进行 GC Roots 开始遍历整个对象图,在整个回收过程中耗时最长,不需要 STW,可以与用户线程并发运行 -- 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,比初始标记时间长但远比并发标记时间短,需要 STW(不停顿就会一直变化,采用写屏障 + 增量更新来避免漏标情况) -- 并发清除:清除标记为可以回收对象,不需要移动存活对象,所以这个阶段可以与用户线程同时并发的 -Mark Sweep 会造成内存碎片,不把算法换成 Mark Compact 的原因: -* Mark Compact 算法会整理内存,导致用户线程使用的对象的地址改变,影响用户线程继续执行 +**** -* Mark Compact 更适合 Stop The World 场景 -在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-CMS收集器.png) +###### 并发标记 -优点:并发收集、低延迟 +并发标记时,对象间的引用可能发生变化**,**多标和漏标的情况就有可能发生 -缺点: +**多标情况:**当 E 变为灰色或黑色时,其他线程断开的 D 对 E 的引用,导致这部分对象仍会被标记为存活,本轮 GC 不会回收这部分内存,这部分本应该回收但是没有回收到的内存,被称之为**浮动垃圾** -- 吞吐量降低:在并发阶段虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,CPU 利用率不够高 -- CMS 收集器**无法处理浮动垃圾**,可能出现 Concurrent Mode Failure 导致另一次 Full GC 的产生 - - 浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾(产生了新对象),这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,CMS 收集需要预留出一部分内存,不能等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS,导致很长的停顿时间 -- 标记 - 清除算法导致的空间碎片,往往出现老年代空间无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC;为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer)技术,而只能够选择空闲列表(Free List)执行内存分配 +* 针对并发标记开始后的**新对象**,通常的做法是直接全部当成黑色,也算浮动垃圾 +* 浮动垃圾并不会影响应用程序的正确性,只是需要等到下一轮垃圾回收中才被清除 -参数设置: + -* `-XX:+UseConcMarkSweepGC`:手动指定使用 CMS 收集器执行内存回收任务 +**漏标情况:** - 开启该参数后会自动将 `-XX:+UseParNewGC` 打开,即:ParNew + CMS + Serial old的组合 +* 条件一:灰色对象断开了对一个白色对象的引用(直接或间接),即灰色对象原成员变量的引用发生了变化 +* 条件二:其他线程中修改了黑色对象,插入了一条或多条对该白色对象的新引用 +* 结果:导致该白色对象当作垃圾被 GC,影响到了应用程序的正确性 -* `-XX:CMSInitiatingoccupanyFraction`:设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收 + - * JDK5 及以前版本的默认值为 68,即当老年代的空间使用率达到 68% 时,会执行一次CMS回收 - * JDK6 及以上版本默认值为 92% +代码角度解释漏标: -* `-XX:+UseCMSCompactAtFullCollection`:用于指定在执行完 Full GC 后对内存空间进行压缩整理,以此避免内存碎片的产生,由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长 +```java +Object G = objE.fieldG; // 读 +objE.fieldG = null; // 写 +objD.fieldG = G; // 写 +``` -* `-XX:CMSFullGCsBeforecompaction`:**设置在执行多少次 Full GC 后对内存空间进行压缩整理** +为了解决问题,可以操作上面三步,**将对象 G 记录起来,然后作为灰色对象再进行遍历**,比如放到一个特定的集合,等初始的 GC Roots 遍历完(并发标记),再遍历该集合(重新标记) -* `-XX:ParallelCMSThreads`:**设置CMS的线程数量** +> 所以**重新标记需要 STW**,应用程序一直在运行,该集合可能会一直增加新的对象,导致永远都运行不完 - * CMS 默认启动的线程数是(ParallelGCThreads+3)/4,ParallelGCThreads 是年轻代并行收集器的线程数 - * 收集线程占用的 CPU 资源多于25%,对用户程序影响可能较大;当 CPU 资源比较紧张时,受到 CMS 收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕 +解决方法:添加读写屏障,读屏障拦截第一步,写屏障拦截第二三步,在读写前后进行一些后置处理: +* **写屏障 + 增量更新**:黑色对象新增引用,会将黑色对象变成灰色对象,最后对该节节点重新扫描 + 增量更新 (Incremental Update) 破坏了条件二,从而保证了不会漏标 -*** + 缺点:对黑色变灰的对象重新扫描所有引用,比较耗费时间 +* **写屏障 (Store Barrier) + SATB**:当原来成员变量的引用发生变化之前,记录下原来的引用对象 + 保留 GC 开始时的对象图,即原始快照 SATB,当 GC Roots 确定后,对象图就已经确定,那后续的标记也应该是按照这个时刻的对象图走,如果期间对白色对象有了新的引用会记录下来,并且将白色对象变灰(说明可达了),重新扫描该对象的引用关系 -#### G1 + SATB (Snapshot At The Beginning) 破坏了条件一,从而保证了不会漏标 -##### G1特点 +* **读屏障 (Load Barrier)**:破坏条件二,黑色对象引用白色对象的前提是获取到该对象,此时读屏障发挥作用 -G1(Garbage-First)是一款面向服务端应用的垃圾收集器,**应用于新生代和老年代**、采用标记-整理算法、软实时、低延迟、可设定目标(最大STW停顿时间)的垃圾回收器,用于代替 CMS,适用于较大的堆(>4 ~ 6G),在 JDK9 之后默认使用 G1 +以 Java HotSpot VM 为例,其并发标记时对漏标的处理方案如下: -G1 对比其他处理器的优点: +- CMS:写屏障 + 增量更新 +- G1:写屏障 + SATB +- ZGC:读屏障 -* 并发与并行: - * 并行性:G1 在回收期间,可以有多个 GC 线程同时工作,有效利用多核计算能力,此时用户线程 STW - * 并发性:G1 拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此不会在整个回收阶段发生完全阻塞应用程序的情况 - * 其他的垃圾收集器使用内置的 JVM 线程执行 GC 的多线程操作,而 G1 GC 可以采用应用线程承担后台运行的 GC 工作,JVM 的 GC 线程处理速度慢时,系统会**调用应用程序线程加速垃圾回收**过程 -* **分区算法**: - * 从分代上看,G1 属于分代型垃圾回收器,区分年轻代和老年代,年轻代依然有 Eden 区和 Survivor 区。从堆结构上看,**新生代和老年代不再物理隔离**,不用担心每个代内存是否足够,这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC - * 将整个堆划分成约 2048 个大小相同的独立 Region 块,每个 Region 块大小根据堆空间的实际大小而定,整体被控制在 1MB 到 32 MB之间且为 2 的 N 次幂,所有 Region 大小相同,在 JVM 生命周期内不会被改变。G1 把堆划分成多个大小相等的独立区域,使得每个小空间可以单独进行垃圾回收 - * **新的区域 Humongous**:本身属于老年代区,当出现了一个巨型对象超出了分区容量的一半,该对象就会进入到该区域。如果一个 H 区装不下一个巨型对象,那么 G1 会寻找连续的 H 分区来存储,为了能找到连续的H区,有时候不得不启动 Full GC - * G1 不会对巨型对象进行拷贝,回收时被优先考虑,G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为 0 的巨型对象就可以在新生代垃圾回收时处理掉 - - * Region 结构图: - -![](https://gitee.com/seazean/images/raw/master/Java/JVM-G1-Region区域.png) +*** -- 空间整合: - - CMS:标记-清除算法、内存碎片、若干次 GC 后进行一次碎片整理 - - G1:整体来看是基于标记 - 整理算法实现的收集器,从局部(Region 之间)上来看是基于复制算法实现的,两种算法都可以避免内存碎片 -- **可预测的停顿时间模型(软实时 soft real-time)**:可以指定在 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒 +#### finalization - - 由于分块的原因,G1 可以只选取部分区域进行内存回收,这样缩小了回收的范围,对于全局停顿情况也能得到较好的控制 - - G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间,通过过去回收的经验获得),在后台维护一个**优先列表**,每次根据允许的收集时间优先回收价值最大的 Region,保证了 G1 收集器在有限的时间内可以获取尽可能高的收集效率 +Java 语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑 - * 相比于 CMS GC,G1 未必能做到 CMS 在最好情况下的延时停顿,但是最差情况要好很多 +垃圾回收此对象之前,会先调用这个对象的 finalize() 方法,finalize() 方法允许在子类中被重写,用于在对象被回收时进行后置处理,通常在这个方法中进行一些资源释放和清理,比如关闭文件、套接字和数据库连接等 -G1垃圾收集器的缺点: +生存 OR 死亡:如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用,此对象需要被回收。但事实上这时候它们暂时处于缓刑阶段。**一个无法触及的对象有可能在某个条件下复活自己**,这样对它的回收就是不合理的,所以虚拟机中的对象可能的三种状态: -* 相较于 CMS,G1 还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1 无论是为了垃圾收集产生的内存占用还是程序运行时的额外执行负载都要比 CMS 要高 -* 从经验上来说,在小内存应用上 CMS 的表现大概率会优于 G1,而 G1 在大内存应用上则发挥其优势。平衡点在 6-8GB 之间 +- 可触及的:从根节点开始,可以到达这个对象。 +- 可复活的:对象的所有引用都被释放,但是对象有可能在finalize() 中复活 +- 不可触及的:对象的 finalize() 被调用并且没有复活,那么就会进入不可触及状态,不可触及的对象不可能被复活,因为 **finalize() 只会被调用一次**,等到这个对象再被标记为可回收时就必须回收 -应用场景: +永远不要主动调用某个对象的 finalize() 方法,应该交给垃圾回收机制调用,原因: -* 面向服务端应用,针对具有大内存、多处理器的机器 -* 需要低 GC 延迟,并具有大堆的应用程序提供解决方案 +* finalize() 时可能会导致对象复活 +* finalize() 方法的执行时间是没有保障的,完全由 GC 线程决定,极端情况下,若不发生 GC,则 finalize() 方法将没有执行机会,因为优先级比较低,即使主动调用该方法,也不会因此就直接进行回收 +* 一个糟糕的 finalize() 会严重影响 GC 的性能 @@ -11123,282 +11396,278 @@ G1垃圾收集器的缺点: -##### 记忆集 +#### 引用分析 -记忆集 Remembered Set 在新生代中,每个 Region 都有一个 Remembered Set,用来被哪些其他 Region 里的对象引用(谁引用了我就记录谁) +无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关,Java 提供了四种强度不同的引用类型 - +1. 强引用:被强引用关联的对象不会被回收,只有所有 GCRoots 都不通过强引用引用该对象,才能被垃圾回收 -* 程序对 Reference 类型数据写操作时,产生一个 Write Barrier 暂时中断操作,检查该对象和 Reference 类型数据是否在不同的 Region(跨代引用),不同就将相关引用信息记录到 Reference 类型所属的 Region 的 Remembered Set 之中 -* 进行内存回收时,在 GC 根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏 + * 强引用可以直接访问目标对象 + * 虚拟机宁愿抛出 OOM 异常,也不会回收强引用所指向对象 + * 强引用可能导致**内存泄漏** -垃圾收集器在新生代中建立了记忆集这样的数据结构,可以理解为它是一个抽象类,具体实现记忆集的三种方式: + ```java + Object obj = new Object();//使用 new 一个新对象的方式来创建强引用 + ``` -* 字长精度 -* 对象精度 -* 卡精度(卡表) +2. 软引用(SoftReference):被软引用关联的对象只有在内存不够的情况下才会被回收 -卡表(Card Table)在老年代中,是一种对记忆集的具体实现,主要定义了记忆集的记录精度、与堆内存的映射关系等,卡表中的每一个元素都对应着一块特定大小的内存块,这个内存块称之为卡页(card page),当存在跨代引用时,会将卡页标记为 dirty,JVM 对于卡页的维护也是通过写屏障的方式 + * **仅(可能有强引用,一个对象可以被多个引用)**有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象 + * 配合**引用队列来释放软引用自身**,在构造软引用时,可以指定一个引用队列,当软引用对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况 + * 软引用通常用来实现内存敏感的缓存,比如高速缓存就有用到软引用;如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时不会耗尽内存 -收集集合 CSet 代表每次 GC 暂停时回收的一系列目标分区,在任意一次收集暂停中,CSet 所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。年轻代收集 CSet 只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到 CSet 中 + ```java + Object obj = new Object(); + SoftReference sf = new SoftReference(obj); + obj = null; // 使对象只被软引用关联 + ``` -* CSet of Young Collection -* CSet of Mix Collection +3. 弱引用(WeakReference):被弱引用关联的对象一定会被回收,只能存活到下一次垃圾回收发生之前 + * 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象 + * 配合引用队列来释放弱引用自身 + * WeakHashMap 用来存储图片信息,可以在内存不足的时候及时回收,避免了 OOM + ```java + Object obj = new Object(); + WeakReference wf = new WeakReference(obj); + obj = null; + ``` -*** +4. 虚引用(PhantomReference):也称为幽灵引用或者幻影引用,是所有引用类型中最弱的一个 + * 一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象 + * 为对象设置虚引用的唯一目的是在于跟踪垃圾回收过程,能在这个对象被回收时收到一个系统通知 + * 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存 + ```java + Object obj = new Object(); + PhantomReference pf = new PhantomReference(obj, null); + obj = null; + ``` -##### 工作原理 +5. 终结器引用(finalization) -G1 中提供了三种垃圾回收模式:YoungGC、Mixed GC 和 FullGC,在不同的条件下被触发 -* 当堆内存使用达到一定值(默认 45%)时,开始老年代并发标记过程 -* 标记完成马上开始混合回收过程 - +*** -顺时针:Young GC → Young GC + Concurrent Mark → Mixed GC 顺序,进行垃圾回收 -* **Young GC**:发生在年轻代的 GC 算法,一般对象(除了巨型对象)都是在 eden region 中分配内存,当所有 eden region 被耗尽无法申请内存时,就会触发一次 young gc,G1 停止应用程序的执行 STW,把活跃对象放入老年代,垃圾对象回收 - **回收过程**: +#### 无用属性 - 1. 扫描根:根引用连同 RSet 记录的外部引用作为扫描存活对象的入口 - 2. 更新 RSet:处理 dirty card queue 更新 RS,此后 RSet 准确的反映对象的引用关系 - * dirty card queue:类似缓存,产生了引用先记录在这里,然后更新到 RSet - * 作用:产生引用直接更新 RSet 需要线程同步开销很大,使用队列性能好 - 3. 处理 RSet:识别被老年代对象指向的 Eden 中的对象,这些被指向的对象被认为是存活的对象,把需要回收的分区放入 Young CSet 中进行回收 - 4. 复制对象:Eden 区内存段中存活的对象会被复制到 survivor 区,survivor 区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到 old 区中空的内存分段,如果 survivor 空间不够,Eden 空间的部分数据会直接晋升到老年代空间 - 5. 处理引用:处理 Soft,Weak,Phantom,JNI Weak 等引用,最终 Eden 空间的数据为空,GC 停止工作 +##### 无用类 -* **Concurrent Mark **: +方法区主要回收的是无用的类 - * 初始标记:标记从根节点直接可达的对象,这个阶段是 STW 的,并且会触发一次年轻代 GC - * 根区域扫描 (Root Region Scanning):扫描 Survivor 区中指向老年代的,被初始标记标记了的引用及引用的对象,这一个过程是并发进行的,但是必须在 Young GC 之前完成 - * 并发标记 (Concurrent Marking):在整个堆中进行并发标记(应用程序并发执行),可能被 YoungGC 中断。会计算每个区域的对象活性,即区域中存活对象的比例,若区域中的所有对象都是垃圾,则这个区域会被立即回收(实时回收),给浮动垃圾准备出更多的空间,把需要收集的 Region 放入 CSet 当中 - * 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这阶段需要停顿线程,但是可并行执行(**防止漏标**) - * 筛选回收:并发清理阶段,首先对 CSet 中各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率 +判定一个类是否是无用的类,需要同时满足下面 3 个条件: - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-G1收集器.jpg) +- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例 +- 加载该类的 `ClassLoader` 已经被回收 +- 该类对应的 `java.lang.Class` 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法 -* **Mixed GC**:当很多对象晋升到老年代时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即 Mixed GC,除了回收整个 young region,还会**回收一部分**的 old region,过程同 YGC +虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的**仅仅是可以**,而并不是和对象一样不使用了就会必然被回收 - 注意:**是一部分老年代,而不是全部老年代**,可以选择哪些老年代 region 收集,对垃圾回收的时间进行控制 - 在 G1 中,Mixed GC 可以通过 `-XX:InitiatingHeapOccupancyPercent` 设置阈值 -* **Full GC**:对象内存分配速度过快,Mixed GC 来不及回收,导致老年代被填满,就会触发一次 Full GC,G1 的 Full GC 算法就是单线程执行的垃圾回收,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免 Full GC +*** - 产生 Full GC 的原因: - * 晋升时没有足够的空间存放晋升的对象 - * 并发处理过程完成之前空间耗尽,浮动垃圾 +##### 废弃常量 +在常量池中存在字符串 "abc",如果当前没有任何 String 对象引用该常量,说明常量 "abc" 是废弃常量,如果这时发生内存回收的话**而且有必要的话**(内存不够用),"abc" 就会被系统清理出常量池 -*** +*** -##### 相关参数 -- `-XX:+UseG1GC`:手动指定使用 G1 垃圾收集器执行内存回收任务 -- `-XX:G1HeapRegionSize`:设置每个 Region 的大小。值是 2 的幂,范围是 1MB 到 32MB 之间,目标是根据最小的 Java 堆大小划分出约 2048 个区域,默认是堆内存的 1/2000 -- `-XX:MaxGCPauseMillis`:设置期望达到的最大 GC 停顿时间指标,JVM会尽力实现,但不保证达到,默认值是 200ms -- `-XX:+ParallelGcThread`:设置 STW 时 GC 线程数的值,最多设置为 8 -- `-XX:ConcGCThreads`:设置并发标记线程数,设置为并行垃圾回收线程数 ParallelGcThreads 的1/4左右 -- `-XX:InitiatingHeapoccupancyPercent`:设置触发并发 Mixed GC 周期的 Java 堆占用率阈值,超过此值,就触发 GC,默认值是 45 -- `-XX:+ClassUnloadingWithConcurrentMark`:并发标记类卸载,默认启用,所有对象都经过并发标记后,就可以知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类 -- `-XX:G1NewSizePercent`:新生代占用整个堆内存的最小百分比(默认5%) -- `-XX:G1MaxNewSizePercent`:新生代占用整个堆内存的最大百分比(默认60%) -- `-XX:G1ReservePercent=10`:保留内存区域,防止 to space(Survivor中的 to 区)溢出 +##### 静态变量 +类加载时(第一次访问),这个类中所有静态成员就会被加载到静态变量区,该区域的成员一旦创建,直到程序退出才会被回收 -*** +如果是静态引用类型的变量,静态变量区只存储一份对象的引用地址,真正的对象在堆内,如果要回收该对象可以设置引用为 null -##### 调优 +参考文章:https://blog.csdn.net/zhengzhb/article/details/7331354 -G1 的设计原则就是简化 JVM 性能调优,只需要简单的三步即可完成调优: -1. 开启 G1 垃圾收集器 -2. 设置堆的最大内存 -3. 设置最大的停顿时间(STW) -不断调优暂停时间指标: +*** -* `XX:MaxGCPauseMillis=x` 可以设置启动应用程序暂停的时间,G1会根据这个参数选择 CSet 来满足响应时间的设置 -* 设置到 100ms 或者 200ms 都可以(不同情况下会不一样),但设置成50ms就不太合理 -* 暂停时间设置的太短,就会导致出现 G1 跟不上垃圾产生的速度,最终退化成 Full GC -* 对这个参数的调优是一个持续的过程,逐步调整到最佳状态 -不要设置新生代和老年代的大小: - -- 避免使用 -Xmn 或 -XX:NewRatio 等相关选项显式设置年轻代大小,G1 收集器在运行的时候会调整新生代和老年代的大小,从而达到我们为收集器设置的暂停时间目标 -- 设置了新生代大小相当于放弃了 G1 为我们做的自动调优,我们需要做的只是设置整个堆内存的大小,剩下的交给 G1 自己去分配各个代的大小 +### 回收算法 +#### 标记清除 -*** +当成功区分出内存中存活对象和死亡对象后,GC 接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。目前在 JVM 中比较常见的三种垃圾收集算法是 +- 标记清除算法(Mark-Sweep) +- 复制算法(copying) +- 标记压缩算法(Mark-Compact) +标记清除算法,是将垃圾回收分为两个个阶段,分别是**标记和清除** -#### ZGC +- **标记**:Collector 从引用根节点开始遍历,标记所有被引用的对象,一般是在对象的 Header 中记录为可达对象,**标记的是引用的对象,不是垃圾** +- **清除**:Collector 对堆内存从头到尾进行线性的遍历,如果发现某个对象在其 Header 中没有标记为可达对象,则将其回收,把分块连接到 **空闲列表**”的单向链表,判断回收后的分块与前一个空闲分块是否连续,若连续会合并这两个分块,之后进行分配时只需要遍历这个空闲列表,就可以找到分块 -ZGC 收集器是一个可伸缩的、低延迟的垃圾收集器,基于 Region 内存布局的,不设分代,使用了读屏障、染色指针和内存多重映射等技术来实现**可并发的标记压缩算法** +- **分配阶段**:程序会搜索空闲链表寻找空间大于等于新对象大小 size 的块 block,如果找到的块等于 size,会直接返回这个分块;如果找到的块大于 size,会将块分割成大小为 size 与 block - size 的两部分,返回大小为 size 的分块,并把大小为 block - size 的块返回给空闲列表 -* 在 CMS 和 G1 中都用到了写屏障,而 ZGC 用到了读屏障 -* 染色指针:直接**将少量额外的信息存储在指针上的技术**,从 64 位的指针中拿高 4 位来标识对象此时的状态 - * 染色指针可以使某个 Region 的存活对象被移走之后,这个 Region 立即就能够被释放和重用 - * 可以直接从指针中看到引用对象的三色标记状态(Marked0、Marked1)、是否进入了重分配集、是否被移动过(Remapped)、是否只能通过 finalize() 方法才能被访问到(Finalizable) - * 可以大幅减少在垃圾收集过程中内存屏障的使用数量,写屏障的目的通常是为了记录对象引用的变动情况,如果将这些信息直接维护在指针中,显然就可以省去一些专门的记录操作 - * 可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据 -* 内存多重映射:多个虚拟地址指向同一个物理地址 +算法缺点: -可并发的标记压缩算法:染色指针标识对象是否被标记或移动,读屏障保证在每次应用程序或 GC 程序访问对象时先根据染色指针的标识判断是否被移动,如果被移动就根据转发表访问新的移动对象,并更新引用,不会像 G1 一样必须等待垃圾回收完成才能访问 +- 标记和清除过程效率都不高 +- 会产生大量不连续的内存碎片,导致无法给大对象分配内存,需要维护一个空闲链表 -ZGC 目标: + -- 停顿时间不会超过 10ms -- 停顿时间不会随着堆的增大而增大(不管多大的堆都能保持在 10ms 以下) -- 可支持几百 M,甚至几 T 的堆大小(最大支持4T) -ZGC 的工作过程可以分为 4 个阶段: -* 并发标记(Concurrent Mark): 遍历对象图做可达性分析的阶段,也要经过初始标记和最终标记,需要短暂停顿 -* 并发预备重分配( Concurrent Prepare for Relocate):根据特定的查询条件统计得出本次收集过程要清理哪些 Region,将这些 Region 组成重分配集(Relocation Set) -* 并发重分配(Concurrent Relocate): 重分配是 ZGC 执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的 Region 上,并为重分配集中的**每个 Region 维护一个转发表**(Forward Table),记录从旧地址到新地址的转向关系 -* 并发重映射(Concurrent Remap):修正整个堆中指向重分配集中旧对象的所有引用,ZGC 的并发映射并不是一个必须要立即完成的任务,ZGC 很巧妙地把并发重映射阶段要做的工作,合并到下一次垃圾收集循环中的并发标记阶段里去完成,因为都是要遍历所有对象,这样合并节省了一次遍历的开销 +*** -ZGC 几乎在所有地方并发执行的,除了初始标记的是 STW 的,但这部分的实际时间是非常少的,所以响应速度快,在尽可能对吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟 -优点:高吞吐量、低延迟 -缺点:浮动垃圾,当 ZGC 准备要对一个很大的堆做一次完整的并发收集,其全过程要持续十分钟以上,由于应用的对象分配速率很高,将创造大量的新对象产生浮动垃圾 +#### 复制算法 +复制算法的核心就是,**将原有的内存空间一分为二,每次只用其中的一块**,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清理,交换两个内存的角色,完成垃圾的回收 +应用场景:如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之则不适合 -参考文章:https://www.cnblogs.com/jimoer/p/13170249.html +![](https://gitee.com/seazean/images/raw/master/Java/JVM-复制算法.png) +算法优点: +- 没有标记和清除过程,实现简单,运行高效 +- 复制过去以后保证空间的连续性,不会出现“碎片”问题。 -*** +算法缺点: +- 主要不足是**只使用了内存的一半** +- 对于 G1 这种分拆成为大量 region 的 GC,复制而不是移动,意味着 GC 需要维护 region 之间对象引用关系,不管是内存占用或者时间开销都不小 +现在的商业虚拟机都采用这种收集算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间 -#### 总结 -Serial GC、Parallel GC、Concurrent Mark Sweep GC 这三个 GC 不同: -- 最小化地使用内存和并行开销,选 Serial GC -- 最大化应用程序的吞吐量,选 Parallel GC -- 最小化 GC 的中断或停顿时间,选 CMS GC +*** -![](https://gitee.com/seazean/images/raw/master/Java/JVM-垃圾回收器总结.png) +#### 标记整理 +标记整理(压缩)算法是在标记清除算法的基础之上,做了优化改进的算法 +标记阶段和标记清除算法一样,也是从根节点开始,对对象的引用进行标记,在清理阶段,并不是简单的直接清理可回收对象,而是将存活对象都向内存另一端移动,然后清理边界以外的垃圾,从而**解决了碎片化**的问题 -*** +优点:不会产生内存碎片 +缺点:需要移动大量对象,处理效率比较低 + -### 内存泄漏 +| | Mark-Sweep | Mark-Compact | Copying | +| -------- | ---------------- | -------------- | ----------------------------------- | +| 速度 | 中等 | 最慢 | 最快 | +| 空间开销 | 少(但会堆积碎片) | 少(不堆积碎片) | 通常需要活对象的2倍大小(不堆积碎片) | +| 移动对象 | 否 | 是 | 是 | -#### 泄露溢出 +- 效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存。 +- 为了尽量兼顾三个指标,标记一整理算法相对来说更平滑一些 -内存泄漏(Memory Leak):是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果 -可达性分析算法来判断对象是否是不再使用的对象,本质都是判断一个对象是否还被引用。由于代码的实现不同就会出现很多种内存泄漏问题,让 JVM 误以为此对象还在引用中,无法回收,造成内存泄漏 -内存溢出(out of memory)指的是申请内存时,没有足够的内存可以使用 +*** -内存泄漏和内存溢出的关系:内存泄漏的越来越多,最终会导致内存溢出 +#### 增量收集 -*** +增量收集算法:通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作,基础仍是标记-清除和复制算法,用于多线程并发环境 +工作原理:如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,影响系统的交互性,所以让垃圾收集线程和应用程序线程交替执行,每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程,依次反复直到垃圾收集完成 +缺点:线程切换和上下文转换消耗资源,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降 -#### 几种情况 -##### 静态集合 -静态集合类的生命周期与 JVM 程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。原因是**长生命周期的对象持有短生命周期对象的引用**,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收 -```java -public class MemoryLeak { - static List list = new ArrayList(); - public void oomTest(){ - Object obj = new Object();//局部变量 - list.add(obj); - } -} -``` +*** -*** +### 垃圾回收器 +#### 概述 -##### 单例模式 +垃圾收集器分类: -单例模式和静态集合导致内存泄露的原因类似,因为单例的静态特性,它的生命周期和 JVM 的生命周期一样长,所以如果单例对象持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏 +* 按线程数分(垃圾回收线程数),可以分为串行垃圾回收器和并行垃圾回收器 + * 除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行 +* 按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器 + * 并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间 + * 独占式垃圾回收器(Stop the world)一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束 +* 按碎片处理方式分,可分为压缩式垃圾回收器和非压缩式垃圾回收器 + * 压缩式垃圾回收器在回收完成后进行压缩整理,消除回收后的碎片,再分配对象空间使用指针碰撞 + * 非压缩式的垃圾回收器不进行这步操作,再分配对象空间使用空闲列表 +* 按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器 +GC 性能指标: +- **吞吐量**:程序的运行时间占总运行时间的比例(总运行时间 = 程序的运行时间 + 内存回收的时间) +- 垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例 +- 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间 +- 收集频率:相对于应用程序的执行,收集操作发生的频率 +- 内存占用:Java 堆区所占的内存大小 +- 快速:一个对象从诞生到被回收所经历的时间 -**** +**垃圾收集器的组合关系**: +![](https://gitee.com/seazean/images/raw/master/Java/JVM-垃圾回收器关系图.png) +新生代收集器:Serial、ParNew、Paralle1 Scavenge; -##### 内部类 +老年代收集器:Serial old、Parallel old、CMS; -内部类持有外部类的情况,如果一个外部类的实例对象调用方法返回了一个内部类的实例对象,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象也不会被回收,造成内存泄漏 +整堆收集器:G1 +* 红色虚线在 JDK9 移除、绿色虚线在 JDK14 弃用该组合、青色虚线在 JDK14 删除 CMS 垃圾回收器 +查看默认的垃圾收回收器: -*** +* `-XX:+PrintcommandLineFlags`:查看命令行相关参数(包含使用的垃圾收集器) +* 使用命令行指令:jinfo -flag 相关垃圾回收器参数 进程 ID -##### 连接相关 -数据库连接、网络连接和 IO 连接等,当不再使用时,需要显式调用 close 方法来释放与连接,垃圾回收器才会回收对应的对象,否则将会造成大量的对象无法被回收,从而引起内存泄漏 +*** -**** +#### Serial +Serial:串行垃圾收集器,作用于新生代,是指使用单线程进行垃圾回收,采用**复制算法** +**STW(Stop-The-World)**:垃圾回收时,只有一个线程在工作,并且 java 应用中的所有线程都要暂停,等待垃圾回收的完成 -##### 不合理域 +**Serial old**:执行老年代垃圾回收的串行收集器,内存回收算法使用的是**标记-整理算法**,同样也采用了串行回收和 STW 机制 -变量不合理的作用域,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏;如果没有及时地把对象设置为 null,也有可能导致内存泄漏的发生 +- Serial old 是 Client 模式下默认的老年代的垃圾回收器 +- Serial old 在 Server 模式下主要有两个用途: + - 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用 + - 作为老年代 CMS 收集器的**后备垃圾回收方案**,在并发收集发生 Concurrent Mode Failure 时使用 -```java -public class UsingRandom { - private String msg; - public void receiveMsg(){ - msg = readFromNet();// 从网络中接受数据保存到 msg 中 - saveDB(msg); // 把 msg 保存到数据库中 - } -} -``` +开启参数:`-XX:+UseSerialGC` 等价于新生代用 Serial GC 且老年代用 Serial old GC -通过 readFromNet 方法把接收消息保存在 msg 中,然后调用 saveDB 方法把内容保存到数据库中,此时 msg 已经可以被回收,但是 msg 的生命周期与对象的生命周期相同,造成 msg 不能回收,产生内存泄漏 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-Serial收集器.png) -解决: +优点:简单而高效(与其他收集器的单线程比),对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,可以获得最高的单线程收集效率 -* msg 变量可以放在 receiveMsg 方法内部,当方法使用完,msg 的生命周期也就结束,就可以被回收了 -* 在使用完 msg 后,把 msg 设置为 null,这样垃圾回收器也会回收 msg 的内存空间。 +缺点:对于交互性较强的应用而言,这种垃圾收集器是不能够接受的,比如 JavaWeb 应用 @@ -11406,255 +11675,262 @@ public class UsingRandom { -##### 改变哈希 - -当一个对象被存储进 HashSet 集合中以后,就**不能修改这个对象中的那些参与计算哈希值的字段**,否则对象修改后的哈希值与最初存储进 HashSet 集合中时的哈希值不同,这种情况下使用该对象的当前引用作为的参数去 HashSet 集合中检索对象返回 false,导致无法从 HashSet 集合中单独删除当前对象,造成内存泄漏 +#### Parallel +Parallel Scavenge 收集器是应用于新生代的并行垃圾回收器,**采用复制算法**、并行回收和 Stop the World 机制 +Parallel Old 收集器:是一个应用于老年代的并行垃圾回收器,**采用标记-整理算法** -*** +对比其他回收器: +* 其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间 +* Parallel 目标是达到一个可控制的吞吐量,被称为**吞吐量优先**收集器 +* Parallel Scavenge 对比 ParNew 拥有**自适应调节策略**,可以通过一个开关参数打开 GC Ergonomics +应用场景: -##### 缓存泄露 +* 停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验 +* 高吞吐量可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互 -内存泄漏的一个常见来源是缓存,一旦把对象引用放入到缓存中,就会很容易被遗忘 +停顿时间和吞吐量的关系:新生代空间变小 → 缩短停顿时间 → 垃圾回收变得频繁 → 导致吞吐量下降 -使用 WeakHashMap 代表缓存,当除了自身有对 key 的引用外没有其他引用,map 会自动丢弃此值 +在注重吞吐量及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge + Parallel Old 收集器,在 Server 模式下的内存回收性能很好,**Java8 默认是此垃圾收集器组合** +![](https://gitee.com/seazean/images/raw/master/Java/JVM-ParallelScavenge收集器.png) +参数配置: -*** +* `-XX:+UseParallelGC`:手动指定年轻代使用Paralle并行收集器执行内存回收任务 +* `-XX:+UseParalleloldcc`:手动指定老年代使用并行回收收集器执行内存回收任务 + * 上面两个参数,默认开启一个,另一个也会被开启(互相激活),默认 jdk8 是开启的 +* `-XX:+UseAdaptivesizepplicy`:设置 Parallel scavenge 收集器具有**自适应调节策略**,在这种模式下,年轻代的大小、Eden 和 Survivor 的比例、晋升老年代的对象年龄等参数会被自动调整,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量 +* `-XX:ParallelGcrhreads`:设置年轻代并行收集器的线程数,一般最好与 CPU 数量相等,以避免过多的线程数影响垃圾收集性能 + * 在默认情况下,当 CPU 数量小于 8 个,ParallelGcThreads 的值等于 CPU 数量 + * 当 CPU 数量大于 8 个,ParallelGCThreads 的值等于 3+[5*CPU Count]/8] +* `-XX:MaxGCPauseMillis`:设置垃圾收集器最大停顿时间(即 STW 的时间),单位是毫秒 + * 对于用户来讲,停顿时间越短体验越好;在服务器端,注重高并发,整体的吞吐量 + * 为了把停顿时间控制在 MaxGCPauseMillis 以内,收集器在工作时会调整 Java 堆大小或其他一些参数 +* `-XX:GCTimeRatio`:垃圾收集时间占总时间的比例 =1/(N+1),用于衡量吞吐量的大小 + * 取值范围(0,100)。默认值 99,也就是垃圾回收时间不超过1 + * 与 `-xx:MaxGCPauseMillis` 参数有一定矛盾性,暂停时间越长,Radio 参数就容易超过设定的比例 -##### 监听器 +*** -监听器和其他回调情况,假如客户端在实现的 API 中注册回调,却没有显式的取消,那么就会一直积聚下去,所以确保回调立即被当作垃圾回收的最佳方法是只保存它的弱引用,比如保存为 WeakHashMap 中的键 +#### ParNew -*** +Par 是 Parallel 并行的缩写,New 是只能处理的是新生代 +并行垃圾收集器在串行垃圾收集器的基础之上做了改进,**采用复制算法**,将单线程改为了多线程进行垃圾回收,可以缩短垃圾回收的时间 +对于其他的行为(收集算法、stop the world、对象分配规则、回收策略等)同 Serial 收集器一样,应用在年轻代,除 Serial 外,只有**ParNew GC 能与 CMS 收集器配合工作** -#### 案例分析 +相关参数: -```java -public class Stack { - private Object[] elements; - private int size = 0; - private static final int DEFAULT_INITIAL_CAPACITY = 16; +* `-XX:+UseParNewGC`:表示年轻代使用并行收集器,不影响老年代 - public Stack() { - elements = new Object[DEFAULT_INITIAL_CAPACITY]; - } +* `-XX:ParallelGCThreads`:默认开启和 CPU 数量相同的线程数 - public void push(Object e) { //入栈 - ensureCapacity(); - elements[size++] = e; - } +![](https://gitee.com/seazean/images/raw/master/Java/JVM-ParNew收集器.png) - public Object pop() { //出栈 - if (size == 0) - throw new EmptyStackException(); - return elements[--size]; - } +ParNew 是很多 JVM 运行在 Server 模式下新生代的默认垃圾收集器 - private void ensureCapacity() { - if (elements.length == size) - elements = Arrays.copyOf(elements, 2 * size + 1); - } -} -``` +- 对于新生代,回收次数频繁,使用并行方式高效 +- 对于老年代,回收次数少,使用串行方式节省资源(CPU 并行需要切换线程,串行可以省去切换线程的资源) -程序并没有明显错误,但 pop 函数存在内存泄漏问题,因为 pop 函数只是把栈顶索引下移一位,并没有把上一个出栈索引处的引用置空,导致栈数组一直强引用着已经出栈的对象 -解决方法: -```java -public Object pop() { - if (size == 0) - throw new EmptyStackException(); - Object result = elements[--size]; - elements[size] = null; - return result; -} -``` +**** +#### CMS +CMS 全称 Concurrent Mark Sweep,是一款**并发的、使用标记-清除**算法、针对老年代的垃圾回收器,其最大特点是**让垃圾收集线程与用户线程同时工作** -*** +CMS 收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,停顿时间越短(**低延迟**)越适合与用户交互的程序,良好的响应速度能提升用户体验 +分为以下四个流程: +- 初始标记:使用 STW 出现短暂停顿,仅标记一下 GC Roots 能直接关联到的对象,速度很快 +- 并发标记:进行 GC Roots 开始遍历整个对象图,在整个回收过程中耗时最长,不需要 STW,可以与用户线程并发运行 +- 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,比初始标记时间长但远比并发标记时间短,需要 STW(不停顿就会一直变化,采用写屏障 + 增量更新来避免漏标情况) +- 并发清除:清除标记为可以回收对象,不需要移动存活对象,所以这个阶段可以与用户线程同时并发的 +Mark Sweep 会造成内存碎片,不把算法换成 Mark Compact 的原因: +* Mark Compact 算法会整理内存,导致用户线程使用的对象的地址改变,影响用户线程继续执行 -## 类加载 +* Mark Compact 更适合 Stop The World 场景 -### 对象访存 +在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿 -#### 存储结构 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-CMS收集器.png) -一个 Java 对象内存中存储为三部分:对象头(Header)、实例数据(Instance Data)和对齐填充 (Padding) +优点:并发收集、低延迟 -对象头: +缺点: -* 普通对象:分为两部分 +- 吞吐量降低:在并发阶段虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,CPU 利用率不够高 +- CMS 收集器**无法处理浮动垃圾**,可能出现 Concurrent Mode Failure 导致另一次 Full GC 的产生 + + 浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾(产生了新对象),这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,CMS 收集需要预留出一部分内存,不能等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS,导致很长的停顿时间 +- 标记 - 清除算法导致的空间碎片,往往出现老年代空间无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC;为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer)技术,而只能够选择空闲列表(Free List)执行内存分配 - * **Mark Word**:用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等 +参数设置: - ```ruby - hash(25) + age(4) + lock(3) = 32bit #32位系统 - unused(25+1) + hash(31) + age(4) + lock(3) = 64bit #64位系统 - ``` +* `-XX:+UseConcMarkSweepGC`:手动指定使用 CMS 收集器执行内存回收任务 - * **Klass Word**:类型指针,**指向该对象的 Class 类对象的指针**,虚拟机通过这个指针来确定这个对象是哪个类的实例;在 64 位系统中,开启指针压缩(-XX:+UseCompressedOops)或者 JVM 堆的最大值小于 32G,这个指针也是 4byte,否则是 8byte(就是 **Java 中的一个引用的大小**) + 开启该参数后会自动将 `-XX:+UseParNewGC` 打开,即:ParNew + CMS + Serial old的组合 - ```ruby - |-----------------------------------------------------| - | Object Header (64 bits) | - |---------------------------|-------------------------| - | Mark Word (32 bits) | Klass Word (32 bits) | - |---------------------------|-------------------------| - ``` +* `-XX:CMSInitiatingoccupanyFraction`:设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收 -* 数组对象:如果对象是一个数组,那在对象头中还有一块数据用于记录数组长度(12 字节) + * JDK5 及以前版本的默认值为 68,即当老年代的空间使用率达到 68% 时,会执行一次CMS回收 + * JDK6 及以上版本默认值为 92% - ```ruby - |-------------------------------------------------------------------------------| - | Object Header (96 bits) | - |-----------------------|-----------------------------|-------------------------| - | Mark Word(32bits) | Klass Word(32bits) | array length(32bits) | - |-----------------------|-----------------------------|-------------------------| - ``` +* `-XX:+UseCMSCompactAtFullCollection`:用于指定在执行完 Full GC 后对内存空间进行压缩整理,以此避免内存碎片的产生,由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长 -实例数据:实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的,都需要记录起来 +* `-XX:CMSFullGCsBeforecompaction`:**设置在执行多少次 Full GC 后对内存空间进行压缩整理** -对齐填充:Padding 起占位符的作用。64 位系统,由于 HotSpot VM 的自动内存管理系统要求**对象起始地址必须是 8 字节的整数倍**,就是对象的大小必须是 8 字节的整数倍,而对象头部分正好是 8 字节的倍数(1 倍或者 2 倍),因此当对象实例数据部分没有对齐时,就需要通过对齐填充来补全 +* `-XX:ParallelCMSThreads`:**设置CMS的线程数量** -32 位系统: + * CMS 默认启动的线程数是(ParallelGCThreads+3)/4,ParallelGCThreads 是年轻代并行收集器的线程数 + * 收集线程占用的 CPU 资源多于25%,对用户程序影响可能较大;当 CPU 资源比较紧张时,受到 CMS 收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕 -* 一个 int 在 java 中占据 4byte,所以 Integer 的大小为: - ```java - private final int value; - ``` - ```ruby - # 需要补位4byte - 4(Mark Word) + 4(Klass Word) + 4(data) + 4(Padding) = 16byte - ``` +*** -* `int[] arr = new int[10]` - ```ruby - # 由于需要8位对齐,所以最终大小为`56byte`。 - 4(Mark Word) + 4(Klass Word) + 4(length) + 4*10(10个int大小) + 4(Padding) = 56sbyte - ``` +#### G1 +##### G1特点 -*** +G1(Garbage-First)是一款面向服务端应用的垃圾收集器,**应用于新生代和老年代**、采用标记-整理算法、软实时、低延迟、可设定目标(最大STW停顿时间)的垃圾回收器,用于代替 CMS,适用于较大的堆(>4 ~ 6G),在 JDK9 之后默认使用 G1 +G1 对比其他处理器的优点: +* 并发与并行: + * 并行性:G1 在回收期间,可以有多个 GC 线程同时工作,有效利用多核计算能力,此时用户线程 STW + * 并发性:G1 拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此不会在整个回收阶段发生完全阻塞应用程序的情况 + * 其他的垃圾收集器使用内置的 JVM 线程执行 GC 的多线程操作,而 G1 GC 可以采用应用线程承担后台运行的 GC 工作,JVM 的 GC 线程处理速度慢时,系统会**调用应用程序线程加速垃圾回收**过程 -#### 实际大小 +* **分区算法**: + * 从分代上看,G1 属于分代型垃圾回收器,区分年轻代和老年代,年轻代依然有 Eden 区和 Survivor 区。从堆结构上看,**新生代和老年代不再物理隔离**,不用担心每个代内存是否足够,这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC + * 将整个堆划分成约 2048 个大小相同的独立 Region 块,每个 Region 块大小根据堆空间的实际大小而定,整体被控制在 1MB 到 32 MB之间且为 2 的 N 次幂,所有 Region 大小相同,在 JVM 生命周期内不会被改变。G1 把堆划分成多个大小相等的独立区域,使得每个小空间可以单独进行垃圾回收 + * **新的区域 Humongous**:本身属于老年代区,当出现了一个巨型对象超出了分区容量的一半,该对象就会进入到该区域。如果一个 H 区装不下一个巨型对象,那么 G1 会寻找连续的 H 分区来存储,为了能找到连续的H区,有时候不得不启动 Full GC + * G1 不会对巨型对象进行拷贝,回收时被优先考虑,G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为 0 的巨型对象就可以在新生代垃圾回收时处理掉 + + * Region 结构图: + -浅堆(Shallow Heap):**对象本身占用的内存,不包括内部引用对象的大小**,32 位系统中一个对象引用占 4 个字节,每个对象头占用 8 个字节,根据堆快照格式不同,对象的大小会同 8 字节进行对齐 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-G1-Region区域.png) -JDK7 中的 String:2个 int 值共占 8 字节,value 对象引用占用 4 字节,对象头 8 字节,对齐后占 24 字节,为 String 对象的浅堆大小,与 value 实际取值无关,无论字符串长度如何,浅堆大小始终是 24 字节 +- 空间整合: -```java -private final char value[]; -private int hash; -private int hash32; -``` + - CMS:标记-清除算法、内存碎片、若干次 GC 后进行一次碎片整理 + - G1:整体来看是基于标记 - 整理算法实现的收集器,从局部(Region 之间)上来看是基于复制算法实现的,两种算法都可以避免内存碎片 -保留集(Retained Set):对象 A 的保留集指当对象 A 被垃圾回收后,可以被释放的所有的对象集合(包括 A 本身),所以对象 A 的保留集就是只能通过对象 A 被直接或间接访问到的所有对象的集合,就是仅被对象 A 所持有的对象的集合 +- **可预测的停顿时间模型(软实时 soft real-time)**:可以指定在 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒 -深堆(Retained Heap):指对象的保留集中所有的对象的浅堆大小之和,一个对象的深堆指只能通过该对象访问到的(直接或间接)所有对象的浅堆之和,即对象被回收后,可以释放的真实空间 + - 由于分块的原因,G1 可以只选取部分区域进行内存回收,这样缩小了回收的范围,对于全局停顿情况也能得到较好的控制 + - G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间,通过过去回收的经验获得),在后台维护一个**优先列表**,每次根据允许的收集时间优先回收价值最大的 Region,保证了 G1 收集器在有限的时间内可以获取尽可能高的收集效率 -对象的实际大小:一个对象所能触及的所有对象的浅堆大小之和,也就是通常意义上我们说的对象大小 + * 相比于 CMS GC,G1 未必能做到 CMS 在最好情况下的延时停顿,但是最差情况要好很多 -下图显示了一个简单的对象引用关系图,对象 A 引用了 C 和 D,对象 B 引用了 C 和 E。那么对象 A 的浅堆大小只是 A 本身,**A 的实际大小为 A、C、D 三者之和**,A 的深堆大小为 A 与 D 之和,由于对象 C 还可以通过对象 B 访问到 C,因此 C 不在对象 A 的深堆范围内 +G1垃圾收集器的缺点: - +* 相较于 CMS,G1 还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1 无论是为了垃圾收集产生的内存占用还是程序运行时的额外执行负载都要比 CMS 要高 +* 从经验上来说,在小内存应用上 CMS 的表现大概率会优于 G1,而 G1 在大内存应用上则发挥其优势。平衡点在 6-8GB 之间 -内存分析工具 MAT 提供了一种叫支配树的对象图,体现了对象实例间的支配关系 +应用场景: -基本性质: +* 面向服务端应用,针对具有大内存、多处理器的机器 +* 需要低 GC 延迟,并具有大堆的应用程序提供解决方案 -- 对象 A 的子树(所有被对象 A 支配的对象集合)表示对象 A 的保留集(retained set),即深堆 -- 如果对象 A 支配对象 B,那么对象 A 的直接支配者也支配对象 B -- 支配树的边与对象引用图的边不直接对应 +*** -左图表示对象引用图,右图表示左图所对应的支配树: -![](https://gitee.com/seazean/images/raw/master/Java/JVM-支配树.png) -比如:对象 F 与对象 D 相互引用,因为到对象 F 的所有路径必然经过对象 D,因此对象 D 是对象 F 的直接支配者 +##### 记忆集 +记忆集 Remembered Set 在新生代中,每个 Region 都有一个 Remembered Set,用来被哪些其他 Region 里的对象引用(谁引用了我就记录谁) + -参考文章:https://www.yuque.com/u21195183/jvm/nkq31c +* 程序对 Reference 类型数据写操作时,产生一个 Write Barrier 暂时中断操作,检查该对象和 Reference 类型数据是否在不同的 Region(跨代引用),不同就将相关引用信息记录到 Reference 类型所属的 Region 的 Remembered Set 之中 +* 进行内存回收时,在 GC 根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏 +垃圾收集器在新生代中建立了记忆集这样的数据结构,可以理解为它是一个抽象类,具体实现记忆集的三种方式: +* 字长精度 +* 对象精度 +* 卡精度(卡表) -*** +卡表(Card Table)在老年代中,是一种对记忆集的具体实现,主要定义了记忆集的记录精度、与堆内存的映射关系等,卡表中的每一个元素都对应着一块特定大小的内存块,这个内存块称之为卡页(card page),当存在跨代引用时,会将卡页标记为 dirty,JVM 对于卡页的维护也是通过写屏障的方式 +收集集合 CSet 代表每次 GC 暂停时回收的一系列目标分区,在任意一次收集暂停中,CSet 所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。年轻代收集 CSet 只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到 CSet 中 +* CSet of Young Collection +* CSet of Mix Collection -#### 节约内存 -* 尽量使用基本数据类型 -* 满足容量前提下,尽量用小字段 +*** -* 尽量用数组,少用集合,数组中是可以使用基本类型的,但是集合中只能放包装类型,如果需要使用集合,推荐比较节约内存的集合工具:fastutil - 一个 ArrayList 集合,如果里面放了 10 个数字,占用多少内存: - ```java - private transient Object[] elementData; - private int size; - ``` +##### 工作原理 - Mark Word 占 4byte,Klass Word 占 4byte,一个 int 字段占 4byte,elementData 数组占 12byte,数组中 10 个 Integer 对象占 10×16,所以整个集合空间大小为 184byte(深堆) +G1 中提供了三种垃圾回收模式:YoungGC、Mixed GC 和 FullGC,在不同的条件下被触发 -* 时间用 long/int 表示,不用 Date 或者 String +* 当堆内存使用达到一定值(默认 45%)时,开始老年代并发标记过程 +* 标记完成马上开始混合回收过程 + +顺时针:Young GC → Young GC + Concurrent Mark → Mixed GC 顺序,进行垃圾回收 -*** +* **Young GC**:发生在年轻代的 GC 算法,一般对象(除了巨型对象)都是在 eden region 中分配内存,当所有 eden region 被耗尽无法申请内存时,就会触发一次 young gc,G1 停止应用程序的执行 STW,把活跃对象放入老年代,垃圾对象回收 + **回收过程**: + 1. 扫描根:根引用连同 RSet 记录的外部引用作为扫描存活对象的入口 + 2. 更新 RSet:处理 dirty card queue 更新 RS,此后 RSet 准确的反映对象的引用关系 + * dirty card queue:类似缓存,产生了引用先记录在这里,然后更新到 RSet + * 作用:产生引用直接更新 RSet 需要线程同步开销很大,使用队列性能好 + 3. 处理 RSet:识别被老年代对象指向的 Eden 中的对象,这些被指向的对象被认为是存活的对象,把需要回收的分区放入 Young CSet 中进行回收 + 4. 复制对象:Eden 区内存段中存活的对象会被复制到 survivor 区,survivor 区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到 old 区中空的内存分段,如果 survivor 空间不够,Eden 空间的部分数据会直接晋升到老年代空间 + 5. 处理引用:处理 Soft,Weak,Phantom,JNI Weak 等引用,最终 Eden 空间的数据为空,GC 停止工作 -#### 对象访问 +* **Concurrent Mark **: -JVM 是通过**栈帧中的对象引用**访问到其内部的对象实例: + * 初始标记:标记从根节点直接可达的对象,这个阶段是 STW 的,并且会触发一次年轻代 GC + * 根区域扫描 (Root Region Scanning):扫描 Survivor 区中指向老年代的,被初始标记标记了的引用及引用的对象,这一个过程是并发进行的,但是必须在 Young GC 之前完成 + * 并发标记 (Concurrent Marking):在整个堆中进行并发标记(应用程序并发执行),可能被 YoungGC 中断。会计算每个区域的对象活性,即区域中存活对象的比例,若区域中的所有对象都是垃圾,则这个区域会被立即回收(实时回收),给浮动垃圾准备出更多的空间,把需要收集的 Region 放入 CSet 当中 + * 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这阶段需要停顿线程,但是可并行执行(**防止漏标**) + * 筛选回收:并发清理阶段,首先对 CSet 中各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率 -* 句柄访问:Java 堆中会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息 - - 优点:reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集)时只会改变句柄中的实例数据指针,而 reference 本身不需要被修改 + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-G1收集器.jpg) - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-对象访问-句柄访问.png) +* **Mixed GC**:当很多对象晋升到老年代时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即 Mixed GC,除了回收整个 young region,还会**回收一部分**的 old region,过程同 YGC -* 直接指针(HotSpot 采用):Java 堆对象的布局必须考虑如何放置访问类型数据的相关信息,reference 中直接存储的对象地址 - - 优点:速度更快,**节省了一次指针定位的时间开销** + 注意:**是一部分老年代,而不是全部老年代**,可以选择哪些老年代 region 收集,对垃圾回收的时间进行控制 - 缺点:对象被移动时(如进行GC后的内存重新排列),对象的 reference 也需要同步更新 - - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-对象访问-直接指针.png) + 在 G1 中,Mixed GC 可以通过 `-XX:InitiatingHeapOccupancyPercent` 设置阈值 +* **Full GC**:对象内存分配速度过快,Mixed GC 来不及回收,导致老年代被填满,就会触发一次 Full GC,G1 的 Full GC 算法就是单线程执行的垃圾回收,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免 Full GC + 产生 Full GC 的原因: -参考文章:https://www.cnblogs.com/afraidToForget/p/12584866.html + * 晋升时没有足够的空间存放晋升的对象 + * 并发处理过程完成之前空间耗尽,浮动垃圾 @@ -11662,178 +11938,123 @@ JVM 是通过**栈帧中的对象引用**访问到其内部的对象实例: -### 对象创建 - -#### 生命周期 +##### 相关参数 -在 Java 中,对象的生命周期包括以下几个阶段: +- `-XX:+UseG1GC`:手动指定使用 G1 垃圾收集器执行内存回收任务 +- `-XX:G1HeapRegionSize`:设置每个 Region 的大小。值是 2 的幂,范围是 1MB 到 32MB 之间,目标是根据最小的 Java 堆大小划分出约 2048 个区域,默认是堆内存的 1/2000 +- `-XX:MaxGCPauseMillis`:设置期望达到的最大 GC 停顿时间指标,JVM会尽力实现,但不保证达到,默认值是 200ms +- `-XX:+ParallelGcThread`:设置 STW 时 GC 线程数的值,最多设置为 8 +- `-XX:ConcGCThreads`:设置并发标记线程数,设置为并行垃圾回收线程数 ParallelGcThreads 的1/4左右 +- `-XX:InitiatingHeapoccupancyPercent`:设置触发并发 Mixed GC 周期的 Java 堆占用率阈值,超过此值,就触发 GC,默认值是 45 +- `-XX:+ClassUnloadingWithConcurrentMark`:并发标记类卸载,默认启用,所有对象都经过并发标记后,就可以知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类 +- `-XX:G1NewSizePercent`:新生代占用整个堆内存的最小百分比(默认5%) +- `-XX:G1MaxNewSizePercent`:新生代占用整个堆内存的最大百分比(默认60%) +- `-XX:G1ReservePercent=10`:保留内存区域,防止 to space(Survivor中的 to 区)溢出 -1. 创建阶段 (Created): -2. 应用阶段 (In Use):对象至少被一个强引用持有着 -3. 不可见阶段 (Invisible):程序的执行已经超出了该对象的作用域,不再持有该对象的任何强引用 -4. 不可达阶段 (Unreachable):该对象不再被任何强引用所持有,包括 GC Root 的强引用 -5. 收集阶段 (Collected):垃圾回收器对该对象的内存空间重新分配做好准备,该对象如果重写了 finalize() 方法,则会去执行该方法 -6. 终结阶段 (Finalized):等待垃圾回收器对该对象空间进行回收,当对象执行完 finalize()方 法后仍然处于不可达状态时进入该阶段 -7. 对象空间重分配阶段 (De-allocated):垃圾回收器对该对象的所占用的内存空间进行回收或者再分配 +*** -参考文章:https://blog.csdn.net/sodino/article/details/38387049 +##### 调优 -*** +G1 的设计原则就是简化 JVM 性能调优,只需要简单的三步即可完成调优: +1. 开启 G1 垃圾收集器 +2. 设置堆的最大内存 +3. 设置最大的停顿时间(STW) +不断调优暂停时间指标: -#### 创建时机 +* `XX:MaxGCPauseMillis=x` 可以设置启动应用程序暂停的时间,G1会根据这个参数选择 CSet 来满足响应时间的设置 +* 设置到 100ms 或者 200ms 都可以(不同情况下会不一样),但设置成50ms就不太合理 +* 暂停时间设置的太短,就会导致出现 G1 跟不上垃圾产生的速度,最终退化成 Full GC +* 对这个参数的调优是一个持续的过程,逐步调整到最佳状态 -类在第一次实例化加载一次,后续实例化不再加载,引用第一次加载的类 +不要设置新生代和老年代的大小: -Java 对象创建时机: +- 避免使用 -Xmn 或 -XX:NewRatio 等相关选项显式设置年轻代大小,G1 收集器在运行的时候会调整新生代和老年代的大小,从而达到我们为收集器设置的暂停时间目标 +- 设置了新生代大小相当于放弃了 G1 为我们做的自动调优,我们需要做的只是设置整个堆内存的大小,剩下的交给 G1 自己去分配各个代的大小 -1. 使用 new 关键字创建对象:由执行类实例创建表达式而引起的对象创建 -2. 使用 Class 类的 newInstance 方法(反射机制) -3. 使用 Constructor 类的 newInstance 方法(反射机制) +*** - ```java - public class Student { - private int id; - public Student(Integer id) { - this.id = id; - } - public static void main(String[] args) throws Exception { - Constructor c = Student.class.getConstructor(Integer.class); - Student stu = c.newInstance(123); - } - } - ``` - 使用 newInstance 方法的这两种方式创建对象使用的就是 Java 的反射机制,事实上 Class 的 newInstance 方法内部调用的也是 Constructor 的 newInstance 方法 -4. 使用 Clone 方法创建对象:用 clone 方法创建对象的过程中并不会调用任何构造函数,要想使用 clone 方法,我们就必须先实现 Cloneable 接口并实现其定义的 clone 方法 +#### ZGC -5. 使用(反)序列化机制创建对象:当反序列化一个对象时,JVM 会创建一个**单独的对象**,在此过程中,JVM 并不会调用任何构造函数,为了反序列化一个对象,需要让类实现 Serializable 接口 +ZGC 收集器是一个可伸缩的、低延迟的垃圾收集器,基于 Region 内存布局的,不设分代,使用了读屏障、染色指针和内存多重映射等技术来实现**可并发的标记压缩算法** -从 Java 虚拟机层面看,除了使用 new 关键字创建对象的方式外,其他方式全部都是通过转变为 invokevirtual 指令直接创建对象的 - - - -*** +* 在 CMS 和 G1 中都用到了写屏障,而 ZGC 用到了读屏障 +* 染色指针:直接**将少量额外的信息存储在指针上的技术**,从 64 位的指针中拿高 4 位来标识对象此时的状态 + * 染色指针可以使某个 Region 的存活对象被移走之后,这个 Region 立即就能够被释放和重用 + * 可以直接从指针中看到引用对象的三色标记状态(Marked0、Marked1)、是否进入了重分配集、是否被移动过(Remapped)、是否只能通过 finalize() 方法才能被访问到(Finalizable) + * 可以大幅减少在垃圾收集过程中内存屏障的使用数量,写屏障的目的通常是为了记录对象引用的变动情况,如果将这些信息直接维护在指针中,显然就可以省去一些专门的记录操作 + * 可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据 +* 内存多重映射:多个虚拟地址指向同一个物理地址 +可并发的标记压缩算法:染色指针标识对象是否被标记或移动,读屏障保证在每次应用程序或 GC 程序访问对象时先根据染色指针的标识判断是否被移动,如果被移动就根据转发表访问新的移动对象,并更新引用,不会像 G1 一样必须等待垃圾回收完成才能访问 +ZGC 目标: -#### 创建过程 +- 停顿时间不会超过 10ms +- 停顿时间不会随着堆的增大而增大(不管多大的堆都能保持在 10ms 以下) +- 可支持几百 M,甚至几 T 的堆大小(最大支持4T) -创建对象的过程: +ZGC 的工作过程可以分为 4 个阶段: -1. 判断对象对应的类是否加载、链接、初始化 +* 并发标记(Concurrent Mark): 遍历对象图做可达性分析的阶段,也要经过初始标记和最终标记,需要短暂停顿 +* 并发预备重分配( Concurrent Prepare for Relocate):根据特定的查询条件统计得出本次收集过程要清理哪些 Region,将这些 Region 组成重分配集(Relocation Set) +* 并发重分配(Concurrent Relocate): 重分配是 ZGC 执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的 Region 上,并为重分配集中的**每个 Region 维护一个转发表**(Forward Table),记录从旧地址到新地址的转向关系 +* 并发重映射(Concurrent Remap):修正整个堆中指向重分配集中旧对象的所有引用,ZGC 的并发映射并不是一个必须要立即完成的任务,ZGC 很巧妙地把并发重映射阶段要做的工作,合并到下一次垃圾收集循环中的并发标记阶段里去完成,因为都是要遍历所有对象,这样合并节省了一次遍历的开销 -2. 为对象分配内存:指针碰撞、空闲链表。当一个对象被创建时,虚拟机就会为其分配内存来存放对象的实例变量及其从父类继承过来的实例变量,即使从**隐藏变量**也会被分配空间(继承部分解释了为什么会隐藏) +ZGC 几乎在所有地方并发执行的,除了初始标记的是 STW 的,但这部分的实际时间是非常少的,所以响应速度快,在尽可能对吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟 -3. 处理并发安全问题: +优点:高吞吐量、低延迟 - * 采用 CAS 配上自旋保证更新的原子性 - * 每个线程预先分配一块 TLAB +缺点:浮动垃圾,当 ZGC 准备要对一个很大的堆做一次完整的并发收集,其全过程要持续十分钟以上,由于应用的对象分配速率很高,将创造大量的新对象产生浮动垃圾 -4. 初始化分配的空间:虚拟机将分配到的内存空间都初始化为零值(不包括对象头),保证对象实例字段在不赋值时可以直接使用,程序能访问到这些字段的数据类型所对应的零值 -5. 设置对象的对象头:将对象的所属类(类的元数据信息)、对象的 HashCode、对象的 GC 信息、锁信息等数据存储在对象头中 -6. 执行 init 方法进行实例化:实例变量初始化、实例代码块初始化 、构造函数初始化 +参考文章:https://www.cnblogs.com/jimoer/p/13170249.html - * 实例变量初始化与实例代码块初始化: - 对实例变量直接赋值或者使用实例代码块赋值,**编译器会将其中的代码放到类的构造函数中去**,并且这些代码会被放在对超类构造函数的调用语句之后 (Java 要求构造函数的第一条语句必须是超类构造函数的调用语句),构造函数本身的代码之前 - * 构造函数初始化: +*** - **Java 要求在实例化类之前,必须先实例化其超类,以保证所创建实例的完整性**,在准备实例化一个类的对象前,首先准备实例化该类的父类,如果该类的父类还有父类,那么准备实例化该类的父类的父类,依次递归直到递归到 Object 类。然后从 Object 类依次对以下各类进行实例化,初始化父类中的变量和执行构造函数 +#### 总结 -*** +Serial GC、Parallel GC、Concurrent Mark Sweep GC 这三个 GC 不同: +- 最小化地使用内存和并行开销,选 Serial GC +- 最大化应用程序的吞吐量,选 Parallel GC +- 最小化 GC 的中断或停顿时间,选 CMS GC +![](https://gitee.com/seazean/images/raw/master/Java/JVM-垃圾回收器总结.png) -#### 承上启下 -1. 一个实例变量在对象初始化的过程中会被赋值几次? - JVM 在为一个对象分配完内存之后,会给每一个实例变量赋予默认值,这个实例变量被第一次赋值 - 在声明实例变量的同时对其进行了赋值操作,那么这个实例变量就被第二次赋值 - 在实例代码块中又对变量做了初始化操作,那么这个实例变量就被第三次赋值 - 在构造函数中也对变量做了初始化操作,那么这个实例变量就被第四次赋值 - 在 Java 的对象初始化过程中,一个实例变量最多可以被初始化 4 次 -2. 类的初始化过程与类的实例化过程的异同? - 类的初始化是指类加载过程中的初始化阶段对类变量按照代码进行赋值的过程 - 类的实例化是指在类完全加载到内存中后创建对象的过程(类的实例化触发了类的初始化,先初始化才能实例化) +*** -3. 假如一个类还未加载到内存中,那么在创建一个该类的实例时,具体过程是怎样的?(**经典案例**) - ```java - public class StaticTest { - public static void main(String[] args) { - staticFunction();//调用静态方法,触发初始化 - } - - static StaticTest st = new StaticTest(); - - static { //静态代码块 - System.out.println("1"); - } - - { // 实例代码块 - System.out.println("2"); - } - - StaticTest() { // 实例构造器 - System.out.println("3"); - System.out.println("a=" + a + ",b=" + b); - } - - public static void staticFunction() { // 静态方法 - System.out.println("4"); - } - - int a = 110; // 实例变量 - static int b = 112; // 静态变量 - }/* Output: - 2 - 3 - a=110,b=0 - 1 - 4 - *///:~ - ``` - `static StaticTest st = new StaticTest();`: +### 内存泄漏 - * 实例实例化不一定要在类初始化结束之后才开始 +#### 泄露溢出 - * 在同一个类加载器下,一个类型只会被初始化一次。所以一旦开始初始化一个类,无论是否完成后续都不会再重新触发该类型的初始化阶段了(只考虑在同一个类加载器下的情形)。因此在实例化上述程序中的 st 变量时,**实际上是把实例化嵌入到了静态初始化流程中,并且在上面的程序中,嵌入到了静态初始化的起始位置**,这就导致了实例初始化完全发生在静态初始化之前,这也是导致 a 为 110,b 为 0 的原因 +内存泄漏(Memory Leak):是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果 - 代码等价于: +可达性分析算法来判断对象是否是不再使用的对象,本质都是判断一个对象是否还被引用。由于代码的实现不同就会出现很多种内存泄漏问题,让 JVM 误以为此对象还在引用中,无法回收,造成内存泄漏 - ```java - public class StaticTest { - (){ - a = 110; // 实例变量 - System.out.println("2"); // 实例代码块 - System.out.println("3"); // 实例构造器中代码的执行 - System.out.println("a=" + a + ",b=" + b); // 实例构造器中代码的执行 - 类变量st被初始化 - System.out.println("1"); //静态代码块 - 类变量b被初始化为112 - } - } - ``` +内存溢出(out of memory)指的是申请内存时,没有足够的内存可以使用 - +内存泄漏和内存溢出的关系:内存泄漏的越来越多,最终会导致内存溢出 @@ -11841,21 +12062,21 @@ Java 对象创建时机: -### 加载过程 - -#### 生命周期 - -类是在运行期间**第一次使用时动态加载**的(不使用不加载),而不是一次性加载所有类,因为一次性加载会占用很多的内存,加载的类信息存放于一块成为方法区的内存空间 +#### 几种情况 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-类的生命周期.png) +##### 静态集合 -包括 7 个阶段: +静态集合类的生命周期与 JVM 程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。原因是**长生命周期的对象持有短生命周期对象的引用**,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收 -* 加载(Loading) -* 链接:验证(Verification)、准备(Preparation)、解析(Resolution) -* 初始化(Initialization) -* 使用(Using) -* 卸载(Unloading) +```java +public class MemoryLeak { + static List list = new ArrayList(); + public void oomTest(){ + Object obj = new Object();//局部变量 + list.add(obj); + } +} +``` @@ -11863,115 +12084,88 @@ Java 对象创建时机: -#### 加载阶段 +##### 单例模式 -加载是类加载的其中一个阶段,注意不要混淆 +单例模式和静态集合导致内存泄露的原因类似,因为单例的静态特性,它的生命周期和 JVM 的生命周期一样长,所以如果单例对象持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏 -加载过程完成以下三件事: -- 通过类的完全限定名称获取定义该类的二进制字节流(二进制字节码) -- 将该字节流表示的**静态存储结构转换为方法区的运行时存储结构**(Java 类模型) -- 在内存中生成一个代表该类的 Class 对象,作为该类在方法区中的各种数据的访问入口 -其中二进制字节流可以从以下方式中获取: +**** -- 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础 -- 从网络中获取,最典型的应用是 Applet -- 由其他文件生成,例如由 JSP 文件生成对应的 Class 类 -- 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 生成字节码 -将字节码文件加载至方法区后,会**在堆中**创建一个 java.lang.Class 对象,用来引用位于方法区内的数据结构,该 Class 对象是在加载类的过程中创建的,每个类都对应有一个 Class 类型的对象 -方法区内部采用 C++ 的 instanceKlass 描述 Java 类的数据结构: +##### 内部类 -* `_java_mirror` 即 java 的类镜像,例如对 String 来说就是 String.class,作用是把 class 暴露给 java 使用 -* `_super` 即父类、`_fields` 即成员变量、`_methods` 即方法、`_constants` 即常量池、`_class_loader` 即类加载器、`_vtable` **虚方法表**、`_itable` 接口方法表 +内部类持有外部类的情况,如果一个外部类的实例对象调用方法返回了一个内部类的实例对象,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象也不会被回收,造成内存泄漏 -加载过程: -* 如果这个类还有父类没有加载,先加载父类 -* 加载和链接可能是交替运行的 -* Class 对象和 _java_mirror 相互持有对方的地址,堆中对象通过 instanceKlass 和元空间进行交互 - +*** -创建数组类有些特殊,因为数组类本身并不是由类加载器负责创建,而是由 JVM 在运行时根据需要而直接创建的,但数组的元素类型仍然需要依靠类加载器去创建,创建数组类的过程: -- 如果数组的元素类型是引用类型,那么遵循定义的加载过程递归加载和创建数组的元素类型 -- JVM 使用指定的元素类型和数组维度来创建新的数组类 -- 基本数据类型由启动类加载器加载 +##### 连接相关 +数据库连接、网络连接和 IO 连接等,当不再使用时,需要显式调用 close 方法来释放与连接,垃圾回收器才会回收对应的对象,否则将会造成大量的对象无法被回收,从而引起内存泄漏 -*** +**** -#### 链接阶段 -##### 验证 -确保 Class 文件的字节流中包含的信息是否符合 JVM 规范,保证被加载类的正确性,不会危害虚拟机自身的安全 +##### 不合理域 -主要包括**四种验证**: +变量不合理的作用域,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏;如果没有及时地把对象设置为 null,也有可能导致内存泄漏的发生 -* 文件格式验证 +```java +public class UsingRandom { + private String msg; + public void receiveMsg(){ + msg = readFromNet();// 从网络中接受数据保存到 msg 中 + saveDB(msg); // 把 msg 保存到数据库中 + } +} +``` -* 语义检查,但凡在语义上不符合规范的,虚拟机不会给予验证通过 +通过 readFromNet 方法把接收消息保存在 msg 中,然后调用 saveDB 方法把内容保存到数据库中,此时 msg 已经可以被回收,但是 msg 的生命周期与对象的生命周期相同,造成 msg 不能回收,产生内存泄漏 - * 是否所有的类都有父类的存在(除了 Object 外,其他类都应该有父类) +解决: - * 是否一些被定义为 final 的方法或者类被重写或继承了 +* msg 变量可以放在 receiveMsg 方法内部,当方法使用完,msg 的生命周期也就结束,就可以被回收了 +* 在使用完 msg 后,把 msg 设置为 null,这样垃圾回收器也会回收 msg 的内存空间。 - * 非抽象类是否实现了所有抽象方法或者接口方法 - * 是否存在不兼容的方法 -* 字节码验证,试图通过对字节码流的分析,判断字节码是否可以被正确地执行 +**** - * 在字节码的执行过程中,是否会跳转到一条不存在的指令 - * 函数的调用是否传递了正确类型的参数 - * 变量的赋值是不是给了正确的数据类型 - * 栈映射帧(StackMapTable)在这个阶段用于检测在特定的字节码处,其局部变量表和操作数栈是否有着正确的数据类型 -* 符号引用验证,Class 文件在其常量池会通过字符串记录将要使用的其他类或者方法 +##### 改变哈希 +当一个对象被存储进 HashSet 集合中以后,就**不能修改这个对象中的那些参与计算哈希值的字段**,否则对象修改后的哈希值与最初存储进 HashSet 集合中时的哈希值不同,这种情况下使用该对象的当前引用作为的参数去 HashSet 集合中检索对象返回 false,导致无法从 HashSet 集合中单独删除当前对象,造成内存泄漏 -*** +*** -##### 准备 -准备阶段为**静态变量分配内存并设置初始值**,使用的是方法区的内存: -* 类变量也叫静态变量,就是是被 static 修饰的变量 -* 实例变量也叫对象变量,即没加 static 的变量 +##### 缓存泄露 -说明:实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次 +内存泄漏的一个常见来源是缓存,一旦把对象引用放入到缓存中,就会很容易被遗忘 -类变量初始化: +使用 WeakHashMap 代表缓存,当除了自身有对 key 的引用外没有其他引用,map 会自动丢弃此值 -* static 变量分配空间和赋值是两个步骤:**分配空间在准备阶段完成,赋值在初始化阶段完成** -* 如果 static 变量是 final 的基本类型以及字符串常量,那么编译阶段值(方法区)就确定了,准备阶段会显式初始化 -* 如果 static 变量是 final 的,但属于引用类型或者构造器方法的字符串,赋值在初始化阶段完成 -实例: -* 初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123: +*** - ```java - public static int value = 123; - ``` -* 常量 value 被初始化为 123 而不是 0: - ```java - public static final int value = 123; - ``` +##### 监听器 -* Java 并不支持 boolean 类型,对于 boolean 类型,内部实现是 int,由于 int 的默认值是0,故 boolean 的默认值就是 false +监听器和其他回调情况,假如客户端在实现的 API 中注册回调,却没有显式的取消,那么就会一直积聚下去,所以确保回调立即被当作垃圾回收的最佳方法是只保存它的弱引用,比如保存为 WeakHashMap 中的键 @@ -11979,194 +12173,195 @@ Java 对象创建时机: -##### 解析 +#### 案例分析 -将常量池中类、接口、字段、方法的**符号引用替换为直接引用**(内存地址)的过程: +```java +public class Stack { + private Object[] elements; + private int size = 0; + private static final int DEFAULT_INITIAL_CAPACITY = 16; -* 符号引用:一组符号来描述目标,可以是任何字面量,属于编译原理方面的概念,如:包括类和接口的全限名、字段的名称和描述符、方法的名称和**方法描述符** -* 直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄,如果有了直接引用,那说明引用的目标必定已经存在于内存之中 + public Stack() { + elements = new Object[DEFAULT_INITIAL_CAPACITY]; + } -例如:在 `com.demo.Solution` 类中引用了 `com.test.Quest`,把 `com.test.Quest` 作为符号引用存进类常量池,在类加载完后,**用这个符号引用去方法区找这个类的内存地址** + public void push(Object e) { //入栈 + ensureCapacity(); + elements[size++] = e; + } -解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等 - -* 在类加载阶段解析的是非虚方法,静态绑定 -* 也可以在初始化阶段之后再开始解析,这是为了支持 Java 的**动态绑定** -* 通过解析操作,符号引用就可以转变为目标方法在类的虚方法表中的位置,从而使得方法被成功调用 + public Object pop() { //出栈 + if (size == 0) + throw new EmptyStackException(); + return elements[--size]; + } -```java -public class Load2 { - public static void main(String[] args) throws Exception{ - ClassLoader classloader = Load2.class.getClassLoader(); - // cloadClass 加载类方法不会导致类的解析和初始化,也不会加载D - Class c = classloader.loadClass("cn.jvm.t3.load.C"); - - // new C();会导致类的解析和初始化,从而解析初始化D - System.in.read(); + private void ensureCapacity() { + if (elements.length == size) + elements = Arrays.copyOf(elements, 2 * size + 1); } } -class C { - D d = new D(); -} -class D { -} ``` +程序并没有明显错误,但 pop 函数存在内存泄漏问题,因为 pop 函数只是把栈顶索引下移一位,并没有把上一个出栈索引处的引用置空,导致栈数组一直强引用着已经出栈的对象 +解决方法: -**** - +```java +public Object pop() { + if (size == 0) + throw new EmptyStackException(); + Object result = elements[--size]; + elements[size] = null; + return result; +} +``` -#### 初始化 -##### 介绍 -初始化阶段才真正开始执行类中定义的 Java 程序代码,在准备阶段,类变量已经赋过一次系统要求的初始值;在初始化阶段,通过程序制定的计划去初始化类变量和其它资源,执行 -在编译生成 class 文件时,编译器会产生两个方法加于 class 文件中,一个是类的初始化方法 clinit,另一个是实例的初始化方法 init +*** -类构造器 () 与实例构造器 () 不同,它不需要程序员进行显式调用,在一个类的生命周期中,类构造器最多被虚拟机**调用一次**,而实例构造器则会被虚拟机调用多次,只要程序员创建对象 -类在第一次实例化加载一次,把 class 读入内存,后续实例化不再加载,引用第一次加载的类 -*** +## 类加载 +### 对象访存 +#### 存储结构 -##### clinit +一个 Java 对象内存中存储为三部分:对象头(Header)、实例数据(Instance Data)和对齐填充 (Padding) -():类构造器,由编译器自动收集类中**所有类变量的赋值动作和静态语句块**中的语句合并产生的 +对象头: -作用:是在类加载过程中的初始化阶段进行静态变量初始化和执行静态代码块 +* 普通对象:分为两部分 -* 如果类中没有静态变量或静态代码块,那么 clinit 方法将不会被生成 -* clinit 方法只执行一次,在执行 clinit 方法时,必须先执行父类的clinit方法 -* static 变量的赋值操作和静态代码块的合并顺序由源文件中出现的顺序决定 -* static 不加 final 的变量都在初始化环节赋值 + * **Mark Word**:用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等 -**线程安全**问题: + ```ruby + hash(25) + age(4) + lock(3) = 32bit #32位系统 + unused(25+1) + hash(31) + age(4) + lock(3) = 64bit #64位系统 + ``` -* 虚拟机会保证一个类的 () 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 () 方法,其它线程都阻塞等待,直到活动线程执行 () 方法完毕 -* 如果在一个类的 () 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽 + * **Klass Word**:类型指针,**指向该对象的 Class 类对象的指针**,虚拟机通过这个指针来确定这个对象是哪个类的实例;在 64 位系统中,开启指针压缩(-XX:+UseCompressedOops)或者 JVM 堆的最大值小于 32G,这个指针也是 4byte,否则是 8byte(就是 **Java 中的一个引用的大小**) -特别注意:静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问 + ```ruby + |-----------------------------------------------------| + | Object Header (64 bits) | + |---------------------------|-------------------------| + | Mark Word (32 bits) | Klass Word (32 bits) | + |---------------------------|-------------------------| + ``` -```java -public class Test { - static { - //i = 0; // 给变量赋值可以正常编译通过 - System.out.print(i); // 这句编译器会提示“非法向前引用” - } - static int i = 1; -} -``` +* 数组对象:如果对象是一个数组,那在对象头中还有一块数据用于记录数组长度(12 字节) -接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 () 方法,两者不同的是: + ```ruby + |-------------------------------------------------------------------------------| + | Object Header (96 bits) | + |-----------------------|-----------------------------|-------------------------| + | Mark Word(32bits) | Klass Word(32bits) | array length(32bits) | + |-----------------------|-----------------------------|-------------------------| + ``` -* 在初始化一个接口时,并不会先初始化它的父接口,所以执行接口的 () 方法不需要先执行父接口的 () 方法 -* 在初始化一个类时,不会先初始化所实现的接口,所以接口的实现类在初始化时不会执行接口的 () 方法 -* 只有当父接口中定义的变量使用时,父接口才会初始化 +实例数据:实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的,都需要记录起来 +对齐填充:Padding 起占位符的作用。64 位系统,由于 HotSpot VM 的自动内存管理系统要求**对象起始地址必须是 8 字节的整数倍**,就是对象的大小必须是 8 字节的整数倍,而对象头部分正好是 8 字节的倍数(1 倍或者 2 倍),因此当对象实例数据部分没有对齐时,就需要通过对齐填充来补全 +32 位系统: -**** +* 一个 int 在 java 中占据 4byte,所以 Integer 的大小为: + ```java + private final int value; + ``` + ```ruby + # 需要补位4byte + 4(Mark Word) + 4(Klass Word) + 4(data) + 4(Padding) = 16byte + ``` -##### 时机 +* `int[] arr = new int[10]` -类的初始化是懒惰的,只有在首次使用时才会被装载,JVM 不会无条件地装载 Class 类型,Java 虚拟机规定,一个类或接口在初次使用前,必须要进行初始化 + ```ruby + # 由于需要8位对齐,所以最终大小为`56byte`。 + 4(Mark Word) + 4(Klass Word) + 4(length) + 4*10(10个int大小) + 4(Padding) = 56sbyte + ``` -**主动引用**:虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列情况必须对类进行初始化(加载、验证、准备都会发生): -* 当创建一个类的实例时,使用 new 关键字,或者通过反射、克隆、反序列化(前文讲述的对象的创建时机) -* 当调用类的静态方法或访问静态字段时,遇到 getstatic、putstatic、invokestatic 这三条字节码指令,如果类没有进行过初始化,则必须先触发其初始化 - * getstatic:程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池) - * putstatic:程序给类的静态变量赋值 - * invokestatic :调用一个类的静态方法 -* 使用 java.lang.reflect 包的方法对类进行反射调用时,如果类没有进行初始化,则需要先触发其初始化 -* 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化,但这条规则并**不适用于接口** -* 当虚拟机启动时,需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类 -* MethodHandle 和 VarHandle 可以看作是轻量级的反射调用机制,而要想使用这两个调用, 就必须先使用 findStaticVarHandle 来初始化要调用的类 -* 补充:当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化 -**被动引用**:所有引用类的方式都不会触发初始化,称为被动引用 +*** -* 通过子类引用父类的静态字段,不会导致子类初始化,只会触发父类的初始化 -* 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法 -* 常量(final 修饰)在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化 -* 调用 ClassLoader 类的 loadClass() 方法加载一个类,并不是对类的主动使用,不会导致类的初始化 +#### 实际大小 -*** +浅堆(Shallow Heap):**对象本身占用的内存,不包括内部引用对象的大小**,32 位系统中一个对象引用占 4 个字节,每个对象头占用 8 个字节,根据堆快照格式不同,对象的大小会同 8 字节进行对齐 +JDK7 中的 String:2个 int 值共占 8 字节,value 对象引用占用 4 字节,对象头 8 字节,对齐后占 24 字节,为 String 对象的浅堆大小,与 value 实际取值无关,无论字符串长度如何,浅堆大小始终是 24 字节 +```java +private final char value[]; +private int hash; +private int hash32; +``` -##### init +保留集(Retained Set):对象 A 的保留集指当对象 A 被垃圾回收后,可以被释放的所有的对象集合(包括 A 本身),所以对象 A 的保留集就是只能通过对象 A 被直接或间接访问到的所有对象的集合,就是仅被对象 A 所持有的对象的集合 -init 指的是实例构造器,主要作用是在类实例化过程中执行,执行内容包括成员变量初始化和代码块的执行 +深堆(Retained Heap):指对象的保留集中所有的对象的浅堆大小之和,一个对象的深堆指只能通过该对象访问到的(直接或间接)所有对象的浅堆之和,即对象被回收后,可以释放的真实空间 -实例化即调用 ()V ,虚拟机会保证这个类的构造方法的线程安全,先为实例变量分配内存空间,再执行赋默认值,然后根据源码中的顺序执行赋初值或代码块,没有成员变量初始化和代码块则不会执行 +对象的实际大小:一个对象所能触及的所有对象的浅堆大小之和,也就是通常意义上我们说的对象大小 -类实例化过程:**父类的类构造器() -> 子类的类构造器() -> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成员变量和实例代码块 -> 子类的构造函数** +下图显示了一个简单的对象引用关系图,对象 A 引用了 C 和 D,对象 B 引用了 C 和 E。那么对象 A 的浅堆大小只是 A 本身,**A 的实际大小为 A、C、D 三者之和**,A 的深堆大小为 A 与 D 之和,由于对象 C 还可以通过对象 B 访问到 C,因此 C 不在对象 A 的深堆范围内 + +内存分析工具 MAT 提供了一种叫支配树的对象图,体现了对象实例间的支配关系 -*** +基本性质: +- 对象 A 的子树(所有被对象 A 支配的对象集合)表示对象 A 的保留集(retained set),即深堆 +- 如果对象 A 支配对象 B,那么对象 A 的直接支配者也支配对象 B -#### 卸载阶段 +- 支配树的边与对象引用图的边不直接对应 -时机:执行了 System.exit() 方法,程序正常执行结束,程序在执行过程中遇到了异常或错误而异常终止,由于操作系统出现错误而导致Java虚拟机进程终止 +左图表示对象引用图,右图表示左图所对应的支配树: -卸载类即该类的 **Class 对象被 GC**,卸载类需要满足3个要求: +![](https://gitee.com/seazean/images/raw/master/Java/JVM-支配树.png) -1. 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象 -2. 该类没有在其他任何地方被引用 -3. 该类的类加载器的实例已被 GC,一般是可替换类加载器的场景,如 OSGi、JSP 的重加载等,很难达成 +比如:对象 F 与对象 D 相互引用,因为到对象 F 的所有路径必然经过对象 D,因此对象 D 是对象 F 的直接支配者 -在 JVM 生命周期类,由 JVM 自带的类加载器加载的类是不会被卸载的,自定义的类加载器加载的类是可能被卸载。因为 JVM 会始终引用启动、扩展、系统类加载器,这些类加载器始终引用它们所加载的类,这些类始终是可及的 +参考文章:https://www.yuque.com/u21195183/jvm/nkq31c -**** +*** -### 类加载器 -#### 类加载 -类加载方式: +#### 节约内存 -* 隐式加载:不直接在代码中调用 ClassLoader 的方法加载类对象 - * 创建类对象、使用类的静态域、创建子类对象、使用子类的静态域 - * 在 JVM 启动时,通过三大类加载器加载 class -* 显式加载: - * ClassLoader.loadClass(className):只加载和连接,**不会进行初始化** - * Class.forName(String name, boolean initialize, ClassLoader loader):使用 loader 进行加载和连接,根据参数 initialize 决定是否初始化 +* 尽量使用基本数据类型 -类的唯一性: +* 满足容量前提下,尽量用小字段 -* 在 JVM 中表示两个 class 对象判断为同一个类存在的两个必要条件: - - 类的完整类名必须一致,包括包名 - - 加载这个类的 ClassLoader(指 ClassLoader 实例对象)必须相同 -* 这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true +* 尽量用数组,少用集合,数组中是可以使用基本类型的,但是集合中只能放包装类型,如果需要使用集合,推荐比较节约内存的集合工具:fastutil -命名空间: + 一个 ArrayList 集合,如果里面放了 10 个数字,占用多少内存: -- 每个类加载器都有自己的命名空间,命名空间由该加载器及所有的父加载器所加载的类组成 -- 在同一命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类 + ```java + private transient Object[] elementData; + private int size; + ``` -基本特征: + Mark Word 占 4byte,Klass Word 占 4byte,一个 int 字段占 4byte,elementData 数组占 12byte,数组中 10 个 Integer 对象占 10×16,所以整个集合空间大小为 184byte(深堆) -* **可见性**,子类加载器可以访问父加载器加载的类型,但是反过来是不允许的 -* **单一性**,由于父加载器的类型对于子加载器是可见的,所以父加载器中加载过的类型,不会在子加载器中重复加载 +* 时间用 long/int 表示,不用 Date 或者 String @@ -12174,110 +12369,90 @@ init 指的是实例构造器,主要作用是在类实例化过程中执行, -#### 加载器 +#### 对象访问 -类加载器是 Java 的核心组件,用于加载字节码到 JVM 内存,得到 Class 类的对象 +JVM 是通过**栈帧中的对象引用**访问到其内部的对象实例: -从 Java 虚拟机规范来讲,只存在以下两种不同的类加载器: +* 句柄访问:Java 堆中会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息 + + 优点:reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集)时只会改变句柄中的实例数据指针,而 reference 本身不需要被修改 -- 启动类加载器(Bootstrap ClassLoader):使用 C++ 实现,是虚拟机自身的一部分 -- 自定义类加载器(User-Defined ClassLoader):Java 虚拟机规范**将所有派生于抽象类 ClassLoader 的类加载器都划分为自定义类加载器**,使用 Java 语言实现,独立于虚拟机 + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-对象访问-句柄访问.png) -从 Java 开发人员的角度看: +* 直接指针(HotSpot 采用):Java 堆对象的布局必须考虑如何放置访问类型数据的相关信息,reference 中直接存储的对象地址 + + 优点:速度更快,**节省了一次指针定位的时间开销** -* 启动类加载器(Bootstrap ClassLoader): - * 处于安全考虑,Bootstrap 启动类加载器只加载包名为 java、javax、sun 等开头的类 - * 类加载器负责加载在 `JAVA_HOME/jre/lib `或 `sun.boot.class.path` 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的类,并且是虚拟机识别的类库加载到虚拟机内存中 - * 仅按照文件名识别,如 rt.jar 名字不符合的类库即使放在 lib 目录中也不会被加载 - * 启动类加载器无法被 Java 程序直接引用,编写自定义类加载器时,如果要把加载请求委派给启动类加载器,直接使用 null 代替 -* 扩展类加载器(Extension ClassLoader): - * 由 ExtClassLoader (sun.misc.Launcher$ExtClassLoader) 实现,上级为 Bootstrap,显示为 null - * 将 `JAVA_HOME/jre/lib/ext `或者被 `java.ext.dir` 系统变量所指定路径中的所有类库加载到内存中 - * 开发者可以使用扩展类加载器,创建的 JAR 放在此目录下,会由扩展类加载器自动加载 -* 应用程序类加载器(Application ClassLoader): - * 由 AppClassLoader(sun.misc.Launcher$AppClassLoader) 实现,上级为 Extension - * 负责加载环境变量 classpath 或系统属性 `java.class.path` 指定路径下的类库 - * 这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此称为系统类加载器 - * 可以直接使用这个类加载器,如果应用程序中没有自定义类加载器,这个就是程序中默认的类加载器 -* 自定义类加载器:由开发人员自定义的类加载器,上级是 Application + 缺点:对象被移动时(如进行GC后的内存重新排列),对象的 reference 也需要同步更新 + + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-对象访问-直接指针.png) -```java -public static void main(String[] args) { - //获取系统类加载器 - ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); - System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2 - //获取其上层 扩展类加载器 - ClassLoader extClassLoader = systemClassLoader.getParent(); - System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@610455d6 - //获取其上层 获取不到引导类加载器 - ClassLoader bootStrapClassLoader = extClassLoader.getParent(); - System.out.println(bootStrapClassLoader);//null +参考文章:https://www.cnblogs.com/afraidToForget/p/12584866.html - //对于用户自定义类来说:使用系统类加载器进行加载 - ClassLoader classLoader = ClassLoaderTest.class.getClassLoader(); - System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2 - //String 类使用引导类加载器进行加载的 --> java核心类库都是使用启动类加载器加载的 - ClassLoader classLoader1 = String.class.getClassLoader(); - System.out.println(classLoader1);//null -} -``` - -补充两个类加载器: +*** -* SecureClassLoader 扩展了 ClassLoader,新增了几个与使用相关的代码源和权限定义类验证(对 class 源码的访问权限)的方法,一般不会直接跟这个类打交道,更多是与它的子类 URLClassLoader 有所关联 -* ClassLoader 是一个抽象类,很多方法是空的没有实现,而 URLClassLoader 这个实现类为这些方法提供了具体的实现,并新增了 URLClassPath 类协助取得 Class 字节流等功能。在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承 URLClassLoader 类,这样就可以避免去编写 findClass() 方法及其获取字节码流的方式,使自定义类加载器编写更加简洁 +### 对象创建 -*** +#### 生命周期 +在 Java 中,对象的生命周期包括以下几个阶段: +1. 创建阶段 (Created): +2. 应用阶段 (In Use):对象至少被一个强引用持有着 +3. 不可见阶段 (Invisible):程序的执行已经超出了该对象的作用域,不再持有该对象的任何强引用 +4. 不可达阶段 (Unreachable):该对象不再被任何强引用所持有,包括 GC Root 的强引用 +5. 收集阶段 (Collected):垃圾回收器对该对象的内存空间重新分配做好准备,该对象如果重写了 finalize() 方法,则会去执行该方法 +6. 终结阶段 (Finalized):等待垃圾回收器对该对象空间进行回收,当对象执行完 finalize()方 法后仍然处于不可达状态时进入该阶段 +7. 对象空间重分配阶段 (De-allocated):垃圾回收器对该对象的所占用的内存空间进行回收或者再分配 -#### 常用API -ClassLoader 类,是一个抽象类,其后所有的类加载器都继承自 ClassLoader(不包括启动类加载器) -获取 ClassLoader 的途径: +参考文章:https://blog.csdn.net/sodino/article/details/38387049 -* 获取当前类的 ClassLoader:`clazz.getClassLoader()` -* 获取当前线程上下文的 ClassLoader:`Thread.currentThread.getContextClassLoader()` -* 获取系统的 ClassLoader:`ClassLoader.getSystemClassLoader()` -* 获取调用者的 ClassLoader:`DriverManager.getCallerClassLoader()` -ClassLoader 类常用方法: -* `getParent()`:返回该类加载器的超类加载器 -* `loadclass(String name)`:加载名为 name 的类,返回结果为 Class 类的实例,**该方法就是双亲委派模式** -* `findclass(String name)`:查找二进制名称为 name 的类,返回结果为 Class 类的实例,该方法会在检查完父类加载器之后被 loadClass() 方法调用 -* `findLoadedClass(String name)`:查找名称为 name 的已经被加载过的类,final 修饰无法重写 -* `defineClass(String name, byte[] b, int off, int len)`:将**字节流**解析成 JVM 能够识别的类对象 -* `resolveclass(Class c)`:链接指定的 Java 类,可以使类的 Class 对象创建完成的同时也被解析 -* `InputStream getResourceAsStream(String name)`:指定资源名称获取输入流 +*** -*** +#### 创建时机 +类在第一次实例化加载一次,后续实例化不再加载,引用第一次加载的类 +Java 对象创建时机: -#### 加载模型 +1. 使用 new 关键字创建对象:由执行类实例创建表达式而引起的对象创建 -##### 加载机制 +2. 使用 Class 类的 newInstance 方法(反射机制) -在 JVM 中,对于类加载模型提供了三种,分别为全盘加载、双亲委派、缓存机制 +3. 使用 Constructor 类的 newInstance 方法(反射机制) -- **全盘加载:**当一个类加载器负责加载某个 Class 时,该 Class 所依赖和引用的其他 Class 也将由该类加载器负责载入,除非显示指定使用另外一个类加载器来载入 + ```java + public class Student { + private int id; + public Student(Integer id) { + this.id = id; + } + public static void main(String[] args) throws Exception { + Constructor c = Student.class.getConstructor(Integer.class); + Student stu = c.newInstance(123); + } + } + ``` -- **双亲委派:**先让父类加载器加载该 Class,在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。简单来说就是,某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,**依次递归**,如果父加载器可以完成类加载任务,就成功返回;只有当父加载器无法完成此加载任务时,才自己去加载 + 使用 newInstance 方法的这两种方式创建对象使用的就是 Java 的反射机制,事实上 Class 的 newInstance 方法内部调用的也是 Constructor 的 newInstance 方法 -- **缓存机制:**会保证所有加载过的 Class 都会被缓存,当程序中需要使用某个 Class 时,类加载器先从缓存区中搜寻该 Class,只有当缓存区中不存在该 Class 对象时,系统才会读取该类对应的二进制数据,并将其转换成 Class 对象存入缓冲区中 - - 这就是修改了 Class 后,必须重新启动 JVM,程序所做的修改才会生效的原因 +4. 使用 Clone 方法创建对象:用 clone 方法创建对象的过程中并不会调用任何构造函数,要想使用 clone 方法,我们就必须先实现 Cloneable 接口并实现其定义的 clone 方法 +5. 使用(反)序列化机制创建对象:当反序列化一个对象时,JVM 会创建一个**单独的对象**,在此过程中,JVM 并不会调用任何构造函数,为了反序列化一个对象,需要让类实现 Serializable 接口 +从 Java 虚拟机层面看,除了使用 new 关键字创建对象的方式外,其他方式全部都是通过转变为 invokevirtual 指令直接创建对象的 @@ -12285,343 +12460,310 @@ ClassLoader 类常用方法: -##### 双亲委派 - -双亲委派模型(Parents Delegation Model):该模型要求除了顶层的启动类加载器外,其它类加载器都要有父类加载器,这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance) +#### 创建过程 -工作过程:一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载 +创建对象的过程: -双亲委派机制的优点: +1. 判断对象对应的类是否加载、链接、初始化 -* 可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证全局唯一性 +2. 为对象分配内存:指针碰撞、空闲链表。当一个对象被创建时,虚拟机就会为其分配内存来存放对象的实例变量及其从父类继承过来的实例变量,即使从**隐藏变量**也会被分配空间(继承部分解释了为什么会隐藏) -* Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一 +3. 处理并发安全问题: -* 保护程序安全,防止类库的核心 API 被随意篡改 + * 采用 CAS 配上自旋保证更新的原子性 + * 每个线程预先分配一块 TLAB - 例如:在工程中新建 java.lang 包,接着在该包下新建 String 类,并定义 main 函数 +4. 初始化分配的空间:虚拟机将分配到的内存空间都初始化为零值(不包括对象头),保证对象实例字段在不赋值时可以直接使用,程序能访问到这些字段的数据类型所对应的零值 - ```java - public class String { - public static void main(String[] args) { - System.out.println("demo info"); - } - } - ``` - - 此时执行 main 函数,会出现异常,在类 java.lang.String 中找不到 main 方法,防止恶意篡改核心 API 库。出现该信息是因为双亲委派的机制,java.lang.String 的在启动类加载器(Bootstrap)得到加载,启动类加载器优先级更高,在核心 jre 库中有其相同名字的类文件,但该类中并没有 main 方法 +5. 设置对象的对象头:将对象的所属类(类的元数据信息)、对象的 HashCode、对象的 GC 信息、锁信息等数据存储在对象头中 -双亲委派机制的缺点:检查类是否加载的委托过程是单向的,这个方式虽然从结构上看比较清晰,使各个 ClassLoader 的职责非常明确,但**顶层的 ClassLoader 无法访问底层的 ClassLoader 所加载的类**(可见性) +6. 执行 init 方法进行实例化:实例变量初始化、实例代码块初始化 、构造函数初始化 - + * 实例变量初始化与实例代码块初始化: + 对实例变量直接赋值或者使用实例代码块赋值,**编译器会将其中的代码放到类的构造函数中去**,并且这些代码会被放在对超类构造函数的调用语句之后 (Java 要求构造函数的第一条语句必须是超类构造函数的调用语句),构造函数本身的代码之前 + * 构造函数初始化: -*** + **Java 要求在实例化类之前,必须先实例化其超类,以保证所创建实例的完整性**,在准备实例化一个类的对象前,首先准备实例化该类的父类,如果该类的父类还有父类,那么准备实例化该类的父类的父类,依次递归直到递归到 Object 类。然后从 Object 类依次对以下各类进行实例化,初始化父类中的变量和执行构造函数 -##### 源码分析 +*** -```java -protected Class loadClass(String name, boolean resolve) - throws ClassNotFoundException { - synchronized (getClassLoadingLock(name)) { - // 调用当前类加载器的 findLoadedClass(name),检查当前类加载器是否已加载过指定 name 的类 - Class c = findLoadedClass(name); - - // 当前类加载器如果没有加载过 - if (c == null) { - long t0 = System.nanoTime(); - try { - // 判断当前类加载器是否有父类加载器 - if (parent != null) { - // 如果当前类加载器有父类加载器,则调用父类加载器的 loadClass(name,false) -          // 父类加载器的 loadClass 方法,又会检查自己是否已经加载过 - c = parent.loadClass(name, false); - } else { - // 当前类加载器没有父类加载器,说明当前类加载器是 BootStrapClassLoader -           // 则调用 BootStrap ClassLoader 的方法加载类 - c = findBootstrapClassOrNull(name); - } - } catch (ClassNotFoundException e) { } - if (c == null) { - // 如果调用父类的类加载器无法对类进行加载,则用自己的 findClass() 方法进行加载 - // 可以自定义 findClass() 方法 - long t1 = System.nanoTime(); - c = findClass(name); - // this is the defining class loader; record the stats - sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); - sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); - sun.misc.PerfCounter.getFindClasses().increment(); - } - } - if (resolve) { - // 链接指定的 Java 类,可以使类的 Class 对象创建完成的同时也被解析 - resolveClass(c); - } - return c; - } -} -``` +#### 承上启下 +1. 一个实例变量在对象初始化的过程中会被赋值几次? + JVM 在为一个对象分配完内存之后,会给每一个实例变量赋予默认值,这个实例变量被第一次赋值 + 在声明实例变量的同时对其进行了赋值操作,那么这个实例变量就被第二次赋值 + 在实例代码块中又对变量做了初始化操作,那么这个实例变量就被第三次赋值 + 在构造函数中也对变量做了初始化操作,那么这个实例变量就被第四次赋值 + 在 Java 的对象初始化过程中,一个实例变量最多可以被初始化 4 次 -**** +2. 类的初始化过程与类的实例化过程的异同? + 类的初始化是指类加载过程中的初始化阶段对类变量按照代码进行赋值的过程 + 类的实例化是指在类完全加载到内存中后创建对象的过程(类的实例化触发了类的初始化,先初始化才能实例化) +3. 假如一个类还未加载到内存中,那么在创建一个该类的实例时,具体过程是怎样的?(**经典案例**) -##### 破坏委派 + ```java + public class StaticTest { + public static void main(String[] args) { + staticFunction();//调用静态方法,触发初始化 + } + + static StaticTest st = new StaticTest(); + + static { //静态代码块 + System.out.println("1"); + } + + { // 实例代码块 + System.out.println("2"); + } + + StaticTest() { // 实例构造器 + System.out.println("3"); + System.out.println("a=" + a + ",b=" + b); + } + + public static void staticFunction() { // 静态方法 + System.out.println("4"); + } + + int a = 110; // 实例变量 + static int b = 112; // 静态变量 + }/* Output: + 2 + 3 + a=110,b=0 + 1 + 4 + *///:~ + ``` -双亲委派模型并不是一个具有强制性约束的模型,而是 Java 设计者推荐给开发者的类加载器实现方式 + `static StaticTest st = new StaticTest();`: -破坏双亲委派模型的方式: + * 实例实例化不一定要在类初始化结束之后才开始 -* 自定义 ClassLoader + * 在同一个类加载器下,一个类型只会被初始化一次。所以一旦开始初始化一个类,无论是否完成后续都不会再重新触发该类型的初始化阶段了(只考虑在同一个类加载器下的情形)。因此在实例化上述程序中的 st 变量时,**实际上是把实例化嵌入到了静态初始化流程中,并且在上面的程序中,嵌入到了静态初始化的起始位置**,这就导致了实例初始化完全发生在静态初始化之前,这也是导致 a 为 110,b 为 0 的原因 - * 如果不想破坏双亲委派模型,只需要重写 findClass 方法 - * 如果想要去破坏双亲委派模型,需要去**重写 loadClass **方法 + 代码等价于: -* 引入线程**上下文类加载器** + ```java + public class StaticTest { + (){ + a = 110; // 实例变量 + System.out.println("2"); // 实例代码块 + System.out.println("3"); // 实例构造器中代码的执行 + System.out.println("a=" + a + ",b=" + b); // 实例构造器中代码的执行 + 类变量st被初始化 + System.out.println("1"); //静态代码块 + 类变量b被初始化为112 + } + } + ``` - Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI 等。这些 SPI 接口由 Java 核心库来提供,而 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径 classpath 里,SPI 接口中的代码需要加载具体的实现类: + - * SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的 - * SPI 的实现类是由系统类加载器来加载的,引导类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader 无法委派 AppClassLoader 来加载类 - JDK 开发人员引入了线程上下文类加载器(Thread Context ClassLoader),这种类加载器可以通过 Thread 类的 setContextClassLoader 方法进行设置线程上下文类加载器,在执行线程中抛弃双亲委派加载模式,使程序可以逆向使用类加载器,使 Bootstrap 加载器拿到了 Application 加载器加载的类,破坏了双亲委派模型 - -* 实现程序的动态性,如代码热替换(Hot Swap)、模块热部署(Hot Deployment) - IBM 公司主导的 JSR一291(OSGiR4.2)实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(OSGi 中称为 Bundle)都有一个自己的类加载器,当更换一个 Bundle 时,就把 Bundle 连同类加载器一起换掉以实现代码的热替换,在 OSGi 环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构 +*** - 当收到类加载请求时,OSGi 将按照下面的顺序进行类搜索: - 1. 将以 java.* 开头的类,委派给父类加载器加载 - 2. 否则,将委派列表名单内的类,委派给父类加载器加载 - 3. 否则,将 Import 列表中的类,委派给 Export 这个类的 Bundle 的类加载器加载 - 4. 否则,查找当前 Bundle 的 ClassPath,使用自己的类加载器加载 - 5. 否则,查找类是否在自己的 Fragment Bundle 中,如果在就委派给 Fragment Bundle 类加载器加载 - 6. 否则,查找 Dynamic Import 列表的 Bundle,委派给对应 Bundle 的类加载器加载 - 7. 否则,类查找失败 - - 热替换是指在程序的运行过程中,不停止服务,只通过替换程序文件来修改程序的行为,**热替换的关键需求在于服务不能中断**,修改必须立即表现正在运行的系统之中 - +### 加载过程 +#### 生命周期 +类是在运行期间**第一次使用时动态加载**的(不使用不加载),而不是一次性加载所有类,因为一次性加载会占用很多的内存,加载的类信息存放于一块成为方法区的内存空间 -*** +![](https://gitee.com/seazean/images/raw/master/Java/JVM-类的生命周期.png) +包括 7 个阶段: +* 加载(Loading) +* 链接:验证(Verification)、准备(Preparation)、解析(Resolution) +* 初始化(Initialization) +* 使用(Using) +* 卸载(Unloading) -#### 沙箱机制 -沙箱机制(Sandbox):将 Java 代码限定在虚拟机特定的运行范围中,并且严格限制代码对本地系统资源访问,来保证对代码的有效隔离,防止对本地系统造成破坏 -沙箱**限制系统资源访问**,包括 CPU、内存、文件系统、网络,不同级别的沙箱对资源访问的限制也不一样 +*** -* JDK1.0:Java 中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码被看作是不受信的。对于授信的本地代码,可以访问一切本地资源,而对于非授信的远程代码不可以访问本地资源,其实依赖于沙箱机制。如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现 -* JDK1.1:针对安全机制做了改进,增加了安全策略。允许用户指定代码对本地资源的访问权限 -* JDK1.2:改进了安全机制,增加了代码签名,不论本地代码或是远程代码都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制 -* JDK1.6:当前最新的安全机制,引入了域(Domain)的概念。虚拟机会把所有代码加载到不同的系统域和应用域,不同的保护域对应不一样的权限。系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问 - +#### 加载阶段 +加载是类加载的其中一个阶段,注意不要混淆 -*** +加载过程完成以下三件事: +- 通过类的完全限定名称获取定义该类的二进制字节流(二进制字节码) +- 将该字节流表示的**静态存储结构转换为方法区的运行时存储结构**(Java 类模型) +- 在内存中生成一个代表该类的 Class 对象,作为该类在方法区中的各种数据的访问入口 +其中二进制字节流可以从以下方式中获取: -#### 自定义 +- 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础 +- 从网络中获取,最典型的应用是 Applet +- 由其他文件生成,例如由 JSP 文件生成对应的 Class 类 +- 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 生成字节码 -对于自定义类加载器的实现,只需要继承 ClassLoader 类,覆写 findClass 方法即可 +将字节码文件加载至方法区后,会**在堆中**创建一个 java.lang.Class 对象,用来引用位于方法区内的数据结构,该 Class 对象是在加载类的过程中创建的,每个类都对应有一个 Class 类型的对象 -作用:隔离加载类、修改类加载的方式、拓展加载源、防止源码泄漏 +方法区内部采用 C++ 的 instanceKlass 描述 Java 类的数据结构: -```java -//自定义类加载器,读取指定的类路径classPath下的class文件 -public class MyClassLoader extends ClassLoader{ - private String classPath; +* `_java_mirror` 即 java 的类镜像,例如对 String 来说就是 String.class,作用是把 class 暴露给 java 使用 +* `_super` 即父类、`_fields` 即成员变量、`_methods` 即方法、`_constants` 即常量池、`_class_loader` 即类加载器、`_vtable` **虚方法表**、`_itable` 接口方法表 - public MyClassLoader(String classPath) { - this.classPath = classPath; - } - - public MyClassLoader(ClassLoader parent, String byteCodePath) { - super(parent); - this.classPath = classPath; - } +加载过程: - @Override - protected Class findClass(String name) throws ClassNotFoundException { - BufferedInputStream bis = null; - ByteArrayOutputStream baos = null; - try { - // 获取字节码文件的完整路径 - String fileName = classPath + className + ".class"; - // 获取一个输入流 - bis = new BufferedInputStream(new FileInputStream(fileName)); - // 获取一个输出流 - baos = new ByteArrayOutputStream(); - // 具体读入数据并写出的过程 - int len; - byte[] data = new byte[1024]; - while ((len = bis.read(data)) != -1) { - baos.write(data, 0, len); - } - // 获取内存中的完整的字节数组的数据 - byte[] byteCodes = baos.toByteArray(); - // 调用 defineClass(),将字节数组的数据转换为 Class 的实例。 - Class clazz = defineClass(null, byteCodes, 0, byteCodes.length); - return clazz; - } catch (IOException e) { - e.printStackTrace(); - } finally { - try { - if (baos != null) - baos.close(); - } catch (IOException e) { - e.printStackTrace(); - } - try { - if (bis != null) - bis.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - return null; - } -} -``` +* 如果这个类还有父类没有加载,先加载父类 +* 加载和链接可能是交替运行的 +* Class 对象和 _java_mirror 相互持有对方的地址,堆中对象通过 instanceKlass 和元空间进行交互 -```java -public static void main(String[] args) { - MyClassLoader loader = new MyClassLoader("D:\Workspace\Project\JVM_study\src\java1\"); + - try { - Class clazz = loader.loadClass("Demo1"); - System.out.println("加载此类的类的加载器为:" + clazz.getClassLoader().getClass().getName());//MyClassLoader +创建数组类有些特殊,因为数组类本身并不是由类加载器负责创建,而是由 JVM 在运行时根据需要而直接创建的,但数组的元素类型仍然需要依靠类加载器去创建,创建数组类的过程: - System.out.println("加载当前类的类的加载器的父类加载器为:" + clazz.getClassLoader().getParent().getClass().getName());//sun.misc.Launcher$AppClassLoader - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } -} -``` +- 如果数组的元素类型是引用类型,那么遵循定义的加载过程递归加载和创建数组的元素类型 +- JVM 使用指定的元素类型和数组维度来创建新的数组类 +- 基本数据类型由启动类加载器加载 -**** +*** -#### JDK9 +#### 链接阶段 -为了保证兼容性,JDK9 没有改变三层类加载器架构和双亲委派模型,但为了模块化系统的顺利运行做了一些变动: +##### 验证 -* 扩展机制被移除,扩展类加载器由于**向后兼容性**的原因被保留,不过被重命名为平台类加载器(platform classloader),可以通过 ClassLoader 的新方法 getPlatformClassLoader() 来获取 +确保 Class 文件的字节流中包含的信息是否符合 JVM 规范,保证被加载类的正确性,不会危害虚拟机自身的安全 -* JDK9 基于模块化进行构建(原来的 rt.jar 和 tools.jar 被拆分成数个 JMOD 文件),其中 Java 类库就满足了可扩展的需求,那就无须再保留 `\lib\ext` 目录,此前使用这个目录或者 `java.ext.dirs` 系统变量来扩展 JDK 功能的机制就不需要再存在 +主要包括**四种验证**: -* 启动类加载器、平台类加载器、应用程序类加载器全都继承于 `jdk.internal.loader.BuiltinClassLoader` +* 文件格式验证 - +* 语义检查,但凡在语义上不符合规范的,虚拟机不会给予验证通过 + * 是否所有的类都有父类的存在(除了 Object 外,其他类都应该有父类) + * 是否一些被定义为 final 的方法或者类被重写或继承了 -*** + * 非抽象类是否实现了所有抽象方法或者接口方法 + * 是否存在不兼容的方法 +* 字节码验证,试图通过对字节码流的分析,判断字节码是否可以被正确地执行 + * 在字节码的执行过程中,是否会跳转到一条不存在的指令 + * 函数的调用是否传递了正确类型的参数 + * 变量的赋值是不是给了正确的数据类型 + * 栈映射帧(StackMapTable)在这个阶段用于检测在特定的字节码处,其局部变量表和操作数栈是否有着正确的数据类型 +* 符号引用验证,Class 文件在其常量池会通过字符串记录将要使用的其他类或者方法 -## 运行机制 -### 执行过程 - Java 文件编译执行的过程: +*** -![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java文件编译执行的过程.png) -- 类加载器:用于装载字节码文件(.class文件) -- 运行时数据区:用于分配存储空间 -- 执行引擎:执行字节码文件或本地方法 -- 垃圾回收器:用于对 JVM 中的垃圾内容进行回收 +##### 准备 +准备阶段为**静态变量分配内存并设置初始值**,使用的是方法区的内存: -**** +* 类变量也叫静态变量,就是是被 static 修饰的变量 +* 实例变量也叫对象变量,即没加 static 的变量 +说明:实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次 +类变量初始化: -### 字节码 +* static 变量分配空间和赋值是两个步骤:**分配空间在准备阶段完成,赋值在初始化阶段完成** +* 如果 static 变量是 final 的基本类型以及字符串常量,那么编译阶段值(方法区)就确定了,准备阶段会显式初始化 +* 如果 static 变量是 final 的,但属于引用类型或者构造器方法的字符串,赋值在初始化阶段完成 -#### 跨平台性 +实例: -Java 语言:跨平台的语言(write once ,run anywhere) +* 初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123: -* 当 Java 源代码成功编译成字节码后,在不同的平台上面运行**无须再次编译** -* 让一个 Java 程序正确地运行在 JVM 中,Java 源码就必须要被编译为符合 JVM 规范的字节码 + ```java + public static int value = 123; + ``` -编译过程中的编译器: +* 常量 value 被初始化为 123 而不是 0: -* 前端编译器: Sun 的全量式编译器 javac、 Eclipse 的增量式编译器 ECJ,把源代码编译为字节码文件 .class + ```java + public static final int value = 123; + ``` - * IntelliJ IDEA 使用 javac 编译器 - * Eclipse 中,当开发人员编写完代码后保存时,ECJ 编译器就会把未编译部分的源码逐行进行编译,而非每次都全量编译,因此 ECJ 的编译效率会比 javac 更加迅速和高效 - * 前端编译器并不会直接涉及编译优化等方面的技术,具体优化细节移交给 HotSpot 的 JIT 编译器负责 +* Java 并不支持 boolean 类型,对于 boolean 类型,内部实现是 int,由于 int 的默认值是0,故 boolean 的默认值就是 false -* 后端运行期编译器:HotSpot VM 的 C1、C2 编译器,也就是 JIT 编译器,Graal 编译器 - * JIT编译器:执行引擎部分详解 - * Graal 编译器:JDK10 HotSpot 加入的一个全新的即时编译器,编译效果短短几年时间就追平了 C2 -* 静态提前编译器:AOT (Ahead Of Time Compiler)编译器,直接把源代码编译成本地机器代码, +*** - * JDK 9 引入,是与即时编译相对立的一个概念,即时编译指的是在程序的运行过程中将字节码转换为机器码,AOT 是程序运行之前便将字节码转换为机器码 - * 优点:JVM 加载已经预编译成二进制库,可以直接执行,不必等待即时编译器的预热,减少 Java 应用第一次运行慢的现象 - * 缺点: - * 破坏了 Java **一次编译,到处运行**,必须为每个不同硬件编译对应的发行包 - * 降低了 Java 链接过程的动态性,加载的代码在编译期就必须全部已知 +##### 解析 +将常量池中类、接口、字段、方法的**符号引用替换为直接引用**(内存地址)的过程: +* 符号引用:一组符号来描述目标,可以是任何字面量,属于编译原理方面的概念,如:包括类和接口的全限名、字段的名称和描述符、方法的名称和**方法描述符** +* 直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄,如果有了直接引用,那说明引用的目标必定已经存在于内存之中 +例如:在 `com.demo.Solution` 类中引用了 `com.test.Quest`,把 `com.test.Quest` 作为符号引用存进类常量池,在类加载完后,**用这个符号引用去方法区找这个类的内存地址** -*** +解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等 +* 在类加载阶段解析的是非虚方法,静态绑定 +* 也可以在初始化阶段之后再开始解析,这是为了支持 Java 的**动态绑定** +* 通过解析操作,符号引用就可以转变为目标方法在类的虚方法表中的位置,从而使得方法被成功调用 +```java +public class Load2 { + public static void main(String[] args) throws Exception{ + ClassLoader classloader = Load2.class.getClassLoader(); + // cloadClass 加载类方法不会导致类的解析和初始化,也不会加载D + Class c = classloader.loadClass("cn.jvm.t3.load.C"); + + // new C();会导致类的解析和初始化,从而解析初始化D + System.in.read(); + } +} +class C { + D d = new D(); +} +class D { +} +``` -#### 语言发展 -机器码:各种用二进制编码方式表示的指令,与 CPU 紧密相关,所以不同种类的 CPU 对应的机器指令不同 -指令:指令就是把机器码中特定的 0 和 1 序列,简化成对应的指令,例如 mov,inc 等,可读性稍好,但是不同的硬件平台的同一种指令(比如 mov),对应的机器码也可能不同 +**** -指令集:不同的硬件平台支持的指令是有区别的,每个平台所支持的指令,称之为对应平台的指令集 -- x86 指令集,对应的是 x86 架构的平台 -- ARM 指令集,对应的是 ARM 架构的平台 -汇编语言:用助记符代替机器指令的操作码,用地址符号或标号代替指令或操作数的地址 +#### 初始化 -* 在不同的硬件平台,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令 -* 计算机只认识指令码,汇编语言编写的程序也必须翻译成机器指令码,计算机才能识别和执行 +##### 介绍 -高级语言:为了使计算机用户编程序更容易些,后来就出现了各种高级计算机语言 +初始化阶段才真正开始执行类中定义的 Java 程序代码,在准备阶段,类变量已经赋过一次系统要求的初始值;在初始化阶段,通过程序制定的计划去初始化类变量和其它资源,执行 -字节码:是一种中间状态(中间码)的二进制代码,比机器码更抽象,需要直译器转译后才能成为机器码 +在编译生成 class 文件时,编译器会产生两个方法加于 class 文件中,一个是类的初始化方法 clinit,另一个是实例的初始化方法 init -* 字节码为了实现特定软件运行和软件环境,与硬件环境无关 -* 通过编译器和虚拟机器实现,编译器将源码编译成字节码,虚拟机器将字节码转译为可以直接执行的指令 +类构造器 () 与实例构造器 () 不同,它不需要程序员进行显式调用,在一个类的生命周期中,类构造器最多被虚拟机**调用一次**,而实例构造器则会被虚拟机调用多次,只要程序员创建对象 - +类在第一次实例化加载一次,把 class 读入内存,后续实例化不再加载,引用第一次加载的类 @@ -12629,108 +12771,83 @@ Java 语言:跨平台的语言(write once ,run anywhere) +##### clinit +():类构造器,由编译器自动收集类中**所有类变量的赋值动作和静态语句块**中的语句合并产生的 -#### 类结构 - -##### 文件结构 +作用:是在类加载过程中的初始化阶段进行静态变量初始化和执行静态代码块 -字节码是一种二进制的类文件,是编译之后供虚拟机解释执行的二进制字节码文件,**一个 class 文件对应一个 public 类型的类或接口** +* 如果类中没有静态变量或静态代码块,那么 clinit 方法将不会被生成 +* clinit 方法只执行一次,在执行 clinit 方法时,必须先执行父类的clinit方法 +* static 变量的赋值操作和静态代码块的合并顺序由源文件中出现的顺序决定 +* static 不加 final 的变量都在初始化环节赋值 -字节码内容是 **JVM 的字节码指令**,不是机器码,C、C++ 经由编译器直接生成机器码,所以执行效率比 Java 高 +**线程安全**问题: -JVM 官方文档:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html +* 虚拟机会保证一个类的 () 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 () 方法,其它线程都阻塞等待,直到活动线程执行 () 方法完毕 +* 如果在一个类的 () 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽 -根据 JVM 规范,类文件结构如下: +特别注意:静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问 ```java -ClassFile { - u4 magic; - u2 minor_version; - u2 major_version; - u2 constant_pool_count; - cp_info constant_pool[constant_pool_count-1]; - u2 access_flags; - u2 this_class; - u2 super_class; - u2 interfaces_count; - u2 interfaces[interfaces_count]; - u2 fields_count; - field_info fields[fields_count]; - u2 methods_count; - method_info methods[methods_count]; - u2 attributes_count; - attribute_info attributes[attributes_count]; +public class Test { + static { + //i = 0; // 给变量赋值可以正常编译通过 + System.out.print(i); // 这句编译器会提示“非法向前引用” + } + static int i = 1; } ``` -| 类型 | 名称 | 说明 | 长度 | 数量 | -| -------------- | ------------------- | -------------------- | ------- | --------------------- | -| u4 | magic | 魔数,识别类文件格式 | 4个字节 | 1 | -| u2 | minor_version | 副版本号(小版本) | 2个字节 | 1 | -| u2 | major_version | 主版本号(大版本) | 2个字节 | 1 | -| u2 | constant_pool_count | 常量池计数器 | 2个字节 | 1 | -| cp_info | constant_pool | 常量池表 | n个字节 | constant_pool_count-1 | -| u2 | access_flags | 访问标识 | 2个字节 | 1 | -| u2 | this_class | 类索引 | 2个字节 | 1 | -| u2 | super_class | 父类索引 | 2个字节 | 1 | -| u2 | interfaces_count | 接口计数 | 2个字节 | 1 | -| u2 | interfaces | 接口索引集合 | 2个字节 | interfaces_count | -| u2 | fields_count | 字段计数器 | 2个字节 | 1 | -| field_info | fields | 字段表 | n个字节 | fields_count | -| u2 | methods_count | 方法计数器 | 2个字节 | 1 | -| method_info | methods | 方法表 | n个字节 | methods_count | -| u2 | attributes_count | 属性计数器 | 2个字节 | 1 | -| attribute_info | attributes | 属性表 | n个字节 | attributes_count | +接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 () 方法,两者不同的是: -Class 文件格式采用一种类似于 C 语言结构体的方式进行数据存储,这种结构中只有两种数据类型:无符号数和表 +* 在初始化一个接口时,并不会先初始化它的父接口,所以执行接口的 () 方法不需要先执行父接口的 () 方法 +* 在初始化一个类时,不会先初始化所实现的接口,所以接口的实现类在初始化时不会执行接口的 () 方法 +* 只有当父接口中定义的变量使用时,父接口才会初始化 -* 无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串 -* 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,表都以 `_info` 结尾,用于描述有层次关系的数据,整个 Class 文件本质上就是一张表,由于表没有固定长度,所以通常会在其前面加上个数说明 -获取方式: -* HelloWorld.java 执行 `javac -parameters -d . HellowWorld.java`指令 -* 写入文件指令 `javap -v xxx.class >xxx.txt` -* IDEA 插件 jclasslib +**** -*** +##### 时机 +类的初始化是懒惰的,只有在首次使用时才会被装载,JVM 不会无条件地装载 Class 类型,Java 虚拟机规定,一个类或接口在初次使用前,必须要进行初始化 +**主动引用**:虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列情况必须对类进行初始化(加载、验证、准备都会发生): -##### 魔数版本 +* 当创建一个类的实例时,使用 new 关键字,或者通过反射、克隆、反序列化(前文讲述的对象的创建时机) +* 当调用类的静态方法或访问静态字段时,遇到 getstatic、putstatic、invokestatic 这三条字节码指令,如果类没有进行过初始化,则必须先触发其初始化 + * getstatic:程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池) + * putstatic:程序给类的静态变量赋值 + * invokestatic :调用一个类的静态方法 +* 使用 java.lang.reflect 包的方法对类进行反射调用时,如果类没有进行初始化,则需要先触发其初始化 +* 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化,但这条规则并**不适用于接口** +* 当虚拟机启动时,需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类 +* MethodHandle 和 VarHandle 可以看作是轻量级的反射调用机制,而要想使用这两个调用, 就必须先使用 findStaticVarHandle 来初始化要调用的类 +* 补充:当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化 -魔数:每个 Class 文件开头的 4 个字节的无符号整数称为魔数(Magic Number),是 Class 文件的标识符,代表这是一个能被虚拟机接受的有效合法的 Class 文件, +**被动引用**:所有引用类的方式都不会触发初始化,称为被动引用 -* 魔数值固定为 0xCAFEBABE,不符合则会抛出错误 +* 通过子类引用父类的静态字段,不会导致子类初始化,只会触发父类的初始化 +* 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法 +* 常量(final 修饰)在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化 +* 调用 ClassLoader 类的 loadClass() 方法加载一个类,并不是对类的主动使用,不会导致类的初始化 -* 使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动 -版本:4 个 字节,5 6两个字节代表的是编译的副版本号 minor_version,而 7 8 两个字节是编译的主版本号 major_version -* 不同版本的 Java 编译器编译的 Class 文件对应的版本是不一样的,高版本的 Java 虚拟机可以执行由低版本编译器生成的 Class 文件,反之 JVM 会抛出异常 `java.lang.UnsupportedClassVersionError` +*** -| 主版本(十进制) | 副版本(十进制) | 编译器版本 | -| ---------------- | ---------------- | ---------- | -| 45 | 3 | 1.1 | -| 46 | 0 | 1.2 | -| 47 | 0 | 1.3 | -| 48 | 0 | 1.4 | -| 49 | 0 | 1.5 | -| 50 | 0 | 1.6 | -| 51 | 0 | 1.7 | -| 52 | 0 | 1.8 | -| 53 | 0 | 1.9 | -| 54 | 0 | 1.10 | -| 55 | 0 | 1.11 | -![](https://gitee.com/seazean/images/raw/master/Java/JVM-类结构.png) +##### init + +init 指的是实例构造器,主要作用是在类实例化过程中执行,执行内容包括成员变量初始化和代码块的执行 +实例化即调用 ()V ,虚拟机会保证这个类的构造方法的线程安全,先为实例变量分配内存空间,再执行赋默认值,然后根据源码中的顺序执行赋初值或代码块,没有成员变量初始化和代码块则不会执行 -图片来源:https://www.bilibili.com/video/BV1PJ411n7xZ +类实例化过程:**父类的类构造器() -> 子类的类构造器() -> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成员变量和实例代码块 -> 子类的构造函数** @@ -12738,81 +12855,53 @@ Class 文件格式采用一种类似于 C 语言结构体的方式进行数据 -##### 常量池 - -常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项 u2 类型的无符号数,代表常量池计数器(constant_pool_count),这个容量计数是从 1 而不是 0 开始,是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达不引用任何一个常量池项目,这种情况可用索引值 0 来表示 +#### 卸载阶段 -constant_pool 是一种表结构,以1 ~ constant_pool_count - 1为索引,表明有多少个常量池表项。表项中存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池 +时机:执行了 System.exit() 方法,程序正常执行结束,程序在执行过程中遇到了异常或错误而异常终止,由于操作系统出现错误而导致Java虚拟机进程终止 -* 字面量(Literal) :基本数据类型、字符串类型常量、声明为 final 的常量值等 +卸载类即该类的 **Class 对象被 GC**,卸载类需要满足3个要求: -* 符号引用(Symbolic References):类和接口的全限定名、字段的名称和描述符、方法的名称和描述符 +1. 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象 +2. 该类没有在其他任何地方被引用 +3. 该类的类加载器的实例已被 GC,一般是可替换类加载器的场景,如 OSGi、JSP 的重加载等,很难达成 - * 全限定名:com/test/Demo 这个就是类的全限定名,仅仅是把包名的 `.` 替换成 `/`,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个 `;` 表示全限定名结束 +在 JVM 生命周期类,由 JVM 自带的类加载器加载的类是不会被卸载的,自定义的类加载器加载的类是可能被卸载。因为 JVM 会始终引用启动、扩展、系统类加载器,这些类加载器始终引用它们所加载的类,这些类始终是可及的 - * 简单名称:指没有类型和参数修饰的方法或者字段名称,比如字段 x 的简单名称就是 x - * 描述符:用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值 - | 标志符 | 含义 | - | ------ | --------------------------------------------------------- | - | B | 基本数据类型 byte | - | C | 基本数据类型 char | - | D | 基本数据类型 double | - | F | 基本数据类型 float | - | I | 基本数据类型 int | - | J | 基本数据类型 long | - | S | 基本数据类型 short | - | Z | 基本数据类型 boolean | - | V | 代表 void 类型 | - | L | 对象类型,比如:`Ljava/lang/Object;`,不同方法间用`;`隔开 | - | [ | 数组类型,代表一维数组。比如:`double[][][] is [[[D` | +**** -常量类型和结构: -| 类型 | 标志(或标识) | 描述 | -| -------------------------------- | ------------ | ---------------------- | -| CONSTANT_utf8_info | 1 | UTF-8编码的字符串 | -| CONSTANT_Integer_info | 3 | 整型字面量 | -| CONSTANT_Float_info | 4 | 浮点型字面量 | -| CONSTANT_Long_info | 5 | 长整型字面量 | -| CONSTANT_Double_info | 6 | 双精度浮点型字面量 | -| CONSTANT_Class_info | 7 | 类或接口的符号引用 | -| CONSTANT_String_info | 8 | 字符串类型字面量 | -| CONSTANT_Fieldref_info | 9 | 字段的符号引用 | -| CONSTANT_Methodref_info | 10 | 类中方法的符号引用 | -| CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 | -| CONSTANT_NameAndType_info | 12 | 字段或方法的符号引用 | -| CONSTANT_MethodHandle_info | 15 | 表示方法句柄 | -| CONSTANT_MethodType_info | 16 | 标志方法类型 | -| CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 | -18 种常量没有出现 byte、short、char,boolean 的原因:编译之后都可以理解为 Integer +### 类加载器 +#### 类加载 +类加载方式: -**** +* 隐式加载:不直接在代码中调用 ClassLoader 的方法加载类对象 + * 创建类对象、使用类的静态域、创建子类对象、使用子类的静态域 + * 在 JVM 启动时,通过三大类加载器加载 class +* 显式加载: + * ClassLoader.loadClass(className):只加载和连接,**不会进行初始化** + * Class.forName(String name, boolean initialize, ClassLoader loader):使用 loader 进行加载和连接,根据参数 initialize 决定是否初始化 +类的唯一性: +* 在 JVM 中表示两个 class 对象判断为同一个类存在的两个必要条件: + - 类的完整类名必须一致,包括包名 + - 加载这个类的 ClassLoader(指 ClassLoader 实例对象)必须相同 +* 这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true -##### 访问标识 +命名空间: -访问标识(access_flag),又叫访问标志、访问标记,该标识用两个字节表示,用于识别一些类或者接口层次的访问信息,包括这个 Class 是类还是接口,是否定义为 public类型,是否定义为 abstract类型等 +- 每个类加载器都有自己的命名空间,命名空间由该加载器及所有的父加载器所加载的类组成 +- 在同一命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类 -* 类的访问权限通常为 ACC_ 开头的常量 -* 每一种类型的表示都是通过设置访问标记的 32 位中的特定位来实现的,比如若是 public final 的类,则该标记为 `ACC_PUBLIC | ACC_FINAL` -* 使用 `ACC_SUPER` 可以让类更准确地定位到父类的方法,确定类或接口里面的 invokespecial 指令使用的是哪一种执行语义,现代编译器都会设置并且使用这个标记 +基本特征: -| 标志名称 | 标志值 | 含义 | -| -------------- | ------ | ------------------------------------------------------------ | -| ACC_PUBLIC | 0x0001 | 标志为 public 类型 | -| ACC_FINAL | 0x0010 | 标志被声明为 final,只有类可以设置 | -| ACC_SUPER | 0x0020 | 标志允许使用 invokespecial 字节码指令的新语义,JDK1.0.2之后编译出来的类的这个标志默认为真,使用增强的方法调用父类方法 | -| ACC_INTERFACE | 0x0200 | 标志这是一个接口 | -| ACC_ABSTRACT | 0x0400 | 是否为 abstract 类型,对于接口或者抽象类来说,次标志值为真,其他类型为假 | -| ACC_SYNTHETIC | 0x1000 | 标志此类并非由用户代码产生(由编译器产生的类,没有源码对应) | -| ACC_ANNOTATION | 0x2000 | 标志这是一个注解 | -| ACC_ENUM | 0x4000 | 标志这是一个枚举 | +* **可见性**,子类加载器可以访问父加载器加载的类型,但是反过来是不允许的 +* **单一性**,由于父加载器的类型对于子加载器是可见的,所以父加载器中加载过的类型,不会在子加载器中重复加载 @@ -12820,84 +12909,89 @@ constant_pool 是一种表结构,以1 ~ constant_pool_count - 1为索引,表 -##### 索引集合 +#### 加载器 -类索引、父类索引、接口索引集合 +类加载器是 Java 的核心组件,用于加载字节码到 JVM 内存,得到 Class 类的对象 -* 类索引用于确定这个类的全限定名 +从 Java 虚拟机规范来讲,只存在以下两种不同的类加载器: -* 父类索引用于确定这个类的父类的全限定名,Java 语言不允许多重继承,所以父类索引只有一个,除了Object 之外,所有的 Java 类都有父类,因此除了 java.lang.Object 外,所有 Java 类的父类索引都不为0 +- 启动类加载器(Bootstrap ClassLoader):使用 C++ 实现,是虚拟机自身的一部分 +- 自定义类加载器(User-Defined ClassLoader):Java 虚拟机规范**将所有派生于抽象类 ClassLoader 的类加载器都划分为自定义类加载器**,使用 Java 语言实现,独立于虚拟机 -* 接口索引集合就用来描述这个类实现了哪些接口 - * interfaces_count 项的值表示当前类或接口的直接超接口数量 - * interfaces[] 接口索引集合,被实现的接口将按 implements 语句后的接口顺序从左到右排列在接口索引集合中 +从 Java 开发人员的角度看: -| 长度 | 含义 | -| ---- | ---------------------------- | -| u2 | this_class | -| u2 | super_class | -| u2 | interfaces_count | -| u2 | interfaces[interfaces_count] | +* 启动类加载器(Bootstrap ClassLoader): + * 处于安全考虑,Bootstrap 启动类加载器只加载包名为 java、javax、sun 等开头的类 + * 类加载器负责加载在 `JAVA_HOME/jre/lib `或 `sun.boot.class.path` 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的类,并且是虚拟机识别的类库加载到虚拟机内存中 + * 仅按照文件名识别,如 rt.jar 名字不符合的类库即使放在 lib 目录中也不会被加载 + * 启动类加载器无法被 Java 程序直接引用,编写自定义类加载器时,如果要把加载请求委派给启动类加载器,直接使用 null 代替 +* 扩展类加载器(Extension ClassLoader): + * 由 ExtClassLoader (sun.misc.Launcher$ExtClassLoader) 实现,上级为 Bootstrap,显示为 null + * 将 `JAVA_HOME/jre/lib/ext `或者被 `java.ext.dir` 系统变量所指定路径中的所有类库加载到内存中 + * 开发者可以使用扩展类加载器,创建的 JAR 放在此目录下,会由扩展类加载器自动加载 +* 应用程序类加载器(Application ClassLoader): + * 由 AppClassLoader(sun.misc.Launcher$AppClassLoader) 实现,上级为 Extension + * 负责加载环境变量 classpath 或系统属性 `java.class.path` 指定路径下的类库 + * 这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此称为系统类加载器 + * 可以直接使用这个类加载器,如果应用程序中没有自定义类加载器,这个就是程序中默认的类加载器 +* 自定义类加载器:由开发人员自定义的类加载器,上级是 Application +```java +public static void main(String[] args) { + //获取系统类加载器 + ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); + System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2 + //获取其上层 扩展类加载器 + ClassLoader extClassLoader = systemClassLoader.getParent(); + System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@610455d6 -*** + //获取其上层 获取不到引导类加载器 + ClassLoader bootStrapClassLoader = extClassLoader.getParent(); + System.out.println(bootStrapClassLoader);//null + //对于用户自定义类来说:使用系统类加载器进行加载 + ClassLoader classLoader = ClassLoaderTest.class.getClassLoader(); + System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2 + //String 类使用引导类加载器进行加载的 --> java核心类库都是使用启动类加载器加载的 + ClassLoader classLoader1 = String.class.getClassLoader(); + System.out.println(classLoader1);//null -##### 字段表 +} +``` -字段 fields 用于描述接口或类中声明的变量,包括类变量以及实例变量,但不包括方法内部、代码块内部声明的局部变量以及从父类或父接口继承。字段叫什么名字、被定义为什么数据类型,都是无法固定的,只能引用常量池中的常量来描述 +补充两个类加载器: -fields_count(字段计数器),表示当前 class 文件 fields 表的成员个数,用两个字节来表示 +* SecureClassLoader 扩展了 ClassLoader,新增了几个与使用相关的代码源和权限定义类验证(对 class 源码的访问权限)的方法,一般不会直接跟这个类打交道,更多是与它的子类 URLClassLoader 有所关联 +* ClassLoader 是一个抽象类,很多方法是空的没有实现,而 URLClassLoader 这个实现类为这些方法提供了具体的实现,并新增了 URLClassPath 类协助取得 Class 字节流等功能。在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承 URLClassLoader 类,这样就可以避免去编写 findClass() 方法及其获取字节码流的方式,使自定义类加载器编写更加简洁 -fields[](字段表): -* 表中的每个成员都是一个 fields_info 结构的数据项,用于表示当前类或接口中某个字段的完整描述 -* 字段访问标识: +*** - | 标志名称 | 标志值 | 含义 | - | ------------- | ------ | -------------------------- | - | ACC_PUBLIC | 0x0001 | 字段是否为public | - | ACC_PRIVATE | 0x0002 | 字段是否为private | - | ACC_PROTECTED | 0x0004 | 字段是否为protected | - | ACC_STATIC | 0x0008 | 字段是否为static | - | ACC_FINAL | 0x0010 | 字段是否为final | - | ACC_VOLATILE | 0x0040 | 字段是否为volatile | - | ACC_TRANSTENT | 0x0080 | 字段是否为transient | - | ACC_SYNCHETIC | 0x1000 | 字段是否为由编译器自动产生 | - | ACC_ENUM | 0x4000 | 字段是否为enum | -* 字段名索引:根据该值查询常量池中的指定索引项即可 -* 描述符索引:用来描述字段的数据类型、方法的参数列表和返回值 +#### 常用API - | 字符 | 类型 | 含义 | - | ----------- | --------- | ----------------------- | - | B | byte | 有符号字节型树 | - | C | char | Unicode字符,UTF-16编码 | - | D | double | 双精度浮点数 | - | F | float | 单精度浮点数 | - | I | int | 整型数 | - | J | long | 长整数 | - | S | short | 有符号短整数 | - | Z | boolean | 布尔值true/false | - | V | void | 代表void类型 | - | L Classname | reference | 一个名为Classname的实例 | - | [ | reference | 一个一维数组 | +ClassLoader 类,是一个抽象类,其后所有的类加载器都继承自 ClassLoader(不包括启动类加载器) -* 属性表集合:属性个数存放在 attribute_count 中,属性具体内容存放在 attribute 数组中,一个字段还可能拥有一些属性,用于存储更多的额外信息,比如初始化值、一些注释信息等 +获取 ClassLoader 的途径: - ```java - ConstantValue_attribute{ - u2 attribute_name_index; - u4 attribute_length; - u2 constantvalue_index; - } - ``` +* 获取当前类的 ClassLoader:`clazz.getClassLoader()` +* 获取当前线程上下文的 ClassLoader:`Thread.currentThread.getContextClassLoader()` +* 获取系统的 ClassLoader:`ClassLoader.getSystemClassLoader()` +* 获取调用者的 ClassLoader:`DriverManager.getCallerClassLoader()` - 对于常量属性而言,attribute_length 值恒为2 +ClassLoader 类常用方法: + +* `getParent()`:返回该类加载器的超类加载器 +* `loadclass(String name)`:加载名为 name 的类,返回结果为 Class 类的实例,**该方法就是双亲委派模式** +* `findclass(String name)`:查找二进制名称为 name 的类,返回结果为 Class 类的实例,该方法会在检查完父类加载器之后被 loadClass() 方法调用 +* `findLoadedClass(String name)`:查找名称为 name 的已经被加载过的类,final 修饰无法重写 +* `defineClass(String name, byte[] b, int off, int len)`:将**字节流**解析成 JVM 能够识别的类对象 +* `resolveclass(Class c)`:链接指定的 Java 类,可以使类的 Class 对象创建完成的同时也被解析 +* `InputStream getResourceAsStream(String name)`:指定资源名称获取输入流 @@ -12905,108 +12999,108 @@ fields[](字段表): -##### 方法表 +#### 加载模型 -方法表是 methods 指向常量池索引集合,其中每一个 method_info 项都对应着一个类或者接口中的方法信息,完整描述了每个方法的签名 +##### 加载机制 -* 如果这个方法不是抽象的或者不是 native 的,字节码中就会体现出来 -* methods 表只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法 -* methods 表可能会出现由编译器自动添加的方法,比如初始化方法 和实例化方法 +在 JVM 中,对于类加载模型提供了三种,分别为全盘加载、双亲委派、缓存机制 -**重载(Overload)**一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,因为返回值不会包含在特征签名之中,因此 Java 语言里无法仅仅依靠返回值的不同来对一个已有方法进行重载。但在 Class 文件格式中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法就可以共存 +- **全盘加载:**当一个类加载器负责加载某个 Class 时,该 Class 所依赖和引用的其他 Class 也将由该类加载器负责载入,除非显示指定使用另外一个类加载器来载入 -methods_count(方法计数器):表示 class 文件 methods 表的成员个数,使用两个字节来表示 +- **双亲委派:**先让父类加载器加载该 Class,在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。简单来说就是,某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,**依次递归**,如果父加载器可以完成类加载任务,就成功返回;只有当父加载器无法完成此加载任务时,才自己去加载 -methods[](方法表):每个表项都是一个 method_info 结构,表示当前类或接口中某个方法的完整描述 +- **缓存机制:**会保证所有加载过的 Class 都会被缓存,当程序中需要使用某个 Class 时,类加载器先从缓存区中搜寻该 Class,只有当缓存区中不存在该 Class 对象时,系统才会读取该类对应的二进制数据,并将其转换成 Class 对象存入缓冲区中 + - 这就是修改了 Class 后,必须重新启动 JVM,程序所做的修改才会生效的原因 -* 方法表结构如下: - | 类型 | 名称 | 含义 | 数量 | - | -------------- | ---------------- | ---------- | ---------------- | - | u2 | access_flags | 访问标志 | 1 | - | u2 | name_index | 字段名索引 | 1 | - | u2 | descriptor_index | 描述符索引 | 1 | - | u2 | attrubutes_count | 属性计数器 | 1 | - | attribute_info | attributes | 属性集合 | attributes_count | -* 方法表访问标志: - | 标志名称 | 标志值 | 含义 | - | ------------- | ------ | -------------------------- | - | ACC_PUBLIC | 0x0001 | 字段是否为 public | - | ACC_PRIVATE | 0x0002 | 字段是否为 private | - | ACC_PROTECTED | 0x0004 | 字段是否为 protected | - | ACC_STATIC | 0x0008 | 字段是否为 static | - | ACC_FINAL | 0x0010 | 字段是否为 final | - | ACC_VOLATILE | 0x0040 | 字段是否为 volatile | - | ACC_TRANSTENT | 0x0080 | 字段是否为 transient | - | ACC_SYNCHETIC | 0x1000 | 字段是否为由编译器自动产生 | - | ACC_ENUM | 0x4000 | 字段是否为 enum | +*** -*** +##### 双亲委派 +双亲委派模型(Parents Delegation Model):该模型要求除了顶层的启动类加载器外,其它类加载器都要有父类加载器,这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance) -##### 属性表 +工作过程:一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载 -属性表集合,指的是 Class 文件所携带的辅助信息,比如该 Class 文件的源文件的名称,以及任何带有 `RetentionPolicy.CLASS` 或者 `RetentionPolicy.RUNTIME` 的注解,这类信息通常被用于 Java 虚拟机的验证和运行,以及 Java 程序的调试。字段表、方法表都可以有自己的属性表,用于描述某些场景专有的信息 +双亲委派机制的优点: -attributes_ count(属性计数器):表示当前文件属性表的成员个数 +* 可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证全局唯一性 -attributes[](属性表):属性表的每个项的值必须是 attribute_info 结构 +* Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一 -* 属性的通用格式: +* 保护程序安全,防止类库的核心 API 被随意篡改 + + 例如:在工程中新建 java.lang 包,接着在该包下新建 String 类,并定义 main 函数 ```java - ConstantValue_attribute{ - u2 attribute_name_index; //属性名索引 - u4 attribute_length; //属性长度 - u2 attribute_info; //属性表 + public class String { + public static void main(String[] args) { + System.out.println("demo info"); + } } ``` + + 此时执行 main 函数,会出现异常,在类 java.lang.String 中找不到 main 方法,防止恶意篡改核心 API 库。出现该信息是因为双亲委派的机制,java.lang.String 的在启动类加载器(Bootstrap)得到加载,启动类加载器优先级更高,在核心 jre 库中有其相同名字的类文件,但该类中并没有 main 方法 -* 属性类型: - - | 属性名称 | 使用位置 | 含义 | - | ------------------------------------- | ------------------ | ------------------------------------------------------------ | - | Code | 方法表 | Java 代码编译成的字节码指令 | - | ConstantValue | 字段表 | final 关键字定义的常量池 | - | Deprecated | 类、方法、字段表 | 被声明为 deprecated 的方法和字段 | - | Exceptions | 方法表 | 方法抛出的异常 | - | EnclosingMethod | 类文件 | 仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法 | - | InnerClass | 类文件 | 内部类列表 | - | LineNumberTable | Code 属性 | Java 源码的行号与字节码指令的对应关系 | - | LocalVariableTable | Code 属性 | 方法的局部变量描述 | - | StackMapTable | Code 属性 | JDK1.6 中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配 | - | Signature | 类,方法表,字段表 | 用于支持泛型情况下的方法签名 | - | SourceFile | 类文件 | 记录源文件名称 | - | SourceDebugExtension | 类文件 | 用于存储额外的调试信息 | - | Syothetic | 类,方法表,字段表 | 标志方法或字段为编泽器自动生成的 | - | LocalVariableTypeTable | 类 | 使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 | - | RuntimeVisibleAnnotations | 类,方法表,字段表 | 为动态注解提供支持 | - | RuntimelnvisibleAnnotations | 类,方法表,字段表 | 用于指明哪些注解是运行时不可见的 | - | RuntimeVisibleParameterAnnotation | 方法表 | 作用与 RuntimeVisibleAnnotations 属性类似,只不过作用对象为方法 | - | RuntirmelnvisibleParameterAnniotation | 方法表 | 作用与 RuntimelnvisibleAnnotations 属性类似,作用对象哪个为方法参数 | - | AnnotationDefauit | 方法表 | 用于记录注解类元素的默认值 | - | BootstrapMethods | 类文件 | 用于保存 invokeddynanic 指令引用的引导方式限定符 | - +双亲委派机制的缺点:检查类是否加载的委托过程是单向的,这个方式虽然从结构上看比较清晰,使各个 ClassLoader 的职责非常明确,但**顶层的 ClassLoader 无法访问底层的 ClassLoader 所加载的类**(可见性) + -**** +*** -#### 编译指令 +##### 源码分析 -##### javac +```java +protected Class loadClass(String name, boolean resolve) + throws ClassNotFoundException { + synchronized (getClassLoadingLock(name)) { + // 调用当前类加载器的 findLoadedClass(name),检查当前类加载器是否已加载过指定 name 的类 + Class c = findLoadedClass(name); + + // 当前类加载器如果没有加载过 + if (c == null) { + long t0 = System.nanoTime(); + try { + // 判断当前类加载器是否有父类加载器 + if (parent != null) { + // 如果当前类加载器有父类加载器,则调用父类加载器的 loadClass(name,false) +          // 父类加载器的 loadClass 方法,又会检查自己是否已经加载过 + c = parent.loadClass(name, false); + } else { + // 当前类加载器没有父类加载器,说明当前类加载器是 BootStrapClassLoader +           // 则调用 BootStrap ClassLoader 的方法加载类 + c = findBootstrapClassOrNull(name); + } + } catch (ClassNotFoundException e) { } -javac:编译命令,将 java 源文件编译成 class 字节码文件 + if (c == null) { + // 如果调用父类的类加载器无法对类进行加载,则用自己的 findClass() 方法进行加载 + // 可以自定义 findClass() 方法 + long t1 = System.nanoTime(); + c = findClass(name); -`javac xx.java` 不会在生成对应的局部变量表等信息,使用 `javac -g xx.java` 可以生成所有相关信息 + // this is the defining class loader; record the stats + sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); + sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); + sun.misc.PerfCounter.getFindClasses().increment(); + } + } + if (resolve) { + // 链接指定的 Java 类,可以使类的 Class 对象创建完成的同时也被解析 + resolveClass(c); + } + return c; + } +} +``` @@ -13014,62 +13108,43 @@ javac:编译命令,将 java 源文件编译成 class 字节码文件 -##### javap - -javap 反编译生成的字节码文件,根据 class 字节码文件,反解析出当前类对应的 code 区 (字节码指令)、局部变量表、异常表和代码行偏移量映射表、常量池等信息 - -用法:javap - -```sh --help --help -? 输出此用法消息 --version 版本信息 --public 仅显示公共类和成员 --protected 显示受保护的/公共类和成员 --package 显示程序包/受保护的/公共类和成员 (默认) --p -private 显示所有类和成员 - #常用的以下三个 --v -verbose 输出附加信息 --l 输出行号和本地变量表 --c 对代码进行反汇编 #反编译 - --s 输出内部类型签名 --sysinfo 显示正在处理的类的系统信息 (路径, 大小, 日期, MD5 散列) --constants 显示最终常量 --classpath 指定查找用户类文件的位置 --cp 指定查找用户类文件的位置 --bootclasspath 覆盖引导类文件的位置 -``` - - +##### 破坏委派 -*** +双亲委派模型并不是一个具有强制性约束的模型,而是 Java 设计者推荐给开发者的类加载器实现方式 +破坏双亲委派模型的方式: +* 自定义 ClassLoader -#### 指令集 + * 如果不想破坏双亲委派模型,只需要重写 findClass 方法 + * 如果想要去破坏双亲委派模型,需要去**重写 loadClass **方法 -##### 执行指令 +* 引入线程**上下文类加载器** -Java 字节码属于 JVM 基本执行指令。由一个字节长度的代表某种操作的操作码(opcode)以及零至多个代表此操作所需参数的操作数(operand)所构成,虚拟机中许多指令并不包含操作数,只有一个操作码(零地址指令) + Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI 等。这些 SPI 接口由 Java 核心库来提供,而 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径 classpath 里,SPI 接口中的代码需要加载具体的实现类: -由于限制了 Java 虚拟机操作码的长度为一个字节(0~255),所以指令集的操作码总数不可能超过 256 条 + * SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的 + * SPI 的实现类是由系统类加载器来加载的,引导类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader 无法委派 AppClassLoader 来加载类 -在 JVM 的指令集中,大多数的指令都包含了其操作所对应的数据类型信息。例如 iload 指令用于从局部变量表中加载 int 型的数据到操作数栈中,而 fload 指令加载的则是 float 类型的数据 + JDK 开发人员引入了线程上下文类加载器(Thread Context ClassLoader),这种类加载器可以通过 Thread 类的 setContextClassLoader 方法进行设置线程上下文类加载器,在执行线程中抛弃双亲委派加载模式,使程序可以逆向使用类加载器,使 Bootstrap 加载器拿到了 Application 加载器加载的类,破坏了双亲委派模型 + +* 实现程序的动态性,如代码热替换(Hot Swap)、模块热部署(Hot Deployment) -* i 代表对 int 类型的数据操作 -* l 代表 long -* s 代表 short -* b 代表 byte -* c 代表 char -* f 代表 float -* d 代表 double + IBM 公司主导的 JSR一291(OSGiR4.2)实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(OSGi 中称为 Bundle)都有一个自己的类加载器,当更换一个 Bundle 时,就把 Bundle 连同类加载器一起换掉以实现代码的热替换,在 OSGi 环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构 -大部分的指令都没有支持 byte、char、short、boolean 类型,编译器会在编译期或运行期将 byte 和 short 类型的数据带符号扩展(Sign-Extend-)为相应的 int 类型数据,将 boolean 和 char 类型数据零位扩展(Zero-Extend-)为相应的 int 类型数据 + 当收到类加载请求时,OSGi 将按照下面的顺序进行类搜索: -在做值相关操作时: + 1. 将以 java.* 开头的类,委派给父类加载器加载 + 2. 否则,将委派列表名单内的类,委派给父类加载器加载 + 3. 否则,将 Import 列表中的类,委派给 Export 这个类的 Bundle 的类加载器加载 + 4. 否则,查找当前 Bundle 的 ClassPath,使用自己的类加载器加载 + 5. 否则,查找类是否在自己的 Fragment Bundle 中,如果在就委派给 Fragment Bundle 类加载器加载 + 6. 否则,查找 Dynamic Import 列表的 Bundle,委派给对应 Bundle 的类加载器加载 + 7. 否则,类查找失败 + + 热替换是指在程序的运行过程中,不停止服务,只通过替换程序文件来修改程序的行为,**热替换的关键需求在于服务不能中断**,修改必须立即表现正在运行的系统之中 -- 一个指令,可以从局部变量表、常量池、堆中对象、方法调用、系统调用中等取得数据,这些数据(可能是值,也可能是对象的引用)被压入操作数栈 -- 一个指令,也可以从操作数栈中取出一到多个值(pop 多次),完成赋值、加减乘除、方法传参、系统调用等等操作 + @@ -13077,195 +13152,180 @@ Java 字节码属于 JVM 基本执行指令。由一个字节长度的代表某 -##### 加载存储 - -加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传递 - -局部变量压栈指令:将给定的局部变量表中的数据压入操作数栈 +#### 沙箱机制 -* xload、xload_n,x 表示取值数据类型,为 i、l、f、d、a, n 为 0 到 3 -* 指令 xload_n 表示将第 n 个局部变量压入操作数栈,aload_n 表示将一个对象引用压栈 -* 指令 xload n 通过指定参数的形式,把局部变量压入操作数栈,局部变量数量超过 4 个时使用这个命令 +沙箱机制(Sandbox):将 Java 代码限定在虚拟机特定的运行范围中,并且严格限制代码对本地系统资源访问,来保证对代码的有效隔离,防止对本地系统造成破坏 -常量入栈指令:将常数压入操作数栈,根据数据类型和入栈内容的不同,又分为 const、push、ldc指令 +沙箱**限制系统资源访问**,包括 CPU、内存、文件系统、网络,不同级别的沙箱对资源访问的限制也不一样 -* push:包括 bipush 和 sipush,区别在于接收数据类型的不同,bipush 接收 8 位整数作为参数,sipush 接收 16 位整数 -* ldc:如果以上指令不能满足需求,可以使用 ldc 指令,接收一个 8 位的参数,该参数指向常量池中的 int、 float 或者 String 的索引,将指定的内容压入堆栈。ldc_w 接收两个 8 位参数,能支持的索引范围更大,如果要压入的元素是 long 或 double 类型的,则使用 ldc2_w 指令 -* aconst_null 将 null 对象引用压入栈,iconst_m1 将 int 类型常量 -1 压入栈,iconst_0 将 int 类型常量 0 压入栈 +* JDK1.0:Java 中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码被看作是不受信的。对于授信的本地代码,可以访问一切本地资源,而对于非授信的远程代码不可以访问本地资源,其实依赖于沙箱机制。如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现 +* JDK1.1:针对安全机制做了改进,增加了安全策略。允许用户指定代码对本地资源的访问权限 +* JDK1.2:改进了安全机制,增加了代码签名,不论本地代码或是远程代码都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制 +* JDK1.6:当前最新的安全机制,引入了域(Domain)的概念。虚拟机会把所有代码加载到不同的系统域和应用域,不同的保护域对应不一样的权限。系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问 -出栈装入局部变量表指令:将操作数栈中栈顶元素弹出后,装入局部变量表的指定位置,用于给局部变量赋值 + -* xstore、xstore_n,x 表示取值类型为 i、l、f、d、a, n 为 0 到 3 -* xastore 表示存入数组,x 取值为 i、l、f、d、a、b、c、s -扩充局部变量表的访问索引的指令:wide +*** -**** +#### 自定义 +对于自定义类加载器的实现,只需要继承 ClassLoader 类,覆写 findClass 方法即可 -##### 算术指令 +作用:隔离加载类、修改类加载的方式、拓展加载源、防止源码泄漏 -算术指令用于对两个操作数栈上的值进行某种特定运算,并把计算结果重新压入操作数栈 +```java +//自定义类加载器,读取指定的类路径classPath下的class文件 +public class MyClassLoader extends ClassLoader{ + private String classPath; -没有直接支持 byte、 short、 char 和 boolean类型的算术指令,对于这些数据的运算,都使用 int 类型的指令来处理,数组类型也是转换成 int 数组 - -* 加法指令:iadd、ladd、fadd、dadd -* 减法指令:isub、lsub、fsub、dsub -* 乘法指令:imu、lmu、fmul、dmul -* 除法指令:idiv、ldiv、fdiv、ddiv -* 求余指令:irem、lrem、frem、drem(remainder 余数) -* 取反指令:ineg、lneg、fneg、dneg (negation 取反) -* 自增指令:iinc(直接**在局部变量 slot 上进行运算**,不用放入操作数栈) -* 位运算指令,又可分为: - - 位移指令:ishl、ishr、 iushr、lshl、lshr、 lushr - - 按位或指令:ior、lor - - 按位与指令:iand、land - - 按位异或指令:ixor、lxor - -* 比较指令:dcmpg、dcmpl、 fcmpg、fcmpl、lcmp - -运算模式: - -* 向最接近数舍入模式,JVM 在进行浮点数计算时,所有的运算结果都必须舍入到适当的精度,非精确结果必须舍入为可被表示的最接近的精确值,如果有两种可表示形式与该值一样接近,将优先选择最低有效位为零的 -* 向零舍入模式:将浮点数转换为整数时,该模式将在目标数值类型中选择一个最接近但是不大于原值的数字作为最精确的舍入结果 - -NaN 值:当一个操作产生溢出时,将会使用有符号的无穷大表示,如果某个操作结果没有明确的数学定义,将使用 NaN 值来表示 - -```java -double j = i / 0.0; -System.out.println(j);//无穷大,NaN: not a number -``` - -**分析 i++**:从字节码角度分析:a++ 和 ++a 的区别是先执行 iload 还是 先执行 iinc - -```java - 4 iload_1 //存入操作数栈 - 5 iinc 1 by 1 //自增i++ - 8 istore_3 //把操作数栈没有自增的数据的存入局部变量表 - 9 iinc 2 by 1 //++i -12 iload_2 //加载到操作数栈 -13 istore 4 //存入局部变量表,这个存入没有 _ 符号,_只能到3 -``` + public MyClassLoader(String classPath) { + this.classPath = classPath; + } + + public MyClassLoader(ClassLoader parent, String byteCodePath) { + super(parent); + this.classPath = classPath; + } -```java -public class Demo { - public static void main(String[] args) { - int a = 10; - int b = a++ + ++a + a--; - System.out.println(a); //11 - System.out.println(b); //34 + @Override + protected Class findClass(String name) throws ClassNotFoundException { + BufferedInputStream bis = null; + ByteArrayOutputStream baos = null; + try { + // 获取字节码文件的完整路径 + String fileName = classPath + className + ".class"; + // 获取一个输入流 + bis = new BufferedInputStream(new FileInputStream(fileName)); + // 获取一个输出流 + baos = new ByteArrayOutputStream(); + // 具体读入数据并写出的过程 + int len; + byte[] data = new byte[1024]; + while ((len = bis.read(data)) != -1) { + baos.write(data, 0, len); + } + // 获取内存中的完整的字节数组的数据 + byte[] byteCodes = baos.toByteArray(); + // 调用 defineClass(),将字节数组的数据转换为 Class 的实例。 + Class clazz = defineClass(null, byteCodes, 0, byteCodes.length); + return clazz; + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + if (baos != null) + baos.close(); + } catch (IOException e) { + e.printStackTrace(); + } + try { + if (bis != null) + bis.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + return null; } } ``` -判断结果: - ```java -public class Demo { - public static void main(String[] args) { - int i = 0; - int x = 0; - while (i < 10) { - x = x++; - i++; - } - System.out.println(x); // 结果是 0 +public static void main(String[] args) { + MyClassLoader loader = new MyClassLoader("D:\Workspace\Project\JVM_study\src\java1\"); + + try { + Class clazz = loader.loadClass("Demo1"); + System.out.println("加载此类的类的加载器为:" + clazz.getClassLoader().getClass().getName());//MyClassLoader + + System.out.println("加载当前类的类的加载器的父类加载器为:" + clazz.getClassLoader().getParent().getClass().getName());//sun.misc.Launcher$AppClassLoader + } catch (ClassNotFoundException e) { + e.printStackTrace(); } } ``` -*** +**** -##### 类型转换 +#### JDK9 -类型转换指令可以将两种不同的数值类型进行相互转换,除了 boolean 之外的七种类型 +为了保证兼容性,JDK9 没有改变三层类加载器架构和双亲委派模型,但为了模块化系统的顺利运行做了一些变动: -宽化类型转换: +* 扩展机制被移除,扩展类加载器由于**向后兼容性**的原因被保留,不过被重命名为平台类加载器(platform classloader),可以通过 ClassLoader 的新方法 getPlatformClassLoader() 来获取 -* JVM 支持以下数值的宽化类型转换(widening numeric conversion),小范围类型到大范围类型的安全转换 - * 从 int 类型到 long、float 或者 double 类型,对应的指令为 i2l、i2f、i2d - * 从 long 类型到 float、 double 类型,对应的指令为 l2f、l2d - * 从 float 类型到 double 类型,对应的指令为 f2d +* JDK9 基于模块化进行构建(原来的 rt.jar 和 tools.jar 被拆分成数个 JMOD 文件),其中 Java 类库就满足了可扩展的需求,那就无须再保留 `\lib\ext` 目录,此前使用这个目录或者 `java.ext.dirs` 系统变量来扩展 JDK 功能的机制就不需要再存在 -* 精度损失问题 - * 宽化类型转换是不会因为超过目标类型最大值而丢失信息 - * 从 int 转换到 float 或者 long 类型转换到 double 时,将可能发生精度丢失 +* 启动类加载器、平台类加载器、应用程序类加载器全都继承于 `jdk.internal.loader.BuiltinClassLoader` -* 从 byte、char 和 short 类型到 int 类型的宽化类型转换实际上是不存在的,JVM 把它们当作 int 处理 + -窄化类型转换: -* Java 虚拟机直接支持以下窄化类型转换: - * 从 int 类型至 byte、 short 或者 char 类型,对应的指令有 i2b、i2c、i2s - * 从 long 类型到 int 类型,对应的指令有 l2i - * 从 float 类型到 int 或者 long 类型,对应的指令有:f2i、f2l - * 从 double 类型到 int、long 或 float 者类型,对应的指令有 d2i、d2、d2f -* 精度损失问题: - * 窄化类型转换可能会导致转换结果具备不同的正负号、不同的数量级,转换过程可能会导致数值丢失精度 - * 将一个浮点值窄化转换为整数类型 T(T 限于 int 或 long 类型之一)时,将遵循以下转换规则: - - 如果浮点值是 NaN,那转换结果就是 int 或 long 类型的 0 - - 如果浮点值不是无穷大的话,浮点值使用 IEEE 754 的向零舍入模式取整,获得整数值 v,如果 v 在目标类型 T 的表示范围之内,那转换结果就是 v,否则将根据 v 的符号,转换为 T 所能表示的最大或者最小正数 +*** -*** +## 运行机制 -##### 创建访问 +### 执行过程 -创建指令: + Java 文件编译执行的过程: -* 创建类实例指令:new,接收一个操作数指向常量池的索引,表示要创建的类型,执行完成后将对象的引用压入栈 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java文件编译执行的过程.png) - ```java - 0: new #2 // class com/jvm/bytecode/Demo - 3: dup - 4: invokespecial #3 // Method "":()V - ``` +- 类加载器:用于装载字节码文件(.class文件) +- 运行时数据区:用于分配存储空间 +- 执行引擎:执行字节码文件或本地方法 +- 垃圾回收器:用于对 JVM 中的垃圾内容进行回收 - **dup 是复制操作数栈栈顶的内容**,需要两份引用原因: - - 一个要配合 invokespecial 调用该对象的构造方法 :()V (会消耗掉栈顶一个引用) - - 一个要配合 astore_1 赋值给局部变量 -* 创建数组的指令:newarray、anewarray、multianewarray +**** - * newarray:创建基本类型数组 - * anewarray:创建引用类型数组 - * multianewarray:创建多维数组 -字段访问指令:对象创建后可以通过对象访问指令获取对象实例或数组实例中的字段或者数组元素 -* 访问类字段(static字段,或者称为类变量)的指令:getstatic、putstatic -* 访问类实例字段(非static字段,或者称为实例变量)的指令:getfield、 putfield +### 字节码 -类型检查指令:检查类实例或数组类型的指令 +#### 跨平台性 -* checkcast:用于检查类型强制转换是否可以进行,如果可以进行 checkcast 指令不会改变操作数栈,否则它会抛出 ClassCastException 异常 +Java 语言:跨平台的语言(write once ,run anywhere) -* instanceof:判断给定对象是否是某一个类的实例,会将判断结果压入操作数栈 +* 当 Java 源代码成功编译成字节码后,在不同的平台上面运行**无须再次编译** +* 让一个 Java 程序正确地运行在 JVM 中,Java 源码就必须要被编译为符合 JVM 规范的字节码 +编译过程中的编译器: +* 前端编译器: Sun 的全量式编译器 javac、 Eclipse 的增量式编译器 ECJ,把源代码编译为字节码文件 .class + * IntelliJ IDEA 使用 javac 编译器 + * Eclipse 中,当开发人员编写完代码后保存时,ECJ 编译器就会把未编译部分的源码逐行进行编译,而非每次都全量编译,因此 ECJ 的编译效率会比 javac 更加迅速和高效 + * 前端编译器并不会直接涉及编译优化等方面的技术,具体优化细节移交给 HotSpot 的 JIT 编译器负责 -**** +* 后端运行期编译器:HotSpot VM 的 C1、C2 编译器,也就是 JIT 编译器,Graal 编译器 + * JIT编译器:执行引擎部分详解 + * Graal 编译器:JDK10 HotSpot 加入的一个全新的即时编译器,编译效果短短几年时间就追平了 C2 +* 静态提前编译器:AOT (Ahead Of Time Compiler)编译器,直接把源代码编译成本地机器代码, -##### 方法指令 + * JDK 9 引入,是与即时编译相对立的一个概念,即时编译指的是在程序的运行过程中将字节码转换为机器码,AOT 是程序运行之前便将字节码转换为机器码 + + * 优点:JVM 加载已经预编译成二进制库,可以直接执行,不必等待即时编译器的预热,减少 Java 应用第一次运行慢的现象 + * 缺点: + * 破坏了 Java **一次编译,到处运行**,必须为每个不同硬件编译对应的发行包 + * 降低了 Java 链接过程的动态性,加载的代码在编译期就必须全部已知 -方法调用指令:invokevirtual、 invokeinterface、invokespecial、invokestatic、invokedynamic -**方法调用章节详解** @@ -13273,140 +13333,101 @@ public class Demo { -##### 操作数栈 +#### 语言发展 -JVM 提供的操作数栈管理指令,可以用于直接操作操作数栈的指令 +机器码:各种用二进制编码方式表示的指令,与 CPU 紧密相关,所以不同种类的 CPU 对应的机器指令不同 -* pop、pop2:将一个或两个元素从栈顶弹出,并且直接废弃 -* dup、dup2,dup_x1、dup2_x1,dup_x2、dup2_x2:复制栈顶一个或两个数值并重新压入栈顶 +指令:指令就是把机器码中特定的 0 和 1 序列,简化成对应的指令,例如 mov,inc 等,可读性稍好,但是不同的硬件平台的同一种指令(比如 mov),对应的机器码也可能不同 -* swap:将栈最顶端的两个 slot 数值位置交换,JVM 没有提供交换两个 64 位数据类型数值的指令 +指令集:不同的硬件平台支持的指令是有区别的,每个平台所支持的指令,称之为对应平台的指令集 -* nop:一个非常特殊的指令,字节码为 0x00,和汇编语言中的 nop 一样,表示什么都不做,一般可用于调试、占位等 +- x86 指令集,对应的是 x86 架构的平台 +- ARM 指令集,对应的是 ARM 架构的平台 +汇编语言:用助记符代替机器指令的操作码,用地址符号或标号代替指令或操作数的地址 +* 在不同的硬件平台,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令 +* 计算机只认识指令码,汇编语言编写的程序也必须翻译成机器指令码,计算机才能识别和执行 -*** +高级语言:为了使计算机用户编程序更容易些,后来就出现了各种高级计算机语言 +字节码:是一种中间状态(中间码)的二进制代码,比机器码更抽象,需要直译器转译后才能成为机器码 +* 字节码为了实现特定软件运行和软件环境,与硬件环境无关 +* 通过编译器和虚拟机器实现,编译器将源码编译成字节码,虚拟机器将字节码转译为可以直接执行的指令 -##### 控制转移 + -比较指令:比较栈顶两个元素的大小,并将比较结果入栈 -* lcmp:比较两个 long 类型值 -* fcmpl:比较两个 float 类型值(当遇到NaN时,返回-1) -* fcmpg:比较两个 float 类型值(当遇到NaN时,返回1) -* dcmpl:比较两个 double 类型值(当遇到NaN时,返回-1) -* dcmpg:比较两个 double 类型值(当遇到NaN时,返回1) +*** -条件跳转指令: -| 指令 | 说明 | -| --------- | -------------------------------------------------- | -| ifeq | equals,当栈顶int类型数值等于0时跳转 | -| ifne | not equals,当栈顶in类型数值不等于0时跳转 | -| iflt | lower than,当栈顶in类型数值小于0时跳转 | -| ifle | lower or equals,当栈顶in类型数值小于等于0时跳转 | -| ifgt | greater than,当栈顶int类型数组大于0时跳转 | -| ifge | greater or equals,当栈顶in类型数值大于等于0时跳转 | -| ifnull | 为 null 时跳转 | -| ifnonnull | 不为 null 时跳转 | -比较条件跳转指令: -| 指令 | 说明 | -| --------- | --------------------------------------------------------- | -| if_icmpeq | 比较栈顶两 int 类型数值大小(下同),当前者等于后者时跳转 | -| if_icmpne | 当前者不等于后者时跳转 | -| if_icmplt | 当前者小于后者时跳转 | -| if_icmple | 当前者小于等于后者时跳转 | -| if_icmpgt | 当前者大于后者时跳转 | -| if_icmpge | 当前者大于等于后者时跳转 | -| if_acmpeq | 当结果相等时跳转 | -| if_acmpne | 当结果不相等时跳转 | -多条件分支跳转指令: +#### 类结构 -* tableswitch:用于 switch 条件跳转,case 值连续 -* lookupswitch:用于 switch 条件跳转,case 值不连续 - -无条件跳转指令: - -* goto:用来进行跳转到指定行号的字节码 - -* goto_w:无条件跳转(宽索引) - - - - - -*** +##### 文件结构 +字节码是一种二进制的类文件,是编译之后供虚拟机解释执行的二进制字节码文件,**一个 class 文件对应一个 public 类型的类或接口** +字节码内容是 **JVM 的字节码指令**,不是机器码,C、C++ 经由编译器直接生成机器码,所以执行效率比 Java 高 -##### 异常处理 +JVM 官方文档:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html -###### 处理机制 +根据 JVM 规范,类文件结构如下: -抛出异常指令:athrow 指令 +```java +ClassFile { + u4 magic; + u2 minor_version; + u2 major_version; + u2 constant_pool_count; + cp_info constant_pool[constant_pool_count-1]; + u2 access_flags; + u2 this_class; + u2 super_class; + u2 interfaces_count; + u2 interfaces[interfaces_count]; + u2 fields_count; + field_info fields[fields_count]; + u2 methods_count; + method_info methods[methods_count]; + u2 attributes_count; + attribute_info attributes[attributes_count]; +} +``` -JVM 处理异常(catch 语句)不是由字节码指令来实现的,而是**采用异常表来完成**的 +| 类型 | 名称 | 说明 | 长度 | 数量 | +| -------------- | ------------------- | -------------------- | ------- | --------------------- | +| u4 | magic | 魔数,识别类文件格式 | 4个字节 | 1 | +| u2 | minor_version | 副版本号(小版本) | 2个字节 | 1 | +| u2 | major_version | 主版本号(大版本) | 2个字节 | 1 | +| u2 | constant_pool_count | 常量池计数器 | 2个字节 | 1 | +| cp_info | constant_pool | 常量池表 | n个字节 | constant_pool_count-1 | +| u2 | access_flags | 访问标识 | 2个字节 | 1 | +| u2 | this_class | 类索引 | 2个字节 | 1 | +| u2 | super_class | 父类索引 | 2个字节 | 1 | +| u2 | interfaces_count | 接口计数 | 2个字节 | 1 | +| u2 | interfaces | 接口索引集合 | 2个字节 | interfaces_count | +| u2 | fields_count | 字段计数器 | 2个字节 | 1 | +| field_info | fields | 字段表 | n个字节 | fields_count | +| u2 | methods_count | 方法计数器 | 2个字节 | 1 | +| method_info | methods | 方法表 | n个字节 | methods_count | +| u2 | attributes_count | 属性计数器 | 2个字节 | 1 | +| attribute_info | attributes | 属性表 | n个字节 | attributes_count | -* 代码: +Class 文件格式采用一种类似于 C 语言结构体的方式进行数据存储,这种结构中只有两种数据类型:无符号数和表 - ```java - public static void main(String[] args) { - int i = 0; - try { - i = 10; - } catch (Exception e) { - i = 20; - } finally { - i = 30; - } - } - ``` - -* 字节码: +* 无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串 +* 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,表都以 `_info` 结尾,用于描述有层次关系的数据,整个 Class 文件本质上就是一张表,由于表没有固定长度,所以通常会在其前面加上个数说明 - * 多出一个 **Exception table** 的结构,**[from, to) 是前闭后开的检测范围**,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号 - * 11 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置,因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用 +获取方式: - ```java - 0: iconst_0 - 1: istore_1 // 0 -> i ->赋值 - 2: bipush 10 // try 10 放入操作数栈顶 - 4: istore_1 // 10 -> i 将操作数栈顶数据弹出,存入局部变量表的 slot1 - 5: bipush 30 // 【finally】 - 7: istore_1 // 30 -> i - 8: goto 27 // return ----------------------------------- - 11: astore_2 // catch Exceptin -> e ---------------------- - 12: bipush 20 // - 14: istore_1 // 20 -> i - 15: bipush 30 // 【finally】 - 17: istore_1 // 30 -> i - 18: goto 27 // return ----------------------------------- - 21: astore_3 // catch any -> slot 3 ---------------------- - 22: bipush 30 // 【finally】 - 24: istore_1 // 30 -> i - 25: aload_3 // 将局部变量表的slot 3数据弹出,放入操作数栈栈顶 - 26: athrow // throw 抛出异常 - 27: return - Exception table: - // 任何阶段出现任务异常都会执行 finally - from to target type - 2 5 11 Class java/lang/Exception - 2 5 21 any // 剩余的异常类型,比如 Error - 11 15 21 any // 剩余的异常类型,比如 Error - LineNumberTable: ... - LocalVariableTable: - Start Length Slot Name Signature - 12 3 2 e Ljava/lang/Exception; - 0 28 0 args [Ljava/lang/String; - 2 26 1 i I - ``` +* HelloWorld.java 执行 `javac -parameters -d . HellowWorld.java`指令 +* 写入文件指令 `javap -v xxx.class >xxx.txt` +* IDEA 插件 jclasslib @@ -13414,418 +13435,482 @@ JVM 处理异常(catch 语句)不是由字节码指令来实现的,而是* -###### finally - -finally 中的代码被**复制了 3 份**,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程(上节案例) - -* 代码: - - ```java - public static int test() { - try { - return 10; - } finally { - return 20; - } - } - ``` +##### 魔数版本 -* 字节码: +魔数:每个 Class 文件开头的 4 个字节的无符号整数称为魔数(Magic Number),是 Class 文件的标识符,代表这是一个能被虚拟机接受的有效合法的 Class 文件, - ```java - 0: bipush 10 // 10 放入栈顶 - 2: istore_0 // 10 -> slot 0 【从栈顶移除了】 - 3: bipush 20 // 20 放入栈顶 - 5: ireturn // 返回栈顶 int(20) - 6: astore_1 // catch any 存入局部变量表的 slot1 - 7: bipush 20 // 20 放入栈顶 - 9: ireturn // 返回栈顶 int(20) - Exception table: - from to target type - 0 3 6 any - ``` +* 魔数值固定为 0xCAFEBABE,不符合则会抛出错误 +* 使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动 +版本:4 个 字节,5 6两个字节代表的是编译的副版本号 minor_version,而 7 8 两个字节是编译的主版本号 major_version -*** +* 不同版本的 Java 编译器编译的 Class 文件对应的版本是不一样的,高版本的 Java 虚拟机可以执行由低版本编译器生成的 Class 文件,反之 JVM 会抛出异常 `java.lang.UnsupportedClassVersionError` +| 主版本(十进制) | 副版本(十进制) | 编译器版本 | +| ---------------- | ---------------- | ---------- | +| 45 | 3 | 1.1 | +| 46 | 0 | 1.2 | +| 47 | 0 | 1.3 | +| 48 | 0 | 1.4 | +| 49 | 0 | 1.5 | +| 50 | 0 | 1.6 | +| 51 | 0 | 1.7 | +| 52 | 0 | 1.8 | +| 53 | 0 | 1.9 | +| 54 | 0 | 1.10 | +| 55 | 0 | 1.11 | +![](https://gitee.com/seazean/images/raw/master/Java/JVM-类结构.png) -###### return -* 吞异常 - ```java - public static int test() { - try { - return 10; - } finally { - return 20; - } - } - ``` +图片来源:https://www.bilibili.com/video/BV1PJ411n7xZ - ```java - 0: bipush 10 // 10 放入栈顶 - 2: istore_0 // 10 -> slot 0 【从栈顶移除了】 - 3: bipush 20 // 20 放入栈顶 - 5: ireturn // 返回栈顶 int(20) - 6: astore_1 // catch any 存入局部变量表的 slot1 - 7: bipush 20 // 20 放入栈顶 - 9: ireturn // 返回栈顶 int(20) - Exception table: - from to target type - 0 3 6 any - ``` - * 由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果以 finally 的为准 - * 字节码中没有 **athrow** ,表明如果在 finally 中出现了 return,会**吞掉异常** -* 不吞异常 +*** - ```java - public class Demo { - public static void main(String[] args) { - int result = test(); - System.out.println(result);//10 - } - public static int test() { - int i = 10; - try { - return i;//返回10 - } finally { - i = 20; - } - } - } - ``` - - ```java - 0: bipush 10 // 10 放入栈顶 - 2: istore_0 // 10 赋值给i,放入slot 0 - 3: iload_0 // i(10)加载至操作数栈 - 4: istore_1 // 10 -> slot 1,【暂存至 slot 1,目的是为了固定返回值】 - 5: bipush 20 // 20 放入栈顶 - 7: istore_0 // 20 slot 0 - 8: iload_1 // slot 1(10) 载入 slot 1 暂存的值 - 9: ireturn // 返回栈顶的 int(10) - 10: astore_2 // catch any -> slot 2 存入局部变量表的 slot2 - 11: bipush 20 - 13: istore_0 - 14: aload_2 - 15: athrow // 不会吞掉异常 - Exception table: - from to target type - 3 5 10 any - ``` +##### 常量池 -*** +常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项 u2 类型的无符号数,代表常量池计数器(constant_pool_count),这个容量计数是从 1 而不是 0 开始,是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达不引用任何一个常量池项目,这种情况可用索引值 0 来表示 +constant_pool 是一种表结构,以1 ~ constant_pool_count - 1为索引,表明有多少个常量池表项。表项中存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池 +* 字面量(Literal) :基本数据类型、字符串类型常量、声明为 final 的常量值等 -##### 同步控制 +* 符号引用(Symbolic References):类和接口的全限定名、字段的名称和描述符、方法的名称和描述符 -方法级的同步:是隐式的,无须通过字节码指令来控制,它实现在方法调用和返回操作之中,虚拟机可以从方法常量池的方法表结构中的 ACC_SYNCHRONIZED 访问标志得知一个方法是否声明为同步方法 + * 全限定名:com/test/Demo 这个就是类的全限定名,仅仅是把包名的 `.` 替换成 `/`,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个 `;` 表示全限定名结束 -方法内指定指令序列的同步:有 monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字的语义 + * 简单名称:指没有类型和参数修饰的方法或者字段名称,比如字段 x 的简单名称就是 x -* montiorenter:进入并获取对象监视器,即为栈顶对象加锁 -* monitorexit:释放并退出对象监视器,即为栈顶对象解锁 + * 描述符:用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值 - + | 标志符 | 含义 | + | ------ | --------------------------------------------------------- | + | B | 基本数据类型 byte | + | C | 基本数据类型 char | + | D | 基本数据类型 double | + | F | 基本数据类型 float | + | I | 基本数据类型 int | + | J | 基本数据类型 long | + | S | 基本数据类型 short | + | Z | 基本数据类型 boolean | + | V | 代表 void 类型 | + | L | 对象类型,比如:`Ljava/lang/Object;`,不同方法间用`;`隔开 | + | [ | 数组类型,代表一维数组。比如:`double[][][] is [[[D` | +常量类型和结构: +| 类型 | 标志(或标识) | 描述 | +| -------------------------------- | ------------ | ---------------------- | +| CONSTANT_utf8_info | 1 | UTF-8编码的字符串 | +| CONSTANT_Integer_info | 3 | 整型字面量 | +| CONSTANT_Float_info | 4 | 浮点型字面量 | +| CONSTANT_Long_info | 5 | 长整型字面量 | +| CONSTANT_Double_info | 6 | 双精度浮点型字面量 | +| CONSTANT_Class_info | 7 | 类或接口的符号引用 | +| CONSTANT_String_info | 8 | 字符串类型字面量 | +| CONSTANT_Fieldref_info | 9 | 字段的符号引用 | +| CONSTANT_Methodref_info | 10 | 类中方法的符号引用 | +| CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 | +| CONSTANT_NameAndType_info | 12 | 字段或方法的符号引用 | +| CONSTANT_MethodHandle_info | 15 | 表示方法句柄 | +| CONSTANT_MethodType_info | 16 | 标志方法类型 | +| CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 | +18 种常量没有出现 byte、short、char,boolean 的原因:编译之后都可以理解为 Integer -*** +**** -#### 执行流程 -原始 Java 代码: +##### 访问标识 -```java -public class Demo { - public static void main(String[] args) { - int a = 10; - int b = Short.MAX_VALUE + 1; - int c = a + b; - System.out.println(c); - } -} -``` +访问标识(access_flag),又叫访问标志、访问标记,该标识用两个字节表示,用于识别一些类或者接口层次的访问信息,包括这个 Class 是类还是接口,是否定义为 public类型,是否定义为 abstract类型等 -javap -v Demo.class:省略 +* 类的访问权限通常为 ACC_ 开头的常量 +* 每一种类型的表示都是通过设置访问标记的 32 位中的特定位来实现的,比如若是 public final 的类,则该标记为 `ACC_PUBLIC | ACC_FINAL` +* 使用 `ACC_SUPER` 可以让类更准确地定位到父类的方法,确定类或接口里面的 invokespecial 指令使用的是哪一种执行语义,现代编译器都会设置并且使用这个标记 -* 常量池载入运行时常量池 +| 标志名称 | 标志值 | 含义 | +| -------------- | ------ | ------------------------------------------------------------ | +| ACC_PUBLIC | 0x0001 | 标志为 public 类型 | +| ACC_FINAL | 0x0010 | 标志被声明为 final,只有类可以设置 | +| ACC_SUPER | 0x0020 | 标志允许使用 invokespecial 字节码指令的新语义,JDK1.0.2之后编译出来的类的这个标志默认为真,使用增强的方法调用父类方法 | +| ACC_INTERFACE | 0x0200 | 标志这是一个接口 | +| ACC_ABSTRACT | 0x0400 | 是否为 abstract 类型,对于接口或者抽象类来说,次标志值为真,其他类型为假 | +| ACC_SYNTHETIC | 0x1000 | 标志此类并非由用户代码产生(由编译器产生的类,没有源码对应) | +| ACC_ANNOTATION | 0x2000 | 标志这是一个注解 | +| ACC_ENUM | 0x4000 | 标志这是一个枚举 | -* 方法区字节码载入方法区 -* main 线程开始运行,分配栈帧内存:(操作数栈stack=2,局部变量表locals=4) -* **执行引擎**开始执行字节码 +*** - `bipush 10`:将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令 - * sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节) - * ldc 将一个 int 压入操作数栈 - * ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节) - * 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池 - `istore_1`:将操作数栈顶数据弹出,存入局部变量表的 slot 1 +##### 索引集合 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程1.png) +类索引、父类索引、接口索引集合 - `ldc #3`:从常量池加载 #3 数据到操作数栈 - Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算完成 +* 类索引用于确定这个类的全限定名 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程2.png) +* 父类索引用于确定这个类的父类的全限定名,Java 语言不允许多重继承,所以父类索引只有一个,除了Object 之外,所有的 Java 类都有父类,因此除了 java.lang.Object 外,所有 Java 类的父类索引都不为0 - `istore_2`:将操作数栈顶数据弹出,存入局部变量表的 slot 2 +* 接口索引集合就用来描述这个类实现了哪些接口 + * interfaces_count 项的值表示当前类或接口的直接超接口数量 + * interfaces[] 接口索引集合,被实现的接口将按 implements 语句后的接口顺序从左到右排列在接口索引集合中 - `iload_1`:将局部变量表的 slot 1数据弹出,放入操作数栈栈顶 +| 长度 | 含义 | +| ---- | ---------------------------- | +| u2 | this_class | +| u2 | super_class | +| u2 | interfaces_count | +| u2 | interfaces[interfaces_count] | - `iload_2`:将局部变量表的 slot 2数据弹出,放入操作数栈栈顶 - `iadd`:执行相加操作 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程3.png) +*** - `istore_3`:将操作数栈顶数据弹出,存入局部变量表的 slot 3 - `getstatic #4`:获取静态字段 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程4.png) +##### 字段表 - `iload_3`: +字段 fields 用于描述接口或类中声明的变量,包括类变量以及实例变量,但不包括方法内部、代码块内部声明的局部变量以及从父类或父接口继承。字段叫什么名字、被定义为什么数据类型,都是无法固定的,只能引用常量池中的常量来描述 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程5.png) +fields_count(字段计数器),表示当前 class 文件 fields 表的成员个数,用两个字节来表示 - `invokevirtual #5`: +fields[](字段表): - * 找到常量池 #5 项 - * 定位到方法区 java/io/PrintStream.println:(I)V 方法 - * **生成新的栈帧**(分配 locals、stack等) - * 传递参数,执行新栈帧中的字节码 - * 执行完毕,弹出栈帧 - * 清除 main 操作数栈内容 +* 表中的每个成员都是一个 fields_info 结构的数据项,用于表示当前类或接口中某个字段的完整描述 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程6.png) +* 字段访问标识: - return:完成 main 方法调用,弹出 main 栈帧,程序结束 + | 标志名称 | 标志值 | 含义 | + | ------------- | ------ | -------------------------- | + | ACC_PUBLIC | 0x0001 | 字段是否为public | + | ACC_PRIVATE | 0x0002 | 字段是否为private | + | ACC_PROTECTED | 0x0004 | 字段是否为protected | + | ACC_STATIC | 0x0008 | 字段是否为static | + | ACC_FINAL | 0x0010 | 字段是否为final | + | ACC_VOLATILE | 0x0040 | 字段是否为volatile | + | ACC_TRANSTENT | 0x0080 | 字段是否为transient | + | ACC_SYNCHETIC | 0x1000 | 字段是否为由编译器自动产生 | + | ACC_ENUM | 0x4000 | 字段是否为enum | - +* 字段名索引:根据该值查询常量池中的指定索引项即可 +* 描述符索引:用来描述字段的数据类型、方法的参数列表和返回值 + | 字符 | 类型 | 含义 | + | ----------- | --------- | ----------------------- | + | B | byte | 有符号字节型树 | + | C | char | Unicode字符,UTF-16编码 | + | D | double | 双精度浮点数 | + | F | float | 单精度浮点数 | + | I | int | 整型数 | + | J | long | 长整数 | + | S | short | 有符号短整数 | + | Z | boolean | 布尔值true/false | + | V | void | 代表void类型 | + | L Classname | reference | 一个名为Classname的实例 | + | [ | reference | 一个一维数组 | -*** +* 属性表集合:属性个数存放在 attribute_count 中,属性具体内容存放在 attribute 数组中,一个字段还可能拥有一些属性,用于存储更多的额外信息,比如初始化值、一些注释信息等 + ```java + ConstantValue_attribute{ + u2 attribute_name_index; + u4 attribute_length; + u2 constantvalue_index; + } + ``` + 对于常量属性而言,attribute_length 值恒为2 -### 执行引擎 -#### 基本介绍 -执行引擎:Java 虚拟机的核心组成部分之一,类加载主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,需要执行引擎将字节码指令解释/编译为对应平台上的本地机器指令 +*** -虚拟机是一个相对于物理机的概念,这两种机器都有代码执行能力: -* 物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上 -* 虚拟机的执行引擎是由软件自行实现的,可以不受物理条件制约地定制指令集与执行引擎的结构体系 -Java 是**半编译半解释型语言**,将解释执行与编译执行二者结合起来进行: +##### 方法表 -* 解释器:根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容翻译为对应平台的本地机器指令执行 -* 即时编译器(JIT : Just In Time Compiler):虚拟机运行时将源代码直接编译成**和本地机器平台相关的机器码**后再执行,并存入 Code Cache,下次遇到相同的代码直接执行,效率高 +方法表是 methods 指向常量池索引集合,其中每一个 method_info 项都对应着一个类或者接口中的方法信息,完整描述了每个方法的签名 +* 如果这个方法不是抽象的或者不是 native 的,字节码中就会体现出来 +* methods 表只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法 +* methods 表可能会出现由编译器自动添加的方法,比如初始化方法 和实例化方法 +**重载(Overload)**一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,因为返回值不会包含在特征签名之中,因此 Java 语言里无法仅仅依靠返回值的不同来对一个已有方法进行重载。但在 Class 文件格式中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法就可以共存 -*** +methods_count(方法计数器):表示 class 文件 methods 表的成员个数,使用两个字节来表示 +methods[](方法表):每个表项都是一个 method_info 结构,表示当前类或接口中某个方法的完整描述 +* 方法表结构如下: -#### 执行方式 + | 类型 | 名称 | 含义 | 数量 | + | -------------- | ---------------- | ---------- | ---------------- | + | u2 | access_flags | 访问标志 | 1 | + | u2 | name_index | 字段名索引 | 1 | + | u2 | descriptor_index | 描述符索引 | 1 | + | u2 | attrubutes_count | 属性计数器 | 1 | + | attribute_info | attributes | 属性集合 | attributes_count | -HotSpot VM 采用**解释器与即时编译器并存的架构**,解释器和即时编译器能够相互协作,去选择最合适的方式来权衡编译本地代码和直接解释执行代码的时间 +* 方法表访问标志: -HostSpot JVM 的默认执行方式: + | 标志名称 | 标志值 | 含义 | + | ------------- | ------ | -------------------------- | + | ACC_PUBLIC | 0x0001 | 字段是否为 public | + | ACC_PRIVATE | 0x0002 | 字段是否为 private | + | ACC_PROTECTED | 0x0004 | 字段是否为 protected | + | ACC_STATIC | 0x0008 | 字段是否为 static | + | ACC_FINAL | 0x0010 | 字段是否为 final | + | ACC_VOLATILE | 0x0040 | 字段是否为 volatile | + | ACC_TRANSTENT | 0x0080 | 字段是否为 transient | + | ACC_SYNCHETIC | 0x1000 | 字段是否为由编译器自动产生 | + | ACC_ENUM | 0x4000 | 字段是否为 enum | -* 当程序启动后,解释器可以马上发挥作用立即执行,省去编译器编译的时间(解释器存在的**必要性**) -* 随着程序运行时间的推移,即时编译器逐渐发挥作用,根据热点探测功能,将有价值的字节码编译为本地机器指令,以换取更高的程序执行效率 -HotSpot VM 可以通过 VM 参数设置程序执行方式: -- -Xint:完全采用解释器模式执行程序 -- -Xcomp:完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行 -- -Xmixed:采用解释器 + 即时编译器的混合模式共同执行程序 +*** -![](https://gitee.com/seazean/images/raw/master/Java/JVM-执行引擎工作流程.png) +##### 属性表 -*** +属性表集合,指的是 Class 文件所携带的辅助信息,比如该 Class 文件的源文件的名称,以及任何带有 `RetentionPolicy.CLASS` 或者 `RetentionPolicy.RUNTIME` 的注解,这类信息通常被用于 Java 虚拟机的验证和运行,以及 Java 程序的调试。字段表、方法表都可以有自己的属性表,用于描述某些场景专有的信息 +attributes_ count(属性计数器):表示当前文件属性表的成员个数 +attributes[](属性表):属性表的每个项的值必须是 attribute_info 结构 -#### 热点探测 +* 属性的通用格式: -热点代码:被 JIT 编译器编译的字节码,根据代码被调用执行的频率而定,一个被多次调用的方法或者一个循环次数较多的循环体都可以被称之为热点代码 + ```java + ConstantValue_attribute{ + u2 attribute_name_index; //属性名索引 + u4 attribute_length; //属性长度 + u2 attribute_info; //属性表 + } + ``` -热点探测:JIT 编译器在运行时会针热点代码做出深度优化,将其直接编译为对应平台的本地机器指令进行缓存,以提升程序执行性能 +* 属性类型: -JIT 编译在默认情况是异步进行的,当触发某方法或某代码块的优化时,先将其放入编译队列,然后由编译线程进行编译,编译之后的代码放在 CodeCache 中,通过 `-XX:-BackgroundCompilation` 参数可以关闭异步编译 + | 属性名称 | 使用位置 | 含义 | + | ------------------------------------- | ------------------ | ------------------------------------------------------------ | + | Code | 方法表 | Java 代码编译成的字节码指令 | + | ConstantValue | 字段表 | final 关键字定义的常量池 | + | Deprecated | 类、方法、字段表 | 被声明为 deprecated 的方法和字段 | + | Exceptions | 方法表 | 方法抛出的异常 | + | EnclosingMethod | 类文件 | 仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法 | + | InnerClass | 类文件 | 内部类列表 | + | LineNumberTable | Code 属性 | Java 源码的行号与字节码指令的对应关系 | + | LocalVariableTable | Code 属性 | 方法的局部变量描述 | + | StackMapTable | Code 属性 | JDK1.6 中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配 | + | Signature | 类,方法表,字段表 | 用于支持泛型情况下的方法签名 | + | SourceFile | 类文件 | 记录源文件名称 | + | SourceDebugExtension | 类文件 | 用于存储额外的调试信息 | + | Syothetic | 类,方法表,字段表 | 标志方法或字段为编泽器自动生成的 | + | LocalVariableTypeTable | 类 | 使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 | + | RuntimeVisibleAnnotations | 类,方法表,字段表 | 为动态注解提供支持 | + | RuntimelnvisibleAnnotations | 类,方法表,字段表 | 用于指明哪些注解是运行时不可见的 | + | RuntimeVisibleParameterAnnotation | 方法表 | 作用与 RuntimeVisibleAnnotations 属性类似,只不过作用对象为方法 | + | RuntirmelnvisibleParameterAnniotation | 方法表 | 作用与 RuntimelnvisibleAnnotations 属性类似,作用对象哪个为方法参数 | + | AnnotationDefauit | 方法表 | 用于记录注解类元素的默认值 | + | BootstrapMethods | 类文件 | 用于保存 invokeddynanic 指令引用的引导方式限定符 | -* CodeCache 用于缓存编译后的机器码、动态生成的代码和本地方法代码 JNI -* 如果 CodeCache 区域被占满,编译器被停用,字节码将不会编译为机器码,应用程序继续运行,但运行性能会降低很多 -HotSpot VM 采用的热点探测方式是基于计数器的热点探测,为每一个方法都建立 2 个不同类型的计数器:方法调用计数器(Invocation Counter)和回边计数器(BackEdge Counter) -* 方法调用计数器:用于统计方法被调用的次数,默认阈值在 Client 模式 下是 1500 次,在 Server 模式下是 10000 次(需要进行激进的优化),超过这个阈值,就会触发 JIT 编译,阈值可以通过虚拟机参数 `-XX:CompileThreshold` 设置 - 工作流程:当一个方法被调用时, 会先检查该方法是否存在被 JIT 编译过的版本,存在则使用编译后的本地代码来执行;如果不存在则将此方法的调用计数器值加 1,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阈值,如果超过阈值会向即时编译器**提交一个该方法的代码编译请求** -* 回边计数器:统计一个方法中循环体代码执行的次数,在字节码中控制流向后跳转的指令称为回边 +**** - 如果一个方法中的循环体需要执行多次,可以优化为为栈上替换,简称 OSR (On StackReplacement) 编译,**OSR 替换循环代码体的入口,C1、C2 替换的是方法调用的入口**,OSR 编译后会出现方法的整段代码被编译了,但是只有循环体部分才执行编译后的机器码,其他部分仍是解释执行 +#### 编译指令 -*** +##### javac +javac:编译命令,将 java 源文件编译成 class 字节码文件 +`javac xx.java` 不会在生成对应的局部变量表等信息,使用 `javac -g xx.java` 可以生成所有相关信息 -#### 分层编译 -HotSpot VM 内嵌两个 JIT 编译器,分别为 Client Compiler 和 Server Compiler,简称 C1 编译器和 C2 编译器 -C1 编译器会对字节码进行简单可靠的优化,耗时短,以达到更快的编译速度,C1 编译器的优化方法: +**** -* 方法内联:**将调用的函数代码编译到调用点处**,这样可以减少栈帧的生成,减少参数传递以及跳转过程 - 方法内联能够消除方法调用的固定开销,任何方法除非被内联,否则调用都会有固定开销,来源于保存程序在该方法中的执行位置,以及新建、压入和弹出新方法所使用的栈帧。 - ```java - private static int square(final int i) { - return i * i; - } - System.out.println(square(9)); - ``` +##### javap - square 是热点方法,会进行内联,把方法内代码拷贝粘贴到调用者的位置: +javap 反编译生成的字节码文件,根据 class 字节码文件,反解析出当前类对应的 code 区 (字节码指令)、局部变量表、异常表和代码行偏移量映射表、常量池等信息 - ```java - System.out.println(9 * 9); - ``` +用法:javap - 还能够进行常量折叠(constant folding)的优化: +```sh +-help --help -? 输出此用法消息 +-version 版本信息 +-public 仅显示公共类和成员 +-protected 显示受保护的/公共类和成员 +-package 显示程序包/受保护的/公共类和成员 (默认) +-p -private 显示所有类和成员 + #常用的以下三个 +-v -verbose 输出附加信息 +-l 输出行号和本地变量表 +-c 对代码进行反汇编 #反编译 - ```java - System.out.println(81); - ``` +-s 输出内部类型签名 +-sysinfo 显示正在处理的类的系统信息 (路径, 大小, 日期, MD5 散列) +-constants 显示最终常量 +-classpath 指定查找用户类文件的位置 +-cp 指定查找用户类文件的位置 +-bootclasspath 覆盖引导类文件的位置 +``` -* 冗余消除:根据运行时状况进行代码折叠或削除 -* 内联缓存:是一种加快动态绑定的优化技术(方法调用部分详解) -C2 编译器进行耗时较长的优化以及激进优化,优化的代码执行效率更高,当激进优化的假设不成立时,再退回使用 C1 编译,这也是使用分层编译的原因 +*** -C2 的优化主要是在全局层面,逃逸分析是优化的基础:标量替换、栈上分配、同步消除 -VM 参数设置: -- -client:指定 Java 虚拟机运行在 Client 模式下,并使用 C1 编译器 -- -server:指定 Java 虚拟机运行在 Server 模式下,并使用 C2 编译器 -- `-server -XX:+TieredCompilation`:在 1.8 之前,分层编译默认是关闭的,可以添加该参数开启 +#### 指令集 -分层编译策略 (Tiered Compilation):程序解释执行可以触发 C1 编译,将字节码编译成机器码,加上性能监控,C2 编译会根据性能监控信息进行激进优化,JVM 将执行状态分成了 5 个层次: +##### 执行指令 -* 0 层,解释执行(Interpreter) +Java 字节码属于 JVM 基本执行指令。由一个字节长度的代表某种操作的操作码(opcode)以及零至多个代表此操作所需参数的操作数(operand)所构成,虚拟机中许多指令并不包含操作数,只有一个操作码(零地址指令) -* 1 层,使用 C1 即时编译器编译执行(不带 profiling) +由于限制了 Java 虚拟机操作码的长度为一个字节(0~255),所以指令集的操作码总数不可能超过 256 条 -* 2 层,使用 C1 即时编译器编译执行(带基本的 profiling) +在 JVM 的指令集中,大多数的指令都包含了其操作所对应的数据类型信息。例如 iload 指令用于从局部变量表中加载 int 型的数据到操作数栈中,而 fload 指令加载的则是 float 类型的数据 -* 3 层,使用 C1 即时编译器编译执行(带完全的 profiling) +* i 代表对 int 类型的数据操作 +* l 代表 long +* s 代表 short +* b 代表 byte +* c 代表 char +* f 代表 float +* d 代表 double -* 4 层,使用 C2 即时编译器编译执行(C1 和 C2 协作运行) +大部分的指令都没有支持 byte、char、short、boolean 类型,编译器会在编译期或运行期将 byte 和 short 类型的数据带符号扩展(Sign-Extend-)为相应的 int 类型数据,将 boolean 和 char 类型数据零位扩展(Zero-Extend-)为相应的 int 类型数据 - 说明:profiling 是指在运行过程中收集一些程序执行状态的数据,例如方法的调用次数,循环的回边次数等 +在做值相关操作时: +- 一个指令,可以从局部变量表、常量池、堆中对象、方法调用、系统调用中等取得数据,这些数据(可能是值,也可能是对象的引用)被压入操作数栈 +- 一个指令,也可以从操作数栈中取出一到多个值(pop 多次),完成赋值、加减乘除、方法传参、系统调用等等操作 -参考文章:https://www.jianshu.com/p/20bd2e9b1f03 +*** -*** +##### 加载存储 +加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传递 -### 方法调用 +局部变量压栈指令:将给定的局部变量表中的数据压入操作数栈 -#### 方法识别 +* xload、xload_n,x 表示取值数据类型,为 i、l、f、d、a, n 为 0 到 3 +* 指令 xload_n 表示将第 n 个局部变量压入操作数栈,aload_n 表示将一个对象引用压栈 +* 指令 xload n 通过指定参数的形式,把局部变量压入操作数栈,局部变量数量超过 4 个时使用这个命令 -Java 虚拟机识别方法的关键在于类名、方法名以及方法描述符(method descriptor) +常量入栈指令:将常数压入操作数栈,根据数据类型和入栈内容的不同,又分为 const、push、ldc指令 -* **方法描述符是由方法的参数类型以及返回类型所构成**,Java 层面叫方法特征签名 -* 在同一个类中,如果同时出现多个名字相同且描述符也相同的方法,那么 Java 虚拟机会在类的验证阶段报错 +* push:包括 bipush 和 sipush,区别在于接收数据类型的不同,bipush 接收 8 位整数作为参数,sipush 接收 16 位整数 +* ldc:如果以上指令不能满足需求,可以使用 ldc 指令,接收一个 8 位的参数,该参数指向常量池中的 int、 float 或者 String 的索引,将指定的内容压入堆栈。ldc_w 接收两个 8 位参数,能支持的索引范围更大,如果要压入的元素是 long 或 double 类型的,则使用 ldc2_w 指令 +* aconst_null 将 null 对象引用压入栈,iconst_m1 将 int 类型常量 -1 压入栈,iconst_0 将 int 类型常量 0 压入栈 -JVM 根据名字和描述符来判断的,只要返回值不一样(方法描述符不一样),其它完全一样,在 JVM 中是允许的,但 Java 语言不允许 +出栈装入局部变量表指令:将操作数栈中栈顶元素弹出后,装入局部变量表的指定位置,用于给局部变量赋值 -```java -// 返回值类型不同,编译阶段直接报错 -public static Integer invoke(Object... args) { - return 1; -} -public static int invoke(Object... args) { - return 2; -} -``` +* xstore、xstore_n,x 表示取值类型为 i、l、f、d、a, n 为 0 到 3 +* xastore 表示存入数组,x 取值为 i、l、f、d、a、b、c、s +扩充局部变量表的访问索引的指令:wide -*** +**** -#### 调用机制 -方法调用并不等于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本,不是方法的具体运行过程 +##### 算术指令 -在 JVM 中,将符号引用转换为直接引用有两种机制: +算术指令用于对两个操作数栈上的值进行某种特定运算,并把计算结果重新压入操作数栈 -- 静态链接:当一个字节码文件被装载进 JVM 内部时,如果被调用的目标方法在编译期可知,且运行期保持不变,将调用方法的符号引用转换为直接引用的过程称之为静态链接(类加载的解析阶段) -- 动态链接:如果被调用的方法在编译期无法被确定下来,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此被称为动态链接(初始化后的解析阶段) +没有直接支持 byte、 short、 char 和 boolean类型的算术指令,对于这些数据的运算,都使用 int 类型的指令来处理,数组类型也是转换成 int 数组 -对应方法的绑定(分配)机制:静态绑定和动态绑定。绑定是一个字段、方法或者类从符号引用被替换为直接引用的过程,仅发生一次: +* 加法指令:iadd、ladd、fadd、dadd +* 减法指令:isub、lsub、fsub、dsub +* 乘法指令:imu、lmu、fmul、dmul +* 除法指令:idiv、ldiv、fdiv、ddiv +* 求余指令:irem、lrem、frem、drem(remainder 余数) +* 取反指令:ineg、lneg、fneg、dneg (negation 取反) +* 自增指令:iinc(直接**在局部变量 slot 上进行运算**,不用放入操作数栈) +* 位运算指令,又可分为: + - 位移指令:ishl、ishr、 iushr、lshl、lshr、 lushr + - 按位或指令:ior、lor + - 按位与指令:iand、land + - 按位异或指令:ixor、lxor -- 静态绑定:被调用的目标方法在编译期可知,且运行期保持不变,将这个方法与所属的类型进行绑定 -- 动态绑定:被调用的目标方法在编译期无法确定,只能在程序运行期根据实际的类型绑定相关的方法 +* 比较指令:dcmpg、dcmpl、 fcmpg、fcmpl、lcmp -* Java 编译器已经区分了重载的方法(静态绑定和动态绑定),因此可以认为虚拟机中不存在重载 +运算模式: -非虚方法: +* 向最接近数舍入模式,JVM 在进行浮点数计算时,所有的运算结果都必须舍入到适当的精度,非精确结果必须舍入为可被表示的最接近的精确值,如果有两种可表示形式与该值一样接近,将优先选择最低有效位为零的 +* 向零舍入模式:将浮点数转换为整数时,该模式将在目标数值类型中选择一个最接近但是不大于原值的数字作为最精确的舍入结果 -- 非虚方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的 -- 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法 -- 所有普通成员方法、实例方法、被重写的方法都是虚方法 +NaN 值:当一个操作产生溢出时,将会使用有符号的无穷大表示,如果某个操作结果没有明确的数学定义,将使用 NaN 值来表示 -动态类型语言和静态类型语言: +```java +double j = i / 0.0; +System.out.println(j);//无穷大,NaN: not a number +``` -- 在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之则是动态类型语言 +**分析 i++**:从字节码角度分析:a++ 和 ++a 的区别是先执行 iload 还是 先执行 iinc -- 静态语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息 +```java + 4 iload_1 //存入操作数栈 + 5 iinc 1 by 1 //自增i++ + 8 istore_3 //把操作数栈没有自增的数据的存入局部变量表 + 9 iinc 2 by 1 //++i +12 iload_2 //加载到操作数栈 +13 istore 4 //存入局部变量表,这个存入没有 _ 符号,_只能到3 +``` -- **Java 是静态类型语言**(尽管 Lambda 表达式为其增加了动态特性),JS,Python 是动态类型语言 +```java +public class Demo { + public static void main(String[] args) { + int a = 10; + int b = a++ + ++a + a--; + System.out.println(a); //11 + System.out.println(b); //34 + } +} +``` - ```java - String s = "abc"; //Java - info = "abc"; //Python - ``` +判断结果: + +```java +public class Demo { + public static void main(String[] args) { + int i = 0; + int x = 0; + while (i < 10) { + x = x++; + i++; + } + System.out.println(x); // 结果是 0 + } +} +``` @@ -13833,34 +13918,36 @@ public static int invoke(Object... args) { -#### 调用指令 - -##### 五种指令 +##### 类型转换 -普通调用指令: +类型转换指令可以将两种不同的数值类型进行相互转换,除了 boolean 之外的七种类型 -- invokestatic:调用静态方法 -- invokespecial:调用私有实例方法、构造器,和父类的实例方法或构造器,以及所实现接口的默认方法 -- invokevirtual:调用所有虚方法(虚方法分派) -- invokeinterface:调用接口方法 +宽化类型转换: -动态调用指令: +* JVM 支持以下数值的宽化类型转换(widening numeric conversion),小范围类型到大范围类型的安全转换 + * 从 int 类型到 long、float 或者 double 类型,对应的指令为 i2l、i2f、i2d + * 从 long 类型到 float、 double 类型,对应的指令为 l2f、l2d + * 从 float 类型到 double 类型,对应的指令为 f2d -- invokedynamic:动态解析出需要调用的方法 - - Java7 为了实现动态类型语言支持而引入了该指令,但是并没有提供直接生成 invokedynamic 指令的方法,需要借助 ASM 这种底层字节码工具来产生 invokedynamic 指令 - - Java8 的 lambda 表达式的出现,invokedynamic 指令在 Java 中才有了直接生成方式 +* 精度损失问题 + * 宽化类型转换是不会因为超过目标类型最大值而丢失信息 + * 从 int 转换到 float 或者 long 类型转换到 double 时,将可能发生精度丢失 -指令对比: +* 从 byte、char 和 short 类型到 int 类型的宽化类型转换实际上是不存在的,JVM 把它们当作 int 处理 -- 普通调用指令固化在虚拟机内部,方法的调用执行不可干预,根据方法的符号引用链接到具体的目标方法 -- 动态调用指令支持用户确定方法 -- invokestatic 和 invokespecial 指令调用的方法称为非虚方法,虚拟机能够直接识别具体的目标方法 -- invokevirtual 和 invokeinterface 指令调用的方法称为虚方法,虚拟机需要在执行过程中根据调用者的动态类型来确定目标方法 +窄化类型转换: -指令说明: +* Java 虚拟机直接支持以下窄化类型转换: + * 从 int 类型至 byte、 short 或者 char 类型,对应的指令有 i2b、i2c、i2s + * 从 long 类型到 int 类型,对应的指令有 l2i + * 从 float 类型到 int 或者 long 类型,对应的指令有:f2i、f2l + * 从 double 类型到 int、long 或 float 者类型,对应的指令有 d2i、d2、d2f -- 如果虚拟机能够确定目标方法有且仅有一个,比如说目标方法被标记为 final,那么可以不通过动态绑定,直接确定目标方法 -- 普通成员方法是由 invokevirtual 调用,属于**动态绑定**,即支持多态 +* 精度损失问题: + * 窄化类型转换可能会导致转换结果具备不同的正负号、不同的数量级,转换过程可能会导致数值丢失精度 + * 将一个浮点值窄化转换为整数类型 T(T 限于 int 或 long 类型之一)时,将遵循以下转换规则: + - 如果浮点值是 NaN,那转换结果就是 int 或 long 类型的 0 + - 如果浮点值不是无穷大的话,浮点值使用 IEEE 754 的向零舍入模式取整,获得整数值 v,如果 v 在目标类型 T 的表示范围之内,那转换结果就是 v,否则将根据 v 的符号,转换为 T 所能表示的最大或者最小正数 @@ -13868,89 +13955,52 @@ public static int invoke(Object... args) { -##### 符号引用 +##### 创建访问 -在编译过程中,虚拟机并不知道目标方法的具体内存地址,Java 编译器会暂时用符号引用来表示该目标方法,这一符号引用包括目标方法所在的类或接口的名字,以及目标方法的方法名和方法描述符 +创建指令: -* 对于静态绑定的方法调用而言,实际引用是一个指向方法的指针 -* 对于需要动态绑定的方法调用而言,实际引用则是一个方法表的索引 +* 创建类实例指令:new,接收一个操作数指向常量池的索引,表示要创建的类型,执行完成后将对象的引用压入栈 -符号引用存储在方法区常量池中,根据目标方法是否为接口方法,分为接口符号引用和非接口符号引用: + ```java + 0: new #2 // class com/jvm/bytecode/Demo + 3: dup + 4: invokespecial #3 // Method "":()V + ``` -```java -Constant pool: -... - #16 = InterfaceMethodref #27.#29 // 接口 -... - #22 = Methodref #1.#33 // 非接口 -... -``` + **dup 是复制操作数栈栈顶的内容**,需要两份引用原因: -对于非接口符号引用,假定该符号引用所指向的类为 C,则 Java 虚拟机会按照如下步骤进行查找: + - 一个要配合 invokespecial 调用该对象的构造方法 :()V (会消耗掉栈顶一个引用) + - 一个要配合 astore_1 赋值给局部变量 -1. 在 C 中查找符合名字及描述符的方法 -2. 如果没有找到,在 C 的父类中继续搜索,直至 Object 类 -3. 如果没有找到,在 C 所直接实现或间接实现的接口中搜索,这一步搜索得到的目标方法必须是非私有、非静态的。如果有多个符合条件的目标方法,则任意返回其中一个 +* 创建数组的指令:newarray、anewarray、multianewarray -对于接口符号引用,假定该符号引用所指向的接口为 I,则 Java 虚拟机会按照如下步骤进行查找: + * newarray:创建基本类型数组 + * anewarray:创建引用类型数组 + * multianewarray:创建多维数组 -1. 在 I 中查找符合名字及描述符的方法 -2. 如果没有找到,在 Object 类中的公有实例方法中搜索 -3. 如果没有找到,则在 I 的超接口中搜索,这一步的搜索结果的要求与非接口符号引用步骤 3 的要求一致 +字段访问指令:对象创建后可以通过对象访问指令获取对象实例或数组实例中的字段或者数组元素 +* 访问类字段(static字段,或者称为类变量)的指令:getstatic、putstatic +* 访问类实例字段(非static字段,或者称为实例变量)的指令:getfield、 putfield +类型检查指令:检查类实例或数组类型的指令 -*** +* checkcast:用于检查类型强制转换是否可以进行,如果可以进行 checkcast 指令不会改变操作数栈,否则它会抛出 ClassCastException 异常 +* instanceof:判断给定对象是否是某一个类的实例,会将判断结果压入操作数栈 -##### 执行流程 -```java -public class Demo { - public Demo() { } - private void test1() { } - private final void test2() { } - public void test3() { } - public static void test4() { } +**** - public static void main(String[] args) { - Demo3_9 d = new Demo3_9(); - d.test1(); - d.test2(); - d.test3(); - d.test4(); - Demo.test4(); - } -} -``` -几种不同的方法调用对应的字节码指令: -```java -0: new #2 // class cn/jvm/t3/bytecode/Demo -3: dup -4: invokespecial #3 // Method "":()V -7: astore_1 -8: aload_1 -9: invokespecial #4 // Method test1:()V -12: aload_1 -13: invokespecial #5 // Method test2:()V -16: aload_1 -17: invokevirtual #6 // Method test3:()V -20: aload_1 -21: pop -22: invokestatic #7 // Method test4:()V -25: invokestatic #7 // Method test4:()V -28: return -``` +##### 方法指令 -- invokespecial 调用该对象的构造方法 :()V +方法调用指令:invokevirtual、 invokeinterface、invokespecial、invokestatic、invokedynamic -- `d.test4()` 是通过**对象引用**调用一个静态方法,在调用 invokestatic 之前执行了 pop 指令,把对象引用从操作数栈弹掉 - - 不建议使用 `对象.静态方法()` 的方式调用静态方法,多了aload 和 pop 指令 - - 成员方法与静态方法调用的区别是:执行方法前是否需要对象引用 +**方法调用章节详解** @@ -13958,129 +14008,177 @@ public class Demo { -#### 多态原理 - -##### 执行原理 - -Java 虚拟机中关于方法重写的判定基于方法描述符,如果子类定义了与父类中非私有、非静态方法同名的方法,只有当这两个方法的参数类型以及返回类型一致,Java 虚拟机才会判定为重写。 - -理解多态: +##### 操作数栈 -- 多态有编译时多态和运行时多态,即静态绑定和动态绑定 -- 前者是通过方法重载实现,后者是通过重写实现(子类覆盖父类方法,虚方法表) -- 虚方法:运行时动态绑定的方法,对比静态绑定的非虚方法调用来说,虚方法调用更加耗时 +JVM 提供的操作数栈管理指令,可以用于直接操作操作数栈的指令 -方法重写的本质: +* pop、pop2:将一个或两个元素从栈顶弹出,并且直接废弃 +* dup、dup2,dup_x1、dup2_x1,dup_x2、dup2_x2:复制栈顶一个或两个数值并重新压入栈顶 -1. 找到操作数栈的第一个元素**所执行的对象的实际类型**,记作 C +* swap:将栈最顶端的两个 slot 数值位置交换,JVM 没有提供交换两个 64 位数据类型数值的指令 -2. 如果在类型 C 中找到与描述符和名称都相符的方法,则进行访问**权限校验**(私有的),如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回 java.lang.IllegalAccessError 异常 +* nop:一个非常特殊的指令,字节码为 0x00,和汇编语言中的 nop 一样,表示什么都不做,一般可用于调试、占位等 - IllegalAccessError:表示程序试图访问或修改一个属性或调用一个方法,这个属性或方法没有权限访问,一般会引起编译器异常。如果这个错误发生在运行时,就说明一个类发生了不兼容的改变 -3. 找不到,就会按照继承关系从下往上依次对 C 的各个父类进行第二步的搜索和验证过程 -4. 如果始终没有找到合适的方法,则抛出 java.lang.AbstractMethodError 异常 +*** -*** +##### 控制转移 +比较指令:比较栈顶两个元素的大小,并将比较结果入栈 -##### 虚方法表 +* lcmp:比较两个 long 类型值 +* fcmpl:比较两个 float 类型值(当遇到NaN时,返回-1) +* fcmpg:比较两个 float 类型值(当遇到NaN时,返回1) +* dcmpl:比较两个 double 类型值(当遇到NaN时,返回-1) +* dcmpg:比较两个 double 类型值(当遇到NaN时,返回1) -在虚拟机工作过程中会频繁使用到动态绑定,每次动态绑定的过程中都要重新在类的元数据中搜索合适目标,影响到执行效率。为了提高性能,JVM 采取了一种用**空间换取时间**的策略来实现动态绑定,在每个类的方法区建立一个虚方法表(virtual method table),实现使用索引表来代替查找,可以快速定位目标方法 +条件跳转指令: -* invokevirtual 所使用的虚方法表(virtual method table,vtable),执行流程 - 1. 先通过栈帧中的对象引用找到对象,分析对象头,找到对象的实际 Class - 2. Class 结构中有 vtable,查表得到方法的具体地址,执行方法的字节码 -* invokeinterface 所使用的接口方法表(interface method table,itable) +| 指令 | 说明 | +| --------- | -------------------------------------------------- | +| ifeq | equals,当栈顶int类型数值等于0时跳转 | +| ifne | not equals,当栈顶in类型数值不等于0时跳转 | +| iflt | lower than,当栈顶in类型数值小于0时跳转 | +| ifle | lower or equals,当栈顶in类型数值小于等于0时跳转 | +| ifgt | greater than,当栈顶int类型数组大于0时跳转 | +| ifge | greater or equals,当栈顶in类型数值大于等于0时跳转 | +| ifnull | 为 null 时跳转 | +| ifnonnull | 不为 null 时跳转 | -虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM 会把该类的方法表也初始化完毕 +比较条件跳转指令: -虚方法表的执行过程: +| 指令 | 说明 | +| --------- | --------------------------------------------------------- | +| if_icmpeq | 比较栈顶两 int 类型数值大小(下同),当前者等于后者时跳转 | +| if_icmpne | 当前者不等于后者时跳转 | +| if_icmplt | 当前者小于后者时跳转 | +| if_icmple | 当前者小于等于后者时跳转 | +| if_icmpgt | 当前者大于后者时跳转 | +| if_icmpge | 当前者大于等于后者时跳转 | +| if_acmpeq | 当结果相等时跳转 | +| if_acmpne | 当结果不相等时跳转 | -* 对于静态绑定的方法调用而言,实际引用将指向具体的目标方法 -* 对于动态绑定的方法调用而言,实际引用则是方法表的索引值,也就是方法的间接地址。Java 虚拟机获取调用者的实际类型,并在该实际类型的虚方法表中,根据索引值获得目标方法内存偏移量(指针) +多条件分支跳转指令: -为了优化对象调用方法的速度,方法区的类型信息会增加一个指针,该指针指向一个记录该类方法的方法表。每个类中都有一个虚方法表,本质上是一个数组,每个数组元素指向一个当前类及其祖先类中非私有的实例方法 +* tableswitch:用于 switch 条件跳转,case 值连续 +* lookupswitch:用于 switch 条件跳转,case 值不连续 -方法表满足以下的特质: +无条件跳转指令: -* 其一,子类方法表中包含父类方法表中的**所有方法**,并且在方法表中的索引值与父类方法表种的索引值相同 -* 其二,**非重写的方法指向父类的方法表项,与父类共享一个方法表项,重写的方法指向本身自己的实现**。所以这就是为什么多态情况下可以访问父类的方法。 +* goto:用来进行跳转到指定行号的字节码 - +* goto_w:无条件跳转(宽索引) -Passenger 类的方法表包括两个方法,分别对应 0 号和 1 号。方法表调换了 toString 方法和 passThroughImmigration 方法的位置,是因为 toString 方法的索引值需要与 Object 类中同名方法的索引值一致,为了保持简洁,这里不考虑 Object 类中的其他方法。 -虚方法表对性能的影响: -* 使用了方法表的动态绑定与静态绑定相比,仅仅多出几个内存解引用操作:访问栈上的调用者、读取调用者的动态类型、读取该类型的方法表、读取方法表中某个索引值所对应的目标方法,但是相对于创建并初始化 Java 栈帧这操作的开销可以忽略不计 -* 上述优化的效果看上去不错,但实际上**仅存在于解释执行**中,或者即时编译代码的最坏情况。因为即时编译还拥有另外两种性能更好的优化手段:内联缓存(inlining cache)和方法内联(method inlining) -```java -class Person { - public String toString() { - return "I'm a person."; - } - public void eat() {} - public void speak() {} -} -class Boy extends Person { - public String toString() { - return "I'm a boy"; - } - public void speak() {} - public void fight() {} -} +*** -class Girl extends Person { - public String toString() { - return "I'm a girl"; - } - public void speak() {} - public void sing() {} -} -``` -![](https://gitee.com/seazean/images/raw/master/Java/JVM-虚方法表指向.png) +##### 异常处理 +###### 处理机制 -参考文档:https://www.cnblogs.com/kaleidoscope/p/9790766.html +抛出异常指令:athrow 指令 +JVM 处理异常(catch 语句)不是由字节码指令来实现的,而是**采用异常表来完成**的 +* 代码: -*** + ```java + public static void main(String[] args) { + int i = 0; + try { + i = 10; + } catch (Exception e) { + i = 20; + } finally { + i = 30; + } + } + ``` + +* 字节码: + * 多出一个 **Exception table** 的结构,**[from, to) 是前闭后开的检测范围**,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号 + * 11 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置,因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用 + ```java + 0: iconst_0 + 1: istore_1 // 0 -> i ->赋值 + 2: bipush 10 // try 10 放入操作数栈顶 + 4: istore_1 // 10 -> i 将操作数栈顶数据弹出,存入局部变量表的 slot1 + 5: bipush 30 // 【finally】 + 7: istore_1 // 30 -> i + 8: goto 27 // return ----------------------------------- + 11: astore_2 // catch Exceptin -> e ---------------------- + 12: bipush 20 // + 14: istore_1 // 20 -> i + 15: bipush 30 // 【finally】 + 17: istore_1 // 30 -> i + 18: goto 27 // return ----------------------------------- + 21: astore_3 // catch any -> slot 3 ---------------------- + 22: bipush 30 // 【finally】 + 24: istore_1 // 30 -> i + 25: aload_3 // 将局部变量表的slot 3数据弹出,放入操作数栈栈顶 + 26: athrow // throw 抛出异常 + 27: return + Exception table: + // 任何阶段出现任务异常都会执行 finally + from to target type + 2 5 11 Class java/lang/Exception + 2 5 21 any // 剩余的异常类型,比如 Error + 11 15 21 any // 剩余的异常类型,比如 Error + LineNumberTable: ... + LocalVariableTable: + Start Length Slot Name Signature + 12 3 2 e Ljava/lang/Exception; + 0 28 0 args [Ljava/lang/String; + 2 26 1 i I + ``` -##### 内联缓存 -内联缓存:是一种加快动态绑定的优化技术,能够缓存虚方法调用中**调用者的动态类型以及该类型所对应的目标方法**。在之后的执行过程中,如果碰到已缓存的类型,便会直接调用该类型所对应的目标方法;反之内联缓存则会退化至使用基于方法表的动态绑定 -多态的三个术语: +*** -* 单态 (monomorphic):指的是仅有一种状态的情况 -* 多态 (polymorphic):指的是有限数量种状态的情况,二态(bimorphic)是多态的其中一种 -* 超多态 (megamorphic):指的是更多种状态的情况,通常用一个具体数值来区分多态和超多态,在这个数值之下,称之为多态,否则称之为超多态 -对于内联缓存来说,有对应的单态内联缓存、多态内联缓存: -* 单态内联缓存:只缓存了一种动态类型以及所对应的目标方法,实现简单,比较所缓存的动态类型,如果命中,则直接调用对应的目标方法。 -* 多态内联缓存:缓存了多个动态类型及其目标方法,需要逐个将所缓存的动态类型与当前动态类型进行比较,如果命中,则调用对应的目标方法 +###### finally -为了节省内存空间,**Java 虚拟机只采用单态内联缓存**,没有命中的处理方法: +finally 中的代码被**复制了 3 份**,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程(上节案例) -* 替换单态内联缓存中的纪录,类似于 CPU 中的数据缓存,对数据的局部性有要求,即在替换内联缓存之后的一段时间内,方法调用的调用者的动态类型应当保持一致,从而能够有效地利用内联缓存 -* 劣化为超多态状态,这也是 Java 虚拟机的具体实现方式,这种状态实际上放弃了优化的机会,将直接访问方法表来动态绑定目标方法,但是与替换内联缓存纪录相比节省了写缓存的额外开销 +* 代码: -虽然内联缓存附带内联二字,但是并没有内联目标方法 + ```java + public static int test() { + try { + return 10; + } finally { + return 20; + } + } + ``` +* 字节码: + ```java + 0: bipush 10 // 10 放入栈顶 + 2: istore_0 // 10 -> slot 0 【从栈顶移除了】 + 3: bipush 20 // 20 放入栈顶 + 5: ireturn // 返回栈顶 int(20) + 6: astore_1 // catch any 存入局部变量表的 slot1 + 7: bipush 20 // 20 放入栈顶 + 9: ireturn // 返回栈顶 int(20) + Exception table: + from to target type + 0 3 6 any + ``` @@ -14088,30 +14186,73 @@ class Girl extends Person { -### 代码优化 +###### return -#### 语法糖 - -语法糖:指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担 +* 吞异常 + ```java + public static int test() { + try { + return 10; + } finally { + return 20; + } + } + ``` + ```java + 0: bipush 10 // 10 放入栈顶 + 2: istore_0 // 10 -> slot 0 【从栈顶移除了】 + 3: bipush 20 // 20 放入栈顶 + 5: ireturn // 返回栈顶 int(20) + 6: astore_1 // catch any 存入局部变量表的 slot1 + 7: bipush 20 // 20 放入栈顶 + 9: ireturn // 返回栈顶 int(20) + Exception table: + from to target type + 0 3 6 any + ``` -#### 构造器 + * 由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果以 finally 的为准 + * 字节码中没有 **athrow** ,表明如果在 finally 中出现了 return,会**吞掉异常** -```java -public class Candy1 { -} -``` +* 不吞异常 -```java -public class Candy1 { - // 这个无参构造是编译器帮助我们加上的 - public Candy1() { - super(); // 即调用父类 Object 的无参构造方法,即调用 java/lang/Object." - ":()V - } -} -``` + ```java + public class Demo { + public static void main(String[] args) { + int result = test(); + System.out.println(result);//10 + } + public static int test() { + int i = 10; + try { + return i;//返回10 + } finally { + i = 20; + } + } + } + ``` + + ```java + 0: bipush 10 // 10 放入栈顶 + 2: istore_0 // 10 赋值给i,放入slot 0 + 3: iload_0 // i(10)加载至操作数栈 + 4: istore_1 // 10 -> slot 1,【暂存至 slot 1,目的是为了固定返回值】 + 5: bipush 20 // 20 放入栈顶 + 7: istore_0 // 20 slot 0 + 8: iload_1 // slot 1(10) 载入 slot 1 暂存的值 + 9: ireturn // 返回栈顶的 int(10) + 10: astore_2 // catch any -> slot 2 存入局部变量表的 slot2 + 11: bipush 20 + 13: istore_0 + 14: aload_2 + 15: athrow // 不会吞掉异常 + Exception table: + from to target type + 3 5 10 any + ``` @@ -14119,130 +14260,100 @@ public class Candy1 { -#### 拆装箱 +##### 同步控制 -```java -Integer x = 1; -int y = x; -``` +方法级的同步:是隐式的,无须通过字节码指令来控制,它实现在方法调用和返回操作之中,虚拟机可以从方法常量池的方法表结构中的 ACC_SYNCHRONIZED 访问标志得知一个方法是否声明为同步方法 -这段代码在 JDK 5 之前是无法编译通过的,必须改写为代码片段2: +方法内指定指令序列的同步:有 monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字的语义 -```java -Integer x = Integer.valueOf(1); -int y = x.intValue(); -``` +* montiorenter:进入并获取对象监视器,即为栈顶对象加锁 +* monitorexit:释放并退出对象监视器,即为栈顶对象解锁 -JDK5 以后编译阶段自动转换成上述片段 + -*** +*** -#### 泛型擦除 -泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行**泛型擦除**的动作,即泛型信息 -在编译为字节码之后就丢失了,实际的类型都**当做了 Object 类型**来处理: -```java -List list = new ArrayList<>(); -list.add(10); // 实际调用的是 List.add(Object e) -Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index); -``` +#### 执行流程 -编译器真正生成的字节码中,还要额外做一个类型转换的操作: +原始 Java 代码: ```java -// 需要将 Object 转为 Integer -Integer x = (Integer)list.get(0); +public class Demo { + public static void main(String[] args) { + int a = 10; + int b = Short.MAX_VALUE + 1; + int c = a + b; + System.out.println(c); + } +} ``` -如果前面的 x 变量类型修改为 int 基本类型那么最终生成的字节码是: - -```java -// 需要将 Object 转为 Integer, 并执行拆箱操作 -int x = ((Integer)list.get(0)).intValue(); -``` +javap -v Demo.class:省略 +* 常量池载入运行时常量池 +* 方法区字节码载入方法区 -*** +* main 线程开始运行,分配栈帧内存:(操作数栈stack=2,局部变量表locals=4) +* **执行引擎**开始执行字节码 + `bipush 10`:将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令 -#### 可变参数 + * sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节) + * ldc 将一个 int 压入操作数栈 + * ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节) + * 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池 -```java -public class Candy4 { - public static void foo(String... args) { - String[] array = args; // 直接赋值 - System.out.println(array); - } - public static void main(String[] args) { - foo("hello", "world"); - } -} -``` + `istore_1`:将操作数栈顶数据弹出,存入局部变量表的 slot 1 -可变参数 `String... args` 其实是 `String[] args` , java 编译器会在编译期间将上述代码变换为: + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程1.png) -```java -public static void main(String[] args) { - foo(new String[]{"hello", "world"}); -} -``` + `ldc #3`:从常量池加载 #3 数据到操作数栈 + Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算完成 -注意:如果调用了 `foo()` 则等价代码为 `foo(new String[]{})` ,创建了一个空的数组,而不会传递 null 进去 + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程2.png) + `istore_2`:将操作数栈顶数据弹出,存入局部变量表的 slot 2 + `iload_1`:将局部变量表的 slot 1数据弹出,放入操作数栈栈顶 -**** + `iload_2`:将局部变量表的 slot 2数据弹出,放入操作数栈栈顶 + `iadd`:执行相加操作 + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程3.png) -#### foreach + `istore_3`:将操作数栈顶数据弹出,存入局部变量表的 slot 3 -**数组的循环:** + `getstatic #4`:获取静态字段 -```java -int[] array = {1, 2, 3, 4, 5}; // 数组赋初值的简化写法也是语法糖 -for (int e : array) { - System.out.println(e); -} -``` + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程4.png) -编译后为循环取数: + `iload_3`: -```java -for(int i = 0; i < array.length; ++i) { - int e = array[i]; - System.out.println(e); -} -``` + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程5.png) -**集合的循环:** + `invokevirtual #5`: -```java -List list = Arrays.asList(1,2,3,4,5); -for (Integer i : list) { - System.out.println(i); -} -``` + * 找到常量池 #5 项 + * 定位到方法区 java/io/PrintStream.println:(I)V 方法 + * **生成新的栈帧**(分配 locals、stack等) + * 传递参数,执行新栈帧中的字节码 + * 执行完毕,弹出栈帧 + * 清除 main 操作数栈内容 -编译后转换为对迭代器的调用: + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程6.png) -```java -List list = Arrays.asList(1, 2, 3, 4, 5); -Iterator iter = list.iterator(); -while(iter.hasNext()) { - Integer e = (Integer)iter.next(); - System.out.println(e); -} -``` + return:完成 main 方法调用,弹出 main 栈帧,程序结束 -注意:foreach 循环写法,能够配合数组以及所有实现了 Iterable 接口的集合类一起使用,其中 Iterable 用来获取集合的迭代器 + @@ -14250,56 +14361,21 @@ while(iter.hasNext()) { -#### switch - -##### 字符串 - -从 JDK 开始,switch 可以作用于字符串和枚举类: +### 执行引擎 -```java -switch (str) { - case "hello": { - System.out.println("h"); - break; - } - case "world": { - System.out.println("w"); - break; - } -} -``` +#### 基本介绍 -注意:**switch 配合 String 和枚举使用时,变量不能为null** +执行引擎:Java 虚拟机的核心组成部分之一,类加载主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,需要执行引擎将字节码指令解释/编译为对应平台上的本地机器指令 -会被编译器转换为: +虚拟机是一个相对于物理机的概念,这两种机器都有代码执行能力: -```java -byte x = -1; -switch(str.hashCode()) { - case 99162322: // hello 的 hashCode - if (str.equals("hello")) { - x = 0; - } - break; - case 113318802: // world 的 hashCode - if (str.equals("world")) { - x = 1; - } -} -switch(x) { - case 0: - System.out.println("h"); - break; - case 1: - System.out.println("w"); - break; -} -``` +* 物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上 +* 虚拟机的执行引擎是由软件自行实现的,可以不受物理条件制约地定制指令集与执行引擎的结构体系 -总结: +Java 是**半编译半解释型语言**,将解释执行与编译执行二者结合起来进行: -* 执行了两遍 switch,第一遍是根据字符串的 hashCode 和 equals 将字符串的转换为相应 byte 类型,第二遍才是利用 byte 执行进行比较 -* hashCode 是为了提高效率,减少可能的比较;而 equals 是为了防止 hashCode 冲突 +* 解释器:根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容翻译为对应平台的本地机器指令执行 +* 即时编译器(JIT : Just In Time Compiler):虚拟机运行时将源代码直接编译成**和本地机器平台相关的机器码**后再执行,并存入 Code Cache,下次遇到相同的代码直接执行,效率高 @@ -14307,249 +14383,116 @@ switch(x) { -##### 枚举 +#### 执行方式 -switch 枚举的例子,原始代码: +HotSpot VM 采用**解释器与即时编译器并存的架构**,解释器和即时编译器能够相互协作,去选择最合适的方式来权衡编译本地代码和直接解释执行代码的时间 -```java -enum Sex { - MALE, FEMALE -} -public class Candy7 { - public static void foo(Sex sex) { - switch (sex) { - case MALE: - System.out.println("男"); - break; - case FEMALE: - System.out.println("女"); - break; - } - } -} -``` +HostSpot JVM 的默认执行方式: -编译转换后的代码: +* 当程序启动后,解释器可以马上发挥作用立即执行,省去编译器编译的时间(解释器存在的**必要性**) +* 随着程序运行时间的推移,即时编译器逐渐发挥作用,根据热点探测功能,将有价值的字节码编译为本地机器指令,以换取更高的程序执行效率 -```java -/** -* 定义一个合成类(仅 jvm 使用,对我们不可见) -* 用来映射枚举的 ordinal 与数组元素的关系 -* 枚举的 ordinal 表示枚举对象的序号,从 0 开始 -* 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1 -*/ -static class $MAP { - // 数组大小即为枚举元素个数,里面存储 case 用来对比的数字 - static int[] map = new int[2]; - static { - map[Sex.MALE.ordinal()] = 1; - map[Sex.FEMALE.ordinal()] = 2; - } -} -public static void foo(Sex sex) { - int x = $MAP.map[sex.ordinal()]; - switch (x) { - case 1: - System.out.println("男"); - break; - case 2: - System.out.println("女"); - break; - } -} -``` +HotSpot VM 可以通过 VM 参数设置程序执行方式: +- -Xint:完全采用解释器模式执行程序 +- -Xcomp:完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行 +- -Xmixed:采用解释器 + 即时编译器的混合模式共同执行程序 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-执行引擎工作流程.png) -*** +*** -#### 枚举类 -JDK 7 新增了枚举类: -```java -enum Sex { - MALE, FEMALE -} -``` +#### 热点探测 -编译转换后: +热点代码:被 JIT 编译器编译的字节码,根据代码被调用执行的频率而定,一个被多次调用的方法或者一个循环次数较多的循环体都可以被称之为热点代码 -```java -public final class Sex extends Enum { - public static final Sex MALE; - public static final Sex FEMALE; - private static final Sex[] $VALUES; - static { - MALE = new Sex("MALE", 0); - FEMALE = new Sex("FEMALE", 1); - $VALUES = new Sex[]{MALE, FEMALE}; - } - private Sex(String name, int ordinal) { - super(name, ordinal); - } - public static Sex[] values() { - return $VALUES.clone(); - } - public static Sex valueOf(String name) { - return Enum.valueOf(Sex.class, name); - } -} -``` +热点探测:JIT 编译器在运行时会针热点代码做出深度优化,将其直接编译为对应平台的本地机器指令进行缓存,以提升程序执行性能 +JIT 编译在默认情况是异步进行的,当触发某方法或某代码块的优化时,先将其放入编译队列,然后由编译线程进行编译,编译之后的代码放在 CodeCache 中,通过 `-XX:-BackgroundCompilation` 参数可以关闭异步编译 +* CodeCache 用于缓存编译后的机器码、动态生成的代码和本地方法代码 JNI +* 如果 CodeCache 区域被占满,编译器被停用,字节码将不会编译为机器码,应用程序继续运行,但运行性能会降低很多 +HotSpot VM 采用的热点探测方式是基于计数器的热点探测,为每一个方法都建立 2 个不同类型的计数器:方法调用计数器(Invocation Counter)和回边计数器(BackEdge Counter) +* 方法调用计数器:用于统计方法被调用的次数,默认阈值在 Client 模式 下是 1500 次,在 Server 模式下是 10000 次(需要进行激进的优化),超过这个阈值,就会触发 JIT 编译,阈值可以通过虚拟机参数 `-XX:CompileThreshold` 设置 -*** + 工作流程:当一个方法被调用时, 会先检查该方法是否存在被 JIT 编译过的版本,存在则使用编译后的本地代码来执行;如果不存在则将此方法的调用计数器值加 1,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阈值,如果超过阈值会向即时编译器**提交一个该方法的代码编译请求** +* 回边计数器:统计一个方法中循环体代码执行的次数,在字节码中控制流向后跳转的指令称为回边 + 如果一个方法中的循环体需要执行多次,可以优化为为栈上替换,简称 OSR (On StackReplacement) 编译,**OSR 替换循环代码体的入口,C1、C2 替换的是方法调用的入口**,OSR 编译后会出现方法的整段代码被编译了,但是只有循环体部分才执行编译后的机器码,其他部分仍是解释执行 -#### try-w-r -JDK 7 开始新增了对需要关闭的资源处理的特殊语法 `try-with-resources`,格式: -```java -try(资源变量 = 创建资源对象){ -} catch( ) { -} -``` +*** -其中资源对象需要实现 **AutoCloseable**接口,例如 InputStream、OutputStream、Connection、Statement、ResultSet 等接口都实现了 AutoCloseable ,使用 try-withresources可以不用写 finally 语句块,编译器会帮助生成关闭资源代码: -```java -try(InputStream is = new FileInputStream("d:\\1.txt")) { - System.out.println(is); -} catch (IOException e) { - e.printStackTrace(); -} -``` -转换成: +#### 分层编译 -`addSuppressed(Throwable e)`:添加被压制异常,是为了防止异常信息的丢失(**fianlly 中如果抛出了异常**) +HotSpot VM 内嵌两个 JIT 编译器,分别为 Client Compiler 和 Server Compiler,简称 C1 编译器和 C2 编译器 -```java -try { - InputStream is = new FileInputStream("d:\\1.txt"); - Throwable t = null; - try { - System.out.println(is); - } catch (Throwable e1) { - // t 是我们代码出现的异常 - t = e1; - throw e1; - } finally { - // 判断了资源不为空 - if (is != null) { - // 如果我们代码有异常 - if (t != null) { - try { - is.close(); - } catch (Throwable e2) { - // 如果 close 出现异常,作为被压制异常添加 - t.addSuppressed(e2); - } - } else { - // 如果我们代码没有异常,close 出现的异常就是最后 catch 块中的 e - is.close(); - } - } - } -} catch (IOException e) { - e.printStackTrace(); -} -``` +C1 编译器会对字节码进行简单可靠的优化,耗时短,以达到更快的编译速度,C1 编译器的优化方法: +* 方法内联:**将调用的函数代码编译到调用点处**,这样可以减少栈帧的生成,减少参数传递以及跳转过程 + 方法内联能够消除方法调用的固定开销,任何方法除非被内联,否则调用都会有固定开销,来源于保存程序在该方法中的执行位置,以及新建、压入和弹出新方法所使用的栈帧。 -*** + ```java + private static int square(final int i) { + return i * i; + } + System.out.println(square(9)); + ``` + square 是热点方法,会进行内联,把方法内代码拷贝粘贴到调用者的位置: + ```java + System.out.println(9 * 9); + ``` -#### 方法重写 + 还能够进行常量折叠(constant folding)的优化: -方法重写时对返回值分两种情况: + ```java + System.out.println(81); + ``` -* 父子类的返回值完全一致 -* 子类返回值可以是父类返回值的子类 +* 冗余消除:根据运行时状况进行代码折叠或削除 -```java -class A { - public Number m() { - return 1; - } -} -class B extends A { - @Override - // 子类m方法的返回值是Integer是父类m方法返回值Number的子类 - public Integer m() { - return 2; - } -} -``` +* 内联缓存:是一种加快动态绑定的优化技术(方法调用部分详解) -对于子类,java 编译器会做如下处理: +C2 编译器进行耗时较长的优化以及激进优化,优化的代码执行效率更高,当激进优化的假设不成立时,再退回使用 C1 编译,这也是使用分层编译的原因 -```java -class B extends A { - public Integer m() { - return 2; - } - // 此方法才是真正重写了父类 public Number m() 方法 - public synthetic bridge Number m() { - // 调用 public Integer m() - return m(); - } -} -``` +C2 的优化主要是在全局层面,逃逸分析是优化的基础:标量替换、栈上分配、同步消除 -其中桥接方法比较特殊,仅对 java 虚拟机可见,并且与原来的 public Integer m() 没有命名冲突 +VM 参数设置: +- -client:指定 Java 虚拟机运行在 Client 模式下,并使用 C1 编译器 +- -server:指定 Java 虚拟机运行在 Server 模式下,并使用 C2 编译器 +- `-server -XX:+TieredCompilation`:在 1.8 之前,分层编译默认是关闭的,可以添加该参数开启 +分层编译策略 (Tiered Compilation):程序解释执行可以触发 C1 编译,将字节码编译成机器码,加上性能监控,C2 编译会根据性能监控信息进行激进优化,JVM 将执行状态分成了 5 个层次: -*** +* 0 层,解释执行(Interpreter) +* 1 层,使用 C1 即时编译器编译执行(不带 profiling) +* 2 层,使用 C1 即时编译器编译执行(带基本的 profiling) -#### 匿名内部类 +* 3 层,使用 C1 即时编译器编译执行(带完全的 profiling) -##### 无参优化 +* 4 层,使用 C2 即时编译器编译执行(C1 和 C2 协作运行) -源代码: + 说明:profiling 是指在运行过程中收集一些程序执行状态的数据,例如方法的调用次数,循环的回边次数等 -```java -public class Candy11 { - public static void main(String[] args) { - Runnable runnable = new Runnable() { - @Override - public void run() { - System.out.println("ok"); - } - }; - } -} -``` -转化后代码: -```java -// 额外生成的类 -final class Candy11$1 implements Runnable { - Candy11$1() { - } - public void run() { - System.out.println("ok"); - } -} -public class Candy11 { - public static void main(String[] args) { - Runnable runnable = new Candy11$1(); - } -} -``` +参考文章:https://www.jianshu.com/p/20bd2e9b1f03 @@ -14557,181 +14500,138 @@ public class Candy11 { -##### 带参优化 +### 方法调用 -引用局部变量的匿名内部类,源代码: +#### 方法识别 + +Java 虚拟机识别方法的关键在于类名、方法名以及方法描述符(method descriptor) + +* **方法描述符是由方法的参数类型以及返回类型所构成**,Java 层面叫方法特征签名 +* 在同一个类中,如果同时出现多个名字相同且描述符也相同的方法,那么 Java 虚拟机会在类的验证阶段报错 + +JVM 根据名字和描述符来判断的,只要返回值不一样(方法描述符不一样),其它完全一样,在 JVM 中是允许的,但 Java 语言不允许 ```java -public class Candy11 { - public static void test(final int x) { - Runnable runnable = new Runnable() { - @Override - public void run() { - System.out.println("ok:" + x); - } - }; - } -} -``` - -转换后代码: - -```java -final class Candy11$1 implements Runnable { - int val$x; - Candy11$1(int x) { - this.val$x = x; - } - public void run() { - System.out.println("ok:" + this.val$x); - } +// 返回值类型不同,编译阶段直接报错 +public static Integer invoke(Object... args) { + return 1; } -public class Candy11 { - public static void test(final int x) { - Runnable runnable = new Candy11$1(x); - } +public static int invoke(Object... args) { + return 2; } ``` -局部变量在底层创建为内部类的成员变量,必须是 final 的原因: - -* 在 Java 中方法调用是值传递的,在匿名内部类中对变量的操作都是基于原变量的副本,不会影响到原变量的值,所以原变量的值的改变也无法同步到副本中 - -* 外部变量为 final 是在编译期以强制手段确保用户不会在内部类中做修改原变量值的操作,也是**防止外部操作修改了变量而内部类无法随之变化**出现的影响 - - 在创建 `Candy11$1 ` 对象时,将 x 的值赋值给了 `Candy11$1` 对象的 val 属性,x 不应该再发生变化了,因为发生变化,this.val$x 属性没有机会再跟着变化 - *** -#### 反射优化 +#### 调用机制 -```java -public class Reflect1 { - public static void foo() { - System.out.println("foo..."); - } - public static void main(String[] args) throws Exception { - Method foo = Reflect1.class.getMethod("foo"); - for (int i = 0; i <= 16; i++) { - System.out.printf("%d\t", i); - foo.invoke(null); - } - System.in.read(); - } -} -``` +方法调用并不等于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本,不是方法的具体运行过程 -foo.invoke 0 ~ 15次调用的是 MethodAccessor 的实现类 `NativeMethodAccessorImpl.invoke0()`,本地方法执行速度慢;当调用到第 16 次时,会采用运行时生成的类 `sun.reflect.GeneratedMethodAccessor1` 代替 +在 JVM 中,将符号引用转换为直接引用有两种机制: -```java -public Object invoke(Object obj, Object[] args)throws Exception { - // inflationThreshold 膨胀阈值,默认 15 - if (++numInvocations > ReflectionFactory.inflationThreshold() - && !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) { - MethodAccessorImpl acc = (MethodAccessorImpl) - new MethodAccessorGenerator(). - generateMethod(method.getDeclaringClass(), - method.getName(), - method.getParameterTypes(), - method.getReturnType(), - method.getExceptionTypes(), - method.getModifiers()); - parent.setDelegate(acc); - } - // 【调用本地方法实现】 - return invoke0(method, obj, args); -} -private static native Object invoke0(Method m, Object obj, Object[] args); -``` +- 静态链接:当一个字节码文件被装载进 JVM 内部时,如果被调用的目标方法在编译期可知,且运行期保持不变,将调用方法的符号引用转换为直接引用的过程称之为静态链接(类加载的解析阶段) +- 动态链接:如果被调用的方法在编译期无法被确定下来,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此被称为动态链接(初始化后的解析阶段) -```java -public class GeneratedMethodAccessor1 extends MethodAccessorImpl { - // 如果有参数,那么抛非法参数异常 - block4 : { - if (arrobject == null || arrobject.length == 0) break block4; - throw new IllegalArgumentException(); - } - try { - // 【可以看到,已经是直接调用方法】 - Reflect1.foo(); - // 因为没有返回值 - return null; - } - //.... -} -``` +对应方法的绑定(分配)机制:静态绑定和动态绑定。绑定是一个字段、方法或者类从符号引用被替换为直接引用的过程,仅发生一次: -通过查看 ReflectionFactory 源码可知: +- 静态绑定:被调用的目标方法在编译期可知,且运行期保持不变,将这个方法与所属的类型进行绑定 +- 动态绑定:被调用的目标方法在编译期无法确定,只能在程序运行期根据实际的类型绑定相关的方法 -* sun.reflect.noInflation 可以用来禁用膨胀,直接生成 GeneratedMethodAccessor1,但首次生成比较耗时,如果仅反射调用一次,不划算 -* sun.reflect.inflationThreshold 可以修改膨胀阈值 +* Java 编译器已经区分了重载的方法(静态绑定和动态绑定),因此可以认为虚拟机中不存在重载 +非虚方法: +- 非虚方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的 +- 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法 +- 所有普通成员方法、实例方法、被重写的方法都是虚方法 +动态类型语言和静态类型语言: +- 在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之则是动态类型语言 -*** +- 静态语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息 +- **Java 是静态类型语言**(尽管 Lambda 表达式为其增加了动态特性),JS,Python 是动态类型语言 + ```java + String s = "abc"; //Java + info = "abc"; //Python + ``` -## 系统优化 +*** -### 性能调优 -#### 性能指标 -性能指标主要是吞吐量、响应时间、QPS、TPS 等、并发用户数等,而这些性能指标又依赖于系统服务器的资源,如 CPU、内存、磁盘 IO、网络 IO 等,对于这些指标数据的收集,通常可以根据Java本身的工具或指令进行查询 +#### 调用指令 -几个重要的指标: +##### 五种指令 -1. 停顿时间(响应时间):提交请求和返回该请求的响应之间使用的时间,比如垃圾回收中 STW 的时间 -2. 吞吐量:对单位时间内完成的工作量(请求)的量度(可以对比 GC 的性能指标) -3. 并发数:同一时刻,对服务器有实际交互的请求数 -4. QPS:Queries Per Second,每秒处理的查询量 -5. TPS:Transactions Per Second,每秒产生的事务数 -6. 内存占用:Java 堆区所占的内存大小 +普通调用指令: +- invokestatic:调用静态方法 +- invokespecial:调用私有实例方法、构造器,和父类的实例方法或构造器,以及所实现接口的默认方法 +- invokevirtual:调用所有虚方法(虚方法分派) +- invokeinterface:调用接口方法 +动态调用指令: -*** +- invokedynamic:动态解析出需要调用的方法 + - Java7 为了实现动态类型语言支持而引入了该指令,但是并没有提供直接生成 invokedynamic 指令的方法,需要借助 ASM 这种底层字节码工具来产生 invokedynamic 指令 + - Java8 的 lambda 表达式的出现,invokedynamic 指令在 Java 中才有了直接生成方式 +指令对比: +- 普通调用指令固化在虚拟机内部,方法的调用执行不可干预,根据方法的符号引用链接到具体的目标方法 +- 动态调用指令支持用户确定方法 +- invokestatic 和 invokespecial 指令调用的方法称为非虚方法,虚拟机能够直接识别具体的目标方法 +- invokevirtual 和 invokeinterface 指令调用的方法称为虚方法,虚拟机需要在执行过程中根据调用者的动态类型来确定目标方法 -#### 优化步骤 +指令说明: -对于一个系统要部署上线时,则一定会对 JVM 进行调整,不经过任何调整直接上线,容易出现线上系统频繁 FullGC 造成系统卡顿、CPU 使用频率过高、系统无反应等问题 +- 如果虚拟机能够确定目标方法有且仅有一个,比如说目标方法被标记为 final,那么可以不通过动态绑定,直接确定目标方法 +- 普通成员方法是由 invokevirtual 调用,属于**动态绑定**,即支持多态 -1. 性能监控:通过运行日志、堆栈信息、线程快照等信息监控是否出现 GC 频繁、OOM、内存泄漏、死锁、响应时间过长等情况 -2. 性能分析: - * 打印 GC 日志,通过 GCviewe r或者 http://gceasy.io 来分析异常信息 +*** - - 运用命令行工具、jstack、jmap、jinfo 等 - - dump 出堆文件,使用内存分析工具分析文件 - - 使用阿里 Arthas、jconsole、JVisualVM 来**实时查看 JVM 状态** +##### 符号引用 - - jstack 查看堆栈信息 +在编译过程中,虚拟机并不知道目标方法的具体内存地址,Java 编译器会暂时用符号引用来表示该目标方法,这一符号引用包括目标方法所在的类或接口的名字,以及目标方法的方法名和方法描述符 -3. 性能调优: +* 对于静态绑定的方法调用而言,实际引用是一个指向方法的指针 +* 对于需要动态绑定的方法调用而言,实际引用则是一个方法表的索引 - * 适当增加内存,根据业务背景选择垃圾回收器 +符号引用存储在方法区常量池中,根据目标方法是否为接口方法,分为接口符号引用和非接口符号引用: - - 优化代码,控制内存使用 +```java +Constant pool: +... + #16 = InterfaceMethodref #27.#29 // 接口 +... + #22 = Methodref #1.#33 // 非接口 +... +``` - - 增加机器,分散节点压力 +对于非接口符号引用,假定该符号引用所指向的类为 C,则 Java 虚拟机会按照如下步骤进行查找: - - 合理设置线程池线程数量 +1. 在 C 中查找符合名字及描述符的方法 +2. 如果没有找到,在 C 的父类中继续搜索,直至 Object 类 +3. 如果没有找到,在 C 所直接实现或间接实现的接口中搜索,这一步搜索得到的目标方法必须是非私有、非静态的。如果有多个符合条件的目标方法,则任意返回其中一个 - - 使用中间件提高程序效率,比如缓存、消息队列等 +对于接口符号引用,假定该符号引用所指向的接口为 I,则 Java 虚拟机会按照如下步骤进行查找: + +1. 在 I 中查找符合名字及描述符的方法 +2. 如果没有找到,在 Object 类中的公有实例方法中搜索 +3. 如果没有找到,则在 I 的超接口中搜索,这一步的搜索结果的要求与非接口符号引用步骤 3 的要求一致 @@ -14739,3061 +14639,83 @@ public class GeneratedMethodAccessor1 extends MethodAccessorImpl { -#### 参数调优 +##### 执行流程 -对于 JVM 调优,主要就是调整年轻代、老年代、元空间的内存空间大小及使用的垃圾回收器类型 +```java +public class Demo { + public Demo() { } + private void test1() { } + private final void test2() { } -* 设置堆的初始大小和最大大小,为了防止垃圾收集器在初始大小、最大大小之间收缩堆而产生额外的时间,通常把最大、初始大小设置为相同的值 + public void test3() { } + public static void test4() { } - ```sh - -Xms:设置堆的初始化大小 - -Xmx:设置堆的最大大小 - ``` + public static void main(String[] args) { + Demo3_9 d = new Demo3_9(); + d.test1(); + d.test2(); + d.test3(); + d.test4(); + Demo.test4(); + } +} +``` -* 设置年轻代中 Eden 区和两个 Survivor 区的大小比例。该值如果不设置,则默认比例为 8:1:1。Java 官方通过增大 Eden 区的大小,来减少 YGC 发生的次数,虽然次数减少了,但 Eden 区满的时候,由于占用的空间较大,导致释放缓慢,此时 STW 的时间较长,因此需要按照程序情况去调优 +几种不同的方法调用对应的字节码指令: - ```sh - -XX:SurvivorRatio - ``` +```java +0: new #2 // class cn/jvm/t3/bytecode/Demo +3: dup +4: invokespecial #3 // Method "":()V +7: astore_1 +8: aload_1 +9: invokespecial #4 // Method test1:()V +12: aload_1 +13: invokespecial #5 // Method test2:()V +16: aload_1 +17: invokevirtual #6 // Method test3:()V +20: aload_1 +21: pop +22: invokestatic #7 // Method test4:()V +25: invokestatic #7 // Method test4:()V +28: return +``` -* 年轻代和老年代默认比例为 1:2,可以通过调整二者空间大小比率来设置两者的大小。 +- invokespecial 调用该对象的构造方法 :()V - ```sh - -XX:newSize 设置年轻代的初始大小 - -XX:MaxNewSize 设置年轻代的最大大小, 初始大小和最大大小两个值通常相同 - ``` +- `d.test4()` 是通过**对象引用**调用一个静态方法,在调用 invokestatic 之前执行了 pop 指令,把对象引用从操作数栈弹掉 + - 不建议使用 `对象.静态方法()` 的方式调用静态方法,多了aload 和 pop 指令 + - 成员方法与静态方法调用的区别是:执行方法前是否需要对象引用 -* 线程堆栈的设置:**每个线程默认会开启 1M 的堆栈**,用于存放栈帧、调用参数、局部变量等,但一般 256K 就够用,通常减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统 - ```sh - -Xss 对每个线程stack大小的调整,-Xss128k - ``` -* 一般一天超过一次 FullGC 就是有问题,首先通过工具查看是否出现内存泄露,如果出现内存泄露则调整代码,没有的话则调整 JVM 参数 +*** -* 系统 CPU 持续飙高的话,首先先排查代码问题,如果代码没问题,则咨询运维或者云服务器供应商,通常服务器重启或者服务器迁移即可解决 -* 如果数据查询性能很低下的话,如果系统并发量并没有多少,则应更加关注数据库的相关问题 -* 如果服务器配置还不错,JDK8 开始尽量使用 G1 或者新生代和老年代组合使用并行垃圾回收器 +#### 多态原理 +##### 执行原理 +Java 虚拟机中关于方法重写的判定基于方法描述符,如果子类定义了与父类中非私有、非静态方法同名的方法,只有当这两个方法的参数类型以及返回类型一致,Java 虚拟机才会判定为重写。 +理解多态: +- 多态有编译时多态和运行时多态,即静态绑定和动态绑定 +- 前者是通过方法重载实现,后者是通过重写实现(子类覆盖父类方法,虚方法表) +- 虚方法:运行时动态绑定的方法,对比静态绑定的非虚方法调用来说,虚方法调用更加耗时 -**** - - - - - -### 命令行篇 - -#### jps - -jps(Java Process Statu):显示指定系统内所有的 HotSpot 虚拟机进程(查看虚拟机进程信息),可用于查询正在运行的虚拟机进程,进程的本地虚拟机 ID 与操作系统的进程 ID 是一致的,是唯一的 - -使用语法:`jps [options] [hostid]` - -options 参数: - -- -q:仅仅显示 LVMID(local virtual machine id),即本地虚拟机唯一 id,不显示主类的名称等 - -- -l:输出应用程序主类的全类名或如果进程执行的是 jar 包,则输出 jar 完整路径 - -- -m:输出虚拟机进程启动时传递给主类 main()的参数 - -- -v:列出虚拟机进程启动时的JVM参数,比如 -Xms20m -Xmx50m是启动程序指定的 jvm 参数 - -ostid 参数:RMI注册表中注册的主机名,如果想要远程监控主机上的 java 程序,需要安装 jstatd - - - -**** - - - -#### jstat - -jstat(JVM Statistics Monitoring Tool):用于监视 JVM 各种运行状态信息的命令行工具,可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据,在没有 GUI 的图形界面,只提供了纯文本控制台环境的服务器上,它是运行期定位虚拟机性能问题的首选工具,常用于检测垃圾回收问题以及内存泄漏问题 - -使用语法:`jstat -