From 5d0fc734eedaac2db57f09b75b0b3e7caa507ce7 Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 13 May 2021 23:36:13 +0800 Subject: [PATCH 001/242] Update Java Notes --- Java.md | 51 +++++++++++++++++++++++++++++++-------------------- SSM.md | 6 +++--- 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/Java.md b/Java.md index 9f24800..86a1bb1 100644 --- a/Java.md +++ b/Java.md @@ -132,6 +132,14 @@ G-->H[double] float f2 = (float) d2;//向下转型需要强转 ``` + ```java + int i1 = 1245; + long l1 = i1; + + long l2 = 1234; + int i2 = (int) l2; + ``` + * 隐式类型转换: 字面量 1 是 int 类型,它比 short 类型精度要高,因此不能隐式地将 int 类型向下转型为 short 类型 @@ -144,14 +152,6 @@ G-->H[double] s1 = (short) (s1 + 1); ``` - ```java - int i1 = 1245; - long l1 = i1; - - long l2 = 1234; - int i2 = (int) l2; - ``` - @@ -4956,7 +4956,7 @@ public static void main(String[] args){ #### HashMap -##### 集合概述 +##### 基本介绍 HashMap基于哈希表的Map接口实现,是以key-value存储形式存在,主要用来存放键值对 @@ -4968,6 +4968,13 @@ HashMap基于哈希表的Map接口实现,是以key-value存储形式存在, * HashMap中的映射不是有序的,即存取是无序的 * **key要存储的是自定义对象,需要重写hashCode和equals方法,防止出现地址不同内容相同的key** +JDK7对比JDK8: + +* 7 = 数组 + 链表,8 = 数组 + 链表 + 红黑树 +* 7中是头插法,多线程容易造成环,8中是尾插法 +* 7的扩容是全部数据重新定位,8中是位置不变或者当前位置 + 旧size大小来实现 +* 7是先判断是否要扩容再插入,8中是先插入再看是否要扩容 + 底层数据结构: * 哈希表(Hash table,也叫散列表),根据关键码值(Key value)而直接访问的数据结构。通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度,这个映射函数叫做散列函数,存放记录的数组叫做散列表 @@ -12604,7 +12611,7 @@ Java语言提供了对象终止(finalization)机制来允许开发人员提 ### 垃圾回收器 -#### GC概述 +#### 概述 垃圾收集器分类: @@ -12784,7 +12791,8 @@ CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿 * `-XX:CMSInitiatingoccupanyFraction`:设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收 - * JDK5及以前版本的默认值为68,即当老年代的空间使用率达到68%时,会执行一次CMS回收,JDK6及以上版本默认值为92% + * JDK5及以前版本的默认值为68,即当老年代的空间使用率达到68%时,会执行一次CMS回收 + * JDK6及以上版本默认值为92% * `-XX:+UseCMSCompactAtFullCollection`:用于指定在执行完Full GC后对内存空间进行压缩整理,以此避免内存碎片的产生,由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长 @@ -12846,7 +12854,7 @@ G1对比其他处理器的优点: G1垃圾收集器的缺点: -* 相较于CMS,G1还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(overload)都要比CMS要高 +* 相较于CMS,G1还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用还是程序运行时的额外执行负载都要比CMS要高 * 从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则发挥其优势。平衡点在6-8GB之间 应用场景: @@ -19834,9 +19842,9 @@ AQS 核心思想: state设计: -* state **使用 volatile 配合 cas** 保证其修改时的原子性 * state 使用了 32bit int 来维护同步状态 -* state 表示允许的进入的线程数 +* state **使用 volatile 修饰配合 cas** 保证其修改时的原子性 +* state 表示已经进入的线程数或者许可进入的线程数 * state API: `protected final int getState()`:获取 state 状态 `protected final void setState(int newState)`:设置 state 状态 @@ -19844,9 +19852,9 @@ state设计: waitstate设计: -* 使用**volatile 配合 cas**保证其修改时的原子性 +* 使用**volatile 修饰配合 cas**保证其修改时的原子性 -* Node 节点的几种状态: +* 表示Node节点的状态,有以下几种状态: ```java //由于超时或中断,此节点被取消,不会再改变状态 @@ -21817,7 +21825,7 @@ class ThreadA extends Thread{ public void run() { try{ sout("线程A,做好了礼物A,等待线程B送来的礼物B"); - //如果等待了5s还没有交换它就去死(抛出异常)! + //如果等待了5s还没有交换就死亡(抛出异常)! String s = exchanger.exchange("礼物A",5,TimeUnit.SECONDS); sout("线程A收到线程B的礼物:" + s); } catch (Exception e) { @@ -21833,7 +21841,7 @@ class ThreadB extends Thread{ @Override public void run() { try { - System.out.println("线程B,做好了礼物B,等待线程A送来的礼物A....."); + sout("线程B,做好了礼物B,等待线程A送来的礼物A....."); // 开始交换礼物。参数是送给其他线程的礼物! sout("线程B收到线程A的礼物:" + exchanger.exchange("礼物B")); } catch (Exception e) { @@ -22580,13 +22588,16 @@ ConcurrentHashMap 对锁粒度进行了优化,**分段锁技术**,将整张 -### LinkedQueue +### LinkedQueue + +(待更新) ConcurrentLinkedQueue 的设计与 LinkedBlockingQueue 相似: * 两把锁,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行 * dummy 节点的引入让两把锁将来锁住的是不同对象,避免竞争 -* 只是这锁使用了 cas 来实现 +* 锁使用了 cas 来实现 +* 此队列不允许使用 null 元素 diff --git a/SSM.md b/SSM.md index 8ec538b..7a42406 100644 --- a/SSM.md +++ b/SSM.md @@ -1305,9 +1305,9 @@ PageInfo相关API: -### Assocation实现延迟加载 +### Assocation -一对多,多对多 +一对多,多对多 * 核心配置文件 @@ -1399,7 +1399,7 @@ PageInfo相关API: -### Collection实现延迟加载 +### Collection 同样在一对多关系配置的结点中配置延迟加载策略。 结点中也有select属性,column属性。 From bc6abb940d32505211d68c5145f7f4b522db5b8f Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 15 May 2021 00:14:07 +0800 Subject: [PATCH 002/242] Update Java Notes --- DB.md | 5 +- Java.md | 626 +++++++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 513 insertions(+), 118 deletions(-) diff --git a/DB.md b/DB.md index 2729b15..c8f4b9f 100644 --- a/DB.md +++ b/DB.md @@ -3707,12 +3707,12 @@ public class JDBCDemo01 { } } ``` -``` - + + **** @@ -3732,7 +3732,6 @@ public class JDBCDemo01 { private Integer age; private Date birthday; ........ -``` - 数据准备 diff --git a/Java.md b/Java.md index 86a1bb1..a2de4cb 100644 --- a/Java.md +++ b/Java.md @@ -111,7 +111,7 @@ G[float] G-->H[double] ``` -##### 上下转型 +上下转型 * float 与 double: @@ -189,7 +189,7 @@ Java为包装类做了一些特殊功能,具体来看特殊功能主要有: 2. 调用Integer.toString(基本数据类型的值)得到字符串 3. 直接把基本数据类型+空字符串就得到了字符串(推荐使用) -* 把字符串类型的数值转换成对应的基本数据类型的值。(**重要**) +* 把字符串类型的数值转换成对应的基本数据类型的值(**重要**) 1. Xxx.parseXxx("字符串类型的数值") ---->Integer.parseInt(numStr) 2. Xxx.valueOf("字符串类型的数值") ---->Integer.valueOf(numStr) (推荐使用) @@ -231,8 +231,9 @@ Java为包装类做了一些特殊功能,具体来看特殊功能主要有: #### 装箱拆箱 -* **自动装箱**:可以直接把基本数据类型的值或者变量赋值给包装类 -* **自动拆箱**:可以把包装类的变量直接赋值给基本数据类型 +**自动装箱**:可以直接把基本数据类型的值或者变量赋值给包装类 + +**自动拆箱**:可以把包装类的变量直接赋值给基本数据类型 ```java public class PackegeClass { @@ -241,10 +242,6 @@ public class PackegeClass { Integer a1 = 12 ; // 自动装箱 Integer a2 = a ; // 自动装箱 - double b = 99.9; - Double b1 = 99.9; // 自动装箱 - Double b2 = b ; // 自动装箱 - Integer c = 100 ; int c1 = c ; // 自动拆箱 @@ -408,12 +405,8 @@ public class ScannerDemo { #### 内存分配 -##### 概念 - 内存是计算机中的重要原件,临时存储区域,作用是运行程序。我们编写的程序是存放在硬盘中的,在硬盘中的程序是不会运行的。必须放进内存中才能运行,运行完毕后会清空内存。 Java虚拟机要运行程序,必须要对内存进行空间的分配和管理。 -目前我们只需要记住两个内存,分别是:栈内存和堆内存 - | 区域名称 | 作用 | | ---------- | -------------------------------------------------------- | | 寄存器 | 给CPU使用,和我们开发无关 | @@ -422,9 +415,7 @@ public class ScannerDemo { | 堆内存 | 存储对象或者数组,new来创建的,都存储在堆内存 | | 方法栈 | 方法运行时使用的内存,比如main方法运行,进入方法栈中执行 | - - -##### 内存图 +**内存分配图**: * Java内存分配-一个数组内存图 @@ -442,7 +433,7 @@ public class ScannerDemo { -#### 异常 +#### 数组异常 * 索引越界异常:ArrayIndexOutOfBoundsException @@ -1384,11 +1375,12 @@ public class ClassDemo { #### 构造器 +构造器:格式: + ```java -构造器: - 格式:修饰符 类名(形参列表){ - - } +修饰符 类名(形参列表){ + +} ``` 作用:初始化类的一个对象返回 @@ -1409,13 +1401,13 @@ public class ClassDemo { ### 包 -* 包: - 分门别类的管理各种不同的技术。 - 企业的代码必须用包区分。便于管理技术,扩展技术,阅读技术。 -* 定义包的格式:package 包名; 必须放在类名的最上面。 -* 注意: - 相同包下的类可以直接访问;不同包下的类必须导包,才可以使用! - 导包格式:import 包名.类名; +包:分门别类的管理各种不同的技术,便于管理技术,扩展技术,阅读技术。 + +定义包的格式:`package 包名; `,必须放在类名的最上面。 + +导包格式:`import 包名.类名;` + +相同包下的类可以直接访问;不同包下的类必须导包才可以使用 @@ -1444,28 +1436,11 @@ public class ClassDemo { ### this this关键字的作用: - this关键字代表了当前对象的引用。 - this出现在方法中:**哪个对象调用这个方法this就代表谁。** - this可以出现在构造器中:代表构造器正在初始化的那个对象。 - this可以区分变量是访问的成员变量还是局部变量。 -```java -public class ThisDemo{ - public static void main(String[] args){ - Animal a = new Animal(); - a.setName("狗子"); - } -} -class Aniaml{ - private String name; - private int age ; - ..... - public void setName(String name) { - // 谁调用这个方法,this就代表谁!!! - this.name = name; // a.name = 狗子 - } -} -``` +* this关键字代表了当前对象的引用 +* this出现在方法中:**哪个对象调用这个方法this就代表谁** +* this可以出现在构造器中:代表构造器正在初始化的那个对象 +* this可以区分变量是访问的成员变量还是局部变量 @@ -1778,29 +1753,22 @@ class Animal{ 继承后super调用父类构造器,父类构造器初始化继承自父类的数据。 -super(...):可以根据参数选择调用父类的某个构造器。 - 总结与拓展: -> this代表了当前对象的引用(继承中指代子类对象): -> this.子类成员变量。 -> this.子类成员方法。 -> this(...):可以根据参数匹配访问本类其他构造器。 -> super代表了父类对象的引用(继承中指代了父类对象空间) -> super.父类成员变量。 -> super.父类的成员方法。 -> super(...):可以根据参数匹配访问父类的构造器。 +* this代表了当前对象的引用(继承中指代子类对象):this.子类成员变量、this.子类成员方法、**this(...)**可以根据参数匹配访问本类其他构造器。 +* super代表了父类对象的引用(继承中指代了父类对象空间):super.父类成员变量、super.父类的成员方法、super(...)可以根据参数匹配访问父类的构造器 **注意:** - this(...)借用本类其他构造器,super(...)调用父类的构造器。 - this(...)和super(...)必须放在构造器的第一行,否则报错! - this(...)和super(...)不能同时出现在构造器中,因为构造函数必须出现在第一行上,只能选择一个。 + +* this(...)借用本类其他构造器,super(...)调用父类的构造器。 +* this(...)或super(...)必须放在构造器的第一行,否则报错! +* this(...)和super(...)不能同时出现在构造器中,因为构造函数必须出现在第一行上,只能选择一个。 ```java public class ThisDemo { public static void main(String[] args) { - // 需求:希望如果不写学校默认就是”黑马“! + // 需求:希望如果不写学校默认就是”张三“! Student s1 = new Student("天蓬元帅", 1000 ); Student s2 = new Student("齐天大圣", 2000, "清华大学" ); } @@ -1821,7 +1789,7 @@ class Student{ } public Student(String name , int age){ // 借用兄弟构造器的功能! - this(name , age , "黑马"); + this(name , age , "张三"); } public Student(String name, int age, String schoolName) { this.name = name; @@ -1859,16 +1827,6 @@ final用于修饰:类,方法,变量 #### 修饰变量 -代码块的相关知识 - ->* 成员变量 -> * 静态成员变量:有static修饰,属于类,只加载一份 -> * 实例成员变量:无static修饰,属于每个对象,与对象一起加载 ->* 局部变量 -> * 只能方法中,构造器中,代码块中,for循环中,用完作用范围就消失了。 ->* final修饰局部变量: -> * 让值被固定或者说保护起来,执行的过程中防止被修改。 - ##### 静态成员变量 final修饰静态成员变量,变量变成了常量 @@ -1884,13 +1842,13 @@ final修饰静态成员变量可以在哪些地方赋值: ```java public class FinalDemo { //常量:public static final修饰,名称字母全部大写,下划线连接。 - public static final String SCHOOL_NAME = "黑马" ; + public static final String SCHOOL_NAME = "张三" ; public static final String SCHOOL_NAME1; static{ //SCHOOL_NAME = "java";//报错 - SCHOOL_NAME1 = "黑马1"; - //SCHOOL_NAME1 = "黑马2"; // 报错,第二次赋值! + SCHOOL_NAME1 = "张三1"; + //SCHOOL_NAME1 = "张三2"; // 报错,第二次赋值! } } ``` @@ -1909,24 +1867,24 @@ final修饰实例成员变量可以在哪些地方赋值1次: ```java public class FinalDemo { - private final String name = "黑马" ; + private final String name = "张三" ; private final String name1; private final String name2; { // 可以在实例代码块中赋值一次。 - name1 = "黑马1"; + name1 = "张三1"; } //构造器赋值一次 public FinalDemo(){ - name2 = "黑马2"; + name2 = "张三2"; } public FinalDemo(String a){ - name2 = "黑马2"; + name2 = "张三2"; } public static void main(String[] args) { FinalDemo f1 = new FinalDemo(); - //f1.name = "黑马1"; // 第二次赋值 报错! + //f1.name = "张三1"; // 第二次赋值 报错! } } ``` @@ -1993,7 +1951,7 @@ public class AbstractDemo { } abstract class Animal{ private String name; - public static String schoolName = "黑马"; + public static String schoolName = "张三"; public Animal(){ } public abstract void run(); @@ -2078,8 +2036,8 @@ abstract class Template{ ```java public interface InterfaceDemo{ - //public static final String SCHOOL_NAME = "黑马"; - String SCHOOL_NAME = "黑马"; + //public static final String SCHOOL_NAME = "张三"; + String SCHOOL_NAME = "张三"; //public abstract void run(); void run();//默认补充 @@ -2155,7 +2113,7 @@ abstract class Template{ -#### JDK1.8以后 +#### JDK8以后 jdk1.8以后新增的功能,实际开发中很少使用 @@ -2218,7 +2176,7 @@ interface InterfaceJDK8{ -#### 抽象类对比 +#### 对比抽象类 | **参数** | **抽象类** | **接口** | | ------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | @@ -2497,7 +2455,7 @@ new 类名|抽象类|接口(形参){ * 匿名内部类一旦写出来,就会立即创建一个匿名内部类的对象返回 * **匿名内部类的对象的类型相当于是当前new的那个的类型的子类类型** * 匿名内部类引用局部变量,局部变量必须是**常量**,底层创建为内部类的成员变量(JVM-->类加载-->编译优化-->内部类) - * 在Java中方法调用是值传递的,在匿名内部类中对变量的操作都是基于原变量的副本,不会影响到原变量的值 + * 在Java中方法调用是值传递的,在匿名内部类中对变量的操作都是基于原变量的副本,不会影响到原变量的值,所以原变量的值的改变也无法同步到副本中 * 外部变量为final是在编译期以强制手段确保用户不会在内部类中做修改原变量值的操作,也是防止外部操作修改了变量而内部类无法随之变化出现的影响 ```java @@ -2580,7 +2538,7 @@ public class CodeDemo { static { System.out.println("静态代码块被触发执行~~~~~~~"); // 在静态代码块中进行静态资源的初始化操作 - schoolName = "黑马"; + schoolName = "张三"; lists.add("3"); lists.add("4"); lists.add("5"); @@ -2593,7 +2551,7 @@ public class CodeDemo { } /*静态代码块被触发执行~~~~~~~ main方法被执行 -黑马 +张三 [3, 4, 5] */ ``` @@ -4465,8 +4423,6 @@ public class ArrayList extends AbstractList * **Fail-Fast**:快速失败,modCount 用来记录 ArrayList **结构发生变化**的次数,结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化 在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了抛出 ConcurrentModificationException异常 - - @@ -5089,9 +5045,9 @@ HashMap继承关系如下图所示: * 为什么必须是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 ,不同位置上,不碰撞; @@ -6929,8 +6885,8 @@ public class StreamDemo { //跳过前两个 list.stream().filter(s -> s.length == 3).skip(2).forEach(...); - // 需求:把名称都加上“黑马的:+xxx” - list.stream().map(s -> "黑马的"+s).forEach(System.out::println); + // 需求:把名称都加上“张三的:+xxx” + list.stream().map(s -> "张三的"+s).forEach(System.out::println); // 需求:把名称都加工厂学生对象放上去!! // list.stream().map(name -> new Student(name)); list.stream.map(Student::new).forEach(System.out::println); @@ -12201,7 +12157,7 @@ JVM是通过栈帧中的对象引用访问到其内部的对象实例:(内 #### TLAB -TLAB:Thread Local Allocation Buffer,为每个线程在**堆内**单独分配了一个缓冲区,多线程分配内存时,使用TLAB可以避免线程安全问题,同时还能够提升内存分配的吞吐量,这种内存分配方式叫做**快速分配策略** +TLAB:Thread Local Allocation Buffer,为每个线程在堆内单独分配了一个缓冲区,多线程分配内存时,使用TLAB可以避免线程安全问题,同时还能够提升内存分配的吞吐量,这种内存分配方式叫做**快速分配策略** - 栈上分配使用的是栈来进行对象内存的分配 - TLAB 分配使用的是 Eden 区域进行内存分配,属于堆内存 @@ -12348,9 +12304,9 @@ Full GC 则相对复杂,**FullGC同时回收新生代和老年代,当前只 垃圾:**如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾** -垃圾收集主要是针对堆和方法区进行。程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收 +垃圾收集主要是针对堆和方法区进行,程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收 -在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为**垃圾标记阶段**,判断对象存活一般有两种方式:**引用计数算法**和**可达性分析算法** +在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段,判断对象存活一般有两种方式:**引用计数算法**和**可达性分析算法** @@ -18710,7 +18666,7 @@ String 类也是不可变的,该类和类中所有属性都是 final 的 -### 无状态 +### State 无状态:成员变量保存的数据也可以称为状态信息,无状态就是没有成员变量 @@ -18722,6 +18678,429 @@ Servlet 为了保证其线程安全,一般不为 Servlet 设置成员变量, +### Local + +#### 基本介绍 + +ThreadLocal类用来提供线程内部的局部变量,这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量,ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程上下文 + +作用: + +* 线程并发:应用在多线程并发的场景下 + +* 传递数据:通过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方案有两个突出的优势: + +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; + } + // ... +} +``` + + + +**** + + + +#### 底层结构 + +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,以当前ThreadLocal为key删除对应的实体entry + 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); + } + ``` + + 这里定义了一个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); + } + + // 扩容阈值时长度的2/3 + private void setThreshold(int len) { + threshold = len * 2 / 3; + } + ``` + + ThreadLocalMap使用**线性探测法**来解决哈希冲突: + + * 该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出 + * 假设当前table长度为16,计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,如果还是冲突会回到0,取table[0],以此类推,直到可以插入,可以把Entry[] table看成一个**环形数组** + + + +*** + + + +##### 内存泄漏 + +Memory leak:内存泄漏是指程序中动态分配的堆内存由于某种原因未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果,内存泄漏的堆积终将导致内存溢出 + +* 如果key使用强引用: + + 使用完ThreadLocal ,threadLocal Ref被回收,但是因为threadLocalMap的Entry强引用了threadLocal,造成threadLocal无法被回收,无法完全避免内存泄漏 + + + +* 如果key使用弱引用: + + 使用完ThreadLocal ,threadLocal Ref被回收,ThreadLocalMap只持有ThreadLocal的弱引用,所以threadlocal也可以被gc回收,此时Entry中的key=null。但没有手动删除这个Entry以及CurrentThread依然运行,依然存在强引用链,value不会被回收,而这块value永远不会被访问到,导致value内存泄漏 + + + +* 两个主要原因: + * 没有手动删除这个Entry + * CurrentThread依然运行 + +根本原因:ThreadLocalMap是Thread的一个属性,生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏 + +使用弱引用的原因:在ThreadLocalMap中的set/getEntry方法中,会对key为null(ThreadLocal为null)进行判断,如果为null的话,那么会对Entry进行垃圾回收。所以**弱引用比强引用多一层保障**,就算不调用remove,也有机会进行GC。 + + + +*** + + + ## 线程池 ### 基本概述 @@ -21887,9 +22266,8 @@ class ThreadB extends Thread{ 3. put,如果该 bin 尚未创建,只需要使用 cas 创建 bin;如果已经有了,锁住链表头进行后续 put 操作,元素 添加至 bin 的尾部 4. get,无锁操作仅需要保证可见性,扩容过程中 get 操作拿到的是 ForwardingNode 会让 get 操作在新 table 进行搜索 -5. 扩容,扩容时以 bin 为单位进行,需要对 bin 进行 synchronized,但这时妙的是其它竞争线程也不是无事可 - 做,它们会帮助把其它 bin 进行扩容,扩容时平均只有 1/6 的节点会把复制到新 table 中 -6. size,元素个数保存在 baseCount 中,并发时的个数变动保存在 CounterCell[] 当中。最后统计数量时累加 +5. 扩容,扩容时以 bin 为单位进行,需要对 bin 进行 synchronized,但这时其它竞争线程也不是无事可做,它们会帮助把其它 bin 进行扩容 +6. size,元素个数保存在 baseCount 中,并发时的个数变动保存在 CounterCell[] 当中,最后统计数量时累加 ```java //需求:多个线程同时往HashMap容器中存入数据会出现安全问题 @@ -22154,6 +22532,7 @@ public ConcurrentHashMap(int initialCapacity,float loadFactor,int concurrencyLev 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)) { @@ -22241,7 +22620,9 @@ public ConcurrentHashMap(int initialCapacity,float loadFactor,int concurrencyLev #### JDK7源码 -ConcurrentHashMap 对锁粒度进行了优化,**分段锁技术**,将整张表分成了多个数组(Segment),每个数组元素又是一个类似 HashMap 数组的结构,当需要并发时,锁住的是每个Segment,其他Segment还是可以操作的,这样不同Segment之间就可以实现并发,大大提高效率 +##### 分段锁 + +ConcurrentHashMap 对锁粒度进行了优化,**分段锁技术**,将整张表分成了多个数组(Segment),每个数组又是一个类似 HashMap 数组的结构。`ConcurrentHashMap`允许多个修改操作并发进行,并发时锁住的是每个Segment,其他Segment还是可以操作的,这样不同Segment之间就可以实现并发,大大提高效率 底层结构: **Segment 数组 + HashEntry 数组 + 链表**(数组+链表是HashMap的结构) @@ -22251,9 +22632,21 @@ ConcurrentHashMap 对锁粒度进行了优化,**分段锁技术**,将整张 ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentHashMap 1.7底层结构.png) -相关方法 -1. 构造方法 + + + +##### 成员方法 + +1. segment:是一种可重入锁,继承ReentrantLock + + ```java + static final class Segment extends ReentrantLock implements Serializable { + transient volatile HashEntry[] table; //可以理解为包含一个HashMap + } + ``` + +2. 构造方法 无参构造: @@ -22266,14 +22659,14 @@ ConcurrentHashMap 对锁粒度进行了优化,**分段锁技术**,将整张 ```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) { // 参数校验 @@ -22300,13 +22693,13 @@ ConcurrentHashMap 对锁粒度进行了优化,**分段锁技术**,将整张 // c = 容量/ssize ,默认16/16 = 1,计算每个Segment中的类似于HashMap的容量 int c = initialCapacity / ssize; if (c * ssize < initialCapacity) - ++c; + ++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), + 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]; @@ -22315,7 +22708,7 @@ ConcurrentHashMap 对锁粒度进行了优化,**分段锁技术**,将整张 } ``` -2. put:头插法 +3. put:头插法 segmentShift 和 segmentMask 的作用是决定将 key 的 hash 结果匹配到哪个 segment,将 hash 值 高位向低位移动 segmentShift 位,结果再与 segmentMask 做位于运算 @@ -22359,7 +22752,7 @@ ConcurrentHashMap 对锁粒度进行了优化,**分段锁技术**,将整张 // 初始化 Segment Segment s = new Segment(lf, threshold, tab); // 自旋检查 u 位置的 Segment 是否为null - while ((seg = (Segment)UNSAFE.getObjectVolatile(ss, u))== null) { + while ((seg = (Segment)UNSAFE.getObjectVolatile(ss, u))==null) { // 使用CAS 赋值,只会成功一次 if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s)) break; @@ -22438,7 +22831,8 @@ ConcurrentHashMap 对锁粒度进行了优化,**分段锁技术**,将整张 } ``` -3. rehash +4. rehash + 发生在 put 中,因为此时已经获得了锁,因此 rehash 时不需要考虑线程安全 扩容扩容到原来的两倍,老数组里的数据移动到新的数组时,位置要么不变,要么变为 index+ oldSize,参数里的 node 会在扩容之后使用链表**头插法**插入到指定位置 @@ -22508,10 +22902,12 @@ ConcurrentHashMap 对锁粒度进行了优化,**分段锁技术**,将整张 * 第一个 for 是为了寻找一个节点,该节点后面的所有 next 节点的新位置都是相同的,然后把这个作为一个链表搬迁到新位置 * 第二个 for 循环是为了把剩余的元素通过头插法插入到指定位置链表 -4. get +5. get 计算得到 key 的存放位置、遍历指定位置查找相同 key 的 value 值 + 用于存储键值对数据的`HashEntry`,它的成员变量value跟`next`都是`volatile`类型的,这样就保证别的线程对value值的修改,get方法可以马上看到 + ```java public V get(Object key) { Segment s; @@ -22535,7 +22931,7 @@ ConcurrentHashMap 对锁粒度进行了优化,**分段锁技术**,将整张 } ``` -5. size +6. size * 计算元素个数前,先不加锁计算两次,如果前后两次结果如一样,认为个数正确返回 * 如果不一样,进行重试,重试次数超过 3,将所有 segment 锁住,重新计算个数返回 @@ -22671,7 +23067,7 @@ public CopyOnWriteArraySet() { ##### get方法 -数据一致性就是读到最新更新的数据 +数据一致性就是读到最新更新的数据: * 强一致性:当更新操作完成之后,任何多个后续进程或者线程的访问都会返回最新的更新过的值 From 1d9590086eeb9849076acdbe883a656d98ad3488 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 15 May 2021 13:21:25 +0800 Subject: [PATCH 003/242] Update Java Notes --- Frame.md | 5131 ------------------------------------------------------ 1 file changed, 5131 deletions(-) delete mode 100644 Frame.md diff --git a/Frame.md b/Frame.md deleted file mode 100644 index 752d248..0000000 --- a/Frame.md +++ /dev/null @@ -1,5131 +0,0 @@ -# Maven - -## 基本介绍 - -### Mvn概述 - -Maven:本质是一个项目管理工具,将项目开发和管理过程抽象成一个项目对象模型(POM) - -POM:Project Object Model,项目对象模型。Maven是用Java语言编写的,它管理的东西以面向对象的形式进行设计,最终把一个项目看成一个对象,而这个对象叫做POM - -pom.xml:Maven需要一个pom.xml文件,Maven通过加载这个配置文件可以知道项目的相关信息,这个文件代表就一个项目。如果我们做8个项目,对应的是8个pom.xml文件 - -依赖管理:Maven对项目所有依赖资源的一种管理,它和项目之间是一种双向关系,即做项目时可以管理所需要的其他资源,当其他项目需要依赖我们项目时,Maven也会把我们的项目当作一种资源去进行管理。 - -管理资源的存储位置:本地仓库,私服,中央仓库 - -![](https://gitee.com/seazean/images/raw/master/Frame/Maven介绍.png) - - - - - -### Mvn作用 - -* 项目构建:提供标准的,跨平台的自动化构建项目的方式 - -* 依赖管理:方便快捷的管理项目依赖的资源(jar包),避免资源间的版本冲突等问题 - -* 统一开发结构:提供标准的,统一的项目开发结构 - - ![](https://gitee.com/seazean/images/raw/master/Frame/Maven标准结构.png) - -各目录存放资源类型说明: - -* src/main/java:项目java源码 - -* src/main/resources:项目的相关配置文件(比如mybatis配置,xml映射配置,自定义配置文件等) - -* src/main/webapp:web资源(比如html,css,js等) - -* src/test/java:测试代码 - -* src/test/resources:测试相关配置文件 - -* src/pom.xml:项目pom文件 - - - - - -### 基础概念 - -* **仓库**:用于存储资源,主要是各种jar包。有本地仓库,私服,中央仓库,私服和中央仓库都是远程仓库 - - * 中央仓库:maven团队自身维护的仓库,属于开源的 - - * 私服:各公司/部门等小范围内存储资源的仓库,私服也可以从中央仓库获取资源,作用: - * 保存具有版权的资源,包含购买或自主研发的jar - * 一定范围内共享资源,能做到仅对内不对外开放 - - * 本地仓库:开发者自己电脑上存储资源的仓库,也可从远程仓库获取资源 - - - -* **坐标**:Maven中的坐标用于描述仓库中资源的位置 - - * 作用:使用唯一标识,唯一性定义资源位置,通过该标识可以将资源的识别与下载工作交由机器完成 - * https://mvnrepository.com:查询maven某一个资源的坐标,输入资源名称进行检索, - - * 依赖设置: - * groupId:定义当前资源隶属组织名称(通常是域名反写,如:org.mybatis;com.seazean) - * artifactId:定义当前资源的名称(通常是项目或模块名称,如:crm,sms) - * version:定义当前资源的版本号 - -* packaging:定义资源的打包方式,取值一般有如下三种 - - * jar:该资源打成jar包,默认是jar - - * war:该资源打成war包 - - * pom:该资源是一个父资源(表明使用maven分模块管理),打包时只生成一个pom.xml不生成jar或其他包结构 - - - - - -*** - - - -## 环境搭建 - -### 环境配置 - -Maven的官网:http://maven.apache.org/ - -下载安装:Maven是一个绿色软件,解压即安装 - -目录结构: - bin:可执行程序目录 - boot:maven自身的启动加载器 - conf:maven配置文件的存放目录 - lib:maven运行所需库的存放目录 - -配置MAVEN_HOME: - -![](https://gitee.com/seazean/images/raw/master/Frame/Maven配置环境变量.png) - - - -环境变量配置好之后需要测试环境配置结果,在DOS命令窗口下输入以下命令查看输出:`mvn -v` - - - -*** - - - -### 仓库配置 - -默认情况下maven本地仓库在系统盘当前用户目录下的`.m2/repository`,修改Maven的配置文件`conf/settings.xml`来修改仓库位置 - -* 修改本地仓库位置:找到标签,修改默认值 - - ```xml - - E:\Workspace\Java\Project\.m2\repository - ``` - - 注意:在仓库的同级目录即`.m2`也应该包含一个`settings.xml`配置文件,局部用户配置优先与全局配置 - - * 全局setting定义了Maven的公共配置 - * 用户setting定义了当前用户的配置 - -* 修改远程仓库:在配置文件中找到``标签,在这组标签下添加国内镜像 - - ```xml - - nexus-aliyun - central - Nexus aliyun - http://maven.aliyun.com/nexus/content/groups/public - - ``` - -* 修改默认JDK:在配置文件中找到``标签,添加配置: - - ```xml - - jdk-10 - - true - 10 - - - UTF-8 - 10 - 10 - - - ``` - - - - - -*** - - - - - -## 项目搭建 - -### 手动搭建 - -1. 在E盘下创建目录`mvnproject`并进入该目录,作为我们的操作目录 - -2. 创建我们的maven项目,创建一个目录`project-java`作为我们的项目文件夹,并进入到该目录 - -3. 创建java代码(源代码)所在目录,即创建`src/main/java` - -4. 创建配置文件所在目录,即创建`src/main/resources` - -5. 创建测试源代码所在目录,即创建`src/test/java` - -6. 创建测试存放配置文件存放目录,即`src/test/resources` - -7. 在`src/main/java`中创建一个包(注意在windos文件夹下就是创建目录)`demo`,在该目录下创建`Demo.java`文件,作为演示所需java程序,内容如下 - - ```java - package demo; - public class Demo{ - public String say(String name){ - System.out.println("hello "+name); - return "hello "+name; - } - } - ``` - -8. 在`src/test/java`中创建一个测试包(目录)`demo`,在该包下创建测试程序`DemoTest.java` - - ```java - package demo; - import org.junit.*; - public class DemoTest{ - @Test - public void testSay(){ - Demo d = new Demo(); - String ret = d.say("maven"); - Assert.assertEquals("hello maven",ret); - } - } - ``` - -9. **在`project-java/src`下创建`pom.xml`文件,格式如下:** - - ```xml - - - - - 4.0.0 - - jar - - - demo - - project-java - - 1.0 - - - - - - junit - junit - 4.12 - - - - ``` - -10. 搭建好了maven的项目结构,通过maven来构建项目 - maven的构建命令以`mvn`开头,后面添加功能参数,可以一次性执行多个命令,用空格分离 - `mvn compile`:编译 - `mvn clean`:清理 - `mvn test`:测试 - `mvn package`:打包 - `mvn install`:安装到本地仓库 - - 注意:执行某一条命令,则会把前面所有的都执行一遍 - - - -*** - - - -### 插件构建 - -![](https://gitee.com/seazean/images/raw/master/Frame/Maven-插件构建.png) - - - -*** - - - -### IDEA搭建 - -#### 不用原型 - -1. 在IDEA中配置Maven,选择maven3.6.1防止依赖问题 - IDEA配置Maven - -2. 创建Maven,New Module --> Maven --> 不选中Create from archetype - -3. 填写项目的坐标 - GroupId:demo - ArtifactId:project-java - -4. 查看各目录颜色标记是否正确 - - ![](https://gitee.com/seazean/images/raw/master/Frame/IDEA创建Maven目录结构.png) - -5. IDEA右侧侧栏有Maven Project,打开后有Lifecycle生命周期 - - ![](https://gitee.com/seazean/images/raw/master/Frame/IDEA-Maven生命周期.png) - -6. 自定义Maven命令:Run --> Edit Configurations --> 左上角 + --> Maven - - ![](https://gitee.com/seazean/images/raw/master/Frame/IDEA配置Maven命令.png) - - - - - -#### 使用原型 - -普通工程: - -1. 创建maven项目的时候选择使用原型骨架 - - ![](https://gitee.com/seazean/images/raw/master/Frame/IDEA创建Maven-quickstart.png) - -2. 创建完成后发现通过这种方式缺少一些目录,需要手动去补全目录,并且要对补全的目录进行标记 - - - -web工程: - -1. 选择web对应的原型骨架(选择maven开头的是简化的) - - ![](https://gitee.com/seazean/images/raw/master/Frame/IDEA创建Maven-webapp.png) - -2. 通过原型创建web项目得到的目录结构是不全的,因此需要我们自行补全,同时要标记正确 - -3. web工程创建之后需要启动运行,使用tomcat插件来运行项目,在`pom.xml`中添加插件的坐标: - - ```xml - - - - 4.0.0 - war - - web01 - demo - web01 - 1.0-SNAPSHOT - - - - - - - - - - - - org.apache.tomcat.maven - tomcat7-maven-plugin - 2.1 - - 80 - / - - - - - - ``` - -4. 插件配置以后,在IDEA右侧`maven-project`操作面板看到该插件,并且可以利用该插件启动项目 - web01-->Plugins-->tomcat7-->tomcat7:run - - - -*** - - - -## 依赖管理 - -### 依赖配置 - -依赖是指在当前项目中运行所需的jar,依赖配置的格式如下: - -```xml - - - - - - junit - - junit - - 4.12 - - -``` - - - -*** - - - -### 依赖传递 - -依赖具有传递性,分两种: - -* 直接依赖:在当前项目中通过依赖配置建立的依赖关系 - -* 间接依赖:被依赖的资源如果依赖其他资源,则表明当前项目间接依赖其他资源 - - 注意:直接依赖和间接依赖其实也是一个相对关系 - - - -依赖传递的冲突问题:在依赖传递过程中产生了冲突,有三种优先法则 - -* 路径优先:当依赖中出现相同资源时,层级越深,优先级越低,反之则越高 - -* 声明优先:当资源在相同层级被依赖时,配置顺序靠前的覆盖靠后的 - -* 特殊优先:当同级配置了相同资源的不同版本时,后配置的覆盖先配置的 - - - -**可选依赖:**对外隐藏当前所依赖的资源,不透明 - -```xml - - junit - junit - 4.11 - true - -``` - -**排除依赖:主动**断开依赖的资源,被排除的资源无需指定版本 - -```xml - - junit - junit - 4.12 - - - org.hamcrest - hamcrest-core - - - -``` - - - -*** - - - -### 依赖范围 - -依赖的jar默认情况可以在任何地方可用,可以通过`scope`标签设定其作用范围,有三种: - -* 主程序范围有效(src/main目录范围内) - -* 测试程序范围内有效(src/test目录范围内) - -* 是否参与打包(package指令范围内) - -`scope`标签的取值有四种:`compile,test,provided,runtime` - -![](https://gitee.com/seazean/images/raw/master/Frame/Maven依赖范围.png) - - - -**依赖范围的传递性:** - -![](https://gitee.com/seazean/images/raw/master/Frame/Maven依赖范围的传递性.png) - - - - - -*** - - - -## 生命周期 - -### 相关事件 - -Maven的构建生命周期描述的是一次构建过程经历了多少个事件 - -最常用的一套流程:compile --> test-compile --> test --> package --> install - -* clean:清理工作 - - * pre-clean:执行一些在clean之前的工作 - * clean:移除上一次构建产生的所有文件 - * post-clean:执行一些在clean之后立刻完成的工作 - -* default:核心工作,例如编译,测试,打包,部署等 - - 对于default生命周期,每个事件在执行之前都会**将之前的所有事件依次执行一遍** - - ![](https://gitee.com/seazean/images/raw/master/Frame/Maven-default生命周期.png) - -* site:产生报告,发布站点等 - - * pre-site:执行一些在生成站点文档之前的工作 - * site:生成项目的站点文档 - * post-site:执行一些在生成站点文档之后完成的工作,并为部署做准备 - * site-deploy:将生成的站点文档部署到特定的服务器上 - - - -*** - - - -### 执行事件 - -Maven的插件用来执行生命周期中的相关事件 - -- 插件与生命周期内的阶段绑定,在执行到对应生命周期时执行对应的插件 - -- maven默认在各个生命周期上都绑定了预先设定的插件来完成相应功能 - -- 插件还可以完成一些自定义功能 - - ```xml - - - - org.apache.maven.plugins - maven-source-plugin - 2.2.1 - - - - - - - jar - - test-jar - - - generate-test-resources - - - - - - ``` - - - -*** - - - -## 模块开发 - -### 拆分 - -工程模块与模块划分: - -![](https://gitee.com/seazean/images/raw/master/Frame/Maven模块划分.png) - -* ssm_pojo拆分 - - * 新建模块,拷贝原始项目中对应的相关内容到ssm_pojo模块中 - * 实体类(User) - * 配置文件(无) - -* ssm_dao拆分 - - * 新建模块 - - * 拷贝原始项目中对应的相关内容到ssm_dao模块中 - - - 数据层接口(UserDao) - - - 配置文件:保留与数据层相关配置文件(3个) - - - 注意:分页插件在配置中与SqlSessionFactoryBean绑定,需要保留 - - - pom.xml:引入数据层相关坐标即可,删除springmvc相关坐标 - - - spring - - mybatis - - spring 整合mybatis - - mysql - - druid - - pagehelper - - 直接依赖ssm_pojo(对ssm_pojo模块执行install指令,将其安装到本地仓库) - - ```xml - - - - demo - ssm_pojo - 1.0-SNAPSHOT - - - - - - - - - - - ``` - -* ssm_service拆分 - - * 新建模块 - * 拷贝原始项目中对应的相关内容到ssm_service模块中 - - - 业务层接口与实现类(UserService、UserServiceImpl) - - 配置文件:保留与数据层相关配置文件(1个) - - pom.xml:引入数据层相关坐标即可,删除springmvc相关坐标 - - - spring - - - junit - - - spring 整合junit - - - 直接依赖ssm_dao(对ssm_dao模块执行install指令,将其安装到本地仓库) - - - 间接依赖ssm_pojo(由ssm_dao模块负责依赖关系的建立) - - 修改service模块spring核心配置文件名,添加模块名称,格式:applicationContext-service.xml - - 修改dao模块spring核心配置文件名,添加模块名称,格式:applicationContext-dao.xml - - 修改单元测试引入的配置文件名称,由单个文件修改为多个文件 - -* ssm_control拆分 - - * 新建模块(使用webapp模板) - - * 拷贝原始项目中对应的相关内容到ssm_controller模块中 - - - 现层控制器类与相关设置类(UserController、异常相关……) - - - 配置文件:保留与表现层相关配置文件(1个)、服务器相关配置文件(1个) - - - pom.xml:引入数据层相关坐标即可,删除springmvc相关坐标 - - - spring - - - springmvc - - - jackson - - - servlet - - - tomcat服务器插件 - - - 直接依赖ssm_service(对ssm_service模块执行install指令,将其安装到本地仓库) - - - 间接依赖ssm_dao、ssm_pojo - - ```xml - - - - demo - ssm_service - 1.0-SNAPSHOT - - - - - - - - - - - - - - - - ``` - - - 修改web.xml配置文件中加载spring环境的配置文件名称,使用*通配,加载所有applicationContext-开始的配置文件: - - ```xml - - - contextConfigLocation - classpath*:applicationContext-*.xml - - ``` - - - spring-mvc - - ```xml - - - ``` - - - -*** - - - -### 聚合 - -作用:聚合用于快速构建maven工程,一次性构建多个项目/模块。 - -制作方式: - -- 创建一个空模块,打包类型定义为pom - - ```xml - pom - ``` - -- 定义当前模块进行构建操作时关联的其他模块名称 - - ```xml - - - 4.0.0 - - demo - ssm - 1.0-SNAPSHOT - - - pom - - - - - ../ssm_pojo - ../ssm_dao - ../ssm_service - ../ssm_controller - - - ``` - -注意事项:参与聚合操作的模块最终执行顺序与模块间的依赖关系有关,与配置顺序无关 - - - -*** - - - -### 继承 - -作用:通过继承可以实现在子工程中沿用父工程中的配置 - -- maven中的继承与java中的继承相似,在子工程中配置继承关系 - -制作方式: - -- 在子工程中声明其父工程坐标与对应的位置 - - ```xml - - - com.seazean - ssm - 1.0-SNAPSHOT - - ../ssm/pom.xml - - ``` - -- 继承依赖的定义:在父工程中定义依赖管理 - - ```xml - - - - - - - org.springframework - spring-context - 5.1.9.RELEASE - - - - - ``` - -- 继承依赖的使用:在子工程中定义依赖关系,**无需声明依赖版本**,版本参照父工程中依赖的版本 - - ```xml - - - - org.springframework - spring-context - - - ``` - -- 继承的资源: - - ```xml - groupId:项目组ID,项目坐标的核心元素 - version:项目版本,项目坐标的核心因素 - description:项目的描述信息 - organization:项目的组织信息 - inceptionYear:项目的创始年份 - url:项目的URL地址 - developers:项目的开发者信息 - contributors:项目的贡献者信息 - distributionManagement:项目的部署配置 - issueManagement:项目的缺陷跟踪系统信息 - ciManagement:项目的持续集成系统信息 - scm:项目的版本控制系统西溪 - malilingLists:项目的邮件列表信息 - properties:自定义的Maven属性 - dependencies:项目的依赖配置 - dependencyManagement:项目的依赖管理配置 - repositories:项目的仓库配置 - build:包括项目的源码目录配置、输出目录配置、插件配置、插件管理配置等 - reporting:包括项目的报告输出目录配置、报告插件配置等 - ``` - -- 继承与聚合: - - 作用: - - - 聚合用于快速构建项目 - - - 继承用于快速配置 - - 相同点: - - - 聚合与继承的pom.xml文件打包方式均为pom,可以将两种关系制作到同一个pom文件中 - - - 聚合与继承均属于设计型模块,并无实际的模块内容 - - 不同点: - - - 聚合是在当前模块中配置关系,聚合可以感知到参与聚合的模块有哪些 - - - 继承是在子模块中配置关系,父模块无法感知哪些子模块继承了自己 - - - -*** - - - -### 属性 - -* 版本统一的重要性: - - ![](https://gitee.com/seazean/images/raw/master/Frame/Maven版本统一的重要性.png) - -* 属性类别: - - 1.自定义属性 2.内置属性 3.Setting属性 4.Java系统属性 5.环境变量属性 - -* 自定义属性: - - 作用:等同于定义变量,方便统一维护 - - 定义格式: - - ```xml - - - 5.1.9.RELEASE - 4.12 - - ``` - - - 聚合与继承的pom.xml文件打包方式均为pom,可以将两种关系制作到同一个pom文件中 - - - 聚合与继承均属于设计型模块,并无实际的模块内容 - - 调用格式: - - ```xml - - org.springframework - spring-context - ${spring.version} - - ``` - -* 内置属性: - - 作用:使用maven内置属性,快速配置 - - 调用格式: - - ```xml - ${project.basedir} or ${project.basedir} - ${version} or ${project.version} - ``` - - * vresion是1.0-SNAPSHOT - - ```xml - demo - ssm - 1.0-SNAPSHOT - ``` - -* Setting属性 - - - 使用Maven配置文件setting.xml中的标签属性,用于动态配置 - - 调用格式: - - ```xml - ${settings.localRepository} - ``` - -* Java系统属性: - - 作用:读取Java系统属性 - - 调用格式: - - ``` - ${user.home} - ``` - - 系统属性查询方式 cmd命令: - - ```sh - mvn help:system - ``` - -* 环境变量属性 - - 作用:使用Maven配置文件setting.xml中的标签属性,用于动态配置 - - 调用格式: - - ``` - ${env.JAVA_HOME} - ``` - - 环境变量属性查询方式: - - ``` - mvn help:system - ``` - - - - -*** - - - -### 工程版本 - -SNAPSHOT(快照版本) - -- 项目开发过程中,为方便团队成员合作,解决模块间相互依赖和时时更新的问题,开发者对每个模块进行构建的时候,输出的临时性版本叫快照版本(测试阶段版本) - -- 快照版本会随着开发的进展不断更新 - -RELEASE(发布版本) - -- 项目开发到进入阶段里程碑后,向团队外部发布较为稳定的版本,这种版本所对应的构件文件是稳定的,即便进行功能的后续开发,也不会改变当前发布版本内容,这种版本称为发布版本 - -约定规范: - -- <主版本>.<次版本>.<增量版本>.<里程碑版本> - -- 主版本:表示项目重大架构的变更,如:spring5相较于spring4的迭代 - -- 次版本:表示有较大的功能增加和变化,或者全面系统地修复漏洞 - -- 增量版本:表示有重大漏洞的修复 - -- 里程碑版本:表明一个版本的里程碑(版本内部)。这样的版本同下一个正式版本相比,相对来说不是很稳定,有待更多的测试 - -范例: - -- 5.1.9.RELEASE - - - -*** - - - - - -### 资源配置 - -作用:在任意配置文件中加载pom文件中定义的属性 - -* 父文件pom.xml - - ```xml - - jdbc:mysql://192.168.0.137:3306/ssm_db?useSSL=false - - ``` - -- 开启配置文件加载pom属性: - - ```xml - - - - - ${project.basedir}/src/main/resources - - true - - - ``` - -* properties文件中调用格式: - - ```xml-dtd - jdbc.driver=com.mysql.jdbc.Driver - jdbc.url=${jdbc.url} - jdbc.username=root - jdbc.password=123456 - ``` - - - -*** - - - -### 多环境配置 - -* 环境配置 - - ```xml - - - - - - pro_env - - - jdbc:mysql://127.1.1.1:3306/ssm_db - - - - true - - - - - dev_env - …… - - - ``` - -* 加载指定环境 - - 作用:加载指定环境配置 - - 调用格式: - - ``` - mvn 指令 –P 环境定义id - ``` - - 范例: - - ``` - mvn install –P pro_env - ``` - - - - -*** - - - -## 跳过测试 - -### 命令跳过 - -命令: - -``` -mvn 指令 –D skipTests -``` - -注意事项:执行的指令生命周期必须包含测试环节 - - - -### IEDA界面 - -![](https://gitee.com/seazean/images/raw/master/Frame/IDEA使用界面操作跳过测试.png) - - - -### 配置跳过 - -```xml - - - maven-surefire-plugin - 2.22.1 - - true - - **/User*Test.java - - - **/User*TestCase.java - - - -``` - - - -*** - - - -## 私服 - -### Nexus - -Nexus是Sonatype公司的一款maven私服产品 - -下载地址:https://help.sonatype.com/repomanager3/download - -启动服务器(命令行启动): - -``` -nexus.exe /run nexus -``` - -访问服务器(默认端口:8081): - -``` -http://localhost:8081 -``` - -修改基础配置信息 - -- 安装路径下etc目录中nexus-default.properties文件保存有nexus基础配置信息,例如默认访问端口 - -修改服务器运行配置信息 - -- 安装路径下bin目录中nexus.vmoptions文件保存有nexus服务器启动对应的配置信息,例如默认占用内存空间 - - - -*** - - - -### 资源操作 - -![](https://gitee.com/seazean/images/raw/master/Frame/Maven私服资源获取.png) - - - -仓库分类: - -* 宿主仓库hosted - * 保存无法从中央仓库获取的资源 - * 自主研发 - * 第三方非开源项目 - -* 代理仓库proxy - * 代理远程仓库,通过nexus访问其他公共仓库,例如中央仓库 - -* 仓库组group - * 将若干个仓库组成一个群组,简化配置 - * 仓库组不能保存资源,属于设计型仓库 - - - -资源上传,上传资源时提供对应的信息 - -- 保存的位置(宿主仓库) - -- 资源文件 - -- 对应坐标 - - - -*** - - - -### IDEA操作 - -#### 上传下载 - -![](https://gitee.com/seazean/images/raw/master/Frame/IDEA环境中资源上传与下载.png) - - - -*** - - - -#### 访问私服配置 - -##### 本地仓库访问私服 - -配置本地仓库访问私服的权限(setting.xml) - -```xml - - - heima-release - admin - admin - - - heima-snapshots - admin - admin - - -``` - -配置本地仓库资源来源(setting.xml) - -```xml - - - nexus-heima - * - http://localhost:8081/repository/maven-public/ - - -``` - - - -##### 项目工程访问私服 - -配置当前项目访问私服上传资源的保存位置(pom.xml) - -```xml - - - heima-release - http://localhost:8081/repository/heima-release/ - - - heima-snapshots - http://localhost:8081/repository/heima-snapshots/ - - -``` - -发布资源到私服命令 - -``` -mvn deploy -``` - - - - - -*** - - - -## 日志 - -### Log4j - -程序中的日志可以用来记录程序在运行时候的详情,并可以进行永久存储。 - -| | 输出语句 | 日志技术 | -| -------- | -------------------------- | ---------------------------------------- | -| 取消日志 | 需要修改代码,灵活性比较差 | 不需要修改代码,灵活性比较好 | -| 输出位置 | 只能是控制台 | 可以将日志信息写入到文件或者数据库中 | -| 多线程 | 和业务代码处于一个线程中 | 多线程方式记录日志,不影响业务代码的性能 | - -Log4j是Apache的一个开源项目。 -使用Log4j,通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。我们可以控制日志信息输送的目的地是控制台、文件等位置,也可以控制每一条日志的输出格式。 - - - - - -*** - - - -### 配置文件 - -配置文件的三个核心: - -+ 配置根Logger - - + 格式:log4j.rootLogger=日志级别,appenderName1,appenderName2,… - - + 日志级别:常见的五个级别:**DEBUG < INFO < WARN < ERROR < FATAL**(可以自定义) - Log4j规则:只输出级别不低于设定级别的日志信息 - - + appenderName1:指定日志信息要输出地址。可以同时指定多个输出目的地,用逗号隔开: - - 例如:log4j.rootLogger=INFO,ca,fa - -+ Appenders(输出源):日志要输出的地方,如控制台(Console)、文件(Files)等 - - + Appenders取值: - + org.apache.log4j.ConsoleAppender(控制台) - + org.apache.log4j.FileAppender(文件) - - + ConsoleAppender常用参数 - + `ImmediateFlush=true`:表示所有消息都会被立即输出,设为false则不输出,默认值是true。 - + `Target=System.err`:默认值是System.out - + FileAppender常用的选项 - + `ImmediateFlush=true`:表示所有消息都会被立即输出。设为false则不输出,默认值是true - - + `Append=false`:true表示将消息添加到指定文件中,原来的消息不覆盖。默认值是true - - + `File=E:/logs/logging.log4j`:指定消息输出到logging.log4j文件中 - -+ Layouts(布局):日志输出的格式,常用的布局管理器: - - + org.apache.log4j.PatternLayout(可以灵活地指定布局模式) - -+ org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串) - -+ org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等信息) - -+ PatternLayout常用的选项 - - - - -*** - - - -### 日志应用 - -* log4j的配置文件,名字为log4j.properties, 放在src根目录下 - - ```properties - log4j.rootLogger=debug,my,fileAppender - - ### direct log messages to my ### - log4j.appender.my=org.apache.log4j.ConsoleAppender - log4j.appender.my.ImmediateFlush = true - log4j.appender.my.Target=System.out - log4j.appender.my.layout=org.apache.log4j.PatternLayout - log4j.appender.my.layout.ConversionPattern=%d %t %5p %c{1}:%L - %m%n - - # fileAppender演示 - log4j.appender.fileAppender=org.apache.log4j.FileAppender - log4j.appender.fileAppender.ImmediateFlush = true - log4j.appender.fileAppender.Append=true - log4j.appender.fileAppender.File=E:/log4j-log.log - log4j.appender.fileAppender.layout=org.apache.log4j.PatternLayout - log4j.appender.fileAppender.layout.ConversionPattern=%d %5p %c{1}:%L - %m%n - ``` - -* 测试类 - - ```java - // 测试类 - public class Log4JTest01 { - - //使用log4j的api来获取日志的对象 - //弊端:如果以后我们更换日志的实现类,那么下面的代码就需要跟着改 - //不推荐使用 - //private static final Logger LOGGER = Logger.getLogger(Log4JTest01.class); - - //使用slf4j里面的api来获取日志的对象 - //好处:如果以后我们更换日志的实现类,那么下面的代码不需要跟着修改 - //推荐使用 - private static final Logger LOGGER = LoggerFactory.getLogger(Log4JTest01.class); - - public static void main(String[] args) { - //1.导入jar包 - //2.编写配置文件 - //3.在代码中获取日志的对象 - //4.按照日志级别设置日志信息 - LOGGER.debug("debug级别的日志"); - LOGGER.info("info级别的日志"); - LOGGER.warn("warn级别的日志"); - LOGGER.error("error级别的日志"); - } - } - ``` - - - - - - - - - - - -*** - - - - - - - -# Dubbo - -## 相关概念 - -### 互联网架构 - -**衡量网站的性能指标:** - -* 响应时间:指执行一个请求从开始到最后收到响应数据所花费的总体时间 - -* 并发数:指系统同时能处理的请求数量 - -* 并发连接数:指的是客户端向服务器发起请求,并建立了TCP连接。每秒钟服务器连接的总TCP数量 - -* 请求数:也称为QPS(Query Per Second)指每秒多少请求 - -* 并发用户数:单位时间内有多少用户 - -* 吞吐量:指单位时间内系统能处理的请求数量。 - - ```java - QPS: Query Per Second每秒查询数。 - TPS: Transactions Per Second每秒事务数。 - ●一个事务是指一 个客户机向服务器发送请求然后服务器做出反应的过程。客户机在发送请求时开始计时,收到服务器响应后结束计时,以此来计算使用的时间和完成的事务个数。 - ●一个页面的一次访问,只会形成一个TPS; 但1次页面请求,可能产生多次对服务器的请求,就会有多个QPS: QPS>=并发连接数>= TPS - ``` - - - -**大型互联网项目架构目标:** - -* 高性能:提供快速的访问体验。 -* 高可用:网站服务- 可以正常访问 - - - -*** - - - -### 架构演进 - -#### 单体架构 - -**单体架构**的优点:简单,开发部署都很方便,小型项目首选 - -单体架构存在的问题: - -- 项目启动慢 - -- 可靠性差 - -- 可伸缩性差 - -- 扩展性和可维护性差 - -- 性能低 - -![](https://gitee.com/seazean/images/raw/master/Frame/Dubbo-单体架构.png) - -#### 垂直架构 - -**垂直架构**:指将单体架构中的多个模块拆分为多个独立的项目,形成多个独立的单体架构 - -垂直架构存在的问题:重复功能太多 - -![](https://gitee.com/seazean/images/raw/master/Frame/Dubbo-垂直架构.png) - - - -#### 分布式架构 - -**分布式架构**:在垂直架构的基础上,将公共业务模块抽取出来。作为独立的服务供其他调用者消费,以实现服务的共享和重用,底层通过RPC(远程过程调用实现) - -RPC:Remote Procedure Call 远程过程调用。有非常多的协议和技术来都实现了RPC的过程。比如: HTTP REST风格,Java RMI规范、WebService SOAP协议Hession等 - -分布式架构存在的问题:服务提供方一旦产生变更,所有消费方都需要变更 - -![](https://gitee.com/seazean/images/raw/master/Frame/Dubbo-分布式架构.png) - -#### SOA架构 - -**SOA (Service- Oriented Architecture,面向服务的架构)**:是一个组件模型,将应用程序的不同功能单元 (称为服务) 进行拆分,并通过这些服务之间定义良好的接口和契约联系起来 - -**ESB (Enterparise Servce Bus)**:企业服务总线,服务中介,主要是提供了一个服务于服务之间的交互。ESB包含的功能如:负载均衡、流量控制、加密处理、服务的监控、异常处理,监控告急等等 - -![Dubbo-SOA架构](https://gitee.com/seazean/images/raw/master/Frame/Dubbo-SOA架构.png) - - - -#### 微服务架构 - -**微服务架构**:在SOA上做的提升,微服务架构强调的重点是“业务需要彻底的组件化和服务化”,原有的单个业务系统会拆分为多个可以独立开、运行的小应用,这些小应用之间通过服务完成交互和集成。 - -特点: - -* 微服务架构 = 80%的SOA服务架构思想 + 100%的组件化架构思想 + 80%的领域建模思想 - -* 服务实现组件化:开发者可以自由选择开发技术。也不需要协调其他团队 -* 服务之间交互一 般使用REST API -* 去中心化:每个微服务有自己私有的数据库持久化业务数据 -* 自动化部署:把应用拆分成为一 个个独立的单个服务,方便自动化部署、测试、运维 - -![](https://gitee.com/seazean/images/raw/master/Frame/Dubbo-微服务架构.png) - - - - - -*** - - - -## Dubbo - -分布式:通过网络连接的多个组件,通过交换信息协作而形成的系统。简单说是一个业务分拆多个子业务,部署在不同的服务器上。 - -集群:同一种组件的多个实例,形成的逻辑上的整体。简单说是同一个业务,部署在多个服务器上。 - -Dubbo概念: - -* Dubbo是阿里巴巴公司开源的一个高性能、轻量级的Java RPC框架。 -* 致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。 -* 官网: https://dubbo.apache.org/zh/ - -![](https://gitee.com/seazean/images/raw/master/Frame/Dubbo架构图.png) - -节点角色说明: - -* Provider:暴露服务的服务提供方 -* Contahier:服务运行容器 -* Consumer:调用远程服务的服务消费方 -* Registry:服务注册与发现的注册中心 -* Monitor:统计服务的调用次数和调用时间的监控中心 - -Dubbo目前支持4种**注册中心**:multicast、zookeeper、redis、simple - -安装zookeeper : - -* 第一步:安装JDK - -* 第二步:把 zookeeper 的压缩包(zookeeper-3.4.6.tar.gz)上传到 linux 系统 - -* 第三步:解压缩压缩包 - - ```sh - tar -zxvf zookeeper-3.4.6.tar.gz - ``` - -* 第四步:进入zookeeper-3.4.6目录,创建data目录 - - ```sh - mkdir data - ``` - -* 第五步:进入conf目录 ,把zoo_sample.cfg 改名为zoo.cfg - - ```sh - cd conf - mv zoo_sample.cfg zoo.cfg - ``` - -* 第六步:打开zoo.cfg文件, 修改data属性 - - ```sh - dataDir=/root/zookeeper-3.4.6/data - ``` - -* 第七步:进入ZooKeeper的bin目录 - - ```sh - ./zkServer.sh start #启动服务命令:STARTED - ./zkServer.sh stop #停止服务命令 - ./zkServer.sh status #查看服务状态:standalone 单节点 - ``` - - - -*** - - - -## 基本操作 - -### SS整合 - -Spring和SpringMVC整合步骤: - 1.创建服务提供者Provider模块 - 2.创建服务消费者Consumer模块 - 3.在服务提供者模块编写UserServicelmpl提供服务 - 4.在服务消费者中的UserC ontroller远程调用 - 5.UserServicelmpl提供的服务 - 6.分别启动两个服务,测试 - -Dubbo作为一个RPC框架,其最核心的功能就是要**实现跨网络的远程调用**。创建两个应用,一个作为服务的提供方,一个作为服务的消费方,通过Dubbo来实现服务消费方远程调用服务提供方的方法。 - -1. 服务提供方开发 - - (1)创建maven工程(打包方式为war)dubbodemo_provider,在pom.xml文件中导入如下坐标 - - ```xml - - 5.1.9.RELEASE - 2.7.4.1 - 4.0.0 - - - - - - - - - - org.apache.dubbo - dubbo - ${dubbo.version} - - - - org.apache.curator - curator-framework - ${zookeeper.version} - - - - org.apache.curator - curator-recipes - ${zookeeper.version} - - - - - - - - ``` - - (2)配置web.xml文件 - - ```xml - - - contextConfigLocation - classpath*:spring/applicationContext*.xml - - - org.springframework.web.context.ContextLoaderListener - - ``` - - (3)创建服务接口 - - ```java - public interface HelloService { - public String sayHello(String name); - } - ``` - - (4)创建服务实现类 - - **注意:**服务实现类上使用的Service注解是Dubbo提供的,用于对外发布服务 - - ```java - import com.alibaba.dubbo.config.annotation.Service; - import service.HelloService; - - //@Service,Spring类注解,将该类对象创建,放到Spring的IOC容器中 - @Service //Dubbo注解 - public class HelloServiceImpl implements HelloService { - public String sayHello(String name) { - return "hello " + name; - } - } - ``` - - (5)tomcat7:run - -2. 服务消费方开发 - - (1)创建maven工程(打包方式为war)dubbodemo_consumer,pom.xml配置同上,将Tomcat插件的端口号修改,防止冲突 - - (2)配置web.xml文件 - - ```xml - - - springmvc - org.springframework.web.servlet.DispatcherServlet - - - contextConfigLocation - classpath:spring/springmvc.xml - - - - - springmvc - *.do - - ``` - - (3)将服务提供者工程中的HelloService接口复制到当前工程,java.service.HelloService - - ​ 一般**创建dubbodemo_interface模块,**把所有接口放入其中,让其他模块依赖接口模块 - - (4)编写Controller - - 注意:Controller中注入HelloService使用的是Dubbo提供的@Reference注解 - - ```java - @RestController - @RequestMapping("/demo") - public class HelloController { - //@Autowired//本地注入 - /* - 1.从zookeeper注册中心获取UserService的访问url - 2.进行远程调用RPC - 3.将结果封装为一个代理对象,给变量赋值 - */ - @Reference//远程注入 - private HelloService helloService; - - @RequestMapping("/hello") - public String getName(String name){ - //远程调用 - String result = helloService.sayHello(name); - System.out.println(result); - return result; - } - } - ``` - -3. 执行流程: - 先install dubbodemo_provider,然后执行dubbodemo_consumer tomcat7:run - - - -*** - - - -### 服务提供者 - -在dubbodemo_provider工程中src/main/resources下创建applicationContext-service.xml - -```xml - - - - - - - - - - - - -``` - - - - - -### 服务消费者 - -在dubbodemo_consumer工程中src/main/resources下创建applicationContext-web.xml - -```xml - - - - - - - - - -``` - -运行测试:tomcat7:run启动 - -在浏览器输入http://localhost:8082/demo/hello.do?name=Jack,查看浏览器输出结果 - - - - - -**** - - - -## 高级特性 - -### admin - -dubbo-admin安装: - -dubbo-admin使用: - - - - - -### 序列化 - -dubbo 内部已经将序列化和反序列化的过程内部封装了,我们只需要在定义pojo类时**实现serializable接口**即可,一般会定义一个公共的pojo模块,让生产者和消费者都依赖该模块。 - -```java -public class User implements Serializable -``` - - - - - -### 地址缓存 - -注册中心关闭,服务是否可以正常访问? - -1. 可以,因为dubbo服务消费者在第一次调用时,会将服务提供方地址缓存到本地,以后在调用则不会访问注册中心 -2. 当服务提供者地址发生变化时,注册中心会通知服务消费者 - - - -**** - - - -### 超时重传 - -超时原因: - -- 服务消费者在调用服务提供者的时候发生了阻塞、等待的情形,服务消费者会直等待下去 -- 在某个峰值时刻,大量的请求都在同时请求服务消费者,会造成线程的大量堆积,势必会造成雪崩 - -解决方法: - -- dubbo利用超时机制来解决这个问题,设置一个超时时间,在这个时间段内,无法完成服务访问,则自动断开连接 -- 使用timeout属性配置超时时间,默认值1000,单位毫秒 -- 配置服务提供者类 - -```java -//timeout 超时时间 单位毫秒 -@Service(timeout = 3000) -``` - - - -重传原因: - -* 设置了超时时间,在这个时间段内,无法完成服务访问,则自动断开连接 -* 如果出现网络抖动,则这一次请求就会失败 - -解决方法: - -* Dubbo提供重试机制来避免类似问题的发生 -* 通过retries属性来设置重试次数,默认为2次 - -```java -//timeout 超时时间 单位毫秒 retries 重试次数 -@Service(timeout = 3000,retries=0) -``` - - - -*** - - - -### 多版本 - -**灰度发布:**当出现新功能会让一部分用户先使用新功能,用户反馈没问题,再将所有用户迁移到新功能 - -dubbo中使用version属性来设置和调用同一个接口的不同版本 - -生产者配置: - -```java -@Service(version="v2.0") -public class UserServiceImp12 implements UserService {...} -``` - -消费者配置: - -```java -@RestController -@RequestMapping("/user") -public class UserController { - @Reference(version = "v2.0")//远程注入 - private UserService userService; - //...... -} -``` - - - -*** - - - -### 负载均衡 - -**负载均衡策略(4种) :** - -* Random:按权重设置随机概率,是负载均衡策略的默认值 - -* RoundRobin:按权重轮询 -* LeastActive:最少活跃调用数,相同活跃数的随机 - -* ConsistentHash:一 致性Hash,相同参数的请求总是发到同一提供者 - - - -服务提供者配置: - -```java -@Service(weight = 100) -public class UserServiceImp12 implements UserService {...} -``` - -配置applicationContext.xml - -```xml - - - - - - - - - - -``` - -消费者配置: - -```java -@RestController -@RequestMapping("/user") -public class UserController { - //@Reference(loadbalance = "roundrobin") - //@Reference(loadbalance = "leastactive") - //@Reference(loadbalance = "consistenthash") - @Reference(loadbalance = "random")//默认 按权重随机 - private UserService userService; - //..... -} -``` - - - -*** - - - -### 集群容错 - -**集群容错模式:** - -* Failover Cluster:失败重试,当出现失败,重试其它服务器,默认重试2次,使用retries配置,是模式的默认值,一般用于读操作 -* Failfast Cluster:快速失败,发起一次调用,失败立即报错,通常用于写操作 -* Failsafe Cluster:失败安全,出现异常时,直接忽略,返回一个空结果 -* Failback Cluster:失败自动恢复,后台记录失败请求,定时重发 -* Forking Cluster:并行调用多个服务器,只要一个成功即返回 -* Broadcast Cluster:广播调用所有提供者,逐个调用,任意一台报错则报错 - -消费者配置: - -```java -@Reference(cluster = "failover")//远程注入 -private UserService userService; -``` - - - -*** - - - -### 服务降级 - -**服务降级**:当服务器压力剧增的情况下,根据实际业务情况及流量,对一些服务和页面有策略的不处理或换种简单的方式处理,从而释放服务器资源以保证**核心交易**正常运作或高效运作 - -**服务降级方式:** - -* mock= force:return null:表示消费方对该服务的方法调用都直接返回null值,不发起远程调用。用来屏蔽不重要服务不可用时对调用方的影响 - -* mock=fail:return null:表示消费方对该服务的方法调用在失败后,再返回null值,不抛异常。用来容忍不重要服务不稳定时对调用方的影响 - -消费方配置: - -```java -//远程注入 -@Reference(mock ="force:return null")//不再调用userService的服务 -private UserService userService; -``` - - - - - - - -*** - - - - - - - -# ZooKeeper - -## 基本概述 - -Zookeeper 是 Apache Hadoop 项目下的一个子项目,是一个树形目录服务 - -Zookeeper 翻译为动物园管理员,用来管理 Hadoop(大象)、Hive(蜜蜂)、Pig(小猪)的管理员,简称zk - -Zookeeper 是一个分布式的、开源的分布式应用程序的协调服务 - -Zookeeper 提供的主要功能包括: - -* 配置管理 - -* 分布式锁 - -* 集群管理 - - - -安装配置: - -安装:Dubbo章节详解了安装步骤 - - - - - -*** - - - -## 命令操作 - -### 数据模型 - -ZooKeeper 是一个树形目录服务,其数据模型和Unix的文件系统目录树很类似,拥有一个层次化结构,这里面的每一个节点都被称为:ZNode,每个节点上都会保存自己的数据和节点信息。节点可以拥有子节点,同时也允许少量(1MB)数据存储在该节点之下。 - -节点可以分为四大类: - -* PERSISTENT 持久化节点 -* EPHEMERAL 临时节点 :-e -* PERSISTENT_SEQUENTIAL 持久化顺序节点 :-s -* EPHEMERAL_SEQUENTIAL 临时顺序节点 :-es - -![](https://gitee.com/seazean/images/raw/master/Frame/ZooKeeper树形目录节点.png) - - - -*** - - - -### 服务端命令 - -启动 ZooKeeper 服务:`./zkServer.sh start` - -查看 ZooKeeper 服务:`./zkServer.sh status` - -停止 ZooKeeper 服务:`./zkServer.sh stop` - -重启 ZooKeeper 服务:`./zkServer.sh restart ` - - - - - -### 客户端命令 - -连接ZooKeeper服务端: - -```shell -./zkCli.sh –server ip:port -``` - -断开连接: - -```shell -quit -``` - -查看命令帮助: - -```shell -help -``` - -显示指定目录下节点: - -```sh -ls /目录 (/代表根目录) -``` - -创建节点: - -```sh -create /节点path (可选value) -``` - -获取节点值: - -```sh -get /节点path -``` - -设置节点值 - -```sh -set /节点path value -``` - -删除单个节点 - -```sh -delete /节点path -``` - -删除带有子节点的节点 - -```sh -deleteall /节点path -``` - -创建临时节点: - -```sh -create -e /节点path value -``` - -创建顺序节点 - -```sh -create -s /节点path value -``` - -创建临时顺序节点: - -```sh -create -es /节点path value #app10000012 删除12后也会继续从13开始,只会增加 -``` - -查询节点详细信息 - -```sh -ls –s /节点path -#属性 -czxid:节点被创建的事务ID -ctime: 创建时间 -mzxid: 最后一次被更新的事务ID -mtime: 修改时间 -pzxid:子节点列表最后一次被更新的事务ID -cversion:子节点的版本号 -dataversion:数据版本号 -aclversion:权限版本号 -ephemeralOwner:用于临时节点,代表临时节点的事务ID,如果为持久节点则为0 -dataLength:节点存储的数据的长度 -numChildren:当前节点的子节点个数 -``` - - - -*** - - - -## JavaAPI - -### Curator - -Curator 是 Apache ZoCoKeeper 的Java客户端库。 - -常见的ZooKeeper Java API:原生Java API、ZkClient、Curator - -Curator 项目的目标是简化 ZooKeeper 客户端的使用 - -官网:http://curator.apache.org/ - - - -*** - - - -### 建立连接 - -#### 搭建建构 - -搭建Maven项目架构: - -第一步:导入坐标: - -```xml - - - junit - junit - 4.10 - test - - - - - org.apache.curator - curator-framework - 4.0.0 - - - org.apache.curator - curator-recipes - 4.0.0 - - - - - org.slf4j - slf4j-api - 1.7.21 - - - org.slf4j - slf4j-log4j12 - 1.7.21 - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.1 - - 1.8 - 1.8 - - - - -``` - - - -#### 建立连接 - -创建测试类,使用curator连接zookeeper: - -```java -public class CuratorTest { - private CuratorFramework client; - - @Before //在所有测试类运行之前运行 - public void testConnect() { - /* - * @param connectString 连接字符串。zk server地址和端口: - "192.168.149.135:2181,192.168.149.135:2182" - * @param sessionTimeoutMs 会话超时时间 单位ms - * @param connectionTimeoutMs 连接超时时间 单位ms - * @param retryPolicy 重试策略 - */ - //重试策略 - RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000,10); - //1.第一种方式 - CuratorFramework client = CuratorFrameworkFactory. - newClient("192.168.149.135:2181",60 * 1000, 15 * 1000, retryPolicy); - - //重试策略 - RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000, 10); - //2.第二种方式 建议使用 - client = CuratorFrameworkFactory.builder() - .connectString("192.168.200.130:2181") - .sessionTimeoutMs(60 * 1000) - .connectionTimeoutMs(15 * 1000) - .retryPolicy(retryPolicy) - .namespace("seazean")//名称空间 - .build(); - - //开启连接 - client.start(); - } -} -``` - -名称空间:所有的操作都基于名称空间节点下,默认不创建,进行操作节点自动创建 - - - -*** - - - -### 结点操作 - -#### 创建节点 - - 创建节点:create 持久 临时 顺序 数据 - -1. 基本创建:`create().forPath("")` - -2. 创建节点带有数据:`create().forPath("",data)` - -3. 设置节点的类型:`create().withMode().forPath("",data)` - -4. 创建多级节点 /app1/p1:`create().creatingParentsIfNeeded().forPath("",data)` - -```java -@Test -public void testCreate1() throws Exception { - //1. 基本创建 - //如果创建节点,没有指定数据,则默认将当前客户端的ip作为数据存储 - String path = client.create().forPath("/app1"); - System.out.println(path); -} - -@Test -public void testCreate2() throws Exception { - //2. 创建节点 带有数据 - //如果创建节点,没有指定数据,则默认将当前客户端的ip作为数据存储 - String path = client.create().forPath("/app2", "hehe".getBytes()); - System.out.println(path); -} - -@Test -public void testCreate3() throws Exception { - //3. 设置节点的类型 - //默认类型:持久化PERSISTENT - //PERSISTENT_SEQUENTIAL:顺序结点 EPHEMERAL:临时节点 EPHEMERAL_SEQUENTIAL:es - String path = client.create().withMode(CreateMode.EPHEMERAL).forPath("/app3"); - System.out.println(path); -} - -@Test -public void testCreate4() throws Exception { - //4. 创建多级节点 /app1/p1 - //creatingParentsIfNeeded():如果父节点不存在,则创建父节点 - String path = client.create().creatingParentsIfNeeded().forPath("/app4/p1"); - System.out.println(path); -} -``` - - - -#### 查询节点 - -查询节点: - -1. 查询数据:get `getData().forPath()` -2. 查询子节点: ls `getChildren().forPath()` -3. 查询节点状态信息:ls -s `getData().storingStatIn(状态对象).forPath()` - -```java -@Test -public void testGet1() throws Exception { - //1. 查询数据:get - byte[] data = client.getData().forPath("/app1"); - System.out.println(new String(data)); -} - -@Test -public void testGet2() throws Exception { - // 2. 查询子节点: ls - List path = client.getChildren().forPath("/"); - System.out.println(path); -} - -@Test -public void testGet3() throws Exception { - Stat status = new Stat(); - System.out.println(status);//0,0,0,0,0,0,0,0,0,0,0 - //3. 查询节点状态信息:ls -s - client.getData().storingStatIn(status).forPath("/app1"); - System.out.println(status); - //40194,40194,1615468041638,1615468041638,0,0,0,0,15,0,40194 -} -``` - - - - - -#### 修改节点 - -修改节点的数据: - -1. 基本修改数据:`setData().forPath()` -2. 根据版本修改:`setData().withVersion().forPath()` - * version 是通过查询获取的,目的为了让其他客户端或者线程不干扰此客户端的执行 - -```java -@Test -public void testSet() throws Exception { - client.setData().forPath("/app1", "itcast".getBytes()); -} - -@Test -public void testSetForVersion() throws Exception { - Stat status = new Stat(); - //3. 查询节点状态信息:ls -s - client.getData().storingStatIn(status).forPath("/app1"); - int version = status.getVersion();//查询出来的 3 - System.out.println(version); - client.setData().withVersion(version).forPath("/app1", "hehe".getBytes()); -} -``` - - - - - -#### 删除节点 - -删除节点: delete deleteall - -1. 删除单个节点:`delete().forPath("/app1");` -2. 删除带有子节点的节点:`delete().deletingChildrenIfNeeded().forPath("/app1");` -3. 必须成功删除:`client.delete().guaranteed().forPath("/app2");` - * 为了防止网络抖动,本质是重试 -4. 回调:inBackground - -```java -@Test -public void testDelete() throws Exception { - // 1. 删除单个节点 - client.delete().forPath("/app1"); -} - -@Test -public void testDelete2() throws Exception { - //2. 删除带有子节点的节点 - client.delete().deletingChildrenIfNeeded().forPath("/app4"); -} -@Test -public void testDelete3() throws Exception { - //3. 必须成功的删除 - client.delete().guaranteed().forPath("/app2"); -} - -@Test -public void testDelete4() throws Exception { - //4. 回调 - client.delete().guaranteed().inBackground(new BackgroundCallback(){ - @Override - public void processResult(CuratorFramework client, CuratorEvent event) throws Exception { - System.out.println("我被删除了~"); - System.out.println("client:" + client); - System.out.println("event:" + event); - } - }).forPath("/app1"); -} -``` - -testDelete4输出信息: - -```java -client:org.apache.curator.framework.imps.CuratorFrameworkImpl@29c9e168 -event:CuratorEventImpl{type=DELETE, resultCode=0, path='/app1', name='null', children=null, context=null, stat=null, data=null, watchedEvent=null, aclList=null, opResults=null} -``` - - - -*** - - - -### Watch监听 - -#### 监听概述 - -ZooKeeper 允许用户在指定节点上注册一些Watcher,并且在一些特定事件触发的时候,ZooKeeper 服务端会将事件通知到感兴趣的客户端上去,该机制是 ZooKeeper 实现分布式协调服务的重要特性 - -ZooKeeper 中引入了Watcher机制来实现了**发布/订阅功能**,能够让多个订阅者同时监听某一个对象,当一个对象自身状态变化时,会通知所有订阅者 - -Curator引入了 Cache 来实现对 ZooKeeper 服务端事件的监听 - -ZooKeeper提供了三种Watcher: - -* NodeCache:只是监听某一个特定的节点 -* PathChildrenCache:监控一个ZNode的子节点 -* TreeCache:可以监控整个树上的所有节点,类似于PathChildrenCache和NodeCache的组合 - -![](https://gitee.com/seazean/images/raw/master/Frame/ZooKeeper-Watch监听.png) - - - - - -#### NodeCache - -NodeCache:给指定一个节点注册监听器 - -```java -@Test -public void testNodeCache() throws Exception { - //1. 创建NodeCache对象 - final NodeCache nodeCache = new NodeCache(client,"/app1"); - //2. 注册监听 - nodeCache.getListenable().addListener(new NodeCacheListener() { - @Override - public void nodeChanged() throws Exception { - System.out.println("节点变化了~"); - //获取修改节点后的数据 - byte[] data = nodeCache.getCurrentData().getData(); - System.out.println(new String(data)); - } - }); - - //3. 开启监听.如果设置为true,则开启监听是,加载缓冲数据 - nodeCache.start(true); - - while (true){ - //不循环直接结束,无法监听到信息 - } -} -``` - - - - - -#### PathChildren - -PathChildrenCache:监听某个节点的所有子节点们 - -```java -@Test -public void testPathChildrenCache() throws Exception { - //1.创建监听对象 - PathChildrenCache pathChildrenCache = new PathChildrenCache(client,"/app2",true); - - //2. 绑定监听器 - pathChildrenCache.getListenable().addListener(new PathChildrenCacheListener() { - @Override - public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception { - System.out.println("子节点变化了~"); - System.out.println(event); - //监听子节点的数据变更,并且拿到变更后的数据 - //1.获取类型 - PathChildrenCacheEvent.Type type = event.getType(); - //2.判断类型是否是update - if(type.equals(PathChildrenCacheEvent.Type.CHILD_UPDATED)){ - System.out.println("数据变了!!!"); - byte[] data = event.getData().getData(); - System.out.println(new String(data)); - } - } - }); - //3. 开启 - pathChildrenCache.start(); - - while (true){ - - } -} -``` - - - - - -#### TreeCache - -TreeCache:监听某个节点自己和所有子节点们 - -```java -@Test -public void testTreeCache() throws Exception { - //1. 创建监听器 - TreeCache treeCache = new TreeCache(client,"/app2"); - - //2. 注册监听 - treeCache.getListenable().addListener(new TreeCacheListener() { - @Override - public void childEvent(CuratorFramework client, TreeCacheEvent event) throws Exception { - System.out.println("节点变化了"); - System.out.println(event); - } - }); - //3. 开启 - treeCache.start(); - - while (true){ - - } -} -``` - - - - - -*** - - - -## 分布式锁 - -### 基本概述 - -为了防止分布式系统中的多个进程之间相互干扰,我们需要一种分布式协调技术来对这些进程进行调度,而这个分布式协调技术的核心就是来实现这个**分布式锁** - -在我们进行单机应用开发,涉及并发同步的时候,我们往往采用synchronized或者Lock的方式来解决多线程间的代码同步问题,这时多线程的运行都是在同一个JVM之下,不会出现问题。当我们的应用是分布式集群工作的情况下,属于多JVM下的工作环境,跨JVM之间已经无法通过多线程的锁解决同步问题,需要一种更加高级的锁机制,来处理种跨机器的进程之间的数据同步问题—这就是分布式锁。 - -![](https://gitee.com/seazean/images/raw/master/Frame/ZooKeeper分布式锁.png) - - - - - -*** - - - -### 锁原理 - -核心思想:当客户端要获取锁,则创建节点,使用完锁,则删除该节点。 - -1. 客户端获取锁时,在lock节点下创建**临时顺序**节点 - * 使用临时节点是为了防止当服务器或客户端宕机以后节点无法删除(持久节点),导致锁无法释放 - * 使用顺序节点是为了系统自动编号排序,找最小的节点,防止客户端饥饿现象 - -2. 然后获取lock下面的所有子节点,客户端获取到所有的子节点之后,如果发现自己创建的子节点序号最小,那么就认为该客户端获取到了锁,使用完锁后,将该节点删除 - -3. 如果发现自己创建的节点并非lock所有子节点中最小的,说明自己还没有获取到锁,此时客户端需要找到比自己小的那个节点,同时**对其注册事件监听器,监听删除事件** - -4. 如果发现比自己小的那个节点被删除,则客户端的Watcher会收到相应通知,此时再次判断自己创建的节点是否是lock子节点中序号最小的,如果是则获取到了锁, 如果不是则重复以上步骤继续获取到比自己小的一个节点并注册监听 - -![](https://gitee.com/seazean/images/raw/master/Frame/ZooKeeper分布式锁原理.png) - - - -*** - - - -### 模拟售票 - -Curator实现分布式锁API,在Curator中有五种锁方案: - -- InterProcessSemaphoreMutex:分布式排它锁(非可重入锁) - -- InterProcessMutex:分布式可重入排它锁 - -- InterProcessReadWriteLock:分布式读写锁 - -- InterProcessMultiLock:将多个锁作为单个实体管理的容器 - -- InterProcessSemaphoreV2:共享信号量 - -![](https://gitee.com/seazean/images/raw/master/Frame/ZooKeeper分布式锁售票案例.png) - -注意:要在可以访问数据库的服务加锁,图中是12306服务器 - -1. 创建线程进行加锁设置 - - ```java - public class Ticket12306 implements Runnable{ - private int tickets = 10;//数据库的票数 - private InterProcessMutex lock;//锁 - - public Ticket12306(){ - //重试策略 - RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000, 10); - //2.第二种方式 - CuratorFramework client = CuratorFrameworkFactory.builder() - .connectString("192.168.149.135:2181") - .sessionTimeoutMs(60 * 1000) - .connectionTimeoutMs(15 * 1000) - .retryPolicy(retryPolicy) - .build(); - - //开启连接 - client.start(); - lock = new InterProcessMutex(client,"/lock"); - } - - @Override - public void run() { - while(true){ - //获取锁 - try { - lock.acquire(3, TimeUnit.SECONDS); - if(tickets > 0){ - System.out.println(Thread.currentThread()+":"+tickets); - Thread.sleep(100); - tickets--; - } - } catch (Exception e) { - e.printStackTrace(); - }finally { - //释放锁 - try { - lock.release(); - } catch (Exception e) { - e.printStackTrace(); - } - } - } - } - } - ``` - -2. 创建连接,并且初始化锁 - - ```java - public class LockTest { - public static void main(String[] args) { - Ticket12306 ticket12306 = new Ticket12306(); - - //创建客户端 - Thread t1 = new Thread(ticket12306,"携程"); - Thread t2 = new Thread(ticket12306,"飞猪"); - t1.start(); - t2.start(); - } - } - ``` - - - -*** - - - -## 集群介绍 - -### 核心理论 - -Leader选举: - -* Serverid:服务器ID - 比如有三台服务器,编号分别是1、2、3,编号越大在选择算法中的权重越大 - -* Zxid:数据ID - 服务器中存放的最大数据ID值越大说明数据越新,在选举算法中数据越新权重越大 - -* 在Leader选举的过程中,如果某台ZooKeeper获得了超过半数的选票,则此ZooKeeper就可以成为Leader了 - - - -**Zookeepe集群角色**,在ZooKeeper集群服中务中有三个角色: - -* Leader 领导者 : - - ​ 1. 处理事务请求 - - ​ 2. 集群内部各服务器的调度者 - -* Follower 跟随者 : - - ​ 1. 处理客户端非事务请求,转发事务请求给Leader服务器 - - ​ 2. 参与Leader选举投票 - -* Observer 观察者: - - 1. 处理客户端非事务请求,转发事务请求给Leader服务器 - -![](https://gitee.com/seazean/images/raw/master/Frame/Zookeeper集群角色.png) - - - -*** - - - -### 集群搭建 - -#### 搭建要求 - -真实的集群是需要部署在不同的服务器上的,但是在我们测试时同时启动很多个虚拟机内存会吃不消,所以我们通常会搭建**伪集群**,也就是把所有的服务都搭建在一台虚拟机上,用端口进行区分。 - -我们这里要求搭建一个三个节点的Zookeeper集群(伪集群)。 - -#### 准备工作 - -重新部署一台虚拟机作为我们搭建集群的测试服务器。 - -(1)安装JDK - -(2)Zookeeper压缩包上传到服务器 -(3)将Zookeeper解压 ,建立/usr/local/zookeeper-cluster目录,将解压后的Zookeeper复制到以下三个目录 - -```shell -[root@localhost ~]# mkdir /usr/local/zookeeper-cluster -[root@localhost ~]# cp -r apache-zookeeper-3.5.6-bin /usr/local/zookeeper-cluster/zookeeper-1 -[root@localhost ~]# cp -r apache-zookeeper-3.5.6-bin /usr/local/zookeeper-cluster/zookeeper-2 -[root@localhost ~]# cp -r apache-zookeeper-3.5.6-bin /usr/local/zookeeper-cluster/zookeeper-3 -``` - -(4)创建data目录 ,并且将 conf下zoo_sample.cfg 文件改名为 zoo.cfg - -```shell -mkdir /usr/local/zookeeper-cluster/zookeeper-1/data -mkdir /usr/local/zookeeper-cluster/zookeeper-2/data -mkdir /usr/local/zookeeper-cluster/zookeeper-3/data - -mv /usr/local/zookeeper-cluster/zookeeper-1/conf/zoo_sample.cfg /usr/local/zookeeper-cluster/zookeeper-1/conf/zoo.cfg -mv /usr/local/zookeeper-cluster/zookeeper-2/conf/zoo_sample.cfg /usr/local/zookeeper-cluster/zookeeper-2/conf/zoo.cfg -mv /usr/local/zookeeper-cluster/zookeeper-3/conf/zoo_sample.cfg /usr/local/zookeeper-cluster/zookeeper-3/conf/zoo.cfg -``` - - -(5) 配置每一个Zookeeper 的dataDir 和 clientPort 分别为2181 2182 2183 - -修改/usr/local/zookeeper-cluster/zookeeper-1/conf/zoo.cfg - -```shell -vim /usr/local/zookeeper-cluster/zookeeper-1/conf/zoo.cfg - -clientPort=2181 -dataDir=/usr/local/zookeeper-cluster/zookeeper-1/data -``` - -修改/usr/local/zookeeper-cluster/zookeeper-2/conf/zoo.cfg - -```shell -vim /usr/local/zookeeper-cluster/zookeeper-2/conf/zoo.cfg - -clientPort=2182 -dataDir=/usr/local/zookeeper-cluster/zookeeper-2/data -``` - -修改/usr/local/zookeeper-cluster/zookeeper-3/conf/zoo.cfg - -```shell -vim /usr/local/zookeeper-cluster/zookeeper-3/conf/zoo.cfg - -clientPort=2183 -dataDir=/usr/local/zookeeper-cluster/zookeeper-3/data -``` - - - - -#### 配置集群 - -(1)在每个zookeeper的 data 目录下创建一个 myid 文件,内容分别是1、2、3 。这个文件就是记录每个服务器的ID - -```shell -echo 1 >/usr/local/zookeeper-cluster/zookeeper-1/data/myid -echo 2 >/usr/local/zookeeper-cluster/zookeeper-2/data/myid -echo 3 >/usr/local/zookeeper-cluster/zookeeper-3/data/myid -``` - -(2)在每一个zookeeper 的 zoo.cfg配置客户端访问端口(clientPort)和集群服务器IP列表。 - -集群服务器IP列表如下 - -```shell -vim /usr/local/zookeeper-cluster/zookeeper-1/conf/zoo.cfg -vim /usr/local/zookeeper-cluster/zookeeper-2/conf/zoo.cfg -vim /usr/local/zookeeper-cluster/zookeeper-3/conf/zoo.cfg - -server.1=192.168.149.135:2881:3881 -server.2=192.168.149.135:2882:3882 -server.3=192.168.149.135:2883:3883 -``` - -解释:server.服务器ID=服务器IP地址:服务器之间通信端口:服务器之间投票选举端口 - - - -#### 启动集群 - -启动集群就是分别启动每个实例。 - -```shell -/usr/local/zookeeper-cluster/zookeeper-1/bin/zkServer.sh start -/usr/local/zookeeper-cluster/zookeeper-2/bin/zkServer.sh start -/usr/local/zookeeper-cluster/zookeeper-3/bin/zkServer.sh start -``` - -启动后我们查询一下每个实例的运行状态 - -```shell -/usr/local/zookeeper-cluster/zookeeper-1/bin/zkServer.sh status -/usr/local/zookeeper-cluster/zookeeper-2/bin/zkServer.sh status -/usr/local/zookeeper-cluster/zookeeper-3/bin/zkServer.sh status -``` - -先查询第一个服务:Mode: follower,表示是**跟随者**(从) - -再查询第二个服务Mode: leader,表示是**领导者**(主) - -查询第三个服务:Mode: follower,表示是跟随者(从) - - - -#### 模拟集群异常 - -(1)首先测试如果是从服务器挂掉,会怎么样?把3号服务器停掉,观察1号和2号,发现状态并没有变化 - -```shell -/usr/local/zookeeper-cluster/zookeeper-3/bin/zkServer.sh stop - -/usr/local/zookeeper-cluster/zookeeper-1/bin/zkServer.sh status -/usr/local/zookeeper-cluster/zookeeper-2/bin/zkServer.sh status -``` - -* 结论:3个节点的集群,从服务器挂掉,集群正常 - -(2)再把1号服务器(从服务器)也停掉,查看2号(主服务器)的状态,发现已经停止运行了 - -```shell -/usr/local/zookeeper-cluster/zookeeper-1/bin/zkServer.sh stop - -/usr/local/zookeeper-cluster/zookeeper-2/bin/zkServer.sh status -``` - -* 结论:3个节点的集群,2个从服务器都挂掉,主服务器也无法运行,因为可运行的机器**没有超过集群总数量的半数** - -(3)再次把1号服务器启动起来,2号服务器又开始正常工作了,而且依然是领导者 - -```shell -/usr/local/zookeeper-cluster/zookeeper-1/bin/zkServer.sh start - -/usr/local/zookeeper-cluster/zookeeper-2/bin/zkServer.sh status -``` - -(4)把3号服务器也启动起来,把2号服务器停掉,停掉后观察1号和3号的状态,新的leader产生 - -```shell -/usr/local/zookeeper-cluster/zookeeper-3/bin/zkServer.sh start -/usr/local/zookeeper-cluster/zookeeper-2/bin/zkServer.sh stop - -/usr/local/zookeeper-cluster/zookeeper-1/bin/zkServer.sh status #Mode:follower -/usr/local/zookeeper-cluster/zookeeper-3/bin/zkServer.sh status #Mode:leader -``` - -* 结论:当集群中的主服务器挂了,集群中的其他服务器会自动进行选举状态,然后产生新得leader - -(5)再次测试,当我们把2号服务器重新启动起来启动后,会发生什么?2号服务器会再次成为新的领导吗? - 2号服务器启动后依然是跟随者(从服务器),3号服务器依然是领导者(主服务器) - -```shell -/usr/local/zookeeper-cluster/zookeeper-2/bin/zkServer.sh start - -/usr/local/zookeeper-cluster/zookeeper-2/bin/zkServer.sh status #Mode:follower -/usr/local/zookeeper-cluster/zookeeper-3/bin/zkServer.sh status #Mode:leader -``` - -结论:当领导者产生后,再次有新服务器加入集群,不会影响到现任领导者。 - - - - - - - -**** - - - - - - - -# RabbitMQ - -## 基本概述 - -### 消息中间件 - -MQ全称为Message Queue,消息队列是应用程序和应用程序之间的通信方法。 - -MQ作用:在项目中,可将一些无需即时返回且耗时的操作提取出来,进行**异步处理**,而这种异步处理的方式大大的节省了服务器的请求响应时间,从而**提高**了**系统**的**吞吐量** - -消息队列的应用场景: - -* **任务异步处理**:将不需要同步处理的并且耗时长的操作由消息队列通知消息接收方进行异步处理。提高了应用程序的响应时间 - -* 应用程序**解耦合**:MQ相当于一个中介,生产方通过MQ与消费方交互,将应用程序进行解耦合 - -* **削峰填谷**:如订单系统,在下单的时候就会往数据库写数据。但是数据库只能支撑每秒1000左右的并发写入,并发量再高就容易宕机,在高峰期时候,并发量会突然激增到5000以上,这个时候数据库就会宕机。消息被MQ保存起来了,然后系统就可以按照自己的消费能力来消费,比如每秒1000个数据,这样慢慢写入数据库,但是使用了MQ之后,限制消费消息的速度为1000,高峰期产生的数据势必会被积压在MQ中,高峰就被“削”掉了。因为消息积压,在高峰期过后的一段时间内,消费消息的速度还是会维持在1000QPS,直到消费完积压的消息,这就叫做“填谷” - - ![](https://gitee.com/seazean/images/raw/master/Frame/RabbitMQ-削峰填谷.jpg) - - - - - -### 实现MQ - -MQ是消息通信的模型,实现MQ的大致有两种主流方式:AMQP、JMS - -AMQP是一种协议,更准确的说是一种binary wire-level protocol(链接协议),AMQP不从API层进行限定,而是直接定义网络交换的数据格式 - -JMS即Java消息服务(JavaMessage Service)应用程序接口,是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信 - -AMQP与JMS的区别: - -- JMS是定义了统一的接口,来对消息操作进行统一;AMQP是通过规定协议来统一数据交互的格式 -- JMS限定了必须使用Java语言;AMQP只是协议,不规定实现方式,因此是跨语言的 -- JMS规定了两种消息模式,而AMQP的消息模式更加丰富 - -市场上常见的消息队列有如下: - -- ActiveMQ:基于JMS -- ZeroMQ:基于C语言开发 -- RabbitMQ:基于AMQP协议,erlang语言开发,稳定性好 -- RocketMQ:基于JMS,阿里巴巴产品 -- Kafka:类似MQ的产品;分布式消息系统,高吞吐量 - - - -### RabbitMQ - -RabbitMQ是由erlang语言开发,基于AMQP(Advanced Message Queue 高级消息队列协议)协议实现的消息队列,它是一种应用程序之间的通信方法,消息队列在分布式系统开发中应用非常广泛 - -RabbitMQ官方地址:http://www.rabbitmq.com/ - -RabbitMQ提供了6种模式:简单模式,work模式,Publish/Subscribe发布与订阅模式,Routing路由模式,Topics主题模式,RPC远程调用模式 - -官网对应模式介绍:https://www.rabbitmq.com/getstarted.html - -安装步骤:https://www.jianshu.com/p/d3f10f539925 - -1.安装erlang,由于rabbitMq需要erlang语言的支持,在安装rabbitMq之前需要安装erlang,执行命令: - -> apt-get install erlang-nox # 安装erlang -> -> erl # 查看relang语言版本,成功执行则说明relang安装成功 - -2.添加公钥 - -> wget -O- https://www.rabbitmq.com/rabbitmq-release-signing-key.asc | sudo apt-key add - - -3.更新软件包 - -> apt-get update - -4.安装 RabbitMQ - -> apt-get install rabbitmq-server #安装成功自动启动 - -5.查看 RabbitMq状态 - -> \#Active: active (running) 说明处于运行状态 -> -> systemctl status rabbitmq-server -> -> \# 用service指令也可以查看,同systemctl指令 -> -> service rabbitmq-server status - -6.启动、停止、重启 - -> service rabbitmq-server start # 启动 -> -> service rabbitmq-server stop # 停止 -> -> service rabbitmq-server restart # 重启 - -7.启用 web端可视化操作界面,我们还需要配置Management Plugin插件 - -> \# 启用插件 -> -> rabbitmq-plugins enable rabbitmq_management -> -> \# 装完后重启 -> -> service rabbitmq-server restart - -8.查看rabbitmq用户 - -> rabbitmqctl list_users - -9.添加管理用户 - -> \# 增加普通用户 -> -> rabbitmqctl add_user admin yourpassword -> -> \# 给普通用户分配管理员角色 -> -> rabbitmqctl set_user_tags admin administrator - -10.访问web控制台 - -> 打开浏览器 -> -> http://服务器IP:15672/ 来访问你的rabbitmq监控页面。使用刚刚添加的新用户(admin)登录 - - - - - -*** - - - - - -## AMQP - -### 相关概念 - -AMQP:一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计 - -AMQP:是一个二进制协议,拥有一些现代化特点:多信道、协商式,异步,安全,扩平台,中立,高效 - -RabbitMQ是AMQP协议的Erlang的实现 - -| 概念 | 说明 | -| -------------- | ------------------------------------------------------------ | -| 连接Connection | 一个网络连接,比如TCP/IP套接字连接 | -| 会话Session | 端点之间的命名对话。在一个会话上下文中,保证“恰好传递一次” | -| 信道Channel | 多路复用连接中的一条独立的双向数据流通道。为会话提供物理传输介质 | -| 客户端Client | AMQP连接或者会话的发起者。AMQP是非对称的,客户端生产和消费消息,服务器存储和路由这些消息 | -| 服务节点Broker | 消息中间件的服务节点;一般情况下可以将一个RabbitMQ Broker看作一台RabbitMQ 服务器 | -| 端点 | AMQP对话的任意一方,一个AMQP连接包括两个端点(一个是客户端,一个是服务器) | -| 消费者Consumer | 一个从消息队列里请求消息的客户端程序 | -| 生产者Producer | 一个向交换机发布消息的客户端应用程序 | - - - -*** - - - -### 运转流程 - -基本的运转流程: - -- 生产者发送消息 - 1. 生产者创建连接(Connection),开启一个信道(Channel),连接到RabbitMQ Broker - 2. 声明队列并设置属性;如是否排它,是否持久化,是否自动删除 - 3. 将路由键(空字符串)与队列绑定起来 - 4. 发送消息至RabbitMQ Broker - 5. 关闭信道 - 6. 关闭连接 -- 消费者接收消息 - 1. 消费者创建连接(Connection),开启一个信道(Channel),连接到RabbitMQ Broker - 2. 向Broker 请求消费相应队列中的消息,设置相应的回调函数 - 3. 等待Broker回应闭关投递响应队列中的消息,消费者接收消息 - 4. 确认(ack,自动确认)接收到的消息 - 5. RabbitMQ从队列中删除相应已经被确认的消息 - 6. 关闭信道 - 7. 关闭连接 - -![](https://gitee.com/seazean/images/raw/master/Frame/RabbitMQ-RabbitMQ运转流程.jpg) - - - -生产者运转流程说明: - -1. 客户端与代理服务器Broker建立连接。会调用newConnection() 方法,这个方法会进一步封装Protocol Header 0-9-1 的报文头发送给Broker ,以此通知Broker 本次交互采用的是AMQPO-9-1 协议,紧接着Broker 返回Connection.Start 来建立连接,在连接的过程中涉及Connection.Start/.Start-OK 、Connection.Tune/.Tune-Ok ,Connection.Open/ .Open-Ok 这6 个命令的交互。 - -2. 客户端调用connection.createChannel方法。此方法开启信道,其包装的channel.open命令发送给Broker,等待channel.basicPublish方法,对应的AMQP命令为Basic.Publish,这个命令包含了content Header 和content Body()。content Header 包含了消息体的属性,例如:投递模式,优先级等,content Body 包含了消息体本身。 - -3. 客户端发送完消息需要关闭资源时,涉及到Channel.Close和Channl.Close-Ok 与Connetion.Close和Connection.Close-Ok的命令交互。 - - ![](https://gitee.com/seazean/images/raw/master/Frame/RabbitMQ-生产者流转过程图.bmp) - -消费者运转流程说明: - -1. 消费者客户端与代理服务器Broker建立连接。会调用newConnection() 方法,这个方法会进一步封装Protocol Header 0-9-1 的报文头发送给Broker ,以此通知Broker 本次交互采用的是AMQPO-9-1 协议,紧接着Broker 返回Connection.Start 来建立连接,在连接的过程中涉及Connection.Start/.Start-OK 、Connection.Tune/.Tune-Ok ,Connection.Open/ .Open-Ok 这6 个命令的交互。 - -2. 消费者客户端调用connection.createChannel方法。和生产者客户端一样,协议涉及Channel . Open/Open-Ok命令。 - -3. 在真正消费之前,消费者客户端需要向Broker 发送Basic.Consume 命令(即调用channel.basicConsume 方法〉将Channel 置为接收模式,之后Broker 回执Basic . Consume - Ok 以告诉消费者客户端准备好消费消息。 - -4. Broker 向消费者客户端推送(Push) 消息,即Basic.Deliver 命令,这个命令和Basic.Publish 命令一样会携带Content Header 和Content Body。 - -5. 消费者接收到消息并正确消费之后,向Broker 发送确认,即Basic.Ack 命令。 - -6. 客户端发送完消息需要关闭资源时,涉及到Channel.Close和Channl.Close-Ok 与Connetion.Close和Connection.Close-Ok的命令交互。 - - ![](https://gitee.com/seazean/images/raw/master/Frame/RabbitMQ-消费者流转过程图.bmp) - - - -**** - - - -## 工作模式 - -### Hello World - -入门案例中其实使用的是如下的简单模式,在上图的模型中,有以下概念: - -- P:生产者,也就是要发送消息的程序 -- C:消费者,消息的接受者,会一直等待消息到来 -- queue:消息队列,图中红色部分,类似一个邮箱,可以缓存消息;生产者向其中投递消息,消费者从其中取出消息 - -![](https://gitee.com/seazean/images/raw/master/Frame/RabbitMQ-简单模式.jpg) - -* 两个模块添加依赖:pom.xml - - ```xml - - com.rabbitmq - amqp-client - 5.6.0 - - ``` - -* 生产者模块 - - 在执行消息发送之后;可以登录rabbitMQ的管理控制台,可以发现队列和其消息 - - ```java - public class Producer { - static final String QUEUE_NAME = "simple_queue"; - public static void main(String[] args) throws Exception { - //创建连接工厂 - ConnectionFactory connectionFactory = new ConnectionFactory(); - //主机地址;默认为 localhost - connectionFactory.setHost("192.168.0.137"); - //连接端口;默认为 5672 - connectionFactory.setPort(5672); - //虚拟主机名称;默认为 / - connectionFactory.setVirtualHost("/sea"); - //连接用户名;默认为guest - connectionFactory.setUsername("admin"); - //连接密码;默认为guest - connectionFactory.setPassword("admin"); - - //创建连接 - Connection connection = connectionFactory.newConnection(); - - //创建频道 - Channel channel = connection.createChannel(); - - // 声明(创建)队列 - /** - * 参数1 queue: 队列名称 - * 参数2 durable: 是否定义持久化队列,当mq重启之后,还存在 - * 参数3 exclusive: 是否独占本次连接,只能有一个消费者监听这队列, - 当Connection关闭时,是否删除队列 - * 参数4 autoDelete: 是否在不使用的时候自动删除队列,没有Consumer时,自动删除 - * 参数5 arguments: 队列其它参数 - */ - channel.queueDeclare(QUEUE_NAME, true, false, false, null); - - // 要发送的信息 - String message = "Hello RabbitMQ!"; - /** - * 参数1 exchange: 交换机名称,如果没有指定则使用默认Default Exchage - * 参数2 routingKey: 路由key,简单模式可以传递队列名称 - * 参数3 props: 消息其它属性,配置消息 - * 参数4 body: 消息内容 - */ - channel.basicPublish("", QUEUE_NAME, null, message.getBytes()); - System.out.println("已发送消息:" + message); - - // 关闭资源 - channel.close(); - connection.close(); - } - } - ``` - -* 消费者模块 - - ```java - public class Consumer { - public static void main(String[] args) throws Exception { - //创建连接 - - // 创建频道 - Channel channel = connection.createChannel(); - - // 声明(创建)队列 - //如果没有一个名字叫simple_queue的队列,则会创建该队列,如果有则不会创建 - channel.queueDeclare(Producer.QUEUE_NAME, true, false, false, null); - - //创建消费者;并设置消息处理 - Consumer consumer = new DefaultConsumer(channel){ - @Override - /** - * consumerTag: 消息者标签,在channel.basicConsume时候可以指定 - * envelope: 消息包的内容,可从中获取消息id,消息routingkey,交换机, - 消息和重传标志(收到消息失败后是否需要重新发送) - * properties: 配置属性信息 - * body: 消息 - */ - public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { - //路由key - System.out.println("路由key为:" + envelope.getRoutingKey()); - //交换机 - System.out.println("交换机为:" + envelope.getExchange()); - //消息id - System.out.println("消息id为:" + envelope.getDeliveryTag()); - //收到的消息 - System.out.println("接收到的消息为:" + new String(body, "utf-8")); - } - }; - //监听消息 - /** - * String: 队列名称 - * boolean: 是否自动确认,设置为true为表示消息接收到自动向mq回复接收到了, - mq接收到回复会删除消息,设置为false则需要手动确认 - * Consumer:消息接收到后回调 - */ - channel.basicConsume(Producer.QUEUE_NAME, true, consumer); - - //不关闭资源,应该一直监听消息 - } - } - ``` - - - -**** - - - -### Work queues - -Work queues官网链接:https://www.rabbitmq.com/tutorials/tutorial-two-python.html - -Work Queues与入门程序的简单模式相比,多了一个或一些消费端,多个消费端共同消费同一个队列中的消息 - -* 生产者 - - ```java - public class Producer { - static final String QUEUE_NAME = "work_queue"; - public static void main(String[] args) throws Exception { - // 创建连接 - Connection connection = ConnectionUtil.getConnection(); - // 创建频道 - Channel channel = connection.createChannel(); - //创建队列 - channel.queueDeclare(QUEUE_NAME, true, false, false, null); - - for (int i = 1; i <= 30; i++) { - // 发送信息 - String message = "你好;小兔子!work模式--" + i; - channel.basicPublish("", QUEUE_NAME, null, message.getBytes()); - System.out.println("已发送消息:" + message); - } - // 关闭资源 - channel.close(); - connection.close(); - } - } - ``` - -* 消费者1 - - ```java - public class Consumer1 { - public static void main(String[] args) throws Exception { - Connection connection = ConnectionUtil.getConnection(); - // 创建频道 - Channel channel = connection.createChannel(); - // 创建队列 - channel.queueDeclare(Producer.QUEUE_NAME, true, false, false, null); - - //一次只能接收并处理一个消息 - channel.basicQos(1); - - //创建消费者;并设置消息处理 - DefaultConsumer consumer = new DefaultConsumer(channel){ - @Override - public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { - try { - //路由key - System.out.println("路由key为:" + envelope.getRoutingKey()); - //交换机 - System.out.println("交换机为:" + envelope.getExchange()); - //消息id - System.out.println("消息id为:" + envelope.getDeliveryTag()); - //收到的消息 - System.out.println("消费者1-接收到的消息为:" + - new String(body, "utf-8")); - Thread.sleep(1000); - //确认消息 - channel.basicAck(envelope.getDeliveryTag(), false); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - }; - //监听消息 - channel.basicConsume(Producer.QUEUE_NAME, false, consumer); - } - } - ``` - -* 消费者2同1 - -* 测试结果: - - ```markdown - 消费者1: - 1 3 5... - 消费者2: - 2 4 6... - ``` - -结论:在一个队列中如果有多个消费者,那么消费者之间对于同一个消息的关系是**竞争**的关系 - - - -*** - - - -### Pub/Sub - -Publish/Subscribe:https://www.rabbitmq.com/tutorials/tutorial-three-python.html - -在订阅模型中,多了一个exchange角色,过程略有变化: - -- P:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给X(交换机) -- C:消费者,消息的接受者,会一直等待消息到来 -- Queue:消息队列,接收消息、缓存消息 -- Exchange:交换机。一方面,接收生产者发送的消息,另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。 - Exchange有常见以下3种类型: - - Fanout:广播,将消息交给所有绑定到交换机的队列 - - Direct:定向,把消息交给符合指定routing key 的队列 - - Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列 - -**Exchange(交换机)只负责转发消息,不具备存储消息的能力**,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失 - -* 生产者,发布与订阅使用的交换机类型为:fanout - - ```java - public class Producer { - //交换机名称 - static final String FANOUT_EXCHAGE = "fanout_exchange"; - //队列名称 - static final String FANOUT_QUEUE_1 = "fanout_queue_1"; - //队列名称 - static final String FANOUT_QUEUE_2 = "fanout_queue_2"; - public static void main(String[] args) throws Exception { - // 创建连接 - Connection connection = ConnectionUtil.getConnection(); - // 创建频道 - Channel channel = connection.createChannel(); - - /** - * 声明交换机 - * 参数1:交换机名称 - * 参数2:交换机类型,fanout、topic、direct、headers - */ - channel.exchangeDeclare(FANOUT_EXCHAGE, BuiltinExchangeType.FANOUT); - - // 声明(创建)队列 - channel.queueDeclare(FANOUT_QUEUE_1, true, false, false, null); - channel.queueDeclare(FANOUT_QUEUE_2, true, false, false, null); - - /** - * 队列绑定交换机 - * queue: 队列名称 - * exchange: 交换机名称 - * routingKey: 路由键,绑定规则,若交换机的类型为fanout,routingKey设置"" - */ - channel.queueBind(FANOUT_QUEUE_1, FANOUT_EXCHAGE, ""); - channel.queueBind(FANOUT_QUEUE_2, FANOUT_EXCHAGE, ""); - - for (int i = 1; i <= 10; i++) { - // 发送信息 - String message = "你好;小兔子!发布订阅模式--" + i; - channel.basicPublish(FANOUT_EXCHAGE, "", null, message.getBytes()); - System.out.println("已发送消息:" + message); - } - - // 关闭资源 - channel.close(); - connection.close(); - } - } - ``` - -* 消费者1 - - ```java - ublic class Consumer1 { - public static void main(String[] args) throws Exception { - Connection connection = ConnectionUtil.getConnection(); - // 创建频道 - Channel channel = connection.createChannel(); - - //声明交换机 - - // 声明(创建)队列 - channel.queueDeclare(Producer.FANOUT_QUEUE_1, true, false, false, null); - - //队列绑定交换机,生产者和消费者写一处,一般写在消费者端 - //channel.queueBind(Producer.FANOUT_QUEUE_1, Producer.FANOUT_EXCHAGE, ""); - - //创建消费者;并设置消息处理 - DefaultConsumer consumer = new DefaultConsumer(channel){ - @Override - public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { - //路由key - System.out.println("路由key为:" + envelope.getRoutingKey()); - //交换机 - System.out.println("交换机为:" + envelope.getExchange()); - //消息id - System.out.println("消息id为:" + envelope.getDeliveryTag()); - //收到的消息 - System.out.println("消费者1-接收到的消息为:" + new String(body, "utf-8")); - } - - channel.basicConsume(Producer.FANOUT_QUEUE_1, true, consumer); - } - } - ``` - -* 消费者2同1 - -* 启动所有消费者,然后使用生产者发送消息;在每个消费者对应的控制台可以查看到生产者发送的所有消息;到达**广播**的效果 - -**发布订阅模式与工作队列模式的区别** - -1. 工作队列模式不用定义交换机,而发布/订阅模式需要定义交换机。 - -2. 发布/订阅模式的生产方是面向交换机发送消息,工作队列模式的生产方是面向队列发送消息(底层使用默认交换机) - -3. 发布/订阅模式需要设置队列和交换机的绑定,工作队列模式不需要设置,实际上工作队列模式会将队列绑 定到默认的交换机 - - - -*** - - - -### Routing - -Routing:https://www.rabbitmq.com/tutorials/tutorial-four-python.html - -路由模式特点: - -- 队列与交换机的绑定,不能是任意绑定了,而是要指定一个`RoutingKey`(路由key) -- 消息的发送方在向 Exchange发送消息时,也必须指定消息的 `RoutingKey`。 -- Exchange不再把消息交给每一个绑定的队列,而是根据消息的`Routing Key`进行判断,只有队列的`Routingkey`与消息的 `Routing key`完全一致,才会接收到消息 - -在编码上与 Publish/Subscribe发布与订阅模式 的区别是交换机的类型为:Direct,还有队列绑定交换机的时候需要指定routing key - -* 生产者 - - ```java - public class Producer { - //交换机名称 - static final String DIRECT_EXCHAGE = "direct_exchange"; - //队列名称 - static final String DIRECT_QUEUE_INSERT = "direct_queue_insert"; - //队列名称 - static final String DIRECT_QUEUE_UPDATE = "direct_queue_update"; - - public static void main(String[] args) throws Exception { - - //创建连接 - Connection connection = ConnectionUtil.getConnection(); - - // 创建频道 - Channel channel = connection.createChannel(); - - - //声明交换机:交换机类型,fanout、topic、direct、headers - channel.exchangeDeclare(DIRECT_EXCHAGE, BuiltinExchangeType.DIRECT); - - // 声明(创建)队列 - channel.queueDeclare(DIRECT_QUEUE_INSERT, true, false, false, null); - channel.queueDeclare(DIRECT_QUEUE_UPDATE, true, false, false, null); - - //队列绑定交换机 - channel.queueBind(DIRECT_QUEUE_INSERT, DIRECT_EXCHAGE, "insert"); - channel.queueBind(DIRECT_QUEUE_UPDATE, DIRECT_EXCHAGE, "update"); - - // 发送信息 - /** - * 参数1:交换机名称,如果没有指定则使用默认Default Exchage - * 参数2:路由key,简单模式可以传递队列名称 - * 参数3:消息其它属性 - * 参数4:消息内容 - */ - String message = "新增了商品。路由模式;routing key 为 insert " ; - channel.basicPublish(DIRECT_EXCHAGE, "insert", null, message.getBytes()); - System.out.println("已发送消息:" + message); - - // 发送信息 - message = "修改了商品。路由模式;routing key 为 update" ; - channel.basicPublish(DIRECT_EXCHAGE, "update", null, message.getBytes()); - System.out.println("已发送消息:" + message); - - // 关闭资源 - channel.close(); - connection.close(); - } - } - ``` - -* 消费者1 - - ```java - public class Consumer1 { - public static void main(String[] args) throws Exception { - Connection connection = ConnectionUtil.getConnection(); - // 创建频道 - Channel channel = connection.createChannel(); - - //创建消费者;并设置消息处理 - DefaultConsumer consumer = new DefaultConsumer(channel){ - @Override - public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { - //路由key - System.out.println("路由key为:" + envelope.getRoutingKey()); - //交换机 - System.out.println("交换机为:" + envelope.getExchange()); - //消息id - System.out.println("消息id为:" + envelope.getDeliveryTag()); - //收到的消息 - System.out.println("消费者1-接收到的消息为:" + new String(body, "utf-8")); - } - }; - //监听消息 - channel.basicConsume(Producer.DIRECT_QUEUE_INSERT, true, consumer); - } - } - ``` - -* 消费者2同1,更改队列名称:Producer.DIRECT_QUEUE_UPDATE - -* 启动所有消费者,然后使用生产者发送消息;在消费者对应的控制台可以查看到生产者发送对应routing key对应队列的消息;到达**按照需要接收**的效果 - -结论:Routing模式中队列在绑定交换机时要指定routing key,消息会转发到符合routing key的队列 - - - -**** - - - -### Topics - -Topics通配符:https://www.rabbitmq.com/tutorials/tutorial-five-python.html - -Topic类型与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。Topic类型Exchange可以让队列在绑定Routing key 的时候**使用通配符** - -`Routingkey` 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: `item.insert` - -通配符规则: - -`#`:匹配一个或多个词 - -`*`:匹配恰好1个词 - -举例: - -`item.#`:能够匹配`item.insert.abc` 或者 `item.insert` - -`item.*`:只能匹配`item.insert` - -* 生产者:使用topic类型的Exchange,发送消息的routing key有3种: `item.insert`、`item.update`、`item.delete` - - ```java - public class Producer { - //交换机名称 - static final String TOPIC_EXCHAGE = "topic_exchange"; - //队列名称 - static final String TOPIC_QUEUE_1 = "topic_queue_1"; - //队列名称 - static final String TOPIC_QUEUE_2 = "topic_queue_2"; - public static void main(String[] args) throws Exception { - //创建连接 - Connection connection = ConnectionUtil.getConnection(); - // 创建频道 - Channel channel = connection.createChannel(); - //声明交换机 - channel.exchangeDeclare(TOPIC_EXCHAGE, BuiltinExchangeType.TOPIC); - - - // 发送信息 - String message = "新增了商品。Topic模式;routing key 为 item.insert " ; - channel.basicPublish(TOPIC_EXCHAGE, "item.insert", null, message.getBytes()); - System.out.println("已发送消息:" + message); - - // 发送信息 - message = "修改了商品。Topic模式;routing key 为 item.update" ; - channel.basicPublish(TOPIC_EXCHAGE, "item.update", null, message.getBytes()); - System.out.println("已发送消息:" + message); - - // 发送信息 - message = "删除了商品。Topic模式;routing key 为 item.delete" ; - channel.basicPublish(TOPIC_EXCHAGE, "item.delete", null, message.getBytes()); - System.out.println("已发送消息:" + message); - - // 关闭资源 - channel.close(); - connection.close(); - } - } - ``` - -* 消费者1:接收两种类型的消息:更新商品和删除商品 - - ```java - public class Consumer1 { - public static void main(String[] args) throws Exception { - Connection connection = ConnectionUtil.getConnection(); - // 创建频道 - Channel channel = connection.createChannel(); - //声明交换机 - channel.exchangeDeclare(Producer.TOPIC_EXCHAGE,BuiltinExchangeType.TOPIC); - //声明(创建)队列 - channel.queueDeclare(Producer.TOPIC_QUEUE_1, true, false, false, null); - - // 队列绑定交换机 - channel.queueBind(Producer.TOPIC_QUEUE_1, Producer.TOPIC_EXCHAGE, "item.update"); - channel.queueBind(Producer.TOPIC_QUEUE_1, Producer.TOPIC_EXCHAGE, "item.delete"); - - //创建消费者;并设置消息处理 - DefaultConsumer consumer = new DefaultConsumer(channel){ - @Override - public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { - //路由key - System.out.println("路由key为:" + envelope.getRoutingKey()); - //交换机 - System.out.println("交换机为:" + envelope.getExchange()); - //消息id - System.out.println("消息id为:" + envelope.getDeliveryTag()); - //收到的消息 - System.out.println("消费者1-接收到的消息为:" + new String(body, "utf-8")); - } - }; - //监听消息 - channel.basicConsume(Producer.TOPIC_QUEUE_1, true, consumer); - } - } - ``` - -* 消费者2:接收所有类型的消息:新增商品,更新商品和删除商品 - - ```java - // 声明(创建)队列 - channel.queueDeclare(Producer.TOPIC_QUEUE_2, true, false, false, null); - // 队列绑定交换机 - channel.queueBind(Producer.TOPIC_QUEUE_2, Producer.TOPIC_EXCHAGE, "item.*"); - //监听消息 - channel.basicConsume(Producer.TOPIC_QUEUE_2, true, consumer); - ``` - -* 启动所有消费者,然后使用生产者发送消息;在消费者对应的控制台可以查看到生产者发送对应routing key对应队列的消息;到达**按照需要接收**的效果;并且这些routing key可以使用通配符 - -结论:Topic主题模式可以实现 `Publish/Subscribe发布与订阅模式` 和 ` Routing路由模式` 的功能;只是Topic在配置routing key 的时候可以使用通配符,显得更加灵活 - - - -*** - - - -### 模式总结 - -RabbitMQ工作模式: - -**1、简单模式 HelloWorld** -一个生产者、一个消费者,不需要设置交换机(使用默认的交换机) - -**2、工作队列模式 Work Queue** -一个生产者、多个消费者(竞争关系),不需要设置交换机(使用默认的交换机) - -**3、发布订阅模式 Publish/subscribe** -需要设置类型为fanout的交换机,并且交换机和队列进行绑定,当发送消息到交换机后,交换机会将消息发送到绑定的队列 - -**4、路由模式 Routing** -需要设置类型为direct的交换机,交换机和队列进行绑定,并且指定routing key,当发送消息到交换机后,交换机会根据routing key将消息发送到对应的队列 - -**5、通配符模式 Topic** -需要设置类型为topic的交换机,交换机和队列进行绑定,并且指定通配符方式的routing key,当发送消息到交换机后,交换机会根据routing key将消息发送到对应的队列 - - - - - -*** - - - -## Spring - -### Spring整合 - -#### 生产者工程 - -##### 添加依赖 - -pom.xml - -```xml - - - org.springframework - spring-context - 5.1.7.RELEASE - - - - org.springframework.amqp - spring-rabbit - 2.1.8.RELEASE - - - - junit - junit - 4.12 - - - - org.springframework - spring-test - 5.1.7.RELEASE - - -``` - - - -##### 配置整合 - -创建`spring-rabbitmq-producer\src\main\resources\properties\rabbitmq.properties`连接参数等配置文件 - -```properties -rabbitmq.host=192.168.0.137 -rabbitmq.port=5672 -rabbitmq.username=admin -rabbitmq.password=admin -rabbitmq.virtual-host=/sea -``` - -创建 `spring-rabbitmq-producer\src\main\resources\spring\spring-rabbitmq.xml` 整合配置文件 - -```xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -``` - - - - - -##### 发送消息 - -创建测试文件 `spring-rabbitmq-producer\src\test\java\com\itheima\rabbitmq\ProducerTest.java` - -```java -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(locations = "classpath:spring/spring-rabbitmq.xml") -public class ProducerTest { - - @Autowired - private RabbitTemplate rabbitTemplate; - - /** - * 只发队列消息 - * 默认交换机类型为 direct - * 交换机的名称为空,路由键为队列的名称 - */ - @Test - public void queueTest(){ - //路由键与队列同名 - rabbitTemplate.convertAndSend("spring_queue", "只发队列spring_queue的消息。"); - } - - /** - * 发送广播 - * 交换机类型为 fanout - * 绑定到该交换机的所有队列都能够收到消息 - */ - @Test - public void fanoutTest(){ - /** - * 参数1:交换机名称 - * 参数2:路由键名(广播设置为空) - * 参数3:发送的消息内容 - */ - rabbitTemplate.convertAndSend("spring_fanout_exchange", "", "发送到spring_fanout_exchange交换机的广播消息"); - } - - /** - * 通配符 - * 交换机类型为 topic - * 匹配路由键的通配符,*表示一个单词,#表示多个单词 - * 绑定到该交换机的匹配队列能够收到对应消息 - */ - @Test - public void topicTest(){ - /** - * 参数1:交换机名称 - * 参数2:路由键名 - * 参数3:发送的消息内容 - */ - rabbitTemplate.convertAndSend("spring_topic_exchange", "heima.bj", "发送到spring_topic_exchange交换机heima.bj的消息"); - rabbitTemplate.convertAndSend("spring_topic_exchange", "heima.bj.1", "发送到spring_topic_exchange交换机heima.bj.1的消息"); - rabbitTemplate.convertAndSend("spring_topic_exchange", "heima.bj.2", "发送到spring_topic_exchange交换机heima.bj.2的消息"); - rabbitTemplate.convertAndSend("spring_topic_exchange", "itcast.cn", "发送到spring_topic_exchange交换机itcast.cn的消息"); - } -} -``` - - - - - -*** - - - - - -#### 消费者工程 - -##### 配置整合 - -创建 `spring-rabbitmq-consumer\src\main\resources\spring\spring-rabbitmq.xml` 整合配置文件 - -```xml - - - - - - - - - - - - - - - - - - - - - - - - -``` - - - -##### 消息监听器 - -###### 队列监听器 - -创建 `spring-rabbitmq-consumer\src\main\java\com\itheima\rabbitmq\listener\SpringQueueListener.java` - -```java -public class SpringQueueListener implements MessageListener { - public void onMessage(Message message) { - try { - String msg = new String(message.getBody(), "utf-8"); - - System.out.printf("接收路由名称为:%s,路由键为:%s,队列名为:%s的消息:%s \n", - message.getMessageProperties().getReceivedExchange(), - message.getMessageProperties().getReceivedRoutingKey(), - message.getMessageProperties().getConsumerQueue(), - msg); - } catch (Exception e) { - e.printStackTrace(); - } - } -} -``` - - - -###### 广播监听器 - -广播监听器1,创建 `spring-rabbitmq-consumer\src\main\java\com\itheima\rabbitmq\listener\FanoutListener1.java` - -```java -public class FanoutListener1 implements MessageListener { - public void onMessage(Message message) { - try { - String msg = new String(message.getBody(), "utf-8"); - - System.out.printf("广播监听器1:接收路由名称为:%s,路由键为:%s,队列名为:%s的消息:%s \n", - message.getMessageProperties().getReceivedExchange(), - message.getMessageProperties().getReceivedRoutingKey(), - message.getMessageProperties().getConsumerQueue(), - msg); - } catch (Exception e) { - e.printStackTrace(); - } - } -} -``` - - - -广播监听器2 - -```java -public class FanoutListener2 implements MessageListener { - public void onMessage(Message message) { - try { - String msg = new String(message.getBody(), "utf-8"); - System.out.printf("广播监听器2:接收路由名称为:%s,路由键为:%s,队列名为:%s的消息:%s \n", - message.getMessageProperties().getReceivedExchange(), - message.getMessageProperties().getReceivedRoutingKey(), - message.getMessageProperties().getConsumerQueue(), - msg); - } catch (Exception e) { - e.printStackTrace(); - } - } -} -``` - - - -###### 星号通配符监听器 - -创建 `spring-rabbitmq-consumer\src\main\java\com\itheima\rabbitmq\listener\TopicListenerStar.java` - -```java -public class TopicListenerStar implements MessageListener { - public void onMessage(Message message) { - try { - String msg = new String(message.getBody(), "utf-8"); - - System.out.printf("通配符*监听器:接收路由名称为:%s,路由键为:%s,队列名为:%s的消息:%s \n", - message.getMessageProperties().getReceivedExchange(), - message.getMessageProperties().getReceivedRoutingKey(), - message.getMessageProperties().getConsumerQueue(), - msg); - } catch (Exception e) { - e.printStackTrace(); - } - } -} -``` - - - -###### #号通配符监听器 - -创建 `spring-rabbitmq-consumer\src\main\java\com\itheima\rabbitmq\listener\TopicListenerWell.java` - -```java -public class TopicListenerWell implements MessageListener { - public void onMessage(Message message) { - try { - String msg = new String(message.getBody(), "utf-8"); - - System.out.printf("通配符#监听器:接收路由名称为:%s,路由键为:%s,队列名为:%s的消息:%s \n", - message.getMessageProperties().getReceivedExchange(), - message.getMessageProperties().getReceivedRoutingKey(), - message.getMessageProperties().getConsumerQueue(), - msg); - } catch (Exception e) { - e.printStackTrace(); - } - } -} -``` - - - - - -*** - - - -### Boot整合 - -#### 实现流程 - -在Spring项目中,可以使用Spring-Rabbit去操作RabbitMQ -https://github.com/spring-projects/spring-amqp - -尤其是在spring boot项目中只需要引入对应的amqp启动器依赖即可,方便的使用RabbitTemplate发送消息,使用注解接收消息 - -**生产者工程:** - -1. application.yml文件配置RabbitMQ相关信息; -2. 在生产者工程中编写配置类,用于创建交换机和队列,并进行绑定 - -3. 注入RabbitTemplate对象,通过RabbitTemplate对象发送消息到交换机 - - - -**消费者工程:** - -1. application.yml文件配置RabbitMQ相关信息 - -2. 创建消息处理类,用于接收队列中的消息并进行处理 - - - -#### 生产者工程 - -##### 添加依赖 - -```xml - - - org.springframework.boot - spring-boot-starter-amqp - - - org.springframework.boot - spring-boot-starter-test - - -``` - - - -##### 启动类 - -````java -@SpringBootApplication -public class ProducerApplication { - public static void main(String[] args) { - SpringApplication.run(ProducerApplication.class); - } -} -```` - - - - - -##### 配置MQ - -创建application.yml,内容如下 - -```yaml -spring: - rabbitmq: - host: 192.168.0.137 - port: 5672 - username: admin - password: admin - virtual-host: /sea -``` - - - -绑定交换机和队列: - -```java -@Configuration -public class RabbitMqConfig { - public static final String EXCHANGE_NAME = "boot_topic_exchange"; - public static final String QUEUE_NAME = "boot_queue"; - - //1.交换机 - @Bean("bootExchange") - public Exchange bootExchange() { - return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build(); - } - - //2.Queue队列 - @Bean("bootQueue") - public Queue bootQueue() { - return QueueBuilder.durable(QUEUE_NAME).build(); - } - - //3.队列和交换机绑定关系 - /* - 1.确定队列 - 2.确定交换机 - 3.routing key - */ - @Bean - public Binding bindQueueExchange(@Qualifier("bootQueue") Queue queue, @Qualifier("bootExchange") Exchange exchange) { - return BindingBuilder.bind(queue).to(exchange).with("boot.#").noargs(); - } -} -``` - - - -*** - - - -#### 消费者工程 - -##### 配置文件 - -application.yml + 启动类 - - - - - -##### 监听器 - -编写消息监听器com.example.rabbitmq.listener.RabbitMqListener - -```java -@Component -public class RabbitMqListener { - @RabbitListener(queues = "boot_queue") - public void ListenerQueue(Message message) { - System.out.println(message); - } -} -``` - - - -*** - - - -#### 测试类 - -在生产者工程springboot-rabbitmq-producer中创建测试类,发送消息: - -```java -@SpringBootTest(classes = ProducerApplication.class) -@RunWith(SpringRunner.class) -public class ProducerTest { - //1.注入RabbitTemplate - @Autowired - private RabbitTemplate rabbitTemplate; - - @Test - public void testSend(){ - rabbitTemplate.convertAndSend( - RabbitMqConfig.EXCHANGE_NAME,"boot.jaskajks","boot mq hello"); - } -} -``` - -先运行上述测试程序(交换机和队列才能先被声明和绑定),然后启动消费者;在消费者工程springboot-rabbitmq-consumer中控制台查看是否接收到对应消息 - - - - - -**** - - - -## 高级特性 - -### 可靠性 - -RabbitMQ 提供了两种方式用来控制消息的投递可靠性模式: - -* confirm 确认模式 - -* return 退回模式 - -rabbitmq 整个消息投递的路径为:producer-->rabbitmq broker-->exchange-->queue-->consumer - -* 消息从 producer 到 exchange 则会返回一个 confirmCallback 。 - -* 消息从 exchange-->queue 投递失败则会返回一个 returnCallback 。 - -利用这两个 callback 控制消息的可靠性投递,防止消息丢失或者投递失败场景 - -实现方法: - -* 设置ConnectionFactory的`publisher-confirms="true"` 开启确认模式 - -* 使用`rabbitTemplate.setConfirmCallback`设置回调函数,当消息发送到exchange后回调confirm方法,在方法中判断ack,如果为true,则发送成功,如果为false,则发送失败,需要处理 - -* 设置ConnectionFactory的`publisher-returns="true"`开启退回模式 - -* 使用`rabbitTemplate.setReturnCallback`设置退回函数,当消息从exchange路由到queue失败后,如果设置了`rabbitTemplate.setMandatory(true)`参数,则会将消息退回给producer,并执行回调函数returnedMessage - -在RabbitMQ中也提供了事务机制,但是性能较差,使用channel下列方法,完成事务控制: - -* txSelect():用于将当前channel设置成transaction模式 - -* txCommit():用于提交事务 - -* txRollback():用于回滚事务 - -确认模式代码实现: - -* 生产者配置文件:spring-rabbitmq-producer.xml - - ```xml - - - - - - - - - - - - - - - - - - - ``` - -* ProducerTest - - 确认模式: - - 1. 确认模式开启:ConnectionFactory中开启publisher-confirms="true" - 2. 在rabbitTemplate定义ConfirmCallBack回调函数 - - ```java - @RunWith(SpringJUnit4ClassRunner.class) - @ContextConfiguration(locations = "classpath:spring-rabbitmq-producer.xml") - public class ProducerTest { - @Autowired - private RabbitTemplate rabbitTemplate; - - @Test - public void testConfirm() { - - //2. 定义回调 - rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() { - /** - * @param correlationData 相关配置信息 - * @param ack exchange交换机 是否成功收到了消息。true 成功,false代表失败 - * @param cause 失败原因 - */ - @Override - public void confirm(CorrelationData correlationData, boolean ack, String cause) { - System.out.println("confirm方法被执行了...."); - if (ack) { - //接收成功 - System.out.println("接收成功消息" + cause); - } else { - //接收失败 - System.out.println("接收失败消息" + cause); - //做一些处理,让消息再次发送。 - } - } - }); - - //3. 发送消息 //错误的交换机,发送失败 - rabbitTemplate.convertAndSend("test_exchange_confirm111", "confirm", "message confirm...."); - } - } - ``` - -退回模式代码实现: - -* ProducerTest - - 回退模式:当消息发送给Exchange后,Exchange路由到Queue失败时才会执行ReturnCallBack - - 1. 开启回退模式:publisher-returns="true" - 2. 设置ReturnCallBack - 3. 设置Exchange处理消息的模式: - * 如果消息没有路由到Queue,则丢弃消息(默认) - * 如果消息没有路由到Queue,返回给消息发送方ReturnCallBack - - ```java - @Test - public void testReturn() { - //设置交换机处理失败消息的模式 - rabbitTemplate.setMandatory(true); - - //2.设置ReturnCallBack - rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() { - /** - * - * @param message 消息对象 - * @param replyCode 错误码 - * @param replyText 错误信息 - * @param exchange 交换机 - * @param routingKey 路由键 - */ - @Override - public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) { - System.out.println("return 执行了...."); - - System.out.println(message); - System.out.println(replyCode); - System.out.println(replyText); - System.out.println(exchange); - System.out.println(routingKey); - - //处理 - } - }); - - //3. 发送消息 - rabbitTemplate.convertAndSend("test_exchange_confirm", "confirm", "message confirm...."); - } - ``` - - -* 消费者测试代码 - - ```java - @RunWith(SpringJUnit4ClassRunner.class) - @ContextConfiguration(locations = "classpath:spring-rabbitmq-consumer.xml") - public class ConsumerTest { - @Test - public void test(){ - while (true){ - - } - } - } - ``` - - - -*** - - - -### ACK - -ACK指Acknowledge,确认。 表示消费端收到消息后的确认方式,有三种确认方式: - -* 自动确认:acknowledge="none" - -* 手动确认:acknowledge="manual" - -* 根据异常情况确认:acknowledge="auto" - -其中自动确认是指,当消息一旦被Consumer接收到,则自动确认收到,并将相应 message 从 RabbitMQ 的消息缓存中移除。但是在实际业务处理中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失。如果设置了手动确认方式,则需要在业务处理成功后,调用channel.basicAck(),手动签收,如果出现异常,则调用channel.basicNack()方法,让其自动重新发送消息 - -Consumer ACK机制: -1. 设置手动签收,在rabbit:listener-container标签中设置acknowledge属性,设置ack方式 none:自动确认,manual:手动确认 -2. 让监听器类实现ChannelAwareMessageListener接口 -3. 如果消息成功处理,则调用channel的 basicAck()签收 -4. 如果消息处理失败,则调用channel的basicNack()拒绝签收,broker重新发送给consumer - -消费者代码实现: - -* 配置文件spring-rabbitmq-consumer.xml: - - ```xml - - - - - - - - - - - - - - ``` - -* 监听器:com.seazean.listener.AckListener - - ```java - @Component - public class AckListener implements ChannelAwareMessageListener { - @Override - public void onMessage(Message message, Channel channel) throws Exception { - long deliveryTag = message.getMessageProperties().getDeliveryTag(); - - try { - //1.接收转换消息 - System.out.println(new String(message.getBody())); - //2. 处理业务逻辑 - System.out.println("处理业务逻辑..."); - int i = 3/0;//出现错误 - //3. 手动签收 - channel.basicAck(deliveryTag,true); - } catch (Exception e) { - //e.printStackTrace(); - - //4.拒绝签收 - //第三个参数:requeue:重回队列。如果设置为true,则消息重新回到queue,broker会重新发送该消息给消费端 - channel.basicNack(deliveryTag,true,true); - //channel.basicReject(deliveryTag,true); - } - } - } - ``` - - - -*** - - - -### 限流 - -消费端限流模型: -![](https://gitee.com/seazean/images/raw/master/Frame/RabbitMQ-消费端限流.png) - -Consumer 限流机制: -1. 确保ack机制为手动确认 -2. 配置属性:perfetch = 1,表示消费端每次从mq拉去一条消息来消费,直到手动确认消费完毕后,才会继续拉去下一条消息。 - -消费者代码实现: - -* 配置文件:spring-rabbitmq-consumer.xml - - ```xml - - - - - - ``` - -* 监听器:com.seazean.listener.QosListener - - ```java - @Component - public class QosListener implements ChannelAwareMessageListener { - @Override - public void onMessage(Message message, Channel channel) throws Exception { - Thread.sleep(1000); - //1.获取消息 - System.out.println(new String(message.getBody())); - - //2. 处理业务逻辑 - - //3. 签收 - channel.basicAck(message.getMessageProperties().getDeliveryTag(),true); - - } - } - ``` - -生产者代码实现: - -* ProducerTest - - ```java - @Test - public void testSend() { - for (int i = 0; i < 10; i++) { - // 发送消息 - rabbitTemplate.convertAndSend("test_exchange_confirm", "confirm", "message confirm...."); - } - } - ``` - - - -*** - - - -### TTL - -TTL 全称 Time To Live(存活时间/过期时间) - -* 当消息到达存活时间后,还没有被消费,会被自动清除 - -* RabbitMQ可以**对消息设置过期时间**,也可以**对整个队列(Queue)设置过期时间** - -基本规则: - -* 如果设置了消息的过期时间,也设置了队列的过期时间,它以时间短的为准 - - * 设置队列过期时间使用参数:x-message-ttl,单位 ms,会对整个队列消息统一过期 - - * 设置消息过期时间使用参数:expiration,单位 ms,当该消息在队列头部时(消费时),会单独判断这一消息是否过期 - -* 队列过期后,会将队列所有消息全部移除 - -* 消息过期后,只有消息在队列顶端,才会判断其是否过期(移除掉) - -生产者代码实现: - -* 配置文件:spring-rabbitmq-consumer.xml - - ```xml - - - - - - - - - - - - - - ``` - -* ProducerTest - - ```java - @Test - public void testTtl() { - // 消息后处理对象,设置一些消息的参数信息 - MessagePostProcessor messagePostProcessor = new MessagePostProcessor() { - @Override - public Message postProcessMessage(Message message) throws AmqpException { - //1.设置message的信息 - message.getMessageProperties().setExpiration("5000");//消息的过期时间 - //2.返回该消息 - return message; - } - }; - - //消息单独过期 - //rabbitTemplate.convertAndSend("test_exchange_ttl", "ttl.hehe", "message ttl....",messagePostProcessor); - - - for (int i = 0; i < 10; i++) { - if(i == 5){ - //消息单独过期 - rabbitTemplate.convertAndSend("test_exchange_ttl", "ttl.hehe", "message ttl....",messagePostProcessor); - }else{ - //不过期的消息 - rabbitTemplate.convertAndSend("test_exchange_ttl", "ttl.hehe", "message ttl...."); - } - } - } - ``` - - - -*** - - - -### 死信队列 - -死信队列,英文缩写:DLX (Dead Letter Exchange 死信交换机),当消息成为Dead message后,可以被重新发送到另一个交换机,这个交换机就是DLX - -**消息成为死信的三种情况:** - -* 队列消息长度到达限制 -* 消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列 requeue=false -* 原队列存在消息过期设置,消息到达超时时间未被消费 - -队列绑定死信交换机:给队列设置参数: x-dead-letter-exchange 和 x-dead-letter-routing-key - -![](https://gitee.com/seazean/images/raw/master/Frame/RabbitMQ-死信交换机.png) - -生产者代码实现 - -* 配置文件:spring-rabbitmq-producer.xml - - 1. 声明正常的队列(test_queue_dlx)和交换机(test_exchange_dlx) - 2. 声明死信队列(queue_dlx)和死信交换机(exchange_dlx) - 3. 正常队列绑定死信交换机,设置两个参数: - * x-dead-letter-exchange:死信交换机名称 - * dead-letter-routing-key:发送给死信交换机的routingkey - - ```xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ``` - -* ProducerTest - - ```java - /** - * 发送测试死信消息: - * 1. 过期时间 - * 2. 长度限制 - * 3. 消息拒收 - */ - @Test - public void testDlx(){ - //1. 测试过期时间,死信消息 - //rabbitTemplate.convertAndSend("test_exchange_dlx","test.dlx.haha","我是一条消息,我会死吗?"); - - //2. 测试长度限制后,消息死信 - /* for (int i = 0; i < 20; i++) { - rabbitTemplate.convertAndSend("test_exchange_dlx","test.dlx.haha","我是一条消息,我会死吗?"); - }*/ - - //3. 测试消息拒收 - rabbitTemplate.convertAndSend("test_exchange_dlx","test.dlx.haha","我是一条消息,我会死吗?"); - } - ``` - -消费者代码实现: - -* 监听器:com.seazean.listener.DlxListener - - ```java - @Component - public class DlxListener implements ChannelAwareMessageListener { - @Override - public void onMessage(Message message, Channel channel) throws Exception { - long deliveryTag = message.getMessageProperties().getDeliveryTag(); - try { - //1.接收转换消息 - System.out.println(new String(message.getBody())); - //2. 处理业务逻辑 - System.out.println("处理业务逻辑..."); - int i = 3/0;//出现错误 - //3. 手动签收 - channel.basicAck(deliveryTag,true); - } catch (Exception e) { - //e.printStackTrace(); - System.out.println("出现异常,拒绝接受"); - //4.拒绝签收,不重回队列 requeue=false - channel.basicNack(deliveryTag,true,false); - } - } - } - ``` - -* 配置文件:spring-rabbitmq-consumer.xml - - ```xml - - - - - - - ``` - - - -*** - - - -### 延迟队列 - -延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费 - -应用场景:下单后,30分钟未支付,取消订单,回滚库存 - -实现方式:定时器、延迟队列 - -RabbitMQ中并未提供延迟队列功能,可以使用:**TTL+死信队列**组合实现延迟队列的效果 - -![](https://gitee.com/seazean/images/raw/master/Frame/RabbitMQ-延迟队列.png) - -生产者代码实现: - -* 配置文件:spring-rabbitmq-producer.xml - - 延迟队列: - 1. 定义正常交换机(order_exchange)和队列(order_queue) - 2. 定义死信交换机(order_exchange_dlx)和队列(order_queue_dlx) - 3. 绑定,设置正常队列过期时间为30分钟 - - ```xml - - - - - - - - - - - - - - - - - - - - - - - ``` - -* ProducerTest - - ```java - @Test - public void testDelay() throws InterruptedException { - //1.发送订单消息。 将来是在订单系统中,下单成功后,发送消息 - rabbitTemplate.convertAndSend("order_exchange","order.msg","订单信息:id=1,time=202年3月17日16:41:47"); - } - ``` - -消费者代码实现: - -* 配置文件:spring-rabbitmq-consumer.xml - - ```xml - - - - - - - ``` - -* 监听器:com.seazean.listener.OrderListener - - ```java - @Component - public class OrderListener implements ChannelAwareMessageListener { - @Override - public void onMessage(Message message, Channel channel) throws Exception { - long deliveryTag = message.getMessageProperties().getDeliveryTag(); - try { - //1.接收转换消息 - System.out.println(new String(message.getBody())); - //2. 处理业务逻辑 - System.out.println("处理业务逻辑..."); - System.out.println("根据订单id查询其状态..."); - System.out.println("判断状态是否为支付成功"); - System.out.println("取消订单,回滚库存...."); - //3. 手动签收 - channel.basicAck(deliveryTag,true); - } catch (Exception e) { - //e.printStackTrace(); - System.out.println("出现异常,拒绝接受"); - //4.拒绝签收,不重回队列 requeue=false - channel.basicNack(deliveryTag,true,false); - } - } - } - ``` - - - -**** - - - -### 日志监控 - -RabbitMQ默认日志存放路径: /var/log/rabbitmq/rabbit@xxx.log - -日志包含了RabbitMQ的版本号、Erlang的版本号、RabbitMQ服务节点名称、cookie的hash值、RabbitMQ配置文件地址、内存限制、磁盘限制、默认账户guest的创建以及权限配置等 - -```sh -rabbitmqctl list_queues #查看队列 -rabbitmqctl list_exchanges #查看exchanges -rabbitmqctl list_users #查看用户 -rabbitmqctl list_connections #查看连接 -rabbitmqctl list_consumers #查看消费者信息 -rabbitmqctl environment #查看环境变量 -rabbitmqctl list_queues name memory #查看单个队列的内存使用 -rabbitmqctl list_queues name messages_ready #查看准备就绪的队列 -rabbitmqctl list_queues name messages_unacknowledged #查看未被确认的队列 -``` - - - -*** - - - -### 消息追踪 - -RabbitMQ中使用Firehose和rabbitmq_tracing插件功能来实现消息追踪 - -应用:在使用任何消息中间件的过程中,可能会出现某条消息异常丢失的情况。生产者或消费者与RabbitMQ断开了连接;交换器与队列之间不同的转发策略;交换器并没有与任何队列进行绑定,生产者又不感知或者没有采取相应的措施;RabbitMQ本身的集群策略也可能导致消息的丢失。这时需要有一个较好的机制跟踪记录消息的投递过程,有助于进行问题的定位 - -**firehose**:firehose机制是将生产者投递给rabbitmq的消息,rabbitmq投递给消费者的消息按照指定的格式发送到默认的exchange上,这个默认的exchange的名称为amq.rabbitmq.trace,它是一个topic类型的exchange。发送到这个exchange上的消息的routing key为 publish.exchangename 和 deliver.queuename。其中exchangename和queuename为实际exchange和queue的名称,分别对应生产者投递到exchange的消息,和消费者从queue上获取的消息。 - -注意:打开 trace 会影响消息写入功能,适当打开后请关闭,Linux命令 - -* rabbitmqctl trace_on:开启Firehose命令 - -* rabbitmqctl trace_off:关闭Firehose命令 - -**rabbitmq_tracing**和Firehose在实现上如出一辙,只不过rabbitmq_tracing的方式比Firehose多了一层GUI的包装,更容易使用和管理 - -* 启用插件:rabbitmq-plugins enable rabbitmq_tracing - - - - - -*** - - - -## 应用问题 - -### 可靠性保障 - -需求:100%确保消息发送成功 - -**消息补偿机制**: - -![](https://gitee.com/seazean/images/raw/master/Frame/RabbitMQ-消息补偿.png) - - - -*** - - - -### 幂等性保障 - -**幂等性**指一次和多次请求某一个资源,对于资源本身应该具有同样的结果。也就是其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。 - -在MQ中指,消费多条相同的消息,得到与消费该消息一次相同的结果,防止双重支付问题 - -MySQL乐观锁机制: - -![](https://gitee.com/seazean/images/raw/master/Frame/RabbitMQ-幂等性.png) - - - -**** - - - -## 集群搭建 - -### 原理概述 - -RabbitMQ这款消息队列中间件产品是基于Erlang编写,Erlang语言天生具备分布式特性(通过同步Erlang集群各节点的magic cookie来实现)。RabbitMQ支持Clustering,这使得RabbitMQ本身不需要像ActiveMQ、Kafka那样通过ZooKeeper分别来实现HA方案和保存集群的元数据。集群是保证可靠性的一种方式,同时可以通过水平扩展以达到增加消息吞吐量能力的目的 - -![](https://gitee.com/seazean/images/raw/master/Frame/RabbitMQ-集群方案.png) - - - -### 单机部署 - -单机多实例部署 - -参考官方文档:https://www.rabbitmq.com/clustering.html - - - -**** - - - -### 集群管理 - -**rabbitmqctl join_cluster {cluster_node} [–ram]** -将节点加入指定集群中。在这个命令执行前需要停止RabbitMQ应用并重置节点。 - -**rabbitmqctl cluster_status** -显示集群的状态。 - -**rabbitmqctl change_cluster_node_type {disc|ram}** -修改集群节点的类型。在这个命令执行前需要停止RabbitMQ应用。 - -**rabbitmqctl forget_cluster_node [–offline]** -将节点从集群中删除,允许离线执行。 - -**rabbitmqctl update_cluster_nodes {clusternode}** -在集群中的节点应用启动前咨询clusternode节点的最新信息,并更新相应的集群信息。这个和join_cluster不同,它不加入集群。考虑这样一种情况,节点A和节点B都在集群中,当节点A离线了,节点C又和节点B组成了一个集群,然后节点B又离开了集群,当A醒来的时候,它会尝试联系节点B,但是这样会失败,因为节点B已经不在集群中了 - -**rabbitmqctl cancel_sync_queue [-p vhost] {queue}** -取消队列queue同步镜像的操作 - -**rabbitmqctl set_cluster_name {name}** -设置集群名称。集群名称在客户端连接时会通报给客户端。Federation和Shovel插件也会有用到集群名称的地方。集群名称默认是集群中第一个节点的名称,通过这个命令可以重新设置 - - - -*** - - - -### 负载均衡 - -HAProxy提供高可用性、负载均衡以及基于TCP和HTTP应用的代理,支持虚拟主机,它是免费、快速并且可靠的一种解决方案,包括Twitter,Reddit,StackOverflow,GitHub在内的多家知名互联网公司在使用。HAProxy实现了一种事件驱动、单一进程模型,此模型支持非常大的并发连接数 - - - From 7b1a7bbe482ea6e51cf44c8fb74ea7fb293d1bf6 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 15 May 2021 14:11:14 +0800 Subject: [PATCH 004/242] Update Java Notes --- Frame.md | 1422 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ Java.md | 328 ++++++++++++- 2 files changed, 1726 insertions(+), 24 deletions(-) create mode 100644 Frame.md diff --git a/Frame.md b/Frame.md new file mode 100644 index 0000000..3803efb --- /dev/null +++ b/Frame.md @@ -0,0 +1,1422 @@ +# Maven + +## 基本介绍 + +### Mvn概述 + +Maven:本质是一个项目管理工具,将项目开发和管理过程抽象成一个项目对象模型(POM) + +POM:Project Object Model,项目对象模型。Maven是用Java语言编写的,它管理的东西以面向对象的形式进行设计,最终把一个项目看成一个对象,而这个对象叫做POM + +pom.xml:Maven需要一个pom.xml文件,Maven通过加载这个配置文件可以知道项目的相关信息,这个文件代表就一个项目。如果我们做8个项目,对应的是8个pom.xml文件 + +依赖管理:Maven对项目所有依赖资源的一种管理,它和项目之间是一种双向关系,即做项目时可以管理所需要的其他资源,当其他项目需要依赖我们项目时,Maven也会把我们的项目当作一种资源去进行管理。 + +管理资源的存储位置:本地仓库,私服,中央仓库 + +![](https://gitee.com/seazean/images/raw/master/Frame/Maven介绍.png) + + + + + +### Mvn作用 + +* 项目构建:提供标准的,跨平台的自动化构建项目的方式 + +* 依赖管理:方便快捷的管理项目依赖的资源(jar包),避免资源间的版本冲突等问题 + +* 统一开发结构:提供标准的,统一的项目开发结构 + + ![](https://gitee.com/seazean/images/raw/master/Frame/Maven标准结构.png) + +各目录存放资源类型说明: + +* src/main/java:项目java源码 + +* src/main/resources:项目的相关配置文件(比如mybatis配置,xml映射配置,自定义配置文件等) + +* src/main/webapp:web资源(比如html,css,js等) + +* src/test/java:测试代码 + +* src/test/resources:测试相关配置文件 + +* src/pom.xml:项目pom文件 + + + + + +### 基础概念 + +* **仓库**:用于存储资源,主要是各种jar包。有本地仓库,私服,中央仓库,私服和中央仓库都是远程仓库 + + * 中央仓库:maven团队自身维护的仓库,属于开源的 + + * 私服:各公司/部门等小范围内存储资源的仓库,私服也可以从中央仓库获取资源,作用: + * 保存具有版权的资源,包含购买或自主研发的jar + * 一定范围内共享资源,能做到仅对内不对外开放 + + * 本地仓库:开发者自己电脑上存储资源的仓库,也可从远程仓库获取资源 + + + +* **坐标**:Maven中的坐标用于描述仓库中资源的位置 + + * 作用:使用唯一标识,唯一性定义资源位置,通过该标识可以将资源的识别与下载工作交由机器完成 + * https://mvnrepository.com:查询maven某一个资源的坐标,输入资源名称进行检索, + + * 依赖设置: + * groupId:定义当前资源隶属组织名称(通常是域名反写,如:org.mybatis;com.seazean) + * artifactId:定义当前资源的名称(通常是项目或模块名称,如:crm,sms) + * version:定义当前资源的版本号 + +* packaging:定义资源的打包方式,取值一般有如下三种 + + * jar:该资源打成jar包,默认是jar + + * war:该资源打成war包 + + * pom:该资源是一个父资源(表明使用maven分模块管理),打包时只生成一个pom.xml不生成jar或其他包结构 + + + + + +*** + + + +## 环境搭建 + +### 环境配置 + +Maven的官网:http://maven.apache.org/ + +下载安装:Maven是一个绿色软件,解压即安装 + +目录结构: + bin:可执行程序目录 + boot:maven自身的启动加载器 + conf:maven配置文件的存放目录 + lib:maven运行所需库的存放目录 + +配置MAVEN_HOME: + +![](https://gitee.com/seazean/images/raw/master/Frame/Maven配置环境变量.png) + + + +环境变量配置好之后需要测试环境配置结果,在DOS命令窗口下输入以下命令查看输出:`mvn -v` + + + +*** + + + +### 仓库配置 + +默认情况下maven本地仓库在系统盘当前用户目录下的`.m2/repository`,修改Maven的配置文件`conf/settings.xml`来修改仓库位置 + +* 修改本地仓库位置:找到标签,修改默认值 + + ```xml + + E:\Workspace\Java\Project\.m2\repository + ``` + + 注意:在仓库的同级目录即`.m2`也应该包含一个`settings.xml`配置文件,局部用户配置优先与全局配置 + + * 全局setting定义了Maven的公共配置 + * 用户setting定义了当前用户的配置 + +* 修改远程仓库:在配置文件中找到``标签,在这组标签下添加国内镜像 + + ```xml + + nexus-aliyun + central + Nexus aliyun + http://maven.aliyun.com/nexus/content/groups/public + + ``` + +* 修改默认JDK:在配置文件中找到``标签,添加配置: + + ```xml + + jdk-10 + + true + 10 + + + UTF-8 + 10 + 10 + + + ``` + + + + + +*** + + + + + +## 项目搭建 + +### 手动搭建 + +1. 在E盘下创建目录`mvnproject`并进入该目录,作为我们的操作目录 + +2. 创建我们的maven项目,创建一个目录`project-java`作为我们的项目文件夹,并进入到该目录 + +3. 创建java代码(源代码)所在目录,即创建`src/main/java` + +4. 创建配置文件所在目录,即创建`src/main/resources` + +5. 创建测试源代码所在目录,即创建`src/test/java` + +6. 创建测试存放配置文件存放目录,即`src/test/resources` + +7. 在`src/main/java`中创建一个包(注意在windos文件夹下就是创建目录)`demo`,在该目录下创建`Demo.java`文件,作为演示所需java程序,内容如下 + + ```java + package demo; + public class Demo{ + public String say(String name){ + System.out.println("hello "+name); + return "hello "+name; + } + } + ``` + +8. 在`src/test/java`中创建一个测试包(目录)`demo`,在该包下创建测试程序`DemoTest.java` + + ```java + package demo; + import org.junit.*; + public class DemoTest{ + @Test + public void testSay(){ + Demo d = new Demo(); + String ret = d.say("maven"); + Assert.assertEquals("hello maven",ret); + } + } + ``` + +9. **在`project-java/src`下创建`pom.xml`文件,格式如下:** + + ```xml + + + + + 4.0.0 + + jar + + + demo + + project-java + + 1.0 + + + + + + junit + junit + 4.12 + + + + ``` + +10. 搭建好了maven的项目结构,通过maven来构建项目 + maven的构建命令以`mvn`开头,后面添加功能参数,可以一次性执行多个命令,用空格分离 + `mvn compile`:编译 + `mvn clean`:清理 + `mvn test`:测试 + `mvn package`:打包 + `mvn install`:安装到本地仓库 + + 注意:执行某一条命令,则会把前面所有的都执行一遍 + + + +*** + + + +### 插件构建 + +![](https://gitee.com/seazean/images/raw/master/Frame/Maven-插件构建.png) + + + +*** + + + +### IDEA搭建 + +#### 不用原型 + +1. 在IDEA中配置Maven,选择maven3.6.1防止依赖问题 + IDEA配置Maven + +2. 创建Maven,New Module --> Maven --> 不选中Create from archetype + +3. 填写项目的坐标 + GroupId:demo + ArtifactId:project-java + +4. 查看各目录颜色标记是否正确 + + ![](https://gitee.com/seazean/images/raw/master/Frame/IDEA创建Maven目录结构.png) + +5. IDEA右侧侧栏有Maven Project,打开后有Lifecycle生命周期 + + ![](https://gitee.com/seazean/images/raw/master/Frame/IDEA-Maven生命周期.png) + +6. 自定义Maven命令:Run --> Edit Configurations --> 左上角 + --> Maven + + ![](https://gitee.com/seazean/images/raw/master/Frame/IDEA配置Maven命令.png) + + + + + +#### 使用原型 + +普通工程: + +1. 创建maven项目的时候选择使用原型骨架 + + ![](https://gitee.com/seazean/images/raw/master/Frame/IDEA创建Maven-quickstart.png) + +2. 创建完成后发现通过这种方式缺少一些目录,需要手动去补全目录,并且要对补全的目录进行标记 + + + +web工程: + +1. 选择web对应的原型骨架(选择maven开头的是简化的) + + ![](https://gitee.com/seazean/images/raw/master/Frame/IDEA创建Maven-webapp.png) + +2. 通过原型创建web项目得到的目录结构是不全的,因此需要我们自行补全,同时要标记正确 + +3. web工程创建之后需要启动运行,使用tomcat插件来运行项目,在`pom.xml`中添加插件的坐标: + + ```xml + + + + 4.0.0 + war + + web01 + demo + web01 + 1.0-SNAPSHOT + + + + + + + + + + + + org.apache.tomcat.maven + tomcat7-maven-plugin + 2.1 + + 80 + / + + + + + + ``` + +4. 插件配置以后,在IDEA右侧`maven-project`操作面板看到该插件,并且可以利用该插件启动项目 + web01-->Plugins-->tomcat7-->tomcat7:run + + + +*** + + + +## 依赖管理 + +### 依赖配置 + +依赖是指在当前项目中运行所需的jar,依赖配置的格式如下: + +```xml + + + + + + junit + + junit + + 4.12 + + +``` + + + +*** + + + +### 依赖传递 + +依赖具有传递性,分两种: + +* 直接依赖:在当前项目中通过依赖配置建立的依赖关系 + +* 间接依赖:被依赖的资源如果依赖其他资源,则表明当前项目间接依赖其他资源 + + 注意:直接依赖和间接依赖其实也是一个相对关系 + + + +依赖传递的冲突问题:在依赖传递过程中产生了冲突,有三种优先法则 + +* 路径优先:当依赖中出现相同资源时,层级越深,优先级越低,反之则越高 + +* 声明优先:当资源在相同层级被依赖时,配置顺序靠前的覆盖靠后的 + +* 特殊优先:当同级配置了相同资源的不同版本时,后配置的覆盖先配置的 + + + +**可选依赖:**对外隐藏当前所依赖的资源,不透明 + +```xml + + junit + junit + 4.11 + true + +``` + +**排除依赖:主动**断开依赖的资源,被排除的资源无需指定版本 + +```xml + + junit + junit + 4.12 + + + org.hamcrest + hamcrest-core + + + +``` + + + +*** + + + +### 依赖范围 + +依赖的jar默认情况可以在任何地方可用,可以通过`scope`标签设定其作用范围,有三种: + +* 主程序范围有效(src/main目录范围内) + +* 测试程序范围内有效(src/test目录范围内) + +* 是否参与打包(package指令范围内) + +`scope`标签的取值有四种:`compile,test,provided,runtime` + +![](https://gitee.com/seazean/images/raw/master/Frame/Maven依赖范围.png) + + + +**依赖范围的传递性:** + +![](https://gitee.com/seazean/images/raw/master/Frame/Maven依赖范围的传递性.png) + + + + + +*** + + + +## 生命周期 + +### 相关事件 + +Maven的构建生命周期描述的是一次构建过程经历了多少个事件 + +最常用的一套流程:compile --> test-compile --> test --> package --> install + +* clean:清理工作 + + * pre-clean:执行一些在clean之前的工作 + * clean:移除上一次构建产生的所有文件 + * post-clean:执行一些在clean之后立刻完成的工作 + +* default:核心工作,例如编译,测试,打包,部署等 + + 对于default生命周期,每个事件在执行之前都会**将之前的所有事件依次执行一遍** + + ![](https://gitee.com/seazean/images/raw/master/Frame/Maven-default生命周期.png) + +* site:产生报告,发布站点等 + + * pre-site:执行一些在生成站点文档之前的工作 + * site:生成项目的站点文档 + * post-site:执行一些在生成站点文档之后完成的工作,并为部署做准备 + * site-deploy:将生成的站点文档部署到特定的服务器上 + + + +*** + + + +### 执行事件 + +Maven的插件用来执行生命周期中的相关事件 + +- 插件与生命周期内的阶段绑定,在执行到对应生命周期时执行对应的插件 + +- maven默认在各个生命周期上都绑定了预先设定的插件来完成相应功能 + +- 插件还可以完成一些自定义功能 + + ```xml + + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + + + + + jar + + test-jar + + + generate-test-resources + + + + + + ``` + + + +*** + + + +## 模块开发 + +### 拆分 + +工程模块与模块划分: + +![](https://gitee.com/seazean/images/raw/master/Frame/Maven模块划分.png) + +* ssm_pojo拆分 + + * 新建模块,拷贝原始项目中对应的相关内容到ssm_pojo模块中 + * 实体类(User) + * 配置文件(无) + +* ssm_dao拆分 + + * 新建模块 + + * 拷贝原始项目中对应的相关内容到ssm_dao模块中 + + - 数据层接口(UserDao) + + - 配置文件:保留与数据层相关配置文件(3个) + + - 注意:分页插件在配置中与SqlSessionFactoryBean绑定,需要保留 + + - pom.xml:引入数据层相关坐标即可,删除springmvc相关坐标 + + - spring + - mybatis + - spring 整合mybatis + - mysql + - druid + - pagehelper + - 直接依赖ssm_pojo(对ssm_pojo模块执行install指令,将其安装到本地仓库) + + ```xml + + + + demo + ssm_pojo + 1.0-SNAPSHOT + + + + + + + + + + + ``` + +* ssm_service拆分 + + * 新建模块 + * 拷贝原始项目中对应的相关内容到ssm_service模块中 + + - 业务层接口与实现类(UserService、UserServiceImpl) + - 配置文件:保留与数据层相关配置文件(1个) + - pom.xml:引入数据层相关坐标即可,删除springmvc相关坐标 + + - spring + + - junit + + - spring 整合junit + + - 直接依赖ssm_dao(对ssm_dao模块执行install指令,将其安装到本地仓库) + + - 间接依赖ssm_pojo(由ssm_dao模块负责依赖关系的建立) + - 修改service模块spring核心配置文件名,添加模块名称,格式:applicationContext-service.xml + - 修改dao模块spring核心配置文件名,添加模块名称,格式:applicationContext-dao.xml + - 修改单元测试引入的配置文件名称,由单个文件修改为多个文件 + +* ssm_control拆分 + + * 新建模块(使用webapp模板) + + * 拷贝原始项目中对应的相关内容到ssm_controller模块中 + + - 现层控制器类与相关设置类(UserController、异常相关……) + + - 配置文件:保留与表现层相关配置文件(1个)、服务器相关配置文件(1个) + + - pom.xml:引入数据层相关坐标即可,删除springmvc相关坐标 + + - spring + + - springmvc + + - jackson + + - servlet + + - tomcat服务器插件 + + - 直接依赖ssm_service(对ssm_service模块执行install指令,将其安装到本地仓库) + + - 间接依赖ssm_dao、ssm_pojo + + ```xml + + + + demo + ssm_service + 1.0-SNAPSHOT + + + + + + + + + + + + + + + + ``` + + - 修改web.xml配置文件中加载spring环境的配置文件名称,使用*通配,加载所有applicationContext-开始的配置文件: + + ```xml + + + contextConfigLocation + classpath*:applicationContext-*.xml + + ``` + + - spring-mvc + + ```xml + + + ``` + + + +*** + + + +### 聚合 + +作用:聚合用于快速构建maven工程,一次性构建多个项目/模块。 + +制作方式: + +- 创建一个空模块,打包类型定义为pom + + ```xml + pom + ``` + +- 定义当前模块进行构建操作时关联的其他模块名称 + + ```xml + + + 4.0.0 + + demo + ssm + 1.0-SNAPSHOT + + + pom + + + + + ../ssm_pojo + ../ssm_dao + ../ssm_service + ../ssm_controller + + + ``` + +注意事项:参与聚合操作的模块最终执行顺序与模块间的依赖关系有关,与配置顺序无关 + + + +*** + + + +### 继承 + +作用:通过继承可以实现在子工程中沿用父工程中的配置 + +- maven中的继承与java中的继承相似,在子工程中配置继承关系 + +制作方式: + +- 在子工程中声明其父工程坐标与对应的位置 + + ```xml + + + com.seazean + ssm + 1.0-SNAPSHOT + + ../ssm/pom.xml + + ``` + +- 继承依赖的定义:在父工程中定义依赖管理 + + ```xml + + + + + + + org.springframework + spring-context + 5.1.9.RELEASE + + + + + ``` + +- 继承依赖的使用:在子工程中定义依赖关系,**无需声明依赖版本**,版本参照父工程中依赖的版本 + + ```xml + + + + org.springframework + spring-context + + + ``` + +- 继承的资源: + + ```xml + groupId:项目组ID,项目坐标的核心元素 + version:项目版本,项目坐标的核心因素 + description:项目的描述信息 + organization:项目的组织信息 + inceptionYear:项目的创始年份 + url:项目的URL地址 + developers:项目的开发者信息 + contributors:项目的贡献者信息 + distributionManagement:项目的部署配置 + issueManagement:项目的缺陷跟踪系统信息 + ciManagement:项目的持续集成系统信息 + scm:项目的版本控制系统西溪 + malilingLists:项目的邮件列表信息 + properties:自定义的Maven属性 + dependencies:项目的依赖配置 + dependencyManagement:项目的依赖管理配置 + repositories:项目的仓库配置 + build:包括项目的源码目录配置、输出目录配置、插件配置、插件管理配置等 + reporting:包括项目的报告输出目录配置、报告插件配置等 + ``` + +- 继承与聚合: + + 作用: + + - 聚合用于快速构建项目 + + - 继承用于快速配置 + + 相同点: + + - 聚合与继承的pom.xml文件打包方式均为pom,可以将两种关系制作到同一个pom文件中 + + - 聚合与继承均属于设计型模块,并无实际的模块内容 + + 不同点: + + - 聚合是在当前模块中配置关系,聚合可以感知到参与聚合的模块有哪些 + + - 继承是在子模块中配置关系,父模块无法感知哪些子模块继承了自己 + + + +*** + + + +### 属性 + +* 版本统一的重要性: + + ![](https://gitee.com/seazean/images/raw/master/Frame/Maven版本统一的重要性.png) + +* 属性类别: + + 1.自定义属性 2.内置属性 3.Setting属性 4.Java系统属性 5.环境变量属性 + +* 自定义属性: + + 作用:等同于定义变量,方便统一维护 + + 定义格式: + + ```xml + + + 5.1.9.RELEASE + 4.12 + + ``` + + - 聚合与继承的pom.xml文件打包方式均为pom,可以将两种关系制作到同一个pom文件中 + + - 聚合与继承均属于设计型模块,并无实际的模块内容 + + 调用格式: + + ```xml + + org.springframework + spring-context + ${spring.version} + + ``` + +* 内置属性: + + 作用:使用maven内置属性,快速配置 + + 调用格式: + + ```xml + ${project.basedir} or ${project.basedir} + ${version} or ${project.version} + ``` + + * vresion是1.0-SNAPSHOT + + ```xml + demo + ssm + 1.0-SNAPSHOT + ``` + +* Setting属性 + + - 使用Maven配置文件setting.xml中的标签属性,用于动态配置 + + 调用格式: + + ```xml + ${settings.localRepository} + ``` + +* Java系统属性: + + 作用:读取Java系统属性 + + 调用格式: + + ``` + ${user.home} + ``` + + 系统属性查询方式 cmd命令: + + ```sh + mvn help:system + ``` + +* 环境变量属性 + + 作用:使用Maven配置文件setting.xml中的标签属性,用于动态配置 + + 调用格式: + + ``` + ${env.JAVA_HOME} + ``` + + 环境变量属性查询方式: + + ``` + mvn help:system + ``` + + + + +*** + + + +### 工程版本 + +SNAPSHOT(快照版本) + +- 项目开发过程中,为方便团队成员合作,解决模块间相互依赖和时时更新的问题,开发者对每个模块进行构建的时候,输出的临时性版本叫快照版本(测试阶段版本) + +- 快照版本会随着开发的进展不断更新 + +RELEASE(发布版本) + +- 项目开发到进入阶段里程碑后,向团队外部发布较为稳定的版本,这种版本所对应的构件文件是稳定的,即便进行功能的后续开发,也不会改变当前发布版本内容,这种版本称为发布版本 + +约定规范: + +- <主版本>.<次版本>.<增量版本>.<里程碑版本> + +- 主版本:表示项目重大架构的变更,如:spring5相较于spring4的迭代 + +- 次版本:表示有较大的功能增加和变化,或者全面系统地修复漏洞 + +- 增量版本:表示有重大漏洞的修复 + +- 里程碑版本:表明一个版本的里程碑(版本内部)。这样的版本同下一个正式版本相比,相对来说不是很稳定,有待更多的测试 + +范例: + +- 5.1.9.RELEASE + + + +*** + + + + + +### 资源配置 + +作用:在任意配置文件中加载pom文件中定义的属性 + +* 父文件pom.xml + + ```xml + + jdbc:mysql://192.168.0.137:3306/ssm_db?useSSL=false + + ``` + +- 开启配置文件加载pom属性: + + ```xml + + + + + ${project.basedir}/src/main/resources + + true + + + ``` + +* properties文件中调用格式: + + ```xml-dtd + jdbc.driver=com.mysql.jdbc.Driver + jdbc.url=${jdbc.url} + jdbc.username=root + jdbc.password=123456 + ``` + + + +*** + + + +### 多环境配置 + +* 环境配置 + + ```xml + + + + + + pro_env + + + jdbc:mysql://127.1.1.1:3306/ssm_db + + + + true + + + + + dev_env + …… + + + ``` + +* 加载指定环境 + + 作用:加载指定环境配置 + + 调用格式: + + ``` + mvn 指令 –P 环境定义id + ``` + + 范例: + + ``` + mvn install –P pro_env + ``` + + + + +*** + + + +## 跳过测试 + +### 命令跳过 + +命令: + +``` +mvn 指令 –D skipTests +``` + +注意事项:执行的指令生命周期必须包含测试环节 + + + +### IEDA界面 + +![](https://gitee.com/seazean/images/raw/master/Frame/IDEA使用界面操作跳过测试.png) + + + +### 配置跳过 + +```xml + + + maven-surefire-plugin + 2.22.1 + + true + + **/User*Test.java + + + **/User*TestCase.java + + + +``` + + + +*** + + + +## 私服 + +### Nexus + +Nexus是Sonatype公司的一款maven私服产品 + +下载地址:https://help.sonatype.com/repomanager3/download + +启动服务器(命令行启动): + +``` +nexus.exe /run nexus +``` + +访问服务器(默认端口:8081): + +``` +http://localhost:8081 +``` + +修改基础配置信息 + +- 安装路径下etc目录中nexus-default.properties文件保存有nexus基础配置信息,例如默认访问端口 + +修改服务器运行配置信息 + +- 安装路径下bin目录中nexus.vmoptions文件保存有nexus服务器启动对应的配置信息,例如默认占用内存空间 + + + +*** + + + +### 资源操作 + +![](https://gitee.com/seazean/images/raw/master/Frame/Maven私服资源获取.png) + + + +仓库分类: + +* 宿主仓库hosted + * 保存无法从中央仓库获取的资源 + * 自主研发 + * 第三方非开源项目 + +* 代理仓库proxy + * 代理远程仓库,通过nexus访问其他公共仓库,例如中央仓库 + +* 仓库组group + * 将若干个仓库组成一个群组,简化配置 + * 仓库组不能保存资源,属于设计型仓库 + + + +资源上传,上传资源时提供对应的信息 + +- 保存的位置(宿主仓库) + +- 资源文件 + +- 对应坐标 + + + +*** + + + +### IDEA操作 + +#### 上传下载 + +![](https://gitee.com/seazean/images/raw/master/Frame/IDEA环境中资源上传与下载.png) + + + +*** + + + +#### 访问私服配置 + +##### 本地仓库访问私服 + +配置本地仓库访问私服的权限(setting.xml) + +```xml + + + heima-release + admin + admin + + + heima-snapshots + admin + admin + + +``` + +配置本地仓库资源来源(setting.xml) + +```xml + + + nexus-heima + * + http://localhost:8081/repository/maven-public/ + + +``` + + + +##### 项目工程访问私服 + +配置当前项目访问私服上传资源的保存位置(pom.xml) + +```xml + + + heima-release + http://localhost:8081/repository/heima-release/ + + + heima-snapshots + http://localhost:8081/repository/heima-snapshots/ + + +``` + +发布资源到私服命令 + +``` +mvn deploy +``` + + + + + +*** + + + +## 日志 + +### Log4j + +程序中的日志可以用来记录程序在运行时候的详情,并可以进行永久存储。 + +| | 输出语句 | 日志技术 | +| -------- | -------------------------- | ---------------------------------------- | +| 取消日志 | 需要修改代码,灵活性比较差 | 不需要修改代码,灵活性比较好 | +| 输出位置 | 只能是控制台 | 可以将日志信息写入到文件或者数据库中 | +| 多线程 | 和业务代码处于一个线程中 | 多线程方式记录日志,不影响业务代码的性能 | + +Log4j是Apache的一个开源项目。 +使用Log4j,通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。我们可以控制日志信息输送的目的地是控制台、文件等位置,也可以控制每一条日志的输出格式。 + + + + + +*** + + + +### 配置文件 + +配置文件的三个核心: + ++ 配置根Logger + + + 格式:log4j.rootLogger=日志级别,appenderName1,appenderName2,… + + + 日志级别:常见的五个级别:**DEBUG < INFO < WARN < ERROR < FATAL**(可以自定义) + Log4j规则:只输出级别不低于设定级别的日志信息 + + + appenderName1:指定日志信息要输出地址。可以同时指定多个输出目的地,用逗号隔开: + + 例如:log4j.rootLogger=INFO,ca,fa + ++ Appenders(输出源):日志要输出的地方,如控制台(Console)、文件(Files)等 + + + Appenders取值: + + org.apache.log4j.ConsoleAppender(控制台) + + org.apache.log4j.FileAppender(文件) + + + ConsoleAppender常用参数 + + `ImmediateFlush=true`:表示所有消息都会被立即输出,设为false则不输出,默认值是true。 + + `Target=System.err`:默认值是System.out + + FileAppender常用的选项 + + `ImmediateFlush=true`:表示所有消息都会被立即输出。设为false则不输出,默认值是true + + + `Append=false`:true表示将消息添加到指定文件中,原来的消息不覆盖。默认值是true + + + `File=E:/logs/logging.log4j`:指定消息输出到logging.log4j文件中 + ++ Layouts(布局):日志输出的格式,常用的布局管理器: + + + org.apache.log4j.PatternLayout(可以灵活地指定布局模式) + ++ org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串) + ++ org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等信息) + ++ PatternLayout常用的选项 + + + + +*** + + + +### 日志应用 + +* log4j的配置文件,名字为log4j.properties, 放在src根目录下 + + ```properties + log4j.rootLogger=debug,my,fileAppender + + ### direct log messages to my ### + log4j.appender.my=org.apache.log4j.ConsoleAppender + log4j.appender.my.ImmediateFlush = true + log4j.appender.my.Target=System.out + log4j.appender.my.layout=org.apache.log4j.PatternLayout + log4j.appender.my.layout.ConversionPattern=%d %t %5p %c{1}:%L - %m%n + + # fileAppender演示 + log4j.appender.fileAppender=org.apache.log4j.FileAppender + log4j.appender.fileAppender.ImmediateFlush = true + log4j.appender.fileAppender.Append=true + log4j.appender.fileAppender.File=E:/log4j-log.log + log4j.appender.fileAppender.layout=org.apache.log4j.PatternLayout + log4j.appender.fileAppender.layout.ConversionPattern=%d %5p %c{1}:%L - %m%n + ``` + +* 测试类 + + ```java + // 测试类 + public class Log4JTest01 { + + //使用log4j的api来获取日志的对象 + //弊端:如果以后我们更换日志的实现类,那么下面的代码就需要跟着改 + //不推荐使用 + //private static final Logger LOGGER = Logger.getLogger(Log4JTest01.class); + + //使用slf4j里面的api来获取日志的对象 + //好处:如果以后我们更换日志的实现类,那么下面的代码不需要跟着修改 + //推荐使用 + private static final Logger LOGGER = LoggerFactory.getLogger(Log4JTest01.class); + + public static void main(String[] args) { + //1.导入jar包 + //2.编写配置文件 + //3.在代码中获取日志的对象 + //4.按照日志级别设置日志信息 + LOGGER.debug("debug级别的日志"); + LOGGER.info("info级别的日志"); + LOGGER.warn("warn级别的日志"); + LOGGER.error("error级别的日志"); + } + } + ``` + + + + + +*** + + + + + +# Netty + + + diff --git a/Java.md b/Java.md index a2de4cb..ff4f350 100644 --- a/Java.md +++ b/Java.md @@ -11402,10 +11402,14 @@ public class XPathDemo { + + *** + + # JVM ## JVM概述 @@ -13401,7 +13405,7 @@ public class Demo3_6_1 { * invokedynamic:动态解析出需要调用的方法, * Java7为了实现动态类型语言支持而引入了该指令,但是并没有提供直接生成invokedynamic指令的方法,需要借助ASM这种底层字节码工具来产生invokedynamic指令 - * Java8的Lambda表达式的出现,invokedynamic指令的生成,在Java中才有了直接生成方式 + * Java8的Lambda表达式的出现,invokedynamic指令在Java中才有了直接生成方式 指令总结: @@ -14995,6 +14999,8 @@ JDK 自带了**监控工具,位于 JDK 的 bin 目录下**,其中最常用 + + *** @@ -17736,9 +17742,9 @@ Balking (犹豫)模式用在一个线程发现另一个线程或本线程已 ```java public class MonitorService { // 用来表示是否已经有线程已经在执行启动了 - private volatile boolean starting; + private volatile boolean starting = false; public void start() { - log.info("尝试启动监控线程..."); + System.out.println("尝试启动监控线程..."); synchronized (this) { if (starting) { return; @@ -17823,11 +17829,10 @@ public final class Singleton implements Serializable { 防止其他类无限创建对象;不能防止反射破坏 * 问题4:这种方式是否能保证单例对象创建时的线程安全? - 没有,静态变量初始化在类加载时完成,由JVM保证线程安全 + 能,静态变量初始化在类加载时完成,由JVM保证线程安全 * 问题5:为什么提供静态方法而不是直接将 INSTANCE 设置为 public? - - 更好的封装性、提供泛型支持、可以改进成懒汉单例设计 +更好的封装性、提供泛型支持、可以改进成懒汉单例设计 @@ -17927,7 +17932,7 @@ public final class Singleton { } public static Singleton getInstance() { - return LazyHolder.INSTANCE; + return LazyHolder.INSTANCE; } } ``` @@ -19232,7 +19237,7 @@ public class LinkedBlockingQueue extends AbstractQueue ![](https://gitee.com/seazean/images/raw/master/Java/JUC-LinkedBlockingQueue出队流程2.png) -* `E x = first.item` -> `first.item = null` +* `E x = first.item` -> `first.item = null`(**head.item = null**) @@ -22980,22 +22985,6 @@ ConcurrentHashMap 对锁粒度进行了优化,**分段锁技术**,将整张 -*** - - - -### LinkedQueue - -(待更新) - -ConcurrentLinkedQueue 的设计与 LinkedBlockingQueue 相似: - -* 两把锁,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行 -* dummy 节点的引入让两把锁将来锁住的是不同对象,避免竞争 -* 锁使用了 cas 来实现 -* 此队列不允许使用 null 元素 - - *** @@ -23153,6 +23142,297 @@ public boolean add(E e) { +*** + + + +### NoBlocking + +#### 非阻塞队列 + +并发编程中,需要用到安全的队列,实现安全队列可以使用2种方式: + +* 加锁,这种实现方式是阻塞队列 +* 使用循环CAS算法实现,这种方式是非阻塞队列 + +ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,采用先进先出的规则对节点进行排序,当添加一个元素时,会添加到队列的尾部,当获取一个元素时,会返回队列头部的元素 + +补充:ConcurrentLinkedDeque 是双向链表结构的无界并发队列 + +ConcurrentLinkedQueue使用约定: + +1. 不允许null入列 +2. 队列中所有未删除的节点的item都不能为null且都能从head节点遍历到 +3. 删除节点是将item设置为null,队列迭代时跳过item为null节点 +4. head节点跟tail不一定指向头节点或尾节点,可能存在滞后性 + +ConcurrentLinkedQueue由head节点和tail节点组成,每个节点(Node)由节点元素(item)和指向下一个节点的引用(next)组成,组成一张链表结构的队列 + +```java +private transient volatile Node head; +private transient volatile Node tail; + +private static class Node { + volatile E item; + volatile Node next; + //..... +} +``` + + + +*** + + + +#### 构造方法 + +* 无参构造方法: + + ```java + public ConcurrentLinkedQueue() { + // 默认情况下head节点存储的元素为空,tail节点等于head节点 + head = tail = new Node(null); + } + ``` + +* 有参构造方法 + + ```java + public ConcurrentLinkedQueue(Collection c) { + Node h = null, t = null; + // 遍历节点 + for (E e : c) { + checkNotNull(e); + Node newNode = new Node(e); + if (h == null) + h = t = newNode; + else { + // 单向链表 + t.lazySetNext(newNode); + t = newNode; + } + } + if (h == null) + h = t = new Node(null); + head = h; + tail = t; + } + ``` + + + +*** + + + +#### 入队方法 + +与传统的链表不同,单线程入队的工作流程: + +* 将入队节点设置成当前队列尾节点的下一个节点 +* 更新tail节点,如果tail节点的next节点不为空,则将入队节点设置成tail节点;如果tail节点的next节点为空,则将入队节点设置成tail的next节点,所以tail节点不总是尾节点,**存在滞后性** + +```java +public boolean offer(E e) { + checkNotNull(e); + // 创建入队节点 + final Node newNode = new Node(e); + + // 循环CAS直到入队成功 + for (Node t = tail, p = t;;) { + // p用来表示队列的尾节点,初始情况下等于tail节点,q 是p的next节点 + Node q = p.next; + // 判断p是不是尾节点 + if (q == null) { + // p是尾节点,设置p节点的下一个节点为新节点 + // 设置成功则casNext返回true,否则返回false,说明有其他线程更新过尾节点 + // 继续寻找尾节点,继续CAS + if (p.casNext(null, newNode)) { + // 首次添加时,p等于t,不进行尾节点更新,所以所尾节点存在滞后性 + if (p != t) + // 将tail设置为新入队的节点,设置失败表示其他线程更新了tail节点 + casTail(t, newNode); + return true; + } + } + else if (p == q) + // 当tail不指向最后节点时,如果执行出列操作,可能将tail也移除,tail不在链表中 + // 此时需要对tail节点进行复位,复位到head节点 + p = (t != (t = tail)) ? t : head; + else + // 推动tail尾节点往队尾移动 + p = (p != t && t != (t = tail)) ? t : q; + } +} +``` + +图解入队: + +![](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节点和尾节点的距离**大于等于1**时(每入队两次)更新tail,可以减少CAS更新tail节点的次数,提高入队效率 + +线程安全问题: + +* 线程1线程2同时入队,无论从哪个位置开始并发入队,都可以循环CAS,直到入队成功,线程安全 +* 线程1遍历,线程2入队,所以造成 ConcurrentLinkedQueue 的size是变化,需要加锁保证安全 +* 线程1线程2同时出列,线程也是安全的 + + + +*** + + + +#### 出队方法 + +出队列的就是从队列里返回一个节点元素,并清空该节点对元素的引用,并不是每次出队都更新head节点 + +* 当head节点里有元素时,直接弹出head节点里的元素,而不会更新head节点 +* 当head节点里没有元素时,出队操作才会更新head节点 + +**批处理方式**可以减少使用CAS更新head节点的消耗,从而提高出队效率 + +```java +public E poll() { + restartFromHead: + for (;;) { + // p节点表示首节点,即需要出队的节点 + for (Node h = head, p = h, q;;) { + E item = p.item; + // 如果p节点的元素不为null,则通过CAS来设置p节点引用元素为null,成功返回item + if (item != null && p.casItem(item, null)) { + if (p != h) + // 对head进行移动 + updateHead(h, ((q = p.next) != null) ? q : p); + return item; + } + // 如果头节点的元素为空或头节点发生了变化,这说明头节点被另外一个线程修改了 + // 那么获取p节点的下一个节点,如果p节点的下一节点为null,则表明队列已经空了 + else if ((q = p.next) == null) { + updateHead(h, p); + return null; + } + // 第一轮操作失败,下一轮继续,调回到循环前 + else if (p == q) + continue restartFromHead; + // 如果下一个元素不为空,则将头节点的下一个节点设置成头节点 + else + p = q; + } + } +} +final void updateHead(Node h, Node p) { + if (h != p && casHead(h, p)) + // 将旧结点h的next域指向为h + h.lazySetNext(h); +} +``` + +在更新完head之后,会将旧的头结点h的next域指向为h,图中所示的虚线也就表示这个节点的自引用,被移动的节点(item为null的节点)会被GC回收 + +![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentLinkedQueue出队操作1.png) + + + + + +如果这时,有一个线程来添加元素,通过tail获取的next节点则仍然是它本身,这就出现了**p == q** 的情况,出现该种情况之后,则会触发执行head的更新,将p节点重新指向为head + +参考文章:https://www.jianshu.com/p/231caf90f30b + + + +*** + + + +#### 成员方法 + +* peek() + + peek操作会改变head指向,执行peek()方法后head会指向第一个具有非空元素的节点 + + ```java + // 获取链表的首部元素,只读取而不移除 + public E peek() { + restartFromHead: + for (;;) { + for (Node h = head, p = h, q;;) { + E item = p.item; + if (item != null || (q = p.next) == null) { + // 更改h的位置为非空元素节点 + updateHead(h, p); + return item; + } + else if (p == q) + continue restartFromHead; + else + p = q; + } + } + } + ``` + +* size() + + 用来获取当前队列的元素个数,因为整个过程都没有加锁,在并发环境中从调用size方法到返回结果期间有可能增删元素,导致统计的元素个数不精确 + + ```java + public int size() { + int count = 0; + // first()获取第一个具有非空元素的节点,若不存在,返回null + // succ(p)方法获取p的后继节点,若p == p的后继节点,则返回head + // 类似遍历链表 + for (Node p = first(); p != null; p = succ(p)) + if (p.item != null) + // 最大返回Integer.MAX_VALUE + if (++count == Integer.MAX_VALUE) + break; + return count; + } + ``` + +* remove() + + ```java + public boolean remove(Object o) { + // 删除的元素不能为null + if (o != null) { + Node next, pred = null; + for (Node p = first(); p != null; pred = p, p = next) { + boolean removed = false; + E item = p.item; + // 节点元素不为null + if (item != null) { + // 若不匹配,则获取next节点继续匹配 + if (!o.equals(item)) { + next = succ(p); + continue; + } + // 若匹配,则通过CAS操作将对应节点元素置为null + removed = p.casItem(item, null); + } + // 获取删除节点的后继节点 + next = succ(p); + // 将被删除的节点移除队列 + if (pred != null && next != null) // unlink + pred.casNext(p, next); + if (removed) + return true; + } + } + return false; + } + ``` + + + From 4b874ba196ec859ddc2e3e8fb05a85392fbac53b Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 17 May 2021 22:29:30 +0800 Subject: [PATCH 005/242] Update Java Notes --- DB.md | 2 +- Java.md | 3058 ++++++++++++++++++++++++++++--------------------------- SSM.md | 35 +- Tool.md | 63 +- 4 files changed, 1612 insertions(+), 1546 deletions(-) diff --git a/DB.md b/DB.md index c8f4b9f..dcd8fe6 100644 --- a/DB.md +++ b/DB.md @@ -627,7 +627,7 @@ LIMIT:分页限定 | <> 或 != | 不等于 | | BETWEEN ... AND ... | 在某个范围之内(都包含) | | **IN(...)** | 多选一 | - | **LIKE 占位符** | 模糊查询 _单个任意字符 %多个任意字符,[] 匹配集合内的字符
`LIKE '[^AB]%' `:不以 A 和 B 开头的任意文本 | + | **LIKE 占位符** | 模糊查询:_单个任意字符、%任意个字符、[] 匹配集合内的字符
`LIKE '[^AB]%' `:不以 A 和 B 开头的任意文本 | | IS NULL | 是NULL | | IS NOT NULL | 不是NULL | | AND 或 && | 并且 | diff --git a/Java.md b/Java.md index ff4f350..1d9b45a 100644 --- a/Java.md +++ b/Java.md @@ -3597,7 +3597,7 @@ java.util.regex 包主要包括以下三个类: ##### 标准字符 标准字符集合 -能够与”多种字符“匹配的表达式。注意区分大小写,大写是相反的意思。只能校验"单"个字符。 +能够与”多种字符“匹配的表达式。注意区分大小写,大写是相反的意思。只能校验**"单"**个字符。 | 元字符 | 说明 | | ------ | ------------------------------------------------------------ | @@ -3620,14 +3620,14 @@ java.util.regex 包主要包括以下三个类: | 元字符 | 说明 | | ------------ | ----------------------------------------- | | [ab5@] | 匹配 "a" 或 "b" 或 "5" 或 "@" | -| [^ abc] | 匹配 "a","b","c" 之外的任意一个字符 | +| [^abc] | 匹配 "a","b","c" 之外的任意一个字符 | | [f-k] | 匹配 "f"~"k" 之间的任意一个字母 | | [^A-F0-3] | 匹配 "A","F","0"~"3" 之外的任意一个字符 | | [a-d[m-p]] | 匹配 a 到 d 或者 m 到 p:[a-dm-p](并集) | | [a-z&&[m-p]] | 匹配 a 到 z 并且 m 到 p:[a-dm-p](交集) | | [^] | 取反 | -* 正则表达式的特殊符号,被包含到中括号中,则失去特殊意义,除了^,-之外 +* 正则表达式的特殊符号,被包含到中括号中,则失去特殊意义,除了^,-之外,需要在前面加 \ * 标准字符集合,除小数点外,如果被包含于中括号,自定义字符集合将包含该集合。 比如:[\d. \ -+]将匹配:数字、小数点、+、- @@ -3664,7 +3664,7 @@ java.util.regex 包主要包括以下三个类: | 元字符 | 说明 | | ------ | ------------------------------------------------------------ | -| ^ | 与字符串开始的地方匹配。(在字符集合中用来求非,在字符集合外用作匹配字符串的开头) | +| ^ | 与字符串开始的地方匹配(在字符集合中用来求非,在字符集合外用作匹配字符串的开头) | | $ | 与字符串结束的地方匹配 | | \b | 匹配一个单词边界 | @@ -3677,7 +3677,7 @@ java.util.regex 包主要包括以下三个类: 在表达式( ( A ) ( B ( C ) ) ),有四个这样的组:((A)(B(C)))、(A)、(B(C))、(C)(按照括号从左到右) * 调用 matcher 对象的groupCount 方法返回一个 int值,表示matcher对象当前有多个捕获组。 -* 特殊的组(group(0)),它代表整个表达式。该组不包括在 groupCount 的返回值中。 +* 特殊的组group(0)、group(),它代表整个表达式,该组不包括在 groupCount 的返回值中。 | 表达式 | 说明 | | ------------------------- | ------------------------------------------------------------ | @@ -3713,15 +3713,15 @@ java.util.regex 包主要包括以下三个类: String regex = "<(h[1-6])>\w*?<\/\1>"; ``` - * 匹配结果 + 匹配结果 - ```java -

x

//匹配 -

x

//匹配 -

x

//不匹配 - ``` + ```java +

x

//匹配 +

x

//匹配 +

x

//不匹配 + ``` - + @@ -3771,6 +3771,73 @@ java.util.regex 包主要包括以下三个类: +#### 分组匹配 + +Pattern类: + `static Pattern compile(String regex)` : 将给定的正则表达式编译为模式 + `Matcher matcher(CharSequence input)` : 创建一个匹配器,匹配给定的输入与此模式 + `static boolean matches(String regex, CharSequence input)` : 编译正则表达式,并匹配输入 + +Matcher类: + `boolean find()` : 扫描输入的序列,查找与该模式匹配的下一个子序列 + `String group()` : 返回与上一个匹配的输入子序列。同group(0),匹配整个表达式的子字符串 + `String group(int group)` : 返回在上一次匹配操作期间由给定组捕获的输入子序列 + `int groupCount()` : 返回此匹配器模式中捕获组的数量 + +```java +public class Demo01{ + public static void main(String[] args) { + //表达式对象 + Pattern p = Pattern.compile("\\w+"); + //创建Matcher对象 + Matcher m = p.matcher("asfsdf2&&3323"); + //boolean b = m.matches();//尝试将整个字符序列与该模式匹配 + //System.out.println(b);//false + //boolean b2 = m.find();//该方法扫描输入的序列,查找与该模式匹配的下一个子序列 + //System.out.println(b2);//true + + //System.out.println(m.find()); + //System.out.println(m.group());//asfsdf2 + //System.out.println(m.find()); + //System.out.println(m.group());//3323 + + while(m.find()){ + System.out.println(m.group()); //group(),group(0)匹配整个表达式的子字符串 + System.out.println(m.group(0)); + } + + } +} +``` + +```java +public class Demo02 { + public static void main(String[] args) { + //在这个字符串:asfsdf23323,是否符合指定的正则表达式:\w+ + //表达式对象 + Pattern p = Pattern.compile("(([a-z]+)([0-9]+))");//不需要加多余的括号 + //创建Matcher对象 + Matcher m = p.matcher("aa232**ssd445"); + + while(m.find()){ + System.out.println(m.group());//aa232 ssd445 + System.out.println(m.group(1));//aa232 ssd445 + System.out.println(m.group(2));//aa ssd + System.out.println(m.group(3));//232 445 + } + } +} +``` + +* 正则表达式改为`"(([a-z]+)(?:[0-9]+))"` 没有group(3) 因为是非捕获组 +* 正则表达式改为`"([a-z]+)([0-9]+)"` 没有 group(3) aa232 - aa --232 + + + +*** + + + #### 应用 ##### 基本验证 @@ -3803,11 +3870,11 @@ public static void checkPhone(String phone){ System.out.println("手机号码格式正确!"); } else {.......} } -//1111@qq.com zhy980823@163.com zhy@pic.com.cn +//1111@qq.com zhy@pic.com.cn public static void checkEmail(String email){ if(email.matches("\\w{1,}@\\w{1,}(\\.\\w{2,5}){1,2}")){ System.out.println("邮箱格式正确!"); - } else {......}// .是任意字符 \\.就是点 + }// .是任意字符 \\.就是点 } ``` @@ -3843,69 +3910,6 @@ public static void main(String[] args) { -##### 分组问题 - -Pattern类: - `static Pattern compile(String regex)` : 将给定的正则表达式编译为模式 - `Matcher matcher(CharSequence input)` : 创建一个匹配器,匹配给定的输入与此模式 - `static boolean matches(String regex, CharSequence input)` : 编译正则表达式,并匹配输入 - -Matcher类: - `boolean find()` : 扫描输入的序列,查找与该模式匹配的下一个子序列。 - `String group()` : 返回与上一个匹配的输入子序列。同group(0),匹配整个表达式的子字符串 - `String group(int group)` : 返回在上一次匹配操作期间由给定组捕获的输入子序列。 - -```java -public class Demo01{ - public static void main(String[] args) { - //表达式对象 - Pattern p = Pattern.compile("\\w+"); - //创建Matcher对象 - Matcher m = p.matcher("asfsdf2&&3323"); - //boolean b = m.matches();//尝试将整个字符序列与该模式匹配 - //System.out.println(b);//false - //boolean b2 = m.find();//该方法扫描输入的序列,查找与该模式匹配的下一个子序列 - //System.out.println(b2);//true - - //System.out.println(m.find()); - //System.out.println(m.group());//asfsdf2 - //System.out.println(m.find()); - //System.out.println(m.group());//3323 - - while(m.find()){ - System.out.println(m.group()); //group(),group(0)匹配整个表达式的子字符串 - System.out.println(m.group(0)); - } - - } -} -``` - -```java -public class Demo02 { - public static void main(String[] args) { - //在这个字符串:asfsdf23323,是否符合指定的正则表达式:\w+ - //表达式对象 - //Pattern p = Pattern.compile("([a-z]+)([0-9]+)"); - Pattern p = Pattern.compile("(([a-z]+)([0-9]+))");//不需要加多余的括号 - //创建Matcher对象 - Matcher m = p.matcher("aa232**ssd445*sds223"); - - while(m.find()){ - System.out.println(m.group());//aa232 ssd445 - System.out.println(m.group(1));//aa232 ssd445 - System.out.println(m.group(2));//aa ssd - System.out.println(m.group(3));//232 445 - } - } -} -``` - -注: - -* 正则表达式改为`"(([a-z]+)(?:[0-9]+))"` No group(3) 非捕获 -* 正则表达式改为`"([a-z]+)([0-9]+)"` No group(3) aa232 - aa --232 - ##### 面试问题 @@ -5439,7 +5443,7 @@ transient int size; 4. resize - 当HashMap中的元素个数超过`(数组长度)*loadFactor(负载因子)`时,就会进行数组扩容,创建新的数组,伴随一次重新hash分配,并且会遍历hash表中所有的元素,非常耗时,所以要尽量避免resize + 当HashMap中的元素个数超过`(数组长度)*loadFactor(负载因子)`或者链表过长时,就会进行数组扩容,创建新的数组,伴随一次重新hash分配,并且遍历hash表中所有的元素,非常耗时,所以要尽量避免resize 扩容机制为扩容为原来容量的2倍: @@ -8266,7 +8270,7 @@ public class InetAddressDemo { ### UDP -#### 概述 +#### 基本介绍 UDP(User Datagram Protocol)协议的特点: 面向无连接的协议 @@ -8376,7 +8380,7 @@ UDP通信方式: ### TCP -#### 概念 +#### 基本介绍 TCP/IP协议 ==> Transfer Control Protocol ==> 传输控制协议 TCP/IP协议的特点: @@ -8844,7 +8848,7 @@ public class Server { ### NIO -#### 概述 +#### 基本介绍 **NIO的介绍**: @@ -8876,7 +8880,7 @@ public class Server { -#### 原理 +#### 实现原理 NIO三大核心部分:**Channel( 通道) ,Buffer( 缓冲区), Selector( 选择器)** @@ -8914,7 +8918,7 @@ NIO的实现框架: #### 缓冲区 -##### 概述 +##### 基本介绍 用于特定基本数据类型的容器。由 java.nio 包定义的,所有缓冲区都是 Buffer抽象类的子类。Java NIO 中的 Buffer 主要用于与 NIO 通道进行 交互,数据是从通道读入缓冲区,从缓冲区写入通道中的 @@ -8925,7 +8929,7 @@ NIO的实现框架: -##### 属性 +##### 基本属性 * 容量 (capacity) :作为一个内存块,Buffer具有一定的固定大小,也称为"容量",缓冲区容量不能为负,并且创建后不能更改。 @@ -9049,23 +9053,18 @@ public class TestBuffer { -##### 直接与非直接缓冲区 +##### 直接内存 -根据官方文档的描述: -`byte byffer`可以是两种类型,一种是基于直接内存(也就是非堆内存);另一种是非直接内存(也就是堆内存)。对于直接内存来说,JVM将会在IO操作上具有更高的性能,因为它直接作用于本地系统的IO操作。而非直接内存,也就是堆内存中的数据,如果要作IO操作,会先从本进程内存复制到直接内存,再利用本地IO处理。 +`byte byffer`可以是两种类型,一种是基于直接内存(也就是非堆内存);另一种是非直接内存(也就是堆内存)。对于直接内存来说,JVM将会在IO操作上具有更高的性能,因为它直接作用于本地系统的IO操作;而非直接内存,也就是堆内存中的数据,如果要作IO操作,会先从本进程内存复制到直接内存,再利用本地IO处理 直接内存创建Buffer对象:`static XxxBuffer allocateDirect(int capacity)` 数据流的角度: -非直接内存的作用链:**本地IO-->直接内存-->非直接内存-->直接内存-->本地IO** -直接内存是:**本地IO-->直接内存-->本地IO** - -**优缺点**:直接内存使用allocateDirect创建,这部分的数据是在JVM之外的,因此它不会占用应用的内存,但是它比申请普通的堆内存需要耗费更高的性能。字节缓冲区是否是直接缓冲区可通过调用isDirect() 方法来确定。 -**使用场景**: +* 非直接内存的作用链:本地IO-->直接内存-->非直接内存-->直接内存-->本地IO +* 直接内存是:本地IO-->直接内存-->本地IO -- 有很大的数据需要存储,数据的生命周期很长 -- 适合频繁的IO操作,比如网络并发场景 +JVM内存结构详解直接内存 @@ -11423,6 +11422,8 @@ JVM:全称Java Virtual Machine,即Java虚拟机,一种规范,本身是 * Java虚拟机基于**二进制字节码**执行,由一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆、一个方法区等组成 * JVM屏蔽了与操作系统平台相关的信息,从而能够让Java程序只需要生成能够在JVM上运行的字节码文件,通过该机制实现的**跨平台性** +Java代码执行流程:java程序 --(编译)--> 字节码文件 --(解释执行)--> 操作系统(Win,Linux) + JVM结构: @@ -11437,15 +11438,40 @@ JVM、JRE、JDK对比: -### 生命周期 +### 架构模型 + +Java编译器输入的指令流是一种基于栈的指令集架构。因为跨平台的设计,java的指令都是根据栈来设计的,不同平台CPU架构不同,所以不能设计为基于寄存器架构 + +* 基于栈式架构的特点: + * 设计和实现简单,适用于资源受限的系统 + * 使用零地址指令方式分配,执行过程依赖操作栈,指令集更小,编译器容易实现 + * 零地址指令:机器指令的一种,是指令系统中的一种不设地址字段的指令,只有操作码而没有地址码。这种指令有两种情况:一是无需操作数,另一种是操作数为默认的(隐含的),默认为操作数在寄存器中,指令可直接访问寄存器 + * 一地址指令:一个地址对应一个操作数 + * 不需要硬件的支持,可移植性更好,更好实现跨平台 +* 基于寄存器架构的特点: + * 需要硬件的支持,可移植性差 + * 性能更好,执行更高效,寄存器比内存快 + * 以一地址指令、二地址指令、三地址指令为主 + + + +*** + -JVM的生命周期分为三个阶段,分别为:启动、运行、死亡。 -- **启动**:当启动一个Java程序时,JVM的实例就已经产生,对于拥有main函数的类就是JVM实例运行的起点。 +### 生命周期 -- **运行**:main()方法是一个程序的初始起点,任何线程均可由在此处启动。在JVM内部有两种线程类型,分别为:**用户线程和守护线程**。JVM通常使用的是守护线程,而main()使用的则是用户线程,守护线程会随着用户线程的结束而结束。 +JVM的生命周期分为三个阶段,分别为:启动、运行、死亡。 -- **死亡**:当程序中的用户线程都中止,JVM才会退出 +- **启动**:当启动一个Java程序时,通过引导类加载器(bootstrap class loader)创建一个初始类(initial class),对于拥有main函数的类就是JVM实例运行的起点 +- **运行**: + - main()方法是一个程序的初始起点,任何线程均可由在此处启动 + - 在JVM内部有两种线程类型,分别为:**用户线程和守护线程**,JVM通常使用的是守护线程,而main()和其他线程使用的是用户线程,守护线程会随着用户线程的结束而结束 + - 执行一个Java程序时,真真正正在执行的是一个Java虚拟机的进程 +- **死亡**: + - 当程序中的用户线程都中止,JVM才会退出 + - 程序正常执行结束、程序异常或错误而异常终止、操作系统错误导致终止 + - 某线程调用Runtime类halt方法或System类exit方法,并且java安全管理器允许这次exit或halt操作 @@ -11454,7 +11480,9 @@ JVM的生命周期分为三个阶段,分别为:启动、运行、死亡。 -### VM参数 +### 相关参数 + +进入 Run/Debug Configurations ---> VM options 设置参数 | 参数 | 功能 | | ------------------------------------------------------------ | ------------------------------------------------------------ | @@ -11476,6 +11504,8 @@ JVM的生命周期分为三个阶段,分别为:启动、运行、死亡。 + + *** @@ -11484,7 +11514,7 @@ JVM的生命周期分为三个阶段,分别为:启动、运行、死亡。 ### 内存概述 -JVM内存结构是JVM中非常重要的一部分,并且在JDK7和JDK8中也进行了一些改动。内存是非常重要的系统资源,是硬盘和 CPU 的中间仓库及桥梁,承载着操作系统和应用程序的实时运行 +内存结构是JVM中非常重要的一部分,是非常重要的系统资源,是硬盘和 CPU 的中间仓库及桥梁,承载着操作系统和应用程序的实时运行,又叫运行时数据区 JVM 内存结构规定了 Java 在运行过程中内存申请、分配、管理的策略,保证了 JVM 的高效稳定运行 @@ -11515,9 +11545,11 @@ JVM 内存结构规定了 Java 在运行过程中内存申请、分配、管理 -### 虚拟机栈 +### JVM内存 + +#### 虚拟机栈 -#### Java栈 +##### Java栈 Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所需要的内存 @@ -11538,7 +11570,6 @@ Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所 设置栈内存大小:`-Xss size` `-Xss 1024k` -* 进入 Run/Debug Configurations ---> VM options 设置参数 * 在 JDK 1.4 中默认为 256K,而在 JDK 1.5+ 默认为 1M 虚拟机栈特点: @@ -11562,7 +11593,7 @@ Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所 -#### 局部变量表 +##### 局部变量 局部变量表也被称之为局部变量数组或本地变量表,本质上定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量 @@ -11584,7 +11615,7 @@ Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所 -#### 操作数栈 +##### 操作数栈 栈 :可以使用数组或者链表来实现 @@ -11605,7 +11636,7 @@ Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所 -#### 动态链接 +##### 动态链接 动态链接就是将符号引用转换为调用方法的直接引用 @@ -11624,7 +11655,7 @@ Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所 -#### 返回地址 +##### 返回地址 Return Address:存放调用该方法的PC寄存器的值 @@ -11641,7 +11672,7 @@ Return Address:存放调用该方法的PC寄存器的值 -#### 附加信息 +##### 附加信息 栈帧中还允许携带与java虚拟机实现相关的一些附加信息。例如,对程序调试提供支持的信息 @@ -11651,7 +11682,7 @@ Return Address:存放调用该方法的PC寄存器的值 -### 本地方法栈 +#### 本地方法栈 本地方法栈是为虚拟机**执行本地方法时提供服务的** @@ -11677,7 +11708,7 @@ Return Address:存放调用该方法的PC寄存器的值 -### 程序计数器 +#### 程序计数器 Program Counter Register 程序计数器(寄存器) @@ -11714,7 +11745,7 @@ Java**反编译**指令:`javap -v Test.class` -### 堆 +#### 堆 Heap 堆:是JVM内存中最大的一块,由所有线程共享,由垃圾回收器管理的主要区域("GC 堆"),堆中对象都需要考虑线程安全的问题 @@ -11723,7 +11754,7 @@ Heap 堆:是JVM内存中最大的一块,由所有线程共享,由垃圾回 * 对象实例:类初始化生成的对象,**基本数据类型的数组也是对象实例**,new 创建对象都使用堆内存 * 字符串常量池: * 字符串常量池原本存放于方法区,jdk7开始放置于堆中 - * 字符串常量池**存储的是string对象的直接引用,而不是直接存放的对象**,是一张string table + * 字符串常量池**存储的是string对象的直接引用或者对象**,是一张string table * 静态变量:静态变量是有static修饰的变量,jdk7时从方法区迁移至堆中 * 线程分配缓冲区(Thread Local Allocation Buffer):线程私有但不影响堆的共性,可以提升对象分配的效率 @@ -11746,16 +11777,14 @@ Heap 堆:是JVM内存中最大的一块,由所有线程共享,由垃圾回 分代原因:不同对象的生命周期不同,70%-99%的对象都是临时对象,优化GC性能 ```java -public static void main(String[] args) throws InterruptedException { - System.out.println("1..."); - Thread.sleep(30000); - byte[] array = new byte[1024 * 1024 * 10]; // 10 Mb - System.out.println("2..."); - Thread.sleep(20000); - array = null; - System.gc(); - System.out.println("3..."); - Thread.sleep(1000000L); +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 } ``` @@ -11765,7 +11794,7 @@ public static void main(String[] args) throws InterruptedException { -### 方法区 +#### 方法区 方法区:是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它也叫 Non-Heap(非堆) @@ -11775,10 +11804,9 @@ public static void main(String[] args) throws InterruptedException { 为了**避免方法区出现OOM**,在JDK8中将堆内的方法区(永久代)移动到了本地内存上,重新开辟了一块空间,叫做**元空间**,元空间存储类的元信息,静态变量和字符串常量池等放入堆中 -类元信息: +类元信息:在类编译期间放入方法区,存放了类的基本信息,包括类的方法、参数、接口以及常量池表 -* 类元信息在类编译期间放入方法区,里面放置了类的基本信息,包括类的方法、参数、接口以及常量池表 -* 常量池表(Constant Pool Table)是Class文件的一部分,存储了类在编译期间生成的字面量、符号引用,这些信息在类加载后会被解析到运行时常量池中,JVM为每个已加载的类维护一个常量池 +常量池表(Constant Pool Table)是Class文件的一部分,存储了类在编译期间生成的字面量、符号引用,这些信息在类加载后会被解析到运行时常量池中,JVM为每个已加载的类维护一个常量池 **运行时常量池**是方法区的一部分 @@ -11817,22 +11845,18 @@ PermGen 被元空间代替,永久代的**类信息、方法、常量池**等 方法区内存溢出: -* JDK1.8以前会导致永久代内存溢出 +* JDK1.8以前会导致永久代内存溢出:java.lang.OutOfMemoryError: PerGen space ```sh - #演示永久代内存溢出 java.lang.OutOfMemoryError: PerGen space - -XX:MaxPermSize=8m + -XX:MaxPermSize=8m #参数设置 ``` - -* JDK1.8以后会导致元空间内存溢出 + +* JDK1.8以后会导致元空间内存溢出:java.lang.OutOfMemoryError: Metaspace ```sh - #演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace - -XX:MaxMetaspaceSize=8m + -XX:MaxMetaspaceSize=8m #参数设置 ``` -场景:spring、mybatis - 元空间内存溢出演示: ```java @@ -11866,20 +11890,42 @@ public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制 #### 直接内存 -Direct Memory特点: +##### 基本介绍 + +直接内存是Java堆外的、直接向系统申请的内存区间,不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域 -* 常见于 NIO 操作时,用于数据缓冲区,使用native函数直接分配堆外内存 -* 分配回收成本较高,但读写性能高 -* 不受 JVM 内存回收管理 +Direct Memory优点: + +* Java 的 NIO 库允许Java程序使用直接内存,用于数据缓冲区,使用native函数直接分配堆外内存 +* 读写性能高,读写频繁的场合可能会考虑使用直接内存 * 大大提高IO性能,避免了在java堆和native堆来回复制数据 +直接内存缺点: + +* 分配回收成本较高,不受 JVM 内存回收管理 + +* 可能导致OutOfMemoryError异常:OutOfMemoryError: Direct buffer memory + +应用场景: + +- 有很大的数据需要存储,数据的生命周期很长 +- 适合频繁的IO操作,比如网络并发场景 + + + +##### 底层原理 + +工作流程: + + + + + 分配和回收原理: * 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法 * ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory 来释放直接内存 - - ```java /** * 直接内存分配的底层原理:Unsafe @@ -12161,6 +12207,8 @@ JVM是通过栈帧中的对象引用访问到其内部的对象实例:(内 #### TLAB +虚拟机采用了两种方式在创建对象时解决并发问题:CAS、TLAB + TLAB:Thread Local Allocation Buffer,为每个线程在堆内单独分配了一个缓冲区,多线程分配内存时,使用TLAB可以避免线程安全问题,同时还能够提升内存分配的吞吐量,这种内存分配方式叫做**快速分配策略** - 栈上分配使用的是栈来进行对象内存的分配 @@ -12198,7 +12246,6 @@ JVM是将TLAB作为内存分配的首选,但不是所有的对象实例都能 * C1编译速度快,优化方式比较保守;C2编译速度慢,优化方式比较激进 * C1+C2在开始阶段采用C1编译,当代码运行到一定热度之后采用G2重新编译 * 在1.8之前,分层编译默认是关闭的,可以添加`-server -XX:+TieredCompilation`参数进行开启 -* JIT介绍参考类加载 --> 运行优化 --> 即时编译 **逃逸分析**:并不是直接的优化手段,而是一个代码分析,通过动态分析对象的作用域,为其它优化手段如栈上分配、标量替换和同步消除等提供依据,发生逃逸行为的情况有两种:方法逃逸和线程逃逸 @@ -12370,7 +12417,7 @@ Full GC 则相对复杂,**FullGC同时回收新生代和老年代,当前只 注意: * **GC Roots是一组活跃的引用,不是对象** -* Root采用栈方式存放变量和指针,如果一个指针保存了堆内存中的对象,但自己不在堆内存中,它就是一个Root +* 如果一个指针保存了堆内存中的对象,但自己不在堆内存中,它就是一个Root 使用可达性分析算法来判断内存是否可回收,分析工作必须在一个能保障**一致性的快照**中进行,否则分析结果的准确性无法保证,这点也是导致GC进行时必须“Stop The World”的一个重要原因 @@ -12782,7 +12829,7 @@ G1对比其他处理器的优点: * 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此不会在整个回收阶段发生完全阻塞应用程序的情况 * 其他的垃圾收集器使用内置的JVM线程执行GC的多线程操作,而G1 GC可以采用应用线程承担后台运行的GC工作,JVM的GC线程处理速度慢时,系统会**调用应用程序线程加速垃圾回收**过程 -* **分代收集:** +* **分区算法:** * 从分代上看,G1属于分代型垃圾回收器,区分年轻代和老年代,年轻代依然有Eden区和Survivor区 从堆结构上看,不要求整个Eden区、年轻代或老年代都是连续的,也不再坚持固定大小和固定数量 @@ -12928,7 +12975,7 @@ G1的设计原则就是简化JVM性能调优,只需要简单的三步即可完 #### ZGC -ZGC收集器是一款基于Region内存布局的,(暂时) 不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记压缩算法的,以低延迟为首要目标的一款垃圾收集器 +ZGC收集器是一款基于Region内存布局的,不设分代,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记压缩算法的,以低延迟为首要目标的一款垃圾收集器 与 CMS 和 G1 类似,ZGC也采用标记-复制算法,不过 ZGC 对该算法做了重大改进,在 ZGC 中出现 Stop The World 的情况会更少,在尽可能对吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟 @@ -12981,374 +13028,385 @@ Serial GC、Parallel GC、Concurrent Mark Sweep GC这三个GC不同: ## 类加载 -### 编译执行 +### 对象创建 -#### 编译过程 +#### 创建时机 - Java文件编译执行的过程: +类在第一次实例化加载一次,后续实例化不再加载,引用第一次加载的类 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java文件编译执行的过程.png) +Java对象创建时机: -- 类加载器:用于装载字节码文件(.class文件) -- 运行时数据区:用于分配存储空间 -- 执行引擎:执行字节码文件或本地方法 -- 垃圾回收器:用于对JVM中的垃圾内容进行回收 +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方法 -#### 执行引擎 +5. 使用(反)序列化机制创建对象:当反序列化一个对象时,JVM会创建一个单独的对象,在此过程中,JVM并不会调用任何构造函数,为了反序列化一个对象,需要让类实现Serializable接口 -执行引擎:Java虚拟机的核心组成部分之一,JVM的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,需要执行引擎将字节码指令解释/编译为对应平台上的本地机器指令 +从Java虚拟机层面看,除了使用new关键字创建对象的方式外,其他方式全部都是通过转变为invokevirtual指令直接创建对象的 -* 解释器:根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行,**满足Java程序实现跨平台特性** -* JIT (Just In Time Compiler)(即时编译器):虚拟机将源代码直接编译成和本地机器平台相关的机器码 -机器码:各种用二进制编码方式表示的指令 +*** -指令:指令就是把机器码中特定的0和1序列,简化成对应的指令,例如mov,inc等,可读性稍好 -指令集:每个平台所支持的指令 -- x86指令集,对应的是x86架构的平台 -- ARM指令集,对应的是ARM架构的平台 +#### 创建过程 -字节码:是一种中间状态(中间码)的二进制代码,比机器码更抽象,需要直译器转译后才能成为机器码 +创建对象的过程: +1. **判断对象对应的类是否加载、链接、初始化** +2. **为对象分配内存**:指针碰撞、空闲链表。当一个对象被创建时,虚拟机就会为其分配内存来存放对象的实例变量及其从父类继承过来的实例变量 (即使从超类继承过来的实例变量有可能被隐藏也会被分配空间) -*** +3. **处理并发安全问题**: + * 采用CAS配上自旋保证更新的原子性 + * 每个线程预先分配一块TLAB +4. **初始化分配的空间**:虚拟机将分配到的内存空间都初始化为零值(不包括对象头),保证对象实例字段在不赋值时可以直接使用,程序能访问到这些字段的数据类型所对应的零值 -### 字节码 +5. **设置对象的对象头**:将对象的所属类(类的元数据信息)、对象的HashCode、对象的GC信息、锁信息等数据存储在对象的对象头中 -#### 类结构 +6. **执行init方法进行实例化**:实例变量初始化、实例代码块初始化 、构造函数初始化 -class文件是编译器编译之后供虚拟机解释执行的二进制字节码文件,一个class文件对应一个public类型的类 + * 实例变量初始化与实例代码块初始化: -根据 JVM 规范,类文件结构如下: + 对实例变量直接赋值或者使用实例代码块赋值,编译器会将其中的代码放到类的构造函数中去,并且这些代码会被放在对超类构造函数的调用语句之后 (**Java要求构造函数的第一条语句必须是超类构造函数的调用语句**),构造函数本身的代码之前 -```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]; -} -``` + * 构造函数初始化: -HelloWorld.java 执行 `javac -parameters -d . HellowWorld.java`指令: + **Java要求在实例化类之前,必须先实例化其超类,以保证所创建实例的完整性**,在准备实例化一个类的对象前,首先准备实例化该类的父类,如果该类的父类还有父类,那么准备实例化该类的父类的父类,依次递归直到递归到Object类。然后从Object类依次对以下各类进行实例化,初始化父类中的变量和执行构造函数 -```sh -0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09 -0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07 -0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29 //... -``` -魔数:0~3 字节,表示是否是Classs类型的文件,ca fe ba be代表Java -版本:4~7 字节,表示类的版本 00 34(52) 表示是 Java 8 +*** -常量池:8~9 字节,表示常量池长度,00 23 (35) 表示常量池有 #1~#34项,注意 #0 项不计入,也没有值 -* 第#1项 0a 表示一个 Method 信息,00 06 和 00 15(21) 表示它引用了常量池中 #6 和 #21 项来获得 - 这个方法的【所属类】和【方法名】 -* 第#2项 09 表示一个 Field 信息,00 16(22)和 00 17(23) 表示它引用了常量池中 #22 和 # 23 项 - 来获得这个成员变量的【所属类】和【成员变量名】 -* 第#n项 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-类结构常量池.png) +#### 承上启下 -```sh -0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01 -``` +1. 一个实例变量在对象初始化的过程中会被赋值几次? -访问标识与继承信息: + JVM在为一个对象分配完内存之后,会给每一个实例变量赋予默认值,这个实例变量被第一次赋值 + 在声明实例变量的同时对其进行了赋值操作,那么这个实例变量就被第二次赋值 + 在实例代码块中又对变量做了初始化操作,那么这个实例变量就被第三次赋值 + 在构造函数中也对变量做了初始化操作,那么这个实例变量就被第四次赋值 + 在Java的对象初始化过程中,一个实例变量最多可以被初始化4次 -* 00 21 表示该 class 是一个类,公共的 -* 00 05 表示根据常量池中 #5 找到本类全限定名 -* 00 06 表示根据常量池中 #6 找到父类全限定名 -* 00 00 表示接口的数量,本类为 0 +2. 类的初始化过程与类的实例化过程的异同? -Field信息:表示成员变量数量,00 00表示本类为 0 + 类的初始化是指类加载过程中的初始化阶段对类变量按照代码进行赋值的过程 + 类的实例化是指在类完全加载到内存中后创建对象的过程(类的实例化触发了类的初始化) -![](https://gitee.com/seazean/images/raw/master/Java/JVM-类结构Field信息.png) +3. 假如一个类还未加载到内存中,那么在创建一个该类的实例时,具体过程是怎样的?(**经典案例**) -Method信息:表示方法数量,00 02 本类为 2,一个方法由 访问修饰符,名称,参数描述,方法属性数量,方法属性组成 + ```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();`: -* 00 01 表示附加属性数量 -* 00 13 表示引用了常量池 #19 项,即【SourceFile】 -* 00 00 00 02 表示此属性的长度 -* 00 14 表示引用了常量池 #20 项,即【HelloWorld.java】 + * 实例初始化不一定要在类初始化结束之后才开始 -```sh -0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00 -0001120 00 00 02 00 14 -``` + * 在同一个类加载器下,一个类型只会被初始化一次。所以一旦开始初始化一个类,无论是否完成后续都不会再重新触发该类型的初始化阶段了(只考虑在同一个类加载器下的情形)。因此,在实例化上述程序中的st变量时,**实际上是把实例初始化嵌入到了静态初始化流程中,并且在上面的程序中,嵌入到了静态初始化的起始位置**,这就导致了实例初始化完全发生在静态初始化之前,这也是导致a为110 b为0的原因 + 代码等价于: + ```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 + } + } + ``` + -**** +*** -#### javap -`javap -v HelloWorld.class`:反编译 class 文件 +### 加载过程 -slot:局部变量表中最基本的存储单元 +#### 生命周期 -aload:从局部变量表的相应位置装载一个对象引用到操作数栈的栈顶 +类是在运行期间**第一次使用时动态加载**的(不使用不加载),而不是一次性加载所有类,因为一次性加载会占用很多的内存,加载的类信息存放于一块成为方法区的内存空间 -* aload_0把this装载到了操作数栈中 -* aload_0是一组格式为aload_的操作码中的一个,这一组操作码把对象的引用装载到操作数栈中标志了待处理的局部变量表中的位置,但取值仅可为0、1、2或者3 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-类的生命周期.png) -`iload_,lload_,fload_,dload_`:i代表int型,l代表long型,f代表float型以及d代表double型 +包括 7 个阶段: -* 在局部变量表中的索引位置大于3的变量的装载可以使用iload、lload、fload,、dload和aload -* 这些操作码都需要一个操作数的参数,用于确认需要装载的局部变量的位置 +* 加载(Loading) +* 链接:验证(Verification)、准备(Preparation)、解析(Resolution) +* 初始化(Initialization) +* 使用(Using) +* 卸载(Unloading) -Ljava.lang.String: +类加载方式: -* `[`:表示一维数组 -* `[[`:表示二维数组 -* `L`:表示一个对象 -* `java.lang.String`:表示对象的类型 +* 隐式加载: + * 创建类对象、使用类的静态域、创建子类对象、使用子类的静态域 + * 在JVM启动时,通过三大类加载器加载class +* 显式加载: + * ClassLoader.loadClass(className),只加载和连接,**不会进行初始化** + * Class.forName(String name, boolean initialize,ClassLoader loader),使用loader进行加载和连接,根据参数initialize决定是否初始化 +*** +#### 加载阶段 -*** +加载是类加载的一个阶段,注意不要混淆 +加载过程完成以下三件事: +- 通过类的完全限定名称获取定义该类的二进制字节流 +- 将该字节流表示的静态存储结构转换为方法区的运行时存储结构 +- 在内存中生成一个代表该类的 Class 对象,作为方法区中该类各种数据的访问入口 -#### 执行流程 +其中二进制字节流可以从以下方式中获取: -原始Java代码: +- 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础 +- 从网络中获取,最典型的应用是 Applet +- 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass +- 由其他文件生成,例如由 JSP 文件生成对应的 Class 类 -```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); - } -} -``` +将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field: -javap -v Demo.class:省略 +* _java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 class 暴露给 java 使用 +* _super 即父类、_fields 即成员变量、_methods 即方法、_constants 即常量池、_class_loader 即类加载器、_vtable 虚方法表、_itable 接口方法表 -* 常量池载入运行时常量池 +注意: -* 方法区字节码载入方法区 +* 如果这个类还有父类没有加载,先加载父类 +* 加载和链接可能是交替运行的 +* instanceKlass和_java_mirror相互持有对方的地址,堆中对象通过instanceKlass和元空间进行交互 -* 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) +##### 验证 - `istore_2`:将操作数栈顶数据弹出,存入局部变量表的 slot 2 +确保 Class 文件的字节流中包含的信息是否符合 JVM规范,保证被加载类的正确性,不会危害虚拟机自身的安全 - `iload_1`:将局部变量表的 slot 1数据弹出,放入操作数栈栈顶 +主要包括四种验证:文件格式验证,源数据验证,字节码验证,符号引用验证 - `iload_2`:将局部变量表的 slot 2数据弹出,放入操作数栈栈顶 - `iadd`:执行相加操作 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程3.png) +##### 准备 - `istore_3`:将操作数栈顶数据弹出,存入局部变量表的 slot 3 +准备阶段为**静态变量分配内存并设置初始值**,使用的是方法区的内存: - `getstatic #4`:获取静态字段 +* 类变量也叫静态变量,就是是被 static 修饰的变量 +* 实例变量也叫对象变量,即没加static 的变量 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程4.png) +实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中。实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次 - `iload_3`: +**类变量初始化**: - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程5.png) +* static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾 +* static 变量分配空间和赋值是两个步骤:**分配空间在准备阶段完成,赋值在初始化阶段完成** +* 如果 static 变量是 final 的基本类型以及字符串常量,那么编译阶段值就确定了,准备阶段会显式初始化 +* 如果 static 变量是 final 的,但属于引用类型或者构造器方法的字符串,赋值在初始化阶段完成 - `invokevirtual #5`: +实例: - * 找到常量池 #5 项 - * 定位到方法区 java/io/PrintStream.println:(I)V 方法 - * **生成新的栈帧**(分配 locals、stack等) - * 传递参数,执行新栈帧中的字节码 - * 执行完毕,弹出栈帧 - * 清除 main 操作数栈内容 +* 初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123: - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程6.png) + ```java + public static int value = 123; + ``` - return:完成 main 方法调用,弹出 main 栈帧,程序结束 +* 常量 value 被初始化为 123 而不是 0: - + ```java + public static final int value = 123; + ``` -*** +##### 解析 -#### 条件判断 +将常量池中类、接口、字段、方法的**符号引用替换为直接引用**(内存地址)的过程 + +* 符号引用:一组符号来描述目标,可以是任何字面量,属于编译原理方面的概念如:包括类和接口的全限名、字段的名称和描述符、方法的名称和描述符 +* 直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄 + +解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等,某些解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。 ```java -public static void main(String[] args) { - int a = 0; - if(a == 0) { - a = 10; - } else { - a = 20; +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 { +} ``` -说明: -* byte,short,char 都会按 int 比较,因为操作数栈都是 4 字节 -* goto 用来进行跳转到指定行号的字节码 -```sh -1: istore_1 -2: iload_1 -3: ifne 12 -6: bipush 10 -8: istore_1 -9: goto 15 -12: bipush 20 -14: istore_1 -15: return -``` +**** - +#### 初始化 -*** +##### 介绍 +初始化阶段才真正开始执行类中定义的 Java 程序代码,在准备阶段,类变量已经赋过一次系统要求的初始值;在初始化阶段,通过程序制定的计划去初始化类变量和其它资源,执行 +在编译生成class文件时,编译器会产生两个方法加于class文件中,一个是类的初始化方法clinit, 另一个是实例的初始化方法init -#### 循环控制 +类构造器()与实例构造器()不同,它不需要程序员进行显式调用,在一个类的生命周期中,类构造器()最多被虚拟机**调用一次**,而实例构造器()则会被虚拟机调用多次,只要程序员创建对象 -iinc 指令:是直接在局部变量 slot 上进行运算 +类在第一次实例化加载一次,把class读入内存,后续实例化不再加载,引用第一次加载的类 -while循环: -```java -public static void main(String[] args) { - int a = 0; - while (a < 10) { - a++; - } -} -``` -```sh -0: iconst_0 -1: istore_1 -2: iload_1 -3: bipush 10 -5: if_icmpge 14 -8: iinc 1, 1 -11: goto 2 -14: return -``` +##### clinit -for循环: +():类构造器,由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的 + +作用:是在类加载过程中的初始化阶段进行静态变量初始化和执行静态代码块 + +* 如果类中没有静态变量或静态代码块,那么clinit方法将不会被生成 +* 在执行clinit方法时,必须先执行父类的clinit方法 +* clinit方法只执行一次 +* static变量的赋值操作和静态代码块的合并顺序由源文件中出现的顺序决定 + +**线程安全**问题: + +* 虚拟机会保证一个类的 () 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 () 方法,其它线程都阻塞等待,直到活动线程执行 () 方法完毕 +* 如果在一个类的 () 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽 + +特别注意:静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问 ```java -for (int i = 0; i < 10; i++) { } +public class Test { + static { + i = 0; // 给变量赋值可以正常编译通过 + System.out.print(i); // 这句编译器会提示“非法向前引用” + } + static int i = 1; +} ``` -```sh -0: iconst_0 -1: istore_1 -2: iload_1 -3: bipush 10 -5: if_icmpge 14 -8: iinc 1, 1 -11: goto 2 -14: return -``` +补充: +接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 () 方法。但两者不同的是,执行接口的 () 方法不需要先执行父接口的 () 方法,只有当父接口中定义的变量使用时,父接口才会初始化;接口的实现类在初始化时也一样不会执行接口的 () 方法 -*** +##### 时机 +类的初始化是懒惰的,初始化时机: -#### 面试题 +**主动引用**:虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随之发生): -##### 分析i++ +* 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化,最常见的生成这 4 条指令的场景是: + * new:使用 new 关键字实例化对象时 + * getstatic:程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池) + * putstatic:程序给类的静态变量赋值 + * invokestatic :调用一个类的静态方法时 +* 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化 +* 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化 +* 当虚拟机启动时,需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类 +* MethodHandle和VarHandle可以看作是轻量级的反射调用机制,而要想使用这2个调用, 就必须先使用findStaticVarHandle来初始化要调用的类 +* 补充:当一个接口中定义了JDK8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化 -从字节码角度分析:a++ 和 ++a 的区别是先执行 iload 还是 先执行 iinc +**被动引用**:所有引用类的方式都不会触发初始化,称为被动引用 -```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 - } -} -``` +* 通过子类引用父类的静态字段,不会导致子类初始化,只会触发父类的初始化 +* 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自Object 的子类,其中包含了数组的属性和方法 +* 常量(final修饰)在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化 -##### 判断结果 +##### init -```java -public class Demo3_6_1 { - public static void main(String[] args) { - int i = 0; - int x = 0; - while (i < 10) { - x = x++; - i++; - } - System.out.println(x); // 结果是 0 - } -} -``` +init指的是实例构造器,主要作用是在类实例化过程中执行,执行内容包括成员变量初始化和代码块的执行 +实例化即调用 ()V ,虚拟机会保证这个类的构造方法的线程安全,先为实例变量分配内存空间,再执行赋默认值,然后根据源码中的顺序执行赋初值或代码块,没有成员变量初始化和代码块则不会执行 +类实例化过程:**父类的类构造器() -> 子类的类构造器() -> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成员变量和实例代码块 -> 子类的构造函数** @@ -13356,115 +13414,112 @@ public class Demo3_6_1 { -#### 方法调用 +#### 卸载阶段 -##### 基本介绍 +时机:执行了System.exit()方法,程序正常执行结束,程序在执行过程中遇到了异常或错误而异常终止,由于操作系统出现错误而导致Java虚拟机进程终止 -在JVM中,将符号引用转换为直接引用有两种机制: +卸载类即该类的**Class对象被GC**,卸载类需要满足3个要求: -* 静态链接:当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变,将调用方法的符号引用转换为直接引用的过程称之为静态链接 -* 动态链接:如果被调用的方法在编译期无法被确定下来,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此被称为动态链接 +1. 该类的所有的实例对象都已被GC,也就是说堆不存在该类的实例对象 +2. 该类没有在其他任何地方被引用 +3. 该类的类加载器的实例已被GC -对应的方法的绑定机制为:静态绑定和动态绑定。绑定是一个字段、方法或者类从符号引用被替换为直接引用的过程,仅发生一次 +在JVM生命周期类,由jvm自带的类加载器加载的类是不会被卸载的,但是由我们自定义的类加载器加载的类是可能被卸载 -* 静态绑定:被调用的目标方法在编译期可知,且运行期保持不变,即可将这个方法与所属的类型进行绑定 -* 动态绑定:被调用的方法在编译期无法被确定,只能够在程序运行期根据实际的类型绑定相关的方法 -非虚方法: -* 方法在编译器就确定了具体的调用版本,这个版本在运行时是不可变的 -* 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法,其他方法称为虚方法 +**** -动态类型语言和静态类型语言: -* 在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之则是动态类型语言 -* 静态语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息 +### 类加载器 -* **Java是静态类型语言**(尽管lambda表达式为其增加了动态特性),js,python是动态类型语言 +#### 加载器 - ```java - String s = "abc"; //Java - info = "abc"; //Python - ``` +类与类加载器的关系: +* 在JVM中表示两个class对象是否为同一个类存在的两个必要条件: + - 类的完整类名必须一致,包括包名 + - 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同 +* 这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true +类加载器作用:加载字节码到JVM内存,得到Class类的对象 +从 Java 虚拟机规范来讲,只存在以下两种不同的类加载器: + +- 启动类加载器(Bootstrap ClassLoader):使用 C++ 实现,是虚拟机自身的一部分 +- 自定义类加载器(User-Defined ClassLoader):Java虚拟机规范**将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器**,使用 Java语言 实现,独立于虚拟机 + +从 Java 开发人员的角度看: + +* **启动类加载器(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 -* invokestatic:调用静态方法,解析阶段确定唯一方法版本 -* invokespecial:调用方法、私有及父类方法,解析阶段确定唯一方法版本 -* invokevirtual:调用所有虚方法 -* invokeinterface:调用接口方法 + //对于用户自定义类来说:使用系统类加载器进行加载 + 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 -* invokedynamic:动态解析出需要调用的方法, - * Java7为了实现动态类型语言支持而引入了该指令,但是并没有提供直接生成invokedynamic指令的方法,需要借助ASM这种底层字节码工具来产生invokedynamic指令 - * Java8的Lambda表达式的出现,invokedynamic指令在Java中才有了直接生成方式 +} +``` -指令总结: -* 前四条指令固化在虚拟机内部,方法的调用执行不可干预,而invokedynamic指令则支持用户确定方法 -* invokestatic指令和invokespecial指令调用的方法称为非虚方法,属于静态绑定 -* 普通成员方法是由 invokevirtual 调用,属于**动态绑定**,即支持多态 +*** -```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 -``` +#### 抽象类 -* new 是在堆中创建对象,执行成功会将**对象引用**压入操作数栈 +ClassLoader类,是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器) -* dup 是复制操作数栈栈顶的内容,本例即为**对象引用**,为什么需要两份引用呢? +获取ClassLoader的途径: - * 一个要配合 invokespecial 调用该对象的构造方法 :()V (会消耗掉栈顶一个引用) +* 获取当前类的ClassLoader:`clazz.getClassLoader()` +* 获取当前线程上下文的ClassLoader:`Thread.currentThread.getContextClassLoader()` +* 获取系统的ClassLoader:`ClassLoader.getSystemClassLoader()` +* 获取调用者的ClassLoader:`DriverManager.getCallerClassLoader()` - * 一个要配合 astore_1 赋值给局部变量 +ClassLoader类常用方法: -* `d.test4()` 是通过**对象引用**调用一个静态方法,在调用invokestatic 之前执行了 pop 指令,把对象引用从操作数栈弹掉 - * 不建议使用`对象.静态方法()`的方式调用静态方法,多了aload和pop指令 - * 成员方法与静态方法调用的区别是:执行方法前是否需要对象引用 +| 方法 | 说明 | +| ----------------------------------------------------- | ------------------------------------------------------------ | +| getParent() | 返回该类加载器的超类加载器 | +| loadClass(String name) | 加载名称为name的类,返回结果为java.lang.Class类的实例 | +| findClass(String name) | 查找名称为name的类,返回结果为java.lang.Class类的实例 | +| findLoadedClass(String name) | 查找名称为name的已经被加载过的类,返回结果为java.lang.Class类的实例 | +| defineClass(String name, byte[] b, int off,int len) | 把字节数组b中的内容转换为一个Java类 ,返回结果为java.lang.Class类的实例 | +| resolveClass(Class c) | 连接指定的一个java类 | @@ -13472,274 +13527,222 @@ public class Demo { -#### 多态原理 +#### 加载模型 -执行 **invokevirtual** 指令: +##### 三种模型 -1. 先通过栈帧中的对象引用找到对象 -2. 分析对象头,找到对象的实际 Class -3. Class 结构中有 vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了 -4. 查表得到方法的具体地址 -5. 执行方法的字节码 +在JVM中,对于类加载模型提供了三种,分别为全盘加载、双亲委派、缓存机制 -**理解多态**: +- **全盘加载:**当一个类加载器负责加载某个Class时,该Class所依赖和引用的其他Class也将由该类加载器负责载入,除非显示指定使用另外一个类加载器来载入 -* 多态有编译时多态和运行时多态,即静态多态和动态多态 -* 前者是通过方法重载实现,后者是通过方法覆盖实现(子类覆盖父类方法,虚方法表) -* 虚方法:运行时动态绑定的方法 +- **双亲委派:**先让父类加载器加载该Class,在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。简单来说就是,某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,**依次递归**,如果父加载器可以完成类加载任务,就成功返回;只有当父加载器无法完成此加载任务时,才自己去加载 -虚方法表:在面向对象编程中,会频繁使用到**动态分派**,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适目标就会影响到执行效率。为了提高性能,JVM采用在类的方法区建立一个虚方法表(virtual method table)来实现,使用索引表来代替查找,每个类中都有一个虚方法表,表中存放着各个方法的实际入口 +- **缓存机制:**会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象存入缓冲区中 + - 这就是修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-方法调用动态分配.png) +*** -*** +##### 双亲委派 +双亲委派模型 (Parents Delegation Model):该模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器,这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance) + -#### 异常处理 +工作过程:一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载 -##### try-catch +双亲委派机制的优点: -```java -public static void main(String[] args) { - int i = 0; - try { - i = 10; - } catch (ArithmeticException e) { - i = 30; - } catch (NullPointerException e) { - i = 40; - } catch (Exception e) { - i = 50; - } -} -``` +* 可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性 + +* Java类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一 + +* 保护程序安全,防止类库的核心API被随意篡改 + + 例如:在工程中新建java.lang包,接着在该包下新建String类,并定义main函数 + + ```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 classLoader)得到加载,启动类加载器优先级更高,在核心jre库中有其相同名字的类文件,但该类中并没有main方法 + +**源码分析:** ```java -public static void main(java.lang.String[]); - descriptor: ([Ljava/lang/String;)V - flags: ACC_PUBLIC, ACC_STATIC - Code: - stack=1, locals=3, args_size=1 - 0: iconst_0 - 1: istore_1 - 2: bipush 10 - 4: istore_1 - 5: goto 26 - 8: astore_2 - 9: bipush 30 - 11: istore_1 - 12: goto 26 - 15: astore_2 - 16: bipush 40 - 18: istore_1 - 19: goto 26 - 22: astore_2 - 23: bipush 50 - 25: istore_1 - 26: return - Exception table: - from to target type - 2 5 8 Class java/lang/Exception - 2 5 15 Class java/lang/NullPointerException - 2 5 22 Class java/lang/Exception - LineNumberTable: ... - LocalVariableTable: - Start Length Slot Name Signature - 9 3 2 e Ljava/lang/ArithmeticException; - 16 3 2 e Ljava/lang/NullPointerException; - 23 3 2 e Ljava/lang/Exception; - 0 27 0 args [Ljava/lang/String; - 2 25 1 i I - StackMapTable: ... - MethodParameters: ... +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) { + // ClassNotFoundException thrown if class not found + // from the non-null parent class loader + } + + if (c == null) { + // 如果调用父类的类加载器无法对类进行加载,则用自己的findClass(name)方法进行加载 + 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) { + resolveClass(c); + } + return c; + } } ``` -* 多出一个 **Exception table** 的结构,[from, to) 是前闭后开的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号 -* 8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置 -* 因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用 +*** -##### finally -finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程 -```java -public static void main(String[] args) { - int i = 0; - try { - i = 10; - } catch (Exception e) { - i = 20; - } finally { - i = 30; - } -} -``` +#### 沙箱机制 -```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: - 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 -``` +沙箱机制:将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,来保证对代码的有效隔离,防止对本地系统造成破坏 +沙箱**限制系统资源访问**,包括CPU、内存、文件系统、网络,不同级别的沙箱对资源访问的限制也不一样 +举例:自定义String类,但是在加载自定义String类的时候会先使用引导类加载器加载,而引导类加载器在加载过程中会先加载jdk自带的文件(rt.jar包中的java\lang\String.class),报错信息说没有main方法就是因为加载的是rt.jar包中的String类,这样可以保证对java核心源代码的保护 -##### return -###### 吞异常 -```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 -> slot 1 存入局部变量表的 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,会**吞掉异常** +#### 自定义 + +对于自定义类加载器的实现,只需要继承ClassLoader类,覆写findClass方法即可 +作用:隔离加载类、修改类加载的方式、拓展加载源、防止源码泄漏 -###### 不吞异常 +java.lang.ClassLoader 的 loadClass() 实现了双亲委派模型的逻辑,自定义类加载器一般不去重写它,但是需要重写 findClass() 方法 ```java -public class Demo3_12_2 { - public static void main(String[] args) { - int result = test(); - System.out.println(result);//10 - } - public static int test() { - int i = 10; +//自定义类加载器,读取指定的类路径classPath下的class文件 +public class MyClassLoader extends ClassLoader{ + private String classPath; + + public MyClassLoader(String classPath) { + this.classPath = classPath; + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + byte[] data = new byte[0]; try { - return i;//返回10 - } finally { - i = 20; + data = loadByte(name); + } catch (Exception e) { + e.printStackTrace(); } - } + return defineClass(name, data, 0, data.length); + } + + private byte[] loadByte(String name) throws Exception { + name = name.replaceAll("\\.", "/"); + FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class"); + int len = fis.available(); + byte[] data = new byte[len]; + fis.read(data); + fis.close(); + return data; + } } ``` ```java - 0: bipush 10 // 10 放入栈顶 - 2: istore_0 // 10 -> i,赋值给i,放入slot 0 - 3: iload_0 // i(10)加载至操作数栈 - 4: istore_1 // 10 -> slot 1,暂存至 slot 1,目的是为了固定返回值 - 5: bipush 20 // 20 放入栈顶 - 7: istore_0 // 20 -> i - 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 +public class ClassLoaderTest { + public static void main(String[] args) throws Exception { + MyClassLoader classLoader = new MyClassLoader("D:\\workspace\\project\\src\\main\\java"); + Class clazz = classLoader.loadClass("com.demo.User"); + System.out.println(clazz.getClassLoader().getClass().getName()); + } +} +//当存在.java文件时,根据双亲委派机制,显示当前类加载器为AppClassLoader +//当将.java文件删除时,则显示使用的是自定义的类加载器 ``` + + *** -### 编译优化 +## 运行机制 -#### 语法糖 +### 执行过程 -语法糖:指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担 + Java文件编译执行的过程: +![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java文件编译执行的过程.png) +- 类加载器:用于装载字节码文件(.class文件) +- 运行时数据区:用于分配存储空间 +- 执行引擎:执行字节码文件或本地方法 +- 垃圾回收器:用于对JVM中的垃圾内容进行回收 -#### 默认构造器 -```java -public class Candy1 { -} -``` -```java -public class Candy1 { - // 这个无参构造是编译器帮助我们加上的 - public Candy1() { - super(); // 即调用父类 Object 的无参构造方法,即调用 java/lang/Object." - ":()V - } -} -``` +*** -*** +### 执行引擎 +#### 基本介绍 +执行引擎:Java虚拟机的核心组成部分之一,JVM的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,需要执行引擎将字节码指令解释/编译为对应平台上的本地机器指令 -#### 自动拆装箱 +虚拟机是一个相对于“物理机”的概念,这两种机器都有代码执行能力: -```java -Integer x = 1; -int y = x; -``` +* 物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上 +* 虚拟机的执行引擎是由软件自行实现的,可以不受物理条件制约地定制指令集与执行引擎的结构体系 -这段代码在 JDK 5 之前是无法编译通过的,必须改写为代码片段2: +编译过程中的编译器: -```java -Integer x = Integer.valueOf(1); -int y = x.intValue(); -``` +* 前端编译器: Sun的Javac、 Eclipse JDT中的增量式编译器ECJ,把源代码编译为字节码文件.class +* 后端运行期编译器:HotSpot VM的C1、C2编译器,也就是JIT编译器 +* 静态提前编译器:AOT编译器,直接把源代码编译成本地机器代码 -JDK5以后编译阶段自动转换成上述片段 +Java是**半编译半解释型语言**,将解释执行与编译执行二者结合起来进行: + +* 解释器:根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行 +* 即时编译器(JIT : Just In Time Compiler):虚拟机运行时将源代码直接编译成**和本地机器平台相关的机器码**后再执行,并存入Code Cache,下次遇到相同的代码直接执行,效率高(一次编译,到处运行) @@ -13747,30 +13750,22 @@ JDK5以后编译阶段自动转换成上述片段 -#### 泛型集合 +#### 执行方式 -泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行**泛型擦除**的动作,即泛型信息 -在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理: +HotSpot VM 采用**解释器与即时编译器并存的架构**,解释器和即时编译器能够相互协作,去选择最合适的方式来权衡编译本地代码和直接解释执行代码的时间 -```java -List list = new ArrayList<>(); -list.add(10); // 实际调用的是 List.add(Object e) -Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index); -``` +HostSpot JVM的默认执行方式: -编译器真正生成的字节码中,还要额外做一个类型转换的操作: +* 当程序启动后,解释器可以马上发挥作用立即执行,省去编译器编译的时间(解释器存在的**必要性**) +* 随着程序运行时间的推移,即时编译器逐渐发挥作用,根据热点探测功能,将有价值的字节码编译为本地机器指令,以换取更高的程序执行效率 -```java -// 需要将 Object 转为 Integer -Integer x = (Integer)list.get(0); -``` +HotSpot VM 可以通过VM参数设置程序执行方式: -如果前面的 x 变量类型修改为 int 基本类型那么最终生成的字节码是: +- -Xint:完全采用解释器模式执行程序 +- -Xcomp:完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行 +- -Xmixed:采用解释器+即时编译器的混合模式共同执行程序 -```java -// 需要将 Object 转为 Integer, 并执行拆箱操作 -int x = ((Integer)list.get(0)).intValue(); -``` +![](https://gitee.com/seazean/images/raw/master/Java/JVM-执行引擎工作流程.png) @@ -13778,188 +13773,97 @@ int x = ((Integer)list.get(0)).intValue(); -#### 可变参数 +#### 热点探测 -```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"); - } -} -``` +热点代码:被JIT编译器编译的字节码,根据代码被调用执行的频率而定 -可变参数`String... args`其实是`String[] args` , java 编译器会在编译期间将上述代码变换为: +* 一个被多次调用的方法或者一个循环次数较多的循环体都可以被称之为热点代码 +* 这种编译方式发生在方法的执行过程中,也称为栈上替换,简称OSR (On StackReplacement) 编译 -```java -public static void main(String[] args) { - foo(new String[]{"hello", "world"}); -} -``` +热点探测:JIT编译器在运行时会针热点代码做出深度优化,将其直接编译为对应平台的本地机器指令,以提升Java程序的执行性能 -注意:如果调用了foo()则等价代码为`foo(new String[]{})` ,创建了一个空的数组,而不会传递 null 进去 +HotSpot VM采用的热点探测方式是基于计数器的热点探测,为每一个方法都建立2个不同类型的计数器:方法调用计数器 (Invocation Counter) 和回边计数器 (BackEdge Counter) +* 方法调用计数器:用于统计方法被调用的次数,默认阈值在Client 模式 下是1500 次,在Server 模式下是10000 次,超过这个阈值,就会触发JIT编译,阈值可以通过虚拟机参数 `-XX:CompileThreshold` 设置 + 工作流程:当一个方法被调用时, 会先检查该方法是否存在被JIT编译过的版本,存在则使用编译后的本地代码来执行;如果不存在则将此方法的调用计数器值加1,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阈值,如果超过阈值会向即时编译器提交一个该方法的代码编译请求 -**** +* 回边计数器:统计一个方法中循环体代码执行的次数,在字节码中控制流向后跳转的指令称为回边 -#### foreach +*** -**数组的循环:** -```java -int[] array = {1, 2, 3, 4, 5}; // 数组赋初值的简化写法也是语法糖哦 -for (int e : array) { - System.out.println(e); -} -``` -编译后: +#### 分层编译 -```java -for(int i = 0; i < array.length; ++i) { - int e = array[i]; - System.out.println(e); -} -``` +在HotSpot VM中内嵌有两个JIT编译器,分别为Client Compiler和Server Compiler,简称为C1编译器和C2编译器 -**集合的循环:** +C1编译器会对字节码进行简单可靠的优化,耗时短,以达到更快的编译速度。C1编译器的优化方法: -```java -List list = Arrays.asList(1,2,3,4,5); -for (Integer i : list) { - System.out.println(i); -} -``` +* 方法内联:将引用的函数代码编译到引用点处,这样可以减少栈帧的生成,减少参数传递以及跳转过程 -编译后转换为对迭代器的调用: + ```java + private static int square(final int i) { + return i * i; + } + System.out.println(square(9)); + ``` -```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); -} -``` + square 是热点方法,会进行内联,把方法内代码拷贝粘贴到调用者的位置: -注意:foreach 循环写法,能够配合数组以及所有实现了 Iterable 接口的集合类一起使用,其中 Iterable 用来获取集合的迭代器( Iterator ) + ```java + System.out.println(9 * 9); + ``` + 还能够进行常量折叠(constant folding)的优化: + ```java + System.out.println(81); + ``` -*** +* 去虚拟化:对唯一的实现类进行内联 +* 冗余消除:在运行期间把一些不会执行的代码折叠掉 +C2编译器进行耗时较长的优化以及激进优化,优化的代码执行效率更高。C2的优化主要是在全局层面,逃逸分析是优化的基础:标量替换、栈上分配、同步消除 -#### switch +参数设置: -##### 字符串 +- -client:指定Java虚拟机运行在Client模式下,并使用C1编译器 +- -server:指定Java虚拟机运行在Server模式下,并使用C2编译器 -从 JDK 开始,switch 可以作用于字符串和枚举类: +分层编译策略 (Tiered Compilation):程序解释执行可以触发C1编译,将字节码编译成机器码。加上性能监控,C2编译会根据性能监控信息进行激进优化,JVM 将执行状态分成了 5 个层次: -```java -switch (str) { - case "hello": { - System.out.println("h"); - break; - } - case "world": { - System.out.println("w"); - break; - } -} -``` +* 0 层,解释执行(Interpreter) -注意:switch 配合 String 和枚举使用时,变量不能为null +* 1 层,使用 C1 即时编译器编译执行(不带 profiling) -会被编译器转换为: +* 2 层,使用 C1 即时编译器编译执行(带基本的 profiling) -```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; -} -``` +* 3 层,使用 C1 即时编译器编译执行(带完全的 profiling) -总结: +* 4 层,使用 C2 即时编译器编译执行 -* 执行了两遍 switch,第一遍是根据字符串的 hashCode 和 equals 将字符串的转换为相应byte 类型,第二遍才是利用 byte 执行进行比较 -* hashCode 是为了提高效率,减少可能的比较;而 equals 是为了防止 hashCode 冲突 + 说明:profiling 是指在运行过程中收集一些程序执行状态的数据,例如方法的调用次数,循环的回边次数等 +*** -##### 枚举 -switch 枚举的例子,原始代码: +#### 其他编译 -```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; - } - } -} -``` +Graal编译器:JDK10 HotSpot 加入的一个全新的即时编译器,编译效果短短几年时间就追评了C2编译器 -编译转换后的代码: +AOT编译器:JDK9引入,静态提前编译器 (Ahead Of Time Compiler),程序运行之前便将字节码转换为机器码的过程,是与即时编译相对立的一个概念,即时编译指的是在程序的运行过程中,将字节码转换为机器码 -```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; - } -} -``` +* 优点:Java虚拟机加载已经预编译成二进制库,可以直接执行,不必等待即时编译器的预热,减少Java应用第一次运行慢的现象 +* 缺点: + * 破坏了java"一次编译,到处运行”,必须为每个不同硬件、SS编译对应的发行包 + * 降低了Java链接过程的动态性,加载的代码在编译期就必须全部已知 @@ -13967,97 +13871,32 @@ public static void foo(Sex sex) { -#### 枚举类 - -JDK 7 新增了枚举类: - -```java -enum Sex { - MALE, FEMALE -} -``` +#### 语言发展 -编译转换后: +机器码:各种用二进制编码方式表示的指令,与CPU紧密相关,所以不同种类的CPU对应的机器指令不同 -```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); - } -} -``` +指令:指令就是把机器码中特定的0和1序列,简化成对应的指令,例如mov,inc等,可读性稍好,但是不同的硬件平台的同一种指令(比如mov),对应的机器码也可能不同 +指令集:不同的硬件平台支持的指令是有区别的,每个平台所支持的指令,称之为对应平台的指令集 +- x86指令集,对应的是x86架构的平台 +- ARM指令集,对应的是ARM架构的平台 -#### 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)`:添加被压制异常,是为了防止异常信息的丢失(try-with-resources 生成的 fianlly 中如果抛出了异常) -```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(); -} -``` @@ -14065,126 +13904,116 @@ try { -#### 方法重写 +### 字节码 -方法重写时对返回值分两种情况: +#### 类结构 -* 父子类的返回值完全一致 -* 子类返回值可以是父类返回值的子类(比较绕口,见下面的例子) +class文件是编译器编译之后供虚拟机解释执行的二进制字节码文件,一个class文件对应一个public类型的类 + +根据 JVM 规范,类文件结构如下: ```java -class A { - public Number m() { - return 1; - } -} -class B extends A { - @Override - // 子类m方法的返回值是Integer是父类m方法返回值Number的子类 - public Integer m() { - return 2; - } +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]; } ``` -对于子类,java 编译器会做如下处理: +HelloWorld.java 执行 `javac -parameters -d . HellowWorld.java`指令: -```java -class B extends A { - public Integer m() { - return 2; - } - // 此方法才是真正重写了父类 public Number m() 方法 - public synthetic bridge Number m() { - // 调用 public Integer m() - return m(); - } -} +```sh +0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09 +0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07 +0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29 //... ``` -其中桥接方法比较特殊,仅对 java 虚拟机可见,并且与原来的 public Integer m() 没有命名冲突 +魔数:0~3 字节,表示是否是Classs类型的文件,ca fe ba be代表Java +版本:4~7 字节,表示类的版本 00 34(52) 表示是 Java 8 +常量池:8~9 字节,表示常量池长度,00 23 (35) 表示常量池有 #1~#34项,注意 #0 项不计入,也没有值 -*** +* 第#1项 0a 表示一个 Method 信息,00 06 和 00 15(21) 表示它引用了常量池中 #6 和 #21 项来获得 + 这个方法的【所属类】和【方法名】 +* 第#2项 09 表示一个 Field 信息,00 16(22)和 00 17(23) 表示它引用了常量池中 #22 和 # 23 项 + 来获得这个成员变量的【所属类】和【成员变量名】 +* 第#n项 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-类结构常量池.png) +```sh +0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01 +``` -#### 匿名内部类 +访问标识与继承信息: -源代码: +* 00 21 表示该 class 是一个类,公共的 +* 00 05 表示根据常量池中 #5 找到本类全限定名 +* 00 06 表示根据常量池中 #6 找到父类全限定名 +* 00 00 表示接口的数量,本类为 0 -```java -public class Candy11 { - public static void main(String[] args) { - Runnable runnable = new Runnable() { - @Override - public void run() { - System.out.println("ok"); - } - }; - } -} -``` +Field信息:表示成员变量数量,00 00表示本类为 0 -转化后代码: +![](https://gitee.com/seazean/images/raw/master/Java/JVM-类结构Field信息.png) -```java -// 额外生成的类 -final class Candy11$1 implements Runnable { - Candy11$1() { - } - public void run() { - System.out.println("ok"); - } -} -``` +Method信息:表示方法数量,00 02 本类为 2,一个方法由 访问修饰符,名称,参数描述,方法属性数量,方法属性组成 -```java -public class Candy11 { - public static void main(String[] args) { - Runnable runnable = new Candy11$1(); - } -} +附加属性: + +* 00 01 表示附加属性数量 +* 00 13 表示引用了常量池 #19 项,即【SourceFile】 +* 00 00 00 02 表示此属性的长度 +* 00 14 表示引用了常量池 #20 项,即【HelloWorld.java】 + +```sh +0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00 +0001120 00 00 02 00 14 ``` -引用局部变量的匿名内部类,源代码: -```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 class Candy11 { - public static void test(final int x) { - Runnable runnable = new Candy11$1(x); - } -} -``` -局部变量必须是 final 的:因为在创建Candy11¥1 对象时,将 x 的值赋值给了 Candy11$1 对象的 val属性,x不应该再发生变化了,因为发生变化,this.val​x属性没有机会再跟着变化 + +#### javap + +`javap -v HelloWorld.class`:反编译 class 文件 + +slot:局部变量表中最基本的存储单元 + +aload:从局部变量表的相应位置装载一个对象引用到操作数栈的栈顶 + +* aload_0把this装载到了操作数栈中 +* aload_0是一组格式为aload_的操作码中的一个,这一组操作码把对象的引用装载到操作数栈中标志了待处理的局部变量表中的位置 + +`iload_,lload_,fload_,dload_`:i代表int型,l代表long型,f代表float型以及d代表double型 + +* 在局部变量表中的索引位置大于3的变量的装载可以使用iload、lload、fload,、dload和aload +* 这些操作码都需要一个操作数的参数,用于确认需要装载的局部变量的位置 + +Ljava.lang.String: + +* `[`:表示一维数组 +* `[[`:表示二维数组 +* `L`:表示一个对象 +* `java.lang.String`:表示对象的类型 @@ -14192,151 +14021,166 @@ public class Candy11 { -### 对象创建 +#### 执行流程 -#### 创建时机 +原始Java代码: -一个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); + } +} +``` -类在第一次实例化加载一次,后续实例化不再加载,引用第一次加载的类 +javap -v Demo.class:省略 -Java对象创建时机: +* 常量池载入运行时常量池 -1. 使用new关键字创建对象:由执行类实例创建表达式而引起的对象创建 +* 方法区字节码载入方法区 -2. 使用Class类的newInstance方法 (反射机制) +* main 线程开始运行,分配栈帧内存:(操作数栈stack=2,局部变量表locals=4) -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); - } - } - ``` + `bipush 10`:将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令 - 使用newInstance方法的这两种方式创建对象使用的就是Java的反射机制,事实上Class的newInstance方法内部调用的也是Constructor的newInstance方法 + * sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节) + * ldc 将一个 int 压入操作数栈 + * ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节) + * 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池 -4. 使用Clone方法创建对象:用clone方法创建对象的过程中并不会调用任何构造函数,要想使用clone方法,我们就必须先实现Cloneable接口并实现其定义的clone方法 -5. 使用(反)序列化机制创建对象:当我们反序列化一个对象时,JVM会给我们创建一个单独的对象,在此过程中,JVM并不会调用任何构造函数。为了反序列化一个对象,需要让类实现Serializable接口 + `istore_1`:将操作数栈顶数据弹出,存入局部变量表的 slot 1 -从Java虚拟机层面看,除了使用new关键字创建对象的方式外,其他方式全部都是通过转变为invokevirtual指令直接创建对象的 + ![](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) -*** + `istore_2`:将操作数栈顶数据弹出,存入局部变量表的 slot 2 + `iload_1`:将局部变量表的 slot 1数据弹出,放入操作数栈栈顶 + `iload_2`:将局部变量表的 slot 2数据弹出,放入操作数栈栈顶 -#### 实例化 + `iadd`:执行相加操作 -创建对象的过程 + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程3.png) -1. 判断对象对应的类是否加载、链接、初始化 -2. 为对象分配内存 -3. 处理并发安全问题 -4. 初始化分配到的空间一所有属性设置默认值,保证对象实例字段在不赋值时可以直接使用 -5. 设置对象的对象头 -6. 执行init方法进行实例化 + `istore_3`:将操作数栈顶数据弹出,存入局部变量表的 slot 3 -当一个对象被创建时,虚拟机就会为其分配内存来存放对象的实例变量及其从父类继承过来的实例变量(即使这些从超类继承过来的实例变量有可能被隐藏也会被分配空间),同时这些实例变量也会被赋予默认值(零值),然后开始进行对象初始化:**实例变量初始化、实例代码块初始化 、构造函数初始化** + `getstatic #4`:获取静态字段 -1. 实例变量初始化与实例代码块初始化 + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程4.png) - 对实例变量直接赋值或者使用实例代码块赋值,编译器会将其中的代码放到类的构造函数中去,并且这些代码会被放在对超类构造函数的调用语句之后 (Java要求构造函数的第一条语句必须是超类构造函数的调用语句),构造函数本身的代码之前 + `iload_3`: -2. 构造函数初始化 + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程5.png) - **Java要求在实例化类之前,必须先实例化其超类,以保证所创建实例的完整性**,在准备实例化一个类的对象前,首先准备实例化该类的父类,如果该类的父类还有父类,那么准备实例化该类的父类的父类,依次递归直到递归到Object类。然后从Object类依次对以下各类进行实例化,初始化父类中的变量和执行构造函数 + `invokevirtual #5`: + * 找到常量池 #5 项 + * 定位到方法区 java/io/PrintStream.println:(I)V 方法 + * **生成新的栈帧**(分配 locals、stack等) + * 传递参数,执行新栈帧中的字节码 + * 执行完毕,弹出栈帧 + * 清除 main 操作数栈内容 + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程6.png) -*** + return:完成 main 方法调用,弹出 main 栈帧,程序结束 + +*** -#### 承上启下 -1. 一个实例变量在对象初始化的过程中会被赋值几次? - JVM在为一个对象分配完内存之后,会给每一个实例变量赋予默认值,这个实例变量被第一次赋值 - 在声明实例变量的同时对其进行了赋值操作,那么这个实例变量就被第二次赋值 - 在实例代码块中又对变量做了初始化操作,那么这个实例变量就被第三次赋值 - 在构造函数中也对变量做了初始化操作,那么这个实例变量就被第四次赋值 - 在Java的对象初始化过程中,一个实例变量最多可以被初始化4次 +#### 条件判断 -2. 类的初始化过程与类的实例化过程的异同? +```java +public static void main(String[] args) { + int a = 0; + if(a == 0) { + a = 10; + } else { + a = 20; + } +} +``` - 类的初始化是指类加载过程中的初始化阶段对类变量按照代码进行赋值的过程 - 类的实例化是指在类完全加载到内存中后创建对象的过程(类的实例化触发了类的初始化) +说明: -3. 假如一个类还未加载到内存中,那么在创建一个该类的实例时,具体过程是怎样的?(**经典案例**) +* byte,short,char 都会按 int 比较,因为操作数栈都是 4 字节 +* goto 用来进行跳转到指定行号的字节码 - ```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 - *///:~ - ``` +```sh +1: istore_1 +2: iload_1 +3: ifne 12 +6: bipush 10 +8: istore_1 +9: goto 15 +12: bipush 20 +14: istore_1 +15: return +``` - `static StaticTest st = new StaticTest();`: + - * 实例初始化不一定要在类初始化结束之后才开始 - * 在同一个类加载器下,一个类型只会被初始化一次。所以一旦开始初始化一个类,无论是否完成后续都不会再重新触发该类型的初始化阶段了(只考虑在同一个类加载器下的情形)。因此,在实例化上述程序中的st变量时,**实际上是把实例初始化嵌入到了静态初始化流程中,并且在上面的程序中,嵌入到了静态初始化的起始位置**,这就导致了实例初始化完全发生在静态初始化之前,这也是导致a为110 b为0的原因 - 代码等价于: +*** - ```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 - } - } - ``` - + +#### 循环控制 + +iinc 指令:是直接在局部变量 slot 上进行运算 + +while循环: + +```java +public static void main(String[] args) { + int a = 0; + while (a < 10) { + a++; + } +} +``` + +```sh +0: iconst_0 +1: istore_1 +2: iload_1 +3: bipush 10 +5: if_icmpge 14 +8: iinc 1, 1 +11: goto 2 +14: return +``` + +for循环: + +```java +for (int i = 0; i < 10; i++) { } +``` + +```sh +0: iconst_0 +1: istore_1 +2: iload_1 +3: bipush 10 +5: if_icmpge 14 +8: iinc 1, 1 +11: goto 2 +14: return +``` @@ -14344,228 +14188,431 @@ Java对象创建时机: -### 加载机制 +#### 面试题 -#### 加载过程 +##### 分析i++ -##### 生命周期 +从字节码角度分析:a++ 和 ++a 的区别是先执行 iload 还是 先执行 iinc -类是在运行期间**第一次使用时动态加载**的(不使用不加载),而不是一次性加载所有类,因为一次性加载会占用很多的内存 +```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 + } +} +``` -![](https://gitee.com/seazean/images/raw/master/Java/JVM-类的生命周期.png) -包括 7 个阶段: -* 加载(Loading) -* 链接:验证(Verification)、准备(Preparation)、解析(Resolution) -* 初始化(Initialization) -* 使用(Using) -* 卸载(Unloading) +##### 判断结果 -类加载方式: +```java +public class Demo3_6_1 { + public static void main(String[] args) { + int i = 0; + int x = 0; + while (i < 10) { + x = x++; + i++; + } + System.out.println(x); // 结果是 0 + } +} +``` -* 隐式加载: - * 创建类对象、使用类的静态域、创建子类对象、使用子类的静态域 - * 在JVM启动时,通过三大类加载器加载class -* 显式加载: - * ClassLoader.loadClass(className),只加载和连接,**不会进行初始化** - * Class.forName(String name, boolean initialize,ClassLoader loader),使用loader进行加载和连接,根据参数initialize决定是否初始化 +*** + + + +#### 异常处理 + +##### try-catch + +```java +public static void main(String[] args) { + int i = 0; + try { + i = 10; + } catch (ArithmeticException e) { + i = 30; + } catch (NullPointerException e) { + i = 40; + } catch (Exception e) { + i = 50; + } +} +``` + +```java +public static void main(java.lang.String[]); + descriptor: ([Ljava/lang/String;)V + flags: ACC_PUBLIC, ACC_STATIC + Code: + stack=1, locals=3, args_size=1 + 0: iconst_0 + 1: istore_1 + 2: bipush 10 + 4: istore_1 + 5: goto 26 + 8: astore_2 + 9: bipush 30 + 11: istore_1 + 12: goto 26 + 15: astore_2 + 16: bipush 40 + 18: istore_1 + 19: goto 26 + 22: astore_2 + 23: bipush 50 + 25: istore_1 + 26: return + Exception table: + from to target type + 2 5 8 Class java/lang/Exception + 2 5 15 Class java/lang/NullPointerException + 2 5 22 Class java/lang/Exception + LineNumberTable: ... + LocalVariableTable: + Start Length Slot Name Signature + 9 3 2 e Ljava/lang/ArithmeticException; + 16 3 2 e Ljava/lang/NullPointerException; + 23 3 2 e Ljava/lang/Exception; + 0 27 0 args [Ljava/lang/String; + 2 25 1 i I + StackMapTable: ... + MethodParameters: ... +} +``` + +* 多出一个 **Exception table** 的结构,[from, to) 是前闭后开的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号 +* 8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置 +* 因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用 + + + +##### finally + +finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程 + +```java +public static void main(String[] args) { + int i = 0; + try { + i = 10; + } catch (Exception e) { + i = 20; + } finally { + i = 30; + } +} +``` + +```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: + 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 +``` + -*** +##### return +###### 吞异常 -##### 加载 +```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 -> slot 1 存入局部变量表的 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,会**吞掉异常** -- 通过类的完全限定名称获取定义该类的二进制字节流 -- 将该字节流表示的静态存储结构转换为方法区的运行时存储结构 -- 在内存中生成一个代表该类的 Class 对象,作为方法区中该类各种数据的访问入口 -其中二进制字节流可以从以下方式中获取: -- 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础 -- 从网络中获取,最典型的应用是 Applet -- 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass -- 由其他文件生成,例如由 JSP 文件生成对应的 Class 类 +###### 不吞异常 -将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field: +```java +public class Demo3_12_2 { + 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_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 class 暴露给 java 使用 -* _super 即父类、_fields 即成员变量、_methods 即方法、_constants 即常量池、_class_loader 即类加载器、_vtable 虚方法表、_itable 接口方法表 +```java + 0: bipush 10 // 10 放入栈顶 + 2: istore_0 // 10 -> i,赋值给i,放入slot 0 + 3: iload_0 // i(10)加载至操作数栈 + 4: istore_1 // 10 -> slot 1,暂存至 slot 1,目的是为了固定返回值 + 5: bipush 20 // 20 放入栈顶 + 7: istore_0 // 20 -> i + 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 +``` -注意: -* 如果这个类还有父类没有加载,先加载父类 -* 加载和链接可能是交替运行的 -* instanceKlass和_java_mirror相互持有对方的地址,堆中对象通过instanceKlass和元空间进行交互 - +*** -*** +### 方法调用 +#### 调用机制 +在JVM中,将符号引用转换为直接引用有两种机制: -##### 验证 +* 静态链接:当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变,将调用方法的符号引用转换为直接引用的过程称之为静态链接 +* 动态链接:如果被调用的方法在编译期无法被确定下来,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此被称为动态链接 -链接阶段包含验证、准备、解析阶段 +对应的方法的绑定机制为:静态绑定和动态绑定。绑定是一个字段、方法或者类从符号引用被替换为直接引用的过程,仅发生一次 -验证:确保 Class 文件的字节流中包含的信息是否符合 JVM规范,并且不会危害虚拟机自身的安全 +* 静态绑定:被调用的目标方法在编译期可知,且运行期保持不变,即可将这个方法与所属的类型进行绑定 +* 动态绑定:被调用的方法在编译期无法被确定,只能够在程序运行期根据实际的类型绑定相关的方法 +非虚方法: +* 方法在编译器就确定了具体的调用版本,这个版本在运行时是不可变的 +* 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法,其他方法称为虚方法 -##### 准备 +动态类型语言和静态类型语言: -准备阶段为**静态变量分配内存并设置初始值**,使用的是方法区的内存: +* 在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之则是动态类型语言 -* 类变量也叫静态变量,就是是被 static 修饰的变量 -* 实例变量也叫对象变量,即没加static 的变量 +* 静态语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息 -实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中。实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次 +* **Java是静态类型语言**(尽管lambda表达式为其增加了动态特性),js,python是动态类型语言 -**类变量初始化**: + ```java + String s = "abc"; //Java + info = "abc"; //Python + ``` -* static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾 -* 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; - ``` +普通调用指令: +* invokestatic:调用静态方法,解析阶段确定唯一方法版本 +* invokespecial:调用方法、私有及父类方法,解析阶段确定唯一方法版本 +* invokevirtual:调用所有虚方法 +* invokeinterface:调用接口方法 +动态调用指令: -##### 解析 +* invokedynamic:动态解析出需要调用的方法, + * Java7为了实现动态类型语言支持而引入了该指令,但是并没有提供直接生成invokedynamic指令的方法,需要借助ASM这种底层字节码工具来产生invokedynamic指令 + * Java8的Lambda表达式的出现,invokedynamic指令在Java中才有了直接生成方式 -将常量池中类、接口、字段、方法的符号引用替换为直接引用(内存地址)的过程 +指令总结: -* 符号引用:一组符号来描述目标,可以是任何字面量,属于编译原理方面的概念如:包括类和接口的全限名、字段的名称和描述符、方法的名称和描述符 -* 直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄 +* 前四条指令固化在虚拟机内部,方法的调用执行不可干预,而invokedynamic指令则支持用户确定方法 -其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。 +* invokestatic指令和invokespecial指令调用的方法称为非虚方法,属于静态绑定 +* 普通成员方法是由 invokevirtual 调用,属于**动态绑定**,即支持多态 ```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(); +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(); } } -class C { - D d = new D(); -} -class D { -} ``` +几种不同的方法调用对应的字节码指令: +```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 +``` -**** +* new 是在堆中创建对象,执行成功会将**对象引用**压入操作数栈 +* dup 是复制操作数栈栈顶的内容,本例即为**对象引用**,为什么需要两份引用呢? + * 一个要配合 invokespecial 调用该对象的构造方法 :()V (会消耗掉栈顶一个引用) -##### 初始化 + * 一个要配合 astore_1 赋值给局部变量 -###### 概述 +* `d.test4()` 是通过**对象引用**调用一个静态方法,在调用invokestatic 之前执行了 pop 指令,把对象引用从操作数栈弹掉 + * 不建议使用`对象.静态方法()`的方式调用静态方法,多了aload和pop指令 + * 成员方法与静态方法调用的区别是:执行方法前是否需要对象引用 -初始化阶段才真正开始执行类中定义的 Java 程序代码,在准备阶段,类变量已经赋过一次系统要求的初始值;在初始化阶段,通过程序制定的计划去初始化类变量和其它资源,执行 -在编译生成class文件时,编译器会产生两个方法加于class文件中,一个是类的初始化方法clinit, 另一个是实例的初始化方法init -类构造器()与实例构造器()不同,它不需要程序员进行显式调用,在一个类的生命周期中,类构造器()最多被虚拟机**调用一次**,而实例构造器()则会被虚拟机调用多次,只要程序员创建对象 +*** -类在第一次实例化加载一次,把class读入内存,后续实例化不再加载,引用第一次加载的类 +#### 多态原理 -###### clinit +执行 **invokevirtual** 指令: -():类构造器,由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的 +1. 先通过栈帧中的对象引用找到对象 +2. 分析对象头,找到对象的实际 Class +3. Class 结构中有 vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了 +4. 查表得到方法的具体地址 +5. 执行方法的字节码 -作用:是在类加载过程中的初始化阶段进行静态变量初始化和执行静态代码块 +**理解多态**: -* 如果类中没有静态变量或静态代码块,那么clinit方法将不会被生成 -* 在执行clinit方法时,必须先执行父类的clinit方法 -* clinit方法只执行一次 -* static变量的赋值操作和静态代码块的合并顺序由源文件中出现的顺序决定 +* 多态有编译时多态和运行时多态,即静态多态和动态多态 +* 前者是通过方法重载实现,后者是通过方法覆盖实现(子类覆盖父类方法,虚方法表) +* 虚方法:运行时动态绑定的方法 -**线程安全**问题: +虚方法表:在面向对象编程中,会频繁使用到**动态分派**,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适目标就会影响到执行效率。为了提高性能,JVM采用在类的方法区建立一个虚方法表(virtual method table)来实现,使用索引表来代替查找,每个类中都有一个虚方法表,表中存放着各个方法的实际入口 -* 虚拟机会保证一个类的 () 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 () 方法,其它线程都阻塞等待,直到活动线程执行 () 方法完毕 -* 如果在一个类的 () 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-方法调用动态分配.png) -特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问 -```java -public class Test { - static { - i = 0; // 给变量赋值可以正常编译通过 - System.out.print(i); // 这句编译器会提示“非法向前引用” - } - static int i = 1; -} -``` -补充: -接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 () 方法。但两者不同的是,执行接口的 () 方法不需要先执行父接口的 () 方法,只有当父接口中定义的变量使用时,父接口才会初始化;接口的实现类在初始化时也一样不会执行接口的 () 方法 +*** -###### 时机 -类的初始化是懒惰的,初始化时机: +### 代码优化 -**主动引用**:虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随之发生): +#### 语法糖 -* 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化,最常见的生成这 4 条指令的场景是: - * new:使用 new 关键字实例化对象时 - * getstatic:程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池) - * putstatic:程序给类的静态变量赋值 - * invokestatic :调用一个类的静态方法时 -* 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化 -* 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化 -* 当虚拟机启动时,需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类 -* MethodHandle和VarHandle可以看作是轻量级的反射调用机制,而要想使用这2个调用, 就必须先使用findStaticVarHandle来初始化要调用的类 -* 补充:当一个接口中定义了JDK8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化 +语法糖:指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担 + + + +#### 默认构造器 + +```java +public class Candy1 { +} +``` + +```java +public class Candy1 { + // 这个无参构造是编译器帮助我们加上的 + public Candy1() { + super(); // 即调用父类 Object 的无参构造方法,即调用 java/lang/Object." + ":()V + } +} +``` -**被动引用**:所有引用类的方式都不会触发初始化,称为被动引用 -* 通过子类引用父类的静态字段,不会导致子类初始化,只会触发父类的初始化 -* 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自Object 的子类,其中包含了数组的属性和方法 -* 常量(final修饰)在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化 +*** -###### init -init指的是实例构造器,主要作用是在类实例化过程中执行,执行内容包括成员变量初始化和代码块的执行 +#### 自动拆装箱 -实例化即调用 ()V ,虚拟机会保证这个类的构造方法的线程安全,先为实例变量分配内存空间,再执行赋默认值,然后根据源码中的顺序执行赋初值或代码块,没有成员变量初始化和代码块则不会执行 +```java +Integer x = 1; +int y = x; +``` -类实例化过程:**父类的类构造器() -> 子类的类构造器() -> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成员变量和实例代码块 -> 子类的构造函数** +这段代码在 JDK 5 之前是无法编译通过的,必须改写为代码片段2: + +```java +Integer x = Integer.valueOf(1); +int y = x.intValue(); +``` + +JDK5以后编译阶段自动转换成上述片段 @@ -14573,68 +14620,108 @@ init指的是实例构造器,主要作用是在类实例化过程中执行, -##### 卸载 +#### 泛型集合 -时机:执行了System.exit()方法,程序正常执行结束,程序在执行过程中遇到了异常或错误而异常终止,由于操作系统出现错误而导致Java虚拟机进程终止 +泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行**泛型擦除**的动作,即泛型信息 +在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理: -卸载类即该类的**Class对象被GC**,卸载类需要满足3个要求: +```java +List list = new ArrayList<>(); +list.add(10); // 实际调用的是 List.add(Object e) +Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index); +``` -1. 该类的所有的实例对象都已被GC,也就是说堆不存在该类的实例对象 -2. 该类没有在其他任何地方被引用 -3. 该类的类加载器的实例已被GC +编译器真正生成的字节码中,还要额外做一个类型转换的操作: -在JVM生命周期类,由jvm自带的类加载器加载的类是不会被卸载的,但是由我们自定义的类加载器加载的类是可能被卸载 +```java +// 需要将 Object 转为 Integer +Integer x = (Integer)list.get(0); +``` +如果前面的 x 变量类型修改为 int 基本类型那么最终生成的字节码是: +```java +// 需要将 Object 转为 Integer, 并执行拆箱操作 +int x = ((Integer)list.get(0)).intValue(); +``` -**** +*** -#### 类加载器 -类与类加载器: -* 两个类相等,需要类本身相等,并且使用同一个类加载器进行加载,因为每一个类加载器都拥有一个独立的类名称空间 -* 这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true +#### 可变参数 -类加载器作用:加载字节码到JVM内存,得到Class类的对象 +```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"); + } +} +``` -从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器: +可变参数`String... args`其实是`String[] args` , java 编译器会在编译期间将上述代码变换为: -- 启动类加载器(Bootstrap ClassLoader),使用 C++ 实现,是虚拟机自身的一部分; -- 所有其它类的加载器,使用 Java 实现,独立于虚拟机,继承自抽象类 java.lang.ClassLoade +```java +public static void main(String[] args) { + foo(new String[]{"hello", "world"}); +} +``` -从 Java 开发人员的角度看,类加载器可以划分得更细致一些: +注意:如果调用了foo()则等价代码为`foo(new String[]{})` ,创建了一个空的数组,而不会传递 null 进去 -* **启动类加载器(Bootstrap ClassLoader)**:此类加载器负责加载在`JAVA_HOME/jre/lib`目录中的,或者被 -Xbootclasspath 参数所指定的路径中的类,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可 -* **扩展类加载器(Extension ClassLoader)**:由ExtClassLoader (sun.misc.Launcher$ExtClassLoader) 实现,负责将`JAVA_HOME/jre/lib/ext`或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器,上级为 Bootstrap,显示为 null -* **应用程序类加载器(Application ClassLoader)**:AppClassLoader(sun.misc.Launcher$AppClassLoader)实现,负责加载用户类路径(ClassPath)上所指定的类库,上级为 Extension - * 这个类加载器是ClassLoader中的getSystemClassLoader() 方法的返回值,因此称为系统类加载器 - * 可以直接使用这个类加载器,如果应用程序中没有自定义类加载器,这个就是程序中默认的类加载器 -* 自定义类加载器:自定义加载目录的类,上级是Application +**** +#### foreach -*** +**数组的循环:** +```java +int[] array = {1, 2, 3, 4, 5}; // 数组赋初值的简化写法也是语法糖哦 +for (int e : array) { + System.out.println(e); +} +``` +编译后: -#### 加载模型 +```java +for(int i = 0; i < array.length; ++i) { + int e = array[i]; + System.out.println(e); +} +``` -##### 三种模型 +**集合的循环:** -在JVM中,对于类加载模型提供了三种,分别为全盘加载、双亲委派、缓存机制 +```java +List list = Arrays.asList(1,2,3,4,5); +for (Integer i : list) { + System.out.println(i); +} +``` -- **全盘加载:**当一个类加载器负责加载某个Class时,该Class所依赖和引用的其他Class也将由该类加载器负责载入,除非显示指定使用另外一个类加载器来载入 +编译后转换为对迭代器的调用: -- **双亲委派:**先让父类加载器加载该Class,在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。简单来说就是,某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,**依次递归**,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载 +```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); +} +``` -- **缓存机制:**会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象存入缓冲区中 - - 这就是修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因 +注意:foreach 循环写法,能够配合数组以及所有实现了 Iterable 接口的集合类一起使用,其中 Iterable 用来获取集合的迭代器( Iterator ) @@ -14642,138 +14729,209 @@ init指的是实例构造器,主要作用是在类实例化过程中执行, -##### 双亲委派模型 +#### switch -双亲委派模型 (Parents Delegation Model):该模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器,这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance) +##### 字符串 - +从 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; +} +``` -* 为了安全,保证类库API不会被修改 +总结: - 在工程中新建java.lang包,接着在该包下新建String类,并定义main函数 +* 执行了两遍 switch,第一遍是根据字符串的 hashCode 和 equals 将字符串的转换为相应byte 类型,第二遍才是利用 byte 执行进行比较 +* hashCode 是为了提高效率,减少可能的比较;而 equals 是为了防止 hashCode 冲突 - ```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 classLoader)得到加载,启动类加载器优先级更高,在核心jre库中有其相同名字的类文件,但该类中并没有main方法 -**源码分析:** +*** -```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) { - // ClassNotFoundException thrown if class not found - // from the non-null parent class loader - } - if (c == null) { - // 如果调用父类的类加载器无法对类进行加载,则用自己的findClass(name)方法进行加载 - 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) { - resolveClass(c); +##### 枚举 + +switch 枚举的例子,原始代码: + +```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; } - return c; + } +} +``` + +编译转换后的代码: + +```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; } } ``` - - *** -#### 自定义 - -对于自定义类加载器的实现,只需要继承ClassLoader类,覆写findClass方法即可 +#### 枚举类 -java.lang.ClassLoader 的 loadClass() 实现了双亲委派模型的逻辑,自定义类加载器一般不去重写它,但是需要重写 findClass() 方法 +JDK 7 新增了枚举类: ```java -//自定义类加载器,读取指定的类路径classPath下的class文件 -public class MyClassLoader extends ClassLoader{ - private String classPath; +enum Sex { + MALE, FEMALE +} +``` - public MyClassLoader(String classPath) { - this.classPath = classPath; - } +编译转换后: - @Override - protected Class findClass(String name) throws ClassNotFoundException { - byte[] data = new byte[0]; - try { - data = loadByte(name); - } catch (Exception e) { - e.printStackTrace(); - } - return defineClass(name, data, 0, data.length); +```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 byte[] loadByte(String name) throws Exception { - name = name.replaceAll("\\.", "/"); - FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class"); - int len = fis.available(); - byte[] data = new byte[len]; - fis.read(data); - fis.close(); - return data; + 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); } } ``` + + +#### try-w-r + +JDK 7 开始新增了对需要关闭的资源处理的特殊语法`try-with-resources`,格式: + +```java +try(资源变量 = 创建资源对象){ +} catch( ) { +} +``` + +其中资源对象需要实现 **AutoCloseable**接口,例如 InputStream、OutputStream、Connection、Statement、ResultSet等接口都实现了 AutoCloseable ,使用 try-withresources可以不用写 finally 语句块,编译器会帮助生成关闭资源代码: + ```java -public class ClassLoaderTest { - public static void main(String[] args) throws Exception { - MyClassLoader classLoader = new MyClassLoader("D:\\workspace\\project\\src\\main\\java"); - Class clazz = classLoader.loadClass("com.demo.User"); - System.out.println(clazz.getClassLoader().getClass().getName()); - } -}//当存在.java文件时,根据双亲委派机制,显示当前类加载器为AppClassLoader,而当将.java文件删除时,则显示使用的是自定义的类加载器 +try(InputStream is = new FileInputStream("d:\\1.txt")) { + System.out.println(is); +} catch (IOException e) { + e.printStackTrace(); +} +``` + +转换成: + +`addSuppressed(Throwable e)`:添加被压制异常,是为了防止异常信息的丢失(try-with-resources 生成的 fianlly 中如果抛出了异常) + +```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(); +} ``` @@ -14782,90 +14940,127 @@ public class ClassLoaderTest { -### 运行优化 +#### 方法重写 -#### 即时编译 +方法重写时对返回值分两种情况: -##### 分层编译 +* 父子类的返回值完全一致 +* 子类返回值可以是父类返回值的子类(比较绕口,见下面的例子) ```java -public static void main(String[] args) { - for (int i = 0; i < 200; i++) { - long start = System.nanoTime(); - for (int j = 0; j < 1000; j++) { - new Object(); - } - long end = System.nanoTime(); - System.out.printf("%d\t%d\n",i,(end - start)); +class A { + public Number m() { + return 1; + } +} +class B extends A { + @Override + // 子类m方法的返回值是Integer是父类m方法返回值Number的子类 + public Integer m() { + return 2; } } ``` +对于子类,java 编译器会做如下处理: + ```java -0 96426 -.... -199 854 +class B extends A { + public Integer m() { + return 2; + } + // 此方法才是真正重写了父类 public Number m() 方法 + public synthetic bridge Number m() { + // 调用 public Integer m() + return m(); + } +} ``` -JVM 将执行状态分成了 5 个层次: - -* 0 层,解释执行(Interpreter) -* 1 层,使用 C1 即时编译器编译执行(不带 profiling) -* 2 层,使用 C1 即时编译器编译执行(带基本的 profiling) -* 3 层,使用 C1 即时编译器编译执行(带完全的 profiling) -* 4 层,使用 C2 即时编译器编译执行 - -说明:profiling 是指在运行过程中收集一些程序执行状态的数据,例如方法的调用次数,循环的回边次数等 - -即时编译器(JIT)与解释器的区别: +其中桥接方法比较特殊,仅对 java 虚拟机可见,并且与原来的 public Integer m() 没有命名冲突 -* 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释 -* JIT 是将一些字节码编译为机器码,并存入**Code Cache**,下次遇到相同的代码,直接执行,无需再编译 -* 解释器是将字节码解释为针对所有平台都通用的机器码 -* JIT 会根据平台类型,生成平台特定的机器码 -对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。 执行效率上简单比较一下 `Interpreter < C1 < C2`,总的目标是发现热点代码(hotspot名称的由来) -这种优化手段称之为逃逸分析,发现新建的对象是否逃逸,可以使用 -XX:-DoEscapeAnalysis 关闭逃逸分析 +*** -##### 方法内联 +#### 匿名内部类 -Inlining +源代码: ```java -private static int square(final int i) { - return i * i; +public class Candy11 { + public static void main(String[] args) { + Runnable runnable = new Runnable() { + @Override + public void run() { + System.out.println("ok"); + } + }; + } } -System.out.println(square(9)); ``` -如果发现 square 是热点方法,并且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝、粘贴到调用者的位置: +转化后代码: ```java -System.out.println(9 * 9); +// 额外生成的类 +final class Candy11$1 implements Runnable { + Candy11$1() { + } + public void run() { + System.out.println("ok"); + } +} ``` -还能够进行常量折叠(constant folding)的优化: - ```java -System.out.println(81); +public class Candy11 { + public static void main(String[] args) { + Runnable runnable = new Candy11$1(); + } +} ``` +引用局部变量的匿名内部类,源代码: -##### 字段优化 +```java +public class Candy11 { + public static void test(final int x) { + Runnable runnable = new Runnable() { + @Override + public void run() { + System.out.println("ok:" + x); + } + }; + } +} +``` -JMH 基准测试请参考:http://openjdk.java.net/projects/code-tools/jmh/ +转换后代码: ```java -@Warmup(iterations = 2, time = 1) //2轮热身 -@Measurement(iterations = 5, time = 1) //5轮测试 -@State(Scope.Benchmark) +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 class Candy11 { + public static void test(final int x) { + Runnable runnable = new Candy11$1(x); + } +} ``` +局部变量必须是 final 的:因为在创建Candy11¥1 对象时,将 x 的值赋值给了 Candy11$1 对象的 val属性,x不应该再发生变化了,因为发生变化,this.val​x属性没有机会再跟着变化 + *** @@ -14943,8 +15138,6 @@ public class GeneratedMethodAccessor1 extends MethodAccessorImpl { - - ## JVM调优 ### 服务器性能 @@ -19876,103 +20069,6 @@ public class ThreadPoolDemo04 { -### Tomcat - -#### 线程池 - -Tomcat执行流程: - -![](https://gitee.com/seazean/images/raw/master/Java/JUC-Tomcat线程池.png) - -* LimitLatch 用来限流,可以控制最大连接个数,类似 J.U.C 中的 Semaphore -* Acceptor 只负责接收新的 socket 连接,每个连接对应一个 socket channel -* Poller 只负责监听 socket channel 是否有可读的 I/O 事件 -* 监听到可读事件,封装一个任务对象(socketProcessor),提交给 Executor 线程池处理 -* Executor 线程池中的工作线程负责最终的处理请求 - -Tomcat 线程池扩展了 ThreadPoolExecutor: - -* 如果总线程数达到 maximumPoolSize,这时不会立刻抛 RejectedExecutionException 异常 -* 会再次尝试将任务放入队列,如果还失败才抛出 RejectedExecutionException 异常 - -```java -public void execute(Runnable command, long timeout, TimeUnit unit) { - submittedCount.incrementAndGet(); - try { - super.execute(command); - } catch (RejectedExecutionException rx) { - if (super.getQueue() instanceof TaskQueue) { - final TaskQueue queue = (TaskQueue)super.getQueue(); - try { - if (!queue.force(command, timeout, unit)) { - submittedCount.decrementAndGet(); - throw new RejectedExecutionException("Queue capacity is full."); - } - } catch (InterruptedException x) { - submittedCount.decrementAndGet(); - Thread.interrupted(); - throw new RejectedExecutionException(x); - } - } else { - submittedCount.decrementAndGet(); - throw rx; - } - } -} -``` - -```java -public boolean force(Runnable o, long timeout, TimeUnit unit) { - if ( parent.isShutdown() ) - throw new RejectedExecutionException( - "Executor not running, can't force a command into the queue"); - return super.offer(o,timeout,unit); - //forces the item onto the queue, to be used if the task is rejected -} -``` - - - -*** - - - -#### 配置 - -Connector 配置: - -| 配置项 | 默认值 | 说明 | -| ------------------- | ------ | -------------------------------------- | -| acceptorThreadCount | 1 | acceptor 线程数量 | -| pollerThreadCount | 1 | poller 线程数量 | -| minSpareThreads | 10 | 核心线程数,即 corePoolSize | -| maxThreads | 200 | 最大线程数,即 maximumPoolSize | -| executor | - | Executor 名称,用来引用下面的 Executor | - -* 如果定义了executor属性,会覆盖Connector的核心线程数和最大线程数 - -Executor 配置: - -| 配置项 | 默认值 | 说明 | -| ----------------------- | ----------------- | ----------------------------------------- | -| threadPriority | 5 | 线程优先级 | -| daemon | true | 是否守护线程(是守护线程,tomcat) | -| minSpareThreads | 20 | 核心线程数,即 corePoolSize | -| maxThreads | 400 | 最大线程数,即 maximumPoolSize | -| maxIdleTime | 60000 | 线程生存时间,单位是毫秒,默认值即 1 分钟 | -| maxQueueSize | Integer.MAX_VALUE | 队列长度(默认整数最大值,无界队列) | -| prestartminSpareThreads | false | 核心线程是否在服务器启动时启动 | - -* Tomcat服务器停止,这些守护线程都会停止 -* maxQueueSize为无界队列当线程数过多时会导致服务器压力太大,任务堆积 -* 核心线程懒惰初始化,服务器启动不会创建,任务提交才创建 - - - -*** - - - ### ForkJoin Fork/Join:线程池的实现,体现是分治思想,适用于能够进行任务拆分的 cpu 密集型运算,用于**并行计算** @@ -22349,6 +22445,8 @@ JDK 8 虽然将扩容算法做了调整,改用了尾插法,但仍不意味 #### JDK8源码 +(待更新) + ##### 成员属性 1. 扩容阈值 diff --git a/SSM.md b/SSM.md index 7a42406..b5de0d0 100644 --- a/SSM.md +++ b/SSM.md @@ -10749,7 +10749,7 @@ SSM(Spring+SpringMVC+MyBatis) -#### 返回消息兼容异常信息 +#### 兼容异常 java.controller.interceptor @@ -11271,9 +11271,6 @@ yml文件优势: @Value("${person.name}") private String name2; - @Value("${person.age}") - private String age; - @Value("${address[0]}") private String address1; @@ -11290,7 +11287,7 @@ yml文件优势: } } ``` - + * Evironment对象 ```java @@ -11307,7 +11304,7 @@ yml文件优势: * 注解@ConfigurationProperties - **注意**:prefix一定要写 + **注意**:参数prefix一定要指定 ```java @Component @@ -11351,13 +11348,11 @@ Profile的配置: 2. **profile配置方式** - 多profile文件方式:提供多个配置文件,每个代表一种环境。 - - ​ application-dev.properties/yml 开发环境 - - ​ application-test.properties/yml 测试环境 + 多profile文件方式:提供多个配置文件,每个代表一种环境 - ​ application-pro.properties/yml 生产环境 + * application-dev.properties/yml 开发环境 + * application-test.properties/yml 测试环境 + * sapplication-pro.properties/yml 生产环境 yml多文档方式:在yml中使用 --- 分隔不同配置 @@ -11460,16 +11455,14 @@ Profile的配置: 2. 导入坐标 ```xml - - - - junit - junit - 4.12 - - + + + junit + junit + 4.12 + ``` - + 3. 测试类 ```java diff --git a/Tool.md b/Tool.md index bd872c4..672b002 100644 --- a/Tool.md +++ b/Tool.md @@ -1838,8 +1838,6 @@ bunzip2命令是.bz2文件的解压缩程序。 简单的来说, vi 是老式的字处理器,不过功能已经很齐全了,但是还是有可以进步的地方。 -vim 则可以说是程序开发者的一项很好用的工具。 - **命令模式**:在Linux终端中输入“vim 文件名”就进入了命令模式,但不能输入文字。 **编辑模式:**在命令模式下按i就会进入编辑模式,此时就可以写入程式,按Esc可回到命令模式。 **末行模式:**在命令模式下按:进入末行模式,左下角会有一个冒号出现,此时可以敲入命令并执行 @@ -1848,10 +1846,9 @@ vim 则可以说是程序开发者的一项很好用的工具。 #### 打开文件 -Ubuntu 默认没有安装vim,需要先安装 vim。**sudo apt-get install vim** +Ubuntu 默认没有安装vim,需要先安装 vim,安装命令:**sudo apt-get install vim** -Vim 有三种模式: - 命令模式(Command mode)、插入模式(Insert mode)、末行模式(Last Line mode)。 +Vim 有三种模式:命令模式(Command mode)、插入模式(Insert mode)、末行模式(Last Line mode) | Vim 使用的选项 | 说明 | 常用 | | :---------------------: | :-------------------------------------------: | :--: | @@ -1865,6 +1862,8 @@ Vim 有三种模式: +*** + #### 插入模式 @@ -1888,6 +1887,8 @@ Vim 有三种模式: +*** + #### 命令模式 @@ -1930,7 +1931,7 @@ Vim 打开一个文件(文件可以存在,也可以不存在),默认就 -##### 撤销 +##### 撤销删除 在学习编辑命令之前,先要知道怎样撤销之前一次 错误的 编辑操作 @@ -1941,9 +1942,7 @@ Vim 打开一个文件(文件可以存在,也可以不存在),默认就 -##### 删除 - -注:删除的内容此时并没有真正的被删除,在剪切板中,按下 p 键,可以将删除的内容粘贴回来 +删除的内容此时并没有真正的被删除,在剪切板中,按下 p 键,可以将删除的内容粘贴回来 | 快捷键 | 功能描述 | | :---------: | :----------------------: | @@ -1985,20 +1984,9 @@ vim 中提供有一个 被复制文本的缓冲区 -##### 替换 - -| 命令 | 功能 | 工作模式 | -| ---- | ---------------------- | -------- | -| r | 替换当前字符 | 命令模式 | -| R | 替换当前行光标后的字符 | 替换模式 | - -- 光标选中要替换的字符 -- `R` 命令可以进入 **替换模式**, 替换完成后, 按下ESC, 按下 ESC可以回到 **命令模式** -- **替换命令** 的作用就是不用进入 **编辑模式**, 对文件进行 **轻量级的修改** - - +##### 查找替换 -##### 查找 +查找 | 快捷键 | 功能描述 | | :----: | :--------------------------------------: | @@ -2011,33 +1999,20 @@ vim 中提供有一个 被复制文本的缓冲区 | n | 查找下一个,向同一方向重复上次的查找指令 | | N | 查找上一个,向相反方向重复上次的查找指令 | +替换: +| 命令 | 功能 | 工作模式 | +| ---- | ---------------------- | -------- | +| r | 替换当前字符 | 命令模式 | +| R | 替换当前行光标后的字符 | 替换模式 | -##### 查找替换 - -在 vim中查找和替换命令需要在 **末行模式** 下执行 - -命令格式:`:%s///g` - -常用: - -* `:n1,n2 s/word1/word2/g` :在第n1到n2行之间寻找 word1 ,并将该字符串取代为 word2。 - -* `:1,$s/word1/word2/g` == `:%s/word1/word2/g`:从第一行到最后一行寻找 word1,并取代 - -**询问是否替换** +- 光标选中要替换的字符 +- `R` 命令可以进入 **替换模式**, 替换完成后, 按下ESC, 按下 ESC可以回到 **命令模式** +- **替换命令** 的作用就是不用进入 **编辑模式**, 对文件进行 **轻量级的修改** -* 如果把末尾的 `g` 改成 `gc` 在替换的时候, 会有提示! (推荐使用) -* 命令:`:%s/旧文本/新文本/gc` - * `y` - `yes` 替换 - * `n` - `no` 不替换 - * `a` - `all` 替换所有 - * `q` -`quit` 退出替换 - * `l` - `last` 最后一个, 并把光标移动到行首 - * `^E` 向下滚屏 - * `^Y` 向上滚屏 +*** From 75805b39d0c5bae7cfe1edbf5749d94d710bfdea Mon Sep 17 00:00:00 2001 From: Seazean Date: Tue, 18 May 2021 00:30:20 +0800 Subject: [PATCH 006/242] Update Java Notes --- Java.md | 162 +++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 130 insertions(+), 32 deletions(-) diff --git a/Java.md b/Java.md index 1d9b45a..fb6cd63 100644 --- a/Java.md +++ b/Java.md @@ -11468,6 +11468,7 @@ JVM的生命周期分为三个阶段,分别为:启动、运行、死亡。 - main()方法是一个程序的初始起点,任何线程均可由在此处启动 - 在JVM内部有两种线程类型,分别为:**用户线程和守护线程**,JVM通常使用的是守护线程,而main()和其他线程使用的是用户线程,守护线程会随着用户线程的结束而结束 - 执行一个Java程序时,真真正正在执行的是一个Java虚拟机的进程 + - JVM有两种运行模式Server与Client,两种模式的区别在于:Client模式启动速度较快,Server模式启动较慢;但是启动进入稳定期长期运行之后Server模式的程序运行速度比Client要快很多 - **死亡**: - 当程序中的用户线程都中止,JVM才会退出 - 程序正常执行结束、程序异常或错误而异常终止、操作系统错误导致终止 @@ -11607,7 +11608,7 @@ Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所 * 参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束,JVM为每一个slot都分配一个访问索引,通过索引即可访问到槽中的数据 * 存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量 * 32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot -* 局部变量表中的槽位是可以重复利用的,如果一个局部变量过了其作用域,那么之后申明的新的局部变量就可能会复用过期局部变量的槽位,从而达到节省资源的目的 +* 局部变量表中的槽位是可以**重复利用**的,如果一个局部变量过了其作用域,那么之后申明的新的局部变量就可能会复用过期局部变量的槽位,从而达到节省资源的目的 @@ -11686,14 +11687,14 @@ Return Address:存放调用该方法的PC寄存器的值 本地方法栈是为虚拟机**执行本地方法时提供服务的** +JNI:Java Native Interface,通过使用 Java本地接口书写程序,可以确保代码在不同的平台上方便移植 + * 不需要进行GC,与虚拟机栈类似,也是线程私有的,有StackOverFlowError和OutOfMemoryError异常 * 虚拟机栈执行的是java方法,在hotSpot JVM中,直接将本地方法栈和虚拟机栈合二为一 * 本地方法一般是由其他语言编写,并且被编译为基于本机硬件和操作系统的程序 -* JNI:Java Native Interface,通过使用 Java本地接口书写程序,可以确保代码在不同的平台上方便移植 - * 当某个线程调用一个本地方法时,就进入了不再受虚拟机限制的世界,和虚拟机拥有同样的权限 * 本地方法可以通过本地方法接口来 **访问虚拟机内部的运行时数据区** @@ -11796,7 +11797,7 @@ public static void main(String[] args) { #### 方法区 -方法区:是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它也叫 Non-Heap(非堆) +方法区:是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是也叫 Non-Heap(非堆) 方法区是一个 JVM 规范,**永久代与元空间都是其一种实现方式** @@ -12164,7 +12165,7 @@ JVM是通过栈帧中的对象引用访问到其内部的对象实例:(内 Eden 和 Survivor 大小比例默认为 8:1:1 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-分代收集算法.png) + @@ -12316,16 +12317,18 @@ JVM是将TLAB作为内存分配的首选,但不是所有的对象实例都能 ### 回收策略 +#### 触发条件 + 内存垃圾回收机制主要集中的区域就是线程共享区域:**堆和方法区** -对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC +Minor GC 触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC -Full GC 则相对复杂,**FullGC同时回收新生代和老年代,当前只会存在一个FullGC的线程进行执行,其他的线程全部会被挂起**,有以下**触发条件**: +FullGC 同时回收新生代、老年代和方法区,只会存在一个FullGC的线程进行执行,其他的线程全部会被**挂起**,有以下触发条件: * 调用 System.gc(): - * 不建议使用这种方式,应该让虚拟机管理内存,一般情况下,垃圾回收应该是自动进行的,无须手动触发。在一些特殊情况下,如正在编写一个性能基准,可以在运行之间调用System.gc() - * 在默认情况下,通过system.gc() 或 Runtime.getRuntime().gc() 的调用,会显式触发FullGC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存,但是虚拟机不一定真正去执行,无法保证对垃圾收集器的调用 + * 在默认情况下,通过system.gc() 或 Runtime.getRuntime().gc() 的调用,会显式触发FullGC,同时对老年代和新生代进行回收,但是虚拟机不一定真正去执行,无法保证对垃圾收集器的调用 (马上触发GC) + * 不建议使用这种方式,应该让虚拟机管理内存。一般情况下,垃圾回收应该是自动进行的,无须手动触发;在一些特殊情况下,如正在编写一个性能基准,可以在运行之间调用System.gc() * 老年代空间不足: @@ -12334,15 +12337,69 @@ Full GC 则相对复杂,**FullGC同时回收新生代和老年代,当前只 * 空间分配担保失败 -* JDK 1.7 及以前的永久代空间不足: - - * 在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC,如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError - * 可采用增大永久代空间或转为使用 CMS GC来避免 +* 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,但是当线程处于Sleep 状态或Blocked状态,线程无法响应JVM的中断请求,运行到安全点去中断挂起,JVM也不可能等待线程被唤醒,对于这种情况,需要安全区域来解决 + +安全区域 (Safe Region):指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的 + +运行流程: + +- 当线程运行到Safe Region的代码时,首先标识已经进入了Safe Region,如果这段时间内发生GC,JVM会忽略标识为Safe Region状态的线程 + +- 当线程即将离开Safe Region时,会检查JVM是否已经完成GC,如果完成了则继续运行,否则线程必须等待直到收到可以安全离开SafeRegion的信号 + *** @@ -12355,15 +12412,21 @@ Full GC 则相对复杂,**FullGC同时回收新生代和老年代,当前只 垃圾:**如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾** +作用:释放没用的对象,清除内存里的记录碎片,碎片整理将所占用的堆内存移到堆的一端,以便JVM 将整理出的内存分配给新的对象 + 垃圾收集主要是针对堆和方法区进行,程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收 在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段,判断对象存活一般有两种方式:**引用计数算法**和**可达性分析算法** +*** + + + #### 引用计数法 -引用计数算法(Reference Counting):对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1;当对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收 +引用计数算法(Reference Counting):对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1;当对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收(Java没有采用) 优点: @@ -12375,7 +12438,7 @@ Full GC 则相对复杂,**FullGC同时回收新生代和老年代,当前只 - 每次对象被引用时,都需要去更新计数器,有一点时间开销。 -- **浪费CPU资源**,即使内存够用,仍然在运行时进行计数器的统计。 +- 浪费CPU资源,即使内存够用,仍然在运行时进行计数器的统计。 - **无法解决循环引用问题,会引发内存泄露**(最大的缺点) @@ -12407,21 +12470,22 @@ Full GC 则相对复杂,**FullGC同时回收新生代和老年代,当前只 可达性分析算法:也可以称为根搜索算法、追踪性垃圾收集 -**GC Roots**: +**GC Roots对象**: - 虚拟机栈中局部变量表中引用的对象:各个线程被调用的方法中使用到的参数、局部变量等 - 本地方法栈中引用的对象 - 方法区中类静态属性引用的对象 - 方法区中的常量引用的对象:字符串常量池(string Table)里的引用 +- 同步锁synchronized持有的对象 -注意: +GC Roots说明: * **GC Roots是一组活跃的引用,不是对象** * 如果一个指针保存了堆内存中的对象,但自己不在堆内存中,它就是一个Root 使用可达性分析算法来判断内存是否可回收,分析工作必须在一个能保障**一致性的快照**中进行,否则分析结果的准确性无法保证,这点也是导致GC进行时必须“Stop The World”的一个重要原因 -过程: +工作过程: - 可达性分析算法是以**根对象集合(GCRoots)**为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达 @@ -12443,7 +12507,7 @@ Full GC 则相对复杂,**FullGC同时回收新生代和老年代,当前只 Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑 -垃圾回收此对象之前,会先调用这个对象的finalize()方法,finalize() 方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等 +垃圾回收此对象之前,会先调用这个对象的finalize()方法,finalize() 方法允许在子类中被重写,用于在对象被回收时进行后置处理,通常在这个方法中进行一些资源释放和清理,比如关闭文件、套接字和数据库连接等 生存OR死亡:如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用,此对象需要被回收。但事实上这时候它们暂时处于”缓刑“阶段。**一个无法触及的对象有可能在某一个条件下“复活”自己**,如果这样对它的回收就是不合理的,所以虚拟机中的对象可能的三种状态: @@ -12451,10 +12515,10 @@ Java语言提供了对象终止(finalization)机制来允许开发人员提 - 可复活的:对象的所有引用都被释放,但是对象有可能在finalize() 中复活 - 不可触及的:对象的 finalize() 被调用,并且没有复活,那么就会进入不可触及状态,不可触及的对象不可能被复活。因为**finalize()只会被调用一次**,等到这个对象再被标记为可回收时就必须回收 -注意:永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用,原因: +永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用,原因: * finalize() 时可能会导致对象复活 -* finalize() 方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生GC,则finalize()方法将没有执行机会。因为优先级比较低,即使主动调用该方法,也不会因此就直接进行回收 +* finalize() 方法的执行时间是没有保障的,完全由GC线程决定,极端情况下,若不发生GC,则finalize()方法将没有执行机会,因为优先级比较低,即使主动调用该方法,也不会因此就直接进行回收 * 一个糟糕的finalize() 会严重影响GC的性能 @@ -12552,11 +12616,11 @@ Java语言提供了对象终止(finalization)机制来允许开发人员提 标记清除算法,是将垃圾回收分为2个阶段,分别是**标记和清除** - **标记**: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) 的块返回给空闲列表 -缺点: +算法缺点: - 标记和清除过程效率都不高 - 进行GC的时候,需要停止整个应用程序,用户体验较差 @@ -12578,15 +12642,15 @@ Java语言提供了对象终止(finalization)机制来允许开发人员提 ![](https://gitee.com/seazean/images/raw/master/Java/JVM-复制算法.png) -优点: +算法优点: - 没有标记和清除过程,实现简单,运行高效 - 复制过去以后保证空间的连续性,不会出现“碎片”问题。 -缺点: +算法缺点: - 主要不足是只使用了内存的一半。 -- 对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小 +- 对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销都不小 现在的商业虚拟机都采用这种收集算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间 @@ -12608,6 +12672,33 @@ Java语言提供了对象终止(finalization)机制来允许开发人员提 +| | Mark-Sweep | Mark-Compact | Copying | +| -------- | ---------------- | -------------- | ----------------------------------- | +| 速度 | 中等 | 最慢 | 最快 | +| 空间开销 | 少(但会堆积碎片) | 少(不堆积碎片) | 通常需要活对象的2倍大小(不堆积碎片) | +| 移动对象 | 否 | 是 | 是 | + +- 效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存。 +- 为了尽量兼顾三个指标,标记一整理算法相对来说更平滑一些 + + + +*** + + + +#### 增量收集 + + + +JVM三色标记法 + + + + + + + @@ -12628,8 +12719,8 @@ Java语言提供了对象终止(finalization)机制来允许开发人员提 * 并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间 * 独占式垃圾回收器(Stop the world)一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束 * 按碎片处理方式分,可分为压缩式垃圾回收器和非压缩式垃圾回收器 - * 压缩式垃圾回收器会在回收完成后,对存活对象进行压缩整理,消除回收后的碎片 - * 非压缩式的垃圾回收器不进行这步操作 + * 压缩式垃圾回收器在回收完成后进行压缩整理,消除回收后的碎片,再分配对象空间使用指针碰撞 + * 非压缩式的垃圾回收器不进行这步操作,再分配对象空间使用空闲列表 * 按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器 GC性能指标: @@ -12682,7 +12773,7 @@ GC性能指标: ![](https://gitee.com/seazean/images/raw/master/Java/JVM-Serial收集器.png) -优点:简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,可以获得最高的单线程收集效率。 +优点:简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,可以获得最高的单线程收集效率 缺点:对于交互性较强的应用而言,这种垃圾收集器是不能够接受的,比如Javaweb应用 @@ -12767,16 +12858,21 @@ ParNew 是很多JVM运行在Server模式下新生代的默认垃圾收集器 CMS全称 Concurrent Mark Sweep,是一款**并发的、使用标记-清除**算法、针对老年代的垃圾回收器,其最大特点是**让垃圾收集线程与用户线程同时工作** -CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,停顿时间越短(**低延迟**)越适合与用户交互的程序,良好的响应速度能提升用户体验,目前大部分的Java应用集中在互联网站或者B/S系统的服务端上 +CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,停顿时间越短(**低延迟**)越适合与用户交互的程序,良好的响应速度能提升用户体验 分为以下四个流程: - 初始标记:使用STW出现短暂停顿,仅标记一下 GC Roots 能直接关联到的对象,速度很快, - 并发标记:进行 GC Roots的直接关联对象开始遍历整个对象图,在整个回收过程中耗时最长,不需要STW,可以与用户线程一起并发运行 - 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,比初始标记时间长但远比并发标记时间短,需要STW -- 并发清除:清除标记为可以回收对象,**不需要移动存活对象**,所以这个阶段可以与用户线程同时并发的 +- 并发清除:清除标记为可以回收对象,不需要移动存活对象,所以这个阶段可以与用户线程同时并发的 标记-清除算法不需要移动存活对象,在与用户线程并发过程中,防止线程使用的对象地址改变而影响运行 +Mark Sweep会造成内存碎片,还不把算法换成Mark Compact的原因: + +* 当并发清除时,用Compact整理内存的话,用户线程使用的内存将无法使用,无法保证用户线程继续执行 +* Mark Compact 更适合 Stop the World 场景 + 在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿 ![](https://gitee.com/seazean/images/raw/master/Java/JVM-CMS收集器.png) @@ -19575,6 +19671,8 @@ public class LinkedBlockingQueue extends AbstractQueue 与其他BlockingQueue不同,SynchronousQueue是一个不存储元素的BlockingQueue +(待更新) + *** From 16c560bdd09becc67f65343a170124af6504fd03 Mon Sep 17 00:00:00 2001 From: Seazean Date: Tue, 18 May 2021 23:52:02 +0800 Subject: [PATCH 007/242] Update Java JVM --- Java.md | 194 +++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 149 insertions(+), 45 deletions(-) diff --git a/Java.md b/Java.md index fb6cd63..7ff3b18 100644 --- a/Java.md +++ b/Java.md @@ -12382,7 +12382,7 @@ public void localvarGC4() { 安全点 (Safepoint):程序执行时并非在所有地方都能停顿下来开始GC,只有在安全点才能停下 -- Safe Point的选择很重要,如果太少可能导致GC等待的时间太长,如果太频繁可能导致运行时的性能问题 +- Safe Point的选择很重要,如果太少可能导致GC等待的时间太长,如果太多可能导致运行时的性能问题 - 大部分指令的执行时间都非常短,通常会根据是否具有让程序长时间执行的特征为标准,选择些执行时间较长的指令作为Safe Point, 如方法调用、循环跳转和异常跳转等 在GC发生时,让所有线程都在最近的安全点停顿下来的方法: @@ -12390,7 +12390,7 @@ public void localvarGC4() { - 抢先式中断:没有虚拟机采用,首先中断所有线程,如果有线程不在安全点,就恢复线程让线程运行到安全点 - 主动式中断:设置一个中断标志,各个线程运行到各个 Safe Point 时就轮询这个标志,如果中断标志为真,则将自己进行中断挂起 -问题:Safepoint 保证程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint,但是当线程处于Sleep 状态或Blocked状态,线程无法响应JVM的中断请求,运行到安全点去中断挂起,JVM也不可能等待线程被唤醒,对于这种情况,需要安全区域来解决 +问题:Safepoint 保证程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint,但是当线程处于 Waiting 状态或Blocked状态,线程无法响应JVM的中断请求,运行到安全点去中断挂起,JVM也不可能等待线程被唤醒,对于这种情况,需要安全区域来解决 安全区域 (Safe Region):指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的 @@ -12398,7 +12398,7 @@ public void localvarGC4() { - 当线程运行到Safe Region的代码时,首先标识已经进入了Safe Region,如果这段时间内发生GC,JVM会忽略标识为Safe Region状态的线程 -- 当线程即将离开Safe Region时,会检查JVM是否已经完成GC,如果完成了则继续运行,否则线程必须等待直到收到可以安全离开SafeRegion的信号 +- 当线程即将离开Safe Region时,会检查JVM是否已经完成GC,如果完成了则继续运行,否则线程必须等待GC完成,收到可以安全离开SafeRegion的信号 @@ -12468,6 +12468,8 @@ public void localvarGC4() { #### 可达性分析 +##### GC Roots + 可达性分析算法:也可以称为根搜索算法、追踪性垃圾收集 **GC Roots对象**: @@ -12480,16 +12482,20 @@ public void localvarGC4() { GC Roots说明: -* **GC Roots是一组活跃的引用,不是对象** +* **GC Roots是一组活跃的引用,不是对象**,放在 GC Roots Set 集合 * 如果一个指针保存了堆内存中的对象,但自己不在堆内存中,它就是一个Root -使用可达性分析算法来判断内存是否可回收,分析工作必须在一个能保障**一致性的快照**中进行,否则分析结果的准确性无法保证,这点也是导致GC进行时必须“Stop The World”的一个重要原因 -工作过程: -- 可达性分析算法是以**根对象集合(GCRoots)**为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达 +##### 工作原理 + +可达性分析算法以**根对象集合 (GCRoots)**为起始点,从上至下的方式搜索被根对象集合所连接的目标对象 + +分析工作必须在一个能保障**一致性的快照**中进行,否则结果的准确性无法保证,这点也是导致GC进行时必须 Stop The World 的一个重要原因 + +基本原理: -- 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain) +- 可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索走过的路径称为引用链 - 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象 @@ -12499,6 +12505,85 @@ GC Roots说明: +##### 三色标记 + +###### 标记算法 + +三色标记法把遍历对象图过程中遇到的对象,按“是否访问过”这个条件标记成以下三种颜色: + +- 白色:尚未访问过 +- 灰色:本对象已访问过,但是本对象引用到的其他对象尚未全部访问 +- 黑色:本对象已访问过,而且本对象引用到的其他对象也全部访问完成 + +当Stop The World (STW) 时,对象间的引用是不会发生变化的,可以轻松完成标记,遍历访问过程为: + +1. 初始时,所有对象都在白色集合 +2. 将GC Roots 直接引用到的对象挪到灰色集合 +3. 从灰色集合中获取对象: + * 将本对象引用到的其他对象全部挪到灰色集合中 + * 将本对象挪到黑色集合里面 +4. 重复步骤3,直至灰色集合为空时结束 +5. 结束后,仍在白色集合的对象即为GC Roots 不可达,可以进行回收 + + + +参考文章:https://www.jianshu.com/p/12544c0ad5c1 + + + +###### 并发标记 + +并发标记时,对象间的引用可能发生变化**,**多标和漏标的情况就有可能发生: + +**多标情况:**当E变为灰色或黑色时,其他线程断开的D对E的引用,导致这部分对象仍会被标记为存活,本轮GC不会回收这部分内存,这部分本应该回收但是没有回收到的内存,被称之为**浮动垃圾** + +* 针对并发标记开始后的**新对象**,通常的做法是直接全部当成黑色,也算浮动垃圾 +* 浮动垃圾并不会影响应用程序的正确性,只是需要等到下一轮垃圾回收中才被清除 + + + +**漏标情况:** + +* 条件一:灰色对象断开了对一个白色对象的引用 (直接或间接),即灰色对象原成员变量的引用发生了变化 +* 条件二:其他线程中修改了黑色对象,插入了一条或多条对该白色对象的新引用 +* 结果:导致该白色对象当作垃圾被GC,影响到了应用程序的正确性 + + + +代码角度解释漏标: + +```java +Object G = objE.fieldG; // 读 +objE.fieldG = null; // 写 +objD.fieldG = G; // 写 +``` + +为了解决问题,可以在上面三步做一下操作,**将对象G记录起来,然后作为灰色对象再进行遍历**,比如放到一个特定的集合,等初始的GC Roots遍历完(并发标记),再遍历该集合(重新标记) + +> 所以重新标记需要STW,应用程序一直在运行,该集合可能会一直增加新的对象,导致永远都运行不完 + +解决方法:添加读写屏障,读屏障拦截第一步,写屏障拦截第二三步,在读写前后进行一些后置处理 (AOP) + +* **写屏障 (Store Barrier) + SATB**:当原来成员变量的引用发生变化之前,记录下原来的引用对象 + + 保留GC开始时的对象图,即原始快照 SATB,当GC Roots确定后,对象图就已经确定,那后续的标记也应该是按照这个时刻的对象图走,如果期间发生变化则记录下来,以后根据这些记录重新标记 + + SATB (Snapshot At The Beginning) 破坏了条件一,从而保证了不会漏标 + +* **写屏障 + 增量更新**:针对新增的引用,记录下新的引用对象,最后进行重新遍历标记 + + 增量更新 (Incremental Update) 破坏了条件二,从而保证了不会漏标 + +* **读屏障 (Load Barrier)**:破坏条件二,黑色对象引用白色对象的前提是获取到该对象,此时读屏障发挥作用 + +以Java HotSpot VM为例,其并发标记时对漏标的处理方案如下: + +- CMS:写屏障 + 增量更新 +- G1:写屏障 + SATB +- ZGC:读屏障 + + + *** @@ -12689,15 +12774,11 @@ Java语言提供了对象终止(finalization)机制来允许开发人员提 #### 增量收集 +增量收集算法:通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作,基础仍是标记-清除和复制算法,用于多线程并发环境 +工作原理:如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,影响系统的交互性,所以让垃圾收集线程和应用程序线程交替执行,每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程,依次反复直到垃圾收集完成 -JVM三色标记法 - - - - - - +缺点:线程切换和上下文转换消耗资源,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降 @@ -12862,16 +12943,16 @@ CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿 分为以下四个流程: -- 初始标记:使用STW出现短暂停顿,仅标记一下 GC Roots 能直接关联到的对象,速度很快, +- 初始标记:使用STW出现短暂停顿,仅标记一下 GC Roots 能直接关联到的对象,速度很快 - 并发标记:进行 GC Roots的直接关联对象开始遍历整个对象图,在整个回收过程中耗时最长,不需要STW,可以与用户线程一起并发运行 - 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,比初始标记时间长但远比并发标记时间短,需要STW - 并发清除:清除标记为可以回收对象,不需要移动存活对象,所以这个阶段可以与用户线程同时并发的 - 标记-清除算法不需要移动存活对象,在与用户线程并发过程中,防止线程使用的对象地址改变而影响运行 Mark Sweep会造成内存碎片,还不把算法换成Mark Compact的原因: -* 当并发清除时,用Compact整理内存的话,用户线程使用的内存将无法使用,无法保证用户线程继续执行 -* Mark Compact 更适合 Stop the World 场景 +* Mark Compact 算法会整理内存,导致用户线程使用的对象的地址改变,影响用户线程继续执行 + +* Mark Compact 更适合 Stop The World 场景 在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿 @@ -12883,7 +12964,7 @@ Mark Sweep会造成内存碎片,还不把算法换成Mark Compact的原因: - 吞吐量降低:在并发阶段虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,CPU 利用率不够高 - CMS收集器无法处理浮动垃圾,可能出现 Concurrent Mode Failure导致另一次Full GC的产生 - 浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,需要预留出一部分内存,CMS 收集不能等待老年代快满的时候再回收,如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS,导致很长的停顿时间 + 浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,CMS 收集需要预留出一部分内存,不能等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS,导致很长的停顿时间 - 标记 - 清除算法导致的空间碎片,往往出现老年代空间无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC;为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer)技术,而只能够选择空闲链表(Free List)执行内存分配 参数设置: @@ -12914,7 +12995,7 @@ Mark Sweep会造成内存碎片,还不把算法换成Mark Compact的原因: #### G1 -##### 特点 +##### G1特点 G1(Garbage-First)是一款面向服务端应用的垃圾收集器,**应用于新生代和老年代**、采用标记-整理算法、软实时、低延迟、可设定目标(最大STW停顿时间)的垃圾回收器,用于代替CMS,适用于较大的堆(>4~6G),在JDK9之后默认使用G1 @@ -12933,7 +13014,7 @@ G1对比其他处理器的优点: * G1 把堆划分成多个大小相等的独立区域,从而将原来的一整块内存空间划分成多个小空间,使得每个小空间可以单独进行垃圾回收;新生代和老年代不再物理隔离,不用担心每个代内存是否足够 * **新的区域Humongous**:本身属于老年代区,当出现了一个巨大的对象,超出了分区容量的一半,则这个对象会进入到该区域。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储,为了能找到连续的H区,有时候不得不启动Full GC - * G1 不会对巨型对象进行拷贝,回收时被优先考虑,G1会跟踪老年代所有 incoming 引用,这样老年代incoming 引用为0 的巨型对象就可以在新生代垃圾回收时处理掉 + * G1 不会对巨型对象进行拷贝,回收时被优先考虑,G1会跟踪老年代所有 incoming 引用,这样老年代incoming 引用为 0 的巨型对象就可以在新生代垃圾回收时处理掉 * Region结构图: @@ -12967,19 +13048,7 @@ G1垃圾收集器的缺点: - - -##### 参数 - -- `-XX:+UseG1GC`:手动指定使用G1垃圾收集器执行内存回收任务 -- `-XX:G1HeapRegionSize`:设置每个Region的大小。值是2的幂,范围是1MB到32MB之间,目标是根据最小的Java堆大小划分出约2048个区域,默认是堆内存的1/2000 -- `-XX:MaxGCPauseMillis`:设置期望达到的最大GC停顿时间指标 (JVM会尽力实现,但不保证达到),默认值是200ms -- `-XX:+ParallelGcThread`:设置STW工作线程数的值,最多设置为8 -- `-XX:ConcGCThreads`:设置并发标记线程数,设置为并行垃圾回收线程数(ParallelGcThreads) 的1/4左右 -- `-XX:InitiatingHeapoccupancyPercent`:设置触发并发Mixed GC周期的Java堆占用率阈值,超过此值,就触发GC,默认值是45 -- `-XX:+ClassUnloadingWithConcurrentMark`:并发标记类卸载,默认启用,所有对象都经过并发标记后,就可以知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类 - - +*** @@ -12989,11 +13058,13 @@ G1垃圾收集器的缺点: -* 程序对Reference类型数据写操作时,产生一个Write Barrier暂时中断操作,检查该对象是否和Reference类型数据在不同的Region(其他收集器:检查老年代对象是否引用了新生代对象),不同便通过 CardTable 把相关引用信息记录到Reference类型所属的 Region 的 Remembered Set 之中 +* 程序对Reference类型数据写操作时,产生一个Write Barrier暂时中断操作,检查该对象和Reference类型数据是否在不同的Region(其他收集器:检查老年代对象是否引用了新生代对象),不同便通过 CardTable 把相关引用信息记录到Reference类型所属的 Region 的 Remembered Set 之中 * 进行内存回收时,在 GC 根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏 +*** + ##### 工作原理 @@ -13009,9 +13080,9 @@ G1中提供了三种垃圾回收模式:YoungGC、Mixed GC和FullGC,在不同 **回收过程**: 1. 扫描根:根引用连同RSet记录的外部引用作为扫描存活对象的入口 - 2. 更新RSet:处理dirty card queue更新RS,在此RSet准确的反映老年代对所在的内存分段中对象的引用 + 2. 更新RSet:处理dirty card queue更新RS,此后RSet准确的反映老年代对所在的内存分段中对象的引用 * dirty card queue:类似缓存,产生了引用先记录在这里,然后更新到RSet - * 产生引用直接更新RSet需要线程同步开销很大,使用队列性能好 + * 作用:产生引用直接更新RSet需要线程同步开销很大,使用队列性能好 3. 处理RSet:识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象 4. 复制对象:Eden区内存段中存活的对象会被复制到Survivor区,Survivor区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到old区中空的内存分段,如果Survivor空间不够,Eden空间的部分数据会直接晋升到老年代空间 5. 处理引用:处理Soft,Weak,Phantom,JNI Weak 等引用,最终Eden空间的数据为空,GC停止工作 @@ -13019,7 +13090,7 @@ G1中提供了三种垃圾回收模式:YoungGC、Mixed GC和FullGC,在不同 * **并发标记过程**: * 初始标记:标记从根节点直接可达的对象,这个阶段是STW的,并且会触发一次年轻代GC - * 根区域扫描 (Root Region Scanning):G1 GC扫描survivor区直接可达的老年代区域对象,并标记被引用的对象,这一过程必须在Young GC之前完成 + * 根区域扫描 (Root Region Scanning):G1 扫描survivor区直接可达的老年代区域对象,并标记被引用的对象,这一过程必须在Young GC之前完成 * 并发标记 (Concurrent Marking):在整个堆中进行并发标记(应用程序并发执行),可能被YoungGC中断。在并发标记阶段,**若发现区域对象中的所有对象都是垃圾,则这个区域会被立即回收(实时回收)**,同时并发标记过程中,会计算每个区域的对象活性,即区域中存活对象的比例 * 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这阶段需要停顿线程,但是可并行执行 * 筛选回收:并发清理阶段,首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率 @@ -13041,6 +13112,24 @@ G1中提供了三种垃圾回收模式:YoungGC、Mixed GC和FullGC,在不同 +*** + + + +##### 相关参数 + +- `-XX:+UseG1GC`:手动指定使用G1垃圾收集器执行内存回收任务 +- `-XX:G1HeapRegionSize`:设置每个Region的大小。值是2的幂,范围是1MB到32MB之间,目标是根据最小的Java堆大小划分出约2048个区域,默认是堆内存的1/2000 +- `-XX:MaxGCPauseMillis`:设置期望达到的最大GC停顿时间指标 (JVM会尽力实现,但不保证达到),默认值是200ms +- `-XX:+ParallelGcThread`:设置STW工作线程数的值,最多设置为8 +- `-XX:ConcGCThreads`:设置并发标记线程数,设置为并行垃圾回收线程数(ParallelGcThreads) 的1/4左右 +- `-XX:InitiatingHeapoccupancyPercent`:设置触发并发Mixed GC周期的Java堆占用率阈值,超过此值,就触发GC,默认值是45 +- `-XX:+ClassUnloadingWithConcurrentMark`:并发标记类卸载,默认启用,所有对象都经过并发标记后,就可以知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类 + + + +*** + ##### 调优 @@ -13053,7 +13142,7 @@ G1的设计原则就是简化JVM性能调优,只需要简单的三步即可完 **不断调优暂停时间指标**: -* `XX:MaxGCPauseMillis=x`可以设置启动应用程序暂停的时间,G1在运行的时候会根据这个参数选择CSet来满足响应时间的设置 +* `XX:MaxGCPauseMillis=x` 可以设置启动应用程序暂停的时间,G1在运行的时候会根据这个参数选择CSet来满足响应时间的设置 * 设置到100ms或者200ms都可以(不同情况下会不一样),但设置成50ms就不太合理 * 暂停时间设置的太短,就会导致出现G1跟不上垃圾产生的速度,最终退化成Full GC * 对这个参数的调优是一个持续的过程,逐步调整到最佳状态 @@ -13071,7 +13160,18 @@ G1的设计原则就是简化JVM性能调优,只需要简单的三步即可完 #### ZGC -ZGC收集器是一款基于Region内存布局的,不设分代,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记压缩算法的,以低延迟为首要目标的一款垃圾收集器 +Shenandoah 垃圾回收器的目标是内存回收实现低停顿 + +Shenandoah GC 暂停时间与堆大小无关,无论将堆设置为200MB还是200GB,99.9%的目标都可以把垃圾收集的停顿时间限制在十毫秒以内,不过实际使用性能将取决于实际工作堆的大小和工作负载 + +* 优点:低延迟 +* 缺点:高运行负担下的吞吐量下降 + +ZGC收集器是一款基于Region内存布局的,(暂时)不设分代,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记压缩算法的,以低延迟为首要目标的一款垃圾收集器 + +* 在 CMS 和 G1 中都用到了写屏障,而 ZGC 用到了读屏障 +* 染色指针:直接将少量额外的信息存储在指针上的技术,从 64 位的指针中拿几位来标识对象此时的状态 +* 内存多重映射:多个虚拟地址指向同一个物理地址 与 CMS 和 G1 类似,ZGC也采用标记-复制算法,不过 ZGC 对该算法做了重大改进,在 ZGC 中出现 Stop The World 的情况会更少,在尽可能对吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟 @@ -19409,6 +19509,8 @@ Memory leak:内存泄漏是指程序中动态分配的堆内存由于某种原 线程池的核心思想:**线程复用**,同一个线程可以被重复使用,来处理多个任务 +池化技术 (Pool) :一种编程技巧,核心思想是资源复用,在请求量大时能优化应用性能,降低系统频繁建连的资源开销 + *** @@ -20422,7 +20524,7 @@ state设计: * state 使用了 32bit int 来维护同步状态 * state **使用 volatile 修饰配合 cas** 保证其修改时的原子性 -* state 表示已经进入的线程数或者许可进入的线程数 +* state 表示线程重入的次数或者许可进入的线程数 * state API: `protected final int getState()`:获取 state 状态 `protected final void setState(int newState)`:设置 state 状态 @@ -21844,6 +21946,8 @@ public static void main(String[] args) { +*** + ##### 解锁原理 @@ -21922,7 +22026,7 @@ StampedLock:读写锁,该类自 JDK 8 加入,是为了进一步优化读 * 在使用读锁、写锁时都必须配合戳使用 * StampedLock 不支持条件变量 -* StampedLock 不支持可重入 +* StampedLock **不支持可重入** 基本用法 @@ -22027,9 +22131,9 @@ class DataContainerStamped { #### 信号量 -synchronized可以起到"锁"的作用,但某个时间段内,只能有一个线程允许执行 +synchronized可以起到锁的作用,但某个时间段内,只能有一个线程允许执行 -Semaphore(信号量)用来限制能同时访问共享资源的线程上限 +Semaphore(信号量)用来限制能同时访问共享资源的线程上限,非重入锁 构造方法: From d8ec5a21f49b4b735ab0e8f4054a03efc73345f7 Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 19 May 2021 23:09:17 +0800 Subject: [PATCH 008/242] Update Java Notes --- DB.md | 560 +++++++++++++++++++++++++++++++++----------------------- Java.md | 10 +- Tool.md | 26 +-- 3 files changed, 352 insertions(+), 244 deletions(-) diff --git a/DB.md b/DB.md index dcd8fe6..d54749a 100644 --- a/DB.md +++ b/DB.md @@ -42,7 +42,7 @@ MySQL数据库是一个最流行的关系型数据库管理系统之一。 -关系型数据库是将数据保存在不同的数据表中,而不是将所有的数据放在一个大仓库内,而且表与表之间还可以有关联关系,这样就提高了访问速度以及提高了灵活性。 +关系型数据库是将数据保存在不同的数据表中,而且表与表之间还可以有关联关系,这样就提高了访问速度以及提高了灵活性。 MySQL所使用的SQL语句是用于访问数据库最常用的标准化语言。 @@ -135,7 +135,7 @@ MySQL配置: - SQL - Structured Query Language:结构化查询语言 - - 定义了操作所有关系型数据库的规则。每一种数据库操作的方式可能会存在不一样的地方,我们称为“方言”。 + - 定义了操作所有关系型数据库的规则,每种数据库操作的方式可能会存在不一样的地方,称为“方言” - SQL通用语法 @@ -150,17 +150,17 @@ MySQL配置: - DDL(Data Definition Language)数据定义语言 - - 用来定义数据库对象:数据库,表,列等。关键字:create, drop,alter 等 + - 用来定义数据库对象:数据库,表,列等。关键字:create、drop,、alter 等 - DML(Data Manipulation Language)数据操作语言 - - 用来对数据库中表的数据进行增删改。关键字:insert, delete, update 等 + - 用来对数据库中表的数据进行增删改。关键字:insert、delete、update 等 - DQL(Data Query Language)数据查询语言 - - 用来查询数据库中表的记录(数据)。关键字:select, where 等 + - 用来查询数据库中表的记录(数据)。关键字:select、where 等 - - DCL(Data Control Language)数据控制语言(了解) + - DCL(Data Control Language)数据控制语言 - 用来定义数据库的访问权限和安全级别,及创建用户。关键字:GRANT, REVOKE 等 @@ -849,7 +849,7 @@ LIMIT:分页限定 ## 约束 -### 概念 +### 约束分类 约束:对表中的数据进行限定,保证数据的正确性、有效性、完整性! @@ -1577,18 +1577,18 @@ CREATE TABLE us_pro( ## 视图 -### 视图概念 +### 视图概述 -视图概念:视图是一种虚拟存在的数据表,这个虚拟的表并不在数据库中实际存在。 +视图概念:视图是一种虚拟存在的数据表,这个虚拟的表并不在数据库中实际存在 -作用:将一些比较复杂的查询语句的结果,封装到一个虚拟表中,再有相同查询需求时,直接查询该虚拟表。 +本质:将一条SELECT查询语句的结果封装到了一个虚拟表中,所以在创建视图的时候,工作重心要放在这条SELECT查询语句上 -本质:视图就是将一条SELECT查询语句的结果封装到了一个虚拟表中。所以我们在创建视图的时候,工作重心要放在这条SELECT查询语句上。 +作用:将一些比较复杂的查询语句的结果,封装到一个虚拟表中,再有相同查询需求时,直接查询该虚拟表 优点: -* 简单,对于使用视图的用户不需要关心表的结构、关联条件和筛选条件,因为这张虚拟表中保存的就是已经过滤好条件的结果集 -* 安全,视图可以设置权限 , 致使访问视图的用户只能访问他们被允许查询的结果集 +* 简单:使用视图的用户不需要关心表的结构、关联条件和筛选条件,因为虚拟表中已经是过滤好的结果集 +* 安全:使用视图的用户只能访问查询的结果集,对表的权限管理并不能限制到某个行某个列 * 数据独立,一旦视图的结构确定,可以屏蔽表结构变化对用户的影响,源表增加列对视图没有影响;源表修改列名,则可以通过修改视图来解决,不会造成对访问者的影响 @@ -1603,9 +1603,17 @@ CREATE TABLE us_pro( * 创建视图 ```mysql - CREATE VIEW 视图名称 [(列名列表)] AS 查询语句; + CREATE [OR REPLACE] + VIEW 视图名称 [(列名列表)] + AS 查询语句 + [WITH [CASCADED | LOCAL] CHECK OPTION]; ``` + `WITH [CASCADED | LOCAL] CHECK OPTION` 决定了是否允许更新数据使记录不再满足视图的条件: + + * LOCAL:只要满足本视图的条件就可以更新 + * CASCADED:必须满足所有针对该视图的所有视图的条件才可以更新, 默认值 + * 例如 ```mysql @@ -1649,7 +1657,8 @@ CREATE TABLE us_pro( * 查询所有数据表,视图也会查询出来 ```mysql - SHOW TABLES + SHOW TABLES; + SHOW TABLE STATUS [\G]; ``` * 查询视图 @@ -1658,7 +1667,7 @@ CREATE TABLE us_pro( SELECT * FROM 视图名称; ``` -* 查询视图创建 +* 查询某个视图创建 ```mysql SHOW CREATE VIEW 视图名称; @@ -1668,18 +1677,21 @@ CREATE TABLE us_pro( ### 视图修改 -**视图表数据修改,会自动修改源表中的数据** +视图表数据修改,会**自动修改源表中的数据**,因为更新的是视图中的基表中的数据 * 修改视图表中的数据 ```mysql - UPDATE 视图名称 SET 列名=值 WHERE 条件; + UPDATE 视图名称 SET 列名 = 值 WHERE 条件; ``` * 修改视图的结构 ```mysql - ALTER VIEW 视图名称 [(列名列表)] AS 查询语句; + ALTER [ALGORITHM = {UNDEFINED | MERGE | TEMPTABLE}] + VIEW 视图名称 [(列名列表)] + AS 查询语句 + [WITH [CASCADED | LOCAL] CHECK OPTION] -- 将视图中的country_name修改为name ALTER @@ -1696,9 +1708,12 @@ CREATE TABLE us_pro( WHERE c1.cid=c2.id; ``` - + + + + ### 视图删除 * 删除视图 @@ -1730,9 +1745,9 @@ CREATE TABLE us_pro( 存储过程和函数的好处: -1. 提高代码的复用性 -2. 减少数据在数据库和应用服务器之间的传输,提高传输效率 -3. 减少代码层面的业务处理 +* 提高代码的复用性 +* 减少数据在数据库和应用服务器之间的传输,提高传输效率 +* 减少代码层面的业务处理 存储过程和函数的区别: @@ -1747,15 +1762,17 @@ CREATE TABLE us_pro( ### 基本操作 -* 小知识 - DELIMITER关键字用来声明sql语句的分隔符,告诉MySQL该段命令已经结束! - sql语句默认的分隔符是分号,但是有的时候我们需要一条功能sql语句中包含分号,但是并不作为结束标识,这个时候就可以使用DELIMITER来指定分隔符了! +DELIMITER: + +* DELIMITER关键字用来声明sql语句的分隔符,告诉MySQL该段命令已经结束 + +* MySQL语句默认的分隔符是分号,但是有时需要一条功能sql语句中包含分号,但是并不作为结束标识,这时使用DELIMITER来指定分隔符: ```mysql DELIMITER 分隔符 ``` -**存储过程的创建调用查看和删除:** +存储过程的创建调用查看和删除: * 创建存储过程 @@ -1791,9 +1808,7 @@ CREATE TABLE us_pro( DROP PROCEDURE [IF EXISTS] 存储过程名称; ``` - - -**练习:** +练习: * 数据准备 @@ -1831,17 +1846,16 @@ CREATE TABLE us_pro( ### 存储语法 -存储过程是可以进行编程的,意味着可以使用变量、表达式、条件控制语句等,来完成比较复杂的功能 - #### 变量使用 -* 定义变量 - DECLARE定义的是局部变量,只能用在BEGIN END范围之内 +存储过程是可以进行编程的,意味着可以使用变量、表达式、条件控制语句等,来完成比较复杂的功能 +* 定义变量:DECLARE定义的是局部变量,只能用在BEGIN END范围之内 + ```mysql DECLARE 变量名 数据类型 [DEFAULT 默认值]; ``` - + * 变量的赋值 ```mysql @@ -1941,9 +1955,9 @@ CREATE TABLE us_pro( * 参数传递的语法 - IN : 代表输入参数,需要由调用者传递实际数据。默认的 - OUT : 代表输出参数,该参数可以作为返回值 - INOUT : 代表既可以作为输入参数,也可以作为输出参数 + IN:代表输入参数,需要由调用者传递实际数据。默认的 + OUT:代表输出参数,该参数可以作为返回值 + INOUT:代表既可以作为输入参数,也可以作为输出参数 ```mysql DELIMITER $ @@ -1962,7 +1976,7 @@ CREATE TABLE us_pro( ```mysql DELIMITER $ - CREATE PROCEDURE pro_test6(IN total INT,OUT description VARCHAR(10)) + CREATE PROCEDURE pro_test6(IN total INT, OUT description VARCHAR(10)) BEGIN -- 判断总分数 IF total >= 380 THEN @@ -1984,7 +1998,7 @@ CREATE TABLE us_pro( * 查看参数方法 - * @变量名 : 这种变量要在变量名称前面加上“@”符号,叫做用户会话变量,代表整个会话过程他都是有作用的,这个类似于全局变量一样。 + * @变量名 : 这种变量要在变量名称前面加上“@”符号,叫做用户会话变量,代表整个会话过程他都是有作用的,类似于全局变量 * @@变量名 : 这种在变量前加上 "@@" 符号, 叫做系统变量 @@ -1999,21 +2013,21 @@ CREATE TABLE us_pro( ```mysql CASE 表达式 - WHEN 值1 THEN 执行sql语句1; - [WHEN 值2 THEN 执行sql语句2;] - ... - [ELSE 执行sql语句n;] + WHEN 值1 THEN 执行sql语句1; + [WHEN 值2 THEN 执行sql语句2;] + ... + [ELSE 执行sql语句n;] END CASE; ``` * 标准语法2 ```mysql - CASE - WHEN 判断条件1 THEN 执行sql语句1; - [WHEN 判断条件2 THEN 执行sql语句2;] - ... - [ELSE 执行sql语句n;] + sCASE + WHEN 判断条件1 THEN 执行sql语句1; + [WHEN 判断条件2 THEN 执行sql语句2;] + ... + [ELSE 执行sql语句n;] END CASE; ``` @@ -2055,13 +2069,12 @@ CREATE TABLE us_pro( * while循环语法 ```mysql - 初始化语句; WHILE 条件判断语句 DO 循环体语句; 条件控制语句; END WHILE; ``` - + * 计算1~100之间的偶数和 ```mysql @@ -2144,20 +2157,19 @@ CREATE TABLE us_pro( #### LOOP循环 +LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定义,通常可以使用 LEAVE 语句实现,如果不加退出循环的语句,那么就变成了死循环 + * loop循环标准语法 ```mysql - 初始化语句; [循环名称:] LOOP 条件判断语句 [LEAVE 循环名称;] 循环体语句; 条件控制语句; END LOOP 循环名称; - -- loop可以实现简单的循环,但是退出循环需要使用其他的语句来定义。我们可以使用leave语句完成! - -- 如果不加退出循环的语句,那么就变成了死循环。 ``` - + * 计算1~10之间的和 ```mysql @@ -2195,100 +2207,106 @@ CREATE TABLE us_pro( #### 游标 -* 游标的概念: - * 游标可以遍历返回的多行结果,每次拿到一整行数据 - * 在存储过程和函数中可以使用游标对结果集进行循环的处理 - * 简单来说游标就类似于集合的迭代器遍历 - * MySQL中的游标只能用在存储过程和函数中 +游标是用来存储查询结果集的数据类型,在存储过程和函数中可以使用光标对结果集进行循环的处理 +* 游标可以遍历返回的多行结果,每次拿到一整行数据 +* 简单来说游标就类似于集合的迭代器遍历 +* MySQL中的游标只能用在存储过程和函数中 -* 游标的语法 +游标的语法 - * 创建游标 +* 创建游标 - ```mysql - DECLARE 游标名称 CURSOR FOR 查询sql语句; - ``` + ```mysql + DECLARE 游标名称 CURSOR FOR 查询sql语句; + ``` - * 打开游标 +* 打开游标 - ```mysql - OPEN 游标名称; - ``` + ```mysql + OPEN 游标名称; + ``` - * 使用游标获取数据 +* 使用游标获取数据 - ```mysql - FETCH 游标名称 INTO 变量名1,变量名2,...; - ``` + ```mysql + FETCH 游标名称 INTO 变量名1,变量名2,...; + ``` - * 关闭游标 +* 关闭游标 - ```mysql - CLOSE 游标名称; - ``` + ```mysql + CLOSE 游标名称; + ``` - +* 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表 +* 数据准备:表student - ```mysql - CREATE TABLE stu_score( - id INT PRIMARY KEY AUTO_INCREMENT, - score INT - ); - ``` + ```mysql + id NAME age gender score + 1 张三 23 男 95 + 2 李四 24 男 98 + 3 王五 25 女 100 + 4 赵六 26 女 90 + ``` - * 将student表中所有的成绩保存到stu_score表中 +* 创建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; - ``` + ```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; + ``` + + @@ -2300,7 +2318,7 @@ CREATE TABLE us_pro( 存储函数和存储过程是非常相似的。存储函数可以做的事情,存储过程也可以做到! -- 存储函数有返回值,存储过程没有返回值(参数的out其实也相当于是返回数据了) +存储函数有返回值,存储过程没有返回值(参数的out其实也相当于是返回数据了) * 创建存储函数 @@ -2317,7 +2335,7 @@ CREATE TABLE us_pro( DELIMITER ; ``` -* 调用存储函数 +* 调用存储函数,因为有返回值,所以使用 SELECT 调用 ```mysql SELECT 函数名称(实际参数); @@ -2358,7 +2376,7 @@ CREATE TABLE us_pro( ## 触发器 -### 触发器概述 +### 触发概述 触发器是与表有关的数据库对象,在insert/update/delete 之前或之后触发并执行触发器中定义的SQL语句 @@ -2397,16 +2415,16 @@ CREATE TABLE us_pro( DELIMITER ; ``` -* 查看触发器 +* 查看触发器的状态、语法等信息 ```mysql SHOW TRIGGERS; ``` -* 删除触发器 +* 删除触发器,如果没有指定 schema_name,默认为当前数据库 ```mysql - DROP TRIGGER 触发器名称; + DROP TRIGGER [schema_name.]trigger_name; ``` @@ -2415,7 +2433,7 @@ CREATE TABLE us_pro( -### 案例演示 +### 触发演示 通过触发器记录账户表的数据变更日志。包含:增加、修改、删除 @@ -2551,9 +2569,10 @@ CREATE TABLE us_pro( ### 事务概述 -* 事务:一条或多条 SQL 语句组成一个执行单元,其特点是这个单元要么同时成功要么同时失败 +事务:一条或多条 SQL 语句组成一个执行单元,其特点是这个单元要么同时成功要么同时失败 + +单元中的每条 SQL 语句都相互依赖,形成一个整体 -* 单元中的每条 SQL 语句都相互依赖,形成一个整体 * 如果某条 SQL 语句执行失败或者出现错误,那么整个单元就会回滚,撤回到事务最初的状态 * 如果单元中所有的 SQL 语句都执行成功,则事务就顺利执行 @@ -2755,7 +2774,7 @@ CREATE TABLE us_pro( -### 引擎概念 +### 引擎概述 存储引擎的介绍: @@ -2868,27 +2887,174 @@ MySQL支持的存储引擎: ### 索引概述 -MySQL索引:是帮助MySQL高效获取数据的一种数据结构,所以,索引的本质就是数据结构。 +#### 基本介绍 -在表数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式指向数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。 +MySQL官方对索引的定义为:索引(index)是帮助MySQL高效获取数据的一种数据结构。在表数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式指向数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。 索引使用:一张数据表,用于保存数据;一个索引配置文件,用于保存索引;每个索引都指向了某一个数据 -![](https://gitee.com/seazean/images/raw/master/DB/索引有无查找原理.png) +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-索引的介绍.png) + +左边是数据表,一共有两列七条记录,最左边的是数据记录的物理地址(注意逻辑上相邻的记录在磁盘上也并不是一定物理相邻的)。为了加快Col2的查找,可以维护一个右边所示的二叉查找树,每个节点分别包含索引键值和一个指向对应数据的物理地址的指针,这样就可以运用二叉查找快速获取到相应数据 + +索引的优点: +* 类似于书籍的目录索引,提高数据检索的效率,降低数据库的IO成本 +* 通过索引列对数据进行排序,降低数据排序的成本,降低CPU的消耗 +索引的缺点: -索引分类: +* 一般来说索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储在磁盘上 +* 虽然索引大大提高了查询效率,同时却也降低更新表的速度。对表进行INSERT、UPDATE、DELETE操作,MySQL不仅要保存数据,还要保存一下索引文件每次更新添加了索引列的字段,还会调整因为更新所带来的键值变化后的索引信息 + + + +*** + + + +#### 索引分类 + +索引一般的分类如下: - 功能分类 - - 普通索引: 最基本的索引,它没有任何限制 - - 唯一索引:索引列的值必须唯一,但允许有空值。如果是组合索引,则列值组合必须唯一 - - 主键索引:一种特殊的唯一索引,不允许有空值。一般在建表时同时创建主键索引 + - 单列索引:一个索引只包含单个列,一个表可以有多个单列索引 - 组合索引:顾名思义,就是将单列索引进行组合 + - 唯一索引:索引列的值必须唯一,允许有空值。如果是组合索引,则列值组合必须唯一 + - 主键索引:一种特殊的唯一索引,不允许有空值,一般在建表时同时创建主键索引 - 外键索引:只有InnoDB引擎支持外键索引,用来保证数据的一致性、完整性和实现级联操作 - - 全文索引:快速匹配全部文档的方式。InnoDB引擎5.6版本后才支持全文索引,MEMORY引擎不支持 + - 结构分类 - - BTree索引 :MySQL使用最频繁的一个索引数据结构,是InnoDB和MyISAM存储引擎默认的索引类型,底层基于B+Tree 数据结构 - - Hash索引 : MySQL中Memory存储引擎默认支持的索引类型 + - BTree索引:MySQL使用最频繁的一个索引数据结构,是InnoDB和MyISAM存储引擎默认的索引类型,底层基于B+Tree 数据结构 + - Hash索引:MySQL中Memory存储引擎默认支持的索引类型 + - R-tree 索引(空间索引):空间索引是MyISAM引擎的一个特殊索引类型,主要用于地理空间数据类型 + - Full-text (全文索引) :快速匹配全部文档的方式。InnoDB引擎5.6版本后才支持全文索引,MEMORY引擎不支持 + + | 索引 | InnoDB引擎 | MyISAM引擎 | Memory引擎 | + | ----------- | --------------- | ---------- | ---------- | + | BTREE索引 | 支持 | 支持 | 支持 | + | HASH 索引 | 不支持 | 不支持 | 支持 | + | R-tree 索引 | 不支持 | 支持 | 不支持 | + | Full-text | 5.6版本之后支持 | 支持 | 不支持 | + + + +*** + + + +### 索引结构 + +#### 原理 + +索引是在MySQL的存储引擎中实现的,不同的存储引擎的所支持的索引不一定完全相同 + +BTree的索引类型是基于B+Tree树型数据结构的,B+Tree又是BTree数据结构的变种,用在数据库和操作系统中的文件系统,特点是能够保持数据稳定有序 + +磁盘存储: + +* 系统从磁盘读取数据到内存时是以磁盘块(block)为基本单位的,位于同一个磁盘块中的数据会被一次性读取出来,而不是需要什么取什么。 + +- InnoDB存储引擎中有页(Page)的概念,页是其磁盘管理的最小单位,InnoDB存储引擎中默认每个页的大小为16KB。 +- InnoDB引擎将若干个地址连接磁盘块,以此来达到页的大小16KB,在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘I/O次数,提高查询效率。 + + + +*** + + + +#### BTree + +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字母向上分裂到新的节点 + + ![](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) + +* 插入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向上分裂 + + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程8.png) + +BTREE树就已经构建完成了,BTREE树和二叉树相比, 查询数据的效率更高, 因为对于相同的数据量来说,**BTREE的层级结构比二叉树小**,所以搜索速度快 + + + +BTree结构的数据可以让系统高效的找到数据所在的磁盘块,定义一条记录为一个二元组[key, data] ,key为记录的键值,对应表中的主键值,data为一行记录中除主键外的数据。对于不同的记录,key值互不相同,BTree中的每个节点根据实际情况可以包含大量的关键字信息和分支 +![](https://gitee.com/seazean/images/raw/master/DB/索引的原理-BTree.png) + + + +*** + + + +#### B+Tree + +##### 数据结构 + +B+Tree为BTree的变种,B+Tree与BTree的区别为: + +* n叉B+Tree最多含有n个key,而BTree最多含有n-1个key。 + +- 所有非叶子节点只存储键值key信息,可以看作key的索引部分 +- 所有数据都存储在叶子节点,按照key大小顺序排列 + + + + + +##### 优化结构 + +BTree数据结构中每个节点中不仅包含数据的key值,还有data值。每一页的存储空间是有限的,如果data数据较大时将会导致每个节点(即一个页)能存储的key的数量很小,当存储的数据量很大时同样会导致B-Tree的深度较大,增大查询时的磁盘I/O次数,进而影响查询效率 + +MySql索引数据结构对经典的B+Tree进行了优化,在原B+Tree的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的B+Tree,提高**区间访问**的性能 + +区间访问的意思是访问索引为 5 - 15 的数据,这样就可以直接根据相邻节点的指针遍历 + +![](https://gitee.com/seazean/images/raw/master/DB/索引的原理-B+Tree.png) + +通常在B+Tree上有两个头指针,一个指向根节点,另一个指向关键字最小的叶子节点,而且所有叶子节点(即数据节点)之间是一种链式环结构。可以对B+Tree进行两种查找运算: + +- 有范围:对于主键的范围查找和分页查找 +- 有顺序:从根节点开始,进行随机查找 + +实际情况中每个节点可能不能填充满,因此在数据库中,B+Tree的高度一般都在2-4层。MySQL的InnoDB存储引擎在设计时是将根节点常驻内存的,也就是说查找某一键值的行记录时最多只需要1~3次磁盘I/O操作 + +B+Tree优点:提高查询速度,减少磁盘的IO次数,树形结构较小 @@ -2898,16 +3064,17 @@ MySQL索引:是帮助MySQL高效获取数据的一种数据结构,所以, ### 索引操作 -* 创建引擎 - 注意:如果一个表中有一列是主键,那么会默认为其创建主键索引!(主键列不需要单独创建索引) +索引在创建表的时候可以同时创建, 也可以随时增加新的索引 +* 创建索引:如果一个表中有一列是主键,那么会默认为其创建主键索引(主键列不需要单独创建索引) + ```mysql CREATE [UNIQUE|FULLTEXT] INDEX 索引名称 [USING 索引类型] -- 默认是B+TREE ON 表名(列名...); ``` - -* 查看引擎 + +* 查看索引 ```mysql SHOW INDEX FROM 表名; @@ -2968,78 +3135,19 @@ MySQL索引:是帮助MySQL高效获取数据的一种数据结构,所以, -### 索引原理 - -#### 索引概述 - -- 索引是在MySQL的存储引擎中实现的,不同的存储引擎的所支持的索引不一定完全相同 -- BTree的索引类型是基于B+Tree树型数据结构的,B+Tree又是BTree数据结构的变种。通常使用在数据库和操作系统中的文件系统,特点是能够保持数据稳定有序。 - - - -#### 磁盘存储 - -- 系统从磁盘读取数据到内存时是以磁盘块(block)为基本单位的 -- 位于同一个磁盘块中的数据会被一次性读取出来,而不是需要什么取什么。 -- InnoDB存储引擎中有页(Page)的概念,页是其磁盘管理的最小单位。InnoDB存储引擎中默认每个页的大小为16KB。 -- InnoDB引擎将若干个地址连接磁盘块,以此来达到页的大小16KB,在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘I/O次数,提高查询效率。 - - - - - -#### BTree - -* BTree结构的数据可以让系统高效的找到数据所在的磁盘块。为了描述BTree,定义一条记录为一个二元组[key, data] ,key为记录的键值,对应表中的主键值,data为一行记录中除主键外的数据。对于不同的记录,key值互不相同,BTree中的每个节点根据实际情况可以包含大量的关键字信息和分支 - ![](https://gitee.com/seazean/images/raw/master/DB/索引的原理-BTree.png) - - - -#### B+Tree - -* B+Tree是在BTree基础上的一种优化,使其更适合实现外存储索引结构,InnoDB存储引擎就是用B+Tree实现其索引结构。 - -* BTree数据结构中每个节点中不仅包含数据的key值,还有data值。每一页的存储空间是有限的,如果data数据较大时将会导致每个节点(即一个页)能存储的key的数量很小,当存储的数据量很大时同样会导致B-Tree的深度较大,增大查询时的磁盘I/O次数,进而影响查询效率 - -* B+Tree相对于BTree区别: - - - 非叶子节点只存储键值key信息 - - 所有数据都存储在叶子节点 - - 所有叶子节点之间都有一个连接指针 - -* B+Tree的优点:提高查询速度、减少磁盘的IO次数、树形结构较小 - - ![](https://gitee.com/seazean/images/raw/master/DB/索引的原理-B+Tree.png) - - - -通常在B+Tree上有两个头指针,一个指向根节点,另一个指向关键字最小的叶子节点,而且所有叶子节点(即数据节点)之间是一种链式环结构。可以对B+Tree进行两种查找运算: - -- 【有范围】对于主键的范围查找和分页查找 -- 【有顺序】从根节点开始,进行随机查找 - -实际情况中每个节点可能不能填充满,因此在数据库中,B+Tree的高度一般都在2-4层。MySQL的InnoDB存储引擎在设计时是将根节点常驻内存的,也就是说查找某一键值的行记录时最多只需要1~3次磁盘I/O操作。 - - - -*** - - - ### 设计原则 -索引的设计可以遵循一些已有的原则,创建索引的时候请尽量考虑符合这些原则,便于提升索引的使用效率,更高效的使用索引。 +索引的设计可以遵循一些已有的原则,创建索引的时候请尽量考虑符合这些原则,便于提升索引的使用效率,更高效的使用索引 -- 创建索引时的原则 - - 对查询频次较高,且数据量比较大的表建立索引。 - - 使用唯一索引,区分度越高,使用索引的效率越高。 - - 索引字段的选择,最佳候选列应当从where子句的条件中提取,如果where子句中的组合比较多,那么应当挑选最常用、过滤效果最好的列的组合。 - - 使用短索引,索引创建之后也是使用硬盘来存储的,因此提升索引访问的I/O效率,也可以提升总体的访问效率。假如构成索引的字段总长度比较短,那么在给定大小的存储块内可以存储更多的索引值,相应的可以有效的提升MySQL访问索引的I/O效率。 - - 索引可以有效的提升查询数据的效率,但索引数量不是多多益善,索引越多,维护索引的代价越高。对于插入、更新、删除等DML操作比较频繁的表来说,索引过多,会引入相当高的维护代价,降低DML操作的效率,增加相应操作的时间消耗。另外索引过多的话,MySQL也会犯选择困难病,虽然最终仍然会找到一个可用的索引,但无疑提高了选择的代价。 +创建索引时的原则: +- 对查询频次较高,且数据量比较大的表建立索引。 +- 使用唯一索引,区分度越高,使用索引的效率越高。 +- 索引字段的选择,最佳候选列应当从where子句的条件中提取,如果where子句中的组合比较多,那么应当挑选最常用、过滤效果最好的列的组合。 +- 使用短索引,索引创建之后也是使用硬盘来存储的,因此提升索引访问的I/O效率,也可以提升总体的访问效率。假如构成索引的字段总长度比较短,那么在给定大小的存储块内可以存储更多的索引值,相应的可以有效的提升MySQL访问索引的I/O效率。 +- 索引可以有效的提升查询数据的效率,但索引数量不是多多益善,索引越多,维护索引的代价越高。对于插入、更新、删除等DML操作比较频繁的表来说,索引过多,会引入相当高的维护代价,降低DML操作的效率,增加相应操作的时间消耗。另外索引过多的话,MySQL也会犯选择困难病,虽然最终仍然会找到一个可用的索引,但无疑提高了选择的代价。 -* 联合索引的特点 - - MySQL建立联合索引时会遵守最左前缀匹配原则,即最左优先,在检索数据时从联合索引的最左边开始匹配 +* MySQL建立联合索引时会遵守最左前缀匹配原则,即最左优先,在检索数据时从联合索引的最左边开始匹配 + N个列组合而成的组合索引,相当于创建了N个索引,如果查询时where句中使用了组成该索引的**前**几个字段,那么这条查询SQL可以利用组合索引来提升查询效率 ```mysql -- 对name、address、phone列建一个联合索引 @@ -3049,16 +3157,16 @@ MySQL索引:是帮助MySQL高效获取数据的一种数据结构,所以, (name,address) (name) - -- 索引的字段可以是任意顺序的,优化器会帮助我们调整顺序,下面的SQL语句都可以命中索引 + -- 索引的字段可以是任意顺序的,优化器会帮助我们调整顺序,下面的SQL语句可以命中索引 SELECT * FROM user WHERE address = '北京' AND phone = '12345' AND name = '张三'; ``` ```mysql -- 如果联合索引中最左边的列不包含在条件查询中,SQL语句就不会命中索引,比如: - SELECT * FROM user WHERE address = '北京' AND phone = '12345'; + SELECT * FROM user WHERE address = '北京' AND phone = '12345'; ``` - + diff --git a/Java.md b/Java.md index 7ff3b18..2821ef9 100644 --- a/Java.md +++ b/Java.md @@ -10465,7 +10465,7 @@ public class AnnotationDemo01{ 注解解析相关的接口: * Annotation:注解类型,该类是所有注解的父类,注解都是一个Annotation的对象 -* AnnotatedElement:该接口定义了与注解解‘析相关的方法 +* AnnotatedElement:该接口定义了与注解解析相关的方法 * Class、Method、Field、Constructor类成分:实现AnnotatedElement接口,拥有解析注解的能力 API : @@ -14642,7 +14642,7 @@ Exception table: * 在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之则是动态类型语言 -* 静态语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息 +* 静态语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息 * **Java是静态类型语言**(尽管lambda表达式为其增加了动态特性),js,python是动态类型语言 @@ -14660,7 +14660,7 @@ Exception table: 普通调用指令: * invokestatic:调用静态方法,解析阶段确定唯一方法版本 -* invokespecial:调用方法、私有及父类方法,解析阶段确定唯一方法版本 +* invokespecial:调用 init 方法、私有及父类方法,解析阶段确定唯一方法版本 * invokevirtual:调用所有虚方法 * invokeinterface:调用接口方法 @@ -14741,7 +14741,7 @@ public class Demo { 1. 先通过栈帧中的对象引用找到对象 2. 分析对象头,找到对象的实际 Class -3. Class 结构中有 vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了 +3. Class 结构中有虚拟方法表,它在类加载的链接阶段就已经根据方法的重写规则生成好了 4. 查表得到方法的具体地址 5. 执行方法的字节码 @@ -14751,7 +14751,7 @@ public class Demo { * 前者是通过方法重载实现,后者是通过方法覆盖实现(子类覆盖父类方法,虚方法表) * 虚方法:运行时动态绑定的方法 -虚方法表:在面向对象编程中,会频繁使用到**动态分派**,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适目标就会影响到执行效率。为了提高性能,JVM采用在类的方法区建立一个虚方法表(virtual method table)来实现,使用索引表来代替查找,每个类中都有一个虚方法表,表中存放着各个方法的实际入口 +虚方法表:在面向对象编程中,会频繁使用到**动态分派**,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适目标就会影响到执行效率。为了提高性能,JVM采用在类的方法区建立一个虚方法表(virtual method table)来实现,使用索引表来代替查找,**每个类中都有一个虚方法表**,表中存放着各个方法的实际入口 ![](https://gitee.com/seazean/images/raw/master/Java/JVM-方法调用动态分配.png) diff --git a/Tool.md b/Tool.md index 672b002..f017b6e 100644 --- a/Tool.md +++ b/Tool.md @@ -840,7 +840,7 @@ gpasswd 是 Linux 工作组文件 /etc/group 和 /etc/gshadow 管理工具,用 ## 系统管理 -### 时间管理 +### date date 可以用来显示或设定系统的日期与时间 @@ -858,7 +858,7 @@ date 可以用来显示或设定系统的日期与时间 -### id命令 +### id id会显示用户以及所属群组的实际与有效ID。若两个ID相同,则仅显示实际ID。若仅指定用户名称,则显示目前用户的ID。 @@ -874,7 +874,7 @@ id会显示用户以及所属群组的实际与有效ID。若两个ID相同, -### sudo命令 +### sudo sudo:控制用户对系统命令的使用权限,root允许的操作。通过sudo可以提高普通用户的操作权限 @@ -893,7 +893,7 @@ sudo:控制用户对系统命令的使用权限,root允许的操作。通过su -### top命令 +### top top:用于实时显示 process 的动态 @@ -925,7 +925,7 @@ top:用于实时显示 process 的动态 -### ps命令 +### ps Linux 系统中查看进程使用情况的命令是 **ps** 指令 @@ -956,7 +956,7 @@ Linux 系统中查看进程使用情况的命令是 **ps** 指令 -### kill命令 +### kill Linux kill命令用于删除执行中的程序或工作(可强制中断) @@ -980,7 +980,7 @@ Linux kill命令用于删除执行中的程序或工作(可强制中断) -### 关机指令 +### shutdown shutdown命令可以用来进行关闭系统,并且在关机以前传送讯息给所有使用者正在执行的程序,shutdown 也可以用来重开机 @@ -1007,7 +1007,7 @@ shutdown命令可以用来进行关闭系统,并且在关机以前传送讯息 -### 重启命令 +### reboot reboot命令用于用来重新启动计算机 @@ -1021,7 +1021,7 @@ reboot命令用于用来重新启动计算机 -### who命令 +### who who命令用于显示系统中有哪些使用者正在上面,显示的资料包含了使用者 ID、使用的终端机、上线时间、呆滞时间、CPU 使用量、动作等等 @@ -1038,7 +1038,7 @@ who命令用于显示系统中有哪些使用者正在上面,显示的资料 -### systemctl命令 +### systemctl 命令:systemctl [command] [unit] @@ -1073,7 +1073,7 @@ who命令用于显示系统中有哪些使用者正在上面,显示的资料 -### timedatectl命令 +### timedatectl timedatectl用于控制系统时间和日期。可以查询和更改系统时钟于设定,同时可以设定和修改时区信息。在实际开发过程中,系统时间的显示会和实际出现不同步;我们为了校正服务器时间、时区会使用timedatectl命令 @@ -1095,7 +1095,7 @@ NTP即Network Time Protocol(网络时间协议),是一个互联网协议 -### clear命令 +### clear clear命令用于清除屏幕 @@ -1103,7 +1103,7 @@ clear命令用于清除屏幕 -### exit命令 +### exit exit命令用于退出目前的shell。执行exit可使shell以指定的状态值退出。若不设置状态值参数,则shell以预设值退出。状态值0代表执行成功,其他值代表执行失败。exit也可用在script,离开正在执行的script,回到shell。 From f2ec18427e97b02aea8a79351711d997f79c2c8f Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 19 May 2021 23:14:18 +0800 Subject: [PATCH 009/242] Update Java Notes --- DB.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/DB.md b/DB.md index d54749a..90b66a9 100644 --- a/DB.md +++ b/DB.md @@ -1708,7 +1708,6 @@ CREATE TABLE us_pro( WHERE c1.cid=c2.id; ``` - @@ -1737,9 +1736,9 @@ CREATE TABLE us_pro( -## 存储过程 +## 过程 -### 存储概述 +### 存储过程 存储过程和函数:存储过程和函数是事先经过编译并存储在数据库中的一段 SQL 语句的集合 @@ -2374,9 +2373,9 @@ LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定 -## 触发器 +## 触发 -### 触发概述 +### 触发器 触发器是与表有关的数据库对象,在insert/update/delete 之前或之后触发并执行触发器中定义的SQL语句 @@ -2742,7 +2741,7 @@ LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定 -## 存储引擎 +## 引擎 ### 体系结构 @@ -2774,7 +2773,7 @@ LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定 -### 引擎概述 +### 存储引擎 存储引擎的介绍: From e566fa56d4407009716d223f3716b2a7b505b681 Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 20 May 2021 15:13:14 +0800 Subject: [PATCH 010/242] Update Java Notes --- DB.md | 162 +++++++++++++++++++++++++++++++++++++------------------- Java.md | 4 +- SSM.md | 31 +++-------- 3 files changed, 117 insertions(+), 80 deletions(-) diff --git a/DB.md b/DB.md index 90b66a9..1fc2859 100644 --- a/DB.md +++ b/DB.md @@ -130,7 +130,7 @@ MySQL配置: ## 单表 -### SQL介绍 +### SQL - SQL @@ -174,7 +174,7 @@ MySQL配置: ### DDL -#### 操作数据库 +#### 数据库 * R(Retrieve):查询 @@ -283,7 +283,7 @@ MySQL配置: -#### 操作数据表 +#### 数据表 - R(Retrieve):查询 @@ -923,7 +923,7 @@ LIMIT:分页限定 -### 主键自增约束 +### 主键自增 主键自增约束可以为空,并自动增长。删除某条数据不影响自增的下一个数值,依然按照前一个值自增。 @@ -1088,14 +1088,22 @@ LIMIT:分页限定 -### 外键级联操作 +### 外键级联 -级联操作:当把主表中的数据进行删除或更新时,从表中有关联的数据也会随之删除(一般不建议使用) +级联操作:当把主表中的数据进行删除或更新时,从表中有关联的数据的相应操作,包括 RESTRICT、CASCADE、SET NULL 和 NO ACTION + +* RESTRICT 和 NO ACTION相同, 是指限制在子表有关联记录的情况下, 父表不能更新 + +* CASCADE 表示父表在更新或者删除时,更新或者删除子表对应的记录 + +* SET NULL 则表示父表在更新或者删除的时候,子表的对应字段被SET NULL + +级联操作: * 添加级联更新 ```mysql - ALTER TABLE 表名 ADD CONSTRAINT 外键名 FOREIGN KEY (本表外键列名) REFERENCES 主表名(主表主键列名) ON UPDATE CASCADE; + ALTER TABLE 表名 ADD CONSTRAINT 外键名 FOREIGN KEY (本表外键列名) REFERENCES 主表名(主表主键列名) ON UPDATE [CASCADE | RESTRICT | SET NULL]; ``` * 添加级联删除 @@ -2595,7 +2603,8 @@ LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定 - 提交:没出现问题,数据进行更新 - 回滚:出现问题,数据恢复到开启事务时的状态 - + +事务操作: * 开启事务 @@ -2745,27 +2754,37 @@ LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定 ### 体系结构 -体系结构详解 +体系结构详解: + +* 第一层:网络连接层 + * 一些客户端和链接服务,包含本地socket 通信和大多数基于客户端/服务端工具实现的 TCP/IP 通信,主要完成一些类似于连接处理、授权认证、及相关的安全方案 + * 在该层上引入了线程池的概念,为通过认证安全接入的客户端提供线程 + * 在该层上实现基于SSL的安全链接,服务器也会为安全接入的每个客户端验证它所具有的操作权限 -- 客户端连接 - - 支持接口:支持的客户端连接,例如C、Java、PHP等语言来连接MySQL数据库 -- 第一层:网络连接层 - - 连接池:管理、缓冲用户的连接,线程处理等需要缓存的需求。 - - 例如:当客户端发送一个请求连接,会从连接池中获取一个连接进行使用。 - 第二层:核心服务层 - - 管理服务和工具:系统的管理和控制工具,例如备份恢复、复制、集群等。 - - SQL接口:接受SQL命令,并且返回查询结果。 - - 查询解析器:验证和解析SQL命令,例如过滤条件、语法结构等。 - - 查询优化器:在执行查询之前,使用默认的一套优化机制进行优化sql语句 - - 缓存:如果缓存当中有想查询的数据,则直接将缓存中的数据返回。没有的话再重新查询! + * 完成大多数的核心服务功能,如SQL接口,并完成缓存的查询,SQL的分析和优化,部分内置函数的执行 + * 所有跨存储引擎的功能在这一层实现,如过程、函数等 + * 在该层服务器会解析查询并创建相应的内部解析树,并对其完成相应的优化如确定表的查询顺序,是否利用索引等, 最后生成相应的执行操作 + * 服务器还会查询内部的缓存,如果缓存空间足够大,可以在大量读操作的环境中提升系统的性能 - 第三层:存储引擎层 - - 插件式存储引擎:管理和操作数据的一种机制,包括(存储数据、如何更新、查询数据等) + - 存储引擎真正的负责了MySQL中数据的存储和提取,服务器通过API和存储引擎进行通信 + - 不同的存储引擎具有不同的功能,这样我们可以根据自己的需要,来选取合适的存储引擎 - 第四层:系统文件层 + - 数据存储层,主要是将数据存储在文件系统之上,并完成与存储引擎的交互 - 文件系统:配置文件、数据文件、日志文件、错误文件、二进制文件等等的保存 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL体系结构.png) +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-体系结构.png) +整个MySQL Server由以下组成 +- Connection Pool : 连接池组件 +- Management Services & Utilities : 管理服务和工具组件 +- SQL Interface : SQL接口组件 +- Parser : 查询分析器组件 +- Optimizer : 优化器组件 +- Caches & Buffers : 缓冲池组件 +- Pluggable Storage Engines : 存储引擎 +- File System : 文件系统 @@ -2775,17 +2794,19 @@ LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定 ### 存储引擎 +对比其他数据库,MySQL的架构可以在不同场景应用并发挥良好作用,主要体现在存储引擎,插件式的存储引擎架构将查询处理和其他的系统任务以及数据的存储提取分离,可以针对不同的存储需求可以选择最优的存储引擎 + 存储引擎的介绍: -- MySQL数据库使用不同的机制存取表文件 , 机制的差别在于不同的存储方式、索引技巧、锁定水平等不同的功能和能力,在MySQL中 , 将这些不同的技术及配套的功能称为**存储引擎** -- Oracle , SqlServer等数据库只有一种存储引擎 , 而MySQL针对不同的需求, 配置不同的存储引擎 , 就会让数据库采取了不同的处理数据的方式和扩展功能。 +- MySQL数据库使用不同的机制存取表文件 , 机制的差别在于不同的存储方式、索引技巧、锁定水平等不同的功能和能力,在MySQL中,将这些不同的技术及配套的功能称为**存储引擎** +- Oracle , SqlServer等数据库只有一种存储引擎,MySQL提供了插件式的存储引擎架构,所以MySQL存在多种存储引擎 , 就会让数据库采取了不同的处理数据的方式和扩展功能 - 在关系型数据库中数据的存储是以表的形式存进行,所以存储引擎也称为表类型(存储和操作此表的类型) - 通过选择不同的引擎,能够获取最佳的方案, 也能够获得额外的速度或者功能,提高程序的整体效果。 MySQL支持的存储引擎: -- MySQL5.7支持的引擎包括:InnoDB、MyISAM、MEMORY、Archive、Federate、CSV、BLACKHOLE等 -- 常用引擎:InnoDB、MyISAM、MEMORY +- MySQL支持的引擎包括:InnoDB、MyISAM、MEMORY、Archive、Federate、CSV、BLACKHOLE等 +- MySQL5.5之前的默认存储引擎是MyISAM,5.5之后就改为了InnoDB @@ -2795,41 +2816,73 @@ MySQL支持的存储引擎: ### 引擎对比 -- MyISAM存储引擎 +MyISAM存储引擎 + +* 特点:不支持事务和外键,读取速度快,节约资源 +* 应用场景:查询和插入操作为主,只有很少更新和删除操作,并对事务的完整性、并发性要求不高 +* 存储方式: + * 每个MyISAM在磁盘上存储成3个文件,其文件名都和表名相同,拓展名不同 + * 表结构保存在.frm文件中,表数据保存在.MYD文件中,索引保存在.MYI文件中 + +InnoDB存储引擎:(MySQL5.5版本后默认的存储引擎) - * 特点:不支持事务和外键,读取速度快,节约资源 - * 应用场景:查询和插入操作为主,只有很少更新和删除操作,并对事务的完整性、并发性要求不高 - * 结构:表结构保存在.frm文件中,表数据保存在.MYD文件中,索引保存在.MYI文件中 +- 特点:支持事务和外键操作,支持并发控制。对比MyISAM的存储引擎,InnoDB写的处理效率差一些,并且会占用更多的磁盘空间以保留数据和索引 +- 应用场景:对事务的完整性有比较高的要求,在并发条件下要求数据的一致性,读写频繁的操作 +- 存储方式: + - 使用共享表空间存储, 这种方式创建的表的表结构保存在.frm文件中, 数据和索引保存在 innodb_data_home_dir 和 innodb_data_file_path定义的表空间中,可以是多个文件 + - 使用多表空间存储, 这种方式创建的表的表结构存在 .frm 文件中,但每个表的数据和索引单独保存在 .ibd 中 -- InnoDB存储引擎:(MySQL5.5版本后默认的存储引擎) +MEMORY存储引擎: - - 特点:支持事务和外键操作,占用磁盘空间大,支持并发控制 - - 应用场景:对事务的完整性有比较高的要求,在并发条件下要求数据的一致性,读写频繁的操作 - - 结构:表结构保存在.frm文件中,如果是共享表空间,数据和索引保存在 innodb_data_home_dir 和 innodb_data_file_path定义的表空间中,可以是多个文件。如果是多表空间存储,每个表的数据和索引单独保存在 .ibd 中 +- 特点:每个MEMORY表实际对应一个磁盘文件,格式是.frm ,该文件中只存储表的结构,表数据保存在内存中,且默认使用HASH索引,这样有利于数据的快速处理,在需要快速定位记录可以提供更快的访问,但是服务一旦关闭,表中的数据就会丢失,数据存储不安全 +- 应用场景:通常用于更新不太频繁的小表,用以快速得到访问结果,类似缓存 +- 存储方式:表结构保存在.frm中 -- MEMORY存储引擎: +MERGE存储引擎 + +* 特点: + + * 是一组MyISAM表的组合,这些MyISAM表必须结构完全相同,通过将不同的表分布在多个磁盘上,有效的改善MERGE表的访问效率 + * MERGE表本身并没有存储数据,对MERGE类型的表可以进行查询、更新、删除操作,这些操作实际上是对内部的MyISAM表进行的。 + +* 应用场景:将一系列等同的MyISAM表以逻辑方式组合在一起,并作为一个对象引用他们,适合做数据仓库 + +* 操作方式: + + * 插入操作是通过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; + ``` - - 特点:所有数据保存在内存中,在需要快速定位记录可以提供更快的访问,但是不安全 - - 应用场景:通常用于更新不太频繁的小表,用以快速得到访问结果 - - 结构:表结构保存在.frm中 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MERGE.png) - | 特性 | MyISAM | InnoDB | MEMORY | - | ------------ | ---------------------------- | ------------- | ------------------ | - | 存储限制 | 有(平台对文件系统大小的限制) | 64TB | 有(平台的内存限制) | - | **事务安全** | **不支持** | **支持** | **不支持** | - | **锁机制** | **表锁** | **表锁/行锁** | **表锁** | - | B+Tree索引 | 支持 | 支持 | 支持 | - | 哈希索引 | 不支持 | 不支持 | 支持 | - | 全文索引 | 支持 | 支持 | 不支持 | - | **集群索引** | **不支持** | **支持** | **不支持** | - | 数据索引 | 不支持 | 支持 | 支持 | - | 数据缓存 | 不支持 | 支持 | N/A | - | 索引缓存 | 支持 | 支持 | N/A | - | 数据可压缩 | 支持 | 不支持 | 不支持 | - | 空间使用 | 低 | 高 | N/A | - | 内存使用 | 低 | 高 | 中等 | - | 批量插入速度 | 高 | 低 | 高 | - | **外键** | **不支持** | **支持** | **不支持** | +| 特性 | MyISAM | InnoDB | MEMORY | +| ------------ | ---------------------------- | ------------- | ------------------ | +| 存储限制 | 有(平台对文件系统大小的限制) | 64TB | 有(平台的内存限制) | +| **事务安全** | **不支持** | **支持** | **不支持** | +| **锁机制** | **表锁** | **表锁/行锁** | **表锁** | +| B+Tree索引 | 支持 | 支持 | 支持 | +| 哈希索引 | 不支持 | 不支持 | 支持 | +| 全文索引 | 支持 | 支持 | 不支持 | +| 集群索引 | 不支持 | 支持 | 不支持 | +| 数据索引 | 不支持 | 支持 | 支持 | +| 数据缓存 | 不支持 | 支持 | N/A | +| 索引缓存 | 支持 | 支持 | N/A | +| 数据可压缩 | 支持 | 不支持 | 不支持 | +| 空间使用 | 低 | 高 | N/A | +| 内存使用 | 低 | 高 | 中等 | +| 批量插入速度 | 高 | 低 | 高 | +| **外键** | **不支持** | **支持** | **不支持** | @@ -2845,6 +2898,7 @@ MySQL支持的存储引擎: ```mysql SHOW ENGINES; + SHOW VARIABLES LIKE '%storage_engine%'; -- 查看Mysql数据库默认的存储引擎 ``` * 查询某个数据库中所有数据表的存储引擎 diff --git a/Java.md b/Java.md index 2821ef9..7dc4058 100644 --- a/Java.md +++ b/Java.md @@ -14672,7 +14672,7 @@ Exception table: 指令总结: -* 前四条指令固化在虚拟机内部,方法的调用执行不可干预,而invokedynamic指令则支持用户确定方法 +* 前四条指令固化在虚拟机内部,方法的调用执行不可干预,根据方法的符号引用链接到具体的目标方法,而invokedynamic指令则支持用户确定方法 * invokestatic指令和invokespecial指令调用的方法称为非虚方法,属于静态绑定 * 普通成员方法是由 invokevirtual 调用,属于**动态绑定**,即支持多态 @@ -14719,7 +14719,7 @@ public class Demo { * new 是在堆中创建对象,执行成功会将**对象引用**压入操作数栈 -* dup 是复制操作数栈栈顶的内容,本例即为**对象引用**,为什么需要两份引用呢? +* dup 是复制操作数栈栈顶的内容,本例即为对象引用,为什么需要两份引用呢? * 一个要配合 invokespecial 调用该对象的构造方法 :()V (会消耗掉栈顶一个引用) diff --git a/SSM.md b/SSM.md index b5de0d0..b12c258 100644 --- a/SSM.md +++ b/SSM.md @@ -5601,7 +5601,7 @@ public class UserServiceDecorator implements UserService{ #### Proxy -JDKProxy动态代理是针对对象做代理,要求原始对象具有接口实现,并对接口方法进行增强,因为代理类继承Proxy +JDKProxy动态代理是针对对象做代理,要求原始对象具有接口实现,并对接口方法进行增强,因为**代理类继承Proxy** 静态代理和动态代理的区别: @@ -5811,11 +5811,7 @@ MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ( #### 传播行为 -事务传播行为描述的是事务协调员对事务管理员所携带事务的处理态度 - -![](https://gitee.com/seazean/images/raw/master/Frame/事务传播行为.png) - -**事务传播行为是为了解决业务层方法之间互相调用的事务问题**: +事务传播行为是为了解决业务层方法之间互相调用的事务问题: * 当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。 @@ -6444,7 +6440,6 @@ public void addAccount{} * `@Transactional` 注解只有作用到 public 方法上事务才生效 * 不推荐在接口上使用`@Transactional` 注解 原因:在接口上使用注解,只有**在使用基于接口的代理时才会生效**,因为注解是不能继承的,这就意味着如果正在使用基于类的代理时,那么事务的设置将不能被基于类的代理所识别 -* 避免同一个类中调用 `@Transactional` 注解的方法,这样会导致事务失效 * 正确的设置 `@Transactional` 的 rollbackFor 和 propagation 属性,否则事务可能会回滚失败 面试题:**事务不生效的问题** @@ -6476,10 +6471,10 @@ public void addAccount{} 原因:Spring的默认的事务规则是遇到运行异常(RuntimeException)和程序错误(Error)才会回滚。想针对非检测异常进行事务回滚,可以在@Transactional 注解里使用rollbackFor 属性明确指定异常 -* 情况6:Spring的事务传播策略在**内部方法**调用时将不起作用,事务注解加到要调用方法上 +* 情况6:Spring的事务传播策略在**内部方法**调用时将不起作用,在一个Service内部,事务方法之间的嵌套调用,普通方法和事务方法之间的嵌套调用,都不会开启新的事务。事务注解要加到调用方法上才生效 + + 原因:Spring的事务都是使用AOP代理的模式,动态代理最终是要调用原始对象,而原始对象在去调用方法时是不会再触发代理,就是方法调用**本对象**的另一个方法,没有通过代理类直接调用,而且事务也就无法生效 - 原因:Spring的事务都是使用AOP代理的模式,仅有外部方法调用过程才会被代理截获,事务才会有效,就是方法调用本对象的另一个方法,没有通过代理类,事务也就无法生效 - ```java @Transactional public int add(){ @@ -6488,7 +6483,7 @@ public void addAccount{} //注解添加在update方法上无效,需要添加到add()方法上 public int update(){} ``` - + @@ -6560,19 +6555,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 From 2f690f4e0499fc43dd914c24f6ea1d21a5bcbebb Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 20 May 2021 21:29:22 +0800 Subject: [PATCH 011/242] Update Java Notes --- SSM.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/SSM.md b/SSM.md index b12c258..0162d64 100644 --- a/SSM.md +++ b/SSM.md @@ -1264,13 +1264,13 @@ PageInfo相关API: 3. 配置statement上面的useCache属性 - 映射文件中的`标签中设置`useCache=”true”`代表当前statement要使用二级缓存。 注意:针对每次查询都需要最新的数据sql,要设置成useCache=false,禁用二级缓存。 ```xml - + ``` 4. 要进行二级缓存的类必须实现java.io.Serializable 接口,可以使用序列化方式来保存对象。 From 5ddee0f8470f246d324261bb36d9865cd8680149 Mon Sep 17 00:00:00 2001 From: Seazean Date: Fri, 21 May 2021 21:36:37 +0800 Subject: [PATCH 012/242] Update Java Notes --- DB.md | 1370 ++++++++++++++++++++++++++++++++++++++++++++++--------- Java.md | 15 +- Web.md | 10 +- 3 files changed, 1171 insertions(+), 224 deletions(-) diff --git a/DB.md b/DB.md index 1fc2859..b1473ca 100644 --- a/DB.md +++ b/DB.md @@ -122,7 +122,41 @@ MySQL配置: - + +*** + + + +### 备份 + +#### 命令行 + +* 备份命令:mysqldump -u root -p 数据库名称 > 文件保存路径 + +* 恢复 + 1. 登录MySQL数据库:`mysql -u root p` + 2. 删除已经备份的数据库 + 3. 重新创建与备份数据库名称相同的数据库 + 4. 使用该数据库 + 5. 导入文件执行:`source 备份文件全路径` + + + +#### 图形化 + +* 备份 + + ![图形化界面备份](https://gitee.com/seazean/images/raw/master/DB/图形化界面备份.png) + +* 恢复 + + ![图形化界面恢复](https://gitee.com/seazean/images/raw/master/DB/图形化界面恢复.png) + + + + + + *** @@ -1261,12 +1295,9 @@ WHERE 条件... ``` -笛卡儿积查询: -```mysql --- 标准语法 -SELECT 列名 FROM 表名1,表名2,...; -``` + +*** @@ -1286,7 +1317,12 @@ SELECT 列名 FROM 表名1,表名2,...; SELECT 列名 FROM 表名1,表名2 WHERE 条件; ``` - + + + +*** + + #### 外连接查询 @@ -1302,7 +1338,12 @@ SELECT 列名 FROM 表名1,表名2,...; SELECT 列名 FROM 表名1 RIGHT [OUTER] JOIN 表名2 ON 条件; ``` - + + + +*** + + #### 子查询 @@ -1335,7 +1376,12 @@ SELECT 列名 FROM 表名1,表名2,...; u.id=o.uid; ``` - + + + +*** + + #### 自关联查询 @@ -1579,6 +1625,8 @@ CREATE TABLE us_pro( + + *** @@ -1755,6 +1803,7 @@ CREATE TABLE us_pro( * 提高代码的复用性 * 减少数据在数据库和应用服务器之间的传输,提高传输效率 * 减少代码层面的业务处理 +* **一次编译永久有效** 存储过程和函数的区别: @@ -2777,14 +2826,14 @@ LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定 整个MySQL Server由以下组成 -- Connection Pool : 连接池组件 -- Management Services & Utilities : 管理服务和工具组件 -- SQL Interface : SQL接口组件 -- Parser : 查询分析器组件 -- Optimizer : 优化器组件 -- Caches & Buffers : 缓冲池组件 -- Pluggable Storage Engines : 存储引擎 -- File System : 文件系统 +- Connection Pool:连接池组件 +- Management Services & Utilities:管理服务和工具组件 +- SQL Interface:SQL接口组件 +- Parse:查询分析器组件 +- Optimizer:优化器组件 +- Caches & Buffers:缓冲池组件 +- Pluggable Storage Engines:存储引擎 +- File System:文件系统 @@ -2940,7 +2989,7 @@ MERGE存储引擎 ### 索引概述 -#### 基本介绍 + MySQL官方对索引的定义为:索引(index)是帮助MySQL高效获取数据的一种数据结构。在表数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式指向数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。 @@ -2965,7 +3014,7 @@ MySQL官方对索引的定义为:索引(index)是帮助MySQL高效获取 -#### 索引分类 +### 索引分类 索引一般的分类如下: @@ -2989,6 +3038,12 @@ MySQL官方对索引的定义为:索引(index)是帮助MySQL高效获取 | R-tree 索引 | 不支持 | 支持 | 不支持 | | Full-text | 5.6版本之后支持 | 支持 | 不支持 | +组合索引图示:根据身高年龄建立的组合索引(height,age) + +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-组合索引图.png) + + + *** @@ -3105,6 +3160,8 @@ 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亿 条记录 + 实际情况中每个节点可能不能填充满,因此在数据库中,B+Tree的高度一般都在2-4层。MySQL的InnoDB存储引擎在设计时是将根节点常驻内存的,也就是说查找某一键值的行记录时最多只需要1~3次磁盘I/O操作 B+Tree优点:提高查询速度,减少磁盘的IO次数,树形结构较小 @@ -3119,7 +3176,7 @@ B+Tree优点:提高查询速度,减少磁盘的IO次数,树形结构较小 索引在创建表的时候可以同时创建, 也可以随时增加新的索引 -* 创建索引:如果一个表中有一列是主键,那么会默认为其创建主键索引(主键列不需要单独创建索引) +* 创建索引:如果一个表中有一列是主键,那么会**默认为其创建主键索引**(主键列不需要单独创建索引) ```mysql CREATE [UNIQUE|FULLTEXT] INDEX 索引名称 @@ -3136,7 +3193,7 @@ B+Tree优点:提高查询速度,减少磁盘的IO次数,树形结构较小 * 添加索引 ```mysql - -- 普通索引 + -- 单列索引 ALTER TABLE 表名 ADD INDEX 索引名称(列名); -- 组合索引 @@ -3227,294 +3284,1183 @@ B+Tree优点:提高查询速度,减少磁盘的IO次数,树形结构较小 -## 锁 +## 优化 -### 锁的概述 +### 优化步骤 -锁机制:数据库为了保证数据的一致性,在共享的资源被并发访问时变得安全有序所设计的一种规则 +#### 执行频率 -作用:锁机制类似于多线程中的同步,可以保证数据的一致性和安全性 +随着生产数据量的急剧增长,很多 SQL 语句逐渐显露出性能问题,对生产的影响也越来越大,此时有问题的 SQL 语句就成为整个系统性能的瓶颈,因此必须要进行优化 -锁的分类: +MySQL 客户端连接成功后,查询服务器状态信息: -- 按操作分类: - - 共享锁:也叫读锁。对同一份数据,多个事务读操作可以同时加锁而不互相影响 ,但不能修改数据 - - 排他锁:也叫写锁。当前的操作没有完成前,会阻断其他操作的读取和写入 -- 按粒度分类: - - 表级锁:会锁定整个表。开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低。偏向于MyISAM存储引擎! - - 行级锁:会锁定当前操作行。开销大,加锁慢;会出现死锁;锁定力度小,发生锁冲突概率低,并发度高。偏向于InnoDB存储引擎! - - 页级锁:锁的力度、发生冲突的概率和加锁开销介于表锁和行锁之间,会出现死锁,并发性能一般。 -- 按使用方式分类: - - 悲观锁:每次查询数据时都认为别人会修改,很悲观,所以查询时加锁。 - - 乐观锁:每次查询数据时都认为别人不会修改,很乐观,但是更新时会判断一下在此期间别人有没有去更新这个数据 +```mysql +SHOW [SESSION|GLOBAL] STATUS LIKE ''; +-- SESSION: 显示当前会话连接的统计结果,默认参数 +-- GLOBAL: 显示自数据库上次启动至今的统计结果 +``` -* 不同存储引擎支持的锁 +* 查看SQL执行频率: - | 存储引擎 | 表级锁 | 行级锁 | 页级锁 | - | -------- | ------ | ------ | ------ | - | MyISAM | 支持 | 不支持 | 不支持 | - | InnoDB | 支持 | 支持 | 不支持 | - | MEMORY | 支持 | 不支持 | 不支持 | - | BDB | 支持 | 不支持 | 支持 | + ```mysql + SHOW STATUS LIKE 'Com_____'; + ``` + Com_xxx 表示每种语句执行的次数 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SQL语句执行频率.png) -*** +* 查询SQL语句影响的行数: + ```mysql + SHOW STATUS LIKE 'Innodb_rows_%'; + ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SQL语句影响的行数.png) -### InnoDB +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 | 慢查询的次数 | -* 加入共享锁 - ```mysql - SELECT语句 LOCK IN SHARE MODE; - -- InnoDB引擎默认是行锁 - -- InnoDB引擎如果不采用带索引的列。则会提升为表锁 - ``` -* 数据准备 +**** - ```mysql - -- 创建db13数据库 - CREATE DATABASE db13; - - -- 使用db13数据库 - USE db13; - - -- 创建student表 - CREATE TABLE student( - id INT PRIMARY KEY AUTO_INCREMENT, - NAME VARCHAR(10), - age INT, - score INT - ); - -- 添加数据 - INSERT INTO student VALUES (NULL,'张三',23,99),(NULL,'李四',24,95), - (NULL,'王五',25,98),(NULL,'赵六',26,97); + + +#### 定位低效 + +通过以下两种方式定位执行效率较低的 SQL 语句 + +* 慢日志查询: 慢查询日志在查询结束以后才纪录,执行效率出现问题时查询日志并不能定位问题 + + 配置文件修改:修改.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 - -- 开启事务 - START TRANSACTION; - - -- 查询id为1的数据记录。加入共享锁 - SELECT * FROM student WHERE id=1 LOCK IN SHARE MODE; - - -- 查询分数为99分的数据记录。加入共享锁,不采用带索引的列,提升为表锁 - SELECT * FROM student WHERE score=99 LOCK IN SHARE MODE; - - -- 提交事务 - COMMIT; + mysql> SET slow_query_log=ON; + mysql> SET GLOBAL slow_query_log=ON; ``` + 查看是否配置成功: + ```mysql - -- 窗口2 - -- 开启事务 - START TRANSACTION; - - -- 查询id为1的数据记录(普通查询,可以查询) - SELECT * FROM student WHERE id=1; - - -- 查询id为1的数据记录,并加入共享锁(可以查询。共享锁和共享锁兼容) - SELECT * FROM student WHERE id=1 LOCK IN SHARE MODE; - - -- 修改id为1的姓名为张三三(不能修改,会出现锁的情况。只有窗口1提交事务后,才能修改成功) - UPDATE student SET NAME='张三三' WHERE id = 1; - - -- 修改id为2的姓名为李四四(修改成功,InnoDB引擎默认是行锁) - UPDATE student SET NAME='李四四' WHERE id = 2; - - -- 修改id为3的姓名为王五五(注意:InnoDB引擎如果不采用带索引的列。则会提升为表锁) - UPDATE student SET NAME='王五五' WHERE id = 3; - - -- 提交事务 - COMMIT; + SHOW VARIABLES LIKE '%query%' ``` +* SHOW PROCESSLIST:查看当前MySQL在进行的线程,包括线程的状态、是否锁表等,可以实时地查看 SQL 的执行情况,同时对一些锁表操作进行优化 + ![](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语句,是判断问题语句的一个重要依据 | -排他锁:加锁的数据,不能被其他事务加锁查询或修改 -* 加入排他锁 - ```mysql - SELECT语句 FOR UPDATE; - ``` -* 排他锁演示 - ```mysql - -- 窗口1 - -- 开启事务 - START TRANSACTION; - - -- 查询id为1的数据记录,并加入排他锁 - SELECT * FROM student WHERE id=1 FOR UPDATE; - - -- 提交事务 - COMMIT; - ``` +*** - ```mysql - -- 窗口2 - -- 开启事务 - START TRANSACTION; - - -- 查询id为1的数据记录(普通查询没问题) - SELECT * FROM student WHERE id=1; - - -- 查询id为1的数据记录,并加入共享锁(不能查询。因为排他锁不能和其他锁共存) - SELECT * FROM student WHERE id=1 LOCK IN SHARE MODE; - - -- 查询id为1的数据记录,并加入排他锁(不能查询。因为排他锁不能和其他锁共存) - SELECT * FROM student WHERE id=1 FOR UPDATE; - - -- 修改id为1的姓名为张三(不能修改,会出现锁的情况。只有窗口1提交事务后,才能修改成功) - UPDATE student SET NAME='张三' WHERE id=1; - - -- 提交事务 - COMMIT; - ``` - +#### EXPLAIN +##### 执行计划 -#### 兼容性 +通过 EXPLAIN 命令获取执行 SQL 语句的信息,包括在 SELECT 语句执行过程中如何连接和连接的顺序 -锁的兼容性 +查询SQL语句的执行计划: -- 共享锁和共享锁 兼容 -- 共享锁和排他锁 冲突 -- 排他锁和排他锁 冲突 -- 排他锁和共享锁 冲突 +```mysql +EXPLAIN SELECT * FROM table_1 WHERE id = 1; +``` +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-explain查询SQL语句的执行计划.png) +| 字段 | 含义 | +| ------------- | ------------------------------------------------------------ | +| id | select查询的序列号,表示查询中执行select子句或操作表的顺序 | +| select_type | 表示 SELECT 的类型 | +| table | 输出结果集的表 | +| type | 表示表的连接类型 | +| possible_keys | 表示查询时,可能使用的索引 | +| key | 表示实际使用的索引 | +| key_len | 索引字段的长度 | +| ref | 列与索引的比较,表示表的连接匹配条件,即哪些列或常量被用于查找索引列上的值 | +| rows | 扫描出的行数,表示MySQL根据表统计信息及索引选用情况,估算的找到所需的记录所需要读取的行数 | +| filtered | 按表条件过滤的行百分比 | +| extra | 执行情况的说明和描述 | -**** +MySQL执行计划的局限: +* EXPLAIN不会告诉你关于触发器、存储过程的信息或用户自定义函数对查询的影响情况 +* EXPLAIN不考虑各种Cache -### MyISAM +* EXPLAIN不能显示MySQL在执行查询时所作的优化工作 -读锁:**所有**连接只能读取数据,不能修改 +* 部分统计信息是估算的,并非精确值 -写锁:**其他**连接不能查询和修改数据 +* EXPALIN只能解释SELECT操作,其他操作要重写为SELECT后查看执行计划 -* 加锁 +环境准备: + +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-执行计划环境准备.png) + + + + + +*** + + + +##### id + +SQL执行的顺序的标识,SQL从大到小的执行 + +* id 相同时,执行顺序由上至下 ```mysql - -- 读锁 - LOCK TABLE 表名 READ; - - -- 写锁 - LOCK TABLE 表名 WRITE; + 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 - -- 将当前会话所有的表进行解锁 - UNLOCK TABLES; + 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 + 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) -### 乐观锁 -悲观锁和乐观锁使用前提: -- 对于读的操作远多于写的操作的时候,一个更新操作加锁会阻塞所有的读取操作,降低了吞吐量。最后需要释放锁,锁是需要一些开销的,这时候可以选择乐观锁。 -- 如果是读写比例差距不是非常大或者系统没有响应不及时,吞吐量瓶颈的问题,那就不要去使用乐观锁,它增加了复杂度,也带来了业务额外的风险,这时候可以选择悲观锁。 +*** -乐观锁的实现方式: +##### select_type -* 版本号 +表示查询中每个select子句的类型(简单 OR复杂) - 1. 给数据表中添加一个version列,每次更新后都将这个列的值加1 +| 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 | - 2. 读取数据时,将版本号读取出来,在执行更新的时候,比较版本号 - 3. 如果相同则执行更新,如果不相同,说明此条数据已经发生了变化 - 4. 用户自行根据这个通知来决定怎么处理,比如重新开始一遍,或者放弃本次更新 +**** - ```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; - ``` - -* 时间戳 +##### table + +输出结果集的表,显示这一步所访问数据库中表名称,有时不是真实的表名字,可能是简称,也可能是第几步执行的结果的简称 + + + +*** + + + +##### type + +对表的访问方式,表示MySQL在表中找到所需行的方式,又称“访问类型” + +| type | 含义 | +| ------ | ------------------------------------------------------------ | +| ALL | Full Table Scan,MySQL将遍历全表以找到匹配的行,全表扫描 | +| index | Full Index Scan,index 与 ALL 区别为 index 类型只遍历索引树 | +| range | 索引范围扫描,常见于between、<、>等的查询 | +| ref | 非唯一性索引扫描,返回匹配某个单独值的所有,本质上也是一种索引访问,常见于使用非唯一索引即唯一索引的非唯一前缀进行的查找 | +| eq_ref | 唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配,常见于主键或唯一索引扫描 | +| const | 当 MySQL 对查询某部分进行优化,并转换为一个常量时,使用该类型访问,如将主键置于where列表中,MySQL就能将该查询转换为一个常量 | +| system | system 是 const 类型的特例,当查询的表只有一行的情况下,使用system | +| NULL | MySQL在优化过程中分解语句,执行时甚至不用访问表或索引 | + +从上到下,性能从差到好,一般来说需要保证查询至少达到 range 级别, 最好达到ref + + + +*** + + + +##### key + +possible_keys: + +* 指出MySQL能使用哪个索引在表中找到记录,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用 +* 如果该列是NULL,则没有相关的索引 + +key: + +* 显示MySQL在查询中实际使用的索引,若没有使用索引,显示为NULL +* 查询中若使用了**覆盖索引**,则该索引仅出现在key列表中 + +key_len: + +* 表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度 +* key_len 显示的值为索引字段的最大可能长度,并非实际使用长度,即key_len是根据表定义计算而得,不是通过表内检索出的 +* 在不损失精确性的前提下,长度越短越好 + + + +*** + + + +##### Extra + +其他的额外的执行计划信息,在该列展示: + +* Using index:该值表示相应的 SELECT 操作中使用了覆盖索引(Covering Index) + * MySQL 可以利用索引返回 SELECT 列表中的字段,而不必根据索引再次读取数据文件,包含所有满足查询需要的数据的索引称为**覆盖索引** + * 使用覆盖索引,要注意 SELECT 列表中只取出需要的列,不可用 SELECT *,因为如果将所有字段一起做索引会导致索引文件过大,查询性能下降 +* Using index condition:搜索条件中虽然出现了索引列,但是有部分条件无法使用索引,会根据能用索引的条件先搜索一遍再匹配无法使用索引的条件,**回表查询**数据 +* Using where:表示存储引擎收到记录后进行“后过滤”(Post-filter),如果查询未能使用索引,Using where的作用是提醒我们 MySQL 将用 WHERE 子句来过滤结果集,即需要回表查询 +* Using temporary:表示 MySQL 需要使用临时表来存储结果集,常见于排序和分组查询 +* Using filesort:当 Query 中包含 order by 操作,而且无法利用索引完成的排序操作称为文件排序 +* Using join buffer:说明在获取连接条件时没有使用索引,并且需要连接缓冲区来存储中间结果 +* Impossible where:说明 WHERE 语句会导致没有符合条件的行,通过收集统计信息不可能存在结果 +* Select tables optimized away:说明仅通过使用索引,优化器可能仅从聚合函数结果中返回一行 + +* No tables used:Query 语句中使用from dual 或不含任何 from 子句 + + + +参考文章:https://www.cnblogs.com/ggjucheng/archive/2012/11/11/2765237.html + + + +**** + + + +#### PROFILES + +SHOW PROFILES 能够在做SQL优化时帮助了解时间的耗费 + +* 通过 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) + + ```mysql + SET profiling=1; //开启profiling 开关; + ``` + +* 执行show profiles 指令, 来查看SQL语句执行的耗时: + + ```mysql + SHOW PROFILES; + ``` + + ![](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 + + + +*** + + + +#### 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: + + ```mysql + SELECT * FROM information_schema.optimizer_trace \G; -- \G代表竖列展示 + ``` + + + + + +**** + + + +### 索引失效 + +#### 创建索引 + +索引是数据库优化最重要的手段之一,通过索引通常可以帮助用户解决大多数的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) + + + +**** + + + +#### 避免失效 + +索引失效的情况: + +* 全值匹配:对索引中所有列都指定具体值,这种情况索引生效,执行效率高 + + ```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='西安市'; + ``` + + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引3.png) + + 如果符合最左法则,但是出现跳跃某一列,只有最左列索引生效: + + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND address='西安市'; + ``` + + ![](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) = '科技'; + ``` + + ![](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 '%科技%'; + ``` + + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引12.png) + + 解决方案:通过覆盖索引来解决 + + ```mysql + EXPLAIN SELECT sellerid,name,status FROM tb_seller WHERE name like '%科技%'; + ``` + + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引13.png) + +* 如果 MySQL 评估使用索引比全表更慢,则不使用索引,索引失效: + + ```mysql + CREATE INDEX idx_address ON tb_seller(address); + EXPLAIN SELECT * FROM tb_seller WHERE address='西安市'; + EXPLAIN SELECT * FROM tb_seller WHERE address='北京市'; + ``` + + 北京市的键值占9/10,所以优化为全表扫描,type = ALL + + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引14.png) + +* IS NULL、IS NOT NULL **有时**索引失效: + + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name IS NULL; + EXPLAIN SELECT * FROM tb_seller WHERE name IS NOT NULL; + ``` + + NOT NULL 失效的原因是 name 列全部不是null,优化为全表扫描,当 NULL 过多时,IS NULL失效 + + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引15.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'); + ``` + +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='西安市'; + ``` + + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引9.png) + + + +*** + + + +#### 查看索引 + +```mysql +SHOW STATUS LIKE 'Handler_read%'; +SHOW GLOBAL STATUS LIKE 'Handler_read%'; +``` + +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL查看索引使用情况.png) + +* Handler_read_first:索引中第一条被读的次数。如果较高,表示服务器正执行大量全索引扫描(这个值越低越好) + +* Handler_read_key:如果索引正在工作,这个值代表一个行被索引值读的次数,如果值越低,表示索引不经常使用,性能改善不好(这个值越高越好)。 + +* Handler_read_next:按照键顺序读下一行的请求数,如果范围约束或执行索引扫描来查询索引列,值增加 + +* Handler_read_prev:按照键顺序读前一行的请求数,该读方法主要用于优化ORDER BY ... DESC + +* Handler_read_rnd:根据固定位置读一行的请求数,如果执行大量查询并对结果进行排序则该值较高,可能是使用了大量需要MySQL扫描整个表的查询或连接,这个值较高意味着运行效率低,应该建立索引来解决 + +* Handler_read_rnd_next:在数据文件中读下一行的请求数。如果你正进行大量的表扫描,该值较高。通常说明你的表索引不正确或写入的查询没有利用索引。 + + + + + +*** + + + +### 优化功能 + +#### 批量插入 + +当使用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顺序排列数据: + + ![](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`,关闭自动提交;导入结束后再执行 SET AUTOCOMMIT=1,打开自动提交,可以提高导入的效率。 + + 事务需要控制大小,事务太大可能会影响执行的效率。MySQL有innodb_log_buffer_size配置项,超过这个值的日志会使用磁盘数据,效率会下降。所以在事务大小达到配置项数据级前进行事务提交可以提高效率 + + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL插入数据手动提交事务.png) + + + +*** + + + +#### 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'); + ``` + + + +**** + + + +#### ORDER BY + +数据准备: + +```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) + +* 多字段排序: + + ```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的排序操作。 + +对于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 +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 +``` + + + +*** + + + +#### 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) + +* 查询包含 GROUP BY 但是用户想要避免排序结果的消耗, 则可以执行 ORDER BY NULL 禁止排序: + + ```mysql + EXPLAIN SELECT age,COUNT(*) FROM emp GROUP BY age ORDER BY NULL; + ``` + + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL GROUP BY排序2.png) + +* 创建索引: + + ```mysql + CREATE INDEX idx_emp_age_salary ON emp(age,salary); + ``` + + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL GROUP BY排序3.png) + + + +*** + + + +#### 嵌套查询 + +MySQL 4.1版本之后,开始支持SQL的子查询 + +* 可以使用SELECT语句来创建一个单列的查询结果,然后把结果作为过滤条件用在另一个查询中 +* 使用子查询可以一次性的完成逻辑上需要多个步骤才能完成的SQL操作,同时也可以避免事务或者表锁死 +* 在有些情况下,子查询是可以被更高效的连接(JOIN)替代 + +例如查找有角色的所有的用户信息: + +* 执行计划: + + ```mysql + EXPLAIN SELECT * FROM t_user WHERE id IN (SELECT user_id FROM user_role); + ``` + + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL嵌套查询1.png) + +* 优化后: + + ```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-优化SQL嵌套查询2.png) + + 连接查询之所以效率更高 ,是因为不需要在内存中创建临时表来完成逻辑上需要两个步骤的查询工作 + + + +*** + + + +#### 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: + 注意:该建议只针对多个索引列有效,如果有column没有被索引,查询效率可能会因为没有选择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 语句的 type 值为 null,const 表示是常量值引用,非常快 + + + +*** + + + +#### 分页 + +一般分页查询时,通过创建覆盖索引能够比较好地提高性能 + +一个常见的问题是 `LIMIT 200000,10`,此时需要MySQL扫描前 200010 记录,仅仅返回200000 - 200010 之间的记录,其他记录丢弃,查询排序的代价非常大 + +* 分页查询: + + ```mysql + EXPLAIN SELECT * FROM tb_user_1 LIMIT 200000,10; + ``` + + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL分页查询1.png) + +* 优化方式一:在索引上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容 + + ```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 + EXPLAIN SELECT * FROM tb_user_1 WHERE id > 200000 LIMIT 10; + ``` + + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL分页查询3.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='小米科技'; + ``` + + ![](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使用一个特定的索引,可在查询中使用 FORCE INDEX 作为提示 + + ```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) + - - 和版本号方式基本一样,给数据表中添加一个列,名称无所谓,数据类型需要是**timestamp** - - 每次更新后都将最新时间插入到此列。 - - 读取数据时,将时间读取出来,在执行更新的时候,比较时间。 - - 如果相同则执行更新,如果不相同,说明此条数据已经发生了变化。 +*** + + + +## 锁 + +### 锁的概述 + +锁机制:数据库为了保证数据的一致性,在共享的资源被并发访问时变得安全有序所设计的一种规则 + +作用:锁机制类似于多线程中的同步,可以保证数据的一致性和安全性 + +锁的分类: + +- 按操作分类: + - 共享锁:也叫读锁。对同一份数据,多个事务读操作可以同时加锁而不互相影响 ,但不能修改数据 + - 排他锁:也叫写锁。当前的操作没有完成前,会阻断其他操作的读取和写入 +- 按粒度分类: + - 表级锁:会锁定整个表。开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低。偏向于MyISAM存储引擎! + - 行级锁:会锁定当前操作行。开销大,加锁慢;会出现死锁;锁定力度小,发生锁冲突概率低,并发度高。偏向于InnoDB存储引擎! + - 页级锁:锁的力度、发生冲突的概率和加锁开销介于表锁和行锁之间,会出现死锁,并发性能一般。 +- 按使用方式分类: + - 悲观锁:每次查询数据时都认为别人会修改,很悲观,所以查询时加锁。 + - 乐观锁:每次查询数据时都认为别人不会修改,很乐观,但是更新时会判断一下在此期间别人有没有去更新这个数据 + +* 不同存储引擎支持的锁 + + | 存储引擎 | 表级锁 | 行级锁 | 页级锁 | + | -------- | ------ | ------ | ------ | + | MyISAM | 支持 | 不支持 | 不支持 | + | InnoDB | 支持 | 支持 | 不支持 | + | MEMORY | 支持 | 不支持 | 不支持 | + | BDB | 支持 | 不支持 | 支持 | + *** -## 备份 +### InnoDB -### 命令行方式 +#### 共享锁 -* 备份命令:mysqldump -u root -p 数据库名称 > 文件保存路径 +共享锁:数据可以被多个事务查询,但是不能修改 -* 恢复 - 1. 登录MySQL数据库:`mysql -u root p` - 2. 删除已经备份的数据库 - 3. 重新创建与备份数据库名称相同的数据库 - 4. 使用该数据库 - 5. 导入文件执行:`source 备份文件全路径` +* 加入共享锁 + + ```mysql + SELECT语句 LOCK IN SHARE MODE; + -- InnoDB引擎默认是行锁 + -- InnoDB引擎如果不采用带索引的列。则会提升为表锁 + ``` + +* 数据准备 + + ```mysql + -- 创建db13数据库 + CREATE DATABASE db13; + + -- 使用db13数据库 + USE db13; + + -- 创建student表 + CREATE TABLE student( + id INT PRIMARY KEY AUTO_INCREMENT, + NAME VARCHAR(10), + age INT, + score INT + ); + -- 添加数据 + INSERT INTO student VALUES (NULL,'张三',23,99),(NULL,'李四',24,95), + (NULL,'王五',25,98),(NULL,'赵六',26,97); + ``` + +* 共享锁演示 + + ```mysql + -- 开启事务 + START TRANSACTION; + + -- 查询id为1的数据记录。加入共享锁 + SELECT * FROM student WHERE id=1 LOCK IN SHARE MODE; + + -- 查询分数为99分的数据记录。加入共享锁,不采用带索引的列,提升为表锁 + SELECT * FROM student WHERE score=99 LOCK IN SHARE MODE; + + -- 提交事务 + COMMIT; + ``` + + ```mysql + -- 窗口2 + -- 开启事务 + START TRANSACTION; + + -- 查询id为1的数据记录(普通查询,可以查询) + SELECT * FROM student WHERE id=1; + + -- 查询id为1的数据记录,并加入共享锁(可以查询。共享锁和共享锁兼容) + SELECT * FROM student WHERE id=1 LOCK IN SHARE MODE; + + -- 修改id为1的姓名为张三三(不能修改,会出现锁的情况。只有窗口1提交事务后,才能修改成功) + UPDATE student SET NAME='张三三' WHERE id = 1; + + -- 修改id为2的姓名为李四四(修改成功,InnoDB引擎默认是行锁) + UPDATE student SET NAME='李四四' WHERE id = 2; + + -- 修改id为3的姓名为王五五(注意:InnoDB引擎如果不采用带索引的列。则会提升为表锁) + UPDATE student SET NAME='王五五' WHERE id = 3; + + -- 提交事务 + COMMIT; + ``` -### 图形化界面 +#### 排他锁 -* 备份 +排他锁:加锁的数据,不能被其他事务加锁查询或修改 - ![图形化界面备份](https://gitee.com/seazean/images/raw/master/DB/图形化界面备份.png) +* 加入排他锁 -* 恢复 + ```mysql + SELECT语句 FOR UPDATE; + ``` - ![图形化界面恢复](https://gitee.com/seazean/images/raw/master/DB/图形化界面恢复.png) +* 排他锁演示 + + ```mysql + -- 窗口1 + -- 开启事务 + START TRANSACTION; + + -- 查询id为1的数据记录,并加入排他锁 + SELECT * FROM student WHERE id=1 FOR UPDATE; + + -- 提交事务 + COMMIT; + ``` + + ```mysql + -- 窗口2 + -- 开启事务 + START TRANSACTION; + + -- 查询id为1的数据记录(普通查询没问题) + SELECT * FROM student WHERE id=1; + + -- 查询id为1的数据记录,并加入共享锁(不能查询。因为排他锁不能和其他锁共存) + SELECT * FROM student WHERE id=1 LOCK IN SHARE MODE; + + -- 查询id为1的数据记录,并加入排他锁(不能查询。因为排他锁不能和其他锁共存) + SELECT * FROM student WHERE id=1 FOR UPDATE; + + -- 修改id为1的姓名为张三(不能修改,会出现锁的情况。只有窗口1提交事务后,才能修改成功) + UPDATE student SET NAME='张三' WHERE id=1; + + -- 提交事务 + COMMIT; + ``` + + + + + +#### 兼容性 + +锁的兼容性 + +- 共享锁和共享锁 兼容 +- 共享锁和排他锁 冲突 +- 排他锁和排他锁 冲突 +- 排他锁和共享锁 冲突 + + + +**** + + + +### MyISAM + +读锁:**所有**连接只能读取数据,不能修改 + +写锁:**其他**连接不能查询和修改数据 + +* 加锁 + + ```mysql + -- 读锁 + LOCK TABLE 表名 READ; + + -- 写锁 + LOCK TABLE 表名 WRITE; + ``` + +* 解锁 + + ```mysql + -- 将当前会话所有的表进行解锁 + UNLOCK TABLES; + ``` + + + +*** + + + +### 乐观锁 + +悲观锁和乐观锁使用前提: + +- 对于读的操作远多于写的操作的时候,一个更新操作加锁会阻塞所有的读取操作,降低了吞吐量。最后需要释放锁,锁是需要一些开销的,这时候可以选择乐观锁。 +- 如果是读写比例差距不是非常大或者系统没有响应不及时,吞吐量瓶颈的问题,那就不要去使用乐观锁,它增加了复杂度,也带来了业务额外的风险,这时候可以选择悲观锁。 + + + +乐观锁的实现方式: + +* 版本号 + + 1. 给数据表中添加一个version列,每次更新后都将这个列的值加1 + + 2. 读取数据时,将版本号读取出来,在执行更新的时候,比较版本号 + + 3. 如果相同则执行更新,如果不相同,说明此条数据已经发生了变化 + + 4. 用户自行根据这个通知来决定怎么处理,比如重新开始一遍,或者放弃本次更新 + + ```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** + - 每次更新后都将最新时间插入到此列。 + - 读取数据时,将时间读取出来,在执行更新的时候,比较时间。 + - 如果相同则执行更新,如果不相同,说明此条数据已经发生了变化。 @@ -3524,7 +4470,7 @@ B+Tree优点:提高查询速度,减少磁盘的IO次数,树形结构较小 -## NF +## 范式 建立科学的,规范的数据库就需要满足一些规则来优化数据的设计和存储,这些规则就称为范式。 diff --git a/Java.md b/Java.md index 7dc4058..6fddd9c 100644 --- a/Java.md +++ b/Java.md @@ -6813,10 +6813,13 @@ public class ConstructorDemo { #### 概述 -Stream流能解决什么问题? - 可以解决已有集合类库或者数组API的弊端。 - Stream认为集合和数组操作的API很不好用,所以采用了Stream流简化集合和数组的操作。 - Stream流其实就是一根传送带,元素在上面可以被Stream流操作。 +Stream流其实就是一根传送带,元素在上面可以被Stream流操作 + +作用: + +* 可以解决已有集合类库或者数组API的弊端。 +* Stream流简化集合和数组的操作 +* 链式编程 ```java list.stream().filter(new Predicate() { @@ -6870,8 +6873,6 @@ Stream arrStream2 = Stream.of(arr); | static Stream concat(Stream a, Stream b) | 合并a和b两个流为一个. 调用: `Stream.concat(s1,s2);` | | Stream distinct() | 返回由该流的不同元素(根据Object.equals(Object) )组成的流 | - - ```java public class StreamDemo { public static void main(String[] args) { @@ -6914,7 +6915,7 @@ class Student{ #### 终结方法 -终结方法:Stream调用了终结方法,流的操作就全部终结,不能继续使用。foreach , count。 +终结方法:Stream调用了终结方法,流的操作就全部终结,不能继续使用,如foreach , count方法等 非终结方法:每次调用完成以后返回一个新的流对象,可以继续使用,支持**链式编程**! diff --git a/Web.md b/Web.md index 441549a..38819e3 100644 --- a/Web.md +++ b/Web.md @@ -5169,9 +5169,9 @@ Filter:过滤器,是JavaWeb三大组件之一,另外两个是Servlet和Lis ### 相关类 -#### Filter类 +#### Filter -* **Filter是一个接口,如果想实现过滤器的功能,必须实现该接口** +**Filter是一个接口,如果想实现过滤器的功能,必须实现该接口** * 核心方法 @@ -5205,7 +5205,7 @@ Filter:过滤器,是JavaWeb三大组件之一,另外两个是Servlet和Lis -#### FilterChain类 +#### FilterChain * FilterChain是一个接口,代表过滤器对象。由Servlet容器提供实现类对象,直接使用即可。 @@ -5219,7 +5219,7 @@ Filter:过滤器,是JavaWeb三大组件之一,另外两个是Servlet和Lis -#### FilterConfig类 +#### FilterConfig * FilterConfig 是一个接口,代表过滤器的配置对象,可以加载一些初始化参数 @@ -5486,7 +5486,7 @@ Filter初始化函数init的参数是FilterConfig 对象 ### Filter案例 -在我们访问html,js,image时,不需要每次都重新发送请求读取资源,就可以通过设置响应消息头的方式,设置缓存时间。但是如果每个Servlet都编写相同的代码,显然不符合我们统一调用和维护的理念。 +在访问html,js,image时,不需要每次都重新发送请求读取资源,就可以通过设置响应消息头的方式,设置缓存时间。但是如果每个Servlet都编写相同的代码,显然不符合我们统一调用和维护的理念。 静态资源设置缓存时间:html设置为1小时,js设置为2小时,css设置为3小时 From 504c9f1acadc98fd9164e0882a600ac8fcce4d95 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 22 May 2021 15:03:22 +0800 Subject: [PATCH 013/242] Update Java Notes --- Java.md | 692 +++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 432 insertions(+), 260 deletions(-) diff --git a/Java.md b/Java.md index 6fddd9c..4d213bd 100644 --- a/Java.md +++ b/Java.md @@ -4,7 +4,7 @@ ### 数据 -#### 变量 +#### 变量类型 | | 成员变量 | 局部变量 | 静态变量 | | :------: | :------------: | :------------------------: | :------------------: | @@ -655,36 +655,33 @@ public static void sum(int... nums){ #### 定义调用 -* 定义格式 +定义格式 - ```java - public static 返回值类型 方法名(参数) { - //方法体; - return 数据 ; - } - //注意:方法定义时,多个参数之间使( ,)分隔 - ``` +```java +public static 返回值类型 方法名(参数) { + //方法体; + return 数据 ; +} +``` -* 调用格式 +调用格式 - ```java - 数据类型 变量名 = 方法名 ( 参数 ) ; - //注意:方法的返回值通常会使用变量接收,否则该返回值将无意义 - ``` +```java +数据类型 变量名 = 方法名 ( 参数 ) ; +//注意:方法的返回值通常会使用变量接收,否则该返回值将无意义 +``` -* 解释 - 如果方法操作完毕,没有数据返回,这里写void,而且方法体中一般不写return +* 方法名:调用方法时候使用的标识 +* 参数:由数据类型和变量名组成,多个参数之间用逗号隔开 +* 方法体:完成功能的代码块 +* return:如果方法操作完毕,有数据返回,用于把数据返回给调用者 - 方法名:调用方法时候使用的标识 - 参数:由数据类型和变量名组成,多个参数之间用逗号隔开 - 方法体:完成功能的代码块 - return:如果方法操作完毕,有数据返回,用于把数据返回给调用者 +如果方法操作完毕 -* 注意 - void类型的方法,直接调用即可 - 非void类型的方法,推荐用变量接收调用 - -* 总结:每个方法在被调用执行的时候,都会进入栈内存,并且拥有自己独立的内存空间,方法内部代码调用完毕之后,会从栈内存中弹栈消失。 +* void类型的方法,直接调用即可,而且方法体中一般不写return +* 非void类型的方法,推荐用变量接收调用 + +原理:每个方法在被调用执行的时候,都会进入栈内存,并且拥有自己独立的内存空间,方法内部代码调用完毕之后,会从栈内存中弹栈消失。 @@ -726,37 +723,29 @@ public static void sum(int... nums){ #### 方法重载 -方法重载指同一个类中定义的多个方法之间的关系,满足下列条件的多个方法相互构成重载 - 1.多个方法在**同一个类**中 - 2.多个方法具有**相同的方法名** - 3.多个方法的**参数不相同**,类型不同或者数量不同 +##### 重载介绍 -注意: - 1.重载仅对应方法的定义,与方法的调用无关,调用方式参照标准格式 - 2.重载仅针对**同一个类**中方法的名称与参数进行识别,**与返回值无关**。 - 3.不能通过返回值来判定两个方法是否相互构成重载 +方法重载指同一个类中定义的多个方法之间的关系,满足下列条件的多个方法相互构成重载: + +1. 多个方法在**同一个类**中 +2. 多个方法具有**相同的方法名** +3. 多个方法的**参数不相同**,类型不同或者数量不同 + +重载仅对应方法的定义,与方法的调用无关,调用方式参照标准格式 + +重载仅针对**同一个类**中方法的名称与参数进行识别,**与返回值无关**,不能通过返回值来判定两个方法是否相互构成重载 ```java -//正确范例 -public class MethodDemo { - public static void fn(int a) { - //方法体 - } - public static int fn(double a) { - //方法体 - } -} -//错误范例 public class MethodDemo { public static void fn(int a) { //方法体 } + public static int fn(int a) { /*错误原因:重载与返回值无关*/ //方法体 } -} -public class MethodDemo01 { - public static int fn(double a) { /*错误原因:这是两个类的两个fn方法*/ + + public static void fn(int a, int b) {/*正确格式*/ //方法体 } } @@ -768,6 +757,50 @@ public class MethodDemo01 { +##### 方法选取 + +重载的方法在编译过程中即可完成识别,方法调用时Java 编译器会根据所传入参数的声明类型(注意与实际类型区分)来选取重载方法。选取的过程共分为三个阶段: + +* 在不考虑对基本类型自动装拆箱 (auto-boxing,auto-unboxing),以及可变长参数的情况下选取重载方法 +* 如果第 1 个阶段中没有找到适配的方法,那么在允许自动装拆箱,但不允许可变长参数的情况下选取重载方法 +* 如果第 2 个阶段中没有找到适配的方法,那么在允许自动装拆箱以及可变长参数的情况下选取重载方法 + +如果 Java 编译器在同一个阶段中找到了多个适配的方法,那么会选择一个最为贴切的,而决定贴切程度的一个关键就是形式参数类型的继承关系,一般会选择形参为参数类型的子类的方法,因为子类时更具体的实现: + +```java +public class MethodDemo { + void invoke(Object obj, Object... args) { ... } + void invoke(String s, Object obj, Object... args) { ... } + + invoke(null, 1); // 调用第二个invoke方法,选取的第二阶段 + invoke(null, 1, 2); // 调用第二个invoke方法,匹配第一个和第二个,但String是Object的子类 + + invoke(null, new Object[]{1}); // 只有手动绕开可变长参数的语法糖,才能调用第一个invoke方法 + // 可变参数底层是数组,JVM->运行机制->代码优化 +} +``` + +因此不提倡可变长参数方法的重载 + + + +*** + + + +##### 继承重载 + +除了同一个类中的方法,重载也可以作用于这个类所继承而来的方法。也就是说,如果子类定义了与父类中**非私有方法**同名的方法,而且这两个方法的参数类型不同,那么在子类中,这两个方法同样构成了重载 + +* 如果这两个方法都是静态的,那么子类中的方法隐藏了父类中的方法 +* 如果这两个方法都不是静态的,且都不是私有的,那么子类的方法重写了父类中的方法,也就是**多态** + + + +*** + + + #### 参数传递 **Java 的参数是以值传递的形式传入方法中** @@ -791,7 +824,7 @@ public class MethodDemo01 { } ``` -* 引用类型:,形式参数的改变,影响实际参数的值 +* 引用类型:形式参数的改变,影响实际参数的值 **引用数据类型的传参,本质上是将对象的地址以值的方式传递到形参中**,内存中会造成两个引用指向同一个内存的效果,所以即使方法弹栈,堆内存中的数据也已经是改变后的结果 ```java @@ -1450,7 +1483,7 @@ this关键字的作用: ### static -#### static介绍 +#### 基本介绍 Java是通过成员变量是否有static修饰来区分是类的还是属于对象的。 @@ -1553,12 +1586,12 @@ static 静态修饰的成员(方法和成员变量)属于类本身的。 ### 继承 -#### 继承概述 +#### 基本介绍 继承是Java中一般到特殊的关系,是一种子类到父类的关系。 -被继承的类称为:父类/超类。 -继承父类的类称为:子类。 +* 被继承的类称为:父类/超类。 +* 继承父类的类称为:子类。 继承的作用: @@ -1582,32 +1615,29 @@ static 静态修饰的成员(方法和成员变量)属于类本身的。 } ``` -子类不能继承父类的东西: +子类继承父类的东西: -* 子类不能继承父类的构造器,子类有自己的构造器。(没有争议的) +* 子类不能继承父类的构造器,子类有自己的构造器 +* 子类是不能可以继承父类的私有成员的,可以反射暴力去访问继承自父类的私有成员 +* 子类是不能继承父类的静态成员的,子类只是可以访问父类的静态成员,父类静态成员只有一份可以被子类共享访问,**共享并非继承** - * 子类是否可以继承父类的私有成员(私有成员变量,私有成员方法)?(有争议) - 子类是可以继承父类的私有成员的,只是不能直接访问而已。可以反射暴力去访问继承自父类的私有成员 - * 子类是否可以继承父类的静态成员?(有争议) - 子类是不能继承父类的静态成员的,子类只是可以访问父类的静态成员,父类静态成员只有一份可以被子类共享访问。**共享并非继承**。 - - ```java - public class ExtendsDemo { - public static void main(String[] args) { - Cat c = new Cat(); - // c.run(); - Cat.test(); - System.out.println(Cat.schoolName); - } - } - class Cat extends Animal{ - } - class Animal{ - public static String schoolName ="seazean"; - public static void test(){} - private void run(){} +```java +public class ExtendsDemo { + public static void main(String[] args) { + Cat c = new Cat(); + // c.run(); + Cat.test(); + System.out.println(Cat.schoolName); } - ``` +} +class Cat extends Animal{ +} +class Animal{ + public static String schoolName ="seazean"; + public static void test(){} + private void run(){} +} +``` @@ -1619,7 +1649,7 @@ static 静态修饰的成员(方法和成员变量)属于类本身的。 继承后成员变量的访问特点:**就近原则**,子类有找子类,子类没有找父类,父类没有就报错! -如果要申明访问父类的成员变量可以使用:super.父类成员变量。super指父类引用。 +如果要申明访问父类的成员变量可以使用:super.父类成员变量。super指父类引用 ```java public class ExtendsDemo { @@ -1654,20 +1684,24 @@ class Animal{ #### 方法重写 -方法重写的概念: - 子类继承了父类,子类就得到了父类的某个方法,子类重写一个与父类申明一样的方法来**覆盖**父类的该方法。 +方法重写:子类继承了父类就得到了父类的方法,子类重写一个与父类申明一样的方法来**覆盖**父类的该方法 -方法重写的校验注解: @Override +方法重写的校验注解:@Override * 方法加了这个注解,那就必须是成功重写父类的方法,否则报错 -* **@Override优势:可读性好,安全,优雅!!** +* @Override优势:可读性好,安全,优雅 -为了满足里式替换原则(子类可以扩展父类的功能,但不能改变父类原有的功能),重写有以下三个限制: +子类可以扩展父类的功能,但不能改变父类原有的功能,重写有以下**三个限制**: - 子类方法的访问权限必须大于等于父类方法 - 子类方法的返回类型必须是父类方法返回类型或为其子类型 - 子类方法抛出的异常类型必须是父类抛出异常类型或为其子类型 +继承中的隐藏问题: + +- 子类和父类方法都是静态的,那么子类中的方法会隐藏父类中的方法 +- 在子类中可以定义和父类成员变量同名的成员变量,此时子类的成员变量隐藏了父类的成员变量,在创建对象为对象分配内存的过程中,**隐藏变量依然会被分配内存** + ```java public class ExtendsDemo { public static void main(String[] args) { @@ -1692,11 +1726,12 @@ class Animal{ #### 面试问题 -* **为什么子类构造器会先调用父类构造器?** - 1.子类的构造器的第一行默认super()调用父类的无参数构造器,写不写都存在! - 2.子类继承父类,子类就得到了父类的属性和行为。 - 调用子类构造器初始化子类对象数据时,必须先调用父类构造器初始化继承自父类的属性和行为 - +* 为什么子类构造器会先调用父类构造器? + + 1. 子类的构造器的第一行默认super()调用父类的无参数构造器,写不写都存在 + 2. 子类继承父类,子类就得到了父类的属性和行为。调用子类构造器初始化子类对象数据时,必须先调用父类构造器初始化继承自父类的属性和行为 + 3. 参考JVM -> 类加载 -> 对象创建 + ```java class Animal{ public Animal(){ @@ -1714,9 +1749,9 @@ class Animal{ } } ``` - - + + * **为什么Java是单继承的?** 答:反证法,假如Java可以多继承,请看如下代码: 补充:多实现是在实现接口时,重名方法需要实现类来实现 @@ -1808,7 +1843,7 @@ class Student{ ### final -#### final概述 +#### 基本介绍 final用于修饰:类,方法,变量 @@ -1897,7 +1932,7 @@ public class FinalDemo { ### 抽象类 -#### 概述 +#### 基本介绍 > 父类知道子类要完成某个功能,但是每个子类实现情况不一样。 @@ -2011,7 +2046,7 @@ abstract class Template{ ### 接口 -#### 概述 +#### 基本介绍 接口,是Java语言中一种引用类型,是方法的集合。 @@ -2052,7 +2087,7 @@ abstract class Template{ -#### 实现 +#### 实现接口 作用:**接口是用来被类实现的。** @@ -2124,8 +2159,7 @@ jdk1.8以后新增的功能,实际开发中很少使用 * 默认会public修饰 * 接口的静态方法必须用接口的类名本身来调用 * 调用格式:ClassName.method() -* 私有方法(就是私有的实例方法):JDK 1.9才开始有的 - * 只能在**本类中**被其他的默认方法或者私有方法访问 +* 私有方法:JDK 1.9才开始有的,只能在**本类中**被其他的默认方法或者私有方法访问 ```java public class InterfaceDemo { @@ -2200,7 +2234,7 @@ interface InterfaceJDK8{ ### 多态 -#### 多态概述 +#### 基本介绍 多态的概念:同一个实体同时具有多种形式同一个类型的对象,执行同一个行为,在不同的状态下会表现出不同的行为特征。 @@ -2218,20 +2252,18 @@ interface InterfaceJDK8{ * 对于方法的调用:**编译看左边,运行看右边。** * 对于变量的调用:**编译看左边,运行看左边。** -多态的使用前提: +多态的使用规则: -* 必须存在继承或者实现关系。 -* 必须存在父类类型的变量引用子类类型的对象。 +* 必须存在继承或者实现关系 +* 必须存在父类类型的变量引用子类类型的对象 * 存在方法重写 -多态的优劣: - -* 优势: - * 在多态形式下,右边对象可以实现组件化切换,业务功能也随之改变,便于扩展和维护。可以实现类与类之间的**解耦** - * 父类类型作为方法形式参数,传递子类对象给方法,可以传入一切子类对象进行方法的调用,更能体现出多态的**扩展性**与便利性 +多态的优势: +* 在多态形式下,右边对象可以实现组件化切换,业务功能也随之改变,便于扩展和维护。可以实现类与类之间的**解耦** +* 父类类型作为方法形式参数,传递子类对象给方法,可以传入一切子类对象进行方法的调用,更能体现出多态的**扩展性**与便利性 -* 劣势: - * 多态形式下,不能直接调用子类特有的功能,因为编译看左边,父类中没有子类独有的功能,所以代码在编译阶段就直接报错了! +多态的劣势: +* 多态形式下,不能直接调用子类特有的功能,因为编译看左边,父类中没有子类独有的功能,所以代码在编译阶段就直接报错了! ```java public class PolymorphicDemo { @@ -3940,7 +3972,7 @@ public class RegexDemo { ## 集合 -### 概述 +### 基本介绍 什么是集合? 集合是一个大小可变的容器,容器中的每个数据称为一个元素。数据==元素 @@ -3959,7 +3991,7 @@ public class RegexDemo { ### 数据结构 -#### 概述 +#### 基本介绍 什么是数据结构? @@ -11640,13 +11672,13 @@ Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所 ##### 动态链接 -动态链接就是将符号引用转换为调用方法的直接引用 +动态链接也就是指向运行时常量池的方法引用 * 为了支持当前方法的代码能够实现动态链接,每一个栈帧内部都包含一个指向运行时常量池或该栈帧所属方法的引用 ![](https://gitee.com/seazean/images/raw/master/Java/JVM-动态链接符号引用.png) -* 在Java源文件被编译成字节码文件中,所有的变量和方法引用都作为符号引用(symbolic Refenrence)保存在class文件的常量池里 +* 在Java源文件被编译成字节码文件中,所有的变量和方法引用都作为符号引用(symbolic Refenrence)保存在class文件的常量池里,在类加载解析阶段变成直接引用 常量池的作用:提供一些符号和常量,便于指令的识别 ![](https://gitee.com/seazean/images/raw/master/Java/JVM-动态链接运行时常量池.png) @@ -11804,11 +11836,11 @@ public static void main(String[] args) { 方法区的大小不必是固定的,可以动态扩展;方法区大小很难确定,因此加载的类太多,可能导致永久代内存溢出 (OutOfMemoryError);对这块区域进行垃圾回收主要是对常量池的回收和对类的卸载,比较难实现 -为了**避免方法区出现OOM**,在JDK8中将堆内的方法区(永久代)移动到了本地内存上,重新开辟了一块空间,叫做**元空间**,元空间存储类的元信息,静态变量和字符串常量池等放入堆中 +为了**避免方法区出现OOM**,在JDK8中将堆内的方法区(永久代)移动到了本地内存上,重新开辟了一块空间,叫做元空间,元空间存储类的元信息,静态变量和字符串常量池等放入堆中 类元信息:在类编译期间放入方法区,存放了类的基本信息,包括类的方法、参数、接口以及常量池表 -常量池表(Constant Pool Table)是Class文件的一部分,存储了类在编译期间生成的字面量、符号引用,这些信息在类加载后会被解析到运行时常量池中,JVM为每个已加载的类维护一个常量池 +常量池表(Constant Pool Table)是Class文件的一部分,存储了类在编译期间生成的**字面量、符号引用**,这些信息在类加载后会被解析到运行时常量池中,JVM为每个已加载的类维护一个常量池 **运行时常量池**是方法区的一部分 @@ -13270,20 +13302,20 @@ Java对象创建时机: 创建对象的过程: -1. **判断对象对应的类是否加载、链接、初始化** +1. 判断对象对应的类是否加载、链接、初始化 -2. **为对象分配内存**:指针碰撞、空闲链表。当一个对象被创建时,虚拟机就会为其分配内存来存放对象的实例变量及其从父类继承过来的实例变量 (即使从超类继承过来的实例变量有可能被隐藏也会被分配空间) +2. 为对象分配内存:指针碰撞、空闲链表。当一个对象被创建时,虚拟机就会为其分配内存来存放对象的实例变量及其从父类继承过来的实例变量 (即使从超类继承过来的实例变量有可能被**隐藏**也会被分配空间) -3. **处理并发安全问题**: +3. 处理并发安全问题: * 采用CAS配上自旋保证更新的原子性 * 每个线程预先分配一块TLAB -4. **初始化分配的空间**:虚拟机将分配到的内存空间都初始化为零值(不包括对象头),保证对象实例字段在不赋值时可以直接使用,程序能访问到这些字段的数据类型所对应的零值 +4. 初始化分配的空间:虚拟机将分配到的内存空间都初始化为零值(不包括对象头),保证对象实例字段在不赋值时可以直接使用,程序能访问到这些字段的数据类型所对应的零值 -5. **设置对象的对象头**:将对象的所属类(类的元数据信息)、对象的HashCode、对象的GC信息、锁信息等数据存储在对象的对象头中 +5. 设置对象的对象头:将对象的所属类(类的元数据信息)、对象的HashCode、对象的GC信息、锁信息等数据存储在对象的对象头中 -6. **执行init方法进行实例化**:实例变量初始化、实例代码块初始化 、构造函数初始化 +6. 执行init方法进行实例化:实例变量初始化、实例代码块初始化 、构造函数初始化 * 实例变量初始化与实例代码块初始化: @@ -13468,7 +13500,7 @@ Java对象创建时机: 实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中。实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次 -**类变量初始化**: +类变量初始化: * static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾 * static 变量分配空间和赋值是两个步骤:**分配空间在准备阶段完成,赋值在初始化阶段完成** @@ -13495,7 +13527,7 @@ Java对象创建时机: 将常量池中类、接口、字段、方法的**符号引用替换为直接引用**(内存地址)的过程 -* 符号引用:一组符号来描述目标,可以是任何字面量,属于编译原理方面的概念如:包括类和接口的全限名、字段的名称和描述符、方法的名称和描述符 +* 符号引用:一组符号来描述目标,可以是任何字面量,属于编译原理方面的概念如:包括类和接口的全限名、字段的名称和描述符、方法的名称和**方法描述符** * 直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等,某些解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。 @@ -14001,6 +14033,8 @@ C1编译器会对字节码进行简单可靠的优化,耗时短,以达到更 * 方法内联:将引用的函数代码编译到引用点处,这样可以减少栈帧的生成,减少参数传递以及跳转过程 + 方法内联能够消除方法调用的固定开销。任何方法调用除非被内联,否则都会有固定开销,来源于保存程序在该方法中的执行位置,以及新建、压入和弹出新方法所使用的栈帧。 + ```java private static int square(final int i) { return i * i; @@ -14097,6 +14131,286 @@ AOT编译器:JDK9引入,静态提前编译器 (Ahead Of Time Compiler),程 +*** + + + +### 方法调用 + +#### 方法识别 + +Java 虚拟机识别方法的关键在于类名、方法名以及方法描述符(method descriptor) + +* 方法描述符是由方法的参数类型以及返回类型所构成,也叫方法签名 +* 在同一个类中,如果同时出现多个名字相同且描述符也相同的方法,那么 Java 虚拟机会在类的验证阶段报错 + +JVM是根据名字和描述符来判断的,只要返回值不一样,其它完全一样,在JVM中是允许的,但Java语言不允许 + +```java +// 返回值类型不同,编译阶段直接报错 +public static Integer invoke(Object... args) { + return 1; +} +public static int invoke(Object... args) { + return 2; +} +``` + + + +*** + + + +#### 调用机制 + +在JVM中,将符号引用转换为直接引用有两种机制: + +- 静态链接:当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变,将调用方法的符号引用转换为直接引用的过程称之为静态链接 +- 动态链接:如果被调用的方法在编译期无法被确定下来,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此被称为动态链接 + +对应的方法的绑定机制为:静态绑定和动态绑定。绑定是一个字段、方法或者类从符号引用被替换为直接引用的过程,仅发生一次 + +- 静态绑定:被调用的目标方法在编译期可知,且运行期保持不变,将这个方法与所属的类型进行绑定 +- 动态绑定:被调用的目标方法在编译期无法确定,只能在程序运行期根据实际的类型绑定相关的方法 + +* Java 编译器已经区分了重载的方法(静态绑定和动态绑定),因此可以认为虚拟机中不存在重载 + +非虚方法: + +- 非虚方法在编译器就确定了具体的调用版本,这个版本在运行时是不可变的 +- 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法 +- 非私有实例方法称为虚方法 + +动态类型语言和静态类型语言: + +- 在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之则是动态类型语言 + +- 静态语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息 + +- **Java是静态类型语言**(尽管lambda表达式为其增加了动态特性),js,python是动态类型语言 + + ```java + String s = "abc"; //Java + info = "abc"; //Python + ``` + + + +*** + + + +#### 调用指令 + +##### 五种指令 + +普通调用指令: + +- invokestatic:调用静态方法 +- invokespecial:调用私有实例方法、构造器,和父类的实例方法或构造器,以及所实现接口的默认方法 +- invokevirtual:调用所有虚方法 +- invokeinterface:调用接口方法 + +动态调用指令: + +- invokedynamic:动态解析出需要调用的方法, + - Java7为了实现动态类型语言支持而引入了该指令,但是并没有提供直接生成invokedynamic指令的方法,需要借助ASM这种底层字节码工具来产生invokedynamic指令 + - Java8的Lambda表达式的出现,invokedynamic指令在Java中才有了直接生成方式 + +指令对比: + +- 普通调用指令固化在虚拟机内部,方法的调用执行不可干预,根据方法的符号引用链接到具体的目标方法 +- 动态调用指令支持用户确定方法 +- invokestatic 和 invokespecial 指令调用的方法称为非虚方法,虚拟机能够直接识别具体的目标方法 +- invokevirtual 和 invokeinterface 指令调用的方法称为虚方法,虚拟机需要在执行过程中根据调用者的动态类型来确定目标方法 + +指令说明: + +- 如果虚拟机能够确定目标方法有且仅有一个,比如说目标方法被标记为 final,那么可以不通过动态类型,直接确定目标方法 +- 普通成员方法是由 invokevirtual 调用,属于**动态绑定**,即支持多态 + + + +*** + + + +##### 符号引用 + +在编译过程中,虚拟机并不知道目标方法的具体内存地址,Java 编译器会暂时用符号引用来表示该目标方法,这一符号引用包括目标方法所在的类或接口的名字,以及目标方法的方法名和方法描述符 + +* 对于静态绑定的方法调用而言,实际引用是一个指向方法的指针 +* 对于需要动态绑定的方法调用而言,实际引用则是一个方法表的索引 + +符号引用存储在方法区常量池中,根据目标方法是否为接口方法,分为接口符号引用和非接口符号引用: + +```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 的要求一致 + +```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 +``` + +- new 是在堆中创建对象,执行成功会将**对象引用**压入操作数栈 +- dup 是复制操作数栈栈顶的内容,本例即为对象引用,为什么需要两份引用呢? + - 一个要配合 invokespecial 调用该对象的构造方法 :()V (会消耗掉栈顶一个引用) + - 一个要配合 astore_1 赋值给局部变量 +- `d.test4()` 是通过**对象引用**调用一个静态方法,在调用invokestatic 之前执行了 pop 指令,把对象引用从操作数栈弹掉 + - 不建议使用`对象.静态方法()`的方式调用静态方法,多了aload和pop指令 + - 成员方法与静态方法调用的区别是:执行方法前是否需要对象引用 + + + +*** + + + +#### 多态原理 + +##### 执行原理 + +Java 虚拟机中关于方法重写的判定基于方法描述符,如果子类定义了与父类中非私有、非静态方法同名的方法,只有当这两个方法的参数类型以及返回类型一致,Java 虚拟机才会判定为重写。 + +**理解多态**: + +- 多态有编译时多态和运行时多态,即静态绑定和动态绑定 +- 前者是通过方法重载实现,后者是通过重写实现(子类覆盖父类方法,虚方法表) +- 虚方法:运行时动态绑定的方法,对比静态绑定的非虚方法调用来说,虚方法调用更加耗时 + +执行 invokevirtual 指令: + +1. 先通过栈帧中的对象引用找到对象 +2. 分析对象头,找到对象的实际 Class +3. Class 结构中有 vtable +4. 查表得到方法的具体地址 +5. 执行方法的字节码 + + + +*** + + + +##### 虚方法表 + +在虚拟机工作过程中会频繁使用到动态链接,每次动态链接的过程中都要重新在类的元数据中搜索合适目标,影响到执行效率。为了提高性能,JVM 采取了一种用**空间换取时间**的策略来实现动态绑定,在类的方法区建立一个虚方法表(virtual method table),实现使用索引表来代替查找,可以快速定位目标方法 + +* invokevirtual 所使用的虚方法表(virtual method table,vtable) +* invokeinterface 所使用的接口方法表(interface method table,itable) + +虚方法表在类加载的**链接阶段**被创建并开始初始化,在类加载的准备阶段构造与该类关联的方法表,在解析阶段把表中的符号引用解析成内存地址 + +虚方法表的执行过程: + +* 对于静态绑定的方法调用而言,实际引用将指向具体的目标方法 +* 对于动态绑定的方法调用而言,实际引用则是方法表的索引值,也就是方法的间接地址 +* Java 虚拟机获取调用者的实际类型,并在该实际类型的虚方法表中,根据索引值获得目标方法 + +每个类中都有一个虚方法表,本质上是一个数组,每个数组元素指向一个当前类及其祖先类中非私有的实例方法 + +方法表满足两个特质: + +* 其一,子类方法表中包含父类方法表中的所有方法 +* 其二,子类方法在方法表中的索引值,与它所重写的父类方法的索引值相同 + + + +Passenger 类的方法表包括两个方法,分别对应 0 号和 1 号。方法表调换了 toString 方法和 passThroughImmigration 方法的位置,是因为 toString 方法的索引值需要与 Object 类中同名方法的索引值一致,为了保持简洁,这里不考虑 Object 类中的其他方法。 + +虚方法表对性能的影响: + +* 使用了方法表的动态绑定与静态绑定相比,仅仅多出几个内存解引用操作:访问栈上的调用者,读取调用者的动态类型,读取该类型的方法表,读取方法表中某个索引值所对应的目标方法,相对于创建并初始化 Java 栈帧这操作的开销可以忽略不计 +* 上述优化的效果看上去不错,但实际上仅存在于解释执行中,或者即时编译代码的最坏情况。因为即时编译还拥有另外两种性能更好的优化手段:内联缓存(inlining cache)和方法内联(method inlining) + + + + + +*** + + + +##### 内联缓存 + +内联缓存:是一种加快动态绑定的优化技术,它能够缓存虚方法调用中调用者的动态类型,以及该类型所对应的目标方法。在之后的执行过程中,如果碰到已缓存的类型,内联缓存便会直接调用该类型所对应的目标方法;如果没有碰到已缓存的类型,内联缓存则会退化至使用基于方法表的动态绑定 + +多态的三个术语: + +* 单态 (monomorphic):指的是仅有一种状态的情况 +* 多态 (polymorphic):指的是有限数量种状态的情况,二态(bimorphic)是多态的其中一种 +* 超多态 (megamorphic):指的是更多种状态的情况。通常我们用一个具体数值来区分多态和超多态,在这个数值之下,称之为多态,否则称之为超多态 + +对于内联缓存来说,有对应的单态内联缓存、多态内联缓存和超多态内联缓存: + +* 单态内联缓存:只缓存了一种动态类型以及所对应的目标方法,实现简单,比较所缓存的动态类型,如果命中,则直接调用对应的目标方法。 +* 多态内联缓存:缓存了多个动态类型及其目标方法,需要逐个将所缓存的动态类型与当前动态类型进行比较,如果命中,则调用对应的目标方法 + +为了节省内存空间,**Java 虚拟机只采用单态内联缓存**,没有命中的处理方法: + +* 替换单态内联缓存中的纪录,类似于 CPU 中的数据缓存,对数据的局部性有要求,即在替换内联缓存之后的一段时间内,方法调用的调用者的动态类型应当保持一致,从而能够有效地利用内联缓存 +* 劣化为超多态状态,这也是 Java 虚拟机的具体实现方式,这种状态实际上放弃了优化的机会,将直接访问方法表来动态绑定目标方法,但是与替换内联缓存纪录相比节省了写缓存的额外开销 + +虽然内联缓存附带内联二字,但是并没有内联目标方法 + + + + + *** @@ -14616,148 +14930,6 @@ Exception table: -*** - - - -### 方法调用 - -#### 调用机制 - -在JVM中,将符号引用转换为直接引用有两种机制: - -* 静态链接:当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变,将调用方法的符号引用转换为直接引用的过程称之为静态链接 -* 动态链接:如果被调用的方法在编译期无法被确定下来,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此被称为动态链接 - -对应的方法的绑定机制为:静态绑定和动态绑定。绑定是一个字段、方法或者类从符号引用被替换为直接引用的过程,仅发生一次 - -* 静态绑定:被调用的目标方法在编译期可知,且运行期保持不变,即可将这个方法与所属的类型进行绑定 -* 动态绑定:被调用的方法在编译期无法被确定,只能够在程序运行期根据实际的类型绑定相关的方法 - -非虚方法: - -* 方法在编译器就确定了具体的调用版本,这个版本在运行时是不可变的 -* 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法,其他方法称为虚方法 - -动态类型语言和静态类型语言: - -* 在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之则是动态类型语言 - -* 静态语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息 - -* **Java是静态类型语言**(尽管lambda表达式为其增加了动态特性),js,python是动态类型语言 - - ```java - String s = "abc"; //Java - info = "abc"; //Python - ``` - - - - - -#### 调用指令 - -普通调用指令: - -* invokestatic:调用静态方法,解析阶段确定唯一方法版本 -* invokespecial:调用 init 方法、私有及父类方法,解析阶段确定唯一方法版本 -* invokevirtual:调用所有虚方法 -* invokeinterface:调用接口方法 - -动态调用指令: - -* invokedynamic:动态解析出需要调用的方法, - * Java7为了实现动态类型语言支持而引入了该指令,但是并没有提供直接生成invokedynamic指令的方法,需要借助ASM这种底层字节码工具来产生invokedynamic指令 - * Java8的Lambda表达式的出现,invokedynamic指令在Java中才有了直接生成方式 - -指令总结: - -* 前四条指令固化在虚拟机内部,方法的调用执行不可干预,根据方法的符号引用链接到具体的目标方法,而invokedynamic指令则支持用户确定方法 - -* invokestatic指令和invokespecial指令调用的方法称为非虚方法,属于静态绑定 -* 普通成员方法是由 invokevirtual 调用,属于**动态绑定**,即支持多态 - -```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 -``` - -* new 是在堆中创建对象,执行成功会将**对象引用**压入操作数栈 - -* dup 是复制操作数栈栈顶的内容,本例即为对象引用,为什么需要两份引用呢? - - * 一个要配合 invokespecial 调用该对象的构造方法 :()V (会消耗掉栈顶一个引用) - - * 一个要配合 astore_1 赋值给局部变量 - -* `d.test4()` 是通过**对象引用**调用一个静态方法,在调用invokestatic 之前执行了 pop 指令,把对象引用从操作数栈弹掉 - * 不建议使用`对象.静态方法()`的方式调用静态方法,多了aload和pop指令 - * 成员方法与静态方法调用的区别是:执行方法前是否需要对象引用 - - - -*** - - - -#### 多态原理 - -执行 **invokevirtual** 指令: - -1. 先通过栈帧中的对象引用找到对象 -2. 分析对象头,找到对象的实际 Class -3. Class 结构中有虚拟方法表,它在类加载的链接阶段就已经根据方法的重写规则生成好了 -4. 查表得到方法的具体地址 -5. 执行方法的字节码 - -**理解多态**: - -* 多态有编译时多态和运行时多态,即静态多态和动态多态 -* 前者是通过方法重载实现,后者是通过方法覆盖实现(子类覆盖父类方法,虚方法表) -* 虚方法:运行时动态绑定的方法 - -虚方法表:在面向对象编程中,会频繁使用到**动态分派**,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适目标就会影响到执行效率。为了提高性能,JVM采用在类的方法区建立一个虚方法表(virtual method table)来实现,使用索引表来代替查找,**每个类中都有一个虚方法表**,表中存放着各个方法的实际入口 - -![](https://gitee.com/seazean/images/raw/master/Java/JVM-方法调用动态分配.png) - - - *** From 387b36136987f9340f4c09dda19f31d8dc3f0ab0 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 22 May 2021 15:38:26 +0800 Subject: [PATCH 014/242] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2c1f7c8..c9703ac 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -作者的个人学习笔记,每次学有所获都会更新笔记,所有的知识都有借鉴其他前辈的博客和文章,排版布局美观整洁,希望对各位朋友有所帮助。 +**Java** 学习笔记,记录着作者从入门到逐渐深入的所有知识,每次学有所获就会更新笔记,所有的知识都有借鉴其他前辈的博客和文章,排版布局美观整洁,希望对各位朋友有所帮助。 注:所有的知识不保证权威性,如果各位朋友发现错误,非常欢迎与我讨论。 From cb5d00ec5dc2f0d8cd73a354c89a56da6ad155a1 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 22 May 2021 15:47:30 +0800 Subject: [PATCH 015/242] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c9703ac..03273d9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -**Java** 学习笔记,记录着作者从入门到逐渐深入的所有知识,每次学有所获就会更新笔记,所有的知识都有借鉴其他前辈的博客和文章,排版布局美观整洁,希望对各位朋友有所帮助。 +**Java** 学习笔记,记录着作者从入门到逐渐深入的所有知识,每次学有所获就会更新笔记,所有的知识都有借鉴其他前辈的博客和文章,排版布局美观整洁,使用 Typora 阅读效果更佳,希望对各位朋友有所帮助。 注:所有的知识不保证权威性,如果各位朋友发现错误,非常欢迎与我讨论。 From f895a7c63efb56fe6ad4a667dc540debf31aca05 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 22 May 2021 19:03:34 +0800 Subject: [PATCH 016/242] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 03273d9..8d78004 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -**Java** 学习笔记,记录着作者从入门到逐渐深入的所有知识,每次学有所获就会更新笔记,所有的知识都有借鉴其他前辈的博客和文章,排版布局美观整洁,使用 Typora 阅读效果更佳,希望对各位朋友有所帮助。 +**Java** 学习笔记,记录着作者从入门到深入学习的所有知识,每次学有所获就会更新笔记,所有的知识都有借鉴其他前辈的博客和文章,排版布局美观整洁,使用 Typora 阅读效果更佳,希望对各位朋友有所帮助。 注:所有的知识不保证权威性,如果各位朋友发现错误,非常欢迎与我讨论。 From a75ab71d0527e709e6ede52f278e894351cdc48e Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 22 May 2021 22:09:10 +0800 Subject: [PATCH 017/242] Update README --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 8d78004..cb0c263 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,12 @@ 注:所有的知识不保证权威性,如果各位朋友发现错误,非常欢迎与我讨论。 +内容说明: + +* DB:MySQL、JDBC、Redis +* Frame:Maven +* Java:JavaSE、JVM、JUC、Design +* SSM:MyBatis、Spring、SpringMVC、SpringBoot +* Tool:Git、Linux、Docker +* Web:HTML、CSS、Servlet、JavaScript + From 705c3aadd62532c11bfdf99bf8dcd1f415f98ea2 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 22 May 2021 22:26:01 +0800 Subject: [PATCH 018/242] Update Java Notes --- DB.md | 628 ++++++++++++++++++++++++++++++++++---------------------- Java.md | 177 +++++++++------- Tool.md | 3 +- Web.md | 10 +- 4 files changed, 494 insertions(+), 324 deletions(-) diff --git a/DB.md b/DB.md index b1473ca..2740890 100644 --- a/DB.md +++ b/DB.md @@ -162,7 +162,7 @@ MySQL配置: -## 单表 +## 单表操作 ### SQL @@ -557,17 +557,52 @@ MySQL配置: 数据库查询遵循条件在前的原则 ```mysql -SELECT:字段列表 -FROM:表名列表 -WHERE:条件列表 -GROUP BY:分组字段 -HAVING:分组之后的条件 -ORDER BY:排序 -LIMIT:分页限定 +SELECT DISTINCT + + +ORDER BY + +LIMIT ``` +*** + + + #### 查询全部 * 查询全部的表数据 @@ -587,7 +622,7 @@ LIMIT:分页限定 ``` * 去除重复查询 - 注意:只有全部重复的才可以去除 + 注意:只有值全部重复的才可以去除 ```mysql SELECT DISTINCT 列名1,列名2,... FROM 表名; @@ -628,14 +663,8 @@ LIMIT:分页限定 SELECT NAME,IFNULL(stock,0)+10 getsum FROM product; ``` -* **CONCAT()**:用于连接两个字段 - ```sql - SELECT CONCAT(TRIM(col1), '(', TRIM(col2), ')') AS concat_col FROM mytable - -- 许多数据库会使用空格把一个值填充为列宽,连接的结果出现一些不必要的空格,使用TRIM()可以去除首尾空格 - ``` - *** @@ -713,7 +742,7 @@ LIMIT:分页限定 ##### 聚合函数 -* 聚合函数:将一列数据作为一个整体,进行纵向的计算 +聚合函数:将一列数据作为一个整体,进行纵向的计算 * 聚合函数语法 @@ -755,18 +784,123 @@ LIMIT:分页限定 -##### 文本处理 +*** + + + +##### 文本函数 + +CONCAT():用于连接两个字段 + +```sql +SELECT CONCAT(TRIM(col1), '(', TRIM(col2), ')') AS concat_col FROM mytable +-- 许多数据库会使用空格把一个值填充为列宽,连接的结果出现一些不必要的空格,使用TRIM()可以去除首尾空格 +``` + +| 函数名称 | 作 用 | +| --------- | ------------------------------------------------------------ | +| LENGTH | 计算字符串长度函数,返回字符串的字节长度 | +| CONCAT | 合并字符串函数,返回结果为连接参数产生的字符串,参数可以使一个或多个 | +| INSERT | 替换字符串函数 | +| LOWER | 将字符串中的字母转换为小写 | +| UPPER | 将字符串中的字母转换为大写 | +| LEFT | 从左侧字截取符串,返回字符串左边的若干个字符 | +| RIGHT | 从右侧字截取符串,返回字符串右边的若干个字符 | +| TRIM | 删除字符串左右两侧的空格 | +| REPLACE | 字符串替换函数,返回替换后的新字符串 | +| SUBSTRING | 截取字符串,返回从指定位置开始的指定长度的字符换 | +| REVERSE | 字符串反转(逆序)函数,返回与原始字符串顺序相反的字符串 | + + + +*** + + + +##### 数字函数 + +| 函数名称 | 作 用 | +| --------------- | ---------------------------------------------------------- | +| ABS | 求绝对值 | +| SQRT | 求二次方根 | +| MOD | 求余数 | +| CEIL 和 CEILING | 两个函数功能相同,都是返回不小于参数的最小整数,即向上取整 | +| FLOOR | 向下取整,返回值转化为一个BIGINT | +| RAND | 生成一个0~1之间的随机数,传入整数参数是,用来产生重复序列 | +| ROUND | 对所传参数进行四舍五入 | +| SIGN | 返回参数的符号 | +| POW 和 POWER | 两个函数的功能相同,都是所传参数的次方的结果值 | +| SIN | 求正弦值 | +| ASIN | 求反正弦值,与函数 SIN 互为反函数 | +| COS | 求余弦值 | +| ACOS | 求反余弦值,与函数 COS 互为反函数 | +| TAN | 求正切值 | +| ATAN | 求反正切值,与函数 TAN 互为反函数 | +| COT | 求余切值 | + + + +*** + + + +##### 日期函数 + +| 函数名称 | 作 用 | +| ----------------------- | ------------------------------------------------------------ | +| CURDATE 和 CURRENT_DATE | 两个函数作用相同,返回当前系统的日期值 | +| CURTIME 和 CURRENT_TIME | 两个函数作用相同,返回当前系统的时间值 | +| NOW 和 SYSDATE | 两个函数作用相同,返回当前系统的日期和时间值 | +| MONTH | 获取指定日期中的月份 | +| MONTHNAME | 获取指定日期中的月份英文名称 | +| DAYNAME | 获取指定曰期对应的星期几的英文名称 | +| DAYOFWEEK | 获取指定日期对应的一周的索引位置值 | +| WEEK | 获取指定日期是一年中的第几周,返回值的范围是否为 0〜52 或 1〜53 | +| DAYOFYEAR | 获取指定曰期是一年中的第几天,返回值范围是1~366 | +| DAYOFMONTH | 获取指定日期是一个月中是第几天,返回值范围是1~31 | +| YEAR | 获取年份,返回值范围是 1970〜2069 | +| TIME_TO_SEC | 将时间参数转换为秒数 | +| SEC_TO_TIME | 将秒数转换为时间,与TIME_TO_SEC 互为反函数 | +| DATE_ADD 和 ADDDATE | 两个函数功能相同,都是向日期添加指定的时间间隔 | +| DATE_SUB 和 SUBDATE | 两个函数功能相同,都是向日期减去指定的时间间隔 | +| ADDTIME | 时间加法运算,在原始时间上添加指定的时间 | +| SUBTIME | 时间减法运算,在原始时间上减去指定的时间 | +| DATEDIFF | 获取两个日期之间间隔,返回参数 1 减去参数 2 的值 | +| DATE_FORMAT | 格式化指定的日期,根据参数返回指定格式的值 | +| WEEKDAY | 获取指定日期在一周内的对应的工作日索引 | + + + +**** + + + +#### 正则查询 + +正则表达式(Regular Expression)是指一个用来描述或者匹配一系列符合某个句法规则的字符串的单个字符串 + +```mysql +SELECT * FROM emp WHERE name REGEXP '^T'; -- 匹配以T开头的name值 +SELECT * FROM emp WHERE name REGEXP '2$'; -- 匹配以2结尾的name值 +SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 +``` -| 函数名 | 功能 | -| --------- | ------------------ | -| LEFT() | 左边的字符 | -| RIGHT() | 右边的字符 | -| LOWER() | 转换为小写字符 | -| UPPER() | 转换为大写字符 | -| LTRIM() | 去除左边的空格 | -| RTRIM() | 去除右边的空格 | -| LENGTH() | 长度去除右边的空格 | -| SOUNDEX() | 转换为语音值 | +| 符号 | 含义 | +| ------ | ----------------------------- | +| ^ | 在字符串开始处进行匹配 | +| $ | 在字符串末尾处进行匹配 | +| . | 匹配任意单个字符, 包括换行符 | +| [...] | 匹配出括号内的任意字符 | +| [^...] | 匹配不出括号内的任意字符 | +| a* | 匹配零个或者多个a(包括空串) | +| a+ | 匹配一个或者多个a(不包括空串) | +| a? | 匹配零个或者一个a | +| a1\|a2 | 匹配a1或a2 | +| a(m) | 匹配m个a | +| a(m,) | 至少匹配m个a | +| a(m,n) | 匹配m个a 到 n个a | +| a(,n) | 匹配0到n个a | +| (...) | 将模式元素组成单一元素 | @@ -881,7 +1015,7 @@ LIMIT:分页限定 -## 约束 +## 约束操作 ### 约束分类 @@ -1160,7 +1294,7 @@ LIMIT:分页限定 -## 多表 +## 多表操作 ### 多表设计 @@ -1623,7 +1757,138 @@ CREATE TABLE us_pro( u.name IN ('张三','李四'); ``` - + + + + + +*** + + + +## 事务机制 + +### 事务概述 + +事务:一条或多条 SQL 语句组成一个执行单元,其特点是这个单元要么同时成功要么同时失败 + +单元中的每条 SQL 语句都相互依赖,形成一个整体 + +* 如果某条 SQL 语句执行失败或者出现错误,那么整个单元就会回滚,撤回到事务最初的状态 + +* 如果单元中所有的 SQL 语句都执行成功,则事务就顺利执行 + + + +*** + + + +### 管理事务 + +管理事务的三个步骤 + +1. 开启事务:记录回滚点,并通知服务器,将要执行一组操作,要么同时成功、要么同时失败 + +2. 执行sql语句:执行具体的一条或多条sql语句 + +3. 结束事务(提交|回滚) + + - 提交:没出现问题,数据进行更新 + - 回滚:出现问题,数据恢复到开启事务时的状态 + + +事务操作: + +* 开启事务 + + ```mysql + START TRANSACTION; + ``` + +* 回滚事务 + + ```mysql + ROLLBACK; + ``` + +* 提交事务 + + ```mysql + COMMIT; + ``` + + + +* 管理实务演示 + + ```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; + ``` + + + +*** + + + +### 提交方式 + +提交方式: + +- 自动提交(MySQL默认为自动提交) +- 手动提交 + +提交方式语法: + +- 查看事务提交方式 + + ```mysql + SELECT @@AUTOCOMMIT; -- 1代表自动提交 0代表手动提交 + ``` + +- 修改事务提交方式 + + ```mysql + SET @@AUTOCOMMIT=数字; + ``` + + + +*** + + + +### 四大特征 + +事务的四大特征:ACID + +- 原子性(atomicity) + 原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响 + +- 一致性(consistency) + 一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态 + + 举例:拿转账来说,假设张三和李四两者的钱加起来一共是2000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是2000,这就是事务的一致性 + +- 隔离性(isolaction) + 隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离 + +- 持久性(durability) + 持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作 @@ -1631,9 +1896,59 @@ CREATE TABLE us_pro( -## 视图 +### 隔离界别 + +事务的隔离级别相关概述: + +* 事务的隔离级别 + 多个客户端操作时,各个客户端的事务之间应该是隔离的,**不同的事务之间不该互相影响**。而如果多个事务操作同一批数据时,则需要设置不同的隔离级别 , 否则就会产生问题 。 + +* 隔离级别分类 + + | 隔离级别 | 名称 | 类型 | 会引发的问题 | 数据库默认隔离级别 | + | ---------------- | -------- | -------- | ---------------------- | ------------------- | + | read uncommitted | 读未提交 | | 脏读、不可重复读、幻读 | | + | read committed | 读已提交 | 表级读锁 | 不可重复读、幻读 | Oracle / SQL Server | + | repeatable read | 可重复读 | 行级写锁 | 幻读 | MySQL | + | serializable | 串行化 | 表级写锁 | 无 | | -### 视图概述 +* 问题解释 + + | 问题 | 现象 | + | ---------- | ------------------------------------------------------------ | + | 脏读 | 是指在一个事务处理过程中读取了另一个未提交的事务中的数据 , 导致两次查询结果不一致 | + | 不可重复读 | 是指在一个事务处理过程中读取了另一个事务中修改并已提交的数据,导致两次查询结果不一致 | + | 幻读 | 读取过程中数据条目发生了变化,查询某数据不存在,准备插入此记录,但执行插入时发现此记录已存在,无法插入;或查询某数据不存在,执行delete删除,却发现删除成功 | + + + +**隔离级别操作语法:** + +* 查询数据库隔离级别 + + ```mysql + SELECT @@TX_ISOLATION; + ``` + +* 修改数据库隔离级别 + + ```mysql + SET GLOBAL TRANSACTION ISOLATION LEVEL 级别字符串; + ``` + + + + + +*** + + + +## 存储结构 + +### 视图 + +#### 视图概述 视图概念:视图是一种虚拟存在的数据表,这个虚拟的表并不在数据库中实际存在 @@ -1654,7 +1969,7 @@ CREATE TABLE us_pro( -### 视图创建 +#### 视图创建 * 创建视图 @@ -1708,7 +2023,7 @@ CREATE TABLE us_pro( -### 视图查询 +#### 视图查询 * 查询所有数据表,视图也会查询出来 @@ -1731,7 +2046,11 @@ CREATE TABLE us_pro( -### 视图修改 +*** + + + +#### 视图修改 视图表数据修改,会**自动修改源表中的数据**,因为更新的是视图中的基表中的数据 @@ -1767,9 +2086,11 @@ CREATE TABLE us_pro( +*** + -### 视图删除 +#### 视图删除 * 删除视图 @@ -1792,10 +2113,10 @@ CREATE TABLE us_pro( -## 过程 - ### 存储过程 +#### 基本介绍 + 存储过程和函数:存储过程和函数是事先经过编译并存储在数据库中的一段 SQL 语句的集合 存储过程和函数的好处: @@ -1816,7 +2137,7 @@ CREATE TABLE us_pro( -### 基本操作 +#### 基本操作 DELIMITER: @@ -1900,9 +2221,9 @@ DELIMITER: -### 存储语法 +#### 存储语法 -#### 变量使用 +##### 变量使用 存储过程是可以进行编程的,意味着可以使用变量、表达式、条件控制语句等,来完成比较复杂的功能 @@ -1955,7 +2276,7 @@ DELIMITER: -#### IF语句 +##### IF语句 * if语句标准语法 @@ -2007,7 +2328,7 @@ DELIMITER: -#### 参数传递 +##### 参数传递 * 参数传递的语法 @@ -2063,7 +2384,7 @@ DELIMITER: -#### CASE语句 +##### CASE * 标准语法1 @@ -2120,7 +2441,7 @@ DELIMITER: -#### WHILE循环 +##### WHILE * while循环语法 @@ -2163,7 +2484,7 @@ DELIMITER: -#### REPEAT循环 +##### REPEAT * repeat循环标准语法 @@ -2211,7 +2532,7 @@ DELIMITER: -#### LOOP循环 +##### LOOP LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定义,通常可以使用 LEAVE 语句实现,如果不加退出循环的语句,那么就变成了死循环 @@ -2261,7 +2582,7 @@ LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定 -#### 游标 +##### 游标 游标是用来存储查询结果集的数据类型,在存储过程和函数中可以使用光标对结果集进行循环的处理 * 游标可以遍历返回的多行结果,每次拿到一整行数据 @@ -2370,7 +2691,7 @@ LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定 -### 存储函数 +#### 存储函数 存储函数和存储过程是非常相似的。存储函数可以做的事情,存储过程也可以做到! @@ -2430,10 +2751,10 @@ LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定 -## 触发 - ### 触发器 +#### 基本介绍 + 触发器是与表有关的数据库对象,在insert/update/delete 之前或之后触发并执行触发器中定义的SQL语句 * 触发器的这种特性可以协助应用在数据库端确保数据的完整性 、日志记录 、数据校验等操作 @@ -2453,7 +2774,7 @@ LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定 -### 基本操作 +#### 基本操作 * 创建触发器 @@ -2489,7 +2810,7 @@ LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定 -### 触发演示 +#### 触发演示 通过触发器记录账户表的数据变更日志。包含:增加、修改、删除 @@ -2621,185 +2942,7 @@ LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定 -## 事务 - -### 事务概述 - -事务:一条或多条 SQL 语句组成一个执行单元,其特点是这个单元要么同时成功要么同时失败 - -单元中的每条 SQL 语句都相互依赖,形成一个整体 - -* 如果某条 SQL 语句执行失败或者出现错误,那么整个单元就会回滚,撤回到事务最初的状态 - -* 如果单元中所有的 SQL 语句都执行成功,则事务就顺利执行 - - - -*** - - - -### 管理事务 - -管理事务的三个步骤 - -1. 开启事务:记录回滚点,并通知服务器,将要执行一组操作,要么同时成功、要么同时失败 - -2. 执行sql语句:执行具体的一条或多条sql语句 - -3. 结束事务(提交|回滚) - - - 提交:没出现问题,数据进行更新 - - 回滚:出现问题,数据恢复到开启事务时的状态 - - -事务操作: - -* 开启事务 - - ```mysql - START TRANSACTION; - ``` - -* 回滚事务 - - ```mysql - ROLLBACK; - ``` - -* 提交事务 - - ```mysql - COMMIT; - ``` - - - -* 管理实务演示 - - ```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; - ``` - - - -*** - - - -### 提交方式 - -提交方式: - -- 自动提交(MySQL默认为自动提交) -- 手动提交 - -提交方式语法: - -- 查看事务提交方式 - - ```mysql - SELECT @@AUTOCOMMIT; -- 1代表自动提交 0代表手动提交 - ``` - -- 修改事务提交方式 - - ```mysql - SET @@AUTOCOMMIT=数字; - ``` - - - -*** - - - -### 四大特征 - -事务的四大特征:ACID - -- 原子性(atomicity) - 原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响 - -- 一致性(consistency) - 一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态 - - 举例:拿转账来说,假设张三和李四两者的钱加起来一共是2000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是2000,这就是事务的一致性 - -- 隔离性(isolaction) - 隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离 - -- 持久性(durability) - 持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作 - - - -*** - - - -### 隔离界别 - -事务的隔离级别相关概述: - -* 事务的隔离级别 - 多个客户端操作时,各个客户端的事务之间应该是隔离的,**不同的事务之间不该互相影响**。而如果多个事务操作同一批数据时,则需要设置不同的隔离级别 , 否则就会产生问题 。 - -* 隔离级别分类 - - | 隔离级别 | 名称 | 类型 | 会引发的问题 | 数据库默认隔离级别 | - | ---------------- | -------- | -------- | ---------------------- | ------------------- | - | read uncommitted | 读未提交 | | 脏读、不可重复读、幻读 | | - | read committed | 读已提交 | 表级读锁 | 不可重复读、幻读 | Oracle / SQL Server | - | repeatable read | 可重复读 | 行级写锁 | 幻读 | MySQL | - | serializable | 串行化 | 表级写锁 | 无 | | - -* 问题解释 - - | 问题 | 现象 | - | ---------- | ------------------------------------------------------------ | - | 脏读 | 是指在一个事务处理过程中读取了另一个未提交的事务中的数据 , 导致两次查询结果不一致 | - | 不可重复读 | 是指在一个事务处理过程中读取了另一个事务中修改并已提交的数据,导致两次查询结果不一致 | - | 幻读 | 读取过程中数据条目发生了变化,查询某数据不存在,准备插入此记录,但执行插入时发现此记录已存在,无法插入;或查询某数据不存在,执行delete删除,却发现删除成功 | - - - -**隔离级别操作语法:** - -* 查询数据库隔离级别 - - ```mysql - SELECT @@TX_ISOLATION; - ``` - -* 修改数据库隔离级别 - - ```mysql - SET GLOBAL TRANSACTION ISOLATION LEVEL 级别字符串; - ``` - - - - - -**** - - - -## 引擎 +## 存储引擎 ### 体系结构 @@ -2875,7 +3018,7 @@ MyISAM存储引擎 InnoDB存储引擎:(MySQL5.5版本后默认的存储引擎) -- 特点:支持事务和外键操作,支持并发控制。对比MyISAM的存储引擎,InnoDB写的处理效率差一些,并且会占用更多的磁盘空间以保留数据和索引 +- 特点:**支持事务**和外键操作,支持并发控制。对比MyISAM的存储引擎,InnoDB写的处理效率差一些,并且会占用更多的磁盘空间以保留数据和索引 - 应用场景:对事务的完整性有比较高的要求,在并发条件下要求数据的一致性,读写频繁的操作 - 存储方式: - 使用共享表空间存储, 这种方式创建的表的表结构保存在.frm文件中, 数据和索引保存在 innodb_data_home_dir 和 innodb_data_file_path定义的表空间中,可以是多个文件 @@ -2985,12 +3128,10 @@ MERGE存储引擎 -## 索引 +## 索引优化 ### 索引概述 - - MySQL官方对索引的定义为:索引(index)是帮助MySQL高效获取数据的一种数据结构。在表数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式指向数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。 索引使用:一张数据表,用于保存数据;一个索引配置文件,用于保存索引;每个索引都指向了某一个数据 @@ -3254,7 +3395,7 @@ B+Tree优点:提高查询速度,减少磁盘的IO次数,树形结构较小 - 使用唯一索引,区分度越高,使用索引的效率越高。 - 索引字段的选择,最佳候选列应当从where子句的条件中提取,如果where子句中的组合比较多,那么应当挑选最常用、过滤效果最好的列的组合。 - 使用短索引,索引创建之后也是使用硬盘来存储的,因此提升索引访问的I/O效率,也可以提升总体的访问效率。假如构成索引的字段总长度比较短,那么在给定大小的存储块内可以存储更多的索引值,相应的可以有效的提升MySQL访问索引的I/O效率。 -- 索引可以有效的提升查询数据的效率,但索引数量不是多多益善,索引越多,维护索引的代价越高。对于插入、更新、删除等DML操作比较频繁的表来说,索引过多,会引入相当高的维护代价,降低DML操作的效率,增加相应操作的时间消耗。另外索引过多的话,MySQL也会犯选择困难病,虽然最终仍然会找到一个可用的索引,但无疑提高了选择的代价。 +- 索引可以有效的提升查询数据的效率,但索引数量不是多多益善,索引越多,维护索引的代价越高。对于插入、更新、删除等DML操作比较频繁的表来说,索引过多,会引入相当高的维护代价,降低DML操作的效率,增加相应操作的时间消耗。另外索引过多的话,MySQL也会犯选择困难病,虽然最终仍然会找到一个可用的索引,但提高了选择的代价。 * MySQL建立联合索引时会遵守最左前缀匹配原则,即最左优先,在检索数据时从联合索引的最左边开始匹配 N个列组合而成的组合索引,相当于创建了N个索引,如果查询时where句中使用了组成该索引的**前**几个字段,那么这条查询SQL可以利用组合索引来提升查询效率 @@ -3284,7 +3425,7 @@ B+Tree优点:提高查询速度,减少磁盘的IO次数,树形结构较小 -## 优化 +## SQL优化 ### 优化步骤 @@ -3424,14 +3565,11 @@ EXPLAIN SELECT * FROM table_1 WHERE id = 1; MySQL执行计划的局限: * EXPLAIN不会告诉你关于触发器、存储过程的信息或用户自定义函数对查询的影响情况 - * EXPLAIN不考虑各种Cache - -* EXPLAIN不能显示MySQL在执行查询时所作的优化工作 - +* EXPLAIN不能显示MySQL在执行查询时所作的优化工作,因为执行计划在执行查询之前生成 * 部分统计信息是估算的,并非精确值 - * EXPALIN只能解释SELECT操作,其他操作要重写为SELECT后查看执行计划 +* 执行计划可以随着底层优化器输入的更改而更改。EXPLAIN PLAN 显示的是在解释语句时数据库将如何运行SQL语句,由于执行环境和 EXPLAIN PLAN 环境的不同,此计划可能与SQL语句实际的执行计划不同 环境准备: @@ -3884,7 +4022,7 @@ SHOW GLOBAL STATUS LIKE 'Handler_read%'; -### 优化功能 +### 优化语句 #### 批量插入 @@ -4208,7 +4346,7 @@ SQL提示,是优化数据库的一个重要手段,就是在SQL语句中加 -## 锁 +## 锁机制 ### 锁的概述 diff --git a/Java.md b/Java.md index 4d213bd..6e31543 100644 --- a/Java.md +++ b/Java.md @@ -3706,7 +3706,7 @@ java.util.regex 包主要包括以下三个类: 捕获组是把多个字符当一个单独单元进行处理的方法,它通过对括号内的字符分组来创建。 -在表达式( ( A ) ( B ( C ) ) ),有四个这样的组:((A)(B(C)))、(A)、(B(C))、(C)(按照括号从左到右) +在表达式( ( A ) ( B ( C ) ) ),有四个这样的组:((A)(B(C)))、(A)、(B(C))、(C)(按照括号从左到右为 group(1)) * 调用 matcher 对象的groupCount 方法返回一个 int值,表示matcher对象当前有多个捕获组。 * 特殊的组group(0)、group(),它代表整个表达式,该组不包括在 groupCount 的返回值中。 @@ -6576,10 +6576,11 @@ public class Demo{ ### lambda -#### 概述 +#### 基本介绍 -Lambda表达式是JDK1.8开始之后的新技术,是一种代码的新语法,一种特殊写法, -作用:为了简化匿名内部类的代码写法。 +Lambda表达式是JDK1.8开始之后的新技术,是一种代码的新语法,一种特殊写法 + +作用:为了简化匿名内部类的代码写法 Lambda表达式的格式: @@ -6591,41 +6592,89 @@ Lambda表达式的格式: Lambda表达式并不能简化所有匿名内部类的写法,只能简化**函数式接口的匿名内部类** -条件:首先必须是接口,接口中只能有一个抽象方法 +简化条件:首先必须是接口,接口中只能有一个抽象方法 @FunctionalInterface函数式接口注解:一旦某个接口加上了这个注解,这个接口只能有且仅有一个抽象方法 -#### 简化Runnable +*** + + + +#### 简化方法 + +Lambda表达式的省略写法(进一步在Lambda表达式的基础上继续简化) + +* 如果Lambda表达式的方法体代码只有一行代码,可以省略大括号不写,同时要省略分号;如果这行代码是return语句,必须省略return不写 +* 参数类型可以省略不写 +* 如果只有一个参数,参数类型可以省略,同时()也可以省略 + +```java +List names = new ArrayList<>(); +names.add("胡"); +names.add("甘"); +names.add("洪"); + +names.forEach(new Consumer() { + @Override + public void accept(String s) { + System.out.println(s); + } +}); + +names.forEach((String s) -> { + System.out.println(s); +}); + +names.forEach((s) -> { + System.out.println(s); +}); + +names.forEach(s -> { + System.out.println(s); +}); + +names.forEach(s -> System.out.println(s) ); +``` + + + +*** + + + +#### 常用简化 + +##### Runnable ```java //1. Thread t = new Thread(new Runnable() { @Override public void run() { - sout(Thread.currentThread().getName()+":执行~~~"); + System.out.println(Thread.currentThread().getName()+":执行~~~"); } }); t.start(); //2. Thread t1 = new Thread(() -> { - sout(Thread.currentThread().getName()+":执行~~~"); + System.out.println(Thread.currentThread().getName()+":执行~~~"); }); t1.start(); //3. new Thread(() -> { - sout(Thread.currentThread().getName()+":执行~~~"); + System.out.println(Thread.currentThread().getName()+":执行~~~"); }).start(); //4.一行代码 -new Thread(() -> sout(Thread.currentThread().getName()+":执行~~~")).start(); +new Thread(() -> System.out.println(Thread.currentThread().getName()+":执行~~~")).start(); ``` -#### 简化Comparator +##### Comparator ```java public class CollectionsDemo { @@ -6651,43 +6700,6 @@ public class CollectionsDemo { -#### 简化方法 - -Lambda表达式的省略写法(进一步在Lambda表达式的基础上继续简化) - (1)如果Lambda表达式的方法体代码只有一行代码。可以省略大括号不写,同时要省略分号! - (2)如果Lambda表达式的方法体代码只有一行代码。可以省略大括号不写。 - 此时,如果这行代码是return语句,必须省略return不写,同时也必须省略";"不写 - (3)参数类型可以省略不写。 - (4)如果只有一个参数,参数类型可以省略,同时()也可以省略。 - -```java -List names = new ArrayList<>(); -names.add("胡"); -names.add("甘"); -names.add("洪"); - -names.forEach(new Consumer() { - @Override - public void accept(String s) { - System.out.println(s); - } -}); - -names.forEach((String s) -> { - System.out.println(s); -}); - -names.forEach((s) -> { - System.out.println(s); -}); - -names.forEach(s -> { - System.out.println(s); -}); - -names.forEach(s -> System.out.println(s) ); -``` - *** @@ -6696,11 +6708,13 @@ names.forEach(s -> System.out.println(s) ); ### 方法引用 -#### 概述 +#### 基本介绍 + +方法引用:方法引用是为了进一步简化Lambda表达式的写法 -方法引用:方法引用是为了进一步简化Lambda表达式的写法。 -方法引用的格式:类型或者对象::引用的方法。 -关键语法是:“::” +方法引用的格式:类型或者对象::引用的方法 + +关键语法是:`::` ```java lists.forEach( s -> System.out.println(s)); @@ -6710,9 +6724,13 @@ lists.forEach(System.out::println); +*** + + + #### 静态方法 -引用格式:类名::静态方法 +引用格式:`类名::静态方法` 简化步骤:定义一个静态方法,把需要简化的代码放到一个静态方法中去 @@ -6720,10 +6738,10 @@ lists.forEach(System.out::println); ```java //定义集合加入几个Student元素 - // 使用静态方法进行简化! - Collections.sort(lists, (o1, o2) -> Student.compareByAge(o1 , o2)); - // 如果前后参数是一样的,而且方法是静态方法,既可以使用静态方法引用 - Collections.sort(lists, Student::compareByAge); +// 使用静态方法进行简化! +Collections.sort(lists, (o1, o2) -> Student.compareByAge(o1 , o2)); +// 如果前后参数是一样的,而且方法是静态方法,既可以使用静态方法引用 +Collections.sort(lists, Student::compareByAge); public class Student { private String name ; @@ -6737,9 +6755,13 @@ public class Student { +*** + + + #### 实例方法 -引用格式:对象::实例方法 +引用格式:`对象::实例方法` 简化步骤:定义一个实例方法,把需要的代码放到实例方法中去 @@ -6763,13 +6785,17 @@ public class MethodDemo { +*** + + + #### 特定类型 -特定类型:String ,任何类型 +特定类型:String,任何类型 -引用格式:特定类型::方法 +引用格式:`特定类型::方法` -注意事项:如果第一个参数列表中的形参中的第一个参数作为了后面的方法的调用者,并且其余参数作为后面方法的形参,那么就可以用特定类型方法引用了。 +注意事项:如果第一个参数列表中的形参中的第一个参数作为了后面的方法的调用者,并且其余参数作为后面方法的形参,那么就可以用特定类型方法引用了 ```java public class MethodDemo{ @@ -6798,11 +6824,15 @@ public class MethodDemo{ +*** + + + #### 构造器 -格式:类名::new。 -注意事项:前后参数一致的情况下,又在创建对象就可以使用构造器引用 - s -> new Student(s) => Student::new +格式:`类名::new` + +注意事项:前后参数一致的情况下,又在创建对象,就可以使用构造器引用 ```java public class ConstructorDemo { @@ -6816,14 +6846,13 @@ public class ConstructorDemo { Object[] objs = lists.toArray(); // 我们想指定转换成字符串类型的数组!最新的写法可以结合构造器引用实现 - // default T[] toArray(IntFunction generator) String[] strs = lists.toArray(new IntFunction() { @Override public String[] apply(int value) { return new String[value]; } }); - String[] strs1 = lists.toArray(s -> new String[s] ); + String[] strs1 = lists.toArray(s -> new String[s]); String[] strs2 = lists.toArray(String[]::new); System.out.println("String类型的数组:"+ Arrays.toString(strs2)); @@ -14029,11 +14058,11 @@ HotSpot VM采用的热点探测方式是基于计数器的热点探测,为每 在HotSpot VM中内嵌有两个JIT编译器,分别为Client Compiler和Server Compiler,简称为C1编译器和C2编译器 -C1编译器会对字节码进行简单可靠的优化,耗时短,以达到更快的编译速度。C1编译器的优化方法: +C1编译器会对字节码进行简单可靠的优化,耗时短,以达到更快的编译速度,C1编译器的优化方法: * 方法内联:将引用的函数代码编译到引用点处,这样可以减少栈帧的生成,减少参数传递以及跳转过程 - 方法内联能够消除方法调用的固定开销。任何方法调用除非被内联,否则都会有固定开销,来源于保存程序在该方法中的执行位置,以及新建、压入和弹出新方法所使用的栈帧。 + 方法内联能够消除方法调用的固定开销。任何方法除非被内联,否则调用都会有固定开销,来源于保存程序在该方法中的执行位置,以及新建、压入和弹出新方法所使用的栈帧。 ```java private static int square(final int i) { @@ -14058,9 +14087,11 @@ C1编译器会对字节码进行简单可靠的优化,耗时短,以达到更 * 冗余消除:在运行期间把一些不会执行的代码折叠掉 -C2编译器进行耗时较长的优化以及激进优化,优化的代码执行效率更高。C2的优化主要是在全局层面,逃逸分析是优化的基础:标量替换、栈上分配、同步消除 +* 内联缓存:是一种加快动态绑定的优化技术 -参数设置: +C2编译器进行耗时较长的优化以及激进优化,优化的代码执行效率更高,C2的优化主要是在全局层面,逃逸分析是优化的基础:标量替换、栈上分配、同步消除 + +VM 参数设置: - -client:指定Java虚拟机运行在Client模式下,并使用C1编译器 - -server:指定Java虚拟机运行在Server模式下,并使用C2编译器 @@ -14417,6 +14448,8 @@ Passenger 类的方法表包括两个方法,分别对应 0 号和 1 号。方 ### 字节码 +(字节码部分笔记待优化) + #### 类结构 class文件是编译器编译之后供虚拟机解释执行的二进制字节码文件,一个class文件对应一个public类型的类 @@ -15511,6 +15544,8 @@ public class GeneratedMethodAccessor1 extends MethodAccessorImpl { ### 服务器性能 +(调优部分笔记待优化) + 对于一个系统要部署上线时,则一定会对JVM进行调整,不经过任何调整直接上线,容易出现线上系统频繁FullGC造成系统卡顿、CPU使用频率过高、系统无反应等问题 对于一个应用来说通常重点关注的性能指标主要是吞吐量、响应时间、QPS、TPS等、并发用户数等,而这些性能指标又依赖于系统服务器的资源,如:CPU、内存、磁盘IO、网络IO等。对于这些指标数据的收集,通常可以根据Java本身的工具或指令进行查询 @@ -23916,7 +23951,7 @@ final void updateHead(Node h, Node p) { -# Pattern +# Design ## 单例模式 diff --git a/Tool.md b/Tool.md index f017b6e..9aee30c 100644 --- a/Tool.md +++ b/Tool.md @@ -1326,12 +1326,11 @@ Linux系统是一种典型的多用户系统,不同的用户处于不同的地 Linux文件属性有两种设置方法,一种是数字,一种是符号 -Linux的文件调用权限分为三级 : 文件属主、属组、其他。利用 chmod 可以控制文件如何被他人所调用。 +Linux的文件调用权限分为三级 : 文件属主、属组、其他,利用 chmod 可以控制文件如何被他人所调用。 ```shell chmod [-cfvR] [--help] [--version] mode file... mode : 权限设定字串,格式: [ugoa...][[+-=][rwxX]...][,...] - ``` * u 表示档案的拥有者,g 表示与该档案拥有者属于同一个group者,o表示其他的人,a 表示这三者皆是。 diff --git a/Web.md b/Web.md index 38819e3..4848b36 100644 --- a/Web.md +++ b/Web.md @@ -2297,7 +2297,7 @@ URI:统一资源标志符 -# EE +# Servlet ## JavaEE @@ -8433,11 +8433,9 @@ v-on:为 HTML 标签绑定事件,有简写方式 ## Element -- Element:网站快速成型工具。是饿了么公司前端开发团队提供的一套基于Vue的网站组件库。 +Element:网站快速成型工具,是饿了么公司前端开发团队提供的一套基于Vue的网站组件库,使用Element前提必须要有Vue -- 使用Element前提必须要有Vue。 - -- 组件:组成网页的部件,例如超链接、按钮、图片、表格等等~ +组件:组成网页的部件,例如超链接、按钮、图片、表格等等 - Element官网:https://element.eleme.cn/#/zh-CN @@ -8520,7 +8518,7 @@ v-on:为 HTML 标签绑定事件,有简写方式 -## 自定义组件 +## 自定义 对组件的封装 From 67150994273826b16ed7a41dd9c80c7e9e091bfa Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 23 May 2021 19:33:44 +0800 Subject: [PATCH 019/242] Update Java Notes --- DB.md | 825 +++++++++++++++++++++++++++++++++++++++++++++---------- Frame.md | 2 +- Java.md | 134 +++++---- SSM.md | 78 +++--- 4 files changed, 815 insertions(+), 224 deletions(-) diff --git a/DB.md b/DB.md index 2740890..baa706a 100644 --- a/DB.md +++ b/DB.md @@ -689,8 +689,8 @@ LIMIT | = | 等于 | | <> 或 != | 不等于 | | BETWEEN ... AND ... | 在某个范围之内(都包含) | - | **IN(...)** | 多选一 | - | **LIKE 占位符** | 模糊查询:_单个任意字符、%任意个字符、[] 匹配集合内的字符
`LIKE '[^AB]%' `:不以 A 和 B 开头的任意文本 | + | IN(...) | 多选一 | + | LIKE | **模糊查询**:_单个任意字符、%任意个字符、[] 匹配集合内的字符
`LIKE '[^AB]%' `:不以 A 和 B 开头的任意文本 | | IS NULL | 是NULL | | IS NOT NULL | 不是NULL | | AND 或 && | 并且 | @@ -1849,7 +1849,7 @@ CREATE TABLE us_pro( 提交方式: -- 自动提交(MySQL默认为自动提交) +- 自动提交(MySQL默认为自动提交) - 手动提交 提交方式语法: @@ -1863,9 +1863,10 @@ CREATE TABLE us_pro( - 修改事务提交方式 ```mysql - SET @@AUTOCOMMIT=数字; + SET @@AUTOCOMMIT=数字; -- 系统 + SET AUTOCOMMIT=数字; -- 会话 ``` - + *** @@ -1898,29 +1899,24 @@ CREATE TABLE us_pro( ### 隔离界别 -事务的隔离级别相关概述: +事务的隔离级别:多个客户端操作时,各个客户端的事务之间应该是隔离的,**不同的事务之间不该互相影响**,而如果多个事务操作同一批数据时,则需要设置不同的隔离级别 , 否则就会产生问题 。 -* 事务的隔离级别 - 多个客户端操作时,各个客户端的事务之间应该是隔离的,**不同的事务之间不该互相影响**。而如果多个事务操作同一批数据时,则需要设置不同的隔离级别 , 否则就会产生问题 。 +隔离级别分类: -* 隔离级别分类 +| 隔离级别 | 名称 | 类型 | 会引发的问题 | 数据库默认隔离级别 | +| ---------------- | -------- | -------- | ---------------------- | ------------------- | +| read uncommitted | 读未提交 | | 脏读、不可重复读、幻读 | | +| read committed | 读已提交 | 表级读锁 | 不可重复读、幻读 | Oracle / SQL Server | +| repeatable read | 可重复读 | 行级写锁 | 幻读 | MySQL | +| serializable | 串行化 | 表级写锁 | 无 | | - | 隔离级别 | 名称 | 类型 | 会引发的问题 | 数据库默认隔离级别 | - | ---------------- | -------- | -------- | ---------------------- | ------------------- | - | read uncommitted | 读未提交 | | 脏读、不可重复读、幻读 | | - | read committed | 读已提交 | 表级读锁 | 不可重复读、幻读 | Oracle / SQL Server | - | repeatable read | 可重复读 | 行级写锁 | 幻读 | MySQL | - | serializable | 串行化 | 表级写锁 | 无 | | +* 丢失更新 (Lost Update):当两个或多个事务选择同一行,最初的事务修改的值,被后面事务修改的值覆盖,所有的隔离级别都可以避免丢失更新 -* 问题解释 - - | 问题 | 现象 | - | ---------- | ------------------------------------------------------------ | - | 脏读 | 是指在一个事务处理过程中读取了另一个未提交的事务中的数据 , 导致两次查询结果不一致 | - | 不可重复读 | 是指在一个事务处理过程中读取了另一个事务中修改并已提交的数据,导致两次查询结果不一致 | - | 幻读 | 读取过程中数据条目发生了变化,查询某数据不存在,准备插入此记录,但执行插入时发现此记录已存在,无法插入;或查询某数据不存在,执行delete删除,却发现删除成功 | +* 脏读 (Dirty Reads):在一个事务处理过程中读取了另一个未提交的事务中的数据 , 导致两次查询结果不一致 +* 不可重复读 (Non-Repeatable Reads):是指在一个事务处理过程中读取了另一个事务中修改并已提交的数据,导致两次查询结果不一致 +* 幻读 (Phantom Reads):读取过程中数据条目发生了变化,查询某数据不存在,准备插入此记录,但执行插入时发现此记录已存在,无法插入;或查询某数据不存在,执行delete删除,却发现删除成功 **隔离级别操作语法:** @@ -1928,6 +1924,7 @@ CREATE TABLE us_pro( ```mysql SELECT @@TX_ISOLATION; + SHOW VARIABLES LIKE 'tx_isolation'; ``` * 修改数据库隔离级别 @@ -3425,7 +3422,7 @@ B+Tree优点:提高查询速度,减少磁盘的IO次数,树形结构较小 -## SQL优化 +## 优化语句 ### 优化步骤 @@ -4022,7 +4019,7 @@ SHOW GLOBAL STATUS LIKE 'Handler_read%'; -### 优化语句 +### 优化功能 #### 批量插入 @@ -4052,7 +4049,7 @@ LOAD DATA LOCAL INFILE = '/home/seazean/sql1.log' INTO TABLE `tb_user_1` FIELD T 3. 手动提交事务:如果应用使用自动提交的方式,建议在导入前执行`SET AUTOCOMMIT=0`,关闭自动提交;导入结束后再执行 SET AUTOCOMMIT=1,打开自动提交,可以提高导入的效率。 - 事务需要控制大小,事务太大可能会影响执行的效率。MySQL有innodb_log_buffer_size配置项,超过这个值的日志会使用磁盘数据,效率会下降。所以在事务大小达到配置项数据级前进行事务提交可以提高效率 + 事务需要控制大小,事务太大可能会影响执行的效率。MySQL有 innodb_log_buffer_size 配置项,超过这个值的日志会写入磁盘数据,效率会下降。所以在事务大小达到配置项数据级前进行事务提交可以提高效率 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL插入数据手动提交事务.png) @@ -4340,15 +4337,317 @@ 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) + +* 分布式数据库架构:适合大数据量、负载高的情况,具有良好的拓展性和高可用性。通过在多台服务器之间分布数据,可以实现在多台服务器之间的负载均衡,提高访问效率 + + + +*** + + + +### 缓存优化 + +#### 工作流程 + +开启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 + + + +*** + + + +### 内存优化 + +#### 优化原则 + +三个原则: + +* 将尽量多的内存分配给 MySQL 做缓存,但也要给操作系统和其他程序预留足够内存 +* MyISAM 存储引擎的数据文件读取依赖于操作系统自身的 IO 缓存,如果有MyISAM表,就要预留更多的内存给操作系统做 IO 缓存 +* 排序区、连接区等缓存是分配给每个数据库会话(Session)专用的,值的设置要根据最大连接数合理分配,如果设置太大,不但浪费资源,而且在并发数较高时会导致物理内存耗尽 + + + +*** + + + +#### MyISAM + +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 独占的,如果默认值设置太大,就会造成内存浪费 + + + +*** + + + +#### InnoDB + +Innodb 用一块内存区做 IO 缓存池,该缓存池不仅用来缓存 Innodb 的索引块,也用来缓存 Innodb 的数据块 + +* innodb_buffer_pool_size:该变量决定了 Innodb 存储引擎表数据和索引数据的最大缓存区大小 + + ```mysql + SHOW VARIABLES LIKE 'innodb_buffer_pool_size'; + ``` + + 在保证操作系统及其他程序有足够内存可用的情况下,innodb_buffer_pool_size 的值越大,缓存命中率越高,访问InnoDB表需要的磁盘I/O 就越少,性能也就越高。通过配置文件修改: + + ```sh + innodb_buffer_pool_size=512M + ``` + +* innodb_log_buffer_size:该值决定了 Innodb 日志缓冲区的大小,保存要写入磁盘上的日志文件数据 + + 对于可能产生大量更新记录的大事务,增加 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 + + 对于需要快速反馈的业务系统,可以将行锁的等待时间调小,以避免事务被长时间挂起; 对于后台运行的批量处理程序来说,可以将行锁的等待时间调大,以避免发生大的回滚操作 + + + + + +**** + + + ## 锁机制 -### 锁的概述 +### 基本介绍 锁机制:数据库为了保证数据的一致性,在共享的资源被并发访问时变得安全有序所设计的一种规则 @@ -4360,11 +4659,11 @@ SQL提示,是优化数据库的一个重要手段,就是在SQL语句中加 - 共享锁:也叫读锁。对同一份数据,多个事务读操作可以同时加锁而不互相影响 ,但不能修改数据 - 排他锁:也叫写锁。当前的操作没有完成前,会阻断其他操作的读取和写入 - 按粒度分类: - - 表级锁:会锁定整个表。开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低。偏向于MyISAM存储引擎! - - 行级锁:会锁定当前操作行。开销大,加锁慢;会出现死锁;锁定力度小,发生锁冲突概率低,并发度高。偏向于InnoDB存储引擎! - - 页级锁:锁的力度、发生冲突的概率和加锁开销介于表锁和行锁之间,会出现死锁,并发性能一般。 + - 表级锁:会锁定整个表,开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低,偏向MyISAM 存储引擎 + - 行级锁:会锁定当前操作行,开销大,加锁慢;会出现死锁;锁定力度小,发生锁冲突概率低,并发度高,偏向InnoDB 存储引擎 + - 页级锁:锁的力度、发生冲突的概率和加锁开销介于表锁和行锁之间,会出现死锁,并发性能一般 - 按使用方式分类: - - 悲观锁:每次查询数据时都认为别人会修改,很悲观,所以查询时加锁。 + - 悲观锁:每次查询数据时都认为别人会修改,很悲观,所以查询时加锁 - 乐观锁:每次查询数据时都认为别人不会修改,很乐观,但是更新时会判断一下在此期间别人有没有去更新这个数据 * 不同存储引擎支持的锁 @@ -4376,192 +4675,432 @@ SQL提示,是优化数据库的一个重要手段,就是在SQL语句中加 | MEMORY | 支持 | 不支持 | 不支持 | | BDB | 支持 | 不支持 | 支持 | +从锁的角度来说:表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如 Web 应用;而行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并查询的应用,如一些在线事务处理系统 + *** -### InnoDB +### MyISAM + +#### 表级锁 + +MyISAM 存储引擎只支持表锁,这也是MySQL开始几个版本中唯一支持的锁类型 -#### 共享锁 +MyISAM 在执行查询语句前,会自动给涉及的所有表加读锁,在执行更新操作前,会自动给涉及的表加写锁,这个过程并不需要用户干预,所以用户一般不需要直接用 LOCK TABLE 命令给 MyISAM 表显式加锁 -共享锁:数据可以被多个事务查询,但是不能修改 +* 加锁命令: -* 加入共享锁 + 读锁:**所有**连接只能读取数据,不能修改 + + 写锁:**其他**连接不能查询和修改数据 ```mysql - SELECT语句 LOCK IN SHARE MODE; - -- InnoDB引擎默认是行锁 - -- InnoDB引擎如果不采用带索引的列。则会提升为表锁 + -- 读锁 + LOCK TABLE table_name READ; + + -- 写锁 + LOCK TABLE table_name WRITE; ``` -* 数据准备 +* 解锁命令: ```mysql - -- 创建db13数据库 - CREATE DATABASE db13; - - -- 使用db13数据库 - USE db13; - - -- 创建student表 - CREATE TABLE student( - id INT PRIMARY KEY AUTO_INCREMENT, - NAME VARCHAR(10), - age INT, - score INT - ); - -- 添加数据 - INSERT INTO student VALUES (NULL,'张三',23,99),(NULL,'李四',24,95), - (NULL,'王五',25,98),(NULL,'赵六',26,97); + -- 将当前会话所有的表进行解锁 + UNLOCK TABLES; ``` -* 共享锁演示 +锁的兼容性: + +* 对MyISAM 表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求 +* 对MyISAM 表的写操作,则会阻塞其他用户对同一表的读和写操作 + +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 锁的兼容性.png) + +MyISAM 的读写锁调度是写优先,因为写锁后其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永远阻塞,所以 MyISAM 不适合做写为主的表的存储引擎 + + + +*** + + + +#### 读锁操作 + +两个客户端操作 Client 1和 Client 2,简化为 C1、C2 + +* 数据准备: ```mysql - -- 开启事务 - START TRANSACTION; - - -- 查询id为1的数据记录。加入共享锁 - SELECT * FROM student WHERE id=1 LOCK IN SHARE MODE; + 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 ; - -- 查询分数为99分的数据记录。加入共享锁,不采用带索引的列,提升为表锁 - SELECT * FROM student WHERE score=99 LOCK IN SHARE MODE; - - -- 提交事务 - COMMIT; + 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 - -- 窗口2 - -- 开启事务 - START TRANSACTION; - - -- 查询id为1的数据记录(普通查询,可以查询) - SELECT * FROM student WHERE id=1; - - -- 查询id为1的数据记录,并加入共享锁(可以查询。共享锁和共享锁兼容) - SELECT * FROM student WHERE id=1 LOCK IN SHARE MODE; - - -- 修改id为1的姓名为张三三(不能修改,会出现锁的情况。只有窗口1提交事务后,才能修改成功) - UPDATE student SET NAME='张三三' WHERE id = 1; - - -- 修改id为2的姓名为李四四(修改成功,InnoDB引擎默认是行锁) - UPDATE student SET NAME='李四四' WHERE id = 2; - - -- 修改id为3的姓名为王五五(注意:InnoDB引擎如果不采用带索引的列。则会提升为表锁) - UPDATE student SET NAME='王五五' WHERE id = 3; - - -- 提交事务 - COMMIT; + 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) +* 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 - SELECT语句 FOR UPDATE; + 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 语句立即执行 + + + +*** + + + +#### 写锁操作 + +两个客户端操作 Client 1和 Client 2,简化为 C1、C2 + +* C1 加写锁,C1、C2查询表,C1 正常查询,C2 需要等待 ```mysql - -- 窗口1 - -- 开启事务 - START TRANSACTION; - - -- 查询id为1的数据记录,并加入排他锁 - SELECT * FROM student WHERE id=1 FOR UPDATE; - - -- 提交事务 - COMMIT; + LOCK TABLE tb_book WRITE; -- C1 + SELECT * FROM tb_book; -- C1、C2 ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 写锁1.png) + + 当在 C1 中释放锁指令 UNLOCK TABLES,C2 中的 SELECT 语句立即执行 + +* C1、C2 同时加写锁 + ```mysql - -- 窗口2 - -- 开启事务 - START TRANSACTION; - - -- 查询id为1的数据记录(普通查询没问题) - SELECT * FROM student WHERE id=1; - - -- 查询id为1的数据记录,并加入共享锁(不能查询。因为排他锁不能和其他锁共存) - SELECT * FROM student WHERE id=1 LOCK IN SHARE MODE; - - -- 查询id为1的数据记录,并加入排他锁(不能查询。因为排他锁不能和其他锁共存) - SELECT * FROM student WHERE id=1 FOR UPDATE; - - -- 修改id为1的姓名为张三(不能修改,会出现锁的情况。只有窗口1提交事务后,才能修改成功) - UPDATE student SET NAME='张三' WHERE id=1; - - -- 提交事务 - COMMIT; + LOCK TABLE tb_book WRITE; ``` - + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 写锁2.png) + +* C1 加写锁,C1、C2查询未锁定的表,C1 报错,C2 正常查询 + +*** + + + +#### 锁争用 + +* 查看锁争用: + + ```mysql + SHOW OPEN TABLES; + ``` + + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-锁争用情况查看1.png) + + In_user:表当前被查询使用的次数,如果该数为零,则表是打开的,但是当前没有被使用 + + Name_locked:表名称是否被锁定,名称锁定用于取消表或对表进行重命名等操作 + + ```mysql + LOCK TABLE tb_book READ; -- 执行命令 + ``` + + ![](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,此值高说明存在着较为严重的表级锁争用情况 + + + +*** + -#### 兼容性 -锁的兼容性 +### InnoDB + +#### 行级锁 + +InnoDB 与 MyISAM 的最大不同有两点:一是支持事务;二是 采用了行级锁 + +InnoDB 实现了以下两种类型的行锁: + +- 共享锁 (S):又称为读锁,简称S锁,就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改 +- 排他锁 (X):又称为写锁,简称X锁,就是不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,只有获取排他锁的事务是可以对数据读取和修改 + +对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁;对于普通SELECT语句,InnoDB不会加任何锁 + +锁的兼容性: - 共享锁和共享锁 兼容 - 共享锁和排他锁 冲突 - 排他锁和排他锁 冲突 - 排他锁和共享锁 冲突 +可以通过以下语句显示给数据集加共享锁或排他锁: +```mysql +SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE -- 共享锁 +SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 +``` -**** +*** + -### MyISAM -读锁:**所有**连接只能读取数据,不能修改 +#### 锁操作 -写锁:**其他**连接不能查询和修改数据 +两个客户端操作 Client 1和 Client 2,简化为 C1、C2 -* 加锁 +* 环境准备 ```mysql - -- 读锁 - LOCK TABLE 表名 READ; + CREATE TABLE test_innodb_lock( + id INT(11), + name VARCHAR(16), + sex VARCHAR(1) + )ENGINE = INNODB DEFAULT CHARSET=utf8; - -- 写锁 - LOCK TABLE 表名 WRITE; + 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 - -- 将当前会话所有的表进行解锁 - UNLOCK TABLES; + SET AUTOCOMMIT=0; -- C1、C2 ``` - + 正常查询数据: + + ```mysql + SELECT * FROM test_innodb_lock; -- C1、C2 + ``` + +* 查询 id 为 3 的数据,正常查询: + + ```mysql + SELECT * FROM test_innodb_lock WHERE id=3; -- C1、C2 + ``` + + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作1.png) + +* C1 更新 id 为 3 的数据,但不提交: + + ```mysql + UPDATE test_innodb_lock SET name='300' WHERE id=3; -- C1 + ``` + + ![](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 + ``` + + ![](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 直接解除阻塞,直接更新 + +* 操作不同行的数据: + + ```mysql + UPDATE test_innodb_lock SET name='10' WHERE id=1; -- C1 + UPDATE test_innodb_lock SET name='30' WHERE id=3; -- C2 + ``` + + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作6.png) + + 由于C1、C2 操作的不同行,获取不同的行锁,所以都可以正常获取行锁 + + *** -### 乐观锁 +#### 锁升级 -悲观锁和乐观锁使用前提: +五索引行锁升级为表锁:不通过索引检索数据,那么 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会对间隙进行加锁,这种锁机制就是间隙锁 (Next-Key锁) + +* 关闭自动提交功能: + + ```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 + ``` + + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 间隙锁2.png) + + 出现间隙锁,C2 被阻塞,等待C1 提交事务后才能更新 + + + +*** + + + +#### 锁争用 + +```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:从系统启动到现在等待最长的一次所花的时间 + +* Innodb_row_lock_waits:系统启动后到现在总共等待的次数 + +当等待的次数很高,而且每次等待的时长也不短的时候,我们就需要分析系统中为什么会有如此多的等待,然后根据分析结果制定优化计划 + + + +*** + + + +#### 锁优化 + +InnoDB 存储引擎实现了行级锁定,虽然在锁定机制的实现方面带来了性能损耗可能比表锁会更高,但是在整体并发处理能力方面要远远优于 MyISAM 的表锁,当系统并发量较高的时候,InnoDB 的整体性能远远好于 MyISAM + +InnoDB 的行级锁,如果使用不当可能会让InnoDB 的整体性能表现不仅不能比 MyISAM 高,甚至可能会更差 + +优化建议: + +- 尽可能让所有数据检索都能通过索引来完成,避免无索引行锁升级为表锁 +- 合理设计索引,尽量缩小锁的范围 +- 尽可能减少索引条件及索引范围,避免间隙锁 +- 尽量控制事务大小,减少锁定资源量和时间长度 +- 尽可使用低级别事务隔离(需要业务层面满足需求) + + + +*** + + + +### 乐观锁 + +悲观锁和乐观锁使用前提: + +- 对于读的操作远多于写的操作的时候,一个更新操作加锁会阻塞所有的读取操作,降低了吞吐量,最后需要释放锁,锁是需要一些开销的,这时候可以选择乐观锁 +- 如果是读写比例差距不是非常大或者系统没有响应不及时,吞吐量瓶颈的问题,那就不要去使用乐观锁,它增加了复杂度,也带来了业务额外的风险,这时候可以选择悲观锁 + +乐观锁的现方式: * 版本号 @@ -4591,14 +5130,12 @@ SQL提示,是优化数据库的一个重要手段,就是在SQL语句中加 UPDATE city SET NAME='北京市',VERSION=VERSION+1 WHERE NAME='北京' AND VERSION=1; ``` - - * 时间戳 - 和版本号方式基本一样,给数据表中添加一个列,名称无所谓,数据类型需要是**timestamp** - - 每次更新后都将最新时间插入到此列。 - - 读取数据时,将时间读取出来,在执行更新的时候,比较时间。 - - 如果相同则执行更新,如果不相同,说明此条数据已经发生了变化。 + - 每次更新后都将最新时间插入到此列 + - 读取数据时,将时间读取出来,在执行更新的时候,比较时间 + - 如果相同则执行更新,如果不相同,说明此条数据已经发生了变化 diff --git a/Frame.md b/Frame.md index 3803efb..9411247 100644 --- a/Frame.md +++ b/Frame.md @@ -1418,5 +1418,5 @@ Log4j是Apache的一个开源项目。 # Netty - +(暂未学习) diff --git a/Java.md b/Java.md index 6e31543..738eb8f 100644 --- a/Java.md +++ b/Java.md @@ -1597,7 +1597,7 @@ static 静态修饰的成员(方法和成员变量)属于类本身的。 * **提高代码的复用**,相同代码可以定义在父类中 * 子类继承父类,可以直接使用父类这些代码(相同代码重复利用) -* 子类得到父类的属性(成员变量)和行为(方法),子类得到了父类的功能,还有自己的功能,子类更强大 +* 子类得到父类的属性(成员变量)和行为(方法),还可以定义自己的功能,子类更强大 继承的特点: @@ -2644,7 +2644,7 @@ public class CodeDemo { ### Object -#### 概述 +#### 基本介绍 Object类是Java中的祖宗类,一个类或者默认继承Object类,或者间接继承Object类,Object类的方法是一切子类都可以直接使用 @@ -2692,16 +2692,15 @@ hashCode的作用: #### 深浅克隆 -clone() 是 Object 的 protected 方法,一个类不显式去重写 clone(),就不能直接去调用该类实例的clone()方法 - -克隆就是制造一个对象的副本,根据所要克隆的对象的成员变量中是否含有引用类型,可以将克隆分为两种:浅克隆(Shallow Clone) 和 深克隆(Deep Clone),默认情况下使用Object中的clone方法进行克隆就是浅克隆,即完成对象域对域的拷贝 +深浅拷贝(克隆)的概念: -* 浅拷贝(shallowCopy):对基本数据类型进行值传递,对引用数据类型只是复制了引用,被复制对象属性的所有的引用仍然指向原来的对象,简而言之就是增加了一个指针指向原来对象的内存地址 +* 浅拷贝 (shallowCopy):对基本数据类型进行值传递,对引用数据类型只是复制了引用,被复制对象属性的所有的引用仍然指向原来的对象,简而言之就是增加了一个指针指向原来对象的内存地址 -* 深拷贝(deepCopy):对基本数据类型进行值传递,对引用数据类型是一个整个独立的对象拷贝,会拷贝所有的属性并指向的动态分配的内存,简而言之就是把所有属性复制到一个新的内存,增加一个指针指向这个新的内存 -* 使用深拷贝的情况下,释放内存的时候不会因为出现浅拷贝时释放同一个内存的错误 +* 深拷贝 (deepCopy):对基本数据类型进行值传递,对引用数据类型是一个整个独立的对象拷贝,会拷贝所有的属性并指向的动态分配的内存,简而言之就是把所有属性复制到一个新的内存,增加一个指针指向新内存。所以使用深拷贝的情况下,释放内存的时候不会出现使用浅拷贝时释放同一块内存的错误 +Object 的 clone() 是 protected 方法,一个类不显式去重写 clone(),就不能直接去调用该类实例的clone()方法 +克隆就是制造一个对象的副本,根据所要克隆的对象的成员变量中是否含有引用类型,可以将克隆分为两种:浅克隆(Shallow Clone) 和 深克隆(Deep Clone),默认情况下使用Object中的clone方法进行克隆就是浅克隆,即完成对象域对域的拷贝 Cloneable 接口是一个标识性接口,即该接口不包含任何方法(包括clone()),但是如果一个类想合法的进行克隆,那么就必须实现这个接口,在使用clone()方法时,若该类未实现 Cloneable 接口,则抛出异常 @@ -2712,15 +2711,15 @@ Cloneable 接口是一个标识性接口,即该接口不包含任何方法( `Student s2 = s.clone()`:会生成一个新的Student对象,并且和s具有相同的属性值和方法 * Shallow Clone & Deep Clone: + 浅克隆:Object中的clone()方法在对某个对象克隆时对其仅仅是简单地执行域对域的copy + + * 对基本数据类型和包装类的克隆是没有问题的。String、Integer等包装类型在内存中是不可以被改变的对象,所以在使用克隆时可以视为基本类型,只需浅克隆引用即可 + * 如果对一个引用类型进行克隆时只是克隆了它的引用,和原始对象共享对象成员变量 ![](https://gitee.com/seazean/images/raw/master/Java/Object浅克隆.jpg) - 对八种基本类型的克隆是没有问题的,String 在克隆时只是克隆了它的引用,因为**String是在内存中不可以被改变的对象**,所以在使用克隆时,我们可以将 String类型视为与基本类型,只需浅克隆即可 - - 但当对一个引用类型进行克隆时只是克隆了它的引用,克隆对象和原始对象共享了同一个对象成员变量 - - 深克隆 : 在对整个对象浅克隆后,还需对其引用变量进行克隆,并将其更新到浅克隆对象中去 + 深克隆:在对整个对象浅克隆后,对其引用变量进行克隆,并将其更新到浅克隆对象中去 ```java public class Student implements Cloneable{ @@ -2792,7 +2791,7 @@ public class Student { ### String -#### 概述 +#### 基本介绍 **String 被声明为 final,因此不可被继承 (Integer 等包装类也不能被继承)** @@ -2831,7 +2830,7 @@ s = s + "cd"; //s = abccd 新对象 直接赋值:`String s = “abc”` 直接赋值的方式创建字符串对象,内容就是abc -- 通过构造方法创建:通过 new 创建的字符串对象,每一次 new 都会申请一个内存空间,虽然内容相同,但是地址值不同 +- 通过构造方法创建:通过 new 创建的字符串对象,每一次 new 都会申请一个内存空间,虽然内容相同,但是地址值不同,**返回堆内存中对象的引用** - 直接赋值方式创建:以“ ”方式给出的字符串,只要字符序列相同(顺序和大小写),无论在程序代码中出现几次,JVM 都只会**在 String Pool 中创建一个字符串对象**,并在字符串池中维护 `String str = new String("abc")`创建字符串对象: @@ -2911,10 +2910,9 @@ s.replace("-","");//12378 **intern()** : -* jdk1.8:当一个字符串调用 intern() 方法时,如果 String Pool 中 +* jdk1.8:当一个字符串调用 intern() 方法时,如果 String Pool 中: * 存在一个字符串和该字符串值相等(使用 equals() 方法进行确定),就会返回 String Pool 中字符串的引用(需要变量接收) - * 不存在会把对象的引用地址复制一份放入串池,并返回串池中的引用地址,前提是堆内存有该对象 - 因为Pool在堆中,为了节省内存不再创建新对象 + * 不存在,会把对象的引用地址复制一份放入串池,并返回串池中的引用地址,前提是堆内存有该对象。因为Pool在堆中,为了节省内存不再创建新对象 * jdk1.6:将这个字符串对象尝试放入串池,如果有就不放入,返回已有的串池中的对象的地址;如果没有会把此对象复制一份,放入串池,把串池中的对象返回 ```java @@ -2951,8 +2949,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"); ``` @@ -11701,13 +11699,13 @@ Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所 ##### 动态链接 -动态链接也就是指向运行时常量池的方法引用 +动态链接是指向运行时常量池的方法引用,涉及到栈操作已经是类加载完成,这个阶段的解析是动态绑定 * 为了支持当前方法的代码能够实现动态链接,每一个栈帧内部都包含一个指向运行时常量池或该栈帧所属方法的引用 ![](https://gitee.com/seazean/images/raw/master/Java/JVM-动态链接符号引用.png) -* 在Java源文件被编译成字节码文件中,所有的变量和方法引用都作为符号引用(symbolic Refenrence)保存在class文件的常量池里,在类加载解析阶段变成直接引用 +* 在Java源文件被编译成字节码文件中,所有的变量和方法引用都作为符号引用保存在class的常量池中 常量池的作用:提供一些符号和常量,便于指令的识别 ![](https://gitee.com/seazean/images/raw/master/Java/JVM-动态链接运行时常量池.png) @@ -11869,13 +11867,16 @@ public static void main(String[] args) { 类元信息:在类编译期间放入方法区,存放了类的基本信息,包括类的方法、参数、接口以及常量池表 -常量池表(Constant Pool Table)是Class文件的一部分,存储了类在编译期间生成的**字面量、符号引用**,这些信息在类加载后会被解析到运行时常量池中,JVM为每个已加载的类维护一个常量池 +常量池表(Constant Pool Table)是Class文件的一部分,存储了类在编译期间生成的**字面量、符号引用**,JVM为每个已加载的类维护一个常量池 **运行时常量池**是方法区的一部分 -* Class 文件中的常量池(编译器生成的字面量和符号引用)会在类加载后被放入这个区域 +* 常量池(编译器生成的字面量和符号引用)中的数据会在类加载的加载阶段放入运行时常量池 +* 类在解析阶段将符号引用替换成直接引用 * 除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern() +方法区的 GC:针对常量池的回收及对类型的卸载 + *** @@ -12549,6 +12550,10 @@ GC Roots说明: +*** + + + ##### 工作原理 可达性分析算法以**根对象集合 (GCRoots)**为起始点,从上至下的方式搜索被根对象集合所连接的目标对象 @@ -12567,6 +12572,10 @@ GC Roots说明: +*** + + + ##### 三色标记 ###### 标记算法 @@ -12620,7 +12629,7 @@ objE.fieldG = null; // 写 objD.fieldG = G; // 写 ``` -为了解决问题,可以在上面三步做一下操作,**将对象G记录起来,然后作为灰色对象再进行遍历**,比如放到一个特定的集合,等初始的GC Roots遍历完(并发标记),再遍历该集合(重新标记) +为了解决问题,可以操作上面三步,**将对象G记录起来,然后作为灰色对象再进行遍历**,比如放到一个特定的集合,等初始的GC Roots遍历完(并发标记),再遍历该集合(重新标记) > 所以重新标记需要STW,应用程序一直在运行,该集合可能会一直增加新的对象,导致永远都运行不完 @@ -13481,7 +13490,7 @@ Java对象创建时机: 加载过程完成以下三件事: - 通过类的完全限定名称获取定义该类的二进制字节流 -- 将该字节流表示的静态存储结构转换为方法区的运行时存储结构 +- 将该字节流表示的**静态存储结构转换为方法区的运行时存储结构**(运行时常量池) - 在内存中生成一个代表该类的 Class 对象,作为方法区中该类各种数据的访问入口 其中二进制字节流可以从以下方式中获取: @@ -13491,16 +13500,16 @@ Java对象创建时机: - 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass - 由其他文件生成,例如由 JSP 文件生成对应的 Class 类 -将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field: +将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,重要 field: -* _java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 class 暴露给 java 使用 -* _super 即父类、_fields 即成员变量、_methods 即方法、_constants 即常量池、_class_loader 即类加载器、_vtable 虚方法表、_itable 接口方法表 +* `_java_mirror` 即 java 的类镜像,例如对 String 来说就是 String.class,作用是把 class 暴露给 java 使用 +* `_super` 即父类、`_fields` 即成员变量、`_methods` 即方法、`_constants` 即常量池、`_class_loader` 即类加载器、`_vtable` **虚方法表**、`_itable` 接口方法表 -注意: +加载过程: * 如果这个类还有父类没有加载,先加载父类 * 加载和链接可能是交替运行的 -* instanceKlass和_java_mirror相互持有对方的地址,堆中对象通过instanceKlass和元空间进行交互 +* instanceKlass 和 _java_mirror 相互持有对方的地址,堆中对象通过 instanceKlass 和元空间进行交互 @@ -13520,6 +13529,10 @@ Java对象创建时机: +*** + + + ##### 准备 准备阶段为**静态变量分配内存并设置初始值**,使用的是方法区的内存: @@ -13552,14 +13565,22 @@ Java对象创建时机: +*** + + + ##### 解析 -将常量池中类、接口、字段、方法的**符号引用替换为直接引用**(内存地址)的过程 +将常量池中类、接口、字段、方法的**符号引用替换为直接引用**(内存地址)的过程: * 符号引用:一组符号来描述目标,可以是任何字面量,属于编译原理方面的概念如:包括类和接口的全限名、字段的名称和描述符、方法的名称和**方法描述符** * 直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄 -解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等,某些解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。 +解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等 + +* 在类加载解析的是非虚方法,静态绑定 + +* 也可以在初始化阶段之后再开始解析,这是为了支持 Java 的**动态绑定** ```java public class Load2 { @@ -14083,8 +14104,6 @@ C1编译器会对字节码进行简单可靠的优化,耗时短,以达到更 System.out.println(81); ``` -* 去虚拟化:对唯一的实现类进行内联 - * 冗余消除:在运行期间把一些不会执行的代码折叠掉 * 内联缓存:是一种加快动态绑定的优化技术 @@ -14118,7 +14137,7 @@ VM 参数设置: #### 其他编译 -Graal编译器:JDK10 HotSpot 加入的一个全新的即时编译器,编译效果短短几年时间就追评了C2编译器 +Graal编译器:JDK10 HotSpot 加入的一个全新的即时编译器,编译效果短短几年时间就追平了C2编译器 AOT编译器:JDK9引入,静态提前编译器 (Ahead Of Time Compiler),程序运行之前便将字节码转换为机器码的过程,是与即时编译相对立的一个概念,即时编译指的是在程序的运行过程中,将字节码转换为机器码 @@ -14195,12 +14214,14 @@ public static int invoke(Object... args) { #### 调用机制 +方法调用并不等于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本,不是方法的具体运行过程 + 在JVM中,将符号引用转换为直接引用有两种机制: -- 静态链接:当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变,将调用方法的符号引用转换为直接引用的过程称之为静态链接 -- 动态链接:如果被调用的方法在编译期无法被确定下来,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此被称为动态链接 +- 静态链接:当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变,将调用方法的符号引用转换为直接引用的过程称之为静态链接(类加载的解析阶段) +- 动态链接:如果被调用的方法在编译期无法被确定下来,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此被称为动态链接(初始化后的解析阶段) -对应的方法的绑定机制为:静态绑定和动态绑定。绑定是一个字段、方法或者类从符号引用被替换为直接引用的过程,仅发生一次 +对应的方法的绑定(分配)机制为:静态绑定和动态绑定。绑定是一个字段、方法或者类从符号引用被替换为直接引用的过程,仅发生一次 - 静态绑定:被调用的目标方法在编译期可知,且运行期保持不变,将这个方法与所属的类型进行绑定 - 动态绑定:被调用的目标方法在编译期无法确定,只能在程序运行期根据实际的类型绑定相关的方法 @@ -14297,6 +14318,14 @@ Constant pool: 2. 如果没有找到,在 Object 类中的公有实例方法中搜索 3. 如果没有找到,则在 I 的超接口中搜索,这一步的搜索结果的要求与非接口符号引用步骤 3 的要求一致 + + +*** + + + +##### 执行流程 + ```java public class Demo { public Demo() { } @@ -14371,6 +14400,18 @@ Java 虚拟机中关于方法重写的判定基于方法描述符,如果子类 4. 查表得到方法的具体地址 5. 执行方法的字节码 +方法重写的本质: + +1. 找到操作数栈的第一个元素所执行的对象的实际类型,记作 C + +2. 如果在类型 C 中找到与常量中的描述符和名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常 + + IllegalAccessError:表示程序试图访问或修改一个属性或调用一个方法,这个属性或方法没有权限访问,一般会引起编译器异常。如果这个错误发生在运行时,就说明一个类发生了不兼容的改变 + +3. 找不到,就会按照继承关系从下往上依次对 C 的各个父类进行第二步的搜索和验证过程 + +4. 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常 + *** @@ -14379,20 +14420,19 @@ Java 虚拟机中关于方法重写的判定基于方法描述符,如果子类 ##### 虚方法表 -在虚拟机工作过程中会频繁使用到动态链接,每次动态链接的过程中都要重新在类的元数据中搜索合适目标,影响到执行效率。为了提高性能,JVM 采取了一种用**空间换取时间**的策略来实现动态绑定,在类的方法区建立一个虚方法表(virtual method table),实现使用索引表来代替查找,可以快速定位目标方法 +在虚拟机工作过程中会频繁使用到动态分配,每次动态分配的过程中都要重新在类的元数据中搜索合适目标,影响到执行效率。为了提高性能,JVM 采取了一种用**空间换取时间**的策略来实现动态绑定,在类的方法区建立一个虚方法表(virtual method table),实现使用索引表来代替查找,可以快速定位目标方法 * invokevirtual 所使用的虚方法表(virtual method table,vtable) * invokeinterface 所使用的接口方法表(interface method table,itable) -虚方法表在类加载的**链接阶段**被创建并开始初始化,在类加载的准备阶段构造与该类关联的方法表,在解析阶段把表中的符号引用解析成内存地址 +虚方法表会在类加载的链接阶段被创建 并开始初始化,类的变量初始值准备完成之后,jvm会把该类的方发表也初始化完毕 虚方法表的执行过程: * 对于静态绑定的方法调用而言,实际引用将指向具体的目标方法 -* 对于动态绑定的方法调用而言,实际引用则是方法表的索引值,也就是方法的间接地址 -* Java 虚拟机获取调用者的实际类型,并在该实际类型的虚方法表中,根据索引值获得目标方法 +* 对于动态绑定的方法调用而言,实际引用则是方法表的索引值,也就是方法的间接地址。Java 虚拟机获取调用者的实际类型,并在该实际类型的虚方法表中,根据索引值获得目标方法内存偏移量(指针) -每个类中都有一个虚方法表,本质上是一个数组,每个数组元素指向一个当前类及其祖先类中非私有的实例方法 +为了优化对象调用方法的速度,方法区的类型信息会增加一个指针,该指针指向一个记录该类方法的方法表。每个类中都有一个虚方法表,本质上是一个数组,每个数组元素指向一个当前类及其祖先类中非私有的实例方法 方法表满足两个特质: @@ -14405,10 +14445,10 @@ Passenger 类的方法表包括两个方法,分别对应 0 号和 1 号。方 虚方法表对性能的影响: -* 使用了方法表的动态绑定与静态绑定相比,仅仅多出几个内存解引用操作:访问栈上的调用者,读取调用者的动态类型,读取该类型的方法表,读取方法表中某个索引值所对应的目标方法,相对于创建并初始化 Java 栈帧这操作的开销可以忽略不计 +* 使用了方法表的动态绑定与静态绑定相比,仅仅多出几个内存解引用操作:访问栈上的调用者、读取调用者的动态类型、读取该类型的方法表、读取方法表中某个索引值所对应的目标方法,但是相对于创建并初始化 Java 栈帧这操作的开销可以忽略不计 * 上述优化的效果看上去不错,但实际上仅存在于解释执行中,或者即时编译代码的最坏情况。因为即时编译还拥有另外两种性能更好的优化手段:内联缓存(inlining cache)和方法内联(method inlining) - +![](https://gitee.com/seazean/images/raw/master/Java/JVM-方法调用虚方法表图.png) @@ -15022,7 +15062,7 @@ JDK5以后编译阶段自动转换成上述片段 -#### 泛型集合 +#### 泛型擦除 泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行**泛型擦除**的动作,即泛型信息 在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理: diff --git a/SSM.md b/SSM.md index 0162d64..14b4605 100644 --- a/SSM.md +++ b/SSM.md @@ -1451,13 +1451,17 @@ PageInfo相关API: 注解可以简化开发操作,省略映射配置文件的编写。 - 常用注解: +常用注解: * @Select(“查询的SQL 语句”):执行查询操作注解 * @Insert(“插入的SQL 语句”):执行新增操作注解 * @Update(“修改的SQL 语句”):执行修改操作注解 * @Delete(“删除的SQL 语句”):执行删除操作注解 +参数注解: + +* @Param:当SQL语句需要多个(大于1)参数时,用来指定参数的对应规则 + 核心配置文件配置映射关系: ```xml @@ -1470,8 +1474,6 @@ PageInfo相关API: ``` - - 基本增删改查: * 创建Mapper接口 @@ -3061,7 +3063,7 @@ Mybatis核心配置文件消失 业务发起使用spring上下文对象获取对应的bean -**原理**:DAO接口不需要去创建实现类,因为MyBatis-Spring提供了一个动态代理的实现**MapperFactoryBean**,这个类可以让你直接注入数据映射器接口到service层 bean 中,底层将会为你创建JDK代理 +**原理**:DAO接口不需要去创建实现类,因为MyBatis-Spring提供了一个动态代理的实现**MapperFactoryBean**,这个类可以让你直接注入数据映射器接口到service层 bean 中,底层将会动态代理创建类 * pom.xml,导入坐标 @@ -5731,7 +5733,7 @@ Spirng可以通过配置的形式控制使用的代理形式,Spring会先判 * JDK动态代理只能对实现了接口的类生成代理,没有实现接口的类不能使用。 * Cglib动态代理即使被代理的类没有实现接口也可以使用,因为Cglib动态代理是使用继承被代理类的方式进行扩展 - * Cglib动态代理是通过继承的方式,覆盖被代理类的方法来进行代理,所以如果方法是被final修饰的话,就不能进行代理。 + * Cglib动态代理是通过继承的方式,覆盖被代理类的方法来进行代理,所以如果方法是被final修饰的话,就不能进行代理 @@ -6747,6 +6749,8 @@ public AnnotationConfigApplicationContext(Class... annotatedClasses) { ### Bean +#### 生命周期 + 单实例:在容器启动时创建对象 多实例:在每次获取的时候创建对象 @@ -6759,6 +6763,14 @@ Bean的生命周期:实例化instantiation,填充属性populate,初始化i ![](https://gitee.com/seazean/images/raw/master/Frame/Sprin-AOP+循环依赖.png) + + +*** + + + +#### 源码解析 + Java启动Spring代码: ```java @@ -6881,9 +6893,9 @@ ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext. ### 循环依赖 -* 解决循环依赖:提前引用,提前暴露创建中的Bean +解决循环依赖:提前引用,提前暴露创建中的Bean - 循环依赖的三级缓存: +* 循环依赖的三级缓存: ```java //一级缓存:存放所有初始化完成单实例bean,单例池 @@ -6895,12 +6907,12 @@ ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext. /** Cache of singleton factories: bean name to ObjectFactory. 3*/ private final Map> singletonFactories = new HashMap<>(16); ``` - + 为什么需要三级缓存? * 循环依赖解决需要提前引用动态代理对象,AOP动态代理是在Bean初始化后的后置处理中进行,这时的bean已经是成品对象,需要提前进行动态代理,三级缓存的ObjectFactory可以提前产生需要代理的对象 * 若存在循环依赖,**后置处理不创建代理对象,真正创建代理对象的过程是在getBean(B)的阶段中** - + 一定会提前引用吗? * 出现循环依赖才去使用,不出现就不使用 @@ -6909,7 +6921,7 @@ ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext. * 存在增强器会创建动态代理,不需要增强就不需要创建动态代理对象 * 不创建就会把最原始的实例化的Bean放到二级缓存,因为addSingletonFactory参数中传入了实例化的Bean,在singletonFactory.getObject()中返回给singletonObject,放入二级缓存 - + 什么时候将Bean的引用提前暴露给第三级缓存的ObjectFactory持有? * 实例化之后,依赖注入之前 @@ -6918,9 +6930,11 @@ ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext. createBeanInstance --> addSingletonFactory --> populateBean ``` -* 解决循环依赖,源码解析(A依赖B,B依赖A) +解决循环依赖,源码解析: - 第二阶段当A创建实例后填充属性前,执行`addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));`方法,注意lambda表达式,getObject()时调用 +* 假如A依赖B,B依赖A + + 当A创建实例后填充属性前,执行`addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));`方法,注意lambda表达式,getObject()时调用 ````java //添加给定的单例工厂以构建指定的单例 @@ -7314,6 +7328,8 @@ AnnotationAwareAspectJAutoProxyCreator是这种类型的后置处理器:Instan #### Transactional +(源码解析待更新) + 如果一个类或者一个类中的 public 方法上被标注@Transactional 注解的话,Spring 容器就会在启动的时候为其创建一个代理类,在调用被@Transactional注解的 public 方法的时候,实际调用的是TransactionInterceptor类中的 invoke()方法。这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务 `TransactionInterceptor` 类中的 `invoke()`方法内部实际调用的是 `TransactionAspectSupport` 类的 `invokeWithinTransaction()`方法 @@ -9984,11 +10000,9 @@ jsp: SSM(Spring+SpringMVC+MyBatis) -* Spring - * 框架基础 +* Spring:框架基础 -* MyBatis - * mysql+druid+pagehelper +* MyBatis:mysql+druid+pagehelper * Spring整合MyBatis @@ -10040,11 +10054,11 @@ SSM(Spring+SpringMVC+MyBatis) ```xml - + < + org.springframework + spring-context + 5.1.9.RELEASE + @@ -10102,16 +10116,16 @@ SSM(Spring+SpringMVC+MyBatis) jackson-databind 2.9.0 - + + com.fasterxml.jackson.core + jackson-core + 2.9.0 + + + com.fasterxml.jackson.core + jackson-annotations + 2.9.0 + @@ -10201,7 +10215,7 @@ SSM(Spring+SpringMVC+MyBatis) * @param password 密码信息 * @return */ - //注意:数据层操作不要和业务层操作的名称混淆,通常数据层仅反映与数据库间的信息交换,不体现业务逻辑 + //数据层操作不要和业务层操作的名称混淆,通常数据层仅反映与数据库间的信息交换,不体现业务逻辑 public User getByUserNameAndPassword(@Param("userName") String userName, @Param("password") String password); } From 430ba6d2092ab78530d1d320565d59e704224d27 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 23 May 2021 19:44:11 +0800 Subject: [PATCH 020/242] Update Java Notes --- Java.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Java.md b/Java.md index 738eb8f..bda983d 100644 --- a/Java.md +++ b/Java.md @@ -13578,7 +13578,7 @@ Java对象创建时机: 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等 -* 在类加载解析的是非虚方法,静态绑定 +* 在类加载阶段解析的是非虚方法,静态绑定 * 也可以在初始化阶段之后再开始解析,这是为了支持 Java 的**动态绑定** From 797a03dd51f7593fd0e48e49c290a113907ad8ac Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 24 May 2021 20:56:17 +0800 Subject: [PATCH 021/242] Update Java Notes --- DB.md | 1081 ++++++++++++++++++++++++++++++++++++++++++++++++------- Java.md | 6 +- Tool.md | 6 +- 3 files changed, 954 insertions(+), 139 deletions(-) diff --git a/DB.md b/DB.md index baa706a..8b5e9df 100644 --- a/DB.md +++ b/DB.md @@ -122,14 +122,125 @@ MySQL配置: +*** + + + +### 常用工具 + +#### mysql + +mysql 不是指 mysql 服务,而是指 mysql 的客户端工具 + +```sh +mysql [options] [database] +``` + +* -u --user=name:指定用户名 +* -p --password[=name]:指定密码 +* -h --host=name:指定服务器IP或域名 +* -P --port=#:指定连接端口 +* -e --execute=name:执行SQL语句并退出,在控制台执行SQL语句,而不用连接到数据库执行 + +示例: + +```sh +mysql -h 127.0.0.1 -P 3306 -u root -p +mysql -uroot -p2143 db01 -e "select * from tb_book"; +``` + + + +*** + + + +#### admin + +mysqladmin 是一个执行管理操作的客户端程序,用来检查服务器的配置和当前状态、创建并删除数据库等 + +通过 `mysqladmin --help` 指令查看帮助文档 + +```sh +mysqladmin -uroot -p2143 create 'test01'; +``` + + + +*** + + + +#### binlog + +服务器生成的日志文件以二进制格式保存,如果需要检查这些文本,就要使用 mysqlbinlog 日志管理工具 + +```sh +mysqlbinlog [options] log-files1 log-files2 ... +``` + +* -d --database=name:指定数据库名称,只列出指定的数据库相关操作 + +* -o --offset=#:忽略掉日志中的前n行命令。 + +* -r --result-file=name:将输出的文本格式日志输出到指定文件。 + +* -s --short-form:显示简单格式, 省略掉一些信息。 + +* --start-datatime=date1 --stop-datetime=date2:指定日期间隔内的所有日志。 + +* --start-position=pos1 --stop-position=pos2:指定位置间隔内的所有日志。 + + + +*** + + + +#### dump + +##### 命令介绍 + +mysqldump 客户端工具用来备份数据库或在不同数据库之间进行数据迁移,备份内容包含创建表,及插入表的SQL语句 + +```sh +mysqldump [options] db_name [tables] +mysqldump [options] --database/-B db1 [db2 db3...] +mysqldump [options] --all-databases/-A +``` + +连接选项: + +* -u --user=name:指定用户名 +* -p --password[=name]:指定密码 +* -h --host=name:指定服务器IP或域名 +* -P --port=#:指定连接端口 + +输出内容选项: + +* --add-drop-database:在每个数据库创建语句前加上 Drop database 语句 +* --add-drop-table:在每个表创建语句前加上 Drop table 语句 , 默认开启 ; 不开启 (--skip-add-drop-table) +* -n --no-create-db:不包含数据库的创建语句 +* -t --no-create-info:不包含数据表的创建语句 +* -d --no-data:不包含数据 +* -T, --tab=name:自动生成两个文件:一个.sql文件,创建表结构的语句;一个.txt文件,数据文件,相当于select into outfile + +示例: + +```sh +mysqldump -uroot -p2143 db01 tb_book --add-drop-database --add-drop-table > a +mysqldump -uroot -p2143 -T /tmp test city +``` + + *** -### 备份 +##### 数据备份 -#### 命令行 +命令行方式: * 备份命令:mysqldump -u root -p 数据库名称 > 文件保存路径 @@ -142,7 +253,7 @@ MySQL配置: -#### 图形化 +图形化界面: * 备份 @@ -156,6 +267,103 @@ MySQL配置: +*** + + + +#### import + +mysqlimport 是客户端数据导入工具,用来导入mysqldump 加 -T 参数后导出的文本文件 + +```sh +mysqlimport [options] db_name textfile1 [textfile2...] +``` + +示例: + +```sh +mysqlimport -uroot -p2143 test /tmp/city.txt +``` + +导入 sql 文件,可以使用 MySQL 中的 source 指令 : + +```mysql +source 文件全路径 +``` + + + +*** + + + +#### show + +mysqlshow 客户端对象查找工具,用来很快地查找存在哪些数据库、数据库中的表、表中的列或者索引 + +```sh +mysqlshow [options] [db_name [table_name [col_name]]] +``` + +* --count:显示数据库及表的统计信息(数据库,表 均可以不指定) + +* -i:显示指定数据库或者指定表的状态信息 + +示例: + +```sh +#查询每个数据库的表的数量及表中记录的数量 +mysqlshow -uroot -p1234 --count +#查询test库中每个表中的字段书,及行数 +mysqlshow -uroot -p1234 test --count +#查询test库中book表的详细情况 +mysqlshow -uroot -p1234 test book --count +``` + + + + + +*** + + + +## 体系结构 + +体系结构详解: + +* 第一层:网络连接层 + * 一些客户端和链接服务,包含本地socket 通信和大多数基于客户端/服务端工具实现的 TCP/IP 通信,主要完成一些类似于连接处理、授权认证、及相关的安全方案 + * 在该层上引入了线程池的概念,为通过认证安全接入的客户端提供线程 + * 在该层上实现基于SSL的安全链接,服务器也会为安全接入的每个客户端验证它所具有的操作权限 + +- 第二层:核心服务层 + * 完成大多数的核心服务功能,如SQL接口,并完成缓存的查询,SQL的分析和优化,部分内置函数的执行 + * 所有**跨存储引擎**的功能在这一层实现,如存储过程、触发器、视图等 + * 在该层服务器会解析查询并创建相应的内部解析树,并对其完成相应的优化如确定表的查询顺序,是否利用索引等, 最后生成相应的执行操作 + * 服务器还会查询内部的缓存,如果缓存空间足够大,可以在大量读操作的环境中提升系统的性能 +- 第三层:存储引擎层 + - 存储引擎真正的负责了MySQL中数据的存储和提取,服务器通过API和存储引擎进行通信 + - 不同的存储引擎具有不同的功能,这样我们可以根据自己的需要,来选取合适的存储引擎 +- 第四层:系统文件层 + - 数据存储层,主要是将数据存储在文件系统之上,并完成与存储引擎的交互 + - 文件系统:配置文件、数据文件、日志文件、错误文件、二进制文件等等的保存 + +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-体系结构.png) + +整个MySQL Server由以下组成 + +- Connection Pool:连接池组件 +- Management Services & Utilities:管理服务和工具组件 +- SQL Interface:SQL接口组件 +- Parse:查询分析器组件 +- Optimizer:优化器组件 +- Caches & Buffers:缓冲池组件 +- Pluggable Storage Engines:存储引擎 +- File System:文件系统 + + + *** @@ -696,6 +904,8 @@ LIMIT | AND 或 && | 并且 | | OR 或 \|\| | 或者 | | NOT 或 ! | 非,不是 | + | UNION | 对两个结果集进行并集操作,不包括重复行,同时进行默认规则的排序 | + | UNION ALL | 对两个结果集进行并集操作,包括重复行,不进行排序 | * 例如: @@ -1768,7 +1978,7 @@ CREATE TABLE us_pro( ## 事务机制 -### 事务概述 +### 事务介绍 事务:一条或多条 SQL 语句组成一个执行单元,其特点是这个单元要么同时成功要么同时失败 @@ -1945,7 +2155,7 @@ CREATE TABLE us_pro( ### 视图 -#### 视图概述 +#### 基本介绍 视图概念:视图是一种虚拟存在的数据表,这个虚拟的表并不在数据库中实际存在 @@ -2941,47 +3151,7 @@ LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定 ## 存储引擎 -### 体系结构 - -体系结构详解: - -* 第一层:网络连接层 - * 一些客户端和链接服务,包含本地socket 通信和大多数基于客户端/服务端工具实现的 TCP/IP 通信,主要完成一些类似于连接处理、授权认证、及相关的安全方案 - * 在该层上引入了线程池的概念,为通过认证安全接入的客户端提供线程 - * 在该层上实现基于SSL的安全链接,服务器也会为安全接入的每个客户端验证它所具有的操作权限 - -- 第二层:核心服务层 - * 完成大多数的核心服务功能,如SQL接口,并完成缓存的查询,SQL的分析和优化,部分内置函数的执行 - * 所有跨存储引擎的功能在这一层实现,如过程、函数等 - * 在该层服务器会解析查询并创建相应的内部解析树,并对其完成相应的优化如确定表的查询顺序,是否利用索引等, 最后生成相应的执行操作 - * 服务器还会查询内部的缓存,如果缓存空间足够大,可以在大量读操作的环境中提升系统的性能 -- 第三层:存储引擎层 - - 存储引擎真正的负责了MySQL中数据的存储和提取,服务器通过API和存储引擎进行通信 - - 不同的存储引擎具有不同的功能,这样我们可以根据自己的需要,来选取合适的存储引擎 -- 第四层:系统文件层 - - 数据存储层,主要是将数据存储在文件系统之上,并完成与存储引擎的交互 - - 文件系统:配置文件、数据文件、日志文件、错误文件、二进制文件等等的保存 - -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-体系结构.png) - -整个MySQL Server由以下组成 - -- Connection Pool:连接池组件 -- Management Services & Utilities:管理服务和工具组件 -- SQL Interface:SQL接口组件 -- Parse:查询分析器组件 -- Optimizer:优化器组件 -- Caches & Buffers:缓冲池组件 -- Pluggable Storage Engines:存储引擎 -- File System:文件系统 - - - -*** - - - -### 存储引擎 +### 基本介绍 对比其他数据库,MySQL的架构可以在不同场景应用并发挥良好作用,主要体现在存储引擎,插件式的存储引擎架构将查询处理和其他的系统任务以及数据的存储提取分离,可以针对不同的存储需求可以选择最优的存储引擎 @@ -3019,7 +3189,7 @@ InnoDB存储引擎:(MySQL5.5版本后默认的存储引擎) - 应用场景:对事务的完整性有比较高的要求,在并发条件下要求数据的一致性,读写频繁的操作 - 存储方式: - 使用共享表空间存储, 这种方式创建的表的表结构保存在.frm文件中, 数据和索引保存在 innodb_data_home_dir 和 innodb_data_file_path定义的表空间中,可以是多个文件 - - 使用多表空间存储, 这种方式创建的表的表结构存在 .frm 文件中,但每个表的数据和索引单独保存在 .ibd 中 + - 使用多表空间存储,创建的表的表结构存在 .frm 文件中,每个表的数据和索引单独保存在 .ibd 中 MEMORY存储引擎: @@ -3127,7 +3297,9 @@ MERGE存储引擎 ## 索引优化 -### 索引概述 +### 索引介绍 + +#### 基本介绍 MySQL官方对索引的定义为:索引(index)是帮助MySQL高效获取数据的一种数据结构。在表数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式指向数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。 @@ -3152,21 +3324,21 @@ MySQL官方对索引的定义为:索引(index)是帮助MySQL高效获取 -### 索引分类 +#### 索引分类 索引一般的分类如下: - 功能分类 - - 单列索引:一个索引只包含单个列,一个表可以有多个单列索引 - - 组合索引:顾名思义,就是将单列索引进行组合 - - 唯一索引:索引列的值必须唯一,允许有空值。如果是组合索引,则列值组合必须唯一 + - 单列索引:一个索引只包含单个列,一个表可以有多个单列索引(普通索引) + - 联合索引:顾名思义,就是将单列索引进行组合 + - 唯一索引:索引列的值必须唯一,允许有空值。如果是联合索引,则列值组合必须唯一 - 主键索引:一种特殊的唯一索引,不允许有空值,一般在建表时同时创建主键索引 - - 外键索引:只有InnoDB引擎支持外键索引,用来保证数据的一致性、完整性和实现级联操作 - + - 外键索引:只有 InnoDB 引擎支持外键索引,用来保证数据的一致性、完整性和实现级联操作 + - 结构分类 - BTree索引:MySQL使用最频繁的一个索引数据结构,是InnoDB和MyISAM存储引擎默认的索引类型,底层基于B+Tree 数据结构 - - Hash索引:MySQL中Memory存储引擎默认支持的索引类型 - - R-tree 索引(空间索引):空间索引是MyISAM引擎的一个特殊索引类型,主要用于地理空间数据类型 + - Hash索引:MySQL中 Memory 存储引擎默认支持的索引类型 + - R-tree 索引(空间索引):空间索引是 MyISAM 引擎的一个特殊索引类型,主要用于地理空间数据类型 - Full-text (全文索引) :快速匹配全部文档的方式。InnoDB引擎5.6版本后才支持全文索引,MEMORY引擎不支持 | 索引 | InnoDB引擎 | MyISAM引擎 | Memory引擎 | @@ -3188,20 +3360,17 @@ MySQL官方对索引的定义为:索引(index)是帮助MySQL高效获取 -### 索引结构 - -#### 原理 +### 聚簇索引 -索引是在MySQL的存储引擎中实现的,不同的存储引擎的所支持的索引不一定完全相同 +#### 索引对比 -BTree的索引类型是基于B+Tree树型数据结构的,B+Tree又是BTree数据结构的变种,用在数据库和操作系统中的文件系统,特点是能够保持数据稳定有序 +聚簇索引是一种数据存储方式,并不是一种单独的索引类型 -磁盘存储: +* 聚簇索引的叶子节点存放的是主键值和数据行,支持覆盖索引 -* 系统从磁盘读取数据到内存时是以磁盘块(block)为基本单位的,位于同一个磁盘块中的数据会被一次性读取出来,而不是需要什么取什么。 +* 非聚簇索引的叶子节点存放的是主键值或指向数据行的指针 -- InnoDB存储引擎中有页(Page)的概念,页是其磁盘管理的最小单位,InnoDB存储引擎中默认每个页的大小为16KB。 -- InnoDB引擎将若干个地址连接磁盘块,以此来达到页的大小16KB,在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘I/O次数,提高查询效率。 +在 Innodb 下主键索引是聚簇索引,在 Myisam 下主键索引是非聚簇索引 @@ -3209,117 +3378,243 @@ BTree的索引类型是基于B+Tree树型数据结构的,B+Tree又是BTree数 -#### BTree +#### Innodb -BTree又叫多路平衡搜索树,一颗m叉的BTree特性如下: +##### 聚簇索引 -- 树中每个节点最多包含m个孩子 -- 除根节点与叶子节点外,每个节点至少有[ceil(m/2)]个孩子 -- 若根节点不是叶子节点,则至少有两个孩子 -- 所有的叶子节点都在同一层 -- 每个非叶子节点由n个key与n+1个指针组成,其中 [ceil(m/2)-1] <= n <= m-1 +在 Innodb 存储引擎,B+树索引可以分为聚簇索引(也称聚集索引、clustered index)和辅助索引(也称非聚簇索引或二级索引、secondary index、non-clustered index) -5叉,key的数量 [ceil(m/2)-1] <= n <= m-1 为 2 <= n <=4 ,当n>4时中间节点分裂到父节点,两边节点分裂 +InnoDB中,聚簇索引是按照每张表的主键构造一颗B+树,同时叶子节点中存放的就是整张表的行记录数据,也将聚集索引的叶子节点称为数据页 -插入 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) +* 数据访问更快,聚簇索引将索引和数据保存在同一个B+树中,因此从聚簇索引中获取数据比非聚簇索引更快 +* 聚簇索引对于主键的排序查找和范围查找速度非常快 -* 插入H,n>4,中间元素G字母向上分裂到新的节点 +聚簇索引的缺点: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程2.png) +* 插入速度严重依赖于插入顺序,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能,所以对于 InnoDB 表,一般都会定义一个自增的ID列为主键 -* 插入E,K,Q不需要分裂 +* 更新主键的代价很高,将会导致被更新的行移动,所以对于InnoDB表,一般定义主键为不可更新 - ![](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) -* 插入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向上分裂 +检索过程:辅助索引找到主键值,再通过聚簇索引找到数据页,最后通过数据页中的 Page Directory 找到数据行 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程8.png) -BTREE树就已经构建完成了,BTREE树和二叉树相比, 查询数据的效率更高, 因为对于相同的数据量来说,**BTREE的层级结构比二叉树小**,所以搜索速度快 +*** -BTree结构的数据可以让系统高效的找到数据所在的磁盘块,定义一条记录为一个二元组[key, data] ,key为记录的键值,对应表中的主键值,data为一行记录中除主键外的数据。对于不同的记录,key值互不相同,BTree中的每个节点根据实际情况可以包含大量的关键字信息和分支 -![](https://gitee.com/seazean/images/raw/master/DB/索引的原理-BTree.png) +##### 索引实现 +InnoDB 使用B+Tree作为索引结构 -*** +**主键索引:** +* 在 InnoDB 中,表数据文件本身就是按 B+Tree 组织的一个索引结构,这个索引的 key 是数据表的主键,叶子节点 data 域保存了完整的数据记录 +* Innodb 的表数据文件**通过主键聚集数据**,如果没有定义主键,会选择非空唯一索引代替,如果也没有这样的列,MySQL 会自动为 InnoDB 表生成一个隐含字段作为主键,这个字段长度为 6 个字节,类型为长整形 -#### B+Tree +**辅助索引:** -##### 数据结构 +InnoDB 的所有辅助索引(二级索引)都引用主键作为 data 域 -B+Tree为BTree的变种,B+Tree与BTree的区别为: +InnoDB 表是基于聚簇索引建立的,因此 InnoDB 的索引能提供一种非常快速的主键查找性能。不过辅助索引也会包含主键列,所以不建议使用过长的字段作为主键,过长的主索引会令辅助索引变得过大 -* n叉B+Tree最多含有n个key,而BTree最多含有n-1个key。 +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB聚簇和辅助索引结构.png) -- 所有非叶子节点只存储键值key信息,可以看作key的索引部分 -- 所有数据都存储在叶子节点,按照key大小顺序排列 - +*** -##### 优化结构 -BTree数据结构中每个节点中不仅包含数据的key值,还有data值。每一页的存储空间是有限的,如果data数据较大时将会导致每个节点(即一个页)能存储的key的数量很小,当存储的数据量很大时同样会导致B-Tree的深度较大,增大查询时的磁盘I/O次数,进而影响查询效率 +#### MyISAM -MySql索引数据结构对经典的B+Tree进行了优化,在原B+Tree的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的B+Tree,提高**区间访问**的性能 +##### 非聚簇 -区间访问的意思是访问索引为 5 - 15 的数据,这样就可以直接根据相邻节点的指针遍历 +MyISAM 的主键索引使用的是非聚簇索引,索引文件和数据文件是分离的,索引文件仅保存数据记录的**地址** -![](https://gitee.com/seazean/images/raw/master/DB/索引的原理-B+Tree.png) +* 主键索引B+树的节点存储了主键,辅助键索引B+树存储了辅助键,表数据存储在独立的地方,这两颗B+树的叶子节点都使用一个地址指向真正的表数据,对于表数据来说,这两个键没有任何差别。 +* 由于索引树是独立的,通过辅助索引检索无需访问主键的索引树 -通常在B+Tree上有两个头指针,一个指向根节点,另一个指向关键字最小的叶子节点,而且所有叶子节点(即数据节点)之间是一种链式环结构。可以对B+Tree进行两种查找运算: +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-聚簇索引和辅助索引检锁数据图.jpg) -- 有范围:对于主键的范围查找和分页查找 -- 有顺序:从根节点开始,进行随机查找 -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优点:提高查询速度,减少磁盘的IO次数,树形结构较小 +##### 索引实现 -*** +MyISAM 的索引方式也叫做非聚集的,之所以这么称呼是为了与 InnoDB 的聚集索引区分 +**主键索引:**MyISAM 引擎使用 B+Tree 作为索引结构,叶节点的data域存放的是数据记录的地址 +**辅助索引:**MyISAM 中主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求key是唯一的,而辅助索引的key可以重复 -### 索引操作 +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM主键和辅助索引结构.png) -索引在创建表的时候可以同时创建, 也可以随时增加新的索引 -* 创建索引:如果一个表中有一列是主键,那么会**默认为其创建主键索引**(主键列不需要单独创建索引) - - ```mysql - CREATE [UNIQUE|FULLTEXT] INDEX 索引名称 - [USING 索引类型] -- 默认是B+TREE - ON 表名(列名...); + +参考文章:https://blog.csdn.net/lm1060891265/article/details/81482136 + + + + + +*** + + + +### 索引结构 + +#### 原理 + +索引是在MySQL的存储引擎中实现的,不同的存储引擎的所支持的索引不一定完全相同 + +BTree的索引类型是基于B+Tree树型数据结构的,B+Tree又是BTree数据结构的变种,用在数据库和操作系统中的文件系统,特点是能够保持数据稳定有序 + +磁盘存储: + +* 系统从磁盘读取数据到内存时是以磁盘块(block)为基本单位的,位于同一个磁盘块中的数据会被一次性读取出来,而不是需要什么取什么。 + +- InnoDB存储引擎中有页(Page)的概念,页是其磁盘管理的最小单位,InnoDB存储引擎中默认每个页的大小为16KB。 +- InnoDB引擎将若干个地址连接磁盘块,以此来达到页的大小16KB,在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘I/O次数,提高查询效率。 + + + +*** + + + +#### BTree + +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字母向上分裂到新的节点 + + ![](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) + +* 插入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向上分裂 + + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程8.png) + +BTREE树就已经构建完成了,BTREE树和二叉树相比, 查询数据的效率更高, 因为对于相同的数据量来说,**BTREE的层级结构比二叉树小**,所以搜索速度快 + +BTree结构的数据可以让系统高效的找到数据所在的磁盘块,定义一条记录为一个二元组[key, data] ,key为记录的键值,对应表中的主键值,data为一行记录中除主键外的数据。对于不同的记录,key值互不相同,BTree中的每个节点根据实际情况可以包含大量的关键字信息和分支 +![](https://gitee.com/seazean/images/raw/master/DB/索引的原理-BTree.png) + + + +*** + + + +#### B+Tree + +##### 数据结构 + +B+Tree为BTree的变种,B+Tree与BTree的区别为: + +* n叉B+Tree最多含有n个key,而BTree最多含有n-1个key。 + +- 所有非叶子节点只存储键值key信息,可以看作key的索引部分 +- 所有数据都存储在叶子节点,按照key大小顺序排列 + + + + + +##### 优化结构 + +BTree数据结构中每个节点中不仅包含数据的key值,还有data值。每一页的存储空间是有限的,如果data数据较大时将会导致每个节点(即一个页)能存储的key的数量很小,当存储的数据量很大时同样会导致B-Tree的深度较大,增大查询时的磁盘I/O次数,进而影响查询效率 + +MySQL 索引数据结构对经典的B+Tree进行了优化,在原B+Tree的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的B+Tree,提高**区间访问**的性能 + +区间访问的意思是访问索引为 5 - 15 的数据,这样就可以直接根据相邻节点的指针遍历 + +![](https://gitee.com/seazean/images/raw/master/DB/索引的原理-B+Tree.png) + +通常在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亿` 条记录 + +实际情况中每个节点可能不能填充满,因此在数据库中,B+Tree的高度一般都在2-4层。MySQL的InnoDB存储引擎在设计时是将根节点常驻内存的,也就是说查找某一键值的行记录时最多只需要1~3次磁盘 I/O 操作 + +B+Tree优点:提高查询速度,减少磁盘的IO次数,树形结构较小 + + + +*** + + + +### 索引操作 + +索引在创建表的时候可以同时创建, 也可以随时增加新的索引 + +* 创建索引:如果一个表中有一列是主键,那么会**默认为其创建主键索引**(主键列不需要单独创建索引) + + ```mysql + CREATE [UNIQUE|FULLTEXT] INDEX 索引名称 + [USING 索引类型] -- 默认是B+TREE + ON 表名(列名...); ``` * 查看索引 @@ -3416,6 +3711,115 @@ B+Tree优点:提高查询速度,减少磁盘的IO次数,树形结构较小 +*** + + + +### 优化方式 + +#### 覆盖索引 + +覆盖索引:包含所有满足查询需要的数据的索引(SELECT 后面的字段刚好是索引字段),可以利用该索引返回 SELECT 列表的字段,而不必根据索引再次读取数据文件 + +回表查询:要查找的字段不在非主键索引树上时,需要通过叶子节点的主键值去主键索引上获取对应的行数据 + +使用覆盖索引,要注意 SELECT 列表中只取出需要的列,不可用 SELECT *,因为如果将所有字段一起做索引会导致索引文件过大,查询性能下降 + +使用覆盖索引,防止回表查询: + +* 表 user 主键为 id,普通索引为 age,查询语句: + + ```mysql + SELECT * FROM user WHERE age = 30; + ``` + + 查询过程:先通过普通索引 age=30 定位到主键值 id=1,再通过聚集索引 id=1 定位到行记录数据,需要两次扫描 B+ 树,这就是回表查询 + +* 使用覆盖索引: + + ```mysql + DROP INDEX idx_age ON user; + CREATE INDEX idx_age_name ON user(age,name); + SELECT id,age FROM user WHERE age = 30; + ``` + + 在一棵索引树上就能获取查询所需的数据,无需回表速度更快 + + + +*** + + + +#### 索引下推 + +索引条件下推优化(Index Condition Pushdown)是 MySQL5.6 添加,用于优化数据查询,减少回表操作 + +索引下推充分利用了索引中的数据,在查询出整行数据之前过滤掉无效的数据,再去主键索引树上查找 + +* 不使用索引下推优化时存储引擎通过索引检索到数据返回给 MySQL 服务器,服务器判断数据是否符合条件 +* 使用索引下推优化时,如果存在某些被索引的列的判断条件时,MySQL 服务器将这一部分**判断条件传递给存储引擎**,然后由存储引擎通过判断索引是否符合MySQL服务器传递的条件,只有当索引符合条件时才会将数据检索出来返回给MySQL服务器,由此减少 IO次数 + +适用条件: + +* 需要存储引擎将索引中的数据与条件进行判断,所以优化是基于存储引擎的,只有特定引擎可以使用,适用于InnoDB 和 MyISAM引擎 +* 存储引擎没有调用存储过程的能力,跨存储引擎的功能有存储过程、触发器、视图,所以调用这些功能的不可以进行索引下推优化 +* 对于 InnoDB 引擎只适用于二级索引,InnoDB 的聚簇索引会将整行数据读到缓冲区,因为数据已经在内存中了,不再需要去读取了,索引下推的目的减少IO次数也就失去了意义 + +工作过程: + +用户表 user,(name,sex) 是联合索引 + +```mysql +SELECT * FROM user WHERE name LIKE '王%' AND sex=1; -- 头部模糊匹配会造成索引失效 +``` + +* 优化前:在非主键索引树上找到满足第一个条件的行,然后通过叶子节点记录的主键值再回到主键索引树上查找到对应的行数据,最后再对比 AND 后的条件是否符合,符合返回数据 + + + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-索引下推优化1.png) + +* 优化后:检查索引中存储的列信息是否符合索引条件,如果符合将整行数据读取出来,然后用剩余的判断条件判断此行数据是否符合要求,符合要求就根据主键值进行回表查询 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-索引下推优化2.png) + +当使用EXPLAIN进行分析时,如果使用了索引条件下推,Extra 会显示 Using index condition + + + +参考文章:https://blog.csdn.net/sinat_29774479/article/details/103470244 +参考文章:https://blog.csdn.net/linuxguitu/article/details/113649245 + + + +*** + + + +#### 前缀索引 + +当要索引的列字符很多时,索引会变大变慢,可以只索引列开始的部分字符串,节约索引空间,提高索引效率 + +优化原则:降低重复的索引值 + +比如地区表: + +```mysql +area gdp code +chinaShanghai 100 aaa +chinaDalian 200 bbb +usaNewYork 300 ccc +chinaFuxin 400 ddd +chinaBeijing 500 eee +``` + +发现 area 字段很多都是以 china 开头的,那么如果以前1-5位字符做前缀索引就会出现大量索引值重复的情况,索引值重复性越低,查询效率也就越高,所以需要建立前 6 位字符的索引: + +```mysql +CREATE INDEX idx_area ON table_name(area(7)); +``` + + + *** @@ -3448,7 +3852,7 @@ SHOW [SESSION|GLOBAL] STATUS LIKE ''; ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SQL语句执行频率.png) -* 查询SQL语句影响的行数: +* 查询 SQL 语句影响的行数: ```mysql SHOW STATUS LIKE 'Innodb_rows_%'; @@ -3696,10 +4100,8 @@ key_len: 其他的额外的执行计划信息,在该列展示: -* Using index:该值表示相应的 SELECT 操作中使用了覆盖索引(Covering Index) - * MySQL 可以利用索引返回 SELECT 列表中的字段,而不必根据索引再次读取数据文件,包含所有满足查询需要的数据的索引称为**覆盖索引** - * 使用覆盖索引,要注意 SELECT 列表中只取出需要的列,不可用 SELECT *,因为如果将所有字段一起做索引会导致索引文件过大,查询性能下降 -* Using index condition:搜索条件中虽然出现了索引列,但是有部分条件无法使用索引,会根据能用索引的条件先搜索一遍再匹配无法使用索引的条件,**回表查询**数据 +* Using index:该值表示相应的 SELECT 操作中使用了**覆盖索引**(Covering Index) +* Using index condition:第一种情况是搜索条件中虽然出现了索引列,但是有部分条件无法使用索引,会根据能用索引的条件先搜索一遍再匹配无法使用索引的条件,回表查询数据;第二种是使用了索引下推 * Using where:表示存储引擎收到记录后进行“后过滤”(Post-filter),如果查询未能使用索引,Using where的作用是提醒我们 MySQL 将用 WHERE 子句来过滤结果集,即需要回表查询 * Using temporary:表示 MySQL 需要使用临时表来存储结果集,常见于排序和分组查询 * Using filesort:当 Query 中包含 order by 操作,而且无法利用索引完成的排序操作称为文件排序 @@ -3895,11 +4297,9 @@ 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 前后两个列是同一个复合索引 + OR 前的条件中的列有索引而后面的列中没有索引或 OR 前后两个列是同一个复合索引,都造成索引失效 ```mysql EXPLAIN SELECT * FROM tb_seller WHERE name='阿里巴巴' OR createtime = '2088-01-01 12:00:00'; @@ -4019,7 +4419,7 @@ SHOW GLOBAL STATUS LIKE 'Handler_read%'; -### 优化功能 +### 优化SQL #### 批量插入 @@ -4284,7 +4684,7 @@ MySQL 4.1版本之后,开始支持SQL的子查询 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL分页查询1.png) -* 优化方式一:在索引上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容 +* 优化方式一:子查询,在索引上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容 ```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; @@ -4308,7 +4708,7 @@ MySQL 4.1版本之后,开始支持SQL的子查询 #### 使用提示 -SQL提示,是优化数据库的一个重要手段,就是在SQL语句中加入一些人为的提示来达到优化操作的目的 +SQL提示,是优化数据库的一个重要手段,就是在SQL语句中加入一些提示来达到优化操作的目的 * USE INDEX:在查询语句中表名的后面,添加 USE INDEX 来提供 MySQL 去参考的索引列表,可以让MySQL不再考虑其他可用的索引 @@ -4639,6 +5039,183 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 +*** + + + +### 主从复制 + +#### 基本介绍 + +复制是指将主数据库的 DDL 和 DML 操作通过二进制日志传到从库服务器中,然后在从库上对这些日志重新执行(也叫重做),从而使得从库和主库的数据保持同步。 + +MySQL支持一台主库同时向多台从库进行复制,从库同时也可以作为其他从服务器的主库,实现链状复制 + +MySQL 复制的优点主要包含以下三个方面: + +- 主库出现问题,可以快速切换到从库提供服务 + +- 可以在从库上执行查询操作,从主库中更新,实现**读写分离**,降低主库的访问压力 + +- 可以在从库中执行备份,以避免备份期间影响主库的服务 + + + +*** + + + +#### 复制原理 + +MySQL 的主从复制原理图: + +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-主从复制原理图.jpg) + +从上层来看,复制分成三步: + +- Master 主库在事务提交时,会把数据变更作为事件 Events 记录在二进制日志文件 Binlog 中 +- 主库推送二进制日志文件 Binlog 中的日志事件到从库的中继日志 Relay Log + +- Slave 重做中继日志中的事件 + + + +**** + + + +#### 搭建流程 + +##### master + +1. 在master 的配置文件(/etc/mysql/my.cnf)中,配置如下内容: + + ```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 + +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:指定不需要同步的数据库 + + + +*** + + + +##### 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; + ``` + +5. 停止同步操作: + + ```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'); + ``` + +2. 在从库中查询数据,进行验证: + + 在从库中,可以查看到刚才创建的数据库: + + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-主从复制验证1.jpg) + + 在该数据库中,查询表中的数据: + + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-主从复制验证2.jpg) + + + **** @@ -5141,6 +5718,244 @@ InnoDB 的行级锁,如果使用不当可能会让InnoDB 的整体性能表现 +*** + + + +## 日志 + +### 日志分类 + +在任何一种数据库中,都会有各种各样的日志,记录着数据库工作的过程,可以帮助数据库管理员追踪数据库曾经发生过的各种事件。在 MySQL 中有 4 种不同的日志,分别是:错误日志、二进制日志(BINLOG 日志)、查询日志和慢查询日志 + + + +*** + + + +### 错误日志 + +错误日志是 MySQL 中最重要的日志之一,它记录了当 mysqld 启动和停止时,以及服务器在运行过程中发生任何严重错误时的相关信息。当数据库出现任何故障导致无法正常使用时,可以首先查看此日志 + +该日志是默认开启的,默认位置是:`/var/log/mysql/error.log` + +查看指令: + +```mysql +SHOW VARIABLES LIKE 'log_error%'; +``` + +查看日志内容: + +```sh +tail -f /var/log/mysql/error.log +``` + + + +*** + + + +### 二进制日志 + +#### 基本介绍 + +二进制日志(BINLOG)记录了所有的 DDL(数据定义语言)语句和 DML(数据操作语言)语句,但不包括数据查询语句。此日志对灾难时的数据恢复有重要作用,MySQL的主从复制, 也是通过该binlog实现 + +二进制日志,默认情况下是没有开启的,需要在 MySQL 配置文件中开启,并配置MySQL日志的格式: + +```sh +cd /etc/mysql +vim my.cnf + +# 配置开启binlog日志, 日志的文件前缀为 mysqlbin -----> 生成的文件名如: mysqlbin.000001 +log_bin=mysqlbin +# 配置二进制日志的格式 +binlog_format=STATEMENT +``` + +日志存放位置:配置时给定了文件名但是没有指定路径,日志默认写入Mysql的数据目录 + +日志格式: + +* STATEMENT:该日志格式在日志文件中记录的都是 SQL 语句 (statement),每一条对数据进行修改的 SQL都会记录在日志文件中,通过 mysqlbinlog 工具,可以查看到每条语句的文本。主从复制时,从库 (slave) 会将日志解析为原语句,并在从库重新执行一 +* ROW:该日志格式在日志文件中记录的是每一行的数据变更,而不是记录SQL语句。比如执行SQL语句`update tb_book set status='1'`,如果是 STATEMENT,在日志中会记录一行 SQL 语句; 如果是ROW,由于是对全表进行更新,就是每一行记录都会发生变更,ROW 格式的日志中会记录每一行的数据变更 + +* MIXED:这是 MySQL 默认的日志格式,混合了STATEMENT 和 ROW两种格式。MIXED 格式能尽量利用两种模式的优点,而避开他们的缺点 + + + +*** + + + +#### 日志读取 + +日志文件存储位置:/var/lib/mysql + +由于日志以二进制方式存储,不能直接读取,需要用 mysqlbinlog 工具来查看,语法如下: + +```sh +mysqlbinlog log-file; +``` + +查看 STATEMENT 格式日志: + +* 执行插入语句: + + ```mysql + INSERT INTO tb_book VALUES(NULL,'Lucene','2088-05-01','0'); + ``` + +* `cd /var/lib/mysql`: + + ```sh + -rw-r----- 1 mysql mysql 177 5月 23 21:08 mysqlbin.000001 + -rw-r----- 1 mysql mysql 18 5月 23 21:04 mysqlbin.index + ``` + + mysqlbin.index:该文件是日志索引文件 , 记录日志的文件名; + + mysqlbing.000001:日志文件 + +* 查看日志内容: + + ```sh + mysqlbinlog mysqlbing.000001; + ``` + + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-日志读取1.png) + +查看 ROW 格式日志: + +* 修改配置: + + ```sh + # 配置二进制日志的格式 + binlog_format=ROW + ``` + +* 插入数据: + + ```mysql + INSERT INTO tb_book VALUES(NULL,'SpringCloud实战','2088-05-05','0'); + ``` + +* 查看日志内容:日志格式 ROW,直接查看数据是乱码,可以在 mysqlbinlog 后面加上参数 -vv + + ```mysql + mysqlbinlog -vv mysqlbin.000002 + ``` + + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-日志读取2.png) + + + +*** + + + +#### 日志删除 + +对于比较繁忙的系统,生成日志量大,这些日志如果长时间不清除,将会占用大量的磁盘空间,需要删除日志 + +* Reset Master 指令删除全部 binlog 日志,删除之后,日志编号将从 xxxx.000001重新开始 + + ```mysql + Reset Master -- MySQL指令 + ``` + +* 执行指令 `PURGE MASTER LOGS TO 'mysqlbin.***`,该命令将删除 ` ***` 编号之前的所有日志 + +* 执行指令 `PURGE MASTER LOGS BEFORE 'yyyy-mm-dd hh:mm:ss'` ,该命令将删除日志为 `yyyy-mm-dd hh:mm:ss` 之前产生的所有日志 + +* 设置参数 `--expire_logs_days=#`,此参数的含义是设置日志的过期天数,过了指定的天数后日志将会被自动删除,这样做有利于减少管理日志的工作量,配置 my.cnf 文件: + + ```sh + log_bin=mysqlbin + binlog_format=ROW + --expire_logs_days=3 + ``` + + + +*** + + + +### 查询日志 + +查询日志中记录了客户端的所有操作语句,而二进制日志不包含查询数据的 SQL 语句 + +默认情况下, 查询日志是未开启的。如果需要开启查询日志,配置 my.cnf: + +```sh +# 该选项用来开启查询日志,可选值0或者1,0代表关闭,1代表开启 +general_log=1 +# 设置日志的文件名,如果没有指定,默认的文件名为host_name.log,存放在/var/lib/mysql +general_log_file=file_name +``` + +配置完毕之后,在数据库执行以下操作: + +```mysql +SELECT * FROM tb_book; +SELECT * FROM tb_book WHERE id = 1; +UPDATE tb_book SET name = 'lucene入门指南' WHERE id = 5; +SELECT * FROM tb_book WHERE id < 8 +``` + +执行完毕之后, 再次来查询日志文件: + +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-查询日志.png) + + + +*** + + + +### 慢日志 + +慢查询日志记录所有执行时间超过 long_query_time 并且扫描记录数不小于 min_examined_row_limit 的所有的 SQL 语句的日志。long_query_time 默认为 10 秒,最小为 0, 精度到微秒 + +慢查询日志默认是关闭的,可以通过两个参数来控制慢查询日志,配置文件`/etc/mysql/my.cnf`: + +```sh +# 该参数用来控制慢查询日志是否开启,可选值0或者1,0代表关闭,1代表开启 +slow_query_log=1 + +# 该参数用来指定慢查询日志的文件名,存放在 /var/lib/mysql +slow_query_log_file=slow_query.log + +# 该选项用来配置查询的时间限制,超过这个时间将认为值慢查询,将需要进行日志记录,默认10s +long_query_time=10 +``` + +日志读取: + +* 直接通过 cat 指令查询该日志文件: + + ```sh + cat slow_query.log + ``` + + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-慢日志读取1.png) + +* 如果慢查询日志内容很多,直接查看文件比较繁琐, 可以借助于mysql 自带的 mysqldumpslow 工具, 来对慢查询日志进行分类汇总: + + ```sh + mysqldumpslow slow_query.log + ``` + + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-慢日志读取2.png) + + + + + *** diff --git a/Java.md b/Java.md index bda983d..01b641e 100644 --- a/Java.md +++ b/Java.md @@ -14924,9 +14924,9 @@ Exception table: 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 + 12 3 2 e Ljava/lang/Exception; + 0 28 0 args [Ljava/lang/String; + 2 26 1 i I ``` diff --git a/Tool.md b/Tool.md index 9aee30c..5ec014f 100644 --- a/Tool.md +++ b/Tool.md @@ -1531,9 +1531,9 @@ grep [-abcEFGhHilLnqrsvVwxy][-A<显示列数>][-B<显示列数>][-C<显示列数 * -v:显示不包含匹配文本的所有行。 * --color=auto :可以将找到的关键词部分加上颜色的显示。 -**管道符:|**:表示将前一个命令处理的结果传递给后面的命令处理。 +**管道符 |**:表示将前一个命令处理的结果传递给后面的命令处理。 -`grep aaaa Filename ` :显示存在关键字aaaa的行 +`grep aaaa Filename `:显示存在关键字aaaa的行 `grep -n aaaa Filename`:显示存在关键字aaaa的行,且显示行号 @@ -1679,7 +1679,7 @@ sort [-bcdfimMnr][文件] * -r 以相反的顺序来排序(sort默认的排序方式是**升序**,改成降序,加-r) * -u 去掉重复 -面试题:一列数字,输出最大的3个不重复的数 +面试题:一列数字,输出最大的4个不重复的数 ```sh sort -ur a.txt | head -n 4 From 988811bb99e5031796eaedebbbfab89522496d43 Mon Sep 17 00:00:00 2001 From: Seazean Date: Tue, 25 May 2021 14:05:52 +0800 Subject: [PATCH 022/242] Update Java Notes --- Java.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Java.md b/Java.md index 01b641e..d563541 100644 --- a/Java.md +++ b/Java.md @@ -23661,7 +23661,10 @@ public static void main(String[] args) throws InterruptedException { * 快速失败:在 A 线程使用迭代器对集合进行遍历的过程中,此时 B 线程对集合进行修改(增删改),或者 A 线程在遍历过程中对集合进行修改,都会导致 A 线程抛出 ConcurrentModificationException 异常 * AbstractList 类中的成员变量 modCount,用来记录 List 结构发生变化的次数,**结构发生变化**是指添加或者删除至少一个元素的操作,或者是调整内部数组的大小,仅仅设置元素的值不算结构发生变化 + * 在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了抛出 CME 异常 + + * 安全失败:采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,故不会抛 ConcurrentModificationException 异常 From 3221dcd32bc635f030724c7580e0a6dfe9ab1313 Mon Sep 17 00:00:00 2001 From: Seazean Date: Tue, 25 May 2021 14:06:20 +0800 Subject: [PATCH 023/242] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cb0c263..463396f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -**Java** 学习笔记,记录着作者从入门到深入学习的所有知识,每次学有所获就会更新笔记,所有的知识都有借鉴其他前辈的博客和文章,排版布局美观整洁,使用 Typora 阅读效果更佳,希望对各位朋友有所帮助。 +**Java** 学习笔记,记录着作者从编程入门到深入学习的所有知识,每次学有所获就会更新笔记,所有的知识都有借鉴其他前辈的博客和文章,排版布局美观整洁,使用 Typora 阅读效果更佳,希望对各位朋友有所帮助。 注:所有的知识不保证权威性,如果各位朋友发现错误,非常欢迎与我讨论。 From 8a67e55c7056885ac715153141175a2db4dc45df Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 26 May 2021 00:10:19 +0800 Subject: [PATCH 024/242] Update Java Notes --- DB.md | 556 ++++++++++++++++++++++++++++++++++++++++++++++++-------- Java.md | 13 +- SSM.md | 2 +- 3 files changed, 489 insertions(+), 82 deletions(-) diff --git a/DB.md b/DB.md index 8b5e9df..2878d79 100644 --- a/DB.md +++ b/DB.md @@ -341,10 +341,11 @@ mysqlshow -uroot -p1234 test book --count * 完成大多数的核心服务功能,如SQL接口,并完成缓存的查询,SQL的分析和优化,部分内置函数的执行 * 所有**跨存储引擎**的功能在这一层实现,如存储过程、触发器、视图等 * 在该层服务器会解析查询并创建相应的内部解析树,并对其完成相应的优化如确定表的查询顺序,是否利用索引等, 最后生成相应的执行操作 - * 服务器还会查询内部的缓存,如果缓存空间足够大,可以在大量读操作的环境中提升系统的性能 + * 服务器会查询内部的缓存,如果缓存空间足够大,可以在大量读操作的环境中提升系统的性能 - 第三层:存储引擎层 - 存储引擎真正的负责了MySQL中数据的存储和提取,服务器通过API和存储引擎进行通信 - 不同的存储引擎具有不同的功能,这样我们可以根据自己的需要,来选取合适的存储引擎 + - MySQL中服务器层不管理事务,**事务是由存储引擎实现的** - 第四层:系统文件层 - 数据存储层,主要是将数据存储在文件系统之上,并完成与存储引擎的交互 - 文件系统:配置文件、数据文件、日志文件、错误文件、二进制文件等等的保存 @@ -1980,7 +1981,7 @@ CREATE TABLE us_pro( ### 事务介绍 -事务:一条或多条 SQL 语句组成一个执行单元,其特点是这个单元要么同时成功要么同时失败 +事务(Transaction)是访问和更新数据库的程序执行单元;事务中可能包含一个或多个sql语句,这些语句要么都执行,要么都不执行。作为一个关系型数据库,MySQL支持事务。 单元中的每条 SQL 语句都相互依赖,形成一个整体 @@ -2060,7 +2061,14 @@ CREATE TABLE us_pro( 提交方式: - 自动提交(MySQL默认为自动提交) -- 手动提交 +- 手动提交: + +工作原理: + +* 在自动提交模式下,如果没有 start transaction 显式地开始一个事务,那么每个sql语句都会被当做一个事务执行提交操作 +* 在手动提交模式下,所有的 sql 语句都在一个事务中,直到执行了commit 或 rollback,该事务结束,同时开始了另外一个事务 + +* 存在一些特殊的命令,在事务中执行了这些命令会马上强制执行 COMMIT 提交事务,如DDL语句 (create table/drop table/alter/table)、lock tables 语句等 提交方式语法: @@ -2077,7 +2085,8 @@ CREATE TABLE us_pro( SET AUTOCOMMIT=数字; -- 会话 ``` - + + *** @@ -2085,21 +2094,144 @@ CREATE TABLE us_pro( ### 四大特征 +#### ACID + 事务的四大特征:ACID - 原子性(atomicity) - 原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响 - - 一致性(consistency) - 一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态 +- 隔离性(isolaction) +- 持久性(durability) - 举例:拿转账来说,假设张三和李四两者的钱加起来一共是2000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是2000,这就是事务的一致性 -- 隔离性(isolaction) - 隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离 -- 持久性(durability) - 持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作 +*** + + + +#### 原子性 + +原子性是指事务是一个不可分割的工作单位,事务的操作如果成功就必须要完全应用到数据库,失败则不能对数据库有任何影响。比如事务中一个 SQL 语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态 + +InnoDB 存储引擎提供了两种事务日志:redo log (重做日志) 和 undo log (回滚日志) + +* redo log 用于保证事务持久性 +* undo log 用于保证事务原子性和隔离性 + +InnoDB 实现回滚依靠 undo log,该日志属于逻辑日志,记录 SQL 执行相关的信息。当事务对数据库进行修改时,InnoDB 会生成对应的 undo log,如果事务执行失败或调用了 rollback 导致事务回滚,InnoDB 会根据 undo log 的内容做与之前相反的操作: + +* 对于每个 insert,回滚时会执行 delete(undo log 记录的是这条delete语句,而不是执行的 insert 语句) + +* 对于每个 delete,回滚时会执行 insert + +* 对于每个 update,回滚时会执行一个相反的 update,把数据修改回去 + +undo log 是采用段 (segment) 的方式来记录的,每个 undo 操作在记录的时候占用一个 undo log segment + +rollback segmen 称为回滚段,每个回滚段中有1024个 undo log segment + +* 在以前老版本,只支持1个rollback segment,只能记录1024个 undo log segment +* MySQL5.5 开始支持128个 rollback segment,支持128*1024个 undo 操作 + + + +*** + + + +#### 一致性 + +一致性是指事务执行前后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。 + +数据库的完整性约束包括但不限于:实体完整性(如行的主键存在且唯一)、列完整性(如字段的类型、大小、长度要符合要求)、外键约束、用户自定义完整性(如转账前后,两个账户余额的和应该不变) + +实现一致性的措施: + +- 保证原子性、持久性和隔离性,如果这些特性无法保证,事务的一致性也无法保证 +- 数据库本身提供保障,例如不允许向整形列插入字符串值、字符串长度不能超过列的限制等 +- 应用层面进行保障,例如如果转账操作只扣除转账者的余额,而没有增加接收者的余额,无论数据库实现的多么完美,也无法保证状态的一致 + + + +*** + + + +#### 隔离性 + +隔离性是指,事务内部的操作与其他事务是隔离的,多个并发事务之间要相互隔离,不能互相干扰 + +* 严格的隔离性,对应了事务隔离级别中的Serializable,实际应用中对性能考虑很少使用可串行化 + +* 与原子性、持久性侧重于研究事务本身不同,隔离性研究的是不同事务之间的相互影响 + +隔离性追求的是并发情形下事务之间互不干扰,考虑最简单的读操作和写操作: + +- 一个事务的写操作对另一个事务的写操作:锁机制保证隔离性 +- 一个事务的写操作对另一个事务的读操作:MVCC保证隔离性 + +锁机制:事务在修改数据之前,需要先获得相应的锁,获得锁之后,事务便可以修改数据;该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁(详解见锁机制) + + + +*** + + + +#### 持久性 + +##### 实现原理 + +持久性是指一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,接下来的其他操作或故障不应该对其有任何影响。 + +实现原理:redo log + +InnoDB 作为存储引擎,数据是存放在磁盘中,每次读写数据都需要磁盘 IO,效率会很低。InnoDB 提供了缓存 Buffer Pool,Buffer Pool 中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲: + +* 从数据库读取数据时,会首先从缓存中读取,如果缓存中没有,则从磁盘读取后放入Buffer Pool +* 向数据库写入数据时,会首先写入缓存,缓存中修改的数据会定期刷新到磁盘(这一过程称为刷脏) + +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 也需要在事务提交时将日志写入磁盘,但是比将 Buffer Pool 中修改的数据写入磁盘快: + +* 刷脏是随机 IO,因为每次修改的数据位置随机,但写 redo log 是尾部追加操作,属于顺序 IO +* 刷脏是以数据页 (Page) 为单位的,MySQL 默认页大小是 16KB,一个页上的一个小修改都要整页写入,而 redo log 中只包含真正需要写入的部分,无效IO大大减少 + +刷盘策略,通过修改参数 `innodb_flush_log_at_trx_commit` 设置: + +* 0:表示当提交事务时,并不将缓冲区的 redo 日志写入磁盘,而是等待主线程每秒刷新一次 +* 1:在事务提交时将缓冲区的 redo 日志同步写入到磁盘,保证一定会写入成功 +* 2:在事务提交时将缓冲区的 redo 日志异步写入到磁盘,不能保证提交时肯定会写入,只是有这个动作 + + + +*** + + + +##### 日志对比 + +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 的写入时机相对多元 + + + +参考文章:https://www.cnblogs.com/kismetv/p/10331633.html @@ -2113,20 +2245,24 @@ CREATE TABLE us_pro( 隔离级别分类: -| 隔离级别 | 名称 | 类型 | 会引发的问题 | 数据库默认隔离级别 | -| ---------------- | -------- | -------- | ---------------------- | ------------------- | -| 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 Reads):在一个事务处理过程中读取了另一个未提交的事务中的数据 , 导致两次查询结果不一致 +* 脏读 (Dirty Reads):在事务中先后两次读取同一个数据,两次读取的结果不一样,原因是在一个事务处理过程中读取了另一个**未提交**的事务中的数据 + +* 不可重复读 (Non-Repeatable Reads):在事务中先后两次读取同一个数据,两次读取的结果不一样,原因是在一个事务处理过程中读取了另一个事务中修改并**已提交**的数据 -* 不可重复读 (Non-Repeatable Reads):是指在一个事务处理过程中读取了另一个事务中修改并已提交的数据,导致两次查询结果不一致 + > 可重复读的意思是不管读几次,结果都一样,可以重复的读 -* 幻读 (Phantom Reads):读取过程中数据条目发生了变化,查询某数据不存在,准备插入此记录,但执行插入时发现此记录已存在,无法插入;或查询某数据不存在,执行delete删除,却发现删除成功 +* 幻读 (Phantom Reads):在事务中按某个条件先后两次查询数据库,两次查询结果的数量不同,**数据条目**发生了变化。比如查询某数据不存在,准备插入此记录,但执行插入时发现此记录已存在,无法插入;或查询某数据不存在,执行delete删除,却发现删除成功 **隔离级别操作语法:** @@ -2145,6 +2281,213 @@ CREATE TABLE us_pro( +*** + + + +### 并发控制 + +#### MVCC + +MVCC 全称 Multi-Version Concurrency Control,即多版本并发控制,是一种用来解决读写冲突的无锁并发控制 + +MVCC 处理读写请求,可以做到在发生读写请求冲突时不用加锁,这个读是指的快照读,而不是当前读 + +* 快照读:实现基于 MVCC,因为是多版本并发,所以快照读读到的数据不一定是当前最新的数据,有可能是历史版本的数据 +* 当前读:读取数据库记录是当前最新的版本,会对读取的数据进行加锁,防止其他事务修改数据,是悲观锁的一种操作。读写操作加共享锁或者排他锁、串行化事务的隔离级别都是当前读 + +数据库并发场景: + +* 读-读:不存在任何问题,也不需要并发控制 + +* 读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读 + +* 写-写:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失 + +MVCC 的优点: + +* 在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能 +* 可以解决脏读,幻读,不可重复读等事务隔离问题(加锁也能解决),但不能解决更新丢失问题 + +提高读写和写写的并发性能: + +* MVCC + 悲观锁:MVCC解决读写冲突,悲观锁解决写写冲突 +* MVCC + 乐观锁:MVCC解决读写冲突,乐观锁解决写写冲突 + + + +参考文章:https://www.jianshu.com/p/8845ddca3b23 + + + +*** + + + +#### 原理 + +##### 版本链 + +实现原理主要是版本链,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产生一个聚簇索引 + +* 补充:删除 flag 的隐藏字段,记录被更新或删除并不代表真的删除,而是删除flag变了 + +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MVCC版本链隐藏字段.png) + + + + + + + +*** + + + +##### undo + +undo log 是逻辑日志,保存修改行的数据的拷贝副本 + +undo log 的作用: + +* 保证事务进行 rollback 时的原子性和一致性,当事务进行回滚的时候可以用 undo log 的数据进行恢复。 +* 用于MVCC快照读的数据,在MVCC多版本控制中,通过读取undo log的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据版本。 + +undo log主要分为两种: + +* insert undo log:代表事务在 insert 新记录时产生的undo log,只在事务回滚时需要,并且在事务提交后可以被立即丢弃 + +* update undo log:事务在进行 update 或 delete 时产生的 undo log,在事务回滚时需要,在快照读时也需要。不能随意删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被 purge 线程统一清除 + +每次对数据库记录进行改动,都会将旧值放到一条undo日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,把这个链表称之为版本链,版本链的头节点就是当前记录最新的值,链尾就是最早的旧记录 + + + +* 有个事务插入 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 可见,那么这条记录一定是可以被安全清除的 + + + +*** + + + +##### 读视图 + +Read View 是事务进行快照读操作时产生的读视图,该事务执行快照读的那一刻会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID + +Read View 用来做可见性判断,当某个事务执行快照读的时候,对该记录创建一个Read View 读视图,根据视图判断当前事务能够看到哪个版本的数据 + +工作流程:将版本链的头节点的事务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 值 +- creator_trx_id:生成该 Read View 的事务的事务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 >= low_limit_id:该版本对应的事务 ID 大于 Read view 中当前系统的最大事务ID,则说明该数据是在当前 Read view 创建之后才产生的,对 creator 不可见 +* up_limit_id <= db_trx_id < low_limit_id:判断 db_trx_id 是否在活跃事务列表 m_ids 中 + * 在列表中,说明该版本对应的事务正在运行,数据不能显示 + * 不在列表中,说明该版本对应的事务已经被提交,数据可以显示 + + + +*** + + + +##### 工作流程 + +表 user 数据 + +```sh +id name age +1 张三 18 +``` + +Transaction 20: + +```mysql +START TRANSACTION; -- 开启事务 +UPDATE user SET name = '李四' WHERE id = 1; +UPDATE user SET name = '王五' WHERE id = 1; +``` + +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) + +只有红框部分才复合条件,所以只有张三对应的版本的数据可以被看到 + + + +参考视频:https://www.bilibili.com/video/BV1t5411u7Fg + + + +*** + + + +#### RC RR + +Read View 用于支持RC(Read Committed,读已提交)和RR(Repeatable Read,可重复读)隔离级别的实现 + +RR、RC生成时机: + +- RC 隔离级别下,每个快照读都会生成并获取最新的 Read View +- RR 隔离级别下,则是同一个事务中的第一个快照读才会创建 Read View,之后的快照读获取的都是同一个Read View,所以一个事务的查询结果每次都是相同的 + +解决幻读问题: + +- 快照读:通过MVCC来进行控制的,不用加锁,当前事务只生成一次 Read View,外部操作没有影响 +- 当前读:通过next-key锁(行锁+间隙锁)来解决问题 + +RC、RR级别下的InnoDB快照读区别 + +- 在RR级别下的某个事务的对某条记录的第一次快照读会创建一个 Read View, 将当前系统活跃的其他事务记录起来。此后在调用快照读的时候,使用的是同一个Read View。 + + 当前事务在其他事务提交之前使用过快照读,那么以后其他事务对数据的修改都是不可见的,就算以后其他事务提交了数据也不可见;早于 Read View 创建的事务所做的修改并提交的均是可见的 + +- RC级别下的,事务中每次快照读都会新生成一个 Read View,这就是在 RC 级别下的事务中可以看到别的事务提交的更新的原因 + + + *** @@ -3175,34 +3518,34 @@ MySQL支持的存储引擎: ### 引擎对比 -MyISAM存储引擎 +MyISAM存储引擎: * 特点:不支持事务和外键,读取速度快,节约资源 * 应用场景:查询和插入操作为主,只有很少更新和删除操作,并对事务的完整性、并发性要求不高 * 存储方式: * 每个MyISAM在磁盘上存储成3个文件,其文件名都和表名相同,拓展名不同 - * 表结构保存在.frm文件中,表数据保存在.MYD文件中,索引保存在.MYI文件中 + * 表的定义保存在.frm文件,表数据保存在.MYD (MYData) 文件中,索引保存在.MYI (MYIndex) 文件中 InnoDB存储引擎:(MySQL5.5版本后默认的存储引擎) - 特点:**支持事务**和外键操作,支持并发控制。对比MyISAM的存储引擎,InnoDB写的处理效率差一些,并且会占用更多的磁盘空间以保留数据和索引 - 应用场景:对事务的完整性有比较高的要求,在并发条件下要求数据的一致性,读写频繁的操作 - 存储方式: - - 使用共享表空间存储, 这种方式创建的表的表结构保存在.frm文件中, 数据和索引保存在 innodb_data_home_dir 和 innodb_data_file_path定义的表空间中,可以是多个文件 + - 使用共享表空间存储, 这种方式创建的表的表结构保存在.frm文件中, 数据和索引保存在 innodb_data_home_dir 和 innodb_data_file_path 定义的表空间中,可以是多个文件 - 使用多表空间存储,创建的表的表结构存在 .frm 文件中,每个表的数据和索引单独保存在 .ibd 中 MEMORY存储引擎: -- 特点:每个MEMORY表实际对应一个磁盘文件,格式是.frm ,该文件中只存储表的结构,表数据保存在内存中,且默认使用HASH索引,这样有利于数据的快速处理,在需要快速定位记录可以提供更快的访问,但是服务一旦关闭,表中的数据就会丢失,数据存储不安全 +- 特点:每个MEMORY表实际对应一个磁盘文件 ,该文件中只存储表的结构,表数据保存在内存中,且默认使用HASH索引,这样有利于数据的快速处理,在需要快速定位记录可以提供更快的访问,但是服务一旦关闭,表中的数据就会丢失,数据存储不安全 - 应用场景:通常用于更新不太频繁的小表,用以快速得到访问结果,类似缓存 - 存储方式:表结构保存在.frm中 -MERGE存储引擎 +MERGE存储引擎: * 特点: - * 是一组MyISAM表的组合,这些MyISAM表必须结构完全相同,通过将不同的表分布在多个磁盘上,有效的改善MERGE表的访问效率 - * MERGE表本身并没有存储数据,对MERGE类型的表可以进行查询、更新、删除操作,这些操作实际上是对内部的MyISAM表进行的。 + * 是一组MyISAM表的组合,这些MyISAM表必须结构完全相同,通过将不同的表分布在多个磁盘上 + * MERGE表本身并没有存储数据,对MERGE类型的表可以进行查询、更新、删除操作,这些操作实际上是对内部的MyISAM表进行的 * 应用场景:将一系列等同的MyISAM表以逻辑方式组合在一起,并作为一个对象引用他们,适合做数据仓库 @@ -3243,7 +3586,14 @@ MERGE存储引擎 | 批量插入速度 | 高 | 低 | 高 | | **外键** | **不支持** | **支持** | **不支持** | +面试问题:MyIsam 和 InnoDB 的区别? + +* 事务:InnoDB 支持事务,MyISAM 不支持事务 +* 外键:InnoDB 支持外键,MyISAM 不支持外键 +* 索引:InnoDB 是聚集(聚簇)索引,MyISAM 是非聚集(非聚簇)索引 +* 锁粒度:InnoDB 最小的锁粒度是行锁,MyISAM 最小的锁粒度是表锁 +* 存储结构:参考本节上半部分 @@ -3339,7 +3689,7 @@ MySQL官方对索引的定义为:索引(index)是帮助MySQL高效获取 - BTree索引:MySQL使用最频繁的一个索引数据结构,是InnoDB和MyISAM存储引擎默认的索引类型,底层基于B+Tree 数据结构 - Hash索引:MySQL中 Memory 存储引擎默认支持的索引类型 - R-tree 索引(空间索引):空间索引是 MyISAM 引擎的一个特殊索引类型,主要用于地理空间数据类型 - - Full-text (全文索引) :快速匹配全部文档的方式。InnoDB引擎5.6版本后才支持全文索引,MEMORY引擎不支持 + - Full-text (全文索引) :快速匹配全部文档的方式。MyISAM支持, InnoDB不支持FULLTEXT类型的索引,但是 InnoDB 可以使用 sphinx 插件支持全文索引,MEMORY引擎不支持 | 索引 | InnoDB引擎 | MyISAM引擎 | Memory引擎 | | ----------- | --------------- | ---------- | ---------- | @@ -3412,12 +3762,14 @@ InnoDB中,聚簇索引是按照每张表的主键构造一颗B+树,同时叶 在聚簇索引之上创建的索引称之为辅助索引,非聚簇索引都是辅助索引,像复合索引、前缀索引、唯一索引等 -辅助索引叶子节点存储的是主键值,而不是行的物理地址,所以访问数据需要二次查找 +辅助索引叶子节点存储的是主键值,而不是行的物理地址,所以访问数据需要二次查找,这也是推荐使用覆盖索引的原因,可以减少回表查询 检索过程:辅助索引找到主键值,再通过聚簇索引找到数据页,最后通过数据页中的 Page Directory 找到数据行 + + *** @@ -3475,9 +3827,9 @@ MyISAM 的索引方式也叫做非聚集的,之所以这么称呼是为了与 -参考文章:https://blog.csdn.net/lm1060891265/article/details/81482136 +参考文章:https://blog.csdn.net/lm1060891265/article/details/81482136 @@ -3557,6 +3909,8 @@ BTREE树就已经构建完成了,BTREE树和二叉树相比, 查询数据的 BTree结构的数据可以让系统高效的找到数据所在的磁盘块,定义一条记录为一个二元组[key, data] ,key为记录的键值,对应表中的主键值,data为一行记录中除主键外的数据。对于不同的记录,key值互不相同,BTree中的每个节点根据实际情况可以包含大量的关键字信息和分支 ![](https://gitee.com/seazean/images/raw/master/DB/索引的原理-BTree.png) +当进行范围查找时会出现回旋查找 + *** @@ -3569,10 +3923,10 @@ BTree结构的数据可以让系统高效的找到数据所在的磁盘块,定 B+Tree为BTree的变种,B+Tree与BTree的区别为: -* n叉B+Tree最多含有n个key,而BTree最多含有n-1个key。 +* n叉B+Tree最多含有n个key(哈希值),而BTree最多含有n-1个key。 -- 所有非叶子节点只存储键值key信息,可以看作key的索引部分 -- 所有数据都存储在叶子节点,按照key大小顺序排列 +- 所有**非叶子节点只存储键值key**信息,可以看作key的索引部分 +- 所有**数据都存储在叶子节点**,按照key大小顺序排列 @@ -3582,7 +3936,7 @@ B+Tree为BTree的变种,B+Tree与BTree的区别为: BTree数据结构中每个节点中不仅包含数据的key值,还有data值。每一页的存储空间是有限的,如果data数据较大时将会导致每个节点(即一个页)能存储的key的数量很小,当存储的数据量很大时同样会导致B-Tree的深度较大,增大查询时的磁盘I/O次数,进而影响查询效率 -MySQL 索引数据结构对经典的B+Tree进行了优化,在原B+Tree的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的B+Tree,提高**区间访问**的性能 +MySQL 索引数据结构对经典的B+Tree进行了优化,在原B+Tree的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的B+Tree,**提高区间访问的性能,防止回旋查找** 区间访问的意思是访问索引为 5 - 15 的数据,这样就可以直接根据相邻节点的指针遍历 @@ -3723,8 +4077,6 @@ B+Tree优点:提高查询速度,减少磁盘的IO次数,树形结构较小 回表查询:要查找的字段不在非主键索引树上时,需要通过叶子节点的主键值去主键索引上获取对应的行数据 -使用覆盖索引,要注意 SELECT 列表中只取出需要的列,不可用 SELECT *,因为如果将所有字段一起做索引会导致索引文件过大,查询性能下降 - 使用覆盖索引,防止回表查询: * 表 user 主键为 id,普通索引为 age,查询语句: @@ -3745,6 +4097,8 @@ B+Tree优点:提高查询速度,减少磁盘的IO次数,树形结构较小 在一棵索引树上就能获取查询所需的数据,无需回表速度更快 +使用覆盖索引,要注意 SELECT 列表中只取出需要的列,不可用 SELECT *,因为如果将所有字段一起做索引会导致索引文件过大,查询性能下降 + *** @@ -4316,7 +4670,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模糊查询,索引失效: 如果是尾部模糊匹配,索引不会失效;如果是头部模糊匹配,索引失效。 @@ -4334,6 +4688,8 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name,status,address); ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引13.png) + 原因:在覆盖索引的这棵 B+ 数上只需要进行 like 的匹配,或者是基于覆盖索引查询再进行 WHERE 的判断就可以获得结果 + * 如果 MySQL 评估使用索引比全表更慢,则不使用索引,索引失效: ```mysql @@ -4364,25 +4720,26 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name,status,address); EXPLAIN SELECT * FROM tb_seller WHERE sellerId NOT IN ('alibaba','huawei'); ``` -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='西安市'; - ``` +索引失效一般是针对联合索引,联合索引一般由几个字段组成,排序方式是先按照第一个字段进行排序,然后排序第二个,依此类推,图示(a, b)索引,a 相等的情况下 b 是有序的 + + + +* 最左前缀法则:当不匹配前面的字段的时候,后面的字段都是无序的。这种无序不仅体现在叶子节点,也会导致查询时的非叶子节点也是无序的,因为索引树相当于忽略的第一个字段,就无法使用二分查找 + +* 范围查询右边的列,不能使用索引,比如语句: `WHERE a > 1 AND b = 1 `,在 a 大于1的时候,b是无序的 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引9.png) + + +* 以%开头的Like模糊查询,索引失效,比如语句:`WHERE a LIKE '%d'`,前面的不确定,导致不符合最匹配,直接去索引中搜索以 d 结尾的节点,所以没有顺序 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-索引失效底层原理3.png) @@ -4401,7 +4758,7 @@ SHOW GLOBAL STATUS LIKE 'Handler_read%'; * Handler_read_first:索引中第一条被读的次数。如果较高,表示服务器正执行大量全索引扫描(这个值越低越好) -* Handler_read_key:如果索引正在工作,这个值代表一个行被索引值读的次数,如果值越低,表示索引不经常使用,性能改善不好(这个值越高越好)。 +* Handler_read_key:如果索引正在工作,这个值代表一个行被索引值读的次数,如果值越低,表示索引不经常使用,性能改善不好(这个值越高越好) * Handler_read_next:按照键顺序读下一行的请求数,如果范围约束或执行索引扫描来查询索引列,值增加 @@ -4421,6 +4778,34 @@ SHOW GLOBAL STATUS LIKE 'Handler_read%'; ### 优化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='西安市'; +``` + +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引9.png) + + + + + +*** + + + #### 批量插入 当使用load 命令导入数据的时候,适当的设置可以提高导入的效率: @@ -4641,7 +5026,7 @@ MySQL 4.1版本之后,开始支持SQL的子查询 * 执行查询语句: ```mysql - EXPLAIN SELECT * FROM emp WHERE id = 1 OR age = 30; + EXPLAIN SELECT * FROM emp WHERE id = 1 OR age = 30; -- 两个索引,并且不是复合索引 ``` ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL OR条件查询1.png) @@ -4650,8 +5035,8 @@ MySQL 4.1版本之后,开始支持SQL的子查询 Extra: Using sort_union(idx_emp_age_salary,PRIMARY); Using where ``` -* 建议使用 UNION 替换 OR: - 注意:该建议只针对多个索引列有效,如果有column没有被索引,查询效率可能会因为没有选择OR而降低 +* 使用 UNION 替换 OR,求并集: + 注意:该优化只针对多个索引列有效,如果有column没有被索引,查询效率可能会因为没有选择OR而降低 ```mysql EXPLAIN SELECT * FROM emp WHERE id = 1 UNION SELECT * FROM emp WHERE age = 30; @@ -5245,12 +5630,12 @@ MySQL 的主从复制原理图: * 不同存储引擎支持的锁 - | 存储引擎 | 表级锁 | 行级锁 | 页级锁 | - | -------- | ------ | ------ | ------ | - | MyISAM | 支持 | 不支持 | 不支持 | - | InnoDB | 支持 | 支持 | 不支持 | - | MEMORY | 支持 | 不支持 | 不支持 | - | BDB | 支持 | 不支持 | 支持 | + | 存储引擎 | 表级锁 | 行级锁 | 页级锁 | + | -------- | -------- | -------- | ------ | + | MyISAM | 支持 | 不支持 | 不支持 | + | InnoDB | **支持** | **支持** | 不支持 | + | MEMORY | 支持 | 不支持 | 不支持 | + | BDB | 支持 | 不支持 | 支持 | 从锁的角度来说:表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如 Web 应用;而行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并查询的应用,如一些在线事务处理系统 @@ -5264,7 +5649,7 @@ MySQL 的主从复制原理图: #### 表级锁 -MyISAM 存储引擎只支持表锁,这也是MySQL开始几个版本中唯一支持的锁类型 +MyISAM 存储引擎只支持表锁,这也是 MySQL 开始几个版本中唯一支持的锁类型 MyISAM 在执行查询语句前,会自动给涉及的所有表加读锁,在执行更新操作前,会自动给涉及的表加写锁,这个过程并不需要用户干预,所以用户一般不需要直接用 LOCK TABLE 命令给 MyISAM 表显式加锁 @@ -5388,9 +5773,9 @@ MyISAM 的读写锁调度是写优先,因为写锁后其他线程不能做任 -#### 锁争用 +#### 锁竞争 -* 查看锁争用: +* 查看锁竞争: ```mysql SHOW OPEN TABLES; @@ -5430,7 +5815,7 @@ MyISAM 的读写锁调度是写优先,因为写锁后其他线程不能做任 #### 行级锁 -InnoDB 与 MyISAM 的最大不同有两点:一是支持事务;二是 采用了行级锁 +InnoDB 与 MyISAM 的最大不同有两点:一是支持事务;二是采用了行级锁,InnoDB同时支持表锁和行锁 InnoDB 实现了以下两种类型的行锁: @@ -5591,6 +5976,8 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 当使用范围条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据进行加锁,对于键值在条件范围内但并不存在的记录,叫做间隙 (GAP) , InnoDB会对间隙进行加锁,这种锁机制就是间隙锁 (Next-Key锁) +间隙锁可以解决事务中的幻读问题,通过对间隙加锁,可以防止读取过程中数据条目发生变化 + * 关闭自动提交功能: ```mysql @@ -5622,10 +6009,10 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 -#### 锁争用 +#### 锁竞争 ```mysql -SHOW STATUS LIKE 'innodb_row_lock%'; +SHOW STATUS LIKE 'innodb_row_lock%'; ``` @@ -5642,7 +6029,18 @@ SHOW STATUS LIKE 'innodb_row_lock%'; * Innodb_row_lock_waits:系统启动后到现在总共等待的次数 -当等待的次数很高,而且每次等待的时长也不短的时候,我们就需要分析系统中为什么会有如此多的等待,然后根据分析结果制定优化计划 +当等待的次数很高,而且每次等待的时长也不短的时候,就需要分析系统中为什么会有如此多的等待,然后根据分析结果制定优化计划 + +查看锁状态: + +```mysql +SELECT * FROM information_schema.innodb_locks; #锁的概况 +SHOW ENGINE INNODB STATUS; #InnoDB整体状态,其中包括锁的情况 +``` + +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB查看锁状态.png) + +lock_id 是锁 id;lock_trx_id 为事务id;lock_mode 为 X 代表排它锁(写锁);lock_type 为 RECORD 代表锁为行锁(记录锁) @@ -5726,7 +6124,17 @@ InnoDB 的行级锁,如果使用不当可能会让InnoDB 的整体性能表现 ### 日志分类 -在任何一种数据库中,都会有各种各样的日志,记录着数据库工作的过程,可以帮助数据库管理员追踪数据库曾经发生过的各种事件。在 MySQL 中有 4 种不同的日志,分别是:错误日志、二进制日志(BINLOG 日志)、查询日志和慢查询日志 +在任何一种数据库中,都会有各种各样的日志,记录着数据库工作的过程,可以帮助数据库管理员追踪数据库曾经发生过的各种事件。 + +MySQL日志主要包括六种: + +1. 重做日志(redo log) +2. 回滚日志(undo log) +3. 归档日志(binlog)(二进制日志) +4. 错误日志(errorlog) +5. 慢查询日志(slow query log) +6. 一般查询日志(general log) +7. 中继日志(relay log) @@ -5736,7 +6144,7 @@ InnoDB 的行级锁,如果使用不当可能会让InnoDB 的整体性能表现 ### 错误日志 -错误日志是 MySQL 中最重要的日志之一,它记录了当 mysqld 启动和停止时,以及服务器在运行过程中发生任何严重错误时的相关信息。当数据库出现任何故障导致无法正常使用时,可以首先查看此日志 +错误日志是 MySQL 中最重要的日志之一,记录了当 mysqld 启动和停止时,以及服务器在运行过程中发生任何严重错误时的相关信息。当数据库出现任何故障导致无法正常使用时,可以首先查看此日志 该日志是默认开启的,默认位置是:`/var/log/mysql/error.log` @@ -5758,13 +6166,15 @@ tail -f /var/log/mysql/error.log -### 二进制日志 +### 归档日志 #### 基本介绍 -二进制日志(BINLOG)记录了所有的 DDL(数据定义语言)语句和 DML(数据操作语言)语句,但不包括数据查询语句。此日志对灾难时的数据恢复有重要作用,MySQL的主从复制, 也是通过该binlog实现 +归档日志(BINLOG)记录了所有的 DDL(数据定义语言)语句和 DML(数据操作语言)语句,但不包括数据查询语句。归档日志也叫二进制日志,是因为采用二进制进行存储,在事务提交时写入 + +作用:**灾难时的数据恢复和 MySQL 的主从复制** -二进制日志,默认情况下是没有开启的,需要在 MySQL 配置文件中开启,并配置MySQL日志的格式: +归档日志默认情况下是没有开启的,需要在 MySQL 配置文件中开启,并配置MySQL日志的格式: ```sh cd /etc/mysql @@ -5895,7 +6305,7 @@ mysqlbinlog log-file; # 该选项用来开启查询日志,可选值0或者1,0代表关闭,1代表开启 general_log=1 # 设置日志的文件名,如果没有指定,默认的文件名为host_name.log,存放在/var/lib/mysql -general_log_file=file_name +general_log_file=mysql_query.log ``` 配置完毕之后,在数据库执行以下操作: diff --git a/Java.md b/Java.md index d563541..123c053 100644 --- a/Java.md +++ b/Java.md @@ -25,13 +25,13 @@ #### 数据类型 -##### 基本数据类型 +##### 基本类型 Java语言提供了八种基本类型。六种数字类型(四个整数型,两个浮点型),一种字符类型,还有一种布尔型。 **byte:** -- byte 数据类型是8位、有符号的,以二进制补码表示的整数;**8位一个字节** +- byte 数据类型是8位、有符号的,以**二进制补码**表示的整数;**8位一个字节** - 最小值是 **-128(-2^7)** - 最大值是 **127(2^7-1)** - 默认值是 **`0`** @@ -160,7 +160,7 @@ G-->H[double] -##### 引用数据类型 +##### 引用类型 引用数据类型:类,接口,数组都是引用数据类型,又叫包装类 @@ -2876,7 +2876,7 @@ s = s + "cd"; //s = abccd 新对象 `public String[] split(String regex)` : 将字符串按给定的正则表达式分割成字符串数组 `public char charAt(int index)` : 取索引处的值 `public char[] toCharArray()` : 将字符串拆分为字符数组后返回 -`public boolean startsWith(String prefix)`测试此字符串是否以指定的前缀开头 +`public boolean startsWith(String prefix)` : 测试此字符串是否以指定的前缀开头 `public int lastIndexOf(String str)` : 返回字符串最后一次出现str的索引,没有返回-1 `public String substring(int beginIndex)` : 返回子字符串,以原字符串指定索引处到结尾 `public String substring(int beginIndex, int endIndex)` : 返回原字符串指定索引处的字符串 @@ -22926,7 +22926,7 @@ JDK 8 虽然将扩容算法做了调整,改用了尾插法,但仍不意味 private transient volatile Node[] nextTable; //扩容时的新 hash 表 ``` -4. 扩容时如果某个 bin 迁移完毕, 用 ForwardingNode 作为旧 table bin 的头结点 +4. 扩容时如果某个 bin 迁移完毕,用 ForwardingNode 作为旧 table bin 的头结点 ```java static final class ForwardingNode extends Node { @@ -23661,10 +23661,7 @@ public static void main(String[] args) throws InterruptedException { * 快速失败:在 A 线程使用迭代器对集合进行遍历的过程中,此时 B 线程对集合进行修改(增删改),或者 A 线程在遍历过程中对集合进行修改,都会导致 A 线程抛出 ConcurrentModificationException 异常 * AbstractList 类中的成员变量 modCount,用来记录 List 结构发生变化的次数,**结构发生变化**是指添加或者删除至少一个元素的操作,或者是调整内部数组的大小,仅仅设置元素的值不算结构发生变化 - * 在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了抛出 CME 异常 - - * 安全失败:采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,故不会抛 ConcurrentModificationException 异常 diff --git a/SSM.md b/SSM.md index 14b4605..1851dd9 100644 --- a/SSM.md +++ b/SSM.md @@ -5759,7 +5759,7 @@ Spirng可以通过配置的形式控制使用的代理形式,Spring会先判 ## 事务 -### 基本概念 +### 基本介绍 #### 事务介绍 From 4683dcb8a5d7d1fc04d1c3239e136d54d070cd75 Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 26 May 2021 09:47:56 +0800 Subject: [PATCH 025/242] Update README --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 463396f..00311d8 100644 --- a/README.md +++ b/README.md @@ -10,4 +10,3 @@ * SSM:MyBatis、Spring、SpringMVC、SpringBoot * Tool:Git、Linux、Docker * Web:HTML、CSS、Servlet、JavaScript - From e2d63531d09d2bbcfd31953c19d64c1d89e9a363 Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 26 May 2021 22:03:19 +0800 Subject: [PATCH 026/242] Update Java Notes --- DB.md | 805 ++++++++++++++++++++++++++------------------------------ Java.md | 144 ++++------ SSM.md | 2 +- 3 files changed, 434 insertions(+), 517 deletions(-) diff --git a/DB.md b/DB.md index 2878d79..e242c4d 100644 --- a/DB.md +++ b/DB.md @@ -40,9 +40,11 @@ ### MySQL -MySQL数据库是一个最流行的关系型数据库管理系统之一。 +MySQL数据库是一个最流行的关系型数据库管理系统之一 -关系型数据库是将数据保存在不同的数据表中,而且表与表之间还可以有关联关系,这样就提高了访问速度以及提高了灵活性。 +关系型数据库是将数据保存在不同的数据表中,而且表与表之间可以有关联关系,提高了灵活性。 + +缺点:数据存储在磁盘中,导致读写性能差,而且数据关系复杂,扩展性差 MySQL所使用的SQL语句是用于访问数据库最常用的标准化语言。 @@ -2084,7 +2086,6 @@ CREATE TABLE us_pro( SET @@AUTOCOMMIT=数字; -- 系统 SET AUTOCOMMIT=数字; -- 会话 ``` - @@ -2118,9 +2119,11 @@ InnoDB 存储引擎提供了两种事务日志:redo log (重做日志) 和 und * redo log 用于保证事务持久性 * undo log 用于保证事务原子性和隔离性 -InnoDB 实现回滚依靠 undo log,该日志属于逻辑日志,记录 SQL 执行相关的信息。当事务对数据库进行修改时,InnoDB 会生成对应的 undo log,如果事务执行失败或调用了 rollback 导致事务回滚,InnoDB 会根据 undo log 的内容做与之前相反的操作: +undo log 属于逻辑日志,根据每行操作进行记录,记录了 SQL 执行相关的信息,用来回滚行记录到某个版本 + +当事务对数据库进行修改时,InnoDB 会生成对应的 undo log,如果事务执行失败或调用了 rollback 导致事务回滚,InnoDB 会根据 undo log 的内容做与之前相反的操作: -* 对于每个 insert,回滚时会执行 delete(undo log 记录的是这条delete语句,而不是执行的 insert 语句) +* 对于每个 insert,回滚时会执行 delete * 对于每个 delete,回滚时会执行 insert @@ -2128,7 +2131,7 @@ InnoDB 实现回滚依靠 undo log,该日志属于逻辑日志,记录 SQL undo log 是采用段 (segment) 的方式来记录的,每个 undo 操作在记录的时候占用一个 undo log segment -rollback segmen 称为回滚段,每个回滚段中有1024个 undo log segment +rollback segment 称为回滚段,每个回滚段中有1024个 undo log segment * 在以前老版本,只支持1个rollback segment,只能记录1024个 undo log segment * MySQL5.5 开始支持128个 rollback segment,支持128*1024个 undo 操作 @@ -2161,14 +2164,14 @@ rollback segmen 称为回滚段,每个回滚段中有1024个 undo log segment 隔离性是指,事务内部的操作与其他事务是隔离的,多个并发事务之间要相互隔离,不能互相干扰 -* 严格的隔离性,对应了事务隔离级别中的Serializable,实际应用中对性能考虑很少使用可串行化 +* 严格的隔离性,对应了事务隔离级别中的 serializable,实际应用中对性能考虑很少使用可串行化 * 与原子性、持久性侧重于研究事务本身不同,隔离性研究的是不同事务之间的相互影响 -隔离性追求的是并发情形下事务之间互不干扰,考虑最简单的读操作和写操作: +隔离性让并发情形下的事务之间互不干扰: -- 一个事务的写操作对另一个事务的写操作:锁机制保证隔离性 -- 一个事务的写操作对另一个事务的读操作:MVCC保证隔离性 +- 一个事务的写操作对另一个事务的写操作(写写):锁机制保证隔离性 +- 一个事务的写操作对另一个事务的读操作(读写):MVCC保证隔离性 锁机制:事务在修改数据之前,需要先获得相应的锁,获得锁之后,事务便可以修改数据;该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁(详解见锁机制) @@ -2184,7 +2187,7 @@ rollback segmen 称为回滚段,每个回滚段中有1024个 undo log segment 持久性是指一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,接下来的其他操作或故障不应该对其有任何影响。 -实现原理:redo log +redo log,记录数据页的物理修改,而不是某一行或某几行的修改,用来恢复提交后的物理数据页,且只能恢复到最后一次提交的位置 InnoDB 作为存储引擎,数据是存放在磁盘中,每次读写数据都需要磁盘 IO,效率会很低。InnoDB 提供了缓存 Buffer Pool,Buffer Pool 中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲: @@ -2221,13 +2224,13 @@ redo log 也需要在事务提交时将日志写入磁盘,但是比将 Buffer 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 是 InnoDB 存储引擎实现的,而 binlog 是MySQL的服务器层实现的,同时支持InnoDB和其他存储引擎,并且二进制日志先于 redo log 被记录。 * 内容不同:redo log 是物理日志,内容基于磁盘的 Page;binlog的内容是二进制的,根据 binlog_format 参数的不同,可能基于SQL 语句、基于数据本身或者二者的混合(日志部分详解) -* 写入时机不同:binlog 在事务提交时写入;redo log 的写入时机相对多元 +* 写入时机不同:binlog 在事务提交时一次写入;redo log 的写入时机相对多元 @@ -2302,11 +2305,11 @@ MVCC 处理读写请求,可以做到在发生读写请求冲突时不用加锁 * 读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读 -* 写-写:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失 +* 写-写:有线程安全问题,可能会存在更新丢失问题 MVCC 的优点: -* 在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能 +* 在并发读写数据库时,做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了并发读写的性能 * 可以解决脏读,幻读,不可重复读等事务隔离问题(加锁也能解决),但不能解决更新丢失问题 提高读写和写写的并发性能: @@ -2326,9 +2329,9 @@ MVCC 的优点: #### 原理 -##### 版本链 +##### 隐藏字段 -实现原理主要是版本链,undo日志,Read View来实现的 +实现原理主要是隐藏字段,undo日志,Read View来实现的 数据库中的每行数据,除了自定义的字段,还有数据库隐式定义的字段: @@ -2359,11 +2362,11 @@ undo log 是逻辑日志,保存修改行的数据的拷贝副本 undo log 的作用: * 保证事务进行 rollback 时的原子性和一致性,当事务进行回滚的时候可以用 undo log 的数据进行恢复。 -* 用于MVCC快照读的数据,在MVCC多版本控制中,通过读取undo log的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据版本。 +* 用于MVCC快照读的数据,在MVCC多版本控制中,通过读取 undo log 的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据版本。 undo log主要分为两种: -* insert undo log:代表事务在 insert 新记录时产生的undo log,只在事务回滚时需要,并且在事务提交后可以被立即丢弃 +* insert undo log:事务在 insert 新记录时产生的 undo log,只在事务回滚时需要,并且在事务提交后可以被立即丢弃 * update undo log:事务在进行 update 或 delete 时产生的 undo log,在事务回滚时需要,在快照读时也需要。不能随意删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被 purge 线程统一清除 @@ -3966,9 +3969,8 @@ B+Tree优点:提高查询速度,减少磁盘的IO次数,树形结构较小 * 创建索引:如果一个表中有一列是主键,那么会**默认为其创建主键索引**(主键列不需要单独创建索引) ```mysql - CREATE [UNIQUE|FULLTEXT] INDEX 索引名称 - [USING 索引类型] -- 默认是B+TREE - ON 表名(列名...); + CREATE [UNIQUE|FULLTEXT] INDEX 索引名称 [USING 索引类型] ON 表名(列名...); + -- 索引类型默认是 B+TREE ``` * 查看索引 @@ -4112,7 +4114,7 @@ B+Tree优点:提高查询速度,减少磁盘的IO次数,树形结构较小 索引下推充分利用了索引中的数据,在查询出整行数据之前过滤掉无效的数据,再去主键索引树上查找 * 不使用索引下推优化时存储引擎通过索引检索到数据返回给 MySQL 服务器,服务器判断数据是否符合条件 -* 使用索引下推优化时,如果存在某些被索引的列的判断条件时,MySQL 服务器将这一部分**判断条件传递给存储引擎**,然后由存储引擎通过判断索引是否符合MySQL服务器传递的条件,只有当索引符合条件时才会将数据检索出来返回给MySQL服务器,由此减少 IO次数 +* 使用索引下推优化时,如果存在某些被索引的列的判断条件时,MySQL 服务器将这一部分**判断条件传递给存储引擎**,然后由存储引擎在索引内部判断索引是否符合传递的条件,只有当索引符合条件时才会将数据检索出来返回给MySQL服务器,由此减少 IO次数 适用条件: @@ -4120,20 +4122,17 @@ B+Tree优点:提高查询速度,减少磁盘的IO次数,树形结构较小 * 存储引擎没有调用存储过程的能力,跨存储引擎的功能有存储过程、触发器、视图,所以调用这些功能的不可以进行索引下推优化 * 对于 InnoDB 引擎只适用于二级索引,InnoDB 的聚簇索引会将整行数据读到缓冲区,因为数据已经在内存中了,不再需要去读取了,索引下推的目的减少IO次数也就失去了意义 -工作过程: - -用户表 user,(name,sex) 是联合索引 +工作过程:用户表 user,(name,sex) 是联合索引 ```mysql SELECT * FROM user WHERE name LIKE '王%' AND sex=1; -- 头部模糊匹配会造成索引失效 ``` -* 优化前:在非主键索引树上找到满足第一个条件的行,然后通过叶子节点记录的主键值再回到主键索引树上查找到对应的行数据,最后再对比 AND 后的条件是否符合,符合返回数据 - +* 优化前:在非主键索引树上找到满足第一个条件的行,然后通过叶子节点记录的主键值再回到主键索引树上查找到对应的行数据,再对比 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 @@ -6170,7 +6169,7 @@ tail -f /var/log/mysql/error.log #### 基本介绍 -归档日志(BINLOG)记录了所有的 DDL(数据定义语言)语句和 DML(数据操作语言)语句,但不包括数据查询语句。归档日志也叫二进制日志,是因为采用二进制进行存储,在事务提交时写入 +归档日志(BINLOG)记录了所有的 DDL(数据定义语言)语句和 DML(数据操作语言)语句,但不包括数据查询语句,在事务提交时写入。归档日志也叫二进制日志,是因为采用二进制进行存储 作用:**灾难时的数据恢复和 MySQL 的主从复制** @@ -6462,7 +6461,7 @@ long_query_time=10 ## 概述 -JDBC(Java DataBase Connectivity,java数据库连接)是一种用于执行SQL语句的Java API,可以为多种关系型数据库提供统一访问,它是由一组用Java语言编写的类和接口组成的。 +JDBC(Java DataBase Connectivity,java数据库连接)是一种用于执行SQL语句的Java API,可以为多种关系型数据库提供统一访问,是由一组用Java语言编写的类和接口组成的。 JDBC其实就是java官方提供的一套规范(接口),用于帮助开发人员快速实现不同关系型数据库的连接 @@ -6509,6 +6508,10 @@ DriverManager:驱动管理对象 +*** + + + ### Connection Connection:数据库连接对象 @@ -6525,6 +6528,10 @@ Connection:数据库连接对象 +*** + + + ### Statement Statement:执行sql语句的对象 @@ -6540,9 +6547,13 @@ Statement:执行sql语句的对象 +*** + + + ### ResultSet -ResultSet:结果集对象。ResultSet对象维护了一个游标,指向当前的数据行,初始在第一行 +ResultSet:结果集对象,ResultSet对象维护了一个游标,指向当前的数据行,初始在第一行 - 判断结果集中是否有数据:`boolean next()` - 有数据返回true,并将索引**向下移动一行** @@ -6555,6 +6566,10 @@ ResultSet:结果集对象。ResultSet对象维护了一个游标,指向当 +*** + + + ### 代码实现 数据准备 @@ -6718,8 +6733,6 @@ public class JDBCDemo01 { - - **** @@ -6861,7 +6874,12 @@ SQL注入攻击演示 SELECT * FROM user WHERE loginname='aaa' AND password='aaa' OR '1'='1'; ``` - + + + +*** + + ### 攻击解决 @@ -7479,218 +7497,6 @@ public class DataSourceUtils { -**** - - - -## JDBC框架 - -### 数据库源信息 - -DataBaseMetaData:数据库的源信息 - -- java.sql.DataBaseMetaData:封装了整个数据库的综合信息 -- 获取方式:Connection对象执行`DatabaseMetaData getMetaData() ` -- 常用方法 - - String getDatabaseProductName():获取数据库产品的名称 - - int getDatabaseProductVersion():获取数据库产品的版本号 - - - -ParameterMetaData:参数的源信息 - -* java.sql.ParameterMetaData:封装的是预编译执行者对象中每个参数的类型和属性 -* 获取方式:PreparedStatement对象执行`ParameterMetaData getParameterMetaData()` -* 常用方法: - * int getParameterCount():获取sql语句中参数的个数 - - - -ResultSetMetaData:结果集的源信息 - -- java.sql.ResultSetMetaData:封装的是结果集对象中列的类型和属性 -- 获取方式:ResultSet对象执行`ResultSetMetaData getMetaData() ` -- 核心功能: - - int getColumnCount():获取列的总数 - - String getColumnName(int i):获取列名 - - - - - -### 代码实现 - -* update - - ```java - public class JDBCTemplate { - private DataSource dataSource; - private Connection con; - private PreparedStatement pst; - private ResultSet rs; - - public JDBCTemplate(DataSource dataSource) { - this.dataSource = dataSource; - } - - //专用于执行增删改sql语句的方法 - public int update(String sql,Object...objs) { - int result = 0; - - try{ - con = dataSource.getConnection(); - pst = con.prepareStatement(sql); - - //获取sql语句中的参数源信息 - ParameterMetaData pData = pst.getParameterMetaData(); - //获取sql语句中参数的个数 - int parameterCount = pData.getParameterCount(); - - //判断参数个数是否一致 - if(parameterCount != objs.length) { - throw new RuntimeException("参数个数不匹配"); - } - - //为sql语句中的?占位符赋值 - for (int i = 0; i < objs.length; i++) { - pst.setObject(i+1,objs[i]); - } - - //执行sql语句 - result = pst.executeUpdate(); - - } catch(Exception e) { - e.printStackTrace(); - } finally { - //释放资源 - DataSourceUtils.close(con,pst); - } - //返回结果 - return result; - } - } - - ``` - -* query - - 用于处理结果集的接口 - - ```java - public interface ResultSetHandler { - //处理结果集的抽象方法。 - T handler(ResultSet rs); - } - - ``` - - BeanHandler实现类:用于完成将查询出来的一条记录,封装到Student对象中 - - ```java - public class BeanHandler implements ResultSetHandler { - //1.声明对象类型变量 - private Class beanClass; - - //2.有参构造对变量赋值 - public BeanHandler(Class beanClass) { - this.beanClass = beanClass; - } - - //将ResultSet结果集中的数据封装到beanClass类型对象 - @Override - public T handler(ResultSet rs) { - //3.声明对象 - T bean = null; - try{ - //4.创建传递参数的对象 - bean = beanClass.newInstance(); - - //5.判断是否有结果集 - if(rs.next()) { - //6.得到所有的列名 - //6.1先得到结果集的源信息 - ResultSetMetaData rsmd = rs.getMetaData(); - //6.2还要得到有多少列 - int columnCount = rsmd.getColumnCount(); - //6.3遍历列数 - for(int i = 1; i <= columnCount; i++) { - //6.4得到每列的列名 - String columnName = rsmd.getColumnName(i); - //6.5通过列名获取数据 - Object columnValue = rs.getObject(columnName); - - //6.6列名其实就是对象中成员变量的名称。于是就可以使用列名得到对象中属性的描述器(get和set方法) - PropertyDescriptor pd = new PropertyDescriptor(columnName.toLowerCase(),beanClass); - //6.7获取set方法 - Method writeMethod = pd.getWriteMethod(); - //6.8执行set方法,给成员变量赋值 - writeMethod.invoke(bean,columnValue); - } - } - } catch (Exception e) { - e.printStackTrace(); - } - - //7.将对象返回 - return bean; - } - } - ``` - -* 学生类 - - ```java - public class Student { - private Integer sid; - private String name; - private Integer age; - private Date birthday; - ...... - } - ``` - - - -* 测试类 - - ```java - public class JDBCTemplateTest { - private JDBCTemplate template = new JDBCTemplate(DataSourceUtils.getDataSource()); - - @Test - public void insert() { - //新增数据的测试 - String sql = "INSERT INTO student VALUES (?,?,?,?)"; - Object[] params = {5,"周七",27,"1997-07-07"}; - int result = template.update(sql, params); - if(result != 0) { - System.out.println("添加成功"); - }else { - System.out.println("添加失败"); - } - } - - @Test - public void delete() { - //删除数据的测试 - String sql = "DELETE FROM student WHERE name=?"; - int result = template.update(sql, "周七"); - System.out.println(result); - } - - @Test - public void queryForObject() { - //查询一条记录并封装自定义对象的测试 - String sql = "SELECT * FROM student WHERE sid=?"; - Student stu = template.queryForObject(sql,new BeanHandler<>(Student.class),1); - System.out.println(stu); - } - } - - ``` - - - @@ -7716,10 +7522,10 @@ MySQL支持ACID特性,保证可靠性和持久性,读取性能不高,因 特征: -* 可扩容,可伸缩。SQL数据关系过于复杂,Nosql不存关系,只存数据 -* 大数据量下高性能。数据不存取在磁盘IO,存取在内存 -* 灵活的数据模型。它设计了一些数据存储格式,能保证效率上的提高 -* 高可用。集群 +* 可扩容,可伸缩,SQL数据关系过于复杂,Nosql 不存关系,只存数据 +* 大数据量下高性能,数据不存取在磁盘IO,存取在内存 +* 灵活的数据模型,设计了一些数据存储格式,能保证效率上的提高 +* 高可用,集群 常见的Nosql:Redis、memcache、HBase、MongoDB @@ -7737,41 +7543,34 @@ Redis (REmote DIctionary Server) :用 C 语言开发的一个开源的高性 特征: -* 数据间没有必然的关联关系 - +* 数据间没有必然的关联关系,**不存关系,只存数据** +* 数据存储在内存,存取速度快,解决了磁盘 IO 速度慢的问题 * 内部采用**单线程**机制进行工作 - -* 高性能。官方测试数据,50个并发执行100000 个请求,读的速度是110000 次/s,写的速度是81000次/s - +* 高性能,官方测试数据,50个并发执行100000 个请求,读的速度是110000 次/s,写的速度是81000次/s * 多数据类型支持 - * 字符串类型:string - * 列表类型:list - * 散列类型:hash - * 集合类型:set - * 有序集合类型:zset/sorted_set - + * 字符串类型:string(String) + * 列表类型:list(LinkedList) + * 散列类型:hash(HashMap) + * 集合类型:set(HashSet) + * 有序集合类型:zset/sorted_set(TreeSet) * 支持持久化,可以进行数据灾难恢复 应用: -* 为热点数据加速查询(主要场景)。如热点商品、热点新闻、热点资讯、推广类等高访问量信息等 - -* 即时信息查询。如排行榜、网站访问统计、公交到站信息、在线人数(聊天室、网站)、设备信号等 +* 为热点数据加速查询(主要场景),如热点商品、热点新闻、热点资讯、推广类等高访问量信息等 -* 时效性信息控制。如验证码控制、投票控制等 +* 即时信息查询,如排行榜、网站访问统计、公交到站信息、在线人数(聊天室、网站)、设备信号等 -* 分布式数据共享。如分布式集群架构中的 session 分离 -* 消息队列. +* 时效性信息控制,如验证码控制、投票控制等 - - -**** +* 分布式数据共享,如分布式集群架构中的 session 分离 +* 消息队列 +*** -## 配置操作 ### 下载安装 @@ -7819,6 +7618,10 @@ Redis (REmote DIctionary Server) :用 C 语言开发的一个开源的高性 +*** + + + #### Ubuntu 安装: @@ -7979,64 +7782,116 @@ dbfilename "dump-6379.rdb" + + *** -### 基本操作 +## 基本指令 -#### 读写数据 +### 操作指令 -设置 key,value 数据: +读写数据: -```sh -set key value -#set name seazean -``` +* 设置 key,value 数据: -根据 key 查询对应的 value,如果**不存在,返回空(nil)**: + ```sh + set key value + #set name seazean + ``` -```sh -get key -#get name -``` +* 根据 key 查询对应的 value,如果**不存在,返回空(nil)**: + ```sh + get key + #get name + ``` +帮助信息: -#### 帮助信息 +* 获取命令帮助文档 -获取命令帮助文档 + ```sh + help [command] + #help set + ``` -```sh -help [command] -#help set -``` +* 获取组中所有命令信息名称 -获取组中所有命令信息名称 + ```sh + help [@group-name] + #help @string + ``` -```bash -help [@group-name] -#help @string -``` +退出服务 +* 退出客户端: + ```sh + quit + exit + ``` +* 退出客户端服务器快捷键: + ```sh + Ctrl+C + ``` -#### 退出服务 + -退出客户端: +*** -```sh -quit -exit -``` -退出客户端服务器快捷键: -```sh -Ctrl+C -``` +### key指令 + +key是一个字符串,通过key获取redis中保存的数据 + +* 基本操作 + + ```sh + del key #删除指定key + exists key #获取key是否存在 + type key #获取key的类型 + sort key [ASC/DESC] #对key中数据排序,默认对数字排序,并不更改集合中的数据位置,只是查询 + sort key alpha #对key中字母排序 + rename key newkey #改名 + renamenx key newkey #改名 + ``` + +* 时效性控制 + + ```sh + expire key seconds #为指定key设置有效期,单位为秒 + pexpire key milliseconds #为指定key设置有效期,单位为毫秒 + expireat key timestamp #为指定key设置有效期,单位为时间戳 + pexpireat key mil-timestamp #为指定key设置有效期,单位为毫秒时间戳 + ttl key #获取key的有效时间,每次获取会自动变化(减小),类似于倒计时, + #-1代表永久性,-2代表不存在/失效 + pttl key #获取key的有效时间,单位是毫秒,每次获取会自动变化(减小) + persist key #切换key从时效性转换为永久性 + ``` + +* 查询模式 + + ```sh + 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 + ``` + + @@ -8044,6 +7899,35 @@ Ctrl+C +### DB指令 + +Redis在使用过程中,伴随着操作数据量的增加,会出现大量的数据以及对应的key,数据不区分种类、类别混在一起,容易引起重复或者冲突 + +Redis为每个服务提供16个数据库,编码0-15,每个数据库之间的数据相互独立,**共用**Redis内存,不区分大小 + +* 基本操作 + + ```sh + select index #切换数据库,index从0-15取值 + ping #测试数据库是否连接正常,返回PONG + echo message #控制台输出信息 + ``` + +* 扩展操作 + + ```sh + move key db #数据移动到指定数据库,db是数据库编号 + dbsize #获取当前数据库的数据总量,即key的个数 + flushdb #清除当前数据库的所有数据 + flushall #清除所有数据 + ``` + + + + + +*** + ## 数据类型 @@ -8052,24 +7936,36 @@ Ctrl+C #### 简介 -**存储的数据**:单个数据,最简单的数据存储类型,也是最常用的数据存储类型。实质上是存一个字符串,注意是value是一个字符串,它是redis中最基本、最简单的存储数据的格式。 +redis 自身是一个 Map,其中所有的数据都是采用 key : value 的形式存储 + +数据类型指的是存储的数据的类型,也就是 value 部分的类型,**key 部分永远都是字符串** + +string类型的数据: + +存储的数据:单个数据,最简单的数据存储类型,也是最常用的数据存储类型,实质上是存一个字符串 + +存储数据的格式:一个存储空间保存一个数据,每一个空间中只能保存一个字符串信息 + +存储内容:通常使用字符串,如果字符串以整数的形式展示,可以作为数字操作使用 -**存储数据的格式**:一个存储空间保存一个数据。每一个空间中只能保存一个字符串信息,这个信息里边如果是存的纯数字,也能当数字使用 + -**存储内容**:通常使用字符串,如果字符串以整数的形式展示,可以作为数字操作使用. -![](https://gitee.com/seazean/images/raw/master/DB/Redis存储空间-string.png) + +*** #### 操作 +指令操作: + * 数据操作: ```sh set key value #添加/修改数据添加/修改数据 del key #删除数据 - setnx key value #判定性添加数据,键值为空则设置 + setnx key value #判定性添加数据,键值为空则设添加 mset k1 v1 k2 v2... #添加/修改多个数据,m:Multiple append key value #追加信息到原始信息后部(如果原始信息存在就追加,否则新建) ``` @@ -8082,8 +7978,6 @@ Ctrl+C strlen key #获取数据字符个数(字符串长度) ``` - ![string单数据与多数据操作](https://gitee.com/seazean/images/raw/master/DB/string单数据与多数据操作.png) - * 设置数值数据增加/减少指定范围的值 ```sh @@ -8101,32 +7995,51 @@ Ctrl+C psetex key milliseconds value #毫秒级 ``` -* 注意事项 +注意事项: + +1. 数据操作不成功的反馈与数据正常操作之间的差异 + + * 表示运行结果是否成功 + (integer) 0 → false 失败 + + (integer) 1 → true 成功 + + * 表示运行结果值 + (integer) 3 → 3 3个 + + (integer) 1 → 1 1个 + +2. 数据未获取到时,对应的数据为(nil),等同于null + +3. 数据最大存储量:512MB + +4. string在redis内部存储默认就是一个字符串,当遇到增减类操作incr,decr时**会转成数值型**进行计算 + +5. 按数值进行操作的数据,如果原始数据不能转成数值,或超越了redis 数值上限范围,将报错 + 9223372036854775807(java中Long型数据最大值,Long.MAX_VALUE) + +6. redis 可用于控制数据库表主键id,为数据库表主键提供生成策略,保障数据库表的主键唯一性 + +7. Redis 所有操作都是**原子性**的,采用**单线程**机制,命令是单个顺序执行,无需考虑并发带来影响 + +单数据和多数据的选择: + +* 单数据执行3条指令的过程:3 次发送 + 3 次处理 + 3次返回 +* 多数据执行1条指令的过程:1 次发送 + 3 次处理 + 1次返回(发送和返回的事件略高于单数据) - 1. 数据操作不成功的反馈与数据正常操作之间的差异 + - * 表示运行结果是否成功 - (integer) 0 → false 失败 - (integer) 1 → true 成功 - * 表示运行结果值 - (integer) 3 → 3 3个 - (integer) 1 → 1 1个 - 2. 数据未获取到时,对应的数据为(nil),等同于null - 3. 数据最大存储量:512MB - 4. string在redis内部存储默认就是一个字符串,当遇到增减类操作incr,decr时会转成数值型进行计算 - 5. 按数值进行操作的数据,如果原始数据不能转成数值,或超越了redis 数值上限范围,将报错 - 9223372036854775807(java中Long型数据最大值,Long.MAX_VALUE) - 6. Redis所有操作都是**原子性**的,采用**单线程**处理所有业务,命令是单个顺序执行,无需考虑并发带来影响 +*** #### 应用 -主页高频访问信息显示控制,例如新浪微博大V主页显示粉丝数与微博数量。 +主页高频访问信息显示控制,例如新浪微博大V主页显示粉丝数与微博数量 * 在Redis中为大V用户设定用户信息,以用户主键和属性值作为key,后台设定定时刷新策略 @@ -8164,19 +8077,27 @@ Ctrl+C 数据存储结构:一个存储空间保存多个键值对数据 -hash类型:底层使用哈希表结构实现数据存储 +hash类型:底层使用**哈希表**结构实现数据存储 + + + +类似Map结构,左边是key,右边是值,中间叫field字段,本质上**hash存了一个key-value的存储空间** - +hash是指的一个数据类型,并不是一个数据 -这种结构叫做hash,类似Map的结构,左边是key,右边是对应的值,中间叫field字段,本质上**hash存了一个key-value的存储空间**,hash是指的一个数据类型,并不是一个数据。底层使用哈希表结构实现 +* 如果field数量较少,存储结构优化为**类数组结构**(有序) +* 如果field数量较多,存储结构使用HashMap结构(无序) -* 如果field数量较少,存储结构优化为类数组结构 -* 如果field数量较多,存储结构使用HashMap结构 + + +*** #### 操作 +指令操作: + * 数据操作 ```sh @@ -8190,10 +8111,10 @@ hash类型:底层使用哈希表结构实现数据存储 ```sh hget key field #获取指定field对应数据 - hgetall key #获取指定key对应数据 + hgetall key #获取指定key所有数据 hmget key field1 field2... #获取多个数据 - hlen key #获取哈希表中字段的数量 hexists key field #获取哈希表中是否存在指定的字段 + hlen key #获取哈希表中字段的数量 ``` * 获取哈希表中所有的字段名或字段值 @@ -8210,10 +8131,17 @@ hash类型:底层使用哈希表结构实现数据存储 hincrbyfloat key field increment#操作小数 ``` -* 注意事项 - 1. hash类型中value只能存储字符串,不允许存储其他数据类型,不存在嵌套现象,如果数据未获取到,对应的值为(nil) - 2. 每个hash可以存储2^32 - 1个键值对。hash类型和对象的数据存储形式相似,并且可以灵活添加删除对象属性。但hash设计初衷不是为了存储大量对象而设计的,不可滥用,不可将hash作为对象列表使用 - 3. hgetall 操作可以获取全部属性,如果内部field过多,遍历整体数据效率就很会低,有可能成为数据访问瓶颈 + +注意事项 + +1. hash类型中value只能存储字符串,不允许存储其他数据类型,不存在嵌套现象,如果数据未获取到,对应的值为(nil) +2. 每个hash可以存储2^32 - 1个键值对 +3. hash类型和对象的数据存储形式相似,并且可以灵活添加删除对象属性。但hash设计初衷不是为了存储大量对象而设计的,不可滥用,不可将hash作为对象列表使用 +4. hgetall 操作可以获取全部属性,如果内部field过多,遍历整体数据效率就很会低,有可能成为数据访问瓶颈 + + + +*** @@ -8223,7 +8151,9 @@ hash类型:底层使用哈希表结构实现数据存储 user:id:3506728370 → {"name":"春晚","fans":12210862,"blogs":83} ``` -对于以上数据,使用单条去存的话,它存的条数会很多。但如果我们用json格式,它存一条数据就够了。问题是,如果说现在粉丝数量发生了变化,你要把整个值都改变。但是用单条存就不存在这个问题,只需要改其中一个就可以。有没有一种新的存储结构,能帮我们解决这个问题? +对于以上数据,使用单条去存的话,存的条数会很多。但如果用json格式,存一条数据就够了。 + +假如现在粉丝数量发生了变化,要把整个值都改变,但是用单条存就不存在这个问题,只需要改其中一个就可以 ![](https://gitee.com/seazean/images/raw/master/DB/hash应用场景结构图.png) @@ -8241,16 +8171,22 @@ user:id:3506728370 → {"name":"春晚","fans":12210862,"blogs":83} 数据存储结构:一个存储空间保存多个数据,且通过数据可以体现进入顺序,允许重复元素 -list类型:保存多个数据,底层使用**双向链表**存储结构实现 +list类型:保存多个数据,底层使用**双向链表**存储结构实现,类似于 LinkedList + + -![](https://gitee.com/seazean/images/raw/master/DB/list结构图.png) +如果两端都能存取数据的话,这就是双端队列,如果只能从一端进一端出,这个模型叫栈 -如果两端都能存取数据的话,这就是双端队列。如果只能从一端进一端出,这个模型叫栈 + + +*** #### 操作 +指令操作: + * 数据操作 ```sh @@ -8276,29 +8212,34 @@ list类型:保存多个数据,底层使用**双向链表**存储结构实现 blpop key1 [key2] timeout #在指定时间内获取指定key(可以多个)的数据,超时则为(nil) #可以从其他客户端写数据,当前客户端阻塞读取数据 brpop key1 [key2] timeout #从右边操作 - brpoplpush source destination timeout #从source获取数据放入destination - #假如在指定时间内没有任何元素被弹出,则返回一个nil和等待时长。反之,返回一个含有两个元素的列表,第一个元素是被弹出元素的值,第二个元素是等待时长 ``` + +* 复制操作 -* 注意事项 + ```sh + brpoplpush source destination timeout #从source获取数据放入destination,假如在指定时间内没有任何元素被弹出,则返回一个nil和等待时长。反之,返回一个含有两个元素的列表,第一个元素是被弹出元素的值,第二个元素是等待时长 + ``` + +注意事项 - 1. list中保存的数据都是string类型的,数据总容量是有限的,最多2^32 - 1 个元素(4294967295) - 2. list具有索引的概念,但操作数据时通常以队列的形式进行入队出队,或以栈的形式进行入栈出栈 - 3. 获取全部数据操作结束索引设置为-1 - 4. list可以对数据进行分页操作,通常第一页的信息来自于list,第2页及更多的信息通过数据库的形式加载 +1. list中保存的数据都是string类型的,数据总容量是有限的,最多2^32 - 1 个元素(4294967295) +2. list具有索引的概念,但操作数据时通常以队列的形式进行入队出队,或以栈的形式进行入栈出栈 +3. 获取全部数据操作结束索引设置为 -1 +4. list可以对数据进行分页操作,通常第一页的信息来自于list,第2页及更多的信息通过数据库的形式加载 +*** + #### 应用 企业运营过程中,系统将产生出大量的运营数据,如何保障多台服务器操作日志的统一顺序输出? -解决方案: - 依赖list的数据具有顺序的特征对信息进行管理,右进左查或者左近左查 - 使用队列模型解决多路信息汇总合并的问题 - 使用栈模型解决最新消息的问题 +* 依赖list的数据具有顺序的特征对信息进行管理,右进左查或者左近左查 +* 使用队列模型解决多路信息汇总合并的问题 +* 使用栈模型解决最新消息的问题 @@ -8314,37 +8255,48 @@ list类型:保存多个数据,底层使用**双向链表**存储结构实现 数据存储结构:能够保存大量的数据,高效的内部存储机制,便于查询 -set类型:与hash存储结构完全相同,仅存储键,不存储值(nil),并且值是不允许重复的,类似Java Set +set类型:与hash存储结构完全相同,仅存储键不存储值(nil),并且值是不允许重复且无序的,类似于HashSet - + + + + +*** #### 操作 +指令操作: + * 数据操作 ```sh sadd key member1 [member2] #添加数据 srem key member1 [member2] #删除数据 - spop key [count] #随机获取集中的某个数据并将该数据移除集合 ``` - + * 查询操作 ```sh smembers key #获取全部数据 scard key #获取集合数据总量 sismember key member #判断集合中是否包含指定数据 - srandmember key [count] #随机获取集合中指定(数量)的数据 ``` +* 随机操作 + + ```sh + spop key [count] #随机获取集中的某个数据并将该数据移除集合 + srandmember key [count] #随机获取集合中指定(数量)的数据 + * 集合的交、并、差 ```sh sinter key1 [key2...] #两个集合的交集,不存在为(empty list or set) sunion key1 [key2...] #两个集合的并集 sdiff key1 [key2...] #两个集合的差集 + sinterstore destination key1 [key2...] #两个集合的交集并存储到指定集合中 sunionstore destination key1 [key2...] #两个集合的并集并存储到指定集合中 sdiffstore destination key1 [key2...] #两个集合的差集并存储到指定集合中 @@ -8356,10 +8308,15 @@ set类型:与hash存储结构完全相同,仅存储键,不存储值(nil smove source destination member #将指定数据从原始集合中移动到目标集合中 ``` -* 注意事项 - 1. set 类型不允许数据重复,如果添加的数据在 set 中已经存在,将只保留一份 - 2. set 虽然与hash的存储结构相同,但是无法启用hash中存储值的空间 +注意事项 + +1. set 类型不允许数据重复,如果添加的数据在 set 中已经存在,将只保留一份 +2. set 虽然与hash的存储结构相同,但是无法启用hash中存储值的空间 + + + +*** @@ -8367,110 +8324,100 @@ set类型:与hash存储结构完全相同,仅存储键,不存储值(nil 应用场景: -1. 黑名单 - - 资讯类信息类网站追求高访问量,但是由于其信息的价值,往往容易被不法分子利用,通过爬虫技术,快速获取信息,个别特种行业网站信息通过爬虫获取分析后,可以转换成商业机密进行出售。例如第三方火车票、酒店刷票代购软件,电商评论。同时爬虫带来的伪流量也会给经营者带来错觉,产生错误的决策,有效避免网站被爬虫反复爬取成为每个网站都要考虑的基本问题。在基于技术层面区分出爬虫用户后,需要将此类用户进行有效的屏蔽,这就是黑名单的典型应用。 +1. 黑名单:资讯类信息类网站追求高访问量,但是由于其信息的价值,往往容易被不法分子利用,通过爬虫技术,快速获取信息,个别特种行业网站信息通过爬虫获取分析后,可以转换成商业机密。 注意:爬虫不一定做摧毁性的工作,有些小型网站需要爬虫为其带来一些流量。 -2. 白名单 +2. 白名单:对于安全性更高的应用访问,仅仅靠黑名单是不能解决安全问题的,此时需要设定可访问的用户群体, 依赖白名单做更为苛刻的访问验证 - 对于安全性更高的应用访问,仅仅靠黑名单是不能解决安全问题的,此时需要设定可访问的用户群体, 依赖白名单做更为苛刻的访问验证 解决方案: -设定用户鉴别规则,周期性更新满足规则的用户黑名单,加入set集合,用户行为信息达到后与黑名单进行对比。 -黑名单过滤IP地址:应用于开放游客访问权限的信息源 -黑名单过滤设备信息:应用于限定访问设备的信息源 -黑名单过滤用户:应用于基于访问权限的信息源 +* 设定用户鉴别规则,周期性更新满足规则的黑名单加入set集合,用户行为信息达到后与黑名单进行对比 +* 黑名单过滤IP地址:应用于开放游客访问权限的信息源 +* 黑名单过滤设备信息:应用于限定访问设备的信息源 +* 黑名单过滤用户:应用于基于访问权限的信息源 -**** +*** +### sorted +#### 简介 -## 常用指令 +数据存储需求:数据排序有利于数据的有效展示,需要提供一种可以根据自身特征进行排序的方式 -### key指令 +数据存储结构:新的存储模型,可以保存可排序的数据 -key是一个字符串,通过key获取redis中保存的数据 +sorted_set类型:在set的存储结构基础上添加可排序字段,类似于 TreeSet -* 基本操作 + - ```sh - del key #删除指定key - exists key #获取key是否存在 - type key #获取key的类型 - sort key [ASC/DESC] #对key中数据排序,默认对数字排序,并不更改集合中的数据,只是查询操作 - sort key alpha #对key中字母排序 - rename key newkey #改名 - renamenx key newkey #改名 - ``` -* 时效性控制 - ```sh - expire key seconds #为指定key设置有效期,单位为秒 - pexpire key milliseconds #为指定key设置有效期,单位为毫秒 - expireat key timestamp #为指定key设置有效期,单位为时间戳 - pexpireat key mil-timestamp #为指定key设置有效期,单位为毫秒时间戳 - ttl key #获取key的有效时间,每次获取会自动变化(减小),类似于倒计时, - #-1代表永久性,-2代表不存在/失效 - pttl key #获取key的有效时间,单位是毫秒,每次获取会自动变化(减小) - persist key #切换key从时效性转换为永久性 - ``` +**** -* 查询模式 - ```sh - 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 + zadd key score1 member1 [score2 member2] #添加数据 + zrem key member [member ...] #删除数据 + zremrangebyrank key start stop #删除指定索引的数据 + zremrangebyscore key min max #删除指定分数区间内的数据 + zscore key member #获取指定值的分数 + zincrby key increment member #指定值的分数增加increment ``` - +* 查询操作 + ```sh + zrange key start stop [WITHSCORES] #获取全部数据,升序,WITHSCORES代表显示分数 + zrevrange key start stop [WITHSCORES] #获取全部数据,降序 + zrangebyscore key min max [WITHSCORES] [LIMIT] #按条件获取数据 + zrevrangebyscore key max min [WITHSCORES] #按条件获取数据 + zcard key #获取集合数据的总量 + zcount key min max #获取指定分数区间内的数据总量 + zrank key member #获取数据对应的索引(排名)升序 + zrevrank key member #获取数据对应的索引(排名)降序 + ``` + * min与max用于限定搜索查询的条件 + * start与stop用于限定查询范围,作用于索引,表示开始和结束索引 + * offset与count用于限定查询范围,作用于查询结果,表示开始位置和数据总量 -*** +* 集合的交、并操作 + ```sh + zinterstore destination numkeys key [key ...] #两个集合的交集并存储到指定集合中 + zunionstore destination numkeys key [key ...] #两个集合的并集并存储到指定集合中 + ``` +注意事项: -### DB指令 +1. score保存的数据存储空间是64位,如果是整数范围是-9007199254740992~9007199254740992 +2. score保存的数据也可以是一个双精度的double值,基于双精度浮点数的特征可能会丢失精度,慎重使用 +3. sorted_set 底层存储还是基于 set 结构的,因此数据不能重复,如果重复添加相同的数据,score值将被反复覆盖,保留最后一次修改的结果 -key是由程序员定义,Redis在使用过程中,伴随着操作数据量的增加,会出现大量的数据以及对应的key,数据不区分种类、类别混在一起,容易引起重复或者冲突 -Redis为每个服务提供16个数据库,编码0-15,每个数据库之间的数据相互独立,**共用**Redis内存,不区分大小 -* 基本操作 +*** - ```sh - select index #切换数据库,index从0-15取值 - ping #测试数据库是否连接正常,返回PONG - ``` -* 扩展操作 - ```sh - move key db #数据移动到指定数据库,db是数据库编号 - dbsize #获取当前数据库的数据总量,即key的个数 - flushdb #清除当前数据库的所有数据 - flushall #清除所有数据 - ``` +#### 应用 - +* 排行榜 +* 对于基于时间线限定的任务处理,将处理时间记录为score值,利用排序功能区分处理的先后顺序 +* 当任务或者消息待处理,形成了任务队列或消息队列时,对于高优先级的任务要保障对其优先处理,采用score记录权重 @@ -8478,8 +8425,6 @@ Redis为每个服务提供16个数据库,编码0-15,每个数据库之间的 - - ## Jedis ### 基本使用 @@ -8503,7 +8448,7 @@ Jedis用于Java语言连接redis服务,并提供对应的操作API 2. 客户端连接redis API文档:http://xetorthio.github.io/jedis/ - 连接redis:`Jedis jedis = new Jedis("192.168..185", 6379);` + 连接redis:`Jedis jedis = new Jedis("192.168.0.185", 6379);` 操作redis:`jedis.set("name", "seazean"); jedis.get("name");` 关闭redis:`jedis.close();` @@ -9699,7 +9644,7 @@ sentinel在通知阶段要不断的去获取master/slave的信息,然后在各 -## 集群cluster +## 集群模式 ### 集群概述 diff --git a/Java.md b/Java.md index 123c053..ecfca31 100644 --- a/Java.md +++ b/Java.md @@ -4981,49 +4981,8 @@ JDK7对比JDK8: * **当链表长度超过(大于)阈值(或者红黑树的边界值,默认为 8)并且当前数组的长度大于等于64时,此索引位置上的所有数据改为红黑树存储。** * 将链表转换成红黑树前会进行判断,即使阈值大于8,但是数组长度小于64,此时并不会将链表变为红黑树,而是选择进行数组扩容。因为数组比较小的情况下变为红黑树结构,反而会降低效率,红黑树需要进行左旋,右旋,变色这些操作来保持平衡。同时数组长度小于64时,搜索时间相对快些,所以为了提高性能和减少搜索时间,底层在阈值大于8并且数组长度大于64时,链表才转换为红黑树,效率也变的更高效 - - - -*** - - - -##### 存储过程 - -存储过程图: - -![](https://gitee.com/seazean/images/raw/master/Java/HashMap哈希表存储过程.png) - -存储过程解析: - -```java -1. HashMap map = new HashMap<>(),当创建HashMap集合对象时,在jdk8之前,构造方法创建一个长度是16的Entry[] table 用来存储键值对数据。在jdk1.8之后不在HashMap构造方法创建数组,是在第一次调用put方法时创建数组Node[] table -2. 向哈希表存储数据 刘德华-53,根据刘德华调用String类中的hashCode()方法根据键计算哈希值,经过算法计算以后,得到在Node数组中的存放位置,图示索引为2,如果此位置为空,则直接添加数据 -3. 向哈希表中存储数据张学友-55,根据张学友计算出Node的索引是2,此位置不为空,比较刘德华和张学友的哈希值,不同则在此空间划出一个节点变为链表存储 -4. 向哈希表中存储数据刘德华-50,根据键计算索引为2,然后和已经存在的哈希值比较,相同则继续用equals()方法比较,返回false则继续添加,返回true则后添加的数据50替换之前的数据53 -``` - -常见面试题: - -* HashMap中hash函数是怎么实现的?还有哪些hash函数的实现方式? - 对于key的hashCode做hash操作,**无符号右移16位然后做异或运算**。还有平方取中法,伪随机数法和取余数法,这三种效率都比较低,无符号右移16位异或运算效率是最高的 -* 计算索引的方法? - 哈希值和链表长度**取模 (取余) hash%length**,计算机中求余效率不如位运算,所以采用 hash & (length-1) -* 当两个对象的hashCode相等时会怎么样? - 会产生哈希碰撞,若key值内容相同则替换旧的value,否则连接到链表后面,链表长度超过阈值8就转换为红黑树存储 -* 什么是哈希碰撞?如何解决哈希碰撞? - 只要两个元素的key计算的哈希码值相同就会发生**哈希碰撞** - jdk8前使用链表解决哈希碰撞,jdk8之后使用链表+红黑树解决哈希碰撞 -* 如果两个键的hashcode相同,如何存储键值对? - hashcode相同,通过equals比较内容是否相同: - * 相同:则新的value覆盖之前的value - * 不相同:则将新的键值对添加到哈希表中 -* 为什么要扩容? - 在不断的添加数据的过程中,会涉及到扩容问题,当超出临界值 (且要存放的位置非空) 时扩容,默认的扩容方式:**扩容为原来容量的2倍,并将原有的数据复制过来** -* 引入红黑树的原因? - JDK 1.8 以前 HashMap 的实现是 数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。当 HashMap 中有大量的元素都存放到同一个桶中时,就相当于一个长的单链表,假如单链表有 n 个元素,遍历的**时间复杂度是 O(n)**,完全失去了它的优势。针对这种情况,JDK 1.8 中引入了 红黑树(查找时**间复杂度为 O(logn)**)来优化这个问题,使得查找效率更高。 - -![](https://gitee.com/seazean/images/raw/master/Java/哈希表.png) + + ![](https://gitee.com/seazean/images/raw/master/Java/哈希表.png) @@ -5312,7 +5271,35 @@ transient int size; ##### 成员方法 -1. put +1. hash + + 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); + } + ``` + + * `(n - 1) & hash`:计算下标位置 + + + + ​ 面试题计算hash的方法:将hashCode无符号右移16位,高16 bit和低16 bit做了一个异或 + + * **为什么这样操作?** + + 如果当n即数组长度很小,假设是16,那么n-1即为 --> 1111 ,这样的值和hashCode()直接做按位与操作,实际上只使用了哈希值的后4位。如果当哈希值的高位变化很大,低位变化很小,就很容易造成哈希冲突了,所以这里**把高低位都利用起来,让高16位也参与运算**,从而解决了这个问题 + + * **余数本质是不断做除法,把剩余的数减去,运算效率要比位运算低** + +2. put jdk1.8前是头插法 (拉链法),多线程下扩容出现循环链表,jdk1.8以后引入红黑树,插入方法变成尾插法 @@ -5335,22 +5322,7 @@ transient int size; } ``` - putVal()方法中key在这里执行了一下hash(): - &(按位与运算):相同的二进制数位上,都是1的时候,结果为1,否则为零。 - ^(按位异或运算):相同的二进制数位上,数字相同,结果为0,不同为1。 - - * **注意**:HashMap是支持Key为空的;HashTable是直接用Key来获取HashCode,key为空会抛异常 - - ```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); - } - ``` - - 在putVal函数中使用到了上述hash函数计算的哈希值: + putVal()方法中key在这里执行了一下hash(),在putVal函数中使用到了上述hash函数计算的哈希值: ```java final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { @@ -5360,21 +5332,18 @@ transient int size; } ``` - * `(n - 1) & hash`:计算下标位置 - - - - 总结: hashcode 转化为32位二进制,高16 bit和低16 bit做了一个异或 - - * **为什么这样操作?** - - 如果当n即数组长度很小,假设是16,那么n-1即为 --> 1111 ,这样的值和hashCode()直接做按位与操作,实际上只使用了哈希值的后4位。如果当哈希值的高位变化很大,低位变化很小,就很容易造成哈希冲突了,所以这里**把高低位都利用起来,让高16位也参与运算**,从而解决了这个问题 - - * **余数本质是不断做除法,把剩余的数减去,运算效率要比位运算低** + * 什么是哈希碰撞?如何解决哈希碰撞? + 只要两个元素的key计算的哈希码值相同就会发生**哈希碰撞**,jdk8前使用链表解决哈希碰撞,jdk8之后使用链表+红黑树解决哈希碰撞 + * 如果两个键的hashcode相同,如何存储键值对? + hashcode相同,通过equals比较内容是否相同: + * 相同:则新的value覆盖之前的value + * 不相同:则将新的键值对添加到哈希表中 + * 引入红黑树的原因? + JDK 1.8 以前 HashMap 的实现是 数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。当 HashMap 中有大量的元素都存放到同一个桶中时,就相当于一个长的单链表,假如单链表有 n 个元素,遍历的**时间复杂度是 O(n)**,完全失去了它的优势。针对这种情况,JDK 1.8 中引入了 红黑树(查找时**间复杂度为 O(logn)**)来优化这个问题,使得查找效率更高 -2. treeifyBin +3. treeifyBin 节点添加完成之后判断此时节点个数是否大于TREEIFY_THRESHOLD临界值8,如果大于则将链表转换为红黑树,转换红黑树的方法 treeifyBin,整体代码如下: @@ -5390,7 +5359,7 @@ transient int size; -3. tableSizeFor +4. tableSizeFor 创建HashMap指定容量时,HashMap通过位移运算和或运算得到比指定初始化容量大的最小的2的n次幂 ```java @@ -5471,7 +5440,7 @@ transient int size; -4. resize +5. resize 当HashMap中的元素个数超过`(数组长度)*loadFactor(负载因子)`或者链表过长时,就会进行数组扩容,创建新的数组,伴随一次重新hash分配,并且遍历hash表中所有的元素,非常耗时,所以要尽量避免resize @@ -5486,13 +5455,13 @@ transient int size; 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) - + 红黑树节点:扩容时split方法会将树**拆成高位和低位两个链表**,判断长度是否小于等于6 - + ```java //如果低位链表首节点不为null,说明有这个链表存在 if (loHead != null) { @@ -18031,12 +18000,9 @@ Volatile是Java虚拟机提供的**轻量级**的同步机制(三大特性) - 不保证原子性 - 保证有序性(禁止指令重排) -作用:修饰成员变量和静态成员变量,避免线程从自己的工作缓存中查找变量的值,**线程操作 volatile 变量都是直接操作主存** - 性能:volatile修饰的变量进行读操作与普通变量几乎没什么差别,但是写操作相对慢一些,因为需要在本地代码中插入很多内存屏障来保证指令不会发生乱序执行,但是开销比锁要小 - *** @@ -18098,7 +18064,11 @@ Volatile是Java虚拟机提供的**轻量级**的同步机制(三大特性) #### 底层原理 -volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence) +使用volatile修饰的共享变量,总线会开启MESI缓存一致性协议以及CPU总线嗅探机制来解决JMM缓存一致性问题,也就是共享变量在多线程中可见性的问题。 + +底层实现主要是通过汇编 lock 前缀指令,共享变量加了 lock 前缀指令,在线程修改完共享变量后,会马上执行 store 和 write 操作。在执行 store 操作前,会先执行缓存锁定的操作,其他的CPU上运行的线程根据CPU总线嗅探机制会修改其共享变量为失效状态,读取时会重新从主内存中读取最新的数据 + +lock前缀指令就相当于内存屏障,Memory Barrier(Memory Fence) * 对 volatile 变量的写指令后会加入写屏障 * 对 volatile 变量的读指令前会加入读屏障 @@ -18589,12 +18559,12 @@ public final class Singleton { ### CAS -#### 概述 +#### 原语 CAS的全称是Compare-And-Swap,是**CPU并发原语** * CAS并发原语体现在Java语言中就是sun.misc.Unsafe类的各个方法,调用UnSafe类中的CAS方法,JVM会实现出CAS汇编指令,这是一种完全依赖于硬件的功能,通过它实现了原子操作 -* CAS是一种系统原语,原语属于操作系统范畴,是由若干条指令组成,用于完成某个功能的一个过程,并且**原语的执行必须是连续的,执行过程中不允许被中断**,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致的问题,也就是说CAS是线程安全的 +* CAS是一种系统原语,原语属于操作系统范畴,是由若干条指令组成 ,用于完成某个功能的一个过程,并且**原语的执行必须是连续的,执行过程中不允许被中断**,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致的问题,也就是说CAS是线程安全的 * CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证比较交换的原子性。在多核状态下,某个核执行到带 lock 的指令时,CPU 会让总线锁住,当这个核把此指令执行完毕,再开启总线,这个过程不会被线程的调度机制所打断,保证了多个线程对内存操作的原子性 作用:比较当前工作内存中的值和主物理内存中的值,如果相同则执行规定操作,否者继续比较直到主内存和工作内存的值一致为止 @@ -18606,7 +18576,7 @@ CAS特点: CAS缺点: -- 循环时间长,开销大(因为执行的是do while,如果比较不成功一直在循环,最差的情况,就是某个线程一直取到的值和预期值都不一样,这样就会无限循环),**使用CAS线程数不要超过CPU的核心数** +- 循环时间长,开销大(因为执行的是do while,如果比较不成功一直在循环,最差的情况某个线程一直取到的值和预期值都不一样,就会无限循环导致饥饿),**使用CAS线程数不要超过CPU的核心数** - 只能保证一个共享变量的原子操作 - 对于一个共享变量执行操作时,可以通过循环CAS的方式来保证原子操作 - 对于多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候只能用锁来保证原子性 @@ -19326,7 +19296,7 @@ Servlet 为了保证其线程安全,一般不为 Servlet 设置成员变量, ThreadLocal类用来提供线程内部的局部变量,这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量,ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程上下文 -作用: +ThreadLocal 作用: * 线程并发:应用在多线程并发的场景下 @@ -23993,6 +23963,8 @@ final void updateHead(Node h, Node p) { # Design +(待学习) + ## 单例模式 单例模式,是一种常用的软件设计模式。通过单例模式可以保证系统中, diff --git a/SSM.md b/SSM.md index 1851dd9..1ce2436 100644 --- a/SSM.md +++ b/SSM.md @@ -2505,7 +2505,7 @@ IoC和DI的关系:IoC与DI是同一件事站在不同角度看待问题 -##### 构造器注入 +##### 构造注入 标签:标签,的子标签 From 5d799b1e631f7d2901f33b74ed5f48765f05fa9c Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 27 May 2021 21:36:13 +0800 Subject: [PATCH 027/242] Update README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 00311d8..b5f8169 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ * DB:MySQL、JDBC、Redis * Frame:Maven +* Issue:Interview * Java:JavaSE、JVM、JUC、Design * SSM:MyBatis、Spring、SpringMVC、SpringBoot * Tool:Git、Linux、Docker From b6138b467048343b7624a08202fd154746f8d078 Mon Sep 17 00:00:00 2001 From: Seazean Date: Fri, 28 May 2021 00:00:56 +0800 Subject: [PATCH 028/242] Update Java Notes --- DB.md | 422 ++++++++++++++++++++++++++++++++++++++++--------------- Issue.md | 276 ++++++++++++++++++++++++++++++++++++ Java.md | 413 ++++++++++++++++++++++++++++++++++++----------------- Tool.md | 114 ++++++++++++--- 4 files changed, 962 insertions(+), 263 deletions(-) create mode 100644 Issue.md diff --git a/DB.md b/DB.md index e242c4d..304a6db 100644 --- a/DB.md +++ b/DB.md @@ -945,10 +945,12 @@ LIMIT SELECT * FROM product WHERE NAME LIKE '%电脑%'; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/DQL数据准备.png) + +*** + #### 函数查询 @@ -1217,7 +1219,7 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 SELECT * FROM product LIMIT 6,2; -- 第四页 开始索引=(4-1) * 2 ``` - ![](https://gitee.com/seazean/images/raw/master/DB/DQL分页查询图解.png) + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-DQL分页查询图解.png) @@ -2211,7 +2213,7 @@ redo log 也需要在事务提交时将日志写入磁盘,但是比将 Buffer 刷盘策略,通过修改参数 `innodb_flush_log_at_trx_commit` 设置: * 0:表示当提交事务时,并不将缓冲区的 redo 日志写入磁盘,而是等待主线程每秒刷新一次 -* 1:在事务提交时将缓冲区的 redo 日志同步写入到磁盘,保证一定会写入成功 +* 1:在事务提交时将缓冲区的 redo 日志**同步写入**到磁盘,保证一定会写入成功 * 2:在事务提交时将缓冲区的 redo 日志异步写入到磁盘,不能保证提交时肯定会写入,只是有这个动作 @@ -7672,7 +7674,7 @@ Redis (REmote DIctionary Server) :用 C 语言开发的一个开源的高性 ### 基本配置 -#### 文件结构 +#### 系统目录 1. 创建文件结构 @@ -7706,7 +7708,11 @@ Redis (REmote DIctionary Server) :用 C 语言开发的一个开源的高性 -#### 服务器配置 +*** + + + +#### 服务器 * 设置服务器以守护进程的方式运行,开启后服务器控制台中将打印服务器运行信息(同日志内容相同): @@ -7732,9 +7738,27 @@ Redis (REmote DIctionary Server) :用 C 语言开发的一个开源的高性 dir path ``` +* 设置数据库的数量: + ```sh + databases 16 + ``` + +* 多服务器快捷配置: -#### 客户端配置 + 导入并加载指定配置文件信息,用于快速创建 redis 公共配置较多的 redis实例配置文件,便于维护 + + ```sh + include /path/conf_name.conf + ``` + + + +*** + + + +#### 客户端 * 服务器允许客户端连接最大数量,默认0,表示无限制,当客户端连接到达上限后,Redis会拒绝新的连接: @@ -7750,6 +7774,10 @@ Redis (REmote DIctionary Server) :用 C 语言开发的一个开源的高性 +*** + + + #### 日志配置 * 设置服务器以指定日志记录级别: @@ -7948,7 +7976,7 @@ string类型的数据: 存储内容:通常使用字符串,如果字符串以整数的形式展示,可以作为数字操作使用 - + @@ -8354,7 +8382,7 @@ set类型:与hash存储结构完全相同,仅存储键不存储值(nil) sorted_set类型:在set的存储结构基础上添加可排序字段,类似于 TreeSet - + @@ -8555,7 +8583,7 @@ public JedisPool(GenericObjectPoolConfig poolConfig, String host, int port) { Redis Desktop Manager -![](https://gitee.com/seazean/images/raw/master/DB/Redis可视化工具.png) + @@ -8565,8 +8593,6 @@ Redis Desktop Manager - - ## 持久化 ### 概述 @@ -8576,7 +8602,7 @@ Redis Desktop Manager 作用:持久化用于防止数据的意外丢失,确保数据安全性 计算机中的数据全部都是二进制,保存一组数据有两种方式 - + 第一种:将当前数据状态进行保存,快照形式,存储数据结果,存储格式简单 @@ -8592,24 +8618,21 @@ Redis Desktop Manager #### save -指令:save +save指令:手动执行一次保存操作 配置redis.conf: ```sh dir path #设置存储.rdb文件的路径,通常设置成存储空间较大的目录中,目录名称data -dbfilename x.rdb #设置本地数据库文件名,默认值为dump.rdb,通常设置为dump-端口号.rdb -rdbcompression yes|no #设置存储至本地数据库时是否压缩数据,默认yes,设置为no节省CPU运行时间, - #但存储文件变大 +dbfilename "x.rdb" #设置本地数据库文件名,默认值为dump.rdb,通常设置为dump-端口号.rdb +rdbcompression yes|no #设置存储至本地数据库时是否压缩数据,默认yes,设置为no节省CPU运行时间 rdbchecksum yes|no #设置读写文件过程是否进行RDB格式校验,默认yes,设置为no,节约读写10%时间 - #消耗,单存在数据损坏的风险 + #消耗,但存在数据损坏的风险 ``` -save指令工作原理: - -![](https://gitee.com/seazean/images/raw/master/DB/save指令工作原理.png) +工作原理:redis 是个单线程的工作模式,会创建一个任务队列,所有的命令都会进到这个队列排队执行。当某个指令在执行的时候,队列后面的指令都要等待,所以这种执行方式会非常耗时。 -redis是个单线程的工作模式,它会创建一个任务队列,所有的命令都会进到这个队列,排队执行。当某个指令在执行的时候,队列后面的指令都要等待,所以这种执行方式会非常耗时。当cpu执行的时候会阻塞redis服务器,直到它执行完毕。 +save 指令的执行会阻塞当前 Redis 服务器,直到当前 RDB 过程完成为止,有可能会造成长时间阻塞,线上环境不建议使用 @@ -8633,11 +8656,13 @@ rdbchecksum yes|no bgsave指令工作原理: -![](https://gitee.com/seazean/images/raw/master/DB/bgsave指令工作原理.png) +![](https://gitee.com/seazean/images/raw/master/DB/Redis-bgsave工作原理.png) -流程:当执行bgsave的时候,客户端发出bgsave指令给到redis服务器。这时服务器返回信息告诉客户端后台已经开始执行指令,与此同时它使用Linux的fork函数创建一个子进程,让这个子进程去执行save相关的操作 +流程:当执行 bgsave 的时候,客户端发出 bgsave 指令给到 redis 服务器,服务器返回后台已经开始执行的信息给客户端,同时使用 fork函数 创建一个子进程,让这个子进程去执行save相关的操作,创建RDB文件保存起来,操作完以后把结果返回。 -子进程开始执行之后,就会创建RDB文件保存起来,操作完以后把结果返回。本质上bgsave的过程分成两个过程,第一个是服务端收到指令直接告诉客户端开始执行;另外一个过程是一个子进程在完成后台的保存操作,操作完以后返回消息。两个进程不相互影响。 +本质上bgsave的过程分成两个过程:第一个是服务端收到指令直接告诉客户端开始执行;另外一个过程是一个子进程在完成后台的保存操作,操作完以后返回消息,两个进程不相互影响 + +注意:bgsave命令是针对save阻塞问题做的优化,Redis内部所有涉及到RDB操作都采用bgsave的方式,save命令可以放弃使用 @@ -8645,12 +8670,14 @@ bgsave指令工作原理: -#### 自动RDB +#### 自动 -配置redis.conf +配置文件自动RDB,无需显式调用相关指令,save配置启动后底层执行的是 bgsave 操作 + +配置redis.conf: ```sh -save second changes#设置自动持久化的条件,满足限定时间范围内key的变化数量就进行持久化(底层bgsave) +save second changes#设置自动持久化条件,满足限定时间范围内key的变化数量就进行持久化(bgsave) ``` 参数: @@ -8658,31 +8685,37 @@ save second changes#设置自动持久化的条件,满足限定时间范围内 * second:监控时间范围 * changes:监控key的变化量 +说明: save 配置中对于 second 与 changes 设置通常具有互补对应关系,尽量不要设置成包含性关系 + 示例: ```sh save 300 10 #300s内10个key发生变化就进行持久化 ``` -原理: -![](https://gitee.com/seazean/images/raw/master/DB/RDB自动执行原理.png) +判定 key 变化的原理: +* 对数据产生了影响 +* 不进行数据比对,比如 name 键存在,重新 set name seazean 也算一次变化 +save配置要根据实际业务情况进行设置,频度过高或过低都会出现性能问题,结果可能是灾难性的 -*** +RDB三种启动方式对比: + +| 方式 | save指令 | bgsave指令 | +| -------------- | -------- | ---------- | +| 读写 | 同步 | 异步 | +| 阻塞客户端指令 | 是 | 否 | +| 额外内存消耗 | 否 | 是 | +| 启动新进程 | 否 | 是 | -#### 方式对比 +*** -* RDB三种启动方式对比 - | 方式 | save指令 | bgsave指令 | - | -------------- | -------- | ---------- | - | 读写 | 同步 | 异步 | - | 阻塞客户端指令 | 是 | 否 | - | 额外内存消耗 | 否 | 是 | - | 启动新进程 | 否 | 是 | + +#### 总结 * RDB特殊启动形式的指令(客户端输入) @@ -8698,18 +8731,20 @@ save 300 10 #300s内10个key发生变化就进行持久化 shutdown save ``` - * 全量复制 + 默认情况下执行 shutdown 命令时,自动执行 bgsave(如果没有开启AOF持久化功能) + + * 全量复制:主从复制部分详解 * RDB优点: - - RDB是一个紧凑压缩的二进制文件,存储效率较高 - - RDB内部存储的是redis在某个时间点的数据快照,非常适合用于数据备份,全量复制等场景 - - RDB恢复数据的速度要比AOF快很多 - - 应用:服务器中每X小时执行bgsave备份,并将RDB文件拷贝到远程机器中,用于灾难恢复 + - RDB 是一个紧凑压缩的二进制文件,存储效率较高,但存储数据量较大时,存储效率较低 + - RDB 内部存储的是 redis 在某个时间点的数据快照,非常适合用于数据备份,全量复制等场景 + - RDB 恢复数据的速度要比 AOF 快很多,因为是快照,直接恢复 + - 应用:服务器中每X小时执行 bgsave 备份,并将 RDB 文件拷贝到远程机器中,用于灾难恢复 * RDB缺点: - - RDB方式无论是执行指令还是利用配置,无法做到实时持久化,具有较大的可能性丢失数据 - - bgsave指令每次运行要执行fork操作创建子进程,会牺牲一些性能 - - Redis的众多版本中未进行RDB文件格式的版本统一,有可能出现各版本服务之间数据格式无法兼容现象 + - RDB 方式无论是执行指令还是利用配置,无法做到实时持久化,具有较大的可能性丢失数据 + - bgsave 指令每次运行要执行 fork 操作创建子进程,会牺牲一些性能 + - Redis 的众多版本中未进行 RDB 文件格式的版本统一,可能出现各版本之间数据格式无法兼容 @@ -8721,14 +8756,12 @@ save 300 10 #300s内10个key发生变化就进行持久化 #### 概述 -**AOF**(append only file)持久化:以独立日志的方式记录每次写命令,重启时再重新执行AOF文件中命令 达到恢复数据的目的。**与RDB相比可以简单理解为由记录数据改为记录数据产生的变化** - -AOF的主要作用是解决了数据持久化的实时性,目前已经是Redis持久化的主流方式 - -写数据过程: - +AOF (append only file) 持久化:以独立日志的方式记录每次写命令,重启时再重新执行AOF文件中命令达到恢复数据的目的,**与RDB相比可以简单理解为由记录数据改为记录数据的变化** +AOF 的主要作用是解决了数据持久化的实时性,目前已经是 Redis 持久化的主流方式 +AOF写数据过程: + @@ -8736,7 +8769,7 @@ AOF的主要作用是解决了数据持久化的实时性,目前已经是Redis -#### 基本配置 +#### 配置 启动AOF基本配置: @@ -8752,13 +8785,19 @@ appendfsync always|everysec|no #AOF写数据策略:默认为everysec AOF写数据三种策略(appendfsync) -- **always** (每次):每次写入操作均同步到AOF文件中,**数据零误差,性能较低**,不建议使用。 +- always (每次):每次写入操作均同步到AOF文件中,**数据零误差,性能较低**,不建议使用。 + +- everysec (每秒):每秒将缓冲区中的指令同步到AOF文件中,在系统突然宕机的情况下丢失1秒内的数据 数据**准确性较高,性能较高**,建议使用,也是默认配置 -- **everysec** (每秒):每秒将缓冲区中的指令同步到AOF文件中,在系统突然宕机的情况下丢失1秒内的数据 数据**准确性较高,性能较高**,建议使用,也是默认配置 +- no (系统控制):由操作系统控制每次同步到AOF文件的周期,整体过程**不可控** -- **no** (系统控制):由操作系统控制每次同步到AOF文件的周期,整体过程**不可控** +**AOF缓冲区同步文件策略**,系统调用 write 和 fsync: + +* write 操作会触发延迟写(delayed write)机制,Linux 在内核提供页缓冲区用来提高硬盘IO性能,write 操作在写入系统缓冲区后直接返回 +* 同步硬盘操作依赖于系统调度机制,比如缓冲区页空间写满或达到特定时间周期。同步文件之前,如果此时系统故障宕机,缓冲区内数据将丢失 +* fsync 针对单个文件操作(比如AOF文件)做强制硬盘同步,fsync 将阻塞到写入硬盘完成后返回,保证了数据持久化 @@ -8766,19 +8805,19 @@ AOF写数据三种策略(appendfsync) -#### AOF重写 +#### 重写 -##### 重写介绍 +##### 介绍 -场景:AOF写数据时,多条指令设置同一个key +随着命令不断写入 AOF,文件会越来越大,为了解决这个问题 Redis 引入了 AOF 重写机制压缩文件体积 -AOF重写:将Redis进程内的数据转化为写命令同步到新AOF文件的过程,简单说就是将对同一个数据的若干个条命令执行结果转化成最终结果数据对应的指令进行记录 +AOF 重写:将Redis进程内的数据转化为写命令同步到**新** AOF 文件的过程,简单说就是将对同一个数据的若干个条命令执行结果转化成最终结果数据对应的指令进行记录 AOF重写作用: - 降低磁盘占用量,提高磁盘利用率 - 提高持久化效率,降低持久化写时间,提高IO性能 -- 降低数据恢复用时,提高数据恢复效率 +- 降低数据恢复的用时,提高数据恢复效率 AOF重写规则: @@ -8787,19 +8826,21 @@ AOF重写规则: - 非写入类的无效指令将被忽略,只保留最终数据的写入命令 - 如del key1、 hdel key2、srem key3、set key4 111、set key4 222等 - - 如select指令虽然不更改数据,但是更改了数据的存储位置,此类命令同样需要记录 + 如del key1、 hdel key2、srem key3、set key4 111、set key4 222等,select 指令虽然不更改数据,但是更改了数据的存储位置,此类命令同样需要记录 - 对同一数据的多条写命令合并为一条命令 - 如lpushlist1 a、lpush list1 b、lpush list1 c可以转化为:lpush list1 a b c。 + 如lpushlist1 a、lpush list1 b、lpush list1 c 可以转化为:lpush list1 a b c。 为防止数据量过大造成客户端缓冲区溢出,对list、set、hash、zset等类型,每条指令最多写入64个元素 -##### 重写方式 +*** + + + +##### 方式 * 手动重写 @@ -8809,9 +8850,7 @@ AOF重写规则: 原理分析: - ![](https://gitee.com/seazean/images/raw/master/DB/AOF手动重写原理.png) - - + ![](https://gitee.com/seazean/images/raw/master/DB/Redis-AOF手动重写原理.png) * 自动重写 @@ -8820,7 +8859,7 @@ AOF重写规则: auto-aof-rewrite-percentage percent #触发AOF文件执行重写的增长率,当前AOF文件大小超过上一次重写的AOF文件大小的百分之多少才会重写 ``` - 自动重写触发比对参数( 运行指令info Persistence获取具体信息 ): + 自动重写触发比对参数( 运行指令 `info Persistence` 获取具体信息 ): ```sh aof_current_size #AOF文件当前尺寸大小(单位:字节) @@ -8840,13 +8879,15 @@ AOF重写规则: -#### 工作流程 +#### 流程 -![](https://gitee.com/seazean/images/raw/master/DB/AOF重写流程1.png) +持久化流程: -![](https://gitee.com/seazean/images/raw/master/DB/AOF重写流程2.png) +![](https://gitee.com/seazean/images/raw/master/DB/Redis-AOF重写流程1.png) +重写流程: + @@ -8854,34 +8895,32 @@ AOF重写规则: -### RA对比 +#### 对比 RDB与AOF对比: | 持久化方式 | RDB | AOF | | ------------ | ------------------ | ------------------ | | 占用存储空间 | 小(数据级:压缩) | 大(指令级:重写) | -| 存储速度 | 慢 | 快 | -| 恢复速度 | 快 | 慢 | +| **存储速度** | 慢 | 快 | +| **恢复速度** | 快 | 慢 | | 数据安全性 | 会丢失数据 | 依据策略决定 | | 资源消耗 | 高/重量级 | 低/轻量级 | | 启动优先级 | 低 | 高 | - - 应用场景: -- 对数据非常敏感,建议使用默认的AOF持久化方案 +- 对数据非常敏感,建议使用默认的 AOF 持久化方案 - AOF持久化策略使用everysecond,每秒钟fsync一次。该策略redis仍可以保持很好的处理性能,当出现问题时,最多丢失0-1秒内的数据。 + AOF 持久化策略使用 everysecond,每秒钟 fsync 一次,该策略 redis 仍可以保持很好的处理性能,当出现问题时,最多丢失0-1秒内的数据。 - 注意:AOF文件存储体积较大,恢复速度较慢 + 注意:AOF文件存储体积较大,恢复速度较慢,因为要执行每条指令 -- 数据呈现阶段有效性,建议使用RDB持久化方案 +- 数据呈现阶段有效性,建议使用 RDB 持久化方案 数据可以良好的做到阶段内无丢失,且恢复速度较快,阶段内数据恢复通常采用RDB方案 - 注意:利用RDB实现紧凑的数据持久化会使Redis降的很低 + 注意:利用 RDB 实现紧凑的数据持久化会使 Redis 降的很低 综合对比: @@ -8893,12 +8932,152 @@ AOF重写规则: +*** + + + +### fork + +(待整理) + +fork函数讲解文章:https://blog.csdn.net/love_gaohz/article/details/41727415 + + + **** +## 事务机制 + +### 基本操作 + +redis事务就是一个命令执行的队列,将一系列预定义命令包装成一个整体(一个队列)。当执行时按照添加顺序依次执行,中间不会被打断或者干扰 + +* 开启事务 + + ```sh + multi #设定事务的开启位置,此指令执行后,后续的所有指令均加入到事务中 + ``` + +* 执行事务 + + ```sh + exec #设定事务的结束位置,同时执行事务,与multi成对出现,成对使用 + ``` + + 加入事务的命令暂时进入到任务队列中,并没有立即执行,只有执行 exec 命令才开始执行 + +* 取消事务 + + ```sh + discard #终止当前事务的定义,发生在multi之后,exec之前 + ``` + + 一般用于事务执行过程中输入了错误的指令,直接取消这次事务 + + + +*** + + + +### 工作流程 + +事务机制整体工作流程: + +![](https://gitee.com/seazean/images/raw/master/DB/Redis-事务的工作流程.png) + +几种常见错误: + +* 定义事务的过程中,命令格式输入错误,出现语法错误造成,整体事务中所有命令均不会执行,包括那些语法正确的命令,事务直接消失 + + + +* 定义事务的过程中,命令执行出现错误,例如对字符串进行 incr 操作,能够正确运行的命令会执行,运行错误的命令不会被执行 + + + +* 已经执行完毕的命令对应的数据不会自动回滚,需要程序员在代码中实现回滚,应该尽可能避免: + + 事务操作之前记录数据的状态 + + * 单数据:string + * 多数据:hash、list、set、zset + + 设置指令恢复所有的被修改的项 + + * 单数据:直接set(注意周边属性,例如时效) + * 多数据:修改对应值或整体克隆复制 + + + +*** + + + +### 监控锁 + +对 key 添加监视锁,在执行 exec 前如果其他客户端的操作导致 key 发生了变化,执行结果为 nil + +* 添加监控锁 + + ```sh + watch key1 [key2……] #可以监控一个或者多个key + ``` + +* 取消对所有 key 的监视 + + ```sh + unwatch + ``` + +应用:基于状态控制的批量任务执行,防止其他线程对变量的修改 + + + +*** + + + +### 分布式锁 + +Redis 分布式锁的基本使用 + +* 使用 setnx 设置一个公共锁 + + ```sh + setnx lock-key value # value任意数,返回为1设置成功,返回为0设置失败 + ``` + + * 对于返回设置成功的,拥有控制权,进行下一步的具体业务操作 + * 对于返回设置失败的,不具有控制权,排队或等待 + +* 操作完毕通过del操作释放锁 + + ```sh + del lock-key + ``` + +* 使用 expire 为锁 key 添加存活(持有)时间,过期自动删除(放弃)锁 + + ```sh + expire lock-key second + pexpire lock-key milliseconds + ``` + +应用:解决抢购时出现超卖现象 + + + + + +*** + + + ## 删除策略 ### 过期数据 @@ -8907,15 +9086,15 @@ Redis是一种内存级数据库,所有数据均存放在内存中,内存中 TTL返回的值有三种情况:正数,-1,-2 -- **正数**:代表该数据在内存中还能存活的时间 -- **-1**:永久有效的数据 -- **2** :已经过期的数据或被删除的数据或未定义的数据 +- 正数:代表该数据在内存中还能存活的时间 +- -1:永久有效的数据 +- 2 :已经过期的数据或被删除的数据或未定义的数据 删除策略:**删除策略就是针对已过期数据的处理策略**,已过期的数据不一定被立即删除,在不同的场景下使用不同的删除方式会有不同效果,这就是删除策略的问题 过期数据是一块独立的存储空间,Hash结构,field是内存地址,value是过期时间,保存了所有key的过期描述,在最终进行过期处理的时候,对该空间的数据进行检测, 当时间到期之后通过field找到内存该地址处的数据,然后进行相关操作 -![](https://gitee.com/seazean/images/raw/master/DB/Redis-时效性数据的存储结构.png) + @@ -8949,8 +9128,6 @@ TTL返回的值有三种情况:正数,-1,-2 - 缺点:CPU压力很大,无论CPU此时负载量多高,均占用CPU,会影响redis服务器响应时间和指令吞吐量 - 总结:用处理器性能换取存储空间(拿时间换空间) -![](https://gitee.com/seazean/images/raw/master/DB/Redis-定时删除.png) - *** @@ -8964,6 +9141,8 @@ TTL返回的值有三种情况:正数,-1,-2 * 如果未过期,返回数据 * 如果已过期,删除,返回不存在 +在任何 get 操作之前都要执行 **expireIfNeeded()**,相当于绑定在一起 + 特点: * 优点:节约CPU性能,发现必须删除的时候才删除 @@ -8976,30 +9155,35 @@ TTL返回的值有三种情况:正数,-1,-2 -#### 定期删除 +#### 定期删除 定时删除和惰性删除这两种方案都是走的极端,定期删除就是折中方案 +定期删除是周期性轮询 redis 库中的时效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删除频度 + 定期删除方案: -- Redis启动服务器初始化时,读取配置server.hz的值,默认为10 +- Redis启动服务器初始化时,读取配置 server.hz 的值,默认为10。执行指令可以查看:info server + +- 每秒钟执行 server.hz 次 serverCron() --> databasesCron() --> activeExpireCycle() -- 每秒钟执行server.hz次**serverCron()--->databasesCron()--->activeExpireCycle()** +- databasesCron()操作是轮询每个数据库 -- activeExpireCycle()对每个expires[*]逐一进行检测,每次执行耗时:250ms/server.hz +- activeExpireCycle() 对某个数据库中的每个 expires 进行检测,每次执行耗时:250ms/server.hz -- 对某个expires[*]检测时,随机挑选W个key检测 - - 如果key超时,删除key - - 如果一轮中删除的key的数量>W*25%,循环该过程 - - 如果一轮中删除的key的数量≤W*25%,检查下一个expires[],0-15循环 - - W取值=ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP属性值 + 对某个 expires[*] 检测时,随机挑选 W 个 key 检测 -* 参数current_db用于记录activeExpireCycle() 进入哪个expires[*] 执行 -* 如果activeExpireCycle()执行时间到期,下次从current_db继续向下执行 + - 如果 key 超时,删除 key + - 如果一轮中删除的 key 的数量 > W*25%,循环该过程 + - 如果一轮中删除的 key 的数量 ≤ W*25%,检查下一个expires[],0-15循环 + - W取值 = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 属性值,自定义值 - +* 参数 current_db 用于记录 activeExpireCycle() 进入哪个expires[*] 执行 +* 如果 activeExpireCycle() 执行时间到期,下次从 current_db 继续向下执行 -总结:定期删除就是周期性轮询redis库中的时效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删除频度 + + +定期删除特点: - CPU性能占用设置有峰值,检测频度可自定义设置 - 内存压力不是很大,长期占用内存的冷数据会被持续清理 @@ -9007,6 +9191,8 @@ TTL返回的值有三种情况:正数,-1,-2 +*** + #### 策略对比 @@ -9027,9 +9213,9 @@ TTL返回的值有三种情况:正数,-1,-2 #### 逐出算法 -**数据淘汰策略**:当新数据进入redis时,在执行每一个命令前,会调用**freeMemoryIfNeeded()**检测内存是否充足。如果内存不满足新加入数据的最低存储要求,redis要临时删除一些数据为当前指令清理存储空间,清理数据的策略称为**逐出算法** +数据淘汰策略:当新数据进入redis时,在执行每一个命令前,会调用 **freeMemoryIfNeeded()** 检测内存是否充足。如果内存不满足新加入数据的最低存储要求,redis要临时删除一些数据为当前指令清理存储空间,清理数据的策略称为**逐出算法** -注意:逐出数据的过程不是100%能够清理出足够的可使用的内存空间,如果不成功则反复执行。当对所有数据尝试完毕,如不能达到内存清理的要求,将出现错误信息如下: +注意:逐出数据的过程不是 100% 能够清理出足够的可使用的内存空间,如果不成功则反复执行,当对所有数据尝试完毕,如不能达到内存清理的要求,将出现错误信息如下: ```shell (error) OOM command not allowed when used memory >'maxmemory' @@ -9043,38 +9229,38 @@ TTL返回的值有三种情况:正数,-1,-2 #### 策略配置 -影响数据淘汰的相关配置如下,配置conf文件: +影响数据淘汰的相关配置如下,配置 conf 文件: * 最大可使用内存,即占用物理内存的比例,默认值为0,表示不限制。生产环境中根据需求设定,通常设置在50%以上 - ```properties + ```sh maxmemory ?mb ``` -* 每次选取待删除数据的个数,采用随机获取数据的方式作为待检测删除数据 +* 每次选取待删除数据的个数,采用随机获取数据的方式作为待检测删除数据,防止全库扫描,导致严重的性能消耗,降低读写性能 - ```properties + ```sh maxmemory-samples count ``` -* 对数据进行删除的选择策略 +* 达到最大内存后的,对被挑选出来的数据进行删除的策略 - ```properties + ```sh maxmemory-policy policy ``` 数据删除的策略policy:3类8种 - **第一类**:检测易失数据(可能会过期的数据集server.db[i].expires ): + 第一类:检测易失数据(可能会过期的数据集server.db[i].expires ): ```sh - volatile-lru #挑选最近最少使用的数据淘汰 + volatile-lru #挑选最近最久未使用使用的数据淘汰 volatile-lfu #挑选最近使用次数最少的数据淘汰 volatile-ttl #挑选将要过期的数据淘汰 volatile-random #任意选择数据淘汰 ``` - **第二类**:检测全库数据(所有数据集server.db[i].dict ): + 第二类:检测全库数据(所有数据集server.db[i].dict ): ```sh allkeys-lru #挑选最近最少使用的数据淘汰 @@ -9082,13 +9268,13 @@ TTL返回的值有三种情况:正数,-1,-2 allkeys-random #任意选择数据淘汰,相当于随机 ``` - **第三类**:放弃数据驱逐 + 第三类:放弃数据驱逐 ```sh no-enviction #禁止驱逐数据(redis4.0中默认策略),会引发OOM(Out Of Memory) ``` -**数据淘汰策略配置依据**: +数据淘汰策略配置依据: 使用INFO命令输出监控信息,查询缓存 hit 和 miss 的次数,根据业务需求调优Redis配置 @@ -9990,7 +10176,7 @@ sentinel在通知阶段要不断的去获取master/slave的信息,然后在各 1. 缓存null:对查询结果为null的数据进行缓存 (长期使用,定期清理) ,设定短时限,例如30-60秒,最高5分钟 -2. 白名单策略:提前预热各种分类数据id对应的bitmaps,id作为bitmaps的offset,相当于设置了数据白名单。当加载正常数据时放行,加载异常数据时直接拦截(效率偏低),使用布隆过滤器(有关布隆过滤器的命中问题对当前状况可以忽略) +2. 白名单策略:提前预热各种分类数据id对应的**bitmaps**,id作为bitmaps的offset,相当于设置了数据白名单。当加载正常数据时放行,加载异常数据时直接拦截(效率偏低),使用布隆过滤器(有关布隆过滤器的命中问题对当前状况可以忽略) 3. 实施监控:实时监控redis命中率(业务正常范围时,通常会有一个波动值)与null数据的占比 @@ -10003,6 +10189,10 @@ sentinel在通知阶段要不断的去获取master/slave的信息,然后在各 **总的来说**:缓存击穿是指访问了不存在的数据,跳过了合法数据的redis数据缓存阶段,每次访问数据库,导致对数据库服务器造成压力。通常此类数据的出现量是一个较低的值,当出现此类情况以毒攻毒,并及时报警。无论是黑名单还是白名单,都是对整体系统的压力,警报解除后尽快移除 +https://www.bilibili.com/video/BV15y4y1r7X3 + + + *** diff --git a/Issue.md b/Issue.md new file mode 100644 index 0000000..9dbbc39 --- /dev/null +++ b/Issue.md @@ -0,0 +1,276 @@ +# 计算机基础 + +## 计算机网络 + +### 传输层 + + + +四次挥手 + + + +* **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是通过序列号来保证这两种性质的,**三次握手就是互换序列号**的一次过程 + + 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 这两个步骤可以合并,**只需要三次握手,**可以提高连接的速度与效率。 + + **原因二**:第三次握手是为了防止失效的连接请求到达服务器,让服务器错误打开连接。客户端发送的连接请求如果在网络中滞留,那么就会隔很长一段时间才能收到服务器端发回的连接确认。客户端等待一个超时重传时间之后,就会重新请求连接。但是这个滞留的连接请求最后还是会到达服务器,如果不进行三次握手,那么服务器就会打开两个连接。如果有第三次握手,客户端会忽略服务器之后发送的对滞留连接请求的连接确认,不进行第三次握手,因此就不会再次打开连接 + + + +* **三次握手的第三次握手发送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报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手 + + + +* TCP 协议如何保证可靠传输? + + + + + +*** + + + +## HTTP + +* **对称加密和非对称加密** + + 对称加密:加密和解密使用同一个秘钥,把密钥转发给需要发送数据的客户机,中途会被拦截(类似于把带锁的箱子和钥匙给别人,对方打开箱子放入数据,上锁后发送) + + * 优点:运算速度快 + * 缺点:无法安全的将密钥传输给通信方 + + 非对称加密:加密和解密使用不同的秘钥,一把作为公开的公钥,另一把作为私钥,公钥公开给任何人(类似于把锁和箱子给别人,对方打开箱子放入数据,上锁后发送) + + * 优点:可以更安全地将公开密钥传输给通信发送方 + * 缺点:运算速度慢 + +* **使用对称加密和非对称加密的方式传送数据** + + * 使用非对称密钥加密方式,传输对称密钥加密方式所需要的 Secret Key,从而保证安全性; + * 获取到 Secret Key 后,再使用对称密钥加密方式进行通信,从而保证效率 + + 思想:锁上加锁 + + + +* **HTTP1.1新特性** + + 默认是长连接、支持流水线、支持同时打开多个 TCP 连接、支持虚拟主机、支持分块传输编码 + 新增状态码 100、新增缓存处理指令 max-age + + + +* **Get和POST比较** + + 作用:GET 用于获取资源,而 POST 用于传输实体主体 + + 参数:GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中。不能因为 POST 参数存储在实体主体中就认为它的安全性更高,因为照样可以通过一些抓包工具(Fiddler)查看 + + 安全:安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的。GET方法是安全的,而POST不是,因为 POST 的目的是传送实体主体内容 + + * 安全的方法除了 GET 之外还有:HEAD、OPTIONS + * 不安全的方法除了 POST 之外还有 PUT、DELETE + + 幂等性:同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的。所有的安全方法也都是幂等的。在正确实现条件下,GET,HEAD,PUT 和 DELETE 等方法都是幂等的,POST 方法不是 + + 可缓存:如果要对响应进行缓存,需要满足以下条件 + + * 请求报文的 HTTP 方法本身是可缓存的,包括 GET 和 HEAD,但是 PUT 和 DELETE 不可缓存,POST 在多数情况下不可缓存的 + * 响应报文的状态码是可缓存的,包括:200, 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501 + * 响应报文的 Cache-Control 首部字段没有指定不进行缓存 + + + +*** + + + +## 操作系统 + +### 进程线程 + +* 操作系统? + + 控制和管理计算机硬件与软件资源的,并合理的组织和调度计算机工作的程序 + +* 什么是系统调度? + + 在用户程序中调用操作系统提供的核心态级别的子功能,结合用户态和核心态区别回答,一般使用陷入(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 + + + + + +### 内存管理 + +* 操作系统的内存管理主要是做什么? + + 操作系统的内存管理主要负责内存的分配与回收,地址转换也就是将逻辑地址转换成相应的物理地址 + +* 内存管理有哪几种方式? + + 连续分配管理方式:块式管理,将内存分为几个固定大小的块,每个块中只包含一个进程 + + 非连续分配管理方式:分页存储,分段存储,段页式管理 + +* 分页机制和分段机制有哪些共同点和区别呢? + + 共同点 : + + - 分页机制和分段机制都是为了提高内存利用率,较少内存碎片 + + 分页、段页式:内部碎片 + + - 以两者都是离散分配内存的方式。但是,每个页和段中的内存是连续的 + + 不同点: + + * 分页对程序员是透明的,但是分段需要程序员显式划分每个段 + * 分页是一维地址空间,分段是二维地址空间 + * 页的大小是固定的,由操作系统决定;而段的大小不固定,取决于我们当前运行的程序 + * 分页主要用于实现虚拟内存,从而获得更大的地址空间;分段主要是为了使程序和数据可以被划分为逻辑上独立的地址空间并且有助于共享和保护 + +* 快表和多级页表 + + 快表:虚拟地址到物理地址的转换要快 + + * 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 ecfca31..7c8558c 100644 --- a/Java.md +++ b/Java.md @@ -12046,116 +12046,6 @@ public class Demo1_27 { -*** - - - -### 对象结构 - -#### 基本构造 - -一个Java对象内存中存储为三部分:对象头 (Header)、实例数据 (Instance Data) 和对齐填充 (Padding) - -对象头: - -* 普通对象(32位系统,64位128位):分为两部分 - - * Mark Word:用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,就是Mark Word - - ```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 - - ```ruby - |-----------------------------------------------------| - | Object Header (64 bits) | - |---------------------------|-------------------------| - | Mark Word (32 bits) | Klass Word (32 bits) | - |---------------------------|-------------------------| - ``` - -* 数组对象:如果对象是一个数组,那在对象头中还有一块数据用于记录数组长度 - - ```ruby - |-------------------------------------------------------------------------------| - | Object Header (96 bits) | - |-----------------------|-----------------------------|-------------------------| - | Mark Word(32bits) | Klass Word(32bits) | array length(32bits) | - |-----------------------|-----------------------------|-------------------------| - ``` - -实例数据:实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来 - -对齐填充:起占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,就是对象的大小必须是8字节的整数倍,而对象头部分正好是8字节的倍数(1倍或者2倍),因此当对象实例数据部分没有对齐时,就需要通过对齐填充来补全 - -32位系统 - -* 一个int在java中占据4byte,所以Integer的大小为: - - ```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 - ``` - - - -*** - - - -#### 节约内存 - -* 尽量使用基本类型 - -* 满足容量前提下,尽量用小字段 - -* 尽量用数组,少用集合,数组中是可以使用基本类型的,但是集合中只能放包装类型,如果需要使用集合,推荐比较节约内存的集合工具:fastutil - - 一个ArrayList集合,如果里面放了10个数字,占用多少内存: - - ```java - private transient Object[] elementData; - private int size; - ``` - - Mark Word 占4byte,Klass Word 占4byte,一个int字段(size)占 4byte,elementData 数组本身占 12(4+4+4),数组中10个Integer对象占 10×16,所以整个集合空间大小为 184byte - -* 时间用long/int表示,不用Date或者String - - - -*** - - - -#### 对象访问 - -JVM是通过栈帧中的对象引用访问到其内部的对象实例:(内部结构查看类加载部分) - -* 句柄访问 - 使用该方式,Java堆中会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息 - 优点:reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。 - - - -* 直接指针(HotSpot采用) - 使用该方式,Java堆对象的布局必须考虑如何放置访问类型数据的相关信息,reference中直接存储的就是对象地址 - 优点:速度更快,**节省了一次指针定位的时间开销** - - - - - *** @@ -13264,8 +13154,142 @@ Serial GC、Parallel GC、Concurrent Mark Sweep GC这三个GC不同: ## 类加载 +### 对象结构 + +#### 基本构造 + +一个Java对象内存中存储为三部分:对象头 (Header)、实例数据 (Instance Data) 和对齐填充 (Padding) + +对象头: + +* 普通对象(32位系统,64位128位):分为两部分 + + * Mark Word:用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,就是Mark Word + + ```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 + + ```ruby + |-----------------------------------------------------| + | Object Header (64 bits) | + |---------------------------|-------------------------| + | Mark Word (32 bits) | Klass Word (32 bits) | + |---------------------------|-------------------------| + ``` + +* 数组对象:如果对象是一个数组,那在对象头中还有一块数据用于记录数组长度 + + ```ruby + |-------------------------------------------------------------------------------| + | Object Header (96 bits) | + |-----------------------|-----------------------------|-------------------------| + | Mark Word(32bits) | Klass Word(32bits) | array length(32bits) | + |-----------------------|-----------------------------|-------------------------| + ``` + +实例数据:实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来 + +对齐填充:起占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,就是对象的大小必须是8字节的整数倍,而对象头部分正好是8字节的倍数(1倍或者2倍),因此当对象实例数据部分没有对齐时,就需要通过对齐填充来补全 + +32位系统 + +* 一个int在java中占据4byte,所以Integer的大小为: + + ```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 + ``` + + + +*** + + + +#### 节约内存 + +* 尽量使用基本类型 + +* 满足容量前提下,尽量用小字段 + +* 尽量用数组,少用集合,数组中是可以使用基本类型的,但是集合中只能放包装类型,如果需要使用集合,推荐比较节约内存的集合工具:fastutil + + 一个ArrayList集合,如果里面放了10个数字,占用多少内存: + + ```java + private transient Object[] elementData; + private int size; + ``` + + Mark Word 占4byte,Klass Word 占4byte,一个int字段(size)占 4byte,elementData 数组本身占 12(4+4+4),数组中10个Integer对象占 10×16,所以整个集合空间大小为 184byte + +* 时间用long/int表示,不用Date或者String + + + +*** + + + +#### 对象访问 + +JVM是通过栈帧中的对象引用访问到其内部的对象实例:(内部结构查看类加载部分) + +* 句柄访问 + 使用该方式,Java堆中会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息 + 优点:reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。 + + + +* 直接指针(HotSpot采用) + 使用该方式,Java堆对象的布局必须考虑如何放置访问类型数据的相关信息,reference中直接存储的就是对象地址 + 优点:速度更快,**节省了一次指针定位的时间开销** + + + + + + + +*** + + + ### 对象创建 +#### 生命周期 + +在Java中,对象的生命周期包括以下几个阶段: + +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 + + + +*** + + + #### 创建时机 类在第一次实例化加载一次,后续实例化不再加载,引用第一次加载的类 @@ -13712,7 +13736,7 @@ init指的是实例构造器,主要作用是在类实例化过程中执行, * **应用程序类加载器(Application ClassLoader)**: * 由AppClassLoader(sun.misc.Launcher$AppClassLoader)实现,上级为 Extension * 负责加载环境变量classpath或系统属性 `java.class.path` 指定路径下的类库 - * 这个类加载器是ClassLoader中的getSystemClassLoader() 方法的返回值,因此称为系统类加载器 + * 这个类加载器是ClassLoader中的 getSystemClassLoader() 方法的返回值,因此称为系统类加载器 * 可以直接使用这个类加载器,如果应用程序中没有自定义类加载器,这个就是程序中默认的类加载器 * 自定义类加载器:由开发人员自定义的类加载器,上级是Application @@ -16261,7 +16285,7 @@ windows: linux: -* ps -fe 查看所有进程 +* ps -ef 查看所有进程 * ps -fT -p 查看某个进程(PID)的所有线程 * kill 杀死进程 * top 按大写 H 切换是否显示线程 @@ -17980,7 +18004,7 @@ Linux查看CPU缓存行:cat /sys/devices/system/cpu/cpu0/cache/index0/coherenc 解决方法:各个处理器访问缓存时都遵循一些协议,在读写时要根据协议进行操作,协议主要有MSI、MESI等 -* MESI:当写数据时,如果发现操作的变量是共享变量,即在其它处理器中也存在该变量的副本,会发出信号通知其它处理器将该内存变量的缓存行设置为无效,因此当其它处理器读取这个变量,发现该变量是无效的,那么就会从内存中重新读取 +* MESI:缓存一致性协议,当写数据时,如果发现操作的变量是共享变量,即在其它处理器中也存在该变量的副本,会发出信号通知其它处理器将该内存变量的缓存行设置为无效,因此当其它处理器读取这个变量,发现该变量是无效的,那么就会从内存中重新读取 * 总线嗅探:每个处理器通过嗅探在总线上传播的数据来检查自己缓存值是否过期了,当处理器发现自己的缓存对应的内存地址被修改,就将当前处理器的缓存行设置为无效状态,当处理器对这个数据进行操作时,会重新从内存中把数据读取到处理器缓存中 * 总线风暴:由于Volatile的MESI缓存一致性协议,需要不断的从主内存嗅探和CAS循环,无效的交互会导致总线带宽达到峰值;因此不要大量使用volatile关键字,至于什么时候使用volatile、syschonized都是需要根据实际场景 @@ -19294,7 +19318,9 @@ Servlet 为了保证其线程安全,一般不为 Servlet 设置成员变量, #### 基本介绍 -ThreadLocal类用来提供线程内部的局部变量,这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量,ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程上下文 +ThreadLocal类用来提供线程内部的局部变量,这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量 + +ThreadLocal实例通常来说都是`private static`类型的,属于一个线程的本地变量,用于关联线程和线程上下文。每个线程都会在 ThreadLocal 中保存一份该线程独有的数据,所以是线程安全的 ThreadLocal 作用: @@ -19373,7 +19399,12 @@ public class MyDemo { ##### 应用场景 -解决事务问题,ThreadLocal方案有两个突出的优势: +ThreadLocal 适用于如下两种场景 + +- 每个线程需要有自己单独的实例 +- 实例需要在多个方法中共享,但不希望被多线程共享 + +**事务管理**,ThreadLocal方案有两个突出的优势: 1. 传递数据:保存每个线程绑定的数据,在需要的地方可以直接获取,避免参数直接传递带来的代码耦合问题 @@ -19622,7 +19653,7 @@ static class Entry extends WeakReference> { } ``` - 这里定义了一个AtomicInteger,每次获取当前值并加上HASH_INCREMENT,这个值跟斐波那契数列(黄金分割数)有关,这样做可以尽量避免hash冲突,让哈希码能均匀的分布在2的n次方的数组里 + ThreadLocal 的散列方式称之为 **斐波那契散列**。这里定义了一个AtomicInteger,每次获取当前值并加上HASH_INCREMENT,这个值跟斐波那契数列(黄金分割数)有关,这样做可以尽量避免hash冲突,让哈希码能均匀的分布在2的n次方的数组里 * set() @@ -19666,16 +19697,38 @@ static class Entry extends WeakReference> { return ((i + 1 < len) ? i + 1 : 0); } - // 扩容阈值时长度的2/3 + ``` + + 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; + threshold = len * 2 / 3; + } + // 扩容条件 + private void rehash() { + expungeStaleEntries(); + if (size >= threshold - threshold / 4) + resize(); } ``` - ThreadLocalMap使用**线性探测法**来解决哈希冲突: + Entry 数组为扩容为 原来的2倍 ,重新计算 key 的散列值,如果遇到 key 为 NULL 的情况,会将其 value 也置为 NULL,帮助虚拟机进行GC - * 该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出 - * 假设当前table长度为16,计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,如果还是冲突会回到0,取table[0],以此类推,直到可以插入,可以把Entry[] table看成一个**环形数组** + ```java + // 具体的扩容函数 + private void resize() { + } + ``` @@ -19683,29 +19736,133 @@ static class Entry extends WeakReference> { -##### 内存泄漏 +#### 内存泄漏 Memory leak:内存泄漏是指程序中动态分配的堆内存由于某种原因未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果,内存泄漏的堆积终将导致内存溢出 * 如果key使用强引用: - 使用完ThreadLocal ,threadLocal Ref被回收,但是因为threadLocalMap的Entry强引用了threadLocal,造成threadLocal无法被回收,无法完全避免内存泄漏 + 使用完 ThreadLocal ,threadLocal Ref 被回收,但是 threadLocalMap 的 Entry 强引用了 threadLocal,造成 threadLocal 无法被回收,无法完全避免内存泄漏 * 如果key使用弱引用: - 使用完ThreadLocal ,threadLocal Ref被回收,ThreadLocalMap只持有ThreadLocal的弱引用,所以threadlocal也可以被gc回收,此时Entry中的key=null。但没有手动删除这个Entry以及CurrentThread依然运行,依然存在强引用链,value不会被回收,而这块value永远不会被访问到,导致value内存泄漏 + 使用完 ThreadLocal ,threadLocal Ref 被回收,ThreadLocalMap 只持有 ThreadLocal 的弱引用,所以threadlocal 也可以被回收,此时Entry中的 key=null。但没有手动删除这个Entry或者 CurrentThread 依然运行,依然存在强引用链,value不会被回收,而这块value永远不会被访问到,导致value内存泄漏 * 两个主要原因: - * 没有手动删除这个Entry - * CurrentThread依然运行 + * 没有手动删除这个 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++; + } + } + } +} +``` + -根本原因:ThreadLocalMap是Thread的一个属性,生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏 -使用弱引用的原因:在ThreadLocalMap中的set/getEntry方法中,会对key为null(ThreadLocal为null)进行判断,如果为null的话,那么会对Entry进行垃圾回收。所以**弱引用比强引用多一层保障**,就算不调用remove,也有机会进行GC。 +参考文章:https://blog.csdn.net/feichitianxia/article/details/110495764 diff --git a/Tool.md b/Tool.md index 5ec014f..b90bb7e 100644 --- a/Tool.md +++ b/Tool.md @@ -141,6 +141,7 @@ GitLab (地址: https://about.gitlab.com/ )是一个用于仓库管理系 | git status | 查看 git 状态 (文件是否进行了添加、提交操作) | | git add filename | 添加,将指定文件添加到暂存区 | | git commit -m 'message' | 提交,将暂存区文件提交到本地仓库,删除暂存区的该文件 | +| git commit --amend | 修改 commit 的 message | | git rm filename | 删除,删除工作区的文件,不是仓库,需要提交 | | git mv filename | 移动或重命名工作区文件 | | git reset filename | 使用当前分支上的修改覆盖暂存区,**将暂存区的文件取消暂存** | @@ -155,6 +156,10 @@ GitLab (地址: https://about.gitlab.com/ )是一个用于仓库管理系 +*** + + + ### 文件状态 * Git工作目录下的文件存在两种状态: @@ -169,11 +174,17 @@ GitLab (地址: https://about.gitlab.com/ )是一个用于仓库管理系 * git status 查看文件状态 * git status –s 查看更简洁的文件状态 + + +*** + + + ### 文件忽略 一般我们总会有些文件无需纳入Git 的管理,也不希望它们总出现在未跟踪文件列表。 通常都是些自动生成的文件,比如日志文件,或者编译过程中创建的临时文件等。 在这种情况下,我们可以在工作目录中创建一个名为 .gitignore 的文件(文件名称固定),列出要忽略的文件模式。下面是一个示例: -``` +```sh # no .a files *.a # but do track lib.a, even though you're ignoring .a files above @@ -544,13 +555,17 @@ Linux 文件系统目录结构和熟知的 windows 系统有较大区别,没 + + *** -## 静态IP +## 远程连接 + +### 设置IP -### NAT模式设置 +#### NAT 首先设置虚拟机中NAT模式的选项,打开VMware,点击“编辑”下的“虚拟网络编辑器”,设置NAT参数 ![](https://gitee.com/seazean/images/raw/master/Tool/配置NAT.jpg) @@ -559,15 +574,16 @@ Linux 文件系统目录结构和熟知的 windows 系统有较大区别,没 ​ ![](https://gitee.com/seazean/images/raw/master/Tool/本地主机网络连接.jpg) -### 设置静态IP +#### 静态IP 在普通用户下不能修改网卡的配置信息;所以我们要切换到root用户进行ip配置:su root/su * 修改网卡配置文件: - vi /etc/sysconfig/network-scripts/ifcfg-ens33 + vi /etc/sysconfig/network-scripts/ifcfg-ens33 或者命令前加sudo * 修改文件内容 + ``` TYPE=Ethernet PROXY_METHOD=none @@ -616,7 +632,7 @@ Linux 文件系统目录结构和熟知的 windows 系统有较大区别,没 -## 远程登陆 +### 远程登陆 **服务器维护工作** 都是在 远程 通过SSH客户端 来完成的, 并没有图形界面, 所有的维护工作都需要通过命令来完成,Linux 服务器需要安装SSH 相关服务。 首先执行 sudo apt-get install openssh-server 指令。接下来用 xshell 连接。 @@ -627,19 +643,6 @@ Linux 文件系统目录结构和熟知的 windows 系统有较大区别,没 -*** - - - -## 命令帮助 - -在控制台输入:命令名 -h/ -help/ --h /空 - -可以看到命令的帮助文档 - -**man** [指令名称] 查看帮助文档 -比如 man ls。退出方式 q - *** @@ -840,6 +843,21 @@ gpasswd 是 Linux 工作组文件 /etc/group 和 /etc/gshadow 管理工具,用 ## 系统管理 +### man + +在控制台输入:命令名 -h/ -help/ --h /空 + +可以看到命令的帮助文档 + +**man** [指令名称] 查看帮助文档 +比如 man ls。退出方式 q + + + +*** + + + ### date date 可以用来显示或设定系统的日期与时间 @@ -858,6 +876,10 @@ date 可以用来显示或设定系统的日期与时间 +*** + + + ### id id会显示用户以及所属群组的实际与有效ID。若两个ID相同,则仅显示实际ID。若仅指定用户名称,则显示目前用户的ID。 @@ -874,6 +896,10 @@ id会显示用户以及所属群组的实际与有效ID。若两个ID相同, +*** + + + ### sudo sudo:控制用户对系统命令的使用权限,root允许的操作。通过sudo可以提高普通用户的操作权限 @@ -893,6 +919,10 @@ sudo:控制用户对系统命令的使用权限,root允许的操作。通过su +*** + + + ### top top:用于实时显示 process 的动态 @@ -925,6 +955,10 @@ top:用于实时显示 process 的动态 +*** + + + ### ps Linux 系统中查看进程使用情况的命令是 **ps** 指令 @@ -956,6 +990,10 @@ Linux 系统中查看进程使用情况的命令是 **ps** 指令 +*** + + + ### kill Linux kill命令用于删除执行中的程序或工作(可强制中断) @@ -978,6 +1016,8 @@ Linux kill命令用于删除执行中的程序或工作(可强制中断) +*** + ### shutdown @@ -1007,6 +1047,10 @@ shutdown命令可以用来进行关闭系统,并且在关机以前传送讯息 +*** + + + ### reboot reboot命令用于用来重新启动计算机 @@ -1021,6 +1065,10 @@ reboot命令用于用来重新启动计算机 +*** + + + ### who who命令用于显示系统中有哪些使用者正在上面,显示的资料包含了使用者 ID、使用的终端机、上线时间、呆滞时间、CPU 使用量、动作等等 @@ -1038,6 +1086,10 @@ who命令用于显示系统中有哪些使用者正在上面,显示的资料 +*** + + + ### systemctl 命令:systemctl [command] [unit] @@ -1071,6 +1123,8 @@ who命令用于显示系统中有哪些使用者正在上面,显示的资料 +*** + ### timedatectl @@ -1095,6 +1149,10 @@ NTP即Network Time Protocol(网络时间协议),是一个互联网协议 +*** + + + ### clear clear命令用于清除屏幕 @@ -1103,6 +1161,10 @@ clear命令用于清除屏幕 +**** + + + ### exit exit命令用于退出目前的shell。执行exit可使shell以指定的状态值退出。若不设置状态值参数,则shell以预设值退出。状态值0代表执行成功,其他值代表执行失败。exit也可用在script,离开正在执行的script,回到shell。 @@ -1123,6 +1185,8 @@ exit命令用于退出目前的shell。执行exit可使shell以指定的状态 + + *** @@ -2208,6 +2272,10 @@ pid_t waitpid(pid_t pid, int *status, int options) +*** + + + ### ifconfig ifconfig是Linux中用于显示或配置网络设备的命令,英文全称是network interfaces configuring @@ -2231,6 +2299,10 @@ ifconfig [网络设备][down up -allmulti -arp -promisc][add<地址>][del<地址 +*** + + + ### ping ping命令用于检测主机。 @@ -2254,6 +2326,10 @@ ping [-dfnqrRv][-c<完成次数>][-i<间隔秒数>][-I<网络界面>][-l<前置 +*** + + + ### netstat netstat命令用于显示网络状态 From aa116596d66b2b1e29c42709e9899571ea7aa5a0 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 29 May 2021 19:25:40 +0800 Subject: [PATCH 029/242] Update Java Notes --- DB.md | 1090 +++++++++++++++++++++++++++++++++++++++++++------------ Java.md | 409 ++++++++++++++------- Tool.md | 65 +++- 3 files changed, 1177 insertions(+), 387 deletions(-) diff --git a/DB.md b/DB.md index 304a6db..18bbfb1 100644 --- a/DB.md +++ b/DB.md @@ -2294,7 +2294,7 @@ MySQL中还存在 binlog(二进制日志) 也可以记录写操作并用于数 #### MVCC -MVCC 全称 Multi-Version Concurrency Control,即多版本并发控制,是一种用来解决读写冲突的无锁并发控制 +MVCC 全称 Multi-Version Concurrency Control,即多版本并发控制,用来解决**读写冲突**的无锁并发控制 MVCC 处理读写请求,可以做到在发生读写请求冲突时不用加锁,这个读是指的快照读,而不是当前读 @@ -3773,8 +3773,6 @@ InnoDB中,聚簇索引是按照每张表的主键构造一颗B+树,同时叶 - - *** @@ -3810,7 +3808,7 @@ InnoDB 表是基于聚簇索引建立的,因此 InnoDB 的索引能提供一 MyISAM 的主键索引使用的是非聚簇索引,索引文件和数据文件是分离的,索引文件仅保存数据记录的**地址** * 主键索引B+树的节点存储了主键,辅助键索引B+树存储了辅助键,表数据存储在独立的地方,这两颗B+树的叶子节点都使用一个地址指向真正的表数据,对于表数据来说,这两个键没有任何差别。 -* 由于索引树是独立的,通过辅助索引检索无需访问主键的索引树 +* 由于索引树是独立的,通过辅助索引检索无需访问主键的索引树回表查询 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-聚簇索引和辅助索引检锁数据图.jpg) @@ -4320,11 +4318,12 @@ EXPLAIN SELECT * FROM table_1 WHERE id = 1; MySQL执行计划的局限: -* EXPLAIN不会告诉你关于触发器、存储过程的信息或用户自定义函数对查询的影响情况 -* EXPLAIN不考虑各种Cache -* EXPLAIN不能显示MySQL在执行查询时所作的优化工作,因为执行计划在执行查询之前生成 -* 部分统计信息是估算的,并非精确值 -* EXPALIN只能解释SELECT操作,其他操作要重写为SELECT后查看执行计划 +* EXPLAIN 不会告诉显示关于触发器、存储过程的信息或用户自定义函数对查询的影响情况 +* EXPLAIN 不考虑各种Cache +* EXPLAIN 不能显示MySQL在执行查询时所作的优化工作,因为执行计划在执行查询之前生成 +* EXPALIN 部分统计信息是估算的,并非精确值 +* EXPALIN 只能解释SELECT操作,其他操作要重写为SELECT后查看执行计划 +* 执行计划在优化器之后、执行器之前生成,然后执行器调用存储引擎检索数据 * 执行计划可以随着底层优化器输入的更改而更改。EXPLAIN PLAN 显示的是在解释语句时数据库将如何运行SQL语句,由于执行环境和 EXPLAIN PLAN 环境的不同,此计划可能与SQL语句实际的执行计划不同 环境准备: @@ -7546,7 +7545,7 @@ Redis (REmote DIctionary Server) :用 C 语言开发的一个开源的高性 特征: * 数据间没有必然的关联关系,**不存关系,只存数据** -* 数据存储在内存,存取速度快,解决了磁盘 IO 速度慢的问题 +* 数据**存储在内存**,存取速度快,解决了磁盘 IO 速度慢的问题 * 内部采用**单线程**机制进行工作 * 高性能,官方测试数据,50个并发执行100000 个请求,读的速度是110000 次/s,写的速度是81000次/s * 多数据类型支持 @@ -7714,7 +7713,7 @@ Redis (REmote DIctionary Server) :用 C 语言开发的一个开源的高性 #### 服务器 -* 设置服务器以守护进程的方式运行,开启后服务器控制台中将打印服务器运行信息(同日志内容相同): +* 设置服务器以守护进程的方式运行,关闭后服务器控制台中将打印服务器运行信息(同日志内容相同): ```sh daemonize yes|no @@ -7816,6 +7815,54 @@ dbfilename "dump-6379.rdb" +## 结构模型 + +Redis 基于 Reactor 模式开发了网络事件处理器,这个处理器被称为文件事件处理器(file event handler),这个文件事件处理器是单线程的,所以Redis叫做单线程的模型 + +文件事件处理器以单线程方式运行,但是使用 I/O 多路复用程序来监听多个套接字,既实现了高性能的网络通信模型,又很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接,保持了 Redis 单线程设计的简单性 + +工作原理: + +* 文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器 + +* 当被监听的套接字准备好执行连接应答 (accept)、读取 (read)、写入 (write)、关闭 (close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器会调用套接字之前关联好的事件处理器来处理事件 + +Redis单线程也能高效的原因: + +* 纯内存操作 + +* 核心是基于非阻塞的IO多路复用机制 + +* 底层使用C语言实现,C 语言实现的程序距离操作系统更近,执行速度相对会更快 + +* 单线程同时也避免了多线程的上下文频繁切换问题,预防了多线程可能产生的竞争问题 + +Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,因为这是 Redis 中的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络),Redis 的多线程只是在网络数据的读写这类耗时操作上使用了, 执行命令仍然是单线程顺序执行,因此不需要担心线程安全问题。 + +Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启需要修改 redis 配置文件 `redis.conf` : + +```sh +io-threads-do-reads yesCopy to clipboardErrorCopied +``` + +开启多线程后,还需要设置线程数,否则是不生效的。同样需要修改 redis 配置文件 : + +```sh +io-threads 4 #官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程 +``` + + + +参考文章:https://blog.csdn.net/xp_xpxp/article/details/100999825 + + + +*** + + + + + ## 基本指令 ### 操作指令 @@ -8449,6 +8496,269 @@ sorted_set类型:在set的存储结构基础上添加可排序字段,类似 +*** + + + +### Bitmaps + +#### 布隆过滤 + +##### 基本介绍 + +布隆过滤器:一种数据结构,是一个很长的二进制向量(位数组)和一系列随机映射函数(哈希函数),既然是二进制,每个空间存放的不是0就是1,但是初始默认值都是0 + + + +这种数据结构是高效且性能很好的,但缺点是具有一定的错误识别率和删除难度。并且,理论情况下,添加到集合中的元素越多,误报的可能性就越大 + + + +*** + + + +##### 工作流程 + +向布隆过滤器中添加一个元素key时,会通过多个hash函数得到多个哈希值,在位数组中把对应下标的值置为 1 + +![](https://gitee.com/seazean/images/raw/master/DB/Redis-布隆过滤器添加数据.png) + +布隆过滤器查询一个数据,是否在二进制的集合中,查询过程如下: + +- 通过 K 个哈希函数计算该数据,对应计算出的 K 个hash值 +- 通过 hash 值找到对应的二进制的数组下标 +- 判断方法:如果存在一处位置的二进制数据是0,那么该数据不存在。如果都是1,该数据存在集合中 + +布隆过滤器优缺点: + +* 优点: + * 二进制组成的数组,占用内存极少,并且插入和查询速度都足够快 + * 去重方便:当字符串第一次存储时对应的位数组下标设置为 1,当第二次存储相同字符串时,因为对应位置已设置为 1,所以很容易知道此值已经存在 +* 缺点: + * 随着数据的增加,误判率会增加:添加数据是通过计算数据的hash值,不同的字符串可能哈希出来的位置相同,导致无法确定到底是哪个数据存在,**这种情况可以适当增加位数组大小或者调整哈希函数** + * 无法删除数据:可能存在几个数据占据相同的位置,所以删除一位会导致很多数据失效 + +* 总结:**布隆过滤器判断某个元素存在,小概率会误判。如果判断某个元素不在,那这个元素一定不在** + + + +参考文章:https://www.cnblogs.com/ysocean/p/12594982.html + + + +*** + + + +##### Guava + +引入 Guava 的依赖: + +```xml + + com.google.guava + guava + 28.0-jre + +``` + +指定误判率为(0.01): + +```java +public static void main(String[] args) { + // 创建布隆过滤器对象 + BloomFilter filter = BloomFilter.create( + Funnels.integerFunnel(), + 1500, + 0.01); + // 判断指定元素是否存在 + System.out.println(filter.mightContain(1)); + System.out.println(filter.mightContain(2)); + // 将元素添加进布隆过滤器 + filter.put(1); + filter.put(2); + System.out.println(filter.mightContain(1)); + System.out.println(filter.mightContain(2)); +} +``` + + + +*** + + + +##### 实现布隆 + +```java +class MyBloomFilter { + //布隆过滤器容量 + private static final int DEFAULT_SIZE = 2 << 28; + //bit数组,用来存放key + private static BitSet bitSet = new BitSet(DEFAULT_SIZE); + //后面hash函数会用到,用来生成不同的hash值,随意设置 + private static final int[] ints = {1, 6, 16, 38, 58, 68}; + + //add方法,计算出key的hash值,并将对应下标置为true + public void add(Object key) { + Arrays.stream(ints).forEach(i -> bitSet.set(hash(key, i))); + } + + //判断key是否存在,true不一定说明key存在,但是false一定说明不存在 + public boolean isContain(Object key) { + boolean result = true; + for (int i : ints) { + //短路与,只要有一个bit位为false,则返回false + result = result && bitSet.get(hash(key, i)); + } + return result; + } + + //hash函数,借鉴了hashmap的扰动算法 + private int hash(Object key, int i) { + int h; + return key == null ? 0 : (i * (DEFAULT_SIZE - 1) & ((h = key.hashCode()) ^ (h >>> 16))); + } +} + +``` + + + +*** + + + +#### 基本操作 + +指令操作: + +* 获取指定 key 对应偏移量上的 bit 值 + + ```sh + getbit key offset + ``` + +* 设置指定 key 对应偏移量上的 bit 值,value 只能是1或0 + + ```sh + setbit key offset value + ``` + +* 对指定 key 按位进行交、并、非、异或操作,并将结果保存到 destKey 中 + + ```sh + bitop option destKey key1 [key2...] + ``` + + option:and 交、or 并、not 非、xor 异或 + +* 统计指定 key 中1的数量 + + ```sh + bitcount key [start end] + ``` + + + +*** + + + +#### 应用场景 + +- 解决Redis缓存穿透,判断给定数据是否存在, 防止缓存穿透 + + + +- 垃圾邮件过滤,对每一个发送邮件的地址进行判断是否在布隆的黑名单中,如果在就判断为垃圾邮件 + +- 爬虫去重,爬给定网址的时候对已经爬取过的 URL 去重 + +- 信息状态统计 + + + +*** + + + +### Hyper + +基数是数据集去重后元素个数,HyperLogLog 是用来做基数统计的,运用了LogLog的算法 + +```java +{1, 3, 5, 7, 5, 7, 8} 基数集: {1, 3, 5 ,7, 8} 基数:5 +{1, 1, 1, 1, 1, 7, 1} 基数集: {1,7} 基数:2 +``` + +相关指令: + +* 添加数据 + + ```sh + pfadd key element [element ...] + ``` + +* 统计数据 + + ```sh + pfcount key [key ...] + ``` + +* 合并数据 + + ```sh + pfmerge destkey sourcekey [sourcekey...] + ``` + +应用场景: + +* 用于进行基数统计,不是集合不保存数据,只记录数量而不是具体数据 +* 核心是基数估算算法,最终数值存在一定误差 +* 误差范围:基数估计的结果是一个带有 0.81% 标准错误的近似值 +* 耗空间极小,每个 hyperloglog key 占用了12K的内存用于标记基数 +* pfadd 命令不是一次性分配12K内存使用,会随着基数的增加内存逐渐增大 +* Pfmerge 命令合并后占用的存储空间为12K,无论合并之前数据量多少 + + + +*** + + + +### GEO + +GeoHash是一种地址编码方法,把二维的空间经纬度数据编码成一个字符串 + +* 添加坐标点 + + ```sh + geoadd key longitude latitude member [longitude latitude member ...] + georadius key longitude latitude radius m|km|ft|mi [withcoord] [withdist] [withhash] [count count] + ``` + +* 获取坐标点 + + ```sh + geopos key member [member ...] + georadiusbymember key member radius m|km|ft|mi [withcoord] [withdist] [withhash] [count count] + ``` + +* 计算距离 + + ```sh + geodist key member1 member2 [unit] #计算坐标点距离 + geohash key member [member ...] #计算经纬度 + ``` + +redis 应用于地理位置计算 + + + + + **** @@ -8599,7 +8909,7 @@ Redis Desktop Manager 持久化:利用永久性存储介质将数据进行保存,在特定的时间将保存的数据进行恢复的工作机制称为持久化 -作用:持久化用于防止数据的意外丢失,确保数据安全性 +作用:持久化用于防止数据的意外丢失,确保数据安全性,因为 Redis 是内存级,所以需要持久化到磁盘 计算机中的数据全部都是二进制,保存一组数据有两种方式 @@ -8938,9 +9248,139 @@ AOF重写规则: ### fork -(待整理) +#### 函数介绍 + +fork() 函数创建一个子进程,子进程与父进程几乎是完全相同的进程,系统先给子进程分配资源,然后把原来的进程的所有数据都复制到子进程中,只有少数值与父进程的值不同,相当于克隆了一个进程 + +在完成对其调用之后,会产生2个进程,且每个进程都会**从fork()的返回处开始执行**,这两个进程将执行相同的程序段,但是拥有各自不同的堆段,栈段,数据段,每个子进程都可修改各自的数据段,堆段,和栈段 + +```c +#include +pid_t fork(void); +// 父进程返回子进程的pid,子进程返回0,错误返回负值 +``` + +fork 调用一次,却能够**返回两次**,可能有三种不同的返回值: + +* 在父进程中,fork返回新创建子进程的进程ID +* 在子进程中,fork返回0 +* 如果出现错误,fork返回一个负值,错误原因: + * 当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN + * 系统内存不足,这时errno的值被设置为ENOMEM + +fpid 的值在父子进程中不同:进程形成了链表,父进程的 fpid 指向子进程的进程 id,因为子进程没有子进程,所以其 fpid 为0 + +创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略 -fork函数讲解文章:https://blog.csdn.net/love_gaohz/article/details/41727415 +每个进程都有一个独特(互不相同)的进程标识符 process ID,可以通过 getpid() 函数获得;还有一个记录父进程 pid 的变量,可以通过 getppid() 函数获得变量的值 + + + +*** + + + +#### 函数使用 + +基本使用: + +```c +#include +#include +int main () +{ + pid_t fpid; // fpid表示fork函数返回的值 + int count=0; + fpid=fork(); + if (fpid < 0) + printf("error in fork!"); + else if (fpid == 0) { + printf("i am the child process, my process id is %d/n", getpid()); + count++; + } + else { + printf("i am the parent process, my process id is %d/n", getpid()); + count++; + } + printf("count: %d/n",count);// 1 + return 0; +} +/*输出内容: + i am the child process, my process id is 5574 + count: 1 + i am the parent process, my process id is 5573 + count: 1 +*/ +``` + +进阶使用: + +```c +#include +#include +int main(void) +{ + int i=0; + // ppid 指当前进程的父进程pid + // pid 指当前进程的pid, + // fpid 指fork返回给当前进程的值,在这可以表示子进程 + for(i=0; i<2; i++){ + pid_t fpid = fork(); + if(fpid == 0) + printf("%d child %4d %4d %4d/n",i,getppid(),getpid(),fpid); + else + printf("%d parent %4d %4d %4d/n",i,getppid(),getpid(),fpid); + } + return 0; +} +/*输出内容: + i 父id id 子id + 0 parent 2043 3224 3225 + 0 child 3224 3225 0 + 1 parent 2043 3224 3226 + 1 parent 3224 3225 3227 + 1 child 1 3227 0 + 1 child 1 3226 0 +*/ +``` + + + +在 p3224 和 p3225 执行完第二个循环后,main函数退出,进程死亡。所以 p3226,p3227 就没有父进程了,成为孤儿进程,所以 p3226 和 p3227 的父进程就被置为 ID 为 1的 init 进程(笔记 Tool -> Linux -> 进程管理详解) + +参考文章:https://blog.csdn.net/love_gaohz/article/details/41727415 + + + +*** + + + +#### 内存关系 + +fork()调用之后父子进程的内存关系 + +早期 Linux 的fork()实现时,就是全部复制,这种方法效率太低,而且造成了很大的内存浪费,现在 Linux 实现采用了两种方法来规避这种浪费: + +* 父子进程的代码段是相同的,所以代码段是没必要复制的,只需内核将代码段标记为只读,父子进程就共享此代码段。fork() 之后在进程创建代码段时,子进程的进程级页表项都指向和父进程相同的物理页帧 + + + +* 对于父进程的数据段,堆段,栈段中的各页,由于父子进程要相互独立,采用**写时复制**的技术,来最大化的提高内存以及内核的利用率 + + 在fork之后两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,**两者的虚拟空间不同,但其对应的物理空间是同一个**。当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间,而代码段继续共享父进程的物理空间(两者的代码完全相同);而如果两者执行的代码不同,子进程的代码段也会分配单独的物理空间。 + + fork之后内核会将子进程放在队列的前面,让子进程先执行,以免父进程执行导致写时复制,而后子进程再执行,因无意义的复制而造成效率的下降 + + + +补充知识: + +vfork(虚拟内存fork virtual memory fork):调用 vfork() 父进程被挂起,子进程使用父进程的地址空间。不采用写时复制,如果子进程修改父地址空间的任何页面,这些修改过的页面对于恢复的父进程是可见的 + + + +参考文章:https://blog.csdn.net/Shreck66/article/details/47039937 @@ -9288,7 +9728,7 @@ TTL返回的值有三种情况:正数,-1,-2 ## 主从复制 -### 基本概述 +### 基本介绍 **三高**架构: @@ -9298,38 +9738,39 @@ TTL返回的值有三种情况:正数,-1,-2 - 高可用: - 可用性:应用服务在全年宕机的时间加在一起就是全年应用服务不可用的时间 - - 业界可用性目标**5个9,即99.999%**,即服务器年宕机时长低于315秒,约5.25分钟 + - 业界可用性目标5个9,即99.999%,即服务器年宕机时长低于315秒,约5.25分钟 + +主从复制: -**主从复制**: +* 概念:将master中的数据即时、有效的复制到slave中 +* 特征:一个master可以拥有多个slave,一个slave只对应一个master +* 职责:master和slave各自的职责不一样 -* **概念:将master中的数据即时、有效的复制到slave中** -* **特征**:一个master可以拥有多个slave,一个slave只对应一个master -* **职责**:master和slave各自的职责不一样 - * master: - * 写数据 - * 执行写操作时,将出现变化的数据自动同步到slave - * 读数据(可忽略) - * slave - * 读数据 - * 写数据(禁止) + master: + * **写数据**,执行写操作时,将出现变化的数据自动同步到slave + * 读数据(可忽略) + + slave + * **读数据** + * 写数据(禁止) 主从复制的作用: -- 读写分离:master写、slave读,提高服务器的读写负载能力 -- 负载均衡:基于主从结构,配合读写分离,由slave分担master负载,并根据需求的变化,改变slave的数 量,通过多个从节点分担数据读取负载,大大提高Redis服务器并发量与数据吞吐量 -- 故障恢复:当master出现问题时,由slave提供服务,实现快速的故障恢复 +- 读写分离:master 写、slave 读,提高服务器的读写负载能力 +- 负载均衡:基于主从结构,配合读写分离,由slave分担master负载,并根据需求的变化,改变slave的数量,通过多个从节点分担数据读取负载,大大提高Redis服务器并发量与数据吞吐量 +- 故障恢复:当 master 出现问题时,由 slave 提供服务,实现快速的故障恢复 - 数据冗余:实现数据热备份,是持久化之外的一种数据冗余方式 -- 高可用基石:基于主从复制,构建哨兵模式与集群,实现Redis的高可用方案 +- 高可用基石:基于主从复制,构建哨兵模式与集群,实现 Redis 的高可用方案 主从复制的应用场景: * 机器故障:硬盘故障、系统崩溃,造成数据丢失,对业务形成灾难性打击,基本上会放弃使用redis -* 容量瓶颈:内存不足,放弃使用redis +* 容量瓶颈:内存不足,放弃使用 redis -* 解决方案:为了避免单点Redis服务器故障,准备多台服务器,互相连通。将数据复制多个副本保存在不同的服务器上,连接在一起,并保证数据是同步的。即使有其中一台服务器宕机,其他服务器依然可以继续提供服务,实现Redis的高可用,同时实现数据冗余备份 +* 解决方案:为了避免单点 Redis 服务器故障,准备多台服务器,互相连通。将数据复制多个副本保存在不同的服务器上连接在一起,并保证数据是同步的。即使有其中一台服务器宕机,其他服务器依然可以继续提供服务,实现Redis的高可用,同时实现数据冗余备份 - ![](https://gitee.com/seazean/images/raw/master/DB/Redis-主从复制多台服务器连接方案.png) + @@ -9337,108 +9778,130 @@ TTL返回的值有三种情况:正数,-1,-2 +### 工作流程 + +主从复制过程大体可以分为3个阶段 +* 建立连接阶段(即准备阶段) +* 数据同步阶段 +* 命令传播阶段 -### 工作流程 +![](https://gitee.com/seazean/images/raw/master/DB/Redis-主从复制工作流程.png) + + + +*** + + + +### 建立连接 -#### 建立连接 +#### 建立流程 -建立slave到master的连接,使master能够识别slave,并保存slave端口号 +建立连接阶段:建立 slave 到 master 的连接,使 master 能够识别 slave,并保存 slave 端口号 流程如下: -1. 设置master的地址和端口,保存master信息 -2. 建立socket连接 -3. 发送ping命令(定时器任务) -4. 身份验证 -5. 发送slave端口信息 +1. 设置 master 的地址和端口,保存 master 信息 +2. 建立 socket 连接 +3. 发送 ping 命令(定时器任务) +4. 身份验证(可能没有) +5. 发送 slave 端口信息 6. 主从连接成功 -当前状态: +连接成功的状态: -* slave:保存master的地址与端口 +* slave:保存 master 的地址与端口 -* master:保存slave的端口 +* master:保存 slave 的端口 -主从之间创建了连接的socket +* 主从之间创建了连接的socket -* **master和slave互联** + + +*** + + + +#### 相关指令 + +* master和slave互联 方式一:客户端发送命令 - ```properties + ```sh slaveof masterip masterport ``` 方式二:服务器带参启动 - ```properties + ```sh redis-server --slaveof masterip masterport ``` 方式三:服务器配置(主流方式) - ```properties + ```sh slaveof masterip masterport ``` - * slave系统信息:info指令 + * slave 系统信息:info 指令 - ```properties + ```sh master_link_down_since_seconds masterhost & masterport ``` - * master系统信息: + * master 系统信息: - ```properties + ```sh uslave_listening_port(多个) ``` -* **主从断开连接** +* 主从断开连接 - 断开slave与master的连接,slave断开连接后,不会删除已有数据,只是不再接受master发送的数据 + 断开 slave 与 master 的连接,slave 断开连接后,不会删除已有数据,只是不再接受 master 发送的数据 slave客户端执行命令: - ```properties + ```sh slaveof no one ``` -* **授权访问** +* 授权访问 - 注意:master有服务端和客户端,slave也有服务端和客户端,不仅服务端之间可以发命令,客户端也可以 + master 有服务端和客户端,slave 也有服务端和客户端,不仅服务端之间可以发命令,客户端也可以 - master客户端发送命令设置密码 + master 客户端发送命令设置密码: - ```properties + ```sh requirepass password ``` - master配置文件设置密码 + master 配置文件设置密码: - ```properties + ```sh config set requirepass password config get requirepass ``` - slave客户端发送命令设置密码 + slave 客户端发送命令设置密码: - ```properties + ```sh auth password ``` - slave配置文件设置密码 + slave 配置文件设置密码: - ```properties + ```sh masterauth password ``` - slave启动服务器设置密码 + slave 启动服务器设置密码: - ```properties + ```sh redis-server –a password ``` @@ -9448,12 +9911,14 @@ TTL返回的值有三种情况:正数,-1,-2 -#### 数据同步 +### 数据同步 + +#### 同步流程 数据同步需求: -- 在slave初次连接master后,复制master中的所有数据到slave -- 将slave的数据库状态更新成master当前的数据库状态 +- 在 slave 初次连接 master 后,复制 master 中的所有数据到 slave +- 将 slave 的数据库状态更新成 master 当前的数据库状态 同步过程如下: @@ -9464,41 +9929,55 @@ TTL返回的值有三种情况:正数,-1,-2 5. 恢复部分同步数据 6. 数据同步工作完成 -当前状态: +同步完成的状态: -* slave:具有master端全部数据,包含RDB过程接收的数据 +* slave:具有 master 端全部数据,包含 RDB 过程接收的数据 -* master:保存slave当前数据同步的位置 +* master:保存 slave 当前数据同步的位置 -主从之间完成了数据克隆 +* 主从之间完成了数据克隆 -* **数据同步阶段master说明** - 1. master数据量巨大,数据同步阶段应避开流量高峰期,避免造成master阻塞,影响业务正常执行 - 2. 复制缓冲区大小设定不合理,会导致数据溢出。比如进行全量复制周期太长,进行部分复制时发现数据已经存在丢失的情况,必须进行第二次全量复制,致使slave陷入死循环状态 +*** + + + +#### 同步优化 + +* 数据同步阶段 master 说明 + + 1. master 数据量巨大,数据同步阶段应避开流量高峰期,避免造成 master 阻塞,影响业务正常执行 - ```properties + 2. 复制缓冲区大小设定不合理,会导致**数据溢出**。比如进行全量复制周期太长,进行部分复制时发现数据已经存在丢失的情况,必须进行第二次全量复制,致使slave陷入死循环状态 + + ```sh repl-backlog-size ?mb ``` + 建议设置如下: + + * 测算从 master 到 slave 的重连平均时长 second + * 获取 master 平均每秒产生写命令数据总量 write_size_per_second + * 最优复制缓冲区空间 = 2 * second * write_size_per_second + 3. master单机内存占用主机内存的比例不应过大,建议使用50%-70%的内存,留下30%-50%的内存用于执 行bgsave命令和创建复制缓冲区 -* **数据同步阶段slave说明** +* 数据同步阶段slave说明 1. 为避免slave进行全量复制、部分复制时服务器响应阻塞或数据不同步,建议关闭此期间的对外服务 - ```properties + ```sh slave-serve-stale-data yes|no ``` - 2. 数据同步阶段,master发送给slave信息可以理解master是slave的一个客户端,主动向slave发送命令 + 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间数据同步延迟较大,数据一致性变差,应谨慎选择 @@ -9506,13 +9985,15 @@ TTL返回的值有三种情况:正数,-1,-2 -#### 命令传播 +### 命令传播 + +#### 传播原理 -命令传播:当master数据库状态被修改后,导致主从服务器数据库状态不一致,此时需要让主从数据同步到一致的状态,同步的动作称为命令传播 +命令传播:当 master 数据库状态被修改后,导致主从服务器数据库状态不一致,此时需要让主从数据同步到一致的状态,同步的动作称为命令传播 -命令传播的过程:master将接收到的数据变更命令发送给slave,slave接收命令后执行命令 +命令传播的过程:master 将接收到的数据变更命令发送给 slave,slave 接收命令后执行命令 -命令传播阶段的部分复制:命令传播阶段出现了断网现象 +命令传播阶段出现了断网现象: * 网络闪断闪连:忽略 * 短时间网络中断:部分复制 @@ -9520,55 +10001,37 @@ TTL返回的值有三种情况:正数,-1,-2 部分复制的三个核心要素:服务器的运行 id(run id)、主服务器的复制积压缓冲区、主从服务器的复制偏移量 -* 服务器运行ID(runid) - - 概念:服务器运行ID是每一台服务器每次运行的身份识别码,一台服务器多次运行可以生成多个运行id - - 组成:运行id由40位字符组成,是一个随机的十六进制字符 - - 作用:运行id被用于在服务器间进行传输识别身份,如果想两次操作均对同一台服务器进行, - 必须每次操作携带对应的运行id,用于对方识别 +* 服务器运行ID(runid):服务器运行ID是每一台服务器每次运行的身份识别码,一台服务器多次运行可以生成多个运行id,由40位字符组成,是一个随机的十六进制字符 - 实现:运行id在每台服务器启动时自动生成,master在首次连接slave时,将自己的运行ID发送给slave, - slave保存此ID,通过info Server命令,可以查看节点的runid + 作用:用于在服务器间进行传输识别身份,如果想两次操作均对同一台服务器进行,必须每次操作携带对应的运行id,用于对方识别 -* 复制缓冲区 + 实现:运行id在每台服务器启动时自动生成,master 在首次连接 slave 时,将运行ID发送给 slave,slave保存此ID,通过 info Server 命令,可以查看节点的 runid - 概念:复制缓冲区,又名复制积压缓冲区,是一个先进先出(FIFO)的队列,用于存储服务器执行过的命令 +* 复制缓冲区:复制积压缓冲区,是一个先进先出(FIFO)的队列,用于存储服务器执行过的命令 - 作用:用于保存master收到的所有指令(仅影响数据变更的指令,例如set,select) + 作用:用于保存 master 收到的所有指令(仅影响数据变更的指令,例如 set,select) - 数据来源:当master接收到主客户端的指令时,除了将指令执行,会将该指令存储到缓冲区中 + 实现方式:每次传播命令,master 都会将传播的命令记录下来,并存储在复制缓冲区,复制缓冲区默认数据存储空间大小是 1M,当入队元素的数量大于队列长度时,最先入队的元素被弹出,新元素会被放入队列 - 实现方式:每次传播命令,master都会将传播的命令记录下来,并存储在复制缓冲区,复制缓冲区默认数据存储空间大小是**1M**,当入队元素的数量大于队列长度时,最先入队的元素被弹出,新元素会被放入队列 +* 复制偏移量:一个数字,描述复制缓冲区中的指令字节位置 - **组成**: - - * 偏移量 - - 概念:一个数字,描述复制缓冲区中的指令字节位置 - - 分类: - - - master复制偏移量:记录发送给所有slave的指令字节对应的位置(多个) - - slave复制偏移量:记录slave接收master发送过来的指令字节对应的位置(一个) - - 作用:同步信息,比对master与slave的差异,当slave断线后,恢复数据使用 - - 数据来源: + - master复制偏移量:记录发送给所有slave的指令字节对应的位置(多个) +- slave复制偏移量:记录slave接收master发送过来的指令字节对应的位置(一个) + + 作用:同步信息,比对 master 与 slave 的差异,当 slave 断线后,恢复数据使用 - - master端:发送一次记录一次 - - slave端:接收一次记录一次 + 数据来源: - * 字节值 + - master 端:发送一次记录一次 +- slave 端:接收一次记录一次 - **工作原理**: +**工作原理**: - - 通过offset区分不同的slave当前数据传播的差异 - - master记录已发送的信息对应的offset - - slave记录已接收的信息对应的offset +- 通过 offset 区分不同的 slave 当前数据传播的差异 +- master 记录已发送的信息对应的 offset +- slave 记录已接收的信息对应的 offset - + @@ -9592,41 +10055,125 @@ TTL返回的值有三种情况:正数,-1,-2 #### 心跳机制 -心跳机制:进入命令传播阶段,master与slave间需要信息交换,使用心跳机制维护,实现双方连接保持在线 +心跳机制:进入命令传播阶段,master 与 slave 间需要信息交换,使用心跳机制维护,实现双方连接保持在线 master心跳任务: - 内部指令:PING -- 周期:由repl-ping-slave-period决定,默认10秒 -- 作用:判断slave是否在线 -- 查询:INFO replication 获取slave最后一次连接时间间隔,lag项维持在0或1视为正常 +- 周期:由 `repl-ping-slave-period` 决定,默认10秒 +- 作用:判断 slave 是否在线 +- 查询:INFO replication 获取 slave 最后一次连接时间间隔,lag 项维持在0或1视为正常 slave心跳任务 - 内部指令:REPLCONF ACK {offset} - 周期:1秒 -- 作用1:汇报slave自己的复制偏移量,获取最新的数据变更指令 -- 作用2:判断master是否在线 +- 作用:汇报 slave 自己的复制偏移量,获取最新的数据变更指令;判断master是否在线 心跳阶段注意事项: -* 当slave多数掉线,或延迟过高时,master为保障数据稳定性,将拒绝所有信息同步 +* 当 slave 多数掉线,或延迟过高时,master 为保障数据稳定性,将拒绝所有信息同步 - slave数量少于2个,或者所有slave的延迟都大于等于8秒时,强制关闭master写功能,停止数据同步 + slave 数量少于2个,或者所有 slave 的延迟都大于等于8秒时,强制关闭 master 写功能,停止数据同步 - ```properties + ```sh min-slaves-to-write 2 min-slaves-max-lag 8 ``` -* slave数量由slave发送REPLCONF ACK命令做确认 +* slave 数量由 slave 发送 REPLCONF ACK 命令做确认 + + +- slave 延迟由 slave 发送 REPLCONF ACK 命令做确认 + + + +**** -- slave延迟由slave发送REPLCONF ACK命令做确认 -完整的主从复制流程: +### 常见问题 -![](https://gitee.com/seazean/images/raw/master/DB/Redis-主从复制完整流程.png) +#### 全量复制 + +系统不断运行,master 的数据量会越来越大,一旦 master 重启,runid 将发生变化,会导致全部 slave 的全量复制操作 + +解决方法:本机保存上次 runid,重启后恢复该值,使所有 slave 认为还是之前的 master + +优化方案: + +* master 内部创建 master_replid 变量,使用 runid 相同的策略生成,长度41位,并发送给所有 slave + +* 在master关闭时执行命令 `shutdown save`,进行RDB持久化,将 runid 与 offset 保存到RDB文件中 + + `redis-check-rdb dump.rdb` 命令可以查看该信息,保存为 repl-id 和 repl-offset + +* master 重启后加载 RDB 文件,恢复数据 + + 重启后,将RDB文件中保存的repl-id与repl-offset加载到内存中 + + * master_repl_id = repl-id,master_repl_offset = repl-offset + * 通过info命令可以查看该信息 + + + +*** + + + +#### 网络中断 + +master 的 CPU 占用过高或 slave 频繁断开连接 + +* 出现的原因: + * slave 每1秒发送 REPLCONF ACK 命令到 master + * 当slave接到了慢查询时(keys * ,hgetall等),会大量占用CPU性能 + * master每1秒调用复制定时函数replicationCron(),比对slave发现长时间没有进行响应 + + 最终导致 master 各种资源(输出缓冲区、带宽、连接等)被严重占用 + +* 解决方法:通过设置合理的超时时间,确认是否释放slave + + ```sh + repl-timeout # 该参数定义了超时时间的阈值(默认60秒),超过该值,释放slave + ``` + +slave 与 master 连接断开 + +* 出现的原因: + * master 发送 ping 指令频度较低 + * master 设定超时时间较短 + * ping 指令在网络中存在丢包 + +* 解决方法:提高ping指令发送的频度 + + ```sh + repl-ping-slave-period + ``` + + 超时时间 repl-time 的时间至少是 ping 指令频度的5到10倍,否则 slave 很容易判定超时 + + + +**** + + + +#### 缓存不一致 + +网络信息不同步,数据发送有延迟,导致多个 slave 获取相同数据不同步 + +解决方案: + +* 优化主从间的网络环境,通常放置在同一个机房部署,如使用阿里云等云服务器时要注意此现象 + +* 监控主从节点延迟(通过offset)判断,如果 slave 延迟过大,暂时屏蔽程序对该 slave 的数据访问 + + ```sh + slave-serve-stale-data yes|no + ``` + + 开启后仅响应info、slaveof等少数命令(慎用,除非对数据一致性要求很高) @@ -9640,22 +10187,20 @@ slave心跳任务 ### 哨兵概述 -引入:如果redis的master宕机了,需要从slave中重新选出一个master,要实现这些功能就需要redis的哨兵 +如果 redis 的 master 宕机了,需要从 slave 中重新选出一个 master,要实现这些功能就需要 redis 的哨兵 -哨兵(sentinel) 是一个分布式系统,用于对主从结构中的每台服务器进行**监控**,当出现故障时通过**投票机制选择**新的master并将所有slave连接到新的master +哨兵 (sentinel) 是一个分布式系统,用于对主从结构中的每台服务器进行**监控**,当出现故障时通过**投票机制选择**新的 master 并将所有 slave 连接到新的 master -![](https://gitee.com/seazean/images/raw/master/DB/Redis-哨兵模式.png) + 哨兵的作用: - 监控:监控master和slave,不断的检查master和slave是否正常运行,master存活检测、master与slave运行情况检测 - - - 通知 (提醒):当被监控的服务器出现问题时,向其他(哨兵间,客户端)发送通知 -- 自动故障转移:断开master与slave连接,选取一个slave作为master,将其他slave连接新的master,并告知客户端新的服务器地址 +- 自动故障转移:断开 master 与 slave 连接,选取一个 slave 作为 master,将其他 slave 连接新的 master,并告知客户端新的服务器地址 注意:哨兵也是一台redis服务器,只是不提供数据相关服务,通常哨兵的数量配置为单数(投票) @@ -9670,51 +10215,50 @@ slave心跳任务 配置哨兵: * 配置一拖二的主从结构 -* 配置三个哨兵(配置相同,端口不同),sentinel.conf - -```sh -port 26401 -dir "/redis/data" -sentinel monitor mymaster 127.0.0.1 6401 2 -sentinel down-after-milliseconds mymaster 5000 -sentinel failover-timeout mymaster 20000 -sentinel parallel-sync mymaster 1 -sentinel deny-scripts-reconfig yes -``` -配置说明: - -* 设置哨兵监听的主服务器信息, sentinel_number表示参与投票的哨兵数量 +* 配置三个哨兵(配置相同,端口不同),sentinel.conf - ```properties - sentinel monitor master_name master_host master_port sentinel_number + ```sh + port 26401 + dir "/redis/data" + sentinel monitor mymaster 127.0.0.1 6401 2 + sentinel down-after-milliseconds mymaster 5000 + sentinel failover-timeout mymaster 20000 + sentinel parallel-sync mymaster 1 + sentinel deny-scripts-reconfig yes ``` -* 设置判定服务器宕机时长,该设置控制是否进行主从切换 + 配置说明: - ```properties - sentinel down-after-milliseconds master_name million_seconds - ``` + * 设置哨兵监听的主服务器信息, sentinel_number 表示参与投票的哨兵数量 -* 设置故障切换的最大超时时间 + ```sh + sentinel monitor master_name master_host master_port sentinel_number + ``` - ```properties - sentinel failover-timeout master_name million_seconds - ``` + * 指定哨兵在监控Redis服务时,设置判定服务器宕机的时长,该设置控制是否进行主从切换 -* 设置主从切换后,同时进行数据同步的slave数量,数值越大,要求网络资源越高,数值越小,同步时间越长 + ```sh + sentinel down-after-milliseconds master_name million_seconds + ``` - ```properties - sentinel parallel-syncs master_name sync_slave_number - ``` + * 出现故障后,故障切换的最大超时时间,超过该值,认定切换失败,默认3分钟 + ```sh + sentinel failover-timeout master_name million_seconds + ``` + * 指定同时进行主从的slave数量,数值越大,要求网络资源越高,要求约小,同步时间约长 + + ```sh + sentinel parallel-syncs master_name sync_slave_number + ``` 启动哨兵: * 服务端命令(Linux命令): - ```properties + ```sh redis-sentinel filename ``` @@ -9724,7 +10268,7 @@ sentinel deny-scripts-reconfig yes -### 哨兵原理 +### 工作原理 #### 三个阶段 @@ -9736,7 +10280,7 @@ sentinel deny-scripts-reconfig yes -#### 监控 +#### 监控阶段 作用:同步各个节点的状态信息 @@ -9764,6 +10308,10 @@ sentinel deny-scripts-reconfig yes 内部的工作原理: +sentinel 1首先连接 master,建立 cmd 通道,根据主节点访问从节点,连接完成 + +sentinel 2 首先连接 master,然后通过 master 中的 sentinels 发现其他哨兵,然后寻找哨兵建立连接,同步数据 + @@ -9772,9 +10320,9 @@ sentinel deny-scripts-reconfig yes -#### 通知 +#### 通知阶段 -sentinel在通知阶段要不断的去获取master/slave的信息,然后在各个sentinel之间进行共享,具体的流程如下: +sentinel 在通知阶段不断的去获取 master/slave 的信息,然后在各个 sentinel 之间进行共享,流程如下: ![](https://gitee.com/seazean/images/raw/master/DB/Redis-哨兵模式通知工作流程.png) @@ -9790,37 +10338,31 @@ sentinel在通知阶段要不断的去获取master/slave的信息,然后在各 * 检测master - sentinel1检测到master下线后会做flag:SRI_S_DOWN标志,此时master的状态是主观下线,并通知其他哨兵,其他哨兵也会尝试与master连接,如果大于 (n/2) + 1 个sentinel检测到master下线,就达成共识更改flag,此时master的状态是客观下线 + sentinel1 检测到 master 下线后会做 flag:SRI_S_DOWN 标志,此时 master 的状态是主观下线,并通知其他哨兵,其他哨兵也会尝试与 master 连接,如果大于 (n/2) + 1 个sentinel检测到master下线,就达成共识更改flag,此时master的状态是客观下线 ![](https://gitee.com/seazean/images/raw/master/DB/Redis-哨兵模式故障转移工作流程1.png) -* 当sentinel认定master下线之后,此时需要决定更换master,选举某个sentinel处理事故 +* 当 sentinel 认定 master 下线之后,此时需要决定更换 master,选举某个 sentinel 处理事故 在选举的时候每一个sentinel都有一票,于是每个sentinel都会发出一个指令,在内网里广播我要做话事人;比如sentinel1和sentinel4发出这个选举指令了,那么sentinel2既能接到sentinel1的也能接到sentinel4的,sentinel2会把一票投给其中一方,投给指令最先到达的sentinel;现在sentinel1就拿到了一票,按照这样的一种形式,最终会有一个选举结果,对应的选举最终得票多的,那自然就成为了处理事故的人。需要注意在这个过程中有可能会存在失败的现象,就是一轮选举完没有选取,那就会接着进行第二轮第三轮直到完成选举。 ![](https://gitee.com/seazean/images/raw/master/DB/Redis-哨兵模式故障转移工作流程2.png) -* 选择新的master - - 在服务器列表中挑选备选master的原则: - - - 不在线的OUT +选择新的master,在服务器列表中挑选备选master的原则: +- 不在线的OUT - 响应慢的OUT - - 与原master断开时间久的OUT - - 优先原则:优先级 --> offset --> runid +选出新的master之后,发送指令( sentinel )给其他的slave -* 选出新的master之后,发送指令( sentinel )给其他的slave - * 向新的master发送slaveof no one +* 向新的master发送slaveof no one - - - 向其他slave发送slaveof 新masterIP端口 + * 向其他slave发送slaveof 新masterIP端口 @@ -9874,11 +10416,9 @@ sentinel在通知阶段要不断的去获取master/slave的信息,然后在各 - 一次命中,直接返回 - 一次未命中,告知具体位置,最多两次命中 -设置数据: - -* 系统默认存储到某一个 +设置数据:系统默认存储到某一个 -![](https://gitee.com/seazean/images/raw/master/DB/Redis-集群查找数据.png) + @@ -10035,9 +10575,84 @@ sentinel在通知阶段要不断的去获取master/slave的信息,然后在各 -## 企业级方案 +## 缓存方案 + +### 缓存模式 + +#### 旁路缓存 + +旁路缓存模式 Cache Aside Pattern 是平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景 + +Cache Aside Pattern 中服务端需要同时维系 DB 和 cache,并且是以 DB 的结果为准 + +* 写操作:先更新 DB,然后直接删除 cache +* 读操作:从 cache 中读取数据,读取到就直接返回;读取不到就从 DB 中读取数据返回,并放到 cache + +数据库和缓存的顺序问题: + +* 在写数据的过程中,不能先删除 cache 再更新 DB,因为会造成缓存的不一致。比如请求1先写数据A,请求2随后读数据A,当请求1删除 cache 后,请求2直接读取了 DB,此时请求1还没写入 DB + +* 在写数据的过程中,先更新 DB 再删除 cache 也会出现问题,但是概率很小,因为缓存的写入速度非常快 + +旁路缓存的缺点: + +* 首次请求数据一定不在 cache 的问题,一般采用缓存预热的方法,将热点数据可以提前放入cache 中 + +* 写操作比较频繁的话导致 cache 中的数据会被频繁被删除,影响缓存命中率 + + 数据库和缓存数据强一致场景 :更新DB的时候同样更新 cache,不过需要加一个锁来保证更新 cache 时不存在线程安全问题,这样可以增加命中率 + + 可以短暂地允许数据库和缓存数据不一致场景 :更新DB的时候同样更新 cache,但是给缓存加一个比较短的过期时间,这样的话就可以保证即使数据不一致影响也比较小 + + + +**** + + + +#### 读写穿透 + +读写穿透模式 Read/Write Through Pattern:服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中,cache 负责将此数据读取和写入 DB,从而减轻了应用程序的职责 + +* 写操作:先查 cache,cache 中不存在,直接更新 DB;cache 中存在则先更新 cache,然后 cache 服务更新 DB(同步更新 cache 和 DB) + +* 读操作:从 cache 中读取数据,读取到就直接返回 ;读取不到先从 DB 加载,写入到 cache 后返回响应 + + Read-Through Pattern 实际只是在 Cache-Aside Pattern 之上进行了封装。在 Cache-Aside Pattern 下,发生读请求的时候,如果 cache 中不存在对应的数据,是由客户端负责把数据写入 cache,而 Read Through Pattern 则是 cache 服务自己来写入缓存的,对客户端是透明的 + +Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解决 + + + +*** + + + +#### 异步缓存 + +异步缓存写入 Write Behind Pattern 由 cache 服务来负责 cache 和 DB 的读写,对比读写穿透不同的是 Write Behind Caching 是只更新缓存,不直接更新 DB,改为异步批量的方式来更新 DB + +缺点:这种模式对数据一致性没有高要求,可能出现 cache 还没异步更新DB,服务就挂掉了 + +应用: + +* DB 的写性能非常高,适合一些数据经常变化又对数据一致性要求不高的场景,比如浏览量、点赞量 -### 缓存预热 +* MySQL 的 InnoDB Buffer Pool 机制用到了这种策略 + + + +参考文章:https://snailclimb.gitee.io/javaguide + + + +**** + + + +### 企业方案 + +#### 缓存预热 场景:宕机,服务器启动后迅速宕机 @@ -10053,7 +10668,7 @@ sentinel在通知阶段要不断的去获取master/slave的信息,然后在各 1. 日常例行统计数据访问记录,统计访问频度较高的热点数据 - 2. 利用LRU数据删除策略,构建数据留存队列例如:storm与kafka配合 + 2. 利用 LRU 数据删除策略,构建数据留存队列例如:storm与kafka配合 - 准备工作: @@ -10069,7 +10684,7 @@ sentinel在通知阶段要不断的去获取master/slave的信息,然后在各 5. 如果条件允许,使用了CDN(内容分发网络),效果会更好 -**总的来说**:缓存预热就是系统启动前,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据! +总的来说:缓存预热就是系统启动前,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据! @@ -10077,7 +10692,7 @@ sentinel在通知阶段要不断的去获取master/slave的信息,然后在各 -### 缓存雪崩 +#### 缓存雪崩 场景:数据库服务器崩溃,一连串的问题会随之而来。系统平稳运行过程中,忽然数据库连接量激增,应用服务器无法及时处理请求,大量408,500错误页面出现,客户反复刷新页面获取数据,造成:数据库崩溃、应用服务器崩溃、重启应用服务器无效、Redis服务器崩溃、Redis集群崩溃、重启数据库后再次被瞬间流量放倒 @@ -10089,29 +10704,28 @@ sentinel在通知阶段要不断的去获取master/slave的信息,然后在各 1. 更多的页面静态化处理 - 2. 构建多级缓存架构:Nginx缓存+redis缓存+ehcache缓存 + 2. 构建**多级缓存**架构:Nginx 缓存 + redis 缓存 + ehcache 缓存 - 3. 检测Mysql严重耗时业务进行优化:对数据库的瓶颈排查:例如超时查询、耗时较高事务等 + 3. 检测 Mysql 严重耗时业务进行优化:对数据库的瓶颈排查:例如超时查询、耗时较高事务等 - 4. 灾难预警机制:监控redis服务器性能指标,CPU占用、CPU使用率、内存容量、查询平均响应时间、线程数 + 4. 灾难预警机制:监控redis服务器性能指标,CPU占用、CPU使用率、内存容量、平均响应时间、线程数 5. 限流、降级:短时间范围内牺牲一些客户体验,限制一部分请求访问,降低应用服务器压力,待业务低速运转后再逐步放开访问 * 实践: - 1. LRU与LFU切换 + 1. LRU 与 LFU切换 - 2. 数据有效期策略调整:根据业务数据有效期进行分类错峰,A类90分钟,B类80分钟,C类70分钟,过期时间使用固定时间+随机值的形式,稀释集中到期的key的数量 + 2. 数据有效期策略调整:根据业务数据有效期进行分类错峰,A类90分钟,B类80分钟,C类70分钟,过期时间使用固定时间 + 随机值的形式,稀释集中到期的key的数量 3. 超热数据使用永久key - 4. 定期维护 (自动+人工):对即将过期数据做访问量分析,确认是否延时,配合访问量统计,做热点数据的延时 + 4. 定期维护:对即将过期数据做访问量分析,确认是否延时,配合访问量统计,做热点数据的延时 5. 加锁:慎用 -* -**总的来说**:缓存雪崩就是瞬间过期数据量太大,导致对数据库服务器造成压力。如能够有效避免过期时间集中,可以有效解决雪崩现象的出现(约40%),配合其他策略一起使用,并监控服务器的运行数据,根据运行记录做快速调整。 +总的来说:缓存雪崩就是瞬间过期数据量太大,导致对数据库服务器造成压力。如能够有效避免过期时间集中,可以有效解决雪崩现象的出现(约40%),配合其他策略一起使用,并监控服务器的运行数据,根据运行记录做快速调整。 @@ -10119,7 +10733,7 @@ sentinel在通知阶段要不断的去获取master/slave的信息,然后在各 -### 缓存击穿 +#### 缓存击穿 场景:系统平稳运行过程中,数据库连接量瞬间激增,Redis服务器无大量key过期,Redis内存平稳,无波动,Redis服务器CPU正常,但是数据库崩溃 @@ -10127,14 +10741,12 @@ sentinel在通知阶段要不断的去获取master/slave的信息,然后在各 1. Redis中某个key过期,该key访问量巨大 -2. 多个数据请求从服务器直接压到Redis后,均未命中 +2. 多个数据请求从服务器直接压到 Redis 后,均未命中 3. Redis在短时间内发起了大量对数据库中同一数据的访问 简而言之两点:单个key高热数据,key过期 - - 解决方案: 1. 预先设定:以电商为例,每个商家根据店铺等级,指定若干款主打商品,在购物节期间,加大此类信息key的过期时长 注意:购物节不仅仅指当天,以及后续若干天,访问峰值呈现逐渐降低的趋势 @@ -10147,7 +10759,7 @@ sentinel在通知阶段要不断的去获取master/slave的信息,然后在各 5. 加锁:分布式锁,防止被击穿,但是要注意也是性能瓶颈,慎重! -**总的来说**:缓存击穿就是单个高热数据过期的瞬间,数据访问量较大,未命中redis后,发起了大量对同一数据的数据库访问,导致对数据库服务器造成压力。应对策略应该在业务数据分析与预防方面进行,配合运行监控测试与即时调整策略,毕竟单个key的过期监控难度较高,配合雪崩处理策略即可。 +总的来说:缓存击穿就是单个高热数据过期的瞬间,数据访问量较大,未命中redis后,发起了大量对同一数据的数据库访问,导致对数据库服务器造成压力。应对策略应该在业务数据分析与预防方面进行,配合运行监控测试与即时调整策略,毕竟单个key的过期监控难度较高,配合雪崩处理策略即可 @@ -10155,7 +10767,7 @@ sentinel在通知阶段要不断的去获取master/slave的信息,然后在各 -### 缓存穿透 +#### 缓存穿透 场景:系统平稳运行过程中,应用服务器流量随时间增量较大,Redis服务器命中率随时间逐步降低,Redis内存平稳,内存无压力,Redis服务器CPU占用激增,数据库服务器压力激增,数据库崩溃 @@ -10187,9 +10799,9 @@ sentinel在通知阶段要不断的去获取master/slave的信息,然后在各 4. key加密:临时启动防灾业务key,对key进行业务层传输加密服务,设定校验程序,过来的key校验;例如每天随机分配60个加密串,挑选2到3个,混淆到页面数据id中,发现访问key不满足规则,驳回数据访问 -**总的来说**:缓存击穿是指访问了不存在的数据,跳过了合法数据的redis数据缓存阶段,每次访问数据库,导致对数据库服务器造成压力。通常此类数据的出现量是一个较低的值,当出现此类情况以毒攻毒,并及时报警。无论是黑名单还是白名单,都是对整体系统的压力,警报解除后尽快移除 +总的来说:缓存击穿是指访问了不存在的数据,跳过了合法数据的 redis 数据缓存阶段,每次访问数据库,导致对数据库服务器造成压力。通常此类数据的出现量是一个较低的值,当出现此类情况以毒攻毒,并及时报警。无论是黑名单还是白名单,都是对整体系统的压力,警报解除后尽快移除 -https://www.bilibili.com/video/BV15y4y1r7X3 +参考视频:https://www.bilibili.com/video/BV15y4y1r7X3 @@ -10207,19 +10819,19 @@ redis中的监控指标如下: 响应请求的平均时间: - ```properties + ```sh latency ``` 平均每秒处理请求总数: - ```properties + ```sh instantaneous_ops_per_sec ``` 缓存查询命中率(通过查询总次数与查询得到非nil数据总次数计算而来): - ```properties + ```sh hit_rate(calculated) ``` @@ -10227,25 +10839,25 @@ redis中的监控指标如下: 当前内存使用量: - ```properties + ```sh used_memory ``` 内存碎片率(关系到是否进行碎片整理): - ```properties + ```sh mem_fragmentation_ratio ``` 为避免内存溢出删除的key的总数量: - ```properties + ```sh evicted_keys ``` 基于阻塞操作(BLPOP等)影响的客户端数量: - ```properties + ```sh blocked_clients ``` @@ -10253,25 +10865,25 @@ redis中的监控指标如下: 当前客户端连接总数: - ```properties + ```sh connected_clients ``` 当前连接slave总数: - ```properties + ```sh connected_slaves ``` 最后一次主从信息交换距现在的秒: - ```properties + ```sh master_last_io_seconds_ago ``` key的总数: - ```properties + ```sh keyspace ``` @@ -10279,13 +10891,13 @@ redis中的监控指标如下: 当前服务器其最后一次RDB持久化的时间: - ```properties + ```sh rdb_last_save_time ``` 当前服务器最后一次RDB持久化后数据变化总量: - ```properties + ```sh rdb_changes_since_last_save ``` @@ -10293,26 +10905,22 @@ redis中的监控指标如下: 被拒绝连接的客户端总数(基于达到最大连接值的因素): - ```properties + ```sh rejected_connections ``` key未命中的总次数: - ```properties + ```sh keyspace_misses ``` 主从断开的秒数: - ```properties + ```sh master_link_down_since_seconds ``` - - - - 要对redis的相关指标进行监控,我们可以采用一些用具: - CloudInsight Redis @@ -10328,13 +10936,13 @@ redis中的监控指标如下: 测试当前服务器的并发性能: - ```properties + ```sh redis-benchmark [-h ] [-p ] [-c ] [-n [-k ] ``` 范例:100个连接,5000次请求对应的性能 - ```properties + ```sh redis-benchmark -c 100 -n 5000 ``` @@ -10344,13 +10952,13 @@ redis中的监控指标如下: monitor:启动服务器调试信息 - ```properties + ```sh monitor ``` slowlog:慢日志 - ```properties + ```sh slowlog [operator] #获取慢查询日志 ``` @@ -10360,7 +10968,7 @@ redis中的监控指标如下: 相关配置: - ```properties + ```sh slowlog-log-slower-than 1000 #设置慢查询的时间下线,单位:微妙 slowlog-max-len 100 #设置慢查询命令对应的日志显示长度,单位:命令数 ``` diff --git a/Java.md b/Java.md index 7c8558c..780c477 100644 --- a/Java.md +++ b/Java.md @@ -759,7 +759,7 @@ public class MethodDemo { ##### 方法选取 -重载的方法在编译过程中即可完成识别,方法调用时Java 编译器会根据所传入参数的声明类型(注意与实际类型区分)来选取重载方法。选取的过程共分为三个阶段: +重载的方法在编译过程中即可完成识别,方法调用时 Java 编译器会根据所传入参数的声明类型(注意与实际类型区分)来选取重载方法。选取的过程共分为三个阶段: * 在不考虑对基本类型自动装拆箱 (auto-boxing,auto-unboxing),以及可变长参数的情况下选取重载方法 * 如果第 1 个阶段中没有找到适配的方法,那么在允许自动装拆箱,但不允许可变长参数的情况下选取重载方法 @@ -790,7 +790,7 @@ public class MethodDemo { ##### 继承重载 -除了同一个类中的方法,重载也可以作用于这个类所继承而来的方法。也就是说,如果子类定义了与父类中**非私有方法**同名的方法,而且这两个方法的参数类型不同,那么在子类中,这两个方法同样构成了重载 +除了同一个类中的方法,重载也可以作用于这个类所继承而来的方法。如果子类定义了与父类中**非私有方法**同名的方法,而且这两个方法的参数类型不同,那么在子类中,这两个方法同样构成了重载 * 如果这两个方法都是静态的,那么子类中的方法隐藏了父类中的方法 * 如果这两个方法都不是静态的,且都不是私有的,那么子类的方法重写了父类中的方法,也就是**多态** @@ -4972,15 +4972,14 @@ JDK7对比JDK8: * JDK1.8 之前 HashMap 由 **数组+链表** 组成 * 数组是 HashMap 的主体 - * 链表则是为了解决哈希冲突而存在的(**拉链法**解决冲突),拉链法就是头插法 + * 链表则是为了**解决哈希冲突**而存在的(**拉链法**解决冲突),拉链法就是头插法 两个对象调用的hashCode方法计算的哈希码值(键的哈希)一致导致计算的数组索引值相同 * JDK1.8 以后 HashMap 由 **数组+链表 +红黑树**数据结构组成 * 解决哈希冲突时有了较大的变化 - * **当链表长度超过(大于)阈值(或者红黑树的边界值,默认为 8)并且当前数组的长度大于等于64时,此索引位置上的所有数据改为红黑树存储。** - - * 将链表转换成红黑树前会进行判断,即使阈值大于8,但是数组长度小于64,此时并不会将链表变为红黑树,而是选择进行数组扩容。因为数组比较小的情况下变为红黑树结构,反而会降低效率,红黑树需要进行左旋,右旋,变色这些操作来保持平衡。同时数组长度小于64时,搜索时间相对快些,所以为了提高性能和减少搜索时间,底层在阈值大于8并且数组长度大于64时,链表才转换为红黑树,效率也变的更高效 + * **当链表长度超过(大于)阈值(或者红黑树的边界值,默认为 8)并且当前数组的长度大于等于64时,此索引位置上的所有数据改为红黑树存储** + * 即使哈希函数取得再好,也很难达到元素百分百均匀分布。当 HashMap 中有大量的元素都存放到同一个桶中时,就相当于一个长的单链表,假如单链表有 n 个元素,遍历的**时间复杂度是 O(n)**,所以 JDK1.8 中引入了 红黑树(查找**时间复杂度为 O(logn)**)来优化这个问题,使得查找效率更高 ![](https://gitee.com/seazean/images/raw/master/Java/哈希表.png) @@ -5098,6 +5097,8 @@ HashMap继承关系如下图所示: * 其他说法 红黑树的平均查找长度是log(n),如果长度为8,平均查找长度为log(8)=3,链表的平均查找长度为n/2,当长度为8时,平均查找长度为8/2=4,这才有转换成树的必要;链表长度如果是小于等于6,6/2=3,而log(6)=2.6,虽然速度也很快的,但转化为树结构和生成树的时间并不短 + + 6. 当链表的值小于6则会从红黑树转回链表 ```java @@ -5112,6 +5113,8 @@ HashMap继承关系如下图所示: static final int MIN_TREEIFY_CAPACITY = 64; ``` + 原因:数组比较小的情况下变为红黑树结构,反而会降低效率,红黑树需要进行左旋,右旋,变色这些操作来保持平衡。同时数组长度小于64时,搜索时间相对快些,所以为了提高性能和减少搜索时间,底层在阈值大于8并且数组长度大于64时,链表才转换为红黑树,效率也变的更高效 + 8. table用来初始化(必须是二的n次幂)(重点) ```java @@ -5275,8 +5278,8 @@ transient int size; HashMap是支持Key为空的;HashTable是直接用Key来获取HashCode,key为空会抛异常 - * &(按位与运算):相同的二进制数位上,都是1的时候,结果为1,否则为零。 - * ^(按位异或运算):相同的二进制数位上,数字相同,结果为0,不同为1。 + * &(按位与运算):相同的二进制数位上,都是1的时候,结果为1,否则为零 + * ^(按位异或运算):相同的二进制数位上,数字相同,结果为0,不同为1,不进位加法 ```java static final int hash(Object key) { @@ -5287,17 +5290,11 @@ transient int size; } ``` - * `(n - 1) & hash`:计算下标位置 - - - - ​ 面试题计算hash的方法:将hashCode无符号右移16位,高16 bit和低16 bit做了一个异或 + 计算hash的方法:将hashCode无符号右移16位,高16 bit和低16 bit做了一个异或 - * **为什么这样操作?** + 原因:当数组长度很小,假设是16,那么 n-1即为 1111 ,这样的值和hashCode()直接做按位与操作,实际上只使用了哈希值的后4位。如果当哈希值的高位变化很大,低位变化很小,就很容易造成哈希冲突了,所以这里**把高低位都利用起来,让高16位也参与运算**,从而解决了这个问题 - 如果当n即数组长度很小,假设是16,那么n-1即为 --> 1111 ,这样的值和hashCode()直接做按位与操作,实际上只使用了哈希值的后4位。如果当哈希值的高位变化很大,低位变化很小,就很容易造成哈希冲突了,所以这里**把高低位都利用起来,让高16位也参与运算**,从而解决了这个问题 - * **余数本质是不断做除法,把剩余的数减去,运算效率要比位运算低** 2. put @@ -5311,9 +5308,9 @@ transient int size; 2. 如果桶上没有碰撞冲突,则直接插入 - 3. 如果出现碰撞冲突了,则需要处理冲突:如果该桶使用红黑树处理冲突,则调用红黑树的方法插入数据;否则采用传统的链式方法插入,如果链的长度达到临界值,则把链转变为红黑树 + 3. 如果出现碰撞冲突:如果该桶使用红黑树处理冲突,则调用红黑树的方法插入数据;否则采用传统的链式方法插入,如果链的长度达到临界值,则把链转变为红黑树 - 4. 如果桶中存在重复的键,则为该键替换新值value + 4. 如果数组位置相同,通过equals比较内容是否相同:相同则新的value覆盖之前的value,不相同则将新的键值对添加到哈希表中 5. 如果size大于阈值threshold,则进行扩容 ```java @@ -5330,16 +5327,14 @@ transient int size; if ((p = tab[i = (n - 1) & hash]) == null)//这里的n表示数组长度16 //。。。。。。。。。。。。。。 } + ``` - * 什么是哈希碰撞?如何解决哈希碰撞? - 只要两个元素的key计算的哈希码值相同就会发生**哈希碰撞**,jdk8前使用链表解决哈希碰撞,jdk8之后使用链表+红黑树解决哈希碰撞 - * 如果两个键的hashcode相同,如何存储键值对? - hashcode相同,通过equals比较内容是否相同: - * 相同:则新的value覆盖之前的value - * 不相同:则将新的键值对添加到哈希表中 - * 引入红黑树的原因? - JDK 1.8 以前 HashMap 的实现是 数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。当 HashMap 中有大量的元素都存放到同一个桶中时,就相当于一个长的单链表,假如单链表有 n 个元素,遍历的**时间复杂度是 O(n)**,完全失去了它的优势。针对这种情况,JDK 1.8 中引入了 红黑树(查找时**间复杂度为 O(logn)**)来优化这个问题,使得查找效率更高 + * `(n - 1) & hash`:计算下标位置 + + + + * 余数本质是不断做除法,把剩余的数减去,运算效率要比位运算低 @@ -5543,7 +5538,9 @@ transient int size; 5. get 1. 通过hash值获取该key映射到的桶 + 2. 桶上的key就是要查找的key,则直接找到并返回 + 3. 桶上的key不是要找的key,则查看后续的节点: * 如果后续节点是红黑树节点,通过调用红黑树的方法根据key获取value @@ -5551,12 +5548,13 @@ transient int size; * 如果后续节点是链表节点,则通过循环遍历链表根据key获取value 4. 红黑树节点调用的是getTreeNode方法通过树形节点的find方法进行查 - + * 查找红黑树,由于之前添加时已经保证这个树是有序的了,因此查找时基本就是折半查找,效率更高。 * 这里和插入时一样,如果对比节点的哈希值和要查找的哈希值相等,就会判断key是否相等,相等就直接返回,不相等就从子树中递归查找 - 5. 若为树,则在树中通过key.equals(k)查找,**O(logn)** - + 5. 时间复杂度 O(1) + 若为树,则在树中通过key.equals(k)查找,**O(logn)** + 若为链表,则在链表中通过key.equals(k)查找,**O(n)** @@ -6037,7 +6035,7 @@ class Dog{} -### 不可变集合 +### 不可变 + 在List、Set、Map接口中都存在of方法,可以创建一个不可变的集合 + 这个集合不能添加,不能删除,不能修改 @@ -8136,53 +8134,46 @@ public class CommonsIODemo01 { ## 网络 -### 概述 +### 介绍 + +#### 网络编程 -#### 软件结构 +网络编程,就是在一定的协议下,实现两台计算机的通信的技术 通信一定是基于软件结构实现的: -* C/S结构 :全称为Client/Server结构,是指客户端和服务器结构。 - 常见程序有QQ、迅雷,IDEA等软件。 +* C/S结构 :全称为Client/Server结构,是指客户端和服务器结构,常见程序有QQ、IDEA等软件。 * B/S结构 :全称为Browser/Server结构,是指浏览器和服务器结构。 - 常见浏览器有谷歌、火狐等、软件:博学谷、京东、淘宝。 - (开发中的重点,基于网页设计界面,界面效果可以更丰富: Java Web开发) 两种架构各有优势,但是无论哪种架构,都离不开网络的支持。 -网络编程,就是在一定的协议下,实现两台计算机的通信的技术。 - - - -#### 三要素 网络通信的三要素: -1. 协议:计算机网络客户端与服务端通信必须事先约定和彼此遵守的通信规则。 - HTTP , FTP , TCP , UDP , SSH , SMTP。 +1. 协议:计算机网络客户端与服务端通信必须约定和彼此遵守的通信规则,HTTP、FTP、TCP、UDP、SMTP + +2. IP地址:互联网协议地址(Internet Protocol Address),用来给一个网络中的计算机设备做唯一的编号 -2. IP地址:互联网协议地址(Internet Protocol Address)。用来给一个网络中的计算机设备做唯一的编号。 - - * IPv4 :4个字节,32位组成。 192.168.1.1 + * IPv4 :4个字节,32位组成,192.168.1.1 * Pv6:可以实现为所有设备分配IP 128位 * ipconfig:查看本机的IP ​ ping 检查本机与某个IP指定的机器是否联通,或者说是检测对方是否在线。 ​ ping 空格 IP地址 :ping 220.181.57.216,ping www.baidu.com - 注意:特殊的IP地址: 本机IP地址.(不受环境的影响,任何时候都存在这两个ip,可以直接找本机!) - ​ **127.0.0.1 == localhost**。 - + 特殊的IP地址: 本机IP地址,**127.0.0.1 == localhost**,回环测试 + 3. 端口:端口号就可以唯一标识设备中的进程(应用程序) - 端口号: - 用两个字节表示的整数,它的取值范围是0~65535。 - 0~1023之间的端口号用于一些知名的网络服务和应用。普通的应用程序需要使用1024以上的端口号。 - 如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败。报出端口被占用异常!! + 端口号:用两个字节表示的整数,的取值范围是0-65535,0-1023之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用1024以上的端口号。如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败,报出端口被占用异常 利用**协议+IP地址+端口号** 三元组合,就可以标识网络中的进程了,那么进程间的通信就可以利用这个标识与其它进程进行交互。 -#### 分层和协议 +**** + + + +#### 通信协议 网络通信协议:对计算机必须遵守的规则,只有遵守这些规则,计算机之间才能进行通信 @@ -8217,41 +8208,146 @@ UDP:用户数据报协议(User Datagram Protocol),是一个面向无连接 相关概念: * 同步:当前线程要自己进行数据的读写操作(自己去银行取钱) -* 异步:当前线程可以去做其他事情(委托一小弟拿银行卡到银行取钱,然后给你) +* 异步:当前线程可以去做其他事情(委托别人拿银行卡到银行取钱,然后给你) * 阻塞:在数据没有的情况下,还是要继续等待着读(排队等待) * 非阻塞:在数据没有的情况下,会去做其他事情,一旦有了数据再来获取(柜台取款,取个号,然后坐在椅子上做其它事,等号广播会通知你办理) -基本通信模型: +Java中的通信模型: -1. BIO通信模式:同步阻塞式通信。(Socket网络编程也就是上面的通信架构) - BIO表示同步阻塞式IO,服务器实现模式为一个连接一个线程, - 即客户端有连接请求时服务器端就需要启动一个线程进行处理, - 如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。 - 同步阻塞式性能极差:大量线程,大量阻塞。 +1. BIO通信模式:同步阻塞式通信,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善。 + 同步阻塞式性能极差:大量线程,大量阻塞 -2. 伪异步通信:引入了线程池。 - 不需要一个客户端一个线程,可以实现1个线程复用来处理很多个客户端! - 这种架构,可以避免系统的死机,因为不会出现很多线程,线程可控。 - 但是高并发下性能还是很差:a.线程数量少,数据依然是阻塞的。数据没有来线程还是要等待! +2. 伪异步通信:引入线程池,不需要一个客户端一个线程,实现线程复用来处理很多个客户端,线程可控。 + 高并发下性能还是很差:线程数量少,数据依然是阻塞的;数据没有来线程还是要等待 -3. NIO表示**同步非阻塞IO**,服务器实现模式为请求对应一个线程, - 客户端发送的连接会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理 +3. NIO表示**同步非阻塞IO**,服务器实现模式为请求对应一个线程,客户端发送的连接会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理 -​ 1个主线程专门负责接收客户端: - ​ 1个线程[c1 ,s2 ,c3,c4, ,s2 ,c3,c4,,c3,c4, ]轮询所有的客户端,发来了数据才会开启线程处理 - ​ 同步:线程还要不断的接收客户端连接,以及处理数据。 - ​ 非阻塞:如果一个管道没有数据,不需要等待,可以轮询下一个管道是否有数据! + 工作原理:1个主线程专门负责接收客户端,1个线程轮询所有的客户端,发来了数据才会开启线程处理 + 同步:线程还要不断的接收客户端连接,以及处理数据 + 非阻塞:如果一个管道没有数据,不需要等待,可以轮询下一个管道是否有数据 -4. AIO表示异步非阻塞IO,服务器实现模式为一个有效请求一个线程, - 客户端的I/O请求都是由操作系统先完成IO操作后再通知服务器应用来启动线程进行处理。 - 异步:服务端线程接收到了客户端管道以后就交给底层处理它的io通信。自己可以做其他事情。 - 非阻塞:底层也是客户端有数据才会处理,有了数据以后处理好通知服务器应用来启动线程进行处理。 +4. AIO表示异步非阻塞IO,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由操作系统先完成,完成后通知服务器应用来启动线程进行处理 + 异步:服务端线程接收到了客户端管道以后就交给底层处理IO通信,线程可以做其他事情 + 非阻塞:底层也是客户端有数据才会处理,有了数据以后处理好通知服务器应用来启动线程进行处理 各种模型应用场景: -* BIO适用于连接数目比较小且固定的架构,该方式对服务器资源要求比较高,并发局限于应用中,程序简单。 -* NIO适用于连接数目多且连接比较短(轻操作)的架构,如聊天服务器,并发局限于应用中,编程复杂,JDK 1.4开始支持。 -* AIO适用于连接数目多且连接比较长(重操作)的架构,如相册服务器,充分调用操作系统参与并发操作,编程复杂,JDK 1.7开始支持。 +* BIO适用于连接数目比较小且固定的架构,该方式对服务器资源要求比较高,并发局限于应用中,程序简单 +* NIO适用于连接数目多且连接比较短(轻操作)的架构,如聊天服务器,并发局限于应用中,编程复杂,JDK 1.4开始支持 +* AIO适用于连接数目多且连接比较长(重操作)的架构,如相册服务器,充分调用操作系统参与并发操作,编程复杂,JDK 1.7开始支持 + + + + + +**** + + + +### I/O + +#### 模型 + +##### IO模型 + +对于一个套接字上的输入操作,第一步是等待数据从网络中到达,当数据到达时被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区 + +Linux 有五种 I/O 模型: + +- 阻塞式 I/O +- 非阻塞式 I/O +- I/O 复用(select 和 poll) +- 信号驱动式 I/O(SIGIO) +- 异步 I/O(AIO) + +五种模型对比: + +* 同步 I/O 包括阻塞式 I/O、非阻塞式 I/O、I/O 复用和信号驱动 I/O ,它们的主要区别在第一个阶段,非阻塞式 I/O 、信号驱动 I/O 和异步 I/O 在第一阶段不会阻塞 + +- 同步 I/O:将数据从内核缓冲区复制到应用进程缓冲区的阶段(第二阶段),应用进程会阻塞 +- 异步 I/O:第二阶段应用进程不会阻塞 + + + +*** + + + +##### 阻塞式IO + +应用进程通过系统调用 recvfrom 接收数据,会被阻塞,直到数据从内核缓冲区复制到应用进程缓冲区中才返回。阻塞不意味着整个操作系统都被阻塞,其它应用进程还可以执行,只是当前阻塞进程不消耗 CPU 时间,这种模型的 CPU 利用率会比较高 + +recvfrom() 用于接收 Socket 传来的数据,并复制到应用进程的缓冲区 buf 中,把 recvfrom() 当成系统调用 + +![](https://gitee.com/seazean/images/raw/master/Java/IO模型-阻塞式IO.png) + + + +*** + + + +##### 非阻塞式 + +应用进程通过 recvfrom 调用不停的去和内核交互,直到内核准备好数据。如果没有准备好数据,内核返回一个错误码,过一段时间应用进程再执行 recvfrom 系统调用,在两次发送请求的时间段,进程可以其他任务,这种方式称为轮询(polling) + +由于 CPU 要处理更多的系统调用,因此这种模型的 CPU 利用率比较低 + +![](https://gitee.com/seazean/images/raw/master/Java/IO模型-非阻塞式IO.png) + + + +*** + + + +##### 信号驱动 + +应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,等待数据阶段应用进程是非阻塞的。当内核数据准备就绪时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中 + +相比于非阻塞式 I/O 的轮询方式,信号驱动 I/O 的 CPU 利用率更高 + +![](https://gitee.com/seazean/images/raw/master/Java/IO模型-信号驱动IO.png) + + + +*** + + + +##### IO复用 + +IO 复用模型使用 select 或者 poll 函数等待数据,select 会监听所有注册好的 IO,等待多个套接字中的任何一个变为可读。等待过程会被**阻塞**,当某个套接字准备好数据变为可读时 select 调用就返回,然后调用 recvfrom 把数据从内核复制到进程中。 + +IO 复用让单个进程具有处理多个 I/O 事件的能力,又被称为 Event Driven I/O,即事件驱动 I/O。 + +如果一个 Web 服务器没有 I/O 复用,那么每一个 Socket 连接都要创建一个线程去处理,如果同时有几万个连接,就需要创建相同数量的线程。相比于多进程和多线程技术,I/O 复用不需要进程线程创建和切换的开销,系统开销更小。 + +![](https://gitee.com/seazean/images/raw/master/Java/IO模型-IO复用模型.png) + + + +*** + + + +##### 异步IO + +应用进程执行 aio_read 系统调用会立即返回,给内核传递描述符、缓冲区指针、缓冲区大小等。应用进程可以继续执行不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。 + +异步 I/O 与信号驱动 I/O 的区别在于,异步 I/O 的信号是通知应用进程 I/O 完成,而信号驱动 I/O 的信号是通知应用进程可以开始 I/O + +![](https://gitee.com/seazean/images/raw/master/Java/IO模型-异步IO模型.png) + + + +**** + + + +#### 函数 + +(待完善select和epoll函数,c语言函数) @@ -12101,13 +12197,13 @@ public class Demo1_27 { 工作机制: -* **对象优先在 Eden 分配**:当创建一个对象的时候,对象会被分配在新生代的Eden区,当Eden区要满了时候,触发YoungGC +* **对象优先在 Eden 分配**:当创建一个对象的时候,对象会被分配在新生代的 Eden 区,当Eden 区要满了时候,触发 YoungGC -* 当进行YoungGC后,此时在Eden区存活的对象被移动到S0区,并且**当前对象的年龄会加1**,清空Eden区 +* 当进行 YoungGC 后,此时在 Eden 区存活的对象被移动到S0区,并且**当前对象的年龄会加1**,清空 Eden 区 -* 当再一次触发YoungGC的时候,会把Eden区中存活下来的对象和S0中的对象,移动到S1区中,这些对象的年龄会加1,清空Eden区和S0区 +* 当再一次触发 YoungGC 的时候,会把 Eden 区中存活下来的对象和 S0 中的对象,移动到 S1 区中,这些对象的年龄会加1,清空 Eden 区和 S0 区 -* to区永远是空Survivor区,from区是有数据的,每次MinorGC后两个区域互换 +* to区永远是空 Survivor 区,from 区是有数据的,每次 MinorGC 后两个区域互换 晋升到老年代: @@ -12120,7 +12216,7 @@ public class Demo1_27 { 空间分配担保: * 在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的 -* 如果不成立,虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。 +* 如果不成立,虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于将尝试着进行一次 Minor GC;如果小于或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。 @@ -12938,14 +13034,12 @@ G1对比其他处理器的优点: * **分区算法:** - * 从分代上看,G1属于分代型垃圾回收器,区分年轻代和老年代,年轻代依然有Eden区和Survivor区 - 从堆结构上看,不要求整个Eden区、年轻代或老年代都是连续的,也不再坚持固定大小和固定数量 - * 将整个堆划分成约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到32MB之间且为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) @@ -12954,21 +13048,20 @@ G1对比其他处理器的优点: - CMS:“标记-清除”算法、内存碎片、若干次GC后进行一次碎片整理 - G1:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(Region 之间)上来看是基于“复制”算法实现的,两种算法都可以避免内存碎片 - - 这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC,尤其是当Java堆非常大的时候,G1的优势更加明显 -- **可预测的停顿时间模型(即:软实时soft real-time):** +- **可预测的停顿时间模型(软实时soft real-time):** - 可以指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒 - - 由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制 - - G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间,通过过去回收的经验获得),在后台维护一个**优先列表**,每次根据允许的收集时间,优先回收价值最大的Region,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率 + - 由于分块的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,对于全局停顿情况也能得到较好的控制 + - G1跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间,通过过去回收的经验获得),在后台维护一个**优先列表**,每次根据允许的收集时间,优先回收价值最大的Region,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率 - * 相比于CMS GC,G1未必能做到CMS在最好情况下的延时停顿,但是最差情况要好很多 + * 相比于 CMS GC,G1 未必能做到 CMS 在最好情况下的延时停顿,但是最差情况要好很多 G1垃圾收集器的缺点: -* 相较于CMS,G1还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用还是程序运行时的额外执行负载都要比CMS要高 +* 相较于 CMS,G1 还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1 无论是为了垃圾收集产生的内存占用还是程序运行时的额外执行负载都要比 CMS 要高 * 从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则发挥其优势。平衡点在6-8GB之间 应用场景: @@ -12988,7 +13081,7 @@ G1垃圾收集器的缺点: -* 程序对Reference类型数据写操作时,产生一个Write Barrier暂时中断操作,检查该对象和Reference类型数据是否在不同的Region(其他收集器:检查老年代对象是否引用了新生代对象),不同便通过 CardTable 把相关引用信息记录到Reference类型所属的 Region 的 Remembered Set 之中 +* 程序对Reference类型数据写操作时,产生一个 Write Barrier 暂时中断操作,检查该对象和Reference类型数据是否在不同的Region(其他收集器:检查老年代对象是否引用了新生代对象),不同便通过 CardTable 把相关引用信息记录到Reference类型所属的 Region 的 Remembered Set 之中 * 进行内存回收时,在 GC 根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏 @@ -13005,31 +13098,31 @@ G1中提供了三种垃圾回收模式:YoungGC、Mixed GC和FullGC,在不同 顺时针:Young GC -> Young GC + Concurrent Mark -> Mixed GC顺序,进行垃圾回收 -* **Young GC**:发生在年轻代的GC算法,一般对象(除了巨型对象)都是在eden region中分配内存,当所有eden region被耗尽无法申请内存时,就会触发一次young gc,G1停止应用程序的执行 (Stop-The-World),把活跃对象放入老年代,垃圾对象回收 +* **Young GC**:发生在年轻代的GC算法,一般对象(除了巨型对象)都是在 eden region 中分配内存,当所有 eden region 被耗尽无法申请内存时,就会触发一次 young gc,G1停止应用程序的执行 (Stop-The-World),把活跃对象放入老年代,垃圾对象回收 **回收过程**: 1. 扫描根:根引用连同RSet记录的外部引用作为扫描存活对象的入口 - 2. 更新RSet:处理dirty card queue更新RS,此后RSet准确的反映老年代对所在的内存分段中对象的引用 + 2. 更新RSet:处理 dirty card queue 更新RS,此后RSet准确的反映老年代对所在的内存分段中对象的引用 * dirty card queue:类似缓存,产生了引用先记录在这里,然后更新到RSet * 作用:产生引用直接更新RSet需要线程同步开销很大,使用队列性能好 3. 处理RSet:识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象 - 4. 复制对象:Eden区内存段中存活的对象会被复制到Survivor区,Survivor区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到old区中空的内存分段,如果Survivor空间不够,Eden空间的部分数据会直接晋升到老年代空间 - 5. 处理引用:处理Soft,Weak,Phantom,JNI Weak 等引用,最终Eden空间的数据为空,GC停止工作 + 4. 复制对象:Eden区内存段中存活的对象会被复制到 survivor 区,survivor 区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到 old 区中空的内存分段,如果 survivor 空间不够,Eden 空间的部分数据会直接晋升到老年代空间 + 5. 处理引用:处理 Soft,Weak,Phantom,JNI Weak 等引用,最终 Eden 空间的数据为空,GC 停止工作 * **并发标记过程**: * 初始标记:标记从根节点直接可达的对象,这个阶段是STW的,并且会触发一次年轻代GC * 根区域扫描 (Root Region Scanning):G1 扫描survivor区直接可达的老年代区域对象,并标记被引用的对象,这一过程必须在Young GC之前完成 - * 并发标记 (Concurrent Marking):在整个堆中进行并发标记(应用程序并发执行),可能被YoungGC中断。在并发标记阶段,**若发现区域对象中的所有对象都是垃圾,则这个区域会被立即回收(实时回收)**,同时并发标记过程中,会计算每个区域的对象活性,即区域中存活对象的比例 + * 并发标记 (Concurrent Marking):在整个堆中进行并发标记(应用程序并发执行),可能被 YoungGC 中断。在并发标记阶段,**若发现区域对象中的所有对象都是垃圾,则这个区域会被立即回收(实时回收)**,同时并发标记过程中,会计算每个区域的对象活性,即区域中存活对象的比例 * 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这阶段需要停顿线程,但是可并行执行 * 筛选回收:并发清理阶段,首先对各个 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**:当很多对象晋升到老年代 old region 时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即 Mixed GC,除了回收整个 young region,还会**回收一部分**的 old region,过程同YGC - 注意:**是一部分老年代,而不是全部老年代**,可以选择哪些old region收集,对垃圾回收的耗时时间进行控制 + 注意:**是一部分老年代,而不是全部老年代**,可以选择哪些old region收集,对垃圾回收的时间进行控制 在G1中,Mixed GC可以通过`-XX:InitiatingHeapOccupancyPercent`设置阈值 @@ -13074,7 +13167,7 @@ G1的设计原则就是简化JVM性能调优,只需要简单的三步即可完 * `XX:MaxGCPauseMillis=x` 可以设置启动应用程序暂停的时间,G1在运行的时候会根据这个参数选择CSet来满足响应时间的设置 * 设置到100ms或者200ms都可以(不同情况下会不一样),但设置成50ms就不太合理 -* 暂停时间设置的太短,就会导致出现G1跟不上垃圾产生的速度,最终退化成Full GC +* 暂停时间设置的太短,就会导致出现G1跟不上垃圾产生的速度,最终退化成 Full GC * 对这个参数的调优是一个持续的过程,逐步调整到最佳状态 **不要设置新生代和老年代的大小**: @@ -14187,7 +14280,7 @@ Java 虚拟机识别方法的关键在于类名、方法名以及方法描述符 * 方法描述符是由方法的参数类型以及返回类型所构成,也叫方法签名 * 在同一个类中,如果同时出现多个名字相同且描述符也相同的方法,那么 Java 虚拟机会在类的验证阶段报错 -JVM是根据名字和描述符来判断的,只要返回值不一样,其它完全一样,在JVM中是允许的,但Java语言不允许 +JVM根据名字和描述符来判断的,只要返回值不一样,其它完全一样,在JVM中是允许的,但Java语言不允许 ```java // 返回值类型不同,编译阶段直接报错 @@ -14418,7 +14511,7 @@ Java 虚拟机中关于方法重写的判定基于方法描述符,如果子类 * invokevirtual 所使用的虚方法表(virtual method table,vtable) * invokeinterface 所使用的接口方法表(interface method table,itable) -虚方法表会在类加载的链接阶段被创建 并开始初始化,类的变量初始值准备完成之后,jvm会把该类的方发表也初始化完毕 +虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,jvm会把该类的方法表也初始化完毕 虚方法表的执行过程: @@ -14439,7 +14532,7 @@ Passenger 类的方法表包括两个方法,分别对应 0 号和 1 号。方 虚方法表对性能的影响: * 使用了方法表的动态绑定与静态绑定相比,仅仅多出几个内存解引用操作:访问栈上的调用者、读取调用者的动态类型、读取该类型的方法表、读取方法表中某个索引值所对应的目标方法,但是相对于创建并初始化 Java 栈帧这操作的开销可以忽略不计 -* 上述优化的效果看上去不错,但实际上仅存在于解释执行中,或者即时编译代码的最坏情况。因为即时编译还拥有另外两种性能更好的优化手段:内联缓存(inlining cache)和方法内联(method inlining) +* 上述优化的效果看上去不错,但实际上**仅存在于解释执行**中,或者即时编译代码的最坏情况。因为即时编译还拥有另外两种性能更好的优化手段:内联缓存(inlining cache)和方法内联(method inlining) ![](https://gitee.com/seazean/images/raw/master/Java/JVM-方法调用虚方法表图.png) @@ -14451,7 +14544,7 @@ Passenger 类的方法表包括两个方法,分别对应 0 号和 1 号。方 ##### 内联缓存 -内联缓存:是一种加快动态绑定的优化技术,它能够缓存虚方法调用中调用者的动态类型,以及该类型所对应的目标方法。在之后的执行过程中,如果碰到已缓存的类型,内联缓存便会直接调用该类型所对应的目标方法;如果没有碰到已缓存的类型,内联缓存则会退化至使用基于方法表的动态绑定 +内联缓存:是一种加快动态绑定的优化技术,它能够缓存虚方法调用中调用者的动态类型以及该类型所对应的目标方法。在之后的执行过程中,如果碰到已缓存的类型,内联缓存便会直接调用该类型所对应的目标方法;如果没有碰到已缓存的类型,内联缓存则会退化至使用基于方法表的动态绑定 多态的三个术语: @@ -17955,12 +18048,16 @@ CPU的基本工作是执行存储的指令序列,即程序,程序的执行 ### cache -#### 缓存结构 +#### 缓存机制 + +##### 缓存结构 在计算机系统中,CPU高速缓存(CPU Cache,简称缓存)是用于减少处理器访问内存所需平均时间的部件;在存储体系中位于自顶向下的第二层,仅次于CPU寄存器;其容量远小于内存,但速度却可以接近处理器的频率 CPU 处理器速度远远大于在主内存中的,为了解决速度差异,在它们之间架设了多级缓存,如 L1、L2、L3 级别的缓存,这些缓存离CPU越近就越快,将频繁操作的数据缓存到这里,加快访问速度 + + | 从 cpu 到 | 大约需要的时钟周期 | | --------- | -------------------------------- | | 寄存器 | 1 cycle (4GHz 的 CPU 约为0.25ns) | @@ -17969,44 +18066,82 @@ CPU 处理器速度远远大于在主内存中的,为了解决速度差异, | L3 | 40~45 cycle | | 内存 | 120~240 cycle | -![](https://gitee.com/seazean/images/raw/master/Java/JMM-CPU缓存结构.png) +##### 缓存使用 -*** +当处理器发出内存访问请求时,会先查看缓存内是否有请求数据,如果存在(命中),则不经访问内存直接返回该数据;如果不存在(失效),则要先把内存中的相应数据载入缓存,再将其返回处理器 +缓存之所以有效,主要因为程序运行时对内存的访问呈现局部性(Locality)特征。既包括空间局部性(Spatial Locality),也包括时间局部性(Temporal Locality),有效利用这种局部性,缓存可以达到极高的命中率。 -#### 缓存使用 -当处理器发出内存访问请求时,会先查看缓存内是否有请求数据,如果存在(命中),则不经访问内存直接返回该数据;如果不存在(失效),则要先把内存中的相应数据载入缓存,再将其返回处理器 +*** -缓存之所以有效,主要因为程序运行时对内存的访问呈现局部性(Locality)特征。既包括空间局部性(Spatial Locality),也包括时间局部性(Temporal Locality),有效利用这种局部性,缓存可以达到极高的命中率。 -多核 CPU 处理器,每个 CPU 处理器内维护了一块字节的内存,每个内核内部维护着一块缓存,当多线程并发读写时,就会出现缓存数据不一致的情况。处理器提供: -* 总线锁定:当处理器要操作共享变量时,在 BUS 总线上发出一个 Lock 信号,其他处理就无法操作这个共享变量,该操作会导致大量阻塞,从而增加系统的性能开销 -* 缓存锁定:当处理器对缓存中的共享变量进行了操作,其他处理器有嗅探机制,将其他处理器的该共享变量的缓存失效,其他线程读取时会重新从主内存中读取最新的数据,基于 MESI 缓存一致性协议来实现 +#### 缓存一致 -Linux查看CPU缓存行:cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size64 +缓存一致性:当多个处理器运算任务都涉及到同一块主内存区域的时候,将可能导致各自的缓存数据不一样 -内存地址格式:[高位组标记] [低位索引] [偏移量] +**MESI**(Modified Exclusive Shared Or Invalid)是一种广泛使用的支持写回策略的缓存一致性协议,CPU中每个缓存行 (caceh line) 使用4种状态进行标记(使用额外的两位 (bit) 表示): +* M:被修改(Modified) + 该缓存行只被缓存在该 CPU 的缓存中,并且是被修改过的,与主存中的数据不一致 (dirty),该缓存行中的内存需要在未来的某个时间点(其它 CPU 读取主存中相应数据之前)写回( write back )主存 -*** + 当被写回主存之后,该缓存行的状态会变成独享 (exclusive) 状态。 +* E:独享的(Exclusive) + 该缓存行只被缓存在该 CPU 的缓存中,它是未被修改过的 (clear),与主存中数据一致,该状态可以在任何时刻有其它 CPU 读取该内存时变成共享状态 (shared) -#### 缓存一致性 + 当 CPU 修改该缓存行中内容时,该状态可以变成 Modified 状态 -缓存一致性:当多个处理器运算任务都涉及到同一块主内存区域的时候,将可能导致各自的缓存数据不一样 +* S:共享的(Shared) + + 该状态意味着该缓存行可能被多个 CPU 缓存,并且各个缓存中的数据与主存数据一致 (clear),当有一个 CPU 修改该缓存行中,其它 CPU 中该缓存行变成无效状态 (Invalid) + +* I:无效的(Invalid) + + 该缓存是无效的,可能有其它 CPU 修改了该缓存行 解决方法:各个处理器访问缓存时都遵循一些协议,在读写时要根据协议进行操作,协议主要有MSI、MESI等 -* MESI:缓存一致性协议,当写数据时,如果发现操作的变量是共享变量,即在其它处理器中也存在该变量的副本,会发出信号通知其它处理器将该内存变量的缓存行设置为无效,因此当其它处理器读取这个变量,发现该变量是无效的,那么就会从内存中重新读取 +Linux查看CPU缓存行: + +* 命令:`cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size64` + +* 内存地址格式:[高位组标记] [低位索引] [偏移量] + +缓存行在 无锁 --> LongAddr --> 伪共享部分详解 + + + +**** + + + +#### 处理机制 + +单核 CPU 处理器会自动保证基本内存操作的原子性 + +多核 CPU 处理器,每个 CPU 处理器内维护了一块字内存,每个内核内部维护着一块缓存,当多线程并发读写时,就会出现缓存数据不一致的情况。处理器提供: + +* 总线锁定:当处理器要操作共享变量时,在 BUS 总线上发出一个 LOCK 信号,其他处理就无法操作这个共享变量,该操作会导致大量阻塞,从而增加系统的性能开销 +* 缓存锁定:当处理器对缓存中的共享变量进行了操作,其他处理器有嗅探机制,将其他处理器的该共享变量的缓存失效,其他线程读取时会重新从主内存中读取最新的数据,基于 MESI 缓存一致性协议来实现 + +有如下两种情况处理器不会使用缓存锁定: + +* 当操作的数据跨多个缓存行,或没被缓存在处理器内部,则处理器会使用总线锁定 + +* 有些处理器不支持缓存锁定,比如:Intel 486 和 Pentium 处理器也会调用总线锁定 + +总线机制: + * 总线嗅探:每个处理器通过嗅探在总线上传播的数据来检查自己缓存值是否过期了,当处理器发现自己的缓存对应的内存地址被修改,就将当前处理器的缓存行设置为无效状态,当处理器对这个数据进行操作时,会重新从内存中把数据读取到处理器缓存中 -* 总线风暴:由于Volatile的MESI缓存一致性协议,需要不断的从主内存嗅探和CAS循环,无效的交互会导致总线带宽达到峰值;因此不要大量使用volatile关键字,至于什么时候使用volatile、syschonized都是需要根据实际场景 + +* 总线风暴:由于 volatile 的 MESI 缓存一致性协议,需要不断的从主内存嗅探和 CAS 循环,无效的交互会导致总线带宽达到峰值;因此不要大量使用 volatile 关键字,使用 volatile、syschonized 都需要根据实际场景 @@ -18088,19 +18223,20 @@ Volatile是Java虚拟机提供的**轻量级**的同步机制(三大特性) #### 底层原理 -使用volatile修饰的共享变量,总线会开启MESI缓存一致性协议以及CPU总线嗅探机制来解决JMM缓存一致性问题,也就是共享变量在多线程中可见性的问题。 +使用volatile修饰的共享变量,总线会开启CPU总线嗅探机制来解决JMM缓存一致性问题,也就是共享变量在多线程中可见性的问题,实现 MESI 缓存一致性协议 -底层实现主要是通过汇编 lock 前缀指令,共享变量加了 lock 前缀指令,在线程修改完共享变量后,会马上执行 store 和 write 操作。在执行 store 操作前,会先执行缓存锁定的操作,其他的CPU上运行的线程根据CPU总线嗅探机制会修改其共享变量为失效状态,读取时会重新从主内存中读取最新的数据 +底层是通过汇编 lock 前缀指令,共享变量加了 lock 前缀指令,在线程修改完共享变量后,会马上执行 store 和 write 操作。在执行 store 操作前,会先执行缓存锁定的操作,其他的CPU上运行的线程根据CPU总线嗅探机制会修改其共享变量为失效状态,读取时会重新从主内存中读取最新的数据 lock前缀指令就相当于内存屏障,Memory Barrier(Memory Fence) * 对 volatile 变量的写指令后会加入写屏障 * 对 volatile 变量的读指令前会加入读屏障 -内存屏障有两个作用: +内存屏障有三个作用: +- 确保对内存的读-改-写操作原子执行 - 阻止屏障两侧的指令重排序 -- 强制把缓存中的脏数据写回主内存,让缓存中相应的数据失效 +- 强制把缓存中的脏数据写回主内存,让缓存行中相应的数据失效 保证可见性: @@ -18583,13 +18719,18 @@ public final class Singleton { ### CAS -#### 原语 +#### 实现原理 CAS的全称是Compare-And-Swap,是**CPU并发原语** * CAS并发原语体现在Java语言中就是sun.misc.Unsafe类的各个方法,调用UnSafe类中的CAS方法,JVM会实现出CAS汇编指令,这是一种完全依赖于硬件的功能,通过它实现了原子操作 * CAS是一种系统原语,原语属于操作系统范畴,是由若干条指令组成 ,用于完成某个功能的一个过程,并且**原语的执行必须是连续的,执行过程中不允许被中断**,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致的问题,也就是说CAS是线程安全的 -* CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证比较交换的原子性。在多核状态下,某个核执行到带 lock 的指令时,CPU 会让总线锁住,当这个核把此指令执行完毕,再开启总线,这个过程不会被线程的调度机制所打断,保证了多个线程对内存操作的原子性 + +底层原理:CAS 的底层是 `lock cmpxchg` 指令(X86 架构),在单核和多核 CPU 下都能够保证比较交换的原子性 + +* 程序是在单核处理器上运行,就会省略 lock 前缀(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果) + +* 程序是在多核处理器上运行,会为 cmpxchg 指令加上 lock 前缀 (lock cmpxchg)。当某个核执行到带 lock 的指令时,CPU 会执行**总线锁定或缓存锁定**,当这个核把此指令执行完毕,这个过程不会被线程的调度机制所打断,保证了多个线程对内存操作的原子性 作用:比较当前工作内存中的值和主物理内存中的值,如果相同则执行规定操作,否者继续比较直到主内存和工作内存的值一致为止 @@ -18888,9 +19029,9 @@ LongAdder和LongAccumulator区别: -### LongAdder +### Adder -#### CAS优化 +#### 优化CAS LongAdder是Java8提供的类,跟AtomicLong有相同的效果,但对CAS机制进行了优化,尝试使用分段CAS以及自动分段迁移的方式来大幅度提升多线程高并发执行CAS操作的性能 diff --git a/Tool.md b/Tool.md index b90bb7e..5693615 100644 --- a/Tool.md +++ b/Tool.md @@ -1976,21 +1976,23 @@ Vim 打开一个文件(文件可以存在,也可以不存在),默认就 -##### 选中文本 +*** + -- 学习 复制 命令前, 应该先学会 **怎么样选中 要复制的代码** -- 在 vi/vim 中要选择文本, 需要显示 visual 命令切换到 **可视模式** +##### 选中文本 + +在 vi/vim 中要选择文本, 需要显示 visual 命令切换到 **可视模式** -- vi/vim 中提供了 **三种** 可视模式, 可以方便程序员的选择 **选中文本的方式** +vi/vim 中提供了 **三种** 可视模式, 可以方便程序员的选择 **选中文本的方式** -- 按 ESC 可以放弃选中, 返回到 **命令模式** +按 ESC 可以放弃选中, 返回到 **命令模式** - | 命令 | 模式 | 功能 | - | -------- | ---------- | ---------------------------------- | - | v | 可视模式 | 从光标位置开始按照正常模式选择文本 | - | V | 可视化模式 | 选中光标经过的完整行 | - | Ctrl + v | 可是块模式 | 垂直方向选中文本 | +| 命令 | 模式 | 功能 | +| -------- | ---------- | ---------------------------------- | +| v | 可视模式 | 从光标位置开始按照正常模式选择文本 | +| V | 可视化模式 | 选中光标经过的完整行 | +| Ctrl + v | 可是块模式 | 垂直方向选中文本 | @@ -2026,6 +2028,10 @@ Vim 打开一个文件(文件可以存在,也可以不存在),默认就 +*** + + + ##### 复制粘贴 vim 中提供有一个 被复制文本的缓冲区 @@ -2043,7 +2049,11 @@ vim 中提供有一个 被复制文本的缓冲区 | p | 将剪切板中的内容粘贴到光标后 | | P(大写) | 将剪切板中的内容粘贴到光标前 | -注意:vi中的 **文本缓冲区**和系统的**剪切板**不是同一个,在其他软件中使用 Ctrl + C复制的内容, 不能在vim 中通过 `p` 命令粘贴,可以在 **编辑模式** 下使用 **鼠标右键粘贴**。 +注意:vi中的 **文本缓冲区**和系统的**剪切板**不是同一个,在其他软件中使用 Ctrl + C 复制的内容,不能在vim 中通过 `p` 命令粘贴,可以在 **编辑模式** 下使用 **鼠标右键粘贴**。 + + + +*** @@ -2175,6 +2185,28 @@ pstree -A #查看所有进程树 +*** + + + +### 进程ID + +进程号: + +* 进程号为 0 的进程通常是调度进程,常常被称为交换进程(swapper),该进程是内核的一部分,它并不执行任何磁盘上的程序,因此也被称为系统进程 + +* 进程号为 1 是init进程,是一个守护进程,在自举过程结束时由内核调用,init 进程绝不会终止,是一个普通的用户进程,但是它以超级用户特权运行 + +父进程 ID 为 0 的进程通常是内核进程,它们作为系统自举过程的一部分而启动,但 init 进程是个例外,它的父进程是0,但是它是用户进程 + +自举程序:存储在内存中 ROM,用来加载操作系统。CPU 的程序计数器指向 ROM 中自举程序第一条指令所对应的位置,当计算机通电,CPU 开始读取并执行自举程序,将操作系统(不是全部,只是启动计算机的那部分程序)装入RAM中,这个过程是自举过程 + +装入完成后,CPU 的程序计数器就被设置为 RAM 中操作系统的第一条指令所对应的位置,接下来CPU将开始执行操作系统的指令 + + + +*** + ### 进程状态 @@ -2191,7 +2223,7 @@ pstree -A #查看所有进程树 * 一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程 -* 孤儿进程将被 init 进程(进程号为 1,守护进程)所收养,并由 init 进程对它们完成状态收集工作 +* 孤儿进程将被 init 进程所收养,并由 init 进程对它们完成状态收集工作 * 孤儿进程会被 init 进程收养,所以孤儿进程不会对系统造成危害 僵尸进程: @@ -2206,6 +2238,7 @@ pstree -A #查看所有进程树 * 守护进程(daemon)是一类在后台运行的特殊进程,用于执行特定的系统任务。 * 守护进程是脱离于终端并且在后台运行的进程,脱离终端是为了避免在执行的过程中的信息在终端上显示,并且进程也不会被任何终端所产生的终端信息所打断 * 很多守护进程在系统引导的时候启动,并且一直运行直到系统关闭;另一些只在需要的时候才启动,完成任务后就自动结束 +* ID 为 0 的进程通常是调度进程,被称为交换进程(swapper),该进程是内核的一部分,并不执行任何磁盘上的程序,因此也被称为系统进程 @@ -2226,6 +2259,10 @@ pstree -A #查看所有进程树 +*** + + + #### wait ```c @@ -2241,6 +2278,10 @@ pid_t wait(int *status) +*** + + + #### waitpid ```c From c6256e283693c121f3126ee187fc511a81a24b57 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 30 May 2021 21:14:03 +0800 Subject: [PATCH 030/242] Update Java Notes --- DB.md | 214 +++++++++++++++++- Java.md | 679 ++++++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 842 insertions(+), 51 deletions(-) diff --git a/DB.md b/DB.md index 18bbfb1..9b78e06 100644 --- a/DB.md +++ b/DB.md @@ -7815,7 +7815,42 @@ dbfilename "dump-6379.rdb" -## 结构模型 +## 体系结构 + +### 存储对象 + +Redis 使用对象来表示数据库中的键和值,当在 Redis 数据库中新创建一个键值对时至少会创建两个对象,一个对象用作键值对的键(键对象),另一个对象用作键值对的值(值对象) + +Redis 中对象由一个 redisObject 结构表示,该结构中和保存数据有关的三个属性分别是type、 encoding、ptr: + +```c +typedef struct redisObiect{ + //类型 + unsigned type:4; + //编码 + unsigned encoding:4; + //指向底层数据结构的指针 + void *ptr; +} +``` + +Redis 中主要数据结构有:简单动态字符串(SDS)、双端链表、字典、压缩列表、整数集合、跳跃表 + +Redis 并没有直接使用数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象,而每种对象又通过不同的编码映射到不同的底层数据结构 + +Redis 自身是一个 Map,其中所有的数据都是采用 key : value 的形式存储,**键对象都是字符串对象**,而值对象有五种基本类型和三种高级类型对象 + +![](https://gitee.com/seazean/images/raw/master/DB/Redis-对象模型.png) + + + + + +*** + + + +### 线程模型 Redis 基于 Reactor 模式开发了网络事件处理器,这个处理器被称为文件事件处理器(file event handler),这个文件事件处理器是单线程的,所以Redis叫做单线程的模型 @@ -8011,12 +8046,6 @@ Redis为每个服务提供16个数据库,编码0-15,每个数据库之间的 #### 简介 -redis 自身是一个 Map,其中所有的数据都是采用 key : value 的形式存储 - -数据类型指的是存储的数据的类型,也就是 value 部分的类型,**key 部分永远都是字符串** - -string类型的数据: - 存储的数据:单个数据,最简单的数据存储类型,也是最常用的数据存储类型,实质上是存一个字符串 存储数据的格式:一个存储空间保存一个数据,每一个空间中只能保存一个字符串信息 @@ -8144,6 +8173,18 @@ string类型的数据: +#### 实现 + +Redis字符串对象底层的数据结构实现主要是 int 和简单动态字符串SDS,涉及C语言相关,先不做记录 + +参考文章:https://www.cnblogs.com/hunternet/p/9957913.html + + + +*** + + + ### hash #### 简介 @@ -8230,7 +8271,50 @@ user:id:3506728370 → {"name":"春晚","fans":12210862,"blogs":83} 假如现在粉丝数量发生了变化,要把整个值都改变,但是用单条存就不存在这个问题,只需要改其中一个就可以 -![](https://gitee.com/seazean/images/raw/master/DB/hash应用场景结构图.png) + + + + +*** + + + +#### 实现 + +##### 底层结构 + +哈希类型的内部编码有两种:ziplist(压缩列表)、hashtable(哈希表、字典) + +当存储的数据量比较小的情况下,Redis 才使用压缩列表来实现字典类型,具体需要满足两个条件: + +- 当哈希类型元素个数小于 hash-max-ziplist-entries 配置(默认512个) +- 所有值都小于 hash-max-ziplist-value 配置(默认64字节) + +ziplist 使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比 hashtable 更加优秀,当 ziplist 无法满足哈希类型时,Redis 会使用 hashtable 作为哈希的内部实现,因为此时 ziplist 的读写效率会下降,而hashtable的读写时间复杂度为O(1) + + + +**** + + + +##### 压缩列表 + + 压缩列表(ziplist)是列表和哈希的底层实现之一,压缩列表用来紧凑数据存储,节省内存: + + + +压缩列表是由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结枃,一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值 + + + +*** + + + +##### 哈希表 + +Redis 字典使用散列表为底层实现,一个散列表里面有多个散列表节点,每个散列表节点就保存了字典中的一个键值对,发生哈希冲突采用链表法解决 @@ -8318,6 +8402,57 @@ list类型:保存多个数据,底层使用**双向链表**存储结构实现 +**** + + + +#### 实现 + +##### 底层结构 + +在 Redis3.2 版本以前列表类型的内部编码有两种:ziplist(压缩列表)和 linkedlist(链表),在 Redis3.2版本 以后对列表数据结构进行了改造,使用 quicklist(快速列表)代替了 ziplist 和 linkedlist + + + +**** + + + +##### 链表结构 + +Redis 链表为双向无环链表,使用 listNode 结构表示 + +```c +typedef struct listNode +{ + // 前置节点 + struct listNode *prev; + // 后置节点 + struct listNode *next; + // 节点的值 + void *value; +} listNode; +``` + +![](https://gitee.com/seazean/images/raw/master/DB/Redis-链表数据结构.png) + +- 双向:链表节点带有前驱、后继指针,获取某个节点的前驱、后继节点的时间复杂度为O(1) +- 无环:链表为非循环链表,表头节点的前驱指针和表尾节点的后继指针都指向NULL,对链表的访问以 NULL 为终点 + + + +*** + + + +##### 快速列表 + +quicklist 实际上是 ziplist 和 linkedlist 的混合体,将 linkedlist 按段切分,每一段使用 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串接起来 + +![](https://gitee.com/seazean/images/raw/master/DB/Redis-快速列表数据结构.png) + + + *** @@ -8415,6 +8550,24 @@ set类型:与hash存储结构完全相同,仅存储键不存储值(nil) +*** + + + +#### 实现 + +集合类型的内部编码有两种: + +* intset(整数集合):当集合中的元素都是整数且元素个数小于set-maxintset-entries配置(默认512个)时,Redis 会选用 intset 来作为集合的内部实现,从而减少内存的使用 + +* hashtable(哈希表):当集合类型无法满足 intset 条件时,Redis会使用 hashtable 作为集合的内部实现 + +整数集合(intset)是 Redis 用于保存整数值的集合抽象数据结构,可以保存类型为 int16_t、int32_t 或者 int64_t的整数值,并且保证集合中的元素是有序不重复的 + + + + + *** @@ -8500,6 +8653,49 @@ sorted_set类型:在set的存储结构基础上添加可排序字段,类似 +#### 实现 + +##### 底层结构 + +有序集合是由 ziplist(压缩列表)或 skiplist(跳跃表)组成的 + +当数据比较少时,有序集合使用的是 ziplist 存储的,使用 ziplist 格式存储需要满足以下两个条件: + +- 有序集合保存的元素个数要小于 128 个; +- 有序集合保存的所有元素成员的长度都必须小于 64 字节 + + + +*** + + + +##### 跳跃表 + +Redis 使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的**元素数量比较多**,又或者有序集合中元素的**成员是比较长的字符串**时,Redis就会使用跳跃表来作为有序集合健的底层实现 + +跳跃表在链表的基础上增加了多级索引以提升查找的效率,索引是占内存的,所以是一个空间换时间的方案。原始链表中存储的有可能是很大的对象,而索引结点只需要存储关键值值和几个指针,并不需要存储对象,因此当节点本身比较大或者元素数量比较多的时候,其优势可以被放大,而缺点则可以忽略 + +* 基于单向链表加索引的方式实现 + +- Redis 的跳跃表实现由 zskiplist 和 zskiplistnode 两个结构组成,其中 zskiplist 用于保存跳跃表信息(比如表头节点、表尾节点、长度),而 zskiplistnode 则用于表示跳跃表节点 +- Redis 每个跳跃表节点的层高都是 1 至 32 之间的随机数 +- 在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的成员对象必须是唯一的。跳跃表中的节点按照分值大小进行排序,当分值相同时节点按照成员对象的大小进行排序 + +![](https://gitee.com/seazean/images/raw/master/DB/Redis-跳跃表数据结构.png) + + + +个人笔记:Java → JUC → 并发包 → ConcurrentSkipListMap详解跳跃表 + +参考文章:https://www.cnblogs.com/hunternet/p/11248192.html + + + +*** + + + ### Bitmaps #### 布隆过滤 @@ -8970,7 +9166,7 @@ bgsave指令工作原理: 流程:当执行 bgsave 的时候,客户端发出 bgsave 指令给到 redis 服务器,服务器返回后台已经开始执行的信息给客户端,同时使用 fork函数 创建一个子进程,让这个子进程去执行save相关的操作,创建RDB文件保存起来,操作完以后把结果返回。 -本质上bgsave的过程分成两个过程:第一个是服务端收到指令直接告诉客户端开始执行;另外一个过程是一个子进程在完成后台的保存操作,操作完以后返回消息,两个进程不相互影响 +bgsave分成两个过程:第一个是服务端收到指令直接告诉客户端开始执行;另外一个过程是一个子进程在完成后台的保存操作,操作完以后返回消息。**两个进程不相互影响**,所以在持久化期间Redis可以正常工作 注意:bgsave命令是针对save阻塞问题做的优化,Redis内部所有涉及到RDB操作都采用bgsave的方式,save命令可以放弃使用 diff --git a/Java.md b/Java.md index 780c477..5c40005 100644 --- a/Java.md +++ b/Java.md @@ -31,7 +31,7 @@ Java语言提供了八种基本类型。六种数字类型(四个整数型, **byte:** -- byte 数据类型是8位、有符号的,以**二进制补码**表示的整数;**8位一个字节** +- byte 数据类型是8位、有符号的,以**二进制补码**表示的整数,**8位一个字节**,首位是符号位 - 最小值是 **-128(-2^7)** - 最大值是 **127(2^7-1)** - 默认值是 **`0`** @@ -539,6 +539,8 @@ public class Test1 { switch 不支持 long、float、double,switch 的设计初衷是对那些只有少数几个值的类型进行等值判断,如果值过于复杂,那么用 if 比较合适 +* break:跳出一层循环 + * 移位运算 计算机里一般用**补码表示数字**,正数、负数的表示区别就是最高位是0还是1 @@ -559,7 +561,7 @@ public class Test1 { -100补码: 11111111 11111111 11111111 10011100 ``` - 补码-->原码:符号位不变,其余位置取反加1 + 补码 --> 原码:符号位不变,其余位置取反加1 运算符: @@ -1158,7 +1160,7 @@ public class BinarySerach { 直接递归:自己的方法调用自己。 间接递归:自己的方法调用别的方法,别的方法又调用自己。 注意: - 递归如果控制的不恰当,会形成递归的死循环,从而导致栈内存溢出错误! + 递归如果控制的不恰当,会形成递归的死循环,从而导致栈内存溢出错误!cjxch @@ -4703,7 +4705,7 @@ LinkedHashSet底层依然是使用哈希表存储元素的,但是每个元素 ##### TreeSet -TreeSet集合自自排序的方式:(重点) +TreeSet集合自排序的方式: 1. 有值特性的元素直接可以升序排序。(浮点型,整型) 2. 字符串类型的元素会按照首字符的编号排序。 @@ -4721,7 +4723,6 @@ TreeSet集合自自排序的方式:(重点) 比较者大于被比较者 返回正数! 比较者小于被比较者 返回负数! 比较者等于被比较者 返回0! - ​ 注意:如果类和集合都带有比较规则,优先使用集合自带的比较规则。 @@ -4757,19 +4758,7 @@ public class Student implements Comparable{ } ``` - - -**** - - - -#### 比较器 - -用来排序,看TreeSet。 - -底层是以第一个元素为基准,加一个新元素,就会和第一个元素比,如果大于,就继续和大于的元素进行比较,直到遇到比新元素大的元素为止,放在该位置的左边。**红黑树** - - +比较器原理:底层是以第一个元素为基准,加一个新元素,就会和第一个元素比,如果大于,就继续和大于的元素进行比较,直到遇到比新元素大的元素为止,放在该位置的左边。(树) @@ -5549,8 +5538,8 @@ transient int size; 4. 红黑树节点调用的是getTreeNode方法通过树形节点的find方法进行查 - * 查找红黑树,由于之前添加时已经保证这个树是有序的了,因此查找时基本就是折半查找,效率更高。 - * 这里和插入时一样,如果对比节点的哈希值和要查找的哈希值相等,就会判断key是否相等,相等就直接返回,不相等就从子树中递归查找 + * 查找红黑树,之前添加时已经保证这个树是有序的,因此查找时就是折半查找,效率更高。 + * 这里和插入时一样,如果对比节点的哈希值相等并且通过equals判断值也相等,就会判断key相等,直接返回,不相等就从子树中递归查找 5. 时间复杂度 O(1) 若为树,则在树中通过key.equals(k)查找,**O(logn)** @@ -8150,7 +8139,7 @@ public class CommonsIODemo01 { 网络通信的三要素: 1. 协议:计算机网络客户端与服务端通信必须约定和彼此遵守的通信规则,HTTP、FTP、TCP、UDP、SMTP - + 2. IP地址:互联网协议地址(Internet Protocol Address),用来给一个网络中的计算机设备做唯一的编号 * IPv4 :4个字节,32位组成,192.168.1.1 @@ -8349,6 +8338,8 @@ IO 复用让单个进程具有处理多个 I/O 事件的能力,又被称为 Ev (待完善select和epoll函数,c语言函数) +select和poll差别不多,一个是数组一个是链表,因此poll的连接数理论上是不受限制的,而select是数组,数量受限。epoll多注册了一个函数ctrl事件监听。套接字是操作系统在管理,所以硬件的中断反馈给操作系统,进程从操作系统读取套接字的fd,所以这里有一个内存拷贝的过程,select和poll是轮询每个套接字,每次都要拷贝这个,而epoll则是在初始化拷贝,每次事件触发的时候直接响应,不用再复制。 + **** @@ -18328,10 +18319,10 @@ public final class Singleton { 不锁INSTANCE的原因: -* INSTANCE要重新赋值 -* INSTANCE是null,线程加锁之前需要获取对象的引用,null没有引用 +* INSTANCE 要重新赋值 +* INSTANCE 是null,线程加锁之前需要获取对象的引用,null没有引用 -实现特点: +实现特点: * 懒惰初始化 * 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁 @@ -18339,6 +18330,10 @@ public final class Singleton { +*** + + + ##### DCL问题 getInstance 方法对应的字节码为: @@ -18383,6 +18378,8 @@ getInstance 方法对应的字节码为: +*** + ##### 解决方法 @@ -18395,16 +18392,6 @@ getInstance 方法对应的字节码为: private static volatile SingletonDemo INSTANCE = null; ``` -可见性: - -* 写屏障(sfence)保证在该屏障之前的 t1 对共享变量的改动,都同步到主存当中 -* 读屏障(lfence)保证在该屏障之后 t2 对共享变量的读取,加载的是主存中最新数据 - -有序性: - -* 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后 -* 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前 - *** @@ -18721,6 +18708,8 @@ public final class Singleton { #### 实现原理 +无锁编程:lock free + CAS的全称是Compare-And-Swap,是**CPU并发原语** * CAS并发原语体现在Java语言中就是sun.misc.Unsafe类的各个方法,调用UnSafe类中的CAS方法,JVM会实现出CAS汇编指令,这是一种完全依赖于硬件的功能,通过它实现了原子操作 @@ -19840,12 +19829,12 @@ static class Entry extends WeakReference> { ``` - ThreadLocalMap使用**线性探测法**来解决哈希冲突: + ThreadLocalMap使用**线性探测法来解决哈希冲突**: * 该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出 -* 在探测过程中 ThreadLocal 会释放 key 为 NULL,value 不为 NULL 的脏 Entry对象,防止内存泄漏 + * 在探测过程中 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 @@ -20878,8 +20867,6 @@ ForkJoinPool 实现了**工作窃取算法**来提高 CPU 的利用率: - - *** @@ -21225,6 +21212,10 @@ ReentrantLock相对于 synchronized 它具备如下特点: +*** + + + #### 使用锁 构造方法:`ReentrantLock lock = new ReentrantLock();` @@ -22831,7 +22822,7 @@ 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(t1.node, 0 ) 时,因为不满足 propagate > 0(剩余资源量 == 0),从而不会唤醒后继结点, **T2 线程得不到唤醒** +* T1 获取信号量成功,调用 setHeadAndPropagate(t1.node, 0) 时,因为不满足 propagate > 0(剩余资源量 == 0),从而不会唤醒后继结点, **T2 线程得不到唤醒** @@ -23057,7 +23048,7 @@ class ThreadB extends Thread{ ### ConHashMap -#### Map背景 +#### 并发集合 ##### 集合对比 @@ -23120,7 +23111,7 @@ public class AddMapDataThread extends Thread{ ##### 并发死链 -JDK1.7的HashMap采用的头插法(拉链法)进行节点的添加,HashMap的扩容长度为原来的 2 倍 +JDK1.7的 HashMap 采用的头插法(拉链法)进行节点的添加,HashMap 的扩容长度为原来的 2 倍 resize() 中 节点(Entry)转移的源代码: @@ -23958,6 +23949,610 @@ public boolean add(E e) { +*** + + + +### SkipListMap + +#### 底层结构 + +跳表 SkipList 是一个**有序的链表**,默认升序,底层是链表加多级索引的结构。跳表可以对元素进行快速查询,类似于平衡树,是一种利用空间换时间的算法 + +对于单链表,即使链表是有序的,如果查找数据也只能从头到尾遍历链表,所以采用链表上建索引的方式提高效率,跳表的查询时间复杂度是 **O(logn)**, + +ConcurrentSkipListMap 提供了一种线程安全的并发访问的排序映射表,内部是跳表结构实现,通过 CAS + volatile 保证线程安全 + +平衡树和跳表的区别: + +* 对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整;而对跳表的插入和删除,只需要对整个数据结构的局部进行操作 +* 在高并发的情况下,保证整个平衡树的线程安全需要一个全局锁;对于跳表则只需要部分锁,拥有更好的性能 + +![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentSkipListMap数据结构.png) + +BaseHeader存储数据,headIndex存储索引,纵向上**所有索引指向链表最下面的节点** + + + +*** + + + +#### 成员变量 + +* 标识索引头节点位置 + + ```java + private static final Object BASE_HEADER = new Object(); + ``` + +* 跳表的顶层索引 + + ```java + private transient volatile HeadIndex head; + ``` + +* 比较器,为 null 则使用自然排序 + + ```java + final Comparator comparator; + ``` + +* Node 节点 + + ```java + static final class Node{ + final K key; // key 是 final 的, 说明节点一旦定下来, 除了删除, 不然不会改动 key + volatile Object value; // 对应的 value + volatile Node next; // 下一个节点 + } + ``` + +* 索引节点 Index + + ```java + static class Index{ + final Node node; // 索引指向的节点, + final Index down; // 下边level层的Index,分层索引 + volatile Index right; // 右边的Index + + // 在index本身和succ之间插入一个新的节点newSucc + final boolean link(Index succ, Index newSucc){ + Node n = node; + newSucc.right = succ; + return n.value != null && casRight(succ, newSucc); + } + + // 将当前的节点 index 设置其的 right 为 succ.right 等于删除 succ 节点 + final boolean unlink(Index succ){ + return node.value != null && casRight(succ, succ.right); + } + } + ``` + +* 头索引节点 HeadIndex + + ```java + static final class HeadIndex extends Index { + final int level;// 标示索引层级,所有的HeadIndex都指向同一个Base_header节点 + HeadIndex(Node node, Index down, Index right, int level) { + super(node, down, right); + this.level = level; + } + } + ``` + + + +*** + + + +#### 成员方法 + +##### 其他方法 + +* 构造方法: + + ```java + public ConcurrentSkipListMap() { + this.comparator = null; // comparator为null,使用key的自然序,如字典序 + initialize(); + } + ``` + + ```java + private void initialize() { + keySet = null; + 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); + } + ``` + +* cpr:排序 + + ```java + // x是比较者,y是被比较者,比较者大于被比较者 返回正数,小于返回负数,相等返回0 + static final int cpr(Comparator c, Object x, Object y) { + return (c != null) ? c.compare(x, y) : ((Comparable)x).compareTo(y); + } + ``` + + + +*** + + + +##### 添加方法 + +* findPredecessor():寻找前驱节点 + + 从最上层的头索引开始向右查找(链表的后续索引),如果后续索引的节点的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节点 + for (Index q = head, r = q.right, d;;) { + //2.右索引节点不为空,则进行向下查找 + if (r != null) { + Node n = r.node; + K k = n.key; + //3.n.value为null说明节点n正在删除的过程中 + if (n.value == null) { + //在index层直接删除r节点,用在删除节点中 + if (!q.unlink(r)) + break;//重新从 head 节点开始查找,break到步骤1 + //删除节点r成功,获取新的r节点, 回到步骤 2 + //还是从这层索引开始向右遍历, 直到 r == null + r = q.right; + continue; + } + //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,是则说明已经到了数据层,也就是第一层 + if ((d = q.down) == null) + return q.node; + //6.未到数据层, 进行重新赋值向下扫描 + q = d; //q指向d + r = d.right;//r指向q的后续索引节点 + } + } + } + ``` + + ```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() + + ```java + public V put(K key, V value) { + // 非空判断,value不能为空 + if (value == null) + throw new NullPointerException(); + return doPut(key, value, false); + } + ``` + + ```java + private V doPut(K key, V value, boolean onlyIfAbsent) { + Node z; + if (key == null)// 非空判断,key不能为空 + throw new NullPointerException(); + Comparator cmp = comparator; + // outer循环,处理并发冲突等其他需要重试的情况 + outer: for (;;) { + //0 + //1.将 key 对应的前继节点找到, b为前继节点, n是前继节点的next, + // 若没发生条件竞争,最终key在b与n之间 (找到的b在base_level上) + for (Node b = findPredecessor(key, cmp), n = b.next;;) { + // 2.n不为null时b不是链表的最后一个节点 + if (n != null) { + Object v; int c; + //3.获取 n 的右节点 + Node f = n.next; + //4.条件竞争( + // 并发下其他线程在b之后插入节点或直接删除节点n, break到步骤0 + if (n != b.next) + break; + // 若节点n已经删除, 则调用helpDelete进行帮助删除 + if ((v = n.value) == null) { + n.helpDelete(b, f); + break; + } + //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之间 + if ((c = cpr(cmp, key, n.key)) > 0) { + b = n; + n = f; + continue; + } + //7.key的值和n.key相等,则可以直接覆盖赋值 + if (c == 0) { + // onlyIfAbsent默认false, + if (onlyIfAbsent || n.casValue(v, value)) { + @SuppressWarnings("unchecked") V vv = (V)v; + return vv;//返回被覆盖的值 + } + // cas失败,返回0,重试 + break; + } + // else c < 0; fall through + } + //8.此时的情况n.key > key > b.key,对应流程图1中的7 + // 创建z节点指向n + z = new Node(key, value, n); + //9.尝试把b.next从n设置成z + if (!b.casNext(n, z)) + // cas失败,返回到步骤0,重试 + break; + //10.break outer后, 上面的for循环不会再执行, 而后执行下面的代码 + break outer; + } + } + // 以上插入节点已经完成,剩下的任务要根据随机数的值来表示是否向上增加层数与上层索引 + // 随机数 + int rnd = ThreadLocalRandom.nextSecondarySeed(); + + //如果随机数的二进制与10000000000000000000000000000001进行与运算为0 + //即随机数的二进制最高位与最末尾必须为0,其他位无所谓,就进入该循环 + //如果随机数的二进制最高位与最末位不为0,不增加新节点的层数 + //11.判断是否需要添加level + if ((rnd & 0x80000001) == 0) { + //索引层level,从1开始 + int level = 1, max; + //12.判断最低位前面有几个1,有几个leve就加几:0..0 0001 1110,这是4个,则1+4=5 + while (((rnd >>>= 1) & 1) != 0) + ++level; + Index idx = null;//最终指向z节点,就是添加的节点 + HeadIndex h = head;//指向头索引节点 + //13.判断level是否比当前最高索引小,图中max为3 + if (level <= (max = h.level)) { + for (int i = 1; i <= level; ++i) + //根据层数level不断创建新增节点的上层索引,索引的后继索引留空 + //第一次idx为null,也就是下层索引为空,第二次把上次的索引作为下层索引 + idx = new Index(z, idx, null); + // 循环以后的索引结构 + // index-3 ← idx + // ↓ + // index-2 + // ↓ + // index-1 + // ↓ + // z-node + } + //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了 + for (int i = 1; i <= level; ++i) + idxs[i] = idx = new Index(z, idx, null); + // index-4 ← idx + // ↓ + // index-3 + // ↓ + // index-2 + // ↓ + // index-1 + // ↓ + // z-node + + for (;;) { + h = head; + //获取头索引的层数 + int oldLevel = h.level; + // 如果level <= oldLevel,说明其他线程进行了index层增加操作,退出循环 + if (level <= oldLevel) + break; + //定义一个新的头索引节点 + HeadIndex newh = h; + //获取头索引的节点,就是BASE_HEADER + Node oldbase = h.node; + // 升级baseHeader索引,升高一级,并发下可能升高多级 + for (int j = oldLevel+1; j <= level; ++j) + newh = new HeadIndex(oldbase, newh, idxs[j], j); + // 执行完for循环之后,baseHeader 索引长这个样子.. + // index-4 → index-4 ← idx + // ↓ ↓ + // index-3 index-3 + // ↓ ↓ + // index-2 index-2 + // ↓ ↓ + // index-1 index-1 + // ↓ ↓ + // baseHeader → .... → z-node + + //cas成功后,map.head字段指向最新的headIndex,baseHeader的index-4s + if (casHead(h, newh)) { + //h指向最新的 index-4 节点 + h = newh; + //idx指向z-node的index-3节点, + //因为从index-3-index-1的这些z-node索引节点 都没有插入到索引链表 + idx = idxs[level = oldLevel]; + break; + } + } + } + //15.把新加的索引插入索引链表中,有上述两种情况,一种索引高度不变,另一种是高度加1 + splice: for (int insertionLevel = level;;) { + //获取头索引的层数, 情况1是3,情况2是4 + int j = h.level; + for (Index q = h, r = q.right, t = idx;;) { + //如果头索引为null或者新增节点索引为null,退出插入索引的总循环 + if (q == null || t == null) + //此处表示有其他线程删除了头索引或者新增节点的索引 + break splice; + //头索引的链表后续索引存在,如果是新层则为新节点索引,如果是老层则为原索引 + if (r != null) { + //获取r的节点 + Node n = r.node; + //插入的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,向右扫描 + if (c > 0) { + q = r; + r = r.right; + continue; + } + } + // 执行到这里,说明key < n.key,判断是否第j层插入新增节点的前置索引 + if (j == insertionLevel) { + // 将新索引节点t插入q r之间 + if (!q.link(r, t)) + break; + //如果新增节点的值为null,表示该节点已经被其他线程删除 + if (t.node.value == null) { + findNode(key); + break splice; + } + // 插入层逐层自减,当为最底层时退出循环 + if (--insertionLevel == 0) + break splice; + } + //其他节点随着插入节点的层数下移而下移 + if (--j >= insertionLevel && j < level) + t = t.down; + q = q.down; + r = q.right; + } + } + } + return null; + } + ``` + +* findNode() + + ```java + private Node findNode(Object key) { + //原理与doGet相同,无非是findNode返回节点,doGet返回 + //寻找到 key 返回 + if ((c = cpr(cmp, key, n.key)) == 0) + return n; + } + ``` + +* helpDelete + + ```java + void helpDelete(Node b, Node f) { + //this节点的后续节点为f,且本身为b的后续节点,一般都是正确的,除非被别的线程删除 + if (f == next && this == b.next) { + //如果f节点为null,说明this是尾节点,f节点值被其他线程修改, + if (f == null || f.value != f) + //通过CAS生成一个key为null,value为this,next为f + casNext(f, new Node(f)); + else + //如果f不为空,通过CAS,将f.next替换掉this节点,即删除本身节点 + b.casNext(this, f.next); + } + } + ``` + + + +*** + + + +##### 获取方法 + +* get(key) + + 寻找 key 的前继节点 b (这时b.next = null || b.next > key, 则说明不存key对应的 Node) + + 接着就判断 b, b.next 与 key之间的关系(其中有些 helpDelete操作) + + ```java + public V get(Object key) { + return doGet(key); + } + ``` + +* doGet() + + ```java + private V doGet(Object key) { + if (key == null) + throw new NullPointerException(); + Comparator cmp = comparator; + outer: for (;;) { + //1.找到最底层节点的前置节点 + for (Node b = findPredecessor(key, cmp), n = b.next;;) { + Object v; int c; + //2.如果该前置节点的链表后续节点为null,说明不存在该节点 + if (n == null) + break outer; + //b → n → f + Node f = n.next; + //3.如果n不为前置节点的后续节点,表示已经有其他线程删除了该节点 + if (n != b.next) + break; + //4.如果后续节点的值为null,删除该节点 + if ((v = n.value) == null) { + n.helpDelete(b, f); + break; + } + //5.如果前置节点已被其他线程删除,重新循环 + if (b.value == null || v == n) + break; + //6.如果要获取的key与后续节点的key相等,返回节点的value + if ((c = cpr(cmp, key, n.key)) == 0) { + @SuppressWarnings("unchecked") V vv = (V)v; + return vv; + } + //7.key < n.key,说明被其他线程删除了,或者不存在该节点 + if (c < 0) + break outer; + b = n; + n = f; + } + } + return null; + } + ``` + + + +**** + + + +##### 删除方法 + +* remove() + + ```java + public V remove(Object key) { + return doRemove(key, null); + } + final V doRemove(Object key, Object value) { + if (key == null) + throw new NullPointerException(); + Comparator cmp = comparator; + outer: for (;;) { + //1.找到最底层目标节点的前置节点,b.key < key + for (Node b = findPredecessor(key, cmp), n = b.next;;) { + Object v; int c; + //2.如果该前置节点的链表后续节点为null,退出循环 + if (n == null) + break outer; + //b → n → f + Node f = n.next; + if (n != b.next) // inconsistent read + break; + if ((v = n.value) == null) { // n is deleted + n.helpDelete(b, f); + break; + } + if (b.value == null || v == n) // b is deleted + break; + //3.key < n.key,说明被其他线程删除了,或者不存在该节点 + if ((c = cpr(cmp, key, n.key)) < 0) + break outer; + //4.key > n.key,继续向后扫描 + if (c > 0) { + b = n; + n = f; + continue; + } + //5.到这里是 key = n.key,value是n.value + if (value != null && !value.equals(v)) + break outer; + //6.把n节点的value置空 + if (!n.casValue(v, null)) + break; + //7.给n添加一个删除标志mark,mark.next=f,然后把b.next设置为f,成功后n出队 + if (!n.appendMarker(f) || !b.casNext(n, f)) + //对key对应的index进行删除 + findNode(key); + else { + //进行操作失败后通过findPredecessor中进行index的删除 + findPredecessor(key, cmp); + if (head.right == null) + //进行headIndex 对应的index 层的删除 + tryReduceLevel(); + } + @SuppressWarnings("unchecked") V vv = (V)v; + return vv; + } + } + return null; + } + ``` + + 经过 findPredecessor() 中的 unlink() 后索引已经被删除 + + ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentSkipListMap-remove流程.png) + +* tryReduceLevel() + + ```java + private void tryReduceLevel() { + HeadIndex h = head; + HeadIndex d; + HeadIndex e; + if (h.level > 3 && + (d = (HeadIndex)h.down) != null && + (e = (HeadIndex)d.down) != null && + e.right == null && + d.right == null && + h.right == null && + //设置头索引 + casHead(h, d) && + //重新检查 + h.right != null) + //重新检查返回true,说明其他线程增加了索引层级,把索引头节点设置回来 + casHead(d, h); + } + ``` + + + +参考文章:https://my.oschina.net/u/3768341/blog/3135659 + +参考视频:https://www.bilibili.com/video/BV1Er4y1P7k1 + + + + + *** From c5d45b9dea86c699133592ca7830dfbca7c0d7ee Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 31 May 2021 21:20:26 +0800 Subject: [PATCH 031/242] Update Java Notes --- DB.md | 2 +- Java.md | 242 ++++++++++++++++++++++++++++++++++++++------------------ 2 files changed, 168 insertions(+), 76 deletions(-) diff --git a/DB.md b/DB.md index 9b78e06..ac5c0be 100644 --- a/DB.md +++ b/DB.md @@ -9564,7 +9564,7 @@ fork()调用之后父子进程的内存关系 * 对于父进程的数据段,堆段,栈段中的各页,由于父子进程要相互独立,采用**写时复制**的技术,来最大化的提高内存以及内核的利用率 - 在fork之后两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,**两者的虚拟空间不同,但其对应的物理空间是同一个**。当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间,而代码段继续共享父进程的物理空间(两者的代码完全相同);而如果两者执行的代码不同,子进程的代码段也会分配单独的物理空间。 + 在fork之后两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,**两者的虚拟空间不同,但其对应的物理空间是同一个**。当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间,如果两者的代码完全相同,代码段继续共享父进程的物理空间;而如果两者执行的代码不同,子进程的代码段也会分配单独的物理空间。 fork之后内核会将子进程放在队列的前面,让子进程先执行,以免父进程执行导致写时复制,而后子进程再执行,因无意义的复制而造成效率的下降 diff --git a/Java.md b/Java.md index 5c40005..588223d 100644 --- a/Java.md +++ b/Java.md @@ -3974,14 +3974,16 @@ public class RegexDemo { ### 基本介绍 -什么是集合? - 集合是一个大小可变的容器,容器中的每个数据称为一个元素。数据==元素 - 集合特点:类型可以不确定,大小不固定;集合有很多,不同的集合特点和使用场景不同 - 数组:类型和长度一旦定义出来就都固定 +集合是一个大小可变的容器,容器中的每个数据称为一个元素 -集合有啥用? - 在开发中,很多时候元素的个数是不确定的。 - 而且经常要进行元素的增删该查操作,集合都是非常合适的。开发中集合用的更多!! +集合特点:类型可以不确定,大小不固定;集合有很多,不同的集合特点和使用场景不同 + +数组:类型和长度一旦定义出来就都固定 + +作用: + +* 在开发中,很多时候元素的个数是不确定的 +* 而且经常要进行元素的增删该查操作,集合都是非常合适的,开发中集合用的更多 @@ -4027,6 +4029,10 @@ Java常见的数据结构有哪些? +*** + + + #### 二叉树 二叉树中,任意一个节点的度要小于等于2 @@ -4036,6 +4042,12 @@ Java常见的数据结构有哪些? ![二叉树结构图](https://gitee.com/seazean/images/raw/master/Java/二叉树结构图.png) + + +**** + + + #### 二叉查找树 + 二叉查找树,又称二叉排序树或者二叉搜索树 @@ -4053,6 +4065,12 @@ Java常见的数据结构有哪些? ![二叉查找树添加节点规则](https://gitee.com/seazean/images/raw/master/Java/二叉查找树添加节点规则.png) + + +*** + + + #### 平衡二叉树 平衡二叉树的特点 @@ -4094,6 +4112,10 @@ Java常见的数据结构有哪些? +*** + + + #### 红黑树 + 红黑树的特点 @@ -4769,8 +4791,8 @@ public class Student implements Comparable{ #### 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)` : 打乱集合顺序。 `public static void sort(List list)` : 将集合中元素按照默认规则排序。 @@ -5590,7 +5612,39 @@ LinkedHashMap是HashMap的子类 void afterNodeInsertion(boolean evict) {} ``` -* get方法 +* put() + + ```java + // 调用父类HashMap的put方法 + public V put(K key, V value) { + return putVal(hash(key), key, value, false, true); + } + final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) + → afterNodeInsertion(evict);// evict为true + ``` + + afterNodeInsertion方法,当 removeEldestEntry() 方法返回 true 时会移除最近最久未使用的节点,也就是链表首部节点 first + + ```java + void afterNodeInsertion(boolean evict) { + LinkedHashMap.Entry first; + // evict 只有在构建 Map 的时候才为 false,这里为 true + if (evict && (first = head) != null && removeEldestEntry(first)) { + K key = first.key; + removeNode(hash(key), key, null, false, true);//移除头节点 + } + } + ``` + + removeEldestEntry() 默认为 false,如果需要让它为 true,需要继承 LinkedHashMap 并且覆盖这个方法的实现,在实现 LRU 的缓存中特别有用,通过移除最近最久未使用的节点,从而保证缓存空间足够,并且缓存的数据都是热点数据 + + ```java + protected boolean removeEldestEntry(Map.Entry eldest) { + return false; + } + ``` + +* get() 当一个节点被访问时,如果 accessOrder 为 true,则会将该节点移到链表尾部。也就是说指定为 LRU 顺序之后,在每次访问一个节点时会将这个节点移到链表尾部,那么链表首部就是最近最久未使用的节点 @@ -5642,32 +5696,16 @@ LinkedHashMap是HashMap的子类 } ``` -* afterNodeInsertion方法 - 当 removeEldestEntry() 方法返回 true 时会移除最近最久未使用的节点,也就是链表首部节点 first - - ```java - void afterNodeInsertion(boolean evict) { - LinkedHashMap.Entry first; - // evict 只有在构建 Map 的时候才为 false,在这里为 true - if (evict && (first = head) != null && removeEldestEntry(first)) { - K key = first.key; - removeNode(hash(key), key, null, false, true); - } - } - ``` - - removeEldestEntry() 默认为 false,如果需要让它为 true,需要继承 LinkedHashMap 并且覆盖这个方法的实现,在实现 LRU 的缓存中特别有用,通过移除最近最久未使用的节点,从而保证缓存空间足够,并且缓存的数据都是热点数据 - +* remove() + ```java - protected boolean removeEldestEntry(Map.Entry eldest) { - return false; - } + //调用HashMap的remove方法 + final Node removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) + → afterNodeRemoval(node); ``` - -* afterNodeRemoval方法 - - 当HashMap删除一个键值对时调用,会把在HashMap中删除的那个键值对一并从链表中删除,保证了哈希表和链表的一致性 - + + 当 HashMap 删除一个键值对时调用,会把在 HashMap 中删除的那个键值对一并从链表中删除 + ```java void afterNodeRemoval(Node e) { LinkedHashMap.Entry p = @@ -5711,7 +5749,7 @@ public static void main(String[] args) { cache.put(3, "c"); cache.get(1);//把1放入尾部 cache.put(4, "d"); - System.out.println(cache.keySet());//[3, 1, 4]只能存3个 + System.out.println(cache.keySet());//[3, 1, 4]只能存3个,移除2 } class LRUCache extends LinkedHashMap { @@ -5735,15 +5773,44 @@ class LRUCache extends LinkedHashMap { #### TreeMap -TreeMap集合按照键是可排序不重复的键值对集合(默认升序) -TreeMap集合和TreeSet集合都是排序不重复集合 -TreeSet集合的底层是基于TreeMap,只是键没有附属值而已 +TreeMap 实现了 SotredMap 接口,是有序不可重复的键值对集合,基于红黑树(Red-Black tree)实现,每个 key-value 都作为一个红黑树的节点。如果构造 TreeMap 没有指定比较器,则根据key执行自然排序(默认升序),如果指定了比较器则按照比较器来进行排序 + +TreeSet 集合的底层是基于TreeMap,只是键的附属值为空对象而已 TreeMap集合指定大小规则有2种方式: * 直接为对象的类实现比较器规则接口Comparable,重写比较方法(拓展方式) * 直接为集合设置比较器Comparator对象,重写比较方法 +成员属性: + +* Entry节点 + + ```java + static final class Entry implements Map.Entry { + K key; + V value; + Entry left; //左孩子节点 + Entry right; //右孩子节点 + Entry parent; //父节点 + boolean color = BLACK; //节点的颜色,在红黑树中只有两种颜色,红色和黑色 + } + ``` + +* compare() + + ```java + //如果comparator为null,采用comparable.compartTo进行比较,否则采用指定比较器比较大小 + final int compare(Object k1, Object k2) { + return comparator==null ? ((Comparable)k1).compareTo((K)k2) + : comparator.compare((K)k1, (K)k2); + } + ``` + + + +参考文章:https://blog.csdn.net/weixin_33991727/article/details/91518677 + *** @@ -7728,7 +7795,7 @@ public static void main(String[] args) throws Exception { 字符型缓冲流高效的原因: -* BufferedReader:每次调用read方法,只有第一次从磁盘中读取了8192(8k)个字符,存储到该类型对象的缓冲区数组中,将其中一个返回给调用者,再次调用read方法时,就不需要访问磁盘,直接从缓冲区中拿出一个数据即可,提升了效率 +* BufferedReader:每次调用read方法,只有第一次从磁盘中读取了8192(**8k**)个字符,存储到该类型对象的缓冲区数组中,将其中一个返回给调用者,再次调用read方法时,就不需要访问磁盘,直接从缓冲区中拿出一个数据即可,提升了效率 * BufferedWriter:每次调用write方法,不会直接将字符刷新到文件中,而是存储到字符数组中,等字符数组写满了,才一次性刷新到文件中,减少了和磁盘交互的次数,提升了效率 字节型缓冲流高效的原因: @@ -11805,9 +11872,9 @@ Return Address:存放调用该方法的PC寄存器的值 JNI:Java Native Interface,通过使用 Java本地接口书写程序,可以确保代码在不同的平台上方便移植 -* 不需要进行GC,与虚拟机栈类似,也是线程私有的,有StackOverFlowError和OutOfMemoryError异常 +* 不需要进行GC,与虚拟机栈类似,也是线程私有的,有 StackOverFlowError 和 OutOfMemoryError 异常 -* 虚拟机栈执行的是java方法,在hotSpot JVM中,直接将本地方法栈和虚拟机栈合二为一 +* 虚拟机栈执行的是java方法,在HotSpot JVM中,直接将本地方法栈和虚拟机栈合二为一 * 本地方法一般是由其他语言编写,并且被编译为基于本机硬件和操作系统的程序 @@ -12417,7 +12484,7 @@ public void localvarGC4() { ### 垃圾判断 -#### 垃圾收集 +#### 垃圾介绍 垃圾:**如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾** @@ -12751,7 +12818,7 @@ Java语言提供了对象终止(finalization)机制来允许开发人员提 算法缺点: -- 主要不足是只使用了内存的一半。 +- 主要不足是**只使用了内存的一半** - 对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销都不小 现在的商业虚拟机都采用这种收集算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间 @@ -13336,8 +13403,8 @@ JVM是通过栈帧中的对象引用访问到其内部的对象实例:(内 -* 直接指针(HotSpot采用) - 使用该方式,Java堆对象的布局必须考虑如何放置访问类型数据的相关信息,reference中直接存储的就是对象地址 +* 直接指针(HotSpot采用) + 使用该方式,Java 堆对象的布局必须考虑如何放置访问类型数据的相关信息,reference中直接存储的就是对象地址 优点:速度更快,**节省了一次指针定位的时间开销** @@ -13403,7 +13470,7 @@ Java对象创建时机: 4. 使用Clone方法创建对象:用clone方法创建对象的过程中并不会调用任何构造函数,要想使用clone方法,我们就必须先实现Cloneable接口并实现其定义的clone方法 -5. 使用(反)序列化机制创建对象:当反序列化一个对象时,JVM会创建一个单独的对象,在此过程中,JVM并不会调用任何构造函数,为了反序列化一个对象,需要让类实现Serializable接口 +5. 使用(反)序列化机制创建对象:当反序列化一个对象时,JVM会创建一个**单独的对象**,在此过程中,JVM并不会调用任何构造函数,为了反序列化一个对象,需要让类实现Serializable接口 从Java虚拟机层面看,除了使用new关键字创建对象的方式外,其他方式全部都是通过转变为invokevirtual指令直接创建对象的 @@ -13566,7 +13633,7 @@ Java对象创建时机: 加载过程完成以下三件事: -- 通过类的完全限定名称获取定义该类的二进制字节流 +- 通过类的完全限定名称获取定义该类的二进制字节流(二进制字节码) - 将该字节流表示的**静态存储结构转换为方法区的运行时存储结构**(运行时常量池) - 在内存中生成一个代表该类的 Class 对象,作为方法区中该类各种数据的访问入口 @@ -13809,18 +13876,18 @@ init指的是实例构造器,主要作用是在类实例化过程中执行, 从 Java 开发人员的角度看: * **启动类加载器(Bootstrap ClassLoader)**: - * 处于安全考虑,BootStrap启动类加载器只加载包名为java、javax、sun等开头的类 - * 类加载器负责加载在 `JAVA_HOME/jre/lib `或 `sun.boot.class.path` 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的类,并且是虚拟机识别的类库加载到虚拟机内存中 - * 仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载 + * 处于安全考虑,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 + * 由 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() 方法的返回值,因此称为系统类加载器 + * 由 AppClassLoader(sun.misc.Launcher$AppClassLoader) 实现,上级为 Extension + * 负责加载环境变量 classpath 或系统属性 `java.class.path` 指定路径下的类库 + * 这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此称为系统类加载器 * 可以直接使用这个类加载器,如果应用程序中没有自定义类加载器,这个就是程序中默认的类加载器 * 自定义类加载器:由开发人员自定义的类加载器,上级是Application @@ -13855,7 +13922,7 @@ public static void main(String[] args) { -#### 抽象类 +#### 加载类 ClassLoader类,是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器) @@ -13885,16 +13952,18 @@ ClassLoader类常用方法: #### 加载模型 -##### 三种模型 +##### 加载机制 在JVM中,对于类加载模型提供了三种,分别为全盘加载、双亲委派、缓存机制 -- **全盘加载:**当一个类加载器负责加载某个Class时,该Class所依赖和引用的其他Class也将由该类加载器负责载入,除非显示指定使用另外一个类加载器来载入 +- **全盘加载:**当一个类加载器负责加载某个 Class 时,该 Class 所依赖和引用的其他 Class 也将由该类加载器负责载入,除非显示指定使用另外一个类加载器来载入 -- **双亲委派:**先让父类加载器加载该Class,在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。简单来说就是,某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,**依次递归**,如果父加载器可以完成类加载任务,就成功返回;只有当父加载器无法完成此加载任务时,才自己去加载 +- **双亲委派:**先让父类加载器加载该 Class,在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。简单来说就是,某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,**依次递归**,如果父加载器可以完成类加载任务,就成功返回;只有当父加载器无法完成此加载任务时,才自己去加载 -- **缓存机制:**会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象存入缓冲区中 - - 这就是修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因 +- **缓存机制:**会保证所有加载过的 Class 都会被缓存,当程序中需要使用某个 Class 时,类加载器先从缓存区中搜寻该 Class,只有当缓存区中不存在该 Class 对象时,系统才会读取该类对应的二进制数据,并将其转换成 Class 对象存入缓冲区中 + - 这就是修改了 Class 后,必须重新启动 JVM,程序所做的修改才会生效的原因 + + @@ -13906,8 +13975,6 @@ ClassLoader类常用方法: 双亲委派模型 (Parents Delegation Model):该模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器,这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance) - - 工作过程:一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载 双亲委派机制的优点: @@ -13929,9 +13996,9 @@ ClassLoader类常用方法: ``` 此时执行main函数,会出现异常,在类 java.lang.String 中找不到 main 方法,防止恶意篡改核心API库 - 出现该信息是因为双亲委派的机制,java.lang.String的在启动类加载器(Bootstrap classLoader)得到加载,启动类加载器优先级更高,在核心jre库中有其相同名字的类文件,但该类中并没有main方法 + 出现该信息是因为双亲委派的机制,java.lang.String 的在启动类加载器(Bootstrap classLoader)得到加载,启动类加载器优先级更高,在核心 jre 库中有其相同名字的类文件,但该类中并没有 main 方法 -**源码分析:** +**源码分析: ** ```java protected Class loadClass(String name, boolean resolve) @@ -13950,7 +14017,6 @@ protected Class loadClass(String name, boolean resolve)           //父类加载器的loadClass方法,又会检查自己是否已经加载过 c = parent.loadClass(name, false); } else { - //当前类加载器没有父类加载器,说明当前类加载器是BootStrapClassLoader            //则调用BootStrap ClassLoader的方法加载类 c = findBootstrapClassOrNull(name); @@ -13981,17 +14047,45 @@ protected Class loadClass(String name, boolean resolve) +**** + + + +##### 破坏双亲 + +破坏双亲委派模型有两种方式: + +* 引入线程上下文类加载器 + + 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 Classloader 加载器拿到了 Application ClassLoader 加载器应该加载的类,破坏了双亲委派模型 + +* 自定义ClassLoader + + * 如果不想破坏双亲委派模型,只需要重写 findClass 方法 + * 如果想要去破坏双亲委派模型,需要去**重写 loadClass **方法 + + + +参考文章:https://www.jianshu.com/p/4132d82ca3a6 + + + *** #### 沙箱机制 -沙箱机制:将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,来保证对代码的有效隔离,防止对本地系统造成破坏 +沙箱机制:将 Java 代码限定在虚拟机特定的运行范围中,并且严格限制代码对本地系统资源访问,来保证对代码的有效隔离,防止对本地系统造成破坏 沙箱**限制系统资源访问**,包括CPU、内存、文件系统、网络,不同级别的沙箱对资源访问的限制也不一样 -举例:自定义String类,但是在加载自定义String类的时候会先使用引导类加载器加载,而引导类加载器在加载过程中会先加载jdk自带的文件(rt.jar包中的java\lang\String.class),报错信息说没有main方法就是因为加载的是rt.jar包中的String类,这样可以保证对java核心源代码的保护 +举例:自定义 String 类,但是在加载自定义 String 类的时候会先使用引导类加载器加载,而引导类加载器在加载过程中会先加载 jdk 自带的文件(rt.jar包中的 java\lang\String.class),报错信息说没有 main 方法就是因为加载的是 rt.jar 包中的 String 类,这样可以保证对 java 核心源代码的保护 @@ -14001,12 +14095,10 @@ protected Class loadClass(String name, boolean resolve) #### 自定义 -对于自定义类加载器的实现,只需要继承ClassLoader类,覆写findClass方法即可 +对于自定义类加载器的实现,只需要继承 ClassLoader 类,覆写 findClass 方法即可 作用:隔离加载类、修改类加载的方式、拓展加载源、防止源码泄漏 -java.lang.ClassLoader 的 loadClass() 实现了双亲委派模型的逻辑,自定义类加载器一般不去重写它,但是需要重写 findClass() 方法 - ```java //自定义类加载器,读取指定的类路径classPath下的class文件 public class MyClassLoader extends ClassLoader{ @@ -14154,13 +14246,13 @@ HotSpot VM采用的热点探测方式是基于计数器的热点探测,为每 #### 分层编译 -在HotSpot VM中内嵌有两个JIT编译器,分别为Client Compiler和Server Compiler,简称为C1编译器和C2编译器 +HotSpot VM中内嵌有两个JIT编译器,分别为Client Compiler和Server Compiler,简称为C1编译器和C2编译器 C1编译器会对字节码进行简单可靠的优化,耗时短,以达到更快的编译速度,C1编译器的优化方法: -* 方法内联:将引用的函数代码编译到引用点处,这样可以减少栈帧的生成,减少参数传递以及跳转过程 +* 方法内联:**将引用的函数代码编译到引用点处**,这样可以减少栈帧的生成,减少参数传递以及跳转过程 - 方法内联能够消除方法调用的固定开销。任何方法除非被内联,否则调用都会有固定开销,来源于保存程序在该方法中的执行位置,以及新建、压入和弹出新方法所使用的栈帧。 + 方法内联能够消除方法调用的固定开销,任何方法除非被内联,否则调用都会有固定开销,来源于保存程序在该方法中的执行位置,以及新建、压入和弹出新方法所使用的栈帧。 ```java private static int square(final int i) { @@ -14189,8 +14281,8 @@ C2编译器进行耗时较长的优化以及激进优化,优化的代码执行 VM 参数设置: -- -client:指定Java虚拟机运行在Client模式下,并使用C1编译器 -- -server:指定Java虚拟机运行在Server模式下,并使用C2编译器 +- -client:指定Java虚拟机运行在 Client 模式下,并使用C1编译器 +- -server:指定Java虚拟机运行在 Server 模式下,并使用C2编译器 分层编译策略 (Tiered Compilation):程序解释执行可以触发C1编译,将字节码编译成机器码。加上性能监控,C2编译会根据性能监控信息进行激进优化,JVM 将执行状态分成了 5 个层次: From 98b381db279364f1eb36533bd1591f77805679c1 Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 31 May 2021 23:48:49 +0800 Subject: [PATCH 032/242] Update Java Notes --- Java.md | 287 ++++++++++++++++++++++++++++++-------------------------- 1 file changed, 154 insertions(+), 133 deletions(-) diff --git a/Java.md b/Java.md index 588223d..968f366 100644 --- a/Java.md +++ b/Java.md @@ -867,8 +867,8 @@ public class MethodDemo { 枚举的特点: -* 枚举类是用final修饰的,枚举类不能被继承 -* 枚举类默认继承了java.lang.Enum枚举类 +* 枚举类是用 final 修饰的,枚举类不能被继承 +* 枚举类默认继承了 java.lang.Enum 枚举类 * 枚举类的第一行都是常量,必须是罗列枚举类的实例名称 * 枚举类相当于是多例设计模式 * 每个枚举项都是一个实例,是一个静态成员变量 @@ -1376,25 +1376,25 @@ Debug是供程序员使用的程序调试工具,它可以用于查看程序的 #### 定义 +定义格式 + ```java -定义类: - 格式:修饰符 class 类名{ - - } +修饰符 class 类名{ +} ``` -**1.类名的首字母建议大写。满足驼峰模式。 StudentNameCode** -2.一个Java代码中可以定义多个类,按照规范一个Java文件一个类 -3.一个Java代码文件中,只能有一个类是public修饰,**public修饰的类名必须成为当前Java代码的文件名称**。 +1. 类名的首字母建议大写,满足驼峰模式,比如 StudentNameCode +2. 一个 Java 代码中可以定义多个类,按照规范一个 Java 文件一个类 +3. 一个 Java 代码文件中,只能有一个类是 public 修饰,**public修饰的类名必须成为当前Java代码的文件名称** ```java 类中的成分:有且仅有五大成分 修饰符 class 类名{ - 1.成员变量(Field): 描述类或者对象的属性信息的。 - 2.成员方法(Method): 描述类或者对象的行为信息的。 - 3.构造器(Constructor): 初始化一个对象返回。 - 4.代码块(后面学习的) - 5.内部类(后面学习的) + 1.成员变量(Field): 描述类或者对象的属性信息的。 + 2.成员方法(Method): 描述类或者对象的行为信息的。 + 3.构造器(Constructor): 初始化一个对象返回。 + 4.代码块 + 5.内部类 } 类中有且仅有这五种成分,否则代码报错! public class ClassDemo { @@ -1410,7 +1410,7 @@ public class ClassDemo { #### 构造器 -构造器:格式: +构造器格式: ```java 修饰符 类名(形参列表){ @@ -1419,14 +1419,15 @@ public class ClassDemo { ``` 作用:初始化类的一个对象返回 + 分类:无参数构造器,有参数构造器 -注意:一个类默认自带一个无参数构造器,写了有参数构造器默认的无参数构造器就消失,还需要用无参数构造器就要自己重新写。 +注意:**一个类默认自带一个无参数构造器**,写了有参数构造器默认的无参数构造器就消失,还需要用无参数构造器就要重新写 -构造器初始化对象的格式:类名 对象名称 = new 构造器; +构造器初始化对象的格式:类名 对象名称 = new 构造器 -* 无参数构造器的作用:初始化一个类的对象(使用对象的默认值初始化)返回。 -* 有参数构造器的作用:初始化一个类的对象(可以在初始化对象的时候为对象赋值)返回。 +* 无参数构造器的作用:初始化一个类的对象(使用对象的默认值初始化)返回 +* 有参数构造器的作用:初始化一个类的对象(可以在初始化对象的时候为对象赋值)返回 @@ -1457,7 +1458,7 @@ public class ClassDemo { 封装的步骤: -1. **成员变量应该私有。用private修饰,只能在本类中直接访问。** +1. **成员变量应该私有,用private修饰,只能在本类中直接访问。** 2. **提供成套的getter和setter方法暴露成员变量的取值和赋值。** 为什么使用private修饰成员变量:实现数据封装,不想让别人使用修改你的数据,比较安全。 @@ -7390,21 +7391,19 @@ ObjectInputStream ObjectOutputStream FileInputStream文件字节输入流: -* 作用:以内存为基准,把磁盘文件中的数据按照字节的形式读入到内存中的流,就是按照字节读取文件数据到内存 +* 作用:以内存为基准,把磁盘文件中的数据按照字节的形式读入到内存中的流 * 构造器: `public FileInputStream(File path)` : 创建一个字节输入流管道与源文件对象接通 - `public FileInputStream(String pathName)` : 创建一个字节输入流管道与文件路径对接, - 底层实质上创建了File对象 - + `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) `: 从输入流中读取所有字节,并按读取的顺序 - 将字节写入给定的输出流(jdk9以后的新方法)`is.transferTo(os)` + `public long transferTo(OutputStream out) ` : 从输入流中读取所有字节,并按读取的顺序,将字节写入给定的输出流(jdk9以后的新方法) `is.transferTo(os)` ```java public class FileInputStreamDemo01 { @@ -7468,15 +7467,13 @@ System.out.println(rs); FileOutputStream文件字节输出流: -* 作用:以内存为基准,把内存中的数据,按照字节的形式写出到磁盘文件中去,就是把内存数据按照字节写出到磁盘文件中去 +* 作用:以内存为基准,把内存中的数据,按照字节的形式写出到磁盘文件中去 * 构造器: `public FileOutputStream(File file)` : 创建一个字节输出流管道通向目标文件对象 `public FileOutputStream(String file) ` : 创建一个字节输出流管道通向目标文件路径 - `public FileOutputStream(File file , boolean append)` : - 创建一个追加数据的字节输出流管道通向目标文件对象 - `public FileOutputStream(String file , boolean append) `: - 创建一个追加数据的字节输出流管道通向目标文件路径 + `public FileOutputStream(File file , boolean append)` : 追加数据的字节输出流管道到目标文件对象 + `public FileOutputStream(String file , boolean append)` : 创建一个追加数据的字节输出流管道通向目标文件路径 * API: `public void write(int a)` : 写一个字节出去 `public void write(byte[] buffer)` :写一个字节数组出去 @@ -7489,11 +7486,11 @@ FileOutputStream文件字节输出流: `OutputStream os = new FileOutputStream("Day09Demo/out05")` : 覆盖数据管道 `OutputStream os = new FileOutputStream("Day09Demo/out05" , true)` : 追加数据的管道 -小结: - 字节输出流只能写字节出去。 - 字节输出流默认是**覆盖**数据管道。 - 换行用: **os.write("\r\n".getBytes());** - 关闭和刷新:刷新流可以继续使用,关闭包含刷新数据但是流就不能使用了! +说明: + +* 字节输出流只能写字节出去,字节输出流默认是**覆盖**数据管道。 +* 换行用: **os.write("\r\n".getBytes());** +* 关闭和刷新:刷新流可以继续使用,关闭包含刷新数据但是流就不能使用了! ```java OutputStream os = new FileOutputStream("Day09Demo/out05"); @@ -7616,24 +7613,23 @@ 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"); // 换行 - 读写字符文件数据建议使用字符流。 + `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"); @@ -7667,10 +7663,12 @@ fw.close; ##### 字节缓冲输入流 字节缓冲输入流:BufferedInputStream - 作用:可以把低级的字节输入流包装成一个高级的缓冲字节输入流管道, 提高字节输入流读数据的性能。 - 构造器: `public BufferedInputStream(InputStream in)` - 原理:缓冲字节输入流管道自带了一个8KB的缓冲池,每次可以直接借用操作系统的功能最多提取8KB - 的数据到缓冲池中去,以后我们直接从缓冲池读取数据,所以性能较好 + +作用:可以把低级的字节输入流包装成一个高级的缓冲字节输入流管道, 提高字节输入流读数据的性能 + +构造器: `public BufferedInputStream(InputStream in)` + +原理:缓冲字节输入流管道自带了一个8KB的缓冲池,每次可以直接借用操作系统的功能最多提取 8KB 的数据到缓冲池中去,以后我们直接从缓冲池读取数据,所以性能较好 ```java public class BufferedInputStreamDemo01 { @@ -7695,9 +7693,12 @@ public class BufferedInputStreamDemo01 { ##### 字节缓冲输出流 字节缓冲输出流:BufferedOutputStream - 作用:可以把低级的字节输出流包装成一个高级的缓冲字节输出流,从而提高写数据的性能。 - 构造器:`public BufferedOutputStream(OutputStream os)` - 原理:缓冲字节输出流自带了8KB缓冲池,数据就直接写入到缓冲池中去,性能极高了! + +作用:可以把低级的字节输出流包装成一个高级的缓冲字节输出流,从而提高写数据的性能 + +构造器:`public BufferedOutputStream(OutputStream os)` + +原理:缓冲字节输出流自带了8KB缓冲池,数据就直接写入到缓冲池中去,性能极高了 ```java public class BufferedOutputStreamDemo02 { @@ -7728,20 +7729,21 @@ public class BufferedOutputStreamDemo02 { (3)使用高级的缓冲字节流按照一个一个字节的形式复制文件。 (4)使用高级的缓冲字节流按照一个一个字节数组的形式复制文件。 -结果: - 高级的缓冲字节流按照一个一个字节数组的形式复制文件,性能最高,建议使用 +高级的缓冲字节流按照一个一个字节数组的形式复制文件,性能最高,建议使用 ##### 字符缓冲输入流 字符缓冲输入流:BufferedReader - 作用:字符缓冲输入流把字符输入流包装成高级的缓冲字符输入流,可以提高字符输入流读数据的性能。 - 构造器:`public BufferedReader(Reader reader):` - 原理:缓冲字符输入流默认会有一个8K的字符缓冲池,可以提高读字符的性能。 -缓冲字符输入流还多了一个按照行读取数据的功能(**重点**): - `public String readLine()` : 读取一行数据返回,读取完毕返回null; +作用:字符缓冲输入流把字符输入流包装成高级的缓冲字符输入流,可以提高字符输入流读数据的性能。 + +构造器:`public BufferedReader(Reader reader)` + +原理:缓冲字符输入流默认会有一个8K的字符缓冲池,可以提高读字符的性能 + +按照行读取数据的功能:`public String readLine()` 读取一行数据返回,读取完毕返回null ```java public static void main(String[] args) throws Exception { @@ -7768,11 +7770,14 @@ public static void main(String[] args) throws Exception { ##### 字符缓冲输出流 符缓冲输出流:BufferedWriter - 作用:把低级的字符输出流包装成一个高级的缓冲字符输出流,提高写字符数据的性能。 - 构造器:`public BufferedWriter(Writer writer)` - 原理:高级的字符缓冲输出流多了一个8k的字符缓冲池,写数据性能极大提高了! -字符缓冲输出流多了一个换行的特有功能:`public void newLine()`:**新建一行**。 +作用:把低级的字符输出流包装成一个高级的缓冲字符输出流,提高写字符数据的性能。 + +构造器:`public BufferedWriter(Writer writer)` + + 原理:高级的字符缓冲输出流多了一个8k的字符缓冲池,写数据性能极大提高了 + +字符缓冲输出流多了一个换行的特有功能:`public void newLine()` **新建一行** ```java public static void main(String[] args) throws Exception { @@ -7832,12 +7837,14 @@ public static void main(String[] args) throws Exception { ##### 字符输入转换流 - 字符输入转换流InputStreamReader: - 作用:可以解决字符流读取不同编码乱码的问题。 - 可以把原始的**字节流**按照当前默认的代码编码或指定的编码**转换成字符输入流** - 构造器: - `public InputStreamReader(InputStream is)`:使用当前代码默认编码(UTF-8)转换成字符流 - `public InputStreamReader(InputStream is,String charset)` : 指定编码把字节流转换成字符流 +字符输入转换流InputStreamReader + +作用:解决字符流读取不同编码的乱码问题,把原始的**字节流**按照默认的编码或指定的编码**转换成字符输入流** + +构造器: + +* `public InputStreamReader(InputStream is)` : 使用当前代码默认编码(UTF-8)转换成字符流 +* `public InputStreamReader(InputStream is,String charset)` : 指定编码把字节流转换成字符流 ```java public class InputStreamReaderDemo{ @@ -7862,11 +7869,13 @@ public class InputStreamReaderDemo{ ##### 字符输出转换流 字符输出转换流:OutputStreamWriter - 作用:可以指定编码**把字节输出流转换成字符输出流**。 - 可以指定写出去的字符的编码。 - 构造器: - `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"); @@ -7883,23 +7892,25 @@ osw.close(); #### 序列化 -##### 概述 +##### 介绍 对象序列化:把Java对象转换成字节序列的过程,将对象写入到IO流中。 对象 => 文件中 -对象反序列化:把字节序列恢复为Java对象的过程,从IO流中恢复对象。 文件中 => 对象 -##### transient +对象反序列化:把字节序列恢复为Java对象的过程,从IO流中恢复对象。 文件中 => 对象 -transient修饰的成员变量,将不参与序列化! +transient 关键字修饰的成员变量,将不参与序列化! ##### 序列化 对象序列化流(对象字节输出流):ObjectOutputStream - 作用:把内存中的Java对象数据保存到文件中去。 - 构造器:`public ObjectOutputStream(OutputStream out)` - 序列化方法:`public final void writeObject(Object obj)` + +作用:把内存中的Java对象数据保存到文件中去 + +构造器:`public ObjectOutputStream(OutputStream out)` + +序列化方法:`public final void writeObject(Object obj)` 注意:对象如果想参与序列化,对象必须实现序列化接口 **implements Serializable** ,否则序列化失败! @@ -7936,12 +7947,15 @@ class User implements Serializable { ##### 反序列化 对象反序列化(对象字节输入流):ObjectInputStream - 作用:读取序列化的对象文件恢复到Java对象中。 - 构造器:`public ObjectInputStream(InputStream is)` - 方法:`public final Object readObject()` -序列化版本号:`private static final long serialVersionUID = 2L`; - 必须序列化使用的版本号和反序列化使用的版本号一致才可以正常反序列化,否则报错! +作用:读取序列化的对象文件恢复到Java对象中 + +构造器:`public ObjectInputStream(InputStream is)` + +方法:`public final Object readObject()` + +序列化版本号:`private static final long serialVersionUID = 2L` +说明:序列化使用的版本号和反序列化使用的版本号一致才可以正常反序列化,否则报错 ```java public class SerializeDemo02 { @@ -7968,24 +7982,20 @@ class User implements Serializable { #### 打印流 -打印流PrintStream / PrintWriter. +打印流 PrintStream / PrintWriter 打印流的作用: - 1.可以方便,快速的写数据出去。 - 2.可以实现打印什么类型,就是什么类型。 - **3.System.out.print() 底层基于打印流实现的** -打印流的构造器: - `public PrintStream(OutputStream os)`: - `public PrintStream(String filepath)`: +* 可以方便,快速的写数据出去,可以实现打印什么类型,就是什么类型 +* PrintStream/PrintWriter 不光可以打印数据,还可以写字节数据和字符数据出去 +* **System.out.print() 底层基于打印流实现的** + +构造器: -System: - public static void setOut(PrintStream out) : 让系统的输出流向打印流。 +* `public PrintStream(OutputStream os)` +* `public PrintStream(String filepath)` -结论: - 打印流可以方便,且高效的打印各种数据。 - PrintStream不光可以打印数据,还可以写"字节数据"出去。 - PrintWriter不光可以打印数据,还可以写"字符数据"出去。 +System:`public static void setOut(PrintStream out)` 让系统的输出流向打印流 ```java public class PrintStreamDemo01 { @@ -8000,12 +8010,12 @@ public class PrintStreamDemo01 { } public class PrintStreamDemo02 { public static void main(String[] args) throws Exception { - System.out.println("==itheima0=="); + System.out.println("==seazean0=="); PrintStream ps = new PrintStream("Day10Demo/src/log.txt"); - System.setOut(ps); // 让系统的输出流向打印流。 + System.setOut(ps); // 让系统的输出流向打印流 //不输出在控制台,输出到文件里 - System.out.println("==itheima1=="); - System.out.println("==itheima2=="); + System.out.println("==seazean1=="); + System.out.println("==seazean2=="); } } ``` @@ -8018,17 +8028,21 @@ public class PrintStreamDemo02 { ### Close -try-with-resources: - try( - // 这里只能放置资源对象,用完会自动调用close()关闭 - ){ +try-with-resources: + +```java +try( + // 这里只能放置资源对象,用完会自动调用close()关闭 +){ + +}catch(Exception e){ + e.printStackTrace(); +} +``` + +资源类一定是实现了 Closeable 接口,实现这个接口的类就是资源 -​ }catch(Exception e){ -​ e.printStackTrace(); -​ } -什么是资源? -​ 资源类一定是实现了Closeable接口,实现这个接口的类就是资源 -​ 有close()方法,try-with-resources会自动调用它的close()关闭资源。 +有 close() 方法,try-with-resources 会自动调用它的 close() 关闭资源 ```java try( @@ -11135,7 +11149,7 @@ person.xsd - targetNamespace="http://www.itheima.cn/javase" + targetNamespace="http://www.seazean.cn/javase" elementFormDefault="qualified" > @@ -11173,8 +11187,8 @@ person.xsd - xmlns="http://www.itheima.cn/javase" - xsi:schemaLocation="http://www.itheima.cn/javase person.xsd" + xmlns="http://www.seazean.cn/javase" + xsi:schemaLocation="http://www.seazean.cn/javase person.xsd" > @@ -11193,7 +11207,7 @@ person.xsd @@ -11225,8 +11239,8 @@ person.xsd 张三 @@ -18382,9 +18396,9 @@ lock前缀指令就相当于内存屏障,Memory Barrier(Memory Fence) -#### DCL +#### 双端检锁 -##### 双端检锁 +##### 检锁机制 Double-Checked Locking:双端检锁机制 @@ -18460,10 +18474,10 @@ getInstance 方法对应的字节码为: * 21 表示利用一个对象引用,调用构造方法初始化对象 * 24 表示利用一个对象引用,赋值给 static INSTANCE -步骤21和步骤23之间不存在数据依赖关系,而且无论重排前后,程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的 +步骤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) @@ -18665,11 +18679,10 @@ public final class Singleton implements Serializable { 解决办法: - * 对单例声明 transient,然后实现readObject(ObjectInputStream in) 方法,复用原来的单例 - * 访问权限为private/protected - * 返回值必须是Object - * 异常可以不抛 - * 实现readResolve()方法,当JVM从内存中反序列化地"组装"一个新对象,就会自动调用readResolve方法返回原来单例 + * 对单例声明 transient,然后实现 readObject(ObjectInputStream in) 方法,复用原来的单例 + + 条件:访问权限为private/protected、返回值必须是Object、异常可以不抛 + * 实现readResolve()方法,当 JVM 从内存中反序列化地"组装"一个新对象,就会自动调用readResolve方法返回原来单例 * 问题3:为什么构造方法设置为私有? 是否能防止反射创建新的实例? 防止其他类无限创建对象;不能防止反射破坏 @@ -18682,6 +18695,8 @@ public final class Singleton implements Serializable { +*** + ##### 枚举式 @@ -18703,7 +18718,7 @@ public static void main(String[] args) { * 问题2:枚举单例在创建时是否有并发问题?否 * 问题3:枚举单例能否被反射破坏单例?否,反射创建对象时判断是枚举类型就直接抛出异常 * 问题4:枚举单例能否被反序列化破坏单例?否 -* 问题5:枚举单例属于懒汉式还是饿汉式?饿汉式 +* 问题5:枚举单例属于懒汉式还是饿汉式?**饿汉式** * 问题6:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做?添加构造方法 反编译后的: @@ -18716,6 +18731,8 @@ public final class Singleton extends java.lang.Enum {//Enum实现序 +*** + ##### 懒汉式 @@ -18737,6 +18754,8 @@ public final class Singleton { +**** + ##### DCL @@ -18765,6 +18784,8 @@ public final class Singleton { +*** + ##### 内部类 @@ -24948,7 +24969,7 @@ final void updateHead(Node h, Node p) { # Design -(待学习) +(正在更新) ## 单例模式 From 2622550101650e59a43d4eb2a9d2c5757f39520f Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 31 May 2021 23:54:25 +0800 Subject: [PATCH 033/242] Update Java Notes --- Java.md | 59 +++++++++++++++++++++++++++------------------------------ 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/Java.md b/Java.md index 968f366..441a742 100644 --- a/Java.md +++ b/Java.md @@ -7055,16 +7055,12 @@ public static void main(String[] args) { #### 概述 -File类:代表操作系统的文件对象。 -File类:是用来操作操作系统的文件对象的,删除文件,获取文件信息,创建文件(文件夹)... - 广义来说操作系统认为文件包含(文件和文件夹) +File类:代表操作系统的文件对象,是用来操作操作系统的文件对象的,删除文件,获取文件信息,创建文件(文件夹),广义来说操作系统认为文件包含(文件和文件夹) -File类的创建文件对象的API: - 包:java.io.File - 构造器: - public File(String pathname):根据路径获取文件对象 - public File(String parent , String child):根据父路径和文件名称获取文件对象! - public File(File parent , String child) +File类构造器: + `public File(String pathname)`:根据路径获取文件对象 + `public File(String parent , String child)`:根据父路径和文件名称获取文件对象! + `public File(File parent , String child)` File类创建文件对象的格式: @@ -7074,9 +7070,9 @@ File类创建文件对象的格式: * 一般是定位某个操作系统中的某个文件对象 * **相对路径**:不带盘符的(重点) * 默认是直接相对到工程目录下寻找文件的。 - * 相对路径只能用于寻找工程下的文件,可以跨平台! + * 相对路径只能用于寻找工程下的文件,可以跨平台 -* `File f = new File("文件对象/文件夹对象");`广义来说:文件是包含文件和文件夹的 +* `File f = new File("文件对象/文件夹对象")` 广义来说:文件是包含文件和文件夹的 ```java public class FileDemo{ @@ -7088,7 +7084,7 @@ public class FileDemo{ // -- c.使用分隔符API:File.separator //File f1 = new File("D:"+File.separator+"it"+File.separator //+"图片资源"+File.separator+"beautiful.jpg"); - File f1 = new File("D:\\itcast\\图片资源\\beautiful.jpg"); + File f1 = new File("D:\\seazean\\图片资源\\beautiful.jpg"); System.out.println(f1.length()); // 获取文件的大小,字节大小 // 2.创建文件对象:使用相对路径 @@ -7218,7 +7214,7 @@ public class FileDemo { ```java public class FileDemo { public static void main(String[] args) { - File dir = new File("D:\\itcast"); + File dir = new File("D:\\seazean"); // a.获取当前目录对象下的全部一级文件名称到一个字符串数组返回。 String[] names = dir.list(); for (String name : names) { @@ -7343,12 +7339,13 @@ public static void searchFiles(File dir , String fileName){ #### 概述 IO输入输出流:输入/输出流 - Input:输入 - Output:输出 + +* Input:输入 +* Output:输出 引入:File类只能操作文件对象本身,不能读写文件对象的内容,读写数据内容,应该使用IO流 -IO流是一个水流模型:IO理解成水管,把数据理解成水流。 +IO流是一个水流模型:IO理解成水管,把数据理解成水流 IO流的分类: @@ -7521,9 +7518,9 @@ public class CopyDemo01 { OutputStream os = null ; try{ //(1)创建一个字节输入流管道与源文件接通。 - is = new FileInputStream("D:\\itcast\\图片资源\\meinv.jpg"); + is = new FileInputStream("D:\\seazean\\图片资源\\meinv.jpg"); //(2)创建一个字节输出流与目标文件接通。 - os = new FileOutputStream("D:\\itcast\\meimei.jpg"); + os = new FileOutputStream("D:\\seazean\\meimei.jpg"); //(3)创建一个字节数组作为桶 byte buffer = new byte[1024]; //(4)从字节输入流管道中读取数据,写出到字节输出流管道即可 @@ -7850,7 +7847,7 @@ public static void main(String[] args) throws Exception { public class InputStreamReaderDemo{ public static void main(String[] args) throws Exception { // 1.提取GBK文件的原始字节流 - InputStream is = new FileInputStream("D:\\itcast\\Netty.txt"); + InputStream is = new FileInputStream("D:\\seazean\\Netty.txt"); // 2.把原始字节输入流通过转换流,转换成 字符输入转换流InputStreamReader InputStreamReader isr = new InputStreamReader(is,"GBK"); // 3.包装成缓冲流 @@ -8047,9 +8044,9 @@ try( ```java try( /** (1)创建一个字节输入流管道与源文件接通。 */ - InputStream is = new FileInputStream("D:\\itcast\\图片资源\\meinv.jpg"); + InputStream is = new FileInputStream("D:\\seazean\\图片资源\\meinv.jpg"); /** (2)创建一个字节输出流与目标文件接通。*/ - OutputStream os = new FileOutputStream("D:\\itcast\\meimei.jpg"); + OutputStream os = new FileOutputStream("D:\\seazean\\meimei.jpg"); /** (5)关闭资源!是自动进行的 */ ){ byte[] buffer = new byte[1024]; @@ -8892,8 +8889,8 @@ class ReaderClientRunnable implements Runnable { ##### 字节流传输 -客户端:本地图片: ‪E:\itcast\图片资源\beautiful.jpg -服务端:服务器路径:E:\itcast\图片服务器 +客户端:本地图片: ‪E:\seazean\图片资源\beautiful.jpg +服务端:服务器路径:E:\seazean\图片服务器 UUID. randomUUID() : 方法生成随机的文件名 @@ -8902,8 +8899,8 @@ UUID. randomUUID() : 方法生成随机的文件名 ```java //常量包 public class Constants { - public static final String SRC_IMAGE = "D:\\itcast\\图片资源\\beautiful.jpg"; - public static final String SERVER_DIR = "D:\\itcast\\图片服务器\\"; + public static final String SRC_IMAGE = "D:\\seazean\\图片资源\\beautiful.jpg"; + public static final String SERVER_DIR = "D:\\seazean\\图片服务器\\"; public static final String SERVER_IP = "127.0.0.1"; public static final int SERVER_PORT = 8888; @@ -11491,17 +11488,17 @@ public class Contact { 潘金莲 - panpan@itcast.cn + panpan@seazean.cn 武松 - wusong@itcast.cn + wusong@seazean.cn 武大狼 - wuda@itcast.cn + wuda@seazean.cn ``` @@ -11574,12 +11571,12 @@ public class XPathDemo { 潘金莲 - panpan@itcast.cn + panpan@seazean.cn 武松 - wusong@itcast.cn + wusong@seazean.cn sql语句 @@ -11587,7 +11584,7 @@ public class XPathDemo { 武大狼 - wuda@itcast.cn + wuda@seazean.cn 外面的名称 From e322ee6207d462afccfa37b1983c0b8b8d0b3a65 Mon Sep 17 00:00:00 2001 From: Seazean Date: Tue, 1 Jun 2021 21:52:55 +0800 Subject: [PATCH 034/242] Update Java Notes --- DB.md | 2 +- Java.md | 194 +++++--------------------------------------------------- 2 files changed, 16 insertions(+), 180 deletions(-) diff --git a/DB.md b/DB.md index ac5c0be..a9a3f93 100644 --- a/DB.md +++ b/DB.md @@ -10931,7 +10931,7 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 #### 缓存击穿 -场景:系统平稳运行过程中,数据库连接量瞬间激增,Redis服务器无大量key过期,Redis内存平稳,无波动,Redis服务器CPU正常,但是数据库崩溃 +场景:系统平稳运行过程中,数据库连接量瞬间激增,Redis服务器无大量key过期,Redis内存平稳,无波动,Redis 服务器 CPU 正常,但是数据库崩溃 问题排查: diff --git a/Java.md b/Java.md index 441a742..3e39438 100644 --- a/Java.md +++ b/Java.md @@ -7384,7 +7384,7 @@ ObjectInputStream ObjectOutputStream #### 字节流 -##### 字节输入流 +##### 字节输入 FileInputStream文件字节输入流: @@ -7460,7 +7460,7 @@ System.out.println(rs); -##### 字节输出流 +##### 字节输出 FileOutputStream文件字节输出流: @@ -7552,9 +7552,9 @@ public class CopyDemo01 { #### 字符流 -##### 字符输入流 +##### 字符输入 -FileReader:文件字符输入流。 +FileReader:文件字符输入流 * 作用:以内存为基准,把磁盘文件的数据以字符的形式读入到内存,读取文本文件内容到内存中去。 * 构造器: @@ -7606,9 +7606,9 @@ public class FileReaderDemo02 {//字符数组 -##### 字符输出流 +##### 字符输出 -FileWriter文件字符输出流的使用。 +FileWriter:文件字符输出流 * 作用:以内存为基准,把内存中的数据按照字符的形式写出到磁盘文件中去 * 构造器: @@ -7941,6 +7941,10 @@ class User implements Serializable { +**** + + + ##### 反序列化 对象反序列化(对象字节输入流):ObjectInputStream @@ -12469,23 +12473,23 @@ public void localvarGC4() { 安全点 (Safepoint):程序执行时并非在所有地方都能停顿下来开始GC,只有在安全点才能停下 -- Safe Point的选择很重要,如果太少可能导致GC等待的时间太长,如果太多可能导致运行时的性能问题 -- 大部分指令的执行时间都非常短,通常会根据是否具有让程序长时间执行的特征为标准,选择些执行时间较长的指令作为Safe Point, 如方法调用、循环跳转和异常跳转等 +- Safe Point 的选择很重要,如果太少可能导致GC等待的时间太长,如果太多可能导致运行时的性能问题 +- 大部分指令的执行时间都非常短,通常会根据是否具有让程序长时间执行的特征为标准,选择些执行时间较长的指令作为 Safe Point, 如方法调用、循环跳转和异常跳转等 在GC发生时,让所有线程都在最近的安全点停顿下来的方法: - 抢先式中断:没有虚拟机采用,首先中断所有线程,如果有线程不在安全点,就恢复线程让线程运行到安全点 - 主动式中断:设置一个中断标志,各个线程运行到各个 Safe Point 时就轮询这个标志,如果中断标志为真,则将自己进行中断挂起 -问题:Safepoint 保证程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint,但是当线程处于 Waiting 状态或Blocked状态,线程无法响应JVM的中断请求,运行到安全点去中断挂起,JVM也不可能等待线程被唤醒,对于这种情况,需要安全区域来解决 +问题:Safepoint 保证程序执行时,在不太长的时间内就会遇到可进入GC的 Safepoint,但是当线程处于 Waiting 状态或 Blocked 状态,线程无法响应JVM的中断请求,运行到安全点去中断挂起,JVM也不可能等待线程被唤醒,对于这种情况,需要安全区域来解决 安全区域 (Safe Region):指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的 运行流程: -- 当线程运行到Safe Region的代码时,首先标识已经进入了Safe Region,如果这段时间内发生GC,JVM会忽略标识为Safe Region状态的线程 +- 当线程运行到 Safe Region 的代码时,首先标识已经进入了 Safe Region,如果这段时间内发生GC,JVM会忽略标识为 Safe Region 状态的线程 -- 当线程即将离开Safe Region时,会检查JVM是否已经完成GC,如果完成了则继续运行,否则线程必须等待GC完成,收到可以安全离开SafeRegion的信号 +- 当线程即将离开 Safe Region 时,会检查JVM是否已经完成GC,如果完成了则继续运行,否则线程必须等待GC 完成,收到可以安全离开 SafeRegion 的信号 @@ -18638,174 +18642,6 @@ public class TestVolatile { -*** - - - -#### 单例模式 - -##### 饿汉式 - -单例模式有很多实现方法,饿汉、懒汉、静态内部类、枚举类,试分析每种实现下获取单例对象时的线程安全 - -* 饿汉式:类加载就会导致该单实例对象被创建 - -* 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建 - -```java -public final class Singleton implements Serializable { - private Singleton() {} - private static final Singleton INSTANCE = new Singleton(); - public static Singleton getInstance() { - return INSTANCE; - } - - //解决序列化问题 - protected Object readResolve() { - return INSTANCE; - } -} -``` - -* 问题1:为什么类加 final 修饰? - 不被子类继承,防止子类中不适当的行为覆盖父类的方法,破坏了单例 - -* 问题2:如果实现了序列化接口,怎么防止防止反序列化破坏单例? - - 将单例对象序列化再反序列化,对象从内存反序列化到程序中会重新创建一个对象,通过反序列化得到的对象是不同的对象,而且得到的对象不是通过构造器得到的,反序列化得到的对象不执行构造器 - - 解决办法: - - * 对单例声明 transient,然后实现 readObject(ObjectInputStream in) 方法,复用原来的单例 - - 条件:访问权限为private/protected、返回值必须是Object、异常可以不抛 - * 实现readResolve()方法,当 JVM 从内存中反序列化地"组装"一个新对象,就会自动调用readResolve方法返回原来单例 - -* 问题3:为什么构造方法设置为私有? 是否能防止反射创建新的实例? - 防止其他类无限创建对象;不能防止反射破坏 - -* 问题4:这种方式是否能保证单例对象创建时的线程安全? - 能,静态变量初始化在类加载时完成,由JVM保证线程安全 - -* 问题5:为什么提供静态方法而不是直接将 INSTANCE 设置为 public? -更好的封装性、提供泛型支持、可以改进成懒汉单例设计 - - - -*** - - - -##### 枚举式 - -```java -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:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做?添加构造方法 - -反编译后的: - -```java -public final class Singleton extends java.lang.Enum {//Enum实现序列化接口 - public static final Singleton INSTANCE = new Singleton(); -} -``` - - - -*** - - - -##### 懒汉式 - -```java -public final class Singleton { - private Singleton() { } - private static Singleton INSTANCE = null; - //分析这里的线程安全,并说明有什么缺点?锁范围太大,每次进入都要加锁 - public static synchronized Singleton getInstance() { - if( INSTANCE != null ){ - return INSTANCE; - } - INSTANCE = new Singleton(); - return INSTANCE; - } -} -``` - - - -**** - - - -##### DCL - -```java -public final class Singleton { - private Singleton() { } - // 防止指令重排序 - private static volatile Singleton INSTANCE = null; - // 首次使用getInstance()才加锁,后续使用时无需加锁,性能好 - public static Singleton getInstance() { - if (INSTANCE != null) { - return INSTANCE; - } - synchronized (Singleton.class) { - // 防止首次创建Singleton时的并发的问题 - if (INSTANCE != null) { // t2 - return INSTANCE; - } - INSTANCE = new Singleton(); - return INSTANCE; - } - } -} -``` - - - -*** - - - -##### 内部类 - -```java -public final class Singleton { - private Singleton() { } - - private static class LazyHolder { - static final Singleton INSTANCE = new Singleton(); - } - - public static Singleton getInstance() { - return LazyHolder.INSTANCE; - } -} -``` - -* 问题1:属于懒汉式还是饿汉式? - 懒汉式,类加载本身就是懒惰的,首次调用时加载,然后对单例进行初始化 -* 问题2:在创建时是否有线程安全问题? - 没有,静态变量初始化在类加载时完成,由JVM保证线程安全 - *** From 9d050277113cdee94264f9bf39aa5810bda7680e Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 2 Jun 2021 17:12:06 +0800 Subject: [PATCH 035/242] Update Java Notes --- DB.md | 2 +- Java.md | 360 ++++---------------------------------------------------- SSM.md | 4 +- Tool.md | 2 +- 4 files changed, 28 insertions(+), 340 deletions(-) diff --git a/DB.md b/DB.md index a9a3f93..06273f5 100644 --- a/DB.md +++ b/DB.md @@ -8702,7 +8702,7 @@ Redis 使用跳跃表作为有序集合键的底层实现之一,如果一个 ##### 基本介绍 -布隆过滤器:一种数据结构,是一个很长的二进制向量(位数组)和一系列随机映射函数(哈希函数),既然是二进制,每个空间存放的不是0就是1,但是初始默认值都是0 +布隆过滤器:一种数据结构,是一个很长的二进制向量(位数组)和一系列随机映射函数(哈希函数),既然是二进制,每个空间存放的不是0就是1,但是初始默认值都是0,所以布隆过滤器不存数据只存状态 diff --git a/Java.md b/Java.md index 3e39438..1bfc081 100644 --- a/Java.md +++ b/Java.md @@ -2750,24 +2750,24 @@ Cloneable 接口是一个标识性接口,即该接口不包含任何方法( ### Objects -* Objects类与Object是继承关系。 +Objects 类与 Object 是继承关系。 -* Objects的方法: - - * `public static boolean equals(Object a, Object b)` : 比较两个对象是否相同。 - 底层进行非空判断,从而可以避免空指针异常,更安全!!推荐使用!! - - ```java - public static boolean equals(Object a, Object b) { - return a == b || a != null && a.equals(b); - } - ``` - - * `public static boolean isNull(Object obj)` : 判断变量是否为null ,为null返回true ,反之! - - * `public static String toString(对象)` : 返回参数中对象的字符串表示形式 - - * `public static String toString(对象, 默认字符串)` : 返回对象的字符串表示形式。 +Objects的方法: + +* `public static boolean equals(Object a, Object b)` : 比较两个对象是否相同。 + 底层进行非空判断,从而可以避免空指针异常,更安全!!推荐使用!! + + ```java + public static boolean equals(Object a, Object b) { + return a == b || a != null && a.equals(b); + } + ``` + +* `public static boolean isNull(Object obj)` : 判断变量是否为null ,为null返回true ,反之! + +* `public static String toString(对象)` : 返回参数中对象的字符串表示形式 + +* `public static String toString(对象, 默认字符串)` : 返回对象的字符串表示形式。 ```java public class ObjectsDemo { @@ -8073,12 +8073,10 @@ try( ### Properties Properties:属性集对象。就是一个Map集合,一个键值对集合 -Properties核心作用:Properties代表的是一个属性文件,可以把键值对数据存入到一个属性文件 -属性文件:后缀是.properties结尾的文件,里面的内容都是 key=value -> 大型框架技术中,属性文件都是很重要的系统配置文件。 -> users.properties -> admin=123456 +核心作用:Properties代表的是一个属性文件,可以把键值对数据存入到一个属性文件 + +属性文件:后缀是.properties结尾的文件,里面的内容都是 key=value Properties方法: @@ -8135,8 +8133,8 @@ 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)` : 设置文件指针偏移,从该文件开头测量,发生下一次读取或写入(插入+覆盖) @@ -24802,316 +24800,4 @@ final void updateHead(Node h, Node p) { # Design -(正在更新) - -## 单例模式 - -单例模式,是一种常用的软件设计模式。通过单例模式可以保证系统中, -该模式的这个类永远只有一个实例。即**一个类永远只有一个对象实例**。 -单例是为了节约内存,单例在有些业务场景下还必须用到 - -* 饿汉单例设计模式 - 在用类获取对象的时候,对象已经提前创建好了。 - a.定义一个类,把构造器私有。 - b.定义一个静态变量存储一个对象。 - c.提供一个返回单例对象的方法。 - -* 懒汉单例设计模式 - 在真正需要该对象的时候,才去创建一个对象(延迟加载对象)。 - a.定义一个类,把构造器私有。 - b.定义一个静态变量存储一个对象。 - c.提供一个返回单例对象的方法。 - -```java -//饿汉单例设计模式 -public class SingleInstanceDemo{ - public static void main(String[] args){ - Singleton s1 = Singleton.getInstance(); - Singleton s2 = Singleton.getInstance(); - System.out.println(s1 == s2);//true - } -} -class Singleton{ - //在用类获取对象的时候,对象已经提前为你创建好了。 - public static final Singleton INSTANCE = new Singleton(); - private Singleton(){} - //返回单例对象 - public static Singleton getInstance(){ - return INSTANCE - } -} -``` - -```java -//懒汉单例设计模式 -public class SingleInstanceDemo{ - public static void main(String[] args){ - Singleton s1 = Singleton.getInstance(); - Singleton s2 = Singleton.getInstance(); - System.out.println(s1 == s2);//true - } -} -class Singleton{ - //在用类获取对象的时候,对象自己创建好了。 - public static Singleton instance; - private Singleton(){} - //返回单例对象 - public static Singleton getInstance(){ - if (instance == null) { - instance = new Singleton; - } - return instance; - } -} -``` - - - -*** - - - -## 动态代理 - -代理就是被代理者没有能力或者不愿意去完成某件事情,需要找个人代替自己去完成这件事。 -动态代理只能为实现接口的实现类对象做代理(也可以只为接口做代理对象) - -> 在业务开发中经常存在很多重复的方法代码,他们前后的代码形式是一样的 -> 只有中间部分代码有差别!!这种时候代码冗余读很高 -> 有没有一种方法可以直接省略前后重复的代码就可以完成功能,这时候用动态代理。 - -* 优点: - * 动态代理非常的灵活,可以为任意的接口实现类对象做代理,可以为被代理对象的所有接口的所有方法做代理,动态代理可以在不改变方法源码的情况下,实现对方法功能的增强; - * 动态代理类简化了编程工作,提高了软件的可扩展性,Java 反射机制可以生成任意类型的动态代理类。 - * 动态代理提高了开发效率。 -* 缺点:**只能针对接口或者接口的实现类对象做代理对象**,普通类是不能做代理对象的 -* 原因:**生成的代理类继承了Proxy**,因为java是单继承的,所以JDK动态代理只能代理接口。 - -```java -public class TestDemo { - public static void main(String[] args) { - // 1.创建一个业务对象 - // 为我们的业务对象做成一个被代理的业务对象!! - UserService userService = ProxyUtil.getProxy(new UserServiceImpl()); - String rs = userService.login("admin","123456");//走代理! - System.out.println(rs); - userService.deleteAll(); // 走代理! - userService.updateAll(); // 走代理! - } -} -``` - -```java -//业务接口 -public interface UserService { - String login(String loginName, String passWord); - void deleteAll(); - void updateAll(); -} -``` - -```java -//业务实现类 -public class UserServiceImpl implements UserService { - @Override - public String login(String loginName, String passWord) { - String flag = "登陆名称或者密码错误"; - if("admin".equals(loginName) && "123456".equals(passWord)){ - flag = "success"; - } - try { - Thread.sleep(2000); - } catch (Exception e) { - e.printStackTrace(); - } - return flag; - } - @Override - public void deleteAll() { - try { - Thread.sleep(1500); - } catch (Exception e) { - e.printStackTrace(); - } - System.out.println("删除成功!"); - } - @Override - public void updateAll() { - try { - Thread.sleep(500); - } catch (Exception e) { - e.printStackTrace(); - } - System.out.println("更新成功!"); - } -} -``` - -代理类:帮助我们做一个被代理的业务对象返回。 -java.lang.reflect.Proxy:这是 Java 动态代理机制的主类,它提供了一个静态方法来为一组接口的实现类动态地生成代理类及其对象。 -**public static Object newProxyInstance(ClassLoader loader,Class[] interfaces, InvocationHandler h)** - 参数一:类加载器,负责加载到时候做好的业务代理对象! - 参数二:被代理业务对象的**全部实现的接口**,以便代理对象可以知道要为哪些方法做代理。 - 参数三:代理真正的执行方法,也就是代理的处理逻辑! - -```java -public class ProxyUtil { - //做一个被代理的业务对象返回! - public static T getProxy(Object obj) { - return (T) Proxy.newProxyInstance(obj.getClass().getClassLoader(), - obj.getClass().getInterfaces(), new InvocationHandler() { - @Override - public Object invoke(Object proxy, Method method, Object[] - params) throws Throwable { - // proxy : 业务代理对象本身。用不到 - // method: 代表当前正在被代理执行的方法!! - // params: 代表的是执行方法的参数,数组的形式! - long startTime = System.currentTimeMillis(); - - // 真正触发真实的方法执行 - Object rs = method.invoke(obj,params); - - long endTime = System.currentTimeMillis(); - sout(method.getName()+"方法耗时:"+ - (endTime - startTime)/1000.0+"s"); - return rs; // 返回方法执行的结果!! - } - }); - } -} -``` - - - -*** - - - -## 工厂模式 - -工厂设计模式: - -* 工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一 - -* 这种类型的设计模式属于创建型模式,它提供了一种创建对象的方式 - -* 以前我们创建类对象时, 都是使用new 对象的形式创建,,除new 对象方式以外,工厂模式也可以创建对象 - -工厂设计模式的作用: - -* 对象通过工厂的方法创建返回,工厂的方法可以为该对象进行加工和数据注入。 -* 可以实现类与类之间的解耦操作(核心思想,重点) - -优点:工厂模式的存在可以改变创建对象的方式,解决类与类之间的**耦合**。 -缺点:工厂设计模式多了一个工厂类!!! - -```java -public class FactoryDemo { - public static void main(String[] args) { - Animal a = FactoryPattern.createAniaml(); - a.run(); - } -} -// 工厂设计模式 -public class FactoryPattern { - // 生产对象的方法:工厂方法 - public static Animal createAniaml(){ - return new Dog(); - } -} - -public abstract class Animal { - public abstract void run(); -} -public class Cat extends Animal { - @Override - public void run() { - System.out.println("猫跑的贼溜~~~~"); - } -} -public class Dog extends Animal { - @Override - public void run() { - System.out.println("狗跑的也贼溜~~~~"); - } -} -``` - - - -*** - - - -## 装饰模式 - -装饰模式指的是在不改变原类, 动态地扩展一个类的功能。 -思想:是创建一个新类,包装原始类,从而在新类中提升原来类的功能!! - -装饰模式可以在不改变原类的基础上对类中的方法进行扩展增强,实现原则为: - 1.定义父类。 - 2.定义原始类,继承父类,定义功能。 - 3.定义装饰类,继承父类,包装原始类,增强功能!! - -```java -public class Demo { - public static void main(String[] args) { - InputStream is = new BufferedInputStrem(new FileInputStream()); - is.read(); - is.close(); - } -} -public abstract class InputStream { - public abstract void read(); - public abstract void close(); -} -public class FileInputStream extends InputStream { - @Override - public void read() { - System.out.println("读取数据~~~"); - } - - @Override - public void close() { - System.out.println("关闭流~~~"); - } -} -// 装饰模式!提升原始功能!!! -public class BufferedInputStrem extends InputStream { - private InputStream is ; - public BufferedInputStrem(InputStream is){ - this.is = is; - } - @Override - public void read() { - System.out.println("开启高效缓冲读取~"); - is.read(); - } - @Override - public void close() { - is.close(); - } -} -``` - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file +(更新中) diff --git a/SSM.md b/SSM.md index 1ce2436..deebf1d 100644 --- a/SSM.md +++ b/SSM.md @@ -5653,9 +5653,11 @@ CGLIB特点: * cglib类 - * JDKProxy仅对接口方法做增强,cglib对所有方法做增强,包括Object类中的方法 (toString hashCode),需要对方法进行判断是否是save,来选择性增强 + * JDKProxy仅对接口方法做增强,cglib对所有方法做增强,包括Object类中的方法 (toString、hashCode) * 返回值类型采用多态向下转型,所以需要设置父类类型 + 需要对方法进行判断是否是save,来选择性增强 + ```java public class UserServiceImplCglibProxy { public static UserService createUserServiceCglibProxy(Class cls){ diff --git a/Tool.md b/Tool.md index 5693615..3c65961 100644 --- a/Tool.md +++ b/Tool.md @@ -1849,7 +1849,7 @@ zip是个使用广泛的压缩程序,文件经它压缩后会另外产生具 #### unzip -unzip命令用于解压缩zip文件,unzip为.zip压缩文件的解压缩程序 +unzip 命令用于解压缩zip文件,unzip 为.zip压缩文件的解压缩程序 命令:unzip [必要参数] [选择参数] [文件] From 11d63f081082c59a1157e0d1f13029f40e7a6518 Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 3 Jun 2021 19:58:29 +0800 Subject: [PATCH 036/242] Update Java Notes --- Java.md | 72 +++++++++++++++++++++++++++++++++++---------------------- SSM.md | 19 +++++++-------- 2 files changed, 54 insertions(+), 37 deletions(-) diff --git a/Java.md b/Java.md index 1bfc081..f553fa3 100644 --- a/Java.md +++ b/Java.md @@ -2927,14 +2927,13 @@ public class Demo { public static void main(String[] args) { String s1 = "a"; // 懒惰的 String s2 = "b"; - String s3 = "ab"; + String s3 = "ab";//串池 // new StringBuilder().append("a").append("b").toString() new String("ab") - String s4 = s1 + s2; + String s4 = s1 + s2; //d String s5 = "a" + "b"; // javac 在编译期间的优化,结果已经在编译期确定为ab System.out.println(s3 == s4); // false System.out.println(s3 == s5); // true - System.out.println(s3 == s6); // true String x2 = new String("c") + new String("d"); // new String("cd") // 虽然new,但是在字符串常量池没有 cd 对象,toString()方法 @@ -13436,13 +13435,13 @@ JVM是通过栈帧中的对象引用访问到其内部的对象实例:(内 在Java中,对象的生命周期包括以下几个阶段: -1. 创建阶段(Created): -2. 应用阶段(In Use):对象至少被一个强引用持有着 -3. 不可见阶段(Invisible):程序的执行已经超出了该对象的作用域,不再持有该对象的任何强引用 -4. 不可达阶段(Unreachable):该对象不再被任何强引用所持有,包括GC Root的强引用 -5. 收集阶段(Collected):垃圾回收器已经对该对象的内存空间重新分配做好准备,该对象如果重写了finalize()方法,则会去执行该方法 -6. 终结阶段(Finalized):等待垃圾回收器对该对象空间进行回收,当对象执行完finalize()方法后仍然处于不可达状态时进入该阶段 -7. 对象空间重分配阶段(De-allocated):垃圾回收器对该对象的所占用的内存空间进行回收或者再分配 +1. 创建阶段 (Created): +2. 应用阶段 (In Use):对象至少被一个强引用持有着 +3. 不可见阶段 (Invisible):程序的执行已经超出了该对象的作用域,不再持有该对象的任何强引用 +4. 不可达阶段 (Unreachable):该对象不再被任何强引用所持有,包括GC Root的强引用 +5. 收集阶段 (Collected):垃圾回收器已经对该对象的内存空间重新分配做好准备,该对象如果重写了finalize()方法,则会去执行该方法 +6. 终结阶段 (Finalized):等待垃圾回收器对该对象空间进行回收,当对象执行完finalize()方法后仍然处于不可达状态时进入该阶段 +7. 对象空间重分配阶段 (De-allocated):垃圾回收器对该对象的所占用的内存空间进行回收或者再分配 @@ -13583,7 +13582,7 @@ Java对象创建时机: * 实例初始化不一定要在类初始化结束之后才开始 - * 在同一个类加载器下,一个类型只会被初始化一次。所以一旦开始初始化一个类,无论是否完成后续都不会再重新触发该类型的初始化阶段了(只考虑在同一个类加载器下的情形)。因此,在实例化上述程序中的st变量时,**实际上是把实例初始化嵌入到了静态初始化流程中,并且在上面的程序中,嵌入到了静态初始化的起始位置**,这就导致了实例初始化完全发生在静态初始化之前,这也是导致a为110 b为0的原因 + * 在同一个类加载器下,一个类型只会被初始化一次。所以一旦开始初始化一个类,无论是否完成后续都不会再重新触发该类型的初始化阶段了(只考虑在同一个类加载器下的情形。因此,在实例化上述程序中的st变量时,**实际上是把实例初始化嵌入到了静态初始化流程中,并且在上面的程序中,嵌入到了静态初始化的起始位置**,这就导致了实例初始化完全发生在静态初始化之前,这也是导致a为110 b为0的原因 代码等价于: @@ -13777,6 +13776,10 @@ class D { +*** + + + ##### clinit ():类构造器,由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的 @@ -13786,7 +13789,7 @@ class D { * 如果类中没有静态变量或静态代码块,那么clinit方法将不会被生成 * 在执行clinit方法时,必须先执行父类的clinit方法 * clinit方法只执行一次 -* static变量的赋值操作和静态代码块的合并顺序由源文件中出现的顺序决定 +* static变量的赋值操作和静态代码块的合并顺序由源文件中**出现的顺序**决定 **线程安全**问题: @@ -13798,7 +13801,7 @@ class D { ```java public class Test { static { - i = 0; // 给变量赋值可以正常编译通过 + //i = 0; // 给变量赋值可以正常编译通过 System.out.print(i); // 这句编译器会提示“非法向前引用” } static int i = 1; @@ -13811,6 +13814,10 @@ public class Test { +**** + + + ##### 时机 类的初始化是懒惰的,初始化时机: @@ -13836,6 +13843,10 @@ public class Test { +*** + + + ##### init init指的是实例构造器,主要作用是在类实例化过程中执行,执行内容包括成员变量初始化和代码块的执行 @@ -17464,11 +17475,10 @@ public static void main(String[] args) { 对比wait & notify: -* wait,notify 和 notifyAll 必须配合Object Monitor一起使用,而park、unpark不需要 +* wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而park、unpark不需要 * park & unpark以线程为单位来阻塞和唤醒线程,而notify 只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程 -* **park & unpark 可以先 unpark**,而 wait & notify 不能先 notify - 类比生产消费,先消费发现有产品就消费,没有就等待;先生产就直接产生商品,然后线程直接消费 -* wait会释放锁资源进入等待队列,park 不会释放锁资源,只负责阻塞当前线程,会释放CPU +* **park & unpark 可以先 unpark**,而 wait & notify 不能先 notify。类比生产消费,先消费发现有产品就消费,没有就等待;先生产就直接产生商品,然后线程直接消费 +* wait 会释放锁资源进入等待队列,park 不会释放锁资源,只负责阻塞当前线程,会释放CPU 原理: @@ -17711,17 +17721,23 @@ class GuardedObject { ```java public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { - try { Thread.sleep(1000); } catch (InterruptedException e) { } - // 当没有『许可』时,当前线程暂停运行;有『许可』时,用掉这个『许可』,当前线程恢复运行 - LockSupport.park(); - System.out.println("1"); - }).start(); - - Thread t2 = new Thread(() -> { - System.out.println("2"); - // 给线程 t1 发放『许可』(多次连续调用 unpark 只会发放一个『许可』) - LockSupport.unpark(t1); - }).start(); + while (true) { + //try { Thread.sleep(1000); } catch (InterruptedException e) { } + // 当没有许可时,当前线程暂停运行;有许可时,用掉这个许可,当前线程恢复运行 + LockSupport.park(); + System.out.println("1"); + } + }); + Thread t2 = new Thread(() -> { + while (true) { + System.out.println("2"); + // 给线程 t1 发放『许可』(多次连续调用 unpark 只会发放一个『许可』) + LockSupport.unpark(t1); + try { Thread.sleep(500); } catch (InterruptedException e) { } + } + }); + t1.start(); + t2.start(); } ``` diff --git a/SSM.md b/SSM.md index deebf1d..d2e4208 100644 --- a/SSM.md +++ b/SSM.md @@ -2690,7 +2690,7 @@ IoC和DI的关系:IoC与DI是同一件事站在不同角度看待问题 - itheima666 + seazean666 666666 @@ -2702,7 +2702,7 @@ IoC和DI的关系:IoC与DI是同一件事站在不同角度看待问题 - itheima + seazean 66666 @@ -7805,7 +7805,7 @@ Controller加载控制:SpringMVC的处理器对应的bean必须按照规范格 SpringMVC将传递的参数封装到处理器方法的形参中,达到快速访问参数的目的 -* 访问URL:http://localhost/requestParam1?name=itheima&age=14 +* 访问URL:http://localhost/requestParam1?name=seazean&age=14 ```java @Controller @@ -7860,7 +7860,7 @@ SpringMVC将传递的参数封装到处理器方法的形参中,达到快速 当POJO中使用简单类型属性时, 参数名称与POJO类属性名保持一致 -* 访问URL: http://localhost/requestParam3?name=itheima&age=14 +* 访问URL: http://localhost/requestParam3?name=seazean&age=14 ```java @RequestMapping("/requestParam3") @@ -7884,7 +7884,7 @@ SpringMVC将传递的参数封装到处理器方法的形参中,达到快速 当POJO类型属性与其他形参出现同名问题时,将被**同时赋值**,建议使用@RequestParam注解进行区分 -* 访问URL: http://localhost/requestParam4?name=itheima&age=14 +* 访问URL: http://localhost/requestParam4?name=seazean&age=14 ```java @RequestMapping("/requestParam4") @@ -8047,7 +8047,7 @@ SpringMVC将传递的参数封装到处理器方法的形参中,达到快速 开启转换配置:` ` 作用:提供Controller请求转发,Json自动转换等功能 -如果访问URL:http://localhost/requestParam1?name=itheima&age=seazean,会出现报错,类型转化异常 +如果访问URL:http://localhost/requestParam1?name=seazean&age=seazean,会出现报错,类型转化异常 ```java @RequestMapping("/requestParam1") @@ -8674,7 +8674,7 @@ public String ajaxPojoToController(@RequestBody User user){ } @RequestMapping("/ajaxListToController") -//如果处理参数是List集合且封装了POJO,且页面发送的数据是JSON格式的对象数组,数据将自动映射到集合参数中 +//如果处理参数是List集合且封装了POJO,且页面发送的数据是JSON格式,数据将自动映射到集合参数 public String ajaxListToController(@RequestBody List userList){ System.out.println("controller list :"+userList); return "page.jsp"; @@ -8903,6 +8903,7 @@ public User cross(HttpServletRequest request){ ### 概述 拦截器( Interceptor)是一种动态拦截方法调用的机制 + 作用: 1. 在指定的方法调用前后执行预先设定后的的代码 @@ -9466,7 +9467,7 @@ Restful请求路径简化配置方式:@RestController = @Controller + @Respons `@PathVariable`注解的参数一般在有多个参数的时候添加 -过滤器:HiddenHttpMethodFilter是SpringMVC对Restful风格的访问支持的过滤器 +过滤器:HiddenHttpMethodFilter 是 SpringMVC 对 Restful 风格的访问支持的过滤器 代码实现: @@ -11804,7 +11805,7 @@ public class UserConfig { ``` ```properties -it=itheima +it=seazean ``` ConditionalOnClass:判断环境中是否有对应字节码文件才初始化Bean From 90a5fd97beaee01973ffb5f24dbd1b9cc42e47ed Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 3 Jun 2021 23:15:49 +0800 Subject: [PATCH 037/242] Update Java Notes --- Java.md | 4 +++- Tool.md | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Java.md b/Java.md index f553fa3..5e31229 100644 --- a/Java.md +++ b/Java.md @@ -534,9 +534,11 @@ public class Test1 { case "b": System.out.println("bbb"); break; + default: + break; } ``` - + switch 不支持 long、float、double,switch 的设计初衷是对那些只有少数几个值的类型进行等值判断,如果值过于复杂,那么用 if 比较合适 * break:跳出一层循环 diff --git a/Tool.md b/Tool.md index 3c65961..df0c1f9 100644 --- a/Tool.md +++ b/Tool.md @@ -2203,6 +2203,8 @@ pstree -A #查看所有进程树 装入完成后,CPU 的程序计数器就被设置为 RAM 中操作系统的第一条指令所对应的位置,接下来CPU将开始执行操作系统的指令 +存储在 ROM 中保留很小的自举装入程序,完整功能的自举程序保存在磁盘的启动块上,启动块位于磁盘的固定位,拥有启动分区的磁盘称为启动磁盘或系统磁盘(C盘) + *** From 0a85464f4ddfa23cc9ce6126610dd237d01a2e5c Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 5 Jun 2021 16:15:11 +0800 Subject: [PATCH 038/242] Update Java Notes --- DB.md | 4 +- Java.md | 4823 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- SSM.md | 2 +- 3 files changed, 4694 insertions(+), 135 deletions(-) diff --git a/DB.md b/DB.md index 06273f5..2d4389a 100644 --- a/DB.md +++ b/DB.md @@ -8201,7 +8201,7 @@ hash类型:底层使用**哈希表**结构实现数据存储 hash是指的一个数据类型,并不是一个数据 -* 如果field数量较少,存储结构优化为**类数组结构**(有序) +* 如果field数量较少,存储结构优化为**压缩列表结构**(有序) * 如果field数量较多,存储结构使用HashMap结构(无序) @@ -8674,7 +8674,7 @@ sorted_set类型:在set的存储结构基础上添加可排序字段,类似 Redis 使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的**元素数量比较多**,又或者有序集合中元素的**成员是比较长的字符串**时,Redis就会使用跳跃表来作为有序集合健的底层实现 -跳跃表在链表的基础上增加了多级索引以提升查找的效率,索引是占内存的,所以是一个空间换时间的方案。原始链表中存储的有可能是很大的对象,而索引结点只需要存储关键值值和几个指针,并不需要存储对象,因此当节点本身比较大或者元素数量比较多的时候,其优势可以被放大,而缺点则可以忽略 +跳跃表在链表的基础上增加了多级索引以提升查找的效率,索引是占内存的,所以是一个**空间换时间**的方案。原始链表中存储的有可能是很大的对象,而索引结点只需要存储关键值值和几个指针,并不需要存储对象,因此当节点本身比较大或者元素数量比较多的时候,其优势可以被放大,而缺点则可以忽略 * 基于单向链表加索引的方式实现 diff --git a/Java.md b/Java.md index 5e31229..e100b1f 100644 --- a/Java.md +++ b/Java.md @@ -269,9 +269,9 @@ public class PackegeClass { new Integer(123) 与 Integer.valueOf(123) 的区别在于: -- new Integer(123) : 每次都会新建一个对象; +- new Integer(123):每次都会新建一个对象 -- Integer.valueOf(123) : 会使用缓存池中的对象,多次调用会取得同一个对象的引用。 +- Integer.valueOf(123):会使用缓存池中的对象,多次调用取得同一个对象的引用,反编译后底层调用该方法自动装箱 ```java Integer x = new Integer(123); @@ -293,7 +293,7 @@ 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 = Integer.valueOf(100); @@ -1455,15 +1455,15 @@ public class ClassDemo { ### 封装 -封装的哲学思维:合理隐藏,合理暴露。 -封装最初的目的:提高代码的安全性和复用性,组件化。 +封装的哲学思维:合理隐藏,合理暴露 +封装最初的目的:提高代码的安全性和复用性,组件化 封装的步骤: -1. **成员变量应该私有,用private修饰,只能在本类中直接访问。** -2. **提供成套的getter和setter方法暴露成员变量的取值和赋值。** +1. **成员变量应该私有,用private修饰,只能在本类中直接访问** +2. **提供成套的getter和setter方法暴露成员变量的取值和赋值** -为什么使用private修饰成员变量:实现数据封装,不想让别人使用修改你的数据,比较安全。 +为什么使用private修饰成员变量:实现数据封装,不想让别人使用修改你的数据,比较安全 @@ -1877,7 +1877,7 @@ final修饰静态成员变量可以在哪些地方赋值: 1. 定义的时候赋值一次 -2. 可以在静态代码块中赋值一次。 +2. 可以在静态代码块中赋值一次 ```java public class FinalDemo { @@ -1897,13 +1897,13 @@ public class FinalDemo { ##### 实例成员变量 -final修饰变量的总规则:有且仅能被赋值一次。 +final修饰变量的总规则:有且仅能被赋值一次 final修饰实例成员变量可以在哪些地方赋值1次: -1. 定义的时候赋值一次。 -2. 可以在实例代码块中赋值一次。 -3. 可以在每个构造器中赋值一次。 +1. 定义的时候赋值一次 +2. 可以在实例代码块中赋值一次 +3. 可以在每个构造器中赋值一次 ```java public class FinalDemo { @@ -2254,8 +2254,8 @@ interface InterfaceJDK8{ 多态的执行: -* 对于方法的调用:**编译看左边,运行看右边。** -* 对于变量的调用:**编译看左边,运行看左边。** +* 对于方法的调用:**编译看左边,运行看右边**(分派机制) +* 对于变量的调用:**编译看左边,运行看左边** 多态的使用规则: @@ -4263,7 +4263,7 @@ Collection集合的遍历方式有三种: `public Iterator iterator()` : 获取集合对应的迭代器,用来遍历集合中的元素的 `E next()` : 获取下一个元素值! `boolean hasNext()` : 判断是否有下一个元素,有返回true ,反之 - `default void remove()` : 从底层集合中删除此迭代器返回的最后一个元, 这种方法只能在每次调用next()时调用一次。 + `default void remove()` : 从底层集合中删除此迭代器返回的最后一个元素,这种方法只能在每次调用next() 时调用一次 2. 增强for循环 增强for循环是一种遍历形式,可以遍历集合或者数组,遍历集合实际上是迭代器遍历的简化写法 @@ -4668,8 +4668,6 @@ Set系列集合:添加的元素是无序,不重复,无索引的。 - 同一个对象多次调用hashCode()方法返回的哈希值是相同的 - 默认情况下,不同对象的哈希值是不同的。而重写hashCode()方法,可以实现让不同对象的哈希值相同 - - **HashSet底层就是基于HashMap实现,值是PRESENT = new Object()** Set集合添加的元素是无序,不重复的。 @@ -4717,9 +4715,9 @@ Set集合添加的元素是无序,不重复的。 ##### Linked -**LinkedHashSet**为什么是有序的? +LinkedHashSet 为什么是有序的? -LinkedHashSet底层依然是使用哈希表存储元素的,但是每个元素都额外带一个链来维护添加顺序,不光增删查快,还有序,缺点是多了一个存储顺序的链会**占内存空间**,而且不允许重复,无索引 +LinkedHashSet 底层依然是使用哈希表存储元素的,但是每个元素都额外带一个链来维护添加顺序,不光增删查快,还有序,缺点是多了一个存储顺序的链会**占内存空间**,而且不允许重复,无索引 @@ -5016,11 +5014,6 @@ HashMap继承关系如下图所示: * Serializable 序列化接口,属于标记性接口,HashMap对象可以被序列化和反序列化。 * AbstractMap 父类提供了Map实现接口,以最大限度地减少实现此接口所需的工作 -```java -补充:通过上述继承关系我们发现一个很奇怪的现象, 就是HashMap已经继承了AbstractMap而AbstractMap类实现了Map接口,那为什么HashMap还要在实现Map接口呢?同样在ArrayList中LinkedList中都是这种结构。 -据 java 集合框架的创始人Josh Bloch描述,这样的写法是一个失误。在java集合框架中,类似这样的写法很多,最开始写java集合框架的时候,他认为这样写,在某些地方可能是有价值的,直到他意识到错了。显然的,JDK的维护者,后来不认为这个小小的失误值得去修改,所以就这样存在下来了。 -``` - *** @@ -5137,56 +5130,49 @@ HashMap继承关系如下图所示: jdk8之前数组类型是Entry类型,从jdk1.8之后是Node类型。只是换了个名字,都实现了一样的接口:Map.Entry,负责存储键值对数据的 - 9. 存放缓存 + 9. HashMap中存放元素的个数(**重点**) ```java - //存放具体元素的集合 - transient Set> entrySet; + //存放元素的个数,HashMap中K-V的实时数量,不是table数组的长度 + transient int size; ``` - 10. HashMap中存放元素的个数(**重点**) - -```java -//存放元素的个数,HashMap中K-V的实时数量,不是table数组的长度。 -transient int size; -``` - -11. 记录HashMap的修改次数 +10. 记录HashMap的修改次数 ```java //每次扩容和更改map结构的计数器 transient int modCount; ``` -12. 调整大小下一个容量的值计算方式为(容量*负载因子) - - ```java - //临界值,当实际大小(容量*负载因子)超过临界值时,会进行扩容 - int threshold; - ``` - -13. **哈希表的加载因子(重点)** - - ```java - // 加载因子 - final float loadFactor; - ``` - - * 加载因子的概述 +11. 调整大小下一个容量的值计算方式为(容量*负载因子) - loadFactor加载因子,是用来衡量 HashMap 满的程度,表示**HashMap的疏密程度**,影响hash操作到同一个数组位置的概率,计算HashMap的实时加载因子的方法为:size/capacity,而不是占用桶的数量去除以capacity,capacity 是桶的数量,也就是 table 的长度length。 - - 当HashMap里面容纳的元素已经达到HashMap数组长度的75%时,表示HashMap拥挤,需要扩容,而扩容这个过程涉及到 rehash、复制数据等操作,非常消耗性能,所以开发中尽量减少扩容的次数,可以通过创建HashMap集合对象时指定初始容量来尽量避免。 - - ```java - HashMap(int initialCapacity, float loadFactor)//构造指定初始容量和加载因子的空HashMap - ``` + ```java + //临界值,当实际大小(容量*负载因子)超过临界值时,会进行扩容 + int threshold; + ``` - * 为什么加载因子设置为0.75,初始化临界值是12? +12. **哈希表的加载因子(重点)** - loadFactor太大导致查找元素效率低,存放的数据拥挤,太小导致数组的利用率低,存放的数据会很分散。loadFactor的默认值为**0.75f是官方给出的一个比较好的临界值**。 + ```java + // 加载因子 + final float loadFactor; + ``` - * **threshold**计算公式:capacity(数组长度默认16) * loadFactor(负载因子默认0.75)。这个值是当前已占用数组长度的最大值。**当Size>=threshold**的时候,那么就要考虑对数组的resize(扩容),这就是 **衡量数组是否需要扩增的一个标准**, 扩容后的 HashMap 容量是之前容量的**两倍**. + * 加载因子的概述 + + loadFactor加载因子,是用来衡量 HashMap 满的程度,表示**HashMap的疏密程度**,影响hash操作到同一个数组位置的概率,计算HashMap的实时加载因子的方法为:size/capacity,而不是占用桶的数量去除以capacity,capacity 是桶的数量,也就是 table 的长度length。 + + 当HashMap里面容纳的元素已经达到HashMap数组长度的75%时,表示HashMap拥挤,需要扩容,而扩容这个过程涉及到 rehash、复制数据等操作,非常消耗性能,所以开发中尽量减少扩容的次数,可以通过创建HashMap集合对象时指定初始容量来尽量避免。 + + ```java + HashMap(int initialCapacity, float loadFactor)//构造指定初始容量和加载因子的空HashMap + ``` + + * 为什么加载因子设置为0.75,初始化临界值是12? + + loadFactor太大导致查找元素效率低,存放的数据拥挤,太小导致数组的利用率低,存放的数据会很分散。loadFactor的默认值为**0.75f是官方给出的一个比较好的临界值**。 + + * **threshold**计算公式:capacity(数组长度默认16) * loadFactor(负载因子默认0.75)。这个值是当前已占用数组长度的最大值。**当Size>=threshold**的时候,那么就要考虑对数组的resize(扩容),这就是 **衡量数组是否需要扩增的一个标准**, 扩容后的 HashMap 容量是之前容量的**两倍**. @@ -5537,7 +5523,7 @@ transient int size; p.next = node.next; ++modCount; --size; - //该方法HashMap没有任何实现逻辑,目的是为了让子类根据需要自行覆盖 + //LinkedHashMap afterNodeRemoval(node); return node; } @@ -12332,11 +12318,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重新编译 -* 在1.8之前,分层编译默认是关闭的,可以添加`-server -XX:+TieredCompilation`参数进行开启 **逃逸分析**:并不是直接的优化手段,而是一个代码分析,通过动态分析对象的作用域,为其它优化手段如栈上分配、标量替换和同步消除等提供依据,发生逃逸行为的情况有两种:方法逃逸和线程逃逸 @@ -12662,16 +12647,16 @@ objD.fieldG = G; // 写 解决方法:添加读写屏障,读屏障拦截第一步,写屏障拦截第二三步,在读写前后进行一些后置处理 (AOP) +* **写屏障 + 增量更新**:针对新增的引用,记录下新的引用对象,最后进行重新遍历标记 + + 增量更新 (Incremental Update) 破坏了条件二,从而保证了不会漏标 + * **写屏障 (Store Barrier) + SATB**:当原来成员变量的引用发生变化之前,记录下原来的引用对象 保留GC开始时的对象图,即原始快照 SATB,当GC Roots确定后,对象图就已经确定,那后续的标记也应该是按照这个时刻的对象图走,如果期间发生变化则记录下来,以后根据这些记录重新标记 SATB (Snapshot At The Beginning) 破坏了条件一,从而保证了不会漏标 -* **写屏障 + 增量更新**:针对新增的引用,记录下新的引用对象,最后进行重新遍历标记 - - 增量更新 (Incremental Update) 破坏了条件二,从而保证了不会漏标 - * **读屏障 (Load Barrier)**:破坏条件二,黑色对象引用白色对象的前提是获取到该对象,此时读屏障发挥作用 以Java HotSpot VM为例,其并发标记时对漏标的处理方案如下: @@ -14249,12 +14234,16 @@ HotSpot VM 可以通过VM参数设置程序执行方式: #### 热点探测 -热点代码:被JIT编译器编译的字节码,根据代码被调用执行的频率而定 +热点代码:被 JIT 编译器编译的字节码,根据代码被调用执行的频率而定 * 一个被多次调用的方法或者一个循环次数较多的循环体都可以被称之为热点代码 * 这种编译方式发生在方法的执行过程中,也称为栈上替换,简称OSR (On StackReplacement) 编译 -热点探测:JIT编译器在运行时会针热点代码做出深度优化,将其直接编译为对应平台的本地机器指令,以提升Java程序的执行性能 +OSR 替换循环代码体的入口,C1、C2 替换的是方法调用的入口,OSR 编译后会出现方法的整段代码被编译了,但是只有循环体部分才执行编译后的机器码,其他部分仍是解释执行 + +热点探测:JIT编译器在运行时会针热点代码做出深度优化,将其直接编译为对应平台的本地机器指令进行缓存,以提升Java程序的执行性能 + +CodeCache 用于缓存编译后的机器码,动态生成的代码和本地方法代码(JNI),如果 CodeCache 区域被占满,编译器被停用,字节码将不会编译为机器码,应用程序继续运行,但运行速度会降低一个数量级,严重影响系统性能 HotSpot VM采用的热点探测方式是基于计数器的热点探测,为每一个方法都建立2个不同类型的计数器:方法调用计数器 (Invocation Counter) 和回边计数器 (BackEdge Counter) @@ -14272,7 +14261,7 @@ HotSpot VM采用的热点探测方式是基于计数器的热点探测,为每 #### 分层编译 -HotSpot VM中内嵌有两个JIT编译器,分别为Client Compiler和Server Compiler,简称为C1编译器和C2编译器 +HotSpot VM 内嵌有两个JIT编译器,分别为 Client Compiler 和 Server Compiler,简称为C1编译器和C2编译器 C1编译器会对字节码进行简单可靠的优化,耗时短,以达到更快的编译速度,C1编译器的优化方法: @@ -14299,18 +14288,19 @@ C1编译器会对字节码进行简单可靠的优化,耗时短,以达到更 System.out.println(81); ``` -* 冗余消除:在运行期间把一些不会执行的代码折叠掉 +* 冗余消除:根据运行时状况进行代码折叠或削除 * 内联缓存:是一种加快动态绑定的优化技术 -C2编译器进行耗时较长的优化以及激进优化,优化的代码执行效率更高,C2的优化主要是在全局层面,逃逸分析是优化的基础:标量替换、栈上分配、同步消除 +C2编译器进行耗时较长的优化以及激进优化,优化的代码执行效率更高,当激进优化的假设不成立时,再退回使用C1编译。C2的优化主要是在全局层面,逃逸分析是优化的基础:标量替换、栈上分配、同步消除 VM 参数设置: -- -client:指定Java虚拟机运行在 Client 模式下,并使用C1编译器 -- -server:指定Java虚拟机运行在 Server 模式下,并使用C2编译器 +- -client:指定 Java 虚拟机运行在 Client 模式下,并使用C1编译器 +- -server:指定 Java 虚拟机运行在 Server 模式下,并使用C2编译器 +- `-server -XX:+TieredCompilation`:在1.8之前,分层编译默认是关闭的,可以添加该参数开启 -分层编译策略 (Tiered Compilation):程序解释执行可以触发C1编译,将字节码编译成机器码。加上性能监控,C2编译会根据性能监控信息进行激进优化,JVM 将执行状态分成了 5 个层次: +分层编译策略 (Tiered Compilation):程序解释执行可以触发C1编译,将字节码编译成机器码,加上性能监控,C2编译会根据性能监控信息进行激进优化,JVM 将执行状态分成了 5 个层次: * 0 层,解释执行(Interpreter) @@ -14320,7 +14310,7 @@ VM 参数设置: * 3 层,使用 C1 即时编译器编译执行(带完全的 profiling) -* 4 层,使用 C2 即时编译器编译执行 +* 4 层,使用 C2 即时编译器编译执行(C1 和 C2 协作运行) 说明:profiling 是指在运行过程中收集一些程序执行状态的数据,例如方法的调用次数,循环的回边次数等 @@ -14425,9 +14415,9 @@ public static int invoke(Object... args) { 非虚方法: -- 非虚方法在编译器就确定了具体的调用版本,这个版本在运行时是不可变的 +- 非虚方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的 - 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法 -- 非私有实例方法称为虚方法 +- 所有普通成员方法和被重写的方法都是虚方法 动态类型语言和静态类型语言: @@ -14587,25 +14577,17 @@ Java 虚拟机中关于方法重写的判定基于方法描述符,如果子类 - 前者是通过方法重载实现,后者是通过重写实现(子类覆盖父类方法,虚方法表) - 虚方法:运行时动态绑定的方法,对比静态绑定的非虚方法调用来说,虚方法调用更加耗时 -执行 invokevirtual 指令: - -1. 先通过栈帧中的对象引用找到对象 -2. 分析对象头,找到对象的实际 Class -3. Class 结构中有 vtable -4. 查表得到方法的具体地址 -5. 执行方法的字节码 - 方法重写的本质: 1. 找到操作数栈的第一个元素所执行的对象的实际类型,记作 C -2. 如果在类型 C 中找到与常量中的描述符和名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常 +2. 如果在类型 C 中找到与常量中的描述符和名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回 java.lang.IllegalAccessError 异常 IllegalAccessError:表示程序试图访问或修改一个属性或调用一个方法,这个属性或方法没有权限访问,一般会引起编译器异常。如果这个错误发生在运行时,就说明一个类发生了不兼容的改变 3. 找不到,就会按照继承关系从下往上依次对 C 的各个父类进行第二步的搜索和验证过程 -4. 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常 +4. 如果始终没有找到合适的方法,则抛出 java.lang.AbstractMethodError 异常 @@ -14617,7 +14599,9 @@ Java 虚拟机中关于方法重写的判定基于方法描述符,如果子类 在虚拟机工作过程中会频繁使用到动态分配,每次动态分配的过程中都要重新在类的元数据中搜索合适目标,影响到执行效率。为了提高性能,JVM 采取了一种用**空间换取时间**的策略来实现动态绑定,在类的方法区建立一个虚方法表(virtual method table),实现使用索引表来代替查找,可以快速定位目标方法 -* invokevirtual 所使用的虚方法表(virtual method table,vtable) +* invokevirtual 所使用的虚方法表(virtual method table,vtable),执行流程 + 1. 先通过栈帧中的对象引用找到对象,分析对象头,找到对象的实际 Class + 2. Class 结构中有 vtable,查表得到方法的具体地址,执行方法的字节码 * invokeinterface 所使用的接口方法表(interface method table,itable) 虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,jvm会把该类的方法表也初始化完毕 @@ -14643,7 +14627,7 @@ Passenger 类的方法表包括两个方法,分别对应 0 号和 1 号。方 * 使用了方法表的动态绑定与静态绑定相比,仅仅多出几个内存解引用操作:访问栈上的调用者、读取调用者的动态类型、读取该类型的方法表、读取方法表中某个索引值所对应的目标方法,但是相对于创建并初始化 Java 栈帧这操作的开销可以忽略不计 * 上述优化的效果看上去不错,但实际上**仅存在于解释执行**中,或者即时编译代码的最坏情况。因为即时编译还拥有另外两种性能更好的优化手段:内联缓存(inlining cache)和方法内联(method inlining) -![](https://gitee.com/seazean/images/raw/master/Java/JVM-方法调用虚方法表图.png) + @@ -14659,9 +14643,9 @@ Passenger 类的方法表包括两个方法,分别对应 0 号和 1 号。方 * 单态 (monomorphic):指的是仅有一种状态的情况 * 多态 (polymorphic):指的是有限数量种状态的情况,二态(bimorphic)是多态的其中一种 -* 超多态 (megamorphic):指的是更多种状态的情况。通常我们用一个具体数值来区分多态和超多态,在这个数值之下,称之为多态,否则称之为超多态 +* 超多态 (megamorphic):指的是更多种状态的情况,通常用一个具体数值来区分多态和超多态,在这个数值之下,称之为多态,否则称之为超多态 -对于内联缓存来说,有对应的单态内联缓存、多态内联缓存和超多态内联缓存: +对于内联缓存来说,有对应的单态内联缓存、多态内联缓存: * 单态内联缓存:只缓存了一种动态类型以及所对应的目标方法,实现简单,比较所缓存的动态类型,如果命中,则直接调用对应的目标方法。 * 多态内联缓存:缓存了多个动态类型及其目标方法,需要逐个将所缓存的动态类型与当前动态类型进行比较,如果命中,则调用对应的目标方法 @@ -15260,7 +15244,7 @@ JDK5以后编译阶段自动转换成上述片段 #### 泛型擦除 泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行**泛型擦除**的动作,即泛型信息 -在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理: +在编译为字节码之后就丢失了,实际的类型都**当做了 Object 类型**来处理: ```java List list = new ArrayList<>(); @@ -15867,6 +15851,8 @@ JDK 自带了**监控工具,位于 JDK 的 bin 目录下**,其中最常用 +*** + ### 对比 @@ -15895,8 +15881,9 @@ JDK 自带了**监控工具,位于 JDK 的 bin 目录下**,其中最常用 * 套接字:与其它通信机制不同的是,它可用于不同机器间的进程通信 * 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量 - Java中的通信机制:volatile、等待/通知机制、join方式、threadLocal - + + Java中的通信机制:volatile、等待/通知机制、join方式、ThreadLocal + * 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低 @@ -19386,7 +19373,7 @@ String 类也是不可变的,该类和类中所有属性都是 final 的 } ``` -更改String类数据时,会构造新字符串对象,生成新的 char[] value,这种通过创建副本对象来避免共享的方式称之为**保护性拷贝(defensive copy)** +更改 String 类数据时,会构造新字符串对象,生成新的 char[] value,这种通过创建副本对象来避免共享的方式称之为**保护性拷贝(defensive copy)** @@ -22596,6 +22583,8 @@ class DataContainerStamped { ## 并发包 +(源码分析待更新) + ### Semaphore #### 信号量 @@ -22612,7 +22601,7 @@ Semaphore(信号量)用来限制能同时访问共享资源的线程上限 常用API: * `public void acquire()`:表示获取许可 -* `public void release()`:表示释放许可,acquire()和release()方法之间的代码为"同步代码" +* `public void release()`:表示释放许可,acquire()和release()方法之间的代码为同步代码 ```java public static void main(String[] args) { @@ -23215,9 +23204,8 @@ public ConcurrentHashMap(int initialCapacity,float loadFactor,int concurrencyLev ##### 成员方法 -1. put - 数组简称(table),链表简称(bin) - +1. put():数组简称(table),链表简称(bin) + ```java public V put(K key, V value) { return putVal(key, value, false); @@ -23298,7 +23286,7 @@ public ConcurrentHashMap(int initialCapacity,float loadFactor,int concurrencyLev return null; } ``` - + 2. initTable ```java @@ -23765,7 +23753,7 @@ ConcurrentHashMap 对锁粒度进行了优化,**分段锁技术**,将整张 CopyOnWriteArrayList 采用了**写入时拷贝**的思想,增删改操作会将底层数组拷贝一份,更改操作在新数组上执行,不影响其它线程的**并发读,读写分离** -CopyOnWriteArraySet底层对CopyOnWriteArrayList进行了包装,装饰器模式 +CopyOnWriteArraySet 底层对 CopyOnWriteArrayList 进行了包装,装饰器模式 ```java public CopyOnWriteArraySet() { @@ -23773,6 +23761,12 @@ public CopyOnWriteArraySet() { } ``` +* 存储结构: + + ```java + private transient volatile Object[] array;//保证了读写线程之间的可见性 + ``` + * 新增数据: ```java @@ -23801,7 +23795,7 @@ 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) { @@ -23814,8 +23808,30 @@ public CopyOnWriteArraySet() { ``` 适合读多写少的应用场景 + +* 迭代器: + + CopyOnWriteArrayList 在返回一个迭代器时,会基于创建这个迭代器时内部数组所拥有的数据,创建一个该内部数组当前的快照,然后迭代器遍历的是该快照,而不是内部的数组,所以这种实现方式也存在一定的数据延迟性,即对其他线程并行添加的数据不可见 + ```java + public Iterator iterator() { + return new COWIterator(getArray(), 0); + } + + // 迭代器会创建一个底层array的快照,故主类的修改不影响该快照 + static final class COWIterator implements ListIterator { + // 内部数组快照 + private final Object[] snapshot; + + //... + // 不支持写操作 + public void remove() { + throw new UnsupportedOperationException(); + } + } + ``` + *** @@ -23867,7 +23883,7 @@ public static void main(String[] args) throws InterruptedException { 不一定弱一致性就不好 -* 数据库的 MVCC 都是弱一致性的表现 +* 数据库的事务隔离级别都是弱一致性的表现 * 并发高和一致性是矛盾的,需要权衡 @@ -23878,12 +23894,12 @@ public static void main(String[] args) throws InterruptedException { #### 安全失败 -**在 java.util 包的集合类就都是快速失败的,而 java.util.concurrent 包下的类都是安全失败** +在 java.util 包的集合类就都是快速失败的,而 java.util.concurrent 包下的类都是安全失败 -* 快速失败:在 A 线程使用迭代器对集合进行遍历的过程中,此时 B 线程对集合进行修改(增删改),或者 A 线程在遍历过程中对集合进行修改,都会导致 A 线程抛出 ConcurrentModificationException 异常 +* 快速失败:在 A 线程使用**迭代器**对集合进行遍历的过程中,此时 B 线程对集合进行修改(增删改),或者 A 线程在遍历过程中对集合进行修改,都会导致 A 线程抛出 ConcurrentModificationException 异常 * AbstractList 类中的成员变量 modCount,用来记录 List 结构发生变化的次数,**结构发生变化**是指添加或者删除至少一个元素的操作,或者是调整内部数组的大小,仅仅设置元素的值不算结构发生变化 * 在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了抛出 CME 异常 -* 安全失败:采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,故不会抛 ConcurrentModificationException 异常 +* 安全失败:采用安全失败机制的集合容器,在**迭代器**遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在复制集合上进行遍历。由于迭代时不是对原集合进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,故不会抛 ConcurrentModificationException 异常 @@ -23899,6 +23915,9 @@ Collections类是用来操作集合的工具类,提供了集合转换成线程 public static Collection synchronizedCollection(Collection c) { return new SynchronizedCollection<>(c); } +public static Map synchronizedMap(Map m) { + return new SynchronizedMap<>(m); +} ``` 源码:底层也是对方法进行加锁 @@ -24055,7 +24074,7 @@ BaseHeader存储数据,headIndex存储索引,纵向上**所有索引指向 * findPredecessor():寻找前驱节点 - 从最上层的头索引开始向右查找(链表的后续索引),如果后续索引的节点的Key大于要查找的Key,则头索引移到下层链表,在下层链表查找,以此反复,一直查找到没有下层的分层索引为止,返回该索引的节点。如果后续索引的节点的Key小于要查找的Key,则在该层链表中向后查找。由于查找的key永远小于索引节点的Key,所以只能找到目标的前置索引节点。其中会有空值索引的存在,通过CAS来处理 + 从最上层的头索引开始向右查找(链表的后续索引),如果后续索引的节点的Key大于要查找的Key,则头索引移到下层链表,在下层链表查找,以此反复,一直查找到没有下层的分层索引为止,返回该索引的节点。如果后续索引的节点的Key小于要查找的Key,则在该层链表中向后查找。由于查找的key可能永远大于索引节点的 key,所以只能找到目标的前置索引节点。如果遇到空值索引的存在,通过CAS来断开索引 ```java private Node findPredecessor(Object key, Comparator cmp) { @@ -24070,7 +24089,7 @@ BaseHeader存储数据,headIndex存储索引,纵向上**所有索引指向 K k = n.key; //3.n.value为null说明节点n正在删除的过程中 if (n.value == null) { - //在index层直接删除r节点,用在删除节点中 + //在index层直接删除r索引节点,用在删除节点中 if (!q.unlink(r)) break;//重新从 head 节点开始查找,break到步骤1 //删除节点r成功,获取新的r节点, 回到步骤 2 @@ -24125,7 +24144,7 @@ BaseHeader存储数据,headIndex存储索引,纵向上**所有索引指向 Comparator cmp = comparator; // outer循环,处理并发冲突等其他需要重试的情况 outer: for (;;) { - //0 + //0.for (;;) //1.将 key 对应的前继节点找到, b为前继节点, n是前继节点的next, // 若没发生条件竞争,最终key在b与n之间 (找到的b在base_level上) for (Node b = findPredecessor(key, cmp), n = b.next;;) { @@ -24134,7 +24153,7 @@ BaseHeader存储数据,headIndex存储索引,纵向上**所有索引指向 Object v; int c; //3.获取 n 的右节点 Node f = n.next; - //4.条件竞争( + //4.条件竞争 // 并发下其他线程在b之后插入节点或直接删除节点n, break到步骤0 if (n != b.next) break; @@ -24179,6 +24198,7 @@ BaseHeader存储数据,headIndex存储索引,纵向上**所有索引指向 } } // 以上插入节点已经完成,剩下的任务要根据随机数的值来表示是否向上增加层数与上层索引 + // 随机数 int rnd = ThreadLocalRandom.nextSecondarySeed(); @@ -24323,29 +24343,12 @@ BaseHeader存储数据,headIndex存储索引,纵向上**所有索引指向 ```java private Node findNode(Object key) { - //原理与doGet相同,无非是findNode返回节点,doGet返回 - //寻找到 key 返回 + //原理与doGet相同,无非是findNode返回节点,doGet返回value if ((c = cpr(cmp, key, n.key)) == 0) return n; } ``` -* helpDelete - - ```java - void helpDelete(Node b, Node f) { - //this节点的后续节点为f,且本身为b的后续节点,一般都是正确的,除非被别的线程删除 - if (f == next && this == b.next) { - //如果f节点为null,说明this是尾节点,f节点值被其他线程修改, - if (f == null || f.value != f) - //通过CAS生成一个key为null,value为this,next为f - casNext(f, new Node(f)); - else - //如果f不为空,通过CAS,将f.next替换掉this节点,即删除本身节点 - b.casNext(this, f.next); - } - } - ``` @@ -24483,6 +24486,33 @@ BaseHeader存储数据,headIndex存储索引,纵向上**所有索引指向 ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentSkipListMap-remove流程.png) +* appendMarker() + + ```java + //添加删除标记节点 + boolean appendMarker(Node f) { + //通过CAS生成一个key为null,value为this,next为f的标记节点 + return casNext(f, new Node(f)); + } + ``` + +* helpDelete() + + ```java + //将添加了删除标记的节点清除 + void helpDelete(Node b, Node f) { + //this节点的后续节点为f,且本身为b的后续节点,一般都是正确的,除非被别的线程删除 + if (f == next && this == b.next) { + //如果n还还没有被标记 + if (f == null || f.value != f) + casNext(f, new Node(f)); + else + //通过CAS,将b的下一个节点n变成f.next,即成为图中的样式 + b.casNext(this, f.next); + } + } + ``` + * tryReduceLevel() ```java @@ -24816,6 +24846,4535 @@ final void updateHead(Node h, Node p) { -# Design +# SDP + +## 软件设计 + +### 设计模式 + +软件设计模式(Software Design Pattern),本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解 + +- 可以提高程序员的思维能力、编程能力和设计能力 +- 使程序设计更加标准化、代码编制更加工程化,使软件开发效率大大提高,从而缩短软件的开发周期 +- 使设计的代码可重用性高、可读性强、可靠性高、灵活性好、可维护性强 + +设计模式分类: + +* **创建型模式**:用于描述如何创建对象,主要特点是将对象的创建与使用分离。GoF 书中提供了单例、原型、工厂方法、抽象工厂、建造者等 5 种创建型模式 +* **结构型模式**:用于描述如何将类或对象按某种布局组成更大的结构,GoF 书中提供了代理、适配器、桥接、装饰、外观、享元、组合等 7 种结构型模式 + +* **行为型模式**:用于描述类或对象之间怎样相互协作共同完成单个对象无法单独完成的任务,以及怎样分配职责。GoF 书中提供了模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器等 11 种行为型模式 + + + +*** + + + +### UML图 + +#### 基本介绍 + +统一建模语言(Unified Modeling Language,UML)是用来设计软件的可视化建模语言,特点是简单、统一、图形化、能表达软件设计中的动态与静态信息 + +UML 从目标系统的不同角度出发,定义了用例图、类图、对象图、状态图、活动图、时序图、协作图、构件图、部署图等 9 种图 + +类图(Class diagram)是显示了模型的静态结构,特别是模型中存在的类、类的内部结构以及它们与其他类的关系等,类图不显示暂时性的信息,类图是面向对象建模的主要组成部分 + +类图的作用: + +* 在软件工程中,类图是一种静态的结构图,描述了系统的类的集合,类的属性和类之间的关系,可以简化人们对系统的理解 +* 类图是系统分析和设计阶段的重要产物,是系统编码和测试的重要模型 + + + +**** + + + +#### 表示方法 + +在UML类图中,类使用包含类名、属性(field) 和方法(method) 且带有分割线的矩 形来表示,比如下图表示一个Employee 类,包含 name、age 和 address这3个属性,以及 work() 方法 + +![](https://gitee.com/seazean/images/raw/master/Java/Design-类图的表示方法.jpg) + +属性/方法名称前加的+和-表示了这个属性/方法的可见性,UML类图中表示可见性的符号有三种: + +* +:表示public + +* -:表示private + +* #:表示protected + +属性的完整表示方式是: **可见性 名称 :类型 [ = 缺省值]** + +方法的完整表示方式是: **可见性 名称(参数列表) [ : 返回类型]** + +* 中括号中的内容表示是可选的 +* 也有将类型放在变量名前面,返回值类型放在方法名前面 + + + +**** + + + +#### 关联关系 + +关联关系是对象之间的一种引用关系,用于表示一类对象与另一类对象之间的联系,如老师和学生。关联关系是类与类之间最常用的一种关系,分为一般关联关系、聚合关系和组合关系 + +一般关联又可以分为单向关联,双向关联,自关联 + +* 单向关联:在UML类图中单向关联用一个带箭头的实线表示,下图表示每个顾客都有一个地址,通过让 Customer 类持有一个类型为 Address 的成员变量类实现 + + + +* 双向关联:双方各自持有对方类型的成员变量 + + + +* 自关联:在UML类图中用一个带有箭头且指向自身的线表示,下图的意思就是Node类包含类型为Node的成员变量,也就是自己包含自己 + + + + + +**** + + + +#### 聚合关系 + +聚合关系是关联关系的一种,是强关联关系,是整体和部分之间的关系。聚合关系通过成员对象来实现,其中成员对象是整体对象的一部分,但是成员对象可以脱离整体对象而独立 存在 + +在 UML 类图中,聚合关系可以用带空心菱形的实线来表示,菱形指向整体,下图表示学校与老师的关系,学校包含老师,但如果学校倒闭了,老师依然存在 + + + + + +*** + + + +#### 组合关系 + +组合关系表示类之间的整体与部分的关系,是一种更强烈的聚合关系。整体对象可以控制部分对象的生命周期,一旦整体对象不存在,部分对象也将不存在,部分对象不能脱离整体对象而存在 + +在 UML 类图中,组合关系用带实心菱形的实线来表示,菱形指向整体,下图所示是头和嘴的关系图: + + + + + +*** + + + +#### 依赖关系 + +依赖关系是一种使用关系,对象之间耦合度最弱的一种关联方式,是临时性的关联。在代码中,某个类的方法通过局部变量、方法的参数或者对静态方法的调用来访问另一个类(被依赖类)中的某些方法来完成一些职责 + +在 UML 类图中,依赖关系使用带箭头的虚线来表示,箭头从使用类指向被依赖的类 + + + + + +*** + + + +#### 继承关系 + +继承关系是对象之间耦合度最大的一种关系,表示一般与特殊的关系,是父类与子类之间的关系,是一种继承关系 + +在 UML 类图中,泛化关系用带空心三角箭头的实线来表示,箭头从子类指向父类。在代码实现时,使用面向对象的继承机制来实现泛化关系。例如 Student 类和 Teacher 类都是 Person 类的子类,其类图如下图所示: + + + + + +**** + + + +#### 实现关系 + +实现关系是接口与实现类之间的关系,类实现了接口,类中的操作实现了接口中所声明的所有的抽象操作 + +在 UML 类图中,实现关系使用带空心三角箭头的虚线来表示,箭头从实现类指向接口,比如汽车和船实现了交通工具: + + + + + +*** + + + +### 设计原则 + +#### 开闭原则 + +**对扩展开放,对修改关闭**,在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果,简言之,是为了使程序的扩展性好,易于维护和升级,使用接口和抽象类 + +抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定,软件中易变的细节可以从抽象派生来的实现类来进行扩展,当软件需要发生变化时,只需要根据需求重新派生一个实现类来扩展就可以。 + + + +*** + + + +#### 里氏代换 + +里氏代换原则是面向对象设计的基本原则之一 + +里氏代换原则:任何基类可以出现的地方,子类一定可以出现,就是子类可以扩展父类的功能,但不能改变父类原有的功能,也就是子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法 + +如果通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大 + + + +*** + + + +#### 依赖倒转 + +高层模块不应该依赖实现模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合 + +比如说组装一台电脑,如果组装的电脑的cpu只能是Intel的,内存条只能是金士顿的,硬盘只能是希捷的,这对用户肯定是不友好的,所以需要把 CPU、内存、硬盘提取成接口类 + + + +*** + + + +#### 接口隔离 + +客户端不应该被迫依赖于不使用的方法,一个类对另一个类的依赖应该建立在最小的接口上 + +比如说 Person 类有姓名、年龄、收入,但是子类 Teacher 类有收入,Student 没有,所以需要设置三个接口。 + + + +*** + + + +#### 迪米特 + +迪米特法则又叫最少知识原则,如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用,其目的是降低类之间的耦合度,提高模块的相对独立 + +比如明星与经纪人的关系: + +![](https://gitee.com/seazean/images/raw/master/Java/Design-迪米特法则.png) + + + +*** + + + +#### 合成复用 + +合成复用原则:尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现 + +类的复用分为继承复用和合成复用两种 + +* 继承复用的缺点: + 1. 继承复用破坏了类的封装性,继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱复用” + 2. 子类与父类的耦合度高,父类的实现的任何改变都会导致子类的实现发生变化,不利于类的扩展与维护 + 3. 限制了复用的灵活性,从父类继承而来的实现是非虚方法,在编译时已经绑定,在运行时不能发生变化 + +* 采用组合或聚合复用,将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能 + 1. 维持了类的封装性,因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用 + 2. 对象间的耦合度低,可以在类的成员位置声明抽象 + 3. 复用的灵活性高,这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象 + +比如:汽车按动力源分为汽油汽车、电动汽车等;按颜色划分可分为白色汽车、黑色汽车和红色汽车等 + +* 类图: + + + +* 将继承复用改为聚合复用,把颜色当作属性: + + + + + + + +**** + + + + + +## 创建型 + +### 单例模式 + +#### 基本介绍 + +创建型模式的主要关注点是怎样创建对象,将对象的创建与使用分离,降低系统的耦合度,使用者不需要关注对象的创建细节 + +创建型模式分为:单例模式、工厂方法模式、抽象工程模式、原型模式、建造者模式 + +单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一,提供了一种创建对象的最佳方式 + +单例设计模式分类两种: + +* 饿汉式:类加载就会导致该单实例对象被创建 + +* 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建 + + + +*** + + + +#### 饿汉式 + +饿汉式在类加载的过程导致该单实例对象被创建,虚拟机会保证类加载的线程安全,但是如果只是为了加载该类不需要实例,则会造成内存的浪费 + +* 静态变量的方式: + + ```java + public class Singleton { + //私有构造方法 + private Singleton() {} + //在成员位置创建该类的对象 + private static final Singleton instance = new Singleton(); + //对外提供静态方法获取该对象 + public static Singleton getInstance() { + return instance; + } + + //解决序列化问题 + protected Object readResolve() { + return INSTANCE; + } + } + ``` + + * 问题1:为什么类加 final 修饰? + 不被子类继承,防止子类中不适当的行为覆盖父类的方法,破坏了单例 + + * 问题2:如果实现了序列化接口,怎么防止防止反序列化破坏单例? + + * 对单例声明 transient,然后实现 readObject(ObjectInputStream in) 方法,复用原来的单例 + + 条件:访问权限为private/protected、返回值必须是Object、异常可以不抛 + + * 实现readResolve()方法,当 JVM 从内存中反序列化地"组装"一个新对象,就会自动调用readResolve方法返回原来单例 + + * 问题3:为什么构造方法设置为私有? 是否能防止反射创建新的实例? + 防止其他类无限创建对象;不能防止反射破坏 + + * 问题4:这种方式是否能保证单例对象创建时的线程安全? + 能,静态变量初始化在类加载时完成,由JVM保证线程安全 + + * 问题5:为什么提供静态方法而不是直接将 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:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做?添加构造方法 + + 反编译结果: + + ```java + public final class Singleton extends java.lang.Enum {//Enum实现序列化接口 + public static final Singleton INSTANCE = new Singleton(); + } + ``` + + + + + +*** + + + +#### 懒汉式 + +* 线程不安全 + + ```java + public class Singleton { + //私有构造方法 + private Singleton() {} + + //在成员位置创建该类的对象 + private static Singleton instance; + + //对外提供静态方法获取该对象 + public static Singleton getInstance() { + if(instance == null) { + //多线程环境,会出现线程安全问题,可能多个线程同时进入这里 + instance = new Singleton(); + } + return instance; + } + } + ``` + +* 双端检锁机制 + + 在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作,所以需要使用 `volatile` 关键字 + + ```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; + } + } + ``` + +* 静态内部类方式 + + ```java + public class Singleton { + //私有构造方法 + private Singleton() {} + + private static class SingletonHolder { + private static final Singleton INSTANCE = new Singleton(); + } + + //对外提供静态方法获取该对象 + public static Singleton getInstance() { + return SingletonHolder.INSTANCE; + } + } + ``` + + * 内部类属于懒汉式,类加载本身就是懒惰的,首次调用时加载,然后对单例进行初始化 + * 没有线程安全问题,静态变量初始化在类加载时完成,由 JVM 保证线程安全 + + + +*** + + + +#### 破坏单例 + +##### 序列化 + +将单例对象序列化再反序列化,对象从内存反序列化到程序中会重新创建一个对象,通过反序列化得到的对象是不同的对象,而且得到的对象不是通过构造器得到的,反序列化得到的对象不执行构造器 + +* Singleton + + ```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; + } + } + ``` + +* 序列化 + + ```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); + } + } + ``` + +* 解决方法: + + 在 Singleton 类中添加`readResolve()`方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新创建的对象 + + ```java + private Object readResolve() { + return SingletonHolder.INSTANCE; + } + ``` + + ObjectInputStream类源码分析: + + ```java + public final Object readObject() throws IOException, ClassNotFoundException{ + int outerHandle = passHandle; + try { + Object obj = readObject0(false);//重点查看readObject0方法 + } + } + + private Object readObject0(boolean unshared) throws IOException { + try { + switch (tc) { + case TC_OBJECT: + return checkResolve(readOrdinaryObject(unshared)); + //重点查看readOrdinaryObject方法 + } + } + } + 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; + } + ``` + + + +*** + + + +##### 反射 + +* 反射 + + ```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; + } + } + } + ``` + + + + + +*** + + + +#### Runtime + +Runtime 类就是使用的单例设计模式中的饿汉式 + +```java +public class Runtime { + private static Runtime currentRuntime = new Runtime(); + public static Runtime getRuntime() { + return currentRuntime; + } + private Runtime() {} + ... +} +``` + +使用 Runtime + +```java +public class RuntimeDemo { + public static void main(String[] args) throws IOException { + //获取Runtime类对象 + Runtime runtime = Runtime.getRuntime(); + + //返回 Java 虚拟机中的内存总量。 + System.out.println(runtime.totalMemory()); + //返回 Java 虚拟机试图使用的最大内存量。 + System.out.println(runtime.maxMemory()); + + //创建一个新的进程执行指定的字符串命令,返回进程对象 + 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")); + } +} +``` + + + + + +**** + + + +### 工厂模式 + +#### 基本介绍 + +工厂模式:使用工厂来生产对象,彻底和对象解耦,如果要更换对象,直接在工厂里更换该对象即可 + +三种工厂: + +* 简单工厂模式(不属于 GOF 的23种经典设计模式) +* 工厂方法模式 +* 抽象工厂模式 + + + +*** + + + +#### 简单工厂 + +简单工厂包含如下角色: + +* 抽象产品 :定义了产品的规范,描述了产品的主要特性和功能 +* 具体产品 :实现或者继承抽象产品的子类 +* 具体工厂 :提供了创建产品的方法,调用者通过该方法来获取产品 + +实现代码: + +* 抽象类: + + ```java + public abstract class Coffee { + public abstract String getName(); + } + ``` + +* 实现类: + + ```java + public class AmericanCoffee extends Coffee { + public String getName() { + return "美式咖啡"; + } + } + public class LatteCoffee extends Coffee { + public String getName() { + return "拿铁咖啡"; + } + } + ``` + +* 简单工厂类,也可以修改为静态工厂模式,在 createCoffee 方法加 static + + ```java + public class SimpleCoffeeFactory { + public Coffee createCoffee(String type) { + //声明Coffee类型的变量,根据不同类型创建不同的coffee子类对象 + Coffee coffee = null; + if("american".equals(type)) { + coffee = new AmericanCoffee(); + } else if("latte".equals(type)) { + coffee = new LatteCoffee(); + } else { + throw new RuntimeException("对不起,您所点的咖啡没有"); + } + + return coffee; + } + } + ``` + +* 测试类: + + ```java + public class Client { + public static void main(String[] args) { + SimpleCoffeeFactory factory = new SimpleCoffeeFactory(); + Coffee coffee = factory.createCoffee("latte"); + + System.out.println(coffee.getName()); + } + } + ``` + +优点: + +* 封装了创建对象的过程,可以通过参数直接获取对象。 +* 把对象的创建和业务逻辑层分开,如果要实现新产品直接修改工厂类,而不需要在原代码中修改,扩展性高 + +缺点:实现新产品需要修改 SimpleCoffeeFactory 的代码,违反了开闭原则 + + + +*** + + + +#### 工厂方法 + +定义一个用于创建对象的接口,让子类决定实例化哪个产品类对象,工厂方法使一个产品类的实例化延迟到其工厂的子类 + +工厂方法模式的主要角色: + +* 抽象工厂:提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法来创建产品 +* 具体工厂:主要是实现抽象工厂中的抽象方法,完成具体产品的创建。 +* 抽象产品:定义了产品的规范,描述了产品的主要特性和功能。 +* 具体产品:实现了抽象产品角色所定义的接口,由具体工厂来创建,同具体工厂之间一一对应 + +代码实现: + +* 抽象工厂: + + ```java + public interface CoffeeFactory { + Coffee createCoffee(); + } + ``` + +* 具体工厂: + + ```java + public class LatteCoffeeFactory implements CoffeeFactory { + public Coffee createCoffee() { + return new LatteCoffee(); + } + } + + public class AmericanCoffeeFactory implements CoffeeFactory { + public Coffee createCoffee() { + return new AmericanCoffee(); + } + } + ``` + +优点: + +- 用户只需要知道具体工厂的名称就可得到所要的产品,无须知道产品的具体创建过程 +- 增加新的产品只需要添加具体产品类和对应的具体工厂类,无须对原工厂进行任何修改,满足开闭原则 + +缺点: + +* 每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度 + + + +*** + + + +#### 抽象工厂 + +抽象工厂:是一种为访问类提供一个创建一组相关或相互依赖对象的接口,且访问类无须指定所要产品的具体类就能得到同族的不同等级的产品的模式结构 + +抽象工厂是工厂方法的升级版,工厂方法模式只生产一个等级的产品,而抽象工厂模式可生产多个等级的产品 + +工厂方法只能加咖啡,不能拓展其他业务,所以使用抽象工厂 + +* 抽象工厂: + + ```java + public interface DessertFactory { + Coffee createCoffee(); + Dessert createDessert(); + } + ``` + +* 具体工厂: + + ```java + //美式甜点工厂 + public class AmericanDessertFactory implements DessertFactory { + public Coffee createCoffee() { + return new AmericanCoffee(); + } + public Dessert createDessert() { + return new MatchaMousse(); + } + } + //意大利风味甜点工厂 + public class ItalyDessertFactory implements DessertFactory { + + public Coffee createCoffee() { + return new LatteCoffee(); + } + + public Dessert createDessert() { + return new Tiramisu(); + } + } + ``` + +优点:当一个产品族中的多个对象被设计成一起工作时,能保证客户端始终只使用同一个产品族中的对象 + +缺点:当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改 + + + +*** + + + +#### 模式拓展 + +通过工厂模式+配置文件的方式解除工厂对象和产品对象的耦合,在工厂类中加载配置文件中的全类名,并创建对象进行存储,客户端如果需要对象,直接进行获取即可 + +* 定义配置文件 bean.properties: + + ```properties + american=pattern.factory.config_factory.AmericanCoffee + latte=pattern.factory.config_factory.LatteCoffee + ``` + +* 改进工厂类: + + ```java + public class CoffeeFactory { + private static Map map = new HashMap(); + static { + Properties p = new Properties(); + InputStream is = CoffeeFactory.class.getClassLoader().getResourceAsStream("bean.properties"); + try { + p.load(is); + //遍历Properties集合对象 + Set keys = p.keySet(); + for (Object key : keys) { + //根据键获取值(全类名) + String className = p.getProperty((String) key); + //获取字节码对象 + Class clazz = Class.forName(className); + Coffee obj = (Coffee) clazz.newInstance(); + map.put((String)key,obj); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static Coffee createCoffee(String name) { + return map.get(name); + } + } + ``` + + 静态成员变量用来存储创建的对象(键存储的是名称,值存储的是对应的对象),而读取配置文件以及创建对象写在静态代码块中,目的就是只需要执行一次 + + + +*** + + + +#### 应用场景 + +##### 使用场景 + +使用场景: + +* 当需要创建的对象是一系列相互关联或相互依赖的产品族时,如电器工厂中的电视机、洗衣机、空调等 +* 系统中有多个产品族,但每次只使用其中的某一族产品,如有人只喜欢穿某一个品牌的衣服和鞋 +* 系统中提供了产品的类库,且所有产品的接口相同,客户端不依赖产品实例的创建细节和内部结构 + + + +##### 源码应用 + +iterator + +```java +public class Demo { + public static void main(String[] args) { + List list = new ArrayList<>(); + list.add("令狐冲"); + list.add("风清扬"); + list.add("任我行"); + + //获取迭代器对象 + Iterator it = list.iterator(); + //使用迭代器遍历 + while(it.hasNext()) { + String ele = it.next(); + System.out.println(ele); + } + } +} +``` + +使用迭代器遍历集合,获取集合中的元素,而单列集合获取迭代器的方法就使用到了工厂方法模式 + + + +Collection 接口是抽象工厂类,ArrayList 是具体的工厂类,Iterator 接口是抽象商品类,ArrayList 类中的 Iter 内部类是具体的商品类,在具体的工厂类中 iterator() 方法创建具体的商品类的对象 + + + +*** + + + +### 原型模式 + +#### 基本介绍 + +用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象 + +原型模式包含如下角色: + +* 抽象原型类:规定了具体原型对象必须实现的的 clone() 方法。 +* 具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象。 +* 访问类:使用具体原型类中的 clone() 方法来复制新的对象 + +接口类图如下: + + + + + +*** + + + +#### 代码实现 + +原型模式的克隆分为浅克隆和深克隆,具体介绍参考 Object 类介绍部分的笔记 + +Java中 的 Object 类中提供了 `clone()` 方法来实现浅克隆,实现 Cloneable 接口的类才可以被克隆 + +* 具体原型类: + + ```java + public class Citation implements Cloneable { + private String name; + public void setName(String name) { + this.name = name; + } + + public String getName() { + return (this.name); + } + + public void show() { + System.out.println(name + "同学:第一学期被评为三好学生。特发此状!"); + } + + @Override + public Citation clone() throws CloneNotSupportedException { + return (Citation) super.clone(); + } + } + ``` + +* 测试类: + + ```java + public class CitationTest { + public static void main(String[] args) throws CloneNotSupportedException { + Citation c1 = new Citation(); + c1.setName("张三"); + //复制奖状 + Citation c2 = c1.clone(); + c2.setName("李四"); + + c1.show();// 张三 + c2.show();// 李四 + } + } + ``` + + + +**** + + + +#### 模式拓展 + +深克隆案例: + +* 原代码: + + ```java + public class Citation implements Cloneable { + private Student stu; + // get + set + + public void show() { + System.out.println(stu.getName() + "同学:在第一学期被评为三好学生。特发此状!"); + } + + @Override + public Citation clone() throws CloneNotSupportedException { + return (Citation) super.clone(); + } + } + //学生类 + public class Student { + private String name; + } + ``` + +* 测试代码 + + ```java + public static void main(String[] args) throws CloneNotSupportedException { + Citation c1 = new Citation(); + Student stu1 = new Student(); + stu1.setName("张三"); + c1.setStu(stu); + + Citation c2 = citation.clone(); + Student stu2 = c2.getStu(); + stu2.setName("李四"); + + citation.show(); //李四... + citation1.show(); //李四... + } + ``` + + stu1 对象和 stu2 对象是同一个对象,将 stu2 对象中 name 属性改为李四,两个Citation对象中都是李四,这就是浅克隆的效果 + +* 序列化实现深克隆,或者重写克隆方法: + + 序列化: + + ```java + public class CitationTest1 { + public static void main(String[] args) throws Exception { + Citation c1 = new Citation(); + Student stu = new Student("张三"); + c1.setStu(stu); + + //创建对象输出流对象 + ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\b.txt")); + //将c1对象写出到文件中 + oos.writeObject(c1); + oos.close(); + + //创建对象输入流对象 + ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C:\\b.txt")); + //读取对象 + Citation c2 = (Citation) ois.readObject(); + //获取c2奖状所属学生对象 + Student stu1 = c2.getStu(); + stu1.setName("李四"); + + //判断stu对象和stu1对象是否是同一个对象 + System.out.println("stu和stu1是同一个对象?" + (stu == stu1));//false + c1.show();//张三 + c2.show();//李四 + } + } + ``` + + 重写: + + ```java + @Override + public Citation clone() throws CloneNotSupportedException { + Citation clone = (Citation) super.clone(); + Student o = (Student) stu.clone(); + clone.setStu(o); + return clone; + } + ``` + + + +*** + + + +### 建造者 + +#### 基本介绍 + +将一个复杂对象的构建与表示分离,使得同样的构建过程可以创建不同的表示 + +* 分离了部件的构造(由Builder来负责)和装配(由Director负责),从而可以构造出复杂的对象,这个模式适用于某个对象的构建过程复杂的情况 +* 由于实现了构建和装配的解耦。不同的构建器,相同的装配,也可以做出不同的对象;相同的构建器,不同的装配顺序也可以做出不同的对象,实现了更好的复用。 +* 建造者模式可以将部件和其组装过程分开,一步一步创建一个复杂的对象。用户只需要指定复杂对象的类型就可以得到该对象,而无须知道其内部的具体构造细节 + +建造者(Builder)模式包含如下角色: + +* 抽象建造者类 (Builder):这个接口定义要实现复杂对象的哪些部分的创建,并不涉及具体的部件对象的创建 + +* 具体建造者类 (ConcreteBuilder):实现 Builder 接口,完成复杂产品的各个部件的具体创建方法,在构造过程完成后,提供产品的实例。 + +* 产品类 (Product):要创建的复杂对象 + +* 指挥者类 (Director):调用具体建造者来创建复杂对象的各个部分,在指导者中不涉及具体产品的信息,只负责保证对象各部分完整创建或按某种顺序创建 + + + +模式优点: + +- 建造者模式的封装性很好,使用建造者模式可以有效的封装变化,将主要的业务逻辑封装在指挥者类中对整体而言可以取得比较好的稳定性 +- 在建造者模式中,客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象 +- 可以更加精细地控制产品的创建过程 ,将复杂产品的创建步骤分解在不同的方法中,更方便使用程序来控制创建过程 +- 建造者模式很容易进行扩展,如果有新的需求,通过实现一个新的建造者类就可以完成,基本上不用修改之前已经测试通过的代码,因此也就不会对原有功能引入风险,符合开闭原则 + +模式缺点:造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制 + +应用场景: + +* 创建的对象较复杂,由多个部件构成,各部件面临着复杂的变化,但构件间的建造顺序是稳定的 +* 创建复杂对象的算法独立于该对象的组成部分以及装配方式,即产品的构建过程和最终的表示是独立的 + + + +**** + + + +#### 代码实现 + +生产自行车是一个复杂的过程,它包含了车架,车座等组件的生产。而车架又有多种材质的,车座有多种材质,对于自行车的生产就可以使用建造者模式 + +* 自行车类: + + ```java + public class Bike { + private String frame; + private String seat; + // ... + } + ``` + +* 抽象 builder 类: + + ```java + public abstract class Builder { + protected Bike bike = new Bike(); + + public abstract void buildFrame(); + public abstract void buildSeat(); + public abstract Bike createBike(); + } + ``` + +* 具体 builder 类: + + ```java + //摩拜单车Builder类 + public class MobikeBuilder extends Builder { + @Override + public void buildFrame() { + bike.setFrame("铝合金车架"); + } + @Override + public void buildSeat() { + bike.setSeat("真皮车座"); + } + @Override + public Bike createBike() { + return bike; + } + } + //ofo单车Builder类 + public class OfoBuilder extends Builder { + @Override + public void buildFrame() { + bike.setFrame("碳纤维车架"); + } + @Override + public void buildSeat() { + bike.setSeat("橡胶车座"); + } + @Override + public Bike createBike() { + return bike; + } + } + ``` + +* 指挥者类: + + ```java + public class Director { + private Builder builder; + + public Director(Builder builder) { + this.builder = builder; + } + + public Bike construct() { + builder.buildFrame(); + builder.buildSeat(); + return builder.createBike(); + } + } + ``` + +* 测试类: + + ```java + public static void main(String[] args) { + Director director = new Director(new MobileBuilder()); + // 指挥者指挥装配自行车 + Bike bike = director.construct(); + System.out.println(bike.getFrame() + bike.getSeat()); + } + ``` + + + +**** + + + +#### 模式拓展 + +当一个类构造器需要传入很多参数时,如果创建这个类的实例,代码可读性会非常差,而且很容易引入错误,此时就可以利用建造者模式进行重构 + +* 重构前代码: + + ```java + public class Phone { + private String cpu; + private String screen; + private String memory; + private String mainboard; + } + public static void main(String[] args) { + //构建Phone对象 + Phone phone = new Phone("intel","三星屏幕","金士顿","华硕"); + System.out.println(phone); + } + ``` + +* 重构后的代码: + + ````java + public class Phone { + private String cpu; + private String screen; + private String memory; + private String mainboard; + + private Phone(Builder builder) { + cpu = builder.cpu; + screen = builder.screen; + memory = builder.memory; + mainboard = builder.mainboard; + } + + public static final class Builder { + private String cpu; + private String screen; + private String memory; + private String mainboard; + + public Builder() {} + //返回值为this 所以支持链式编程 + public Builder cpu(String val) { + cpu = val; + return this; + } + public Builder screen(String val) { + screen = val; + return this; + } + public Builder memory(String val) { + memory = val; + return this; + } + public Builder mainboard(String val) { + mainboard = val; + return this; + } + public Phone build() { + return new Phone(this); + } + } + } + + public static void main(String[] args) { + Phone phone = new Phone.Builder() + .cpu("intel") + .mainboard("华硕") + .memory("金士顿") + .screen("三星") + .build(); + System.out.println(phone); + } + ```` + + + + + +*** + + + +#### 模式对比 + +工厂方法模式对比建造者模式 + +* 工厂方法模式注重的是整体对象的创建方式 +* 建造者模式注重的是部件构建的过程,意在通过一步一步地精确构造创建出一个复杂的对象 + +抽象工厂模式对比建造者模式 + +* 抽象工厂模式实现对产品家族的创建,一个产品家族是这样的一系列产品,不需要关心构建过程,只关心什么产品由什么工厂生产即可 + +* 建造者模式则是要求按照指定的蓝图建造产品,主要目的是通过组装零配件而产生一个新产品 + + 如果将抽象工厂模式看成汽车配件生产工厂,生产一个产品族的产品,那么建造者模式就是一个汽车组装工厂,通过对部件的组装可以返回一辆完整的汽车 + + + + + +**** + + + + + +## 结构型 + +### 代理模式 + +#### 基本介绍 + +结构型模式描述如何将类或对象按某种布局组成更大的结构,分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合方式来组合对象。由于组合关系或聚合关系比继承关系耦合度低,满足合成复用原则,所以对象结构型模式比类结构型模式具有更大的灵活性 + +结构型模式分为 7 种:代理模式、适配器模式、装饰者模式、桥接模式、外观模式、组合模式、享元模式 + +代理模式:由于某些原因需要给某对象提供一个代理以控制对该对象的访问,访问对象不适合或者不能直接引用为目标对象,代理对象作为访问对象和目标对象之间的中介 + +Java中的代理按照代理类生成时机不同又分为静态代理和动态代理,静态代理代理类在编译期就生成,而动态代理代理类则是在Java运行时动态生成,动态代理又有JDK代理和CGLib代理两种 + +代理(Proxy)模式分为三种角色: + +* 抽象主题(Subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法 +* 真实主题(Real Subject)类: 实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象 +* 代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,可以访问、控制或扩展真实主题的功能 + + + +*** + + + +#### 静态代理 + +买票案例,火车站是目标对象,代售点是代理对象 + +* 卖票接口: + + ```java + public interface SellTickets { void sell();} + ``` + +* 火车站,具有卖票功能,需要实现SellTickets接口 + + ```java + public class TrainStation implements SellTickets { + + public void sell() { + System.out.println("火车站卖票"); + } + } + ``` + +* 代售点: + + ```java + public class ProxyPoint implements SellTickets { + private TrainStation station = new TrainStation(); + + public void sell() { + System.out.println("代理点收取一些服务费用"); + station.sell(); + } + } + ``` + +* 测试类: + + ```java + public class Client { + public static void main(String[] args) { + ProxyPoint pp = new ProxyPoint(); + pp.sell(); + } + } + ``` + + 测试类直接访问的是 ProxyPoint 类对象,也就是 ProxyPoint 作为访问对象和目标对象的中介 + + + +**** + + + +#### JDK + +##### 使用方式 + +Java 中提供了一个动态代理类 Proxy,Proxy 并不是代理对象的类,而是提供了一个创建代理对象的静态方法 newProxyInstance() 来获取代理对象 + +`static Object newProxyInstance(ClassLoader loader,Class[] interfaces, InvocationHandler h) ` + +* 参数一:类加载器,负责加载代理类 +* 参数二:被代理业务对象的**全部实现的接口**,代理对象与真实对象实现相同接口,知道为哪些方法做代理 +* 参数三:代理真正的执行方法,也就是代理的处理逻辑 + +代码实现: + +* 代理工厂:创建代理对象 + + ```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) throws Throwable { + System.out.println("代理点收取服务费用(JDK动态代理方式)"); + //执行真实对象 + Object result = method.invoke(station, args); + return result; + } + }); + return sellTickets; + } + } + ``` + +* 测试类: + + ```java + public class Client { + public static void main(String[] args) { + //获取代理对象 + ProxyFactory factory = new ProxyFactory(); + SellTickets proxyObject = factory.getProxyObject(); + proxyObject.sell(); + } + } + ``` + + + +*** + + + +##### 原理解析 + +JDK动态代理方式的优缺点: + +- 优点:可以为任意的接口实现类对象做代理,也可以为被代理对象的所有接口的所有方法做代理,动态代理可以在不改变方法源码的情况下,实现对方法功能的增强,提高了软件的可扩展性,Java 反射机制可以生成任意类型的动态代理类 +- 缺点:**只能针对接口或者接口的实现类对象做代理对象**,普通类是不能做代理对象的 +- 原因:**生成的代理类继承了Proxy**,java 是单继承的,所以JDK动态代理只能代理接口 + +ProxyFactory 不是代理模式中的代理类,而代理类是程序在运行过程中动态的在内存中生成的类,可以通过 Arthas 工具查看代理类结构: + +* 代理类($Proxy0)实现了 SellTickets 接口,真实类和代理类实现同样的接口 +* 代理类($Proxy0)将提供了的匿名内部类对象传递给了父类 + +```java +//程序运行过程中动态生成的代理类 +public final class $Proxy0 extends Proxy implements SellTickets { + private static Method m3; + + public $Proxy0(InvocationHandler invocationHandler) { + super(invocationHandler);//InvocationHandler对象传递给父类 + } + + static { + m3 = Class.forName("proxy.dynamic.jdk.SellTickets").getMethod("sell", new Class[0]); + } + + 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() 方法 + + + +*** + + + +#### CGLIB + +##### 使用方式 + +CGLIB 是一个功能强大,高性能的代码生成包,为没有实现接口的类提供代理,为 JDK 动态代理提供了补充 + +* CGLIB 是第三方提供的包,所以需要引入 jar 包的坐标: + + ```xml + + cglib + cglib + 2.2.2 + + ``` + +* 代理工厂类: + + ```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; + } + } + ``` + + + +*** + + + +##### 原理分析 + +CGLIB 的优缺点 + +* 优点: + * CGLIB 动态代理**不限定**是否具有接口,可以对任意操作进行增强 + * CGLIB 动态代理无需要原始被代理对象,动态创建出新的代理对象 + * JDKProxy仅对接口方法做增强,cglib对所有方法做增强,包括Object类中的方法 (toString、hashCode) +* 缺点:CGLIB 不能对声明为 final 的类或者方法进行代理,因为 CGLIB 原理是动态生成被代理类的子类,继承被代理类 + + + + + +**** + + + +#### 方式对比 + +三种方式对比: + +* JDK 代理和 CGLIB 代理 + + 使用 CGLIB 实现动态代理,CGLIB 底层采用 ASM 字节码生成框架,使用字节码技术生成代理类,在 JDK1.6之前比使用 Java 反射效率要高,到 JDK1.8 的时候,JDK 代理效率高于 CGLIB 代理。所以如果有接口使用 JDK 动态代理,如果没有接口使用 CGLIB 代理 + +* 动态代理和静态代理 + + 动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理(InvocationHandler.invoke),在接口方法数量比较多的时候,可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转 + + 如果接口增加一个方法,静态代理模式除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。而动态代理不会出现该问题 + +代理模式的优缺点: + +* 优点: + * 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用 + * 代理对象可以扩展目标对象的功能 + * 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度 + +* 缺点:增加了系统的复杂度 + +代理模式的使用场景: + +* 远程(Remote)代理 + + 本地服务通过网络请求远程服务,需要实现网络通信,处理其中可能的异常。为了良好的代码设计和可维护性,将网络通信部分隐藏起来,只暴露给本地服务一个接口,通过该接口即可访问远程服务提供的功能 + +* 防火墙(Firewall)代理 + + 当你将浏览器配置成使用代理功能时,防火墙就将你的浏览器的请求转给互联网,当互联网返回响应时,代理服务器再把它转给你的浏览器 + +* 保护(Protect or Access)代理 + + 控制对一个对象的访问,如果需要,可以给不同的用户提供不同级别的使用权限 + + + + + +*** + + + +### 适配器 + +#### 基本介绍 + +适配器:将一个类的接口转换成另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作,比如 Type-C 转接头 + +适配器模式分为类适配器模式和对象适配器模式,前者类之间的耦合度比后者高,且要求了解现有组件库中的相关组件的内部结构,所以应用相对较少 + +还有一个适配器模式是接口适配器模式,当不希望实现一个接口中所有的方法时,可以创建一个抽象类Adapter 实现所有方法,而此时我们只需要继承该抽象类实现自己想实现的功能即可 + +适配器模式(Adapter)包含以下主要角色: + +* 目标(Target)接口:当前系统业务所期待的接口,可以是抽象类或接口 +* 适配者(Adaptee)类:被访问和适配的现存组件库中的组件接口 +* 适配器(Adapter)类:是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让开发人员按目标接口的格式访问适配者 + + + +**** + + + +#### 类适配器 + +实现方式:定义一个适配器类来实现当前系统的业务接口,同时又继承现有组件库中已经存在的组件 + +例如:现有一台电脑只能读取 SD 卡,而要读取 TF 卡中的内容的话就需要使用到适配器模式 + +* SD卡: + + ```java + //接口 + public interface SDCard { + //读取SD卡方法 + String readSD(); + //写入SD卡功能 + void writeSD(String msg); + } + //实现类 + public class SDCardImpl implements SDCard { + public String readSD() { + String msg = "sd card read a msg :hello word SD"; + return msg; + } + public void writeSD(String msg) { + System.out.println("sd card write msg : " + msg); + } + } + ``` + +* 电脑类: + + ```java + public class Computer { + public String readSD(SDCard sdCard) { + if(sdCard == null) { + throw new NullPointerException("sd card null"); + } + return sdCard.readSD(); + } + } + ``` + +* TF卡: + + ```java + //接口 + public interface TFCard { + //读取TF卡方法 + String readTF(); + //写入TF卡功能 + void writeTF(String msg); + } + //实现类 + public class TFCardImpl implements TFCard { + public String readTF() { + String msg ="tf card read msg : hello word tf card"; + return msg; + } + public void writeTF(String msg) { + System.out.println("tf card write a msg : " + msg); + } + } + ``` + +* 定义适配器类(SD兼容TF): + + ```java + public class SDAdapterTF extends TFCardImpl implements SDCard { + public String readSD() { + System.out.println("adapter read tf card "); + return readTF(); + } + + public void writeSD(String msg) { + System.out.println("adapter write tf card"); + writeTF(msg); + } + } + ``` + +* 测试类,可以读取 TF 卡中的数据了: + + ```java + public static void main(String[] args) { + Computer computer = new Computer(); + SDAdapterTF adapter = new SDAdapterTF(); + System.out.println(computer.readSD(adapter)); + } + ``` + +类适配器模式违背了合成复用原则,类适配器是客户类有一个接口规范的情况下可用,反之不可用 + + + +*** + + + +#### 对象适配 + +对象适配器模式可釆用将现有组件库中已经实现的组件引入适配器类中,该类同时实现当前系统的业务接口 + +使用对象适配器模式将读卡器的案例进行改写: + +* 适配器类: + + ```java + public class SDAdapterTF implements SDCard { + private TFCard tfCard; + public SDAdapterTF(TFCard tfCard) { + this.tfCard = tfCard; + } + + public String readSD() { + System.out.println("adapter read tf card "); + return tfCard.readTF(); + } + + public void writeSD(String msg) { + System.out.println("adapter write tf card"); + tfCard.writeTF(msg); + } + } + ``` + + + +*** + + + +#### 应用场景 + +##### 使用场景 + +* 开发的系统存在满足新系统功能需求的类,但其接口同新系统的接口不一致 +* 使用第三方提供的组件,但组件接口定义和自己要求的接口定义不同 + + + +##### 源码应用 + +Reader(字符流)、InputStream(字节流)的适配使用的是InputStreamReader + +```java +public int read() throws IOException { + return sd.read();// sd StreamDecoder +} +``` + +StreamDecoder 用来编码解码,编码:字符转为字节;解码:字节转字符 + + + + + +**** + + + +### 装饰者 + +#### 基本介绍 + +装饰者模式:指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式 + +使用继承的方式存在的问题:扩展性不好、产生过多的子类 + +装饰(Decorator)模式中的角色: + +* 抽象构件(Component)角色:定义一个抽象接口以规范准备接收附加责任的对象 +* 具体构件(Concrete Component)角色:实现抽象构件,通过装饰角色为其添加一些职责 +* 抽象装饰(Decorator)角色:继承或实现抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能 +* 具体装饰(ConcreteDecorator)角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任 + + + + +*** + + + +#### 代码实现 + +对快餐进行装饰,增加鸡蛋或者培根 + +* 快餐类: + + ```java + public abstract class FastFood { + private float price; + private String desc; + // set + get + public abstract float cost(); //获取价格 + } + + //炒饭 + public class FriedRice extends FastFood { + public FriedRice() { + super(10, "炒饭"); + } + public float cost() { + return getPrice(); + } + } + ``` + +* 配料类: + + ```java + public abstract class Garnish extends FastFood { + private FastFood fastFood; + //get + set + + public Garnish(FastFood fastFood, float price, String desc) { + super(price,desc); + this.fastFood = fastFood; + } + } + //鸡蛋配料 + public class Egg extends Garnish { + public Egg(FastFood fastFood) { + super(fastFood, 1, "鸡蛋"); + } + public float cost() { + return getPrice() + getFastFood().getPrice(); + } + @Override + public String getDesc() { + return super.getDesc() + getFastFood().getDesc(); + } + } + //培根配料 + public class Bacon extends Garnish { + public Bacon(FastFood fastFood) { + super(fastFood, 2, "培根"); + } + @Override + public float cost() { + return getPrice() + getFastFood().getPrice(); + } + @Override + public String getDesc() { + return super.getDesc() + getFastFood().getDesc(); + } + } + ``` + +* 测试类: + + ```java + public class Client { + public static void main(String[] args) { + //点一份炒饭 + FastFood food = new FriedRice(); + //花费的价格 + System.out.println(food.getDesc() + " " + food.cost() + "元"); + + System.out.println("========"); + //点一份加鸡蛋的炒饭 + FastFood food1 = new FriedRice(); + food1 = new Egg(food1); + //花费的价格 + System.out.println(food1.getDesc() + " " + food1.cost() + "元"); + } + } + ``` + + + +**** + + + +#### 应用场景 + +##### 使用场景 + +* 当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时 + + 不能采用继承的情况主要有两类: + + * 第一类是系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目增长很多 + * 第二类是因为类定义不能继承(如final类) + +* 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责 + + 当对象的功能要求可以动态地添加,也可以再动态地撤销时 + + + +##### 源码应用 + +IO流中的包装类使用到了装饰者模式,BufferedInputStream,BufferedOutputStream,BufferedReader,BufferedWriter + +举例: + +```java +public class Demo { + public static void main(String[] args) throws Exception{ + //创建FileWriter对象 + FileWriter fw = new FileWriter("C:\\Users\\Think\\Desktop\\a.txt"); + //创建BufferedWriter对象 + BufferedWriter bw = new BufferedWriter(fw); + //写数据 + bw.write("hello Buffered"); + bw.close(); + } +} +``` + + + +​ BufferedWriter 使用装饰者模式对 Writer 子实现类进行了增强,添加了缓冲区,提高了读写数据的效率 + + + +*** + + + +#### 模式对比 + +静态代理和装饰者模式的区别: + +* 相同点: + * 都要实现与目标类相同的业务接口 + * 在两个类中都要声明目标对象 + * 都可以在不修改目标类的前提下增强目标方法 +* 不同点: + * 目的不同:装饰者是为了增强目标对象,静态代理是为了保护和隐藏目标对象 + * 获取目标对象构建的地方不同:装饰者是由外界传递进来,可以通过构造方法传递;静态代理是在代理类内部创建,以此来隐藏目标对象 + + + +*** + + + +### 桥接模式 + +#### 基本介绍 + +桥接模式:将抽象与实现分离,使它们可以独立变化,用组合关系代替继承关系实现,从而降低了抽象和实现这两个可变维度的耦合度 + +桥接(Bridge)模式包含以下主要角色: + +* 抽象化(Abstraction)角色 :定义抽象类,并包含一个对实现化对象的引用 +* 扩展抽象化(Refined Abstraction)角色 :是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法 +* 实现化(Implementor)角色 :定义实现化角色的接口,供扩展抽象化角色调用 +* 具体实现化(Concrete Implementor)角色 :给出实现化角色接口的具体实现 + +应用场景: + +* 当一个类存在两个独立变化的维度,且这两个维度都需要进行扩展时 +* 当一个系统不希望使用继承或因为多层次继承导致系统类的数量增加时 +* 当一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性时,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系 + +优点:桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。如果现在还有一种视频文件类型 wmv,只需再定义一个类实现 VideoFile 接口即可,其他类不需要发生变化 + + + +*** + + + +#### 代码实现 + +开发一个跨平台视频播放器,可以在不同操作系统平台(如Windows、Mac、Linux等)上播放多种格式的视频文件,常见的视频格式包括RMVB、AVI、WMV等,该播放器包含了两个维度,适合使用桥接模式 + +* 视频文件类: + + ```java + //视频文件 + public interface VideoFile { + void decode(String fileName); + } + + //avi文件 + public class AVIFile implements VideoFile { + public void decode(String fileName) { + System.out.println("avi视频文件:" + fileName); + } + } + + //rmvb文件 + public class REVBBFile implements VideoFile { + public void decode(String fileName) { + System.out.println("rmvb文件:" + fileName); + } + } + ``` + +* 操作系统类: + + ```java + //操作系统版本 + public abstract class OperatingSystemVersion { + protected VideoFile videoFile; + + public OperatingSystemVersion(VideoFile videoFile) { + this.videoFile = videoFile; + } + + public abstract void play(String fileName); + } + + //Windows版本 + public class Windows extends OperatingSystem { + public Windows(VideoFile videoFile) { + super(videoFile); + } + public void play(String fileName) { + videoFile.decode(fileName); + } + } + + //mac版本 + public class Mac extends OperatingSystemVersion { + public Mac(VideoFile videoFile) { + super(videoFile); + } + public void play(String fileName) { + videoFile.decode(fileName); + } + } + ``` + +* 测试类: + + ```java + public class Client { + public static void main(String[] args) { + OperatingSystem os = new Windows(new AVIFile()); + os.play("The Godfather"); + } + } + ``` + + + + + +*** + + + +### 外观模式 + +#### 基本介绍 + +外观模式:又名门面模式,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体的细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性,是“迪米特法则”的典型应用 + +外观(Facade)模式包含以下主要角色: + +* 外观(Facade)角色:为多个子系统对外提供一个共同的接口 +* 子系统(Sub System)角色:实现系统的部分功能,用户可以通过外观角色访问它 + +模式优点: + +* 降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类 +* 对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易 + +模式缺点:不符合开闭原则,修改很麻烦 + + + +*** + + + +#### 代码实现 + +智能家电控制,一键关闭或者开启所有家电 + +* 子系统角色: + + ```java + //灯类 + public class Light { + public void on() { + System.out.println("打开了灯...."); + } + public void off() { + System.out.println("关闭了灯...."); + } + } + //电视类 + public class TV { + public void on() { + System.out.println("打开了电视...."); + } + + public void off() { + System.out.println("关闭了电视...."); + } + } + //空调类 + public class AirCondition { + public void on() { + System.out.println("打开了空调...."); + } + public void off() { + System.out.println("关闭了空调...."); + } + } + ``` + +* 外观角色: + + ```java + //智能音箱 + public class SmartAppliancesFacade { + private Light light; + private TV tv; + private AirCondition airCondition; + + public SmartAppliancesFacade() { + light = new Light(); + tv = new TV(); + airCondition = new AirCondition(); + } + + public void say(String message) { + if(message.contains("打开")) { + on(); + } else if(message.contains("关闭")) { + off(); + } else { + System.out.println("我还听不懂你说的!!!"); + } + } + + //起床后一键开电器 + private void on() { + System.out.println("起床了"); + light.on(); + tv.on(); + airCondition.on(); + } + + //睡觉一键关电器 + private void off() { + System.out.println("睡觉了"); + light.off(); + tv.off(); + airCondition.off(); + } + } + ``` + +* 测试类: + + ```java + public static void main(String[] args) { + //创建外观对象 + SmartAppliancesFacade facade = new SmartAppliancesFacade(); + //客户端直接与外观对象进行交互 + facade.say("打开家电"); + facade.say("关闭家电"); + } + ``` + + + +*** + + + +#### 应用场景 + +##### 使用场景 + +* 对分层结构系统构建时,使用外观模式定义子系统中每层的入口点可以简化子系统之间的依赖关系 +* 当一个复杂系统的子系统很多时,外观模式可以为系统设计一个简单的接口供外界访问 +* 当客户端与多个子系统之间存在很大的联系时,引入外观模式可将它们分离,从而提高子系统的独立性和可移植性 + + + +##### 源码应用 + +RequestFacade 类使用了外观模式 + + + +定义 RequestFacade 类,分别实现 ServletRequest ,同时定义私有成员变量 Request,并且方法的实现调用 Request 的实现。然后将 RequestFacade 上转为 ServletRequest 传给 servlet 的 service 方法,这样即使在 servlet 中被下转为 RequestFacade ,也不能访问私有成员变量对象中的方法。既用了 Request 又能防止其中方法被不合理的访问 + + + +*** + + + +### 组合模式 + +#### 基本介绍 + +组合模式:部分整体模式,用于把一组相似的对象当作一个单一的对象,组合模式依据树形结构来组合对象,用来表示部分以及整体层次,创建了对象组的树形结构,类比 Linux 树形文件图 + +组合模式主要包含三种角色: + +* 抽象根节点(Component):定义系统各层次对象的共有方法和属性,可以预先定义一些默认行为和属性 +* 树枝节点(Composite):定义树枝节点的行为,存储子节点,组合树枝节点和叶子节点形成一个树形结构 +* 叶子节点(Leaf):叶子节点对象,其下再无分支,是系统层次遍历的最小单位 + +应用场景:组合模式应树形结构而生,所以组合模式的使用场景就是出现树形结构的地方,比如文件目录显示,多级目录呈现等树形结构数据的操作 + +在使用组合模式时,根据抽象构件类的定义形式,可将组合模式分为透明组合模式和安全组合模式两种形式: + +* 透明组合模式 + + 透明组合模式中,抽象根节点角色中声明了所有用于管理成员对象的方法,比如在示例中 `MenuComponent` 声明了 `add`、`remove` 、`getChild` 方法,这样做的好处是确保所有的构件类都有相同的接口,透明组合模式也是组合模式的标准形式 + + 透明组合模式的缺点是不够安全,因为叶子对象和容器对象在本质上是有区别的,叶子对象不可能有下一个层次的对象,即不可能包含成员对象,因此为其提供 add()、remove() 等方法是没有意义的,这在编译阶段不会出错,但在运行阶段如果调用这些方法可能会出错(如果没有提供相应的错误处理代码) + +* 安全组合模式 + + 在安全组合模式中,在抽象构件角色中没有声明任何用于管理成员对象的方法,而是在树枝节点 `Menu` 类中声明并实现这些方法。安全组合模式的缺点是不够透明,因为叶子构件和容器构件具有不同的方法,且容器构件中那些用于管理成员对象的方法没有在抽象构件类中定义,因此客户端不能完全针对抽象编程,必须有区别地对待叶子构件和容器构件 + +组合模式的优点: + +* 组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,让客户端忽略了层次的差异,方便对整个层次结构进行控制 +* 客户端可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码 +* 在组合模式中增加新的树枝节点和叶子节点都很方便,无须对现有类库进行任何修改,符合“开闭原则” +* 组合模式为树形结构的面向对象实现提供了一种灵活的解决方案,通过叶子节点和树枝节点的递归组合,可以形成复杂的树形结构,但对树形结构的控制却非常简单 + + + +*** + + + +#### 代码实现 + +不管是菜单还是菜单项,都应该继承自统一的接口,将这个统一的接口称为菜单组件 + +* 菜单组件: + + MenuComponent 定义为抽象类,有一些共有的属性和行为要在该类中实现,其他类就可以只覆盖自己需要的方法。这里给出的默认实现是抛出异常,可以根据自己的需要改写默认实现 + + ```java + public abstract class MenuComponent { + protected String name; + protected int level; //层级 几级菜单 + + //添加菜单 + public void add(MenuComponent menuComponent){ + throw new UnsupportedOperationException(); + } + + //移除菜单 + public void remove(MenuComponent menuComponent){ + throw new UnsupportedOperationException(); + } + + //获取指定的子菜单 + public MenuComponent getChild(int i){ + throw new UnsupportedOperationException(); + } + + //获取菜单名称 + public String getName(){ + return name; + } + + public void print(){ + throw new UnsupportedOperationException(); + } + } + ``` + +* 菜单: + + Menu 类具有添加菜单,移除菜单和获取子菜单的功能 + + ```java + public class Menu extends MenuComponent { + private List menuComponentList; + + public Menu(String name,int level){ + this.level = level; + this.name = name; + menuComponentList = new ArrayList(); + } + + @Override + public void add(MenuComponent menuComponent) { + menuComponentList.add(menuComponent); + } + + @Override + public void remove(MenuComponent menuComponent) { + menuComponentList.remove(menuComponent); + } + + @Override + public MenuComponent getChild(int i) { + return menuComponentList.get(i); + } + + @Override + public void print() { + for (int i = 1; i < level; i++) { + System.out.print("--"); + } + System.out.println(name); + for (MenuComponent menuComponent : menuComponentList) { + menuComponent.print(); + } + } + } + ``` + +* 菜单项: + + MenuItem 是菜单项,不能再有子菜单,所以添加菜单,移除菜单和获取子菜单的功能并不能实现 + + ```java + public class MenuItem extends MenuComponent { + public MenuItem(String name, int level) { + this.name = name; + this.level = level; + } + + @Override + public void print() { + for (int i = 1; i < level; i++) { + System.out.print("--"); + } + System.out.println(name); + } + } + ``` + + + +*** + + + +### 享元模式 + +#### 基本介绍 + +享元模式:运用共享技术来有效地支持大量细粒度对象的复用,共享已经存在的对象来大幅度减少大量相似对象的开销,从而提高系统资源的利用率 + +享元(Flyweight )模式中存在以下两种状态: + +* 内部状态,不会随着环境的改变而改变的可共享部分 +* 外部状态,随着环境改变而改变的不可以共享的部分,享元模式的实现要领就是区分应用中的这两种状态,并将外部状态外部化 + +享元模式的主要有以下角色: + +* 抽象享元角色(Flyweight):通常是一个接口或抽象类,在抽象享元类中声明具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),也可以通过这些方法来设置外部数据(外部状态) +* 具体享元(Concrete Flyweight)角色 :实现了抽象享元类,称为享元对象,在具体享元类中为内部状态提供了存储空间。通常结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象 +* 非享元(Unsharable Flyweight)角色 :并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类,当需要一个非共享具体享元类的对象时可以直接通过实例化创建 +* 享元工厂(Flyweight Factory)角色 :负责创建和管理享元角色。当用户请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给用户;如果不存在则创建一个新的享元对象 + +模式优点: + +- 极大减少内存中相似或相同对象数量,节约系统资源,提供系统性能 +- 享元模式中的外部状态相对独立,且不影响内部状态 + +模式缺点:为了使对象可以共享,需要将享元对象的部分状态外部化,分离内部状态和外部状态,使程序逻辑复杂 + + + +**** + + + +#### 代码实现 + +俄罗斯方块,类图: + + + +* 抽象享元角色: + + ```java + public abstract class AbstractBox { + public abstract String getShape(); + + public void display(String color) { + System.out.println("方块形状:" + this.getShape() + " 颜色:" + color); + } + } + ``` + +* 具体享元角色: + + ```java + public class IBox extends AbstractBox { + @Override + public String getShape() { + return "I"; + } + } + public class LBox extends AbstractBox { + @Override + public String getShape() { + return "L"; + } + } + public class OBox extends AbstractBox { + @Override + public String getShape() { + return "O"; + } + } + ``` + +* 享元工厂角色: + + ```java + public class BoxFactory { + private static HashMap map; + + private BoxFactory() { + map = new HashMap(); + AbstractBox iBox = new IBox(); + AbstractBox lBox = new LBox(); + AbstractBox oBox = new OBox(); + map.put("I", iBox); + map.put("L", lBox); + map.put("O", oBox); + } + + public static final BoxFactory getInstance() { + return SingletonHolder.INSTANCE; + } + private static class SingletonHolder { + private static final BoxFactory INSTANCE = new BoxFactory(); + } + public AbstractBox getBox(String key) { + return map.get(key); + } + } + ``` + + + +**** + + + +#### 应用场景 + +##### 使用场景 + +* 一个系统有大量相同或者相似的对象,造成内存的大量耗费 +* 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中 +* 在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,应当在需要多次重复使用享元对象时才值得使用享元模式 + + + +##### 源码应用 + +Integer类使用了享元模式: + +```java +public class Demo { + public static void main(String[] args) { + Integer i1 = 127; + Integer i2 = 127; + System.out.println("i1和i2对象是否是同一个对象?" + (i1 == i2));//true + + Integer i3 = 128; + Integer i4 = 128; + System.out.println("i3和i4对象是否是同一个对象?" + (i3 == i4));//false + } +} +``` + +反编译后发现直接给 Integer 类型的变量赋值基本数据类型数据的操作底层使用的是 `valueOf()` + +`Integer` 默认先创建并缓存 `-128 ~ 127` 之间数的 `Integer` 对象,当调用 `valueOf` 时如果参数在 `-128 ~ 127` 之间则计算下标并从缓存中返回,否则创建一个新的 `Integer` 对象 + + + + + +**** + + + + + +## 行为型 + +### 模式分类 + +行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,涉及算法与对象间职责的分配 + +行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为,由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性 + +除了模板方法模式和解释器模式是类行为型模式,其他的全部属于对象行为型模式 + +行为型模式分为: + +* 模板方法模式 +* 策略模式 +* 命令模式 +* 职责链模式 +* 状态模式 +* 观察者模式 +* 中介者模式 +* 迭代器模式 +* 访问者模式 +* 备忘录模式 +* 解释器模式 + + + +*** + + + +### 模板方法 + +#### 基本介绍 + +模板方法模式:定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤 + +模板方法(Template Method)模式包含以下主要角色: + +* 抽象类(Abstract Class):负责给出一个算法的轮廓和骨架,由一个模板方法和若干个基本方法构成 + + * 模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法 + + * 基本方法:是实现算法各个步骤的方法,是模板方法的组成部分,基本方法又可以分为三种: + + * 抽象方法(Abstract Method):一个抽象方法由抽象类声明,由其具体子类实现 + + * 具体方法(Concrete Method):一个具体方法由一个抽象类或具体类声明并实现,其子类可以进行覆盖也可以直接继承 + + * 钩子方法(Hook Method):在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法 + + 一般钩子方法是用于判断的逻辑方法,这类方法名一般为isXxx,返回值类型为boolean类型 + +* 具体子类(Concrete Class):实现抽象类中所定义的抽象方法和钩子方法,是一个顶级逻辑的组成步骤 + +模式优点: + +* 提高代码复用性,将相同部分的代码放在抽象的父类中,而将不同的代码放入不同的子类中 +* 实现反向控制,通过一个父类调用其子类的操作,通过对子类的具体实现扩展不同的行为,实现了反向控制,并符合开闭原则 + + +模式缺点: + +* 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象 +* 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,提高了代码阅读的难度 + + + +**** + + + +#### 代码实现 + +炒菜步骤是固定的,现通过模板方法模式来用代码模拟 + +* 抽象类: + + 注意:为防止恶意操作,一般模板方法都加上 final 关键词 + + ```java + public abstract class AbstractClass { + public final void cookProcess() { + //第一步:倒油 + this.pourOil(); + //第二步:热油 + this.heatOil(); + //第三步:倒蔬菜 + this.pourVegetable(); + //第四步:倒调味料 + this.pourSauce(); + //第五步:翻炒 + this.fry(); + } + //第一步:倒油 + public void pourOil() { + System.out.println("倒油"); + } + //第二步:热油是一样的,所以直接实现 + public void heatOil() { + System.out.println("热油"); + } + //第三步:倒蔬菜是不一样的 + public abstract void pourVegetable(); + //第四步:倒调味料是不一样 + public abstract void pourSauce(); + + //第五步:翻炒是一样的,所以直接实现 + public void fry(){ + System.out.println("炒啊炒啊炒到熟啊"); + } + } + ``` + +* 具体子类: + + ```java + public class ConcreteClass_BaoCai extends AbstractClass { + @Override + public void pourVegetable() { + System.out.println("下锅的蔬菜是包菜"); + } + + @Override + public void pourSauce() { + System.out.println("下锅的酱料是辣椒"); + } + } + ``` + +* 测试类: + + ```java + public static void main(String[] args) { + //炒手撕包菜 + ConcreteClass_BaoCai baoCai = new ConcreteClass_BaoCai(); + baoCai.cookProcess(); + } + ``` + + + +*** + + + +#### 应用场景 + +##### 使用场景 + +* 算法的整体步骤很固定,但其中个别部分易变时,这时使用模板方法模式将易变的部分抽象出来,供子类实现 +* 需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制 + + + + + +##### 源码应用 + +InputStream类就使用了模板方法模式,在InputStream类中定义了多个 read() 方法: + +```java +public abstract class InputStream implements Closeable { + //抽象方法,要求子类必须重写 + public abstract int read() throws IOException; + public int read(byte b[]) throws IOException {...} + public int read(byte b[], int off, int len) throws IOException { + //... + int c = read(); //调用了无参的read方法,该方法是每次读取一个字节数据 + //... + } +} +``` + +在 InputStream 父类中已经定义好了读取一个字节数组数据的方法是每次读取一个字节,并将其存储到数组的第一个索引位置,读取 len 个字节数据,具体如何读取一个字节数据由子类实现 + + + +*** + + + +### 策略模式 + +#### 基本介绍 + +策略模式:定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的用户。策略模式属于对象行为模式,通过对算法进行封装,把使用算法和算法的实现分割开来,并委派给不同的对象对这些算法进行管理 + +策略模式的主要角色如下: + +* 抽象策略(Strategy)类:一个抽象角色,通常由一个接口或抽象类实现,给出所有的具体策略类所需的接口 +* 具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现或行为 +* 环境(Context)类:持有一个策略类的引用,最终给客户端调用 + +模式优点: + +* 策略类之间可以自由切换,由于策略类都实现同一个接口,所以使它们之间可以自由切换 +* 易于扩展,增加一个新的策略只需要添加一个具体的策略类,基本不需要改变原有的代码,符合开闭原则 +* 避免使用多重条件选择语句(if else),充分体现面向对象设计思想 + +缺点: + +* 客户端必须知道所有的策略类,并自行决定使用哪一个策略类 +* 策略模式将造成产生很多策略类,可以通过使用享元模式在一定程度上减少对象的数量 + + + +*** + + + +#### 代码实现 + +促销活动,针对不同的节日推出不同的促销活动,由促销员将促销活动展示给客户 + +* 抽象策略类: + + ```java + public interface Strategy { + void show(); + } + ``` + +* 具体策略类: + + ```java + + //为春节准备的促销活动A + public class StrategyA implements Strategy { + public void show() { + System.out.println("买一送一"); + } + } + + //为中秋准备的促销活动B + public class StrategyB implements Strategy { + + public void show() { + System.out.println("满200元减50元"); + } + } + ``` + +* 环境类:用于连接上下文,即把促销活动推销给客户,这里可以理解为促销员 + + ```java + public class SalesMan { + //持有抽象策略角色的引用 + private Strategy strategy; + + public SalesMan(Strategy strategy) { + this.strategy = strategy; + } + + //向客户展示促销活动 + public void salesManShow(){ + strategy.show(); + } + } + ``` + + + +*** + + + +#### 应用场景 + +##### 使用场景 + +* 一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略类中 +* 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条件分支移入各自的策略类中以代替这些条件语句 +* 系统中各算法彼此完全独立,且要求对客户隐藏具体算法的实现细节时 +* 系统要求使用算法的客户不应该知道其操作的数据时,可使用策略模式来隐藏与算法相关的数据结构 +* 多个类只区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为 + + + +*** + + + +##### 源码应用 + +Comparator 中的策略模式,在 Arrays 类中有一个 sort() 方法,如下: + +```java +public static void sort(T[] a, Comparator c) { + if (c == null) { + sort(a); + } else { + if (LegacyMergeSort.userRequested) + legacyMergeSort(a, c); + else + TimSort.sort(a, 0, a.length, c, null, 0, 0); + } +} +``` + +Arrays 就是一个环境角色类,这个 sort 方法可以传一个新策略让Arrays根据这个策略来进行排序,Comparator充当的是抽象策略角色,而具体的子实现类充当的是具体策略角色 + +通过 TimSort 类的 sort() 方法,最终会跑到 `countRunAndMakeAscending()` 这个方法中,只用了 compare 方法,所以在调用 Arrays.sort 方法只传具体 compare 重写方法的类对象就可以,这也是 Comparator 接口中必须要子类实现的一个方法 + +```java +private static int countRunAndMakeAscending(T[] a, int lo, int hi,Comparator c) { + assert lo < hi; + int runHi = lo + 1; + if (runHi == hi) + return 1; + + // Find end of run, and reverse range if descending + if (c.compare(a[runHi++], a[lo]) < 0) { // Descending + while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) < 0) + runHi++; + reverseRange(a, lo, runHi); + } else { // Ascending + while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) >= 0) + runHi++; + } + + return runHi - lo; +} +``` + + + + + +*** + + + +### 命令模式 + +#### 基本介绍 + +命令模式:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开,这样两者之间通过命令对象进行沟通,方便将命令对象进行存储、传递、调用、增加与管理 + +命令模式包含以下主要角色: + +* 抽象命令类(Command)角色:定义命令的接口,声明执行的方法。 +* 具体命令(Concrete Command)角色:具体的命令,实现命令接口,通常会持有接收者,并调用接收者的功能来完成命令要执行的操作 +* 实现者/接收者(Receiver)角色:接收者,真正执行命令的对象,任何类都可能成为一个接收者,只要能够实现命令要求实现的相应功能 +* 调用者/请求者(Invoker)角色:要求命令对象执行请求,通常会持有命令对象,可以持有很多的命令对象。这个是客户端真正触发命令并要求命令执行相应操作的地方,也就是说相当于使用命令对象的入口 + +模式优点: + +* 降低系统的耦合度,命令模式能将调用操作的对象与实现该操作的对象解耦 +* 增加或删除命令非常方便,采用命令模式增加与删除命令不会影响其他类,满足“开闭原则”,对扩展比较灵活 +* 可以实现宏命令,命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令 +* 方便实现 Undo 和 Redo 操作,命令模式可以与备忘录模式结合,实现命令的撤销与恢复 + +模式缺点: + +* 使用命令模式可能会导致某些系统有过多的具体命令类。 +* 系统结构更加复杂 + + + +**** + + + +#### 代码实现 + +饭店案例 + +* 抽象命令类: + + ```java + public interface Command { + void execute();//只需要定义一个统一的执行方法 + } + ``` + +* 具体命令角色: + + ```java + public class Order { + // 餐桌号码 + private int diningTable; + //set + get + + // 用来存储餐名并记录份数 + private Map foodDic = new HashMap(); + + public Map getFoodDic() { + return foodDic; + } + + public void setFoodDic(String name, int num) { + foodDic.put(name,num); + } + } + ``` + + ```java + public class OrderCommand implements Command { + //持有接受者对象 + private SeniorChef receiver; + private Order order; + + public OrderCommand(SeniorChef receiver, Order order){ + this.receiver = receiver; + this.order = order; + } + + public void execute() { + System.out.println(order.getDiningTable() + "桌的订单:"); + Set keys = order.getFoodDic().keySet(); + for (String key : keys) { + receiver.makeFood(order.getFoodDic().get(key),key); + } + try { + Thread.sleep(100);//停顿一下 模拟做饭的过程 + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println(order.getDiningTable() + "桌的饭弄好了"); + } + } + ``` + +* 接收者: + + ```java + // 厨师类 + public class SeniorChef { + public void makeFood(int num, String foodName) { + System.out.println(num + "份" + foodName); + } + } + ``` + +* 请求者: + + ```java + //服务员类 + public class Waitor { + private ArrayList commands;//可以持有很多的命令对象 + public Waitor() { + commands = new ArrayList(); + } + + public void setCommand(Command cmd){ + //存储d + commands.add(cmd); + } + // 发出命令 喊 订单来了,厨师开始执行 + public void orderUp() { + System.out.println("服务员:大厨,新订单来了......."); + for (int i = 0; i < commands.size(); i++) { + Command cmd = commands.get(i); + if (cmd != null) { + cmd.execute(); + } + } + } + } + ``` + +* 测试类: + + ```java + public static void main(String[] args) { + //创建1个order + Order order = new Order(); + order.setDiningTable(1); + order.getFoodDic().put("西红柿鸡蛋面",1); + order.getFoodDic().put("小杯可乐",2); + + //创建接收者 + SeniorChef receiver=new SeniorChef(); + //将订单和接收者封装成命令对象 + OrderCommand cmd = new OrderCommand(receiver, order1); + //创建调用者 waitor + Waitor invoker = new Waitor(); + invoker.setCommand(cmd); + + //将订单带到柜台 并向厨师喊 订单来了 + invoker.orderUp(); + } + ``` + + + +**** + + + +#### 应用场景 + +##### 使用场景 + +* 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互 +* 系统需要在不同的时间指定请求、将请求排队和执行请求 +* 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作 + + + +##### 源码应用 + +Runable 是一个典型命令模式,Runnable 担当命令的角色,Thread 充当的是调用者,start 方法就是其执行方法 + +```java +//命令接口(抽象命令角色) +public interface Runnable { + public abstract void run(); +} +//调用者 +public class Thread implements Runnable { + private Runnable target; + public synchronized void start() { + //... + start0(); + //.... + } + private native void start0(); +} +``` + +调用一个 native 方法 start0(),调用系统方法,开启一个线程。而接收者是对程序员开放的,可以自定义接收者 + + + +**** + + + +### 责任链 + +#### 基本介绍 + +责任链模式:为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链,当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止 + +职责链模式主要包含以下角色: + +* 抽象处理者(Handler)角色:定义一个处理请求的接口,包含抽象处理方法和一个后继连接 +* 具体处理者(Concrete Handler)角色:实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给后继者 +* 客户类(Client)角色:创建处理链,并向链头的具体处理者对象提交请求,不关心处理细节和请求的传递过程 + +模式优点: + +* 降低了对象之间的耦合度,降低了请求发送者和接收者的耦合度 + +* 增强了系统的可扩展性,可以根据需要增加新的请求处理类,满足开闭原则 + +* 增强了给对象指派职责的灵活性,当工作流程发生变化,可以动态地改变链内的成员或者修改它们的次序,也可动态地新增或者删除责任 + +* 责任链简化了对象之间的连接,一个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多的 if 或者 if···else 语句。 + +* 责任分担,每个类只需要处理自己该处理的工作,不能处理的传递给下一个对象完成,明确各类的责任范围,符合类的单一职责原则 + + +模式缺点: + +* 不能保证每个请求一定被处理,由于一个请求没有明确的接收者,所以不能保证一定会被处理,该请求可能一直传到链的末端都得不到处理 +* 对比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响 +* 职责链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于职责链的错误设置而导致系统出错,如可能会造成循环调用 + + + +**** + + + +#### 代码实现 + +开发一个请假流程控制系统,请假一天以下的假只需要小组长同意即可,请假1天到3天的假还需要部门经理同意,请求3天到7天还需要总经理同意才行 + +* 请假类: + + ```java + public class LeaveRequest { + private String name;//姓名 + private int num;//请假天数 + private String content;//请假内容 + // constructor + set + get + } + ``` + +* 抽象处理者: + + ```java + //处理者抽象类 + public abstract class Handler { + protected final static int NUM_ONE = 1; + protected final static int NUM_THREE = 3; + protected final static int NUM_SEVEN = 7; + + //该领导处理的请假天数区间 + private int numStart; + private int numEnd; + + //领导上面还有领导 + private Handler nextHandler; + + //设置请假天数范围 上不封顶 + public Handler(int numStart) { + this.numStart = numStart; + } + + //设置请假天数范围 + public Handler(int numStart, int numEnd) { + this.numStart = numStart; + this.numEnd = numEnd; + } + + //设置上级领导 + public void setNextHandler(Handler nextHandler){ + this.nextHandler = nextHandler; + } + + //提交请假条 + public final void submit(LeaveRequest leave){ + if(0 == this.numStart){ + return; + } + + //如果请假天数达到该领导者的处理要求 + if(leave.getNum() >= this.numStart){ + this.handleLeave(leave); + + //如果还有上级 并且请假天数超过了当前领导的处理范围 + if(null != this.nextHandler && leave.getNum() > numEnd){ + this.nextHandler.submit(leave);//继续提交 + } else { + System.out.println("流程结束"); + } + } + } + //各级领导处理请假条方法 + protected abstract void handleLeave(LeaveRequest leave); + } + ``` + +* 具体处理者: + + ```java + public class GroupLeader extends Handler { + public GroupLeader() { + //小组长处理1-3天的请假 + super(Handler.NUM_ONE, Handler.NUM_THREE); + } + @Override + protected void handleLeave(LeaveRequest leave) { + System.out.println(leave.getName() + "请假" + leave.getNum() + "天," + leave.getContent() + "。"); + System.out.println("小组长审批:同意。"); + } + } + //部门经理 + public class Manager extends Handler { + public Manager() { + //部门经理处理3-7天的请假 + super(Handler.NUM_THREE, Handler.NUM_SEVEN); + } + @Override + protected void handleLeave(LeaveRequest leave) { + System.out.println(leave.getName() + "请假" + leave.getNum() + "天," + leave.getContent() + "。"); + System.out.println("部门经理审批:同意。"); + } + } + //总经理 + public class GeneralManager extends Handler { + public GeneralManager() { + //部门经理处理7天以上的请假 + super(Handler.NUM_SEVEN); + } + @Override + protected void handleLeave(LeaveRequest leave) { + System.out.println(leave.getName() + "请假" + leave.getNum() + "天," + leave.getContent() + "。"); + System.out.println("总经理审批:同意。"); + } + } + ``` + +* 测试类: + + ```java + public static void main(String[] args) { + //请假条来一张 + LeaveRequest leave = new LeaveRequest("小花",5,"身体不适"); + + //各位领导 + GroupLeader groupLeader = new GroupLeader(); + Manager manager = new Manager(); + GeneralManager generalManager = new GeneralManager(); + + groupLeader.setNextHandler(manager);//小组长的领导是部门经理 + manager.setNextHandler(generalManager);//部门经理的领导是总经理 + //之所以在这里设置上级领导,是因为可以根据实际需求来更改设置,如果实战中上级领导人都是固定的,则可以移到领导实现类中。 + + //提交申请 + groupLeader.submit(leave); + } + ``` + + + +*** + + + +#### 应用场景 + +在 JavaWeb应用开发中,FilterChain是职责链(过滤器)模式的典型应用,以下是 Filter 的模拟实现: + +* 模拟 web 请求 Request 以及 web 响应 Response: + + ```java + public interface Request{ + } + public interface Response{ + } + ``` + +* 模拟 web 过滤器 Filter: + + ```java + public interface Filter { + public void doFilter(Request req, Response res, FilterChain c); + } + ``` + +* 模拟实现具体过滤器 : + + ```java + public class FirstFilter implements Filter { + @Override + public void doFilter(Request request, Response response, FilterChain chain) { + System.out.println("过滤器1 前置处理"); + // 先执行所有request再倒序执行所有response + chain.doFilter(request, response); + System.out.println("过滤器1 后置处理"); + } + } + + public class SecondFilter implements Filter { + @Override + public void doFilter(Request request, Response response, FilterChain chain) { + System.out.println("过滤器2 前置处理"); + // 先执行所有request再倒序执行所有response + chain.doFilter(request, response); + System.out.println("过滤器2 后置处理"); + } + } + ``` + +* 模拟实现过滤器链FilterChain + + ```java + public class FilterChain { + private List filters = new ArrayList(); + private int index = 0; + + // 链式调用 + public FilterChain addFilter(Filter filter) { + this.filters.add(filter); + return this; + } + + public void doFilter(Request request, Response response) { + if (index == filters.size()) { + return; + } + Filter filter = filters.get(index); + index++; + filter.doFilter(request, response, this); + } + } + ``` + +* 测试类 + + ```java + public class Client { + public static void main(String[] args) { + Request req = null; + Response res = null ; + + FilterChain filterChain = new FilterChain(); + filterChain.addFilter(new FirstFilter()).addFilter(new SecondFilter()); + filterChain.doFilter(req,res); + } + } + /* + 过滤器1 前置处理 + 过滤器2 前置处理 + 过滤器2 后置处理 + 过滤器1 后置处理 + */ + ``` + + + +**** + + + +### 状态模式 + +#### 基本介绍 + +在状态模式(State Pattern)中,类的行为是基于它的状态改变的 + +状态模式包含以下主要角色。 + +* 环境(Context)角色:也称为上下文,它定义了客户程序需要的接口,维护一个当前状态,并将与状态相关的操作委托给当前状态对象来处理 +* 抽象状态(State)角色:定义一个接口,用以封装环境对象中的特定状态所对应的行为 +* 具体状态(Concrete State)角色:实现抽象状态所对应的行为 + +模式优点: + +* 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为 +* 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块 + +模式缺点: + +* 状态模式的使用必然会增加系统类和对象的个数。 +* 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱 +* 状态模式对开闭原则的支持并不太好 + +使用场景: + +- 当一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为时,可以使用状态模式 +- 一个操作中含有庞大的分支结构,并且这些分支决定于对象的状态时 + + + +*** + + + +#### 代码实现 + +通过按钮来控制一个电梯的状态,一个电梯有开门状态,关门状态,停止状态,运行状态,每一种状态改变,都有可能要根据其他状态来更新处理 + +* 抽象状态角色: + + ```java + public abstract class LiftState { + //定义一个环境角色,也就是封装状态的变化引起的功能变化 + protected Context context; + public void setContext(Context context) { + this.context = context; + } + //电梯开门动作 + public abstract void open(); + //电梯关门动作 + public abstract void close(); + //电梯运行动作 + public abstract void run(); + //电梯停止动作 + public abstract void stop(); + } + ``` + +* 电梯门开启状态: + + ```java + public class OpenningState extends LiftState { + @Override + public void open() { + System.out.println("电梯门开启..."); + } + //开启了就可以关闭了 + @Override + public void close() { + //状态修改 + super.context.setLiftState(Context.closeingState); + //动作委托为CloseState来执行,也就是委托给了ClosingState子类执行这个动作 + super.context.getLiftState().close(); + } + //电梯门不能开着就跑,这里什么也不做 + @Override public void run() { + //do nothing + } + //开门状态已经是停止的了 + @Override + public void stop() { + //do nothing + } + } + //电梯门关闭状态 省略 + ``` + +* 电梯运行状态: + + ```java + public class RunningState extends LiftState { + //运行的时候不能开电梯门 + @Override + public void open() { + //do nothing + } + //电梯门关闭?这是肯定了 + @Override + public void close() { + //虽然可以关门,但这个动作不归我执行 + //do nothing + } + //这是在运行状态下要实现的方法 + @Override + public void run() { + System.out.println("电梯正在运行..."); + } + //这个事是合理的 + @Override + public void stop() { + super.context.setLiftState(Context.stoppingState); + super.context.stop(); + } + } + //电梯停止状态 省略 + ``` + +* 环境角色类: + + ```java + public class Context { + //定义出所有的电梯状态,开门状态,这时候电梯只能关闭 + public final static OpenningState openningState = new OpenningState(); + //关闭状态,这时候电梯可以运行、停止和开门 + public final static ClosingState closeingState = new ClosingState(); + //运行状态,这时候电梯只能停止 + public final static RunningState runningState = new RunningState(); + //停止状态,这时候电梯可以开门、运行 + public final static StoppingState stoppingState = new StoppingState(); + //定义一个当前电梯状态 + private LiftState liftState; + public LiftState getLiftState() { + return this.liftState; + } + public void setLiftState(LiftState liftState) { + //当前环境改变 + this.liftState = liftState; + //把当前的环境通知到各个实现类中 + this.liftState.setContext(this); + } + public void open() { + this.liftState.open(); + } + public void close() { + this.liftState.close(); + } + public void run() { + this.liftState.run(); + } + public void stop() { + this.liftState.stop(); + } + } + ``` + +* 测试类: + + ```java + public static void main(String[] args) { + Context context = new Context(); + context.setLiftState(new ClosingState()); + context.open(); + context.close(); + context.run(); + context.stop(); + } + ``` + + + +***** + + + +### 观察者 + +#### 基本介绍 + +观察者模式:又称为发布-订阅(Publish/Subscribe)模式,定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态变化时,会通知所有的观察者对象,使他们能够自动更新自己 + +在观察者模式中有如下角色: + +* Subject:抽象主题(抽象被观察者),抽象主题角色把所有观察者对象保存在一个集合里,每个主题都可以有任意数量的观察者,抽象主题提供一个接口,可以增加和删除观察者对象 +* ConcreteSubject:具体主题(具体被观察者),该角色将有关状态存入具体观察者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发送通知 +* Observer:抽象观察者,是观察者的抽象类,定义了一个更新接口,在得到主题更改通知时更新自己 +* ConcrereObserver:具体观察者,实现抽象观察者定义的更新接口,在得到主题更改通知时更新自身状态 + +模式优点: + +* 降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系 +* 被观察者发送通知,所有注册的观察者都会收到信息,可以实现**广播**机制 + +模式缺点: + +* 如果观察者非常多的话,那么所有的观察者收到被观察者发送的通知会比较耗时 +* 如果被观察者有循环依赖的话,那么被观察者发送通知会使观察者循环调用,会导致系统崩溃 + + + +**** + + + +#### 代码实现 + +微信公众号订阅,微信用户就是观察者,微信公众号是被观察者 + + + +* 抽象观察者类: + + ```java + public interface Observer { + void update(String message); + } + ``` + +* 具体观察者类,微信用户是观察者: + + ```java + public class WeixinUser implements Observer { + // 微信用户名 + private String name; + public WeixinUser(String name) { + this.name = name; + } + @Override + public void update(String message) { + System.out.println(name + "-" + message); + } + } + ``` + +* 抽象主题类: + + ```java + public interface Subject { + //增加订阅者 + public void attach(Observer observer); + //删除订阅者 + public void detach(Observer observer); + //通知订阅者更新消息 + public void notify(String message); + } + ``` + +* 微信公众号是具体主题(具体被观察者): + + ```java + public class SubscriptionSubject implements Subject { + //储存订阅公众号的微信用户 + private List weixinUserlist = new ArrayList(); + @Override + public void attach(Observer observer) { + weixinUserlist.add(observer); + } + @Override + public void detach(Observer observer) { + weixinUserlist.remove(observer); + } + @Override + public void notify(String message) { + for (Observer observer : weixinUserlist) { + observer.update(message); + } + } + } + ``` + +* 测试类: + + ```java + public static void main(String[] args) { + SubscriptionSubject mSubscriptionSubject = new SubscriptionSubject(); + //创建微信用户 + WeixinUser user1 = new WeixinUser("孙悟空"); + WeixinUser user2 = new WeixinUser("猪悟能"); + WeixinUser user3 = new WeixinUser("沙悟净"); + //订阅公众号 + mSubscriptionSubject.attach(user1); + mSubscriptionSubject.attach(user2); + mSubscriptionSubject.attach(user3); + //公众号更新发出消息给订阅的微信用户 + mSubscriptionSubject.notify("唐僧的专栏更新了"); + } + ``` + + + +**** + + + +#### 应用场景 + +##### 使用场景 + +* 对象间存在一对多关系,一个对象的状态发生改变会影响其他对象 +* 当一个抽象模型有两个方面,其中一个方面依赖于另一方面时 + + + +##### 源码应用 + +在 Java 中,通过 java.util.Observable 类和 java.util.Observer 接口定义了观察者模式,只要实现它们的子类就可以编写观察者模式实例 + +* Observable类是抽象目标类(被观察者),有一个 Vector 集合成员变量,保存所有要通知的观察者对象 + * `void addObserver(Observer o)`:用于将新的观察者对象添加到集合中 + + * `void notifyObservers(Object arg)`:调用集合中的所有观察者对象的 update 方法,通知它们数据发生改变,通常越晚加入集合的观察者越先得到通知(从后向前遍历) + + * `void setChange()`:用来设置一个 boolean 类型的内部标志,注明目标对象发生了变化,当它为true时,notifyObservers() 就会通知观察者 + +* Observer 接口是抽象观察者,监视目标对象的变化,当目标对象发生变化时,观察者得到通知,并调用 update 方法,进行相应的工作 + +实例:警察抓小偷使用观察者模式来实现,代码如下: + +* 小偷是一个被观察者,需要继承Observable类: + + ```java + public class Thief extends Observable { + private String name; + // construct + set + get + + public void steal() { + System.out.println("小偷:我偷东西了,有没有人来抓我!!!"); + super.setChanged(); //changed = true + super.notifyObservers(); + } + } + ``` + +* 警察是一个观察者,需要实现Observer接口: + + ```java + public class Policemen implements Observer { + private String name; + // construct + set + get + + @Override + public void update(Observable o, Object arg) { + System.out.println("警察:" + ((Thief) o).getName() + ",我盯你很久了"); + } + } + ``` + +* 测试类: + + ```java + public static void main(String[] args) { + //创建小偷对象 + Thief t = new Thief("12"); + //创建警察对象 + Policemen p = new Policemen("66"); + //让警察盯着小偷 + t.addObserver(p); + //小偷偷东西 + t.steal(); + } + ``` + + + +*** + + + +### 中介者 + +#### 基本介绍 + +中介者模式:又叫调停模式,定义一个中介角色来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互 + + +中介者模式包含以下主要角色: + +* 抽象中介者(Mediator)角色:中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法 +* 具体中介者(ConcreteMediator)角色:实现中介者接口,定义一个 List 来管理同事对象,协调各个同事角色之间的交互关系,因此依赖于同事角色 +* 抽象同事类(Colleague)角色:定义同事类的接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能 +* 具体同事类(Concrete Colleague)角色:是抽象同事类的实现者,当需要与其他同事对象交互时,由中介者对象负责后续的交互 + +模式优点: + +* 松散耦合:中介者模式通过把多个同事对象之间的交互封装到中介者对象里面,从而使得同事对象之间松散耦合,基本上可以做到互补依赖,同事对象就可以独立地变化和复用 + +* 集中控制交互:多个同事对象的交互,被封装在中介者对象里面集中管理,使得这些交互行为发生变化时,只需要修改中介者对象就可以,如果是已经做好的系统,那么就扩展中介者对象,而各个同事类不需要做修改 + +* 一对多关联转变为一对一的关联:没有使用中介者模式的时候,同事对象之间的关系通常是一对多的,引入中介者对象以后,中介者对象和同事对象的关系通常变成双向的一对一,这会让对象的关系更容易理解和实现 + +模式缺点:当同事类太多时,中介者的职责将很大,会变得复杂而庞大,以至于系统难以维护 + +应用场景: + +* 系统中对象之间存在复杂的引用关系,系统结构混乱且难以理解。 +* 当想创建一个运行于多个类之间的对象,又不想生成新的子类时 + + + +**** + + + +#### 代码实现 + +案例:通过房屋中介租房 + +* 抽象中介者: + + ```java + public abstract class Mediator { + //申明一个联络方法 + public abstract void constact(String message,Person person); + } + ``` + +* 抽象同事类: + + ```java + public abstract class Person { + protected String name; + protected Mediator mediator; + + public Person(String name, Mediator mediator){ + this.name = name; + this.mediator = mediator; + } + } + ``` + +* 具体同事类: + + ```java + // 房屋拥有者 + public class HouseOwner extends Person { + public HouseOwner(String name, Mediator mediator) { + super(name, mediator); + } + //与中介者联系 + public void constact(String message){ + mediator.constact(message, this); + } + //获取信息 + public void getMessage(String message){ + System.out.println("房主" + name +"获取到的信息:" + message); + } + } + //承租人 + public class Tenant extends Person { + public Tenant(String name, Mediator mediator) { + super(name, mediator); + } + //与中介者联系 + public void constact(String message){ + mediator.constact(message, this); + } + //获取信息 + public void getMessage(String message){ + System.out.println("租房者" + name +"获取到的信息:" + message); + } + } + ``` + +* 具体中介者: + + ```java + public class MediatorStructure extends Mediator { + //首先中介结构必须知道所有房主和租房者的信息 + private HouseOwner houseOwner; + private Tenant tenant; + //set + get + + public void constact(String message, Person person) { + if (person == houseOwner) { //如果是房主,则租房者获得信息 + tenant.getMessage(message); + } else { //反正则是房主获得信息 + houseOwner.getMessage(message); + } + } + } + ``` + +* 测试类: + + ```java + public static void main(String[] args) { + //一个房主、一个租房者、一个中介机构 + MediatorStructure mediator = new MediatorStructure(); + + //房主和租房者只需要知道中介机构即可 + HouseOwner houseOwner = new HouseOwner("张三", mediator); + Tenant tenant = new Tenant("李四", mediator); + + //中介结构要知道房主和租房者 + mediator.setHouseOwner(houseOwner); + mediator.setTenant(tenant); + + tenant.constact("需要租三室的房子"); + houseOwner.constact("我这有三室的房子,你需要租吗?"); + } + ``` + + + +**** + + + +### 迭代器 + +#### 基本介绍 + +迭代器模式:提供一个对象来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示 + +迭代器模式主要包含以下角色: + +* 抽象聚合(Aggregate)角色:定义存储、添加、删除聚合元素以及创建迭代器对象的接口 + +* 具体聚合(ConcreteAggregate)角色:实现抽象聚合类,返回一个具体迭代器的实例 +* 抽象迭代器(Iterator)角色:定义访问和遍历聚合元素的接口,通常包含 hasNext()、next() 等方法 +* 具体迭代器(Concretelterator)角色:实现抽象迭代器接口中所定义的方法,完成对聚合对象的遍历,记录遍历的当前位置 + +模式优点: + +* 支持以不同的方式遍历一个聚合对象,在迭代器模式中只要用一个不同的迭代器来替换原有迭代器即可改变遍历算法,也可以自定义迭代器的子类以支持新的遍历方式 +* 迭代器简化了聚合类,在原有的聚合对象中不需要再自行提供数据遍历等方法,这样可以简化聚合类的设计 +* 在迭代器模式中,由于引入了抽象层,增加新的聚合类和迭代器类都很方便,无须修改原有代码,满足开闭原则的要求 + +模式缺点:增加了类的个数,这在一定程度上增加了系统的复杂 + + + +**** + + + +#### 代码实现 + +定义一个可以存储学生对象的容器对象,将遍历该容器的功能交由迭代器实现 + +* 迭代器接口: + + ```java + public interface StudentIterator { + //判断是否还有元素 + boolean hasNext(); + //获取下一个元素 + Student next(); + } + ``` + +* 具体的迭代器类,重写所有的抽象方法: + + ```java + public class StudentIteratorImpl implements StudentIterator { + private List list; + private int position = 0;//用来记录遍历时的位置 + + public StudentIteratorImpl(List list) { + this.list = list; + } + + @Override + public boolean hasNext() { + return position < list.size(); + } + + @Override + public Student next() { + Student currentStudent = list.get(position); + position ++; + return currentStudent; + } + } + ``` + +* 抽象容器类: + + ```java + public interface StudentAggregate { + void addStudent(Student student); + void removeStudent(Student student); + StudentIterator getStudentIterator(); + } + ``` + +* 具体容器类: + + ```java + public class StudentAggregateImpl implements StudentAggregate { + private List list = new ArrayList(); // 学生列表 + + @Override + public void addStudent(Student student) { + this.list.add(student); + } + + @Override + public void removeStudent(Student student) { + this.list.remove(student); + } + + @Override + public StudentIterator getStudentIterator() { + return new StudentIteratorImpl(list); + } + } + ``` + + + +*** + + + +#### 应用场景 + +使用场景: + +* 需要为聚合对象提供多种遍历方式 +* 需要为遍历不同的聚合结构提供一个统一的接口 +* 访问一个聚合对象的内容而无须暴露其内部细节的表示 + +使用 JAVA 开发的时候,想使用迭代器模式,只要让自定义的容器类实现 `java.util.Iterable` 并实现其中的iterator() 方法使其返回一个 `java.util.Iterator` 的实现类就可以 + + + +**** + + + +### 访问者 + +#### 基本介绍 + +访问者模式:封装一些作用于某种数据结构中的各元素的操作,可以在不改变这个数据结构的前提下定义作用于这些元素的新操作 + +访问者模式包含以下主要角色: + +* 抽象访问者(Visitor)角色:定义了对每一个元素(Element)访问的行为,它的参数就是可以访问的元素,它的方法个数理论上来讲与元素类个数(Element 的实现类个数)是一样的,从这点不难看出,访问者模式要求元素类的个数不能改变 +* 具体访问者(ConcreteVisitor)角色:给出对每一个元素类访问时所产生的具体行为 +* 抽象元素(Element)角色:定义了一个接受访问者的方法(accept),每一个元素都可以被访问者访问 +* 具体元素(ConcreteElement)角色: 提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法 +* 对象结构(Object Structure)角色:定义当中所提到的对象结构,对象结构是一个抽象表述,可以理解为一个具有容器性质或者复合对象特性的类,它含有一组元素并且可以迭代这些元素,供访问者访问 + +模式优点: + +* 扩展性好,在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能 + +* 复用性好,通过访问者来定义整个对象结构通用的功能,从而提高复用程度 + +* 分离无关行为,通过访问者来分离无关的行为,把相关的行为封装在一起构成一个访问者,这样每一个访问者的功能都比较单一 + +模式缺点: + +* 对象结构变化很困难,在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了开闭原则 +* 违反了依赖倒置原则,访问者模式依赖了具体类,而没有依赖抽象类 + +应用场景: + +* 对象结构相对稳定,但其操作算法经常变化的程序 + +* 对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构 + + + +*** + + + +#### 代码实现 + +以给宠物喂食为例,当然宠物还分为狗,猫等,要给宠物喂食的话,主人可以喂,其他人也可以喂食 + +- 访问者角色:给宠物喂食的人 +- 具体访问者角色:主人、其他人 +- 抽象元素角色:动物抽象类 +- 具体元素角色:宠物狗、宠物猫 +- 结构对象角色:主人家 + +![](https://gitee.com/seazean/images/raw/master/Java/Design-访问者模式.png) + +* 抽象访问者接口: + + ```java + public interface Person { + void feed(Cat cat); + void feed(Dog dog); + } + ``` + +* 具体访问者角色,需要实现 Person 接口: + + ```java + public class Owner implements Person { + @Override + public void feed(Cat cat) { + System.out.println("主人喂食猫"); + } + @Override + public void feed(Dog dog) { + System.out.println("主人喂食狗"); + } + } + ``` + +* 抽象元素接口: + + ```java + public interface Animal { + void accept(Person person); + } + ``` + +* 具体元素角色: + + ```java + public class Dog implements Animal { + @Override + public void accept(Person person) { + person.feed(this); + System.out.println("汪汪汪!!!"); + } + } + + public class Cat implements Animal { + @Override + public void accept(Person person) { + person.feed(this); + System.out.println("喵喵喵!!!"); + } + } + ``` + +* 对象结构,此案例中是主人的家: + + ```java + public class Home { + private List nodeList = new ArrayList(); + + public void action(Person person) { + for (Animal node : nodeList) { + node.accept(person); + } + } + //添加操作 + public void add(Animal animal) { + nodeList.add(animal); + } + } + ``` + +* 测试类: + + ```java + public static void main(String[] args) { + Home home = new Home(); + home.add(new Dog()); + home.add(new Cat()); + + Owner owner = new Owner(); + home.action(owner); + } + ``` + + + +*** + + + +#### 分派机制 + +访问者模式用到了一种双分派的技术 + +变量被声明时的类型叫做变量的静态类型,也叫做明显类型;而变量所引用的对象的真实类型又叫做变量的实际类型。比如 `Map map = new HashMap()` ,map变量的静态类型是 `Map` ,实际类型是 `HashMap` 。根据对象的类型而对方法进行的选择,就是分派(Dispatch),分派(Dispatch)又分为两种,即静态分派和动态分派 + +* 静态分派(Static Dispatch):发生在编译时期,根据静态类型信息分派,方法重载就是静态分派 +* 动态分派(Dynamic Dispatch):发生在运行时期,动态地置换掉某个方法,通过方法重写支持动态分派 + +双分派技术是在选择一个方法时,不仅要根据消息接收者的运行时区别,还要根据参数的运行时区别。双分派实现动态绑定的本质,就是在重载方法委派的前加上继承体系中覆盖的环节,由于覆盖是动态的,所以重载是动态的 + + + +*** + + + +### 备忘录 + +#### 基本介绍 + +备忘录模式:又叫快照模式,在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态 + +备忘录模式提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤,当新的状态无效或者存在问题时,可以使用暂时存储起来的备忘录将状态复原。很多软件都提供了撤销(Undo)操作,使文档恢复到之前的状态;比如数据库事务管理中的回滚操作、玩游戏时的存档功能、棋类游戏中的悔棋功能等都属于这类 + +备忘录模式的主要角色如下: + +* 发起人(Originator)角色:记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,实现其他业务功能,它可以访问备忘录里的所有信息 +* 备忘录(Memento)角色:负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人 +* 管理者(Caretaker)角色:对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改 + +备忘录有两个等效的接口: + +* 窄接口:管理者对象看到的是备忘录的窄接口(narror Interface),只允许他把备忘录对象传给其他的对象 +* 宽接口:与管理者看到的窄接口相反,发起人对象可以看到一个宽接口(wide Interface),这个宽接口允许它读取所有的数据,以便根据这些数据恢复这个发起人对象的内部状态 + +模式优点: + +- 提供了一种可以恢复状态的机制,当用户需要时能够比较方便地将数据恢复到某个历史的状态 +- 实现了内部状态的封装,除了创建它的发起人之外,其他对象都不能够访问这些状态信息 +- 简化了发起人类,发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理,这符合单一职责原则 + +模式缺点:资源消耗大,如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源 + +使用场景: + +* 需要保存与恢复数据的场景,如玩游戏时的中间结果的存档功能 + +* 需要提供一个可回滚操作的场景,如按 Ctrl+Z 组合键 + + + +*** + + + +#### 代码实现 + +##### 白箱模式 + +备忘录角色对任何对象都提供一个接口,即宽接口,备忘录角色的内部所存储的状态就对所有对象公开 + + + +* 发起人类,游戏角色类: + + ```java + public class GameRole { + private int vit; //生命力 + private int atk; //攻击力 + private int def; //防御力 + // set + get + + //初始化状态 + public void initState() { + this.vit = 100; + this.atk = 100; + this.def = 100; + } + //战斗后的状态 + public void fight() { + this.vit = 0; + this.atk = 0; + this.def = 0; + } + + //保存角色状态 + public RoleStateMemento saveState() { + return new RoleStateMemento(vit, atk, def); + } + //恢复角色状态 + public void recoverState(RoleStateMemento roleStateMemento) { + this.vit = roleStateMemento.getVit(); + this.atk = roleStateMemento.getAtk(); + this.def = roleStateMemento.getDef(); + } + //展示角色状态 + public void stateDisplay() { + System.out.println("角色生命力:" + vit); + System.out.println("角色攻击力:" + atk); + System.out.println("角色防御力:" + def); + } + } + ``` + +* 备忘录类,游戏状态存储类: + + ```java + public class RoleStateMemento { + private int vit; + private int atk; + private int def; + // construct +set + get + } + ``` + +* 角色状态管理者类: + + ```java + public class RoleStateCaretaker { + private RoleStateMemento roleStateMemento; + // get + set + } + ``` + +* 测试类: + + ```java + public static void main(String[] args) { + //大战Boss前 + GameRole gameRole = new GameRole(); + gameRole.initState(); + + //保存进度 + RoleStateCaretaker roleStateCaretaker = new RoleStateCaretaker(); + roleStateCaretaker.setRoleStateMemento(gameRole.saveState()); + + //大战Boss时 + gameRole.fight(); + + //恢复之前状态 + gameRole.recoverState(roleStateCaretaker.getRoleStateMemento()); + gameRole.stateDisplay(); + } + ``` + +白箱备忘录模式是破坏封装性的,但是通过程序员自律,同样可以在一定程度上实现模式的大部分目的 + + + +*** + + + +##### 黑箱模式 + +备忘录角色对发起人对象提供一个宽接口,而为其他对象提供一个窄接口。在Java语言中,实现双重接口的办法就是将备忘录类设计成发起人类的内部成员类 + + + +* 窄接口 Memento,这是一个标识接口: + + ```java + public interface Memento {} + ``` + +* 发起人类 GameRole,并在内部定义备忘录内部类 RoleStateMemento(该内部类设置为私有的): + + ```java + public class GameRole { + //....... + private class RoleStateMemento implements Memento { + private int vit; + private int atk; + private int def; + // set + get + } + } + ``` + +* 负责人角色类 RoleStateCaretaker 得到的备忘录对象是以 Memento 为接口的,由于这个接口仅仅是一个标识接口,负责人角色不可能改变这个备忘录对象的内容 + + ```java + public class RoleStateCaretaker { + private Memento memento; + // set + get + } + ``` + + + +*** + + + +### 解释器 + +#### 基本介绍 + +解释器模式(Interpreter Pattern)提供了评估语言的语法或表达式的方式,使用该表示来解释语言中的句子 + +解释器模式包含以下主要角色。 + +* 抽象表达式(Abstract Expression)角色:定义解释器的接口,约定解释器的解释操作,主要包含解释方法 interpret() + +* 终结符表达式(Terminal Expression)角色:是抽象表达式的子类,用来实现文法中与终结符相关的操作,文法中的每一个终结符都有一个具体终结表达式与之相对应 +* 非终结符表达式(Nonterminal Expression)角色:抽象表达式的子类,用来实现文法中与非终结符相关的操作,文法中的每条规则都对应于一个非终结符表达式 +* 环境(Context)角色:通常包含各个解释器需要的数据或是公共的功能,一般用来传递被所有解释器共享的数据,后面的解释器可以从这里获取这些值 +* 客户端(Client):主要任务是将需要分析的句子或表达式转换成使用解释器对象描述的抽象语法树,然后调用解释器的解释方法,也可以通过环境角色间接访问解释器的解释方法 + +抽象语法树(AbstractSyntaxTree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示,以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构 + +用树形来表示符合文法规则的句子: + + + +模式优点: + +* 易于改变和扩展文法,由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法,每一条文法规则都可以表示为一个类,因此可以方便地实现一个简单的语言 + +* 实现文法较为容易,在抽象语法树中每一个表达式节点类的实现方式都是相似的,代码编写不会特别复杂 + +* 增加新的解释表达式较为方便,如果用户需要增加新的解释表达式只需要对应增加一个新的终结符表达式或非终结符表达式类,原有表达式类代码无须修改,符合开闭原则 + +模式缺点: + +- 对于复杂文法难以维护,在解释器模式中,每一条规则至少需要定义一个类,因此如果一个语言包含太多文法规则,类的个数将会急剧增加,导致系统难以管理和维护 +- 执行效率较低,在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度很慢,而且代码的调试过程也比较麻烦 + +使用场景: + +* 当语言的文法较为简单,且执行效率不是关键问题时 +* 当问题重复出现,且可以用一种简单的语言来进行表达时 +* 当一个语言需要解释执行,并且语言中的句子可以表示为一个抽象语法树的时候 + + + +*** + + + +#### 代码实现 + +设计实现加减法的软件 + +* 抽象角色类: + + ```java + public abstract class AbstractExpression { + public abstract int interpret(Context context); + } + ``` + +* 终结符表达式角色,变量表达式: + + ```java + public class Variable extends AbstractExpression { + private String name; + + public Variable(String name) { + this.name = name; + } + + @Override + public int interpret(Context ctx) { + return ctx.getValue(this); + } + + @Override + public String toString() { + return name; + } + } + ``` + +* 非终结符表达式角色: + + ```java + //加法表达式 + public class Plus extends AbstractExpression { + private AbstractExpression left; // + 左边的表达式 + private AbstractExpression right; // + 右边的表达式 + + public Plus(AbstractExpression left, AbstractExpression right) { + this.left = left; + this.right = right; + } + + @Override + public int interpret(Context context) { + return left.interpret(context) + right.interpret(context); + } + + @Override + public String toString() { + return "(" + left.toString() + " + " + right.toString() + ")"; + } + } + // 剑法仿照加法 + public class Minus extends AbstractExpression {...} + ``` + +* 环境类: + + ```java + public class Context { + private Map map = new HashMap(); + + public void assign(Variable var, Integer value) { + map.put(var, value); + } + + public int getValue(Variable var) { + Integer value = map.get(var); + return value; + } + } + ``` + +* 测试类: + + ```java + public static void main(String[] args) { + Context context = new Context(); + + Variable a = new Variable("a"); + Variable b = new Variable("b"); + Variable c = new Variable("c"); + Variable d = new Variable("d"); + Variable e = new Variable("e"); + + context.assign(a, 1); + context.assign(b, 2); + context.assign(c, 3); + context.assign(d, 4); + context.assign(e, 5); + + AbstractExpression expression = new Minus(new Plus(new Plus(new Plus(a, b), c), d), e); + + System.out.println(expression + "= " + expression.interpret(context)); + } + ``` + + + + + + -(更新中) diff --git a/SSM.md b/SSM.md index d2e4208..53b50a7 100644 --- a/SSM.md +++ b/SSM.md @@ -6922,7 +6922,7 @@ ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext. wrapIfNecessary一定创建代理对象吗? * 存在增强器会创建动态代理,不需要增强就不需要创建动态代理对象 - * 不创建就会把最原始的实例化的Bean放到二级缓存,因为addSingletonFactory参数中传入了实例化的Bean,在singletonFactory.getObject()中返回给singletonObject,放入二级缓存 + * 不创建就会把最原始的实例化的Bean放到二级缓存,因为 addSingletonFactory 参数中传入了实例化的Bean,在singletonFactory.getObject()中返回给singletonObject,放入二级缓存 什么时候将Bean的引用提前暴露给第三级缓存的ObjectFactory持有? From 79ceac8a0f730577dc4e4c709c96c3f8a97569f2 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 5 Jun 2021 16:17:28 +0800 Subject: [PATCH 039/242] Update README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b5f8169..3372f3a 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ * DB:MySQL、JDBC、Redis * Frame:Maven -* Issue:Interview -* Java:JavaSE、JVM、JUC、Design +* Issue:Interview Questions +* Java:JavaSE、JVM、JUC、Design Pattern * SSM:MyBatis、Spring、SpringMVC、SpringBoot * Tool:Git、Linux、Docker * Web:HTML、CSS、Servlet、JavaScript From ab7d20c9d30214f2e4c7c7fe976efe11851dec2b Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 5 Jun 2021 20:58:04 +0800 Subject: [PATCH 040/242] Update Java Notes --- DB.md | 149 ++++++++++++++++++++++++++++++++++---------------------- Java.md | 6 +-- 2 files changed, 93 insertions(+), 62 deletions(-) diff --git a/DB.md b/DB.md index 2d4389a..c52744b 100644 --- a/DB.md +++ b/DB.md @@ -393,21 +393,21 @@ mysqlshow -uroot -p1234 test book --count - SQL分类 - - DDL(Data Definition Language)数据定义语言 + - DDL(Data Definition Language)数据定义语言 - 用来定义数据库对象:数据库,表,列等。关键字:create、drop,、alter 等 - - DML(Data Manipulation Language)数据操作语言 + - DML(Data Manipulation Language)数据操作语言 - 用来对数据库中表的数据进行增删改。关键字:insert、delete、update 等 - - DQL(Data Query Language)数据查询语言 + - DQL(Data Query Language)数据查询语言 - 用来查询数据库中表的记录(数据)。关键字:select、where 等 - - DCL(Data Control Language)数据控制语言 + - DCL(Data Control Language)数据控制语言 - - 用来定义数据库的访问权限和安全级别,及创建用户。关键字:GRANT, REVOKE 等 + - 用来定义数据库的访问权限和安全级别,及创建用户。关键字:grant, revoke等 ![](https://gitee.com/seazean/images/raw/master/DB/SQL分类.png) @@ -2116,14 +2116,14 @@ CREATE TABLE us_pro( 原子性是指事务是一个不可分割的工作单位,事务的操作如果成功就必须要完全应用到数据库,失败则不能对数据库有任何影响。比如事务中一个 SQL 语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态 -InnoDB 存储引擎提供了两种事务日志:redo log (重做日志) 和 undo log (回滚日志) +InnoDB 存储引擎提供了两种事务日志:redo log(重做日志)和 undo log(回滚日志) * redo log 用于保证事务持久性 * undo log 用于保证事务原子性和隔离性 undo log 属于逻辑日志,根据每行操作进行记录,记录了 SQL 执行相关的信息,用来回滚行记录到某个版本 -当事务对数据库进行修改时,InnoDB 会生成对应的 undo log,如果事务执行失败或调用了 rollback 导致事务回滚,InnoDB 会根据 undo log 的内容做与之前相反的操作: +当事务对数据库进行修改时,InnoDB 会生成对应的 undo log,如果事务执行失败或调用了 rollback 导致事务回滚,InnoDB 会根据 undo log 的内容**做与之前相反的操作**: * 对于每个 insert,回滚时会执行 delete @@ -2131,12 +2131,12 @@ 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 +rollback segment 称为回滚段,每个回滚段中有 1024 个 undo log segment -* 在以前老版本,只支持1个rollback segment,只能记录1024个 undo log segment -* MySQL5.5 开始支持128个 rollback segment,支持128*1024个 undo 操作 +* 在以前老版本,只支持1个 rollback segment,只能记录 1024 个 undo log segment +* MySQL5.5 开始支持128个 rollback segment,支持 128*1024 个 undo 操作 @@ -2194,7 +2194,7 @@ redo log,记录数据页的物理修改,而不是某一行或某几行的修 InnoDB 作为存储引擎,数据是存放在磁盘中,每次读写数据都需要磁盘 IO,效率会很低。InnoDB 提供了缓存 Buffer Pool,Buffer Pool 中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲: * 从数据库读取数据时,会首先从缓存中读取,如果缓存中没有,则从磁盘读取后放入Buffer Pool -* 向数据库写入数据时,会首先写入缓存,缓存中修改的数据会定期刷新到磁盘(这一过程称为刷脏) +* 向数据库写入数据时,会首先写入缓存,缓存中修改的数据会定期刷新到磁盘,这一过程称为刷脏 Buffer Pool 的使用提高了读写数据的效率,但是也带了新的问题:如果 MySQL 宕机,此时 Buffer Pool 中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证,所以引入 redo log @@ -2244,7 +2244,7 @@ MySQL中还存在 binlog(二进制日志) 也可以记录写操作并用于数 -### 隔离界别 +### 隔离级别 事务的隔离级别:多个客户端操作时,各个客户端的事务之间应该是隔离的,**不同的事务之间不该互相影响**,而如果多个事务操作同一批数据时,则需要设置不同的隔离级别 , 否则就会产生问题 。 @@ -2299,7 +2299,7 @@ MVCC 全称 Multi-Version Concurrency Control,即多版本并发控制,用 MVCC 处理读写请求,可以做到在发生读写请求冲突时不用加锁,这个读是指的快照读,而不是当前读 * 快照读:实现基于 MVCC,因为是多版本并发,所以快照读读到的数据不一定是当前最新的数据,有可能是历史版本的数据 -* 当前读:读取数据库记录是当前最新的版本,会对读取的数据进行加锁,防止其他事务修改数据,是悲观锁的一种操作。读写操作加共享锁或者排他锁、串行化事务的隔离级别都是当前读 +* 当前读:读取数据库记录是当前最新的版本,会对读取的数据进行加锁,防止其他事务修改数据,是悲观锁的一种操作,读写操作加共享锁或者排他锁和串行化事务的隔离级别都是当前读 数据库并发场景: @@ -2312,7 +2312,7 @@ MVCC 处理读写请求,可以做到在发生读写请求冲突时不用加锁 MVCC 的优点: * 在并发读写数据库时,做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了并发读写的性能 -* 可以解决脏读,幻读,不可重复读等事务隔离问题(加锁也能解决),但不能解决更新丢失问题 +* 可以解决脏读,不可重复读等事务隔离问题(加锁也能解决),但不能解决更新丢失问题 提高读写和写写的并发性能: @@ -2337,13 +2337,13 @@ 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 产生一个聚簇索引 -* 补充:删除 flag 的隐藏字段,记录被更新或删除并不代表真的删除,而是删除flag变了 +* DELETED_BIT:删除标志的隐藏字段,记录被更新或删除并不代表真的删除,而是删除位变了 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MVCC版本链隐藏字段.png) @@ -2363,8 +2363,8 @@ undo log 是逻辑日志,保存修改行的数据的拷贝副本 undo log 的作用: -* 保证事务进行 rollback 时的原子性和一致性,当事务进行回滚的时候可以用 undo log 的数据进行恢复。 -* 用于MVCC快照读的数据,在MVCC多版本控制中,通过读取 undo log 的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据版本。 +* 保证事务进行 rollback 时的原子性和一致性,当事务进行回滚的时候可以用 undo log 的数据进行恢复 +* 用于 MVCC 快照读的数据,在 MVCC 多版本控制中,通过读取 undo log 的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据版本 undo log主要分为两种: @@ -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线程: +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,9 +2393,7 @@ purge线程: ##### 读视图 -Read View 是事务进行快照读操作时产生的读视图,该事务执行快照读的那一刻会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID - -Read View 用来做可见性判断,当某个事务执行快照读的时候,对该记录创建一个Read View 读视图,根据视图判断当前事务能够看到哪个版本的数据 +Read View 是事务进行快照读操作时产生的读视图,该事务执行快照读的那一刻会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID,用来做可见性判断,根据视图判断当前事务能够看到哪个版本的数据 工作流程:将版本链的头节点的事务ID(最新数据事务ID)DB_TRX_ID 取出来,与系统当前活跃事务的 ID 对比,如果 DB_TRX_ID 不符合可见性,那就通过 DB_ROLL_PTR 回滚指针去取出 undo log 中的下一个 DB_TRX_ID 再比较,直到找到满足特定条件的 DB_TRX_ID,该事务 ID 所在的旧记录就是当前事务能看见的最新的记录 @@ -2403,7 +2401,7 @@ Read View 几个属性: - m_ids:生成 Read View 时当前系统中活跃的事务 id 列表 - up_limit_id:生成 Read View 时当前系统中活跃的最小的事务 id,也就是 m_ids 中的最小值 -- low_limit_id:生成 Read View 时系统应该分配给下一个事务的 id 值 +- low_limit_id:生成 Read View 时系统应该分配给下一个事务的 id 值,m_ids 中的最大值加1 - creator_trx_id:生成该 Read View 的事务的事务id creator 创建一个 Read View,进行可见性算法分析:(**解决了读未提交**) @@ -2448,7 +2446,7 @@ START TRANSACTION; -- 开启事务 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MVCC工作流程1.png) -ID 为0的事务创建 Read View: +ID 为 0 的事务创建 Read View: * m_ids:20、60 * up_limit_id:20 @@ -2480,12 +2478,12 @@ RR、RC生成时机: 解决幻读问题: -- 快照读:通过MVCC来进行控制的,不用加锁,当前事务只生成一次 Read View,外部操作没有影响 -- 当前读:通过next-key锁(行锁+间隙锁)来解决问题 +- 快照读:通过 MVCC 来进行控制的,不用加锁,当前事务只生成一次 Read View,外部操作没有影响 +- 当前读:通过 next-key 锁(行锁+间隙锁)来解决问题 -RC、RR级别下的InnoDB快照读区别 +RC、RR 级别下的 InnoDB 快照读区别 -- 在RR级别下的某个事务的对某条记录的第一次快照读会创建一个 Read View, 将当前系统活跃的其他事务记录起来。此后在调用快照读的时候,使用的是同一个Read View。 +- 在RR级别下的某个事务的对某条记录的第一次快照读会创建一个 Read View, 将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,使用的是同一个Read View。 当前事务在其他事务提交之前使用过快照读,那么以后其他事务对数据的修改都是不可见的,就算以后其他事务提交了数据也不可见;早于 Read View 创建的事务所做的修改并提交的均是可见的 @@ -2507,7 +2505,7 @@ RC、RR级别下的InnoDB快照读区别 视图概念:视图是一种虚拟存在的数据表,这个虚拟的表并不在数据库中实际存在 -本质:将一条SELECT查询语句的结果封装到了一个虚拟表中,所以在创建视图的时候,工作重心要放在这条SELECT查询语句上 +本质:将一条 SELECT 查询语句的结果封装到了一个虚拟表中,所以在创建视图的时候,工作重心要放在这条 SELECT 查询语句上 作用:将一些比较复杂的查询语句的结果,封装到一个虚拟表中,再有相同查询需求时,直接查询该虚拟表 @@ -4114,12 +4112,12 @@ B+Tree优点:提高查询速度,减少磁盘的IO次数,树形结构较小 索引下推充分利用了索引中的数据,在查询出整行数据之前过滤掉无效的数据,再去主键索引树上查找 * 不使用索引下推优化时存储引擎通过索引检索到数据返回给 MySQL 服务器,服务器判断数据是否符合条件 -* 使用索引下推优化时,如果存在某些被索引的列的判断条件时,MySQL 服务器将这一部分**判断条件传递给存储引擎**,然后由存储引擎在索引内部判断索引是否符合传递的条件,只有当索引符合条件时才会将数据检索出来返回给MySQL服务器,由此减少 IO次数 +* 使用索引下推优化时,如果存在某些被索引的列的判断条件时,MySQL 服务器将这一部分**判断条件传递给存储引擎**,然后由存储引擎在索引内部判断索引是否符合传递的条件,只有当索引符合条件时才会将数据检索出来返回给 MySQL 服务器,由此减少 IO次数 适用条件: * 需要存储引擎将索引中的数据与条件进行判断,所以优化是基于存储引擎的,只有特定引擎可以使用,适用于InnoDB 和 MyISAM引擎 -* 存储引擎没有调用存储过程的能力,跨存储引擎的功能有存储过程、触发器、视图,所以调用这些功能的不可以进行索引下推优化 +* 存储引擎没有调用跨存储引擎的能力,跨存储引擎的功能有存储过程、触发器、视图,所以调用这些功能的不可以进行索引下推优化 * 对于 InnoDB 引擎只适用于二级索引,InnoDB 的聚簇索引会将整行数据读到缓冲区,因为数据已经在内存中了,不再需要去读取了,索引下推的目的减少IO次数也就失去了意义 工作过程:用户表 user,(name,sex) 是联合索引 @@ -4132,7 +4130,7 @@ SELECT * FROM user WHERE name LIKE '王%' AND sex=1; -- 头部模糊匹配会 -* 优化后:检查索引中存储的列信息是否符合索引条件,如果符合将整行数据读取出来,然后用剩余的判断条件判断此行数据是否符合要求,符合要求就根据主键值进行回表查询,2 次回表 +* 优化后:检查索引中存储的列信息是否符合索引条件,然后交由存储引擎用剩余的判断条件判断此行数据是否符合要求,**不满足条件的不去读取表中的数据**,满足下推条件的就根据主键值进行回表查询,2 次回表 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-索引下推优化2.png) 当使用EXPLAIN进行分析时,如果使用了索引条件下推,Extra 会显示 Using index condition @@ -4323,7 +4321,7 @@ MySQL执行计划的局限: * EXPLAIN 不能显示MySQL在执行查询时所作的优化工作,因为执行计划在执行查询之前生成 * EXPALIN 部分统计信息是估算的,并非精确值 * EXPALIN 只能解释SELECT操作,其他操作要重写为SELECT后查看执行计划 -* 执行计划在优化器之后、执行器之前生成,然后执行器调用存储引擎检索数据 +* 执行计划 在优化器之后、执行器之前生成,然后执行器调用存储引擎检索数据 * 执行计划可以随着底层优化器输入的更改而更改。EXPLAIN PLAN 显示的是在解释语句时数据库将如何运行SQL语句,由于执行环境和 EXPLAIN PLAN 环境的不同,此计划可能与SQL语句实际的执行计划不同 环境准备: @@ -4374,7 +4372,7 @@ SQL执行的顺序的标识,SQL从大到小的执行 ##### select_type -表示查询中每个select子句的类型(简单 OR复杂) +表示查询中每个select子句的类型(简单 OR 复杂) | select_type | 含义 | | ------------------ | ------------------------------------------------------------ | @@ -4436,7 +4434,7 @@ possible_keys: key: * 显示MySQL在查询中实际使用的索引,若没有使用索引,显示为NULL -* 查询中若使用了**覆盖索引**,则该索引仅出现在key列表中 +* 查询中若使用了**覆盖索引**,则该索引仅出现在key列表 key_len: @@ -4540,7 +4538,7 @@ MySQL 提供了对SQL的跟踪, 通过trace文件能够进一步了解执行 SELECT * FROM tb_item WHERE id < 4; ``` -* 检查information_schema.optimizer_trace: +* 检查 information_schema.optimizer_trace: ```mysql SELECT * FROM information_schema.optimizer_trace \G; -- \G代表竖列展示 @@ -4738,7 +4736,7 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name,status,address); -* 以%开头的Like模糊查询,索引失效,比如语句:`WHERE a LIKE '%d'`,前面的不确定,导致不符合最匹配,直接去索引中搜索以 d 结尾的节点,所以没有顺序 +* 以%开头的Like模糊查询,索引失效,比如语句:`WHERE a LIKE '%d'`,前面的不确定,导致不符合最左匹配,直接去索引中搜索以 d 结尾的节点,所以没有顺序 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-索引失效底层原理3.png) @@ -4770,8 +4768,6 @@ SHOW GLOBAL STATUS LIKE 'Handler_read%'; - - *** @@ -5165,7 +5161,7 @@ SQL提示,是优化数据库的一个重要手段,就是在SQL语句中加 INSERT INTO tb_test VALUES(1,'Tom'),(2,'Cat'),(3,'Jerry'); -- 连接一次 ``` -增加cache层:在应用中增加缓存层来达到减轻数据库负担的目的。可以部分数据从数据库中抽取出来放到应用端以文本方式存储, 或者使用框架(Mybatis)提供的一级缓存 / 二级缓存,或者使用Redis数据库来缓存数据 +增加 cache 层:在应用中增加缓存层来达到减轻数据库负担的目的。可以部分数据从数据库中抽取出来放到应用端以文本方式存储, 或者使用框架(Mybatis)提供的一级缓存 / 二级缓存,或者使用Redis数据库来缓存数据 @@ -5313,7 +5309,7 @@ SQL提示,是优化数据库的一个重要手段,就是在SQL语句中加 * 在存储过程、触发器或存储函数的主体内执行的查询,缓存失效 -* 如果表更改,则使用该表的所有高速缓存查询都将变为无效并从高速缓存中删除,包括使用 MERGE 映射到已更改表的表的查询。比如:INSERT、UPDATE、DELETE、ALTER TABLE、DROP TABLE、DROP DATABASE +* 如果表更改,则使用该表的所有高速缓存查询都将变为无效并从高速缓存中删除,包括使用 MERGE 映射到已更改表的表的查询,比如:INSERT、UPDATE、DELETE、ALTER TABLE、DROP TABLE、DROP DATABASE @@ -5328,7 +5324,7 @@ SQL提示,是优化数据库的一个重要手段,就是在SQL语句中加 三个原则: * 将尽量多的内存分配给 MySQL 做缓存,但也要给操作系统和其他程序预留足够内存 -* MyISAM 存储引擎的数据文件读取依赖于操作系统自身的 IO 缓存,如果有MyISAM表,就要预留更多的内存给操作系统做 IO 缓存 +* MyISAM 存储引擎的数据文件读取依赖于操作系统自身的 IO 缓存,如果有 MyISAM 表,就要预留更多的内存给操作系统做 IO 缓存 * 排序区、连接区等缓存是分配给每个数据库会话(Session)专用的,值的设置要根据最大连接数合理分配,如果设置太大,不但浪费资源,而且在并发数较高时会导致物理内存耗尽 @@ -5418,7 +5414,7 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 为了加快连接数据库的速度,MySQL 会缓存一定数量的客户服务线程以备重用,池化思想 -* innodb_lock_wait_timeout:设置 InnoDB 事务等待行锁的时间,默认值是50ms +* innodb_lock_wait_timeout:设置 InnoDB 事务等待行锁的时间,默认值是 50ms 对于需要快速反馈的业务系统,可以将行锁的等待时间调小,以避免事务被长时间挂起; 对于后台运行的批量处理程序来说,可以将行锁的等待时间调大,以避免发生大的回滚操作 @@ -5432,7 +5428,7 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 #### 基本介绍 -复制是指将主数据库的 DDL 和 DML 操作通过二进制日志传到从库服务器中,然后在从库上对这些日志重新执行(也叫重做),从而使得从库和主库的数据保持同步。 +复制是指将主数据库的 DDL 和 DML 操作通过二进制日志传到从库服务器中,然后在从库上对这些日志重新执行(也叫重做),从而使得从库和主库的数据保持同步 MySQL支持一台主库同时向多台从库进行复制,从库同时也可以作为其他从服务器的主库,实现链状复制 @@ -5456,12 +5452,47 @@ MySQL 的主从复制原理图: ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-主从复制原理图.jpg) -从上层来看,复制分成三步: +主从复制需要三个线程:**master(binlog dump thread)、slave(I/O thread 、SQL thread)** + +- binlog dump thread:在主库事务提交时,负责把数据变更作为事件 Events 记录在二进制日志文件 binlog 中,并通知 slave 有数据更新 +- I/O thread:负责从主服务器上读取二进制日志,并将 binlog 日志内容依次写到 relay log 文件的最末端,并将新的 binlog 文件名和位置记录到 master-info 文件中,以便下一次读取master 端新 binlog 日志时能告诉 master 服务器从新 binlog 日志的指定文件及位置开始读取新的 binlog 日志内容 +- SQL thread:监测本地 relay log中新增了日志内容,读取中继日志并重做其中的 SQL 语句 +- 从库在 relay-log.info 中记录当前应用中继日志的文件名和位置点以便下一次数据复制 + + + +**** + + + +#### 主从延迟 + +主从延迟就是主从之间是存在一定时间的数据不一致: + +- 主库 A 执行完成一个事务,写入 binlog,该时刻记为 T1 +- 传给从库B,从库接受完这个binlog的时刻记为 T2 +- 从库B执行完这个事务,该时刻记为 T3 + +同一个事务,从库执行完成的时间和主库执行完成的时间之间的差值,即 T3-T,通过在从库执行 `show slave status` 命令,返回结果会显示 seconds_behind_master 表示当前从库延迟了多少秒 + +- 每一个事务的 binlog 都有一个时间字段,用于记录主库上写入的时间 +- 从库取出当前正在执行的事务的时间字段,跟系统的时间进行相减,得到的就是 seconds_behind_master + +主从延迟的原因: + +* 从库的查询压力大 +* 大事务的执行,主库必须要等到事务完成之后才会写入 binlog,导致从节点出现应用 binlog 延迟 +* 主库的DDL,从库与主库的 DDL 同步是串行进行,DDL 在主库执行时间很长,那么从库也会消耗同样的时间 +* 锁冲突问题也可能导致从节点的 SQL 线程执行慢 +* 从库的机器性能比主库的差,导致从库的复制能力弱 -- Master 主库在事务提交时,会把数据变更作为事件 Events 记录在二进制日志文件 Binlog 中 -- 主库推送二进制日志文件 Binlog 中的日志事件到从库的中继日志 Relay Log +主从同步问题永远都是一致性和性能的权衡,需要根据实际的应用场景,可以采取下面的办法: -- Slave 重做中继日志中的事件 +* 降低多线程大事务并发的概率,优化业务逻辑 +* 优化SQL,避免慢SQL,减少批量操作 +* 提高从库机器的配置,减少主库写 binlog 和从库读 binlog 的效率差 +* 尽量采用短的链路,主库和从库服务器的距离尽量要短,提升端口带宽,减少 binlog 传输的网络延时 +* 实时性要求的业务读强制走主库,从库只做灾备,备份 @@ -5773,7 +5804,7 @@ MyISAM 的读写锁调度是写优先,因为写锁后其他线程不能做任 -#### 锁竞争 +#### 锁状态 * 查看锁竞争: @@ -5822,7 +5853,7 @@ InnoDB 实现了以下两种类型的行锁: - 共享锁 (S):又称为读锁,简称S锁,就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改 - 排他锁 (X):又称为写锁,简称X锁,就是不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,只有获取排他锁的事务是可以对数据读取和修改 -对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁;对于普通SELECT语句,InnoDB不会加任何锁 +对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会自动给涉及数据集加排他锁;对于普通 SELECT 语句,InnoDB 不会加任何锁 锁的兼容性: @@ -5939,14 +5970,14 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 #### 锁升级 -五索引行锁升级为表锁:不通过索引检索数据,那么 InnoDB 将对表中的所有记录加锁,实际效果和加表锁一样 +无索引行锁升级为表锁:不通过索引检索数据,那么 InnoDB 将对表中的所有记录加锁,实际效果和加表锁一样 索引失效会造成锁升级,实际开发过程应避免出现索引失效的状况 * 查看当前表的索引: ```mysql - SHOW INDEX FROM test_innodb_lock; + SHOW INDEX FROM test_innodb_lock; ``` * 关闭自动提交功能: @@ -6009,7 +6040,7 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 -#### 锁竞争 +#### 锁状态 ```mysql SHOW STATUS LIKE 'innodb_row_lock%'; @@ -6170,7 +6201,7 @@ tail -f /var/log/mysql/error.log #### 基本介绍 -归档日志(BINLOG)记录了所有的 DDL(数据定义语言)语句和 DML(数据操作语言)语句,但不包括数据查询语句,在事务提交时写入。归档日志也叫二进制日志,是因为采用二进制进行存储 +归档日志(BINLOG)记录了所有的 DDL(数据定义语言)语句和 DML(数据操作语言)语句,但**不包括数据查询语句**,在事务提交时写入。归档日志也叫二进制日志,是因为采用二进制进行存储 作用:**灾难时的数据恢复和 MySQL 的主从复制** diff --git a/Java.md b/Java.md index e100b1f..0082535 100644 --- a/Java.md +++ b/Java.md @@ -19399,7 +19399,7 @@ Servlet 为了保证其线程安全,一般不为 Servlet 设置成员变量, ThreadLocal类用来提供线程内部的局部变量,这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量 -ThreadLocal实例通常来说都是`private static`类型的,属于一个线程的本地变量,用于关联线程和线程上下文。每个线程都会在 ThreadLocal 中保存一份该线程独有的数据,所以是线程安全的 +ThreadLocal实例通常来说都是 `private static` 类型的,属于一个线程的本地变量,用于关联线程和线程上下文。每个线程都会在 ThreadLocal 中保存一份该线程独有的数据,所以是线程安全的 ThreadLocal 作用: @@ -25550,7 +25550,7 @@ public class RuntimeDemo { #### 简单工厂 -简单工厂包含如下角色: +简单工厂,也称为静态工厂模式,包含如下角色: * 抽象产品 :定义了产品的规范,描述了产品的主要特性和功能 * 具体产品 :实现或者继承抽象产品的子类 @@ -25581,7 +25581,7 @@ public class RuntimeDemo { } ``` -* 简单工厂类,也可以修改为静态工厂模式,在 createCoffee 方法加 static +* 简单工厂类,在 createCoffee 方法加 static ```java public class SimpleCoffeeFactory { From e5c1a2728927a6054adf3a8b874963dca56b4066 Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 7 Jun 2021 19:54:45 +0800 Subject: [PATCH 041/242] Update Java Notes --- DB.md | 32 +- Java.md | 1102 ++++++++++++++++++++++--------------------------------- SSM.md | 8 +- Tool.md | 6 +- 4 files changed, 467 insertions(+), 681 deletions(-) diff --git a/DB.md b/DB.md index c52744b..839c282 100644 --- a/DB.md +++ b/DB.md @@ -2189,7 +2189,7 @@ rollback segment 称为回滚段,每个回滚段中有 1024 个 undo log segme 持久性是指一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,接下来的其他操作或故障不应该对其有任何影响。 -redo log,记录数据页的物理修改,而不是某一行或某几行的修改,用来恢复提交后的物理数据页,且只能恢复到最后一次提交的位置 +redo log,记录**数据页的物理修改**,而不是某一行或某几行的修改,用来恢复提交后的物理数据页,且只能恢复到最后一次提交的位置 InnoDB 作为存储引擎,数据是存放在磁盘中,每次读写数据都需要磁盘 IO,效率会很低。InnoDB 提供了缓存 Buffer Pool,Buffer Pool 中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲: @@ -5455,7 +5455,7 @@ MySQL 的主从复制原理图: 主从复制需要三个线程:**master(binlog dump thread)、slave(I/O thread 、SQL thread)** - binlog dump thread:在主库事务提交时,负责把数据变更作为事件 Events 记录在二进制日志文件 binlog 中,并通知 slave 有数据更新 -- I/O thread:负责从主服务器上读取二进制日志,并将 binlog 日志内容依次写到 relay log 文件的最末端,并将新的 binlog 文件名和位置记录到 master-info 文件中,以便下一次读取master 端新 binlog 日志时能告诉 master 服务器从新 binlog 日志的指定文件及位置开始读取新的 binlog 日志内容 +- I/O thread:负责从主服务器上拉取二进制日志,并将 binlog 日志内容依次写到 relay log 文件的最末端,并将新的 binlog 文件名和 offset 记录到 master-info 文件中,以便下一次读取master 端新 binlog 日志时能告诉 master 服务器从新 binlog 日志的指定文件及位置开始读取新的 binlog 日志内容 - SQL thread:监测本地 relay log中新增了日志内容,读取中继日志并重做其中的 SQL 语句 - 从库在 relay-log.info 中记录当前应用中继日志的文件名和位置点以便下一次数据复制 @@ -9621,7 +9621,9 @@ vfork(虚拟内存fork virtual memory fork):调用 vfork() 父进程被挂 ### 基本操作 -redis事务就是一个命令执行的队列,将一系列预定义命令包装成一个整体(一个队列)。当执行时按照添加顺序依次执行,中间不会被打断或者干扰 + Redis 事务没有隔离级别的概念,Redis 单条命令式保存原子性的,但是事务不保证原子性 + +Redis 事务就是一个命令执行的队列,将一系列预定义命令包装成一个整体(一个队列)。当执行时按照添加顺序依次执行,中间不会被打断或者干扰 * 开启事务 @@ -9722,7 +9724,7 @@ Redis 分布式锁的基本使用 * 对于返回设置成功的,拥有控制权,进行下一步的具体业务操作 * 对于返回设置失败的,不具有控制权,排队或等待 -* 操作完毕通过del操作释放锁 +* 操作完毕通过 del 操作释放锁 ```sh del lock-key @@ -9749,9 +9751,9 @@ Redis 分布式锁的基本使用 ### 过期数据 -Redis是一种内存级数据库,所有数据均存放在内存中,内存中的数据可以通过TTL指令获取其状态 +Redis 是一种内存级数据库,所有数据均存放在内存中,内存中的数据可以通过 TTL 指令获取其状态 -TTL返回的值有三种情况:正数,-1,-2 +TTL 返回的值有三种情况:正数,-1,-2 - 正数:代表该数据在内存中还能存活的时间 - -1:永久有效的数据 @@ -9803,7 +9805,7 @@ TTL返回的值有三种情况:正数,-1,-2 #### 惰性删除 -数据到达过期时间,不做处理。等下次访问该数据时,我们需要判断: +数据到达过期时间,不做处理,等下次访问该数据时,我们需要判断: * 如果未过期,返回数据 * 如果已过期,删除,返回不存在 @@ -9832,9 +9834,9 @@ TTL返回的值有三种情况:正数,-1,-2 - Redis启动服务器初始化时,读取配置 server.hz 的值,默认为10。执行指令可以查看:info server -- 每秒钟执行 server.hz 次 serverCron() --> databasesCron() --> activeExpireCycle() +- 每秒钟执行 server.hz 次 serverCron() → databasesCron() → activeExpireCycle() -- databasesCron()操作是轮询每个数据库 +- databasesCron() 操作是轮询每个数据库 - activeExpireCycle() 对某个数据库中的每个 expires 进行检测,每次执行耗时:250ms/server.hz @@ -9852,7 +9854,7 @@ TTL返回的值有三种情况:正数,-1,-2 定期删除特点: -- CPU性能占用设置有峰值,检测频度可自定义设置 +- CPU 性能占用设置有峰值,检测频度可自定义设置 - 内存压力不是很大,长期占用内存的冷数据会被持续清理 - 周期性抽查存储空间(随机抽查,重点抽查) @@ -10190,7 +10192,7 @@ TTL返回的值有三种情况:正数,-1,-2 * 获取 master 平均每秒产生写命令数据总量 write_size_per_second * 最优复制缓冲区空间 = 2 * second * write_size_per_second - 3. master单机内存占用主机内存的比例不应过大,建议使用50%-70%的内存,留下30%-50%的内存用于执 行bgsave命令和创建复制缓冲区 + 3. master 单机内存占用主机内存的比例不应过大,建议使用 50%-70% 的内存,留下 30%-50% 的内存用于执行 bgsave 命令和创建复制缓冲区 * 数据同步阶段slave说明 @@ -10243,14 +10245,14 @@ TTL返回的值有三种情况:正数,-1,-2 * 复制偏移量:一个数字,描述复制缓冲区中的指令字节位置 - master复制偏移量:记录发送给所有slave的指令字节对应的位置(多个) -- slave复制偏移量:记录slave接收master发送过来的指令字节对应的位置(一个) + - slave复制偏移量:记录slave接收master发送过来的指令字节对应的位置(一个) 作用:同步信息,比对 master 与 slave 的差异,当 slave 断线后,恢复数据使用 - + 数据来源: - + - master 端:发送一次记录一次 -- slave 端:接收一次记录一次 + - slave 端:接收一次记录一次 **工作原理**: diff --git a/Java.md b/Java.md index 0082535..ce52078 100644 --- a/Java.md +++ b/Java.md @@ -115,7 +115,7 @@ G-->H[double] * float 与 double: - Java 不能隐式执行**向下转型**,因为这会使得精度降低(参考多态) + Java 不能隐式执行**向下转型**,因为这会使得精度降低(参考多态),但是可以向上转型 ```java //1.1字面量属于double类型,不能直接将1.1直接赋值给 float 变量,因为这是向下转型 @@ -314,13 +314,14 @@ System.out.println(x == y); // false #### 输入数据 -语法:`Scanner sc = new Scanner(System.in);` - next() : 遇到了空格, 就不再录入数据了 , 结束标记: 空格, tab键 - nextLine() : 可以将数据完整的接收过来 , 结束标记: 回车换行符 +语法:`Scanner sc = new Scanner(System.in)` -一般使用`sc.nextInt()`或者`sc.nextLine()`接受整型和字符串,然后转成需要的数据类型。 +* next():遇到了空格, 就不再录入数据了 , 结束标记: 空格, tab键 +* nextLine():可以将数据完整的接收过来 , 结束标记: 回车换行符 -Scanner:`BufferedReader br = new BufferedReader(new InputStreamReader(System.in))` +一般使用 `sc.nextInt()` 或者 `sc.nextLine()` 接受整型和字符串,然后转成需要的数据类型 + +Scanner:`BufferedReader br = new BufferedReader(new InputStreamReader(System.in))` print:`PrintStream.write()` > 使用引用数据类型的API @@ -1498,7 +1499,7 @@ static 静态修饰的成员(方法和成员变量)属于类本身的。 * 成员变量: * 静态成员变量(类变量): - 有static修饰的成员变量称为静态成员变量也叫类变量,属于类本身,**与类一起加载一次,只有一个**,直接用类名访问即可。 + 有static修饰的成员变量称为静态成员变量也叫类变量,属于类本身,**与类一起加载一次,只有一个**,直接用类名访问即可。 * 实例成员变量: 无static修饰的成员变量称为实例成员变量,属于类的每个对象的。**与类的对象一起加载**,对象有多少个,实例成员变量就加载多少个,必须用类的对象来访问。 @@ -2310,7 +2311,7 @@ class Animal{ 引用数据类型的**自动**类型转换语法:子类类型的对象或者变量可以自动类型转换赋值给父类类型的变量。 -核心:**父类引用指向子类对象** +**父类引用指向子类对象** - **向上转型(upcasting)**:通过子类对象(小范围)实例化父类对象(大范围),这种属于自动转换 - **向下转型(downcasting)**:通过父类对象(大范围)实例化子类对象(小范围),这种属于强制转换 @@ -6904,9 +6905,14 @@ list.stream().filter(s -> s.startsWith("张")); +*** + + + #### 获取流 -集合获取Stream流用: default Stream stream(); +集合获取Stream流用:default Stream stream() + 数组:Arrays.stream(数组) / Stream.of(数组); ```java @@ -6930,6 +6936,10 @@ Stream arrStream2 = Stream.of(arr); +**** + + + #### 常用API | 方法名 | 说明 | @@ -6983,6 +6993,10 @@ class Student{ +*** + + + #### 终结方法 终结方法:Stream调用了终结方法,流的操作就全部终结,不能继续使用,如foreach , count方法等 @@ -6997,6 +7011,10 @@ class Student{ +*** + + + #### 收集流 收集Stream流的含义:就是把Stream流的数据转回到集合中去。 @@ -7280,11 +7298,12 @@ public static void searchFiles(File dir , String fileName){ ### Character -字符集:各个国家为自己国家的字符取的一套编号规则。 - 计算机的底层是不能直接存储字符的,只能存储二进制,010101。 +字符集:各个国家为自己国家的字符取的一套编号规则 + +计算机的底层是不能直接存储字符的,只能存储二进制,010101。 美国人: - 8个开关一组就可以编码字符。 1个字节。 + 8个开关一组就可以编码字符,1个字节。 2^8 = 256 一个字节存储一个字符完全够用了。 @@ -7382,12 +7401,10 @@ FileInputStream文件字节输入流: `public FileInputStream(String pathName)` : 创建一个字节输入流管道与文件路径对接,底层实质上创建了File对象 * 方法: - `public int read()` : 每次读取一个字节返回!读取完毕会返回-1 - `public int read(byte[] buffer)`:从字节输入流中读取字节到字节数组中去, - 返回读取的字节数量,没有字节可读返回-1。 - **byte中新读取的数据默认是覆盖原数据**,构造String需要设定长度 + `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) ` : 从输入流中读取所有字节,并按读取的顺序,将字节写入给定的输出流(jdk9以后的新方法) `is.transferTo(os)` + `public long transferTo(OutputStream out) ` : 从输入流中读取所有字节,并按读取的顺序,将字节写入给定的输出流`is.transferTo(os)` ```java public class FileInputStreamDemo01 { @@ -7410,7 +7427,7 @@ public class FileInputStreamDemo01 { } ``` -一个一个字节读取英文和数字没有问题。但是一旦读取中文输出无法避免乱码,因为会截断中文的字节。一个一个字节的读取数据,性能也较差,所以**禁止使用上面的方案**! +一个一个字节读取英文和数字没有问题。但是一旦读取中文输出无法避免乱码,因为会截断中文的字节。一个一个字节的读取数据,性能也较差,所以**禁止使用上面的方案** 采取下面的方案: @@ -7420,9 +7437,9 @@ public static void main(String[] args) throws Exception { InputStream is = new FileInputStream("Day09Demo/src/dlei01.txt"); byte[] buffer = new byte[3];//开发中使用byte[1024] int len; - while((len = is.read(buffer))!=-1){ + while((len = is.read(buffer)) !=-1){ // 读取了多少就倒出多少! - String rs = new String(buffer,0,len);!!!!!!!!!!! + String rs = new String(buffer, 0, len); System.out.print(rs); } } @@ -7717,6 +7734,10 @@ public class BufferedOutputStreamDemo02 { +**** + + + ##### 字符缓冲输入流 字符缓冲输入流:BufferedReader @@ -7932,7 +7953,7 @@ class User implements Serializable { -##### 反序列化 +##### 反序列 对象反序列化(对象字节输入流):ObjectInputStream @@ -7983,7 +8004,9 @@ class User implements Serializable { * `public PrintStream(OutputStream os)` * `public PrintStream(String filepath)` -System:`public static void setOut(PrintStream out)` 让系统的输出流向打印流 +System类: + +* `public static void setOut(PrintStream out)`:让系统的输出流向打印流 ```java public class PrintStreamDemo01 { @@ -8120,8 +8143,8 @@ 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)` : 设置文件指针偏移,从该文件开头测量,发生下一次读取或写入(插入+覆盖) @@ -8144,10 +8167,11 @@ public static void main(String[] args) throws Exception { -### Commons-io +### Commons + +commons-io 是apache开源基金组织提供的一组有关IO操作的类库,可以挺提高IO功能开发的效率。 -commons-io是apache开源基金组织提供的一组有关IO操作的类库,可以挺提高IO功能开发的效率。 -commons-io工具包提供了很多有关io操作的类: +commons-io 工具包提供了很多有关 IO 操作的类: | 包 | 功能描述 | | ----------------------------------- | :------------------------------------------- | @@ -8156,9 +8180,7 @@ commons-io工具包提供了很多有关io操作的类: | org.apache.commons.io.output | 输出流相关的实现类,包含Writer和OutputStream | | org.apache.commons.io.serialization | 序列化相关的类 | - - -IOUtils和FileUtils可以方便的复制文件和文件夹!! +IOUtils 和 FileUtils 可以方便的复制文件和文件夹 ```java public class CommonsIODemo01 { @@ -8259,7 +8281,7 @@ UDP:用户数据报协议(User Datagram Protocol),是一个面向无连接 -#### 通信模型 +#### Java模型 相关概念: @@ -8270,7 +8292,7 @@ UDP:用户数据报协议(User Datagram Protocol),是一个面向无连接 Java中的通信模型: -1. BIO通信模式:同步阻塞式通信,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善。 +1. BIO表示同步阻塞式通信,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善。 同步阻塞式性能极差:大量线程,大量阻塞 2. 伪异步通信:引入线程池,不需要一个客户端一个线程,实现线程复用来处理很多个客户端,线程可控。 @@ -8282,7 +8304,7 @@ Java中的通信模型: 同步:线程还要不断的接收客户端连接,以及处理数据 非阻塞:如果一个管道没有数据,不需要等待,可以轮询下一个管道是否有数据 -4. AIO表示异步非阻塞IO,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由操作系统先完成,完成后通知服务器应用来启动线程进行处理 +4. AIO表示异步非阻塞IO,AIO 引入异步通道的概念,采用了 Proactor 模式,有效的请求才启动线程,特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用 异步:服务端线程接收到了客户端管道以后就交给底层处理IO通信,线程可以做其他事情 非阻塞:底层也是客户端有数据才会处理,有了数据以后处理好通知服务器应用来启动线程进行处理 @@ -8345,7 +8367,7 @@ recvfrom() 用于接收 Socket 传来的数据,并复制到应用进程的缓 ##### 非阻塞式 -应用进程通过 recvfrom 调用不停的去和内核交互,直到内核准备好数据。如果没有准备好数据,内核返回一个错误码,过一段时间应用进程再执行 recvfrom 系统调用,在两次发送请求的时间段,进程可以其他任务,这种方式称为轮询(polling) +应用进程通过 recvfrom 调用不停的去和内核交互,直到内核准备好数据。如果没有准备好数据,内核返回一个错误码,过一段时间应用进程再执行 recvfrom 系统调用,在两次发送请求的时间段,进程可以进行其他任务,这种方式称为轮询(polling) 由于 CPU 要处理更多的系统调用,因此这种模型的 CPU 利用率比较低 @@ -8415,13 +8437,13 @@ select和poll差别不多,一个是数组一个是链表,因此poll的连接 ### 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 { @@ -8456,43 +8478,52 @@ public class InetAddressDemo { #### 基本介绍 UDP(User Datagram Protocol)协议的特点: - 面向无连接的协议 - 发送端只管发送,不确认对方是否能收到。 - 基于数据包进行数据传输。 - 发送数据的包的大小限制**64KB**以内 - 因为面向无连接,速度快,但是不可靠。会丢失数据! -UDP协议的使用场景:在线视频,网络语音,电话。 +* 面向无连接的协议 +* 发送端只管发送,不确认对方是否能收到 +* 基于数据包进行数据传输 +* 发送数据的包的大小限制**64KB**以内 +* 因为面向无连接,速度快,但是不可靠,会丢失数据 + +UDP协议的使用场景:在线视频、网络语音、电话 #### UDP实现 UDP协议相关的两个类 - DatagramPacket(数据包对象):用来封装要发送或要接收的数据,比如:集装箱 - DatagramSocket(发送对象):用来发送或接收数据包,比如:码头 + +* DatagramPacket(数据包对象):用来封装要发送或要接收的数据,比如:集装箱 +* DatagramSocket(发送对象):用来发送或接收数据包,比如:码头 **DatagramPacket**: * DatagramPacket类 - * `public new DatagramPacket(byte[] buf, int length, InetAddress address, int port)` : 创建发送端数据包对象 - 参数: buf:要发送的内容,字节数组 length:要发送内容的长度,单位是字节 - address:接收端的IP地址对象 port:接收端的端口号 - * `public new DatagramPacket(byte[] buf, int length)` : 创建接收端的数据包对象 - 参数:buf:用来存储接收到内容 length:能够接收内容的长度 -* DatagramPacket类常用方法 + + `public new DatagramPacket(byte[] buf, int length, InetAddress address, int port)` : 创建发送端数据包对象,参数: + + * buf:要发送的内容,字节数组 + * length:要发送内容的长度,单位是字节 + * address:接收端的IP地址对象 + * port:接收端的端口号 + + `public new DatagramPacket(byte[] buf, int length)` : 创建接收端的数据包对象,参数: + + * buf:用来存储接收到内容 + * length:能够接收内容的长度 +* DatagramPacket 类常用方法 `public int getLength()` : 获得实际接收到的字节个数 `public byte[] getData()` : 返回数据缓冲区 **DatagramSocket**: * DatagramSocket类构造方法 - `protected DatagramSocket()` : 创建发送端的Socket对象,系统会随机分配一个端口号。 - `protected DatagramSocket(int port)` : 创建接收端的Socket对象并指定端口号 + `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 { @@ -8535,26 +8566,24 @@ public class UDPServerDemo{ -#### 通讯方式 +*** -UDP通信方式: -+ 单播 - 单播用于两个主机之间的端对端通信 +#### 通讯方式 -+ 组播 +UDP通信方式: - 组播用于对一组特定的主机进行通信 ++ 单播:用于两个主机之间的端对端通信 + ++ 组播:用于对一组特定的主机进行通信 IP : 224.0.1.0 Socket对象 : MulticastSocket - -+ 广播 - - 广播用于一个主机对整个局域网上所有主机上的数据通信 + ++ 广播:用于一个主机对整个局域网上所有主机上的数据通信 IP : 255.255.255.255 Socket对象 : DatagramSocket - + *** @@ -8566,16 +8595,17 @@ UDP通信方式: #### 基本介绍 TCP/IP协议 ==> Transfer Control Protocol ==> 传输控制协议 + TCP/IP协议的特点: - 面向连接的协议 - 只能由客户端主动发送数据给服务器端,服务器端接收到数据之后,可以给客户端响应数据。 - 通过**三次握手**建立连接,连接成功形成数据传输通道。 - 通过**四次挥手**断开连接 - 基于IO流进行数据传输 - 传输数据大小没有限制 - 因为面向连接的协议,速度慢,但是是可靠的协议。 -TCP协议的使用场景:文件上传和下载;邮件发送和接收;远程登录。 +* 面向连接的协议 +* 只能由客户端主动发送数据给服务器端,服务器端接收到数据之后,可以给客户端响应数据 +* 通过**三次握手**建立连接,连接成功形成数据传输通道;通过**四次挥手**断开连接 +* 基于IO流进行数据传输 +* 传输数据大小没有限制 +* 因为面向连接的协议,速度慢,但是是可靠的协议。 + +TCP协议的使用场景:文件上传和下载、邮件发送和接收、远程登录 注意:**TCP不会为没有数据的ACK超时重传** @@ -8591,33 +8621,34 @@ TCP协议的使用场景:文件上传和下载;邮件发送和接收;远 #### Socket -TCP通信也叫**Socket网络编程**,只要代码基于Socket开发,底层就是基于了可靠传输的TCP通信。 +TCP通信也叫**Socket网络编程**,只要代码基于Socket开发,底层就是基于了可靠传输的TCP通信 -TCP协议相关的类 - Socket:一个该类的对象就代表一个客户端程序。 - ServerSocket:一个该类的对象就代表一个服务器端程序。 +TCP协议相关的类: -* Socket类 +* Socket:一个该类的对象就代表一个客户端程序。 +* ServerSocket:一个该类的对象就代表一个服务器端程序。 - * 构造方法: - `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类 + +* 构造方法: + `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类: + +* 构造方法:`public ServerSocket(int port)` +* 常用API:`public Socket accept()`,**阻塞等待**接收一个客户端的Socket管道连接请求,连接成功返回一个Socket对象 + +相当于客户端和服务器建立一个数据管道,管道一般不用close -* ServerSocket类: - - * 构造方法:`public ServerSocket(int port)` - * 常用API:`public Socket accept()`:**阻塞等待**接收一个客户端的Socket管道连接请求,连接成功返回一个Socket对象 - 相当于客户端和服务器建立一个数据管道,管道一般不用close。 - - *** @@ -8628,14 +8659,17 @@ TCP协议相关的类 ##### 开发流程 客户端的开发流程: - 1.客户端要请求于服务端的socket管道连接。 - 2.从socket通信管道中得到一个字节输出流 - 3.通过字节输出流给服务端写出数据。 + +1. 客户端要请求于服务端的socket管道连接 +2. 从socket通信管道中得到一个字节输出流 +3. 通过字节输出流给服务端写出数据 + 服务端的开发流程: - 1.用ServerSocket注册端口。 - 2.接收客户端的Socket管道连接。 - 3.从socket通信管道中得到一个字节输入流。 - 4.从字节输入流中读取客户端发来的数据。 + +1. 用ServerSocket注册端口 +2. 接收客户端的Socket管道连接 +3. 从socket通信管道中得到一个字节输入流 +4. 从字节输入流中读取客户端发来的数据 ![](https://gitee.com/seazean/images/raw/master/Java/BIO工作机制.png) @@ -8651,7 +8685,7 @@ TCP协议相关的类 -##### BIO +##### BIO通信 需求一:客户端发送一行数据,服务端接收一行数据 @@ -8755,9 +8789,9 @@ public class ServerDemo{ System.out.println("----服务端启动----"); ServerSocket serverSocket = new ServerSocket(8080); while(true){ - // 3.开始等待接收客户端的Socket管道连接。 + // 开始等待接收客户端的Socket管道连接。 Socket socket = serverSocket.accept(); - // 4.每接收到一个客户端必须为这个客户端管道分配一个独立的线程来处理与之通信。 + // 每接收到一个客户端必须为这个客户端管道分配一个独立的线程来处理与之通信。 new ServerReaderThread(socket).start(); } } @@ -8772,10 +8806,10 @@ class ServerReaderThread extends Thread{ ){ String line; while((line = br.readLine()) != null){ - sout(socket.getRemoteSocketAddress() + ":" + line); + sout(socket.getRemoteSocketAddress() + ":" + line); } }catch(Exception e){ - sout(socket.getRemoteSocketAddress()+"下线了~~~~~~"); + sout(socket.getRemoteSocketAddress() + "下线了~~~~~~"); } } } @@ -8783,88 +8817,67 @@ class ServerReaderThread extends Thread{ -##### 伪异步通信 +*** -一个客户端要一个线程,这种模型是不行的,并发越高,系统瘫痪的越快!! -我们可以在服务端引入线程池,使用线程池来处理与客户端的消息通信! -优势:不会引起系统的死机,可以控制并发线程的数量。 -劣势:同时可以并发的线程将受到限制。 + +##### 伪异步 + +一个客户端要一个线程,这种模型是不行的,并发越高系统瘫痪的越快,可以在服务端引入线程池,使用线程池来处理与客户端的消息通信 + +优势:不会引起系统的死机,可以控制并发线程的数量 +劣势:同时可以并发的线程将受到限制 ```java -public class Client { - public static void main(String[] args) throws Exception{ - Socket socket = new Socket("127.0.0.1",8080); - OutputStream os = new socket.getOutputStream(); - PrintStream ps = new PrintStream(os); - BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); - String msg; - while ((msg = br.readLine()) != null) { - ps.println(msg); - ps.flush(); +public class BIOServer { + public static void main(String[] args) throws Exception { + //线程池机制 + //创建一个线程池,如果有客户端连接,就创建一个线程,与之通讯(单独写一个方法) + ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(); + //创建ServerSocket + ServerSocket serverSocket = new ServerSocket(6666); + System.out.println("服务器启动了"); + while (true) { + System.out.println("线程名字 = " + Thread.currentThread().getName()); + //监听,等待客户端连接 + System.out.println("等待连接...."); + final Socket socket = serverSocket.accept(); + System.out.println("连接到一个客户端"); + //创建一个线程,与之通讯 + newCachedThreadPool.execute(new Runnable() { + public void run() { + //可以和客户端通讯 + handler(socket); + } + }); } } -} - -public class Server { - public static void main(String[] args) { - try { - System.out.println("----------服务端启动成功------------"); - ServrSocket ss = new ServerSocket(8080); - // 一个服务端只需要对应一个线程池 - HandlerSocketThreadPool handlerSocketThreadPool = - new HandlerSocketThreadPool(3, 100); - // 客户端可能有很多个 - while(true){ - Socket socket = ss.accept() ; - System.out.println("有人上线了!!"); - // 每次收到一个客户端的socket请求,都需要为这个客户端分配一个 - // 独立的线程 专门负责对这个客户端的通信!! - handlerSocketThreadPool.execute(new ReaderClientRunnable(socket)); - } - } catch (Exception e) { - e.printStackTrace(); - } - } -} -// 线程池处理类 -public class HandlerSocketThreadPool { - - // 线程池 - private ExecutorService executor; - // 线程池:3个线程 100个 - public HandlerSocketThreadPool(int maxPoolSize, int queueSize){ - executor = new ThreadPoolExecutor( - maxPoolSize, - maxPoolSize, - 120L, - TimeUnit.SECONDS, - new ArrayBlockingQueue(queueSize) ); - } - public void execute(Runnable task){ - this.executor.execute(task); - } -} -class ReaderClientRunnable implements Runnable { - private Socket socket ; - public ReaderClientRunnable(Socket socket) {this.socket = socket;} - @Override - public void run() { - try { - InputStream is = socket.getInputStream() ; - // 转成一个缓冲字符流 - Reader fr = new InputStreamReader(is); - BufferedReader br = new BufferedReader(fr); - // 一行一行的读取数据 - String line = null ; - while((line = br.readLine())!=null){ // 阻塞式的!! - System.out.println(socket.getRemoteSocketAddress() + line); - } - } catch (Exception e) { - System.out.println(socket.getRemoteSocketAddress()+"下线了"); - } - } + //编写一个handler方法,和客户端通讯 + public static void handler(Socket socket) { + try { + System.out.println("线程名字 = " + Thread.currentThread().getName()); + byte[] bytes = new byte[1024]; + //通过socket获取输入流 + InputStream inputStream = socket.getInputStream(); + int len; + //循环的读取客户端发送的数据 + while ((len = inputStream.read(bytes)) != -1) { + System.out.println("线程名字 = " + Thread.currentThread().getName()); + //输出客户端发送的数据 + System.out.println(new String(bytes, 0, read)); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + System.out.println("关闭和client的连接"); + try { + socket.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } } ``` @@ -8876,14 +8889,14 @@ class ReaderClientRunnable implements Runnable { #### 文件传输 -##### 字节流传输 +##### 字节流 客户端:本地图片: ‪E:\seazean\图片资源\beautiful.jpg 服务端:服务器路径:E:\seazean\图片服务器 UUID. randomUUID() : 方法生成随机的文件名 -**socket.shutdownOutput()**:这个必须执行,不然服务器会一直循环等待数据,最后文件损坏,程序报错。 +**socket.shutdownOutput()**:这个必须执行,不然服务器会一直循环等待数据,最后文件损坏,程序报错 ```java //常量包 @@ -8945,7 +8958,7 @@ class ServerReaderThread extends Thread{ (Constants.SERVER_DIR+UUID.randomUUID().toString()+".jpg")); byte[] buffer = new byte[1024]; int len; - while((len = bis.read(buffer))!=-1){ + while((len = bis.read(buffer)) != -1){ bos.write(buffer,0,len); } bos.close(); @@ -8965,19 +8978,19 @@ 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 { @@ -9035,21 +9048,19 @@ public class Server { **NIO的介绍**: -* Java NIO(New IO)也有人称之为 java non-blocking IO是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面**向缓冲区**的、基于**通道**的IO操作。NIO将以更加高效的方式进行文件的读写操作。NIO可以理解为非阻塞IO,传统的IO的read和write只能阻塞执行,线程在读写IO期间不能干其他事情,比如调用socket.read()时,如果服务器一直没有数据传输过来,线程就一直阻塞,而NIO中可以配置socket为非阻塞模式。 -* NIO 相关类都被放在 java.nio 包及子包下,并且对原 java.io 包中的很多类进行改写。 -* NIO 有三大核心部分:**Channel( 通道) ,Buffer( 缓冲区), Selector( 选择器)** -* Java NIO 的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 -* 通俗理解:NIO 是可以做到用一个线程来处理多个操作的。假设有 1000 个请求过来,根据实际情况,可以分配20 或者 80个线程来处理。不像之前的阻塞 IO 那样,非得分配 1000 个。 - +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 可以做到用一个线程来处理多个操作的。假设有 1000 个请求过来,根据实际情况可以分配20 或者 80个线程来处理,不像之前的阻塞 IO 那样分配 1000 个 -**NIO 和 BIO 的比较**: +NIO 和 BIO 的比较: -* BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多 +* BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多 * BIO 是阻塞的,NIO 则是非阻塞的 -* BIO 基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道 +* BIO 基于字节流和字符流进行操作,而 NIO 基于 Channel 和 Buffer 进行操作,数据从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector 用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道 | NIO | BIO | | ------------------------- | ------------------- | @@ -9063,35 +9074,34 @@ public class Server { -#### 实现原理 +#### NIO原理 -NIO三大核心部分:**Channel( 通道) ,Buffer( 缓冲区), Selector( 选择器)** +NIO 三大核心部分:**Channel( 通道) ,Buffer( 缓冲区), Selector( 选择器)** -* Buffer缓冲区 +* Buffer 缓冲区 - 缓冲区本质是一块可以写入数据、读取数据的内存。这块内存被包装成NIO Buffer对象,并且提供了相应的方法,用来操作这块内存,相比较直接对数组的操作,Buffer的API更加容易操作和管理。 + 缓冲区本质是一块可以写入数据、读取数据的内存,这块内存被包装成NIO Buffer对象,并且提供了相应的方法用来操作这块内存,相比较直接对数组的操作,Buffer 的 API 更加容易操作和管理 -* Channel通道 +* Channel 通道 - Java NIO的通道类似流,不同的是既可以从通道中读取数据,又可以写数据到通道,流的(input或output)读写通常是单向的。通道可以非阻塞读取和写入通道,支持读取或写入缓冲区,也支持异步地读写。 + Java NIO 的通道类似流,不同的是既可以从通道中读取数据,又可以写数据到通道,流的读写通常是单向的,通道可以非阻塞读取和写入通道,支持读取或写入缓冲区,也支持异步地读写。 -* Selector选择器 +* Selector 选择器 - Selector是一个Java NIO组件,能够检查一个或多个 NIO 通道,并确定哪些通道已经准备好进行读取或写入。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接,提高效率 - - + Selector 是一个 Java NIO 组件,能够检查一个或多个 NIO 通道,并确定哪些通道已经准备好进行读取或写入,这样一个单独的线程可以管理多个channel,从而管理多个网络连接,提高效率 NIO的实现框架: ![](https://gitee.com/seazean/images/raw/master/Java/NIO框架.png) -* 每个 channel 都会对应一个 Buffer -* 一个线程对应Selector , 一个Selector对应多个 channel(连接) -* 程序切换到哪个channel是由事件决定的 +* 每个 Channel 对应一个 Buffer +* 一个线程对应 Selector , 一个 Selector 对应多个 Channel(连接) +* 程序切换到哪个Channel 是由事件决定的,Event 是一个重要的概念 * Selector 会根据不同的事件,在各个通道上切换 * Buffer 就是一个内存块 , **底层是一个数组** -* 数据的读取写入是通过 Buffer完成的 , BIO 中要么是输入流,或者是输出流, 不能双向,但是 NIO 的 Buffer 是可以读也可以写。 -* Java NIO系统的核心在于:通道(Channel)和缓冲区 (Buffer)。通道表示打开到 IO 设备(例如:文件、 套接字)的连接。若需要使用 NIO 系统,获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区,然后操作缓冲区,对数据进行处理。简而言之,Channel 负责传输, Buffer 负责存取数据 +* 数据的读取写入是通过 Buffer 完成的 , BIO 中要么是输入流,或者是输出流,不能双向,NIO 的 Buffer 是可以读也可以写, flip() 切换 Buffer 的工作模式 + +Java NIO 系统的核心在于:通道和缓冲区,通道表示打开到 IO 设备(例如:文件、 套接字)的连接。若需要使用 NIO 系统,获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区,然后操作缓冲区,对数据进行处理。简而言之,Channel 负责传输, Buffer 负责存取数据 @@ -9103,79 +9113,87 @@ NIO的实现框架: ##### 基本介绍 -用于特定基本数据类型的容器。由 java.nio 包定义的,所有缓冲区都是 Buffer抽象类的子类。Java NIO 中的 Buffer 主要用于与 NIO 通道进行 交互,数据是从通道读入缓冲区,从缓冲区写入通道中的 +缓冲区(Buffer):缓冲区本质上是一个**可以读写数据的内存块**,用于特定基本数据类型的容器,用于与 NIO 通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的 -![](https://gitee.com/seazean/images/raw/master/Java/NIO-Buffer.png) +![](https://gitee.com/seazean/images/raw/master//NIO-Buffer.png) -**Buffer** 就像一个数组,可以保存多个相同类型的数据,根据数据类型不同 ,有以下 Buffer 常用子类: - ByteBuffer, CharBuffer, ShortBuffer, IntBuffer, LongBuffer, FloatBuffer, DoubleBuffer +Buffer 底层是一个数组,可以保存多个相同类型的数据,根据数据类型不同 ,有以下 Buffer 常用子类:ByteBuffer, CharBuffer, ShortBuffer, IntBuffer, LongBuffer, FloatBuffer, DoubleBuffer + + + +*** ##### 基本属性 -* 容量 (capacity) :作为一个内存块,Buffer具有一定的固定大小,也称为"容量",缓冲区容量不能为负,并且创建后不能更改。 +* 容量 (capacity):作为一个内存块,Buffer 具有固定大小,缓冲区容量不能为负,并且创建后不能更改 -* 限制 (limit):表示缓冲区中**可以操作数据**的大小(limit 后数据不能进行读写)。缓冲区的限制不能为负,并且不能大于其容量。 **写入模式,限制等于buffer的容量。读取模式下,limit等于写入的数据量**。 +* 限制 (limit):表示缓冲区中可以操作数据的大小(limit 后数据不能进行读写),缓冲区的限制不能为负,并且不能大于其容量。 **写入模式,限制等于 buffer 的容量;读取模式下,limit 等于写入的数据量** -* 位置 (position):下一个要读取或写入的数据的索引。缓冲区的位置不能为负,并且不能大于其限制 +* 位置 (position):下一个要读取或写入的数据的索引,缓冲区的位置不能为负,并且不能大于其限制 -* **标记 (mark)与重置 (reset)**:标记是一个索引,通过Buffer中的mark() 方法指定 Buffer 中一个特定的position,之后可以通过调用 reset() 方法恢复到这 个 position. +* 标记 (mark)与重置 (reset):标记是一个索引,通过Buffer中的 mark() 方法指定 Buffer 中一个特定的位置,可以通过调用 reset() 方法恢复到这个 position -* 标记、位置、限制、容量遵守以下不变式: **0 <= mark <= position <= limit <= capacity** +* 位置、限制、容量遵守以下不变式: **0 <= position <= limit <= capacity** - ![](https://gitee.com/seazean/images/raw/master/Java/NIO-Buffer操作.png) + +*** + ##### 常用API -**获取Buffer对象**: -`static XxxBuffer allocate(int capacity)` : 创建一个容量为capacity 的XxxBuffer 对象 +`static XxxBuffer allocate(int capacity)` : 创建一个容量为capacity 的 XxxBuffer 对象 -**Buffer基本操作**: +Buffer 基本操作: | 方法 | 说明 | | ------------------------------------------- | ------------------------------------------------------- | | public Buffer clear() | 清空缓冲区,不清空内容,将位置设置为零,限制设置为容量 | -| public Buffer flip() | 翻转缓冲区,将缓冲区的界限设置为当前位置,position置0 | -| public int capacity() | 返回 Buffer的capacity 大小 | -| public final boolean hasRemaining() | 判断缓冲区中是否还有元素 | -| public final int limit() | 返回Buffer的界限(limit)的位置 | -| public Buffer limit(int newLimit) | 将设置缓冲区界限为 n, 返回一个具有新 limit 的缓冲区对象 | +| public Buffer flip() | 翻转缓冲区,将缓冲区的界限设置为当前位置,position 置 0 | +| public int capacity() | 返回 Buffer的 capacity 大小 | +| public final int limit() | 返回 Buffer 的界限 limit 的位置 | +| public Buffer limit(int n) | 设置缓冲区界限为 n | | public Buffer mark() | 在此位置对缓冲区设置标记 | -| public final int position() | 返回缓冲区的当前位置position | -| public Buffer position(int n) | 将设置缓冲区的当前位置为n,并返回修改后的 Buffer 对象 | -| public final int remaining() | 返回当前位置position和limit之间的元素个数 | -| public Buffer reset() | 将位置 position 重置为先前mark标记的位置。 | +| public final int position() | 返回缓冲区的当前位置 position | +| public Buffer position(int n) | 设置缓冲区的当前位置为n | +| public Buffer reset() | 将位置 position 重置为先前 mark 标记的位置 | | public Buffer rewind() | 将位置设为为0,取消设置的 mark | +| public final int remaining() | 返回当前位置 position 和 limit 之间的元素个数 | +| public final boolean hasRemaining() | 判断缓冲区中是否还有元素 | | public static ByteBuffer wrap(byte[] array) | 将一个字节数组包装到缓冲区中 | -**Buffer数据操作**: +Buffer 数据操作: -| 方法 | 说明 | -| ------------------------------------------------- | ------------------------------------------------- | -| public abstract byte get() | 读取该缓冲区当前位置的单个字节,然后增加位置 | -| public ByteBuffer get(byte[] dst) | 读取多个字节到字节数组dst中 | -| public abstract byte get(int index) | 读取指定索引位置的字节(不会移动 position) | -| public abstract ByteBuffer put(byte b) | 将给定单个字节写入缓冲区的当前位置,然后增加位置 | -| public final ByteBuffer put(byte[] src) | 将src 字节数组写入缓冲区的当前位置 | -| public abstract ByteBuffer put(int index, byte b) | 将指定字节写入缓冲区的索引位置(不会移动 position) | +| 方法 | 说明 | +| ------------------------------------------------- | ----------------------------------------------- | +| public abstract byte get() | 读取该缓冲区当前位置的单个字节,然后增加位置 | +| public ByteBuffer get(byte[] dst) | 读取多个字节到字节数组dst中 | +| public abstract byte get(int index) | 读取指定索引位置的字节,不移动 position | +| public abstract ByteBuffer put(byte b) | 将给定单个字节写入缓冲区的当前位置,position+1 | +| public final ByteBuffer put(byte[] src) | 将 src 字节数组写入缓冲区的当前位置 | +| public abstract ByteBuffer put(int index, byte b) | 将指定字节写入缓冲区的索引位置,不移动 position | 提示:"\n",占用两个字节 +**** + + + ##### 读写数据 使用Buffer读写数据一般遵循以下四个步骤: -* 1.写入数据到Buffer -* 2.调用flip()方法,转换为读取模式 -* 3.从Buffer中读取数据 -* 4.调用buffer.clear()方法或者buffer.compact()方法清除缓冲区 +* 写入数据到 Buffer +* 调用 flip()方法,转换为读取模式 +* 从 Buffer 中读取数据 +* 调用 buffer.clear() 方法清除缓冲区 ```java public class TestBuffer { @@ -9208,19 +9226,10 @@ public class TestBuffer { buffer.get(dst); System.out.println(dst.length); System.out.println(new String(dst, 0, dst.length)); - System.out.println("-----------------get()----------------"); System.out.println(buffer.position());//7 System.out.println(buffer.limit());//7 - System.out.println(buffer.capacity());//1024 - - //5. rewind() : 可重复读 - buffer.rewind(); - System.out.println("-----------------rewind()----------------"); - System.out.println(buffer.position());//0 - System.out.println(buffer.limit());//7 - System.out.println(buffer.capacity());//1024 - - //6. clear() : 清空缓冲区. 但是缓冲区中的数据依然存在,但是处于“被遗忘”状态 + + //5. clear() : 清空缓冲区. 但是缓冲区中的数据依然存在,但是处于“被遗忘”状态 System.out.println(buffer.hasRemaining());//true buffer.clear(); System.out.println(buffer.hasRemaining());//true @@ -9234,20 +9243,22 @@ public class TestBuffer { +**** + ##### 直接内存 -`byte byffer`可以是两种类型,一种是基于直接内存(也就是非堆内存);另一种是非直接内存(也就是堆内存)。对于直接内存来说,JVM将会在IO操作上具有更高的性能,因为它直接作用于本地系统的IO操作;而非直接内存,也就是堆内存中的数据,如果要作IO操作,会先从本进程内存复制到直接内存,再利用本地IO处理 +`byte buffer` 可以是两种类型,一种是基于直接内存(也就是非堆内存),另一种是非直接内存(也就是堆内存)。对于直接内存来说,JVM将会在IO操作上具有更高的性能,因为直接作用于本地系统的IO操作,而非直接内存,也就是堆内存中的数据,如果要作IO操作,会先从本进程内存复制到直接内存,再利用本地IO处理 直接内存创建Buffer对象:`static XxxBuffer allocateDirect(int capacity)` 数据流的角度: * 非直接内存的作用链:本地IO-->直接内存-->非直接内存-->直接内存-->本地IO -* 直接内存是:本地IO-->直接内存-->本地IO +* 直接内存是:本地IO → 直接内存 → 本地IO -JVM内存结构详解直接内存 +JVM 内存结构详解直接内存 @@ -9257,62 +9268,56 @@ JVM内存结构详解直接内存 #### 通道 -##### 概述 +##### 基本介绍 -通道(Channel):由 java.nio.channels 包定义 的。Channel 表示 IO 源与目标打开的连接。 Channel 类似于传统的“流”。只不过 Channel 本身不能直接访问数据,Channel 只能与 Buffer 进行**交互**。 +通道(Channel):表示 IO 源与目标打开的连接,Channel 类似于传统的流,只不过 Channel 本身不能直接访问数据,Channel 只能与 Buffer **进行交互** -1、 NIO 的通道类似于流,但有些区别如下: +1. NIO 的通道类似于流,但有些区别如下: + * 通道可以同时进行读写,而流只能读或者只能写 + * 通道可以实现异步读写数据 + * 通道可以从缓冲读数据,也可以写数据到缓冲 -* 通道可以同时进行读写,而流只能读或者只能写 +2. BIO 中的 stream 是单向的,NIO中的 Channel 是双向的,可以读操作,也可以写操作 -* 通道可以实现异步读写数据 +3. Channel 在 NIO 中是一个接口:`public interface Channel extends Closeable{}` -* 通道可以从缓冲读数据,也可以写数据到缓冲: -2、BIO 中的 stream 是单向的,NIO中的通道(Channel)是双向的,可以读操作,也可以写操作。 -3、Channel 在 NIO 中是一个接口: `public interface Channel extends Closeable{}` +Channel 实现类: +* FileChannel:用于读取、写入、映射和操作文件的通道 +* DatagramChannel:通过 UDP 读写网络中的数据通道 +* SocketChannel:通过 TCP 读写网络中的数据 +* ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个SocketChannel。 + 提示:ServerSocketChanne 类似 ServerSocket , SocketChannel 类似 Socket -**Channel实现类**: -* FileChannel:用于读取、写入、映射和操作文件的通道。 -* DatagramChannel:通过 UDP 读写网络中的数据通道。 -* SocketChannel:通过 TCP 读写网络中的数据。 -* ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个SocketChannel。 - 提示:ServerSocketChanne 类似 ServerSocket , SocketChannel 类似 Socket +*** -**FileChannel类:** -* FileInputStream -* FileOutputStream -* RandomAccessFile -* DatagramSocket -* Socket -* ServerSocket +##### 常用API +获取 Channel 方式: -##### 常用API +* 对支持通道的对象调用 `getChannel()` 方法 +* 通过通道的静态方法 `open()` 打开并返回指定通道 +* 使用 Files 类的静态方法 `newByteChannel()` 获取字节通道 -获取通道方式: +Channel 基本操作: -* 对支持通道的对象调用getChannel()方法 -* 通过通道的静态方法 open() 打开并返回指定通道 -* 使用 Files 类的静态方法 newByteChannel() 获取字节通道。 +| 方法 | 说明 | +| ------------------------------------------ | -------------------------------------------------------- | +| public abstract int read(ByteBuffer dst) | 从 Channel 中读取数据到 ByteBuffer,从 position 开始储存 | +| public final long read(ByteBuffer[] dsts) | 将Channel中的数据“分散”到ByteBuffer[] | +| public abstract int write(ByteBuffer src) | 将 ByteBuffer 中的数据写入 Channel,从 position 开始写出 | +| public final long write(ByteBuffer[] srcs) | 将ByteBuffer[]到中的数据“聚集”到Channel | +| public abstract long position() | 返回此通道的文件位置 | +| FileChannel position(long newPosition) | 设置此通道的文件位置 | +| public abstract long size() | 返回此通道的文件的当前大小 | -| 方法 | 说明 | -| ------------------------------------------ | ------------------------------------------------------- | -| public abstract int read(ByteBuffer dst) | 从Channel中读取数据到ByteBuffer,从position位置开始储存 | -| public final long read(ByteBuffer[] dsts) | 将Channel中的数据“分散”到ByteBuffer[] | -| public abstract int write(ByteBuffer src) | 将ByteBuffer中的数据写入Channel,从position位置开始写出 | -| public final long write(ByteBuffer[] srcs) | 将ByteBuffer[]到中的数据“聚集”到Channel | -| public abstract long position() | 返回此通道的文件位置 | -| FileChannel position(long newPosition) | 设置此通道的文件位置 | -| public abstract long size() | 返回此通道的文件的当前大小 | -| FileChannel truncate(long size) | 将此通道的文件截取为给定大小 | -| void force(boolean metaData) | 强制将所有对此通道的文件更新写入到存储设备中 | +**读写都是相对于内存来看,也就是缓冲区** @@ -9355,24 +9360,28 @@ public class ChannelTest { -##### 文件复制 +*** -方式一:Buffer -方式二:transferFrom transferTo -* public abstract long transferFrom(ReadableByteChannel src, long position, long count):从给定的可读字节通道将字节传输到该通道的文件中 +##### 文件复制 + +Channel 的两个方法: - * src - 源通道 - * position - 文件中要进行传输的位置; 必须是非负的 - * count - 要传输的最大字节数; 必须是非负的 +* `abstract long transferFrom(ReadableByteChannel src, long position, long count)`:从给定的可读字节通道将字节传输到该通道的文件中 + * src:源通道 + * position:文件中要进行传输的位置,必须是非负的 + * count:要传输的最大字节数,必须是非负的 -* public abstract long transferTo(long position, long count, WritableByteChannel target):将该通道文件的字节传输到给定的可写字节通道。 +* `abstract long transferTo(long position, long count, WritableByteChannel target)`:将该通道文件的字节传输到给定的可写字节通道。 + * position:传输开始的文件中的位置; 必须是非负的 + * count:要传输的最大字节数; 必须是非负的 + * target:目标通道 - * position - 传输开始的文件中的位置; 必须是非负的 - * count - 要传输的最大字节数; 必须是非负的 +文件复制的两种方式: - * target - 目标通道 +1. Buffer +2. 使用上述两种方法 ```java public class ChannelTest { @@ -9408,7 +9417,7 @@ public class ChannelTest { } @Test - public void test02() throws Exception { + public void copy02() throws Exception { // 1、字节输入管道 FileInputStream fis = new FileInputStream("data01.txt"); FileChannel isChannel = fis.getChannel(); @@ -9422,7 +9431,7 @@ public class ChannelTest { } @Test - public void test02() throws Exception { + public void copy03() throws Exception { // 1、字节输入管道 FileInputStream fis = new FileInputStream("data01.txt"); FileChannel isChannel = fis.getChannel(); @@ -9439,6 +9448,10 @@ public class ChannelTest { +*** + + + ##### 分散聚集 分散读取(Scatter ):是指把Channel通道的数据读入到多个缓冲区中去 @@ -9483,28 +9496,29 @@ public class ChannelTest { #### 选择器 -##### 选择器概述 - -选择器(Selector) 是 SelectableChannle 对象的**多路复用器**,Selector 可以同时监控多个 SelectableChannel 的 IO 状况,利用 Selector可使一个单独的线程管理多个 Channel。**Selector 是非阻塞 IO 的核心**。 - -![](https://gitee.com/seazean/images/raw/master/Java/NIO-Selector.png) +##### 基本介绍 +选择器(Selector) 是 SelectableChannle 对象的**多路复用器**,Selector 可以同时监控多个通道的状况,利用 Selector 可使一个单独的线程管理多个 Channel。**Selector 是非阻塞 IO 的核心**。 +![](https://gitee.com/seazean/images/raw/master//NIO-Selector.png) -* Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到 Selector(选择器) -* Selector 能够检测多个注册的通道上是否有事件发生(注意:多个 Channel 以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求 +* Selector 能够检测多个注册的通道上是否有事件发生(多个 Channel 以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理,就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求 * 只有在连接/通道真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程 * 避免了多线程之间的上下文切换导致的开销 -##### 选择器用法 +*** + -创建 Selector(调用静态方法) :`Selector selector = Selector.open();` + +##### 常用API + +创建 Selector:`Selector selector = Selector.open();` 向选择器注册通道:`SelectableChannel.register(Selector sel, int ops)` -将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数 ops 指定。监听的事件类型用SelectionKey的四个常量表示: +将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数 ops 指定。监听的事件类型用四个常量表示: * 读 : SelectionKey.OP_READ (1) * 写 : SelectionKey.OP_WRITE (4) @@ -9517,14 +9531,17 @@ public class ChannelTest { **Selector API**: -| 方法 | 说明 | -| ------------------------------------------------ | ----------------------------------------- | -| public static Selector open() | 打开选择器 | -| public abstract void close() | 关闭此选择器 | -| public abstract int select() | 选择一组其相应通道准备好进行I / O操作的键 | -| public abstract Set selectedKeys() | 返回此选择器的选择键集 | +| 方法 | 说明 | +| ------------------------------------------------ | ------------------------------------- | +| public static Selector open() | 打开选择器 | +| public abstract void close() | 关闭此选择器 | +| public abstract int select() | 阻塞选择一组通道准备好进行I/O操作的键 | +| public abstract int select(long timeout) | 阻塞等待 timeout 毫秒 | +| public abstract int selectNow() | 获取一下,不阻塞,立刻返回 | +| public abstract Selector wakeup() | 唤醒正在阻塞的 selector | +| public abstract Set selectedKeys() | 返回此选择器的选择键集 | -**SelectionKey API**: +SelectionKey API: | 方法 | 说明 | | ------------------------------------------- | -------------------------------------------------- | @@ -9535,8 +9552,6 @@ public class ChannelTest { | public final boolean isReadable() | 检测此密钥的频道是否可以阅读 | | public final boolean isWritable() | 检测此密钥的通道是否准备好进行写入 | - - 基本步骤: ```java @@ -9548,7 +9563,7 @@ ssChannel.configureBlocking(false); ssChannel.bin(new InetSocketAddress(9999)); //4.获取选择器 Selector selector = Selector.open(); -//5.将通道注册到选择器上,并且指定“监听接收事件” +//5.将通道注册到选择器上,并且指定“监听接收事件” ssChannel.register(selector, SelectionKey.OP_ACCEPT); ``` @@ -9560,28 +9575,27 @@ ssChannel.register(selector, SelectionKey.OP_ACCEPT); #### NIO实现 -##### 实现流程 +##### 常用API -* **SelectableChannel_API** +* SelectableChannel_API - | 方法 | 说明 | - | ------------------------------------------------------------ | ------------------------ | - | public final SelectableChannel configureBlocking(boolean block) | 设置此通道的阻塞模式 | - | public final SelectionKey register(Selector sel, int ops) | 向给定的选择器注册此通道 | + | 方法 | 说明 | + | ------------------------------------------------------------ | -------------------------------------------- | + | public final SelectableChannel configureBlocking(boolean block) | 设置此通道的阻塞模式 | + | public final SelectionKey register(Selector sel, int ops) | 向给定的选择器注册此通道,并选择感兴趣的事件 | -* **SocketChannel_API**: +* SocketChannel_API: - | 方法 | 说明 | - | :---------------------------------------------------------- | ------------------------------ | - | public static SocketChannel open() | 打开套接字通道 | - | public static SocketChannel open(SocketAddress remote) | 打开套接字通道并连接到远程地址 | - | public abstract boolean connect(SocketAddress remote) | 连接此通道的到远程地址 | - | public abstract SocketChannel bind(SocketAddress local) | 将通道的套接字绑定到本地地址 | - | public abstract SocketAddress getLocalAddress() | 返回套接字绑定的本地套接字地址 | - | public abstract SocketAddress getRemoteAddress() | 返回套接字连接的远程套接字地址 | - | abstract SelectableChannel configureBlocking(boolean block) | 调整此通道的阻塞模式 | + | 方法 | 说明 | + | :------------------------------------------------------ | ------------------------------ | + | public static SocketChannel open() | 打开套接字通道 | + | public static SocketChannel open(SocketAddress remote) | 打开套接字通道并连接到远程地址 | + | public abstract boolean connect(SocketAddress remote) | 连接此通道的到远程地址 | + | public abstract SocketChannel bind(SocketAddress local) | 将通道的套接字绑定到本地地址 | + | public abstract SocketAddress getLocalAddress() | 返回套接字绑定的本地套接字地址 | + | public abstract SocketAddress getRemoteAddress() | 返回套接字连接的远程套接字地址 | -* **ServerSocketChannel_API**: +* ServerSocketChannel_API: | 方法 | 说明 | | ---------------------------------------------------------- | ------------------------------------------------------------ | @@ -9589,11 +9603,20 @@ ssChannel.register(selector, SelectionKey.OP_ACCEPT); | public final ServerSocketChannel bind(SocketAddress local) | 将通道的套接字绑定到本地地址,并配置套接字以监听连接 | | public abstract SocketChannel accept() | 接受与此通道套接字的连接,通过此方法返回的套接字通道将处于阻塞模式 | + * 如果 ServerSocketChannel 处于非阻塞模式,如果没有挂起连接,则此方法将立即返回 null + * 如果通道处于阻塞模式,如果没有挂起连接将无限期地阻塞,直到有新的连接或发生I / O错误 + -服务端 +*** + + + +##### 代码实现 + +服务端 : -1. 获取通道,当客户端连接服务端时,服务端会通过**`ServerSocketChannel.accept`**得到 SocketChannel +1. 获取通道,当客户端连接服务端时,服务端会通过 `ServerSocketChannel.accept` 得到 SocketChannel 2. 切换非阻塞模式 @@ -9605,20 +9628,14 @@ ssChannel.register(selector, SelectionKey.OP_ACCEPT); 6. 轮询式的获取选择器上已经“准备就绪”的事件 - - -客户端 +客户端: -1. 获取通道: `SocketChannel sc = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999));` +1. 获取通道:`SocketChannel sc = SocketChannel.open(new InetSocketAddress(HOST, PORT))` 2. 切换非阻塞模式 -3. 分配指定大小的缓冲区: `ByteBuffer buffer = ByteBuffer.allocate(1024);` +3. 分配指定大小的缓冲区:`ByteBuffer buffer = ByteBuffer.allocate(1024)` 4. 发送数据给服务端 - - -##### 代码实现 - -37行代码,如果判断条件改为 !=-1,需要客户端shutdown一下 +37行代码,如果判断条件改为 !=-1,需要客户端 shutdown 一下 ```java public class Server { @@ -9664,6 +9681,7 @@ public class Server { buffer.clear();// 清除之前的数据 } } + //删除当前的 selectionKey,防止重复操作 it.remove(); } } @@ -9675,8 +9693,7 @@ public class Server { public class Client { public static void main(String[] args) throws Exception { // 1、获取通道 - SocketChannel socketChannel = SocketChannel.open(new - InetSocketAddress("127.0.0.1", 9999)); + SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999)); // 2、切换成非阻塞模式 socketChannel.configureBlocking(false); // 3、分配指定缓冲区大小 @@ -9686,7 +9703,7 @@ public class Client { while (true){ System.out.print("请说:"); String msg = sc.nextLine(); - buffer.put(("波妞:"+msg).getBytes()); + buffer.put(("波妞:" + msg).getBytes()); buffer.flip(); socketChannel.write(buffer); buffer.clear(); @@ -9697,257 +9714,13 @@ public class Client { -*** - - - -#### NIO群聊 - -需求:进一步理解 NIO非阻塞网络编程机制,实现多人群聊 - -* 编写一个 NIO 群聊系统,实现客户端与客户端的通信需求(非阻塞) -* 服务器端:可以监测用户上线,离线,并实现消息转发功能 -* 客户端:通过channel可以无阻塞发送消息给其它所有客户端用户,同时可以接受其它客户端用户通过服务端转发来的消息 - -```java -public class ChatServer { - //定义属性 - private Selector selector; - private ServerSocketChannel listenChannel; - private static final int PORT = 6667; - //构造器 - //初始化工作 - public ChatServer() { - try { - //得到选择器 - selector = Selector.open(); - //ServerSocketChannel - listenChannel = ServerSocketChannel.open(); - //绑定端口 - listenChannel.socket().bind(new InetSocketAddress(PORT)); - //设置非阻塞模式 - listenChannel.configureBlocking(false); - //将该listenChannel 注册到selector - listenChannel.register(selector, SelectionKey.OP_ACCEPT); - }catch (IOException e) { - e.printStackTrace(); - } - - } - - //监听 - public void listen() { - System.out.println("Listening thread:" + Thread.currentThread().getName()); - try { - //循环处理 - while (true) { - int count = selector.select(); - if(count > 0) {//有事件处理 - //遍历得到selectionKey 集合 - Iterator keyIterator = selector.selectedKeys().iterator(); - while (keyIterator.hasNext()) { - //取出selectionKey - SelectionKey selectionKey = keyIterator.next(); - - //监听到accept - if(selectionKey.isAcceptable()) { - SocketChannel socketChannel = listenChannel.accept(); - socketChannel.configureBlocking(false); - //将该 sc 注册到selector - socketChannel.register(selector, SelectionKey.OP_READ); - //提示 - System.out.println(socketChannel.getRemoteAddress() + "is online..."); - } - //通道发送read事件,即通道是可读的状态 - if(selectionKey.isReadable()) { - //处理读 (专门写方法..) - readData(selectionKey); - } - //当前的key 删除,防止重复处理 - keyIterator.remove(); - } - } else { - System.out.println("waiting...."); - } - } - - }catch (Exception e) { - e.printStackTrace(); - - }finally { - //发生异常处理.... - } - } - - //读取客户端消息 - private void readData(SelectionKey selectionKey) { - //取到关联的channel - SocketChannel channel = null; - try { - //得到channel - channel = (SocketChannel) selectionKey.channel(); - //创建buffer - ByteBuffer buffer = ByteBuffer.allocate(1024); - - int len = channel.read(buffer); - //根据count的值做处理 - if(len > 0) { - //把缓存区的数据转成字符串 - String msg = new String(buffer.array(), 0, len); - //输出该消息 - System.out.println("form Client:" + msg); - //向其它的客户端转发消息(去掉自己), 专门写一个方法来处理 - sendInfoToOtherClients(msg, channel); - } - - }catch (IOException e) { - try { - System.out.println(channel.getRemoteAddress() + "is offline..."); - //取消注册 - selectionKey.cancel(); - //关闭通道 - channel.close(); - }catch (IOException e2) { - e2.printStackTrace();; - } - } - } - - //转发消息给其它客户(通道) - private void sendInfoToOtherClients(String msg, SocketChannel selfChannel ) throws IOException{ - - System.out.println("Server forwarding message..."); - System.out.println("Server forwarding data thread:" + Thread.currentThread().getName()); - //遍历 所有注册到selector 上的 SocketChannel,并排除 self - for(SelectionKey selectionKey: selector.keys()) { - - //通过 key 取出对应的 SocketChannel - Channel targetChannel = selectionKey.channel(); - - //排除自己 - if(targetChannel instanceof SocketChannel && targetChannel != selfChannel) { - - //转型 - SocketChannel socketChannel = (SocketChannel)targetChannel; - //将msg 存储到buffer - //ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes()); - ByteBuffer buffer = ByteBuffer.allocate(1024); - buffer.put(msg.getBytes()); - buffer.flip(); - //将buffer 的数据写入 通道 - socketChannel.write(buffer); - } - } - - } - - public static void main(String[] args) { - //创建服务器对象 - ChatServer groupChatServer = new ChatServer(); - groupChatServer.listen(); - } -} -``` - - - -```java -public class ChatClient { - - //定义相关的属性 - private final String HOST = "127.0.0.1"; // 服务器的ip - private final int PORT = 6667; //服务器端口 - private Selector selector; - private SocketChannel socketChannel; - private String username; - - //构造器, 完成初始化工作 - public ChatClient() throws IOException { - - selector = Selector.open(); - //连接服务器 - socketChannel = socketChannel.open(new InetSocketAddress("127.0.0.1", PORT)); - //设置非阻塞 - socketChannel.configureBlocking(false); - //将channel 注册到selector - socketChannel.register(selector, SelectionKey.OP_READ); - //得到username - //socketChannel.getLocalAddress().toString(); /127.0.0.1:8182 - username = socketChannel.getLocalAddress().toString().substring(1); - System.out.println(username + " is ok..."); - - } - - //向服务器发送消息 - public void sendInfo(String info) { - info = username + ":" + info; - try { - socketChannel.write(ByteBuffer.wrap(info.getBytes())); - }catch (IOException e) { - e.printStackTrace(); - } - } +**** - //读取从服务器端回复的消息 - public void readInfo() { - try { - int readChannels = selector.select(); - if(readChannels > 0) {//有可以用的通道 - - Iterator iterator = selector.selectedKeys().iterator(); - while (iterator.hasNext()) { - - SelectionKey key = iterator.next(); - if(key.isReadable()) { - //得到相关的通道 - SocketChannel sc = (SocketChannel) key.channel(); - //得到一个Buffer - ByteBuffer buffer = ByteBuffer.allocate(1024); - //读取 - sc.read(buffer); - buffer.flip(); - //把读到的缓冲区的数据转成字符串 - String msg = new String(buffer.array(),0,buffer.remaining()); - System.out.println(msg.trim()); - } - iterator.remove(); //删除当前的selectionKey, 防止重复操作 - } - } else { - //System.out.println("没有可以用的通道..."); - } - }catch (Exception e) { - e.printStackTrace(); - } - } - public static void main(String[] args) throws Exception { - //启动我们客户端 - ChatClient chatClient = new ChatClient(); - //启动一个线程, 每2秒,读取从服务器发送数据 - new Thread() { - @Override - public void run() { - while (true) { - chatClient.readInfo(); - try { - Thread.currentThread().sleep(2000); - }catch (InterruptedException e) { - e.printStackTrace(); - } - } - } - }.start(); - //发送数据给服务器端 - Scanner scanner = new Scanner(System.in); - while (scanner.hasNextLine()) { - String s = scanner.nextLine(); - chatClient.sendInfo(s); - } - } -} +#### 零拷贝 -``` +(待更新) @@ -9957,7 +9730,7 @@ public class ChatClient { ### AIO -Java AIO(NIO.2) : 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。 +Java AIO(NIO.2) : AsynchronousI/O,异步非阻塞,采用了 Proactor 模式。服务器实现模式为一个有效请求一个线程,客户端的 I/O 请求都是由 OS 先完成了再通知服务器应用去启动线程进行处理。 ```java AIO异步非阻塞,基于NIO的,可以称之为NIO2.0 @@ -9966,10 +9739,13 @@ Socket SocketChannel AsynchronousSocketChannel ServerSocket ServerSocketChannel AsynchronousServerSocketChannel ``` -与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可, 这两种方法均为异步的。对于读操作,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区;对于写操作,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。 +当进行读写操作时,调用 API 的 read 或 write 方法,这两种方法均为异步的,完成后会主动调用回调函数: -在JDK1.7中,这部分内容被称作NIO.2,主要在Java.nio.channels包下增加了下面四个异步通道: -AsynchronousSocketChannel, AsynchronousServerSocketChannel, AsynchronousFileChannel, AsynchronousDatagramChannel +* 对于读操作,当有流可读取时,操作系统会将可读的流传入 read 方法的缓冲区 +* 对于写操作,当操作系统将 write 方法传递的流写入完毕时,操作系统主动通知应用程序 + +在JDK1.7中,这部分内容被称作NIO.2,主要在 Java.nio.channels 包下增加了下面四个异步通道: +AsynchronousSocketChannel、AsynchronousServerSocketChannel、AsynchronousFileChannel、AsynchronousDatagramChannel @@ -10023,23 +9799,24 @@ Junit框架的使用步骤: 测试方法注意事项:**必须是public修饰的,没有返回值,没有参数,使用注解@Test修饰** -Junit常用注解(Junit 4.xxxx版本) - @Test 测试方法! - @Before:用来修饰实例方法,该方法会在每一个测试方法执行之前执行一次。 - @After:用来修饰实例方法,该方法会在每一个测试方法执行之后执行一次。 - @BeforeClass:用来静态修饰方法,该方法会在所有测试方法之前**只**执行一次。 - @AfterClass:用来静态修饰方法,该方法会在所有测试方法之后**只**执行一次。 +Junit常用注解(Junit 4.xxxx版本),@Test 测试方法: + +* @Before:用来修饰实例方法,该方法会在每一个测试方法执行之前执行一次 +* @After:用来修饰实例方法,该方法会在每一个测试方法执行之后执行一次 +* @BeforeClass:用来静态修饰方法,该方法会在所有测试方法之前**只**执行一次 +* @AfterClass:用来静态修饰方法,该方法会在所有测试方法之后**只**执行一次 -Junit常用注解(Junit5.xxxx版本) - @Test 测试方法! - @BeforeEach:用来修饰实例方法,该方法会在每一个测试方法执行之前执行一次。 - @AfterEach:用来修饰实例方法,该方法会在每一个测试方法执行之后执行一次。 - @BeforeAll:用来静态修饰方法,该方法会在所有测试方法之前只执行一次。 - @AfterAll:用来静态修饰方法,该方法会在所有测试方法之后只执行一次。 +Junit常用注解(Junit5.xxxx版本),@Test 测试方法: + +* @BeforeEach:用来修饰实例方法,该方法会在每一个测试方法执行之前执行一次 +* @AfterEach:用来修饰实例方法,该方法会在每一个测试方法执行之后执行一次 +* @BeforeAll:用来静态修饰方法,该方法会在所有测试方法之前只执行一次 +* @AfterAll:用来静态修饰方法,该方法会在所有测试方法之后只执行一次 作用: - 开始执行的方法:初始化资源 - 执行完之后的方法:释放资源 + +* 开始执行的方法:初始化资源 +* 执行完之后的方法:释放资源 ```java public class UserService { @@ -10116,7 +9893,7 @@ public class UserServiceTest { 核心思想:在运行时获取类编译后的字节码文件对象,然后解析类中的全部成分。 -反射提供了一个Class类型:HelloWorld.java -> javac -> HelloWorld.class +反射提供了一个Class类型:HelloWorld.java → javac → HelloWorld.class * `Class c = HelloWorld.class;` @@ -12146,10 +11923,12 @@ public class Demo1_27 { -*** +参考文章:https://juejin.cn/post/6844904182483271694 +*** + ### 变量位置 @@ -12266,7 +12045,7 @@ public class Demo1_27 { 晋升到老年代: * **长期存活的对象进入老年代**:为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中 - `-XX:MaxTenuringThreshold`:定义年龄的阈值,在JVM中用4个bit存储(放在对象头中),所以其最大值是15,默认也是15 + `-XX:MaxTenuringThreshold`:定义年龄的阈值,对象头中用4个bit存储,所以最大值是15,默认也是15 * **大对象直接进入老年代**:需要连续内存空间的对象,最典型的大对象是很长的字符串以及数组;避免在 Eden 和 Survivor 之间的大量复制;经常出现大对象会提前触发GC以获取足够的连续空间分配给大对象 `-XX:PretenureSizeThreshold`:大于此值的对象直接在老年代分配 * **动态对象年龄判定**:如果在Survivor区中相同年龄的对象的所有大小之和超过Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代 @@ -15776,6 +15555,10 @@ JDK 自带了**监控工具,位于 JDK 的 bin 目录下**,其中最常用 +*** + + + ### 参数调优 对于JVM调优,主要就是调整年轻代、老年代、元空间的内存空间大小及使用的垃圾回收器类型 @@ -17147,12 +16930,13 @@ class BigRoom { 死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放,由于线程被无限期地阻塞,因此程序不可能正常终止 java 死锁产生的四个必要条件: - 1、互斥条件,即当资源被一个线程使用(占有)时,别的线程不能使用。 - 2、不剥夺条件,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。 - 3、请求和保持条件,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。 - 4、循环等待条件,即存在一个等待循环队列:p1要p2的资源,p2要p1的资源,形成了一个等待环路 -四个条件都成立的时候,便形成死锁。死锁情况下打破上述任何一个条件,便可让死锁消失。 +1. 互斥条件,即当资源被一个线程使用(占有)时,别的线程不能使用 +2. 不剥夺条件,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放 +3. 请求和保持条件,即当资源请求者在请求其他的资源的同时保持对原有资源的占有 +4. 循环等待条件,即存在一个等待循环队列:p1要p2的资源,p2要p1的资源,形成了一个等待环路 + +四个条件都成立的时候,便形成死锁。死锁情况下打破上述任何一个条件,便可让死锁消失 ```java public class Dead { @@ -17264,7 +17048,7 @@ class HoldLockThread implements Runnable { * 避免死锁:避免死锁要注意加锁顺序 -* 也可以使用 jconsole工具,在jdk\bin目录下 +* 也可以使用 jconsole 工具,在 `jdk\bin` 目录下 @@ -17338,7 +17122,7 @@ public final native void wait(long timeout):有时限的等待, 到n毫秒后结 * 原理不同:sleep()方法是属于Thread类,是线程用来控制自身流程的,使此线程暂停执行一段时间而把执行机会让给其他线程;wait()方法属于Object类,用于线程间通信 * 对锁的处理机制不同:调用sleep()方法的过程中,线程不会释放对象锁,当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,但是都会释放CPU -* 使用区域不同:wait()方法必须放在**同步控制方法和同步代码块(先获取锁)**中使用,sleep()方法则可以放在任何地方使用 +* 使用区域不同:wait()方法必须放在**同步控制方法和同步代码块(先获取锁)**中使用,sleep() 方法则可以放在任何地方使用 底层原理: @@ -17474,7 +17258,7 @@ public static void main(String[] args) { * 先park: 1. 当前线程调用 Unsafe.park() 方法 2. 检查 _counter ,本情况为 0,这时获得 _mutex 互斥锁 - 3. 线程进入 _cond 条件变量阻塞,设置 _counter = 0 + 3. 线程进入 _cond 条件变量阻塞 4. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1 5. 唤醒 _cond 条件变量中的 Thread_0,Thread_0 恢复运行,设置 _counter 为 0 @@ -18111,7 +17895,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操作) @@ -18215,7 +17999,7 @@ Linux查看CPU缓存行: * 内存地址格式:[高位组标记] [低位索引] [偏移量] -缓存行在 无锁 --> LongAddr --> 伪共享部分详解 +缓存行在 无锁 → LongAddr → 伪共享部分详解 @@ -18227,10 +18011,10 @@ Linux查看CPU缓存行: 单核 CPU 处理器会自动保证基本内存操作的原子性 -多核 CPU 处理器,每个 CPU 处理器内维护了一块字内存,每个内核内部维护着一块缓存,当多线程并发读写时,就会出现缓存数据不一致的情况。处理器提供: +多核 CPU 处理器,每个 CPU 处理器内维护了一块内存,每个内核内部维护着一块缓存,当多线程并发读写时,就会出现缓存数据不一致的情况。处理器提供: -* 总线锁定:当处理器要操作共享变量时,在 BUS 总线上发出一个 LOCK 信号,其他处理就无法操作这个共享变量,该操作会导致大量阻塞,从而增加系统的性能开销 -* 缓存锁定:当处理器对缓存中的共享变量进行了操作,其他处理器有嗅探机制,将其他处理器的该共享变量的缓存失效,其他线程读取时会重新从主内存中读取最新的数据,基于 MESI 缓存一致性协议来实现 +* 总线锁定:当处理器要操作共享变量时,在 BUS 总线上发出一个 LOCK 信号,其他处理器就无法操作这个共享变量,该操作会导致大量阻塞,从而增加系统的性能开销 +* 缓存锁定:当处理器对缓存中的共享变量进行了操作,其他处理器有嗅探机制,将该共享变量的缓存失效,其他线程读取时会重新从主内存中读取最新的数据,基于 MESI 缓存一致性协议来实现 有如下两种情况处理器不会使用缓存锁定: @@ -18240,7 +18024,7 @@ Linux查看CPU缓存行: 总线机制: -* 总线嗅探:每个处理器通过嗅探在总线上传播的数据来检查自己缓存值是否过期了,当处理器发现自己的缓存对应的内存地址被修改,就将当前处理器的缓存行设置为无效状态,当处理器对这个数据进行操作时,会重新从内存中把数据读取到处理器缓存中 +* 总线嗅探:每个处理器通过嗅探在总线上传播的数据来检查自己缓存值是否过期了,当处理器发现自己的缓存对应的内存地址数据被修改,就将当前处理器的缓存行设置为无效状态,当处理器对这个数据进行操作时,会重新从内存中把数据读取到处理器缓存中 * 总线风暴:由于 volatile 的 MESI 缓存一致性协议,需要不断的从主内存嗅探和 CAS 循环,无效的交互会导致总线带宽达到峰值;因此不要大量使用 volatile 关键字,使用 volatile、syschonized 都需要根据实际场景 @@ -18326,7 +18110,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) @@ -18341,7 +18125,7 @@ lock前缀指令就相当于内存屏障,Memory Barrier(Memory Fence) 保证可见性: -* 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中 +* 写屏障(sfence,Store Barrier)保证在该屏障之前的,对共享变量的改动,都同步到主存当中 ```java public void actor2(I_Result r) { @@ -18351,7 +18135,7 @@ lock前缀指令就相当于内存屏障,Memory Barrier(Memory Fence) } ``` -* 读屏障(lfence)保证在该屏障之后的,对共享变量的读取,加载的是主存中最新数据 +* 读屏障(lfence,Load Barrier)保证在该屏障之后的,对共享变量的读取,加载的是主存中最新数据 ```java public void actor1(I_Result r) { @@ -18367,6 +18151,8 @@ lock前缀指令就相当于内存屏障,Memory Barrier(Memory Fence) +* 全能屏障:mfence(modify/mix Barrier),兼具sfence和lfence的功能 + 保证有序性: * 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后 @@ -19054,9 +18840,7 @@ CPU三层缓存结构: -CPU 与 内存的速度差异很大,需要靠预读数据至缓存来提升效率,而**缓存以缓存行为单位,每个缓存行对应着一块内存**,一般是 64 byte(8 个 long) - -缓存会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中,CPU 要保证数据的一致性,需要做到某个 CPU 核心更改了数据,其它 CPU 核心对应的**整个缓存行必须失效** +CPU 与 内存的速度差异很大,需要靠预读数据至缓存来提升效率,而**缓存以缓存行为单位,每个缓存行对应着一块内存**,一般是 64 byte(8 个 long)。缓存会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中,CPU 要保证数据的一致性,需要做到某个 CPU 核心更改了数据,其它 CPU 核心对应的**整个缓存行必须失效**,这就是伪共享 Cell 是数组形式,**在内存中是连续存储的**,一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value),因 此缓存行可以存下 2 个的 Cell 对象,当Core-0 要修改 Cell[0]、Core-1 要修改 Cell[1],无论谁修改成功都会导致对方 Core 的缓存行失效,需要重新去主存获取 diff --git a/SSM.md b/SSM.md index 53b50a7..a63a48b 100644 --- a/SSM.md +++ b/SSM.md @@ -998,7 +998,7 @@ PageInfo相关API: id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(20) ); - INSERT INTO classes VALUES (NULL,'黑马一班'),(NULL,'黑马二班') + INSERT INTO classes VALUES (NULL,'程序一班'),(NULL,'程序二班') CREATE TABLE student( id INT PRIMARY KEY AUTO_INCREMENT, @@ -3874,9 +3874,7 @@ ApplicationContext: -FileSystemXmlApplicationContext: - -* 加载文件系统中任意位置的配置文件,而ClassPathXmlApplicationContext只能加载类路径下的配置文件 +FileSystemXmlApplicationContext:加载文件系统中任意位置的配置文件,而ClassPathXmlApplicationContext只能加载类路径下的配置文件 ![](https://gitee.com/seazean/images/raw/master/Frame/ApplicationContext层级结构图.png) @@ -4158,6 +4156,8 @@ BeanDefinitionRegistryPostProcessor: +***** + #### 监听器 diff --git a/Tool.md b/Tool.md index df0c1f9..27836f9 100644 --- a/Tool.md +++ b/Tool.md @@ -961,7 +961,7 @@ top:用于实时显示 process 的动态 ### ps -Linux 系统中查看进程使用情况的命令是 **ps** 指令 +Linux 系统中查看进程使用情况的命令是 ps 指令 命令:ps @@ -1478,7 +1478,7 @@ chown [-R] 属主名:属组名 文件名 chown root aaa:将文件aaa的属主更改成root -chown itcast:itcast aaa:将文件aaa的属主和属组更改为itcast +chown seazean:seazean aaa:将文件aaa的属主和属组更改为seazean @@ -1623,7 +1623,7 @@ grep [-abcEFGhHilLnqrsvVwxy][-A<显示列数>][-B<显示列数>][-C<显示列数 - 通过 `命令 >> 文件` 将**命令的成功结果** **追加** 指定文件的后面 - 通过 `命令 &>> 文件` 将 **命令的失败结果** **追加** 指定文件的后面 -`echo "黑马程序员" >> a.txt`:将黑马程序员追加到a.txt后面 +`echo "程序员" >> a.txt`:将程序员追加到a.txt后面 `cat 不存在的目录 &>> error.log`:将错误信息追加到error.log文件 From ced69469491880f01ac1d2cff3eb178837c90116 Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 7 Jun 2021 21:51:08 +0800 Subject: [PATCH 042/242] Update Java Notes --- Java.md | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 77 insertions(+), 6 deletions(-) diff --git a/Java.md b/Java.md index ce52078..f1851cf 100644 --- a/Java.md +++ b/Java.md @@ -9115,7 +9115,7 @@ Java NIO 系统的核心在于:通道和缓冲区,通道表示打开到 IO 缓冲区(Buffer):缓冲区本质上是一个**可以读写数据的内存块**,用于特定基本数据类型的容器,用于与 NIO 通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的 -![](https://gitee.com/seazean/images/raw/master//NIO-Buffer.png) +![](https://gitee.com/seazean/images/raw/master/Java/NIO-Buffer.png) Buffer 底层是一个数组,可以保存多个相同类型的数据,根据数据类型不同 ,有以下 Buffer 常用子类:ByteBuffer, CharBuffer, ShortBuffer, IntBuffer, LongBuffer, FloatBuffer, DoubleBuffer @@ -9166,6 +9166,7 @@ Buffer 基本操作: | public final int remaining() | 返回当前位置 position 和 limit 之间的元素个数 | | public final boolean hasRemaining() | 判断缓冲区中是否还有元素 | | public static ByteBuffer wrap(byte[] array) | 将一个字节数组包装到缓冲区中 | +| abstract ByteBuffer asReadOnlyBuffer() | 创建一个新的只读字节缓冲区 | Buffer 数据操作: @@ -9249,7 +9250,7 @@ public class TestBuffer { ##### 直接内存 -`byte buffer` 可以是两种类型,一种是基于直接内存(也就是非堆内存),另一种是非直接内存(也就是堆内存)。对于直接内存来说,JVM将会在IO操作上具有更高的性能,因为直接作用于本地系统的IO操作,而非直接内存,也就是堆内存中的数据,如果要作IO操作,会先从本进程内存复制到直接内存,再利用本地IO处理 +Byte Buffer可以是两种类型,一种是基于直接内存(也就是非堆内存),另一种是非直接内存(也就是堆内存)。对于直接内存来说,JVM将会在IO操作上具有更高的性能,因为直接作用于本地系统的IO操作,而非直接内存,也就是堆内存中的数据,如果要作IO操作,会先从本进程内存复制到直接内存,再利用本地IO处理 直接内存创建Buffer对象:`static XxxBuffer allocateDirect(int capacity)` @@ -9258,7 +9259,77 @@ public class TestBuffer { * 非直接内存的作用链:本地IO-->直接内存-->非直接内存-->直接内存-->本地IO * 直接内存是:本地IO → 直接内存 → 本地IO -JVM 内存结构详解直接内存 +JVM直接内存详解 + + + + + + + +**** + + + +##### 共享内存 + +FileChannel 提供 map 方法把文件映射到虚拟内存,通常情况可以映射整个文件,如果文件比较大,可以进行分段映射 + +FileChannel 中的成员属性: + +* MapMode.mode:内存映像文件访问的方式,共三种: + * MapMode.READ_ONLY:只读,试图修改得到的缓冲区将导致抛出异常。 + * MapMode.READ_WRITE:读/写,对得到的缓冲区的更改最终将写入文件;但该更改对映射到同一文件的其他程序不一定是可见的 + * MapMode.PRIVATE:私用,可读可写,但是修改的内容不会写入文件,只是 buffer 自身的改变,称之为 copy on write 写时复制 + +* position:文件映射时的起始位置 +* `public final FileLock lock()`:获取此文件通道的排他锁 + +MappedByteBuffer,可以让文件直接在内存(堆外内存)中进行修改,这种方式叫做内存映射,可以直接调用系统底层的缓存,没有JVM和系统之间的复制操作,提高了传输效率,作用: + +* 用在进程间的通信,能达到**共享内存页**的作用,但在高并发下要对文件内存进行加锁,防止出现读写内容混乱和不一致性,Java 提供了文件锁 FileLock,但在父/子进程中锁定后另一进程会一直等待,效率不高 +* 读写那些太大而不能放进内存中的文件 + +MappedByteBuffer 较之 ByteBuffer新增的三个方法 + +- `final MappedByteBuffer force()`:缓冲区是 READ_WRITE 模式下,对缓冲区内容的修改强行写入文件 +- `final MappedByteBuffer load()`:将缓冲区的内容载入物理内存,并返回该缓冲区的引用 +- `final boolean isLoaded()`:如果缓冲区的内容在物理内存中,则返回真,否则返回假 + +```java +public class MappedByteBufferTest { + public static void main(String[] args) throws Exception { + RandomAccessFile ra = new RandomAccessFile("1.txt", "rw"); + //获取对应的通道 + FileChannel channel = ra.getChannel(); + + /** + * 参数1 FileChannel.MapMode.READ_WRITE 使用的读写模式 + * 参数2 0: 可以直接修改的起始位置 + * 参数3 5: 是映射到内存的大小(不是索引位置),即将 1.txt 的多少个字节映射到内存 + * 可以直接修改的范围就是 0-5 + * 实际类型 DirectByteBuffer + */ + MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5); + + buffer.put(0, (byte) 'H'); + buffer.put(3, (byte) '9'); + buffer.put(5, (byte) 'Y'); //IndexOutOfBoundsException + + ra.close(); + System.out.println("修改成功~~"); + } +} +``` + +从硬盘上将文件读入内存,要经过文件系统进行数据拷贝,拷贝操作是由文件系统和硬件驱动实现。通过内存映射的方法访问硬盘上的文件,拷贝数据的效率要比 read 和 write 系统调用高: + +- read() 是系统调用,首先将文件从硬盘拷贝到内核空间的一个缓冲区,再将这些数据拷贝到用户空间,实际上进行了两次数据拷贝 +- map() 也是系统调用,但没有进行数据拷贝,当缺页中断发生时,直接将文件从硬盘拷贝到用户空间,只进行了一次数据拷贝 + + + +参考文章:https://www.jianshu.com/p/f90866dcbffc @@ -9383,6 +9454,8 @@ Channel 的两个方法: 1. Buffer 2. 使用上述两种方法 +![](https://gitee.com/seazean/images/raw/master/Java/NIO-复制文件.png) + ```java public class ChannelTest { @Test @@ -9500,7 +9573,7 @@ public class ChannelTest { 选择器(Selector) 是 SelectableChannle 对象的**多路复用器**,Selector 可以同时监控多个通道的状况,利用 Selector 可使一个单独的线程管理多个 Channel。**Selector 是非阻塞 IO 的核心**。 -![](https://gitee.com/seazean/images/raw/master//NIO-Selector.png) +![](https://gitee.com/seazean/images/raw/master/Java/NIO-Selector.png) * Selector 能够检测多个注册的通道上是否有事件发生(多个 Channel 以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理,就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求 * 只有在连接/通道真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程 @@ -9749,8 +9822,6 @@ AsynchronousSocketChannel、AsynchronousServerSocketChannel、AsynchronousFileCh - - *** From 71a47d1b7da7cd0e0bc9195906f827a551fa07c1 Mon Sep 17 00:00:00 2001 From: Seazean Date: Tue, 8 Jun 2021 23:02:04 +0800 Subject: [PATCH 043/242] Update Java Notes --- Issue.md | 57 ++-------------- Java.md | 199 +++++++++++++++++++++++++++---------------------------- Web.md | 173 +++++++++++++++++++++++++++++++++-------------- 3 files changed, 227 insertions(+), 202 deletions(-) diff --git a/Issue.md b/Issue.md index 9dbbc39..08f15c3 100644 --- a/Issue.md +++ b/Issue.md @@ -88,59 +88,6 @@ -## HTTP - -* **对称加密和非对称加密** - - 对称加密:加密和解密使用同一个秘钥,把密钥转发给需要发送数据的客户机,中途会被拦截(类似于把带锁的箱子和钥匙给别人,对方打开箱子放入数据,上锁后发送) - - * 优点:运算速度快 - * 缺点:无法安全的将密钥传输给通信方 - - 非对称加密:加密和解密使用不同的秘钥,一把作为公开的公钥,另一把作为私钥,公钥公开给任何人(类似于把锁和箱子给别人,对方打开箱子放入数据,上锁后发送) - - * 优点:可以更安全地将公开密钥传输给通信发送方 - * 缺点:运算速度慢 - -* **使用对称加密和非对称加密的方式传送数据** - - * 使用非对称密钥加密方式,传输对称密钥加密方式所需要的 Secret Key,从而保证安全性; - * 获取到 Secret Key 后,再使用对称密钥加密方式进行通信,从而保证效率 - - 思想:锁上加锁 - - - -* **HTTP1.1新特性** - - 默认是长连接、支持流水线、支持同时打开多个 TCP 连接、支持虚拟主机、支持分块传输编码 - 新增状态码 100、新增缓存处理指令 max-age - - - -* **Get和POST比较** - - 作用:GET 用于获取资源,而 POST 用于传输实体主体 - - 参数:GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中。不能因为 POST 参数存储在实体主体中就认为它的安全性更高,因为照样可以通过一些抓包工具(Fiddler)查看 - - 安全:安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的。GET方法是安全的,而POST不是,因为 POST 的目的是传送实体主体内容 - - * 安全的方法除了 GET 之外还有:HEAD、OPTIONS - * 不安全的方法除了 POST 之外还有 PUT、DELETE - - 幂等性:同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的。所有的安全方法也都是幂等的。在正确实现条件下,GET,HEAD,PUT 和 DELETE 等方法都是幂等的,POST 方法不是 - - 可缓存:如果要对响应进行缓存,需要满足以下条件 - - * 请求报文的 HTTP 方法本身是可缓存的,包括 GET 和 HEAD,但是 PUT 和 DELETE 不可缓存,POST 在多数情况下不可缓存的 - * 响应报文的状态码是可缓存的,包括:200, 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501 - * 响应报文的 Cache-Control 首部字段没有指定不进行缓存 - - - -*** - ## 操作系统 @@ -204,6 +151,10 @@ +**** + + + ### 内存管理 * 操作系统的内存管理主要是做什么? diff --git a/Java.md b/Java.md index f1851cf..f6b5488 100644 --- a/Java.md +++ b/Java.md @@ -35,7 +35,7 @@ Java语言提供了八种基本类型。六种数字类型(四个整数型, - 最小值是 **-128(-2^7)** - 最大值是 **127(2^7-1)** - 默认值是 **`0`** -- byte 类型用在大型数组中节约空间,主要代替整数,因为 byte 变量占用的空间只有 int 类型的四分之一; +- byte 类型用在大型数组中节约空间,主要代替整数,因为 byte 变量占用的空间只有 int 类型的四分之一 - 例子:`byte a = 100,byte b = -50` **short:** @@ -43,7 +43,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` @@ -87,7 +87,7 @@ Java语言提供了八种基本类型。六种数字类型(四个整数型, - boolean数据类型表示一位的信息 - 只有两个取值:true 和 false - 这种类型只作为一种标志来记录 true/false 情况 -- JVM规范指出boolean当做int处理,也就是4字节,boolean数组当做byte数组处理,这样我们可以得出boolean类型占了单独使用是4个字节,在数组中是确定的1个字节 +- JVM 规范指出 boolean 当做 int 处理,也就是4字节,boolean 数组当做 byte 数组处理,这样可以得出 boolean 类型单独使用占了4个字节,在数组中是1个字节 - 默认值是 **`false`** - 例子:`boolean one = true` @@ -293,7 +293,7 @@ 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 = Integer.valueOf(100); @@ -351,7 +351,7 @@ public class ScannerDemo { * 引用数据类型那么好,为什么还用基本数据类型? - > 引用类型的对象要多储存一个对象头,对基本数据类型来说空间浪费率太高 + > 引用类型的对象要多储存对象头,对基本数据类型来说空间浪费率太高 > 逻辑上来讲,java只有包装类就够了,为了运行速度,需要用到基本数据类型;优先考虑运行效率的问题,所以二者同时存在是合乎情理的。 * Java集合不能存放基本数据类型,只存放对象的引用? @@ -1171,7 +1171,7 @@ public class BinarySerach { -#### 算法思想 +#### 算法 ##### 核心思想 @@ -1216,7 +1216,7 @@ public static int f(int n){ -##### 注意 +##### 注意事项 以上理论只能针对于**规律化递归**,如果是非规律化是不能套用以上公式的! 非规律化递归的问题:文件搜索,啤酒问题。 @@ -1227,7 +1227,7 @@ public static int f(int n){ -#### 经典案例 +#### 案例 ##### 猴子吃桃 @@ -1351,10 +1351,6 @@ Debug是供程序员使用的程序调试工具,它可以用于查看程序的 - - - - *** @@ -2864,7 +2860,7 @@ s = s + "cd"; //s = abccd 新对象 ``` * 对象6:new String("ab") - * StringBuilder的toString()的调用,在字符串常量池中没有生成"ab",new String("ab")会创建两个对象是因为传参数的时候使用字面量创建了对象“ab”,当使用数组构造string对象时,没有加入常量池的操作 + * StringBuilder 的 toString() 调用,在字符串常量池中没有生成"ab",new String("ab") 会创建两个对象因为传参数的时候使用字面量创建了对象 “ab ”,当使用数组构造 String 对象时,没有加入常量池的操作 @@ -9655,7 +9651,7 @@ ssChannel.register(selector, SelectionKey.OP_ACCEPT); | 方法 | 说明 | | ------------------------------------------------------------ | -------------------------------------------- | | public final SelectableChannel configureBlocking(boolean block) | 设置此通道的阻塞模式 | - | public final SelectionKey register(Selector sel, int ops) | 向给定的选择器注册此通道,并选择感兴趣的事件 | + | public final SelectionKey register(Selector sel, int ops) | 向给定的选择器注册此通道,并选择关注的的事件 | * SocketChannel_API: @@ -9992,13 +9988,13 @@ public class UserServiceTest { ### 获取元素 -#### 获取类对象 +#### 获取类 反射技术的第一步是先得到Class类对象,有三种方式获取: * 类名.class * 通过类的对象.getClass()方法 -* Class.forName("类的全限名") --> `public static Class forName(String className) ` +* Class.forName("类的全限名"):`public static Class forName(String className) ` Class类下的方法: @@ -10039,7 +10035,7 @@ class Student{} -#### 获取构造器 +#### 获取构造 获取构造器的API: @@ -10138,7 +10134,7 @@ public class TestStudent02 { -#### 获取成员变量 +#### 获取变量 获取Field成员变量API: @@ -10234,7 +10230,7 @@ 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{ @@ -10289,11 +10285,11 @@ public class Dog { ### 暴力攻击 +泛型只能工作在编译阶段,运行阶段泛型就消失了,反射工作在运行时阶段 + 1. 反射可以破坏面向对象的封装性(暴力反射) 2. 同时可以破坏泛型的约束性 - 泛型只能工作在编译阶段,运行阶段泛型就消失了,反射工作在运行时阶段。 - ```java public class ReflectDemo { public static void main(String[] args) throws Exception { @@ -10490,8 +10486,7 @@ public class AnnotationDemo01{ ### 注解解析 -> 我们会使用注解注释一个类的成分,那么就设计到要解析出这些注解的数据。 -> 开发中经常要知道一个类的成分上面到底有哪些注解,注解有哪些属性数据,这都需要进行注解的解析。 +开发中经常要知道一个类的成分上面到底有哪些注解,注解有哪些属性数据,这都需要进行注解的解析。 注解解析相关的接口: @@ -10505,7 +10500,7 @@ API : `T getAnnotation(Class annotationClass)` : 根据注解类型获得对应注解对象 `boolean isAnnotationPresent(Class class)` : 判断对象是否使用了指定的注解 -注解原理:注解本质是一个继承了`Annotation` 的特殊接口,其具体实现类是Java 运行时生成的**动态代理类**,通过反射获取注解时,返回的是Java 运行时生成的动态代理对象`$Proxy1`,通过代理对象调用自定义注解(接口)的方法,会最终调用`AnnotationInvocationHandler` 的`invoke`方法,该方法会从`memberValues` 这个Map 中找出对应的值,而`memberValues` 的来源是Java 常量池 +注解原理:注解本质是一个继承了`Annotation` 的特殊接口,其具体实现类是 Java 运行时生成的**动态代理类**,通过反射获取注解时,返回的是 Java 运行时生成的动态代理对象 `$Proxy1`,通过代理对象调用自定义注解(接口)的方法,会最终调用 `AnnotationInvocationHandler` 的 `invoke` 方法,该方法会从 `memberValues` 这个Map 中找出对应的值,而 `memberValues` 的来源是 Java 常量池 解析注解数据的原理:注解在哪个成分上,就先拿哪个成分对象,比如注解作用在类上,则要该类的Class对象,再来拿上面的注解 @@ -12497,16 +12492,20 @@ objD.fieldG = G; // 写 解决方法:添加读写屏障,读屏障拦截第一步,写屏障拦截第二三步,在读写前后进行一些后置处理 (AOP) -* **写屏障 + 增量更新**:针对新增的引用,记录下新的引用对象,最后进行重新遍历标记 +* **写屏障 + 增量更新**:黑色对象新增的引用,将引用记录下,最后对黑色节点重新扫描 增量更新 (Incremental Update) 破坏了条件二,从而保证了不会漏标 + 缺点:对黑色变灰的对象重新扫描所有引用,比较耗费时间 + * **写屏障 (Store Barrier) + SATB**:当原来成员变量的引用发生变化之前,记录下原来的引用对象 - 保留GC开始时的对象图,即原始快照 SATB,当GC Roots确定后,对象图就已经确定,那后续的标记也应该是按照这个时刻的对象图走,如果期间发生变化则记录下来,以后根据这些记录重新标记 + 保留GC开始时的对象图,即原始快照 SATB,当 GC Roots 确定后,对象图就已经确定,那后续的标记也应该是按照这个时刻的对象图走,如果期间对白色对象有了新的引用会记录下来,白色变灰,重新扫描该对象 SATB (Snapshot At The Beginning) 破坏了条件一,从而保证了不会漏标 + 缺点: + * **读屏障 (Load Barrier)**:破坏条件二,黑色对象引用白色对象的前提是获取到该对象,此时读屏障发挥作用 以Java HotSpot VM为例,其并发标记时对漏标的处理方案如下: @@ -13193,11 +13192,11 @@ Serial GC、Parallel GC、Concurrent Mark Sweep GC这三个GC不同: 实例数据:实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来 -对齐填充:起占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,就是对象的大小必须是8字节的整数倍,而对象头部分正好是8字节的倍数(1倍或者2倍),因此当对象实例数据部分没有对齐时,就需要通过对齐填充来补全 +对齐填充:Padding 起占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,就是对象的大小必须是8字节的整数倍,而对象头部分正好是8字节的倍数(1倍或者2倍),因此当对象实例数据部分没有对齐时,就需要通过对齐填充来补全 32位系统 -* 一个int在java中占据4byte,所以Integer的大小为: +* 一个 int 在 java 中占据 4byte,所以 Integer 的大小为: ```ruby # 需要补位4byte @@ -13232,9 +13231,9 @@ Serial GC、Parallel GC、Concurrent Mark Sweep GC这三个GC不同: private int size; ``` - Mark Word 占4byte,Klass Word 占4byte,一个int字段(size)占 4byte,elementData 数组本身占 12(4+4+4),数组中10个Integer对象占 10×16,所以整个集合空间大小为 184byte + Mark Word 占 4byte,Klass Word 占 4byte,一个int字段(size)占 4byte,elementData 数组本身占 12(4+4+4),数组中 10 个 Integer 对象占 10×16,所以整个集合空间大小为 184byte -* 时间用long/int表示,不用Date或者String +* 时间用 long/int 表示,不用 Date 或者 String @@ -15736,7 +15735,7 @@ JDK 自带了**监控工具,位于 JDK 的 bin 目录下**,其中最常用 * 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量 - Java中的通信机制:volatile、等待/通知机制、join方式、ThreadLocal + Java中的通信机制:volatile、等待/通知机制、join方式、InheritableThreadLocal、MappedByteBuffer * 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低 @@ -16078,45 +16077,43 @@ public class Test14 { `public static boolean interrupted()`:判断当前线程是否被打断,清除打断标记 `public boolean isInterrupted()`:判断当前线程是否被打断,不清除打断标记 -sleep,wait,join方法都会让线程进入阻塞状态,打断进程会**清空打断状态** (false) - -```java -public static void main(String[] args) throws InterruptedException { - Thread t1 = new Thread(()->{ - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - }, "t1"); - t1.start(); - Thread.sleep(500); - t1.interrupt(); - System.out.println(" 打断状态: {}" + t1.isInterrupted());// 打断状态: {}false -} -``` - +* sleep,wait,join方法都会让线程进入阻塞状态,打断进程会**清空打断状态** (false) + ```java + public static void main(String[] args) throws InterruptedException { + Thread t1 = new Thread(()->{ + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }, "t1"); + t1.start(); + Thread.sleep(500); + t1.interrupt(); + System.out.println(" 打断状态: {}" + t1.isInterrupted());// 打断状态: {}false + } + ``` -打断正常运行的线程:不会清空打断状态(true) +* 打断正常运行的线程:不会清空打断状态(true) -```java -public static void main(String[] args) throws Exception { - Thread t2 = new Thread(()->{ - while(true) { - Thread current = Thread.currentThread(); - boolean interrupted = current.isInterrupted(); - if(interrupted) { - System.out.println(" 打断状态: {}" + interrupted);//打断状态: {}true - break; - } - } - }, "t2"); - t2.start(); - Thread.sleep(500); - t2.interrupt(); -} -``` + ```java + public static void main(String[] args) throws Exception { + Thread t2 = new Thread(()->{ + while(true) { + Thread current = Thread.currentThread(); + boolean interrupted = current.isInterrupted(); + if(interrupted) { + System.out.println(" 打断状态: {}" + interrupted);//打断状态: {}true + break; + } + } + }, "t2"); + t2.start(); + Thread.sleep(500); + t2.interrupt(); + } + ``` @@ -16177,7 +16174,7 @@ System.out.println("unpark...");//和上一个unpark同时执行 打断线程可能在任何时间,所以需要考虑在任何时刻被打断的处理方法: ```java -public class Test13 { +public class Test { public static void main(String[] args) throws InterruptedException { TwoPhaseTermination tpt = new TwoPhaseTermination(); tpt.start(); @@ -20133,36 +20130,6 @@ class DelayTask implements Delayed { ### 操作Pool -#### 状态信息 - -ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量 - -| 状态 | 高3位 | 接收新任务 | 处理阻塞任务队列 | 说明 | -| ---------- | ----- | ---------- | ---------------- | --------------------------------------- | -| RUNNING | 111 | Y | Y | | -| SHUTDOWN | 000 | N | Y | 不接收新任务,但处理阻塞队列剩余任务 | -| STOP | 001 | N | N | 中断正在执行的任务,并抛弃阻塞队列任务 | -| TIDYING | 010 | - | - | 任务全执行完毕,活动线程为0即将进入终结 | -| TERMINATED | 011 | - | - | 终止状态 | - -这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 cas 原子操作 -进行赋值 - -```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) - - - -*** - - - #### 创建方法 ##### Executor @@ -20343,7 +20310,7 @@ Executors提供了四种线程池的创建:newCachedThreadPool、newFixedThrea -#### 提交任务 +#### 提交方法 ExecutorService类API: @@ -20418,6 +20385,36 @@ System.out.println(future.get()); +*** + + + +#### 状态信息 + +ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量 + +| 状态 | 高3位 | 接收新任务 | 处理阻塞任务队列 | 说明 | +| ---------- | ----- | ---------- | ---------------- | --------------------------------------- | +| RUNNING | 111 | Y | Y | | +| SHUTDOWN | 000 | N | Y | 不接收新任务,但处理阻塞队列剩余任务 | +| STOP | 001 | N | N | 中断正在执行的任务,并抛弃阻塞队列任务 | +| TIDYING | 010 | - | - | 任务全执行完毕,活动线程为0即将进入终结 | +| TERMINATED | 011 | - | - | 终止状态 | + +这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 cas 原子操作 +进行赋值 + +```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) + + + *** @@ -20462,7 +20459,7 @@ private static void method1() { #### Scheduled -任务调度线程池ScheduledThreadPoolExecutor继承ThreadPoolExecutor: +任务调度线程池 ScheduledThreadPoolExecutor 继承 ThreadPoolExecutor: 构造方法:`Executors.newScheduledThreadPool(int corePoolSize)` @@ -22854,6 +22851,8 @@ class ThreadB extends Thread{ ### ConHashMap +(待更新) + #### 并发集合 ##### 集合对比 diff --git a/Web.md b/Web.md index 4848b36..7730300 100644 --- a/Web.md +++ b/Web.md @@ -2067,7 +2067,9 @@ a{ -**** + + +*** @@ -2075,30 +2077,44 @@ a{ # HTTP -## 协议概述 +## 相关概念 + +HTTP:Hyper Text Transfer Protocol,意为超文本传输协议,是建立在**TCP/IP协议**基础上,指的是服务器和客户端之间交互必须遵循的一问一答的规则,形容这个规则:问答机制、握手机制 -HTTP:Hyper Text Transfer Protocol,意为超文本传输协议,是建立在**TCP/IP协议**基础上,它指的是服务器和客户端之间交互必须遵循的一问一答的规则,形容这个规则:问答机制、握手机制 +HTTP协议是**一个无状态的面向连接的协议**,指的是协议对于事务处理没有记忆能力,服务器不知道客户端是什么状态。所以打开一个服务器上的网页和上一次打开这个服务器上的网页之间没有任何联系 -互联网通信协议 HTTP 协议,是一个**无状态协议**,所有的资源状态都保存在服务器端 +注意:无状态并不是代表HTTP就是UDP,面向连接也不是代表HTTP就是TCP HTTP作用:用于定义WEB浏览器与WEB服务器之间交换数据的过程和数据本身的内容 -浏览器和服务器交互过程: 浏览器请求, 服务请求响应 +浏览器和服务器交互过程:浏览器请求,服务请求响应 * 请求(请求行,请求头,请求体) * 响应(响应行,响应头,响应体) -URL:统一资源定位符, +URL 和 URI + +* URL:统一资源定位符 + 格式:http://127.0.0.1:8080/request/servletDemo01 + 详解:http:协议;127.0.0.1:域名;8080:端口;request/servletDemo01:请求资源路径 -* 格式:http://127.0.0.1:8080/request/servletDemo01 -* 详解:http:协议;127.0.0.1:域名;8080:端口;request/servletDemo01:请求资源路径 +* URI:统一资源标志符 + 格式:/request/servletDemo01 -URI:统一资源标志符 +* 区别:`URL-HOST=URI`,URI是抽象的定义,URL用地址定位,URI 用名称定位。 + **只要能唯一标识资源的是URI,在URI的基础上给出其资源的访问方式的是URL** -* 格式:/request/servletDemo01 +短连接和长连接: -区别:URL-HOST=URI。URI是抽象的定义。URL用地址定位,URI 用名称定位。 - **只要能唯一标识资源的是URI,在URI的基础上给出其资源的访问方式的是URL** +* 短连接:客户端和服务器每进行一次 HTTP 操作,就建立一次连接,任务结束就中断连接。 + + 使用短连接的情况下,当浏览器访问的某个 HTML 或其他类型的 Web 页中包含有其他的 Web 资源(图像文件、CSS文件等),每遇到这样一个Web资源,浏览器就会重新建立一个HTTP会话 + +* 长连接:使用长连接的 HTTP 协议,会在响应头加入这行代码 `Connection:keep-alive` + + 使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输 HTTP 数据的 TCP 连接不会关闭,客户端再次访问这个服务器时,会继续使用这一条已经建立的连接。Keep-Alive 有一个保持时间,不会永久保持连接,设置以后可以实现长连接,前提是需要客户端和服务端都支持长连接 + +* HTTP协议的长连接和短连接,实质上是TCP协议的长连接和短连接 @@ -2115,15 +2131,44 @@ URI:统一资源标志符 * HTTP/1.1 默认长连接(一次TCP连接可以多次请求);支持PUT、DELETE、PATCH等六种请求;增加host头,支持虚拟主机;支持断点续传功能 * HTTP/2.0 多路复用,降低开销(一次TCP连接可以处理多个请求);服务器主动推送(相关资源一个请求全部推送);解析基于二进制,解析错误少,更高效(HTTP/1.X解析基于文本);报头压缩,降低开销。 -目前HTTP协议主要是1.0版本和1.1版本。这两个版本的区别主要是两个方面 +HTTP 1.0 和 HTTP 1.1 的主要区别: + +* 长短连接: + + **在HTTP/1.0中,默认使用的是短连接**,每次请求都要重新建立一次连接。HTTP 基于 TCP/IP 协议的,每一次建立或者断开连接都需要三次握手四次挥手的开销,如果每次请求都要这样的话,开销会比较大。因此最好能维持一个长连接,可以用个长连接来发多个请求 -1. HTTP1.1版本比1.0版本多了一些消息头 + **HTTP 1.1起,默认使用长连接** ,默认开启 `Connection: keep-alive`,HTTP/1.1 的持续连接有非流水线方式和流水线方式 ,流水线方式是客户端在收到HTTP的响应报文之前就能接着发送新的请求报文,非流水线方式是客户端在收到前一个响应后才能发送下一个请求 -2. HTTP1.1版本和1.0版本的执行过程不一样 +* 错误状态响应码:在 HTTP1.1 中新增了 24 个错误状态响应码,如 409(Conflict) 表示请求的资源与资源的当前状态发生冲突,410(Gone) 表示服务器上的某个资源被永久性的删除 + +* 缓存处理:在 HTTP1.0 中主要使用 header 里的 If-Modified-Since,Expires 来做为缓存判断的标准,HTTP1.1 则引入了更多的缓存控制策略例如 Entity tag,If-Unmodified-Since,If-Match,If-None-Match等更多可供选择的缓存头来控制缓存策略 + +* 带宽优化及网络连接的使用:HTTP1.0 中,存在一些浪费带宽的现象,例如客户端只需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,HTTP1.1则在请求头引入了 range 头域,允许只请求资源的某个部分,即返回码是 206(Partial Content) ,这样就方便了开发者自由的选择以便于充分利用带宽和连接 + +HTTP 和 HTTPS 的区别: + +* 端口 :HTTP 默认使用端口 80,HTTPS 默认使用端口443 +* 安全性: HTTP 协议运行在 TCP 之上,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份;HTTPS 是运行在 SSL/TLS 之上的 HTTP 协议,SSL/TLS 运行在 TCP 之上,所有传输的内容都经过加密,加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密 +* 资源消耗:HTTP 安全性没有 HTTPS高,但是 HTTPS 比 HTTP 耗费更多服务器资源 + +**对称加密和非对称加密** + +* 对称加密:加密和解密使用同一个秘钥,把密钥转发给需要发送数据的客户机,中途会被拦截(类似于把带锁的箱子和钥匙给别人,对方打开箱子放入数据,上锁后发送),典型的对称加密算法有DES、AES等 + * 优点:运算速度快 + * 缺点:无法安全的将密钥传输给通信方 + +* 非对称加密:加密和解密使用不同的秘钥,一把作为公开的公钥,另一把作为私钥,公钥公开给任何人(类似于把锁和箱子给别人,对方打开箱子放入数据,上锁后发送),典型的非对称加密算法有RSA、DSA等 + * 优点:可以更安全地将公开密钥传输给通信发送方 + * 缺点:运算速度慢 + +* **使用对称加密和非对称加密的方式传送数据** + + * 使用非对称密钥加密方式,传输对称密钥加密方式所需要的 Secret Key,从而保证安全性 + * 获取到 Secret Key 后,再使用对称密钥加密方式进行通信,从而保证效率 + + 思想:锁上加锁 - * HTTP1.0: 创建连接(TCP/IP)-->发送请求-->得到响应-->关闭连接->创建连接(TCP/IP)-->循环 - * HTTP1.1: 创建连接(TCP/IP)-->发送请求1-->得到响应1-->发送请求2-->得到响应2.....-->连接超时或手动关闭连接 @@ -2162,9 +2207,26 @@ URI:统一资源标志符 Cookie: Idea-b77ddca6=4bc282fe-febf-4fd1-b6c9-72e9e0f381e8 ``` - + * 面试题:**Get 和POST比较** + + 作用:GET 用于获取资源,而 POST 用于传输实体主体 + 参数:GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中。不能因为 POST 参数存储在实体主体中就认为它的安全性更高,因为照样可以通过一些抓包工具(Fiddler)查看 + 安全:安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的。GET方法是安全的,而POST不是,因为 POST 的目的是传送实体主体内容 + + * 安全的方法除了 GET 之外还有:HEAD、OPTIONS + * 不安全的方法除了 POST 之外还有 PUT、DELETE + + 幂等性:同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的。所有的安全方法也都是幂等的。在正确实现条件下,GET,HEAD,PUT 和 DELETE 等方法都是幂等的,POST 方法不是 + + 可缓存:如果要对响应进行缓存,需要满足以下条件 + + * 请求报文的 HTTP 方法本身是可缓存的,包括 GET 和 HEAD,但是 PUT 和 DELETE 不可缓存,POST 在多数情况下不可缓存的 + * 响应报文的状态码是可缓存的,包括:200, 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501 + * 响应报文的 Cache-Control 首部字段没有指定不进行缓存 + + * 请求行详解 @@ -2297,6 +2359,8 @@ URI:统一资源标志符 + + # Servlet ## JavaEE @@ -4063,17 +4127,28 @@ public class ServletDemo08 extends HttpServlet { ### 会话技术 -* **会话**:浏览器和服务器之间的多次请求和响应 +**会话**:浏览器和服务器之间的多次请求和响应 + +浏览器和服务器可能产生多次的请求和响应,从浏览器访问服务器开始,到访问服务器结束(关闭浏览器、到了过期时间),这期间产生的多次请求和响应加在一起称为浏览器和服务器之间的一次对话 + +**作用**:保存用户各自的数据(以浏览器为单位),在多次请求间实现数据共享 + +**常用的会话管理技术**: + +* Cookie:客户端会话管理技术,用户浏览的信息以键值对(key=value)的形式保存在浏览器上。如果没有关闭浏览器,再次访问服务器,会把cookie带到服务端,服务端就可以做相应的处理 +* Session:服务端会话管理技术。服务器为每一个浏览器开辟一块内存空间,即session。由于内存空间是每一个浏览器独享的,所有用户在访问的时候,可以把信息保存在session对象中。同时,每一个session对象都对应一个sessionId,服务器把sessionId写到cookie中,再次访问的时候,浏览器会把cookie(sessionId)带过来,找到对应的session对象。 - 浏览器和服务器可能产生多次的请求和响应,从浏览器访问服务器开始,到访问服务器结束(关闭浏览器、到了过期时间),这期间产生的多次请求和响应加在一起称为浏览器和服务器之间的一次对话 +两者区别: -* **作用**:保存用户各自的数据(以浏览器为单位),在多次请求间实现数据共享 - 我们在会话的过程(多次请求)中,用户可能会产生一些数据,这些数据有的需要保存起来的,就可以通过会话技术来保存用户各自的数据 +* Cookie 存储在客户端中,而 Session 存储在服务器上,相对来说 Session 安全性更高。如果要在 Cookie 中存储一些敏感信息,不要直接写入 Cookie,应该将 Cookie 信息加密然后使用到的时候再去服务器端解密 + +* Cookie 一般用来保存用户信息 + + 在 Cookie 中保存已经登录过得用户信息,下次访问网站的时候就不需要重新登录,因为用户登录的时候可以存放一个 Token 在 Cookie 中,下次登录的时候只需要根据 Token 值来查找用户即可(为了安全考虑,重新登录一般要将 Token 重写),所以登录一次网站后访问网站其他页面不需要重新登录 + +* Session 通过服务端记录用户的状态,服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户 -* **常用的会话管理技术**: - * Cookie:客户端会话管理技术,用户浏览的信息以键值对(key=value)的形式保存在浏览器上。如果没有关闭浏览器,再次访问服务器,会把cookie带到服务端,服务端就可以做响应的处理 - * Session:服务端会话管理技术。服务器为每一个浏览器开辟一块内存空间,即session。由于内存空间是每一个浏览器独享的,所有用户在访问的时候,可以把信息保存在session对象中。同时,每一个session对象都对应一个sessionId,服务器把sessionId写到cookie中,再次访问的时候,浏览器会把cookie(sessionId)带过来,找到对应的session对象。 @@ -4081,7 +4156,7 @@ public class ServletDemo08 extends HttpServlet { -### Cookie概述 +### 基本介绍 Cookie:客户端会话管理技术,把要共享的数据保存到了客户端(也就是浏览器端)。每次请求时,把会话信息带到服务器,从而实现多次请求的数据共享。 @@ -4095,9 +4170,9 @@ Cookie:客户端会话管理技术,把要共享的数据保存到了客户 -### Cookie使用 +### 基本使用 -#### CookieAPI +#### 常用API * **Cookie属性:** @@ -4120,10 +4195,10 @@ Cookie:客户端会话管理技术,把要共享的数据保存到了客户 * Cookie属性对应的set和get方法,name属性被final修饰,没有set方法 * HttpServletResponse类API: - `void addCookie(Cookie cookie)` : 向客户端添加Cookie,Adds the specified cookie to the response. + `void addCookie(Cookie cookie)` : 向客户端添加Cookie,Adds the specified cookie to the response. * HttpServletRequest类API: - `Cookie[] getCookies()` : 获取所有的Cookie对象, all of the Cookie objects the client sent with this request. + `Cookie[] getCookies()` : 获取所有的Cookie对象,client sent with this request @@ -4131,12 +4206,13 @@ Cookie:客户端会话管理技术,把要共享的数据保存到了客户 -#### Cookie有效期 +#### 有效期 设置Cookie存活时间API:`void setMaxAge(int expiry)` - -1:默认。代表Cookie数据存到浏览器关闭(保存在浏览器文件中) - 0:代表删除Cookie,如果要删除Cookie要确保**路径一致**。 - 正整数:以秒为单位保存数据有有效时间(把缓存数据保存到磁盘中) + +* -1:默认。代表Cookie数据存到浏览器关闭(保存在浏览器文件中) +* 0:代表删除Cookie,如果要删除Cookie要确保**路径一致**。 +* 正整数:以秒为单位保存数据有有效时间(把缓存数据保存到磁盘中) ```java @WebServlet("/servletDemo01") @@ -4184,7 +4260,7 @@ public class ServletDemo01 extends HttpServlet{ -#### Cookie路径 +#### 有效路径 `setPath(String url)` : Cookie设置有效路径 @@ -4194,9 +4270,7 @@ public class ServletDemo01 extends HttpServlet{ 2. 路径不一样, cookie的key可以相同 3. 保证自己的项目可以合理的利用自己项目的cookie - - -判断路径是否携带Cookie:请求资源URI.startWith(cookie的path),返回true就带 +判断路径是否携带 Cookie:请求资源URI.startWith(cookie的path),返回true就带 | 访问URL | URI部分 | Cookie的Path | 是否携带Cookie | 能否取到Cookie | | ------------------------------------------------------------ | -------------------------- | ------------ | -------------- | -------------- | @@ -4214,11 +4288,11 @@ public class ServletDemo01 extends HttpServlet{ -#### Cookie安全 +#### 安全性 -如果Cookie中设置了HttpOnly属性,C么通过js脚本将无法读取到cookie信息,这样能有效的防止XSS攻击,窃取cookie内容,这样就增加了cookie的安全性,即便是这样,也不要将重要信息存入cookie。 +如果 Cookie 中设置了 HttpOnly 属性,通过 js 脚本将无法读取到 cookie 信息,这样能有效的防止 XSS 攻击,窃取 cookie 内容,这样就增加了安全性,即便是这样,也不要将重要信息存入cookie。 -XSS全称Cross SiteScript,跨站脚本攻击,是Web程序中常见的漏洞,XSS属于被动式且用于客户端的攻击方式,所以容易被忽略其危害性。其原理是攻击者向有XSS漏洞的网站中输入(传入)恶意的HTML代码,当其它用户浏览该网站时,这段HTML代码会自动执行,从而达到攻击的目的。如盗取用户Cookie、破坏页面结构、重定向到其它网站等。 +XSS 全称 Cross SiteScript,跨站脚本攻击,是Web程序中常见的漏洞,XSS 属于被动式且用于客户端的攻击方式,所以容易被忽略其危害性。其原理是攻击者向有 XSS 漏洞的网站中输入(传入)恶意的 HTML 代码,当其它用户浏览该网站时,这段HTML代码会自动执行,从而达到攻击的目的。如盗取用户 Cookie、破坏页面结构、重定向到其它网站等。 @@ -4231,10 +4305,9 @@ XSS全称Cross SiteScript,跨站脚本攻击,是Web程序中常见的漏洞 ## Session -### Session概述 +### 基本介绍 -Session:服务器端会话管理技术, -本质也是采用客户端会话管理技术,不过在客户端保存的是一个特殊标识,共享的数据保存到了服务器的内存对象中。每次请求时,会将特殊标识带到服务器端,根据标识来找到对应的内存空间,从而实现数据共享。简单说它就是一个服务端会话对象,用于存储用户的会话数据。 +Session:服务器端会话管理技术,本质也是采用客户端会话管理技术,不过在客户端保存的是一个特殊标识,共享的数据保存到了服务器的内存对象中。每次请求时,会将特殊标识带到服务器端,根据标识来找到对应的内存空间,从而实现数据共享。简单说它就是一个服务端会话对象,用于存储用户的会话数据。 Session域(会话域)对象是Servlet规范中四大域对象之一,并且它也是用于实现数据共享的 @@ -4250,9 +4323,9 @@ Session域(会话域)对象是Servlet规范中四大域对象之一,并且 -### Session使用 +### 基本使用 -#### 获取Session +#### 获取会话 HttpServletRequest类获取Session: @@ -4278,7 +4351,7 @@ HttpServletRequest类获取Session: -#### 实现Sesson +#### 实现会话 通过第一个Servlet设置共享的数据用户名,并在第二个Servlet获取到。 @@ -4332,7 +4405,7 @@ public class ServletDemo02 extends HttpServlet{ -### Session问题 +### 会话问题 #### 禁用Cookie @@ -4361,7 +4434,7 @@ public class ServletDemo02 extends HttpServlet{ } ``` -* 方式二:访问时拼接jsessionid标识,通过encodeURL()方法**重写地址** +* 方式二:访问时拼接 jsessionid 标识,通过 encodeURL() 方法**重写地址** ```java @Override @@ -8115,6 +8188,8 @@ $("#btn5").click(function(){ + + # VUE ## 概述 From a9dbda2a257b0caa755458c07baf4f93544606a9 Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 9 Jun 2021 21:35:26 +0800 Subject: [PATCH 044/242] Update Java Notes --- DB.md | 61 ++++++++++++++++++++++++++++++++++----------------------- Java.md | 47 +++++++++++++++++++++++++++++++------------- 2 files changed, 70 insertions(+), 38 deletions(-) diff --git a/DB.md b/DB.md index 839c282..316e7a7 100644 --- a/DB.md +++ b/DB.md @@ -3648,7 +3648,7 @@ MERGE存储引擎: -## 索引优化 +## 索引机制 ### 索引介绍 @@ -4069,7 +4069,7 @@ B+Tree优点:提高查询速度,减少磁盘的IO次数,树形结构较小 -### 优化方式 +### 索引优化 #### 覆盖索引 @@ -4177,7 +4177,7 @@ CREATE INDEX idx_area ON table_name(area(7)); -## 优化语句 +## 语句优化 ### 优化步骤 @@ -4317,12 +4317,12 @@ EXPLAIN SELECT * FROM table_1 WHERE id = 1; MySQL执行计划的局限: * EXPLAIN 不会告诉显示关于触发器、存储过程的信息或用户自定义函数对查询的影响情况 -* EXPLAIN 不考虑各种Cache -* EXPLAIN 不能显示MySQL在执行查询时所作的优化工作,因为执行计划在执行查询之前生成 +* EXPLAIN 不考虑各种 Cache +* EXPLAIN 不能显示 MySQL 在执行查询时所作的优化工作,因为执行计划在执行查询之前生成 * EXPALIN 部分统计信息是估算的,并非精确值 -* EXPALIN 只能解释SELECT操作,其他操作要重写为SELECT后查看执行计划 +* EXPALIN 只能解释 SELECT 操作,其他操作要重写为 SELECT 后查看执行计划 * 执行计划 在优化器之后、执行器之前生成,然后执行器调用存储引擎检索数据 -* 执行计划可以随着底层优化器输入的更改而更改。EXPLAIN PLAN 显示的是在解释语句时数据库将如何运行SQL语句,由于执行环境和 EXPLAIN PLAN 环境的不同,此计划可能与SQL语句实际的执行计划不同 +* 执行计划可以随着底层优化器输入的更改而更改。EXPLAIN PLAN 显示的是在解释语句时数据库将如何运行 SQL 语句,由于执行环境和 EXPLAIN PLAN 环境的不同,此计划可能与 SQL 语句实际的执行计划不同 环境准备: @@ -4338,7 +4338,7 @@ MySQL执行计划的局限: ##### id -SQL执行的顺序的标识,SQL从大到小的执行 +SQL 执行的顺序的标识,SQL 从大到小的执行 * id 相同时,执行顺序由上至下 @@ -4461,7 +4461,7 @@ key_len: * Impossible where:说明 WHERE 语句会导致没有符合条件的行,通过收集统计信息不可能存在结果 * Select tables optimized away:说明仅通过使用索引,优化器可能仅从聚合函数结果中返回一行 -* No tables used:Query 语句中使用from dual 或不含任何 from 子句 +* No tables used:Query 语句中使用 from dual 或不含任何 from 子句 @@ -5017,7 +5017,7 @@ MySQL 4.1版本之后,开始支持SQL的子查询 #### OR -对于包含OR的查询子句,如果要利用索引,则OR之间的每个条件列都必须用到索引,而且不能使用到复合索引,如果没有索引,则应该考虑增加索引 +对于包含OR的查询子句,如果要利用索引,则 OR 之间的每个条件列都必须用到索引,而且不能使用到复合索引,如果没有索引,则应该考虑增加索引 * 执行查询语句: @@ -5122,7 +5122,7 @@ SQL提示,是优化数据库的一个重要手段,就是在SQL语句中加 -## 优化系统 +## 系统优化 ### 应用优化 @@ -5424,9 +5424,9 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 -### 主从复制 +## 主从复制 -#### 基本介绍 +### 基本介绍 复制是指将主数据库的 DDL 和 DML 操作通过二进制日志传到从库服务器中,然后在从库上对这些日志重新执行(也叫重做),从而使得从库和主库的数据保持同步 @@ -5436,36 +5436,47 @@ MySQL 复制的优点主要包含以下三个方面: - 主库出现问题,可以快速切换到从库提供服务 -- 可以在从库上执行查询操作,从主库中更新,实现**读写分离**,降低主库的访问压力 +- 可以在从库上执行查询操作,从主库中更新,实现读写分离 - 可以在从库中执行备份,以避免备份期间影响主库的服务 +**读写分离**: + +* 读写分离可以降低主库的访问压力,提高系统的并发能力 +* 主库不建查询的索引,从库建查询的索引。因为索引需要维护的,比如插入一条数据,不仅要在聚簇索引上面插入,对应的二级索引也得插入,修改也是一样的。所以将读操作分到从库了之后,可以在主库把查询要用的索引删了,减少写操作对主库的影响 + *** -#### 复制原理 +### 复制原理 MySQL 的主从复制原理图: ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-主从复制原理图.jpg) -主从复制需要三个线程:**master(binlog dump thread)、slave(I/O thread 、SQL thread)** +主从复制主要依赖的是 binlog,MySQL 默认是异步复制,需要三个线程:**master(binlog dump thread)、slave(I/O thread 、SQL thread)** - binlog dump thread:在主库事务提交时,负责把数据变更作为事件 Events 记录在二进制日志文件 binlog 中,并通知 slave 有数据更新 - I/O thread:负责从主服务器上拉取二进制日志,并将 binlog 日志内容依次写到 relay log 文件的最末端,并将新的 binlog 文件名和 offset 记录到 master-info 文件中,以便下一次读取master 端新 binlog 日志时能告诉 master 服务器从新 binlog 日志的指定文件及位置开始读取新的 binlog 日志内容 - SQL thread:监测本地 relay log中新增了日志内容,读取中继日志并重做其中的 SQL 语句 - 从库在 relay-log.info 中记录当前应用中继日志的文件名和位置点以便下一次数据复制 +同步与异步: + +* 异步复制有数据丢失风险,例如数据还未同步到从库,主库就给客户端响应,然后主库挂了,此时从库晋升为主库的话数据是缺失的 +* 同步复制,主库需要将 binlog 复制到所有从库,等所有从库响应了之后才会给客户端响应,这样的话性能很差,一般不会选择同步复制 +* MySQL 5.7 之出现了半同步复制,有参数可以选择成功同步几个从库就返回响应 + **** -#### 主从延迟 +### 主从延迟 主从延迟就是主从之间是存在一定时间的数据不一致: @@ -5488,11 +5499,13 @@ MySQL 的主从复制原理图: 主从同步问题永远都是一致性和性能的权衡,需要根据实际的应用场景,可以采取下面的办法: +* 二次查询,如果从库查不到数据,则再去主库查一遍,由 API 封装,比较简单,但导致主库压力大 * 降低多线程大事务并发的概率,优化业务逻辑 -* 优化SQL,避免慢SQL,减少批量操作 +* 优化 SQL,避免慢 SQL,减少批量操作 * 提高从库机器的配置,减少主库写 binlog 和从库读 binlog 的效率差 * 尽量采用短的链路,主库和从库服务器的距离尽量要短,提升端口带宽,减少 binlog 传输的网络延时 * 实时性要求的业务读强制走主库,从库只做灾备,备份 +* 强制将写之后立马读的操作转移到主库,比如刚注册的用户,直接登录从库查询可能查询不到,先走主库登录 @@ -5500,9 +5513,9 @@ MySQL 的主从复制原理图: -#### 搭建流程 +### 搭建流程 -##### master +#### master 1. 在master 的配置文件(/etc/mysql/my.cnf)中,配置如下内容: @@ -5562,7 +5575,7 @@ MySQL 的主从复制原理图: -##### slave +#### slave 1. 在 slave 端配置文件中,配置如下内容: @@ -5601,7 +5614,7 @@ MySQL 的主从复制原理图: -##### 验证 +#### 验证 1. 在主库中创建数据库,创建表并插入数据: @@ -7883,7 +7896,7 @@ Redis 自身是一个 Map,其中所有的数据都是采用 key : value 的形 ### 线程模型 -Redis 基于 Reactor 模式开发了网络事件处理器,这个处理器被称为文件事件处理器(file event handler),这个文件事件处理器是单线程的,所以Redis叫做单线程的模型 +Redis 基于 Reactor 模式开发了网络事件处理器,这个处理器被称为文件事件处理器(file event handler),这个文件事件处理器是单线程的,所以 Redis 叫做单线程的模型 文件事件处理器以单线程方式运行,但是使用 I/O 多路复用程序来监听多个套接字,既实现了高性能的网络通信模型,又很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接,保持了 Redis 单线程设计的简单性 @@ -10831,7 +10844,7 @@ Cache Aside Pattern 中服务端需要同时维系 DB 和 cache,并且是以 D 数据库和缓存数据强一致场景 :更新DB的时候同样更新 cache,不过需要加一个锁来保证更新 cache 时不存在线程安全问题,这样可以增加命中率 - 可以短暂地允许数据库和缓存数据不一致场景 :更新DB的时候同样更新 cache,但是给缓存加一个比较短的过期时间,这样的话就可以保证即使数据不一致影响也比较小 + 可以短暂地允许数据库和缓存数据不一致场景:更新DB的时候同样更新 cache,但是给缓存加一个比较短的过期时间,这样的话就可以保证即使数据不一致影响也比较小 diff --git a/Java.md b/Java.md index f6b5488..3b24c04 100644 --- a/Java.md +++ b/Java.md @@ -316,8 +316,8 @@ System.out.println(x == y); // false 语法:`Scanner sc = new Scanner(System.in)` -* next():遇到了空格, 就不再录入数据了 , 结束标记: 空格, tab键 -* nextLine():可以将数据完整的接收过来 , 结束标记: 回车换行符 +* next():遇到了空格,就不再录入数据了,结束标记:空格、tab键 +* nextLine():可以将数据完整的接收过来,结束标记:回车换行符 一般使用 `sc.nextInt()` 或者 `sc.nextLine()` 接受整型和字符串,然后转成需要的数据类型 @@ -327,10 +327,10 @@ print:`PrintStream.write()` > 使用引用数据类型的API ```java -import java.util.Scanner; -public class ScannerDemo { - public static void main(String[] args) { - Scanner sc = new Scanner(System.in); +public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + while (sc.hasNextLine()) { + String msg = sc.nextLine(); } } ``` @@ -7645,7 +7645,7 @@ fw.close; #### 缓冲流 -##### 概述 +##### 基本介绍 作用:缓冲流可以提高字节流和字符流的读写数据的性能。 @@ -12131,7 +12131,7 @@ public class Demo1_27 { 虚拟机采用了两种方式在创建对象时解决并发问题:CAS、TLAB -TLAB:Thread Local Allocation Buffer,为每个线程在堆内单独分配了一个缓冲区,多线程分配内存时,使用TLAB可以避免线程安全问题,同时还能够提升内存分配的吞吐量,这种内存分配方式叫做**快速分配策略** +TLAB:Thread Local Allocation Buffer,为每个线程在堆内单独分配了一个缓冲区,多线程分配内存时,使用TLAB 可以避免线程安全问题,同时还能够提升内存分配的吞吐量,这种内存分配方式叫做**快速分配策略** - 栈上分配使用的是栈来进行对象内存的分配 - TLAB 分配使用的是 Eden 区域进行内存分配,属于堆内存 @@ -12142,7 +12142,7 @@ TLAB:Thread Local Allocation Buffer,为每个线程在堆内单独分配了 ![](https://gitee.com/seazean/images/raw/master/Java/JVM-TLAB内存分配策略.jpg) -JVM是将TLAB作为内存分配的首选,但不是所有的对象实例都能够在TLAB中成功分配内存,一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过**使用加锁机制确保数据操作的原子性**,从而直接在Eden空间中分配内存 +JVM 是将 TLAB 作为内存分配的首选,但不是所有的对象实例都能够在 TLAB 中成功分配内存,一旦对象在TLAB空间分配内存失败时,JVM 就会通过**使用加锁机制确保数据操作的原子性**,从而直接在 Eden 空间中分配内存 栈上分配优先于 TLAB 分配进行,逃逸分析中若可进行栈上分配优化,会优先进行对象栈上直接分配内存 @@ -19363,6 +19363,27 @@ public class JdbcUtils { } ``` +用 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); + } +} +``` + **** @@ -19679,7 +19700,7 @@ Memory leak:内存泄漏是指程序中动态分配的堆内存由于某种原 * 如果key使用弱引用: - 使用完 ThreadLocal ,threadLocal Ref 被回收,ThreadLocalMap 只持有 ThreadLocal 的弱引用,所以threadlocal 也可以被回收,此时Entry中的 key=null。但没有手动删除这个Entry或者 CurrentThread 依然运行,依然存在强引用链,value不会被回收,而这块value永远不会被访问到,导致value内存泄漏 + 使用完 ThreadLocal ,threadLocal Ref 被回收,ThreadLocalMap 只持有 ThreadLocal 的弱引用,所以threadlocal 也可以被回收,此时 Entry 中的 key=null。但没有手动删除这个 Entry 或者 CurrentThread 依然运行,依然存在强引用链,value 不会被回收,而这块 value 永远不会被访问到,导致 value 内存泄漏 @@ -25240,10 +25261,8 @@ UML 从目标系统的不同角度出发,定义了用例图、类图、对象 ```java public final Object readObject() throws IOException, ClassNotFoundException{ - int outerHandle = passHandle; - try { - Object obj = readObject0(false);//重点查看readObject0方法 - } + //... + Object obj = readObject0(false);//重点查看readObject0方法 } private Object readObject0(boolean unshared) throws IOException { From 7f3c228b609ad33b5761fb71751c0e214451f264 Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 10 Jun 2021 08:50:07 +0800 Subject: [PATCH 045/242] Update README --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3372f3a..f894a1b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ **Java** 学习笔记,记录着作者从编程入门到深入学习的所有知识,每次学有所获就会更新笔记,所有的知识都有借鉴其他前辈的博客和文章,排版布局美观整洁,使用 Typora 阅读效果更佳,希望对各位朋友有所帮助。 -注:所有的知识不保证权威性,如果各位朋友发现错误,非常欢迎与我讨论。 +声明:所有的知识不保证权威性,如果各位朋友发现错误,非常欢迎与我讨论。 + +邮箱:zhyzhyang@sina.com 内容说明: From cc8a7470c5a2df4518ccdd091819f7b0957561c3 Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 10 Jun 2021 15:10:03 +0800 Subject: [PATCH 046/242] Update README --- Frame.md | 1422 ----------------------------------------------------- README.md | 7 +- 2 files changed, 3 insertions(+), 1426 deletions(-) delete mode 100644 Frame.md diff --git a/Frame.md b/Frame.md deleted file mode 100644 index 9411247..0000000 --- a/Frame.md +++ /dev/null @@ -1,1422 +0,0 @@ -# Maven - -## 基本介绍 - -### Mvn概述 - -Maven:本质是一个项目管理工具,将项目开发和管理过程抽象成一个项目对象模型(POM) - -POM:Project Object Model,项目对象模型。Maven是用Java语言编写的,它管理的东西以面向对象的形式进行设计,最终把一个项目看成一个对象,而这个对象叫做POM - -pom.xml:Maven需要一个pom.xml文件,Maven通过加载这个配置文件可以知道项目的相关信息,这个文件代表就一个项目。如果我们做8个项目,对应的是8个pom.xml文件 - -依赖管理:Maven对项目所有依赖资源的一种管理,它和项目之间是一种双向关系,即做项目时可以管理所需要的其他资源,当其他项目需要依赖我们项目时,Maven也会把我们的项目当作一种资源去进行管理。 - -管理资源的存储位置:本地仓库,私服,中央仓库 - -![](https://gitee.com/seazean/images/raw/master/Frame/Maven介绍.png) - - - - - -### Mvn作用 - -* 项目构建:提供标准的,跨平台的自动化构建项目的方式 - -* 依赖管理:方便快捷的管理项目依赖的资源(jar包),避免资源间的版本冲突等问题 - -* 统一开发结构:提供标准的,统一的项目开发结构 - - ![](https://gitee.com/seazean/images/raw/master/Frame/Maven标准结构.png) - -各目录存放资源类型说明: - -* src/main/java:项目java源码 - -* src/main/resources:项目的相关配置文件(比如mybatis配置,xml映射配置,自定义配置文件等) - -* src/main/webapp:web资源(比如html,css,js等) - -* src/test/java:测试代码 - -* src/test/resources:测试相关配置文件 - -* src/pom.xml:项目pom文件 - - - - - -### 基础概念 - -* **仓库**:用于存储资源,主要是各种jar包。有本地仓库,私服,中央仓库,私服和中央仓库都是远程仓库 - - * 中央仓库:maven团队自身维护的仓库,属于开源的 - - * 私服:各公司/部门等小范围内存储资源的仓库,私服也可以从中央仓库获取资源,作用: - * 保存具有版权的资源,包含购买或自主研发的jar - * 一定范围内共享资源,能做到仅对内不对外开放 - - * 本地仓库:开发者自己电脑上存储资源的仓库,也可从远程仓库获取资源 - - - -* **坐标**:Maven中的坐标用于描述仓库中资源的位置 - - * 作用:使用唯一标识,唯一性定义资源位置,通过该标识可以将资源的识别与下载工作交由机器完成 - * https://mvnrepository.com:查询maven某一个资源的坐标,输入资源名称进行检索, - - * 依赖设置: - * groupId:定义当前资源隶属组织名称(通常是域名反写,如:org.mybatis;com.seazean) - * artifactId:定义当前资源的名称(通常是项目或模块名称,如:crm,sms) - * version:定义当前资源的版本号 - -* packaging:定义资源的打包方式,取值一般有如下三种 - - * jar:该资源打成jar包,默认是jar - - * war:该资源打成war包 - - * pom:该资源是一个父资源(表明使用maven分模块管理),打包时只生成一个pom.xml不生成jar或其他包结构 - - - - - -*** - - - -## 环境搭建 - -### 环境配置 - -Maven的官网:http://maven.apache.org/ - -下载安装:Maven是一个绿色软件,解压即安装 - -目录结构: - bin:可执行程序目录 - boot:maven自身的启动加载器 - conf:maven配置文件的存放目录 - lib:maven运行所需库的存放目录 - -配置MAVEN_HOME: - -![](https://gitee.com/seazean/images/raw/master/Frame/Maven配置环境变量.png) - - - -环境变量配置好之后需要测试环境配置结果,在DOS命令窗口下输入以下命令查看输出:`mvn -v` - - - -*** - - - -### 仓库配置 - -默认情况下maven本地仓库在系统盘当前用户目录下的`.m2/repository`,修改Maven的配置文件`conf/settings.xml`来修改仓库位置 - -* 修改本地仓库位置:找到标签,修改默认值 - - ```xml - - E:\Workspace\Java\Project\.m2\repository - ``` - - 注意:在仓库的同级目录即`.m2`也应该包含一个`settings.xml`配置文件,局部用户配置优先与全局配置 - - * 全局setting定义了Maven的公共配置 - * 用户setting定义了当前用户的配置 - -* 修改远程仓库:在配置文件中找到``标签,在这组标签下添加国内镜像 - - ```xml - - nexus-aliyun - central - Nexus aliyun - http://maven.aliyun.com/nexus/content/groups/public - - ``` - -* 修改默认JDK:在配置文件中找到``标签,添加配置: - - ```xml - - jdk-10 - - true - 10 - - - UTF-8 - 10 - 10 - - - ``` - - - - - -*** - - - - - -## 项目搭建 - -### 手动搭建 - -1. 在E盘下创建目录`mvnproject`并进入该目录,作为我们的操作目录 - -2. 创建我们的maven项目,创建一个目录`project-java`作为我们的项目文件夹,并进入到该目录 - -3. 创建java代码(源代码)所在目录,即创建`src/main/java` - -4. 创建配置文件所在目录,即创建`src/main/resources` - -5. 创建测试源代码所在目录,即创建`src/test/java` - -6. 创建测试存放配置文件存放目录,即`src/test/resources` - -7. 在`src/main/java`中创建一个包(注意在windos文件夹下就是创建目录)`demo`,在该目录下创建`Demo.java`文件,作为演示所需java程序,内容如下 - - ```java - package demo; - public class Demo{ - public String say(String name){ - System.out.println("hello "+name); - return "hello "+name; - } - } - ``` - -8. 在`src/test/java`中创建一个测试包(目录)`demo`,在该包下创建测试程序`DemoTest.java` - - ```java - package demo; - import org.junit.*; - public class DemoTest{ - @Test - public void testSay(){ - Demo d = new Demo(); - String ret = d.say("maven"); - Assert.assertEquals("hello maven",ret); - } - } - ``` - -9. **在`project-java/src`下创建`pom.xml`文件,格式如下:** - - ```xml - - - - - 4.0.0 - - jar - - - demo - - project-java - - 1.0 - - - - - - junit - junit - 4.12 - - - - ``` - -10. 搭建好了maven的项目结构,通过maven来构建项目 - maven的构建命令以`mvn`开头,后面添加功能参数,可以一次性执行多个命令,用空格分离 - `mvn compile`:编译 - `mvn clean`:清理 - `mvn test`:测试 - `mvn package`:打包 - `mvn install`:安装到本地仓库 - - 注意:执行某一条命令,则会把前面所有的都执行一遍 - - - -*** - - - -### 插件构建 - -![](https://gitee.com/seazean/images/raw/master/Frame/Maven-插件构建.png) - - - -*** - - - -### IDEA搭建 - -#### 不用原型 - -1. 在IDEA中配置Maven,选择maven3.6.1防止依赖问题 - IDEA配置Maven - -2. 创建Maven,New Module --> Maven --> 不选中Create from archetype - -3. 填写项目的坐标 - GroupId:demo - ArtifactId:project-java - -4. 查看各目录颜色标记是否正确 - - ![](https://gitee.com/seazean/images/raw/master/Frame/IDEA创建Maven目录结构.png) - -5. IDEA右侧侧栏有Maven Project,打开后有Lifecycle生命周期 - - ![](https://gitee.com/seazean/images/raw/master/Frame/IDEA-Maven生命周期.png) - -6. 自定义Maven命令:Run --> Edit Configurations --> 左上角 + --> Maven - - ![](https://gitee.com/seazean/images/raw/master/Frame/IDEA配置Maven命令.png) - - - - - -#### 使用原型 - -普通工程: - -1. 创建maven项目的时候选择使用原型骨架 - - ![](https://gitee.com/seazean/images/raw/master/Frame/IDEA创建Maven-quickstart.png) - -2. 创建完成后发现通过这种方式缺少一些目录,需要手动去补全目录,并且要对补全的目录进行标记 - - - -web工程: - -1. 选择web对应的原型骨架(选择maven开头的是简化的) - - ![](https://gitee.com/seazean/images/raw/master/Frame/IDEA创建Maven-webapp.png) - -2. 通过原型创建web项目得到的目录结构是不全的,因此需要我们自行补全,同时要标记正确 - -3. web工程创建之后需要启动运行,使用tomcat插件来运行项目,在`pom.xml`中添加插件的坐标: - - ```xml - - - - 4.0.0 - war - - web01 - demo - web01 - 1.0-SNAPSHOT - - - - - - - - - - - - org.apache.tomcat.maven - tomcat7-maven-plugin - 2.1 - - 80 - / - - - - - - ``` - -4. 插件配置以后,在IDEA右侧`maven-project`操作面板看到该插件,并且可以利用该插件启动项目 - web01-->Plugins-->tomcat7-->tomcat7:run - - - -*** - - - -## 依赖管理 - -### 依赖配置 - -依赖是指在当前项目中运行所需的jar,依赖配置的格式如下: - -```xml - - - - - - junit - - junit - - 4.12 - - -``` - - - -*** - - - -### 依赖传递 - -依赖具有传递性,分两种: - -* 直接依赖:在当前项目中通过依赖配置建立的依赖关系 - -* 间接依赖:被依赖的资源如果依赖其他资源,则表明当前项目间接依赖其他资源 - - 注意:直接依赖和间接依赖其实也是一个相对关系 - - - -依赖传递的冲突问题:在依赖传递过程中产生了冲突,有三种优先法则 - -* 路径优先:当依赖中出现相同资源时,层级越深,优先级越低,反之则越高 - -* 声明优先:当资源在相同层级被依赖时,配置顺序靠前的覆盖靠后的 - -* 特殊优先:当同级配置了相同资源的不同版本时,后配置的覆盖先配置的 - - - -**可选依赖:**对外隐藏当前所依赖的资源,不透明 - -```xml - - junit - junit - 4.11 - true - -``` - -**排除依赖:主动**断开依赖的资源,被排除的资源无需指定版本 - -```xml - - junit - junit - 4.12 - - - org.hamcrest - hamcrest-core - - - -``` - - - -*** - - - -### 依赖范围 - -依赖的jar默认情况可以在任何地方可用,可以通过`scope`标签设定其作用范围,有三种: - -* 主程序范围有效(src/main目录范围内) - -* 测试程序范围内有效(src/test目录范围内) - -* 是否参与打包(package指令范围内) - -`scope`标签的取值有四种:`compile,test,provided,runtime` - -![](https://gitee.com/seazean/images/raw/master/Frame/Maven依赖范围.png) - - - -**依赖范围的传递性:** - -![](https://gitee.com/seazean/images/raw/master/Frame/Maven依赖范围的传递性.png) - - - - - -*** - - - -## 生命周期 - -### 相关事件 - -Maven的构建生命周期描述的是一次构建过程经历了多少个事件 - -最常用的一套流程:compile --> test-compile --> test --> package --> install - -* clean:清理工作 - - * pre-clean:执行一些在clean之前的工作 - * clean:移除上一次构建产生的所有文件 - * post-clean:执行一些在clean之后立刻完成的工作 - -* default:核心工作,例如编译,测试,打包,部署等 - - 对于default生命周期,每个事件在执行之前都会**将之前的所有事件依次执行一遍** - - ![](https://gitee.com/seazean/images/raw/master/Frame/Maven-default生命周期.png) - -* site:产生报告,发布站点等 - - * pre-site:执行一些在生成站点文档之前的工作 - * site:生成项目的站点文档 - * post-site:执行一些在生成站点文档之后完成的工作,并为部署做准备 - * site-deploy:将生成的站点文档部署到特定的服务器上 - - - -*** - - - -### 执行事件 - -Maven的插件用来执行生命周期中的相关事件 - -- 插件与生命周期内的阶段绑定,在执行到对应生命周期时执行对应的插件 - -- maven默认在各个生命周期上都绑定了预先设定的插件来完成相应功能 - -- 插件还可以完成一些自定义功能 - - ```xml - - - - org.apache.maven.plugins - maven-source-plugin - 2.2.1 - - - - - - - jar - - test-jar - - - generate-test-resources - - - - - - ``` - - - -*** - - - -## 模块开发 - -### 拆分 - -工程模块与模块划分: - -![](https://gitee.com/seazean/images/raw/master/Frame/Maven模块划分.png) - -* ssm_pojo拆分 - - * 新建模块,拷贝原始项目中对应的相关内容到ssm_pojo模块中 - * 实体类(User) - * 配置文件(无) - -* ssm_dao拆分 - - * 新建模块 - - * 拷贝原始项目中对应的相关内容到ssm_dao模块中 - - - 数据层接口(UserDao) - - - 配置文件:保留与数据层相关配置文件(3个) - - - 注意:分页插件在配置中与SqlSessionFactoryBean绑定,需要保留 - - - pom.xml:引入数据层相关坐标即可,删除springmvc相关坐标 - - - spring - - mybatis - - spring 整合mybatis - - mysql - - druid - - pagehelper - - 直接依赖ssm_pojo(对ssm_pojo模块执行install指令,将其安装到本地仓库) - - ```xml - - - - demo - ssm_pojo - 1.0-SNAPSHOT - - - - - - - - - - - ``` - -* ssm_service拆分 - - * 新建模块 - * 拷贝原始项目中对应的相关内容到ssm_service模块中 - - - 业务层接口与实现类(UserService、UserServiceImpl) - - 配置文件:保留与数据层相关配置文件(1个) - - pom.xml:引入数据层相关坐标即可,删除springmvc相关坐标 - - - spring - - - junit - - - spring 整合junit - - - 直接依赖ssm_dao(对ssm_dao模块执行install指令,将其安装到本地仓库) - - - 间接依赖ssm_pojo(由ssm_dao模块负责依赖关系的建立) - - 修改service模块spring核心配置文件名,添加模块名称,格式:applicationContext-service.xml - - 修改dao模块spring核心配置文件名,添加模块名称,格式:applicationContext-dao.xml - - 修改单元测试引入的配置文件名称,由单个文件修改为多个文件 - -* ssm_control拆分 - - * 新建模块(使用webapp模板) - - * 拷贝原始项目中对应的相关内容到ssm_controller模块中 - - - 现层控制器类与相关设置类(UserController、异常相关……) - - - 配置文件:保留与表现层相关配置文件(1个)、服务器相关配置文件(1个) - - - pom.xml:引入数据层相关坐标即可,删除springmvc相关坐标 - - - spring - - - springmvc - - - jackson - - - servlet - - - tomcat服务器插件 - - - 直接依赖ssm_service(对ssm_service模块执行install指令,将其安装到本地仓库) - - - 间接依赖ssm_dao、ssm_pojo - - ```xml - - - - demo - ssm_service - 1.0-SNAPSHOT - - - - - - - - - - - - - - - - ``` - - - 修改web.xml配置文件中加载spring环境的配置文件名称,使用*通配,加载所有applicationContext-开始的配置文件: - - ```xml - - - contextConfigLocation - classpath*:applicationContext-*.xml - - ``` - - - spring-mvc - - ```xml - - - ``` - - - -*** - - - -### 聚合 - -作用:聚合用于快速构建maven工程,一次性构建多个项目/模块。 - -制作方式: - -- 创建一个空模块,打包类型定义为pom - - ```xml - pom - ``` - -- 定义当前模块进行构建操作时关联的其他模块名称 - - ```xml - - - 4.0.0 - - demo - ssm - 1.0-SNAPSHOT - - - pom - - - - - ../ssm_pojo - ../ssm_dao - ../ssm_service - ../ssm_controller - - - ``` - -注意事项:参与聚合操作的模块最终执行顺序与模块间的依赖关系有关,与配置顺序无关 - - - -*** - - - -### 继承 - -作用:通过继承可以实现在子工程中沿用父工程中的配置 - -- maven中的继承与java中的继承相似,在子工程中配置继承关系 - -制作方式: - -- 在子工程中声明其父工程坐标与对应的位置 - - ```xml - - - com.seazean - ssm - 1.0-SNAPSHOT - - ../ssm/pom.xml - - ``` - -- 继承依赖的定义:在父工程中定义依赖管理 - - ```xml - - - - - - - org.springframework - spring-context - 5.1.9.RELEASE - - - - - ``` - -- 继承依赖的使用:在子工程中定义依赖关系,**无需声明依赖版本**,版本参照父工程中依赖的版本 - - ```xml - - - - org.springframework - spring-context - - - ``` - -- 继承的资源: - - ```xml - groupId:项目组ID,项目坐标的核心元素 - version:项目版本,项目坐标的核心因素 - description:项目的描述信息 - organization:项目的组织信息 - inceptionYear:项目的创始年份 - url:项目的URL地址 - developers:项目的开发者信息 - contributors:项目的贡献者信息 - distributionManagement:项目的部署配置 - issueManagement:项目的缺陷跟踪系统信息 - ciManagement:项目的持续集成系统信息 - scm:项目的版本控制系统西溪 - malilingLists:项目的邮件列表信息 - properties:自定义的Maven属性 - dependencies:项目的依赖配置 - dependencyManagement:项目的依赖管理配置 - repositories:项目的仓库配置 - build:包括项目的源码目录配置、输出目录配置、插件配置、插件管理配置等 - reporting:包括项目的报告输出目录配置、报告插件配置等 - ``` - -- 继承与聚合: - - 作用: - - - 聚合用于快速构建项目 - - - 继承用于快速配置 - - 相同点: - - - 聚合与继承的pom.xml文件打包方式均为pom,可以将两种关系制作到同一个pom文件中 - - - 聚合与继承均属于设计型模块,并无实际的模块内容 - - 不同点: - - - 聚合是在当前模块中配置关系,聚合可以感知到参与聚合的模块有哪些 - - - 继承是在子模块中配置关系,父模块无法感知哪些子模块继承了自己 - - - -*** - - - -### 属性 - -* 版本统一的重要性: - - ![](https://gitee.com/seazean/images/raw/master/Frame/Maven版本统一的重要性.png) - -* 属性类别: - - 1.自定义属性 2.内置属性 3.Setting属性 4.Java系统属性 5.环境变量属性 - -* 自定义属性: - - 作用:等同于定义变量,方便统一维护 - - 定义格式: - - ```xml - - - 5.1.9.RELEASE - 4.12 - - ``` - - - 聚合与继承的pom.xml文件打包方式均为pom,可以将两种关系制作到同一个pom文件中 - - - 聚合与继承均属于设计型模块,并无实际的模块内容 - - 调用格式: - - ```xml - - org.springframework - spring-context - ${spring.version} - - ``` - -* 内置属性: - - 作用:使用maven内置属性,快速配置 - - 调用格式: - - ```xml - ${project.basedir} or ${project.basedir} - ${version} or ${project.version} - ``` - - * vresion是1.0-SNAPSHOT - - ```xml - demo - ssm - 1.0-SNAPSHOT - ``` - -* Setting属性 - - - 使用Maven配置文件setting.xml中的标签属性,用于动态配置 - - 调用格式: - - ```xml - ${settings.localRepository} - ``` - -* Java系统属性: - - 作用:读取Java系统属性 - - 调用格式: - - ``` - ${user.home} - ``` - - 系统属性查询方式 cmd命令: - - ```sh - mvn help:system - ``` - -* 环境变量属性 - - 作用:使用Maven配置文件setting.xml中的标签属性,用于动态配置 - - 调用格式: - - ``` - ${env.JAVA_HOME} - ``` - - 环境变量属性查询方式: - - ``` - mvn help:system - ``` - - - - -*** - - - -### 工程版本 - -SNAPSHOT(快照版本) - -- 项目开发过程中,为方便团队成员合作,解决模块间相互依赖和时时更新的问题,开发者对每个模块进行构建的时候,输出的临时性版本叫快照版本(测试阶段版本) - -- 快照版本会随着开发的进展不断更新 - -RELEASE(发布版本) - -- 项目开发到进入阶段里程碑后,向团队外部发布较为稳定的版本,这种版本所对应的构件文件是稳定的,即便进行功能的后续开发,也不会改变当前发布版本内容,这种版本称为发布版本 - -约定规范: - -- <主版本>.<次版本>.<增量版本>.<里程碑版本> - -- 主版本:表示项目重大架构的变更,如:spring5相较于spring4的迭代 - -- 次版本:表示有较大的功能增加和变化,或者全面系统地修复漏洞 - -- 增量版本:表示有重大漏洞的修复 - -- 里程碑版本:表明一个版本的里程碑(版本内部)。这样的版本同下一个正式版本相比,相对来说不是很稳定,有待更多的测试 - -范例: - -- 5.1.9.RELEASE - - - -*** - - - - - -### 资源配置 - -作用:在任意配置文件中加载pom文件中定义的属性 - -* 父文件pom.xml - - ```xml - - jdbc:mysql://192.168.0.137:3306/ssm_db?useSSL=false - - ``` - -- 开启配置文件加载pom属性: - - ```xml - - - - - ${project.basedir}/src/main/resources - - true - - - ``` - -* properties文件中调用格式: - - ```xml-dtd - jdbc.driver=com.mysql.jdbc.Driver - jdbc.url=${jdbc.url} - jdbc.username=root - jdbc.password=123456 - ``` - - - -*** - - - -### 多环境配置 - -* 环境配置 - - ```xml - - - - - - pro_env - - - jdbc:mysql://127.1.1.1:3306/ssm_db - - - - true - - - - - dev_env - …… - - - ``` - -* 加载指定环境 - - 作用:加载指定环境配置 - - 调用格式: - - ``` - mvn 指令 –P 环境定义id - ``` - - 范例: - - ``` - mvn install –P pro_env - ``` - - - - -*** - - - -## 跳过测试 - -### 命令跳过 - -命令: - -``` -mvn 指令 –D skipTests -``` - -注意事项:执行的指令生命周期必须包含测试环节 - - - -### IEDA界面 - -![](https://gitee.com/seazean/images/raw/master/Frame/IDEA使用界面操作跳过测试.png) - - - -### 配置跳过 - -```xml - - - maven-surefire-plugin - 2.22.1 - - true - - **/User*Test.java - - - **/User*TestCase.java - - - -``` - - - -*** - - - -## 私服 - -### Nexus - -Nexus是Sonatype公司的一款maven私服产品 - -下载地址:https://help.sonatype.com/repomanager3/download - -启动服务器(命令行启动): - -``` -nexus.exe /run nexus -``` - -访问服务器(默认端口:8081): - -``` -http://localhost:8081 -``` - -修改基础配置信息 - -- 安装路径下etc目录中nexus-default.properties文件保存有nexus基础配置信息,例如默认访问端口 - -修改服务器运行配置信息 - -- 安装路径下bin目录中nexus.vmoptions文件保存有nexus服务器启动对应的配置信息,例如默认占用内存空间 - - - -*** - - - -### 资源操作 - -![](https://gitee.com/seazean/images/raw/master/Frame/Maven私服资源获取.png) - - - -仓库分类: - -* 宿主仓库hosted - * 保存无法从中央仓库获取的资源 - * 自主研发 - * 第三方非开源项目 - -* 代理仓库proxy - * 代理远程仓库,通过nexus访问其他公共仓库,例如中央仓库 - -* 仓库组group - * 将若干个仓库组成一个群组,简化配置 - * 仓库组不能保存资源,属于设计型仓库 - - - -资源上传,上传资源时提供对应的信息 - -- 保存的位置(宿主仓库) - -- 资源文件 - -- 对应坐标 - - - -*** - - - -### IDEA操作 - -#### 上传下载 - -![](https://gitee.com/seazean/images/raw/master/Frame/IDEA环境中资源上传与下载.png) - - - -*** - - - -#### 访问私服配置 - -##### 本地仓库访问私服 - -配置本地仓库访问私服的权限(setting.xml) - -```xml - - - heima-release - admin - admin - - - heima-snapshots - admin - admin - - -``` - -配置本地仓库资源来源(setting.xml) - -```xml - - - nexus-heima - * - http://localhost:8081/repository/maven-public/ - - -``` - - - -##### 项目工程访问私服 - -配置当前项目访问私服上传资源的保存位置(pom.xml) - -```xml - - - heima-release - http://localhost:8081/repository/heima-release/ - - - heima-snapshots - http://localhost:8081/repository/heima-snapshots/ - - -``` - -发布资源到私服命令 - -``` -mvn deploy -``` - - - - - -*** - - - -## 日志 - -### Log4j - -程序中的日志可以用来记录程序在运行时候的详情,并可以进行永久存储。 - -| | 输出语句 | 日志技术 | -| -------- | -------------------------- | ---------------------------------------- | -| 取消日志 | 需要修改代码,灵活性比较差 | 不需要修改代码,灵活性比较好 | -| 输出位置 | 只能是控制台 | 可以将日志信息写入到文件或者数据库中 | -| 多线程 | 和业务代码处于一个线程中 | 多线程方式记录日志,不影响业务代码的性能 | - -Log4j是Apache的一个开源项目。 -使用Log4j,通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。我们可以控制日志信息输送的目的地是控制台、文件等位置,也可以控制每一条日志的输出格式。 - - - - - -*** - - - -### 配置文件 - -配置文件的三个核心: - -+ 配置根Logger - - + 格式:log4j.rootLogger=日志级别,appenderName1,appenderName2,… - - + 日志级别:常见的五个级别:**DEBUG < INFO < WARN < ERROR < FATAL**(可以自定义) - Log4j规则:只输出级别不低于设定级别的日志信息 - - + appenderName1:指定日志信息要输出地址。可以同时指定多个输出目的地,用逗号隔开: - - 例如:log4j.rootLogger=INFO,ca,fa - -+ Appenders(输出源):日志要输出的地方,如控制台(Console)、文件(Files)等 - - + Appenders取值: - + org.apache.log4j.ConsoleAppender(控制台) - + org.apache.log4j.FileAppender(文件) - - + ConsoleAppender常用参数 - + `ImmediateFlush=true`:表示所有消息都会被立即输出,设为false则不输出,默认值是true。 - + `Target=System.err`:默认值是System.out - + FileAppender常用的选项 - + `ImmediateFlush=true`:表示所有消息都会被立即输出。设为false则不输出,默认值是true - - + `Append=false`:true表示将消息添加到指定文件中,原来的消息不覆盖。默认值是true - - + `File=E:/logs/logging.log4j`:指定消息输出到logging.log4j文件中 - -+ Layouts(布局):日志输出的格式,常用的布局管理器: - - + org.apache.log4j.PatternLayout(可以灵活地指定布局模式) - -+ org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串) - -+ org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等信息) - -+ PatternLayout常用的选项 - - - - -*** - - - -### 日志应用 - -* log4j的配置文件,名字为log4j.properties, 放在src根目录下 - - ```properties - log4j.rootLogger=debug,my,fileAppender - - ### direct log messages to my ### - log4j.appender.my=org.apache.log4j.ConsoleAppender - log4j.appender.my.ImmediateFlush = true - log4j.appender.my.Target=System.out - log4j.appender.my.layout=org.apache.log4j.PatternLayout - log4j.appender.my.layout.ConversionPattern=%d %t %5p %c{1}:%L - %m%n - - # fileAppender演示 - log4j.appender.fileAppender=org.apache.log4j.FileAppender - log4j.appender.fileAppender.ImmediateFlush = true - log4j.appender.fileAppender.Append=true - log4j.appender.fileAppender.File=E:/log4j-log.log - log4j.appender.fileAppender.layout=org.apache.log4j.PatternLayout - log4j.appender.fileAppender.layout.ConversionPattern=%d %5p %c{1}:%L - %m%n - ``` - -* 测试类 - - ```java - // 测试类 - public class Log4JTest01 { - - //使用log4j的api来获取日志的对象 - //弊端:如果以后我们更换日志的实现类,那么下面的代码就需要跟着改 - //不推荐使用 - //private static final Logger LOGGER = Logger.getLogger(Log4JTest01.class); - - //使用slf4j里面的api来获取日志的对象 - //好处:如果以后我们更换日志的实现类,那么下面的代码不需要跟着修改 - //推荐使用 - private static final Logger LOGGER = LoggerFactory.getLogger(Log4JTest01.class); - - public static void main(String[] args) { - //1.导入jar包 - //2.编写配置文件 - //3.在代码中获取日志的对象 - //4.按照日志级别设置日志信息 - LOGGER.debug("debug级别的日志"); - LOGGER.info("info级别的日志"); - LOGGER.warn("warn级别的日志"); - LOGGER.error("error级别的日志"); - } - } - ``` - - - - - -*** - - - - - -# Netty - -(暂未学习) - diff --git a/README.md b/README.md index f894a1b..817da9e 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,13 @@ 声明:所有的知识不保证权威性,如果各位朋友发现错误,非常欢迎与我讨论。 -邮箱:zhyzhyang@sina.com +邮箱:imseazean@gmail.com 内容说明: -* DB:MySQL、JDBC、Redis -* Frame:Maven +* DB:MySQL、Redis * Issue:Interview Questions * Java:JavaSE、JVM、JUC、Design Pattern * SSM:MyBatis、Spring、SpringMVC、SpringBoot * Tool:Git、Linux、Docker -* Web:HTML、CSS、Servlet、JavaScript +* Web:HTML、CSS、HTTP、Servlet、JavaScript From 22fd1af7125953eec6c5674e3a98e5549513b67c Mon Sep 17 00:00:00 2001 From: Seazean Date: Fri, 11 Jun 2021 21:21:41 +0800 Subject: [PATCH 047/242] Update Java Notes --- DB.md | 46 ++++++--- Issue.md | 2 +- Java.md | 307 ++++++++++++++++++++++++++++++++++--------------------- Tool.md | 50 +++++---- 4 files changed, 251 insertions(+), 154 deletions(-) diff --git a/DB.md b/DB.md index 316e7a7..eb8fb9f 100644 --- a/DB.md +++ b/DB.md @@ -5504,7 +5504,7 @@ MySQL 的主从复制原理图: * 优化 SQL,避免慢 SQL,减少批量操作 * 提高从库机器的配置,减少主库写 binlog 和从库读 binlog 的效率差 * 尽量采用短的链路,主库和从库服务器的距离尽量要短,提升端口带宽,减少 binlog 传输的网络延时 -* 实时性要求的业务读强制走主库,从库只做灾备,备份 +* 实时性要求高的业务读强制走主库,从库只做灾备,备份 * 强制将写之后立马读的操作转移到主库,比如刚注册的用户,直接登录从库查询可能查询不到,先走主库登录 @@ -8219,7 +8219,7 @@ Redis为每个服务提供16个数据库,编码0-15,每个数据库之间的 #### 实现 -Redis字符串对象底层的数据结构实现主要是 int 和简单动态字符串SDS,涉及C语言相关,先不做记录 +Redis字符串对象底层的数据结构实现主要是 int 和简单动态字符串 SDS,涉及C语言相关,先不做记录 参考文章:https://www.cnblogs.com/hunternet/p/9957913.html @@ -8464,7 +8464,7 @@ list类型:保存多个数据,底层使用**双向链表**存储结构实现 ##### 链表结构 -Redis 链表为双向无环链表,使用 listNode 结构表示 +Redis 链表为**双向无环链表**,使用 listNode 结构表示 ```c typedef struct listNode @@ -8493,7 +8493,7 @@ typedef struct listNode quicklist 实际上是 ziplist 和 linkedlist 的混合体,将 linkedlist 按段切分,每一段使用 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串接起来 -![](https://gitee.com/seazean/images/raw/master/DB/Redis-快速列表数据结构.png) + @@ -8723,7 +8723,7 @@ Redis 使用跳跃表作为有序集合键的底层实现之一,如果一个 * 基于单向链表加索引的方式实现 - Redis 的跳跃表实现由 zskiplist 和 zskiplistnode 两个结构组成,其中 zskiplist 用于保存跳跃表信息(比如表头节点、表尾节点、长度),而 zskiplistnode 则用于表示跳跃表节点 -- Redis 每个跳跃表节点的层高都是 1 至 32 之间的随机数 +- Redis 每个跳跃表节点的层高都是 1 至 32 之间的随机数(Redis5之后最大层数为64) - 在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的成员对象必须是唯一的。跳跃表中的节点按照分值大小进行排序,当分值相同时节点按照成员对象的大小进行排序 ![](https://gitee.com/seazean/images/raw/master/DB/Redis-跳跃表数据结构.png) @@ -9129,7 +9129,7 @@ public JedisPool(GenericObjectPoolConfig poolConfig, String host, int port) { -### 可视化工具 +### 可视化 Redis Desktop Manager @@ -9956,9 +9956,7 @@ TTL 返回的值有三种情况:正数,-1,-2 no-enviction #禁止驱逐数据(redis4.0中默认策略),会引发OOM(Out Of Memory) ``` -数据淘汰策略配置依据: - - 使用INFO命令输出监控信息,查询缓存 hit 和 miss 的次数,根据业务需求调优Redis配置 +数据淘汰策略配置依据:使用INFO命令输出监控信息,查询缓存 hit 和 miss 的次数,根据需求调优 Redis 配置 @@ -10819,6 +10817,18 @@ sentinel 在通知阶段不断的去获取 master/slave 的信息,然后在各 ## 缓存方案 +### 缓存本质 + +弥补 CPU 的高算力和 IO 的慢读写之间巨大的鸿沟 + + + + + +*** + + + ### 缓存模式 #### 旁路缓存 @@ -10830,7 +10840,7 @@ Cache Aside Pattern 中服务端需要同时维系 DB 和 cache,并且是以 D * 写操作:先更新 DB,然后直接删除 cache * 读操作:从 cache 中读取数据,读取到就直接返回;读取不到就从 DB 中读取数据返回,并放到 cache -数据库和缓存的顺序问题: +时序导致的不一致问题: * 在写数据的过程中,不能先删除 cache 再更新 DB,因为会造成缓存的不一致。比如请求1先写数据A,请求2随后读数据A,当请求1删除 cache 后,请求2直接读取了 DB,此时请求1还没写入 DB @@ -10839,12 +10849,20 @@ Cache Aside Pattern 中服务端需要同时维系 DB 和 cache,并且是以 D 旁路缓存的缺点: * 首次请求数据一定不在 cache 的问题,一般采用缓存预热的方法,将热点数据可以提前放入cache 中 - * 写操作比较频繁的话导致 cache 中的数据会被频繁被删除,影响缓存命中率 - 数据库和缓存数据强一致场景 :更新DB的时候同样更新 cache,不过需要加一个锁来保证更新 cache 时不存在线程安全问题,这样可以增加命中率 - 可以短暂地允许数据库和缓存数据不一致场景:更新DB的时候同样更新 cache,但是给缓存加一个比较短的过期时间,这样的话就可以保证即使数据不一致影响也比较小 +缓存不一致的方法: + +* 数据库和缓存数据**强一致**场景 : + * 更新DB时同样更新 cache,加一个锁来保证更新 cache 时不存在线程安全问题,这样可以增加命中率 + * 延迟双删:先淘汰缓存再写数据库,休眠1秒再次淘汰缓存,可以将1秒内造成的缓存脏数据再次删除 + * CDC 同步:通过 canal 订阅 MySQL binlog 的变更上报给 Kafka,系统监听 Kafka 消息触发缓存失效 +* 可以短暂地允许数据库和缓存数据**不一致**场景:更新DB的时候同样更新 cache,但是给缓存加一个比较短的过期时间,这样的话就可以保证即使数据不一致影响也比较小 + + + +参考文章:http://cccboke.com/archives/2020-09-30-21-29-56 @@ -10872,7 +10890,7 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 #### 异步缓存 -异步缓存写入 Write Behind Pattern 由 cache 服务来负责 cache 和 DB 的读写,对比读写穿透不同的是 Write Behind Caching 是只更新缓存,不直接更新 DB,改为异步批量的方式来更新 DB +异步缓存写入 Write Behind Pattern 由 cache 服务来负责 cache 和 DB 的读写,对比读写穿透不同的是 Write Behind Caching 是只更新缓存,不直接更新 DB,改为**异步批量**的方式来更新 DB,可以减小写的成本 缺点:这种模式对数据一致性没有高要求,可能出现 cache 还没异步更新DB,服务就挂掉了 diff --git a/Issue.md b/Issue.md index 08f15c3..f7f0127 100644 --- a/Issue.md +++ b/Issue.md @@ -196,7 +196,7 @@ * 什么是CPU寻址? - 现代处理器使用的是一种称为虚拟寻址(Virtual Addressing) 的寻址方式。使用虚拟寻址,CPU 将虚拟(逻辑)地址翻译成物理地址,这样才能访问到真实的物理内存。 实际上完成虚拟地址转换为物理地址转换的硬件是 CPU 中含有一个被称为内存管理单元(Memory Management Unit, MMU)的硬件 + 现代处理器使用的是一种称为虚拟寻址(Virtual Addressing)的寻址方式。使用虚拟寻址 CPU 将虚拟(逻辑)地址翻译成物理地址,这样才能访问到真实的物理内存。 实际上完成虚拟地址转换为物理地址转换的硬件是 CPU 中含有一个被称为内存管理单元(Memory Management Unit, MMU)的硬件 虚拟地址空间好处:防止用户程序可以访问任意内存,寻址内存的每个字节,这样很容易破坏操作系统,造成操作系统崩溃 diff --git a/Java.md b/Java.md index 3b24c04..b3fb332 100644 --- a/Java.md +++ b/Java.md @@ -5144,32 +5144,31 @@ HashMap继承关系如下图所示: 11. 调整大小下一个容量的值计算方式为(容量*负载因子) ```java - //临界值,当实际大小(容量*负载因子)超过临界值时,会进行扩容 + //临界值,当实际大小(容量*负载因子)超过临界值时,会进行扩容 int threshold; ``` 12. **哈希表的加载因子(重点)** - ```java - // 加载因子 + ```java final float loadFactor; - ``` - - * 加载因子的概述 - - loadFactor加载因子,是用来衡量 HashMap 满的程度,表示**HashMap的疏密程度**,影响hash操作到同一个数组位置的概率,计算HashMap的实时加载因子的方法为:size/capacity,而不是占用桶的数量去除以capacity,capacity 是桶的数量,也就是 table 的长度length。 - - 当HashMap里面容纳的元素已经达到HashMap数组长度的75%时,表示HashMap拥挤,需要扩容,而扩容这个过程涉及到 rehash、复制数据等操作,非常消耗性能,所以开发中尽量减少扩容的次数,可以通过创建HashMap集合对象时指定初始容量来尽量避免。 - - ```java - HashMap(int initialCapacity, float loadFactor)//构造指定初始容量和加载因子的空HashMap - ``` - - * 为什么加载因子设置为0.75,初始化临界值是12? - - loadFactor太大导致查找元素效率低,存放的数据拥挤,太小导致数组的利用率低,存放的数据会很分散。loadFactor的默认值为**0.75f是官方给出的一个比较好的临界值**。 - - * **threshold**计算公式:capacity(数组长度默认16) * loadFactor(负载因子默认0.75)。这个值是当前已占用数组长度的最大值。**当Size>=threshold**的时候,那么就要考虑对数组的resize(扩容),这就是 **衡量数组是否需要扩增的一个标准**, 扩容后的 HashMap 容量是之前容量的**两倍**. + ``` + + * 加载因子的概述 + + loadFactor加载因子,是用来衡量 HashMap 满的程度,表示**HashMap的疏密程度**,影响hash操作到同一个数组位置的概率,计算HashMap的实时加载因子的方法为:size/capacity,而不是占用桶的数量去除以capacity,capacity 是桶的数量,也就是 table 的长度length。 + + 当HashMap里面容纳的元素已经达到HashMap数组长度的75%时,表示HashMap拥挤,需要扩容,而扩容这个过程涉及到 rehash、复制数据等操作,非常消耗性能,所以开发中尽量减少扩容的次数,可以通过创建HashMap集合对象时指定初始容量来尽量避免。 + + ```java + HashMap(int initialCapacity, float loadFactor)//构造指定初始容量和加载因子的空HashMap + ``` + + * 为什么加载因子设置为0.75,初始化临界值是12? + + loadFactor太大导致查找元素效率低,存放的数据拥挤,太小导致数组的利用率低,存放的数据会很分散。loadFactor的默认值为**0.75f是官方给出的一个比较好的临界值**。 + + * **threshold**计算公式:capacity(数组长度默认16) * loadFactor(负载因子默认0.75)。这个值是当前已占用数组长度的最大值。**当Size>=threshold**的时候,那么就要考虑对数组的resize(扩容),这就是 **衡量数组是否需要扩增的一个标准**, 扩容后的 HashMap 容量是之前容量的**两倍**. @@ -8320,9 +8319,9 @@ Java中的通信模型: ### I/O -#### 模型 +#### IO模型 -##### IO模型 +##### 五种模型 对于一个套接字上的输入操作,第一步是等待数据从网络中到达,当数据到达时被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区 @@ -8391,11 +8390,11 @@ 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。 +IO 复用让单个进程具有处理多个 I/O 事件的能力,又被称为 Event Driven I/O,即事件驱动 I/O -如果一个 Web 服务器没有 I/O 复用,那么每一个 Socket 连接都要创建一个线程去处理,如果同时有几万个连接,就需要创建相同数量的线程。相比于多进程和多线程技术,I/O 复用不需要进程线程创建和切换的开销,系统开销更小。 +如果一个 Web 服务器没有 I/O 复用,那么每一个 Socket 连接都要创建一个线程去处理,如果同时有几万个连接,就需要创建相同数量的线程。相比于多进程和多线程技术,I/O 复用不需要进程线程创建和切换的开销,系统开销更小 ![](https://gitee.com/seazean/images/raw/master/Java/IO模型-IO复用模型.png) @@ -8419,15 +8418,11 @@ IO 复用让单个进程具有处理多个 I/O 事件的能力,又被称为 Ev -#### 函数 -(待完善select和epoll函数,c语言函数) -select和poll差别不多,一个是数组一个是链表,因此poll的连接数理论上是不受限制的,而select是数组,数量受限。epoll多注册了一个函数ctrl事件监听。套接字是操作系统在管理,所以硬件的中断反馈给操作系统,进程从操作系统读取套接字的fd,所以这里有一个内存拷贝的过程,select和poll是轮询每个套接字,每次都要拷贝这个,而epoll则是在初始化拷贝,每次事件触发的时候直接响应,不用再复制。 - -**** +*** @@ -9076,7 +9071,7 @@ NIO 三大核心部分:**Channel( 通道) ,Buffer( 缓冲区), Selector( 选 * Buffer 缓冲区 - 缓冲区本质是一块可以写入数据、读取数据的内存,这块内存被包装成NIO Buffer对象,并且提供了相应的方法用来操作这块内存,相比较直接对数组的操作,Buffer 的 API 更加容易操作和管理 + 缓冲区本质是一块可以写入数据、读取数据的内存,**底层是一个数组**,这块内存被包装成NIO Buffer对象,并且提供了方法用来操作这块内存,相比较直接对数组的操作,Buffer 的 API 更加容易操作和管理 * Channel 通道 @@ -9094,7 +9089,7 @@ NIO的实现框架: * 一个线程对应 Selector , 一个 Selector 对应多个 Channel(连接) * 程序切换到哪个Channel 是由事件决定的,Event 是一个重要的概念 * Selector 会根据不同的事件,在各个通道上切换 -* Buffer 就是一个内存块 , **底层是一个数组** +* Buffer 是一个内存块 , 底层是一个数组 * 数据的读取写入是通过 Buffer 完成的 , BIO 中要么是输入流,或者是输出流,不能双向,NIO 的 Buffer 是可以读也可以写, flip() 切换 Buffer 的工作模式 Java NIO 系统的核心在于:通道和缓冲区,通道表示打开到 IO 设备(例如:文件、 套接字)的连接。若需要使用 NIO 系统,获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区,然后操作缓冲区,对数据进行处理。简而言之,Channel 负责传输, Buffer 负责存取数据 @@ -9246,16 +9241,49 @@ public class TestBuffer { ##### 直接内存 -Byte Buffer可以是两种类型,一种是基于直接内存(也就是非堆内存),另一种是非直接内存(也就是堆内存)。对于直接内存来说,JVM将会在IO操作上具有更高的性能,因为直接作用于本地系统的IO操作,而非直接内存,也就是堆内存中的数据,如果要作IO操作,会先从本进程内存复制到直接内存,再利用本地IO处理 +Byte Buffer 可以是两种类型,一种是基于直接内存(也就是非堆内存),另一种是非直接内存(也就是堆内存)。对于直接内存来说,JVM将会在IO操作上具有更高的性能,因为直接作用于本地系统的IO操作,而非直接内存,也就是堆内存中的数据,如果要作IO操作,会先从本进程内存复制到直接内存,再利用本地IO处理 + +直接内存创建 Buffer 对象:`static XxxBuffer allocateDirect(int capacity)` + +直接内存的分配与回收机制参考:JVM → 内存结构 → 本地内存 → 直接内存 + +堆外内存不受 JVM GC 控制,可以使用堆外内存进行通信,防止 GC 后缓冲区位置发生变化的情况,源码: -直接内存创建Buffer对象:`static XxxBuffer allocateDirect(int capacity)` +* SocketChannel#write(java.nio.ByteBuffer) → SocketChannelImpl#write(java.nio.ByteBuffer) + + ```java + public int write(ByteBuffer var1) throws IOException { + do { + var3 = IOUtil.write(this.fd, var1, -1L, nd); + } while(var3 == -3 && this.isOpen()); + } + ``` + +* IOUtil#write(java.io.FileDescriptor, java.nio.ByteBuffer, long, sun.nio.ch.NativeDispatcher) + + ```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 + ByteBuffer var8 = Util.getTemporaryDirectBuffer(var7); + var8.put(var1); + //... + //从堆外写到内核缓冲区 + int var9 = writeFromNativeBuffer(var0, var8, var2, var4); + } + } + ``` 数据流的角度: -* 非直接内存的作用链:本地IO-->直接内存-->非直接内存-->直接内存-->本地IO +* 非直接内存的作用链:本地IO → 直接内存 → 非直接内存 → 直接内存 → 本地IO * 直接内存是:本地IO → 直接内存 → 本地IO -JVM直接内存详解 +JVM 直接内存详解: @@ -9269,7 +9297,7 @@ JVM直接内存详解 ##### 共享内存 -FileChannel 提供 map 方法把文件映射到虚拟内存,通常情况可以映射整个文件,如果文件比较大,可以进行分段映射 +FileChannel 提供 map 方法把文件映射到虚拟内存,通常情况可以映射整个文件,如果文件比较大,可以进行分段映射,完成映射后对物理内存的操作会被同步到硬盘上 FileChannel 中的成员属性: @@ -9281,7 +9309,7 @@ FileChannel 中的成员属性: * position:文件映射时的起始位置 * `public final FileLock lock()`:获取此文件通道的排他锁 -MappedByteBuffer,可以让文件直接在内存(堆外内存)中进行修改,这种方式叫做内存映射,可以直接调用系统底层的缓存,没有JVM和系统之间的复制操作,提高了传输效率,作用: +MappedByteBuffer,可以让文件直接在内存(堆外内存)中进行修改,这种方式叫做内存映射,可以直接调用系统底层的缓存,没有 JVM 和系统之间的复制操作,提高了传输效率,作用: * 用在进程间的通信,能达到**共享内存页**的作用,但在高并发下要对文件内存进行加锁,防止出现读写内容混乱和不一致性,Java 提供了文件锁 FileLock,但在父/子进程中锁定后另一进程会一直等待,效率不高 * 读写那些太大而不能放进内存中的文件 @@ -9323,6 +9351,8 @@ public class MappedByteBufferTest { - read() 是系统调用,首先将文件从硬盘拷贝到内核空间的一个缓冲区,再将这些数据拷贝到用户空间,实际上进行了两次数据拷贝 - map() 也是系统调用,但没有进行数据拷贝,当缺页中断发生时,直接将文件从硬盘拷贝到用户空间,只进行了一次数据拷贝 +注意:mmap 的文件映射,在 Full GC 时才会进行释放,如果需要手动清除内存映射文件,可以反射调用sun.misc.Cleaner 方法 + 参考文章:https://www.jianshu.com/p/f90866dcbffc @@ -9783,16 +9813,6 @@ public class Client { -**** - - - -#### 零拷贝 - -(待更新) - - - *** @@ -9824,7 +9844,7 @@ AsynchronousSocketChannel、AsynchronousServerSocketChannel、AsynchronousFileCh ## 反射 -### Junit +### 测试框架 > 单元测试是指程序员写的测试代码给自己的类中的方法进行预期正确性的验证。 > 单元测试一旦写好了这些测试代码,就可以一直使用,可以实现一定程度上的自动化测试。 @@ -11489,16 +11509,16 @@ Java编译器输入的指令流是一种基于栈的指令集架构。因为跨 JVM的生命周期分为三个阶段,分别为:启动、运行、死亡。 -- **启动**:当启动一个Java程序时,通过引导类加载器(bootstrap class loader)创建一个初始类(initial class),对于拥有main函数的类就是JVM实例运行的起点 +- **启动**:当启动一个 Java 程序时,通过引导类加载器(bootstrap class loader)创建一个初始类(initial class),对于拥有 main 函数的类就是 JVM 实例运行的起点 - **运行**: - - main()方法是一个程序的初始起点,任何线程均可由在此处启动 - - 在JVM内部有两种线程类型,分别为:**用户线程和守护线程**,JVM通常使用的是守护线程,而main()和其他线程使用的是用户线程,守护线程会随着用户线程的结束而结束 - - 执行一个Java程序时,真真正正在执行的是一个Java虚拟机的进程 - - JVM有两种运行模式Server与Client,两种模式的区别在于:Client模式启动速度较快,Server模式启动较慢;但是启动进入稳定期长期运行之后Server模式的程序运行速度比Client要快很多 + - main() 方法是一个程序的初始起点,任何线程均可由在此处启动 + - 在 JVM 内部有两种线程类型,分别为:**用户线程和守护线程**,JVM通常使用的是守护线程,而main()和其他线程使用的是用户线程,守护线程会随着用户线程的结束而结束 + - 执行一个 Java 程序时,真真正正在执行的是一个 Java 虚拟机的进程 + - JVM 有两种运行模式 Server 与 Client,两种模式的区别在于:Client 模式启动速度较快,Server 模式启动较慢;但是启动进入稳定期长期运行之后 Server 模式的程序运行速度比 Client 要快很多 - **死亡**: - - 当程序中的用户线程都中止,JVM才会退出 + - 当程序中的用户线程都中止,JVM 才会退出 - 程序正常执行结束、程序异常或错误而异常终止、操作系统错误导致终止 - - 某线程调用Runtime类halt方法或System类exit方法,并且java安全管理器允许这次exit或halt操作 + - 线程调用 Runtime 类 halt 方法或 System 类 exit 方法,并且 java 安全管理器允许这次 exit 或 halt 操作 @@ -11717,13 +11737,13 @@ JNI:Java Native Interface,通过使用 Java本地接口书写程序,可以 * 不需要进行GC,与虚拟机栈类似,也是线程私有的,有 StackOverFlowError 和 OutOfMemoryError 异常 -* 虚拟机栈执行的是java方法,在HotSpot JVM中,直接将本地方法栈和虚拟机栈合二为一 +* 虚拟机栈执行的是 Java 方法,在 HotSpot JVM 中,直接将本地方法栈和虚拟机栈合二为一 * 本地方法一般是由其他语言编写,并且被编译为基于本机硬件和操作系统的程序 * 当某个线程调用一个本地方法时,就进入了不再受虚拟机限制的世界,和虚拟机拥有同样的权限 - * 本地方法可以通过本地方法接口来 **访问虚拟机内部的运行时数据区** + * 本地方法可以通过本地方法接口来**访问虚拟机内部的运行时数据区** * 直接从本地内存的堆中分配任意数量的内存 * 可以直接使用本地处理器中的寄存器 @@ -11926,15 +11946,15 @@ public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制 Direct Memory优点: -* Java 的 NIO 库允许Java程序使用直接内存,用于数据缓冲区,使用native函数直接分配堆外内存 +* Java 的 NIO 库允许 Java 程序使用直接内存,用于数据缓冲区,使用 native 函数直接分配堆外内存 * 读写性能高,读写频繁的场合可能会考虑使用直接内存 -* 大大提高IO性能,避免了在java堆和native堆来回复制数据 +* 大大提高IO性能,避免了在 Java 堆和 native 堆来回复制数据 直接内存缺点: * 分配回收成本较高,不受 JVM 内存回收管理 - * 可能导致OutOfMemoryError异常:OutOfMemoryError: Direct buffer memory +* 回收依赖 System.gc() 的调用,但这个调用 JVM 不保证执行、也不保证何时执行,行为是不可控的。程序一般需要自行管理,成对去调用 malloc、free 应用场景: @@ -11943,18 +11963,41 @@ Direct Memory优点: -##### 底层原理 +***** -工作流程: - - +##### 分配回收 + +DirectByteBuffer 源码分析: + +```java +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 { + public void run() { + unsafe.freeMemory(address); + //... + } +} +``` 分配和回收原理: -* 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法 -* ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory 来释放直接内存 +* 使用了 Unsafe 对象的 allocateMemory 方法完成直接内存的分配,setMemory 方法完成赋值 +* ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦 ByteBuffer 对象被垃圾回收,那么 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 Deallocator 的 run方法,最后通过 freeMemory 来释放直接内存 ```java /** @@ -12397,8 +12440,9 @@ public void localvarGC4() { - 虚拟机栈中局部变量表中引用的对象:各个线程被调用的方法中使用到的参数、局部变量等 - 本地方法栈中引用的对象 - 方法区中类静态属性引用的对象 -- 方法区中的常量引用的对象:字符串常量池(string Table)里的引用 -- 同步锁synchronized持有的对象 +- 方法区中的常量引用的对象 +- 字符串常量池(string Table)里的引用 +- 同步锁 synchronized 持有的对象 GC Roots说明: @@ -12544,7 +12588,7 @@ Java语言提供了对象终止(finalization)机制来允许开发人员提 -#### 四种引用 +#### 引用分析 无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关,Java 提供了四种强度不同的引用类型 @@ -12552,7 +12596,7 @@ Java语言提供了对象终止(finalization)机制来允许开发人员提 * 强引用可以直接访问目标对象 * 虚拟机宁愿抛出OOM异常,也不会回收强引用所指向对象 - * 强引用可能导致**内存泄漏**(引用计数法小节解释了什么是内存泄漏) + * 强引用可能导致**内存泄漏**(引用计数法章节解释了什么是内存泄漏) ```java Object obj = new Object();//使用 new 一个新对象的方式来创建强引用 @@ -12561,7 +12605,7 @@ Java语言提供了对象终止(finalization)机制来允许开发人员提 2. 软引用(SoftReference):被软引用关联的对象只有在内存不够的情况下才会被回收 * **仅(可能有强引用,一个对象可以被多个引用)**有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象 - * 配合引用队列来释放软**引用自身**,在构造软引用时,可以指定一个引用队列,当软引用对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况 + * 配合**引用队列来释放软引用自身**,在构造软引用时,可以指定一个引用队列,当软引用对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况 * 软引用通常用来实现内存敏感的缓存,比如高速缓存就有用到软引用;如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时不会耗尽内存 ```java @@ -12596,6 +12640,15 @@ Java语言提供了对象终止(finalization)机制来允许开发人员提 5. 终结器引用(finalization) + 引用的四种状态: + +* Active:激活,创建 ref 对象时就是激活状态 +* Pending:等待入队,所对应的强引用被GC,就要进入引用队列 +* Enqueued:入队了 + * 如果指定了 refQueue,pending 移动到 enqueued 状态,refQueue.poll 时进入失效状态 + * 如果没有指定 refQueue,直接到失效状态 +* Inactive:失效 + *** @@ -12640,7 +12693,6 @@ Java语言提供了对象终止(finalization)机制来允许开发人员提 算法缺点: - 标记和清除过程效率都不高 -- 进行GC的时候,需要停止整个应用程序,用户体验较差 - 会产生大量不连续的内存碎片,导致无法给大对象分配内存,需要维护一个空闲链表 @@ -12805,7 +12857,7 @@ Parallel Old收集器:是一个应用于老年代的并行垃圾回收器,** 对比其他回收器: * 其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间 -* Parallel目标是达到一个可控制的吞吐量,被称为“**吞吐量优先**”收集器 +* Parallel目标是达到一个可控制的吞吐量,被称为**吞吐量优先**收集器 * Parallel Scavenge对比ParNew拥有**自适应调节策略**,可以通过一个开关参数打开GC Ergonomics 应用场景: @@ -12856,7 +12908,7 @@ Par是Parallel并行的缩写,New:只能处理的是新生代 ![](https://gitee.com/seazean/images/raw/master/Java/JVM-ParNew收集器.png) -ParNew 是很多JVM运行在Server模式下新生代的默认垃圾收集器 +ParNew 是很多 JVM 运行在 Server 模式下新生代的默认垃圾收集器 - 对于新生代,回收次数频繁,使用并行方式高效 - 对于老年代,回收次数少,使用串行方式节省资源(CPU并行需要切换线程,串行可以省去切换线程的资源) @@ -12877,10 +12929,10 @@ CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿 - 初始标记:使用STW出现短暂停顿,仅标记一下 GC Roots 能直接关联到的对象,速度很快 - 并发标记:进行 GC Roots的直接关联对象开始遍历整个对象图,在整个回收过程中耗时最长,不需要STW,可以与用户线程一起并发运行 -- 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,比初始标记时间长但远比并发标记时间短,需要STW +- 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,比初始标记时间长但远比并发标记时间短,需要 STW - 并发清除:清除标记为可以回收对象,不需要移动存活对象,所以这个阶段可以与用户线程同时并发的 -Mark Sweep会造成内存碎片,还不把算法换成Mark Compact的原因: +Mark Sweep 会造成内存碎片,还不把算法换成 Mark Compact 的原因: * Mark Compact 算法会整理内存,导致用户线程使用的对象的地址改变,影响用户线程继续执行 @@ -12963,8 +13015,6 @@ G1对比其他处理器的优点: * 相比于 CMS GC,G1 未必能做到 CMS 在最好情况下的延时停顿,但是最差情况要好很多 - - G1垃圾收集器的缺点: * 相较于 CMS,G1 还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1 无论是为了垃圾收集产生的内存占用还是程序运行时的额外执行负载都要比 CMS 要高 @@ -12987,7 +13037,7 @@ G1垃圾收集器的缺点: -* 程序对Reference类型数据写操作时,产生一个 Write Barrier 暂时中断操作,检查该对象和Reference类型数据是否在不同的Region(其他收集器:检查老年代对象是否引用了新生代对象),不同便通过 CardTable 把相关引用信息记录到Reference类型所属的 Region 的 Remembered Set 之中 +* 程序对 Reference 类型数据写操作时,产生一个 Write Barrier 暂时中断操作,检查该对象和 Reference 类型数据是否在不同的 Region(其他收集器:检查老年代对象是否引用了新生代对象),不同便通过 CardTable 把相关引用信息记录到 Reference 类型所属的 Region 的 Remembered Set 之中 * 进行内存回收时,在 GC 根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏 @@ -12998,28 +13048,28 @@ G1垃圾收集器的缺点: ##### 工作原理 -G1中提供了三种垃圾回收模式:YoungGC、Mixed GC和FullGC,在不同的条件下被触发 +G1中提供了三种垃圾回收模式:YoungGC、Mixed GC 和 FullGC,在不同的条件下被触发 -顺时针:Young GC -> Young GC + Concurrent Mark -> Mixed GC顺序,进行垃圾回收 +顺时针:Young GC -> Young GC + Concurrent Mark -> Mixed GC 顺序,进行垃圾回收 * **Young GC**:发生在年轻代的GC算法,一般对象(除了巨型对象)都是在 eden region 中分配内存,当所有 eden region 被耗尽无法申请内存时,就会触发一次 young gc,G1停止应用程序的执行 (Stop-The-World),把活跃对象放入老年代,垃圾对象回收 **回收过程**: - 1. 扫描根:根引用连同RSet记录的外部引用作为扫描存活对象的入口 - 2. 更新RSet:处理 dirty card queue 更新RS,此后RSet准确的反映老年代对所在的内存分段中对象的引用 - * dirty card queue:类似缓存,产生了引用先记录在这里,然后更新到RSet - * 作用:产生引用直接更新RSet需要线程同步开销很大,使用队列性能好 + 1. 扫描根:根引用连同 RSet 记录的外部引用作为扫描存活对象的入口 + 2. 更新RSet:处理 dirty card queue 更新 RS,此后 RSet 准确的反映对象的引用关系 + * dirty card queue:类似缓存,产生了引用先记录在这里,然后更新到 RSet + * 作用:产生引用直接更新 RSet 需要线程同步开销很大,使用队列性能好 3. 处理RSet:识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象 4. 复制对象:Eden区内存段中存活的对象会被复制到 survivor 区,survivor 区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到 old 区中空的内存分段,如果 survivor 空间不够,Eden 空间的部分数据会直接晋升到老年代空间 5. 处理引用:处理 Soft,Weak,Phantom,JNI Weak 等引用,最终 Eden 空间的数据为空,GC 停止工作 * **并发标记过程**: - * 初始标记:标记从根节点直接可达的对象,这个阶段是STW的,并且会触发一次年轻代GC - * 根区域扫描 (Root Region Scanning):G1 扫描survivor区直接可达的老年代区域对象,并标记被引用的对象,这一过程必须在Young GC之前完成 + * 初始标记:标记从根节点直接可达的对象,这个阶段是 STW 的,并且会触发一次年轻代 GC + * 根区域扫描 (Root Region Scanning):G1 扫描 survivor 区直接可达的老年代区域对象,并标记被引用的对象,这一过程必须在 Young GC 之前完成 * 并发标记 (Concurrent Marking):在整个堆中进行并发标记(应用程序并发执行),可能被 YoungGC 中断。在并发标记阶段,**若发现区域对象中的所有对象都是垃圾,则这个区域会被立即回收(实时回收)**,同时并发标记过程中,会计算每个区域的对象活性,即区域中存活对象的比例 * 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这阶段需要停顿线程,但是可并行执行 * 筛选回收:并发清理阶段,首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率 @@ -18033,6 +18083,31 @@ CPU 处理器速度远远大于在主内存中的,为了解决速度差异, +#### 伪共享 + +**缓存以缓存行 cache line 为单位,每个缓存行对应着一块内存**,一般是 64 byte(8 个 long),在CPU从主存获取数据时,以 cache line 为单位加载,于是相邻的数据会一并加载到缓存中 + +缓存会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中,CPU 要保证数据的一致性,需要做到某个 CPU 核心更改了数据,其它 CPU 核心对应的**整个缓存行必须失效**,这就是伪共享 + + + +解决方法: + +* padding:通过填充,让数据落在不同的 cache line 中 + +* @Contended:原理参考 无锁 → Addr → 优化机制 → 伪共享 + +Linux查看CPU缓存行: + +* 命令:`cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size64` +* 内存地址格式:[高位组标记] [低位索引] [偏移量] + + + +*** + + + #### 缓存一致 缓存一致性:当多个处理器运算任务都涉及到同一块主内存区域的时候,将可能导致各自的缓存数据不一样 @@ -18061,14 +18136,6 @@ CPU 处理器速度远远大于在主内存中的,为了解决速度差异, 解决方法:各个处理器访问缓存时都遵循一些协议,在读写时要根据协议进行操作,协议主要有MSI、MESI等 -Linux查看CPU缓存行: - -* 命令:`cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size64` - -* 内存地址格式:[高位组标记] [低位索引] [偏移量] - -缓存行在 无锁 → LongAddr → 伪共享部分详解 - **** @@ -18837,15 +18904,25 @@ CAS底层实现是在一个循环中不断地尝试修改目标值,直到修 -#### 成员变量 +#### 优化机制 + +##### 分段机制 -**分段CAS机制**: +分段CAS机制: * 在发生竞争时,创建Cell数组用于将不同线程的操作离散(通过hash等算法映射)到不同的节点上 * 设置多个累加单元(会根据需要扩容,最大为CPU核数),Therad-0 累加 Cell[0],而 Thread-1 累加 Cell[1] 等,最后将结果汇总 * 在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能 -**自动分段迁移机制**:某个Cell的value执行CAS失败,就会自动寻找另一个Cell分段内的value值进行CAS操作 + + +*** + + + +##### 分段迁移 + +自动分段迁移机制:某个Cell的value执行CAS失败,就会自动寻找另一个Cell分段内的value值进行CAS操作 ```java // 累加单元数组, 懒惰初始化 @@ -18858,7 +18935,7 @@ transient volatile int cellsBusy; Cells占用内存是相对比较大的,是惰性加载的,在无竞争的情况下直接更新base域,在第一次发生竞争的时候(CAS失败)就会创建一个大小为2的cells数组,每次扩容都是加倍 -扩容数组等行为只能有一个线程执行,因此需要一个锁,这里通过CAS更新cellsBusy来实现一个简单的lock +扩容数组等行为只能有一个线程执行,因此需要一个锁,这里通过 CAS 更新 cellsBusy 来实现一个简单的lock CAS锁: @@ -18880,6 +18957,14 @@ public class LockCas { } ``` + + +*** + + + +##### 伪共享 + Cell为累加单元:数组访问索引是通过Thread里的threadLocalRandomProbe域取模实现的,这个域是ThreadLocalRandom更新的 ```java @@ -18896,31 +18981,18 @@ Cell为累加单元:数组访问索引是通过Thread里的threadLocalRandomPr @sun.misc.Contended注解:防止缓存行伪共享 - - -*** - - - -#### 伪共享 - -CPU三层缓存结构: - - - -CPU 与 内存的速度差异很大,需要靠预读数据至缓存来提升效率,而**缓存以缓存行为单位,每个缓存行对应着一块内存**,一般是 64 byte(8 个 long)。缓存会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中,CPU 要保证数据的一致性,需要做到某个 CPU 核心更改了数据,其它 CPU 核心对应的**整个缓存行必须失效**,这就是伪共享 - -Cell 是数组形式,**在内存中是连续存储的**,一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value),因 -此缓存行可以存下 2 个的 Cell 对象,当Core-0 要修改 Cell[0]、Core-1 要修改 Cell[1],无论谁修改成功都会导致对方 Core 的缓存行失效,需要重新去主存获取 +Cell 是数组形式,**在内存中是连续存储的**,一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value),每一个 cache line 为 64 字节,因此缓存行可以存下 2 个的 Cell 对象,当Core-0 要修改 Cell[0]、Core-1 要修改 Cell[1],无论谁修改成功都会导致对方 Core 的缓存行失效,需要重新去主存获取 ![](https://gitee.com/seazean/images/raw/master/Java/JUC-伪共享1.png) -@sun.misc.Contended:在使用此注解的对象或字段的前后各增加 128 字节大小的padding,从而让 CPU 将对象预读至缓存时占用不同的缓存行,这样就不会造成对方缓存行的失效 +@sun.misc.Contended:在使用此注解的对象或字段的前后各增加 128 字节大小的padding,使用2倍于大多数硬件缓存行让 CPU 将对象预读至缓存时占用不同的缓存行,这样就不会造成对方缓存行的失效 ![](https://gitee.com/seazean/images/raw/master/Java/JUC-伪共享2.png) + + *** @@ -24085,6 +24157,7 @@ BaseHeader存储数据,headIndex存储索引,纵向上**所有索引指向 //索引层level,从1开始 int level = 1, max; //12.判断最低位前面有几个1,有几个leve就加几:0..0 0001 1110,这是4个,则1+4=5 + // 最大有30个就是 1 + 30 while (((rnd >>>= 1) & 1) != 0) ++level; Index idx = null;//最终指向z节点,就是添加的节点 diff --git a/Tool.md b/Tool.md index 27836f9..3a46a4f 100644 --- a/Tool.md +++ b/Tool.md @@ -79,19 +79,25 @@ GitLab (地址: https://about.gitlab.com/ )是一个用于仓库管理系 -## 获取仓库 +## 本地仓库 -* **本地仓库初始化** - 1.在电脑的任意位置创建一个空目录(例如repo1)作为我们的本地Git仓库 - 2.进入这个目录中,点击右键打开Git bash窗口 - 3.执行命令**git init** - - 如果在当前目录中看到.git文件夹(此文件夹为隐藏文件夹)则说明Git仓库创建成功 +### 获取仓库 +* **本地仓库初始化** + + 1. 在电脑的任意位置创建一个空目录(例如repo1)作为本地 Git 仓库 + + 2. 进入这个目录中,点击右键打开 Git bash 窗口 + + 3. 执行命令 **git init** + + 如果在当前目录中看到.git文件夹(此文件夹为隐藏文件夹)则说明Git仓库创建成功 + * **远程仓库克隆** - 通过Git提供的命令从远程仓库进行克隆,将远程仓库克隆到本地 - 命令:git clone 远程Git仓库地址 (HTTPS或者SSH) - + 通过 Git 提供的命令从远程仓库进行克隆,将远程仓库克隆到本地 + + 命令:git clone 远程 Git 仓库地址(HTTPS或者SSH) + * 生成SSH公钥步骤 * 设置账户 @@ -114,7 +120,7 @@ GitLab (地址: https://about.gitlab.com/ )是一个用于仓库管理系 -## 工作过程 +### 工作过程 ![](https://gitee.com/seazean/images/raw/master/Tool/Git基本工作流程.png) @@ -132,9 +138,9 @@ GitLab (地址: https://about.gitlab.com/ )是一个用于仓库管理系 -## 文件操作 +### 文件操作 -### 常用命令 +#### 常用命令 | 命令 | 作用 | | ----------------------- | ------------------------------------------------------------ | @@ -160,7 +166,7 @@ GitLab (地址: https://about.gitlab.com/ )是一个用于仓库管理系 -### 文件状态 +#### 文件状态 * Git工作目录下的文件存在两种状态: * untracked 未跟踪(未被纳入版本控制) @@ -180,7 +186,7 @@ GitLab (地址: https://about.gitlab.com/ )是一个用于仓库管理系 -### 文件忽略 +#### 文件忽略 一般我们总会有些文件无需纳入Git 的管理,也不希望它们总出现在未跟踪文件列表。 通常都是些自动生成的文件,比如日志文件,或者编译过程中创建的临时文件等。 在这种情况下,我们可以在工作目录中创建一个名为 .gitignore 的文件(文件名称固定),列出要忽略的文件模式。下面是一个示例: @@ -222,7 +228,7 @@ fetch是从远程仓库更新到本地仓库,pull是从远程仓库直接更 -### 查看远程仓库 +### 查看仓库 git remote:显示所有远程仓库的简写 @@ -232,13 +238,13 @@ git remote show :显示某个远程仓库的详细信息 -### 添加远程仓库 +### 添加仓库 git remote add :添加一个新的远程仓库,并指定一个可以引用的简写 -### 克隆远程仓库 +### 克隆仓库 git clone (HTTPS or SSH):克隆远程仓库 @@ -246,13 +252,13 @@ Git 克隆的是该 Git 仓库服务器上的几乎所有数据(包括日志 -### 删除远程仓库 +### 删除仓库 git remote rm :移除远程仓库,从本地移除远程仓库的记录,并不会影响到远程仓库 -### 抓取与拉取 +### 拉取仓库 git fetch 从远程仓库获取最新版本到本地仓库,不会自动merge @@ -262,7 +268,7 @@ git pull 从远程仓库获取最新版本并merge到 -### 推送 +### 推送仓库 git push 上传本地指定分支到远程仓库 @@ -384,7 +390,7 @@ git push origin :refs/tags/ tag-name :删除远程标签 -## IDEA集成Git +## IDEA操作 ### 环境配置 From f17caabed9c112c1c29d8d5aa33081dcbd4a3d77 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 12 Jun 2021 13:08:05 +0800 Subject: [PATCH 048/242] Update Java Notes --- Java.md | 441 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 437 insertions(+), 4 deletions(-) diff --git a/Java.md b/Java.md index b3fb332..c7ac38b 100644 --- a/Java.md +++ b/Java.md @@ -8418,7 +8418,440 @@ IO 复用让单个进程具有处理多个 I/O 事件的能力,又被称为 Ev +#### 多路复用 +##### select + +select 允许应用程序监视一组文件描述符,等待一个或者多个描述符成为就绪状态,从而完成 I/O 操作。 + +```c +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 有三种类型的描述符:readset、writeset、exceptset,对应读、写、异常条件的描述符集合 + +- n 是监测的 socket 的最大数量 + +- timeout 为超时参数,调用 select 会一直阻塞直到有描述符的事件到达或者等待的时间超过 timeout + + ```c + struct timeval{ + long tv_sec; //秒 + long tv_usec;//微秒 + } + ``` + + timeout == null:等待无限长的时间 + tv_sec == 0 && tv_usec == 0:获取后直接返回,不阻塞等待 + tv_sec != 0 || tv_usec != 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 +``` + +示例: + +```c +sockfd = socket(AF_INET, SOCK_STREAM, 0); +memset(&addr, 0, sizeof(addr))); +addr.sin_family = AF_INET; +addr.sin_port = htons(2000); +addr.sin_addr.s_addr = INADDR_ANY; +bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));//绑定连接 +listen(sockfd, 5);//监听5个端口 +for(i = 0; i < 5; i++) { + memset(&client, e, sizeof(client)); + addrlen = sizeof(client); + fds[i] = accept(sockfd, (struct sockaddr*)&client, &addrlen); + //将监听的对应的文件描述符fd存入fds:[3,4,5,6,7] + if(fds[i] > max) + max = fds[i]; +} +while(1) { + FD_ZERO(&rset);//置为0 + for(i = 0; i < 5; i++) { + FD_SET(fds[i], &rset);//对应位置1 [0001 1111 00.....] + } + print("round again"); + select(max + 1, &rset, NULL, NULL, NULL);//监听 + + for(i = 0; i <5; i++) { + if(FD_ISSET(fds[i], &rset)) {//判断监听哪一个端口 + memset(buffer, 0, MAXBUF); + read(fds[i], buffer, MAXBUF);//进入内核态读数据 + print(buffer); + } + } +} +``` + + + +流程图:https://gitee.com/seazean/images/blob/master/Java/IO-select%E5%8E%9F%E7%90%86%E5%9B%BE.jpg + +图片来源:https://www.processon.com/view/link/5f62b9a6e401fd2ad7e5d6d1 + +参考视频:https://www.bilibili.com/video/BV19D4y1o797 + + + +**** + + + +##### poll + +poll 的功能与 select 类似,也是等待一组描述符中的一个成为就绪状态 + +```c +int poll(struct pollfd *fds, unsigned int nfds, int timeout); +``` + +poll 中的描述符是 pollfd 类型的数组,pollfd 的定义如下: + +```c +struct pollfd { + int fd; /* file descriptor */ + short events; /* requested events */ + short revents; /* returned events */ +}; +``` + +select 和 poll 对比: + +- select 会修改描述符,而 poll 不会 +- select 的描述符类型使用数组实现,有描述符的限制;而 poll 使用**链表**实现,没有描述符数量的限制 +- poll 提供了更多的事件类型,并且对描述符的重复利用上比 select 高 +- 如果一个线程对某个描述符调用了 select 或者 poll,另一个线程关闭了该描述符,会导致调用结果不确定 + +* select 和 poll 速度都比较慢,每次调用都需要将全部描述符数组从应用进程缓冲区复制到内核缓冲区 + +* 几乎所有的系统都支持 select,但是只有比较新的系统支持 poll +* select 和 poll 的时间复杂度 O(n),对 socket 进行扫描时是线性扫描,即采用轮询的方法,效率较低,因为并不知道具体是哪个 socket 具有事件,所以随着 FD 的增加会造成遍历速度慢的**线性下降**性能问题 +* poll 还有一个特点是水平触发,如果报告了 fd 后,没有被处理,那么下次 poll 时会再次报告该 fd + + + +参考文章:https://github.com/CyC2018/CS-Notes/blob/master/notes/Socket.md + + + +**** + + + +##### epoll + +###### 函数 + +epoll_ctl() 用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵**红黑树**上,通过回调函数内核会将 I/O 准备好的描述符加入到一个**链表**中管理,进程调用 epoll_wait() 便可以得到事件完成的描述符 + +```c +int epoll_create(int size); +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); +``` + +epoll 使用事件的就绪通知方式,通过 epoll_ctl 注册fd,一旦该fd就绪,内核就会采用 callback 的回调机制来激活该 fd,epoll_wait 便可以收到通知 + +* epall_create:一个系统函数,函数将在内核空间内开辟一块新的空间,可以理解为 epoll 结构空间,返回值为 epoll 的文件描述符编号,所以 epoll 使用一个文件描述符管理多个描述符 + +* epall_ctl:epoll 的事件注册函数,select 函数是调用时指定需要监听的描述符和事件,epoll 先将用户感兴趣的描述符事件注册到 epoll 空间。此函数是非阻塞函数,用来增删改 epoll 空间内的描述符,参数解释: + + * epfd:epoll 结构的进程 fd 编号,函数将依靠该编号找到对应的 epoll 结构 + + * op:表示当前请求类型,有三个宏定义: + + * EPOLL_CTL_ADD:注册新的fd到epfd中 + * EPOLL_CTL_MOD:修改已经注册的fd的监听事件 + * EPOLL_CTI_DEL:从epfd中删除一个fd + + * fd:需要监听的文件描述符,一般指 socket_fd + + * event:告诉内核对该fd资源感兴趣的事件,epoll_event 的结构: + + ```c + struct epoll_event { + _uint32_t events; /*epoll events*/ + epoll_data_t data; /*user data variable*/ + } + ``` + + events 可以是以下几个宏集合:EPOLLIN、EPOLOUT、EPOLLPRI、EPOLLERR、EPOLLHUP(挂断)、EPOLET(边缘触发)、EPOLLONESHOT(只监听一次,事件触发后自动清除该 fd,从 epoll 列表) + +* epoll_wait:等待事件的产生,类似于 select() 调用,返回值为本次就绪的 fd 个数 + + * epfd:指定感兴趣的 epoll 事件列表 + * events:指向一个 epoll_event 结构数组,当函数返回时,内核会把就绪状态的数据拷贝到该数组 + * maxevents:标明 epoll_event 数组最多能接收的数据量,即本次操作最多能获取多少就绪数据 + * timeout:单位为毫秒 + * 0:表示立即返回,非阻塞调用 + * -1:阻塞调用,直到有用户感兴趣的事件就绪为止 + * 大于0:阻塞调用,阻塞指定时间内如果有事件就绪则提前返回,否则等待指定时间后返回 + +epoll 的描述符事件有两种触发模式:LT(level trigger)和 ET(edge trigger): + +* LT 模式:当 epoll_wait() 检测到描述符事件到达时,将此事件通知进程,进程可以不立即处理该事件,下次调用 epoll_wait() 会再次通知进程。是默认的一种模式,并且同时支持 Blocking 和 No-Blocking +* ET 模式:通知之后进程必须立即处理事件,下次再调用 epoll_wait() 时不会再得到事件到达的通知。减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高;只支持 No-Blocking,以避免由于一个文件的阻塞读/阻塞写操作把处理多个文件描述符的任务饥饿 + +```c +// 创建 epoll 描述符,每个应用程序只需要一个,用于监控所有套接字 +int pollingfd = epoll_create(0xCAFE); +if ( pollingfd < 0 )// report error +// 初始化 epoll 结构 +struct epoll_event ev = { 0 }; + +// 将连接类实例与事件相关联,可以关联任何想要的东西 +ev.data.ptr = pConnection1; + +// 监视输入,并且在事件发生后不自动重新准备描述符 +ev.events = EPOLLIN | EPOLLONESHOT; +// 将描述符添加到监控列表中,即使另一个线程在epoll_wait中等待,描述符将被正确添加 +if ( epoll_ctl( epollfd, EPOLL_CTL_ADD, pConnection1->getSocket(), &ev) != 0 ) + // report error + +// 最多等待 20 个事件 +struct epoll_event pevents[20]; + +// 等待10秒,检索20个并存入epoll_event数组 +int ready = epoll_wait(pollingfd, pevents, 20, 10000); +// 检查epoll是否成功 +if ( ret == -1)// report error and abort +else if ( ret == 0)// timeout; no event detected +else +{ + for (int i = 0; i < ready; i+ ) + { + if ( pevents[i].events & EPOLLIN ) + { + // 获取连接指针 + Connection * c = (Connection*) pevents[i].data.ptr; + c->handleReadEvent(); + } + } +} +``` + + + +流程图: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 + + + +*** + + + +###### 特点 + +epoll 的特点: + +* epoll 使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中 +* epoll 的时间复杂度 O(1),epoll 理解为 event poll,不同于忙轮询和无差别轮询,调用 epoll_wait 不断轮询就绪链表,但是设备就绪时调用回调函数,把就绪 fd 放入就绪链表中,并唤醒在 epoll_wait 中进入睡眠的进程,所以 epoll 实际上是**事件驱动(每个事件关联上fd)**的 +* epoll 内核中根据每个 fd 上的 callback 函数来实现,只有活跃的 socket 才会主动调用 callback,所以使用 epoll 没有前面两者的线性下降的性能问题,效率提高 +* epoll 仅适用于 Linux 系统 + +* epoll 比 select 和 poll 更加灵活而且没有描述符数量限制 +* epoll 只需要将描述符从进程缓冲区向内核缓冲区拷贝一次, 利用 mmap() 文件映射内存加速与内核空间的消息传递,减少复制开销 + +* epoll 对多线程编程更有友好,一个线程调用了 epoll_wait() 另一个线程关闭了同一个描述符,也不会产生像 select 和 poll 的不确定情况 + +* select,poll 每次调用都要把 fd 集合从用户态往内核态拷贝一次,并且要把 current 往设备等待队列中挂一次,而 epoll 只要一次拷贝,而且把 current 往等待队列上挂也只挂一次(注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列),这也能节省不少的开销 + + + +参考文章:https://www.jianshu.com/p/dfd940e7fca2 + + + +*** + + + +##### 应用 + +应用场景: + +* select 应用场景: + * select 的 timeout 参数精度为微秒,而 poll 和 epoll 为毫秒,因此 select 更加适用于实时性要求比较高的场景,比如核反应堆的控制 + * select 可移植性更好,几乎被所有主流平台所支持 + +* poll 应用场景:poll 没有最大描述符数量的限制,适用于平台支持并且对实时性要求不高的情况 + +* epoll 应用场景: + * 运行在 Linux 平台上,有大量的描述符需要同时轮询,并且这些连接最好是长连接 + * 需要同时监控小于 1000 个描述符,没必要使用 epoll,因为这个应用场景下并不能体现 epoll 的优势 + * 需要监控的描述符状态变化多,而且都是非常短暂的,也没有必要使用 epoll。因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用,频繁系统调用降低效率,并且 epoll 的描述符存储在内核,不容易调试 + + + +参考文章:https://github.com/CyC2018/CS-Notes/blob/master/notes/Socket.md + + + +**** + + + +#### 系统调用 + +##### 内核态 + +用户空间:用户代码、用户堆栈 + +内核空间:内核代码、内核调度程序、进程描述符(内核堆栈、thread_info进程描述符) + +* 进程描述符和用户的进程是一一对应的 +* SYS_API系统调用:如 read、write。系统调用就是 0X80 中断 +* 进程描述符pd:进程从用户态切换到内核态时,需要保存用户态时的上下文信息, +* 线程上下文:用户程序基地址,程序计数器、cpu cache、寄存器等,方便程序切回用户态时恢复现场 +* 内核堆栈:系统调用函数也是要创建变量的,这些变量在内核堆栈上分配 + +![](https://gitee.com/seazean/images/raw/master/Java/IO-用户态和内核态.png) + + + +*** + + + +##### 80中断 + +在用户程序中调用操作系统提供的核心态级别的子功能,为了系统安全需要进行用户态和内核态转换,状态的转换需要进行 CPU 中断,中断分为硬中断和软中断: + +* 硬中断:如网络传输中,数据到达网卡后,网卡经过一系列操作后发起硬件中断 +* 软中断:如程序运行过程中本身产生的一些中断 + - 如进行系统调用 system_call,则发起 `0X80` 中断 + - 如程序执行碰到除 0 异常 + +系统调用 system_call 函数所对应的中断指令编号是 0X80(十进制是8×16=128),而该指令编号对应的就是系统调用程序的入口,所以称系统调用为 80 中断 + +系统调用的流程: + +* 在 CPU 寄存器里存一个系统调用号,表示哪个系统函数,比如 read +* 将 CPU 的临时数据都保存到 thread_info 中 +* 执行 80 中断处理程序,找到刚刚存的系统调用号(read),先检查缓存中有没有对应的数据,没有就去磁盘中加载到内核缓冲区,然后从内核缓冲区拷贝到用户空间 +* 最后恢复到用户态,通过 thread_info 恢复现场,用户态继续执行 + +![](https://gitee.com/seazean/images/raw/master/Java/IO-系统调用的过程.jpg) + + + +参考文章:https://blog.csdn.net/hancoder/article/details/112149121 + + + +**** + + + +#### 零拷贝 + +##### DMA + +DMA (Direct Memory Access) :直接存储器访问,让外部设备不通过 CPU 直接与系统内存交换数据的接口技术 + +作用:可以解决批量数据的输入/输出问题,使数据的传送速度取决于存储器和外设的工作速度 + +把内存数据传输到网卡然后发送: + +* 没有 DMA:CPU 读内存数据到 CPU 高速缓存,再写到网卡,这样就把 CPU 的速度拉低到和网卡一个速度 +* 使用DMA:把内存数据读到 socket 内核缓存区(CPU复制),CPU 分配给 DMA 开始**异步**操作,DMA 读取 socket 缓冲区到 DMA 缓冲区,然后写到网卡。DMA 执行完后中断 CPU,这时 socket 内核缓冲区为空,CPU 从用户态切换到内核态,执行中断处理程序,将需要使用 socket 缓冲区的阻塞进程移到运行队列 + +一个完整的 DMA 传输过程必须经历 DMA 请求、DMA 响应、DMA 传输、DMA 结束四个步骤: + + + +DMA 方式是一种完全由硬件进行组信息传送的控制方式,通常系统总线由 CPU 管理,在 DMA 方式中,CPU 把总线让出来由 DMA 控制器接管,用来控制传送的字节数、判断 DMA 是否结束、以及发出DMA结束信号,所以 DMA 控制器必须有以下功能: + +* 能向 CPU 发出系统保持(HOLD)信号,提出总线接管请求 +* 当 CPU 发出允许接管信号后,负责对总线的控制,进入 DMA 方式 +* 能对存储器寻址及能修改地址指针,实现对内存的读写 +* 能决定本次 DMA 传送的字节数,判断 DMA 传送是否结束 +* 发出 DMA 结束信号,使 CPU 恢复正常工作状态(中断) + + + +*** + + + +##### BIO + +传统的 I/O 操作进行了 4 次用户空间与内核空间的上下文切换,以及 4 次数据拷贝: + +* JVM发出read() 系统调用,OS 上下文切换到内核模式(切换1)并将数据从网卡或硬盘等通过 DMA 读取到内核空间缓冲区(拷贝1) +* OS 内核将数据复制到用户空间缓冲区(拷贝2),然后 read 系统调用返回,又会导致一次内核空间到用户空间的上下文切换(切换2) +* JVM 处理代码逻辑并发送 write() 系统调用,OS 上下文切换到内核模式(切换3)并从用户空间缓冲区复制数据到内核空间缓冲区(拷贝3) +* write 系统调用返回,导致内核空间到用户空间的再次上下文切换(切换4),将内核空间缓冲区中的数据写到 hardware(拷贝4) + +![](https://gitee.com/seazean/images/raw/master/Java/IO-BIO工作流程.png) + +read 调用图示: + + + + + +*** + + + +##### mmap + +mmap(Memory Mapped Files)加 write 实现零拷贝,零拷贝就是没有数据从内核空间复制到用户空间 + +用户空间和内核空间共享同一块物理地址,省去用户态和内核态之间的拷贝。写网卡时,共享空间的内容拷贝到 socket 缓冲区,然后交给 DMA 发送到网卡,只需要 3 次复制 + +进行了 4 次用户空间与内核空间的上下文切换,以及 3 次数据拷贝(2 次 DMA,一次 CPU 复制): + +* 发出 mmap 系统调用,DMA 拷贝到内核缓冲区;mmap系统调用返回,无需拷贝 +* 发出write系统调用,将数据从内核缓冲区拷贝到内核 socket 缓冲区;write系统调用返回,DMA 将内核空间 socket 缓冲区中的数据传递到协议引擎 + +![](https://gitee.com/seazean/images/raw/master/Java/IO-mmap工作流程.png) + +原理:利用操作系统的 Page 来实现文件到物理内存的直接映射,完成映射后对物理内存的操作会被同步到硬盘上 + +缺点:不可靠,写到 mmap 中的数据并没有被真正的写到硬盘,操作系统会在程序主动调用 flush 的时候才把数据真正的写到硬盘 + +Java NIO 提供了 **MappedByteBuffer** 类可以用来实现 mmap 内存映射,MappedByteBuffer 类对象只能通过调用 `FileChannel.map()` 获取 + + + +**** + + + +##### sendfile + +sendfile 实现零拷贝,打开文件的文件描述符 fd 和 socket 的 fd 传递给 sendfile,然后经过 3 次复制和 2 次用户态和内核态的切换 + +原理:数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer,由于和用户态完全无关,就减少了一次上下文切换 + +![](https://gitee.com/seazean/images/raw/master/Java/IO-sendfile工作流程.png) + +sendfile2.4 之后,sendfile 实现了更简单的方式,文件到达内核缓冲区后,不必再将数据全部复制到 socket buffer 缓冲区,而是只**将记录数据位置和长度相关等描述符信息**保存到 socket buffer,DMA 根据 socket 缓冲区中描述符提供的位置和偏移量信息直接将内核空间缓冲区中的数据拷贝到协议引擎上(2 次复制 2 次切换) + +Java NIO 对 sendfile 的支持是 `FileChannel.transferTo()/transferFrom()`,把磁盘文件读取 OS 内核缓冲区后的 fileChannel,直接转给 socketChannel 发送,底层就是sendfile + + + +参考文章:https://blog.csdn.net/hancoder/article/details/112149121 @@ -8480,7 +8913,7 @@ UDP协议的使用场景:在线视频、网络语音、电话 -#### UDP实现 +#### 实现UDP UDP协议相关的两个类 @@ -8645,7 +9078,7 @@ ServerSocket类: -#### TCP实现 +#### 实现TCP ##### 开发流程 @@ -8664,10 +9097,10 @@ ServerSocket类: ![](https://gitee.com/seazean/images/raw/master/Java/BIO工作机制.png) -![](https://gitee.com/seazean/images/raw/master/Java/TCP工作模型.png) +![](https://gitee.com/seazean/images/raw/master/Java/TCP-工作模型.png) * 如果输出缓冲区空间不够存放主机发送的数据,则会被阻塞,输入缓冲区同理 -* 缓冲区不属于APP,位于内核 +* 缓冲区不属于应用程序,属于内核 * TCP从输出缓冲区读取数据会加锁阻塞线程 From b415ff4b0e928babc96c934ced058f66ca24f84d Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 12 Jun 2021 18:08:45 +0800 Subject: [PATCH 049/242] Update Java Notes --- Java.md | 55 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/Java.md b/Java.md index c7ac38b..a04b05c 100644 --- a/Java.md +++ b/Java.md @@ -8422,13 +8422,17 @@ IO 复用让单个进程具有处理多个 I/O 事件的能力,又被称为 Ev ##### select +###### 函数 + +socket 不是文件,只是一个标识符,但是 Unix 操作系统把所有东西都**看作**是文件,所以 socket 说成file descriptor,也就是 fd + select 允许应用程序监视一组文件描述符,等待一个或者多个描述符成为就绪状态,从而完成 I/O 操作。 ```c 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,对应读、写、异常条件的描述符集合 @@ -8496,11 +8500,33 @@ while(1) { -流程图:https://gitee.com/seazean/images/blob/master/Java/IO-select%E5%8E%9F%E7%90%86%E5%9B%BE.jpg +参考视频:https://www.bilibili.com/video/BV19D4y1o797 -图片来源:https://www.processon.com/view/link/5f62b9a6e401fd2ad7e5d6d1 -参考视频:https://www.bilibili.com/video/BV19D4y1o797 + +**** + + + +###### 流程 + +select 调用流程图: + +![](https://gitee.com/seazean/images/raw/master/Java/IO-select调用过程.png) + +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 便被唤醒 +5. poll 方法返回时会返回一个描述读写操作是否就绪的 mask 掩码,根据这个 mask 掩码给 fd_set 赋值 +6. 如果遍历完所有的 fd,还没有返回一个可读写的 mask 掩码,则会调用 schedule_timeout 让调用 select 的进程(就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程,如果超过一定的超时时间(schedule_timeout指定),没有其他线程唤醒,则调用 select 的进程会重新被唤醒获得 CPU,进而重新遍历 fd,判断有没有就绪的 fd +7. 把 fd_set 从内核空间拷贝到用户空间,阻塞进程继续执行 + + + +参考文章:https://www.cnblogs.com/anker/p/3265058.html + +其他流程图:https://www.processon.com/view/link/5f62b9a6e401fd2ad7e5d6d1 @@ -8561,7 +8587,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); ``` -epoll 使用事件的就绪通知方式,通过 epoll_ctl 注册fd,一旦该fd就绪,内核就会采用 callback 的回调机制来激活该 fd,epoll_wait 便可以收到通知 +epoll 使用事件的就绪通知方式,通过 epoll_ctl 注册 fd,一旦该 fd 就绪,内核就会采用 callback 的回调机制来激活该 fd,epoll_wait 便可以收到通知 * epall_create:一个系统函数,函数将在内核空间内开辟一块新的空间,可以理解为 epoll 结构空间,返回值为 epoll 的文件描述符编号,所以 epoll 使用一个文件描述符管理多个描述符 @@ -8571,13 +8597,13 @@ epoll 使用事件的就绪通知方式,通过 epoll_ctl 注册fd,一旦该f * op:表示当前请求类型,有三个宏定义: - * EPOLL_CTL_ADD:注册新的fd到epfd中 - * EPOLL_CTL_MOD:修改已经注册的fd的监听事件 - * EPOLL_CTI_DEL:从epfd中删除一个fd + * EPOLL_CTL_ADD:注册新的 fd 到 epfd 中 + * EPOLL_CTL_MOD:修改已经注册的 fd 的监听事件 + * EPOLL_CTI_DEL:从 epfd 中删除一个 fd * fd:需要监听的文件描述符,一般指 socket_fd - * event:告诉内核对该fd资源感兴趣的事件,epoll_event 的结构: + * event:告诉内核对该 fd 资源感兴趣的事件,epoll_event 的结构: ```c struct epoll_event { @@ -8662,18 +8688,15 @@ else epoll 的特点: * epoll 使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中 -* epoll 的时间复杂度 O(1),epoll 理解为 event poll,不同于忙轮询和无差别轮询,调用 epoll_wait 不断轮询就绪链表,但是设备就绪时调用回调函数,把就绪 fd 放入就绪链表中,并唤醒在 epoll_wait 中进入睡眠的进程,所以 epoll 实际上是**事件驱动(每个事件关联上fd)**的 +* 没有最大并发连接的限制,能打开的 fd 的上限远大于1024(1G的内存上能监听约10万个端口) +* epoll 的时间复杂度 O(1),epoll 理解为 event poll,不同于忙轮询和无差别轮询,调用 epoll_wait 不断轮询监听列表,当设备就绪时调用回调函数,把就绪 fd 放入就绪链表中,并唤醒在 epoll_wait 中进入睡眠的进程,所以 epoll 实际上是**事件驱动(每个事件关联上fd)**的 * epoll 内核中根据每个 fd 上的 callback 函数来实现,只有活跃的 socket 才会主动调用 callback,所以使用 epoll 没有前面两者的线性下降的性能问题,效率提高 * epoll 仅适用于 Linux 系统 - * epoll 比 select 和 poll 更加灵活而且没有描述符数量限制 * epoll 只需要将描述符从进程缓冲区向内核缓冲区拷贝一次, 利用 mmap() 文件映射内存加速与内核空间的消息传递,减少复制开销 - * epoll 对多线程编程更有友好,一个线程调用了 epoll_wait() 另一个线程关闭了同一个描述符,也不会产生像 select 和 poll 的不确定情况 - -* select,poll 每次调用都要把 fd 集合从用户态往内核态拷贝一次,并且要把 current 往设备等待队列中挂一次,而 epoll 只要一次拷贝,而且把 current 往等待队列上挂也只挂一次(注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列),这也能节省不少的开销 - - +* select,poll 每次调用都要把 fd 集合从用户态往内核态拷贝一次,并且要把 current 往设备等待队列中挂一次,而 epoll 只要一次拷贝,而且把 current 往等待队列上挂也只挂一次(注意这里的等待队列并不是设备等待队列,只是一个 epoll 内部定义的等待队列),这也能节省不少的开销(看流程图会有更好的认识) + 参考文章:https://www.jianshu.com/p/dfd940e7fca2 From 03ef5334358e4e80249cd27e935f40c9552f5f97 Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 14 Jun 2021 16:59:40 +0800 Subject: [PATCH 050/242] Update Java Notes --- Java.md | 53 ++++++++++++++++++++++++++++------------------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/Java.md b/Java.md index a04b05c..42e1545 100644 --- a/Java.md +++ b/Java.md @@ -8557,13 +8557,12 @@ select 和 poll 对比: - select 会修改描述符,而 poll 不会 - select 的描述符类型使用数组实现,有描述符的限制;而 poll 使用**链表**实现,没有描述符数量的限制 - poll 提供了更多的事件类型,并且对描述符的重复利用上比 select 高 -- 如果一个线程对某个描述符调用了 select 或者 poll,另一个线程关闭了该描述符,会导致调用结果不确定 - -* select 和 poll 速度都比较慢,每次调用都需要将全部描述符数组从应用进程缓冲区复制到内核缓冲区 +* select 和 poll 速度都比较慢,每次调用都需要将全部描述符数组 fd 从应用进程缓冲区复制到内核缓冲区,同时每次都需要在内核遍历传递进来的所有 fd ,这个开销在 fd 很多时会很大 * 几乎所有的系统都支持 select,但是只有比较新的系统支持 poll * select 和 poll 的时间复杂度 O(n),对 socket 进行扫描时是线性扫描,即采用轮询的方法,效率较低,因为并不知道具体是哪个 socket 具有事件,所以随着 FD 的增加会造成遍历速度慢的**线性下降**性能问题 * poll 还有一个特点是水平触发,如果报告了 fd 后,没有被处理,那么下次 poll 时会再次报告该 fd +* 如果一个线程对某个描述符调用了 select 或者 poll,另一个线程关闭了该描述符,会导致调用结果不确定 @@ -8579,7 +8578,7 @@ select 和 poll 对比: ###### 函数 -epoll_ctl() 用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵**红黑树**上,通过回调函数内核会将 I/O 准备好的描述符加入到一个**链表**中管理,进程调用 epoll_wait() 便可以得到事件完成的描述符 +epoll 使用事件的就绪通知方式,通过 epoll_ctl() 向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵**红黑树**上,一旦该 fd 就绪,内核通过 callback 回调函数将 I/O 准备好的描述符加入到一个**链表**中管理,进程调用 epoll_wait() 便可以得到事件完成的描述符 ```c int epoll_create(int size); @@ -8587,8 +8586,6 @@ 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); ``` -epoll 使用事件的就绪通知方式,通过 epoll_ctl 注册 fd,一旦该 fd 就绪,内核就会采用 callback 的回调机制来激活该 fd,epoll_wait 便可以收到通知 - * epall_create:一个系统函数,函数将在内核空间内开辟一块新的空间,可以理解为 epoll 结构空间,返回值为 epoll 的文件描述符编号,所以 epoll 使用一个文件描述符管理多个描述符 * epall_ctl:epoll 的事件注册函数,select 函数是调用时指定需要监听的描述符和事件,epoll 先将用户感兴趣的描述符事件注册到 epoll 空间。此函数是非阻塞函数,用来增删改 epoll 空间内的描述符,参数解释: @@ -8687,19 +8684,22 @@ else epoll 的特点: +* epoll 仅适用于 Linux 系统 * epoll 使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中 -* 没有最大并发连接的限制,能打开的 fd 的上限远大于1024(1G的内存上能监听约10万个端口) -* epoll 的时间复杂度 O(1),epoll 理解为 event poll,不同于忙轮询和无差别轮询,调用 epoll_wait 不断轮询监听列表,当设备就绪时调用回调函数,把就绪 fd 放入就绪链表中,并唤醒在 epoll_wait 中进入睡眠的进程,所以 epoll 实际上是**事件驱动(每个事件关联上fd)**的 +* 没有最大描述符数量(并发连接)的限制,打开 fd 的上限远大于1024(1G 内存能监听约10万个端口) +* epoll 的时间复杂度 O(1),epoll 理解为 event poll,不同于忙轮询和无差别轮询,调用 epoll_wait 只是轮询就绪链表。当监听列表有设备就绪时调用回调函数,把就绪 fd 放入就绪链表中,并唤醒在 epoll_wait 中阻塞的进程,所以 epoll 实际上是**事件驱动(每个事件关联上fd)**的 * epoll 内核中根据每个 fd 上的 callback 函数来实现,只有活跃的 socket 才会主动调用 callback,所以使用 epoll 没有前面两者的线性下降的性能问题,效率提高 -* epoll 仅适用于 Linux 系统 -* epoll 比 select 和 poll 更加灵活而且没有描述符数量限制 -* epoll 只需要将描述符从进程缓冲区向内核缓冲区拷贝一次, 利用 mmap() 文件映射内存加速与内核空间的消息传递,减少复制开销 + +* epoll 每次注册新的事件到 epoll 句柄中时,会把所有的 fd 拷贝进内核,但不是每次 epoll_wait 的时重复拷贝,对比前面两种,epoll 只需要将描述符从进程缓冲区向内核缓冲区拷贝一次。 epoll 也可以利用 mmap() 文件映射内存加速与内核空间的消息传递,减少复制开销 +* 前面两者要把 current 往设备等待队列中挂一次,epoll 也只把 current 往等待队列上挂一次,但是这里的等待队列并不是设备等待队列,只是一个 epoll 内部定义的等待队列,这样节省不少的开销 * epoll 对多线程编程更有友好,一个线程调用了 epoll_wait() 另一个线程关闭了同一个描述符,也不会产生像 select 和 poll 的不确定情况 -* select,poll 每次调用都要把 fd 集合从用户态往内核态拷贝一次,并且要把 current 往设备等待队列中挂一次,而 epoll 只要一次拷贝,而且把 current 往等待队列上挂也只挂一次(注意这里的等待队列并不是设备等待队列,只是一个 epoll 内部定义的等待队列),这也能节省不少的开销(看流程图会有更好的认识) - + + 参考文章:https://www.jianshu.com/p/dfd940e7fca2 +参考文章:https://www.cnblogs.com/anker/p/3265058.html + *** @@ -8740,7 +8740,7 @@ epoll 的特点: 内核空间:内核代码、内核调度程序、进程描述符(内核堆栈、thread_info进程描述符) * 进程描述符和用户的进程是一一对应的 -* SYS_API系统调用:如 read、write。系统调用就是 0X80 中断 +* SYS_API 系统调用:如 read、write,系统调用就是 0X80 中断 * 进程描述符pd:进程从用户态切换到内核态时,需要保存用户态时的上下文信息, * 线程上下文:用户程序基地址,程序计数器、cpu cache、寄存器等,方便程序切回用户态时恢复现场 * 内核堆栈:系统调用函数也是要创建变量的,这些变量在内核堆栈上分配 @@ -8759,7 +8759,8 @@ epoll 的特点: * 硬中断:如网络传输中,数据到达网卡后,网卡经过一系列操作后发起硬件中断 * 软中断:如程序运行过程中本身产生的一些中断 - - 如进行系统调用 system_call,则发起 `0X80` 中断 + - 如进行系 + - 发起 `0X80` 中断 - 如程序执行碰到除 0 异常 系统调用 system_call 函数所对应的中断指令编号是 0X80(十进制是8×16=128),而该指令编号对应的就是系统调用程序的入口,所以称系统调用为 80 中断 @@ -8794,7 +8795,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 结束四个步骤: @@ -8818,14 +8819,16 @@ DMA 方式是一种完全由硬件进行组信息传送的控制方式,通常 传统的 I/O 操作进行了 4 次用户空间与内核空间的上下文切换,以及 4 次数据拷贝: -* JVM发出read() 系统调用,OS 上下文切换到内核模式(切换1)并将数据从网卡或硬盘等通过 DMA 读取到内核空间缓冲区(拷贝1) +* JVM 发出 read() 系统调用,OS 上下文切换到内核模式(切换1)并将数据从网卡或硬盘等通过 DMA 读取到内核空间缓冲区(拷贝1) * OS 内核将数据复制到用户空间缓冲区(拷贝2),然后 read 系统调用返回,又会导致一次内核空间到用户空间的上下文切换(切换2) * JVM 处理代码逻辑并发送 write() 系统调用,OS 上下文切换到内核模式(切换3)并从用户空间缓冲区复制数据到内核空间缓冲区(拷贝3) * write 系统调用返回,导致内核空间到用户空间的再次上下文切换(切换4),将内核空间缓冲区中的数据写到 hardware(拷贝4) +流程图中的箭头反过来也成立,可以从网卡获取数据 + ![](https://gitee.com/seazean/images/raw/master/Java/IO-BIO工作流程.png) -read 调用图示: +read 调用图示:read、write 都是系统调用指令 @@ -12303,7 +12306,9 @@ public static void main(String[] args) { 方法区是一个 JVM 规范,**永久代与元空间都是其一种实现方式** -方法区的大小不必是固定的,可以动态扩展;方法区大小很难确定,因此加载的类太多,可能导致永久代内存溢出 (OutOfMemoryError);对这块区域进行垃圾回收主要是对常量池的回收和对类的卸载,比较难实现 +方法区的大小不必是固定的,可以动态扩展,加载的类太多,可能导致永久代内存溢出 (OutOfMemoryError) + +方法区的 GC:针对常量池的回收及对类型的卸载,比较难实现 为了**避免方法区出现OOM**,在JDK8中将堆内的方法区(永久代)移动到了本地内存上,重新开辟了一块空间,叫做元空间,元空间存储类的元信息,静态变量和字符串常量池等放入堆中 @@ -12317,8 +12322,6 @@ public static void main(String[] args) { * 类在解析阶段将符号引用替换成直接引用 * 除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern() -方法区的 GC:针对常量池的回收及对类型的卸载 - *** @@ -12398,9 +12401,9 @@ public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制 ##### 基本介绍 -直接内存是Java堆外的、直接向系统申请的内存区间,不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域 +直接内存是 Java 堆外的、直接向系统申请的内存区间,不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域 -Direct Memory优点: +Direct Memory 优点: * Java 的 NIO 库允许 Java 程序使用直接内存,用于数据缓冲区,使用 native 函数直接分配堆外内存 * 读写性能高,读写频繁的场合可能会考虑使用直接内存 @@ -14175,7 +14178,7 @@ public class Test { * 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化 * 当虚拟机启动时,需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类 * MethodHandle和VarHandle可以看作是轻量级的反射调用机制,而要想使用这2个调用, 就必须先使用findStaticVarHandle来初始化要调用的类 -* 补充:当一个接口中定义了JDK8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化 +* 补充:当一个接口中定义了 JDK8 新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化 **被动引用**:所有引用类的方式都不会触发初始化,称为被动引用 @@ -14213,7 +14216,7 @@ init指的是实例构造器,主要作用是在类实例化过程中执行, 2. 该类没有在其他任何地方被引用 3. 该类的类加载器的实例已被GC -在JVM生命周期类,由jvm自带的类加载器加载的类是不会被卸载的,但是由我们自定义的类加载器加载的类是可能被卸载 +在JVM生命周期类,由 JVM 自带的类加载器加载的类是不会被卸载的,由我们自定义的类加载器加载的类是可能被卸载。因为 JVM 会始终引用启动、扩展、系统类加载器,这些类加载器始终引用它们所加载的类,所以这些类始终是可及的 From 3762c92afe08d19d75a6dba7ee59e512c0729acf Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 17 Jun 2021 08:34:06 +0800 Subject: [PATCH 051/242] Update Java Notes --- DB.md | 38 ++++++++++++++------------------------ Java.md | 2 +- Tool.md | 4 +++- 3 files changed, 18 insertions(+), 26 deletions(-) diff --git a/DB.md b/DB.md index eb8fb9f..0415f34 100644 --- a/DB.md +++ b/DB.md @@ -10817,22 +10817,12 @@ sentinel 在通知阶段不断的去获取 master/slave 的信息,然后在各 ## 缓存方案 -### 缓存本质 - -弥补 CPU 的高算力和 IO 的慢读写之间巨大的鸿沟 - - - - - -*** - - - ### 缓存模式 #### 旁路缓存 +缓存本质:弥补 CPU 的高算力和 IO 的慢读写之间巨大的鸿沟 + 旁路缓存模式 Cache Aside Pattern 是平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景 Cache Aside Pattern 中服务端需要同时维系 DB 和 cache,并且是以 DB 的结果为准 @@ -10855,10 +10845,10 @@ Cache Aside Pattern 中服务端需要同时维系 DB 和 cache,并且是以 D 缓存不一致的方法: * 数据库和缓存数据**强一致**场景 : - * 更新DB时同样更新 cache,加一个锁来保证更新 cache 时不存在线程安全问题,这样可以增加命中率 + * 更新 DB 时同样更新 cache,加一个锁来保证更新 cache 时不存在线程安全问题,这样可以增加命中率 * 延迟双删:先淘汰缓存再写数据库,休眠1秒再次淘汰缓存,可以将1秒内造成的缓存脏数据再次删除 * CDC 同步:通过 canal 订阅 MySQL binlog 的变更上报给 Kafka,系统监听 Kafka 消息触发缓存失效 -* 可以短暂地允许数据库和缓存数据**不一致**场景:更新DB的时候同样更新 cache,但是给缓存加一个比较短的过期时间,这样的话就可以保证即使数据不一致影响也比较小 +* 可以短暂地允许数据库和缓存数据**不一致**场景:更新 DB 的时候同样更新 cache,但是给缓存加一个比较短的过期时间,这样就可以保证即使数据不一致影响也比较小 @@ -10954,9 +10944,9 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 #### 缓存雪崩 -场景:数据库服务器崩溃,一连串的问题会随之而来。系统平稳运行过程中,忽然数据库连接量激增,应用服务器无法及时处理请求,大量408,500错误页面出现,客户反复刷新页面获取数据,造成:数据库崩溃、应用服务器崩溃、重启应用服务器无效、Redis服务器崩溃、Redis集群崩溃、重启数据库后再次被瞬间流量放倒 +场景:数据库服务器崩溃,一连串的问题会随之而来。系统平稳运行过程中,忽然数据库连接量激增,应用服务器无法及时处理请求,大量408,500错误页面出现,客户反复刷新页面获取数据,造成数据库崩溃、应用服务器崩溃、重启应用服务器无效、Redis 服务器崩溃、Redis 集群崩溃、重启数据库后再次被瞬间流量放倒 -问题排查:在一个较短的时间内,缓存中较多的key集中过期,此周期内请求访问过期的数据,redis未命中,redis向数据库获取数据,数据库同时接收到大量的请求无法及时处理;Redis大量请求被积压,开始出现超时现象;数据库流量激增,数据库崩溃,重启后仍然面对缓存中无数据可用;Redis服务器资源被严重占用,Redis服务器崩溃;Redis集群呈现崩塌,集群瓦解;应用服务器无法及时得到数据响应请求,来自客户端的请求数量越来越多,应用服务器崩溃;应用服务器,redis,数据库全部重启,效果不理想 +问题排查:在一个较短的时间内,缓存中较多的 key 集中过期,此周期内请求访问过期的数据,Redis 未命中,Redis 向数据库获取数据,数据库同时接收到大量的请求无法及时处理;Redis 大量请求被积压,开始出现超时现象;数据库流量激增,数据库崩溃,重启后仍然面对缓存中无数据可用;Redis 服务器资源被严重占用,Redis 服务器崩溃;Redis 集群呈现崩塌,集群瓦解;应用服务器无法及时得到数据响应请求,来自客户端的请求数量越来越多,应用服务器崩溃;应用服务器,redis,数据库全部重启,效果不理想 解决方案: @@ -10966,7 +10956,7 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 2. 构建**多级缓存**架构:Nginx 缓存 + redis 缓存 + ehcache 缓存 - 3. 检测 Mysql 严重耗时业务进行优化:对数据库的瓶颈排查:例如超时查询、耗时较高事务等 + 3. 检测 Mysql 严重耗时业务进行优化:对数据库的瓶颈排查,例如超时查询、耗时较高事务等 4. 灾难预警机制:监控redis服务器性能指标,CPU占用、CPU使用率、内存容量、平均响应时间、线程数 @@ -10976,7 +10966,7 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 1. LRU 与 LFU切换 - 2. 数据有效期策略调整:根据业务数据有效期进行分类错峰,A类90分钟,B类80分钟,C类70分钟,过期时间使用固定时间 + 随机值的形式,稀释集中到期的key的数量 + 2. 数据有效期策略调整:根据业务数据有效期进行分类错峰,A类90分钟,B类80分钟,C类70分钟,过期时间使用固定时间 + 随机值的形式,稀释集中到期的 key 的数量 3. 超热数据使用永久key @@ -10995,17 +10985,17 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 #### 缓存击穿 -场景:系统平稳运行过程中,数据库连接量瞬间激增,Redis服务器无大量key过期,Redis内存平稳,无波动,Redis 服务器 CPU 正常,但是数据库崩溃 +场景:系统平稳运行过程中,数据库连接量瞬间激增,Redis 服务器无大量 key 过期,Redis 内存平稳,无波动,Redis 服务器 CPU 正常,但是数据库崩溃 问题排查: -1. Redis中某个key过期,该key访问量巨大 +1. Redis 中某个 key 过期,该 key 访问量巨大 2. 多个数据请求从服务器直接压到 Redis 后,均未命中 -3. Redis在短时间内发起了大量对数据库中同一数据的访问 +3. Redis 在短时间内发起了大量对数据库中同一数据的访问 -简而言之两点:单个key高热数据,key过期 +简而言之两点:单个 key 高热数据,key 过期 解决方案: @@ -11048,9 +11038,9 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 1. 缓存null:对查询结果为null的数据进行缓存 (长期使用,定期清理) ,设定短时限,例如30-60秒,最高5分钟 -2. 白名单策略:提前预热各种分类数据id对应的**bitmaps**,id作为bitmaps的offset,相当于设置了数据白名单。当加载正常数据时放行,加载异常数据时直接拦截(效率偏低),使用布隆过滤器(有关布隆过滤器的命中问题对当前状况可以忽略) +2. 白名单策略:提前预热各种分类数据 id 对应的 **bitmaps**,id 作为 bitmaps 的 offset,相当于设置了数据白名单。当加载正常数据时放行,加载异常数据时直接拦截(效率偏低),也可以使用布隆过滤器(有关布隆过滤器的命中问题对当前状况可以忽略) -3. 实施监控:实时监控redis命中率(业务正常范围时,通常会有一个波动值)与null数据的占比 +3. 实施监控:实时监控 redis 命中率(业务正常范围时,通常会有一个波动值)与null数据的占比 * 非活动时段波动:通常检测3-5倍,超过5倍纳入重点排查对象 * 活动时段波动:通常检测10-50倍,超过50倍纳入重点排查对象 diff --git a/Java.md b/Java.md index 42e1545..2c5eac9 100644 --- a/Java.md +++ b/Java.md @@ -14995,7 +14995,7 @@ Passenger 类的方法表包括两个方法,分别对应 0 号和 1 号。方 ##### 内联缓存 -内联缓存:是一种加快动态绑定的优化技术,它能够缓存虚方法调用中调用者的动态类型以及该类型所对应的目标方法。在之后的执行过程中,如果碰到已缓存的类型,内联缓存便会直接调用该类型所对应的目标方法;如果没有碰到已缓存的类型,内联缓存则会退化至使用基于方法表的动态绑定 +内联缓存:是一种加快动态绑定的优化技术,它能够缓存虚方法调用中**调用者的动态类型以及该类型所对应的目标方法**。在之后的执行过程中,如果碰到已缓存的类型,内联缓存便会直接调用该类型所对应的目标方法;如果没有碰到已缓存的类型,内联缓存则会退化至使用基于方法表的动态绑定 多态的三个术语: diff --git a/Tool.md b/Tool.md index 3a46a4f..a863045 100644 --- a/Tool.md +++ b/Tool.md @@ -2205,7 +2205,9 @@ pstree -A #查看所有进程树 父进程 ID 为 0 的进程通常是内核进程,它们作为系统自举过程的一部分而启动,但 init 进程是个例外,它的父进程是0,但是它是用户进程 -自举程序:存储在内存中 ROM,用来加载操作系统。CPU 的程序计数器指向 ROM 中自举程序第一条指令所对应的位置,当计算机通电,CPU 开始读取并执行自举程序,将操作系统(不是全部,只是启动计算机的那部分程序)装入RAM中,这个过程是自举过程 +自举程序:存储在内存中 ROM(BIOS 芯片),用来加载操作系统。CPU 的程序计数器指向 ROM 中自举程序第一条指令所对应的位置,当计算机通电,CPU 开始读取并执行自举程序,将操作系统(不是全部,只是启动计算机的那部分程序)装入RAM中,这个过程是自举过程 + +主存 = RAM + BIOS部分的 ROM 装入完成后,CPU 的程序计数器就被设置为 RAM 中操作系统的第一条指令所对应的位置,接下来CPU将开始执行操作系统的指令 From 46eb886272fa66d9da6c2c466ef400ced552fdab Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 17 Jun 2021 08:38:57 +0800 Subject: [PATCH 052/242] Update Java Notes --- Java.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Java.md b/Java.md index 2c5eac9..7e3e003 100644 --- a/Java.md +++ b/Java.md @@ -6,6 +6,8 @@ #### 变量类型 +开始 + | | 成员变量 | 局部变量 | 静态变量 | | :------: | :------------: | :------------------------: | :------------------: | | 定义位置 | 在类中,方法外 | 方法中或者方法的形参 | 在类中,方法外 | From e96ce20002ecfa76396d6ff45d83bdcfd9e1c88a Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 17 Jun 2021 08:53:48 +0800 Subject: [PATCH 053/242] Update Java Notes --- Java.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/Java.md b/Java.md index 7e3e003..2c5eac9 100644 --- a/Java.md +++ b/Java.md @@ -6,8 +6,6 @@ #### 变量类型 -开始 - | | 成员变量 | 局部变量 | 静态变量 | | :------: | :------------: | :------------------------: | :------------------: | | 定义位置 | 在类中,方法外 | 方法中或者方法的形参 | 在类中,方法外 | From 4e6a884707d9c7ab151e363cb50c7845b7fc585c Mon Sep 17 00:00:00 2001 From: Seazean Date: Fri, 18 Jun 2021 11:08:37 +0800 Subject: [PATCH 054/242] Update Java Notes --- DB.md | 38 +++++++++++++++++++------------------- Java.md | 15 +++++++-------- Tool.md | 2 +- 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/DB.md b/DB.md index 0415f34..de8e0dc 100644 --- a/DB.md +++ b/DB.md @@ -9208,11 +9208,11 @@ bgsave指令工作原理: ![](https://gitee.com/seazean/images/raw/master/DB/Redis-bgsave工作原理.png) -流程:当执行 bgsave 的时候,客户端发出 bgsave 指令给到 redis 服务器,服务器返回后台已经开始执行的信息给客户端,同时使用 fork函数 创建一个子进程,让这个子进程去执行save相关的操作,创建RDB文件保存起来,操作完以后把结果返回。 +流程:当执行 bgsave 的时候,客户端发出 bgsave 指令给到 redis 服务器,服务器返回后台已经开始执行的信息给客户端,同时使用 fork函数创建一个子进程,让子进程去执行 save 相关的操作,创建 RDB 文件保存起来,操作完以后把结果返回。 -bgsave分成两个过程:第一个是服务端收到指令直接告诉客户端开始执行;另外一个过程是一个子进程在完成后台的保存操作,操作完以后返回消息。**两个进程不相互影响**,所以在持久化期间Redis可以正常工作 +bgsave 分成两个过程:第一个是服务端收到指令直接告诉客户端开始执行;另外一个过程是一个子进程在完成后台的保存操作,操作完以后返回消息。**两个进程不相互影响**,所以在持久化期间 Redis 可以正常工作 -注意:bgsave命令是针对save阻塞问题做的优化,Redis内部所有涉及到RDB操作都采用bgsave的方式,save命令可以放弃使用 +注意:bgsave 命令是针对 save 阻塞问题做的优化,Redis 内部所有涉及到 RDB 操作都采用 bgsave 的方式,save 命令可以放弃使用 @@ -9248,7 +9248,7 @@ save 300 10 #300s内10个key发生变化就进行持久化 * 对数据产生了影响 * 不进行数据比对,比如 name 键存在,重新 set name seazean 也算一次变化 -save配置要根据实际业务情况进行设置,频度过高或过低都会出现性能问题,结果可能是灾难性的 +save 配置要根据实际业务情况进行设置,频度过高或过低都会出现性能问题,结果可能是灾难性的 RDB三种启动方式对比: @@ -9289,7 +9289,7 @@ RDB三种启动方式对比: - RDB 是一个紧凑压缩的二进制文件,存储效率较高,但存储数据量较大时,存储效率较低 - RDB 内部存储的是 redis 在某个时间点的数据快照,非常适合用于数据备份,全量复制等场景 - RDB 恢复数据的速度要比 AOF 快很多,因为是快照,直接恢复 - - 应用:服务器中每X小时执行 bgsave 备份,并将 RDB 文件拷贝到远程机器中,用于灾难恢复 + - 应用:服务器中每 X 小时执行 bgsave 备份,并将 RDB 文件拷贝到远程机器中,用于灾难恢复 * RDB缺点: - RDB 方式无论是执行指令还是利用配置,无法做到实时持久化,具有较大的可能性丢失数据 @@ -9376,11 +9376,11 @@ AOF重写规则: - 非写入类的无效指令将被忽略,只保留最终数据的写入命令 - 如del key1、 hdel key2、srem key3、set key4 111、set key4 222等,select 指令虽然不更改数据,但是更改了数据的存储位置,此类命令同样需要记录 + 如 del key1、 hdel key2、srem key3、set key4 111、set key4 222等,select 指令虽然不更改数据,但是更改了数据的存储位置,此类命令同样需要记录 - 对同一数据的多条写命令合并为一条命令 - 如lpushlist1 a、lpush list1 b、lpush list1 c 可以转化为:lpush list1 a b c。 + 如 lpushlist1 a、lpush list1 b、lpush list1 c 可以转化为:lpush list1 a b c。 为防止数据量过大造成客户端缓冲区溢出,对list、set、hash、zset等类型,每条指令最多写入64个元素 @@ -9468,17 +9468,17 @@ AOF重写规则: - 数据呈现阶段有效性,建议使用 RDB 持久化方案 - 数据可以良好的做到阶段内无丢失,且恢复速度较快,阶段内数据恢复通常采用RDB方案 + 数据可以良好的做到阶段内无丢失,且恢复速度较快,阶段内数据恢复通常采用 RDB 方案 - 注意:利用 RDB 实现紧凑的数据持久化会使 Redis 降的很低 + 注意:利用 RDB 实现紧凑的数据持久化,存储数据量较大时,存储效率较低 综合对比: -- RDB与AOF的选择实际上是在做一种权衡,每种都有利有弊 -- 如不能承受数分钟以内的数据丢失,对业务数据非常敏感,选用AOF -- 如能承受数分钟以内的数据丢失,且追求大数据集的恢复速度,选用RDB -- 灾难恢复选用RDB -- 双保险策略,同时开启 RDB和 AOF,重启后,Redis优先使用 AOF 来恢复数据,降低丢失数据的量 +- RDB 与 AOF 的选择实际上是在做一种权衡,每种都有利有弊 +- 如不能承受数分钟以内的数据丢失,对业务数据非常敏感,选用 AOF +- 如能承受数分钟以内的数据丢失,且追求大数据集的恢复速度,选用 RDB +- 灾难恢复选用 RDB +- 双保险策略,同时开启 RDB和 AOF,重启后,Redis 优先使用 AOF 来恢复数据,降低丢失数据的量 @@ -9488,7 +9488,7 @@ AOF重写规则: ### fork -#### 函数介绍 +#### 介绍 fork() 函数创建一个子进程,子进程与父进程几乎是完全相同的进程,系统先给子进程分配资源,然后把原来的进程的所有数据都复制到子进程中,只有少数值与父进程的值不同,相当于克隆了一个进程 @@ -9520,7 +9520,7 @@ fpid 的值在父子进程中不同:进程形成了链表,父进程的 fpid -#### 函数使用 +#### 使用 基本使用: @@ -9596,11 +9596,11 @@ int main(void) -#### 内存关系 +#### 内存 -fork()调用之后父子进程的内存关系 +fork() 调用之后父子进程的内存关系 -早期 Linux 的fork()实现时,就是全部复制,这种方法效率太低,而且造成了很大的内存浪费,现在 Linux 实现采用了两种方法来规避这种浪费: +早期 Linux 的 fork() 实现时,就是全部复制,这种方法效率太低,而且造成了很大的内存浪费,现在 Linux 实现采用了两种方法来规避这种浪费: * 父子进程的代码段是相同的,所以代码段是没必要复制的,只需内核将代码段标记为只读,父子进程就共享此代码段。fork() 之后在进程创建代码段时,子进程的进程级页表项都指向和父进程相同的物理页帧 diff --git a/Java.md b/Java.md index 2c5eac9..b3dc6a1 100644 --- a/Java.md +++ b/Java.md @@ -8759,9 +8759,8 @@ epoll 的特点: * 硬中断:如网络传输中,数据到达网卡后,网卡经过一系列操作后发起硬件中断 * 软中断:如程序运行过程中本身产生的一些中断 - - 如进行系 - 发起 `0X80` 中断 - - 如程序执行碰到除 0 异常 + - 程序执行碰到除 0 异常 系统调用 system_call 函数所对应的中断指令编号是 0X80(十进制是8×16=128),而该指令编号对应的就是系统调用程序的入口,所以称系统调用为 80 中断 @@ -8801,13 +8800,13 @@ DMA (Direct Memory Access) :直接存储器访问,让外部设备不通过 C -DMA 方式是一种完全由硬件进行组信息传送的控制方式,通常系统总线由 CPU 管理,在 DMA 方式中,CPU 把总线让出来由 DMA 控制器接管,用来控制传送的字节数、判断 DMA 是否结束、以及发出DMA结束信号,所以 DMA 控制器必须有以下功能: +DMA 方式是一种完全由硬件进行组信息传送的控制方式,通常系统总线由 CPU 管理,在 DMA 方式中,CPU 的主存控制信号被禁止使用,CPU 把总线(地址总线、数据总线、控制总线)让出来由 DMA 控制器接管,用来控制传送的字节数、判断 DMA 是否结束、以及发出 DMA 结束信号,所以 DMA 控制器必须有以下功能: -* 能向 CPU 发出系统保持(HOLD)信号,提出总线接管请求 -* 当 CPU 发出允许接管信号后,负责对总线的控制,进入 DMA 方式 -* 能对存储器寻址及能修改地址指针,实现对内存的读写 -* 能决定本次 DMA 传送的字节数,判断 DMA 传送是否结束 -* 发出 DMA 结束信号,使 CPU 恢复正常工作状态(中断) +* 接受外设发出的 DMA 请求,并向 CPU 发出总线接管请求 +* 当 CPU 发出允许接管信号后,进入 DMA 操作周期 +* 确定传送数据的主存单元地址及长度,并自动修改主存地址计数和传送长度计数 +* 规定数据在主存和外设间的传送方向,发出读写等控制信号,执行数据传送操作 +* 判断 DMA 传送是否结束,发出 DMA 结束信号,使 CPU 恢复正常工作状态(中断) diff --git a/Tool.md b/Tool.md index a863045..a889196 100644 --- a/Tool.md +++ b/Tool.md @@ -4055,7 +4055,7 @@ Docker官方的Docker hub(https://hub.docker.com)是一个用于管理公共 -## 对比虚拟机 +## 虚拟机 容器: From 838ece489fa68ef9b674f74fbf85bd666e9ce6a6 Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 21 Jun 2021 23:16:09 +0800 Subject: [PATCH 055/242] Update Java Notes --- DB.md | 55 ++-- Issue.md | 16 +- Java.md | 907 ++++++++++++++++++++++++++++++++++++++----------------- SSM.md | 2 +- Tool.md | 4 - Web.md | 60 +++- 6 files changed, 730 insertions(+), 314 deletions(-) diff --git a/DB.md b/DB.md index de8e0dc..cd361ee 100644 --- a/DB.md +++ b/DB.md @@ -449,7 +449,7 @@ mysqlshow -uroot -p1234 test book --count CREATE DATABASE db1; -- 创建db1数据库 ``` - * 创建数据库(判断,如果不存在则创建) + * 创建数据库(判断,如果不存在则创建) ```mysql CREATE DATABASE IF NOT EXISTS 数据库名称; @@ -1633,7 +1633,7 @@ INSERT INTO stu_course VALUES (NULL,1,1),(NULL,1,2),(NULL,2,1),(NULL,2,2); * 子查询 * 自关联查询 -多表查询格式: +多表查询格式:(笛卡儿积) ```mysql SELECT @@ -1650,7 +1650,7 @@ WHERE -#### 内连接查询 +#### 内连接 查询原理:内连接查询的是两张表有交集的部分数据 @@ -1673,7 +1673,7 @@ WHERE -#### 外连接查询 +#### 外连接 * 左外连接:查询左表的全部数据,和左右两张表有交集部分的数据 @@ -1732,7 +1732,7 @@ WHERE -#### 自关联查询 +#### 自关联 自关联查询:同一张表中有数据关联。可以多次查询这同一个表! @@ -5493,11 +5493,11 @@ MySQL 的主从复制原理图: * 从库的查询压力大 * 大事务的执行,主库必须要等到事务完成之后才会写入 binlog,导致从节点出现应用 binlog 延迟 -* 主库的DDL,从库与主库的 DDL 同步是串行进行,DDL 在主库执行时间很长,那么从库也会消耗同样的时间 +* 主库的 DDL,从库与主库的 DDL 同步是串行进行,DDL 在主库执行时间很长,那么从库也会消耗同样的时间 * 锁冲突问题也可能导致从节点的 SQL 线程执行慢 * 从库的机器性能比主库的差,导致从库的复制能力弱 -主从同步问题永远都是一致性和性能的权衡,需要根据实际的应用场景,可以采取下面的办法: +主从同步问题永远都是**一致性和性能的权衡**,需要根据实际的应用场景,可以采取下面的办法: * 二次查询,如果从库查不到数据,则再去主库查一遍,由 API 封装,比较简单,但导致主库压力大 * 降低多线程大事务并发的概率,优化业务逻辑 @@ -5875,7 +5875,7 @@ InnoDB 实现了以下两种类型的行锁: - 排他锁和排他锁 冲突 - 排他锁和共享锁 冲突 -可以通过以下语句显示给数据集加共享锁或排他锁: +可以通过以下语句显式给数据集加共享锁或排他锁: ```mysql SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE -- 共享锁 @@ -5983,7 +5983,7 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 #### 锁升级 -无索引行锁升级为表锁:不通过索引检索数据,那么 InnoDB 将对表中的所有记录加锁,实际效果和加表锁一样 +无索引行锁升级为表锁:不通过索引检索数据,InnoDB 会将对表中的所有记录加锁,实际效果和**表锁**一样 索引失效会造成锁升级,实际开发过程应避免出现索引失效的状况 @@ -10399,7 +10399,7 @@ slave 与 master 连接断开 -#### 缓存不一致 +#### 一致性 网络信息不同步,数据发送有延迟,导致多个 slave 获取相同数据不同步 @@ -10842,19 +10842,6 @@ Cache Aside Pattern 中服务端需要同时维系 DB 和 cache,并且是以 D * 写操作比较频繁的话导致 cache 中的数据会被频繁被删除,影响缓存命中率 -缓存不一致的方法: - -* 数据库和缓存数据**强一致**场景 : - * 更新 DB 时同样更新 cache,加一个锁来保证更新 cache 时不存在线程安全问题,这样可以增加命中率 - * 延迟双删:先淘汰缓存再写数据库,休眠1秒再次淘汰缓存,可以将1秒内造成的缓存脏数据再次删除 - * CDC 同步:通过 canal 订阅 MySQL binlog 的变更上报给 Kafka,系统监听 Kafka 消息触发缓存失效 -* 可以短暂地允许数据库和缓存数据**不一致**场景:更新 DB 的时候同样更新 cache,但是给缓存加一个比较短的过期时间,这样就可以保证即使数据不一致影响也比较小 - - - -参考文章:http://cccboke.com/archives/2020-09-30-21-29-56 - - **** @@ -10900,6 +10887,28 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 +### 缓存一致 + +使用缓存代表不需要强一致性,只需要最终一致性 + +缓存不一致的方法: + +* 数据库和缓存数据**强一致**场景 : + * 更新 DB 时同样更新 cache,加一个锁来保证更新 cache 时不存在线程安全问题,这样可以增加命中率 + * 延迟双删:先淘汰缓存再写数据库,休眠1秒再次淘汰缓存,可以将1秒内造成的缓存脏数据再次删除 + * CDC 同步:通过 canal 订阅 MySQL binlog 的变更上报给 Kafka,系统监听 Kafka 消息触发缓存失效 +* 可以短暂地允许数据库和缓存数据**不一致**场景:更新 DB 的时候同样更新 cache,但是给缓存加一个比较短的过期时间,这样就可以保证即使数据不一致影响也比较小 + + + +参考文章:http://cccboke.com/archives/2020-09-30-21-29-56 + + + +*** + + + ### 企业方案 #### 缓存预热 diff --git a/Issue.md b/Issue.md index f7f0127..b51d67c 100644 --- a/Issue.md +++ b/Issue.md @@ -1,6 +1,16 @@ -# 计算机基础 +# Base -## 计算机网络 +## Structure + + + + + +**** + + + +## Network ### 传输层 @@ -90,7 +100,7 @@ -## 操作系统 +## System ### 进程线程 diff --git a/Java.md b/Java.md index b3dc6a1..58383ec 100644 --- a/Java.md +++ b/Java.md @@ -84,7 +84,7 @@ Java语言提供了八种基本类型。六种数字类型(四个整数型, **boolean:** -- boolean数据类型表示一位的信息 +- boolean 数据类型表示一位的信息 - 只有两个取值:true 和 false - 这种类型只作为一种标志来记录 true/false 情况 - JVM 规范指出 boolean 当做 int 处理,也就是4字节,boolean 数组当做 byte 数组处理,这样可以得出 boolean 类型单独使用占了4个字节,在数组中是1个字节 @@ -93,7 +93,7 @@ Java语言提供了八种基本类型。六种数字类型(四个整数型, **char:** -- char类型是一个单一的 16 位 Unicode 字符 +- char 类型是一个单一的 16 位两个字节的 Unicode 字符 - 最小值是 **`\u0000`**(即为0) - 最大值是 **`\uffff`**(即为65,535) - char 数据类型可以储存任何字符 @@ -926,244 +926,53 @@ public class MethodDemo { } ``` - - - - - -**** - - - -### 排序 - -#### 冒泡排序 - -冒泡排序的作用:可以用于对数组或者对集合的元素进行大小排序! - -冒泡排序的核心算法思想: - int[] arr = new int[] {55, 22, 99, 88}; - 思想:每次从数组的第一个位置开始两两比较。把较大的元素与较小的元素进行层层交换。 - 最终把当前最大的一个元素存入到数组当前的末尾。这就是冒泡思想。 - - -冒泡排序的核心点:每次两两比较找出当前最大值冒出到当前最后面即可!! - -冒泡排序的实现核心: - 1.确定总共需要冒几轮: 数组的长度-1. - 2.每轮两两比较几次。 - -> i(轮数) 次数 每轮次数的规律:数组长度-i-1 次 -> 0 3 -> 1 2 -> 2 1 - -```java -// 0 1位置比较,大的放后面,然后1 2位置比较,大的继续放后面,一轮循环最后一位是最大值 - -public class BubbleSort { - public static void main(String[] args) { - int[] arr = new int[] {55, 22, 99, 88}; - // 1.定义一个循环控制总共需要冒泡几轮:数组的长度-1 - for(int i = 0 ; i < arr.length - 1 ; i++ ){ - // 2.控制每轮比较几次。 - // j = 1; j < arr.length - i; - for(int j = 0 ; j < arr.length - i - 1 ; j++ ){ - // 如果当前元素大于后一个元素 - if(arr[j] > arr[j+1]){ - // 交换位置。大的元素必须后移! - // 定义一个临时变量存储后一个元素 - int temp = arr[j+1]; - arr[j+1] = arr[j]; - arr[j] = temp; - } - } - } - System.out.println("数组:"+ Arrays.toString(arr)); - } -} -``` - - - -*** -#### 选择排序 - -选择排序的思想:从当前位置开始找出后面的较小值与该位置交换。 -数组:int[] arr = {5 , 1 , 3 , 2} - -选择排序的实现思路: - (1)控制选择几轮:数组的长度-1. - (2)控制每轮从当前位置开始比较几次。 - - > i(轮数) 次数 - > 0 3 - > 1 2 - > 2 1 - -```java -// 0 1位置比较,小的放0位置,然后0 2位置比,小的继续放0位置,一轮循环0位置是最小值 -public class SelectSort { - public static void main(String[] args) { - int[] arr = {5 , 1 , 3 , 2}; - // 1.定义一个循环控制选择几轮 - for(int i = 0 ; i < arr.length - 1 ; i++ ){ - // 2.定义一个循环控制每轮比较几次,一定是以当前位置与后面元素比较 - for(int j = i+1 ; j < arr.length ; j++ ){ - // 拿当前位置与j指定的元素进行大小比较,后面的较小就交换位置 - if(arr[j] < arr[i]){ - int temp = arr[i]; - arr[i] = arr[j]; - arr[j] = temp; - } - } - } - System.out.println("数组:"+ Arrays.toString(arr)); - } -} -``` - *** -#### 快速排序 - -快速排序算法中,每一次递归时以第一个数为基准数,找到数组中所有比基准数小的.再找到所有比基准数大的.小的全部放左边,大的全部放右边,确定基准数的正确位置 - -> 不用递归可以直接找出某个数字在数组中的位置 - -```java -public class MyQuiteSortDemo2 { - public static void main(String[] args) { -// 1,从右开始找比基准数小的 -// 2,从左开始找比基准数大的 -// 3,交换两个值的位置 -// 4,红色继续往左找,蓝色继续往右找,直到两个箭头指向同一个索引为止 -// 5,基准数归位 - int[] arr = {6, 1, 2, 7, 9, 3, 4, 5, 10, 8}; - quiteSort(arr,0,arr.length-1); - for (int i = 0; i < arr.length; i++) { - System.out.print(arr[i] + " "); - } - } +### Debug - private static void quiteSort(int[] arr, int left, int right) { - // 递归结束的条件 - if(right < left){ - return; - } - - int left0 = left; - int right0 = right; - int baseNumber = arr[left0];//计算出基准数 +Debug是供程序员使用的程序调试工具,它可以用于查看程序的执行流程,也可以用于追踪程序执行过程来调试程序。 - while(left != right){ - //1.从右开始找比基准数小的 - while(arr[right] >= baseNumber && right > left){ - right--; - } - //2.从左开始找比基准数大的 - while(arr[left] <= baseNumber && right > left){ - left++; - } - //3.交换两个值的位置 - int temp = arr[left]; - arr[left] = arr[right]; - arr[right] = temp; - } - //基准数归位 - int temp = arr[left]; - arr[left] = arr[left0]; - arr[left0] = temp; - - // 递归调用自己,将左半部分排好序 - quiteSort(arr,left0,--left); - // 递归调用自己,将右半部分排好序 - quiteSort(arr,++right,right0); - } -} -``` +加断点->Debug运行->单步运行->看Debugger窗口->看Console窗口 +![](https://gitee.com/seazean/images/raw/master/Java/Debug按键说明.png) +Debug条件断点 -*** -#### 二分查找 -> 正常查找:从第一个元素开始遍历,一个一个的往后找,综合查找比较耗时。 -二分查找的前提:对数组是有要求的,数组必须已经排好序。 -每次先与中间的元素进行比较,如果大于往右边找,如果小于往左边找,如果等于就返回该元素索引位置!如果没有该元素,返回-1。 -```java -/*定义一个方法,记录开始的索引位置和结束的索引位置。 -取出中间索引位置的值,拿元素与中间位置的值进行比较,如果小于中间值,结束位置=中间索引-1. -取出中间索引位置的值,拿元素与中间位置的值进行比较,如果大于中间值,开始位置=中间索引+1. -循环正常执行的条件:开始位置索引<=结束位置索引。否则说明寻找完毕但是没有该元素值返回-1.*/ - -public class BinarySerach { - public static void main(String[] args) { - // 1.数组 - int[] arr = {10, 14, 21, 38, 45, 47, 53, 81, 87, 99}; - // 2.需求是从数组中二分查询某个元素值的索引(提高性能) - System.out.println("81的索引是:" + binarySerach(arr,23)); - - } - - /** - * @param arr 被检索的数组 - * @param number 被检索的元素值 - * @return 返回元素在数组中的索引值,不存在该元素返回-1 - */ - public static int binarySerach(int[] arr , int number){ - // 3.记录当前区间搜索的开始索引和结束索引。 - int start = 0 ;//左 - int end = arr.length - 1;//右 - // 4.定义一个循环,反复去循环元素。 - while(start <= end){ - // 5.取中间索引位置 - int middleIndex = (start + end) / 2 ; - // 6.判断当前元素与中间元素的大小 - if(number < arr[middleIndex]){ - // 7.往左边继续寻找,结束索引应该-1 - end = middleIndex - 1; - }else if(number > arr[middleIndex]){ - start = middleIndex + 1; - }else if(number == arr[middleIndex]){ - return middleIndex; - } - } - // 如果上述循环执行完毕还没有返回索引,说明根本不存在该元素值,直接返回-1 - return -1; - } -} -``` +**** -*** +## 算法 ### 递归 #### 概述 -递归:方法在方法中又调用了自己。 +算法:解题方案的准确而完整的描述,是一系列解决问题的清晰指令,代表着用系统的方法解决问题的策略机制 + +递归:程序调用自身的编程技巧 递归: - 直接递归:自己的方法调用自己。 - 间接递归:自己的方法调用别的方法,别的方法又调用自己。 -注意: - 递归如果控制的不恰当,会形成递归的死循环,从而导致栈内存溢出错误!cjxch + +* 直接递归:自己的方法调用自己 +* 间接递归:自己的方法调用别的方法,别的方法又调用自己 + +递归如果控制的不恰当,会形成递归的死循环,从而导致栈内存溢出错误 @@ -1176,9 +985,10 @@ public class BinarySerach { ##### 核心思想 递归的三要素(理论): - 1.递归的终结点 - 2.递归的公式 - 3.递归的方向:必须走向终结点 + +1. 递归的终结点 +2. 递归的公式 +3. 递归的方向:必须走向终结点 ```java //f(x)=f(x-1)+1; f(1)=1; f(10)=? @@ -1337,17 +1147,109 @@ public class BeerDemo{ -### Debug +### 排序 -Debug是供程序员使用的程序调试工具,它可以用于查看程序的执行流程,也可以用于追踪程序执行过程来调试程序。 +#### 冒泡排序 -加断点->Debug运行->单步运行->看Debugger窗口->看Console窗口 +冒泡排序(Bubble Sort):两个数比较大小,较大的数下沉,较小的数冒起来 -![](https://gitee.com/seazean/images/raw/master/Java/Debug按键说明.png) +算法描述:每次从数组的第一个位置开始两两比较,把较大的元素与较小的元素进行层层交换,最终把当前最大的一个元素存入到数组当前的末尾 + +实现思路: + +1. 确定总共需要冒几轮:数组的长度-1 +2. 每轮两两比较几次 + + + +```java +// 0 1位置比较,大的放后面,然后1 2位置比较,大的继续放后面,一轮循环最后一位是最大值 +public class BubbleSort { + public static void main(String[] args) { + int[] arr = {55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88}; + //比较i和i+1,不需要再比最后一个位置 + for (int i = 0; i < arr.length - 1; i++) { + //最后i位不需要比,已经排序好 + for (int j = 0; j < arr.length - 1 - i; j++) { + if (arr[j] > arr[j + 1]) { + int temp = arr[j]; + arr[j] = arr[j + 1]; + arr[j + 1] = temp; + } + } + } + System.out.println(Arrays.toString(arr)); + } +} +``` + +冒泡排序时间复杂度:最坏情况 + +* 元素比较的次数为:`(N-1)+(N-2)+(N-3)+...+2+1=((N-1)+1)*(N-1)/2=N^2/2-N/2` +* 元素交换的次数为:`(N-1)+(N-2)+(N-3)+...+2+1=((N-1)+1)*(N-1)/2=N^2/2-N/2` +* 总执行次数为:`(N^2/2-N/2)+(N^2/2-N/2)=N^2-N` + +按照大 O 推导法则,保留函数中的最高阶项那么最终冒泡排序的时间复杂度为 O(N^2) + + + +*** + + + +#### 选择排序 + +##### 简单选择 + +选择排序(Selection-sort):一种简单直观的排序算法 + +算法描述:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕 + +实现思路: + +1. 控制选择几轮:数组的长度-1 +2. 控制每轮从当前位置开始比较几次 + + + +```java +// 0 1位置比较,小的放0位置,然后0 2位置比,小的继续放0位置,一轮循环0位置是最小值 +public class SelectSort { + public static void main(String[] args) { + int[] arr = {55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88}; + for (int i = 0; i < arr.length - 1; i++) { + //获取最小索引位置 + int minIndex = i; + for (int j = i + 1; j < arr.length; j++) { + if (arr[minIndex] > arr[j]) { + minIndex = j; + } + } + //交换元素 + int temp = arr[i]; + arr[i] = arr[minIndex]; + arr[minIndex] = temp; + } + System.out.println(Arrays.toString(arr)); + } +} +``` + +选择排序时间复杂度: + +* 数据比较次数:`(N-1)+(N-2)+(N-3)+...+2+1=((N-1)+1)*(N-1)/2=N^2/2-N/2` +* 数据交换次数:`N-1` +* 时间复杂度:`N^2/2-N/2+(N-1)=N^2/2+N/2-1` + +根据大 O 推导法则,保留最高阶项,去除常数因子,时间复杂度为 O(N^2) + + + +*** -Debug条件断点 +##### 堆排序 @@ -1355,6 +1257,437 @@ Debug是供程序员使用的程序调试工具,它可以用于查看程序的 +#### 插入排序 + +##### 直接插入 + +插入排序(Insertion Sort):在要排序的一组数中,假定前 n-1 个数已经排好序,现在将第 n 个数插到这个有序数列中,使得这 n 个数也是排好顺序的,如此反复循环,直到全部排好顺序 + + + +```java +public class InsertSort { + public static void main(String[] args) { + 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处的值大,则交换数据, + //如果不大,那么就找到合适的位置了,退出循环即可; + if (arr[j - 1] > arr[j]) { + int temp = arr[j]; + arr[j] = arr[j - 1]; + arr[j - 1] = temp; + } + } + } + System.out.println(Arrays.toString(arr)); + } +} +``` + +插入排序时间复杂度: + +* 比较的次数为:`(N-1)+(N-2)+(N-3)+...+2+1=((N-1)+1)*(N-1)/2=N^2/2-N/2` +* 交换的次数为:`(N-1)+(N-2)+(N-3)+...+2+1=((N-1)+1)(N-1)/2=N^2/2-N/2` +* 总执行次数为:`(N^2/2-N/2)+(N^2/2-N/2)=N^2-N` + +按照大 O 推导法则,保留函数中的最高阶项那么最终插入排序的时间复杂度为 O(N^2) + + + +*** + + + +##### 希尔排序 + +希尔排序(Shell Sort):也是一种插入排序,也称为缩小增量排序 + +实现思路: + +1. 选定一个增长量h,按照增长量h作为数据分组的依据,对数据进行分组 +2. 对分好组的每一组数据完成插入排序 +3. 减小增长量,最小减为1,重复第二步操作 + + + +希尔排序的核心在于间隔序列的设定,既可以提前设定好间隔序列,也可以动态的定义间隔序列。希尔排序就是插入排序增加了间隔 + +```java +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的初始值 + 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] + if (arr[j] < arr[j - h]) { + int temp = arr[j]; + arr[j] = arr[j - h]; + arr[j - h] = temp; + } + } + } + //3. 减小h的值,减小规则为: + h = h / 2; + } + System.out.println(Arrays.toString(arr)); + } +} +``` + +在希尔排序中,增长量h并没有固定的规则,有很多论文研究了各种不同的递增序列,但都无法证明某个序列是最 +好的,所以对于希尔排序的时间复杂度分析就认为 O(nlogn) + + + +*** + + + +#### 归并排序 + +##### 实现方式 + +归并排序(Merge Sort):建立在归并操作上的一种有效的排序算法,该算法是采用分治法的典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 + +实现思路: + +1. 一组数据拆分成两个元素相等的子组,并对每一个子组继续拆分,直到拆分后的每个子组的元素个数是1为止 +2. 将相邻的两个子组进行合并成一个有序的大组 +3. 不断的重复步骤2,直到最终只有一个组为止 + + + +归并步骤:每次比较两端最小的值,把最小的值放在辅助数组的左边 + +![](https://gitee.com/seazean/images/raw/master/Java/Sort-归并步骤1.png) + +![](https://gitee.com/seazean/images/raw/master/Java/Sort-归并步骤2.png) + +![](https://gitee.com/seazean/images/raw/master/Java/Sort-归并步骤3.png) + + + + + +*** + + + +##### 实现代码 + +```java +public class MergeSort { + public static void main(String[] args) { + int[] arr = new int[]{55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88}; + int[] newArr = mergetSort(arr, 0, arr.length - 1); + System.out.println(Arrays.toString(newArr)); + } + + public static int[] mergetSort(int[] arr, int low, int high) { + if (high < low) { + throw new ArrayIndexOutOfBoundsException("索引输入错了"); + } + //递归结束的条件 + if (low == high) { + return new int[]{arr[low]}; + } + int mid = low + (high - low) / 2; + int[] leftArr = mergetSort(arr, low, mid);//左有序数组 + int[] rightArr = mergetSort(arr, mid + 1, high);//右有序数组 + int[] newArr = new int[leftArr.length + rightArr.length];//新有序数组 + + int m = 0; + int l = 0, r = 0;//定义左右指针 + while (l < leftArr.length && r < rightArr.length) { + newArr[m++] = leftArr[l] < rightArr[r] ? leftArr[l++] : rightArr[r++]; + } + + while (l < leftArr.length) { + newArr[m++] = leftArr[l++]; + } + while (r < rightArr.length) { + newArr[m++] = rightArr[r++]; + } + return newArr; + } +} +``` + + + +用树状图来描述归并,如果一个数组有8个元素,那么每次除以2找最小的子数组,共拆 log8 次,所以树共有3层,那么自顶向下第 k 层有 `2^k` 个子数组,每个数组的长度为 `2^(3-k)`,归并最多需要 `2^(3-k)` 次比较()。因此每层的比较次数为 `2^k * 2^(3-k)=2^3`,那么3层总共为`3*2^3` + +假设元素的个数为 n,那么使用归并排序拆分的次数为 `log2(n)`,那么使用 `log2(n)` 替换`3*2^3`中的这个层数,最终得出的归并排序的时间复杂度为 `log2(n)* 2^(log2(n))=log2(n)*n`,根据大O推导法则,忽略底 +数,最终归并排序的时间复杂度为 O(nlogn) + +归并排序的缺点:需要申请额外的数组空间,导致空间复杂度提升,是典型的**以空间换时间**的操作 + + + + + +**** + + + +#### 快速排序 + +快速排序(Quick Sort):通过**分治思想**对冒泡排序的改进,基本过程是通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,以此达到整个数据变成有序序列 + +实现思路: + +1. 从数列中挑出一个元素,称为基准(pivot) +2. 重新排序数列,所有比基准值小的摆放在基准前面,所有比基准值大的摆在基准的后面(相同的数可以到任一边),在这个分区退出之后,该基准就处于数列的中间位置,这个称为分区(partition)操作; +3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序 + + + +```java +public class QuickSort { + public static void main(String[] args) { + int[] arr = {55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88}; + quickSort(arr, 0, arr.length - 1); + System.out.println(Arrays.toString(arr)); + } + + public static void quickSort(int[] arr, int low, int high) { + int left = low; + int right = high; + + if (low >= high) { + return; + } + int temp = arr[low];//基准数 + while (left < right) { + // 用 <= 可以防止多余的交换 + while (arr[right] >= temp && right > left) { + right--; + } + // arr[right] < temp 放在左边,做判断防止相等 + if (right > left) { + arr[left] = arr[right];//此时把arr[right]元素视为空 + left++; + } + while (arr[left] <= temp && left < right) { + left++; + } + if (right > left) { + arr[right] = arr[left]; + right--; + } + } + // left == right + arr[left] = temp; + quickSort(arr, low, left-1); + quickSort(arr, right + 1, high); + } +} +``` + +快速排序和归并排序的区别: + +* 快速排序是另外一种分治的排序算法,将一个数组分成两个子数组,将两部分独立的排序 +* 快速排序和归并排序是互补的:归并排序将数组分成两个子数组分别排序,并将有序的子数组归并从而将整个数组排序,而快速排序的方式则是当两个数组都有序时,整个数组自然就有序了 +* 在归并排序中,一个数组被等分为两半,归并调用发生在处理整个数组之前,在快速排序中,切分数组的位置取决于数组的内容,递归调用发生在处理整个数组之后 + +时间复杂度: + +* 最优情况:每一次切分选择的基准数字刚好将当前序列等分。把数组的切分看做是一个树,共切分了logn次,所以,最优情况下快速排序的时间复杂度为 O(nlogn) + +* 最坏情况:每一次切分选择的基准数字是当前序列中最大数或者最小数,这使得每次切分都会有一个子组,那么总共就得切分n次,所以最坏情况下,快速排序的时间复杂度为 O(n^2) + + + +* 平均情况:每一次切分选择的基准数字不是最大值和最小值,也不是中值,这种情况用数学归纳法证明,快速排序的时间复杂度为 O(nlogn) + + + +推荐视频:https://www.bilibili.com/video/BV1b7411N798?t=1001&p=81 + +参考文章:https://blog.csdn.net/nrsc272420199/article/details/82587933 + + + + + +**** + + + +#### 基数排序 + +基数排序(Radix Sort):又叫桶排序和箱排序,借助多关键字排序的思想对单逻辑关键字进行排序的方法 + +按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前 + + + +```java +public class Test { + public static void main(String[] args) { + int[] arr = new int[]{55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88}; + bucketSort(arr); + System.out.println(Arrays.toString(arr)); + } + + public static void bucketSort(int[] arr){ + // 计算最大值与最小值 + int max = Integer.MIN_VALUE; + int min = Integer.MAX_VALUE; + for(int i = 0; i < arr.length; i++){ + max = Math.max(max, arr[i]); + min = Math.min(min, arr[i]); + } + + // 计算桶的数量 + int bucketNum = (max - min) / arr.length + 1; + ArrayList> bucketArr = new ArrayList<>(bucketNum); + for(int i = 0; i < bucketNum; i++){ + bucketArr.add(new ArrayList()); + } + + // 将每个元素放入桶 + for(int i = 0; i < arr.length; i++){ + int num = (arr[i] - min) / (arr.length); + bucketArr.get(num).add(arr[i]); + } + + // 对每个桶进行排序 + for(int i = 0; i < bucketArr.size(); i++){ + Collections.sort(bucketArr.get(i)); + } + + // 将桶中的元素赋值到原序列 + int index = 0; + for(int i = 0; i < bucketArr.size(); i++){ + for(int j = 0; j < bucketArr.get(i).size(); j++){ + arr[index++] = bucketArr.get(i).get(j); + } + } + } +} +``` + + + +推荐视频:https://www.bilibili.com/video/BV1b7411N798?p=86 + +参考文章:https://www.toutiao.com/a6593273307280179715/?iid=6593273307280179715 + + + +*** + + + +#### 稳定性 + +稳定性:在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中 `r[i]=r[j]`,且 r[i] 在 r[j] 之前,而在排序后的序列中,r[i] 仍在 r[j] 之前,则称这种排序算法是稳定的,否则称为不稳定的 + +如果一组数据只需要一次排序,则稳定性一般是没有意义的,如果一组数据需要多次排序,稳定性是有意义的。 + + + +* 冒泡排序:只有当 `arr[i]>arr[i+1]` 的时候,才会交换元素的位置,而相等的时候并不交换位置,所以冒泡排序是一种稳定排序算法 +* 选择排序:是给每个位置选择当前元素最小的,例如有数据{5(1),8 ,5(2), 3, 9 },第一遍选择到的最小元素为3,所以5(1)会和3进行交换位置,此时5(1)到了5(2)后面,破坏了稳定性,所以是不稳定的排序算法 +* 插入排序:比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么把要插入的元素放在相等元素的后面。相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的 +* 希尔排序:按照不同步长对元素进行插入排序,虽然一次插入排序是稳定的,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以希尔排序是不稳定的 +* 归并排序在归并的过程中,只有 `arr[i] arr[mid]) { + start = mid + 1; + } else if (des < arr[mid]) { + end = mid - 1; + } + } + // 如果上述循环执行完毕还没有返回索引,说明根本不存在该元素值,直接返回-1 + return -1; + } +} +``` + +![](https://gitee.com/seazean/images/raw/master/Java/二分查找.gif) + + + + + + + +*** + + + + + ## 对象 ### 概述 @@ -1371,6 +1704,10 @@ Debug是供程序员使用的程序调试工具,它可以用于查看程序的 +*** + + + ### 类 #### 定义 @@ -2488,9 +2825,7 @@ new 类名|抽象类|接口(形参){ * 匿名内部类不能定义静态成员 * 匿名内部类一旦写出来,就会立即创建一个匿名内部类的对象返回 * **匿名内部类的对象的类型相当于是当前new的那个的类型的子类类型** -* 匿名内部类引用局部变量,局部变量必须是**常量**,底层创建为内部类的成员变量(JVM-->类加载-->编译优化-->内部类) - * 在Java中方法调用是值传递的,在匿名内部类中对变量的操作都是基于原变量的副本,不会影响到原变量的值,所以原变量的值的改变也无法同步到副本中 - * 外部变量为final是在编译期以强制手段确保用户不会在内部类中做修改原变量值的操作,也是防止外部操作修改了变量而内部类无法随之变化出现的影响 +* 匿名内部类引用局部变量必须是**常量**,底层创建为内部类的成员变量(原因:JVM → 运行机制 → 代码优化) ```java public class Anonymity { @@ -3971,7 +4306,7 @@ public class RegexDemo { ## 集合 -### 基本介绍 +### 集合概述 集合是一个大小可变的容器,容器中的每个数据称为一个元素 @@ -3986,21 +4321,17 @@ public class RegexDemo { -**** - +*** -### 数据结构 -#### 基本介绍 +### 存储结构 -什么是数据结构? +#### 数据结构 -* 数据结构指的是数据以什么方式组织在一起,不同的数据结构,增删查的性能是不一样的 -* 不同的集合底层会采用不同的数据结构,了解集合的底层是基于哪种数据结构操作数据才能知道具体场景 +数据结构指的是数据以什么方式组织在一起,不同的数据结构,增删查的性能是不一样的 -Java常见的数据结构有哪些? - 数据存储的常用结构有:栈、队列、数组、链表和红黑树 +数据存储的常用结构有:栈、队列、数组、链表和红黑树 * 队列(queue):先进先出,后进后出。(FIFO first in first out) 场景:各种排队、叫号系统,有很多集合可以实现队列 @@ -4096,6 +4427,7 @@ Java常见的数据结构有哪些? ![平衡二叉树右旋](https://gitee.com/seazean/images/raw/master/Java/平衡二叉树右旋01.png) * 平衡二叉树旋转的四种情况 + * 左左:当根节点左子树的左子树有节点插入,导致二叉树不平衡 * 如何旋转:直接对整体进行右旋即可 ![平衡二叉树左左](https://gitee.com/seazean/images/raw/master/Java/平衡二叉树左左.png) @@ -4163,7 +4495,9 @@ Java常见的数据结构有哪些? -*** + + +**** @@ -4173,7 +4507,6 @@ Java常见的数据结构有哪些? > Java中集合的代表是:Collection. > Collection集合是Java中集合的祖宗类。 -> 学习Collection集合的功能,那么一切集合都可以用这些功能!! Collection集合底层为数组:`[value1, value2, ....]` @@ -4409,7 +4742,7 @@ public class ArrayList extends AbstractList } ``` - 如果需要的容量大于数组长度,进行扩容 + 如果需要的容量大于数组长度,进行扩容: ```java //判断是否需要扩容 @@ -4421,9 +4754,22 @@ public class ArrayList extends AbstractList } ``` + 指定索引插入,在旧数组上操作: + + ```java + public void add(int index, E element) { + rangeCheckForAdd(index); + ensureCapacityInternal(size + 1); // Increments modCount!! + System.arraycopy(elementData, index, elementData, index + 1, + size - index); + elementData[index] = element; + size++; + } + ``` + * 扩容:新容量的大小为 `oldCapacity + (oldCapacity >> 1)`,`oldCapacity >> 1` 需要取整,所以新容量大约是旧容量的 1.5 倍左右,即 oldCapacity+oldCapacity/2 - 扩容操作需要调用`Arrays.copyOf()`(底层`System.arraycopy()`)把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数 + 扩容操作需要调用`Arrays.copyOf()`(底层`System.arraycopy()`)把原数组整个复制到**新数组**中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数 ```java private void grow(int minCapacity) { @@ -4446,7 +4792,7 @@ public class ArrayList extends AbstractList * OutOfMemoryError:Requested array size exceeds VM limit(请求的数组大小超出VM限制) * OutOfMemoryError: Java heap space(堆区内存不足,可以通过设置JVM参数 -Xmx 来调节) -* 删除元素:需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),可以看到 ArrayList 删除元素的代价是非常高的 +* 删除元素:需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,在旧数组上操作,该操作的时间复杂度为 O(N),可以看到 ArrayList 删除元素的代价是非常高的, ```java private void fastRemove(Object[] es, int i) { @@ -4714,7 +5060,7 @@ Set集合添加的元素是无序,不重复的。 LinkedHashSet 为什么是有序的? -LinkedHashSet 底层依然是使用哈希表存储元素的,但是每个元素都额外带一个链来维护添加顺序,不光增删查快,还有序,缺点是多了一个存储顺序的链会**占内存空间**,而且不允许重复,无索引 +LinkedHashSet 底层依然是使用哈希表存储元素的,但是每个元素都额外带一个链来维护添加顺序,不光增删查快,还有顺序,缺点是多了一个存储顺序的链会**占内存空间**,而且不允许重复,无索引 @@ -11076,12 +11422,12 @@ public class TestDemo{ XML介绍: -- XML 指可扩展标记语言(**EXtensible Markup Language**) +- XML 指可扩展标记语言(EXtensible Markup Language) - XML 是一种**标记语言**,很类似 HTML,HTML文件也是XML文档 - XML 的设计宗旨是**传输数据**,而非显示数据 -- XML 标签没有被预定义。您需要**自行定义标签**。 -- XML 被设计为具有**自我描述性(就是易于阅读)**。 -- XML 是 **W3C 的推荐标准** +- XML 标签没有被预定义,需要自行定义标签 +- XML 被设计为具有自我描述性,易于阅读 +- XML 是 W3C 的推荐标准 **xml与html的区别**: @@ -11919,12 +12265,12 @@ public class XPathDemo { ### 基本介绍 -JVM:全称Java Virtual Machine,即Java虚拟机,一种规范,本身是一个虚拟计算机,直接和操作系统进行交互,与硬件不直接交互,而操作系统可以帮我们完成和硬件进行交互的工作 +JVM:全称 Java Virtual Machine,即 Java 虚拟机,一种规范,本身是一个虚拟计算机,直接和操作系统进行交互,与硬件不直接交互,而操作系统可以帮我们完成和硬件进行交互的工作 特点: -* Java虚拟机基于**二进制字节码**执行,由一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆、一个方法区等组成 -* JVM屏蔽了与操作系统平台相关的信息,从而能够让Java程序只需要生成能够在JVM上运行的字节码文件,通过该机制实现的**跨平台性** +* Java 虚拟机基于**二进制字节码**执行,由一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆、一个方法区等组成 +* JVM 屏蔽了与操作系统平台相关的信息,从而能够让 Java 程序只需要生成能够在 JVM 上运行的字节码文件,通过该机制实现的**跨平台性** Java代码执行流程:java程序 --(编译)--> 字节码文件 --(解释执行)--> 操作系统(Win,Linux) @@ -11970,7 +12316,7 @@ JVM的生命周期分为三个阶段,分别为:启动、运行、死亡。 - **启动**:当启动一个 Java 程序时,通过引导类加载器(bootstrap class loader)创建一个初始类(initial class),对于拥有 main 函数的类就是 JVM 实例运行的起点 - **运行**: - main() 方法是一个程序的初始起点,任何线程均可由在此处启动 - - 在 JVM 内部有两种线程类型,分别为:**用户线程和守护线程**,JVM通常使用的是守护线程,而main()和其他线程使用的是用户线程,守护线程会随着用户线程的结束而结束 + - 在 JVM 内部有两种线程类型,分别为:**用户线程和守护线程**,JVM 通常使用的是守护线程,而 main() 和其他线程使用的是用户线程,守护线程会随着用户线程的结束而结束 - 执行一个 Java 程序时,真真正正在执行的是一个 Java 虚拟机的进程 - JVM 有两种运行模式 Server 与 Client,两种模式的区别在于:Client 模式启动速度较快,Server 模式启动较慢;但是启动进入稳定期长期运行之后 Server 模式的程序运行速度比 Client 要快很多 - **死亡**: @@ -12500,7 +12846,7 @@ public class Demo1_27 { ### 变量位置 -变量的位置不取决于它是基本数据类型还是引用数据类型,取决于它的声明位置 +**变量的位置不取决于它是基本数据类型还是引用数据类型,取决于它的声明位置** 静态内部类和其他内部类: @@ -15553,7 +15899,7 @@ Exception table: -#### 默认构造器 +#### 构造器 ```java public class Candy1 { @@ -15576,7 +15922,7 @@ public class Candy1 { -#### 自动拆装箱 +#### 拆装箱 ```java Integer x = 1; @@ -15670,7 +16016,7 @@ for (int e : array) { } ``` -编译后: +编译后为循环取数: ```java for(int i = 0; i < array.length; ++i) { @@ -15755,7 +16101,7 @@ switch(x) { 总结: -* 执行了两遍 switch,第一遍是根据字符串的 hashCode 和 equals 将字符串的转换为相应byte 类型,第二遍才是利用 byte 执行进行比较 +* 执行了两遍 switch,第一遍是根据字符串的 hashCode 和 equals 将字符串的转换为相应 byte 类型,第二遍才是利用 byte 执行进行比较 * hashCode 是为了提高效率,减少可能的比较;而 equals 是为了防止 hashCode 冲突 @@ -15776,9 +16122,11 @@ public class Candy7 { public static void foo(Sex sex) { switch (sex) { case MALE: - System.out.println("男"); break; + System.out.println("男"); + break; case FEMALE: - System.out.println("女"); break; + System.out.println("女"); + break; } } } @@ -15866,7 +16214,7 @@ try(资源变量 = 创建资源对象){ } ``` -其中资源对象需要实现 **AutoCloseable**接口,例如 InputStream、OutputStream、Connection、Statement、ResultSet等接口都实现了 AutoCloseable ,使用 try-withresources可以不用写 finally 语句块,编译器会帮助生成关闭资源代码: +其中资源对象需要实现 **AutoCloseable**接口,例如 InputStream、OutputStream、Connection、Statement、ResultSet 等接口都实现了 AutoCloseable ,使用 try-withresources可以不用写 finally 语句块,编译器会帮助生成关闭资源代码: ```java try(InputStream is = new FileInputStream("d:\\1.txt")) { @@ -15923,7 +16271,7 @@ try { 方法重写时对返回值分两种情况: * 父子类的返回值完全一致 -* 子类返回值可以是父类返回值的子类(比较绕口,见下面的例子) +* 子类返回值可以是父类返回值的子类 ```java class A { @@ -15965,6 +16313,8 @@ class B extends A { #### 匿名内部类 +##### 无参优化 + 源代码: ```java @@ -15991,9 +16341,6 @@ final class Candy11$1 implements Runnable { System.out.println("ok"); } } -``` - -```java public class Candy11 { public static void main(String[] args) { Runnable runnable = new Candy11$1(); @@ -16003,6 +16350,8 @@ public class Candy11 { +##### 带参优化 + 引用局部变量的匿名内部类,源代码: ```java @@ -16037,7 +16386,13 @@ public class Candy11 { } ``` -局部变量必须是 final 的:因为在创建Candy11¥1 对象时,将 x 的值赋值给了 Candy11$1 对象的 val属性,x不应该再发生变化了,因为发生变化,this.val​x属性没有机会再跟着变化 +局部变量在底层创建为内部类的成员变量,必须是 final 的原因: + +* 在Java中方法调用是值传递的,在匿名内部类中对变量的操作都是基于原变量的副本,不会影响到原变量的值,所以原变量的值的改变也无法同步到副本中 + +* 外部变量为final是在编译期以强制手段确保用户不会在内部类中做修改原变量值的操作,也是防止外部操作修改了变量而内部类无法随之变化出现的影响 + + 在创建 `Candy11$1 ` 对象时,将 x 的值赋值给了 `Candy11$1` 对象的 val 属性,x 不应该再发生变化了,因为发生变化,this.val$x 属性没有机会再跟着变化 @@ -16063,7 +16418,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 { @@ -17196,7 +17551,7 @@ LocalVariableTable: -#### 锁优化 +#### 锁升级 ##### 偏向锁 @@ -17408,6 +17763,8 @@ public class SpinLock { +#### 锁优化 + ##### 锁消除 锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除 @@ -20694,13 +21051,13 @@ private final HashSet workers = new HashSet(); 构造方法: ```java -public ThreadPoolExecutor( int corePoolSize, - int maximumPoolSize, - long keepAliveTime, - TimeUnit unit, - BlockingQueue workQueue, - ThreadFactory threadFactory, - RejectedExecutionHandler handler) +public ThreadPoolExecutor(int corePoolSize, + int maximumPoolSize, + long keepAliveTime, + TimeUnit unit, + BlockingQueue workQueue, + ThreadFactory threadFactory, + RejectedExecutionHandler handler) ``` 参数介绍: @@ -20728,7 +21085,7 @@ public ThreadPoolExecutor( int corePoolSize, 补充:其他框架拒绝策略 - * Dubbo:在抛出 RejectedExecutionException 异常前记录日志,并dump线程栈信息,方便定位问题 + * Dubbo:在抛出 RejectedExecutionException 异常前记录日志,并 dump 线程栈信息,方便定位问题 * Netty:创建一个新线程来执行任务 * ActiveMQ:带超时等待(60s)尝试放入队列 * PinPoint:它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略 @@ -20739,14 +21096,14 @@ public ThreadPoolExecutor( int corePoolSize, 1. 创建线程池,这时没有创建线程(**懒惰**),等待提交过来的任务请求,调用execute方法才会创建线程 -2. 当调用execute()方法添加一个请求任务时,线程池会做如下判断: - * 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务 - * 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列 - * 如果这时队列满了且正在运行的线程数量还小于maximumPoolSize,那么会创建非核心线程**立刻运行**这个任务(对于阻塞队列中的任务不公平) - * 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行 +2. 当调用 execute() 方法添加一个请求任务时,线程池会做如下判断: + * 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务 + * 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列 + * 如果这时队列满了且正在运行的线程数量还小于 maximumPoolSize,那么会创建非核心线程**立刻运行**这个任务(对于阻塞队列中的任务不公平) + * 如果队列满了且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会启动饱和**拒绝策略**来执行 3. 当一个线程完成任务时,会从队列中取下一个任务来执行 -4. 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断:如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉,所以线程池的所有任务完成后最终会收缩到corePoolSize的大小 +4. 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断:如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉,所以线程池的所有任务完成后最终会收缩到 corePoolSize 大小 @@ -20783,7 +21140,7 @@ Executors提供了四种线程池的创建:newCachedThreadPool、newFixedThrea ``` * 核心线程数是 0, 最大线程数是 Integer.MAX_VALUE,全部都是救急线程(60s 后可以回收),可能会创建大量线程,从而导致 **OOM** - * SynchronousQueue作为阻塞队列,没有容量,对于每一个take的线程会阻塞直到有一个put的线程放入元素为止(类似一手交钱、一手交货) + * SynchronousQueue 作为阻塞队列,没有容量,对于每一个take的线程会阻塞直到有一个put的线程放入元素为止(类似一手交钱、一手交货) * 适合任务数比较密集,但每个任务执行时间较短的情况 @@ -20829,9 +21186,9 @@ Executors提供了四种线程池的创建:newCachedThreadPool、newFixedThrea Executors返回的线程池对象弊端如下: - - FixedThreadPool和SingleThreadPool: + - FixedThreadPool 和 SingleThreadPool: - 运行的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM - - CacheThreadPool和ScheduledThreadPool: + - CacheThreadPool 和 ScheduledThreadPool: - 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM 创建多大容量的线程池合适? diff --git a/SSM.md b/SSM.md index a63a48b..4969eab 100644 --- a/SSM.md +++ b/SSM.md @@ -2391,7 +2391,7 @@ ApplicationContext子类相关API: ![](https://gitee.com/seazean/images/raw/master/Frame/DI介绍.png) -IoC和DI的关系:IoC与DI是同一件事站在不同角度看待问题 +IoC 和 DI 的关系:IoC 与 DI是同一件事站在不同角度看待问题 diff --git a/Tool.md b/Tool.md index a889196..4912bfc 100644 --- a/Tool.md +++ b/Tool.md @@ -1417,8 +1417,6 @@ mode : 权限设定字串,格式: [ugoa...][[+-=][rwxX]...][,...] - xyz : 就是刚刚提到的数字类型的权限属性,为 rwx 属性数值的相加。 - -R : 进行递归(recursive)的持续变更,亦即连同次目录下的所有文件都会变更 - - 文件的权限字符为:[-rwxrwxrwx], 这九个权限是三三一组的,我们使用数字来代表各个权限。 @@ -1437,8 +1435,6 @@ mode : 权限设定字串,格式: [ugoa...][[+-=][rwxX]...][,...] ##### 符号权限 - - ![](https://gitee.com/seazean/images/raw/master/Tool/权限符号表.png) - user 属主权限 diff --git a/Web.md b/Web.md index 7730300..596a4d9 100644 --- a/Web.md +++ b/Web.md @@ -2083,9 +2083,9 @@ HTTP:Hyper Text Transfer Protocol,意为超文本传输协议,是建立在 HTTP协议是**一个无状态的面向连接的协议**,指的是协议对于事务处理没有记忆能力,服务器不知道客户端是什么状态。所以打开一个服务器上的网页和上一次打开这个服务器上的网页之间没有任何联系 -注意:无状态并不是代表HTTP就是UDP,面向连接也不是代表HTTP就是TCP +注意:无状态并不是代表 HTTP 就是 UDP,面向连接也不是代表 HTTP 就 是TCP -HTTP作用:用于定义WEB浏览器与WEB服务器之间交换数据的过程和数据本身的内容 +HTTP作用:用于定义 WEB 浏览器与WEB服务器之间交换数据的过程和数据本身的内容 浏览器和服务器交互过程:浏览器请求,服务请求响应 @@ -4136,7 +4136,9 @@ public class ServletDemo08 extends HttpServlet { **常用的会话管理技术**: * Cookie:客户端会话管理技术,用户浏览的信息以键值对(key=value)的形式保存在浏览器上。如果没有关闭浏览器,再次访问服务器,会把cookie带到服务端,服务端就可以做相应的处理 -* Session:服务端会话管理技术。服务器为每一个浏览器开辟一块内存空间,即session。由于内存空间是每一个浏览器独享的,所有用户在访问的时候,可以把信息保存在session对象中。同时,每一个session对象都对应一个sessionId,服务器把sessionId写到cookie中,再次访问的时候,浏览器会把cookie(sessionId)带过来,找到对应的session对象。 +* Session:服务端会话管理技术。当客户端第一次请求 session 对象时候,服务器为每一个浏览器开辟一块内存空间,并将通过特殊算法算出一个 session 的 ID,用来标识该 session 对象。由于内存空间是每一个浏览器独享的,所有用户在访问的时候,可以把信息保存在session对象中。同时服务器把sessionId写到cookie中,再次访问的时候,浏览器会把cookie(sessionId)带过来,找到对应的session对象。 + + tomcat 生成的 sessionID 叫做 jsessionID 两者区别: @@ -4148,8 +4150,12 @@ public class ServletDemo08 extends HttpServlet { * Session 通过服务端记录用户的状态,服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户 +* Cookie 只能存储 ASCII 码,而 Session 可以存储任何类型的数据 + +参考文章:https://blog.csdn.net/weixin_43625577/article/details/92393581 + *** @@ -4192,7 +4198,7 @@ Cookie:客户端会话管理技术,把要共享的数据保存到了客户 * `Cookie(String name, String value)` : 构造方法创建Cookie对象 - * Cookie属性对应的set和get方法,name属性被final修饰,没有set方法 + * Cookie 属性对应的set和get方法,name属性被final修饰,没有set方法 * HttpServletResponse类API: `void addCookie(Cookie cookie)` : 向客户端添加Cookie,Adds the specified cookie to the response. @@ -4208,6 +4214,10 @@ Cookie:客户端会话管理技术,把要共享的数据保存到了客户 #### 有效期 +如果不设置过期时间,表示这个cookie生命周期为浏览器会话期间,只要关闭浏览器窗口cookie就消失,这种生命期为浏览会话期的cookie被称为会话cookie,会话cookie一般不保存在硬盘上而是保存在内存里。 + +如果设置过期时间,浏览器就会把cookie保存到硬盘上,关闭后再次打开浏览器,这些cookie依然有效直到超过设定的过期时间。存储在硬盘上的cookie可以在**不同的浏览器进程间共享**,比如两个IE窗口,而对于保存在内存的cookie,不同的浏览器有不同的处理方式 + 设置Cookie存活时间API:`void setMaxAge(int expiry)` * -1:默认。代表Cookie数据存到浏览器关闭(保存在浏览器文件中) @@ -4297,8 +4307,6 @@ XSS 全称 Cross SiteScript,跨站脚本攻击,是Web程序中常见的漏 - - *** @@ -4336,6 +4344,12 @@ HttpServletRequest类获取Session: + + +*** + + + #### 常用API | 方法 | 说明 | @@ -4349,13 +4363,15 @@ HttpServletRequest类获取Session: +**** + #### 实现会话 通过第一个Servlet设置共享的数据用户名,并在第二个Servlet获取到。 -项目执行完以后,去浏览器抓包,Request Headers中的Cookie JSESSIONID的值是一样的 +项目执行完以后,去浏览器抓包,Request Headers 中的 Cookie JSESSIONID的值是一样的 ```java @WebServlet("/servletDemo01") @@ -4401,6 +4417,32 @@ public class ServletDemo02 extends HttpServlet{ +**** + + + +#### 生命周期 + +Session 的创建:一个常见的错误是以为 Session 在有客户端访问时就被创建,事实是直到某 server 端程序(如Servlet)调用 `HttpServletRequest.getSession(true)` 这样的语句时才会被创建 + +Session 在以下情况会被删除: + +* 程序调用 HttpSession.invalidate() +* 距离上一次收到客户端发送的 session id 时间间隔超过了 session 的最大有效时间 +* 服务器进程被停止 + +注意事项: + +* 客户端只保存 sessionID 到 cookie 中,而不会保存 session +* 关闭浏览器只会使存储在客户端浏览器内存中的 cookie 失效,不会使服务器端的 session 对象失效,同样也不会使已经保存到硬盘上的持久化cookie消失 + +打开两个浏览器窗口访问应用程序会使用的是不同的session,通常 session cookie 是不能跨窗口使用,当新开了一个浏览器窗口进入相同页面时,系统会赋予一个新的 session id,实现跨窗口信息共享: + +* 先把 session id 保存在 persistent cookie 中(通过设置session的最大有效时间) +* 在新窗口中读出来,就可以得到上一个窗口的 session id,这样通过 session cookie 和 persistent cookie 的结合就可以实现跨窗口的会话跟踪 + + + *** @@ -4457,7 +4499,9 @@ public class ServletDemo02 extends HttpServlet{ #### 钝化活化 -钝化:序列化,持久态。把长时间不用,但还不到过期时间的HttpSession进行序列化写到磁盘上。 +Session 存放在服务器端的内存中,可以做持久化管理。 + +钝化:序列化,持久态。把长时间不用,但还不到过期时间的 HttpSession 进行序列化写到磁盘上。 活化:相反的状态 From 3c5666c172497abca67759722693511b9080febf Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 23 Jun 2021 13:07:52 +0800 Subject: [PATCH 056/242] Update README.md --- README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 817da9e..75b381c 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,4 @@ -**Java** 学习笔记,记录着作者从编程入门到深入学习的所有知识,每次学有所获就会更新笔记,所有的知识都有借鉴其他前辈的博客和文章,排版布局美观整洁,使用 Typora 阅读效果更佳,希望对各位朋友有所帮助。 - -声明:所有的知识不保证权威性,如果各位朋友发现错误,非常欢迎与我讨论。 - -邮箱:imseazean@gmail.com +**Java** 学习笔记,记录着作者从编程入门到深入学习的所有知识,每次学有所获就会更新笔记,所有的知识都有借鉴其他前辈的博客和文章,排版布局美观整洁,如果对各位朋友有所帮助,希望可以给个 star。 内容说明: @@ -12,3 +8,11 @@ * SSM:MyBatis、Spring、SpringMVC、SpringBoot * Tool:Git、Linux、Docker * Web:HTML、CSS、HTTP、Servlet、JavaScript + +其他说明: + +* 所有的知识不保证权威性,如果各位朋友发现错误,非常欢迎与我讨论。 + +* Java.md 更新后大于 1M,导致网页无法显示,所以推荐大家使用 Typora 阅读笔记 + +个人邮箱:imseazean@gmail.com From e78fcf44cb25d254d0d177f9b47ead07dd1849c3 Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 23 Jun 2021 13:08:41 +0800 Subject: [PATCH 057/242] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 75b381c..a1baf15 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,6 @@ * 所有的知识不保证权威性,如果各位朋友发现错误,非常欢迎与我讨论。 -* Java.md 更新后大于 1M,导致网页无法显示,所以推荐大家使用 Typora 阅读笔记 +* Java.md 更新后大于 1M,导致网页无法显示,推荐大家使用 Typora 阅读笔记。 个人邮箱:imseazean@gmail.com From 9eb609908cc5f80d5029cbe84e2371d7efce7716 Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 24 Jun 2021 21:57:18 +0800 Subject: [PATCH 058/242] Update Java Notes --- DB.md | 54 ++++---- Java.md | 381 ++++++++++++++++++++++++++++++++++++++++---------------- SSM.md | 11 +- 3 files changed, 309 insertions(+), 137 deletions(-) diff --git a/DB.md b/DB.md index cd361ee..d12a7f3 100644 --- a/DB.md +++ b/DB.md @@ -2136,7 +2136,7 @@ undo log 是采用段 (segment) 的方式来记录,每个 undo 操作在记录 rollback segment 称为回滚段,每个回滚段中有 1024 个 undo log segment * 在以前老版本,只支持1个 rollback segment,只能记录 1024 个 undo log segment -* MySQL5.5 开始支持128个 rollback segment,支持 128*1024 个 undo 操作 +* MySQL5.5 开始支持 128 个 rollback segment,支持 128*1024 个 undo 操作 @@ -2194,7 +2194,7 @@ redo log,记录**数据页的物理修改**,而不是某一行或某几行 InnoDB 作为存储引擎,数据是存放在磁盘中,每次读写数据都需要磁盘 IO,效率会很低。InnoDB 提供了缓存 Buffer Pool,Buffer Pool 中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲: * 从数据库读取数据时,会首先从缓存中读取,如果缓存中没有,则从磁盘读取后放入Buffer Pool -* 向数据库写入数据时,会首先写入缓存,缓存中修改的数据会定期刷新到磁盘,这一过程称为刷脏 +* 向数据库写入数据时,会首先写入缓存,缓存中修改的数据会**定期刷新**到磁盘,这一过程称为刷脏 Buffer Pool 的使用提高了读写数据的效率,但是也带了新的问题:如果 MySQL 宕机,此时 Buffer Pool 中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证,所以引入 redo log @@ -2205,10 +2205,10 @@ Buffer Pool 的使用提高了读写数据的效率,但是也带了新的问 redo log 采用的是 WAL(Write-ahead logging,**预写式日志**),所有修改要先写入日志,再更新到 Buffer Pool,保证了数据不会因 MySQL 宕机而丢失,从而满足了持久性要求 -redo log 也需要在事务提交时将日志写入磁盘,但是比将 Buffer Pool 中修改的数据写入磁盘快: +redo log 也需要**在事务提交时**将日志写入磁盘,但是比将 Buffer Pool 中修改的数据写入磁盘快: * 刷脏是随机 IO,因为每次修改的数据位置随机,但写 redo log 是尾部追加操作,属于顺序 IO -* 刷脏是以数据页 (Page) 为单位的,MySQL 默认页大小是 16KB,一个页上的一个小修改都要整页写入,而 redo log 中只包含真正需要写入的部分,无效IO大大减少 +* 刷脏是以数据页 (Page) 为单位的,MySQL 默认页大小是 16KB,一个页上的一个小修改都要整页写入,而 redo log 中只包含真正需要写入的部分,无效 IO 大大减少 刷盘策略,通过修改参数 `innodb_flush_log_at_trx_commit` 设置: @@ -2226,11 +2226,11 @@ redo log 也需要在事务提交时将日志写入磁盘,但是比将 Buffer 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 被记录。 -* 内容不同:redo log 是物理日志,内容基于磁盘的 Page;binlog的内容是二进制的,根据 binlog_format 参数的不同,可能基于SQL 语句、基于数据本身或者二者的混合(日志部分详解) +* 内容不同:redo log 是物理日志,内容基于磁盘的 Page;binlog 的内容是二进制的,根据 binlog_format 参数的不同,可能基于SQL 语句、基于数据本身或者二者的混合(日志部分详解) * 写入时机不同:binlog 在事务提交时一次写入;redo log 的写入时机相对多元 @@ -2246,7 +2246,7 @@ MySQL中还存在 binlog(二进制日志) 也可以记录写操作并用于数 ### 隔离级别 -事务的隔离级别:多个客户端操作时,各个客户端的事务之间应该是隔离的,**不同的事务之间不该互相影响**,而如果多个事务操作同一批数据时,则需要设置不同的隔离级别 , 否则就会产生问题 。 +事务的隔离级别:多个客户端操作时,各个客户端的事务之间应该是隔离的,**不同的事务之间不该互相影响**,而如果多个事务操作同一批数据时,则需要设置不同的隔离级别,否则就会产生问题。 隔离级别分类: @@ -2307,7 +2307,7 @@ MVCC 处理读写请求,可以做到在发生读写请求冲突时不用加锁 * 读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读 -* 写-写:有线程安全问题,可能会存在更新丢失问题 +* 写-写:有线程安全问题,可能会存在丢失更新问题 MVCC 的优点: @@ -2316,8 +2316,8 @@ MVCC 的优点: 提高读写和写写的并发性能: -* MVCC + 悲观锁:MVCC解决读写冲突,悲观锁解决写写冲突 -* MVCC + 乐观锁:MVCC解决读写冲突,乐观锁解决写写冲突 +* MVCC + 悲观锁:MVCC 解决读写冲突,悲观锁解决写写冲突 +* MVCC + 乐观锁:MVCC 解决读写冲突,乐观锁解决写写冲突 @@ -2333,7 +2333,7 @@ MVCC 的优点: ##### 隐藏字段 -实现原理主要是隐藏字段,undo日志,Read View来实现的 +实现原理主要是隐藏字段,undo日志,Read View 来实现的 数据库中的每行数据,除了自定义的字段,还有数据库隐式定义的字段: @@ -2341,7 +2341,7 @@ MVCC 的优点: * 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:删除标志的隐藏字段,记录被更新或删除并不代表真的删除,而是删除位变了 @@ -2366,19 +2366,19 @@ undo log 的作用: * 保证事务进行 rollback 时的原子性和一致性,当事务进行回滚的时候可以用 undo log 的数据进行恢复 * 用于 MVCC 快照读的数据,在 MVCC 多版本控制中,通过读取 undo log 的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据版本 -undo log主要分为两种: +undo log 主要分为两种: * insert undo log:事务在 insert 新记录时产生的 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 线程: @@ -2402,7 +2402,7 @@ 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 +- creator_trx_id:生成该 Read View 的事务的事务id,就是判断该id的事务能读到什么数据 creator 创建一个 Read View,进行可见性算法分析:(**解决了读未提交**) @@ -2411,7 +2411,7 @@ creator 创建一个 Read View,进行可见性算法分析:(**解决了读 * 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 中 - * 在列表中,说明该版本对应的事务正在运行,数据不能显示 + * 在列表中,说明该版本对应的事务正在运行,数据不能显示(不能读到未提交的数据) * 不在列表中,说明该版本对应的事务已经被提交,数据可以显示 @@ -2469,12 +2469,12 @@ ID 为 0 的事务创建 Read View: #### RC RR -Read View 用于支持RC(Read Committed,读已提交)和RR(Repeatable Read,可重复读)隔离级别的实现 +Read View 用于支持 RC(Read Committed,读已提交)和 RR(Repeatable Read,可重复读)隔离级别的实现 -RR、RC生成时机: +RR、RC 生成时机: - RC 隔离级别下,每个快照读都会生成并获取最新的 Read View -- RR 隔离级别下,则是同一个事务中的第一个快照读才会创建 Read View,之后的快照读获取的都是同一个Read View,所以一个事务的查询结果每次都是相同的 +- RR 隔离级别下,则是同一个事务中的第一个快照读才会创建 Read View 解决幻读问题: @@ -2483,11 +2483,11 @@ RR、RC生成时机: RC、RR 级别下的 InnoDB 快照读区别 -- 在RR级别下的某个事务的对某条记录的第一次快照读会创建一个 Read View, 将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,使用的是同一个Read View。 +- RC 级别下的,事务中每次快照读都会新生成一个 Read View,这就是在 RC 级别下的事务中可以看到别的事务提交的更新的原因 - 当前事务在其他事务提交之前使用过快照读,那么以后其他事务对数据的修改都是不可见的,就算以后其他事务提交了数据也不可见;早于 Read View 创建的事务所做的修改并提交的均是可见的 +- RR 级别下的某个事务的对某条记录的第一次快照读会创建一个 Read View, 将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,使用的是同一个Read View,所以一个事务的查询结果每次都是相同的 -- RC级别下的,事务中每次快照读都会新生成一个 Read View,这就是在 RC 级别下的事务中可以看到别的事务提交的更新的原因 + 当前事务在其他事务提交之前使用过快照读,那么以后其他事务对数据的修改都是不可见的,就算以后其他事务提交了数据也不可见;早于 Read View 创建的事务所做的修改并提交的均是可见的 @@ -4726,11 +4726,11 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name,status,address); #### 底层原理 -索引失效一般是针对联合索引,联合索引一般由几个字段组成,排序方式是先按照第一个字段进行排序,然后排序第二个,依此类推,图示(a, b)索引,a 相等的情况下 b 是有序的 +索引失效一般是针对联合索引,联合索引一般由几个字段组成,排序方式是先按照第一个字段进行排序,然后排序第二个,依此类推,图示(a, b)索引,**a 相等的情况下 b 是有序的** -* 最左前缀法则:当不匹配前面的字段的时候,后面的字段都是无序的。这种无序不仅体现在叶子节点,也会导致查询时的非叶子节点也是无序的,因为索引树相当于忽略的第一个字段,就无法使用二分查找 +* 最左前缀法则:当不匹配前面的字段的时候,后面的字段都是无序的。这种无序不仅体现在叶子节点,也会**导致查询时的非叶子节点也是无序的**,因为索引树相当于忽略的第一个字段,就无法使用二分查找 * 范围查询右边的列,不能使用索引,比如语句: `WHERE a > 1 AND b = 1 `,在 a 大于1的时候,b是无序的 @@ -6235,7 +6235,7 @@ binlog_format=STATEMENT 日志格式: * STATEMENT:该日志格式在日志文件中记录的都是 SQL 语句 (statement),每一条对数据进行修改的 SQL都会记录在日志文件中,通过 mysqlbinlog 工具,可以查看到每条语句的文本。主从复制时,从库 (slave) 会将日志解析为原语句,并在从库重新执行一 -* ROW:该日志格式在日志文件中记录的是每一行的数据变更,而不是记录SQL语句。比如执行SQL语句`update tb_book set status='1'`,如果是 STATEMENT,在日志中会记录一行 SQL 语句; 如果是ROW,由于是对全表进行更新,就是每一行记录都会发生变更,ROW 格式的日志中会记录每一行的数据变更 +* ROW:该日志格式在日志文件中记录的是每一行的数据变更,而不是记录 SQL 语句。比如执行 SQL 语句`update tb_book set status='1'`,如果是 STATEMENT,在日志中会记录一行 SQL 语句; 如果是 ROW,由于是对全表进行更新,就是每一行记录都会发生变更,ROW 格式的日志中会记录每一行的数据变更 * MIXED:这是 MySQL 默认的日志格式,混合了STATEMENT 和 ROW两种格式。MIXED 格式能尽量利用两种模式的优点,而避开他们的缺点 diff --git a/Java.md b/Java.md index 58383ec..b170ea5 100644 --- a/Java.md +++ b/Java.md @@ -191,8 +191,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 { @@ -373,7 +373,7 @@ public static void main(String[] args) { #### 初始化 -数组就是存储数据长度固定的容器,存储多个数据的**数据类型要一致** +数组就是存储数据长度固定的容器,存储多个数据的**数据类型要一致**,数组也是一个对象 创建数组: @@ -468,9 +468,11 @@ public static void main(String[] args) { 初始化: * 动态初始化: + 数据类型[][] 变量名 = new 数据类型[m] [n] : `int[][] arr = new int[3][3];` - m表示这个二维数组,可以存放多少个一维数组 - n表示每一个一维数组,可以存放多少个元素 + + * m 表示这个二维数组,可以存放多少个一维数组,行 + * n 表示每一个一维数组,可以存放多少个元素,列 * 静态初始化 * 数据类型[][] 变量名 = new 数据类型[][]{ {元素1, 元素2...} , {元素1, 元素2...} * 数据类型[][] 变量名 = { {元素1, 元素2...} , {元素1, 元素2...} ...} @@ -1464,18 +1466,20 @@ public class QuickSort { public static void quickSort(int[] arr, int low, int high) { int left = low; int right = high; - + + //递归结束的条件,等于防止一次多余的赋值 if (low >= high) { return; } - int temp = arr[low];//基准数 + int temp = arr[left];//基准数 while (left < right) { // 用 <= 可以防止多余的交换 while (arr[right] >= temp && right > left) { right--; } - // arr[right] < temp 放在左边,做判断防止相等 + //做判断防止相等 if (right > left) { + // 到这里说明 arr[right] < temp arr[left] = arr[right];//此时把arr[right]元素视为空 left++; } @@ -1533,52 +1537,63 @@ public class QuickSort { +实现思路: + +- 获得最大数的位数,可以通过将最大数变为String类型,再求长度 +- 将所有待比较数值(正整数)统一为同样的数位长度,**位数较短的数前面补零** +- 从最低位开始,依次进行一次排序 +- 从最低位排序一直到最高位(个位->十位->百位->…->最高位)排序完成以后,,数列就变成一个有序序列 + ```java -public class Test { +public class BucketSort { public static void main(String[] args) { - int[] arr = new int[]{55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88}; + int[] arr = new int[]{576, 22, 26, 548, 1, 3, 843, 536, 735, 43, 3, 912, 88}; bucketSort(arr); System.out.println(Arrays.toString(arr)); } - public static void bucketSort(int[] arr){ - // 计算最大值与最小值 - int max = Integer.MIN_VALUE; - int min = Integer.MAX_VALUE; - for(int i = 0; i < arr.length; i++){ - max = Math.max(max, arr[i]); - min = Math.min(min, arr[i]); - } - - // 计算桶的数量 - int bucketNum = (max - min) / arr.length + 1; - ArrayList> bucketArr = new ArrayList<>(bucketNum); - for(int i = 0; i < bucketNum; i++){ - bucketArr.add(new ArrayList()); - } - - // 将每个元素放入桶 - for(int i = 0; i < arr.length; i++){ - int num = (arr[i] - min) / (arr.length); - bucketArr.get(num).add(arr[i]); - } + private static void bucketSort(int[] arr) { + // 桶的个数固定为10个(个位是0~9),最大容量由数组的长度决定 + int[][] bucket = new int[10][arr.length]; + //记录每个桶中的有多少个元素 + int[] elementCounts = new int[10]; - // 对每个桶进行排序 - for(int i = 0; i < bucketArr.size(); i++){ - Collections.sort(bucketArr.get(i)); + //获取数组的最大元素 + int max = arr[0]; + for (int i = 1; i < arr.length; i++) { + if (arr[i] > max) { + max = arr[i]; + } } - - // 将桶中的元素赋值到原序列 - int index = 0; - for(int i = 0; i < bucketArr.size(); i++){ - for(int j = 0; j < bucketArr.get(i).size(); j++){ - arr[index++] = bucketArr.get(i).get(j); + String maxEle = Integer.toString(max); + //将数组中的元素放入桶中 + for (int i = 0, step = 1; i < maxEle.length(); i++, step *= 10) { + for (int j = 0; j < arr.length; j++) { + //获取最后一位的数据,也就是索引 + int index = (arr[j] / step) % 10; + //放入具体位置 + bucket[index][elementCounts[index]] = arr[j]; + elementCounts[index]++; + } + //收集回数组 + for (int j = 0, index = 0; j < 10; j++) { + //先进先出 + int position = 0; + //桶中有元素就取出 + while (elementCounts[j] > 0) { + arr[index] = bucket[j][position]; + elementCounts[j]--; + position++; + index++; + } } } } } ``` +空间换时间 + 推荐视频:https://www.bilibili.com/video/BV1b7411N798?p=86 @@ -1678,6 +1693,165 @@ public class binarySearch { +*** + + + +### 匹配 + +#### BF + +暴力匹配算法: + +```java +public static void main(String[] args) { + String s = "seazean"; + String t = "az"; + System.out.println(match(s,t));//2 +} + +public static int match(String s,String t) { + int k = 0; + int i = k, j = 0; + //防止越界 + while (i < s.length() && j < t.length()) { + if (s.charAt(i) == t.charAt(j)) { + ++i; + ++j; + } else { + k++; + i = k; + j = 0; + } + } + //说明是匹配成功 + if (j >= t.length()) { + return k; + } + return 0; +} +``` + + + +*** + + + +#### RK + + + + + +*** + + + +#### KMP + +KMP匹配: + +* next 数组的核心就是自己匹配自己,主串代表后缀,模式串代表前缀 +* nextVal 数组的核心就是 + +```java +public class Kmp { + public static void main(String[] args) { + String s = "acababaabc"; + String t = "abaabc"; + //[-1, 0, 0, 1, 1, 2] + System.out.println(Arrays.toString(getNext(t))); + //[-1, 0, -1, 1, 0, 2] + System.out.println(Arrays.toString(getNextVal(t))); + //5 + System.out.println(kmp(s, t)); + } + + private static int kmp(String s, String t) { + int[] next = getNext(t); + int i = 0, j = 0; + while (i < s.length() && j < t.length()) { + //j==-1时说明第一个位置匹配失败,所以将s的下一个和t的首字符比较 + if (j == -1 || s.charAt(i) == t.charAt(j)) { + i++; + j++; + } else { + //模式串右移,比较s的当前位置与t的next[j]位置 + j = next[j]; + } + } + if (j >= t.length()) { + return i - j + 1; + } + return -1; + } + //next数组 + private static int[] getNext(String t) { + int[] next = new int[t.length()]; + next[0] = -1; + int j = -1; + int i = 0; + while (i < t.length() - 1) { + // 根据已知的前j位推测第j+1位 + // j=-1说明首位就没有匹配,即t[0]!=t[i],说明next[i+1]没有最大前缀,为0 + if (j == -1 || t.charAt(i) == t.charAt(j)) { + // i位置和j位置的数据相同,当i+1位置不匹配时,可以跳转到j+1的位置对比 + // 所以只需要将i的最大公共前缀+1就代表i+1的最大前缀,依次类推 + // 所以2位置的最大公共前缀只需要1位置的最大前缀+1 + i++; + j++; + next[i] = j; + } else { + //i位置的数据和j位置的不相等,所以回退对比next[j]和i位置的数据 + j = next[j]; + } + + } + return next; + } + //nextVal + private static int[] getNextVal(String t) { + int[] nextVal = new int[t.length()]; + nextVal[0] = -1; + int j = -1; + int i = 0; + while (i < t.length() - 1) { + if (j == -1 || t.charAt(i) == t.charAt(j)) { + i++; + j++; + // 如果t[i+1]==t[j+1],回退后仍然失配,所以要继续回退 + if (t.charAt(i) == t.charAt(j)) { + nextVal[i] = nextVal[j]; + } else { + nextVal[i] = j; + } + } else { + j = nextVal[j]; + } + } + return nextVal; + } + /*根据next求nextVal + private static int[] getNextVal(String t, int[] next) { + int[] nextVal = new int[next.length]; + nextVal[0] = -1; + for (int i = 1; i < nextVal.length; i++) { + if (t.charAt(i) == t.charAt(next[i])) { + nextVal[i] = nextVal[next[i]]; + } else { + nextVal[i] = next[i]; + } + } + return nextVal; + }*/ +} +``` + + + +参考文章:https://www.cnblogs.com/tangzhengyue/p/4315393.html + @@ -3074,7 +3248,7 @@ Cloneable 接口是一个标识性接口,即该接口不包含任何方法( } ``` - +SDP → 创建型 → 原型模式 @@ -3168,7 +3342,7 @@ s = s + "cd"; //s = abccd 新对象 直接赋值:`String s = “abc”` 直接赋值的方式创建字符串对象,内容就是abc - 通过构造方法创建:通过 new 创建的字符串对象,每一次 new 都会申请一个内存空间,虽然内容相同,但是地址值不同,**返回堆内存中对象的引用** -- 直接赋值方式创建:以“ ”方式给出的字符串,只要字符序列相同(顺序和大小写),无论在程序代码中出现几次,JVM 都只会**在 String Pool 中创建一个字符串对象**,并在字符串池中维护 +- 直接赋值方式创建:以“ ”方式给出的字符串,只要字符序列相同(顺序和大小写),无论在程序代码中出现几次,JVM 都只会**在 String Pool 中创建一个字符串对象**,并在字符串池中维护 `String str = new String("abc")`创建字符串对象: @@ -3214,6 +3388,7 @@ s = s + "cd"; //s = abccd 新对象 `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 beginIndex, int endIndex)` : 返回原字符串指定索引处的字符串 @@ -3249,7 +3424,7 @@ s.replace("-","");//12378 * jdk1.8:当一个字符串调用 intern() 方法时,如果 String Pool 中: * 存在一个字符串和该字符串值相等(使用 equals() 方法进行确定),就会返回 String Pool 中字符串的引用(需要变量接收) - * 不存在,会把对象的引用地址复制一份放入串池,并返回串池中的引用地址,前提是堆内存有该对象。因为Pool在堆中,为了节省内存不再创建新对象 + * 不存在,会把对象的引用地址复制一份放入串池,并返回串池中的引用地址,前提是堆内存有该对象。因为 Pool 在堆中,为了节省内存不再创建新对象 * jdk1.6:将这个字符串对象尝试放入串池,如果有就不放入,返回已有的串池中的对象的地址;如果没有会把此对象复制一份,放入串池,把串池中的对象返回 ```java @@ -4841,7 +5016,7 @@ public class ArrayList extends AbstractList 扩容:Vector 的构造函数可以传入 capacityIncrement 参数,作用是在扩容时使容量 capacity 增长 capacityIncrement,如果这个参数的值小于等于 0(默认0),扩容时每次都令 capacity 为原来的两倍 -对比ArrayList +对比 ArrayList 1. Vector 是同步的,开销比 ArrayList 要大,访问速度更慢。最好使用 ArrayList 而不是 Vector,因为同步操作完全可以由程序来控制 @@ -5308,7 +5483,7 @@ HashMap基于哈希表的Map接口实现,是以key-value存储形式存在, * HashMap的实现不是同步的,这意味着它不是线程安全的 * key是唯一不重复的,底层的哈希表结构,依赖hashCode方法和equals方法保证键的唯一 -* key、value都可以为null,但是键位置只能是一个null +* key、value都可以为null,但是key位置只能是一个null * HashMap中的映射不是有序的,即存取是无序的 * **key要存储的是自定义对象,需要重写hashCode和equals方法,防止出现地址不同内容相同的key** @@ -5417,7 +5592,7 @@ HashMap继承关系如下图所示: * int类型是32位整型,占4个字节 * Java的原始类型里没有无符号类型,所以首位是符号位正数为0,负数为1 -5. 当链表的值超过8则会转红黑树(**1.8新增**) +5. 当链表的值超过8则会转红黑树(1.8新增**) ```java //当桶(bucket)上的结点数大于这个值时会转成红黑树 @@ -7346,8 +7521,8 @@ class Student{ ```java // foreach终结方法 - list.stream().filter(s -> s.startsWith("张")) - .filter(s -> s.length() == 3).forEach(System.out::println); +list.stream().filter(s -> s.startsWith("张")) + .filter(s -> s.length() == 3).forEach(System.out::println); ``` @@ -8770,7 +8945,7 @@ IO 复用让单个进程具有处理多个 I/O 事件的能力,又被称为 Ev ###### 函数 -socket 不是文件,只是一个标识符,但是 Unix 操作系统把所有东西都**看作**是文件,所以 socket 说成file descriptor,也就是 fd +socket 不是文件,只是一个标识符,但是 Unix 操作系统把所有东西都**看作**是文件,所以 socket 说成 file descriptor,也就是 fd select 允许应用程序监视一组文件描述符,等待一个或者多个描述符成为就绪状态,从而完成 I/O 操作。 @@ -9033,7 +9208,7 @@ epoll 的特点: * epoll 仅适用于 Linux 系统 * epoll 使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中 * 没有最大描述符数量(并发连接)的限制,打开 fd 的上限远大于1024(1G 内存能监听约10万个端口) -* epoll 的时间复杂度 O(1),epoll 理解为 event poll,不同于忙轮询和无差别轮询,调用 epoll_wait 只是轮询就绪链表。当监听列表有设备就绪时调用回调函数,把就绪 fd 放入就绪链表中,并唤醒在 epoll_wait 中阻塞的进程,所以 epoll 实际上是**事件驱动(每个事件关联上fd)**的 +* epoll 的时间复杂度 O(1),epoll 理解为 event poll,不同于忙轮询和无差别轮询,调用 epoll_wait **只是轮询就绪链表**。当监听列表有设备就绪时调用回调函数,把就绪 fd 放入就绪链表中,并唤醒在 epoll_wait 中阻塞的进程,所以 epoll 实际上是**事件驱动**(每个事件关联上fd)的 * epoll 内核中根据每个 fd 上的 callback 函数来实现,只有活跃的 socket 才会主动调用 callback,所以使用 epoll 没有前面两者的线性下降的性能问题,效率提高 * epoll 每次注册新的事件到 epoll 句柄中时,会把所有的 fd 拷贝进内核,但不是每次 epoll_wait 的时重复拷贝,对比前面两种,epoll 只需要将描述符从进程缓冲区向内核缓冲区拷贝一次。 epoll 也可以利用 mmap() 文件映射内存加速与内核空间的消息传递,减少复制开销 @@ -9192,11 +9367,11 @@ mmap(Memory Mapped Files)加 write 实现零拷贝,零拷贝就是没有 进行了 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) -原理:利用操作系统的 Page 来实现文件到物理内存的直接映射,完成映射后对物理内存的操作会被同步到硬盘上 +原理:利用操作系统的 Page 来实现文件到物理内存的直接映射,完成映射后对物理内存的操作会**被同步**到硬盘上 缺点:不可靠,写到 mmap 中的数据并没有被真正的写到硬盘,操作系统会在程序主动调用 flush 的时候才把数据真正的写到硬盘 @@ -9212,7 +9387,7 @@ Java NIO 提供了 **MappedByteBuffer** 类可以用来实现 mmap 内存映射 sendfile 实现零拷贝,打开文件的文件描述符 fd 和 socket 的 fd 传递给 sendfile,然后经过 3 次复制和 2 次用户态和内核态的切换 -原理:数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer,由于和用户态完全无关,就减少了一次上下文切换 +原理:数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer,由于和用户态完全无关,就减少了两次上下文切换 ![](https://gitee.com/seazean/images/raw/master/Java/IO-sendfile工作流程.png) @@ -9912,7 +10087,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 @@ -10106,9 +10281,9 @@ FileChannel 提供 map 方法把文件映射到虚拟内存,通常情况可以 FileChannel 中的成员属性: * MapMode.mode:内存映像文件访问的方式,共三种: - * MapMode.READ_ONLY:只读,试图修改得到的缓冲区将导致抛出异常。 - * MapMode.READ_WRITE:读/写,对得到的缓冲区的更改最终将写入文件;但该更改对映射到同一文件的其他程序不一定是可见的 - * MapMode.PRIVATE:私用,可读可写,但是修改的内容不会写入文件,只是 buffer 自身的改变,称之为 copy on write 写时复制 + * `MapMode.READ_ONLY`:只读,试图修改得到的缓冲区将导致抛出异常。 + * `MapMode.READ_WRITE`:读/写,对得到的缓冲区的更改最终将写入文件;但该更改对映射到同一文件的其他程序不一定是可见的 + * `MapMode.PRIVATE`:私用,可读可写,但是修改的内容不会写入文件,只是 buffer 自身的改变,称之为 copy on write 写时复制 * position:文件映射时的起始位置 * `public final FileLock lock()`:获取此文件通道的排他锁 @@ -10222,6 +10397,10 @@ Channel 基本操作: +**** + + + ##### 文件读写 ```java @@ -10421,7 +10600,7 @@ public class ChannelTest { 向选择器注册通道:`SelectableChannel.register(Selector sel, int ops)` -将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数 ops 指定。监听的事件类型用四个常量表示: +选择器对通道的监听事件,需要通过第二个参数 ops 指定。监听的事件类型用四个常量表示: * 读 : SelectionKey.OP_READ (1) * 写 : SelectionKey.OP_WRITE (4) @@ -10507,7 +10686,7 @@ ssChannel.register(selector, SelectionKey.OP_ACCEPT); | public abstract SocketChannel accept() | 接受与此通道套接字的连接,通过此方法返回的套接字通道将处于阻塞模式 | * 如果 ServerSocketChannel 处于非阻塞模式,如果没有挂起连接,则此方法将立即返回 null - * 如果通道处于阻塞模式,如果没有挂起连接将无限期地阻塞,直到有新的连接或发生I / O错误 + * 如果通道处于阻塞模式,如果没有挂起连接将无限期地阻塞,直到有新的连接或发生 I/O 错误 @@ -10552,7 +10731,7 @@ public class Server { // 4、获取选择器Selector Selector selector = Selector.open(); // 5、将通道都注册到选择器上去,并且开始指定监听接收事件 - serverSocketChannel. register(selector, SelectionKey.OP_ACCEPT); + serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); // 6、使用Selector选择器轮询已经就绪好的事件 while (selector.select() > 0) { System.out.println("----开始新一轮的时间处理----"); @@ -10578,7 +10757,6 @@ public class Server { ByteBuffer buffer = ByteBuffer.allocate(1024); int len; while ((len = socketChannel.read(buffer)) > 0) { - System.out.println(len); buffer.flip(); System.out.println(socketChannel.getRemoteAddress() + ":" + new String(buffer.array(), 0, len)); buffer.clear();// 清除之前的数据 @@ -11442,7 +11620,7 @@ XML介绍: -### 创建文件 +### 创建 person.xml @@ -11461,7 +11639,7 @@ person.xml -### 组成部分 +### 组成 XML文件中常见的组成元素有:文档声明、元素、属性、注释、转义字符、字符区。文件后缀名为xml @@ -11561,7 +11739,7 @@ XML文件中常见的组成元素有:文档声明、元素、属性、注释、 ### 约束 -#### DTD约束 +#### DTD ##### DTD定义 @@ -11768,9 +11946,9 @@ persondtd.dtd文件 -#### Schema约束 +#### Schema -##### Schema定义 +##### 定义 1.Schema 语言也可作为 XSD(XML Schema Definition) 2.schema约束文件本身也是一个xml文件,符合xml的语法,这个文件的后缀名.xsd @@ -11780,7 +11958,7 @@ persondtd.dtd文件 -##### Schema规则 +##### 规则 1、创建一个文件,这个文件的后缀名为.xsd。 2、定义文档声明 @@ -11827,7 +12005,7 @@ person.xsd -##### Schema引入 +##### 引入 1、在根标签上定义属性xmlns="http://www.w3.org/2001/XMLSchema-instance" 2、**通过xmlns引入约束文件的名称空间** @@ -11854,7 +12032,7 @@ person.xsd -##### Schema属性 +##### Sc属性 ```scheme @@ -12167,25 +12345,25 @@ public class Contact { ### XPath -> Dom4J可以用于解析整个XML的数据。但是如果要检索XML中的某些信息,建议使用XPath. +Dom4J 可以用于解析整个XML的数据。但是如果要检索XML中的某些信息,建议使用XPath + +XPath常用API: + +* List selectNodes(String var1) : 检索出一批节点集合 +* Node selectSingleNode(String var1) : 检索出一个节点返回 -XPath使用步骤: - 1.导入dom4j框架。(XPath依赖于Dom4j技术,必须先倒入dom4j框架!) - 2.导入XPath独有的框架包。jaxen-1.1.2.jar -XPath常用API: - List selectNodes(String var1) : 检索出一批节点集合。 - Node selectSingleNode(String var1) : 检索出一个节点返回。 XPath提供的四种检索数据的写法: - 1.绝对路径: /根元素/子元素/子元素。 - 2.相对路径: ./子元素/子元素。 (.代表了当前元素) - 3.全文搜索: - //元素 在全文找这个元素 - //元素1/元素2 在全文找元素1下面的一级元素2 - //元素1//元素2 在全文找元素1下面的全部元素2 - 4.属性查找。 - //@属性名称 在全文检索属性对象。 - //元素[@属性名称] 在全文检索包含该属性的元素对象。 - //元素[@属性名称=值] 在全文检索包含该属性的元素且属性值为该值的元素对象。 + +1. 绝对路径:/根元素/子元素/子元素。 +2. 相对路径:./子元素/子元素。 (.代表了当前元素) +3. 全文搜索: + * //元素 在全文找这个元素 + * //元素1/元素2 在全文找元素1下面的一级元素2 + * //元素1//元素2 在全文找元素1下面的全部元素2 +4. 属性查找。 + * //@属性名称 在全文检索属性对象。 + * //元素[@属性名称] 在全文检索包含该属性的元素对象。 + * //元素[@属性名称=值] 在全文检索包含该属性的元素且属性值为该值的元素对象。 ```java public class XPathDemo { @@ -12295,8 +12473,8 @@ Java编译器输入的指令流是一种基于栈的指令集架构。因为跨 * 基于栈式架构的特点: * 设计和实现简单,适用于资源受限的系统 * 使用零地址指令方式分配,执行过程依赖操作栈,指令集更小,编译器容易实现 - * 零地址指令:机器指令的一种,是指令系统中的一种不设地址字段的指令,只有操作码而没有地址码。这种指令有两种情况:一是无需操作数,另一种是操作数为默认的(隐含的),默认为操作数在寄存器中,指令可直接访问寄存器 - * 一地址指令:一个地址对应一个操作数 + * 零地址指令:机器指令的一种,是指令系统中的一种不设地址字段的指令,只有操作码而没有地址码。这种指令有两种情况:一是无需操作数,另一种是操作数为默认的(隐含的),默认为操作数在寄存器(ACC)中,指令可直接访问寄存器 + * 一地址指令:一个操作码对应一个地址码,通过地址码寻找操作数 * 不需要硬件的支持,可移植性更好,更好实现跨平台 * 基于寄存器架构的特点: * 需要硬件的支持,可移植性差 @@ -12512,14 +12690,14 @@ Return Address:存放调用该方法的PC寄存器的值 方法的结束有两种方式:正常执行完成、出现未处理的异常,在方法退出后都返回到该方法被调用的位置 -* 正常:调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址 +* 正常:调用者的pc计数器的值作为返回地址,即调用该方法的指令的**下一条指令的地址** * 异常:返回地址是要通过异常表来确定 正常完成出口:执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者 异常完成出口:方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内进行处理,本方法的异常表中没有搜素到匹配的异常处理器,导致方法退出 -* 两者区别:通过异常完成出口退出的不会给上层调用者产生任何的返回值 +两者区别:通过异常完成出口退出的不会给上层调用者产生任何的返回值 @@ -12908,7 +13086,7 @@ public class Demo1_27 { * 如果内存规整,使用指针碰撞(BumpThePointer) 所有用过的内存在一边,空闲的内存在另外一边,中间有一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离 -* 如果内存不规整,虚拟机需要维护一个列表,使用空闲列表(Free List)分配 +* 如果内存不规整,虚拟机需要维护一个空闲列表(Free List)分配 已使用的内存和未使用的内存相互交错,虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容 @@ -15936,7 +16114,7 @@ Integer x = Integer.valueOf(1); int y = x.intValue(); ``` -JDK5以后编译阶段自动转换成上述片段 +JDK5 以后编译阶段自动转换成上述片段 @@ -21175,7 +21353,7 @@ Executors提供了四种线程池的创建:newCachedThreadPool、newFixedThrea #### 开发要求 -阿里巴巴Java开发手册要求 +阿里巴巴 Java 开发手册要求: - **线程资源必须通过线程池提供,不允许在应用中自行显式创建线程** @@ -28231,20 +28409,6 @@ public class Demo { 除了模板方法模式和解释器模式是类行为型模式,其他的全部属于对象行为型模式 -行为型模式分为: - -* 模板方法模式 -* 策略模式 -* 命令模式 -* 职责链模式 -* 状态模式 -* 观察者模式 -* 中介者模式 -* 迭代器模式 -* 访问者模式 -* 备忘录模式 -* 解释器模式 - *** @@ -28446,7 +28610,6 @@ public abstract class InputStream implements Closeable { * 具体策略类: ```java - //为春节准备的促销活动A public class StrategyA implements Strategy { public void show() { @@ -28462,7 +28625,7 @@ public abstract class InputStream implements Closeable { } } ``` - + * 环境类:用于连接上下文,即把促销活动推销给客户,这里可以理解为促销员 ```java diff --git a/SSM.md b/SSM.md index 4969eab..20deeee 100644 --- a/SSM.md +++ b/SSM.md @@ -772,6 +772,10 @@ Mapper 接口开发需要遵循以下规范: +*** + + + ### 插件使用 开发步骤: @@ -808,12 +812,15 @@ Mapper 接口开发需要遵循以下规范: +**** + ### 参数获取 PageInfo构造方法: - `PageInfo info = new PageInfo<>(list)` : list是SQL执行返回的结果集合,参考上一节 + +* `PageInfo info = new PageInfo<>(list)` : list是SQL执行返回的结果集合,参考上一节 PageInfo相关API: @@ -11227,6 +11234,8 @@ yml文件优势: person: {name: zhangsan} ``` + 注意:不建议使用 JSON,应该使用 yaml 语法 + * 数组:一组按次序排列的值 ```yaml From a6c6396be57221c6293dfbee1e713b5f8270b628 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 27 Jun 2021 22:21:57 +0800 Subject: [PATCH 059/242] Update Java Notes --- DB.md | 53 +-- Issue.md | 14 +- Java.md | 976 +++++++++++++++++++++++++++++++++++++++++-------------- Tool.md | 27 +- 4 files changed, 770 insertions(+), 300 deletions(-) diff --git a/DB.md b/DB.md index d12a7f3..d176ae1 100644 --- a/DB.md +++ b/DB.md @@ -3842,16 +3842,16 @@ MyISAM 的索引方式也叫做非聚集的,之所以这么称呼是为了与 #### 原理 -索引是在MySQL的存储引擎中实现的,不同的存储引擎的所支持的索引不一定完全相同 +索引是在 MySQL 的存储引擎中实现的,不同的存储引擎的所支持的索引不一定完全相同 -BTree的索引类型是基于B+Tree树型数据结构的,B+Tree又是BTree数据结构的变种,用在数据库和操作系统中的文件系统,特点是能够保持数据稳定有序 +BTree 的索引类型是基于 B+Tree 树型数据结构的,B+Tree 又是 BTree 数据结构的变种,用在数据库和操作系统中的文件系统,特点是能够保持数据稳定有序 磁盘存储: * 系统从磁盘读取数据到内存时是以磁盘块(block)为基本单位的,位于同一个磁盘块中的数据会被一次性读取出来,而不是需要什么取什么。 -- InnoDB存储引擎中有页(Page)的概念,页是其磁盘管理的最小单位,InnoDB存储引擎中默认每个页的大小为16KB。 -- InnoDB引擎将若干个地址连接磁盘块,以此来达到页的大小16KB,在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘I/O次数,提高查询效率。 +- InnoDB 存储引擎中有页(Page)的概念,页是其磁盘管理的最小单位,InnoDB 存储引擎中默认每个页的大小为 16KB。 +- InnoDB 引擎将若干个地址连接磁盘块,以此来达到页的大小16KB,在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘I/O次数,提高查询效率。 @@ -3861,15 +3861,15 @@ BTree的索引类型是基于B+Tree树型数据结构的,B+Tree又是BTree数 #### BTree -BTree又叫多路平衡搜索树,一颗m叉的BTree特性如下: +BTree 又叫多路平衡搜索树,一颗 m 叉的 BTree 特性如下: - 树中每个节点最多包含m个孩子 -- 除根节点与叶子节点外,每个节点至少有[ceil(m/2)]个孩子 +- 除根节点与叶子节点外,每个节点至少有 [ceil(m/2)] 个孩子 - 若根节点不是叶子节点,则至少有两个孩子 - 所有的叶子节点都在同一层 -- 每个非叶子节点由n个key与n+1个指针组成,其中 [ceil(m/2)-1] <= n <= m-1 +- 每个非叶子节点由 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 数据的工作流程: @@ -3905,12 +3905,12 @@ 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中的每个节点根据实际情况可以包含大量的关键字信息和分支 +BTree 结构的数据可以让系统高效的找到数据所在的磁盘块,定义一条记录为一个二元组 [key, data] ,key 为记录的键值,对应表中的主键值,data 为一行记录中除主键外的数据。对于不同的记录,key 值互不相同,BTree 中的每个节点根据实际情况可以包含大量的关键字信息和分支 ![](https://gitee.com/seazean/images/raw/master/DB/索引的原理-BTree.png) -当进行范围查找时会出现回旋查找 +缺点:当进行范围查找时会出现回旋查找 @@ -3922,37 +3922,44 @@ BTree结构的数据可以让系统高效的找到数据所在的磁盘块,定 ##### 数据结构 -B+Tree为BTree的变种,B+Tree与BTree的区别为: +B+Tree 为 BTree 的变种,B+Tree 与 BTree 的区别为: -* n叉B+Tree最多含有n个key(哈希值),而BTree最多含有n-1个key。 +* n 叉 B+Tree 最多含有 n 个 key(哈希值),而 BTree 最多含有 n-1 个 key -- 所有**非叶子节点只存储键值key**信息,可以看作key的索引部分 -- 所有**数据都存储在叶子节点**,按照key大小顺序排列 +- 所有**非叶子节点只存储键值 key**信息,可以看作 key 的索引部分 +- 所有**数据都存储在叶子节点**,按照 key 大小顺序排列 +- 节点从上到下的所有节点中的 key 在叶子节点中也存在(比如 5),key 允许重复,B 树不同节点不存在重复的 key +B* 树:是 B+ 树的变体,在 B+ 树的非根和非叶子结点再增加指向兄弟的指针 + + + +*** + ##### 优化结构 -BTree数据结构中每个节点中不仅包含数据的key值,还有data值。每一页的存储空间是有限的,如果data数据较大时将会导致每个节点(即一个页)能存储的key的数量很小,当存储的数据量很大时同样会导致B-Tree的深度较大,增大查询时的磁盘I/O次数,进而影响查询效率 +BTree 数据结构中每个节点中不仅包含数据的 key 值,还有 data 值。每一页的存储空间是有限的,如果 data 数据较大时将会导致每个节点(即一个页)能存储的key的数量很小,当存储的数据量很大时同样会导致 B-Tree 的深度较大,增大查询时的磁盘 I/O 次数,进而影响查询效率 -MySQL 索引数据结构对经典的B+Tree进行了优化,在原B+Tree的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的B+Tree,**提高区间访问的性能,防止回旋查找** +MySQL 索引数据结构对经典的 B+Tree 进行了优化,在原 B+Tree 的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的 B+Tree,**提高区间访问的性能,防止回旋查找** 区间访问的意思是访问索引为 5 - 15 的数据,这样就可以直接根据相邻节点的指针遍历 ![](https://gitee.com/seazean/images/raw/master/DB/索引的原理-B+Tree.png) -通常在B+Tree上有两个头指针,一个指向根节点,另一个指向关键字最小的叶子节点,而且所有叶子节点(即数据节点)之间是一种链式环结构。可以对B+Tree进行两种查找运算: +通常在 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次数,树形结构较小 +B+Tree 优点:提高查询速度,减少磁盘的 IO 次数,树形结构较小 @@ -7617,7 +7624,7 @@ Redis (REmote DIctionary Server) :用 C 语言开发的一个开源的高性 -### 下载安装 +### 安装启动 #### CentOS diff --git a/Issue.md b/Issue.md index b51d67c..50806a0 100644 --- a/Issue.md +++ b/Issue.md @@ -1,15 +1,5 @@ # Base -## Structure - - - - - -**** - - - ## Network ### 传输层 @@ -108,6 +98,8 @@ 控制和管理计算机硬件与软件资源的,并合理的组织和调度计算机工作的程序 + 特征:并发、异步、共享、虚拟 + * 什么是系统调度? 在用户程序中调用操作系统提供的核心态级别的子功能,结合用户态和核心态区别回答,一般使用陷入(trap),按调用功能分为:设备管理、文件管理、进程控制、进程通信、内存管理, @@ -122,6 +114,8 @@ 区别:资源、并发、切换、通信、 + 进程特征:并发、异步、动态、独立 + * 进程通信的方式? 同一台计算机的进程通信称为 IPC(Inter-process communication) diff --git a/Java.md b/Java.md index b170ea5..2554b3f 100644 --- a/Java.md +++ b/Java.md @@ -513,7 +513,7 @@ public class Test1 { ### 运算 * i++与++i的区别? - i++表示先将i放在表达式中运算,然后再加1; + i++表示先将i放在表达式中运算,然后再加1 ++i表示先将i加1,然后再放在表达式中运算 * ||和|,&&和&的区别,逻辑运算符 @@ -566,7 +566,7 @@ public class Test1 { -100补码: 11111111 11111111 11111111 10011100 ``` - 补码 --> 原码:符号位不变,其余位置取反加1 + 补码 → 原码:符号位不变,其余位置取反加1 运算符: @@ -1169,16 +1169,23 @@ public class BeerDemo{ public class BubbleSort { public static void main(String[] args) { int[] arr = {55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88}; + int flag;//标记本趟排序是否发生了交换 //比较i和i+1,不需要再比最后一个位置 for (int i = 0; i < arr.length - 1; i++) { + flag = 0; //最后i位不需要比,已经排序好 for (int j = 0; j < arr.length - 1 - i; j++) { if (arr[j] > arr[j + 1]) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; + flag = 1;//发生了交换 } } + //没有发生交换,证明已经有序,不需要继续排序 + if(flag == 0) { + break; + } } System.out.println(Arrays.toString(arr)); } @@ -1253,6 +1260,75 @@ public class SelectSort { ##### 堆排序 +堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法,堆结构是一个近似完全二叉树的结构,并同时满足子结点的键值或索引总是小于(或者大于)父节点 + +优先队列:堆排序每次上浮过程都会将最大或者最小值放在堆顶,应用于优先队列可以将优先级最高的元素浮到堆顶 + +实现思路: + +1. 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,并通过上浮对堆进行调整,此堆为初始的无序区,堆顶为最大数 + +2. 将堆顶元素 R[1] 与最后一个元素 R[n] 交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区 Rn,且满足 R[1,2…n-1]<=R[n] + +3. 交换后新的堆顶 R[1] 可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将 R[1] 与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn),不断重复此过程直到有序区的元素个数为 n-1,则整个排序过程完成 + + + +floor:向下取整 + +```java +public class HeapSort { + public static void main(String[] args) { + int[] arr = {55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88}; + heapSort(arr, arr.length); + System.out.println(Arrays.toString(arr)); + } + + //len为数组长度 + private static void heapSort(int[] arr, int len) { + //建堆,逆排序,因为堆排序定义的交换顺序是从当前结点往下交换,逆序排可以避免多余的交换 + for (int i = len / 2 - 1; i >= 0; i--) { + //调整函数 + sift(arr, i, len - 1); + } + //从尾索引开始排序 + for (int i = len - 1; i > 0; i--) { + //将最大的节点放入末尾 + int temp = arr[0]; + arr[0] = arr[i]; + arr[i] = temp; + //继续寻找最大的节点 + sift(arr, 0, i - 1); + } + } + + //调整函数,调整arr[low]的元素,从索引low到high的范围调整 + private static void sift(int[] arr, int low, int high) { + //暂存调整元素 + int temp = arr[low]; + int i = low, j = low * 2 + 1;//j是左节点 + while (j <= high) { + //判断是否有右孩子,并且比较左右孩子中较大的节点 + if (j < high && arr[j] < arr[j + 1]) { + j++; //指向右孩子 + } + if (temp < arr[j]) { + arr[i] = arr[j]; + i = j; //继续向下调整 + j = 2 * i + 1; + } else { + //temp > arr[j],说明也大于j的孩子,探测结束 + break; + } + } + //将被调整的节点放入最终的位置 + arr[i] = temp; + } +} +``` + +堆排序的时间复杂度是 O(nlogn) + *** @@ -1314,7 +1390,7 @@ public class InsertSort { -希尔排序的核心在于间隔序列的设定,既可以提前设定好间隔序列,也可以动态的定义间隔序列。希尔排序就是插入排序增加了间隔 +希尔排序的核心在于间隔序列的设定,既可以提前设定好间隔序列,也可以动态的定义间隔序列,希尔排序就是插入排序增加了间隔 ```java public class ShellSort { @@ -1347,8 +1423,7 @@ public class ShellSort { } ``` -在希尔排序中,增长量h并没有固定的规则,有很多论文研究了各种不同的递增序列,但都无法证明某个序列是最 -好的,所以对于希尔排序的时间复杂度分析就认为 O(nlogn) +在希尔排序中,增长量h并没有固定的规则,有很多论文研究了各种不同的递增序列,但都无法证明某个序列是最好的,所以对于希尔排序的时间复杂度分析就认为 O(nlogn) @@ -1392,46 +1467,44 @@ public class ShellSort { public class MergeSort { public static void main(String[] args) { int[] arr = new int[]{55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88}; - int[] newArr = mergetSort(arr, 0, arr.length - 1); - System.out.println(Arrays.toString(newArr)); + mergeSort(arr, 0, arr.length - 1); + System.out.println(Arrays.toString(arr)); } - - public static int[] mergetSort(int[] arr, int low, int high) { - if (high < low) { - throw new ArrayIndexOutOfBoundsException("索引输入错了"); - } - //递归结束的条件 - if (low == high) { - return new int[]{arr[low]}; + //low 为arr最小索引,high为最大索引 + public static void mergeSort(int[] arr, int low, int high) { + if (low < high) { + int mid = (low + high) / 2; + mergeSort(arr, low, mid);//归并排序前半段 + mergeSort(arr, mid + 1, high);//归并排序后半段 + merge(arr, low, mid, high);//将两段有序段合成一段有序段 } - int mid = low + (high - low) / 2; - int[] leftArr = mergetSort(arr, low, mid);//左有序数组 - int[] rightArr = mergetSort(arr, mid + 1, high);//右有序数组 - int[] newArr = new int[leftArr.length + rightArr.length];//新有序数组 + } + private static void merge(int[] arr, int low, int mid, int high) { int m = 0; - int l = 0, r = 0;//定义左右指针 - while (l < leftArr.length && r < rightArr.length) { - newArr[m++] = leftArr[l] < rightArr[r] ? leftArr[l++] : rightArr[r++]; + //定义左右指针 + int left = low, right = mid + 1; + int[] assist = new int[high - low + 1]; + while (left <= mid && right <= high) { + assist[m++] = arr[left] < arr[right] ? arr[left++] : arr[right++]; } - - while (l < leftArr.length) { - newArr[m++] = leftArr[l++]; + while (left <= mid) { + assist[m++] = arr[left++]; } - while (r < rightArr.length) { - newArr[m++] = rightArr[r++]; + while (right <= high) { + assist[m++] = arr[right++]; + } + + for (int k = 0; k < assist.length; k++) { + arr[low++] = assist[k]; } - return newArr; } } ``` -用树状图来描述归并,如果一个数组有8个元素,那么每次除以2找最小的子数组,共拆 log8 次,所以树共有3层,那么自顶向下第 k 层有 `2^k` 个子数组,每个数组的长度为 `2^(3-k)`,归并最多需要 `2^(3-k)` 次比较()。因此每层的比较次数为 `2^k * 2^(3-k)=2^3`,那么3层总共为`3*2^3` - -假设元素的个数为 n,那么使用归并排序拆分的次数为 `log2(n)`,那么使用 `log2(n)` 替换`3*2^3`中的这个层数,最终得出的归并排序的时间复杂度为 `log2(n)* 2^(log2(n))=log2(n)*n`,根据大O推导法则,忽略底 -数,最终归并排序的时间复杂度为 O(nlogn) +用树状图来描述归并,假设元素的个数为 n,那么使用归并排序拆分的次数为 `log2(n)`,即层数,每次归并需要做 n 次对比,最终得出的归并排序的时间复杂度为 `log2(n)*n`,根据大O推导法则,忽略底数,最终归并排序的时间复杂度为 O(nlogn) 归并排序的缺点:需要申请额外的数组空间,导致空间复杂度提升,是典型的**以空间换时间**的操作 @@ -1464,20 +1537,21 @@ public class QuickSort { } public static void quickSort(int[] arr, int low, int high) { - int left = low; - int right = high; - - //递归结束的条件,等于防止一次多余的赋值 + //递归结束的条件 if (low >= high) { return; } + + int left = low; + int right = high; + int temp = arr[left];//基准数 while (left < right) { - // 用 <= 可以防止多余的交换 + // 用 >= 可以防止多余的交换 while (arr[right] >= temp && right > left) { right--; } - //做判断防止相等 + // 做判断防止相等 if (right > left) { // 到这里说明 arr[right] < temp arr[left] = arr[right];//此时把arr[right]元素视为空 @@ -1507,7 +1581,7 @@ public class QuickSort { 时间复杂度: -* 最优情况:每一次切分选择的基准数字刚好将当前序列等分。把数组的切分看做是一个树,共切分了logn次,所以,最优情况下快速排序的时间复杂度为 O(nlogn) +* 最优情况:每一次切分选择的基准数字刚好将当前序列等分。把数组的切分看做是一个树,共切分了 logn 次,所以,最优情况下快速排序的时间复杂度为 O(nlogn) * 最坏情况:每一次切分选择的基准数字是当前序列中最大数或者最小数,这使得每次切分都会有一个子组,那么总共就得切分n次,所以最坏情况下,快速排序的时间复杂度为 O(n^2) @@ -1535,6 +1609,8 @@ public class QuickSort { 按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前 +解释:先排低位再排高位,可以说明在高位相等的情况下低位是递增的,如果高位也是递增,则数据有序 + 实现思路: @@ -1542,7 +1618,7 @@ public class QuickSort { - 获得最大数的位数,可以通过将最大数变为String类型,再求长度 - 将所有待比较数值(正整数)统一为同样的数位长度,**位数较短的数前面补零** - 从最低位开始,依次进行一次排序 -- 从最低位排序一直到最高位(个位->十位->百位->…->最高位)排序完成以后,,数列就变成一个有序序列 +- 从最低位排序一直到最高位(个位 → 十位 → 百位 → … →最高位)排序完成以后,数列就变成一个有序序列 ```java public class BucketSort { @@ -1553,7 +1629,7 @@ public class BucketSort { } private static void bucketSort(int[] arr) { - // 桶的个数固定为10个(个位是0~9),最大容量由数组的长度决定 + // 桶的个数固定为10个(个位是0~9),数组长度为了防止所有的数在同一行 int[][] bucket = new int[10][arr.length]; //记录每个桶中的有多少个元素 int[] elementCounts = new int[10]; @@ -1561,18 +1637,17 @@ public class BucketSort { //获取数组的最大元素 int max = arr[0]; for (int i = 1; i < arr.length; i++) { - if (arr[i] > max) { - max = arr[i]; - } + max = max > arr[i] ? max : arr[i]; } String maxEle = Integer.toString(max); - //将数组中的元素放入桶中 + //将数组中的元素放入桶中,最大数的位数相当于需要几次放入桶中 for (int i = 0, step = 1; i < maxEle.length(); i++, step *= 10) { for (int j = 0; j < arr.length; j++) { //获取最后一位的数据,也就是索引 int index = (arr[j] / step) % 10; //放入具体位置 bucket[index][elementCounts[index]] = arr[j]; + //存储每个桶的数量 elementCounts[index]++; } //收集回数组 @@ -1655,6 +1730,8 @@ public class BucketSort { 过程:每次先与中间的元素进行比较,如果大于往右边找,如果小于往左边找,如果等于就返回该元素索引位置!如果没有该元素,返回-1 +时间复杂度:O(logn) + ```java /*定义一个方法,记录开始的索引位置和结束的索引位置。 取出中间索引位置的值,拿元素与中间位置的值进行比较,如果小于中间值,结束位置=中间索引-1. @@ -1701,7 +1778,7 @@ public class binarySearch { #### BF -暴力匹配算法: +Brute Force 暴力匹配算法: ```java public static void main(String[] args) { @@ -1732,6 +1809,8 @@ public static int match(String s,String t) { } ``` +平均时间复杂度:O(m+n),最坏时间复杂度:O(m*n) + *** @@ -1740,7 +1819,11 @@ public static int match(String s,String t) { #### RK +把主串得长度记为 n,模式串得长度记为 m,通过哈希算法对主串中的 n-m+1 个子串分别求哈希值,然后逐个与模式串的哈希值比较大小,如果某个子串的哈希值与模式串相等,再去对比值是否相等(防止哈希冲突),那就说明对应的子串和模式串匹配了 + +因为哈希值是一个数字,数字之间比较是否相等是非常快速的 +第一部分计算哈希值的时间复杂度为 O(n),第二部分对比的时间复杂度为 O(1),整体平均时间复杂度为 O(n),最坏为 O(n*m) @@ -1753,7 +1836,7 @@ public static int match(String s,String t) { KMP匹配: * next 数组的核心就是自己匹配自己,主串代表后缀,模式串代表前缀 -* nextVal 数组的核心就是 +* nextVal 数组的核心就是回退失配 ```java public class Kmp { @@ -1848,12 +1931,532 @@ public class Kmp { } ``` +平均和最坏时间复杂度都是 O(m+n) + 参考文章:https://www.cnblogs.com/tangzhengyue/p/4315393.html +*** + + + +### 树 + +#### 二叉树 + +二叉树中,任意一个节点的度要小于等于 2 + ++ 节点:在树结构中,每一个元素称之为节点 ++ 度:每一个节点的子节点数量称之为度 + +二叉树结构图 + + + +**** + + + +#### 排序树 + +##### 存储结构 + +二叉排序树(BST),又称二叉查找树或者二叉搜索树 + ++ 每一个节点上最多有两个子节点 ++ 左子树上所有节点的值都小于根节点的值 ++ 右子树上所有节点的值都大于根节点的值 ++ 不存在重复的节点 + +二叉查找树 + + + + + +*** + + + +##### 代码实现 + +* 节点类: + + ```java + private static class TreeNode { + int key; + TreeNode left; //左节点 + TreeNode right; //右节点 + + private TreeNode(int key) { + this.key = key; + } + } + ``` + +* 查找节点: + + ```java + // 递归查找 + private static TreeNode search(TreeNode root, int key) { + //递归结束的条件 + if (root == null) { + return null; + } + if (key == root.key) { + return root; + } else if (key > root.key) { + return search(root.right, key); + } else { + return search(root.left, key); + } + } + + // 非递归 + private static TreeNode search1(TreeNode root, int key) { + while (root != null) { + if (key == root.key) { + return root; + } else if (key > root.key) { + root = root.right; + } else { + root = root.left; + } + } + return null; + } + ``` + +* 插入节点: + + ```java + private static int insert(TreeNode root, int key) { + if (root == null) { + root = new TreeNode(key); + root.left = null; + root.right = null; + return 1; + } else { + if (key == root.key) { + return 0; + } else if (key > root.key) { + return insert(root.right, key); + } else { + return insert(root.left, key); + } + } + } + ``` + +* 构造函数: + + ```java + // 构造函数,返回根节点 + private static TreeNode createBST(int[] arr) { + if (arr.length > 0) { + TreeNode root = new TreeNode(arr[0]); + for (int i = 1; i < arr.length; i++) { + insert(root, arr[i]); + } + return root; + } + return null; + } + ``` + +* 删除节点:要删除节点12,先找到节点19,然后移动并替换节点12 + + 代码链接:https://leetcode-cn.com/submissions/detail/190232548/ + + + +参考视频:https://www.bilibili.com/video/BV1iJ411E7xW?t=756&p=86 + +图片来源:https://leetcode-cn.com/problems/delete-node-in-a-bst/solution/tu-jie-yi-dong-jie-dian-er-bu-shi-xiu-ga-edtn/ + + + +*** + + + +#### 平衡树 + +平衡二叉树(AVL)的特点: + ++ 二叉树左右两个子树的高度差不超过1 ++ 任意节点的左右两个子树都是一颗平衡二叉树 + +平衡二叉树旋转: + ++ 旋转触发时机:当添加一个节点之后,该树不再是一颗平衡二叉树 + ++ 平衡二叉树和二叉查找树对比结构图 + +![平衡二叉树和二叉查找树对比](https://gitee.com/seazean/images/raw/master/Java/平衡二叉树和二叉查找树对比结构图.png) + ++ 左旋 + 将根节点的右侧往左拉,原先的右子节点变成新的父节点,并把多余的左子节点出让,给已经降级的根节点当右子节点 + + ![平衡二叉树左旋](https://gitee.com/seazean/images/raw/master/Java/平衡二叉树左旋01.png) + +* 右旋 + 将根节点的左侧往右拉,左子节点变成了新的父节点,并把多余的右子节点出让,给已经降级根节点当左子节点 + + ![平衡二叉树右旋](https://gitee.com/seazean/images/raw/master/Java/平衡二叉树右旋01.png) + +* 平衡二叉树旋转的四种情况 + + * 左左:当根节点左子树的左子树有节点插入,导致二叉树不平衡 + * 如何旋转:直接对整体进行右旋即可 + ![平衡二叉树左左](https://gitee.com/seazean/images/raw/master/Java/平衡二叉树左左.png) + * 左右:当根节点左子树的右子树有节点插入,导致二叉树不平衡 + * 如何旋转:先在左子树对应的节点位置进行左旋,在对整体进行右旋 + ![平衡二叉树左右](https://gitee.com/seazean/images/raw/master/Java/平衡二叉树左右.png) + * 右右:当根节点右子树的右子树有节点插入,导致二叉树不平衡 + * 如何旋转:直接对整体进行左旋即可 + ![平衡二叉树右右](https://gitee.com/seazean/images/raw/master/Java/平衡二叉树右右.png) + * 右左:当根节点右子树的左子树有节点插入,导致二叉树不平衡 + * 如何旋转:先在右子树对应的节点位置进行右旋,在对整体进行左旋 + ![平衡二叉树右左](https://gitee.com/seazean/images/raw/master/Java/平衡二叉树右左.png) + + + +*** + + + +#### 红黑树 + +红黑树的特点: + +* 每一个节点可以是红或者黑 + ++ 红黑树不是高度平衡的,它的平衡是通过"自己的红黑规则"进行实现的 + +红黑树的红黑规则有哪些: + +1. 每一个节点或是红色的,或者是黑色的 +2. 根节点必须是黑色 +3. 如果一个节点没有子节点或者父节点,则该节点相应的指针属性值为Nil,这些Nil视为叶节点,每个叶节点(Nil)是黑色的 +4. 如果某一个节点是红色,那么它的子节点必须是黑色(不能出现两个红色节点相连 的情况) +5. 对每一个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点 + +红黑树与AVL树的比较: + +- 红黑树的插入删除比AVL树更便于控制操作,红黑树更适合于插入修改密集型任务 + +* AVL树是更加严格的平衡,可以提供更快的查找速度,一般读取查找密集型任务,适用AVL树 + +- 红黑树整体性能略优于AVL树,AVL树的旋转比红黑树的旋转更加难以平衡和调试 + +![红黑树](https://gitee.com/seazean/images/raw/master/Java/红黑树结构图.png) + + + +红黑树添加节点的默认颜色为红色,效率高 +![](https://gitee.com/seazean/images/raw/master/Java/红黑树添加节点颜色.png) + + + +**红黑树添加节点后如何保持红黑规则:** + ++ 根节点位置 + + 直接变为黑色 ++ 非根节点位置 + + 父节点为黑色 + + 不需要任何操作,默认红色即可 + + 父节点为红色 + + 叔叔节点为红色 + 1. 将"父节点"设为黑色,将"叔叔节点"设为黑色 + 2. 将"祖父节点"设为红色 + 3. 如果"祖父节点"为根节点,则将根节点再次变成黑色 + + 叔叔节点为黑色 + 1. 将"父节点"设为黑色 + 2. 将"祖父节点"设为红色 + 3. 以"祖父节点"为支点进行旋转 + + + + + +*** + + + +#### 并查集 + +##### 基本实现 + +并查集是一种树型的数据结构,有以下特点: + +* 每个元素都唯一的对应一个结点 +* 每一组数据中的多个元素都在同一颗树中 +* 一个组中的数据对应的树和另外一个组中的数据对应的树之间没有任何联系 +* 元素在树中并没有子父级关系的硬性要求 + + + +可以高效地进行如下操作: + +* 查询元素 p 和元素 q 是否属于同一组 +* 合并元素 p 和元素 q 所在的组 + +存储结构: + + + +合并方式: + + + + + +代码实现: + +* 类实现: + + ```java + public class UF { + //记录节点元素和该元素所在分组的标识 + private int[] eleAndGroup; + //记录分组的个数 + private int count; + + //初始化并查集 + public UF(int N) { + //初始化分组数量 + this.count = N; + + //初始化eleAndGroup数量 + this.eleAndGroup = new int[N]; + + //初始化eleAndGroup中的元素及其所在分组的标识符,eleAndGroup索引作为并查集的每个节点的元素 + //每个索引处的值就是该组的索引,就是该元素所在的组的标识符 + for (int i = 0; i < eleAndGroup.length; i++) { + eleAndGroup[i] = i; + } + } + + //查询p所在的分组的标识符 + public int find(int p) { + return eleAndGroup[p]; + } + + //判断并查集中元素p和元素q是否在同一分组中 + public boolean connect(int p, int q) { + return find(p) == find(q); + } + + //把p元素所在分组和q元素所在分组合并 + public void union(int p, int q) { + //判断元素q和p是否已经在同一个分组中,如果已经在同一个分组中,则结束方法就可以了 + if (connect(p, q)) { + return; + } + int pGroup = find(p);//找到p所在分组的标识符 + int qGroup = find(q);//找到q所在分组的标识符 + + //合并组,让p所在组的 所有元素 的组标识符变为q所在分组的标识符 + for (int i = 0; i < eleAndGroup.length; i++) { + if (eleAndGroup[i] == pGroup) { + eleAndGroup[i] = qGroup; + } + } + //分组个数-1 + this.count--; + } + } + ``` + +* 测试代码: + + ```java + public static void main(String[] args) { + //创建并查集对象 + UF uf = new UF(5); + System.out.println(uf); + + //从控制台录入两个合并的元素,调用union方法合并,观察合并后并查集的分组 + Scanner sc = new Scanner(System.in); + + while (true) { + System.out.println("输入第一个要合并的元素"); + int p = sc.nextInt(); + System.out.println("输入第二个要合并的元素"); + int q = sc.nextInt(); + if (uf.connect(p, q)) { + System.out.println(p + "元素已经和" + q + "元素已经在同一个组"); + continue; + } + uf.union(p, q); + System.out.println("当前并查集中还有:" + uf.count() + "个分组"); + System.out.println(uf); + System.out.println("********************"); + } + } + ``` + +最坏情况下 union 算法的时间复杂度也是 O(N^2) + + + +**** + + + +##### 优化实现 + +让每个索引处的节点都指向它的父节点,当 eleGroup[i] = i 时,说明 i 是根节点 + + + +```java +//查询p所在的分组的标识符,递归寻找父标识符,直到找到根节点 +public int findRoot(int p) { + while (p != eleAndGroup[p]) { + p = eleAndGroup[p]; + } + return p; +} + +//判断并查集中元素p和元素q是否在同一分组中 +public boolean connect(int p, int q) { + return findRoot(p) == findRoot(q); +} + +//把p元素所在分组和q元素所在分组合并 +public void union(int p, int q) { + //找到p q对应的根节点 + int pRoot = findRoot(p); + int qRoot = findRoot(q); + if (pRoot == qRoot) { + return; + } + //让p所在树的节点根节点为q的所在的根节点,只需要把根节点改一下,时间复杂度 O(1) + eleAndGroup[pRoot] = qRoot; +} +``` + +平均时间复杂度为 O(N),最坏时间复杂度是 O(N^2) + + + +继续优化:路径压缩,保证每次把小树合并到大树 + +```java +public class UF_Tree_Weighted { + private int[] eleAndGroup; + private int count; + private int[] size;//存储每一个根结点对应的树中的保存的节点的个数 + + //初始化并查集 + public UF_Tree_Weighted(int N) { + this.count = N; + this.eleAndGroup = new int[N]; + for (int i = 0; i < eleAndGroup.length; i++) { + eleAndGroup[i] = i; + } + this.size = new int[N]; + //默认情况下,size中每个索引处的值都是1 + for (int i = 0; i < size.length; i++) { + size[i] = 1; + } + } + //查询p所在的分组的标识符,父标识符 + public int findRoot(int p) { + while (p != eleAndGroup[p]) { + p = eleAndGroup[p]; + } + return p; + } + + //判断并查集中元素p和元素q是否在同一分组中 + public boolean connect(int p, int q) { + return findRoot(p) == findRoot(q); + } + + //把p元素所在分组和q元素所在分组合并 + public void union(int p, int q) { + //找到p q对应的根节点 + int pRoot = findRoot(p); + int qRoot = findRoot(q); + if (pRoot == qRoot) { + return; + } + //判断pRoot对应的树大还是qRoot对应的树大,最终需要把较小的树合并到较大的树中 + if (size[pRoot] < size[qRoot]) { + eleAndGroup[pRoot] = qRoot; + size[qRoot] += size[pRoot]; + } else { + eleAndGroup[qRoot] = pRoot; + size[pRoot] += size[qRoot]; + } + //组的数量-1、 + this.count--; + } +} +``` + + + +*** + + + +##### 应用场景 + +并查集存储的每一个整数表示的是一个大型计算机网络中的计算机 + +* 可以通过 connected(int p, int q) 来检测该网络中的某两台计算机之间是否连通, +* 可以调用 union(int p,int q) 使得 p 和 q 之间连通,这样两台计算机之间就可以通信 + +畅通工程:某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府畅通工程的目标是使全省任何两个城镇间都可以实现交通,但不一定有直接的道路相连,只要互相间接通过道路可达即可,问最少还需要建设多少条道路? + + + +解题思路: + +1. 创建一个并查集 UF_Tree_Weighted(20) +2. 分别调用 union(0,1)、union(6,9)、union(3,8)、union(5,11)、union(2,12)、union(6,10)、union(4,8),表示已经修建好的道路把对应的城市连接起来 +3. 如果城市全部连接起来,那么并查集中剩余的分组数目为 1,所有的城市都在一个树中,只需要获取当前并查集中剩余的数目减去 1,就是还需要修建的道路数目 + +```java +public static void main(String[] args)throws Exception { + Scanner sc = new Scanner(System.in); + //读取城市数目,初始化并查集 + int number = sc.nextInt(); + //读取已经修建好的道路数目 + int roadNumber = sc.nextInt(); + UF_Tree_Weighted uf = new UF_Tree_Weighted(number); + //循环读取已经修建好的道路,并调用union方法 + for (int i = 0; i < roadNumber; i++) { + int p = sc.nextInt(); + int q = sc.nextInt(); + uf.union(p,q); + } + //获取剩余的分组数量 + int groupNumber = uf.count(); + //计算出还需要修建的道路 + System.out.println("还需要修建"+(groupNumber-1)+"道路,城市才能相通"); +} +``` + + + +参考视频:https://www.bilibili.com/video/BV1iJ411E7xW?p=142 + + + +**** + + + +### 图 + *** @@ -4502,8 +5105,6 @@ public class RegexDemo { ### 存储结构 -#### 数据结构 - 数据结构指的是数据以什么方式组织在一起,不同的数据结构,增删查的性能是不一样的 数据存储的常用结构有:栈、队列、数组、链表和红黑树 @@ -4519,13 +5120,13 @@ public class RegexDemo { 特点:**查询元素快**(根据索引快速计算出元素的地址,然后立即去定位) **增删元素慢**(创建新数组,迁移元素) -* 链表:元素不是内存中的连续区域存储,元素是游离存储的。每个元素会记录下个元素的地址 +* 链表:元素不是内存中的连续区域存储,元素是游离存储的,每个元素会记录下个元素的地址 特点:**查询元素慢,增删元素快**(针对于首尾元素,速度极快,一般是双链表) * 树 - * 二叉树:binary tree 永远只有一个根节点,是每个结点不超过2个节点的树(tree) - 特点:查找二叉树,排序二叉树:小的左边,大的右边,但是可能树很高,性能变差 + * 二叉树:binary tree 永远只有一个根节点,是每个结点不超过2个节点的树(tree) + 特点:二叉排序树:小的左边,大的右边,但是可能树很高,性能变差 为了做排序和搜索会进行左旋和右旋实现平衡查找二叉树,让树的高度差不大于1 * 红黑树(基于红黑规则实现自平衡的排序二叉树):树保证到了很矮小,但是又排好序,性能最高的树 @@ -4538,144 +5139,6 @@ public class RegexDemo { -#### 二叉树 - -二叉树中,任意一个节点的度要小于等于2 - -+ 节点:在树结构中,每一个元素称之为节点 -+ 度:每一个节点的子节点数量称之为度 - -![二叉树结构图](https://gitee.com/seazean/images/raw/master/Java/二叉树结构图.png) - - - -**** - - - -#### 二叉查找树 - -+ 二叉查找树,又称二叉排序树或者二叉搜索树 -+ 每一个节点上最多有两个子节点 -+ 左子树上所有节点的值都小于根节点的值 -+ 右子树上所有节点的值都大于根节点的值 - -![二叉查找树](https://gitee.com/seazean/images/raw/master/Java/二叉查找树结构图.png) - -二叉查找树添加节点规则 - -+ 小的存左边 -+ 大的存右边 -+ 一样的不存 - -![二叉查找树添加节点规则](https://gitee.com/seazean/images/raw/master/Java/二叉查找树添加节点规则.png) - - - -*** - - - -#### 平衡二叉树 - -平衡二叉树的特点 - -+ 二叉树左右两个子树的高度差不超过1 -+ 任意节点的左右两个子树都是一颗平衡二叉树 - -平衡二叉树旋转 - -+ 旋转触发时机:当添加一个节点之后,该树不再是一颗平衡二叉树 - -+ 平衡二叉树和二叉查找树对比结构图 - -![平衡二叉树和二叉查找树对比](https://gitee.com/seazean/images/raw/master/Java/平衡二叉树和二叉查找树对比结构图.png) - -+ 左旋 - 将根节点的右侧往左拉,原先的右子节点变成新的父节点,并把多余的左子节点出让,给已经降级的根节点当右子节点 - - ![平衡二叉树左旋](https://gitee.com/seazean/images/raw/master/Java/平衡二叉树左旋01.png) - -* 右旋 - 将根节点的左侧往右拉,左子节点变成了新的父节点,并把多余的右子节点出让,给已经降级根节点当左子节点 - - ![平衡二叉树右旋](https://gitee.com/seazean/images/raw/master/Java/平衡二叉树右旋01.png) - -* 平衡二叉树旋转的四种情况 - - * 左左:当根节点左子树的左子树有节点插入,导致二叉树不平衡 - * 如何旋转:直接对整体进行右旋即可 - ![平衡二叉树左左](https://gitee.com/seazean/images/raw/master/Java/平衡二叉树左左.png) - * 左右:当根节点左子树的右子树有节点插入,导致二叉树不平衡 - * 如何旋转:先在左子树对应的节点位置进行左旋,在对整体进行右旋 - ![平衡二叉树左右](https://gitee.com/seazean/images/raw/master/Java/平衡二叉树左右.png) - * 右右:当根节点右子树的右子树有节点插入,导致二叉树不平衡 - * 如何旋转:直接对整体进行左旋即可 - ![平衡二叉树右右](https://gitee.com/seazean/images/raw/master/Java/平衡二叉树右右.png) - * 右左:当根节点右子树的左子树有节点插入,导致二叉树不平衡 - * 如何旋转:先在右子树对应的节点位置进行右旋,在对整体进行左旋 - ![平衡二叉树右左](https://gitee.com/seazean/images/raw/master/Java/平衡二叉树右左.png) - - - -*** - - - -#### 红黑树 - -+ 红黑树的特点 - - + 平衡二叉B树 - + 每一个节点可以是红或者黑 - + 红黑树不是高度平衡的,它的平衡是通过"自己的红黑规则"进行实现的 - -+ 红黑树的红黑规则有哪些 - - 1. 每一个节点或是红色的,或者是黑色的 - - 2. 根节点必须是黑色 - - 3. 如果一个节点没有子节点或者父节点,则该节点相应的指针属性值为Nil,这些Nil视为叶节点,每个叶节点(Nil)是黑色的 - - 4. 如果某一个节点是红色,那么它的子节点必须是黑色(不能出现两个红色节点相连 的情况) - - 5. 对每一个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点 - -![红黑树](https://gitee.com/seazean/images/raw/master/Java/红黑树结构图.png) - - - -* 红黑树添加节点的默认颜色为红色,效率高 - ![](https://gitee.com/seazean/images/raw/master/Java/红黑树添加节点颜色.png) - - - -**红黑树添加节点后如何保持红黑规则:** - -+ 根节点位置 - + 直接变为黑色 -+ 非根节点位置 - + 父节点为黑色 - + 不需要任何操作,默认红色即可 - + 父节点为红色 - + 叔叔节点为红色 - 1. 将"父节点"设为黑色,将"叔叔节点"设为黑色 - 2. 将"祖父节点"设为红色 - 3. 如果"祖父节点"为根节点,则将根节点再次变成黑色 - + 叔叔节点为黑色 - 1. 将"父节点"设为黑色 - 2. 将"祖父节点"设为红色 - 3. 以"祖父节点"为支点进行旋转 - - - - - -**** - - - ### Collection #### 概述 @@ -5443,10 +5906,11 @@ public static void main(String[] args){ Map maps = new HashMap<>(); //(1)键找值 Set keys = maps.keySet(); - //Iterator iterator = hm.keySet().iterator(); for(String key : keys) { System.out.println(key + "=" + maps.get(key)); } + //Iterator iterator = hm.keySet().iterator(); + //(2)键值对 //(2.1)普通方式 Set> entries = maps.entrySet(); @@ -5795,7 +6259,7 @@ HashMap继承关系如下图所示: HashMap是支持Key为空的;HashTable是直接用Key来获取HashCode,key为空会抛异常 * &(按位与运算):相同的二进制数位上,都是1的时候,结果为1,否则为零 - * ^(按位异或运算):相同的二进制数位上,数字相同,结果为0,不同为1,不进位加法 + * ^(按位异或运算):相同的二进制数位上,数字相同,结果为0,不同为1,**不进位加法** ```java static final int hash(Object key) { @@ -5806,7 +6270,7 @@ HashMap继承关系如下图所示: } ``` - 计算hash的方法:将hashCode无符号右移16位,高16 bit和低16 bit做了一个异或 + 计算 hash 的方法:将hashCode无符号右移16位,高16bit 和低16bit 做了一个异或 原因:当数组长度很小,假设是16,那么 n-1即为 1111 ,这样的值和hashCode()直接做按位与操作,实际上只使用了哈希值的后4位。如果当哈希值的高位变化很大,低位变化很小,就很容易造成哈希冲突了,所以这里**把高低位都利用起来,让高16位也参与运算**,从而解决了这个问题 @@ -5814,9 +6278,9 @@ HashMap继承关系如下图所示: 2. put - jdk1.8前是头插法 (拉链法),多线程下扩容出现循环链表,jdk1.8以后引入红黑树,插入方法变成尾插法 + jdk1.8 前是头插法 (拉链法),多线程下扩容出现循环链表,jdk1.8 以后引入红黑树,插入方法变成尾插法 - 第一次调用put方法时创建数组Node[] table,因为散列表耗费内存,为了防止内存浪费,所以**延迟初始化** + 第一次调用 put 方法时创建数组 Node[] table,因为散列表耗费内存,为了防止内存浪费,所以**延迟初始化** 存储数据步骤(存储过程): @@ -6069,9 +6533,10 @@ HashMap继承关系如下图所示: * 这里和插入时一样,如果对比节点的哈希值相等并且通过equals判断值也相等,就会判断key相等,直接返回,不相等就从子树中递归查找 5. 时间复杂度 O(1) - 若为树,则在树中通过key.equals(k)查找,**O(logn)** - - 若为链表,则在链表中通过key.equals(k)查找,**O(n)** + + * 若为树,则在树中通过key.equals(k)查找,**O(logn)** + + * 若为链表,则在链表中通过key.equals(k)查找,**O(n)** @@ -7688,7 +8153,6 @@ System.out.println(f.isDirectory()); // false ##### 创建删除 `public boolean createNewFile()` : 当且仅当具有该名称的文件尚不存在时, 创建一个新的空文件。 - (几乎不用的,因为以后文件都是自动创建的!) `public boolean delete()` : 删除由此File表示的文件或目录。 (只能删除空目录) `public boolean mkdir()` : 创建由此File表示的目录。(只能创建一级目录) `public boolean mkdirs()` : 可以创建多级目录(建议使用的) @@ -12494,7 +12958,7 @@ JVM的生命周期分为三个阶段,分别为:启动、运行、死亡。 - **启动**:当启动一个 Java 程序时,通过引导类加载器(bootstrap class loader)创建一个初始类(initial class),对于拥有 main 函数的类就是 JVM 实例运行的起点 - **运行**: - main() 方法是一个程序的初始起点,任何线程均可由在此处启动 - - 在 JVM 内部有两种线程类型,分别为:**用户线程和守护线程**,JVM 通常使用的是守护线程,而 main() 和其他线程使用的是用户线程,守护线程会随着用户线程的结束而结束 + - 在 JVM 内部有两种线程类型,分别为:用户线程和守护线程,**JVM 使用的是守护线程,main() 和其他线程使用的是用户线程**,守护线程会随着用户线程的结束而结束 - 执行一个 Java 程序时,真真正正在执行的是一个 Java 虚拟机的进程 - JVM 有两种运行模式 Server 与 Client,两种模式的区别在于:Client 模式启动速度较快,Server 模式启动较慢;但是启动进入稳定期长期运行之后 Server 模式的程序运行速度比 Client 要快很多 - **死亡**: @@ -13325,19 +13789,19 @@ public void localvarGC4() { #### 安全区域 -安全点 (Safepoint):程序执行时并非在所有地方都能停顿下来开始GC,只有在安全点才能停下 +安全点 (Safepoint):程序执行时并非在所有地方都能停顿下来开始 GC,只有在安全点才能停下 -- Safe Point 的选择很重要,如果太少可能导致GC等待的时间太长,如果太多可能导致运行时的性能问题 +- Safe Point 的选择很重要,如果太少可能导致 GC 等待的时间太长,如果太多可能导致运行时的性能问题 - 大部分指令的执行时间都非常短,通常会根据是否具有让程序长时间执行的特征为标准,选择些执行时间较长的指令作为 Safe Point, 如方法调用、循环跳转和异常跳转等 -在GC发生时,让所有线程都在最近的安全点停顿下来的方法: +在 GC 发生时,让所有线程都在最近的安全点停顿下来的方法: - 抢先式中断:没有虚拟机采用,首先中断所有线程,如果有线程不在安全点,就恢复线程让线程运行到安全点 - 主动式中断:设置一个中断标志,各个线程运行到各个 Safe Point 时就轮询这个标志,如果中断标志为真,则将自己进行中断挂起 -问题:Safepoint 保证程序执行时,在不太长的时间内就会遇到可进入GC的 Safepoint,但是当线程处于 Waiting 状态或 Blocked 状态,线程无法响应JVM的中断请求,运行到安全点去中断挂起,JVM也不可能等待线程被唤醒,对于这种情况,需要安全区域来解决 +问题:Safepoint 保证程序执行时,在不太长的时间内就会遇到可进入GC 的 Safepoint,但是当线程处于 Waiting 状态或 Blocked 状态,线程无法响应 JVM 的中断请求,运行到安全点去中断挂起,JVM 也不可能等待线程被唤醒,对于这种情况,需要安全区域来解决 -安全区域 (Safe Region):指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的 +安全区域 (Safe Region):指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始 GC 都是安全的 运行流程: @@ -13439,9 +13903,9 @@ GC Roots说明: ##### 工作原理 -可达性分析算法以**根对象集合 (GCRoots)**为起始点,从上至下的方式搜索被根对象集合所连接的目标对象 +可达性分析算法以**根对象集合(GCRoots)**为起始点,从上至下的方式搜索被根对象集合所连接的目标对象 -分析工作必须在一个能保障**一致性的快照**中进行,否则结果的准确性无法保证,这点也是导致GC进行时必须 Stop The World 的一个重要原因 +分析工作必须在一个能保障**一致性的快照**中进行,否则结果的准确性无法保证,这点也是导致 GC 进行时必须 Stop The World 的一个重要原因 基本原理: @@ -13469,10 +13933,10 @@ GC Roots说明: - 灰色:本对象已访问过,但是本对象引用到的其他对象尚未全部访问 - 黑色:本对象已访问过,而且本对象引用到的其他对象也全部访问完成 -当Stop The World (STW) 时,对象间的引用是不会发生变化的,可以轻松完成标记,遍历访问过程为: +当 Stop The World (STW) 时,对象间的引用是不会发生变化的,可以轻松完成标记,遍历访问过程为: 1. 初始时,所有对象都在白色集合 -2. 将GC Roots 直接引用到的对象挪到灰色集合 +2. 将 GC Roots 直接引用到的对象挪到灰色集合 3. 从灰色集合中获取对象: * 将本对象引用到的其他对象全部挪到灰色集合中 * 将本对象挪到黑色集合里面 @@ -13487,9 +13951,9 @@ GC Roots说明: ###### 并发标记 -并发标记时,对象间的引用可能发生变化**,**多标和漏标的情况就有可能发生: +并发标记时,对象间的引用可能发生变化**,**多标和漏标的情况就有可能发生 -**多标情况:**当E变为灰色或黑色时,其他线程断开的D对E的引用,导致这部分对象仍会被标记为存活,本轮GC不会回收这部分内存,这部分本应该回收但是没有回收到的内存,被称之为**浮动垃圾** +**多标情况:**当E变为灰色或黑色时,其他线程断开的 D 对 E 的引用,导致这部分对象仍会被标记为存活,本轮 GC 不会回收这部分内存,这部分本应该回收但是没有回收到的内存,被称之为**浮动垃圾** * 针对并发标记开始后的**新对象**,通常的做法是直接全部当成黑色,也算浮动垃圾 * 浮动垃圾并不会影响应用程序的正确性,只是需要等到下一轮垃圾回收中才被清除 @@ -13498,7 +13962,7 @@ GC Roots说明: **漏标情况:** -* 条件一:灰色对象断开了对一个白色对象的引用 (直接或间接),即灰色对象原成员变量的引用发生了变化 +* 条件一:灰色对象断开了对一个白色对象的引用(直接或间接),即灰色对象原成员变量的引用发生了变化 * 条件二:其他线程中修改了黑色对象,插入了一条或多条对该白色对象的新引用 * 结果:导致该白色对象当作垃圾被GC,影响到了应用程序的正确性 @@ -13512,13 +13976,13 @@ objE.fieldG = null; // 写 objD.fieldG = G; // 写 ``` -为了解决问题,可以操作上面三步,**将对象G记录起来,然后作为灰色对象再进行遍历**,比如放到一个特定的集合,等初始的GC Roots遍历完(并发标记),再遍历该集合(重新标记) +为了解决问题,可以操作上面三步,**将对象G记录起来,然后作为灰色对象再进行遍历**,比如放到一个特定的集合,等初始的 GC Roots遍历完(并发标记),再遍历该集合(重新标记) > 所以重新标记需要STW,应用程序一直在运行,该集合可能会一直增加新的对象,导致永远都运行不完 -解决方法:添加读写屏障,读屏障拦截第一步,写屏障拦截第二三步,在读写前后进行一些后置处理 (AOP) +解决方法:添加读写屏障,读屏障拦截第一步,写屏障拦截第二三步,在读写前后进行一些后置处理: -* **写屏障 + 增量更新**:黑色对象新增的引用,将引用记录下,最后对黑色节点重新扫描 +* **写屏障 + 增量更新**:黑色对象新增引用,会将黑色对象变成灰色对象,最后对该节节点重新扫描 增量更新 (Incremental Update) 破坏了条件二,从而保证了不会漏标 @@ -13526,15 +13990,13 @@ objD.fieldG = G; // 写 * **写屏障 (Store Barrier) + SATB**:当原来成员变量的引用发生变化之前,记录下原来的引用对象 - 保留GC开始时的对象图,即原始快照 SATB,当 GC Roots 确定后,对象图就已经确定,那后续的标记也应该是按照这个时刻的对象图走,如果期间对白色对象有了新的引用会记录下来,白色变灰,重新扫描该对象 + 保留 GC 开始时的对象图,即原始快照 SATB,当 GC Roots 确定后,对象图就已经确定,那后续的标记也应该是按照这个时刻的对象图走,如果期间对白色对象有了新的引用会记录下来,并且将白色对象变灰,重新扫描该对象 SATB (Snapshot At The Beginning) 破坏了条件一,从而保证了不会漏标 - 缺点: - * **读屏障 (Load Barrier)**:破坏条件二,黑色对象引用白色对象的前提是获取到该对象,此时读屏障发挥作用 -以Java HotSpot VM为例,其并发标记时对漏标的处理方案如下: +以 Java HotSpot VM 为例,其并发标记时对漏标的处理方案如下: - CMS:写屏障 + 增量更新 - G1:写屏障 + SATB @@ -18605,23 +19067,23 @@ class GuardedObject { ```java public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { - while (true) { - //try { Thread.sleep(1000); } catch (InterruptedException e) { } - // 当没有许可时,当前线程暂停运行;有许可时,用掉这个许可,当前线程恢复运行 - LockSupport.park(); - System.out.println("1"); - } - }); - Thread t2 = new Thread(() -> { - while (true) { - System.out.println("2"); - // 给线程 t1 发放『许可』(多次连续调用 unpark 只会发放一个『许可』) - LockSupport.unpark(t1); - try { Thread.sleep(500); } catch (InterruptedException e) { } - } - }); - t1.start(); - t2.start(); + while (true) { + //try { Thread.sleep(1000); } catch (InterruptedException e) { } + // 当没有许可时,当前线程暂停运行;有许可时,用掉这个许可,当前线程恢复运行 + LockSupport.park(); + System.out.println("1"); + } + }); + Thread t2 = new Thread(() -> { + while (true) { + System.out.println("2"); + // 给线程 t1 发放『许可』(多次连续调用 unpark 只会发放一个『许可』) + LockSupport.unpark(t1); + try { Thread.sleep(500); } catch (InterruptedException e) { } + } + }); + t1.start(); + t2.start(); } ``` @@ -19175,6 +19637,8 @@ Volatile是Java虚拟机提供的**轻量级**的同步机制(三大特性) 性能:volatile修饰的变量进行读操作与普通变量几乎没什么差别,但是写操作相对慢一些,因为需要在本地代码中插入很多内存屏障来保证指令不会发生乱序执行,但是开销比锁要小 + + *** @@ -19236,11 +19700,11 @@ 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) +lock 前缀指令就相当于内存屏障,Memory Barrier(Memory Fence) * 对 volatile 变量的写指令后会加入写屏障 * 对 volatile 变量的读指令前会加入读屏障 @@ -19279,7 +19743,7 @@ lock前缀指令就相当于内存屏障,Memory Barrier(Memory Fence) -* 全能屏障:mfence(modify/mix Barrier),兼具sfence和lfence的功能 +* 全能屏障:mfence(modify/mix Barrier),兼具 sfence 和 lfence 的功能 保证有序性: @@ -19576,13 +20040,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 是线程安全的 底层原理:CAS 的底层是 `lock cmpxchg` 指令(X86 架构),在单核和多核 CPU 下都能够保证比较交换的原子性 -* 程序是在单核处理器上运行,就会省略 lock 前缀(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果) +* 程序是在单核处理器上运行,会省略 lock 前缀,单处理器自身会维护处理器内的顺序一致性,不需要 lock 前缀的内存屏障效果 -* 程序是在多核处理器上运行,会为 cmpxchg 指令加上 lock 前缀 (lock cmpxchg)。当某个核执行到带 lock 的指令时,CPU 会执行**总线锁定或缓存锁定**,当这个核把此指令执行完毕,这个过程不会被线程的调度机制所打断,保证了多个线程对内存操作的原子性 +* 程序是在多核处理器上运行,会为 cmpxchg 指令加上 lock 前缀(lock cmpxchg)。当某个核执行到带 lock 的指令时,CPU 会执行**总线锁定或缓存锁定**,当这个核把此指令执行完毕,这个过程不会被线程的调度机制所打断,保证了多个线程对内存操作的原子性 作用:比较当前工作内存中的值和主物理内存中的值,如果相同则执行规定操作,否者继续比较直到主内存和工作内存的值一致为止 @@ -20949,14 +21413,14 @@ java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:**FIFO | 检查(队首元素) | element() | peek() | 不可用 | 不可用 | * 抛出异常组: - * 当阻塞队列满时:在往队列中add插入元素会抛出 IIIegalStateException: Queue full - * 当阻塞队列空时:再往队列中remove移除元素,会抛出NoSuchException + * 当阻塞队列满时:在往队列中 add 插入元素会抛出 IIIegalStateException: Queue full + * 当阻塞队列空时:再往队列中remove移除元素,会抛出 NoSuchException * 特殊值组: - * 插入方法:成功true,失败false - * 移除方法:成功返回出队列元素,队列没有就返回null + * 插入方法:成功 true,失败 false + * 移除方法:成功返回出队列元素,队列没有就返回 null * 阻塞组: - * 当阻塞队列满时,生产者继续往队列里put元素,队列会一直阻塞生产线程直到put数据或响应中断退出 - * 当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程直到队列可用 + * 当阻塞队列满时,生产者继续往队列里 put 元素,队列会一直阻塞生产线程直到 put 数据或响应中断退出 + * 当阻塞队列空时,消费者线程试图从队列里 take 元素,队列会一直阻塞消费者线程直到队列可用 * 超时退出:当阻塞队列满时,队里会阻塞生产者线程一定时间,超过限时后生产者线程会退出 @@ -21175,9 +21639,11 @@ public class LinkedBlockingQueue extends AbstractQueue #### 延迟队列 +##### 延迟阻塞 + DelayQueue 是一个支持延时获取元素的阻塞队列, 内部采用优先队列 PriorityQueue 存储元素,同时元素必须实现 Delayed 接口;在创建元素时可以指定多久才可以从队列中获取当前元素,只有在延迟期满时才能从队列中提取元素 -DelayQueue只能添加(offer/put/add)实现了Delayed接口的对象,不能添加int、String +DelayQueue 只能添加(offer/put/add)实现了 Delayed 接口的对象,不能添加 int、String API: @@ -21208,6 +21674,12 @@ class DelayTask implements Delayed { +**** + + + +##### 优先队列 + *** @@ -23044,7 +23516,7 @@ signal 流程: 独占锁:指该锁一次只能被一个线程所持有,对ReentrantLock和Synchronized而言都是独占锁 共享锁:指该锁可以被多个线程锁持有 -ReentrantReadWriteLock其读锁是共享,其写锁是独占 +ReentrantReadWriteLock 其读锁是共享,其写锁是独占 作用:多个线程同时读一个资源类没有任何问题,为了满足并发量,读取共享资源应该同时进行,但是如果一个线程想去写共享资源,就不应该再有其它线程可以对该资源进行读或写 @@ -23527,7 +23999,7 @@ class DataContainerStamped { #### 信号量 -synchronized可以起到锁的作用,但某个时间段内,只能有一个线程允许执行 +synchronized 可以起到锁的作用,但某个时间段内,只能有一个线程允许执行 Semaphore(信号量)用来限制能同时访问共享资源的线程上限,非重入锁 diff --git a/Tool.md b/Tool.md index 4912bfc..301d061 100644 --- a/Tool.md +++ b/Tool.md @@ -2,7 +2,7 @@ ## Git概述 -### Git与SVN +### 版本系统 SVN是集中式版本控制系统,版本库是集中放在中央服务器的,而开发人员工作的时候,用的都是自己的电脑,所以首先要从中央服务器下载最新的版本,然后开发,开发完后,需要把自己开发的代码提交到中央服务器。 @@ -15,13 +15,13 @@ Git是分布式版本控制系统(Distributed Version Control System,简称 * 本地仓库:是在开发人员自己电脑上的Git仓库 * 远程仓库:是在远程服务器上的Git仓库 -Clone:克隆,就是将远程仓库复制到本地 -Push:推送,就是将本地仓库代码上传到远程仓库 -Pull:拉取,就是将远程仓库代码下载到本地仓库 +*** + -### Git工作流程 + +### 工作流程 1.从远程仓库中克隆代码到本地仓库 @@ -41,7 +41,7 @@ Pull:拉取,就是将远程仓库代码下载到本地仓库 -### Git代码托管服务 +### 代码托管 Git中存在两种类型的仓库,即本地仓库和远程仓库。那么我们如何搭建Git远程仓库呢?我们可以借助互联网上提供的一些代码托管服务来实现,其中比较常用的有GitHub、码云、GitLab等。 @@ -400,7 +400,7 @@ File→Settings打开设置窗口,找到Version Control下的git选项 -### 创建本地仓库 +### 创建仓库 1、VCS -> Import into Version Control -> Create Git Repository @@ -451,13 +451,13 @@ File→Settings打开设置窗口,找到Version Control下的git选项 -### 推送远程仓库 +### 推送仓库 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 @@ -2197,15 +2197,13 @@ pstree -A #查看所有进程树 * 进程号为 0 的进程通常是调度进程,常常被称为交换进程(swapper),该进程是内核的一部分,它并不执行任何磁盘上的程序,因此也被称为系统进程 -* 进程号为 1 是init进程,是一个守护进程,在自举过程结束时由内核调用,init 进程绝不会终止,是一个普通的用户进程,但是它以超级用户特权运行 - -父进程 ID 为 0 的进程通常是内核进程,它们作为系统自举过程的一部分而启动,但 init 进程是个例外,它的父进程是0,但是它是用户进程 +* 进程号为 1 是 init 进程,是一个守护进程,在自举过程结束时由内核调用,init 进程绝不会终止,是一个普通的用户进程,但是它以超级用户特权运行 -自举程序:存储在内存中 ROM(BIOS 芯片),用来加载操作系统。CPU 的程序计数器指向 ROM 中自举程序第一条指令所对应的位置,当计算机通电,CPU 开始读取并执行自举程序,将操作系统(不是全部,只是启动计算机的那部分程序)装入RAM中,这个过程是自举过程 +父进程 ID 为 0 的进程通常是内核进程,它们作为系统自举过程的一部分而启动,init 进程是个例外,它的父进程是0,但它是用户进程 主存 = RAM + BIOS部分的 ROM -装入完成后,CPU 的程序计数器就被设置为 RAM 中操作系统的第一条指令所对应的位置,接下来CPU将开始执行操作系统的指令 +自举程序存储在内存中 ROM(BIOS 芯片),用来加载操作系统。CPU 的程序计数器指向 ROM 中自举程序第一条指令所对应的位置,当计算机通电,CPU 开始读取并执行自举程序,将操作系统(不是全部,只是启动计算机的那部分程序)装入RAM中,这个过程是自举过程。装入完成后 CPU 的程序计数器就被设置为 RAM 中操作系统的第一条指令所对应的位置,接下来 CPU 将开始执行操作系统的指令 存储在 ROM 中保留很小的自举装入程序,完整功能的自举程序保存在磁盘的启动块上,启动块位于磁盘的固定位,拥有启动分区的磁盘称为启动磁盘或系统磁盘(C盘) @@ -2244,7 +2242,6 @@ pstree -A #查看所有进程树 * 守护进程(daemon)是一类在后台运行的特殊进程,用于执行特定的系统任务。 * 守护进程是脱离于终端并且在后台运行的进程,脱离终端是为了避免在执行的过程中的信息在终端上显示,并且进程也不会被任何终端所产生的终端信息所打断 * 很多守护进程在系统引导的时候启动,并且一直运行直到系统关闭;另一些只在需要的时候才启动,完成任务后就自动结束 -* ID 为 0 的进程通常是调度进程,被称为交换进程(swapper),该进程是内核的一部分,并不执行任何磁盘上的程序,因此也被称为系统进程 From 65039ea8102f4f8552897b17f825e083b7c5ea08 Mon Sep 17 00:00:00 2001 From: Seazean Date: Tue, 29 Jun 2021 22:10:12 +0800 Subject: [PATCH 060/242] Update Java Notes --- DB.md | 44 +++--- Issue.md | 18 +++ Java.md | 415 ++++++++++++++++++++++++++++++++++++++----------------- Tool.md | 4 +- 4 files changed, 332 insertions(+), 149 deletions(-) diff --git a/DB.md b/DB.md index d176ae1..f76780e 100644 --- a/DB.md +++ b/DB.md @@ -409,7 +409,7 @@ mysqlshow -uroot -p1234 test book --count - 用来定义数据库的访问权限和安全级别,及创建用户。关键字:grant, revoke等 - ![](https://gitee.com/seazean/images/raw/master/DB/SQL分类.png) + ![](https://gitee.com/seazean/images/raw/master/Java/SQL分类.png) @@ -5467,9 +5467,9 @@ MySQL 的主从复制原理图: 主从复制主要依赖的是 binlog,MySQL 默认是异步复制,需要三个线程:**master(binlog dump thread)、slave(I/O thread 、SQL thread)** - binlog dump thread:在主库事务提交时,负责把数据变更作为事件 Events 记录在二进制日志文件 binlog 中,并通知 slave 有数据更新 -- I/O thread:负责从主服务器上拉取二进制日志,并将 binlog 日志内容依次写到 relay log 文件的最末端,并将新的 binlog 文件名和 offset 记录到 master-info 文件中,以便下一次读取master 端新 binlog 日志时能告诉 master 服务器从新 binlog 日志的指定文件及位置开始读取新的 binlog 日志内容 +- I/O thread:负责从主服务器上拉取二进制日志,并将 binlog 日志内容依次写到 relay log 文件的最末端,并将新的 binlog 文件名和 offset 记录到 master-info 文件中,以便下一次读取日志时能告诉 master 服务器从指定 binlog 日志文件及位置开始读取新的 binlog 日志内容 - SQL thread:监测本地 relay log中新增了日志内容,读取中继日志并重做其中的 SQL 语句 -- 从库在 relay-log.info 中记录当前应用中继日志的文件名和位置点以便下一次数据复制 +- 从库在 relay-log.info 中记录当前应用中继日志的文件名和位置点以便下一次执行 同步与异步: @@ -5477,6 +5477,8 @@ MySQL 的主从复制原理图: * 同步复制,主库需要将 binlog 复制到所有从库,等所有从库响应了之后才会给客户端响应,这样的话性能很差,一般不会选择同步复制 * MySQL 5.7 之出现了半同步复制,有参数可以选择成功同步几个从库就返回响应 +并行复制:MySQL 5.6 版本增加了并行复制功能,为了改善复制延迟问题,在从库中有两个线程 IO Thread 和 SQL Thread,以采用多线程机制来促进执行,减少从库复制延迟 + **** @@ -8250,10 +8252,10 @@ hash类型:底层使用**哈希表**结构实现数据存储 类似Map结构,左边是key,右边是值,中间叫field字段,本质上**hash存了一个key-value的存储空间** -hash是指的一个数据类型,并不是一个数据 +hash 是指的一个数据类型,并不是一个数据 -* 如果field数量较少,存储结构优化为**压缩列表结构**(有序) -* 如果field数量较多,存储结构使用HashMap结构(无序) +* 如果 field 数量较少,存储结构优化为**压缩列表结构**(有序) +* 如果 field 数量较多,存储结构使用HashMap结构(无序) @@ -8341,7 +8343,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) @@ -8351,7 +8353,7 @@ ziplist 使用更加紧凑的结构实现多个元素的连续存储,所以在 ##### 压缩列表 - 压缩列表(ziplist)是列表和哈希的底层实现之一,压缩列表用来紧凑数据存储,节省内存: +压缩列表(ziplist)是列表和哈希的底层实现之一,压缩列表用来紧凑数据存储,节省内存: @@ -8365,7 +8367,7 @@ ziplist 使用更加紧凑的结构实现多个元素的连续存储,所以在 ##### 哈希表 -Redis 字典使用散列表为底层实现,一个散列表里面有多个散列表节点,每个散列表节点就保存了字典中的一个键值对,发生哈希冲突采用链表法解决 +Redis 字典使用散列表为底层实现,一个散列表里面有多个散列表节点,每个散列表节点就保存了字典中的一个键值对,发生哈希冲突采用链表法解决,存储无序 @@ -8723,14 +8725,14 @@ sorted_set类型:在set的存储结构基础上添加可排序字段,类似 ##### 跳跃表 -Redis 使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的**元素数量比较多**,又或者有序集合中元素的**成员是比较长的字符串**时,Redis就会使用跳跃表来作为有序集合健的底层实现 +Redis 使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的**元素数量比较多**,又或者有序集合中元素的**成员是比较长的字符串**时,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) @@ -11015,9 +11017,9 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 解决方案: -1. 预先设定:以电商为例,每个商家根据店铺等级,指定若干款主打商品,在购物节期间,加大此类信息key的过期时长 注意:购物节不仅仅指当天,以及后续若干天,访问峰值呈现逐渐降低的趋势 +1. 预先设定:以电商为例,每个商家根据店铺等级,指定若干款主打商品,在购物节期间,加大此类信息 key 的过期时长 注意:购物节不仅仅指当天,以及后续若干天,访问峰值呈现逐渐降低的趋势 -2. 现场调整:监控访问量,对自然流量激增的数据延长过期时间或设置为永久性key +2. 现场调整:监控访问量,对自然流量激增的数据延长过期时间或设置为永久性 key 3. 后台刷新数据:启动定时任务,高峰期来临之前,刷新数据有效期,确保不丢失 @@ -11025,7 +11027,7 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 5. 加锁:分布式锁,防止被击穿,但是要注意也是性能瓶颈,慎重! -总的来说:缓存击穿就是单个高热数据过期的瞬间,数据访问量较大,未命中redis后,发起了大量对同一数据的数据库访问,导致对数据库服务器造成压力。应对策略应该在业务数据分析与预防方面进行,配合运行监控测试与即时调整策略,毕竟单个key的过期监控难度较高,配合雪崩处理策略即可 +总的来说:缓存击穿就是单个高热数据过期的瞬间,数据访问量较大,未命中 redis 后,发起了大量对同一数据的数据库访问,导致对数据库服务器造成压力。应对策略应该在业务数据分析与预防方面进行,配合运行监控测试与即时调整策略,毕竟单个 key 的过期监控难度较高,配合雪崩处理策略即可 @@ -11035,24 +11037,24 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 #### 缓存穿透 -场景:系统平稳运行过程中,应用服务器流量随时间增量较大,Redis服务器命中率随时间逐步降低,Redis内存平稳,内存无压力,Redis服务器CPU占用激增,数据库服务器压力激增,数据库崩溃 +场景:系统平稳运行过程中,应用服务器流量随时间增量较大,Redis 服务器命中率随时间逐步降低,Redis 内存平稳,内存无压力,Redis 服务器 CPU 占用激增,数据库服务器压力激增,数据库崩溃 问题排查: -1. Redis中大面积出现未命中 +1. Redis 中大面积出现未命中 -2. 出现非正常URL访问 +2. 出现非正常 URL 访问 问题分析: - 获取的数据在数据库中也不存在,数据库查询未得到对应数据 -- Redis获取到null数据未进行持久化,直接返回 +- Redis 获取到 null 数据未进行持久化,直接返回 - 下次此类数据到达重复上述过程 - 出现黑客攻击服务器 解决方案: -1. 缓存null:对查询结果为null的数据进行缓存 (长期使用,定期清理) ,设定短时限,例如30-60秒,最高5分钟 +1. 缓存 null:对查询结果为null的数据进行缓存 (长期使用,定期清理) ,设定短时限,例如30-60秒,最高5分钟 2. 白名单策略:提前预热各种分类数据 id 对应的 **bitmaps**,id 作为 bitmaps 的 offset,相当于设置了数据白名单。当加载正常数据时放行,加载异常数据时直接拦截(效率偏低),也可以使用布隆过滤器(有关布隆过滤器的命中问题对当前状况可以忽略) @@ -11063,7 +11065,7 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 ​ 根据倍数不同,启动不同的排查流程。然后使用黑名单进行防控(运营) -4. key加密:临时启动防灾业务key,对key进行业务层传输加密服务,设定校验程序,过来的key校验;例如每天随机分配60个加密串,挑选2到3个,混淆到页面数据id中,发现访问key不满足规则,驳回数据访问 +4. key 加密:临时启动防灾业务 key,对 key 进行业务层传输加密服务,设定校验程序,过来的 key 校验;例如每天随机分配60个加密串,挑选2到3个,混淆到页面数据 id 中,发现访问 key 不满足规则,驳回数据访问 总的来说:缓存击穿是指访问了不存在的数据,跳过了合法数据的 redis 数据缓存阶段,每次访问数据库,导致对数据库服务器造成压力。通常此类数据的出现量是一个较低的值,当出现此类情况以毒攻毒,并及时报警。无论是黑名单还是白名单,都是对整体系统的压力,警报解除后尽快移除 diff --git a/Issue.md b/Issue.md index 50806a0..d9e278b 100644 --- a/Issue.md +++ b/Issue.md @@ -1,5 +1,23 @@ # Base +## Algorithm + +排序类问题: + +* 海量数据排序: + * 外部排序:归并 + 败者树 + * 基数排序:https://time.geekbang.org/column/article/42038 + + + + + + + +*** + + + ## Network ### 传输层 diff --git a/Java.md b/Java.md index 2554b3f..0f39789 100644 --- a/Java.md +++ b/Java.md @@ -547,35 +547,36 @@ public class Test1 { * break:跳出一层循环 * 移位运算 + 计算机里一般用**补码表示数字**,正数、负数的表示区别就是最高位是0还是1 - + * 正数的原码反码补码相同 - + ```java 100: 00000000 00000000 00000000 01100100 ``` - + * 负数: 原码:最高位为1,其余位置和正数相同 反码:保证符号位不变,其余位置取反 补码:保证符号位不变,其余位置取反加1,即反码+1 - + ```java -100原码: 10000000 00000000 00000000 01100100 //32位 -100反码: 11111111 11111111 11111111 10011011 -100补码: 11111111 11111111 11111111 10011100 ``` - + 补码 → 原码:符号位不变,其余位置取反加1 - + 运算符: - - * `>>`运算符:将二进制位进行右移操作 - * `<<`运算符:将二进制位进行左移操作 + + * `>>`运算符:将二进制位进行右移操作,相当于除 2 + * `<<`运算符:将二进制位进行左移操作,相当于乘 2 * `>>>`运算符:无符号右移,忽略符号位,空位都以0补齐 - + 运算规则: - + * 正数的左移与右移,空位补0 * 负数原码的左移与右移,空位补0 负数反码的左移与右移,空位补1 @@ -592,13 +593,12 @@ public class Test1 { #### 形参实参 -* 形参 - 可以理解为形式参数,用于定义方法的时候使用的参数,只能是变量 - 形参只有在方法被调用的时候,虚拟机才分配内存单元,方法调用结束之后便会释放所分配的内存单元 +形参: -* 实参 +* 形式参数,用于定义方法的时候使用的参数,只能是变量 +* 形参只有在方法被调用的时候,虚拟机才分配内存单元,方法调用结束之后便会释放所分配的内存单元 - 调用方法时传递的数据可以是常量,也可以是变量 +实参:调用方法时传递的数据可以是常量,也可以是变量 @@ -978,6 +978,10 @@ Debug是供程序员使用的程序调试工具,它可以用于查看程序的 +推荐阅读:https://time.geekbang.org/column/article/41440 + + + *** @@ -1008,6 +1012,10 @@ public static int f(int x){ +*** + + + ##### 公式转换 ```java @@ -1043,9 +1051,7 @@ public static int f(int n){ ##### 猴子吃桃 -猴子第一天摘了若干个桃子,当即吃了一半,觉得好不过瘾,然后又多吃了一个。 -第二天又吃了前一天剩下的一半,觉得好不过瘾,然后又多吃了一个。以后每天都是如此 -等到第十天再吃的时候发现只有1个桃子,请问猴子第一天总共摘了多少个桃子。 +猴子第一天摘了若干个桃子,当即吃了一半,觉得好不过瘾,然后又多吃了一个。第二天又吃了前一天剩下的一半,觉得好不过瘾,然后又多吃了一个。以后每天都是如此。等到第十天再吃的时候发现只有1个桃子,问猴子第一天总共摘了多少个桃子? ```java /* @@ -1065,7 +1071,11 @@ public static int f(int x){ -##### 求和 +*** + + + +##### 递归求和 ```java //(1)递归的终点接:f(1) = 1 @@ -1079,22 +1089,33 @@ public static int f(int n){ -##### 阶乘 +**** + + + +##### 汉诺塔 ```java -//(1)递归的终点接: f(1) = 1 -//(2)递归的公式 f(n) = f(n-1)*n -//(3)递归的方向必须走向终结点 +public class Hanoi { + public static void main(String[] args) { + hanoi('X', 'Y', 'Z', 3); + } -public static int f(int n){ - if(n == 1){ - return 1 ; - }else{ - return f(n-1)*n; - } + //将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 + System.out.println(x + "→" + z); //然后将x最下面的块(最大的)移动到z + hanoi(y, x, z, n - 1); //最后将n-1个块从y移动到z,x为辅助柱 + } + } } ``` +时间复杂度 O(2^n) + **** @@ -1576,6 +1597,7 @@ public class QuickSort { 快速排序和归并排序的区别: * 快速排序是另外一种分治的排序算法,将一个数组分成两个子数组,将两部分独立的排序 +* 归并排序的处理过程是由下到上的,先处理子问题,然后再合并。而快排正好相反,它的处理过程是由上到下的,先分区,然后再处理子问题 * 快速排序和归并排序是互补的:归并排序将数组分成两个子数组分别排序,并将有序的子数组归并从而将整个数组排序,而快速排序的方式则是当两个数组都有序时,整个数组自然就有序了 * 在归并排序中,一个数组被等分为两半,归并调用发生在处理整个数组之前,在快速排序中,切分数组的位置取决于数组的内容,递归调用发生在处理整个数组之后 @@ -1607,6 +1629,8 @@ public class QuickSort { 基数排序(Radix Sort):又叫桶排序和箱排序,借助多关键字排序的思想对单逻辑关键字进行排序的方法 +计数排序其实是桶排序的一种特殊情况,当要排序的 n 个数据,所处的范围并不大的时候,比如最大值是 k,我们就可以把数据划分成 k 个桶,每个桶内的数据值都是相同的,省掉了桶内排序的时间 + 按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前 解释:先排低位再排高位,可以说明在高位相等的情况下低位是递增的,如果高位也是递增,则数据有序 @@ -1768,6 +1792,36 @@ public class binarySearch { ![](https://gitee.com/seazean/images/raw/master/Java/二分查找.gif) +查找第一个匹配的元素: + +```java +public static int binarySearch(int[] arr, int des) { + int start = 0; + int end = arr.length - 1; + + while (start <= end) { + int mid = (start + end) / 2; + if (des == arr[mid]) { + //如果 mid 等于 0,那这个元素已经是数组的第一个元素,那肯定是我要找的 + if (mid == 0 || a[mid - 1] != des) { + return mid; + } else { + //a[mid]前面的一个元素 a[mid-1]也等于 value, + //要找的元素肯定出现在[low, mid-1]之间 + high = mid - 1 + } + } else if (des > arr[mid]) { + start = mid + 1; + } else if (des < arr[mid]) { + end = mid - 1; + } + } + return -1; + } +``` + + + *** @@ -1879,14 +1933,14 @@ public class Kmp { // 根据已知的前j位推测第j+1位 // j=-1说明首位就没有匹配,即t[0]!=t[i],说明next[i+1]没有最大前缀,为0 if (j == -1 || t.charAt(i) == t.charAt(j)) { - // i位置和j位置的数据相同,当i+1位置不匹配时,可以跳转到j+1的位置对比 - // 所以只需要将i的最大公共前缀+1就代表i+1的最大前缀,依次类推 - // 所以2位置的最大公共前缀只需要1位置的最大前缀+1 + // 因为模式串已经匹配到了索引j处,说明之前的位都是相等的 + // 因为是自己匹配自己,所以模式串就是前缀,主串就是后缀,j就是最长公共前缀 + // 当i+1位置不匹配时(i位之前匹配),可以跳转到j+1位置对比,next[i+1]=j+1 i++; j++; next[i] = j; } else { - //i位置的数据和j位置的不相等,所以回退对比next[j]和i位置的数据 + //i位置的数据和j位置的不相等,所以回退对比i和next[j]位置的数据 j = next[j]; } @@ -1903,7 +1957,7 @@ public class Kmp { if (j == -1 || t.charAt(i) == t.charAt(j)) { i++; j++; - // 如果t[i+1]==t[j+1],回退后仍然失配,所以要继续回退 + // 如果t[i+1] == t[next(i+1)]=next[j+1],回退后仍然失配,所以要继续回退 if (t.charAt(i) == t.charAt(j)) { nextVal[i] = nextVal[j]; } else { @@ -2098,30 +2152,16 @@ public class Kmp { ![平衡二叉树和二叉查找树对比](https://gitee.com/seazean/images/raw/master/Java/平衡二叉树和二叉查找树对比结构图.png) -+ 左旋 - 将根节点的右侧往左拉,原先的右子节点变成新的父节点,并把多余的左子节点出让,给已经降级的根节点当右子节点 - ++ 左旋:将根节点的右侧往左拉,原先的右子节点变成新的父节点,并把多余的左子节点出让,给已经降级的根节点当右子节点 + ![平衡二叉树左旋](https://gitee.com/seazean/images/raw/master/Java/平衡二叉树左旋01.png) -* 右旋 - 将根节点的左侧往右拉,左子节点变成了新的父节点,并把多余的右子节点出让,给已经降级根节点当左子节点 - +* 右旋:将根节点的左侧往右拉,左子节点变成了新的父节点,并把多余的右子节点出让,给已经降级根节点当左子节点 + ![平衡二叉树右旋](https://gitee.com/seazean/images/raw/master/Java/平衡二叉树右旋01.png) -* 平衡二叉树旋转的四种情况 +推荐文章:https://pdai.tech/md/algorithm/alg-basic-tree-balance.html - * 左左:当根节点左子树的左子树有节点插入,导致二叉树不平衡 - * 如何旋转:直接对整体进行右旋即可 - ![平衡二叉树左左](https://gitee.com/seazean/images/raw/master/Java/平衡二叉树左左.png) - * 左右:当根节点左子树的右子树有节点插入,导致二叉树不平衡 - * 如何旋转:先在左子树对应的节点位置进行左旋,在对整体进行右旋 - ![平衡二叉树左右](https://gitee.com/seazean/images/raw/master/Java/平衡二叉树左右.png) - * 右右:当根节点右子树的右子树有节点插入,导致二叉树不平衡 - * 如何旋转:直接对整体进行左旋即可 - ![平衡二叉树右右](https://gitee.com/seazean/images/raw/master/Java/平衡二叉树右右.png) - * 右左:当根节点右子树的左子树有节点插入,导致二叉树不平衡 - * 如何旋转:先在右子树对应的节点位置进行右旋,在对整体进行左旋 - ![平衡二叉树右左](https://gitee.com/seazean/images/raw/master/Java/平衡二叉树右左.png) @@ -2141,23 +2181,22 @@ public class Kmp { 1. 每一个节点或是红色的,或者是黑色的 2. 根节点必须是黑色 -3. 如果一个节点没有子节点或者父节点,则该节点相应的指针属性值为Nil,这些Nil视为叶节点,每个叶节点(Nil)是黑色的 -4. 如果某一个节点是红色,那么它的子节点必须是黑色(不能出现两个红色节点相连 的情况) +3. 如果一个节点没有子节点或者父节点,则该节点相应的指针属性值为 Nil,这些 Nil 视为叶节点,每个叶节点(Nil) 是黑色的 +4. 如果某一个节点是红色,那么它的子节点必须是黑色(不能出现两个红色节点相连 的情况) 5. 对每一个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点 -红黑树与AVL树的比较: +红黑树与 AVL 树的比较: -- 红黑树的插入删除比AVL树更便于控制操作,红黑树更适合于插入修改密集型任务 +* AVL树是更加严格的平衡,可以提供更快的查找速度,适用于读取查找密集型任务 +* 红黑树只是做到了近似平衡,并不是严格的平衡,红黑树的插入删除比AVL树更便于控制操作,红黑树更适合于插入修改密集型任务 -* AVL树是更加严格的平衡,可以提供更快的查找速度,一般读取查找密集型任务,适用AVL树 - -- 红黑树整体性能略优于AVL树,AVL树的旋转比红黑树的旋转更加难以平衡和调试 +- 红黑树整体性能略优于AVL树,AVL树的旋转比红黑树的旋转多,更加难以平衡和调试,插入和删除的效率比红黑树慢 ![红黑树](https://gitee.com/seazean/images/raw/master/Java/红黑树结构图.png) -红黑树添加节点的默认颜色为红色,效率高 +红黑树添加节点的默认颜色为红色,效率高 ![](https://gitee.com/seazean/images/raw/master/Java/红黑树添加节点颜色.png) @@ -2451,11 +2490,107 @@ public static void main(String[] args)throws Exception { -**** +*** -### 图 +#### 字典树 + +##### 基本介绍 + +Trie 树,也叫字典树,是一种专门处理字符串匹配的树形结构,用来解决在一组字符串集合中快速查找某个字符串的问题,Trie 树的本质就是利用字符串之间的公共前缀,将重复的前缀合并在一起 + +* 根节点不包含任何信息 +* 每个节点表示一个字符串中的字符,从**根节点到红色节点的一条路径表示一个字符串** +* 红色节点并不都是叶子节点 + + + + + +注意:要查找的是字符串“he”,从根节点开始,沿着某条路径来匹配,可以匹配成功。但是路径的最后一个节点“e”并不是红色的,也就是说,“he”是某个字符串的前缀子串,但并不能完全匹配任何字符串 + + + +*** + + + +##### 实现Trie + +通过一个下标与字符一一映射的数组,来存储子节点的指针 + + + +时间复杂度是 O(n)(n 表示要查找字符串的长度) + +```java +public class Trie { + private TrieNode root = new TrieNode('/'); + + //插入一个字符 + public void insert(char[] chars) { + TrieNode p = root; + for (int i = 0; i < chars.length; i++) { + //获取字符的索引位置 + int index = chars[i] - 'a'; + if (p.children[index] == null) { + TrieNode node = new TrieNode(chars[i]); + p.children[index] = node; + } + p = p.children[index]; + } + p.isEndChar = true; + } + + //查找一个字符串 + public boolean find(char[] chars) { + TrieNode p = root; + for (int i = 0; i < chars.length; i++) { + int index = chars[i] - 'a'; + if (p.children[index] == null) { + return false; + } + p = p.children[index]; + } + if (p.isEndChar) { + //完全匹配 + return true; + } else { + // 不能完全匹配,只是前缀 + return false; + } + } + + + private class TrieNode { + char data; + TrieNode[] children = new TrieNode[26];//26个英文字母 + boolean isEndChar = false;//结尾字符为true + public TrieNode(char data) { + this.data = data; + } + } +} +``` + + + +*** + + + +##### 优化Trie + +Trie 树是非常耗内存,采取空间换时间的思路。Trie 树的变体有很多,可以在一定程度上解决内存消耗的问题。比如缩点优化,对只有一个子节点的节点,而且此节点不是一个串的结束节点,可以将此节点与子节点合并 + +![](https://gitee.com/seazean/images/raw/master/Java/Tree-字典树缩点优化.png) + + + +参考文章:https://time.geekbang.org/column/article/72414 + + @@ -4191,13 +4326,24 @@ public class Demo1_25 { ### StringBuilder -**String StringBuffer StringBuilder区别**: - String : **不可变**的字符序列,线程安全 - StringBuffer : **可变**的字符序列,线程安全,效率低 - StringBuilder : **可变**的字符序列,JDK5.0新增;线程不安全,效率高。 - 同:底层使用char[]/byte[]存储 +String StringBuffer 和 StringBuilder 区别: + +* String : **不可变**的字符序列,线程安全 +* StringBuffer : **可变**的字符序列,线程安全,底层方法加 synchronized,效率低 +* StringBuilder : **可变**的字符序列,JDK5.0新增;线程不安全,效率高 + +相同点:底层使用 char[] 存储 + +构造方法: + public StringBuilder():创建一个空白可变字符串对象,不含有任何内容 + public StringBuilder(String str):根据字符串的内容,来创建可变字符串对象 + +常用API : + `public StringBuilder append(任意类型)` : 添加数据,并返回对象本身 + `public StringBuilder reverse()` : 返回相反的字符序列 + `public String toString()` : 通过 toString() 就可以实现把 StringBuilder 转换为 String -**源码分析**: +存储原理: ```java String str = "abc"; @@ -4206,14 +4352,31 @@ StringBuffer sb1 = new StringBuffer();//new byte[16] sb1.append('a'); //value[0] = 'a'; ``` -**构造方法**: - public StringBuilder():创建一个空白可变字符串对象,不含有任何内容 - public StringBuilder(String str):根据字符串的内容,来创建可变字符串对象 +append 源码: + +```java +public AbstractStringBuilder append(String str) { + if (str == null) return appendNull(); + int len = str.length(); + ensureCapacityInternal(count + len); + str.getChars(0, len, value, count); + count += len; + return this; +} +private void ensureCapacityInternal(int minimumCapacity) { + // 创建超过数组长度就新的char数组,把数据拷贝过去 + if (minimumCapacity - value.length > 0) { + //int newCapacity = (value.length << 1) + 2;每次扩容2倍+2 + value = Arrays.copyOf(value, newCapacity(minimumCapacity)); + } +} + public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) { + //将字符串中的字符复制到目标字符数组中 + System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin); +} +``` + -**常用API** : - `public StringBuilder append(任意类型)` : 添加数据,并返回对象本身 - `public StringBuilder reverse()` : 返回相反的字符序列 - `public String toString()` : 通过 toString() 就可以实现把 StringBuilder 转换为 String @@ -5361,7 +5524,7 @@ public class ArrayList extends AbstractList } ``` - 当add 第 1 个元素到 ArrayList,size是0,进入ensureCapacityInternal方法, + 当add 第 1 个元素到 ArrayList,size是0,进入 ensureCapacityInternal 方法, ```java private void ensureCapacityInternal(int minCapacity) { @@ -5407,7 +5570,7 @@ public class ArrayList extends AbstractList * 扩容:新容量的大小为 `oldCapacity + (oldCapacity >> 1)`,`oldCapacity >> 1` 需要取整,所以新容量大约是旧容量的 1.5 倍左右,即 oldCapacity+oldCapacity/2 - 扩容操作需要调用`Arrays.copyOf()`(底层`System.arraycopy()`)把原数组整个复制到**新数组**中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数 + 扩容操作需要调用 `Arrays.copyOf()`(底层 `System.arraycopy()`)把原数组整个复制到**新数组**中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数 ```java private void grow(int minCapacity) { @@ -9571,7 +9734,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 使用一个文件描述符管理多个描述符 +* epall_create:一个系统函数,函数将在内核空间内创建一个 epoll 数据结构,可以理解为 epoll 结构空间,返回值为 epoll 的文件描述符编号,后面当有client连接时,向该 epoll 区中添加监听,所以 epoll 使用一个文件描述符管理多个描述符 * epall_ctl:epoll 的事件注册函数,select 函数是调用时指定需要监听的描述符和事件,epoll 先将用户感兴趣的描述符事件注册到 epoll 空间。此函数是非阻塞函数,用来增删改 epoll 空间内的描述符,参数解释: @@ -9672,7 +9835,7 @@ epoll 的特点: * epoll 仅适用于 Linux 系统 * epoll 使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中 * 没有最大描述符数量(并发连接)的限制,打开 fd 的上限远大于1024(1G 内存能监听约10万个端口) -* epoll 的时间复杂度 O(1),epoll 理解为 event poll,不同于忙轮询和无差别轮询,调用 epoll_wait **只是轮询就绪链表**。当监听列表有设备就绪时调用回调函数,把就绪 fd 放入就绪链表中,并唤醒在 epoll_wait 中阻塞的进程,所以 epoll 实际上是**事件驱动**(每个事件关联上fd)的 +* 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() 文件映射内存加速与内核空间的消息传递,减少复制开销 @@ -13091,15 +13254,15 @@ Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所 局部变量表也被称之为局部变量数组或本地变量表,本质上定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量 * 表是建立在线程的栈上,是线程私有的数据,因此不存在数据安全问题 -* 表的容量大小是在编译期确定的,保存在方法的Code属性的maximum local variables数据项中 +* 表的容量大小是在编译期确定的,保存在方法的 Code 属性的 maximum local variables 数据项中 * 表中的变量只在当前方法调用中有效,方法结束栈帧销毁,局部变量表也会随之销毁 * 表中的变量也是重要的垃圾回收根节点,只要被表中数据直接或间接引用的对象都不会被回收 -局部变量表最基本的存储单元是**slot(变量槽)**: +局部变量表最基本的存储单元是 **slot(变量槽)**: * 参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束,JVM为每一个slot都分配一个访问索引,通过索引即可访问到槽中的数据 * 存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量 -* 32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot +* 32 位以内的类型只占用一个 slot(包括returnAddress类型),64位的类型(long和double)占用两个slot * 局部变量表中的槽位是可以**重复利用**的,如果一个局部变量过了其作用域,那么之后申明的新的局部变量就可能会复用过期局部变量的槽位,从而达到节省资源的目的 @@ -13119,7 +13282,7 @@ Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所 * Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈 * 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中 -栈顶缓存技术ToS(Top-of-Stack Cashing):将栈顶元素全部缓存在CPU的寄存器中,以此降低对内存的读/写次数,提升执行的效率 +栈顶缓存技术 ToS(Top-of-Stack Cashing):将栈顶元素全部缓存在 CPU 的寄存器中,以此降低对内存的读/写次数,提升执行的效率 基于栈式架构的虚拟机使用的零地址指令更加紧凑,完成一项操作需要使用很多入栈和出栈指令,所以需要更多的指令分派(instruction dispatch)次数和内存读/写次数,由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度,所以需要栈顶缓存技术 @@ -13591,11 +13754,11 @@ public class Demo1_27 { * **对象优先在 Eden 分配**:当创建一个对象的时候,对象会被分配在新生代的 Eden 区,当Eden 区要满了时候,触发 YoungGC -* 当进行 YoungGC 后,此时在 Eden 区存活的对象被移动到S0区,并且**当前对象的年龄会加1**,清空 Eden 区 +* 当进行 YoungGC 后,此时在 Eden 区存活的对象被移动到 to 区,并且**当前对象的年龄会加1**,清空 Eden 区 -* 当再一次触发 YoungGC 的时候,会把 Eden 区中存活下来的对象和 S0 中的对象,移动到 S1 区中,这些对象的年龄会加1,清空 Eden 区和 S0 区 +* 当再一次触发 YoungGC 的时候,会把 Eden 区中存活下来的对象和 to 中的对象,移动到 from 区中,这些对象的年龄会加 1,清空 Eden 区和 to 区 -* to区永远是空 Survivor 区,from 区是有数据的,每次 MinorGC 后两个区域互换 +* to 区永远是空 Survivor 区,from 区是有数据的,每次 MinorGC 后两个区域互换 晋升到老年代: @@ -13736,8 +13899,8 @@ FullGC 同时回收新生代、老年代和方法区,只会存在一个FullGC * 调用 System.gc(): - * 在默认情况下,通过system.gc() 或 Runtime.getRuntime().gc() 的调用,会显式触发FullGC,同时对老年代和新生代进行回收,但是虚拟机不一定真正去执行,无法保证对垃圾收集器的调用 (马上触发GC) - * 不建议使用这种方式,应该让虚拟机管理内存。一般情况下,垃圾回收应该是自动进行的,无须手动触发;在一些特殊情况下,如正在编写一个性能基准,可以在运行之间调用System.gc() + * 在默认情况下,通过 System.gc() 或 Runtime.getRuntime().gc() 的调用,会显式触发 FullGC,同时对老年代和新生代进行回收,但是虚拟机不一定真正去执行,无法保证对垃圾收集器的调用 (马上触发GC) + * 不建议使用这种方式,应该让虚拟机管理内存。一般情况下,垃圾回收应该是自动进行的,无须手动触发;在一些特殊情况下,如正在编写一个性能基准,可以在运行之间调用 System.gc() * 老年代空间不足: @@ -14294,7 +14457,7 @@ GC性能指标: #### Parallel -Parallel Scavenge收集器是应用于新生代的并行垃圾回收器,**采用复制算法**、并行回收和"Stop the World"机制 +Parallel Scavenge 收集器是应用于新生代的并行垃圾回收器,**采用复制算法**、并行回收和"Stop the World"机制 Parallel Old收集器:是一个应用于老年代的并行垃圾回收器,**采用标记-整理算法** @@ -14393,7 +14556,7 @@ Mark Sweep 会造成内存碎片,还不把算法换成 Mark Compact 的原因 - 吞吐量降低:在并发阶段虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,CPU 利用率不够高 - CMS收集器无法处理浮动垃圾,可能出现 Concurrent Mode Failure导致另一次Full GC的产生 浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,CMS 收集需要预留出一部分内存,不能等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS,导致很长的停顿时间 -- 标记 - 清除算法导致的空间碎片,往往出现老年代空间无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC;为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer)技术,而只能够选择空闲链表(Free List)执行内存分配 +- 标记 - 清除算法导致的空间碎片,往往出现老年代空间无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC;为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer)技术,而只能够选择空闲列表(Free List)执行内存分配 参数设置: @@ -14427,19 +14590,19 @@ Mark Sweep 会造成内存碎片,还不把算法换成 Mark Compact 的原因 G1(Garbage-First)是一款面向服务端应用的垃圾收集器,**应用于新生代和老年代**、采用标记-整理算法、软实时、低延迟、可设定目标(最大STW停顿时间)的垃圾回收器,用于代替CMS,适用于较大的堆(>4~6G),在JDK9之后默认使用G1 -G1对比其他处理器的优点: +G1 对比其他处理器的优点: * **并发与并行:** - * 并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力,此时用户线程STW + * 并行性:G1在回收期间,可以有多个 GC 线程同时工作,有效利用多核计算能力,此时用户线程 STW * 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此不会在整个回收阶段发生完全阻塞应用程序的情况 - * 其他的垃圾收集器使用内置的JVM线程执行GC的多线程操作,而G1 GC可以采用应用线程承担后台运行的GC工作,JVM的GC线程处理速度慢时,系统会**调用应用程序线程加速垃圾回收**过程 + * 其他的垃圾收集器使用内置的 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 属于分代型垃圾回收器,区分年轻代和老年代,年轻代依然有 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 的巨型对象就可以在新生代垃圾回收时处理掉 * Region结构图: @@ -14448,14 +14611,14 @@ G1对比其他处理器的优点: - **空间整合:** - - CMS:“标记-清除”算法、内存碎片、若干次GC后进行一次碎片整理 + - CMS:“标记-清除”算法、内存碎片、若干次 GC 后进行一次碎片整理 - G1:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(Region 之间)上来看是基于“复制”算法实现的,两种算法都可以避免内存碎片 - **可预测的停顿时间模型(软实时soft real-time):** - 可以指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒 - - 由于分块的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,对于全局停顿情况也能得到较好的控制 - - G1跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间,通过过去回收的经验获得),在后台维护一个**优先列表**,每次根据允许的收集时间,优先回收价值最大的Region,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率 + - 由于分块的原因,G1 可以只选取部分区域进行内存回收,这样缩小了回收的范围,对于全局停顿情况也能得到较好的控制 + - G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间,通过过去回收的经验获得),在后台维护一个**优先列表**,每次根据允许的收集时间优先回收价值最大的 Region,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率 * 相比于 CMS GC,G1 未必能做到 CMS 在最好情况下的延时停顿,但是最差情况要好很多 @@ -14467,7 +14630,7 @@ G1垃圾收集器的缺点: 应用场景: * 面向服务端应用,针对具有大内存、多处理器的机器 -* 需要低GC延迟,并具有大堆的应用程序提供解决方案 +* 需要低 GC 延迟,并具有大堆的应用程序提供解决方案 @@ -14496,18 +14659,18 @@ G1中提供了三种垃圾回收模式:YoungGC、Mixed GC 和 FullGC,在不 -顺时针:Young GC -> Young GC + Concurrent Mark -> Mixed GC 顺序,进行垃圾回收 +顺时针:Young GC → Young GC + Concurrent Mark → Mixed GC 顺序,进行垃圾回收 -* **Young GC**:发生在年轻代的GC算法,一般对象(除了巨型对象)都是在 eden region 中分配内存,当所有 eden region 被耗尽无法申请内存时,就会触发一次 young gc,G1停止应用程序的执行 (Stop-The-World),把活跃对象放入老年代,垃圾对象回收 +* **Young GC**:发生在年轻代的 GC 算法,一般对象(除了巨型对象)都是在 eden region 中分配内存,当所有 eden region 被耗尽无法申请内存时,就会触发一次 young gc,G1停止应用程序的执行 (Stop-The-World),把活跃对象放入老年代,垃圾对象回收 **回收过程**: 1. 扫描根:根引用连同 RSet 记录的外部引用作为扫描存活对象的入口 - 2. 更新RSet:处理 dirty card queue 更新 RS,此后 RSet 准确的反映对象的引用关系 + 2. 更新 RSet:处理 dirty card queue 更新 RS,此后 RSet 准确的反映对象的引用关系 * dirty card queue:类似缓存,产生了引用先记录在这里,然后更新到 RSet * 作用:产生引用直接更新 RSet 需要线程同步开销很大,使用队列性能好 - 3. 处理RSet:识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象 - 4. 复制对象:Eden区内存段中存活的对象会被复制到 survivor 区,survivor 区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到 old 区中空的内存分段,如果 survivor 空间不够,Eden 空间的部分数据会直接晋升到老年代空间 + 3. 处理 RSet:识别被老年代对象指向的 Eden 中的对象,这些被指向的对象被认为是存活的对象 + 4. 复制对象:Eden 区内存段中存活的对象会被复制到 survivor 区,survivor 区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到 old 区中空的内存分段,如果 survivor 空间不够,Eden 空间的部分数据会直接晋升到老年代空间 5. 处理引用:处理 Soft,Weak,Phantom,JNI Weak 等引用,最终 Eden 空间的数据为空,GC 停止工作 * **并发标记过程**: @@ -14520,18 +14683,18 @@ G1中提供了三种垃圾回收模式:YoungGC、Mixed GC 和 FullGC,在不 ![](https://gitee.com/seazean/images/raw/master/Java/JVM-G1收集器.jpg) -* **Mixed GC**:当很多对象晋升到老年代 old region 时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即 Mixed GC,除了回收整个 young region,还会**回收一部分**的 old region,过程同YGC +* **Mixed GC**:当很多对象晋升到老年代 old region 时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即 Mixed GC,除了回收整个 young region,还会**回收一部分**的 old region,过程同 YGC - 注意:**是一部分老年代,而不是全部老年代**,可以选择哪些old region收集,对垃圾回收的时间进行控制 + 注意:**是一部分老年代,而不是全部老年代**,可以选择哪些 old region 收集,对垃圾回收的时间进行控制 - 在G1中,Mixed GC可以通过`-XX:InitiatingHeapOccupancyPercent`设置阈值 + 在G1中,Mixed GC可以通过 `-XX:InitiatingHeapOccupancyPercent` 设置阈值 -* **Full GC**:对象内存分配速度过快,Mixed GC来不及回收,导致老年代被填满,就会触发一次Full GC,G1的Full GC算法就是单线程执行的垃圾回收,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免Full GC +* **Full GC**:对象内存分配速度过快,Mixed GC 来不及回收,导致老年代被填满,就会触发一次 Full GC,G1 的 Full GC 算法就是单线程执行的垃圾回收,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免 Full GC - 产生Full GC的原因: + 产生 Full GC 的原因: * 晋升时没有足够的空间存放晋升的对象 - * 并发处理过程完成之前空间耗尽 + * 并发处理过程完成之前空间耗尽,浮动垃圾 @@ -14557,9 +14720,9 @@ G1中提供了三种垃圾回收模式:YoungGC、Mixed GC 和 FullGC,在不 ##### 调优 -G1的设计原则就是简化JVM性能调优,只需要简单的三步即可完成调优: +G1 的设计原则就是简化 JVM 性能调优,只需要简单的三步即可完成调优: -1. 开启G1垃圾收集器 +1. 开启 G1 垃圾收集器 2. 设置堆的最大内存 3. 设置最大的停顿时间(stw) @@ -14572,8 +14735,8 @@ G1的设计原则就是简化JVM性能调优,只需要简单的三步即可完 **不要设置新生代和老年代的大小**: -- 避免使用-Xmn或-XX:NewRatio等相关选项显式设置年轻代大小,G1收集器在运行的时候会调整新生代和老年代的大小,从而达到我们为收集器设置的暂停时间目标 -- 设置了新生代大小相当于放弃了G1为我们做的自动调优,我们需要做的只是设置整个堆内存的大小,剩下的交给G1自己去分配各个代的大小 +- 避免使用 -Xmn 或 -XX:NewRatio 等相关选项显式设置年轻代大小,G1 收集器在运行的时候会调整新生代和老年代的大小,从而达到我们为收集器设置的暂停时间目标 +- 设置了新生代大小相当于放弃了 G1 为我们做的自动调优,我们需要做的只是设置整个堆内存的大小,剩下的交给 G1 自己去分配各个代的大小 @@ -19890,15 +20053,15 @@ 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 @@ -19923,7 +20086,7 @@ Java内存模型具备一些先天的“有序性”,即不需要通过任何 ### 设计模式 -#### 两阶段终止 +#### 终止模式 终止模式之两阶段终止模式:停止标记用 volatile 是为了保证该变量在多个线程之间的可见性 @@ -20000,9 +20163,9 @@ public class MonitorService { 对比保护性暂停模式:保护性暂停模式用在一个线程等待另一个线程的执行结果,当条件不满足时线程等待 -例子:希望 doInit() 方法仅被调用一次,下面的实现是否有问题,为什么?有问题: +例子:希望 doInit() 方法仅被调用一次,下面的实现出现的问题: -* 当t1线程进入 init() 准备 doInit(),t2 线程进来,initialized还为false,则t2就又初始化一次 +* 当 t1 线程进入 init() 准备 doInit(),t2 线程进来,initialized 还为f alse,则 t2 就又初始化一次 * volatile 适合一个线程写,其他线程读的情况,这个代码需要加锁 ```java @@ -25352,7 +25515,7 @@ public boolean add(E e) { 跳表 SkipList 是一个**有序的链表**,默认升序,底层是链表加多级索引的结构。跳表可以对元素进行快速查询,类似于平衡树,是一种利用空间换时间的算法 -对于单链表,即使链表是有序的,如果查找数据也只能从头到尾遍历链表,所以采用链表上建索引的方式提高效率,跳表的查询时间复杂度是 **O(logn)**, +对于单链表,即使链表是有序的,如果查找数据也只能从头到尾遍历链表,所以采用链表上建索引的方式提高效率,跳表的查询时间复杂度是 **O(logn)**,空间复杂度 O(n) ConcurrentSkipListMap 提供了一种线程安全的并发访问的排序映射表,内部是跳表结构实现,通过 CAS + volatile 保证线程安全 @@ -25363,7 +25526,7 @@ ConcurrentSkipListMap 提供了一种线程安全的并发访问的排序映射 ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentSkipListMap数据结构.png) -BaseHeader存储数据,headIndex存储索引,纵向上**所有索引指向链表最下面的节点** +BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引指向链表最下面的节点** diff --git a/Tool.md b/Tool.md index 301d061..dc5fc2d 100644 --- a/Tool.md +++ b/Tool.md @@ -2534,7 +2534,7 @@ mount [-fnrsvw] [-t vfstype] [-o options] device dir ### 状态 -**启动语法:service 服务 status** +**启动语法:service 服务 status** * 查看防火墙状态:`service iptables status` @@ -2545,7 +2545,7 @@ mount [-fnrsvw] [-t vfstype] [-o options] device dir -### 防火墙放行 +### 放行 设置端口防火墙放行 From 78f11d23e75a019db0a6b70da0147b33171fdf68 Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 30 Jun 2021 22:00:25 +0800 Subject: [PATCH 061/242] Update Java Notes --- DB.md | 145 +++------------------------------- Java.md | 240 +++++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 237 insertions(+), 148 deletions(-) diff --git a/DB.md b/DB.md index f76780e..d375940 100644 --- a/DB.md +++ b/DB.md @@ -3926,9 +3926,10 @@ B+Tree 为 BTree 的变种,B+Tree 与 BTree 的区别为: * n 叉 B+Tree 最多含有 n 个 key(哈希值),而 BTree 最多含有 n-1 个 key -- 所有**非叶子节点只存储键值 key**信息,可以看作 key 的索引部分 -- 所有**数据都存储在叶子节点**,按照 key 大小顺序排列 -- 节点从上到下的所有节点中的 key 在叶子节点中也存在(比如 5),key 允许重复,B 树不同节点不存在重复的 key +- 所有**非叶子节点只存储键值 key**信息,只进行数据索引,使每个非叶子节点所能保存的关键字大大增加 +- 所有**数据都存储在叶子节点**,所以每次数据查询的次数都一样 +- 叶子节点按照 key 大小顺序排列,左边结尾数据都会保存右边节点开始数据的指针,形成一个链表 +- 所有节点中的 key 在叶子节点中也存在(比如 5),key 允许重复,B 树不同节点不存在重复的 key @@ -3946,7 +3947,7 @@ BTree 数据结构中每个节点中不仅包含数据的 key 值,还有 data MySQL 索引数据结构对经典的 B+Tree 进行了优化,在原 B+Tree 的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的 B+Tree,**提高区间访问的性能,防止回旋查找** -区间访问的意思是访问索引为 5 - 15 的数据,这样就可以直接根据相邻节点的指针遍历 +区间访问的意思是访问索引为 5 - 15 的数据,可以直接根据相邻节点的指针遍历 ![](https://gitee.com/seazean/images/raw/master/DB/索引的原理-B+Tree.png) @@ -3955,7 +3956,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 操作 @@ -8751,135 +8752,7 @@ Redis 使用跳跃表作为有序集合键的底层实现之一,如果一个 ### Bitmaps -#### 布隆过滤 - -##### 基本介绍 - -布隆过滤器:一种数据结构,是一个很长的二进制向量(位数组)和一系列随机映射函数(哈希函数),既然是二进制,每个空间存放的不是0就是1,但是初始默认值都是0,所以布隆过滤器不存数据只存状态 - - - -这种数据结构是高效且性能很好的,但缺点是具有一定的错误识别率和删除难度。并且,理论情况下,添加到集合中的元素越多,误报的可能性就越大 - - - -*** - - - -##### 工作流程 - -向布隆过滤器中添加一个元素key时,会通过多个hash函数得到多个哈希值,在位数组中把对应下标的值置为 1 - -![](https://gitee.com/seazean/images/raw/master/DB/Redis-布隆过滤器添加数据.png) - -布隆过滤器查询一个数据,是否在二进制的集合中,查询过程如下: - -- 通过 K 个哈希函数计算该数据,对应计算出的 K 个hash值 -- 通过 hash 值找到对应的二进制的数组下标 -- 判断方法:如果存在一处位置的二进制数据是0,那么该数据不存在。如果都是1,该数据存在集合中 - -布隆过滤器优缺点: - -* 优点: - * 二进制组成的数组,占用内存极少,并且插入和查询速度都足够快 - * 去重方便:当字符串第一次存储时对应的位数组下标设置为 1,当第二次存储相同字符串时,因为对应位置已设置为 1,所以很容易知道此值已经存在 -* 缺点: - * 随着数据的增加,误判率会增加:添加数据是通过计算数据的hash值,不同的字符串可能哈希出来的位置相同,导致无法确定到底是哪个数据存在,**这种情况可以适当增加位数组大小或者调整哈希函数** - * 无法删除数据:可能存在几个数据占据相同的位置,所以删除一位会导致很多数据失效 - -* 总结:**布隆过滤器判断某个元素存在,小概率会误判。如果判断某个元素不在,那这个元素一定不在** - - - -参考文章:https://www.cnblogs.com/ysocean/p/12594982.html - - - -*** - - - -##### Guava - -引入 Guava 的依赖: - -```xml - - com.google.guava - guava - 28.0-jre - -``` - -指定误判率为(0.01): - -```java -public static void main(String[] args) { - // 创建布隆过滤器对象 - BloomFilter filter = BloomFilter.create( - Funnels.integerFunnel(), - 1500, - 0.01); - // 判断指定元素是否存在 - System.out.println(filter.mightContain(1)); - System.out.println(filter.mightContain(2)); - // 将元素添加进布隆过滤器 - filter.put(1); - filter.put(2); - System.out.println(filter.mightContain(1)); - System.out.println(filter.mightContain(2)); -} -``` - - - -*** - - - -##### 实现布隆 - -```java -class MyBloomFilter { - //布隆过滤器容量 - private static final int DEFAULT_SIZE = 2 << 28; - //bit数组,用来存放key - private static BitSet bitSet = new BitSet(DEFAULT_SIZE); - //后面hash函数会用到,用来生成不同的hash值,随意设置 - private static final int[] ints = {1, 6, 16, 38, 58, 68}; - - //add方法,计算出key的hash值,并将对应下标置为true - public void add(Object key) { - Arrays.stream(ints).forEach(i -> bitSet.set(hash(key, i))); - } - - //判断key是否存在,true不一定说明key存在,但是false一定说明不存在 - public boolean isContain(Object key) { - boolean result = true; - for (int i : ints) { - //短路与,只要有一个bit位为false,则返回false - result = result && bitSet.get(hash(key, i)); - } - return result; - } - - //hash函数,借鉴了hashmap的扰动算法 - private int hash(Object key, int i) { - int h; - return key == null ? 0 : (i * (DEFAULT_SIZE - 1) & ((h = key.hashCode()) ^ (h >>> 16))); - } -} - -``` - - - -*** - - - -#### 基本操作 +#### 操作 指令操作: @@ -8915,10 +8788,12 @@ class MyBloomFilter { -#### 应用场景 +#### 应用 - 解决Redis缓存穿透,判断给定数据是否存在, 防止缓存穿透 + 布隆过滤器在 Java.md → SE → 算法 → 位图 部分详解 + - 垃圾邮件过滤,对每一个发送邮件的地址进行判断是否在布隆的黑名单中,如果在就判断为垃圾邮件 diff --git a/Java.md b/Java.md index 0f39789..d278d31 100644 --- a/Java.md +++ b/Java.md @@ -86,8 +86,7 @@ Java语言提供了八种基本类型。六种数字类型(四个整数型, - boolean 数据类型表示一位的信息 - 只有两个取值:true 和 false -- 这种类型只作为一种标志来记录 true/false 情况 -- JVM 规范指出 boolean 当做 int 处理,也就是4字节,boolean 数组当做 byte 数组处理,这样可以得出 boolean 类型单独使用占了4个字节,在数组中是1个字节 +- JVM 规范指出 boolean 当做 int 处理,boolean 数组当做 byte 数组处理,这样可以得出 boolean 类型单独使用占了4个字节,在数组中是1个字节 - 默认值是 **`false`** - 例子:`boolean one = true` @@ -2182,7 +2181,7 @@ public class Kmp { 1. 每一个节点或是红色的,或者是黑色的 2. 根节点必须是黑色 3. 如果一个节点没有子节点或者父节点,则该节点相应的指针属性值为 Nil,这些 Nil 视为叶节点,每个叶节点(Nil) 是黑色的 -4. 如果某一个节点是红色,那么它的子节点必须是黑色(不能出现两个红色节点相连 的情况) +4. 如果某一个节点是红色,那么它的子节点必须是黑色(不能出现两个红色节点相连的情况) 5. 对每一个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点 红黑树与 AVL 树的比较: @@ -2592,6 +2591,213 @@ Trie 树是非常耗内存,采取空间换时间的思路。Trie 树的变体 +*** + + + +### 图 + +图的邻接表形式: + +```java +public class AGraph { + private VertexNode[] adjList; //邻接数组 + private int vLen, eLen; //顶点数和边数 + + public AGraph(int vLen, int eLen) { + this.vLen = vLen; + this.eLen = eLen; + adjList = new VertexNode[vLen]; + } + //弧节点 + private class ArcNode { + int adjVex; //该边所指向的顶点的位置 + ArcNode nextArc; //下一条边(弧) + //int info //添加权值 + + public ArcNode(int adjVex) { + this.adjVex = adjVex; + nextArc = null; + } + } + + //表顶点 + private class VertexNode { + char data; //顶点信息 + ArcNode firstArc; //指向第一条边的指针 + + public VertexNode(char data) { + this.data = data; + firstArc = null; + } + } +} +``` + +图的邻接矩阵形式: + +```java +public class MGraph { + private int[][] edges; //邻接矩阵定义,有权图将int改为float + private int vLen; //顶点数 + private int eLen; //边数 + private VertexNode[] vex; //存放节点信息 + + public MGraph(int vLen, int eLen) { + this.vLen = vLen; + this.eLen = eLen; + this.edges = new int[vLen][vLen]; + this.vex = new VertexNode[vLen]; + } + + private class VertexNode { + int num; //顶点编号 + String info; //顶点信息 + + public VertexNode(int num) { + this.num = num; + this.info = null; + } + } +} +``` + + + +图相关的算法需要很多的流程图,此处不再一一列举,推荐参考书籍《数据结构高分笔记》 + + + +*** + + + +### 位图 + +#### 基本介绍 + +布隆过滤器:一种数据结构,是一个很长的二进制向量(位数组)和一系列随机映射函数(哈希函数),既然是二进制,每个空间存放的不是0就是1,但是初始默认值都是0,所以布隆过滤器不存数据只存状态 + + + +这种数据结构是高效且性能很好的,但缺点是具有一定的错误识别率和删除难度。并且,理论情况下,添加到集合中的元素越多,误报的可能性就越大 + + + +*** + + + +#### 工作流程 + +向布隆过滤器中添加一个元素key时,会通过多个hash函数得到多个哈希值,在位数组中把对应下标的值置为 1 + +![](https://gitee.com/seazean/images/raw/master/DB/Redis-布隆过滤器添加数据.png) + +布隆过滤器查询一个数据,是否在二进制的集合中,查询过程如下: + +- 通过 K 个哈希函数计算该数据,对应计算出的 K 个hash值 +- 通过 hash 值找到对应的二进制的数组下标 +- 判断方法:如果存在一处位置的二进制数据是0,那么该数据不存在。如果都是1,该数据存在集合中 + +布隆过滤器优缺点: + +* 优点: + * 二进制组成的数组,占用内存极少,并且插入和查询速度都足够快 + * 去重方便:当字符串第一次存储时对应的位数组下标设置为 1,当第二次存储相同字符串时,因为对应位置已设置为 1,所以很容易知道此值已经存在 +* 缺点: + * 随着数据的增加,误判率会增加:添加数据是通过计算数据的hash值,不同的字符串可能哈希出来的位置相同,导致无法确定到底是哪个数据存在,**这种情况可以适当增加位数组大小或者调整哈希函数** + * 无法删除数据:可能存在几个数据占据相同的位置,所以删除一位会导致很多数据失效 + +* 总结:**布隆过滤器判断某个元素存在,小概率会误判。如果判断某个元素不在,那这个元素一定不在** + + + +参考文章:https://www.cnblogs.com/ysocean/p/12594982.html + + + +*** + + + +#### Guava + +引入 Guava 的依赖: + +```xml + + com.google.guava + guava + 28.0-jre + +``` + +指定误判率为(0.01): + +```java +public static void main(String[] args) { + // 创建布隆过滤器对象 + BloomFilter filter = BloomFilter.create( + Funnels.integerFunnel(), + 1500, + 0.01); + // 判断指定元素是否存在 + System.out.println(filter.mightContain(1)); + System.out.println(filter.mightContain(2)); + // 将元素添加进布隆过滤器 + filter.put(1); + filter.put(2); + System.out.println(filter.mightContain(1)); + System.out.println(filter.mightContain(2)); +} +``` + + + +*** + + + +#### 实现布隆 + +```java +class MyBloomFilter { + //布隆过滤器容量 + private static final int DEFAULT_SIZE = 2 << 28; + //bit数组,用来存放key + private static BitSet bitSet = new BitSet(DEFAULT_SIZE); + //后面hash函数会用到,用来生成不同的hash值,随意设置 + private static final int[] ints = {1, 6, 16, 38, 58, 68}; + + //add方法,计算出key的hash值,并将对应下标置为true + public void add(Object key) { + Arrays.stream(ints).forEach(i -> bitSet.set(hash(key, i))); + } + + //判断key是否存在,true不一定说明key存在,但是false一定说明不存在 + public boolean isContain(Object key) { + boolean result = true; + for (int i : ints) { + //短路与,只要有一个bit位为false,则返回false + result = result && bitSet.get(hash(key, i)); + } + return result; + } + + //hash函数,借鉴了hashmap的扰动算法 + private int hash(Object key, int i) { + int h; + return key == null ? 0 : (i * (DEFAULT_SIZE - 1) & ((h = key.hashCode()) ^ (h >>> 16))); + } +} + +``` + + + + + *** @@ -3580,10 +3786,11 @@ class Cat extends Animal{} #### instanceof -* 引用类型强制类型转换:父类类型的变量或者对象强制类型转换成子类类型的变量,否则报错! +instanceof:判断左边的对象是否是右边的类的实例,或者是其直接或间接子类,或者是其接口的实现类 + +* 引用类型强制类型转换:父类类型的变量或者对象强制类型转换成子类类型的变量,否则报错 * 强制类型转换的格式:**类型 变量名称 = (类型)(对象或者变量)** -* 有继承/实现关系的两个类型就可以进行强制类型转换,编译阶段一定不报错!但是运行阶段可能出现:类型转换异常 ClassCastException -* **instanceof**:判断左边的对象是否是右边的类的实例,或者是其直接或间接子类,或者是其接口的实现类 +* 有继承/实现关系的两个类型就可以进行强制类型转换,编译阶段一定不报错,但是运行阶段可能出现类型转换异常 ClassCastException ```java public class Demo{ @@ -5296,6 +5503,14 @@ public class RegexDemo { 特点:**红黑树的增删查改性能都好** +各数据结构时间复杂度对比: + +![](https://gitee.com/seazean/images/raw/master/Java/数据结构的复杂度对比.png) + + + +图片来源:https://www.bigocheatsheet.com/ + *** @@ -8810,9 +9025,9 @@ fw.close; 作用:可以把低级的字节输入流包装成一个高级的缓冲字节输入流管道, 提高字节输入流读数据的性能 -构造器: `public BufferedInputStream(InputStream in)` +构造器:`public BufferedInputStream(InputStream in)` -原理:缓冲字节输入流管道自带了一个8KB的缓冲池,每次可以直接借用操作系统的功能最多提取 8KB 的数据到缓冲池中去,以后我们直接从缓冲池读取数据,所以性能较好 +原理:缓冲字节输入流管道自带了一个 8KB 的缓冲池,每次可以直接借用操作系统的功能最多提取 8KB 的数据到缓冲池中去,以后我们直接从缓冲池读取数据,所以性能较好 ```java public class BufferedInputStreamDemo01 { @@ -9989,7 +10204,7 @@ read 调用图示:read、write 都是系统调用指令 mmap(Memory Mapped Files)加 write 实现零拷贝,零拷贝就是没有数据从内核空间复制到用户空间 -用户空间和内核空间共享同一块物理地址,省去用户态和内核态之间的拷贝。写网卡时,共享空间的内容拷贝到 socket 缓冲区,然后交给 DMA 发送到网卡,只需要 3 次复制 +用户空间和内核空间都使用内存,所以可以共享同一块物理内存地址,省去用户态和内核态之间的拷贝。写网卡时,共享空间的内容拷贝到 socket 缓冲区,然后交给 DMA 发送到网卡,只需要 3 次复制 进行了 4 次用户空间与内核空间的上下文切换,以及 3 次数据拷贝(2 次 DMA,一次 CPU 复制): @@ -10002,7 +10217,7 @@ mmap(Memory Mapped Files)加 write 实现零拷贝,零拷贝就是没有 缺点:不可靠,写到 mmap 中的数据并没有被真正的写到硬盘,操作系统会在程序主动调用 flush 的时候才把数据真正的写到硬盘 -Java NIO 提供了 **MappedByteBuffer** 类可以用来实现 mmap 内存映射,MappedByteBuffer 类对象只能通过调用 `FileChannel.map()` 获取 +Java NIO 提供了 **MappedByteBuffer** 类可以用来实现 mmap 内存映射,MappedByteBuffer 类对象**只能**通过调用 `FileChannel.map()` 获取 @@ -10912,7 +11127,6 @@ FileChannel 中的成员属性: * `MapMode.READ_WRITE`:读/写,对得到的缓冲区的更改最终将写入文件;但该更改对映射到同一文件的其他程序不一定是可见的 * `MapMode.PRIVATE`:私用,可读可写,但是修改的内容不会写入文件,只是 buffer 自身的改变,称之为 copy on write 写时复制 -* position:文件映射时的起始位置 * `public final FileLock lock()`:获取此文件通道的排他锁 MappedByteBuffer,可以让文件直接在内存(堆外内存)中进行修改,这种方式叫做内存映射,可以直接调用系统底层的缓存,没有 JVM 和系统之间的复制操作,提高了传输效率,作用: @@ -10935,7 +11149,7 @@ public class MappedByteBufferTest { /** * 参数1 FileChannel.MapMode.READ_WRITE 使用的读写模式 - * 参数2 0: 可以直接修改的起始位置 + * 参数2 0: 文件映射时的起始位置 * 参数3 5: 是映射到内存的大小(不是索引位置),即将 1.txt 的多少个字节映射到内存 * 可以直接修改的范围就是 0-5 * 实际类型 DirectByteBuffer @@ -10955,7 +11169,7 @@ public class MappedByteBufferTest { 从硬盘上将文件读入内存,要经过文件系统进行数据拷贝,拷贝操作是由文件系统和硬件驱动实现。通过内存映射的方法访问硬盘上的文件,拷贝数据的效率要比 read 和 write 系统调用高: - read() 是系统调用,首先将文件从硬盘拷贝到内核空间的一个缓冲区,再将这些数据拷贝到用户空间,实际上进行了两次数据拷贝 -- map() 也是系统调用,但没有进行数据拷贝,当缺页中断发生时,直接将文件从硬盘拷贝到用户空间,只进行了一次数据拷贝 +- mmap() 也是系统调用,但没有进行数据拷贝,当缺页中断发生时,直接将文件从硬盘拷贝到共享内存,只进行了一次数据拷贝 注意:mmap 的文件映射,在 Full GC 时才会进行释放,如果需要手动清除内存映射文件,可以反射调用sun.misc.Cleaner 方法 From f45b9485f7b2c5e386bd6b79271a360fdbe00b42 Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 1 Jul 2021 21:07:22 +0800 Subject: [PATCH 062/242] Update Java Notes --- DB.md | 26 +++++++++++++------ Issue.md | 41 +++++++++++++++++++++++++++--- Java.md | 76 ++++++++++++++++++++++++++++++++++---------------------- 3 files changed, 102 insertions(+), 41 deletions(-) diff --git a/DB.md b/DB.md index d375940..221c5e9 100644 --- a/DB.md +++ b/DB.md @@ -8341,8 +8341,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) @@ -8370,6 +8370,9 @@ ziplist 使用更加紧凑的结构实现多个元素的连续存储,所以在 Redis 字典使用散列表为底层实现,一个散列表里面有多个散列表节点,每个散列表节点就保存了字典中的一个键值对,发生哈希冲突采用链表法解决,存储无序 +* 为了避免散列表性能的下降,当装载因子大于 1 的时候,Redis 会触发扩容,将散列表扩大为原来大小的 2 倍左右 +* 当数据动态减少之后,为了节省内存,当装载因子小于 0.1 的时候,Redis 就会触发缩容,缩小为字典中数据个数的大约 2 倍大小 + *** @@ -8384,7 +8387,7 @@ Redis 字典使用散列表为底层实现,一个散列表里面有多个散 数据存储结构:一个存储空间保存多个数据,且通过数据可以体现进入顺序,允许重复元素 -list类型:保存多个数据,底层使用**双向链表**存储结构实现,类似于 LinkedList +list 类型:保存多个数据,底层使用**双向链表**存储结构实现,类似于 LinkedList @@ -8464,7 +8467,14 @@ list类型:保存多个数据,底层使用**双向链表**存储结构实现 ##### 底层结构 -在 Redis3.2 版本以前列表类型的内部编码有两种:ziplist(压缩列表)和 linkedlist(链表),在 Redis3.2版本 以后对列表数据结构进行了改造,使用 quicklist(快速列表)代替了 ziplist 和 linkedlist +在 Redis3.2 版本以前列表类型的内部编码有两种:ziplist(压缩列表)和 linkedlist(链表) + +列表中存储的数据量比较小的时候,列表就可以采用压缩列表的方式实现: + +* 列表中保存的单个数据(有可能是字符串类型的)小于 64 字节 +* 列表中数据个数少于 512 个 + +在 Redis3.2版本 以后对列表数据结构进行了改造,使用 quicklist(快速列表)代替了 ziplist 和 linkedlist @@ -8612,11 +8622,11 @@ set类型:与hash存储结构完全相同,仅存储键不存储值(nil) 集合类型的内部编码有两种: -* intset(整数集合):当集合中的元素都是整数且元素个数小于set-maxintset-entries配置(默认512个)时,Redis 会选用 intset 来作为集合的内部实现,从而减少内存的使用 +* intset(整数集合):当集合中的元素都是整数且元素个数小于 set-maxintset-entries配置(默认512个)时,Redis 会选用 intset 来作为集合的内部实现,从而减少内存的使用 -* hashtable(哈希表):当集合类型无法满足 intset 条件时,Redis会使用 hashtable 作为集合的内部实现 +* hashtable(哈希表):当集合类型无法满足 intset 条件时,Redis 会使用 hashtable 作为集合的内部实现 -整数集合(intset)是 Redis 用于保存整数值的集合抽象数据结构,可以保存类型为 int16_t、int32_t 或者 int64_t的整数值,并且保证集合中的元素是有序不重复的 +整数集合(intset)是 Redis 用于保存整数值的集合抽象数据结构,可以保存类型为 int16_t、int32_t 或者 int64_t的整数值,并且保证集合中的元素是**有序不重复**的 @@ -8716,7 +8726,7 @@ sorted_set类型:在set的存储结构基础上添加可排序字段,类似 当数据比较少时,有序集合使用的是 ziplist 存储的,使用 ziplist 格式存储需要满足以下两个条件: - 有序集合保存的元素个数要小于 128 个; -- 有序集合保存的所有元素成员的长度都必须小于 64 字节 +- 有序集合保存的所有元素大小都小于 64 字节 diff --git a/Issue.md b/Issue.md index d9e278b..1d471a7 100644 --- a/Issue.md +++ b/Issue.md @@ -110,7 +110,7 @@ ## System -### 进程线程 +### 操作系统 * 操作系统? @@ -120,7 +120,15 @@ * 什么是系统调度? - 在用户程序中调用操作系统提供的核心态级别的子功能,结合用户态和核心态区别回答,一般使用陷入(trap),按调用功能分为:设备管理、文件管理、进程控制、进程通信、内存管理, + 在用户程序中调用操作系统提供的核心态级别的子功能,结合用户态和核心态区别回答,一般使用陷入(trap),按调用功能分为:设备管理、文件管理、进程控制、进程通信、内存管理 + + + +*** + + + +### 进程线程 * 进程线程? @@ -167,7 +175,34 @@ * down 和 up 操作需要被设计成原语,不可分割,通常的做法是在执行这些操作的时候屏蔽中断 * 如果信号量的取值只能为 0 或者 1,那么就成为了 互斥量(Mutex) - 管程:Java中的synchronized + 管程:Java 中的 synchronized + +* 进程状态转换: + ![](https://gitee.com/seazean/images/raw/master/Issue/OS-进程状态转换.jpg) + +* 死锁问题: + + 预防死锁: + + * 破坏互斥条件:有些资源必须互斥使用,无法破环互斥条件 + * 破坏不剥夺条件:增加系统开销,降低吞吐量 + * 破坏请求和保持条件:严重浪费系统资源,还可能导致饥饿现象 + * 破坏循环等待条件:浪费系统资源,并造成编程不便 + + 避免死锁: + + * 安全状态:能找到一个分配资源的序列能让所有进程都顺序完成 + * 银行家算法:采用预分配策略检查分配完成时系统是否处在安全状态 + + 检测死锁:利用死锁定理化简资源分配图以检测死锁的存在 + + 解除死锁: + + * 资源剥夺法:挂起某些死锁进程并抢夺它的资源,以便让其他进程继续推进 + * 撤销进程法:强制撤销部分、甚至全部死锁进程并剥夺这些进程的资源 + * 进程回退法:让一个或多个进程回退到足以回避死锁的地步 + + diff --git a/Java.md b/Java.md index d278d31..2f803e0 100644 --- a/Java.md +++ b/Java.md @@ -2268,11 +2268,9 @@ public class Kmp { public UF(int N) { //初始化分组数量 this.count = N; - //初始化eleAndGroup数量 this.eleAndGroup = new int[N]; - - //初始化eleAndGroup中的元素及其所在分组的标识符,eleAndGroup索引作为并查集的每个节点的元素 + //初始化eleAndGroup中的元素及其所在分组的标识符,eleAndGroup索引作为每个节点的元素 //每个索引处的值就是该组的索引,就是该元素所在的组的标识符 for (int i = 0; i < eleAndGroup.length; i++) { eleAndGroup[i] = i; @@ -2309,7 +2307,7 @@ public class Kmp { } } ``` - + * 测试代码: ```java @@ -2358,6 +2356,7 @@ public int findRoot(int p) { while (p != eleAndGroup[p]) { p = eleAndGroup[p]; } + //p == eleGroup[p],说明p是根节点 return p; } @@ -2447,9 +2446,9 @@ public class UF_Tree_Weighted { ##### 应用场景 -并查集存储的每一个整数表示的是一个大型计算机网络中的计算机 +并查集存储的每一个整数表示的是一个大型计算机网络中的计算机: -* 可以通过 connected(int p, int q) 来检测该网络中的某两台计算机之间是否连通, +* 可以通过 connected(int p, int q) 来检测该网络中的某两台计算机之间是否连通 * 可以调用 union(int p,int q) 使得 p 和 q 之间连通,这样两台计算机之间就可以通信 畅通工程:某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府畅通工程的目标是使全省任何两个城镇间都可以实现交通,但不一定有直接的道路相连,只要互相间接通过道路可达即可,问最少还需要建设多少条道路? @@ -6652,6 +6651,11 @@ HashMap继承关系如下图所示: 原因:当数组长度很小,假设是16,那么 n-1即为 1111 ,这样的值和hashCode()直接做按位与操作,实际上只使用了哈希值的后4位。如果当哈希值的高位变化很大,低位变化很小,就很容易造成哈希冲突了,所以这里**把高低位都利用起来,让高16位也参与运算**,从而解决了这个问题 + 哈希冲突的处理方式: + + * 开放定址法:线性探查法(ThreadLocalMap部分详解),平方探查法(i + 1^2、i - 1^2、i + 2^2……)、双重散列(多个哈希函数) + * 链地址法:拉链法 + 2. put @@ -15041,7 +15045,7 @@ Serial GC、Parallel GC、Concurrent Mark Sweep GC这三个GC不同: unused(25+1) + hash(31) + age(4) + lock(3) = 64bit #64位系统 ``` - * Klass Word:类型指针,对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;在64位系统中,开启指针压缩(-XX:+UseCompressedOops)或者JVM堆的最大值小于32G,这个指针也是4byte,否则是8byte + * Klass Word:类型指针,对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;在64位系统中,开启指针压缩 (-XX:+UseCompressedOops) 或者 JVM 堆的最大值小于 32G,这个指针也是 4byte,否则是 8byte ```ruby |-----------------------------------------------------| @@ -15060,10 +15064,12 @@ Serial GC、Parallel GC、Concurrent Mark Sweep GC这三个GC不同: | Mark Word(32bits) | Klass Word(32bits) | array length(32bits) | |-----------------------|-----------------------------|-------------------------| ``` + + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-对象头结构.png) 实例数据:实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来 -对齐填充:Padding 起占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,就是对象的大小必须是8字节的整数倍,而对象头部分正好是8字节的倍数(1倍或者2倍),因此当对象实例数据部分没有对齐时,就需要通过对齐填充来补全 +对齐填充:Padding 起占位符的作用。64位系统,由于 HotSpot VM 的自动内存管理系统要求对象起始地址必须是8字节的整数倍,就是对象的大小必须是8字节的整数倍,而对象头部分正好是8字节的倍数(1倍或者2倍),因此当对象实例数据部分没有对齐时,就需要通过对齐填充来补全 32位系统 @@ -15114,7 +15120,7 @@ Serial GC、Parallel GC、Concurrent Mark Sweep GC这三个GC不同: #### 对象访问 -JVM是通过栈帧中的对象引用访问到其内部的对象实例:(内部结构查看类加载部分) +JVM是通过栈帧中的对象引用访问到其内部的对象实例: * 句柄访问 使用该方式,Java堆中会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息 @@ -18570,16 +18576,24 @@ LocalVariableTable: #### 锁升级 -##### 偏向锁 +##### 升级过程 -###### 优化 - -**synchronized是可重入、不公平的重量级锁**,所以可以对其进行优化 +**synchronized 是可重入、不公平的重量级锁**,所以可以对其进行优化 ```java 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁 //随着竞争的增加,只能锁升级,不能降级 ``` +![](https://gitee.com/seazean/images/raw/master/Java/JUC-锁升级过程.png) + + + +*** + + + +##### 偏向锁 + 偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程之后重新获取该锁不再需要同步操作: * 当锁对象第一次被线程获得的时候进入偏向状态,标记为101,同时使用 CAS 操作将线程 ID 记录到Mark Word。如果 CAS 操作成功,这个线程以后进入这个锁相关的同步块,查看这个线程 ID 是自己的就表示没有竞争,就不需要再进行任何同步操作 @@ -18593,19 +18607,17 @@ LocalVariableTable: * 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的 thread、epoch、age 都为 0 * 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加VM参数 `-XX:BiasedLockingStartupDelay=0` 来禁用延迟 -* 如果禁用了偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,**第一次用到 hashcode 时才会赋值**,添加 VM 参数 `-XX:-UseBiasedLocking` 禁用偏向锁 - - -###### 撤销 + JDK 8 延迟 4s 开启偏向锁原因:在刚开始执行代码时,会有好多线程来抢锁,如果开偏向锁效率反而降低 +* 如果禁用了偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,**第一次用到 hashcode 时才会赋值**,添加 VM 参数 `-XX:-UseBiasedLocking` 禁用偏向锁 撤销偏向锁的状态: -* 调用对象的hashCode:偏向锁的对象 MarkWord 中存储的是线程 id,调用 hashCode导致偏向锁被撤销 +* 调用对象的 hashCode:偏向锁的对象 MarkWord 中存储的是线程 id,调用 hashCode 导致偏向锁被撤销 * 轻量级锁会在锁记录中记录 hashCode * 重量级锁会在 Monitor 中记录 hashCode * 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁 -* 调用wait/notify +* 调用 wait/notify @@ -18659,7 +18671,7 @@ public static void method2() { * 如果CAS失败,有两种情况: * 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程 - * 如果是自己执行了synchronized锁重入,就添加一条 Lock Record 作为重入的计数 + * 如果是自己执行了 synchronized 锁重入,就添加一条 Lock Record 作为重入的计数 ![](https://gitee.com/seazean/images/raw/master/Java/JUC-轻量级锁原理3.png) @@ -18692,10 +18704,14 @@ public static void method2() { + + *** +#### 锁优化 + ##### 自旋锁 **重量级锁竞争**时,尝试获取锁的线程不会立即阻塞,可以使用**自旋**来进行优化,采用循环的方式去尝试获取锁 @@ -18780,8 +18796,6 @@ public class SpinLock { -#### 锁优化 - ##### 锁消除 锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除 @@ -18881,10 +18895,10 @@ class BigRoom { java 死锁产生的四个必要条件: -1. 互斥条件,即当资源被一个线程使用(占有)时,别的线程不能使用 -2. 不剥夺条件,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放 +1. 互斥条件,即当资源被一个线程使用(占有)时,别的线程不能使用 +2. 不可剥夺条件,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放 3. 请求和保持条件,即当资源请求者在请求其他的资源的同时保持对原有资源的占有 -4. 循环等待条件,即存在一个等待循环队列:p1要p2的资源,p2要p1的资源,形成了一个等待环路 +4. 循环等待条件,即存在一个等待循环队列:p1 要 p2 的资源,p2 要 p1 的资源,形成了一个等待环路 四个条件都成立的时候,便形成死锁。死锁情况下打破上述任何一个条件,便可让死锁消失 @@ -18960,7 +18974,7 @@ class HoldLockThread implements Runnable { 定位死锁的方法: -* 使用 jps 定位进程 id,再用 `jstack id`定位死锁,找到死锁的线程去查看源码,解决优化 +* 使用 jps 定位进程 id,再用 `jstack id` 定位死锁,找到死锁的线程去查看源码,解决优化 ```sh "Thread-1" #12 prio=5 os_prio=0 tid=0x000000001eb69000 nid=0xd40 waiting formonitor entry [0x000000001f54f000] @@ -18994,11 +19008,11 @@ class HoldLockThread implements Runnable { at thread.TestDeadLock$$Lambda$1/495053715 ``` -* linux 下可以通过 top 先定位到CPU 占用高的 Java 进程,再利用 `top -Hp 进程id` 来定位是哪个线程,最后再用 jstack 的输出来看各个线程栈, +* linux 下可以通过 top 先定位到 CPU 占用高的 Java 进程,再利用 `top -Hp 进程id` 来定位是哪个线程,最后再用 jstack 的输出来看各个线程栈, * 避免死锁:避免死锁要注意加锁顺序 -* 也可以使用 jconsole 工具,在 `jdk\bin` 目录下 +* 可以使用 jconsole 工具,在 `jdk\bin` 目录下 @@ -20742,7 +20756,7 @@ CAS底层实现是在一个循环中不断地尝试修改目标值,直到修 ##### 分段机制 -分段CAS机制: +分段 CAS 机制: * 在发生竞争时,创建Cell数组用于将不同线程的操作离散(通过hash等算法映射)到不同的节点上 * 设置多个累加单元(会根据需要扩容,最大为CPU核数),Therad-0 累加 Cell[0],而 Thread-1 累加 Cell[1] 等,最后将结果汇总 @@ -21557,12 +21571,14 @@ static class Entry extends WeakReference> { ``` - ThreadLocalMap使用**线性探测法来解决哈希冲突**: + 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 From 40f5950297189ae34609135e860a465450479408 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 3 Jul 2021 22:32:39 +0800 Subject: [PATCH 063/242] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a1baf15..1a12481 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -**Java** 学习笔记,记录着作者从编程入门到深入学习的所有知识,每次学有所获就会更新笔记,所有的知识都有借鉴其他前辈的博客和文章,排版布局美观整洁,如果对各位朋友有所帮助,希望可以给个 star。 +**Java** 学习笔记,记录着作者从编程入门到深入学习的所有知识,每次学有所获就会更新笔记,所有的知识都有借鉴其他前辈的博客和文章,排版布局**美观整洁**,如果对各位朋友有所帮助,希望可以给个 star。 内容说明: @@ -12,7 +12,7 @@ 其他说明: * 所有的知识不保证权威性,如果各位朋友发现错误,非常欢迎与我讨论。 - * Java.md 更新后大于 1M,导致网页无法显示,推荐大家使用 Typora 阅读笔记。 +* 如果使用 Typora 阅读笔记出现卡顿,可以使用 VS Code 或转成 PDF 文件。 个人邮箱:imseazean@gmail.com From 1b1801cc83a907dee376dc8fd549bac3065ada5f Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 4 Jul 2021 18:37:52 +0800 Subject: [PATCH 064/242] Update Java Notes --- DB.md | 147 +- Java.md | 30503 ++++++++++++++++++------------------------------------ Prog.md | 11285 ++++++++++++++++++++ 3 files changed, 21233 insertions(+), 20702 deletions(-) create mode 100644 Prog.md diff --git a/DB.md b/DB.md index 221c5e9..5af1eb9 100644 --- a/DB.md +++ b/DB.md @@ -4081,7 +4081,7 @@ B+Tree 优点:提高查询速度,减少磁盘的 IO 次数,树形结构较 #### 覆盖索引 -覆盖索引:包含所有满足查询需要的数据的索引(SELECT 后面的字段刚好是索引字段),可以利用该索引返回 SELECT 列表的字段,而不必根据索引再次读取数据文件 +覆盖索引:包含所有满足查询需要的数据的索引(SELECT 后面的字段刚好是索引字段),可以利用该索引返回 SELECT 列表的字段,而不必根据索引去聚簇索引上读取数据文件 回表查询:要查找的字段不在非主键索引树上时,需要通过叶子节点的主键值去主键索引上获取对应的行数据 @@ -4120,13 +4120,17 @@ B+Tree 优点:提高查询速度,减少磁盘的 IO 次数,树形结构较 索引下推充分利用了索引中的数据,在查询出整行数据之前过滤掉无效的数据,再去主键索引树上查找 * 不使用索引下推优化时存储引擎通过索引检索到数据返回给 MySQL 服务器,服务器判断数据是否符合条件 + + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-不使用索引下推.png) * 使用索引下推优化时,如果存在某些被索引的列的判断条件时,MySQL 服务器将这一部分**判断条件传递给存储引擎**,然后由存储引擎在索引内部判断索引是否符合传递的条件,只有当索引符合条件时才会将数据检索出来返回给 MySQL 服务器,由此减少 IO次数 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-使用索引下推.png) + 适用条件: -* 需要存储引擎将索引中的数据与条件进行判断,所以优化是基于存储引擎的,只有特定引擎可以使用,适用于InnoDB 和 MyISAM引擎 +* 需要存储引擎将索引中的数据与条件进行判断,所以优化是基于存储引擎的,只有特定引擎可以使用,适用于InnoDB 和 MyISAM 引擎 * 存储引擎没有调用跨存储引擎的能力,跨存储引擎的功能有存储过程、触发器、视图,所以调用这些功能的不可以进行索引下推优化 -* 对于 InnoDB 引擎只适用于二级索引,InnoDB 的聚簇索引会将整行数据读到缓冲区,因为数据已经在内存中了,不再需要去读取了,索引下推的目的减少IO次数也就失去了意义 +* 对于 InnoDB 引擎只适用于二级索引,InnoDB 的聚簇索引会将整行数据读到缓冲区,因为数据已经在内存中了,不再需要去读取了,索引下推的目的减少 IO 次数也就失去了意义 工作过程:用户表 user,(name,sex) 是联合索引 @@ -4141,7 +4145,7 @@ SELECT * FROM user WHERE name LIKE '王%' AND sex=1; -- 头部模糊匹配会 * 优化后:检查索引中存储的列信息是否符合索引条件,然后交由存储引擎用剩余的判断条件判断此行数据是否符合要求,**不满足条件的不去读取表中的数据**,满足下推条件的就根据主键值进行回表查询,2 次回表 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-索引下推优化2.png) -当使用EXPLAIN进行分析时,如果使用了索引条件下推,Extra 会显示 Using index condition +当使用 EXPLAIN 进行分析时,如果使用了索引条件下推,Extra 会显示 Using index condition @@ -4324,12 +4328,14 @@ EXPLAIN SELECT * FROM table_1 WHERE id = 1; MySQL执行计划的局限: +* 只是计划,不是执行 SQL 语句 + * EXPLAIN 不会告诉显示关于触发器、存储过程的信息或用户自定义函数对查询的影响情况 * EXPLAIN 不考虑各种 Cache * EXPLAIN 不能显示 MySQL 在执行查询时所作的优化工作,因为执行计划在执行查询之前生成 * EXPALIN 部分统计信息是估算的,并非精确值 * EXPALIN 只能解释 SELECT 操作,其他操作要重写为 SELECT 后查看执行计划 -* 执行计划 在优化器之后、执行器之前生成,然后执行器调用存储引擎检索数据 +* 执行计划在优化器之后、执行器之前生成,然后执行器调用存储引擎检索数据 * 执行计划可以随着底层优化器输入的更改而更改。EXPLAIN PLAN 显示的是在解释语句时数据库将如何运行 SQL 语句,由于执行环境和 EXPLAIN PLAN 环境的不同,此计划可能与 SQL 语句实际的执行计划不同 环境准备: @@ -4378,9 +4384,9 @@ SQL 执行的顺序的标识,SQL 从大到小的执行 -##### select_type +##### select -表示查询中每个select子句的类型(简单 OR 复杂) +表示查询中每个 select 子句的类型(简单 OR 复杂) | select_type | 含义 | | ------------------ | ------------------------------------------------------------ | @@ -4411,7 +4417,7 @@ SQL 执行的顺序的标识,SQL 从大到小的执行 ##### type -对表的访问方式,表示MySQL在表中找到所需行的方式,又称“访问类型” +对表的访问方式,表示MySQL在表中找到所需行的方式,又称访问类型 | type | 含义 | | ------ | ------------------------------------------------------------ | @@ -4436,18 +4442,18 @@ SQL 执行的顺序的标识,SQL 从大到小的执行 possible_keys: -* 指出MySQL能使用哪个索引在表中找到记录,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用 -* 如果该列是NULL,则没有相关的索引 +* 指出 MySQL 能使用哪个索引在表中找到记录,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用 +* 如果该列是 NULL,则没有相关的索引 key: -* 显示MySQL在查询中实际使用的索引,若没有使用索引,显示为NULL -* 查询中若使用了**覆盖索引**,则该索引仅出现在key列表 +* 显示MySQL在查询中实际使用的索引,若没有使用索引,显示为 NULL +* 查询中若使用了**覆盖索引**,则该索引仅出现在 key 列表,不出现在 possible_keys key_len: * 表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度 -* key_len 显示的值为索引字段的最大可能长度,并非实际使用长度,即key_len是根据表定义计算而得,不是通过表内检索出的 +* key_len 显示的值为索引字段的最大可能长度,并非实际使用长度,即 key_len 是根据表定义计算而得,不是通过表内检索出的 * 在不损失精确性的前提下,长度越短越好 @@ -4462,7 +4468,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:当 Query 中包含 order by 操作,而且无法利用索引完成的排序操作称为文件排序 * Using join buffer:说明在获取连接条件时没有使用索引,并且需要连接缓冲区来存储中间结果 @@ -4483,12 +4489,12 @@ key_len: #### PROFILES -SHOW PROFILES 能够在做SQL优化时帮助了解时间的耗费 +SHOW PROFILES 能够在做 SQL 优化时帮助了解时间的耗费 -* 通过 have_profiling 参数,能够看到当前MySQL是否支持profile: +* 通过 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) @@ -4496,7 +4502,7 @@ SHOW PROFILES 能够在做SQL优化时帮助了解时间的耗费 SET profiling=1; //开启profiling 开关; ``` -* 执行show profiles 指令, 来查看SQL语句执行的耗时: +* 执行 SHOW PROFILES 指令, 来查看 SQL 语句执行的耗时: ```mysql SHOW PROFILES; @@ -4504,7 +4510,7 @@ SHOW PROFILES 能够在做SQL优化时帮助了解时间的耗费 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL- 查看SQL语句执行耗时.png) -* 查看到该SQL执行过程中每个线程的状态和消耗的时间: +* 查看到该 SQL 执行过程中每个线程的状态和消耗的时间: ```mysql SHOW PROFILE FOR QUERY query_id; @@ -4512,16 +4518,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的耗费时间: +* 在获取到最消耗时间的线程状态后,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 + CPU_user:当前用户占有的 CPU + CPU_system:系统占有的 CPU @@ -4531,16 +4537,16 @@ SHOW PROFILES 能够在做SQL优化时帮助了解时间的耗费 #### trace -MySQL 提供了对SQL的跟踪, 通过trace文件能够进一步了解执行过程。 +MySQL 提供了对 SQL 的跟踪, 通过 trace 文件能够进一步了解执行过程。 -* 打开trace,设置格式为 JSON,并设置trace最大能够使用的内存大小,避免解析过程中因为默认内存过小而不能够完整展示 +* 打开 trace,设置格式为 JSON,并设置 trace 最大能够使用的内存大小,避免解析过程中因为默认内存过小而不能够完整展示 ```mysql SET optimizer_trace="enabled=on",end_markers_in_json=ON; SET optimizer_trace_max_mem_size=1000000; ``` -* 执行SQL语句: +* 执行 SQL 语句: ```mysql SELECT * FROM tb_item WHERE id < 4; @@ -4564,7 +4570,7 @@ MySQL 提供了对SQL的跟踪, 通过trace文件能够进一步了解执行 #### 创建索引 -索引是数据库优化最重要的手段之一,通过索引通常可以帮助用户解决大多数的MySQL的性能优化问题 +索引是数据库优化最重要的手段之一,通过索引通常可以帮助用户解决大多数的 MySQL 的性能优化问题 ```mysql CREATE TABLE `tb_seller` ( @@ -4649,7 +4655,7 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name,status,address); * 字符串不加单引号,造成索引失效: - 在查询时,没有对字符串加单引号,MySQL的查询优化器,会自动的进行类型转换,造成索引失效 + 在查询时,没有对字符串加单引号,MySQL 的查询优化器,会自动的进行类型转换,造成索引失效 ```mysql EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status=1; @@ -4676,7 +4682,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 模糊查询,索引失效: 如果是尾部模糊匹配,索引不会失效;如果是头部模糊匹配,索引失效。 @@ -4704,7 +4710,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) @@ -4715,11 +4721,11 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name,status,address); EXPLAIN SELECT * FROM tb_seller WHERE name IS NOT NULL; ``` - NOT NULL 失效的原因是 name 列全部不是null,优化为全表扫描,当 NULL 过多时,IS NULL失效 + NOT NULL 失效的原因是 name 列全部不是 null,优化为全表扫描,当 NULL 过多时,IS NULL 失效 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引15.png) -* IN肯定会走索引,但是当IN的取值范围较大时会导致索引失效,走全表扫描: +* IN 肯定会走索引,但是当 IN 的取值范围较大时会导致索引失效,走全表扫描: ```mysql EXPLAIN SELECT * FROM tb_seller WHERE sellerId IN ('alibaba','huawei');-- 都走索引 @@ -4744,7 +4750,7 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name,status,address); -* 以%开头的Like模糊查询,索引失效,比如语句:`WHERE a LIKE '%d'`,前面的不确定,导致不符合最左匹配,直接去索引中搜索以 d 结尾的节点,所以没有顺序 +* 以 % 开头的 LIKE 模糊查询,索引失效,比如语句:`WHERE a LIKE '%d'`,前面的不确定,导致不符合最左匹配,直接去索引中搜索以 d 结尾的节点,所以没有顺序 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-索引失效底层原理3.png) @@ -4786,7 +4792,7 @@ SHOW GLOBAL STATUS LIKE 'Handler_read%'; 复合索引叶子节点不仅保存了复合索引的值,还有主键索引,所以使用覆盖索引的时候,加上主键也会用到索引 -尽量使用覆盖索引,避免select *: +尽量使用覆盖索引,避免 SELECT *: ```mysql EXPLAIN SELECT name,status,address FROM tb_seller WHERE name='小米科技' AND status='1' AND address='西安市'; @@ -4812,7 +4818,7 @@ EXPLAIN SELECT name,status,address,password FROM tb_seller WHERE name='小米科 #### 批量插入 -当使用load 命令导入数据的时候,适当的设置可以提高导入的效率: +当使用 load 命令导入数据的时候,适当的设置可以提高导入的效率: ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL load data.png) @@ -4838,7 +4844,7 @@ LOAD DATA LOCAL INFILE = '/home/seazean/sql1.log' INTO TABLE `tb_user_1` FIELD T 3. 手动提交事务:如果应用使用自动提交的方式,建议在导入前执行`SET AUTOCOMMIT=0`,关闭自动提交;导入结束后再执行 SET AUTOCOMMIT=1,打开自动提交,可以提高导入的效率。 - 事务需要控制大小,事务太大可能会影响执行的效率。MySQL有 innodb_log_buffer_size 配置项,超过这个值的日志会写入磁盘数据,效率会下降。所以在事务大小达到配置项数据级前进行事务提交可以提高效率 + 事务需要控制大小,事务太大可能会影响执行的效率。MySQL 有 innodb_log_buffer_size 配置项,超过这个值的日志会写入磁盘数据,效率会下降。所以在事务大小达到配置项数据级前进行事务提交可以提高效率 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL插入数据手动提交事务.png) @@ -4850,7 +4856,7 @@ LOAD DATA LOCAL INFILE = '/home/seazean/sql1.log' INTO TABLE `tb_user_1` FIELD T #### INSERT -当进行数据的insert操作的时候,可以考虑采用以下几种优化方案: +当进行数据的 INSERT 操作的时候,可以考虑采用以下几种优化方案: * 如果需要同时对一张表插入很多行数据时,优化为一条插入语句,这种方式将大大的缩减客户端与数据库之间的连接、关闭等消耗 @@ -4866,9 +4872,9 @@ LOAD DATA LOCAL INFILE = '/home/seazean/sql1.log' INTO TABLE `tb_user_1` FIELD T ```mysql start transaction; - 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'); + INSERT INTO tb_test VALUES(2,'Cat'); + INSERT INTO tb_test VALUES(3,'Jerry'); commit; -- 手动提交,分段提交 ``` @@ -4928,11 +4934,11 @@ 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 的排序操作。 -对于Filesort , MySQL 有两种排序算法: +对于 Filesort , MySQL 有两种排序算法: * 两次扫描算法:MySQL4.1 之前,使用该方式排序。首先根据条件取出排序字段和行指针信息,然后在排序区 sort buffer 中排序,如果 sort buffer 不够,则在临时表 temporary table 中存储排序结果。完成排序后,再根据行指针回表读取记录,该操作可能会导致大量随机I/O操作 * 一次扫描算法:一次性取出满足条件的所有字段,然后在排序区 sort buffer 中排序后直接输出结果集。排序时内存开销较大,但是排序效率比两次扫描算法高 @@ -4967,6 +4973,8 @@ GROUP BY 也会进行排序操作,与 ORDER BY 相比,GROUP BY 主要只是 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL GROUP BY排序1.png) + Using temporary:表示 MySQL 需要使用临时表来存储结果集,常见于排序和分组查询 + * 查询包含 GROUP BY 但是用户想要避免排序结果的消耗, 则可以执行 ORDER BY NULL 禁止排序: ```mysql @@ -4991,10 +4999,10 @@ GROUP BY 也会进行排序操作,与 ORDER BY 相比,GROUP BY 主要只是 #### 嵌套查询 -MySQL 4.1版本之后,开始支持SQL的子查询 +MySQL 4.1版本之后,开始支持 SQL 的子查询 -* 可以使用SELECT语句来创建一个单列的查询结果,然后把结果作为过滤条件用在另一个查询中 -* 使用子查询可以一次性的完成逻辑上需要多个步骤才能完成的SQL操作,同时也可以避免事务或者表锁死 +* 可以使用 SELECT 语句来创建一个单列的查询结果,然后把结果作为过滤条件用在另一个查询中 +* 使用子查询可以一次性的完成逻辑上需要多个步骤才能完成的 SQL 操作,同时也可以避免事务或者表锁死 * 在有些情况下,子查询是可以被更高效的连接(JOIN)替代 例如查找有角色的所有的用户信息: @@ -5025,7 +5033,7 @@ MySQL 4.1版本之后,开始支持SQL的子查询 #### OR -对于包含OR的查询子句,如果要利用索引,则 OR 之间的每个条件列都必须用到索引,而且不能使用到复合索引,如果没有索引,则应该考虑增加索引 +对于包含 OR 的查询子句,如果要利用索引,则 OR 之间的每个条件列都必须用到索引,而且不能使用到复合索引,如果没有索引,则应该考虑增加索引 * 执行查询语句: @@ -5040,7 +5048,7 @@ MySQL 4.1版本之后,开始支持SQL的子查询 ``` * 使用 UNION 替换 OR,求并集: - 注意:该优化只针对多个索引列有效,如果有column没有被索引,查询效率可能会因为没有选择OR而降低 + 注意:该优化只针对多个索引列有效,如果有 column 没有被索引,查询效率可能会因为没有选择 OR 而降低 ```mysql EXPLAIN SELECT * FROM emp WHERE id = 1 UNION SELECT * FROM emp WHERE age = 30; @@ -5051,7 +5059,7 @@ MySQL 4.1版本之后,开始支持SQL的子查询 * UNION 要优于 OR 的原因: * UNION 语句的 type 值为 ref,OR 语句的 type 值为 range - * UNION 语句的 ref 值为 const,OR 语句的 type 值为 null,const 表示是常量值引用,非常快 + * UNION 语句的 ref 值为 const,OR 语句的 ref 值为 null,const 表示是常量值引用,非常快 @@ -5099,7 +5107,7 @@ MySQL 4.1版本之后,开始支持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); @@ -5108,7 +5116,7 @@ SQL提示,是优化数据库的一个重要手段,就是在SQL语句中加 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用提示1.png) -* IGNORE INDEX:让MySQL忽略一个或者多个索引,则可以使用 IGNORE INDEX 作为提示 +* IGNORE INDEX:让 MySQL 忽略一个或者多个索引,则可以使用 IGNORE INDEX 作为提示 ```mysql EXPLAIN SELECT * FROM tb_seller IGNORE INDEX(idx_seller_name) WHERE name = '小米科技'; @@ -5116,7 +5124,7 @@ SQL提示,是优化数据库的一个重要手段,就是在SQL语句中加 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用提示2.png) -* FORCE INDEX:为强制MySQL使用一个特定的索引,可在查询中使用 FORCE INDEX 作为提示 +* FORCE INDEX:为强制 MySQL 使用一个特定的索引,可在查询中使用 FORCE INDEX 作为提示 ```mysql EXPLAIN SELECT * FROM tb_seller FORCE INDEX(idx_seller_name_sta_addr) WHERE NAME='小米科技'; @@ -5869,6 +5877,8 @@ MyISAM 的读写锁调度是写优先,因为写锁后其他线程不能做任 #### 行级锁 +##### 介绍锁 + InnoDB 与 MyISAM 的最大不同有两点:一是支持事务;二是采用了行级锁,InnoDB同时支持表锁和行锁 InnoDB 实现了以下两种类型的行锁: @@ -5898,7 +5908,7 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 -#### 锁操作 +##### 锁操作 两个客户端操作 Client 1和 Client 2,简化为 C1、C2 @@ -6028,9 +6038,14 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 #### 间隙锁 -当使用范围条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据进行加锁,对于键值在条件范围内但并不存在的记录,叫做间隙 (GAP) , InnoDB会对间隙进行加锁,这种锁机制就是间隙锁 (Next-Key锁) +当使用范围条件检索数据,并请求共享或排他锁时,InnoDB 会给符合条件的已有数据进行加锁,对于键值在条件范围内但并不存在的记录,叫做间隙(GAP), InnoDB 会对间隙进行加锁,就是间隙锁 + +* next-key lock 是行锁和这条记录前面的 gap lock 的组合,就是行锁加间隙锁 +* 唯一索引加锁只有在值存在时才是行锁,值不存在会变成间隙锁,所以范围查询时容易出现间隙锁 +* 对于联合索引且是唯一索引,如果 where 条件只包括联合索引的一部分,那么会加间隙锁 +* 加锁的基本单位是 next-key lock,前开后闭原则,假设有索引值 10、11、13,那么可能的间隙锁包括:(负无穷,10]、(10,11]、(11,13]、(13,20,正无穷),锁住索引 11 会同时对间隙 (10,11]、(11,13] 加锁 -间隙锁可以解决事务中的幻读问题,通过对间隙加锁,可以防止读取过程中数据条目发生变化 +在 RR 级别下,间隙锁可以解决事务中的**幻读问题**,通过对间隙加锁,可以防止读取过程中数据条目发生变化 * 关闭自动提交功能: @@ -6063,6 +6078,28 @@ 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,那么就可以同时插入 + + + +**** + + + #### 锁状态 ```mysql diff --git a/Java.md b/Java.md index 2f803e0..01ac9de 100644 --- a/Java.md +++ b/Java.md @@ -27,15 +27,15 @@ ##### 基本类型 -Java语言提供了八种基本类型。六种数字类型(四个整数型,两个浮点型),一种字符类型,还有一种布尔型。 +Java语言提供了八种基本类型。六种数字类型(四个整数型,两个浮点型),一种字符类型,还有一种布尔型 **byte:** -- byte 数据类型是8位、有符号的,以**二进制补码**表示的整数,**8位一个字节**,首位是符号位 +- byte 数据类型是 8 位、有符号的,以**二进制补码**表示的整数,**8 位一个字节**,首位是符号位 - 最小值是 **-128(-2^7)** - 最大值是 **127(2^7-1)** - 默认值是 **`0`** -- byte 类型用在大型数组中节约空间,主要代替整数,因为 byte 变量占用的空间只有 int 类型的四分之一 +- byte 类型用在大型数组中节约空间,主要代替整数,byte 变量占用的空间只有 int 类型的四分之一 - 例子:`byte a = 100,byte b = -50` **short:** @@ -68,7 +68,7 @@ Java语言提供了八种基本类型。六种数字类型(四个整数型, **float:** -- float 数据类型是单精度、32位、符合IEEE 754标准的浮点数 +- float 数据类型是单精度、32 位、符合 IEEE 754 标准的浮点数 - float 在储存大型浮点数组的时候可节省内存空间 - 默认值是 **`0.0f`** - 浮点数不能用来表示精确的值,如货币 @@ -76,9 +76,9 @@ Java语言提供了八种基本类型。六种数字类型(四个整数型, **double:** -- double 数据类型是双精度、64 位、符合IEEE 754标准的浮点数 -- 浮点数的默认类型为double类型 -- double类型同样不能表示精确的值,如货币 +- double 数据类型是双精度、64 位、符合 IEEE 754 标准的浮点数 +- 浮点数的默认类型为 double 类型 +- double 类型同样不能表示精确的值,如货币 - 默认值是 **`0.0d`** - 例子:`double d1 = 123.4` @@ -86,15 +86,15 @@ Java语言提供了八种基本类型。六种数字类型(四个整数型, - boolean 数据类型表示一位的信息 - 只有两个取值:true 和 false -- JVM 规范指出 boolean 当做 int 处理,boolean 数组当做 byte 数组处理,这样可以得出 boolean 类型单独使用占了4个字节,在数组中是1个字节 +- JVM 规范指出 boolean 当做 int 处理,boolean 数组当做 byte 数组处理,这样可以得出 boolean 类型单独使用占了 4 个字节,在数组中是 1 个字节 - 默认值是 **`false`** - 例子:`boolean one = true` **char:** - char 类型是一个单一的 16 位两个字节的 Unicode 字符 -- 最小值是 **`\u0000`**(即为0) -- 最大值是 **`\uffff`**(即为65,535) +- 最小值是 **`\u0000`**(即为 0) +- 最大值是 **`\uffff`**(即为 65535) - char 数据类型可以储存任何字符 - 例子:`char c = 'A';` `char c = '张';` @@ -240,14 +240,11 @@ public class PackegeClass { int a = 12 ; Integer a1 = 12 ; // 自动装箱 Integer a2 = a ; // 自动装箱 + Integer a3 = null; // 引用数据类型的默认值可以为null Integer c = 100 ; int c1 = c ; // 自动拆箱 - int d = 12; - Integer d1 = null; // 引用数据类型的默认值可以为null - Integer d2 = 0; - Integer it = Integer.valueOf(12); // 手工装箱! // Integer it1 = new Integer(12); // 手工装箱! Integer it2 = 12; @@ -270,7 +267,7 @@ new Integer(123) 与 Integer.valueOf(123) 的区别在于: - new Integer(123):每次都会新建一个对象 -- Integer.valueOf(123):会使用缓存池中的对象,多次调用取得同一个对象的引用,反编译后底层调用该方法自动装箱 +- Integer.valueOf(123):会使用缓存池中的对象,多次调用取得同一个对象的引用 ```java Integer x = new Integer(123); @@ -305,6 +302,18 @@ 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); +} +``` + + + *** @@ -739,7 +748,9 @@ public static 返回值类型 方法名(参数) { 重载仅对应方法的定义,与方法的调用无关,调用方式参照标准格式 -重载仅针对**同一个类**中方法的名称与参数进行识别,**与返回值无关**,不能通过返回值来判定两个方法是否相互构成重载 +重载仅针对**同一个类**中方法的名称与参数进行识别,**与返回值无关**,不能通过返回值来判定两个方法是否构成重载 + +原理:JVM → 运行机制 → 字节码 → 方法表 ```java public class MethodDemo { @@ -952,32 +963,25 @@ Debug是供程序员使用的程序调试工具,它可以用于查看程序的 -**** - - - - - -## 算法 +*** -### 递归 -#### 概述 -算法:解题方案的准确而完整的描述,是一系列解决问题的清晰指令,代表着用系统的方法解决问题的策略机制 -递归:程序调用自身的编程技巧 -递归: +## 对象 -* 直接递归:自己的方法调用自己 -* 间接递归:自己的方法调用别的方法,别的方法又调用自己 +### 概述 -递归如果控制的不恰当,会形成递归的死循环,从而导致栈内存溢出错误 +**Java是一种面向对象的高级编程语言。** +**三大特征:封装,继承,多态** +面向对象最重要的两个概念:类和对象。 -推荐阅读:https://time.geekbang.org/column/article/41440 +* 类:相同事物共同特征的描述。类只是学术上的一个概念并非真实存在的,只能描述一类事物。 +* 对象:是真实存在的实例, 实例==对象。**对象是类的实例化**! +* 结论:有了类和对象就可以描述万千世界所有的事物。 必须先有类才能有对象。 @@ -985,412 +989,312 @@ Debug是供程序员使用的程序调试工具,它可以用于查看程序的 -#### 算法 - -##### 核心思想 +### 类 -递归的三要素(理论): +#### 定义 -1. 递归的终结点 -2. 递归的公式 -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.递归的方向:必须走向终结点 -public static int f(int x){ - if(x == 1){ - return 1; - }else{ - return f(x-1) + 1; - } +修饰符 class 类名{ } ``` - - -*** - - - -##### 公式转换 +1. 类名的首字母建议大写,满足驼峰模式,比如 StudentNameCode +2. 一个 Java 代码中可以定义多个类,按照规范一个 Java 文件一个类 +3. 一个 Java 代码文件中,只能有一个类是 public 修饰,**public修饰的类名必须成为当前Java代码的文件名称** ```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 -//(1)递归的公式: f(n) = f(n-1)- 2 ; -//(2)递归的终结点: f(1) = 1 -//(3)递归的方向:必须走向终结点。 -public static int f(int n){ - if(n == 1){ - return 1; - }else{ - return f(n-1) - 2; - } +类中的成分:有且仅有五大成分 +修饰符 class 类名{ + 1.成员变量(Field): 描述类或者对象的属性信息的。 + 2.成员方法(Method): 描述类或者对象的行为信息的。 + 3.构造器(Constructor): 初始化一个对象返回。 + 4.代码块 + 5.内部类 + } +类中有且仅有这五种成分,否则代码报错! +public class ClassDemo { + System.out.println(1);//报错 } ``` -##### 注意事项 - -以上理论只能针对于**规律化递归**,如果是非规律化是不能套用以上公式的! -非规律化递归的问题:文件搜索,啤酒问题。 - - - *** -#### 案例 - -##### 猴子吃桃 +#### 构造器 -猴子第一天摘了若干个桃子,当即吃了一半,觉得好不过瘾,然后又多吃了一个。第二天又吃了前一天剩下的一半,觉得好不过瘾,然后又多吃了一个。以后每天都是如此。等到第十天再吃的时候发现只有1个桃子,问猴子第一天总共摘了多少个桃子? +构造器格式: ```java -/* -(1)公式: f(x+1)=f(x)-f(x)/2-1; ==> 2f(x+1) = f(x) - 2 ==> f(x)=2f(x+1)+2 -(2)终结点:f(10) = 1 -(3)递归的方向:走向了终结点 -*/ +修饰符 类名(形参列表){ -public static int f(int x){ - if(x == 10){ - return 1; - } else { - return 2*f(x+1)+2 - } } ``` +作用:初始化类的一个对象返回 +分类:无参数构造器,有参数构造器 -*** +注意:**一个类默认自带一个无参数构造器**,写了有参数构造器默认的无参数构造器就消失,还需要用无参数构造器就要重新写 +构造器初始化对象的格式:类名 对象名称 = new 构造器 +* 无参数构造器的作用:初始化一个类的对象(使用对象的默认值初始化)返回 +* 有参数构造器的作用:初始化一个类的对象(可以在初始化对象的时候为对象赋值)返回 -##### 递归求和 -```java -//(1)递归的终点接:f(1) = 1 -//(2)递归的公式: f(n) = f(n-1) + n -//(3)递归的方向必须走向终结点: -public static int f(int n){ - if(n == 1 ) return 1; - return f(n-1) + n; -} -``` +------ -**** +### 包 +包:分门别类的管理各种不同的技术,便于管理技术,扩展技术,阅读技术。 -##### 汉诺塔 +定义包的格式:`package 包名; `,必须放在类名的最上面。 -```java -public class Hanoi { - public static void main(String[] args) { - hanoi('X', 'Y', 'Z', 3); - } +导包格式:`import 包名.类名;` - //将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 - System.out.println(x + "→" + z); //然后将x最下面的块(最大的)移动到z - hanoi(y, x, z, n - 1); //最后将n-1个块从y移动到z,x为辅助柱 - } - } -} -``` +相同包下的类可以直接访问;不同包下的类必须导包才可以使用 -时间复杂度 O(2^n) +*** -**** +### 封装 -##### 啤酒问题 +封装的哲学思维:合理隐藏,合理暴露 +封装最初的目的:提高代码的安全性和复用性,组件化 -非规律化递归问题。 +封装的步骤: -啤酒2元一瓶,4个盖子可以换一瓶,2个空瓶可以换一瓶。 +1. **成员变量应该私有,用private修饰,只能在本类中直接访问** +2. **提供成套的getter和setter方法暴露成员变量的取值和赋值** -```java -public class BeerDemo{ - // 定义一个静态变量存储可以喝酒的总数 - public static int totalNum; - public static int lastBottleNum; - public static int lastCoverNum; - public static void main(String[] args) { - buyBeer(10); - System.out.println("总数:"+totalNum); - System.out.println("剩余盖子:"+ lastCoverNum); - System.out.println("剩余瓶子:"+ lastBottleNum); - } - public static void buyBeer(int money){ - int number = money / 2; - totalNum += number; - // 算出当前剩余的全部盖子和瓶子数,换算成金额继续购买。 - int currentBottleNum = lastBottleNum + number ; - int currentCoverNum = lastCoverNum + number ; - // 把他们换算成金额 - int totalMoney = 0 ; - totalMoney += (currentBottleNum/2)*2;//除2代表可以换几个瓶子,乘2代表换算成钱,秒! - lastBottleNum = currentBottleNum % 2 ;//取余//算出剩余的瓶子 - - totalMoney += (currentCoverNum / 4) * 2; - lastCoverNum = currentCoverNum % 4 ; +为什么使用private修饰成员变量:实现数据封装,不想让别人使用修改你的数据,比较安全 - // 继续拿钱买酒 - if(totalMoney >= 2){ - buyBeer(totalMoney); - } - } -} -``` +*** -*** +### this +this关键字的作用: +* this关键字代表了当前对象的引用 +* this出现在方法中:**哪个对象调用这个方法this就代表谁** +* this可以出现在构造器中:代表构造器正在初始化的那个对象 +* this可以区分变量是访问的成员变量还是局部变量 -### 排序 -#### 冒泡排序 -冒泡排序(Bubble Sort):两个数比较大小,较大的数下沉,较小的数冒起来 +------ -算法描述:每次从数组的第一个位置开始两两比较,把较大的元素与较小的元素进行层层交换,最终把当前最大的一个元素存入到数组当前的末尾 -实现思路: -1. 确定总共需要冒几轮:数组的长度-1 -2. 每轮两两比较几次 +### static - +#### 基本介绍 -```java -// 0 1位置比较,大的放后面,然后1 2位置比较,大的继续放后面,一轮循环最后一位是最大值 -public class BubbleSort { - public static void main(String[] args) { - int[] arr = {55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88}; - int flag;//标记本趟排序是否发生了交换 - //比较i和i+1,不需要再比最后一个位置 - for (int i = 0; i < arr.length - 1; i++) { - flag = 0; - //最后i位不需要比,已经排序好 - for (int j = 0; j < arr.length - 1 - i; j++) { - if (arr[j] > arr[j + 1]) { - int temp = arr[j]; - arr[j] = arr[j + 1]; - arr[j + 1] = temp; - flag = 1;//发生了交换 - } - } - //没有发生交换,证明已经有序,不需要继续排序 - if(flag == 0) { - break; - } - } - System.out.println(Arrays.toString(arr)); - } -} -``` +Java是通过成员变量是否有static修饰来区分是类的还是属于对象的。 -冒泡排序时间复杂度:最坏情况 +static 静态修饰的成员(方法和成员变量)属于类本身的。 -* 元素比较的次数为:`(N-1)+(N-2)+(N-3)+...+2+1=((N-1)+1)*(N-1)/2=N^2/2-N/2` -* 元素交换的次数为:`(N-1)+(N-2)+(N-3)+...+2+1=((N-1)+1)*(N-1)/2=N^2/2-N/2` -* 总执行次数为:`(N^2/2-N/2)+(N^2/2-N/2)=N^2-N` +按照有无static修饰,成员变量和方法可以分为: -按照大 O 推导法则,保留函数中的最高阶项那么最终冒泡排序的时间复杂度为 O(N^2) +* 成员变量: + * 静态成员变量(类变量): + 有static修饰的成员变量称为静态成员变量也叫类变量,属于类本身,**与类一起加载一次,只有一个**,直接用类名访问即可。 + * 实例成员变量: + 无static修饰的成员变量称为实例成员变量,属于类的每个对象的。**与类的对象一起加载**,对象有多少个,实例成员变量就加载多少个,必须用类的对象来访问。 +* 成员方法: + * 静态方法: + 有static修饰的成员方法称为静态方法也叫类方法,属于类本身的,直接用类名访问即可。 + * 实例方法: + 无static修饰的成员方法称为实例方法,属于类的每个对象的,必须用类的对象来访问。 -*** +**** -#### 选择排序 -##### 简单选择 +#### static用法 -选择排序(Selection-sort):一种简单直观的排序算法 +成员变量的访问语法: -算法描述:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕 +* 静态成员变量:只有一份可以被类和类的对象**共享访问** + * 类名.静态成员变量(同一个类中访问静态成员变量可以省略类名不写) + * 对象.静态成员变量(不推荐) -实现思路: +* 实例成员变量: + * 对象.实例成员变量(先创建对象) -1. 控制选择几轮:数组的长度-1 -2. 控制每轮从当前位置开始比较几次 +成员方法的访问语法: - +* 静态方法:有static修饰,属于类 -```java -// 0 1位置比较,小的放0位置,然后0 2位置比,小的继续放0位置,一轮循环0位置是最小值 -public class SelectSort { - public static void main(String[] args) { - int[] arr = {55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88}; - for (int i = 0; i < arr.length - 1; i++) { - //获取最小索引位置 - int minIndex = i; - for (int j = i + 1; j < arr.length; j++) { - if (arr[minIndex] > arr[j]) { - minIndex = j; - } - } - //交换元素 - int temp = arr[i]; - arr[i] = arr[minIndex]; - arr[minIndex] = temp; - } - System.out.println(Arrays.toString(arr)); - } -} -``` + * 类名.静态方法(同一个类中访问静态成员可以省略类名不写) + * 对象.静态方法(不推荐,参考 JVM类加载--> 字节码 --> 方法调用) + +* 实例方法:无static修饰,属于对象 -选择排序时间复杂度: + * 对象.实例方法 + + ```java + public class Student { + // 1.静态方法:有static修饰,属于类,直接用类名访问即可! + public static void inAddr(){ } + // 2.实例方法:无static修饰,属于对象,必须用对象访问! + public void eat(){} + + public static void main(String[] args) { + // a.类名.静态方法 + Student.inAddr(); + inAddr(); + // b.对象.实例方法 + // Student.eat(); // 报错了! + Student zbj = new Student(); + zbj.eat(); + } + } + ``` -* 数据比较次数:`(N-1)+(N-2)+(N-3)+...+2+1=((N-1)+1)*(N-1)/2=N^2/2-N/2` -* 数据交换次数:`N-1` -* 时间复杂度:`N^2/2-N/2+(N-1)=N^2/2+N/2-1` -根据大 O 推导法则,保留最高阶项,去除常数因子,时间复杂度为 O(N^2) +*** -*** +#### 两个问题 +内存问题: -##### 堆排序 +* **栈内存存放main方法和地址** -堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法,堆结构是一个近似完全二叉树的结构,并同时满足子结点的键值或索引总是小于(或者大于)父节点 +* **堆内存存放对象和变量** -优先队列:堆排序每次上浮过程都会将最大或者最小值放在堆顶,应用于优先队列可以将优先级最高的元素浮到堆顶 +* **方法区存放class和静态变量(jdk8以后移入堆)** -实现思路: +访问问题: -1. 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,并通过上浮对堆进行调整,此堆为初始的无序区,堆顶为最大数 +​ a.实例方法是否可以直接访问实例成员变量?可以的,因为它们都属于对象。 +​ b.实例方法是否可以直接访问静态成员变量?可以的,静态成员变量可以被共享访问。 +​ c.实例方法是否可以直接访问实例方法? 可以的,实例方法和实例方法都属于对象。 +​ d.实例方法是否可以直接访问静态方法?可以的,静态方法可以被共享访问! -2. 将堆顶元素 R[1] 与最后一个元素 R[n] 交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区 Rn,且满足 R[1,2…n-1]<=R[n] +​ a.静态方法是否可以直接访问实例变量? 不可以的,实例变量必须用对象访问!! +​ b.静态方法是否可以直接访问静态变量? 可以的,静态成员变量可以被共享访问。 +​ c.静态方法是否可以直接访问实例方法? 不可以的,实例方法必须用对象访问!! +​ d.静态方法是否可以直接访问静态方法?可以的,静态方法可以被共享访问!! -3. 交换后新的堆顶 R[1] 可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将 R[1] 与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn),不断重复此过程直到有序区的元素个数为 n-1,则整个排序过程完成 - -floor:向下取整 +------ -```java -public class HeapSort { - public static void main(String[] args) { - int[] arr = {55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88}; - heapSort(arr, arr.length); - System.out.println(Arrays.toString(arr)); - } - //len为数组长度 - private static void heapSort(int[] arr, int len) { - //建堆,逆排序,因为堆排序定义的交换顺序是从当前结点往下交换,逆序排可以避免多余的交换 - for (int i = len / 2 - 1; i >= 0; i--) { - //调整函数 - sift(arr, i, len - 1); - } - //从尾索引开始排序 - for (int i = len - 1; i > 0; i--) { - //将最大的节点放入末尾 - int temp = arr[0]; - arr[0] = arr[i]; - arr[i] = temp; - //继续寻找最大的节点 - sift(arr, 0, i - 1); - } - } - //调整函数,调整arr[low]的元素,从索引low到high的范围调整 - private static void sift(int[] arr, int low, int high) { - //暂存调整元素 - int temp = arr[low]; - int i = low, j = low * 2 + 1;//j是左节点 - while (j <= high) { - //判断是否有右孩子,并且比较左右孩子中较大的节点 - if (j < high && arr[j] < arr[j + 1]) { - j++; //指向右孩子 - } - if (temp < arr[j]) { - arr[i] = arr[j]; - i = j; //继续向下调整 - j = 2 * i + 1; - } else { - //temp > arr[j],说明也大于j的孩子,探测结束 - break; - } - } - //将被调整的节点放入最终的位置 - arr[i] = temp; - } -} -``` +### 继承 -堆排序的时间复杂度是 O(nlogn) +#### 基本介绍 +继承是Java中一般到特殊的关系,是一种子类到父类的关系。 +* 被继承的类称为:父类/超类。 +* 继承父类的类称为:子类。 -*** +继承的作用: +* **提高代码的复用**,相同代码可以定义在父类中 +* 子类继承父类,可以直接使用父类这些代码(相同代码重复利用) +* 子类得到父类的属性(成员变量)和行为(方法),还可以定义自己的功能,子类更强大 +继承的特点: -#### 插入排序 +1. 子类的全部构造器默认先访问父类的无参数构造器,再执行自己的构造器 +2. **单继承**:一个类只能继承一个直接父类 +3. 多层继承:一个类可以间接继承多个父类(家谱) +4. 一个类可以有多个子类 +5. 一个类要么默认继承了Object类,要么间接继承了Object类,Object类是Java中的祖宗类 -##### 直接插入 +继承的格式: -插入排序(Insertion Sort):在要排序的一组数中,假定前 n-1 个数已经排好序,现在将第 n 个数插到这个有序数列中,使得这 n 个数也是排好顺序的,如此反复循环,直到全部排好顺序 +```java +子类 extends 父类{ - +} +``` + +子类继承父类的东西: + +* 子类不能继承父类的构造器,子类有自己的构造器 +* 子类是不能可以继承父类的私有成员的,可以反射暴力去访问继承自父类的私有成员 +* 子类是不能继承父类的静态成员的,子类只是可以访问父类的静态成员,父类静态成员只有一份可以被子类共享访问,**共享并非继承** ```java -public class InsertSort { +public class ExtendsDemo { public static void main(String[] args) { - 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处的值大,则交换数据, - //如果不大,那么就找到合适的位置了,退出循环即可; - if (arr[j - 1] > arr[j]) { - int temp = arr[j]; - arr[j] = arr[j - 1]; - arr[j - 1] = temp; - } - } - } - System.out.println(Arrays.toString(arr)); + Cat c = new Cat(); + // c.run(); + Cat.test(); + System.out.println(Cat.schoolName); } } +class Cat extends Animal{ +} +class Animal{ + public static String schoolName ="seazean"; + public static void test(){} + private void run(){} +} ``` -插入排序时间复杂度: -* 比较的次数为:`(N-1)+(N-2)+(N-3)+...+2+1=((N-1)+1)*(N-1)/2=N^2/2-N/2` -* 交换的次数为:`(N-1)+(N-2)+(N-3)+...+2+1=((N-1)+1)(N-1)/2=N^2/2-N/2` -* 总执行次数为:`(N^2/2-N/2)+(N^2/2-N/2)=N^2-N` -按照大 O 推导法则,保留函数中的最高阶项那么最终插入排序的时间复杂度为 O(N^2) +*** + + + +#### 继承访问 + +继承后成员变量的访问特点:**就近原则**,子类有找子类,子类没有找父类,父类没有就报错! + +如果要申明访问父类的成员变量可以使用:super.父类成员变量。super指父类引用 + +```java +public class ExtendsDemo { + public static void wmain(String[] args) { + Wolf w = new Wolf();w + w.showName(); + } +} +class Wolf extends Animal{ + private String name = "子类狼"; + public void showName(){ + String name = "局部名称"; + System.out.println(name); // 局部name + System.out.println(this.name); // 子类对象的name + System.out.println(super.name); // 父类的 + System.out.println(name1); // 父类的 + //System.out.println(name2); // 报错。子类父类都没有 + } +} + +class Animal{ + public String name = "父类动物名称"; + public String name1 = "父类"; +} +``` @@ -1398,334 +1302,361 @@ public class InsertSort { -##### 希尔排序 +#### 方法重写 -希尔排序(Shell Sort):也是一种插入排序,也称为缩小增量排序 +方法重写:子类继承了父类就得到了父类的方法,子类重写一个与父类申明一样的方法来**覆盖**父类的该方法 -实现思路: +方法重写的校验注解:@Override -1. 选定一个增长量h,按照增长量h作为数据分组的依据,对数据进行分组 -2. 对分好组的每一组数据完成插入排序 -3. 减小增长量,最小减为1,重复第二步操作 +* 方法加了这个注解,那就必须是成功重写父类的方法,否则报错 +* @Override优势:可读性好,安全,优雅 - +子类可以扩展父类的功能,但不能改变父类原有的功能,重写有以下**三个限制**: -希尔排序的核心在于间隔序列的设定,既可以提前设定好间隔序列,也可以动态的定义间隔序列,希尔排序就是插入排序增加了间隔 +- 子类方法的访问权限必须大于等于父类方法 +- 子类方法的返回类型必须是父类方法返回类型或为其子类型 +- 子类方法抛出的异常类型必须是父类抛出异常类型或为其子类型 + +继承中的隐藏问题: + +- 子类和父类方法都是静态的,那么子类中的方法会隐藏父类中的方法 +- 在子类中可以定义和父类成员变量同名的成员变量,此时子类的成员变量隐藏了父类的成员变量,在创建对象为对象分配内存的过程中,**隐藏变量依然会被分配内存** ```java -public class ShellSort { +public class ExtendsDemo { public static void main(String[] args) { - int[] arr = {55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88}; - //1. 确定增长量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] - if (arr[j] < arr[j - h]) { - int temp = arr[j]; - arr[j] = arr[j - h]; - arr[j - h] = temp; - } - } - } - //3. 减小h的值,减小规则为: - h = h / 2; - } - System.out.println(Arrays.toString(arr)); + Wolf w = new Wolf(); + w.run(); } } +class Wolf extends Animal{ + @Override + public void run(){}// +} +class Animal{ + public void run(){} +} ``` -在希尔排序中,增长量h并没有固定的规则,有很多论文研究了各种不同的递增序列,但都无法证明某个序列是最好的,所以对于希尔排序的时间复杂度分析就认为 O(nlogn) - *** -#### 归并排序 - -##### 实现方式 - -归并排序(Merge Sort):建立在归并操作上的一种有效的排序算法,该算法是采用分治法的典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 +#### 面试问题 -实现思路: +* 为什么子类构造器会先调用父类构造器? + + 1. 子类的构造器的第一行默认super()调用父类的无参数构造器,写不写都存在 + 2. 子类继承父类,子类就得到了父类的属性和行为。调用子类构造器初始化子类对象数据时,必须先调用父类构造器初始化继承自父类的属性和行为 + 3. 参考JVM -> 类加载 -> 对象创建 + + ```java + class Animal{ + public Animal(){ + System.out.println("==父类Animal的无参数构造器=="); + } + } + class Tiger extends Animal{ + public Tiger(){ + super(); // 默认存在的,根据参数去匹配调用父类的构造器。 + System.out.println("==子类Tiger的无参数构造器=="); + } + public Tiger(String name){ + //super(); 默认存在的,根据参数去匹配调用父类的构造器。 + System.out.println("==子类Tiger的有参数构造器=="); + } + } + ``` + + + +* **为什么Java是单继承的?** + 答:反证法,假如Java可以多继承,请看如下代码: + 补充:多实现是在实现接口时,重名方法需要实现类来实现 -1. 一组数据拆分成两个元素相等的子组,并对每一个子组继续拆分,直到拆分后的每个子组的元素个数是1为止 -2. 将相邻的两个子组进行合并成一个有序的大组 -3. 不断的重复步骤2,直到最终只有一个组为止 + ```java + class A{ + public void test(){ + System.out.println("A"); + } + } + class B{ + public void test(){ + System.out.println("B"); + } + } + class C extends A , B { + public static void main(String[] args){ + C c = new C(); + c.test(); + // 出现了类的二义性!所以Java不能多继承!! + } + } + ``` - + -归并步骤:每次比较两端最小的值,把最小的值放在辅助数组的左边 -![](https://gitee.com/seazean/images/raw/master/Java/Sort-归并步骤1.png) -![](https://gitee.com/seazean/images/raw/master/Java/Sort-归并步骤2.png) +------ -![](https://gitee.com/seazean/images/raw/master/Java/Sort-归并步骤3.png) +### super +继承后super调用父类构造器,父类构造器初始化继承自父类的数据。 -*** +总结与拓展: +* this代表了当前对象的引用(继承中指代子类对象):this.子类成员变量、this.子类成员方法、**this(...)**可以根据参数匹配访问本类其他构造器。 +* super代表了父类对象的引用(继承中指代了父类对象空间):super.父类成员变量、super.父类的成员方法、super(...)可以根据参数匹配访问父类的构造器 +**注意:** -##### 实现代码 +* this(...)借用本类其他构造器,super(...)调用父类的构造器。 +* this(...)或super(...)必须放在构造器的第一行,否则报错! +* this(...)和super(...)不能同时出现在构造器中,因为构造函数必须出现在第一行上,只能选择一个。 ```java -public class MergeSort { +public class ThisDemo { public static void main(String[] args) { - int[] arr = new int[]{55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88}; - mergeSort(arr, 0, arr.length - 1); - System.out.println(Arrays.toString(arr)); - } - //low 为arr最小索引,high为最大索引 - public static void mergeSort(int[] arr, int low, int high) { - if (low < high) { - int mid = (low + high) / 2; - mergeSort(arr, low, mid);//归并排序前半段 - mergeSort(arr, mid + 1, high);//归并排序后半段 - merge(arr, low, mid, high);//将两段有序段合成一段有序段 - } + // 需求:希望如果不写学校默认就是”张三“! + Student s1 = new Student("天蓬元帅", 1000 ); + Student s2 = new Student("齐天大圣", 2000, "清华大学" ); } +} +class Study extends Student { + public Study(String name, int age, String schoolName) { + super(name , age , schoolName) ; + // 根据参数匹配调用父类构造器 + } +} - private static void merge(int[] arr, int low, int mid, int high) { - int m = 0; - //定义左右指针 - int left = low, right = mid + 1; - int[] assist = new int[high - low + 1]; - while (left <= mid && right <= high) { - assist[m++] = arr[left] < arr[right] ? arr[left++] : arr[right++]; - } - while (left <= mid) { - assist[m++] = arr[left++]; - } - while (right <= high) { - assist[m++] = arr[right++]; - } +class Student{ + private String name ; + private int age ; + private String schoolName ; - for (int k = 0; k < assist.length; k++) { - arr[low++] = assist[k]; - } + public Student() { + } + public Student(String name , int age){ + // 借用兄弟构造器的功能! + this(name , age , "张三"); + } + public Student(String name, int age, String schoolName) { + this.name = name; + this.age = age; + this.schoolName = schoolName; } +// .......get + set } ``` - -用树状图来描述归并,假设元素的个数为 n,那么使用归并排序拆分的次数为 `log2(n)`,即层数,每次归并需要做 n 次对比,最终得出的归并排序的时间复杂度为 `log2(n)*n`,根据大O推导法则,忽略底数,最终归并排序的时间复杂度为 O(nlogn) -归并排序的缺点:需要申请额外的数组空间,导致空间复杂度提升,是典型的**以空间换时间**的操作 +*** +### final +#### 基本介绍 -**** +final用于修饰:类,方法,变量 +* final修饰类,类不能被继承了,类中的方法和变量可以使用 +* final可以修饰方法,方法就不能被重写 +* final修饰变量总规则:变量有且仅能被赋值一次 +**面试题**:final和abstract的关系? + 互斥关系,不能同时修饰类或者同时修饰方法!! -#### 快速排序 -快速排序(Quick Sort):通过**分治思想**对冒泡排序的改进,基本过程是通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,以此达到整个数据变成有序序列 -实现思路: +*** -1. 从数列中挑出一个元素,称为基准(pivot) -2. 重新排序数列,所有比基准值小的摆放在基准前面,所有比基准值大的摆在基准的后面(相同的数可以到任一边),在这个分区退出之后,该基准就处于数列的中间位置,这个称为分区(partition)操作; -3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序 - -```java -public class QuickSort { - public static void main(String[] args) { - int[] arr = {55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88}; - quickSort(arr, 0, arr.length - 1); - System.out.println(Arrays.toString(arr)); - } +#### 修饰变量 - public static void quickSort(int[] arr, int low, int high) { - //递归结束的条件 - if (low >= high) { - return; - } - - int left = low; - int right = high; - - int temp = arr[left];//基准数 - while (left < right) { - // 用 >= 可以防止多余的交换 - while (arr[right] >= temp && right > left) { - right--; - } - // 做判断防止相等 - if (right > left) { - // 到这里说明 arr[right] < temp - arr[left] = arr[right];//此时把arr[right]元素视为空 - left++; - } - while (arr[left] <= temp && left < right) { - left++; - } - if (right > left) { - arr[right] = arr[left]; - right--; - } - } - // left == right - arr[left] = temp; - quickSort(arr, low, left-1); - quickSort(arr, right + 1, high); - } -} -``` +##### 静态成员变量 -快速排序和归并排序的区别: +final修饰静态成员变量,变量变成了常量 -* 快速排序是另外一种分治的排序算法,将一个数组分成两个子数组,将两部分独立的排序 -* 归并排序的处理过程是由下到上的,先处理子问题,然后再合并。而快排正好相反,它的处理过程是由上到下的,先分区,然后再处理子问题 -* 快速排序和归并排序是互补的:归并排序将数组分成两个子数组分别排序,并将有序的子数组归并从而将整个数组排序,而快速排序的方式则是当两个数组都有序时,整个数组自然就有序了 -* 在归并排序中,一个数组被等分为两半,归并调用发生在处理整个数组之前,在快速排序中,切分数组的位置取决于数组的内容,递归调用发生在处理整个数组之后 +**常量:有public static final修饰,名称字母全部大写,多个单词用下划线连接。** -时间复杂度: +final修饰静态成员变量可以在哪些地方赋值: -* 最优情况:每一次切分选择的基准数字刚好将当前序列等分。把数组的切分看做是一个树,共切分了 logn 次,所以,最优情况下快速排序的时间复杂度为 O(nlogn) +1. 定义的时候赋值一次 -* 最坏情况:每一次切分选择的基准数字是当前序列中最大数或者最小数,这使得每次切分都会有一个子组,那么总共就得切分n次,所以最坏情况下,快速排序的时间复杂度为 O(n^2) +2. 可以在静态代码块中赋值一次 - +```java +public class FinalDemo { +//常量:public static final修饰,名称字母全部大写,下划线连接。 + public static final String SCHOOL_NAME = "张三" ; + public static final String SCHOOL_NAME1; -* 平均情况:每一次切分选择的基准数字不是最大值和最小值,也不是中值,这种情况用数学归纳法证明,快速排序的时间复杂度为 O(nlogn) + static{ + //SCHOOL_NAME = "java";//报错 + SCHOOL_NAME1 = "张三1"; + //SCHOOL_NAME1 = "张三2"; // 报错,第二次赋值! + } +} +``` -推荐视频:https://www.bilibili.com/video/BV1b7411N798?t=1001&p=81 - -参考文章:https://blog.csdn.net/nrsc272420199/article/details/82587933 +##### 实例成员变量 +final修饰变量的总规则:有且仅能被赋值一次 +final修饰实例成员变量可以在哪些地方赋值1次: +1. 定义的时候赋值一次 +2. 可以在实例代码块中赋值一次 +3. 可以在每个构造器中赋值一次 +```java +public class FinalDemo { + private final String name = "张三" ; + private final String name1; + private final String name2; + { + // 可以在实例代码块中赋值一次。 + name1 = "张三1"; + } + //构造器赋值一次 + public FinalDemo(){ + name2 = "张三2"; + } + public FinalDemo(String a){ + name2 = "张三2"; + } -**** + public static void main(String[] args) { + FinalDemo f1 = new FinalDemo(); + //f1.name = "张三1"; // 第二次赋值 报错! + } +} +``` -#### 基数排序 +*** -基数排序(Radix Sort):又叫桶排序和箱排序,借助多关键字排序的思想对单逻辑关键字进行排序的方法 -计数排序其实是桶排序的一种特殊情况,当要排序的 n 个数据,所处的范围并不大的时候,比如最大值是 k,我们就可以把数据划分成 k 个桶,每个桶内的数据值都是相同的,省掉了桶内排序的时间 -按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前 +### 抽象类 -解释:先排低位再排高位,可以说明在高位相等的情况下低位是递增的,如果高位也是递增,则数据有序 +#### 基本介绍 - +> 父类知道子类要完成某个功能,但是每个子类实现情况不一样。 -实现思路: +抽象方法:没有方法体,只有方法签名,必须用**abstract**修饰的方法就是抽象方法 +抽象类:拥有抽象方法的类必须定义成抽象类,必须用**abstract**修饰,抽象类是为了被继承 -- 获得最大数的位数,可以通过将最大数变为String类型,再求长度 -- 将所有待比较数值(正整数)统一为同样的数位长度,**位数较短的数前面补零** -- 从最低位开始,依次进行一次排序 -- 从最低位排序一直到最高位(个位 → 十位 → 百位 → … →最高位)排序完成以后,数列就变成一个有序序列 +一个类继承抽象类,**必须重写抽象类的全部抽象方法**,否则这个类必须定义成抽象类,因为拥有抽象方法的类必须定义成抽象类 ```java -public class BucketSort { +public class AbstractDemo { public static void main(String[] args) { - int[] arr = new int[]{576, 22, 26, 548, 1, 3, 843, 536, 735, 43, 3, 912, 88}; - bucketSort(arr); - System.out.println(Arrays.toString(arr)); + Dog d = new Dog(); + d.run(); } +} - private static void bucketSort(int[] arr) { - // 桶的个数固定为10个(个位是0~9),数组长度为了防止所有的数在同一行 - int[][] bucket = new int[10][arr.length]; - //记录每个桶中的有多少个元素 - int[] elementCounts = new int[10]; - - //获取数组的最大元素 - int max = arr[0]; - for (int i = 1; i < arr.length; i++) { - max = max > arr[i] ? max : arr[i]; - } - String maxEle = Integer.toString(max); - //将数组中的元素放入桶中,最大数的位数相当于需要几次放入桶中 - for (int i = 0, step = 1; i < maxEle.length(); i++, step *= 10) { - for (int j = 0; j < arr.length; j++) { - //获取最后一位的数据,也就是索引 - int index = (arr[j] / step) % 10; - //放入具体位置 - bucket[index][elementCounts[index]] = arr[j]; - //存储每个桶的数量 - elementCounts[index]++; - } - //收集回数组 - for (int j = 0, index = 0; j < 10; j++) { - //先进先出 - int position = 0; - //桶中有元素就取出 - while (elementCounts[j] > 0) { - arr[index] = bucket[j][position]; - elementCounts[j]--; - position++; - index++; - } - } - } +class Dog extends Animal{ + @Override + public void run() { + System.out.println("🐕跑"); } } + +abstract class Animal{ + public abstract void run(); +} ``` -空间换时间 +*** -推荐视频:https://www.bilibili.com/video/BV1b7411N798?p=86 -参考文章:https://www.toutiao.com/a6593273307280179715/?iid=6593273307280179715 +#### 面试问题 +一、抽象类是否有构造器,是否可以创建对象,为什么? +答:抽象类作为类一定有构造器,而且必须有构造器,提供给子类继承后调用父类构造器使用的 -*** +1、抽象类有构造器,但是抽象类不能创建对象,类的其他成分它都具备 +2、抽象类中存在抽象方法,但不能执行,**抽象类中也可没有抽象方法** +> 抽象在学术上本身意味着不能实例化 +```java +public class AbstractDemo { + public static void main(String[] args) { + //Animal a = new Animal(); 抽象类不能创建对象! + //a.run(); // 抽象方法不能执行 + } +} +abstract class Animal{ + private String name; + public static String schoolName = "张三"; + public Animal(){ } -#### 稳定性 + public abstract void run(); + //普通方法 + public void go(){ } +} +``` -稳定性:在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中 `r[i]=r[j]`,且 r[i] 在 r[j] 之前,而在排序后的序列中,r[i] 仍在 r[j] 之前,则称这种排序算法是稳定的,否则称为不稳定的 -如果一组数据只需要一次排序,则稳定性一般是没有意义的,如果一组数据需要多次排序,稳定性是有意义的。 - +二、static与abstract能同时使用吗? +答:不能,被static修饰的方法属于类,是类自己的东西,不是给子类来继承的,而抽象方法本身没有实现,就是用来给子类继承 -* 冒泡排序:只有当 `arr[i]>arr[i+1]` 的时候,才会交换元素的位置,而相等的时候并不交换位置,所以冒泡排序是一种稳定排序算法 -* 选择排序:是给每个位置选择当前元素最小的,例如有数据{5(1),8 ,5(2), 3, 9 },第一遍选择到的最小元素为3,所以5(1)会和3进行交换位置,此时5(1)到了5(2)后面,破坏了稳定性,所以是不稳定的排序算法 -* 插入排序:比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么把要插入的元素放在相等元素的后面。相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的 -* 希尔排序:按照不同步长对元素进行插入排序,虽然一次插入排序是稳定的,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以希尔排序是不稳定的 -* 归并排序在归并的过程中,只有 `arr[i] 接口称为 被实现,实现接口的类称为**实现类** +```java + 修饰符 interface 接口名称{ + // 抽象方法 + // 默认方法 + // 静态方法 + // 私有方法 +} +``` +* 抽象方法:接口中的抽象方法默认会加上public abstract修饰,所以可以省略不写 -*** +* 静态方法:静态方法必须有方法体 +* 常量:常量是public static final修饰的成员变量,仅能被赋值一次,值不能改变。常量的名称规范上要求全部大写,多个单词下划线连接。public static final可以省略不写。 + ```java + public interface InterfaceDemo{ + //public static final String SCHOOL_NAME = "张三"; + String SCHOOL_NAME = "张三"; + + //public abstract void run(); + void run();//默认补充 + } + ``` -### 查找 -正常查找:从第一个元素开始遍历,一个一个的往后找,综合查找比较耗时。 -二分查找也称折半查找(Binary Search)是一种效率较高的查找方法,数组必须是有序数组 -过程:每次先与中间的元素进行比较,如果大于往右边找,如果小于往左边找,如果等于就返回该元素索引位置!如果没有该元素,返回-1 +*** -时间复杂度:O(logn) -```java -/*定义一个方法,记录开始的索引位置和结束的索引位置。 -取出中间索引位置的值,拿元素与中间位置的值进行比较,如果小于中间值,结束位置=中间索引-1. -取出中间索引位置的值,拿元素与中间位置的值进行比较,如果大于中间值,开始位置=中间索引+1. -循环正常执行的条件:开始位置索引<=结束位置索引。否则说明寻找完毕但是没有该元素值返回-1.*/ -public class binarySearch { - public static void main(String[] args) { - int[] arr = {10, 14, 21, 38, 45, 47, 53, 81, 87, 99}; - System.out.println("81的索引是:" + binarySearch(arr,81)); - } - public static int binarySearch(int[] arr, int des) { - int start = 0; - int end = arr.length - 1; +#### 实现接口 - //确保不会出现重复查找,越界 - while (start <= end) { - //计算出中间索引值 - int mid = (start + end) / 2; - if (des == arr[mid]) { - return mid; - } else if (des > arr[mid]) { - start = mid + 1; - } else if (des < arr[mid]) { - end = mid - 1; - } - } - // 如果上述循环执行完毕还没有返回索引,说明根本不存在该元素值,直接返回-1 - return -1; - } -} -``` +作用:**接口是用来被类实现的。** -![](https://gitee.com/seazean/images/raw/master/Java/二分查找.gif) +类与类是继承关系:一个类只能直接继承一个父类,单继承 +类与接口是实现关系:一个类可以实现多个接口,多实现,接口不能继承类 +接口与接口继承关系:**多继承** -查找第一个匹配的元素: +>子类 继承 父类 +>实现类 实现 接口 ```java -public static int binarySearch(int[] arr, int des) { - int start = 0; - int end = arr.length - 1; +修饰符 class 实现类名称 implements 接口1,接口2,接口3,....{ - while (start <= end) { - int mid = (start + end) / 2; - if (des == arr[mid]) { - //如果 mid 等于 0,那这个元素已经是数组的第一个元素,那肯定是我要找的 - if (mid == 0 || a[mid - 1] != des) { - return mid; - } else { - //a[mid]前面的一个元素 a[mid-1]也等于 value, - //要找的元素肯定出现在[low, mid-1]之间 - high = mid - 1 - } - } else if (des > arr[mid]) { - start = mid + 1; - } else if (des < arr[mid]) { - end = mid - 1; - } - } - return -1; - } -``` - - - - - -*** - - - -### 匹配 - -#### BF - -Brute Force 暴力匹配算法: - -```java -public static void main(String[] args) { - String s = "seazean"; - String t = "az"; - System.out.println(match(s,t));//2 } - -public static int match(String s,String t) { - int k = 0; - int i = k, j = 0; - //防止越界 - while (i < s.length() && j < t.length()) { - if (s.charAt(i) == t.charAt(j)) { - ++i; - ++j; - } else { - k++; - i = k; - j = 0; - } - } - //说明是匹配成功 - if (j >= t.length()) { - return k; - } - return 0; +修饰符 interface 接口名 extend 接口1,接口2,接口3,....{ + } ``` -平均时间复杂度:O(m+n),最坏时间复杂度:O(m*n) - - - -*** - - +实现多个接口的使用注意事项: -#### RK +1. 当一个类实现多个接口时,多个接口中存在同名的静态方法并不会冲突,只能通过各自接口名访问静态方法 -把主串得长度记为 n,模式串得长度记为 m,通过哈希算法对主串中的 n-m+1 个子串分别求哈希值,然后逐个与模式串的哈希值比较大小,如果某个子串的哈希值与模式串相等,再去对比值是否相等(防止哈希冲突),那就说明对应的子串和模式串匹配了 +2. 当一个类实现多个接口时,多个接口中存在同名的默认方法,实现类必须重写这个方法 -因为哈希值是一个数字,数字之间比较是否相等是非常快速的 +3. 当一个类,既继承一个父类,又实现若干个接口时,父类中的成员方法与接口中的默认方法重名,子类**就近选择执行父类**的成员方法 -第一部分计算哈希值的时间复杂度为 O(n),第二部分对比的时间复杂度为 O(1),整体平均时间复杂度为 O(n),最坏为 O(n*m) +4. 接口中,没有构造器,**不能创建对象**,接口是更彻底的抽象,连构造器都没有,自然不能创建对象!! + ```java + public class InterfaceDemo { + public static void main(String[] args) { + Student s = new Student(); + s.run(); + s.rule(); + } + } + class Student implements Food, Person{ + @Override + public void eat() {} + + @Override + public void run() {} + } + interface Food{ + void eat(); + } + interface Person{ + void run(); + } + //可以直接 interface Person extend Food, + //然后 class Student implements Person 效果一样 + ``` + *** -#### KMP +#### JDK8以后 -KMP匹配: +jdk1.8以后新增的功能,实际开发中很少使用 -* next 数组的核心就是自己匹配自己,主串代表后缀,模式串代表前缀 -* nextVal 数组的核心就是回退失配 +* 默认方法(就是之前写的普通实例方法) + * 必须用default修饰,默认会public修饰 + * 必须用接口的实现类的对象来调用 +* 静态方法 + * 默认会public修饰 + * 接口的静态方法必须用接口的类名本身来调用 + * 调用格式:ClassName.method() +* 私有方法:JDK 1.9才开始有的,只能在**本类中**被其他的默认方法或者私有方法访问 ```java -public class Kmp { +public class InterfaceDemo { public static void main(String[] args) { - String s = "acababaabc"; - String t = "abaabc"; - //[-1, 0, 0, 1, 1, 2] - System.out.println(Arrays.toString(getNext(t))); - //[-1, 0, -1, 1, 0, 2] - System.out.println(Arrays.toString(getNextVal(t))); - //5 - System.out.println(kmp(s, t)); + // 1.默认方法调用:必须用接口的实现类对象调用。 + Man m = new Man(); + m.run(); + m.work(); + + // 2.接口的静态方法必须用接口的类名本身来调用。 + InterfaceJDK8.inAddr(); + } +} +class Man implements InterfaceJDK8{ + @Override + public void work() { + System.out.println("工作中。。。"); } +} - private static int kmp(String s, String t) { - int[] next = getNext(t); - int i = 0, j = 0; - while (i < s.length() && j < t.length()) { - //j==-1时说明第一个位置匹配失败,所以将s的下一个和t的首字符比较 - if (j == -1 || s.charAt(i) == t.charAt(j)) { - i++; - j++; - } else { - //模式串右移,比较s的当前位置与t的next[j]位置 - j = next[j]; - } - } - if (j >= t.length()) { - return i - j + 1; - } - return -1; +interface InterfaceJDK8{ + //抽象方法!! + void work(); + // a.默认方法(就是之前写的普通实例方法) + // 必须用接口的实现类的对象来调用。 + default void run(){ + go(); + System.out.println("开始跑步🏃‍"); } - //next数组 - private static int[] getNext(String t) { - int[] next = new int[t.length()]; - next[0] = -1; - int j = -1; - int i = 0; - while (i < t.length() - 1) { - // 根据已知的前j位推测第j+1位 - // j=-1说明首位就没有匹配,即t[0]!=t[i],说明next[i+1]没有最大前缀,为0 - if (j == -1 || t.charAt(i) == t.charAt(j)) { - // 因为模式串已经匹配到了索引j处,说明之前的位都是相等的 - // 因为是自己匹配自己,所以模式串就是前缀,主串就是后缀,j就是最长公共前缀 - // 当i+1位置不匹配时(i位之前匹配),可以跳转到j+1位置对比,next[i+1]=j+1 - i++; - j++; - next[i] = j; - } else { - //i位置的数据和j位置的不相等,所以回退对比i和next[j]位置的数据 - j = next[j]; - } - } - return next; + // b.静态方法 + // 注意:接口的静态方法必须用接口的类名本身来调用 + static void inAddr(){ + System.out.println("我们在武汉"); } - //nextVal - private static int[] getNextVal(String t) { - int[] nextVal = new int[t.length()]; - nextVal[0] = -1; - int j = -1; - int i = 0; - while (i < t.length() - 1) { - if (j == -1 || t.charAt(i) == t.charAt(j)) { - i++; - j++; - // 如果t[i+1] == t[next(i+1)]=next[j+1],回退后仍然失配,所以要继续回退 - if (t.charAt(i) == t.charAt(j)) { - nextVal[i] = nextVal[j]; - } else { - nextVal[i] = j; - } - } else { - j = nextVal[j]; - } - } - return nextVal; + + // c.私有方法(就是私有的实例方法): JDK 1.9才开始有的。 + // 只能在本接口中被其他的默认方法或者私有方法访问。 + private void go(){ + System.out.println("开始。。"); } - /*根据next求nextVal - private static int[] getNextVal(String t, int[] next) { - int[] nextVal = new int[next.length]; - nextVal[0] = -1; - for (int i = 1; i < nextVal.length; i++) { - if (t.charAt(i) == t.charAt(next[i])) { - nextVal[i] = nextVal[next[i]]; - } else { - nextVal[i] = next[i]; - } - } - return nextVal; - }*/ } ``` -平均和最坏时间复杂度都是 O(m+n) +*** -参考文章:https://www.cnblogs.com/tangzhengyue/p/4315393.html +#### 对比抽象类 -*** +| **参数** | **抽象类** | **接口** | +| ------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | +| 默认的方法实现 | 可以有默认的方法实现 | 接口完全是抽象的,jdk8以后有默认的实现 | +| 实现 | 子类使用**extends**关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现。 | 子类使用关键字**implements**来实现接口。它需要提供接口中所有声明的方法的实现 | +| 构造器 | 抽象类可以有构造器 | 接口不能有构造器 | +| 与正常Java类的区别 | 除了不能实例化抽象类之外,和普通Java类没有任何区别 | 接口是完全不同的类型 | +| 访问修饰符 | 抽象方法可以有**public**、**protected**和**default**这些修饰符 | 接口方法默认修饰符是**public**,别的修饰符需要有方法体 | +| main方法 | 抽象方法可以有main方法并且我们可以运行它 | jdk8以前接口没有main方法,不能运行;jdk8以后接口可以有default和static方法,可以运行main方法 | +| 多继承 | 抽象方法可以继承一个类和实现多个接口 | 接口可以继承一个或多个其它接口,接口不可继承类 | +| 速度 | 比接口速度要快 | 接口是稍微有点慢的,因为它需要时间去寻找在类中实现的方法 | +| 添加新方法 | 如果往抽象类中添加新的方法,可以给它提供默认的实现,因此不需要改变现在的代码 | 如果往接口中添加方法,那么必须改变实现该接口的类 | -### 树 -#### 二叉树 -二叉树中,任意一个节点的度要小于等于 2 +------ -+ 节点:在树结构中,每一个元素称之为节点 -+ 度:每一个节点的子节点数量称之为度 -二叉树结构图 +### 多态 +#### 基本介绍 -**** +多态的概念:同一个实体同时具有多种形式同一个类型的对象,执行同一个行为,在不同的状态下会表现出不同的行为特征。 +多态的格式: +* 父类类型范围 > 子类类型范围 -#### 排序树 +```java +父类类型 对象名称 = new 子类构造器; +接口 对象名称 = new 实现类构造器; +``` -##### 存储结构 +多态的执行: -二叉排序树(BST),又称二叉查找树或者二叉搜索树 +* 对于方法的调用:**编译看左边,运行看右边**(分派机制) +* 对于变量的调用:**编译看左边,运行看左边** -+ 每一个节点上最多有两个子节点 -+ 左子树上所有节点的值都小于根节点的值 -+ 右子树上所有节点的值都大于根节点的值 -+ 不存在重复的节点 +多态的使用规则: -二叉查找树 +* 必须存在继承或者实现关系 +* 必须存在父类类型的变量引用子类类型的对象 +* 存在方法重写 + +多态的优势: +* 在多态形式下,右边对象可以实现组件化切换,业务功能也随之改变,便于扩展和维护。可以实现类与类之间的**解耦** +* 父类类型作为方法形式参数,传递子类对象给方法,可以传入一切子类对象进行方法的调用,更能体现出多态的**扩展性**与便利性 + +多态的劣势: +* 多态形式下,不能直接调用子类特有的功能,因为编译看左边,父类中没有子类独有的功能,所以代码在编译阶段就直接报错了! + +```java +public class PolymorphicDemo { + public static void main(String[] args) { + Animal c = new Cat(); + c.run(); + //c.eat();//报错 编译看左边 需要强转 + go(c); + go(new Dog); + } + //用 Dog或者Cat 都没办法让所有动物参与进来,只能用Anima + public static void go(Animal d){} + +} +class Dog extends Animal{} +class Cat extends Animal{ + public void eat(); + @Override + public void run(){} +} +class Animal{ + public void run(){} +} +``` @@ -2034,101 +1917,61 @@ public class Kmp { -##### 代码实现 +#### 上下转型 -* 节点类: +>基本数据类型的转换: +> 1.小范围类型的变量或者值可以直接赋值给大范围类型的变量。 +> 2.大范围类型的变量或者值必须强制类型转换给小范围类型的变量。 - ```java - private static class TreeNode { - int key; - TreeNode left; //左节点 - TreeNode right; //右节点 - - private TreeNode(int key) { - this.key = key; - } - } - ``` +引用数据类型的**自动**类型转换语法:子类类型的对象或者变量可以自动类型转换赋值给父类类型的变量。 -* 查找节点: +**父类引用指向子类对象** - ```java - // 递归查找 - private static TreeNode search(TreeNode root, int key) { - //递归结束的条件 - if (root == null) { - return null; - } - if (key == root.key) { - return root; - } else if (key > root.key) { - return search(root.right, key); - } else { - return search(root.left, key); - } - } - - // 非递归 - private static TreeNode search1(TreeNode root, int key) { - while (root != null) { - if (key == root.key) { - return root; - } else if (key > root.key) { - root = root.right; - } else { - root = root.left; - } - } - return null; - } - ``` +- **向上转型(upcasting)**:通过子类对象(小范围)实例化父类对象(大范围),这种属于自动转换 +- **向下转型(downcasting)**:通过父类对象(大范围)实例化子类对象(小范围),这种属于强制转换 -* 插入节点: +```java +public class PolymorphicDemo { + public static void main(String[] args){ + Animal a = new Cat();//向上转型 + Cat c = (Cat)a;//向下转型 + } +} +class Animal{} +class Cat extends Animal{} +``` - ```java - private static int insert(TreeNode root, int key) { - if (root == null) { - root = new TreeNode(key); - root.left = null; - root.right = null; - return 1; - } else { - if (key == root.key) { - return 0; - } else if (key > root.key) { - return insert(root.right, key); - } else { - return insert(root.left, key); - } - } - } - ``` -* 构造函数: - ```java - // 构造函数,返回根节点 - private static TreeNode createBST(int[] arr) { - if (arr.length > 0) { - TreeNode root = new TreeNode(arr[0]); - for (int i = 1; i < arr.length; i++) { - insert(root, arr[i]); - } - return root; - } - return null; - } - ``` +*** -* 删除节点:要删除节点12,先找到节点19,然后移动并替换节点12 - 代码链接:https://leetcode-cn.com/submissions/detail/190232548/ - +#### instanceof -参考视频:https://www.bilibili.com/video/BV1iJ411E7xW?t=756&p=86 +instanceof:判断左边的对象是否是右边的类的实例,或者是其直接或间接子类,或者是其接口的实现类 -图片来源:https://leetcode-cn.com/problems/delete-node-in-a-bst/solution/tu-jie-yi-dong-jie-dian-er-bu-shi-xiu-ga-edtn/ +* 引用类型强制类型转换:父类类型的变量或者对象强制类型转换成子类类型的变量,否则报错 +* 强制类型转换的格式:**类型 变量名称 = (类型)(对象或者变量)** +* 有继承/实现关系的两个类型就可以进行强制类型转换,编译阶段一定不报错,但是运行阶段可能出现类型转换异常 ClassCastException + +```java +public class Demo{ + public static void main(String[] args){ + Aniaml a = new Dog(); + //Dog d = (Dog)a; + //Cat c = (Cat)a; 编译不报错,运行报ClassCastException错误 + if(a instanceof Cat){ + Cat c = (Cat)a; + } else if(a instanceof Dog) { + Dog d = (Dog)a; + } + } +} +class Dog extends Animal{} +class Cat extends Animal{} +class Animal{} +``` @@ -2136,88 +1979,85 @@ public class Kmp { -#### 平衡树 - -平衡二叉树(AVL)的特点: +### 内部类 -+ 二叉树左右两个子树的高度差不超过1 -+ 任意节点的左右两个子树都是一颗平衡二叉树 +#### 概述 -平衡二叉树旋转: +内部类是类的五大成分之一:成员变量,方法,构造器,代码块,内部类 -+ 旋转触发时机:当添加一个节点之后,该树不再是一颗平衡二叉树 +概念:定义在一个类里面的类就是内部类 -+ 平衡二叉树和二叉查找树对比结构图 +作用:提供更好的封装性,体现出组件思想,**间接解决类无法多继承引起的一系列问题** -![平衡二叉树和二叉查找树对比](https://gitee.com/seazean/images/raw/master/Java/平衡二叉树和二叉查找树对比结构图.png) +分类:静态内部类、实例内部类(成员内部类)、局部内部类、**匿名内部类**(重点) -+ 左旋:将根节点的右侧往左拉,原先的右子节点变成新的父节点,并把多余的左子节点出让,给已经降级的根节点当右子节点 - - ![平衡二叉树左旋](https://gitee.com/seazean/images/raw/master/Java/平衡二叉树左旋01.png) -* 右旋:将根节点的左侧往右拉,左子节点变成了新的父节点,并把多余的右子节点出让,给已经降级根节点当左子节点 - - ![平衡二叉树右旋](https://gitee.com/seazean/images/raw/master/Java/平衡二叉树右旋01.png) -推荐文章:https://pdai.tech/md/algorithm/alg-basic-tree-balance.html +*** +#### 静态内部类 -*** +定义:有static修饰,属于外部类本身,会加载一次 +静态内部类中的成分研究: +* 类有的成分它都有,静态内部类属于外部类本身,只会加载一次 +* 特点与外部类是完全一样的,只是位置在别人里面 +* 可以定义静态成员 -#### 红黑树 +静态内部类的访问格式:外部类名称.内部类名称 -红黑树的特点: +静态内部类创建对象的格式:外部类名称.内部类名称 对象名称 = new 外部类名称.内部类构造器; -* 每一个节点可以是红或者黑 +静态内部类的访问拓展: -+ 红黑树不是高度平衡的,它的平衡是通过"自己的红黑规则"进行实现的 +* 静态内部类中是否可以直接访问外部类的静态成员? 可以,外部类的静态成员只有一份,可以被共享 +* 静态内部类中是否可以直接访问外部类的实例成员? 不可以,外部类的成员必须用外部类对象访问 -红黑树的红黑规则有哪些: +```java +public class Demo{ + public static void main(String[] args){ + Outter.Inner in = new Outter.Inner(); + } +} -1. 每一个节点或是红色的,或者是黑色的 -2. 根节点必须是黑色 -3. 如果一个节点没有子节点或者父节点,则该节点相应的指针属性值为 Nil,这些 Nil 视为叶节点,每个叶节点(Nil) 是黑色的 -4. 如果某一个节点是红色,那么它的子节点必须是黑色(不能出现两个红色节点相连的情况) -5. 对每一个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点 +static class Outter{ + public static int age; + private double salary; + public static class Inner{ + //拥有类的所有功能 构造器 方法 成员变量 + System.out.println(age); + //System.out.println(salary);报错 + } +} +``` -红黑树与 AVL 树的比较: -* AVL树是更加严格的平衡,可以提供更快的查找速度,适用于读取查找密集型任务 -* 红黑树只是做到了近似平衡,并不是严格的平衡,红黑树的插入删除比AVL树更便于控制操作,红黑树更适合于插入修改密集型任务 -- 红黑树整体性能略优于AVL树,AVL树的旋转比红黑树的旋转多,更加难以平衡和调试,插入和删除的效率比红黑树慢 +*** -![红黑树](https://gitee.com/seazean/images/raw/master/Java/红黑树结构图.png) +#### 实例内部类 -红黑树添加节点的默认颜色为红色,效率高 -![](https://gitee.com/seazean/images/raw/master/Java/红黑树添加节点颜色.png) +定义:无static修饰的内部类,属于外部类的每个对象,跟着外部类对象一起加载。 +实例内部类的成分特点:实例内部类中不能定义静态成员,其他都可以定义 +实例内部类的访问格式:外部类名称.内部类名称 -**红黑树添加节点后如何保持红黑规则:** +创建对象的格式:外部类名称.内部类名称 对象名称 = new 外部类构造器.new 内部构造器; -+ 根节点位置 - + 直接变为黑色 -+ 非根节点位置 - + 父节点为黑色 - + 不需要任何操作,默认红色即可 - + 父节点为红色 - + 叔叔节点为红色 - 1. 将"父节点"设为黑色,将"叔叔节点"设为黑色 - 2. 将"祖父节点"设为红色 - 3. 如果"祖父节点"为根节点,则将根节点再次变成黑色 - + 叔叔节点为黑色 - 1. 将"父节点"设为黑色 - 2. 将"祖父节点"设为红色 - 3. 以"祖父节点"为支点进行旋转 +* `Outter.Inner in = new Outter().new Inner();` +拓展:**实例内部类可以访问外部类的全部成员** +> * 实例内部类中是否可以直接访问外部类的静态成员? +> 可以,外部类的静态成员可以被共享访问! +> * 实例内部类中是否可以访问外部类的实例成员? +> 可以,实例内部类属于外部类对象,可以直接访问外部类对象的实例成员! @@ -2225,266 +2065,146 @@ public class Kmp { -#### 并查集 +#### 局部内部类 -##### 基本实现 +局部内部类:定义在方法中,在构造器中,代码块中,for循环中定义的内部类。 -并查集是一种树型的数据结构,有以下特点: - -* 每个元素都唯一的对应一个结点 -* 每一组数据中的多个元素都在同一颗树中 -* 一个组中的数据对应的树和另外一个组中的数据对应的树之间没有任何联系 -* 元素在树中并没有子父级关系的硬性要求 - - - -可以高效地进行如下操作: - -* 查询元素 p 和元素 q 是否属于同一组 -* 合并元素 p 和元素 q 所在的组 - -存储结构: - - - -合并方式: - - - - - -代码实现: - -* 类实现: +局部内部类中的成分特点:只能定义实例成员,不能定义静态成员 - ```java - public class UF { - //记录节点元素和该元素所在分组的标识 - private int[] eleAndGroup; - //记录分组的个数 - private int count; - - //初始化并查集 - public UF(int N) { - //初始化分组数量 - this.count = N; - //初始化eleAndGroup数量 - this.eleAndGroup = new int[N]; - //初始化eleAndGroup中的元素及其所在分组的标识符,eleAndGroup索引作为每个节点的元素 - //每个索引处的值就是该组的索引,就是该元素所在的组的标识符 - for (int i = 0; i < eleAndGroup.length; i++) { - eleAndGroup[i] = i; - } - } - - //查询p所在的分组的标识符 - public int find(int p) { - return eleAndGroup[p]; - } - - //判断并查集中元素p和元素q是否在同一分组中 - public boolean connect(int p, int q) { - return find(p) == find(q); - } - - //把p元素所在分组和q元素所在分组合并 - public void union(int p, int q) { - //判断元素q和p是否已经在同一个分组中,如果已经在同一个分组中,则结束方法就可以了 - if (connect(p, q)) { - return; - } - int pGroup = find(p);//找到p所在分组的标识符 - int qGroup = find(q);//找到q所在分组的标识符 - - //合并组,让p所在组的 所有元素 的组标识符变为q所在分组的标识符 - for (int i = 0; i < eleAndGroup.length; i++) { - if (eleAndGroup[i] == pGroup) { - eleAndGroup[i] = qGroup; - } - } - //分组个数-1 - this.count--; - } - } - ``` - -* 测试代码: +```java +public class InnerClass{ + public static void main(String[] args){ + String name; + class{} + } + public static void test(){ + class Animal{} + class Cat extends Animal{} + } +} +``` - ```java - public static void main(String[] args) { - //创建并查集对象 - UF uf = new UF(5); - System.out.println(uf); - - //从控制台录入两个合并的元素,调用union方法合并,观察合并后并查集的分组 - Scanner sc = new Scanner(System.in); - - while (true) { - System.out.println("输入第一个要合并的元素"); - int p = sc.nextInt(); - System.out.println("输入第二个要合并的元素"); - int q = sc.nextInt(); - if (uf.connect(p, q)) { - System.out.println(p + "元素已经和" + q + "元素已经在同一个组"); - continue; - } - uf.union(p, q); - System.out.println("当前并查集中还有:" + uf.count() + "个分组"); - System.out.println(uf); - System.out.println("********************"); - } - } - ``` -最坏情况下 union 算法的时间复杂度也是 O(N^2) +*** -**** +#### 匿名内部类 +匿名内部类:没有名字的局部内部类 +作用:简化代码,是开发中常用的形式 -##### 优化实现 +匿名内部类的格式: -让每个索引处的节点都指向它的父节点,当 eleGroup[i] = i 时,说明 i 是根节点 +```java +new 类名|抽象类|接口(形参){ + //方法重写。 +} +``` + 匿名内部类的特点: - +* 匿名内部类不能定义静态成员 +* 匿名内部类一旦写出来,就会立即创建一个匿名内部类的对象返回 +* **匿名内部类的对象的类型相当于是当前new的那个的类型的子类类型** +* 匿名内部类引用局部变量必须是**常量**,底层创建为内部类的成员变量(原因:JVM → 运行机制 → 代码优化) ```java -//查询p所在的分组的标识符,递归寻找父标识符,直到找到根节点 -public int findRoot(int p) { - while (p != eleAndGroup[p]) { - p = eleAndGroup[p]; +public class Anonymity { + public static void main(String[] args) { + Animal a = new Animal(){ + @Override + public void run() { + System.out.println("猫跑的贼溜~~"); + //System.out.println(n); + } + }; + a.run(); + a.go(); } - //p == eleGroup[p],说明p是根节点 - return p; -} - -//判断并查集中元素p和元素q是否在同一分组中 -public boolean connect(int p, int q) { - return findRoot(p) == findRoot(q); } +abstract class Animal{ + public abstract void run(); -//把p元素所在分组和q元素所在分组合并 -public void union(int p, int q) { - //找到p q对应的根节点 - int pRoot = findRoot(p); - int qRoot = findRoot(q); - if (pRoot == qRoot) { - return; + public void go(){ + System.out.println("开始go~~~"); } - //让p所在树的节点根节点为q的所在的根节点,只需要把根节点改一下,时间复杂度 O(1) - eleAndGroup[pRoot] = qRoot; } ``` -平均时间复杂度为 O(N),最坏时间复杂度是 O(N^2) - -继续优化:路径压缩,保证每次把小树合并到大树 +*** -```java -public class UF_Tree_Weighted { - private int[] eleAndGroup; - private int count; - private int[] size;//存储每一个根结点对应的树中的保存的节点的个数 - //初始化并查集 - public UF_Tree_Weighted(int N) { - this.count = N; - this.eleAndGroup = new int[N]; - for (int i = 0; i < eleAndGroup.length; i++) { - eleAndGroup[i] = i; - } - this.size = new int[N]; - //默认情况下,size中每个索引处的值都是1 - for (int i = 0; i < size.length; i++) { - size[i] = 1; - } - } - //查询p所在的分组的标识符,父标识符 - public int findRoot(int p) { - while (p != eleAndGroup[p]) { - p = eleAndGroup[p]; - } - return p; - } - //判断并查集中元素p和元素q是否在同一分组中 - public boolean connect(int p, int q) { - return findRoot(p) == findRoot(q); - } +### 权限符 - //把p元素所在分组和q元素所在分组合并 - public void union(int p, int q) { - //找到p q对应的根节点 - int pRoot = findRoot(p); - int qRoot = findRoot(q); - if (pRoot == qRoot) { - return; - } - //判断pRoot对应的树大还是qRoot对应的树大,最终需要把较小的树合并到较大的树中 - if (size[pRoot] < size[qRoot]) { - eleAndGroup[pRoot] = qRoot; - size[qRoot] += size[pRoot]; - } else { - eleAndGroup[qRoot] = pRoot; - size[pRoot] += size[qRoot]; - } - //组的数量-1、 - this.count--; - } -} -``` +权限修饰符:有四种**(private -> 缺省 -> protected - > public )** +可以修饰成员变量,修饰方法,修饰构造器,内部类,不同修饰符修饰的成员能够被访问的权限将受到限制! +| 四种修饰符访问权限 | private | 缺省 | protected | public | +| ------------------ | :-----: | :--: | :-------: | :----: | +| 本类中 | √ | √ | √ | √ | +| 子类中 | X | √ | √ | √ | +| 本包下其他类中 | X | √ | √ | √ | +| 其他包下的子类中 | X | X | √ | √ | +| 其他包下的其他类中 | X | X | X | √ | +protected 用于修饰成员,表示在继承体系中成员对于子类可见,子类需要重写方法才可以调用 -*** -##### 应用场景 -并查集存储的每一个整数表示的是一个大型计算机网络中的计算机: +*** -* 可以通过 connected(int p, int q) 来检测该网络中的某两台计算机之间是否连通 -* 可以调用 union(int p,int q) 使得 p 和 q 之间连通,这样两台计算机之间就可以通信 -畅通工程:某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府畅通工程的目标是使全省任何两个城镇间都可以实现交通,但不一定有直接的道路相连,只要互相间接通过道路可达即可,问最少还需要建设多少条道路? - +### 代码块 -解题思路: +#### 静态代码块 -1. 创建一个并查集 UF_Tree_Weighted(20) -2. 分别调用 union(0,1)、union(6,9)、union(3,8)、union(5,11)、union(2,12)、union(6,10)、union(4,8),表示已经修建好的道路把对应的城市连接起来 -3. 如果城市全部连接起来,那么并查集中剩余的分组数目为 1,所有的城市都在一个树中,只需要获取当前并查集中剩余的数目减去 1,就是还需要修建的道路数目 +静态代码块的格式: -```java -public static void main(String[] args)throws Exception { - Scanner sc = new Scanner(System.in); - //读取城市数目,初始化并查集 - int number = sc.nextInt(); - //读取已经修建好的道路数目 - int roadNumber = sc.nextInt(); - UF_Tree_Weighted uf = new UF_Tree_Weighted(number); - //循环读取已经修建好的道路,并调用union方法 - for (int i = 0; i < roadNumber; i++) { - int p = sc.nextInt(); - int q = sc.nextInt(); - uf.union(p,q); - } - //获取剩余的分组数量 - int groupNumber = uf.count(); - //计算出还需要修建的道路 - System.out.println("还需要修建"+(groupNumber-1)+"道路,城市才能相通"); + ```java +static { } -``` + ``` +* 静态代码块特点: + * 必须有static修饰 + * 会与类一起优先加载,且自动触发执行一次 + * 只能访问静态资源 +* 静态代码块作用: + * 可以在执行类的方法等操作之前先在静态代码块中进行静态资源的初始化 + * **先执行静态代码块,在执行main函数里的操作** +```java +public class CodeDemo { + public static String schoolName ; + public static ArrayList lists = new ArrayList<>(); -参考视频:https://www.bilibili.com/video/BV1iJ411E7xW?p=142 + // 静态代码块,属于类,与类一起加载一次! + static { + System.out.println("静态代码块被触发执行~~~~~~~"); + // 在静态代码块中进行静态资源的初始化操作 + schoolName = "张三"; + lists.add("3"); + lists.add("4"); + lists.add("5"); + } + public static void main(String[] args) { + System.out.println("main方法被执行"); + System.out.println(schoolName); + System.out.println(lists); + } +} +/*静态代码块被触发执行~~~~~~~ +main方法被执行 +张三 +[3, 4, 5] */ +``` @@ -2492,82 +2212,40 @@ public static void main(String[] args)throws Exception { -#### 字典树 - -##### 基本介绍 - -Trie 树,也叫字典树,是一种专门处理字符串匹配的树形结构,用来解决在一组字符串集合中快速查找某个字符串的问题,Trie 树的本质就是利用字符串之间的公共前缀,将重复的前缀合并在一起 - -* 根节点不包含任何信息 -* 每个节点表示一个字符串中的字符,从**根节点到红色节点的一条路径表示一个字符串** -* 红色节点并不都是叶子节点 - - - - - -注意:要查找的是字符串“he”,从根节点开始,沿着某条路径来匹配,可以匹配成功。但是路径的最后一个节点“e”并不是红色的,也就是说,“he”是某个字符串的前缀子串,但并不能完全匹配任何字符串 - - - -*** - - +#### 实例代码块 -##### 实现Trie +实例代码块的格式: -通过一个下标与字符一一映射的数组,来存储子节点的指针 +```java +{ - +} +``` -时间复杂度是 O(n)(n 表示要查找字符串的长度) +* 实例代码块的特点: + * 无static修饰,属于对象 + * 会与类的对象一起加载,每次创建类的对象的时候,实例代码块都会被加载且自动触发执行一次 + * 实例代码块的代码在底层实际上是提取到每个构造器中去执行的 + +* 实例代码块的作用:实例代码块可以在创建对象之前进行实例资源的初始化操作 ```java -public class Trie { - private TrieNode root = new TrieNode('/'); - - //插入一个字符 - public void insert(char[] chars) { - TrieNode p = root; - for (int i = 0; i < chars.length; i++) { - //获取字符的索引位置 - int index = chars[i] - 'a'; - if (p.children[index] == null) { - TrieNode node = new TrieNode(chars[i]); - p.children[index] = node; - } - p = p.children[index]; - } - p.isEndChar = true; - } - - //查找一个字符串 - public boolean find(char[] chars) { - TrieNode p = root; - for (int i = 0; i < chars.length; i++) { - int index = chars[i] - 'a'; - if (p.children[index] == null) { - return false; - } - p = p.children[index]; - } - if (p.isEndChar) { - //完全匹配 - return true; - } else { - // 不能完全匹配,只是前缀 - return false; - } +public class CodeDemo { + private String name; + private ArrayList lists = new ArrayList<>(); + { + name = "代码块"; + lists.add("java"); + System.out.println("实例代码块被触发执行一次~~~~~~~~"); } + public CodeDemo02(){ }//构造方法 + public CodeDemo02(String name){} - - private class TrieNode { - char data; - TrieNode[] children = new TrieNode[26];//26个英文字母 - boolean isEndChar = false;//结尾字符为true - public TrieNode(char data) { - this.data = data; - } + public static void main(String[] args) { + CodeDemo c = new CodeDemo();//实例代码块被触发执行一次 + System.out.println(c.name); + System.out.println(c.lists); + new CodeDemo02();//实例代码块被触发执行一次 } } ``` @@ -2578,177 +2256,149 @@ public class Trie { -##### 优化Trie - -Trie 树是非常耗内存,采取空间换时间的思路。Trie 树的变体有很多,可以在一定程度上解决内存消耗的问题。比如缩点优化,对只有一个子节点的节点,而且此节点不是一个串的结束节点,可以将此节点与子节点合并 - -![](https://gitee.com/seazean/images/raw/master/Java/Tree-字典树缩点优化.png) - -参考文章:https://time.geekbang.org/column/article/72414 +## API +### Object -*** +#### 基本介绍 +Object类是Java中的祖宗类,一个类或者默认继承Object类,或者间接继承Object类,Object类的方法是一切子类都可以直接使用 +Object类常用方法: -### 图 +* `public String toString()`: + 默认是返回当前对象在堆内存中的地址信息:类的全限名@内存地址,例:Student@735b478; + 直接输出对象名称,默认会调用toString()方法,所以省略toString()不写; + 如果输出对象的内容,需要重写toString()方法,toString方法存在的意义是为了被子类重写 +* `public boolean equals(Object o)`:默认是比较两个对象的引用是否相同 +* `protected Object clone()`:创建并返回此对象的副本 -图的邻接表形式: +只要两个对象的内容一样,就认为是相等的: ```java -public class AGraph { - private VertexNode[] adjList; //邻接数组 - private int vLen, eLen; //顶点数和边数 - - public AGraph(int vLen, int eLen) { - this.vLen = vLen; - this.eLen = eLen; - adjList = new VertexNode[vLen]; - } - //弧节点 - private class ArcNode { - int adjVex; //该边所指向的顶点的位置 - ArcNode nextArc; //下一条边(弧) - //int info //添加权值 - - public ArcNode(int adjVex) { - this.adjVex = adjVex; - nextArc = null; - } - } - - //表顶点 - private class VertexNode { - char data; //顶点信息 - ArcNode firstArc; //指向第一条边的指针 - - public VertexNode(char data) { - this.data = data; - firstArc = null; - } - } +public boolean equals(Object o) { + // 1.判断是否自己和自己比较,如果是同一个对象比较直接返回true + if (this == o) return true; + // 2.判断被比较者是否为null ,以及是否是学生类型。 + if (o == null || this.getClass() != o.getClass()) return false; + // 3.o一定是学生类型,强制转换成学生,开始比较内容! + Student student = (Student) o; + return age == student.age && + sex == student.sex && + Objects.equals(name, student.name); } ``` -图的邻接矩阵形式: +**面试题**:== 和equals的区别 -```java -public class MGraph { - private int[][] edges; //邻接矩阵定义,有权图将int改为float - private int vLen; //顶点数 - private int eLen; //边数 - private VertexNode[] vex; //存放节点信息 +* == 比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的**地址**是否相同,即是否是指相同一个对象,比较的是真正意义上的指针操作。 +* 重写equals方法比较的是两个对象的**内容**是否相等,所有的类都是继承自java.lang.Object类,所以适用于所有对象,如果**没有对该方法进行覆盖的话,调用的仍然是Object类中的方法,比较两个对象的引用** - public MGraph(int vLen, int eLen) { - this.vLen = vLen; - this.eLen = eLen; - this.edges = new int[vLen][vLen]; - this.vex = new VertexNode[vLen]; - } +hashCode的作用: - private class VertexNode { - int num; //顶点编号 - String info; //顶点信息 +* hashCode的存在主要是用于查找的快捷性,如Hashtable,HashMap等,可以在散列存储结构中确定对象的存储地址 +* 如果两个对象相同,就是适用于equals(java.lang.Object) 方法,那么这两个对象的hashCode一定要相同 +* 哈希值相同的数据不一定内容相同,内容相同的数据哈希值一定相同 - public VertexNode(int num) { - this.num = num; - this.info = null; - } - } -} -``` +*** -图相关的算法需要很多的流程图,此处不再一一列举,推荐参考书籍《数据结构高分笔记》 +#### 深浅克隆 -*** +深浅拷贝(克隆)的概念: +* 浅拷贝 (shallowCopy):对基本数据类型进行值传递,对引用数据类型只是复制了引用,被复制对象属性的所有的引用仍然指向原来的对象,简而言之就是增加了一个指针指向原来对象的内存地址 +* 深拷贝 (deepCopy):对基本数据类型进行值传递,对引用数据类型是一个整个独立的对象拷贝,会拷贝所有的属性并指向的动态分配的内存,简而言之就是把所有属性复制到一个新的内存,增加一个指针指向新内存。所以使用深拷贝的情况下,释放内存的时候不会出现使用浅拷贝时释放同一块内存的错误 -### 位图 +Object 的 clone() 是 protected 方法,一个类不显式去重写 clone(),就不能直接去调用该类实例的clone()方法 -#### 基本介绍 +克隆就是制造一个对象的副本,根据所要克隆的对象的成员变量中是否含有引用类型,可以将克隆分为两种:浅克隆(Shallow Clone) 和 深克隆(Deep Clone),默认情况下使用Object中的clone方法进行克隆就是浅克隆,即完成对象域对域的拷贝 -布隆过滤器:一种数据结构,是一个很长的二进制向量(位数组)和一系列随机映射函数(哈希函数),既然是二进制,每个空间存放的不是0就是1,但是初始默认值都是0,所以布隆过滤器不存数据只存状态 +Cloneable 接口是一个标识性接口,即该接口不包含任何方法(包括clone()),但是如果一个类想合法的进行克隆,那么就必须实现这个接口,在使用clone()方法时,若该类未实现 Cloneable 接口,则抛出异常 - +* Clone & Copy:`Student s = new Student` -这种数据结构是高效且性能很好的,但缺点是具有一定的错误识别率和删除难度。并且,理论情况下,添加到集合中的元素越多,误报的可能性就越大 + `Student s1 = s`:只是copy了一下reference,s和s1指向内存中同一个object,对对象的修改会影响对方 + `Student s2 = s.clone()`:会生成一个新的Student对象,并且和s具有相同的属性值和方法 +* Shallow Clone & Deep Clone: + + 浅克隆:Object中的clone()方法在对某个对象克隆时对其仅仅是简单地执行域对域的copy + + * 对基本数据类型和包装类的克隆是没有问题的。String、Integer等包装类型在内存中是不可以被改变的对象,所以在使用克隆时可以视为基本类型,只需浅克隆引用即可 + * 如果对一个引用类型进行克隆时只是克隆了它的引用,和原始对象共享对象成员变量 -*** + ![](https://gitee.com/seazean/images/raw/master/Java/Object浅克隆.jpg) + 深克隆:在对整个对象浅克隆后,对其引用变量进行克隆,并将其更新到浅克隆对象中去 + ```java + public class Student implements Cloneable{ + private String name; + private Integer age; + private Date date; + + @Override + protected Object clone() throws CloneNotSupportedException { + Student s = (Student) super.clone(); + s.date = (Date) date.clone(); + return s; + } + //..... + } + ``` -#### 工作流程 - -向布隆过滤器中添加一个元素key时,会通过多个hash函数得到多个哈希值,在位数组中把对应下标的值置为 1 - -![](https://gitee.com/seazean/images/raw/master/DB/Redis-布隆过滤器添加数据.png) - -布隆过滤器查询一个数据,是否在二进制的集合中,查询过程如下: - -- 通过 K 个哈希函数计算该数据,对应计算出的 K 个hash值 -- 通过 hash 值找到对应的二进制的数组下标 -- 判断方法:如果存在一处位置的二进制数据是0,那么该数据不存在。如果都是1,该数据存在集合中 - -布隆过滤器优缺点: - -* 优点: - * 二进制组成的数组,占用内存极少,并且插入和查询速度都足够快 - * 去重方便:当字符串第一次存储时对应的位数组下标设置为 1,当第二次存储相同字符串时,因为对应位置已设置为 1,所以很容易知道此值已经存在 -* 缺点: - * 随着数据的增加,误判率会增加:添加数据是通过计算数据的hash值,不同的字符串可能哈希出来的位置相同,导致无法确定到底是哪个数据存在,**这种情况可以适当增加位数组大小或者调整哈希函数** - * 无法删除数据:可能存在几个数据占据相同的位置,所以删除一位会导致很多数据失效 +SDP → 创建型 → 原型模式 -* 总结:**布隆过滤器判断某个元素存在,小概率会误判。如果判断某个元素不在,那这个元素一定不在** +*** -参考文章:https://www.cnblogs.com/ysocean/p/12594982.html +### Objects -*** +Objects 类与 Object 是继承关系。 +Objects的方法: +* `public static boolean equals(Object a, Object b)` : 比较两个对象是否相同。 + 底层进行非空判断,从而可以避免空指针异常,更安全!!推荐使用!! -#### Guava + ```java + public static boolean equals(Object a, Object b) { + return a == b || a != null && a.equals(b); + } + ``` -引入 Guava 的依赖: +* `public static boolean isNull(Object obj)` : 判断变量是否为null ,为null返回true ,反之! -```xml - - com.google.guava - guava - 28.0-jre - -``` +* `public static String toString(对象)` : 返回参数中对象的字符串表示形式 -指定误判率为(0.01): +* `public static String toString(对象, 默认字符串)` : 返回对象的字符串表示形式。 ```java -public static void main(String[] args) { - // 创建布隆过滤器对象 - BloomFilter filter = BloomFilter.create( - Funnels.integerFunnel(), - 1500, - 0.01); - // 判断指定元素是否存在 - System.out.println(filter.mightContain(1)); - System.out.println(filter.mightContain(2)); - // 将元素添加进布隆过滤器 - filter.put(1); - filter.put(2); - System.out.println(filter.mightContain(1)); - System.out.println(filter.mightContain(2)); +public class ObjectsDemo { + public static void main(String[] args) { + Student s1 = null; + Student s2 = new Student(); + System.out.println(Objects.equals(s1 , s2));//推荐使用 + // System.out.println(s1.equals(s2)); // 空指针异常 + + System.out.println(Objects.isNull(s1)); + System.out.println(s1 == null);//直接判断比较好 + } +} + +public class Student { } ``` @@ -2758,101 +2408,105 @@ public static void main(String[] args) { -#### 实现布隆 +### String + +#### 基本介绍 + +**String 被声明为 final,因此不可被继承 (Integer 等包装类也不能被继承)** ```java -class MyBloomFilter { - //布隆过滤器容量 - private static final int DEFAULT_SIZE = 2 << 28; - //bit数组,用来存放key - private static BitSet bitSet = new BitSet(DEFAULT_SIZE); - //后面hash函数会用到,用来生成不同的hash值,随意设置 - private static final int[] ints = {1, 6, 16, 38, 58, 68}; +public final class String implements java.io.Serializable, Comparable, CharSequence { + /** The value is used for character storage. */ + private final byte[] value; + /** The identifier of the encoding used to encode the bytes in {@code value}. */ + private final byte coder; +} +``` - //add方法,计算出key的hash值,并将对应下标置为true - public void add(Object key) { - Arrays.stream(ints).forEach(i -> bitSet.set(hash(key, i))); - } +在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 `coder` 来标识使用了哪种编码 - //判断key是否存在,true不一定说明key存在,但是false一定说明不存在 - public boolean isContain(Object key) { - boolean result = true; - for (int i : ints) { - //短路与,只要有一个bit位为false,则返回false - result = result && bitSet.get(hash(key, i)); - } - return result; - } +value 数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组,并且 String 内部没有改变 value 数组的方法,因此可以**保证 String 不可变,也保证线程安全** - //hash函数,借鉴了hashmap的扰动算法 - private int hash(Object key, int i) { - int h; - return key == null ? 0 : (i * (DEFAULT_SIZE - 1) & ((h = key.hashCode()) ^ (h >>> 16))); - } -} +**注意:不能改变的意思是每次更改字符串都会产生新的对象,并不是对原始对象进行改变** +```java +String s = "abc"; +s = s + "cd"; //s = abccd 新对象 ``` +**** +#### 构造方式 -*** - +构造方法: + `public String()` : 创建一个空白字符串对象,不含有任何内容 + `public String(char[] chs)` : 根据字符数组的内容,来创建字符串对象 + `public String(String original)` : 根据传入的字符串内容,来创建字符串对象 +直接赋值:`String s = “abc”` 直接赋值的方式创建字符串对象,内容就是abc +- 通过构造方法创建:通过 new 创建的字符串对象,每一次 new 都会申请一个内存空间,虽然内容相同,但是地址值不同,**返回堆内存中对象的引用** +- 直接赋值方式创建:以“ ”方式给出的字符串,只要字符序列相同(顺序和大小写),无论在程序代码中出现几次,JVM 都只会**在 String Pool 中创建一个字符串对象**,并在字符串池中维护 +`String str = new String("abc")`创建字符串对象: -## 对象 +* 创建一个对象:字符串池中已经存在"abc"对象,那么直接在创建一个对象放入堆中,返回str引用 +* 创建两个对象:字符串池中未找到"abc"对象,那么分别在堆中和字符串池中创建一个对象,字符串池中的比较都是采用equals()方法 + -### 概述 +`new String("a") + new String("b")`创建字符串对象: -**Java是一种面向对象的高级编程语言。** +* 对象1:new StringBuilder() -**三大特征:封装,继承,多态** +* 对象2:new String("a")、对象3:常量池中的"a" -面向对象最重要的两个概念:类和对象。 +* 对象4:new String("b")、对象5:常量池中的"b" + -* 类:相同事物共同特征的描述。类只是学术上的一个概念并非真实存在的,只能描述一类事物。 -* 对象:是真实存在的实例, 实例==对象。**对象是类的实例化**! -* 结论:有了类和对象就可以描述万千世界所有的事物。 必须先有类才能有对象。 +* StringBuilder的toString(): + ```java + @Override + public String toString() { + return new String(value, 0, count); + } + ``` + * 对象6:new String("ab") + * StringBuilder 的 toString() 调用,在字符串常量池中没有生成"ab",new String("ab") 会创建两个对象因为传参数的时候使用字面量创建了对象 “ab ”,当使用数组构造 String 对象时,没有加入常量池的操作 -*** +*** -### 类 -#### 定义 -定义格式 -```java -修饰符 class 类名{ -} -``` +#### 常用方法 -1. 类名的首字母建议大写,满足驼峰模式,比如 StudentNameCode -2. 一个 Java 代码中可以定义多个类,按照规范一个 Java 文件一个类 -3. 一个 Java 代码文件中,只能有一个类是 public 修饰,**public修饰的类名必须成为当前Java代码的文件名称** +`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 beginIndex, int endIndex)` : 返回原字符串指定索引处的字符串 +`public String toLowerCase()` : 将此String所有字符转换为小写,使用默认语言环境的规则 +`public String toUpperCase()` : 使用默认语言环境的规则将此String所有字符转换为大写 +`public String replace(CharSequence target, CharSequence replacement)` : 使用新值,将字符串中的旧值替换,得到新的字符串 ```java -类中的成分:有且仅有五大成分 -修饰符 class 类名{ - 1.成员变量(Field): 描述类或者对象的属性信息的。 - 2.成员方法(Method): 描述类或者对象的行为信息的。 - 3.构造器(Constructor): 初始化一个对象返回。 - 4.代码块 - 5.内部类 - } -类中有且仅有这五种成分,否则代码报错! -public class ClassDemo { - System.out.println(1);//报错 -} +String s = 123-78; +s.replace("-","");//12378 ``` @@ -2861,60 +2515,115 @@ public class ClassDemo { -#### 构造器 +#### String Pool -构造器格式: +##### 基本介绍 -```java -修饰符 类名(形参列表){ +**字符串常量池(String Pool / StringTable / 串池)**保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定,常量池类似于Java系统级别提供的**缓存**,存放对象和引用 -} -``` +* StringTable,hashtable (哈希表 + 链表)结构,不能扩容,默认值大小长度是1009 -作用:初始化类的一个对象返回 +* 常量池中的字符串仅是符号,第一次使用时才变为对象,可以避免重复创建字符串对象 +* 字符串**变量**的拼接的原理是StringBuilder(jdk1.8),append效率要比字符串拼接高很多 +* 字符串**常量**拼接的原理是编译期优化,结果在常量池 +* 可以使用 String 的 intern() 方法在运行过程将字符串添加到 String Pool 中 -分类:无参数构造器,有参数构造器 + **intern()** : -注意:**一个类默认自带一个无参数构造器**,写了有参数构造器默认的无参数构造器就消失,还需要用无参数构造器就要重新写 +* jdk1.8:当一个字符串调用 intern() 方法时,如果 String Pool 中: + * 存在一个字符串和该字符串值相等(使用 equals() 方法进行确定),就会返回 String Pool 中字符串的引用(需要变量接收) + * 不存在,会把对象的引用地址复制一份放入串池,并返回串池中的引用地址,前提是堆内存有该对象。因为 Pool 在堆中,为了节省内存不再创建新对象 +* jdk1.6:将这个字符串对象尝试放入串池,如果有就不放入,返回已有的串池中的对象的地址;如果没有会把此对象复制一份,放入串池,把串池中的对象返回 -构造器初始化对象的格式:类名 对象名称 = new 构造器 +```java +public class Demo { +//常量池中的信息都加载到运行时常量池,这时a b ab是常量池中的符号,还不是java字符串对象,是懒惰的 + // ldc #2 会把 a 符号变为 "a" 字符串对象 ldc:反编译后的指令 + // ldc #3 会把 b 符号变为 "b" 字符串对象 + // ldc #4 会把 ab 符号变为 "ab" 字符串对象 + public static void main(String[] args) { + String s1 = "a"; // 懒惰的 + String s2 = "b"; + String s3 = "ab";//串池 + // new StringBuilder().append("a").append("b").toString() new String("ab") + String s4 = s1 + s2; //d + 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()方法 + x2.intern(); + String x1 = "cd"; + System.out.println(x1 == x2); //true + } +} +``` ------- +- == 比较基本数据类型:比较的是具体的值 +- == 比较引用数据类型:比较的是对象地址值 +面试问题: +```java +String s1 = "ab"; //串池 +String s2 = new String("a") + new String("b"); //堆 +//上面两条指令的结果和下面的效果相同 +String s = new String("ab"); +``` -### 包 +```java +public static void main(String[] args) { + String s = new String("a") + new String("b");//new String("ab") + //在上一行代码执行完以后,字符串常量池中并没有"ab" -包:分门别类的管理各种不同的技术,便于管理技术,扩展技术,阅读技术。 + String s2 = s.intern(); + //jdk6:串池中创建一个字符串"ab" + //jdk8:串池中没有创建字符串"ab",而是创建一个引用指向new String("ab"),将此引用返回 -定义包的格式:`package 包名; `,必须放在类名的最上面。 + System.out.println(s2 == "ab");//jdk6:true jdk8:true + System.out.println(s == "ab");//jdk6:false jdk8:true +} +``` -导包格式:`import 包名.类名;` -相同包下的类可以直接访问;不同包下的类必须导包才可以使用 +*** -*** +##### 内存位置 +Java 7之前,String Pool 被放在运行时常量池中,它属于永久代;Java 7以后,String Pool 被移到堆中,这是因为永久代的空间有限,在大量使用字符串的场景下会导致OutOfMemoryError 错误 -### 封装 +演示 StringTable 位置: -封装的哲学思维:合理隐藏,合理暴露 -封装最初的目的:提高代码的安全性和复用性,组件化 +* `-Xmx10m`设置堆内存10m -封装的步骤: +* 在jdk8下设置: `-Xmx10m -XX:-UseGCOverheadLimit`(运行参数在Run Configurations VM options) -1. **成员变量应该私有,用private修饰,只能在本类中直接访问** -2. **提供成套的getter和setter方法暴露成员变量的取值和赋值** +* 在jdk6下设置: `-XX:MaxPermSize=10m` -为什么使用private修饰成员变量:实现数据封装,不想让别人使用修改你的数据,比较安全 + ```java + public static void main(String[] args) throws InterruptedException { + List list = new ArrayList(); + int i = 0; + try { + for (int j = 0; j < 260000; j++) { + list.add(String.valueOf(j).intern()); + i++; + } + } catch (Throwable e) { + e.printStackTrace(); + } finally { + System.out.println(i); + } + } + ``` + +![](https://gitee.com/seazean/images/raw/master/Java/JVM-内存图对比.png) @@ -2922,213 +2631,226 @@ public class ClassDemo { -### this +#### 优化常量池 -this关键字的作用: +两种方式: -* this关键字代表了当前对象的引用 -* this出现在方法中:**哪个对象调用这个方法this就代表谁** -* this可以出现在构造器中:代表构造器正在初始化的那个对象 -* this可以区分变量是访问的成员变量还是局部变量 +* 调整 -XX:StringTableSize=桶个数,数量越少,性能越差 +* intern 将字符串对象放入常量池,通过复用字符串的引用,减少内存占用 +```java +/** + * 演示 intern 减少内存占用 + * -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics + * -Xsx500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=200000 + */ +public class Demo1_25 { + public static void main(String[] args) throws IOException { + List address = new ArrayList<>(); + System.in.read(); + for (int i = 0; i < 10; i++) { + //很多数据 + try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) { + String line = null; + long start = System.nanoTime(); + while (true) { + line = reader.readLine(); + if(line == null) { + break; + } + address.add(line.intern()); + } + System.out.println("cost:" +(System.nanoTime()-start)/1000000); + } + } + System.in.read(); + } +} +``` ------- +*** -### static -#### 基本介绍 -Java是通过成员变量是否有static修饰来区分是类的还是属于对象的。 +#### 不可变好处 -static 静态修饰的成员(方法和成员变量)属于类本身的。 +* 可以缓存 hash 值 + 因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,只需要进行一次计算 +* String Pool 的需要 + 如果一个String对象已经被创建过了,就会从 String Pool中取得引用。只有 String是不可变的,才可能使用 String Pool +* 安全性 + String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 的那一方以为现在连接的是其它主机,而实际情况却不一定是 +* String 不可变性天生具备线程安全,可以在多个线程中安全地使用 +* 防止子类继承,破坏String的API的使用 -按照有无static修饰,成员变量和方法可以分为: -* 成员变量: - * 静态成员变量(类变量): - 有static修饰的成员变量称为静态成员变量也叫类变量,属于类本身,**与类一起加载一次,只有一个**,直接用类名访问即可。 - * 实例成员变量: - 无static修饰的成员变量称为实例成员变量,属于类的每个对象的。**与类的对象一起加载**,对象有多少个,实例成员变量就加载多少个,必须用类的对象来访问。 -* 成员方法: - * 静态方法: - 有static修饰的成员方法称为静态方法也叫类方法,属于类本身的,直接用类名访问即可。 - * 实例方法: - 无static修饰的成员方法称为实例方法,属于类的每个对象的,必须用类的对象来访问。 +*** -**** +### StringBuilder -#### static用法 +String StringBuffer 和 StringBuilder 区别: -成员变量的访问语法: +* String : **不可变**的字符序列,线程安全 +* StringBuffer : **可变**的字符序列,线程安全,底层方法加 synchronized,效率低 +* StringBuilder : **可变**的字符序列,JDK5.0新增;线程不安全,效率高 -* 静态成员变量:只有一份可以被类和类的对象**共享访问** - * 类名.静态成员变量(同一个类中访问静态成员变量可以省略类名不写) - * 对象.静态成员变量(不推荐) +相同点:底层使用 char[] 存储 -* 实例成员变量: - * 对象.实例成员变量(先创建对象) +构造方法: -成员方法的访问语法: +* `public StringBuilder()`:创建一个空白可变字符串对象,不含有任何内容 +* `public StringBuilder(String str)`:根据字符串的内容,来创建可变字符串对象 -* 静态方法:有static修饰,属于类 +常用API : - * 类名.静态方法(同一个类中访问静态成员可以省略类名不写) - * 对象.静态方法(不推荐,参考 JVM类加载--> 字节码 --> 方法调用) - -* 实例方法:无static修饰,属于对象 +* `public StringBuilder append(任意类型)`:添加数据,并返回对象本身 +* `public StringBuilder reverse()`:返回相反的字符序列 +* `public String toString()`:通过 toString() 就可以实现把 StringBuilder 转换为 String - * 对象.实例方法 - - ```java - public class Student { - // 1.静态方法:有static修饰,属于类,直接用类名访问即可! - public static void inAddr(){ } - // 2.实例方法:无static修饰,属于对象,必须用对象访问! - public void eat(){} - - public static void main(String[] args) { - // a.类名.静态方法 - Student.inAddr(); - inAddr(); - // b.对象.实例方法 - // Student.eat(); // 报错了! - Student zbj = new Student(); - zbj.eat(); - } - } - ``` +存储原理: +```java +String str = "abc"; +char data[] = {'a', 'b', 'c'}; +StringBuffer sb1 = new StringBuffer();//new byte[16] +sb1.append('a'); //value[0] = 'a'; +``` +append 源码: -*** +```java +public AbstractStringBuilder append(String str) { + if (str == null) return appendNull(); + int len = str.length(); + ensureCapacityInternal(count + len); + str.getChars(0, len, value, count); + count += len; + return this; +} +private void ensureCapacityInternal(int minimumCapacity) { + // 创建超过数组长度就新的char数组,把数据拷贝过去 + if (minimumCapacity - value.length > 0) { + //int newCapacity = (value.length << 1) + 2;每次扩容2倍+2 + value = Arrays.copyOf(value, newCapacity(minimumCapacity)); + } +} + public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) { + //将字符串中的字符复制到目标字符数组中 + System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin); +} +``` -#### 两个问题 -内存问题: -* **栈内存存放main方法和地址** +**** -* **堆内存存放对象和变量** -* **方法区存放class和静态变量(jdk8以后移入堆)** -访问问题: +### Arrays -​ a.实例方法是否可以直接访问实例成员变量?可以的,因为它们都属于对象。 -​ b.实例方法是否可以直接访问静态成员变量?可以的,静态成员变量可以被共享访问。 -​ c.实例方法是否可以直接访问实例方法? 可以的,实例方法和实例方法都属于对象。 -​ d.实例方法是否可以直接访问静态方法?可以的,静态方法可以被共享访问! +Array 的工具类 -​ a.静态方法是否可以直接访问实例变量? 不可以的,实例变量必须用对象访问!! -​ b.静态方法是否可以直接访问静态变量? 可以的,静态成员变量可以被共享访问。 -​ c.静态方法是否可以直接访问实例方法? 不可以的,实例方法必须用对象访问!! -​ d.静态方法是否可以直接访问静态方法?可以的,静态方法可以被共享访问!! +常用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)` : 返回由指定数组支持的列表。 +```java +public class MyArraysDemo { + public static void main(String[] args) { + //按照数字顺序排列指定的数组 + int [] arr = {3,2,4,6,7}; + Arrays.sort(arr); + System.out.println(Arrays.toString(arr)); + + int [] arr = {1,2,3,4,5,6,7,8,9,10}; + int index = Arrays.binarySearch(arr, 0); + System.out.println(index); + //1,数组必须有序 + //2.如果要查找的元素存在,那么返回的是这个元素实际的索引 + //3.如果要查找的元素不存在,那么返回的是 (-插入点-1) + //插入点:如果这个元素在数组中,他应该在哪个索引上. + } + } +``` ------- -### 继承 -#### 基本介绍 +*** -继承是Java中一般到特殊的关系,是一种子类到父类的关系。 -* 被继承的类称为:父类/超类。 -* 继承父类的类称为:子类。 -继承的作用: +### Random -* **提高代码的复用**,相同代码可以定义在父类中 -* 子类继承父类,可以直接使用父类这些代码(相同代码重复利用) -* 子类得到父类的属性(成员变量)和行为(方法),还可以定义自己的功能,子类更强大 +用于生成伪随机数。 -继承的特点: +使用步骤: +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. 子类的全部构造器默认先访问父类的无参数构造器,再执行自己的构造器 -2. **单继承**:一个类只能继承一个直接父类 -3. 多层继承:一个类可以间接继承多个父类(家谱) -4. 一个类可以有多个子类 -5. 一个类要么默认继承了Object类,要么间接继承了Object类,Object类是Java中的祖宗类 - -继承的格式: - -```java -子类 extends 父类{ +4. 随机小数:`public double nextDouble()`从范围`0.0d`(含)至`1.0d` (不包括),伪随机地生成并返回 -} -``` -子类继承父类的东西: -* 子类不能继承父类的构造器,子类有自己的构造器 -* 子类是不能可以继承父类的私有成员的,可以反射暴力去访问继承自父类的私有成员 -* 子类是不能继承父类的静态成员的,子类只是可以访问父类的静态成员,父类静态成员只有一份可以被子类共享访问,**共享并非继承** +*** -```java -public class ExtendsDemo { - public static void main(String[] args) { - Cat c = new Cat(); - // c.run(); - Cat.test(); - System.out.println(Cat.schoolName); - } -} -class Cat extends Animal{ -} -class Animal{ - public static String schoolName ="seazean"; - public static void test(){} - private void run(){} -} -``` +### Date -*** +构造器: +* `public Date()`:创建当前系统的此刻日期时间对象。 +* `public Date(long time)`:把时间毫秒值转换成日期对象 +方法: -#### 继承访问 +* `public long getTime()`:返回自 1970 年 1 月 1 日 00:00:00 GMT 以来总的毫秒数。 -继承后成员变量的访问特点:**就近原则**,子类有找子类,子类没有找父类,父类没有就报错! +时间记录的两种方式: -如果要申明访问父类的成员变量可以使用:super.父类成员变量。super指父类引用 +1. Date日期对象 +2. 时间毫秒值:从1970-01-01 00:00:00开始走到此刻的总的毫秒值。 1s = 1000ms ```java -public class ExtendsDemo { - public static void wmain(String[] args) { - Wolf w = new Wolf();w - w.showName(); - } -} -class Wolf extends Animal{ - private String name = "子类狼"; - public void showName(){ - String name = "局部名称"; - System.out.println(name); // 局部name - System.out.println(this.name); // 子类对象的name - System.out.println(super.name); // 父类的 - System.out.println(name1); // 父类的 - //System.out.println(name2); // 报错。子类父类都没有 +public class DateDemo { + public static void main(String[] args) { + Date d = new Date(); + System.out.println(d);//Fri Oct 16 21:58:44 CST 2020 + long time = d.getTime() + 121*1000;//过121s是什么时间 + System.out.println(time);//1602856875485 + + Date d1 = new Date(time); + System.out.println(d1);//Fri Oct 16 22:01:15 CST 2020 } } +``` -class Animal{ - public String name = "父类动物名称"; - public String name1 = "父类"; +```java +public static void main(String[] args){ + Date d = new Date(); + long startTime = d.getTime(); + for(int i = 0; i < 10000; i++){输出i} + long endTime = new Date().getTime(); + System.out.println( (endTime - startTime) / 1000.0 +"s"); + //运行一万次输出需要多长时间 } ``` @@ -3138,244 +2860,173 @@ class Animal{ -#### 方法重写 - -方法重写:子类继承了父类就得到了父类的方法,子类重写一个与父类申明一样的方法来**覆盖**父类的该方法 +### DateFormat -方法重写的校验注解:@Override +DateFormat 作用: -* 方法加了这个注解,那就必须是成功重写父类的方法,否则报错 -* @Override优势:可读性好,安全,优雅 +1. 可以把“日期对象”或者“时间毫秒值”格式化成我们喜欢的时间形式(格式化时间) +2. 可以把字符串的时间形式解析成日期对象(解析字符串时间) -子类可以扩展父类的功能,但不能改变父类原有的功能,重写有以下**三个限制**: +DateFormat 是一个抽象类,不能直接使用,使用它的子类:SimpleDateFormat -- 子类方法的访问权限必须大于等于父类方法 -- 子类方法的返回类型必须是父类方法返回类型或为其子类型 -- 子类方法抛出的异常类型必须是父类抛出异常类型或为其子类型 +SimpleDateFormat 简单日期格式化类: -继承中的隐藏问题: +* `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" 周几 上午下午 ```java -public class ExtendsDemo { - public static void main(String[] args) { - Wolf w = new Wolf(); - w.run(); - } -} -class Wolf extends Animal{ - @Override - public void run(){}// -} -class Animal{ - public void run(){} +public static void main(String[] args){ + Date date = new Date(); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss); + String time = sdf.format(date); + System.out.println(time);//2020-10-18 19:58:34 + //过121s后是什么时间 + long time = date.getTime(); + time+=121; + System.out.println(sdf.formate(time)); + String d = "2020-10-18 20:20:20";//格式一致 + Date newDate = sdf.parse(d); + System.out.println(sdf.format(newDate)); //按照前面的方法输出 } ``` -*** +**** -#### 面试问题 -* 为什么子类构造器会先调用父类构造器? - - 1. 子类的构造器的第一行默认super()调用父类的无参数构造器,写不写都存在 - 2. 子类继承父类,子类就得到了父类的属性和行为。调用子类构造器初始化子类对象数据时,必须先调用父类构造器初始化继承自父类的属性和行为 - 3. 参考JVM -> 类加载 -> 对象创建 - - ```java - class Animal{ - public Animal(){ - System.out.println("==父类Animal的无参数构造器=="); - } - } - class Tiger extends Animal{ - public Tiger(){ - super(); // 默认存在的,根据参数去匹配调用父类的构造器。 - System.out.println("==子类Tiger的无参数构造器=="); - } - public Tiger(String name){ - //super(); 默认存在的,根据参数去匹配调用父类的构造器。 - System.out.println("==子类Tiger的有参数构造器=="); - } - } - ``` - - - -* **为什么Java是单继承的?** - 答:反证法,假如Java可以多继承,请看如下代码: - 补充:多实现是在实现接口时,重名方法需要实现类来实现 +### Calendar - ```java - class A{ - public void test(){ - System.out.println("A"); - } - } - class B{ - public void test(){ - System.out.println("B"); - } - } - class C extends A , B { - public static void main(String[] args){ - C c = new C(); - c.test(); - // 出现了类的二义性!所以Java不能多继承!! - } - } - ``` +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()`: 拿到此刻时间毫秒值 - +```java +public static void main(String[] args){ + Calendar rightNow = Calendar.getInsance(); + int year = rightNow.get(Calendar.YEAR);//获取年 + int month = rightNow.get(Calendar.MONTH) + 1;//月要+1 + int days = rightNow.get(Calendar.DAY_OF_YEAR); + rightNow.set(Calendar.YEAR , 2099);//修改某个字段 + rightNow.add(Calendar.HOUR , 15);//加15小时 -15就是减去15小时 + Date date = rightNow.getTime();//日历对象 + long time = rightNow.getTimeInMillis();//时间毫秒值 + //700天后是什么日子 + rightNow.add(Calendar.DAY_OF_YEAR , 701); + Date date d = rightNow.getTime(); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + System.out.println(sdf.format(d));//输出700天后的日期 +} +``` ------- +*** -### super +### LocalDateTime -继承后super调用父类构造器,父类构造器初始化继承自父类的数据。 +JDK1.8 新增,线程安全 ++ LocalDate 表示日期(年月日) ++ LocalTime 表示时间(时分秒) ++ LocalDateTime 表示时间+ 日期 (年月日时分秒) -总结与拓展: +构造方法: -* this代表了当前对象的引用(继承中指代子类对象):this.子类成员变量、this.子类成员方法、**this(...)**可以根据参数匹配访问本类其他构造器。 -* super代表了父类对象的引用(继承中指代了父类对象空间):super.父类成员变量、super.父类的成员方法、super(...)可以根据参数匹配访问父类的构造器 +* public static LocalDateTime now():获取当前系统时间 +* public static LocalDateTime of(年, 月 , 日, 时, 分, 秒):使用指定年月日和时分秒初始化一个对象 -**注意:** +常用API: -* this(...)借用本类其他构造器,super(...)调用父类的构造器。 -* this(...)或super(...)必须放在构造器的第一行,否则报错! -* this(...)和super(...)不能同时出现在构造器中,因为构造函数必须出现在第一行上,只能选择一个。 +| 方法名 | 说明 | +| --------------------------------------------------------- | ----------------------------------------------------------- | +| 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 ThisDemo { +public class JDK8DateDemo2 { public static void main(String[] args) { - // 需求:希望如果不写学校默认就是”张三“! - Student s1 = new Student("天蓬元帅", 1000 ); - Student s2 = new Student("齐天大圣", 2000, "清华大学" ); + LocalDateTime now = LocalDateTime.now(); + System.out.println(now); + + LocalDateTime localDateTime = LocalDateTime.of(2020, 11, 11, 11, 11, 11); + System.out.println(localDateTime); + DateTimeFormatter pattern = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss"); + String s = localDateTime.format(pattern); + LocalDateTime parse = LocalDateTime.parse(s, pattern); } } -class Study extends Student { - public Study(String name, int age, String schoolName) { - super(name , age , schoolName) ; - // 根据参数匹配调用父类构造器 - } -} - -class Student{ - private String name ; - private int age ; - private String schoolName ; - - public Student() { - } - public Student(String name , int age){ - // 借用兄弟构造器的功能! - this(name , age , "张三"); - } - public Student(String name, int age, String schoolName) { - this.name = name; - this.age = age; - this.schoolName = schoolName; - } -// .......get + set -} -``` - - - -*** - - - -### final - -#### 基本介绍 - -final用于修饰:类,方法,变量 - -* final修饰类,类不能被继承了,类中的方法和变量可以使用 -* final可以修饰方法,方法就不能被重写 -* final修饰变量总规则:变量有且仅能被赋值一次 - -**面试题**:final和abstract的关系? - 互斥关系,不能同时修饰类或者同时修饰方法!! - - - -*** - - - -#### 修饰变量 - -##### 静态成员变量 - -final修饰静态成员变量,变量变成了常量 - -**常量:有public static final修饰,名称字母全部大写,多个单词用下划线连接。** - -final修饰静态成员变量可以在哪些地方赋值: - -1. 定义的时候赋值一次 - -2. 可以在静态代码块中赋值一次 - -```java -public class FinalDemo { -//常量:public static final修饰,名称字母全部大写,下划线连接。 - public static final String SCHOOL_NAME = "张三" ; - public static final String SCHOOL_NAME1; - - static{ - //SCHOOL_NAME = "java";//报错 - SCHOOL_NAME1 = "张三1"; - //SCHOOL_NAME1 = "张三2"; // 报错,第二次赋值! - } -} -``` - +``` +| 方法名 | 说明 | +| --------------------------------------------------- | ------------------------------ | +| 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) | 直接修改秒 | -##### 实例成员变量 -final修饰变量的总规则:有且仅能被赋值一次 -final修饰实例成员变量可以在哪些地方赋值1次: +**时间间隔** Duration 类API: -1. 定义的时候赋值一次 -2. 可以在实例代码块中赋值一次 -3. 可以在每个构造器中赋值一次 +| 方法名 | 说明 | +| ------------------------------------------------ | -------------------- | +| public static Period between(开始时间,结束时间) | 计算两个“时间"的间隔 | +| public int getYears() | 获得这段时间的年数 | +| public int getMonths() | 获得此期间的总月数 | +| public int getDays() | 获得此期间的天数 | +| public long toTotalMonths() | 获取此期间的总月数 | +| public static Durationbetween(开始时间,结束时间) | 计算两个“时间"的间隔 | +| public long toSeconds() | 获得此时间间隔的秒 | +| public long toMillis() | 获得此时间间隔的毫秒 | +| public long toNanos() | 获得此时间间隔的纳秒 | ```java -public class FinalDemo { - private final String name = "张三" ; - private final String name1; - private final String name2; - { - // 可以在实例代码块中赋值一次。 - name1 = "张三1"; - } - //构造器赋值一次 - public FinalDemo(){ - name2 = "张三2"; - } - public FinalDemo(String a){ - name2 = "张三2"; - } - +public class JDK8DateDemo9 { public static void main(String[] args) { - FinalDemo f1 = new FinalDemo(); - //f1.name = "张三1"; // 第二次赋值 报错! + LocalDate localDate1 = LocalDate.of(2020, 1, 1); + LocalDate localDate2 = LocalDate.of(2048, 12, 12); + Period period = Period.between(localDate1, localDate2); + System.out.println(period);//P28Y11M11D + Duration duration = Duration.between(localDateTime1, localDateTime2); + System.out.println(duration);//PT21H57M58S } } ``` @@ -3386,9945 +3037,578 @@ public class FinalDemo { -### 抽象类 - -#### 基本介绍 - -> 父类知道子类要完成某个功能,但是每个子类实现情况不一样。 +### Math -抽象方法:没有方法体,只有方法签名,必须用**abstract**修饰的方法就是抽象方法 -抽象类:拥有抽象方法的类必须定义成抽象类,必须用**abstract**修饰,抽象类是为了被继承 +Math用于做数学运算。 +Math类中的方法全部是静态方法,直接用类名调用即可。 +方法: -一个类继承抽象类,**必须重写抽象类的全部抽象方法**,否则这个类必须定义成抽象类,因为拥有抽象方法的类必须定义成抽象类 +| 方法 | 说明 | +| -------------------------------------------- | --------------------------------- | +| public static int abs(int a) | 获取参数a的绝对值 | +| public static double ceil(double a) | 向上取整 | +| public static double floor(double a) | 向下取整 | +| public static double pow(double a, double b) | 获取 a 的 b 次幂 | +| public static long round(double a) | 四舍五入取整 | +| public static int max(int a,int b) | 返回较大值 | +| public static int min(int a,int b) | 返回较小值 | +| public static double random() | 返回值为 double 的正值,[0.0,1.0) | ```java -public class AbstractDemo { +public class MathDemo { public static void main(String[] args) { - Dog d = new Dog(); - d.run(); - } -} - -class Dog extends Animal{ - @Override - public void run() { - System.out.println("🐕跑"); + // 1.取绝对值:返回正数。 + System.out.println(Math.abs(10)); + System.out.println(Math.abs(-10.3)); + // 2.向上取整: 5 + System.out.println(Math.ceil(4.00000001)); // 5.0 + System.out.println(Math.ceil(-4.00000001));//4.0 + // 3.向下取整:4 + System.out.println(Math.floor(4.99999999)); // 4.0 + System.out.println(Math.floor(-4.99999999)); // 5.0 + // 4.求指数次方 + System.out.println(Math.pow(2 , 3)); // 2^3 = 8.0 + // 5.四舍五入 10 + System.out.println(Math.round(4.49999)); // 4 + System.out.println(Math.round(4.500001)); // 5 + System.out.println(Math.round(5.5));//6 } } - -abstract class Animal{ - public abstract void run(); -} ``` -*** - - - -#### 面试问题 - -一、抽象类是否有构造器,是否可以创建对象,为什么? -答:抽象类作为类一定有构造器,而且必须有构造器,提供给子类继承后调用父类构造器使用的 - -1、抽象类有构造器,但是抽象类不能创建对象,类的其他成分它都具备 -2、抽象类中存在抽象方法,但不能执行,**抽象类中也可没有抽象方法** +### DecimalFormat -> 抽象在学术上本身意味着不能实例化 +使任何形式的数字解析和格式化 ```java -public class AbstractDemo { - public static void main(String[] args) { - //Animal a = new Animal(); 抽象类不能创建对象! - //a.run(); // 抽象方法不能执行 - } -} -abstract class Animal{ - private String name; - public static String schoolName = "张三"; - public Animal(){ } - - public abstract void run(); - //普通方法 - public void go(){ } -} +public static void main(String[]args){ +    double pi = 3.1415927; //圆周率 +    //取一位整数 +    System.out.println(new DecimalFormat("0").format(pi));   //3 +    //取一位整数和两位小数 +    System.out.println(new DecimalFormat("0.00").format(pi)); //3.14 +    //取两位整数和三位小数,整数不足部分以0填补。 +    System.out.println(new DecimalFormat("00.000").format(pi));// 03.142 +    //取所有整数部分 +    System.out.println(new DecimalFormat("#").format(pi));   //3 +    //以百分比方式计数,并取两位小数 +    System.out.println(new DecimalFormat("#.##%").format(pi)); //314.16% + +     long c =299792458;  //光速 +    //显示为科学计数法,并取五位小数 +    System.out.println(new DecimalFormat("#.#####E0").format(c));//2.99792E8 +    //显示为两位整数的科学计数法,并取四位小数 +    System.out.println(new DecimalFormat("00.####E0").format(c));//29.9792E7 +    //每三位以逗号进行分隔。 +    System.out.println(new DecimalFormat(",###").format(c));//299,792,458 +    //将格式嵌入文本 +    System.out.println(new DecimalFormat("光速大小为每秒,###米。").format(c)); + +  } ``` -二、static与abstract能同时使用吗? -答:不能,被static修饰的方法属于类,是类自己的东西,不是给子类来继承的,而抽象方法本身没有实现,就是用来给子类继承 - - - *** -#### 存在意义 +### 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 ExtendsDemo { +public class SystemDemo { public static void main(String[] args) { - Student xiaoMa = new Student(); - xiaoMa.write(); - } -} -class Student extends Template{ - @Override - public String writeText() {return "\t内容"} -} -// 1.写一个模板类:代表了作文模板。 -abstract class Template{ - private String title = "\t\t\t\t\t标题"; - private String start = "\t开头"; - private String last = "\t结尾"; - public void write(){ - System.out.println(title+"\n"+start); - System.out.println(writeText()); - System.out.println(last); - } - // 正文部分定义成抽象方法,交给子类重写!! - public abstract String writeText(); -} -``` - - - -*** - - - -### 接口 - -#### 基本介绍 - -接口,是Java语言中一种引用类型,是方法的集合。 - -接口是更加彻底的抽象,接口中只有抽象方法和常量,没有其他成分,jdk1.8前 - -> 接口称为 被实现,实现接口的类称为**实现类** - -```java - 修饰符 interface 接口名称{ - // 抽象方法 - // 默认方法 - // 静态方法 - // 私有方法 -} -``` - -* 抽象方法:接口中的抽象方法默认会加上public abstract修饰,所以可以省略不写 - -* 静态方法:静态方法必须有方法体 - -* 常量:常量是public static final修饰的成员变量,仅能被赋值一次,值不能改变。常量的名称规范上要求全部大写,多个单词下划线连接。public static final可以省略不写。 - - ```java - public interface InterfaceDemo{ - //public static final String SCHOOL_NAME = "张三"; - String SCHOOL_NAME = "张三"; - - //public abstract void run(); - void run();//默认补充 - } - ``` - - - - -*** - - - - -#### 实现接口 - -作用:**接口是用来被类实现的。** - -类与类是继承关系:一个类只能直接继承一个父类,单继承 -类与接口是实现关系:一个类可以实现多个接口,多实现,接口不能继承类 -接口与接口继承关系:**多继承** - ->子类 继承 父类 ->实现类 实现 接口 - -```java -修饰符 class 实现类名称 implements 接口1,接口2,接口3,....{ - -} -修饰符 interface 接口名 extend 接口1,接口2,接口3,....{ - -} -``` - -实现多个接口的使用注意事项: - -1. 当一个类实现多个接口时,多个接口中存在同名的静态方法并不会冲突,只能通过各自接口名访问静态方法 - -2. 当一个类实现多个接口时,多个接口中存在同名的默认方法,实现类必须重写这个方法 - -3. 当一个类,既继承一个父类,又实现若干个接口时,父类中的成员方法与接口中的默认方法重名,子类**就近选择执行父类**的成员方法 - -4. 接口中,没有构造器,**不能创建对象**,接口是更彻底的抽象,连构造器都没有,自然不能创建对象!! - - ```java - public class InterfaceDemo { - public static void main(String[] args) { - Student s = new Student(); - s.run(); - s.rule(); - } - } - class Student implements Food, Person{ - @Override - public void eat() {} - - @Override - public void run() {} - } - interface Food{ - void eat(); - } - interface Person{ - void run(); - } - //可以直接 interface Person extend Food, - //然后 class Student implements Person 效果一样 - ``` - - - -*** - - - -#### JDK8以后 - -jdk1.8以后新增的功能,实际开发中很少使用 - -* 默认方法(就是之前写的普通实例方法) - * 必须用default修饰,默认会public修饰 - * 必须用接口的实现类的对象来调用 -* 静态方法 - * 默认会public修饰 - * 接口的静态方法必须用接口的类名本身来调用 - * 调用格式:ClassName.method() -* 私有方法:JDK 1.9才开始有的,只能在**本类中**被其他的默认方法或者私有方法访问 - -```java -public class InterfaceDemo { - public static void main(String[] args) { - // 1.默认方法调用:必须用接口的实现类对象调用。 - Man m = new Man(); - m.run(); - m.work(); - - // 2.接口的静态方法必须用接口的类名本身来调用。 - InterfaceJDK8.inAddr(); - } -} -class Man implements InterfaceJDK8{ - @Override - public void work() { - System.out.println("工作中。。。"); - } -} - -interface InterfaceJDK8{ - //抽象方法!! - void work(); - // a.默认方法(就是之前写的普通实例方法) - // 必须用接口的实现类的对象来调用。 - default void run(){ - go(); - System.out.println("开始跑步🏃‍"); - } - - // b.静态方法 - // 注意:接口的静态方法必须用接口的类名本身来调用 - static void inAddr(){ - System.out.println("我们在武汉"); - } - - // c.私有方法(就是私有的实例方法): JDK 1.9才开始有的。 - // 只能在本接口中被其他的默认方法或者私有方法访问。 - private void go(){ - System.out.println("开始。。"); - } -} -``` - - - -*** - - - -#### 对比抽象类 - -| **参数** | **抽象类** | **接口** | -| ------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | -| 默认的方法实现 | 可以有默认的方法实现 | 接口完全是抽象的,jdk8以后有默认的实现 | -| 实现 | 子类使用**extends**关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现。 | 子类使用关键字**implements**来实现接口。它需要提供接口中所有声明的方法的实现 | -| 构造器 | 抽象类可以有构造器 | 接口不能有构造器 | -| 与正常Java类的区别 | 除了不能实例化抽象类之外,和普通Java类没有任何区别 | 接口是完全不同的类型 | -| 访问修饰符 | 抽象方法可以有**public**、**protected**和**default**这些修饰符 | 接口方法默认修饰符是**public**,别的修饰符需要有方法体 | -| main方法 | 抽象方法可以有main方法并且我们可以运行它 | jdk8以前接口没有main方法,不能运行;jdk8以后接口可以有default和static方法,可以运行main方法 | -| 多继承 | 抽象方法可以继承一个类和实现多个接口 | 接口可以继承一个或多个其它接口,接口不可继承类 | -| 速度 | 比接口速度要快 | 接口是稍微有点慢的,因为它需要时间去寻找在类中实现的方法 | -| 添加新方法 | 如果往抽象类中添加新的方法,可以给它提供默认的实现,因此不需要改变现在的代码 | 如果往接口中添加方法,那么必须改变实现该接口的类 | - - - - - ------- - - - -### 多态 - -#### 基本介绍 - -多态的概念:同一个实体同时具有多种形式同一个类型的对象,执行同一个行为,在不同的状态下会表现出不同的行为特征。 - -多态的格式: - -* 父类类型范围 > 子类类型范围 - -```java -父类类型 对象名称 = new 子类构造器; -接口 对象名称 = new 实现类构造器; -``` - -多态的执行: - -* 对于方法的调用:**编译看左边,运行看右边**(分派机制) -* 对于变量的调用:**编译看左边,运行看左边** - -多态的使用规则: - -* 必须存在继承或者实现关系 -* 必须存在父类类型的变量引用子类类型的对象 -* 存在方法重写 - -多态的优势: -* 在多态形式下,右边对象可以实现组件化切换,业务功能也随之改变,便于扩展和维护。可以实现类与类之间的**解耦** -* 父类类型作为方法形式参数,传递子类对象给方法,可以传入一切子类对象进行方法的调用,更能体现出多态的**扩展性**与便利性 - -多态的劣势: -* 多态形式下,不能直接调用子类特有的功能,因为编译看左边,父类中没有子类独有的功能,所以代码在编译阶段就直接报错了! - -```java -public class PolymorphicDemo { - public static void main(String[] args) { - Animal c = new Cat(); - c.run(); - //c.eat();//报错 编译看左边 需要强转 - go(c); - go(new Dog); - } - //用 Dog或者Cat 都没办法让所有动物参与进来,只能用Anima - public static void go(Animal d){} - -} -class Dog extends Animal{} - -class Cat extends Animal{ - public void eat(); - @Override - public void run(){} -} - -class Animal{ - public void run(){} -} -``` - - - -*** - - - -#### 上下转型 - ->基本数据类型的转换: -> 1.小范围类型的变量或者值可以直接赋值给大范围类型的变量。 -> 2.大范围类型的变量或者值必须强制类型转换给小范围类型的变量。 - -引用数据类型的**自动**类型转换语法:子类类型的对象或者变量可以自动类型转换赋值给父类类型的变量。 - -**父类引用指向子类对象** - -- **向上转型(upcasting)**:通过子类对象(小范围)实例化父类对象(大范围),这种属于自动转换 -- **向下转型(downcasting)**:通过父类对象(大范围)实例化子类对象(小范围),这种属于强制转换 - -```java -public class PolymorphicDemo { - public static void main(String[] args){ - Animal a = new Cat();//向上转型 - Cat c = (Cat)a;//向下转型 - } -} -class Animal{} -class Cat extends Animal{} -``` - - - -*** - - - -#### instanceof - -instanceof:判断左边的对象是否是右边的类的实例,或者是其直接或间接子类,或者是其接口的实现类 - -* 引用类型强制类型转换:父类类型的变量或者对象强制类型转换成子类类型的变量,否则报错 -* 强制类型转换的格式:**类型 变量名称 = (类型)(对象或者变量)** -* 有继承/实现关系的两个类型就可以进行强制类型转换,编译阶段一定不报错,但是运行阶段可能出现类型转换异常 ClassCastException - -```java -public class Demo{ - public static void main(String[] args){ - Aniaml a = new Dog(); - //Dog d = (Dog)a; - //Cat c = (Cat)a; 编译不报错,运行报ClassCastException错误 - if(a instanceof Cat){ - Cat c = (Cat)a; - } else if(a instanceof Dog) { - Dog d = (Dog)a; - } - } -} -class Dog extends Animal{} -class Cat extends Animal{} -class Animal{} -``` - - - -*** - - - -### 内部类 - -#### 概述 - -内部类是类的五大成分之一:成员变量,方法,构造器,代码块,内部类 - -概念:定义在一个类里面的类就是内部类 - -作用:提供更好的封装性,体现出组件思想,**间接解决类无法多继承引起的一系列问题** - -分类:静态内部类、实例内部类(成员内部类)、局部内部类、**匿名内部类**(重点) - - - -*** - - - -#### 静态内部类 - -定义:有static修饰,属于外部类本身,会加载一次 - -静态内部类中的成分研究: - -* 类有的成分它都有,静态内部类属于外部类本身,只会加载一次 -* 特点与外部类是完全一样的,只是位置在别人里面 -* 可以定义静态成员 - -静态内部类的访问格式:外部类名称.内部类名称 - -静态内部类创建对象的格式:外部类名称.内部类名称 对象名称 = new 外部类名称.内部类构造器; - -静态内部类的访问拓展: - -* 静态内部类中是否可以直接访问外部类的静态成员? 可以,外部类的静态成员只有一份,可以被共享 -* 静态内部类中是否可以直接访问外部类的实例成员? 不可以,外部类的成员必须用外部类对象访问 - -```java -public class Demo{ - public static void main(String[] args){ - Outter.Inner in = new Outter.Inner(); - } -} - -static class Outter{ - public static int age; - private double salary; - public static class Inner{ - //拥有类的所有功能 构造器 方法 成员变量 - System.out.println(age); - //System.out.println(salary);报错 - } -} -``` - - - -*** - - - -#### 实例内部类 - -定义:无static修饰的内部类,属于外部类的每个对象,跟着外部类对象一起加载。 - -实例内部类的成分特点:实例内部类中不能定义静态成员,其他都可以定义 - -实例内部类的访问格式:外部类名称.内部类名称 - -创建对象的格式:外部类名称.内部类名称 对象名称 = new 外部类构造器.new 内部构造器; - -* `Outter.Inner in = new Outter().new Inner();` - -拓展:**实例内部类可以访问外部类的全部成员** - -> * 实例内部类中是否可以直接访问外部类的静态成员? -> 可以,外部类的静态成员可以被共享访问! -> * 实例内部类中是否可以访问外部类的实例成员? -> 可以,实例内部类属于外部类对象,可以直接访问外部类对象的实例成员! - - - -*** - - - -#### 局部内部类 - -局部内部类:定义在方法中,在构造器中,代码块中,for循环中定义的内部类。 - -局部内部类中的成分特点:只能定义实例成员,不能定义静态成员 - -```java -public class InnerClass{ - public static void main(String[] args){ - String name; - class{} - } - public static void test(){ - class Animal{} - class Cat extends Animal{} - } -} -``` - - - -*** - - - -#### 匿名内部类 - -匿名内部类:没有名字的局部内部类 -作用:简化代码,是开发中常用的形式 - -匿名内部类的格式: - -```java -new 类名|抽象类|接口(形参){ - //方法重写。 -} -``` - 匿名内部类的特点: - -* 匿名内部类不能定义静态成员 -* 匿名内部类一旦写出来,就会立即创建一个匿名内部类的对象返回 -* **匿名内部类的对象的类型相当于是当前new的那个的类型的子类类型** -* 匿名内部类引用局部变量必须是**常量**,底层创建为内部类的成员变量(原因:JVM → 运行机制 → 代码优化) - -```java -public class Anonymity { - public static void main(String[] args) { - Animal a = new Animal(){ - @Override - public void run() { - System.out.println("猫跑的贼溜~~"); - //System.out.println(n); - } - }; - a.run(); - a.go(); - } -} -abstract class Animal{ - public abstract void run(); - - public void go(){ - System.out.println("开始go~~~"); - } -} -``` - - - -*** - - - -### 权限符 - -权限修饰符:有四种**(private -> 缺省 -> protected - > public )** -可以修饰成员变量,修饰方法,修饰构造器,内部类,不同修饰符修饰的成员能够被访问的权限将受到限制! - -| 四种修饰符访问权限 | private | 缺省 | protected | public | -| ------------------ | :-----: | :--: | :-------: | :----: | -| 本类中 | √ | √ | √ | √ | -| 子类中 | X | √ | √ | √ | -| 本包下其他类中 | X | √ | √ | √ | -| 其他包下的子类中 | X | X | √ | √ | -| 其他包下的其他类中 | X | X | X | √ | - -protected 用于修饰成员,表示在继承体系中成员对于子类可见,子类需要重写方法才可以调用 - - - - - -*** - - - -### 代码块 - -#### 静态代码块 - -静态代码块的格式: - - ```java -static { -} - ``` - -* 静态代码块特点: - * 必须有static修饰 - * 会与类一起优先加载,且自动触发执行一次 - * 只能访问静态资源 -* 静态代码块作用: - * 可以在执行类的方法等操作之前先在静态代码块中进行静态资源的初始化 - * **先执行静态代码块,在执行main函数里的操作** - -```java -public class CodeDemo { - public static String schoolName ; - public static ArrayList lists = new ArrayList<>(); - - // 静态代码块,属于类,与类一起加载一次! - static { - System.out.println("静态代码块被触发执行~~~~~~~"); - // 在静态代码块中进行静态资源的初始化操作 - schoolName = "张三"; - lists.add("3"); - lists.add("4"); - lists.add("5"); - } - public static void main(String[] args) { - System.out.println("main方法被执行"); - System.out.println(schoolName); - System.out.println(lists); - } -} -/*静态代码块被触发执行~~~~~~~ -main方法被执行 -张三 -[3, 4, 5] */ -``` - - - -*** - - - -#### 实例代码块 - -实例代码块的格式: - -```java -{ - -} -``` - -* 实例代码块的特点: - * 无static修饰,属于对象 - * 会与类的对象一起加载,每次创建类的对象的时候,实例代码块都会被加载且自动触发执行一次 - * 实例代码块的代码在底层实际上是提取到每个构造器中去执行的 - -* 实例代码块的作用:实例代码块可以在创建对象之前进行实例资源的初始化操作 - -```java -public class CodeDemo { - private String name; - private ArrayList lists = new ArrayList<>(); - { - name = "代码块"; - lists.add("java"); - System.out.println("实例代码块被触发执行一次~~~~~~~~"); - } - public CodeDemo02(){ }//构造方法 - public CodeDemo02(String name){} - - public static void main(String[] args) { - CodeDemo c = new CodeDemo();//实例代码块被触发执行一次 - System.out.println(c.name); - System.out.println(c.lists); - new CodeDemo02();//实例代码块被触发执行一次 - } -} -``` - - - -*** - - - - - - -## API - -### Object - -#### 基本介绍 - -Object类是Java中的祖宗类,一个类或者默认继承Object类,或者间接继承Object类,Object类的方法是一切子类都可以直接使用 - -Object类常用方法: - -* `public String toString()`: - 默认是返回当前对象在堆内存中的地址信息:类的全限名@内存地址,例:Student@735b478; - 直接输出对象名称,默认会调用toString()方法,所以省略toString()不写; - 如果输出对象的内容,需要重写toString()方法,toString方法存在的意义是为了被子类重写 -* `public boolean equals(Object o)`:默认是比较两个对象的引用是否相同 -* `protected Object clone()`:创建并返回此对象的副本 - -只要两个对象的内容一样,就认为是相等的: - -```java -public boolean equals(Object o) { - // 1.判断是否自己和自己比较,如果是同一个对象比较直接返回true - if (this == o) return true; - // 2.判断被比较者是否为null ,以及是否是学生类型。 - if (o == null || this.getClass() != o.getClass()) return false; - // 3.o一定是学生类型,强制转换成学生,开始比较内容! - Student student = (Student) o; - return age == student.age && - sex == student.sex && - Objects.equals(name, student.name); -} -``` - -**面试题**:== 和equals的区别 - -* == 比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的**地址**是否相同,即是否是指相同一个对象,比较的是真正意义上的指针操作。 -* 重写equals方法比较的是两个对象的**内容**是否相等,所有的类都是继承自java.lang.Object类,所以适用于所有对象,如果**没有对该方法进行覆盖的话,调用的仍然是Object类中的方法,比较两个对象的引用** - -hashCode的作用: - -* hashCode的存在主要是用于查找的快捷性,如Hashtable,HashMap等,可以在散列存储结构中确定对象的存储地址 -* 如果两个对象相同,就是适用于equals(java.lang.Object) 方法,那么这两个对象的hashCode一定要相同 -* 哈希值相同的数据不一定内容相同,内容相同的数据哈希值一定相同 - - - -*** - - - -#### 深浅克隆 - -深浅拷贝(克隆)的概念: - -* 浅拷贝 (shallowCopy):对基本数据类型进行值传递,对引用数据类型只是复制了引用,被复制对象属性的所有的引用仍然指向原来的对象,简而言之就是增加了一个指针指向原来对象的内存地址 - -* 深拷贝 (deepCopy):对基本数据类型进行值传递,对引用数据类型是一个整个独立的对象拷贝,会拷贝所有的属性并指向的动态分配的内存,简而言之就是把所有属性复制到一个新的内存,增加一个指针指向新内存。所以使用深拷贝的情况下,释放内存的时候不会出现使用浅拷贝时释放同一块内存的错误 - -Object 的 clone() 是 protected 方法,一个类不显式去重写 clone(),就不能直接去调用该类实例的clone()方法 - -克隆就是制造一个对象的副本,根据所要克隆的对象的成员变量中是否含有引用类型,可以将克隆分为两种:浅克隆(Shallow Clone) 和 深克隆(Deep Clone),默认情况下使用Object中的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具有相同的属性值和方法 - -* Shallow Clone & Deep Clone: - - 浅克隆:Object中的clone()方法在对某个对象克隆时对其仅仅是简单地执行域对域的copy - - * 对基本数据类型和包装类的克隆是没有问题的。String、Integer等包装类型在内存中是不可以被改变的对象,所以在使用克隆时可以视为基本类型,只需浅克隆引用即可 - * 如果对一个引用类型进行克隆时只是克隆了它的引用,和原始对象共享对象成员变量 - - ![](https://gitee.com/seazean/images/raw/master/Java/Object浅克隆.jpg) - - 深克隆:在对整个对象浅克隆后,对其引用变量进行克隆,并将其更新到浅克隆对象中去 - - ```java - public class Student implements Cloneable{ - private String name; - private Integer age; - private Date date; - - @Override - protected Object clone() throws CloneNotSupportedException { - Student s = (Student) super.clone(); - s.date = (Date) date.clone(); - return s; - } - //..... - } - ``` - -SDP → 创建型 → 原型模式 - - - -*** - - - -### Objects - -Objects 类与 Object 是继承关系。 - -Objects的方法: - -* `public static boolean equals(Object a, Object b)` : 比较两个对象是否相同。 - 底层进行非空判断,从而可以避免空指针异常,更安全!!推荐使用!! - - ```java - public static boolean equals(Object a, Object b) { - return a == b || a != null && a.equals(b); - } - ``` - -* `public static boolean isNull(Object obj)` : 判断变量是否为null ,为null返回true ,反之! - -* `public static String toString(对象)` : 返回参数中对象的字符串表示形式 - -* `public static String toString(对象, 默认字符串)` : 返回对象的字符串表示形式。 - -```java -public class ObjectsDemo { - public static void main(String[] args) { - Student s1 = null; - Student s2 = new Student(); - System.out.println(Objects.equals(s1 , s2));//推荐使用 - // System.out.println(s1.equals(s2)); // 空指针异常 - - System.out.println(Objects.isNull(s1)); - System.out.println(s1 == null);//直接判断比较好 - } -} - -public class Student { -} -``` - - - -*** - - - -### String - -#### 基本介绍 - -**String 被声明为 final,因此不可被继承 (Integer 等包装类也不能被继承)** - -```java -public final class String implements java.io.Serializable, Comparable, CharSequence { - /** The value is used for character storage. */ - private final byte[] value; - /** The identifier of the encoding used to encode the bytes in {@code value}. */ - private final byte coder; -} -``` - -在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 `coder` 来标识使用了哪种编码 - -value 数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组,并且 String 内部没有改变 value 数组的方法,因此可以**保证 String 不可变,也保证线程安全** - -**注意:不能改变的意思是每次更改字符串都会产生新的对象,并不是对原始对象进行改变** - -```java -String s = "abc"; -s = s + "cd"; //s = abccd 新对象 -``` - - - -**** - - - -#### 构造方式 - -构造方法: - `public String()` : 创建一个空白字符串对象,不含有任何内容 - `public String(char[] chs)` : 根据字符数组的内容,来创建字符串对象 - `public String(String original)` : 根据传入的字符串内容,来创建字符串对象 - -直接赋值:`String s = “abc”` 直接赋值的方式创建字符串对象,内容就是abc - -- 通过构造方法创建:通过 new 创建的字符串对象,每一次 new 都会申请一个内存空间,虽然内容相同,但是地址值不同,**返回堆内存中对象的引用** -- 直接赋值方式创建:以“ ”方式给出的字符串,只要字符序列相同(顺序和大小写),无论在程序代码中出现几次,JVM 都只会**在 String Pool 中创建一个字符串对象**,并在字符串池中维护 - -`String str = new String("abc")`创建字符串对象: - -* 创建一个对象:字符串池中已经存在"abc"对象,那么直接在创建一个对象放入堆中,返回str引用 -* 创建两个对象:字符串池中未找到"abc"对象,那么分别在堆中和字符串池中创建一个对象,字符串池中的比较都是采用equals()方法 - - -`new String("a") + new String("b")`创建字符串对象: - -* 对象1:new StringBuilder() - -* 对象2:new String("a")、对象3:常量池中的"a" - -* 对象4:new String("b")、对象5:常量池中的"b" - - -* StringBuilder的toString(): - - ```java - @Override - public String toString() { - return new String(value, 0, count); - } - ``` - - * 对象6:new String("ab") - * StringBuilder 的 toString() 调用,在字符串常量池中没有生成"ab",new String("ab") 会创建两个对象因为传参数的时候使用字面量创建了对象 “ab ”,当使用数组构造 String 对象时,没有加入常量池的操作 - - - -*** - - - - -#### 常用方法 - -`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 beginIndex, int endIndex)` : 返回原字符串指定索引处的字符串 -`public String toLowerCase()` : 将此String所有字符转换为小写,使用默认语言环境的规则 -`public String toUpperCase()` : 使用默认语言环境的规则将此String所有字符转换为大写 -`public String replace(CharSequence target, CharSequence replacement)` : 使用新值,将字符串中的旧值替换,得到新的字符串 - -```java -String s = 123-78; -s.replace("-","");//12378 -``` - - - -*** - - - -#### String Pool - -##### 基本介绍 - -**字符串常量池(String Pool / StringTable / 串池)**保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定,常量池类似于Java系统级别提供的**缓存**,存放对象和引用 - -* StringTable,hashtable (哈希表 + 链表)结构,不能扩容,默认值大小长度是1009 - -* 常量池中的字符串仅是符号,第一次使用时才变为对象,可以避免重复创建字符串对象 -* 字符串**变量**的拼接的原理是StringBuilder(jdk1.8),append效率要比字符串拼接高很多 -* 字符串**常量**拼接的原理是编译期优化,结果在常量池 -* 可以使用 String 的 intern() 方法在运行过程将字符串添加到 String Pool 中 - - **intern()** : - -* jdk1.8:当一个字符串调用 intern() 方法时,如果 String Pool 中: - * 存在一个字符串和该字符串值相等(使用 equals() 方法进行确定),就会返回 String Pool 中字符串的引用(需要变量接收) - * 不存在,会把对象的引用地址复制一份放入串池,并返回串池中的引用地址,前提是堆内存有该对象。因为 Pool 在堆中,为了节省内存不再创建新对象 -* jdk1.6:将这个字符串对象尝试放入串池,如果有就不放入,返回已有的串池中的对象的地址;如果没有会把此对象复制一份,放入串池,把串池中的对象返回 - -```java -public class Demo { -//常量池中的信息都加载到运行时常量池,这时a b ab是常量池中的符号,还不是java字符串对象,是懒惰的 - // ldc #2 会把 a 符号变为 "a" 字符串对象 ldc:反编译后的指令 - // ldc #3 会把 b 符号变为 "b" 字符串对象 - // ldc #4 会把 ab 符号变为 "ab" 字符串对象 - public static void main(String[] args) { - String s1 = "a"; // 懒惰的 - String s2 = "b"; - String s3 = "ab";//串池 - // new StringBuilder().append("a").append("b").toString() new String("ab") - String s4 = s1 + s2; //d - 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()方法 - x2.intern(); - String x1 = "cd"; - - System.out.println(x1 == x2); //true - } -} -``` - -- == 比较基本数据类型:比较的是具体的值 -- == 比较引用数据类型:比较的是对象地址值 - -面试问题: - -```java -String s1 = "ab"; //串池 -String s2 = new String("a") + new String("b"); //堆 -//上面两条指令的结果和下面的效果相同 -String s = new String("ab"); -``` - -```java -public static void main(String[] args) { - String s = new String("a") + new String("b");//new String("ab") - //在上一行代码执行完以后,字符串常量池中并没有"ab" - - String s2 = s.intern(); - //jdk6:串池中创建一个字符串"ab" - //jdk8:串池中没有创建字符串"ab",而是创建一个引用指向new String("ab"),将此引用返回 - - System.out.println(s2 == "ab");//jdk6:true jdk8:true - System.out.println(s == "ab");//jdk6:false jdk8:true -} -``` - - - -*** - - - -##### 内存位置 - -Java 7之前,String Pool 被放在运行时常量池中,它属于永久代;Java 7以后,String Pool 被移到堆中,这是因为永久代的空间有限,在大量使用字符串的场景下会导致OutOfMemoryError 错误 - -演示 StringTable 位置: - -* `-Xmx10m`设置堆内存10m - -* 在jdk8下设置: `-Xmx10m -XX:-UseGCOverheadLimit`(运行参数在Run Configurations VM options) - -* 在jdk6下设置: `-XX:MaxPermSize=10m` - - ```java - public static void main(String[] args) throws InterruptedException { - List list = new ArrayList(); - int i = 0; - try { - for (int j = 0; j < 260000; j++) { - list.add(String.valueOf(j).intern()); - i++; - } - } catch (Throwable e) { - e.printStackTrace(); - } finally { - System.out.println(i); - } - } - ``` - -![](https://gitee.com/seazean/images/raw/master/Java/JVM-内存图对比.png) - - - -*** - - - -#### 优化常量池 - -两种方式: - -* 调整 -XX:StringTableSize=桶个数,数量越少,性能越差 - -* intern 将字符串对象放入常量池,通过复用字符串的引用,减少内存占用 - -```java -/** - * 演示 intern 减少内存占用 - * -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics - * -Xsx500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=200000 - */ -public class Demo1_25 { - public static void main(String[] args) throws IOException { - List address = new ArrayList<>(); - System.in.read(); - for (int i = 0; i < 10; i++) { - //很多数据 - try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) { - String line = null; - long start = System.nanoTime(); - while (true) { - line = reader.readLine(); - if(line == null) { - break; - } - address.add(line.intern()); - } - System.out.println("cost:" +(System.nanoTime()-start)/1000000); - } - } - System.in.read(); - } -} -``` - - - -*** - - - -#### 不可变好处 - -* 可以缓存 hash 值 - 因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,只需要进行一次计算 -* String Pool 的需要 - 如果一个String对象已经被创建过了,就会从 String Pool中取得引用。只有 String是不可变的,才可能使用 String Pool -* 安全性 - String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 的那一方以为现在连接的是其它主机,而实际情况却不一定是 -* String 不可变性天生具备线程安全,可以在多个线程中安全地使用 -* 防止子类继承,破坏String的API的使用 - - - - - -*** - - - -### StringBuilder - -String StringBuffer 和 StringBuilder 区别: - -* String : **不可变**的字符序列,线程安全 -* StringBuffer : **可变**的字符序列,线程安全,底层方法加 synchronized,效率低 -* StringBuilder : **可变**的字符序列,JDK5.0新增;线程不安全,效率高 - -相同点:底层使用 char[] 存储 - -构造方法: - public StringBuilder():创建一个空白可变字符串对象,不含有任何内容 - public StringBuilder(String str):根据字符串的内容,来创建可变字符串对象 - -常用API : - `public StringBuilder append(任意类型)` : 添加数据,并返回对象本身 - `public StringBuilder reverse()` : 返回相反的字符序列 - `public String toString()` : 通过 toString() 就可以实现把 StringBuilder 转换为 String - -存储原理: - -```java -String str = "abc"; -char data[] = {'a', 'b', 'c'}; -StringBuffer sb1 = new StringBuffer();//new byte[16] -sb1.append('a'); //value[0] = 'a'; -``` - -append 源码: - -```java -public AbstractStringBuilder append(String str) { - if (str == null) return appendNull(); - int len = str.length(); - ensureCapacityInternal(count + len); - str.getChars(0, len, value, count); - count += len; - return this; -} -private void ensureCapacityInternal(int minimumCapacity) { - // 创建超过数组长度就新的char数组,把数据拷贝过去 - if (minimumCapacity - value.length > 0) { - //int newCapacity = (value.length << 1) + 2;每次扩容2倍+2 - value = Arrays.copyOf(value, newCapacity(minimumCapacity)); - } -} - public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) { - //将字符串中的字符复制到目标字符数组中 - System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin); -} -``` - - - - - -**** - - - -### Arrays - -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)` : 返回由指定数组支持的列表。 - -```java -public class MyArraysDemo { - public static void main(String[] args) { - //按照数字顺序排列指定的数组 - int [] arr = {3,2,4,6,7}; - Arrays.sort(arr); - System.out.println(Arrays.toString(arr)); - - int [] arr = {1,2,3,4,5,6,7,8,9,10}; - int index = Arrays.binarySearch(arr, 0); - System.out.println(index); - //1,数组必须有序 - //2.如果要查找的元素存在,那么返回的是这个元素实际的索引 - //3.如果要查找的元素不存在,那么返回的是 (-插入点-1) - //插入点:如果这个元素在数组中,他应该在哪个索引上. - } - } -``` - - - - - -*** - - - -### Random - -用于生成伪随机数。 - -使用步骤: -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` (不包括),伪随机地生成并返回 - - - -*** - - - -### Date - -包:java.util.Date。 -构造器: - `public Date()`:创建当前系统的此刻日期时间对象。 - `public Date(long time)`:把时间毫秒值转换成日期对象。 -方法: - `public long getTime()`:返回自 1970 年 1 月 1 日 00:00:00 GMT 以来总的毫秒数。 - -时间记录的两种方式: - -1. Date日期对象 -2. 时间毫秒值:从1970-01-01 00:00:00开始走到此刻的总的毫秒值。 1s = 1000ms - -```java -public class DateDemo { - public static void main(String[] args) { - Date d = new Date(); - System.out.println(d);//Fri Oct 16 21:58:44 CST 2020 - long time = d.getTime() + 121*1000;//过121s是什么时间 - System.out.println(time);//1602856875485 - - Date d1 = new Date(time); - System.out.println(d1);//Fri Oct 16 22:01:15 CST 2020 - } -} -``` - -```java -public static void main(String[] args){ - Date d = new Date(); - long startTime = d.getTime(); - for(int i = 0; i < 10000; i++){输出i} - long endTime = new Date().getTime(); - System.out.println( (endTime - startTime) / 1000.0 +"s"); - //运行一万次输出需要多长时间 -} -``` - - - -*** - - - -### DateFormat - -DateFormat作用: -1.可以把“日期对象”或者“时间毫秒值”格式化成我们喜欢的时间形式。(格式化时间) -2.可以把字符串的时间形式解析成日期对象。(解析字符串时间) - -DateFormat是一个抽象类,不能直接使用,要找它的子类:SimpleDateFormat - -SimpleDateFormat简单日期格式化类: - 包:java.text.SimpleDateFormat - 构造器:public SimpleDateFormat(String pattern) : 指定时间的格式创建简单日期对象 - 方法: - `public String format(Date date) `: 把日期对象格式化成我们喜欢的时间形式,返回字符串! - `public String format(Object time)` : 把时间毫秒值格式化成设定的时间形式,返回字符串! - `public Date parse(String date) throws ParseException` : 把字符串的时间解析成日期对象 - ->yyyy年MM月dd日 HH:mm:ss EEE a" 周几 上午下午 - -```java -public static void main(String[] args){ - Date date = new Date(); - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss); - String time = sdf.format(date); - System.out.println(time);//2020-10-18 19:58:34 - //过121s后是什么时间 - long time = date.getTime(); - time+=121; - System.out.println(sdf.formate(time)); - String d = "2020-10-18 20:20:20";//格式一致 - Date newDate = sdf.parse(d); - System.out.println(sdf.format(newDate)); //按照前面的方法输出 -} -``` - - - - -**** - - - -### 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()`: 拿到此刻时间毫秒值 - -```java -public static void main(String[] args){ - Calendar rightNow = Calendar.getInsance(); - int year = rightNow.get(Calendar.YEAR);//获取年 - int month = rightNow.get(Calendar.MONTH) + 1;//月要+1 - int days = rightNow.get(Calendar.DAY_OF_YEAR); - rightNow.set(Calendar.YEAR , 2099);//修改某个字段 - rightNow.add(Calendar.HOUR , 15);//加15小时 -15就是减去15小时 - Date date = rightNow.getTime();//日历对象 - long time = rightNow.getTimeInMillis();//时间毫秒值 - //700天后是什么日子 - rightNow.add(Calendar.DAY_OF_YEAR , 701); - Date date d = rightNow.getTime(); - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - System.out.println(sdf.format(d));//输出700天后的日期 -} -``` - - - -*** - - - -### LocalDateTime - -> JDK1.8新增,线程安全。 - -+ LocalDate 表示日期(年月日) -+ LocalTime 表示时间(时分秒) -+ LocalDateTime 表示时间+ 日期 (年月日时分秒) - -构造方法: - public static LocalDateTime now() 获取当前系统时间 - public static LocalDateTime of (年, 月 , 日, 时, 分, 秒) 使用指定年月日和时分秒初始化一个对象 - -常用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对象 | - -```java -public class JDK8DateDemo2 { - public static void main(String[] args) { - LocalDateTime now = LocalDateTime.now(); - System.out.println(now); - - LocalDateTime localDateTime = LocalDateTime.of(2020, 11, 11, 11, 11, 11); - System.out.println(localDateTime); - DateTimeFormatter pattern = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss"); - String s = localDateTime.format(pattern); - LocalDateTime parse = LocalDateTime.parse(s, pattern); - } -} -``` - -| 方法名 | 说明 | -| --------------------------------------------------- | ------------------------------ | -| 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) | 直接修改秒 | - - - -**时间间隔** - -Duration类API: - -| 方法名 | 说明 | -| ------------------------------------------------ | -------------------- | -| public static Period between(开始时间,结束时间) | 计算两个“时间"的间隔 | -| public int getYears() | 获得这段时间的年数 | -| public int getMonths() | 获得此期间的总月数 | -| public int getDays() | 获得此期间的天数 | -| public long toTotalMonths() | 获取此期间的总月数 | -| public static Durationbetween(开始时间,结束时间) | 计算两个“时间"的间隔 | -| public long toSeconds() | 获得此时间间隔的秒 | -| public long toMillis() | 获得此时间间隔的毫秒 | -| public long toNanos() | 获得此时间间隔的纳秒 | - -```java -public class JDK8DateDemo9 { - public static void main(String[] args) { - LocalDate localDate1 = LocalDate.of(2020, 1, 1); - LocalDate localDate2 = LocalDate.of(2048, 12, 12); - Period period = Period.between(localDate1, localDate2); - System.out.println(period);//P28Y11M11D - Duration duration = Duration.between(localDateTime1, localDateTime2); - System.out.println(duration);//PT21H57M58S - } -} -``` - - - -*** - - - -### Math - -Math用于做数学运算。 -Math类中的方法全部是静态方法,直接用类名调用即可。 -方法: - -| 方法 | 说明 | -| -------------------------------------------- | ------------------------------- | -| public static int abs(int a) | 获取参数a的绝对值 | -| public static double ceil(double a) | 向上取整 | -| public static double floor(double a) | 向下取整 | -| public static double pow(double a, double b) | 获取a的b次幂 | -| public static long round(double a) | 四舍五入取整 | -| public static int max(int a,int b) | 返回较大值 | -| public static int min(int a,int b) | 返回较小值 | -| public static double random() | 返回值为double的正值,[0.0,1.0) | - -```java -public class MathDemo { - public static void main(String[] args) { - // 1.取绝对值:返回正数。 - System.out.println(Math.abs(10)); - System.out.println(Math.abs(-10.3)); - // 2.向上取整: 5 - System.out.println(Math.ceil(4.00000001)); // 5.0 - System.out.println(Math.ceil(-4.00000001));//4.0 - // 3.向下取整:4 - System.out.println(Math.floor(4.99999999)); // 4.0 - System.out.println(Math.floor(-4.99999999)); // 5.0 - // 4.求指数次方 - System.out.println(Math.pow(2 , 3)); // 2^3 = 8.0 - // 5.四舍五入 10 - System.out.println(Math.round(4.49999)); // 4 - System.out.println(Math.round(4.500001)); // 5 - System.out.println(Math.round(5.5));//6 - } -} -``` - - - -### DecimalFormat - -使任何形式的数字解析和格式化 - -```java -public static void main(String[]args){ -    double pi = 3.1415927; //圆周率 -    //取一位整数 -    System.out.println(new DecimalFormat("0").format(pi));   //3 -    //取一位整数和两位小数 -    System.out.println(new DecimalFormat("0.00").format(pi)); //3.14 -    //取两位整数和三位小数,整数不足部分以0填补。 -    System.out.println(new DecimalFormat("00.000").format(pi));// 03.142 -    //取所有整数部分 -    System.out.println(new DecimalFormat("#").format(pi));   //3 -    //以百分比方式计数,并取两位小数 -    System.out.println(new DecimalFormat("#.##%").format(pi)); //314.16% - -     long c =299792458;  //光速 -    //显示为科学计数法,并取五位小数 -    System.out.println(new DecimalFormat("#.#####E0").format(c));//2.99792E8 -    //显示为两位整数的科学计数法,并取四位小数 -    System.out.println(new DecimalFormat("00.####E0").format(c));//29.9792E7 -    //每三位以逗号进行分隔。 -    System.out.println(new DecimalFormat(",###").format(c));//299,792,458 -    //将格式嵌入文本 -    System.out.println(new DecimalFormat("光速大小为每秒,###米。").format(c)); - -  } -``` - - - -*** - - - -### 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); - } -} -``` - - - -*** - - - -### BigDecimal - -Java在java.math包中提供的API类BigDecimal,用来对超过16位有效位的数进行精确的运算 - -构造方法: - `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,精确几位,舍入模式)` : 除法 - -```java -public class BigDecimalDemo { - public static void main(String[] args) { - // 浮点型运算的时候直接+ - * / 可能会出现数据失真(精度问题)。 - System.out.println(0.1 + 0.2); - System.out.println(1.301 / 100); - - double a = 0.1 ; - double b = 0.2 ; - double c = a + b ; - System.out.println(c);//0.30000000000000004 - - // 1.把浮点数转换成大数据对象运算 - BigDecimal a1 = BigDecimal.valueOf(a); - BigDecimal b1 = BigDecimal.valueOf(b); - BigDecimal c1 = a1.add(b1);//a1.divide(b1);也可以 - System.out.println(c1); - - // BigDecimal只是解决精度问题的手段,double数据才是我们的目的!! - double d = c1.doubleValue(); - } -} -``` - -+ 总结 - - 1. BigDecimal是用来进行精确计算的 - 2. 创建BigDecimal的对象,构造方法使用参数类型为字符串的。 - 3. 四则运算中的除法,如果除不尽请使用divide的三个参数的方法。 - - ```java - BigDecimal divide = bd1.divide(参与运算的对象,小数点后精确到多少位,舍入模式); - 参数1:表示参与运算的BigDecimal 对象。 - 参数2:表示小数点后面精确到多少位 - 参数3:舍入模式 - BigDecimal.ROUND_UP 进一法 - BigDecimal.ROUND_FLOOR 去尾法 - BigDecimal.ROUND_HALF_UP 四舍五入 - ``` - - - -*** - - - -### Regex - -#### 概述 - -正则表达式的作用: - 是一些特殊字符组成的校验规则,可以校验信息的正确性,校验邮箱、电话号码、金额等。 - -比如检验qq号: - -```java -public static boolean checkQQRegex(String qq){ - return qq!=null && qq.matches("\\d{4,}");//即是数字 必须大于4位数 -}// 用\\d 是因为\用来告诉它是一个校验类,不是普通的字符 比如 \t \n -``` - -java.util.regex 包主要包括以下三个类: - -- Pattern 类: - - pattern 对象是一个正则表达式的编译表示。Pattern 类没有公共构造方法,要创建一个 Pattern 对象,必须首先调用其公共静态编译方法,返回一个 Pattern 对象。该方法接受一个正则表达式作为它的第一个参数 - -- Matcher 类: - - Matcher 对象是对输入字符串进行解释和匹配操作的引擎。与Pattern 类一样,Matcher 也没有公共构造方法,需要调用 Pattern 对象的 matcher 方法来获得一个 Matcher 对象 - -- PatternSyntaxException: - - PatternSyntaxException 是一个非强制异常类,它表示一个正则表达式模式中的语法错误。 - - - -*** - - - -#### 字符匹配 - -##### 普通字符 - -字母、数字、汉字、下划线、以及没有特殊定义的标点符号,都是“普通字符”。表达式中的普通字符,在匹配一个字符串的时候,匹配与之相同的一个字符。其他统称**元字符** - - - -##### 特殊字符 - -\r\n 是Windows中的文本行结束标签,在Unix/Linux则是 \n - -| 元字符 | 说明 | -| ------ | ------------------------------------------------------------ | -| \ | 将下一个字符标记为一个特殊字符或原义字符,告诉它是一个校验类,不是普通字符 | -| \f | 换页符 | -| \n | 换行符 | -| \r | 回车符 | -| \t | 制表符 | -| \\ | 代表\本身 | -| () | 使用( )定义一个子表达式。子表达式的内容可以当成一个独立元素 | - - - -##### 标准字符 - -标准字符集合 -能够与”多种字符“匹配的表达式。注意区分大小写,大写是相反的意思。只能校验**"单"**个字符。 - -| 元字符 | 说明 | -| ------ | ------------------------------------------------------------ | -| . | 匹配任意一个字符(除了换行符),如果要匹配包括“\n”在内的所有字符,一般用[\s\S] | -| \d | 数字字符,0~9 中的任意一个,等价于 [0-9] | -| \D | 非数字字符,等价于 [ ^0-9] | -| \w | 大小写字母或数字或下划线,等价于[a-zA-Z_0-9_] | -| \W | 对\w取非,等价于[ ^\w] | -| \s | 空格、制表符、换行符等空白字符的其中任意一个,等价于[\f\n\r\t\v] | -| \S | 对 \s 取非 | - -\x 匹配十六进制字符,\0 匹配八进制,例如 \xA 对应值为 10 的 ASCII 字符 ,即 \n - - - -##### 自定义符 - -自定义符号集合,[ ]方括号匹配方式,能够匹配方括号中**任意一个**字符 - -| 元字符 | 说明 | -| ------------ | ----------------------------------------- | -| [ab5@] | 匹配 "a" 或 "b" 或 "5" 或 "@" | -| [^abc] | 匹配 "a","b","c" 之外的任意一个字符 | -| [f-k] | 匹配 "f"~"k" 之间的任意一个字母 | -| [^A-F0-3] | 匹配 "A","F","0"~"3" 之外的任意一个字符 | -| [a-d[m-p]] | 匹配 a 到 d 或者 m 到 p:[a-dm-p](并集) | -| [a-z&&[m-p]] | 匹配 a 到 z 并且 m 到 p:[a-dm-p](交集) | -| [^] | 取反 | - -* 正则表达式的特殊符号,被包含到中括号中,则失去特殊意义,除了^,-之外,需要在前面加 \ - -* 标准字符集合,除小数点外,如果被包含于中括号,自定义字符集合将包含该集合。 - 比如:[\d. \ -+]将匹配:数字、小数点、+、- - - - -##### 量词字符 - -修饰匹配次数的特殊符号。 - -* 匹配次数中的贪婪模式(匹配字符越多越好,默认!),\* 和 + 都是贪婪型元字符。 -* 匹配次数中的非贪婪模式(匹配字符越少越好,修饰匹配次数的特殊符号后再加上一个 "?" 号) - -| 元字符 | 说明 | -| ------ | -------------------------------- | -| X? | X一次或一次也没,有相当于 {0,1} | -| X* | X不出现或出现任意次,相当于 {0,} | -| X+ | X至少一次,相当于 {1,} | -| X{n} | X恰好 n 次 | -| {n,} | X至少 n 次 | -| {n,m} | X至少 n 次,但是不超过 m 次 | - - - -*** - - - -#### 位置匹配 - -##### 字符边界 - -本组标记匹配的不是字符而是位置,符合某种条件的位置 - -| 元字符 | 说明 | -| ------ | ------------------------------------------------------------ | -| ^ | 与字符串开始的地方匹配(在字符集合中用来求非,在字符集合外用作匹配字符串的开头) | -| $ | 与字符串结束的地方匹配 | -| \b | 匹配一个单词边界 | - - - -##### 捕获组 - -捕获组是把多个字符当一个单独单元进行处理的方法,它通过对括号内的字符分组来创建。 - -在表达式( ( A ) ( B ( C ) ) ),有四个这样的组:((A)(B(C)))、(A)、(B(C))、(C)(按照括号从左到右为 group(1)) - -* 调用 matcher 对象的groupCount 方法返回一个 int值,表示matcher对象当前有多个捕获组。 -* 特殊的组group(0)、group(),它代表整个表达式,该组不包括在 groupCount 的返回值中。 - -| 表达式 | 说明 | -| ------------------------- | ------------------------------------------------------------ | -| \| (分支结构) | 左右两边表达式之间 "或" 关系,匹配左边或者右边 | -| () (捕获组) | (1) 在被修饰匹配次数的时候,括号中的表达式可以作为整体被修饰
(2) 取匹配结果的时候,括号中的表达式匹配到的内容可以被单独得到
(3) 每一对括号分配一个编号,()的捕获根据左括号的顺序从1开始自动编号。捕获元素编号为零的第一个捕获是由整个正则表达式模式匹配的文本 | -| (?:Expression) 非捕获组 | 一些表达式中,不得不使用( ),但又不需要保存( )中子表达式匹配的内容,这时可以用非捕获组来抵消使用( )带来的副作用。 | - - - -##### 反向引用 - -反向引用(\number),又叫回溯引用: - -* 每一对()会分配一个编号,使用 () 的捕获根据左括号的顺序从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) - * 实例:43438n44 - -* 应用2:爬虫 - - ```java - String regex = "<(h[1-6])>\w*?<\/\1>"; - ``` - - 匹配结果 - - ```java -

x

//匹配 -

x

//匹配 -

x

//不匹配 - ``` - - - - - - - -##### 零宽断言 - -预搜索(零宽断言)(环视) - -* 只进行子表达式的匹配,匹配内容不计入最终的匹配结果,是零宽度 - -* 判断当前位置的前后字符,是否符合指定的条件,但不匹配前后的字符。**是对位置的匹配**。 - -* 正则表达式匹配过程中,如果子表达式匹配到的是字符内容,而非位置,并被保存到最终的匹配结果中,那么就认为这个子表达式是占有字符的;如果子表达式匹配的仅仅是位置,或者匹配的内容并不保存到最终的匹配结果中,那么就认为这个子表达式是**零宽度**的。占有字符还是零宽度,是针对匹配的内容是否保存到最终的匹配结果中而言的 - - | 表达式 | 说明 | - | -------- | --------------------------------------- | - | (?=exp) | 断言自身出现的位置的后面能匹配表达式exp | - | (?<=exp) | 断言自身出现的位置的前面能匹配表达式exp | - | (?!exp) | 断言此位置的后面不能匹配表达式exp | - | (? Java中集合的代表是:Collection. -> Collection集合是Java中集合的祖宗类。 - -Collection集合底层为数组:`[value1, value2, ....]` - -```java -Collection集合的体系: - Collection(接口) - / \ - Set(接口) List(接口) - / \ / \ - HashSet(实现类) TreeSet<>(实现类) ArrayList(实现类) LinekdList<>(实现类) - / -LinkedHashSet<>(实现类) -``` - -**集合的特点:**(非常重要) - Set系列集合:添加的元素是无序,不重复,无索引的。 - -- HashSet: 添加的元素是无序,不重复,无索引的。 - -- LinkedHashSet: 添加的元素是有序,不重复,无索引的。 - -- TreeSet: 不重复,无索引,按照大小默认升序排序!! - List系列集合:添加的元素是有序,可重复,有索引。 - -- ArrayList:添加的元素是有序,可重复,有索引。 - -- LinekdList:添加的元素是有序,可重复,有索引。 - - - -*** - - - -#### API - -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)` : 将指定集合中的所有元素添加到此集合 - -```java -public class CollectionDemo { - public static void main(String[] args) { - Collection sets = new HashSet<>(); - sets.add("MyBatis"); - System.out.println(sets.add("Java"));//true - System.out.println(sets.add("Java"));//false - sets.add("Spring"); - sets.add("MySQL"); - System.out.println(sets)//[]无序的; - System.out.println(sets.contains("java"));//true 存在 - Object[] arrs = sets.toArray(); - System.out.println("数组:"+ Arrays.toString(arrs)); - - Collection c1 = new ArrayList<>(); - c1.add("java"); - Collection c2 = new ArrayList<>(); - c2.add("ee"); - c1.addAll(c2);// c1:[java,ee] c2:[ee]; - } -} -``` - - - -*** - - - -#### 遍历 - -Collection集合的遍历方式有三种: - -集合可以直接输出内容,因为底层重写了toString()方法。 - -1. 迭代器 - `public Iterator iterator()` : 获取集合对应的迭代器,用来遍历集合中的元素的 - `E next()` : 获取下一个元素值! - `boolean hasNext()` : 判断是否有下一个元素,有返回true ,反之 - `default void remove()` : 从底层集合中删除此迭代器返回的最后一个元素,这种方法只能在每次调用next() 时调用一次 - -2. 增强for循环 - 增强for循环是一种遍历形式,可以遍历集合或者数组,遍历集合实际上是迭代器遍历的简化写法 -```java - for(被遍历集合或者数组中元素的类型 变量名称 : 被遍历集合或者数组){ - - } -``` - -缺点:遍历无法知道遍历到了哪个元素了,因为没有索引。 - -3. JDK 1.8开始之后的新技术Lambda表达式 - - ```java - public class CollectionDemo { - public static void main(String[] args) { - Collection lists = new ArrayList<>(); - lists.add("aa"); - lists.add("bb"); - lists.add("cc"); - System.out.println(lists); // lists = [aa, bb, cc] - //迭代器流程 - // 1.得到集合的迭代器对象。 - Iterator it = lists.iterator(); - // 2.使用while循环遍历。 - while(it.hasNext()){ - String ele = it.next(); - System.out.println(ele); - } - - //增强for - for (String ele : lists) { - System.out.println(ele); - } - //lambda表达式 - lists.forEach(s -> { - System.out.println(s); - }); - } - } - ``` - - - - - -*** - - - -#### List - -##### 概述 - -List集合继承了Collection集合全部的功能。 - -List系列集合有索引,所以多了很多按照索引操作元素的功能:for循环遍历(4种遍历) - -List系列集合:添加的元素是有序,可重复,有索引。 - -* ArrayList:添加的元素是有序,可重复,有索引。 - -* LinekdList:添加的元素是有序,可重复,有索引。 - - - -*** - - - -##### ArrayList - -###### 介绍 - -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。 - -```java -public static void main(String[] args){ - List lists = new ArrayList<>();//多态 - lists.add("java1"); - lists.add("java1");//可以重复 - lists.add("java2"); - for(int i = 0 ; i < lists.size() ; i++ ) { - String ele = lists.get(i); - System.out.println(ele); - } -} -``` - -![ArrayList源码分析](https://gitee.com/seazean/images/raw/master/Java/ArrayList添加元素源码解析.png) - - - -###### 源码 - -ArrayList 是基于数组实现的,所以支持快速随机访问 - -```java -public class ArrayList extends AbstractList - implements List, RandomAccess, Cloneable, java.io.Serializable{} -``` - -- `RandomAccess` 是一个标志接口,表明实现这个这个接口的 List 集合是支持**快速随机访问**的。在 `ArrayList` 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。 -- `ArrayList` 实现了 `Cloneable` 接口 ,即覆盖了函数`clone()`,能被克隆 -- `ArrayList` 实现了 `Serializable `接口,这意味着`ArrayList`支持序列化,能通过序列化去传输 - -核心方法: - -* 构造函数:以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量,即向数组中添加第一个元素时,**数组容量扩为 10** - -* 添加元素: - - ```java - //e 插入的元素 elementData底层数组 size 插入的位置 - public boolean add(E e) { - ensureCapacityInternal(size + 1); // Increments modCount!! - elementData[size++] = e; // 插入size位置,然后加一 - return true; - } - ``` - - 当add 第 1 个元素到 ArrayList,size是0,进入 ensureCapacityInternal 方法, - - ```java - private void ensureCapacityInternal(int minCapacity) { - ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); - } - ``` - - ```java - private static int calculateCapacity(Object[] elementData, int minCapacity) { - //判断elementData是不是空数组 - if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { - //返回默认值和最小需求容量最大的一个 - return Math.max(DEFAULT_CAPACITY, minCapacity); - } - return minCapacity; - } - ``` - - 如果需要的容量大于数组长度,进行扩容: - - ```java - //判断是否需要扩容 - private void ensureExplicitCapacity(int minCapacity) { - modCount++; - if (minCapacity - elementData.length > 0) - //调用grow方法进行扩容,调用此方法代表已经开始扩容了 - grow(minCapacity); - } - ``` - - 指定索引插入,在旧数组上操作: - - ```java - public void add(int index, E element) { - rangeCheckForAdd(index); - ensureCapacityInternal(size + 1); // Increments modCount!! - System.arraycopy(elementData, index, elementData, index + 1, - size - index); - elementData[index] = element; - size++; - } - ``` - -* 扩容:新容量的大小为 `oldCapacity + (oldCapacity >> 1)`,`oldCapacity >> 1` 需要取整,所以新容量大约是旧容量的 1.5 倍左右,即 oldCapacity+oldCapacity/2 - - 扩容操作需要调用 `Arrays.copyOf()`(底层 `System.arraycopy()`)把原数组整个复制到**新数组**中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数 - - ```java - private void grow(int minCapacity) { - int oldCapacity = elementData.length; - int newCapacity = oldCapacity + (oldCapacity >> 1); - //检查新容量是否大于最小需要容量,若小于最小需要容量,就把最小需要容量当作数组的新容量 - if (newCapacity - minCapacity < 0) - newCapacity = minCapacity;//不需要扩容计算 - //检查新容量是否大于最大数组容量 - if (newCapacity - MAX_ARRAY_SIZE > 0) - //如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE` - //否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8` - newCapacity = hugeCapacity(minCapacity); - elementData = Arrays.copyOf(elementData, newCapacity); - } - ``` - - MAX_ARRAY_SIZE:要分配的数组的最大大小,分配更大的**可能**会导致 - - * OutOfMemoryError:Requested array size exceeds VM limit(请求的数组大小超出VM限制) - * OutOfMemoryError: Java heap space(堆区内存不足,可以通过设置JVM参数 -Xmx 来调节) - -* 删除元素:需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,在旧数组上操作,该操作的时间复杂度为 O(N),可以看到 ArrayList 删除元素的代价是非常高的, - - ```java - private void fastRemove(Object[] es, int i) { - modCount++; - final int newSize; - if ((newSize = size - 1) > i) - System.arraycopy(es, i + 1, es, i, newSize - i); - es[size = newSize] = null; - } - ``` - -* 序列化:ArrayList 基于数组并且具有动态扩容特性,因此保存元素的数组不一定都会被使用,就没必要全部进行序列化。保存元素的数组 elementData 使用 transient 修饰,该关键字声明数组默认不会被序列化 - - ```java - transient Object[] elementData; - ``` - -* ensureCapacity:增加此实例的容量,以确保它至少可以容纳最小容量参数指定的元素数,减少增量重新分配的次数 - - ```java - public void ensureCapacity(int minCapacity) { - if (minCapacity > elementData.length - && !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA - && minCapacity <= DEFAULT_CAPACITY)) { - modCount++; - grow(minCapacity); - } - } - ``` - -* **Fail-Fast**:快速失败,modCount 用来记录 ArrayList **结构发生变化**的次数,结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化 - - 在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了抛出 ConcurrentModificationException异常 - - - -*** - - - -##### Vector - -同步:Vector的实现与 ArrayList 类似,但是使用了 synchronized 进行同步 - -构造:默认长度为10的数组 - -扩容:Vector 的构造函数可以传入 capacityIncrement 参数,作用是在扩容时使容量 capacity 增长 capacityIncrement,如果这个参数的值小于等于 0(默认0),扩容时每次都令 capacity 为原来的两倍 - -对比 ArrayList - -1. Vector 是同步的,开销比 ArrayList 要大,访问速度更慢。最好使用 ArrayList 而不是 Vector,因为同步操作完全可以由程序来控制 - -2. Vector 每次扩容请求其大小的 2 倍(也可以通过构造函数设置增长的容量),而 ArrayList 是 1.5 倍 - -3. 底层都是 `Object[]`数组存储 - - - -**** - - - -##### LinkedList - -###### 介绍 - -LinkedList也是List的实现类:基于**双向链表**实现,使用 Node 存储链表节点信息,增删比较快,查询慢 - -LinkedList除了拥有List集合的全部功能还多了很多操作首尾元素的特殊功能: - `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 { - public static void main(String[] args) { - // 1.用LinkedList做一个队列:先进先出,后进后出。 - LinkedList queue = new LinkedList<>(); - // 入队 - queue.addLast("1号"); - queue.addLast("2号"); - queue.addLast("3号"); - System.out.println(queue); // [1号, 2号, 3号] - // 出队 - System.out.println(queue.removeFirst());//1号 - System.out.println(queue.removeFirst());//2号 - System.out.println(queue);//[3号] - - // 做一个栈 先进后出 - LinkedList stack = new LinkedList<>(); - // 压栈 - stack.push("第1颗子弹");//addFirst(e); - stack.push("第2颗子弹"); - stack.push("第3颗子弹"); - System.out.println(stack); // [ 第3颗子弹, 第2颗子弹, 第1颗子弹] - // 弹栈 - System.out.println(stack.pop());//removeFirst(); 第3颗子弹 - System.out.println(stack.pop()); - System.out.println(stack);// [第1颗子弹] - } -} -``` - -![](https://gitee.com/seazean/images/raw/master/Java/LinkedList添加元素源码解析.png) - - - - - -###### 源码 - -LinkedList是一个实现了List接口的**双端链表**,支持高效的插入和删除操作,另外也实现了Deque接口,使得LinkedList类也具有队列的特性 - -![](https://gitee.com/seazean/images/raw/master/Java/LinkedList底层结构.png) - -核心方法: - -* 使LinkedList变成线程安全的,可以调用静态类Collections类中的synchronizedList方法: - - ```java - List list = Collections.synchronizedList(new LinkedList(...)); - ``` - -* 私有内部类Node:这个类代表双端链表的节点Node - - ```java - private static class Node { - E item; - Node next; - Node prev; - - Node(Node prev, E element, Node next) { - this.item = element; - this.next = next; - this.prev = prev; - } - } - ``` - -* 构造方法:只有无参构造和用已有的集合创建链表的构造方法 - -* 添加元素:默认加到尾部 - - ```java - public boolean add(E e) { - linkLast(e); - return true; - } - ``` - -* 获取元素:`get(int index)` 根据指定索引返回数据 - - * 获取头节点 (index=0):getFirst()、element()、peek()、peekFirst() 这四个获取头结点方法的区别在于对链表为空时的处理,是抛出异常还是返回null,其中**getFirst() 和element()** 方法将会在链表为空时,抛出异常 - * 获取尾节点 (index=-1):getLast() 方法在链表为空时,会抛出NoSuchElementException,而peekLast() 则不会,只会返回 null - -* 删除元素: - - * remove()、removeFirst()、pop():删除头节点 - * removeLast()、pollLast():删除尾节点,removeLast()在链表为空时抛出NoSuchElementException,而pollLast()方法返回null - -对比ArrayList - -1. 是否保证线程安全:ArrayList和LinkedList都是不同步的,也就是不保证线程安全 -2. 底层数据结构: - * Arraylist底层使用的是 `Object` 数组 - * LinkedList 底层使用的是 双向链表 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环) -3. 插入和删除是否受元素位置的影响: - * ArrayList采用数组存储,所以插入和删除元素受元素位置的影响 - * LinkedList采用链表存储,所以对于`add(E e)`方法的插入,删除元素不受元素位置的影响 -4. 是否支持快速随机访问: - * LinkedList不支持高效的随机元素访问,ArrayList支持 - * 快速随机访问就是通过元素的序号快速获取元素对象(对应于`get(int index)`方法)。 -5. 内存空间占用: - * ArrayList的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间 - * LinkedList的空间花费则体现在它的每一个元素都需要消耗比 ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据) - - - -*** - - - -#### Set - -##### 概述 - -Set系列集合:添加的元素是无序,不重复,无索引的。 - -* HashSet:添加的元素是无序,不重复,无索引的。 -* LinkedHashSet:添加的元素是有序,不重复,无索引的。 -* TreeSet:不重复,无索引,按照大小默认升序排序!! - -**面试问题**:没有索引,不能使用普通for循环遍历 - - - -##### HashSet - -哈希值: - -- 哈希值:JDK根据对象的地址或者字符串或者数字计算出来的数值 - -- 获取哈希值:Object类中的public int hashCode() - -- 哈希值的特点 - - - 同一个对象多次调用hashCode()方法返回的哈希值是相同的 - - 默认情况下,不同对象的哈希值是不同的。而重写hashCode()方法,可以实现让不同对象的哈希值相同 - -**HashSet底层就是基于HashMap实现,值是PRESENT = new Object()** - -Set集合添加的元素是无序,不重复的。 - -* 是如何去重复的? - - ```java - 1.对于有值特性的,Set集合可以直接判断进行去重复。 - 2.对于引用数据类型的类对象,Set集合是按照如下流程进行是否重复的判断。 - Set集合会让两两对象,先调用自己的hashCode()方法得到彼此的哈希值(所谓的内存地址) - 然后比较两个对象的哈希值是否相同,如果不相同则直接认为两个对象不重复。 - 如果哈希值相同,会继续让两个对象进行equals比较内容是否相同,如果相同认为真的重复了 - 如果不相同认为不重复。 - - Set集合会先让对象调用hashCode()方法获取两个对象的哈希值比较 - / \ - false true - / \ - 不重复 继续让两个对象进行equals比较 - / \ - false true - / \ - 不重复 重复了 - ``` - -* Set系列集合元素无序的根本原因 - - 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方法** - - - -**** - - - -##### Linked - -LinkedHashSet 为什么是有序的? - -LinkedHashSet 底层依然是使用哈希表存储元素的,但是每个元素都额外带一个链来维护添加顺序,不光增删查快,还有顺序,缺点是多了一个存储顺序的链会**占内存空间**,而且不允许重复,无索引 - - - -**** - - - -##### TreeSet - -TreeSet集合自排序的方式: - -1. 有值特性的元素直接可以升序排序。(浮点型,整型) -2. 字符串类型的元素会按照首字符的编号排序。 -3. 对于自定义的引用数据类型,TreeSet默认无法排序,执行的时候报错,因为不知道排序规则。 - -自定义的引用数据类型,TreeSet默认无法排序 所们需要定制排序的规则,定义大小规则的方案有2种: - - * 直接为**对象的类**实现比较器规则接口Comparable,重写比较方法 - `public int compareTo(Employee o): this是比较者, o是被比较者 ` - 比较者大于被比较者 返回正数! - 比较者小于被比较者 返回负数! - 比较者等于被比较者 返回0! - * 直接为**集合**设置比较器Comparator对象,重写比较方法 - `public int compare(Employee o1, Employee o2): o1比较者, o2被比较者` - 比较者大于被比较者 返回正数! - 比较者小于被比较者 返回负数! - 比较者等于被比较者 返回0! - -注意:如果类和集合都带有比较规则,优先使用集合自带的比较规则。 - -```java -public class TreeSetDemo{ - public static void main(String[] args){ - Set students = new TreeSet<>(); - Collections.add(students,s1,s2,s3); - System.out.println(students);//按照年龄比较 升序 - - Set s = new TreeSet<>(new Comparator(){ - @Override - public int compare(Student o1, Student o2) { - // o1比较者 o2被比较者 - return o2.getAge() - o1.getAge();//降序 - } - }); - } -} - -public class Student implements Comparable{ - private String name; - private int age; - // 重写了比较方法。 - // e1.compareTo(o) - // 比较者:this - // 被比较者:o - // 需求:按照年龄比较 升序,年龄相同按照姓名 - @Override - public int compareTo(Student o) { - int result = this.age - o.age; - return result == 0 ? this.getName().compareTo(o.getName):result; -} -``` - -比较器原理:底层是以第一个元素为基准,加一个新元素,就会和第一个元素比,如果大于,就继续和大于的元素进行比较,直到遇到比新元素大的元素为止,放在该位置的左边。(树) - - - - -*** - - - -#### Collections - -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 - -```java -public class CollectionsDemo { - public static void main(String[] args) { - Collection names = new ArrayList<>(); - Collections.addAll(names,"张","王","李","赵"); - - List scores = new ArrayList<>(); - Collections.addAll(scores, 98.5, 66.5 , 59.5 , 66.5 , 99.5 ); - Collections.shuffle(scores); - Collections.sort(scores); // 默认升序排序! - System.out.println(scores); - - List students = new ArrayList<>(); - Collections.addAll(students,s1,s2,s3,s4); - Collections.sort(students,new Comparator(){ - - }) - } -} - -public class Student{ - private String name; - private int age; -} -``` - - - - - -*** - - - -### Map - -#### 概述 - ->Collection是单值集合体系。 ->Map集合是一种双列集合,每个元素包含两个值。 - -Map集合的每个元素的格式:key=value(键值对元素)。Map集合也被称为“键值对集合” - -Map集合的完整格式:`{key1=value1 , key2=value2 , key3=value3 , ...}` - -``` -Map集合的体系: - Map(接口,Map集合的祖宗类) - / \ - TreeMap HashMap(实现类,经典的,用的最多) - \ - LinkedHashMap(实现类) -``` - -Map集合的特点: - -1. Map集合的特点都是由键决定的。 -2. Map集合的键是无序,不重复的,无索引的。(Set) -3. Map集合的值无要求。(List) -4. Map集合的键值对都可以为null。 -5. Map集合后面重复的键对应元素会覆盖前面的元素 - -HashMap:元素按照键是无序,不重复,无索引,值不做要求。 - -LinkedHashMap:元素按照键是有序,不重复,无索引,值不做要求。 - -```java -//经典代码 -Map maps = new HashMap<>(); -maps.put("手机",1); -System.out.println(maps); -``` - - - -*** - - - -#### 常用API - -Map集合的常用API - `public V put(K key, V value)` : 把指定的键与值添加到Map集合中,**重复的键会覆盖前面的值元素** - `public V remove(Object key)` : 把指定的键对应的键值对元素在集合中删除,返回被删除元素的值 - `public V get(Object key)` : 根据指定的键,在Map集合中获取对应的值。 - `public Set keySet()` : 获取Map集合中所有的键,存储到**Set集合**中。 - `public Collection values()` : 获取全部值的集合,存储到**Collection集合** - `public Set> entrySet()` : 获取Map集合中所有的键值对对象的集合(Set集合) - `public boolean containKey(Object key)` : 判断该集合中是否有此键。 - `public boolean containsKey(Object key)` : 判断集合是否为空。 - -```java -public class MapDemo { - public static void main(String[] args) { - Map maps = new HashMap<>(); - maps.put(.....); - System.out.println(maps.isEmpty());//false - Integer value = maps.get("....");//返回键值对象 - Set keys = maps.keySet();//获取Map集合中所有的键, - //Map集合的键是无序不重复的,所以返回的是一个Set集合 - Collection values = maps.values(); - //Map集合的值是不做要求的,可能重复,所以值要用Collection集合接收! - } -} -``` - - - -*** - - - -#### 遍历方式 - -Map集合的遍历方式有:3种。 - (1)“键找值”的方式遍历:先获取Map集合全部的键,再根据遍历键找值。 - (2)“键值对”的方式遍历:难度较大,采用增强for或者迭代器 - (3)JDK 1.8开始之后的新技术:foreach,采用Lambda表达式 - -集合可以直接输出内容,因为底层重写了toString()方法。 - -```java -public static void main(String[] args){ - Map maps = new HashMap<>(); - //(1)键找值 - Set keys = maps.keySet(); - for(String key : keys) { - System.out.println(key + "=" + maps.get(key)); - } - //Iterator iterator = hm.keySet().iterator(); - - //(2)键值对 - //(2.1)普通方式 - Set> entries = maps.entrySet(); - for (Map.Entry entry : entries) { - System.out.println(entry.getKey() + "=>" + entry.getValue()); - } - //(2.2)迭代器方式 - Iterator> iterator = maps.entrySet().iterator(); - while (iterator.hasNext()) { - Map.Entry entry = iterator.next(); - System.out.println(entry.getKey() + "=" + entry.getValue()); - - } - //(3) Lamda - maps.forEach((k,v) -> { - System.out.println(k + "==>" + v); - }) -} -``` - - - -*** - - - -#### HashMap - -##### 基本介绍 - -HashMap基于哈希表的Map接口实现,是以key-value存储形式存在,主要用来存放键值对 - -特点: - -* HashMap的实现不是同步的,这意味着它不是线程安全的 -* key是唯一不重复的,底层的哈希表结构,依赖hashCode方法和equals方法保证键的唯一 -* key、value都可以为null,但是key位置只能是一个null -* HashMap中的映射不是有序的,即存取是无序的 -* **key要存储的是自定义对象,需要重写hashCode和equals方法,防止出现地址不同内容相同的key** - -JDK7对比JDK8: - -* 7 = 数组 + 链表,8 = 数组 + 链表 + 红黑树 -* 7中是头插法,多线程容易造成环,8中是尾插法 -* 7的扩容是全部数据重新定位,8中是位置不变或者当前位置 + 旧size大小来实现 -* 7是先判断是否要扩容再插入,8中是先插入再看是否要扩容 - -底层数据结构: - -* 哈希表(Hash table,也叫散列表),根据关键码值(Key value)而直接访问的数据结构。通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度,这个映射函数叫做散列函数,存放记录的数组叫做散列表 - -* JDK1.8 之前 HashMap 由 **数组+链表** 组成 - - * 数组是 HashMap 的主体 - * 链表则是为了**解决哈希冲突**而存在的(**拉链法**解决冲突),拉链法就是头插法 - 两个对象调用的hashCode方法计算的哈希码值(键的哈希)一致导致计算的数组索引值相同 - -* JDK1.8 以后 HashMap 由 **数组+链表 +红黑树**数据结构组成 - - * 解决哈希冲突时有了较大的变化 - * **当链表长度超过(大于)阈值(或者红黑树的边界值,默认为 8)并且当前数组的长度大于等于64时,此索引位置上的所有数据改为红黑树存储** - * 即使哈希函数取得再好,也很难达到元素百分百均匀分布。当 HashMap 中有大量的元素都存放到同一个桶中时,就相当于一个长的单链表,假如单链表有 n 个元素,遍历的**时间复杂度是 O(n)**,所以 JDK1.8 中引入了 红黑树(查找**时间复杂度为 O(logn)**)来优化这个问题,使得查找效率更高 - - ![](https://gitee.com/seazean/images/raw/master/Java/哈希表.png) - - - -*** - - - -##### 继承关系 - -HashMap继承关系如下图所示: - -![](https://gitee.com/seazean/images/raw/master/Java/HashMap继承关系.bmp) - - - -说明: - -* Cloneable 空接口,表示可以克隆, 创建并返回HashMap对象的一个副本。 -* Serializable 序列化接口,属于标记性接口,HashMap对象可以被序列化和反序列化。 -* AbstractMap 父类提供了Map实现接口,以最大限度地减少实现此接口所需的工作 - - - -*** - - - -##### 成员变量 - -1. 序列化版本号 - - ```java - private static final long serialVersionUID = 362498820763181265L; - ``` - -2. 集合的初始化容量( **必须是二的n次幂** ) - - ```java - //默认的初始容量是16 -- 1<<4相当于1*2的4次方---1*16 - static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; - ``` - - HashMap构造方法指定集合的初始化容量大小: - - ```java - HashMap(int initialCapacity)//构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap - ``` - - * 为什么必须是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,可以**保证散列的均匀性**,减少碰撞 - - ```java - 例如长度为8时候,3&(8-1)=3 2&(8-1)=2 ,不同位置上,不碰撞; - 例如长度为9时候,3&(9-1)=0 2&(9-1)=0 ,都在0上,碰撞了; - ``` - - * 如果输入值不是2的幂会怎么样? - - 创建HashMap对象时,HashMap通过位移运算和或运算得到的肯定是2的幂次数,并且是大于那个数的最近的数字,底层采用tableSizeFor()方法 - -3. 默认的负载因子,默认值是0.75 - - ```java - static final float DEFAULT_LOAD_FACTOR = 0.75f; - ``` - -4. 集合最大容量 - - ```java - //集合最大容量的上限是:2的30次幂 - static final int MAXIMUM_CAPACITY = 1 << 30; - ``` - - 最大容量为什么是2的30次方原因: - - * int类型是32位整型,占4个字节 - * Java的原始类型里没有无符号类型,所以首位是符号位正数为0,负数为1 - -5. 当链表的值超过8则会转红黑树(1.8新增**) - - ```java - //当桶(bucket)上的结点数大于这个值时会转成红黑树 - static final int TREEIFY_THRESHOLD = 8; - ``` - - 为什么Map桶中节点个数大于8才转为红黑树? - - * 在HashMap中有一段注释说明:**空间和时间的权衡** - - ```java - TreeNodes占用空间大约是普通节点的两倍,所以我们只在箱子包含足够的节点时才使用树节点。当节点变少(由于删除或调整大小)时,就会被转换回普通的桶。在使用分布良好的用户hashcode时,很少使用树箱。理想情况下,在随机哈希码下,箱子中节点的频率服从"泊松分布",默认调整阈值为0.75,平均参数约为0.5,尽管由于调整粒度的差异很大。忽略方差,列表大小k的预期出现次数是(exp(-0.5)*pow(0.5, k)/factorial(k)) - 0: 0.60653066 - 1: 0.30326533 - 2: 0.07581633 - 3: 0.01263606 - 4: 0.00157952 - 5: 0.00015795 - 6: 0.00001316 - 7: 0.00000094 - 8: 0.00000006 - more: less than 1 in ten million - 一个bin中链表长度达到8个元素的概率为0.00000006,几乎是不可能事件,所以我们选择8这个数字 - ``` - - * 其他说法 - 红黑树的平均查找长度是log(n),如果长度为8,平均查找长度为log(8)=3,链表的平均查找长度为n/2,当长度为8时,平均查找长度为8/2=4,这才有转换成树的必要;链表长度如果是小于等于6,6/2=3,而log(6)=2.6,虽然速度也很快的,但转化为树结构和生成树的时间并不短 - - - -6. 当链表的值小于6则会从红黑树转回链表 - - ```java - //当桶(bucket)上的结点数小于这个值时树转链表 - static final int UNTREEIFY_THRESHOLD = 6; - ``` - -7. 当Map里面的数量**大于等于**这个阈值时,表中的桶才能进行树形化 ,否则桶内元素太多时会扩容,而不是树形化。为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD (8) - - ```java - //桶中结构转化为红黑树对应的数组长度最小的值 - static final int MIN_TREEIFY_CAPACITY = 64; - ``` - - 原因:数组比较小的情况下变为红黑树结构,反而会降低效率,红黑树需要进行左旋,右旋,变色这些操作来保持平衡。同时数组长度小于64时,搜索时间相对快些,所以为了提高性能和减少搜索时间,底层在阈值大于8并且数组长度大于64时,链表才转换为红黑树,效率也变的更高效 - -8. table用来初始化(必须是二的n次幂)(重点) - - ```java - //存储元素的数组 - transient Node[] table; - ``` - - jdk8之前数组类型是Entry类型,从jdk1.8之后是Node类型。只是换了个名字,都实现了一样的接口:Map.Entry,负责存储键值对数据的 - - 9. HashMap中存放元素的个数(**重点**) - - ```java - //存放元素的个数,HashMap中K-V的实时数量,不是table数组的长度 - transient int size; - ``` - -10. 记录HashMap的修改次数 - - ```java - //每次扩容和更改map结构的计数器 - transient int modCount; - ``` - -11. 调整大小下一个容量的值计算方式为(容量*负载因子) - - ```java - //临界值,当实际大小(容量*负载因子)超过临界值时,会进行扩容 - int threshold; - ``` - -12. **哈希表的加载因子(重点)** - - ```java - final float loadFactor; - ``` - - * 加载因子的概述 - - loadFactor加载因子,是用来衡量 HashMap 满的程度,表示**HashMap的疏密程度**,影响hash操作到同一个数组位置的概率,计算HashMap的实时加载因子的方法为:size/capacity,而不是占用桶的数量去除以capacity,capacity 是桶的数量,也就是 table 的长度length。 - - 当HashMap里面容纳的元素已经达到HashMap数组长度的75%时,表示HashMap拥挤,需要扩容,而扩容这个过程涉及到 rehash、复制数据等操作,非常消耗性能,所以开发中尽量减少扩容的次数,可以通过创建HashMap集合对象时指定初始容量来尽量避免。 - - ```java - HashMap(int initialCapacity, float loadFactor)//构造指定初始容量和加载因子的空HashMap - ``` - - * 为什么加载因子设置为0.75,初始化临界值是12? - - loadFactor太大导致查找元素效率低,存放的数据拥挤,太小导致数组的利用率低,存放的数据会很分散。loadFactor的默认值为**0.75f是官方给出的一个比较好的临界值**。 - - * **threshold**计算公式:capacity(数组长度默认16) * loadFactor(负载因子默认0.75)。这个值是当前已占用数组长度的最大值。**当Size>=threshold**的时候,那么就要考虑对数组的resize(扩容),这就是 **衡量数组是否需要扩增的一个标准**, 扩容后的 HashMap 容量是之前容量的**两倍**. - - - -*** - - - -##### 构造方法 - -1. 构造一个空的 HashMap ,**默认初始容量(16)和默认负载因子(0.75)** - - ```java - public HashMap() { - this.loadFactor = DEFAULT_LOAD_FACTOR; - //将默认的加载因子0.75赋值给loadFactor,并没有创建数组 - } - ``` - -2. 构造一个具有指定的初始容量和默认负载因子(0.75)HashMap - - ```java - // 指定“容量大小”的构造函数 - public HashMap(int initialCapacity) { - this(initialCapacity, DEFAULT_LOAD_FACTOR); - } - ``` - -3. 构造一个具有指定的初始容量和负载因子的HashMap - - ```java - public HashMap(int initialCapacity, float loadFactor) { - //进行判断 - //将指定的加载因子赋值给HashMap成员变量的负载因子loadFactor - this.loadFactor = loadFactor; - //最后调用了tableSizeFor - this.threshold = tableSizeFor(initialCapacity); - } - ``` - - * 对于`this.threshold = tableSizeFor(initialCapacity);` - - 有些人会觉得这里是一个bug应该这样书写: - `this.threshold = tableSizeFor(initialCapacity) * this.loadFactor;` - 这样才符合threshold的概念(当HashMap的size到达threshold这个阈值时会扩容)。 - 但是在jdk8以后的构造方法中,并没有对table这个成员变量进行初始化,table的初始化被推迟到了put方法中,在put方法中会对threshold重新计算 - -4. 包含另一个`Map`的构造函数 - - ```java - //构造一个映射关系与指定 Map 相同的新 HashMap - public HashMap(Map m) { - //负载因子loadFactor变为默认的负载因子0.75 - this.loadFactor = DEFAULT_LOAD_FACTOR; - putMapEntries(m, false); - } - ``` - - 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); - } - } - } - ``` - - `float ft = ((float)s / loadFactor) + 1.0F;`这一行代码中为什么要加1.0F ? - - s / loadFactor的结果是小数,加1.0F相当于是对小数做一个向上取整以尽可能的保证更大容量,更大的容量能够减少resize的调用次数,这样可以减少数组的扩容 - - - -*** - - - -##### 成员方法 - -1. hash - - 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 - - jdk1.8 前是头插法 (拉链法),多线程下扩容出现循环链表,jdk1.8 以后引入红黑树,插入方法变成尾插法 - - 第一次调用 put 方法时创建数组 Node[] table,因为散列表耗费内存,为了防止内存浪费,所以**延迟初始化** - - 存储数据步骤(存储过程): - - 1. 先通过hash值计算出key映射到哪个桶 - - 2. 如果桶上没有碰撞冲突,则直接插入 - - 3. 如果出现碰撞冲突:如果该桶使用红黑树处理冲突,则调用红黑树的方法插入数据;否则采用传统的链式方法插入,如果链的长度达到临界值,则把链转变为红黑树 - - 4. 如果数组位置相同,通过equals比较内容是否相同:相同则新的value覆盖之前的value,不相同则将新的键值对添加到哈希表中 - 5. 如果size大于阈值threshold,则进行扩容 - - ```java - 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 - //。。。。。。。。。。。。。。 - } - - ``` - - * `(n - 1) & hash`:计算下标位置 - - - - * 余数本质是不断做除法,把剩余的数减去,运算效率要比位运算低 - - - -3. treeifyBin - - 节点添加完成之后判断此时节点个数是否大于TREEIFY_THRESHOLD临界值8,如果大于则将链表转换为红黑树,转换红黑树的方法 treeifyBin,整体代码如下: - - ```java - if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st - //转换为红黑树 tab表示数组名 hash表示哈希值 - treeifyBin(tab, hash); - ``` - - 1. 如果当前数组为空或者数组的长度小于进行树形化的阈值(MIN_TREEIFY_CAPACITY = 64)就去扩容,而不是将节点变为红黑树 - 2. 如果是树形化遍历桶中的元素,创建相同个数的树形节点,复制内容,建立起联系,类似单向链表转换为双向链表 - 3. 让桶中的第一个元素即数组中的元素指向新建的红黑树的节点,以后这个桶里的元素就是红黑树而不是链表数据结构了 - - - -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; - } - ``` - - 分析算法: - - 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; - 00000000 00000000 00000000 00001001 //9 - 00000000 00000000 00000000 00000100 //9右移之后变为4 - -------------------------------------------------- - 00000000 00000000 00000000 00001101 //按位或之后是13 - //使得n的二进制表示中与最高位的1紧邻的右边一位为1 - ``` - - * 第二次右移 - - ```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 - ``` - - 注意:容量最大是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 - ``` - - * JDK11 - - ```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); - } - ``` - - - -5. resize - - 当HashMap中的元素个数超过`(数组长度)*loadFactor(负载因子)`或者链表过长时,就会进行数组扩容,创建新的数组,伴随一次重新hash分配,并且遍历hash表中所有的元素,非常耗时,所以要尽量避免resize - - 扩容机制为扩容为原来容量的2倍: - - ```java - else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && - oldCap >= DEFAULT_INITIAL_CAPACITY) - newThr = oldThr << 1; // double threshold - ``` - - 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) - - 红黑树节点:扩容时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); - } - } - ``` - -​ - -4. 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; - } - ``` - - - -5. get - - 1. 通过hash值获取该key映射到的桶 - - 2. 桶上的key就是要查找的key,则直接找到并返回 - - 3. 桶上的key不是要找的key,则查看后续的节点: - - * 如果后续节点是红黑树节点,通过调用红黑树的方法根据key获取value - - * 如果后续节点是链表节点,则通过循环遍历链表根据key获取value - - 4. 红黑树节点调用的是getTreeNode方法通过树形节点的find方法进行查 - - * 查找红黑树,之前添加时已经保证这个树是有序的,因此查找时就是折半查找,效率更高。 - * 这里和插入时一样,如果对比节点的哈希值相等并且通过equals判断值也相等,就会判断key相等,直接返回,不相等就从子树中递归查找 - - 5. 时间复杂度 O(1) - - * 若为树,则在树中通过key.equals(k)查找,**O(logn)** - - * 若为链表,则在链表中通过key.equals(k)查找,**O(n)** - - - -*** - - - -#### LinkedMap - -##### 原理分析 - -LinkedHashMap是HashMap的子类 - -* 优点:添加的元素按照键有序不重复的,有序的原因是底层维护了一个双向链表 - -* 缺点:会占用一些内存空间 - -对比Set: - -* HashSet集合相当于是HashMap集合的键,不带值 -* LinkedHashSet集合相当于是LinkedHashMap集合的键,不带值 -* 底层原理完全一样,都是基于哈希表按照键存储数据的,只是Map多了一个键的值 - -源码解析: - -* 内部维护了一个双向链表,用来维护插入顺序或者 LRU 顺序 - - ```java - transient LinkedHashMap.Entry head; - transient LinkedHashMap.Entry tail; - ``` - -* accessOrder 决定了顺序,默认为 false 维护的是插入顺序,true为访问顺序(LRU顺序) - - ```java - final boolean accessOrder; - ``` - -* 维护顺序的函数 - - ```java - void afterNodeAccess(Node p) {} - void afterNodeInsertion(boolean evict) {} - ``` - -* put() - - ```java - // 调用父类HashMap的put方法 - public V put(K key, V value) { - return putVal(hash(key), key, value, false, true); - } - final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) - → afterNodeInsertion(evict);// evict为true - ``` - - afterNodeInsertion方法,当 removeEldestEntry() 方法返回 true 时会移除最近最久未使用的节点,也就是链表首部节点 first - - ```java - void afterNodeInsertion(boolean evict) { - LinkedHashMap.Entry first; - // evict 只有在构建 Map 的时候才为 false,这里为 true - if (evict && (first = head) != null && removeEldestEntry(first)) { - K key = first.key; - removeNode(hash(key), key, null, false, true);//移除头节点 - } - } - ``` - - removeEldestEntry() 默认为 false,如果需要让它为 true,需要继承 LinkedHashMap 并且覆盖这个方法的实现,在实现 LRU 的缓存中特别有用,通过移除最近最久未使用的节点,从而保证缓存空间足够,并且缓存的数据都是热点数据 - - ```java - protected boolean removeEldestEntry(Map.Entry eldest) { - return false; - } - ``` - -* get() - - 当一个节点被访问时,如果 accessOrder 为 true,则会将该节点移到链表尾部。也就是说指定为 LRU 顺序之后,在每次访问一个节点时会将这个节点移到链表尾部,那么链表首部就是最近最久未使用的节点 - - ```java - public V get(Object key) { - Node e; - if ((e = getNode(hash(key), key)) == null) - return null; - if (accessOrder) - afterNodeAccess(e); - return e.value; - } - ``` - - ```java - void afterNodeAccess(Node e) { - LinkedHashMap.Entry last; - if (accessOrder && (last = tail) != e) { - // 向下转型 - LinkedHashMap.Entry p = - (LinkedHashMap.Entry)e, b = p.before, a = p.after; - p.after = null; - // 判断 p 是否是首节点 - if (b == null) - //是头节点 让p后继节点成为头节点 - head = a; - else - //不是头节点 让p的前驱节点的next指向p的后继节点,维护链表的连接 - b.after = a; - // 判断p是否是尾节点 - if (a != null) - // 不是尾节点 让p后继节点指向p的前驱节点 - a.before = b; - else - // 是尾节点 让last指向p的前驱节点 - last = b; - // 判断last是否是空 - if (last == null) - // last为空说明p是尾节点或者只有p一个节点 - head = p; - else { - // last和p相互连接 - p.before = last; - last.after = p; - } - tail = p; - ++modCount; - } - } - ``` - -* remove() - - ```java - //调用HashMap的remove方法 - final Node removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) - → afterNodeRemoval(node); - ``` - - 当 HashMap 删除一个键值对时调用,会把在 HashMap 中删除的那个键值对一并从链表中删除 - - ```java - void afterNodeRemoval(Node e) { - LinkedHashMap.Entry p = - (LinkedHashMap.Entry)e, b = p.before, a = p.after; - // 让p节点与前驱节点和后继节点断开链接 - p.before = p.after = null; - // 判断p是否是头节点 - if (b == null) - // p是头节点 让head指向p的后继节点 - head = a; - else - // p不是头节点 让p的前驱节点的next指向p的后继节点,维护链表的连接 - b.after = a; - // 判断p是否是尾节点,是就让tail指向p的前驱节点,不是就让p.after指向前驱节点,双向 - if (a == null) - tail = b; - else - a.before = b; - } - ``` - - - -*** - - - -##### LRU - -使用 LinkedHashMap 实现的一个 LRU 缓存: - -- 设定最大缓存空间 MAX_ENTRIES 为 3 -- 使用 LinkedHashMap 的构造函数将 accessOrder 设置为 true,开启 LRU 顺序 -- 覆盖 removeEldestEntry() 方法实现,在节点多于 MAX_ENTRIES 就会将最近最久未使用的数据移除 - -```java -public static void main(String[] args) { - LRUCache cache = new LRUCache<>(); - cache.put(1, "a"); - cache.put(2, "b"); - cache.put(3, "c"); - cache.get(1);//把1放入尾部 - cache.put(4, "d"); - System.out.println(cache.keySet());//[3, 1, 4]只能存3个,移除2 -} - -class LRUCache extends LinkedHashMap { - private static final int MAX_ENTRIES = 3; - - protected boolean removeEldestEntry(Map.Entry eldest) { - return size() > MAX_ENTRIES; - } - - LRUCache() { - super(MAX_ENTRIES, 0.75f, true); - } -} -``` - - - -*** - - - -#### TreeMap - -TreeMap 实现了 SotredMap 接口,是有序不可重复的键值对集合,基于红黑树(Red-Black tree)实现,每个 key-value 都作为一个红黑树的节点。如果构造 TreeMap 没有指定比较器,则根据key执行自然排序(默认升序),如果指定了比较器则按照比较器来进行排序 - -TreeSet 集合的底层是基于TreeMap,只是键的附属值为空对象而已 - -TreeMap集合指定大小规则有2种方式: - -* 直接为对象的类实现比较器规则接口Comparable,重写比较方法(拓展方式) -* 直接为集合设置比较器Comparator对象,重写比较方法 - -成员属性: - -* Entry节点 - - ```java - static final class Entry implements Map.Entry { - K key; - V value; - Entry left; //左孩子节点 - Entry right; //右孩子节点 - Entry parent; //父节点 - boolean color = BLACK; //节点的颜色,在红黑树中只有两种颜色,红色和黑色 - } - ``` - -* compare() - - ```java - //如果comparator为null,采用comparable.compartTo进行比较,否则采用指定比较器比较大小 - final int compare(Object k1, Object k2) { - return comparator==null ? ((Comparable)k1).compareTo((K)k2) - : comparator.compare((K)k1, (K)k2); - } - ``` - - - -参考文章:https://blog.csdn.net/weixin_33991727/article/details/91518677 - - - -*** - - - -#### WeakMap - -WeakHashMap 是基于弱引用的 - -内部的 Entry 继承 WeakReference,被弱引用关联的对象在下一次垃圾回收时会被回收,并且构造方法传入引用队列,用来在清理对象完成以后清理引用 - -```java -private static class Entry extends WeakReference implements Map.Entry { - Entry(Object key, V value, - ReferenceQueue queue, - int hash, Entry next) { - super(key, queue); - this.value = value; - this.hash = hash; - this.next = next; - } -} -``` - -WeakHashMap 主要用来实现缓存,使用 WeakHashMap 来引用缓存对象,由 JVM 对这部分缓存进行回收 - -Tomcat 中的 ConcurrentCache 使用了 WeakHashMap 来实现缓存功能,ConcurrentCache 采取分代缓存: - -* 经常使用的对象放入 eden 中,eden 使用 ConcurrentHashMap 实现,不用担心会被回收(伊甸园) - -* 不常用的对象放入 longterm,longterm 使用 WeakHashMap 实现,这些老对象会被垃圾收集器回收 - -* 当调用 get() 方法时,会先从 eden 区获取,如果没有找到的话再到 longterm 获取,当从 longterm 获取到就把对象放入 eden 中,从而保证经常被访问的节点不容易被回收 - -* 当调用 put() 方法时,如果 eden 的大小超过了 size,那么就将 eden 中的所有对象都放入 longterm 中,利用虚拟机回收掉一部分不经常使用的对象 - - ```java - public final class ConcurrentCache { - private final int size; - private final Map eden; - private final Map longterm; - - public ConcurrentCache(int size) { - this.size = size; - this.eden = new ConcurrentHashMap<>(size); - this.longterm = new WeakHashMap<>(size); - } - - public V get(K k) { - V v = this.eden.get(k); - if (v == null) { - v = this.longterm.get(k); - if (v != null) - this.eden.put(k, v); - } - return v; - } - - public void put(K k, V v) { - if (this.eden.size() >= size) { - this.longterm.putAll(this.eden); - this.eden.clear(); - } - this.eden.put(k, v); - } - } - - - - - -*** - - - -#### 面试题 - -输出一个字符串中每个字符出现的次数。 - -```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); - } -} -``` - - - -*** - - - -### 泛型 - -#### 概述 - -泛型(Generic): - 泛型就是一个标签:<数据类型> - 泛型可以在编译阶段约束只能操作某种数据类型。 - -注意: JDK 1.7开始之后,泛型后面的申明可以省略不写!! - **泛型和集合都只能支持引用数据类型,不支持基本数据类型。** - -```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); -} -``` - -优点:泛型在编译阶段约束了操作的数据类型,从而不会出现类型转换异常 - 体现的是Java的严谨性和规范性,数据类型,经常需要进行统一! - - - -**** - - - -#### 自定义 - -##### 自定义泛型类 - -泛型类:使用了泛型定义的类就是泛型类。 - -泛型类格式: - -```java -修饰符 class 类名<泛型变量>{ - -} -泛型变量建议使用 E , T , K , V -``` - -```java -public class GenericDemo { - public static void main(String[] args) { - MyArrayList list = new MyArrayList(); - MyArrayList list1 = new MyArrayList(); - list.add("自定义泛型类"); - } -} -class MyArrayList{ - public void add(E e){} - public void remove(E e){} -} -``` - - - -##### 自定义泛型方法 - -泛型方法:定义了泛型的方法就是泛型方法。 -泛型方法的定义格式: - -```java -修饰符 <泛型变量> 返回值类型 方法名称(形参列表){ - -} -``` - -方法定义了是什么泛型变量,后面就只能用什么泛型变量。 -泛型类的核心思想:把出现泛型变量的地方全部替换成传输的真实数据类型 - -```java -public class GenericDemo { - public static void main(String[] args) { - Integer[] num = {10 , 20 , 30 , 40 , 50}; - String s1 = arrToString(nums); - - String[] name = {"贾乃亮","王宝绿","陈羽凡"}; - String s2 = arrToString(names); - } - - public static String arrToString(T[] arr){ - -------------- - } -} -``` - - - -自定义泛型接口 - -泛型接口:使用了泛型定义的接口就是泛型接口。 -泛型接口的格式: - -```java -修饰符 interface 接口名称<泛型变量>{ - -} -``` - -```java -public class GenericDemo { - public static void main(String[] args) { - Data d = new StudentData(); - d.add(new Student()); - ................ - } -} - -public interface Data{ - void add(E e); - void delete(E e); - void update(E e); - E query(int index); -} -class Student{} -class StudentData implements Data{重写所有方法} -``` - - - -**** - - - -#### 泛型通配符 - -* 通配符:? - ?可以用在使用泛型的时候代表一切类型。 - E , T , K , V是在定义泛型的时候使用代表一切类型。 - -* 泛型的上下限: - ? extends Car : 那么?必须是Car或者其子类。(泛型的上限) - ? super Car :那么?必须是Car或者其父类。(泛型的下限。不是很常见) - -```java -//需求:开发一个极品飞车的游戏,所有的汽车都能一起参与比赛。 -public class GenericDemo { - public static void main(String[] args) { - ArrayList bmws = new ArrayList<>(); - ArrayList ads = new ArrayList<>(); - ArrayList dogs = new ArrayList<>(); - run(bmws); - //run(dogs); - } - //public static void run(ArrayList car){}//这样 dou对象也能进入 - public static void run(ArrayList car){} -} - -class Car{} -class BMW extends Car{} -class AD extends Car{} -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为常见的代码异常都设计一个类来代表。 - -错误:Error ,程序员无法处理的错误,只能重启系统,比如内存奔溃,JVM本身的奔溃 - -Java中异常继承的根类是:Throwable - -``` -异常的体系: - Throwable(根类,不是异常类) - / \ - Error Exception(异常,需要研究和处理) - / \ - 编译时异常 RuntimeException(运行时异常) - -``` - -Exception异常的分类: - -* 编译时异常:继承自Exception的异常或者其子类,编译阶段就会报错, - 必须程序员处理的。否则代码编译就不能通过!! -* 运行时异常: 继承自RuntimeException的异常或者其子类,编译阶段是不会出错的,它是在 - 运行时阶段可能出现,编译阶段是不会出错的,但是运行阶段可能出现,建议提前处理!! - - - -*** - - - -### 处理过程 - -异常的产生默认的处理过程解析。(自动处理的过程!) - -(1)默认会在出现异常的代码那里自动的创建一个异常对象:ArithmeticException(算术异常) -(2)异常会从方法中出现的点这里抛出给调用者,调用者最终抛出给JVM虚拟机。 -(3)虚拟机接收到异常对象后,先在控制台直接输出**异常栈**信息数据。 -(4)直接从当前执行的异常点干掉当前程序。 -(5)后续代码没有机会执行了,因为程序已经死亡。 - -```java -public class ExceptionDemo { - public static void main(String[] args) { - System.out.println("程序开始。。。。。。。。。。"); - chu( 10 ,0 ); - System.out.println("程序结束。。。。。。。。。。");//不执行 - } - public static void chu(int a , int b){ - int c = a / b ;// 出现了运行时异常,自动创建异常对象:ArithmeticException - System.out.println("结果是:"+c); - } -} -``` - - - -*** - - - -### 编译时异常 - -#### 概念 - -编译时异常:继承自Exception的异常或者其子类,没有继承RuntimeException - "编译时异常是编译阶段就会报错", - 必须程序员编译阶段就处理的。否则代码编译就报错!! - -编译时异常的作用是什么: - 是担心程序员的技术不行,在编译阶段就爆出一个错误, 目的在于提醒! - 提醒程序员这里很可能出错,请检查并注意不要出bug。 - -```java -public static void main(String[] args) throws ParseException { - String date = "2015-01-12 10:23:21"; - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - Date d = sdf.parse(date); - System.out.println(d); -} -``` - - - -#### 处理机制 - -##### throws - -在出现编译时异常的地方层层把异常抛出去给调用者,调用者最终抛出给JVM虚拟机。 -JVM虚拟机输出异常信息,直接干掉程序,这种方式与默认方式是一样的。 - -* 优点:可以解决代码编译时的错误 -* 运行时出现异常,程序还是会立即死亡! - -**Exception是异常最高类型可以抛出一切异常!** - -```java -public static void main(String[] args) throws Exception { - System.out.println("程序开始。。。。"); - String s = "2013-03-23 10:19:23"; - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - Date date = sdf.parse(s); - System.out.println("程序结束。。。。。"); -} -``` - - - -##### try/catch - -可以处理异常,并且出现异常后代码也不会死亡。 - -* 自己捕获异常和处理异常的格式:**捕获处理** - - ```java - try{ - // 监视可能出现异常的代码! - }catch(异常类型1 变量){ - // 处理异常 - }catch(异常类型2 变量){ - // 处理异常 - }...finall{ - //资源释放 - } - ``` - -* 监视捕获处理异常企业级写法: - Exception可以捕获处理一切异常类型! - - ```java - try{ - // 可能出现异常的代码! - }catch (Exception e){ - e.printStackTrace(); // **直接打印异常栈信息** - } - ``` - -**Throwable成员方法:** - `public String getMessage()` : 返回此 throwable 的详细消息字符串 - `public String toString()` : 返回此可抛出的简短描述 - `public void printStackTrace()` : 把异常的错误信息输出在控制台 - -```java -public static void main(String[] args) { - System.out.println("程序开始。。。。"); - try { - String s = "2013-03-23 10:19:23"; - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - Date date = sdf.parse(s); - InputStream is = new FileInputStream("D:/meinv.png"); - } catch (Exception e) { - e.printStackTrace(); - } - System.out.println("程序结束。。。。。"); -} -``` - - - -##### 方法三 - -在出现异常的地方把异常一层一层的抛出给最外层调用者,最外层调用者集中捕获处理!(**规范做法**) -这种方案最外层调用者可以知道底层执行的情况,同时程序在出现异常后也不会立即死亡(最好的方案) - -```java -public class ExceptionDemo{ - public static void main(String[] args){ - System.out.println("程序开始。。。。"); - try { - parseDate("2013-03-23 10:19:23"); - }catch (Exception e){ - e.printStackTrace(); - } - System.out.println("程序结束。。。。"); - } - public static void parseDate(String time) throws Exception{...} -} -``` - - - -*** - - - -### 运行时异常 - -#### 概念 - -​ 继承自RuntimeException的异常或者其子类, -​ 编译阶段是不会出错的,它是在运行时阶段可能出现的错误 -​ 运行时异常编译阶段可以处理也可以不处理,代码编译都能通过!! - -**常见的运行时异常。(面试题)** - -​ 1.数组索引越界异常: ArrayIndexOutOfBoundsException -​ 2.空指针异常 : NullPointerException,直接输出没问题,调用空指针的变量的功能就会报错! -​ 3.类型转换异常:ClassCastException -​ 4.迭代器遍历没有此元素异常:NoSuchElementException -​ 5.算术异常(数学操作异常):ArithmeticException -​ 6.数字转换异常: NumberFormatException - -```java -public class ExceptionDemo { - public static void main(String[] args) { - System.out.println("程序开始。。。。。。"); - // 1.数组索引越界异常: ArrayIndexOutOfBoundsException。 - int[] arrs = {10 ,20 ,30}; - System.out.println(arrs[3]); //出现了数组索引越界异常。代码在此处直接执行死亡! - - // 2.空指针异常 : NullPointerException。 - String name = null ; - System.out.println(name); // 直接输出没有问题 - System.out.println(name.length());//出现了空指针异常。代码直接执行死亡! - - /** 3.类型转换异常:ClassCastException。 */ - Object o = "齐天大圣"; - Integer s = (Integer) o; // 此处出现了类型转换异常。代码在此处直接执行死亡! - - /** 5.数学操作异常:ArithmeticException。 */ - int c = 10 / 0 ; // 此处出现了数学操作异常。代码在此处直接执行死亡! - - /** 6.数字转换异常: NumberFormatException。 */ - String num = "23aa"; - Integer it = Integer.valueOf(num); //出现了数字转换异常。代码在此处执行死亡! - - System.out.println("程序结束。。。。。。"); - } -} -``` - - - -#### 处理机制 - -运行时异常在编译阶段是不会报错,在运行阶段才会出错。 -运行时异常在编译阶段不处理不会报错,但是运行时出错了程序还是会死亡,运行时异常也建议要处理。 -运行时异常是自动往外抛出的,不需要我们手工抛出。 - -**运行时异常的处理规范**:直接在最外层捕获处理即可,底层会自动抛出!! - -```java -public class ExceptionDemo{ - public static void main(String[] args){ - System.out.println("程序开始。。。。"); - try{ - chu(10 / 0);//ArithmeticException: / by zero - System.out.println("操作成功!");//没输出 - }catch (Exception e){ - e.printStackTrace(); - System.out.println("操作失败!");//输出了 - } - System.out.println("程序结束。。。。");//输出了 - } - - public static void chu(int a , int b) { System.out.println( a / b );} -} -``` - - - -*** - - - -### Finally - -用在捕获处理的异常格式中的,放在最后面。 - -```java -try{ - // 可能出现异常的代码! -}catch(Exception e){ - e.printStackTrace(); -}finally{ - // 无论代码是出现异常还是正常执行,最终一定要执行这里的代码!! -} -try: 1次。 -catch:0-N次 (如果有finally那么catch可以没有!!) -finally: 0-1次 -``` - - - -**finally的作用**:可以在代码执行完毕以后进行资源的释放操作 - -资源:资源都是实现了Closeable接口的,都自带close()关闭方法! - -注意:如果在 finally 中出现了 return,会吞掉异常 - -```java -public class FinallyDemo { - public static void main(String[] args) { - System.out.println(chu());//一定会输出 finally,优先级比return高 - } - - public static int chu(){ - try{ - int a = 10 / 2 ; - return a ; - }catch (Exception e){ - e.printStackTrace(); - return -1; - }finally { - System.out.println("=====finally被执行"); - //return 111; // 不建议在finally中写return,会覆盖前面所有的return值! - } - } - public static void test(){ - InputStream is = null; - try{ - is = new FileInputStream("D:/cang.png"); - }catch (Exception e){ - e.printStackTrace(); - }finally { - System.out.println("==finally被执行==="); - // 回收资源。用于在代码执行完毕以后进行资源的回收操作! - try { - if(is!=null)is.close(); - } catch (Exception e) { - e.printStackTrace(); - } - } - } -} -``` - - - -**** - - - -### 注意事项 - -异常的语法注意: - -1. 运行时异常被抛出可以不处理,可以自动抛出;**编译时异常必须处理**;按照规范都应该处理 -2. **重写方法申明抛出的异常,应该与父类被重写方法申明抛出的异常一样或者范围更小** -3. 方法默认都可以自动抛出运行时异常, throws RuntimeException可以省略不写 -4. 当多异常处理时,捕获处理,前面的异常类不能是后面异常类的父类。 -5. 在try/catch后可以追加finally代码块,其中的代码一定会被执行,通常用于资源回收操作。 - - - -*** - - - -### 自定义异常 - -自定义异常: - -* 自定义编译时异常. - 1. 定义一个异常类继承Exception. - 2. 重写构造器。 - 3. 在出现异常的地方用throw new 自定义对象抛出! -* 自定义运行时异常. - 1. 定义一个异常类继承RuntimeException. - 2. 重写构造器。 - 3. 在出现异常的地方用throw new 自定义对象抛出! - -**throws: 用在方法上,用于抛出方法中的异常。** - 用于告诉调用者,本方法内部可能会抛出异常,请你处理一下 -**throw: 用在出现异常的地方,创建异常对象且立即从此处抛出。** - 将这个异常对象传递到调用者处,并结束当前方法的执行 - -```java -//需求:认为年龄小于0岁,大于200岁就是一个异常。 -public class ExceptionDemo { - public static void main(String[] args) { - try { - checkAge(101); - } catch (AgeIllegalException e) { - e.printStackTrace(); - } - } - - public static void checkAge(int age) throws ItheimaAgeIllegalException { - if(age < 0 || age > 200){//年龄在0-200之间 - throw new AgeIllegalException("/ age is illegal!"); - //throw new AgeIllegalRuntimeException("/ age is illegal!"); - }else{ - System.out.println("年龄是:" + age); - } - } -} - -public class AgeIllegalException extends Exception{ - Alt + Insert->Constructor -}//编译时异常 -public class AgeIllegalRuntimeException extends RuntimeException{ - public AgeIllegalRuntimeException() { - } - - public AgeIllegalRuntimeException(String message) { - super(message); - } -}//运行时异常 -``` - - - -*** - - - -### 异常作用 - -1、可以处理代码问题,防止程序出现异常后的死亡。 -2、提高了程序的健壮性和安全性。 - -```java -public class Demo{ - public static void main(String[] args){ - //请输入一个合法的年龄 - while(true){ - try{ - Scanner sc = new Scanner(System.in); - System.out.println("请您输入您的年年龄:"); - int age = sc.nextInt(); - System.out.println("年龄:"+age); - break; - }catch(Exception e){ - System.err.println("您的年龄是瞎输入的!"); - } - } - } -} -``` - - - - - -*** - - - -## λ - -### lambda - -#### 基本介绍 - -Lambda表达式是JDK1.8开始之后的新技术,是一种代码的新语法,一种特殊写法 - -作用:为了简化匿名内部类的代码写法 - -Lambda表达式的格式: - -```java -(匿名内部类被重写方法的形参列表) -> { - //被重写方法的方法体代码。 -} -``` - -Lambda表达式并不能简化所有匿名内部类的写法,只能简化**函数式接口的匿名内部类** - -简化条件:首先必须是接口,接口中只能有一个抽象方法 - -@FunctionalInterface函数式接口注解:一旦某个接口加上了这个注解,这个接口只能有且仅有一个抽象方法 - - - -*** - - - -#### 简化方法 - -Lambda表达式的省略写法(进一步在Lambda表达式的基础上继续简化) - -* 如果Lambda表达式的方法体代码只有一行代码,可以省略大括号不写,同时要省略分号;如果这行代码是return语句,必须省略return不写 -* 参数类型可以省略不写 -* 如果只有一个参数,参数类型可以省略,同时()也可以省略 - -```java -List names = new ArrayList<>(); -names.add("胡"); -names.add("甘"); -names.add("洪"); - -names.forEach(new Consumer() { - @Override - public void accept(String s) { - System.out.println(s); - } -}); - -names.forEach((String s) -> { - System.out.println(s); -}); - -names.forEach((s) -> { - System.out.println(s); -}); - -names.forEach(s -> { - System.out.println(s); -}); - -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 - -```java -public class CollectionsDemo { - public static void main(String[] args) { - List lists = new ArrayList<>();//...s1 s2 s3 - Collections.addAll(lists , s1 , s2 , s3); - Collections.sort(lists, new Comparator() { - @Override - public int compare(Student s1, Student s2) { - return s1.getAge() - s2.getAge(); - } - }); - - // 简化写法 - Collections.sort(lists ,(Student t1, Student t2) -> { - return t1.getAge() - t2.getAge(); - }); - // 参数类型可以省略,最简单的 - Collections.sort(lists ,(t1,t2) -> t1.getAge()-t2.getAge()); - } -} -``` - - - - - -*** - - - -### 方法引用 - -#### 基本介绍 - -方法引用:方法引用是为了进一步简化Lambda表达式的写法 - -方法引用的格式:类型或者对象::引用的方法 - -关键语法是:`::` - -```java -lists.forEach( s -> System.out.println(s)); -// 方法引用! -lists.forEach(System.out::println); -``` - - - -*** - - - -#### 静态方法 - -引用格式:`类名::静态方法` - -简化步骤:定义一个静态方法,把需要简化的代码放到一个静态方法中去 - -静态方法引用的注意事项:被引用的方法的参数列表要和函数式接口中的抽象方法的参数列表一致,才能引用简化 - -```java -//定义集合加入几个Student元素 -// 使用静态方法进行简化! -Collections.sort(lists, (o1, o2) -> Student.compareByAge(o1 , o2)); -// 如果前后参数是一样的,而且方法是静态方法,既可以使用静态方法引用 -Collections.sort(lists, Student::compareByAge); - -public class Student { - private String name ; - private int age ; - - public static int compareByAge(Student o1 , Student o2){ - return o1.getAge() - o2.getAge(); - } -} -``` - - - -*** - - - -#### 实例方法 - -引用格式:`对象::实例方法` - -简化步骤:定义一个实例方法,把需要的代码放到实例方法中去 - -实例方法引用的注意事项:被引用的方法的参数列表要和函数式接口中的抽象方法的参数列表一致。 - -```java -public class MethodDemo { - public static void main(String[] args) { - List lists = new ArrayList<>(); - lists.add("java1"); - lists.add("java2"); - lists.add("java3"); - // 对象是 System.out = new PrintStream(); - // 实例方法:println() - // 前后参数正好都是一个 - lists.forEach(s -> System.out.println(s)); - lists.forEach(System.out::println); - } -} -``` - - - -*** - - - -#### 特定类型 - -特定类型:String,任何类型 - -引用格式:`特定类型::方法` - -注意事项:如果第一个参数列表中的形参中的第一个参数作为了后面的方法的调用者,并且其余参数作为后面方法的形参,那么就可以用特定类型方法引用了 - -```java -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"}; - - // public static void sort(T[] a, Comparator c) - // 需求:按照元素的首字符(忽略大小写)升序排序!!! - Arrays.sort(strs, new Comparator() { - @Override - public int compare(String s1, String s2) { - return s1.compareToIgnoreCase(s2);//按照元素的首字符(忽略大小写) - } - }); - - Arrays.sort(strs, ( s1, s2 ) -> s1.compareToIgnoreCase(s2)); - - // 特定类型的方法引用: - Arrays.sort(strs, String::compareToIgnoreCase); - System.out.println(Arrays.toString(strs)); - } -} -``` - - - -*** - - - -#### 构造器 - -格式:`类名::new` - -注意事项:前后参数一致的情况下,又在创建对象,就可以使用构造器引用 - -```java -public class ConstructorDemo { - public static void main(String[] args) { - List lists = new ArrayList<>(); - lists.add("java1"); - lists.add("java2"); - lists.add("java3"); - - // 集合默认只能转成Object类型的数组。 - Object[] objs = lists.toArray(); - - // 我们想指定转换成字符串类型的数组!最新的写法可以结合构造器引用实现 - String[] strs = lists.toArray(new IntFunction() { - @Override - public String[] apply(int value) { - return new String[value]; - } - }); - String[] strs1 = lists.toArray(s -> new String[s]); - String[] strs2 = lists.toArray(String[]::new); - - System.out.println("String类型的数组:"+ Arrays.toString(strs2)); - } -} -``` - - - - - -*** - - - -## IO - -### Stream - -#### 概述 - -Stream流其实就是一根传送带,元素在上面可以被Stream流操作 - -作用: - -* 可以解决已有集合类库或者数组API的弊端。 -* Stream流简化集合和数组的操作 -* 链式编程 - -```java -list.stream().filter(new Predicate() { - @Override - public boolean test(String s) { - return s.startsWith("张"); - } - }); - -list.stream().filter(s -> s.startsWith("张")); -``` - - - -*** - - - -#### 获取流 - -集合获取Stream流用:default Stream stream() - -数组:Arrays.stream(数组) / Stream.of(数组); - -```java -// Collection集合获取Stream流。 -Collection c = new ArrayList<>(); -Stream listStream = c.stream(); - -//Map集合获取流 -// 先获取键的Stream流。 -Stream keysStream = map.keySet().stream(); -// 在获取值的Stream流 -Stream valuesStream = map.values().stream(); -// 获取键值对的Stream流(key=value: Map.Entry) -Stream> keyAndValues = map.entrySet().stream(); - -//数组获取流 -String[] arr = new String[]{"Java", "JavaEE" ,"Spring Boot"}; -Stream arrStream1 = Arrays.stream(arr); -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) )组成的流 | - -```java -public class StreamDemo { - public static void main(String[] args) { - List list = new ArrayList<>(); - list.add("张无忌"); list.add("周芷若"); list.add("赵敏"); - list.add("张强"); list.add("张三丰"); list.add("张三丰"); - //取以张开头并且名字是三位数的 - list.stream().filter(s -> s.startsWith("张") - .filter(s -> s.length == 3).forEach(System.out::println); - //统计数量 - long count = list.stream().filter(s -> s.startsWith("张") - .filter(s -> s.length == 3).count(); - //取前两个 - list.stream().filter(s -> s.length == 3).limit(2).forEach(...); - //跳过前两个 - list.stream().filter(s -> s.length == 3).skip(2).forEach(...); - - // 需求:把名称都加上“张三的:+xxx” - list.stream().map(s -> "张三的"+s).forEach(System.out::println); - // 需求:把名称都加工厂学生对象放上去!! - // list.stream().map(name -> new Student(name)); - list.stream.map(Student::new).forEach(System.out::println); - - //数组流 - Stream s1 = Stream.of(10,20,30,40,50); - //集合流 - Stream s2 = list.stream(); - //合并流 - Stream s3 = Stream.concat(s1,s2); - s3.forEach(System.out::println); - } -} -class Student{ - private String name; - //...... -} -``` - - - -*** - - - -#### 终结方法 - -终结方法:Stream调用了终结方法,流的操作就全部终结,不能继续使用,如foreach , count方法等 - -非终结方法:每次调用完成以后返回一个新的流对象,可以继续使用,支持**链式编程**! - -```java -// foreach终结方法 -list.stream().filter(s -> s.startsWith("张")) - .filter(s -> s.length() == 3).forEach(System.out::println); -``` - - - -*** - - - -#### 收集流 - -收集Stream流的含义:就是把Stream流的数据转回到集合中去。 - -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() | 把元素收集数组中 | - -```java -public static void main(String[] args) { - List list = new ArrayList<>(); - 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("张")); - List list = stream.collect(Collectors.toList()); - - //把stream流转换成数组。 - Stream stream2 =list.stream().filter(s -> s.startsWith("张")); - Object[] arr = stream2.toArray(); - // 可以借用构造器引用申明转换成的数组类型!!! - String[] arr1 = stream2.toArray(String[]::new); -} -``` - - - -*** - - - -### File - -#### 概述 - -File类:代表操作系统的文件对象,是用来操作操作系统的文件对象的,删除文件,获取文件信息,创建文件(文件夹),广义来说操作系统认为文件包含(文件和文件夹) - -File类构造器: - `public File(String pathname)`:根据路径获取文件对象 - `public File(String parent , String child)`:根据父路径和文件名称获取文件对象! - `public File(File parent , String child)` - -File类创建文件对象的格式: - -* `File f = new File("绝对路径/相对路径");` - * 绝对路径:从磁盘的的盘符一路走到目的位置的路径。 - * 绝对路径依赖具体的环境,一旦脱离环境,代码可能出错 - * 一般是定位某个操作系统中的某个文件对象 - * **相对路径**:不带盘符的(重点) - * 默认是直接相对到工程目录下寻找文件的。 - * 相对路径只能用于寻找工程下的文件,可以跨平台 - -* `File f = new File("文件对象/文件夹对象")` 广义来说:文件是包含文件和文件夹的 - -```java -public class FileDemo{ - public static void main(String[] args) { - // 1.创建文件对象:使用绝对路径 - // 文件路径分隔符: - // -- a.使用正斜杠: / - // -- b.使用反斜杠: \\ - // -- c.使用分隔符API:File.separator - //File f1 = new File("D:"+File.separator+"it"+File.separator - //+"图片资源"+File.separator+"beautiful.jpg"); - File f1 = new File("D:\\seazean\\图片资源\\beautiful.jpg"); - System.out.println(f1.length()); // 获取文件的大小,字节大小 - - // 2.创建文件对象:使用相对路径 - File f2 = new File("Day09Demo/src/dlei.txt"); - System.out.println(f2.length()); - - // 3.创建文件对象:代表文件夹。 - File f3 = new File("D:\\it\\图片资源"); - System.out.println(f3.exists());// 判断路径是否存在!! - } -} -``` - - - -*** - - - -#### 常用API - -##### 常用方法 - -`public String getAbsolutePath()` : 返回此File的绝对路径名字符串。 -`public String getPath()` : 获取创建文件对象的时候用的路径 -`public String getName()` : 返回由此File表示的文件或目录的名称。 -`public long length()` : 返回由此File表示的文件的长度(大小)。 -`public long length(FileFilter filter)` : 文件过滤器。 - -```java -public class FileDemo { - public static void main(String[] args) { - // 1.绝对路径创建一个文件对象 - File f1 = new File("E:/图片/meinv.jpg"); - // a.获取它的绝对路径。 - System.out.println(f1.getAbsolutePath()); - // b.获取文件定义的时候使用的路径。 - System.out.println(f1.getPath()); - // c.获取文件的名称:带后缀。 - System.out.println(f1.getName()); - // d.获取文件的大小:字节个数。 - System.out.println(f1.length()); - System.out.println("------------------------"); - - // 2.相对路径 - File f2 = new File("Day09Demo/src/dlei01.txt"); - // a.获取它的绝对路径。 - System.out.println(f2.getAbsolutePath()); - // b.获取文件定义的时候使用的路径。 - System.out.println(f2.getPath()); - // c.获取文件的名称:带后缀。 - System.out.println(f2.getName()); - // d.获取文件的大小:字节个数。 - System.out.println(f2.length()); - } -} - -``` - - - -##### 判断方法 - -`public boolean exists()` : 此File表示的文件或目录是否实际存在。 -`public boolean isDirectory()` : 此File表示的是否为目录。 -`public boolean isFile()` : 此File表示的是否为文件 - -```java -File f = new File("Day09Demo/src/dlei01.txt"); -// a.判断文件路径是否存在 -System.out.println(f.exists()); // true -// b.判断文件对象是否是文件,是文件返回true ,反之 -System.out.println(f.isFile()); // true -// c.判断文件对象是否是文件夹,是文件夹返回true ,反之 -System.out.println(f.isDirectory()); // false -``` - - - -##### 创建删除 - -`public boolean createNewFile()` : 当且仅当具有该名称的文件尚不存在时, 创建一个新的空文件。 -`public boolean delete()` : 删除由此File表示的文件或目录。 (只能删除空目录) -`public boolean mkdir()` : 创建由此File表示的目录。(只能创建一级目录) -`public boolean mkdirs()` : 可以创建多级目录(建议使用的) - -```java -public class FileDemo { - public static void main(String[] args) throws IOException { - File f = new File("Day09Demo/src/dlei02.txt"); - // a.创建新文件,创建成功返回true ,反之 - System.out.println(f.createNewFile()); - - // b.删除文件或者空文件夹 - System.out.println(f.delete()); - // 不能删除非空文件夹,只能删除空文件夹 - File f1 = new File("E:/it/aaaaa"); - System.out.println(f1.delete()); - - // c.创建一级目录 - File f2 = new File("E:/bbbb"); - System.out.println(f2.mkdir()); - - // d.创建多级目录 - File f3 = new File("D:/it/e/a/d/ds/fas/fas/fas/fas/fas/fas"); - System.out.println(f3.mkdirs()); - } -} -``` - - - -*** - - - -#### 遍历目录 - -- `public String[] list()`: - 获取当前目录下所有的"一级文件名称"到一个字符串数组中去返回。 -- `public File[] listFiles()(常用)`: - 获取当前目录下所有的"一级文件对象"到一个**文件对象数组**中去返回(**重点**) -- `public long lastModified` : - 返回此抽象路径名表示的文件上次修改的时间。 - -```java -public class FileDemo { - public static void main(String[] args) { - File dir = new File("D:\\seazean"); - // a.获取当前目录对象下的全部一级文件名称到一个字符串数组返回。 - String[] names = dir.list(); - for (String name : names) { - System.out.println(name); - } - // b.获取当前目录对象下的全部一级文件对象到一个File类型的数组返回。 - File[] files = dir.listFiles(); - for (File file : files) { - System.out.println(file.getAbsolutePath()); - } - - // c - File f1 = new File("D:\\it\\图片资源\\beautiful.jpg"); - long time = f1.lastModified(); // 最后修改时间! - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - System.out.println(sdf.format(time)); - } -} -``` - - - -*** - - - -#### 文件搜索 - -递归实现文件搜索(非规律递归) - (1)定义一个方法用于做搜索。 - (2)进入方法中进行业务搜索分析。 - -```java -/** - * 去某个目录下搜索某个文件 - * @param dir 搜索文件的目录。 - * @param fileName 搜索文件的名称。 - */ -public static void searchFiles(File dir , String fileName){ - // 1.判断是否存在该路径,是否是文件夹 - if(dir.exists() && dir.isDirectory()){ - // 2.提取当前目录下的全部一级文件对象 - File files = dir.listFiles();// 可能是null/也可能是空集合[] - // 3.判断是否存在一级文件对象,判断是否不为空目录 - if(files != null && files.length > 0){ - // 4.判断一级文件对象 - for(File file : files){ - // 5.判断file是文件还是文件夹 - if(file.isFile()){ - // 6.判断该文件是否为我要找的文件对象 - if(f.getName().contains(fileName)){//模糊查找 - sout(f.getAbsolutePath()); - try { - // 启动它(拓展) - Runtime r = Runtime.getRuntime(); - r.exec(f.getAbsolutePath()); - } catch (IOException e) { - e.printStackTrace(); - } - } - } else { - // 7.该文件是文件夹,文件夹要递归进入继续寻找 - searchFiles(file,fileName) - } - } - } - } -} -``` - - - -*** - - - -### Character - -字符集:各个国家为自己国家的字符取的一套编号规则 - -计算机的底层是不能直接存储字符的,只能存储二进制,010101。 - -美国人: - 8个开关一组就可以编码字符,1个字节。 - 2^8 = 256 - 一个字节存储一个字符完全够用了。 - -​ a 97 -​ b 98 - -​ A 65 -​ B 66 - -​ 0 48 -​ 1 49 -​ 这套编码是ASCII编码。 -​ 英文和数字在底层存储的时候都是采用1个字节存储的。 - -中国人: - 中国人的字符很多:9万左右字符。 - 2个字节编码一个中文字符,1个字节编码一个英文字符。 - 这套编码叫:GBK编码。 - 它也必须兼容ASCII编码表。 - -美国人: - 我来收集全球所有的字符,统一编号。这套编码叫 Unicode编码(万国码) - 一个英文等于两个字节,一个中文(含繁体)等于两个字节。中文标点占两个字节,英文标点占两个字节 - -​ UTF-8就是变种形式,它也必须兼容ASCII编码表。 -​ UTF-8一个中文一般占3个字节,中文标点占3个。英文字母和数字1个字节 -​ - -编码前与编码后的编码集必须一致才不会乱码!! - - - -*** - - - -### IOStream - -#### 概述 - -IO输入输出流:输入/输出流 - -* Input:输入 -* Output:输出 - -引入:File类只能操作文件对象本身,不能读写文件对象的内容,读写数据内容,应该使用IO流 - -IO流是一个水流模型:IO理解成水管,把数据理解成水流 - -IO流的分类: - -* 按照流的方向分为:输入流,输出流。 - * 输出流:以内存为基准,把内存中的数据写出到磁盘文件或者网络介质中去的流称为输出流 - 输出流的作用:写数据到文件,或者写数据发送给别人 - * 输入流:以内存为基准,把磁盘文件中的数据或者网络中的数据读入到内存中的流称为输入流 - 输入流的作用:读取数据到内存 -* 按照流的内容分为:字节流,字符流 - * 字节流:流中的数据的最小单位是一个一个的字节,这个流就是字节流 - * 字符流:流中的数据的最小单位是一个一个的字符,这个流就是字符流(**针对于文本内容**) - -流大体分为四大类: - -* 字节输入流:以内存为基准,把磁盘文件中的数据或者网络中的数据以一个一个的字节的形式读入到内存中去的流称为字节输入流 -* 字节输出流:以内存为基准,把内存中的数据以一个一个的字节写出到磁盘文件或者网络介质中去的流称为字节输出流 -* 字符输入流:以内存为基准,把磁盘文件中的数据或者网络中的数据以一个一个的字符的形式读入到内存中去的流称为字符输入流 -* 字符输出流:以内存为基准,把内存中的数据以一个一个的字符写出到磁盘文件或者网络介质中去的流称为字符输出流 - -```java -IO流的体系: - 字节流 字符流 - 字节输入流 字节输出流 字符输入流 字符输出流 -InputStream OutputStream Reader Writer (抽象类) -FileInputStream FileOutputStream FileReader FileWriter(实现类) -BufferedInputStream BufferedOutputStream BufferedReader BufferedWriter(实现类缓冲流) - InputStreamReader OutputStreamWriter -ObjectInputStream ObjectOutputStream -``` - - - -**** - - - -#### 字节流 - -##### 字节输入 - -FileInputStream文件字节输入流: - -* 作用:以内存为基准,把磁盘文件中的数据按照字节的形式读入到内存中的流 - -* 构造器: - `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)` - -```java -public class FileInputStreamDemo01 { - public static void main(String[] args) throws Exception { - // 1.创建文件对象定位dlei01.txt - File file = new File("Day09Demo/src/dlei01.txt"); - // 2.创建一个字节输入流管道与源文件接通 - InputStream is = new FileInputStream(file); - // 3.读取一个字节的编号返回,读取完毕返回-1 - //int code1 = is.read(); // 读取一滴水,一个字节 - //System.out.println((char)code1); - - // 4.使用while读取字节数 - // 定义一个整数变量存储字节 - int ch = 0 ; - while((ch = is.read())!= -1){ - System.out.print((char) ch); - } - } -} -``` - -一个一个字节读取英文和数字没有问题。但是一旦读取中文输出无法避免乱码,因为会截断中文的字节。一个一个字节的读取数据,性能也较差,所以**禁止使用上面的方案** - -采取下面的方案: - -```java -public static void main(String[] args) throws Exception { - //简化写法,底层实质上创建了File对象 - InputStream is = new FileInputStream("Day09Demo/src/dlei01.txt"); - byte[] buffer = new byte[3];//开发中使用byte[1024] - int len; - while((len = is.read(buffer)) !=-1){ - // 读取了多少就倒出多少! - String rs = new String(buffer, 0, len); - System.out.print(rs); - } -} -``` - -```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"); -InputStream is = new FileInputStream(f); -byte[] buffer = is.readAllBytes(); -String rs = new String(buffer); -System.out.println(rs); -``` - - - -##### 字节输出 - -FileOutputStream文件字节输出流: - -* 作用:以内存为基准,把内存中的数据,按照字节的形式写出到磁盘文件中去 - -* 构造器: - `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)` : 写一个字节数组的一部分出去 - 参数一,字节数组;参数二:起始字节索引位置,参数三:写多少个字节数出去。 - - - -* FileOutputStream字节输出流每次启动写数据的时候都会先清空之前的全部数据,重新写入: - `OutputStream os = new FileOutputStream("Day09Demo/out05")` : 覆盖数据管道 - `OutputStream os = new FileOutputStream("Day09Demo/out05" , true)` : 追加数据的管道 - -说明: - -* 字节输出流只能写字节出去,字节输出流默认是**覆盖**数据管道。 -* 换行用: **os.write("\r\n".getBytes());** -* 关闭和刷新:刷新流可以继续使用,关闭包含刷新数据但是流就不能使用了! - -```java -OutputStream os = new FileOutputStream("Day09Demo/out05"); -os.write(97);//a -os.write('b'); -os.write("\r\n".getBytes()); -os.write("我爱Java".getBytes()); -os.close(); -``` - - - -##### 文件复制 - -思想:字节是计算机中一切文件的组成,所以字节流适合做一切文件的复制 - -分析步骤: - (1)创建一个字节输入流管道与源文件接通。 - (2)创建一个字节输出流与目标文件接通。 - (3)创建一个字节数组作为桶 - (4)从字节输入流管道中读取数据,写出到字节输出流管道即可。 - (5)关闭资源! - -```java -public class CopyDemo01 { - public static void main(String[] args) { - InputStream is = null ; - OutputStream os = null ; - try{ - //(1)创建一个字节输入流管道与源文件接通。 - is = new FileInputStream("D:\\seazean\\图片资源\\meinv.jpg"); - //(2)创建一个字节输出流与目标文件接通。 - os = new FileOutputStream("D:\\seazean\\meimei.jpg"); - //(3)创建一个字节数组作为桶 - byte buffer = new byte[1024]; - //(4)从字节输入流管道中读取数据,写出到字节输出流管道即可 - int len = 0; - while((len = is.read(buffer)) != -1){ - os.write(buffer,0,len); - } - System.out.println("复制完成!"); - }catch (Exception e){ - e.printStackTrace(); - } finally { - /**(5)关闭资源! */ - try{ - if(os!=null)os.close(); - if(is!=null)is.close(); - }catch (Exception e){ - e.printStackTrace(); - } - } - } -} -``` - - - -*** - - - -#### 字符流 - -##### 字符输入 - -FileReader:文件字符输入流 - - * 作用:以内存为基准,把磁盘文件的数据以字符的形式读入到内存,读取文本文件内容到内存中去。 - * 构造器: - `public FileReader(File file)` : 创建一个字符输入流与源文件对象接通。 - `public FileReader(String filePath)` : 创建一个字符输入流与源文件路径接通。 - * 方法: - `public int read()` : 读取一个字符的编号返回! 读取完毕返回-1 - `public int read(char[] buffer)` : 读取一个字符数组,读取多少个就返回多少个,读取完毕返回-1 - * 结论: - 字符流一个一个字符的读取文本内容输出,可以解决中文读取输出乱码的问题,适合操作文本文件。 - 但是:一个一个字符的读取文本内容性能较差!! - 字符流按照**字符数组循环读取数据**,可以解决中文读取输出乱码的问题,而且性能也较好!! - * **字符流不能复制图片,视频等类型的文件**。字符流在读取完了字节数据后并没有直接往目的地写,而是先查编码表,查到对应的数据就将该数据写入目的地。如果查不到,则码表会将一些未知区域中的数据去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); - int ch; - while((ch = fr.read()) != -1){ - System.out.print((char)ch); - } - } -} -public class FileReaderDemo02 {//字符数组 - public static void main(String[] args) throws Exception { - Reader fr = new FileReader("Day10Demo/src/dlei01.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) { - System.out.print(new String(buffer, 0 , len)); - } - } -} -``` - - - -##### 字符输出 - -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"); -fw.write(97); // 字符a -fw.write('b'); // 字符b -fw.write("Java是最优美的语言!"); -fw.write("\r\n"); -fw.close; -``` - - - -**** - - - -#### 缓冲流 - -##### 基本介绍 - -作用:缓冲流可以提高字节流和字符流的读写数据的性能。 - -缓冲流分为四类: - (1)BufferedInputStream:字节缓冲输入流,可以提高字节输入流读数据的性能。 - (2)BufferedOutStream: 字节缓冲输出流,可以提高字节输出流写数据的性能。 - (3)BufferedReader: 字符缓冲输入流,可以提高字符输入流读数据的性能。 - (4)BufferedWriter: 字符缓冲输出流,可以提高字符输出流写数据的性能。 - - - -##### 字节缓冲输入流 - -字节缓冲输入流:BufferedInputStream - -作用:可以把低级的字节输入流包装成一个高级的缓冲字节输入流管道, 提高字节输入流读数据的性能 - -构造器:`public BufferedInputStream(InputStream in)` - -原理:缓冲字节输入流管道自带了一个 8KB 的缓冲池,每次可以直接借用操作系统的功能最多提取 8KB 的数据到缓冲池中去,以后我们直接从缓冲池读取数据,所以性能较好 - -```java -public class BufferedInputStreamDemo01 { - public static void main(String[] args) throws Exception { - // 1.定义一个低级的字节输入流与源文件接通 - InputStream is = new FileInputStream("Day10Demo/src/dlei04.txt"); - // 2.把低级的字节输入流包装成一个高级的缓冲字节输入流。 - BufferInputStream bis = new BufferInputStream(is); - // 3.定义一个字节数组按照循环读取。 - byte[] buffer = new byte[1024]; - int len; - while((len = bis.read(buffer)) != -1){ - String rs = new String(buffer, 0 , len); - System.out.print(rs); - } - } -} -``` - - - -##### 字节缓冲输出流 - -字节缓冲输出流:BufferedOutputStream - -作用:可以把低级的字节输出流包装成一个高级的缓冲字节输出流,从而提高写数据的性能 - -构造器:`public BufferedOutputStream(OutputStream os)` - -原理:缓冲字节输出流自带了8KB缓冲池,数据就直接写入到缓冲池中去,性能极高了 - -```java -public class BufferedOutputStreamDemo02 { - public static void main(String[] args) throws Exception { - // 1.写一个原始的字节输出流 - OutputStream os = new FileOutputStream("Day10Demo/src/dlei05.txt"); - // 2.把低级的字节输出流包装成一个高级的缓冲字节输出流 - BufferedOutputStream bos = new BufferedOutputStream(os); - // 3.写数据出去 - bos.write('a'); - bos.write(100); - bos.write("我爱中国".getBytes()); - bos.close(); - } -} - -``` - - - -##### 字节流的性能分析 - -利用字节流的复制统计各种写法形式下缓冲流的性能执行情况。 - -复制流: - (1)使用低级的字节流按照一个一个字节的形式复制文件。 - (2)使用低级的字节流按照一个一个字节数组的形式复制文件。 - (3)使用高级的缓冲字节流按照一个一个字节的形式复制文件。 - (4)使用高级的缓冲字节流按照一个一个字节数组的形式复制文件。 - -高级的缓冲字节流按照一个一个字节数组的形式复制文件,性能最高,建议使用 - - - -**** - - - -##### 字符缓冲输入流 - -字符缓冲输入流:BufferedReader - -作用:字符缓冲输入流把字符输入流包装成高级的缓冲字符输入流,可以提高字符输入流读数据的性能。 - -构造器:`public BufferedReader(Reader reader)` - -原理:缓冲字符输入流默认会有一个8K的字符缓冲池,可以提高读字符的性能 - -按照行读取数据的功能:`public String readLine()` 读取一行数据返回,读取完毕返回null - -```java -public static void main(String[] args) throws Exception { - // 1.定义一个原始的字符输入流读取源文件 - Reader fr = new FileReader("Day10Demo/src/dlei06.txt"); - // 2.把低级的字符输入流管道包装成一个高级的缓冲字符输入流管道 - BufferedReader br = new BufferedReader(fr); - // 定义一个字符串变量存储每行数据 - String line; - while((line = br.readLine()) != null){ - System.out.println(line); - } - br.close(); - //淘汰数组循环读取 - //char[] buffer = new char[1024]; - //int len; - //while((len = br.read(buffer)) != -1){ - //System.out.println(new String(buffer , 0 , len)); -} -``` - - - -##### 字符缓冲输出流 - -符缓冲输出流:BufferedWriter - -作用:把低级的字符输出流包装成一个高级的缓冲字符输出流,提高写字符数据的性能。 - -构造器:`public BufferedWriter(Writer writer)` - - 原理:高级的字符缓冲输出流多了一个8k的字符缓冲池,写数据性能极大提高了 - -字符缓冲输出流多了一个换行的特有功能:`public void newLine()` **新建一行** - -```java -public static void main(String[] args) throws Exception { - Writer fw = new FileWriter("Day10Demo/src/dlei07.txt",true);//追加 - BufferedWriter bw = new BufferedWriter(fw); - - bw.write("我爱学习Java"); - bw.newLine();//换行 - bw.close(); -} -``` - - - -*** - - - -##### 高效原因 - -字符型缓冲流高效的原因: - -* BufferedReader:每次调用read方法,只有第一次从磁盘中读取了8192(**8k**)个字符,存储到该类型对象的缓冲区数组中,将其中一个返回给调用者,再次调用read方法时,就不需要访问磁盘,直接从缓冲区中拿出一个数据即可,提升了效率 -* BufferedWriter:每次调用write方法,不会直接将字符刷新到文件中,而是存储到字符数组中,等字符数组写满了,才一次性刷新到文件中,减少了和磁盘交互的次数,提升了效率 - -字节型缓冲流高效的原因: - -* BufferedInputStream:在该类型中准备了一个数组,存储字节信息,当外界调用read()方法想获取一个字节的时候,该对象从文件中一次性读取了8192个字节到数组中,只返回了第一个字节给调用者。将来调用者再次调用read方法时,当前对象就不需要再次访问磁盘,只需要从数组中取出一个字节返回给调用者即可,由于读取的是数组,所以速度非常快。当8192个字节全都读取完成之后,再需要读取一个字节,就得让该对象到文件中读取下一个8192个字节 -* BufferedOutputStream:在该类型中准备了一个数组,存储字节信息,当外界调用write方法想写出一个字节的时候,该对象直接将这个字节存储到了自己的数组中,而不刷新到文件中。一直到该数组所有8192个位置全都占满,该对象才把这个数组中的所有数据一次性写出到目标文件中。如果最后一次循环过程中,没有将数组写满,最终在关闭流对象的时候,也会将该数组中的数据刷新到文件中。 - - - -注意:**字节流和字符流,都是装满时自动写出,或者没满时手动flush写出,或close时刷新写出** - - - -*** - - - -#### 转换流 - -##### 乱码问题 - -``` -字符流读取: - 代码编码 文件编码 中文情况。 - UTF-8 UTF-8 不乱码! - GBK GBK 不乱码! - UTF-8 GBK 乱码! -``` - -如果代码编码和读取的文件编码一致。字符流读取的时候不会乱码。 -如果代码编码和读取的文件编码不一致。字符流读取的时候会乱码。 - - - -##### 字符输入转换流 - -字符输入转换流InputStreamReader - -作用:解决字符流读取不同编码的乱码问题,把原始的**字节流**按照默认的编码或指定的编码**转换成字符输入流** - -构造器: - -* `public InputStreamReader(InputStream is)` : 使用当前代码默认编码(UTF-8)转换成字符流 -* `public InputStreamReader(InputStream is,String charset)` : 指定编码把字节流转换成字符流 - -```java -public class InputStreamReaderDemo{ - public static void main(String[] args) throws Exception { - // 1.提取GBK文件的原始字节流 - InputStream is = new FileInputStream("D:\\seazean\\Netty.txt"); - // 2.把原始字节输入流通过转换流,转换成 字符输入转换流InputStreamReader - InputStreamReader isr = new InputStreamReader(is,"GBK"); - // 3.包装成缓冲流 - BufferedReader br = new BufferedReader(isr); - //循环读取 - String line; - while((line = br.readLine()) != null){ - System.out.println(line); - } - } -} -``` - - - -##### 字符输出转换流 - -字符输出转换流:OutputStreamWriter - -作用:可以指定编码**把字节输出流转换成字符输出流**,可以指定写出去的字符的编码 - -构造器: - -* `public OutputStreamWriter(OutputStream os)` : 用默认编码UTF-8把字节输出流转换成字符输出流 -* `public OutputStreamWriter(OutputStream os ,String charset)` : 指定编码把字节输出流转换成 - -```Java -OutputStream os = new FileOutputStream("Day10Demo/src/dlei07.txt"); -OutputStreamWriter osw = new OutputStreamWriter(os,"GBK"); -osw.write("我在学习Java"); -osw.close(); -``` - - - -**** - - - -#### 序列化 - -##### 介绍 - -对象序列化:把Java对象转换成字节序列的过程,将对象写入到IO流中。 对象 => 文件中 - -对象反序列化:把字节序列恢复为Java对象的过程,从IO流中恢复对象。 文件中 => 对象 - -transient 关键字修饰的成员变量,将不参与序列化! - - - -##### 序列化 - -对象序列化流(对象字节输出流):ObjectOutputStream - -作用:把内存中的Java对象数据保存到文件中去 - -构造器:`public ObjectOutputStream(OutputStream out)` - -序列化方法:`public final void writeObject(Object obj)` - -注意:对象如果想参与序列化,对象必须实现序列化接口 **implements Serializable** ,否则序列化失败! - -```java -public class SerializeDemo01 { - public static void main(String[] args) throws Exception { - // 1.创建User用户对象 - User user = new User("seazean","980823","七十一"); - // 2.创建低级的字节输出流通向目标文件 - OutputStream os = new FileOutputStream("Day10Demo/src/obj.dat"); - // 3.把低级的字节输出流包装成高级的对象字节输出流ObjectOutputStream - ObjectOutputStream oos = new ObjectOutputStream(os); - // 4.通过对象字节输出流序列化对象: - oos.writeObject(user); - // 5.释放资源 - oos.close(); - System.out.println("序列化对象成功~~~~"); - } -} - -class User implements Serializable { - // 加入序列版本号 - private static final long serialVersionUID = 1L; - - private String loginName; - private transient String passWord; - private String userName; - ///get+set -} -``` - - - -**** - - - -##### 反序列 - -对象反序列化(对象字节输入流):ObjectInputStream - -作用:读取序列化的对象文件恢复到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"); - ObjectInputStream ois = new ObjectInputStream(is); - User user = (User)ois.readObject();//反序列化 - System.out.println(user); - System.out.println("反序列化完成!"); - } -} -class User implements Serializable { - // 加入序列版本号 - private static final long serialVersionUID = 1L; - //........ -} -``` - - - -**** - - - -#### 打印流 - -打印流 PrintStream / PrintWriter - -打印流的作用: - -* 可以方便,快速的写数据出去,可以实现打印什么类型,就是什么类型 -* PrintStream/PrintWriter 不光可以打印数据,还可以写字节数据和字符数据出去 -* **System.out.print() 底层基于打印流实现的** - -构造器: - -* `public PrintStream(OutputStream os)` -* `public PrintStream(String filepath)` - -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"); - ps.println(任何类型的数据); - ps.print(不换行); - ps.write("我爱你".getBytes()); - ps.close(); - } -} -public class PrintStreamDemo02 { - public static void main(String[] args) throws Exception { - System.out.println("==seazean0=="); - PrintStream ps = new PrintStream("Day10Demo/src/log.txt"); - System.setOut(ps); // 让系统的输出流向打印流 - //不输出在控制台,输出到文件里 - System.out.println("==seazean1=="); - System.out.println("==seazean2=="); - } -} -``` - - - -*** - - - -### Close - -try-with-resources: - -```java -try( - // 这里只能放置资源对象,用完会自动调用close()关闭 -){ - -}catch(Exception e){ - e.printStackTrace(); -} -``` - -资源类一定是实现了 Closeable 接口,实现这个接口的类就是资源 - -有 close() 方法,try-with-resources 会自动调用它的 close() 关闭资源 - -```java -try( - /** (1)创建一个字节输入流管道与源文件接通。 */ - InputStream is = new FileInputStream("D:\\seazean\\图片资源\\meinv.jpg"); - /** (2)创建一个字节输出流与目标文件接通。*/ - OutputStream os = new FileOutputStream("D:\\seazean\\meimei.jpg"); - /** (5)关闭资源!是自动进行的 */ -){ - byte[] buffer = new byte[1024]; - int len = 0; - while((len = is.read(buffer)) != -1){ - os.write(buffer, 0 , len); - } - System.out.println("复制完成!"); -}catch (Exception e){ - e.printStackTrace(); -} -``` - - - -*** - - - -### Properties - -Properties:属性集对象。就是一个Map集合,一个键值对集合 - -核心作用:Properties代表的是一个属性文件,可以把键值对数据存入到一个属性文件 - -属性文件:后缀是.properties结尾的文件,里面的内容都是 key=value - -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) | 保存数据到属性文件中去 | - -````java -public class PropertiesDemo01 { - public static void main(String[] args) throws Exception { - // a.创建一个属性集对象:Properties的对象。 - Properties properties = new Properties();//{} - properties.setProperty("admin" , "123456"); - // b.把属性集对象的数据存入到属性文件中去(重点) - OutputStream os = new FileOutputStream("Day10Demo/src/users.properties"); - properties.store(os,"i am very happy!!我保存了用户数据!"); - //参数一:被保存数据的输出管道 - //参数二:保存心得。就是对象保存的数据进行解释说明! - } -} -```` - -````java -public class PropertiesDemo02 { - public static void main(String[] args) throws Exception { - Properties properties = new Properties();//底层基于map集合 - properties.load(new FileInputStream("Day10Demo/src/users.properties")); - System.out.println(properties); - System.out.println(properties.getProperty("admin")); - - Set set = properties.stringPropertyNames(); - for (String s : set) { - String value = properties.getProperty(s); - System.out.println(s + value); - } - } -} -```` - - - -*** - - - -### RandomIO - -RandomAccessFile类:该类的实例支持读取和写入随机访问文件 - -构造器: -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个字节的数据到字节数组 - -```java -public static void main(String[] args) throws Exception { - RandomAccessFile rf = new RandomAccessFile(new File(),"rw"); - rf.write("hello world".getBytes()); - rf.seek(5);//helloxxxxld - rf.write("xxxx".getBytes()); - rf.close(); -} -``` - - - -*** - - - -### Commons - -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 | 序列化相关的类 | - -IOUtils 和 FileUtils 可以方便的复制文件和文件夹 - -```java -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")); - // 2.完成文件复制到某个文件夹下! - FileUtils.copyFileToDirectory(new File("Day13Demo/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")); - } -} -``` - - - - - -**** - - - -## 网络 - -### 介绍 - -#### 网络编程 - -网络编程,就是在一定的协议下,实现两台计算机的通信的技术 - -通信一定是基于软件结构实现的: - -* C/S结构 :全称为Client/Server结构,是指客户端和服务器结构,常见程序有QQ、IDEA等软件。 -* B/S结构 :全称为Browser/Server结构,是指浏览器和服务器结构。 - -两种架构各有优势,但是无论哪种架构,都离不开网络的支持。 - -网络通信的三要素: - -1. 协议:计算机网络客户端与服务端通信必须约定和彼此遵守的通信规则,HTTP、FTP、TCP、UDP、SMTP - -2. IP地址:互联网协议地址(Internet Protocol Address),用来给一个网络中的计算机设备做唯一的编号 - - * IPv4 :4个字节,32位组成,192.168.1.1 - * Pv6:可以实现为所有设备分配IP 128位 - - * 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以上的端口号。如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败,报出端口被占用异常 - -利用**协议+IP地址+端口号** 三元组合,就可以标识网络中的进程了,那么进程间的通信就可以利用这个标识与其它进程进行交互。 - - - -**** - - - -#### 通信协议 - -网络通信协议:对计算机必须遵守的规则,只有遵守这些规则,计算机之间才能进行通信 - -> 应用层:应用程序(QQ,微信,浏览器),可能用到的协议(HTTP,FTP,SMTP) -> -> 传输层:TCP/IP协议 - UDP协议 -> -> 网络层 :IP协议,封装自己的IP和对方的IP和端口 -> -> 数据链路层 : 进入到硬件(网) - -TCP/IP协议:传输控制协议 (Transmission Control Protocol) - -TCP:面向连接的安全的可靠的传输通信协议 - -* 在通信之前必须确定对方在线并且连接成功才可以通信 -* 例如下载文件、浏览网页等(要求可靠传输) - -UDP:用户数据报协议(User Datagram Protocol),是一个面向无连接的不可靠传输的协议 - -* 直接发消息给对方,不管对方是否在线,发消息后也不需要确认 -* 无线(视频会议,通话),性能好,可能丢失一些数据 - - - -**** - - - -#### Java模型 - -相关概念: - -* 同步:当前线程要自己进行数据的读写操作(自己去银行取钱) -* 异步:当前线程可以去做其他事情(委托别人拿银行卡到银行取钱,然后给你) -* 阻塞:在数据没有的情况下,还是要继续等待着读(排队等待) -* 非阻塞:在数据没有的情况下,会去做其他事情,一旦有了数据再来获取(柜台取款,取个号,然后坐在椅子上做其它事,等号广播会通知你办理) - -Java中的通信模型: - -1. BIO表示同步阻塞式通信,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善。 - 同步阻塞式性能极差:大量线程,大量阻塞 - -2. 伪异步通信:引入线程池,不需要一个客户端一个线程,实现线程复用来处理很多个客户端,线程可控。 - 高并发下性能还是很差:线程数量少,数据依然是阻塞的;数据没有来线程还是要等待 - -3. NIO表示**同步非阻塞IO**,服务器实现模式为请求对应一个线程,客户端发送的连接会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理 - - 工作原理:1个主线程专门负责接收客户端,1个线程轮询所有的客户端,发来了数据才会开启线程处理 - 同步:线程还要不断的接收客户端连接,以及处理数据 - 非阻塞:如果一个管道没有数据,不需要等待,可以轮询下一个管道是否有数据 - -4. AIO表示异步非阻塞IO,AIO 引入异步通道的概念,采用了 Proactor 模式,有效的请求才启动线程,特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用 - 异步:服务端线程接收到了客户端管道以后就交给底层处理IO通信,线程可以做其他事情 - 非阻塞:底层也是客户端有数据才会处理,有了数据以后处理好通知服务器应用来启动线程进行处理 - -各种模型应用场景: - -* BIO适用于连接数目比较小且固定的架构,该方式对服务器资源要求比较高,并发局限于应用中,程序简单 -* NIO适用于连接数目多且连接比较短(轻操作)的架构,如聊天服务器,并发局限于应用中,编程复杂,JDK 1.4开始支持 -* AIO适用于连接数目多且连接比较长(重操作)的架构,如相册服务器,充分调用操作系统参与并发操作,编程复杂,JDK 1.7开始支持 - - - - - -**** - - - -### I/O - -#### IO模型 - -##### 五种模型 - -对于一个套接字上的输入操作,第一步是等待数据从网络中到达,当数据到达时被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区 - -Linux 有五种 I/O 模型: - -- 阻塞式 I/O -- 非阻塞式 I/O -- I/O 复用(select 和 poll) -- 信号驱动式 I/O(SIGIO) -- 异步 I/O(AIO) - -五种模型对比: - -* 同步 I/O 包括阻塞式 I/O、非阻塞式 I/O、I/O 复用和信号驱动 I/O ,它们的主要区别在第一个阶段,非阻塞式 I/O 、信号驱动 I/O 和异步 I/O 在第一阶段不会阻塞 - -- 同步 I/O:将数据从内核缓冲区复制到应用进程缓冲区的阶段(第二阶段),应用进程会阻塞 -- 异步 I/O:第二阶段应用进程不会阻塞 - - - -*** - - - -##### 阻塞式IO - -应用进程通过系统调用 recvfrom 接收数据,会被阻塞,直到数据从内核缓冲区复制到应用进程缓冲区中才返回。阻塞不意味着整个操作系统都被阻塞,其它应用进程还可以执行,只是当前阻塞进程不消耗 CPU 时间,这种模型的 CPU 利用率会比较高 - -recvfrom() 用于接收 Socket 传来的数据,并复制到应用进程的缓冲区 buf 中,把 recvfrom() 当成系统调用 - -![](https://gitee.com/seazean/images/raw/master/Java/IO模型-阻塞式IO.png) - - - -*** - - - -##### 非阻塞式 - -应用进程通过 recvfrom 调用不停的去和内核交互,直到内核准备好数据。如果没有准备好数据,内核返回一个错误码,过一段时间应用进程再执行 recvfrom 系统调用,在两次发送请求的时间段,进程可以进行其他任务,这种方式称为轮询(polling) - -由于 CPU 要处理更多的系统调用,因此这种模型的 CPU 利用率比较低 - -![](https://gitee.com/seazean/images/raw/master/Java/IO模型-非阻塞式IO.png) - - - -*** - - - -##### 信号驱动 - -应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,等待数据阶段应用进程是非阻塞的。当内核数据准备就绪时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中 - -相比于非阻塞式 I/O 的轮询方式,信号驱动 I/O 的 CPU 利用率更高 - -![](https://gitee.com/seazean/images/raw/master/Java/IO模型-信号驱动IO.png) - - - -*** - - - -##### IO复用 - -IO 复用模型使用 select 或者 poll 函数等待数据,select 会监听所有注册好的 IO,等待多个套接字中的任何一个变为可读,等待过程会被**阻塞**,当某个套接字准备好数据变为可读时 select 调用就返回,然后调用 recvfrom 把数据从内核复制到进程中 - -IO 复用让单个进程具有处理多个 I/O 事件的能力,又被称为 Event Driven I/O,即事件驱动 I/O - -如果一个 Web 服务器没有 I/O 复用,那么每一个 Socket 连接都要创建一个线程去处理,如果同时有几万个连接,就需要创建相同数量的线程。相比于多进程和多线程技术,I/O 复用不需要进程线程创建和切换的开销,系统开销更小 - -![](https://gitee.com/seazean/images/raw/master/Java/IO模型-IO复用模型.png) - - - -*** - - - -##### 异步IO - -应用进程执行 aio_read 系统调用会立即返回,给内核传递描述符、缓冲区指针、缓冲区大小等。应用进程可以继续执行不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。 - -异步 I/O 与信号驱动 I/O 的区别在于,异步 I/O 的信号是通知应用进程 I/O 完成,而信号驱动 I/O 的信号是通知应用进程可以开始 I/O - -![](https://gitee.com/seazean/images/raw/master/Java/IO模型-异步IO模型.png) - - - -**** - - - -#### 多路复用 - -##### select - -###### 函数 - -socket 不是文件,只是一个标识符,但是 Unix 操作系统把所有东西都**看作**是文件,所以 socket 说成 file descriptor,也就是 fd - -select 允许应用程序监视一组文件描述符,等待一个或者多个描述符成为就绪状态,从而完成 I/O 操作。 - -```c -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 有三种类型的描述符:readset、writeset、exceptset,对应读、写、异常条件的描述符集合 - -- n 是监测的 socket 的最大数量 - -- timeout 为超时参数,调用 select 会一直阻塞直到有描述符的事件到达或者等待的时间超过 timeout - - ```c - struct timeval{ - long tv_sec; //秒 - long tv_usec;//微秒 - } - ``` - - timeout == null:等待无限长的时间 - tv_sec == 0 && tv_usec == 0:获取后直接返回,不阻塞等待 - tv_sec != 0 || tv_usec != 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 -``` - -示例: - -```c -sockfd = socket(AF_INET, SOCK_STREAM, 0); -memset(&addr, 0, sizeof(addr))); -addr.sin_family = AF_INET; -addr.sin_port = htons(2000); -addr.sin_addr.s_addr = INADDR_ANY; -bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));//绑定连接 -listen(sockfd, 5);//监听5个端口 -for(i = 0; i < 5; i++) { - memset(&client, e, sizeof(client)); - addrlen = sizeof(client); - fds[i] = accept(sockfd, (struct sockaddr*)&client, &addrlen); - //将监听的对应的文件描述符fd存入fds:[3,4,5,6,7] - if(fds[i] > max) - max = fds[i]; -} -while(1) { - FD_ZERO(&rset);//置为0 - for(i = 0; i < 5; i++) { - FD_SET(fds[i], &rset);//对应位置1 [0001 1111 00.....] - } - print("round again"); - select(max + 1, &rset, NULL, NULL, NULL);//监听 - - for(i = 0; i <5; i++) { - if(FD_ISSET(fds[i], &rset)) {//判断监听哪一个端口 - memset(buffer, 0, MAXBUF); - read(fds[i], buffer, MAXBUF);//进入内核态读数据 - print(buffer); - } - } -} -``` - - - -参考视频:https://www.bilibili.com/video/BV19D4y1o797 - - - -**** - - - -###### 流程 - -select 调用流程图: - -![](https://gitee.com/seazean/images/raw/master/Java/IO-select调用过程.png) - -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 便被唤醒 -5. poll 方法返回时会返回一个描述读写操作是否就绪的 mask 掩码,根据这个 mask 掩码给 fd_set 赋值 -6. 如果遍历完所有的 fd,还没有返回一个可读写的 mask 掩码,则会调用 schedule_timeout 让调用 select 的进程(就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程,如果超过一定的超时时间(schedule_timeout指定),没有其他线程唤醒,则调用 select 的进程会重新被唤醒获得 CPU,进而重新遍历 fd,判断有没有就绪的 fd -7. 把 fd_set 从内核空间拷贝到用户空间,阻塞进程继续执行 - - - -参考文章:https://www.cnblogs.com/anker/p/3265058.html - -其他流程图:https://www.processon.com/view/link/5f62b9a6e401fd2ad7e5d6d1 - - - -**** - - - -##### poll - -poll 的功能与 select 类似,也是等待一组描述符中的一个成为就绪状态 - -```c -int poll(struct pollfd *fds, unsigned int nfds, int timeout); -``` - -poll 中的描述符是 pollfd 类型的数组,pollfd 的定义如下: - -```c -struct pollfd { - int fd; /* file descriptor */ - short events; /* requested events */ - short revents; /* returned events */ -}; -``` - -select 和 poll 对比: - -- select 会修改描述符,而 poll 不会 -- select 的描述符类型使用数组实现,有描述符的限制;而 poll 使用**链表**实现,没有描述符数量的限制 -- poll 提供了更多的事件类型,并且对描述符的重复利用上比 select 高 - -* select 和 poll 速度都比较慢,每次调用都需要将全部描述符数组 fd 从应用进程缓冲区复制到内核缓冲区,同时每次都需要在内核遍历传递进来的所有 fd ,这个开销在 fd 很多时会很大 -* 几乎所有的系统都支持 select,但是只有比较新的系统支持 poll -* select 和 poll 的时间复杂度 O(n),对 socket 进行扫描时是线性扫描,即采用轮询的方法,效率较低,因为并不知道具体是哪个 socket 具有事件,所以随着 FD 的增加会造成遍历速度慢的**线性下降**性能问题 -* poll 还有一个特点是水平触发,如果报告了 fd 后,没有被处理,那么下次 poll 时会再次报告该 fd -* 如果一个线程对某个描述符调用了 select 或者 poll,另一个线程关闭了该描述符,会导致调用结果不确定 - - - -参考文章:https://github.com/CyC2018/CS-Notes/blob/master/notes/Socket.md - - - -**** - - - -##### epoll - -###### 函数 - -epoll 使用事件的就绪通知方式,通过 epoll_ctl() 向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵**红黑树**上,一旦该 fd 就绪,内核通过 callback 回调函数将 I/O 准备好的描述符加入到一个**链表**中管理,进程调用 epoll_wait() 便可以得到事件完成的描述符 - -```c -int epoll_create(int size); -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_ctl:epoll 的事件注册函数,select 函数是调用时指定需要监听的描述符和事件,epoll 先将用户感兴趣的描述符事件注册到 epoll 空间。此函数是非阻塞函数,用来增删改 epoll 空间内的描述符,参数解释: - - * epfd:epoll 结构的进程 fd 编号,函数将依靠该编号找到对应的 epoll 结构 - - * op:表示当前请求类型,有三个宏定义: - - * EPOLL_CTL_ADD:注册新的 fd 到 epfd 中 - * EPOLL_CTL_MOD:修改已经注册的 fd 的监听事件 - * EPOLL_CTI_DEL:从 epfd 中删除一个 fd - - * fd:需要监听的文件描述符,一般指 socket_fd - - * event:告诉内核对该 fd 资源感兴趣的事件,epoll_event 的结构: - - ```c - struct epoll_event { - _uint32_t events; /*epoll events*/ - epoll_data_t data; /*user data variable*/ - } - ``` - - events 可以是以下几个宏集合:EPOLLIN、EPOLOUT、EPOLLPRI、EPOLLERR、EPOLLHUP(挂断)、EPOLET(边缘触发)、EPOLLONESHOT(只监听一次,事件触发后自动清除该 fd,从 epoll 列表) - -* epoll_wait:等待事件的产生,类似于 select() 调用,返回值为本次就绪的 fd 个数 - - * epfd:指定感兴趣的 epoll 事件列表 - * events:指向一个 epoll_event 结构数组,当函数返回时,内核会把就绪状态的数据拷贝到该数组 - * maxevents:标明 epoll_event 数组最多能接收的数据量,即本次操作最多能获取多少就绪数据 - * timeout:单位为毫秒 - * 0:表示立即返回,非阻塞调用 - * -1:阻塞调用,直到有用户感兴趣的事件就绪为止 - * 大于0:阻塞调用,阻塞指定时间内如果有事件就绪则提前返回,否则等待指定时间后返回 - -epoll 的描述符事件有两种触发模式:LT(level trigger)和 ET(edge trigger): - -* LT 模式:当 epoll_wait() 检测到描述符事件到达时,将此事件通知进程,进程可以不立即处理该事件,下次调用 epoll_wait() 会再次通知进程。是默认的一种模式,并且同时支持 Blocking 和 No-Blocking -* ET 模式:通知之后进程必须立即处理事件,下次再调用 epoll_wait() 时不会再得到事件到达的通知。减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高;只支持 No-Blocking,以避免由于一个文件的阻塞读/阻塞写操作把处理多个文件描述符的任务饥饿 - -```c -// 创建 epoll 描述符,每个应用程序只需要一个,用于监控所有套接字 -int pollingfd = epoll_create(0xCAFE); -if ( pollingfd < 0 )// report error -// 初始化 epoll 结构 -struct epoll_event ev = { 0 }; - -// 将连接类实例与事件相关联,可以关联任何想要的东西 -ev.data.ptr = pConnection1; - -// 监视输入,并且在事件发生后不自动重新准备描述符 -ev.events = EPOLLIN | EPOLLONESHOT; -// 将描述符添加到监控列表中,即使另一个线程在epoll_wait中等待,描述符将被正确添加 -if ( epoll_ctl( epollfd, EPOLL_CTL_ADD, pConnection1->getSocket(), &ev) != 0 ) - // report error - -// 最多等待 20 个事件 -struct epoll_event pevents[20]; - -// 等待10秒,检索20个并存入epoll_event数组 -int ready = epoll_wait(pollingfd, pevents, 20, 10000); -// 检查epoll是否成功 -if ( ret == -1)// report error and abort -else if ( ret == 0)// timeout; no event detected -else -{ - for (int i = 0; i < ready; i+ ) - { - if ( pevents[i].events & EPOLLIN ) - { - // 获取连接指针 - Connection * c = (Connection*) pevents[i].data.ptr; - c->handleReadEvent(); - } - } -} -``` - - - -流程图: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 - - - -*** - - - -###### 特点 - -epoll 的特点: - -* epoll 仅适用于 Linux 系统 -* 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() 文件映射内存加速与内核空间的消息传递,减少复制开销 -* 前面两者要把 current 往设备等待队列中挂一次,epoll 也只把 current 往等待队列上挂一次,但是这里的等待队列并不是设备等待队列,只是一个 epoll 内部定义的等待队列,这样节省不少的开销 -* epoll 对多线程编程更有友好,一个线程调用了 epoll_wait() 另一个线程关闭了同一个描述符,也不会产生像 select 和 poll 的不确定情况 - - - -参考文章:https://www.jianshu.com/p/dfd940e7fca2 - -参考文章:https://www.cnblogs.com/anker/p/3265058.html - - - -*** - - - -##### 应用 - -应用场景: - -* select 应用场景: - * select 的 timeout 参数精度为微秒,而 poll 和 epoll 为毫秒,因此 select 更加适用于实时性要求比较高的场景,比如核反应堆的控制 - * select 可移植性更好,几乎被所有主流平台所支持 - -* poll 应用场景:poll 没有最大描述符数量的限制,适用于平台支持并且对实时性要求不高的情况 - -* epoll 应用场景: - * 运行在 Linux 平台上,有大量的描述符需要同时轮询,并且这些连接最好是长连接 - * 需要同时监控小于 1000 个描述符,没必要使用 epoll,因为这个应用场景下并不能体现 epoll 的优势 - * 需要监控的描述符状态变化多,而且都是非常短暂的,也没有必要使用 epoll。因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用,频繁系统调用降低效率,并且 epoll 的描述符存储在内核,不容易调试 - - - -参考文章:https://github.com/CyC2018/CS-Notes/blob/master/notes/Socket.md - - - -**** - - - -#### 系统调用 - -##### 内核态 - -用户空间:用户代码、用户堆栈 - -内核空间:内核代码、内核调度程序、进程描述符(内核堆栈、thread_info进程描述符) - -* 进程描述符和用户的进程是一一对应的 -* SYS_API 系统调用:如 read、write,系统调用就是 0X80 中断 -* 进程描述符pd:进程从用户态切换到内核态时,需要保存用户态时的上下文信息, -* 线程上下文:用户程序基地址,程序计数器、cpu cache、寄存器等,方便程序切回用户态时恢复现场 -* 内核堆栈:系统调用函数也是要创建变量的,这些变量在内核堆栈上分配 - -![](https://gitee.com/seazean/images/raw/master/Java/IO-用户态和内核态.png) - - - -*** - - - -##### 80中断 - -在用户程序中调用操作系统提供的核心态级别的子功能,为了系统安全需要进行用户态和内核态转换,状态的转换需要进行 CPU 中断,中断分为硬中断和软中断: - -* 硬中断:如网络传输中,数据到达网卡后,网卡经过一系列操作后发起硬件中断 -* 软中断:如程序运行过程中本身产生的一些中断 - - 发起 `0X80` 中断 - - 程序执行碰到除 0 异常 - -系统调用 system_call 函数所对应的中断指令编号是 0X80(十进制是8×16=128),而该指令编号对应的就是系统调用程序的入口,所以称系统调用为 80 中断 - -系统调用的流程: - -* 在 CPU 寄存器里存一个系统调用号,表示哪个系统函数,比如 read -* 将 CPU 的临时数据都保存到 thread_info 中 -* 执行 80 中断处理程序,找到刚刚存的系统调用号(read),先检查缓存中有没有对应的数据,没有就去磁盘中加载到内核缓冲区,然后从内核缓冲区拷贝到用户空间 -* 最后恢复到用户态,通过 thread_info 恢复现场,用户态继续执行 - -![](https://gitee.com/seazean/images/raw/master/Java/IO-系统调用的过程.jpg) - - - -参考文章:https://blog.csdn.net/hancoder/article/details/112149121 - - - -**** - - - -#### 零拷贝 - -##### DMA - -DMA (Direct Memory Access) :直接存储器访问,让外部设备不通过 CPU 直接与系统内存交换数据的接口技术 - -作用:可以解决批量数据的输入/输出问题,使数据的传送速度取决于存储器和外设的工作速度 - -把内存数据传输到网卡然后发送: - -* 没有 DMA:CPU 读内存数据到 CPU 高速缓存,再写到网卡,这样就把 CPU 的速度拉低到和网卡一个速度 -* 使用 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 发出总线接管请求 -* 当 CPU 发出允许接管信号后,进入 DMA 操作周期 -* 确定传送数据的主存单元地址及长度,并自动修改主存地址计数和传送长度计数 -* 规定数据在主存和外设间的传送方向,发出读写等控制信号,执行数据传送操作 -* 判断 DMA 传送是否结束,发出 DMA 结束信号,使 CPU 恢复正常工作状态(中断) - - - -*** - - - -##### BIO - -传统的 I/O 操作进行了 4 次用户空间与内核空间的上下文切换,以及 4 次数据拷贝: - -* JVM 发出 read() 系统调用,OS 上下文切换到内核模式(切换1)并将数据从网卡或硬盘等通过 DMA 读取到内核空间缓冲区(拷贝1) -* OS 内核将数据复制到用户空间缓冲区(拷贝2),然后 read 系统调用返回,又会导致一次内核空间到用户空间的上下文切换(切换2) -* JVM 处理代码逻辑并发送 write() 系统调用,OS 上下文切换到内核模式(切换3)并从用户空间缓冲区复制数据到内核空间缓冲区(拷贝3) -* write 系统调用返回,导致内核空间到用户空间的再次上下文切换(切换4),将内核空间缓冲区中的数据写到 hardware(拷贝4) - -流程图中的箭头反过来也成立,可以从网卡获取数据 - -![](https://gitee.com/seazean/images/raw/master/Java/IO-BIO工作流程.png) - -read 调用图示:read、write 都是系统调用指令 - - - - - -*** - - - -##### mmap - -mmap(Memory Mapped Files)加 write 实现零拷贝,零拷贝就是没有数据从内核空间复制到用户空间 - -用户空间和内核空间都使用内存,所以可以共享同一块物理内存地址,省去用户态和内核态之间的拷贝。写网卡时,共享空间的内容拷贝到 socket 缓冲区,然后交给 DMA 发送到网卡,只需要 3 次复制 - -进行了 4 次用户空间与内核空间的上下文切换,以及 3 次数据拷贝(2 次 DMA,一次 CPU 复制): - -* 发出 mmap 系统调用,DMA 拷贝到内核缓冲区;mmap系统调用返回,无需拷贝 -* 发出 write 系统调用,将数据从内核缓冲区拷贝到内核 socket 缓冲区;write系统调用返回,DMA 将内核空间 socket 缓冲区中的数据传递到协议引擎 - -![](https://gitee.com/seazean/images/raw/master/Java/IO-mmap工作流程.png) - -原理:利用操作系统的 Page 来实现文件到物理内存的直接映射,完成映射后对物理内存的操作会**被同步**到硬盘上 - -缺点:不可靠,写到 mmap 中的数据并没有被真正的写到硬盘,操作系统会在程序主动调用 flush 的时候才把数据真正的写到硬盘 - -Java NIO 提供了 **MappedByteBuffer** 类可以用来实现 mmap 内存映射,MappedByteBuffer 类对象**只能**通过调用 `FileChannel.map()` 获取 - - - -**** - - - -##### sendfile - -sendfile 实现零拷贝,打开文件的文件描述符 fd 和 socket 的 fd 传递给 sendfile,然后经过 3 次复制和 2 次用户态和内核态的切换 - -原理:数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer,由于和用户态完全无关,就减少了两次上下文切换 - -![](https://gitee.com/seazean/images/raw/master/Java/IO-sendfile工作流程.png) - -sendfile2.4 之后,sendfile 实现了更简单的方式,文件到达内核缓冲区后,不必再将数据全部复制到 socket buffer 缓冲区,而是只**将记录数据位置和长度相关等描述符信息**保存到 socket buffer,DMA 根据 socket 缓冲区中描述符提供的位置和偏移量信息直接将内核空间缓冲区中的数据拷贝到协议引擎上(2 次复制 2 次切换) - -Java NIO 对 sendfile 的支持是 `FileChannel.transferTo()/transferFrom()`,把磁盘文件读取 OS 内核缓冲区后的 fileChannel,直接转给 socketChannel 发送,底层就是sendfile - - - -参考文章:https://blog.csdn.net/hancoder/article/details/112149121 - - - -*** - - - -### Inet - -一个该 InetAddress 类的对象就代表一个IP地址对象 - -成员方法: -`static InetAddress getLocalHost()` : 获得本地主机IP地址对象 -`static InetAddress getByName(String host)` : 根据IP地址字符串或主机名获得对应的IP地址对象 -`String getHostName()` : 获取主机名 -`String getHostAddress()` : 获得IP地址字符串 - -```java -public class InetAddressDemo { - public static void main(String[] args) throws Exception { - // 1.获取本机地址对象 - InetAddress ip = InetAddress.getLocalHost(); - System.out.println(ip.getHostName());//DESKTOP-NNMBHQR - System.out.println(ip.getHostAddress());//192.168.11.1 - // 2.获取域名ip对象 - InetAddress ip2 = InetAddress.getByName("www.baidu.com"); - System.out.println(ip2.getHostName());//www.baidu.com - System.out.println(ip2.getHostAddress());//14.215.177.38 - // 3.获取公网IP对象。 - InetAddress ip3 = InetAddress.getByName("182.61.200.6"); - System.out.println(ip3.getHostName());//182.61.200.6 - System.out.println(ip3.getHostAddress());//182.61.200.6 - - // 4.判断是否能通: ping 5s之前测试是否可通 - System.out.println(ip2.isReachable(5000)); // ping百度 - } -} -``` - - - -*** - - - -### UDP - -#### 基本介绍 - -UDP(User Datagram Protocol)协议的特点: - -* 面向无连接的协议 -* 发送端只管发送,不确认对方是否能收到 -* 基于数据包进行数据传输 -* 发送数据的包的大小限制**64KB**以内 -* 因为面向无连接,速度快,但是不可靠,会丢失数据 - -UDP协议的使用场景:在线视频、网络语音、电话 - - - -#### 实现UDP - -UDP协议相关的两个类 - -* DatagramPacket(数据包对象):用来封装要发送或要接收的数据,比如:集装箱 -* DatagramSocket(发送对象):用来发送或接收数据包,比如:码头 - -**DatagramPacket**: - -* DatagramPacket类 - - `public new DatagramPacket(byte[] buf, int length, InetAddress address, int port)` : 创建发送端数据包对象,参数: - - * buf:要发送的内容,字节数组 - * length:要发送内容的长度,单位是字节 - * address:接收端的IP地址对象 - * port:接收端的端口号 - - `public new DatagramPacket(byte[] buf, int length)` : 创建接收端的数据包对象,参数: - - * buf:用来存储接收到内容 - * length:能够接收内容的长度 -* DatagramPacket 类常用方法 - `public int getLength()` : 获得实际接收到的字节个数 - `public byte[] getData()` : 返回数据缓冲区 - -**DatagramSocket**: - -* DatagramSocket类构造方法 - `protected DatagramSocket()` : 创建发送端的Socket对象,系统会随机分配一个端口号 - `protected DatagramSocket(int port)` : 创建接收端的Socket对象并指定端口号 -* DatagramSocket类成员方法 - `public void send(DatagramPacket dp)` : 发送数据包 - `public void receive(DatagramPacket p)` : 接收数据包 - `public void close()` : 关闭数据报套接字 - -```java -public class UDPClientDemo { - public static void main(String[] args) throws Exception { - System.out.println("===启动客户端==="); - // 1.创建一个集装箱对象,用于封装需要发送的数据包! - byte[] buffer = "我学Java".getBytes(); - DatagramPacket packet = new DatagramPacket(buffer,bubffer.length - ,InetAddress.getLoclHost,8000); - // 2.创建一个码头对象 - DatagramSocket socket = new DatagramSocket(); - // 3.开始发送数据包对象 - socket.send(packet); - socket.close(); - } -} -public class UDPServerDemo{ - public static void main(String[] args) throws Exception { - System.out.println("==启动服务端程序=="); - // 1.创建一个接收客户都端的数据包对象(集装箱) - byte[] buffer = new byte[1024*64]; - DatagramPacket packet = new DatagramPacket(buffer,bubffer.length); - // 2.创建一个接收端的码头对象 - DatagramSocket socket = new DatagramSocket(8000); - // 3.开始接收 - socket.receive(packet); - // 4.从集装箱中获取本次读取的数据量 - int len = packet.getLength(); - // 5.输出数据 - //String rs = new String(socket.getData(), 0, len) - String rs = new String(buffer , 0 , len); - System.out.println(rs); - // 6.服务端还可以获取发来信息的客户端的IP和端口。 - String ip = packet.getAddress().getHostAdress(); - int port = packet.getPort(); - socket.close(); - } -} -``` - - - -*** - - - -#### 通讯方式 - -UDP通信方式: - -+ 单播:用于两个主机之间的端对端通信 - -+ 组播:用于对一组特定的主机进行通信 - IP : 224.0.1.0 - Socket对象 : MulticastSocket - -+ 广播:用于一个主机对整个局域网上所有主机上的数据通信 - IP : 255.255.255.255 - Socket对象 : DatagramSocket - - - -*** - - - -### TCP - -#### 基本介绍 - -TCP/IP协议 ==> Transfer Control Protocol ==> 传输控制协议 - -TCP/IP协议的特点: - -* 面向连接的协议 -* 只能由客户端主动发送数据给服务器端,服务器端接收到数据之后,可以给客户端响应数据 -* 通过**三次握手**建立连接,连接成功形成数据传输通道;通过**四次挥手**断开连接 -* 基于IO流进行数据传输 -* 传输数据大小没有限制 -* 因为面向连接的协议,速度慢,但是是可靠的协议。 - -TCP协议的使用场景:文件上传和下载、邮件发送和接收、远程登录 - -注意:**TCP不会为没有数据的ACK超时重传** - -三次握手 - -四次挥手 - - - -*** - - - -#### Socket - -TCP通信也叫**Socket网络编程**,只要代码基于Socket开发,底层就是基于了可靠传输的TCP通信 - -TCP协议相关的类: - -* Socket:一个该类的对象就代表一个客户端程序。 -* ServerSocket:一个该类的对象就代表一个服务器端程序。 - -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 - -ServerSocket类: - -* 构造方法:`public ServerSocket(int port)` -* 常用API:`public Socket accept()`,**阻塞等待**接收一个客户端的Socket管道连接请求,连接成功返回一个Socket对象 - -相当于客户端和服务器建立一个数据管道,管道一般不用close - - - -*** - - - -#### 实现TCP - -##### 开发流程 - -客户端的开发流程: - -1. 客户端要请求于服务端的socket管道连接 -2. 从socket通信管道中得到一个字节输出流 -3. 通过字节输出流给服务端写出数据 - -服务端的开发流程: - -1. 用ServerSocket注册端口 -2. 接收客户端的Socket管道连接 -3. 从socket通信管道中得到一个字节输入流 -4. 从字节输入流中读取客户端发来的数据 - -![](https://gitee.com/seazean/images/raw/master/Java/BIO工作机制.png) - -![](https://gitee.com/seazean/images/raw/master/Java/TCP-工作模型.png) - -* 如果输出缓冲区空间不够存放主机发送的数据,则会被阻塞,输入缓冲区同理 -* 缓冲区不属于应用程序,属于内核 -* TCP从输出缓冲区读取数据会加锁阻塞线程 - - - -*** - - - -##### BIO通信 - -需求一:客户端发送一行数据,服务端接收一行数据 - -````java -public class ClientDemo { - public static void main(String[] args) throws Exception { - // 1.客户端要请求于服务端的socket管道连接。 - Socket socket = new Socket("127.0.0.1",8080); - // 2.从socket通信管道中得到一个字节输出流 - OutputStream os = new socket.getOutputStream(); - // 3.把低级的字节输出流包装成高级的打印流。 - PrintStream ps = new PrintStream(os); - // 4.开始发消息出去 - ps.println("我是客户端"); - ps.flush();//一般不关闭IO流 - System.out.println("客户端发送完毕~~~~"); - } -} -public class ServerDemo{ - public static void main(String[] args) throws Exception { - System.out.println("----服务端启动----"); - // 1.注册端口: public ServerSocket(int port) - ServerSocket serverSocket = new ServerSocket(8080); - // 2.开始等待接收客户端的Socket管道连接。 - Socket socket = serverSocket.accept(); - // 3.从socket通信管道中得到一个字节输入流。 - InputStream is = socket.getInputStream(); - // 4.把字节输入流转换成字符输入流 - BufferedReader br = new BufferedReader(new InputStreamReader(is)); - // 6.按照行读取消息 。 - String line; - if((line = br.readLine()) != null){ - System.out.println(line); - } - } -} -```` - - - -需求二:客户2端可以反复发送数据,服务端可以反复数据2 - -```java -public class ClientDemo { - public static void main(String[] args) throws Exception { - // 1.客户端要请求于服务端的socket管道连接。 - Socket socket = new Socket("127.0.0.1",8080); - // 2.从socket通信管道中得到一个字节输出流 - OutputStream os = new socket.getOutputStream(); - // 3.把低级的字节输出流包装成高级的打印流。 - PrintStream ps = new PrintStream(os); - // 4.开始发消息出去 - while(true){ - Scanner sc = new Scanner(System.in); - System.out.print("请说:"); - ps.println(sc.nextLine()); - ps.flush(); - } - } -} -public class ServerDemo{ - public static void main(String[] args) throws Exception { - System.out.println("----服务端启动----"); - // 1.注册端口: public ServerSocket(int port) - ServerSocket serverSocket = new ServerSocket(8080); - // 2.开始等待接收客户端的Socket管道连接。 - Socket socket = serverSocket.accept(); - // 3.从socket通信管道中得到一个字节输入流。 - InputStream is = socket.getInputStream(); - // 4.把字节输入流转换成字符输入流 - BufferedReader br = new BufferedReader(new InputStreamReader(is)); - // 6.按照行读取消息 。 - String line; - while((line = br.readLine()) != null){ - System.out.println(line); - } - } -} -``` - - - -需求三:实现一个服务端可以同时接收多个客户端的消息。 - -```java -public class ClientDemo { - public static void main(String[] args) throws Exception { - Socket socket = new Socket("127.0.0.1",8080); - OutputStream os = new socket.getOutputStream(); - PrintStream ps = new PrintStream(os); - while(true){ - Scanner sc = new Scanner(System.in); - System.out.print("请说:"); - ps.println(sc.nextLine()); - ps.flush(); - } - } -} -public class ServerDemo{ - public static void main(String[] args) throws Exception { - System.out.println("----服务端启动----"); - ServerSocket serverSocket = new ServerSocket(8080); - while(true){ - // 开始等待接收客户端的Socket管道连接。 - Socket socket = serverSocket.accept(); - // 每接收到一个客户端必须为这个客户端管道分配一个独立的线程来处理与之通信。 - new ServerReaderThread(socket).start(); - } - } -} -class ServerReaderThread extends Thread{ - privat Socket socket; - public ServerReaderThread(Socket socket){this.socket = socket;} - @Override - public void run() { - try(InputStream is = socket.getInputStream(); - BufferedReader br = new BufferedReader(new InputStreamReader(is)) - ){ - String line; - while((line = br.readLine()) != null){ - sout(socket.getRemoteSocketAddress() + ":" + line); - } - }catch(Exception e){ - sout(socket.getRemoteSocketAddress() + "下线了~~~~~~"); - } - } -} -``` - - - -*** - - - -##### 伪异步 - -一个客户端要一个线程,这种模型是不行的,并发越高系统瘫痪的越快,可以在服务端引入线程池,使用线程池来处理与客户端的消息通信 - -优势:不会引起系统的死机,可以控制并发线程的数量 -劣势:同时可以并发的线程将受到限制 - -```java -public class BIOServer { - public static void main(String[] args) throws Exception { - //线程池机制 - //创建一个线程池,如果有客户端连接,就创建一个线程,与之通讯(单独写一个方法) - ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(); - //创建ServerSocket - ServerSocket serverSocket = new ServerSocket(6666); - System.out.println("服务器启动了"); - while (true) { - System.out.println("线程名字 = " + Thread.currentThread().getName()); - //监听,等待客户端连接 - System.out.println("等待连接...."); - final Socket socket = serverSocket.accept(); - System.out.println("连接到一个客户端"); - //创建一个线程,与之通讯 - newCachedThreadPool.execute(new Runnable() { - public void run() { - //可以和客户端通讯 - handler(socket); - } - }); - } - } - - //编写一个handler方法,和客户端通讯 - public static void handler(Socket socket) { - try { - System.out.println("线程名字 = " + Thread.currentThread().getName()); - byte[] bytes = new byte[1024]; - //通过socket获取输入流 - InputStream inputStream = socket.getInputStream(); - int len; - //循环的读取客户端发送的数据 - while ((len = inputStream.read(bytes)) != -1) { - System.out.println("线程名字 = " + Thread.currentThread().getName()); - //输出客户端发送的数据 - System.out.println(new String(bytes, 0, read)); - } - } catch (Exception e) { - e.printStackTrace(); - } finally { - System.out.println("关闭和client的连接"); - try { - socket.close(); - } catch (Exception e) { - e.printStackTrace(); - } - } - } -} -``` - - - -**** - - - -#### 文件传输 - -##### 字节流 - -客户端:本地图片: ‪E:\seazean\图片资源\beautiful.jpg -服务端:服务器路径:E:\seazean\图片服务器 - -UUID. randomUUID() : 方法生成随机的文件名 - -**socket.shutdownOutput()**:这个必须执行,不然服务器会一直循环等待数据,最后文件损坏,程序报错 - -```java -//常量包 -public class Constants { - public static final String SRC_IMAGE = "D:\\seazean\\图片资源\\beautiful.jpg"; - public static final String SERVER_DIR = "D:\\seazean\\图片服务器\\"; - public static final String SERVER_IP = "127.0.0.1"; - public static final int SERVER_PORT = 8888; - -} -public class ClientDemo { - public static void main(String[] args) throws Exception { - Socket socket = new Socket(Constants.ERVER_IP,Constants.SERVER_PORT); - BufferedOutputStream bos=new BufferedOutputStream(socket.getOutputStream()); - //提取本机的图片上传给服务端。Constants.SRC_IMAGE - BufferedInputStream bis = new BufferedInputStream(new FileInputStream()); - byte[] buffer = new byte[1024]; - int len ; - while((len = bis.read(buffer)) != -1) { - bos.write(buffer, 0 ,len); - } - bos.flush();// 刷新图片数据到服务端!! - socket.shutdownOutput();// 告诉服务端我的数据已经发送完毕,不要在等我了! - bis.close(); - - //等待着服务端的响应数据!! - BufferedReader br = new BufferedReader( - new InputStreamReader(socket.getInputStream())); - System.out.println("收到服务端响应:"+br.readLine()); - } -} -``` - -```java -public class ServerDemo { - public static void main(String[] args) throws Exception { - System.out.println("----服务端启动----"); - // 1.注册端口: - ServerSocket serverSocket = new ServerSocket(Constants.SERVER_PORT); - // 2.定义一个循环不断的接收客户端的连接请求 - while(true){ - // 3.开始等待接收客户端的Socket管道连接。 - Socket socket = serverSocket.accept(); - // 4.每接收到一个客户端必须为这个客户端管道分配一个独立的线程来处理与之通信。 - new ServerReaderThread(socket).start(); - } - } -} -class ServerReaderThread extends Thread{ - private Socket socket ; - public ServerReaderThread(Socket socket){this.socket = socket;} - @Override - public void run() { - try{ - InputStream is = socket.getInputStream(); - BufferedInputStream bis = new BufferedInputStream(is); - BufferedOutputStream bos = new BufferedOutputStream( - new FileOutputStream - (Constants.SERVER_DIR+UUID.randomUUID().toString()+".jpg")); - byte[] buffer = new byte[1024]; - int len; - while((len = bis.read(buffer)) != -1){ - bos.write(buffer,0,len); - } - bos.close(); - System.out.println("服务端接收完毕了!"); - - // 4.响应数据给客户端 - PrintStream ps = new PrintStream(socket.getOutputStream()); - ps.println("您好,已成功接收您上传的图片!"); - ps.flush(); - Thread.sleep(10000); - }catch (Exception e){ - sout(socket.getRemoteSocketAddress() + "下线了"); - } - } -} -``` - - - -**** - - - -##### 数据流 - -构造方法: -`DataOutputStream(OutputStream out)` : 创建一个新的数据输出流,以将数据写入指定的底层输出流 -`DataInputStream(InputStream in) ` : 创建使用指定的底层 InputStream 的 DataInputStream - -常用API: -`final void writeUTF(String str)` : 使用机器无关的方式使用 UTF-8 编码将字符串写入底层输出流 -`final String readUTF()` : 读取以modified UTF-8格式编码的 Unicode 字符串,返回 String 类型 - -```java -public class Client { - public static void main(String[] args) { - InputStream is = new FileInputStream("path"); - // 1、请求与服务端的Socket链接 - Socket socket = new Socket("127.0.0.1" , 8888); - // 2、把字节输出流包装成一个数据输出流 - DataOutputStream dos = new DataOutputStream(socket.getOutputStream()); - // 3、先发送上传文件的后缀给服务端 - dos.writeUTF(".png"); - // 4、把文件数据发送给服务端进行接收 - byte[] buffer = new byte[1024]; - int len; - while((len = is.read(buffer)) > 0 ){ - dos.write(buffer , 0 , len); - } - dos.flush(); - Thread.sleep(10000); - } -} - -public class Server { - public static void main(String[] args) { - ServerSocket ss = new ServerSocket(8888); - Socket socket = ss.accept(); - // 1、得到一个数据输入流读取客户端发送过来的数据 - DataInputStream dis = new DataInputStream(socket.getInputStream()); - // 2、读取客户端发送过来的文件类型 - String suffix = dis.readUTF(); - // 3、定义一个字节输出管道负责把客户端发来的文件数据写出去 - OutputStream os = new FileOutputStream("path"+ - UUID.randomUUID().toString()+suffix); - // 4、从数据输入流中读取文件数据,写出到字节输出流中去 - byte[] buffer = new byte[1024]; - int len; - while((len = dis.read(buffer)) > 0){ - os.write(buffer,0, len); - } - os.close(); - System.out.println("服务端接收文件保存成功!"); - } -} -``` - - - -*** - - - -### NIO - -#### 基本介绍 - -**NIO的介绍**: - -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 可以做到用一个线程来处理多个操作的。假设有 1000 个请求过来,根据实际情况可以分配20 或者 80个线程来处理,不像之前的阻塞 IO 那样分配 1000 个 - -NIO 和 BIO 的比较: - -* BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多 - -* BIO 是阻塞的,NIO 则是非阻塞的 - -* BIO 基于字节流和字符流进行操作,而 NIO 基于 Channel 和 Buffer 进行操作,数据从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector 用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道 - - | NIO | BIO | - | ------------------------- | ------------------- | - | 面向缓冲区(Buffer) | 面向流(Stream) | - | 非阻塞(Non Blocking IO) | 阻塞IO(Blocking IO) | - | 选择器(Selectors) | | - - - -*** - - - -#### NIO原理 - -NIO 三大核心部分:**Channel( 通道) ,Buffer( 缓冲区), Selector( 选择器)** - -* Buffer 缓冲区 - - 缓冲区本质是一块可以写入数据、读取数据的内存,**底层是一个数组**,这块内存被包装成NIO Buffer对象,并且提供了方法用来操作这块内存,相比较直接对数组的操作,Buffer 的 API 更加容易操作和管理 - -* Channel 通道 - - Java NIO 的通道类似流,不同的是既可以从通道中读取数据,又可以写数据到通道,流的读写通常是单向的,通道可以非阻塞读取和写入通道,支持读取或写入缓冲区,也支持异步地读写。 - -* Selector 选择器 - - Selector 是一个 Java NIO 组件,能够检查一个或多个 NIO 通道,并确定哪些通道已经准备好进行读取或写入,这样一个单独的线程可以管理多个channel,从而管理多个网络连接,提高效率 - -NIO的实现框架: - -![](https://gitee.com/seazean/images/raw/master/Java/NIO框架.png) - -* 每个 Channel 对应一个 Buffer -* 一个线程对应 Selector , 一个 Selector 对应多个 Channel(连接) -* 程序切换到哪个Channel 是由事件决定的,Event 是一个重要的概念 -* Selector 会根据不同的事件,在各个通道上切换 -* Buffer 是一个内存块 , 底层是一个数组 -* 数据的读取写入是通过 Buffer 完成的 , BIO 中要么是输入流,或者是输出流,不能双向,NIO 的 Buffer 是可以读也可以写, flip() 切换 Buffer 的工作模式 - -Java NIO 系统的核心在于:通道和缓冲区,通道表示打开到 IO 设备(例如:文件、 套接字)的连接。若需要使用 NIO 系统,获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区,然后操作缓冲区,对数据进行处理。简而言之,Channel 负责传输, Buffer 负责存取数据 - - - -*** - - - -#### 缓冲区 - -##### 基本介绍 - -缓冲区(Buffer):缓冲区本质上是一个**可以读写数据的内存块**,用于特定基本数据类型的容器,用于与 NIO 通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的 - -![](https://gitee.com/seazean/images/raw/master/Java/NIO-Buffer.png) - -Buffer 底层是一个数组,可以保存多个相同类型的数据,根据数据类型不同 ,有以下 Buffer 常用子类:ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer - - - -*** - - - -##### 基本属性 - -* 容量 (capacity):作为一个内存块,Buffer 具有固定大小,缓冲区容量不能为负,并且创建后不能更改 - -* 限制 (limit):表示缓冲区中可以操作数据的大小(limit 后数据不能进行读写),缓冲区的限制不能为负,并且不能大于其容量。 **写入模式,限制等于 buffer 的容量;读取模式下,limit 等于写入的数据量** - -* 位置 (position):下一个要读取或写入的数据的索引,缓冲区的位置不能为负,并且不能大于其限制 - -* 标记 (mark)与重置 (reset):标记是一个索引,通过Buffer中的 mark() 方法指定 Buffer 中一个特定的位置,可以通过调用 reset() 方法恢复到这个 position - -* 位置、限制、容量遵守以下不变式: **0 <= position <= limit <= capacity** - - - - - -*** - - - -##### 常用API - -`static XxxBuffer allocate(int capacity)` : 创建一个容量为capacity 的 XxxBuffer 对象 - -Buffer 基本操作: - -| 方法 | 说明 | -| ------------------------------------------- | ------------------------------------------------------- | -| public Buffer clear() | 清空缓冲区,不清空内容,将位置设置为零,限制设置为容量 | -| public Buffer flip() | 翻转缓冲区,将缓冲区的界限设置为当前位置,position 置 0 | -| public int capacity() | 返回 Buffer的 capacity 大小 | -| public final int limit() | 返回 Buffer 的界限 limit 的位置 | -| public Buffer limit(int n) | 设置缓冲区界限为 n | -| public Buffer mark() | 在此位置对缓冲区设置标记 | -| public final int position() | 返回缓冲区的当前位置 position | -| public Buffer position(int n) | 设置缓冲区的当前位置为n | -| public Buffer reset() | 将位置 position 重置为先前 mark 标记的位置 | -| public Buffer rewind() | 将位置设为为0,取消设置的 mark | -| public final int remaining() | 返回当前位置 position 和 limit 之间的元素个数 | -| public final boolean hasRemaining() | 判断缓冲区中是否还有元素 | -| public static ByteBuffer wrap(byte[] array) | 将一个字节数组包装到缓冲区中 | -| abstract ByteBuffer asReadOnlyBuffer() | 创建一个新的只读字节缓冲区 | - -Buffer 数据操作: - -| 方法 | 说明 | -| ------------------------------------------------- | ----------------------------------------------- | -| public abstract byte get() | 读取该缓冲区当前位置的单个字节,然后增加位置 | -| public ByteBuffer get(byte[] dst) | 读取多个字节到字节数组dst中 | -| public abstract byte get(int index) | 读取指定索引位置的字节,不移动 position | -| public abstract ByteBuffer put(byte b) | 将给定单个字节写入缓冲区的当前位置,position+1 | -| public final ByteBuffer put(byte[] src) | 将 src 字节数组写入缓冲区的当前位置 | -| public abstract ByteBuffer put(int index, byte b) | 将指定字节写入缓冲区的索引位置,不移动 position | - -提示:"\n",占用两个字节 - - - -**** - - - -##### 读写数据 - -使用Buffer读写数据一般遵循以下四个步骤: - -* 写入数据到 Buffer -* 调用 flip()方法,转换为读取模式 -* 从 Buffer 中读取数据 -* 调用 buffer.clear() 方法清除缓冲区 - -```java -public class TestBuffer { - @Test - public void test(){ - String str = "seazean"; - //1. 分配一个指定大小的缓冲区 - ByteBuffer buffer = ByteBuffer.allocate(1024); - System.out.println("-----------------allocate()----------------"); - System.out.println(bufferf.position());//0 - System.out.println(buffer.limit());//1024 - System.out.println(buffer.capacity());//1024 - - //2. 利用 put() 存入数据到缓冲区中 - buffer.put(str.getBytes()); - System.out.println("-----------------put()----------------"); - System.out.println(bufferf.position());//7 - System.out.println(buffer.limit());//1024 - System.out.println(buffer.capacity());//1024 - - //3. 切换读取数据模式 - buffer.flip(); - System.out.println("-----------------flip()----------------"); - System.out.println(buffer.position());//0 - System.out.println(buffer.limit());//7 - System.out.println(buffer.capacity());//1024 - - //4. 利用 get() 读取缓冲区中的数据 - byte[] dst = new byte[buffer.limit()]; - buffer.get(dst); - System.out.println(dst.length); - System.out.println(new String(dst, 0, dst.length)); - System.out.println(buffer.position());//7 - System.out.println(buffer.limit());//7 - - //5. clear() : 清空缓冲区. 但是缓冲区中的数据依然存在,但是处于“被遗忘”状态 - System.out.println(buffer.hasRemaining());//true - buffer.clear(); - System.out.println(buffer.hasRemaining());//true - System.out.println("-----------------clear()----------------"); - System.out.println(buffer.position());//0 - System.out.println(buffer.limit());//1024 - System.out.println(buffer.capacity());//1024 - } -} -``` - - - -**** - - - -##### 直接内存 - -Byte Buffer 可以是两种类型,一种是基于直接内存(也就是非堆内存),另一种是非直接内存(也就是堆内存)。对于直接内存来说,JVM将会在IO操作上具有更高的性能,因为直接作用于本地系统的IO操作,而非直接内存,也就是堆内存中的数据,如果要作IO操作,会先从本进程内存复制到直接内存,再利用本地IO处理 - -直接内存创建 Buffer 对象:`static XxxBuffer allocateDirect(int capacity)` - -直接内存的分配与回收机制参考:JVM → 内存结构 → 本地内存 → 直接内存 - -堆外内存不受 JVM GC 控制,可以使用堆外内存进行通信,防止 GC 后缓冲区位置发生变化的情况,源码: - -* SocketChannel#write(java.nio.ByteBuffer) → SocketChannelImpl#write(java.nio.ByteBuffer) - - ```java - public int write(ByteBuffer var1) throws IOException { - do { - var3 = IOUtil.write(this.fd, var1, -1L, nd); - } while(var3 == -3 && this.isOpen()); - } - ``` - -* IOUtil#write(java.io.FileDescriptor, java.nio.ByteBuffer, long, sun.nio.ch.NativeDispatcher) - - ```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 - ByteBuffer var8 = Util.getTemporaryDirectBuffer(var7); - var8.put(var1); - //... - //从堆外写到内核缓冲区 - int var9 = writeFromNativeBuffer(var0, var8, var2, var4); - } - } - ``` - -数据流的角度: - -* 非直接内存的作用链:本地IO → 直接内存 → 非直接内存 → 直接内存 → 本地IO -* 直接内存是:本地IO → 直接内存 → 本地IO - -JVM 直接内存详解: - - - - - - - -**** - - - -##### 共享内存 - -FileChannel 提供 map 方法把文件映射到虚拟内存,通常情况可以映射整个文件,如果文件比较大,可以进行分段映射,完成映射后对物理内存的操作会被同步到硬盘上 - -FileChannel 中的成员属性: - -* MapMode.mode:内存映像文件访问的方式,共三种: - * `MapMode.READ_ONLY`:只读,试图修改得到的缓冲区将导致抛出异常。 - * `MapMode.READ_WRITE`:读/写,对得到的缓冲区的更改最终将写入文件;但该更改对映射到同一文件的其他程序不一定是可见的 - * `MapMode.PRIVATE`:私用,可读可写,但是修改的内容不会写入文件,只是 buffer 自身的改变,称之为 copy on write 写时复制 - -* `public final FileLock lock()`:获取此文件通道的排他锁 - -MappedByteBuffer,可以让文件直接在内存(堆外内存)中进行修改,这种方式叫做内存映射,可以直接调用系统底层的缓存,没有 JVM 和系统之间的复制操作,提高了传输效率,作用: - -* 用在进程间的通信,能达到**共享内存页**的作用,但在高并发下要对文件内存进行加锁,防止出现读写内容混乱和不一致性,Java 提供了文件锁 FileLock,但在父/子进程中锁定后另一进程会一直等待,效率不高 -* 读写那些太大而不能放进内存中的文件 - -MappedByteBuffer 较之 ByteBuffer新增的三个方法 - -- `final MappedByteBuffer force()`:缓冲区是 READ_WRITE 模式下,对缓冲区内容的修改强行写入文件 -- `final MappedByteBuffer load()`:将缓冲区的内容载入物理内存,并返回该缓冲区的引用 -- `final boolean isLoaded()`:如果缓冲区的内容在物理内存中,则返回真,否则返回假 - -```java -public class MappedByteBufferTest { - public static void main(String[] args) throws Exception { - RandomAccessFile ra = new RandomAccessFile("1.txt", "rw"); - //获取对应的通道 - FileChannel channel = ra.getChannel(); - - /** - * 参数1 FileChannel.MapMode.READ_WRITE 使用的读写模式 - * 参数2 0: 文件映射时的起始位置 - * 参数3 5: 是映射到内存的大小(不是索引位置),即将 1.txt 的多少个字节映射到内存 - * 可以直接修改的范围就是 0-5 - * 实际类型 DirectByteBuffer - */ - MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5); - - buffer.put(0, (byte) 'H'); - buffer.put(3, (byte) '9'); - buffer.put(5, (byte) 'Y'); //IndexOutOfBoundsException - - ra.close(); - System.out.println("修改成功~~"); - } -} -``` - -从硬盘上将文件读入内存,要经过文件系统进行数据拷贝,拷贝操作是由文件系统和硬件驱动实现。通过内存映射的方法访问硬盘上的文件,拷贝数据的效率要比 read 和 write 系统调用高: - -- read() 是系统调用,首先将文件从硬盘拷贝到内核空间的一个缓冲区,再将这些数据拷贝到用户空间,实际上进行了两次数据拷贝 -- mmap() 也是系统调用,但没有进行数据拷贝,当缺页中断发生时,直接将文件从硬盘拷贝到共享内存,只进行了一次数据拷贝 - -注意:mmap 的文件映射,在 Full GC 时才会进行释放,如果需要手动清除内存映射文件,可以反射调用sun.misc.Cleaner 方法 - - - -参考文章:https://www.jianshu.com/p/f90866dcbffc - - - -*** - - - -#### 通道 - -##### 基本介绍 - -通道(Channel):表示 IO 源与目标打开的连接,Channel 类似于传统的流,只不过 Channel 本身不能直接访问数据,Channel 只能与 Buffer **进行交互** - -1. NIO 的通道类似于流,但有些区别如下: - * 通道可以同时进行读写,而流只能读或者只能写 - * 通道可以实现异步读写数据 - * 通道可以从缓冲读数据,也可以写数据到缓冲 - -2. BIO 中的 stream 是单向的,NIO中的 Channel 是双向的,可以读操作,也可以写操作 - -3. Channel 在 NIO 中是一个接口:`public interface Channel extends Closeable{}` - - - -Channel 实现类: - -* FileChannel:用于读取、写入、映射和操作文件的通道 -* DatagramChannel:通过 UDP 读写网络中的数据通道 -* SocketChannel:通过 TCP 读写网络中的数据 -* ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个SocketChannel。 - 提示:ServerSocketChanne 类似 ServerSocket , SocketChannel 类似 Socket - - - -*** - - - -##### 常用API - -获取 Channel 方式: - -* 对支持通道的对象调用 `getChannel()` 方法 -* 通过通道的静态方法 `open()` 打开并返回指定通道 -* 使用 Files 类的静态方法 `newByteChannel()` 获取字节通道 - -Channel 基本操作: - -| 方法 | 说明 | -| ------------------------------------------ | -------------------------------------------------------- | -| public abstract int read(ByteBuffer dst) | 从 Channel 中读取数据到 ByteBuffer,从 position 开始储存 | -| public final long read(ByteBuffer[] dsts) | 将Channel中的数据“分散”到ByteBuffer[] | -| public abstract int write(ByteBuffer src) | 将 ByteBuffer 中的数据写入 Channel,从 position 开始写出 | -| public final long write(ByteBuffer[] srcs) | 将ByteBuffer[]到中的数据“聚集”到Channel | -| public abstract long position() | 返回此通道的文件位置 | -| FileChannel position(long newPosition) | 设置此通道的文件位置 | -| public abstract long size() | 返回此通道的文件的当前大小 | - -**读写都是相对于内存来看,也就是缓冲区** - - - -**** - - - -##### 文件读写 - -```java -public class ChannelTest { - @Test - public void write() throws Exception{ - // 1、字节输出流通向目标文件 - FileOutputStream fos = new FileOutputStream("data01.txt"); - // 2、得到字节输出流对应的通道Channel - FileChannel channel = fos.getChannel(); - // 3、分配缓冲区 - ByteBuffer buffer = ByteBuffer.allocate(1024); - buffer.put("hello,黑马Java程序员!".getBytes()); - // 4、把缓冲区切换成写出模式 - buffer.flip(); - channel.write(buffer); - channel.close(); - System.out.println("写数据到文件中!"); - } - @Test - public void read() throws Exception { - // 1、定义一个文件字节输入流与源文件接通 - FileInputStream fis = new FileInputStream("data01.txt"); - // 2、需要得到文件字节输入流的文件通道 - FileChannel channel = fis.getChannel(); - // 3、定义一个缓冲区 - ByteBuffer buffer = ByteBuffer.allocate(1024); - // 4、读取数据到缓冲区 - channel.read(buffer); - buffer.flip(); - // 5、读取出缓冲区中的数据并输出即可 - String rs = new String(buffer.array(),0,buffer.remaining()); - System.out.println(rs); - } -} -``` - - - -*** - - - -##### 文件复制 - -Channel 的两个方法: - -* `abstract long transferFrom(ReadableByteChannel src, long position, long count)`:从给定的可读字节通道将字节传输到该通道的文件中 - * src:源通道 - * position:文件中要进行传输的位置,必须是非负的 - * count:要传输的最大字节数,必须是非负的 - -* `abstract long transferTo(long position, long count, WritableByteChannel target)`:将该通道文件的字节传输到给定的可写字节通道。 - * position:传输开始的文件中的位置; 必须是非负的 - * count:要传输的最大字节数; 必须是非负的 - * target:目标通道 - -文件复制的两种方式: - -1. Buffer -2. 使用上述两种方法 - -![](https://gitee.com/seazean/images/raw/master/Java/NIO-复制文件.png) - -```java -public class ChannelTest { - @Test - public void copy1() throws Exception { - File srcFile = new File("C:\\壁纸.jpg"); - File destFile = new File("C:\\Users\\壁纸new.jpg"); - // 得到一个字节字节输入流 - FileInputStream fis = new FileInputStream(srcFile); - // 得到一个字节输出流 - FileOutputStream fos = new FileOutputStream(destFile); - // 得到的是文件通道 - FileChannel isChannel = fis.getChannel(); - FileChannel osChannel = fos.getChannel(); - // 分配缓冲区 - ByteBuffer buffer = ByteBuffer.allocate(1024); - while(true){ - // 必须先清空缓冲然后再写入数据到缓冲区 - buffer.clear(); - // 开始读取一次数据 - int flag = isChannel.read(buffer); - if(flag == -1){ - break; - } - // 已经读取了数据 ,把缓冲区的模式切换成可读模式 - buffer.flip(); - // 把数据写出到 - osChannel.write(buffer); - } - isChannel.close(); - osChannel.close(); - System.out.println("复制完成!"); - } - - @Test - public void copy02() throws Exception { - // 1、字节输入管道 - FileInputStream fis = new FileInputStream("data01.txt"); - FileChannel isChannel = fis.getChannel(); - // 2、字节输出流管道 - FileOutputStream fos = new FileOutputStream("data03.txt"); - FileChannel osChannel = fos.getChannel(); - // 3、复制 - osChannel.transferFrom(isChannel,isChannel.position(),isChannel.size()); - isChannel.close(); - osChannel.close(); - } - - @Test - public void copy03() throws Exception { - // 1、字节输入管道 - FileInputStream fis = new FileInputStream("data01.txt"); - FileChannel isChannel = fis.getChannel(); - // 2、字节输出流管道 - FileOutputStream fos = new FileOutputStream("data04.txt"); - FileChannel osChannel = fos.getChannel(); - // 3、复制 - isChannel.transferTo(isChannel.position() , isChannel.size() , osChannel); - isChannel.close(); - osChannel.close(); - } -} -``` - - - -*** - - - -##### 分散聚集 - -分散读取(Scatter ):是指把Channel通道的数据读入到多个缓冲区中去 - -聚集写入(Gathering ):是指将多个 Buffer 中的数据“聚集”到 Channel。 - -```java -public class ChannelTest { - @Test - public void test() throws IOException{ - // 1、字节输入管道 - FileInputStream is = new FileInputStream("data01.txt"); - FileChannel isChannel = is.getChannel(); - // 2、字节输出流管道 - FileOutputStream fos = new FileOutputStream("data02.txt"); - FileChannel osChannel = fos.getChannel(); - // 3、定义多个缓冲区做数据分散 - ByteBuffer buffer1 = ByteBuffer.allocate(4); - ByteBuffer buffer2 = ByteBuffer.allocate(1024); - ByteBuffer[] buffers = {buffer1 , buffer2}; - // 4、从通道中读取数据分散到各个缓冲区 - isChannel.read(buffers); - // 5、从每个缓冲区中查询是否有数据读取到了 - for(ByteBuffer buffer : buffers){ - buffer.flip();// 切换到读数据模式 - System.out.println(new String(buffer.array() , 0 , buffer.remaining())); - } - // 6、聚集写入到通道 - osChannel.write(buffers); - isChannel.close(); - osChannel.close(); - System.out.println("文件复制~~"); - } -} -``` - - - -*** - - - -#### 选择器 - -##### 基本介绍 - -选择器(Selector) 是 SelectableChannle 对象的**多路复用器**,Selector 可以同时监控多个通道的状况,利用 Selector 可使一个单独的线程管理多个 Channel。**Selector 是非阻塞 IO 的核心**。 - -![](https://gitee.com/seazean/images/raw/master/Java/NIO-Selector.png) - -* Selector 能够检测多个注册的通道上是否有事件发生(多个 Channel 以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理,就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求 -* 只有在连接/通道真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程 -* 避免了多线程之间的上下文切换导致的开销 - - - -*** - - - -##### 常用API - -创建 Selector:`Selector selector = Selector.open();` - -向选择器注册通道:`SelectableChannel.register(Selector sel, int ops)` - -选择器对通道的监听事件,需要通过第二个参数 ops 指定。监听的事件类型用四个常量表示: - -* 读 : SelectionKey.OP_READ (1) -* 写 : SelectionKey.OP_WRITE (4) -* 连接 : SelectionKey.OP_CONNECT (8) -* 接收 : SelectionKey.OP_ACCEPT (16) -* 若注册时不止监听一个事件,则可以使用“位或”操作符连接: - `int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE ` - - - -**Selector API**: - -| 方法 | 说明 | -| ------------------------------------------------ | ------------------------------------- | -| public static Selector open() | 打开选择器 | -| public abstract void close() | 关闭此选择器 | -| public abstract int select() | 阻塞选择一组通道准备好进行I/O操作的键 | -| public abstract int select(long timeout) | 阻塞等待 timeout 毫秒 | -| public abstract int selectNow() | 获取一下,不阻塞,立刻返回 | -| public abstract Selector wakeup() | 唤醒正在阻塞的 selector | -| public abstract Set selectedKeys() | 返回此选择器的选择键集 | - -SelectionKey API: - -| 方法 | 说明 | -| ------------------------------------------- | -------------------------------------------------- | -| public abstract void cancel() | 取消该键的通道与其选择器的注册 | -| public abstract SelectableChannel channel() | 返回创建此键的通道,该方法在取消键之后仍将返回通道 | -| public final boolean isAcceptable() | 检测此密钥的通道是否已准备好接受新的套接字连接 | -| public final boolean isConnectable() | 检测此密钥的通道是否已完成或未完成其套接字连接操作 | -| public final boolean isReadable() | 检测此密钥的频道是否可以阅读 | -| public final boolean isWritable() | 检测此密钥的通道是否准备好进行写入 | - -基本步骤: - -```java -//1.获取通道 -ServerSocketChannel ssChannel = ServerSocketChannel.open(); -//2.切换非阻塞模式 -ssChannel.configureBlocking(false); -//3.绑定连接 -ssChannel.bin(new InetSocketAddress(9999)); -//4.获取选择器 -Selector selector = Selector.open(); -//5.将通道注册到选择器上,并且指定“监听接收事件” -ssChannel.register(selector, SelectionKey.OP_ACCEPT); -``` - - - -*** - - - -#### NIO实现 - -##### 常用API - -* SelectableChannel_API - - | 方法 | 说明 | - | ------------------------------------------------------------ | -------------------------------------------- | - | public final SelectableChannel configureBlocking(boolean block) | 设置此通道的阻塞模式 | - | public final SelectionKey register(Selector sel, int ops) | 向给定的选择器注册此通道,并选择关注的的事件 | - -* SocketChannel_API: - - | 方法 | 说明 | - | :------------------------------------------------------ | ------------------------------ | - | public static SocketChannel open() | 打开套接字通道 | - | public static SocketChannel open(SocketAddress remote) | 打开套接字通道并连接到远程地址 | - | public abstract boolean connect(SocketAddress remote) | 连接此通道的到远程地址 | - | public abstract SocketChannel bind(SocketAddress local) | 将通道的套接字绑定到本地地址 | - | public abstract SocketAddress getLocalAddress() | 返回套接字绑定的本地套接字地址 | - | public abstract SocketAddress getRemoteAddress() | 返回套接字连接的远程套接字地址 | - -* ServerSocketChannel_API: - - | 方法 | 说明 | - | ---------------------------------------------------------- | ------------------------------------------------------------ | - | public static ServerSocketChannel open() | 打开服务器套接字通道 | - | public final ServerSocketChannel bind(SocketAddress local) | 将通道的套接字绑定到本地地址,并配置套接字以监听连接 | - | public abstract SocketChannel accept() | 接受与此通道套接字的连接,通过此方法返回的套接字通道将处于阻塞模式 | - - * 如果 ServerSocketChannel 处于非阻塞模式,如果没有挂起连接,则此方法将立即返回 null - * 如果通道处于阻塞模式,如果没有挂起连接将无限期地阻塞,直到有新的连接或发生 I/O 错误 - - - -*** - - - -##### 代码实现 - -服务端 : - -1. 获取通道,当客户端连接服务端时,服务端会通过 `ServerSocketChannel.accept` 得到 SocketChannel - -2. 切换非阻塞模式 - -3. 绑定连接 - -4. 获取选择器 - -5. 将通道注册到选择器上, 并且指定“监听接收事件” - -6. 轮询式的获取选择器上已经“准备就绪”的事件 - -客户端: - -1. 获取通道:`SocketChannel sc = SocketChannel.open(new InetSocketAddress(HOST, PORT))` -2. 切换非阻塞模式 -3. 分配指定大小的缓冲区:`ByteBuffer buffer = ByteBuffer.allocate(1024)` -4. 发送数据给服务端 - -37行代码,如果判断条件改为 !=-1,需要客户端 shutdown 一下 - -```java -public class Server { - public static void main(String[] args){ - // 1、获取通道 - ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); - // 2、切换为非阻塞模式 - serverSocketChannel.configureBlocking(false); - // 3、绑定连接的端口 - serverSocketChannel.bind(new InetSocketAddress(9999)); - // 4、获取选择器Selector - Selector selector = Selector.open(); - // 5、将通道都注册到选择器上去,并且开始指定监听接收事件 - serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); - // 6、使用Selector选择器轮询已经就绪好的事件 - while (selector.select() > 0) { - System.out.println("----开始新一轮的时间处理----"); - // 7、获取选择器中的所有注册的通道中已经就绪好的事件 - Set selectionKeys = selector.selectedKeys(); - Iterator it = selectionKeys.iterator(); - // 8、开始遍历这些准备好的事件 - while (it.hasNext()) { - SelectionKey key = it.next();// 提取当前这个事件 - // 9、判断这个事件具体是什么 - if (key.isAcceptable()) { - // 10、直接获取当前接入的客户端通道 - SocketChannel socketChannel = serverSocketChannel.accept(); - // 11 、切换成非阻塞模式 - socketChannel.configureBlocking(false); - // 12、将本客户端通道注册到选择器 - socketChannel.register(selector, SelectionKey.OP_READ); - } else if (key.isReadable()) { - // 13、获取当前选择器上的读就绪事件 - SelectableChannel channel = key.channel(); - SocketChannel socketChannel = (SocketChannel) channel; - // 14、读取数据 - ByteBuffer buffer = ByteBuffer.allocate(1024); - int len; - while ((len = socketChannel.read(buffer)) > 0) { - buffer.flip(); - System.out.println(socketChannel.getRemoteAddress() + ":" + new String(buffer.array(), 0, len)); - buffer.clear();// 清除之前的数据 - } - } - //删除当前的 selectionKey,防止重复操作 - it.remove(); - } - } - } -} -``` - -```java -public class Client { - public static void main(String[] args) throws Exception { - // 1、获取通道 - SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999)); - // 2、切换成非阻塞模式 - socketChannel.configureBlocking(false); - // 3、分配指定缓冲区大小 - ByteBuffer buffer = ByteBuffer.allocate(1024); - // 4、发送数据给服务端 - Scanner sc = new Scanner(System.in); - while (true){ - System.out.print("请说:"); - String msg = sc.nextLine(); - buffer.put(("波妞:" + msg).getBytes()); - buffer.flip(); - socketChannel.write(buffer); - buffer.clear(); - } - } -} -``` - - - -*** - - - -### AIO - -Java AIO(NIO.2) : AsynchronousI/O,异步非阻塞,采用了 Proactor 模式。服务器实现模式为一个有效请求一个线程,客户端的 I/O 请求都是由 OS 先完成了再通知服务器应用去启动线程进行处理。 - -```java -AIO异步非阻塞,基于NIO的,可以称之为NIO2.0 - BIO NIO AIO -Socket SocketChannel AsynchronousSocketChannel -ServerSocket ServerSocketChannel AsynchronousServerSocketChannel -``` - -当进行读写操作时,调用 API 的 read 或 write 方法,这两种方法均为异步的,完成后会主动调用回调函数: - -* 对于读操作,当有流可读取时,操作系统会将可读的流传入 read 方法的缓冲区 -* 对于写操作,当操作系统将 write 方法传递的流写入完毕时,操作系统主动通知应用程序 - -在JDK1.7中,这部分内容被称作NIO.2,主要在 Java.nio.channels 包下增加了下面四个异步通道: -AsynchronousSocketChannel、AsynchronousServerSocketChannel、AsynchronousFileChannel、AsynchronousDatagramChannel - - - -*** - - - -## 反射 - -### 测试框架 - -> 单元测试是指程序员写的测试代码给自己的类中的方法进行预期正确性的验证。 -> 单元测试一旦写好了这些测试代码,就可以一直使用,可以实现一定程度上的自动化测试。 - -单元测试的经典框架:Junit - -* Junit : 是Java语言编写的第三方单元测试框架,可以帮助我们方便快速的测试我们代码的正确性。 -* 单元测试: - * 单元:在Java中,一个类就是一个单元 - * 单元测试:Junit编写的一小段代码,用来对某个类中的某个方法进行功能测试或业务逻辑测试 - -Junit单元测试框架的作用: - -* 用来对类中的方法功能进行有目的的测试,以保证程序的正确性和稳定性 -* 能够**独立的**测试某个方法或者所有方法的预期正确性 - -Junit框架的使用步骤: - -```java -(1)下载这个框架。 - 框架一般是jar包的形式,jar包里面都是class文件。(Java工程的最终形式) - class文件就是我们调用的核心代码。Junit已经被IDEA下载好了,可以直接导入到项目使用的。 - -(2)直接用Junit测试代码即可 - a.先模拟业务代码 - b.写测试类 - 测试类的命名规范:以Test开头,以业务类类名结尾,使用驼峰命名法 - 业务名称是:UserService - 测试这个业务类的测试类:TestUserService/UserServiceTest - c.在测试类中写测试方法 - 测试方法的命名规则:以test开头,以业务方法名结尾 - 比如被测试业务方法名为:login,那么测试方法名就应该叫:testLogin - -(3)如何运行测试方法 - 选中方法名 --> 右键 --> Run '测试方法名' 运行选中的测试方法 - 选中测试类类名 --> 右键 --> Run '测试类类名' 运行测试类中所有测试方法 - 选中模块名 --> 右键 --> Run 'All Tests' 运行模块中的所有测试类的所有测试方法 -``` - -测试方法注意事项:**必须是public修饰的,没有返回值,没有参数,使用注解@Test修饰** - -Junit常用注解(Junit 4.xxxx版本),@Test 测试方法: - -* @Before:用来修饰实例方法,该方法会在每一个测试方法执行之前执行一次 -* @After:用来修饰实例方法,该方法会在每一个测试方法执行之后执行一次 -* @BeforeClass:用来静态修饰方法,该方法会在所有测试方法之前**只**执行一次 -* @AfterClass:用来静态修饰方法,该方法会在所有测试方法之后**只**执行一次 - -Junit常用注解(Junit5.xxxx版本),@Test 测试方法: - -* @BeforeEach:用来修饰实例方法,该方法会在每一个测试方法执行之前执行一次 -* @AfterEach:用来修饰实例方法,该方法会在每一个测试方法执行之后执行一次 -* @BeforeAll:用来静态修饰方法,该方法会在所有测试方法之前只执行一次 -* @AfterAll:用来静态修饰方法,该方法会在所有测试方法之后只执行一次 - -作用: - -* 开始执行的方法:初始化资源 -* 执行完之后的方法:释放资源 - -```java -public class UserService { - public String login(String loginName , String passWord){ - if("admin".equals(loginName)&&"123456".equals(passWord)){ - return "success"; - } - return "用户名或者密码错误!"; - } - public void chu(int a , int b){ - System.out.println(a / b); - } -} -``` - -```java -//测试方法的要求:1.必须public修饰 2.没有返回值没有参数 3. 必须使注解@Test修饰 -public class UserServiceTest { - // @Before:用来修饰实例方法,该方法会在每一个测试方法执行之前执行一次。 - @Before - public void before(){ - System.out.println("===before==="); - } - // @After:用来修饰实例方法,该方法会在每一个测试方法执行之后执行一次。 - @After - public void after(){ - System.out.println("===after==="); - } - // @BeforeClass:用来静态修饰方法,该方法会在所有测试方法之前只执行一次。 - @BeforeClass - public static void beforeClass(){ - System.out.println("===beforeClass==="); - } - // @AfterClass:用来静态修饰方法,该方法会在所有测试方法之后只执行一次。 - @AfterClass - public static void afterClass(){ - System.out.println("===afterClass==="); - } - @Test - public void testLogin(){ - UserService userService = new UserService(); - String rs = userService.login("admin","123456"); - /**断言预期结果的正确性。 - * 参数一:测试失败的提示信息。 - * 参数二:期望值。 - * 参数三:实际值 - */ - Assert.assertEquals("登录业务功能方法有错误,请检查!","success",rs); - } - @Test - public void testChu(){ - UserService userService = new UserService(); - userService.chu(10 , 0); - } -} -``` - - - - - -**** - - - -### 介绍反射 - -反射是指对于任何一个类,在"运行的时候"都可以直接得到这个类全部成分 - -* 构造器对象:Constructor -* 成员变量对象:Field - -* 成员方法对象:Method - -核心思想:在运行时获取类编译后的字节码文件对象,然后解析类中的全部成分。 - -反射提供了一个Class类型:HelloWorld.java → javac → HelloWorld.class - -* `Class c = HelloWorld.class;` - -注意:反射是工作在**运行时**的技术,只有运行之后才会有class类对象 - -作用:可以在运行时得到一个类的全部成分然后操作,破坏封装性,也可以破坏泛型的约束性。 - -**反射的优点:** - -- 可扩展性:应用程序可以利用全限定名创建可扩展对象的实例,来使用来自外部的用户自定义类 -- 类浏览器和可视化开发环境:一个类浏览器需要可以枚举类的成员,可视化开发环境(如 IDE)可以从利用反射中可用的类型信息中受益,以帮助程序员编写正确的代码 -- 调试器和测试工具: 调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率 - -**反射的缺点:** - -- 性能开销:反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化,反射操作的效率要比那些非射操作低得多,应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。 -- 安全限制:使用反射技术要求程序必须在一个没有安全限制的环境中运行,如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了 -- 内部暴露:由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化 - - - -*** - - - -### 获取元素 - -#### 获取类 - -反射技术的第一步是先得到Class类对象,有三种方式获取: - -* 类名.class -* 通过类的对象.getClass()方法 -* Class.forName("类的全限名"):`public static Class forName(String className) ` - -Class类下的方法: - -| 方法 | 作用 | -| ---------------------- | ------------------------------------------------------------ | -| String getSimpleName() | 获得类名字符串:类名 | -| String getName() | 获得类全名:包名+类名 | -| T newInstance() | 创建Class对象关联类的对象,底层是调用无参数构造器,已经被淘汰 | - -```java -public class ReflectDemo{ - public static void main(String[] args) throws Exception { - // 反射的第一步永远是先得到类的Class文件对象: 字节码文件。 - // 1.类名.class - Class c1 = Student.class; - System.out.println(c1);//class _03反射_获取Class类对象.Student - - // 2.对象.getClass() - Student swk = new Student(); - Class c2 = swk.getClass(); - System.out.println(c2); - - // 3.Class.forName("类的全限名") - // 直接去加载该类的class文件。 - Class c3 = Class.forName("_03反射_获取Class类对象.Student"); - System.out.println(c3); - - System.out.println(c1.getSimpleName()); // 获取类名本身(简名)Student - System.out.println(c1.getName()); //获取类的全限名_03反射_获取Class类对象.Student - } -} -class Student{} -``` - - - -*** - - - -#### 获取构造 - -获取构造器的API: - -* Constructor getConstructor(Class... parameterTypes):根据参数匹配获取某个构造器,只能拿public修饰的构造器,几乎不用! -* **Constructor getDeclaredConstructor(Class... parameterTypes)**:根据参数匹配获取某个构造器,只要申明就可以定位,不关心权限修饰符 -* Constructor[] getConstructors():获取所有的构造器,只能拿public修饰的构造器,几乎不用! -* **Constructor[] getDeclaredConstructors()**:获取所有构造器,只要申明就可以定位,不关心权限修饰符。 - -Constructor的常用API: - -| 方法 | 作用 | -| --------------------------------- | -------------------------------------- | -| T newInstance(Object... initargs) | 创建对象,注入构造器需要的数据 | -| void setAccessible(true) | 修改访问权限,true攻破权限(暴力反射) | -| String getName() | 以字符串形式返回此构造函数的名称 | -| int getParameterCount() | 返回参数数量 | -| Class[] getParameterTypes | 返回参数类型数组 | - -```java -public class TestStudent01 { - @Test - public void getDeclaredConstructors(){ - // a.反射第一步先得到Class类对象 - Class c = Student.class ; - // b.定位全部构造器,只要申明了就可以拿到 - Constructor[] cons = c.getDeclaredConstructors(); - // c.遍历这些构造器 - for (Constructor con : cons) { - System.out.println(con.getName()+"->"+con.getParameterCount()); - } - } - @Test - public void getDeclaredConstructor() throws Exception { - // a.反射第一步先得到Class类对象 - Class c = Student.class ; - // b.定位某个构造器,根据参数匹配,只要申明了就可以获取 - //Constructor con = c.getDeclaredConstructor(); // 可以拿到!定位无参数构造器! - Constructor con = c.getDeclaredConstructor(String.class, int.class); //有参数的!! - // c.构造器名称和参数 - System.out.println(con.getName()+"->"+con.getParameterCount()); - } -} -``` - -```java -public class Student { - private String name ; - private int age ; - private Student(){ - System.out.println("无参数构造器被执行~~~~"); - } - public Student(String name, int age) { - System.out.println("有参数构造器被执行~~~~"); - this.name = name; - this.age = age; - } -} -``` - -```java -//测试方法 -public class TestStudent02 { - // 1.调用无参数构造器得到一个类的对象返回。 - @Test - public void createObj01() throws Exception { - // a.反射第一步是先得到Class类对象 - Class c = Student.class ; - // b.定位无参数构造器对象 - Constructor constructor = c.getDeclaredConstructor(); - // c.暴力打开私有构造器的访问权限 - constructor.setAccessible(true); - // d.通过无参数构造器初始化对象返回 - Student swk = (Student) constructor.newInstance(); // 最终还是调用无参数构造器的! - System.out.println(swk);//Student{name='null', age=0} - } - - // 2.调用有参数构造器得到一个类的对象返回。 - @Test - public void createObj02() throws Exception { - // a.反射第一步是先得到Class类对象 - Class c = Student.class ; - // b.定位有参数构造器对象 - Constructor constructor = c.getDeclaredConstructor(String.class , int.class); - // c.通过无参数构造器初始化对象返回 - Student swk = (Student) constructor.newInstance("孙悟空",500); // 最终还是调用有参数构造器的! - System.out.println(swk);//Student{name='孙悟空', age=500} - } -} - - -``` - - - -*** - - - -#### 获取变量 - -获取Field成员变量API: - -* Field getField(String name) : 根据成员变量名获得对应Field对象,只能获得public修饰 -* Field getDeclaredField(String name) : 根据成员变量名获得对应Field对象,所有申明的变量 -* Field[] getFields() : 获得所有的成员变量对应的Field对象,只能获得public的 -* Field[] getDeclaredFields() : 获得所有的成员变量对应的Field对象,只要申明了就可以得到 - -Field的方法:给成员变量赋值和取值 - -| 方法 | 作用 | -| ---------------------------------- | --------------------------------------------------------- | -| 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 { - //获取全部成员变量 - @Test - public void getDeclaredFields(){ - // a.先获取class类对象 - Class c = Dog.class; - // b.获取全部申明的成员变量对象 - Field[] fields = c.getDeclaredFields(); - for (Field field : fields) { - System.out.println(field.getName()+"->"+field.getType()); - } - } - //获取某个成员变量 - @Test - public void getDeclaredField() throws Exception { - // a.先获取class类对象 - Class c = Dog.class; - // b.定位某个成员变量对象 :根据名称定位!! - Field ageF = c.getDeclaredField("age"); - System.out.println(ageF.getName()+"->"+ageF.getType()); - } -} -``` - -```java -public class Dog { - private String name; - private int age ; - private String color ; - public static String school; - public static final String SCHOOL_1 = "宠物学校"; - - public Dog() { - } - - public Dog(String name, int age, String color) { - this.name = name; - this.age = age; - this.color = color; - } -} -``` - -```java -//测试方法 -public class FieldDemo02 { - @Test - public void setField() throws Exception { - // a.反射的第一步获取Class类对象 - Class c = Dog.class ; - // b.定位name成员变量 - Field name = c.getDeclaredField("name"); - // c.为这个成员变量赋值! - Dog d = new Dog(); - name.setAccessible(true); - name.set(d,"泰迪"); - System.out.println(d);//Dog{name='泰迪', age=0, color='null'} - // d.获取成员变量的值 - String value = name.get(d)+""; - System.out.println(value);//泰迪 - } -} -``` - - - -#### 获取方法 - -获取Method方法API: - -* 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对象名 - -```java -public class MethodDemo{ - //获得类中的所有成员方法对象 - @Test - public void getDeclaredMethods(){ - // a.先获取class类对象 - Class c = Dog.class ; - // b.获取全部申明的方法! - Method[] methods = c.getDeclaredMethods(); - // c.遍历这些方法 - for (Method method : methods) { - System.out.println(method.getName()+"->" - + method.getParameterCount()+"->" + method.getReturnType()); - } - } - @Test - public void getDeclardMethod() throws Exception { - Class c = Dog.class; - Method run = c.getDeclaredMethod("run"); - // c.触发方法执行! - Dog d = new Dog(); - Object o = run.invoke(d); - System.out.println(o);// 如果方法没有返回值,结果是null - - //参数一:方法名称 参数二:方法的参数个数和类型(可变参数!) - Method eat = c.getDeclaredMethod("eat",String.class); - eat.setAccessible(true); // 暴力反射! - - //参数一:被触发方法所在的对象 参数二:方法需要的入参值 - Object o1 = eat.invoke(d,"肉"); - System.out.println(o1);// 如果方法没有返回值,结果是null - } -} - -public class Dog { - private String name ; - public Dog(){ - } - public void run(){System.out.println("狗跑的贼快~~");} - private void eat(){System.out.println("狗吃骨头");} - private void eat(String name){System.out.println("狗吃"+name);} - public static void inAddr(){System.out.println("在吉山区有一只单身狗!");} -} -``` - - - -*** - - - -### 暴力攻击 - -泛型只能工作在编译阶段,运行阶段泛型就消失了,反射工作在运行时阶段 - -1. 反射可以破坏面向对象的封装性(暴力反射) -2. 同时可以破坏泛型的约束性 - -```java -public class ReflectDemo { - public static void main(String[] args) throws Exception { - List scores = new ArrayList<>(); - scores.add(99.3); - scores.add(199.3); - scores.add(89.5); - // 拓展:通过反射暴力的注入一个其他类型的数据进去。 - // a.先得到集合对象的Class文件对象 - Class c = scores.getClass(); - // b.从ArrayList的Class对象中定位add方法 - Method add = c.getDeclaredMethod("add", Object.class); - // c.触发scores集合对象中的add执行(运行阶段,泛型不能约束了) - add.invoke(scores,"波仔"); - System.out.println(scores); - } -} -``` - - - - - -*** - - - - - -## 注解 - -### 概念 - -注解:类的组成部分,可以给类携带一些额外的信息,提供一种安全的类似注释标记的机制,用来将任何信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联 - -* 注解是JDK1.5的新特性 -* 注解是给编译器或JVM看的,编译器或JVM可以根据注解来完成对应的功能 -* 注解类似修饰符,应用于包、类型、构造方法、方法、成员变量、参数及本地变量的声明语句中 - -注解作用: - -* 标记 -* 框架技术多半都是在使用注解和反射,都是属于框架的底层基础技术 -* 在编译时进行格式检查,比如方法重写约束 @Override、函数式接口约束 @FunctionalInterface. - - - -*** - - - -### 注解格式 - -定义格式:自定义注解用@interface关键字,注解默认可以标记很多地方 - -```java -修饰符 @interface 注解名{ - // 注解属性 -} -``` - -使用注解的格式:@注解名 - -```java -@Book -@MyTest -public class MyBook { - //方法变量都可以注解 -} - -@interface Book{ -} -@interface MyTest{ -} -``` - - - -*** - - - -### 注解属性 - -#### 普通属性 - -注解可以有属性,**属性名必须带()**,在用注解的时候,属性必须赋值,除非属性有默认值 - -属性的格式: - -* 格式1:数据类型 属性名(); -* 格式2:数据类型 属性名() default 默认值; - -属性适用的数据类型: - -* 八种数据数据类型(int,short,long,double,byte,char,boolean,float) 和 String、Class -* 以上类型的数组形式都支持 - -```java -@MyBook(name="《精通Java基础》",authors = {"播仔","Dlei","播妞"} , price = 99.9 ) -public class AnnotationDemo01 { - @MyBook(name="《精通MySQL数据库入门到删库跑路》",authors = {"小白","小黑"} , - price = 19.9 , address = "北京") - public static void main(String[] args) { - } -} -// 自定义一个注解 -@interface MyBook{ - String name(); - String[] authors(); // 数组 - double price(); - String address() default "武汉"; -} - -``` - - - -#### 特殊属性 - -注解的特殊属性名称:value - -* 如果只有一个value属性的情况下,使用value属性的时候可以省略value名称不写 -* 如果有多个属性,且多个属性没有默认值,那么value是不能省略的 - -```java -//@Book("/deleteBook.action") -@Book(value = "/deleteBook.action" , age = 12) -public class AnnotationDemo01{ -} - -@interface Book{ - String value(); - int age() default 10; -} -``` - - - -*** - - - -### 元注解 - -元注解是sun公司提供的,用来注解自定义注解 - -元注解有四个: - -* @Target:约束自定义注解可以标记的范围,默认值为任何元素,表示该注解用于什么地方 - - 可使用的值定义在ElementType枚举类中: - - - `ElementType.CONSTRUCTOR`:用于描述构造器 - - `ElementType.FIELD`:成员变量、对象、属性(包括enum实例) - - `ElementType.LOCAL_VARIABLE`:用于描述局部变量 - - `ElementType.METHOD`:用于描述方法 - - `ElementType.PACKAGE`:用于描述包 - - `ElementType.PARAMETER`:用于描述参数 - - `ElementType.TYPE`:用于描述类、接口(包括注解类型) 或enum声明 - -* @Retention:定义该注解的生命周期,申明注解的作用范围:编译时,运行时 - - 可使用的值定义在RetentionPolicy枚举类中: - - - `RetentionPolicy.SOURCE`:在编译阶段丢弃,这些注解在编译结束之后就不再有任何意义,只作用在源码阶段,生成的字节码文件中不存在。`@Override`, `@SuppressWarnings`都属于这类注解 - - `RetentionPolicy.CLASS`:在类加载时丢弃,在字节码文件的处理中有用,运行阶段不存在,默认值 - - `RetentionPolicy.RUNTIME` : 始终不会丢弃,运行期也保留该注解,因此可以使用反射机制读取该注解的信息,自定义的注解通常使用这种方式 - -* @Inherited:表示修饰的自定义注解可以被子类继承 - -* @Documented:表示是否将自定义的注解信息添加在 java 文档中 - -```java -public class AnnotationDemo01{ - // @MyTest // 只能注解方法 - private String name; - - @MyTest - public static void main( String[] args) { - } -} -@Target(ElementType.METHOD) // 申明只能注解方法 -@Retention(RetentionPolicy.RUNTIME) // 申明注解从写代码一直到运行还在,永远存活!! -@interface MyTest{ -} -``` - - - -*** - - - -### 注解解析 - -开发中经常要知道一个类的成分上面到底有哪些注解,注解有哪些属性数据,这都需要进行注解的解析。 - -注解解析相关的接口: - -* Annotation:注解类型,该类是所有注解的父类,注解都是一个Annotation的对象 -* AnnotatedElement:该接口定义了与注解解析相关的方法 -* Class、Method、Field、Constructor类成分:实现AnnotatedElement接口,拥有解析注解的能力 - -API : - `Annotation[] getDeclaredAnnotations()` : 获得当前对象上使用的所有注解,返回注解数组 - `T getDeclaredAnnotation(Class annotationClass)` : 根据注解类型获得对应注解对象 - `T getAnnotation(Class annotationClass)` : 根据注解类型获得对应注解对象 - `boolean isAnnotationPresent(Class class)` : 判断对象是否使用了指定的注解 - -注解原理:注解本质是一个继承了`Annotation` 的特殊接口,其具体实现类是 Java 运行时生成的**动态代理类**,通过反射获取注解时,返回的是 Java 运行时生成的动态代理对象 `$Proxy1`,通过代理对象调用自定义注解(接口)的方法,会最终调用 `AnnotationInvocationHandler` 的 `invoke` 方法,该方法会从 `memberValues` 这个Map 中找出对应的值,而 `memberValues` 的来源是 Java 常量池 - -解析注解数据的原理:注解在哪个成分上,就先拿哪个成分对象,比如注解作用在类上,则要该类的Class对象,再来拿上面的注解 - -```java -public class AnnotationDemo{ - @Test - public void parseClass() { - // 1.定位Class类对象 - Class c = BookStore.class; - // 2.判断这个类上是否使用了某个注解 - if(c.isAnnotationPresent(Book.class)){ - // 3.获取这个注解对象 - Book b = (Book)c.getDeclarAnnotation(Book.class); - System.out.println(book.value()); - System.out.println(book.price()); - System.out.println(Arrays.toString(book.authors())); - } - } - @Test - public void parseMethod() throws Exception { - Class c = BookStore.class; - Method run = c.getDeclaredMethod("run"); - if(run.isAnnotationPresent(Book.class)){ - Book b = (Book)run.getDeclaredAnnotation(Book.class); - sout(上面的三个); - } - } -} - -@Book(value = "《Java基础到精通》", price = 99.5, authors = {"波仔","波妞"}) -class BookStore{ - @Book(value = "《Mybatis持久层框架》", price = 199.5, authors = {"dlei","播客"}) - public void run(){ - } -} -@Target({ElementType.TYPE,ElementType.METHOD}) // 类和成员方法上使用 -@Retention(RetentionPolicy.RUNTIME) // 注解永久存活 -@interface Book{ - String value(); - double price() default 100; - String[] authors(); -} -``` - - - -*** - - - -### 注解模拟 - -注解模拟写一个Junit框架的基本使用 - -1. 定义一个自定义注解MyTest,只能注解方法,存活范围一直都在。 -2. 定义若干个方法,只要有@MyTest注解的方法就能被触发执行,没有这个注解的方法不能执行!! - -```java -public class TestDemo{ - @MyTest - public void test01(){System.out.println("===test01===");} - public void test02(){System.out.println("===test02===");} - @MyTest - public void test03(){System.out.println("===test03===");} - @MyTest - public void test04(){System.out.println("===test04===");} - - public static void main(String[] args) throws Exception { - TestDemo t = new TestDemo(); - Class c = TestDemo.class; - Method[] methods = c.getDeclaredMethods(); - for (Method method : methods) { - if(method.isAnnotationPresent(MyTest.class)){ - method.invoke(t); - } - } - } -} - -@Target(ElementType.METHOD) // 只能注解方法! -@Retention(RetentionPolicy.RUNTIME) // 一直都活着 -@interface MyTest{ -} -``` - - - -**** - - - -## XML - -### 概述 - -XML介绍: - -- XML 指可扩展标记语言(EXtensible Markup Language) -- XML 是一种**标记语言**,很类似 HTML,HTML文件也是XML文档 -- XML 的设计宗旨是**传输数据**,而非显示数据 -- XML 标签没有被预定义,需要自行定义标签 -- XML 被设计为具有自我描述性,易于阅读 -- XML 是 W3C 的推荐标准 - -**xml与html的区别**: - -​ XML 不是 HTML 的替代,XML 和 HTML 为不同的目的而设计。 -​ XML 被设计为传输和存储数据,其焦点是数据的内容;XMl标签可自定义,便于阅读。 -​ HTML 被设计用来显示数据,其焦点是数据的外观;HTML标签被预设好,便于浏览器识别。 -​ HTML 旨在显示信息,而 XML 旨在传输信息。 - - - -**** - - - -### 创建 - -person.xml - -```xml - - - 18 - 张三 - - -``` - - - -*** - - - -### 组成 - -XML文件中常见的组成元素有:文档声明、元素、属性、注释、转义字符、字符区。文件后缀名为xml - -* **文档声明** - ```` - 文档声明必须在第一行,以结束, - version:指定XML文档版本。必须属性,这里一般选择1.0; - enconding:指定当前文档的编码,可选属性,默认值是utf-8; - standalone: 该属性不是必须的,描述XML文件是否依赖其他的xml文件,取值为yes/no - -* **元素** - - * 格式1:` ` - 格式2:`` - 普通元素的结构由开始标签、元素体、结束标签组成; - 标签由一对尖括号和合法标识符组成,标签必须成对出现。特殊的标签可以不成对,必须有结束标记; - -* 元素体:可以是元素,也可以是文本,例如:``张三`` - * 空元素:空元素只有标签,而没有结束标签,但**元素必须自己闭合**,例如:```` - * 元素命名:区分大小写、不能使用空格冒号、不建议用XML xml Xml等开头 - * 必须存在一个根标签,有且只能有一个 - -* **属性** - `` - 属性是元素的一部分,它必须出现在元素的开始标签中 - 属性的定义格式:`属性名=“属性值”`,其中属性值必须使用单引或双引号括起来 - 一个元素可以有0~N个属性,但一个元素中不能出现同名属性 - 属性名不能使用空格 , 不要使用冒号等特殊字符,且必须以字母开头 - -* **注释** - - XML的注释与HTML相同,既以````结束。 - -* **转义字符** - XML中的转义字符与HTML一样。因为很多符号已经被文档结构所使用,所以在元素体或属性值中想使用这些符号就必须使用转义字符(也叫实体字符),例如:">"、"<"、"'"、"""、"&"。 - XML 中仅有字符 "<"和"&" 是非法的。省略号、引号和大于号是合法的,把它们替换为实体引用 - - | 字符 | 预定义的转义字符 | 说明 | - | :--: | :--------------: | :----: | - | < | ``<`` | 小于 | - | > | `` >`` | 大于 | - | " | `` "`` | 双引号 | - | ' | `` '`` | 单引号 | - | & | `` &`` | 和号 | - -* **字符区** - - ```xml - - ``` - - * CDATA 指的是不应由 XML 解析器进行解析的文本数据(Unparsed Character Data) -* CDATA 部分由 "" 结束; - * 大量的转义字符在xml文档中时,会使XML文档的可读性大幅度降低。这时使用CDATA段就会好一些 - - * 规则 - CDATA 部分不能包含字符串 "]]>"。也不允许嵌套的 CDATA 部分。 - 标记 CDATA 部分结尾的 "]]>" 不能包含空格或折行。 - - ```xml - - - - - - - - - 西门庆 - 32 - - - - select * from student where age < 18 && age > 10; - - - - 10; - ]]> - - - ``` - - - -**** - - - -### 约束 - -#### DTD - -##### DTD定义 - -DTD是文档类型定义(Document Type Definition)。DTD 可以定义在 XML 文档中出现的元素、这些元素出现的次序、它们如何相互嵌套以及XML文档结构的其它详细信息。 - -##### DTD规则 - -* 约束元素的嵌套层级 - - ```dtd - - ``` - -* 约束元素体里面的数据 - -* 语法 - - ```dtd - - ``` - -* 判断元素 - 简单元素:没有子元素。 - 复杂元素:有子元素的元素; - - * 标签类型 - - | 标签类型 | 代码写法 | 说明 | - | -------- | --------- | -------------------- | - | PCDATA | (#PCDATA) | 被解释的字符串数据 | - | EMPTY | EMPTY | 即空元素,例如\
| - | ANY | ANY | 即任意类型 | - - * 代码 - - ```dtd - - - - - ``` - - * 数量词 - - | 数量词符号 | 含义 | - | ---------- | ---------------------------- | - | 空 | 表示元素出现一次 | - | * | 表示元素可以出现0到多个 | - | + | 表示元素可以出现至少1个 | - | ? | 表示元素可以是0或1个 | - | , | 表示元素需要按照顺序显示 | - | \| | 表示元素需要选择其中的某一个 | - - - -* 属性声明 - - * 语法 - - ```dtd - - ``` - - * 属性类型 - - | 属性类型 | 含义 | - | ---------- | ------------------------------------------------------------ | - | CDATA | 代表属性是文本字符串, eg: | - | ID | 代码该属性值唯一,不能以数字开头, eg: | - | ENUMERATED | 代表属性值在指定范围内进行枚举 Eg: "社科类"是默认值,属性如果不设置默认值就是"社科类" | - - * 属性说明 - - | 属性说明 | 含义 | - | --------- | ----------------------------------------------------------- | - | #REQUIRED | 代表属性是必须有的 | - | #IMPLIED | 代表属性可有可无 | - | #FIXED | 代表属性为固定值,实现方式:book_info CDATA #FIXED "固定值" | - - * 代码 + //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");//程序用时 - ```dtd - - id ID #REQUIRED - 编号 CDATA #IMPLIED - 出版社 (清华|北大|传智播客) "传智播客" - type CDATA #FIXED "IT" - > - - ``` - - + 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); + } +} +``` -##### DTD引入 +*** -* 引入本地dtd - ```dtd - - ``` -* 在xml文件内部引入 +### BigDecimal - ```dtd - - ``` +Java在java.math包中提供的API类BigDecimal,用来对超过16位有效位的数进行精确的运算 -* 引入网络dtd +构造方法: + `public static BigDecimal valueOf(double val)` : 包装浮点数成为大数据对象。 + `public BigDecimal(double val)` : + `public BigDecimal(String val)` : - ```dtd - - ``` +常用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,精确几位,舍入模式)` : 除法 -```dtd - - - - - +```java +public class BigDecimalDemo { + public static void main(String[] args) { + // 浮点型运算的时候直接+ - * / 可能会出现数据失真(精度问题)。 + System.out.println(0.1 + 0.2); + System.out.println(1.301 / 100); + + double a = 0.1 ; + double b = 0.2 ; + double c = a + b ; + System.out.println(c);//0.30000000000000004 + + // 1.把浮点数转换成大数据对象运算 + BigDecimal a1 = BigDecimal.valueOf(a); + BigDecimal b1 = BigDecimal.valueOf(b); + BigDecimal c1 = a1.add(b1);//a1.divide(b1);也可以 + System.out.println(c1); + + // BigDecimal只是解决精度问题的手段,double数据才是我们的目的!! + double d = c1.doubleValue(); + } +} ``` -```xml - - - - - 张三 - 23 - +总结 - +1. BigDecimal 是用来进行精确计算的 +2. 创建 BigDecimal 的对象,构造方法使用参数类型为字符串的。 +3. 四则运算中的除法,如果除不尽请使用 divide 的三个参数的方法。 + +```java +BigDecimal divide = bd1.divide(参与运算的对象,小数点后精确到多少位,舍入模式); +参数1:表示参与运算的BigDecimal 对象。 +参数2:表示小数点后面精确到多少位 +参数3:舍入模式 + BigDecimal.ROUND_UP 进一法 + BigDecimal.ROUND_FLOOR 去尾法 + BigDecimal.ROUND_HALF_UP 四舍五入 ``` -```xml-dtd - - - - - - - ]> - - - 张三 - 23 - - -``` -```dtd - - +*** - - - 张三 - 23 - - -``` +### Regex -##### DTD实现 +#### 概述 -persondtd.dtd文件 +正则表达式的作用:是一些特殊字符组成的校验规则,可以校验信息的正确性,校验邮箱、电话号码、金额等。 -```dtd - - - - - +比如检验qq号: + +```java +public static boolean checkQQRegex(String qq){ + return qq!=null && qq.matches("\\d{4,}");//即是数字 必须大于4位数 +}// 用\\d 是因为\用来告诉它是一个校验类,不是普通的字符 比如 \t \n ``` -```xml-dtd - - +java.util.regex 包主要包括以下三个类: - - - 张三 - 23 - +- Pattern 类: - - 张三 - 23 - - -``` + pattern 对象是一个正则表达式的编译表示。Pattern 类没有公共构造方法,要创建一个 Pattern 对象,必须首先调用其公共静态编译方法,返回一个 Pattern 对象。该方法接受一个正则表达式作为它的第一个参数 +- Matcher 类: + Matcher 对象是对输入字符串进行解释和匹配操作的引擎。与Pattern 类一样,Matcher 也没有公共构造方法,需要调用 Pattern 对象的 matcher 方法来获得一个 Matcher 对象 -*** +- PatternSyntaxException: + PatternSyntaxException 是一个非强制异常类,它表示一个正则表达式模式中的语法错误。 -#### Schema -##### 定义 +*** -1.Schema 语言也可作为 XSD(XML Schema Definition) -2.schema约束文件本身也是一个xml文件,符合xml的语法,这个文件的后缀名.xsd -3.一个xml中可以引用多个schema约束文件,多个schema使用名称空间区分(名称空间类似于java包名) -4.dtd里面元素类型的取值比较单一常见的是PCDATA类型,但是在schema里面可以支持很多个数据类型 -**5.Schema文件约束xml文件的同时也被别的文件约束着** +#### 字符匹配 -##### 规则 +##### 普通字符 -1、创建一个文件,这个文件的后缀名为.xsd。 -2、定义文档声明 -3、schema文件的根标签为: -4、在中定义属性: - xmlns=http://www.w3.org/2001/XMLSchema - 代表当前文件时约束别人的,同时这个文件也对该Schema进行约束 -5、在中定义属性 : - targetNamespace = 唯一的url地址,指定当前这个schema文件的名称空间。 - **名称空间**:当其他xml使用该schema文件,需要引入此空间 -6、在中定义属性 : - elementFormDefault="qualified“,表示当前schema文件是一个质量良好的文件。 -7、通过element定义元素 -8、**判断当前元素是简单元素还是复杂元素** +字母、数字、汉字、下划线、以及没有特殊定义的标点符号,都是“普通字符”。表达式中的普通字符,在匹配一个字符串的时候,匹配与之相同的一个字符。其他统称**元字符** -person.xsd -```scheme - - - targetNamespace="http://www.seazean.cn/javase" - elementFormDefault="qualified" -> - - - - - - - - - - - - - - - - - -``` +##### 特殊字符 + +\r\n 是Windows中的文本行结束标签,在Unix/Linux则是 \n +| 元字符 | 说明 | +| ------ | ------------------------------------------------------------ | +| \ | 将下一个字符标记为一个特殊字符或原义字符,告诉它是一个校验类,不是普通字符 | +| \f | 换页符 | +| \n | 换行符 | +| \r | 回车符 | +| \t | 制表符 | +| \\ | 代表\本身 | +| () | 使用( )定义一个子表达式。子表达式的内容可以当成一个独立元素 | -##### 引入 -1、在根标签上定义属性xmlns="http://www.w3.org/2001/XMLSchema-instance" -2、**通过xmlns引入约束文件的名称空间** -3、给某一个xmlns属性添加一个标识,用于区分不同的名称空间 - 格式为: xmlns:标识=“名称空间url” ,标识可以是任意的,但是一般取值都是xsi -4、通过xsi:schemaLocation指定名称空间所对应的约束文件路径 - 格式为: xsi:schemaLocation = "名称空间url 文件路径“ +##### 标准字符 -```scheme - - - xmlns="http://www.seazean.cn/javase" - xsi:schemaLocation="http://www.seazean.cn/javase person.xsd" -> +标准字符集合 +能够与”多种字符“匹配的表达式。注意区分大小写,大写是相反的意思。只能校验**"单"**个字符。 - - 张三 - 23 - +| 元字符 | 说明 | +| ------ | ------------------------------------------------------------ | +| . | 匹配任意一个字符(除了换行符),如果要匹配包括“\n”在内的所有字符,一般用[\s\S] | +| \d | 数字字符,0~9 中的任意一个,等价于 [0-9] | +| \D | 非数字字符,等价于 [ ^0-9] | +| \w | 大小写字母或数字或下划线,等价于[a-zA-Z_0-9_] | +| \W | 对\w取非,等价于[ ^\w] | +| \s | 空格、制表符、换行符等空白字符的其中任意一个,等价于[\f\n\r\t\v] | +| \S | 对 \s 取非 | - -``` +\x 匹配十六进制字符,\0 匹配八进制,例如 \xA 对应值为 10 的 ASCII 字符 ,即 \n -##### Sc属性 +##### 自定义符 -```scheme - - +自定义符号集合,[ ]方括号匹配方式,能够匹配方括号中**任意一个**字符 + +| 元字符 | 说明 | +| ------------ | ----------------------------------------- | +| [ab5@] | 匹配 "a" 或 "b" 或 "5" 或 "@" | +| [^abc] | 匹配 "a","b","c" 之外的任意一个字符 | +| [f-k] | 匹配 "f"~"k" 之间的任意一个字母 | +| [^A-F0-3] | 匹配 "A","F","0"~"3" 之外的任意一个字符 | +| [a-d[m-p]] | 匹配 a 到 d 或者 m 到 p:[a-dm-p](并集) | +| [a-z&&[m-p]] | 匹配 a 到 z 并且 m 到 p:[a-dm-p](交集) | +| [^] | 取反 | + +* 正则表达式的特殊符号,被包含到中括号中,则失去特殊意义,除了^,-之外,需要在前面加 \ - - - - - - - - - - - - - - - - - - - - - - - - +* 标准字符集合,除小数点外,如果被包含于中括号,自定义字符集合将包含该集合。 + 比如:[\d. \ -+]将匹配:数字、小数点、+、- - - - - 张三 - 23 - - -``` +##### 量词字符 +修饰匹配次数的特殊符号。 -*** +* 匹配次数中的贪婪模式(匹配字符越多越好,默认!),\* 和 + 都是贪婪型元字符。 +* 匹配次数中的非贪婪模式(匹配字符越少越好,修饰匹配次数的特殊符号后再加上一个 "?" 号) +| 元字符 | 说明 | +| ------ | -------------------------------- | +| X? | X一次或一次也没,有相当于 {0,1} | +| X* | X不出现或出现任意次,相当于 {0,} | +| X+ | X至少一次,相当于 {1,} | +| X{n} | X恰好 n 次 | +| {n,} | X至少 n 次 | +| {n,m} | X至少 n 次,但是不超过 m 次 | -### Dom4J -#### 解析 +*** -* 概述:xml解析就是从xml中获取到数据。DOM是解析思想。 -* DOM(Document Object Model)文档对象模型:就是把文档的各个组成部分看做成对应的对象。 - 会把xml文件全部加载到内存,在内存中形成一个树形结构,再获取对应的值 -* 工具:dom4j属于第三方技术,必须导入该框架 - https://dom4j.github.io/ 去下载dom4j,在idea中当前模块下新建一个lib文件夹,将jar包复制到文件夹中 - 选中jar包 -> 右键 -> 选择add as library即可 +#### 位置匹配 -* dom4j实现 - * dom4j解析器构造方法:`SAXReader saxReader = new SAXReader();` - * 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 - `public InputStream getResourceAsStream(String path)`:加载文件成为一个字节输入流返回 +##### 字符边界 +本组标记匹配的不是字符而是位置,符合某种条件的位置 +| 元字符 | 说明 | +| ------ | ------------------------------------------------------------ | +| ^ | 与字符串开始的地方匹配(在字符集合中用来求非,在字符集合外用作匹配字符串的开头) | +| $ | 与字符串结束的地方匹配 | +| \b | 匹配一个单词边界 | -#### 解析根元素 -Document方法: - Element getRootElement():获取根元素。 -```java -// 需求:解析books.xml文件成为一个Document文档树对象,得到根元素对象。 -public class Dom4JDemo { - public static void main(String[] args) throws Exception { - // 1.创建一个dom4j的解析器对象:代表整个dom4j框架。 - SAXReader saxReader = new SAXReader(); - // 2.第一种方式(简单):通过解析器对象去加载xml文件数据,成为一个Document文档树对象。 - //Document document = saxReader.read(new File("Day13Demo/src/books.xml")); - - // 3.第二种方式(代码多点)先把xml文件读成一个字节输入流 - // 这里的“/”是直接去src类路径下寻找文件。 - InputStream is = Dom4JDemo01.class.getResourceAsStream("/books.xml"); - Document document = saxReader.read(is); - System.out.println(document); - //org.dom4j.tree.DefaultDocument@27a5f880 [Document: name null] - // 4.从document文档树对象中提取根元素对象 - Element root = document.getRootElement(); - System.out.println(root.getName());//books - } -} -``` +##### 捕获组 -```xml - - - - JavaWeb开发教程 - 张孝祥 - 100.00元 - - - 三国演义 - 罗贯中 - 100.00元 - - - - +捕获组是把多个字符当一个单独单元进行处理的方法,它通过对括号内的字符分组来创建。 -``` +在表达式( ( A ) ( B ( C ) ) ),有四个这样的组:((A)(B(C)))、(A)、(B(C))、(C)(按照括号从左到右为 group(1)) +* 调用 matcher 对象的groupCount 方法返回一个 int值,表示matcher对象当前有多个捕获组。 +* 特殊的组group(0)、group(),它代表整个表达式,该组不包括在 groupCount 的返回值中。 +| 表达式 | 说明 | +| ------------------------- | ------------------------------------------------------------ | +| \| (分支结构) | 左右两边表达式之间 "或" 关系,匹配左边或者右边 | +| () (捕获组) | (1) 在被修饰匹配次数的时候,括号中的表达式可以作为整体被修饰
(2) 取匹配结果的时候,括号中的表达式匹配到的内容可以被单独得到
(3) 每一对括号分配一个编号,()的捕获根据左括号的顺序从1开始自动编号。捕获元素编号为零的第一个捕获是由整个正则表达式模式匹配的文本 | +| (?:Expression) 非捕获组 | 一些表达式中,不得不使用( ),但又不需要保存( )中子表达式匹配的内容,这时可以用非捕获组来抵消使用( )带来的副作用。 | -#### 解析子元素 -Element元素的API: - String getName() : 取元素的名称。 - List elements() : 获取当前元素下的全部子元素(一级) - List elements(String name) : 获取当前元素下的指定名称的全部子元素(一级) - Element element(String name) : 获取当前元素下的指定名称的某个子元素,默认取第一个(一级) -```java -public class Dom4JDemo { - public static void main(String[] args) throws Exception { - SAXReader saxReader = new SAXReader(); - Document document = saxReader.read(new File("Day13Demo/src/books.xml")); - // 3.获取根元素对象 - Element root = document.getRootElement(); - System.out.println(root.getName()); +##### 反向引用 - // 4.获取根元素下的全部子元素 - List sonElements = root.elements(); - for (Element sonElement : sonElements) { - System.out.println(sonElement.getName()); - } - // 5.获取根源下的全部book子元素 - List sonElements1 = root.elements("book"); - for (Element sonElement : sonElements1) { - System.out.println(sonElement.getName()); - } - - // 6.获取根源下的指定的某个元素 - Element son = root.element("user"); - System.out.println(son.getName()); - // 默认会提取第一个名称一样的子元素对象返回! - Element son1 = root.element("book"); - System.out.println(son1.attributeValue("id")); - } -} +反向引用(\number),又叫回溯引用: -``` +* 每一对()会分配一个编号,使用 () 的捕获根据左括号的顺序从1开始自动编号 +* 通过反向引用,可以对分组已捕获的字符串进行引用,继续匹配 +* **把匹配到的字符重复一遍在进行匹配** -#### 解析属性 +* 应用1: -Element元素的API: - List attributes() : 获取元素的全部属性对象。 - Attribute attribute(String name) : 根据名称获取某个元素的属性对象。 - String attributeValue(String var) : 直接获取某个元素的某个属性名称的值。 + ```java + String regex = "((\d)3)\1[0-9](\w)\2{2}"; + ``` -Attribute对象的API: - String getName() : 获取属性名称。 - String getValue() : 获取属性值。 + * 首先匹配((\d)3),其次\1匹配((\d)3)已经匹配到的内容,\2匹配(\d), {2}指的是\2的值出现两次 + * 实例:23238n22(匹配到2未来就继续匹配2) + * 实例:43438n44 -```java -public class Dom4JDemo { - public static void main(String[] args) throws Exception { - SAXReader saxReader = new SAXReader(); - Document document = saxReader.read(new File("Day13Demo/src/books.xml")); - Element root = document.getRootElement(); - // 4.获取book子元素 - Element bookEle = root.element("book"); +* 应用2:爬虫 - // 5.获取book元素的全部属性对象 - List attributes = bookEle.attributes(); - for (Attribute attribute : attributes) { - System.out.println(attribute.getName()+"->"+attribute.getValue()); - } + ```java + String regex = "<(h[1-6])>\w*?<\/\1>"; + ``` - // 6.获取Book元素的某个属性对象 - Attribute descAttr = bookEle.attribute("desc"); - System.out.println(descAttr.getName()+"->"+descAttr.getValue()); + 匹配结果 - // 7.可以直接获取元素的属性值 - System.out.println(bookEle.attributeValue("id")); - System.out.println(bookEle.attributeValue("desc")); - } -} -``` + ```java +

x

//匹配 +

x

//匹配 +

x

//不匹配 + ``` + -#### 解析文本 -Element: - String elementText(String name) : 可以直接获取当前元素的子元素的文本内容 - String elementTextTrim(String name) : 去前后空格,直接获取当前元素的子元素的文本内容 - String getText() : 直接获取当前元素的文本内容。 - String getTextTrim() : 去前后空格,直接获取当前元素的文本内容。 -```java -public class Dom4JDemo { - public static void main(String[] args) throws Exception { - SAXReader saxReader = new SAXReader(); - Document document = saxReader.read(new File("Day13Demo/src/books.xml")); - Element root = document.getRootElement(); - // 4.得到第一个子元素book - Element bookEle = root.element("book"); - // 5.直接拿到当前book元素下的子元素文本值 - System.out.println(bookEle.elementText("name")); - System.out.println(bookEle.elementTextTrim("name")); // 去前后空格 - System.out.println(bookEle.elementText("author")); - System.out.println(bookEle.elementTextTrim("author")); // 去前后空格 - System.out.println(bookEle.elementText("sale")); - System.out.println(bookEle.elementTextTrim("sale")); // 去前后空格 +##### 零宽断言 - // 6.先获取到子元素对象,再获取该文本值 - Element bookNameEle = bookEle.element("name"); - System.out.println(bookNameEle.getText()); - System.out.println(bookNameEle.getTextTrim());// 去前后空格 - } -} -``` +预搜索(零宽断言)(环视) +* 只进行子表达式的匹配,匹配内容不计入最终的匹配结果,是零宽度 +* 判断当前位置的前后字符,是否符合指定的条件,但不匹配前后的字符。**是对位置的匹配**。 -**** +* 正则表达式匹配过程中,如果子表达式匹配到的是字符内容,而非位置,并被保存到最终的匹配结果中,那么就认为这个子表达式是占有字符的;如果子表达式匹配的仅仅是位置,或者匹配的内容并不保存到最终的匹配结果中,那么就认为这个子表达式是**零宽度**的。占有字符还是零宽度,是针对匹配的内容是否保存到最终的匹配结果中而言的 + + | 表达式 | 说明 | + | -------- | --------------------------------------- | + | (?=exp) | 断言自身出现的位置的后面能匹配表达式exp | + | (?<=exp) | 断言自身出现的位置的前面能匹配表达式exp | + | (?!exp) | 断言此位置的后面不能匹配表达式exp | + | (? List +*** -```java -public class Dom4JDemo { - public static void main(String[] args) throws Exception { - SAXReader saxReader = new SAXReader(); - Document document = saxReader.read(new File("Day13Demo/src/Contacts.xml")); - Element root = docment.getRootElement(); - // 4.获取根元素下的全部子元素 - List sonElements = root.elements(); - // 5.遍历子元素 封装成List集合对象 - List contactList = new ArrayList<>(); - if(sonElements != null && sonElements.size() > 0) { - for (Element sonElement : sonElements) { - Contact c = new Contact(); - c.setID(Integer.valueOf(sonElement.attributeValue("id"))); - contact.setVip(Boolean.valueOf(sonElement.attributeValue("vip"))); - contact.setName(sonElement.elementText("name")); - contact.setSex(sonElement.elementText("gender").charAt(0)); - contact.setEmail(sonElement.elementText("email")); - contactList.add(contact); - } - } - System.out.println(contactList); - } -} -public class Contact { - private int id ; - private boolean vip; - private String name ; - private char sex ; - private String email ; - //构造器 -} -``` -```xml - - - - 潘金莲 - - panpan@seazean.cn - - - 武松 - - wusong@seazean.cn - - - 武大狼 - - wuda@seazean.cn - - -``` +#### 匹配模式 +正则表达式的匹配模式: -**** +* IGNORECASE 忽略大小写模式 + * 匹配时忽略大小写。 + * 默认情况下,正则表达式是要区分大小写的。 +* SINGLELINE 单行模式 + * 整个文本看作一个字符串,只有一个开头,一个结尾。 + * 使小数点 "." 可以匹配包含换行符(\n)在内的任意字符。 +* MULTILINE 多行模式 + * 每行都是一个字符串,都有开头和结尾。 + * 在指定了 MULTILINE 之后,如果需要仅匹配字符串开始和结束位置,可以使用 \A 和 \Z -### XPath +*** -Dom4J 可以用于解析整个XML的数据。但是如果要检索XML中的某些信息,建议使用XPath -XPath常用API: -* List selectNodes(String var1) : 检索出一批节点集合 -* Node selectSingleNode(String var1) : 检索出一个节点返回 +#### 分组匹配 -XPath提供的四种检索数据的写法: +Pattern类: + `static Pattern compile(String regex)` : 将给定的正则表达式编译为模式 + `Matcher matcher(CharSequence input)` : 创建一个匹配器,匹配给定的输入与此模式 + `static boolean matches(String regex, CharSequence input)` : 编译正则表达式,并匹配输入 -1. 绝对路径:/根元素/子元素/子元素。 -2. 相对路径:./子元素/子元素。 (.代表了当前元素) -3. 全文搜索: - * //元素 在全文找这个元素 - * //元素1/元素2 在全文找元素1下面的一级元素2 - * //元素1//元素2 在全文找元素1下面的全部元素2 -4. 属性查找。 - * //@属性名称 在全文检索属性对象。 - * //元素[@属性名称] 在全文检索包含该属性的元素对象。 - * //元素[@属性名称=值] 在全文检索包含该属性的元素且属性值为该值的元素对象。 +Matcher类: + `boolean find()` : 扫描输入的序列,查找与该模式匹配的下一个子序列 + `String group()` : 返回与上一个匹配的输入子序列。同group(0),匹配整个表达式的子字符串 + `String group(int group)` : 返回在上一次匹配操作期间由给定组捕获的输入子序列 + `int groupCount()` : 返回此匹配器模式中捕获组的数量 ```java -public class XPathDemo { - public static void main(String[] args) throws Exception { - SAXReader saxReader = new SAXReader(); - InputStream is = XPathDemo.class.getResourceAsStream("/Contact.xml"); - Document document = saxReader.read(is); - //1.使用绝对路径定位全部的name名称 - List nameNodes1 = document.selectNodes("/contactList/contact/name"); - for (Node nameNode : nameNodes) { - System.out.println(nameNode.getText()); - } - - //2.相对路径。从根元素开始检索,.代表很根元素 - List nameNodes2 = root.selectNodes("./contact/name"); - - //3.1 在全文中检索name节点 - List nameNodes3 = root.selectNodes("//name");//全部的 - //3.2 在全文中检索所有contact下的所有name节点 //包括sql,不外面的 - List nameNodes3 = root.selectNodes("//contact//name"); - //3.3 在全文中检索所有contact下的直接name节点 - List nameNodes3 = root.selectNodes("//contact/name");//不包括sql和外面 - - //4.1 检索全部属性对象 - List attributes1 = root.selectNodes("//@id");//包括sql4 - //4.2 在全文检索包含该属性的元素对象 - List attributes1 = root.selectNodes("//contact[@id]"); - //4.3 在全文检索包含该属性的元素且属性值为该值的元素对象 - Node nodeEle = document.selectSingleNode("//contact[@id=2]"); - Element ele = (Element)nodeEle; - System.out.println(ele.elementTextTrim("name"));//武松 - } +public class Demo01{ + public static void main(String[] args) { + //表达式对象 + Pattern p = Pattern.compile("\\w+"); + //创建Matcher对象 + Matcher m = p.matcher("asfsdf2&&3323"); + //boolean b = m.matches();//尝试将整个字符序列与该模式匹配 + //System.out.println(b);//false + //boolean b2 = m.find();//该方法扫描输入的序列,查找与该模式匹配的下一个子序列 + //System.out.println(b2);//true + + //System.out.println(m.find()); + //System.out.println(m.group());//asfsdf2 + //System.out.println(m.find()); + //System.out.println(m.group());//3323 + + while(m.find()){ + System.out.println(m.group()); //group(),group(0)匹配整个表达式的子字符串 + System.out.println(m.group(0)); + } + + } } ``` -```xml - - - - 潘金莲 - - panpan@seazean.cn - - - 武松 - - wusong@seazean.cn - - sql语句 - - - - 武大狼 - - wuda@seazean.cn - - -外面的名称 - +```java +public class Demo02 { + public static void main(String[] args) { + //在这个字符串:asfsdf23323,是否符合指定的正则表达式:\w+ + //表达式对象 + Pattern p = Pattern.compile("(([a-z]+)([0-9]+))");//不需要加多余的括号 + //创建Matcher对象 + Matcher m = p.matcher("aa232**ssd445"); + + while(m.find()){ + System.out.println(m.group());//aa232 ssd445 + System.out.println(m.group(1));//aa232 ssd445 + System.out.println(m.group(2));//aa ssd + System.out.println(m.group(3));//232 445 + } + } +} ``` +* 正则表达式改为`"(([a-z]+)(?:[0-9]+))"` 没有group(3) 因为是非捕获组 +* 正则表达式改为`"([a-z]+)([0-9]+)"` 没有 group(3) aa232 - aa --232 +*** -*** - +#### 应用 +##### 基本验证 +```java +public static void main(String[] args){ + System.out.println("a".matches("[abc]"));//true判断a是否在abc + System.out.println("a".matches("[^abc]"));//false 判断a是否在abc之外的 + System.out.println("a".matches("\\d")); //false 是否a是整数 + System.out.println("a".matches("\\w")); //true 是否是字符 + System.out.println("你".matches("\\w")); // false + System.out.println("aa".matches("\\w"));//false 只能检验单个字符 + + // 密码 必须是数字 字母 下划线 至少 6位 + System.out.println("ssds3c".matches("\\w{6,}")); // true + // 验证。必须是数字和字符 必须是4位 + System.out.println("dsd22".matches("[a-zA-Z0-9]{4}")); // false + System.out.println("A3dy".matches("[a-zA-Z0-9]{4}")); // true +} +``` -# JVM -## JVM概述 +##### 验证号码 -### 基本介绍 +```java +//1开头 第二位是2-9的数字 +public static void checkPhone(String phone){ + if(phone.matches("1[3-9]\\d{9}")){ + System.out.println("手机号码格式正确!"); + } else {.......} +} +//1111@qq.com zhy@pic.com.cn +public static void checkEmail(String email){ + if(email.matches("\\w{1,}@\\w{1,}(\\.\\w{2,5}){1,2}")){ + System.out.println("邮箱格式正确!"); + }// .是任意字符 \\.就是点 +} +``` -JVM:全称 Java Virtual Machine,即 Java 虚拟机,一种规范,本身是一个虚拟计算机,直接和操作系统进行交互,与硬件不直接交互,而操作系统可以帮我们完成和硬件进行交互的工作 -特点: -* Java 虚拟机基于**二进制字节码**执行,由一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆、一个方法区等组成 -* JVM 屏蔽了与操作系统平台相关的信息,从而能够让 Java 程序只需要生成能够在 JVM 上运行的字节码文件,通过该机制实现的**跨平台性** +##### 查找替换 -Java代码执行流程:java程序 --(编译)--> 字节码文件 --(解释执行)--> 操作系统(Win,Linux) +* `public String[] split(String regex)`:按照正则表达式匹配的内容进行分割字符串,反回一个字符串数组 +* `public String replaceAll(String regex,String newStr)`:按照正则表达式匹配的内容进行替换 -JVM结构: +```java +//数组分割 +public static void main(String[] args) { + // 1.split的基础用法 + String names = "贾乃亮,王宝强,陈羽凡"; + // 以“,”分割成字符串数组 + String[] nameArrs = names.split(","); - + // 2.split集合正则表达式做分割 + String names1 = "贾乃亮lv434fda324王宝强87632fad2342423陈羽凡"; + // 以匹配正则表达式的内容为分割点分割成字符串数组 + String[] nameArrs1 = names1.split("\\w+"); + + // 使用正则表达式定位出内容,替换成/ + System.out.println(names1.replaceAll("\\w+","/"));//贾乃亮/王宝强/羽凡 -JVM、JRE、JDK对比: + String names3 = "贾乃亮,王宝强,羽凡"; + System.out.println(names3.replaceAll(",","-"));//贾乃亮-王宝强-羽凡 +} +``` - -*** +##### 面试问题 +找出所有189和132开头的手机号 -### 架构模型 +```java +public class RegexDemo { + public static void main(String[] args) { + String rs = "189asjk65as1891898777745gkkkk189745612318936457894"; + String regex = "(?=((189|132)\\d{8}))"; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(rs); + while (matcher.find()) { + System.out.println(matcher.group(1)); + } + } +} +``` -Java编译器输入的指令流是一种基于栈的指令集架构。因为跨平台的设计,java的指令都是根据栈来设计的,不同平台CPU架构不同,所以不能设计为基于寄存器架构 -* 基于栈式架构的特点: - * 设计和实现简单,适用于资源受限的系统 - * 使用零地址指令方式分配,执行过程依赖操作栈,指令集更小,编译器容易实现 - * 零地址指令:机器指令的一种,是指令系统中的一种不设地址字段的指令,只有操作码而没有地址码。这种指令有两种情况:一是无需操作数,另一种是操作数为默认的(隐含的),默认为操作数在寄存器(ACC)中,指令可直接访问寄存器 - * 一地址指令:一个操作码对应一个地址码,通过地址码寻找操作数 - * 不需要硬件的支持,可移植性更好,更好实现跨平台 -* 基于寄存器架构的特点: - * 需要硬件的支持,可移植性差 - * 性能更好,执行更高效,寄存器比内存快 - * 以一地址指令、二地址指令、三地址指令为主 @@ -13332,156 +3616,207 @@ Java编译器输入的指令流是一种基于栈的指令集架构。因为跨 -### 生命周期 +## 集合 -JVM的生命周期分为三个阶段,分别为:启动、运行、死亡。 +### 集合概述 -- **启动**:当启动一个 Java 程序时,通过引导类加载器(bootstrap class loader)创建一个初始类(initial class),对于拥有 main 函数的类就是 JVM 实例运行的起点 -- **运行**: - - main() 方法是一个程序的初始起点,任何线程均可由在此处启动 - - 在 JVM 内部有两种线程类型,分别为:用户线程和守护线程,**JVM 使用的是守护线程,main() 和其他线程使用的是用户线程**,守护线程会随着用户线程的结束而结束 - - 执行一个 Java 程序时,真真正正在执行的是一个 Java 虚拟机的进程 - - JVM 有两种运行模式 Server 与 Client,两种模式的区别在于:Client 模式启动速度较快,Server 模式启动较慢;但是启动进入稳定期长期运行之后 Server 模式的程序运行速度比 Client 要快很多 -- **死亡**: - - 当程序中的用户线程都中止,JVM 才会退出 - - 程序正常执行结束、程序异常或错误而异常终止、操作系统错误导致终止 - - 线程调用 Runtime 类 halt 方法或 System 类 exit 方法,并且 java 安全管理器允许这次 exit 或 halt 操作 +集合是一个大小可变的容器,容器中的每个数据称为一个元素 +集合特点:类型可以不确定,大小不固定;集合有很多,不同的集合特点和使用场景不同 +数组:类型和长度一旦定义出来就都固定 +作用: -*** +* 在开发中,很多时候元素的个数是不确定的 +* 而且经常要进行元素的增删该查操作,集合都是非常合适的,开发中集合用的更多 -### 相关参数 +*** -进入 Run/Debug Configurations ---> VM options 设置参数 -| 参数 | 功能 | -| ------------------------------------------------------------ | ------------------------------------------------------------ | -| -Xms | 堆初始大小(默认为物理内存的1/64) | -| -Xmx 或 -XX:MaxHeapSize=size | 堆最大大小(默认为物理内存的1/4) | -| -Xmn 或 -XX:NewSize=size + -XX:MaxNewSize=size | 新生代大小(初始值及最大值) | -| -XX:NewRatio | 新生代与老年代在堆结构的占比 | -| -XX:SurvivorRatio=ratio | 幸存区比例(Eden和S0/S1空间的比例) | -| -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy | 幸存区比例(动态) | -| -XX:MaxTenuringThreshold=threshold | 晋升阈值 | -| -XX:+PrintTenuringDistribution | 晋升详情 | -| -XX:+PrintFlagsInitial | 查看所有的参数的默认初始值 | -| -XX:+PrintFlagsFinal | 查看所有的参数的最终值
(可能会存在修改,不再是初始值) | -| -XX:+PrintGCDetails | GC详情,打印gc简要信息:
1. -XX:+PrintGC 2. - verbose:gc | -| -XX:+ScavengeBeforeFullGC | FullGC 前 MinorGC | -| -XX:+DisableExplicitGC | 禁用显式垃圾回收,让System.gc无效 | -说明:参数前面是`+`号说明是开启,如果是`- `号说明是关闭 +### 存储结构 +数据结构指的是数据以什么方式组织在一起,不同的数据结构,增删查的性能是不一样的 +数据存储的常用结构有:栈、队列、数组、链表和红黑树 +* 队列(queue):先进先出,后进后出。(FIFO first in first out) + 场景:各种排队、叫号系统,有很多集合可以实现队列 +* 栈(stack):后进先出,先进后出 (LIFO) + 压栈 == 入栈、弹栈 == 出栈 + 场景:手枪的弹夹 -*** +* 数组:数组是内存中的连续存储区域,分成若干等分的小区域(每个区域大小是一样的)。元素存在索引 + 特点:**查询元素快**(根据索引快速计算出元素的地址,然后立即去定位) + **增删元素慢**(创建新数组,迁移元素) +* 链表:元素不是内存中的连续区域存储,元素是游离存储的,每个元素会记录下个元素的地址 + 特点:**查询元素慢,增删元素快**(针对于首尾元素,速度极快,一般是双链表) +* 树 -## 内存结构 + * 二叉树:binary tree 永远只有一个根节点,是每个结点不超过2个节点的树(tree) + 特点:二叉排序树:小的左边,大的右边,但是可能树很高,性能变差 + 为了做排序和搜索会进行左旋和右旋实现平衡查找二叉树,让树的高度差不大于1 -### 内存概述 + * 红黑树(基于红黑规则实现自平衡的排序二叉树):树保证到了很矮小,但是又排好序,性能最高的树 -内存结构是JVM中非常重要的一部分,是非常重要的系统资源,是硬盘和 CPU 的中间仓库及桥梁,承载着操作系统和应用程序的实时运行,又叫运行时数据区 + 特点:**红黑树的增删查改性能都好** -JVM 内存结构规定了 Java 在运行过程中内存申请、分配、管理的策略,保证了 JVM 的高效稳定运行 +各数据结构时间复杂度对比: -* Java1.8以前的内存结构图: - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java7内存结构图.png) +![](https://gitee.com/seazean/images/raw/master/Java/数据结构的复杂度对比.png) -* Java1.8之后的内存结果图: - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java8内存结构图.png) -线程运行诊断: +图片来源:https://www.bigocheatsheet.com/ -* 定位:jps定位进程id -* jstack 进程id:用于打印出给定的java进程ID或core file或远程调试服务的Java堆栈信息 -常见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 +*** -*** +### Collection +#### 概述 +> Java中集合的代表是:Collection. +> Collection集合是Java中集合的祖宗类。 -### JVM内存 +Collection集合底层为数组:`[value1, value2, ....]` -#### 虚拟机栈 +```java +Collection集合的体系: + Collection(接口) + / \ + Set(接口) List(接口) + / \ / \ + HashSet(实现类) TreeSet<>(实现类) ArrayList(实现类) LinekdList<>(实现类) + / +LinkedHashSet<>(实现类) +``` -##### Java栈 +**集合的特点:**(非常重要) + Set系列集合:添加的元素是无序,不重复,无索引的。 + -- HashSet: 添加的元素是无序,不重复,无索引的。 + -- LinkedHashSet: 添加的元素是有序,不重复,无索引的。 + -- TreeSet: 不重复,无索引,按照大小默认升序排序!! + List系列集合:添加的元素是有序,可重复,有索引。 + -- ArrayList:添加的元素是有序,可重复,有索引。 + -- LinekdList:添加的元素是有序,可重复,有索引。 -Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所需要的内存 -* 每个方法被执行时,都会在虚拟机栈中创建一个栈帧 stack frame(**一个方法一个栈帧**) -* java虚拟机规范允许**Java栈的大小是动态的或者是固定不变的** +*** -* 虚拟机栈是**每个线程私有的**,每个线程只能有一个活动栈帧,对应方法调用到执行完成的整个过程 -* 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存,每个栈帧中存储着: - * 局部变量表:存储方法里的java基本数据类型以及对象的引用 - * 动态链接:也叫指向运行时常量池的方法引用 - * 方法返回地址:方法正常退出或者异常退出的定义 - * 操作数栈或表达式栈和其他一些附加信息 - - +#### API -设置栈内存大小:`-Xss size` `-Xss 1024k` +Collection是集合的祖宗类,它的功能是全部集合都可以继承使用的,所以要学习它。 -* 在 JDK 1.4 中默认为 256K,而在 JDK 1.5+ 默认为 1M +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)` : 将指定集合中的所有元素添加到此集合 -* 栈内存**不需要进行GC**,方法开始执行的时候会进栈,方法调用后自动弹栈,相当于清空了数据 +```java +public class CollectionDemo { + public static void main(String[] args) { + Collection sets = new HashSet<>(); + sets.add("MyBatis"); + System.out.println(sets.add("Java"));//true + System.out.println(sets.add("Java"));//false + sets.add("Spring"); + sets.add("MySQL"); + System.out.println(sets)//[]无序的; + System.out.println(sets.contains("java"));//true 存在 + Object[] arrs = sets.toArray(); + System.out.println("数组:"+ Arrays.toString(arrs)); + + Collection c1 = new ArrayList<>(); + c1.add("java"); + Collection c2 = new ArrayList<>(); + c2.add("ee"); + c1.addAll(c2);// c1:[java,ee] c2:[ee]; + } +} +``` -* 栈内存分配越大越大,可用的线程数越少(内存越大,每个线程拥有的内存越大) -* 方法内的局部变量是否**线程安全**: - * 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的 - * 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全 -异常: +*** -* 栈帧过多导致栈内存溢出 (超过了栈的容量),会抛出 OutOfMemoryError 异常 -* 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常 +#### 遍历 -*** +Collection集合的遍历方式有三种: +集合可以直接输出内容,因为底层重写了toString()方法。 +1. 迭代器 + `public Iterator iterator()` : 获取集合对应的迭代器,用来遍历集合中的元素的 + `E next()` : 获取下一个元素值! + `boolean hasNext()` : 判断是否有下一个元素,有返回true ,反之 + `default void remove()` : 从底层集合中删除此迭代器返回的最后一个元素,这种方法只能在每次调用next() 时调用一次 -##### 局部变量 +2. 增强for循环 + 增强for循环是一种遍历形式,可以遍历集合或者数组,遍历集合实际上是迭代器遍历的简化写法 +```java + for(被遍历集合或者数组中元素的类型 变量名称 : 被遍历集合或者数组){ + + } +``` -局部变量表也被称之为局部变量数组或本地变量表,本质上定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量 +缺点:遍历无法知道遍历到了哪个元素了,因为没有索引。 -* 表是建立在线程的栈上,是线程私有的数据,因此不存在数据安全问题 -* 表的容量大小是在编译期确定的,保存在方法的 Code 属性的 maximum local variables 数据项中 -* 表中的变量只在当前方法调用中有效,方法结束栈帧销毁,局部变量表也会随之销毁 -* 表中的变量也是重要的垃圾回收根节点,只要被表中数据直接或间接引用的对象都不会被回收 +3. JDK 1.8开始之后的新技术Lambda表达式 -局部变量表最基本的存储单元是 **slot(变量槽)**: + ```java + public class CollectionDemo { + public static void main(String[] args) { + Collection lists = new ArrayList<>(); + lists.add("aa"); + lists.add("bb"); + lists.add("cc"); + System.out.println(lists); // lists = [aa, bb, cc] + //迭代器流程 + // 1.得到集合的迭代器对象。 + Iterator it = lists.iterator(); + // 2.使用while循环遍历。 + while(it.hasNext()){ + String ele = it.next(); + System.out.println(ele); + } + + //增强for + for (String ele : lists) { + System.out.println(ele); + } + //lambda表达式 + lists.forEach(s -> { + System.out.println(s); + }); + } + } + ``` -* 参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束,JVM为每一个slot都分配一个访问索引,通过索引即可访问到槽中的数据 -* 存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量 -* 32 位以内的类型只占用一个 slot(包括returnAddress类型),64位的类型(long和double)占用两个slot -* 局部变量表中的槽位是可以**重复利用**的,如果一个局部变量过了其作用域,那么之后申明的新的局部变量就可能会复用过期局部变量的槽位,从而达到节省资源的目的 + @@ -13489,129 +3824,210 @@ Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所 -##### 操作数栈 - -栈 :可以使用数组或者链表来实现 +#### List -操作数栈:在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)或出栈(pop) +##### 概述 -* 保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间,是执行引擎的一个工作区 +List集合继承了Collection集合全部的功能。 -* Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈 -* 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中 +List系列集合有索引,所以多了很多按照索引操作元素的功能:for循环遍历(4种遍历) -栈顶缓存技术 ToS(Top-of-Stack Cashing):将栈顶元素全部缓存在 CPU 的寄存器中,以此降低对内存的读/写次数,提升执行的效率 +List系列集合:添加的元素是有序,可重复,有索引。 -基于栈式架构的虚拟机使用的零地址指令更加紧凑,完成一项操作需要使用很多入栈和出栈指令,所以需要更多的指令分派(instruction dispatch)次数和内存读/写次数,由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度,所以需要栈顶缓存技术 +* ArrayList:添加的元素是有序,可重复,有索引。 +* LinekdList:添加的元素是有序,可重复,有索引。 + *** -##### 动态链接 - -动态链接是指向运行时常量池的方法引用,涉及到栈操作已经是类加载完成,这个阶段的解析是动态绑定 +##### ArrayList -* 为了支持当前方法的代码能够实现动态链接,每一个栈帧内部都包含一个指向运行时常量池或该栈帧所属方法的引用 +###### 介绍 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-动态链接符号引用.png) +ArrayList添加的元素,是有序,可重复,有索引的。 +ArrayList实现类集合底层**基于数组存储数据**的,查询快,增删慢! -* 在Java源文件被编译成字节码文件中,所有的变量和方法引用都作为符号引用保存在class的常量池中 - 常量池的作用:提供一些符号和常量,便于指令的识别 +`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。 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-动态链接运行时常量池.png) +```java +public static void main(String[] args){ + List lists = new ArrayList<>();//多态 + lists.add("java1"); + lists.add("java1");//可以重复 + lists.add("java2"); + for(int i = 0 ; i < lists.size() ; i++ ) { + String ele = lists.get(i); + System.out.println(ele); + } +} +``` +![ArrayList源码分析](https://gitee.com/seazean/images/raw/master/Java/ArrayList添加元素源码解析.png) -*** +###### 源码 +ArrayList 是基于数组实现的,所以支持快速随机访问 -##### 返回地址 +```java +public class ArrayList extends AbstractList + implements List, RandomAccess, Cloneable, java.io.Serializable{} +``` -Return Address:存放调用该方法的PC寄存器的值 +- `RandomAccess` 是一个标志接口,表明实现这个这个接口的 List 集合是支持**快速随机访问**的。在 `ArrayList` 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。 +- `ArrayList` 实现了 `Cloneable` 接口 ,即覆盖了函数`clone()`,能被克隆 +- `ArrayList` 实现了 `Serializable `接口,这意味着`ArrayList`支持序列化,能通过序列化去传输 -方法的结束有两种方式:正常执行完成、出现未处理的异常,在方法退出后都返回到该方法被调用的位置 +核心方法: -* 正常:调用者的pc计数器的值作为返回地址,即调用该方法的指令的**下一条指令的地址** -* 异常:返回地址是要通过异常表来确定 +* 构造函数:以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量,即向数组中添加第一个元素时,**数组容量扩为 10** -正常完成出口:执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者 +* 添加元素: -异常完成出口:方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内进行处理,本方法的异常表中没有搜素到匹配的异常处理器,导致方法退出 + ```java + //e 插入的元素 elementData底层数组 size 插入的位置 + public boolean add(E e) { + ensureCapacityInternal(size + 1); // Increments modCount!! + elementData[size++] = e; // 插入size位置,然后加一 + return true; + } + ``` -两者区别:通过异常完成出口退出的不会给上层调用者产生任何的返回值 + 当add 第 1 个元素到 ArrayList,size是0,进入 ensureCapacityInternal 方法, + ```java + private void ensureCapacityInternal(int minCapacity) { + ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); + } + ``` + ```java + private static int calculateCapacity(Object[] elementData, int minCapacity) { + //判断elementData是不是空数组 + if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { + //返回默认值和最小需求容量最大的一个 + return Math.max(DEFAULT_CAPACITY, minCapacity); + } + return minCapacity; + } + ``` -##### 附加信息 + 如果需要的容量大于数组长度,进行扩容: -栈帧中还允许携带与java虚拟机实现相关的一些附加信息。例如,对程序调试提供支持的信息 + ```java + //判断是否需要扩容 + private void ensureExplicitCapacity(int minCapacity) { + modCount++; + if (minCapacity - elementData.length > 0) + //调用grow方法进行扩容,调用此方法代表已经开始扩容了 + grow(minCapacity); + } + ``` + 指定索引插入,在旧数组上操作: + ```java + public void add(int index, E element) { + rangeCheckForAdd(index); + ensureCapacityInternal(size + 1); // Increments modCount!! + System.arraycopy(elementData, index, elementData, index + 1, + size - index); + elementData[index] = element; + size++; + } + ``` -*** +* 扩容:新容量的大小为 `oldCapacity + (oldCapacity >> 1)`,`oldCapacity >> 1` 需要取整,所以新容量大约是旧容量的 1.5 倍左右,即 oldCapacity+oldCapacity/2 + 扩容操作需要调用 `Arrays.copyOf()`(底层 `System.arraycopy()`)把原数组整个复制到**新数组**中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数 + ```java + private void grow(int minCapacity) { + int oldCapacity = elementData.length; + int newCapacity = oldCapacity + (oldCapacity >> 1); + //检查新容量是否大于最小需要容量,若小于最小需要容量,就把最小需要容量当作数组的新容量 + if (newCapacity - minCapacity < 0) + newCapacity = minCapacity;//不需要扩容计算 + //检查新容量是否大于最大数组容量 + if (newCapacity - MAX_ARRAY_SIZE > 0) + //如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE` + //否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8` + newCapacity = hugeCapacity(minCapacity); + elementData = Arrays.copyOf(elementData, newCapacity); + } + ``` -#### 本地方法栈 + MAX_ARRAY_SIZE:要分配的数组的最大大小,分配更大的**可能**会导致 -本地方法栈是为虚拟机**执行本地方法时提供服务的** + * OutOfMemoryError:Requested array size exceeds VM limit(请求的数组大小超出VM限制) + * OutOfMemoryError: Java heap space(堆区内存不足,可以通过设置JVM参数 -Xmx 来调节) -JNI:Java Native Interface,通过使用 Java本地接口书写程序,可以确保代码在不同的平台上方便移植 +* 删除元素:需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,在旧数组上操作,该操作的时间复杂度为 O(N),可以看到 ArrayList 删除元素的代价是非常高的, -* 不需要进行GC,与虚拟机栈类似,也是线程私有的,有 StackOverFlowError 和 OutOfMemoryError 异常 + ```java + private void fastRemove(Object[] es, int i) { + modCount++; + final int newSize; + if ((newSize = size - 1) > i) + System.arraycopy(es, i + 1, es, i, newSize - i); + es[size = newSize] = null; + } + ``` -* 虚拟机栈执行的是 Java 方法,在 HotSpot JVM 中,直接将本地方法栈和虚拟机栈合二为一 +* 序列化:ArrayList 基于数组并且具有动态扩容特性,因此保存元素的数组不一定都会被使用,就没必要全部进行序列化。保存元素的数组 elementData 使用 transient 修饰,该关键字声明数组默认不会被序列化 -* 本地方法一般是由其他语言编写,并且被编译为基于本机硬件和操作系统的程序 + ```java + transient Object[] elementData; + ``` -* 当某个线程调用一个本地方法时,就进入了不再受虚拟机限制的世界,和虚拟机拥有同样的权限 +* ensureCapacity:增加此实例的容量,以确保它至少可以容纳最小容量参数指定的元素数,减少增量重新分配的次数 - * 本地方法可以通过本地方法接口来**访问虚拟机内部的运行时数据区** - * 直接从本地内存的堆中分配任意数量的内存 - * 可以直接使用本地处理器中的寄存器 - - + ```java + public void ensureCapacity(int minCapacity) { + if (minCapacity > elementData.length + && !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA + && minCapacity <= DEFAULT_CAPACITY)) { + modCount++; + grow(minCapacity); + } + } + ``` +* **Fail-Fast**:快速失败,modCount 用来记录 ArrayList **结构发生变化**的次数,结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化 + 在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了抛出 ConcurrentModificationException异常 -*** +*** -#### 程序计数器 -Program Counter Register 程序计数器(寄存器) -作用:内部保存字节码的行号,用于记录正在执行的字节码指令地址(如果正在执行的是本地方法则为空) +##### Vector -原理: +同步:Vector的实现与 ArrayList 类似,但是使用了 synchronized 进行同步 -* java虚拟机对于多线程是通过线程轮流切换并且分配线程执行时间,一个处理器只会处理执行一个线程 -* 切换线程需要从程序计数器中来回去到当前的线程上一次执行的行号 +构造:默认长度为10的数组 -特点: +扩容:Vector 的构造函数可以传入 capacityIncrement 参数,作用是在扩容时使容量 capacity 增长 capacityIncrement,如果这个参数的值小于等于 0(默认0),扩容时每次都令 capacity 为原来的两倍 -* **是线程私有的** -* 不会存在内存溢出,是JVM规范中唯一一个不出现OOM的区域,所以这个空间不会进行GC +对比 ArrayList -Java**反编译**指令:`javap -v Test.class` +1. Vector 是同步的,开销比 ArrayList 要大,访问速度更慢。最好使用 ArrayList 而不是 Vector,因为同步操作完全可以由程序来控制 -#20:去Constant pool查看该地址的指令 +2. Vector 每次扩容请求其大小的 2 倍(也可以通过构造函数设置增长的容量),而 ArrayList 是 1.5 倍 -```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 // -- -``` +3. 底层都是 `Object[]`数组存储 @@ -13619,76 +4035,130 @@ Java**反编译**指令:`javap -v Test.class` -#### 堆 +##### LinkedList -Heap 堆:是JVM内存中最大的一块,由所有线程共享,由垃圾回收器管理的主要区域("GC 堆"),堆中对象都需要考虑线程安全的问题 +###### 介绍 -存放哪些资源: +LinkedList也是List的实现类:基于**双向链表**实现,使用 Node 存储链表节点信息,增删比较快,查询慢 -* 对象实例:类初始化生成的对象,**基本数据类型的数组也是对象实例**,new 创建对象都使用堆内存 -* 字符串常量池: - * 字符串常量池原本存放于方法区,jdk7开始放置于堆中 - * 字符串常量池**存储的是string对象的直接引用或者对象**,是一张string table -* 静态变量:静态变量是有static修饰的变量,jdk7时从方法区迁移至堆中 -* 线程分配缓冲区(Thread Local Allocation Buffer):线程私有但不影响堆的共性,可以提升对象分配的效率 +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)` : 删除指定位置的元素 -设置堆内存指令:`-Xmx Size` +```java +public class ListDemo { + public static void main(String[] args) { + // 1.用LinkedList做一个队列:先进先出,后进后出。 + LinkedList queue = new LinkedList<>(); + // 入队 + queue.addLast("1号"); + queue.addLast("2号"); + queue.addLast("3号"); + System.out.println(queue); // [1号, 2号, 3号] + // 出队 + System.out.println(queue.removeFirst());//1号 + System.out.println(queue.removeFirst());//2号 + System.out.println(queue);//[3号] -内存溢出:new出对象,循环添加字符数据,当堆中没有内存空间可分配给实例,也无法再扩展时,就会抛出OutOfMemoryError异常 + // 做一个栈 先进后出 + LinkedList stack = new LinkedList<>(); + // 压栈 + stack.push("第1颗子弹");//addFirst(e); + stack.push("第2颗子弹"); + stack.push("第3颗子弹"); + System.out.println(stack); // [ 第3颗子弹, 第2颗子弹, 第1颗子弹] + // 弹栈 + System.out.println(stack.pop());//removeFirst(); 第3颗子弹 + System.out.println(stack.pop()); + System.out.println(stack);// [第1颗子弹] + } +} +``` -堆内存诊断工具:(控制台命令) +![](https://gitee.com/seazean/images/raw/master/Java/LinkedList添加元素源码解析.png) -1. jps:查看当前系统中有哪些 java 进程 -2. jmap:查看堆内存占用情况 `jhsdb jmap --heap --pid 进程id` -3. jconsole:图形界面的,多功能的监测工具,可以连续监测 -在Java7中堆内会存在**年轻代、老年代和方法区(永久代)**: -* Young区被划分为三部分,Eden区和两个大小严格相同的Survivor区。Survivor区间,某一时刻只有其中一个是被使用的,另外一个留做垃圾回收时复制对象。在Eden区变满的时候, GC就会将存活的对象移到空闲的Survivor区间中,根据JVM的策略,在经过几次垃圾回收后,仍然存活于Survivor的对象将被移动到Tenured区间 -* Tenured区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在Young复制转移一定的次数以后,对象就会被转移到Tenured区 -* Perm代主要保存**Class、ClassLoader、静态变量、常量、编译后的代码**,在java7中堆内方法区会受到GC的管理 -分代原因:不同对象的生命周期不同,70%-99%的对象都是临时对象,优化GC性能 -```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 -} -``` +###### 源码 +LinkedList是一个实现了List接口的**双端链表**,支持高效的插入和删除操作,另外也实现了Deque接口,使得LinkedList类也具有队列的特性 +![](https://gitee.com/seazean/images/raw/master/Java/LinkedList底层结构.png) -*** +核心方法: +* 使LinkedList变成线程安全的,可以调用静态类Collections类中的synchronizedList方法: + ```java + List list = Collections.synchronizedList(new LinkedList(...)); + ``` -#### 方法区 +* 私有内部类Node:这个类代表双端链表的节点Node -方法区:是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是也叫 Non-Heap(非堆) + ```java + private static class Node { + E item; + Node next; + Node prev; + + Node(Node prev, E element, Node next) { + this.item = element; + this.next = next; + this.prev = prev; + } + } + ``` -方法区是一个 JVM 规范,**永久代与元空间都是其一种实现方式** +* 构造方法:只有无参构造和用已有的集合创建链表的构造方法 -方法区的大小不必是固定的,可以动态扩展,加载的类太多,可能导致永久代内存溢出 (OutOfMemoryError) +* 添加元素:默认加到尾部 -方法区的 GC:针对常量池的回收及对类型的卸载,比较难实现 + ```java + public boolean add(E e) { + linkLast(e); + return true; + } + ``` -为了**避免方法区出现OOM**,在JDK8中将堆内的方法区(永久代)移动到了本地内存上,重新开辟了一块空间,叫做元空间,元空间存储类的元信息,静态变量和字符串常量池等放入堆中 +* 获取元素:`get(int index)` 根据指定索引返回数据 -类元信息:在类编译期间放入方法区,存放了类的基本信息,包括类的方法、参数、接口以及常量池表 + * 获取头节点 (index=0):getFirst()、element()、peek()、peekFirst() 这四个获取头结点方法的区别在于对链表为空时的处理,是抛出异常还是返回null,其中**getFirst() 和element()** 方法将会在链表为空时,抛出异常 + * 获取尾节点 (index=-1):getLast() 方法在链表为空时,会抛出NoSuchElementException,而peekLast() 则不会,只会返回 null -常量池表(Constant Pool Table)是Class文件的一部分,存储了类在编译期间生成的**字面量、符号引用**,JVM为每个已加载的类维护一个常量池 +* 删除元素: -**运行时常量池**是方法区的一部分 + * remove()、removeFirst()、pop():删除头节点 + * removeLast()、pollLast():删除尾节点,removeLast()在链表为空时抛出NoSuchElementException,而pollLast()方法返回null -* 常量池(编译器生成的字面量和符号引用)中的数据会在类加载的加载阶段放入运行时常量池 -* 类在解析阶段将符号引用替换成直接引用 -* 除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern() +对比ArrayList + +1. 是否保证线程安全:ArrayList和LinkedList都是不同步的,也就是不保证线程安全 +2. 底层数据结构: + * Arraylist底层使用的是 `Object` 数组 + * LinkedList 底层使用的是 双向链表 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环) +3. 插入和删除是否受元素位置的影响: + * ArrayList采用数组存储,所以插入和删除元素受元素位置的影响 + * LinkedList采用链表存储,所以对于`add(E e)`方法的插入,删除元素不受元素位置的影响 +4. 是否支持快速随机访问: + * LinkedList不支持高效的随机元素访问,ArrayList支持 + * 快速随机访问就是通过元素的序号快速获取元素对象(对应于`get(int index)`方法)。 +5. 内存空间占用: + * ArrayList的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间 + * LinkedList的空间花费则体现在它的每一个元素都需要消耗比 ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据) @@ -13696,224 +4166,241 @@ public static void main(String[] args) { -### 本地内存 +#### Set -#### 本地内存 +##### 概述 -虚拟机内存:Java虚拟机在执行的时候会把管理的内存分配成不同的区域,受虚拟机内存大小的参数控制,当大小超过参数设置的大小时就会报OOM +Set系列集合:添加的元素是无序,不重复,无索引的。 -本地内存:又叫做**堆外内存**,线程共享的区域,本地内存这块区域是不会受到JVM的控制的,不会发生GC;因此对于整个java的执行效率是提升非常大,但是如果内存的占用超出物理内存的大小,同样也会报OOM +* HashSet:添加的元素是无序,不重复,无索引的。 +* LinkedHashSet:添加的元素是有序,不重复,无索引的。 +* TreeSet:不重复,无索引,按照大小默认升序排序!! -本地内存概述图: +**面试问题**:没有索引,不能使用普通for循环遍历 - +##### HashSet -*** +哈希值: +- 哈希值:JDK根据对象的地址或者字符串或者数字计算出来的数值 +- 获取哈希值:Object类中的public int hashCode() -#### 元空间 +- 哈希值的特点 -PermGen 被元空间代替,永久代的**类信息、方法、常量池**等都移动到元空间区 + - 同一个对象多次调用hashCode()方法返回的哈希值是相同的 + - 默认情况下,不同对象的哈希值是不同的。而重写hashCode()方法,可以实现让不同对象的哈希值相同 -元空间与永久代区别:元空间不在虚拟机中,使用的本地内存,默认情况下,元空间的大小仅受本地内存限制 +**HashSet底层就是基于HashMap实现,值是PRESENT = new Object()** -方法区内存溢出: +Set集合添加的元素是无序,不重复的。 -* JDK1.8以前会导致永久代内存溢出:java.lang.OutOfMemoryError: PerGen space +* 是如何去重复的? - ```sh - -XX:MaxPermSize=8m #参数设置 - ``` + ```java + 1.对于有值特性的,Set集合可以直接判断进行去重复。 + 2.对于引用数据类型的类对象,Set集合是按照如下流程进行是否重复的判断。 + Set集合会让两两对象,先调用自己的hashCode()方法得到彼此的哈希值(所谓的内存地址) + 然后比较两个对象的哈希值是否相同,如果不相同则直接认为两个对象不重复。 + 如果哈希值相同,会继续让两个对象进行equals比较内容是否相同,如果相同认为真的重复了 + 如果不相同认为不重复。 -* JDK1.8以后会导致元空间内存溢出:java.lang.OutOfMemoryError: Metaspace - - ```sh - -XX:MaxMetaspaceSize=8m #参数设置 + Set集合会先让对象调用hashCode()方法获取两个对象的哈希值比较 + / \ + false true + / \ + 不重复 继续让两个对象进行equals比较 + / \ + false true + / \ + 不重复 重复了 ``` -元空间内存溢出演示: +* Set系列集合元素无序的根本原因 -```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); - } - } -} -``` + 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方法** -*** +**** -#### 直接内存 -##### 基本介绍 -直接内存是 Java 堆外的、直接向系统申请的内存区间,不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域 +##### Linked -Direct Memory 优点: +LinkedHashSet 为什么是有序的? -* Java 的 NIO 库允许 Java 程序使用直接内存,用于数据缓冲区,使用 native 函数直接分配堆外内存 -* 读写性能高,读写频繁的场合可能会考虑使用直接内存 -* 大大提高IO性能,避免了在 Java 堆和 native 堆来回复制数据 +LinkedHashSet 底层依然是使用哈希表存储元素的,但是每个元素都额外带一个链来维护添加顺序,不光增删查快,还有顺序,缺点是多了一个存储顺序的链会**占内存空间**,而且不允许重复,无索引 -直接内存缺点: -* 分配回收成本较高,不受 JVM 内存回收管理 -* 可能导致OutOfMemoryError异常:OutOfMemoryError: Direct buffer memory -* 回收依赖 System.gc() 的调用,但这个调用 JVM 不保证执行、也不保证何时执行,行为是不可控的。程序一般需要自行管理,成对去调用 malloc、free -应用场景: +**** -- 有很大的数据需要存储,数据的生命周期很长 -- 适合频繁的IO操作,比如网络并发场景 +##### TreeSet -***** +TreeSet 集合自排序的方式: +1. 有值特性的元素直接可以升序排序(浮点型,整型) +2. 字符串类型的元素会按照首字符的编号排序 +3. 对于自定义的引用数据类型,TreeSet 默认无法排序,执行的时候报错,因为不知道排序规则 +自定义的引用数据类型,TreeSet 默认无法排序,需要定制排序的规则,方案有2种: -##### 分配回收 + * 直接为**对象的类**实现比较器规则接口 Comparable,重写比较方法: + + 方法:`public int compareTo(Employee o): this是比较者, o是被比较者 ` + + * 比较者大于被比较者,返回正数 + * 比较者小于被比较者,返回负数 + * 比较者等于被比较者,返回0 + + * 直接为**集合**设置比较器 Comparator 对象,重写比较方法: + + 方法:`public int compare(Employee o1, Employee o2): o1比较者, o2被比较者` + + * 比较者大于被比较者,返回正数 + * 比较者小于被比较者,返回负数 + * 比较者等于被比较者,返回0 -DirectByteBuffer 源码分析: +注意:如果类和集合都带有比较规则,优先使用集合自带的比较规则 ```java -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; +public class TreeSetDemo{ + public static void main(String[] args){ + Set students = new TreeSet<>(); + Collections.add(students,s1,s2,s3); + System.out.println(students);//按照年龄比较 升序 + + Set s = new TreeSet<>(new Comparator(){ + @Override + public int compare(Student o1, Student o2) { + // o1比较者 o2被比较者 + return o2.getAge() - o1.getAge();//降序 + } + }); } - cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); } -private static class Deallocator implements Runnable { - public void run() { - unsafe.freeMemory(address); - //... - } + +public class Student implements Comparable{ + private String name; + private int age; + // 重写了比较方法。 + // e1.compareTo(o) + // 比较者:this + // 被比较者:o + // 需求:按照年龄比较 升序,年龄相同按照姓名 + @Override + public int compareTo(Student o) { + int result = this.age - o.age; + return result == 0 ? this.getName().compareTo(o.getName):result; } ``` -分配和回收原理: +比较器原理:底层是以第一个元素为基准,加一个新元素,就会和第一个元素比,如果大于,就继续和大于的元素进行比较,直到遇到比新元素大的元素为止,放在该位置的左边。(树) -* 使用了 Unsafe 对象的 allocateMemory 方法完成直接内存的分配,setMemory 方法完成赋值 -* ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦 ByteBuffer 对象被垃圾回收,那么 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 Deallocator 的 run方法,最后通过 freeMemory 来释放直接内存 -```java -/** - * 直接内存分配的底层原理:Unsafe - */ -public class Demo1_27 { - static int _1Gb = 1024 * 1024 * 1024; - public static void main(String[] args) throws IOException { - Unsafe unsafe = getUnsafe(); - // 分配内存 - long base = unsafe.allocateMemory(_1Gb); - unsafe.setMemory(base, _1Gb, (byte) 0); - System.in.read(); - // 释放内存 - unsafe.freeMemory(base); - System.in.read(); + +*** + + + +#### Collections + +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 + +```java +public class CollectionsDemo { + public static void main(String[] args) { + Collection names = new ArrayList<>(); + Collections.addAll(names,"张","王","李","赵"); + + List scores = new ArrayList<>(); + Collections.addAll(scores, 98.5, 66.5 , 59.5 , 66.5 , 99.5 ); + Collections.shuffle(scores); + Collections.sort(scores); // 默认升序排序! + System.out.println(scores); + + List students = new ArrayList<>(); + Collections.addAll(students,s1,s2,s3,s4); + Collections.sort(students,new Comparator(){ + + }) } +} - public static Unsafe getUnsafe() { - try { - Field f = Unsafe.class.getDeclaredField("theUnsafe"); - f.setAccessible(true); - Unsafe unsafe = (Unsafe) f.get(null); - return unsafe; - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new RuntimeException(e); - } - } +public class Student{ + private String name; + private int age; } ``` -参考文章:https://juejin.cn/post/6844904182483271694 - *** -### 变量位置 - -**变量的位置不取决于它是基本数据类型还是引用数据类型,取决于它的声明位置** - -静态内部类和其他内部类: - -* **一个class文件只能对应一个public类型的类**,这个类可以有内部类,但不会生成新的class文件 - -* 静态内部类属于类本身,加载到方法区,其他内部类属于内部类的属性,加载到栈(待考证) - -类变量: - -* 类变量是用static修饰符修饰,定义在方法外的变量,随着java进程产生和销毁 -* 在java8之前把静态变量存放于方法区,在java8时存放在**堆中的静态变量区** - - -实例变量: - -* 实例(成员)变量是定义在类中,没有static修饰的变量,随着类的实例产生和销毁,是类实例的一部分 -* 在类初始化的时候,从运行时常量池取出直接引用或者值,**与初始化的对象一起放入堆中** - -局部变量: +### Map -* 局部变量是定义在类的方法中的变量 -* 在所在方法被调用时**放入虚拟机栈的栈帧**中,方法执行结束后从虚拟机栈中弹出, +#### 概述 -类常量池、运行时常量池、字符串常量池有什么关系?有什么区别? +>Collection是单值集合体系。 +>Map集合是一种双列集合,每个元素包含两个值。 -* 类常量池与运行时常量池都存储在方法区,而字符串常量池在jdk7时就已经从方法区迁移到了java堆中 -* 在类编译过程中,会把类元信息放到方法区,类元信息的其中一部分便是类常量池,主要存放字面量和符号引用,而字面量的一部分便是文本字符 -* 在类加载时将字面量和符号引用解析为直接引用存储在运行时常量池 -* 对于文本字符来说,会在解析时查找字符串常量池,查出这个文本字符对应的字符串对象的直接引用,将直接引用存储在运行时常量池 +Map集合的每个元素的格式:key=value(键值对元素)。Map集合也被称为“键值对集合” -什么是字面量?什么是符号引用? +Map集合的完整格式:`{key1=value1 , key2=value2 , key3=value3 , ...}` -* 字面量:java代码在编译过程中是无法构建引用的,字面量就是在编译时对于数据的一种表示 +``` +Map集合的体系: + Map(接口,Map集合的祖宗类) + / \ + TreeMap HashMap(实现类,经典的,用的最多) + \ + LinkedHashMap(实现类) +``` - ```java - int a=1;//这个1便是字面量 - String b="iloveu";//iloveu便是字面量 - ``` +Map集合的特点: -* 符号引用:在编译过程中并不知道每个类的地址,因为可能这个类还没有加载,如果在一个类中引用了另一个类,无法知道它的内存地址,只能用他的类名作为符号引用,在类加载完后用这个符号引用去获取内存地址 +1. Map集合的特点都是由键决定的。 +2. Map集合的键是无序,不重复的,无索引的。(Set) +3. Map集合的值无要求。(List) +4. Map集合的键值对都可以为null。 +5. Map集合后面重复的键对应元素会覆盖前面的元素 - 例如:在com.demo.Solution类中引用了com.test.Quest,把`com.test.Quest`作为符号引用存进类常量池,在类加载完后,用这个符号引用去方法区找这个类的内存地址 +HashMap:元素按照键是无序,不重复,无索引,值不做要求。 +LinkedHashMap:元素按照键是有序,不重复,无索引,值不做要求。 +```java +//经典代码 +Map maps = new HashMap<>(); +maps.put("手机",1); +System.out.println(maps); +``` @@ -13921,18 +4408,32 @@ public class Demo1_27 { -## 内存管理 - -### 内存分配 - -#### 两种方式 +#### 常用API -为对象分配内存:首先计算对象占用空间大小,接着在堆中划分一块内存给新对象 +Map集合的常用API + `public V put(K key, V value)` : 把指定的键与值添加到Map集合中,**重复的键会覆盖前面的值元素** + `public V remove(Object key)` : 把指定的键对应的键值对元素在集合中删除,返回被删除元素的值 + `public V get(Object key)` : 根据指定的键,在Map集合中获取对应的值。 + `public Set keySet()` : 获取Map集合中所有的键,存储到**Set集合**中。 + `public Collection values()` : 获取全部值的集合,存储到**Collection集合** + `public Set> entrySet()` : 获取Map集合中所有的键值对对象的集合(Set集合) + `public boolean containKey(Object key)` : 判断该集合中是否有此键。 + `public boolean containsKey(Object key)` : 判断集合是否为空。 -* 如果内存规整,使用指针碰撞(BumpThePointer) - 所有用过的内存在一边,空闲的内存在另外一边,中间有一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离 -* 如果内存不规整,虚拟机需要维护一个空闲列表(Free List)分配 - 已使用的内存和未使用的内存相互交错,虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容 +```java +public class MapDemo { + public static void main(String[] args) { + Map maps = new HashMap<>(); + maps.put(.....); + System.out.println(maps.isEmpty());//false + Integer value = maps.get("....");//返回键值对象 + Set keys = maps.keySet();//获取Map集合中所有的键, + //Map集合的键是无序不重复的,所以返回的是一个Set集合 + Collection values = maps.values(); + //Map集合的值是不做要求的,可能重复,所以值要用Collection集合接收! + } +} +``` @@ -13940,25 +4441,44 @@ public class Demo1_27 { -#### 分代思想 - -##### 分代介绍 - -在java8时,堆被分为了两份:新生代和老年代(1:2),在java7时,还存在一个永久代 - -- 新生代使用:复制算法 -- 老年代使用:标记 - 清除 或者 标记 - 整理 算法 - -**Minor GC 和 Full GC**: - -- Minor GC:回收新生代,新生代对象存活时间很短,所以 Minor GC 会频繁执行,执行的速度比较快 -- Full GC:回收老年代和新生代,老年代对象其存活时间长,所以 Full GC 很少执行,执行速度会比 Minor GC 慢很多 +#### 遍历方式 - Eden 和 Survivor 大小比例默认为 8:1:1 +Map集合的遍历方式有:3种。 + (1)“键找值”的方式遍历:先获取Map集合全部的键,再根据遍历键找值。 + (2)“键值对”的方式遍历:难度较大,采用增强for或者迭代器 + (3)JDK 1.8开始之后的新技术:foreach,采用Lambda表达式 - +集合可以直接输出内容,因为底层重写了toString()方法。 +```java +public static void main(String[] args){ + Map maps = new HashMap<>(); + //(1)键找值 + Set keys = maps.keySet(); + for(String key : keys) { + System.out.println(key + "=" + maps.get(key)); + } + //Iterator iterator = hm.keySet().iterator(); + + //(2)键值对 + //(2.1)普通方式 + Set> entries = maps.entrySet(); + for (Map.Entry entry : entries) { + System.out.println(entry.getKey() + "=>" + entry.getValue()); + } + //(2.2)迭代器方式 + Iterator> iterator = maps.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + System.out.println(entry.getKey() + "=" + entry.getValue()); + } + //(3) Lamda + maps.forEach((k,v) -> { + System.out.println(k + "==>" + v); + }) +} +``` @@ -13966,64 +4486,64 @@ public class Demo1_27 { -##### 分代分配 - -工作机制: - -* **对象优先在 Eden 分配**:当创建一个对象的时候,对象会被分配在新生代的 Eden 区,当Eden 区要满了时候,触发 YoungGC - -* 当进行 YoungGC 后,此时在 Eden 区存活的对象被移动到 to 区,并且**当前对象的年龄会加1**,清空 Eden 区 +#### HashMap -* 当再一次触发 YoungGC 的时候,会把 Eden 区中存活下来的对象和 to 中的对象,移动到 from 区中,这些对象的年龄会加 1,清空 Eden 区和 to 区 +##### 基本介绍 -* to 区永远是空 Survivor 区,from 区是有数据的,每次 MinorGC 后两个区域互换 +HashMap基于哈希表的Map接口实现,是以key-value存储形式存在,主要用来存放键值对 -晋升到老年代: +特点: -* **长期存活的对象进入老年代**:为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中 - `-XX:MaxTenuringThreshold`:定义年龄的阈值,对象头中用4个bit存储,所以最大值是15,默认也是15 -* **大对象直接进入老年代**:需要连续内存空间的对象,最典型的大对象是很长的字符串以及数组;避免在 Eden 和 Survivor 之间的大量复制;经常出现大对象会提前触发GC以获取足够的连续空间分配给大对象 - `-XX:PretenureSizeThreshold`:大于此值的对象直接在老年代分配 -* **动态对象年龄判定**:如果在Survivor区中相同年龄的对象的所有大小之和超过Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代 +* HashMap的实现不是同步的,这意味着它不是线程安全的 +* key是唯一不重复的,底层的哈希表结构,依赖hashCode方法和equals方法保证键的唯一 +* key、value都可以为null,但是key位置只能是一个null +* HashMap中的映射不是有序的,即存取是无序的 +* **key要存储的是自定义对象,需要重写hashCode和equals方法,防止出现地址不同内容相同的key** -空间分配担保: +JDK7对比JDK8: -* 在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的 -* 如果不成立,虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于将尝试着进行一次 Minor GC;如果小于或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。 +* 7 = 数组 + 链表,8 = 数组 + 链表 + 红黑树 +* 7中是头插法,多线程容易造成环,8中是尾插法 +* 7的扩容是全部数据重新定位,8中是位置不变或者当前位置 + 旧size大小来实现 +* 7是先判断是否要扩容再插入,8中是先插入再看是否要扩容 +底层数据结构: +* 哈希表(Hash table,也叫散列表),根据关键码值(Key value)而直接访问的数据结构。通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度,这个映射函数叫做散列函数,存放记录的数组叫做散列表 -*** +* JDK1.8 之前 HashMap 由 **数组+链表** 组成 + * 数组是 HashMap 的主体 + * 链表则是为了**解决哈希冲突**而存在的(**拉链法**解决冲突),拉链法就是头插法 + 两个对象调用的hashCode方法计算的哈希码值(键的哈希)一致导致计算的数组索引值相同 +* JDK1.8 以后 HashMap 由 **数组+链表 +红黑树**数据结构组成 -#### TLAB + * 解决哈希冲突时有了较大的变化 + * **当链表长度超过(大于)阈值(或者红黑树的边界值,默认为 8)并且当前数组的长度大于等于64时,此索引位置上的所有数据改为红黑树存储** + * 即使哈希函数取得再好,也很难达到元素百分百均匀分布。当 HashMap 中有大量的元素都存放到同一个桶中时,就相当于一个长的单链表,假如单链表有 n 个元素,遍历的**时间复杂度是 O(n)**,所以 JDK1.8 中引入了 红黑树(查找**时间复杂度为 O(logn)**)来优化这个问题,使得查找效率更高 + + ![](https://gitee.com/seazean/images/raw/master/Java/哈希表.png) -虚拟机采用了两种方式在创建对象时解决并发问题:CAS、TLAB -TLAB:Thread Local Allocation Buffer,为每个线程在堆内单独分配了一个缓冲区,多线程分配内存时,使用TLAB 可以避免线程安全问题,同时还能够提升内存分配的吞吐量,这种内存分配方式叫做**快速分配策略** -- 栈上分配使用的是栈来进行对象内存的分配 -- TLAB 分配使用的是 Eden 区域进行内存分配,属于堆内存 +*** -背景:堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据,由于对象实例的创建在JVM中非常频繁,因此在并发环境下为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度 -问题:堆空间都是共享的么? 不一定,因为还有TLAB,在堆中划分出一块区域,为每个线程所独占 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-TLAB内存分配策略.jpg) +##### 继承关系 -JVM 是将 TLAB 作为内存分配的首选,但不是所有的对象实例都能够在 TLAB 中成功分配内存,一旦对象在TLAB空间分配内存失败时,JVM 就会通过**使用加锁机制确保数据操作的原子性**,从而直接在 Eden 空间中分配内存 +HashMap继承关系如下图所示: -栈上分配优先于 TLAB 分配进行,逃逸分析中若可进行栈上分配优化,会优先进行对象栈上直接分配内存 +![](https://gitee.com/seazean/images/raw/master/Java/HashMap继承关系.bmp) -参数设置: -* `-XX:UseTLAB`:设置是否开启TLAB空间 -* `-XX:TLABWasteTargetPercent`:设置TLAB空间所占用Eden空间的百分比大小,默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1,即1% -* `-XX:TLABRefillWasteFraction`:指当 TLAB 空间不足,请求分配的对象内存大小超过此阈值时不会进行 TLAB 分配,直接进行堆内存分配,否则还是会优先进行 TLAB 分配 +说明: -![](https://gitee.com/seazean/images/raw/master/Java/JVM-TLAB内存分配过程.jpg) +* Cloneable 空接口,表示可以克隆, 创建并返回HashMap对象的一个副本。 +* Serializable 序列化接口,属于标记性接口,HashMap对象可以被序列化和反序列化。 +* AbstractMap 父类提供了Map实现接口,以最大限度地减少实现此接口所需的工作 @@ -14031,182 +4551,249 @@ JVM 是将 TLAB 作为内存分配的首选,但不是所有的对象实例都 -#### 逃逸分析 - -即时编译(Just-in-time Compilation,JIT)是一种通过在运行时将字节码翻译为机器码,从而改善字节码编译语言性能的技术,在HotSpot实现中有多种选择:C1、C2 和 C1+C2,分别对应 client、server 和分层编译 +##### 成员变量 -* C1编译速度快,优化方式比较保守;C2编译速度慢,优化方式比较激进 -* C1+C2在开始阶段采用C1编译,当代码运行到一定热度之后采用G2重新编译 +1. 序列化版本号 -**逃逸分析**:并不是直接的优化手段,而是一个代码分析,通过动态分析对象的作用域,为其它优化手段如栈上分配、标量替换和同步消除等提供依据,发生逃逸行为的情况有两种:方法逃逸和线程逃逸 + ```java + private static final long serialVersionUID = 362498820763181265L; + ``` -* 方法逃逸:当一个对象在方法中定义之后,被外部方法引用 - * 全局逃逸:一个对象的作用范围逃出了当前方法或者当前线程,比如对象是一个静态变量、全局变量赋值、已经发生逃逸的对象、作为当前方法的返回值 - * 参数逃逸:一个对象被作为方法参数传递或者被参数引用 -* 线程逃逸:如类变量或实例变量,可能被其它线程访问到 +2. 集合的初始化容量( **必须是二的n次幂** ) -如果不存在逃逸行为,则可以对该对象进行如下优化:同步消除、标量替换和栈上分配 + ```java + //默认的初始容量是16 -- 1<<4相当于1*2的4次方---1*16 + static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; + ``` -* **同步消除** + HashMap构造方法指定集合的初始化容量大小: - 线程同步本身比较耗时,如果确定一个对象不会逃逸出线程,不被其它线程访问到,那对象的读写就不会存在竞争,则可以消除对该对象的**同步锁**,通过`-XX:+EliminateLocks`可以开启同步消除 ( - 号关闭) + ```java + HashMap(int initialCapacity)//构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap + ``` -* **标量替换** + * 为什么必须是2的n次幂? - * 标量替换:如果把一个对象拆散,将其成员变量恢复到基本类型来访问 - * 标量 (scalar) :不可分割的量,如基本数据类型和reference类型 - 聚合量 (Aggregate):一个数据可以继续分解,对象一般是聚合量 - * 如果逃逸分析发现一个对象不会被外部访问,并且该对象可以被拆散,那么经过优化之后,并不直接生成该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替 - * 参数设置: - `-XX:+EliminateAllocations`:开启标量替换 - `-XX:+PrintEliminateAllocations`:查看标量替换情况 + 向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,可以**保证散列的均匀性**,减少碰撞 - JIT编译器在编译期间根据逃逸分析的结果,如果一个对象没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收,这样就无需GC + ```java + 例如长度为8时候,3&(8-1)=3 2&(8-1)=2 ,不同位置上,不碰撞; + 例如长度为9时候,3&(9-1)=0 2&(9-1)=0 ,都在0上,碰撞了; + ``` + + * 如果输入值不是2的幂会怎么样? - User对象的作用域局限在方法fn中,可以使用标量替换的优化手段在栈上分配对象的成员变量,这样就不会生成User对象,大大减轻GC的压力 + 创建HashMap对象时,HashMap通过位移运算和或运算得到的肯定是2的幂次数,并且是大于那个数的最近的数字,底层采用tableSizeFor()方法 - ```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; - } - } - ``` +3. 默认的负载因子,默认值是0.75 - + ```java + static final float DEFAULT_LOAD_FACTOR = 0.75f; + ``` -*** +4. 集合最大容量 + ```java + //集合最大容量的上限是:2的30次幂 + static final int MAXIMUM_CAPACITY = 1 << 30; + ``` + 最大容量为什么是2的30次方原因: -### 回收策略 + * int类型是32位整型,占4个字节 + * Java的原始类型里没有无符号类型,所以首位是符号位正数为0,负数为1 -#### 触发条件 +5. 当链表的值超过8则会转红黑树(1.8新增**) -内存垃圾回收机制主要集中的区域就是线程共享区域:**堆和方法区** + ```java + //当桶(bucket)上的结点数大于这个值时会转成红黑树 + static final int TREEIFY_THRESHOLD = 8; + ``` -Minor GC 触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC + 为什么Map桶中节点个数大于8才转为红黑树? -FullGC 同时回收新生代、老年代和方法区,只会存在一个FullGC的线程进行执行,其他的线程全部会被**挂起**,有以下触发条件: + * 在HashMap中有一段注释说明:**空间和时间的权衡** -* 调用 System.gc(): + ```java + TreeNodes占用空间大约是普通节点的两倍,所以我们只在箱子包含足够的节点时才使用树节点。当节点变少(由于删除或调整大小)时,就会被转换回普通的桶。在使用分布良好的用户hashcode时,很少使用树箱。理想情况下,在随机哈希码下,箱子中节点的频率服从"泊松分布",默认调整阈值为0.75,平均参数约为0.5,尽管由于调整粒度的差异很大。忽略方差,列表大小k的预期出现次数是(exp(-0.5)*pow(0.5, k)/factorial(k)) + 0: 0.60653066 + 1: 0.30326533 + 2: 0.07581633 + 3: 0.01263606 + 4: 0.00157952 + 5: 0.00015795 + 6: 0.00001316 + 7: 0.00000094 + 8: 0.00000006 + more: less than 1 in ten million + 一个bin中链表长度达到8个元素的概率为0.00000006,几乎是不可能事件,所以我们选择8这个数字 + ``` + + * 其他说法 + 红黑树的平均查找长度是log(n),如果长度为8,平均查找长度为log(8)=3,链表的平均查找长度为n/2,当长度为8时,平均查找长度为8/2=4,这才有转换成树的必要;链表长度如果是小于等于6,6/2=3,而log(6)=2.6,虽然速度也很快的,但转化为树结构和生成树的时间并不短 - * 在默认情况下,通过 System.gc() 或 Runtime.getRuntime().gc() 的调用,会显式触发 FullGC,同时对老年代和新生代进行回收,但是虚拟机不一定真正去执行,无法保证对垃圾收集器的调用 (马上触发GC) - * 不建议使用这种方式,应该让虚拟机管理内存。一般情况下,垃圾回收应该是自动进行的,无须手动触发;在一些特殊情况下,如正在编写一个性能基准,可以在运行之间调用 System.gc() + -* 老年代空间不足: +6. 当链表的值小于6则会从红黑树转回链表 - * 为了避免引起的 Full GC,应当尽量不要创建过大的对象以及数组 - * 通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间 + ```java + //当桶(bucket)上的结点数小于这个值时树转链表 + static final int UNTREEIFY_THRESHOLD = 6; + ``` -* 空间分配担保失败 +7. 当Map里面的数量**大于等于**这个阈值时,表中的桶才能进行树形化 ,否则桶内元素太多时会扩容,而不是树形化。为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD (8) -* JDK 1.7 及以前的永久代空间不足 + ```java + //桶中结构转化为红黑树对应的数组长度最小的值 + static final int MIN_TREEIFY_CAPACITY = 64; + ``` -* Concurrent Mode Failure: + 原因:数组比较小的情况下变为红黑树结构,反而会降低效率,红黑树需要进行左旋,右旋,变色这些操作来保持平衡。同时数组长度小于64时,搜索时间相对快些,所以为了提高性能和减少搜索时间,底层在阈值大于8并且数组长度大于64时,链表才转换为红黑树,效率也变的更高效 - 执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC +8. table用来初始化(必须是二的n次幂)(重点) -手动GC测试,VM参数:`-XX:+PrintGcDetails` + ```java + //存储元素的数组 + transient Node[] table; + ``` -```java -public void localvarGC1() { - byte[] buffer = new byte[10 * 1024 * 1024];//10MB - System.gc(); //输出: 不会被回收, FullGC时被放入老年代 -} + jdk8之前数组类型是Entry类型,从jdk1.8之后是Node类型。只是换了个名字,都实现了一样的接口:Map.Entry,负责存储键值对数据的 -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时被放入老年代 - } + 9. HashMap中存放元素的个数(**重点**) -public void localvarGC4() { - { - byte[] buffer = new byte[10 * 1024 * 1024]; - } - int value = 10; - System.gc(); //输出: 正常被回收,slot复用,局部变量过了其作用域 buffer置空 -} -``` + ```java + //存放元素的个数,HashMap中K-V的实时数量,不是table数组的长度 + transient int size; + ``` +10. 记录HashMap的修改次数 + ```java + //每次扩容和更改map结构的计数器 + transient int modCount; + ``` -*** +11. 调整大小下一个容量的值计算方式为(容量*负载因子) + ```java + //临界值,当实际大小(容量*负载因子)超过临界值时,会进行扩容 + int threshold; + ``` +12. **哈希表的加载因子(重点)** -#### 安全区域 + ```java + final float loadFactor; + ``` + + * 加载因子的概述 + + loadFactor加载因子,是用来衡量 HashMap 满的程度,表示**HashMap的疏密程度**,影响hash操作到同一个数组位置的概率,计算HashMap的实时加载因子的方法为:size/capacity,而不是占用桶的数量去除以capacity,capacity 是桶的数量,也就是 table 的长度length。 + + 当HashMap里面容纳的元素已经达到HashMap数组长度的75%时,表示HashMap拥挤,需要扩容,而扩容这个过程涉及到 rehash、复制数据等操作,非常消耗性能,所以开发中尽量减少扩容的次数,可以通过创建HashMap集合对象时指定初始容量来尽量避免。 + + ```java + HashMap(int initialCapacity, float loadFactor)//构造指定初始容量和加载因子的空HashMap + ``` + + * 为什么加载因子设置为0.75,初始化临界值是12? + + loadFactor太大导致查找元素效率低,存放的数据拥挤,太小导致数组的利用率低,存放的数据会很分散。loadFactor的默认值为**0.75f是官方给出的一个比较好的临界值**。 + + * **threshold**计算公式:capacity(数组长度默认16) * loadFactor(负载因子默认0.75)。这个值是当前已占用数组长度的最大值。**当Size>=threshold**的时候,那么就要考虑对数组的resize(扩容),这就是 **衡量数组是否需要扩增的一个标准**, 扩容后的 HashMap 容量是之前容量的**两倍**. -安全点 (Safepoint):程序执行时并非在所有地方都能停顿下来开始 GC,只有在安全点才能停下 -- Safe Point 的选择很重要,如果太少可能导致 GC 等待的时间太长,如果太多可能导致运行时的性能问题 -- 大部分指令的执行时间都非常短,通常会根据是否具有让程序长时间执行的特征为标准,选择些执行时间较长的指令作为 Safe Point, 如方法调用、循环跳转和异常跳转等 -在 GC 发生时,让所有线程都在最近的安全点停顿下来的方法: +*** -- 抢先式中断:没有虚拟机采用,首先中断所有线程,如果有线程不在安全点,就恢复线程让线程运行到安全点 -- 主动式中断:设置一个中断标志,各个线程运行到各个 Safe Point 时就轮询这个标志,如果中断标志为真,则将自己进行中断挂起 -问题:Safepoint 保证程序执行时,在不太长的时间内就会遇到可进入GC 的 Safepoint,但是当线程处于 Waiting 状态或 Blocked 状态,线程无法响应 JVM 的中断请求,运行到安全点去中断挂起,JVM 也不可能等待线程被唤醒,对于这种情况,需要安全区域来解决 -安全区域 (Safe Region):指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始 GC 都是安全的 +##### 构造方法 -运行流程: +1. 构造一个空的 HashMap ,**默认初始容量(16)和默认负载因子(0.75)** -- 当线程运行到 Safe Region 的代码时,首先标识已经进入了 Safe Region,如果这段时间内发生GC,JVM会忽略标识为 Safe Region 状态的线程 + ```java + public HashMap() { + this.loadFactor = DEFAULT_LOAD_FACTOR; + //将默认的加载因子0.75赋值给loadFactor,并没有创建数组 + } + ``` -- 当线程即将离开 Safe Region 时,会检查JVM是否已经完成GC,如果完成了则继续运行,否则线程必须等待GC 完成,收到可以安全离开 SafeRegion 的信号 +2. 构造一个具有指定的初始容量和默认负载因子(0.75)HashMap + ```java + // 指定“容量大小”的构造函数 + public HashMap(int initialCapacity) { + this(initialCapacity, DEFAULT_LOAD_FACTOR); + } + ``` +3. 构造一个具有指定的初始容量和负载因子的HashMap -*** + ```java + public HashMap(int initialCapacity, float loadFactor) { + //进行判断 + //将指定的加载因子赋值给HashMap成员变量的负载因子loadFactor + this.loadFactor = loadFactor; + //最后调用了tableSizeFor + this.threshold = tableSizeFor(initialCapacity); + } + ``` + * 对于`this.threshold = tableSizeFor(initialCapacity);` + 有些人会觉得这里是一个bug应该这样书写: + `this.threshold = tableSizeFor(initialCapacity) * this.loadFactor;` + 这样才符合threshold的概念(当HashMap的size到达threshold这个阈值时会扩容)。 + 但是在jdk8以后的构造方法中,并没有对table这个成员变量进行初始化,table的初始化被推迟到了put方法中,在put方法中会对threshold重新计算 -### 垃圾判断 +4. 包含另一个`Map`的构造函数 -#### 垃圾介绍 + ```java + //构造一个映射关系与指定 Map 相同的新 HashMap + public HashMap(Map m) { + //负载因子loadFactor变为默认的负载因子0.75 + this.loadFactor = DEFAULT_LOAD_FACTOR; + putMapEntries(m, false); + } + ``` -垃圾:**如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾** + putMapEntries源码分析: -作用:释放没用的对象,清除内存里的记录碎片,碎片整理将所占用的堆内存移到堆的一端,以便JVM 将整理出的内存分配给新的对象 + ```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 ? -在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段,判断对象存活一般有两种方式:**引用计数算法**和**可达性分析算法** + s / loadFactor的结果是小数,加1.0F相当于是对小数做一个向上取整以尽可能的保证更大容量,更大的容量能够减少resize的调用次数,这样可以减少数组的扩容 @@ -14214,265 +4801,513 @@ public void localvarGC4() { -#### 引用计数法 - -引用计数算法(Reference Counting):对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1;当对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收(Java没有采用) - -优点: - -- 回收没有延迟性,无需等到内存不够的时候才开始回收,运行时根据对象计数器是否为0,可以直接回收 -- 在垃圾回收过程中,应用无需挂起;如果申请内存时,内存不足,则立刻报OOM错误。 -- 区域性,更新对象的计数器时,只是影响到该对象,不会扫描全部对象 - -缺点: - -- 每次对象被引用时,都需要去更新计数器,有一点时间开销。 - -- 浪费CPU资源,即使内存够用,仍然在运行时进行计数器的统计。 - -- **无法解决循环引用问题,会引发内存泄露**(最大的缺点) - - 内存泄漏(Memory Leak):是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果 +##### 成员方法 - ```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 - } - } - ``` +1. hash -![](https://gitee.com/seazean/images/raw/master/Java/JVM-循环引用.png) + 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 + jdk1.8 前是头插法 (拉链法),多线程下扩容出现循环链表,jdk1.8 以后引入红黑树,插入方法变成尾插法 -*** + 第一次调用 put 方法时创建数组 Node[] table,因为散列表耗费内存,为了防止内存浪费,所以**延迟初始化** + 存储数据步骤(存储过程): + 1. 先通过hash值计算出key映射到哪个桶 -#### 可达性分析 + 2. 如果桶上没有碰撞冲突,则直接插入 -##### GC Roots + 3. 如果出现碰撞冲突:如果该桶使用红黑树处理冲突,则调用红黑树的方法插入数据;否则采用传统的链式方法插入,如果链的长度达到临界值,则把链转变为红黑树 -可达性分析算法:也可以称为根搜索算法、追踪性垃圾收集 + 4. 如果数组位置相同,通过equals比较内容是否相同:相同则新的value覆盖之前的value,不相同则将新的键值对添加到哈希表中 + 5. 如果size大于阈值threshold,则进行扩容 -**GC Roots对象**: + ```java + public V put(K key, V value) { + return putVal(hash(key), key, value, false, true); + } + ``` -- 虚拟机栈中局部变量表中引用的对象:各个线程被调用的方法中使用到的参数、局部变量等 -- 本地方法栈中引用的对象 -- 方法区中类静态属性引用的对象 -- 方法区中的常量引用的对象 -- 字符串常量池(string Table)里的引用 -- 同步锁 synchronized 持有的对象 + putVal()方法中key在这里执行了一下hash(),在putVal函数中使用到了上述hash函数计算的哈希值: -GC Roots说明: + ```java + final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { + //。。。。。。。。。。。。。。 + if ((p = tab[i = (n - 1) & hash]) == null)//这里的n表示数组长度16 + //。。。。。。。。。。。。。。 + } + + ``` -* **GC Roots是一组活跃的引用,不是对象**,放在 GC Roots Set 集合 -* 如果一个指针保存了堆内存中的对象,但自己不在堆内存中,它就是一个Root + * `(n - 1) & hash`:计算下标位置 + + * 余数本质是不断做除法,把剩余的数减去,运算效率要比位运算低 -*** + +3. treeifyBin + 节点添加完成之后判断此时节点个数是否大于TREEIFY_THRESHOLD临界值8,如果大于则将链表转换为红黑树,转换红黑树的方法 treeifyBin,整体代码如下: -##### 工作原理 + ```java + if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st + //转换为红黑树 tab表示数组名 hash表示哈希值 + treeifyBin(tab, hash); + ``` -可达性分析算法以**根对象集合(GCRoots)**为起始点,从上至下的方式搜索被根对象集合所连接的目标对象 + 1. 如果当前数组为空或者数组的长度小于进行树形化的阈值(MIN_TREEIFY_CAPACITY = 64)就去扩容,而不是将节点变为红黑树 + 2. 如果是树形化遍历桶中的元素,创建相同个数的树形节点,复制内容,建立起联系,类似单向链表转换为双向链表 + 3. 让桶中的第一个元素即数组中的元素指向新建的红黑树的节点,以后这个桶里的元素就是红黑树而不是链表数据结构了 -分析工作必须在一个能保障**一致性的快照**中进行,否则结果的准确性无法保证,这点也是导致 GC 进行时必须 Stop The World 的一个重要原因 + -基本原理: +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; + } + ``` -- 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象 + 分析算法: -- 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象 + 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; + 00000000 00000000 00000000 00001001 //9 + 00000000 00000000 00000000 00000100 //9右移之后变为4 + -------------------------------------------------- + 00000000 00000000 00000000 00001101 //按位或之后是13 + //使得n的二进制表示中与最高位的1紧邻的右边一位为1 + ``` -*** + * 第二次右移 + ```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 + ``` + 注意:容量最大是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 + ``` -三色标记法把遍历对象图过程中遇到的对象,按“是否访问过”这个条件标记成以下三种颜色: + * JDK11 -- 白色:尚未访问过 -- 灰色:本对象已访问过,但是本对象引用到的其他对象尚未全部访问 -- 黑色:本对象已访问过,而且本对象引用到的其他对象也全部访问完成 + ```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); + } + ``` -当 Stop The World (STW) 时,对象间的引用是不会发生变化的,可以轻松完成标记,遍历访问过程为: + -1. 初始时,所有对象都在白色集合 -2. 将 GC Roots 直接引用到的对象挪到灰色集合 -3. 从灰色集合中获取对象: - * 将本对象引用到的其他对象全部挪到灰色集合中 - * 将本对象挪到黑色集合里面 -4. 重复步骤3,直至灰色集合为空时结束 -5. 结束后,仍在白色集合的对象即为GC Roots 不可达,可以进行回收 +5. resize - + 当 HashMap 中的元素个数超过`(数组长度)*loadFactor(负载因子)`或者链表过长时,就会进行数组扩容,创建新的数组,伴随一次重新 hash 分配,并且遍历 hash 表中所有的元素非常耗时,所以要尽量避免 resize -参考文章:https://www.jianshu.com/p/12544c0ad5c1 + 扩容机制为扩容为原来容量的2倍: + ```java + else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && + oldCap >= DEFAULT_INITIAL_CAPACITY) + newThr = oldThr << 1; // double threshold + ``` + HashMap 在进行扩容后,节点**要么就在原来的位置,要么就被分配到"原位置+旧容量"的位置** -###### 并发标记 + 判断:e.hash 与 oldCap 对应的有效高位上的值是 1,即当前数组长度 n 为 1 的位为 x,如果 key 的哈希值 x 位也为 1,则扩容后的索引为 now + n -并发标记时,对象间的引用可能发生变化**,**多标和漏标的情况就有可能发生 + 注意:这里也要求**数组长度2的幂** -**多标情况:**当E变为灰色或黑色时,其他线程断开的 D 对 E 的引用,导致这部分对象仍会被标记为存活,本轮 GC 不会回收这部分内存,这部分本应该回收但是没有回收到的内存,被称之为**浮动垃圾** + ![](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); + } + } + ``` -**漏标情况:** +​ -* 条件一:灰色对象断开了对一个白色对象的引用(直接或间接),即灰色对象原成员变量的引用发生了变化 -* 条件二:其他线程中修改了黑色对象,插入了一条或多条对该白色对象的新引用 -* 结果:导致该白色对象当作垃圾被GC,影响到了应用程序的正确性 +4. 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; + } + ``` + + + +5. get -代码角度解释漏标: + 1. 通过hash值获取该key映射到的桶 + + 2. 桶上的key就是要查找的key,则直接找到并返回 -```java -Object G = objE.fieldG; // 读 -objE.fieldG = null; // 写 -objD.fieldG = G; // 写 -``` + 3. 桶上的key不是要找的key,则查看后续的节点: -为了解决问题,可以操作上面三步,**将对象G记录起来,然后作为灰色对象再进行遍历**,比如放到一个特定的集合,等初始的 GC Roots遍历完(并发标记),再遍历该集合(重新标记) + * 如果后续节点是红黑树节点,通过调用红黑树的方法根据key获取value -> 所以重新标记需要STW,应用程序一直在运行,该集合可能会一直增加新的对象,导致永远都运行不完 + * 如果后续节点是链表节点,则通过循环遍历链表根据key获取value -解决方法:添加读写屏障,读屏障拦截第一步,写屏障拦截第二三步,在读写前后进行一些后置处理: + 4. 红黑树节点调用的是getTreeNode方法通过树形节点的find方法进行查 + + * 查找红黑树,之前添加时已经保证这个树是有序的,因此查找时就是折半查找,效率更高。 + * 这里和插入时一样,如果对比节点的哈希值相等并且通过equals判断值也相等,就会判断key相等,直接返回,不相等就从子树中递归查找 -* **写屏障 + 增量更新**:黑色对象新增引用,会将黑色对象变成灰色对象,最后对该节节点重新扫描 + 5. 时间复杂度 O(1) + + * 若为树,则在树中通过key.equals(k)查找,**O(logn)** + + * 若为链表,则在链表中通过key.equals(k)查找,**O(n)** - 增量更新 (Incremental Update) 破坏了条件二,从而保证了不会漏标 - 缺点:对黑色变灰的对象重新扫描所有引用,比较耗费时间 -* **写屏障 (Store Barrier) + SATB**:当原来成员变量的引用发生变化之前,记录下原来的引用对象 +*** - 保留 GC 开始时的对象图,即原始快照 SATB,当 GC Roots 确定后,对象图就已经确定,那后续的标记也应该是按照这个时刻的对象图走,如果期间对白色对象有了新的引用会记录下来,并且将白色对象变灰,重新扫描该对象 - SATB (Snapshot At The Beginning) 破坏了条件一,从而保证了不会漏标 -* **读屏障 (Load Barrier)**:破坏条件二,黑色对象引用白色对象的前提是获取到该对象,此时读屏障发挥作用 +#### LinkedMap -以 Java HotSpot VM 为例,其并发标记时对漏标的处理方案如下: +##### 原理分析 -- CMS:写屏障 + 增量更新 -- G1:写屏障 + SATB -- ZGC:读屏障 +LinkedHashMap是 HashMap 的子类 +* 优点:添加的元素按照键有序不重复的,有序的原因是底层维护了一个双向链表 +* 缺点:会占用一些内存空间 -*** +对比 Set: +* HashSet 集合相当于是 HashMap 集合的键,不带值 +* LinkedHashSet 集合相当于是 LinkedHashMap 集合的键,不带值 +* 底层原理完全一样,都是基于哈希表按照键存储数据的,只是 Map 多了一个键的值 +源码解析: -#### finalization +* 内部维护了一个双向链表,用来维护插入顺序或者 LRU 顺序 -Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑 + ```java + transient LinkedHashMap.Entry head; + transient LinkedHashMap.Entry tail; + ``` -垃圾回收此对象之前,会先调用这个对象的finalize()方法,finalize() 方法允许在子类中被重写,用于在对象被回收时进行后置处理,通常在这个方法中进行一些资源释放和清理,比如关闭文件、套接字和数据库连接等 +* accessOrder 决定了顺序,默认为 false 维护的是插入顺序,true为访问顺序(LRU顺序) -生存OR死亡:如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用,此对象需要被回收。但事实上这时候它们暂时处于”缓刑“阶段。**一个无法触及的对象有可能在某一个条件下“复活”自己**,如果这样对它的回收就是不合理的,所以虚拟机中的对象可能的三种状态: + ```java + final boolean accessOrder; + ``` -- 可触及的:从根节点开始,可以到达这个对象。 -- 可复活的:对象的所有引用都被释放,但是对象有可能在finalize() 中复活 -- 不可触及的:对象的 finalize() 被调用,并且没有复活,那么就会进入不可触及状态,不可触及的对象不可能被复活。因为**finalize()只会被调用一次**,等到这个对象再被标记为可回收时就必须回收 +* 维护顺序的函数 -永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用,原因: + ```java + void afterNodeAccess(Node p) {} + void afterNodeInsertion(boolean evict) {} + ``` -* finalize() 时可能会导致对象复活 -* finalize() 方法的执行时间是没有保障的,完全由GC线程决定,极端情况下,若不发生GC,则finalize()方法将没有执行机会,因为优先级比较低,即使主动调用该方法,也不会因此就直接进行回收 -* 一个糟糕的finalize() 会严重影响GC的性能 +* put() + ```java + // 调用父类HashMap的put方法 + public V put(K key, V value) { + return putVal(hash(key), key, value, false, true); + } + final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) + → afterNodeInsertion(evict);// evict为true + ``` + afterNodeInsertion方法,当 removeEldestEntry() 方法返回 true 时会移除最近最久未使用的节点,也就是链表首部节点 first -*** + ```java + void afterNodeInsertion(boolean evict) { + LinkedHashMap.Entry first; + // evict 只有在构建 Map 的时候才为 false,这里为 true + if (evict && (first = head) != null && removeEldestEntry(first)) { + K key = first.key; + removeNode(hash(key), key, null, false, true);//移除头节点 + } + } + ``` + removeEldestEntry() 默认为 false,如果需要让它为 true,需要继承 LinkedHashMap 并且覆盖这个方法的实现,在实现 LRU 的缓存中特别有用,通过移除最近最久未使用的节点,从而保证缓存空间足够,并且缓存的数据都是热点数据 + ```java + protected boolean removeEldestEntry(Map.Entry eldest) { + return false; + } + ``` -#### 引用分析 +* get() -无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关,Java 提供了四种强度不同的引用类型 + 当一个节点被访问时,如果 accessOrder 为 true,则会将该节点移到链表尾部。也就是说指定为 LRU 顺序之后,在每次访问一个节点时会将这个节点移到链表尾部,那么链表首部就是最近最久未使用的节点 -1. 强引用:被强引用关联的对象不会被回收,只有所有GCRoots都不通过强引用引用该对象,才能被垃圾回收 + ```java + public V get(Object key) { + Node e; + if ((e = getNode(hash(key), key)) == null) + return null; + if (accessOrder) + afterNodeAccess(e); + return e.value; + } + ``` - * 强引用可以直接访问目标对象 - * 虚拟机宁愿抛出OOM异常,也不会回收强引用所指向对象 - * 强引用可能导致**内存泄漏**(引用计数法章节解释了什么是内存泄漏) + ```java + void afterNodeAccess(Node e) { + LinkedHashMap.Entry last; + if (accessOrder && (last = tail) != e) { + // 向下转型 + LinkedHashMap.Entry p = + (LinkedHashMap.Entry)e, b = p.before, a = p.after; + p.after = null; + // 判断 p 是否是首节点 + if (b == null) + //是头节点 让p后继节点成为头节点 + head = a; + else + //不是头节点 让p的前驱节点的next指向p的后继节点,维护链表的连接 + b.after = a; + // 判断p是否是尾节点 + if (a != null) + // 不是尾节点 让p后继节点指向p的前驱节点 + a.before = b; + else + // 是尾节点 让last指向p的前驱节点 + last = b; + // 判断last是否是空 + if (last == null) + // last为空说明p是尾节点或者只有p一个节点 + head = p; + else { + // last和p相互连接 + p.before = last; + last.after = p; + } + tail = p; + ++modCount; + } + } + ``` - ```java - Object obj = new Object();//使用 new 一个新对象的方式来创建强引用 - ``` +* remove() + + ```java + //调用HashMap的remove方法 + final Node removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) + → afterNodeRemoval(node); + ``` + + 当 HashMap 删除一个键值对时调用,会把在 HashMap 中删除的那个键值对一并从链表中删除 + + ```java + void afterNodeRemoval(Node e) { + LinkedHashMap.Entry p = + (LinkedHashMap.Entry)e, b = p.before, a = p.after; + // 让p节点与前驱节点和后继节点断开链接 + p.before = p.after = null; + // 判断p是否是头节点 + if (b == null) + // p是头节点 让head指向p的后继节点 + head = a; + else + // p不是头节点 让p的前驱节点的next指向p的后继节点,维护链表的连接 + b.after = a; + // 判断p是否是尾节点,是就让tail指向p的前驱节点,不是就让p.after指向前驱节点,双向 + if (a == null) + tail = b; + else + a.before = b; + } + ``` -2. 软引用(SoftReference):被软引用关联的对象只有在内存不够的情况下才会被回收 - * **仅(可能有强引用,一个对象可以被多个引用)**有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象 - * 配合**引用队列来释放软引用自身**,在构造软引用时,可以指定一个引用队列,当软引用对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况 - * 软引用通常用来实现内存敏感的缓存,比如高速缓存就有用到软引用;如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时不会耗尽内存 - ```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; - ``` +##### LRU -4. 虚引用(PhantomReference):也称为“幽灵引用”或者“幻影引用”,是所有引用类型中最弱的一个 +使用 LinkedHashMap 实现的一个 LRU 缓存: - * 一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象 - * 为对象设置虚引用的唯一目的是在于跟踪垃圾回收过程,能在这个对象被回收时收到一个系统通知 - * 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存 +- 设定最大缓存空间 MAX_ENTRIES 为 3 +- 使用 LinkedHashMap 的构造函数将 accessOrder 设置为 true,开启 LRU 顺序 +- 覆盖 removeEldestEntry() 方法实现,在节点多于 MAX_ENTRIES 就会将最近最久未使用的数据移除 - ```java - Object obj = new Object(); - PhantomReference pf = new PhantomReference(obj, null); - obj = null; - ``` +```java +public static void main(String[] args) { + LRUCache cache = new LRUCache<>(); + cache.put(1, "a"); + cache.put(2, "b"); + cache.put(3, "c"); + cache.get(1);//把1放入尾部 + cache.put(4, "d"); + System.out.println(cache.keySet());//[3, 1, 4]只能存3个,移除2 +} -5. 终结器引用(finalization) +class LRUCache extends LinkedHashMap { + private static final int MAX_ENTRIES = 3; - 引用的四种状态: + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > MAX_ENTRIES; + } -* Active:激活,创建 ref 对象时就是激活状态 -* Pending:等待入队,所对应的强引用被GC,就要进入引用队列 -* Enqueued:入队了 - * 如果指定了 refQueue,pending 移动到 enqueued 状态,refQueue.poll 时进入失效状态 - * 如果没有指定 refQueue,直接到失效状态 -* Inactive:失效 + LRUCache() { + super(MAX_ENTRIES, 0.75f, true); + } +} +``` @@ -14480,390 +5315,647 @@ Java语言提供了对象终止(finalization)机制来允许开发人员提 -#### 无用类 +#### TreeMap -方法区主要回收的是无用的类 +TreeMap 实现了 SotredMap 接口,是有序不可重复的键值对集合,基于红黑树(Red-Black tree)实现,每个 key-value 都作为一个红黑树的节点。如果构造 TreeMap 没有指定比较器,则根据key执行自然排序(默认升序),如果指定了比较器则按照比较器来进行排序 -判定一个类是否是无用的类,需要同时满足下面 3 个条件 : +TreeSet 集合的底层是基于TreeMap,只是键的附属值为空对象而已 -- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例 -- 加载该类的`ClassLoader`已经被回收 -- 该类对应的`java.lang.Class`对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法 +TreeMap集合指定大小规则有2种方式: -虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收 +* 直接为对象的类实现比较器规则接口Comparable,重写比较方法(拓展方式) +* 直接为集合设置比较器Comparator对象,重写比较方法 +成员属性: +* Entry节点 -*** + ```java + static final class Entry implements Map.Entry { + K key; + V value; + Entry left; //左孩子节点 + Entry right; //右孩子节点 + Entry parent; //父节点 + boolean color = BLACK; //节点的颜色,在红黑树中只有两种颜色,红色和黑色 + } + ``` +* compare() + ```java + //如果comparator为null,采用comparable.compartTo进行比较,否则采用指定比较器比较大小 + final int compare(Object k1, Object k2) { + return comparator==null ? ((Comparable)k1).compareTo((K)k2) + : comparator.compare((K)k1, (K)k2); + } + ``` -### 回收算法 -#### 标记清除 -当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。目前在JVM中比较常见的三种垃圾收集算法是 +参考文章:https://blog.csdn.net/weixin_33991727/article/details/91518677 -- 标记清除算法(Mark-Sweep) -- 复制算法(copying) -- 标记压缩算法(Mark-Compact) -标记清除算法,是将垃圾回收分为2个阶段,分别是**标记和清除** -- **标记**:Collector从引用根节点开始遍历,**标记所有被引用的对象**,一般是在对象的Header中记录为可达对象,**标记的是引用的对象,不是垃圾** -- **清除**:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收,把分块连接到 “**空闲列表**” 的单向链表,判断回收后的分块与前一个空闲分块是否连续,若连续会合并这两个分块,之后进行分配时只需要遍历这个空闲列表,就可以找到分块 +*** -- **分配阶段**:程序会搜索空闲链表寻找空间大于等于新对象大小 size 的块 block,如果找到的块等于 size,会直接返回这个分块;如果找到的块大于 size,会将块分割成大小为 size 与 (block - size) 的两部分,返回大小为 size 的分块,并把大小为 (block - size) 的块返回给空闲列表 -算法缺点: -- 标记和清除过程效率都不高 -- 会产生大量不连续的内存碎片,导致无法给大对象分配内存,需要维护一个空闲链表 +#### WeakMap - +WeakHashMap 是基于弱引用的 +内部的 Entry 继承 WeakReference,被弱引用关联的对象在下一次垃圾回收时会被回收,并且构造方法传入引用队列,用来在清理对象完成以后清理引用 +```java +private static class Entry extends WeakReference implements Map.Entry { + Entry(Object key, V value, + ReferenceQueue queue, + int hash, Entry next) { + super(key, queue); + this.value = value; + this.hash = hash; + this.next = next; + } +} +``` -*** +WeakHashMap 主要用来实现缓存,使用 WeakHashMap 来引用缓存对象,由 JVM 对这部分缓存进行回收 +Tomcat 中的 ConcurrentCache 使用了 WeakHashMap 来实现缓存功能,ConcurrentCache 采取分代缓存: +* 经常使用的对象放入 eden 中,eden 使用 ConcurrentHashMap 实现,不用担心会被回收(伊甸园) -#### 复制算法 +* 不常用的对象放入 longterm,longterm 使用 WeakHashMap 实现,这些老对象会被垃圾收集器回收 -复制算法的核心就是,**将原有的内存空间一分为二,每次只用其中的一块**,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清理,交换两个内存的角色,完成垃圾的回收 +* 当调用 get() 方法时,会先从 eden 区获取,如果没有找到的话再到 longterm 获取,当从 longterm 获取到就把对象放入 eden 中,从而保证经常被访问的节点不容易被回收 -应用场景:如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之,则不适合 +* 当调用 put() 方法时,如果 eden 的大小超过了 size,那么就将 eden 中的所有对象都放入 longterm 中,利用虚拟机回收掉一部分不经常使用的对象 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-复制算法.png) + ```java + public final class ConcurrentCache { + private final int size; + private final Map eden; + private final Map longterm; + + public ConcurrentCache(int size) { + this.size = size; + this.eden = new ConcurrentHashMap<>(size); + this.longterm = new WeakHashMap<>(size); + } + + public V get(K k) { + V v = this.eden.get(k); + if (v == null) { + v = this.longterm.get(k); + if (v != null) + this.eden.put(k, v); + } + return v; + } + + public void put(K k, V v) { + if (this.eden.size() >= size) { + this.longterm.putAll(this.eden); + this.eden.clear(); + } + this.eden.put(k, v); + } + } -算法优点: -- 没有标记和清除过程,实现简单,运行高效 -- 复制过去以后保证空间的连续性,不会出现“碎片”问题。 -算法缺点: -- 主要不足是**只使用了内存的一半** -- 对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销都不小 -现在的商业虚拟机都采用这种收集算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间 +*** -*** +#### 面试题 + +输出一个字符串中每个字符出现的次数。 + +```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); + } +} +``` -#### 标记整理 +*** -标记整理(压缩)算法是在标记清除算法的基础之上,做了优化改进的算法 -标记阶段和标记清除算法一样,也是从根节点开始,对对象的引用进行标记,在清理阶段,并不是简单的直接清理可回收对象,而是将存活对象都向内存另一端移动,然后清理边界以外的垃圾,从而**解决了碎片化**的问题 -优点:不会产生内存碎片 +### 泛型 -缺点:需要移动大量对象,处理效率比较低 +#### 概述 - +泛型(Generic): + 泛型就是一个标签:<数据类型> + 泛型可以在编译阶段约束只能操作某种数据类型。 -| | Mark-Sweep | Mark-Compact | Copying | -| -------- | ---------------- | -------------- | ----------------------------------- | -| 速度 | 中等 | 最慢 | 最快 | -| 空间开销 | 少(但会堆积碎片) | 少(不堆积碎片) | 通常需要活对象的2倍大小(不堆积碎片) | -| 移动对象 | 否 | 是 | 是 | +注意: JDK 1.7开始之后,泛型后面的申明可以省略不写!! + **泛型和集合都只能支持引用数据类型,不支持基本数据类型。** -- 效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存。 -- 为了尽量兼顾三个指标,标记一整理算法相对来说更平滑一些 +```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); +} +``` +优点:泛型在编译阶段约束了操作的数据类型,从而不会出现类型转换异常 + 体现的是Java的严谨性和规范性,数据类型,经常需要进行统一! -*** +**** -#### 增量收集 -增量收集算法:通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作,基础仍是标记-清除和复制算法,用于多线程并发环境 +#### 自定义 -工作原理:如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,影响系统的交互性,所以让垃圾收集线程和应用程序线程交替执行,每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程,依次反复直到垃圾收集完成 +##### 自定义泛型类 -缺点:线程切换和上下文转换消耗资源,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降 +泛型类:使用了泛型定义的类就是泛型类。 + +泛型类格式: +```java +修饰符 class 类名<泛型变量>{ +} +泛型变量建议使用 E , T , K , V +``` +```java +public class GenericDemo { + public static void main(String[] args) { + MyArrayList list = new MyArrayList(); + MyArrayList list1 = new MyArrayList(); + list.add("自定义泛型类"); + } +} +class MyArrayList{ + public void add(E e){} + public void remove(E e){} +} +``` -*** +##### 自定义泛型方法 +泛型方法:定义了泛型的方法就是泛型方法。 +泛型方法的定义格式: -### 垃圾回收器 +```java +修饰符 <泛型变量> 返回值类型 方法名称(形参列表){ -#### 概述 +} +``` -垃圾收集器分类: +方法定义了是什么泛型变量,后面就只能用什么泛型变量。 +泛型类的核心思想:把出现泛型变量的地方全部替换成传输的真实数据类型 -* 按线程数分(垃圾回收线程数),可以分为串行垃圾回收器和并行垃圾回收器 - * 除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行 -* 按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器 - * 并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间 - * 独占式垃圾回收器(Stop the world)一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束 -* 按碎片处理方式分,可分为压缩式垃圾回收器和非压缩式垃圾回收器 - * 压缩式垃圾回收器在回收完成后进行压缩整理,消除回收后的碎片,再分配对象空间使用指针碰撞 - * 非压缩式的垃圾回收器不进行这步操作,再分配对象空间使用空闲列表 -* 按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器 +```java +public class GenericDemo { + public static void main(String[] args) { + Integer[] num = {10 , 20 , 30 , 40 , 50}; + String s1 = arrToString(nums); + + String[] name = {"贾乃亮","王宝绿","陈羽凡"}; + String s2 = arrToString(names); + } -GC性能指标: + public static String arrToString(T[] arr){ + -------------- + } +} +``` -- **吞吐量**:程序的运行时间占总运行时间的比例(总运行时间 = 程序的运行时间 + 内存回收的时间) -- 垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例 -- **暂停时间**:执行垃圾收集时,程序的工作线程被暂停的时间 -- **收集频率**:相对于应用程序的执行,收集操作发生的频率 -- **内存占用**:Java堆区所占的内存大小 -- 快速:一个对象从诞生到被回收所经历的时间 -**垃圾收集器的组合关系**: -![](https://gitee.com/seazean/images/raw/master/Java/JVM-垃圾回收器关系图.png) +自定义泛型接口 -新生代收集器:Serial、ParNew、Paralle1 Scavenge; +泛型接口:使用了泛型定义的接口就是泛型接口。 +泛型接口的格式: -老年代收集器:Serial old、Parallel old、CMS; +```java +修饰符 interface 接口名称<泛型变量>{ -整堆收集器:G1 +} +``` -* 红色虚线在JDK9移除、绿色虚线在JDK14弃用该组合、青色虚线在JDK14删除CMS垃圾回收器 +```java +public class GenericDemo { + public static void main(String[] args) { + Data d = new StudentData(); + d.add(new Student()); + ................ + } +} -查看默认的垃圾收回收器: +public interface Data{ + void add(E e); + void delete(E e); + void update(E e); + E query(int index); +} +class Student{} +class StudentData implements Data{重写所有方法} +``` -* `-XX:+PrintcommandLineFlags`:查看命令行相关参数(包含使用的垃圾收集器) -* 使用命令行指令:jinfo -flag 相关垃圾回收器参数 进程ID +**** -*** +#### 泛型通配符 +* 通配符:? + ?可以用在使用泛型的时候代表一切类型。 + E , T , K , V是在定义泛型的时候使用代表一切类型。 -#### Serial +* 泛型的上下限: + ? extends Car : 那么?必须是Car或者其子类。(泛型的上限) + ? super Car :那么?必须是Car或者其父类。(泛型的下限。不是很常见) -**Serial**:串行垃圾收集器,作用于新生代,是指使用单线程进行垃圾回收,采用**复制算法** +```java +//需求:开发一个极品飞车的游戏,所有的汽车都能一起参与比赛。 +public class GenericDemo { + public static void main(String[] args) { + ArrayList bmws = new ArrayList<>(); + ArrayList ads = new ArrayList<>(); + ArrayList dogs = new ArrayList<>(); + run(bmws); + //run(dogs); + } + //public static void run(ArrayList car){}//这样 dou对象也能进入 + public static void run(ArrayList car){} +} -**STW(Stop-The-World)**:垃圾回收时,只有一个线程在工作,并且java应用中的所有线程都要暂停,等待垃圾回收的完成 +class Car{} +class BMW extends Car{} +class AD extends Car{} +class Dog{} +``` -**Serial old**:执行老年代垃圾回收的串行收集器,内存回收算法使用的是**标记-整理算法**,同样也采用了串行回收和"Stop the World"机制, -- Serial old是运行在Client模式下默认的老年代的垃圾回收器 -- Serial old在Server模式下主要有两个用途: - - 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用 - - 作为老年代CMS收集器的**后备垃圾回收方案**,在并发收集发生 Concurrent Mode Failure 时使用 -开启参数:`-XX:+UseSerialGC == Serial + SerialOld` 等价于新生代用Serial GC且老年代用Serial old GC -![](https://gitee.com/seazean/images/raw/master/Java/JVM-Serial收集器.png) -优点:简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,可以获得最高的单线程收集效率 +*** -缺点:对于交互性较强的应用而言,这种垃圾收集器是不能够接受的,比如Javaweb应用 +### 不可变 -**** ++ 在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(); -#### Parallel + } -Parallel Scavenge 收集器是应用于新生代的并行垃圾回收器,**采用复制算法**、并行回收和"Stop the World"机制 + private static void method4() { + Map map = Map.ofEntries( + Map.entry("zhangsan", "江苏"), + Map.entry("lisi", "北京")); + System.out.println(map); + } -Parallel Old收集器:是一个应用于老年代的并行垃圾回收器,**采用标记-整理算法** + 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); + } -* 其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间 -* Parallel目标是达到一个可控制的吞吐量,被称为**吞吐量优先**收集器 -* Parallel Scavenge对比ParNew拥有**自适应调节策略**,可以通过一个开关参数打开GC Ergonomics + 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); + } +} +```` -* 停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验 -* 高吞吐量可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互 -停顿时间和吞吐量的关系:新生代空间变小 -> 缩短停顿时间 -> 垃圾回收变得频繁 -> 导致吞吐量下降 -在注重吞吐量及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge+Parallel Old收集器,在server模式下的内存回收性能很好,**Java8默认是此垃圾收集器组合** -![](https://gitee.com/seazean/images/raw/master/Java/JVM-ParallelScavenge收集器.png) -参数配置: +*** -* `-XX:+UseAdaptivesizepplicy`:设置Parallel scavenge收集器具有**自适应调节策略**,在这种模式下,年轻代的大小、Eden和Survivor的比例、晋升老年代的对象年龄等参数会被自动调整,**虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量** -* `-XX:+UseParallelGC`:手动指定年轻代使用Paralle并行收集器执行内存回收任务 -* `-XX:+UseParalleloldcc`:手动指定老年代使用并行回收收集器执行内存回收任务 - * 上面两个参数,默认开启一个,另一个也会被开启(互相激活),默认jdk8是开启的 -* `-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参数就容易超过设定的比例 +## 异常 +### 概述 -*** +异常:程序在"编译"或者"执行"的过程中可能出现的问题,Java为常见的代码异常都设计一个类来代表。 +错误:Error ,程序员无法处理的错误,只能重启系统,比如内存奔溃,JVM本身的奔溃 +Java中异常继承的根类是:Throwable -#### ParNew +``` +异常的体系: + Throwable(根类,不是异常类) + / \ + Error Exception(异常,需要研究和处理) + / \ + 编译时异常 RuntimeException(运行时异常) + +``` -Par是Parallel并行的缩写,New:只能处理的是新生代 +Exception异常的分类: -**并行垃圾收集器**在串行垃圾收集器的基础之上做了改进,**采用复制算法**,将单线程改为了多线程进行垃圾回收,这样可以缩短垃圾回收的时间 +* 编译时异常:继承自Exception的异常或者其子类,编译阶段就会报错, + 必须程序员处理的。否则代码编译就不能通过!! +* 运行时异常: 继承自RuntimeException的异常或者其子类,编译阶段是不会出错的,它是在 + 运行时阶段可能出现,编译阶段是不会出错的,但是运行阶段可能出现,建议提前处理!! -对于其他的行为(收集算法、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并行需要切换线程,串行可以省去切换线程的资源) +### 处理过程 +异常的产生默认的处理过程解析。(自动处理的过程!) +(1)默认会在出现异常的代码那里自动的创建一个异常对象:ArithmeticException(算术异常) +(2)异常会从方法中出现的点这里抛出给调用者,调用者最终抛出给JVM虚拟机。 +(3)虚拟机接收到异常对象后,先在控制台直接输出**异常栈**信息数据。 +(4)直接从当前执行的异常点干掉当前程序。 +(5)后续代码没有机会执行了,因为程序已经死亡。 -**** +```java +public class ExceptionDemo { + public static void main(String[] args) { + System.out.println("程序开始。。。。。。。。。。"); + chu( 10 ,0 ); + System.out.println("程序结束。。。。。。。。。。");//不执行 + } + public static void chu(int a , int b){ + int c = a / b ;// 出现了运行时异常,自动创建异常对象:ArithmeticException + System.out.println("结果是:"+c); + } +} +``` -#### CMS +*** -CMS全称 Concurrent Mark Sweep,是一款**并发的、使用标记-清除**算法、针对老年代的垃圾回收器,其最大特点是**让垃圾收集线程与用户线程同时工作** -CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,停顿时间越短(**低延迟**)越适合与用户交互的程序,良好的响应速度能提升用户体验 -分为以下四个流程: +### 编译时异常 -- 初始标记:使用STW出现短暂停顿,仅标记一下 GC Roots 能直接关联到的对象,速度很快 -- 并发标记:进行 GC Roots的直接关联对象开始遍历整个对象图,在整个回收过程中耗时最长,不需要STW,可以与用户线程一起并发运行 -- 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,比初始标记时间长但远比并发标记时间短,需要 STW -- 并发清除:清除标记为可以回收对象,不需要移动存活对象,所以这个阶段可以与用户线程同时并发的 +#### 概念 -Mark Sweep 会造成内存碎片,还不把算法换成 Mark Compact 的原因: +编译时异常:继承自Exception的异常或者其子类,没有继承RuntimeException + "编译时异常是编译阶段就会报错", + 必须程序员编译阶段就处理的。否则代码编译就报错!! -* Mark Compact 算法会整理内存,导致用户线程使用的对象的地址改变,影响用户线程继续执行 +编译时异常的作用是什么: + 是担心程序员的技术不行,在编译阶段就爆出一个错误, 目的在于提醒! + 提醒程序员这里很可能出错,请检查并注意不要出bug。 -* Mark Compact 更适合 Stop The World 场景 +```java +public static void main(String[] args) throws ParseException { + String date = "2015-01-12 10:23:21"; + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date d = sdf.parse(date); + System.out.println(d); +} +``` -在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-CMS收集器.png) -优点:并发收集、低延迟 +#### 处理机制 -缺点: +##### throws -- 吞吐量降低:在并发阶段虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,CPU 利用率不够高 -- CMS收集器无法处理浮动垃圾,可能出现 Concurrent Mode Failure导致另一次Full GC的产生 - 浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,CMS 收集需要预留出一部分内存,不能等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS,导致很长的停顿时间 -- 标记 - 清除算法导致的空间碎片,往往出现老年代空间无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC;为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer)技术,而只能够选择空闲列表(Free List)执行内存分配 +在出现编译时异常的地方层层把异常抛出去给调用者,调用者最终抛出给JVM虚拟机。 +JVM虚拟机输出异常信息,直接干掉程序,这种方式与默认方式是一样的。 -参数设置: +* 优点:可以解决代码编译时的错误 +* 运行时出现异常,程序还是会立即死亡! -* `-XX:+UseConcMarkSweepGC`:手动指定使用CMS收集器执行内存回收任务 +**Exception是异常最高类型可以抛出一切异常!** - 开启该参数后会自动将`-XX:+UseParNewGC`打开,即:ParNew+CMS+Serial old的组合 +```java +public static void main(String[] args) throws Exception { + System.out.println("程序开始。。。。"); + String s = "2013-03-23 10:19:23"; + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date date = sdf.parse(s); + System.out.println("程序结束。。。。。"); +} +``` -* `-XX:CMSInitiatingoccupanyFraction`:设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收 - * JDK5及以前版本的默认值为68,即当老年代的空间使用率达到68%时,会执行一次CMS回收 - * JDK6及以上版本默认值为92% -* `-XX:+UseCMSCompactAtFullCollection`:用于指定在执行完Full GC后对内存空间进行压缩整理,以此避免内存碎片的产生,由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长 +##### try/catch -* `-XX:CMSFullGCsBeforecompaction`:设置在执行多少次Full GC后对内存空间进行压缩整理 +可以处理异常,并且出现异常后代码也不会死亡。 -* `-XX:ParallelCMSThreads`:**设置CMS的线程数量** +* 自己捕获异常和处理异常的格式:**捕获处理** - * CMS默认启动的线程数是(ParallelGCThreads+3)/4,ParallelGCThreads是年轻代并行收集器的线程数 - * 收集线程占用的CPU资源多于25%,对用户程序影响可能较大;当CPU资源比较紧张时,受到CMS收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕 + ```java + try{ + // 监视可能出现异常的代码! + }catch(异常类型1 变量){ + // 处理异常 + }catch(异常类型2 变量){ + // 处理异常 + }...finall{ + //资源释放 + } + ``` +* 监视捕获处理异常企业级写法: + Exception可以捕获处理一切异常类型! + ```java + try{ + // 可能出现异常的代码! + }catch (Exception e){ + e.printStackTrace(); // **直接打印异常栈信息** + } + ``` -*** +**Throwable成员方法:** + `public String getMessage()` : 返回此 throwable 的详细消息字符串 + `public String toString()` : 返回此可抛出的简短描述 + `public void printStackTrace()` : 把异常的错误信息输出在控制台 +```java +public static void main(String[] args) { + System.out.println("程序开始。。。。"); + try { + String s = "2013-03-23 10:19:23"; + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date date = sdf.parse(s); + InputStream is = new FileInputStream("D:/meinv.png"); + } catch (Exception e) { + e.printStackTrace(); + } + System.out.println("程序结束。。。。。"); +} +``` -#### G1 -##### G1特点 +##### 方法三 -G1(Garbage-First)是一款面向服务端应用的垃圾收集器,**应用于新生代和老年代**、采用标记-整理算法、软实时、低延迟、可设定目标(最大STW停顿时间)的垃圾回收器,用于代替CMS,适用于较大的堆(>4~6G),在JDK9之后默认使用G1 +在出现异常的地方把异常一层一层的抛出给最外层调用者,最外层调用者集中捕获处理!(**规范做法**) +这种方案最外层调用者可以知道底层执行的情况,同时程序在出现异常后也不会立即死亡(最好的方案) -G1 对比其他处理器的优点: +```java +public class ExceptionDemo{ + public static void main(String[] args){ + System.out.println("程序开始。。。。"); + try { + parseDate("2013-03-23 10:19:23"); + }catch (Exception e){ + e.printStackTrace(); + } + System.out.println("程序结束。。。。"); + } + public static void parseDate(String time) throws Exception{...} +} +``` -* **并发与并行:** - * 并行性: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 的巨型对象就可以在新生代垃圾回收时处理掉 - - * Region结构图: +*** - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-G1-Region区域.png) -- **空间整合:** - - CMS:“标记-清除”算法、内存碎片、若干次 GC 后进行一次碎片整理 - - G1:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(Region 之间)上来看是基于“复制”算法实现的,两种算法都可以避免内存碎片 +### 运行时异常 -- **可预测的停顿时间模型(软实时soft real-time):** +#### 概念 - - 可以指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒 - - 由于分块的原因,G1 可以只选取部分区域进行内存回收,这样缩小了回收的范围,对于全局停顿情况也能得到较好的控制 - - G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间,通过过去回收的经验获得),在后台维护一个**优先列表**,每次根据允许的收集时间优先回收价值最大的 Region,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率 +​ 继承自RuntimeException的异常或者其子类, +​ 编译阶段是不会出错的,它是在运行时阶段可能出现的错误 +​ 运行时异常编译阶段可以处理也可以不处理,代码编译都能通过!! - * 相比于 CMS GC,G1 未必能做到 CMS 在最好情况下的延时停顿,但是最差情况要好很多 +**常见的运行时异常。(面试题)** -G1垃圾收集器的缺点: +​ 1.数组索引越界异常: ArrayIndexOutOfBoundsException +​ 2.空指针异常 : NullPointerException,直接输出没问题,调用空指针的变量的功能就会报错! +​ 3.类型转换异常:ClassCastException +​ 4.迭代器遍历没有此元素异常:NoSuchElementException +​ 5.算术异常(数学操作异常):ArithmeticException +​ 6.数字转换异常: NumberFormatException -* 相较于 CMS,G1 还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1 无论是为了垃圾收集产生的内存占用还是程序运行时的额外执行负载都要比 CMS 要高 -* 从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则发挥其优势。平衡点在6-8GB之间 +```java +public class ExceptionDemo { + public static void main(String[] args) { + System.out.println("程序开始。。。。。。"); + // 1.数组索引越界异常: ArrayIndexOutOfBoundsException。 + int[] arrs = {10 ,20 ,30}; + System.out.println(arrs[3]); //出现了数组索引越界异常。代码在此处直接执行死亡! -应用场景: + // 2.空指针异常 : NullPointerException。 + String name = null ; + System.out.println(name); // 直接输出没有问题 + System.out.println(name.length());//出现了空指针异常。代码直接执行死亡! -* 面向服务端应用,针对具有大内存、多处理器的机器 -* 需要低 GC 延迟,并具有大堆的应用程序提供解决方案 + /** 3.类型转换异常:ClassCastException。 */ + Object o = "齐天大圣"; + Integer s = (Integer) o; // 此处出现了类型转换异常。代码在此处直接执行死亡! + /** 5.数学操作异常:ArithmeticException。 */ + int c = 10 / 0 ; // 此处出现了数学操作异常。代码在此处直接执行死亡! + /** 6.数字转换异常: NumberFormatException。 */ + String num = "23aa"; + Integer it = Integer.valueOf(num); //出现了数字转换异常。代码在此处执行死亡! -*** + System.out.println("程序结束。。。。。。"); + } +} +``` -##### 记忆集 +#### 处理机制 -每个 Region 都有一个 Remembered Set,用来被哪些其他Region里的对象引用(谁引用了我就记录谁) +运行时异常在编译阶段是不会报错,在运行阶段才会出错。 +运行时异常在编译阶段不处理不会报错,但是运行时出错了程序还是会死亡,运行时异常也建议要处理。 +运行时异常是自动往外抛出的,不需要我们手工抛出。 - +**运行时异常的处理规范**:直接在最外层捕获处理即可,底层会自动抛出!! -* 程序对 Reference 类型数据写操作时,产生一个 Write Barrier 暂时中断操作,检查该对象和 Reference 类型数据是否在不同的 Region(其他收集器:检查老年代对象是否引用了新生代对象),不同便通过 CardTable 把相关引用信息记录到 Reference 类型所属的 Region 的 Remembered Set 之中 -* 进行内存回收时,在 GC 根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏 +```java +public class ExceptionDemo{ + public static void main(String[] args){ + System.out.println("程序开始。。。。"); + try{ + chu(10 / 0);//ArithmeticException: / by zero + System.out.println("操作成功!");//没输出 + }catch (Exception e){ + e.printStackTrace(); + System.out.println("操作失败!");//输出了 + } + System.out.println("程序结束。。。。");//输出了 + } + + public static void chu(int a , int b) { System.out.println( a / b );} +} +``` @@ -14871,48 +5963,83 @@ G1垃圾收集器的缺点: -##### 工作原理 +### Finally -G1中提供了三种垃圾回收模式:YoungGC、Mixed GC 和 FullGC,在不同的条件下被触发 +用在捕获处理的异常格式中的,放在最后面。 - +```java +try{ + // 可能出现异常的代码! +}catch(Exception e){ + e.printStackTrace(); +}finally{ + // 无论代码是出现异常还是正常执行,最终一定要执行这里的代码!! +} +try: 1次。 +catch:0-N次 (如果有finally那么catch可以没有!!) +finally: 0-1次 +``` -顺时针:Young GC → Young GC + Concurrent Mark → Mixed GC 顺序,进行垃圾回收 -* **Young GC**:发生在年轻代的 GC 算法,一般对象(除了巨型对象)都是在 eden region 中分配内存,当所有 eden region 被耗尽无法申请内存时,就会触发一次 young gc,G1停止应用程序的执行 (Stop-The-World),把活跃对象放入老年代,垃圾对象回收 - **回收过程**: +**finally的作用**:可以在代码执行完毕以后进行资源的释放操作 - 1. 扫描根:根引用连同 RSet 记录的外部引用作为扫描存活对象的入口 - 2. 更新 RSet:处理 dirty card queue 更新 RS,此后 RSet 准确的反映对象的引用关系 - * dirty card queue:类似缓存,产生了引用先记录在这里,然后更新到 RSet - * 作用:产生引用直接更新 RSet 需要线程同步开销很大,使用队列性能好 - 3. 处理 RSet:识别被老年代对象指向的 Eden 中的对象,这些被指向的对象被认为是存活的对象 - 4. 复制对象:Eden 区内存段中存活的对象会被复制到 survivor 区,survivor 区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到 old 区中空的内存分段,如果 survivor 空间不够,Eden 空间的部分数据会直接晋升到老年代空间 - 5. 处理引用:处理 Soft,Weak,Phantom,JNI Weak 等引用,最终 Eden 空间的数据为空,GC 停止工作 +资源:资源都是实现了Closeable接口的,都自带close()关闭方法! -* **并发标记过程**: +注意:如果在 finally 中出现了 return,会吞掉异常 + +```java +public class FinallyDemo { + public static void main(String[] args) { + System.out.println(chu());//一定会输出 finally,优先级比return高 + } + + public static int chu(){ + try{ + int a = 10 / 2 ; + return a ; + }catch (Exception e){ + e.printStackTrace(); + return -1; + }finally { + System.out.println("=====finally被执行"); + //return 111; // 不建议在finally中写return,会覆盖前面所有的return值! + } + } + public static void test(){ + InputStream is = null; + try{ + is = new FileInputStream("D:/cang.png"); + }catch (Exception e){ + e.printStackTrace(); + }finally { + System.out.println("==finally被执行==="); + // 回收资源。用于在代码执行完毕以后进行资源的回收操作! + try { + if(is!=null)is.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } +} +``` - * 初始标记:标记从根节点直接可达的对象,这个阶段是 STW 的,并且会触发一次年轻代 GC - * 根区域扫描 (Root Region Scanning):G1 扫描 survivor 区直接可达的老年代区域对象,并标记被引用的对象,这一过程必须在 Young GC 之前完成 - * 并发标记 (Concurrent Marking):在整个堆中进行并发标记(应用程序并发执行),可能被 YoungGC 中断。在并发标记阶段,**若发现区域对象中的所有对象都是垃圾,则这个区域会被立即回收(实时回收)**,同时并发标记过程中,会计算每个区域的对象活性,即区域中存活对象的比例 - * 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这阶段需要停顿线程,但是可并行执行 - * 筛选回收:并发清理阶段,首先对各个 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 +**** - 注意:**是一部分老年代,而不是全部老年代**,可以选择哪些 old region 收集,对垃圾回收的时间进行控制 - 在G1中,Mixed GC可以通过 `-XX:InitiatingHeapOccupancyPercent` 设置阈值 -* **Full GC**:对象内存分配速度过快,Mixed GC 来不及回收,导致老年代被填满,就会触发一次 Full GC,G1 的 Full GC 算法就是单线程执行的垃圾回收,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免 Full GC +### 注意事项 - 产生 Full GC 的原因: +异常的语法注意: - * 晋升时没有足够的空间存放晋升的对象 - * 并发处理过程完成之前空间耗尽,浮动垃圾 +1. 运行时异常被抛出可以不处理,可以自动抛出;**编译时异常必须处理**;按照规范都应该处理 +2. **重写方法申明抛出的异常,应该与父类被重写方法申明抛出的异常一样或者范围更小** +3. 方法默认都可以自动抛出运行时异常, throws RuntimeException可以省略不写 +4. 当多异常处理时,捕获处理,前面的异常类不能是后面异常类的父类。 +5. 在try/catch后可以追加finally代码块,其中的代码一定会被执行,通常用于资源回收操作。 @@ -14920,41 +6047,57 @@ G1中提供了三种垃圾回收模式:YoungGC、Mixed GC 和 FullGC,在不 -##### 相关参数 - -- `-XX:+UseG1GC`:手动指定使用G1垃圾收集器执行内存回收任务 -- `-XX:G1HeapRegionSize`:设置每个Region的大小。值是2的幂,范围是1MB到32MB之间,目标是根据最小的Java堆大小划分出约2048个区域,默认是堆内存的1/2000 -- `-XX:MaxGCPauseMillis`:设置期望达到的最大GC停顿时间指标 (JVM会尽力实现,但不保证达到),默认值是200ms -- `-XX:+ParallelGcThread`:设置STW工作线程数的值,最多设置为8 -- `-XX:ConcGCThreads`:设置并发标记线程数,设置为并行垃圾回收线程数(ParallelGcThreads) 的1/4左右 -- `-XX:InitiatingHeapoccupancyPercent`:设置触发并发Mixed GC周期的Java堆占用率阈值,超过此值,就触发GC,默认值是45 -- `-XX:+ClassUnloadingWithConcurrentMark`:并发标记类卸载,默认启用,所有对象都经过并发标记后,就可以知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类 - - - -*** - - +### 自定义异常 -##### 调优 +自定义异常: -G1 的设计原则就是简化 JVM 性能调优,只需要简单的三步即可完成调优: +* 自定义编译时异常. + 1. 定义一个异常类继承Exception. + 2. 重写构造器。 + 3. 在出现异常的地方用throw new 自定义对象抛出! +* 自定义运行时异常. + 1. 定义一个异常类继承RuntimeException. + 2. 重写构造器。 + 3. 在出现异常的地方用throw new 自定义对象抛出! -1. 开启 G1 垃圾收集器 -2. 设置堆的最大内存 -3. 设置最大的停顿时间(stw) +**throws: 用在方法上,用于抛出方法中的异常。** + 用于告诉调用者,本方法内部可能会抛出异常,请你处理一下 +**throw: 用在出现异常的地方,创建异常对象且立即从此处抛出。** + 将这个异常对象传递到调用者处,并结束当前方法的执行 -**不断调优暂停时间指标**: +```java +//需求:认为年龄小于0岁,大于200岁就是一个异常。 +public class ExceptionDemo { + public static void main(String[] args) { + try { + checkAge(101); + } catch (AgeIllegalException e) { + e.printStackTrace(); + } + } -* `XX:MaxGCPauseMillis=x` 可以设置启动应用程序暂停的时间,G1在运行的时候会根据这个参数选择CSet来满足响应时间的设置 -* 设置到100ms或者200ms都可以(不同情况下会不一样),但设置成50ms就不太合理 -* 暂停时间设置的太短,就会导致出现G1跟不上垃圾产生的速度,最终退化成 Full GC -* 对这个参数的调优是一个持续的过程,逐步调整到最佳状态 + public static void checkAge(int age) throws ItheimaAgeIllegalException { + if(age < 0 || age > 200){//年龄在0-200之间 + throw new AgeIllegalException("/ age is illegal!"); + //throw new AgeIllegalRuntimeException("/ age is illegal!"); + }else{ + System.out.println("年龄是:" + age); + } + } +} -**不要设置新生代和老年代的大小**: +public class AgeIllegalException extends Exception{ + Alt + Insert->Constructor +}//编译时异常 +public class AgeIllegalRuntimeException extends RuntimeException{ + public AgeIllegalRuntimeException() { + } -- 避免使用 -Xmn 或 -XX:NewRatio 等相关选项显式设置年轻代大小,G1 收集器在运行的时候会调整新生代和老年代的大小,从而达到我们为收集器设置的暂停时间目标 -- 设置了新生代大小相当于放弃了 G1 为我们做的自动调优,我们需要做的只是设置整个堆内存的大小,剩下的交给 G1 自己去分配各个代的大小 + public AgeIllegalRuntimeException(String message) { + super(message); + } +}//运行时异常 +``` @@ -14962,26 +6105,31 @@ G1 的设计原则就是简化 JVM 性能调优,只需要简单的三步即可 -#### ZGC - -Shenandoah 垃圾回收器的目标是内存回收实现低停顿 - -Shenandoah GC 暂停时间与堆大小无关,无论将堆设置为200MB还是200GB,99.9%的目标都可以把垃圾收集的停顿时间限制在十毫秒以内,不过实际使用性能将取决于实际工作堆的大小和工作负载 - -* 优点:低延迟 -* 缺点:高运行负担下的吞吐量下降 - -ZGC收集器是一款基于Region内存布局的,(暂时)不设分代,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记压缩算法的,以低延迟为首要目标的一款垃圾收集器 +### 异常作用 -* 在 CMS 和 G1 中都用到了写屏障,而 ZGC 用到了读屏障 -* 染色指针:直接将少量额外的信息存储在指针上的技术,从 64 位的指针中拿几位来标识对象此时的状态 -* 内存多重映射:多个虚拟地址指向同一个物理地址 +1、可以处理代码问题,防止程序出现异常后的死亡。 +2、提高了程序的健壮性和安全性。 -与 CMS 和 G1 类似,ZGC也采用标记-复制算法,不过 ZGC 对该算法做了重大改进,在 ZGC 中出现 Stop The World 的情况会更少,在尽可能对吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟 +```java +public class Demo{ + public static void main(String[] args){ + //请输入一个合法的年龄 + while(true){ + try{ + Scanner sc = new Scanner(System.in); + System.out.println("请您输入您的年年龄:"); + int age = sc.nextInt(); + System.out.println("年龄:"+age); + break; + }catch(Exception e){ + System.err.println("您的年龄是瞎输入的!"); + } + } + } +} +``` -ZGC的工作过程可以分为4个阶段:并发标记一并发预备重分配一并发重分配一并发重映射 -ZGC几乎在所有地方并发执行的,除了初始标记的是STW的,但这部分的实际时间是非常少的,所以响应速度快 @@ -14989,128 +6137,155 @@ ZGC几乎在所有地方并发执行的,除了初始标记的是STW的,但 -#### 总结 - -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) +### lambda +#### 基本介绍 +Lambda表达式是JDK1.8开始之后的新技术,是一种代码的新语法,一种特殊写法 +作用:为了简化匿名内部类的代码写法 +Lambda表达式的格式: -*** +```java +(匿名内部类被重写方法的形参列表) -> { + //被重写方法的方法体代码。 +} +``` +Lambda表达式并不能简化所有匿名内部类的写法,只能简化**函数式接口的匿名内部类** +简化条件:首先必须是接口,接口中只能有一个抽象方法 -### 日志分析 +@FunctionalInterface函数式接口注解:一旦某个接口加上了这个注解,这个接口只能有且仅有一个抽象方法 -内存分配与垃圾回收的参数列表:进入 Run/Debug Configurations ---> VM options 设置参 -- `-XX:+PrintGC`:输出GC日志,类似:-verbose:gc -- `-XX:+PrintGcDetails`:输出GC的详细日志 -- `-XX:+PrintGcTimestamps`:输出GC的时间戳(以基准时间的形式) -- `-XX:+PrintGCDatestamps`:输出GC的时间戳(以日期的形式,如2013-05-04T21:53:59.234+0800) -- `-XX:+PrintHeapAtGC`:在进行GC的前后打印出堆的信息 -- `-Xloggc:../logs/gc.1og`:日志文件的输出路径 +*** +#### 简化方法 -*** +Lambda表达式的省略写法(进一步在Lambda表达式的基础上继续简化) +* 如果Lambda表达式的方法体代码只有一行代码,可以省略大括号不写,同时要省略分号;如果这行代码是return语句,必须省略return不写 +* 参数类型可以省略不写 +* 如果只有一个参数,参数类型可以省略,同时()也可以省略 +```java +List names = new ArrayList<>(); +names.add("胡"); +names.add("甘"); +names.add("洪"); -## 类加载 +names.forEach(new Consumer() { + @Override + public void accept(String s) { + System.out.println(s); + } +}); -### 对象结构 +names.forEach((String s) -> { + System.out.println(s); +}); -#### 基本构造 +names.forEach((s) -> { + System.out.println(s); +}); -一个Java对象内存中存储为三部分:对象头 (Header)、实例数据 (Instance Data) 和对齐填充 (Padding) +names.forEach(s -> { + System.out.println(s); +}); -对象头: +names.forEach(s -> System.out.println(s) ); +``` -* 普通对象(32位系统,64位128位):分为两部分 - * Mark Word:用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,就是Mark Word - ```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 - ```ruby - |-----------------------------------------------------| - | Object Header (64 bits) | - |---------------------------|-------------------------| - | Mark Word (32 bits) | Klass Word (32 bits) | - |---------------------------|-------------------------| - ``` -* 数组对象:如果对象是一个数组,那在对象头中还有一块数据用于记录数组长度 +#### 常用简化 - ```ruby - |-------------------------------------------------------------------------------| - | Object Header (96 bits) | - |-----------------------|-----------------------------|-------------------------| - | Mark Word(32bits) | Klass Word(32bits) | array length(32bits) | - |-----------------------|-----------------------------|-------------------------| - ``` - - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-对象头结构.png) +##### Runnable -实例数据:实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来 +```java +//1. +Thread t = new Thread(new Runnable() { + @Override + public void run() { + System.out.println(Thread.currentThread().getName()+":执行~~~"); + } +}); +t.start(); -对齐填充:Padding 起占位符的作用。64位系统,由于 HotSpot VM 的自动内存管理系统要求对象起始地址必须是8字节的整数倍,就是对象的大小必须是8字节的整数倍,而对象头部分正好是8字节的倍数(1倍或者2倍),因此当对象实例数据部分没有对齐时,就需要通过对齐填充来补全 +//2. +Thread t1 = new Thread(() -> { + System.out.println(Thread.currentThread().getName()+":执行~~~"); +}); +t1.start(); +//3. +new Thread(() -> { + System.out.println(Thread.currentThread().getName()+":执行~~~"); +}).start(); -32位系统 +//4.一行代码 +new Thread(() -> System.out.println(Thread.currentThread().getName()+":执行~~~")).start(); +``` -* 一个 int 在 java 中占据 4byte,所以 Integer 的大小为: - ```ruby - # 需要补位4byte - 4(Mark Word) + 4(Klass Word) + 4(data) + 4(Padding) = 16byte - ``` -* `int[] arr = new int[10]` +##### Comparator - ```ruby - # 由于需要8位对齐,所以最终大小为`56byte`。 - 4(Mark Word) + 4(Klass Word) + 4(length) + 4*10(10个int大小) + 4(Padding) = 56sbyte - ``` +```java +public class CollectionsDemo { + public static void main(String[] args) { + List lists = new ArrayList<>();//...s1 s2 s3 + Collections.addAll(lists , s1 , s2 , s3); + Collections.sort(lists, new Comparator() { + @Override + public int compare(Student s1, Student s2) { + return s1.getAge() - s2.getAge(); + } + }); + + // 简化写法 + Collections.sort(lists ,(Student t1, Student t2) -> { + return t1.getAge() - t2.getAge(); + }); + // 参数类型可以省略,最简单的 + Collections.sort(lists ,(t1,t2) -> t1.getAge()-t2.getAge()); + } +} +``` -*** +*** -#### 节约内存 -* 尽量使用基本类型 -* 满足容量前提下,尽量用小字段 +### 方法引用 -* 尽量用数组,少用集合,数组中是可以使用基本类型的,但是集合中只能放包装类型,如果需要使用集合,推荐比较节约内存的集合工具:fastutil +#### 基本介绍 - 一个ArrayList集合,如果里面放了10个数字,占用多少内存: +方法引用:方法引用是为了进一步简化Lambda表达式的写法 - ```java - private transient Object[] elementData; - private int size; - ``` +方法引用的格式:类型或者对象::引用的方法 - Mark Word 占 4byte,Klass Word 占 4byte,一个int字段(size)占 4byte,elementData 数组本身占 12(4+4+4),数组中 10 个 Integer 对象占 10×16,所以整个集合空间大小为 184byte +关键语法是:`::` -* 时间用 long/int 表示,不用 Date 或者 String +```java +lists.forEach( s -> System.out.println(s)); +// 方法引用! +lists.forEach(System.out::println); +``` @@ -15118,23 +6293,30 @@ Serial GC、Parallel GC、Concurrent Mark Sweep GC这三个GC不同: -#### 对象访问 - -JVM是通过栈帧中的对象引用访问到其内部的对象实例: +#### 静态方法 -* 句柄访问 - 使用该方式,Java堆中会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息 - 优点:reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。 +引用格式:`类名::静态方法` - +简化步骤:定义一个静态方法,把需要简化的代码放到一个静态方法中去 -* 直接指针(HotSpot采用) - 使用该方式,Java 堆对象的布局必须考虑如何放置访问类型数据的相关信息,reference中直接存储的就是对象地址 - 优点:速度更快,**节省了一次指针定位的时间开销** +静态方法引用的注意事项:被引用的方法的参数列表要和函数式接口中的抽象方法的参数列表一致,才能引用简化 - +```java +//定义集合加入几个Student元素 +// 使用静态方法进行简化! +Collections.sort(lists, (o1, o2) -> Student.compareByAge(o1 , o2)); +// 如果前后参数是一样的,而且方法是静态方法,既可以使用静态方法引用 +Collections.sort(lists, Student::compareByAge); +public class Student { + private String name ; + private int age ; + public static int compareByAge(Student o1 , Student o2){ + return o1.getAge() - o2.getAge(); + } +} +``` @@ -15142,209 +6324,245 @@ JVM是通过栈帧中的对象引用访问到其内部的对象实例: -### 对象创建 +#### 实例方法 -#### 生命周期 +引用格式:`对象::实例方法` -在Java中,对象的生命周期包括以下几个阶段: +简化步骤:定义一个实例方法,把需要的代码放到实例方法中去 -1. 创建阶段 (Created): -2. 应用阶段 (In Use):对象至少被一个强引用持有着 -3. 不可见阶段 (Invisible):程序的执行已经超出了该对象的作用域,不再持有该对象的任何强引用 -4. 不可达阶段 (Unreachable):该对象不再被任何强引用所持有,包括GC Root的强引用 -5. 收集阶段 (Collected):垃圾回收器已经对该对象的内存空间重新分配做好准备,该对象如果重写了finalize()方法,则会去执行该方法 -6. 终结阶段 (Finalized):等待垃圾回收器对该对象空间进行回收,当对象执行完finalize()方法后仍然处于不可达状态时进入该阶段 -7. 对象空间重分配阶段 (De-allocated):垃圾回收器对该对象的所占用的内存空间进行回收或者再分配 +实例方法引用的注意事项:被引用的方法的参数列表要和函数式接口中的抽象方法的参数列表一致。 + +```java +public class MethodDemo { + public static void main(String[] args) { + List lists = new ArrayList<>(); + lists.add("java1"); + lists.add("java2"); + lists.add("java3"); + // 对象是 System.out = new PrintStream(); + // 实例方法:println() + // 前后参数正好都是一个 + lists.forEach(s -> System.out.println(s)); + lists.forEach(System.out::println); + } +} +``` -参考文章:https://blog.csdn.net/sodino/article/details/38387049 +*** -*** +#### 特定类型 +特定类型:String,任何类型 +引用格式:`特定类型::方法` -#### 创建时机 +注意事项:如果第一个参数列表中的形参中的第一个参数作为了后面的方法的调用者,并且其余参数作为后面方法的形参,那么就可以用特定类型方法引用了 -类在第一次实例化加载一次,后续实例化不再加载,引用第一次加载的类 +```java +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"}; -Java对象创建时机: + // public static void sort(T[] a, Comparator c) + // 需求:按照元素的首字符(忽略大小写)升序排序!!! + Arrays.sort(strs, new Comparator() { + @Override + public int compare(String s1, String s2) { + return s1.compareToIgnoreCase(s2);//按照元素的首字符(忽略大小写) + } + }); -1. 使用new关键字创建对象:由执行类实例创建表达式而引起的对象创建 + Arrays.sort(strs, ( s1, s2 ) -> s1.compareToIgnoreCase(s2)); -2. 使用Class类的newInstance方法 (反射机制) + // 特定类型的方法引用: + Arrays.sort(strs, String::compareToIgnoreCase); + System.out.println(Arrays.toString(strs)); + } +} +``` -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方法 -5. 使用(反)序列化机制创建对象:当反序列化一个对象时,JVM会创建一个**单独的对象**,在此过程中,JVM并不会调用任何构造函数,为了反序列化一个对象,需要让类实现Serializable接口 -从Java虚拟机层面看,除了使用new关键字创建对象的方式外,其他方式全部都是通过转变为invokevirtual指令直接创建对象的 +#### 构造器 +格式:`类名::new` +注意事项:前后参数一致的情况下,又在创建对象,就可以使用构造器引用 -*** +```java +public class ConstructorDemo { + public static void main(String[] args) { + List lists = new ArrayList<>(); + lists.add("java1"); + lists.add("java2"); + lists.add("java3"); + // 集合默认只能转成Object类型的数组。 + Object[] objs = lists.toArray(); + // 我们想指定转换成字符串类型的数组!最新的写法可以结合构造器引用实现 + String[] strs = lists.toArray(new IntFunction() { + @Override + public String[] apply(int value) { + return new String[value]; + } + }); + String[] strs1 = lists.toArray(s -> new String[s]); + String[] strs2 = lists.toArray(String[]::new); -#### 创建过程 + System.out.println("String类型的数组:"+ Arrays.toString(strs2)); + } +} +``` -创建对象的过程: -1. 判断对象对应的类是否加载、链接、初始化 -2. 为对象分配内存:指针碰撞、空闲链表。当一个对象被创建时,虚拟机就会为其分配内存来存放对象的实例变量及其从父类继承过来的实例变量 (即使从超类继承过来的实例变量有可能被**隐藏**也会被分配空间) -3. 处理并发安全问题: - * 采用CAS配上自旋保证更新的原子性 - * 每个线程预先分配一块TLAB +*** -4. 初始化分配的空间:虚拟机将分配到的内存空间都初始化为零值(不包括对象头),保证对象实例字段在不赋值时可以直接使用,程序能访问到这些字段的数据类型所对应的零值 -5. 设置对象的对象头:将对象的所属类(类的元数据信息)、对象的HashCode、对象的GC信息、锁信息等数据存储在对象的对象头中 -6. 执行init方法进行实例化:实例变量初始化、实例代码块初始化 、构造函数初始化 +## IO - * 实例变量初始化与实例代码块初始化: +### Stream - 对实例变量直接赋值或者使用实例代码块赋值,编译器会将其中的代码放到类的构造函数中去,并且这些代码会被放在对超类构造函数的调用语句之后 (**Java要求构造函数的第一条语句必须是超类构造函数的调用语句**),构造函数本身的代码之前 +#### 概述 - * 构造函数初始化: +Stream 流其实就是一根传送带,元素在上面可以被 Stream 流操作 - **Java要求在实例化类之前,必须先实例化其超类,以保证所创建实例的完整性**,在准备实例化一个类的对象前,首先准备实例化该类的父类,如果该类的父类还有父类,那么准备实例化该类的父类的父类,依次递归直到递归到Object类。然后从Object类依次对以下各类进行实例化,初始化父类中的变量和执行构造函数 +作用: +* 可以解决已有集合类库或者数组API的弊端。 +* Stream 流简化集合和数组的操作 +* 链式编程 +```java +list.stream().filter(new Predicate() { + @Override + public boolean test(String s) { + return s.startsWith("张"); + } + }); -*** +list.stream().filter(s -> s.startsWith("张")); +``` -#### 承上启下 +*** -1. 一个实例变量在对象初始化的过程中会被赋值几次? - JVM在为一个对象分配完内存之后,会给每一个实例变量赋予默认值,这个实例变量被第一次赋值 - 在声明实例变量的同时对其进行了赋值操作,那么这个实例变量就被第二次赋值 - 在实例代码块中又对变量做了初始化操作,那么这个实例变量就被第三次赋值 - 在构造函数中也对变量做了初始化操作,那么这个实例变量就被第四次赋值 - 在Java的对象初始化过程中,一个实例变量最多可以被初始化4次 -2. 类的初始化过程与类的实例化过程的异同? +#### 获取流 - 类的初始化是指类加载过程中的初始化阶段对类变量按照代码进行赋值的过程 - 类的实例化是指在类完全加载到内存中后创建对象的过程(类的实例化触发了类的初始化) +集合获取 Stream 流用:default Stream stream() -3. 假如一个类还未加载到内存中,那么在创建一个该类的实例时,具体过程是怎样的?(**经典案例**) +数组:Arrays.stream(数组) / Stream.of(数组); - ```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 +// Collection集合获取Stream流。 +Collection c = new ArrayList<>(); +Stream listStream = c.stream(); - `static StaticTest st = new StaticTest();`: +//Map集合获取流 +// 先获取键的Stream流。 +Stream keysStream = map.keySet().stream(); +// 在获取值的Stream流 +Stream valuesStream = map.values().stream(); +// 获取键值对的Stream流(key=value: Map.Entry) +Stream> keyAndValues = map.entrySet().stream(); - * 实例初始化不一定要在类初始化结束之后才开始 +//数组获取流 +String[] arr = new String[]{"Java", "JavaEE" ,"Spring Boot"}; +Stream arrStream1 = Arrays.stream(arr); +Stream arrStream2 = Stream.of(arr); +``` - * 在同一个类加载器下,一个类型只会被初始化一次。所以一旦开始初始化一个类,无论是否完成后续都不会再重新触发该类型的初始化阶段了(只考虑在同一个类加载器下的情形。因此,在实例化上述程序中的st变量时,**实际上是把实例初始化嵌入到了静态初始化流程中,并且在上面的程序中,嵌入到了静态初始化的起始位置**,这就导致了实例初始化完全发生在静态初始化之前,这也是导致a为110 b为0的原因 - 代码等价于: - ```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 - } - } - ``` +**** - +#### 常用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) )组成的流 | + +```java +public class StreamDemo { + public static void main(String[] args) { + List list = new ArrayList<>(); + list.add("张无忌"); list.add("周芷若"); list.add("赵敏"); + list.add("张强"); list.add("张三丰"); list.add("张三丰"); + //取以张开头并且名字是三位数的 + list.stream().filter(s -> s.startsWith("张") + .filter(s -> s.length == 3).forEach(System.out::println); + //统计数量 + long count = list.stream().filter(s -> s.startsWith("张") + .filter(s -> s.length == 3).count(); + //取前两个 + list.stream().filter(s -> s.length == 3).limit(2).forEach(...); + //跳过前两个 + list.stream().filter(s -> s.length == 3).skip(2).forEach(...); + // 需求:把名称都加上“张三的:+xxx” + list.stream().map(s -> "张三的"+s).forEach(System.out::println); + // 需求:把名称都加工厂学生对象放上去!! + // list.stream().map(name -> new Student(name)); + list.stream.map(Student::new).forEach(System.out::println); + + //数组流 + Stream s1 = Stream.of(10,20,30,40,50); + //集合流 + Stream s2 = list.stream(); + //合并流 + Stream s3 = Stream.concat(s1,s2); + s3.forEach(System.out::println); + } +} +class Student{ + private String name; + //...... +} +``` -### 加载过程 -#### 生命周期 +*** -类是在运行期间**第一次使用时动态加载**的(不使用不加载),而不是一次性加载所有类,因为一次性加载会占用很多的内存,加载的类信息存放于一块成为方法区的内存空间 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-类的生命周期.png) -包括 7 个阶段: +#### 终结方法 -* 加载(Loading) -* 链接:验证(Verification)、准备(Preparation)、解析(Resolution) -* 初始化(Initialization) -* 使用(Using) -* 卸载(Unloading) +终结方法:Stream调用了终结方法,流的操作就全部终结,不能继续使用,如foreach , count方法等 -类加载方式: +非终结方法:每次调用完成以后返回一个新的流对象,可以继续使用,支持**链式编程**! -* 隐式加载: - * 创建类对象、使用类的静态域、创建子类对象、使用子类的静态域 - * 在JVM启动时,通过三大类加载器加载class -* 显式加载: - * ClassLoader.loadClass(className),只加载和连接,**不会进行初始化** - * Class.forName(String name, boolean initialize,ClassLoader loader),使用loader进行加载和连接,根据参数initialize决定是否初始化 +```java +// foreach终结方法 +list.stream().filter(s -> s.startsWith("张")) + .filter(s -> s.length() == 3).forEach(System.out::println); +``` @@ -15352,49 +6570,93 @@ Java对象创建时机: -#### 加载阶段 +#### 收集流 -加载是类加载的一个阶段,注意不要混淆 +收集 Stream:把 Stream 流的数据转回到集合中去 -加载过程完成以下三件事: +* Stream流:工具 +* 集合:目的 -- 通过类的完全限定名称获取定义该类的二进制字节流(二进制字节码) -- 将该字节流表示的**静态存储结构转换为方法区的运行时存储结构**(运行时常量池) -- 在内存中生成一个代表该类的 Class 对象,作为方法区中该类各种数据的访问入口 +| 方法名 | 说明 | +| ------------------------------------------------------------ | ---------------------- | +| 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() | 把元素收集数组中 | -其中二进制字节流可以从以下方式中获取: +```java +public static void main(String[] args) { + List list = new ArrayList<>(); + 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("张")); + List list = stream.collect(Collectors.toList()); + + //把stream流转换成数组。 + Stream stream2 =list.stream().filter(s -> s.startsWith("张")); + Object[] arr = stream2.toArray(); + // 可以借用构造器引用申明转换成的数组类型!!! + String[] arr1 = stream2.toArray(String[]::new); +} +``` -- 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础 -- 从网络中获取,最典型的应用是 Applet -- 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass -- 由其他文件生成,例如由 JSP 文件生成对应的 Class 类 -将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,重要 field: -* `_java_mirror` 即 java 的类镜像,例如对 String 来说就是 String.class,作用是把 class 暴露给 java 使用 -* `_super` 即父类、`_fields` 即成员变量、`_methods` 即方法、`_constants` 即常量池、`_class_loader` 即类加载器、`_vtable` **虚方法表**、`_itable` 接口方法表 +*** -加载过程: -* 如果这个类还有父类没有加载,先加载父类 -* 加载和链接可能是交替运行的 -* instanceKlass 和 _java_mirror 相互持有对方的地址,堆中对象通过 instanceKlass 和元空间进行交互 - +### File +#### 概述 +File类:代表操作系统的文件对象,是用来操作操作系统的文件对象的,删除文件,获取文件信息,创建文件(文件夹),广义来说操作系统认为文件包含(文件和文件夹) -*** +File类构造器: + `public File(String pathname)`:根据路径获取文件对象 + `public File(String parent , String child)`:根据父路径和文件名称获取文件对象! + `public File(File parent , String child)` +File类创建文件对象的格式: +* `File f = new File("绝对路径/相对路径");` + * 绝对路径:从磁盘的的盘符一路走到目的位置的路径。 + * 绝对路径依赖具体的环境,一旦脱离环境,代码可能出错 + * 一般是定位某个操作系统中的某个文件对象 + * **相对路径**:不带盘符的(重点) + * 默认是直接相对到工程目录下寻找文件的。 + * 相对路径只能用于寻找工程下的文件,可以跨平台 -#### 链接阶段 +* `File f = new File("文件对象/文件夹对象")` 广义来说:文件是包含文件和文件夹的 -##### 验证 +```java +public class FileDemo{ + public static void main(String[] args) { + // 1.创建文件对象:使用绝对路径 + // 文件路径分隔符: + // -- a.使用正斜杠: / + // -- b.使用反斜杠: \\ + // -- c.使用分隔符API:File.separator + //File f1 = new File("D:"+File.separator+"it"+File.separator + //+"图片资源"+File.separator+"beautiful.jpg"); + File f1 = new File("D:\\seazean\\图片资源\\beautiful.jpg"); + System.out.println(f1.length()); // 获取文件的大小,字节大小 -确保 Class 文件的字节流中包含的信息是否符合 JVM规范,保证被加载类的正确性,不会危害虚拟机自身的安全 + // 2.创建文件对象:使用相对路径 + File f2 = new File("Day09Demo/src/dlei.txt"); + System.out.println(f2.length()); -主要包括四种验证:文件格式验证,源数据验证,字节码验证,符号引用验证 + // 3.创建文件对象:代表文件夹。 + File f3 = new File("D:\\it\\图片资源"); + System.out.println(f3.exists());// 判断路径是否存在!! + } +} +``` @@ -15402,254 +6664,441 @@ Java对象创建时机: -##### 准备 +#### 常用API -准备阶段为**静态变量分配内存并设置初始值**,使用的是方法区的内存: +##### 常用方法 -* 类变量也叫静态变量,就是是被 static 修饰的变量 -* 实例变量也叫对象变量,即没加static 的变量 +`public String getAbsolutePath()` : 返回此File的绝对路径名字符串。 +`public String getPath()` : 获取创建文件对象的时候用的路径 +`public String getName()` : 返回由此File表示的文件或目录的名称。 +`public long length()` : 返回由此File表示的文件的长度(大小)。 +`public long length(FileFilter filter)` : 文件过滤器。 -实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中。实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次 +```java +public class FileDemo { + public static void main(String[] args) { + // 1.绝对路径创建一个文件对象 + File f1 = new File("E:/图片/meinv.jpg"); + // a.获取它的绝对路径。 + System.out.println(f1.getAbsolutePath()); + // b.获取文件定义的时候使用的路径。 + System.out.println(f1.getPath()); + // c.获取文件的名称:带后缀。 + System.out.println(f1.getName()); + // d.获取文件的大小:字节个数。 + System.out.println(f1.length()); + System.out.println("------------------------"); -类变量初始化: + // 2.相对路径 + File f2 = new File("Day09Demo/src/dlei01.txt"); + // a.获取它的绝对路径。 + System.out.println(f2.getAbsolutePath()); + // b.获取文件定义的时候使用的路径。 + System.out.println(f2.getPath()); + // c.获取文件的名称:带后缀。 + System.out.println(f2.getName()); + // d.获取文件的大小:字节个数。 + System.out.println(f2.length()); + } +} -* static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾 -* static 变量分配空间和赋值是两个步骤:**分配空间在准备阶段完成,赋值在初始化阶段完成** -* 如果 static 变量是 final 的基本类型以及字符串常量,那么编译阶段值就确定了,准备阶段会显式初始化 -* 如果 static 变量是 final 的,但属于引用类型或者构造器方法的字符串,赋值在初始化阶段完成 +``` -实例: -* 初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123: - ```java - public static int value = 123; - ``` +##### 判断方法 + +`public boolean exists()` : 此File表示的文件或目录是否实际存在。 +`public boolean isDirectory()` : 此File表示的是否为目录。 +`public boolean isFile()` : 此File表示的是否为文件 + +```java +File f = new File("Day09Demo/src/dlei01.txt"); +// a.判断文件路径是否存在 +System.out.println(f.exists()); // true +// b.判断文件对象是否是文件,是文件返回true ,反之 +System.out.println(f.isFile()); // true +// c.判断文件对象是否是文件夹,是文件夹返回true ,反之 +System.out.println(f.isDirectory()); // false +``` + + -* 常量 value 被初始化为 123 而不是 0: +##### 创建删除 - ```java - public static final int value = 123; - ``` +`public boolean createNewFile()` : 当且仅当具有该名称的文件尚不存在时, 创建一个新的空文件。 +`public boolean delete()` : 删除由此File表示的文件或目录。 (只能删除空目录) +`public boolean mkdir()` : 创建由此File表示的目录。(只能创建一级目录) +`public boolean mkdirs()` : 可以创建多级目录(建议使用的) +```java +public class FileDemo { + public static void main(String[] args) throws IOException { + File f = new File("Day09Demo/src/dlei02.txt"); + // a.创建新文件,创建成功返回true ,反之 + System.out.println(f.createNewFile()); + // b.删除文件或者空文件夹 + System.out.println(f.delete()); + // 不能删除非空文件夹,只能删除空文件夹 + File f1 = new File("E:/it/aaaaa"); + System.out.println(f1.delete()); -*** + // c.创建一级目录 + File f2 = new File("E:/bbbb"); + System.out.println(f2.mkdir()); + // d.创建多级目录 + File f3 = new File("D:/it/e/a/d/ds/fas/fas/fas/fas/fas/fas"); + System.out.println(f3.mkdirs()); + } +} +``` -##### 解析 -将常量池中类、接口、字段、方法的**符号引用替换为直接引用**(内存地址)的过程: +*** -* 符号引用:一组符号来描述目标,可以是任何字面量,属于编译原理方面的概念如:包括类和接口的全限名、字段的名称和描述符、方法的名称和**方法描述符** -* 直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄 -解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等 -* 在类加载阶段解析的是非虚方法,静态绑定 +#### 遍历目录 -* 也可以在初始化阶段之后再开始解析,这是为了支持 Java 的**动态绑定** +- `public String[] list()`: + 获取当前目录下所有的"一级文件名称"到一个字符串数组中去返回。 +- `public File[] listFiles()(常用)`: + 获取当前目录下所有的"一级文件对象"到一个**文件对象数组**中去返回(**重点**) +- `public long lastModified` : + 返回此抽象路径名表示的文件上次修改的时间。 ```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(); +public class FileDemo { + public static void main(String[] args) { + File dir = new File("D:\\seazean"); + // a.获取当前目录对象下的全部一级文件名称到一个字符串数组返回。 + String[] names = dir.list(); + for (String name : names) { + System.out.println(name); + } + // b.获取当前目录对象下的全部一级文件对象到一个File类型的数组返回。 + File[] files = dir.listFiles(); + for (File file : files) { + System.out.println(file.getAbsolutePath()); + } + + // c + File f1 = new File("D:\\it\\图片资源\\beautiful.jpg"); + long time = f1.lastModified(); // 最后修改时间! + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + System.out.println(sdf.format(time)); } } -class C { - D d = new D(); -} -class D { -} ``` -**** +*** -#### 初始化 +#### 文件搜索 -##### 介绍 +递归实现文件搜索(非规律递归) + (1)定义一个方法用于做搜索。 + (2)进入方法中进行业务搜索分析。 -初始化阶段才真正开始执行类中定义的 Java 程序代码,在准备阶段,类变量已经赋过一次系统要求的初始值;在初始化阶段,通过程序制定的计划去初始化类变量和其它资源,执行 +```java +/** + * 去某个目录下搜索某个文件 + * @param dir 搜索文件的目录。 + * @param fileName 搜索文件的名称。 + */ +public static void searchFiles(File dir , String fileName){ + // 1.判断是否存在该路径,是否是文件夹 + if(dir.exists() && dir.isDirectory()){ + // 2.提取当前目录下的全部一级文件对象 + File files = dir.listFiles();// 可能是null/也可能是空集合[] + // 3.判断是否存在一级文件对象,判断是否不为空目录 + if(files != null && files.length > 0){ + // 4.判断一级文件对象 + for(File file : files){ + // 5.判断file是文件还是文件夹 + if(file.isFile()){ + // 6.判断该文件是否为我要找的文件对象 + if(f.getName().contains(fileName)){//模糊查找 + sout(f.getAbsolutePath()); + try { + // 启动它(拓展) + Runtime r = Runtime.getRuntime(); + r.exec(f.getAbsolutePath()); + } catch (IOException e) { + e.printStackTrace(); + } + } + } else { + // 7.该文件是文件夹,文件夹要递归进入继续寻找 + searchFiles(file,fileName) + } + } + } + } +} +``` -在编译生成class文件时,编译器会产生两个方法加于class文件中,一个是类的初始化方法clinit, 另一个是实例的初始化方法init -类构造器()与实例构造器()不同,它不需要程序员进行显式调用,在一个类的生命周期中,类构造器()最多被虚拟机**调用一次**,而实例构造器()则会被虚拟机调用多次,只要程序员创建对象 -类在第一次实例化加载一次,把class读入内存,后续实例化不再加载,引用第一次加载的类 +*** -*** +### Character +字符集:各个国家为自己国家的字符取的一套编号规则 +计算机的底层是不能直接存储字符的,只能存储二进制,010101。 -##### clinit +美国人: + 8个开关一组就可以编码字符,1个字节。 + 2^8 = 256 + 一个字节存储一个字符完全够用了。 -():类构造器,由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的 +​ a 97 +​ b 98 -作用:是在类加载过程中的初始化阶段进行静态变量初始化和执行静态代码块 +​ A 65 +​ B 66 -* 如果类中没有静态变量或静态代码块,那么clinit方法将不会被生成 -* 在执行clinit方法时,必须先执行父类的clinit方法 -* clinit方法只执行一次 -* static变量的赋值操作和静态代码块的合并顺序由源文件中**出现的顺序**决定 +​ 0 48 +​ 1 49 +​ 这套编码是ASCII编码。 +​ 英文和数字在底层存储的时候都是采用1个字节存储的。 -**线程安全**问题: +中国人: + 中国人的字符很多:9万左右字符。 + 2个字节编码一个中文字符,1个字节编码一个英文字符。 + 这套编码叫:GBK编码。 + 它也必须兼容ASCII编码表。 -* 虚拟机会保证一个类的 () 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 () 方法,其它线程都阻塞等待,直到活动线程执行 () 方法完毕 -* 如果在一个类的 () 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽 +美国人: + 我来收集全球所有的字符,统一编号。这套编码叫 Unicode编码(万国码) + 一个英文等于两个字节,一个中文(含繁体)等于两个字节。中文标点占两个字节,英文标点占两个字节 -特别注意:静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问 +​ UTF-8就是变种形式,它也必须兼容ASCII编码表。 +​ UTF-8一个中文一般占3个字节,中文标点占3个。英文字母和数字1个字节 +​ -```java -public class Test { - static { - //i = 0; // 给变量赋值可以正常编译通过 - System.out.print(i); // 这句编译器会提示“非法向前引用” - } - static int i = 1; -} -``` +编码前与编码后的编码集必须一致才不会乱码!! -补充: -接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 () 方法。但两者不同的是,执行接口的 () 方法不需要先执行父接口的 () 方法,只有当父接口中定义的变量使用时,父接口才会初始化;接口的实现类在初始化时也一样不会执行接口的 () 方法 +*** -**** +### IOStream +#### 概述 -##### 时机 +IO输入输出流:输入/输出流 -类的初始化是懒惰的,初始化时机: +* Input:输入 +* Output:输出 -**主动引用**:虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随之发生): +引入:File类只能操作文件对象本身,不能读写文件对象的内容,读写数据内容,应该使用IO流 -* 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化,最常见的生成这 4 条指令的场景是: - * new:使用 new 关键字实例化对象时 - * getstatic:程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池) - * putstatic:程序给类的静态变量赋值 - * invokestatic :调用一个类的静态方法时 -* 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化 -* 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化 -* 当虚拟机启动时,需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类 -* MethodHandle和VarHandle可以看作是轻量级的反射调用机制,而要想使用这2个调用, 就必须先使用findStaticVarHandle来初始化要调用的类 -* 补充:当一个接口中定义了 JDK8 新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化 +IO流是一个水流模型:IO理解成水管,把数据理解成水流 -**被动引用**:所有引用类的方式都不会触发初始化,称为被动引用 +IO流的分类: -* 通过子类引用父类的静态字段,不会导致子类初始化,只会触发父类的初始化 -* 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自Object 的子类,其中包含了数组的属性和方法 -* 常量(final修饰)在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化 +* 按照流的方向分为:输入流,输出流。 + * 输出流:以内存为基准,把内存中的数据写出到磁盘文件或者网络介质中去的流称为输出流 + 输出流的作用:写数据到文件,或者写数据发送给别人 + * 输入流:以内存为基准,把磁盘文件中的数据或者网络中的数据读入到内存中的流称为输入流 + 输入流的作用:读取数据到内存 +* 按照流的内容分为:字节流,字符流 + * 字节流:流中的数据的最小单位是一个一个的字节,这个流就是字节流 + * 字符流:流中的数据的最小单位是一个一个的字符,这个流就是字符流(**针对于文本内容**) +流大体分为四大类: +* 字节输入流:以内存为基准,把磁盘文件中的数据或者网络中的数据以一个一个的字节的形式读入到内存中去的流称为字节输入流 +* 字节输出流:以内存为基准,把内存中的数据以一个一个的字节写出到磁盘文件或者网络介质中去的流称为字节输出流 +* 字符输入流:以内存为基准,把磁盘文件中的数据或者网络中的数据以一个一个的字符的形式读入到内存中去的流称为字符输入流 +* 字符输出流:以内存为基准,把内存中的数据以一个一个的字符写出到磁盘文件或者网络介质中去的流称为字符输出流 -*** +```java +IO流的体系: + 字节流 字符流 + 字节输入流 字节输出流 字符输入流 字符输出流 +InputStream OutputStream Reader Writer (抽象类) +FileInputStream FileOutputStream FileReader FileWriter(实现类) +BufferedInputStream BufferedOutputStream BufferedReader BufferedWriter(实现类缓冲流) + InputStreamReader OutputStreamWriter +ObjectInputStream ObjectOutputStream +``` -##### init +**** -init指的是实例构造器,主要作用是在类实例化过程中执行,执行内容包括成员变量初始化和代码块的执行 -实例化即调用 ()V ,虚拟机会保证这个类的构造方法的线程安全,先为实例变量分配内存空间,再执行赋默认值,然后根据源码中的顺序执行赋初值或代码块,没有成员变量初始化和代码块则不会执行 -类实例化过程:**父类的类构造器() -> 子类的类构造器() -> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成员变量和实例代码块 -> 子类的构造函数** +#### 字节流 + +##### 字节输入 + +FileInputStream文件字节输入流: + +* 作用:以内存为基准,把磁盘文件中的数据按照字节的形式读入到内存中的流 + +* 构造器: + `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)` +```java +public class FileInputStreamDemo01 { + public static void main(String[] args) throws Exception { + // 1.创建文件对象定位dlei01.txt + File file = new File("Day09Demo/src/dlei01.txt"); + // 2.创建一个字节输入流管道与源文件接通 + InputStream is = new FileInputStream(file); + // 3.读取一个字节的编号返回,读取完毕返回-1 + //int code1 = is.read(); // 读取一滴水,一个字节 + //System.out.println((char)code1); + // 4.使用while读取字节数 + // 定义一个整数变量存储字节 + int ch = 0 ; + while((ch = is.read())!= -1){ + System.out.print((char) ch); + } + } +} +``` -*** +一个一个字节读取英文和数字没有问题。但是一旦读取中文输出无法避免乱码,因为会截断中文的字节。一个一个字节的读取数据,性能也较差,所以**禁止使用上面的方案** +采取下面的方案: +```java +public static void main(String[] args) throws Exception { + //简化写法,底层实质上创建了File对象 + InputStream is = new FileInputStream("Day09Demo/src/dlei01.txt"); + byte[] buffer = new byte[3];//开发中使用byte[1024] + int len; + while((len = is.read(buffer)) !=-1){ + // 读取了多少就倒出多少! + String rs = new String(buffer, 0, len); + System.out.print(rs); + } +} +``` -#### 卸载阶段 +```java +//定义一个字节数组与文件的大小刚刚一样大,然后一桶水读取全部字节数据再输出! +//可以避免中文读取输出乱码,但是如果读取的文件过大,会出现内存溢出!! +//字节流并不适合读取文本文件内容输出,读写文件内容建议使用字符流。 +/* + byte[] buffer = new byte[(int) f.length()]; + int len = is.read(buffer); + String rs = new String(buffer); +*/ -时机:执行了System.exit()方法,程序正常执行结束,程序在执行过程中遇到了异常或错误而异常终止,由于操作系统出现错误而导致Java虚拟机进程终止 +File f = new File("Day09Demo/src/dlei03.txt"); +InputStream is = new FileInputStream(f); +byte[] buffer = is.readAllBytes(); +String rs = new String(buffer); +System.out.println(rs); +``` -卸载类即该类的**Class对象被GC**,卸载类需要满足3个要求: -1. 该类的所有的实例对象都已被GC,也就是说堆不存在该类的实例对象 -2. 该类没有在其他任何地方被引用 -3. 该类的类加载器的实例已被GC -在JVM生命周期类,由 JVM 自带的类加载器加载的类是不会被卸载的,由我们自定义的类加载器加载的类是可能被卸载。因为 JVM 会始终引用启动、扩展、系统类加载器,这些类加载器始终引用它们所加载的类,所以这些类始终是可及的 +##### 字节输出 +FileOutputStream文件字节输出流: +* 作用:以内存为基准,把内存中的数据,按照字节的形式写出到磁盘文件中去 -**** +* 构造器: + `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)` : 写一个字节数组的一部分出去 + 参数一,字节数组;参数二:起始字节索引位置,参数三:写多少个字节数出去。 -### 类加载器 +* FileOutputStream字节输出流每次启动写数据的时候都会先清空之前的全部数据,重新写入: + `OutputStream os = new FileOutputStream("Day09Demo/out05")` : 覆盖数据管道 + `OutputStream os = new FileOutputStream("Day09Demo/out05" , true)` : 追加数据的管道 -#### 加载器 +说明: -类与类加载器的关系: +* 字节输出流只能写字节出去,字节输出流默认是**覆盖**数据管道。 +* 换行用: **os.write("\r\n".getBytes());** +* 关闭和刷新:刷新流可以继续使用,关闭包含刷新数据但是流就不能使用了! -* 在JVM中表示两个class对象是否为同一个类存在的两个必要条件: - - 类的完整类名必须一致,包括包名 - - 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同 -* 这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true +```java +OutputStream os = new FileOutputStream("Day09Demo/out05"); +os.write(97);//a +os.write('b'); +os.write("\r\n".getBytes()); +os.write("我爱Java".getBytes()); +os.close(); +``` -类加载器作用:加载字节码到JVM内存,得到Class类的对象 -从 Java 虚拟机规范来讲,只存在以下两种不同的类加载器: -- 启动类加载器(Bootstrap ClassLoader):使用 C++ 实现,是虚拟机自身的一部分 -- 自定义类加载器(User-Defined ClassLoader):Java虚拟机规范**将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器**,使用 Java语言 实现,独立于虚拟机 +##### 文件复制 -从 Java 开发人员的角度看: +思想:字节是计算机中一切文件的组成,所以字节流适合做一切文件的复制 -* **启动类加载器(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 +分析步骤: + (1)创建一个字节输入流管道与源文件接通。 + (2)创建一个字节输出流与目标文件接通。 + (3)创建一个字节数组作为桶 + (4)从字节输入流管道中读取数据,写出到字节输出流管道即可。 + (5)关闭资源! ```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 - +public class CopyDemo01 { + public static void main(String[] args) { + InputStream is = null ; + OutputStream os = null ; + try{ + //(1)创建一个字节输入流管道与源文件接通。 + is = new FileInputStream("D:\\seazean\\图片资源\\meinv.jpg"); + //(2)创建一个字节输出流与目标文件接通。 + os = new FileOutputStream("D:\\seazean\\meimei.jpg"); + //(3)创建一个字节数组作为桶 + byte buffer = new byte[1024]; + //(4)从字节输入流管道中读取数据,写出到字节输出流管道即可 + int len = 0; + while((len = is.read(buffer)) != -1){ + os.write(buffer,0,len); + } + System.out.println("复制完成!"); + }catch (Exception e){ + e.printStackTrace(); + } finally { + /**(5)关闭资源! */ + try{ + if(os!=null)os.close(); + if(is!=null)is.close(); + }catch (Exception e){ + e.printStackTrace(); + } + } + } } ``` @@ -15659,247 +7108,268 @@ public static void main(String[] args) { -#### 加载类 - -ClassLoader类,是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器) - -获取ClassLoader的途径: - -* 获取当前类的ClassLoader:`clazz.getClassLoader()` -* 获取当前线程上下文的ClassLoader:`Thread.currentThread.getContextClassLoader()` -* 获取系统的ClassLoader:`ClassLoader.getSystemClassLoader()` -* 获取调用者的ClassLoader:`DriverManager.getCallerClassLoader()` - -ClassLoader类常用方法: - -| 方法 | 说明 | -| ----------------------------------------------------- | ------------------------------------------------------------ | -| getParent() | 返回该类加载器的超类加载器 | -| loadClass(String name) | 加载名称为name的类,返回结果为java.lang.Class类的实例 | -| findClass(String name) | 查找名称为name的类,返回结果为java.lang.Class类的实例 | -| findLoadedClass(String name) | 查找名称为name的已经被加载过的类,返回结果为java.lang.Class类的实例 | -| defineClass(String name, byte[] b, int off,int len) | 把字节数组b中的内容转换为一个Java类 ,返回结果为java.lang.Class类的实例 | -| resolveClass(Class c) | 连接指定的一个java类 | - - - -*** +#### 字符流 +##### 字符输入 +FileReader:文件字符输入流 -#### 加载模型 + * 作用:以内存为基准,把磁盘文件的数据以字符的形式读入到内存,读取文本文件内容到内存中去。 + * 构造器: + `public FileReader(File file)` : 创建一个字符输入流与源文件对象接通。 + `public FileReader(String filePath)` : 创建一个字符输入流与源文件路径接通。 + * 方法: + `public int read()` : 读取一个字符的编号返回! 读取完毕返回-1 + `public int read(char[] buffer)` : 读取一个字符数组,读取多少个就返回多少个,读取完毕返回-1 + * 结论: + 字符流一个一个字符的读取文本内容输出,可以解决中文读取输出乱码的问题,适合操作文本文件。 + 但是:一个一个字符的读取文本内容性能较差!! + 字符流按照**字符数组循环读取数据**,可以解决中文读取输出乱码的问题,而且性能也较好!! + * **字符流不能复制图片,视频等类型的文件**。字符流在读取完了字节数据后并没有直接往目的地写,而是先查编码表,查到对应的数据就将该数据写入目的地。如果查不到,则码表会将一些未知区域中的数据去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); + int ch; + while((ch = fr.read()) != -1){ + System.out.print((char)ch); + } + } +} +public class FileReaderDemo02 {//字符数组 + public static void main(String[] args) throws Exception { + Reader fr = new FileReader("Day10Demo/src/dlei01.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) { + System.out.print(new String(buffer, 0 , len)); + } + } +} +``` -在JVM中,对于类加载模型提供了三种,分别为全盘加载、双亲委派、缓存机制 -- **全盘加载:**当一个类加载器负责加载某个 Class 时,该 Class 所依赖和引用的其他 Class 也将由该类加载器负责载入,除非显示指定使用另外一个类加载器来载入 -- **双亲委派:**先让父类加载器加载该 Class,在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。简单来说就是,某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,**依次递归**,如果父加载器可以完成类加载任务,就成功返回;只有当父加载器无法完成此加载任务时,才自己去加载 +##### 字符输出 -- **缓存机制:**会保证所有加载过的 Class 都会被缓存,当程序中需要使用某个 Class 时,类加载器先从缓存区中搜寻该 Class,只有当缓存区中不存在该 Class 对象时,系统才会读取该类对应的二进制数据,并将其转换成 Class 对象存入缓冲区中 - - 这就是修改了 Class 后,必须重新启动 JVM,程序所做的修改才会生效的原因 +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"); +fw.write(97); // 字符a +fw.write('b'); // 字符b +fw.write("Java是最优美的语言!"); +fw.write("\r\n"); +fw.close; +``` -*** +**** -##### 双亲委派 -双亲委派模型 (Parents Delegation Model):该模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器,这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance) +#### 缓冲流 -工作过程:一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载 +##### 基本介绍 -双亲委派机制的优点: +作用:缓冲流可以提高字节流和字符流的读写数据的性能。 -* 可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性 +缓冲流分为四类: + (1)BufferedInputStream:字节缓冲输入流,可以提高字节输入流读数据的性能。 + (2)BufferedOutStream: 字节缓冲输出流,可以提高字节输出流写数据的性能。 + (3)BufferedReader: 字符缓冲输入流,可以提高字符输入流读数据的性能。 + (4)BufferedWriter: 字符缓冲输出流,可以提高字符输出流写数据的性能。 -* Java类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一 -* 保护程序安全,防止类库的核心API被随意篡改 - 例如:在工程中新建java.lang包,接着在该包下新建String类,并定义main函数 +##### 字节缓冲输入流 - ```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 classLoader)得到加载,启动类加载器优先级更高,在核心 jre 库中有其相同名字的类文件,但该类中并没有 main 方法 +字节缓冲输入流:BufferedInputStream -**源码分析: ** +作用:可以把低级的字节输入流包装成一个高级的缓冲字节输入流管道, 提高字节输入流读数据的性能 -```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) { - // ClassNotFoundException thrown if class not found - // from the non-null parent class loader - } +构造器:`public BufferedInputStream(InputStream in)` - if (c == null) { - // 如果调用父类的类加载器无法对类进行加载,则用自己的findClass(name)方法进行加载 - long t1 = System.nanoTime(); - c = findClass(name); +原理:缓冲字节输入流管道自带了一个 8KB 的缓冲池,每次可以直接借用操作系统的功能最多提取 8KB 的数据到缓冲池中去,以后我们直接从缓冲池读取数据,所以性能较好 - // 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) { - resolveClass(c); +```java +public class BufferedInputStreamDemo01 { + public static void main(String[] args) throws Exception { + // 1.定义一个低级的字节输入流与源文件接通 + InputStream is = new FileInputStream("Day10Demo/src/dlei04.txt"); + // 2.把低级的字节输入流包装成一个高级的缓冲字节输入流。 + BufferInputStream bis = new BufferInputStream(is); + // 3.定义一个字节数组按照循环读取。 + byte[] buffer = new byte[1024]; + int len; + while((len = bis.read(buffer)) != -1){ + String rs = new String(buffer, 0 , len); + System.out.print(rs); } - return c; } } ``` -**** - - +##### 字节缓冲输出流 -##### 破坏双亲 +字节缓冲输出流:BufferedOutputStream -破坏双亲委派模型有两种方式: +作用:可以把低级的字节输出流包装成一个高级的缓冲字节输出流,从而提高写数据的性能 -* 引入线程上下文类加载器 +构造器:`public BufferedOutputStream(OutputStream os)` - Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI 等。这些 SPI 接口由 Java 核心库来提供,而 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径 classpath 里,SPI 接口中的代码需要加载具体的实现类: +原理:缓冲字节输出流自带了8KB缓冲池,数据就直接写入到缓冲池中去,性能极高了 - * SPI 的接口是 Java核心库的一部分,是由引导类加载器来加载的 - * SPI的实现类是由系统类加载器来加载的,引导类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader 无法委派 AppClassLoader 来加载类。 +```java +public class BufferedOutputStreamDemo02 { + public static void main(String[] args) throws Exception { + // 1.写一个原始的字节输出流 + OutputStream os = new FileOutputStream("Day10Demo/src/dlei05.txt"); + // 2.把低级的字节输出流包装成一个高级的缓冲字节输出流 + BufferedOutputStream bos = new BufferedOutputStream(os); + // 3.写数据出去 + bos.write('a'); + bos.write(100); + bos.write("我爱中国".getBytes()); + bos.close(); + } +} - JDK 开发人员引入了线程上下文类加载器(Thread Context ClassLoader),这种类加载器可以通过 Thread 类的 setContextClassLoader 方法进行设置线程上下文类加载器,在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器,使 Bootstrap Classloader 加载器拿到了 Application ClassLoader 加载器应该加载的类,破坏了双亲委派模型 +``` -* 自定义ClassLoader - * 如果不想破坏双亲委派模型,只需要重写 findClass 方法 - * 如果想要去破坏双亲委派模型,需要去**重写 loadClass **方法 +##### 字节流的性能分析 +利用字节流的复制统计各种写法形式下缓冲流的性能执行情况。 -参考文章:https://www.jianshu.com/p/4132d82ca3a6 +复制流: + (1)使用低级的字节流按照一个一个字节的形式复制文件。 + (2)使用低级的字节流按照一个一个字节数组的形式复制文件。 + (3)使用高级的缓冲字节流按照一个一个字节的形式复制文件。 + (4)使用高级的缓冲字节流按照一个一个字节数组的形式复制文件。 +高级的缓冲字节流按照一个一个字节数组的形式复制文件,性能最高,建议使用 -*** +**** -#### 沙箱机制 -沙箱机制:将 Java 代码限定在虚拟机特定的运行范围中,并且严格限制代码对本地系统资源访问,来保证对代码的有效隔离,防止对本地系统造成破坏 +##### 字符缓冲输入流 -沙箱**限制系统资源访问**,包括CPU、内存、文件系统、网络,不同级别的沙箱对资源访问的限制也不一样 +字符缓冲输入流:BufferedReader -举例:自定义 String 类,但是在加载自定义 String 类的时候会先使用引导类加载器加载,而引导类加载器在加载过程中会先加载 jdk 自带的文件(rt.jar包中的 java\lang\String.class),报错信息说没有 main 方法就是因为加载的是 rt.jar 包中的 String 类,这样可以保证对 java 核心源代码的保护 +作用:字符缓冲输入流把字符输入流包装成高级的缓冲字符输入流,可以提高字符输入流读数据的性能。 +构造器:`public BufferedReader(Reader reader)` +原理:缓冲字符输入流默认会有一个8K的字符缓冲池,可以提高读字符的性能 -*** +按照行读取数据的功能:`public String readLine()` 读取一行数据返回,读取完毕返回null +```java +public static void main(String[] args) throws Exception { + // 1.定义一个原始的字符输入流读取源文件 + Reader fr = new FileReader("Day10Demo/src/dlei06.txt"); + // 2.把低级的字符输入流管道包装成一个高级的缓冲字符输入流管道 + BufferedReader br = new BufferedReader(fr); + // 定义一个字符串变量存储每行数据 + String line; + while((line = br.readLine()) != null){ + System.out.println(line); + } + br.close(); + //淘汰数组循环读取 + //char[] buffer = new char[1024]; + //int len; + //while((len = br.read(buffer)) != -1){ + //System.out.println(new String(buffer , 0 , len)); +} +``` -#### 自定义 -对于自定义类加载器的实现,只需要继承 ClassLoader 类,覆写 findClass 方法即可 +##### 字符缓冲输出流 -作用:隔离加载类、修改类加载的方式、拓展加载源、防止源码泄漏 +符缓冲输出流:BufferedWriter -```java -//自定义类加载器,读取指定的类路径classPath下的class文件 -public class MyClassLoader extends ClassLoader{ - private String classPath; +作用:把低级的字符输出流包装成一个高级的缓冲字符输出流,提高写字符数据的性能。 - public MyClassLoader(String classPath) { - this.classPath = classPath; - } +构造器:`public BufferedWriter(Writer writer)` - @Override - protected Class findClass(String name) throws ClassNotFoundException { - byte[] data = new byte[0]; - try { - data = loadByte(name); - } catch (Exception e) { - e.printStackTrace(); - } - return defineClass(name, data, 0, data.length); - } + 原理:高级的字符缓冲输出流多了一个8k的字符缓冲池,写数据性能极大提高了 - private byte[] loadByte(String name) throws Exception { - name = name.replaceAll("\\.", "/"); - FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class"); - int len = fis.available(); - byte[] data = new byte[len]; - fis.read(data); - fis.close(); - return data; - } -} -``` +字符缓冲输出流多了一个换行的特有功能:`public void newLine()` **新建一行** ```java -public class ClassLoaderTest { - public static void main(String[] args) throws Exception { - MyClassLoader classLoader = new MyClassLoader("D:\\workspace\\project\\src\\main\\java"); - Class clazz = classLoader.loadClass("com.demo.User"); - System.out.println(clazz.getClassLoader().getClass().getName()); - } +public static void main(String[] args) throws Exception { + Writer fw = new FileWriter("Day10Demo/src/dlei07.txt",true);//追加 + BufferedWriter bw = new BufferedWriter(fw); + + bw.write("我爱学习Java"); + bw.newLine();//换行 + bw.close(); } -//当存在.java文件时,根据双亲委派机制,显示当前类加载器为AppClassLoader -//当将.java文件删除时,则显示使用的是自定义的类加载器 ``` +*** -*** +##### 高效原因 +字符型缓冲流高效的原因: -## 运行机制 +* BufferedReader:每次调用read方法,只有第一次从磁盘中读取了8192(**8k**)个字符,存储到该类型对象的缓冲区数组中,将其中一个返回给调用者,再次调用read方法时,就不需要访问磁盘,直接从缓冲区中拿出一个数据即可,提升了效率 +* BufferedWriter:每次调用write方法,不会直接将字符刷新到文件中,而是存储到字符数组中,等字符数组写满了,才一次性刷新到文件中,减少了和磁盘交互的次数,提升了效率 -### 执行过程 +字节型缓冲流高效的原因: - Java文件编译执行的过程: +* BufferedInputStream:在该类型中准备了一个数组,存储字节信息,当外界调用read()方法想获取一个字节的时候,该对象从文件中一次性读取了8192个字节到数组中,只返回了第一个字节给调用者。将来调用者再次调用read方法时,当前对象就不需要再次访问磁盘,只需要从数组中取出一个字节返回给调用者即可,由于读取的是数组,所以速度非常快。当8192个字节全都读取完成之后,再需要读取一个字节,就得让该对象到文件中读取下一个8192个字节 +* BufferedOutputStream:在该类型中准备了一个数组,存储字节信息,当外界调用write方法想写出一个字节的时候,该对象直接将这个字节存储到了自己的数组中,而不刷新到文件中。一直到该数组所有8192个位置全都占满,该对象才把这个数组中的所有数据一次性写出到目标文件中。如果最后一次循环过程中,没有将数组写满,最终在关闭流对象的时候,也会将该数组中的数据刷新到文件中。 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java文件编译执行的过程.png) -- 类加载器:用于装载字节码文件(.class文件) -- 运行时数据区:用于分配存储空间 -- 执行引擎:执行字节码文件或本地方法 -- 垃圾回收器:用于对JVM中的垃圾内容进行回收 + +注意:**字节流和字符流,都是装满时自动写出,或者没满时手动flush写出,或close时刷新写出** @@ -15907,188 +7377,314 @@ public class ClassLoaderTest { -### 执行引擎 +#### 转换流 -#### 基本介绍 +##### 乱码问题 -执行引擎:Java虚拟机的核心组成部分之一,JVM的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,需要执行引擎将字节码指令解释/编译为对应平台上的本地机器指令 +``` +字符流读取: + 代码编码 文件编码 中文情况。 + UTF-8 UTF-8 不乱码! + GBK GBK 不乱码! + UTF-8 GBK 乱码! +``` -虚拟机是一个相对于“物理机”的概念,这两种机器都有代码执行能力: +如果代码编码和读取的文件编码一致。字符流读取的时候不会乱码。 +如果代码编码和读取的文件编码不一致。字符流读取的时候会乱码。 -* 物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上 -* 虚拟机的执行引擎是由软件自行实现的,可以不受物理条件制约地定制指令集与执行引擎的结构体系 -编译过程中的编译器: -* 前端编译器: Sun的Javac、 Eclipse JDT中的增量式编译器ECJ,把源代码编译为字节码文件.class -* 后端运行期编译器:HotSpot VM的C1、C2编译器,也就是JIT编译器 -* 静态提前编译器:AOT编译器,直接把源代码编译成本地机器代码 +##### 字符输入转换流 + +字符输入转换流InputStreamReader + +作用:解决字符流读取不同编码的乱码问题,把原始的**字节流**按照默认的编码或指定的编码**转换成字符输入流** -Java是**半编译半解释型语言**,将解释执行与编译执行二者结合起来进行: +构造器: -* 解释器:根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行 -* 即时编译器(JIT : Just In Time Compiler):虚拟机运行时将源代码直接编译成**和本地机器平台相关的机器码**后再执行,并存入Code Cache,下次遇到相同的代码直接执行,效率高(一次编译,到处运行) +* `public InputStreamReader(InputStream is)` : 使用当前代码默认编码(UTF-8)转换成字符流 +* `public InputStreamReader(InputStream is,String charset)` : 指定编码把字节流转换成字符流 +```java +public class InputStreamReaderDemo{ + public static void main(String[] args) throws Exception { + // 1.提取GBK文件的原始字节流 + InputStream is = new FileInputStream("D:\\seazean\\Netty.txt"); + // 2.把原始字节输入流通过转换流,转换成 字符输入转换流InputStreamReader + InputStreamReader isr = new InputStreamReader(is,"GBK"); + // 3.包装成缓冲流 + BufferedReader br = new BufferedReader(isr); + //循环读取 + String line; + while((line = br.readLine()) != null){ + System.out.println(line); + } + } +} +``` -*** +##### 字符输出转换流 +字符输出转换流:OutputStreamWriter -#### 执行方式 +作用:可以指定编码**把字节输出流转换成字符输出流**,可以指定写出去的字符的编码 -HotSpot VM 采用**解释器与即时编译器并存的架构**,解释器和即时编译器能够相互协作,去选择最合适的方式来权衡编译本地代码和直接解释执行代码的时间 +构造器: -HostSpot JVM的默认执行方式: +* `public OutputStreamWriter(OutputStream os)` : 用默认编码UTF-8把字节输出流转换成字符输出流 +* `public OutputStreamWriter(OutputStream os ,String charset)` : 指定编码把字节输出流转换成 -* 当程序启动后,解释器可以马上发挥作用立即执行,省去编译器编译的时间(解释器存在的**必要性**) -* 随着程序运行时间的推移,即时编译器逐渐发挥作用,根据热点探测功能,将有价值的字节码编译为本地机器指令,以换取更高的程序执行效率 +```Java +OutputStream os = new FileOutputStream("Day10Demo/src/dlei07.txt"); +OutputStreamWriter osw = new OutputStreamWriter(os,"GBK"); +osw.write("我在学习Java"); +osw.close(); +``` -HotSpot VM 可以通过VM参数设置程序执行方式: -- -Xint:完全采用解释器模式执行程序 -- -Xcomp:完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行 -- -Xmixed:采用解释器+即时编译器的混合模式共同执行程序 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-执行引擎工作流程.png) +**** -*** +#### 序列化 +##### 介绍 +对象序列化:把Java对象转换成字节序列的过程,将对象写入到IO流中。 对象 => 文件中 -#### 热点探测 +对象反序列化:把字节序列恢复为Java对象的过程,从IO流中恢复对象。 文件中 => 对象 -热点代码:被 JIT 编译器编译的字节码,根据代码被调用执行的频率而定 +transient 关键字修饰的成员变量,将不参与序列化! -* 一个被多次调用的方法或者一个循环次数较多的循环体都可以被称之为热点代码 -* 这种编译方式发生在方法的执行过程中,也称为栈上替换,简称OSR (On StackReplacement) 编译 -OSR 替换循环代码体的入口,C1、C2 替换的是方法调用的入口,OSR 编译后会出现方法的整段代码被编译了,但是只有循环体部分才执行编译后的机器码,其他部分仍是解释执行 -热点探测:JIT编译器在运行时会针热点代码做出深度优化,将其直接编译为对应平台的本地机器指令进行缓存,以提升Java程序的执行性能 +##### 序列化 -CodeCache 用于缓存编译后的机器码,动态生成的代码和本地方法代码(JNI),如果 CodeCache 区域被占满,编译器被停用,字节码将不会编译为机器码,应用程序继续运行,但运行速度会降低一个数量级,严重影响系统性能 +对象序列化流(对象字节输出流):ObjectOutputStream -HotSpot VM采用的热点探测方式是基于计数器的热点探测,为每一个方法都建立2个不同类型的计数器:方法调用计数器 (Invocation Counter) 和回边计数器 (BackEdge Counter) +作用:把内存中的Java对象数据保存到文件中去 -* 方法调用计数器:用于统计方法被调用的次数,默认阈值在Client 模式 下是1500 次,在Server 模式下是10000 次,超过这个阈值,就会触发JIT编译,阈值可以通过虚拟机参数 `-XX:CompileThreshold` 设置 +构造器:`public ObjectOutputStream(OutputStream out)` - 工作流程:当一个方法被调用时, 会先检查该方法是否存在被JIT编译过的版本,存在则使用编译后的本地代码来执行;如果不存在则将此方法的调用计数器值加1,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阈值,如果超过阈值会向即时编译器提交一个该方法的代码编译请求 +序列化方法:`public final void writeObject(Object obj)` -* 回边计数器:统计一个方法中循环体代码执行的次数,在字节码中控制流向后跳转的指令称为回边 +注意:对象如果想参与序列化,对象必须实现序列化接口 **implements Serializable** ,否则序列化失败! +```java +public class SerializeDemo01 { + public static void main(String[] args) throws Exception { + // 1.创建User用户对象 + User user = new User("seazean","980823","七十一"); + // 2.创建低级的字节输出流通向目标文件 + OutputStream os = new FileOutputStream("Day10Demo/src/obj.dat"); + // 3.把低级的字节输出流包装成高级的对象字节输出流ObjectOutputStream + ObjectOutputStream oos = new ObjectOutputStream(os); + // 4.通过对象字节输出流序列化对象: + oos.writeObject(user); + // 5.释放资源 + oos.close(); + System.out.println("序列化对象成功~~~~"); + } +} +class User implements Serializable { + // 加入序列版本号 + private static final long serialVersionUID = 1L; -*** + private String loginName; + private transient String passWord; + private String userName; + ///get+set +} +``` -#### 分层编译 +**** -HotSpot VM 内嵌有两个JIT编译器,分别为 Client Compiler 和 Server Compiler,简称为C1编译器和C2编译器 -C1编译器会对字节码进行简单可靠的优化,耗时短,以达到更快的编译速度,C1编译器的优化方法: -* 方法内联:**将引用的函数代码编译到引用点处**,这样可以减少栈帧的生成,减少参数传递以及跳转过程 +##### 反序列 - 方法内联能够消除方法调用的固定开销,任何方法除非被内联,否则调用都会有固定开销,来源于保存程序在该方法中的执行位置,以及新建、压入和弹出新方法所使用的栈帧。 +对象反序列化(对象字节输入流):ObjectInputStream - ```java - private static int square(final int i) { - return i * i; - } - System.out.println(square(9)); - ``` +作用:读取序列化的对象文件恢复到Java对象中 - square 是热点方法,会进行内联,把方法内代码拷贝粘贴到调用者的位置: +构造器:`public ObjectInputStream(InputStream is)` - ```java - System.out.println(9 * 9); - ``` +方法:`public final Object readObject()` - 还能够进行常量折叠(constant folding)的优化: +序列化版本号:`private static final long serialVersionUID = 2L` +说明:序列化使用的版本号和反序列化使用的版本号一致才可以正常反序列化,否则报错 - ```java - System.out.println(81); - ``` +```java +public class SerializeDemo02 { + public static void main(String[] args) throws Exception { + InputStream is = new FileInputStream("Day10Demo/src/obj.dat"); + ObjectInputStream ois = new ObjectInputStream(is); + User user = (User)ois.readObject();//反序列化 + System.out.println(user); + System.out.println("反序列化完成!"); + } +} +class User implements Serializable { + // 加入序列版本号 + private static final long serialVersionUID = 1L; + //........ +} +``` -* 冗余消除:根据运行时状况进行代码折叠或削除 -* 内联缓存:是一种加快动态绑定的优化技术 -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) +打印流 PrintStream / PrintWriter -* 1 层,使用 C1 即时编译器编译执行(不带 profiling) +打印流的作用: -* 2 层,使用 C1 即时编译器编译执行(带基本的 profiling) +* 可以方便,快速的写数据出去,可以实现打印什么类型,就是什么类型 +* PrintStream/PrintWriter 不光可以打印数据,还可以写字节数据和字符数据出去 +* **System.out.print() 底层基于打印流实现的** -* 3 层,使用 C1 即时编译器编译执行(带完全的 profiling) +构造器: -* 4 层,使用 C2 即时编译器编译执行(C1 和 C2 协作运行) +* `public PrintStream(OutputStream os)` +* `public PrintStream(String filepath)` - 说明:profiling 是指在运行过程中收集一些程序执行状态的数据,例如方法的调用次数,循环的回边次数等 +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"); + ps.println(任何类型的数据); + ps.print(不换行); + ps.write("我爱你".getBytes()); + ps.close(); + } +} +public class PrintStreamDemo02 { + public static void main(String[] args) throws Exception { + System.out.println("==seazean0=="); + PrintStream ps = new PrintStream("Day10Demo/src/log.txt"); + System.setOut(ps); // 让系统的输出流向打印流 + //不输出在控制台,输出到文件里 + System.out.println("==seazean1=="); + System.out.println("==seazean2=="); + } +} +``` -*** +*** -#### 其他编译 -Graal编译器:JDK10 HotSpot 加入的一个全新的即时编译器,编译效果短短几年时间就追平了C2编译器 -AOT编译器:JDK9引入,静态提前编译器 (Ahead Of Time Compiler),程序运行之前便将字节码转换为机器码的过程,是与即时编译相对立的一个概念,即时编译指的是在程序的运行过程中,将字节码转换为机器码 +### Close -* 优点:Java虚拟机加载已经预编译成二进制库,可以直接执行,不必等待即时编译器的预热,减少Java应用第一次运行慢的现象 -* 缺点: - * 破坏了java"一次编译,到处运行”,必须为每个不同硬件、SS编译对应的发行包 - * 降低了Java链接过程的动态性,加载的代码在编译期就必须全部已知 +try-with-resources: +```java +try( + // 这里只能放置资源对象,用完会自动调用close()关闭 +){ +}catch(Exception e){ + e.printStackTrace(); +} +``` -*** +资源类一定是实现了 Closeable 接口,实现这个接口的类就是资源 +有 close() 方法,try-with-resources 会自动调用它的 close() 关闭资源 +```java +try( + /** (1)创建一个字节输入流管道与源文件接通。 */ + InputStream is = new FileInputStream("D:\\seazean\\图片资源\\meinv.jpg"); + /** (2)创建一个字节输出流与目标文件接通。*/ + OutputStream os = new FileOutputStream("D:\\seazean\\meimei.jpg"); + /** (5)关闭资源!是自动进行的 */ +){ + byte[] buffer = new byte[1024]; + int len = 0; + while((len = is.read(buffer)) != -1){ + os.write(buffer, 0 , len); + } + System.out.println("复制完成!"); +}catch (Exception e){ + e.printStackTrace(); +} +``` -#### 语言发展 -机器码:各种用二进制编码方式表示的指令,与CPU紧密相关,所以不同种类的CPU对应的机器指令不同 -指令:指令就是把机器码中特定的0和1序列,简化成对应的指令,例如mov,inc等,可读性稍好,但是不同的硬件平台的同一种指令(比如mov),对应的机器码也可能不同 +*** -指令集:不同的硬件平台支持的指令是有区别的,每个平台所支持的指令,称之为对应平台的指令集 -- x86指令集,对应的是x86架构的平台 -- ARM指令集,对应的是ARM架构的平台 -汇编语言:用助记符代替机器指令的操作码,用地址符号或标号代替指令或操作数的地址 +### Properties -* 在不同的硬件平台,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令 -* 计算机只认识指令码,汇编语言编写的程序也必须翻译成机器指令码,计算机才能识别和执行 +Properties:属性集对象。就是一个Map集合,一个键值对集合 -高级语言:为了使计算机用户编程序更容易些,后来就出现了各种高级计算机语言 +核心作用:Properties代表的是一个属性文件,可以把键值对数据存入到一个属性文件 -字节码:是一种中间状态(中间码)的二进制代码,比机器码更抽象,需要直译器转译后才能成为机器码 +属性文件:后缀是.properties结尾的文件,里面的内容都是 key=value -* 字节码为了实现特定软件运行和软件环境,与硬件环境无关 -* 通过编译器和虚拟机器实现,编译器将源码编译成字节码,虚拟机器将字节码转译为可以直接执行的指令 +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) | 保存数据到属性文件中去 | +````java +public class PropertiesDemo01 { + public static void main(String[] args) throws Exception { + // a.创建一个属性集对象:Properties的对象。 + Properties properties = new Properties();//{} + properties.setProperty("admin" , "123456"); + // b.把属性集对象的数据存入到属性文件中去(重点) + OutputStream os = new FileOutputStream("Day10Demo/src/users.properties"); + properties.store(os,"i am very happy!!我保存了用户数据!"); + //参数一:被保存数据的输出管道 + //参数二:保存心得。就是对象保存的数据进行解释说明! + } +} +```` +````java +public class PropertiesDemo02 { + public static void main(String[] args) throws Exception { + Properties properties = new Properties();//底层基于map集合 + properties.load(new FileInputStream("Day10Demo/src/users.properties")); + System.out.println(properties); + System.out.println(properties.getProperty("admin")); + + Set set = properties.stringPropertyNames(); + for (String s : set) { + String value = properties.getProperty(s); + System.out.println(s + value); + } + } +} +```` @@ -16096,24 +7692,26 @@ AOT编译器:JDK9引入,静态提前编译器 (Ahead Of Time Compiler),程 -### 方法调用 - -#### 方法识别 +### RandomIO -Java 虚拟机识别方法的关键在于类名、方法名以及方法描述符(method descriptor) +RandomAccessFile类:该类的实例支持读取和写入随机访问文件 -* 方法描述符是由方法的参数类型以及返回类型所构成,也叫方法签名 -* 在同一个类中,如果同时出现多个名字相同且描述符也相同的方法,那么 Java 虚拟机会在类的验证阶段报错 +构造器: +RandomAccessFile(File file, String mode):创建随机访问文件流,从File参数指定的文件读取,可选择写入 +RandomAccessFile(String name, String mode):创建随机访问文件流,从指定名称文件读取,可选择写入文件 -JVM根据名字和描述符来判断的,只要返回值不一样,其它完全一样,在JVM中是允许的,但Java语言不允许 +常用方法: +`public void seek(long pos)` : 设置文件指针偏移,从该文件开头测量,发生下一次读取或写入(插入+覆盖) +`public void write(byte[] b)` : 从指定的字节数组写入 b.length个字节到该文件 +`public int read(byte[] b)` : 从该文件读取最多b.length个字节的数据到字节数组 ```java -// 返回值类型不同,编译阶段直接报错 -public static Integer invoke(Object... args) { - return 1; -} -public static int invoke(Object... args) { - return 2; +public static void main(String[] args) throws Exception { + RandomAccessFile rf = new RandomAccessFile(new File(),"rw"); + rf.write("hello world".getBytes()); + rf.seek(5);//helloxxxxld + rf.write("xxxx".getBytes()); + rf.close(); } ``` @@ -16123,40 +7721,42 @@ public static int invoke(Object... args) { -#### 调用机制 - -方法调用并不等于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本,不是方法的具体运行过程 - -在JVM中,将符号引用转换为直接引用有两种机制: - -- 静态链接:当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变,将调用方法的符号引用转换为直接引用的过程称之为静态链接(类加载的解析阶段) -- 动态链接:如果被调用的方法在编译期无法被确定下来,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此被称为动态链接(初始化后的解析阶段) - -对应的方法的绑定(分配)机制为:静态绑定和动态绑定。绑定是一个字段、方法或者类从符号引用被替换为直接引用的过程,仅发生一次 - -- 静态绑定:被调用的目标方法在编译期可知,且运行期保持不变,将这个方法与所属的类型进行绑定 -- 动态绑定:被调用的目标方法在编译期无法确定,只能在程序运行期根据实际的类型绑定相关的方法 +### Commons -* Java 编译器已经区分了重载的方法(静态绑定和动态绑定),因此可以认为虚拟机中不存在重载 +commons-io 是apache开源基金组织提供的一组有关IO操作的类库,可以挺提高IO功能开发的效率。 -非虚方法: +commons-io 工具包提供了很多有关 IO 操作的类: -- 非虚方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的 -- 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法 -- 所有普通成员方法和被重写的方法都是虚方法 +| 包 | 功能描述 | +| ----------------------------------- | :------------------------------------------- | +| 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 可以方便的复制文件和文件夹 -- 在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之则是动态类型语言 +```java +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")); + // 2.完成文件复制到某个文件夹下! + FileUtils.copyFileToDirectory(new File("Day13Demo/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")); + } +} +``` -- **Java是静态类型语言**(尽管lambda表达式为其增加了动态特性),js,python是动态类型语言 - ```java - String s = "abc"; //Java - info = "abc"; //Python - ``` @@ -16164,345 +7764,526 @@ public static int invoke(Object... args) { -#### 调用指令 - -##### 五种指令 - -普通调用指令: - -- invokestatic:调用静态方法 -- invokespecial:调用私有实例方法、构造器,和父类的实例方法或构造器,以及所实现接口的默认方法 -- invokevirtual:调用所有虚方法 -- invokeinterface:调用接口方法 - -动态调用指令: - -- invokedynamic:动态解析出需要调用的方法, - - Java7为了实现动态类型语言支持而引入了该指令,但是并没有提供直接生成invokedynamic指令的方法,需要借助ASM这种底层字节码工具来产生invokedynamic指令 - - Java8的Lambda表达式的出现,invokedynamic指令在Java中才有了直接生成方式 +## 反射 -指令对比: +### 测试框架 -- 普通调用指令固化在虚拟机内部,方法的调用执行不可干预,根据方法的符号引用链接到具体的目标方法 -- 动态调用指令支持用户确定方法 -- invokestatic 和 invokespecial 指令调用的方法称为非虚方法,虚拟机能够直接识别具体的目标方法 -- invokevirtual 和 invokeinterface 指令调用的方法称为虚方法,虚拟机需要在执行过程中根据调用者的动态类型来确定目标方法 +> 单元测试是指程序员写的测试代码给自己的类中的方法进行预期正确性的验证。 +> 单元测试一旦写好了这些测试代码,就可以一直使用,可以实现一定程度上的自动化测试。 -指令说明: +单元测试的经典框架:Junit -- 如果虚拟机能够确定目标方法有且仅有一个,比如说目标方法被标记为 final,那么可以不通过动态类型,直接确定目标方法 -- 普通成员方法是由 invokevirtual 调用,属于**动态绑定**,即支持多态 +* Junit : 是Java语言编写的第三方单元测试框架,可以帮助我们方便快速的测试我们代码的正确性。 +* 单元测试: + * 单元:在Java中,一个类就是一个单元 + * 单元测试:Junit编写的一小段代码,用来对某个类中的某个方法进行功能测试或业务逻辑测试 +Junit单元测试框架的作用: +* 用来对类中的方法功能进行有目的的测试,以保证程序的正确性和稳定性 +* 能够**独立的**测试某个方法或者所有方法的预期正确性 -*** +测试方法注意事项:**必须是public修饰的,没有返回值,没有参数,使用注解@Test修饰** +Junit常用注解(Junit 4.xxxx版本),@Test 测试方法: +* @Before:用来修饰实例方法,该方法会在每一个测试方法执行之前执行一次 +* @After:用来修饰实例方法,该方法会在每一个测试方法执行之后执行一次 +* @BeforeClass:用来静态修饰方法,该方法会在所有测试方法之前**只**执行一次 +* @AfterClass:用来静态修饰方法,该方法会在所有测试方法之后**只**执行一次 -##### 符号引用 +Junit常用注解(Junit5.xxxx版本),@Test 测试方法: -在编译过程中,虚拟机并不知道目标方法的具体内存地址,Java 编译器会暂时用符号引用来表示该目标方法,这一符号引用包括目标方法所在的类或接口的名字,以及目标方法的方法名和方法描述符 +* @BeforeEach:用来修饰实例方法,该方法会在每一个测试方法执行之前执行一次 +* @AfterEach:用来修饰实例方法,该方法会在每一个测试方法执行之后执行一次 +* @BeforeAll:用来静态修饰方法,该方法会在所有测试方法之前只执行一次 +* @AfterAll:用来静态修饰方法,该方法会在所有测试方法之后只执行一次 -* 对于静态绑定的方法调用而言,实际引用是一个指向方法的指针 -* 对于需要动态绑定的方法调用而言,实际引用则是一个方法表的索引 +作用: -符号引用存储在方法区常量池中,根据目标方法是否为接口方法,分为接口符号引用和非接口符号引用: +* 开始执行的方法:初始化资源 +* 执行完之后的方法:释放资源 ```java -Constant pool: -... - #16 = InterfaceMethodref #27.#29 // 接口 -... - #22 = Methodref #1.#33 // 非接口 -... +public class UserService { + public String login(String loginName , String passWord){ + if("admin".equals(loginName)&&"123456".equals(passWord)){ + return "success"; + } + return "用户名或者密码错误!"; + } + public void chu(int a , int b){ + System.out.println(a / b); + } +} ``` -对于非接口符号引用,假定该符号引用所指向的类为 C,则 Java 虚拟机会按照如下步骤进行查找: - -1. 在 C 中查找符合名字及描述符的方法 -2. 如果没有找到,在 C 的父类中继续搜索,直至 Object 类 -3. 如果没有找到,在 C 所直接实现或间接实现的接口中搜索,这一步搜索得到的目标方法必须是非私有、非静态的。如果有多个符合条件的目标方法,则任意返回其中一个 - -对于接口符号引用,假定该符号引用所指向的接口为 I,则 Java 虚拟机会按照如下步骤进行查找: - -1. 在 I 中查找符合名字及描述符的方法 -2. 如果没有找到,在 Object 类中的公有实例方法中搜索 -3. 如果没有找到,则在 I 的超接口中搜索,这一步的搜索结果的要求与非接口符号引用步骤 3 的要求一致 - - - -*** +```java +//测试方法的要求:1.必须public修饰 2.没有返回值没有参数 3. 必须使注解@Test修饰 +public class UserServiceTest { + // @Before:用来修饰实例方法,该方法会在每一个测试方法执行之前执行一次。 + @Before + public void before(){ + System.out.println("===before==="); + } + // @After:用来修饰实例方法,该方法会在每一个测试方法执行之后执行一次。 + @After + public void after(){ + System.out.println("===after==="); + } + // @BeforeClass:用来静态修饰方法,该方法会在所有测试方法之前只执行一次。 + @BeforeClass + public static void beforeClass(){ + System.out.println("===beforeClass==="); + } + // @AfterClass:用来静态修饰方法,该方法会在所有测试方法之后只执行一次。 + @AfterClass + public static void afterClass(){ + System.out.println("===afterClass==="); + } + @Test + public void testLogin(){ + UserService userService = new UserService(); + String rs = userService.login("admin","123456"); + /**断言预期结果的正确性。 + * 参数一:测试失败的提示信息。 + * 参数二:期望值。 + * 参数三:实际值 + */ + Assert.assertEquals("登录业务功能方法有错误,请检查!","success",rs); + } + @Test + public void testChu(){ + UserService userService = new UserService(); + userService.chu(10 , 0); + } +} +``` -##### 执行流程 -```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 -``` +### 介绍反射 -- new 是在堆中创建对象,执行成功会将**对象引用**压入操作数栈 -- dup 是复制操作数栈栈顶的内容,本例即为对象引用,为什么需要两份引用呢? - - 一个要配合 invokespecial 调用该对象的构造方法 :()V (会消耗掉栈顶一个引用) - - 一个要配合 astore_1 赋值给局部变量 -- `d.test4()` 是通过**对象引用**调用一个静态方法,在调用invokestatic 之前执行了 pop 指令,把对象引用从操作数栈弹掉 - - 不建议使用`对象.静态方法()`的方式调用静态方法,多了aload和pop指令 - - 成员方法与静态方法调用的区别是:执行方法前是否需要对象引用 +反射是指对于任何一个类,在"运行的时候"都可以直接得到这个类全部成分 +* 构造器对象:Constructor +* 成员变量对象:Field +* 成员方法对象:Method -*** +核心思想:在运行时获取类编译后的字节码文件对象,然后解析类中的全部成分 +反射提供了一个Class类型:HelloWorld.java → javac → HelloWorld.class +* `Class c = HelloWorld.class;` -#### 多态原理 +注意:反射是工作在**运行时**的技术,只有运行之后才会有 class 类对象 -##### 执行原理 +作用:可以在运行时得到一个类的全部成分然后操作,破坏封装性,也可以破坏泛型的约束性。 -Java 虚拟机中关于方法重写的判定基于方法描述符,如果子类定义了与父类中非私有、非静态方法同名的方法,只有当这两个方法的参数类型以及返回类型一致,Java 虚拟机才会判定为重写。 +**反射的优点:** -**理解多态**: +- 可扩展性:应用程序可以利用全限定名创建可扩展对象的实例,来使用来自外部的用户自定义类 +- 类浏览器和可视化开发环境:一个类浏览器需要可以枚举类的成员,可视化开发环境(如 IDE)可以从利用反射中可用的类型信息中受益,以帮助程序员编写正确的代码 +- 调试器和测试工具: 调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率 -- 多态有编译时多态和运行时多态,即静态绑定和动态绑定 -- 前者是通过方法重载实现,后者是通过重写实现(子类覆盖父类方法,虚方法表) -- 虚方法:运行时动态绑定的方法,对比静态绑定的非虚方法调用来说,虚方法调用更加耗时 +**反射的缺点:** -方法重写的本质: +- 性能开销:反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化,反射操作的效率要比那些非射操作低得多,应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。 +- 安全限制:使用反射技术要求程序必须在一个没有安全限制的环境中运行,如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了 +- 内部暴露:由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化 -1. 找到操作数栈的第一个元素所执行的对象的实际类型,记作 C -2. 如果在类型 C 中找到与常量中的描述符和名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回 java.lang.IllegalAccessError 异常 - IllegalAccessError:表示程序试图访问或修改一个属性或调用一个方法,这个属性或方法没有权限访问,一般会引起编译器异常。如果这个错误发生在运行时,就说明一个类发生了不兼容的改变 +*** -3. 找不到,就会按照继承关系从下往上依次对 C 的各个父类进行第二步的搜索和验证过程 -4. 如果始终没有找到合适的方法,则抛出 java.lang.AbstractMethodError 异常 +### 获取元素 +#### 获取类 -*** +反射技术的第一步是先得到Class类对象,有三种方式获取: +* 类名.class +* 通过类的对象.getClass()方法 +* Class.forName("类的全限名"):`public static Class forName(String className) ` +Class类下的方法: -##### 虚方法表 +| 方法 | 作用 | +| ---------------------- | ------------------------------------------------------------ | +| String getSimpleName() | 获得类名字符串:类名 | +| String getName() | 获得类全名:包名+类名 | +| T newInstance() | 创建Class对象关联类的对象,底层是调用无参数构造器,已经被淘汰 | -在虚拟机工作过程中会频繁使用到动态分配,每次动态分配的过程中都要重新在类的元数据中搜索合适目标,影响到执行效率。为了提高性能,JVM 采取了一种用**空间换取时间**的策略来实现动态绑定,在类的方法区建立一个虚方法表(virtual method table),实现使用索引表来代替查找,可以快速定位目标方法 +```java +public class ReflectDemo{ + public static void main(String[] args) throws Exception { + // 反射的第一步永远是先得到类的Class文件对象: 字节码文件。 + // 1.类名.class + Class c1 = Student.class; + System.out.println(c1);//class _03反射_获取Class类对象.Student -* invokevirtual 所使用的虚方法表(virtual method table,vtable),执行流程 - 1. 先通过栈帧中的对象引用找到对象,分析对象头,找到对象的实际 Class - 2. Class 结构中有 vtable,查表得到方法的具体地址,执行方法的字节码 -* invokeinterface 所使用的接口方法表(interface method table,itable) + // 2.对象.getClass() + Student swk = new Student(); + Class c2 = swk.getClass(); + System.out.println(c2); -虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,jvm会把该类的方法表也初始化完毕 + // 3.Class.forName("类的全限名") + // 直接去加载该类的class文件。 + Class c3 = Class.forName("_03反射_获取Class类对象.Student"); + System.out.println(c3); -虚方法表的执行过程: + System.out.println(c1.getSimpleName()); // 获取类名本身(简名)Student + System.out.println(c1.getName()); //获取类的全限名_03反射_获取Class类对象.Student + } +} +class Student{} +``` -* 对于静态绑定的方法调用而言,实际引用将指向具体的目标方法 -* 对于动态绑定的方法调用而言,实际引用则是方法表的索引值,也就是方法的间接地址。Java 虚拟机获取调用者的实际类型,并在该实际类型的虚方法表中,根据索引值获得目标方法内存偏移量(指针) -为了优化对象调用方法的速度,方法区的类型信息会增加一个指针,该指针指向一个记录该类方法的方法表。每个类中都有一个虚方法表,本质上是一个数组,每个数组元素指向一个当前类及其祖先类中非私有的实例方法 -方法表满足两个特质: +*** -* 其一,子类方法表中包含父类方法表中的所有方法 -* 其二,子类方法在方法表中的索引值,与它所重写的父类方法的索引值相同 - -Passenger 类的方法表包括两个方法,分别对应 0 号和 1 号。方法表调换了 toString 方法和 passThroughImmigration 方法的位置,是因为 toString 方法的索引值需要与 Object 类中同名方法的索引值一致,为了保持简洁,这里不考虑 Object 类中的其他方法。 +#### 获取构造 -虚方法表对性能的影响: +获取构造器的API: -* 使用了方法表的动态绑定与静态绑定相比,仅仅多出几个内存解引用操作:访问栈上的调用者、读取调用者的动态类型、读取该类型的方法表、读取方法表中某个索引值所对应的目标方法,但是相对于创建并初始化 Java 栈帧这操作的开销可以忽略不计 -* 上述优化的效果看上去不错,但实际上**仅存在于解释执行**中,或者即时编译代码的最坏情况。因为即时编译还拥有另外两种性能更好的优化手段:内联缓存(inlining cache)和方法内联(method inlining) +* Constructor getConstructor(Class... parameterTypes):根据参数匹配获取某个构造器,只能拿public修饰的构造器,几乎不用! +* **Constructor getDeclaredConstructor(Class... parameterTypes)**:根据参数匹配获取某个构造器,只要申明就可以定位,不关心权限修饰符 +* Constructor[] getConstructors():获取所有的构造器,只能拿public修饰的构造器,几乎不用! +* **Constructor[] getDeclaredConstructors()**:获取所有构造器,只要申明就可以定位,不关心权限修饰符。 - +Constructor的常用API: +| 方法 | 作用 | +| --------------------------------- | -------------------------------------- | +| T newInstance(Object... initargs) | 创建对象,注入构造器需要的数据 | +| void setAccessible(true) | 修改访问权限,true攻破权限(暴力反射) | +| String getName() | 以字符串形式返回此构造函数的名称 | +| int getParameterCount() | 返回参数数量 | +| Class[] getParameterTypes | 返回参数类型数组 | +```java +public class TestStudent01 { + @Test + public void getDeclaredConstructors(){ + // a.反射第一步先得到Class类对象 + Class c = Student.class ; + // b.定位全部构造器,只要申明了就可以拿到 + Constructor[] cons = c.getDeclaredConstructors(); + // c.遍历这些构造器 + for (Constructor con : cons) { + System.out.println(con.getName()+"->"+con.getParameterCount()); + } + } + @Test + public void getDeclaredConstructor() throws Exception { + // a.反射第一步先得到Class类对象 + Class c = Student.class ; + // b.定位某个构造器,根据参数匹配,只要申明了就可以获取 + //Constructor con = c.getDeclaredConstructor(); // 可以拿到!定位无参数构造器! + Constructor con = c.getDeclaredConstructor(String.class, int.class); //有参数的!! + // c.构造器名称和参数 + System.out.println(con.getName()+"->"+con.getParameterCount()); + } +} +``` -*** +```java +public class Student { + private String name ; + private int age ; + private Student(){ + System.out.println("无参数构造器被执行~~~~"); + } + public Student(String name, int age) { + System.out.println("有参数构造器被执行~~~~"); + this.name = name; + this.age = age; + } +} +``` + +```java +//测试方法 +public class TestStudent02 { + // 1.调用无参数构造器得到一个类的对象返回。 + @Test + public void createObj01() throws Exception { + // a.反射第一步是先得到Class类对象 + Class c = Student.class ; + // b.定位无参数构造器对象 + Constructor constructor = c.getDeclaredConstructor(); + // c.暴力打开私有构造器的访问权限 + constructor.setAccessible(true); + // d.通过无参数构造器初始化对象返回 + Student swk = (Student) constructor.newInstance(); // 最终还是调用无参数构造器的! + System.out.println(swk);//Student{name='null', age=0} + } + // 2.调用有参数构造器得到一个类的对象返回。 + @Test + public void createObj02() throws Exception { + // a.反射第一步是先得到Class类对象 + Class c = Student.class ; + // b.定位有参数构造器对象 + Constructor constructor = c.getDeclaredConstructor(String.class , int.class); + // c.通过无参数构造器初始化对象返回 + Student swk = (Student) constructor.newInstance("孙悟空",500); // 最终还是调用有参数构造器的! + System.out.println(swk);//Student{name='孙悟空', age=500} + } +} -##### 内联缓存 +``` -内联缓存:是一种加快动态绑定的优化技术,它能够缓存虚方法调用中**调用者的动态类型以及该类型所对应的目标方法**。在之后的执行过程中,如果碰到已缓存的类型,内联缓存便会直接调用该类型所对应的目标方法;如果没有碰到已缓存的类型,内联缓存则会退化至使用基于方法表的动态绑定 -多态的三个术语: -* 单态 (monomorphic):指的是仅有一种状态的情况 -* 多态 (polymorphic):指的是有限数量种状态的情况,二态(bimorphic)是多态的其中一种 -* 超多态 (megamorphic):指的是更多种状态的情况,通常用一个具体数值来区分多态和超多态,在这个数值之下,称之为多态,否则称之为超多态 +*** -对于内联缓存来说,有对应的单态内联缓存、多态内联缓存: -* 单态内联缓存:只缓存了一种动态类型以及所对应的目标方法,实现简单,比较所缓存的动态类型,如果命中,则直接调用对应的目标方法。 -* 多态内联缓存:缓存了多个动态类型及其目标方法,需要逐个将所缓存的动态类型与当前动态类型进行比较,如果命中,则调用对应的目标方法 -为了节省内存空间,**Java 虚拟机只采用单态内联缓存**,没有命中的处理方法: +#### 获取变量 -* 替换单态内联缓存中的纪录,类似于 CPU 中的数据缓存,对数据的局部性有要求,即在替换内联缓存之后的一段时间内,方法调用的调用者的动态类型应当保持一致,从而能够有效地利用内联缓存 -* 劣化为超多态状态,这也是 Java 虚拟机的具体实现方式,这种状态实际上放弃了优化的机会,将直接访问方法表来动态绑定目标方法,但是与替换内联缓存纪录相比节省了写缓存的额外开销 +获取Field成员变量API: -虽然内联缓存附带内联二字,但是并没有内联目标方法 +* Field getField(String name) : 根据成员变量名获得对应Field对象,只能获得public修饰 +* Field getDeclaredField(String name) : 根据成员变量名获得对应Field对象,所有申明的变量 +* Field[] getFields() : 获得所有的成员变量对应的Field对象,只能获得public的 +* Field[] getDeclaredFields() : 获得所有的成员变量对应的Field对象,只要申明了就可以得到 +Field的方法:给成员变量赋值和取值 + +| 方法 | 作用 | +| ---------------------------------- | --------------------------------------------------------- | +| 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 { + //获取全部成员变量 + @Test + public void getDeclaredFields(){ + // a.先获取class类对象 + Class c = Dog.class; + // b.获取全部申明的成员变量对象 + Field[] fields = c.getDeclaredFields(); + for (Field field : fields) { + System.out.println(field.getName()+"->"+field.getType()); + } + } + //获取某个成员变量 + @Test + public void getDeclaredField() throws Exception { + // a.先获取class类对象 + Class c = Dog.class; + // b.定位某个成员变量对象 :根据名称定位!! + Field ageF = c.getDeclaredField("age"); + System.out.println(ageF.getName()+"->"+ageF.getType()); + } +} +``` +```java +public class Dog { + private String name; + private int age ; + private String color ; + public static String school; + public static final String SCHOOL_1 = "宠物学校"; + public Dog() { + } -*** + public Dog(String name, int age, String color) { + this.name = name; + this.age = age; + this.color = color; + } +} +``` +```java +//测试方法 +public class FieldDemo02 { + @Test + public void setField() throws Exception { + // a.反射的第一步获取Class类对象 + Class c = Dog.class ; + // b.定位name成员变量 + Field name = c.getDeclaredField("name"); + // c.为这个成员变量赋值! + Dog d = new Dog(); + name.setAccessible(true); + name.set(d,"泰迪"); + System.out.println(d);//Dog{name='泰迪', age=0, color='null'} + // d.获取成员变量的值 + String value = name.get(d)+""; + System.out.println(value);//泰迪 + } +} +``` -### 字节码 -(字节码部分笔记待优化) +#### 获取方法 -#### 类结构 +获取Method方法API: -class文件是编译器编译之后供虚拟机解释执行的二进制字节码文件,一个class文件对应一个public类型的类 +* Method getMethod(String name,Class...args):根据方法名和参数类型获得方法对象,public修饰 +* Method getDeclaredMethod(String name,Class...args):根据方法名和参数类型获得方法对象,包括private +* Method[] getMethods():获得类中的所有成员方法对象返回数组,只能获得public修饰且包含父类的 +* Method[] getDeclaredMethods():获得类中的所有成员方法对象,返回数组,只获得本类申明的方法 -根据 JVM 规范,类文件结构如下: +Method常用API: +`public Object invoke(Object obj, Object... args) `:使用指定的参数调用由此方法对象,obj对象名 ```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 MethodDemo{ + //获得类中的所有成员方法对象 + @Test + public void getDeclaredMethods(){ + // a.先获取class类对象 + Class c = Dog.class ; + // b.获取全部申明的方法! + Method[] methods = c.getDeclaredMethods(); + // c.遍历这些方法 + for (Method method : methods) { + System.out.println(method.getName()+"->" + + method.getParameterCount()+"->" + method.getReturnType()); + } + } + @Test + public void getDeclardMethod() throws Exception { + Class c = Dog.class; + Method run = c.getDeclaredMethod("run"); + // c.触发方法执行! + Dog d = new Dog(); + Object o = run.invoke(d); + System.out.println(o);// 如果方法没有返回值,结果是null + + //参数一:方法名称 参数二:方法的参数个数和类型(可变参数!) + Method eat = c.getDeclaredMethod("eat",String.class); + eat.setAccessible(true); // 暴力反射! + + //参数一:被触发方法所在的对象 参数二:方法需要的入参值 + Object o1 = eat.invoke(d,"肉"); + System.out.println(o1);// 如果方法没有返回值,结果是null + } +} + +public class Dog { + private String name ; + public Dog(){ + } + public void run(){System.out.println("狗跑的贼快~~");} + private void eat(){System.out.println("狗吃骨头");} + private void eat(String name){System.out.println("狗吃"+name);} + public static void inAddr(){System.out.println("在吉山区有一只单身狗!");} } ``` -HelloWorld.java 执行 `javac -parameters -d . HellowWorld.java`指令: -```sh -0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09 -0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07 -0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29 //... -``` -魔数:0~3 字节,表示是否是Classs类型的文件,ca fe ba be代表Java +*** -版本:4~7 字节,表示类的版本 00 34(52) 表示是 Java 8 -常量池:8~9 字节,表示常量池长度,00 23 (35) 表示常量池有 #1~#34项,注意 #0 项不计入,也没有值 -* 第#1项 0a 表示一个 Method 信息,00 06 和 00 15(21) 表示它引用了常量池中 #6 和 #21 项来获得 - 这个方法的【所属类】和【方法名】 -* 第#2项 09 表示一个 Field 信息,00 16(22)和 00 17(23) 表示它引用了常量池中 #22 和 # 23 项 - 来获得这个成员变量的【所属类】和【成员变量名】 -* 第#n项 +### 暴力攻击 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-类结构常量池.png) +泛型只能工作在编译阶段,运行阶段泛型就消失了,反射工作在运行时阶段 -```sh -0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01 +1. 反射可以破坏面向对象的封装性(暴力反射) +2. 同时可以破坏泛型的约束性 + +```java +public class ReflectDemo { + public static void main(String[] args) throws Exception { + List scores = new ArrayList<>(); + scores.add(99.3); + scores.add(199.3); + scores.add(89.5); + // 拓展:通过反射暴力的注入一个其他类型的数据进去。 + // a.先得到集合对象的Class文件对象 + Class c = scores.getClass(); + // b.从ArrayList的Class对象中定位add方法 + Method add = c.getDeclaredMethod("add", Object.class); + // c.触发scores集合对象中的add执行(运行阶段,泛型不能约束了) + add.invoke(scores,"波仔"); + System.out.println(scores); + } +} ``` -访问标识与继承信息: + + -* 00 21 表示该 class 是一个类,公共的 -* 00 05 表示根据常量池中 #5 找到本类全限定名 -* 00 06 表示根据常量池中 #6 找到父类全限定名 -* 00 00 表示接口的数量,本类为 0 -Field信息:表示成员变量数量,00 00表示本类为 0 +*** -![](https://gitee.com/seazean/images/raw/master/Java/JVM-类结构Field信息.png) -Method信息:表示方法数量,00 02 本类为 2,一个方法由 访问修饰符,名称,参数描述,方法属性数量,方法属性组成 -附加属性: -* 00 01 表示附加属性数量 -* 00 13 表示引用了常量池 #19 项,即【SourceFile】 -* 00 00 00 02 表示此属性的长度 -* 00 14 表示引用了常量池 #20 项,即【HelloWorld.java】 -```sh -0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00 -0001120 00 00 02 00 14 -``` +## 注解 +### 概念 +注解:类的组成部分,可以给类携带一些额外的信息,提供一种安全的类似注释标记的机制,用来将任何信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联 +* 注解是JDK1.5的新特性 +* 注解是给编译器或JVM看的,编译器或JVM可以根据注解来完成对应的功能 +* 注解类似修饰符,应用于包、类型、构造方法、方法、成员变量、参数及本地变量的声明语句中 +注解作用: -**** +* 标记 +* 框架技术多半都是在使用注解和反射,都是属于框架的底层基础技术 +* 在编译时进行格式检查,比如方法重写约束 @Override、函数式接口约束 @FunctionalInterface. -#### javap +*** -`javap -v HelloWorld.class`:反编译 class 文件 -slot:局部变量表中最基本的存储单元 -aload:从局部变量表的相应位置装载一个对象引用到操作数栈的栈顶 +### 注解格式 -* aload_0把this装载到了操作数栈中 -* aload_0是一组格式为aload_的操作码中的一个,这一组操作码把对象的引用装载到操作数栈中标志了待处理的局部变量表中的位置 +定义格式:自定义注解用@interface关键字,注解默认可以标记很多地方 -`iload_,lload_,fload_,dload_`:i代表int型,l代表long型,f代表float型以及d代表double型 +```java +修饰符 @interface 注解名{ + // 注解属性 +} +``` -* 在局部变量表中的索引位置大于3的变量的装载可以使用iload、lload、fload,、dload和aload -* 这些操作码都需要一个操作数的参数,用于确认需要装载的局部变量的位置 +使用注解的格式:@注解名 -Ljava.lang.String: +```java +@Book +@MyTest +public class MyBook { + //方法变量都可以注解 +} -* `[`:表示一维数组 -* `[[`:表示二维数组 -* `L`:表示一个对象 -* `java.lang.String`:表示对象的类型 +@interface Book{ +} +@interface MyTest{ +} +``` @@ -16510,165 +8291,177 @@ Ljava.lang.String: -#### 执行流程 - -原始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); - } -} -``` +#### 普通属性 -javap -v Demo.class:省略 +注解可以有属性,**属性名必须带()**,在用注解的时候,属性必须赋值,除非属性有默认值 -* 常量池载入运行时常量池 +属性的格式: -* 方法区字节码载入方法区 +* 格式1:数据类型 属性名(); +* 格式2:数据类型 属性名() default 默认值; -* main 线程开始运行,分配栈帧内存:(操作数栈stack=2,局部变量表locals=4) +属性适用的数据类型: -* **执行引擎**开始执行字节码 +* 八种数据数据类型(int,short,long,double,byte,char,boolean,float) 和 String、Class +* 以上类型的数组形式都支持 - `bipush 10`:将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令 +```java +@MyBook(name="《精通Java基础》",authors = {"播仔","Dlei","播妞"} , price = 99.9 ) +public class AnnotationDemo01 { + @MyBook(name="《精通MySQL数据库入门到删库跑路》",authors = {"小白","小黑"} , + price = 19.9 , address = "北京") + public static void main(String[] args) { + } +} +// 自定义一个注解 +@interface MyBook{ + String name(); + String[] authors(); // 数组 + double price(); + String address() default "武汉"; +} - * 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) +注解的特殊属性名称:value - `istore_2`:将操作数栈顶数据弹出,存入局部变量表的 slot 2 +* 如果只有一个value属性的情况下,使用value属性的时候可以省略value名称不写 +* 如果有多个属性,且多个属性没有默认值,那么value是不能省略的 - `iload_1`:将局部变量表的 slot 1数据弹出,放入操作数栈栈顶 +```java +//@Book("/deleteBook.action") +@Book(value = "/deleteBook.action" , age = 12) +public class AnnotationDemo01{ +} - `iload_2`:将局部变量表的 slot 2数据弹出,放入操作数栈栈顶 +@interface Book{ + String value(); + int age() default 10; +} +``` - `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`: +### 元注解 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程5.png) +元注解是sun公司提供的,用来注解自定义注解 - `invokevirtual #5`: +元注解有四个: - * 找到常量池 #5 项 - * 定位到方法区 java/io/PrintStream.println:(I)V 方法 - * **生成新的栈帧**(分配 locals、stack等) - * 传递参数,执行新栈帧中的字节码 - * 执行完毕,弹出栈帧 - * 清除 main 操作数栈内容 +* @Target:约束自定义注解可以标记的范围,默认值为任何元素,表示该注解用于什么地方 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程6.png) + 可使用的值定义在ElementType枚举类中: - return:完成 main 方法调用,弹出 main 栈帧,程序结束 + - `ElementType.CONSTRUCTOR`:用于描述构造器 + - `ElementType.FIELD`:成员变量、对象、属性(包括enum实例) + - `ElementType.LOCAL_VARIABLE`:用于描述局部变量 + - `ElementType.METHOD`:用于描述方法 + - `ElementType.PACKAGE`:用于描述包 + - `ElementType.PARAMETER`:用于描述参数 + - `ElementType.TYPE`:用于描述类、接口(包括注解类型) 或enum声明 - +* @Retention:定义该注解的生命周期,申明注解的作用范围:编译时,运行时 -*** + 可使用的值定义在RetentionPolicy枚举类中: + - `RetentionPolicy.SOURCE`:在编译阶段丢弃,这些注解在编译结束之后就不再有任何意义,只作用在源码阶段,生成的字节码文件中不存在。`@Override`, `@SuppressWarnings`都属于这类注解 + - `RetentionPolicy.CLASS`:在类加载时丢弃,在字节码文件的处理中有用,运行阶段不存在,默认值 + - `RetentionPolicy.RUNTIME` : 始终不会丢弃,运行期也保留该注解,因此可以使用反射机制读取该注解的信息,自定义的注解通常使用这种方式 +* @Inherited:表示修饰的自定义注解可以被子类继承 -#### 条件判断 +* @Documented:表示是否将自定义的注解信息添加在 java 文档中 ```java -public static void main(String[] args) { - int a = 0; - if(a == 0) { - a = 10; - } else { - a = 20; +public class AnnotationDemo01{ + // @MyTest // 只能注解方法 + private String name; + + @MyTest + public static void main( String[] args) { } } +@Target(ElementType.METHOD) // 申明只能注解方法 +@Retention(RetentionPolicy.RUNTIME) // 申明注解从写代码一直到运行还在,永远存活!! +@interface MyTest{ +} ``` -说明: -* byte,short,char 都会按 int 比较,因为操作数栈都是 4 字节 -* goto 用来进行跳转到指定行号的字节码 -```sh -1: istore_1 -2: iload_1 -3: ifne 12 -6: bipush 10 -8: istore_1 -9: goto 15 -12: bipush 20 -14: istore_1 -15: return -``` +*** - +### 注解解析 -*** +开发中经常要知道一个类的成分上面到底有哪些注解,注解有哪些属性数据,这都需要进行注解的解析。 +注解解析相关的接口: +* Annotation:注解类型,该类是所有注解的父类,注解都是一个Annotation的对象 +* AnnotatedElement:该接口定义了与注解解析相关的方法 +* Class、Method、Field、Constructor类成分:实现AnnotatedElement接口,拥有解析注解的能力 -#### 循环控制 +API : + `Annotation[] getDeclaredAnnotations()` : 获得当前对象上使用的所有注解,返回注解数组 + `T getDeclaredAnnotation(Class annotationClass)` : 根据注解类型获得对应注解对象 + `T getAnnotation(Class annotationClass)` : 根据注解类型获得对应注解对象 + `boolean isAnnotationPresent(Class class)` : 判断对象是否使用了指定的注解 -iinc 指令:是直接在局部变量 slot 上进行运算 +注解原理:注解本质是一个继承了`Annotation` 的特殊接口,其具体实现类是 Java 运行时生成的**动态代理类**,通过反射获取注解时,返回的是 Java 运行时生成的动态代理对象 `$Proxy1`,通过代理对象调用自定义注解(接口)的方法,会最终调用 `AnnotationInvocationHandler` 的 `invoke` 方法,该方法会从 `memberValues` 这个Map 中找出对应的值,而 `memberValues` 的来源是 Java 常量池 -while循环: +解析注解数据的原理:注解在哪个成分上,就先拿哪个成分对象,比如注解作用在类上,则要该类的Class对象,再来拿上面的注解 ```java -public static void main(String[] args) { - int a = 0; - while (a < 10) { - a++; +public class AnnotationDemo{ + @Test + public void parseClass() { + // 1.定位Class类对象 + Class c = BookStore.class; + // 2.判断这个类上是否使用了某个注解 + if(c.isAnnotationPresent(Book.class)){ + // 3.获取这个注解对象 + Book b = (Book)c.getDeclarAnnotation(Book.class); + System.out.println(book.value()); + System.out.println(book.price()); + System.out.println(Arrays.toString(book.authors())); + } + } + @Test + public void parseMethod() throws Exception { + Class c = BookStore.class; + Method run = c.getDeclaredMethod("run"); + if(run.isAnnotationPresent(Book.class)){ + Book b = (Book)run.getDeclaredAnnotation(Book.class); + sout(上面的三个); + } } } -``` - -```sh -0: iconst_0 -1: istore_1 -2: iload_1 -3: bipush 10 -5: if_icmpge 14 -8: iinc 1, 1 -11: goto 2 -14: return -``` - -for循环: - -```java -for (int i = 0; i < 10; i++) { } -``` -```sh -0: iconst_0 -1: istore_1 -2: iload_1 -3: bipush 10 -5: if_icmpge 14 -8: iinc 1, 1 -11: goto 2 -14: return +@Book(value = "《Java基础到精通》", price = 99.5, authors = {"波仔","波妞"}) +class BookStore{ + @Book(value = "《Mybatis持久层框架》", price = 199.5, authors = {"dlei","播客"}) + public void run(){ + } +} +@Target({ElementType.TYPE,ElementType.METHOD}) // 类和成员方法上使用 +@Retention(RetentionPolicy.RUNTIME) // 注解永久存活 +@interface Book{ + String value(); + double price() default 100; + String[] authors(); +} ``` @@ -16677,745 +8470,881 @@ for (int i = 0; i < 10; i++) { } -#### 面试题 +### 注解模拟 -##### 分析i++ +注解模拟写一个Junit框架的基本使用 -从字节码角度分析:a++ 和 ++a 的区别是先执行 iload 还是 先执行 iinc +1. 定义一个自定义注解MyTest,只能注解方法,存活范围一直都在。 +2. 定义若干个方法,只要有@MyTest注解的方法就能被触发执行,没有这个注解的方法不能执行!! ```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 +public class TestDemo{ + @MyTest + public void test01(){System.out.println("===test01===");} + public void test02(){System.out.println("===test02===");} + @MyTest + public void test03(){System.out.println("===test03===");} + @MyTest + public void test04(){System.out.println("===test04===");} + + public static void main(String[] args) throws Exception { + TestDemo t = new TestDemo(); + Class c = TestDemo.class; + Method[] methods = c.getDeclaredMethods(); + for (Method method : methods) { + if(method.isAnnotationPresent(MyTest.class)){ + method.invoke(t); + } + } } } + +@Target(ElementType.METHOD) // 只能注解方法! +@Retention(RetentionPolicy.RUNTIME) // 一直都活着 +@interface MyTest{ +} ``` -##### 判断结果 +**** -```java -public class Demo3_6_1 { - public static void main(String[] args) { - int i = 0; - int x = 0; - while (i < 10) { - x = x++; - i++; - } - System.out.println(x); // 结果是 0 - } -} -``` +## XML -*** +### 概述 +XML介绍: +- XML 指可扩展标记语言(EXtensible Markup Language) +- XML 是一种**标记语言**,很类似 HTML,HTML文件也是XML文档 +- XML 的设计宗旨是**传输数据**,而非显示数据 +- XML 标签没有被预定义,需要自行定义标签 +- XML 被设计为具有自我描述性,易于阅读 +- XML 是 W3C 的推荐标准 -#### 异常处理 +**xml与html的区别**: -##### try-catch +​ XML 不是 HTML 的替代,XML 和 HTML 为不同的目的而设计。 +​ XML 被设计为传输和存储数据,其焦点是数据的内容;XMl标签可自定义,便于阅读。 +​ HTML 被设计用来显示数据,其焦点是数据的外观;HTML标签被预设好,便于浏览器识别。 +​ HTML 旨在显示信息,而 XML 旨在传输信息。 -```java -public static void main(String[] args) { - int i = 0; - try { - i = 10; - } catch (ArithmeticException e) { - i = 30; - } catch (NullPointerException e) { - i = 40; - } catch (Exception e) { - i = 50; - } -} -``` -```java -public static void main(java.lang.String[]); - descriptor: ([Ljava/lang/String;)V - flags: ACC_PUBLIC, ACC_STATIC - Code: - stack=1, locals=3, args_size=1 - 0: iconst_0 - 1: istore_1 - 2: bipush 10 - 4: istore_1 - 5: goto 26 - 8: astore_2 - 9: bipush 30 - 11: istore_1 - 12: goto 26 - 15: astore_2 - 16: bipush 40 - 18: istore_1 - 19: goto 26 - 22: astore_2 - 23: bipush 50 - 25: istore_1 - 26: return - Exception table: - from to target type - 2 5 8 Class java/lang/Exception - 2 5 15 Class java/lang/NullPointerException - 2 5 22 Class java/lang/Exception - LineNumberTable: ... - LocalVariableTable: - Start Length Slot Name Signature - 9 3 2 e Ljava/lang/ArithmeticException; - 16 3 2 e Ljava/lang/NullPointerException; - 23 3 2 e Ljava/lang/Exception; - 0 27 0 args [Ljava/lang/String; - 2 25 1 i I - StackMapTable: ... - MethodParameters: ... -} -``` - -* 多出一个 **Exception table** 的结构,[from, to) 是前闭后开的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号 -* 8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置 -* 因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用 - - - -##### finally -finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程 +**** -```java -public static void main(String[] args) { - int i = 0; - try { - i = 10; - } catch (Exception e) { - i = 20; - } finally { - i = 30; - } -} -``` -```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: - 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 -``` +### 创建 +person.xml -##### return +```xml + + + 18 + 张三 + + +``` -###### 吞异常 -```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 -> slot 1 存入局部变量表的 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,会**吞掉异常** +### 组成 -###### 不吞异常 +XML文件中常见的组成元素有:文档声明、元素、属性、注释、转义字符、字符区。文件后缀名为xml -```java -public class Demo3_12_2 { - 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; - } - } -} -``` +* **文档声明** + ```` + 文档声明必须在第一行,以结束, + version:指定XML文档版本。必须属性,这里一般选择1.0; + enconding:指定当前文档的编码,可选属性,默认值是utf-8; + standalone: 该属性不是必须的,描述XML文件是否依赖其他的xml文件,取值为yes/no + +* **元素** + + * 格式1:` ` + 格式2:`` + 普通元素的结构由开始标签、元素体、结束标签组成; + 标签由一对尖括号和合法标识符组成,标签必须成对出现。特殊的标签可以不成对,必须有结束标记; + +* 元素体:可以是元素,也可以是文本,例如:``张三`` + * 空元素:空元素只有标签,而没有结束标签,但**元素必须自己闭合**,例如:```` + * 元素命名:区分大小写、不能使用空格冒号、不建议用XML xml Xml等开头 + * 必须存在一个根标签,有且只能有一个 + +* **属性** + `` + 属性是元素的一部分,它必须出现在元素的开始标签中 + 属性的定义格式:`属性名=“属性值”`,其中属性值必须使用单引或双引号括起来 + 一个元素可以有0~N个属性,但一个元素中不能出现同名属性 + 属性名不能使用空格 , 不要使用冒号等特殊字符,且必须以字母开头 -```java - 0: bipush 10 // 10 放入栈顶 - 2: istore_0 // 10 -> i,赋值给i,放入slot 0 - 3: iload_0 // i(10)加载至操作数栈 - 4: istore_1 // 10 -> slot 1,暂存至 slot 1,目的是为了固定返回值 - 5: bipush 20 // 20 放入栈顶 - 7: istore_0 // 20 -> i - 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 -``` +* **注释** + + XML的注释与HTML相同,既以````结束。 + +* **转义字符** + XML中的转义字符与HTML一样。因为很多符号已经被文档结构所使用,所以在元素体或属性值中想使用这些符号就必须使用转义字符(也叫实体字符),例如:">"、"<"、"'"、"""、"&"。 + XML 中仅有字符 "<"和"&" 是非法的。省略号、引号和大于号是合法的,把它们替换为实体引用 + + | 字符 | 预定义的转义字符 | 说明 | + | :--: | :--------------: | :----: | + | < | ``<`` | 小于 | + | > | `` >`` | 大于 | + | " | `` "`` | 双引号 | + | ' | `` '`` | 单引号 | + | & | `` &`` | 和号 | +* **字符区** + ```xml + + ``` + + * CDATA 指的是不应由 XML 解析器进行解析的文本数据(Unparsed Character Data) +* CDATA 部分由 "" 结束; + * 大量的转义字符在xml文档中时,会使XML文档的可读性大幅度降低。这时使用CDATA段就会好一些 + + * 规则 + CDATA 部分不能包含字符串 "]]>"。也不允许嵌套的 CDATA 部分。 + 标记 CDATA 部分结尾的 "]]>" 不能包含空格或折行。 + + ```xml + + + + + + + + + 西门庆 + 32 + + + + select * from student where age < 18 && age > 10; + + + + 10; + ]]> + + + ``` + + +**** -*** +### 约束 +#### DTD -### 代码优化 +##### DTD定义 -#### 语法糖 +DTD是文档类型定义(Document Type Definition)。DTD 可以定义在 XML 文档中出现的元素、这些元素出现的次序、它们如何相互嵌套以及XML文档结构的其它详细信息。 -语法糖:指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担 +##### DTD规则 +* 约束元素的嵌套层级 + ```dtd + + ``` -#### 构造器 +* 约束元素体里面的数据 + +* 语法 + + ```dtd + + ``` + +* 判断元素 + 简单元素:没有子元素。 + 复杂元素:有子元素的元素; + + * 标签类型 + + | 标签类型 | 代码写法 | 说明 | + | -------- | --------- | -------------------- | + | PCDATA | (#PCDATA) | 被解释的字符串数据 | + | EMPTY | EMPTY | 即空元素,例如\
| + | ANY | ANY | 即任意类型 | + + * 代码 + + ```dtd + + + + + ``` + + * 数量词 + + | 数量词符号 | 含义 | + | ---------- | ---------------------------- | + | 空 | 表示元素出现一次 | + | * | 表示元素可以出现0到多个 | + | + | 表示元素可以出现至少1个 | + | ? | 表示元素可以是0或1个 | + | , | 表示元素需要按照顺序显示 | + | \| | 表示元素需要选择其中的某一个 | + + + +* 属性声明 -```java -public class Candy1 { -} -``` + * 语法 -```java -public class Candy1 { - // 这个无参构造是编译器帮助我们加上的 - public Candy1() { - super(); // 即调用父类 Object 的无参构造方法,即调用 java/lang/Object." - ":()V - } -} -``` + ```dtd + + ``` + * 属性类型 + | 属性类型 | 含义 | + | ---------- | ------------------------------------------------------------ | + | CDATA | 代表属性是文本字符串, eg: | + | ID | 代码该属性值唯一,不能以数字开头, eg: | + | ENUMERATED | 代表属性值在指定范围内进行枚举 Eg: "社科类"是默认值,属性如果不设置默认值就是"社科类" | -*** + * 属性说明 + | 属性说明 | 含义 | + | --------- | ----------------------------------------------------------- | + | #REQUIRED | 代表属性是必须有的 | + | #IMPLIED | 代表属性可有可无 | + | #FIXED | 代表属性为固定值,实现方式:book_info CDATA #FIXED "固定值" | + * 代码 -#### 拆装箱 + ```dtd + + id ID #REQUIRED + 编号 CDATA #IMPLIED + 出版社 (清华|北大|传智播客) "传智播客" + type CDATA #FIXED "IT" + > + + ``` + + -```java -Integer x = 1; -int y = x; -``` -这段代码在 JDK 5 之前是无法编译通过的,必须改写为代码片段2: -```java -Integer x = Integer.valueOf(1); -int y = x.intValue(); -``` +##### DTD引入 -JDK5 以后编译阶段自动转换成上述片段 +* 引入本地dtd + ```dtd + + ``` +* 在xml文件内部引入 -*** + ```dtd + + ``` +* 引入网络dtd + ```dtd + + ``` -#### 泛型擦除 +```dtd + + + + + +``` -泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行**泛型擦除**的动作,即泛型信息 -在编译为字节码之后就丢失了,实际的类型都**当做了 Object 类型**来处理: +```xml + + + + + 张三 + 23 + -```java -List list = new ArrayList<>(); -list.add(10); // 实际调用的是 List.add(Object e) -Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index); + ``` -编译器真正生成的字节码中,还要额外做一个类型转换的操作: +```xml-dtd + + + + + + + ]> -```java -// 需要将 Object 转为 Integer -Integer x = (Integer)list.get(0); + + + 张三 + 23 + + ``` -如果前面的 x 变量类型修改为 int 基本类型那么最终生成的字节码是: +```dtd + + -```java -// 需要将 Object 转为 Integer, 并执行拆箱操作 -int x = ((Integer)list.get(0)).intValue(); + + + 张三 + 23 + + ``` -*** +##### DTD实现 + +persondtd.dtd文件 +```dtd + + + + + +``` +```xml-dtd + + -#### 可变参数 + + + 张三 + 23 + -```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"); - } -} + + 张三 + 23 + + ``` -可变参数`String... args`其实是`String[] args` , java 编译器会在编译期间将上述代码变换为: -```java -public static void main(String[] args) { - foo(new String[]{"hello", "world"}); -} -``` -注意:如果调用了foo()则等价代码为`foo(new String[]{})` ,创建了一个空的数组,而不会传递 null 进去 +*** -**** +#### Schema +##### 定义 +1.Schema 语言也可作为 XSD(XML Schema Definition) +2.schema约束文件本身也是一个xml文件,符合xml的语法,这个文件的后缀名.xsd +3.一个xml中可以引用多个schema约束文件,多个schema使用名称空间区分(名称空间类似于java包名) +4.dtd里面元素类型的取值比较单一常见的是PCDATA类型,但是在schema里面可以支持很多个数据类型 +**5.Schema文件约束xml文件的同时也被别的文件约束着** -#### foreach -**数组的循环:** -```java -int[] array = {1, 2, 3, 4, 5}; // 数组赋初值的简化写法也是语法糖哦 -for (int e : array) { - System.out.println(e); -} -``` +##### 规则 + +1、创建一个文件,这个文件的后缀名为.xsd。 +2、定义文档声明 +3、schema文件的根标签为: +4、在中定义属性: + xmlns=http://www.w3.org/2001/XMLSchema + 代表当前文件时约束别人的,同时这个文件也对该Schema进行约束 +5、在中定义属性 : + targetNamespace = 唯一的url地址,指定当前这个schema文件的名称空间。 + **名称空间**:当其他xml使用该schema文件,需要引入此空间 +6、在中定义属性 : + elementFormDefault="qualified“,表示当前schema文件是一个质量良好的文件。 +7、通过element定义元素 +8、**判断当前元素是简单元素还是复杂元素** + +person.xsd -编译后为循环取数: +```scheme + + + targetNamespace="http://www.seazean.cn/javase" + elementFormDefault="qualified" +> -```java -for(int i = 0; i < array.length; ++i) { - int e = array[i]; - System.out.println(e); -} + + + + + + + + + + + + + + + + + ``` -**集合的循环:** -```java -List list = Arrays.asList(1,2,3,4,5); -for (Integer i : list) { - System.out.println(i); -} -``` -编译后转换为对迭代器的调用: +##### 引入 -```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); -} -``` +1、在根标签上定义属性xmlns="http://www.w3.org/2001/XMLSchema-instance" +2、**通过xmlns引入约束文件的名称空间** +3、给某一个xmlns属性添加一个标识,用于区分不同的名称空间 + 格式为: xmlns:标识=“名称空间url” ,标识可以是任意的,但是一般取值都是xsi +4、通过xsi:schemaLocation指定名称空间所对应的约束文件路径 + 格式为: xsi:schemaLocation = "名称空间url 文件路径“ -注意:foreach 循环写法,能够配合数组以及所有实现了 Iterable 接口的集合类一起使用,其中 Iterable 用来获取集合的迭代器( Iterator ) +```scheme + + + xmlns="http://www.seazean.cn/javase" + xsi:schemaLocation="http://www.seazean.cn/javase person.xsd" +> + + 张三 + 23 + + +``` -*** +##### Sc属性 -#### switch +```scheme + + -##### 字符串 + + + + + + + + + + + + + + + + + + + + + + + + -从 JDK 开始,switch 可以作用于字符串和枚举类: + + + + 张三 + 23 + -```java -switch (str) { - case "hello": { - System.out.println("h"); - break; - } - case "world": { - System.out.println("w"); - break; - } -} + ``` -注意:switch 配合 String 和枚举使用时,变量不能为null -会被编译器转换为: -```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; -} -``` +*** -总结: -* 执行了两遍 switch,第一遍是根据字符串的 hashCode 和 equals 将字符串的转换为相应 byte 类型,第二遍才是利用 byte 执行进行比较 -* hashCode 是为了提高效率,减少可能的比较;而 equals 是为了防止 hashCode 冲突 +### Dom4J +#### 解析 -*** +* 概述:xml解析就是从xml中获取到数据。DOM是解析思想。 +* DOM(Document Object Model)文档对象模型:就是把文档的各个组成部分看做成对应的对象。 + 会把xml文件全部加载到内存,在内存中形成一个树形结构,再获取对应的值 +* 工具:dom4j属于第三方技术,必须导入该框架 + https://dom4j.github.io/ 去下载dom4j,在idea中当前模块下新建一个lib文件夹,将jar包复制到文件夹中 + 选中jar包 -> 右键 -> 选择add as library即可 -##### 枚举 +* dom4j实现 + * dom4j解析器构造方法:`SAXReader saxReader = new SAXReader();` + * 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 + `public InputStream getResourceAsStream(String path)`:加载文件成为一个字节输入流返回 -switch 枚举的例子,原始代码: -```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; - } - } -} -``` -编译转换后的代码: +#### 解析根元素 + +Document方法: + Element getRootElement():获取根元素。 ```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; +// 需求:解析books.xml文件成为一个Document文档树对象,得到根元素对象。 +public class Dom4JDemo { + public static void main(String[] args) throws Exception { + // 1.创建一个dom4j的解析器对象:代表整个dom4j框架。 + SAXReader saxReader = new SAXReader(); + // 2.第一种方式(简单):通过解析器对象去加载xml文件数据,成为一个Document文档树对象。 + //Document document = saxReader.read(new File("Day13Demo/src/books.xml")); + + // 3.第二种方式(代码多点)先把xml文件读成一个字节输入流 + // 这里的“/”是直接去src类路径下寻找文件。 + InputStream is = Dom4JDemo01.class.getResourceAsStream("/books.xml"); + Document document = saxReader.read(is); + System.out.println(document); + //org.dom4j.tree.DefaultDocument@27a5f880 [Document: name null] + // 4.从document文档树对象中提取根元素对象 + Element root = document.getRootElement(); + System.out.println(root.getName());//books } } ``` +```xml + + + + JavaWeb开发教程 + 张孝祥 + 100.00元 + + + 三国演义 + 罗贯中 + 100.00元 + + + + - -*** +``` -#### 枚举类 +#### 解析子元素 -JDK 7 新增了枚举类: +Element元素的API: + String getName() : 取元素的名称。 + List elements() : 获取当前元素下的全部子元素(一级) + List elements(String name) : 获取当前元素下的指定名称的全部子元素(一级) + Element element(String name) : 获取当前元素下的指定名称的某个子元素,默认取第一个(一级) ```java -enum Sex { - MALE, FEMALE -} -``` - -编译转换后: +public class Dom4JDemo { + public static void main(String[] args) throws Exception { + SAXReader saxReader = new SAXReader(); + Document document = saxReader.read(new File("Day13Demo/src/books.xml")); + // 3.获取根元素对象 + Element root = document.getRootElement(); + System.out.println(root.getName()); -```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); + // 4.获取根元素下的全部子元素 + List sonElements = root.elements(); + for (Element sonElement : sonElements) { + System.out.println(sonElement.getName()); + } + // 5.获取根源下的全部book子元素 + List sonElements1 = root.elements("book"); + for (Element sonElement : sonElements1) { + System.out.println(sonElement.getName()); + } + + // 6.获取根源下的指定的某个元素 + Element son = root.element("user"); + System.out.println(son.getName()); + // 默认会提取第一个名称一样的子元素对象返回! + Element son1 = root.element("book"); + System.out.println(son1.attributeValue("id")); } } -``` +``` -#### try-w-r -JDK 7 开始新增了对需要关闭的资源处理的特殊语法`try-with-resources`,格式: +#### 解析属性 -```java -try(资源变量 = 创建资源对象){ -} catch( ) { -} -``` +Element元素的API: + List attributes() : 获取元素的全部属性对象。 + Attribute attribute(String name) : 根据名称获取某个元素的属性对象。 + String attributeValue(String var) : 直接获取某个元素的某个属性名称的值。 -其中资源对象需要实现 **AutoCloseable**接口,例如 InputStream、OutputStream、Connection、Statement、ResultSet 等接口都实现了 AutoCloseable ,使用 try-withresources可以不用写 finally 语句块,编译器会帮助生成关闭资源代码: +Attribute对象的API: + String getName() : 获取属性名称。 + String getValue() : 获取属性值。 ```java -try(InputStream is = new FileInputStream("d:\\1.txt")) { - System.out.println(is); -} catch (IOException e) { - e.printStackTrace(); -} -``` +public class Dom4JDemo { + public static void main(String[] args) throws Exception { + SAXReader saxReader = new SAXReader(); + Document document = saxReader.read(new File("Day13Demo/src/books.xml")); + Element root = document.getRootElement(); + // 4.获取book子元素 + Element bookEle = root.element("book"); -转换成: + // 5.获取book元素的全部属性对象 + List attributes = bookEle.attributes(); + for (Attribute attribute : attributes) { + System.out.println(attribute.getName()+"->"+attribute.getValue()); + } -`addSuppressed(Throwable e)`:添加被压制异常,是为了防止异常信息的丢失(try-with-resources 生成的 fianlly 中如果抛出了异常) + // 6.获取Book元素的某个属性对象 + Attribute descAttr = bookEle.attribute("desc"); + System.out.println(descAttr.getName()+"->"+descAttr.getValue()); -```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(); + // 7.可以直接获取元素的属性值 + System.out.println(bookEle.attributeValue("id")); + System.out.println(bookEle.attributeValue("desc")); + } } ``` -*** - - - -#### 方法重写 - -方法重写时对返回值分两种情况: +#### 解析文本 -* 父子类的返回值完全一致 -* 子类返回值可以是父类返回值的子类 +Element: + String elementText(String name) : 可以直接获取当前元素的子元素的文本内容 + String elementTextTrim(String name) : 去前后空格,直接获取当前元素的子元素的文本内容 + String getText() : 直接获取当前元素的文本内容。 + String getTextTrim() : 去前后空格,直接获取当前元素的文本内容。 ```java -class A { - public Number m() { - return 1; - } -} -class B extends A { - @Override - // 子类m方法的返回值是Integer是父类m方法返回值Number的子类 - public Integer m() { - return 2; - } -} -``` +public class Dom4JDemo { + public static void main(String[] args) throws Exception { + SAXReader saxReader = new SAXReader(); + Document document = saxReader.read(new File("Day13Demo/src/books.xml")); + Element root = document.getRootElement(); + // 4.得到第一个子元素book + Element bookEle = root.element("book"); -对于子类,java 编译器会做如下处理: + // 5.直接拿到当前book元素下的子元素文本值 + System.out.println(bookEle.elementText("name")); + System.out.println(bookEle.elementTextTrim("name")); // 去前后空格 + System.out.println(bookEle.elementText("author")); + System.out.println(bookEle.elementTextTrim("author")); // 去前后空格 + System.out.println(bookEle.elementText("sale")); + System.out.println(bookEle.elementTextTrim("sale")); // 去前后空格 -```java -class B extends A { - public Integer m() { - return 2; - } - // 此方法才是真正重写了父类 public Number m() 方法 - public synthetic bridge Number m() { - // 调用 public Integer m() - return m(); + // 6.先获取到子元素对象,再获取该文本值 + Element bookNameEle = bookEle.element("name"); + System.out.println(bookNameEle.getText()); + System.out.println(bookNameEle.getTextTrim());// 去前后空格 } } ``` -其中桥接方法比较特殊,仅对 java 虚拟机可见,并且与原来的 public Integer m() 没有命名冲突 - -*** - +**** -#### 匿名内部类 -##### 无参优化 +#### 案例 -源代码: +Dom4j解析XML文件:Contacts.xml成为一个Java的对象 +Contacts.xml 解析成===> List ```java -public class Candy11 { - public static void main(String[] args) { - Runnable runnable = new Runnable() { - @Override - public void run() { - System.out.println("ok"); +public class Dom4JDemo { + public static void main(String[] args) throws Exception { + SAXReader saxReader = new SAXReader(); + Document document = saxReader.read(new File("Day13Demo/src/Contacts.xml")); + Element root = docment.getRootElement(); + // 4.获取根元素下的全部子元素 + List sonElements = root.elements(); + // 5.遍历子元素 封装成List集合对象 + List contactList = new ArrayList<>(); + if(sonElements != null && sonElements.size() > 0) { + for (Element sonElement : sonElements) { + Contact c = new Contact(); + c.setID(Integer.valueOf(sonElement.attributeValue("id"))); + contact.setVip(Boolean.valueOf(sonElement.attributeValue("vip"))); + contact.setName(sonElement.elementText("name")); + contact.setSex(sonElement.elementText("gender").charAt(0)); + contact.setEmail(sonElement.elementText("email")); + contactList.add(contact); } - }; + } + System.out.println(contactList); } +} +public class Contact { + private int id ; + private boolean vip; + private String name ; + private char sex ; + private String email ; + //构造器 } ``` -转化后代码: +```xml + + + + 潘金莲 + + panpan@seazean.cn + + + 武松 + + wusong@seazean.cn + + + 武大狼 + + wuda@seazean.cn + + +``` + -```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(); - } -} -``` +**** -##### 带参优化 -引用局部变量的匿名内部类,源代码: +### XPath -```java -public class Candy11 { - public static void test(final int x) { - Runnable runnable = new Runnable() { - @Override - public void run() { - System.out.println("ok:" + x); - } - }; - } -} -``` +Dom4J 可以用于解析整个XML的数据。但是如果要检索XML中的某些信息,建议使用XPath -转换后代码: +XPath常用API: + +* List selectNodes(String var1) : 检索出一批节点集合 +* Node selectSingleNode(String var1) : 检索出一个节点返回 + +XPath提供的四种检索数据的写法: + +1. 绝对路径:/根元素/子元素/子元素。 +2. 相对路径:./子元素/子元素。 (.代表了当前元素) +3. 全文搜索: + * //元素 在全文找这个元素 + * //元素1/元素2 在全文找元素1下面的一级元素2 + * //元素1//元素2 在全文找元素1下面的全部元素2 +4. 属性查找。 + * //@属性名称 在全文检索属性对象。 + * //元素[@属性名称] 在全文检索包含该属性的元素对象。 + * //元素[@属性名称=值] 在全文检索包含该属性的元素且属性值为该值的元素对象。 ```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 class Candy11 { - public static void test(final int x) { - Runnable runnable = new Candy11$1(x); +public class XPathDemo { + public static void main(String[] args) throws Exception { + SAXReader saxReader = new SAXReader(); + InputStream is = XPathDemo.class.getResourceAsStream("/Contact.xml"); + Document document = saxReader.read(is); + //1.使用绝对路径定位全部的name名称 + List nameNodes1 = document.selectNodes("/contactList/contact/name"); + for (Node nameNode : nameNodes) { + System.out.println(nameNode.getText()); + } + + //2.相对路径。从根元素开始检索,.代表很根元素 + List nameNodes2 = root.selectNodes("./contact/name"); + + //3.1 在全文中检索name节点 + List nameNodes3 = root.selectNodes("//name");//全部的 + //3.2 在全文中检索所有contact下的所有name节点 //包括sql,不外面的 + List nameNodes3 = root.selectNodes("//contact//name"); + //3.3 在全文中检索所有contact下的直接name节点 + List nameNodes3 = root.selectNodes("//contact/name");//不包括sql和外面 + + //4.1 检索全部属性对象 + List attributes1 = root.selectNodes("//@id");//包括sql4 + //4.2 在全文检索包含该属性的元素对象 + List attributes1 = root.selectNodes("//contact[@id]"); + //4.3 在全文检索包含该属性的元素且属性值为该值的元素对象 + Node nodeEle = document.selectSingleNode("//contact[@id=2]"); + Element ele = (Element)nodeEle; + System.out.println(ele.elementTextTrim("name"));//武松 } } ``` -局部变量在底层创建为内部类的成员变量,必须是 final 的原因: +```xml + + + + 潘金莲 + + panpan@seazean.cn + + + 武松 + + wusong@seazean.cn + + sql语句 + + + + 武大狼 + + wuda@seazean.cn + + +外面的名称 + +``` + -* 在Java中方法调用是值传递的,在匿名内部类中对变量的操作都是基于原变量的副本,不会影响到原变量的值,所以原变量的值的改变也无法同步到副本中 -* 外部变量为final是在编译期以强制手段确保用户不会在内部类中做修改原变量值的操作,也是防止外部操作修改了变量而内部类无法随之变化出现的影响 - 在创建 `Candy11$1 ` 对象时,将 x 的值赋值给了 `Candy11$1` 对象的 val 属性,x 不应该再发生变化了,因为发生变化,this.val$x 属性没有机会再跟着变化 @@ -17423,91 +9352,51 @@ public class Candy11 { -#### 反射优化 -```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 源码可知: +JVM:全称 Java Virtual Machine,即 Java 虚拟机,一种规范,本身是一个虚拟计算机,直接和操作系统进行交互,与硬件不直接交互,而操作系统可以帮我们完成和硬件进行交互的工作 -* sun.reflect.noInflation 可以用来禁用膨胀,直接生成 GeneratedMethodAccessor1,但首次生成比较耗时,如果仅反射调用一次,不划算 -* sun.reflect.inflationThreshold 可以修改膨胀阈值 +特点: +* Java 虚拟机基于**二进制字节码**执行,由一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆、一个方法区等组成 +* JVM 屏蔽了与操作系统平台相关的信息,从而能够让 Java 程序只需要生成能够在 JVM 上运行的字节码文件,通过该机制实现的**跨平台性** +Java代码执行流程:java程序 --(编译)--> 字节码文件 --(解释执行)--> 操作系统(Win,Linux) +JVM结构: + -*** +JVM、JRE、JDK对比: + -## JVM调优 -### 服务器性能 +*** -(调优部分笔记待优化) -对于一个系统要部署上线时,则一定会对JVM进行调整,不经过任何调整直接上线,容易出现线上系统频繁FullGC造成系统卡顿、CPU使用频率过高、系统无反应等问题 -对于一个应用来说通常重点关注的性能指标主要是吞吐量、响应时间、QPS、TPS等、并发用户数等,而这些性能指标又依赖于系统服务器的资源,如:CPU、内存、磁盘IO、网络IO等。对于这些指标数据的收集,通常可以根据Java本身的工具或指令进行查询 +### 架构模型 -JDK 自带了**监控工具,位于 JDK 的 bin 目录下**,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具: +Java编译器输入的指令流是一种基于栈的指令集架构。因为跨平台的设计,java的指令都是根据栈来设计的,不同平台CPU架构不同,所以不能设计为基于寄存器架构 -* jconsole:用于对 JVM 中的内存、线程和类等进行监控; -* jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等 +* 基于栈式架构的特点: + * 设计和实现简单,适用于资源受限的系统 + * 使用零地址指令方式分配,执行过程依赖操作栈,指令集更小,编译器容易实现 + * 零地址指令:机器指令的一种,是指令系统中的一种不设地址字段的指令,只有操作码而没有地址码。这种指令有两种情况:一是无需操作数,另一种是操作数为默认的(隐含的),默认为操作数在寄存器(ACC)中,指令可直接访问寄存器 + * 一地址指令:一个操作码对应一个地址码,通过地址码寻找操作数 + * 不需要硬件的支持,可移植性更好,更好实现跨平台 +* 基于寄存器架构的特点: + * 需要硬件的支持,可移植性差 + * 性能更好,执行更高效,寄存器比内存快 + * 以一地址指令、二地址指令、三地址指令为主 @@ -17515,115 +9404,134 @@ JDK 自带了**监控工具,位于 JDK 的 bin 目录下**,其中最常用 -### 参数调优 +### 生命周期 -对于JVM调优,主要就是调整年轻代、老年代、元空间的内存空间大小及使用的垃圾回收器类型 +JVM的生命周期分为三个阶段,分别为:启动、运行、死亡。 -* 设置堆的初始大小和最大大小,为了防止垃圾收集器在初始大小、最大大小之间收缩堆而产生额外的时间,通常把最大、初始大小设置为相同的值 +- **启动**:当启动一个 Java 程序时,通过引导类加载器(bootstrap class loader)创建一个初始类(initial class),对于拥有 main 函数的类就是 JVM 实例运行的起点 +- **运行**: + - main() 方法是一个程序的初始起点,任何线程均可由在此处启动 + - 在 JVM 内部有两种线程类型,分别为:用户线程和守护线程,**JVM 使用的是守护线程,main() 和其他线程使用的是用户线程**,守护线程会随着用户线程的结束而结束 + - 执行一个 Java 程序时,真真正正在执行的是一个 Java 虚拟机的进程 + - JVM 有两种运行模式 Server 与 Client,两种模式的区别在于:Client 模式启动速度较快,Server 模式启动较慢;但是启动进入稳定期长期运行之后 Server 模式的程序运行速度比 Client 要快很多 +- **死亡**: + - 当程序中的用户线程都中止,JVM 才会退出 + - 程序正常执行结束、程序异常或错误而异常终止、操作系统错误导致终止 + - 线程调用 Runtime 类 halt 方法或 System 类 exit 方法,并且 java 安全管理器允许这次 exit 或 halt 操作 - ```sh - -Xms:设置堆的初始化大小 - -Xmx:设置堆的最大大小 - ``` -* 设置年轻代中Eden区和两个Survivor区的大小比例。该值如果不设置,则默认比例为8:1:1。Java官方通过增大Eden区的大小,来减少YGC发生的次数,但有时我们发现,虽然次数减少了,但Eden区满的时候,由于占用的空间较大,导致释放缓慢,此时STW的时间较长,因此需要按照程序情况去调优 - ```sh - -XX:SurvivorRatio - ``` -* 年轻代和老年代默认比例为1:2。可以通过调整二者空间大小比率来设置两者的大小。 +*** - ```sh - -XX:newSize 设置年轻代的初始大小 - -XX:MaxNewSize 设置年轻代的最大大小, 初始大小和最大大小两个值通常相同 - ``` -* 线程堆栈的设置:**每个线程默认会开启1M的堆栈**,用于存放栈帧、调用参数、局部变量等,但一般256K就够用,通常减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统 - ```sh - -Xss 对每个线程stack大小的调整,-Xss128k - ``` +### 相关参数 + +进入 Run/Debug Configurations ---> VM options 设置参数 + +| 参数 | 功能 | +| ------------------------------------------------------------ | ------------------------------------------------------------ | +| -Xms | 堆初始大小(默认为物理内存的1/64) | +| -Xmx 或 -XX:MaxHeapSize=size | 堆最大大小(默认为物理内存的1/4) | +| -Xmn 或 -XX:NewSize=size + -XX:MaxNewSize=size | 新生代大小(初始值及最大值) | +| -XX:NewRatio | 新生代与老年代在堆结构的占比 | +| -XX:SurvivorRatio=ratio | 幸存区比例(Eden和S0/S1空间的比例) | +| -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy | 幸存区比例(动态) | +| -XX:MaxTenuringThreshold=threshold | 晋升阈值 | +| -XX:+PrintTenuringDistribution | 晋升详情 | +| -XX:+PrintFlagsInitial | 查看所有的参数的默认初始值 | +| -XX:+PrintFlagsFinal | 查看所有的参数的最终值
(可能会存在修改,不再是初始值) | +| -XX:+PrintGCDetails | GC详情,打印gc简要信息:
1. -XX:+PrintGC 2. - verbose:gc | +| -XX:+ScavengeBeforeFullGC | FullGC 前 MinorGC | +| -XX:+DisableExplicitGC | 禁用显式垃圾回收,让System.gc无效 | + +说明:参数前面是`+`号说明是开启,如果是`- `号说明是关闭 + -* 一般一天超过一次FullGC就是有问题,首先通过工具查看是否出现内存泄露,如果出现内存泄露则调整代码,没有的话则调整JVM参数 -* 系统CPU持续飙高的话,首先先排查代码问题,如果代码没问题,则咨询运维或者云服务器供应商,通常服务器重启或者服务器迁移即可解决 -* 如果数据查询性能很低下的话,如果系统并发量并没有多少,则应更加关注数据库的相关问题 -* 如果服务器配置还不错,JDK8开始尽量使用G1或者新生代和老年代组合使用并行垃圾回收器 +*** +## 内存结构 -*** +### 内存概述 +内存结构是JVM中非常重要的一部分,是非常重要的系统资源,是硬盘和 CPU 的中间仓库及桥梁,承载着操作系统和应用程序的实时运行,又叫运行时数据区 +JVM 内存结构规定了 Java 在运行过程中内存申请、分配、管理的策略,保证了 JVM 的高效稳定运行 +* Java1.8以前的内存结构图: + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java7内存结构图.png) +* Java1.8之后的内存结果图: -# JUC + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java8内存结构图.png) -## 进程 +线程运行诊断: -### 概述 +* 定位:jps定位进程id +* jstack 进程id:用于打印出给定的 java 进程 ID 或 core file 或远程调试服务的 Java 堆栈信息 -进程:程序是静止的,进程实体的运行过程就是进程,是系统进行资源分配的基本单位 +常见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 -**线程**:线程是属于进程的,是一个基本的CPU执行单元,是程序执行流的最小单元。线程是进程中的一个实体,是被系统独立调度和分配的基本单位,线程本身不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其他线程共享进程所拥有的全部资源。 -关系:一个进程可以包含多个线程,这就是多线程,比如看视频是进程,图画、声音、广告等就是多个线程 -线程的作用:使多道程序更好的并发执行,提高资源利用率和系统吞吐量,增强操作系统的并发性能 +*** -**并发并行**: -* 并行:在同一时刻,有多个指令在多个CPU上同时执行 -* 并发:在同一时刻,有多个指令在单个CPU上交替执行 -**同步异步**: +### JVM内存 -* 需要等待结果返回,才能继续运行就是同步 -* 不需要等待结果返回,就能继续运行就是异步 +#### 虚拟机栈 +##### Java栈 +Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所需要的内存 -*** +* 每个方法被执行时,都会在虚拟机栈中创建一个栈帧 stack frame(**一个方法一个栈帧**) +* java虚拟机规范允许 **Java 栈的大小是动态的或者是固定不变的** +* 虚拟机栈是**每个线程私有的**,每个线程只能有一个活动栈帧,对应方法调用到执行完成的整个过程 -### 对比 +* 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存,每个栈帧中存储着: -线程进程对比: + * 局部变量表:存储方法里的java基本数据类型以及对象的引用 + * 动态链接:也叫指向运行时常量池的方法引用 + * 方法返回地址:方法正常退出或者异常退出的定义 + * 操作数栈或表达式栈和其他一些附加信息 + + -* 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集 +设置栈内存大小:`-Xss size` `-Xss 1024k` -* 进程拥有共享的资源,如内存空间等,供其内部的线程共享 +* 在 JDK 1.4 中默认为 256K,而在 JDK 1.5+ 默认为 1M -* 进程间通信较为复杂 +虚拟机栈特点: - 同一台计算机的进程通信称为 IPC(Inter-process communication) +* 栈内存**不需要进行GC**,方法开始执行的时候会进栈,方法调用后自动弹栈,相当于清空了数据 - * 信号量:信号量是一个计数器,用于多进程对共享数据的访问,解决同步相关的问题并避免竞争条件 - * 共享存储:多个进程可以访问同一块内存空间,需要使用信号量用来同步对共享存储的访问 - * 管道通信:管道是用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,pipe文件 - * 匿名管道(Pipes) :用于具有亲缘关系的父子进程间或者兄弟进程之间的通信,只支持半双工通信 - * 命名管道(Names Pipes):以磁盘文件的方式存在,可以实现本机任意两个进程通信,遵循FIFO - * 消息队列:消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识,对比管道: - * 匿名管道存在于内存中的文件;命名管道存在于实际的磁盘介质或者文件系统;消息队列存放在内核中,只有在内核重启(操作系统重启)或者显示地删除一个消息队列时,该消息队列才被真正删除 - * 读进程可以根据消息类型有选择地接收消息,而不像 FIFO 那样只能默认地接收 +* 栈内存分配越大越大,可用的线程数越少(内存越大,每个线程拥有的内存越大) - 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP +* 方法内的局部变量是否**线程安全**: + * 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的 + * 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全 - * 套接字:与其它通信机制不同的是,它可用于不同机器间的进程通信 +异常: -* 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量 - - Java中的通信机制:volatile、等待/通知机制、join方式、InheritableThreadLocal、MappedByteBuffer - -* 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低 +* 栈帧过多导致栈内存溢出 (超过了栈的容量),会抛出 OutOfMemoryError 异常 +* 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常 @@ -17631,156 +9539,88 @@ JDK 自带了**监控工具,位于 JDK 的 bin 目录下**,其中最常用 -## 线程 - -### 创建线程 +##### 局部变量 -#### 三种方式 +局部变量表也被称之为局部变量数组或本地变量表,本质上定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量 -运行一个程序就是开启一个进程,在进程中创建线程的方式有三种: +* 表是建立在线程的栈上,是线程私有的数据,因此不存在数据安全问题 +* 表的容量大小是在编译期确定的,保存在方法的 Code 属性的 maximum local variables 数据项中 +* 表中的变量只在当前方法调用中有效,方法结束栈帧销毁,局部变量表也会随之销毁 +* 表中的变量也是重要的垃圾回收根节点,只要被表中数据直接或间接引用的对象都不会被回收 -1. 直接定义一个类继承线程类Thread,重写run()方法,创建线程对象,调用线程对象的start()方法启动线程 -2. 定义一个线程任务类实现Runnable接口,重写run()方法,创建线程任务对象,把线程任务对象包装成线程对象, 调用线程对象的start()方法启动线程 -3. 实现Callable接口 +局部变量表最基本的存储单元是 **slot(变量槽)**: +* 参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束,JVM为每一个slot都分配一个访问索引,通过索引即可访问到槽中的数据 +* 存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress 类型的变量 +* 32 位以内的类型只占一个 slot(包括returnAddress类型),64 位的类型(long 和 double)占两个 slot +* 局部变量表中的槽位是可以**重复利用**的,如果一个局部变量过了其作用域,那么之后申明的新的局部变量就可能会复用过期局部变量的槽位,从而达到节省资源的目的 +*** -#### Thread -Thread创建线程方式:创建线程类,匿名内部类方式 -* **start()方法底层其实是给CPU注册当前线程,并且触发run()方法执行**。 -* 线程的启动必须调用start()方法,如果线程直接调用run()方法,相当于变成了普通类的执行,此时将只有主线程在执行该线程 -* 建议线程先创建子线程,主线程的任务放在之后,否则主线程(main)永远是先执行完 +##### 操作数栈 -Thread构造器: +栈:可以使用数组或者链表来实现 -* `public Thread()` -* `public Thread(String name)` +操作数栈:在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)或出栈(pop) -```java -puclic 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) - } - // main线程输出放在上面 就变成有先后顺序了 - } -} -class MyThread extends Thread{ - @Override - public void run() { - for(int i = 0 ; i < 100 ; i++ ){ - System.out.println("子线程输出:"+i) - } - } -} -``` +* 保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间,是执行引擎的一个工作区 -继承Thread类的优缺点: - 优点:编码简单。 - 缺点:线程类已经继承了Thread类无法继承其他类了,功能不能通过继承拓展(单继承的局限性) +* Java 虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈 +* 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中 +栈顶缓存技术 ToS(Top-of-Stack Cashing):将栈顶元素全部缓存在 CPU 的寄存器中,以此降低对内存的读/写次数,提升执行的效率 +基于栈式架构的虚拟机使用的零地址指令更加紧凑,完成一项操作需要使用很多入栈和出栈指令,所以需要更多的指令分派(instruction dispatch)次数和内存读/写次数,由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度,所以需要栈顶缓存技术 -#### Runnable +*** -Runnable创建线程方式:创建线程类,匿名内部类方式 -**Thread类本身也是实现了Runnable接口** -Thread的构造器: - `public Thread(Runnable target)` - `public Thread(Runnable target, String name)` +##### 动态链接 -```java -public class ThreadDemo { - public static void main(String[] args) { - Runnable target = new MyRunnable(); - Thread t1 = new Thread(target,"1号线程"); - t1.start(); - Thread t2 = new Thread(target);//Thread-0 - } -} +动态链接是指向运行时常量池的方法引用,涉及到栈操作已经是类加载完成,这个阶段的解析是动态绑定 -public class MyRunnable implements Runnable{ - @Override - public void run() { - for(int i = 0 ; i < 10 ; i++ ){ - System.out.println(Thread.currentThread().getName()+"->"+i); - } - } -} -``` +* 为了支持当前方法的代码能够实现动态链接,每一个栈帧内部都包含一个指向运行时常量池或该栈帧所属方法的引用 -* 缺点:代码复杂一点。 + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-动态链接符号引用.png) -* 优点: +* 在 Java 源文件被编译成字节码文件中,所有的变量和方法引用都作为符号引用保存在 class 的常量池中 + 常量池的作用:提供一些符号和常量,便于指令的识别 - 1. 线程任务类只是实现了Runnable接口,可以继续继承其他类,避免了单继承的局限性 + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-动态链接运行时常量池.png) - 2. 同一个线程任务对象可以被包装成多个线程对象 - 3. 适合多个多个线程去共享同一个资源 - 4. 实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码和线程独立 +*** - 5. 线程池可以放入实现Runnable或Callable线程任务对象 -​ +##### 返回地址 -#### Callable +Return Address:存放调用该方法的PC寄存器的值 -实现Callable接口: - 1.定义一个线程任务类实现Callable接口,申明线程执行的结果类型 - 2.重写线程任务类的call方法,这个方法可以直接返回执行的结果 - 3.创建一个Callable的线程任务对象 - 4.把Callable的线程任务对象包装成一个未来任务对象 - 5.把未来任务对象包装成线程对象 - 6.调用线程的start()方法启动线程 +方法的结束有两种方式:正常执行完成、出现未处理的异常,在方法退出后都返回到该方法被调用的位置 -`public FutureTask(Callable callable)`:未来任务对象,在线程执行完后**得到线程的执行结果** +* 正常:调用者的 pc 计数器的值作为返回地址,即调用该方法的指令的**下一条指令的地址** +* 异常:返回地址是要通过异常表来确定 -* 其实就是Runnable对象,这样被包装成未来任务对象 +正常完成出口:执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者 -`public V get()`:同步等待 task 执行完毕的结果 +异常完成出口:方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内进行处理,本方法的异常表中没有搜素到匹配的异常处理器,导致方法退出 -* 如果在线程中获取另一个线程执行结果,会阻塞等待,用于线程同步 +两者区别:通过异常完成出口退出的不会给上层调用者产生任何的返回值 -优缺点: -* 优点:同Runnable,并且能得到线程执行的结果 -* 缺点:编码复杂 -```java -public class ThreadDemo { - public static void main(String[] args) { - Callable call = new MyCallable(); - FutureTask task = new FutureTask<>(call); - Thread t = new Thread(task); - t.start(); - try { - String s = task.get(); // 获取call方法返回的结果(正常/异常结果) - System.out.println(s); - } catch (Exception e) { - e.printStackTrace(); - } - } +##### 附加信息 -public class MyCallable implements Callable { - @Override//重写线程任务类方法 - public String call() throws Exception { - return Thread.currentThread().getName() + "->" + "Hello World"; - } -} -``` +栈帧中还允许携带与 Java 虚拟机实现相关的一些附加信息,例如对程序调试提供支持的信息 @@ -17788,27 +9628,25 @@ public class MyCallable implements Callable { -### 运行原理 - -JVM 中由堆、栈、方法区所组成 +#### 本地方法栈 -Java Virtual Machine Stacks(Java 虚拟机栈):每个线程启动后,虚拟机就会为其分配一块栈内存 +本地方法栈是为虚拟机**执行本地方法时提供服务的** -* 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存 -* 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法 +JNI:Java Native Interface,通过使用 Java 本地接口书写程序,可以确保代码在不同的平台上方便移植 -线程上下文切换(Thread Context Switch):一些原因导致 cpu 不再执行当前线程,转而执行另一个线程 +* 不需要进行GC,与虚拟机栈类似,也是线程私有的,有 StackOverFlowError 和 OutOfMemoryError 异常 -* 线程的 cpu 时间片用完 -* 垃圾回收 -* 有更高优先级的线程需要运行 -* 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法 +* 虚拟机栈执行的是 Java 方法,在 HotSpot JVM 中,直接将本地方法栈和虚拟机栈合二为一 -程序计数器(Program Counter Register):记住下一条 jvm 指令的执行地址,是线程私有的 +* 本地方法一般是由其他语言编写,并且被编译为基于本机硬件和操作系统的程序 -当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等 +* 当某个线程调用一个本地方法时,就进入了不再受虚拟机限制的世界,和虚拟机拥有同样的权限 -Java创建的线程是内核级线程,**线程的调度是在内核态运行的,而线程中的代码是在用户态运行**,所以线程切换(状态改变)会导致用户与内核态转换,这是非常消耗性能 + * 本地方法可以通过本地方法接口来**访问虚拟机内部的运行时数据区** + * 直接从本地内存的堆中分配任意数量的内存 + * 可以直接使用本地处理器中的寄存器 + + @@ -17816,71 +9654,85 @@ Java创建的线程是内核级线程,**线程的调度是在内核态运行 -### 常用API +#### 程序计数器 + +Program Counter Register 程序计数器(寄存器) -#### API +作用:内部保存字节码的行号,用于记录正在执行的字节码指令地址(如果正在执行的是本地方法则为空) -Thread类API: +原理: -| 方法 | 说明 | -| ------------------------------------------- | ------------------------------------------------------------ | -| public void start() | 启动一个新线程;Java虚拟机调用此线程的run方法 | -| public void run() | 线程启动后调用该方法 | -| public void setName(String name) | 给当前线程取名字 | -| public void getName() | 获取当前线程的名字
线程存在默认名称:子线程是Thread-索引,主线程是main | -| public static Thread currentThread() | 获取当前线程对象,代码在哪个线程中执行 | -| public static void sleep(long time) | 让当前线程休眠多少毫秒再继续执行
**Thread.sleep(0)** : 让操作系统立刻重新进行一次cpu竞争 | -| public static native void yield() | 提示线程调度器让出当前线程对CPU的使用 | -| public final int getPriority() | 返回此线程的优先级 | -| public final void setPriority(int priority) | 更改此线程的优先级,常用1 5 10 | -| public void interrupt() | 中断这个线程,异常处理机制 | -| public static boolean interrupted() | 判断当前线程是否被打断,清除打断标记 | -| public boolean isInterrupted() | 判断当前线程是否被打断,不清除打断标记 | -| public final void join() | 等待这个线程结束 | -| public final void join(long millis) | 等待这个线程死亡millis毫秒,0意味着永远等待 | -| public final native boolean isAlive() | 线程是否存活(还没有运行完毕) | -| public final void setDaemon(boolean on) | 将此线程标记为守护线程或用户线程 | +* java虚拟机对于多线程是通过线程轮流切换并且分配线程执行时间,一个处理器只会处理执行一个线程 +* 切换线程需要从程序计数器中来回去到当前的线程上一次执行的行号 +特点: +* **是线程私有的** +* 不会存在内存溢出,是JVM规范中唯一一个不出现OOM的区域,所以这个空间不会进行GC -*** +Java**反编译**指令:`javap -v Test.class` +#20:去Constant pool查看该地址的指令 + +```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 // -- +``` -#### run start -run:称为线程体,包含了要执行的这个线程的内容,方法运行结束,此线程随即终止。直接调用 run 是在主线程中执行了 run,没有启动新的线程,需要顺序执行 +**** -start:使用 start 是启动新的线程,此线程处于就绪(可运行)状态,通过新的线程间接执行 run 中的代码 -说明:**线程控制资源类** -**面试问题**:run()方法中的异常不能抛出,只能try/catch +#### 堆 -* 因为父类中没有抛出任何异常,子类不能比父类抛出更多的异常 -* 异常不能跨线程传播回 main() 中,因此必须在本地进行处理 +Heap 堆:是JVM内存中最大的一块,由所有线程共享,由垃圾回收器管理的主要区域("GC 堆"),堆中对象都需要考虑线程安全的问题 +存放哪些资源: +* 对象实例:类初始化生成的对象,**基本数据类型的数组也是对象实例**,new 创建对象都使用堆内存 +* 字符串常量池: + * 字符串常量池原本存放于方法区,jdk7开始放置于堆中 + * 字符串常量池**存储的是string对象的直接引用或者对象**,是一张string table +* 静态变量:静态变量是有static修饰的变量,jdk7时从方法区迁移至堆中 +* 线程分配缓冲区(Thread Local Allocation Buffer):线程私有但不影响堆的共性,可以提升对象分配的效率 -*** +设置堆内存指令:`-Xmx Size` +内存溢出:new出对象,循环添加字符数据,当堆中没有内存空间可分配给实例,也无法再扩展时,就会抛出OutOfMemoryError异常 +堆内存诊断工具:(控制台命令) -#### sleep yield +1. jps:查看当前系统中有哪些 java 进程 +2. jmap:查看堆内存占用情况 `jhsdb jmap --heap --pid 进程id` +3. jconsole:图形界面的,多功能的监测工具,可以连续监测 -sleep: +在Java7中堆内会存在**年轻代、老年代和方法区(永久代)**: -* 调用 sleep 会让当前线程从 Running 进入 `Timed Waiting` 状态(阻塞) -* sleep()方法的过程中,线程不会释放对象锁 -* 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException -* 睡眠结束后的线程未必会立刻得到执行 -* 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性 +* Young区被划分为三部分,Eden区和两个大小严格相同的Survivor区。Survivor区间,某一时刻只有其中一个是被使用的,另外一个留做垃圾回收时复制对象。在Eden区变满的时候, GC就会将存活的对象移到空闲的Survivor区间中,根据JVM的策略,在经过几次垃圾回收后,仍然存活于Survivor的对象将被移动到Tenured区间 +* Tenured区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在Young复制转移一定的次数以后,对象就会被转移到Tenured区 +* Perm代主要保存**Class、ClassLoader、静态变量、常量、编译后的代码**,在java7中堆内方法区会受到GC的管理 -yield: +分代原因:不同对象的生命周期不同,70%-99%的对象都是临时对象,优化GC性能 -* 调用 yield 会让提示线程调度器让出当前线程对CPU的使用 -* 具体的实现依赖于操作系统的任务调度器 -* **会放弃CPU资源,锁资源不会释放** +```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 +} +``` @@ -17888,218 +9740,130 @@ yield: -#### priority +#### 方法区 -* 线程优先级会提示(hint)调度器优先调度该线程,但这仅仅是一个提示,调度器可以忽略它 -* 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作 +方法区:是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是也叫 Non-Heap(非堆) +方法区是一个 JVM 规范,**永久代与元空间都是其一种实现方式** +方法区的大小不必是固定的,可以动态扩展,加载的类太多,可能导致永久代内存溢出 (OutOfMemoryError) -*** +方法区的 GC:针对常量池的回收及对类型的卸载,比较难实现 +为了**避免方法区出现OOM**,在JDK8中将堆内的方法区(永久代)移动到了本地内存上,重新开辟了一块空间,叫做元空间,元空间存储类的元信息,静态变量和字符串常量池等放入堆中 +类元信息:在类编译期间放入方法区,存放了类的基本信息,包括类的方法、参数、接口以及常量池表 -#### join +常量池表(Constant Pool Table)是Class文件的一部分,存储了类在编译期间生成的**字面量、符号引用**,JVM为每个已加载的类维护一个常量池 -public final void join():等待这个线程结束 +- 字面量:基本数据类型、字符串类型常量、声明为 final 的常量值等 +- 符号引用:类、字段、方法、接口等的符号引用 -原理:调用者轮询检查线程 alive 状态,t1.join()等价于: +**运行时常量池**是方法区的一部分 -```java -synchronized (t1) { - // 调用者线程进入 t1 的 waitSet 等待, 直到 t1 运行结束 - while (t1.isAlive()) { - t1.wait(0); - } -} -``` +* 常量池(编译器生成的字面量和符号引用)中的数据会在类加载的加载阶段放入运行时常量池 +* 类在解析阶段将符号引用替换成直接引用 +* 除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern() -* join方法是被synchronized修饰的,本质上是一个对象锁,其内部的wait方法调用也是释放锁的,但是**释放的是当前线程的对象锁,而不是外面的lock锁** -* t1会强占CPU资源,直至线程执行结束,当调用某个线程的join方法后,该线程抢占到CPU资源,就不再释放,直到线程执行完毕 -线程同步: +*** -* join实现线程同步,因为会阻塞等待另一个线程的结束,才能继续向下运行 - * 需要外部共享变量,不符合面向对象封装的思想 - * 必须等待线程结束,不能配合线程池使用 -* Future 实现(同步):get()方法阻塞等待执行结果 - * main 线程接收结果 - * get 方法是让调用线程同步等待 -```java -public class Test14 { - static int r = 0; - public static void main(String[] args) throws InterruptedException { - test1(); - } - private static void test1() throws InterruptedException { - Thread t1 = new Thread(() -> { - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - r = 10; - }); - t1.start(); - t1.join();//不等待线程执行结束,输出的10 - System.out.println(r); - } -} -``` +### 本地内存 +#### 本地内存 -*** +虚拟机内存:Java虚拟机在执行的时候会把管理的内存分配成不同的区域,受虚拟机内存大小的参数控制,当大小超过参数设置的大小时就会报OOM +本地内存:又叫做**堆外内存**,线程共享的区域,本地内存这块区域是不会受到JVM的控制的,不会发生GC;因此对于整个java的执行效率是提升非常大,但是如果内存的占用超出物理内存的大小,同样也会报OOM +本地内存概述图: -#### interrupt + -##### 打断线程 -`public void interrupt()`:中断这个线程,异常处理机制 -`public static boolean interrupted()`:判断当前线程是否被打断,清除打断标记 -`public boolean isInterrupted()`:判断当前线程是否被打断,不清除打断标记 -* sleep,wait,join方法都会让线程进入阻塞状态,打断进程会**清空打断状态** (false) +*** - ```java - public static void main(String[] args) throws InterruptedException { - Thread t1 = new Thread(()->{ - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - }, "t1"); - t1.start(); - Thread.sleep(500); - t1.interrupt(); - System.out.println(" 打断状态: {}" + t1.isInterrupted());// 打断状态: {}false - } - ``` -* 打断正常运行的线程:不会清空打断状态(true) - ```java - public static void main(String[] args) throws Exception { - Thread t2 = new Thread(()->{ - while(true) { - Thread current = Thread.currentThread(); - boolean interrupted = current.isInterrupted(); - if(interrupted) { - System.out.println(" 打断状态: {}" + interrupted);//打断状态: {}true - break; - } - } - }, "t2"); - t2.start(); - Thread.sleep(500); - t2.interrupt(); - } - ``` +#### 元空间 +PermGen 被元空间代替,永久代的**类信息、方法、常量池**等都移动到元空间区 +元空间与永久代区别:元空间不在虚拟机中,使用的本地内存,默认情况下,元空间的大小仅受本地内存限制 -*** +方法区内存溢出: +* JDK1.8以前会导致永久代内存溢出:java.lang.OutOfMemoryError: PerGen space + ```sh + -XX:MaxPermSize=8m #参数设置 + ``` + +* JDK1.8以后会导致元空间内存溢出:java.lang.OutOfMemoryError: Metaspace -##### 打断park + ```sh + -XX:MaxMetaspaceSize=8m #参数设置 + ``` -park作用类似sleep,打断 park 线程,不会清空打断状态(true) +元空间内存溢出演示: ```java -public static void main(String[] args) throws Exception { - Thread t1 = new Thread(() -> { - System.out.println("park..."); - LockSupport.park(); - System.out.println("unpark..."); - Sout("打断状态:" + Thread.currentThread().isInterrupted());//打断状态:true - }, "t1"); - t1.start(); - Thread.sleep(2000); - t1.interrupt(); +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); + } + } } ``` -如果打断标记已经是 true, 则 park 会失效, -```java -LockSupport.park(); -System.out.println("unpark..."); -LockSupport.park();//失效,不会阻塞 -System.out.println("unpark...");//和上一个unpark同时执行 -``` - -可以修改获取打断状态方法,使用`Thread.interrupted()`,清除打断标记 +*** -*** +#### 直接内存 +直接内存是 Java 堆外的、直接向系统申请的内存区间,不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域 -##### 终止模式 +Direct Memory 优点: -终止模式之两阶段终止模式:Two Phase Termination +* Java 的 NIO 库允许 Java 程序使用直接内存,用于数据缓冲区,使用 native 函数直接分配堆外内存 +* 读写性能高,读写频繁的场合可能会考虑使用直接内存 +* 大大提高 IO 性能,避免了在 Java 堆和 native 堆来回复制数据 -目标:在一个线程 T1 中如何“优雅”终止线程 T2?”优雅“指的是给 T2 一个后置处理器 +直接内存缺点: -错误思想: +* 分配回收成本较高,不受 JVM 内存回收管理 +* 可能导致 OutOfMemoryError 异常:OutOfMemoryError: Direct buffer memory +* 回收依赖 System.gc() 的调用,但这个调用 JVM 不保证执行、也不保证何时执行,行为是不可控的。程序一般需要自行管理,成对去调用 malloc、free -* 使用线程对象的 stop() 方法停止线程:stop 方法会真正杀死线程,如果这时线程锁住了共享资源,当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁 -* 使用 System.exit(int) 方法停止线程:目的仅是停止一个线程,但这种做法会让整个程序都停止 +应用场景: -两阶段终止模式图示: +- 有很大的数据需要存储,数据的生命周期很长 +- 适合频繁的 IO 操作,比如网络并发场景 - -打断线程可能在任何时间,所以需要考虑在任何时刻被打断的处理方法: -```java -public class Test { - public static void main(String[] args) throws InterruptedException { - TwoPhaseTermination tpt = new TwoPhaseTermination(); - tpt.start(); - Thread.sleep(3500); - tpt.stop(); - } -} -class TwoPhaseTermination { - private Thread monitor; - //启动监控线程 - public void start() { - monitor = new Thread(new Runnable() { - @Override - public void run() { - while (true) { - Thread thread = Thread.currentThread(); - if (thread.isInterrupted()) { - System.out.println("后置处理"); - break; - } - try { - Thread.sleep(1000);//睡眠 - System.out.println("执行监控记录");//在此被打断不会异常 - } catch (InterruptedException e) {//在睡眠期间被打断 - e.printStackTrace(); - //重新设置打断标记 - thread.interrupt(); - } - } - } - }); - monitor.start(); - } - //停止监控线程 - public void stop() { - monitor.interrupt(); - } -} -``` +直接内存机制参考:NET → NIO → 缓冲区 → 直接内存 @@ -18107,369 +9871,300 @@ class TwoPhaseTermination { -#### daemon +### 变量位置 -`public final void setDaemon(boolean on)`:如果是 true ,将此线程标记为守护线程 +**变量的位置不取决于它是基本数据类型还是引用数据类型,取决于它的声明位置** -线程**启动前**调用此方法: +静态内部类和其他内部类: -```java -Thread t = new Thread() { - @Override - public void run() { - System.out.println("running"); - } -}; -// 设置该线程为守护线程 -t.setDaemon(true); -t.start(); -``` +* **一个class文件只能对应一个public类型的类**,这个类可以有内部类,但不会生成新的class文件 -用户线程:平常创建的普通线程 +* 静态内部类属于类本身,加载到方法区,其他内部类属于内部类的属性,加载到栈(待考证) -守护线程:服务于用户线程,只要其它非守护线程运行结束了,即使守护线程代码没有执行完,也会强制结束 +类变量: -说明:当运行的线程都是守护线程,Java虚拟机将退出,因为普通线程执行完后,守护线程不会继续运行下去 +* 类变量是用static修饰符修饰,定义在方法外的变量,随着java进程产生和销毁 +* 在java8之前把静态变量存放于方法区,在java8时存放在**堆中的静态变量区** -常见的守护线程: -* 垃圾回收器线程就是一种守护线程 -* Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等 - 待它们处理完当前请求 +实例变量: +* 实例(成员)变量是定义在类中,没有static修饰的变量,随着类的实例产生和销毁,是类实例的一部分 +* 在类初始化的时候,从运行时常量池取出直接引用或者值,**与初始化的对象一起放入堆中** +局部变量: -*** +* 局部变量是定义在类的方法中的变量 +* 在所在方法被调用时**放入虚拟机栈的栈帧**中,方法执行结束后从虚拟机栈中弹出, +类常量池、运行时常量池、字符串常量池有什么关系?有什么区别? +* 类常量池与运行时常量池都存储在方法区,而字符串常量池在jdk7时就已经从方法区迁移到了java堆中 +* 在类编译过程中,会把类元信息放到方法区,类元信息的其中一部分便是类常量池,主要存放字面量和符号引用,而字面量的一部分便是文本字符 +* 在类加载时将字面量和符号引用解析为直接引用存储在运行时常量池 +* 对于文本字符来说,会在解析时查找字符串常量池,查出这个文本字符对应的字符串对象的直接引用,将直接引用存储在运行时常量池 -#### 不推荐方法 +什么是字面量?什么是符号引用? -不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁: +* 字面量:java代码在编译过程中是无法构建引用的,字面量就是在编译时对于数据的一种表示 -| 方法 | 功能 | -| --------------------------- | -------------------- | -| public final void stop() | 停止线程运行 | -| public final void suspend() | 挂起(暂停)线程运行 | -| public final void resume() | 恢复线程运行 | + ```java + int a=1;//这个1便是字面量 + String b="iloveu";//iloveu便是字面量 + ``` +* 符号引用:在编译过程中并不知道每个类的地址,因为可能这个类还没有加载,如果在一个类中引用了另一个类,无法知道它的内存地址,只能用他的类名作为符号引用,在类加载完后用这个符号引用去获取内存地址 + 例如:在com.demo.Solution类中引用了com.test.Quest,把`com.test.Quest`作为符号引用存进类常量池,在类加载完后,用这个符号引用去方法区找这个类的内存地址 -*** -### 线程状态 -进程的状态参考操作系统:创建态、就绪态、运行态、阻塞态、终止态 +*** -线程由生到死的完整过程(生命周期):当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态,在API中`java.lang.Thread.State`这个枚举中给出了六种线程状态: -| 线程状态 | 导致状态发生条件 | -| ----------------------- | ------------------------------------------------------------ | -| NEW(新建) | 线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread只有线程对象,没有线程特征。 | -| 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 +### 内存分配 -* RUNNABLE <--> WAITING: +#### 两种方式 - * 调用 obj.wait() 方法时 +为对象分配内存:首先计算对象占用空间大小,接着在堆中划分一块内存给新对象 - 调用 obj.notify()、obj.notifyAll()、t.interrupt(): +* 如果内存规整,使用指针碰撞(BumpThePointer) + 所有用过的内存在一边,空闲的内存在另外一边,中间有一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离 +* 如果内存不规整,虚拟机需要维护一个空闲列表(Free List)分配 + 已使用的内存和未使用的内存相互交错,虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容 - * 竞争锁成功,t 线程从 WAITING --> RUNNABLE - * 竞争锁失败,t 线程从 WAITING --> BLOCKED - * 当前线程调用 t.join() 方法,注意是当前线程在t 线程对象的监视器上等待 - * 当前线程调用 LockSupport.park() 方法 +*** -* RUNNABLE <--> TIMED_WAITING:调用 obj.wait(long n) 方法、当前线程调用 t.join(long n) 方法、当前线程调用 Thread.sleep(long n) -* RUNNABLE <--> BLOCKED:t 线程用 synchronized(obj) 获取了对象锁时竞争失败 +#### 分代思想 +##### 分代介绍 -*** +在java8时,堆被分为了两份:新生代和老年代(1:2),在java7时,还存在一个永久代 +- 新生代使用:复制算法 +- 老年代使用:标记 - 清除 或者 标记 - 整理 算法 +**Minor GC 和 Full GC**: -### 查看线程 +- Minor GC:回收新生代,新生代对象存活时间很短,所以 Minor GC 会频繁执行,执行的速度比较快 +- Full GC:回收老年代和新生代,老年代对象其存活时间长,所以 Full GC 很少执行,执行速度会比 Minor GC 慢很多 -windows: + Eden 和 Survivor 大小比例默认为 8:1:1 -* 任务管理器可以查看进程和线程数,也可以用来杀死进程 -* tasklist 查看进程 -* taskkill 杀死进程 + -linux: -* ps -ef 查看所有进程 -* ps -fT -p 查看某个进程(PID)的所有线程 -* kill 杀死进程 -* top 按大写 H 切换是否显示线程 -* top -H -p 查看某个进程(PID)的所有线程 -Java: -* jps 命令查看所有 Java 进程 -* jstack 查看某个 Java 进程(PID)的所有线程状态 -* jconsole 来查看某个 Java 进程中线程的运行情况(图形界面) +*** +##### 分代分配 -*** +工作机制: +* **对象优先在 Eden 分配**:当创建一个对象的时候,对象会被分配在新生代的 Eden 区,当Eden 区要满了时候,触发 YoungGC +* 当进行 YoungGC 后,此时在 Eden 区存活的对象被移动到 to 区,并且**当前对象的年龄会加1**,清空 Eden 区 -## 管程 +* 当再一次触发 YoungGC 的时候,会把 Eden 区中存活下来的对象和 to 中的对象,移动到 from 区中,这些对象的年龄会加 1,清空 Eden 区和 to 区 -### 临界区 +* to 区永远是空 Survivor 区,from 区是有数据的,每次 MinorGC 后两个区域互换 -临界资源:一次仅允许一个进程使用的资源成为临界资源 +晋升到老年代: -临界区:访问临界资源的代码块 +* **长期存活的对象进入老年代**:为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中 + `-XX:MaxTenuringThreshold`:定义年龄的阈值,对象头中用4个bit存储,所以最大值是15,默认也是15 +* **大对象直接进入老年代**:需要连续内存空间的对象,最典型的大对象是很长的字符串以及数组;避免在 Eden 和 Survivor 之间的大量复制;经常出现大对象会提前触发GC以获取足够的连续空间分配给大对象 + `-XX:PretenureSizeThreshold`:大于此值的对象直接在老年代分配 +* **动态对象年龄判定**:如果在Survivor区中相同年龄的对象的所有大小之和超过Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代 -竞态条件:多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件 +空间分配担保: -一个程序运行多个线程本身是没有问题的,多个线程访问共享资源会出现问题: +* 在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的 +* 如果不成立,虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于将尝试着进行一次 Minor GC;如果小于或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。 -* 多个线程读共享资源也没有问题 -* 在多个线程对共享资源读写操作时发生指令交错,就会出现问题 -为了避免临界区的竞态条件发生(解决线程安全问题): -* 阻塞式的解决方案:synchronized,Lock -* 非阻塞式的解决方案:原子变量 +*** -管程(monitor):由局部于自己的若干公共变量和所有访问这些公共变量的过程所组成的软件模块,保证同一时刻只有一个进程在管程内活动,即管程内定义的操作在同一时刻只被一个进程调用(由编译器实现) -**synchronized:对象锁,保证了临界区内代码的原子性**,采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其它线程获取这个对象锁时会阻塞,保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换 -互斥和同步都可以采用 synchronized 关键字来完成,区别: +#### TLAB -* 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码 -* 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点 +虚拟机采用了两种方式在创建对象时解决并发问题:CAS、TLAB -性能: +TLAB:Thread Local Allocation Buffer,为每个线程在堆内单独分配了一个缓冲区,多线程分配内存时,使用TLAB 可以避免线程安全问题,同时还能够提升内存分配的吞吐量,这种内存分配方式叫做**快速分配策略** -* 线程安全,性能差 -* 线程不安全性能好,假如开发中不会存在多线程安全问题,建议使用线程不安全的设计类 +- 栈上分配使用的是栈来进行对象内存的分配 +- TLAB 分配使用的是 Eden 区域进行内存分配,属于堆内存 +背景:堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据,由于对象实例的创建在JVM中非常频繁,因此在并发环境下为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度 +问题:堆空间都是共享的么? 不一定,因为还有TLAB,在堆中划分出一块区域,为每个线程所独占 -*** +![](https://gitee.com/seazean/images/raw/master/Java/JVM-TLAB内存分配策略.jpg) +JVM 是将 TLAB 作为内存分配的首选,但不是所有的对象实例都能够在 TLAB 中成功分配内存,一旦对象在TLAB空间分配内存失败时,JVM 就会通过**使用加锁机制确保数据操作的原子性**,从而直接在 Eden 空间中分配内存 +栈上分配优先于 TLAB 分配进行,逃逸分析中若可进行栈上分配优化,会优先进行对象栈上直接分配内存 -### syn-ed +参数设置: -#### 基本使用 +* `-XX:UseTLAB`:设置是否开启TLAB空间 -##### 同步代码块 +* `-XX:TLABWasteTargetPercent`:设置TLAB空间所占用Eden空间的百分比大小,默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1,即1% +* `-XX:TLABRefillWasteFraction`:指当 TLAB 空间不足,请求分配的对象内存大小超过此阈值时不会进行 TLAB 分配,直接进行堆内存分配,否则还是会优先进行 TLAB 分配 -锁对象:理论上可以是任意的“唯一”对象 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-TLAB内存分配过程.jpg) -synchronized是可重入、不公平的重量级锁 -原则上: -* 锁对象建议使用共享资源 -* 在实例方法中建议用this作为锁对象,锁住的this正好是共享资源 -* 在静态方法中建议用类名.class字节码作为锁对象 - * 因为静态成员属于类,被所有实例对象共享,所以需要锁住类 - * 锁住类以后,类的所有实例都相当于同一把锁,参考线程八锁 +*** -格式: -```java -synchronized(锁对象){ - // 访问共享资源的核心代码 -} -``` -实例: +#### 逃逸分析 -```java -public class demo { - static int counter = 0; - //static修饰,则元素是属于类本身的,不属于对象 ,与类一起加载一次,只有一个 - static final Object room = new Object(); - public static void main(String[] args) throws InterruptedException { - Thread t1 = new Thread(() -> { - for (int i = 0; i < 5000; i++) { - synchronized (room) { - counter++; - } - } - }, "t1"); - Thread t2 = new Thread(() -> { - for (int i = 0; i < 5000; i++) { - synchronized (room) { - counter--; - } - } - }, "t2"); - t1.start(); - t2.start(); - t1.join(); - t2.join(); - System.out.println(counter); - } -} -``` +即时编译(Just-in-time Compilation,JIT)是一种通过在运行时将字节码翻译为机器码,从而改善字节码编译语言性能的技术,在HotSpot实现中有多种选择:C1、C2 和 C1+C2,分别对应 client、server 和分层编译 +* C1编译速度快,优化方式比较保守;C2编译速度慢,优化方式比较激进 +* C1+C2在开始阶段采用C1编译,当代码运行到一定热度之后采用G2重新编译 +**逃逸分析**:并不是直接的优化手段,而是一个代码分析,通过动态分析对象的作用域,为其它优化手段如栈上分配、标量替换和同步消除等提供依据,发生逃逸行为的情况有两种:方法逃逸和线程逃逸 -*** +* 方法逃逸:当一个对象在方法中定义之后,被外部方法引用 + * 全局逃逸:一个对象的作用范围逃出了当前方法或者当前线程,比如对象是一个静态变量、全局变量赋值、已经发生逃逸的对象、作为当前方法的返回值 + * 参数逃逸:一个对象被作为方法参数传递或者被参数引用 +* 线程逃逸:如类变量或实例变量,可能被其它线程访问到 +如果不存在逃逸行为,则可以对该对象进行如下优化:同步消除、标量替换和栈上分配 +* **同步消除** -##### 同步方法 + 线程同步本身比较耗时,如果确定一个对象不会逃逸出线程,不被其它线程访问到,那对象的读写就不会存在竞争,则可以消除对该对象的**同步锁**,通过`-XX:+EliminateLocks`可以开启同步消除 ( - 号关闭) -作用:把出现线程安全问题的核心方法锁起来,每次只能一个线程进入访问 +* **标量替换** -用法:直接给方法加上一个修饰符 synchronized + * 标量替换:如果把一个对象拆散,将其成员变量恢复到基本类型来访问 + * 标量 (scalar) :不可分割的量,如基本数据类型和reference类型 + 聚合量 (Aggregate):一个数据可以继续分解,对象一般是聚合量 + * 如果逃逸分析发现一个对象不会被外部访问,并且该对象可以被拆散,那么经过优化之后,并不直接生成该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替 + * 参数设置: + `-XX:+EliminateAllocations`:开启标量替换 + `-XX:+PrintEliminateAllocations`:查看标量替换情况 -```java -//同步方法 -修饰符 synchronized 返回值类型 方法名(方法参数) { - 方法体; -} -//同步静态方法 -修饰符 static synchronized 返回值类型 方法名(方法参数) { - 方法体; -} -``` +* **栈上分配** -同步方法底层也是有锁对象的: + JIT编译器在编译期间根据逃逸分析的结果,如果一个对象没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收,这样就无需GC -* 如果方法是实例方法:同步方法默认用this作为的锁对象 + User对象的作用域局限在方法fn中,可以使用标量替换的优化手段在栈上分配对象的成员变量,这样就不会生成User对象,大大减轻GC的压力 ```java - public synchronized void test() {} //等价于 - public void test() { - synchronized(this) {} + 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; + } } ``` -* 如果方法是静态方法:同步方法默认用类名.class作为的锁对象 + - ```java - class Test{ - public synchronized static void test() {} - } - //等价于 - class Test{ - public void test() { - synchronized(Test.class) {} - } - } - ``` +*** -面向对象实例: -```java -public class Demo { - public static void main(String[] args) throws InterruptedException { - Room room = new Room(); - Thread t1 = new Thread(() -> { - for (int j = 0; j < 5000; j++) { - room.increment(); - } - }, "t1"); - Thread t2 = new Thread(() -> { - for (int j = 0; j < 5000; j++) { - room.decrement(); - } - }, "t2"); - t1.start(); - t2.start(); - t1.join(); - t2.join(); - System.out.println(room.get()); - } -} -class Room { - int value = 0; - public synchronized void increment() { - value++; - } - public synchronized void decrement() { - value--; - } - public synchronized int get() { - return value; - } -} -``` +### 回收策略 + +#### 触发条件 +内存垃圾回收机制主要集中的区域就是线程共享区域:**堆和方法区** +Minor GC 触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC -*** +FullGC 同时回收新生代、老年代和方法区,只会存在一个FullGC的线程进行执行,其他的线程全部会被**挂起**,有以下触发条件: +* 调用 System.gc(): + * 在默认情况下,通过 System.gc() 或 Runtime.getRuntime().gc() 的调用,会显式触发 FullGC,同时对老年代和新生代进行回收,但是虚拟机不一定真正去执行,无法保证对垃圾收集器的调用 (马上触发GC) + * 不建议使用这种方式,应该让虚拟机管理内存。一般情况下,垃圾回收应该是自动进行的,无须手动触发;在一些特殊情况下,如正在编写一个性能基准,可以在运行之间调用 System.gc() -##### 线程八锁 +* 老年代空间不足: -所谓的“线程八锁”,其实就是考察 synchronized 锁住的是哪个对象,直接百度搜索 + * 为了避免引起的 Full GC,应当尽量不要创建过大的对象以及数组 + * 通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间 -说明:主要关注锁住的对象是不是同一个 +* 空间分配担保失败 -* 锁住类对象,所有类的实例的方法都是安全的 -* 锁住this对象,只有在当前实例对象的线程内是安全的,如果有多个实例就不安全 +* JDK 1.7 及以前的永久代空间不足 -线程不安全:因为锁住的不是同一个对象,线程1调用a方法锁住的类对象,线程2调用b方法锁住的n2对象,不是同一个对象 +* Concurrent Mode Failure: -```java -class Number{ - public static synchronized void a(){ - Thread.sleep(1000); - System.out.println("1"); - } - public synchronized void b() { - System.out.println("2"); - } -} -public static void main(String[] args) { - Number n1 = new Number(); - Number n2 = new Number(); - new Thread(()->{ n1.a(); }).start(); - new Thread(()->{ n2.b(); }).start(); -} -``` + 执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC -线程安全:因为n1调用a()方法,锁住的是类对象,n2调用b()方法,锁住的也是类对象,所以线程安全 +手动GC测试,VM参数:`-XX:+PrintGcDetails` ```java -class Number{ - public static synchronized void a(){ - Thread.sleep(1000); - System.out.println("1"); - } - public static synchronized void b() { - System.out.println("2"); - } -} -public static void main(String[] args) { - Number n1 = new Number(); - Number n2 = new Number(); - new Thread(()->{ n1.a(); }).start(); - new Thread(()->{ n2.b(); }).start(); +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置空 +} +``` @@ -18477,96 +10172,45 @@ public static void main(String[] args) { -#### 锁原理 - -##### Monitor - -Monitor 被翻译为监视器或管程 - -每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 -Mark Word 中就被设置指向 Monitor 对象的指针,这就是重量级锁 +#### 安全区域 -* Mark Word结构: +安全点 (Safepoint):程序执行时并非在所有地方都能停顿下来开始 GC,只有在安全点才能停下 - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-Monitor-MarkWord结构32位.png) +- Safe Point 的选择很重要,如果太少可能导致 GC 等待的时间太长,如果太多可能导致运行时的性能问题 +- 大部分指令的执行时间都非常短,通常会根据是否具有让程序长时间执行的特征为标准,选择些执行时间较长的指令作为 Safe Point, 如方法调用、循环跳转和异常跳转等 -* 64位虚拟机Mark Word: +在 GC 发生时,让所有线程都在最近的安全点停顿下来的方法: - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-Monitor-MarkWord结构64位.png) +- 抢先式中断:没有虚拟机采用,首先中断所有线程,如果有线程不在安全点,就恢复线程让线程运行到安全点 +- 主动式中断:设置一个中断标志,各个线程运行到各个 Safe Point 时就轮询这个标志,如果中断标志为真,则将自己进行中断挂起 -工作流程: +问题:Safepoint 保证程序执行时,在不太长的时间内就会遇到可进入GC 的 Safepoint,但是当线程处于 Waiting 状态或 Blocked 状态,线程无法响应 JVM 的中断请求,运行到安全点去中断挂起,JVM 也不可能等待线程被唤醒,对于这种情况,需要安全区域来解决 -* 开始时 Monitor 中 Owner 为 null -* 当 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 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争是**非公平的** -* WaitSet 中的Thread-0,是以前获得过锁,但条件不满足进入 WAITING 状态的线程(wait-notify ) +安全区域 (Safe Region):指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始 GC 都是安全的 -![](https://gitee.com/seazean/images/raw/master/Java/JUC-Monitor工作原理2.png) +运行流程: -注意: +- 当线程运行到 Safe Region 的代码时,首先标识已经进入了 Safe Region,如果这段时间内发生GC,JVM会忽略标识为 Safe Region 状态的线程 -* synchronized 必须是进入同一个对象的 monitor 才有上述的效果 -* 不加 synchronized 的对象不会关联监视器,不遵从以上规则 +- 当线程即将离开 Safe Region 时,会检查JVM是否已经完成GC,如果完成了则继续运行,否则线程必须等待GC 完成,收到可以安全离开 SafeRegion 的信号 -**** +*** -##### 字节码 +### 垃圾判断 -代码: +#### 垃圾介绍 -```java -public static void main(String[] args) { - Object lock = new Object(); - synchronized (lock) { - System.out.println("ok"); - } -} -``` +垃圾:**如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾** -```java -0: new #2 // new Object -3: dup -4: invokespecial #1 // invokespecial :()V,非虚方法 -7: astore_1 // lock引用 -> lock -8: aload_1 // lock (synchronized开始) -9: dup //一份用来初始化,一份用来引用 -10: astore_2 // lock引用 -> slot 2 -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 -22: goto 30 -25: astore_3 // any -> slot 3 -26: aload_2 // slot 2(lock引用) -27: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList -28: aload_3 -29: athrow -30: return -Exception table: - from to target type - 12 22 25 any - 25 28 25 any -LineNumberTable: ... -LocalVariableTable: - Start Length Slot Name Signature - 0 31 0 args [Ljava/lang/String; - 8 23 1 lock Ljava/lang/Object; -``` +作用:释放没用的对象,清除内存里的记录碎片,碎片整理将所占用的堆内存移到堆的一端,以便JVM 将整理出的内存分配给新的对象 -说明: +垃圾收集主要是针对堆和方法区进行,程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收 -* 通过异常**try-catch机制**,确保一定会被解锁 -* 方法级别的 synchronized 不会在字节码指令中有所体现 +在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段,判断对象存活一般有两种方式:**引用计数算法**和**可达性分析算法** @@ -18574,58 +10218,67 @@ LocalVariableTable: -#### 锁升级 - -##### 升级过程 - -**synchronized 是可重入、不公平的重量级锁**,所以可以对其进行优化 +#### 引用计数法 -```java -无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁 //随着竞争的增加,只能锁升级,不能降级 -``` +引用计数算法(Reference Counting):对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1;当对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收(Java没有采用) -![](https://gitee.com/seazean/images/raw/master/Java/JUC-锁升级过程.png) +优点: +- 回收没有延迟性,无需等到内存不够的时候才开始回收,运行时根据对象计数器是否为0,可以直接回收 +- 在垃圾回收过程中,应用无需挂起;如果申请内存时,内存不足,则立刻报OOM错误。 +- 区域性,更新对象的计数器时,只是影响到该对象,不会扫描全部对象 +缺点: -*** +- 每次对象被引用时,都需要去更新计数器,有一点时间开销。 +- 浪费CPU资源,即使内存够用,仍然在运行时进行计数器的统计。 +- **无法解决循环引用问题,会引发内存泄露**(最大的缺点) -##### 偏向锁 + 内存泄漏(Memory Leak):是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果 -偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程之后重新获取该锁不再需要同步操作: + ```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 + } + } + ``` -* 当锁对象第一次被线程获得的时候进入偏向状态,标记为101,同时使用 CAS 操作将线程 ID 记录到Mark Word。如果 CAS 操作成功,这个线程以后进入这个锁相关的同步块,查看这个线程 ID 是自己的就表示没有竞争,就不需要再进行任何同步操作 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-循环引用.png) -* 当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定状态或轻量级锁状态 - -一个对象创建时: +*** -* 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的 - thread、epoch、age 都为 0 -* 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加VM参数 `-XX:BiasedLockingStartupDelay=0` 来禁用延迟 - JDK 8 延迟 4s 开启偏向锁原因:在刚开始执行代码时,会有好多线程来抢锁,如果开偏向锁效率反而降低 -* 如果禁用了偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,**第一次用到 hashcode 时才会赋值**,添加 VM 参数 `-XX:-UseBiasedLocking` 禁用偏向锁 -撤销偏向锁的状态: +#### 可达性分析 -* 调用对象的 hashCode:偏向锁的对象 MarkWord 中存储的是线程 id,调用 hashCode 导致偏向锁被撤销 - * 轻量级锁会在锁记录中记录 hashCode - * 重量级锁会在 Monitor 中记录 hashCode -* 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁 -* 调用 wait/notify +##### GC Roots +可达性分析算法:也可以称为根搜索算法、追踪性垃圾收集 +**GC Roots对象**: -**批量撤销**:如果对象被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID +- 虚拟机栈中局部变量表中引用的对象:各个线程被调用的方法中使用到的参数、局部变量等 +- 本地方法栈中引用的对象 +- 方法区中类静态属性引用的对象 +- 方法区中的常量引用的对象 +- 字符串常量池(string Table)里的引用 +- 同步锁 synchronized 持有的对象 -* 批量重偏向:当撤销偏向锁阈值超过 20 次后,jvm会觉得是不是偏向错了,于是在给这些对象加锁时重新偏向至加锁线程 +GC Roots说明: -* 批量撤销:当撤销偏向锁阈值超过 40 次后,jvm会觉得自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的 +* **GC Roots是一组活跃的引用,不是对象**,放在 GC Roots Set 集合 +* 如果一个指针保存了堆内存中的对象,但自己不在堆内存中,它就是一个Root @@ -18633,162 +10286,130 @@ LocalVariableTable: -##### 轻量级锁 - -一个对象有多个线程要加锁,但加锁的时间是错开的(没有竞争),那么可以使用轻量级锁来优化,轻量级锁对使用者是透明的(不可见) +##### 工作原理 -可重入锁:**线程可以进入任何一个它已经拥有的锁所同步着的代码块,可重入锁最大的作用是避免死锁** +可达性分析算法以**根对象集合(GCRoots)**为起始点,从上至下的方式搜索被根对象集合所连接的目标对象 -轻量级锁在没有竞争时(锁重入时),每次重入仍然需要执行 CAS 操作,Java 6 才引入的偏向锁来优化 +分析工作必须在一个能保障**一致性的快照**中进行,否则结果的准确性无法保证,这点也是导致 GC 进行时必须 Stop The World 的一个重要原因 -锁重入实例: +基本原理: -```java -static final Object obj = new Object(); -public static void method1() { - synchronized( obj ) { - // 同步块 A - method2(); - } -} -public static void method2() { - synchronized( obj ) { - // 同步块 B - } -} -``` +- 可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索走过的路径称为引用链 -* 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,存储锁定对象的Mark Word +- 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象 - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-轻量级锁原理1.png) +- 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象 -* 让锁记录中Object reference指向锁对象,并尝试用CAS替换Object的Mark Word,将Mark Word的值存 - 入锁记录 + -* 如果CAS替换成功,对象头中存储了锁记录地址和状态 00(轻量级锁) ,表示由该线程给对象加锁 - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-轻量级锁原理2.png) -* 如果CAS失败,有两种情况: - * 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程 - * 如果是自己执行了 synchronized 锁重入,就添加一条 Lock Record 作为重入的计数 +*** - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-轻量级锁原理3.png) -* 当退出 synchronized 代码块(解锁时) - * 如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减1 - * 如果锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头 - * 成功,则解锁成功 - * 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程 +##### 三色标记 +###### 标记算法 +三色标记法把遍历对象图过程中遇到的对象,按“是否访问过”这个条件标记成以下三种颜色: -*** +- 白色:尚未访问过 +- 灰色:本对象已访问过,但是本对象引用到的其他对象尚未全部访问 +- 黑色:本对象已访问过,而且本对象引用到的其他对象也全部访问完成 +当 Stop The World (STW) 时,对象间的引用是不会发生变化的,可以轻松完成标记,遍历访问过程为: +1. 初始时,所有对象都在白色集合 +2. 将 GC Roots 直接引用到的对象挪到灰色集合 +3. 从灰色集合中获取对象: + * 将本对象引用到的其他对象全部挪到灰色集合中 + * 将本对象挪到黑色集合里面 +4. 重复步骤3,直至灰色集合为空时结束 +5. 结束后,仍在白色集合的对象即为GC Roots 不可达,可以进行回收 -##### 锁膨胀 + -在尝试加轻量级锁的过程中,CAS 操作无法成功,可能是其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为**重量级锁** +参考文章:https://www.jianshu.com/p/12544c0ad5c1 -* 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁 - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-重量级锁原理1.png) -* Thread-1 加轻量级锁失败,进入锁膨胀流程:为 Object 对象申请 Monitor 锁,让Object 对象头指向重量级锁地址,Monitor 的 Owner 置为 Thread-0,然后自己进入 Monitor 的 EntryList BLOCKED +###### 并发标记 - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-重量级锁原理2.png) +并发标记时,对象间的引用可能发生变化**,**多标和漏标的情况就有可能发生 -* 当Thread-0退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败,这时进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程 +**多标情况:**当E变为灰色或黑色时,其他线程断开的 D 对 E 的引用,导致这部分对象仍会被标记为存活,本轮 GC 不会回收这部分内存,这部分本应该回收但是没有回收到的内存,被称之为**浮动垃圾** +* 针对并发标记开始后的**新对象**,通常的做法是直接全部当成黑色,也算浮动垃圾 +* 浮动垃圾并不会影响应用程序的正确性,只是需要等到下一轮垃圾回收中才被清除 + +**漏标情况:** +* 条件一:灰色对象断开了对一个白色对象的引用(直接或间接),即灰色对象原成员变量的引用发生了变化 +* 条件二:其他线程中修改了黑色对象,插入了一条或多条对该白色对象的新引用 +* 结果:导致该白色对象当作垃圾被GC,影响到了应用程序的正确性 -*** + +代码角度解释漏标: +```java +Object G = objE.fieldG; // 读 +objE.fieldG = null; // 写 +objD.fieldG = G; // 写 +``` -#### 锁优化 +为了解决问题,可以操作上面三步,**将对象G记录起来,然后作为灰色对象再进行遍历**,比如放到一个特定的集合,等初始的 GC Roots遍历完(并发标记),再遍历该集合(重新标记) -##### 自旋锁 +> 所以重新标记需要STW,应用程序一直在运行,该集合可能会一直增加新的对象,导致永远都运行不完 -**重量级锁竞争**时,尝试获取锁的线程不会立即阻塞,可以使用**自旋**来进行优化,采用循环的方式去尝试获取锁 +解决方法:添加读写屏障,读屏障拦截第一步,写屏障拦截第二三步,在读写前后进行一些后置处理: -注意: +* **写屏障 + 增量更新**:黑色对象新增引用,会将黑色对象变成灰色对象,最后对该节节点重新扫描 -* 自旋占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势 -* 自旋失败的线程会进入阻塞状态 + 增量更新 (Incremental Update) 破坏了条件二,从而保证了不会漏标 -优点:不会进入阻塞状态,减少线程上下文切换的消耗 + 缺点:对黑色变灰的对象重新扫描所有引用,比较耗费时间 -缺点:当自旋的线程越来越多时,会不断的消耗CPU资源 +* **写屏障 (Store Barrier) + SATB**:当原来成员变量的引用发生变化之前,记录下原来的引用对象 -自旋锁情况: + 保留 GC 开始时的对象图,即原始快照 SATB,当 GC Roots 确定后,对象图就已经确定,那后续的标记也应该是按照这个时刻的对象图走,如果期间对白色对象有了新的引用会记录下来,并且将白色对象变灰,重新扫描该对象 -* 自旋成功的情况: - + SATB (Snapshot At The Beginning) 破坏了条件一,从而保证了不会漏标 -* 自旋失败的情况: +* **读屏障 (Load Barrier)**:破坏条件二,黑色对象引用白色对象的前提是获取到该对象,此时读屏障发挥作用 - +以 Java HotSpot VM 为例,其并发标记时对漏标的处理方案如下: -自旋锁说明: +- CMS:写屏障 + 增量更新 +- G1:写屏障 + SATB +- ZGC:读屏障 -* 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会 - 高,就多自旋几次;反之,就少自旋甚至不自旋,比较智能 -* Java 7 之后不能控制是否开启自旋功能,由JVM控制 -```java -//手写自旋锁 -public class SpinLock { - // 泛型装的是Thread,原子引用线程 - AtomicReference atomicReference = new AtomicReference<>(); - public void lock() { - Thread thread = Thread.currentThread(); - System.out.println(thread.getName() + " come in"); +*** - //开始自旋,期望值为null,更新值是当前线程 - while (!atomicReference.compareAndSet(null, thread)) { - Thread.sleep(1000); - System.out.println(thread.getName() + " 正在自旋"); - } - System.out.println(thread.getName() + " 自旋成功"); - } - public void unlock() { - Thread thread = Thread.currentThread(); - //线程使用完锁把引用变为null - atomicReference.compareAndSet(thread, null); - System.out.println(thread.getName() + " invoke unlock"); - } +#### finalization - public static void main(String[] args) throws InterruptedException { - SpinLock lock = new SpinLock(); - new Thread(() -> { - //占有锁 - lock.lock(); - Thread.sleep(10000); +Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑 - //释放锁 - lock.unlock(); - },"t1").start(); +垃圾回收此对象之前,会先调用这个对象的finalize()方法,finalize() 方法允许在子类中被重写,用于在对象被回收时进行后置处理,通常在这个方法中进行一些资源释放和清理,比如关闭文件、套接字和数据库连接等 - // 让main线程暂停1秒,使得t1线程,先执行 - Thread.sleep(1000); +生存OR死亡:如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用,此对象需要被回收。但事实上这时候它们暂时处于”缓刑“阶段。**一个无法触及的对象有可能在某一个条件下“复活”自己**,如果这样对它的回收就是不合理的,所以虚拟机中的对象可能的三种状态: - new Thread(() -> { - lock.lock(); - lock.unlock(); - },"t2").start(); - } -} -``` +- 可触及的:从根节点开始,可以到达这个对象。 +- 可复活的:对象的所有引用都被释放,但是对象有可能在finalize() 中复活 +- 不可触及的:对象的 finalize() 被调用,并且没有复活,那么就会进入不可触及状态,不可触及的对象不可能被复活。因为**finalize()只会被调用一次**,等到这个对象再被标记为可回收时就必须回收 +永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用,原因: +* finalize() 时可能会导致对象复活 +* finalize() 方法的执行时间是没有保障的,完全由GC线程决定,极端情况下,若不发生GC,则finalize()方法将没有执行机会,因为优先级比较低,即使主动调用该方法,也不会因此就直接进行回收 +* 一个糟糕的finalize() 会严重影响GC的性能 @@ -18796,88 +10417,84 @@ public class SpinLock { -##### 锁消除 - -锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除 - -锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除(同步消除:JVM内存分配) +#### 引用分析 +无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关,Java 提供了四种强度不同的引用类型 +1. 强引用:被强引用关联的对象不会被回收,只有所有GCRoots都不通过强引用引用该对象,才能被垃圾回收 -*** + * 强引用可以直接访问目标对象 + * 虚拟机宁愿抛出OOM异常,也不会回收强引用所指向对象 + * 强引用可能导致**内存泄漏**(引用计数法章节解释了什么是内存泄漏) + ```java + Object obj = new Object();//使用 new 一个新对象的方式来创建强引用 + ``` +2. 软引用(SoftReference):被软引用关联的对象只有在内存不够的情况下才会被回收 -##### 锁粗化 + * **仅(可能有强引用,一个对象可以被多个引用)**有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象 + * 配合**引用队列来释放软引用自身**,在构造软引用时,可以指定一个引用队列,当软引用对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况 + * 软引用通常用来实现内存敏感的缓存,比如高速缓存就有用到软引用;如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时不会耗尽内存 -对相同对象多次加锁,导致线程发生多次重入,频繁的加锁操作就会导致性能损耗,可以使用锁粗化方式优化 + ```java + Object obj = new Object(); + SoftReference sf = new SoftReference(obj); + obj = null; // 使对象只被软引用关联 + ``` -如果虚拟机探测到一串零碎的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部 +3. 弱引用(WeakReference):被弱引用关联的对象一定会被回收,只能存活到下一次垃圾回收发生之前 -* 一些看起来没有加锁的代码,其实隐式的加了很多锁: + * 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象 + * 配合引用队列来释放弱引用自身 + * WeakHashMap用来存储图片信息,可以在内存不足的时候及时回收,避免了OOM - ```java - public static String concatString(String s1, String s2, String s3) { - return s1 + s2 + s3; - } - ``` + ```java + Object obj = new Object(); + WeakReference wf = new WeakReference(obj); + obj = null; + ``` -* String 是一个不可变的类,编译器会对 String 的拼接自动优化。在 JDK 1.5 之前,转化为StringBuffer对象的连续 append() 操作,每个append() 方法中都有一个同步块 +4. 虚引用(PhantomReference):也称为“幽灵引用”或者“幻影引用”,是所有引用类型中最弱的一个 - ```java - public static String concatString(String s1, String s2, String s3) { - StringBuffer sb = new StringBuffer(); - sb.append(s1); - sb.append(s2); - sb.append(s3); - return sb.toString(); - } - ``` + * 一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象 + * 为对象设置虚引用的唯一目的是在于跟踪垃圾回收过程,能在这个对象被回收时收到一个系统通知 + * 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存 -扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,只需要加锁一次就可以 + ```java + Object obj = new Object(); + PhantomReference pf = new PhantomReference(obj, null); + obj = null; + ``` +5. 终结器引用(finalization) + 引用的四种状态: -**** +* Active:激活,创建 ref 对象时就是激活状态 +* Pending:等待入队,所对应的强引用被GC,就要进入引用队列 +* Enqueued:入队了 + * 如果指定了 refQueue,pending 移动到 enqueued 状态,refQueue.poll 时进入失效状态 + * 如果没有指定 refQueue,直接到失效状态 +* Inactive:失效 -#### 多把锁 +*** -多把不相干的锁:一间大屋子有两个功能:睡觉、学习,互不相干。现在一人要学习,一人要睡觉,如果只用一间屋子(一个对象锁)的话,那么并发度很低 -将锁的粒度细分: -* 好处,是可以增强并发度 -* 坏处,如果一个线程需要同时获得多把锁,就容易发生死锁 +#### 无用类 -解决方法:准备多个对象锁 +方法区主要回收的是无用的类 -```java -public static void main(String[] args) { - BigRoom bigRoom = new BigRoom(); - new Thread(() -> { bigRoom.study(); }).start(); - new Thread(() -> { bigRoom.sleep(); }).start(); -} -class BigRoom { - private final Object studyRoom = new Object(); - private final Object sleepRoom = new Object(); - - public void sleep() throws InterruptedException { - synchronized (sleepRoom) { - System.out.println("sleeping 2 小时"); - Thread.sleep(2000); - } - } +判定一个类是否是无用的类,需要同时满足下面 3 个条件 : - public void study() throws InterruptedException { - synchronized (studyRoom) { - System.out.println("study 1 小时"); - Thread.sleep(1000); - } - } -} -``` +- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例 +- 加载该类的`ClassLoader`已经被回收 +- 该类对应的`java.lang.Class`对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法 + +虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收 @@ -18885,181 +10502,82 @@ class BigRoom { -#### 活跃性 +### 回收算法 -##### 死锁 +#### 标记清除 -###### 形成 +当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。目前在JVM中比较常见的三种垃圾收集算法是 -死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放,由于线程被无限期地阻塞,因此程序不可能正常终止 +- 标记清除算法(Mark-Sweep) +- 复制算法(copying) +- 标记压缩算法(Mark-Compact) -java 死锁产生的四个必要条件: +标记清除算法,是将垃圾回收分为2个阶段,分别是**标记和清除** -1. 互斥条件,即当资源被一个线程使用(占有)时,别的线程不能使用 -2. 不可剥夺条件,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放 -3. 请求和保持条件,即当资源请求者在请求其他的资源的同时保持对原有资源的占有 -4. 循环等待条件,即存在一个等待循环队列:p1 要 p2 的资源,p2 要 p1 的资源,形成了一个等待环路 +- **标记**:Collector从引用根节点开始遍历,**标记所有被引用的对象**,一般是在对象的Header中记录为可达对象,**标记的是引用的对象,不是垃圾** +- **清除**:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收,把分块连接到 “**空闲列表**” 的单向链表,判断回收后的分块与前一个空闲分块是否连续,若连续会合并这两个分块,之后进行分配时只需要遍历这个空闲列表,就可以找到分块 -四个条件都成立的时候,便形成死锁。死锁情况下打破上述任何一个条件,便可让死锁消失 +- **分配阶段**:程序会搜索空闲链表寻找空间大于等于新对象大小 size 的块 block,如果找到的块等于 size,会直接返回这个分块;如果找到的块大于 size,会将块分割成大小为 size 与 (block - size) 的两部分,返回大小为 size 的分块,并把大小为 (block - size) 的块返回给空闲列表 -```java -public class Dead { - public static Object resources1 = new Object(); - public static Object resources2 = new Object(); - public static void main(String[] args) { - new Thread(() -> { - // 线程1:占用资源1 ,请求资源2 - synchronized(resources1){ - System.out.println("线程1已经占用了资源1,开始请求资源2"); - Thread.sleep(2000);//休息两秒,防止线程1直接运行完成。 - //2秒内线程2肯定可以锁住资源2 - synchronized (resources2){ - System.out.println("线程1已经占用了资源2"); - } - }).start(); - new Thread(() -> { - // 线程2:占用资源2 ,请求资源1 - synchronized(resources2){ - System.out.println("线程2已经占用了资源2,开始请求资源1"); - Thread.sleep(2000); - synchronized (resources1){ - System.out.println("线程2已经占用了资源1"); - } - }} - }).start(); - } -} -``` +算法缺点: -面向对象写法: +- 标记和清除过程效率都不高 +- 会产生大量不连续的内存碎片,导致无法给大对象分配内存,需要维护一个空闲链表 -```java -public class DeadLock { - static String lockA = "lockA"; - static String lockB = "lockB"; - public static void main(String[] args) { - new Thread(new HoldLockThread(lockA, lockB)).start(); - new Thread(new HoldLockThread(lockB, lockA)).start(); - } -} -class HoldLockThread implements Runnable { - private String lockA; - private String lockB; + - public HoldLockThread(String lockA, String lockB) { - this.lockA = lockA; - this.lockB = lockB; - } - @Override - public void run() { - synchronized (lockA) { - System.out.println(Thread.currentThread().getName() + " 持有" + lockA + ",尝试获得" + lockB); - try { - Thread.sleep(2000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - synchronized (lockB) { - System.out.println(Thread.currentThread().getName() + " 持有" + lockB + ",尝试获得" + lockA); - } - } - } -} -``` +*** -###### 定位 -定位死锁的方法: +#### 复制算法 -* 使用 jps 定位进程 id,再用 `jstack id` 定位死锁,找到死锁的线程去查看源码,解决优化 +复制算法的核心就是,**将原有的内存空间一分为二,每次只用其中的一块**,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清理,交换两个内存的角色,完成垃圾的回收 - ```sh - "Thread-1" #12 prio=5 os_prio=0 tid=0x000000001eb69000 nid=0xd40 waiting formonitor entry [0x000000001f54f000] - java.lang.Thread.State: BLOCKED (on object monitor) - #省略 - "Thread-1" #12 prio=5 os_prio=0 tid=0x000000001eb69000 nid=0xd40 waiting for monitor entry [0x000000001f54f000] - java.lang.Thread.State: BLOCKED (on object monitor) - #省略 - - Found one Java-level deadlock: - =================================================== - "Thread-1": - waiting to lock monitor 0x000000000361d378 (object 0x000000076b5bf1c0, a java.lang.Object), - which is held by "Thread-0" - "Thread-0": - waiting to lock monitor 0x000000000361e768 (object 0x000000076b5bf1d0, a java.lang.Object), - which is held by "Thread-1" - - Java stack information for the threads listed above: - =================================================== - "Thread-1": - at thread.TestDeadLock.lambda$main$1(TestDeadLock.java:28) - - waiting to lock <0x000000076b5bf1c0> (a java.lang.Object) - - locked <0x000000076b5bf1d0> (a java.lang.Object) - at thread.TestDeadLock$$Lambda$2/883049899.run(Unknown Source) - at java.lang.Thread.run(Thread.java:745) - "Thread-0": - at thread.TestDeadLock.lambda$main$0(TestDeadLock.java:15) - - waiting to lock <0x000000076b5bf1d0> (a java.lang.Object) - - locked <0x000000076b5bf1c0> (a java.lang.Object) - at thread.TestDeadLock$$Lambda$1/495053715 - ``` - -* linux 下可以通过 top 先定位到 CPU 占用高的 Java 进程,再利用 `top -Hp 进程id` 来定位是哪个线程,最后再用 jstack 的输出来看各个线程栈, +应用场景:如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之,则不适合 -* 避免死锁:避免死锁要注意加锁顺序 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-复制算法.png) -* 可以使用 jconsole 工具,在 `jdk\bin` 目录下 +算法优点: +- 没有标记和清除过程,实现简单,运行高效 +- 复制过去以后保证空间的连续性,不会出现“碎片”问题。 +算法缺点: -*** +- 主要不足是**只使用了内存的一半** +- 对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销都不小 +现在的商业虚拟机都采用这种收集算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间 -##### 活锁 -活锁:指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试—失败—尝试—失败的过程 +*** -两个线程互相改变对方的结束条件,最后谁也无法结束: -```java -class TestLiveLock { - static volatile int count = 10; - static final Object lock = new Object(); - public static void main(String[] args) { - new Thread(() -> { - // 期望减到 0 退出循环 - while (count > 0) { - Thread.sleep(200); - count--; - System.out.println("线程一count:" + count); - } - }, "t1").start(); - new Thread(() -> { - // 期望超过 20 退出循环 - while (count < 20) { - Thread.sleep(200); - count++; - System.out.println("线程二count:"+ count); - } - }, "t2").start(); - } -} -``` +#### 标记整理 +标记整理(压缩)算法是在标记清除算法的基础之上,做了优化改进的算法 -*** +标记阶段和标记清除算法一样,也是从根节点开始,对对象的引用进行标记,在清理阶段,并不是简单的直接清理可回收对象,而是将存活对象都向内存另一端移动,然后清理边界以外的垃圾,从而**解决了碎片化**的问题 + +优点:不会产生内存碎片 +缺点:需要移动大量对象,处理效率比较低 + -##### 饥饿 +| | Mark-Sweep | Mark-Compact | Copying | +| -------- | ---------------- | -------------- | ----------------------------------- | +| 速度 | 中等 | 最慢 | 最快 | +| 空间开销 | 少(但会堆积碎片) | 少(不堆积碎片) | 通常需要活对象的2倍大小(不堆积碎片) | +| 移动对象 | 否 | 是 | 是 | -饥饿:一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束 +- 效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存。 +- 为了尽量兼顾三个指标,标记一整理算法相对来说更平滑一些 @@ -19067,416 +10585,220 @@ class TestLiveLock { -### wait-ify +#### 增量收集 -#### 基本使用 +增量收集算法:通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作,基础仍是标记-清除和复制算法,用于多线程并发环境 -需要获取对象锁后才可以调用`锁对象.wait()`,调用notify随机唤醒一个线程,notifyAll唤醒所有线程去竞争CPU +工作原理:如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,影响系统的交互性,所以让垃圾收集线程和应用程序线程交替执行,每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程,依次反复直到垃圾收集完成 -Object类API: +缺点:线程切换和上下文转换消耗资源,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降 -```java -public final void notify():唤醒正在等待对象监视器的单个线程。 -public final void notifyAll():唤醒正在等待对象监视器的所有线程。 -public final void wait():导致当前线程等待,直到另一个线程调用该对象的notify()方法或 notifyAll()方法。 -public final native void wait(long timeout):有时限的等待, 到n毫秒后结束等待,或是被唤醒 -``` -对比sleep(): -* 原理不同:sleep()方法是属于Thread类,是线程用来控制自身流程的,使此线程暂停执行一段时间而把执行机会让给其他线程;wait()方法属于Object类,用于线程间通信 -* 对锁的处理机制不同:调用sleep()方法的过程中,线程不会释放对象锁,当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,但是都会释放CPU -* 使用区域不同:wait()方法必须放在**同步控制方法和同步代码块(先获取锁)**中使用,sleep() 方法则可以放在任何地方使用 -底层原理: -* Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态 -* BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片 -* BLOCKED 线程会在 Owner 线程释放锁时唤醒 -* WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入 - EntryList 重新竞争 +*** -![](https://gitee.com/seazean/images/raw/master/Java/JUC-Monitor工作原理2.png) +### 垃圾回收器 -*** +#### 概述 +垃圾收集器分类: +* 按线程数分(垃圾回收线程数),可以分为串行垃圾回收器和并行垃圾回收器 + * 除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行 +* 按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器 + * 并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间 + * 独占式垃圾回收器(Stop the world)一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束 +* 按碎片处理方式分,可分为压缩式垃圾回收器和非压缩式垃圾回收器 + * 压缩式垃圾回收器在回收完成后进行压缩整理,消除回收后的碎片,再分配对象空间使用指针碰撞 + * 非压缩式的垃圾回收器不进行这步操作,再分配对象空间使用空闲列表 +* 按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器 -#### 代码优化 +GC性能指标: -虚假唤醒:notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程 +- **吞吐量**:程序的运行时间占总运行时间的比例(总运行时间 = 程序的运行时间 + 内存回收的时间) +- 垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例 +- **暂停时间**:执行垃圾收集时,程序的工作线程被暂停的时间 +- **收集频率**:相对于应用程序的执行,收集操作发生的频率 +- **内存占用**:Java堆区所占的内存大小 +- 快速:一个对象从诞生到被回收所经历的时间 -解决方法:采用notifyAll +**垃圾收集器的组合关系**: -notifyAll 仅解决某个线程的唤醒问题,使用 if + wait 判断仅有一次机会,一旦条件不成立,无法重新判断 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-垃圾回收器关系图.png) -解决方法:用 while + wait,当条件不成立,再次 wait +新生代收集器:Serial、ParNew、Paralle1 Scavenge; -```java -@Slf4j(topic = "c.demo") -public class demo { - static final Object room = new Object(); - static boolean hasCigarette = false; //有没有烟 - static boolean hasTakeout = false; +老年代收集器:Serial old、Parallel old、CMS; - public static void main(String[] args) throws InterruptedException { - new Thread(() -> { - synchronized (room) { - log.debug("有烟没?[{}]", hasCigarette); - while (!hasCigarette) {//while防止虚假唤醒 - log.debug("没烟,先歇会!"); - try { - room.wait(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - log.debug("有烟没?[{}]", hasCigarette); - if (hasCigarette) { - log.debug("可以开始干活了"); - } else { - log.debug("没干成活..."); - } - } - }, "小南").start(); - - new Thread(() -> { - synchronized (room) { - Thread thread = Thread.currentThread(); - log.debug("外卖送到没?[{}]", hasTakeout); - if (!hasTakeout) { - log.debug("没外卖,先歇会!"); - try { - room.wait(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - log.debug("外卖送到没?[{}]", hasTakeout); - if (hasTakeout) { - log.debug("可以开始干活了"); - } else { - log.debug("没干成活..."); - } - } - }, "小女").start(); +整堆收集器:G1 +* 红色虚线在JDK9移除、绿色虚线在JDK14弃用该组合、青色虚线在JDK14删除CMS垃圾回收器 - Thread.sleep(1000); - new Thread(() -> { - // 这里能不能加 synchronized (room)? - synchronized (room) { - hasTakeout = true; - //log.debug("烟到了噢!"); - log.debug("外卖到了噢!"); - room.notifyAll(); - } - }, "送外卖的").start(); - } -} -``` +查看默认的垃圾收回收器: +* `-XX:+PrintcommandLineFlags`:查看命令行相关参数(包含使用的垃圾收集器) +* 使用命令行指令:jinfo -flag 相关垃圾回收器参数 进程ID -**** +*** -### park-un +#### Serial -LockSupport 类方法: +**Serial**:串行垃圾收集器,作用于新生代,是指使用单线程进行垃圾回收,采用**复制算法** -* `LockSupport.park()`:暂停当前线程 -* `LockSupport.unpark(暂停的线程对象)`:恢复某个线程的运行 +**STW(Stop-The-World)**:垃圾回收时,只有一个线程在工作,并且java应用中的所有线程都要暂停,等待垃圾回收的完成 -```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 - LockSupport.park(); - System.out.println("resume...");//4 - },"t1"); - t1.start(); - Thread.sleep(2000); - System.out.println("unpark...");//3 - LockSupport.unpark(t1); -} -``` +**Serial old**:执行老年代垃圾回收的串行收集器,内存回收算法使用的是**标记-整理算法**,同样也采用了串行回收和"Stop the World"机制, -对比wait & notify: +- Serial old是运行在Client模式下默认的老年代的垃圾回收器 +- Serial old在Server模式下主要有两个用途: + - 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用 + - 作为老年代CMS收集器的**后备垃圾回收方案**,在并发收集发生 Concurrent Mode Failure 时使用 -* wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而park、unpark不需要 -* park & unpark以线程为单位来阻塞和唤醒线程,而notify 只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程 -* **park & unpark 可以先 unpark**,而 wait & notify 不能先 notify。类比生产消费,先消费发现有产品就消费,没有就等待;先生产就直接产生商品,然后线程直接消费 -* wait 会释放锁资源进入等待队列,park 不会释放锁资源,只负责阻塞当前线程,会释放CPU +开启参数:`-XX:+UseSerialGC == Serial + SerialOld` 等价于新生代用Serial GC且老年代用Serial old GC -原理: +![](https://gitee.com/seazean/images/raw/master/Java/JVM-Serial收集器.png) -* 先park: - 1. 当前线程调用 Unsafe.park() 方法 - 2. 检查 _counter ,本情况为 0,这时获得 _mutex 互斥锁 - 3. 线程进入 _cond 条件变量阻塞 - 4. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1 - 5. 唤醒 _cond 条件变量中的 Thread_0,Thread_0 恢复运行,设置 _counter 为 0 +优点:简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,可以获得最高的单线程收集效率 -![](https://gitee.com/seazean/images/raw/master/Java/JUC-park原理1.png) +缺点:对于交互性较强的应用而言,这种垃圾收集器是不能够接受的,比如Javaweb应用 -* 先unpark: - 1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1 - 2. 当前线程调用 Unsafe.park() 方法 - 3. 检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行,设置 _counter 为 0 - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-park原理2.png) +**** +#### Parallel +Parallel Scavenge 收集器是应用于新生代的并行垃圾回收器,**采用复制算法**、并行回收和"Stop the World"机制 -*** +Parallel Old收集器:是一个应用于老年代的并行垃圾回收器,**采用标记-整理算法** +对比其他回收器: +* 其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间 +* Parallel目标是达到一个可控制的吞吐量,被称为**吞吐量优先**收集器 +* Parallel Scavenge对比ParNew拥有**自适应调节策略**,可以通过一个开关参数打开GC Ergonomics -### 安全分析 +应用场景: -成员变量和静态变量: +* 停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验 +* 高吞吐量可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互 -* 如果它们没有共享,则线程安全 -* 如果它们被共享了,根据它们的状态是否能够改变,分两种情况: - * 如果只有读操作,则线程安全 - * 如果有读写操作,则这段代码是临界区,需要考虑线程安全问题 +停顿时间和吞吐量的关系:新生代空间变小 -> 缩短停顿时间 -> 垃圾回收变得频繁 -> 导致吞吐量下降 -局部变量: +在注重吞吐量及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge+Parallel Old收集器,在server模式下的内存回收性能很好,**Java8默认是此垃圾收集器组合** -* 局部变量是线程安全的 -* 局部变量引用的对象不一定线程安全(逃逸分析): - * 如果该对象没有逃离方法的作用访问,它是线程安全的(每一个方法有一个栈帧) - * 如果该对象逃离方法的作用范围,需要考虑线程安全问题(暴露引用) +![](https://gitee.com/seazean/images/raw/master/Java/JVM-ParallelScavenge收集器.png) -常见线程安全类:String、Integer、StringBuffer、Random、Vector、Hashtable、java.util.concurrent包 +参数配置: -* 线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的 +* `-XX:+UseAdaptivesizepplicy`:设置Parallel scavenge收集器具有**自适应调节策略**,在这种模式下,年轻代的大小、Eden和Survivor的比例、晋升老年代的对象年龄等参数会被自动调整,**虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量** +* `-XX:+UseParallelGC`:手动指定年轻代使用Paralle并行收集器执行内存回收任务 -* 每个方法是原子的,但多个方法的组合不是原子的,只能保证调用的方法内部安全: +* `-XX:+UseParalleloldcc`:手动指定老年代使用并行回收收集器执行内存回收任务 + * 上面两个参数,默认开启一个,另一个也会被开启(互相激活),默认jdk8是开启的 +* `-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参数就容易超过设定的比例 - ```java - Hashtable table = new Hashtable(); - // 线程1,线程2 - if( table.get("key") == null) { - table.put("key", value); - } - ``` -不可变类线程安全:String、Integer 等都是不可变类,因为内部的状态不可以改变,因此方法是线程安全 -* replace等方法底层是新建一个对象,复制过去 +*** - ```java - 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);` -无状态类线程安全 +#### 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并行需要切换线程,串行可以省去切换线程的资源) -Guarded Suspension,用在一个线程等待另一个线程的执行结果 -* 有一个结果需要从一个线程传递到另一个线程,让它们关联同一个 GuardedObject -* 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者) -* JDK 中,join 的实现、Future 的实现,采用的就是此模式 -![](https://gitee.com/seazean/images/raw/master/Java/JUC-保护性暂停.png) +**** -```java -public static void main(String[] args) { - GuardedObject object = new GuardedObjectV2(); - new Thread(() -> { - sleep(1); - object.complete(Arrays.asList("a", "b", "c")); - }).start(); - - Object response = object.get(2500); - if (response != null) { - log.debug("get response: [{}] lines", ((List) response).size()); - } else { - log.debug("can't get response"); - } -} -class GuardedObject { - private Object response; - private final Object lock = new Object(); - - //获取结果 - //timeout :最大等待时间 - public Object get(long millis) { - synchronized (lock) { - // 1) 记录最初时间 - long begin = System.currentTimeMillis(); - // 2) 已经经历的时间 - long timePassed = 0; - while (response == null) { - // 4) 假设 millis 是 1000,结果在 400 时唤醒了,那么还有 600 要等 - long waitTime = millis - timePassed; - log.debug("waitTime: {}", waitTime); - //经历时间超过最大等待时间退出循环 - if (waitTime <= 0) { - log.debug("break..."); - break; - } - try { - lock.wait(waitTime); - } catch (InterruptedException e) { - e.printStackTrace(); - } - // 3) 如果提前被唤醒,这时已经经历的时间假设为 400 - timePassed = System.currentTimeMillis() - begin; - log.debug("timePassed: {}, object is null {}", - timePassed, response == null); - } - return response; - } - } - //产生结果 - public void complete(Object response) { - synchronized (lock) { - // 条件满足,通知等待线程 - this.response = response; - log.debug("notify..."); - lock.notifyAll(); - } - } -} -``` +#### CMS +CMS全称 Concurrent Mark Sweep,是一款**并发的、使用标记-清除**算法、针对老年代的垃圾回收器,其最大特点是**让垃圾收集线程与用户线程同时工作** +CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,停顿时间越短(**低延迟**)越适合与用户交互的程序,良好的响应速度能提升用户体验 -##### 多任务版 +分为以下四个流程: -多任务版保护性暂停: +- 初始标记:使用STW出现短暂停顿,仅标记一下 GC Roots 能直接关联到的对象,速度很快 +- 并发标记:进行 GC Roots的直接关联对象开始遍历整个对象图,在整个回收过程中耗时最长,不需要STW,可以与用户线程一起并发运行 +- 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,比初始标记时间长但远比并发标记时间短,需要 STW +- 并发清除:清除标记为可以回收对象,不需要移动存活对象,所以这个阶段可以与用户线程同时并发的 -![](https://gitee.com/seazean/images/raw/master/Java/JUC-保护性暂停多任务版.png) +Mark Sweep 会造成内存碎片,还不把算法换成 Mark Compact 的原因: -```java -public static void main(String[] args) throws InterruptedException { - for (int i = 0; i < 3; i++) { - new People().start(); - } - Thread.sleep(1000); - for (Integer id : Mailboxes.getIds()) { - new Postman(id, id + "号快递到了").start(); - } -} +* Mark Compact 算法会整理内存,导致用户线程使用的对象的地址改变,影响用户线程继续执行 -@Slf4j(topic = "c.People") -class People extends Thread{ - @Override - public void run() { - // 收信 - GuardedObject guardedObject = Mailboxes.createGuardedObject(); - log.debug("开始收信i d:{}", guardedObject.getId()); - Object mail = guardedObject.get(5000); - log.debug("收到信id:{},内容:{}", guardedObject.getId(),mail); - } -} +* Mark Compact 更适合 Stop The World 场景 -class Postman extends Thread{ - private int id; - private String mail; - //构造方法 - @Override - public void run() { - GuardedObject guardedObject = Mailboxes.getGuardedObject(id); - log.debug("开始送信i d:{},内容:{}", guardedObject.getId(),mail); - guardedObject.complete(mail); - } -} +在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿 -class Mailboxes { - private static Map boxes = new Hashtable<>(); - private static int id = 1; +![](https://gitee.com/seazean/images/raw/master/Java/JVM-CMS收集器.png) - //产生唯一的id - private static synchronized int generateId() { - return id++; - } +优点:并发收集、低延迟 - public static GuardedObject getGuardedObject(int id) { - return boxes.remove(id); - } +缺点: - public static GuardedObject createGuardedObject() { - GuardedObject go = new GuardedObject(generateId()); - boxes.put(go.getId(), go); - return go; - } +- 吞吐量降低:在并发阶段虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,CPU 利用率不够高 +- CMS收集器无法处理浮动垃圾,可能出现 Concurrent Mode Failure导致另一次Full GC的产生 + 浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,CMS 收集需要预留出一部分内存,不能等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS,导致很长的停顿时间 +- 标记 - 清除算法导致的空间碎片,往往出现老年代空间无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC;为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer)技术,而只能够选择空闲列表(Free List)执行内存分配 - public static Set getIds() { - return boxes.keySet(); - } -} -class GuardedObject { - //标识,Guarded Object - private int id;//添加get set方法 -} -``` +参数设置: +* `-XX:+UseConcMarkSweepGC`:手动指定使用CMS收集器执行内存回收任务 + 开启该参数后会自动将`-XX:+UseParNewGC`打开,即:ParNew+CMS+Serial old的组合 -**** +* `-XX:CMSInitiatingoccupanyFraction`:设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收 + * JDK5及以前版本的默认值为68,即当老年代的空间使用率达到68%时,会执行一次CMS回收 + * JDK6及以上版本默认值为92% +* `-XX:+UseCMSCompactAtFullCollection`:用于指定在执行完Full GC后对内存空间进行压缩整理,以此避免内存碎片的产生,由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长 -#### 顺序输出 +* `-XX:CMSFullGCsBeforecompaction`:设置在执行多少次Full GC后对内存空间进行压缩整理 -顺序输出 2 1 +* `-XX:ParallelCMSThreads`:**设置CMS的线程数量** -```java -public static void main(String[] args) throws InterruptedException { - Thread t1 = new Thread(() -> { - while (true) { - //try { Thread.sleep(1000); } catch (InterruptedException e) { } - // 当没有许可时,当前线程暂停运行;有许可时,用掉这个许可,当前线程恢复运行 - LockSupport.park(); - System.out.println("1"); - } - }); - Thread t2 = new Thread(() -> { - while (true) { - System.out.println("2"); - // 给线程 t1 发放『许可』(多次连续调用 unpark 只会发放一个『许可』) - LockSupport.unpark(t1); - try { Thread.sleep(500); } catch (InterruptedException e) { } - } - }); - t1.start(); - t2.start(); -} -``` + * CMS默认启动的线程数是(ParallelGCThreads+3)/4,ParallelGCThreads是年轻代并行收集器的线程数 + * 收集线程占用的CPU资源多于25%,对用户程序影响可能较大;当CPU资源比较紧张时,受到CMS收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕 @@ -19484,233 +10806,76 @@ public static void main(String[] args) throws InterruptedException { -#### 交替输出 - -连续输出5次abc +#### G1 -```java -public class day2_14 { - public static void main(String[] args) throws InterruptedException { - AwaitSignal awaitSignal = new AwaitSignal(5); - Condition a = awaitSignal.newCondition(); - Condition b = awaitSignal.newCondition(); - Condition c = awaitSignal.newCondition(); - new Thread(() -> { - awaitSignal.print("a", a, b); - }).start(); - new Thread(() -> { - awaitSignal.print("b", b, c); - }).start(); - new Thread(() -> { - awaitSignal.print("c", c, a); - }).start(); +##### G1特点 - Thread.sleep(1000); - awaitSignal.lock(); - try { - a.signal(); - } finally { - awaitSignal.unlock(); - } - } -} +G1(Garbage-First)是一款面向服务端应用的垃圾收集器,**应用于新生代和老年代**、采用标记-整理算法、软实时、低延迟、可设定目标(最大STW停顿时间)的垃圾回收器,用于代替CMS,适用于较大的堆(>4~6G),在JDK9之后默认使用G1 -class AwaitSignal extends ReentrantLock { - private int loopNumber; +G1 对比其他处理器的优点: - public AwaitSignal(int loopNumber) { - this.loopNumber = loopNumber; - } - //参数1:打印内容 参数二:条件变量 参数二:唤醒下一个 - public void print(String str, Condition condition, Condition next) { - for (int i = 0; i < loopNumber; i++) { - lock(); - try { - condition.await(); - System.out.print(str); - next.signal(); - } catch (InterruptedException e) { - e.printStackTrace(); - } finally { - unlock(); - } - } - } -} -``` +* **并发与并行:** + * 并行性: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 的巨型对象就可以在新生代垃圾回收时处理掉 + + * Region结构图: -*** + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-G1-Region区域.png) +- **空间整合:** + - CMS:“标记-清除”算法、内存碎片、若干次 GC 后进行一次碎片整理 + - G1:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(Region 之间)上来看是基于“复制”算法实现的,两种算法都可以避免内存碎片 -### 异步模式 +- **可预测的停顿时间模型(软实时soft real-time):** -#### 传统版 + - 可以指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒 + - 由于分块的原因,G1 可以只选取部分区域进行内存回收,这样缩小了回收的范围,对于全局停顿情况也能得到较好的控制 + - G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间,通过过去回收的经验获得),在后台维护一个**优先列表**,每次根据允许的收集时间优先回收价值最大的 Region,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率 -异步模式之生产者/消费者: + * 相比于 CMS GC,G1 未必能做到 CMS 在最好情况下的延时停顿,但是最差情况要好很多 -```java -class ShareData { - private int number = 0; - private Lock lock = new ReentrantLock(); - private Condition condition = lock.newCondition(); +G1垃圾收集器的缺点: - public void increment() throws Exception{ - // 同步代码块,加锁 - lock.lock(); - try { - // 判断 防止虚假唤醒 - while(number != 0) { - // 等待不能生产 - condition.await(); - } - // 干活 - number++; - System.out.println(Thread.currentThread().getName() + "\t " + number); - // 通知 唤醒 - condition.signalAll(); - } catch (Exception e) { - e.printStackTrace(); - } finally { - lock.unlock(); - } - } +* 相较于 CMS,G1 还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1 无论是为了垃圾收集产生的内存占用还是程序运行时的额外执行负载都要比 CMS 要高 +* 从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则发挥其优势。平衡点在6-8GB之间 - public void decrement() throws Exception{ - // 同步代码块,加锁 - lock.lock(); - try { - // 判断 防止虚假唤醒 - while(number == 0) { - // 等待不能消费 - condition.await(); - } - // 干活 - number--; - System.out.println(Thread.currentThread().getName() + "\t " + number); - // 通知 唤醒 - condition.signalAll(); - } catch (Exception e) { - e.printStackTrace(); - } finally { - lock.unlock(); - } - } -} +应用场景: -public class TraditionalProducerConsumer { - public static void main(String[] args) { - ShareData shareData = new ShareData(); - // t1线程,生产 - new Thread(() -> { - for (int i = 0; i < 5; i++) { - shareData.increment(); - } - }, "t1").start(); +* 面向服务端应用,针对具有大内存、多处理器的机器 +* 需要低 GC 延迟,并具有大堆的应用程序提供解决方案 - // t2线程,消费 - new Thread(() -> { - for (int i = 0; i < 5; i++) { - shareData.decrement(); - } - }, "t2").start(); - } -} -``` +*** -#### 改进版 -异步模式之生产者/消费者: -* 消费队列可以用来平衡生产和消费的线程资源,不需要产生结果和消费结果的线程一一对应 -* 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据 -* 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据 -* JDK 中各种阻塞队列,采用的就是这种模式 +##### 记忆集 -![](https://gitee.com/seazean/images/raw/master/Java/JUC-生产者消费者模式.png) +记忆集 Remembered Set 在新生代中,每个 Region 都有一个 Remembered Set,用来被哪些其他 Region 里的对象引用(谁引用了我就记录谁) -```java -public class demo { - public static void main(String[] args) { - MessageQueue queue = new MessageQueue(2); - for (int i = 0; i < 3; i++) { - int id = i; - new Thread(() -> { - queue.put(new Message(id,"值"+id)); - }, "生产者" + i).start(); - } - - new Thread(() -> { - while (true) { - try { - Thread.sleep(1000); - Message message = queue.take(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - },"消费者").start(); - } -} + -//消息队列类,Java间线程之间通信 -class MessageQueue { - private LinkedList list = new LinkedList<>();//消息的队列集合 - private int capacity;//队列容量 - public MessageQueue(int capacity) { - this.capacity = capacity; - } +* 程序对 Reference 类型数据写操作时,产生一个 Write Barrier 暂时中断操作,检查该对象和 Reference 类型数据是否在不同的 Region(其他收集器:检查老年代对象是否引用了新生代对象),不同便通过 CardTable 把相关引用信息记录到 Reference 类型所属的 Region 的 Remembered Set 之中 +* 进行内存回收时,在 GC 根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏 - //获取消息 - public Message take() { - //检查队列是否为空 - synchronized (list) { - while (list.isEmpty()) { - try { - sout(Thread.currentThread().getName() + ":队列为空,消费者线程等待"); - list.wait(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - //从队列的头部获取消息返回 - Message message = list.removeFirst(); - sout(Thread.currentThread().getName() + ":已消费消息--" + message); - list.notifyAll(); - return message; - } - } +垃圾收集器在新生代中建立了记忆集这样的数据结构,可以理解为它是一个抽象类,具体实现记忆集的三种方式: - //存入消息 - public void put(Message message) { - synchronized (list) { - //检查队列是否满 - while (list.size() == capacity) { - try { - sout(Thread.currentThread().getName()+":队列为已满,生产者线程等待"); - list.wait(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - //将消息加入队列尾部 - list.addLast(message); - sout(Thread.currentThread().getName() + ":已生产消息--" + message); - list.notifyAll(); - } - } -} +* 字长精度 +* 对象精度 +* 卡精度(卡表) -final class Message { - private int id; - private Object value; - //get set -} -``` +卡表(Card Table)在老年代中,是一种对记忆集的具体实现,主要定义了记忆集的记录精度、与堆内存的映射关系等,卡表中的每一个元素都对应着一块特定大小的内存块。这个内存块称之为卡页(card page),当存在跨代引用时,会将卡页标记为 dirty,JVM 对于卡页的维护也是通过写屏障的方式 @@ -19718,71 +10883,67 @@ final class Message { -#### 阻塞队列 +##### 工作原理 -```java -public static void main(String[] args) { - ExecutorService consumer = Executors.newFixedThreadPool(1); - ExecutorService producer = Executors.newFixedThreadPool(1); - BlockingQueue queue = new SynchronousQueue<>(); - producer.submit(() -> { - try { - System.out.println("生产..."); - Thread.sleep(1000); - queue.put(10); - } catch (InterruptedException e) { - e.printStackTrace(); - } - }); - consumer.submit(() -> { - try { - System.out.println("等待消费..."); - Integer result = queue.take(); - System.out.println("结果为:" + result); - } catch (InterruptedException e) { - e.printStackTrace(); - } - }); -} -``` +G1中提供了三种垃圾回收模式:YoungGC、Mixed GC 和 FullGC,在不同的条件下被触发 +* 当堆内存使用达到一定值(默认45%)时,开始老年代并发标记过程 +* 标记完成马上开始混合回收过程 + + +顺时针:Young GC → Young GC + Concurrent Mark → Mixed GC 顺序,进行垃圾回收 +* **Young GC**:发生在年轻代的 GC 算法,一般对象(除了巨型对象)都是在 eden region 中分配内存,当所有 eden region 被耗尽无法申请内存时,就会触发一次 young gc,G1停止应用程序的执行 (Stop-The-World),把活跃对象放入老年代,垃圾对象回收 + **回收过程**: -*** + 1. 扫描根:根引用连同 RSet 记录的外部引用作为扫描存活对象的入口 + 2. 更新 RSet:处理 dirty card queue 更新 RS,此后 RSet 准确的反映对象的引用关系 + * dirty card queue:类似缓存,产生了引用先记录在这里,然后更新到 RSet + * 作用:产生引用直接更新 RSet 需要线程同步开销很大,使用队列性能好 + 3. 处理 RSet:识别被老年代对象指向的 Eden 中的对象,这些被指向的对象被认为是存活的对象 + 4. 复制对象:Eden 区内存段中存活的对象会被复制到 survivor 区,survivor 区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到 old 区中空的内存分段,如果 survivor 空间不够,Eden 空间的部分数据会直接晋升到老年代空间 + 5. 处理引用:处理 Soft,Weak,Phantom,JNI Weak 等引用,最终 Eden 空间的数据为空,GC 停止工作 +* **并发标记过程**: + * 初始标记:标记从根节点直接可达的对象,这个阶段是 STW 的,并且会触发一次年轻代 GC + * 根区域扫描 (Root Region Scanning):G1 扫描 survivor 区直接可达的老年代区域对象,并标记被引用的对象,这一过程必须在 Young GC 之前完成 + * 并发标记 (Concurrent Marking):在整个堆中进行并发标记(应用程序并发执行),可能被 YoungGC 中断。在并发标记阶段,**若发现区域对象中的所有对象都是垃圾,则这个区域会被立即回收(实时回收)**,同时并发标记过程中,会计算每个区域的对象活性,即区域中存活对象的比例 + * 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这阶段需要停顿线程,但是可并行执行 + * 筛选回收:并发清理阶段,首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率 -## 内存 + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-G1收集器.jpg) -### JMM +* **Mixed GC**:当很多对象晋升到老年代 old region 时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即 Mixed GC,除了回收整个 young region,还会**回收一部分**的 old region,过程同 YGC -#### 内存模型 + 注意:**是一部分老年代,而不是全部老年代**,可以选择哪些 old region 收集,对垃圾回收的时间进行控制 -Java 内存模型是 Java MemoryModel(JMM),本身是一种**抽象的概念**,实际上并不存在,描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式 + 在G1中,Mixed GC可以通过 `-XX:InitiatingHeapOccupancyPercent` 设置阈值 -作用: +* **Full GC**:对象内存分配速度过快,Mixed GC 来不及回收,导致老年代被填满,就会触发一次 Full GC,G1 的 Full GC 算法就是单线程执行的垃圾回收,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免 Full GC -* 屏蔽各种硬件和操作系统的内存访问差异,实现让Java程序在各种平台下都能达到一致的内存访问效果 -* 规定了线程和内存之间的一些关系 + 产生 Full GC 的原因: -根据JMM的设计,系统存在一个主内存(Main Memory),Java中所有变量都存储在主存中,对于所有线程都是共享的;每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些**变量的拷贝**,线程对所有变量的操作都是先对变量进行拷贝,然后在工作内存中进行,不能直接操作主内存中的变量;线程之间无法相互直接访问,线程间的通信(传递)必须通过主内存来完成 + * 晋升时没有足够的空间存放晋升的对象 + * 并发处理过程完成之前空间耗尽,浮动垃圾 -![](https://gitee.com/seazean/images/raw/master/Java/JMM内存模型.png) -主内存和工作内存: -* 主内存:计算机的内存,也就是经常提到的8G内存,16G内存,存储所有共享变量的值 -* 工作内存:存储该线程使用到的共享变量在主内存的的值的副本拷贝 +*** -**jvm和jmm之间的关系**: +##### 相关参数 -* jmm中的主内存、工作内存与jvm中的Java堆、栈、方法区等并不是同一个层次的内存划分,这两者基本上是没有关系的,如果两者一定要勉强对应起来: - * 主内存主要对应于Java堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域 - * 从更低层次上说,主内存直接对应于物理硬件的内存,工作内存对应寄存器和高速缓存 +- `-XX:+UseG1GC`:手动指定使用G1垃圾收集器执行内存回收任务 +- `-XX:G1HeapRegionSize`:设置每个Region的大小。值是2的幂,范围是1MB到32MB之间,目标是根据最小的Java堆大小划分出约2048个区域,默认是堆内存的1/2000 +- `-XX:MaxGCPauseMillis`:设置期望达到的最大GC停顿时间指标 (JVM会尽力实现,但不保证达到),默认值是200ms +- `-XX:+ParallelGcThread`:设置STW工作线程数的值,最多设置为8 +- `-XX:ConcGCThreads`:设置并发标记线程数,设置为并行垃圾回收线程数(ParallelGcThreads) 的1/4左右 +- `-XX:InitiatingHeapoccupancyPercent`:设置触发并发Mixed GC周期的Java堆占用率阈值,超过此值,就触发GC,默认值是45 +- `-XX:+ClassUnloadingWithConcurrentMark`:并发标记类卸载,默认启用,所有对象都经过并发标记后,就可以知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类 @@ -19790,79 +10951,68 @@ Java 内存模型是 Java MemoryModel(JMM),本身是一种**抽象的概 -#### 内存交互 - -Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作: +##### 调优 - +G1 的设计原则就是简化 JVM 性能调优,只需要简单的三步即可完成调优: -* lock:将一个变量标识为被一个线程独占状态 -* unclock:将一个变量从独占状态释放出来,释放后的变量才可以被其他线程锁定 -* read:把一个变量的值从主内存传输到工作内存中 -* load:在 read 之后执行,把 read 得到的值放入工作内存的变量副本中 -* use:把工作内存中一个变量的值传递给执行引擎,每当遇到一个使用到变量的操作时都要使用该指令 -* assign:把从执行引擎接收到的一个值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的操作时,都要使用该指令 -* store:把工作内存的一个变量的值传送到主内存中 -* write:在 store 之后执行,把 store 得到的值放入主内存的变量中 +1. 开启 G1 垃圾收集器 +2. 设置堆的最大内存 +3. 设置最大的停顿时间(stw) +**不断调优暂停时间指标**: +* `XX:MaxGCPauseMillis=x` 可以设置启动应用程序暂停的时间,G1在运行的时候会根据这个参数选择CSet来满足响应时间的设置 +* 设置到100ms或者200ms都可以(不同情况下会不一样),但设置成50ms就不太合理 +* 暂停时间设置的太短,就会导致出现G1跟不上垃圾产生的速度,最终退化成 Full GC +* 对这个参数的调优是一个持续的过程,逐步调整到最佳状态 -*** +**不要设置新生代和老年代的大小**: +- 避免使用 -Xmn 或 -XX:NewRatio 等相关选项显式设置年轻代大小,G1 收集器在运行的时候会调整新生代和老年代的大小,从而达到我们为收集器设置的暂停时间目标 +- 设置了新生代大小相当于放弃了 G1 为我们做的自动调优,我们需要做的只是设置整个堆内存的大小,剩下的交给 G1 自己去分配各个代的大小 -#### 三大特性 -##### 可见性 +*** -可见性:是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值 -存在可见性问题的根本原因是由于缓存的存在,线程持有的是共享变量的副本,无法感知其他线程对于共享变量的更改,导致读取的值不是最新的 -main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止: +#### ZGC -```java -static boolean run = true; //添加volatile -public static void main(String[] args) throws InterruptedException { - Thread t = new Thread(()->{ - while(run){ - // .... - } - }); - t.start(); - sleep(1); - run = false; // 线程t不会如预想的停下来 -} -``` +ZGC 收集器是一个可伸缩的、低延迟的垃圾收集器,基于 Region 内存布局的,不设分代,使用了读屏障、染色指针和内存多重映射等技术来实现**可并发的标记压缩算法** -原因: +* 在 CMS 和 G1 中都用到了写屏障,而 ZGC 用到了读屏障 +* 染色指针:直接将少量额外的信息存储在指针上的技术,从 64 位的指针中拿高 4 位来标识对象此时的状态 + * 染色指针可以使得一旦某个 Region 的存活对象被移走之后,这个 Region 立即就能够被释放和重用掉 + * 可以直接从指针中看到引用对象的三色标记状态(Marked0、Marked1)、是否进入了重分配集(是否被移动过 Remapped)、是否只能通过 finalize() 方法才能被访问到(Finalizable) + * 可以大幅减少在垃圾收集过程中内存屏障的使用数量,写屏障的目的通常是为了记录对象引用的变动情况,如果将这些信息直接维护在指针中,显然就可以省去一些专门的记录操作 + * 可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据 +* 内存多重映射:多个虚拟地址指向同一个物理地址 -* 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存 -* 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中, - 减少对主存中 run 的访问,提高效率 -* 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量 - 的值,结果永远是旧值 +可并发的标记压缩算法:染色指针标识对象是否被标记或移动,读屏障保证在每次应用程序或 GC 程序访问对象时先根据染色指针的标识判断是否被移动,如果被移动就访问新的复制对象,并更新引用,不会像 G1 一样必须等待垃圾回收完成才能访问 -![](https://gitee.com/seazean/images/raw/master/Java/JMM-可见性例子.png) +ZGC 目标: +- 停顿时间不会超过 10ms +- 停顿时间不会随着堆的增大而增大(不管多大的堆都能保持在 10ms 以下) +- 可支持几百 M,甚至几 T 的堆大小(最大支持4T) +ZGC 的工作过程可以分为 4 个阶段: -*** +* 并发标记(Concurrent Mark): 遍历对象图做可达性分析的阶段,也要经过类似于 G1的初始标记、最终标记的短暂停顿 +* 并发预备重分配( Concurrent Prepare for Relocate): 根据特定的查询条件统计得出本次收集过程要清理哪些 Region,将这些 Region 组成重分配集(Relocation Set) +* 并发重分配(Concurrent Relocate): 重分配是 ZGC 执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的 Region 上,并为重分配集中的每个 Region 维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系 +* 并发重映射(Concurrent Remap): 修正整个堆中指向重分配集中旧对象的所有引用,ZGC 的并发映射并不是一个必须要立即完成的任务,ZGC 很巧妙地把并发重映射阶段要做的工作,合并到下一次垃圾收集循环中的并发标记阶段里去完成,因为都是要遍历所有对象的,这样合并节省了一次遍历的开销 +ZGC 几乎在所有地方并发执行的,除了初始标记的是 STW 的,但这部分的实际时间是非常少的,所以响应速度快,在尽可能对吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟 +优点:高吞吐量、低延迟 -##### 原子性 +缺点:浮动垃圾,当 ZGC 准备要对一个很大的堆做一次完整的并发收集,其全过程要持续十分钟以上,由于应用的对象分配速率很高,将创造大量的新对象产生浮动垃圾 -原子性:不可分割,完整性,也就是说某个线程正在做某个具体业务时,中间不可以被分割,需要具体完成,要么同时成功,要么同时失败,保证指令不会受到线程上下文切换的影响 -定义原子操作的使用规则: -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操作) +参考文章:https://www.cnblogs.com/jimoer/p/13170249.html @@ -19870,83 +11020,103 @@ public static void main(String[] args) throws InterruptedException { -##### 有序性 +#### 总结 -有序性:在本线程内观察,所有操作都是有序的;在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序 +Serial GC、Parallel GC、Concurrent Mark Sweep GC这三个GC不同: -CPU的基本工作是执行存储的指令序列,即程序,程序的执行过程实际上是不断地取出指令、分析指令、执行指令的过程,为了提高性能,编译器和处理器会对指令重排,一般分为以下三种: +- 最小化地使用内存和并行开销,选Serial GC +- 最大化应用程序的吞吐量,选Parallel GC +- 最小化GC的中断或停顿时间,选CMS GC -```java -源代码 -> 编译器优化的重排 -> 指令并行的重排 -> 内存系统的重排 -> 最终执行指令 -``` +![](https://gitee.com/seazean/images/raw/master/Java/JVM-垃圾回收器总结.png) -现代 CPU 支持多级指令流水线,几乎所有的冯•诺伊曼型计算机的CPU,其工作都可以分为5个阶段:取指令、指令译码、执行指令、访存取数和结果写回,可以称之为**五级指令流水线**。这时 CPU 可以在一个时钟周期内,同时运行五条指令的**不同阶段**(每个线程不同的阶段),本质上流水线技术并不能缩短单条指令的执行时间,但变相地提高了指令地吞吐率 -处理器在进行重排序时,必须要考虑**指令之间的数据依赖性** -* 单线程环境也存在指令重排,由于存在依赖,最终执行结果和代码顺序的结果一致 -* 多线程环境中线程交替执行,由于编译器优化重排,两个线程中使用的变量能否保证一致性是无法确定的 +*** -*** +### 日志分析 +内存分配与垃圾回收的参数列表:进入 Run/Debug Configurations ---> VM options 设置参 +- `-XX:+PrintGC`:输出GC日志,类似:-verbose:gc +- `-XX:+PrintGcDetails`:输出GC的详细日志 +- `-XX:+PrintGcTimestamps`:输出GC的时间戳(以基准时间的形式) +- `-XX:+PrintGCDatestamps`:输出GC的时间戳(以日期的形式,如2013-05-04T21:53:59.234+0800) +- `-XX:+PrintHeapAtGC`:在进行GC的前后打印出堆的信息 +- `-Xloggc:../logs/gc.1og`:日志文件的输出路径 -### cache -#### 缓存机制 -##### 缓存结构 -在计算机系统中,CPU高速缓存(CPU Cache,简称缓存)是用于减少处理器访问内存所需平均时间的部件;在存储体系中位于自顶向下的第二层,仅次于CPU寄存器;其容量远小于内存,但速度却可以接近处理器的频率 -CPU 处理器速度远远大于在主内存中的,为了解决速度差异,在它们之间架设了多级缓存,如 L1、L2、L3 级别的缓存,这些缓存离CPU越近就越快,将频繁操作的数据缓存到这里,加快访问速度 +*** - -| 从 cpu 到 | 大约需要的时钟周期 | -| --------- | -------------------------------- | -| 寄存器 | 1 cycle (4GHz 的 CPU 约为0.25ns) | -| L1 | 3~4 cycle | -| L2 | 10~20 cycle | -| L3 | 40~45 cycle | -| 内存 | 120~240 cycle | +## 类加载 +### 对象结构 -##### 缓存使用 +#### 基本构造 -当处理器发出内存访问请求时,会先查看缓存内是否有请求数据,如果存在(命中),则不经访问内存直接返回该数据;如果不存在(失效),则要先把内存中的相应数据载入缓存,再将其返回处理器 +一个Java对象内存中存储为三部分:对象头 (Header)、实例数据 (Instance Data) 和对齐填充 (Padding) -缓存之所以有效,主要因为程序运行时对内存的访问呈现局部性(Locality)特征。既包括空间局部性(Spatial Locality),也包括时间局部性(Temporal Locality),有效利用这种局部性,缓存可以达到极高的命中率。 +对象头: +* 普通对象(32位系统,64位128位):分为两部分 + * Mark Word:用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,就是Mark Word -*** + ```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 + ```ruby + |-----------------------------------------------------| + | Object Header (64 bits) | + |---------------------------|-------------------------| + | Mark Word (32 bits) | Klass Word (32 bits) | + |---------------------------|-------------------------| + ``` -#### 伪共享 +* 数组对象:如果对象是一个数组,那在对象头中还有一块数据用于记录数组长度 -**缓存以缓存行 cache line 为单位,每个缓存行对应着一块内存**,一般是 64 byte(8 个 long),在CPU从主存获取数据时,以 cache line 为单位加载,于是相邻的数据会一并加载到缓存中 + ```ruby + |-------------------------------------------------------------------------------| + | Object Header (96 bits) | + |-----------------------|-----------------------------|-------------------------| + | Mark Word(32bits) | Klass Word(32bits) | array length(32bits) | + |-----------------------|-----------------------------|-------------------------| + ``` + + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-对象头结构.png) -缓存会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中,CPU 要保证数据的一致性,需要做到某个 CPU 核心更改了数据,其它 CPU 核心对应的**整个缓存行必须失效**,这就是伪共享 +实例数据:实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来 - +对齐填充:Padding 起占位符的作用。64位系统,由于 HotSpot VM 的自动内存管理系统要求对象起始地址必须是8字节的整数倍,就是对象的大小必须是8字节的整数倍,而对象头部分正好是8字节的倍数(1倍或者2倍),因此当对象实例数据部分没有对齐时,就需要通过对齐填充来补全 -解决方法: +32位系统 -* padding:通过填充,让数据落在不同的 cache line 中 +* 一个 int 在 java 中占据 4byte,所以 Integer 的大小为: -* @Contended:原理参考 无锁 → Addr → 优化机制 → 伪共享 + ```ruby + # 需要补位4byte + 4(Mark Word) + 4(Klass Word) + 4(data) + 4(Padding) = 16byte + ``` -Linux查看CPU缓存行: +* `int[] arr = new int[10]` -* 命令:`cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size64` -* 内存地址格式:[高位组标记] [低位索引] [偏移量] + ```ruby + # 由于需要8位对齐,所以最终大小为`56byte`。 + 4(Mark Word) + 4(Klass Word) + 4(length) + 4*10(10个int大小) + 4(Padding) = 56sbyte + ``` @@ -19954,60 +11124,72 @@ Linux查看CPU缓存行: -#### 缓存一致 +#### 节约内存 + +* 尽量使用基本类型 + +* 满足容量前提下,尽量用小字段 + +* 尽量用数组,少用集合,数组中是可以使用基本类型的,但是集合中只能放包装类型,如果需要使用集合,推荐比较节约内存的集合工具:fastutil + + 一个ArrayList集合,如果里面放了10个数字,占用多少内存: -缓存一致性:当多个处理器运算任务都涉及到同一块主内存区域的时候,将可能导致各自的缓存数据不一样 + ```java + private transient Object[] elementData; + private int size; + ``` -**MESI**(Modified Exclusive Shared Or Invalid)是一种广泛使用的支持写回策略的缓存一致性协议,CPU中每个缓存行 (caceh line) 使用4种状态进行标记(使用额外的两位 (bit) 表示): + Mark Word 占 4byte,Klass Word 占 4byte,一个int字段(size)占 4byte,elementData 数组本身占 12(4+4+4),数组中 10 个 Integer 对象占 10×16,所以整个集合空间大小为 184byte -* M:被修改(Modified) +* 时间用 long/int 表示,不用 Date 或者 String - 该缓存行只被缓存在该 CPU 的缓存中,并且是被修改过的,与主存中的数据不一致 (dirty),该缓存行中的内存需要在未来的某个时间点(其它 CPU 读取主存中相应数据之前)写回( write back )主存 - 当被写回主存之后,该缓存行的状态会变成独享 (exclusive) 状态。 -* E:独享的(Exclusive) +*** - 该缓存行只被缓存在该 CPU 的缓存中,它是未被修改过的 (clear),与主存中数据一致,该状态可以在任何时刻有其它 CPU 读取该内存时变成共享状态 (shared) - 当 CPU 修改该缓存行中内容时,该状态可以变成 Modified 状态 -* S:共享的(Shared) +#### 对象访问 - 该状态意味着该缓存行可能被多个 CPU 缓存,并且各个缓存中的数据与主存数据一致 (clear),当有一个 CPU 修改该缓存行中,其它 CPU 中该缓存行变成无效状态 (Invalid) +JVM是通过栈帧中的对象引用访问到其内部的对象实例: -* I:无效的(Invalid) +* 句柄访问 + 使用该方式,Java堆中会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息 + 优点:reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。 - 该缓存是无效的,可能有其它 CPU 修改了该缓存行 + -解决方法:各个处理器访问缓存时都遵循一些协议,在读写时要根据协议进行操作,协议主要有MSI、MESI等 +* 直接指针(HotSpot采用) + 使用该方式,Java 堆对象的布局必须考虑如何放置访问类型数据的相关信息,reference中直接存储的就是对象地址 + 优点:速度更快,**节省了一次指针定位的时间开销** + -**** -#### 处理机制 +*** -单核 CPU 处理器会自动保证基本内存操作的原子性 -多核 CPU 处理器,每个 CPU 处理器内维护了一块内存,每个内核内部维护着一块缓存,当多线程并发读写时,就会出现缓存数据不一致的情况。处理器提供: -* 总线锁定:当处理器要操作共享变量时,在 BUS 总线上发出一个 LOCK 信号,其他处理器就无法操作这个共享变量,该操作会导致大量阻塞,从而增加系统的性能开销 -* 缓存锁定:当处理器对缓存中的共享变量进行了操作,其他处理器有嗅探机制,将该共享变量的缓存失效,其他线程读取时会重新从主内存中读取最新的数据,基于 MESI 缓存一致性协议来实现 +### 对象创建 -有如下两种情况处理器不会使用缓存锁定: +#### 生命周期 -* 当操作的数据跨多个缓存行,或没被缓存在处理器内部,则处理器会使用总线锁定 +在 Java 中,对象的生命周期包括以下几个阶段: -* 有些处理器不支持缓存锁定,比如:Intel 486 和 Pentium 处理器也会调用总线锁定 +1. 创建阶段 (Created): +2. 应用阶段 (In Use):对象至少被一个强引用持有着 +3. 不可见阶段 (Invisible):程序的执行已经超出了该对象的作用域,不再持有该对象的任何强引用 +4. 不可达阶段 (Unreachable):该对象不再被任何强引用所持有,包括GC Root的强引用 +5. 收集阶段 (Collected):垃圾回收器已经对该对象的内存空间重新分配做好准备,该对象如果重写了finalize()方法,则会去执行该方法 +6. 终结阶段 (Finalized):等待垃圾回收器对该对象空间进行回收,当对象执行完finalize()方法后仍然处于不可达状态时进入该阶段 +7. 对象空间重分配阶段 (De-allocated):垃圾回收器对该对象的所占用的内存空间进行回收或者再分配 -总线机制: -* 总线嗅探:每个处理器通过嗅探在总线上传播的数据来检查自己缓存值是否过期了,当处理器发现自己的缓存对应的内存地址数据被修改,就将当前处理器的缓存行设置为无效状态,当处理器对这个数据进行操作时,会重新从内存中把数据读取到处理器缓存中 -* 总线风暴:由于 volatile 的 MESI 缓存一致性协议,需要不断的从主内存嗅探和 CAS 循环,无效的交互会导致总线带宽达到峰值;因此不要大量使用 volatile 关键字,使用 volatile、syschonized 都需要根据实际场景 +参考文章:https://blog.csdn.net/sodino/article/details/38387049 @@ -20015,153 +11197,154 @@ Linux查看CPU缓存行: -### volatile - -#### 基本特性 - -Volatile是Java虚拟机提供的**轻量级**的同步机制(三大特性) +#### 创建时机 -- 保证可见性 -- 不保证原子性 -- 保证有序性(禁止指令重排) +类在第一次实例化加载一次,后续实例化不再加载,引用第一次加载的类 -性能:volatile修饰的变量进行读操作与普通变量几乎没什么差别,但是写操作相对慢一些,因为需要在本地代码中插入很多内存屏障来保证指令不会发生乱序执行,但是开销比锁要小 +Java对象创建时机: +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 方法 -#### 解决重排 +5. 使用(反)序列化机制创建对象:当反序列化一个对象时,JVM 会创建一个**单独的对象**,在此过程中,JVM 并不会调用任何构造函数,为了反序列化一个对象,需要让类实现 Serializable 接口 -**volatile 修饰的变量,可以禁用指令重排** +从 Java 虚拟机层面看,除了使用 new 关键字创建对象的方式外,其他方式全部都是通过转变为 invokevirtual 指令直接创建对象的 -**synchronized 无法禁止指令重排和处理器优化**,为什么可以保证有序性? -加了锁之后,只能有一个线程获得到了锁,获得不到锁的线程就要阻塞,所以同一时间只有一个线程执行,相当于单线程,而单线程的指令重排是没有问题的 -指令重排实例: -* example 1: +*** - ```java - public void mySort() { - int x = 11; //语句1 - int y = 12; //语句2 谁先执行效果一样 - x = x + 5; //语句3 - y = x * x; //语句4 - } - ``` - 执行顺序是:1 2 3 4、2 1 3 4、1 3 2 4 - 指令重排也有限制不会出现:4321,语句4需要依赖于 y 以及 x 的申明,因为存在数据依赖,无法首先执行 -* example 2: +#### 创建过程 - ```java - int num = 0; - boolean ready = false; - // 线程1 执行此方法 - public void actor1(I_Result r) { - if(ready) { - r.r1 = num + num; - } else { - r.r1 = 1; - } - } - // 线程2 执行此方法 - public void actor2(I_Result r) { - num = 2; - ready = true; - } - ``` +创建对象的过程: - 情况一:线程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 - 发生指令重排 +1. 判断对象对应的类是否加载、链接、初始化 +2. 为对象分配内存:指针碰撞、空闲链表。当一个对象被创建时,虚拟机就会为其分配内存来存放对象的实例变量及其从父类继承过来的实例变量,即使从**隐藏变量**也会被分配空间(继承部分解释了为什么会隐藏) +3. 处理并发安全问题: -**** + * 采用 CAS 配上自旋保证更新的原子性 + * 每个线程预先分配一块 TLAB +4. 初始化分配的空间:虚拟机将分配到的内存空间都初始化为零值(不包括对象头),保证对象实例字段在不赋值时可以直接使用,程序能访问到这些字段的数据类型所对应的零值 +5. 设置对象的对象头:将对象的所属类(类的元数据信息)、对象的HashCode、对象的GC信息、锁信息等数据存储在对象的对象头中 -#### 底层原理 +6. 执行init方法进行实例化:实例变量初始化、实例代码块初始化 、构造函数初始化 -使用 volatile 修饰的共享变量,总线会开启 CPU 总线嗅探机制来解决 JMM 缓存一致性问题,也就是共享变量在多线程中可见性的问题,实现 MESI 缓存一致性协议 + * 实例变量初始化与实例代码块初始化: -底层是通过汇编 lock 前缀指令,共享变量加了 lock 前缀指令,在线程修改完共享变量后,会马上执行 store 和 write 操作写回主存。在执行 store 操作前,会先执行缓存锁定的操作,其他的 CPU 上运行的线程根据 CPU 总线嗅探机制会修改其共享变量为失效状态,读取时会重新从主内存中读取最新的数据 + 对实例变量直接赋值或者使用实例代码块赋值,编译器会将其中的代码放到类的构造函数中去,并且这些代码会被放在对超类构造函数的调用语句之后 (**Java要求构造函数的第一条语句必须是超类构造函数的调用语句**),构造函数本身的代码之前 -lock 前缀指令就相当于内存屏障,Memory Barrier(Memory Fence) + * 构造函数初始化: -* 对 volatile 变量的写指令后会加入写屏障 -* 对 volatile 变量的读指令前会加入读屏障 + **Java要求在实例化类之前,必须先实例化其超类,以保证所创建实例的完整性**,在准备实例化一个类的对象前,首先准备实例化该类的父类,如果该类的父类还有父类,那么准备实例化该类的父类的父类,依次递归直到递归到Object类。然后从Object类依次对以下各类进行实例化,初始化父类中的变量和执行构造函数 -内存屏障有三个作用: -- 确保对内存的读-改-写操作原子执行 -- 阻止屏障两侧的指令重排序 -- 强制把缓存中的脏数据写回主内存,让缓存行中相应的数据失效 -保证可见性: +*** -* 写屏障(sfence,Store Barrier)保证在该屏障之前的,对共享变量的改动,都同步到主存当中 - ```java - public void actor2(I_Result r) { - num = 2; - ready = true; // ready 是 volatile 赋值带写屏障 - // 写屏障 - } - ``` -* 读屏障(lfence,Load Barrier)保证在该屏障之后的,对共享变量的读取,加载的是主存中最新数据 +#### 承上启下 - ```java - public void actor1(I_Result r) { - // 读屏障 - // ready 是 volatile 读取值带读屏障 - if(ready) { - r.r1 = num + num; - } else { - r.r1 = 1; - } - } - ``` +1. 一个实例变量在对象初始化的过程中会被赋值几次? - + JVM在为一个对象分配完内存之后,会给每一个实例变量赋予默认值,这个实例变量被第一次赋值 + 在声明实例变量的同时对其进行了赋值操作,那么这个实例变量就被第二次赋值 + 在实例代码块中又对变量做了初始化操作,那么这个实例变量就被第三次赋值 + 在构造函数中也对变量做了初始化操作,那么这个实例变量就被第四次赋值 + 在Java的对象初始化过程中,一个实例变量最多可以被初始化4次 -* 全能屏障:mfence(modify/mix Barrier),兼具 sfence 和 lfence 的功能 +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();`: -* 有序性的保证也只是保证了本线程内相关代码不被重排序 + * 实例初始化不一定要在类初始化结束之后才开始 - ```java - volatile i = 0; - new Thread(() -> {i++}); - new Thread(() -> {i--}); - ``` + * 在同一个类加载器下,一个类型只会被初始化一次。所以一旦开始初始化一个类,无论是否完成后续都不会再重新触发该类型的初始化阶段了(只考虑在同一个类加载器下的情形。因此,在实例化上述程序中的st变量时,**实际上是把实例初始化嵌入到了静态初始化流程中,并且在上面的程序中,嵌入到了静态初始化的起始位置**,这就导致了实例初始化完全发生在静态初始化之前,这也是导致a为110 b为0的原因 - i++反编译后的指令: + 代码等价于: - ```java - 0: iconst_1 //当int取值 -1~5 时,JVM采用iconst指令将常量压入栈中 - 1: istore_1 //将操作数栈顶数据弹出,存入局部变量表的 slot 1 - 2: iinc 1, 1 - ``` + ```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 + } + } + ``` - + @@ -20169,43 +11352,30 @@ lock 前缀指令就相当于内存屏障,Memory Barrier(Memory Fence) -#### 双端检锁 - -##### 检锁机制 +### 加载过程 -Double-Checked Locking:双端检锁机制 +#### 生命周期 -DCL(双端检锁)机制不一定是线程安全的,原因是有指令重排的存在,加入volatile可以禁止指令重排 +类是在运行期间**第一次使用时动态加载**的(不使用不加载),而不是一次性加载所有类,因为一次性加载会占用很多的内存,加载的类信息存放于一块成为方法区的内存空间 -```java -public final class Singleton { - private Singleton() { } - private static Singleton INSTANCE = null; - - public static Singleton getInstance() { - if(INSTANCE == null) { // t2 - // 首次访问会同步,而之后的使用没有 synchronized - synchronized(Singleton.class) { - if (INSTANCE == null) { // t1 - INSTANCE = new Singleton(); - } - } - } - return INSTANCE; - } -} -``` +![](https://gitee.com/seazean/images/raw/master/Java/JVM-类的生命周期.png) -不锁INSTANCE的原因: +包括 7 个阶段: -* INSTANCE 要重新赋值 -* INSTANCE 是null,线程加锁之前需要获取对象的引用,null没有引用 +* 加载(Loading) +* 链接:验证(Verification)、准备(Preparation)、解析(Resolution) +* 初始化(Initialization) +* 使用(Using) +* 卸载(Unloading) -实现特点: +类加载方式: -* 懒惰初始化 -* 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁 -* 第一个 if 使用了 INSTANCE 变量,是在同步块之外,但在多线程环境下会产生问题 +* 隐式加载: + * 创建类对象、使用类的静态域、创建子类对象、使用子类的静态域 + * 在 JVM 启动时,通过三大类加载器加载 class +* 显式加载: + * ClassLoader.loadClass(className),只加载和连接,**不会进行初始化** + * Class.forName(String name, boolean initialize,ClassLoader loader),使用 loader 进行加载和连接,根据参数 initialize 决定是否初始化 @@ -20213,63 +11383,35 @@ public final class Singleton { -##### DCL问题 - -getInstance 方法对应的字节码为: - -```java -0: getstatic #2 // Field INSTANCE:Ltest/Singleton; -3: ifnonnull 37 -6: ldc #3 // class test/Singleton -8: dup -9: astore_0 -10: monitorenter -11: getstatic #2 // Field INSTANCE:Ltest/Singleton; -14: ifnonnull 27 -17: new #3 // class test/Singleton -20: dup -21: invokespecial #4 // Method "":()V -24: putstatic #2 // Field INSTANCE:Ltest/Singleton; -27: aload_0 -28: monitorexit -29: goto 37 -32: astore_1 -33: aload_0 -34: monitorexit -35: aload_1 -36: athrow -37: getstatic #2 // Field INSTANCE:Ltest/Singleton; -40: areturn -``` - -* 17 表示创建对象,将对象引用入栈 -* 20 表示复制一份对象引用,引用地址 -* 21 表示利用一个对象引用,调用构造方法初始化对象 -* 24 表示利用一个对象引用,赋值给 static INSTANCE - -步骤21和步骤24之间不存在数据依赖关系,而且无论重排前后,程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的 - -* 关键在于 0: getstatic 这行代码在 monitor 控制之外,可以越过 monitor 读取INSTANCE 变量的值 -* 当其他线程访问 instance 不为null时,由于 instance 实例未必已初始化,那么 t2 拿到的是将是一个未初 - 始化完毕的单例返回,这就造成了线程安全的问题 +#### 加载阶段 -![](https://gitee.com/seazean/images/raw/master/Java/JMM-DCL出现的问题.png) +加载是类加载的一个阶段,注意不要混淆 +加载过程完成以下三件事: +- 通过类的完全限定名称获取定义该类的二进制字节流(二进制字节码) +- 将该字节流表示的**静态存储结构转换为方法区的运行时存储结构**(运行时常量池) +- 在内存中生成一个代表该类的 Class 对象,作为方法区中该类各种数据的访问入口 -*** +其中二进制字节流可以从以下方式中获取: +- 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础 +- 从网络中获取,最典型的应用是 Applet +- 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass +- 由其他文件生成,例如由 JSP 文件生成对应的 Class 类 +将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,重要 field: -##### 解决方法 +* `_java_mirror` 即 java 的类镜像,例如对 String 来说就是 String.class,作用是把 class 暴露给 java 使用 +* `_super` 即父类、`_fields` 即成员变量、`_methods` 即方法、`_constants` 即常量池、`_class_loader` 即类加载器、`_vtable` **虚方法表**、`_itable` 接口方法表 -指令重排只会保证串行语义的执行一致性(单线程),但并不会关系多线程间的语义一致性 +加载过程: -引入volatile,来保证出现指令重排的问题,从而保证单例模式的线程安全性: +* 如果这个类还有父类没有加载,先加载父类 +* 加载和链接可能是交替运行的 +* instanceKlass 和 _java_mirror 相互持有对方的地址,堆中对象通过 instanceKlass 和元空间进行交互 -```java -private static volatile SingletonDemo INSTANCE = null; -``` + @@ -20277,201 +11419,171 @@ private static volatile SingletonDemo INSTANCE = null; -### ha-be +#### 链接阶段 -happens-before 先行发生 +##### 验证 -Java 内存模型具备一些先天的“有序性”,即不需要通过任何同步手段(volatile、synchronized等)就能够得到保证的安全,这个通常也称为 happens-before 原则,它是可见性与有序性的一套规则总结 +确保 Class 文件的字节流中包含的信息是否符合 JVM 规范,保证被加载类的正确性,不会危害虚拟机自身的安全 -不符合 happens-before 规则,JMM 并不能保证一个线程的可见性和有序性 +主要包括四种验证:文件格式验证,源数据验证,字节码验证,符号引用验证 -1. 程序次序规则 (Program Order Rule):一个线程内,逻辑上书写在前面的操作先行发生于书写在后面的操作 ,因为多个操作之间有先后依赖关系,则不允许对这些操作进行重排序 -2. 锁定规则 (Monitor Lock Rule):一个 unLock 操作先行发生于后面(时间的先后)对同一个锁的 lock 操作,所以线程解锁 m 之前对变量的写(锁内的写),对于接下来对 m 加锁的其它线程对该变量的读可见 -3. **volatile变量规则** (Volatile Variable Rule):对 volatile 变量的写操作先行发生于后面对这个变量的读 +*** -4. 传递规则 (Transitivity):具有传递性,如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C -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()方法的调用先行发生于被中断线程的代码检测到中断事件的发生 +准备阶段为**静态变量分配内存并设置初始值**,使用的是方法区的内存: -7. 线程终止规则 (Thread Termination Rule):线程中所有的操作都先行发生于线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行 +* 类变量也叫静态变量,就是是被 static 修饰的变量 +* 实例变量也叫对象变量,即没加static 的变量 -8. 对象终结规则(Finaizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始 +实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中。实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次 +类变量初始化: +* static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾 +* 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; + ``` -终止模式之两阶段终止模式:停止标记用 volatile 是为了保证该变量在多个线程之间的可见性 -```java -class TwoPhaseTermination { - //监控线程 - private Thread monitor; - //停止标记 - private volatile boolean stop = false;; - //启动监控线程 - public void start() { - monitor = new Thread(() -> { - while (true) { - Thread thread = Thread.currentThread(); - if (stop) { - System.out.println("后置处理"); - break; - } - try { - Thread.sleep(1000);//睡眠 - System.out.println(thread.getName() + "执行监控记录"); - } catch (InterruptedException e) { - System.out.println("被打断,退出睡眠"); - } - } - }); - monitor.start(); - } +*** - //停止监控线程 - public void stop() { - stop = true; - monitor.interrupt();//让线程尽快退出Timed Waiting - } -} -//测试 -public static void main(String[] args) throws InterruptedException { - TwoPhaseTermination tpt = new TwoPhaseTermination(); - tpt.start(); - Thread.sleep(3500); - System.out.println("停止监控"); - tpt.stop(); -} -``` +##### 解析 -**** +将常量池中类、接口、字段、方法的**符号引用替换为直接引用**(内存地址)的过程: +* 符号引用:一组符号来描述目标,可以是任何字面量,属于编译原理方面的概念如:包括类和接口的全限名、字段的名称和描述符、方法的名称和**方法描述符** +* 直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄,如果有了直接引用,那说明引用的目标必定已经存在于内存之中 +解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等 -#### Balking +* 在类加载阶段解析的是非虚方法,静态绑定 -Balking (犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做 -了,直接结束返回 +* 也可以在初始化阶段之后再开始解析,这是为了支持 Java 的**动态绑定** ```java -public class MonitorService { - // 用来表示是否已经有线程已经在执行启动了 - private volatile boolean starting = false; - public void start() { - System.out.println("尝试启动监控线程..."); - synchronized (this) { - if (starting) { - return; - } - starting = true; - } - // 真正启动监控线程... +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 { +} ``` -对比保护性暂停模式:保护性暂停模式用在一个线程等待另一个线程的执行结果,当条件不满足时线程等待 -例子:希望 doInit() 方法仅被调用一次,下面的实现出现的问题: -* 当 t1 线程进入 init() 准备 doInit(),t2 线程进来,initialized 还为f alse,则 t2 就又初始化一次 -* volatile 适合一个线程写,其他线程读的情况,这个代码需要加锁 +**** -```java -public class TestVolatile { - volatile boolean initialized = false; - - void init() { - if (initialized) { - return; - } - doInit(); - initialized = true; - } - private void doInit() { - } -} -``` +#### 初始化 +##### 介绍 +初始化阶段才真正开始执行类中定义的 Java 程序代码,在准备阶段,类变量已经赋过一次系统要求的初始值;在初始化阶段,通过程序制定的计划去初始化类变量和其它资源,执行 -*** +在编译生成 class 文件时,编译器会产生两个方法加于 class 文件中,一个是类的初始化方法 clinit, 另一个是实例的初始化方法 init +类构造器 () 与实例构造器 () 不同,它不需要程序员进行显式调用,在一个类的生命周期中,类构造器最多被虚拟机**调用一次**,而实例构造器则会被虚拟机调用多次,只要程序员创建对象 +类在第一次实例化加载一次,把 class 读入内存,后续实例化不再加载,引用第一次加载的类 -## 无锁 -### CAS -#### 实现原理 +*** -无锁编程:lock free -CAS的全称是Compare-And-Swap,是**CPU并发原语** -* CAS并发原语体现在Java语言中就是sun.misc.Unsafe类的各个方法,调用UnSafe类中的CAS方法,JVM会实现出CAS汇编指令,这是一种完全依赖于硬件的功能,通过它实现了原子操作 -* CAS是一种系统原语,原语属于操作系统范畴,是由若干条指令组成 ,用于完成某个功能的一个过程,并且**原语的执行必须是连续的,执行过程中不允许被中断**,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致的问题,所以 CAS 是线程安全的 +##### clinit -底层原理:CAS 的底层是 `lock cmpxchg` 指令(X86 架构),在单核和多核 CPU 下都能够保证比较交换的原子性 +():类构造器,由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的 -* 程序是在单核处理器上运行,会省略 lock 前缀,单处理器自身会维护处理器内的顺序一致性,不需要 lock 前缀的内存屏障效果 +作用:是在类加载过程中的初始化阶段进行静态变量初始化和执行静态代码块 -* 程序是在多核处理器上运行,会为 cmpxchg 指令加上 lock 前缀(lock cmpxchg)。当某个核执行到带 lock 的指令时,CPU 会执行**总线锁定或缓存锁定**,当这个核把此指令执行完毕,这个过程不会被线程的调度机制所打断,保证了多个线程对内存操作的原子性 +* 如果类中没有静态变量或静态代码块,那么clinit方法将不会被生成 +* 在执行 clinit 方法时,必须先执行父类的clinit方法 +* clinit 方法只执行一次 +* static 变量的赋值操作和静态代码块的合并顺序由源文件中**出现的顺序**决定 -作用:比较当前工作内存中的值和主物理内存中的值,如果相同则执行规定操作,否者继续比较直到主内存和工作内存的值一致为止 +**线程安全**问题: -CAS特点: +* 虚拟机会保证一个类的 () 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 () 方法,其它线程都阻塞等待,直到活动线程执行 () 方法完毕 +* 如果在一个类的 () 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽 -* CAS 体现的是**无锁并发、无阻塞并发**,没有使用 synchronized,所以线程不会陷入阻塞,线程不需要频繁切换状态(上下文切换,系统调用) -* CAS 是基于乐观锁的思想 +特别注意:静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问 -CAS缺点: +```java +public class Test { + static { + //i = 0; // 给变量赋值可以正常编译通过 + System.out.print(i); // 这句编译器会提示“非法向前引用” + } + static int i = 1; +} +``` -- 循环时间长,开销大(因为执行的是do while,如果比较不成功一直在循环,最差的情况某个线程一直取到的值和预期值都不一样,就会无限循环导致饥饿),**使用CAS线程数不要超过CPU的核心数** -- 只能保证一个共享变量的原子操作 - - 对于一个共享变量执行操作时,可以通过循环CAS的方式来保证原子操作 - - 对于多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候只能用锁来保证原子性 -- 引出来ABA问题 +补充: +接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 () 方法。但两者不同的是,执行接口的 () 方法不需要先执行父接口的 () 方法,只有当父接口中定义的变量使用时,父接口才会初始化;接口的实现类在初始化时也一样不会执行接口的 () 方法 +**** -*** +##### 时机 -#### 乐观锁 +类的初始化是懒惰的,初始化时机: -CAS与Synchronized总结: +**主动引用**:虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随之发生): -* Synchronized是从悲观的角度出发: - 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。(**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**),因此Synchronized我们也将其称之为悲观锁。jdk中的ReentrantLock也是一种悲观锁,**性能较差** -* CAS是从乐观的角度出发: - 总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。**如果别人修改过,则获取现在最新的值。如果别人没修改过,直接修改共享数据的值**,CAS这种机制我们也可以将其称之为乐观锁。**综合性能较好**! +* 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化,最常见的生成这 4 条指令的场景是: + * new:使用 new 关键字实例化对象时 + * getstatic:程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池) + * putstatic:程序给类的静态变量赋值 + * invokestatic :调用一个类的静态方法时 +* 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化 +* 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化 +* 当虚拟机启动时,需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类 +* MethodHandle和VarHandle可以看作是轻量级的反射调用机制,而要想使用这2个调用, 就必须先使用findStaticVarHandle来初始化要调用的类 +* 补充:当一个接口中定义了 JDK8 新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化 +**被动引用**:所有引用类的方式都不会触发初始化,称为被动引用 +* 通过子类引用父类的静态字段,不会导致子类初始化,只会触发父类的初始化 +* 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自Object 的子类,其中包含了数组的属性和方法 +* 常量(final修饰)在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化 @@ -20479,127 +11591,98 @@ CAS与Synchronized总结: -### Atomic - -#### 常用API +##### init -常见原子类:AtomicInteger、AtomicBoolean、AtomicLong +init指的是实例构造器,主要作用是在类实例化过程中执行,执行内容包括成员变量初始化和代码块的执行 -构造方法: +实例化即调用 ()V ,虚拟机会保证这个类的构造方法的线程安全,先为实例变量分配内存空间,再执行赋默认值,然后根据源码中的顺序执行赋初值或代码块,没有成员变量初始化和代码块则不会执行 -* `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 | +*** -*** +#### 卸载阶段 +时机:执行了 System.exit() 方法,程序正常执行结束,程序在执行过程中遇到了异常或错误而异常终止,由于操作系统出现错误而导致Java虚拟机进程终止 -#### 原理分析 +卸载类即该类的 **Class 对象被 GC**,卸载类需要满足3个要求: -**AtomicInteger原理**:自旋锁 + CAS 算法 +1. 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象 +2. 该类没有在其他任何地方被引用 +3. 该类的类加载器的实例已被 GC -CAS算法:有3个操作数(内存值V, 旧的预期值A,要修改的值B) +在 JVM 生命周期类,由 JVM 自带的类加载器加载的类是不会被卸载的,由我们自定义的类加载器加载的类是可能被卸载。因为 JVM 会始终引用启动、扩展、系统类加载器,这些类加载器始终引用它们所加载的类,所以这些类始终是可及的 -* 当旧的预期值A == 内存值V 此时可以修改,将V改为B -* 当旧的预期值A != 内存值V 此时不能修改,并重新获取现在的最新值,重新获取的动作就是自旋 -分析getAndSet方法: -* AtomicInteger: +**** - ```java - public final int getAndSet(int newValue) { - /** - * this: 当前对象 - * valueOffset: 内存偏移量,内存地址 - */ - return unsafe.getAndSetInt(this, valueOffset, newValue); - } - ``` - valueOffset:表示该变量值在内存中的偏移地址,Unsafe就是根据内存偏移地址获取数据 - ```java - valueOffset = unsafe.objectFieldOffset - (AtomicInteger.class.getDeclaredField("value")); - //调用本地方法 --> - public native long objectFieldOffset(Field var1); - ``` +### 类加载器 -* unsafe类: +#### 加载器 - ```java - //val1: AtomicInteger对象本身 var2: 该对象值得引用地址 var4: 需要变动的数 - public final int getAndSetInt(Object var1, long var2, int var4) { - int var5; - do { - //var5: 用var1和var2找到的内存中的真实值 - var5 = this.getIntVolatile(var1, var2); - } while(!this.compareAndSwapInt(var1, var2, var5, var4)); - - return var5; - } - ``` +类与类加载器的关系: - var5:从主内存中拷贝到工作内存中的值(每次都要从主内存拿到最新的值到自己的本地内存),然后执行`compareAndSwapInt()`再和主内存的值进行比较,假设方法返回false,那么就一直执行 while方法,直到期望的值和真实值一样,修改数据 +* 在JVM中表示两个class对象是否为同一个类存在的两个必要条件: + - 类的完整类名必须一致,包括包名 + - 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同 +* 这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true -* 变量value用volatile修饰,保证了多线程之间的内存可见性,避免线程从自己的工作缓存中查找变量 +类加载器作用:加载字节码到JVM内存,得到Class类的对象 - ```java - private volatile int value - ``` +从 Java 虚拟机规范来讲,只存在以下两种不同的类加载器: - CAS 必须借助 volatile 才能读取到共享变量的最新值来实现**比较并交换**的效果 +- 启动类加载器(Bootstrap ClassLoader):使用 C++ 实现,是虚拟机自身的一部分 +- 自定义类加载器(User-Defined ClassLoader):Java虚拟机规范**将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器**,使用 Java语言 实现,独立于虚拟机 -分析getAndUpdate方法: +从 Java 开发人员的角度看: -* getAndUpdate: +* **启动类加载器(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 final int getAndUpdate(IntUnaryOperator updateFunction) { - int prev, next; - do { - prev = get(); //当前值,cas的期望值 - next = updateFunction.applyAsInt(prev);//期望值更新到该值 - } while (!compareAndSet(prev, next));//自旋 - return prev; - } - ``` +```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 - ```java - AtomicInteger a = new AtomicInteger(); - a.getAndUpdate(i -> i + 10); - ``` + //获取其上层 获取不到引导类加载器 + ClassLoader bootStrapClassLoader = extClassLoader.getParent(); + System.out.println(bootStrapClassLoader);//null -* compareAndSet: + //对于用户自定义类来说:使用系统类加载器进行加载 + ClassLoader classLoader = ClassLoaderTest.class.getClassLoader(); + System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2 - ```java - public final boolean compareAndSet(int expect, int update) { - /** - * this: 当前对象 - * valueOffset: 内存偏移量,内存地址 - * expect: 期望的值 - * update: 更新的值 - */ - return unsafe.compareAndSwapInt(this, valueOffset, expect, update); - } - ``` + //String 类使用引导类加载器进行加载的 --> java核心类库都是使用启动类加载器加载的 + ClassLoader classLoader1 = String.class.getClassLoader(); + System.out.println(classLoader1);//null - +} +``` @@ -20607,72 +11690,48 @@ CAS算法:有3个操作数(内存值V, 旧的预期值A,要修改的值B -#### 原子引用 +#### 加载类 -原子引用:对Object进行原子操作,提供一种读和写都是原子性的对象引用变量 +ClassLoader类,是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器) -原子引用类:AtomicReference、AtomicStampedReference、AtomicMarkableReference +获取ClassLoader的途径: -AtomicReference类: +* 获取当前类的ClassLoader:`clazz.getClassLoader()` +* 获取当前线程上下文的ClassLoader:`Thread.currentThread.getContextClassLoader()` +* 获取系统的ClassLoader:`ClassLoader.getSystemClassLoader()` +* 获取调用者的ClassLoader:`DriverManager.getCallerClassLoader()` -* 构造方法:`AtomicReference atomicReference = new AtomicReference();` +ClassLoader类常用方法: -* 常用API: +| 方法 | 说明 | +| ----------------------------------------------------- | ------------------------------------------------------------ | +| getParent() | 返回该类加载器的超类加载器 | +| loadClass(String name) | 加载名称为name的类,返回结果为java.lang.Class类的实例 | +| findClass(String name) | 查找名称为name的类,返回结果为java.lang.Class类的实例 | +| findLoadedClass(String name) | 查找名称为name的已经被加载过的类,返回结果为java.lang.Class类的实例 | +| defineClass(String name, byte[] b, int off,int len) | 把字节数组b中的内容转换为一个Java类 ,返回结果为java.lang.Class类的实例 | +| resolveClass(Class c) | 连接指定的一个java类 | - `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"); - - //创建原子引用包装类 - AtomicReference atomicReference = new AtomicReference<>(); - //设置主内存共享变量为s1 - atomicReference.set(s1); - - //比较并交换,如果现在主物理内存的值为z3,那么交换成l4 - while (true) { - Student s2 = new Student(44,"l4"); - if (atomicReference.compareAndSet(s1, s2)) { - break; - } - } - System.out.println(atomicReference.get()); - } -} -class Student { - private int id; - private String name; - //。。。。 -} -``` +*** -*** +#### 加载模型 +##### 加载机制 +在JVM中,对于类加载模型提供了三种,分别为全盘加载、双亲委派、缓存机制 -#### 原子数组 +- **全盘加载:**当一个类加载器负责加载某个 Class 时,该 Class 所依赖和引用的其他 Class 也将由该类加载器负责载入,除非显示指定使用另外一个类加载器来载入 -原子数组类:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray +- **双亲委派:**先让父类加载器加载该 Class,在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。简单来说就是,某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,**依次递归**,如果父加载器可以完成类加载任务,就成功返回;只有当父加载器无法完成此加载任务时,才自己去加载 -AtomicIntegerArray类方法: +- **缓存机制:**会保证所有加载过的 Class 都会被缓存,当程序中需要使用某个 Class 时,类加载器先从缓存区中搜寻该 Class,只有当缓存区中不存在该 Class 对象时,系统才会读取该类对应的二进制数据,并将其转换成 Class 对象存入缓冲区中 + - 这就是修改了 Class 后,必须重新启动 JVM,程序所做的修改才会生效的原因 -```java -/** -* i the index -* expect the expected value -* update the new value -*/ -public final boolean compareAndSet(int i, int expect, int update) { - return compareAndSetRaw(checkedByteOffset(i), expect, update); -} -``` + @@ -20680,71 +11739,107 @@ public final boolean compareAndSet(int i, int expect, int update) { -#### 原子更新器 +##### 双亲委派 -原子更新器类:AtomicReferenceFieldUpdater、AtomicIntegerFieldUpdater、AtomicLongFieldUpdater +双亲委派模型 (Parents Delegation Model):该模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器,这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance) -利用字段更新器,可以针对对象的某个域(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 +双亲委派机制的优点: -```java -public class UpdateDemo { - private volatile int field; - - public static void main(String[] args) { - AtomicIntegerFieldUpdater fieldUpdater = AtomicIntegerFieldUpdater - .newUpdater(UpdateDemo.class, "field"); - UpdateDemo updateDemo = new UpdateDemo(); - fieldUpdater.compareAndSet(updateDemo, 0, 10); - System.out.println(updateDemo.field);//10 - } -} -``` +* 可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性 +* Java类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一 +* 保护程序安全,防止类库的核心API被随意篡改 -*** + 例如:在工程中新建java.lang包,接着在该包下新建String类,并定义main函数 + ```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 classLoader)得到加载,启动类加载器优先级更高,在核心 jre 库中有其相同名字的类文件,但该类中并没有 main 方法 +**源码分析: ** -#### 原子累加器 +```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) { + // ClassNotFoundException thrown if class not found + // from the non-null parent class loader + } -原子累加器类:LongAdder、DoubleAdder、LongAccumulator、DoubleAccumulator + if (c == null) { + // 如果调用父类的类加载器无法对类进行加载,则用自己的findClass(name)方法进行加载 + long t1 = System.nanoTime(); + c = findClass(name); -LongAdder和LongAccumulator区别: + // 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) { + resolveClass(c); + } + return c; + } +} +``` -相同点: -* LongAddr与LongAccumulator类都是使用非阻塞算法CAS实现的, -* LongAddr类是LongAccumulator类的一个特例,只是LongAccumulator提供了更强大的功能,可以自定义累加规则,当accumulatorFunction为null时就等价于LongAddr -不同点: +**** -* 调用casBase时,LongAccumulator使用function.applyAsLong(b = base, x)来计算,LongAddr使用casBase(b = base, b + x)来计算 -* LongAccumulator类功能更加强大,构造方法参数中 - * accumulatorFunction是一个双目运算器接口,可以指定累加规则,比如累加或者相乘,其根据输入的两个参数返回一个计算值,LongAdder内置累加规则 - * identity则是LongAccumulator累加器的初始值,LongAccumulator可以为累加器提供非0的初始值,而LongAdder只能提供默认的0 +##### 破坏双亲 +破坏双亲委派模型有两种方式: +* 引入线程上下文类加载器 -*** + 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 Classloader 加载器拿到了 Application ClassLoader 加载器应该加载的类,破坏了双亲委派模型 -### Adder +* 自定义ClassLoader -#### 优化CAS + * 如果不想破坏双亲委派模型,只需要重写 findClass 方法 + * 如果想要去破坏双亲委派模型,需要去**重写 loadClass **方法 -LongAdder是Java8提供的类,跟AtomicLong有相同的效果,但对CAS机制进行了优化,尝试使用分段CAS以及自动分段迁移的方式来大幅度提升多线程高并发执行CAS操作的性能 -CAS底层实现是在一个循环中不断地尝试修改目标值,直到修改成功。如果竞争不激烈修改成功率很高,否则失败率很高,失败后这些重复的原子性操作会耗费性能(导致大量线程**空循环,自旋转**) -优化核心思想:数据分离,将AtomicLong的单点的**更新压力分担到各个节点**,在低并发的时候直接更新,可以保障和AtomicLong的性能基本一致,而在高并发的时候通过分散提高了性能 +参考文章:https://www.jianshu.com/p/4132d82ca3a6 @@ -20752,15 +11847,13 @@ CAS底层实现是在一个循环中不断地尝试修改目标值,直到修 -#### 优化机制 +#### 沙箱机制 -##### 分段机制 +沙箱机制:将 Java 代码限定在虚拟机特定的运行范围中,并且严格限制代码对本地系统资源访问,来保证对代码的有效隔离,防止对本地系统造成破坏 -分段 CAS 机制: +沙箱**限制系统资源访问**,包括CPU、内存、文件系统、网络,不同级别的沙箱对资源访问的限制也不一样 -* 在发生竞争时,创建Cell数组用于将不同线程的操作离散(通过hash等算法映射)到不同的节点上 -* 设置多个累加单元(会根据需要扩容,最大为CPU核数),Therad-0 累加 Cell[0],而 Thread-1 累加 Cell[1] 等,最后将结果汇总 -* 在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能 +举例:自定义 String 类,但是在加载自定义 String 类的时候会先使用引导类加载器加载,而引导类加载器在加载过程中会先加载 jdk 自带的文件(rt.jar包中的 java\lang\String.class),报错信息说没有 main 方法就是因为加载的是 rt.jar 包中的 String 类,这样可以保证对 java 核心源代码的保护 @@ -20768,997 +11861,728 @@ CAS底层实现是在一个循环中不断地尝试修改目标值,直到修 -##### 分段迁移 +#### 自定义 + +对于自定义类加载器的实现,只需要继承 ClassLoader 类,覆写 findClass 方法即可 -自动分段迁移机制:某个Cell的value执行CAS失败,就会自动寻找另一个Cell分段内的value值进行CAS操作 +作用:隔离加载类、修改类加载的方式、拓展加载源、防止源码泄漏 ```java -// 累加单元数组, 懒惰初始化 -transient volatile Cell[] cells; -// 基础值, 如果没有竞争, 则用 cas 累加这个域 -transient volatile long base; -// 在 cells 创建或扩容时, 置为 1, 表示加锁 -transient volatile int cellsBusy; -``` +//自定义类加载器,读取指定的类路径classPath下的class文件 +public class MyClassLoader extends ClassLoader{ + private String classPath; -Cells占用内存是相对比较大的,是惰性加载的,在无竞争的情况下直接更新base域,在第一次发生竞争的时候(CAS失败)就会创建一个大小为2的cells数组,每次扩容都是加倍 + public MyClassLoader(String classPath) { + this.classPath = classPath; + } -扩容数组等行为只能有一个线程执行,因此需要一个锁,这里通过 CAS 更新 cellsBusy 来实现一个简单的lock + @Override + protected Class findClass(String name) throws ClassNotFoundException { + byte[] data = new byte[0]; + try { + data = loadByte(name); + } catch (Exception e) { + e.printStackTrace(); + } + return defineClass(name, data, 0, data.length); + } -CAS锁: + private byte[] loadByte(String name) throws Exception { + name = name.replaceAll("\\.", "/"); + FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class"); + int len = fis.available(); + byte[] data = new byte[len]; + fis.read(data); + fis.close(); + return data; + } +} +``` ```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); +public class ClassLoaderTest { + public static void main(String[] args) throws Exception { + MyClassLoader classLoader = new MyClassLoader("D:\\workspace\\project\\src\\main\\java"); + Class clazz = classLoader.loadClass("com.demo.User"); + System.out.println(clazz.getClassLoader().getClass().getName()); } } +//当存在.java文件时,根据双亲委派机制,显示当前类加载器为AppClassLoader +//当将.java文件删除时,则显示使用的是自定义的类加载器 ``` -*** - -##### 伪共享 +*** -Cell为累加单元:数组访问索引是通过Thread里的threadLocalRandomProbe域取模实现的,这个域是ThreadLocalRandom更新的 -```java -@sun.misc.Contended static final class Cell { - volatile long value; - Cell(long x) { value = x; } - // 最重要的方法, 用来 cas 方式进行累加, prev 表示旧值, next 表示新值 - final boolean cas(long prev, long next) { - return UNSAFE.compareAndSwapLong(this, valueOffset, prev, next); - } - // 省略不重要代码 -} -``` -@sun.misc.Contended注解:防止缓存行伪共享 +## 运行机制 -Cell 是数组形式,**在内存中是连续存储的**,一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value),每一个 cache line 为 64 字节,因此缓存行可以存下 2 个的 Cell 对象,当Core-0 要修改 Cell[0]、Core-1 要修改 Cell[1],无论谁修改成功都会导致对方 Core 的缓存行失效,需要重新去主存获取 +### 执行过程 -![](https://gitee.com/seazean/images/raw/master/Java/JUC-伪共享1.png) + Java文件编译执行的过程: -@sun.misc.Contended:在使用此注解的对象或字段的前后各增加 128 字节大小的padding,使用2倍于大多数硬件缓存行让 CPU 将对象预读至缓存时占用不同的缓存行,这样就不会造成对方缓存行的失效 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java文件编译执行的过程.png) -![](https://gitee.com/seazean/images/raw/master/Java/JUC-伪共享2.png) +- 类加载器:用于装载字节码文件(.class文件) +- 运行时数据区:用于分配存储空间 +- 执行引擎:执行字节码文件或本地方法 +- 垃圾回收器:用于对 JVM 中的垃圾内容进行回收 +**** -*** +### 字节码 +#### 跨平台性 -#### 成员方法 +Java 语言:跨平台的语言(write once ,run anywhere) -* add:累加方法 +* 当 Java 源代码成功编译成字节码后,在不同的平台上面运行无须再次编译 +* 让一个 Java 程序正确地运行在 JVM 中,Java 源码就必须要被编译为符合 JVM 规范的字节码 - ```java - public void add(long x) { - // as 为累加单元数组 b 为基础值 x 为累加值 - Cell[] as; long b, v; int m; Cell a; - // 1. as 有值, 表示已经发生过竞争, 进入 if - // 2. cas 给 base 累加时失败了, 表示 base 发生了竞争, 进入 if - if ((as = cells) != null || !casBase(b = base, b + x)) { - // uncontended 表示 cell 没有竞争 - boolean uncontended = true; - if ( - // as 还没有创建 - as == null || (m = as.length - 1) < 0 || - // 当前线程对应的 cell 还没有创建 - (a = as[getProbe() & m]) == null || - // 当前线程的cell累加失败,a为当前线程的cell - !(uncontended = a.cas(v = a.value, v + x)) - //uncontended = false代表有竞争 - ) { - // 进入 cell 数组创建、cell 创建的流程 - longAccumulate(x, null, uncontended); - } - } - } - ``` +编译过程中的编译器: -* longAccumulate:cell数组创建 +* 前端编译器: Sun 的全量式编译器 javac、 Eclipse 的增量式编译器 ECJ,把源代码编译为字节码文件.class - ```java - // x null false - final void longAccumulate(long x, LongBinaryOperator fn, boolean w...ed) { - int h; - // 当前线程还没有对应的 cell, 需要随机生成一个 h 值用来将当前线程绑定到 cell - if ((h = getProbe()) == 0) { - ThreadLocalRandom.current(); // 初始化 probe - h = getProbe(); //h 对应新的 probe 值, 用来对应 cell - wasUncontended = true; - } - //collide 为 true 表示需要扩容 - boolean collide = false; - for (;;) { - Cell[] as; Cell a; int n; long v; - // cells已经创建 - if ((as = cells) != null && (n = as.length) > 0) { - // 线程对应的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; - try { - Cell[] rs; int m, j; - if ((rs = cells) != null && - (m = rs.length) > 0 && - rs[j = (m - 1) & h] == null) { - rs[j] = r; - created = true; - } - } finally { - cellsBusy = 0; - } - if (created) - break;// 成功则 break, 否则继续 continue 循环 - continue; - } - } - collide = false; - } - // 有竞争, 改变线程对应的 cell 来重试 cas - else if (!wasUncontended) - wasUncontended = true; - //cas尝试累加, fn配合LongAccumulator不为null, 配合LongAdder为null - else if (a.cas(v = a.value, ((fn == null) ? v + x : - fn.applyAsLong(v, x)))) - break; - // cells长度已经超过了最大长度或者已经扩容, 改变线程对应的cell来重试cas - else if (n >= NCPU || cells != as) - collide = false; // At max size or stale - // collide 为 false 进入此分支, 就不会进入下面的 else if 进行扩容了 - else if (!collide) - collide = true; - //加锁扩容 - else if (cellsBusy == 0 && casCellsBusy()) { - try { - if (cells == as) { // Expand table unless stale - Cell[] rs = new Cell[n << 1]; - for (int i = 0; i < n; ++i) - rs[i] = as[i]; - cells = rs; - } - } finally { - cellsBusy = 0; - } - collide = false; - continue;、 - } - h = advanceProbe(h); - } - //还没有 cells, 尝试给 cellsBusy 加锁 - else if (cellsBusy == 0 && cells == as && casCellsBusy()) { - boolean init = false; - try { - // 初始化 cells, 最开始长度为2, 填充一个初始累加值为x的cell - if (cells == as) { - Cell[] rs = new Cell[2]; - rs[h & 1] = new Cell(x);//填充线程对应的cell - cells = rs; - init = true; - } - } finally { - cellsBusy = 0; - } - if (init) - break; - } - // 上两种情况失败, 尝试给 base 累加 - else if (casBase(v = base, ((fn == null) ? v + x : - fn.applyAsLong(v, x)))) - break; // Fall back on using base - } - } - ``` + * IntelliJ IDEA 使用 javac 编译器 + * Eclipse 中,当开发人员编写完代码后保存时,ECJ 编译器就会把未编译部分的源码逐行进行编译,而非每次都全量编译,因此 ECJ 的编译效率会比 javac 更加迅速和高效 + * 前端编译器并不会直接涉及编译优化等方面的技术,具体优化细节移交给 HotSpot 的 JIT 编译器负责 -* sum:获取最终结果通过 sum 整合 +* 后端运行期编译器:HotSpot VM 的 C1、C2 编译器,也就是 JIT 编译器,Graal 编译器 + * JIT编译器:执行引擎部分详解 + * Graal 编译器:JDK10 HotSpot 加入的一个全新的即时编译器,编译效果短短几年时间就追平了 C2 +* 静态提前编译器:AOT (Ahead Of Time Compiler)编译器,直接把源代码编译成本地机器代码, -*** + * JDK 9 引入,是与即时编译相对立的一个概念,即时编译指的是在程序的运行过程中将字节码转换为机器码,AOT 是程序运行之前便将字节码转换为机器码 + * 优点:Java 虚拟机加载已经预编译成二进制库,可以直接执行,不必等待即时编译器的预热,减少 Java 应用第一次运行慢的现象 + * 缺点: + * 破坏了 Java "一次编译,到处运行”,必须为每个不同硬件编译对应的发行包 + * 降低了 Java 链接过程的动态性,加载的代码在编译期就必须全部已知 -### ABA -ABA问题:当进行获取主内存值时,该内存值在写入主内存时已经被修改了N次,但是最终又改成原来的值 -其他线程先把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 void set(V newReference, int newStamp)`:设置值和版本号 - `public V getReference()`:返回引用的值 - `public int getStamp()`:返回当前版本号 -```java -public static void main(String[] args) { - AtomicStampedReference atomicReference = new AtomicStampedReference<>(100,1); - int startStamp = atomicReference.getStamp(); - new Thread(() ->{ - int stamp = atomicReference.getStamp(); - atomicReference.compareAndSet(100, 101, stamp, stamp + 1); - stamp = atomicReference.getStamp(); - atomicReference.compareAndSet(101, 100, stamp, stamp + 1); - },"t1").start(); - - new Thread(() ->{ - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - if (!atomicReference.compareAndSet(100, 200, startStamp, startStamp + 1)) { - System.out.println(atomicReference.getReference());//100 - System.out.println(Thread.currentThread().getName() + "线程修改失败"); - } - },"t2").start(); -} -``` +#### 语言发展 +机器码:各种用二进制编码方式表示的指令,与CPU紧密相关,所以不同种类的CPU对应的机器指令不同 +指令:指令就是把机器码中特定的0和1序列,简化成对应的指令,例如mov,inc等,可读性稍好,但是不同的硬件平台的同一种指令(比如mov),对应的机器码也可能不同 +指令集:不同的硬件平台支持的指令是有区别的,每个平台所支持的指令,称之为对应平台的指令集 +- x86指令集,对应的是x86架构的平台 +- ARM指令集,对应的是ARM架构的平台 -*** +汇编语言:用助记符代替机器指令的操作码,用地址符号或标号代替指令或操作数的地址 +* 在不同的硬件平台,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令 +* 计算机只认识指令码,汇编语言编写的程序也必须翻译成机器指令码,计算机才能识别和执行 +高级语言:为了使计算机用户编程序更容易些,后来就出现了各种高级计算机语言 -### Unsafe +字节码:是一种中间状态(中间码)的二进制代码,比机器码更抽象,需要直译器转译后才能成为机器码 -Unsafe是CAS的核心类,由于Java无法直接访问底层系统,需要通过本地 (Native) 方法来访问 +* 字节码为了实现特定软件运行和软件环境,与硬件环境无关 +* 通过编译器和虚拟机器实现,编译器将源码编译成字节码,虚拟机器将字节码转译为可以直接执行的指令 -Unsafe类存在sun.misc包,其中所有方法都是native修饰的,都是直接调用操作系统底层资源执行相应的任务,基于该类可以直接操作特定的内存数据,其内部方法操作类似C的指针 + -模拟实现原子整数: -```java -public static void main(String[] args) { - MyAtomicInteger atomicInteger = new MyAtomicInteger(10); - if (atomicInteger.compareAndSwap(20)) { - System.out.println(atomicInteger.getValue()); - } -} -class MyAtomicInteger { - private static final Unsafe UNSAFE; - private static final long VALUE_OFFSET; - private volatile int value; +*** - static { - try { - Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); - theUnsafe.setAccessible(true); - UNSAFE = (Unsafe) theUnsafe.get(null); - VALUE_OFFSET = UNSAFE.objectFieldOffset( - MyAtomicInteger.class.getDeclaredField("value")); - } catch (NoSuchFieldException | IllegalAccessException e) { - e.printStackTrace(); - throw new RuntimeException(); - } - } - public MyAtomicInteger(int value) { - this.value = value; - } - public int getValue() { - return value; - } - public boolean compareAndSwap(int update) { - while (true) { - int prev = this.value; - int next = update; - // 当前对象 内存偏移量 期望值 更新值 - if (UNSAFE.compareAndSwapInt(this, VALUE_OFFSET, prev, update)) { - System.out.println("CAS成功"); - return true; - } - } - } -} -``` +#### 类结构 -*** +##### 文件结构 +字节码是一种二进制的类文件,是编译之后供虚拟机解释执行的二进制字节码文件,**一个 class 文件对应一个 public 类型的类或接口** +字节码内容是 JVM 的字节码指令,不是机器码,C、C++ 经由编译器直接生成机器码,所以 C 执行效率比 Java 高 -### final +JVM 官方文档:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html -#### 原理 +根据 JVM 规范,类文件结构如下: ```java -public class TestFinal { - final int a = 20; +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]; } ``` -字节码: +| 类型 | 名称 | 说明 | 长度 | 数量 | +| -------------- | ------------------- | -------------------- | ------- | --------------------- | +| 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 | -```java -0: aload_0 -1: invokespecial #1 // Method java/lang/Object."":()V -4: aload_0 -5: bipush 20 //将值直接放入栈中 -7: putfield #2 // Field a:I -<-- 写屏障 -10: return -``` +Class 文件格式采用一种类似于 C 语言结构体的方式进行数据存储,这种结构中只有两种数据类型:无符号数和表 -final 变量的赋值通过 putfield 指令来完成,在这条指令之后也会加入写屏障,保证在其它线程读到它的值时不会出现为 0 的情况 +* 无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串 +* 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,表都以 `_info` 结尾,用于描述有层次关系的数据,整个 Class 文件本质上就是一张表,由于表没有固定长度,所以通常会在其前面加上个数说明 -其他线程访问final修饰的变量会复制一份放入栈中,效率更高 +获取方式: +* HelloWorld.java 执行 `javac -parameters -d . HellowWorld.java`指令 +* 写入文件指令 `javap -v xxx.class >xxx.txt` +* IDEA 插件 jclasslib -*** +*** -#### 不可变 -不可变:如果一个对象不能够修改其内部状态(属性),那么就是不可变对象 +##### 魔数版本 -不可变对象线程安全的,因为不存在并发修改,是另一种避免竞争的方式 +魔数:每个 Class 文件开头的 4 个字节的无符号整数称为魔数(Magic Number),是 Class 文件的标识符,代表这是一个能被虚拟机接受的有效合法的 Class 文件, -String 类也是不可变的,该类和类中所有属性都是 final 的 +* 魔数值固定为 0xCAFEBABE,不符合则会抛出错误 -* 类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性保 +* 使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动 -* 属性用 final 修饰保证了该属性是只读的,不能修改 +版本:4 个 字节,5 6两个字节代表的是编译的副版本号 minor_version,而 7 8 两个字节是编译的主版本号 major_version - ```java - public final class String - implements java.io.Serializable, Comparable, CharSequence { - /** The value is used for character storage. */ - private final char value[]; - //.... - } - ``` +* 不同版本的 Java 编译器编译的 Class 文件对应的版本是不一样的,高版本的 Java 虚拟机可以执行由低版本编译器生成的 Class 文件,反之 JVM 会抛出异常 `java.lang.UnsupportedClassVersionError` -更改 String 类数据时,会构造新字符串对象,生成新的 char[] value,这种通过创建副本对象来避免共享的方式称之为**保护性拷贝(defensive copy)** +| 主版本(十进制) | 副版本(十进制) | 编译器版本 | +| ---------------- | ---------------- | ---------- | +| 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) -*** +图片来源:https://www.bilibili.com/video/BV1PJ411n7xZ -### State -无状态:成员变量保存的数据也可以称为状态信息,无状态就是没有成员变量 +*** -Servlet 为了保证其线程安全,一般不为 Servlet 设置成员变量,这种没有任何成员变量的类是线程安全的 +##### 常量池 -*** +常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项 u2 类型的无符号数,代表常量池计数器(constant_pool_count),这个容量计数是从 1 而不是 0 开始,是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达不引用任何一个常量池项目,这种情况可用索引值 0 来表示 +constant_pool 是一种表结构,以1 ~ constant_pool_count - 1为索引,表明有多少个常量池表项。表项中存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池 +* 字面量(Literal) :基本数据类型、字符串类型常量、声明为 final 的常量值等 -### Local +* 符号引用(Symbolic References):类和接口的全限定名、字段的名称和描述符、方法的名称和描述符 -#### 基本介绍 + * 全限定名:com/test/Demo 这个就是类的全限定名,仅仅是把包名的 `.` 替换成 `/`,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个 `;` 表示全限定名结束 -ThreadLocal类用来提供线程内部的局部变量,这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量 + * 简单名称:指没有类型和参数修饰的方法或者字段名称,比如字段 x 的简单名称就是 x -ThreadLocal实例通常来说都是 `private static` 类型的,属于一个线程的本地变量,用于关联线程和线程上下文。每个线程都会在 ThreadLocal 中保存一份该线程独有的数据,所以是线程安全的 + * 描述符:用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值 -ThreadLocal 作用: + | 标志符 | 含义 | + | ------ | ---------------------------------------------------- | + | 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` | -* 线程并发:应用在多线程并发的场景下 +常量类型和结构: -* 传递数据:通过ThreadLocal实现在同一线程不同函数或组件中传递公共变量,减少传递复杂度 +| 类型 | 标志(或标识) | 描述 | +| -------------------------------- | ------------ | ---------------------- | +| 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 -对比synchronized: -| | synchronized | ThreadLocal | -| ------ | ------------------------------------------------------------ | ------------------------------------------------------------ | -| 原理 | 同步机制采用**以时间换空间**的方式,只提供了一份变量,让不同的线程排队访问 | ThreadLocal采用**以空间换时间**的方式,为每个线程都提供了一份变量的副本,从而实现同时访问而相不干扰 | -| 侧重点 | 多个线程之间访问资源的同步 | 多线程中让每个线程之间的数据相互隔离 | +**** -*** +##### 访问标识 +访问标识(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 | 标志这是一个枚举 | -| 方法 | 描述 | -| -------------------------- | ---------------------------- | -| 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(); - } - } -} -``` +类索引、父类索引、接口索引集合 +* 类索引用于确定这个类的全限定名 +* 父类索引用于确定这个类的父类的全限定名,Java 语言不允许多重继承,所以父类索引只有一个,除了Object 之外,所有的 Java 类都有父类,因此除了 java.lang.Object 外,所有 Java 类的父类索引都不为0 -*** +* 接口索引集合就用来描述这个类实现了哪些接口 + * interfaces_count 项的值表示当前类或接口的直接超接口数量 + * interfaces[] 接口索引集合,被实现的接口将按 implements 语句后的接口顺序从左到右排列在接口索引集合中 +| 长度 | 含义 | +| ---- | ---------------------------- | +| u2 | this_class | +| u2 | super_class | +| u2 | interfaces_count | +| u2 | interfaces[interfaces_count] | -##### 应用场景 -ThreadLocal 适用于如下两种场景 +*** -- 每个线程需要有自己单独的实例 -- 实例需要在多个方法中共享,但不希望被多线程共享 -**事务管理**,ThreadLocal方案有两个突出的优势: -1. 传递数据:保存每个线程绑定的数据,在需要的地方可以直接获取,避免参数直接传递带来的代码耦合问题 +##### 字段表 -2. 线程隔离:各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失 +字段 fields 用于描述接口或类中声明的变量,包括类变量以及实例变量,但不包括方法内部、代码块内部声明的局部变量以及从父类或父接口继承。字段叫什么名字、被定义为什么数据类型,都是无法固定的,只能引用常量池中的常量来描述 -```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; - } - // ... -} -``` +fields_count(字段计数器),表示当前 class 文件 fields 表的成员个数,用两个字节来表示 -用 ThreadLocal 使 SimpleDateFormat 从独享变量变成单个线程变量: +fields[](字段表): -```java -public class ThreadLocalDateUtil { - private static ThreadLocal threadLocal = new ThreadLocal() { - @Override - protected DateFormat initialValue() { - return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - } - }; +* 表中的每个成员都是一个 fields_info 结构的数据项,用于表示当前类或接口中某个字段的完整描述 - public static Date parse(String dateStr) throws ParseException { - return threadLocal.get().parse(dateStr); - } +* 字段访问标识: - public static String format(Date date) { - return threadLocal.get().format(date); - } -} -``` + | 标志名称 | 标志值 | 含义 | + | ------------- | ------ | -------------------------- | + | 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 -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也会随之销毁,能减少内存的使用 +方法表是 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 | -* set() +* 方法表访问标志: - * 获取当前线程,并根据当前线程获取一个Map - * 获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key) - * 如果Map为空,则给该线程创建 Map,并设置初始值 + | 标志名称 | 标志值 | 含义 | + | ------------- | ------ | -------------------------- | + | 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 | - ```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,以当前ThreadLocal为key删除对应的实体entry - m.remove(this); - } - ``` -* initialValue() +##### 属性表 - 作用:返回该线程局部变量的初始值。 +属性表集合,指的是 Class 文件所携带的辅助信息,比如该 Class 文件的源文件的名称,以及任何带有 `RetentionPolicy.CLASS` 或者 `RetentionPolicy.RUNTIME` 的注解,这类信息通常被用于 Java 虚拟机的验证和运行,以及 Java 程序的调试。字段表、方法表都可以有自己的属性表,用于描述某些场景专有的信息 - * 延迟调用的方法,在set方法还未调用而先调用了get方法时才执行,并且仅执行1次 +attributes_ count(属性计数器):表示当前文件属性表的成员个数 - * 该方法缺省(默认)实现直接返回一个``null`` +attributes[](属性表):属性表的每个项的值必须是 attribute_info 结构 - * 如果想要一个初始值,可以重写此方法, 该方法是一个``protected``的方法,为了让子类覆盖而设计的 +* 属性的通用格式: ```java - protected T initialValue() { - return null; + ConstantValue_attribute{ + u2 attribute_name_index; //属性名索引 + u4 attribute_length; //属性长度 + u2 attribute_info; //属性表 } ``` - +* 属性类型: -*** + | 属性名称 | 使用位置 | 含义 | + | ------------------------------------- | ------------------ | ------------------------------------------------------------ | + | 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 指令引用的引导方式限定符 | -#### 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; +#### javap -// 进行扩容的阈值,表使用量大于它的时候进行扩容。 -private int threshold; // Default to 0 -``` +##### javac -存储结构 Entry: +javac:编译命令,将 java 源文件编译成 class 字节码文件 -* Entry继承WeakReference,key是弱引用,目的是将ThreadLocal对象的生命周期和线程生命周期解绑 -* Entry限制只能用ThreadLocal作为key,key为null (entry.get() == null) 意味着key不再被引用,entry也可以从table中清除 +`javac xx.java` 不会在生成对应的局部变量表等信息,使用 `javac -g xx.java` 可以生成所有相关信息 -```java -static class Entry extends WeakReference> { - Object value; - Entry(ThreadLocal k, Object v) { - super(k); - value = v; - } -} -``` +**** -*** +##### javap -##### 成员方法 +javap 反编译生成的字节码文件,根据 class 字节码文件,反解析出当前类对应的 code 区 (字节码指令)、局部变量表、异常表和代码行偏移量映射表、常量池等信息 -* 构造方法 +用法:javap - ```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); - } - ``` +```sh +-help --help -? 输出此用法消息 +-version 版本信息 +-public 仅显示公共类和成员 +-protected 显示受保护的/公共类和成员 +-package 显示程序包/受保护的/公共类和成员 (默认) +-p -private 显示所有类和成员 + #常用的以下三个 +-v -verbose 输出附加信息 +-l 输出行号和本地变量表 +-c 对代码进行反汇编 -* hashcode +-s 输出内部类型签名 +-sysinfo 显示正在处理的类的系统信息 (路径, 大小, 日期, MD5 散列) +-constants 显示最终常量 +-classpath 指定查找用户类文件的位置 +-cp 指定查找用户类文件的位置 +-bootclasspath 覆盖引导类文件的位置 +``` - ```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() { - } - ``` +Java 字节码属于 JVM 基本执行指令。由一个字节长度的代表某种操作的操作码(opcode)以及零至多个代表此操作所需参数的操作数(operand)所构成,虚拟机中许多指令并不包含操作数,只有一个操作码(零地址指令) +由于限制了 Java 虚拟机操作码的长度为一个字节(0~255),所以指令集的操作码总数不可能超过 256 条 +在 JVM 的指令集中,大多数的指令都包含了其操作所对应的数据类型信息。例如 iload 指令用于从局部变量表中加载 int 型的数据到操作数栈中,而 fload 指令加载的则是 float 类型的数据 -*** +* i 代表对 int 类型的数据操作 +* l 代表 long +* s 代表 short +* b 代表 byte +* c 代表 char +* f 代表 float +* d 代表 double +大部分的指令都没有支持 byte、char、short、boolean 类型,编译器会在编译期或运行期将 byte 和 short 类型的数据带符号扩展(Sign-Extend-)为相应的 int 类型数据,将 boolean 和 char 类型数据零位扩展(Zero-Extend-)为相应的 int 类型数据 +在做值相关操作时: -#### 内存泄漏 +- 一个指令,可以从局部变量表、常量池、堆中对象、方法调用、系统调用中等取得数据,这些数据(可能是值,也可能是对象的引用)被压入操作数栈 +- 一个指令,也可以从操作数栈中取出一到多个值(pop 多次),完成赋值、加减乘除、方法传参、系统调用等等操作 -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 就会导致内存泄漏 +* xload、xload_n,x 表示取值数据类型,为 i、l、f、d、a, n 为 0 到 3 +* 指令 xload_n 表示将第 n 个局部变量压入操作数栈,aload_n 表示将一个对象引用压栈 +* 指令 xload n 通过指定参数的形式,把局部变量压入操作数栈,局部变量数量超过 4 个时使用这个命令 -解决方法:使用完 ThreadLocal 中存储的内容后将它 **remove** 掉就可以 +常量入栈指令:将常数压入操作数栈,根据数据类型和入栈内容的不同,又分为 const、push、ldc指令 -ThreadLocal 内部解决方法:在 ThreadLocalMap 中的 set/getEntry 方法中,会对 key 进行判断,如果为null (ThreadLocal 为 null) 的话,那么会对Entry进行垃圾回收。所以**使用弱引用比强引用多一层保障**,就算不调用remove,也有机会进行GC +* 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 压入栈 +出栈装入局部变量表指令:将操作数栈中栈顶元素弹出后,装入局部变量表的指定位置,用于给局部变量赋值 +* xstore、xstore_n,x 表示取值类型为 i、l、f、d、a, n 为 0 到 3 +* xastore 表示存入数组,x 取值为 i、l、f、d、a、b、c、s -*** +扩充局部变量表的访问索引的指令:wide -#### 变量传递 +**** -##### 基本使用 -父子线程:创建子线程的线程是父线程,比如实例中的 main 线程就是父线程 -ThreadLocal 中存储的是线程的局部变量,如果想实现线程间局部变量传递可以使用 InheritableThreadLocal 类 +##### 算术指令 -```java -public static void main(String[] args) { - ThreadLocal threadLocal = new InheritableThreadLocal<>(); - threadLocal.set("父线程设置的值"); +算术指令用于对两个操作数栈上的值进行某种特定运算,并把计算结果重新压入操作数栈 - new Thread(() -> System.out.println("子线程输出:" + threadLocal.get())).start(); -} -// 子线程输出:父线程设置的值 -``` +没有直接支持 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 +``` -InheritableThreadLocal 源码: +分析i++:从字节码角度分析:a++ 和 ++a 的区别是先执行 iload 还是 先执行 iinc ```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); +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 } } ``` -实现父子线程间的局部变量共享需要追溯到 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++; - } +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 } } ``` -参考文章:https://blog.csdn.net/feichitianxia/article/details/110495764 - +*** -*** +##### 类型转换 +类型转换指令可以将两种不同的数值类型进行相互转换,除了 boolean 之外的七种类型 -## 线程池 +宽化类型转换: -### 基本概述 +* JVM 支持以下数值的宽化类型转换(widening numeric conversion),小范围类型到大范围类型的安全转换 + * 从 int 类型到 long、float 或者 double 类型,对应的指令为 i2l、i2f、i2d + * 从 long 类型到 float、 double 类型,对应的指令为 l2f、l2d + * 从 float 类型到 double 类型,对应的指令为 f2d -线程池:一个容纳多个线程的容器,容器中的线程可以重复使用,省去了频繁创建和销毁线程对象的操作 +* 精度损失问题 + * 宽化类型转换是不会因为超过目标类型最大值而丢失信息 + * 从 int 转换到 float 或者 long 类型转换到 double 时,将可能发生精度丢失 -线程池作用: +* 从 byte、char 和 short 类型到 int 类型的宽化类型转换实际上是不存在的,JVM 把它们当作 int 处理 -1. 降低资源消耗,减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务 -2. 提高响应速度,当任务到达时,如果有线程可以直接用,不会出现系统僵死 -3. 提高线程的可管理性,如果无限制的创建线程,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控 +窄化类型转换: -线程池的核心思想:**线程复用**,同一个线程可以被重复使用,来处理多个任务 +* Java 虚拟机直接支持以下窄化类型转换: + * 从 int 类型至 byte、 short 或者 char 类型,对应的指令有 i2b、i2c、i2s + * 从 long 类型到 int 类型,对应的指令有 l2i + * 从 float 类型到 int 或者 long 类型,对应的指令有:f2i、f2l + * 从 double 类型到 int、long 或 float 者类型,对应的指令有 d2i、d2、d2f -池化技术 (Pool) :一种编程技巧,核心思想是资源复用,在请求量大时能优化应用性能,降低系统频繁建连的资源开销 +* 精度损失问题: + * 窄化类型转换可能会导致转换结果具备不同的正负号、不同的数量级,转换过程可能会导致数值丢失精度 + * 将一个浮点值窄化转换为整数类型 T(T 限于 int 或 long 类型之一)时,将遵循以下转换规则: + - 如果浮点值是 NaN,那转换结果就是 int 或 long 类型的 0 + - 如果浮点值不是无穷大的话,浮点值使用 IEEE 754 的向零舍入模式取整,获得整数值 v,如果 v 在目标类型 T 的表示范围之内,那转换结果就是 v,否则将根据 v 的符号,转换为 T 所能表示的最大或者最小正数 @@ -21766,251 +12590,163 @@ private ThreadLocalMap(ThreadLocalMap parentMap) { -### 阻塞队列 +##### 创建访问 -#### 基本介绍 +创建指令: -有界队列和无界队列: +* 创建类实例指令:new,接收一个操作数指向常量池的索引,表示要创建的类型,执行完成后将对象的引用压入栈 -* 有界队列:有固定大小的队列,比如设定了固定大小的 LinkedBlockingQueue,又或者大小为 0 + ```java + 0: new #2 // class com/jvm/bytecode/Demo + 3: dup + 4: invokespecial #3 // Method "":()V + ``` -* 无界队列:没有设置固定大小的队列,这些队列可以直接入队,直到溢出,一般不会有到这么大的容量(超过 Integer.MAX_VALUE),所以相当于 “无界” + dup 是复制操作数栈栈顶的内容,需要两份引用原因: -java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:**FIFO 队列** + - 一个要配合 invokespecial 调用该对象的构造方法 :()V (会消耗掉栈顶一个引用) + - 一个要配合 astore_1 赋值给局部变量 -- ArrayBlockQueue:由数组结构组成的有界阻塞队列 -- LinkedBlockingQueue:由链表结构组成的无界(默认大小 Integer.MAX_VALUE)的阻塞队列 -- PriorityBlockQueue:支持优先级排序的无界阻塞队列 -- DelayQueue:使用优先级队列实现的延迟无界阻塞队列 -- SynchronousQueue:不存储元素的阻塞队列,每一个生产线程会阻塞到有一个put的线程放入元素为止 -- LinkedTransferQueue:由链表结构组成的无界阻塞队列 -- LinkedBlockingDeque:由链表结构组成的**双向**阻塞队列 +* 创建数组的指令:newarray、anewarray、multianewarray -与普通队列(LinkedList、ArrayList等)的不同点在于阻塞队列中阻塞添加和阻塞删除方法,以及线程安全: + * newarray:创建基本类型数组 + * anewarray:创建引用类型数组 + * multianewarray:创建多维数组 -* 阻塞添加 take():当阻塞队列元素已满时,添加队列元素的线程会被阻塞,直到队列元素不满时才重新唤醒线程执行 -* 阻塞删除 put():在队列元素为空时,删除队列元素的线程将被阻塞,直到队列不为空再执行删除操作(一般都会返回被删除的元素) +字段访问指令:对象创建后可以通过对象访问指令获取对象实例或数组实例中的字段或者数组元素 +* 访问类字段(static字段,或者称为类变量)的指令:getstatic、putstatic +* 访问类实例字段(非static字段,或者称为实例变量)的指令:getfield、 putfield +类型检查指令:检查类实例或数组类型的指令 -*** +* checkcast:用于检查类型强制转换是否可以进行,如果可以进行 checkcast 指令不会改变操作数栈,否则它会抛出 ClassCastException 异常 +* instanceof:判断给定对象是否是某一个类的实例,会将判断结果压入操作数栈 -#### 核心方法 -| 方法类型 | 抛出异常 | 特殊值 | 阻塞 | 超时 | -| ---------------- | --------- | -------- | ------ | ------------------ | -| 插入 | add(e) | offer(e) | put(e) | offer(e,time,unit) | -| 移除 | remove() | poll() | take() | poll(time,unit) | -| 检查(队首元素) | element() | peek() | 不可用 | 不可用 | -* 抛出异常组: - * 当阻塞队列满时:在往队列中 add 插入元素会抛出 IIIegalStateException: Queue full - * 当阻塞队列空时:再往队列中remove移除元素,会抛出 NoSuchException -* 特殊值组: - * 插入方法:成功 true,失败 false - * 移除方法:成功返回出队列元素,队列没有就返回 null -* 阻塞组: - * 当阻塞队列满时,生产者继续往队列里 put 元素,队列会一直阻塞生产线程直到 put 数据或响应中断退出 - * 当阻塞队列空时,消费者线程试图从队列里 take 元素,队列会一直阻塞消费者线程直到队列可用 -* 超时退出:当阻塞队列满时,队里会阻塞生产者线程一定时间,超过限时后生产者线程会退出 +**** -*** +##### 方法指令 +方法调用指令:invokevirtual、 invokeinterface、invokespecial、invokestatic、invokedynamic +**方法调用章节详解** -#### 链表队列 -##### 入队出队 -LinkedBlockingQueue源码: +*** -```java -public class LinkedBlockingQueue extends AbstractQueue - implements BlockingQueue, java.io.Serializable { - static class Node { - E item; - /** - * 下列三种情况之一 - * - 真正的后继节点 - * - 自己, 发生在出队时 - * - null, 表示是没有后继节点, 是最后了 - */ - Node next; - Node(E x) { item = x; } - } -} -``` -入队: +##### 操作数栈 -* 初始化链表 `last = head = new Node(null)`,Dummy 节点用来占位,item 为 null - -* 当一个节点入队 `last = last.next = node` - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-LinkedBlockingQueue入队流程.png) +JVM 提供的操作数栈管理指令,可以用于直接操作操作数栈的指令 -* 再来一个节点入队`last = last.next = node` +* pop、pop2:将一个或两个元素从栈顶弹出,并且直接废弃 +* dup、dup2,dup_x1、dup2_x1,dup_x2、dup2_x2:复制栈顶一个或两个数值并重新压入栈顶 -出队:出队首节点,先入先出 +* swap:将栈最顶端的两个 slot 数值位置交换,JVM 没有提供交换两个 64 位数据类型数值的指令 -* 源码: +* nop:一个非常特殊的指令,字节码为 0x00,和汇编语言中的 nop 一样,表示什么都不做,一般可用于调试、占位等 - ```java - Node h = head; - Node first = h.next; - h.next = h; // help GC - head = first; - E x = first.item;// 保存数据 - first.item = null; - return x; - ``` -* `h = head` -> `first = h.next` - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-LinkedBlockingQueue出队流程1.png) +*** -* `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**) +##### 控制转移 - +###### 指令介绍 - -*** +比较指令:比较栈顶两个元素的大小,并将比较结果入栈 +* 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 时跳转 | -用了两把锁和 dummy 节点: +比较条件跳转指令: -* 用一把锁,同一时刻,最多只允许有一个线程(生产者或消费者,二选一)执行 -* 用两把锁,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行 - * 消费者与消费者线程仍然串行 - * 生产者与生产者线程仍然串行 +| 指令 | 说明 | +| --------- | --------------------------------------------------------- | +| if_icmpeq | 比较栈顶两 int 类型数值大小(下同),当前者等于后者时跳转 | +| if_icmpne | 当前者不等于后者时跳转 | +| if_icmplt | 当前者小于后者时跳转 | +| if_icmple | 当前者小于等于后者时跳转 | +| if_icmpgt | 当前者大于后者时跳转 | +| if_icmpge | 当前者大于等于后者时跳转 | +| if_acmpeq | 当结果相等时跳转 | +| if_acmpne | 当结果不相等时跳转 | -线程安全分析: +多条件分支跳转指令: -* 当节点总数大于 2 时(包括 dummy 节点),putLock 保证的是 last 节点的线程安全,takeLock 保证的是 - head 节点的线程安全,两把锁保证了入队和出队没有竞争 +* tableswitch:用于 switch 条件跳转,case 值连续 +* lookupswitch:用于 switch 条件跳转,case 值不连续 -* 当节点总数等于 2 时(即一个 dummy 节点,一个正常节点)这时候,仍然是两把锁锁两个对象,不会竞争 +无条件跳转指令: -* 当节点总数等于 1 时(就一个 dummy 节点)这时 take 线程会被 notEmpty 条件阻塞,有竞争,会阻塞 +* goto:用来进行跳转到指定行号的字节码 - ```java - // 用于 put(阻塞) offer(非阻塞) - private final ReentrantLock putLock = new ReentrantLock(); - // 用户 take(阻塞) poll(非阻塞) - private final ReentrantLock takeLock = new ReentrantLock(); - ``` +* goto_w:无条件跳转(宽索引) -入队出队: -* put 操作: - ```java - public void put(E e) throws InterruptedException { - if (e == null) throw new NullPointerException(); - int c = -1; - 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线程 - if (c + 1 < capacity) - notFull.signal(); - } finally { - putLock.unlock(); - } - // 如果还有一个元素,唤醒 take 线程 - if (c == 0) - signalNotEmpty(); - } - private void signalNotEmpty() { - final ReentrantLock takeLock = this.takeLock; - takeLock.lock(); - try { - // 调用的是 notEmpty.signal(),而不是 notEmpty.signalAll(),是为了减少竞争 - // 因为只剩下一个元素 - notEmpty.signal(); - } finally { - takeLock.unlock(); - } - } - ``` +*** -* take 操作: - ```java - public E take() throws InterruptedException { - E x; - 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 - x = dequeue(); - // 返回自减前的数子 - c = count.getAndDecrement(); - // 队列还有元素 - if (c > 1) - // 唤醒其他消费take线程 - notEmpty.signal(); - } finally { - takeLock.unlock(); - } - // 如果队列中只有一个空位时, 叫醒 put 线程 - // 如果有多个线程进行出队, 第一个线程满足 c == capacity, 但后续线程 c < capacity - if (c == capacity) - // 调用的是 notFull.signal() 而不是 notFull.signalAll() 是为了减少竞争 - signalNotFull(); - return x; - } - ``` +###### 代码示例 +条件判断: -*** +```java +public static void main(String[] args) { int a = 0; if(a == 0) { a = 10; } else { a = 20; }} +``` +```sh +1: istore_12: iload_13: ifne 126: bipush 108: istore_19: goto 1512: bipush 2014: istore_115: return +``` +while循环: -##### 性能比较 +```java +public static void main(String[] args) { int a = 0; while (a < 10) { a++; }} +``` -主要列举 LinkedBlockingQueue 与 ArrayBlockingQueue 的性能比较: +```sh +0: iconst_01: istore_12: iload_13: bipush 105: if_icmpge 148: iinc 1, 111: goto 214: return +``` -* Linked 支持有界,Array 强制有界 -* Linked 实现是链表,Array 实现是数组 -* Linked 是懒惰的,而 Array 需要提前初始化 Node 数组 -* Linked 每次入队会生成新 Node,而 Array 的 Node 是提前创建好的 -* Linked 两把锁,Array 一把锁 +for循环: +```java +for (int i = 0; i < 10; i++) { } +``` +```java +0: iconst_01: istore_12: iload_13: bipush 105: if_icmpge 148: iinc 1, 1 //在slot上直接递增11: goto 214: return +``` @@ -22018,242 +12754,220 @@ public class LinkedBlockingQueue extends AbstractQueue -#### 同步队列 +##### 异常处理 -与其他BlockingQueue不同,SynchronousQueue是一个不存储元素的BlockingQueue +###### 处理机制 -(待更新) +抛出异常指令:athrow指令 +JVM 处理异常(catch语句)不是由字节码指令来实现的,而是**采用异常表来完成**的 +* 代码: -*** + ```java + public static void main(String[] args) { int i = 0; try { i = 10; } catch (ArithmeticException e) { i = 30; } catch (NullPointerException e) { i = 40; } catch (Exception e) { i = 50; }} + ``` +* 字节码: + * 多出一个 **Exception table** 的结构,[from, to) 是**前闭后开**的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号 + * 8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置,因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用 -#### 延迟队列 + ```java + public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=3, args_size=1 0: iconst_0 1: istore_1 2: bipush 10 4: istore_1 5: goto 26 8: astore_2 9: bipush 30 11: istore_1 12: goto 26 15: astore_2 16: bipush 40 18: istore_1 19: goto 26 22: astore_2 23: bipush 50 25: istore_1 26: return Exception table: from to target type 2 5 8 Class java/lang/Exception 2 5 15 Class java/lang/NullPointerException 2 5 22 Class java/lang/Exception LineNumberTable: ... LocalVariableTable: Start Length Slot Name Signature 9 3 2 e Ljava/lang/ArithmeticException; 16 3 2 e Ljava/lang/NullPointerException; 23 3 2 e Ljava/lang/Exception; 0 27 0 args [Ljava/lang/String; 2 25 1 i I StackMapTable: ... MethodParameters: ...} + ``` -##### 延迟阻塞 -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 +###### finally - // 需要实现的接口,获得延迟时间 用过期时间-当前时间 - @Override - public long getDelay(TimeUnit unit) { - return unit.convert((start + time) - System.currentTimeMillis(), TimeUnit.MILLISECONDS); - } +finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程 - // 用于延迟队列内部比较排序 当前时间的延迟时间 - 被比较对象的延迟时间 - @Override - public int compareTo(Delayed o) { - DelayTask obj = (DelayTask) o; - return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS)); - } -} -``` +* 代码: + ```java + public static void main(String[] args) { int i = 0; try { i = 10; } catch (Exception e) { i = 20; } finally { i = 30; }} + ``` +* 字节码: + + ```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: returnException table: from to target type 2 5 11 Class java/lang/Exception 2 5 21 any // 剩余的异常类型,比如 Error 11 15 21 any // 剩余的异常类型,比如 ErrorLineNumberTable: ...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 + ``` -**** +*** -##### 优先队列 +###### return -*** +* 吞异常 + ```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 -> slot 1 存入局部变量表的 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,会**吞掉异常** +* 不吞异常 -### 操作Pool + ```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,赋值给i,放入slot 0 3: iload_0 // i(10)加载至操作数栈 4: istore_1 // 10 -> slot 1,暂存至 slot 1,目的是为了固定返回值 5: bipush 20 // 20 放入栈顶 7: istore_0 // 20 -> i 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 + ``` -##### Executor -存放线程的容器: -```java -private final HashSet workers = new HashSet(); -``` +*** -构造方法: -```java -public ThreadPoolExecutor(int corePoolSize, - int maximumPoolSize, - long keepAliveTime, - TimeUnit unit, - BlockingQueue workQueue, - ThreadFactory threadFactory, - RejectedExecutionHandler handler) -``` -参数介绍: +##### 同步控制 -* corePoolSize:核心线程数,定义了最小可以同时运行的线程数量 +方法级的同步:是隐式的,无须通过字节码指令来控制,它实现在方法调用和返回操作之中,虚拟机可以从方法常量池的方法表结构中的 ACC_SYNCHRONIZED 访问标志得知一个方法是否声明为同步方法 -* maximumPoolSize:最大线程数,当队列中存放的任务达到队列容量时,当前可以同时运行的数量变为最大线程数,创建线程并立即执行最新的任务,与核心线程数之间的差值又叫救急线程数 +方法内指定指令序列的同步:有 monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字的语义 -* keepAliveTime:救急线程最大存活时间,当线程池中的线程数量大于 `corePoolSize` 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等到`keepAliveTime`时间超过销毁 +* montiorenter:进入并获取对象监视器,即为栈顶对象加锁 +* monitorexit:释放并退出对象监视器,即为栈顶对象解锁 -* unit:`keepAliveTime` 参数的时间单位 + -* workQueue:阻塞队列,被提交但尚未被执行的任务 -* threadFactory:线程工厂,创建新线程时用到,可以为线程创建时起名字 -* handler:拒绝策略,线程到达最大线程数仍有新任务时会执行拒绝策略 - RejectedExecutionHandler下有4个实现类: - * AbortPolicy:让调用者抛出 RejectedExecutionException 异常,默认策略 - * CallerRunsPolicy:"调用者运行"的调节机制,将某些任务回退到调用者,从而降低新任务的流量 - * DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常 - * DiscardOldestPolicy:放弃队列中最早的任务,把当前任务加入队列中尝试再次提交当前任务 +*** + - 补充:其他框架拒绝策略 - * Dubbo:在抛出 RejectedExecutionException 异常前记录日志,并 dump 线程栈信息,方便定位问题 - * Netty:创建一个新线程来执行任务 - * ActiveMQ:带超时等待(60s)尝试放入队列 - * PinPoint:它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略 +#### 执行流程 -工作原理: +原始 Java 代码: -![](https://gitee.com/seazean/images/raw/master/Java/JUC-线程池工作原理.png) +```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); }} +``` -1. 创建线程池,这时没有创建线程(**懒惰**),等待提交过来的任务请求,调用execute方法才会创建线程 +javap -v Demo.class:省略 -2. 当调用 execute() 方法添加一个请求任务时,线程池会做如下判断: - * 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务 - * 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列 - * 如果这时队列满了且正在运行的线程数量还小于 maximumPoolSize,那么会创建非核心线程**立刻运行**这个任务(对于阻塞队列中的任务不公平) - * 如果队列满了且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会启动饱和**拒绝策略**来执行 -3. 当一个线程完成任务时,会从队列中取下一个任务来执行 +* 常量池载入运行时常量池 -4. 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断:如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉,所以线程池的所有任务完成后最终会收缩到 corePoolSize 大小 +* 方法区字节码载入方法区 +* 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 实际是在编译期间计算完成 -##### Executors + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程2.png) -Executors提供了四种线程池的创建:newCachedThreadPool、newFixedThreadPool、newSingleThreadExecutor、newScheduledThreadPool + `istore_2`:将操作数栈顶数据弹出,存入局部变量表的 slot 2 -* newFixedThreadPool:创建一个拥有 n 个线程的线程池 + `iload_1`:将局部变量表的 slot 1数据弹出,放入操作数栈栈顶 - ```java - public static ExecutorService newFixedThreadPool(int nThreads) { - return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, - new LinkedBlockingQueue()); - } - ``` + `iload_2`:将局部变量表的 slot 2数据弹出,放入操作数栈栈顶 - * 核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间 - * LinkedBlockingQueue是一个单向链表实现的阻塞队列,默认大小为`Integer.MAX_VALUE`,也就是无界队列,可以放任意数量的任务,在任务比较多的时候会导致 OOM(内存溢出) - * 适用于任务量已知,相对耗时的长期任务 + `iadd`:执行相加操作 -* newCachedThreadPool:创建一个可扩容的线程池 + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程3.png) - ```java - public static ExecutorService newCachedThreadPool() { - return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, - new SynchronousQueue()); - } - ``` + `istore_3`:将操作数栈顶数据弹出,存入局部变量表的 slot 3 - * 核心线程数是 0, 最大线程数是 Integer.MAX_VALUE,全部都是救急线程(60s 后可以回收),可能会创建大量线程,从而导致 **OOM** - * SynchronousQueue 作为阻塞队列,没有容量,对于每一个take的线程会阻塞直到有一个put的线程放入元素为止(类似一手交钱、一手交货) + `getstatic #4`:获取静态字段 - * 适合任务数比较密集,但每个任务执行时间较短的情况 + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程4.png) -* newSingleThreadExecutor:创建一个只有1个线程的单线程池 + `iload_3`: - ```java - public static ExecutorService newSingleThreadExecutor() { - return new FinalizableDelegatedExecutorService - (new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS, - new LinkedBlockingQueue())); - } - ``` + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程5.png) - * 保证所有任务按照**指定顺序执行**,线程数固定为 1,任务数多于 1 时会放入无界队列排队,任务执行完毕,这唯一的线程也不会被释放 + `invokevirtual #5`: - 对比: + * 找到常量池 #5 项 + * 定位到方法区 java/io/PrintStream.println:(I)V 方法 + * **生成新的栈帧**(分配 locals、stack等) + * 传递参数,执行新栈帧中的字节码 + * 执行完毕,弹出栈帧 + * 清除 main 操作数栈内容 - * 创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一 - 个线程,保证池的正常工作 + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程6.png) - * Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改。F...D..ExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法 + return:完成 main 方法调用,弹出 main 栈帧,程序结束 - 原因:父类不能直接调用子类中的方法,需要反射或者创建对象的方式,可以调用子类静态方法 + - * Executors.newFixedThreadPool(1) 初始时为1,可以修改。对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改 - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-newSingleThreadExecutor.png) *** -#### 开发要求 +### 执行引擎 + +#### 基本介绍 -阿里巴巴 Java 开发手册要求: +执行引擎:Java虚拟机的核心组成部分之一,JVM的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,需要执行引擎将字节码指令解释/编译为对应平台上的本地机器指令 -- **线程资源必须通过线程池提供,不允许在应用中自行显式创建线程** +虚拟机是一个相对于物理机的概念,这两种机器都有代码执行能力: - - 使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题 - - 如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题 +* 物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上 +* 虚拟机的执行引擎是由软件自行实现的,可以不受物理条件制约地定制指令集与执行引擎的结构体系 -- 线程池不允许使用Executors去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式更加明确线程池的运行规则,规避资源耗尽的风险 +Java 是**半编译半解释型语言**,将解释执行与编译执行二者结合起来进行: - Executors返回的线程池对象弊端如下: +* 解释器:根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行 +* 即时编译器(JIT : Just In Time Compiler):虚拟机运行时将源代码直接编译成**和本地机器平台相关的机器码**后再执行,并存入 Code Cache,下次遇到相同的代码直接执行,效率高 - - FixedThreadPool 和 SingleThreadPool: - - 运行的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM - - CacheThreadPool 和 ScheduledThreadPool: - - 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM -创建多大容量的线程池合适? -* 一般来说池中**总线程数是核心池线程数量两倍**,确保当核心池有线程停止时,核心池外有线程进入核心池 +*** -* 过小会导致程序不能充分地利用系统资源、容易导致饥饿 -* 过大会导致更多的线程上下文切换,占用更多内存 - 上下文切换:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态,任务从保存到再加载的过程就是一次上下文切换 -核心线程数常用公式: +#### 执行方式 -- **CPU 密集型任务(N+1):** 这种任务消耗的是 CPU 资源,可以将核心线程数设置为 N (CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程发生缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间 +HotSpot VM 采用**解释器与即时编译器并存的架构**,解释器和即时编译器能够相互协作,去选择最合适的方式来权衡编译本地代码和直接解释执行代码的时间 - CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行分析 +HostSpot JVM的默认执行方式: -- **I/O 密集型任务:** 这种系统 CPU 处于阻塞状态,用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用,因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N 或 CPU核数/ (1-阻塞系数),阻塞系数在0.8~0.9之间 +* 当程序启动后,解释器可以马上发挥作用立即执行,省去编译器编译的时间(解释器存在的**必要性**) +* 随着程序运行时间的推移,即时编译器逐渐发挥作用,根据热点探测功能,将有价值的字节码编译为本地机器指令,以换取更高的程序执行效率 - IO 密集型就是涉及到网络读取,文件读取此类任务 ,特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上 +HotSpot VM 可以通过VM参数设置程序执行方式: +- -Xint:完全采用解释器模式执行程序 +- -Xcomp:完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行 +- -Xmixed:采用解释器+即时编译器的混合模式共同执行程序 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-执行引擎工作流程.png) @@ -22261,110 +12975,87 @@ Executors提供了四种线程池的创建:newCachedThreadPool、newFixedThrea -#### 提交方法 - -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 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消 | +热点代码:被 JIT 编译器编译的字节码,根据代码被调用执行的频率而定 -execute和submit都属于线程池的方法,对比: +* 一个被多次调用的方法或者一个循环次数较多的循环体都可以被称之为热点代码 +* 这种编译方式发生在方法的执行过程中,也称为栈上替换,简称 **OSR (On StackReplacement) 编译** -* execute只能提交Runnable类型的任务,而submit既能提交Runnable类型任务也能提交Callable类型任务 +OSR 替换循环代码体的入口,C1、C2 替换的是方法调用的入口,OSR 编译后会出现方法的整段代码被编译了,但是只有循环体部分才执行编译后的机器码,其他部分仍是解释执行 -* execute会直接抛出任务执行时的异常,submit会吞掉异常,可通过Future的get方法将任务执行时的异常重新抛出 +热点探测:JIT 编译器在运行时会针热点代码做出深度优化,将其直接编译为对应平台的本地机器指令进行缓存,以提升 Java 程序的执行性能 +CodeCache 用于缓存编译后的机器码,动态生成的代码和本地方法代码 JNI,如果 CodeCache 区域被占满,编译器被停用,字节码将不会编译为机器码,应用程序继续运行,但运行速度会降低一个数量级,严重影响系统性能 +HotSpot VM 采用的热点探测方式是基于计数器的热点探测,为每一个方法都建立2个不同类型的计数器:方法调用计数器(Invocation Counter)和回边计数器(BackEdge Counter) -*** +* 方法调用计数器:用于统计方法被调用的次数,默认阈值在Client 模式 下是1500 次,在 Server 模式下是10000 次,超过这个阈值,就会触发 JIT 编译,阈值可以通过虚拟机参数 `-XX:CompileThreshold` 设置 + 工作流程:当一个方法被调用时, 会先检查该方法是否存在被 JIT 编译过的版本,存在则使用编译后的本地代码来执行;如果不存在则将此方法的调用计数器值加 1,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阈值,如果超过阈值会向即时编译器提交一个该方法的代码编译请求 +* 回边计数器:统计一个方法中循环体代码执行的次数,在字节码中控制流向后跳转的指令称为回边 -#### 关闭方法 -ExecutorService类API: -| 方法 | 说明 | -| ----------------------------------------------------- | ------------------------------------------------------------ | -| void shutdown() | 线程池状态变为 SHUTDOWN,等待任务执行完后关闭线程池,不会接收新任务,但已提交任务会执行完 | -| List shutdownNow() | 线程池状态变为 STOP,用 interrupt 中断正在执行的任务,直接关闭线程池,不会接收新任务,会将队列中的任务返回, | -| boolean isShutdown() | 不在 RUNNING 状态的线程池,此执行者已被关闭,方法返回 true | -| boolean isTerminated() | 线程池状态是否是 TERMINATED,如果所有任务在关闭后完成,返回true | -| boolean awaitTermination(long timeout, TimeUnit unit) | 调用 shutdown 后,由于调用线程不会等待所有任务运行结束,如果它想在线程池 TERMINATED 后做些事情,可以利用此方法等待 | +*** -*** +#### 分层编译 +HotSpot VM 内嵌两个 JIT 编译器,分别为 Client Compiler 和 Server Compiler,简称 C1 编译器和 C2 编译器 +C1编译器会对字节码进行简单可靠的优化,耗时短,以达到更快的编译速度,C1编译器的优化方法: -#### 处理异常 +* 方法内联:**将引用的函数代码编译到引用点处**,这样可以减少栈帧的生成,减少参数传递以及跳转过程 -execute会直接抛出任务执行时的异常,submit会吞掉异常,有两种处理方法 + 方法内联能够消除方法调用的固定开销,任何方法除非被内联,否则调用都会有固定开销,来源于保存程序在该方法中的执行位置,以及新建、压入和弹出新方法所使用的栈帧。 -方法1:主动捉异常 + ```java + private static int square(final int i) { + return i * i; + } + System.out.println(square(9)); + ``` -```java -ExecutorService executorService = Executors.newFixedThreadPool(1); -pool.submit(() -> { - try { - System.out.println("task1"); - int i = 1 / 0; - } catch (Exception e) { - e.printStackTrace(); - } -}); -``` + square 是热点方法,会进行内联,把方法内代码拷贝粘贴到调用者的位置: -方法2:使用 Future + ```java + System.out.println(9 * 9); + ``` -```java -ExecutorService executorService = Executors.newFixedThreadPool(1); -Future future = pool.submit(() -> { - System.out.println("task1"); - int i = 1 / 0; - return true; -}); -System.out.println(future.get()); -``` + 还能够进行常量折叠(constant folding)的优化: + ```java + System.out.println(81); + ``` +* 冗余消除:根据运行时状况进行代码折叠或削除 -*** +* 内联缓存:是一种加快动态绑定的优化技术 +C2 编译器进行耗时较长的优化以及激进优化,优化的代码执行效率更高,当激进优化的假设不成立时,再退回使用 C1 编译。C2 的优化主要是在全局层面,逃逸分析是优化的基础:标量替换、栈上分配、同步消除 +VM 参数设置: -#### 状态信息 +- -client:指定 Java 虚拟机运行在 Client 模式下,并使用C1编译器 +- -server:指定 Java 虚拟机运行在 Server 模式下,并使用C2编译器 +- `-server -XX:+TieredCompilation`:在1.8之前,分层编译默认是关闭的,可以添加该参数开启 -ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量 +分层编译策略 (Tiered Compilation):程序解释执行可以触发 C1 编译,将字节码编译成机器码,加上性能监控,C2 编译会根据性能监控信息进行激进优化,JVM 将执行状态分成了 5 个层次: -| 状态 | 高3位 | 接收新任务 | 处理阻塞任务队列 | 说明 | -| ---------- | ----- | ---------- | ---------------- | --------------------------------------- | -| RUNNING | 111 | Y | Y | | -| SHUTDOWN | 000 | N | Y | 不接收新任务,但处理阻塞队列剩余任务 | -| STOP | 001 | N | N | 中断正在执行的任务,并抛弃阻塞队列任务 | -| TIDYING | 010 | - | - | 任务全执行完毕,活动线程为0即将进入终结 | -| TERMINATED | 011 | - | - | 终止状态 | +* 0 层,解释执行(Interpreter) -这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 cas 原子操作 -进行赋值 +* 1 层,使用 C1 即时编译器编译执行(不带 profiling) -```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; } -``` +* 2 层,使用 C1 即时编译器编译执行(带基本的 profiling) -![](https://gitee.com/seazean/images/raw/master/Java/JUC-线程池状态转换图.png) +* 3 层,使用 C1 即时编译器编译执行(带完全的 profiling) +* 4 层,使用 C2 即时编译器编译执行(C1 和 C2 协作运行) + 说明:profiling 是指在运行过程中收集一些程序执行状态的数据,例如方法的调用次数,循环的回边次数等 @@ -22372,33 +13063,24 @@ private static int ctlOf(int rs, int wc) { return rs | wc; } -### 任务调度 +### 方法调用 + +#### 方法识别 + +Java 虚拟机识别方法的关键在于类名、方法名以及方法描述符(method descriptor) -#### Timer +* 方法描述符是由方法的参数类型以及返回类型所构成,也叫方法特征签名 +* 在同一个类中,如果同时出现多个名字相同且描述符也相同的方法,那么 Java 虚拟机会在类的验证阶段报错 -Timer 实现定时功能,Timer 的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务 +JVM 根据名字和描述符来判断的,只要返回值不一样,其它完全一样,在 JVM 中是允许的,但 Java 语言不允许 ```java -private static void method1() { - Timer timer = new Timer(); - TimerTask task1 = new TimerTask() { - @Override - public void run() { - System.out.println("task 1"); - //int i = 1 / 0;//任务一的出错会导致任务二无法执行 - Thread.sleep(2000); - } - }; - TimerTask task2 = new TimerTask() { - @Override - public void run() { - System.out.println("task 2"); - } - }; - // 使用 timer 添加两个任务,希望它们都在 1s 后执行 - // 但由于 timer 内只有一个线程来顺序执行队列中的任务,因此任务1的延时,影响了任务2的执行 - timer.schedule(task1,1000);//17:45:56 c.ThreadPool [Timer-0] - task 1 - timer.schedule(task2,1000);//17:45:58 c.ThreadPool [Timer-0] - task 2 +// 返回值类型不同,编译阶段直接报错 +public static Integer invoke(Object... args) { + return 1; +} +public static int invoke(Object... args) { + return 2; } ``` @@ -22408,81 +13090,39 @@ private static void method1() { -#### Scheduled +#### 调用机制 -任务调度线程池 ScheduledThreadPoolExecutor 继承 ThreadPoolExecutor: +方法调用并不等于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本,不是方法的具体运行过程 -构造方法:`Executors.newScheduledThreadPool(int corePoolSize)` +在 JVM 中,将符号引用转换为直接引用有两种机制: -```java -public ScheduledThreadPoolExecutor(int corePoolSize) { - super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, - new DelayedWorkQueue()); -} -``` +- 静态链接:当一个字节码文件被装载进 JVM 内部时,如果被调用的目标方法在编译期可知,且运行期保持不变,将调用方法的符号引用转换为直接引用的过程称之为静态链接(类加载的解析阶段) +- 动态链接:如果被调用的方法在编译期无法被确定下来,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此被称为动态链接(初始化后的解析阶段) -常用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)`:定时执行任务,参数为初始延迟时间、间隔时间、单位 +- 静态绑定:被调用的目标方法在编译期可知,且运行期保持不变,将这个方法与所属的类型进行绑定 +- 动态绑定:被调用的目标方法在编译期无法确定,只能在程序运行期根据实际的类型绑定相关的方法 -基本使用: +* Java 编译器已经区分了重载的方法(静态绑定和动态绑定),因此可以认为虚拟机中不存在重载 -* 延迟任务,但是出现异常并不会在控制台打印,也不会影响其他线程的执行 +非虚方法: - ```java - public static void main(String[] args){ - // 线程池大小为1时也是串行执行 - ScheduledExecutorService executor = Executors.newScheduledThreadPool(2); - // 添加两个任务,都在 1s 后同时执行 - executor.schedule(() -> { - System.out.println("任务1,执行时间:" + new Date()); - //int i = 1 / 0; - try { Thread.sleep(2000); } catch (InterruptedException e) { } - }, 1000, TimeUnit.MILLISECONDS); - - executor.schedule(() -> { - System.out.println("任务2,执行时间:" + new Date()); - }, 1000, TimeUnit.MILLISECONDS); - } - ``` +- 非虚方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的 +- 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法 +- 所有普通成员方法、实例方法、被重写的方法都是虚方法 -* 定时任务 scheduleAtFixedRate:**一个任务的启动到下一个任务的启动**之间只要大于间隔时间,抢占到CPU就会立即执行 +动态类型语言和静态类型语言: - ```java - public static void main(String[] args) { - ScheduledExecutorService pool = Executors.newScheduledThreadPool(1); - System.out.println("start..." + new Date()); - - pool.scheduleAtFixedRate(() -> { - System.out.println("running..." + new Date()); - Thread.sleep(2000); - }, 1, 1, TimeUnit.SECONDS); - } - - /*start...Sat Apr 24 18:08:12 CST 2021 - running...Sat Apr 24 18:08:13 CST 2021 - running...Sat Apr 24 18:08:15 CST 2021 - running...Sat Apr 24 18:08:17 CST 2021 - ``` +- 在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之则是动态类型语言 + +- 静态语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息 -* 定时任务 scheduleWithFixedDelay:**一个任务的结束到下一个任务的启动之间**等于间隔时间,抢占到CPU就会立即执行,这个方法才是真正的设置两个任务之间的间隔 +- **Java是静态类型语言**(尽管lambda表达式为其增加了动态特性),js,python是动态类型语言 ```java - public static void main(String[] args){ - ScheduledExecutorService pool = Executors.newScheduledThreadPool(3); - System.out.println("start..." + new Date()); - - pool.scheduleWithFixedDelay(() -> { - System.out.println("running..." + new Date()); - Thread.sleep(2000); - }, 1, 1, TimeUnit.SECONDS); - } - /*start...Sat Apr 24 18:11:41 CST 2021 - running...Sat Apr 24 18:11:42 CST 2021 - running...Sat Apr 24 18:11:45 CST 2021 - running...Sat Apr 24 18:11:48 CST 2021 + String s = "abc"; //Java + info = "abc"; //Python ``` @@ -22491,227 +13131,124 @@ 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); - } -} -``` +普通调用指令: +- invokestatic:调用静态方法 +- invokespecial:调用私有实例方法、构造器,和父类的实例方法或构造器,以及所实现接口的默认方法 +- invokevirtual:调用所有虚方法(虚方法分派) +- invokeinterface:调用接口方法 +动态调用指令: -*** +- invokedynamic:动态解析出需要调用的方法, + - Java7 为了实现动态类型语言支持而引入了该指令,但是并没有提供直接生成 invokedynamic 指令的方法,需要借助 ASM 这种底层字节码工具来产生 invokedynamic 指令 + - Java8 的 lambda 表达式的出现,invokedynamic 指令在 Java 中才有了直接生成方式 +指令对比: +- 普通调用指令固化在虚拟机内部,方法的调用执行不可干预,根据方法的符号引用链接到具体的目标方法 +- 动态调用指令支持用户确定方法 +- invokestatic 和 invokespecial 指令调用的方法称为非虚方法,虚拟机能够直接识别具体的目标方法 +- invokevirtual 和 invokeinterface 指令调用的方法称为虚方法,虚拟机需要在执行过程中根据调用者的动态类型来确定目标方法 -### ForkJoin +指令说明: -Fork/Join:线程池的实现,体现是分治思想,适用于能够进行任务拆分的 cpu 密集型运算,用于**并行计算** +- 如果虚拟机能够确定目标方法有且仅有一个,比如说目标方法被标记为 final,那么可以不通过动态类型,直接确定目标方法 +- 普通成员方法是由 invokevirtual 调用,属于**动态绑定**,即支持多态 -任务拆分:是将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的一些计算,如归并排序、斐波那契数列都可以用分治思想进行求解 -* Fork/Join 在分治的基础上加入了多线程,把每个任务的分解和合并交给不同的线程来完成,提升了运算效率 -* ForkJoin 使用 ForkJoinPool 来启动,是一个特殊的线程池,默认会创建与 cpu 核心数大小相同的线程池 -* 任务有返回值继承 RecursiveTask,没有返回值继承 RecursiveAction +*** -```java -public static void main(String[] args) { - ForkJoinPool pool = new ForkJoinPool(4); - System.out.println(pool.invoke(new MyTask(5))); - //拆分 5 + MyTask(4) --> 4 + MyTask(3) --> -} -// 1~ n 之间整数的和 -class MyTask extends RecursiveTask { - private int n; - public MyTask(int n) { - this.n = n; - } +##### 符号引用 - @Override - public String toString() { - return "MyTask{" + "n=" + n + '}'; - } +在编译过程中,虚拟机并不知道目标方法的具体内存地址,Java 编译器会暂时用符号引用来表示该目标方法,这一符号引用包括目标方法所在的类或接口的名字,以及目标方法的方法名和方法描述符 - @Override - protected Integer compute() { - // 如果 n 已经为 1,可以求得结果了 - if (n == 1) { - return n; - } - // 将任务进行拆分(fork) - MyTask t1 = new MyTask(n - 1); - t1.fork(); - // 合并(join)结果 - int result = n + t1.join(); - return result; - } -} -``` +* 对于静态绑定的方法调用而言,实际引用是一个指向方法的指针 +* 对于需要动态绑定的方法调用而言,实际引用则是一个方法表的索引 -继续拆分优化: +符号引用存储在方法区常量池中,根据目标方法是否为接口方法,分为接口符号引用和非接口符号引用: ```java -class AddTask extends RecursiveTask { - int begin; - int end; - public AddTask(int begin, int end) { - this.begin = begin; - this.end = end; - } - - @Override - public String toString() { - return "{" + begin + "," + end + '}'; - } - - @Override - protected Integer compute() { - // 5, 5 - if (begin == end) { - return begin; - } - // 4, 5 防止多余的拆分 提高效率 - if (end - begin == 1) { - return end + begin; - } - // 1 5 - int mid = (end + begin) / 2; // 3 - AddTask t1 = new AddTask(begin, mid); // 1,3 - t1.fork(); - AddTask t2 = new AddTask(mid + 1, end); // 4,5 - t2.fork(); - int result = t1.join() + t2.join(); - return result; - } -} +Constant pool: +... + #16 = InterfaceMethodref #27.#29 // 接口 +... + #22 = Methodref #1.#33 // 非接口 +... ``` -ForkJoinPool 实现了**工作窃取算法**来提高 CPU 的利用率: - -* 每个线程都维护了一个双端队列,用来存储需要执行的任务 -* 工作窃取算法允许空闲的线程从其它线程的双端队列中窃取一个任务来执行 -* 窃取的必须是**最晚的任务**,避免和队列所属线程发生竞争,但是队列中只有一个任务时还是会发生竞争 - +对于非接口符号引用,假定该符号引用所指向的类为 C,则 Java 虚拟机会按照如下步骤进行查找: +1. 在 C 中查找符合名字及描述符的方法 +2. 如果没有找到,在 C 的父类中继续搜索,直至 Object 类 +3. 如果没有找到,在 C 所直接实现或间接实现的接口中搜索,这一步搜索得到的目标方法必须是非私有、非静态的。如果有多个符合条件的目标方法,则任意返回其中一个 -*** +对于接口符号引用,假定该符号引用所指向的接口为 I,则 Java 虚拟机会按照如下步骤进行查找: +1. 在 I 中查找符合名字及描述符的方法 +2. 如果没有找到,在 Object 类中的公有实例方法中搜索 +3. 如果没有找到,则在 I 的超接口中搜索,这一步的搜索结果的要求与非接口符号引用步骤 3 的要求一致 -### 享元模式 -享元模式 (Flyweight pattern): 用于减少创建对象的数量,以减少内存占用和提高性能,这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结构的方式 +*** -异步模式:让有限的工作线程(Worker Thread)来轮流异步处理无限多的任务,也可将其归类为分工模式,典型实现就是线程池 -工作机制:享元模式尝试重用现有的同类对象,如果未找到匹配的对象,则创建新对象 -自定义连接池: +##### 执行流程 ```java -public static void main(String[] args) { - Pool pool = new Pool(2); - for (int i = 0; i < 5; i++) { - new Thread(() -> { - Connection con = pool.borrow(); - try { - Thread.sleep(new Random().nextInt(1000)); - } catch (InterruptedException e) { - e.printStackTrace(); - } - pool.free(con); - }).start(); - } -} -class Pool { - //连接池的大小 - private final int poolSize; - //连接对象的数组 - private Connection[] connections; - //连接状态数组 0表示空闲 1表示繁忙 - private AtomicIntegerArray states; //int[] -> AtomicIntegerArray - - //构造方法 - public Pool(int poolSize) { - this.poolSize = poolSize; - this.connections = new Connection[poolSize]; - this.states = new AtomicIntegerArray(new int[poolSize]); - for (int i = 0; i < poolSize; i++) { - connections[i] = new MockConnection("连接" + (i + 1)); - } - } +public class Demo { + public Demo() { } + private void test1() { } + private final void test2() { } - //使用连接 - public Connection borrow() { - while (true) { - for (int i = 0; i < poolSize; i++) { - if (states.get(i) == 0) { - if (states.compareAndSet(i, 0, 1)) { - System.out.println(Thread.currentThread().getName() + " borrow " + connections[i]); - return connections[i]; - } - } - } - //如果没有空闲连接,当前线程等待 - synchronized (this) { - try { - System.out.println(Thread.currentThread().getName() + " wait..."); - this.wait(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - } - } + public void test3() { } + public static void test4() { } - //归还连接 - public void free(Connection con) { - for (int i = 0; i < poolSize; i++) { - if (connections[i] == con) {//判断是否是同一个对象 - states.set(i, 0);//不用cas的原因是只会有一个线程使用该连接 - synchronized (this) { - System.out.println(Thread.currentThread().getName() + " free " + con); - this.notifyAll(); - } - break; - } - } + public static void main(String[] args) { + Demo3_9 d = new Demo3_9(); + d.test1(); + d.test2(); + d.test3(); + d.test4(); + Demo.test4(); } - } +``` -class MockConnection implements Connection { - private String name; - //..... -} +几种不同的方法调用对应的字节码指令: + +```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 +- `d.test4()` 是通过**对象引用**调用一个静态方法,在调用 invokestatic 之前执行了 pop 指令,把对象引用从操作数栈弹掉 + - 不建议使用 `对象.静态方法()` 的方式调用静态方法,多了aload 和 pop 指令 + - 成员方法与静态方法调用的区别是:执行方法前是否需要对象引用 @@ -22719,32 +13256,29 @@ class MockConnection implements Connection { +#### 多态原理 +##### 执行原理 -## 同步器 - -### AQS +Java 虚拟机中关于方法重写的判定基于方法描述符,如果子类定义了与父类中非私有、非静态方法同名的方法,只有当这两个方法的参数类型以及返回类型一致,Java 虚拟机才会判定为重写。 -#### 思想 +**理解多态**: -AQS:AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架,许多同步类实现都依赖于它 +- 多态有编译时多态和运行时多态,即静态绑定和动态绑定 +- 前者是通过方法重载实现,后者是通过重写实现(子类覆盖父类方法,虚方法表) +- 虚方法:运行时动态绑定的方法,对比静态绑定的非虚方法调用来说,虚方法调用更加耗时 -* 用 state 属性来表示资源的状态(分**独占模式和共享模式**),子类需要定义如何维护这个状态,控制如何获取 - 锁和释放锁 - * 独占模式是只有一个线程能够访问资源,如ReentrantLock - * 共享模式允许多个线程访问资源,如Semaphore,ReentrantReadWriteLock是组合式 -* 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList(**同步队列:双向,便于出队入队**) -* 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet(**条件队列:单向**) +方法重写的本质: -AQS 核心思想: +1. 找到操作数栈的第一个元素所执行的对象的实际类型,记作 C -* 如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置锁定状态 +2. 如果在类型 C 中找到与常量中的描述符和名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回 java.lang.IllegalAccessError 异常 -* 被请求的共享资源被占用,AQS 用 CLH 队列实现线程阻塞等待以及被唤醒时锁分配的(park)机制,即将暂时获取不到锁的线程加入到队列中 + IllegalAccessError:表示程序试图访问或修改一个属性或调用一个方法,这个属性或方法没有权限访问,一般会引起编译器异常。如果这个错误发生在运行时,就说明一个类发生了不兼容的改变 - CLH是一种基于单向链表的**高性能、公平的自旋锁**,AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配 +3. 找不到,就会按照继承关系从下往上依次对 C 的各个父类进行第二步的搜索和验证过程 - +4. 如果始终没有找到合适的方法,则抛出 java.lang.AbstractMethodError 异常 @@ -22752,119 +13286,69 @@ AQS 核心思想: -#### 原理 +##### 虚方法表 + +在虚拟机工作过程中会频繁使用到动态分配,每次动态分配的过程中都要重新在类的元数据中搜索合适目标,影响到执行效率。为了提高性能,JVM 采取了一种用**空间换取时间**的策略来实现动态绑定,在类的方法区建立一个虚方法表(virtual method table),实现使用索引表来代替查找,可以快速定位目标方法 -设计思想: +* invokevirtual 所使用的虚方法表(virtual method table,vtable),执行流程 + 1. 先通过栈帧中的对象引用找到对象,分析对象头,找到对象的实际 Class + 2. Class 结构中有 vtable,查表得到方法的具体地址,执行方法的字节码 +* invokeinterface 所使用的接口方法表(interface method table,itable) -* 获取锁: +虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,jvm会把该类的方法表也初始化完毕 - ```java - while(state 状态不允许获取) {//tryAcquire(arg) - if(队列中还没有此线程) { - 入队并阻塞 park unpark - } - } - 当前线程出队 - ``` +虚方法表的执行过程: -* 释放锁: +* 对于静态绑定的方法调用而言,实际引用将指向具体的目标方法 +* 对于动态绑定的方法调用而言,实际引用则是方法表的索引值,也就是方法的间接地址。Java 虚拟机获取调用者的实际类型,并在该实际类型的虚方法表中,根据索引值获得目标方法内存偏移量(指针) - ```java - if(state 状态允许了) {//tryRelease(arg) - 恢复阻塞的线程(s) - } - ``` +为了优化对象调用方法的速度,方法区的类型信息会增加一个指针,该指针指向一个记录该类方法的方法表。每个类中都有一个虚方法表,本质上是一个数组,每个数组元素指向一个当前类及其祖先类中非私有的实例方法 -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 +* 其一,子类方法表中包含父类方法表中的所有方法 +* 其二,子类方法在方法表中的索引值,与它所重写的父类方法的索引值相同 -waitstate设计: + -* 使用**volatile 修饰配合 cas**保证其修改时的原子性 +Passenger 类的方法表包括两个方法,分别对应 0 号和 1 号。方法表调换了 toString 方法和 passThroughImmigration 方法的位置,是因为 toString 方法的索引值需要与 Object 类中同名方法的索引值一致,为了保持简洁,这里不考虑 Object 类中的其他方法。 -* 表示Node节点的状态,有以下几种状态: +虚方法表对性能的影响: - ```java - //由于超时或中断,此节点被取消,不会再改变状态 - static final int CANCELLED = 1; - //此节点后面的节点已(或即将)被阻止(通过park),当前节点在释放或取消时必须唤醒后面的节点 - static final int SIGNAL = -1; - //此节点当前在条件队列中 - static final int CONDITION = -2; - //将releaseShared传播到其他节点 - static final int PROPAGATE = -3; - ``` +* 使用了方法表的动态绑定与静态绑定相比,仅仅多出几个内存解引用操作:访问栈上的调用者、读取调用者的动态类型、读取该类型的方法表、读取方法表中某个索引值所对应的目标方法,但是相对于创建并初始化 Java 栈帧这操作的开销可以忽略不计 +* 上述优化的效果看上去不错,但实际上**仅存在于解释执行**中,或者即时编译代码的最坏情况。因为即时编译还拥有另外两种性能更好的优化手段:内联缓存(inlining cache)和方法内联(method inlining) -阻塞恢复设计: + -* 使用 park & unpark 来实现线程的暂停和恢复,因为命令的先后顺序不影响结果 -* park & unpark 是针对线程的,而不是针对同步器的,因此控制粒度更为精细 -* park 线程可以通过 interrupt 打断 -队列设计: -* 使用了 FIFO 先入先出队列,并不支持优先级队列 +*** -* 设计时借鉴了 CLH 队列,CLH是一种单向无锁队列 - - ```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)) { - t.next = node; - return t; - } - } - } - } - ``` +##### 内联缓存 - +内联缓存:是一种加快动态绑定的优化技术,它能够缓存虚方法调用中**调用者的动态类型以及该类型所对应的目标方法**。在之后的执行过程中,如果碰到已缓存的类型,内联缓存便会直接调用该类型所对应的目标方法;如果没有碰到已缓存的类型,内联缓存则会退化至使用基于方法表的动态绑定 -*** +多态的三个术语: +* 单态 (monomorphic):指的是仅有一种状态的情况 +* 多态 (polymorphic):指的是有限数量种状态的情况,二态(bimorphic)是多态的其中一种 +* 超多态 (megamorphic):指的是更多种状态的情况,通常用一个具体数值来区分多态和超多态,在这个数值之下,称之为多态,否则称之为超多态 +对于内联缓存来说,有对应的单态内联缓存、多态内联缓存: -#### 模板 +* 单态内联缓存:只缓存了一种动态类型以及所对应的目标方法,实现简单,比较所缓存的动态类型,如果命中,则直接调用对应的目标方法。 +* 多态内联缓存:缓存了多个动态类型及其目标方法,需要逐个将所缓存的动态类型与当前动态类型进行比较,如果命中,则调用对应的目标方法 -同步器的设计是基于模板方法模式,该模式是基于”继承“的,主要是为了在不改变模板结构的前提下在子类中重新定义模板中的内容以实现复用代码 +为了节省内存空间,**Java 虚拟机只采用单态内联缓存**,没有命中的处理方法: -* 使用者继承 `AbstractQueuedSynchronizer` 并重写指定的方法 -* 将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,这些模板方法会调用使用者重写的方法 +* 替换单态内联缓存中的纪录,类似于 CPU 中的数据缓存,对数据的局部性有要求,即在替换内联缓存之后的一段时间内,方法调用的调用者的动态类型应当保持一致,从而能够有效地利用内联缓存 +* 劣化为超多态状态,这也是 Java 虚拟机的具体实现方式,这种状态实际上放弃了优化的机会,将直接访问方法表来动态绑定目标方法,但是与替换内联缓存纪录相比节省了写缓存的额外开销 -AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的模板方法: +虽然内联缓存附带内联二字,但是并没有内联目标方法 -```java -isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。 -tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。 -tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。 -tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 -tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。 -``` -* 默认情况下,每个方法都抛出 `UnsupportedOperationException` -* 这些方法的实现必须是内部线程安全的 -* AQS 类中的其他方法都是 final ,所以无法被其他类使用,只有这几个方法可以被其他类使用 @@ -22872,1119 +13356,710 @@ tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true -#### 自定义 -```java -// 自定义锁(不可重入锁) -class MyLock implements Lock { - //独占锁 不可重入 - class MySync extends AbstractQueuedSynchronizer { - @Override - protected boolean tryAcquire(int arg) { - if (compareAndSetState(0, 1)) { - // 加上锁 设置 owner 为当前线程 - setExclusiveOwnerThread(Thread.currentThread()); - return true; - } - return false; - } - @Override //解锁 - protected boolean tryRelease(int arg) { - setExclusiveOwnerThread(null); - setState(0);//volatile 修饰的变量放在后面,防止指令重排 - return true; - } - @Override //是否持有独占锁 - protected boolean isHeldExclusively() { - return getState() == 1; - } - public Condition newCondition() { - return new ConditionObject(); - } - } - private MySync sync = new MySync(); - @Override //加锁(不成功进入等待队列等待) - public void lock() { - sync.acquire(1); - } - @Override //加锁 可打断 - public void lockInterruptibly() throws InterruptedException { - sync.acquireInterruptibly(1); - } +*** + + + +### 代码优化 - @Override //尝试加锁,尝试一次 - public boolean tryLock() { - return sync.tryAcquire(1); - } +#### 语法糖 - @Override //尝试加锁,带超时 - public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { - return sync.tryAcquireNanos(1, unit.toNanos(time)); - } - - @Override //解锁 - public void unlock() { - sync.release(1); - } - - @Override //条件变量 - public Condition newCondition() { - return sync.newCondition(); - } -} -``` +语法糖:指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担 +#### 构造器 +```java +public class Candy1 { +} +``` -*** +```java +public class Candy1 { + // 这个无参构造是编译器帮助我们加上的 + public Candy1() { + super(); // 即调用父类 Object 的无参构造方法,即调用 java/lang/Object." + ":()V + } +} +``` -### Re-Lock +*** -#### 锁对比 -ReentrantLock相对于 synchronized 它具备如下特点: -1. 锁的实现:synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的 -2. 性能:新版本 Java 对 synchronized 进行了很多优化,synchronized 与 ReentrantLock 大致相同 -3. 使用:ReentrantLock 需要手动解锁,synchronized 执行完代码块自动解锁 -4. 可中断:ReentrantLock 可中断,而 synchronized 不行 -5. **公平锁**:公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁 - * ReentrantLock 可以设置公平锁,synchronized 中的锁是非公平的 -6. 锁超时:尝试获取锁,超时获取不到直接放弃,不进入阻塞队列 - * ReentrantLock 可以设置超时时间,synchronized会一直等待 -7. 锁绑定多个条件:一个 ReentrantLock 可以同时绑定多个 Condition 对象 -8. 两者都是可重入锁 +#### 拆装箱 +```java +Integer x = 1; +int y = x; +``` +这段代码在 JDK 5 之前是无法编译通过的,必须改写为代码片段2: -*** +```java +Integer x = Integer.valueOf(1); +int y = x.intValue(); +``` +JDK5 以后编译阶段自动转换成上述片段 -#### 使用锁 -构造方法:`ReentrantLock lock = new ReentrantLock();` +*** -ReentrantLock类API: -* `public void lock()`:获得锁 - * 如果锁没有被另一个线程占用,则将锁定计数设置为1。 - * 如果当前线程已经保持锁定,则保持计数增加1 +#### 泛型擦除 - * 如果锁被另一个线程保持,则当前线程被禁用线程调度,并且在锁定已被获取之前处于休眠状态 +泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行**泛型擦除**的动作,即泛型信息 +在编译为字节码之后就丢失了,实际的类型都**当做了 Object 类型**来处理: -* `public void unlock()`:尝试释放锁 - * 如果当前线程是该锁的持有者,则保持计数递减 - * 如果保持计数现在为零,则锁定被释放 - * 如果当前线程不是该锁的持有者,则抛出异常 +```java +List list = new ArrayList<>(); +list.add(10); // 实际调用的是 List.add(Object e) +Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index); +``` -基本语法: +编译器真正生成的字节码中,还要额外做一个类型转换的操作: ```java -// 获取锁 -reentrantLock.lock(); -try { - // 临界区 -} finally { - // 释放锁 - reentrantLock.unlock(); -} +// 需要将 Object 转为 Integer +Integer x = (Integer)list.get(0); ``` +如果前面的 x 变量类型修改为 int 基本类型那么最终生成的字节码是: +```java +// 需要将 Object 转为 Integer, 并执行拆箱操作 +int x = ((Integer)list.get(0)).intValue(); +``` -*** +*** -#### 公平锁 -##### 基本使用 -构造方法:`ReentrantLock lock = new ReentrantLock(true)` +#### 可变参数 ```java -public ReentrantLock(boolean fair) { - sync = fair ? new FairSync() : new NonfairSync(); +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"); + } } ``` -ReentrantLock 默认是不公平的: +可变参数`String... args`其实是`String[] args` , java 编译器会在编译期间将上述代码变换为: ```java -public ReentrantLock() { - sync = new NonfairSync(); +public static void main(String[] args) { + foo(new String[]{"hello", "world"}); } ``` -说明:公平锁一般没有必要,会降低并发度 +注意:如果调用了foo()则等价代码为`foo(new String[]{})` ,创建了一个空的数组,而不会传递 null 进去 -*** +**** + +#### foreach + +**数组的循环:** -##### 非公原理 +```java +int[] array = {1, 2, 3, 4, 5}; // 数组赋初值的简化写法也是语法糖哦 +for (int e : array) { + System.out.println(e); +} +``` -###### 加锁 +编译后为循环取数: -NonfairSync 继承自 AQS +```java +for(int i = 0; i < array.length; ++i) { + int e = array[i]; + System.out.println(e); +} +``` -没有竞争:ExclusiveOwnerThread属于 Thread-0,state设置为1 +**集合的循环:** ```java -final void lock() { - //首先用 cas 尝试(仅尝试一次)将 state 从 0 改为 1, 如果成功表示获得了独占锁 - if (compareAndSetState(0, 1)) - setExclusiveOwnerThread(Thread.currentThread()); - else - acquire(1);//失败进入 +List list = Arrays.asList(1,2,3,4,5); +for (Integer i : list) { + System.out.println(i); } ``` -第一个竞争出现: +编译后转换为对迭代器的调用: ```java -public final void acquire(int arg) { - // 当 tryAcquire 返回为 false 时, 先调用addWaiter, 接着 acquireQueued - if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); +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); } ``` - +注意:foreach 循环写法,能够配合数组以及所有实现了 Iterable 接口的集合类一起使用,其中 Iterable 用来获取集合的迭代器( Iterator ) -Thread-1执行: -* CAS 尝试将 state 由 0 改为 1,结果失败(第一次) -* 进入 tryAcquire 逻辑,这时 state 已经是1,结果仍然失败(第二次) +*** - ```java - 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; - } - } - // 如果已经获得了锁, 线程还是当前线程, 表示发生了锁重入 - 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;//获取失败 - } - ``` -* 接下来进入 addWaiter 逻辑,构造 Node 队列 - * 图中黄色三角表示该 Node 的 waitStatus 状态,其中 0 为默认正常状态 - * Node 的创建是懒惰的 - * 其中第一个 Node 称为 Dummy(哑元)或哨兵,用来占位,并不关联线程 +#### switch - ```java - private Node addWaiter(Node mode) { - // 将当前线程关联到一个 Node 对象上, 模式为独占模式 - 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);//添加到尾节点 - return node; - } - ``` +##### 字符串 - +从 JDK 开始,switch 可以作用于字符串和枚举类: -* 线程进入 acquireQueued 逻辑 +```java +switch (str) { + case "hello": { + System.out.println("h"); + break; + } + case "world": { + System.out.println("w"); + break; + } +} +``` - * acquireQueued 会在一个死循环中不断尝试获得锁,失败后进入 park 阻塞 +注意:switch 配合 String 和枚举使用时,变量不能为null - * 如果当前线程是在head节点后,会再次 tryAcquire 尝试获取锁,state 仍为 1 则失败(第三次) +会被编译器转换为: - ```java - final boolean acquireQueued(final Node node, int arg) { - boolean failed = true; - try { - boolean interrupted = false; - for (;;) { - final Node p = node.predecessor(); - // 上一个节点是 head, 表示轮到自己获取锁 - if (p == head && tryAcquire(arg)) { - // 获取成功, 设置自己(当前线程对应的 node)为 head - setHead(node); - p.next = null; // help GC - failed = false; - return interrupted; - } - // 判断是否应当 park,返回false后需要新一轮的循环 - if (shouldParkAfterFailedAcquire(p, node) && - parkAndCheckInterrupt()) - interrupted = true; - } - } finally { - if (failed) - cancelAcquire(node); - } - } - ``` +```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; +} +``` - * 进入 shouldParkAfterFailedAcquire 逻辑,将前驱 node,即 head 的 waitStatus 改为 -1,返回 false;waitStatus 为 -1 的节点用来唤醒下一个节点 +总结: - ```java - private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { - int ws = pred.waitStatus; - if (ws == Node.SIGNAL) - // 上一个节点都在阻塞, 那么当前线程也阻塞 - return true; - if (ws > 0) { - // 上一个节点取消, 那么重构删除前面所有取消的节点, 返回到外层循环重试 - do { - node.prev = pred = pred.prev; - } while (pred.waitStatus > 0); - pred.next = node; - } else { - // 设置上一个节点状态为 Node.SIGNAL,返回外层循环重试 - compareAndSetWaitStatus(pred, ws, Node.SIGNAL); - } - return false; - } - ``` +* 执行了两遍 switch,第一遍是根据字符串的 hashCode 和 equals 将字符串的转换为相应 byte 类型,第二遍才是利用 byte 执行进行比较 +* hashCode 是为了提高效率,减少可能的比较;而 equals 是为了防止 hashCode 冲突 - * 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) - ```java - private final boolean parkAndCheckInterrupt() { - // 阻塞当前线程,如果打断标记已经是 true, 则 park 会失效 - LockSupport.park(this); - // 判断当前线程是否被打断,清除打断标记 - return Thread.interrupted(); - } - ``` +*** - +##### 枚举 -###### 解锁 +switch 枚举的例子,原始代码: ```java -public void unlock() { - sync.release(1); +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; + } + } } ``` -Thread-0 释放锁,进入 release 流程 - -* 进入 tryRelease +编译转换后的代码: - * 设置exclusiveOwnerThread 为 null - * state = 0 +```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; + } +} +``` -* 当前队列不为 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; - } - 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); - return free; - } - ``` +*** -* 进入unparkSuccessor 方法 - * 找到队列中离 head 最近的一个 Node(没取消的),unpark 恢复其运行,本例中即为 Thread-1 - * 回到 Thread-1 的 acquireQueued 流程 - ```java - private void unparkSuccessor(Node node) { - int ws = node.waitStatus; - if (ws < 0) - // 尝试重置状态为 0 - compareAndSetWaitStatus(node, ws, 0); - // 找到需要 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) - LockSupport.unpark(s.thread); - } - ``` +#### 枚举类 -* 如果加锁成功(没有竞争),会设置 +JDK 7 新增了枚举类: - * exclusiveOwnerThread 为 Thread-1,state = 1 - * head 指向刚刚 Thread-1 所在的 Node,该 Node 清空 Thread - * 原本的 head 因为从链表断开,而可被垃圾回收 +```java +enum Sex { + MALE, FEMALE +} +``` - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ReentrantLock-非公平锁4.png) +编译转换后: -* 如果这时候有其它线程来竞争(非公平),例如这时有 Thread-4 来了并抢占了锁 +```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); + } +} +``` - * Thread-4 被设置为 exclusiveOwnerThread,state = 1 - * Thread-1 再次进入 acquireQueued 流程,获取锁失败,重新进入 park 阻塞 - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ReentrantLock-非公平锁5.png) +#### 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(); +} +``` -##### 公平原理 +转换成: -与非公平锁主要区别在于 tryAcquire 方法:先检查 AQS 队列中是否有前驱节点,没有才去CAS竞争 +`addSuppressed(Throwable e)`:添加被压制异常,是为了防止异常信息的丢失(try-with-resources 生成的 fianlly 中如果抛出了异常) ```java -static final class FairSync extends Sync { - private static final long serialVersionUID = -3000897897090466540L; - final void lock() { - acquire(1); - } - - protected final boolean tryAcquire(int acquires) { - final Thread current = Thread.currentThread(); - int c = getState(); - if (c == 0) { - // 先检查 AQS 队列中是否有前驱节点, 没有(false)才去竞争 - if (!hasQueuedPredecessors() && - compareAndSetState(0, acquires)) { - setExclusiveOwnerThread(current); - return true; +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(); } - } - // 锁重入 - return false; - } + } + } +} catch (IOException e) { + e.printStackTrace(); } ``` -```java -public final boolean hasQueuedPredecessors() { - Node t = tail; - Node h = head; - Node s; - //头尾指向一个节点,列表为空,返回false, - return h != t && - // 头尾之间有节点,判断头节点的下一个是不是空 - // 不是空进入最后的判断,第二个节点的线程是否是本线程 - ((s = h.next) == null || s.thread != Thread.currentThread());} -``` - *** -#### 可重入 +#### 方法重写 -可重入是指同一个线程如果首次获得了这把锁,那么它是这把锁的拥有者,因此有权利再次获取这把锁,如果不可重入锁,那么第二次获得锁时,自己也会被锁挡住 +方法重写时对返回值分两种情况: -源码解析参考:`nonfairTryAcquire(int acquires)) `和 `tryRelease(int releases)` +* 父子类的返回值完全一致 +* 子类返回值可以是父类返回值的子类 ```java -static ReentrantLock lock = new ReentrantLock(); -public static void main(String[] args) { - method1(); -} -public static void method1() { - lock.lock(); - try { - System.out.println(Thread.currentThread().getName() + " execute method1"); - method2(); - } finally { - lock.unlock(); +class A { + public Number m() { + return 1; } } -public static void method2() { - lock.lock(); - try { - System.out.println(Thread.currentThread().getName() + " execute method2"); - } finally { - lock.unlock(); +class B extends A { + @Override + // 子类m方法的返回值是Integer是父类m方法返回值Number的子类 + public Integer m() { + return 2; } } ``` -面试题:在Lock方法加两把锁会是什么情况呢? - -* 加锁两次解锁两次:正常执行 -* 加锁两次解锁一次:程序直接卡死,线程不能出来,也就说明**申请几把锁,最后需要解除几把锁** -* 加锁一次解锁两次:运行程序会直接报错 +对于子类,java 编译器会做如下处理: ```java -public void getLock() { - lock.lock(); - lock.lock(); - try { - System.out.println(Thread.currentThread().getName() + "\t get Lock"); - } finally { - lock.unlock(); - //lock.unlock(); +class B extends A { + public Integer m() { + return 2; + } + // 此方法才是真正重写了父类 public Number m() 方法 + public synthetic bridge Number m() { + // 调用 public Integer m() + return m(); } } ``` +其中桥接方法比较特殊,仅对 java 虚拟机可见,并且与原来的 public Integer m() 没有命名冲突 -**** - +*** -#### 可打断 -##### 基本使用 -`public void lockInterruptibly()`:获得可打断的锁 +#### 匿名内部类 -* 如果没有竞争此方法就会获取lock对象锁 -* 如果有竞争就进入阻塞队列,可以被其他线程用interrupt打断 +##### 无参优化 -注意:如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断 +源代码: ```java -public static void main(String[] args) throws InterruptedException { - ReentrantLock lock = new ReentrantLock(); - Thread t1 = new Thread(() -> { - try { - System.out.println("尝试获取锁"); - lock.lockInterruptibly(); - } catch (InterruptedException e) { - System.out.println("没有获取到锁,被打断,直接返回"); - return; - } - try { - System.out.println("获取到锁"); - } finally { - lock.unlock(); - } - }, "t1"); - lock.lock(); - t1.start(); - Thread.sleep(2000); - System.out.println("主线程进行打断锁"); - t1.interrupt(); +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(); + } +} +``` -##### 实现原理 -* 不可打断模式:即使它被打断,仍会驻留在 AQS 队列中,一直要等到获得锁后方能得知自己被打断了 - ```java - public final void acquire(int arg) { - if (!tryAcquire(arg) && - acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//阻塞等待 - // 如果acquireQueued返回true,打断状态interrupted = true - selfInterrupt(); - } - static void selfInterrupt() { - // 知道自己被打断了,需要重新产生一次中断完成中断效果 - Thread.currentThread().interrupt(); - } - ``` +##### 带参优化 - ```java - final boolean acquireQueued(final Node node, int arg) { - try { - boolean interrupted = false; - for (;;) { - final Node p = node.predecessor(); - if (p == head && tryAcquire(arg)) { - setHead(node); - p.next = null; // help GC - failed = false; - // 还是需要获得锁后, 才能返回打断状态 - return interrupted; - } - if (shouldParkAfterFailedAcquire(p, node) && - parkAndCheckInterrupt())//被打断 返回true - interrupted = true; - } - } - private final boolean parkAndCheckInterrupt() { - // 阻塞当前线程,如果打断标记已经是 true, 则 park 会失效 - LockSupport.park(this); - // 判断当前线程是否被打断,清除打断标记,被打断返回true - return Thread.interrupted(); - } - } - ``` +引用局部变量的匿名内部类,源代码: -* 可打断模式: +```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 - public void lockInterruptibly() throws InterruptedException { - sync.acquireInterruptibly(1); - } - public final void acquireInterruptibly(int arg) { - 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 会抛出异常, 而不会再次进入循环 - } - } - } - ``` +转换后代码: + +```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 class Candy11 { + public static void test(final int x) { + Runnable runnable = new Candy11$1(x); + } +} +``` +局部变量在底层创建为内部类的成员变量,必须是 final 的原因: +* 在Java中方法调用是值传递的,在匿名内部类中对变量的操作都是基于原变量的副本,不会影响到原变量的值,所以原变量的值的改变也无法同步到副本中 -*** +* 外部变量为final是在编译期以强制手段确保用户不会在内部类中做修改原变量值的操作,也是防止外部操作修改了变量而内部类无法随之变化出现的影响 + 在创建 `Candy11$1 ` 对象时,将 x 的值赋值给了 `Candy11$1` 对象的 val 属性,x 不应该再发生变化了,因为发生变化,this.val$x 属性没有机会再跟着变化 -#### 锁超时 -##### 基本使用 +*** -`public boolean tryLock()`:尝试获取锁,获取到返回true,获取不到直接放弃,不进入阻塞队列 -`public boolean tryLock(long timeout, TimeUnit unit)`:在给定时间内获取锁,获取不到就退出 -注意:tryLock期间也可以被打断 +#### 反射优化 ```java -public static void main(String[] args) { - ReentrantLock lock = new ReentrantLock(); - Thread t1 = new Thread(() -> { - try { - if (!lock.tryLock(2, TimeUnit.SECONDS)) { - System.out.println("获取不到锁"); - return; - } - } catch (InterruptedException e) { - System.out.println("被打断,获取不到锁"); - return; - } - try { - log.debug("获取到锁"); - } finally { - lock.unlock(); +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); } - }, "t1"); - lock.lock(); - System.out.println("主线程获取到锁"); - t1.start(); - - Thread.sleep(1000); - try { - System.out.println("主线程释放了锁"); - } finally { - lock.unlock(); + System.in.read(); } } ``` +foo.invoke 0 ~ 15次调用的是 MethodAccessor 的实现类`NativeMethodAccessorImpl.invoke0()`,本地方法执行速度慢;当调用到第 16 次时,会采用运行时生成的类`sun.reflect.GeneratedMethodAccessor1`代替 - -##### 实现原理 - -* tryLock() - - ```java - public boolean tryLock() { - return sync.nonfairTryAcquire(1);//只尝试一次 - } - ``` - -* tryLock(long timeout, TimeUnit unit) - - ```java - public final boolean tryAcquireNanos(int arg, long nanosTimeout) { - if (Thread.interrupted()) - throw new InterruptedException(); - //tryAcquire 尝试一次 - return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout); - } - protected final boolean tryAcquire(int acquires) { - return nonfairTryAcquire(acquires); - } - ``` - - ```java - private boolean doAcquireNanos(int arg, long nanosTimeout) { - 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(); - } - } - } - ``` - - - -##### 哲学家就餐 - -```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(); - 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(); - } - } - } +```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); } -class Chopstick extends ReentrantLock { - String name; - public Chopstick(String name) { - this.name = name; - } - @Override - public String toString() { - return "筷子{" + name + '}'; +private static native Object invoke0(Method m, Object obj, Object[] args); +``` + +```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 可以修改膨胀阈值 -*** -#### 条件变量 +*** -##### 基本使用 -synchronized 中的条件变量,就是当条件不满足时进入 waitSet 等待 -ReentrantLock 的条件变量比 synchronized 强大之处在于,它支持多个条件变量 -ReentrantLock类获取Condition对象:`public Condition newCondition()` +## JVM调优 -Condition类API: +### 服务器性能 -* `void await()`:当前线程从运行状态进入等待状态,释放锁 -* `void signal()`:唤醒一个等待在Condition上的线程,但是必须获得与该Condition相关的锁 +(调优部分笔记待优化) -使用流程: +对于一个系统要部署上线时,则一定会对JVM进行调整,不经过任何调整直接上线,容易出现线上系统频繁FullGC造成系统卡顿、CPU使用频率过高、系统无反应等问题 -* **await / signal 前需要获得锁** -* await 执行后,会释放锁进入 conditionObject 等待 -* await 的线程被唤醒(打断、超时)去重新竞争 lock 锁 -* 竞争 lock 锁成功后,从 await 后继续执行 +对于一个应用来说通常重点关注的性能指标主要是吞吐量、响应时间、QPS、TPS等、并发用户数等,而这些性能指标又依赖于系统服务器的资源,如:CPU、内存、磁盘IO、网络IO等。对于这些指标数据的收集,通常可以根据Java本身的工具或指令进行查询 -```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(); - } - }).start(); - Thread.sleep(1000); - //叫醒 - new Thread(() -> { - try { - lock.lock(); - //唤醒 - condition2.signal(); - } finally { - lock.unlock(); - } - }).start(); -} -``` +JDK 自带了**监控工具,位于 JDK 的 bin 目录下**,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具: +* jconsole:用于对 JVM 中的内存、线程和类等进行监控; +* jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等 -##### 实现原理 -await流程: +*** -* 开始 Thread-0 持有锁,调用 await,进入 ConditionObject 的 addConditionWaiter 流程 - ```java - // 等待,直到被唤醒或打断 - public final void await() throws InterruptedException { - if (Thread.interrupted()) - throw new InterruptedException(); - // 添加一个 Node 至等待队列, - Node node = addConditionWaiter(); - // 释放节点持有的锁 - int savedState = fullyRelease(node); - int interruptMode = 0; - // 如果该节点还没有转移至 AQS 队列, park 阻塞 - while (!isOnSyncQueue(node)) { - LockSupport.park(this); - // 如果被打断, 退出等待队列,判断打断模式 - if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) - break; - } - // 退出等待队列后, 还需要获得 AQS 队列的锁 - if (acquireQueued(node, savedState) && interruptMode != THROW_IE) - interruptMode = REINTERRUPT; - // 所有已取消的 Node 从队列链表删除 - if (node.nextWaiter != null) - unlinkCancelledWaiters(); - // 应用打断模式 - if (interruptMode != 0) - reportInterruptAfterWait(interruptMode); - } - ``` - ```java - // 打断模式 - 在退出等待时重新设置打断状态 - private static final int REINTERRUPT = 1; - // 打断模式 - 在退出等待时抛出异常 - private static final int THROW_IE = -1; - ``` +### 参数调优 -* 创建新的 Node 状态为 -2(Node.CONDITION),关联 Thread-0,加入等待队列尾部 +对于 JVM 调优,主要就是调整年轻代、老年代、元空间的内存空间大小及使用的垃圾回收器类型 - ```java - // 添加一个 Node 至等待队列 - private Node addConditionWaiter() { - Node t = lastWaiter; - // 所有已取消的 Node 从队列链表删除, - if (t != null && t.waitStatus != Node.CONDITION) { - unlinkCancelledWaiters(); - t = lastWaiter; - } - // 创建一个关联当前线程的新 Node, 添加至队列尾部 - Node node = new Node(Thread.currentThread(), Node.CONDITION); - if (t == null) - firstWaiter = node; - else - 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 - } - } + ```sh + -Xms:设置堆的初始化大小 + -Xmx:设置堆的最大大小 ``` -* 接下来进入 AQS 的 fullyRelease 流程,释放同步器上的锁 - - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ReentrantLock-条件变量1.png) +* 设置年轻代中Eden区和两个Survivor区的大小比例。该值如果不设置,则默认比例为8:1:1。Java官方通过增大Eden区的大小,来减少YGC发生的次数,但有时我们发现,虽然次数减少了,但Eden区满的时候,由于占用的空间较大,导致释放缓慢,此时STW的时间较长,因此需要按照程序情况去调优 - ```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; - } - } + ```sh + -XX:SurvivorRatio ``` -* unpark AQS 队列中的下一个节点,竞争锁,假设没有其他竞争线程,那么 Thread-1 竞争成功 +* 年轻代和老年代默认比例为1:2。可以通过调整二者空间大小比率来设置两者的大小。 -* park 阻塞 Thread-0 + ```sh + -XX:newSize 设置年轻代的初始大小 + -XX:MaxNewSize 设置年轻代的最大大小, 初始大小和最大大小两个值通常相同 + ``` - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ReentrantLock-条件变量2.png) +* 线程堆栈的设置:**每个线程默认会开启1M的堆栈**,用于存放栈帧、调用参数、局部变量等,但一般256K就够用,通常减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统 + ```sh + -Xss 对每个线程stack大小的调整,-Xss128k + ``` +* 一般一天超过一次FullGC就是有问题,首先通过工具查看是否出现内存泄露,如果出现内存泄露则调整代码,没有的话则调整JVM参数 +* 系统CPU持续飙高的话,首先先排查代码问题,如果代码没问题,则咨询运维或者云服务器供应商,通常服务器重启或者服务器迁移即可解决 +* 如果数据查询性能很低下的话,如果系统并发量并没有多少,则应更加关注数据库的相关问题 +* 如果服务器配置还不错,JDK8开始尽量使用G1或者新生代和老年代组合使用并行垃圾回收器 -signal 流程: -* 假设 Thread-1 要来唤醒 Thread-0,进入 ConditionObject 的 doSignal 流程,取得等待队列中第一个 Node,即 Thread-0 所在 Node - ```java - // 唤醒 - 必须持有锁才能唤醒, 因此 doSignal 内无需考虑加锁 - public final void signal() { - // 调用方法的线程是否是资源的持有线程 - if (!isHeldExclusively()) - throw new IllegalMonitorStateException(); - // 取得等待队列中第一个 Node - Node first = firstWaiter; - if (first != null) - doSignal(first); - } - ``` - ```java - // 唤醒 - 将没取消的第一个节点转移至 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); - } - private void doSignalAll(Node first) { - lastWaiter = firstWaiter = null; - do { - Node next = first.nextWaiter; - first.nextWaiter = null; - transferForSignal(first); - first = next; - } while (first != null); - } - ``` -* 执行 transferForSignal 流程,将该 Node 加入 AQS 队列尾部,将 Thread-0 的 waitStatus 改为 0,Thread-3 的waitStatus 改为 -1 - ```java - // 如果节点状态是取消, 返回 false 表示转移失败, 否则转移成功 - final boolean transferForSignal(Node node) { - // 如果状态已经不是 Node.CONDITION, 说明被取消了 - if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) - return false; - // 加入 AQS 队列尾部 - Node p = enq(node); - int ws = p.waitStatus; - // 上一个节点被取消 上一个节点不能设置状态为 Node.SIGNAL - if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) - // unpark 取消阻塞, 让线程重新同步状态 - LockSupport.unpark(node.thread); - return true; - } - ``` - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ReentrantLock-条件变量3.png) +*** -* Thread-1 释放锁,进入 unlock 流程 +# ALG -*** +## 递归 +### 概述 +算法:解题方案的准确而完整的描述,是一系列解决问题的清晰指令,代表着用系统的方法解决问题的策略机制 -### ReadWrite +递归:程序调用自身的编程技巧 -#### 读写锁 +递归: -独占锁:指该锁一次只能被一个线程所持有,对ReentrantLock和Synchronized而言都是独占锁 -共享锁:指该锁可以被多个线程锁持有 +* 直接递归:自己的方法调用自己 +* 间接递归:自己的方法调用别的方法,别的方法又调用自己 -ReentrantReadWriteLock 其读锁是共享,其写锁是独占 +递归如果控制的不恰当,会形成递归的死循环,从而导致栈内存溢出错误 -作用:多个线程同时读一个资源类没有任何问题,为了满足并发量,读取共享资源应该同时进行,但是如果一个线程想去写共享资源,就不应该再有其它线程可以对该资源进行读或写 -使用规则: -* 加锁解锁格式: +推荐阅读:https://time.geekbang.org/column/article/41440 - ```java - r.lock(); - try { - // 临界区 - } finally { - r.unlock(); - } - ``` -* 读-读 能共存、读-写 不能共存、写-写 不能共存 -* 读锁不支持条件变量 +*** -* **重入时升级不支持**:持有读锁的情况下去获取写锁会导致获取写锁永久等待,需要先释放读,再去获得写 -* **重入时降级支持**:持有写锁的情况下去获取读锁 - ```java - w.lock(); - try { - r.lock();// 降级为读锁, 释放写锁, 这样能够让其它线程读取缓存 - try { - // ... - } finally{ - w.unlock();// 要在写锁释放之前获取读锁 - } - } finally{ - r.unlock(); - } - ``` +### 算法 -构造方法: - `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()`:尝试获取锁 +递归的三要素(理论): -读读并发: +1. 递归的终结点 +2. 递归的公式 +3. 递归的方向:必须走向终结点 ```java -public static void main(String[] args) { - ReentrantReadWriteLock rw = new ReentrantReadWriteLock(); - ReentrantReadWriteLock.ReadLock r = rw.readLock(); - ReentrantReadWriteLock.WriteLock w = rw.writeLock(); - - new Thread(() -> { - r.lock(); - try { - Thread.sleep(2000); - System.out.println("Thread 1 running " + new Date()); - } finally { - r.unlock(); - } - },"t1").start(); - new Thread(() -> { - r.lock(); - try { - Thread.sleep(2000); - System.out.println("Thread 2 running " + new Date()); - } finally { - r.unlock(); - } - },"t2").start(); +//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; + }else{ + return f(x-1) + 1; + } } ``` @@ -23994,19 +14069,30 @@ public static void main(String[] args) { -#### 缓存应用 +#### 公式转换 -缓存更新时,是先清缓存还是先更新数据库 - -* 先清缓存:可能造成刚清理缓存还没有更新数据库,线程直接查询了数据库更新缓存 +```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 +//(1)递归的公式: f(n) = f(n-1)- 2 ; +//(2)递归的终结点: f(1) = 1 +//(3)递归的方向:必须走向终结点。 +public static int f(int n){ + if(n == 1){ + return 1; + }else{ + return f(n-1) - 2; + } +} +``` -* 先更新据库:可能造成刚更新数据库,还没清空缓存就有线程从缓存拿到了旧数据 -* 补充情况:查询线程 A 查询数据时恰好缓存数据由于时间到期失效,或是第一次查询 - +#### 注意事项 -可以使用读写锁进行操作 +以上理论只能针对于**规律化递归**,如果是非规律化是不能套用以上公式的! +非规律化递归的问题:文件搜索,啤酒问题。 @@ -24014,261 +14100,122 @@ public static void main(String[] args) { -#### 实现原理 - -##### 加锁原理 +### 应用 -读写锁用的是同一个 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)) - selfInterrupt(); - } - ``` +猴子第一天摘了若干个桃子,当即吃了一半,觉得好不过瘾,然后又多吃了一个。第二天又吃了前一天剩下的一半,觉得好不过瘾,然后又多吃了一个。以后每天都是如此。等到第十天再吃的时候发现只有1个桃子,问猴子第一天总共摘了多少个桃子? - ```java - protected final boolean tryAcquire(int acquires) { - Thread current = Thread.currentThread(); - int c = getState(); - // 获得低 16 位, 代表写锁的 state 计数 - int w = exclusiveCount(c); - if (c != 0) { - // c != 0 and w == 0 表示r != 0,有读锁,并且写锁的拥有者不是自己,获取失败 - if (w == 0 || current != getExclusiveOwnerThread()) - return false; - // 锁重入计数超过低 16 位, 报异常 - if (w + exclusiveCount(acquires) > MAX_COUNT) - throw new Error("Maximum lock count exceeded"); - // 写锁重入, 获得锁成功 - setState(c + acquires); - return true; - } - // c == 0,没有任何锁,判断写锁是否该阻塞 - if (writerShouldBlock() || - !compareAndSetState(c, c + acquires)) - return false; - // 获得锁成功 - setExclusiveOwnerThread(current); - return true; - } - // 非公平锁 writerShouldBlock 总是返回 false, 无需阻塞 - final boolean writerShouldBlock() { - return false; - } - // 公平锁会检查 AQS 队列中是否有前驱节点, 没有(false)才去竞争 - final boolean writerShouldBlock() { - return hasQueuedPredecessors(); - } - ``` +```java +/* +(1)公式: f(x+1)=f(x)-f(x)/2-1; ==> 2f(x+1) = f(x) - 2 ==> f(x)=2f(x+1)+2 +(2)终结点:f(10) = 1 +(3)递归的方向:走向了终结点 +*/ -* t2 r.lock(读锁),进入 tryAcquireShared 流程,如果有写锁占据,那么 tryAcquireShared +public static int f(int x){ + if(x == 10){ + return 1; + } else { + return 2*f(x+1)+2 + } +} +``` - * 返回 -1 表示失败 - * 如果返回 0 表示成功 - * 返回正数表示还有多少后继节点支持共享模式,读写锁返回1 - ```java - public void lock() { - sync.acquireShared(1); - } - public final void acquireShared(int arg) { - // tryAcquireShared 返回负数, 表示获取读锁失败 - if (tryAcquireShared(arg) < 0) - doAcquireShared(arg); - } - ``` - ```java - // 尝试以共享模式获取 - protected final int tryAcquireShared(int unused) { - Thread current = Thread.currentThread(); - int c = getState(); - // 如果是其它线程持有写锁, 并且写锁的持有者不是当前线程,获取读锁失败 - if (exclusiveCount(c) != 0 && //低 16 位, 代表写锁的 state - getExclusiveOwnerThread() != current) - return -1; - // 高 16 位,代表读锁的 state - int r = sharedCount(c); - if (!readerShouldBlock() && // 读锁不该阻塞 - r < MAX_COUNT && // 小于读锁计数 - compareAndSetState(c, c + SHARED_UNIT)) {// 尝试增加计数成功 - // .... - // 读锁加锁成功 - return 1; - } - // 与 tryAcquireShared 功能类似, 但会不断尝试 for (;;) 获取读锁, 执行过程中无阻塞 - return fullTryAcquireShared(current); - } - // 非公平锁 readerShouldBlock 看 AQS 队列中第一个节点是否是写锁,是则阻塞,反之不阻塞 - // true 则该阻塞, false 则不阻塞 - final boolean readerShouldBlock() { - return apparentlyFirstQueuedIsExclusive(); - } - ``` +*** -* 进入 sync.doAcquireShared(1) 流程,首先也是调用 addWaiter 添加节点,不同之处在于节点被设置为Node.SHARED 模式而非 Node.EXCLUSIVE 模式,注意此时 t2 仍处于活跃状态 - ```java - private void doAcquireShared(int arg) { - // 将当前线程关联到一个 Node 对象上, 模式为共享模式 - final Node node = addWaiter(Node.SHARED); - boolean failed = true; - try { - boolean interrupted = false; - for (;;) { - // 获取前驱节点 - final Node p = node.predecessor(); - if (p == head) { - // 再一次尝试获取读锁 - int r = tryAcquireShared(arg); - // r >= 0 表示获取成功 - if (r >= 0) { - setHeadAndPropagate(node, r); - p.next = null; // help GC - if (interrupted) - selfInterrupt(); - failed = false; - return; - } - } - // 是否在获取读锁失败时阻塞 - if (shouldParkAfterFailedAcquire(p, node) && - // park 当前线程 - parkAndCheckInterrupt()) - interrupted = true; - } - } finally { - if (failed) - cancelAcquire(node); - } - } - ``` - ```java - private void setHeadAndPropagate(Node node, int propagate) { - Node h = head; - // 设置自己为 head 节点 - setHead(node); - // 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(); - } - } - ``` +#### 递归求和 - ```java - 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) { - // 因为读锁共享,如果其它线程也在释放读锁,那么需要将 waitStatus 先改为 0 - // 防止 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; - } - } - ``` +```java +//(1)递归的终点接:f(1) = 1 +//(2)递归的公式: f(n) = f(n-1) + n +//(3)递归的方向必须走向终结点: +public static int f(int n){ + if(n == 1 ) return 1; + return f(n-1) + n; +} +``` -* 如果没有成功,在 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) +#### 汉诺塔 -*** +```java +public class Hanoi { + public static void main(String[] args) { + hanoi('X', 'Y', 'Z', 3); + } + //将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 + System.out.println(x + "→" + z); //然后将x最下面的块(最大的)移动到z + hanoi(y, x, z, n - 1); //最后将n-1个块从y移动到z,x为辅助柱 + } + } +} +``` +时间复杂度 O(2^n) -##### 解锁原理 -* 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 所在节点 +非规律化递归问题。 -* t2 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,但计数还不为零 - t3 同样让计数减一,计数为零,进入doReleaseShared() 将头节点从 -1 改为 0 并唤醒下一个节点 +啤酒2元一瓶,4个盖子可以换一瓶,2个空瓶可以换一瓶。 - ```java - public void unlock() { - sync.releaseShared(1); - } - public final boolean releaseShared(int arg) { - if (tryReleaseShared(arg)) { - doReleaseShared(); - return true; - } - return false; - } - ``` +```java +public class BeerDemo{ + // 定义一个静态变量存储可以喝酒的总数 + public static int totalNum; + public static int lastBottleNum; + public static int lastCoverNum; + public static void main(String[] args) { + buyBeer(10); + System.out.println("总数:"+totalNum); + System.out.println("剩余盖子:"+ lastCoverNum); + System.out.println("剩余瓶子:"+ lastBottleNum); + } + public static void buyBeer(int money){ + int number = money / 2; + totalNum += number; + // 算出当前剩余的全部盖子和瓶子数,换算成金额继续购买。 + int currentBottleNum = lastBottleNum + number ; + int currentCoverNum = lastCoverNum + number ; + // 把他们换算成金额 + int totalMoney = 0 ; + totalMoney += (currentBottleNum/2)*2;//除2代表可以换几个瓶子,乘2代表换算成钱,秒! + lastBottleNum = currentBottleNum % 2 ;//取余//算出剩余的瓶子 + + totalMoney += (currentCoverNum / 4) * 2; + lastCoverNum = currentCoverNum % 4 ; - ```java - protected final boolean tryReleaseShared(int unused) { - // - for (;;) { - int c = getState(); - int nextc = c - SHARED_UNIT; - // 读锁的计数不会影响其它获取读锁线程, 但会影响其它获取写锁线程 - // 计数为 0 才是真正释放 - if (compareAndSetState(c, nextc)) - return nextc == 0; - } - } - ``` + // 继续拿钱买酒 + if(totalMoney >= 2){ + buyBeer(totalMoney); + } + } +} +``` -* t4 在 acquireQueued 中 parkAndCheckInterrupt 处恢复运行,再次 for (;;) 这次自己是头节点的临节点,并且没有其他节点竞争,tryAcquire(1) 成功,修改头结点,流程结束 - @@ -24276,1420 +14223,998 @@ public static void main(String[] args) { -#### Stamped +## 排序 -StampedLock:读写锁,该类自 JDK 8 加入,是为了进一步优化读性能 +### 冒泡排序 -特点: +冒泡排序(Bubble Sort):两个数比较大小,较大的数下沉,较小的数冒起来 -* 在使用读锁、写锁时都必须配合戳使用 +算法描述:每次从数组的第一个位置开始两两比较,把较大的元素与较小的元素进行层层交换,最终把当前最大的一个元素存入到数组当前的末尾 -* StampedLock 不支持条件变量 -* StampedLock **不支持可重入** +实现思路: -基本用法 +1. 确定总共需要冒几轮:数组的长度-1 +2. 每轮两两比较几次 -* 加解读锁: + - ```java - long stamp = lock.readLock(); - lock.unlockRead(stamp); - ``` +```java +// 0 1位置比较,大的放后面,然后1 2位置比较,大的继续放后面,一轮循环最后一位是最大值 +public class BubbleSort { + public static void main(String[] args) { + int[] arr = {55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88}; + int flag;//标记本趟排序是否发生了交换 + //比较i和i+1,不需要再比最后一个位置 + for (int i = 0; i < arr.length - 1; i++) { + flag = 0; + //最后i位不需要比,已经排序好 + for (int j = 0; j < arr.length - 1 - i; j++) { + if (arr[j] > arr[j + 1]) { + int temp = arr[j]; + arr[j] = arr[j + 1]; + arr[j + 1] = temp; + flag = 1;//发生了交换 + } + } + //没有发生交换,证明已经有序,不需要继续排序 + if(flag == 0) { + break; + } + } + System.out.println(Arrays.toString(arr)); + } +} +``` -* 加解写锁: +冒泡排序时间复杂度:最坏情况 - ```java - long stamp = lock.writeLock(); - lock.unlockWrite(stamp); - ``` +* 元素比较的次数为:`(N-1)+(N-2)+(N-3)+...+2+1=((N-1)+1)*(N-1)/2=N^2/2-N/2` +* 元素交换的次数为:`(N-1)+(N-2)+(N-3)+...+2+1=((N-1)+1)*(N-1)/2=N^2/2-N/2` +* 总执行次数为:`(N^2/2-N/2)+(N^2/2-N/2)=N^2-N` -* 乐观读,StampedLock 支持`tryOptimisticRead()`方法,读取完毕后做一次**戳校验**,如果校验通过,表示这期间没有其他线程的写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全 +按照大 O 推导法则,保留函数中的最高阶项那么最终冒泡排序的时间复杂度为 O(N^2) - ```java - long stamp = lock.tryOptimisticRead(); - // 验戳 - if(!lock.validate(stamp)){ - // 锁升级 - } - ``` -提供一个数据容器类内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法: -* 读-读 可以优化 -* 读-写 优化读,补加读锁 +*** -```java -public static void main(String[] args) throws InterruptedException { - DataContainerStamped dataContainer = new DataContainerStamped(1); - new Thread(() -> { - dataContainer.read(1000); - },"t1").start(); - Thread.sleep(500); - - new Thread(() -> { - dataContainer.write(1000); - },"t2").start(); -} - -class DataContainerStamped { - private int data; - private final StampedLock lock = new StampedLock(); - - public int read(int readTime) throws InterruptedException { - 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(); - System.out.println(new Date() + " read lock" + stamp); - Thread.sleep(readTime); - System.out.println(new Date() + " read finish..." + stamp); - return data; - } finally { - System.out.println(new Date() + " read unlock " + stamp); - lock.unlockRead(stamp); - } - } - public void write(int newData) { - long stamp = lock.writeLock(); - System.out.println(new Date() + " write lock " + stamp); - try { - Thread.sleep(2000); - this.data = newData; - } catch (InterruptedException e) { - e.printStackTrace(); - } finally { - System.out.println(new Date() + " write unlock " + stamp); - lock.unlockWrite(stamp); +### 选择排序 + +#### 简单选择 + +选择排序(Selection-sort):一种简单直观的排序算法 + +算法描述:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕 + +实现思路: + +1. 控制选择几轮:数组的长度-1 +2. 控制每轮从当前位置开始比较几次 + + + +```java +// 0 1位置比较,小的放0位置,然后0 2位置比,小的继续放0位置,一轮循环0位置是最小值 +public class SelectSort { + public static void main(String[] args) { + int[] arr = {55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88}; + for (int i = 0; i < arr.length - 1; i++) { + //获取最小索引位置 + int minIndex = i; + for (int j = i + 1; j < arr.length; j++) { + if (arr[minIndex] > arr[j]) { + minIndex = j; + } + } + //交换元素 + int temp = arr[i]; + arr[i] = arr[minIndex]; + arr[minIndex] = temp; } + System.out.println(Arrays.toString(arr)); } } ``` +选择排序时间复杂度: +* 数据比较次数:`(N-1)+(N-2)+(N-3)+...+2+1=((N-1)+1)*(N-1)/2=N^2/2-N/2` +* 数据交换次数:`N-1` +* 时间复杂度:`N^2/2-N/2+(N-1)=N^2/2+N/2-1` - - -*** +根据大 O 推导法则,保留最高阶项,去除常数因子,时间复杂度为 O(N^2) +*** -## 并发包 -(源码分析待更新) +#### 堆排序 -### Semaphore +堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法,堆结构是一个近似完全二叉树的结构,并同时满足子结点的键值或索引总是小于(或者大于)父节点 -#### 信号量 +优先队列:堆排序每次上浮过程都会将最大或者最小值放在堆顶,应用于优先队列可以将优先级最高的元素浮到堆顶 -synchronized 可以起到锁的作用,但某个时间段内,只能有一个线程允许执行 +实现思路: -Semaphore(信号量)用来限制能同时访问共享资源的线程上限,非重入锁 +1. 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,并通过上浮对堆进行调整,此堆为初始的无序区,堆顶为最大数 -构造方法: +2. 将堆顶元素 R[1] 与最后一个元素 R[n] 交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区 Rn,且满足 R[1,2…n-1]<=R[n] -* `public Semaphore(int permits)`:permits 表示许可线程的数量(state) -* `public Semaphore(int permits, boolean fair)`:fair 表示公平性,如果这个设为 true 的话,下次执行的线程会是等待最久的线程 +3. 交换后新的堆顶 R[1] 可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将 R[1] 与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn),不断重复此过程直到有序区的元素个数为 n-1,则整个排序过程完成 -常用API: + -* `public void acquire()`:表示获取许可 -* `public void release()`:表示释放许可,acquire()和release()方法之间的代码为同步代码 +floor:向下取整 ```java -public static void main(String[] args) { - // 1.创建Semaphore对象 - Semaphore semaphore = new Semaphore(3); +public class HeapSort { + public static void main(String[] args) { + int[] arr = {55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88}; + heapSort(arr, arr.length); + System.out.println(Arrays.toString(arr)); + } - // 2. 10个线程同时运行 - for (int i = 0; i < 10; i++) { - new Thread(() -> { - try { - // 3. 获取许可 - semaphore.acquire(); - sout(Thread.currentThread().getName() + " running..."); - Thread.sleep(1000); - sout(Thread.currentThread().getName() + " end..."); - } catch (InterruptedException e) { - e.printStackTrace(); - } finally { - // 4. 释放许可 - semaphore.release(); + //len为数组长度 + private static void heapSort(int[] arr, int len) { + //建堆,逆排序,因为堆排序定义的交换顺序是从当前结点往下交换,逆序排可以避免多余的交换 + for (int i = len / 2 - 1; i >= 0; i--) { + //调整函数 + sift(arr, i, len - 1); + } + //从尾索引开始排序 + for (int i = len - 1; i > 0; i--) { + //将最大的节点放入末尾 + int temp = arr[0]; + arr[0] = arr[i]; + arr[i] = temp; + //继续寻找最大的节点 + sift(arr, 0, i - 1); + } + } + + //调整函数,调整arr[low]的元素,从索引low到high的范围调整 + private static void sift(int[] arr, int low, int high) { + //暂存调整元素 + int temp = arr[low]; + int i = low, j = low * 2 + 1;//j是左节点 + while (j <= high) { + //判断是否有右孩子,并且比较左右孩子中较大的节点 + if (j < high && arr[j] < arr[j + 1]) { + j++; //指向右孩子 + } + if (temp < arr[j]) { + arr[i] = arr[j]; + i = j; //继续向下调整 + j = 2 * i + 1; + } else { + //temp > arr[j],说明也大于j的孩子,探测结束 + break; } - }).start(); + } + //将被调整的节点放入最终的位置 + arr[i] = temp; } } ``` +堆排序的时间复杂度是 O(nlogn) + *** -#### 实现原理 +### 插入排序 -加锁流程: +#### 直接插入 -* Semaphore 的 permits(state)为 3,这时 5 个线程来获取资源 +插入排序(Insertion Sort):在要排序的一组数中,假定前 n-1 个数已经排好序,现在将第 n 个数插到这个有序数列中,使得这 n 个数也是排好顺序的,如此反复循环,直到全部排好顺序 - ```java - Sync(int permits) { - setState(permits); - } - ``` + - 假设其中 Thread-1,Thread-2,Thread-4 CAS 竞争成功,permits 变为 0,而 Thread-0 和 Thread-3 竞争失败,进入 AQS 队列park 阻塞 +```java +public class InsertSort { + public static void main(String[] args) { + 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处的值大,则交换数据, + //如果不大,那么就找到合适的位置了,退出循环即可; + if (arr[j - 1] > arr[j]) { + int temp = arr[j]; + arr[j] = arr[j - 1]; + arr[j - 1] = temp; + } + } + } + System.out.println(Arrays.toString(arr)); + } +} +``` - ```java - // acquire() -> sync.acquireSharedInterruptibly(1); - public final void acquireSharedInterruptibly(int arg) { - if (Thread.interrupted()) - throw new InterruptedException(); - if (tryAcquireShared(arg) < 0) - doAcquireSharedInterruptibly(arg); - } - - // tryAcquireShared() -> nonfairTryAcquireShared() - // 非公平,公平锁会在循环内 hasQueuedPredecessors()方法判断是否有临头节点(第二个节点) - final int nonfairTryAcquireShared(int acquires) { - for (;;) { - int available = getState(); - int remaining = available - acquires; - // 如果许可已经用完, 返回负数, 表示获取失败, - if (remaining < 0 || - // 如果 cas 重试成功, 返回正数, 表示获取成功 - compareAndSetState(available, remaining)) - return remaining; - } - } - ``` +插入排序时间复杂度: - ```java - private void doAcquireSharedInterruptibly(int arg) { - 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 - p.next = null; // help GC - failed = false; - return; - } - } - // 不成功, 设置上一个节点 waitStatus = Node.SIGNAL, 下轮进入 park 阻塞 - if (shouldParkAfterFailedAcquire(p, node) && - parkAndCheckInterrupt()) - throw new InterruptedException(); - } - } finally { - if (failed) - cancelAcquire(node); - } - } - ``` +* 比较的次数为:`(N-1)+(N-2)+(N-3)+...+2+1=((N-1)+1)*(N-1)/2=N^2/2-N/2` +* 交换的次数为:`(N-1)+(N-2)+(N-3)+...+2+1=((N-1)+1)(N-1)/2=N^2/2-N/2` +* 总执行次数为:`(N^2/2-N/2)+(N^2/2-N/2)=N^2-N` - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-Semaphore工作流程1.png) +按照大 O 推导法则,保留函数中的最高阶项那么最终插入排序的时间复杂度为 O(N^2) -* 这时 Thread-4 释放了 permits,状态如下 - ```java - // release() -> releaseShared() - 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; - if (next < current) - throw new Error("Maximum permit count exceeded"); - // 释放锁 - if (compareAndSetState(current, next)) - return true; - } - } - private void doReleaseShared() { - // PROPAGATE 详解 - // 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark - // 如果 head.waitStatus == 0 ==> Node.PROPAGATE - } - ``` - ![](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 状态 +#### 希尔排序 -**** +希尔排序(Shell Sort):也是一种插入排序,也称为缩小增量排序 +实现思路: +1. 选定一个增长量h,按照增长量h作为数据分组的依据,对数据进行分组 +2. 对分好组的每一组数据完成插入排序 +3. 减小增长量,最小减为1,重复第二步操作 -#### PROPAGATE + -假设存在某次循环中队列里排队的结点情况为 `head(-1)->t1(-1)->t2(0)` -假设存在将要信号量释放的 T3 和 T4,释放顺序为先 T3 后 T4 +希尔排序的核心在于间隔序列的设定,既可以提前设定好间隔序列,也可以动态的定义间隔序列,希尔排序就是插入排序增加了间隔 ```java -// 老版本代码 -private void setHeadAndPropagate(Node node, int propagate) { - setHead(node); - // 有空闲资源 - if (propagate > 0 && node.waitStatus != 0) { - Node s = node.next; - // 下一个 - if (s == null || s.isShared()) - unparkSuccessor(node); +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的初始值 + 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] + if (arr[j] < arr[j - h]) { + int temp = arr[j]; + arr[j] = arr[j - h]; + arr[j - h] = temp; + } + } + } + //3. 减小h的值,减小规则为: + h = h / 2; + } + System.out.println(Arrays.toString(arr)); } } ``` -正常流程: +在希尔排序中,增长量h并没有固定的规则,有很多论文研究了各种不同的递增序列,但都无法证明某个序列是最好的,所以对于希尔排序的时间复杂度分析就认为 O(nlogn) -* T3 调用 releaseShared(1),直接调用了 unparkSuccessor(head),head.waitStatus 从 -1 变为 0 -* T1 由于 T3 释放信号量被唤醒,然后T4 释放,唤醒 T2 -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(t1.node, 0) 时,因为不满足 propagate > 0(剩余资源量 == 0),从而不会唤醒后继结点, **T2 线程得不到唤醒** +*** -更新后流程: +### 归并排序 -* 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 时,读到 h.waitStatus < 0,从而调用 doReleaseShared() 唤醒 T2 +归并排序(Merge Sort):建立在归并操作上的一种有效的排序算法,该算法是采用分治法的典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 -```java -private void setHeadAndPropagate(Node node, int 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(); - - }} -``` - -```java -// 唤醒 -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; - } -} -``` +实现思路: +1. 一组数据拆分成两个元素相等的子组,并对每一个子组继续拆分,直到拆分后的每个子组的元素个数是1为止 +2. 将相邻的两个子组进行合并成一个有序的大组 +3. 不断的重复步骤2,直到最终只有一个组为止 + +归并步骤:每次比较两端最小的值,把最小的值放在辅助数组的左边 +![](https://gitee.com/seazean/images/raw/master/Java/Sort-归并步骤1.png) -*** +![](https://gitee.com/seazean/images/raw/master/Java/Sort-归并步骤2.png) +![](https://gitee.com/seazean/images/raw/master/Java/Sort-归并步骤3.png) -### 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(); +public class MergeSort { + public static void main(String[] args) { + int[] arr = new int[]{55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88}; + mergeSort(arr, 0, arr.length - 1); + System.out.println(Arrays.toString(arr)); + } + //low 为arr最小索引,high为最大索引 + public static void mergeSort(int[] arr, int low, int high) { + if (low < high) { + int mid = (low + high) / 2; + mergeSort(arr, low, mid);//归并排序前半段 + mergeSort(arr, mid + 1, high);//归并排序后半段 + merge(arr, low, mid, high);//将两段有序段合成一段有序段 + } + } - 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(); - }); + private static void merge(int[] arr, int low, int mid, int high) { + int m = 0; + //定义左右指针 + int left = low, right = mid + 1; + int[] assist = new int[high - low + 1]; + while (left <= mid && right <= high) { + assist[m++] = arr[left] < arr[right] ? arr[left++] : arr[right++]; + } + while (left <= mid) { + assist[m++] = arr[left++]; + } + while (right <= high) { + assist[m++] = arr[right++]; + } + + for (int k = 0; k < assist.length; k++) { + arr[low++] = assist[k]; + } } - latch.await(); - System.out.println("\n游戏开始"); - service.shutdown(); } -/* -[100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%] -游戏开始 ``` + +用树状图来描述归并,假设元素的个数为 n,那么使用归并排序拆分的次数为 `log2(n)`,即层数,每次归并需要做 n 次对比,最终得出的归并排序的时间复杂度为 `log2(n)*n`,根据大O推导法则,忽略底数,最终归并排序的时间复杂度为 O(nlogn) -*** +归并排序的缺点:需要申请额外的数组空间,导致空间复杂度提升,是典型的**以空间换时间**的操作 -### CyclicBarrier -CyclicBarrier作用:循环屏障,用来进行线程协作,等待线程满足某个计数,才能触发自己执行 -常用方法: +**** + + -* `public CyclicBarrier(int parties, Runnable barrierAction)`:用于在线程到达屏障parties时,执行barrierAction - * parties:代表多少个线程到达屏障开始触发线程任务 - * barrierAction:线程任务 -* `public int await()`:线程调用 await 方法通知 CyclicBarrier 本线程已经到达屏障 +### 快速排序 -与 CountDownLatch 的区别:CyclicBarrier 是可以重用的 +快速排序(Quick Sort):通过**分治思想**对冒泡排序的改进,基本过程是通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,以此达到整个数据变成有序序列 + +实现思路: + +1. 从数列中挑出一个元素,称为基准(pivot) +2. 重新排序数列,所有比基准值小的摆放在基准前面,所有比基准值大的摆在基准的后面(相同的数可以到任一边),在这个分区退出之后,该基准就处于数列的中间位置,这个称为分区(partition)操作; +3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序 -应用:可以实现多线程中,某个任务在等待其他线程执行完毕以后触发 + ```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(); - } - }); +public class QuickSort { + public static void main(String[] args) { + int[] arr = {55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88}; + quickSort(arr, 0, arr.length - 1); + System.out.println(Arrays.toString(arr)); + } - service.submit(() -> { - System.out.println("task2 begin..."); - try { - Thread.sleep(2000); - barrier.await(); // 1 - 1 = 0 - } catch (InterruptedException | BrokenBarrierException e) { - e.printStackTrace(); + public static void quickSort(int[] arr, int low, int high) { + //递归结束的条件 + if (low >= high) { + return; + } + + int left = low; + int right = high; + + int temp = arr[left];//基准数 + while (left < right) { + // 用 >= 可以防止多余的交换 + while (arr[right] >= temp && right > left) { + right--; } - }); + // 做判断防止相等 + if (right > left) { + // 到这里说明 arr[right] < temp + arr[left] = arr[right];//此时把arr[right]元素视为空 + left++; + } + while (arr[left] <= temp && left < right) { + left++; + } + if (right > left) { + arr[right] = arr[left]; + right--; + } + } + // left == right + arr[left] = temp; + quickSort(arr, low, left-1); + quickSort(arr, right + 1, high); } - service.shutdown(); } ``` +快速排序和归并排序的区别: +* 快速排序是另外一种分治的排序算法,将一个数组分成两个子数组,将两部分独立的排序 +* 归并排序的处理过程是由下到上的,先处理子问题,然后再合并。而快排正好相反,它的处理过程是由上到下的,先分区,然后再处理子问题 +* 快速排序和归并排序是互补的:归并排序将数组分成两个子数组分别排序,并将有序的子数组归并从而将整个数组排序,而快速排序的方式则是当两个数组都有序时,整个数组自然就有序了 +* 在归并排序中,一个数组被等分为两半,归并调用发生在处理整个数组之前,在快速排序中,切分数组的位置取决于数组的内容,递归调用发生在处理整个数组之后 -*** - +时间复杂度: +* 最优情况:每一次切分选择的基准数字刚好将当前序列等分。把数组的切分看做是一个树,共切分了 logn 次,所以,最优情况下快速排序的时间复杂度为 O(nlogn) -### Exchanger +* 最坏情况:每一次切分选择的基准数字是当前序列中最大数或者最小数,这使得每次切分都会有一个子组,那么总共就得切分n次,所以最坏情况下,快速排序的时间复杂度为 O(n^2) -Exchanger:交换器,是一个用于线程间协作的工具类,用于进行线程间的数据交换 + -工作流程:两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据 +* 平均情况:每一次切分选择的基准数字不是最大值和最小值,也不是中值,这种情况用数学归纳法证明,快速排序的时间复杂度为 O(nlogn) -常用方法: -* `public Exchanger()`:创建一个新的交换器 -* `public V exchange(V x)`:等待另一个线程到达此交换点 -* `public V exchange(V x, long timeout, TimeUnit unit)`:等待一定的时间 -```java -public class ExchangerDemo { - public static void main(String[] args) { - // 创建交换对象(信使) - Exchanger exchanger = new Exchanger<>(); - new ThreadA(exchanger).start(); - new ThreadA(exchanger).start(); - } -} -class ThreadA extends Thread{ - private Exchanger exchanger(); - public ThreadA(Exchanger exchanger){this.exchanger = exchanger;} - @Override - public void run() { - try{ - sout("线程A,做好了礼物A,等待线程B送来的礼物B"); - //如果等待了5s还没有交换就死亡(抛出异常)! - String s = exchanger.exchange("礼物A",5,TimeUnit.SECONDS); - sout("线程A收到线程B的礼物:" + s); - } catch (Exception e) { - System.out.println("线程A等待了5s,没有收到礼物,最终就执行结束了!"); - } - } -} -class ThreadB extends Thread{ - private Exchanger exchanger; - public ThreadB(Exchanger exchanger) { - this.exchanger = exchanger; - } - @Override - public void run() { - try { - sout("线程B,做好了礼物B,等待线程A送来的礼物A....."); - // 开始交换礼物。参数是送给其他线程的礼物! - sout("线程B收到线程A的礼物:" + exchanger.exchange("礼物B")); - } catch (Exception e) { - e.printStackTrace(); - } - } -} -``` +推荐视频:https://www.bilibili.com/video/BV1b7411N798?t=1001&p=81 +参考文章:https://blog.csdn.net/nrsc272420199/article/details/82587933 -*** -### ConHashMap +**** -(待更新) -#### 并发集合 -##### 集合对比 +### 基数排序 -三种集合: +基数排序(Radix Sort):又叫桶排序和箱排序,借助多关键字排序的思想对单逻辑关键字进行排序的方法 -* HashMap是线程不安全的,性能好 -* Hashtable线程安全基于synchronized,综合性能差,已经被淘汰 -* ConcurrentHashMap保证了线程安全,综合性能较好,不止线程安全,而且效率高,性能好 +计数排序其实是桶排序的一种特殊情况,当要排序的 n 个数据,所处的范围并不大的时候,比如最大值是 k,我们就可以把数据划分成 k 个桶,每个桶内的数据值都是相同的,省掉了桶内排序的时间 -集合对比: +按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前 -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[] 当中,最后统计数量时累加 +- 获得最大数的位数,可以通过将最大数变为String类型,再求长度 +- 将所有待比较数值(正整数)统一为同样的数位长度,**位数较短的数前面补零** +- 从最低位开始,依次进行一次排序 +- 从最低位排序一直到最高位(个位 → 十位 → 百位 → … →最高位)排序完成以后,数列就变成一个有序序列 ```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 BucketSort { + public static void main(String[] args) { + int[] arr = new int[]{576, 22, 26, 548, 1, 3, 843, 536, 735, 43, 3, 912, 88}; + bucketSort(arr); + System.out.println(Arrays.toString(arr)); } -} -public class AddMapDataThread extends Thread{ - @Override - public void run() { - for(int i = 0 ; i < 1000000 ; i++ ){ - ConcurrentHashMapDemo.map.put("键:"+i , "值"+i); + private static void bucketSort(int[] arr) { + // 桶的个数固定为10个(个位是0~9),数组长度为了防止所有的数在同一行 + int[][] bucket = new int[10][arr.length]; + //记录每个桶中的有多少个元素 + int[] elementCounts = new int[10]; + + //获取数组的最大元素 + int max = arr[0]; + for (int i = 1; i < arr.length; i++) { + max = max > arr[i] ? max : arr[i]; + } + String maxEle = Integer.toString(max); + //将数组中的元素放入桶中,最大数的位数相当于需要几次放入桶中 + for (int i = 0, step = 1; i < maxEle.length(); i++, step *= 10) { + for (int j = 0; j < arr.length; j++) { + //获取最后一位的数据,也就是索引 + int index = (arr[j] / step) % 10; + //放入具体位置 + bucket[index][elementCounts[index]] = arr[j]; + //存储每个桶的数量 + elementCounts[index]++; + } + //收集回数组 + for (int j = 0, index = 0; j < 10; j++) { + //先进先出 + int position = 0; + //桶中有元素就取出 + while (elementCounts[j] > 0) { + arr[index] = bucket[j][position]; + elementCounts[j]--; + position++; + index++; + } + } } } } ``` +空间换时间 -**** +推荐视频:https://www.bilibili.com/video/BV1b7411N798?p=86 +参考文章:https://www.toutiao.com/a6593273307280179715/?iid=6593273307280179715 -##### 并发死链 -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 虽然将扩容算法做了调整,改用了尾插法,但仍不意味着能够在多线程环境下能够安全扩容,还会出现其它问题(如扩容丢数据) +稳定性:在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中 `r[i]=r[j]`,且 r[i] 在 r[j] 之前,而在排序后的序列中,r[i] 仍在 r[j] 之前,则称这种排序算法是稳定的,否则称为不稳定的 +如果一组数据只需要一次排序,则稳定性一般是没有意义的,如果一组数据需要多次排序,稳定性是有意义的。 + -*** +* 冒泡排序:只有当 `arr[i]>arr[i+1]` 的时候,才会交换元素的位置,而相等的时候并不交换位置,所以冒泡排序是一种稳定排序算法 +* 选择排序:是给每个位置选择当前元素最小的,例如有数据{5(1),8 ,5(2), 3, 9 },第一遍选择到的最小元素为3,所以5(1)会和3进行交换位置,此时5(1)到了5(2)后面,破坏了稳定性,所以是不稳定的排序算法 +* 插入排序:比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么把要插入的元素放在相等元素的后面。相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的 +* 希尔排序:按照不同步长对元素进行插入排序,虽然一次插入排序是稳定的,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以希尔排序是不稳定的 +* 归并排序在归并的过程中,只有 `arr[i] 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表 +![](https://gitee.com/seazean/images/raw/master/Java/Sort-排序算法对比.png) - ```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{} - ``` +正常查找:从第一个元素开始遍历,一个一个的往后找,综合查找比较耗时。 - +二分查找也称折半查找(Binary Search)是一种效率较高的查找方法,数组必须是有序数组 -*** +过程:每次先与中间的元素进行比较,如果大于往右边找,如果小于往左边找,如果等于就返回该元素索引位置!如果没有该元素,返回-1 +时间复杂度:O(logn) +```java +/*定义一个方法,记录开始的索引位置和结束的索引位置。 +取出中间索引位置的值,拿元素与中间位置的值进行比较,如果小于中间值,结束位置=中间索引-1. +取出中间索引位置的值,拿元素与中间位置的值进行比较,如果大于中间值,开始位置=中间索引+1. +循环正常执行的条件:开始位置索引<=结束位置索引。否则说明寻找完毕但是没有该元素值返回-1.*/ +public class binarySearch { + public static void main(String[] args) { + int[] arr = {10, 14, 21, 38, 45, 47, 53, 81, 87, 99}; + System.out.println("81的索引是:" + binarySearch(arr,81)); -##### 构造方法 + } -懒惰初始化,在构造方法中仅计算了 table 的大小,在第一次使用时才会真正创建: + public static int binarySearch(int[] arr, int des) { + int start = 0; + int end = arr.length - 1; -```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; + //确保不会出现重复查找,越界 + while (start <= end) { + //计算出中间索引值 + int mid = (start + end) / 2; + if (des == arr[mid]) { + return mid; + } else if (des > arr[mid]) { + start = mid + 1; + } else if (des < arr[mid]) { + end = mid - 1; + } + } + // 如果上述循环执行完毕还没有返回索引,说明根本不存在该元素值,直接返回-1 + return -1; + } } ``` +![](https://gitee.com/seazean/images/raw/master/Java/二分查找.gif) +查找第一个匹配的元素: -*** +```java +public static int binarySearch(int[] arr, int des) { + int start = 0; + int end = arr.length - 1; + while (start <= end) { + int mid = (start + end) / 2; + if (des == arr[mid]) { + //如果 mid 等于 0,那这个元素已经是数组的第一个元素,那肯定是我要找的 + if (mid == 0 || a[mid - 1] != des) { + return mid; + } else { + //a[mid]前面的一个元素 a[mid-1]也等于 value, + //要找的元素肯定出现在[low, mid-1]之间 + high = mid - 1 + } + } else if (des > arr[mid]) { + start = mid + 1; + } else if (des < arr[mid]) { + end = mid - 1; + } + } + return -1; + } +``` -##### 成员方法 -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 来累加计数 +### BF - ```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; - } - } - } - ``` +Brute Force 暴力匹配算法: - +```java +public static void main(String[] args) { + String s = "seazean"; + String t = "az"; + System.out.println(match(s,t));//2 +} -*** +public static int match(String s,String t) { + int k = 0; + int i = k, j = 0; + //防止越界 + while (i < s.length() && j < t.length()) { + if (s.charAt(i) == t.charAt(j)) { + ++i; + ++j; + } else { + k++; + i = k; + j = 0; + } + } + //说明是匹配成功 + if (j >= t.length()) { + return k; + } + return 0; +} +``` +平均时间复杂度:O(m+n),最坏时间复杂度:O(m*n) -#### JDK7源码 -##### 分段锁 +*** -ConcurrentHashMap 对锁粒度进行了优化,**分段锁技术**,将整张表分成了多个数组(Segment),每个数组又是一个类似 HashMap 数组的结构。`ConcurrentHashMap`允许多个修改操作并发进行,并发时锁住的是每个Segment,其他Segment还是可以操作的,这样不同Segment之间就可以实现并发,大大提高效率 -底层结构: **Segment 数组 + HashEntry 数组 + 链表**(数组+链表是HashMap的结构) -* 优点:如果多个线程访问不同的 segment,实际是没有冲突的,这与 jdk8 中是类似的 +### RK -* 缺点:Segments 数组默认大小为16,这个容量初始化指定后就不能改变了,并且不是懒惰初始化 +把主串得长度记为 n,模式串得长度记为 m,通过哈希算法对主串中的 n-m+1 个子串分别求哈希值,然后逐个与模式串的哈希值比较大小,如果某个子串的哈希值与模式串相等,再去对比值是否相等(防止哈希冲突),那就说明对应的子串和模式串匹配了 - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentHashMap 1.7底层结构.png) +因为哈希值是一个数字,数字之间比较是否相等是非常快速的 +第一部分计算哈希值的时间复杂度为 O(n),第二部分对比的时间复杂度为 O(1),整体平均时间复杂度为 O(n),最坏为 O(n*m) +*** -##### 成员方法 -1. segment:是一种可重入锁,继承ReentrantLock - ```java - static final class Segment extends ReentrantLock implements Serializable { - transient volatile HashEntry[] table; //可以理解为包含一个HashMap - } - ``` - -2. 构造方法 +### KMP - 无参构造: +KMP匹配: - ```java - public ConcurrentHashMap() { - this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL); - } - ``` +* next 数组的核心就是自己匹配自己,主串代表后缀,模式串代表前缀 +* nextVal 数组的核心就是回退失配 - ```java - // 默认初始化容量 - static final int DEFAULT_INITIAL_CAPACITY = 16; - // 默认负载因子 - static final float DEFAULT_LOAD_FACTOR = 0.75f; - // 默认并发级别 - static final int DEFAULT_CONCURRENCY_LEVEL = 16; - ``` +```java +public class Kmp { + public static void main(String[] args) { + String s = "acababaabc"; + String t = "abaabc"; + //[-1, 0, 0, 1, 1, 2] + System.out.println(Arrays.toString(getNext(t))); + //[-1, 0, -1, 1, 0, 2] + System.out.println(Arrays.toString(getNextVal(t))); + //5 + System.out.println(kmp(s, t)); + } - 说明:并发度就是程序运行时能够**同时更新**ConccurentHashMap且不产生锁竞争的最大线程数,实际上就是ConcurrentHashMap中的分段锁个数。如果并发度设置的过小,会带来严重的锁竞争问题;如果并发度设置的过大,原本位于同一个Segment内的访问会扩散到不同的Segment中,**CPU cache命中率**会下降 + private static int kmp(String s, String t) { + int[] next = getNext(t); + int i = 0, j = 0; + while (i < s.length() && j < t.length()) { + //j==-1时说明第一个位置匹配失败,所以将s的下一个和t的首字符比较 + if (j == -1 || s.charAt(i) == t.charAt(j)) { + i++; + j++; + } else { + //模式串右移,比较s的当前位置与t的next[j]位置 + j = next[j]; + } + } + if (j >= t.length()) { + return i - j + 1; + } + return -1; + } + //next数组 + private static int[] getNext(String t) { + int[] next = new int[t.length()]; + next[0] = -1; + int j = -1; + int i = 0; + while (i < t.length() - 1) { + // 根据已知的前j位推测第j+1位 + // j=-1说明首位就没有匹配,即t[0]!=t[i],说明next[i+1]没有最大前缀,为0 + if (j == -1 || t.charAt(i) == t.charAt(j)) { + // 因为模式串已经匹配到了索引j处,说明之前的位都是相等的 + // 因为是自己匹配自己,所以模式串就是前缀,主串就是后缀,j就是最长公共前缀 + // 当i+1位置不匹配时(i位之前匹配),可以跳转到j+1位置对比,next[i+1]=j+1 + i++; + j++; + next[i] = j; + } else { + //i位置的数据和j位置的不相等,所以回退对比i和next[j]位置的数据 + j = next[j]; + } - ```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; - } - ``` + } + return next; + } + //nextVal + private static int[] getNextVal(String t) { + int[] nextVal = new int[t.length()]; + nextVal[0] = -1; + int j = -1; + int i = 0; + while (i < t.length() - 1) { + if (j == -1 || t.charAt(i) == t.charAt(j)) { + i++; + j++; + // 如果t[i+1] == t[next(i+1)]=next[j+1],回退后仍然失配,所以要继续回退 + if (t.charAt(i) == t.charAt(j)) { + nextVal[i] = nextVal[j]; + } else { + nextVal[i] = j; + } + } else { + j = nextVal[j]; + } + } + return nextVal; + } + /*根据next求nextVal + private static int[] getNextVal(String t, int[] next) { + int[] nextVal = new int[next.length]; + nextVal[0] = -1; + for (int i = 1; i < nextVal.length; i++) { + if (t.charAt(i) == t.charAt(next[i])) { + nextVal[i] = nextVal[next[i]]; + } else { + nextVal[i] = next[i]; + } + } + return nextVal; + }*/ +} +``` -3. put:头插法 +平均和最坏时间复杂度都是 O(m+n) - 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; - } - ``` +参考文章:https://www.cnblogs.com/tangzhengyue/p/4315393.html - 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; - } - ``` +二叉树中,任意一个节点的度要小于等于 2 - * 第一个 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; - } - ``` +### 排序树 +#### 存储结构 +二叉排序树(BST),又称二叉查找树或者二叉搜索树 ++ 每一个节点上最多有两个子节点 ++ 左子树上所有节点的值都小于根节点的值 ++ 右子树上所有节点的值都大于根节点的值 ++ 不存在重复的节点 +二叉查找树 -*** -### CopyOnWrite -#### 原理分析 +*** -CopyOnWriteArrayList 采用了**写入时拷贝**的思想,增删改操作会将底层数组拷贝一份,更改操作在新数组上执行,不影响其它线程的**并发读,读写分离** -CopyOnWriteArraySet 底层对 CopyOnWriteArrayList 进行了包装,装饰器模式 -```java -public CopyOnWriteArraySet() { - al = new CopyOnWriteArrayList(); -} -``` +#### 代码实现 -* 存储结构: +* 节点类: ```java - private transient volatile Object[] array;//保证了读写线程之间的可见性 - ``` + private static class TreeNode { + int key; + TreeNode left; //左节点 + TreeNode right; //右节点 -* 新增数据: + private TreeNode(int key) { + this.key = key; + } + } + ``` + +* 查找节点: ```java - public boolean add(E e) { - final ReentrantLock lock = this.lock; - lock.lock(); - try { - // 获取旧的数组 - Object[] elements = getArray(); - int len = elements.length; - // 拷贝新的数组(这里是比较耗时的操作,但不影响其它读线程) - Object[] newElements = Arrays.copyOf(elements, len + 1); - // 添加新元素 - newElements[len] = e; - // 替换旧的数组 - setArray(newElements); - return true; - } finally { - lock.unlock(); + // 递归查找 + private static TreeNode search(TreeNode root, int key) { + //递归结束的条件 + if (root == null) { + return null; + } + if (key == root.key) { + return root; + } else if (key > root.key) { + return search(root.right, key); + } else { + return search(root.left, key); + } + } + + // 非递归 + private static TreeNode search1(TreeNode root, int key) { + while (root != null) { + if (key == root.key) { + return root; + } else if (key > root.key) { + root = root.right; + } else { + root = root.left; + } } + return null; } ``` -* 读操作: +* 插入节点: ```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); + private static int insert(TreeNode root, int key) { + if (root == null) { + root = new TreeNode(key); + root.left = null; + root.right = null; + return 1; + } else { + if (key == root.key) { + return 0; + } else if (key > root.key) { + return insert(root.right, key); + } else { + return insert(root.left, key); + } } } ``` - 适合读多写少的应用场景 - -* 迭代器: - - CopyOnWriteArrayList 在返回一个迭代器时,会基于创建这个迭代器时内部数组所拥有的数据,创建一个该内部数组当前的快照,然后迭代器遍历的是该快照,而不是内部的数组,所以这种实现方式也存在一定的数据延迟性,即对其他线程并行添加的数据不可见 +* 构造函数: ```java - public Iterator iterator() { - return new COWIterator(getArray(), 0); - } - - // 迭代器会创建一个底层array的快照,故主类的修改不影响该快照 - static final class COWIterator implements ListIterator { - // 内部数组快照 - private final Object[] snapshot; - - //... - // 不支持写操作 - public void remove() { - throw new UnsupportedOperationException(); - } + // 构造函数,返回根节点 + private static TreeNode createBST(int[] arr) { + if (arr.length > 0) { + TreeNode root = new TreeNode(arr[0]); + for (int i = 1; i < arr.length; i++) { + insert(root, arr[i]); + } + return root; + } + return null; } ``` +* 删除节点:要删除节点12,先找到节点19,然后移动并替换节点12 + + 代码链接:https://leetcode-cn.com/submissions/detail/190232548/ + -*** +参考视频:https://www.bilibili.com/video/BV1iJ411E7xW?t=756&p=86 + +图片来源:https://leetcode-cn.com/problems/delete-node-in-a-bst/solution/tu-jie-yi-dong-jie-dian-er-bu-shi-xiu-ga-edtn/ + +*** + -#### 弱一致性 -##### get方法 +### 平衡树 -数据一致性就是读到最新更新的数据: +平衡二叉树(AVL)的特点: -* 强一致性:当更新操作完成之后,任何多个后续进程或者线程的访问都会返回最新的更新过的值 ++ 二叉树左右两个子树的高度差不超过1 ++ 任意节点的左右两个子树都是一颗平衡二叉树 -* 弱一致性:系统并不保证进程或者线程的访问都会返回最新的更新过的值,也不会承诺多久之后可以读到 +平衡二叉树旋转: - ++ 旋转触发时机:当添加一个节点之后,该树不再是一颗平衡二叉树 -| 时间点 | 操作 | -| ------ | ---------------------------- | -| 1 | Thread-0 getArray() | -| 2 | Thread-1 getArray() | -| 3 | Thread-1 setArray(arrayCopy) | -| 4 | Thread-0 array[index] | ++ 平衡二叉树和二叉查找树对比结构图 -Thread-0读到了脏数据 +![平衡二叉树和二叉查找树对比](https://gitee.com/seazean/images/raw/master/Java/平衡二叉树和二叉查找树对比结构图.png) ++ 左旋:将根节点的右侧往左拉,原先的右子节点变成新的父节点,并把多余的左子节点出让,给已经降级的根节点当右子节点 + ![平衡二叉树左旋](https://gitee.com/seazean/images/raw/master/Java/平衡二叉树左旋01.png) -##### 迭代器 +* 右旋:将根节点的左侧往右拉,左子节点变成了新的父节点,并把多余的右子节点出让,给已经降级根节点当左子节点 -```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 - } -} -``` + ![平衡二叉树右旋](https://gitee.com/seazean/images/raw/master/Java/平衡二叉树右旋01.png) -不一定弱一致性就不好 +推荐文章:https://pdai.tech/md/algorithm/alg-basic-tree-balance.html -* 数据库的事务隔离级别都是弱一致性的表现 -* 并发高和一致性是矛盾的,需要权衡 @@ -25697,41 +15222,56 @@ public static void main(String[] args) throws InterruptedException { -#### 安全失败 +### 红黑树 + +红黑树的特点: -在 java.util 包的集合类就都是快速失败的,而 java.util.concurrent 包下的类都是安全失败 +* 每一个节点可以是红或者黑 -* 快速失败:在 A 线程使用**迭代器**对集合进行遍历的过程中,此时 B 线程对集合进行修改(增删改),或者 A 线程在遍历过程中对集合进行修改,都会导致 A 线程抛出 ConcurrentModificationException 异常 - * AbstractList 类中的成员变量 modCount,用来记录 List 结构发生变化的次数,**结构发生变化**是指添加或者删除至少一个元素的操作,或者是调整内部数组的大小,仅仅设置元素的值不算结构发生变化 - * 在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了抛出 CME 异常 -* 安全失败:采用安全失败机制的集合容器,在**迭代器**遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在复制集合上进行遍历。由于迭代时不是对原集合进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,故不会抛 ConcurrentModificationException 异常 ++ 红黑树不是高度平衡的,它的平衡是通过"自己的红黑规则"进行实现的 +红黑树的红黑规则有哪些: +1. 每一个节点或是红色的,或者是黑色的 +2. 根节点必须是黑色 +3. 如果一个节点没有子节点或者父节点,则该节点相应的指针属性值为 Nil,这些 Nil 视为叶节点,每个叶节点(Nil) 是黑色的 +4. 如果某一个节点是红色,那么它的子节点必须是黑色(不能出现两个红色节点相连的情况) +5. 对每一个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点 -*** +红黑树与 AVL 树的比较: +* AVL树是更加严格的平衡,可以提供更快的查找速度,适用于读取查找密集型任务 +* 红黑树只是做到了近似平衡,并不是严格的平衡,红黑树的插入删除比AVL树更便于控制操作,红黑树更适合于插入修改密集型任务 +- 红黑树整体性能略优于AVL树,AVL树的旋转比红黑树的旋转多,更加难以平衡和调试,插入和删除的效率比红黑树慢。 -### Collections +![红黑树](https://gitee.com/seazean/images/raw/master/Java/红黑树结构图.png) -Collections类是用来操作集合的工具类,提供了集合转换成线程安全的方法: -```java - public static Collection synchronizedCollection(Collection c) { - return new SynchronizedCollection<>(c); - } -public static Map synchronizedMap(Map m) { - return new SynchronizedMap<>(m); -} -``` -源码:底层也是对方法进行加锁 +红黑树添加节点的默认颜色为红色,效率高 +![](https://gitee.com/seazean/images/raw/master/Java/红黑树添加节点颜色.png) + + + +**红黑树添加节点后如何保持红黑规则:** + ++ 根节点位置 + + 直接变为黑色 ++ 非根节点位置 + + 父节点为黑色 + + 不需要任何操作,默认红色即可 + + 父节点为红色 + + 叔叔节点为红色 + 1. 将"父节点"设为黑色,将"叔叔节点"设为黑色 + 2. 将"祖父节点"设为红色 + 3. 如果"祖父节点"为根节点,则将根节点再次变成黑色 + + 叔叔节点为黑色 + 1. 将"父节点"设为黑色 + 2. 将"祖父节点"设为红色 + 3. 以"祖父节点"为支点进行旋转 + -```java -public boolean add(E e) { - synchronized (mutex) {return c.add(e);} -} -``` @@ -25739,94 +15279,218 @@ public boolean add(E e) { -### SkipListMap +### 并查集 + +#### 基本实现 + +并查集是一种树型的数据结构,有以下特点: + +* 每个元素都唯一的对应一个结点 +* 每一组数据中的多个元素都在同一颗树中 +* 一个组中的数据对应的树和另外一个组中的数据对应的树之间没有任何联系 +* 元素在树中并没有子父级关系的硬性要求 + + + +可以高效地进行如下操作: + +* 查询元素 p 和元素 q 是否属于同一组 +* 合并元素 p 和元素 q 所在的组 + +存储结构: + + + +合并方式: + + -#### 底层结构 -跳表 SkipList 是一个**有序的链表**,默认升序,底层是链表加多级索引的结构。跳表可以对元素进行快速查询,类似于平衡树,是一种利用空间换时间的算法 -对于单链表,即使链表是有序的,如果查找数据也只能从头到尾遍历链表,所以采用链表上建索引的方式提高效率,跳表的查询时间复杂度是 **O(logn)**,空间复杂度 O(n) +代码实现: -ConcurrentSkipListMap 提供了一种线程安全的并发访问的排序映射表,内部是跳表结构实现,通过 CAS + volatile 保证线程安全 +* 类实现: -平衡树和跳表的区别: + ```java + public class UF { + //记录节点元素和该元素所在分组的标识 + private int[] eleAndGroup; + //记录分组的个数 + private int count; + + //初始化并查集 + public UF(int N) { + //初始化分组数量 + this.count = N; + //初始化eleAndGroup数量 + this.eleAndGroup = new int[N]; + //初始化eleAndGroup中的元素及其所在分组的标识符,eleAndGroup索引作为每个节点的元素 + //每个索引处的值就是该组的索引,就是该元素所在的组的标识符 + for (int i = 0; i < eleAndGroup.length; i++) { + eleAndGroup[i] = i; + } + } + + //查询p所在的分组的标识符 + public int find(int p) { + return eleAndGroup[p]; + } + + //判断并查集中元素p和元素q是否在同一分组中 + public boolean connect(int p, int q) { + return find(p) == find(q); + } + + //把p元素所在分组和q元素所在分组合并 + public void union(int p, int q) { + //判断元素q和p是否已经在同一个分组中,如果已经在同一个分组中,则结束方法就可以了 + if (connect(p, q)) { + return; + } + int pGroup = find(p);//找到p所在分组的标识符 + int qGroup = find(q);//找到q所在分组的标识符 + + //合并组,让p所在组的 所有元素 的组标识符变为q所在分组的标识符 + for (int i = 0; i < eleAndGroup.length; i++) { + if (eleAndGroup[i] == pGroup) { + eleAndGroup[i] = qGroup; + } + } + //分组个数-1 + this.count--; + } + } + ``` -* 对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整;而对跳表的插入和删除,只需要对整个数据结构的局部进行操作 -* 在高并发的情况下,保证整个平衡树的线程安全需要一个全局锁;对于跳表则只需要部分锁,拥有更好的性能 +* 测试代码: -![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentSkipListMap数据结构.png) + ```java + public static void main(String[] args) { + //创建并查集对象 + UF uf = new UF(5); + System.out.println(uf); + + //从控制台录入两个合并的元素,调用union方法合并,观察合并后并查集的分组 + Scanner sc = new Scanner(System.in); + + while (true) { + System.out.println("输入第一个要合并的元素"); + int p = sc.nextInt(); + System.out.println("输入第二个要合并的元素"); + int q = sc.nextInt(); + if (uf.connect(p, q)) { + System.out.println(p + "元素已经和" + q + "元素已经在同一个组"); + continue; + } + uf.union(p, q); + System.out.println("当前并查集中还有:" + uf.count() + "个分组"); + System.out.println(uf); + System.out.println("********************"); + } + } + ``` -BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引指向链表最下面的节点** +最坏情况下 union 算法的时间复杂度也是 O(N^2) -*** +**** -#### 成员变量 +#### 优化实现 -* 标识索引头节点位置 +让每个索引处的节点都指向它的父节点,当 eleGroup[i] = i 时,说明 i 是根节点 - ```java - private static final Object BASE_HEADER = new Object(); - ``` + -* 跳表的顶层索引 +```java +//查询p所在的分组的标识符,递归寻找父标识符,直到找到根节点 +public int findRoot(int p) { + while (p != eleAndGroup[p]) { + p = eleAndGroup[p]; + } + //p == eleGroup[p],说明p是根节点 + return p; +} - ```java - private transient volatile HeadIndex head; - ``` +//判断并查集中元素p和元素q是否在同一分组中 +public boolean connect(int p, int q) { + return findRoot(p) == findRoot(q); +} -* 比较器,为 null 则使用自然排序 +//把p元素所在分组和q元素所在分组合并 +public void union(int p, int q) { + //找到p q对应的根节点 + int pRoot = findRoot(p); + int qRoot = findRoot(q); + if (pRoot == qRoot) { + return; + } + //让p所在树的节点根节点为q的所在的根节点,只需要把根节点改一下,时间复杂度 O(1) + eleAndGroup[pRoot] = qRoot; +} +``` - ```java - final Comparator comparator; - ``` +平均时间复杂度为 O(N),最坏时间复杂度是 O(N^2) -* Node 节点 + - ```java - static final class Node{ - final K key; // key 是 final 的, 说明节点一旦定下来, 除了删除, 不然不会改动 key - volatile Object value; // 对应的 value - volatile Node next; // 下一个节点 - } - ``` +继续优化:路径压缩,保证每次把小树合并到大树 -* 索引节点 Index +```java +public class UF_Tree_Weighted { + private int[] eleAndGroup; + private int count; + private int[] size;//存储每一个根结点对应的树中的保存的节点的个数 - ```java - static class Index{ - final Node node; // 索引指向的节点, - final Index down; // 下边level层的Index,分层索引 - volatile Index right; // 右边的Index - - // 在index本身和succ之间插入一个新的节点newSucc - final boolean link(Index succ, Index newSucc){ - Node n = node; - newSucc.right = succ; - return n.value != null && casRight(succ, newSucc); - } - - // 将当前的节点 index 设置其的 right 为 succ.right 等于删除 succ 节点 - final boolean unlink(Index succ){ - return node.value != null && casRight(succ, succ.right); - } - } - ``` + //初始化并查集 + public UF_Tree_Weighted(int N) { + this.count = N; + this.eleAndGroup = new int[N]; + for (int i = 0; i < eleAndGroup.length; i++) { + eleAndGroup[i] = i; + } + this.size = new int[N]; + //默认情况下,size中每个索引处的值都是1 + for (int i = 0; i < size.length; i++) { + size[i] = 1; + } + } + //查询p所在的分组的标识符,父标识符 + public int findRoot(int p) { + while (p != eleAndGroup[p]) { + p = eleAndGroup[p]; + } + return p; + } -* 头索引节点 HeadIndex + //判断并查集中元素p和元素q是否在同一分组中 + public boolean connect(int p, int q) { + return findRoot(p) == findRoot(q); + } - ```java - static final class HeadIndex extends Index { - final int level;// 标示索引层级,所有的HeadIndex都指向同一个Base_header节点 - HeadIndex(Node node, Index down, Index right, int level) { - super(node, down, right); - this.level = level; - } - } - ``` + //把p元素所在分组和q元素所在分组合并 + public void union(int p, int q) { + //找到p q对应的根节点 + int pRoot = findRoot(p); + int qRoot = findRoot(q); + if (pRoot == qRoot) { + return; + } + //判断pRoot对应的树大还是qRoot对应的树大,最终需要把较小的树合并到较大的树中 + if (size[pRoot] < size[qRoot]) { + eleAndGroup[pRoot] = qRoot; + size[qRoot] += size[pRoot]; + } else { + eleAndGroup[qRoot] = pRoot; + size[pRoot] += size[qRoot]; + } + //组的数量-1、 + this.count--; + } +} +``` @@ -25834,327 +15498,69 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引指向 -#### 成员方法 - -##### 其他方法 +#### 应用场景 -* 构造方法: +并查集存储的每一个整数表示的是一个大型计算机网络中的计算机: - ```java - public ConcurrentSkipListMap() { - this.comparator = null; // comparator为null,使用key的自然序,如字典序 - initialize(); - } - ``` +* 可以通过 connected(int p, int q) 来检测该网络中的某两台计算机之间是否连通 +* 可以调用 union(int p,int q) 使得 p 和 q 之间连通,这样两台计算机之间就可以通信 - ```java - private void initialize() { - keySet = null; - 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); - } - ``` +畅通工程:某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府畅通工程的目标是使全省任何两个城镇间都可以实现交通,但不一定有直接的道路相连,只要互相间接通过道路可达即可,问最少还需要建设多少条道路? -* cpr:排序 + - ```java - // x是比较者,y是被比较者,比较者大于被比较者 返回正数,小于返回负数,相等返回0 - static final int cpr(Comparator c, Object x, Object y) { - return (c != null) ? c.compare(x, y) : ((Comparable)x).compareTo(y); - } - ``` +解题思路: +1. 创建一个并查集 UF_Tree_Weighted(20) +2. 分别调用 union(0,1)、union(6,9)、union(3,8)、union(5,11)、union(2,12)、union(6,10)、union(4,8),表示已经修建好的道路把对应的城市连接起来 +3. 如果城市全部连接起来,那么并查集中剩余的分组数目为 1,所有的城市都在一个树中,只需要获取当前并查集中剩余的数目减去 1,就是还需要修建的道路数目 +```java +public static void main(String[] args)throws Exception { + Scanner sc = new Scanner(System.in); + //读取城市数目,初始化并查集 + int number = sc.nextInt(); + //读取已经修建好的道路数目 + int roadNumber = sc.nextInt(); + UF_Tree_Weighted uf = new UF_Tree_Weighted(number); + //循环读取已经修建好的道路,并调用union方法 + for (int i = 0; i < roadNumber; i++) { + int p = sc.nextInt(); + int q = sc.nextInt(); + uf.union(p,q); + } + //获取剩余的分组数量 + int groupNumber = uf.count(); + //计算出还需要修建的道路 + System.out.println("还需要修建"+(groupNumber-1)+"道路,城市才能相通"); +} +``` -*** +参考视频:https://www.bilibili.com/video/BV1iJ411E7xW?p=142 -##### 添加方法 -* findPredecessor():寻找前驱节点 - 从最上层的头索引开始向右查找(链表的后续索引),如果后续索引的节点的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节点 - for (Index q = head, r = q.right, d;;) { - //2.右索引节点不为空,则进行向下查找 - if (r != null) { - Node n = r.node; - K k = n.key; - //3.n.value为null说明节点n正在删除的过程中 - if (n.value == null) { - //在index层直接删除r索引节点,用在删除节点中 - if (!q.unlink(r)) - break;//重新从 head 节点开始查找,break到步骤1 - //删除节点r成功,获取新的r节点, 回到步骤 2 - //还是从这层索引开始向右遍历, 直到 r == null - r = q.right; - continue; - } - //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,是则说明已经到了数据层,也就是第一层 - if ((d = q.down) == null) - return q.node; - //6.未到数据层, 进行重新赋值向下扫描 - q = d; //q指向d - r = d.right;//r指向q的后续索引节点 - } - } - } - ``` - ```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() +#### 基本介绍 - ```java - public V put(K key, V value) { - // 非空判断,value不能为空 - if (value == null) - throw new NullPointerException(); - return doPut(key, value, false); - } - ``` +Trie 树,也叫字典树,是一种专门处理字符串匹配的树形结构,用来解决在一组字符串集合中快速查找某个字符串的问题,Trie 树的本质就是利用字符串之间的公共前缀,将重复的前缀合并在一起 - ```java - private V doPut(K key, V value, boolean onlyIfAbsent) { - Node z; - if (key == null)// 非空判断,key不能为空 - throw new NullPointerException(); - Comparator cmp = comparator; - // outer循环,处理并发冲突等其他需要重试的情况 - outer: for (;;) { - //0.for (;;) - //1.将 key 对应的前继节点找到, b为前继节点, n是前继节点的next, - // 若没发生条件竞争,最终key在b与n之间 (找到的b在base_level上) - for (Node b = findPredecessor(key, cmp), n = b.next;;) { - // 2.n不为null时b不是链表的最后一个节点 - if (n != null) { - Object v; int c; - //3.获取 n 的右节点 - Node f = n.next; - //4.条件竞争 - // 并发下其他线程在b之后插入节点或直接删除节点n, break到步骤0 - if (n != b.next) - break; - // 若节点n已经删除, 则调用helpDelete进行帮助删除 - if ((v = n.value) == null) { - n.helpDelete(b, f); - break; - } - //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之间 - if ((c = cpr(cmp, key, n.key)) > 0) { - b = n; - n = f; - continue; - } - //7.key的值和n.key相等,则可以直接覆盖赋值 - if (c == 0) { - // onlyIfAbsent默认false, - if (onlyIfAbsent || n.casValue(v, value)) { - @SuppressWarnings("unchecked") V vv = (V)v; - return vv;//返回被覆盖的值 - } - // cas失败,返回0,重试 - break; - } - // else c < 0; fall through - } - //8.此时的情况n.key > key > b.key,对应流程图1中的7 - // 创建z节点指向n - z = new Node(key, value, n); - //9.尝试把b.next从n设置成z - if (!b.casNext(n, z)) - // cas失败,返回到步骤0,重试 - break; - //10.break outer后, 上面的for循环不会再执行, 而后执行下面的代码 - break outer; - } - } - // 以上插入节点已经完成,剩下的任务要根据随机数的值来表示是否向上增加层数与上层索引 - - // 随机数 - int rnd = ThreadLocalRandom.nextSecondarySeed(); - - //如果随机数的二进制与10000000000000000000000000000001进行与运算为0 - //即随机数的二进制最高位与最末尾必须为0,其他位无所谓,就进入该循环 - //如果随机数的二进制最高位与最末位不为0,不增加新节点的层数 - //11.判断是否需要添加level - if ((rnd & 0x80000001) == 0) { - //索引层level,从1开始 - int level = 1, max; - //12.判断最低位前面有几个1,有几个leve就加几:0..0 0001 1110,这是4个,则1+4=5 - // 最大有30个就是 1 + 30 - while (((rnd >>>= 1) & 1) != 0) - ++level; - Index idx = null;//最终指向z节点,就是添加的节点 - HeadIndex h = head;//指向头索引节点 - //13.判断level是否比当前最高索引小,图中max为3 - if (level <= (max = h.level)) { - for (int i = 1; i <= level; ++i) - //根据层数level不断创建新增节点的上层索引,索引的后继索引留空 - //第一次idx为null,也就是下层索引为空,第二次把上次的索引作为下层索引 - idx = new Index(z, idx, null); - // 循环以后的索引结构 - // index-3 ← idx - // ↓ - // index-2 - // ↓ - // index-1 - // ↓ - // z-node - } - //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了 - for (int i = 1; i <= level; ++i) - idxs[i] = idx = new Index(z, idx, null); - // index-4 ← idx - // ↓ - // index-3 - // ↓ - // index-2 - // ↓ - // index-1 - // ↓ - // z-node - - for (;;) { - h = head; - //获取头索引的层数 - int oldLevel = h.level; - // 如果level <= oldLevel,说明其他线程进行了index层增加操作,退出循环 - if (level <= oldLevel) - break; - //定义一个新的头索引节点 - HeadIndex newh = h; - //获取头索引的节点,就是BASE_HEADER - Node oldbase = h.node; - // 升级baseHeader索引,升高一级,并发下可能升高多级 - for (int j = oldLevel+1; j <= level; ++j) - newh = new HeadIndex(oldbase, newh, idxs[j], j); - // 执行完for循环之后,baseHeader 索引长这个样子.. - // index-4 → index-4 ← idx - // ↓ ↓ - // index-3 index-3 - // ↓ ↓ - // index-2 index-2 - // ↓ ↓ - // index-1 index-1 - // ↓ ↓ - // baseHeader → .... → z-node - - //cas成功后,map.head字段指向最新的headIndex,baseHeader的index-4s - if (casHead(h, newh)) { - //h指向最新的 index-4 节点 - h = newh; - //idx指向z-node的index-3节点, - //因为从index-3-index-1的这些z-node索引节点 都没有插入到索引链表 - idx = idxs[level = oldLevel]; - break; - } - } - } - //15.把新加的索引插入索引链表中,有上述两种情况,一种索引高度不变,另一种是高度加1 - splice: for (int insertionLevel = level;;) { - //获取头索引的层数, 情况1是3,情况2是4 - int j = h.level; - for (Index q = h, r = q.right, t = idx;;) { - //如果头索引为null或者新增节点索引为null,退出插入索引的总循环 - if (q == null || t == null) - //此处表示有其他线程删除了头索引或者新增节点的索引 - break splice; - //头索引的链表后续索引存在,如果是新层则为新节点索引,如果是老层则为原索引 - if (r != null) { - //获取r的节点 - Node n = r.node; - //插入的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,向右扫描 - if (c > 0) { - q = r; - r = r.right; - continue; - } - } - // 执行到这里,说明key < n.key,判断是否第j层插入新增节点的前置索引 - if (j == insertionLevel) { - // 将新索引节点t插入q r之间 - if (!q.link(r, t)) - break; - //如果新增节点的值为null,表示该节点已经被其他线程删除 - if (t.node.value == null) { - findNode(key); - break splice; - } - // 插入层逐层自减,当为最底层时退出循环 - if (--insertionLevel == 0) - break splice; - } - //其他节点随着插入节点的层数下移而下移 - if (--j >= insertionLevel && j < level) - t = t.down; - q = q.down; - r = q.right; - } - } - } - return null; - } - ``` +* 根节点不包含任何信息 +* 每个节点表示一个字符串中的字符,从**根节点到红色节点的一条路径表示一个字符串** +* 红色节点并不都是叶子节点 -* findNode() + - ```java - private Node findNode(Object key) { - //原理与doGet相同,无非是findNode返回节点,doGet返回value - if ((c = cpr(cmp, key, n.key)) == 0) - return n; - } - ``` + +注意:要查找的是字符串“he”,从根节点开始,沿着某条路径来匹配,可以匹配成功。但是路径的最后一个节点“e”并不是红色的,也就是说,“he”是某个字符串的前缀子串,但并不能完全匹配任何字符串 @@ -26162,231 +15568,156 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引指向 -##### 获取方法 - -* get(key) +#### 实现Trie - 寻找 key 的前继节点 b (这时b.next = null || b.next > key, 则说明不存key对应的 Node) - - 接着就判断 b, b.next 与 key之间的关系(其中有些 helpDelete操作) +通过一个下标与字符一一映射的数组,来存储子节点的指针 - ```java - public V get(Object key) { - return doGet(key); - } - ``` + -* doGet() +时间复杂度是 O(n)(n 表示要查找字符串的长度) - ```java - private V doGet(Object key) { - if (key == null) - throw new NullPointerException(); - Comparator cmp = comparator; - outer: for (;;) { - //1.找到最底层节点的前置节点 - for (Node b = findPredecessor(key, cmp), n = b.next;;) { - Object v; int c; - //2.如果该前置节点的链表后续节点为null,说明不存在该节点 - if (n == null) - break outer; - //b → n → f - Node f = n.next; - //3.如果n不为前置节点的后续节点,表示已经有其他线程删除了该节点 - if (n != b.next) - break; - //4.如果后续节点的值为null,删除该节点 - if ((v = n.value) == null) { - n.helpDelete(b, f); - break; - } - //5.如果前置节点已被其他线程删除,重新循环 - if (b.value == null || v == n) - break; - //6.如果要获取的key与后续节点的key相等,返回节点的value - if ((c = cpr(cmp, key, n.key)) == 0) { - @SuppressWarnings("unchecked") V vv = (V)v; - return vv; - } - //7.key < n.key,说明被其他线程删除了,或者不存在该节点 - if (c < 0) - break outer; - b = n; - n = f; - } - } - return null; - } - ``` +```java +public class Trie { + private TrieNode root = new TrieNode('/'); - + //插入一个字符 + public void insert(char[] chars) { + TrieNode p = root; + for (int i = 0; i < chars.length; i++) { + //获取字符的索引位置 + int index = chars[i] - 'a'; + if (p.children[index] == null) { + TrieNode node = new TrieNode(chars[i]); + p.children[index] = node; + } + p = p.children[index]; + } + p.isEndChar = true; + } -**** + //查找一个字符串 + public boolean find(char[] chars) { + TrieNode p = root; + for (int i = 0; i < chars.length; i++) { + int index = chars[i] - 'a'; + if (p.children[index] == null) { + return false; + } + p = p.children[index]; + } + if (p.isEndChar) { + //完全匹配 + return true; + } else { + // 不能完全匹配,只是前缀 + return false; + } + } + private class TrieNode { + char data; + TrieNode[] children = new TrieNode[26];//26个英文字母 + boolean isEndChar = false;//结尾字符为true + public TrieNode(char data) { + this.data = data; + } + } +} +``` -##### 删除方法 -* remove() - ```java - public V remove(Object key) { - return doRemove(key, null); - } - final V doRemove(Object key, Object value) { - if (key == null) - throw new NullPointerException(); - Comparator cmp = comparator; - outer: for (;;) { - //1.找到最底层目标节点的前置节点,b.key < key - for (Node b = findPredecessor(key, cmp), n = b.next;;) { - Object v; int c; - //2.如果该前置节点的链表后续节点为null,退出循环 - if (n == null) - break outer; - //b → n → f - Node f = n.next; - if (n != b.next) // inconsistent read - break; - if ((v = n.value) == null) { // n is deleted - n.helpDelete(b, f); - break; - } - if (b.value == null || v == n) // b is deleted - break; - //3.key < n.key,说明被其他线程删除了,或者不存在该节点 - if ((c = cpr(cmp, key, n.key)) < 0) - break outer; - //4.key > n.key,继续向后扫描 - if (c > 0) { - b = n; - n = f; - continue; - } - //5.到这里是 key = n.key,value是n.value - if (value != null && !value.equals(v)) - break outer; - //6.把n节点的value置空 - if (!n.casValue(v, null)) - break; - //7.给n添加一个删除标志mark,mark.next=f,然后把b.next设置为f,成功后n出队 - if (!n.appendMarker(f) || !b.casNext(n, f)) - //对key对应的index进行删除 - findNode(key); - else { - //进行操作失败后通过findPredecessor中进行index的删除 - findPredecessor(key, cmp); - if (head.right == null) - //进行headIndex 对应的index 层的删除 - tryReduceLevel(); - } - @SuppressWarnings("unchecked") V vv = (V)v; - return vv; - } - } - return null; - } - ``` +*** - 经过 findPredecessor() 中的 unlink() 后索引已经被删除 - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentSkipListMap-remove流程.png) -* appendMarker() +#### 优化Trie - ```java - //添加删除标记节点 - boolean appendMarker(Node f) { - //通过CAS生成一个key为null,value为this,next为f的标记节点 - return casNext(f, new Node(f)); - } - ``` - -* helpDelete() +Trie 树是非常耗内存,采取空间换时间的思路。Trie 树的变体有很多,可以在一定程度上解决内存消耗的问题。比如缩点优化,对只有一个子节点的节点,而且此节点不是一个串的结束节点,可以将此节点与子节点合并 - ```java - //将添加了删除标记的节点清除 - void helpDelete(Node b, Node f) { - //this节点的后续节点为f,且本身为b的后续节点,一般都是正确的,除非被别的线程删除 - if (f == next && this == b.next) { - //如果n还还没有被标记 - if (f == null || f.value != f) - casNext(f, new Node(f)); - else - //通过CAS,将b的下一个节点n变成f.next,即成为图中的样式 - b.casNext(this, f.next); - } - } - ``` - -* tryReduceLevel() +![](https://gitee.com/seazean/images/raw/master/Java/Tree-字典树缩点优化.png) - ```java - private void tryReduceLevel() { - HeadIndex h = head; - HeadIndex d; - HeadIndex e; - if (h.level > 3 && - (d = (HeadIndex)h.down) != null && - (e = (HeadIndex)d.down) != null && - e.right == null && - d.right == null && - h.right == null && - //设置头索引 - casHead(h, d) && - //重新检查 - h.right != null) - //重新检查返回true,说明其他线程增加了索引层级,把索引头节点设置回来 - casHead(d, h); - } - ``` +参考文章:https://time.geekbang.org/column/article/72414 -参考文章:https://my.oschina.net/u/3768341/blog/3135659 -参考视频:https://www.bilibili.com/video/BV1Er4y1P7k1 +*** +## 图 -*** +图的邻接表形式: +```java +public class AGraph { + private VertexNode[] adjList; //邻接数组 + private int vLen, eLen; //顶点数和边数 + public AGraph(int vLen, int eLen) { + this.vLen = vLen; + this.eLen = eLen; + adjList = new VertexNode[vLen]; + } + //弧节点 + private class ArcNode { + int adjVex; //该边所指向的顶点的位置 + ArcNode nextArc; //下一条边(弧) + //int info //添加权值 -### NoBlocking + public ArcNode(int adjVex) { + this.adjVex = adjVex; + nextArc = null; + } + } -#### 非阻塞队列 + //表顶点 + private class VertexNode { + char data; //顶点信息 + ArcNode firstArc; //指向第一条边的指针 -并发编程中,需要用到安全的队列,实现安全队列可以使用2种方式: + public VertexNode(char data) { + this.data = data; + firstArc = null; + } + } +} +``` -* 加锁,这种实现方式是阻塞队列 -* 使用循环CAS算法实现,这种方式是非阻塞队列 +图的邻接矩阵形式: -ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,采用先进先出的规则对节点进行排序,当添加一个元素时,会添加到队列的尾部,当获取一个元素时,会返回队列头部的元素 +```java +public class MGraph { + private int[][] edges; //邻接矩阵定义,有权图将int改为float + private int vLen; //顶点数 + private int eLen; //边数 + private VertexNode[] vex; //存放节点信息 -补充:ConcurrentLinkedDeque 是双向链表结构的无界并发队列 + public MGraph(int vLen, int eLen) { + this.vLen = vLen; + this.eLen = eLen; + this.edges = new int[vLen][vLen]; + this.vex = new VertexNode[vLen]; + } -ConcurrentLinkedQueue使用约定: + private class VertexNode { + int num; //顶点编号 + String info; //顶点信息 -1. 不允许null入列 -2. 队列中所有未删除的节点的item都不能为null且都能从head节点遍历到 -3. 删除节点是将item设置为null,队列迭代时跳过item为null节点 -4. head节点跟tail不一定指向头节点或尾节点,可能存在滞后性 + public VertexNode(int num) { + this.num = num; + this.info = null; + } + } +} +``` -ConcurrentLinkedQueue由head节点和tail节点组成,每个节点(Node)由节点元素(item)和指向下一个节点的引用(next)组成,组成一张链表结构的队列 -```java -private transient volatile Node head; -private transient volatile Node tail; -private static class Node { - volatile E item; - volatile Node next; - //..... -} -``` +图相关的算法需要很多的流程图,此处不再一一列举,推荐参考书籍《数据结构高分笔记》 @@ -26394,40 +15725,15 @@ private static class Node { -#### 构造方法 +## 位图 -* 无参构造方法: +### 基本介绍 - ```java - public ConcurrentLinkedQueue() { - // 默认情况下head节点存储的元素为空,tail节点等于head节点 - head = tail = new Node(null); - } - ``` +布隆过滤器:一种数据结构,是一个很长的二进制向量(位数组)和一系列随机映射函数(哈希函数),既然是二进制,每个空间存放的不是0就是1,但是初始默认值都是0,所以布隆过滤器不存数据只存状态 -* 有参构造方法 + - ```java - public ConcurrentLinkedQueue(Collection c) { - Node h = null, t = null; - // 遍历节点 - for (E e : c) { - checkNotNull(e); - Node newNode = new Node(e); - if (h == null) - h = t = newNode; - else { - // 单向链表 - t.lazySetNext(newNode); - t = newNode; - } - } - if (h == null) - h = t = new Node(null); - head = h; - tail = t; - } - ``` +这种数据结构是高效且性能很好的,但缺点是具有一定的错误识别率和删除难度。并且,理论情况下,添加到集合中的元素越多,误报的可能性就越大 @@ -26435,62 +15741,32 @@ private static class Node { -#### 入队方法 +### 工作流程 -与传统的链表不同,单线程入队的工作流程: +向布隆过滤器中添加一个元素key时,会通过多个hash函数得到多个哈希值,在位数组中把对应下标的值置为 1 -* 将入队节点设置成当前队列尾节点的下一个节点 -* 更新tail节点,如果tail节点的next节点不为空,则将入队节点设置成tail节点;如果tail节点的next节点为空,则将入队节点设置成tail的next节点,所以tail节点不总是尾节点,**存在滞后性** +![](https://gitee.com/seazean/images/raw/master/DB/Redis-布隆过滤器添加数据.png) -```java -public boolean offer(E e) { - checkNotNull(e); - // 创建入队节点 - final Node newNode = new Node(e); - - // 循环CAS直到入队成功 - for (Node t = tail, p = t;;) { - // p用来表示队列的尾节点,初始情况下等于tail节点,q 是p的next节点 - Node q = p.next; - // 判断p是不是尾节点 - if (q == null) { - // p是尾节点,设置p节点的下一个节点为新节点 - // 设置成功则casNext返回true,否则返回false,说明有其他线程更新过尾节点 - // 继续寻找尾节点,继续CAS - if (p.casNext(null, newNode)) { - // 首次添加时,p等于t,不进行尾节点更新,所以所尾节点存在滞后性 - if (p != t) - // 将tail设置为新入队的节点,设置失败表示其他线程更新了tail节点 - casTail(t, newNode); - return true; - } - } - else if (p == q) - // 当tail不指向最后节点时,如果执行出列操作,可能将tail也移除,tail不在链表中 - // 此时需要对tail节点进行复位,复位到head节点 - p = (t != (t = tail)) ? t : head; - else - // 推动tail尾节点往队尾移动 - p = (p != t && t != (t = tail)) ? t : q; - } -} -``` +布隆过滤器查询一个数据,是否在二进制的集合中,查询过程如下: -图解入队: +- 通过 K 个哈希函数计算该数据,对应计算出的 K 个hash值 +- 通过 hash 值找到对应的二进制的数组下标 +- 判断方法:如果存在一处位置的二进制数据是0,那么该数据不存在。如果都是1,该数据存在集合中 -![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentLinkedQueue入队操作1.png) +布隆过滤器优缺点: -![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentLinkedQueue入队操作2.png) +* 优点: + * 二进制组成的数组,占用内存极少,并且插入和查询速度都足够快 + * 去重方便:当字符串第一次存储时对应的位数组下标设置为 1,当第二次存储相同字符串时,因为对应位置已设置为 1,所以很容易知道此值已经存在 +* 缺点: + * 随着数据的增加,误判率会增加:添加数据是通过计算数据的hash值,不同的字符串可能哈希出来的位置相同,导致无法确定到底是哪个数据存在,**这种情况可以适当增加位数组大小或者调整哈希函数** + * 无法删除数据:可能存在几个数据占据相同的位置,所以删除一位会导致很多数据失效 -![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentLinkedQueue入队操作3.png) +* 总结:**布隆过滤器判断某个元素存在,小概率会误判。如果判断某个元素不在,那这个元素一定不在** -当tail节点和尾节点的距离**大于等于1**时(每入队两次)更新tail,可以减少CAS更新tail节点的次数,提高入队效率 -线程安全问题: -* 线程1线程2同时入队,无论从哪个位置开始并发入队,都可以循环CAS,直到入队成功,线程安全 -* 线程1遍历,线程2入队,所以造成 ConcurrentLinkedQueue 的size是变化,需要加锁保证安全 -* 线程1线程2同时出列,线程也是安全的 +参考文章:https://www.cnblogs.com/ysocean/p/12594982.html @@ -26498,147 +15774,78 @@ public boolean offer(E e) { -#### 出队方法 +### Guava -出队列的就是从队列里返回一个节点元素,并清空该节点对元素的引用,并不是每次出队都更新head节点 +引入 Guava 的依赖: -* 当head节点里有元素时,直接弹出head节点里的元素,而不会更新head节点 -* 当head节点里没有元素时,出队操作才会更新head节点 +```xml + + com.google.guava + guava + 28.0-jre + +``` -**批处理方式**可以减少使用CAS更新head节点的消耗,从而提高出队效率 +指定误判率为(0.01): ```java -public E poll() { - restartFromHead: - for (;;) { - // p节点表示首节点,即需要出队的节点 - for (Node h = head, p = h, q;;) { - E item = p.item; - // 如果p节点的元素不为null,则通过CAS来设置p节点引用元素为null,成功返回item - if (item != null && p.casItem(item, null)) { - if (p != h) - // 对head进行移动 - updateHead(h, ((q = p.next) != null) ? q : p); - return item; - } - // 如果头节点的元素为空或头节点发生了变化,这说明头节点被另外一个线程修改了 - // 那么获取p节点的下一个节点,如果p节点的下一节点为null,则表明队列已经空了 - else if ((q = p.next) == null) { - updateHead(h, p); - return null; - } - // 第一轮操作失败,下一轮继续,调回到循环前 - else if (p == q) - continue restartFromHead; - // 如果下一个元素不为空,则将头节点的下一个节点设置成头节点 - else - p = q; - } - } -} -final void updateHead(Node h, Node p) { - if (h != p && casHead(h, p)) - // 将旧结点h的next域指向为h - h.lazySetNext(h); +public static void main(String[] args) { + // 创建布隆过滤器对象 + BloomFilter filter = BloomFilter.create( + Funnels.integerFunnel(), + 1500, + 0.01); + // 判断指定元素是否存在 + System.out.println(filter.mightContain(1)); + System.out.println(filter.mightContain(2)); + // 将元素添加进布隆过滤器 + filter.put(1); + filter.put(2); + System.out.println(filter.mightContain(1)); + System.out.println(filter.mightContain(2)); } ``` -在更新完head之后,会将旧的头结点h的next域指向为h,图中所示的虚线也就表示这个节点的自引用,被移动的节点(item为null的节点)会被GC回收 - -![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentLinkedQueue出队操作1.png) - - - - - -如果这时,有一个线程来添加元素,通过tail获取的next节点则仍然是它本身,这就出现了**p == q** 的情况,出现该种情况之后,则会触发执行head的更新,将p节点重新指向为head - -参考文章:https://www.jianshu.com/p/231caf90f30b - *** -#### 成员方法 - -* peek() - - peek操作会改变head指向,执行peek()方法后head会指向第一个具有非空元素的节点 - - ```java - // 获取链表的首部元素,只读取而不移除 - public E peek() { - restartFromHead: - for (;;) { - for (Node h = head, p = h, q;;) { - E item = p.item; - if (item != null || (q = p.next) == null) { - // 更改h的位置为非空元素节点 - updateHead(h, p); - return item; - } - else if (p == q) - continue restartFromHead; - else - p = q; - } - } - } - ``` +### 实现布隆 -* size() +```java +class MyBloomFilter { + //布隆过滤器容量 + private static final int DEFAULT_SIZE = 2 << 28; + //bit数组,用来存放key + private static BitSet bitSet = new BitSet(DEFAULT_SIZE); + //后面hash函数会用到,用来生成不同的hash值,随意设置 + private static final int[] ints = {1, 6, 16, 38, 58, 68}; - 用来获取当前队列的元素个数,因为整个过程都没有加锁,在并发环境中从调用size方法到返回结果期间有可能增删元素,导致统计的元素个数不精确 + //add方法,计算出key的hash值,并将对应下标置为true + public void add(Object key) { + Arrays.stream(ints).forEach(i -> bitSet.set(hash(key, i))); + } - ```java - public int size() { - int count = 0; - // first()获取第一个具有非空元素的节点,若不存在,返回null - // succ(p)方法获取p的后继节点,若p == p的后继节点,则返回head - // 类似遍历链表 - for (Node p = first(); p != null; p = succ(p)) - if (p.item != null) - // 最大返回Integer.MAX_VALUE - if (++count == Integer.MAX_VALUE) - break; - return count; - } - ``` + //判断key是否存在,true不一定说明key存在,但是false一定说明不存在 + public boolean isContain(Object key) { + boolean result = true; + for (int i : ints) { + //短路与,只要有一个bit位为false,则返回false + result = result && bitSet.get(hash(key, i)); + } + return result; + } -* remove() + //hash函数,借鉴了hashmap的扰动算法 + private int hash(Object key, int i) { + int h; + return key == null ? 0 : (i * (DEFAULT_SIZE - 1) & ((h = key.hashCode()) ^ (h >>> 16))); + } +} - ```java - public boolean remove(Object o) { - // 删除的元素不能为null - if (o != null) { - Node next, pred = null; - for (Node p = first(); p != null; pred = p, p = next) { - boolean removed = false; - E item = p.item; - // 节点元素不为null - if (item != null) { - // 若不匹配,则获取next节点继续匹配 - if (!o.equals(item)) { - next = succ(p); - continue; - } - // 若匹配,则通过CAS操作将对应节点元素置为null - removed = p.casItem(item, null); - } - // 获取删除节点的后继节点 - next = succ(p); - // 将被删除的节点移除队列 - if (pred != null && next != null) // unlink - pred.casNext(p, next); - if (removed) - return true; - } - } - return false; - } - ``` +``` @@ -26646,7 +15853,7 @@ final void updateHead(Node h, Node p) { -*** +**** @@ -28054,51 +17261,52 @@ Java中 的 Object 类中提供了 `clone()` 方法来实现浅克隆,实现 C ## 结构型 -### 代理模式 - -#### 基本介绍 +### 模式分类 结构型模式描述如何将类或对象按某种布局组成更大的结构,分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合方式来组合对象。由于组合关系或聚合关系比继承关系耦合度低,满足合成复用原则,所以对象结构型模式比类结构型模式具有更大的灵活性 结构型模式分为 7 种:代理模式、适配器模式、装饰者模式、桥接模式、外观模式、组合模式、享元模式 -代理模式:由于某些原因需要给某对象提供一个代理以控制对该对象的访问,访问对象不适合或者不能直接引用为目标对象,代理对象作为访问对象和目标对象之间的中介 -Java中的代理按照代理类生成时机不同又分为静态代理和动态代理,静态代理代理类在编译期就生成,而动态代理代理类则是在Java运行时动态生成,动态代理又有JDK代理和CGLib代理两种 -代理(Proxy)模式分为三种角色: +*** -* 抽象主题(Subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法 -* 真实主题(Real Subject)类: 实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象 -* 代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,可以访问、控制或扩展真实主题的功能 +### 代理模式 -*** +#### 静态代理 +代理模式:由于某些原因需要给某对象提供一个代理以控制对该对象的访问,访问对象不适合或者不能直接引用为目标对象,代理对象作为访问对象和目标对象之间的中介 +Java 中的代理按照代理类生成时机不同又分为静态代理和动态代理,静态代理代理类在编译期就生成,而动态代理代理类则是在 Java 运行时动态生成,动态代理又有 JDK 代理和 CGLib 代理两种 -#### 静态代理 +代理(Proxy)模式分为三种角色: + +* 抽象主题(Subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法 +* 真实主题(Real Subject)类: 实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象 +* 代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,可以访问、控制或扩展真实主题的功能 买票案例,火车站是目标对象,代售点是代理对象 * 卖票接口: ```java - public interface SellTickets { void sell();} + public interface SellTickets { + void sell(); + } ``` * 火车站,具有卖票功能,需要实现SellTickets接口 ```java public class TrainStation implements SellTickets { - public void sell() { System.out.println("火车站卖票"); } } ``` - + * 代售点: ```java @@ -28137,7 +17345,7 @@ Java中的代理按照代理类生成时机不同又分为静态代理和动态 Java 中提供了一个动态代理类 Proxy,Proxy 并不是代理对象的类,而是提供了一个创建代理对象的静态方法 newProxyInstance() 来获取代理对象 -`static Object newProxyInstance(ClassLoader loader,Class[] interfaces, InvocationHandler h) ` +`static Object newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h) ` * 参数一:类加载器,负责加载代理类 * 参数二:被代理业务对象的**全部实现的接口**,代理对象与真实对象实现相同接口,知道为哪些方法做代理 @@ -28158,7 +17366,7 @@ Java 中提供了一个动态代理类 Proxy,Proxy 并不是代理对象的类 station.getClass().getInterfaces(), new InvocationHandler() { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - System.out.println("代理点收取服务费用(JDK动态代理方式)"); + System.out.println("代理点(JDK动态代理方式)"); //执行真实对象 Object result = method.invoke(station, args); return result; @@ -28190,11 +17398,11 @@ Java 中提供了一个动态代理类 Proxy,Proxy 并不是代理对象的类 ##### 原理解析 -JDK动态代理方式的优缺点: +JDK 动态代理方式的优缺点: - 优点:可以为任意的接口实现类对象做代理,也可以为被代理对象的所有接口的所有方法做代理,动态代理可以在不改变方法源码的情况下,实现对方法功能的增强,提高了软件的可扩展性,Java 反射机制可以生成任意类型的动态代理类 - 缺点:**只能针对接口或者接口的实现类对象做代理对象**,普通类是不能做代理对象的 -- 原因:**生成的代理类继承了Proxy**,java 是单继承的,所以JDK动态代理只能代理接口 +- 原因:**生成的代理类继承了 Proxy**,java 是单继承的,所以 JDK 动态代理只能代理接口 ProxyFactory 不是代理模式中的代理类,而代理类是程序在运行过程中动态的在内存中生成的类,可以通过 Arthas 工具查看代理类结构: @@ -28299,8 +17507,8 @@ CGLIB 的优缺点 * 优点: * CGLIB 动态代理**不限定**是否具有接口,可以对任意操作进行增强 * CGLIB 动态代理无需要原始被代理对象,动态创建出新的代理对象 - * JDKProxy仅对接口方法做增强,cglib对所有方法做增强,包括Object类中的方法 (toString、hashCode) -* 缺点:CGLIB 不能对声明为 final 的类或者方法进行代理,因为 CGLIB 原理是动态生成被代理类的子类,继承被代理类 + * JDKProxy 仅对接口方法做增强,CGLIB 对所有方法做增强,包括 Object 类中的方法,toString、hashCode 等 +* 缺点:CGLIB 不能对声明为 final 的类或者方法进行代理,因为 CGLIB 原理是**动态生成被代理类的子类,继承被代理类** @@ -28320,9 +17528,10 @@ CGLIB 的优缺点 * 动态代理和静态代理 - 动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理(InvocationHandler.invoke),在接口方法数量比较多的时候,可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转 + * 动态代理将接口中声明的所有方法都被转移到一个集中的方法中处理(InvocationHandler.invoke),在接口方法数量比较多的时候,可以进行灵活处理,不需要像静态代理那样每一个方法进行中转 - 如果接口增加一个方法,静态代理模式除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。而动态代理不会出现该问题 + * 静态代理是在编译时就已经将接口、代理类、被代理类的字节码文件确定下来 + * 动态代理是程序在运行后通过反射创建字节码文件交由 JVM 加载 代理模式的优缺点: diff --git a/Prog.md b/Prog.md new file mode 100644 index 0000000..9956959 --- /dev/null +++ b/Prog.md @@ -0,0 +1,11285 @@ +# JUC + +## 进程 + +### 概述 + +进程:程序是静止的,进程实体的运行过程就是进程,是系统进行资源分配的基本单位 + +进程的特征:动态性、并发性、独立性、异步性、结构性 + +**线程**:线程是属于进程的,是一个基本的CPU执行单元,是程序执行流的最小单元。线程是进程中的一个实体,是被系统独立调度和分配的基本单位,线程本身不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其他线程共享进程所拥有的全部资源。 + +关系:一个进程可以包含多个线程,这就是多线程,比如看视频是进程,图画、声音、广告等就是多个线程 + +线程的作用:使多道程序更好的并发执行,提高资源利用率和系统吞吐量,增强操作系统的并发性能 + +**并发并行**: + +* 并行:在同一时刻,有多个指令在多个CPU上同时执行 +* 并发:在同一时刻,有多个指令在单个CPU上交替执行 + +**同步异步**: + +* 需要等待结果返回,才能继续运行就是同步 +* 不需要等待结果返回,就能继续运行就是异步 + + + +*** + + + +### 对比 + +线程进程对比: + +* 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集 + +* 进程拥有共享的资源,如内存空间等,供其内部的线程共享 + +* 进程间通信较为复杂 + + 同一台计算机的进程通信称为 IPC(Inter-process communication) + + * 信号量:信号量是一个计数器,用于多进程对共享数据的访问,解决同步相关的问题并避免竞争条件 + * 共享存储:多个进程可以访问同一块内存空间,需要使用信号量用来同步对共享存储的访问 + * 管道通信:管道是用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,pipe文件 + * 匿名管道(Pipes) :用于具有亲缘关系的父子进程间或者兄弟进程之间的通信,只支持半双工通信 + * 命名管道(Names Pipes):以磁盘文件的方式存在,可以实现本机任意两个进程通信,遵循FIFO + * 消息队列:消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识,对比管道: + * 匿名管道存在于内存中的文件;命名管道存在于实际的磁盘介质或者文件系统;消息队列存放在内核中,只有在内核重启(操作系统重启)或者显示地删除一个消息队列时,该消息队列才被真正删除 + * 读进程可以根据消息类型有选择地接收消息,而不像 FIFO 那样只能默认地接收 + + 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP + + * 套接字:与其它通信机制不同的是,它可用于不同机器间的进程通信 + +* 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量 + + Java中的通信机制:volatile、等待/通知机制、join方式、InheritableThreadLocal、MappedByteBuffer + +* 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低 + + + + + +*** + + + + + +## 线程 + +### 创建线程 + +#### 三种方式 + +运行一个程序就是开启一个进程,在进程中创建线程的方式有三种: + +1. 直接定义一个类继承线程类Thread,重写run()方法,创建线程对象,调用线程对象的start()方法启动线程 +2. 定义一个线程任务类实现Runnable接口,重写run()方法,创建线程任务对象,把线程任务对象包装成线程对象, 调用线程对象的start()方法启动线程 +3. 实现Callable接口 + + + + + +#### Thread + +Thread创建线程方式:创建线程类,匿名内部类方式 + +* **start() 方法底层其实是给 CPU 注册当前线程,并且触发 run() 方法执行** +* 线程的启动必须调用 start() 方法,如果线程直接调用 run() 方法,相当于变成了普通类的执行,此时将只有主线程在执行该线程 +* 建议线程先创建子线程,主线程的任务放在之后,否则主线程(main)永远是先执行完 + +Thread构造器: + +* `public Thread()` +* `public Thread(String name)` + +```java +puclic 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) + } + // main线程输出放在上面 就变成有先后顺序了 + } +} +class MyThread extends Thread{ + @Override + public void run() { + for(int i = 0 ; i < 100 ; i++ ){ + System.out.println("子线程输出:"+i) + } + } +} +``` + +继承 Thread 类的优缺点: + +* 优点:编码简单 +* 缺点:线程类已经继承了Thread类无法继承其他类了,功能不能通过继承拓展(单继承的局限性) + + + +*** + + + +#### Runnable + +Runnable创建线程方式:创建线程类,匿名内部类方式 + +**Thread类本身也是实现了Runnable接口** + +Thread的构造器: + +* `public Thread(Runnable target)` +* `public Thread(Runnable target, String name)` + +```java +public class ThreadDemo { + public static void main(String[] args) { + Runnable target = new MyRunnable(); + Thread t1 = new Thread(target,"1号线程"); + t1.start(); + Thread t2 = new Thread(target);//Thread-0 + } +} + +public class MyRunnable implements Runnable{ + @Override + public void run() { + for(int i = 0 ; i < 10 ; i++ ){ + System.out.println(Thread.currentThread().getName()+"->"+i); + } + } +} +``` + +* 缺点:代码复杂一点。 + +* 优点: + + 1. 线程任务类只是实现了Runnable接口,可以继续继承其他类,避免了单继承的局限性 + + 2. 同一个线程任务对象可以被包装成多个线程对象 + + 3. 适合多个多个线程去共享同一个资源 + + 4. 实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码和线程独立 + + 5. 线程池可以放入实现Runnable或Callable线程任务对象 + +​ + +**** + + + +#### Callable + +实现Callable接口: + +1. 定义一个线程任务类实现Callable接口,申明线程执行的结果类型 +2. 重写线程任务类的call方法,这个方法可以直接返回执行的结果 +3. 创建一个Callable的线程任务对象 +4. 把Callable的线程任务对象包装成一个未来任务对象 +5. 把未来任务对象包装成线程对象 +6. 调用线程的start()方法启动线程 + +`public FutureTask(Callable callable)`:未来任务对象,在线程执行完后**得到线程的执行结果** + +* 其实就是Runnable对象,这样被包装成未来任务对象 + +`public V get()`:同步等待 task 执行完毕的结果 + +* 如果在线程中获取另一个线程执行结果,会阻塞等待,用于线程同步 + +优缺点: + +* 优点:同Runnable,并且能得到线程执行的结果 +* 缺点:编码复杂 + +```java +public class ThreadDemo { + public static void main(String[] args) { + Callable call = new MyCallable(); + FutureTask task = new FutureTask<>(call); + Thread t = new Thread(task); + t.start(); + try { + String s = task.get(); // 获取call方法返回的结果(正常/异常结果) + System.out.println(s); + } catch (Exception e) { + e.printStackTrace(); + } + } + +public class MyCallable implements Callable { + @Override//重写线程任务类方法 + public String call() throws Exception { + return Thread.currentThread().getName() + "->" + "Hello World"; + } +} +``` + + + +*** + + + +### 运行原理 + +JVM 中由堆、栈、方法区所组成 + +Java Virtual Machine Stacks(Java 虚拟机栈):每个线程启动后,虚拟机就会为其分配一块栈内存 + +* 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存 +* 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法 + +线程上下文切换(Thread Context Switch):一些原因导致 cpu 不再执行当前线程,转而执行另一个线程 + +* 线程的 cpu 时间片用完 +* 垃圾回收 +* 有更高优先级的线程需要运行 +* 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法 + +程序计数器(Program Counter Register):记住下一条 jvm 指令的执行地址,是线程私有的 + +当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等 + +Java创建的线程是内核级线程,**线程的调度是在内核态运行的,而线程中的代码是在用户态运行**,所以线程切换(状态改变)会导致用户与内核态转换,这是非常消耗性能 + + + +*** + + + +### 常用API + +#### API + +Thread类API: + +| 方法 | 说明 | +| ------------------------------------------- | ------------------------------------------------------------ | +| public void start() | 启动一个新线程;Java虚拟机调用此线程的run方法 | +| public void run() | 线程启动后调用该方法 | +| public void setName(String name) | 给当前线程取名字 | +| public void getName() | 获取当前线程的名字
线程存在默认名称:子线程是Thread-索引,主线程是main | +| public static Thread currentThread() | 获取当前线程对象,代码在哪个线程中执行 | +| public static void sleep(long time) | 让当前线程休眠多少毫秒再继续执行
**Thread.sleep(0)** : 让操作系统立刻重新进行一次cpu竞争 | +| public static native void yield() | 提示线程调度器让出当前线程对CPU的使用 | +| public final int getPriority() | 返回此线程的优先级 | +| public final void setPriority(int priority) | 更改此线程的优先级,常用1 5 10 | +| public void interrupt() | 中断这个线程,异常处理机制 | +| public static boolean interrupted() | 判断当前线程是否被打断,清除打断标记 | +| public boolean isInterrupted() | 判断当前线程是否被打断,不清除打断标记 | +| public final void join() | 等待这个线程结束 | +| public final void join(long millis) | 等待这个线程死亡millis毫秒,0意味着永远等待 | +| public final native boolean isAlive() | 线程是否存活(还没有运行完毕) | +| public final void setDaemon(boolean on) | 将此线程标记为守护线程或用户线程 | + + + +*** + + + +#### run start + +run:称为线程体,包含了要执行的这个线程的内容,方法运行结束,此线程随即终止。直接调用 run 是在主线程中执行了 run,没有启动新的线程,需要顺序执行 + +start:使用 start 是启动新的线程,此线程处于就绪(可运行)状态,通过新的线程间接执行 run 中的代码 + +说明:**线程控制资源类** + +**面试问题**:run()方法中的异常不能抛出,只能try/catch + +* 因为父类中没有抛出任何异常,子类不能比父类抛出更多的异常 +* 异常不能跨线程传播回 main() 中,因此必须在本地进行处理 + + + +*** + + + +#### sleep yield + +sleep: + +* 调用 sleep 会让当前线程从 Running 进入 `Timed Waiting` 状态(阻塞) +* sleep()方法的过程中,线程不会释放对象锁 +* 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException +* 睡眠结束后的线程未必会立刻得到执行 +* 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性 + +yield: + +* 调用 yield 会让提示线程调度器让出当前线程对CPU的使用 +* 具体的实现依赖于操作系统的任务调度器 +* **会放弃CPU资源,锁资源不会释放** + + + +*** + + + +#### priority + +* 线程优先级会提示(hint)调度器优先调度该线程,但这仅仅是一个提示,调度器可以忽略它 +* 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作 + + + +*** + + + +#### join + +public final void join():等待这个线程结束 + +原理:调用者轮询检查线程 alive 状态,t1.join()等价于: + +```java +synchronized (t1) { + // 调用者线程进入 t1 的 waitSet 等待, 直到 t1 运行结束 + while (t1.isAlive()) { + t1.wait(0); + } +} +``` + +* join方法是被synchronized修饰的,本质上是一个对象锁,其内部的wait方法调用也是释放锁的,但是**释放的是当前线程的对象锁,而不是外面的lock锁** + +* t1会强占CPU资源,直至线程执行结束,当调用某个线程的join方法后,该线程抢占到CPU资源,就不再释放,直到线程执行完毕 + +线程同步: + +* join实现线程同步,因为会阻塞等待另一个线程的结束,才能继续向下运行 + * 需要外部共享变量,不符合面向对象封装的思想 + * 必须等待线程结束,不能配合线程池使用 +* Future 实现(同步):get()方法阻塞等待执行结果 + * main 线程接收结果 + * get 方法是让调用线程同步等待 + +```java +public class Test { + static int r = 0; + public static void main(String[] args) throws InterruptedException { + test1(); + } + private static void test1() throws InterruptedException { + Thread t1 = new Thread(() -> { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + r = 10; + }); + t1.start(); + t1.join();//不等待线程执行结束,输出的10 + System.out.println(r); + } +} +``` + + + +*** + + + +#### interrupt + +##### 打断线程 + +`public void interrupt()`:中断这个线程,异常处理机制 +`public static boolean interrupted()`:判断当前线程是否被打断,清除打断标记 +`public boolean isInterrupted()`:判断当前线程是否被打断,不清除打断标记 + +* sleep,wait,join方法都会让线程进入阻塞状态,打断进程会**清空打断状态** (false) + + ```java + public static void main(String[] args) throws InterruptedException { + Thread t1 = new Thread(()->{ + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }, "t1"); + t1.start(); + Thread.sleep(500); + t1.interrupt(); + System.out.println(" 打断状态: {}" + t1.isInterrupted());// 打断状态: {}false + } + ``` + +* 打断正常运行的线程:不会清空打断状态(true) + + ```java + public static void main(String[] args) throws Exception { + Thread t2 = new Thread(()->{ + while(true) { + Thread current = Thread.currentThread(); + boolean interrupted = current.isInterrupted(); + if(interrupted) { + System.out.println(" 打断状态: {}" + interrupted);//打断状态: {}true + break; + } + } + }, "t2"); + t2.start(); + Thread.sleep(500); + t2.interrupt(); + } + ``` + + + +*** + + + +##### 打断park + +park作用类似sleep,打断 park 线程,不会清空打断状态(true) + +```java +public static void main(String[] args) throws Exception { + Thread t1 = new Thread(() -> { + System.out.println("park..."); + LockSupport.park(); + System.out.println("unpark..."); + Sout("打断状态:" + Thread.currentThread().isInterrupted());//打断状态:true + }, "t1"); + t1.start(); + Thread.sleep(2000); + t1.interrupt(); +} +``` + +如果打断标记已经是 true, 则 park 会失效, + +```java +LockSupport.park(); +System.out.println("unpark..."); +LockSupport.park();//失效,不会阻塞 +System.out.println("unpark...");//和上一个unpark同时执行 +``` + +可以修改获取打断状态方法,使用`Thread.interrupted()`,清除打断标记 + + + +*** + + + +##### 终止模式 + +终止模式之两阶段终止模式:Two Phase Termination + +目标:在一个线程 T1 中如何“优雅”终止线程 T2?”优雅“指的是给 T2 一个后置处理器 + +错误思想: + +* 使用线程对象的 stop() 方法停止线程:stop 方法会真正杀死线程,如果这时线程锁住了共享资源,当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁 +* 使用 System.exit(int) 方法停止线程:目的仅是停止一个线程,但这种做法会让整个程序都停止 + +两阶段终止模式图示: + + + +打断线程可能在任何时间,所以需要考虑在任何时刻被打断的处理方法: + +```java +public class Test { + public static void main(String[] args) throws InterruptedException { + TwoPhaseTermination tpt = new TwoPhaseTermination(); + tpt.start(); + Thread.sleep(3500); + tpt.stop(); + } +} +class TwoPhaseTermination { + private Thread monitor; + //启动监控线程 + public void start() { + monitor = new Thread(new Runnable() { + @Override + public void run() { + while (true) { + Thread thread = Thread.currentThread(); + if (thread.isInterrupted()) { + System.out.println("后置处理"); + break; + } + try { + Thread.sleep(1000);//睡眠 + System.out.println("执行监控记录");//在此被打断不会异常 + } catch (InterruptedException e) {//在睡眠期间被打断 + e.printStackTrace(); + //重新设置打断标记 + thread.interrupt(); + } + } + } + }); + monitor.start(); + } + //停止监控线程 + public void stop() { + monitor.interrupt(); + } +} +``` + + + +*** + + + +#### daemon + +`public final void setDaemon(boolean on)`:如果是 true ,将此线程标记为守护线程 + +线程**启动前**调用此方法: + +```java +Thread t = new Thread() { + @Override + public void run() { + System.out.println("running"); + } +}; +// 设置该线程为守护线程 +t.setDaemon(true); +t.start(); +``` + +用户线程:平常创建的普通线程 + +守护线程:服务于用户线程,只要其它非守护线程运行结束了,即使守护线程代码没有执行完,也会强制结束 + +说明:当运行的线程都是守护线程,Java 虚拟机将退出,因为普通线程执行完后,守护线程不会继续运行下去 + +常见的守护线程: + +* 垃圾回收器线程就是一种守护线程 +* Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等 + 待它们处理完当前请求 + + + +*** + + + +#### 不推荐方法 + +不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁: + +| 方法 | 功能 | +| --------------------------- | -------------------- | +| public final void stop() | 停止线程运行 | +| public final void suspend() | 挂起(暂停)线程运行 | +| public final void resume() | 恢复线程运行 | + + + +*** + + + +### 线程状态 + +进程的状态参考操作系统:创建态、就绪态、运行态、阻塞态、终止态 + +线程由生到死的完整过程(生命周期):当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态,在API中`java.lang.Thread.State`这个枚举中给出了六种线程状态: + +| 线程状态 | 导致状态发生条件 | +| ----------------------- | ------------------------------------------------------------ | +| NEW(新建) | 线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread只有线程对象,没有线程特征。 | +| 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 + +* RUNNABLE <--> WAITING: + + * 调用 obj.wait() 方法时 + + 调用 obj.notify()、obj.notifyAll()、t.interrupt(): + + * 竞争锁成功,t 线程从 WAITING --> RUNNABLE + * 竞争锁失败,t 线程从 WAITING --> BLOCKED + + * 当前线程调用 t.join() 方法,注意是当前线程在t 线程对象的监视器上等待 + + * 当前线程调用 LockSupport.park() 方法 + +* RUNNABLE <--> TIMED_WAITING:调用 obj.wait(long n) 方法、当前线程调用 t.join(long n) 方法、当前线程调用 Thread.sleep(long n) + +* RUNNABLE <--> BLOCKED:t 线程用 synchronized(obj) 获取了对象锁时竞争失败 + + + +*** + + + +### 查看线程 + +windows: + +* 任务管理器可以查看进程和线程数,也可以用来杀死进程 +* tasklist 查看进程 +* taskkill 杀死进程 + +linux: + +* ps -ef 查看所有进程 +* ps -fT -p 查看某个进程(PID)的所有线程 +* kill 杀死进程 +* top 按大写 H 切换是否显示线程 +* top -H -p 查看某个进程(PID)的所有线程 + +Java: + +* jps 命令查看所有 Java 进程 +* jstack 查看某个 Java 进程(PID)的所有线程状态 +* jconsole 来查看某个 Java 进程中线程的运行情况(图形界面) + + + + + +*** + + + + + +## 管程 + +### 临界区 + +临界资源:一次仅允许一个进程使用的资源成为临界资源 + +临界区:访问临界资源的代码块 + +竞态条件:多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件 + +一个程序运行多个线程本身是没有问题的,多个线程访问共享资源会出现问题: + +* 多个线程读共享资源也没有问题 +* 在多个线程对共享资源读写操作时发生指令交错,就会出现问题 + +为了避免临界区的竞态条件发生(解决线程安全问题): + +* 阻塞式的解决方案:synchronized,Lock +* 非阻塞式的解决方案:原子变量 + +管程(monitor):由局部于自己的若干公共变量和所有访问这些公共变量的过程所组成的软件模块,保证同一时刻只有一个进程在管程内活动,即管程内定义的操作在同一时刻只被一个进程调用(由编译器实现) + +**synchronized:对象锁,保证了临界区内代码的原子性**,采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其它线程获取这个对象锁时会阻塞,保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换 + +互斥和同步都可以采用 synchronized 关键字来完成,区别: + +* 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码 +* 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点 + +性能: + +* 线程安全,性能差 +* 线程不安全性能好,假如开发中不会存在多线程安全问题,建议使用线程不安全的设计类 + + + +*** + + + +### syn-ed + +#### 基本使用 + +##### 同步代码块 + +锁对象:理论上可以是任意的“唯一”对象 + +synchronized是可重入、不公平的重量级锁 + +原则上: + +* 锁对象建议使用共享资源 +* 在实例方法中建议用this作为锁对象,锁住的this正好是共享资源 +* 在静态方法中建议用类名.class字节码作为锁对象 + * 因为静态成员属于类,被所有实例对象共享,所以需要锁住类 + * 锁住类以后,类的所有实例都相当于同一把锁,参考线程八锁 + +格式: + +```java +synchronized(锁对象){ + // 访问共享资源的核心代码 +} +``` + +实例: + +```java +public class demo { + static int counter = 0; + //static修饰,则元素是属于类本身的,不属于对象 ,与类一起加载一次,只有一个 + static final Object room = new Object(); + public static void main(String[] args) throws InterruptedException { + Thread t1 = new Thread(() -> { + for (int i = 0; i < 5000; i++) { + synchronized (room) { + counter++; + } + } + }, "t1"); + Thread t2 = new Thread(() -> { + for (int i = 0; i < 5000; i++) { + synchronized (room) { + counter--; + } + } + }, "t2"); + t1.start(); + t2.start(); + t1.join(); + t2.join(); + System.out.println(counter); + } +} +``` + + + +*** + + + +##### 同步方法 + +作用:把出现线程安全问题的核心方法锁起来,每次只能一个线程进入访问 + +用法:直接给方法加上一个修饰符 synchronized + +```java +//同步方法 +修饰符 synchronized 返回值类型 方法名(方法参数) { + 方法体; +} +//同步静态方法 +修饰符 static synchronized 返回值类型 方法名(方法参数) { + 方法体; +} +``` + +同步方法底层也是有锁对象的: + +* 如果方法是实例方法:同步方法默认用this作为的锁对象 + + ```java + public synchronized void test() {} //等价于 + public void test() { + synchronized(this) {} + } + ``` + +* 如果方法是静态方法:同步方法默认用类名.class作为的锁对象 + + ```java + class Test{ + public synchronized static void test() {} + } + //等价于 + class Test{ + public void test() { + synchronized(Test.class) {} + } + } + ``` + +面向对象实例: + +```java +public class Demo { + public static void main(String[] args) throws InterruptedException { + Room room = new Room(); + Thread t1 = new Thread(() -> { + for (int j = 0; j < 5000; j++) { + room.increment(); + } + }, "t1"); + Thread t2 = new Thread(() -> { + for (int j = 0; j < 5000; j++) { + room.decrement(); + } + }, "t2"); + t1.start(); + t2.start(); + t1.join(); + t2.join(); + System.out.println(room.get()); + } +} + +class Room { + int value = 0; + public synchronized void increment() { + value++; + } + public synchronized void decrement() { + value--; + } + public synchronized int get() { + return value; + } +} +``` + + + +*** + + + +##### 线程八锁 + +所谓的“线程八锁”,其实就是考察 synchronized 锁住的是哪个对象,直接百度搜索 + +说明:主要关注锁住的对象是不是同一个 + +* 锁住类对象,所有类的实例的方法都是安全的 +* 锁住this对象,只有在当前实例对象的线程内是安全的,如果有多个实例就不安全 + +线程不安全:因为锁住的不是同一个对象,线程1调用a方法锁住的类对象,线程2调用b方法锁住的n2对象,不是同一个对象 + +```java +class Number{ + public static synchronized void a(){ + Thread.sleep(1000); + System.out.println("1"); + } + public synchronized void b() { + System.out.println("2"); + } +} +public static void main(String[] args) { + Number n1 = new Number(); + Number n2 = new Number(); + new Thread(()->{ n1.a(); }).start(); + new Thread(()->{ n2.b(); }).start(); +} +``` + +线程安全:因为n1调用a()方法,锁住的是类对象,n2调用b()方法,锁住的也是类对象,所以线程安全 + +```java +class Number{ + public static synchronized void a(){ + Thread.sleep(1000); + System.out.println("1"); + } + public static synchronized void b() { + System.out.println("2"); + } +} +public static void main(String[] args) { + Number n1 = new Number(); + Number n2 = new Number(); + new Thread(()->{ n1.a(); }).start(); + new Thread(()->{ n2.b(); }).start(); +} +``` + + + + + +*** + + + +#### 锁原理 + +##### Monitor + +Monitor 被翻译为监视器或管程 + +每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 +Mark Word 中就被设置指向 Monitor 对象的指针,这就是重量级锁 + +* Mark Word结构: + + ![](https://gitee.com/seazean/images/raw/master/Java/JUC-Monitor-MarkWord结构32位.png) + +* 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 上锁的过程中,Thread-3、Thread-4、Thread-5也来执行 synchronized(obj),就会进入 + EntryList BLOCKED(双向链表) +* Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争是**非公平的** +* WaitSet 中的Thread-0,是以前获得过锁,但条件不满足进入 WAITING 状态的线程(wait-notify ) + +![](https://gitee.com/seazean/images/raw/master/Java/JUC-Monitor工作原理2.png) + +注意: + +* synchronized 必须是进入同一个对象的 monitor 才有上述的效果 +* 不加 synchronized 的对象不会关联监视器,不遵从以上规则 + + + +**** + + + +##### 字节码 + +代码: + +```java +public static void main(String[] args) { + Object lock = new Object(); + synchronized (lock) { + System.out.println("ok"); + } +} +``` + +```java +0: new #2 // new Object +3: dup +4: invokespecial #1 // invokespecial :()V,非虚方法 +7: astore_1 // lock引用 -> lock +8: aload_1 // lock (synchronized开始) +9: dup // 一份用来初始化,一份用来引用 +10: astore_2 // lock引用 -> slot 2 +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 +22: goto 30 +25: astore_3 // any -> slot 3 +26: aload_2 // slot 2(lock引用) +27: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList +28: aload_3 +29: athrow +30: return +Exception table: + from to target type + 12 22 25 any + 25 28 25 any +LineNumberTable: ... +LocalVariableTable: + Start Length Slot Name Signature + 0 31 0 args [Ljava/lang/String; + 8 23 1 lock Ljava/lang/Object; +``` + +说明: + +* 通过异常 **try-catch 机制**,确保一定会被解锁 +* 方法级别的 synchronized 不会在字节码指令中有所体现 + + + +*** + + + +#### 锁升级 + +##### 升级过程 + +**synchronized 是可重入、不公平的重量级锁**,所以可以对其进行优化 + +```java +无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁 //随着竞争的增加,只能锁升级,不能降级 +``` + +![](https://gitee.com/seazean/images/raw/master/Java/JUC-锁升级过程.png) + + + +*** + + + +##### 偏向锁 + +偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程之后重新获取该锁不再需要同步操作: + +* 当锁对象第一次被线程获得的时候进入偏向状态,标记为101,同时使用 CAS 操作将线程 ID 记录到Mark Word。如果 CAS 操作成功,这个线程以后进入这个锁相关的同步块,查看这个线程 ID 是自己的就表示没有竞争,就不需要再进行任何同步操作 + +* 当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定状态或轻量级锁状态 + + + +一个对象创建时: + +* 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的 + thread、epoch、age 都为 0 + +* 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加VM参数 `-XX:BiasedLockingStartupDelay=0` 来禁用延迟 + + JDK 8 延迟 4s 开启偏向锁原因:在刚开始执行代码时,会有好多线程来抢锁,如果开偏向锁效率反而降低 + +* 如果禁用了偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,**第一次用到 hashcode 时才会赋值**,添加 VM 参数 `-XX:-UseBiasedLocking` 禁用偏向锁 + +撤销偏向锁的状态: + +* 调用对象的 hashCode:偏向锁的对象 MarkWord 中存储的是线程 id,调用 hashCode 导致偏向锁被撤销 + * 轻量级锁会在锁记录中记录 hashCode + * 重量级锁会在 Monitor 中记录 hashCode +* 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁 +* 调用 wait/notify + + + +**批量撤销**:如果对象被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID + +* 批量重偏向:当撤销偏向锁阈值超过 20 次后,jvm会觉得是不是偏向错了,于是在给这些对象加锁时重新偏向至加锁线程 + +* 批量撤销:当撤销偏向锁阈值超过 40 次后,jvm会觉得自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的 + + + +*** + + + +##### 轻量级锁 + +一个对象有多个线程要加锁,但加锁的时间是错开的(没有竞争),那么可以使用轻量级锁来优化,轻量级锁对使用者是透明的(不可见) + +可重入锁:**线程可以进入任何一个它已经拥有的锁所同步着的代码块,可重入锁最大的作用是避免死锁** + +轻量级锁在没有竞争时(锁重入时),每次重入仍然需要执行 CAS 操作,Java 6 才引入的偏向锁来优化 + +锁重入实例: + +```java +static final Object obj = new Object(); +public static void method1() { + synchronized( obj ) { + // 同步块 A + method2(); + } +} +public static void method2() { + synchronized( obj ) { + // 同步块 B + } +} +``` + +* 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,存储锁定对象的Mark Word + + ![](https://gitee.com/seazean/images/raw/master/Java/JUC-轻量级锁原理1.png) + +* 让锁记录中Object reference指向锁对象,并尝试用CAS替换Object的Mark Word,将Mark Word的值存 + 入锁记录 + +* 如果CAS替换成功,对象头中存储了锁记录地址和状态 00(轻量级锁) ,表示由该线程给对象加锁 + ![](https://gitee.com/seazean/images/raw/master/Java/JUC-轻量级锁原理2.png) + +* 如果CAS失败,有两种情况: + + * 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程 + * 如果是自己执行了 synchronized 锁重入,就添加一条 Lock Record 作为重入的计数 + + ![](https://gitee.com/seazean/images/raw/master/Java/JUC-轻量级锁原理3.png) + +* 当退出 synchronized 代码块(解锁时) + + * 如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减1 + * 如果锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头 + * 成功,则解锁成功 + * 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程 + + + +*** + + + +##### 锁膨胀 + +在尝试加轻量级锁的过程中,CAS 操作无法成功,可能是其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为**重量级锁** + +* 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁 + + ![](https://gitee.com/seazean/images/raw/master/Java/JUC-重量级锁原理1.png) + +* Thread-1 加轻量级锁失败,进入锁膨胀流程:为 Object 对象申请 Monitor 锁,让Object 对象头指向重量级锁地址,Monitor 的 Owner 置为 Thread-0,然后自己进入 Monitor 的 EntryList BLOCKED + + ![](https://gitee.com/seazean/images/raw/master/Java/JUC-重量级锁原理2.png) + +* 当Thread-0退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败,这时进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程 + + + + + +*** + + + +#### 锁优化 + +##### 自旋锁 + +**重量级锁竞争**时,尝试获取锁的线程不会立即阻塞,可以使用**自旋**来进行优化,采用循环的方式去尝试获取锁 + +注意: + +* 自旋占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势 +* 自旋失败的线程会进入阻塞状态 + +优点:不会进入阻塞状态,减少线程上下文切换的消耗 + +缺点:当自旋的线程越来越多时,会不断的消耗CPU资源 + +自旋锁情况: + +* 自旋成功的情况: + + +* 自旋失败的情况: + + + +自旋锁说明: + +* 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会 + 高,就多自旋几次;反之,就少自旋甚至不自旋,比较智能 +* Java 7 之后不能控制是否开启自旋功能,由JVM控制 + +```java +//手写自旋锁 +public class SpinLock { + // 泛型装的是Thread,原子引用线程 + AtomicReference atomicReference = new AtomicReference<>(); + + public void lock() { + Thread thread = Thread.currentThread(); + System.out.println(thread.getName() + " come in"); + + //开始自旋,期望值为null,更新值是当前线程 + while (!atomicReference.compareAndSet(null, thread)) { + Thread.sleep(1000); + System.out.println(thread.getName() + " 正在自旋"); + } + System.out.println(thread.getName() + " 自旋成功"); + } + + public void unlock() { + Thread thread = Thread.currentThread(); + + //线程使用完锁把引用变为null + atomicReference.compareAndSet(thread, null); + System.out.println(thread.getName() + " invoke unlock"); + } + + public static void main(String[] args) throws InterruptedException { + SpinLock lock = new SpinLock(); + new Thread(() -> { + //占有锁 + lock.lock(); + Thread.sleep(10000); + + //释放锁 + lock.unlock(); + },"t1").start(); + + // 让main线程暂停1秒,使得t1线程,先执行 + Thread.sleep(1000); + + new Thread(() -> { + lock.lock(); + lock.unlock(); + },"t2").start(); + } +} +``` + + + + + +*** + + + +##### 锁消除 + +锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除 + +锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除(同步消除:JVM内存分配) + + + +*** + + + +##### 锁粗化 + +对相同对象多次加锁,导致线程发生多次重入,频繁的加锁操作就会导致性能损耗,可以使用锁粗化方式优化 + +如果虚拟机探测到一串零碎的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部 + +* 一些看起来没有加锁的代码,其实隐式的加了很多锁: + + ```java + public static String concatString(String s1, String s2, String s3) { + return s1 + s2 + s3; + } + ``` + +* String 是一个不可变的类,编译器会对 String 的拼接自动优化。在 JDK 1.5 之前,转化为StringBuffer对象的连续 append() 操作,每个append() 方法中都有一个同步块 + + ```java + public static String concatString(String s1, String s2, String s3) { + StringBuffer sb = new StringBuffer(); + sb.append(s1); + sb.append(s2); + sb.append(s3); + return sb.toString(); + } + ``` + +扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,只需要加锁一次就可以 + + + +**** + + + +#### 多把锁 + +多把不相干的锁:一间大屋子有两个功能:睡觉、学习,互不相干。现在一人要学习,一人要睡觉,如果只用一间屋子(一个对象锁)的话,那么并发度很低 + +将锁的粒度细分: + +* 好处,是可以增强并发度 +* 坏处,如果一个线程需要同时获得多把锁,就容易发生死锁 + +解决方法:准备多个对象锁 + +```java +public static void main(String[] args) { + BigRoom bigRoom = new BigRoom(); + new Thread(() -> { bigRoom.study(); }).start(); + new Thread(() -> { bigRoom.sleep(); }).start(); +} +class BigRoom { + private final Object studyRoom = new Object(); + private final Object sleepRoom = new Object(); + + public void sleep() throws InterruptedException { + synchronized (sleepRoom) { + System.out.println("sleeping 2 小时"); + Thread.sleep(2000); + } + } + + public void study() throws InterruptedException { + synchronized (studyRoom) { + System.out.println("study 1 小时"); + Thread.sleep(1000); + } + } +} +``` + + + +*** + + + +#### 活跃性 + +##### 死锁 + +###### 形成 + +死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放,由于线程被无限期地阻塞,因此程序不可能正常终止 + +java 死锁产生的四个必要条件: + +1. 互斥条件,即当资源被一个线程使用(占有)时,别的线程不能使用 +2. 不可剥夺条件,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放 +3. 请求和保持条件,即当资源请求者在请求其他的资源的同时保持对原有资源的占有 +4. 循环等待条件,即存在一个等待循环队列:p1 要 p2 的资源,p2 要 p1 的资源,形成了一个等待环路 + +四个条件都成立的时候,便形成死锁。死锁情况下打破上述任何一个条件,便可让死锁消失 + +```java +public class Dead { + public static Object resources1 = new Object(); + public static Object resources2 = new Object(); + public static void main(String[] args) { + new Thread(() -> { + // 线程1:占用资源1 ,请求资源2 + synchronized(resources1){ + System.out.println("线程1已经占用了资源1,开始请求资源2"); + Thread.sleep(2000);//休息两秒,防止线程1直接运行完成。 + //2秒内线程2肯定可以锁住资源2 + synchronized (resources2){ + System.out.println("线程1已经占用了资源2"); + } + }).start(); + new Thread(() -> { + // 线程2:占用资源2 ,请求资源1 + synchronized(resources2){ + System.out.println("线程2已经占用了资源2,开始请求资源1"); + Thread.sleep(2000); + synchronized (resources1){ + System.out.println("线程2已经占用了资源1"); + } + }} + }).start(); + } +} +``` + +面向对象写法: + +```java +public class DeadLock { + static String lockA = "lockA"; + static String lockB = "lockB"; + public static void main(String[] args) { + new Thread(new HoldLockThread(lockA, lockB)).start(); + new Thread(new HoldLockThread(lockB, lockA)).start(); + } +} +class HoldLockThread implements Runnable { + private String lockA; + private String lockB; + + public HoldLockThread(String lockA, String lockB) { + this.lockA = lockA; + this.lockB = lockB; + } + + @Override + public void run() { + synchronized (lockA) { + System.out.println(Thread.currentThread().getName() + " 持有" + lockA + ",尝试获得" + lockB); + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + synchronized (lockB) { + System.out.println(Thread.currentThread().getName() + " 持有" + lockB + ",尝试获得" + lockA); + } + } + } +} +``` + + + +###### 定位 + +定位死锁的方法: + +* 使用 jps 定位进程 id,再用 `jstack id` 定位死锁,找到死锁的线程去查看源码,解决优化 + + ```sh + "Thread-1" #12 prio=5 os_prio=0 tid=0x000000001eb69000 nid=0xd40 waiting formonitor entry [0x000000001f54f000] + java.lang.Thread.State: BLOCKED (on object monitor) + #省略 + "Thread-1" #12 prio=5 os_prio=0 tid=0x000000001eb69000 nid=0xd40 waiting for monitor entry [0x000000001f54f000] + java.lang.Thread.State: BLOCKED (on object monitor) + #省略 + + Found one Java-level deadlock: + =================================================== + "Thread-1": + waiting to lock monitor 0x000000000361d378 (object 0x000000076b5bf1c0, a java.lang.Object), + which is held by "Thread-0" + "Thread-0": + waiting to lock monitor 0x000000000361e768 (object 0x000000076b5bf1d0, a java.lang.Object), + which is held by "Thread-1" + + Java stack information for the threads listed above: + =================================================== + "Thread-1": + at thread.TestDeadLock.lambda$main$1(TestDeadLock.java:28) + - waiting to lock <0x000000076b5bf1c0> (a java.lang.Object) + - locked <0x000000076b5bf1d0> (a java.lang.Object) + at thread.TestDeadLock$$Lambda$2/883049899.run(Unknown Source) + at java.lang.Thread.run(Thread.java:745) + "Thread-0": + at thread.TestDeadLock.lambda$main$0(TestDeadLock.java:15) + - waiting to lock <0x000000076b5bf1d0> (a java.lang.Object) + - locked <0x000000076b5bf1c0> (a java.lang.Object) + at thread.TestDeadLock$$Lambda$1/495053715 + ``` + +* linux 下可以通过 top 先定位到 CPU 占用高的 Java 进程,再利用 `top -Hp 进程id` 来定位是哪个线程,最后再用 jstack 的输出来看各个线程栈 + +* 避免死锁:避免死锁要注意加锁顺序 + +* 可以使用 jconsole 工具,在 `jdk\bin` 目录下 + + + +*** + + + +##### 活锁 + +活锁:指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试—失败—尝试—失败的过程 + +两个线程互相改变对方的结束条件,最后谁也无法结束: + +```java +class TestLiveLock { + static volatile int count = 10; + static final Object lock = new Object(); + public static void main(String[] args) { + new Thread(() -> { + // 期望减到 0 退出循环 + while (count > 0) { + Thread.sleep(200); + count--; + System.out.println("线程一count:" + count); + } + }, "t1").start(); + new Thread(() -> { + // 期望超过 20 退出循环 + while (count < 20) { + Thread.sleep(200); + count++; + System.out.println("线程二count:"+ count); + } + }, "t2").start(); + } +} +``` + + + +*** + + + +##### 饥饿 + +饥饿:一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束 + + + +*** + + + +### wait-ify + +#### 基本使用 + +需要获取对象锁后才可以调用`锁对象.wait()`,调用notify随机唤醒一个线程,notifyAll唤醒所有线程去竞争CPU + +Object类API: + +```java +public final void notify():唤醒正在等待对象监视器的单个线程。 +public final void notifyAll():唤醒正在等待对象监视器的所有线程。 +public final void wait():导致当前线程等待,直到另一个线程调用该对象的notify()方法或 notifyAll()方法。 +public final native void wait(long timeout):有时限的等待, 到n毫秒后结束等待,或是被唤醒 +``` + +对比sleep(): + +* 原理不同:sleep()方法是属于Thread类,是线程用来控制自身流程的,使此线程暂停执行一段时间而把执行机会让给其他线程;wait()方法属于Object类,用于线程间通信 +* 对锁的处理机制不同:调用sleep()方法的过程中,线程不会释放对象锁,当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,但是都会释放CPU +* 使用区域不同:wait()方法必须放在**同步控制方法和同步代码块(先获取锁)**中使用,sleep() 方法则可以放在任何地方使用 + +底层原理: + +* Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态 +* BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片 +* BLOCKED 线程会在 Owner 线程释放锁时唤醒 +* WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入 + EntryList 重新竞争 + +![](https://gitee.com/seazean/images/raw/master/Java/JUC-Monitor工作原理2.png) + + + +*** + + + +#### 代码优化 + +虚假唤醒:notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程 + +解决方法:采用notifyAll + +notifyAll 仅解决某个线程的唤醒问题,使用 if + wait 判断仅有一次机会,一旦条件不成立,无法重新判断 + +解决方法:用 while + wait,当条件不成立,再次 wait + +```java +@Slf4j(topic = "c.demo") +public class demo { + static final Object room = new Object(); + static boolean hasCigarette = false; //有没有烟 + static boolean hasTakeout = false; + + public static void main(String[] args) throws InterruptedException { + new Thread(() -> { + synchronized (room) { + log.debug("有烟没?[{}]", hasCigarette); + while (!hasCigarette) {//while防止虚假唤醒 + log.debug("没烟,先歇会!"); + try { + room.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + log.debug("有烟没?[{}]", hasCigarette); + if (hasCigarette) { + log.debug("可以开始干活了"); + } else { + log.debug("没干成活..."); + } + } + }, "小南").start(); + + new Thread(() -> { + synchronized (room) { + Thread thread = Thread.currentThread(); + log.debug("外卖送到没?[{}]", hasTakeout); + if (!hasTakeout) { + log.debug("没外卖,先歇会!"); + try { + room.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + log.debug("外卖送到没?[{}]", hasTakeout); + if (hasTakeout) { + log.debug("可以开始干活了"); + } else { + log.debug("没干成活..."); + } + } + }, "小女").start(); + + + Thread.sleep(1000); + new Thread(() -> { + // 这里能不能加 synchronized (room)? + synchronized (room) { + hasTakeout = true; + //log.debug("烟到了噢!"); + log.debug("外卖到了噢!"); + room.notifyAll(); + } + }, "送外卖的").start(); + } +} +``` + + + + + +**** + + + +### park-un + +LockSupport 类方法: + +* `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 + LockSupport.park(); + System.out.println("resume...");//4 + },"t1"); + t1.start(); + Thread.sleep(2000); + System.out.println("unpark...");//3 + LockSupport.unpark(t1); +} +``` + +对比wait & notify: + +* wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而park、unpark不需要 +* park & unpark以线程为单位来阻塞和唤醒线程,而notify 只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程 +* **park & unpark 可以先 unpark**,而 wait & notify 不能先 notify。类比生产消费,先消费发现有产品就消费,没有就等待;先生产就直接产生商品,然后线程直接消费 +* wait 会释放锁资源进入等待队列,park 不会释放锁资源,只负责阻塞当前线程,会释放CPU + +原理: + +* 先park: + 1. 当前线程调用 Unsafe.park() 方法 + 2. 检查 _counter ,本情况为 0,这时获得 _mutex 互斥锁 + 3. 线程进入 _cond 条件变量阻塞 + 4. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1 + 5. 唤醒 _cond 条件变量中的 Thread_0,Thread_0 恢复运行,设置 _counter 为 0 + +![](https://gitee.com/seazean/images/raw/master/Java/JUC-park原理1.png) + +* 先unpark: + + 1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1 + 2. 当前线程调用 Unsafe.park() 方法 + 3. 检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行,设置 _counter 为 0 + + ![](https://gitee.com/seazean/images/raw/master/Java/JUC-park原理2.png) + + + + + +*** + + + +### 安全分析 + +成员变量和静态变量: + +* 如果它们没有共享,则线程安全 +* 如果它们被共享了,根据它们的状态是否能够改变,分两种情况: + * 如果只有读操作,则线程安全 + * 如果有读写操作,则这段代码是临界区,需要考虑线程安全问题 + +局部变量: + +* 局部变量是线程安全的 +* 局部变量引用的对象不一定线程安全(逃逸分析): + * 如果该对象没有逃离方法的作用访问,它是线程安全的(每一个方法有一个栈帧) + * 如果该对象逃离方法的作用范围,需要考虑线程安全问题(暴露引用) + +常见线程安全类:String、Integer、StringBuffer、Random、Vector、Hashtable、java.util.concurrent包 + +* 线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的 + +* 每个方法是原子的,但多个方法的组合不是原子的,只能保证调用的方法内部安全: + + ```java + Hashtable table = new Hashtable(); + // 线程1,线程2 + if( table.get("key") == null) { + table.put("key", value); + } + ``` + +不可变类线程安全:String、Integer 等都是不可变类,因为内部的状态不可以改变,因此方法是线程安全 + +* replace等方法底层是新建一个对象,复制过去 + + ```java + 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);` + +无状态类线程安全 + + + +*** + + + +### 同步模式 + +#### 保护性暂停 + +##### 单任务版 + +Guarded Suspension,用在一个线程等待另一个线程的执行结果 + +* 有一个结果需要从一个线程传递到另一个线程,让它们关联同一个 GuardedObject +* 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者) +* JDK 中,join 的实现、Future 的实现,采用的就是此模式 + +![](https://gitee.com/seazean/images/raw/master/Java/JUC-保护性暂停.png) + +```java +public static void main(String[] args) { + GuardedObject object = new GuardedObjectV2(); + new Thread(() -> { + sleep(1); + object.complete(Arrays.asList("a", "b", "c")); + }).start(); + + Object response = object.get(2500); + if (response != null) { + log.debug("get response: [{}] lines", ((List) response).size()); + } else { + log.debug("can't get response"); + } +} + +class GuardedObject { + private Object response; + private final Object lock = new Object(); + + //获取结果 + //timeout :最大等待时间 + public Object get(long millis) { + synchronized (lock) { + // 1) 记录最初时间 + long begin = System.currentTimeMillis(); + // 2) 已经经历的时间 + long timePassed = 0; + while (response == null) { + // 4) 假设 millis 是 1000,结果在 400 时唤醒了,那么还有 600 要等 + long waitTime = millis - timePassed; + log.debug("waitTime: {}", waitTime); + //经历时间超过最大等待时间退出循环 + if (waitTime <= 0) { + log.debug("break..."); + break; + } + try { + lock.wait(waitTime); + } catch (InterruptedException e) { + e.printStackTrace(); + } + // 3) 如果提前被唤醒,这时已经经历的时间假设为 400 + timePassed = System.currentTimeMillis() - begin; + log.debug("timePassed: {}, object is null {}", + timePassed, response == null); + } + return response; + } + } + + //产生结果 + public void complete(Object response) { + synchronized (lock) { + // 条件满足,通知等待线程 + this.response = response; + log.debug("notify..."); + lock.notifyAll(); + } + } +} +``` + + + +##### 多任务版 + +多任务版保护性暂停: + +![](https://gitee.com/seazean/images/raw/master/Java/JUC-保护性暂停多任务版.png) + +```java +public static void main(String[] args) throws InterruptedException { + for (int i = 0; i < 3; i++) { + new People().start(); + } + Thread.sleep(1000); + for (Integer id : Mailboxes.getIds()) { + new Postman(id, id + "号快递到了").start(); + } +} + +@Slf4j(topic = "c.People") +class People extends Thread{ + @Override + public void run() { + // 收信 + GuardedObject guardedObject = Mailboxes.createGuardedObject(); + log.debug("开始收信i d:{}", guardedObject.getId()); + Object mail = guardedObject.get(5000); + log.debug("收到信id:{},内容:{}", guardedObject.getId(),mail); + } +} + +class Postman extends Thread{ + private int id; + private String mail; + //构造方法 + @Override + public void run() { + GuardedObject guardedObject = Mailboxes.getGuardedObject(id); + log.debug("开始送信i d:{},内容:{}", guardedObject.getId(),mail); + guardedObject.complete(mail); + } +} + +class Mailboxes { + private static Map boxes = new Hashtable<>(); + private static int id = 1; + + //产生唯一的id + private static synchronized int generateId() { + return id++; + } + + public static GuardedObject getGuardedObject(int id) { + return boxes.remove(id); + } + + public static GuardedObject createGuardedObject() { + GuardedObject go = new GuardedObject(generateId()); + boxes.put(go.getId(), go); + return go; + } + + public static Set getIds() { + return boxes.keySet(); + } +} +class GuardedObject { + //标识,Guarded Object + private int id;//添加get set方法 +} +``` + + + +**** + + + +#### 顺序输出 + +顺序输出 2 1 + +```java +public static void main(String[] args) throws InterruptedException { + Thread t1 = new Thread(() -> { + while (true) { + //try { Thread.sleep(1000); } catch (InterruptedException e) { } + // 当没有许可时,当前线程暂停运行;有许可时,用掉这个许可,当前线程恢复运行 + LockSupport.park(); + System.out.println("1"); + } + }); + Thread t2 = new Thread(() -> { + while (true) { + System.out.println("2"); + // 给线程 t1 发放『许可』(多次连续调用 unpark 只会发放一个『许可』) + LockSupport.unpark(t1); + try { Thread.sleep(500); } catch (InterruptedException e) { } + } + }); + t1.start(); + t2.start(); +} +``` + + + +*** + + + +#### 交替输出 + +连续输出5次abc + +```java +public class day2_14 { + public static void main(String[] args) throws InterruptedException { + AwaitSignal awaitSignal = new AwaitSignal(5); + Condition a = awaitSignal.newCondition(); + Condition b = awaitSignal.newCondition(); + Condition c = awaitSignal.newCondition(); + new Thread(() -> { + awaitSignal.print("a", a, b); + }).start(); + new Thread(() -> { + awaitSignal.print("b", b, c); + }).start(); + new Thread(() -> { + awaitSignal.print("c", c, a); + }).start(); + + Thread.sleep(1000); + awaitSignal.lock(); + try { + a.signal(); + } finally { + awaitSignal.unlock(); + } + } +} + +class AwaitSignal extends ReentrantLock { + private int loopNumber; + + public AwaitSignal(int loopNumber) { + this.loopNumber = loopNumber; + } + //参数1:打印内容 参数二:条件变量 参数二:唤醒下一个 + public void print(String str, Condition condition, Condition next) { + for (int i = 0; i < loopNumber; i++) { + lock(); + try { + condition.await(); + System.out.print(str); + next.signal(); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + unlock(); + } + } + } +} +``` + + + +*** + + + +### 异步模式 + +#### 传统版 + +异步模式之生产者/消费者: + +```java +class ShareData { + private int number = 0; + private Lock lock = new ReentrantLock(); + private Condition condition = lock.newCondition(); + + public void increment() throws Exception{ + // 同步代码块,加锁 + lock.lock(); + try { + // 判断 防止虚假唤醒 + while(number != 0) { + // 等待不能生产 + condition.await(); + } + // 干活 + number++; + System.out.println(Thread.currentThread().getName() + "\t " + number); + // 通知 唤醒 + condition.signalAll(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + } + + public void decrement() throws Exception{ + // 同步代码块,加锁 + lock.lock(); + try { + // 判断 防止虚假唤醒 + while(number == 0) { + // 等待不能消费 + condition.await(); + } + // 干活 + number--; + System.out.println(Thread.currentThread().getName() + "\t " + number); + // 通知 唤醒 + condition.signalAll(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + } +} + +public class TraditionalProducerConsumer { + public static void main(String[] args) { + ShareData shareData = new ShareData(); + // t1线程,生产 + new Thread(() -> { + for (int i = 0; i < 5; i++) { + shareData.increment(); + } + }, "t1").start(); + + // t2线程,消费 + new Thread(() -> { + for (int i = 0; i < 5; i++) { + shareData.decrement(); + } + }, "t2").start(); + } +} +``` + + + +#### 改进版 + +异步模式之生产者/消费者: + +* 消费队列可以用来平衡生产和消费的线程资源,不需要产生结果和消费结果的线程一一对应 +* 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据 +* 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据 +* JDK 中各种阻塞队列,采用的就是这种模式 + +![](https://gitee.com/seazean/images/raw/master/Java/JUC-生产者消费者模式.png) + +```java +public class demo { + public static void main(String[] args) { + MessageQueue queue = new MessageQueue(2); + for (int i = 0; i < 3; i++) { + int id = i; + new Thread(() -> { + queue.put(new Message(id,"值"+id)); + }, "生产者" + i).start(); + } + + new Thread(() -> { + while (true) { + try { + Thread.sleep(1000); + Message message = queue.take(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + },"消费者").start(); + } +} + +//消息队列类,Java间线程之间通信 +class MessageQueue { + private LinkedList list = new LinkedList<>();//消息的队列集合 + private int capacity;//队列容量 + public MessageQueue(int capacity) { + this.capacity = capacity; + } + + //获取消息 + public Message take() { + //检查队列是否为空 + synchronized (list) { + while (list.isEmpty()) { + try { + sout(Thread.currentThread().getName() + ":队列为空,消费者线程等待"); + list.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + //从队列的头部获取消息返回 + Message message = list.removeFirst(); + sout(Thread.currentThread().getName() + ":已消费消息--" + message); + list.notifyAll(); + return message; + } + } + + //存入消息 + public void put(Message message) { + synchronized (list) { + //检查队列是否满 + while (list.size() == capacity) { + try { + sout(Thread.currentThread().getName()+":队列为已满,生产者线程等待"); + list.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + //将消息加入队列尾部 + list.addLast(message); + sout(Thread.currentThread().getName() + ":已生产消息--" + message); + list.notifyAll(); + } + } +} + +final class Message { + private int id; + private Object value; + //get set +} +``` + + + +*** + + + +#### 阻塞队列 + +```java +public static void main(String[] args) { + ExecutorService consumer = Executors.newFixedThreadPool(1); + ExecutorService producer = Executors.newFixedThreadPool(1); + BlockingQueue queue = new SynchronousQueue<>(); + producer.submit(() -> { + try { + System.out.println("生产..."); + Thread.sleep(1000); + queue.put(10); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }); + consumer.submit(() -> { + try { + System.out.println("等待消费..."); + Integer result = queue.take(); + System.out.println("结果为:" + result); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }); +} +``` + + + + + +**** + + + + + +## 内存 + +### JMM + +#### 内存模型 + +Java 内存模型是 Java MemoryModel(JMM),本身是一种**抽象的概念**,实际上并不存在,描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式 + +作用: + +* 屏蔽各种硬件和操作系统的内存访问差异,实现让Java程序在各种平台下都能达到一致的内存访问效果 +* 规定了线程和内存之间的一些关系 + +根据JMM的设计,系统存在一个主内存(Main Memory),Java中所有变量都存储在主存中,对于所有线程都是共享的;每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些**变量的拷贝**,线程对所有变量的操作都是先对变量进行拷贝,然后在工作内存中进行,不能直接操作主内存中的变量;线程之间无法相互直接访问,线程间的通信(传递)必须通过主内存来完成 + +![](https://gitee.com/seazean/images/raw/master/Java/JMM内存模型.png) + +主内存和工作内存: + +* 主内存:计算机的内存,也就是经常提到的8G内存,16G内存,存储所有共享变量的值 +* 工作内存:存储该线程使用到的共享变量在主内存的的值的副本拷贝 + + + +**jvm和jmm之间的关系**: + +* jmm中的主内存、工作内存与jvm中的Java堆、栈、方法区等并不是同一个层次的内存划分,这两者基本上是没有关系的,如果两者一定要勉强对应起来: + * 主内存主要对应于Java堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域 + * 从更低层次上说,主内存直接对应于物理硬件的内存,工作内存对应寄存器和高速缓存 + + + +*** + + + +#### 内存交互 + +Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作: + + + +* lock:将一个变量标识为被一个线程独占状态 +* unclock:将一个变量从独占状态释放出来,释放后的变量才可以被其他线程锁定 +* read:把一个变量的值从主内存传输到工作内存中 +* load:在 read 之后执行,把 read 得到的值放入工作内存的变量副本中 +* use:把工作内存中一个变量的值传递给执行引擎,每当遇到一个使用到变量的操作时都要使用该指令 +* assign:把从执行引擎接收到的一个值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的操作时,都要使用该指令 +* store:把工作内存的一个变量的值传送到主内存中 +* write:在 store 之后执行,把 store 得到的值放入主内存的变量中 + + + +*** + + + +#### 三大特性 + +##### 可见性 + +可见性:是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值 + +存在可见性问题的根本原因是由于缓存的存在,线程持有的是共享变量的副本,无法感知其他线程对于共享变量的更改,导致读取的值不是最新的 + +main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止: + +```java +static boolean run = true; //添加volatile +public static void main(String[] args) throws InterruptedException { + Thread t = new Thread(()->{ + while(run){ + // .... + } + }); + t.start(); + sleep(1); + run = false; // 线程t不会如预想的停下来 +} +``` + +原因: + +* 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存 +* 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中, + 减少对主存中 run 的访问,提高效率 +* 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量 + 的值,结果永远是旧值 + +![](https://gitee.com/seazean/images/raw/master/Java/JMM-可见性例子.png) + + + +*** + + + +##### 原子性 + +原子性:不可分割,完整性,也就是说某个线程正在做某个具体业务时,中间不可以被分割,需要具体完成,要么同时成功,要么同时失败,保证指令不会受到线程上下文切换的影响 + +定义原子操作的使用规则: + +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操作) + + + +*** + + + +##### 有序性 + +有序性:在本线程内观察,所有操作都是有序的;在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序 + +CPU的基本工作是执行存储的指令序列,即程序,程序的执行过程实际上是不断地取出指令、分析指令、执行指令的过程,为了提高性能,编译器和处理器会对指令重排,一般分为以下三种: + +```java +源代码 -> 编译器优化的重排 -> 指令并行的重排 -> 内存系统的重排 -> 最终执行指令 +``` + +现代 CPU 支持多级指令流水线,几乎所有的冯•诺伊曼型计算机的CPU,其工作都可以分为5个阶段:取指令、指令译码、执行指令、访存取数和结果写回,可以称之为**五级指令流水线**。这时 CPU 可以在一个时钟周期内,同时运行五条指令的**不同阶段**(每个线程不同的阶段),本质上流水线技术并不能缩短单条指令的执行时间,但变相地提高了指令地吞吐率 + +处理器在进行重排序时,必须要考虑**指令之间的数据依赖性** + +* 单线程环境也存在指令重排,由于存在依赖,最终执行结果和代码顺序的结果一致 +* 多线程环境中线程交替执行,由于编译器优化重排,两个线程中使用的变量能否保证一致性是无法确定的 + + + + + +*** + + + +### cache + +#### 缓存机制 + +##### 缓存结构 + +在计算机系统中,CPU高速缓存(CPU Cache,简称缓存)是用于减少处理器访问内存所需平均时间的部件;在存储体系中位于自顶向下的第二层,仅次于CPU寄存器;其容量远小于内存,但速度却可以接近处理器的频率 + +CPU 处理器速度远远大于在主内存中的,为了解决速度差异,在它们之间架设了多级缓存,如 L1、L2、L3 级别的缓存,这些缓存离CPU越近就越快,将频繁操作的数据缓存到这里,加快访问速度 + + + +| 从 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),有效利用这种局部性,缓存可以达到极高的命中率。 + + + +*** + + + +#### 伪共享 + +**缓存以缓存行 cache line 为单位,每个缓存行对应着一块内存**,一般是 64 byte(8 个 long),在CPU从主存获取数据时,以 cache line 为单位加载,于是相邻的数据会一并加载到缓存中 + +缓存会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中,CPU 要保证数据的一致性,需要做到某个 CPU 核心更改了数据,其它 CPU 核心对应的**整个缓存行必须失效**,这就是伪共享 + + + +解决方法: + +* padding:通过填充,让数据落在不同的 cache line 中 + +* @Contended:原理参考 无锁 → Addr → 优化机制 → 伪共享 + +Linux查看CPU缓存行: + +* 命令:`cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size64` +* 内存地址格式:[高位组标记] [低位索引] [偏移量] + + + +*** + + + +#### 缓存一致 + +缓存一致性:当多个处理器运算任务都涉及到同一块主内存区域的时候,将可能导致各自的缓存数据不一样 + +**MESI**(Modified Exclusive Shared Or Invalid)是一种广泛使用的支持写回策略的缓存一致性协议,CPU中每个缓存行 (caceh line) 使用4种状态进行标记(使用额外的两位 (bit) 表示): + +* M:被修改(Modified) + + 该缓存行只被缓存在该 CPU 的缓存中,并且是被修改过的,与主存中的数据不一致 (dirty),该缓存行中的内存需要在未来的某个时间点(其它 CPU 读取主存中相应数据之前)写回( write back )主存 + + 当被写回主存之后,该缓存行的状态会变成独享 (exclusive) 状态。 + +* E:独享的(Exclusive) + + 该缓存行只被缓存在该 CPU 的缓存中,它是未被修改过的 (clear),与主存中数据一致,该状态可以在任何时刻有其它 CPU 读取该内存时变成共享状态 (shared) + + 当 CPU 修改该缓存行中内容时,该状态可以变成 Modified 状态 + +* S:共享的(Shared) + + 该状态意味着该缓存行可能被多个 CPU 缓存,并且各个缓存中的数据与主存数据一致 (clear),当有一个 CPU 修改该缓存行中,其它 CPU 中该缓存行变成无效状态 (Invalid) + +* I:无效的(Invalid) + + 该缓存是无效的,可能有其它 CPU 修改了该缓存行 + +解决方法:各个处理器访问缓存时都遵循一些协议,在读写时要根据协议进行操作,协议主要有MSI、MESI等 + + + +**** + + + +#### 处理机制 + +单核 CPU 处理器会自动保证基本内存操作的原子性 + +多核 CPU 处理器,每个 CPU 处理器内维护了一块内存,每个内核内部维护着一块缓存,当多线程并发读写时,就会出现缓存数据不一致的情况。处理器提供: + +* 总线锁定:当处理器要操作共享变量时,在 BUS 总线上发出一个 LOCK 信号,其他处理器就无法操作这个共享变量,该操作会导致大量阻塞,从而增加系统的性能开销 +* 缓存锁定:当处理器对缓存中的共享变量进行了操作,其他处理器有嗅探机制,将该共享变量的缓存失效,其他线程读取时会重新从主内存中读取最新的数据,基于 MESI 缓存一致性协议来实现 + +有如下两种情况处理器不会使用缓存锁定: + +* 当操作的数据跨多个缓存行,或没被缓存在处理器内部,则处理器会使用总线锁定 + +* 有些处理器不支持缓存锁定,比如:Intel 486 和 Pentium 处理器也会调用总线锁定 + +总线机制: + +* 总线嗅探:每个处理器通过嗅探在总线上传播的数据来检查自己缓存值是否过期了,当处理器发现自己的缓存对应的内存地址数据被修改,就将当前处理器的缓存行设置为无效状态,当处理器对这个数据进行操作时,会重新从内存中把数据读取到处理器缓存中 + +* 总线风暴:由于 volatile 的 MESI 缓存一致性协议,需要不断的从主内存嗅探和 CAS 循环,无效的交互会导致总线带宽达到峰值;因此不要大量使用 volatile 关键字,使用 volatile、syschonized 都需要根据实际场景 + + + +*** + + + +### volatile + +#### 基本特性 + +Volatile是Java虚拟机提供的**轻量级**的同步机制(三大特性) + +- 保证可见性 +- 不保证原子性 +- 保证有序性(禁止指令重排) + +性能:volatile修饰的变量进行读操作与普通变量几乎没什么差别,但是写操作相对慢一些,因为需要在本地代码中插入很多内存屏障来保证指令不会发生乱序执行,但是开销比锁要小 + + + + +*** + + + +#### 解决重排 + +**volatile 修饰的变量,可以禁用指令重排** + +**synchronized 无法禁止指令重排和处理器优化**,为什么可以保证有序性? +加了锁之后,只能有一个线程获得到了锁,获得不到锁的线程就要阻塞,所以同一时间只有一个线程执行,相当于单线程,而单线程的指令重排是没有问题的 + +指令重排实例: + +* example 1: + + ```java + public void mySort() { + int x = 11; //语句1 + int y = 12; //语句2 谁先执行效果一样 + x = x + 5; //语句3 + y = x * x; //语句4 + } + ``` + + 执行顺序是:1 2 3 4、2 1 3 4、1 3 2 4 + 指令重排也有限制不会出现:4321,语句4需要依赖于 y 以及 x 的申明,因为存在数据依赖,无法首先执行 + +* example 2: + + ```java + int num = 0; + boolean ready = false; + // 线程1 执行此方法 + public void actor1(I_Result r) { + if(ready) { + r.r1 = num + num; + } else { + r.r1 = 1; + } + } + // 线程2 执行此方法 + public void actor2(I_Result r) { + num = 2; + ready = true; + } + ``` + + 情况一:线程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 + 发生指令重排 + + + +**** + + + +#### 底层原理 + +使用 volatile 修饰的共享变量,总线会开启 CPU 总线嗅探机制来解决 JMM 缓存一致性问题,也就是共享变量在多线程中可见性的问题,实现 MESI 缓存一致性协议 + +底层是通过汇编 lock 前缀指令,共享变量加了 lock 前缀指令,在线程修改完共享变量后,会马上执行 store 和 write 操作写回主存。在执行 store 操作前,会先执行缓存锁定的操作,其他的 CPU 上运行的线程根据 CPU 总线嗅探机制会修改其共享变量为失效状态,读取时会重新从主内存中读取最新的数据 + +lock 前缀指令就相当于内存屏障,Memory Barrier(Memory Fence) + +* 对 volatile 变量的写指令后会加入写屏障 +* 对 volatile 变量的读指令前会加入读屏障 + +内存屏障有三个作用: + +- 确保对内存的读-改-写操作原子执行 +- 阻止屏障两侧的指令重排序 +- 强制把缓存中的脏数据写回主内存,让缓存行中相应的数据失效 + +保证可见性: + +* 写屏障(sfence,Store Barrier)保证在该屏障之前的,对共享变量的改动,都同步到主存当中 + + ```java + public void actor2(I_Result r) { + num = 2; + ready = true; // ready 是 volatile 赋值带写屏障 + // 写屏障 + } + ``` + +* 读屏障(lfence,Load Barrier)保证在该屏障之后的,对共享变量的读取,加载的是主存中最新数据 + + ```java + public void actor1(I_Result r) { + // 读屏障 + // ready 是 volatile 读取值带读屏障 + if(ready) { + r.r1 = num + num; + } else { + r.r1 = 1; + } + } + ``` + + + +* 全能屏障:mfence(modify/mix Barrier),兼具 sfence 和 lfence 的功能 + +保证有序性: + +* 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后 +* 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前 + +不能解决指令交错: + +* 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证其他的读跑到写屏障之前 + +* 有序性的保证也只是保证了本线程内相关代码不被重排序 + + ```java + volatile i = 0; + new Thread(() -> {i++}); + new Thread(() -> {i--}); + ``` + + i++反编译后的指令: + + ```java + 0: iconst_1 //当int取值 -1~5 时,JVM采用iconst指令将常量压入栈中 + 1: istore_1 //将操作数栈顶数据弹出,存入局部变量表的 slot 1 + 2: iinc 1, 1 + ``` + + + + + +*** + + + +#### 双端检锁 + +##### 检锁机制 + +Double-Checked Locking:双端检锁机制 + +DCL(双端检锁)机制不一定是线程安全的,原因是有指令重排的存在,加入volatile可以禁止指令重排 + +```java +public final class Singleton { + private Singleton() { } + private static Singleton INSTANCE = null; + + public static Singleton getInstance() { + if(INSTANCE == null) { // t2 + // 首次访问会同步,而之后的使用没有 synchronized + synchronized(Singleton.class) { + if (INSTANCE == null) { // t1 + INSTANCE = new Singleton(); + } + } + } + return INSTANCE; + } +} +``` + +不锁INSTANCE的原因: + +* INSTANCE 要重新赋值 +* INSTANCE 是null,线程加锁之前需要获取对象的引用,null没有引用 + +实现特点: + +* 懒惰初始化 +* 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁 +* 第一个 if 使用了 INSTANCE 变量,是在同步块之外,但在多线程环境下会产生问题 + + + +*** + + + +##### DCL问题 + +getInstance 方法对应的字节码为: + +```java +0: getstatic #2 // Field INSTANCE:Ltest/Singleton; +3: ifnonnull 37 +6: ldc #3 // class test/Singleton +8: dup +9: astore_0 +10: monitorenter +11: getstatic #2 // Field INSTANCE:Ltest/Singleton; +14: ifnonnull 27 +17: new #3 // class test/Singleton +20: dup +21: invokespecial #4 // Method "":()V +24: putstatic #2 // Field INSTANCE:Ltest/Singleton; +27: aload_0 +28: monitorexit +29: goto 37 +32: astore_1 +33: aload_0 +34: monitorexit +35: aload_1 +36: athrow +37: getstatic #2 // Field INSTANCE:Ltest/Singleton; +40: areturn +``` + +* 17 表示创建对象,将对象引用入栈 +* 20 表示复制一份对象引用,引用地址 +* 21 表示利用一个对象引用,调用构造方法初始化对象 +* 24 表示利用一个对象引用,赋值给 static INSTANCE + +步骤 21 和步骤 24 之间不存在数据依赖关系,而且无论重排前后,程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的 + +* 关键在于 0:getstatic 这行代码在 monitor 控制之外,可以越过 monitor 读取INSTANCE 变量的值 +* 当其他线程访问 instance 不为 null 时,由于 instance 实例未必已初始化,那么 t2 拿到的是将是一个未初 + 始化完毕的单例返回,这就造成了线程安全的问题 + +![](https://gitee.com/seazean/images/raw/master/Java/JMM-DCL出现的问题.png) + + + +*** + + + +##### 解决方法 + +指令重排只会保证串行语义的执行一致性(单线程),但并不会关系多线程间的语义一致性 + +引入volatile,来保证出现指令重排的问题,从而保证单例模式的线程安全性: + +```java +private static volatile SingletonDemo INSTANCE = null; +``` + + + +*** + + + +### ha-be + +happens-before 先行发生 + +Java 内存模型具备一些先天的“有序性”,即不需要通过任何同步手段(volatile、synchronized等)就能够得到保证的安全,这个通常也称为 happens-before 原则,它是可见性与有序性的一套规则总结 + +不符合 happens-before 规则,JMM 并不能保证一个线程的可见性和有序性 + +1. 程序次序规则 (Program Order Rule):一个线程内,逻辑上书写在前面的操作先行发生于书写在后面的操作 ,因为多个操作之间有先后依赖关系,则不允许对这些操作进行重排序 + +2. 锁定规则 (Monitor Lock Rule):一个 unLock 操作先行发生于后面(时间的先后)对同一个锁的 lock 操作,所以线程解锁 m 之前对变量的写(锁内的写),对于接下来对 m 加锁的其它线程对该变量的读可见 + +3. **volatile变量规则** (Volatile Variable Rule):对 volatile 变量的写操作先行发生于后面对这个变量的读 + +4. 传递规则 (Transitivity):具有传递性,如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C + +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()方法的调用先行发生于被中断线程的代码检测到中断事件的发生 + +7. 线程终止规则 (Thread Termination Rule):线程中所有的操作都先行发生于线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行 + +8. 对象终结规则(Finaizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始 + + + +*** + + + +### 设计模式 + +#### 终止模式 + +终止模式之两阶段终止模式:停止标记用 volatile 是为了保证该变量在多个线程之间的可见性 + +```java +class TwoPhaseTermination { + //监控线程 + private Thread monitor; + //停止标记 + private volatile boolean stop = false;; + + //启动监控线程 + public void start() { + monitor = new Thread(() -> { + while (true) { + Thread thread = Thread.currentThread(); + if (stop) { + System.out.println("后置处理"); + break; + } + try { + Thread.sleep(1000);//睡眠 + System.out.println(thread.getName() + "执行监控记录"); + } catch (InterruptedException e) { + System.out.println("被打断,退出睡眠"); + } + } + }); + monitor.start(); + } + + //停止监控线程 + public void stop() { + stop = true; + monitor.interrupt();//让线程尽快退出Timed Waiting + } +} +//测试 +public static void main(String[] args) throws InterruptedException { + TwoPhaseTermination tpt = new TwoPhaseTermination(); + tpt.start(); + Thread.sleep(3500); + System.out.println("停止监控"); + tpt.stop(); +} +``` + + + +**** + + + +#### Balking + +Balking (犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做 +了,直接结束返回 + +```java +public class MonitorService { + // 用来表示是否已经有线程已经在执行启动了 + private volatile boolean starting = false; + public void start() { + System.out.println("尝试启动监控线程..."); + synchronized (this) { + if (starting) { + return; + } + starting = true; + } + // 真正启动监控线程... + } +} +``` + +对比保护性暂停模式:保护性暂停模式用在一个线程等待另一个线程的执行结果,当条件不满足时线程等待 + +例子:希望 doInit() 方法仅被调用一次,下面的实现出现的问题: + +* 当 t1 线程进入 init() 准备 doInit(),t2 线程进来,initialized 还为f alse,则 t2 就又初始化一次 +* volatile 适合一个线程写,其他线程读的情况,这个代码需要加锁 + +```java +public class TestVolatile { + volatile boolean initialized = false; + + void init() { + if (initialized) { + return; + } + doInit(); + initialized = true; + } + private void doInit() { + } +} +``` + + + + + +**** + + + + + +## 无锁 + +### CAS + +#### 实现原理 + +无锁编程:lock free + +CAS的全称是Compare-And-Swap,是**CPU并发原语** + +* CAS并发原语体现在Java语言中就是sun.misc.Unsafe类的各个方法,调用UnSafe类中的CAS方法,JVM会实现出CAS汇编指令,这是一种完全依赖于硬件的功能,通过它实现了原子操作 +* CAS是一种系统原语,原语属于操作系统范畴,是由若干条指令组成 ,用于完成某个功能的一个过程,并且**原语的执行必须是连续的,执行过程中不允许被中断**,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致的问题,所以 CAS 是线程安全的 + +底层原理:CAS 的底层是 `lock cmpxchg` 指令(X86 架构),在单核和多核 CPU 下都能够保证比较交换的原子性 + +* 程序是在单核处理器上运行,会省略 lock 前缀,单处理器自身会维护处理器内的顺序一致性,不需要 lock 前缀的内存屏障效果 + +* 程序是在多核处理器上运行,会为 cmpxchg 指令加上 lock 前缀(lock cmpxchg)。当某个核执行到带 lock 的指令时,CPU 会执行**总线锁定或缓存锁定**,当这个核把此指令执行完毕,这个过程不会被线程的调度机制所打断,保证了多个线程对内存操作的原子性 + +作用:比较当前工作内存中的值和主物理内存中的值,如果相同则执行规定操作,否者继续比较直到主内存和工作内存的值一致为止 + +CAS特点: + +* CAS 体现的是**无锁并发、无阻塞并发**,没有使用 synchronized,所以线程不会陷入阻塞,线程不需要频繁切换状态(上下文切换,系统调用) +* CAS 是基于乐观锁的思想 + +CAS缺点: + +- 循环时间长,开销大(因为执行的是do while,如果比较不成功一直在循环,最差的情况某个线程一直取到的值和预期值都不一样,就会无限循环导致饥饿),**使用CAS线程数不要超过CPU的核心数** +- 只能保证一个共享变量的原子操作 + - 对于一个共享变量执行操作时,可以通过循环CAS的方式来保证原子操作 + - 对于多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候只能用锁来保证原子性 +- 引出来ABA问题 + + + + + +*** + + + +#### 乐观锁 + +CAS与Synchronized总结: + +* Synchronized是从悲观的角度出发: + 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。(**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**),因此Synchronized我们也将其称之为悲观锁。jdk中的ReentrantLock也是一种悲观锁,**性能较差** +* CAS是从乐观的角度出发: + 总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。**如果别人修改过,则获取现在最新的值。如果别人没修改过,直接修改共享数据的值**,CAS这种机制我们也可以将其称之为乐观锁。**综合性能较好**! + + + + + +*** + + + +### Atomic + +#### 常用API + +常见原子类:AtomicInteger、AtomicBoolean、AtomicLong + +构造方法: + +* `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 | + + + +*** + + + +#### 原理分析 + +**AtomicInteger原理**:自旋锁 + CAS 算法 + +CAS算法:有3个操作数(内存值V, 旧的预期值A,要修改的值B) + +* 当旧的预期值A == 内存值V 此时可以修改,将V改为B +* 当旧的预期值A != 内存值V 此时不能修改,并重新获取现在的最新值,重新获取的动作就是自旋 + +分析getAndSet方法: + +* AtomicInteger: + + ```java + public final int getAndSet(int newValue) { + /** + * this: 当前对象 + * valueOffset: 内存偏移量,内存地址 + */ + return unsafe.getAndSetInt(this, valueOffset, newValue); + } + ``` + + valueOffset:表示该变量值在内存中的偏移地址,Unsafe就是根据内存偏移地址获取数据 + + ```java + valueOffset = unsafe.objectFieldOffset + (AtomicInteger.class.getDeclaredField("value")); + //调用本地方法 --> + public native long objectFieldOffset(Field var1); + ``` + +* unsafe类: + + ```java + //val1: AtomicInteger对象本身 var2: 该对象值得引用地址 var4: 需要变动的数 + public final int getAndSetInt(Object var1, long var2, int var4) { + int var5; + do { + //var5: 用var1和var2找到的内存中的真实值 + var5 = this.getIntVolatile(var1, var2); + } while(!this.compareAndSwapInt(var1, var2, var5, var4)); + + return var5; + } + ``` + + var5:从主内存中拷贝到工作内存中的值(每次都要从主内存拿到最新的值到自己的本地内存),然后执行`compareAndSwapInt()`再和主内存的值进行比较,假设方法返回false,那么就一直执行 while方法,直到期望的值和真实值一样,修改数据 + +* 变量value用volatile修饰,保证了多线程之间的内存可见性,避免线程从自己的工作缓存中查找变量 + + ```java + private volatile int value + ``` + + CAS 必须借助 volatile 才能读取到共享变量的最新值来实现**比较并交换**的效果 + +分析getAndUpdate方法: + +* getAndUpdate: + + ```java + public final int getAndUpdate(IntUnaryOperator updateFunction) { + int prev, next; + do { + prev = get(); //当前值,cas的期望值 + next = updateFunction.applyAsInt(prev);//期望值更新到该值 + } while (!compareAndSet(prev, next));//自旋 + return prev; + } + ``` + + 函数式接口:可以自定义操作逻辑 + + ```java + AtomicInteger a = new AtomicInteger(); + a.getAndUpdate(i -> i + 10); + ``` + +* compareAndSet: + + ```java + public final boolean compareAndSet(int expect, int update) { + /** + * this: 当前对象 + * valueOffset: 内存偏移量,内存地址 + * expect: 期望的值 + * update: 更新的值 + */ + return unsafe.compareAndSwapInt(this, valueOffset, expect, update); + } + ``` + + + + + +*** + + + +#### 原子引用 + +原子引用:对Object进行原子操作,提供一种读和写都是原子性的对象引用变量 + +原子引用类:AtomicReference、AtomicStampedReference、AtomicMarkableReference + +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()`:返回当前值 + +```java +public class AtomicReferenceDemo { + public static void main(String[] args) { + Student s1 = new Student(33,"z3"); + + //创建原子引用包装类 + AtomicReference atomicReference = new AtomicReference<>(); + //设置主内存共享变量为s1 + atomicReference.set(s1); + + //比较并交换,如果现在主物理内存的值为z3,那么交换成l4 + while (true) { + Student s2 = new Student(44,"l4"); + if (atomicReference.compareAndSet(s1, s2)) { + break; + } + } + System.out.println(atomicReference.get()); + } +} + +class Student { + private int id; + private String name; + //。。。。 +} +``` + + + +*** + + + +#### 原子数组 + +原子数组类:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray + +AtomicIntegerArray类方法: + +```java +/** +* i the index +* expect the expected value +* update the new value +*/ +public final boolean compareAndSet(int i, int expect, int update) { + return compareAndSetRaw(checkedByteOffset(i), expect, update); +} +``` + + + +*** + + + +#### 原子更新器 + +原子更新器类:AtomicReferenceFieldUpdater、AtomicIntegerFieldUpdater、AtomicLongFieldUpdater + +利用字段更新器,可以针对对象的某个域(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 + +```java +public class UpdateDemo { + private volatile int field; + + public static void main(String[] args) { + AtomicIntegerFieldUpdater fieldUpdater = AtomicIntegerFieldUpdater + .newUpdater(UpdateDemo.class, "field"); + UpdateDemo updateDemo = new UpdateDemo(); + fieldUpdater.compareAndSet(updateDemo, 0, 10); + System.out.println(updateDemo.field);//10 + } +} +``` + + + +*** + + + +#### 原子累加器 + +原子累加器类:LongAdder、DoubleAdder、LongAccumulator、DoubleAccumulator + +LongAdder和LongAccumulator区别: + +相同点: + +* LongAddr与LongAccumulator类都是使用非阻塞算法CAS实现的, +* LongAddr类是LongAccumulator类的一个特例,只是LongAccumulator提供了更强大的功能,可以自定义累加规则,当accumulatorFunction为null时就等价于LongAddr + +不同点: + +* 调用casBase时,LongAccumulator使用function.applyAsLong(b = base, x)来计算,LongAddr使用casBase(b = base, b + x)来计算 +* LongAccumulator类功能更加强大,构造方法参数中 + + * accumulatorFunction是一个双目运算器接口,可以指定累加规则,比如累加或者相乘,其根据输入的两个参数返回一个计算值,LongAdder内置累加规则 + * identity则是LongAccumulator累加器的初始值,LongAccumulator可以为累加器提供非0的初始值,而LongAdder只能提供默认的0 + + + + +*** + + + +### 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 重试失败,从而提高性能 + + + +*** + + + +##### 分段迁移 + +自动分段迁移机制:某个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为累加单元:数组访问索引是通过Thread里的threadLocalRandomProbe域取模实现的,这个域是ThreadLocalRandom更新的 + +```java +@sun.misc.Contended static final class Cell { + volatile long value; + Cell(long x) { value = x; } + // 最重要的方法, 用来 cas 方式进行累加, prev 表示旧值, next 表示新值 + final boolean cas(long prev, long next) { + return UNSAFE.compareAndSwapLong(this, valueOffset, prev, next); + } + // 省略不重要代码 +} +``` + +@sun.misc.Contended注解:防止缓存行伪共享 + +Cell 是数组形式,**在内存中是连续存储的**,一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value),每一个 cache line 为 64 字节,因此缓存行可以存下 2 个的 Cell 对象,当Core-0 要修改 Cell[0]、Core-1 要修改 Cell[1],无论谁修改成功都会导致对方 Core 的缓存行失效,需要重新去主存获取 + +![](https://gitee.com/seazean/images/raw/master/Java/JUC-伪共享1.png) + +@sun.misc.Contended:在使用此注解的对象或字段的前后各增加 128 字节大小的padding,使用2倍于大多数硬件缓存行让 CPU 将对象预读至缓存时占用不同的缓存行,这样就不会造成对方缓存行的失效 + +![](https://gitee.com/seazean/images/raw/master/Java/JUC-伪共享2.png) + + + + + +*** + + + +#### 成员方法 + +* add:累加方法 + + ```java + public void add(long x) { + // as 为累加单元数组 b 为基础值 x 为累加值 + Cell[] as; long b, v; int m; Cell a; + // 1. as 有值, 表示已经发生过竞争, 进入 if + // 2. cas 给 base 累加时失败了, 表示 base 发生了竞争, 进入 if + if ((as = cells) != null || !casBase(b = base, b + x)) { + // uncontended 表示 cell 没有竞争 + boolean uncontended = true; + if ( + // as 还没有创建 + as == null || (m = as.length - 1) < 0 || + // 当前线程对应的 cell 还没有创建 + (a = as[getProbe() & m]) == null || + // 当前线程的cell累加失败,a为当前线程的cell + !(uncontended = a.cas(v = a.value, v + x)) + //uncontended = false代表有竞争 + ) { + // 进入 cell 数组创建、cell 创建的流程 + longAccumulate(x, null, uncontended); + } + } + } + ``` + +* longAccumulate:cell数组创建 + + ```java + // x null false + final void longAccumulate(long x, LongBinaryOperator fn, boolean w...ed) { + int h; + // 当前线程还没有对应的 cell, 需要随机生成一个 h 值用来将当前线程绑定到 cell + if ((h = getProbe()) == 0) { + ThreadLocalRandom.current(); // 初始化 probe + h = getProbe(); //h 对应新的 probe 值, 用来对应 cell + wasUncontended = true; + } + //collide 为 true 表示需要扩容 + boolean collide = false; + for (;;) { + Cell[] as; Cell a; int n; long v; + // cells已经创建 + if ((as = cells) != null && (n = as.length) > 0) { + // 线程对应的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; + try { + Cell[] rs; int m, j; + if ((rs = cells) != null && + (m = rs.length) > 0 && + rs[j = (m - 1) & h] == null) { + rs[j] = r; + created = true; + } + } finally { + cellsBusy = 0; + } + if (created) + break;// 成功则 break, 否则继续 continue 循环 + continue; + } + } + collide = false; + } + // 有竞争, 改变线程对应的 cell 来重试 cas + else if (!wasUncontended) + wasUncontended = true; + //cas尝试累加, fn配合LongAccumulator不为null, 配合LongAdder为null + else if (a.cas(v = a.value, ((fn == null) ? v + x : + fn.applyAsLong(v, x)))) + break; + // cells长度已经超过了最大长度或者已经扩容, 改变线程对应的cell来重试cas + else if (n >= NCPU || cells != as) + collide = false; // At max size or stale + // collide 为 false 进入此分支, 就不会进入下面的 else if 进行扩容了 + else if (!collide) + collide = true; + //加锁扩容 + else if (cellsBusy == 0 && casCellsBusy()) { + try { + if (cells == as) { // Expand table unless stale + Cell[] rs = new Cell[n << 1]; + for (int i = 0; i < n; ++i) + rs[i] = as[i]; + cells = rs; + } + } finally { + cellsBusy = 0; + } + collide = false; + continue;、 + } + h = advanceProbe(h); + } + //还没有 cells, 尝试给 cellsBusy 加锁 + else if (cellsBusy == 0 && cells == as && casCellsBusy()) { + boolean init = false; + try { + // 初始化 cells, 最开始长度为2, 填充一个初始累加值为x的cell + if (cells == as) { + Cell[] rs = new Cell[2]; + rs[h & 1] = new Cell(x);//填充线程对应的cell + cells = rs; + init = true; + } + } finally { + cellsBusy = 0; + } + if (init) + break; + } + // 上两种情况失败, 尝试给 base 累加 + else if (casBase(v = base, ((fn == null) ? v + x : + fn.applyAsLong(v, x)))) + break; // Fall back on using base + } + } + ``` + +* sum:获取最终结果通过 sum 整合 + + + +*** + + + +### ABA + +ABA问题:当进行获取主内存值时,该内存值在写入主内存时已经被修改了N次,但是最终又改成原来的值 + +其他线程先把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 void set(V newReference, int newStamp)`:设置值和版本号 + + `public V getReference()`:返回引用的值 + `public int getStamp()`:返回当前版本号 + +```java +public static void main(String[] args) { + AtomicStampedReference atomicReference = new AtomicStampedReference<>(100,1); + int startStamp = atomicReference.getStamp(); + new Thread(() ->{ + int stamp = atomicReference.getStamp(); + atomicReference.compareAndSet(100, 101, stamp, stamp + 1); + stamp = atomicReference.getStamp(); + atomicReference.compareAndSet(101, 100, stamp, stamp + 1); + },"t1").start(); + + new Thread(() ->{ + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + if (!atomicReference.compareAndSet(100, 200, startStamp, startStamp + 1)) { + System.out.println(atomicReference.getReference());//100 + System.out.println(Thread.currentThread().getName() + "线程修改失败"); + } + },"t2").start(); +} +``` + + + + + +*** + + + +### Unsafe + +Unsafe是CAS的核心类,由于Java无法直接访问底层系统,需要通过本地 (Native) 方法来访问 + +Unsafe类存在sun.misc包,其中所有方法都是native修饰的,都是直接调用操作系统底层资源执行相应的任务,基于该类可以直接操作特定的内存数据,其内部方法操作类似C的指针 + +模拟实现原子整数: + +```java +public static void main(String[] args) { + MyAtomicInteger atomicInteger = new MyAtomicInteger(10); + if (atomicInteger.compareAndSwap(20)) { + System.out.println(atomicInteger.getValue()); + } +} + +class MyAtomicInteger { + private static final Unsafe UNSAFE; + private static final long VALUE_OFFSET; + private volatile int value; + + static { + try { + Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); + theUnsafe.setAccessible(true); + UNSAFE = (Unsafe) theUnsafe.get(null); + VALUE_OFFSET = UNSAFE.objectFieldOffset( + MyAtomicInteger.class.getDeclaredField("value")); + } catch (NoSuchFieldException | IllegalAccessException e) { + e.printStackTrace(); + throw new RuntimeException(); + } + } + + public MyAtomicInteger(int value) { + this.value = value; + } + public int getValue() { + return value; + } + + public boolean compareAndSwap(int update) { + while (true) { + int prev = this.value; + int next = update; + // 当前对象 内存偏移量 期望值 更新值 + if (UNSAFE.compareAndSwapInt(this, VALUE_OFFSET, prev, update)) { + System.out.println("CAS成功"); + return true; + } + } + } +} +``` + + + +*** + + + +### final + +#### 原理 + +```java +public class TestFinal { + final int a = 20; +} +``` + +字节码: + +```java +0: aload_0 +1: invokespecial #1 // Method java/lang/Object."":()V +4: aload_0 +5: bipush 20 //将值直接放入栈中 +7: putfield #2 // Field a:I +<-- 写屏障 +10: return +``` + +final 变量的赋值通过 putfield 指令来完成,在这条指令之后也会加入写屏障,保证在其它线程读到它的值时不会出现为 0 的情况 + +其他线程访问final修饰的变量会复制一份放入栈中,效率更高 + + + +*** + + + +#### 不可变 + +不可变:如果一个对象不能够修改其内部状态(属性),那么就是不可变对象 + +不可变对象线程安全的,因为不存在并发修改,是另一种避免竞争的方式 + +String 类也是不可变的,该类和类中所有属性都是 final 的 + +* 类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性保 + +* 属性用 final 修饰保证了该属性是只读的,不能修改 + + ```java + public final class String + implements java.io.Serializable, Comparable, CharSequence { + /** The value is used for character storage. */ + private final char value[]; + //.... + } + ``` + +更改 String 类数据时,会构造新字符串对象,生成新的 char[] value,这种通过创建副本对象来避免共享的方式称之为**保护性拷贝(defensive copy)** + + + +*** + + + +### State + +无状态:成员变量保存的数据也可以称为状态信息,无状态就是没有成员变量 + +Servlet 为了保证其线程安全,一般不为 Servlet 设置成员变量,这种没有任何成员变量的类是线程安全的 + + + +*** + + + +### Local + +#### 基本介绍 + +ThreadLocal类用来提供线程内部的局部变量,这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量 + +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,以当前ThreadLocal为key删除对应的实体entry + 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 + + + + + +*** + + + + + +## 线程池 + +### 基本概述 + +线程池:一个容纳多个线程的容器,容器中的线程可以重复使用,省去了频繁创建和销毁线程对象的操作 + +线程池作用: + +1. 降低资源消耗,减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务 +2. 提高响应速度,当任务到达时,如果有线程可以直接用,不会出现系统僵死 +3. 提高线程的可管理性,如果无限制的创建线程,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控 + +线程池的核心思想:**线程复用**,同一个线程可以被重复使用,来处理多个任务 + +池化技术 (Pool) :一种编程技巧,核心思想是资源复用,在请求量大时能优化应用性能,降低系统频繁建连的资源开销 + + + +*** + + + +### 阻塞队列 + +#### 基本介绍 + +有界队列和无界队列: + +* 有界队列:有固定大小的队列,比如设定了固定大小的 LinkedBlockingQueue,又或者大小为 0 + +* 无界队列:没有设置固定大小的队列,这些队列可以直接入队,直到溢出,一般不会有到这么大的容量(超过 Integer.MAX_VALUE),所以相当于 “无界” + +java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:**FIFO 队列** + +- ArrayBlockQueue:由数组结构组成的有界阻塞队列 +- LinkedBlockingQueue:由链表结构组成的无界(默认大小 Integer.MAX_VALUE)的阻塞队列 +- PriorityBlockQueue:支持优先级排序的无界阻塞队列 +- DelayQueue:使用优先级队列实现的延迟无界阻塞队列 +- SynchronousQueue:不存储元素的阻塞队列,每一个生产线程会阻塞到有一个put的线程放入元素为止 +- LinkedTransferQueue:由链表结构组成的无界阻塞队列 +- LinkedBlockingDeque:由链表结构组成的**双向**阻塞队列 + +与普通队列(LinkedList、ArrayList等)的不同点在于阻塞队列中阻塞添加和阻塞删除方法,以及线程安全: + +* 阻塞添加 take():当阻塞队列元素已满时,添加队列元素的线程会被阻塞,直到队列元素不满时才重新唤醒线程执行 +* 阻塞删除 put():在队列元素为空时,删除队列元素的线程将被阻塞,直到队列不为空再执行删除操作(一般都会返回被删除的元素) + + + +*** + + + +#### 核心方法 + +| 方法类型 | 抛出异常 | 特殊值 | 阻塞 | 超时 | +| ---------------- | --------- | -------- | ------ | ------------------ | +| 插入 | add(e) | offer(e) | put(e) | offer(e,time,unit) | +| 移除 | remove() | poll() | take() | poll(time,unit) | +| 检查(队首元素) | element() | peek() | 不可用 | 不可用 | + +* 抛出异常组: + * 当阻塞队列满时:在往队列中 add 插入元素会抛出 IIIegalStateException: Queue full + * 当阻塞队列空时:再往队列中 remove 移除元素,会抛出 NoSuchException +* 特殊值组: + * 插入方法:成功 true,失败 false + * 移除方法:成功返回出队列元素,队列没有就返回 null +* 阻塞组: + * 当阻塞队列满时,生产者继续往队列里 put 元素,队列会一直阻塞生产线程直到 put 数据或响应中断退出 + * 当阻塞队列空时,消费者线程试图从队列里 take 元素,队列会一直阻塞消费者线程直到队列可用 +* 超时退出:当阻塞队列满时,队里会阻塞生产者线程一定时间,超过限时后生产者线程会退出 + + + +*** + + + +#### 链表队列 + +##### 入队出队 + +LinkedBlockingQueue源码: + +```java +public class LinkedBlockingQueue extends AbstractQueue + implements BlockingQueue, java.io.Serializable { + static class Node { + E item; + /** + * 下列三种情况之一 + * - 真正的后继节点 + * - 自己, 发生在出队时 + * - null, 表示是没有后继节点, 是最后了 + */ + Node next; + + Node(E x) { item = x; } + } +} +``` + +入队: + +* 初始化链表 `last = head = new Node(null)`,Dummy 节点用来占位,item 为 null + +* 当一个节点入队 `last = last.next = node` + ![](https://gitee.com/seazean/images/raw/master/Java/JUC-LinkedBlockingQueue入队流程.png) + +* 再来一个节点入队`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; + ``` + +* `h = head` -> `first = h.next` + + ![](https://gitee.com/seazean/images/raw/master/Java/JUC-LinkedBlockingQueue出队流程1.png) + +* `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**) + + + + + +*** + + + +##### 加锁分析 + +用了两把锁和 dummy 节点: + +* 用一把锁,同一时刻,最多只允许有一个线程(生产者或消费者,二选一)执行 +* 用两把锁,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行 + * 消费者与消费者线程仍然串行 + * 生产者与生产者线程仍然串行 + +线程安全分析: + +* 当节点总数大于 2 时(包括 dummy 节点),putLock 保证的是 last 节点的线程安全,takeLock 保证的是 + head 节点的线程安全,两把锁保证了入队和出队没有竞争 + +* 当节点总数等于 2 时(即一个 dummy 节点,一个正常节点)这时候,仍然是两把锁锁两个对象,不会竞争 + +* 当节点总数等于 1 时(就一个 dummy 节点)这时 take 线程会被 notEmpty 条件阻塞,有竞争,会阻塞 + + ```java + // 用于 put(阻塞) offer(非阻塞) + private final ReentrantLock putLock = new ReentrantLock(); + // 用户 take(阻塞) poll(非阻塞) + private final ReentrantLock takeLock = new ReentrantLock(); + ``` + +入队出队: + +* put 操作: + + ```java + public void put(E e) throws InterruptedException { + if (e == null) throw new NullPointerException(); + int c = -1; + 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线程 + if (c + 1 < capacity) + notFull.signal(); + } finally { + putLock.unlock(); + } + // 如果还有一个元素,唤醒 take 线程 + if (c == 0) + signalNotEmpty(); + } + private void signalNotEmpty() { + final ReentrantLock takeLock = this.takeLock; + takeLock.lock(); + try { + // 调用的是 notEmpty.signal(),而不是 notEmpty.signalAll(),是为了减少竞争 + // 因为只剩下一个元素 + notEmpty.signal(); + } finally { + takeLock.unlock(); + } + } + ``` + +* take 操作: + + ```java + public E take() throws InterruptedException { + E x; + 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 + x = dequeue(); + // 返回自减前的数子 + c = count.getAndDecrement(); + // 队列还有元素 + if (c > 1) + // 唤醒其他消费take线程 + notEmpty.signal(); + } finally { + takeLock.unlock(); + } + // 如果队列中只有一个空位时, 叫醒 put 线程 + // 如果有多个线程进行出队, 第一个线程满足 c == capacity, 但后续线程 c < capacity + if (c == capacity) + // 调用的是 notFull.signal() 而不是 notFull.signalAll() 是为了减少竞争 + signalNotFull(); + return x; + } + ``` + + + +*** + + + +##### 性能比较 + +主要列举 LinkedBlockingQueue 与 ArrayBlockingQueue 的性能比较: + +* Linked 支持有界,Array 强制有界 +* Linked 实现是链表,Array 实现是数组 +* Linked 是懒惰的,而 Array 需要提前初始化 Node 数组 +* Linked 每次入队会生成新 Node,而 Array 的 Node 是提前创建好的 +* Linked 两把锁,Array 一把锁 + + + + + +*** + + + +#### 同步队列 + +与其他BlockingQueue不同,SynchronousQueue是一个不存储元素的BlockingQueue + +(待更新) + + + +*** + + + +#### 延迟队列 + +##### 延迟阻塞 + +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)); + } +} +``` + + + +**** + + + +##### 优先队列 + + + +*** + + + +### 操作Pool + +#### 创建方法 + +##### Executor + +存放线程的容器: + +```java +private final HashSet workers = new HashSet(); +``` + +构造方法: + +```java +public ThreadPoolExecutor(int corePoolSize, + int maximumPoolSize, + long keepAliveTime, + TimeUnit unit, + BlockingQueue workQueue, + ThreadFactory threadFactory, + RejectedExecutionHandler handler) +``` + +参数介绍: + +* corePoolSize:核心线程数,定义了最小可以同时运行的线程数量 + +* maximumPoolSize:最大线程数,当队列中存放的任务达到队列容量时,当前可以同时运行的数量变为最大线程数,创建线程并立即执行最新的任务,与核心线程数之间的差值又叫救急线程数 + +* keepAliveTime:救急线程最大存活时间,当线程池中的线程数量大于 `corePoolSize` 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等到`keepAliveTime`时间超过销毁 + +* unit:`keepAliveTime` 参数的时间单位 + +* workQueue:阻塞队列,被提交但尚未被执行的任务 + +* threadFactory:线程工厂,创建新线程时用到,可以为线程创建时起名字 + +* handler:拒绝策略,线程到达最大线程数仍有新任务时会执行拒绝策略 + + RejectedExecutionHandler下有4个实现类: + + * AbortPolicy:让调用者抛出 RejectedExecutionException 异常,默认策略 + * CallerRunsPolicy:"调用者运行"的调节机制,将某些任务回退到调用者,从而降低新任务的流量 + * DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常 + * DiscardOldestPolicy:放弃队列中最早的任务,把当前任务加入队列中尝试再次提交当前任务 + + 补充:其他框架拒绝策略 + + * Dubbo:在抛出 RejectedExecutionException 异常前记录日志,并 dump 线程栈信息,方便定位问题 + * Netty:创建一个新线程来执行任务 + * ActiveMQ:带超时等待(60s)尝试放入队列 + * PinPoint:它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略 + +工作原理: + +![](https://gitee.com/seazean/images/raw/master/Java/JUC-线程池工作原理.png) + +1. 创建线程池,这时没有创建线程(**懒惰**),等待提交过来的任务请求,调用execute方法才会创建线程 + +2. 当调用 execute() 方法添加一个请求任务时,线程池会做如下判断: + * 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务 + * 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列 + * 如果这时队列满了且正在运行的线程数量还小于 maximumPoolSize,那么会创建非核心线程**立刻运行**这个任务(对于阻塞队列中的任务不公平) + * 如果队列满了且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会启动饱和**拒绝策略**来执行 +3. 当一个线程完成任务时,会从队列中取下一个任务来执行 + +4. 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断:如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉,所以线程池的所有任务完成后最终会收缩到 corePoolSize 大小 + + + + + +*** + + + +##### Executors + +Executors提供了四种线程池的创建:newCachedThreadPool、newFixedThreadPool、newSingleThreadExecutor、newScheduledThreadPool + +* newFixedThreadPool:创建一个拥有 n 个线程的线程池 + + ```java + public static ExecutorService newFixedThreadPool(int nThreads) { + return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue()); + } + ``` + + * 核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间 + * LinkedBlockingQueue是一个单向链表实现的阻塞队列,默认大小为`Integer.MAX_VALUE`,也就是无界队列,可以放任意数量的任务,在任务比较多的时候会导致 OOM(内存溢出) + * 适用于任务量已知,相对耗时的长期任务 + +* newCachedThreadPool:创建一个可扩容的线程池 + + ```java + public static ExecutorService newCachedThreadPool() { + return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, + new SynchronousQueue()); + } + ``` + + * 核心线程数是 0, 最大线程数是 Integer.MAX_VALUE,全部都是救急线程(60s 后可以回收),可能会创建大量线程,从而导致 **OOM** + * SynchronousQueue 作为阻塞队列,没有容量,对于每一个take的线程会阻塞直到有一个put的线程放入元素为止(类似一手交钱、一手交货) + + * 适合任务数比较密集,但每个任务执行时间较短的情况 + +* newSingleThreadExecutor:创建一个只有1个线程的单线程池 + + ```java + public static ExecutorService newSingleThreadExecutor() { + return new FinalizableDelegatedExecutorService + (new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue())); + } + ``` + + * 保证所有任务按照**指定顺序执行**,线程数固定为 1,任务数多于 1 时会放入无界队列排队,任务执行完毕,这唯一的线程也不会被释放 + + 对比: + + * 创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一 + 个线程,保证池的正常工作 + + * Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改。F...D..ExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法 + + 原因:父类不能直接调用子类中的方法,需要反射或者创建对象的方式,可以调用子类静态方法 + + * Executors.newFixedThreadPool(1) 初始时为1,可以修改。对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改 + + ![](https://gitee.com/seazean/images/raw/master/Java/JUC-newSingleThreadExecutor.png) + +*** + + + +#### 开发要求 + +阿里巴巴 Java 开发手册要求: + +- **线程资源必须通过线程池提供,不允许在应用中自行显式创建线程** + + - 使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题 + - 如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题 + +- 线程池不允许使用Executors去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式更加明确线程池的运行规则,规避资源耗尽的风险 + + Executors返回的线程池对象弊端如下: + + - FixedThreadPool 和 SingleThreadPool: + - 运行的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM + - CacheThreadPool 和 ScheduledThreadPool: + - 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM + +创建多大容量的线程池合适? + +* 一般来说池中**总线程数是核心池线程数量两倍**,确保当核心池有线程停止时,核心池外有线程进入核心池 + +* 过小会导致程序不能充分地利用系统资源、容易导致饥饿 + +* 过大会导致更多的线程上下文切换,占用更多内存 + 上下文切换:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态,任务从保存到再加载的过程就是一次上下文切换 + +核心线程数常用公式: + +- **CPU 密集型任务(N+1):** 这种任务消耗的是 CPU 资源,可以将核心线程数设置为 N (CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程发生缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间 + + CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行分析 + +- **I/O 密集型任务:** 这种系统 CPU 处于阻塞状态,用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用,因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N 或 CPU核数/ (1-阻塞系数),阻塞系数在0.8~0.9之间 + + IO 密集型就是涉及到网络读取,文件读取此类任务 ,特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上 + + + + + +*** + + + +#### 提交方法 + +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 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消 | + +execute和submit都属于线程池的方法,对比: + +* execute只能提交Runnable类型的任务,而submit既能提交Runnable类型任务也能提交Callable类型任务 + +* execute会直接抛出任务执行时的异常,submit会吞掉异常,可通过Future的get方法将任务执行时的异常重新抛出 + + + +*** + + + +#### 关闭方法 + +ExecutorService类API: + +| 方法 | 说明 | +| ----------------------------------------------------- | ------------------------------------------------------------ | +| void shutdown() | 线程池状态变为 SHUTDOWN,等待任务执行完后关闭线程池,不会接收新任务,但已提交任务会执行完 | +| List shutdownNow() | 线程池状态变为 STOP,用 interrupt 中断正在执行的任务,直接关闭线程池,不会接收新任务,会将队列中的任务返回, | +| boolean isShutdown() | 不在 RUNNING 状态的线程池,此执行者已被关闭,方法返回 true | +| boolean isTerminated() | 线程池状态是否是 TERMINATED,如果所有任务在关闭后完成,返回true | +| boolean awaitTermination(long timeout, TimeUnit unit) | 调用 shutdown 后,由于调用线程不会等待所有任务运行结束,如果它想在线程池 TERMINATED 后做些事情,可以利用此方法等待 | + + + +*** + + + +#### 处理异常 + +execute会直接抛出任务执行时的异常,submit会吞掉异常,有两种处理方法 + +方法1:主动捉异常 + +```java +ExecutorService executorService = Executors.newFixedThreadPool(1); +pool.submit(() -> { + try { + System.out.println("task1"); + int i = 1 / 0; + } catch (Exception e) { + e.printStackTrace(); + } +}); +``` + +方法2:使用 Future + +```java +ExecutorService executorService = Executors.newFixedThreadPool(1); +Future future = pool.submit(() -> { + System.out.println("task1"); + int i = 1 / 0; + return true; +}); +System.out.println(future.get()); +``` + + + +*** + + + +#### 状态信息 + +ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量 + +| 状态 | 高3位 | 接收新任务 | 处理阻塞任务队列 | 说明 | +| ---------- | ----- | ---------- | ---------------- | --------------------------------------- | +| RUNNING | 111 | Y | Y | | +| SHUTDOWN | 000 | N | Y | 不接收新任务,但处理阻塞队列剩余任务 | +| STOP | 001 | N | N | 中断正在执行的任务,并抛弃阻塞队列任务 | +| TIDYING | 010 | - | - | 任务全执行完毕,活动线程为0即将进入终结 | +| TERMINATED | 011 | - | - | 终止状态 | + +这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 cas 原子操作 +进行赋值 + +```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) + + + + + +*** + + + +### 任务调度 + +#### Timer + +Timer 实现定时功能,Timer 的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务 + +```java +private static void method1() { + Timer timer = new Timer(); + TimerTask task1 = new TimerTask() { + @Override + public void run() { + System.out.println("task 1"); + //int i = 1 / 0;//任务一的出错会导致任务二无法执行 + Thread.sleep(2000); + } + }; + TimerTask task2 = new TimerTask() { + @Override + public void run() { + System.out.println("task 2"); + } + }; + // 使用 timer 添加两个任务,希望它们都在 1s 后执行 + // 但由于 timer 内只有一个线程来顺序执行队列中的任务,因此任务1的延时,影响了任务2的执行 + timer.schedule(task1,1000);//17:45:56 c.ThreadPool [Timer-0] - task 1 + timer.schedule(task2,1000);//17:45:58 c.ThreadPool [Timer-0] - task 2 +} +``` + + + +*** + + + +#### Scheduled + +任务调度线程池 ScheduledThreadPoolExecutor 继承 ThreadPoolExecutor: + +构造方法:`Executors.newScheduledThreadPool(int corePoolSize)` + +```java +public ScheduledThreadPoolExecutor(int corePoolSize) { + super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, + new DelayedWorkQueue()); +} +``` + +常用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)`:定时执行任务,参数为初始延迟时间、间隔时间、单位 + +基本使用: + +* 延迟任务,但是出现异常并不会在控制台打印,也不会影响其他线程的执行 + + ```java + public static void main(String[] args){ + // 线程池大小为1时也是串行执行 + ScheduledExecutorService executor = Executors.newScheduledThreadPool(2); + // 添加两个任务,都在 1s 后同时执行 + executor.schedule(() -> { + System.out.println("任务1,执行时间:" + new Date()); + //int i = 1 / 0; + try { Thread.sleep(2000); } catch (InterruptedException e) { } + }, 1000, TimeUnit.MILLISECONDS); + + executor.schedule(() -> { + System.out.println("任务2,执行时间:" + new Date()); + }, 1000, TimeUnit.MILLISECONDS); + } + ``` + +* 定时任务 scheduleAtFixedRate:**一个任务的启动到下一个任务的启动**之间只要大于间隔时间,抢占到CPU就会立即执行 + + ```java + public static void main(String[] args) { + ScheduledExecutorService pool = Executors.newScheduledThreadPool(1); + System.out.println("start..." + new Date()); + + pool.scheduleAtFixedRate(() -> { + System.out.println("running..." + new Date()); + Thread.sleep(2000); + }, 1, 1, TimeUnit.SECONDS); + } + + /*start...Sat Apr 24 18:08:12 CST 2021 + running...Sat Apr 24 18:08:13 CST 2021 + running...Sat Apr 24 18:08:15 CST 2021 + running...Sat Apr 24 18:08:17 CST 2021 + ``` + +* 定时任务 scheduleWithFixedDelay:**一个任务的结束到下一个任务的启动之间**等于间隔时间,抢占到CPU就会立即执行,这个方法才是真正的设置两个任务之间的间隔 + + ```java + public static void main(String[] args){ + ScheduledExecutorService pool = Executors.newScheduledThreadPool(3); + System.out.println("start..." + new Date()); + + pool.scheduleWithFixedDelay(() -> { + System.out.println("running..." + new Date()); + Thread.sleep(2000); + }, 1, 1, TimeUnit.SECONDS); + } + /*start...Sat Apr 24 18:11:41 CST 2021 + running...Sat Apr 24 18:11:42 CST 2021 + running...Sat Apr 24 18:11:45 CST 2021 + running...Sat Apr 24 18:11:48 CST 2021 + ``` + + + +*** + + + +#### 定时任务 + +让每周四 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); + } +} +``` + + + +*** + + + +### ForkJoin + +Fork/Join:线程池的实现,体现是分治思想,适用于能够进行任务拆分的 cpu 密集型运算,用于**并行计算** + +任务拆分:是将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的一些计算,如归并排序、斐波那契数列都可以用分治思想进行求解 + +* Fork/Join 在分治的基础上加入了多线程,把每个任务的分解和合并交给不同的线程来完成,提升了运算效率 + +* ForkJoin 使用 ForkJoinPool 来启动,是一个特殊的线程池,默认会创建与 cpu 核心数大小相同的线程池 +* 任务有返回值继承 RecursiveTask,没有返回值继承 RecursiveAction + +```java +public static void main(String[] args) { + ForkJoinPool pool = new ForkJoinPool(4); + System.out.println(pool.invoke(new MyTask(5))); + //拆分 5 + MyTask(4) --> 4 + MyTask(3) --> +} + +// 1~ n 之间整数的和 +class MyTask extends RecursiveTask { + private int n; + + public MyTask(int n) { + this.n = n; + } + + @Override + public String toString() { + return "MyTask{" + "n=" + n + '}'; + } + + @Override + protected Integer compute() { + // 如果 n 已经为 1,可以求得结果了 + if (n == 1) { + return n; + } + // 将任务进行拆分(fork) + MyTask t1 = new MyTask(n - 1); + t1.fork(); + // 合并(join)结果 + int result = n + t1.join(); + return result; + } +} +``` + +继续拆分优化: + +```java +class AddTask extends RecursiveTask { + int begin; + int end; + public AddTask(int begin, int end) { + this.begin = begin; + this.end = end; + } + + @Override + public String toString() { + return "{" + begin + "," + end + '}'; + } + + @Override + protected Integer compute() { + // 5, 5 + if (begin == end) { + return begin; + } + // 4, 5 防止多余的拆分 提高效率 + if (end - begin == 1) { + return end + begin; + } + // 1 5 + int mid = (end + begin) / 2; // 3 + AddTask t1 = new AddTask(begin, mid); // 1,3 + t1.fork(); + AddTask t2 = new AddTask(mid + 1, end); // 4,5 + t2.fork(); + int result = t1.join() + t2.join(); + return result; + } +} +``` + +ForkJoinPool 实现了**工作窃取算法**来提高 CPU 的利用率: + +* 每个线程都维护了一个双端队列,用来存储需要执行的任务 +* 工作窃取算法允许空闲的线程从其它线程的双端队列中窃取一个任务来执行 +* 窃取的必须是**最晚的任务**,避免和队列所属线程发生竞争,但是队列中只有一个任务时还是会发生竞争 + + + +*** + + + +### 享元模式 + +享元模式 (Flyweight pattern): 用于减少创建对象的数量,以减少内存占用和提高性能,这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结构的方式 + +异步模式:让有限的工作线程(Worker Thread)来轮流异步处理无限多的任务,也可将其归类为分工模式,典型实现就是线程池 + +工作机制:享元模式尝试重用现有的同类对象,如果未找到匹配的对象,则创建新对象 + +自定义连接池: + +```java +public static void main(String[] args) { + Pool pool = new Pool(2); + for (int i = 0; i < 5; i++) { + new Thread(() -> { + Connection con = pool.borrow(); + try { + Thread.sleep(new Random().nextInt(1000)); + } catch (InterruptedException e) { + e.printStackTrace(); + } + pool.free(con); + }).start(); + } +} +class Pool { + //连接池的大小 + private final int poolSize; + //连接对象的数组 + private Connection[] connections; + //连接状态数组 0表示空闲 1表示繁忙 + private AtomicIntegerArray states; //int[] -> AtomicIntegerArray + + //构造方法 + public Pool(int poolSize) { + this.poolSize = poolSize; + this.connections = new Connection[poolSize]; + this.states = new AtomicIntegerArray(new int[poolSize]); + for (int i = 0; i < poolSize; i++) { + connections[i] = new MockConnection("连接" + (i + 1)); + } + } + + //使用连接 + public Connection borrow() { + while (true) { + for (int i = 0; i < poolSize; i++) { + if (states.get(i) == 0) { + if (states.compareAndSet(i, 0, 1)) { + System.out.println(Thread.currentThread().getName() + " borrow " + connections[i]); + return connections[i]; + } + } + } + //如果没有空闲连接,当前线程等待 + synchronized (this) { + try { + System.out.println(Thread.currentThread().getName() + " wait..."); + this.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + } + + //归还连接 + public void free(Connection con) { + for (int i = 0; i < poolSize; i++) { + if (connections[i] == con) {//判断是否是同一个对象 + states.set(i, 0);//不用cas的原因是只会有一个线程使用该连接 + synchronized (this) { + System.out.println(Thread.currentThread().getName() + " free " + con); + this.notifyAll(); + } + break; + } + } + } + +} + +class MockConnection implements Connection { + private String name; + //..... +} +``` + + + + + +**** + + + + + +## 同步器 + +### AQS + +#### 思想 + +AQS:AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架,许多同步类实现都依赖于它 + +* 用 state 属性来表示资源的状态(分**独占模式和共享模式**),子类需要定义如何维护这个状态,控制如何获取 + 锁和释放锁 + * 独占模式是只有一个线程能够访问资源,如ReentrantLock + * 共享模式允许多个线程访问资源,如Semaphore,ReentrantReadWriteLock是组合式 +* 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList(**同步队列:双向,便于出队入队**) +* 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet(**条件队列:单向**) + +AQS 核心思想: + +* 如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置锁定状态 + +* 被请求的共享资源被占用,AQS 用 CLH 队列实现线程阻塞等待以及被唤醒时锁分配的(park)机制,即将暂时获取不到锁的线程加入到队列中 + + CLH是一种基于单向链表的**高性能、公平的自旋锁**,AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配 + + + + + +*** + + + +#### 原理 + +设计思想: + +* 获取锁: + + ```java + while(state 状态不允许获取) {//tryAcquire(arg) + if(队列中还没有此线程) { + 入队并阻塞 park unpark + } + } + 当前线程出队 + ``` + +* 释放锁: + + ```java + if(state 状态允许了) {//tryRelease(arg) + 恢复阻塞的线程(s) + } + ``` + +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 + +waitstate设计: + +* 使用**volatile 修饰配合 cas**保证其修改时的原子性 + +* 表示Node节点的状态,有以下几种状态: + + ```java + //由于超时或中断,此节点被取消,不会再改变状态 + static final int CANCELLED = 1; + //此节点后面的节点已(或即将)被阻止(通过park),当前节点在释放或取消时必须唤醒后面的节点 + static final int SIGNAL = -1; + //此节点当前在条件队列中 + static final int CONDITION = -2; + //将releaseShared传播到其他节点 + static final int PROPAGATE = -3; + ``` + +阻塞恢复设计: + +* 使用 park & unpark 来实现线程的暂停和恢复,因为命令的先后顺序不影响结果 +* park & unpark 是针对线程的,而不是针对同步器的,因此控制粒度更为精细 +* park 线程可以通过 interrupt 打断 + +队列设计: + +* 使用了 FIFO 先入先出队列,并不支持优先级队列 + +* 设计时借鉴了 CLH 队列,CLH是一种单向无锁队列 + + + + ```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)) { + t.next = node; + return t; + } + } + } + } + ``` + + + +*** + + + +#### 模板 + +同步器的设计是基于模板方法模式,该模式是基于”继承“的,主要是为了在不改变模板结构的前提下在子类中重新定义模板中的内容以实现复用代码 + +* 使用者继承 `AbstractQueuedSynchronizer` 并重写指定的方法 +* 将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,这些模板方法会调用使用者重写的方法 + +AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的模板方法: + +```java +isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。 +tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。 +tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。 +tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 +tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。 +``` + +* 默认情况下,每个方法都抛出 `UnsupportedOperationException` +* 这些方法的实现必须是内部线程安全的 +* AQS 类中的其他方法都是 final ,所以无法被其他类使用,只有这几个方法可以被其他类使用 + + + +*** + + + +#### 自定义 + +自定义一个不可重入锁: + +```java +class MyLock implements Lock { + //独占锁 不可重入 + class MySync extends AbstractQueuedSynchronizer { + @Override + protected boolean tryAcquire(int arg) { + if (compareAndSetState(0, 1)) { + // 加上锁 设置 owner 为当前线程 + setExclusiveOwnerThread(Thread.currentThread()); + return true; + } + return false; + } + @Override //解锁 + protected boolean tryRelease(int arg) { + setExclusiveOwnerThread(null); + setState(0);//volatile 修饰的变量放在后面,防止指令重排 + return true; + } + @Override //是否持有独占锁 + protected boolean isHeldExclusively() { + return getState() == 1; + } + public Condition newCondition() { + return new ConditionObject(); + } + } + + private MySync sync = new MySync(); + + @Override //加锁(不成功进入等待队列等待) + public void lock() { + sync.acquire(1); + } + + @Override //加锁 可打断 + public void lockInterruptibly() throws InterruptedException { + sync.acquireInterruptibly(1); + } + + @Override //尝试加锁,尝试一次 + public boolean tryLock() { + return sync.tryAcquire(1); + } + + @Override //尝试加锁,带超时 + public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { + return sync.tryAcquireNanos(1, unit.toNanos(time)); + } + + @Override //解锁 + public void unlock() { + sync.release(1); + } + + @Override //条件变量 + public Condition newCondition() { + return sync.newCondition(); + } +} +``` + + + + + +*** + + + +### Re-Lock + +#### 锁对比 + +ReentrantLock相对于 synchronized 它具备如下特点: + +1. 锁的实现:synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的 +2. 性能:新版本 Java 对 synchronized 进行了很多优化,synchronized 与 ReentrantLock 大致相同 +3. 使用:ReentrantLock 需要手动解锁,synchronized 执行完代码块自动解锁 +4. 可中断:ReentrantLock 可中断,而 synchronized 不行 +5. **公平锁**:公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁 + * ReentrantLock 可以设置公平锁,synchronized 中的锁是非公平的 +6. 锁超时:尝试获取锁,超时获取不到直接放弃,不进入阻塞队列 + * ReentrantLock 可以设置超时时间,synchronized会一直等待 +7. 锁绑定多个条件:一个 ReentrantLock 可以同时绑定多个 Condition 对象 +8. 两者都是可重入锁 + + + +*** + + + +#### 使用锁 + +构造方法:`ReentrantLock lock = new ReentrantLock();` + +ReentrantLock类API: + +* `public void lock()`:获得锁 + * 如果锁没有被另一个线程占用,则将锁定计数设置为1。 + + * 如果当前线程已经保持锁定,则保持计数增加1 + + * 如果锁被另一个线程保持,则当前线程被禁用线程调度,并且在锁定已被获取之前处于休眠状态 + +* `public void unlock()`:尝试释放锁 + * 如果当前线程是该锁的持有者,则保持计数递减 + * 如果保持计数现在为零,则锁定被释放 + * 如果当前线程不是该锁的持有者,则抛出异常 + +基本语法: + +```java +// 获取锁 +reentrantLock.lock(); +try { + // 临界区 +} finally { + // 释放锁 + reentrantLock.unlock(); +} +``` + + + +*** + + + +#### 公平锁 + +##### 基本使用 + +构造方法:`ReentrantLock lock = new ReentrantLock(true)` + +```java +public ReentrantLock(boolean fair) { + sync = fair ? new FairSync() : new NonfairSync(); +} +``` + +ReentrantLock 默认是不公平的: + +```java +public ReentrantLock() { + sync = new NonfairSync(); +} +``` + +说明:公平锁一般没有必要,会降低并发度 + + + +*** + + + +##### 非公原理 + +###### 加锁 + +NonfairSync 继承自 AQS + +没有竞争:ExclusiveOwnerThread属于 Thread-0,state设置为1 + +```java +final void lock() { + //首先用 cas 尝试(仅尝试一次)将 state 从 0 改为 1, 如果成功表示获得了独占锁 + if (compareAndSetState(0, 1)) + setExclusiveOwnerThread(Thread.currentThread()); + else + acquire(1);//失败进入 +} +``` + +第一个竞争出现: + +```java +public final void acquire(int arg) { + // 当 tryAcquire 返回为 false 时, 先调用addWaiter, 接着 acquireQueued + if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); +} +``` + + + +Thread-1执行: + +* CAS 尝试将 state 由 0 改为 1,结果失败(第一次) + +* 进入 tryAcquire 逻辑,这时 state 已经是1,结果仍然失败(第二次) + + ```java + 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; + } + } + // 如果已经获得了锁, 线程还是当前线程, 表示发生了锁重入 + 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;//获取失败 + } + ``` + +* 接下来进入 addWaiter 逻辑,构造 Node 队列 + + * 图中黄色三角表示该 Node 的 waitStatus 状态,其中 0 为默认正常状态 + * Node 的创建是懒惰的 + * 其中第一个 Node 称为 Dummy(哑元)或哨兵,用来占位,并不关联线程 + + ```java + private Node addWaiter(Node mode) { + // 将当前线程关联到一个 Node 对象上, 模式为独占模式 + 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);//添加到尾节点 + return node; + } + ``` + + + +* 线程进入 acquireQueued 逻辑 + + * acquireQueued 会在一个死循环中不断尝试获得锁,失败后进入 park 阻塞 + + * 如果当前线程是在head节点后,会再次 tryAcquire 尝试获取锁,state 仍为 1 则失败(第三次) + + ```java + final boolean acquireQueued(final Node node, int arg) { + boolean failed = true; + try { + boolean interrupted = false; + for (;;) { + final Node p = node.predecessor(); + // 上一个节点是 head, 表示轮到自己获取锁 + if (p == head && tryAcquire(arg)) { + // 获取成功, 设置自己(当前线程对应的 node)为 head + setHead(node); + p.next = null; // help GC + failed = false; + return interrupted; + } + // 判断是否应当 park,返回false后需要新一轮的循环 + if (shouldParkAfterFailedAcquire(p, node) && + parkAndCheckInterrupt()) + interrupted = true; + } + } finally { + if (failed) + cancelAcquire(node); + } + } + ``` + + * 进入 shouldParkAfterFailedAcquire 逻辑,将前驱 node,即 head 的 waitStatus 改为 -1,返回 false;waitStatus 为 -1 的节点用来唤醒下一个节点 + + ```java + private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { + int ws = pred.waitStatus; + if (ws == Node.SIGNAL) + // 上一个节点都在阻塞, 那么当前线程也阻塞 + return true; + if (ws > 0) { + // 上一个节点取消, 那么重构删除前面所有取消的节点, 返回到外层循环重试 + do { + node.prev = pred = pred.prev; + } while (pred.waitStatus > 0); + pred.next = node; + } else { + // 设置上一个节点状态为 Node.SIGNAL,返回外层循环重试 + compareAndSetWaitStatus(pred, ws, Node.SIGNAL); + } + 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) + + ```java + private final boolean parkAndCheckInterrupt() { + // 阻塞当前线程,如果打断标记已经是 true, 则 park 会失效 + LockSupport.park(this); + // 判断当前线程是否被打断,清除打断标记 + return Thread.interrupted(); + } + ``` + + + + + +###### 解锁 + +```java +public void unlock() { + sync.release(1); +} +``` + +Thread-0 释放锁,进入 release 流程 + +* 进入 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; + } + 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); + return free; + } + ``` + +* 进入unparkSuccessor 方法 + + * 找到队列中离 head 最近的一个 Node(没取消的),unpark 恢复其运行,本例中即为 Thread-1 + * 回到 Thread-1 的 acquireQueued 流程 + + ```java + private void unparkSuccessor(Node node) { + int ws = node.waitStatus; + if (ws < 0) + // 尝试重置状态为 0 + compareAndSetWaitStatus(node, ws, 0); + // 找到需要 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) + LockSupport.unpark(s.thread); + } + ``` + +* 如果加锁成功(没有竞争),会设置 + + * exclusiveOwnerThread 为 Thread-1,state = 1 + * head 指向刚刚 Thread-1 所在的 Node,该 Node 清空 Thread + * 原本的 head 因为从链表断开,而可被垃圾回收 + + ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ReentrantLock-非公平锁4.png) + +* 如果这时候有其它线程来竞争(非公平),例如这时有 Thread-4 来了并抢占了锁 + + * Thread-4 被设置为 exclusiveOwnerThread,state = 1 + * Thread-1 再次进入 acquireQueued 流程,获取锁失败,重新进入 park 阻塞 + + ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ReentrantLock-非公平锁5.png) + + + +*** + + + +##### 公平原理 + +与非公平锁主要区别在于 tryAcquire 方法:先检查 AQS 队列中是否有前驱节点,没有才去CAS竞争 + +```java +static final class FairSync extends Sync { + private static final long serialVersionUID = -3000897897090466540L; + final void lock() { + acquire(1); + } + + protected final boolean tryAcquire(int acquires) { + final Thread current = Thread.currentThread(); + int c = getState(); + if (c == 0) { + // 先检查 AQS 队列中是否有前驱节点, 没有(false)才去竞争 + if (!hasQueuedPredecessors() && + compareAndSetState(0, acquires)) { + setExclusiveOwnerThread(current); + return true; + } + } + // 锁重入 + return false; + } +} +``` + +```java +public final boolean hasQueuedPredecessors() { + Node t = tail; + Node h = head; + Node s; + //头尾指向一个节点,列表为空,返回false, + return h != t && + // 头尾之间有节点,判断头节点的下一个是不是空 + // 不是空进入最后的判断,第二个节点的线程是否是本线程 + ((s = h.next) == null || s.thread != Thread.currentThread());} +``` + + + +*** + + + +#### 可重入 + +可重入是指同一个线程如果首次获得了这把锁,那么它是这把锁的拥有者,因此有权利再次获取这把锁,如果不可重入锁,那么第二次获得锁时,自己也会被锁挡住 + +源码解析参考:`nonfairTryAcquire(int acquires)) `和 `tryRelease(int releases)` + +```java +static ReentrantLock lock = new ReentrantLock(); +public static void main(String[] args) { + method1(); +} +public static void method1() { + lock.lock(); + try { + System.out.println(Thread.currentThread().getName() + " execute method1"); + method2(); + } finally { + lock.unlock(); + } +} +public static void method2() { + lock.lock(); + try { + System.out.println(Thread.currentThread().getName() + " execute method2"); + } finally { + lock.unlock(); + } +} +``` + +面试题:在Lock方法加两把锁会是什么情况呢? + +* 加锁两次解锁两次:正常执行 +* 加锁两次解锁一次:程序直接卡死,线程不能出来,也就说明**申请几把锁,最后需要解除几把锁** +* 加锁一次解锁两次:运行程序会直接报错 + +```java +public void getLock() { + lock.lock(); + lock.lock(); + try { + System.out.println(Thread.currentThread().getName() + "\t get Lock"); + } finally { + lock.unlock(); + //lock.unlock(); + } +} +``` + + + +**** + + + +#### 可打断 + +##### 基本使用 + +`public void lockInterruptibly()`:获得可打断的锁 + +* 如果没有竞争此方法就会获取lock对象锁 +* 如果有竞争就进入阻塞队列,可以被其他线程用interrupt打断 + +注意:如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断 + +```java +public static void main(String[] args) throws InterruptedException { + ReentrantLock lock = new ReentrantLock(); + Thread t1 = new Thread(() -> { + try { + System.out.println("尝试获取锁"); + lock.lockInterruptibly(); + } catch (InterruptedException e) { + System.out.println("没有获取到锁,被打断,直接返回"); + return; + } + try { + System.out.println("获取到锁"); + } finally { + lock.unlock(); + } + }, "t1"); + lock.lock(); + t1.start(); + Thread.sleep(2000); + System.out.println("主线程进行打断锁"); + t1.interrupt(); +} +``` + + + +##### 实现原理 + +* 不可打断模式:即使它被打断,仍会驻留在 AQS 队列中,一直要等到获得锁后方能得知自己被打断了 + + ```java + public final void acquire(int arg) { + if (!tryAcquire(arg) && + acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//阻塞等待 + // 如果acquireQueued返回true,打断状态interrupted = true + selfInterrupt(); + } + static void selfInterrupt() { + // 知道自己被打断了,需要重新产生一次中断完成中断效果 + Thread.currentThread().interrupt(); + } + ``` + + ```java + final boolean acquireQueued(final Node node, int arg) { + try { + boolean interrupted = false; + for (;;) { + final Node p = node.predecessor(); + if (p == head && tryAcquire(arg)) { + setHead(node); + p.next = null; // help GC + failed = false; + // 还是需要获得锁后, 才能返回打断状态 + return interrupted; + } + if (shouldParkAfterFailedAcquire(p, node) && + parkAndCheckInterrupt())//被打断 返回true + interrupted = true; + } + } + private final boolean parkAndCheckInterrupt() { + // 阻塞当前线程,如果打断标记已经是 true, 则 park 会失效 + LockSupport.park(this); + // 判断当前线程是否被打断,清除打断标记,被打断返回true + return Thread.interrupted(); + } + } + ``` + +* 可打断模式: + + ```java + public void lockInterruptibly() throws InterruptedException { + sync.acquireInterruptibly(1); + } + public final void acquireInterruptibly(int arg) { + 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 会抛出异常, 而不会再次进入循环 + } + } + } + ``` + + + +*** + + + +#### 锁超时 + +##### 基本使用 + +`public boolean tryLock()`:尝试获取锁,获取到返回true,获取不到直接放弃,不进入阻塞队列 + +`public boolean tryLock(long timeout, TimeUnit unit)`:在给定时间内获取锁,获取不到就退出 + +注意:tryLock期间也可以被打断 + +```java +public static void main(String[] args) { + ReentrantLock lock = new ReentrantLock(); + Thread t1 = new Thread(() -> { + try { + if (!lock.tryLock(2, TimeUnit.SECONDS)) { + System.out.println("获取不到锁"); + return; + } + } catch (InterruptedException e) { + System.out.println("被打断,获取不到锁"); + return; + } + try { + log.debug("获取到锁"); + } finally { + lock.unlock(); + } + }, "t1"); + lock.lock(); + System.out.println("主线程获取到锁"); + t1.start(); + + Thread.sleep(1000); + try { + System.out.println("主线程释放了锁"); + } finally { + lock.unlock(); + } +} +``` + + + +##### 实现原理 + +* tryLock() + + ```java + public boolean tryLock() { + return sync.nonfairTryAcquire(1);//只尝试一次 + } + ``` + +* tryLock(long timeout, TimeUnit unit) + + ```java + public final boolean tryAcquireNanos(int arg, long nanosTimeout) { + if (Thread.interrupted()) + throw new InterruptedException(); + //tryAcquire 尝试一次 + return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout); + } + protected final boolean tryAcquire(int acquires) { + return nonfairTryAcquire(acquires); + } + ``` + + ```java + private boolean doAcquireNanos(int arg, long nanosTimeout) { + 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(); + } + } + } + ``` + + + +##### 哲学家就餐 + +```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(); + 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 Chopstick extends ReentrantLock { + String name; + public Chopstick(String name) { + this.name = name; + } + @Override + public String toString() { + return "筷子{" + name + '}'; + } +} +``` + + + +*** + + + +#### 条件变量 + +##### 基本使用 + +synchronized 中的条件变量,就是当条件不满足时进入 waitSet 等待 +ReentrantLock 的条件变量比 synchronized 强大之处在于,它支持多个条件变量 + +ReentrantLock类获取Condition对象:`public Condition newCondition()` + +Condition类API: + +* `void await()`:当前线程从运行状态进入等待状态,释放锁 +* `void signal()`:唤醒一个等待在Condition上的线程,但是必须获得与该Condition相关的锁 + +使用流程: + +* **await / signal 前需要获得锁** +* await 执行后,会释放锁进入 conditionObject 等待 +* await 的线程被唤醒(打断、超时)去重新竞争 lock 锁 +* 竞争 lock 锁成功后,从 await 后继续执行 + +```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(); + } + }).start(); + Thread.sleep(1000); + //叫醒 + new Thread(() -> { + try { + lock.lock(); + //唤醒 + condition2.signal(); + } finally { + lock.unlock(); + } + }).start(); +} +``` + + + +##### 实现原理 + +await流程: + +* 开始 Thread-0 持有锁,调用 await,进入 ConditionObject 的 addConditionWaiter 流程 + + ```java + // 等待,直到被唤醒或打断 + public final void await() throws InterruptedException { + if (Thread.interrupted()) + throw new InterruptedException(); + // 添加一个 Node 至等待队列, + Node node = addConditionWaiter(); + // 释放节点持有的锁 + int savedState = fullyRelease(node); + int interruptMode = 0; + // 如果该节点还没有转移至 AQS 队列, park 阻塞 + while (!isOnSyncQueue(node)) { + LockSupport.park(this); + // 如果被打断, 退出等待队列,判断打断模式 + if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) + break; + } + // 退出等待队列后, 还需要获得 AQS 队列的锁 + if (acquireQueued(node, savedState) && interruptMode != THROW_IE) + interruptMode = REINTERRUPT; + // 所有已取消的 Node 从队列链表删除 + if (node.nextWaiter != null) + unlinkCancelledWaiters(); + // 应用打断模式 + if (interruptMode != 0) + reportInterruptAfterWait(interruptMode); + } + ``` + + ```java + // 打断模式 - 在退出等待时重新设置打断状态 + private static final int REINTERRUPT = 1; + // 打断模式 - 在退出等待时抛出异常 + private static final int THROW_IE = -1; + ``` + +* 创建新的 Node 状态为 -2(Node.CONDITION),关联 Thread-0,加入等待队列尾部 + + ```java + // 添加一个 Node 至等待队列 + private Node addConditionWaiter() { + Node t = lastWaiter; + // 所有已取消的 Node 从队列链表删除, + if (t != null && t.waitStatus != Node.CONDITION) { + unlinkCancelledWaiters(); + t = lastWaiter; + } + // 创建一个关联当前线程的新 Node, 添加至队列尾部 + Node node = new Node(Thread.currentThread(), Node.CONDITION); + if (t == null) + firstWaiter = node; + else + 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 + } + } + ``` + +* 接下来进入 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; + } + } + ``` + +* unpark AQS 队列中的下一个节点,竞争锁,假设没有其他竞争线程,那么 Thread-1 竞争成功 + +* park 阻塞 Thread-0 + + ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ReentrantLock-条件变量2.png) + + + +signal 流程: + +* 假设 Thread-1 要来唤醒 Thread-0,进入 ConditionObject 的 doSignal 流程,取得等待队列中第一个 Node,即 Thread-0 所在 Node + + ```java + // 唤醒 - 必须持有锁才能唤醒, 因此 doSignal 内无需考虑加锁 + public final void signal() { + // 调用方法的线程是否是资源的持有线程 + if (!isHeldExclusively()) + throw new IllegalMonitorStateException(); + // 取得等待队列中第一个 Node + Node first = firstWaiter; + if (first != null) + doSignal(first); + } + ``` + + ```java + // 唤醒 - 将没取消的第一个节点转移至 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); + } + private void doSignalAll(Node first) { + lastWaiter = firstWaiter = null; + do { + Node next = first.nextWaiter; + first.nextWaiter = null; + transferForSignal(first); + first = next; + } while (first != null); + } + ``` + +* 执行 transferForSignal 流程,将该 Node 加入 AQS 队列尾部,将 Thread-0 的 waitStatus 改为 0,Thread-3 的waitStatus 改为 -1 + + ```java + // 如果节点状态是取消, 返回 false 表示转移失败, 否则转移成功 + final boolean transferForSignal(Node node) { + // 如果状态已经不是 Node.CONDITION, 说明被取消了 + if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) + return false; + // 加入 AQS 队列尾部 + Node p = enq(node); + int ws = p.waitStatus; + // 上一个节点被取消 上一个节点不能设置状态为 Node.SIGNAL + if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) + // unpark 取消阻塞, 让线程重新同步状态 + LockSupport.unpark(node.thread); + return true; + } + ``` + + ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ReentrantLock-条件变量3.png) + +* Thread-1 释放锁,进入 unlock 流程 + + + + + +*** + + + +### ReadWrite + +#### 读写锁 + +独占锁:指该锁一次只能被一个线程所持有,对ReentrantLock和Synchronized而言都是独占锁 +共享锁:指该锁可以被多个线程锁持有 + +ReentrantReadWriteLock 其读锁是共享,其写锁是独占 + +作用:多个线程同时读一个资源类没有任何问题,为了满足并发量,读取共享资源应该同时进行,但是如果一个线程想去写共享资源,就不应该再有其它线程可以对该资源进行读或写 + +使用规则: + +* 加锁解锁格式: + + ```java + r.lock(); + try { + // 临界区 + } finally { + r.unlock(); + } + ``` + +* 读-读 能共存、读-写 不能共存、写-写 不能共存 + +* 读锁不支持条件变量 + +* **重入时升级不支持**:持有读锁的情况下去获取写锁会导致获取写锁永久等待,需要先释放读,再去获得写 + +* **重入时降级支持**:持有写锁的情况下去获取读锁 + + ```java + w.lock(); + try { + r.lock();// 降级为读锁, 释放写锁, 这样能够让其它线程读取缓存 + try { + // ... + } finally{ + w.unlock();// 要在写锁释放之前获取读锁 + } + } finally{ + r.unlock(); + } + ``` + +构造方法: + `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()`:尝试获取锁 + +读读并发: + +```java +public static void main(String[] args) { + ReentrantReadWriteLock rw = new ReentrantReadWriteLock(); + ReentrantReadWriteLock.ReadLock r = rw.readLock(); + ReentrantReadWriteLock.WriteLock w = rw.writeLock(); + + new Thread(() -> { + r.lock(); + try { + Thread.sleep(2000); + System.out.println("Thread 1 running " + new Date()); + } finally { + r.unlock(); + } + },"t1").start(); + new Thread(() -> { + r.lock(); + try { + Thread.sleep(2000); + System.out.println("Thread 2 running " + new Date()); + } finally { + r.unlock(); + } + },"t2").start(); +} +``` + + + +*** + + + +#### 缓存应用 + +缓存更新时,是先清缓存还是先更新数据库 + +* 先清缓存:可能造成刚清理缓存还没有更新数据库,线程直接查询了数据库更新缓存 + +* 先更新据库:可能造成刚更新数据库,还没清空缓存就有线程从缓存拿到了旧数据 + +* 补充情况:查询线程 A 查询数据时恰好缓存数据由于时间到期失效,或是第一次查询 + + + +可以使用读写锁进行操作 + + + +*** + + + +#### 实现原理 + +##### 加锁原理 + +读写锁用的是同一个 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)) + selfInterrupt(); + } + ``` + + ```java + protected final boolean tryAcquire(int acquires) { + Thread current = Thread.currentThread(); + int c = getState(); + // 获得低 16 位, 代表写锁的 state 计数 + int w = exclusiveCount(c); + if (c != 0) { + // c != 0 and w == 0 表示r != 0,有读锁,并且写锁的拥有者不是自己,获取失败 + if (w == 0 || current != getExclusiveOwnerThread()) + return false; + // 锁重入计数超过低 16 位, 报异常 + if (w + exclusiveCount(acquires) > MAX_COUNT) + throw new Error("Maximum lock count exceeded"); + // 写锁重入, 获得锁成功 + setState(c + acquires); + return true; + } + // c == 0,没有任何锁,判断写锁是否该阻塞 + if (writerShouldBlock() || + !compareAndSetState(c, c + acquires)) + return false; + // 获得锁成功 + setExclusiveOwnerThread(current); + return true; + } + // 非公平锁 writerShouldBlock 总是返回 false, 无需阻塞 + final boolean writerShouldBlock() { + return false; + } + // 公平锁会检查 AQS 队列中是否有前驱节点, 没有(false)才去竞争 + final boolean writerShouldBlock() { + return hasQueuedPredecessors(); + } + ``` + +* t2 r.lock(读锁),进入 tryAcquireShared 流程,如果有写锁占据,那么 tryAcquireShared + + * 返回 -1 表示失败 + * 如果返回 0 表示成功 + * 返回正数表示还有多少后继节点支持共享模式,读写锁返回1 + + ```java + public void lock() { + sync.acquireShared(1); + } + public final void acquireShared(int arg) { + // tryAcquireShared 返回负数, 表示获取读锁失败 + if (tryAcquireShared(arg) < 0) + doAcquireShared(arg); + } + ``` + + ```java + // 尝试以共享模式获取 + protected final int tryAcquireShared(int unused) { + Thread current = Thread.currentThread(); + int c = getState(); + // 如果是其它线程持有写锁, 并且写锁的持有者不是当前线程,获取读锁失败 + if (exclusiveCount(c) != 0 && //低 16 位, 代表写锁的 state + getExclusiveOwnerThread() != current) + return -1; + // 高 16 位,代表读锁的 state + int r = sharedCount(c); + if (!readerShouldBlock() && // 读锁不该阻塞 + r < MAX_COUNT && // 小于读锁计数 + compareAndSetState(c, c + SHARED_UNIT)) {// 尝试增加计数成功 + // .... + // 读锁加锁成功 + return 1; + } + // 与 tryAcquireShared 功能类似, 但会不断尝试 for (;;) 获取读锁, 执行过程中无阻塞 + return fullTryAcquireShared(current); + } + // 非公平锁 readerShouldBlock 看 AQS 队列中第一个节点是否是写锁,是则阻塞,反之不阻塞 + // true 则该阻塞, false 则不阻塞 + final boolean readerShouldBlock() { + return apparentlyFirstQueuedIsExclusive(); + } + ``` + +* 进入 sync.doAcquireShared(1) 流程,首先也是调用 addWaiter 添加节点,不同之处在于节点被设置为Node.SHARED 模式而非 Node.EXCLUSIVE 模式,注意此时 t2 仍处于活跃状态 + + ```java + private void doAcquireShared(int arg) { + // 将当前线程关联到一个 Node 对象上, 模式为共享模式 + final Node node = addWaiter(Node.SHARED); + boolean failed = true; + try { + boolean interrupted = false; + for (;;) { + // 获取前驱节点 + final Node p = node.predecessor(); + if (p == head) { + // 再一次尝试获取读锁 + int r = tryAcquireShared(arg); + // r >= 0 表示获取成功 + if (r >= 0) { + setHeadAndPropagate(node, r); + p.next = null; // help GC + if (interrupted) + selfInterrupt(); + failed = false; + return; + } + } + // 是否在获取读锁失败时阻塞 + if (shouldParkAfterFailedAcquire(p, node) && + // park 当前线程 + parkAndCheckInterrupt()) + interrupted = true; + } + } finally { + if (failed) + cancelAcquire(node); + } + } + ``` + + ```java + private void setHeadAndPropagate(Node node, int propagate) { + Node h = head; + // 设置自己为 head 节点 + setHead(node); + // 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(); + } + } + ``` + + ```java + 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) { + // 因为读锁共享,如果其它线程也在释放读锁,那么需要将 waitStatus 先改为 0 + // 防止 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; + } + } + ``` + +* 如果没有成功,在 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 所在节点 + +* t2 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,但计数还不为零 + t3 同样让计数减一,计数为零,进入doReleaseShared() 将头节点从 -1 改为 0 并唤醒下一个节点 + + ```java + public void unlock() { + sync.releaseShared(1); + } + public final boolean releaseShared(int arg) { + if (tryReleaseShared(arg)) { + doReleaseShared(); + return true; + } + return false; + } + ``` + + ```java + protected final boolean tryReleaseShared(int unused) { + // + for (;;) { + int c = getState(); + int nextc = c - SHARED_UNIT; + // 读锁的计数不会影响其它获取读锁线程, 但会影响其它获取写锁线程 + // 计数为 0 才是真正释放 + if (compareAndSetState(c, nextc)) + return nextc == 0; + } + } + ``` + +* t4 在 acquireQueued 中 parkAndCheckInterrupt 处恢复运行,再次 for (;;) 这次自己是头节点的临节点,并且没有其他节点竞争,tryAcquire(1) 成功,修改头结点,流程结束 + + + + + +*** + + + +#### Stamped + +StampedLock:读写锁,该类自 JDK 8 加入,是为了进一步优化读性能 + +特点: + +* 在使用读锁、写锁时都必须配合戳使用 + +* StampedLock 不支持条件变量 +* StampedLock **不支持可重入** + +基本用法 + +* 加解读锁: + + ```java + long stamp = lock.readLock(); + lock.unlockRead(stamp); + ``` + +* 加解写锁: + + ```java + long stamp = lock.writeLock(); + lock.unlockWrite(stamp); + ``` + +* 乐观读,StampedLock 支持`tryOptimisticRead()`方法,读取完毕后做一次**戳校验**,如果校验通过,表示这期间没有其他线程的写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全 + + ```java + long stamp = lock.tryOptimisticRead(); + // 验戳 + if(!lock.validate(stamp)){ + // 锁升级 + } + ``` + +提供一个数据容器类内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法: + +* 读-读 可以优化 +* 读-写 优化读,补加读锁 + +```java +public static void main(String[] args) throws InterruptedException { + DataContainerStamped dataContainer = new DataContainerStamped(1); + new Thread(() -> { + dataContainer.read(1000); + },"t1").start(); + Thread.sleep(500); + + new Thread(() -> { + dataContainer.write(1000); + },"t2").start(); +} + +class DataContainerStamped { + private int data; + private final StampedLock lock = new StampedLock(); + + public int read(int readTime) throws InterruptedException { + 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(); + System.out.println(new Date() + " read lock" + stamp); + Thread.sleep(readTime); + System.out.println(new Date() + " read finish..." + stamp); + return data; + } finally { + System.out.println(new Date() + " read unlock " + stamp); + lock.unlockRead(stamp); + } + } + + public void write(int newData) { + long stamp = lock.writeLock(); + System.out.println(new Date() + " write lock " + stamp); + try { + Thread.sleep(2000); + this.data = newData; + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + System.out.println(new Date() + " write unlock " + stamp); + lock.unlockWrite(stamp); + } + } +} +``` + + + + + +*** + + + + + +## 并发包 + +(源码分析待更新) + +### Semaphore + +#### 信号量 + +synchronized 可以起到锁的作用,但某个时间段内,只能有一个线程允许执行 + +Semaphore(信号量)用来限制能同时访问共享资源的线程上限,非重入锁 + +构造方法: + +* `public Semaphore(int permits)`:permits 表示许可线程的数量(state) +* `public Semaphore(int permits, boolean fair)`:fair 表示公平性,如果这个设为 true 的话,下次执行的线程会是等待最久的线程 + +常用API: + +* `public void acquire()`:表示获取许可 +* `public void release()`:表示释放许可,acquire()和release()方法之间的代码为同步代码 + +```java +public static void main(String[] args) { + // 1.创建Semaphore对象 + Semaphore semaphore = new Semaphore(3); + + // 2. 10个线程同时运行 + for (int i = 0; i < 10; i++) { + new Thread(() -> { + try { + // 3. 获取许可 + semaphore.acquire(); + sout(Thread.currentThread().getName() + " running..."); + Thread.sleep(1000); + sout(Thread.currentThread().getName() + " end..."); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + // 4. 释放许可 + semaphore.release(); + } + }).start(); + } +} +``` + + + +*** + + + +#### 实现原理 + +加锁流程: + +* Semaphore 的 permits(state)为 3,这时 5 个线程来获取资源 + + ```java + Sync(int permits) { + setState(permits); + } + ``` + + 假设其中 Thread-1,Thread-2,Thread-4 CAS 竞争成功,permits 变为 0,而 Thread-0 和 Thread-3 竞争失败,进入 AQS 队列park 阻塞 + + ```java + // acquire() -> sync.acquireSharedInterruptibly(1); + public final void acquireSharedInterruptibly(int arg) { + if (Thread.interrupted()) + throw new InterruptedException(); + if (tryAcquireShared(arg) < 0) + doAcquireSharedInterruptibly(arg); + } + + // tryAcquireShared() -> nonfairTryAcquireShared() + // 非公平,公平锁会在循环内 hasQueuedPredecessors()方法判断是否有临头节点(第二个节点) + final int nonfairTryAcquireShared(int acquires) { + for (;;) { + int available = getState(); + int remaining = available - acquires; + // 如果许可已经用完, 返回负数, 表示获取失败, + if (remaining < 0 || + // 如果 cas 重试成功, 返回正数, 表示获取成功 + compareAndSetState(available, remaining)) + return remaining; + } + } + ``` + + ```java + private void doAcquireSharedInterruptibly(int arg) { + 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 + p.next = null; // help GC + failed = false; + return; + } + } + // 不成功, 设置上一个节点 waitStatus = Node.SIGNAL, 下轮进入 park 阻塞 + if (shouldParkAfterFailedAcquire(p, node) && + parkAndCheckInterrupt()) + throw new InterruptedException(); + } + } finally { + if (failed) + cancelAcquire(node); + } + } + ``` + + ![](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; + } + return false; + } + protected final boolean tryReleaseShared(int 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; + } + } + private void doReleaseShared() { + // PROPAGATE 详解 + // 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark + // 如果 head.waitStatus == 0 ==> Node.PROPAGATE + } + ``` + + ![](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 状态 + + + +**** + + + +#### PROPAGATE + +假设存在某次循环中队列里排队的结点情况为 `head(-1)->t1(-1)->t2(0)` +假设存在将要信号量释放的 T3 和 T4,释放顺序为先 T3 后 T4 + +```java +// 老版本代码 +private void setHeadAndPropagate(Node node, int propagate) { + setHead(node); + // 有空闲资源 + if (propagate > 0 && node.waitStatus != 0) { + Node s = node.next; + // 下一个 + if (s == null || s.isShared()) + unparkSuccessor(node); + } +} +``` + +正常流程: + +* T3 调用 releaseShared(1),直接调用了 unparkSuccessor(head),head.waitStatus 从 -1 变为 0 +* T1 由于 T3 释放信号量被唤醒,然后T4 释放,唤醒 T2 + +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(t1.node, 0) 时,因为不满足 propagate > 0(剩余资源量 == 0),从而不会唤醒后继结点, **T2 线程得不到唤醒** + + + +更新后流程: + +* 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 时,读到 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 + if (propagate > 0 || h == null || h.waitStatus < 0 || + (h = head) == null || h.waitStatus < 0) { + Node s = node.next; + // 如果是最后一个节点或者是等待共享读锁的节点,做一次唤醒 + if (s == null || s.isShared()) + doReleaseShared(); + + }} +``` + +```java +// 唤醒 +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代表覆盖 + } + latch.countDown(); + }); + } + 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(); +} +``` + + + +*** + + + +### Exchanger + +Exchanger:交换器,是一个用于线程间协作的工具类,用于进行线程间的数据交换 + +工作流程:两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据 + +常用方法: + +* `public Exchanger()`:创建一个新的交换器 +* `public V exchange(V x)`:等待另一个线程到达此交换点 +* `public V exchange(V x, long timeout, TimeUnit unit)`:等待一定的时间 + +```java +public class ExchangerDemo { + public static void main(String[] args) { + // 创建交换对象(信使) + Exchanger exchanger = new Exchanger<>(); + new ThreadA(exchanger).start(); + new ThreadA(exchanger).start(); + } +} +class ThreadA extends Thread{ + private Exchanger exchanger(); + public ThreadA(Exchanger exchanger){this.exchanger = exchanger;} + @Override + public void run() { + try{ + sout("线程A,做好了礼物A,等待线程B送来的礼物B"); + //如果等待了5s还没有交换就死亡(抛出异常)! + String s = exchanger.exchange("礼物A",5,TimeUnit.SECONDS); + sout("线程A收到线程B的礼物:" + s); + } catch (Exception e) { + System.out.println("线程A等待了5s,没有收到礼物,最终就执行结束了!"); + } + } +} +class ThreadB extends Thread{ + private Exchanger exchanger; + public ThreadB(Exchanger exchanger) { + this.exchanger = exchanger; + } + @Override + public void run() { + try { + sout("线程B,做好了礼物B,等待线程A送来的礼物A....."); + // 开始交换礼物。参数是送给其他线程的礼物! + sout("线程B收到线程A的礼物:" + exchanger.exchange("礼物B")); + } catch (Exception e) { + e.printStackTrace(); + } + } +} +``` + + + +*** + + + +### 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; + } + } +} +``` + +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; + } + ``` + + + + + +*** + + + +### CopyOnWrite + +#### 原理分析 + +CopyOnWriteArrayList 采用了**写入时拷贝**的思想,增删改操作会将底层数组拷贝一份,更改操作在新数组上执行,不影响其它线程的**并发读,读写分离** + +CopyOnWriteArraySet 底层对 CopyOnWriteArrayList 进行了包装,装饰器模式 + +```java +public CopyOnWriteArraySet() { + al = new CopyOnWriteArrayList(); +} +``` + +* 存储结构: + + ```java + private transient volatile Object[] array;//保证了读写线程之间的可见性 + ``` + +* 新增数据: + + ```java + public boolean add(E e) { + final ReentrantLock lock = this.lock; + lock.lock(); + try { + // 获取旧的数组 + Object[] elements = getArray(); + int len = elements.length; + // 拷贝新的数组(这里是比较耗时的操作,但不影响其它读线程) + Object[] newElements = Arrays.copyOf(elements, len + 1); + // 添加新元素 + newElements[len] = e; + // 替换旧的数组 + setArray(newElements); + return true; + } finally { + lock.unlock(); + } + } + ``` + +* 读操作: + + ```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); + } + } + ``` + + 适合读多写少的应用场景 + +* 迭代器: + + CopyOnWriteArrayList 在返回一个迭代器时,会基于创建这个迭代器时内部数组所拥有的数据,创建一个该内部数组当前的快照,然后迭代器遍历的是该快照,而不是内部的数组,所以这种实现方式也存在一定的数据延迟性,即对其他线程并行添加的数据不可见 + + ```java + public Iterator iterator() { + return new COWIterator(getArray(), 0); + } + + // 迭代器会创建一个底层array的快照,故主类的修改不影响该快照 + static final class COWIterator implements ListIterator { + // 内部数组快照 + private final Object[] snapshot; + + //... + // 不支持写操作 + public void remove() { + throw new UnsupportedOperationException(); + } + } + ``` + + + +*** + + + +#### 弱一致性 + +##### get方法 + +数据一致性就是读到最新更新的数据: + +* 强一致性:当更新操作完成之后,任何多个后续进程或者线程的访问都会返回最新的更新过的值 + +* 弱一致性:系统并不保证进程或者线程的访问都会返回最新的更新过的值,也不会承诺多久之后可以读到 + + + +| 时间点 | 操作 | +| ------ | ---------------------------- | +| 1 | Thread-0 getArray() | +| 2 | Thread-1 getArray() | +| 3 | Thread-1 setArray(arrayCopy) | +| 4 | Thread-0 array[index] | + +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 包下的类都是安全失败 + +* 快速失败:在 A 线程使用**迭代器**对集合进行遍历的过程中,此时 B 线程对集合进行修改(增删改),或者 A 线程在遍历过程中对集合进行修改,都会导致 A 线程抛出 ConcurrentModificationException 异常 + * AbstractList 类中的成员变量 modCount,用来记录 List 结构发生变化的次数,**结构发生变化**是指添加或者删除至少一个元素的操作,或者是调整内部数组的大小,仅仅设置元素的值不算结构发生变化 + * 在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了抛出 CME 异常 +* 安全失败:采用安全失败机制的集合容器,在**迭代器**遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在复制集合上进行遍历。由于迭代时不是对原集合进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,故不会抛 ConcurrentModificationException 异常 + + + +*** + + + +### Collections + +Collections类是用来操作集合的工具类,提供了集合转换成线程安全的方法: + +```java + public static Collection synchronizedCollection(Collection c) { + return new SynchronizedCollection<>(c); + } +public static Map synchronizedMap(Map m) { + return new SynchronizedMap<>(m); +} +``` + +源码:底层也是对方法进行加锁 + +```java +public boolean add(E e) { + synchronized (mutex) {return c.add(e);} +} +``` + + + +*** + + + +### SkipListMap + +#### 底层结构 + +跳表 SkipList 是一个**有序的链表**,默认升序,底层是链表加多级索引的结构。跳表可以对元素进行快速查询,类似于平衡树,是一种利用空间换时间的算法 + +对于单链表,即使链表是有序的,如果查找数据也只能从头到尾遍历链表,所以采用链表上建索引的方式提高效率,跳表的查询时间复杂度是 **O(logn)**,空间复杂度 O(n) + +ConcurrentSkipListMap 提供了一种线程安全的并发访问的排序映射表,内部是跳表结构实现,通过 CAS + volatile 保证线程安全 + +平衡树和跳表的区别: + +* 对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整;而对跳表的插入和删除,只需要对整个数据结构的局部进行操作 +* 在高并发的情况下,保证整个平衡树的线程安全需要一个全局锁;对于跳表则只需要部分锁,拥有更好的性能 + +![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentSkipListMap数据结构.png) + +BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引指向链表最下面的节点** + + + +*** + + + +#### 成员变量 + +* 标识索引头节点位置 + + ```java + private static final Object BASE_HEADER = new Object(); + ``` + +* 跳表的顶层索引 + + ```java + private transient volatile HeadIndex head; + ``` + +* 比较器,为 null 则使用自然排序 + + ```java + final Comparator comparator; + ``` + +* Node 节点 + + ```java + static final class Node{ + final K key; // key 是 final 的, 说明节点一旦定下来, 除了删除, 不然不会改动 key + volatile Object value; // 对应的 value + volatile Node next; // 下一个节点 + } + ``` + +* 索引节点 Index + + ```java + static class Index{ + final Node node; // 索引指向的节点, + final Index down; // 下边level层的Index,分层索引 + volatile Index right; // 右边的Index + + // 在index本身和succ之间插入一个新的节点newSucc + final boolean link(Index succ, Index newSucc){ + Node n = node; + newSucc.right = succ; + return n.value != null && casRight(succ, newSucc); + } + + // 将当前的节点 index 设置其的 right 为 succ.right 等于删除 succ 节点 + final boolean unlink(Index succ){ + return node.value != null && casRight(succ, succ.right); + } + } + ``` + +* 头索引节点 HeadIndex + + ```java + static final class HeadIndex extends Index { + final int level;// 标示索引层级,所有的HeadIndex都指向同一个Base_header节点 + HeadIndex(Node node, Index down, Index right, int level) { + super(node, down, right); + this.level = level; + } + } + ``` + + + +*** + + + +#### 成员方法 + +##### 其他方法 + +* 构造方法: + + ```java + public ConcurrentSkipListMap() { + this.comparator = null; // comparator为null,使用key的自然序,如字典序 + initialize(); + } + ``` + + ```java + private void initialize() { + keySet = null; + 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); + } + ``` + +* cpr:排序 + + ```java + // x是比较者,y是被比较者,比较者大于被比较者 返回正数,小于返回负数,相等返回0 + static final int cpr(Comparator c, Object x, Object y) { + return (c != null) ? c.compare(x, y) : ((Comparable)x).compareTo(y); + } + ``` + + + +*** + + + +##### 添加方法 + +* findPredecessor():寻找前驱节点 + + 从最上层的头索引开始向右查找(链表的后续索引),如果后续索引的节点的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节点 + for (Index q = head, r = q.right, d;;) { + //2.右索引节点不为空,则进行向下查找 + if (r != null) { + Node n = r.node; + K k = n.key; + //3.n.value为null说明节点n正在删除的过程中 + if (n.value == null) { + //在index层直接删除r索引节点,用在删除节点中 + if (!q.unlink(r)) + break;//重新从 head 节点开始查找,break到步骤1 + //删除节点r成功,获取新的r节点, 回到步骤 2 + //还是从这层索引开始向右遍历, 直到 r == null + r = q.right; + continue; + } + //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,是则说明已经到了数据层,也就是第一层 + if ((d = q.down) == null) + return q.node; + //6.未到数据层, 进行重新赋值向下扫描 + q = d; //q指向d + r = d.right;//r指向q的后续索引节点 + } + } + } + ``` + + ```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() + + ```java + public V put(K key, V value) { + // 非空判断,value不能为空 + if (value == null) + throw new NullPointerException(); + return doPut(key, value, false); + } + ``` + + ```java + private V doPut(K key, V value, boolean onlyIfAbsent) { + Node z; + if (key == null)// 非空判断,key不能为空 + throw new NullPointerException(); + Comparator cmp = comparator; + // outer循环,处理并发冲突等其他需要重试的情况 + outer: for (;;) { + //0.for (;;) + //1.将 key 对应的前继节点找到, b为前继节点, n是前继节点的next, + // 若没发生条件竞争,最终key在b与n之间 (找到的b在base_level上) + for (Node b = findPredecessor(key, cmp), n = b.next;;) { + // 2.n不为null时b不是链表的最后一个节点 + if (n != null) { + Object v; int c; + //3.获取 n 的右节点 + Node f = n.next; + //4.条件竞争 + // 并发下其他线程在b之后插入节点或直接删除节点n, break到步骤0 + if (n != b.next) + break; + // 若节点n已经删除, 则调用helpDelete进行帮助删除 + if ((v = n.value) == null) { + n.helpDelete(b, f); + break; + } + //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之间 + if ((c = cpr(cmp, key, n.key)) > 0) { + b = n; + n = f; + continue; + } + //7.key的值和n.key相等,则可以直接覆盖赋值 + if (c == 0) { + // onlyIfAbsent默认false, + if (onlyIfAbsent || n.casValue(v, value)) { + @SuppressWarnings("unchecked") V vv = (V)v; + return vv;//返回被覆盖的值 + } + // cas失败,返回0,重试 + break; + } + // else c < 0; fall through + } + //8.此时的情况n.key > key > b.key,对应流程图1中的7 + // 创建z节点指向n + z = new Node(key, value, n); + //9.尝试把b.next从n设置成z + if (!b.casNext(n, z)) + // cas失败,返回到步骤0,重试 + break; + //10.break outer后, 上面的for循环不会再执行, 而后执行下面的代码 + break outer; + } + } + // 以上插入节点已经完成,剩下的任务要根据随机数的值来表示是否向上增加层数与上层索引 + + // 随机数 + int rnd = ThreadLocalRandom.nextSecondarySeed(); + + //如果随机数的二进制与10000000000000000000000000000001进行与运算为0 + //即随机数的二进制最高位与最末尾必须为0,其他位无所谓,就进入该循环 + //如果随机数的二进制最高位与最末位不为0,不增加新节点的层数 + //11.判断是否需要添加level + if ((rnd & 0x80000001) == 0) { + //索引层level,从1开始 + int level = 1, max; + //12.判断最低位前面有几个1,有几个leve就加几:0..0 0001 1110,这是4个,则1+4=5 + // 最大有30个就是 1 + 30 + while (((rnd >>>= 1) & 1) != 0) + ++level; + Index idx = null;//最终指向z节点,就是添加的节点 + HeadIndex h = head;//指向头索引节点 + //13.判断level是否比当前最高索引小,图中max为3 + if (level <= (max = h.level)) { + for (int i = 1; i <= level; ++i) + //根据层数level不断创建新增节点的上层索引,索引的后继索引留空 + //第一次idx为null,也就是下层索引为空,第二次把上次的索引作为下层索引 + idx = new Index(z, idx, null); + // 循环以后的索引结构 + // index-3 ← idx + // ↓ + // index-2 + // ↓ + // index-1 + // ↓ + // z-node + } + //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了 + for (int i = 1; i <= level; ++i) + idxs[i] = idx = new Index(z, idx, null); + // index-4 ← idx + // ↓ + // index-3 + // ↓ + // index-2 + // ↓ + // index-1 + // ↓ + // z-node + + for (;;) { + h = head; + //获取头索引的层数 + int oldLevel = h.level; + // 如果level <= oldLevel,说明其他线程进行了index层增加操作,退出循环 + if (level <= oldLevel) + break; + //定义一个新的头索引节点 + HeadIndex newh = h; + //获取头索引的节点,就是BASE_HEADER + Node oldbase = h.node; + // 升级baseHeader索引,升高一级,并发下可能升高多级 + for (int j = oldLevel+1; j <= level; ++j) + newh = new HeadIndex(oldbase, newh, idxs[j], j); + // 执行完for循环之后,baseHeader 索引长这个样子.. + // index-4 → index-4 ← idx + // ↓ ↓ + // index-3 index-3 + // ↓ ↓ + // index-2 index-2 + // ↓ ↓ + // index-1 index-1 + // ↓ ↓ + // baseHeader → .... → z-node + + //cas成功后,map.head字段指向最新的headIndex,baseHeader的index-4s + if (casHead(h, newh)) { + //h指向最新的 index-4 节点 + h = newh; + //idx指向z-node的index-3节点, + //因为从index-3-index-1的这些z-node索引节点 都没有插入到索引链表 + idx = idxs[level = oldLevel]; + break; + } + } + } + //15.把新加的索引插入索引链表中,有上述两种情况,一种索引高度不变,另一种是高度加1 + splice: for (int insertionLevel = level;;) { + //获取头索引的层数, 情况1是3,情况2是4 + int j = h.level; + for (Index q = h, r = q.right, t = idx;;) { + //如果头索引为null或者新增节点索引为null,退出插入索引的总循环 + if (q == null || t == null) + //此处表示有其他线程删除了头索引或者新增节点的索引 + break splice; + //头索引的链表后续索引存在,如果是新层则为新节点索引,如果是老层则为原索引 + if (r != null) { + //获取r的节点 + Node n = r.node; + //插入的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,向右扫描 + if (c > 0) { + q = r; + r = r.right; + continue; + } + } + // 执行到这里,说明key < n.key,判断是否第j层插入新增节点的前置索引 + if (j == insertionLevel) { + // 将新索引节点t插入q r之间 + if (!q.link(r, t)) + break; + //如果新增节点的值为null,表示该节点已经被其他线程删除 + if (t.node.value == null) { + findNode(key); + break splice; + } + // 插入层逐层自减,当为最底层时退出循环 + if (--insertionLevel == 0) + break splice; + } + //其他节点随着插入节点的层数下移而下移 + if (--j >= insertionLevel && j < level) + t = t.down; + q = q.down; + r = q.right; + } + } + } + return null; + } + ``` + +* findNode() + + ```java + private Node findNode(Object key) { + //原理与doGet相同,无非是findNode返回节点,doGet返回value + if ((c = cpr(cmp, key, n.key)) == 0) + return n; + } + ``` + + + + +*** + + + +##### 获取方法 + +* get(key) + + 寻找 key 的前继节点 b (这时b.next = null || b.next > key, 则说明不存key对应的 Node) + + 接着就判断 b, b.next 与 key之间的关系(其中有些 helpDelete操作) + + ```java + public V get(Object key) { + return doGet(key); + } + ``` + +* doGet() + + ```java + private V doGet(Object key) { + if (key == null) + throw new NullPointerException(); + Comparator cmp = comparator; + outer: for (;;) { + //1.找到最底层节点的前置节点 + for (Node b = findPredecessor(key, cmp), n = b.next;;) { + Object v; int c; + //2.如果该前置节点的链表后续节点为null,说明不存在该节点 + if (n == null) + break outer; + //b → n → f + Node f = n.next; + //3.如果n不为前置节点的后续节点,表示已经有其他线程删除了该节点 + if (n != b.next) + break; + //4.如果后续节点的值为null,删除该节点 + if ((v = n.value) == null) { + n.helpDelete(b, f); + break; + } + //5.如果前置节点已被其他线程删除,重新循环 + if (b.value == null || v == n) + break; + //6.如果要获取的key与后续节点的key相等,返回节点的value + if ((c = cpr(cmp, key, n.key)) == 0) { + @SuppressWarnings("unchecked") V vv = (V)v; + return vv; + } + //7.key < n.key,说明被其他线程删除了,或者不存在该节点 + if (c < 0) + break outer; + b = n; + n = f; + } + } + return null; + } + ``` + + + +**** + + + +##### 删除方法 + +* remove() + + ```java + public V remove(Object key) { + return doRemove(key, null); + } + final V doRemove(Object key, Object value) { + if (key == null) + throw new NullPointerException(); + Comparator cmp = comparator; + outer: for (;;) { + //1.找到最底层目标节点的前置节点,b.key < key + for (Node b = findPredecessor(key, cmp), n = b.next;;) { + Object v; int c; + //2.如果该前置节点的链表后续节点为null,退出循环 + if (n == null) + break outer; + //b → n → f + Node f = n.next; + if (n != b.next) // inconsistent read + break; + if ((v = n.value) == null) { // n is deleted + n.helpDelete(b, f); + break; + } + if (b.value == null || v == n) // b is deleted + break; + //3.key < n.key,说明被其他线程删除了,或者不存在该节点 + if ((c = cpr(cmp, key, n.key)) < 0) + break outer; + //4.key > n.key,继续向后扫描 + if (c > 0) { + b = n; + n = f; + continue; + } + //5.到这里是 key = n.key,value是n.value + if (value != null && !value.equals(v)) + break outer; + //6.把n节点的value置空 + if (!n.casValue(v, null)) + break; + //7.给n添加一个删除标志mark,mark.next=f,然后把b.next设置为f,成功后n出队 + if (!n.appendMarker(f) || !b.casNext(n, f)) + //对key对应的index进行删除 + findNode(key); + else { + //进行操作失败后通过findPredecessor中进行index的删除 + findPredecessor(key, cmp); + if (head.right == null) + //进行headIndex 对应的index 层的删除 + tryReduceLevel(); + } + @SuppressWarnings("unchecked") V vv = (V)v; + return vv; + } + } + return null; + } + ``` + + 经过 findPredecessor() 中的 unlink() 后索引已经被删除 + + ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentSkipListMap-remove流程.png) + +* appendMarker() + + ```java + //添加删除标记节点 + boolean appendMarker(Node f) { + //通过CAS生成一个key为null,value为this,next为f的标记节点 + return casNext(f, new Node(f)); + } + ``` + +* helpDelete() + + ```java + //将添加了删除标记的节点清除 + void helpDelete(Node b, Node f) { + //this节点的后续节点为f,且本身为b的后续节点,一般都是正确的,除非被别的线程删除 + if (f == next && this == b.next) { + //如果n还还没有被标记 + if (f == null || f.value != f) + casNext(f, new Node(f)); + else + //通过CAS,将b的下一个节点n变成f.next,即成为图中的样式 + b.casNext(this, f.next); + } + } + ``` + +* tryReduceLevel() + + ```java + private void tryReduceLevel() { + HeadIndex h = head; + HeadIndex d; + HeadIndex e; + if (h.level > 3 && + (d = (HeadIndex)h.down) != null && + (e = (HeadIndex)d.down) != null && + e.right == null && + d.right == null && + h.right == null && + //设置头索引 + casHead(h, d) && + //重新检查 + h.right != null) + //重新检查返回true,说明其他线程增加了索引层级,把索引头节点设置回来 + casHead(d, h); + } + ``` + + + +参考文章:https://my.oschina.net/u/3768341/blog/3135659 + +参考视频:https://www.bilibili.com/video/BV1Er4y1P7k1 + + + + + +*** + + + +### NoBlocking + +#### 非阻塞队列 + +并发编程中,需要用到安全的队列,实现安全队列可以使用2种方式: + +* 加锁,这种实现方式是阻塞队列 +* 使用循环CAS算法实现,这种方式是非阻塞队列 + +ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,采用先进先出的规则对节点进行排序,当添加一个元素时,会添加到队列的尾部,当获取一个元素时,会返回队列头部的元素 + +补充:ConcurrentLinkedDeque 是双向链表结构的无界并发队列 + +ConcurrentLinkedQueue使用约定: + +1. 不允许null入列 +2. 队列中所有未删除的节点的item都不能为null且都能从head节点遍历到 +3. 删除节点是将item设置为null,队列迭代时跳过item为null节点 +4. head节点跟tail不一定指向头节点或尾节点,可能存在滞后性 + +ConcurrentLinkedQueue由head节点和tail节点组成,每个节点(Node)由节点元素(item)和指向下一个节点的引用(next)组成,组成一张链表结构的队列 + +```java +private transient volatile Node head; +private transient volatile Node tail; + +private static class Node { + volatile E item; + volatile Node next; + //..... +} +``` + + + +*** + + + +#### 构造方法 + +* 无参构造方法: + + ```java + public ConcurrentLinkedQueue() { + // 默认情况下head节点存储的元素为空,tail节点等于head节点 + head = tail = new Node(null); + } + ``` + +* 有参构造方法 + + ```java + public ConcurrentLinkedQueue(Collection c) { + Node h = null, t = null; + // 遍历节点 + for (E e : c) { + checkNotNull(e); + Node newNode = new Node(e); + if (h == null) + h = t = newNode; + else { + // 单向链表 + t.lazySetNext(newNode); + t = newNode; + } + } + if (h == null) + h = t = new Node(null); + head = h; + tail = t; + } + ``` + + + +*** + + + +#### 入队方法 + +与传统的链表不同,单线程入队的工作流程: + +* 将入队节点设置成当前队列尾节点的下一个节点 +* 更新tail节点,如果tail节点的next节点不为空,则将入队节点设置成tail节点;如果tail节点的next节点为空,则将入队节点设置成tail的next节点,所以tail节点不总是尾节点,**存在滞后性** + +```java +public boolean offer(E e) { + checkNotNull(e); + // 创建入队节点 + final Node newNode = new Node(e); + + // 循环CAS直到入队成功 + for (Node t = tail, p = t;;) { + // p用来表示队列的尾节点,初始情况下等于tail节点,q 是p的next节点 + Node q = p.next; + // 判断p是不是尾节点 + if (q == null) { + // p是尾节点,设置p节点的下一个节点为新节点 + // 设置成功则casNext返回true,否则返回false,说明有其他线程更新过尾节点 + // 继续寻找尾节点,继续CAS + if (p.casNext(null, newNode)) { + // 首次添加时,p等于t,不进行尾节点更新,所以所尾节点存在滞后性 + if (p != t) + // 将tail设置为新入队的节点,设置失败表示其他线程更新了tail节点 + casTail(t, newNode); + return true; + } + } + else if (p == q) + // 当tail不指向最后节点时,如果执行出列操作,可能将tail也移除,tail不在链表中 + // 此时需要对tail节点进行复位,复位到head节点 + p = (t != (t = tail)) ? t : head; + else + // 推动tail尾节点往队尾移动 + p = (p != t && t != (t = tail)) ? t : q; + } +} +``` + +图解入队: + +![](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节点和尾节点的距离**大于等于1**时(每入队两次)更新tail,可以减少CAS更新tail节点的次数,提高入队效率 + +线程安全问题: + +* 线程1线程2同时入队,无论从哪个位置开始并发入队,都可以循环CAS,直到入队成功,线程安全 +* 线程1遍历,线程2入队,所以造成 ConcurrentLinkedQueue 的size是变化,需要加锁保证安全 +* 线程1线程2同时出列,线程也是安全的 + + + +*** + + + +#### 出队方法 + +出队列的就是从队列里返回一个节点元素,并清空该节点对元素的引用,并不是每次出队都更新head节点 + +* 当head节点里有元素时,直接弹出head节点里的元素,而不会更新head节点 +* 当head节点里没有元素时,出队操作才会更新head节点 + +**批处理方式**可以减少使用CAS更新head节点的消耗,从而提高出队效率 + +```java +public E poll() { + restartFromHead: + for (;;) { + // p节点表示首节点,即需要出队的节点 + for (Node h = head, p = h, q;;) { + E item = p.item; + // 如果p节点的元素不为null,则通过CAS来设置p节点引用元素为null,成功返回item + if (item != null && p.casItem(item, null)) { + if (p != h) + // 对head进行移动 + updateHead(h, ((q = p.next) != null) ? q : p); + return item; + } + // 如果头节点的元素为空或头节点发生了变化,这说明头节点被另外一个线程修改了 + // 那么获取p节点的下一个节点,如果p节点的下一节点为null,则表明队列已经空了 + else if ((q = p.next) == null) { + updateHead(h, p); + return null; + } + // 第一轮操作失败,下一轮继续,调回到循环前 + else if (p == q) + continue restartFromHead; + // 如果下一个元素不为空,则将头节点的下一个节点设置成头节点 + else + p = q; + } + } +} +final void updateHead(Node h, Node p) { + if (h != p && casHead(h, p)) + // 将旧结点h的next域指向为h + h.lazySetNext(h); +} +``` + +在更新完head之后,会将旧的头结点h的next域指向为h,图中所示的虚线也就表示这个节点的自引用,被移动的节点(item为null的节点)会被GC回收 + +![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentLinkedQueue出队操作1.png) + + + + + +如果这时,有一个线程来添加元素,通过tail获取的next节点则仍然是它本身,这就出现了**p == q** 的情况,出现该种情况之后,则会触发执行head的更新,将p节点重新指向为head + +参考文章:https://www.jianshu.com/p/231caf90f30b + + + +*** + + + +#### 成员方法 + +* peek() + + peek操作会改变head指向,执行peek()方法后head会指向第一个具有非空元素的节点 + + ```java + // 获取链表的首部元素,只读取而不移除 + public E peek() { + restartFromHead: + for (;;) { + for (Node h = head, p = h, q;;) { + E item = p.item; + if (item != null || (q = p.next) == null) { + // 更改h的位置为非空元素节点 + updateHead(h, p); + return item; + } + else if (p == q) + continue restartFromHead; + else + p = q; + } + } + } + ``` + +* size() + + 用来获取当前队列的元素个数,因为整个过程都没有加锁,在并发环境中从调用size方法到返回结果期间有可能增删元素,导致统计的元素个数不精确 + + ```java + public int size() { + int count = 0; + // first()获取第一个具有非空元素的节点,若不存在,返回null + // succ(p)方法获取p的后继节点,若p == p的后继节点,则返回head + // 类似遍历链表 + for (Node p = first(); p != null; p = succ(p)) + if (p.item != null) + // 最大返回Integer.MAX_VALUE + if (++count == Integer.MAX_VALUE) + break; + return count; + } + ``` + +* remove() + + ```java + public boolean remove(Object o) { + // 删除的元素不能为null + if (o != null) { + Node next, pred = null; + for (Node p = first(); p != null; pred = p, p = next) { + boolean removed = false; + E item = p.item; + // 节点元素不为null + if (item != null) { + // 若不匹配,则获取next节点继续匹配 + if (!o.equals(item)) { + next = succ(p); + continue; + } + // 若匹配,则通过CAS操作将对应节点元素置为null + removed = p.casItem(item, null); + } + // 获取删除节点的后继节点 + next = succ(p); + // 将被删除的节点移除队列 + if (pred != null && next != null) // unlink + pred.casNext(p, next); + if (removed) + return true; + } + } + return false; + } + ``` + + + + + + + +*** + + + + + + + +# NET + +## 介绍 + +### 网络编程 + +网络编程,就是在一定的协议下,实现两台计算机的通信的技术 + +通信一定是基于软件结构实现的: + +* C/S结构 :全称为Client/Server结构,是指客户端和服务器结构,常见程序有QQ、IDEA等软件。 +* B/S结构 :全称为Browser/Server结构,是指浏览器和服务器结构。 + +两种架构各有优势,但是无论哪种架构,都离不开网络的支持。 + +网络通信的三要素: + +1. 协议:计算机网络客户端与服务端通信必须约定和彼此遵守的通信规则,HTTP、FTP、TCP、UDP、SMTP + +2. IP地址:互联网协议地址(Internet Protocol Address),用来给一个网络中的计算机设备做唯一的编号 + + * IPv4 :4个字节,32位组成,192.168.1.1 + * Pv6:可以实现为所有设备分配IP 128位 + + * 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以上的端口号。如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败,报出端口被占用异常 + +利用**协议+IP地址+端口号** 三元组合,就可以标识网络中的进程了,那么进程间的通信就可以利用这个标识与其它进程进行交互。 + + + +**** + + + +### 通信协议 + +网络通信协议:对计算机必须遵守的规则,只有遵守这些规则,计算机之间才能进行通信 + +> 应用层:应用程序(QQ,微信,浏览器),可能用到的协议(HTTP,FTP,SMTP) +> +> 传输层:TCP/IP协议 - UDP协议 +> +> 网络层 :IP协议,封装自己的IP和对方的IP和端口 +> +> 数据链路层 : 进入到硬件(网) + +TCP/IP协议:传输控制协议 (Transmission Control Protocol) + +TCP:面向连接的安全的可靠的传输通信协议 + +* 在通信之前必须确定对方在线并且连接成功才可以通信 +* 例如下载文件、浏览网页等(要求可靠传输) + +UDP:用户数据报协议(User Datagram Protocol),是一个面向无连接的不可靠传输的协议 + +* 直接发消息给对方,不管对方是否在线,发消息后也不需要确认 +* 无线(视频会议,通话),性能好,可能丢失一些数据 + + + +**** + + + +### Java模型 + +相关概念: + +* 同步:当前线程要自己进行数据的读写操作(自己去银行取钱) +* 异步:当前线程可以去做其他事情(委托别人拿银行卡到银行取钱,然后给你) +* 阻塞:在数据没有的情况下,还是要继续等待着读(排队等待) +* 非阻塞:在数据没有的情况下,会去做其他事情,一旦有了数据再来获取(柜台取款,取个号,然后坐在椅子上做其它事,等号广播会通知你办理) + +Java中的通信模型: + +1. BIO表示同步阻塞式通信,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善。 + 同步阻塞式性能极差:大量线程,大量阻塞 + +2. 伪异步通信:引入线程池,不需要一个客户端一个线程,实现线程复用来处理很多个客户端,线程可控。 + 高并发下性能还是很差:线程数量少,数据依然是阻塞的;数据没有来线程还是要等待 + +3. NIO表示**同步非阻塞IO**,服务器实现模式为请求对应一个线程,客户端发送的连接会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理 + + 工作原理:1个主线程专门负责接收客户端,1个线程轮询所有的客户端,发来了数据才会开启线程处理 + 同步:线程还要不断的接收客户端连接,以及处理数据 + 非阻塞:如果一个管道没有数据,不需要等待,可以轮询下一个管道是否有数据 + +4. AIO表示异步非阻塞IO,AIO 引入异步通道的概念,采用了 Proactor 模式,有效的请求才启动线程,特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用 + 异步:服务端线程接收到了客户端管道以后就交给底层处理IO通信,线程可以做其他事情 + 非阻塞:底层也是客户端有数据才会处理,有了数据以后处理好通知服务器应用来启动线程进行处理 + +各种模型应用场景: + +* BIO适用于连接数目比较小且固定的架构,该方式对服务器资源要求比较高,并发局限于应用中,程序简单 +* NIO适用于连接数目多且连接比较短(轻操作)的架构,如聊天服务器,并发局限于应用中,编程复杂,JDK 1.4开始支持 +* AIO适用于连接数目多且连接比较长(重操作)的架构,如相册服务器,充分调用操作系统参与并发操作,编程复杂,JDK 1.7开始支持 + + + + + +**** + + + +## I/O + +### IO模型 + +#### 五种模型 + +对于一个套接字上的输入操作,第一步是等待数据从网络中到达,当数据到达时被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区 + +Linux 有五种 I/O 模型: + +- 阻塞式 I/O +- 非阻塞式 I/O +- I/O 复用(select 和 poll) +- 信号驱动式 I/O(SIGIO) +- 异步 I/O(AIO) + +五种模型对比: + +* 同步 I/O 包括阻塞式 I/O、非阻塞式 I/O、I/O 复用和信号驱动 I/O ,它们的主要区别在第一个阶段,非阻塞式 I/O 、信号驱动 I/O 和异步 I/O 在第一阶段不会阻塞 + +- 同步 I/O:将数据从内核缓冲区复制到应用进程缓冲区的阶段(第二阶段),应用进程会阻塞 +- 异步 I/O:第二阶段应用进程不会阻塞 + + + +*** + + + +#### 阻塞式IO + +应用进程通过系统调用 recvfrom 接收数据,会被阻塞,直到数据从内核缓冲区复制到应用进程缓冲区中才返回。阻塞不意味着整个操作系统都被阻塞,其它应用进程还可以执行,只是当前阻塞进程不消耗 CPU 时间,这种模型的 CPU 利用率会比较高 + +recvfrom() 用于接收 Socket 传来的数据,并复制到应用进程的缓冲区 buf 中,把 recvfrom() 当成系统调用 + +![](https://gitee.com/seazean/images/raw/master/Java/IO模型-阻塞式IO.png) + + + +*** + + + +#### 非阻塞式 + +应用进程通过 recvfrom 调用不停的去和内核交互,直到内核准备好数据。如果没有准备好数据,内核返回一个错误码,过一段时间应用进程再执行 recvfrom 系统调用,在两次发送请求的时间段,进程可以进行其他任务,这种方式称为轮询(polling) + +由于 CPU 要处理更多的系统调用,因此这种模型的 CPU 利用率比较低 + +![](https://gitee.com/seazean/images/raw/master/Java/IO模型-非阻塞式IO.png) + + + +*** + + + +#### 信号驱动 + +应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,等待数据阶段应用进程是非阻塞的。当内核数据准备就绪时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中 + +相比于非阻塞式 I/O 的轮询方式,信号驱动 I/O 的 CPU 利用率更高 + +![](https://gitee.com/seazean/images/raw/master/Java/IO模型-信号驱动IO.png) + + + +*** + + + +#### IO复用 + +IO 复用模型使用 select 或者 poll 函数等待数据,select 会监听所有注册好的 IO,等待多个套接字中的任何一个变为可读,等待过程会被**阻塞**,当某个套接字准备好数据变为可读时 select 调用就返回,然后调用 recvfrom 把数据从内核复制到进程中 + +IO 复用让单个进程具有处理多个 I/O 事件的能力,又被称为 Event Driven I/O,即事件驱动 I/O + +如果一个 Web 服务器没有 I/O 复用,那么每一个 Socket 连接都要创建一个线程去处理,如果同时有几万个连接,就需要创建相同数量的线程。相比于多进程和多线程技术,I/O 复用不需要进程线程创建和切换的开销,系统开销更小 + +![](https://gitee.com/seazean/images/raw/master/Java/IO模型-IO复用模型.png) + + + +*** + + + +##### 异步IO + +应用进程执行 aio_read 系统调用会立即返回,给内核传递描述符、缓冲区指针、缓冲区大小等。应用进程可以继续执行不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。 + +异步 I/O 与信号驱动 I/O 的区别在于,异步 I/O 的信号是通知应用进程 I/O 完成,而信号驱动 I/O 的信号是通知应用进程可以开始 I/O + +![](https://gitee.com/seazean/images/raw/master/Java/IO模型-异步IO模型.png) + + + +**** + + + +### 多路复用 + +#### select + +##### 函数 + +socket 不是文件,只是一个标识符,但是 Unix 操作系统把所有东西都**看作**是文件,所以 socket 说成 file descriptor,也就是 fd + +select 允许应用程序监视一组文件描述符,等待一个或者多个描述符成为就绪状态,从而完成 I/O 操作。 + +```c +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 有三种类型的描述符:readset、writeset、exceptset,对应读、写、异常条件的描述符集合 + +- n 是监测的 socket 的最大数量 + +- timeout 为超时参数,调用 select 会一直阻塞直到有描述符的事件到达或者等待的时间超过 timeout + + ```c + struct timeval{ + long tv_sec; //秒 + long tv_usec;//微秒 + } + ``` + + timeout == null:等待无限长的时间 + tv_sec == 0 && tv_usec == 0:获取后直接返回,不阻塞等待 + tv_sec != 0 || tv_usec != 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 +``` + +示例: + +```c +sockfd = socket(AF_INET, SOCK_STREAM, 0); +memset(&addr, 0, sizeof(addr))); +addr.sin_family = AF_INET; +addr.sin_port = htons(2000); +addr.sin_addr.s_addr = INADDR_ANY; +bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));//绑定连接 +listen(sockfd, 5);//监听5个端口 +for(i = 0; i < 5; i++) { + memset(&client, e, sizeof(client)); + addrlen = sizeof(client); + fds[i] = accept(sockfd, (struct sockaddr*)&client, &addrlen); + //将监听的对应的文件描述符fd存入fds:[3,4,5,6,7] + if(fds[i] > max) + max = fds[i]; +} +while(1) { + FD_ZERO(&rset);//置为0 + for(i = 0; i < 5; i++) { + FD_SET(fds[i], &rset);//对应位置1 [0001 1111 00.....] + } + print("round again"); + select(max + 1, &rset, NULL, NULL, NULL);//监听 + + for(i = 0; i <5; i++) { + if(FD_ISSET(fds[i], &rset)) {//判断监听哪一个端口 + memset(buffer, 0, MAXBUF); + read(fds[i], buffer, MAXBUF);//进入内核态读数据 + print(buffer); + } + } +} +``` + + + +参考视频:https://www.bilibili.com/video/BV19D4y1o797 + + + +**** + + + +##### 流程 + +select 调用流程图: + +![](https://gitee.com/seazean/images/raw/master/Java/IO-select调用过程.png) + +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 便被唤醒 +5. poll 方法返回时会返回一个描述读写操作是否就绪的 mask 掩码,根据这个 mask 掩码给 fd_set 赋值 +6. 如果遍历完所有的 fd,还没有返回一个可读写的 mask 掩码,则会调用 schedule_timeout 让调用 select 的进程(就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程,如果超过一定的超时时间(schedule_timeout指定),没有其他线程唤醒,则调用 select 的进程会重新被唤醒获得 CPU,进而重新遍历 fd,判断有没有就绪的 fd +7. 把 fd_set 从内核空间拷贝到用户空间,阻塞进程继续执行 + + + +参考文章:https://www.cnblogs.com/anker/p/3265058.html + +其他流程图:https://www.processon.com/view/link/5f62b9a6e401fd2ad7e5d6d1 + + + +**** + + + +#### poll + +poll 的功能与 select 类似,也是等待一组描述符中的一个成为就绪状态 + +```c +int poll(struct pollfd *fds, unsigned int nfds, int timeout); +``` + +poll 中的描述符是 pollfd 类型的数组,pollfd 的定义如下: + +```c +struct pollfd { + int fd; /* file descriptor */ + short events; /* requested events */ + short revents; /* returned events */ +}; +``` + +select 和 poll 对比: + +- select 会修改描述符,而 poll 不会 +- select 的描述符类型使用数组实现,有描述符的限制;而 poll 使用**链表**实现,没有描述符数量的限制 +- poll 提供了更多的事件类型,并且对描述符的重复利用上比 select 高 + +* select 和 poll 速度都比较慢,每次调用都需要将全部描述符数组 fd 从应用进程缓冲区复制到内核缓冲区,同时每次都需要在内核遍历传递进来的所有 fd ,这个开销在 fd 很多时会很大 +* 几乎所有的系统都支持 select,但是只有比较新的系统支持 poll +* select 和 poll 的时间复杂度 O(n),对 socket 进行扫描时是线性扫描,即采用轮询的方法,效率较低,因为并不知道具体是哪个 socket 具有事件,所以随着 FD 的增加会造成遍历速度慢的**线性下降**性能问题 +* poll 还有一个特点是水平触发,如果报告了 fd 后,没有被处理,那么下次 poll 时会再次报告该 fd +* 如果一个线程对某个描述符调用了 select 或者 poll,另一个线程关闭了该描述符,会导致调用结果不确定 + + + +参考文章:https://github.com/CyC2018/CS-Notes/blob/master/notes/Socket.md + + + +**** + + + +#### epoll + +##### 函数 + +epoll 使用事件的就绪通知方式,通过 epoll_ctl() 向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵**红黑树**上,一旦该 fd 就绪,内核通过 callback 回调函数将 I/O 准备好的描述符加入到一个**链表**中管理,进程调用 epoll_wait() 便可以得到事件完成的描述符 + +```c +int epoll_create(int size); +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_ctl:epoll 的事件注册函数,select 函数是调用时指定需要监听的描述符和事件,epoll 先将用户感兴趣的描述符事件注册到 epoll 空间。此函数是非阻塞函数,用来增删改 epoll 空间内的描述符,参数解释: + + * epfd:epoll 结构的进程 fd 编号,函数将依靠该编号找到对应的 epoll 结构 + + * op:表示当前请求类型,有三个宏定义: + + * EPOLL_CTL_ADD:注册新的 fd 到 epfd 中 + * EPOLL_CTL_MOD:修改已经注册的 fd 的监听事件 + * EPOLL_CTI_DEL:从 epfd 中删除一个 fd + + * fd:需要监听的文件描述符,一般指 socket_fd + + * event:告诉内核对该 fd 资源感兴趣的事件,epoll_event 的结构: + + ```c + struct epoll_event { + _uint32_t events; /*epoll events*/ + epoll_data_t data; /*user data variable*/ + } + ``` + + events 可以是以下几个宏集合:EPOLLIN、EPOLOUT、EPOLLPRI、EPOLLERR、EPOLLHUP(挂断)、EPOLET(边缘触发)、EPOLLONESHOT(只监听一次,事件触发后自动清除该 fd,从 epoll 列表) + +* epoll_wait:等待事件的产生,类似于 select() 调用,返回值为本次就绪的 fd 个数 + + * epfd:指定感兴趣的 epoll 事件列表 + * events:指向一个 epoll_event 结构数组,当函数返回时,内核会把就绪状态的数据拷贝到该数组 + * maxevents:标明 epoll_event 数组最多能接收的数据量,即本次操作最多能获取多少就绪数据 + * timeout:单位为毫秒 + * 0:表示立即返回,非阻塞调用 + * -1:阻塞调用,直到有用户感兴趣的事件就绪为止 + * 大于0:阻塞调用,阻塞指定时间内如果有事件就绪则提前返回,否则等待指定时间后返回 + +epoll 的描述符事件有两种触发模式:LT(level trigger)和 ET(edge trigger): + +* LT 模式:当 epoll_wait() 检测到描述符事件到达时,将此事件通知进程,进程可以不立即处理该事件,下次调用 epoll_wait() 会再次通知进程。是默认的一种模式,并且同时支持 Blocking 和 No-Blocking +* ET 模式:通知之后进程必须立即处理事件,下次再调用 epoll_wait() 时不会再得到事件到达的通知。减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高;只支持 No-Blocking,以避免由于一个文件的阻塞读/阻塞写操作把处理多个文件描述符的任务饥饿 + +```c +// 创建 epoll 描述符,每个应用程序只需要一个,用于监控所有套接字 +int pollingfd = epoll_create(0xCAFE); +if ( pollingfd < 0 )// report error +// 初始化 epoll 结构 +struct epoll_event ev = { 0 }; + +// 将连接类实例与事件相关联,可以关联任何想要的东西 +ev.data.ptr = pConnection1; + +// 监视输入,并且在事件发生后不自动重新准备描述符 +ev.events = EPOLLIN | EPOLLONESHOT; +// 将描述符添加到监控列表中,即使另一个线程在epoll_wait中等待,描述符将被正确添加 +if ( epoll_ctl( epollfd, EPOLL_CTL_ADD, pConnection1->getSocket(), &ev) != 0 ) + // report error + +// 最多等待 20 个事件 +struct epoll_event pevents[20]; + +// 等待10秒,检索20个并存入epoll_event数组 +int ready = epoll_wait(pollingfd, pevents, 20, 10000); +// 检查epoll是否成功 +if ( ret == -1)// report error and abort +else if ( ret == 0)// timeout; no event detected +else +{ + for (int i = 0; i < ready; i+ ) + { + if ( pevents[i].events & EPOLLIN ) + { + // 获取连接指针 + Connection * c = (Connection*) pevents[i].data.ptr; + c->handleReadEvent(); + } + } +} +``` + + + +流程图: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 + + + +*** + + + +##### 特点 + +epoll 的特点: + +* epoll 仅适用于 Linux 系统 +* 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() 文件映射内存加速与内核空间的消息传递,减少复制开销 +* 前面两者要把 current 往设备等待队列中挂一次,epoll 也只把 current 往等待队列上挂一次,但是这里的等待队列并不是设备等待队列,只是一个 epoll 内部定义的等待队列,这样节省不少的开销 +* epoll 对多线程编程更有友好,一个线程调用了 epoll_wait() 另一个线程关闭了同一个描述符,也不会产生像 select 和 poll 的不确定情况 + + + +参考文章:https://www.jianshu.com/p/dfd940e7fca2 + +参考文章:https://www.cnblogs.com/anker/p/3265058.html + + + +*** + + + +#### 应用 + +应用场景: + +* select 应用场景: + * select 的 timeout 参数精度为微秒,而 poll 和 epoll 为毫秒,因此 select 更加适用于实时性要求比较高的场景,比如核反应堆的控制 + * select 可移植性更好,几乎被所有主流平台所支持 + +* poll 应用场景:poll 没有最大描述符数量的限制,适用于平台支持并且对实时性要求不高的情况 + +* epoll 应用场景: + * 运行在 Linux 平台上,有大量的描述符需要同时轮询,并且这些连接最好是长连接 + * 需要同时监控小于 1000 个描述符,没必要使用 epoll,因为这个应用场景下并不能体现 epoll 的优势 + * 需要监控的描述符状态变化多,而且都是非常短暂的,也没有必要使用 epoll。因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用,频繁系统调用降低效率,并且 epoll 的描述符存储在内核,不容易调试 + + + +参考文章:https://github.com/CyC2018/CS-Notes/blob/master/notes/Socket.md + + + +**** + + + +### 系统调用 + +#### 内核态 + +用户空间:用户代码、用户堆栈 + +内核空间:内核代码、内核调度程序、进程描述符(内核堆栈、thread_info进程描述符) + +* 进程描述符和用户的进程是一一对应的 +* SYS_API 系统调用:如 read、write,系统调用就是 0X80 中断 +* 进程描述符pd:进程从用户态切换到内核态时,需要保存用户态时的上下文信息, +* 线程上下文:用户程序基地址,程序计数器、cpu cache、寄存器等,方便程序切回用户态时恢复现场 +* 内核堆栈:系统调用函数也是要创建变量的,这些变量在内核堆栈上分配 + +![](https://gitee.com/seazean/images/raw/master/Java/IO-用户态和内核态.png) + + + +*** + + + +#### 80中断 + +在用户程序中调用操作系统提供的核心态级别的子功能,为了系统安全需要进行用户态和内核态转换,状态的转换需要进行 CPU 中断,中断分为硬中断和软中断: + +* 硬中断:如网络传输中,数据到达网卡后,网卡经过一系列操作后发起硬件中断 +* 软中断:如程序运行过程中本身产生的一些中断 + - 发起 `0X80` 中断 + - 程序执行碰到除 0 异常 + +系统调用 system_call 函数所对应的中断指令编号是 0X80(十进制是8×16=128),而该指令编号对应的就是系统调用程序的入口,所以称系统调用为 80 中断 + +系统调用的流程: + +* 在 CPU 寄存器里存一个系统调用号,表示哪个系统函数,比如 read +* 将 CPU 的临时数据都保存到 thread_info 中 +* 执行 80 中断处理程序,找到刚刚存的系统调用号(read),先检查缓存中有没有对应的数据,没有就去磁盘中加载到内核缓冲区,然后从内核缓冲区拷贝到用户空间 +* 最后恢复到用户态,通过 thread_info 恢复现场,用户态继续执行 + +![](https://gitee.com/seazean/images/raw/master/Java/IO-系统调用的过程.jpg) + + + +参考文章:https://blog.csdn.net/hancoder/article/details/112149121 + + + +**** + + + +### 零拷贝 + +#### DMA + +DMA (Direct Memory Access) :直接存储器访问,让外部设备不通过 CPU 直接与系统内存交换数据的接口技术 + +作用:可以解决批量数据的输入/输出问题,使数据的传送速度取决于存储器和外设的工作速度 + +把内存数据传输到网卡然后发送: + +* 没有 DMA:CPU 读内存数据到 CPU 高速缓存,再写到网卡,这样就把 CPU 的速度拉低到和网卡一个速度 +* 使用 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 发出总线接管请求 +* 当 CPU 发出允许接管信号后,进入 DMA 操作周期 +* 确定传送数据的主存单元地址及长度,并自动修改主存地址计数和传送长度计数 +* 规定数据在主存和外设间的传送方向,发出读写等控制信号,执行数据传送操作 +* 判断 DMA 传送是否结束,发出 DMA 结束信号,使 CPU 恢复正常工作状态(中断) + + + +*** + + + +#### BIO + +传统的 I/O 操作进行了 4 次用户空间与内核空间的上下文切换,以及 4 次数据拷贝: + +* JVM 发出 read() 系统调用,OS 上下文切换到内核模式(切换1)并将数据从网卡或硬盘等通过 DMA 读取到内核空间缓冲区(拷贝1) +* OS 内核将数据复制到用户空间缓冲区(拷贝2),然后 read 系统调用返回,又会导致一次内核空间到用户空间的上下文切换(切换2) +* JVM 处理代码逻辑并发送 write() 系统调用,OS 上下文切换到内核模式(切换3)并从用户空间缓冲区复制数据到内核空间缓冲区(拷贝3) +* write 系统调用返回,导致内核空间到用户空间的再次上下文切换(切换4),将内核空间缓冲区中的数据写到 hardware(拷贝4) + +流程图中的箭头反过来也成立,可以从网卡获取数据 + +![](https://gitee.com/seazean/images/raw/master/Java/IO-BIO工作流程.png) + +read 调用图示:read、write 都是系统调用指令 + + + + + +*** + + + +#### mmap + +mmap(Memory Mapped Files)加 write 实现零拷贝,零拷贝就是没有数据从内核空间复制到用户空间 + +用户空间和内核空间都使用内存,所以可以共享同一块物理内存地址,省去用户态和内核态之间的拷贝。写网卡时,共享空间的内容拷贝到 socket 缓冲区,然后交给 DMA 发送到网卡,只需要 3 次复制 + +进行了 4 次用户空间与内核空间的上下文切换,以及 3 次数据拷贝(2 次 DMA,一次 CPU 复制): + +* 发出 mmap 系统调用,DMA 拷贝到内核缓冲区;mmap系统调用返回,无需拷贝 +* 发出 write 系统调用,将数据从内核缓冲区拷贝到内核 socket 缓冲区;write系统调用返回,DMA 将内核空间 socket 缓冲区中的数据传递到协议引擎 + +![](https://gitee.com/seazean/images/raw/master/Java/IO-mmap工作流程.png) + +原理:利用操作系统的 Page 来实现文件到物理内存的直接映射,完成映射后对物理内存的操作会**被同步**到硬盘上 + +缺点:不可靠,写到 mmap 中的数据并没有被真正的写到硬盘,操作系统会在程序主动调用 flush 的时候才把数据真正的写到硬盘 + +Java NIO 提供了 **MappedByteBuffer** 类可以用来实现 mmap 内存映射,MappedByteBuffer 类对象**只能**通过调用 `FileChannel.map()` 获取 + + + +**** + + + +#### sendfile + +sendfile 实现零拷贝,打开文件的文件描述符 fd 和 socket 的 fd 传递给 sendfile,然后经过 3 次复制和 2 次用户态和内核态的切换 + +原理:数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer,由于和用户态完全无关,就减少了两次上下文切换 + +![](https://gitee.com/seazean/images/raw/master/Java/IO-sendfile工作流程.png) + +sendfile2.4 之后,sendfile 实现了更简单的方式,文件到达内核缓冲区后,不必再将数据全部复制到 socket buffer 缓冲区,而是只**将记录数据位置和长度相关等描述符信息**保存到 socket buffer,DMA 根据 socket 缓冲区中描述符提供的位置和偏移量信息直接将内核空间缓冲区中的数据拷贝到协议引擎上(2 次复制 2 次切换) + +Java NIO 对 sendfile 的支持是 `FileChannel.transferTo()/transferFrom()`,把磁盘文件读取 OS 内核缓冲区后的 fileChannel,直接转给 socketChannel 发送,底层就是sendfile + + + +参考文章:https://blog.csdn.net/hancoder/article/details/112149121 + + + +*** + + + +## Inet + +一个该 InetAddress 类的对象就代表一个IP地址对象 + +成员方法: +`static InetAddress getLocalHost()` : 获得本地主机IP地址对象 +`static InetAddress getByName(String host)` : 根据IP地址字符串或主机名获得对应的IP地址对象 +`String getHostName()` : 获取主机名 +`String getHostAddress()` : 获得IP地址字符串 + +```java +public class InetAddressDemo { + public static void main(String[] args) throws Exception { + // 1.获取本机地址对象 + InetAddress ip = InetAddress.getLocalHost(); + System.out.println(ip.getHostName());//DESKTOP-NNMBHQR + System.out.println(ip.getHostAddress());//192.168.11.1 + // 2.获取域名ip对象 + InetAddress ip2 = InetAddress.getByName("www.baidu.com"); + System.out.println(ip2.getHostName());//www.baidu.com + System.out.println(ip2.getHostAddress());//14.215.177.38 + // 3.获取公网IP对象。 + InetAddress ip3 = InetAddress.getByName("182.61.200.6"); + System.out.println(ip3.getHostName());//182.61.200.6 + System.out.println(ip3.getHostAddress());//182.61.200.6 + + // 4.判断是否能通: ping 5s之前测试是否可通 + System.out.println(ip2.isReachable(5000)); // ping百度 + } +} +``` + + + +*** + + + +## UDP + +### 基本介绍 + +UDP(User Datagram Protocol)协议的特点: + +* 面向无连接的协议 +* 发送端只管发送,不确认对方是否能收到 +* 基于数据包进行数据传输 +* 发送数据的包的大小限制**64KB**以内 +* 因为面向无连接,速度快,但是不可靠,会丢失数据 + +UDP协议的使用场景:在线视频、网络语音、电话 + + + +*** + + + +### 实现UDP + +UDP协议相关的两个类 + +* DatagramPacket(数据包对象):用来封装要发送或要接收的数据,比如:集装箱 +* DatagramSocket(发送对象):用来发送或接收数据包,比如:码头 + +**DatagramPacket**: + +* DatagramPacket类 + + `public new DatagramPacket(byte[] buf, int length, InetAddress address, int port)` : 创建发送端数据包对象,参数: + + * buf:要发送的内容,字节数组 + * length:要发送内容的长度,单位是字节 + * address:接收端的IP地址对象 + * port:接收端的端口号 + + `public new DatagramPacket(byte[] buf, int length)` : 创建接收端的数据包对象,参数: + + * buf:用来存储接收到内容 + * length:能够接收内容的长度 + +* DatagramPacket 类常用方法 + `public int getLength()` : 获得实际接收到的字节个数 + `public byte[] getData()` : 返回数据缓冲区 + +**DatagramSocket**: + +* DatagramSocket类构造方法 + `protected DatagramSocket()` : 创建发送端的Socket对象,系统会随机分配一个端口号 + `protected DatagramSocket(int port)` : 创建接收端的Socket对象并指定端口号 +* DatagramSocket类成员方法 + `public void send(DatagramPacket dp)` : 发送数据包 + `public void receive(DatagramPacket p)` : 接收数据包 + `public void close()` : 关闭数据报套接字 + +```java +public class UDPClientDemo { + public static void main(String[] args) throws Exception { + System.out.println("===启动客户端==="); + // 1.创建一个集装箱对象,用于封装需要发送的数据包! + byte[] buffer = "我学Java".getBytes(); + DatagramPacket packet = new DatagramPacket(buffer,bubffer.length + ,InetAddress.getLoclHost,8000); + // 2.创建一个码头对象 + DatagramSocket socket = new DatagramSocket(); + // 3.开始发送数据包对象 + socket.send(packet); + socket.close(); + } +} +public class UDPServerDemo{ + public static void main(String[] args) throws Exception { + System.out.println("==启动服务端程序=="); + // 1.创建一个接收客户都端的数据包对象(集装箱) + byte[] buffer = new byte[1024*64]; + DatagramPacket packet = new DatagramPacket(buffer,bubffer.length); + // 2.创建一个接收端的码头对象 + DatagramSocket socket = new DatagramSocket(8000); + // 3.开始接收 + socket.receive(packet); + // 4.从集装箱中获取本次读取的数据量 + int len = packet.getLength(); + // 5.输出数据 + //String rs = new String(socket.getData(), 0, len) + String rs = new String(buffer , 0 , len); + System.out.println(rs); + // 6.服务端还可以获取发来信息的客户端的IP和端口。 + String ip = packet.getAddress().getHostAdress(); + int port = packet.getPort(); + socket.close(); + } +} +``` + + + +*** + + + +### 通讯方式 + +UDP通信方式: + ++ 单播:用于两个主机之间的端对端通信 + ++ 组播:用于对一组特定的主机进行通信 + IP : 224.0.1.0 + Socket对象 : MulticastSocket + ++ 广播:用于一个主机对整个局域网上所有主机上的数据通信 + IP : 255.255.255.255 + Socket对象 : DatagramSocket + + + +*** + + + +## TCP + +### 基本介绍 + +TCP/IP协议 ==> Transfer Control Protocol ==> 传输控制协议 + +TCP/IP协议的特点: + +* 面向连接的协议 +* 只能由客户端主动发送数据给服务器端,服务器端接收到数据之后,可以给客户端响应数据 +* 通过**三次握手**建立连接,连接成功形成数据传输通道;通过**四次挥手**断开连接 +* 基于IO流进行数据传输 +* 传输数据大小没有限制 +* 因为面向连接的协议,速度慢,但是是可靠的协议。 + +TCP协议的使用场景:文件上传和下载、邮件发送和接收、远程登录 + +注意:**TCP不会为没有数据的ACK超时重传** + +三次握手 + +四次挥手 + + + +*** + + + +### Socket + +TCP通信也叫**Socket网络编程**,只要代码基于Socket开发,底层就是基于了可靠传输的TCP通信 + +TCP协议相关的类: + +* Socket:一个该类的对象就代表一个客户端程序。 +* ServerSocket:一个该类的对象就代表一个服务器端程序。 + +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 + +ServerSocket类: + +* 构造方法:`public ServerSocket(int port)` +* 常用API:`public Socket accept()`,**阻塞等待**接收一个客户端的Socket管道连接请求,连接成功返回一个Socket对象 + +相当于客户端和服务器建立一个数据管道,管道一般不用close + + + +*** + + + +### 实现TCP + +#### 开发流程 + +客户端的开发流程: + +1. 客户端要请求于服务端的socket管道连接 +2. 从socket通信管道中得到一个字节输出流 +3. 通过字节输出流给服务端写出数据 + +服务端的开发流程: + +1. 用ServerSocket注册端口 +2. 接收客户端的Socket管道连接 +3. 从socket通信管道中得到一个字节输入流 +4. 从字节输入流中读取客户端发来的数据 + +![](https://gitee.com/seazean/images/raw/master/Java/BIO工作机制.png) + +![](https://gitee.com/seazean/images/raw/master/Java/TCP-工作模型.png) + +* 如果输出缓冲区空间不够存放主机发送的数据,则会被阻塞,输入缓冲区同理 +* 缓冲区不属于应用程序,属于内核 +* TCP从输出缓冲区读取数据会加锁阻塞线程 + + + +*** + + + +#### BIO通信 + +需求一:客户端发送一行数据,服务端接收一行数据 + +````java +public class ClientDemo { + public static void main(String[] args) throws Exception { + // 1.客户端要请求于服务端的socket管道连接。 + Socket socket = new Socket("127.0.0.1",8080); + // 2.从socket通信管道中得到一个字节输出流 + OutputStream os = new socket.getOutputStream(); + // 3.把低级的字节输出流包装成高级的打印流。 + PrintStream ps = new PrintStream(os); + // 4.开始发消息出去 + ps.println("我是客户端"); + ps.flush();//一般不关闭IO流 + System.out.println("客户端发送完毕~~~~"); + } +} +public class ServerDemo{ + public static void main(String[] args) throws Exception { + System.out.println("----服务端启动----"); + // 1.注册端口: public ServerSocket(int port) + ServerSocket serverSocket = new ServerSocket(8080); + // 2.开始等待接收客户端的Socket管道连接。 + Socket socket = serverSocket.accept(); + // 3.从socket通信管道中得到一个字节输入流。 + InputStream is = socket.getInputStream(); + // 4.把字节输入流转换成字符输入流 + BufferedReader br = new BufferedReader(new InputStreamReader(is)); + // 6.按照行读取消息 。 + String line; + if((line = br.readLine()) != null){ + System.out.println(line); + } + } +} +```` + + + +需求二:客户2端可以反复发送数据,服务端可以反复数据2 + +```java +public class ClientDemo { + public static void main(String[] args) throws Exception { + // 1.客户端要请求于服务端的socket管道连接。 + Socket socket = new Socket("127.0.0.1",8080); + // 2.从socket通信管道中得到一个字节输出流 + OutputStream os = new socket.getOutputStream(); + // 3.把低级的字节输出流包装成高级的打印流。 + PrintStream ps = new PrintStream(os); + // 4.开始发消息出去 + while(true){ + Scanner sc = new Scanner(System.in); + System.out.print("请说:"); + ps.println(sc.nextLine()); + ps.flush(); + } + } +} +public class ServerDemo{ + public static void main(String[] args) throws Exception { + System.out.println("----服务端启动----"); + // 1.注册端口: public ServerSocket(int port) + ServerSocket serverSocket = new ServerSocket(8080); + // 2.开始等待接收客户端的Socket管道连接。 + Socket socket = serverSocket.accept(); + // 3.从socket通信管道中得到一个字节输入流。 + InputStream is = socket.getInputStream(); + // 4.把字节输入流转换成字符输入流 + BufferedReader br = new BufferedReader(new InputStreamReader(is)); + // 6.按照行读取消息 。 + String line; + while((line = br.readLine()) != null){ + System.out.println(line); + } + } +} +``` + + + +需求三:实现一个服务端可以同时接收多个客户端的消息。 + +```java +public class ClientDemo { + public static void main(String[] args) throws Exception { + Socket socket = new Socket("127.0.0.1",8080); + OutputStream os = new socket.getOutputStream(); + PrintStream ps = new PrintStream(os); + while(true){ + Scanner sc = new Scanner(System.in); + System.out.print("请说:"); + ps.println(sc.nextLine()); + ps.flush(); + } + } +} +public class ServerDemo{ + public static void main(String[] args) throws Exception { + System.out.println("----服务端启动----"); + ServerSocket serverSocket = new ServerSocket(8080); + while(true){ + // 开始等待接收客户端的Socket管道连接。 + Socket socket = serverSocket.accept(); + // 每接收到一个客户端必须为这个客户端管道分配一个独立的线程来处理与之通信。 + new ServerReaderThread(socket).start(); + } + } +} +class ServerReaderThread extends Thread{ + privat Socket socket; + public ServerReaderThread(Socket socket){this.socket = socket;} + @Override + public void run() { + try(InputStream is = socket.getInputStream(); + BufferedReader br = new BufferedReader(new InputStreamReader(is)) + ){ + String line; + while((line = br.readLine()) != null){ + sout(socket.getRemoteSocketAddress() + ":" + line); + } + }catch(Exception e){ + sout(socket.getRemoteSocketAddress() + "下线了~~~~~~"); + } + } +} +``` + + + +*** + + + +#### 伪异步 + +一个客户端要一个线程,这种模型是不行的,并发越高系统瘫痪的越快,可以在服务端引入线程池,使用线程池来处理与客户端的消息通信 + +优势:不会引起系统的死机,可以控制并发线程的数量 +劣势:同时可以并发的线程将受到限制 + +```java +public class BIOServer { + public static void main(String[] args) throws Exception { + //线程池机制 + //创建一个线程池,如果有客户端连接,就创建一个线程,与之通讯(单独写一个方法) + ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(); + //创建ServerSocket + ServerSocket serverSocket = new ServerSocket(6666); + System.out.println("服务器启动了"); + while (true) { + System.out.println("线程名字 = " + Thread.currentThread().getName()); + //监听,等待客户端连接 + System.out.println("等待连接...."); + final Socket socket = serverSocket.accept(); + System.out.println("连接到一个客户端"); + //创建一个线程,与之通讯 + newCachedThreadPool.execute(new Runnable() { + public void run() { + //可以和客户端通讯 + handler(socket); + } + }); + } + } + + //编写一个handler方法,和客户端通讯 + public static void handler(Socket socket) { + try { + System.out.println("线程名字 = " + Thread.currentThread().getName()); + byte[] bytes = new byte[1024]; + //通过socket获取输入流 + InputStream inputStream = socket.getInputStream(); + int len; + //循环的读取客户端发送的数据 + while ((len = inputStream.read(bytes)) != -1) { + System.out.println("线程名字 = " + Thread.currentThread().getName()); + //输出客户端发送的数据 + System.out.println(new String(bytes, 0, read)); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + System.out.println("关闭和client的连接"); + try { + socket.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } +} +``` + + + +**** + + + +### 文件传输 + +#### 字节流 + +客户端:本地图片: ‪E:\seazean\图片资源\beautiful.jpg +服务端:服务器路径:E:\seazean\图片服务器 + +UUID. randomUUID() : 方法生成随机的文件名 + +**socket.shutdownOutput()**:这个必须执行,不然服务器会一直循环等待数据,最后文件损坏,程序报错 + +```java +//常量包 +public class Constants { + public static final String SRC_IMAGE = "D:\\seazean\\图片资源\\beautiful.jpg"; + public static final String SERVER_DIR = "D:\\seazean\\图片服务器\\"; + public static final String SERVER_IP = "127.0.0.1"; + public static final int SERVER_PORT = 8888; + +} +public class ClientDemo { + public static void main(String[] args) throws Exception { + Socket socket = new Socket(Constants.ERVER_IP,Constants.SERVER_PORT); + BufferedOutputStream bos=new BufferedOutputStream(socket.getOutputStream()); + //提取本机的图片上传给服务端。Constants.SRC_IMAGE + BufferedInputStream bis = new BufferedInputStream(new FileInputStream()); + byte[] buffer = new byte[1024]; + int len ; + while((len = bis.read(buffer)) != -1) { + bos.write(buffer, 0 ,len); + } + bos.flush();// 刷新图片数据到服务端!! + socket.shutdownOutput();// 告诉服务端我的数据已经发送完毕,不要在等我了! + bis.close(); + + //等待着服务端的响应数据!! + BufferedReader br = new BufferedReader( + new InputStreamReader(socket.getInputStream())); + System.out.println("收到服务端响应:"+br.readLine()); + } +} +``` + +```java +public class ServerDemo { + public static void main(String[] args) throws Exception { + System.out.println("----服务端启动----"); + // 1.注册端口: + ServerSocket serverSocket = new ServerSocket(Constants.SERVER_PORT); + // 2.定义一个循环不断的接收客户端的连接请求 + while(true){ + // 3.开始等待接收客户端的Socket管道连接。 + Socket socket = serverSocket.accept(); + // 4.每接收到一个客户端必须为这个客户端管道分配一个独立的线程来处理与之通信。 + new ServerReaderThread(socket).start(); + } + } +} +class ServerReaderThread extends Thread{ + private Socket socket ; + public ServerReaderThread(Socket socket){this.socket = socket;} + @Override + public void run() { + try{ + InputStream is = socket.getInputStream(); + BufferedInputStream bis = new BufferedInputStream(is); + BufferedOutputStream bos = new BufferedOutputStream( + new FileOutputStream + (Constants.SERVER_DIR+UUID.randomUUID().toString()+".jpg")); + byte[] buffer = new byte[1024]; + int len; + while((len = bis.read(buffer)) != -1){ + bos.write(buffer,0,len); + } + bos.close(); + System.out.println("服务端接收完毕了!"); + + // 4.响应数据给客户端 + PrintStream ps = new PrintStream(socket.getOutputStream()); + ps.println("您好,已成功接收您上传的图片!"); + ps.flush(); + Thread.sleep(10000); + }catch (Exception e){ + sout(socket.getRemoteSocketAddress() + "下线了"); + } + } +} +``` + + + +**** + + + +#### 数据流 + +构造方法: +`DataOutputStream(OutputStream out)` : 创建一个新的数据输出流,以将数据写入指定的底层输出流 +`DataInputStream(InputStream in) ` : 创建使用指定的底层 InputStream 的 DataInputStream + +常用API: +`final void writeUTF(String str)` : 使用机器无关的方式使用 UTF-8 编码将字符串写入底层输出流 +`final String readUTF()` : 读取以modified UTF-8格式编码的 Unicode 字符串,返回 String 类型 + +```java +public class Client { + public static void main(String[] args) { + InputStream is = new FileInputStream("path"); + // 1、请求与服务端的Socket链接 + Socket socket = new Socket("127.0.0.1" , 8888); + // 2、把字节输出流包装成一个数据输出流 + DataOutputStream dos = new DataOutputStream(socket.getOutputStream()); + // 3、先发送上传文件的后缀给服务端 + dos.writeUTF(".png"); + // 4、把文件数据发送给服务端进行接收 + byte[] buffer = new byte[1024]; + int len; + while((len = is.read(buffer)) > 0 ){ + dos.write(buffer , 0 , len); + } + dos.flush(); + Thread.sleep(10000); + } +} + +public class Server { + public static void main(String[] args) { + ServerSocket ss = new ServerSocket(8888); + Socket socket = ss.accept(); + // 1、得到一个数据输入流读取客户端发送过来的数据 + DataInputStream dis = new DataInputStream(socket.getInputStream()); + // 2、读取客户端发送过来的文件类型 + String suffix = dis.readUTF(); + // 3、定义一个字节输出管道负责把客户端发来的文件数据写出去 + OutputStream os = new FileOutputStream("path"+ + UUID.randomUUID().toString()+suffix); + // 4、从数据输入流中读取文件数据,写出到字节输出流中去 + byte[] buffer = new byte[1024]; + int len; + while((len = dis.read(buffer)) > 0){ + os.write(buffer,0, len); + } + os.close(); + System.out.println("服务端接收文件保存成功!"); + } +} +``` + + + +*** + + + +## NIO + +### 基本介绍 + +**NIO的介绍**: + +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 可以做到用一个线程来处理多个操作的。假设有 1000 个请求过来,根据实际情况可以分配20 或者 80个线程来处理,不像之前的阻塞 IO 那样分配 1000 个 + +NIO 和 BIO 的比较: + +* BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多 + +* BIO 是阻塞的,NIO 则是非阻塞的 + +* BIO 基于字节流和字符流进行操作,而 NIO 基于 Channel 和 Buffer 进行操作,数据从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector 用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道 + + | NIO | BIO | + | ------------------------- | ------------------- | + | 面向缓冲区(Buffer) | 面向流(Stream) | + | 非阻塞(Non Blocking IO) | 阻塞IO(Blocking IO) | + | 选择器(Selectors) | | + + + +*** + + + +### NIO原理 + +NIO 三大核心部分:**Channel( 通道) ,Buffer( 缓冲区), Selector( 选择器)** + +* Buffer 缓冲区 + + 缓冲区本质是一块可以写入数据、读取数据的内存,**底层是一个数组**,这块内存被包装成NIO Buffer对象,并且提供了方法用来操作这块内存,相比较直接对数组的操作,Buffer 的 API 更加容易操作和管理 + +* Channel 通道 + + Java NIO 的通道类似流,不同的是既可以从通道中读取数据,又可以写数据到通道,流的读写通常是单向的,通道可以非阻塞读取和写入通道,支持读取或写入缓冲区,也支持异步地读写。 + +* Selector 选择器 + + Selector 是一个 Java NIO 组件,能够检查一个或多个 NIO 通道,并确定哪些通道已经准备好进行读取或写入,这样一个单独的线程可以管理多个channel,从而管理多个网络连接,提高效率 + +NIO的实现框架: + +![](https://gitee.com/seazean/images/raw/master/Java/NIO框架.png) + +* 每个 Channel 对应一个 Buffer +* 一个线程对应 Selector , 一个 Selector 对应多个 Channel(连接) +* 程序切换到哪个Channel 是由事件决定的,Event 是一个重要的概念 +* Selector 会根据不同的事件,在各个通道上切换 +* Buffer 是一个内存块 , 底层是一个数组 +* 数据的读取写入是通过 Buffer 完成的 , BIO 中要么是输入流,或者是输出流,不能双向,NIO 的 Buffer 是可以读也可以写, flip() 切换 Buffer 的工作模式 + +Java NIO 系统的核心在于:通道和缓冲区,通道表示打开到 IO 设备(例如:文件、 套接字)的连接。若需要使用 NIO 系统,获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区,然后操作缓冲区,对数据进行处理。简而言之,Channel 负责传输, Buffer 负责存取数据 + + + +*** + + + +### 缓冲区 + +#### 基本介绍 + +缓冲区(Buffer):缓冲区本质上是一个**可以读写数据的内存块**,用于特定基本数据类型的容器,用于与 NIO 通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的 + +![](https://gitee.com/seazean/images/raw/master/Java/NIO-Buffer.png) + +Buffer 底层是一个数组,可以保存多个相同类型的数据,根据数据类型不同 ,有以下 Buffer 常用子类:ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer + + + +*** + + + +#### 基本属性 + +* 容量 (capacity):作为一个内存块,Buffer 具有固定大小,缓冲区容量不能为负,并且创建后不能更改 + +* 限制 (limit):表示缓冲区中可以操作数据的大小(limit 后数据不能进行读写),缓冲区的限制不能为负,并且不能大于其容量。 **写入模式,限制等于 buffer 的容量;读取模式下,limit 等于写入的数据量** + +* 位置 (position):下一个要读取或写入的数据的索引,缓冲区的位置不能为负,并且不能大于其限制 + +* 标记 (mark)与重置 (reset):标记是一个索引,通过Buffer中的 mark() 方法指定 Buffer 中一个特定的位置,可以通过调用 reset() 方法恢复到这个 position + +* 位置、限制、容量遵守以下不变式: **0 <= position <= limit <= capacity** + + + + + +*** + + + +#### 常用API + +`static XxxBuffer allocate(int capacity)` : 创建一个容量为capacity 的 XxxBuffer 对象 + +Buffer 基本操作: + +| 方法 | 说明 | +| ------------------------------------------- | ------------------------------------------------------- | +| public Buffer clear() | 清空缓冲区,不清空内容,将位置设置为零,限制设置为容量 | +| public Buffer flip() | 翻转缓冲区,将缓冲区的界限设置为当前位置,position 置 0 | +| public int capacity() | 返回 Buffer的 capacity 大小 | +| public final int limit() | 返回 Buffer 的界限 limit 的位置 | +| public Buffer limit(int n) | 设置缓冲区界限为 n | +| public Buffer mark() | 在此位置对缓冲区设置标记 | +| public final int position() | 返回缓冲区的当前位置 position | +| public Buffer position(int n) | 设置缓冲区的当前位置为n | +| public Buffer reset() | 将位置 position 重置为先前 mark 标记的位置 | +| public Buffer rewind() | 将位置设为为0,取消设置的 mark | +| public final int remaining() | 返回当前位置 position 和 limit 之间的元素个数 | +| public final boolean hasRemaining() | 判断缓冲区中是否还有元素 | +| public static ByteBuffer wrap(byte[] array) | 将一个字节数组包装到缓冲区中 | +| abstract ByteBuffer asReadOnlyBuffer() | 创建一个新的只读字节缓冲区 | + +Buffer 数据操作: + +| 方法 | 说明 | +| ------------------------------------------------- | ----------------------------------------------- | +| public abstract byte get() | 读取该缓冲区当前位置的单个字节,然后增加位置 | +| public ByteBuffer get(byte[] dst) | 读取多个字节到字节数组dst中 | +| public abstract byte get(int index) | 读取指定索引位置的字节,不移动 position | +| public abstract ByteBuffer put(byte b) | 将给定单个字节写入缓冲区的当前位置,position+1 | +| public final ByteBuffer put(byte[] src) | 将 src 字节数组写入缓冲区的当前位置 | +| public abstract ByteBuffer put(int index, byte b) | 将指定字节写入缓冲区的索引位置,不移动 position | + +提示:"\n",占用两个字节 + + + +**** + + + +#### 读写数据 + +使用Buffer读写数据一般遵循以下四个步骤: + +* 写入数据到 Buffer +* 调用 flip()方法,转换为读取模式 +* 从 Buffer 中读取数据 +* 调用 buffer.clear() 方法清除缓冲区 + +```java +public class TestBuffer { + @Test + public void test(){ + String str = "seazean"; + //1. 分配一个指定大小的缓冲区 + ByteBuffer buffer = ByteBuffer.allocate(1024); + System.out.println("-----------------allocate()----------------"); + System.out.println(bufferf.position());//0 + System.out.println(buffer.limit());//1024 + System.out.println(buffer.capacity());//1024 + + //2. 利用 put() 存入数据到缓冲区中 + buffer.put(str.getBytes()); + System.out.println("-----------------put()----------------"); + System.out.println(bufferf.position());//7 + System.out.println(buffer.limit());//1024 + System.out.println(buffer.capacity());//1024 + + //3. 切换读取数据模式 + buffer.flip(); + System.out.println("-----------------flip()----------------"); + System.out.println(buffer.position());//0 + System.out.println(buffer.limit());//7 + System.out.println(buffer.capacity());//1024 + + //4. 利用 get() 读取缓冲区中的数据 + byte[] dst = new byte[buffer.limit()]; + buffer.get(dst); + System.out.println(dst.length); + System.out.println(new String(dst, 0, dst.length)); + System.out.println(buffer.position());//7 + System.out.println(buffer.limit());//7 + + //5. clear() : 清空缓冲区. 但是缓冲区中的数据依然存在,但是处于“被遗忘”状态 + System.out.println(buffer.hasRemaining());//true + buffer.clear(); + System.out.println(buffer.hasRemaining());//true + System.out.println("-----------------clear()----------------"); + System.out.println(buffer.position());//0 + System.out.println(buffer.limit());//1024 + System.out.println(buffer.capacity());//1024 + } +} +``` + + + +**** + + + +#### 直接内存 + +##### 源码分析 + +Byte Buffer 可以是两种类型,一种是基于直接内存(也就是非堆内存),另一种是非直接内存(也就是堆内存)。对于直接内存来说,JVM将会在IO操作上具有更高的性能,因为直接作用于本地系统的IO操作,而非直接内存,也就是堆内存中的数据,如果要作IO操作,会先从本进程内存复制到直接内存,再利用本地IO处理 + +直接内存创建 Buffer 对象:`static XxxBuffer allocateDirect(int capacity)` + +堆外内存不受 JVM GC 控制,可以使用堆外内存进行**通信**,防止 GC 后缓冲区位置发生变化的情况,源码: + +* SocketChannel#write(java.nio.ByteBuffer) → SocketChannelImpl#write(java.nio.ByteBuffer) + + ```java + public int write(ByteBuffer var1) throws IOException { + do { + var3 = IOUtil.write(this.fd, var1, -1L, nd); + } while(var3 == -3 && this.isOpen()); + } + ``` + +* IOUtil#write(java.io.FileDescriptor, java.nio.ByteBuffer, long, sun.nio.ch.NativeDispatcher) + + ```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 + ByteBuffer var8 = Util.getTemporaryDirectBuffer(var7); + var8.put(var1); + //... + //从堆外写到内核缓冲区 + int var9 = writeFromNativeBuffer(var0, var8, var2, var4); + } + } + ``` + +数据流的角度: + +* 非直接内存的作用链:本地IO → 直接内存 → 非直接内存 → 直接内存 → 本地IO +* 直接内存是:本地IO → 直接内存 → 本地IO + +JVM 直接内存图解: + + + + + + + +*** + + + +##### 分配回收 + +DirectByteBuffer 源码分析: + +```java +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 { + public void run() { + unsafe.freeMemory(address); + //... + } +} +``` + +分配和回收原理: + +* 使用了 Unsafe 对象的 allocateMemory 方法完成直接内存的分配,setMemory 方法完成赋值 +* ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦 ByteBuffer 对象被垃圾回收,那么 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 Deallocator 的 run方法,最后通过 freeMemory 来释放直接内存 + +```java +/** + * 直接内存分配的底层原理:Unsafe + */ +public class Demo1_27 { + static int _1Gb = 1024 * 1024 * 1024; + + public static void main(String[] args) throws IOException { + Unsafe unsafe = getUnsafe(); + // 分配内存 + long base = unsafe.allocateMemory(_1Gb); + unsafe.setMemory(base, _1Gb, (byte) 0); + System.in.read(); + // 释放内存 + unsafe.freeMemory(base); + System.in.read(); + } + + public static Unsafe getUnsafe() { + try { + Field f = Unsafe.class.getDeclaredField("theUnsafe"); + f.setAccessible(true); + Unsafe unsafe = (Unsafe) f.get(null); + return unsafe; + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } +} +``` + + + + + +**** + + + +#### 共享内存 + +FileChannel 提供 map 方法把文件映射到虚拟内存,通常情况可以映射整个文件,如果文件比较大,可以进行分段映射,完成映射后对物理内存的操作会被同步到硬盘上 + +FileChannel 中的成员属性: + +* MapMode.mode:内存映像文件访问的方式,共三种: + * `MapMode.READ_ONLY`:只读,试图修改得到的缓冲区将导致抛出异常。 + * `MapMode.READ_WRITE`:读/写,对得到的缓冲区的更改最终将写入文件;但该更改对映射到同一文件的其他程序不一定是可见的 + * `MapMode.PRIVATE`:私用,可读可写,但是修改的内容不会写入文件,只是 buffer 自身的改变,称之为 copy on write 写时复制 + +* `public final FileLock lock()`:获取此文件通道的排他锁 + +MappedByteBuffer,可以让文件直接在内存(堆外内存)中进行修改,这种方式叫做内存映射,可以直接调用系统底层的缓存,没有 JVM 和系统之间的复制操作,提高了传输效率,作用: + +* 用在进程间的通信,能达到**共享内存页**的作用,但在高并发下要对文件内存进行加锁,防止出现读写内容混乱和不一致性,Java 提供了文件锁 FileLock,但在父/子进程中锁定后另一进程会一直等待,效率不高 +* 读写那些太大而不能放进内存中的文件 + +MappedByteBuffer 较之 ByteBuffer新增的三个方法 + +- `final MappedByteBuffer force()`:缓冲区是 READ_WRITE 模式下,对缓冲区内容的修改强行写入文件 +- `final MappedByteBuffer load()`:将缓冲区的内容载入物理内存,并返回该缓冲区的引用 +- `final boolean isLoaded()`:如果缓冲区的内容在物理内存中,则返回真,否则返回假 + +```java +public class MappedByteBufferTest { + public static void main(String[] args) throws Exception { + RandomAccessFile ra = new RandomAccessFile("1.txt", "rw"); + //获取对应的通道 + FileChannel channel = ra.getChannel(); + + /** + * 参数1 FileChannel.MapMode.READ_WRITE 使用的读写模式 + * 参数2 0: 文件映射时的起始位置 + * 参数3 5: 是映射到内存的大小(不是索引位置),即将 1.txt 的多少个字节映射到内存 + * 可以直接修改的范围就是 0-5 + * 实际类型 DirectByteBuffer + */ + MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5); + + buffer.put(0, (byte) 'H'); + buffer.put(3, (byte) '9'); + buffer.put(5, (byte) 'Y'); //IndexOutOfBoundsException + + ra.close(); + System.out.println("修改成功~~"); + } +} +``` + +从硬盘上将文件读入内存,要经过文件系统进行数据拷贝,拷贝操作是由文件系统和硬件驱动实现。通过内存映射的方法访问硬盘上的文件,拷贝数据的效率要比 read 和 write 系统调用高: + +- read() 是系统调用,首先将文件从硬盘拷贝到内核空间的一个缓冲区,再将这些数据拷贝到用户空间,实际上进行了两次数据拷贝 +- mmap() 也是系统调用,但没有进行数据拷贝,当缺页中断发生时,直接将文件从硬盘拷贝到共享内存,只进行了一次数据拷贝 + +注意:mmap 的文件映射,在 Full GC 时才会进行释放,如果需要手动清除内存映射文件,可以反射调用sun.misc.Cleaner 方法 + + + +参考文章:https://www.jianshu.com/p/f90866dcbffc + + + +*** + + + +### 通道 + +#### 基本介绍 + +通道(Channel):表示 IO 源与目标打开的连接,Channel 类似于传统的流,只不过 Channel 本身不能直接访问数据,Channel 只能与 Buffer **进行交互** + +1. NIO 的通道类似于流,但有些区别如下: + * 通道可以同时进行读写,而流只能读或者只能写 + * 通道可以实现异步读写数据 + * 通道可以从缓冲读数据,也可以写数据到缓冲 + +2. BIO 中的 stream 是单向的,NIO中的 Channel 是双向的,可以读操作,也可以写操作 + +3. Channel 在 NIO 中是一个接口:`public interface Channel extends Closeable{}` + + + +Channel 实现类: + +* FileChannel:用于读取、写入、映射和操作文件的通道 +* DatagramChannel:通过 UDP 读写网络中的数据通道 +* SocketChannel:通过 TCP 读写网络中的数据 +* ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个SocketChannel。 + 提示:ServerSocketChanne 类似 ServerSocket , SocketChannel 类似 Socket + + + +*** + + + +#### 常用API + +获取 Channel 方式: + +* 对支持通道的对象调用 `getChannel()` 方法 +* 通过通道的静态方法 `open()` 打开并返回指定通道 +* 使用 Files 类的静态方法 `newByteChannel()` 获取字节通道 + +Channel 基本操作: + +| 方法 | 说明 | +| ------------------------------------------ | -------------------------------------------------------- | +| public abstract int read(ByteBuffer dst) | 从 Channel 中读取数据到 ByteBuffer,从 position 开始储存 | +| public final long read(ByteBuffer[] dsts) | 将Channel中的数据“分散”到ByteBuffer[] | +| public abstract int write(ByteBuffer src) | 将 ByteBuffer 中的数据写入 Channel,从 position 开始写出 | +| public final long write(ByteBuffer[] srcs) | 将ByteBuffer[]到中的数据“聚集”到Channel | +| public abstract long position() | 返回此通道的文件位置 | +| FileChannel position(long newPosition) | 设置此通道的文件位置 | +| public abstract long size() | 返回此通道的文件的当前大小 | + +**读写都是相对于内存来看,也就是缓冲区** + + + +**** + + + +#### 文件读写 + +```java +public class ChannelTest { + @Test + public void write() throws Exception{ + // 1、字节输出流通向目标文件 + FileOutputStream fos = new FileOutputStream("data01.txt"); + // 2、得到字节输出流对应的通道Channel + FileChannel channel = fos.getChannel(); + // 3、分配缓冲区 + ByteBuffer buffer = ByteBuffer.allocate(1024); + buffer.put("hello,黑马Java程序员!".getBytes()); + // 4、把缓冲区切换成写出模式 + buffer.flip(); + channel.write(buffer); + channel.close(); + System.out.println("写数据到文件中!"); + } + @Test + public void read() throws Exception { + // 1、定义一个文件字节输入流与源文件接通 + FileInputStream fis = new FileInputStream("data01.txt"); + // 2、需要得到文件字节输入流的文件通道 + FileChannel channel = fis.getChannel(); + // 3、定义一个缓冲区 + ByteBuffer buffer = ByteBuffer.allocate(1024); + // 4、读取数据到缓冲区 + channel.read(buffer); + buffer.flip(); + // 5、读取出缓冲区中的数据并输出即可 + String rs = new String(buffer.array(),0,buffer.remaining()); + System.out.println(rs); + } +} +``` + + + +*** + + + +#### 文件复制 + +Channel 的两个方法: + +* `abstract long transferFrom(ReadableByteChannel src, long position, long count)`:从给定的可读字节通道将字节传输到该通道的文件中 + * src:源通道 + * position:文件中要进行传输的位置,必须是非负的 + * count:要传输的最大字节数,必须是非负的 + +* `abstract long transferTo(long position, long count, WritableByteChannel target)`:将该通道文件的字节传输到给定的可写字节通道。 + * position:传输开始的文件中的位置; 必须是非负的 + * count:要传输的最大字节数; 必须是非负的 + * target:目标通道 + +文件复制的两种方式: + +1. Buffer +2. 使用上述两种方法 + +![](https://gitee.com/seazean/images/raw/master/Java/NIO-复制文件.png) + +```java +public class ChannelTest { + @Test + public void copy1() throws Exception { + File srcFile = new File("C:\\壁纸.jpg"); + File destFile = new File("C:\\Users\\壁纸new.jpg"); + // 得到一个字节字节输入流 + FileInputStream fis = new FileInputStream(srcFile); + // 得到一个字节输出流 + FileOutputStream fos = new FileOutputStream(destFile); + // 得到的是文件通道 + FileChannel isChannel = fis.getChannel(); + FileChannel osChannel = fos.getChannel(); + // 分配缓冲区 + ByteBuffer buffer = ByteBuffer.allocate(1024); + while(true){ + // 必须先清空缓冲然后再写入数据到缓冲区 + buffer.clear(); + // 开始读取一次数据 + int flag = isChannel.read(buffer); + if(flag == -1){ + break; + } + // 已经读取了数据 ,把缓冲区的模式切换成可读模式 + buffer.flip(); + // 把数据写出到 + osChannel.write(buffer); + } + isChannel.close(); + osChannel.close(); + System.out.println("复制完成!"); + } + + @Test + public void copy02() throws Exception { + // 1、字节输入管道 + FileInputStream fis = new FileInputStream("data01.txt"); + FileChannel isChannel = fis.getChannel(); + // 2、字节输出流管道 + FileOutputStream fos = new FileOutputStream("data03.txt"); + FileChannel osChannel = fos.getChannel(); + // 3、复制 + osChannel.transferFrom(isChannel,isChannel.position(),isChannel.size()); + isChannel.close(); + osChannel.close(); + } + + @Test + public void copy03() throws Exception { + // 1、字节输入管道 + FileInputStream fis = new FileInputStream("data01.txt"); + FileChannel isChannel = fis.getChannel(); + // 2、字节输出流管道 + FileOutputStream fos = new FileOutputStream("data04.txt"); + FileChannel osChannel = fos.getChannel(); + // 3、复制 + isChannel.transferTo(isChannel.position() , isChannel.size() , osChannel); + isChannel.close(); + osChannel.close(); + } +} +``` + + + +*** + + + +#### 分散聚集 + +分散读取(Scatter ):是指把Channel通道的数据读入到多个缓冲区中去 + +聚集写入(Gathering ):是指将多个 Buffer 中的数据“聚集”到 Channel。 + +```java +public class ChannelTest { + @Test + public void test() throws IOException{ + // 1、字节输入管道 + FileInputStream is = new FileInputStream("data01.txt"); + FileChannel isChannel = is.getChannel(); + // 2、字节输出流管道 + FileOutputStream fos = new FileOutputStream("data02.txt"); + FileChannel osChannel = fos.getChannel(); + // 3、定义多个缓冲区做数据分散 + ByteBuffer buffer1 = ByteBuffer.allocate(4); + ByteBuffer buffer2 = ByteBuffer.allocate(1024); + ByteBuffer[] buffers = {buffer1 , buffer2}; + // 4、从通道中读取数据分散到各个缓冲区 + isChannel.read(buffers); + // 5、从每个缓冲区中查询是否有数据读取到了 + for(ByteBuffer buffer : buffers){ + buffer.flip();// 切换到读数据模式 + System.out.println(new String(buffer.array() , 0 , buffer.remaining())); + } + // 6、聚集写入到通道 + osChannel.write(buffers); + isChannel.close(); + osChannel.close(); + System.out.println("文件复制~~"); + } +} +``` + + + +*** + + + +### 选择器 + +#### 基本介绍 + +选择器(Selector) 是 SelectableChannle 对象的**多路复用器**,Selector 可以同时监控多个通道的状况,利用 Selector 可使一个单独的线程管理多个 Channel。**Selector 是非阻塞 IO 的核心**。 + +![](https://gitee.com/seazean/images/raw/master/Java/NIO-Selector.png) + +* Selector 能够检测多个注册的通道上是否有事件发生(多个 Channel 以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理,就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求 +* 只有在连接/通道真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程 +* 避免了多线程之间的上下文切换导致的开销 + + + +*** + + + +#### 常用API + +创建 Selector:`Selector selector = Selector.open();` + +向选择器注册通道:`SelectableChannel.register(Selector sel, int ops)` + +选择器对通道的监听事件,需要通过第二个参数 ops 指定。监听的事件类型用四个常量表示: + +* 读 : SelectionKey.OP_READ (1) +* 写 : SelectionKey.OP_WRITE (4) +* 连接 : SelectionKey.OP_CONNECT (8) +* 接收 : SelectionKey.OP_ACCEPT (16) +* 若注册时不止监听一个事件,则可以使用“位或”操作符连接: + `int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE ` + + + +**Selector API**: + +| 方法 | 说明 | +| ------------------------------------------------ | ------------------------------------- | +| public static Selector open() | 打开选择器 | +| public abstract void close() | 关闭此选择器 | +| public abstract int select() | 阻塞选择一组通道准备好进行I/O操作的键 | +| public abstract int select(long timeout) | 阻塞等待 timeout 毫秒 | +| public abstract int selectNow() | 获取一下,不阻塞,立刻返回 | +| public abstract Selector wakeup() | 唤醒正在阻塞的 selector | +| public abstract Set selectedKeys() | 返回此选择器的选择键集 | + +SelectionKey API: + +| 方法 | 说明 | +| ------------------------------------------- | -------------------------------------------------- | +| public abstract void cancel() | 取消该键的通道与其选择器的注册 | +| public abstract SelectableChannel channel() | 返回创建此键的通道,该方法在取消键之后仍将返回通道 | +| public final boolean isAcceptable() | 检测此密钥的通道是否已准备好接受新的套接字连接 | +| public final boolean isConnectable() | 检测此密钥的通道是否已完成或未完成其套接字连接操作 | +| public final boolean isReadable() | 检测此密钥的频道是否可以阅读 | +| public final boolean isWritable() | 检测此密钥的通道是否准备好进行写入 | + +基本步骤: + +```java +//1.获取通道 +ServerSocketChannel ssChannel = ServerSocketChannel.open(); +//2.切换非阻塞模式 +ssChannel.configureBlocking(false); +//3.绑定连接 +ssChannel.bin(new InetSocketAddress(9999)); +//4.获取选择器 +Selector selector = Selector.open(); +//5.将通道注册到选择器上,并且指定“监听接收事件” +ssChannel.register(selector, SelectionKey.OP_ACCEPT); +``` + + + +*** + + + +### NIO实现 + +#### 常用API + +* SelectableChannel_API + + | 方法 | 说明 | + | ------------------------------------------------------------ | -------------------------------------------- | + | public final SelectableChannel configureBlocking(boolean block) | 设置此通道的阻塞模式 | + | public final SelectionKey register(Selector sel, int ops) | 向给定的选择器注册此通道,并选择关注的的事件 | + +* SocketChannel_API: + + | 方法 | 说明 | + | :------------------------------------------------------ | ------------------------------ | + | public static SocketChannel open() | 打开套接字通道 | + | public static SocketChannel open(SocketAddress remote) | 打开套接字通道并连接到远程地址 | + | public abstract boolean connect(SocketAddress remote) | 连接此通道的到远程地址 | + | public abstract SocketChannel bind(SocketAddress local) | 将通道的套接字绑定到本地地址 | + | public abstract SocketAddress getLocalAddress() | 返回套接字绑定的本地套接字地址 | + | public abstract SocketAddress getRemoteAddress() | 返回套接字连接的远程套接字地址 | + +* ServerSocketChannel_API: + + | 方法 | 说明 | + | ---------------------------------------------------------- | ------------------------------------------------------------ | + | public static ServerSocketChannel open() | 打开服务器套接字通道 | + | public final ServerSocketChannel bind(SocketAddress local) | 将通道的套接字绑定到本地地址,并配置套接字以监听连接 | + | public abstract SocketChannel accept() | 接受与此通道套接字的连接,通过此方法返回的套接字通道将处于阻塞模式 | + + * 如果 ServerSocketChannel 处于非阻塞模式,如果没有挂起连接,则此方法将立即返回 null + * 如果通道处于阻塞模式,如果没有挂起连接将无限期地阻塞,直到有新的连接或发生 I/O 错误 + + + +*** + + + +#### 代码实现 + +服务端 : + +1. 获取通道,当客户端连接服务端时,服务端会通过 `ServerSocketChannel.accept` 得到 SocketChannel + +2. 切换非阻塞模式 + +3. 绑定连接 + +4. 获取选择器 + +5. 将通道注册到选择器上, 并且指定“监听接收事件” + +6. 轮询式的获取选择器上已经“准备就绪”的事件 + +客户端: + +1. 获取通道:`SocketChannel sc = SocketChannel.open(new InetSocketAddress(HOST, PORT))` +2. 切换非阻塞模式 +3. 分配指定大小的缓冲区:`ByteBuffer buffer = ByteBuffer.allocate(1024)` +4. 发送数据给服务端 + +37行代码,如果判断条件改为 !=-1,需要客户端 shutdown 一下 + +```java +public class Server { + public static void main(String[] args){ + // 1、获取通道 + ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); + // 2、切换为非阻塞模式 + serverSocketChannel.configureBlocking(false); + // 3、绑定连接的端口 + serverSocketChannel.bind(new InetSocketAddress(9999)); + // 4、获取选择器Selector + Selector selector = Selector.open(); + // 5、将通道都注册到选择器上去,并且开始指定监听接收事件 + serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); + // 6、使用Selector选择器轮询已经就绪好的事件 + while (selector.select() > 0) { + System.out.println("----开始新一轮的时间处理----"); + // 7、获取选择器中的所有注册的通道中已经就绪好的事件 + Set selectionKeys = selector.selectedKeys(); + Iterator it = selectionKeys.iterator(); + // 8、开始遍历这些准备好的事件 + while (it.hasNext()) { + SelectionKey key = it.next();// 提取当前这个事件 + // 9、判断这个事件具体是什么 + if (key.isAcceptable()) { + // 10、直接获取当前接入的客户端通道 + SocketChannel socketChannel = serverSocketChannel.accept(); + // 11 、切换成非阻塞模式 + socketChannel.configureBlocking(false); + // 12、将本客户端通道注册到选择器 + socketChannel.register(selector, SelectionKey.OP_READ); + } else if (key.isReadable()) { + // 13、获取当前选择器上的读就绪事件 + SelectableChannel channel = key.channel(); + SocketChannel socketChannel = (SocketChannel) channel; + // 14、读取数据 + ByteBuffer buffer = ByteBuffer.allocate(1024); + int len; + while ((len = socketChannel.read(buffer)) > 0) { + buffer.flip(); + System.out.println(socketChannel.getRemoteAddress() + ":" + new String(buffer.array(), 0, len)); + buffer.clear();// 清除之前的数据 + } + } + //删除当前的 selectionKey,防止重复操作 + it.remove(); + } + } + } +} +``` + +```java +public class Client { + public static void main(String[] args) throws Exception { + // 1、获取通道 + SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999)); + // 2、切换成非阻塞模式 + socketChannel.configureBlocking(false); + // 3、分配指定缓冲区大小 + ByteBuffer buffer = ByteBuffer.allocate(1024); + // 4、发送数据给服务端 + Scanner sc = new Scanner(System.in); + while (true){ + System.out.print("请说:"); + String msg = sc.nextLine(); + buffer.put(("波妞:" + msg).getBytes()); + buffer.flip(); + socketChannel.write(buffer); + buffer.clear(); + } + } +} +``` + + + +*** + + + +## AIO + +Java AIO(NIO.2) : AsynchronousI/O,异步非阻塞,采用了 Proactor 模式。服务器实现模式为一个有效请求一个线程,客户端的 I/O 请求都是由 OS 先完成了再通知服务器应用去启动线程进行处理。 + +```java +AIO异步非阻塞,基于NIO的,可以称之为NIO2.0 + BIO NIO AIO +Socket SocketChannel AsynchronousSocketChannel +ServerSocket ServerSocketChannel AsynchronousServerSocketChannel +``` + +当进行读写操作时,调用 API 的 read 或 write 方法,这两种方法均为异步的,完成后会主动调用回调函数: + +* 对于读操作,当有流可读取时,操作系统会将可读的流传入 read 方法的缓冲区 +* 对于写操作,当操作系统将 write 方法传递的流写入完毕时,操作系统主动通知应用程序 + +在JDK1.7中,这部分内容被称作NIO.2,主要在 Java.nio.channels 包下增加了下面四个异步通道: +AsynchronousSocketChannel、AsynchronousServerSocketChannel、AsynchronousFileChannel、AsynchronousDatagramChannel + From ed5c67f712aabc91fff2800dc171c7f734981a04 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 4 Jul 2021 18:44:13 +0800 Subject: [PATCH 065/242] Update README.md --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1a12481..70875a2 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,16 @@ * DB:MySQL、Redis * Issue:Interview Questions -* Java:JavaSE、JVM、JUC、Design Pattern +* Java:JavaSE、JVM、Algorithm、Design Pattern +* Prog:JUC、NET * SSM:MyBatis、Spring、SpringMVC、SpringBoot * Tool:Git、Linux、Docker * Web:HTML、CSS、HTTP、Servlet、JavaScript 其他说明: +* 推荐使用 Typora 阅读笔记,阅读效果更佳。 * 所有的知识不保证权威性,如果各位朋友发现错误,非常欢迎与我讨论。 -* Java.md 更新后大于 1M,导致网页无法显示,推荐大家使用 Typora 阅读笔记。 -* 如果使用 Typora 阅读笔记出现卡顿,可以使用 VS Code 或转成 PDF 文件。 +* Java.md 更新后大于 1M,导致网页无法显示,所以分割为 Java 和 Program 两个文档。 个人邮箱:imseazean@gmail.com From 0b882af50547ebb9e13140430c9fa6253419846a Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 4 Jul 2021 19:07:23 +0800 Subject: [PATCH 066/242] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 70875a2..47a94a7 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ 其他说明: -* 推荐使用 Typora 阅读笔记,阅读效果更佳。 +* 推荐使用 Typora 阅读笔记,观看效果更佳。 * 所有的知识不保证权威性,如果各位朋友发现错误,非常欢迎与我讨论。 -* Java.md 更新后大于 1M,导致网页无法显示,所以分割为 Java 和 Program 两个文档。 +* Java.md 更新后大于 1M,导致网页无法显示,所以划分为 Java 和 Program 两个文档。 个人邮箱:imseazean@gmail.com From c425c6070226944ef98ad8608575c8c1f6cebc03 Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 5 Jul 2021 18:11:05 +0800 Subject: [PATCH 067/242] Update Java Notes --- Java.md | 740 +++++++++++++++++++++++++++++++++++--------------------- Prog.md | 69 ++++-- 2 files changed, 506 insertions(+), 303 deletions(-) diff --git a/Java.md b/Java.md index 01ac9de..9737806 100644 --- a/Java.md +++ b/Java.md @@ -27,7 +27,7 @@ ##### 基本类型 -Java语言提供了八种基本类型。六种数字类型(四个整数型,两个浮点型),一种字符类型,还有一种布尔型 +Java 语言提供了八种基本类型。六种数字类型(四个整数型,两个浮点型),一种字符类型,还有一种布尔型 **byte:** @@ -1188,15 +1188,14 @@ static 静态修饰的成员(方法和成员变量)属于类本身的。 访问问题: -​ a.实例方法是否可以直接访问实例成员变量?可以的,因为它们都属于对象。 -​ b.实例方法是否可以直接访问静态成员变量?可以的,静态成员变量可以被共享访问。 -​ c.实例方法是否可以直接访问实例方法? 可以的,实例方法和实例方法都属于对象。 -​ d.实例方法是否可以直接访问静态方法?可以的,静态方法可以被共享访问! - -​ a.静态方法是否可以直接访问实例变量? 不可以的,实例变量必须用对象访问!! -​ b.静态方法是否可以直接访问静态变量? 可以的,静态成员变量可以被共享访问。 -​ c.静态方法是否可以直接访问实例方法? 不可以的,实例方法必须用对象访问!! -​ d.静态方法是否可以直接访问静态方法?可以的,静态方法可以被共享访问!! +* 实例方法是否可以直接访问实例成员变量?可以的,因为它们都属于对象 +* 实例方法是否可以直接访问静态成员变量?可以的,静态成员变量可以被共享访问 +* 实例方法是否可以直接访问实例方法? 可以的,实例方法和实例方法都属于对象 +* 实例方法是否可以直接访问静态方法?可以的,静态方法可以被共享访问 +* 静态方法是否可以直接访问实例变量? 不可以的,实例变量必须用对象访问!! +* 静态方法是否可以直接访问静态变量? 可以的,静态成员变量可以被共享访问。 +* 静态方法是否可以直接访问实例方法? 不可以的,实例方法必须用对象访问!! +* 静态方法是否可以直接访问静态方法?可以的,静态方法可以被共享访问!! @@ -1208,7 +1207,7 @@ static 静态修饰的成员(方法和成员变量)属于类本身的。 #### 基本介绍 -继承是Java中一般到特殊的关系,是一种子类到父类的关系。 +继承是 Java 中一般到特殊的关系,是一种子类到父类的关系 * 被继承的类称为:父类/超类。 * 继承父类的类称为:子类。 @@ -1225,7 +1224,7 @@ static 静态修饰的成员(方法和成员变量)属于类本身的。 2. **单继承**:一个类只能继承一个直接父类 3. 多层继承:一个类可以间接继承多个父类(家谱) 4. 一个类可以有多个子类 -5. 一个类要么默认继承了Object类,要么间接继承了Object类,Object类是Java中的祖宗类 +5. 一个类要么默认继承了 Object 类,要么间接继承了 Object 类,Object 类是 Java 中的祖宗类 继承的格式: @@ -1267,7 +1266,7 @@ class Animal{ #### 继承访问 -继承后成员变量的访问特点:**就近原则**,子类有找子类,子类没有找父类,父类没有就报错! +继承后成员变量的访问特点:**就近原则**,子类有找子类,子类没有找父类,父类没有就报错 如果要申明访问父类的成员变量可以使用:super.父类成员变量。super指父类引用 @@ -1309,7 +1308,7 @@ class Animal{ 方法重写的校验注解:@Override * 方法加了这个注解,那就必须是成功重写父类的方法,否则报错 -* @Override优势:可读性好,安全,优雅 +* @Override 优势:可读性好,安全,优雅 子类可以扩展父类的功能,但不能改变父类原有的功能,重写有以下**三个限制**: @@ -1348,7 +1347,7 @@ class Animal{ * 为什么子类构造器会先调用父类构造器? - 1. 子类的构造器的第一行默认super()调用父类的无参数构造器,写不写都存在 + 1. 子类的构造器的第一行默认 super() 调用父类的无参数构造器,写不写都存在 2. 子类继承父类,子类就得到了父类的属性和行为。调用子类构造器初始化子类对象数据时,必须先调用父类构造器初始化继承自父类的属性和行为 3. 参考JVM -> 类加载 -> 对象创建 @@ -1370,12 +1369,9 @@ class Animal{ } ``` +* **为什么 Java 是单继承的?** + 答:反证法,假如 Java 可以多继承,请看如下代码: - -* **为什么Java是单继承的?** - 答:反证法,假如Java可以多继承,请看如下代码: - 补充:多实现是在实现接口时,重名方法需要实现类来实现 - ```java class A{ public void test(){ @@ -1395,7 +1391,7 @@ class Animal{ } } ``` - + @@ -1406,19 +1402,19 @@ class Animal{ ### super -继承后super调用父类构造器,父类构造器初始化继承自父类的数据。 +继承后 super 调用父类构造器,父类构造器初始化继承自父类的数据。 总结与拓展: -* this代表了当前对象的引用(继承中指代子类对象):this.子类成员变量、this.子类成员方法、**this(...)**可以根据参数匹配访问本类其他构造器。 -* super代表了父类对象的引用(继承中指代了父类对象空间):super.父类成员变量、super.父类的成员方法、super(...)可以根据参数匹配访问父类的构造器 +* this 代表了当前对象的引用(继承中指代子类对象):this.子类成员变量、this.子类成员方法、**this(...)**可以根据参数匹配访问本类其他构造器。 +* super 代表了父类对象的引用(继承中指代了父类对象空间):super.父类成员变量、super.父类的成员方法、super(...)可以根据参数匹配访问父类的构造器 **注意:** -* this(...)借用本类其他构造器,super(...)调用父类的构造器。 -* this(...)或super(...)必须放在构造器的第一行,否则报错! -* this(...)和super(...)不能同时出现在构造器中,因为构造函数必须出现在第一行上,只能选择一个。 +* this(...) 借用本类其他构造器,super(...) 调用父类的构造器。 +* this(...) 或 super(...) 必须放在构造器的第一行,否则报错! +* this(...) 和 super(...) 不能同时出现在构造器中,因为构造函数必须出现在第一行上,只能选择一个。 ```java public class ThisDemo { @@ -1465,14 +1461,13 @@ class Student{ #### 基本介绍 -final用于修饰:类,方法,变量 +final 用于修饰:类,方法,变量 -* final修饰类,类不能被继承了,类中的方法和变量可以使用 -* final可以修饰方法,方法就不能被重写 -* final修饰变量总规则:变量有且仅能被赋值一次 +* final 修饰类,类不能被继承了,类中的方法和变量可以使用 +* final 可以修饰方法,方法就不能被重写 +* final 修饰变量总规则:变量有且仅能被赋值一次 -**面试题**:final和abstract的关系? - 互斥关系,不能同时修饰类或者同时修饰方法!! +**面试题**:final 和 abstract 的关系是互斥关系,不能同时修饰类或者同时修饰方法! @@ -1482,13 +1477,13 @@ final用于修饰:类,方法,变量 #### 修饰变量 -##### 静态成员变量 +##### 静态变量 -final修饰静态成员变量,变量变成了常量 +final 修饰静态成员变量,变量变成了常量 -**常量:有public static final修饰,名称字母全部大写,多个单词用下划线连接。** +**常量:有 public static final 修饰,名称字母全部大写,多个单词用下划线连接。** -final修饰静态成员变量可以在哪些地方赋值: +final 修饰静态成员变量可以在哪些地方赋值: 1. 定义的时候赋值一次 @@ -1510,11 +1505,11 @@ public class FinalDemo { -##### 实例成员变量 +##### 实例变量 -final修饰变量的总规则:有且仅能被赋值一次 +final 修饰变量的总规则:有且仅能被赋值一次 -final修饰实例成员变量可以在哪些地方赋值1次: +final 修饰实例成员变量可以在哪些地方赋值 1 次: 1. 定义的时候赋值一次 2. 可以在实例代码块中赋值一次 @@ -1589,11 +1584,11 @@ abstract class Animal{ #### 面试问题 -一、抽象类是否有构造器,是否可以创建对象,为什么? +一、抽象类是否有构造器,是否可以创建对象? 答:抽象类作为类一定有构造器,而且必须有构造器,提供给子类继承后调用父类构造器使用的 -1、抽象类有构造器,但是抽象类不能创建对象,类的其他成分它都具备 -2、抽象类中存在抽象方法,但不能执行,**抽象类中也可没有抽象方法** +* 抽象类有构造器,但是抽象类不能创建对象,类的其他成分它都具备 +* 抽象类中存在抽象方法,但不能执行,**抽象类中也可没有抽象方法** > 抽象在学术上本身意味着不能实例化 @@ -1615,10 +1610,8 @@ abstract class Animal{ } ``` - - -二、static与abstract能同时使用吗? -答:不能,被static修饰的方法属于类,是类自己的东西,不是给子类来继承的,而抽象方法本身没有实现,就是用来给子类继承 +二、static 与 abstract 能同时使用吗? +答:不能,被 static 修饰的方法属于类,是类自己的东西,不是给子类来继承的,而抽象方法本身没有实现,就是用来给子类继承 @@ -1628,8 +1621,9 @@ abstract class Animal{ #### 存在意义 - * **被继承**,抽象类就是为了被子类继承,否则抽象类将毫无意义。(核心) - * 抽象类体现的是"模板思想":**部分实现,部分抽象**。 可以使用抽象类设计一个模板模式。 +**被继承**,抽象类就是为了被子类继承,否则抽象类将毫无意义(核心) + +抽象类体现的是"模板思想":**部分实现,部分抽象**,可以使用抽象类设计一个模板模式 ```java //作文模板 @@ -1668,11 +1662,9 @@ abstract class Template{ #### 基本介绍 -接口,是Java语言中一种引用类型,是方法的集合。 - -接口是更加彻底的抽象,接口中只有抽象方法和常量,没有其他成分,jdk1.8前 +接口,是 Java 语言中一种引用类型,是方法的集合。 -> 接口称为 被实现,实现接口的类称为**实现类** +接口是更加彻底的抽象,接口中只有抽象方法和常量,没有其他成分,jdk1.8 前 ```java 修饰符 interface 接口名称{ @@ -1768,18 +1760,18 @@ abstract class Template{ -#### JDK8以后 +#### 新增功能 -jdk1.8以后新增的功能,实际开发中很少使用 +jdk1.8 以后新增的功能: -* 默认方法(就是之前写的普通实例方法) - * 必须用default修饰,默认会public修饰 +* 默认方法(就是普通实例方法) + * 必须用 default 修饰,默认会 public 修饰 * 必须用接口的实现类的对象来调用 * 静态方法 - * 默认会public修饰 + * 默认会 public 修饰 * 接口的静态方法必须用接口的类名本身来调用 * 调用格式:ClassName.method() -* 私有方法:JDK 1.9才开始有的,只能在**本类中**被其他的默认方法或者私有方法访问 +* 私有方法:JDK 1.9 才开始有的,只能在**本类中**被其他的默认方法或者私有方法访问 ```java public class InterfaceDemo { @@ -2151,7 +2143,10 @@ abstract class Animal{ | 其他包下的子类中 | X | X | √ | √ | | 其他包下的其他类中 | X | X | X | √ | -protected 用于修饰成员,表示在继承体系中成员对于子类可见,子类需要重写方法才可以调用 +protected 用于修饰成员,表示在继承体系中成员对于子类可见 + +* 基类的 protected 成员是包内可见的,并且对子类可见 +* 若子类与基类不在同一包中,那么子类实例可以访问其从基类继承而来的 protected 方法(重写),而不能访问基类实例的 protected 方法 @@ -2454,8 +2449,8 @@ s = s + "cd"; //s = abccd 新对象 `String str = new String("abc")`创建字符串对象: -* 创建一个对象:字符串池中已经存在"abc"对象,那么直接在创建一个对象放入堆中,返回str引用 -* 创建两个对象:字符串池中未找到"abc"对象,那么分别在堆中和字符串池中创建一个对象,字符串池中的比较都是采用equals()方法 +* 创建一个对象:字符串池中已经存在"abc"对象,那么直接在创建一个对象放入堆中,返回堆内引用 +* 创建两个对象:字符串池中未找到"abc"对象,那么分别在堆中和字符串池中创建一个对象,字符串池中的比较都是采用 equals() 方法 `new String("a") + new String("b")`创建字符串对象: @@ -2524,7 +2519,7 @@ s.replace("-","");//12378 * StringTable,hashtable (哈希表 + 链表)结构,不能扩容,默认值大小长度是1009 * 常量池中的字符串仅是符号,第一次使用时才变为对象,可以避免重复创建字符串对象 -* 字符串**变量**的拼接的原理是StringBuilder(jdk1.8),append效率要比字符串拼接高很多 +* 字符串**变量**的拼接的原理是StringBuilder(jdk1.8),append 效率要比字符串拼接高很多 * 字符串**常量**拼接的原理是编译期优化,结果在常量池 * 可以使用 String 的 intern() 方法在运行过程将字符串添加到 String Pool 中 @@ -2532,7 +2527,7 @@ s.replace("-","");//12378 * jdk1.8:当一个字符串调用 intern() 方法时,如果 String Pool 中: * 存在一个字符串和该字符串值相等(使用 equals() 方法进行确定),就会返回 String Pool 中字符串的引用(需要变量接收) - * 不存在,会把对象的引用地址复制一份放入串池,并返回串池中的引用地址,前提是堆内存有该对象。因为 Pool 在堆中,为了节省内存不再创建新对象 + * 不存在,会把对象的**引用地址**复制一份放入串池,并返回串池中的引用地址,前提是堆内存有该对象,因为 Pool 在堆中,为了节省内存不再创建新对象 * jdk1.6:将这个字符串对象尝试放入串池,如果有就不放入,返回已有的串池中的对象的地址;如果没有会把此对象复制一份,放入串池,把串池中的对象返回 ```java @@ -2546,7 +2541,7 @@ public class Demo { String s2 = "b"; String s3 = "ab";//串池 // new StringBuilder().append("a").append("b").toString() new String("ab") - String s4 = s1 + s2; //d + String s4 = s1 + s2; // 堆内地址 String s5 = "a" + "b"; // javac 在编译期间的优化,结果已经在编译期确定为ab System.out.println(s3 == s4); // false @@ -3039,8 +3034,8 @@ public class JDK8DateDemo9 { ### Math -Math用于做数学运算。 -Math类中的方法全部是静态方法,直接用类名调用即可。 +Math 用于做数学运算 +Math 类中的方法全部是静态方法,直接用类名调用即可 方法: | 方法 | 说明 | @@ -3084,29 +3079,29 @@ public class MathDemo { ```java public static void main(String[]args){ -    double pi = 3.1415927; //圆周率 -    //取一位整数 -    System.out.println(new DecimalFormat("0").format(pi));   //3 -    //取一位整数和两位小数 -    System.out.println(new DecimalFormat("0.00").format(pi)); //3.14 -    //取两位整数和三位小数,整数不足部分以0填补。 -    System.out.println(new DecimalFormat("00.000").format(pi));// 03.142 -    //取所有整数部分 -    System.out.println(new DecimalFormat("#").format(pi));   //3 -    //以百分比方式计数,并取两位小数 -    System.out.println(new DecimalFormat("#.##%").format(pi)); //314.16% - -     long c =299792458;  //光速 -    //显示为科学计数法,并取五位小数 -    System.out.println(new DecimalFormat("#.#####E0").format(c));//2.99792E8 -    //显示为两位整数的科学计数法,并取四位小数 -    System.out.println(new DecimalFormat("00.####E0").format(c));//29.9792E7 -    //每三位以逗号进行分隔。 -    System.out.println(new DecimalFormat(",###").format(c));//299,792,458 -    //将格式嵌入文本 -    System.out.println(new DecimalFormat("光速大小为每秒,###米。").format(c)); - -  } + double pi = 3.1415927; //圆周率 + //取一位整数 + System.out.println(new DecimalFormat("0").format(pi));   //3 + //取一位整数和两位小数 + System.out.println(new DecimalFormat("0.00").format(pi)); //3.14 + //取两位整数和三位小数,整数不足部分以0填补。 + System.out.println(new DecimalFormat("00.000").format(pi));// 03.142 + //取所有整数部分 + System.out.println(new DecimalFormat("#").format(pi));   //3 + //以百分比方式计数,并取两位小数 + System.out.println(new DecimalFormat("#.##%").format(pi)); //314.16% + + long c =299792458;  //光速 + //显示为科学计数法,并取五位小数 + System.out.println(new DecimalFormat("#.#####E0").format(c));//2.99792E8 + //显示为两位整数的科学计数法,并取四位小数 + System.out.println(new DecimalFormat("00.####E0").format(c));//29.9792E7 + //每三位以逗号进行分隔。 + System.out.println(new DecimalFormat(",###").format(c));//299,792,458 + //将格式嵌入文本 + System.out.println(new DecimalFormat("光速大小为每秒,###米。").format(c)); + +} ``` @@ -3118,6 +3113,7 @@ public static void main(String[]args){ ### System System代表当前系统。 + 静态方法: 1. `public static void exit(int status)` : 终止JVM虚拟机,非0是异常终止 @@ -3154,7 +3150,7 @@ public class SystemDemo { ### BigDecimal -Java在java.math包中提供的API类BigDecimal,用来对超过16位有效位的数进行精确的运算 +Java 在 java.math 包中提供的 API 类,用来对超过16位有效位的数进行精确的运算 构造方法: `public static BigDecimal valueOf(double val)` : 包装浮点数成为大数据对象。 @@ -9773,11 +9769,11 @@ public static void main(String[] args) { ### 本地内存 -#### 本地内存 +#### 基本介绍 -虚拟机内存:Java虚拟机在执行的时候会把管理的内存分配成不同的区域,受虚拟机内存大小的参数控制,当大小超过参数设置的大小时就会报OOM +虚拟机内存:Java 虚拟机在执行的时候会把管理的内存分配成不同的区域,受虚拟机内存大小的参数控制,当大小超过参数设置的大小时就会报 OOM -本地内存:又叫做**堆外内存**,线程共享的区域,本地内存这块区域是不会受到JVM的控制的,不会发生GC;因此对于整个java的执行效率是提升非常大,但是如果内存的占用超出物理内存的大小,同样也会报OOM +本地内存:又叫做**堆外内存**,线程共享的区域,本地内存这块区域是不会受到 JVM 的控制的,不会发生 GC;因此对于整个 java 的执行效率是提升非常大,但是如果内存的占用超出物理内存的大小,同样也会报 OOM 本地内存概述图: @@ -9844,26 +9840,9 @@ public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制 直接内存是 Java 堆外的、直接向系统申请的内存区间,不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域 -Direct Memory 优点: - -* Java 的 NIO 库允许 Java 程序使用直接内存,用于数据缓冲区,使用 native 函数直接分配堆外内存 -* 读写性能高,读写频繁的场合可能会考虑使用直接内存 -* 大大提高 IO 性能,避免了在 Java 堆和 native 堆来回复制数据 - -直接内存缺点: - -* 分配回收成本较高,不受 JVM 内存回收管理 -* 可能导致 OutOfMemoryError 异常:OutOfMemoryError: Direct buffer memory -* 回收依赖 System.gc() 的调用,但这个调用 JVM 不保证执行、也不保证何时执行,行为是不可控的。程序一般需要自行管理,成对去调用 malloc、free - -应用场景: - -- 有很大的数据需要存储,数据的生命周期很长 -- 适合频繁的 IO 操作,比如网络并发场景 - -直接内存机制参考:NET → NIO → 缓冲区 → 直接内存 +直接内存详解参考:NET → NIO → 直接内存 @@ -10330,10 +10309,16 @@ GC Roots说明: + + 参考文章:https://www.jianshu.com/p/12544c0ad5c1 +**** + + + ###### 并发标记 并发标记时,对象间的引用可能发生变化**,**多标和漏标的情况就有可能发生 @@ -10983,8 +10968,8 @@ ZGC 收集器是一个可伸缩的、低延迟的垃圾收集器,基于 Region * 在 CMS 和 G1 中都用到了写屏障,而 ZGC 用到了读屏障 * 染色指针:直接将少量额外的信息存储在指针上的技术,从 64 位的指针中拿高 4 位来标识对象此时的状态 - * 染色指针可以使得一旦某个 Region 的存活对象被移走之后,这个 Region 立即就能够被释放和重用掉 - * 可以直接从指针中看到引用对象的三色标记状态(Marked0、Marked1)、是否进入了重分配集(是否被移动过 Remapped)、是否只能通过 finalize() 方法才能被访问到(Finalizable) + * 染色指针可以使某个 Region 的存活对象被移走之后,这个 Region 立即就能够被释放和重用 + * 可以直接从指针中看到引用对象的三色标记状态(Marked0、Marked1)、是否进入了重分配集、是否被移动过 Remapped、是否只能通过 finalize() 方法才能被访问到(Finalizable) * 可以大幅减少在垃圾收集过程中内存屏障的使用数量,写屏障的目的通常是为了记录对象引用的变动情况,如果将这些信息直接维护在指针中,显然就可以省去一些专门的记录操作 * 可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据 * 内存多重映射:多个虚拟地址指向同一个物理地址 @@ -11001,8 +10986,8 @@ ZGC 的工作过程可以分为 4 个阶段: * 并发标记(Concurrent Mark): 遍历对象图做可达性分析的阶段,也要经过类似于 G1的初始标记、最终标记的短暂停顿 * 并发预备重分配( Concurrent Prepare for Relocate): 根据特定的查询条件统计得出本次收集过程要清理哪些 Region,将这些 Region 组成重分配集(Relocation Set) -* 并发重分配(Concurrent Relocate): 重分配是 ZGC 执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的 Region 上,并为重分配集中的每个 Region 维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系 -* 并发重映射(Concurrent Remap): 修正整个堆中指向重分配集中旧对象的所有引用,ZGC 的并发映射并不是一个必须要立即完成的任务,ZGC 很巧妙地把并发重映射阶段要做的工作,合并到下一次垃圾收集循环中的并发标记阶段里去完成,因为都是要遍历所有对象的,这样合并节省了一次遍历的开销 +* 并发重分配(Concurrent Relocate): 重分配是 ZGC 执行过程中的核心阶段,这个过程要把重分配集中的**存活对象**复制到新的 Region 上,并为重分配集中的每个 Region 维护一个转发表(Forward Table),记录从旧地址到新地址的转向关系 +* 并发重映射(Concurrent Remap):修正整个堆中指向重分配集中旧对象的所有引用,ZGC 的并发映射并不是一个必须要立即完成的任务,ZGC 很巧妙地把并发重映射阶段要做的工作,合并到下一次垃圾收集循环中的并发标记阶段里去完成,因为都是要遍历所有对象的,这样合并节省了一次遍历的开销 ZGC 几乎在所有地方并发执行的,除了初始标记的是 STW 的,但这部分的实际时间是非常少的,所以响应速度快,在尽可能对吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟 @@ -11063,7 +11048,7 @@ Serial GC、Parallel GC、Concurrent Mark Sweep GC这三个GC不同: #### 基本构造 -一个Java对象内存中存储为三部分:对象头 (Header)、实例数据 (Instance Data) 和对齐填充 (Padding) +一个 Java 对象内存中存储为三部分:对象头(Header)、实例数据(Instance Data)和对齐填充 (Padding) 对象头: @@ -11076,7 +11061,7 @@ Serial GC、Parallel GC、Concurrent Mark Sweep GC这三个GC不同: unused(25+1) + hash(31) + age(4) + lock(3) = 64bit #64位系统 ``` - * Klass Word:类型指针,对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;在64位系统中,开启指针压缩 (-XX:+UseCompressedOops) 或者 JVM 堆的最大值小于 32G,这个指针也是 4byte,否则是 8byte + * Klass Word:类型指针,对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;在64位系统中,开启指针压缩(-XX:+UseCompressedOops)或者 JVM 堆的最大值小于 32G,这个指针也是 4byte,否则是 8byte ```ruby |-----------------------------------------------------| @@ -11132,7 +11117,7 @@ Serial GC、Parallel GC、Concurrent Mark Sweep GC这三个GC不同: * 尽量用数组,少用集合,数组中是可以使用基本类型的,但是集合中只能放包装类型,如果需要使用集合,推荐比较节约内存的集合工具:fastutil - 一个ArrayList集合,如果里面放了10个数字,占用多少内存: + 一个 ArrayList 集合,如果里面放了 10 个数字,占用多少内存: ```java private transient Object[] elementData; @@ -11154,8 +11139,8 @@ Serial GC、Parallel GC、Concurrent Mark Sweep GC这三个GC不同: JVM是通过栈帧中的对象引用访问到其内部的对象实例: * 句柄访问 - 使用该方式,Java堆中会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息 - 优点:reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。 + 使用该方式,Java 堆中会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息 + 优点:reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集)时只会改变句柄中的实例数据指针,而 reference 本身不需要被修改。 @@ -11368,15 +11353,6 @@ Java对象创建时机: * 使用(Using) * 卸载(Unloading) -类加载方式: - -* 隐式加载: - * 创建类对象、使用类的静态域、创建子类对象、使用子类的静态域 - * 在 JVM 启动时,通过三大类加载器加载 class -* 显式加载: - * ClassLoader.loadClass(className),只加载和连接,**不会进行初始化** - * Class.forName(String name, boolean initialize,ClassLoader loader),使用 loader 进行加载和连接,根据参数 initialize 决定是否初始化 - *** @@ -11390,17 +11366,19 @@ Java对象创建时机: 加载过程完成以下三件事: - 通过类的完全限定名称获取定义该类的二进制字节流(二进制字节码) -- 将该字节流表示的**静态存储结构转换为方法区的运行时存储结构**(运行时常量池) +- 将该字节流表示的**静态存储结构转换为方法区的运行时存储结构**(Java 类模型) - 在内存中生成一个代表该类的 Class 对象,作为方法区中该类各种数据的访问入口 其中二进制字节流可以从以下方式中获取: - 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础 - 从网络中获取,最典型的应用是 Applet -- 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass - 由其他文件生成,例如由 JSP 文件生成对应的 Class 类 +- 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass + +将字节码文件加载至元空间后,会**在堆中**创建一个 java.lang.Class 对象,用来封装类位于方法区内的数据结构,该 Class 对象是在加载类的过程中创建的,每个类都对应有一个 Class 类型的对象 -将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,重要 field: +方法区内部采用 C++ 的 instanceKlass 描述 java 类的数据结构: * `_java_mirror` 即 java 的类镜像,例如对 String 来说就是 String.class,作用是把 class 暴露给 java 使用 * `_super` 即父类、`_fields` 即成员变量、`_methods` 即方法、`_constants` 即常量池、`_class_loader` 即类加载器、`_vtable` **虚方法表**、`_itable` 接口方法表 @@ -11409,10 +11387,16 @@ Java对象创建时机: * 如果这个类还有父类没有加载,先加载父类 * 加载和链接可能是交替运行的 -* instanceKlass 和 _java_mirror 相互持有对方的地址,堆中对象通过 instanceKlass 和元空间进行交互 +* Class 对象和 _java_mirror 相互持有对方的地址,堆中对象通过 instanceKlass 和元空间进行交互 +创建数组类有些特殊,因为数组类本身并不是由类加载器负责创建,而是由 JVM 在运行时根据需要而直接创建的,但数组的元素类型仍然需要依靠类加载器去创建,创建数组类的过程: + +- 如果数组的元素类型是引用类型,那么遵循定义的加载过程递归加载和创建数组的元素类型 +- JVM 使用指定的元素类型和数组维度来创建新的数组类 +- 基本数据类型由启动类加载器加载 + *** @@ -11425,7 +11409,28 @@ Java对象创建时机: 确保 Class 文件的字节流中包含的信息是否符合 JVM 规范,保证被加载类的正确性,不会危害虚拟机自身的安全 -主要包括四种验证:文件格式验证,源数据验证,字节码验证,符号引用验证 +主要包括**四种验证**: + +* 文件格式验证 + +* 语义检查,但凡在语义上不符合规范的,虚拟机不会给予验证通过 + + * 是否所有的类都有父类的存在(除了 Object 外,其他类都应该有父类) + + * 是否一些被定义为 final 的方法或者类被重写或继承了 + + * 非抽象类是否实现了所有抽象方法或者接口方法 + + * 是否存在不兼容的方法 + +* 字节码验证,试图通过对字节码流的分析,判断字节码是否可以被正确地执行 + + * 在字节码的执行过程中,是否会跳转到一条不存在的指令 + * 函数的调用是否传递了正确类型的参数 + * 变量的赋值是不是给了正确的数据类型 + * 栈映射帧(StackMapTable)在这个阶段用于检测在特定的字节码处,其局部变量表和操作数栈是否有着正确的数据类型 + +* 符号引用验证,Class 文件在其常量池会通过字符串记录将要使用的其他类或者方法 @@ -11438,9 +11443,9 @@ Java对象创建时机: 准备阶段为**静态变量分配内存并设置初始值**,使用的是方法区的内存: * 类变量也叫静态变量,就是是被 static 修饰的变量 -* 实例变量也叫对象变量,即没加static 的变量 +* 实例变量也叫对象变量,即没加 static 的变量 -实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中。实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次 +实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中。类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次 类变量初始化: @@ -11463,6 +11468,8 @@ Java对象创建时机: public static final int value = 123; ``` +* Java 并不支持 boolean 类型,对于 boolean 类型,内部实现是 int,由于 int 的默认值是0,故 boolean 的默认值就是 false + *** @@ -11479,8 +11486,8 @@ Java对象创建时机: 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等 * 在类加载阶段解析的是非虚方法,静态绑定 - * 也可以在初始化阶段之后再开始解析,这是为了支持 Java 的**动态绑定** +* 通过解析操作,符号引用就可以转变为目标方法在类中虚方法表中的位置,从而使得方法被成功调用 ```java public class Load2 { @@ -11530,10 +11537,10 @@ class D { 作用:是在类加载过程中的初始化阶段进行静态变量初始化和执行静态代码块 -* 如果类中没有静态变量或静态代码块,那么clinit方法将不会被生成 -* 在执行 clinit 方法时,必须先执行父类的clinit方法 -* clinit 方法只执行一次 +* 如果类中没有静态变量或静态代码块,那么 clinit 方法将不会被生成 +* clinit 方法只执行一次,在执行 clinit 方法时,必须先执行父类的clinit方法 * static 变量的赋值操作和静态代码块的合并顺序由源文件中**出现的顺序**决定 +* static 不加 final 的变量都在初始化环节赋值 **线程安全**问题: @@ -11564,26 +11571,29 @@ public class Test { ##### 时机 -类的初始化是懒惰的,初始化时机: +类的初始化是懒惰的,只有在首次使用时才会被装载,JVM 不会无条件地装载 Class 类型,Java虚拟机规定,一个类或接口在初次使用前,必须要进行初始化 -**主动引用**:虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随之发生): +**主动引用**:虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列情况必须对类进行初始化(加载、验证、准备都会随之发生): -* 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化,最常见的生成这 4 条指令的场景是: - * new:使用 new 关键字实例化对象时 - * getstatic:程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池) +* 当创建一个类的实例时,使用 new 关键字,或者通过反射、克隆、反序列化 +* 当调用类的静态方法或访问静态字段时,遇到 getstatic、putstatic、invokestatic 这三条字节码指令,如果类没有进行过初始化,则必须先触发其初始化 + * getstatic:程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池) * putstatic:程序给类的静态变量赋值 - * invokestatic :调用一个类的静态方法时 -* 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化 -* 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化 + * invokestatic :调用一个类的静态方法 +* 使用 java.lang.reflect 包的方法对类进行反射调用时,如果类没有进行初始化,则需要先触发其初始化 +* 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化,但这条规则并不适用于接口,只有当程序首次使用特定接口的静态字段时,才会导致该接口的初始化 + - 在初始化一个类时,并不会先初始化所实现的接口 + - 在初始化一个接口时,并不会先初始化它的父接口 * 当虚拟机启动时,需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类 -* MethodHandle和VarHandle可以看作是轻量级的反射调用机制,而要想使用这2个调用, 就必须先使用findStaticVarHandle来初始化要调用的类 +* MethodHandle 和 VarHandle 可以看作是轻量级的反射调用机制,而要想使用这两个调用, 就必须先使用 findStaticVarHandle 来初始化要调用的类 * 补充:当一个接口中定义了 JDK8 新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化 **被动引用**:所有引用类的方式都不会触发初始化,称为被动引用 * 通过子类引用父类的静态字段,不会导致子类初始化,只会触发父类的初始化 -* 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自Object 的子类,其中包含了数组的属性和方法 +* 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法 * 常量(final修饰)在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化 +* 调用 ClassLoader 类的 loadClass() 方法加载一个类,并不是对类的主动使用,不会导致类的初始化 @@ -11593,7 +11603,7 @@ public class Test { ##### init -init指的是实例构造器,主要作用是在类实例化过程中执行,执行内容包括成员变量初始化和代码块的执行 +init 指的是实例构造器,主要作用是在类实例化过程中执行,执行内容包括成员变量初始化和代码块的执行 实例化即调用 ()V ,虚拟机会保证这个类的构造方法的线程安全,先为实例变量分配内存空间,再执行赋默认值,然后根据源码中的顺序执行赋初值或代码块,没有成员变量初始化和代码块则不会执行 @@ -11613,9 +11623,9 @@ init指的是实例构造器,主要作用是在类实例化过程中执行, 1. 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象 2. 该类没有在其他任何地方被引用 -3. 该类的类加载器的实例已被 GC +3. 该类的类加载器的实例已被 GC,一般是可替换类加载器的场景,如 OSGi、JSP 的重加载等,很难达成 -在 JVM 生命周期类,由 JVM 自带的类加载器加载的类是不会被卸载的,由我们自定义的类加载器加载的类是可能被卸载。因为 JVM 会始终引用启动、扩展、系统类加载器,这些类加载器始终引用它们所加载的类,所以这些类始终是可及的 +在 JVM 生命周期类,由 JVM 自带的类加载器加载的类是不会被卸载的,自定义的类加载器加载的类是可能被卸载。因为 JVM 会始终引用启动、扩展、系统类加载器,这些类加载器始终引用它们所加载的类,这些类始终是可及的 @@ -11625,39 +11635,66 @@ init指的是实例构造器,主要作用是在类实例化过程中执行, ### 类加载器 -#### 加载器 +#### 类加载 + +类加载方式: + +* 隐式加载:不直接在代码中调用 ClassLoader 的方法加载类对象 + * 创建类对象、使用类的静态域、创建子类对象、使用子类的静态域 + * 在 JVM 启动时,通过三大类加载器加载 class +* 显式加载: + * ClassLoader.loadClass(className),只加载和连接,**不会进行初始化** + * Class.forName(String name, boolean initialize,ClassLoader loader),使用 loader 进行加载和连接,根据参数 initialize 决定是否初始化 -类与类加载器的关系: +类的唯一性: -* 在JVM中表示两个class对象是否为同一个类存在的两个必要条件: +* 在 JVM 中表示两个 class 对象是否为同一个类存在的两个必要条件: - 类的完整类名必须一致,包括包名 - - 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同 + - 加载这个类的 ClassLoader(指 ClassLoader 实例对象)必须相同 * 这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true -类加载器作用:加载字节码到JVM内存,得到Class类的对象 +命名空间: + +- 每个类加载器都有自己的命名空间,命名空间由该加载器及所有的父加载器所加载的类组成 +- 在同一命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类 + +基本特征: + +* 可见性,子类加载器可以访问父加载器加载的类型,但是反过来是不允许的 +* 单一性,由于父加载器的类型对于子加载器是可见的,所以父加载器中加载过的类型,不会在子加载器中重复加载 + + + +*** + + + +#### 加载器 + +类加载器是 Java 的核心组件,用于加载字节码到 JVM 内存,得到 Class 类的对象 从 Java 虚拟机规范来讲,只存在以下两种不同的类加载器: - 启动类加载器(Bootstrap ClassLoader):使用 C++ 实现,是虚拟机自身的一部分 -- 自定义类加载器(User-Defined ClassLoader):Java虚拟机规范**将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器**,使用 Java语言 实现,独立于虚拟机 +- 自定义类加载器(User-Defined ClassLoader):Java虚拟机规范**将所有派生于抽象类 ClassLoader 的类加载器都划分为自定义类加载器**,使用 Java 语言实现,独立于虚拟机 从 Java 开发人员的角度看: -* **启动类加载器(Bootstrap ClassLoader)**: - * 处于安全考虑,BootStrap启动类加载器只加载包名为 java、javax、sun 等开头的类 +* 启动类加载器(Bootstrap ClassLoader): + * 处于安全考虑,Bootstrap 启动类加载器只加载包名为 java、javax、sun 等开头的类 * 类加载器负责加载在 `JAVA_HOME/jre/lib `或 `sun.boot.class.path` 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的类,并且是虚拟机识别的类库加载到虚拟机内存中 * 仅按照文件名识别,如 rt.jar 名字不符合的类库即使放在 lib 目录中也不会被加载 * 启动类加载器无法被 Java 程序直接引用,在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替 -* **扩展类加载器(Extension ClassLoader)**: +* 扩展类加载器(Extension ClassLoader): * 由 ExtClassLoader (sun.misc.Launcher$ExtClassLoader) 实现,上级为 Bootstrap,显示为 null * 将 `JAVA_HOME/jre/lib/ext `或者被 `java.ext.dir` 系统变量所指定路径中的所有类库加载到内存中 - * 开发者可以使用扩展类加载器,创建的JAR放在此目录下,会由拓展类加载器自动加载 -* **应用程序类加载器(Application ClassLoader)**: + * 开发者可以使用扩展类加载器,创建的 JAR 放在此目录下,会由扩展类加载器自动加载 +* 应用程序类加载器(Application ClassLoader): * 由 AppClassLoader(sun.misc.Launcher$AppClassLoader) 实现,上级为 Extension * 负责加载环境变量 classpath 或系统属性 `java.class.path` 指定路径下的类库 * 这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此称为系统类加载器 * 可以直接使用这个类加载器,如果应用程序中没有自定义类加载器,这个就是程序中默认的类加载器 -* 自定义类加载器:由开发人员自定义的类加载器,上级是Application +* 自定义类加载器:由开发人员自定义的类加载器,上级是 Application ```java public static void main(String[] args) { @@ -11684,33 +11721,36 @@ public static void main(String[] args) { } ``` +补充两个类加载器: + +* SecureClassLoader 扩展了 ClassLoader,新增了几个与使用相关的代码源和权限定义类验证(对 class 源码的访问权限)的方法,一般不会直接跟这个类打交道,更多是与它的子类 URLClassLoader 有所关联 +* ClassLoader 是一个抽象类,很多方法是空的没有实现,而 URLClassLoader 这个实现类为这些方法提供了具体的实现,并新增了 URLClassPath 类协助取得 Class 字节流等功能。在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承 URLClassLoader 类,这样就可以避免去编写 findClass() 方法及其获取字节码流的方式,使自定义类加载器编写更加简洁 + *** -#### 加载类 +#### 常用API -ClassLoader类,是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器) +ClassLoader 类,是一个抽象类,其后所有的类加载器都继承自 ClassLoader(不包括启动类加载器) -获取ClassLoader的途径: +获取 ClassLoader 的途径: -* 获取当前类的ClassLoader:`clazz.getClassLoader()` -* 获取当前线程上下文的ClassLoader:`Thread.currentThread.getContextClassLoader()` -* 获取系统的ClassLoader:`ClassLoader.getSystemClassLoader()` -* 获取调用者的ClassLoader:`DriverManager.getCallerClassLoader()` +* 获取当前类的 ClassLoader:`clazz.getClassLoader()` +* 获取当前线程上下文的 ClassLoader:`Thread.currentThread.getContextClassLoader()` +* 获取系统的 ClassLoader:`ClassLoader.getSystemClassLoader()` +* 获取调用者的 ClassLoader:`DriverManager.getCallerClassLoader()` -ClassLoader类常用方法: +ClassLoader 类常用方法: -| 方法 | 说明 | -| ----------------------------------------------------- | ------------------------------------------------------------ | -| getParent() | 返回该类加载器的超类加载器 | -| loadClass(String name) | 加载名称为name的类,返回结果为java.lang.Class类的实例 | -| findClass(String name) | 查找名称为name的类,返回结果为java.lang.Class类的实例 | -| findLoadedClass(String name) | 查找名称为name的已经被加载过的类,返回结果为java.lang.Class类的实例 | -| defineClass(String name, byte[] b, int off,int len) | 把字节数组b中的内容转换为一个Java类 ,返回结果为java.lang.Class类的实例 | -| resolveClass(Class c) | 连接指定的一个java类 | +* `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 对象创建完成的同时也被解析 @@ -11722,7 +11762,7 @@ ClassLoader类常用方法: ##### 加载机制 -在JVM中,对于类加载模型提供了三种,分别为全盘加载、双亲委派、缓存机制 +在 JVM 中,对于类加载模型提供了三种,分别为全盘加载、双亲委派、缓存机制 - **全盘加载:**当一个类加载器负责加载某个 Class 时,该 Class 所依赖和引用的其他 Class 也将由该类加载器负责载入,除非显示指定使用另外一个类加载器来载入 @@ -11731,7 +11771,7 @@ ClassLoader类常用方法: - **缓存机制:**会保证所有加载过的 Class 都会被缓存,当程序中需要使用某个 Class 时,类加载器先从缓存区中搜寻该 Class,只有当缓存区中不存在该 Class 对象时,系统才会读取该类对应的二进制数据,并将其转换成 Class 对象存入缓冲区中 - 这就是修改了 Class 后,必须重新启动 JVM,程序所做的修改才会生效的原因 - + @@ -11741,19 +11781,19 @@ ClassLoader类常用方法: ##### 双亲委派 -双亲委派模型 (Parents Delegation Model):该模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器,这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance) +双亲委派模型(Parents Delegation Model):该模型要求除了顶层的启动类加载器外,其它类加载器都要有父类加载器,这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance) 工作过程:一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载 双亲委派机制的优点: -* 可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性 +* 可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证全局唯一性 -* Java类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一 +* Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一 -* 保护程序安全,防止类库的核心API被随意篡改 +* 保护程序安全,防止类库的核心 API 被随意篡改 - 例如:在工程中新建java.lang包,接着在该包下新建String类,并定义main函数 + 例如:在工程中新建 java.lang 包,接着在该包下新建 String 类,并定义 main 函数 ```java public class String { @@ -11763,10 +11803,20 @@ ClassLoader类常用方法: } ``` - 此时执行main函数,会出现异常,在类 java.lang.String 中找不到 main 方法,防止恶意篡改核心API库 - 出现该信息是因为双亲委派的机制,java.lang.String 的在启动类加载器(Bootstrap classLoader)得到加载,启动类加载器优先级更高,在核心 jre 库中有其相同名字的类文件,但该类中并没有 main 方法 + 此时执行main函数,会出现异常,在类 java.lang.String 中找不到 main 方法,防止恶意篡改核心 API 库 + 出现该信息是因为双亲委派的机制,java.lang.String 的在启动类加载器(Bootstrap)得到加载,启动类加载器优先级更高,在核心 jre 库中有其相同名字的类文件,但该类中并没有 main 方法 + +双亲委派机制的缺点:检查类是否加载的委托过程是单向的,这个方式虽然从结构上看比较清晰,使各个 ClassLoader 的职责非常明确,但顶层的 ClassLoader 无法访问底层的 ClassLoader 所加载的类 -**源码分析: ** + + + + +*** + + + +##### 源码分析 ```java protected Class loadClass(String name, boolean resolve) @@ -11775,7 +11825,7 @@ protected Class loadClass(String name, boolean resolve) //调用当前类加载器的findLoadedClass(name),检查当前类加载器是否已加载过指定name的类 Class c = findLoadedClass(name); - //判断当前类加载器如果没有加载过 + //当前类加载器如果没有加载过 if (c == null) { long t0 = System.nanoTime(); try { @@ -11795,7 +11845,8 @@ protected Class loadClass(String name, boolean resolve) } if (c == null) { - // 如果调用父类的类加载器无法对类进行加载,则用自己的findClass(name)方法进行加载 + // 如果调用父类的类加载器无法对类进行加载,则用自己的findClass()方法进行加载 + // k自定义findClass()方法 long t1 = System.nanoTime(); c = findClass(name); @@ -11819,27 +11870,51 @@ protected Class loadClass(String name, boolean resolve) -##### 破坏双亲 +##### 破坏委派 + +双亲委派模型并不是一个具有强制性约束的模型,而是 Java 设计者推荐给开发者的类加载器实现方式 + +破坏双亲委派模型的方式: -破坏双亲委派模型有两种方式: +* 自定义 ClassLoader + + * 如果不想破坏双亲委派模型,只需要重写 findClass 方法 + * 如果想要去破坏双亲委派模型,需要去**重写 loadClass **方法 * 引入线程上下文类加载器 Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI 等。这些 SPI 接口由 Java 核心库来提供,而 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径 classpath 里,SPI 接口中的代码需要加载具体的实现类: - * SPI 的接口是 Java核心库的一部分,是由引导类加载器来加载的 + * SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的 * SPI 的实现类是由系统类加载器来加载的,引导类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader 无法委派 AppClassLoader 来加载类 - JDK 开发人员引入了线程上下文类加载器(Thread Context ClassLoader),这种类加载器可以通过 Thread 类的 setContextClassLoader 方法进行设置线程上下文类加载器,在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器,使 Bootstrap Classloader 加载器拿到了 Application ClassLoader 加载器应该加载的类,破坏了双亲委派模型 + JDK 开发人员引入了线程上下文类加载器(Thread Context ClassLoader),这种类加载器可以通过 Thread 类的 setContextClassLoader 方法进行设置线程上下文类加载器,在执行线程中抛弃双亲委派加载模式,使程序可以逆向使用类加载器,使 Bootstrap 加载器拿到了 Application 加载器加载的类,破坏了双亲委派模型 + +* 实现程序的动态性,如代码热替换(Hot Swap)、模块热部署(Hot Deployment) -* 自定义ClassLoader + IBM 公司主导的 JSR一291(OSGiR4.2)实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(OSGi 中称为 Bundle)都有一个自己的类加载器,当更换一个 Bundle 时,就把 Bundle 连同类加载器一起换掉以实现代码的热替换,在 OSGi 环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构 - * 如果不想破坏双亲委派模型,只需要重写 findClass 方法 - * 如果想要去破坏双亲委派模型,需要去**重写 loadClass **方法 + 当收到类加载请求时,OSGi将按照下面的顺序进行类搜索: + + 1. 将以 java.* 开头的类,委派给父类加载器加载 + 2. 否则,将委派列表名单内的类,委派给父类加载器加载 + 3. 否则,将 Import 列表中的类,委派给 Export 这个类的 Bundle 的类加载器加载 + 4. 否则,查找当前 Bundle 的 ClassPath,使用自己的类加载器加载 + 5. 否则,查找类是否在自己的 Fragment Bundle 中,如果在就委派给 Fragment Bundle 类加载器加载 + 6. 否则,查找 Dynamic Import 列表的 Bundle,委派给对应 Bundle 的类加载器加载 + 7. 否则,类查找失败 + + + +**** -参考文章:https://www.jianshu.com/p/4132d82ca3a6 +##### 热替换 + +热替换是指在程序的运行过程中,不停止服务,只通过替换程序文件来修改程序的行为,**热替换的关键需求在于服务不能中断**,修改必须立即表现正在运行的系统之中 + + @@ -11849,11 +11924,16 @@ protected Class loadClass(String name, boolean resolve) #### 沙箱机制 -沙箱机制:将 Java 代码限定在虚拟机特定的运行范围中,并且严格限制代码对本地系统资源访问,来保证对代码的有效隔离,防止对本地系统造成破坏 +沙箱机制(Sandbox):将 Java 代码限定在虚拟机特定的运行范围中,并且严格限制代码对本地系统资源访问,来保证对代码的有效隔离,防止对本地系统造成破坏 + +沙箱**限制系统资源访问**,包括 CPU、内存、文件系统、网络,不同级别的沙箱对资源访问的限制也不一样 -沙箱**限制系统资源访问**,包括CPU、内存、文件系统、网络,不同级别的沙箱对资源访问的限制也不一样 +* JDK1.0:Java 中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码被看作是不受信的。对于授信的本地代码,可以访问一切本地资源,而对于非授信的远程代码不可以访问本地资源,其实依赖于沙箱机制。如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现 +* JDK1.1:针对安全机制做了改进,增加了安全策略。允许用户指定代码对本地资源的访问权限 +* JDK1.2:改进了安全机制,增加了代码签名,不论本地代码或是远程代码都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制 +* JDK1.6:当前最新的安全机制,引入了域(Domain)的概念。虚拟机会把所有代码加载到不同的系统域和应用域,不同的保护域对应不一样的权限。系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问 -举例:自定义 String 类,但是在加载自定义 String 类的时候会先使用引导类加载器加载,而引导类加载器在加载过程中会先加载 jdk 自带的文件(rt.jar包中的 java\lang\String.class),报错信息说没有 main 方法就是因为加载的是 rt.jar 包中的 String 类,这样可以保证对 java 核心源代码的保护 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-沙箱机制.png) @@ -11875,44 +11955,88 @@ public class MyClassLoader extends ClassLoader{ 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 { - byte[] data = new byte[0]; + BufferedInputStream bis = null; + ByteArrayOutputStream baos = null; try { - data = loadByte(name); - } catch (Exception e) { + //获取字节码文件的完整路径 + String fileName = byteCodePath + 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 defineClass(name, data, 0, data.length); - } - - private byte[] loadByte(String name) throws Exception { - name = name.replaceAll("\\.", "/"); - FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class"); - int len = fis.available(); - byte[] data = new byte[len]; - fis.read(data); - fis.close(); - return data; + return null; } } ``` ```java -public class ClassLoaderTest { - public static void main(String[] args) throws Exception { - MyClassLoader classLoader = new MyClassLoader("D:\\workspace\\project\\src\\main\\java"); - Class clazz = classLoader.loadClass("com.demo.User"); - System.out.println(clazz.getClassLoader().getClass().getName()); +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(); } } -//当存在.java文件时,根据双亲委派机制,显示当前类加载器为AppClassLoader -//当将.java文件删除时,则显示使用的是自定义的类加载器 ``` +**** + + + +#### JDK9 + +为了保证兼容性,JDK9 没有改变三层类加载器架构和双亲委派模型,但为了模块化系统的顺利运行做了一些变动: + +* 扩展机制被移除,扩展类加载器由于向后兼容性的原因被保留,不过被重命名为平台类加载器(platform classloader),可以通过 ClassLoader 的新方法 getPlatformClassLoader() 来获取 + +* JDK9 基于模块化进行构建(原来的 rt.jar 和 tools.jar 被拆分成数十个JMOD文件),其中的 Java 类库就满足了可扩展的需求,那就无须再保留 `\lib\ext` 目录,此前使用这个目录或者 `java.ext.dirs` 系统变量来扩展 JDK 功能的机制就不需要继续存在 + +* 启动类加载器、平台类加载器、应用程序类加载器全都继承于 `jdk.internal.loader.BuiltinClassLoader` + + + *** @@ -12662,8 +12786,6 @@ JVM 提供的操作数栈管理指令,可以用于直接操作操作数栈的 ##### 控制转移 -###### 指令介绍 - 比较指令:比较栈顶两个元素的大小,并将比较结果入栈 @@ -12712,42 +12834,6 @@ JVM 提供的操作数栈管理指令,可以用于直接操作操作数栈的 -*** - - - -###### 代码示例 - -条件判断: - -```java -public static void main(String[] args) { int a = 0; if(a == 0) { a = 10; } else { a = 20; }} -``` - -```sh -1: istore_12: iload_13: ifne 126: bipush 108: istore_19: goto 1512: bipush 2014: istore_115: return -``` - -while循环: - -```java -public static void main(String[] args) { int a = 0; while (a < 10) { a++; }} -``` - -```sh -0: iconst_01: istore_12: iload_13: bipush 105: if_icmpge 148: iinc 1, 111: goto 214: return -``` - -for循环: - -```java -for (int i = 0; i < 10; i++) { } -``` - -```java -0: iconst_01: istore_12: iload_13: bipush 105: if_icmpge 148: iinc 1, 1 //在slot上直接递增11: goto 214: return -``` - *** @@ -12758,14 +12844,25 @@ for (int i = 0; i < 10; i++) { } ###### 处理机制 -抛出异常指令:athrow指令 +抛出异常指令:athrow 指令 JVM 处理异常(catch语句)不是由字节码指令来实现的,而是**采用异常表来完成**的 * 代码: ```java - public static void main(String[] args) { int i = 0; try { i = 10; } catch (ArithmeticException e) { i = 30; } catch (NullPointerException e) { i = 40; } catch (Exception e) { i = 50; }} + public static void main(String[] args) { + int i = 0; + try { + i = 10; + } catch (ArithmeticException e) { + i = 30; + } catch (NullPointerException e) { + i = 40; + } catch (Exception e) { + i = 50; + } + } ``` * 字节码: @@ -12774,7 +12871,36 @@ JVM 处理异常(catch语句)不是由字节码指令来实现的,而是** * 8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置,因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用 ```java - public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=3, args_size=1 0: iconst_0 1: istore_1 2: bipush 10 4: istore_1 5: goto 26 8: astore_2 9: bipush 30 11: istore_1 12: goto 26 15: astore_2 16: bipush 40 18: istore_1 19: goto 26 22: astore_2 23: bipush 50 25: istore_1 26: return Exception table: from to target type 2 5 8 Class java/lang/Exception 2 5 15 Class java/lang/NullPointerException 2 5 22 Class java/lang/Exception LineNumberTable: ... LocalVariableTable: Start Length Slot Name Signature 9 3 2 e Ljava/lang/ArithmeticException; 16 3 2 e Ljava/lang/NullPointerException; 23 3 2 e Ljava/lang/Exception; 0 27 0 args [Ljava/lang/String; 2 25 1 i I StackMapTable: ... MethodParameters: ...} + 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: + 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 ``` @@ -12790,13 +12916,28 @@ finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程 * 代码: ```java - public static void main(String[] args) { int i = 0; try { i = 10; } catch (Exception e) { i = 20; } finally { i = 30; }} + public static int test() { + try { + return 10; + } finally { + return 20; + } + } ``` * 字节码: ```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: returnException table: from to target type 2 5 11 Class java/lang/Exception 2 5 21 any // 剩余的异常类型,比如 Error 11 15 21 any // 剩余的异常类型,比如 ErrorLineNumberTable: ...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 + 0: bipush 10 // 10 放入栈顶 + 2: istore_0 // 10 -> slot 0 (从栈顶移除了) + 3: bipush 20 // 20 放入栈顶 + 5: ireturn // 返回栈顶 int(20) + 6: astore_1 // catch any -> slot 1 存入局部变量表的 slot1 + 7: bipush 20 // 20 放入栈顶 + 9: ireturn // 返回栈顶 int(20) + Exception table: + from to target type + 0 3 6 any ``` @@ -12810,11 +12951,26 @@ finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程 * 吞异常 ```java - public static int test() { try { return 10; } finally { return 20; }} + 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 -> slot 1 存入局部变量表的 slot1 7: bipush 20 // 20 放入栈顶 9: ireturn // 返回栈顶 int(20)Exception table: from to target type 0 3 6 any + 0: bipush 10 // 10 放入栈顶 + 2: istore_0 // 10 -> slot 0 (从栈顶移除了) + 3: bipush 20 // 20 放入栈顶 + 5: ireturn // 返回栈顶 int(20) + 6: astore_1 // catch any -> slot 1 存入局部变量表的 slot1 + 7: bipush 20 // 20 放入栈顶 + 9: ireturn // 返回栈顶 int(20) + Exception table: + from to target type + 0 3 6 any ``` * 由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果肯 finally 的为准 @@ -12823,11 +12979,39 @@ finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程 * 不吞异常 ```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; } }} + 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,赋值给i,放入slot 0 3: iload_0 // i(10)加载至操作数栈 4: istore_1 // 10 -> slot 1,暂存至 slot 1,目的是为了固定返回值 5: bipush 20 // 20 放入栈顶 7: istore_0 // 20 -> i 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 + 0: bipush 10 // 10 放入栈顶 + 2: istore_0 // 10 -> i,赋值给i,放入slot 0 + 3: iload_0 // i(10)加载至操作数栈 + 4: istore_1 // 10 -> slot 1,暂存至 slot 1,目的是为了固定返回值 + 5: bipush 20 // 20 放入栈顶 + 7: istore_0 // 20 -> i + 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 ``` @@ -13352,14 +13536,6 @@ Passenger 类的方法表包括两个方法,分别对应 0 号和 1 号。方 -*** - - - - - - - *** diff --git a/Prog.md b/Prog.md index 9956959..d217e9e 100644 --- a/Prog.md +++ b/Prog.md @@ -4070,13 +4070,13 @@ static class Entry extends WeakReference> { Memory leak:内存泄漏是指程序中动态分配的堆内存由于某种原因未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果,内存泄漏的堆积终将导致内存溢出 -* 如果key使用强引用: +* 如果 key 使用强引用: 使用完 ThreadLocal ,threadLocal Ref 被回收,但是 threadLocalMap 的 Entry 强引用了 threadLocal,造成 threadLocal 无法被回收,无法完全避免内存泄漏 -* 如果key使用弱引用: +* 如果 key 使用弱引用: 使用完 ThreadLocal ,threadLocal Ref 被回收,ThreadLocalMap 只持有 ThreadLocal 的弱引用,所以threadlocal 也可以被回收,此时 Entry 中的 key=null。但没有手动删除这个 Entry 或者 CurrentThread 依然运行,依然存在强引用链,value 不会被回收,而这块 value 永远不会被访问到,导致 value 内存泄漏 @@ -4091,7 +4091,7 @@ Memory leak:内存泄漏是指程序中动态分配的堆内存由于某种原 解决方法:使用完 ThreadLocal 中存储的内容后将它 **remove** 掉就可以 -ThreadLocal 内部解决方法:在 ThreadLocalMap 中的 set/getEntry 方法中,会对 key 进行判断,如果为null (ThreadLocal 为 null) 的话,那么会对Entry进行垃圾回收。所以**使用弱引用比强引用多一层保障**,就算不调用remove,也有机会进行GC +ThreadLocal 内部解决方法:在 ThreadLocalMap 中的 set/getEntry 方法中,会对 key 进行判断,如果为 null (ThreadLocal 为 null) 的话,那么会对 Entry 进行垃圾回收。所以**使用弱引用比强引用多一层保障**,就算不调用 remove,也有机会进行 GC @@ -4103,7 +4103,7 @@ ThreadLocal 内部解决方法:在 ThreadLocalMap 中的 set/getEntry 方法 ##### 基本使用 -父子线程:创建子线程的线程是父线程,比如实例中的 main 线程就是父线程 +父子线程:**创建子线程的线程是父线程**,比如实例中的 main 线程就是父线程 ThreadLocal 中存储的是线程的局部变量,如果想实现线程间局部变量传递可以使用 InheritableThreadLocal 类 @@ -10439,7 +10439,7 @@ NIO 和 BIO 的比较: -### NIO原理 +### 实现原理 NIO 三大核心部分:**Channel( 通道) ,Buffer( 缓冲区), Selector( 选择器)** @@ -10455,7 +10455,7 @@ NIO 三大核心部分:**Channel( 通道) ,Buffer( 缓冲区), Selector( 选 Selector 是一个 Java NIO 组件,能够检查一个或多个 NIO 通道,并确定哪些通道已经准备好进行读取或写入,这样一个单独的线程可以管理多个channel,从而管理多个网络连接,提高效率 -NIO的实现框架: +NIO 的实现框架: ![](https://gitee.com/seazean/images/raw/master/Java/NIO框架.png) @@ -10613,11 +10613,49 @@ public class TestBuffer { -#### 直接内存 +### 直接内存 -##### 源码分析 +#### 基本介绍 + +Byte Buffer 有两种类型,一种是基于直接内存(也就是非堆内存),另一种是非直接内存(也就是堆内存) + +Direct Memory 优点: + +* Java 的 NIO 库允许 Java 程序使用直接内存,用于数据缓冲区,使用 native 函数直接分配堆外内存 +* 读写性能高,读写频繁的场合可能会考虑使用直接内存 +* 大大提高 IO 性能,避免了在 Java 堆和 native 堆来回复制数据 + +直接内存缺点: + +* 分配回收成本较高,不受 JVM 内存回收管理 +* 可能导致 OutOfMemoryError 异常:OutOfMemoryError: Direct buffer memory +* 回收依赖 System.gc() 的调用,但这个调用 JVM 不保证执行、也不保证何时执行,行为是不可控的。程序一般需要自行管理,成对去调用 malloc、free + +应用场景: + +- 有很大的数据需要存储,数据的生命周期很长 +- 适合频繁的 IO 操作,比如网络并发场景 + +数据流的角度: + +* 非直接内存的作用链:本地IO → 内核缓冲区→ 用户缓冲区 →内核缓冲区 → 本地IO +* 直接内存是:本地IO → 直接内存 → 本地IO + +JVM 直接内存图解: + + + + -Byte Buffer 可以是两种类型,一种是基于直接内存(也就是非堆内存),另一种是非直接内存(也就是堆内存)。对于直接内存来说,JVM将会在IO操作上具有更高的性能,因为直接作用于本地系统的IO操作,而非直接内存,也就是堆内存中的数据,如果要作IO操作,会先从本进程内存复制到直接内存,再利用本地IO处理 + + + + +*** + + + +#### 源码解析 直接内存创建 Buffer 对象:`static XxxBuffer allocateDirect(int capacity)` @@ -10652,24 +10690,13 @@ Byte Buffer 可以是两种类型,一种是基于直接内存(也就是非 } ``` -数据流的角度: - -* 非直接内存的作用链:本地IO → 直接内存 → 非直接内存 → 直接内存 → 本地IO -* 直接内存是:本地IO → 直接内存 → 本地IO - -JVM 直接内存图解: - - - - - *** -##### 分配回收 +#### 分配回收 DirectByteBuffer 源码分析: From 0e144402b2ae315cf84abf23bd10fcd6e32e0dff Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 8 Jul 2021 09:45:59 +0800 Subject: [PATCH 068/242] Update Java Notes --- DB.md | 699 +++++++++++++++++++++++++++++++++----------------------- Java.md | 54 +++-- Prog.md | 12 +- 3 files changed, 461 insertions(+), 304 deletions(-) diff --git a/DB.md b/DB.md index 5af1eb9..79909b5 100644 --- a/DB.md +++ b/DB.md @@ -4,13 +4,13 @@ ### 数据库 -数据库:DataBase,简称DB,用于存储和管理数据的仓库,它的存储空间很大,可以存放百万上亿条数据。 +数据库:DataBase,简称 DB,用于存储和管理数据的仓库,它的存储空间很大,可以存放百万上亿条数据。 数据库的优势: - 可以持久化存储数据 - 方便存储和管理数据 -- 使用了统一的方式操作数据库 -- SQL +- 使用了统一的方式操作数据库 SQL 数据库、数据表、数据的关系介绍: @@ -22,7 +22,7 @@ - 数据表 - 数据库最重要的组成部分之一 - - 它由纵向的列和横向的行组成(类似excel表格) + - 它由纵向的列和横向的行组成(类似 excel 表格) - 可以指定列名、数据类型、约束等 - 一个表中可以存储多条数据 @@ -31,8 +31,12 @@ - 想要永久化存储的数据 ![](https://gitee.com/seazean/images/raw/master/DB/数据库、数据表、数据之间的关系.png) - - + + + +参考视频:https://www.bilibili.com/video/BV1zJ411M7TB(推荐观看) + + *** @@ -50,11 +54,11 @@ MySQL所使用的SQL语句是用于访问数据库最常用的标准化语言。 MySQL配置: -* MySQL安装:https://www.jianshu.com/p/ba48f1e386f0 +* MySQL 安装:https://www.jianshu.com/p/ba48f1e386f0 -* MySQL配置: +* MySQL 配置: - * 修改MySQL默认字符集 + * 修改 MySQL 默认字符集:安装 MySQL 之后第一件事就是修改字符集编码 ```mysql vim /etc/mysql/my.cnf @@ -68,13 +72,13 @@ MySQL配置: default-character-set=utf8 ``` - * 启动MySQL服务: + * 启动 MySQL 服务: ```shell systemctl start/restart mysql ``` - * 登录MySQL: + * 登录 MySQL: ```shell mysql -u root -p 敲回车,输入密码 @@ -97,7 +101,7 @@ MySQL配置: set password=password('密码'); ``` - * 授予远程连接权限(MySQL内输入): + * 授予远程连接权限(MySQL 内输入): ```mysql -- 授权 @@ -106,7 +110,7 @@ MySQL配置: flush privileges; ``` -* 修改MySQL绑定IP: +* 修改 MySQL 绑定 IP: ```shell cd /etc/mysql/mysql.conf.d @@ -115,7 +119,7 @@ MySQL配置: #bind-address = 127.0.0.1注释该行 ``` -* 关闭Linux防火墙 +* 关闭 Linux 防火墙 ```shell systemctl stop firewalld.service @@ -335,36 +339,30 @@ mysqlshow -uroot -p1234 test book --count 体系结构详解: * 第一层:网络连接层 - * 一些客户端和链接服务,包含本地socket 通信和大多数基于客户端/服务端工具实现的 TCP/IP 通信,主要完成一些类似于连接处理、授权认证、及相关的安全方案 - * 在该层上引入了线程池的概念,为通过认证安全接入的客户端提供线程 - * 在该层上实现基于SSL的安全链接,服务器也会为安全接入的每个客户端验证它所具有的操作权限 + * 一些客户端和链接服务,包含本地 socket 通信和大多数基于客户端/服务端工具实现的 TCP/IP 通信,主要完成一些类似于连接处理、授权认证、及相关的安全方案 + * 在该层上引入了线程池 Connection Pool 的概念,管理缓冲用户连接,线程处理等需要缓存的需求 + * 在该层上实现基于 SSL 的安全链接,服务器也会为安全接入的每个客户端验证它所具有的操作权限 - 第二层:核心服务层 - * 完成大多数的核心服务功能,如SQL接口,并完成缓存的查询,SQL的分析和优化,部分内置函数的执行 + * 完成大多数核心服务功能,如 SQL接口,并完成缓存的查询,SQL的分析和优化: + * Management Serveices & Utilities:系统管理和控制工具,备份、安全、复制、集群等 + * SQL Interface:接受用户的 SQL 命令,并且返回用户需要查询的结果 + * Parser:SQL 语句解析器 + * Optimizer:查询优化器,SQL 语句在查询之前会使用查询优化器进行优化,优化客户端查找请求,根据客户端请求的 query 语句和数据库中的一些统计信息进行分析,得出一个最优策略 + * Caches & Buffers:查询缓存,服务器会查询内部的缓存,如果缓存空间足够大,可以在大量读操作的环境中提升系统的性能 * 所有**跨存储引擎**的功能在这一层实现,如存储过程、触发器、视图等 * 在该层服务器会解析查询并创建相应的内部解析树,并对其完成相应的优化如确定表的查询顺序,是否利用索引等, 最后生成相应的执行操作 - * 服务器会查询内部的缓存,如果缓存空间足够大,可以在大量读操作的环境中提升系统的性能 + * MySQL 中服务器层不管理事务,**事务是由存储引擎实现的** - 第三层:存储引擎层 - - 存储引擎真正的负责了MySQL中数据的存储和提取,服务器通过API和存储引擎进行通信 - - 不同的存储引擎具有不同的功能,这样我们可以根据自己的需要,来选取合适的存储引擎 - - MySQL中服务器层不管理事务,**事务是由存储引擎实现的** + - Pluggable Storage Engines:存储引擎接口,MySQL 区别于其他数据库的最重要的特点就是其插件式的表存储引擎(**存储引擎是基于表的,而不是数据库**) + - 存储引擎真正的负责了 MySQL 中数据的存储和提取,服务器通过 API 和存储引擎进行通信 + - 不同的存储引擎具有不同的功能,可以根据开发的需要,来选取合适的存储引擎 - 第四层:系统文件层 - 数据存储层,主要是将数据存储在文件系统之上,并完成与存储引擎的交互 - - 文件系统:配置文件、数据文件、日志文件、错误文件、二进制文件等等的保存 + - File System:文件系统,保存配置文件、数据文件、日志文件、错误文件、二进制文件等 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-体系结构.png) -整个MySQL Server由以下组成 - -- Connection Pool:连接池组件 -- Management Services & Utilities:管理服务和工具组件 -- SQL Interface:SQL接口组件 -- Parse:查询分析器组件 -- Optimizer:优化器组件 -- Caches & Buffers:缓冲池组件 -- Pluggable Storage Engines:存储引擎 -- File System:文件系统 - @@ -1687,6 +1685,8 @@ WHERE SELECT 列名 FROM 表名1 RIGHT [OUTER] JOIN 表名2 ON 条件; ``` +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-JOIN查询图.png) + @@ -2193,7 +2193,7 @@ redo log,记录**数据页的物理修改**,而不是某一行或某几行 InnoDB 作为存储引擎,数据是存放在磁盘中,每次读写数据都需要磁盘 IO,效率会很低。InnoDB 提供了缓存 Buffer Pool,Buffer Pool 中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲: -* 从数据库读取数据时,会首先从缓存中读取,如果缓存中没有,则从磁盘读取后放入Buffer Pool +* 从数据库读取数据时,会首先从缓存中读取,如果缓存中没有,则从磁盘读取后放入 Buffer Pool * 向数据库写入数据时,会首先写入缓存,缓存中修改的数据会**定期刷新**到磁盘,这一过程称为刷脏 Buffer Pool 的使用提高了读写数据的效率,但是也带了新的问题:如果 MySQL 宕机,此时 Buffer Pool 中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证,所以引入 redo log @@ -3499,19 +3499,19 @@ LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定 ### 基本介绍 -对比其他数据库,MySQL的架构可以在不同场景应用并发挥良好作用,主要体现在存储引擎,插件式的存储引擎架构将查询处理和其他的系统任务以及数据的存储提取分离,可以针对不同的存储需求可以选择最优的存储引擎 +对比其他数据库,MySQL 的架构可以在不同场景应用并发挥良好作用,主要体现在存储引擎,插件式的存储引擎架构将查询处理和其他的系统任务以及数据的存储提取分离,可以针对不同的存储需求可以选择最优的存储引擎 存储引擎的介绍: -- MySQL数据库使用不同的机制存取表文件 , 机制的差别在于不同的存储方式、索引技巧、锁定水平等不同的功能和能力,在MySQL中,将这些不同的技术及配套的功能称为**存储引擎** -- Oracle , SqlServer等数据库只有一种存储引擎,MySQL提供了插件式的存储引擎架构,所以MySQL存在多种存储引擎 , 就会让数据库采取了不同的处理数据的方式和扩展功能 +- MySQL 数据库使用不同的机制存取表文件 , 机制的差别在于不同的存储方式、索引技巧、锁定水平等不同的功能和能力,在 MySQL 中,将这些不同的技术及配套的功能称为**存储引擎** +- Oracle , SqlServer 等数据库只有一种存储引擎,MySQL 提供了插件式的存储引擎架构,所以 MySQL 存在多种存储引擎 , 就会让数据库采取了不同的处理数据的方式和扩展功能 - 在关系型数据库中数据的存储是以表的形式存进行,所以存储引擎也称为表类型(存储和操作此表的类型) - 通过选择不同的引擎,能够获取最佳的方案, 也能够获得额外的速度或者功能,提高程序的整体效果。 MySQL支持的存储引擎: -- MySQL支持的引擎包括:InnoDB、MyISAM、MEMORY、Archive、Federate、CSV、BLACKHOLE等 -- MySQL5.5之前的默认存储引擎是MyISAM,5.5之后就改为了InnoDB +- MySQL 支持的引擎包括:InnoDB、MyISAM、MEMORY、Archive、Federate、CSV、BLACKHOLE 等 +- MySQL5.5 之前的默认存储引擎是 MyISAM,5.5 之后就改为了 InnoDB @@ -3521,41 +3521,41 @@ MySQL支持的存储引擎: ### 引擎对比 -MyISAM存储引擎: +MyISAM 存储引擎: * 特点:不支持事务和外键,读取速度快,节约资源 * 应用场景:查询和插入操作为主,只有很少更新和删除操作,并对事务的完整性、并发性要求不高 * 存储方式: - * 每个MyISAM在磁盘上存储成3个文件,其文件名都和表名相同,拓展名不同 - * 表的定义保存在.frm文件,表数据保存在.MYD (MYData) 文件中,索引保存在.MYI (MYIndex) 文件中 + * 每个 MyISAM 在磁盘上存储成 3 个文件,其文件名都和表名相同,拓展名不同 + * 表的定义保存在 .frm 文件,表数据保存在 .MYD (MYData) 文件中,索引保存在 .MYI (MYIndex) 文件中 -InnoDB存储引擎:(MySQL5.5版本后默认的存储引擎) +InnoDB 存储引擎:(MySQL5.5版本后默认的存储引擎) -- 特点:**支持事务**和外键操作,支持并发控制。对比MyISAM的存储引擎,InnoDB写的处理效率差一些,并且会占用更多的磁盘空间以保留数据和索引 +- 特点:**支持事务**和外键操作,支持并发控制。对比 MyISAM 的存储引擎,InnoDB 写的处理效率差一些,并且会占用更多的磁盘空间以保留数据和索引 - 应用场景:对事务的完整性有比较高的要求,在并发条件下要求数据的一致性,读写频繁的操作 - 存储方式: - - 使用共享表空间存储, 这种方式创建的表的表结构保存在.frm文件中, 数据和索引保存在 innodb_data_home_dir 和 innodb_data_file_path 定义的表空间中,可以是多个文件 + - 使用共享表空间存储, 这种方式创建的表的表结构保存在 .frm 文件中, 数据和索引保存在 innodb_data_home_dir 和 innodb_data_file_path 定义的表空间中,可以是多个文件 - 使用多表空间存储,创建的表的表结构存在 .frm 文件中,每个表的数据和索引单独保存在 .ibd 中 -MEMORY存储引擎: +MEMORY 存储引擎: -- 特点:每个MEMORY表实际对应一个磁盘文件 ,该文件中只存储表的结构,表数据保存在内存中,且默认使用HASH索引,这样有利于数据的快速处理,在需要快速定位记录可以提供更快的访问,但是服务一旦关闭,表中的数据就会丢失,数据存储不安全 +- 特点:每个 MEMORY 表实际对应一个磁盘文件 ,该文件中只存储表的结构,表数据保存在内存中,且默认使用 HASH 索引,这样有利于数据的快速处理,在需要快速定位记录可以提供更快的访问,但是服务一旦关闭,表中的数据就会丢失,数据存储不安全 - 应用场景:通常用于更新不太频繁的小表,用以快速得到访问结果,类似缓存 -- 存储方式:表结构保存在.frm中 +- 存储方式:表结构保存在 .frm 中 MERGE存储引擎: * 特点: - * 是一组MyISAM表的组合,这些MyISAM表必须结构完全相同,通过将不同的表分布在多个磁盘上 - * MERGE表本身并没有存储数据,对MERGE类型的表可以进行查询、更新、删除操作,这些操作实际上是对内部的MyISAM表进行的 + * 是一组 MyISAM 表的组合,这些 MyISAM 表必须结构完全相同,通过将不同的表分布在多个磁盘上 + * MERGE 表本身并没有存储数据,对 MERGE 类型的表可以进行查询、更新、删除操作,这些操作实际上是对内部的 MyISAM 表进行的 -* 应用场景:将一系列等同的MyISAM表以逻辑方式组合在一起,并作为一个对象引用他们,适合做数据仓库 +* 应用场景:将一系列等同的 MyISAM 表以逻辑方式组合在一起,并作为一个对象引用他们,适合做数据仓库 * 操作方式: - * 插入操作是通过INSERT_METHOD子句定义插入的表,使用 FIRST 或 LAST 值使得插入操作被相应地作用在第一或者最后一个表上;不定义这个子句或者定义为NO,表示不能对MERGE表执行插入操作 - * 对MERGE表进行DROP操作,但是这个操作只是删除MERGE表的定义,对内部的表是没有任何影响的 + * 插入操作是通过 INSERT_METHOD 子句定义插入的表,使用 FIRST 或 LAST 值使得插入操作被相应地作用在第一或者最后一个表上;不定义这个子句或者定义为 NO,表示不能对 MERGE 表执行插入操作 + * 对 MERGE 表进行 DROP 操作,但是这个操作只是删除MERGE表的定义,对内部的表是没有任何影响的 ```mysql CREATE TABLE order_1( @@ -3654,22 +3654,23 @@ MERGE存储引擎: #### 基本介绍 -MySQL官方对索引的定义为:索引(index)是帮助MySQL高效获取数据的一种数据结构。在表数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式指向数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。 +MySQL官方对索引的定义为:索引(index)是帮助 MySQL 高效获取数据的一种数据结构,本质是排好序的快速查找数据结构。在表数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式指向数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。 索引使用:一张数据表,用于保存数据;一个索引配置文件,用于保存索引;每个索引都指向了某一个数据 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-索引的介绍.png) -左边是数据表,一共有两列七条记录,最左边的是数据记录的物理地址(注意逻辑上相邻的记录在磁盘上也并不是一定物理相邻的)。为了加快Col2的查找,可以维护一个右边所示的二叉查找树,每个节点分别包含索引键值和一个指向对应数据的物理地址的指针,这样就可以运用二叉查找快速获取到相应数据 +左边是数据表,一共有两列七条记录,最左边的是数据记录的物理地址(注意逻辑上相邻的记录在磁盘上也并不是一定物理相邻的)。为了加快 Col2 的查找,可以维护一个右边所示的二叉查找树,每个节点分别包含索引键值和一个指向对应数据的物理地址的指针,这样就可以运用二叉查找快速获取到相应数据 索引的优点: -* 类似于书籍的目录索引,提高数据检索的效率,降低数据库的IO成本 -* 通过索引列对数据进行排序,降低数据排序的成本,降低CPU的消耗 +* 类似于书籍的目录索引,提高数据检索的效率,降低数据库的 IO 成本 +* 通过索引列对数据进行排序,降低数据排序的成本,降低 CPU 的消耗 索引的缺点: -* 一般来说索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储在磁盘上 -* 虽然索引大大提高了查询效率,同时却也降低更新表的速度。对表进行INSERT、UPDATE、DELETE操作,MySQL不仅要保存数据,还要保存一下索引文件每次更新添加了索引列的字段,还会调整因为更新所带来的键值变化后的索引信息 +* 一般来说索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式**存储在磁盘**上 +* 虽然索引大大提高了查询效率,同时却也降低更新表的速度。对表进行 INSERT、UPDATE、DELETE 操作,MySQL 不仅要保存数据,还要保存一下索引文件每次更新添加了索引列的字段,还会调整因为更新所带来的键值变化后的索引信息 +* 索引会影响到 WHERE 的查询条件和排序 ORDER BY 两大功能 @@ -3689,7 +3690,7 @@ 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引擎不支持 @@ -3735,21 +3736,21 @@ MySQL官方对索引的定义为:索引(index)是帮助MySQL高效获取 ##### 聚簇索引 -在 Innodb 存储引擎,B+树索引可以分为聚簇索引(也称聚集索引、clustered index)和辅助索引(也称非聚簇索引或二级索引、secondary index、non-clustered index) +在 Innodb 存储引擎,B+ 树索引可以分为聚簇索引(也称聚集索引、clustered index)和辅助索引(也称非聚簇索引或二级索引、secondary index、non-clustered index) -InnoDB中,聚簇索引是按照每张表的主键构造一颗B+树,同时叶子节点中存放的就是整张表的行记录数据,也将聚集索引的叶子节点称为数据页 +InnoDB 中,聚簇索引是按照每张表的主键构造一颗 B+ 树,同时叶子节点中存放的就是整张表的行记录数据,也将聚集索引的叶子节点称为数据页 * 这个特性决定了数据也是索引的一部分,所以一张表只能有一个聚簇索引 * 辅助索引的存在不影响聚簇索引中数据的组织,所以一张表可以有多个辅助索引 聚簇索引的优点: -* 数据访问更快,聚簇索引将索引和数据保存在同一个B+树中,因此从聚簇索引中获取数据比非聚簇索引更快 +* 数据访问更快,聚簇索引将索引和数据保存在同一个 B+ 树中,因此从聚簇索引中获取数据比非聚簇索引更快 * 聚簇索引对于主键的排序查找和范围查找速度非常快 聚簇索引的缺点: -* 插入速度严重依赖于插入顺序,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能,所以对于 InnoDB 表,一般都会定义一个自增的ID列为主键 +* 插入速度严重依赖于插入顺序,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能,所以对于 InnoDB 表,一般都会定义一个自增的 ID 列为主键 * 更新主键的代价很高,将会导致被更新的行移动,所以对于InnoDB表,一般定义主键为不可更新 @@ -3777,13 +3778,13 @@ InnoDB中,聚簇索引是按照每张表的主键构造一颗B+树,同时叶 ##### 索引实现 -InnoDB 使用B+Tree作为索引结构 +InnoDB 使用 B+Tree 作为索引结构 **主键索引:** * 在 InnoDB 中,表数据文件本身就是按 B+Tree 组织的一个索引结构,这个索引的 key 是数据表的主键,叶子节点 data 域保存了完整的数据记录 -* Innodb 的表数据文件**通过主键聚集数据**,如果没有定义主键,会选择非空唯一索引代替,如果也没有这样的列,MySQL 会自动为 InnoDB 表生成一个隐含字段作为主键,这个字段长度为 6 个字节,类型为长整形 +* Innodb 的表数据文件**通过主键聚集数据**,如果没有定义主键,会选择非空唯一索引代替,如果也没有这样的列,MySQL 会自动为 InnoDB 表生成一个**隐含字段**作为主键,这个字段长度为 6 个字节,类型为长整形(MVCC 部分的笔记提及) **辅助索引:** @@ -3805,7 +3806,7 @@ InnoDB 表是基于聚簇索引建立的,因此 InnoDB 的索引能提供一 MyISAM 的主键索引使用的是非聚簇索引,索引文件和数据文件是分离的,索引文件仅保存数据记录的**地址** -* 主键索引B+树的节点存储了主键,辅助键索引B+树存储了辅助键,表数据存储在独立的地方,这两颗B+树的叶子节点都使用一个地址指向真正的表数据,对于表数据来说,这两个键没有任何差别。 +* 主键索引 B+ 树的节点存储了主键,辅助键索引 B+ 树存储了辅助键,表数据存储在独立的地方,这两颗 B+ 树的叶子节点都使用一个地址指向真正的表数据,对于表数据来说,这两个键没有任何差别 * 由于索引树是独立的,通过辅助索引检索无需访问主键的索引树回表查询 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-聚簇索引和辅助索引检锁数据图.jpg) @@ -3822,7 +3823,7 @@ MyISAM 的索引方式也叫做非聚集的,之所以这么称呼是为了与 **主键索引:**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) @@ -4042,18 +4043,19 @@ B+Tree 优点:提高查询速度,减少磁盘的 IO 次数,树形结构较 ### 设计原则 -索引的设计可以遵循一些已有的原则,创建索引的时候请尽量考虑符合这些原则,便于提升索引的使用效率,更高效的使用索引 +索引的设计可以遵循一些已有的原则,创建索引的时候请尽量考虑符合这些原则,便于提升索引的使用效率 创建索引时的原则: -- 对查询频次较高,且数据量比较大的表建立索引。 -- 使用唯一索引,区分度越高,使用索引的效率越高。 -- 索引字段的选择,最佳候选列应当从where子句的条件中提取,如果where子句中的组合比较多,那么应当挑选最常用、过滤效果最好的列的组合。 -- 使用短索引,索引创建之后也是使用硬盘来存储的,因此提升索引访问的I/O效率,也可以提升总体的访问效率。假如构成索引的字段总长度比较短,那么在给定大小的存储块内可以存储更多的索引值,相应的可以有效的提升MySQL访问索引的I/O效率。 -- 索引可以有效的提升查询数据的效率,但索引数量不是多多益善,索引越多,维护索引的代价越高。对于插入、更新、删除等DML操作比较频繁的表来说,索引过多,会引入相当高的维护代价,降低DML操作的效率,增加相应操作的时间消耗。另外索引过多的话,MySQL也会犯选择困难病,虽然最终仍然会找到一个可用的索引,但提高了选择的代价。 - -* MySQL建立联合索引时会遵守最左前缀匹配原则,即最左优先,在检索数据时从联合索引的最左边开始匹配 - N个列组合而成的组合索引,相当于创建了N个索引,如果查询时where句中使用了组成该索引的**前**几个字段,那么这条查询SQL可以利用组合索引来提升查询效率 +- 对查询频次较高,且数据量比较大的表建立索引 +- 使用唯一索引,区分度越高,使用索引的效率越高 +- 索引字段的选择,最佳候选列应当从 where 子句的条件中提取,如果 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); @@ -4065,12 +4067,19 @@ B+Tree 优点:提高查询速度,减少磁盘的 IO 次数,树形结构较 -- 索引的字段可以是任意顺序的,优化器会帮助我们调整顺序,下面的SQL语句可以命中索引 SELECT * FROM user WHERE address = '北京' AND phone = '12345' AND name = '张三'; ``` - + ```mysql -- 如果联合索引中最左边的列不包含在条件查询中,SQL语句就不会命中索引,比如: SELECT * FROM user WHERE address = '北京' AND phone = '12345'; ``` +哪些情况不要建立索引: + +* 记录太少的表 +* 经常增删改的表 +* 频繁更新的字段不适合创建索引 +* where 条件里用不到的字段不创建索引 + *** @@ -4251,7 +4260,7 @@ Innodb_xxxx:这几个参数只是针对InnoDB 存储引擎的,累加的算 通过以下两种方式定位执行效率较低的 SQL 语句 -* 慢日志查询: 慢查询日志在查询结束以后才纪录,执行效率出现问题时查询日志并不能定位问题 +* 慢日志查询: 慢查询日志在查询结束以后才记录,执行效率出现问题时查询日志并不能定位问题 配置文件修改:修改.cnf文件`vim /etc/mysql/my.cnf`,重启MySQL服务器 @@ -4275,20 +4284,20 @@ 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) | 参数 | 含义 | | ------- | ------------------------------------------------------------ | - | ID | 用户登录mysql时系统分配的"connection_id",可以使用函数connection_id()查看 | - | User | 显示当前用户,如果不是root,这个命令就只显示用户权限范围的sql语句 | - | Host | 显示这个语句是从哪个ip的哪个端口上发的,可以用来跟踪出现问题语句的用户 | + | ID | 用户登录mysql时系统分配的"connection_id",可以使用函数 connection_id() 查看 | + | User | 显示当前用户,如果不是 root,这个命令就只显示用户权限范围的 sql 语句 | + | Host | 显示这个语句是从哪个 ip 的哪个端口上发的,可以用来跟踪出现问题语句的用户 | | db | 显示这个进程目前连接的是哪个数据库 | - | Command | 显示当前连接的执行的命令,一般取值为休眠Sleep、查询Query、连接Connect等 | + | Command | 显示当前连接的执行的命令,一般取值为休眠 Sleep、查询 Query、连接 Connect 等 | | Time | 显示这个状态持续的时间,单位是秒 | - | State | 显示使用当前连接的sql语句的状态,以查询为例,需要经过copying to tmp table、sorting result、sending data等状态才可以完成 | - | Info | 显示执行的sql语句,是判断问题语句的一个重要依据 | + | State | 显示使用当前连接的 sql 语句的状态,以查询为例,需要经过 copying to tmp table、sorting result、sending data等状态才可以完成 | + | Info | 显示执行的 sql 语句,是判断问题语句的一个重要依据 | @@ -4304,7 +4313,7 @@ Innodb_xxxx:这几个参数只是针对InnoDB 存储引擎的,累加的算 通过 EXPLAIN 命令获取执行 SQL 语句的信息,包括在 SELECT 语句执行过程中如何连接和连接的顺序 -查询SQL语句的执行计划: +查询 SQL语句的执行计划: ```mysql EXPLAIN SELECT * FROM table_1 WHERE id = 1; @@ -4470,7 +4479,7 @@ key_len: * Using index condition:第一种情况是搜索条件中虽然出现了索引列,但是有部分条件无法使用索引,会根据能用索引的条件先搜索一遍再匹配无法使用索引的条件,回表查询数据;第二种是使用了索引下推 * Using where:表示存储引擎收到记录后进行“后过滤”(Post-filter),如果查询未能使用索引,Using where 的作用是提醒我们 MySQL 将用 WHERE 子句来过滤结果集,即需要回表查询 * Using temporary:表示 MySQL 需要使用临时表来存储结果集,常见于排序和分组查询 -* Using filesort:当 Query 中包含 order by 操作,而且无法利用索引完成的排序操作称为文件排序 +* Using filesort:MySQL 会对数据使用一个外部的索引排序,而不是按照表内的索引顺序进行读取,无法利用索引完成的排序操作称为文件排序 * Using join buffer:说明在获取连接条件时没有使用索引,并且需要连接缓冲区来存储中间结果 * Impossible where:说明 WHERE 语句会导致没有符合条件的行,通过收集统计信息不可能存在结果 * Select tables optimized away:说明仅通过使用索引,优化器可能仅从聚合函数结果中返回一行 @@ -4489,7 +4498,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) @@ -4607,7 +4616,7 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name,status,address); ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引1.png) -* 最左前缀法则:联合索引遵守最左前缀法则 +* **最左前缀法则**:联合索引遵守最左前缀法则 匹配最左前缀法则,走索引: @@ -4635,17 +4644,17 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name,status,address); ![](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 没有用到索引 + 根据前面的两个字段 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) = '科技'; @@ -4653,7 +4662,7 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name,status,address); ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引6.png) -* 字符串不加单引号,造成索引失效: +* **字符串不加单引号**,造成索引失效: 在查询时,没有对字符串加单引号,MySQL 的查询优化器,会自动的进行类型转换,造成索引失效 @@ -4663,7 +4672,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 前后两个列是同一个复合索引,都造成索引失效 @@ -4682,7 +4691,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 **模糊查询**,索引失效: 如果是尾部模糊匹配,索引不会失效;如果是头部模糊匹配,索引失效。 @@ -5105,7 +5114,7 @@ MySQL 4.1版本之后,开始支持 SQL 的子查询 #### 使用提示 -SQL提示,是优化数据库的一个重要手段,就是在SQL语句中加入一些提示来达到优化操作的目的 +SQL 提示,是优化数据库的一个重要手段,就是在SQL语句中加入一些提示来达到优化操作的目的 * USE INDEX:在查询语句中表名的后面,添加 USE INDEX 来提供 MySQL 去参考的索引列表,可以让 MySQL 不再考虑其他可用的索引 @@ -5189,7 +5198,7 @@ SQL提示,是优化数据库的一个重要手段,就是在SQL语句中加 负载均衡是应用中使用非常普遍的一种优化方法,机制就是利用某种均衡算法,将固定的负载量分布到不同的服务器上, 以此来降低单台服务器的负载,达到优化的效果 -* 分流查询:通过MySQL的主从复制,实现读写分离,使增删改操作走主节点,查询操作走从节点,从而可以降低单台服务器的读写压力 +* 分流查询:通过 MySQL 的主从复制,实现读写分离,使增删改操作走主节点,查询操作走从节点,从而可以降低单台服务器的读写压力 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-负载均衡主从复制.jpg) @@ -5205,7 +5214,7 @@ SQL提示,是优化数据库的一个重要手段,就是在SQL语句中加 #### 工作流程 -开启Mysql的查询缓存,当执行完全相同的SQL语句的时候,服务器就会直接从缓存中读取结果,当数据被修改,之前的缓存会失效,修改比较频繁的表不适合做查询缓存 +开启 Mysql 的查询缓存,当执行完全相同的 SQL 语句的时候,服务器就会直接从缓存中读取结果,当数据被修改,之前的缓存会失效,修改比较频繁的表不适合做查询缓存 查询过程: @@ -5225,7 +5234,7 @@ SQL提示,是优化数据库的一个重要手段,就是在SQL语句中加 #### 缓存配置 -1. 查看当前的MySQL数据库是否支持查询缓存: +1. 查看当前的 MySQL 数据库是否支持查询缓存: ```mysql SHOW VARIABLES LIKE 'have_query_cache'; -- YES @@ -5684,7 +5693,7 @@ MySQL 的主从复制原理图: - 排他锁:也叫写锁。当前的操作没有完成前,会阻断其他操作的读取和写入 - 按粒度分类: - 表级锁:会锁定整个表,开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低,偏向MyISAM 存储引擎 - - 行级锁:会锁定当前操作行,开销大,加锁慢;会出现死锁;锁定力度小,发生锁冲突概率低,并发度高,偏向InnoDB 存储引擎 + - 行级锁:会锁定当前操作行,开销大,加锁慢;会出现死锁;锁定力度小,发生锁冲突概率低,并发度高,偏向 InnoDB 存储引擎 - 页级锁:锁的力度、发生冲突的概率和加锁开销介于表锁和行锁之间,会出现死锁,并发性能一般 - 按使用方式分类: - 悲观锁:每次查询数据时都认为别人会修改,很悲观,所以查询时加锁 @@ -5713,7 +5722,7 @@ MySQL 的主从复制原理图: MyISAM 存储引擎只支持表锁,这也是 MySQL 开始几个版本中唯一支持的锁类型 -MyISAM 在执行查询语句前,会自动给涉及的所有表加读锁,在执行更新操作前,会自动给涉及的表加写锁,这个过程并不需要用户干预,所以用户一般不需要直接用 LOCK TABLE 命令给 MyISAM 表显式加锁 +MyISAM 引擎在执行查询语句之前,会**自动**给涉及到的所有表加读锁,在执行增删改之前,会自动给涉及的表加写锁,这个过程并不需要用户干预,所以用户一般不需要直接用 LOCK TABLE 命令给 MyISAM 表显式加锁 * 加锁命令: @@ -5738,12 +5747,12 @@ MyISAM 在执行查询语句前,会自动给涉及的所有表加读锁,在 锁的兼容性: -* 对MyISAM 表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求 -* 对MyISAM 表的写操作,则会阻塞其他用户对同一表的读和写操作 +* 对 MyISAM 表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求 +* 对 MyISAM 表的写操作,则会阻塞其他用户对同一表的读和写操作 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 锁的兼容性.png) -MyISAM 的读写锁调度是写优先,因为写锁后其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永远阻塞,所以 MyISAM 不适合做写为主的表的存储引擎 +锁调度:MyISAM 的读写锁调度是写优先,因为写锁后其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永远阻塞,所以 MyISAM 不适合做写为主的表的存储引擎 @@ -5751,7 +5760,9 @@ MyISAM 的读写锁调度是写优先,因为写锁后其他线程不能做任 -#### 读锁操作 +#### 锁操作 + +##### 读锁 两个客户端操作 Client 1和 Client 2,简化为 C1、C2 @@ -5804,7 +5815,7 @@ MyISAM 的读写锁调度是写优先,因为写锁后其他线程不能做任 -#### 写锁操作 +##### 写锁 两个客户端操作 Client 1和 Client 2,简化为 C1、C2 @@ -5863,9 +5874,9 @@ MyISAM 的读写锁调度是写优先,因为写锁后其他线程不能做任 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 锁状态.png) - Table_locks_immediate:指的是能立即获得表级锁的次数,每立即获取锁,值加1 + Table_locks_immediate:指的是能立即获得表级锁的次数,每立即获取锁,值加 1 - Table_locks_waited:指的是不能立即获取表级锁而需要等待的次数,每等待一次,该值加1,此值高说明存在着较为严重的表级锁争用情况 + Table_locks_waited:指的是不能立即获取表级锁而需要等待的次数,每等待一次,该值加 1,此值高说明存在着较为严重的表级锁争用情况 @@ -5877,14 +5888,12 @@ MyISAM 的读写锁调度是写优先,因为写锁后其他线程不能做任 #### 行级锁 -##### 介绍锁 - -InnoDB 与 MyISAM 的最大不同有两点:一是支持事务;二是采用了行级锁,InnoDB同时支持表锁和行锁 +InnoDB 与 MyISAM 的**最大不同**有两点:一是支持事务;二是采用了行级锁,InnoDB 同时支持表锁和行锁 InnoDB 实现了以下两种类型的行锁: -- 共享锁 (S):又称为读锁,简称S锁,就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改 -- 排他锁 (X):又称为写锁,简称X锁,就是不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,只有获取排他锁的事务是可以对数据读取和修改 +- 共享锁 (S):又称为读锁,简称 S 锁,就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改 +- 排他锁 (X):又称为写锁,简称 X 锁,就是不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,只有获取排他锁的事务是可以对数据读取和修改 对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会自动给涉及数据集加排他锁;对于普通 SELECT 语句,InnoDB 不会加任何锁 @@ -5908,7 +5917,7 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 -##### 锁操作 +#### 锁操作 两个客户端操作 Client 1和 Client 2,简化为 C1、C2 @@ -6040,12 +6049,17 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 当使用范围条件检索数据,并请求共享或排他锁时,InnoDB 会给符合条件的已有数据进行加锁,对于键值在条件范围内但并不存在的记录,叫做间隙(GAP), InnoDB 会对间隙进行加锁,就是间隙锁 -* next-key lock 是行锁和这条记录前面的 gap lock 的组合,就是行锁加间隙锁 * 唯一索引加锁只有在值存在时才是行锁,值不存在会变成间隙锁,所以范围查询时容易出现间隙锁 * 对于联合索引且是唯一索引,如果 where 条件只包括联合索引的一部分,那么会加间隙锁 -* 加锁的基本单位是 next-key lock,前开后闭原则,假设有索引值 10、11、13,那么可能的间隙锁包括:(负无穷,10]、(10,11]、(11,13]、(13,20,正无穷),锁住索引 11 会同时对间隙 (10,11]、(11,13] 加锁 -在 RR 级别下,间隙锁可以解决事务中的**幻读问题**,通过对间隙加锁,可以防止读取过程中数据条目发生变化 +加锁的基本单位是 next-key lock,该锁是行锁和这条记录前面的 gap lock 的组合,就是行锁加间隙锁。 + +* 加锁遵循前开后闭原则 +* 假设有索引值 10、11、13,那么可能的间隙锁包括:(负无穷,10]、(10,11]、(11,13]、(13,20,正无穷),锁住索引 11 会同时对间隙 (10,11]、(11,13] 加锁 + +间隙锁优点:RR 级别下间隙锁可以解决事务的**幻读问题**,通过对间隙加锁,防止读取过程中数据条目发生变化 + +间隙锁危害:当锁定一个范围的键值后,即使某些不存在的键值也会被无辜的锁定,造成在锁定的时候无法插入锁定键值范围内的任何数据,在某些场景下这可能会对性能造成很大的危害 * 关闭自动提交功能: @@ -6070,7 +6084,7 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 间隙锁2.png) - 出现间隙锁,C2 被阻塞,等待C1 提交事务后才能更新 + 出现间隙锁,C2 被阻塞,等待 C1 提交事务后才能更新 @@ -6131,7 +6145,7 @@ SHOW ENGINE INNODB STATUS; #InnoDB整体状态,其中包括锁的情况 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB查看锁状态.png) -lock_id 是锁 id;lock_trx_id 为事务id;lock_mode 为 X 代表排它锁(写锁);lock_type 为 RECORD 代表锁为行锁(记录锁) +lock_id 是锁 id;lock_trx_id 为事务 id;lock_mode 为 X 代表排它锁(写锁);lock_type 为 RECORD 代表锁为行锁(记录锁) @@ -7608,7 +7622,7 @@ public class DataSourceUtils { NoSQL (Not-Only SQL) : 泛指非关系型的数据库,作为关系型数据库的补充。 -MySQL支持ACID特性,保证可靠性和持久性,读取性能不高,因此需要缓存的来减缓数据库的访问压力。 +MySQL 支持 ACID 特性,保证可靠性和持久性,读取性能不高,因此需要缓存的来减缓数据库的访问压力。 作用:应对基于海量用户和海量数据前提下的数据处理问题 @@ -7625,6 +7639,12 @@ MySQL支持ACID特性,保证可靠性和持久性,读取性能不高,因 +参考视频:https://www.bilibili.com/video/BV1CJ411m7Gc + +参考视频:https://www.bilibili.com/video/BV1Rv41177Af + + + *** @@ -7780,21 +7800,13 @@ Redis (REmote DIctionary Server) :用 C 语言开发的一个开源的高性 mkdir data ``` - 创建快速访问链接 - - ```sh - ln -s redis-5.0.0 redis - ``` - -2. 创建配置文件副本放入 conf 目录 - - Ubuntu系统配置文件 redis.conf 在目录 `/etc/redis` 中 +2. 创建配置文件副本放入 conf 目录,Ubuntu系统配置文件 redis.conf 在目录 `/etc/redis` 中 ```sh cat redis.conf | grep -v "#" | grep -v "^$" -> /conf/redis-6379.conf ``` - - 去除配置文件的注释和空格,输出到新的文件,命令方式采用 redis-port号.conf + + 去除配置文件的注释和空格,输出到新的文件,命令方式采用 redis-port.conf @@ -7912,7 +7924,7 @@ dbfilename "dump-6379.rdb" Redis 使用对象来表示数据库中的键和值,当在 Redis 数据库中新创建一个键值对时至少会创建两个对象,一个对象用作键值对的键(键对象),另一个对象用作键值对的值(值对象) -Redis 中对象由一个 redisObject 结构表示,该结构中和保存数据有关的三个属性分别是type、 encoding、ptr: +Redis 中对象由一个 redisObject 结构表示,该结构中和保存数据有关的三个属性分别是 type、 encoding、ptr: ```c typedef struct redisObiect{ @@ -7925,7 +7937,7 @@ typedef struct redisObiect{ } ``` -Redis 中主要数据结构有:简单动态字符串(SDS)、双端链表、字典、压缩列表、整数集合、跳跃表 +Redis 中主要数据结构有:简单动态字符串(SDS)、双端链表、字典、压缩列表、整数集合、跳跃表 Redis 并没有直接使用数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象,而每种对象又通过不同的编码映射到不同的底层数据结构 @@ -7953,17 +7965,22 @@ Redis 基于 Reactor 模式开发了网络事件处理器,这个处理器被 * 当被监听的套接字准备好执行连接应答 (accept)、读取 (read)、写入 (write)、关闭 (close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器会调用套接字之前关联好的事件处理器来处理事件 -Redis单线程也能高效的原因: +Redis 单线程也能高效的原因: * 纯内存操作 +* 核心是基于非阻塞的 IO 多路复用机制 +* 底层使用 C 语言实现,C 语言实现的程序距离操作系统更近,执行速度相对会更快 +* 单线程同时也避免了多线程的上下文频繁切换问题,预防了多线程可能产生的竞争问题 -* 核心是基于非阻塞的IO多路复用机制 -* 底层使用C语言实现,C 语言实现的程序距离操作系统更近,执行速度相对会更快 -* 单线程同时也避免了多线程的上下文频繁切换问题,预防了多线程可能产生的竞争问题 +**** -Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,因为这是 Redis 中的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络),Redis 的多线程只是在网络数据的读写这类耗时操作上使用了, 执行命令仍然是单线程顺序执行,因此不需要担心线程安全问题。 + + +### 多线程 + +Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,因为这是 Redis 中的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络),Redis 的多线程只是用来**处理网络数据的读写和协议解析**, 执行命令仍然是单线程顺序执行,因此不需要担心线程安全问题。 Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启需要修改 redis 配置文件 `redis.conf` : @@ -8054,6 +8071,7 @@ key是一个字符串,通过key获取redis中保存的数据 ```sh del key #删除指定key + unlink key #非阻塞删除key,真正的删除会在后续异步操作 exists key #获取key是否存在 type key #获取key的类型 sort key [ASC/DESC] #对key中数据排序,默认对数字排序,并不更改集合中的数据位置,只是查询 @@ -8069,6 +8087,7 @@ key是一个字符串,通过key获取redis中保存的数据 pexpire key milliseconds #为指定key设置有效期,单位为毫秒 expireat key timestamp #为指定key设置有效期,单位为时间戳 pexpireat key mil-timestamp #为指定key设置有效期,单位为毫秒时间戳 + ttl key #获取key的有效时间,每次获取会自动变化(减小),类似于倒计时, #-1代表永久性,-2代表不存在/失效 pttl key #获取key的有效时间,单位是毫秒,每次获取会自动变化(减小) @@ -8102,9 +8121,9 @@ key是一个字符串,通过key获取redis中保存的数据 ### DB指令 -Redis在使用过程中,伴随着操作数据量的增加,会出现大量的数据以及对应的key,数据不区分种类、类别混在一起,容易引起重复或者冲突 +Redis 在使用过程中,伴随着操作数据量的增加,会出现大量的数据以及对应的 key,数据不区分种类、类别混在一起,容易引起重复或者冲突 -Redis为每个服务提供16个数据库,编码0-15,每个数据库之间的数据相互独立,**共用**Redis内存,不区分大小 +Redis 为每个服务提供 16 个数据库,编码 0-15,每个数据库之间相互独立,**共用 **Redis 内存,不区分大小 * 基本操作 @@ -8123,7 +8142,49 @@ Redis为每个服务提供16个数据库,编码0-15,每个数据库之间的 flushall #清除所有数据 ``` - + + + +**** + + + +### 通信指令 + +Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。 + +Redis 客户端可以订阅任意数量的频道 + +![](https://gitee.com/seazean/images/raw/master/DB/Redis-发布订阅.png) + +操作命令: + +1. 打开一个客户端订阅 channel1:`SUBSCRIBE channel1` +2. 打开另一个客户端,给 channel1发布消息 hello:`publish channel1 hello` +3. 第一个客户端可以看到发送的消息 + + + +注意:发布的消息没有持久化,订阅的客户端只能收到订阅后发布的消息 + + + +**** + + + +### ACL指令 + +Redis ACL 是 Access Control List(访问控制列表)的缩写,该功能允许根据可以执行的命令和可以访问的键来限制某些连接 + +![](https://gitee.com/seazean/images/raw/master/DB/Redis-ACL指令.png) + +* acl cat:查看添加权限指令类别 +* acl whoami:查看当前用户 + +* acl setuser username on >password ~cached:* +get:设置有用户名、密码、ACL 权限(只能 get) + + @@ -8137,7 +8198,7 @@ Redis为每个服务提供16个数据库,编码0-15,每个数据库之间的 #### 简介 -存储的数据:单个数据,最简单的数据存储类型,也是最常用的数据存储类型,实质上是存一个字符串 +存储的数据:单个数据,最简单的数据存储类型,也是最常用的数据存储类型,实质上是存一个字符串,String 类型是二进制安全的,意味着 Redis 的 string 可以包含任何数据,比如图片或者序列化的对象 存储数据的格式:一个存储空间保存一个数据,每一个空间中只能保存一个字符串信息 @@ -8145,6 +8206,8 @@ Redis为每个服务提供16个数据库,编码0-15,每个数据库之间的 +Redis 所有操作都是**原子性**的,采用**单线程**机制,命令是单个顺序执行,无需考虑并发带来影响,原子性就是有一个失败则都失败 + *** @@ -8206,16 +8269,15 @@ Redis为每个服务提供16个数据库,编码0-15,每个数据库之间的 2. 数据未获取到时,对应的数据为(nil),等同于null -3. 数据最大存储量:512MB +3. **数据最大存储量**:512MB -4. string在redis内部存储默认就是一个字符串,当遇到增减类操作incr,decr时**会转成数值型**进行计算 +4. string 在 redis 内部存储默认就是一个字符串,当遇到增减类操作 incr,decr 时**会转成数值型**进行计算 5. 按数值进行操作的数据,如果原始数据不能转成数值,或超越了redis 数值上限范围,将报错 - 9223372036854775807(java中Long型数据最大值,Long.MAX_VALUE) + 9223372036854775807(java 中 Long 型数据最大值,Long.MAX_VALUE) 6. redis 可用于控制数据库表主键id,为数据库表主键提供生成策略,保障数据库表的主键唯一性 -7. Redis 所有操作都是**原子性**的,采用**单线程**机制,命令是单个顺序执行,无需考虑并发带来影响 单数据和多数据的选择: @@ -8236,7 +8298,7 @@ Redis为每个服务提供16个数据库,编码0-15,每个数据库之间的 主页高频访问信息显示控制,例如新浪微博大V主页显示粉丝数与微博数量 -* 在Redis中为大V用户设定用户信息,以用户主键和属性值作为key,后台设定定时刷新策略 +* 在 Redis 中为大V用户设定用户信息,以用户主键和属性值作为 key,后台设定定时刷新策略 ```sh set user:id:3506728370:fans 12210947 @@ -8244,7 +8306,7 @@ Redis为每个服务提供16个数据库,编码0-15,每个数据库之间的 set user:id:3506728370:focuses 83 ``` -* 使用JSON格式保存数据 +* 使用 JSON 格式保存数据 ```sh user:id:3506728370 → {"fans":12210947,"blogs":6164,"focuses":83} @@ -8266,9 +8328,15 @@ Redis为每个服务提供16个数据库,编码0-15,每个数据库之间的 #### 实现 -Redis字符串对象底层的数据结构实现主要是 int 和简单动态字符串 SDS,涉及C语言相关,先不做记录 +Redis 字符串对象底层的数据结构实现主要是 int 和简单动态字符串 SDS,是可以修改的字符串,内部结构实现上类似于 Java 的 ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配 + +![](https://gitee.com/seazean/images/raw/master/DB/Redis-string数据结构.png) + +内部为当前字符串实际分配的空间 capacity 一般要高于实际字符串长度 len,当字符串长度小于 1M 时,扩容都是双倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间。需要注意的是字符串最大长度为 512M + -参考文章:https://www.cnblogs.com/hunternet/p/9957913.html + +详解请参考文章:https://www.cnblogs.com/hunternet/p/9957913.html @@ -8293,7 +8361,7 @@ hash类型:底层使用**哈希表**结构实现数据存储 hash 是指的一个数据类型,并不是一个数据 * 如果 field 数量较少,存储结构优化为**压缩列表结构**(有序) -* 如果 field 数量较多,存储结构使用HashMap结构(无序) +* 如果 field 数量较多,存储结构使用 HashMap 结构(无序) @@ -8358,7 +8426,7 @@ hash 是指的一个数据类型,并不是一个数据 user:id:3506728370 → {"name":"春晚","fans":12210862,"blogs":83} ``` -对于以上数据,使用单条去存的话,存的条数会很多。但如果用json格式,存一条数据就够了。 +对于以上数据,使用单条去存的话,存的条数会很多。但如果用 json 格式,存一条数据就够了。 假如现在粉丝数量发生了变化,要把整个值都改变,但是用单条存就不存在这个问题,只需要改其中一个就可以 @@ -8475,10 +8543,10 @@ list 类型:保存多个数据,底层使用**双向链表**存储结构实 注意事项 -1. list中保存的数据都是string类型的,数据总容量是有限的,最多2^32 - 1 个元素(4294967295) -2. list具有索引的概念,但操作数据时通常以队列的形式进行入队出队,或以栈的形式进行入栈出栈 +1. list 中保存的数据都是 string 类型的,数据总容量是有限的,最多 2^32 - 1 个元素(4294967295) +2. list 具有索引的概念,但操作数据时通常以队列的形式进行入队出队,或以栈的形式进行入栈出栈 3. 获取全部数据操作结束索引设置为 -1 -4. list可以对数据进行分页操作,通常第一页的信息来自于list,第2页及更多的信息通过数据库的形式加载 +4. list 可以对数据进行分页操作,通常第一页的信息来自于 list,第 2 页及更多的信息通过数据库的形式加载 @@ -8490,7 +8558,7 @@ list 类型:保存多个数据,底层使用**双向链表**存储结构实 企业运营过程中,系统将产生出大量的运营数据,如何保障多台服务器操作日志的统一顺序输出? -* 依赖list的数据具有顺序的特征对信息进行管理,右进左查或者左近左查 +* 依赖 list 的数据具有顺序的特征对信息进行管理,右进左查或者左近左查 * 使用队列模型解决多路信息汇总合并的问题 * 使用栈模型解决最新消息的问题 @@ -8506,12 +8574,12 @@ list 类型:保存多个数据,底层使用**双向链表**存储结构实 在 Redis3.2 版本以前列表类型的内部编码有两种:ziplist(压缩列表)和 linkedlist(链表) -列表中存储的数据量比较小的时候,列表就可以采用压缩列表的方式实现: +列表中存储的数据量比较小的时候,列表就会使用一块连续的内存存储,采用压缩列表的方式实现: * 列表中保存的单个数据(有可能是字符串类型的)小于 64 字节 * 列表中数据个数少于 512 个 -在 Redis3.2版本 以后对列表数据结构进行了改造,使用 quicklist(快速列表)代替了 ziplist 和 linkedlist +在 Redis3.2 版本 以后对列表数据结构进行了改造,使用 **quicklist(快速列表)**代替了 linkedlist @@ -8548,7 +8616,7 @@ typedef struct listNode ##### 快速列表 -quicklist 实际上是 ziplist 和 linkedlist 的混合体,将 linkedlist 按段切分,每一段使用 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串接起来 +quicklist 实际上是 ziplist 和 linkedlist 的混合体,将 linkedlist 按段切分,每一段使用 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串接起来,既满足了快速的插入删除性能,又不会出现太大的空间冗余 @@ -8566,7 +8634,7 @@ quicklist 实际上是 ziplist 和 linkedlist 的混合体,将 linkedlist 按 数据存储结构:能够保存大量的数据,高效的内部存储机制,便于查询 -set类型:与hash存储结构完全相同,仅存储键不存储值(nil),并且值是不允许重复且无序的,类似于HashSet +set 类型:与 hash 存储结构哈希表完全相同,只是仅存储键不存储值(nil),所以添加,删除,查找的复杂度都是O(1)**,并且值是**不允许重复且无序**的, @@ -8623,7 +8691,7 @@ set类型:与hash存储结构完全相同,仅存储键不存储值(nil) 注意事项 1. set 类型不允许数据重复,如果添加的数据在 set 中已经存在,将只保留一份 -2. set 虽然与hash的存储结构相同,但是无法启用hash中存储值的空间 +2. set 虽然与 hash 的存储结构相同,但是无法启用 hash 中存储值的空间 @@ -8644,7 +8712,7 @@ set类型:与hash存储结构完全相同,仅存储键不存储值(nil) 解决方案: -* 设定用户鉴别规则,周期性更新满足规则的黑名单加入set集合,用户行为信息达到后与黑名单进行对比 +* 设定用户鉴别规则,周期性更新满足规则的黑名单加入 set 集合,用户行为信息达到后与黑名单进行对比 * 黑名单过滤IP地址:应用于开放游客访问权限的信息源 * 黑名单过滤设备信息:应用于限定访问设备的信息源 * 黑名单过滤用户:应用于基于访问权限的信息源 @@ -8661,7 +8729,7 @@ set类型:与hash存储结构完全相同,仅存储键不存储值(nil) * intset(整数集合):当集合中的元素都是整数且元素个数小于 set-maxintset-entries配置(默认512个)时,Redis 会选用 intset 来作为集合的内部实现,从而减少内存的使用 -* hashtable(哈希表):当集合类型无法满足 intset 条件时,Redis 会使用 hashtable 作为集合的内部实现 +* hashtable(哈希表,字典):当无法满足 intset 条件时,Redis 会使用 hashtable 作为集合的内部实现 整数集合(intset)是 Redis 用于保存整数值的集合抽象数据结构,可以保存类型为 int16_t、int32_t 或者 int64_t的整数值,并且保证集合中的元素是**有序不重复**的 @@ -8681,7 +8749,7 @@ set类型:与hash存储结构完全相同,仅存储键不存储值(nil) 数据存储结构:新的存储模型,可以保存可排序的数据 -sorted_set类型:在set的存储结构基础上添加可排序字段,类似于 TreeSet +sorted_set类型:在 set 的存储结构基础上添加可排序字段,类似于 TreeSet @@ -8711,17 +8779,19 @@ sorted_set类型:在set的存储结构基础上添加可排序字段,类似 ```sh zrange key start stop [WITHSCORES] #获取全部数据,升序,WITHSCORES代表显示分数 zrevrange key start stop [WITHSCORES] #获取全部数据,降序 - zrangebyscore key min max [WITHSCORES] [LIMIT] #按条件获取数据 - zrevrangebyscore key max min [WITHSCORES] #按条件获取数据 + + zrangebyscore key min max [WITHSCORES] [LIMIT offset count] #按条件获取数据,从小到大 + zrevrangebyscore key max min [WITHSCORES] [...] #从大到小 + zcard key #获取集合数据的总量 zcount key min max #获取指定分数区间内的数据总量 zrank key member #获取数据对应的索引(排名)升序 zrevrank key member #获取数据对应的索引(排名)降序 ``` - * min与max用于限定搜索查询的条件 - * start与stop用于限定查询范围,作用于索引,表示开始和结束索引 - * offset与count用于限定查询范围,作用于查询结果,表示开始位置和数据总量 + * min 与 max 用于限定搜索查询的条件 + * start 与 stop 用于限定查询范围,作用于索引,表示开始和结束索引 + * offset 与 count 用于限定查询范围,作用于查询结果,表示开始位置和数据总量 * 集合的交、并操作 @@ -8745,8 +8815,8 @@ sorted_set类型:在set的存储结构基础上添加可排序字段,类似 #### 应用 * 排行榜 -* 对于基于时间线限定的任务处理,将处理时间记录为score值,利用排序功能区分处理的先后顺序 -* 当任务或者消息待处理,形成了任务队列或消息队列时,对于高优先级的任务要保障对其优先处理,采用score记录权重 +* 对于基于时间线限定的任务处理,将处理时间记录为 score 值,利用排序功能区分处理的先后顺序 +* 当任务或者消息待处理,形成了任务队列或消息队列时,对于高优先级的任务要保障对其优先处理,采用 score 记录权重 @@ -8787,7 +8857,7 @@ Redis 使用跳跃表作为有序集合键的底层实现之一,如果一个 -个人笔记:Java → JUC → 并发包 → ConcurrentSkipListMap详解跳跃表 +个人笔记:JUC → 并发包 → ConcurrentSkipListMap详解跳跃表 参考文章:https://www.cnblogs.com/hunternet/p/11248192.html @@ -8801,6 +8871,10 @@ Redis 使用跳跃表作为有序集合键的底层实现之一,如果一个 #### 操作 +Bitmaps 本身不是一种数据类型, 实际上就是字符串(key-value) , 但是它可以对字符串的位进行操作 + +数据结构的详解查看 Java → Algorithm → 位图 + 指令操作: * 获取指定 key 对应偏移量上的 bit 值 @@ -8809,7 +8883,7 @@ Redis 使用跳跃表作为有序集合键的底层实现之一,如果一个 getbit key offset ``` -* 设置指定 key 对应偏移量上的 bit 值,value 只能是1或0 +* 设置指定 key 对应偏移量上的 bit 值,value 只能是 1 或 0 ```sh setbit key offset value @@ -8837,9 +8911,7 @@ Redis 使用跳跃表作为有序集合键的底层实现之一,如果一个 #### 应用 -- 解决Redis缓存穿透,判断给定数据是否存在, 防止缓存穿透 - - 布隆过滤器在 Java.md → SE → 算法 → 位图 部分详解 +- 解决 Redis 缓存穿透,判断给定数据是否存在, 防止缓存穿透 @@ -8857,7 +8929,7 @@ Redis 使用跳跃表作为有序集合键的底层实现之一,如果一个 ### Hyper -基数是数据集去重后元素个数,HyperLogLog 是用来做基数统计的,运用了LogLog的算法 +基数是数据集去重后元素个数,HyperLogLog 是用来做基数统计的,运用了 LogLog 的算法 ```java {1, 3, 5, 7, 5, 7, 8} 基数集: {1, 3, 5 ,7, 8} 基数:5 @@ -8886,7 +8958,7 @@ Redis 使用跳跃表作为有序集合键的底层实现之一,如果一个 应用场景: -* 用于进行基数统计,不是集合不保存数据,只记录数量而不是具体数据 +* 用于进行基数统计,不是集合不保存数据,只记录数量而不是具体数据,比如网站的访问量 * 核心是基数估算算法,最终数值存在一定误差 * 误差范围:基数估计的结果是一个带有 0.81% 标准错误的近似值 * 耗空间极小,每个 hyperloglog key 占用了12K的内存用于标记基数 @@ -8901,7 +8973,7 @@ Redis 使用跳跃表作为有序集合键的底层实现之一,如果一个 ### GEO -GeoHash是一种地址编码方法,把二维的空间经纬度数据编码成一个字符串 +GeoHash 是一种地址编码方法,把二维的空间经纬度数据编码成一个字符串 * 添加坐标点 @@ -8930,7 +9002,7 @@ redis 应用于地理位置计算 -**** +*** @@ -9139,7 +9211,7 @@ bgsave指令工作原理: ![](https://gitee.com/seazean/images/raw/master/DB/Redis-bgsave工作原理.png) -流程:当执行 bgsave 的时候,客户端发出 bgsave 指令给到 redis 服务器,服务器返回后台已经开始执行的信息给客户端,同时使用 fork函数创建一个子进程,让子进程去执行 save 相关的操作,创建 RDB 文件保存起来,操作完以后把结果返回。 +流程:当执行 bgsave 的时候,客户端发出 bgsave 指令给到 redis 服务器,服务器返回后台已经开始执行的信息给客户端,同时使用 fork 函数创建一个子进程,让子进程去执行 save 相关的操作。持久化过程是先将数据写入到一个临时文件中,持久化操作结束再用这个临时文件**替换**上次持久化的文件,在这个过程中主进程是不进行任何 IO 操作的,这确保了极高的性能 bgsave 分成两个过程:第一个是服务端收到指令直接告诉客户端开始执行;另外一个过程是一个子进程在完成后台的保存操作,操作完以后返回消息。**两个进程不相互影响**,所以在持久化期间 Redis 可以正常工作 @@ -9153,12 +9225,12 @@ bgsave 分成两个过程:第一个是服务端收到指令直接告诉客户 #### 自动 -配置文件自动RDB,无需显式调用相关指令,save配置启动后底层执行的是 bgsave 操作 +配置文件自动 RDB,无需显式调用相关指令,save 配置启动后底层执行的是 bgsave 操作 -配置redis.conf: +配置 redis.conf: ```sh -save second changes#设置自动持久化条件,满足限定时间范围内key的变化数量就进行持久化(bgsave) +save second changes #设置自动持久化条件,满足限定时间范围内key的变化数量就进行持久化(bgsave) ``` 参数: @@ -9223,8 +9295,9 @@ RDB三种启动方式对比: - 应用:服务器中每 X 小时执行 bgsave 备份,并将 RDB 文件拷贝到远程机器中,用于灾难恢复 * RDB缺点: - - RDB 方式无论是执行指令还是利用配置,无法做到实时持久化,具有较大的可能性丢失数据 + - bgsave 指令每次运行要执行 fork 操作创建子进程,会牺牲一些性能 + - RDB 方式无论是执行指令还是利用配置,无法做到实时持久化,具有较大的可能性丢失数据,**最后一次持久化后的数据可能丢失** - Redis 的众多版本中未进行 RDB 文件格式的版本统一,可能出现各版本之间数据格式无法兼容 @@ -9237,11 +9310,11 @@ RDB三种启动方式对比: #### 概述 -AOF (append only file) 持久化:以独立日志的方式记录每次写命令,重启时再重新执行AOF文件中命令达到恢复数据的目的,**与RDB相比可以简单理解为由记录数据改为记录数据的变化** +AOF(append only file)持久化:以独立日志的方式记录每次写命令(不记录读),**增量保存**,只许追加文件但不可以改写文件,重启时再重新执行 AOF 文件中命令达到恢复数据的目的,**与 RDB 相比可以简单理解为由记录数据改为记录数据的变化** -AOF 的主要作用是解决了数据持久化的实时性,目前已经是 Redis 持久化的主流方式 +AOF 主要作用是解决了数据持久化的实时性,目前已经是 Redis 持久化的主流方式 -AOF写数据过程: +AOF 写数据过程: @@ -9250,9 +9323,11 @@ AOF写数据过程: -#### 配置 +#### 策略 -启动AOF基本配置: +客户端的请求写命令会被 append 追加到 AOF 缓冲区内 + +启动 AOF 基本配置: ```sh appendonly yes|no #开启AOF持久化功能,默认no,即不开启状态 @@ -9264,21 +9339,23 @@ dir #AOF持久化文件保存路径,与RDB持久化文件路径保持一 appendfsync always|everysec|no #AOF写数据策略:默认为everysec ``` -AOF写数据三种策略(appendfsync) +AOF 写数据三种策略(appendfsync): -- always (每次):每次写入操作均同步到AOF文件中,**数据零误差,性能较低**,不建议使用。 +- always(每次):每次写入操作均同步到 AOF 文件中,**数据零误差,性能较低**,不建议使用。 -- everysec (每秒):每秒将缓冲区中的指令同步到AOF文件中,在系统突然宕机的情况下丢失1秒内的数据 数据**准确性较高,性能较高**,建议使用,也是默认配置 +- everysec(每秒):每秒将缓冲区中的指令同步到 AOF 文件中,在系统突然宕机的情况下丢失1秒内的数据 数据**准确性较高,性能较高**,建议使用,也是默认配置 -- no (系统控制):由操作系统控制每次同步到AOF文件的周期,整体过程**不可控** +- no(系统控制):由操作系统控制每次同步到 AOF 文件的周期,整体过程**不可控** -**AOF缓冲区同步文件策略**,系统调用 write 和 fsync: +**AOF 缓冲区同步文件策略**,系统调用 write 和 fsync: -* write 操作会触发延迟写(delayed write)机制,Linux 在内核提供页缓冲区用来提高硬盘IO性能,write 操作在写入系统缓冲区后直接返回 +* write 操作会触发延迟写(delayed write)机制,Linux 在内核提供页缓冲区用来提高硬盘 IO 性能,write 操作在写入系统缓冲区后直接返回 * 同步硬盘操作依赖于系统调度机制,比如缓冲区页空间写满或达到特定时间周期。同步文件之前,如果此时系统故障宕机,缓冲区内数据将丢失 -* fsync 针对单个文件操作(比如AOF文件)做强制硬盘同步,fsync 将阻塞到写入硬盘完成后返回,保证了数据持久化 +* fsync 针对单个文件操作(比如 AOF 文件)做强制硬盘同步,fsync 将阻塞到写入硬盘完成后返回,保证了数据持久化 + +异常恢复:AOF 文件损坏,通过 **redis-check-aof--fix appendonly.aof** 进行恢复,重启 redis,然后重新加载 @@ -9292,17 +9369,17 @@ AOF写数据三种策略(appendfsync) 随着命令不断写入 AOF,文件会越来越大,为了解决这个问题 Redis 引入了 AOF 重写机制压缩文件体积 -AOF 重写:将Redis进程内的数据转化为写命令同步到**新** AOF 文件的过程,简单说就是将对同一个数据的若干个条命令执行结果转化成最终结果数据对应的指令进行记录 +AOF 重写:将 Redis 进程内的数据转化为写命令同步到**新** AOF 文件的过程,简单说就是将对同一个数据的若干个条命令执行结果转化成最终结果数据对应的指令进行记录 -AOF重写作用: +AOF 重写作用: - 降低磁盘占用量,提高磁盘利用率 -- 提高持久化效率,降低持久化写时间,提高IO性能 +- 提高持久化效率,降低持久化写时间,提高 IO 性能 - 降低数据恢复的用时,提高数据恢复效率 -AOF重写规则: +AOF 重写规则: -- 进程内具有时效性的数据,并且数据已超时将不再写入文件 +- 进程内具有时效性的数据,并且数据已超时将不再写入文件 - 非写入类的无效指令将被忽略,只保留最终数据的写入命令 @@ -9313,7 +9390,9 @@ AOF重写规则: 如 lpushlist1 a、lpush list1 b、lpush list1 c 可以转化为:lpush list1 a b c。 - 为防止数据量过大造成客户端缓冲区溢出,对list、set、hash、zset等类型,每条指令最多写入64个元素 + 为防止数据量过大造成客户端缓冲区溢出,对 list、set、hash、zset 等类型,每条指令最多写入64个元素 + + @@ -9335,20 +9414,22 @@ AOF重写规则: * 自动重写 + 触发时机:Redis 会记录上次重写时的 AOF 大小,默认配置是当 AOF 文件大小是上次重写后大小的一倍且文件大于 64M 时触发 + ```sh - auto-aof-rewrite-min-size size #触发AOF文件执行重写的最小尺寸,2MB、4MB等 - auto-aof-rewrite-percentage percent #触发AOF文件执行重写的增长率,当前AOF文件大小超过上一次重写的AOF文件大小的百分之多少才会重写 + auto-aof-rewrite-min-size size #设置重写的基准值,最小文件 64MB,达到这个值开始重写 + auto-aof-rewrite-percentage percent #触发AOF文件执行重写的增长率,当前AOF文件大小超过上一次重写的AOF文件大小的百分之多少才会重写,比如文件达到 100% 时开始重写就是两倍时触发 ``` 自动重写触发比对参数( 运行指令 `info Persistence` 获取具体信息 ): - + ```sh aof_current_size #AOF文件当前尺寸大小(单位:字节) aof_base_size #AOF文件上次启动和重写时的尺寸大小(单位:字节) ``` 自动重写触发条件公式: - + - aof_current_size > auto-aof-rewrite-min-size - (aof_current_size - aof_base_size) / aof_base_size >= auto-aof-rewrite-percentage @@ -9370,15 +9451,19 @@ AOF重写规则: +使用**新的 AOF 文件覆盖旧的 AOF 文件**,完成 AOF 重写 + **** -#### 对比 +### 对比 - RDB与AOF对比: +AOF 和 RDB 同时开启,系统默认取 AOF 的数据(数据不会存在丢失) + + RDB 与 AOF 对比: | 持久化方式 | RDB | AOF | | ------------ | ------------------ | ------------------ | @@ -9391,13 +9476,13 @@ AOF重写规则: 应用场景: -- 对数据非常敏感,建议使用默认的 AOF 持久化方案 +- 对数据**非常敏感**,建议使用默认的 AOF 持久化方案 AOF 持久化策略使用 everysecond,每秒钟 fsync 一次,该策略 redis 仍可以保持很好的处理性能,当出现问题时,最多丢失0-1秒内的数据。 注意:AOF文件存储体积较大,恢复速度较慢,因为要执行每条指令 -- 数据呈现阶段有效性,建议使用 RDB 持久化方案 +- 数据呈现**阶段有效性**,建议使用 RDB 持久化方案 数据可以良好的做到阶段内无丢失,且恢复速度较快,阶段内数据恢复通常采用 RDB 方案 @@ -9409,7 +9494,8 @@ AOF重写规则: - 如不能承受数分钟以内的数据丢失,对业务数据非常敏感,选用 AOF - 如能承受数分钟以内的数据丢失,且追求大数据集的恢复速度,选用 RDB - 灾难恢复选用 RDB -- 双保险策略,同时开启 RDB和 AOF,重启后,Redis 优先使用 AOF 来恢复数据,降低丢失数据的量 +- 双保险策略,同时开启 RDB和 AOF,重启后 Redis 优先使用 AOF 来恢复数据,降低丢失数据的量 +- 不建议单独用 AOF,因为可能会出现 Bug,如果只是做纯内存缓存,可以都不用 @@ -9423,7 +9509,7 @@ AOF重写规则: fork() 函数创建一个子进程,子进程与父进程几乎是完全相同的进程,系统先给子进程分配资源,然后把原来的进程的所有数据都复制到子进程中,只有少数值与父进程的值不同,相当于克隆了一个进程 -在完成对其调用之后,会产生2个进程,且每个进程都会**从fork()的返回处开始执行**,这两个进程将执行相同的程序段,但是拥有各自不同的堆段,栈段,数据段,每个子进程都可修改各自的数据段,堆段,和栈段 +在完成对其调用之后,会产生2个进程,且每个进程都会**从 fork() 的返回处开始执行**,这两个进程将执行相同的程序段,但是拥有各自不同的堆段,栈段,数据段,每个子进程都可修改各自的数据段,堆段,和栈段 ```c #include @@ -9539,7 +9625,7 @@ fork() 调用之后父子进程的内存关系 * 对于父进程的数据段,堆段,栈段中的各页,由于父子进程要相互独立,采用**写时复制**的技术,来最大化的提高内存以及内核的利用率 - 在fork之后两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,**两者的虚拟空间不同,但其对应的物理空间是同一个**。当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间,如果两者的代码完全相同,代码段继续共享父进程的物理空间;而如果两者执行的代码不同,子进程的代码段也会分配单独的物理空间。 + 在 fork 之后两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,**两者的虚拟空间不同,但其对应的物理空间是同一个**。当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间,如果两者的代码完全相同,代码段继续共享父进程的物理空间;而如果两者执行的代码不同,子进程的代码段也会分配单独的物理空间。 fork之后内核会将子进程放在队列的前面,让子进程先执行,以免父进程执行导致写时复制,而后子进程再执行,因无意义的复制而造成效率的下降 @@ -9547,7 +9633,7 @@ fork() 调用之后父子进程的内存关系 补充知识: -vfork(虚拟内存fork virtual memory fork):调用 vfork() 父进程被挂起,子进程使用父进程的地址空间。不采用写时复制,如果子进程修改父地址空间的任何页面,这些修改过的页面对于恢复的父进程是可见的 +vfork(虚拟内存 fork virtual memory fork):调用 vfork() 父进程被挂起,子进程使用父进程的地址空间。不采用写时复制,如果子进程修改父地址空间的任何页面,这些修改过的页面对于恢复的父进程是可见的 @@ -9565,9 +9651,7 @@ vfork(虚拟内存fork virtual memory fork):调用 vfork() 父进程被挂 ### 基本操作 - Redis 事务没有隔离级别的概念,Redis 单条命令式保存原子性的,但是事务不保证原子性 - -Redis 事务就是一个命令执行的队列,将一系列预定义命令包装成一个整体(一个队列)。当执行时按照添加顺序依次执行,中间不会被打断或者干扰 +Redis 事务的主要作用就是串联多个命令防止别的命令插队 * 开启事务 @@ -9589,7 +9673,13 @@ Redis 事务就是一个命令执行的队列,将一系列预定义命令包 discard #终止当前事务的定义,发生在multi之后,exec之前 ``` - 一般用于事务执行过程中输入了错误的指令,直接取消这次事务 + 一般用于事务执行过程中输入了错误的指令,直接取消这次事务,类似于回滚 + +Redis 事务的三大特性: + +* Redis 事务是一个单独的隔离操作,将一系列预定义命令包装成一个整体(一个队列),当执行时按照添加顺序依次执行,中间不会被打断或者干扰 +* Redis 事务**没有隔离级别**的概念,队列中的命令在事务没有提交之前都不会实际被执行 +* Redis 单条命令式保存原子性的,但是事务不保证原子性,事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚 @@ -9605,7 +9695,7 @@ Redis 事务就是一个命令执行的队列,将一系列预定义命令包 几种常见错误: -* 定义事务的过程中,命令格式输入错误,出现语法错误造成,整体事务中所有命令均不会执行,包括那些语法正确的命令,事务直接消失 +* 定义事务的过程中,命令格式输入错误,出现语法错误造成,**整体事务中所有命令均不会执行**,包括那些语法正确的命令 @@ -9622,7 +9712,7 @@ Redis 事务就是一个命令执行的队列,将一系列预定义命令包 设置指令恢复所有的被修改的项 - * 单数据:直接set(注意周边属性,例如时效) + * 单数据:直接 set(注意周边属性,例如时效) * 多数据:修改对应值或整体克隆复制 @@ -9633,7 +9723,7 @@ Redis 事务就是一个命令执行的队列,将一系列预定义命令包 ### 监控锁 -对 key 添加监视锁,在执行 exec 前如果其他客户端的操作导致 key 发生了变化,执行结果为 nil +对 key 添加监视锁,是一种乐观锁,在执行 exec 前如果其他客户端的操作导致 key 发生了变化,执行结果为 nil * 添加监控锁 @@ -9657,7 +9747,11 @@ Redis 事务就是一个命令执行的队列,将一系列预定义命令包 ### 分布式锁 -Redis 分布式锁的基本使用 +#### 基本操作 + +由于分布式系统多线程并发分布在不同机器上,这将使单机部署情况下的并发控制锁策略失效,需要分布式锁 + +Redis 分布式锁的基本使用,悲观锁 * 使用 setnx 设置一个公共锁 @@ -9668,6 +9762,10 @@ Redis 分布式锁的基本使用 * 对于返回设置成功的,拥有控制权,进行下一步的具体业务操作 * 对于返回设置失败的,不具有控制权,排队或等待 + NX:只在键不存在时,才对键进行设置操作,`SET key value NX` 效果等同于 `SETNX key value` + + XX :只在键已经存在时,才对键进行设置操作 + * 操作完毕通过 del 操作释放锁 ```sh @@ -9680,11 +9778,33 @@ Redis 分布式锁的基本使用 expire lock-key second pexpire lock-key milliseconds ``` + + 通过 expire 设置过期时间缺乏原子性,如果在 setnx 和 expire 之间出现异常,锁也无法释放 + +* 在 set 时指定过期时间 + + ```sh + SET key value [EX seconds | PX milliseconds] NX 应用:解决抢购时出现超卖现象 +**** + + + +#### 防误删 + +setnx 获取锁时,设置一个指定的唯一值(uuid),释放前获取这个值,判断是否自己的锁,防止出现线程之间误删了其他线程的锁 + +```java +// 加锁, unique_value作为客户端唯一性的标识 +SET lock_key unique_value NX PX 10000 +``` + +unique_value 是客户端的唯一标识,可以用一个随机生成的字符串来表示,PX 10000 则表示 lock_key 会在 10s 后过期,以免客户端在这期间发生异常而无法释放锁 + *** @@ -9719,9 +9839,9 @@ TTL 返回的值有三种情况:正数,-1,-2 #### 删除策略 -在内存占用与CPU占用之间寻找一种平衡,顾此失彼都会造成整体redis性能的下降,甚至引发服务器宕机或内存泄露 +在内存占用与 CPU 占用之间寻找一种平衡,顾此失彼都会造成整体 redis 性能的下降,甚至引发服务器宕机或内存泄露 -针对过期数据有三种删除策略 +针对过期数据有三种删除策略: - 定时删除 - 惰性删除 @@ -9735,10 +9855,10 @@ TTL 返回的值有三种情况:正数,-1,-2 #### 定时删除 -创建一个定时器,当key设置有过期时间,且过期时间到达时,由定时器任务立即执行对键的删除操作 +创建一个定时器,当 key 设置有过期时间,且过期时间到达时,由定时器任务立即执行对键的删除操作 - 优点:节约内存,到时就删除,快速释放掉不必要的内存占用 -- 缺点:CPU压力很大,无论CPU此时负载量多高,均占用CPU,会影响redis服务器响应时间和指令吞吐量 +- 缺点:无论 CPU 此时负载量多高,均占用 CPU,会影响 redis 服务器响应时间和指令吞吐量 - 总结:用处理器性能换取存储空间(拿时间换空间) @@ -9913,24 +10033,34 @@ TTL 返回的值有三种情况:正数,-1,-2 主从复制: -* 概念:将master中的数据即时、有效的复制到slave中 -* 特征:一个master可以拥有多个slave,一个slave只对应一个master -* 职责:master和slave各自的职责不一样 +* 概念:将 master 中的数据即时、有效的复制到 slave 中 +* 特征:一个 master 可以拥有多个 slave,一个slave 只对应一个 master +* 职责:master 和 slave 各自的职责不一样 master: - * **写数据**,执行写操作时,将出现变化的数据自动同步到slave + * **写数据**,执行写操作时,将出现变化的数据自动同步到 slave * 读数据(可忽略) slave * **读数据** * 写数据(禁止) +主从复制的机制: + +* **薪火相传**:一个 slave 可以是下一个 slave 的 master,slave 同样可以接收其他 slave 的连接和同步请求,那么该 slave 作为了链条中下一个的 master, 可以有效减轻 master 的写压力,去中心化降低风险 + + 注意:主机挂了,从机还是从机,无法写数据了 + +* **反客为主**:当一个 master 宕机后,后面的 slave 可以立刻升为 master,其后面的 slave 不做任何修改 + + 将从机变为主机的命令:`slaveof no one` + 主从复制的作用: -- 读写分离:master 写、slave 读,提高服务器的读写负载能力 -- 负载均衡:基于主从结构,配合读写分离,由slave分担master负载,并根据需求的变化,改变slave的数量,通过多个从节点分担数据读取负载,大大提高Redis服务器并发量与数据吞吐量 -- 故障恢复:当 master 出现问题时,由 slave 提供服务,实现快速的故障恢复 -- 数据冗余:实现数据热备份,是持久化之外的一种数据冗余方式 +- **读写分离**:master 写、slave 读,提高服务器的读写负载能力 +- **负载均衡**:基于主从结构,配合读写分离,由 slave 分担 master 负载,并根据需求的变化,改变 slave 的数量,通过多个从节点分担数据读取负载,大大提高 Redis 服务器并发量与数据吞吐量 +- **故障恢复**:当 master 出现问题时,由 slave 提供服务,实现快速的故障恢复 +- **数据冗余**:实现数据热备份,是持久化之外的一种数据冗余方式 - 高可用基石:基于主从复制,构建哨兵模式与集群,实现 Redis 的高可用方案 主从复制的应用场景: @@ -9986,7 +10116,7 @@ TTL 返回的值有三种情况:正数,-1,-2 * master:保存 slave 的端口 -* 主从之间创建了连接的socket +* 主从之间创建了连接的 socket @@ -9998,7 +10128,7 @@ TTL 返回的值有三种情况:正数,-1,-2 #### 相关指令 -* master和slave互联 +* master 和 slave 互联 方式一:客户端发送命令 @@ -10030,6 +10160,12 @@ TTL 返回的值有三种情况:正数,-1,-2 ```sh uslave_listening_port(多个) ``` + + * 系统信息: + + ```sh + info replication + ``` * 主从断开连接 @@ -10094,8 +10230,8 @@ TTL 返回的值有三种情况:正数,-1,-2 同步过程如下: 1. 请求同步数据 -2. 创建RDB同步数据 -3. 恢复RDB同步数据 +2. 创建 RDB 同步数据 +3. 恢复 RDB 同步数据(清空原有数据) 4. 请求部分同步数据 5. 恢复部分同步数据 6. 数据同步工作完成 @@ -10122,7 +10258,7 @@ TTL 返回的值有三种情况:正数,-1,-2 1. master 数据量巨大,数据同步阶段应避开流量高峰期,避免造成 master 阻塞,影响业务正常执行 - 2. 复制缓冲区大小设定不合理,会导致**数据溢出**。比如进行全量复制周期太长,进行部分复制时发现数据已经存在丢失的情况,必须进行第二次全量复制,致使slave陷入死循环状态 + 2. 复制缓冲区大小设定不合理,会导致**数据溢出**。比如进行全量复制周期太长,进行部分复制时发现数据已经存在丢失的情况,必须进行第二次全量复制,致使 slave 陷入死循环状态 ```sh repl-backlog-size ?mb @@ -10136,9 +10272,9 @@ TTL 返回的值有三种情况:正数,-1,-2 3. master 单机内存占用主机内存的比例不应过大,建议使用 50%-70% 的内存,留下 30%-50% 的内存用于执行 bgsave 命令和创建复制缓冲区 -* 数据同步阶段slave说明 +* 数据同步阶段 slave 说明 - 1. 为避免slave进行全量复制、部分复制时服务器响应阻塞或数据不同步,建议关闭此期间的对外服务 + 1. 为避免 slave 进行全量复制、部分复制时服务器响应阻塞或数据不同步,建议关闭此期间的对外服务 ```sh slave-serve-stale-data yes|no @@ -10148,7 +10284,7 @@ TTL 返回的值有三种情况:正数,-1,-2 3. 多个 slave 同时对 master 请求数据同步,master 发送的 RDB 文件增多,会对带宽造成巨大冲击,如果master 带宽不足,因此数据同步需要根据业务需求,适量错峰 - 4. slave 过多时,建议调整拓扑结构,由一主多从结构变为树状结构,中间的节点既是 master,也是slave。注意使用树状结构时,由于层级深度,导致深度越高的slave与最顶层master间数据同步延迟较大,数据一致性变差,应谨慎选择 + 4. slave 过多时,建议调整拓扑结构,由一主多从结构变为树状结构,中间的节点既是 master,也是slave。注意使用树状结构时,由于层级深度,导致深度越高的 slave 与最顶层 master 间数据同步延迟较大,数据一致性变差,应谨慎选择 @@ -10172,11 +10308,11 @@ TTL 返回的值有三种情况:正数,-1,-2 部分复制的三个核心要素:服务器的运行 id(run id)、主服务器的复制积压缓冲区、主从服务器的复制偏移量 -* 服务器运行ID(runid):服务器运行ID是每一台服务器每次运行的身份识别码,一台服务器多次运行可以生成多个运行id,由40位字符组成,是一个随机的十六进制字符 +* 服务器运行ID(runid):服务器运行 ID 是每一台服务器每次运行的身份识别码,一台服务器多次运行可以生成多个运行 ID,由 40 位字符组成,是一个随机的十六进制字符 - 作用:用于在服务器间进行传输识别身份,如果想两次操作均对同一台服务器进行,必须每次操作携带对应的运行id,用于对方识别 + 作用:用于在服务器间进行传输识别身份,如果想两次操作均对同一台服务器进行,必须每次操作携带对应的运行 ID,用于对方识别 - 实现:运行id在每台服务器启动时自动生成,master 在首次连接 slave 时,将运行ID发送给 slave,slave保存此ID,通过 info Server 命令,可以查看节点的 runid + 实现:运行 ID 在每台服务器启动时自动生成,master 在首次连接 slave 时,将运行 ID 发送给 slave,slave保存此ID,通过 info Server 命令,可以查看节点的 runid * 复制缓冲区:复制积压缓冲区,是一个先进先出(FIFO)的队列,用于存储服务器执行过的命令 @@ -10186,8 +10322,8 @@ TTL 返回的值有三种情况:正数,-1,-2 * 复制偏移量:一个数字,描述复制缓冲区中的指令字节位置 - - master复制偏移量:记录发送给所有slave的指令字节对应的位置(多个) - - slave复制偏移量:记录slave接收master发送过来的指令字节对应的位置(一个) + - master 复制偏移量:记录发送给所有 slave 的指令字节对应的位置(多个) + - slave 复制偏移量:记录 slave 接收 master 发送过来的指令字节对应的位置(一个) 作用:同步信息,比对 master 与 slave 的差异,当 slave 断线后,恢复数据使用 @@ -10228,14 +10364,14 @@ TTL 返回的值有三种情况:正数,-1,-2 心跳机制:进入命令传播阶段,master 与 slave 间需要信息交换,使用心跳机制维护,实现双方连接保持在线 -master心跳任务: +master 心跳任务: - 内部指令:PING - 周期:由 `repl-ping-slave-period` 决定,默认10秒 - 作用:判断 slave 是否在线 - 查询:INFO replication 获取 slave 最后一次连接时间间隔,lag 项维持在0或1视为正常 -slave心跳任务 +slave 心跳任务 - 内部指令:REPLCONF ACK {offset} - 周期:1秒 @@ -10265,7 +10401,7 @@ slave心跳任务 ### 常见问题 -#### 全量复制 +#### 重启恢复 系统不断运行,master 的数据量会越来越大,一旦 master 重启,runid 将发生变化,会导致全部 slave 的全量复制操作 @@ -10281,12 +10417,12 @@ slave心跳任务 * master 重启后加载 RDB 文件,恢复数据 - 重启后,将RDB文件中保存的repl-id与repl-offset加载到内存中 + 重启后,将RDB文件中保存的 repl-id 与 repl-offset 加载到内存中 * master_repl_id = repl-id,master_repl_offset = repl-offset - * 通过info命令可以查看该信息 - + * 通过 info 命令可以查看该信息 + *** @@ -10298,12 +10434,12 @@ master 的 CPU 占用过高或 slave 频繁断开连接 * 出现的原因: * slave 每1秒发送 REPLCONF ACK 命令到 master - * 当slave接到了慢查询时(keys * ,hgetall等),会大量占用CPU性能 - * master每1秒调用复制定时函数replicationCron(),比对slave发现长时间没有进行响应 + * 当 slave 接到了慢查询时(keys * ,hgetall等),会大量占用 CPU 性能 + * master 每1秒调用复制定时函数 replicationCron(),比对 slave 发现长时间没有进行响应 最终导致 master 各种资源(输出缓冲区、带宽、连接等)被严重占用 -* 解决方法:通过设置合理的超时时间,确认是否释放slave +* 解决方法:通过设置合理的超时时间,确认是否释放 slave ```sh repl-timeout # 该参数定义了超时时间的阈值(默认60秒),超过该值,释放slave @@ -10316,7 +10452,7 @@ slave 与 master 连接断开 * master 设定超时时间较短 * ping 指令在网络中存在丢包 -* 解决方法:提高ping指令发送的频度 +* 解决方法:提高 ping 指令发送的频度 ```sh repl-ping-slave-period @@ -10344,7 +10480,7 @@ slave 与 master 连接断开 slave-serve-stale-data yes|no ``` - 开启后仅响应info、slaveof等少数命令(慎用,除非对数据一致性要求很高) + 开启后仅响应 info、slaveof 等少数命令(慎用,除非对数据一致性要求很高) @@ -10505,35 +10641,34 @@ sentinel 在通知阶段不断的去获取 master/slave 的信息,然后在各 #### 故障转移 -当master宕机后,sentinel会判断出master是否真的宕机,具体的操作流程: +当 master 宕机后,sentinel 会判断出 master 是否真的宕机,具体的操作流程: -* 检测master +* 检测 master - sentinel1 检测到 master 下线后会做 flag:SRI_S_DOWN 标志,此时 master 的状态是主观下线,并通知其他哨兵,其他哨兵也会尝试与 master 连接,如果大于 (n/2) + 1 个sentinel检测到master下线,就达成共识更改flag,此时master的状态是客观下线 + sentinel1 检测到 master 下线后会做 flag:SRI_S_DOWN 标志,此时 master 的状态是主观下线,并通知其他哨兵,其他哨兵也会尝试与 master 连接,如果大于 (n/2) + 1 个sentinel 检测到 master 下线,就达成共识更改 flag,此时 master 的状态是客观下线 ![](https://gitee.com/seazean/images/raw/master/DB/Redis-哨兵模式故障转移工作流程1.png) * 当 sentinel 认定 master 下线之后,此时需要决定更换 master,选举某个 sentinel 处理事故 - 在选举的时候每一个sentinel都有一票,于是每个sentinel都会发出一个指令,在内网里广播我要做话事人;比如sentinel1和sentinel4发出这个选举指令了,那么sentinel2既能接到sentinel1的也能接到sentinel4的,sentinel2会把一票投给其中一方,投给指令最先到达的sentinel;现在sentinel1就拿到了一票,按照这样的一种形式,最终会有一个选举结果,对应的选举最终得票多的,那自然就成为了处理事故的人。需要注意在这个过程中有可能会存在失败的现象,就是一轮选举完没有选取,那就会接着进行第二轮第三轮直到完成选举。 + 在选举的时候每一个 sentinel 都有一票,于是每个 sentinel 都会发出一个指令,在内网广播要做主持人;比如 sentinel1 和 sentinel4 发出这个选举指令了,那么 sentinel2 既能接到 sentinel1 的也能接到 sentinel4 的,sentinel2 会把一票投给其中一方,投给指令最先到达的 sentinel。选举最终得票多的,就成为了处理事故的哨兵,需要注意在这个过程中有可能会存在失败的现象,就是一轮选举完没有选取,那就会接着进行第二轮第三轮直到完成选举。 ![](https://gitee.com/seazean/images/raw/master/DB/Redis-哨兵模式故障转移工作流程2.png) -选择新的master,在服务器列表中挑选备选master的原则: - -- 不在线的OUT +选择新的 master,在服务器列表中挑选备选 master 的原则: - - 响应慢的OUT +- 不在线的 OUT - - 与原master断开时间久的OUT + - 响应慢的 OUT - - 优先原则:优先级 --> offset --> runid + - 与原 master 断开时间久的 OUT -选出新的master之后,发送指令( sentinel )给其他的slave + - 优先原则:优先级 → offset → runid -* 向新的master发送slaveof no one +选出新的 master之后,发送指令(sentinel )给其他的 slave - * 向其他slave发送slaveof 新masterIP端口 +* 向新的 master 发送 slaveof no one +* 向其他 slave 发送 slaveof 新 masterIP 端口 @@ -10763,13 +10898,13 @@ 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 也会出现问题,但是概率很小,因为缓存的写入速度非常快 旁路缓存的缺点: -* 首次请求数据一定不在 cache 的问题,一般采用缓存预热的方法,将热点数据可以提前放入cache 中 +* 首次请求数据一定不在 cache 的问题,一般采用缓存预热的方法,将热点数据可以提前放入 cache 中 * 写操作比较频繁的话导致 cache 中的数据会被频繁被删除,影响缓存命中率 @@ -10800,7 +10935,7 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 异步缓存写入 Write Behind Pattern 由 cache 服务来负责 cache 和 DB 的读写,对比读写穿透不同的是 Write Behind Caching 是只更新缓存,不直接更新 DB,改为**异步批量**的方式来更新 DB,可以减小写的成本 -缺点:这种模式对数据一致性没有高要求,可能出现 cache 还没异步更新DB,服务就挂掉了 +缺点:这种模式对数据一致性没有高要求,可能出现 cache 还没异步更新 DB,服务就挂掉了 应用: @@ -10884,9 +11019,9 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 #### 缓存雪崩 -场景:数据库服务器崩溃,一连串的问题会随之而来。系统平稳运行过程中,忽然数据库连接量激增,应用服务器无法及时处理请求,大量408,500错误页面出现,客户反复刷新页面获取数据,造成数据库崩溃、应用服务器崩溃、重启应用服务器无效、Redis 服务器崩溃、Redis 集群崩溃、重启数据库后再次被瞬间流量放倒 +场景:数据库服务器崩溃,一连串的问题会随之而来。系统平稳运行过程中,忽然数据库连接量激增,应用服务器无法及时处理请求,大量 408,500 错误页面出现,客户反复刷新页面获取数据,造成数据库崩溃、应用服务器崩溃、重启应用服务器无效、Redis 服务器崩溃、Redis 集群崩溃、重启数据库后再次被瞬间流量放倒 -问题排查:在一个较短的时间内,缓存中较多的 key 集中过期,此周期内请求访问过期的数据,Redis 未命中,Redis 向数据库获取数据,数据库同时接收到大量的请求无法及时处理;Redis 大量请求被积压,开始出现超时现象;数据库流量激增,数据库崩溃,重启后仍然面对缓存中无数据可用;Redis 服务器资源被严重占用,Redis 服务器崩溃;Redis 集群呈现崩塌,集群瓦解;应用服务器无法及时得到数据响应请求,来自客户端的请求数量越来越多,应用服务器崩溃;应用服务器,redis,数据库全部重启,效果不理想 +问题排查:在一个较短的时间内,缓存中**较多的 key 集中过期**,此周期内请求访问过期的数据 Redis 未命中,Redis 向数据库获取数据,数据库同时收到大量的请求无法及时处理。 解决方案: @@ -10898,7 +11033,7 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 3. 检测 Mysql 严重耗时业务进行优化:对数据库的瓶颈排查,例如超时查询、耗时较高事务等 - 4. 灾难预警机制:监控redis服务器性能指标,CPU占用、CPU使用率、内存容量、平均响应时间、线程数 + 4. 灾难预警机制:监控 redis 服务器性能指标,CPU 使用率、内存容量、平均响应时间、线程数 5. 限流、降级:短时间范围内牺牲一些客户体验,限制一部分请求访问,降低应用服务器压力,待业务低速运转后再逐步放开访问 @@ -10906,9 +11041,9 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 1. LRU 与 LFU切换 - 2. 数据有效期策略调整:根据业务数据有效期进行分类错峰,A类90分钟,B类80分钟,C类70分钟,过期时间使用固定时间 + 随机值的形式,稀释集中到期的 key 的数量 + 2. 数据有效期策略调整:根据业务数据有效期进行分类错峰,A 类 90 分钟,B 类 80 分钟,C 类 70 分钟,过期时间使用固定时间 + 随机值的形式,稀释集中到期的 key 的数量 - 3. 超热数据使用永久key + 3. 超热数据使用永久 key 4. 定期维护:对即将过期数据做访问量分析,确认是否延时,配合访问量统计,做热点数据的延时 @@ -10980,7 +11115,7 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 2. 白名单策略:提前预热各种分类数据 id 对应的 **bitmaps**,id 作为 bitmaps 的 offset,相当于设置了数据白名单。当加载正常数据时放行,加载异常数据时直接拦截(效率偏低),也可以使用布隆过滤器(有关布隆过滤器的命中问题对当前状况可以忽略) -3. 实施监控:实时监控 redis 命中率(业务正常范围时,通常会有一个波动值)与null数据的占比 +3. 实时监控:实时监控 redis 命中率(业务正常范围时,通常会有一个波动值)与null数据的占比 * 非活动时段波动:通常检测3-5倍,超过5倍纳入重点排查对象 * 活动时段波动:通常检测10-50倍,超过50倍纳入重点排查对象 diff --git a/Java.md b/Java.md index 9737806..b5370b8 100644 --- a/Java.md +++ b/Java.md @@ -19,6 +19,10 @@ +参考视频:https://www.bilibili.com/video/BV1TE41177mP + + + *** @@ -2526,7 +2530,7 @@ s.replace("-","");//12378 **intern()** : * jdk1.8:当一个字符串调用 intern() 方法时,如果 String Pool 中: - * 存在一个字符串和该字符串值相等(使用 equals() 方法进行确定),就会返回 String Pool 中字符串的引用(需要变量接收) + * 存在一个字符串和该字符串值相等,就会返回 String Pool 中字符串的引用(需要变量接收) * 不存在,会把对象的**引用地址**复制一份放入串池,并返回串池中的引用地址,前提是堆内存有该对象,因为 Pool 在堆中,为了节省内存不再创建新对象 * jdk1.6:将这个字符串对象尝试放入串池,如果有就不放入,返回已有的串池中的对象的地址;如果没有会把此对象复制一份,放入串池,把串池中的对象返回 @@ -3350,9 +3354,9 @@ 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对象当前有多个捕获组。 +* 调用 matcher 对象的 groupCount 方法返回一个 int 值,表示 matcher 对象当前有多个捕获组。 * 特殊的组group(0)、group(),它代表整个表达式,该组不包括在 groupCount 的返回值中。 | 表达式 | 说明 | @@ -3363,6 +3367,8 @@ java.util.regex 包主要包括以下三个类: + + ##### 反向引用 反向引用(\number),又叫回溯引用: @@ -3401,8 +3407,6 @@ java.util.regex 包主要包括以下三个类: - - ##### 零宽断言 预搜索(零宽断言)(环视) @@ -3844,8 +3848,7 @@ List系列集合:添加的元素是有序,可重复,有索引。 ###### 介绍 -ArrayList添加的元素,是有序,可重复,有索引的。 -ArrayList实现类集合底层**基于数组存储数据**的,查询快,增删慢! +ArrayList 添加的元素,是有序,可重复,有索引的。 `public boolean add(E e)` : 将指定的元素追加到此集合的末尾 `public void add(int index, E element)` : 将指定的元素,添加到该集合中的指定位置上。 @@ -3873,7 +3876,7 @@ public static void main(String[] args){ ###### 源码 -ArrayList 是基于数组实现的,所以支持快速随机访问 +ArrayList 实现类集合底层**基于数组存储数据**的,查询快,增删慢,支持快速随机访问 ```java public class ArrayList extends AbstractList @@ -3899,7 +3902,7 @@ public class ArrayList extends AbstractList } ``` - 当add 第 1 个元素到 ArrayList,size是0,进入 ensureCapacityInternal 方法, + 当add 第 1 个元素到 ArrayList,size 是 0,进入 ensureCapacityInternal 方法, ```java private void ensureCapacityInternal(int minCapacity) { @@ -4180,16 +4183,16 @@ Set系列集合:添加的元素是无序,不重复,无索引的。 哈希值: -- 哈希值:JDK根据对象的地址或者字符串或者数字计算出来的数值 +- 哈希值:JDK 根据对象的地址或者字符串或者数字计算出来的数值 -- 获取哈希值:Object类中的public int hashCode() +- 获取哈希值:Object 类中的 public int hashCode() - 哈希值的特点 - - 同一个对象多次调用hashCode()方法返回的哈希值是相同的 - - 默认情况下,不同对象的哈希值是不同的。而重写hashCode()方法,可以实现让不同对象的哈希值相同 + - 同一个对象多次调用 hashCode() 方法返回的哈希值是相同的 + - 默认情况下,不同对象的哈希值是不同的。而重写 hashCode() 方法,可以实现让不同对象的哈希值相同 -**HashSet底层就是基于HashMap实现,值是PRESENT = new Object()** +**HashSet 底层就是基于 HashMap 实现,值是 PRESENT = new Object()** Set集合添加的元素是无序,不重复的。 @@ -4217,16 +4220,16 @@ Set集合添加的元素是无序,不重复的。 * Set系列集合元素无序的根本原因 Set系列集合添加元素无序的根本原因是因为**底层采用了哈希表存储元素**。 - JDK 1.8之前:哈希表 = 数组(初始容量16) + 链表 + (哈希算法) - JDK 1.8之后:哈希表 = 数组(初始容量16) + 链表 + 红黑树 + (哈希算法) + 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() 的值进行响应的算法运算,计算出的值相同的存入一个数组块中,以链表的形式存储,如果链表长度超过8就采取红黑树存储,所以输出的元素是无序的。 -* 如何设置只要对象内容一样,就希望集合认为它们重复了:**重写hashCode和equals方法** +* 如何设置只要对象内容一样,就希望集合认为它们重复了:**重写 hashCode 和 equals 方法** @@ -9375,13 +9378,19 @@ JVM、JRE、JDK对比: +参考视频:https://www.bilibili.com/video/BV1PJ411n7xZ + +参考视频:https://www.bilibili.com/video/BV1yE411Z7AP + + + *** ### 架构模型 -Java编译器输入的指令流是一种基于栈的指令集架构。因为跨平台的设计,java的指令都是根据栈来设计的,不同平台CPU架构不同,所以不能设计为基于寄存器架构 +Java 编译器输入的指令流是一种基于栈的指令集架构。因为跨平台的设计,java 的指令都是根据栈来设计的,不同平台 CPU 架构不同,所以不能设计为基于寄存器架构 * 基于栈式架构的特点: * 设计和实现简单,适用于资源受限的系统 @@ -14209,6 +14218,8 @@ JDK 自带了**监控工具,位于 JDK 的 bin 目录下**,其中最常用 推荐阅读:https://time.geekbang.org/column/article/41440 +参考书籍:《数据结构高分笔记》 + *** @@ -16051,11 +16062,14 @@ class MyBloomFilter { * **创建型模式**:用于描述如何创建对象,主要特点是将对象的创建与使用分离。GoF 书中提供了单例、原型、工厂方法、抽象工厂、建造者等 5 种创建型模式 * **结构型模式**:用于描述如何将类或对象按某种布局组成更大的结构,GoF 书中提供了代理、适配器、桥接、装饰、外观、享元、组合等 7 种结构型模式 - * **行为型模式**:用于描述类或对象之间怎样相互协作共同完成单个对象无法单独完成的任务,以及怎样分配职责。GoF 书中提供了模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器等 11 种行为型模式 +参考视频:https://www.bilibili.com/video/BV1Np4y1z7BU + + + *** diff --git a/Prog.md b/Prog.md index d217e9e..eac6c70 100644 --- a/Prog.md +++ b/Prog.md @@ -26,6 +26,10 @@ +参考视频:https://www.bilibili.com/video/BV16J411h7Rd(推荐观看) + + + *** @@ -9126,8 +9130,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结构,是指浏览器和服务器结构。 两种架构各有优势,但是无论哪种架构,都离不开网络的支持。 @@ -9153,6 +9157,10 @@ final void updateHead(Node h, Node p) { +参考视频:https://www.bilibili.com/video/BV1kT4y1M7vt + + + **** From d8d81eb3bd58682e4ea3d33011d374b802d9a4cf Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 11 Jul 2021 16:30:49 +0800 Subject: [PATCH 069/242] Update Java Notes --- DB.md | 132 +-- Java.md | 200 ++--- Prog.md | 104 +-- SSM.md | 2479 ++++++++++++++++++++++++++++++++++--------------------- Tool.md | 22 +- 5 files changed, 1763 insertions(+), 1174 deletions(-) diff --git a/DB.md b/DB.md index 79909b5..366e1b3 100644 --- a/DB.md +++ b/DB.md @@ -3168,7 +3168,7 @@ LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定 CLOSE 游标名称; ``` -* Mysql通过一个Error handler声明来判断指针是否到尾部,并且必须和创建游标的SQL语句声明在一起: +* Mysql 通过一个 Error handler 声明来判断指针是否到尾部,并且必须和创建游标的 SQL 语句声明在一起: ```mysql DECLARE EXIT HANDLER FOR NOT FOUND (do some action,一般是设置标志变量) @@ -3178,7 +3178,7 @@ LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定 游标的基本使用 -* 数据准备:表student +* 数据准备:表 student ```mysql id NAME age gender score @@ -3188,7 +3188,7 @@ LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定 4 赵六 26 女 90 ``` -* 创建stu_score表 +* 创建 stu_score 表 ```mysql CREATE TABLE stu_score( @@ -6569,9 +6569,9 @@ long_query_time=10 JDBC(Java DataBase Connectivity,java数据库连接)是一种用于执行SQL语句的Java API,可以为多种关系型数据库提供统一访问,是由一组用Java语言编写的类和接口组成的。 -JDBC其实就是java官方提供的一套规范(接口),用于帮助开发人员快速实现不同关系型数据库的连接 +JDBC 是 java 官方提供的一套规范(接口),用于帮助开发人员快速实现不同关系型数据库的连接 -使用JDBC需要导包 +使用 JDBC 需要导包 @@ -6590,7 +6590,7 @@ DriverManager:驱动管理对象 * 代码实现语法:`Class.forName("com.mysql.jdbc.Driver)` - * com.mysql.jdbc.Driver中存在静态代码块 + * com.mysql.jdbc.Driver 中存在静态代码块 ```java static { @@ -6602,15 +6602,17 @@ DriverManager:驱动管理对象 } ``` - * 不需要通过DriverManager调用静态方法registerDriver,因为Driver类被使用,则自动执行静态代码块完成注册驱动 + * 不需要通过 DriverManager 调用静态方法 registerDriver,因为 Driver 类被使用,则自动执行静态代码块完成注册驱动 - * jar包中META-INF目录下存在一个java.sql.Driver配置文件,文件中指定了com.mysql.jdbc.Driver + * jar 包中 META-INF 目录下存在一个 java.sql.Driver 配置文件,文件中指定了 com.mysql.jdbc.Driver * 获取数据库连接并返回连接对象 - * `public static Connection getConnection(String url, String user, String password)` - * url:指定连接的路径。语法:`jdbc:mysql://ip地址(域名):端口号/数据库名称` - * user:用户名 - * password:密码 + + `public static Connection getConnection(String url, String user, String password)` + + * url:指定连接的路径。语法:`jdbc:mysql://ip地址(域名):端口号/数据库名称` + * user:用户名 + * password:密码 @@ -6640,16 +6642,16 @@ Connection:数据库连接对象 ### Statement -Statement:执行sql语句的对象 +Statement:执行 sql 语句的对象 -- 执行DML语句:`int executeUpdate(String sql)` - - 返回值int:返回影响的行数 - - 参数sql:可以执行insert、update、delete语句 +- 执行 DML 语句:`int executeUpdate(String sql)` + - 返回值 int:返回影响的行数 + - 参数 sql:可以执行 insert、update、delete 语句 - 执行DQL语句:`ResultSet executeQuery(String sql)` - - 返回值ResultSet:封装查询的结果 - - 参数sql:可以执行select语句 + - 返回值 ResultSet:封装查询的结果 + - 参数 sql:可以执行 select 语句 - 释放资源 - - 释放此Statement对象的数据库和JDBC资源:`void close()` + - 释放此 Statement 对象的数据库和 JDBC 资源:`void close()` @@ -6659,16 +6661,16 @@ Statement:执行sql语句的对象 ### ResultSet -ResultSet:结果集对象,ResultSet对象维护了一个游标,指向当前的数据行,初始在第一行 +ResultSet:结果集对象,ResultSet 对象维护了一个游标,指向当前的数据行,初始在第一行 - 判断结果集中是否有数据:`boolean next()` - - 有数据返回true,并将索引**向下移动一行** - - 没有数据返回false + - 有数据返回 true,并将索引**向下移动一行** + - 没有数据返回 false - 获取结果集中**当前行**的数据:`XXX getXxx("列名")` - - XXX代表数据类型(要获取某列数据,这一列的数据类型) - - 例如:String getString("name"); int getInt("age"); + - XXX 代表数据类型(要获取某列数据,这一列的数据类型) + - 例如:String getString("name"); int getInt("age"); - 释放资源 - - 释放ResultSet对象的数据库和JDBC资源:`void close()` + - 释放 ResultSet 对象的数据库和 JDBC 资源:`void close()` @@ -6687,7 +6689,6 @@ CREATE DATABASE db14; -- 使用db14数据库 USE db14; - -- 创建student表 CREATE TABLE student( sid INT PRIMARY KEY AUTO_INCREMENT, -- 学生id @@ -6701,7 +6702,7 @@ INSERT INTO student VALUES (NULL,'张三',23,'1999-09-23'),(NULL,'李四',24,'19 (NULL,'王五',25,'1996-06-06'),(NULL,'赵六',26,'1994-10-20'); ``` -JDBC连接代码: +JDBC 连接代码: ```java public class JDBCDemo01 { @@ -6744,7 +6745,7 @@ public class JDBCDemo01 { ## 工具类 -* 配置文件(在src下创建config.properties) +* 配置文件(在 src 下创建 config.properties) ```properties driverClass=com.mysql.jdbc.Driver @@ -6974,7 +6975,7 @@ 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'; @@ -6989,16 +6990,16 @@ SQL注入攻击演示 ### 攻击解决 -PreparedStatement:预编译sql语句的执行者对象,继承`PreparedStatement extends Statement` +PreparedStatement:预编译 sql 语句的执行者对象,继承`PreparedStatement extends Statement` -* 在执行sql语句之前,将sql语句进行提前编译。明确sql语句的格式,剩余的内容都会认为是参数 -* sql语句中的参数使用?作为占位符 +* 在执行 sql 语句之前,将 sql 语句进行提前编译。明确 sql 语句的格式,剩余的内容都会认为是参数 +* sql 语句中的参数使用 ? 作为占位符 -为?占位符赋值的方法:setXxx(参数1,参数2); +为 ? 占位符赋值的方法:setXxx(参数1,参数2) -- 参数1:?的位置编号(编号从1开始) +- 参数1:? 的位置编号(编号从1开始) -- 参数2:?的实际参数 +- 参数2:? 的实际参数 ```java String sql = "SELECT * FROM user WHERE loginname=? AND password=?"; @@ -7007,10 +7008,10 @@ PreparedStatement:预编译sql语句的执行者对象,继承`PreparedStatem pst.setString(2,password); ``` -执行sql语句的方法 +执行 sql 语句的方法 -- 执行insert、update、delete语句:`int executeUpdate()` -- 执行select语句:`ResultSet executeQuery()` +- 执行 insert、update、delete 语句:`int executeUpdate()` +- 执行 select 语句:`ResultSet executeQuery()` @@ -7042,10 +7043,10 @@ PreparedStatement:预编译sql语句的执行者对象,继承`PreparedStatem ### 自定义池 -DataSource接口概述: +DataSource 接口概述: -* java.sql.DataSource接口:数据源(数据库连接池)。 -* Java中DataSource是一个标准的数据源接口,官方提供的数据库连接池规范,连接池类实现该接口。 +* java.sql.DataSource 接口:数据源(数据库连接池) +* Java 中 DataSource 是一个标准的数据源接口,官方提供的数据库连接池规范,连接池类实现该接口 * 获取数据库连接对象:`Connection getConnection()` 自定义连接池: @@ -7128,11 +7129,11 @@ public class MyDataSourceTest { #### 继承方式 -继承(无法解决) +继承(无法解决) -- 通过打印连接对象,发现DriverManager获取的连接实现类是JDBC4Connection -- 自定义一个类,继承JDBC4Connection这个类,重写close()方法 -- 通过查看JDBC工具类获取连接的方法我们发现:我们虽然自定义了一个子类,完成了归还连接的操作。但是DriverManager获取的还是JDBC4Connection这个对象,并不是我们的子类对象。 +- 通过打印连接对象,发现 DriverManager 获取的连接实现类是 JDBC4Connection +- 自定义一个类,继承 JDBC4Connection 这个类,重写 close() 方法 +- 通过查看 JDBC 工具类获取连接的方法我们发现:我们虽然自定义了一个子类,完成了归还连接的操作。但是DriverManager 获取的还是 JDBC4Connection 这个对象,并不是我们的子类对象。 代码实现 @@ -7185,11 +7186,11 @@ public class MyDataSourceTest { -#### 装饰设计模式 +#### 装饰者 -自定义类实现Connection接口,通过装饰设计模式,实现和mysql驱动包中的Connection实现类相同的功能 +自定义类实现 Connection 接口,通过装饰设计模式,实现和 mysql 驱动包中的 Connection 实现类相同的功能 -在实现类对每个获取的Connection进行装饰:把连接和连接池参数传递进行包装 +在实现类对每个获取的 Connection 进行装饰:把连接和连接池参数传递进行包装 特点:通过装饰设计模式连接类我们发现,有很多需要重写的方法,代码太繁琐 @@ -7246,9 +7247,9 @@ public class MyDataSourceTest { -#### 适配器设计 +#### 适配器 -使用适配器设计模式改进,提供一个适配器类,实现Connection接口,将所有功能进行实现(除了close方法),自定义连接类只需要继承这个适配器类,重写需要改进的close()方法即可。 +使用适配器设计模式改进,提供一个适配器类,实现 Connection 接口,将所有功能进行实现(除了 close 方法),自定义连接类只需要继承这个适配器类,重写需要改进的 close() 方法即可。 特点:自定义连接类中很简洁。剩余所有的方法抽取到了适配器类中,但是适配器这个类还是我们自己编写。 @@ -7383,17 +7384,13 @@ public class MyDataSource implements DataSource { -### 开源连接池 +### 开源项目 #### C3P0 -使用C3P0连接池 - 1.导入jar包 - 2.导入配置文件到src目录下 - 3.创建c3p0连接池对象 - 4.获取数据库连接进行使用 +使用 C3P0 连接池: -* 配置文件名称:c3p0-config.xml。必须放在src目录下 +* 配置文件名称:c3p0-config.xml,必须放在 src 目录下 ```xml @@ -7454,12 +7451,7 @@ public class MyDataSource implements DataSource { #### Druid -Druid连接池 - 1.导入jar包 - 2.编写配置文件,放在src目录下 - 3.通过Properties集合加载配置文件 - 4.通过Druid连接池工厂类获取数据库连接池对象 - 5.获取数据库连接,进行使用 +Druid 连接池: * 配置文件:druid.properties,必须放在src目录下 @@ -7510,7 +7502,7 @@ Druid连接池 -### 连接池工具类 +### 工具类 数据库连接池的工具类: @@ -8330,6 +8322,18 @@ Redis 所有操作都是**原子性**的,采用**单线程**机制,命令是 Redis 字符串对象底层的数据结构实现主要是 int 和简单动态字符串 SDS,是可以修改的字符串,内部结构实现上类似于 Java 的 ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配 +```c +struct sdshdr{ + //记录buf数组中已使用字节的数量 + //等于 SDS 保存字符串的长度 + int len; + //记录 buf 数组中未使用字节的数量 + int free; + //字节数组,用于保存字符串 + char buf[]; +} +``` + ![](https://gitee.com/seazean/images/raw/master/DB/Redis-string数据结构.png) 内部为当前字符串实际分配的空间 capacity 一般要高于实际字符串长度 len,当字符串长度小于 1M 时,扩容都是双倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间。需要注意的是字符串最大长度为 512M diff --git a/Java.md b/Java.md index b5370b8..2f86438 100644 --- a/Java.md +++ b/Java.md @@ -2491,7 +2491,7 @@ s = s + "cd"; //s = abccd 新对象 `public boolean equalsIgnoreCase(String anotherString)` : 比较字符串的内容,忽略大小写 `public int length()` : 返回此字符串的长度 `public String trim()` : 返回一个字符串,其值为此字符串,并删除任何前导和尾随空格 -`public String[] split(String regex)` : 将字符串按给定的正则表达式分割成字符串数组 +`public String[] split(String regex)` : 将字符串按给定的正则表达式分割成字符串数组 `public char charAt(int index)` : 取索引处的值 `public char[] toCharArray()` : 将字符串拆分为字符数组后返回 `public boolean startsWith(String prefix)` : 测试此字符串是否以指定的前缀开头 @@ -2741,9 +2741,10 @@ private void ensureCapacityInternal(int minimumCapacity) { value = Arrays.copyOf(value, newCapacity(minimumCapacity)); } } - public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) { - //将字符串中的字符复制到目标字符数组中 - System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin); +public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) { + //将字符串中的字符复制到目标字符数组中 + // 字符串调用该方法,此时value是字符串的值,dst是目标字符数组 + System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin); } ``` @@ -3927,6 +3928,7 @@ public class ArrayList extends AbstractList //判断是否需要扩容 private void ensureExplicitCapacity(int minCapacity) { modCount++; + //索引越界 if (minCapacity - elementData.length > 0) //调用grow方法进行扩容,调用此方法代表已经开始扩容了 grow(minCapacity); @@ -4014,7 +4016,7 @@ public class ArrayList extends AbstractList ##### Vector -同步:Vector的实现与 ArrayList 类似,但是使用了 synchronized 进行同步 +同步:Vector的实现与 ArrayList 类似,但是方法上使用了 synchronized 进行同步 构造:默认长度为10的数组 @@ -4094,19 +4096,19 @@ public class ListDemo { ###### 源码 -LinkedList是一个实现了List接口的**双端链表**,支持高效的插入和删除操作,另外也实现了Deque接口,使得LinkedList类也具有队列的特性 +LinkedList 是一个实现了 List 接口的**双端链表**,支持高效的插入和删除操作,另外也实现了 Deque 接口,使得LinkedList 类也具有队列的特性 ![](https://gitee.com/seazean/images/raw/master/Java/LinkedList底层结构.png) 核心方法: -* 使LinkedList变成线程安全的,可以调用静态类Collections类中的synchronizedList方法: +* 使 LinkedList 变成线程安全的,可以调用静态类 Collections 类中的 synchronizedList 方法: ```java List list = Collections.synchronizedList(new LinkedList(...)); ``` -* 私有内部类Node:这个类代表双端链表的节点Node +* 私有内部类 Node:这个类代表双端链表的节点 Node ```java private static class Node { @@ -4143,21 +4145,21 @@ LinkedList是一个实现了List接口的**双端链表**,支持高效的插 * remove()、removeFirst()、pop():删除头节点 * removeLast()、pollLast():删除尾节点,removeLast()在链表为空时抛出NoSuchElementException,而pollLast()方法返回null -对比ArrayList +对比 ArrayList -1. 是否保证线程安全:ArrayList和LinkedList都是不同步的,也就是不保证线程安全 +1. 是否保证线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全 2. 底层数据结构: - * Arraylist底层使用的是 `Object` 数组 - * LinkedList 底层使用的是 双向链表 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环) + * Arraylist 底层使用的是 `Object` 数组 + * LinkedList 底层使用的是双向链表数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环) 3. 插入和删除是否受元素位置的影响: - * ArrayList采用数组存储,所以插入和删除元素受元素位置的影响 - * LinkedList采用链表存储,所以对于`add(E e)`方法的插入,删除元素不受元素位置的影响 + * ArrayList 采用数组存储,所以插入和删除元素受元素位置的影响 + * LinkedList采 用链表存储,所以对于`add(E e)`方法的插入,删除元素不受元素位置的影响 4. 是否支持快速随机访问: - * LinkedList不支持高效的随机元素访问,ArrayList支持 + * LinkedList 不支持高效的随机元素访问,ArrayList 支持 * 快速随机访问就是通过元素的序号快速获取元素对象(对应于`get(int index)`方法)。 5. 内存空间占用: - * ArrayList的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间 - * LinkedList的空间花费则体现在它的每一个元素都需要消耗比 ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据) + * ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间 + * LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据) @@ -8404,7 +8406,7 @@ public class AnnotationDemo01{ ### 注解解析 -开发中经常要知道一个类的成分上面到底有哪些注解,注解有哪些属性数据,这都需要进行注解的解析。 +开发中经常要知道一个类的成分上面到底有哪些注解,注解有哪些属性数据,这都需要进行注解的解析 注解解析相关的接口: @@ -9007,29 +9009,32 @@ person.xsd #### 解析 -* 概述:xml解析就是从xml中获取到数据。DOM是解析思想。 +* 概述:xml 解析就是从 xml 中获取到数据,DOM 是解析思想。 * DOM(Document Object Model)文档对象模型:就是把文档的各个组成部分看做成对应的对象。 会把xml文件全部加载到内存,在内存中形成一个树形结构,再获取对应的值 -* 工具:dom4j属于第三方技术,必须导入该框架 +* 工具:dom4j 属于第三方技术,必须导入该框架 https://dom4j.github.io/ 去下载dom4j,在idea中当前模块下新建一个lib文件夹,将jar包复制到文件夹中 选中jar包 -> 右键 -> 选择add as library即可 -* dom4j实现 - * dom4j解析器构造方法:`SAXReader saxReader = new SAXReader();` +* dom4j 实现 + * dom4j 解析器构造方法:`SAXReader saxReader = new SAXReader();` + * 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)`:加载文件成为一个字节输入流返回 -#### 解析根元素 +#### 根元素 -Document方法: - Element getRootElement():获取根元素。 +Document 方法:Element getRootElement() 获取根元素。 ```java // 需求:解析books.xml文件成为一个Document文档树对象,得到根元素对象。 @@ -9074,13 +9079,14 @@ public class Dom4JDemo { -#### 解析子元素 +#### 子元素 + +Element 元素的 API: -Element元素的API: - String getName() : 取元素的名称。 - List elements() : 获取当前元素下的全部子元素(一级) - List elements(String name) : 获取当前元素下的指定名称的全部子元素(一级) - Element element(String name) : 获取当前元素下的指定名称的某个子元素,默认取第一个(一级) +* String getName():取元素的名称。 +* List elements():获取当前元素下的全部子元素(一级) +* List elements(String name):获取当前元素下的指定名称的全部子元素(一级) +* Element element(String name):获取当前元素下的指定名称的某个子元素,默认取第一个(一级) ```java public class Dom4JDemo { @@ -9115,16 +9121,22 @@ public class Dom4JDemo { -#### 解析属性 +*** + + + +#### 属性 + +Element 元素的 API: + +* List attributes():获取元素的全部属性对象。 +* Attribute attribute(String name):根据名称获取某个元素的属性对象。 +* String attributeValue(String var):直接获取某个元素的某个属性名称的值。 -Element元素的API: - List attributes() : 获取元素的全部属性对象。 - Attribute attribute(String name) : 根据名称获取某个元素的属性对象。 - String attributeValue(String var) : 直接获取某个元素的某个属性名称的值。 +Attribute 对象的API: -Attribute对象的API: - String getName() : 获取属性名称。 - String getValue() : 获取属性值。 +* String getName():获取属性名称。 +* String getValue():获取属性值。 ```java public class Dom4JDemo { @@ -9154,13 +9166,18 @@ public class Dom4JDemo { -#### 解析文本 +*** + + -Element: - String elementText(String name) : 可以直接获取当前元素的子元素的文本内容 - String elementTextTrim(String name) : 去前后空格,直接获取当前元素的子元素的文本内容 - String getText() : 直接获取当前元素的文本内容。 - String getTextTrim() : 去前后空格,直接获取当前元素的文本内容。 +#### 文本 + +Element: + +* String elementText(String name):可以直接获取当前元素的子元素的文本内容 +* String elementTextTrim(String name):去前后空格,直接获取当前元素的子元素的文本内容 +* String getText():直接获取当前元素的文本内容。 +* String getTextTrim():去前后空格,直接获取当前元素的文本内容。 ```java public class Dom4JDemo { @@ -9189,70 +9206,6 @@ public class Dom4JDemo { -**** - - - -#### 案例 - -Dom4j解析XML文件:Contacts.xml成为一个Java的对象 -Contacts.xml 解析成===> List - -```java -public class Dom4JDemo { - public static void main(String[] args) throws Exception { - SAXReader saxReader = new SAXReader(); - Document document = saxReader.read(new File("Day13Demo/src/Contacts.xml")); - Element root = docment.getRootElement(); - // 4.获取根元素下的全部子元素 - List sonElements = root.elements(); - // 5.遍历子元素 封装成List集合对象 - List contactList = new ArrayList<>(); - if(sonElements != null && sonElements.size() > 0) { - for (Element sonElement : sonElements) { - Contact c = new Contact(); - c.setID(Integer.valueOf(sonElement.attributeValue("id"))); - contact.setVip(Boolean.valueOf(sonElement.attributeValue("vip"))); - contact.setName(sonElement.elementText("name")); - contact.setSex(sonElement.elementText("gender").charAt(0)); - contact.setEmail(sonElement.elementText("email")); - contactList.add(contact); - } - } - System.out.println(contactList); - } -} -public class Contact { - private int id ; - private boolean vip; - private String name ; - private char sex ; - private String email ; - //构造器 -} -``` - -```xml - - - - 潘金莲 - - panpan@seazean.cn - - - 武松 - - wusong@seazean.cn - - - 武大狼 - - wuda@seazean.cn - - -``` - **** @@ -9261,7 +9214,7 @@ public class Contact { ### XPath -Dom4J 可以用于解析整个XML的数据。但是如果要检索XML中的某些信息,建议使用XPath +Dom4J 可以用于解析整个 XML 的数据,但是如果要检索 XML 中的某些信息,建议使用 XPath XPath常用API: @@ -11570,7 +11523,11 @@ public class Test { 补充: -接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 () 方法。但两者不同的是,执行接口的 () 方法不需要先执行父接口的 () 方法,只有当父接口中定义的变量使用时,父接口才会初始化;接口的实现类在初始化时也一样不会执行接口的 () 方法 +接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 () 方法。但两者不同的是, + +* 在初始化一个接口时,并不会先初始化它的父接口,所以执行接口的 () 方法不需要先执行父接口的 () 方法 +* 在初始化一个类时,不会先初始化所实现的接口,所以接口的实现类在初始化时不会执行接口的 () 方法 +* 只有当父接口中定义的变量使用时,父接口才会初始化 @@ -11590,9 +11547,7 @@ public class Test { * putstatic:程序给类的静态变量赋值 * invokestatic :调用一个类的静态方法 * 使用 java.lang.reflect 包的方法对类进行反射调用时,如果类没有进行初始化,则需要先触发其初始化 -* 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化,但这条规则并不适用于接口,只有当程序首次使用特定接口的静态字段时,才会导致该接口的初始化 - - 在初始化一个类时,并不会先初始化所实现的接口 - - 在初始化一个接口时,并不会先初始化它的父接口 +* 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化,但这条规则并**不适用于接口** * 当虚拟机启动时,需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类 * MethodHandle 和 VarHandle 可以看作是轻量级的反射调用机制,而要想使用这两个调用, 就必须先使用 findStaticVarHandle 来初始化要调用的类 * 补充:当一个接口中定义了 JDK8 新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化 @@ -15427,10 +15382,10 @@ public class Kmp { 红黑树与 AVL 树的比较: -* AVL树是更加严格的平衡,可以提供更快的查找速度,适用于读取查找密集型任务 -* 红黑树只是做到了近似平衡,并不是严格的平衡,红黑树的插入删除比AVL树更便于控制操作,红黑树更适合于插入修改密集型任务 +* AVL 树是更加严格的平衡,可以提供更快的查找速度,适用于读取查找密集型任务 +* 红黑树只是做到了近似平衡,并不是严格的平衡,红黑树的插入删除比 AVL 树更便于控制操作,红黑树更适合于插入修改密集型任务 -- 红黑树整体性能略优于AVL树,AVL树的旋转比红黑树的旋转多,更加难以平衡和调试,插入和删除的效率比红黑树慢。 +- 红黑树整体性能略优于 AVL 树,AVL 树的旋转比红黑树的旋转多,更加难以平衡和调试,插入和删除的效率比红黑树慢。 ![红黑树](https://gitee.com/seazean/images/raw/master/Java/红黑树结构图.png) @@ -16406,7 +16361,7 @@ UML 从目标系统的不同角度出发,定义了用例图、类图、对象 } ``` -* 枚举方式:枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式 +* 枚举方式:枚举类型是所用单例实现中**唯一一种**不会被破坏的单例实现模式 ```java public enum Singleton { @@ -16511,6 +16466,9 @@ UML 从目标系统的不同角度出发,定义了用例图、类图、对象 ``` * 内部类属于懒汉式,类加载本身就是懒惰的,首次调用时加载,然后对单例进行初始化 + + 类加载的时候方法不会被调用,所以不会触发 getInstance 方法调用 invokestatic 指令对内部类进行加载;加载的时候字节码常量池会被加入类的运行时常量池,解析工作是将常量池中的符号引用解析成直接引用,但是解析过程不一定非得在类加载时完成,可以延迟到运行时进行,所以静态内部类实现单例会**延迟加载** + * 没有线程安全问题,静态变量初始化在类加载时完成,由 JVM 保证线程安全 @@ -19353,7 +19311,9 @@ public class Thread implements Runnable { #### 应用场景 -在 JavaWeb应用开发中,FilterChain是职责链(过滤器)模式的典型应用,以下是 Filter 的模拟实现: +在 JavaWeb 应用开发中,FilterChain是职责链(过滤器)模式的典型应用,以下是 Filter 的模拟实现: + +类似于栈的执行流程,先进先出 * 模拟 web 请求 Request 以及 web 响应 Response: diff --git a/Prog.md b/Prog.md index eac6c70..7fbc016 100644 --- a/Prog.md +++ b/Prog.md @@ -8,7 +8,7 @@ 进程的特征:动态性、并发性、独立性、异步性、结构性 -**线程**:线程是属于进程的,是一个基本的CPU执行单元,是程序执行流的最小单元。线程是进程中的一个实体,是被系统独立调度和分配的基本单位,线程本身不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其他线程共享进程所拥有的全部资源。 +**线程**:线程是属于进程的,是一个基本的 CPU 执行单元,是程序执行流的最小单元。线程是进程中的一个实体,是被系统独立调度和分配的基本单位,线程本身不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其他线程共享进程所拥有的全部资源。 关系:一个进程可以包含多个线程,这就是多线程,比如看视频是进程,图画、声音、广告等就是多个线程 @@ -16,8 +16,8 @@ **并发并行**: -* 并行:在同一时刻,有多个指令在多个CPU上同时执行 -* 并发:在同一时刻,有多个指令在单个CPU上交替执行 +* 并行:在同一时刻,有多个指令在多个 CPU 上同时执行 +* 并发:在同一时刻,有多个指令在单个 CPU 上交替执行 **同步异步**: @@ -83,12 +83,14 @@ 运行一个程序就是开启一个进程,在进程中创建线程的方式有三种: -1. 直接定义一个类继承线程类Thread,重写run()方法,创建线程对象,调用线程对象的start()方法启动线程 -2. 定义一个线程任务类实现Runnable接口,重写run()方法,创建线程任务对象,把线程任务对象包装成线程对象, 调用线程对象的start()方法启动线程 -3. 实现Callable接口 +1. 直接定义一个类继承线程类 Thread,重写 run() 方法,创建线程对象,调用线程对象的 start() 方法启动线程 +2. 定义一个线程任务类实现 Runnable 接口,重写 run() 方法,创建线程任务对象,把线程任务对象包装成线程对象, 调用线程对象的 start() 方法启动线程 +3. 实现 Callable 接口 +*** + #### Thread @@ -189,18 +191,18 @@ public class MyRunnable implements Runnable{ #### Callable -实现Callable接口: +实现 Callable 接口: -1. 定义一个线程任务类实现Callable接口,申明线程执行的结果类型 -2. 重写线程任务类的call方法,这个方法可以直接返回执行的结果 -3. 创建一个Callable的线程任务对象 -4. 把Callable的线程任务对象包装成一个未来任务对象 +1. 定义一个线程任务类实现 Callable 接口,申明线程执行的结果类型 +2. 重写线程任务类的 call 方法,这个方法可以直接返回执行的结果 +3. 创建一个 Callable 的线程任务对象 +4. 把 Callable 的线程任务对象包装成一个未来任务对象 5. 把未来任务对象包装成线程对象 -6. 调用线程的start()方法启动线程 +6. 调用线程的 start() 方法启动线程 `public FutureTask(Callable callable)`:未来任务对象,在线程执行完后**得到线程的执行结果** -* 其实就是Runnable对象,这样被包装成未来任务对象 +* 其实就是 Runnable 对象,这样被包装成未来任务对象 `public V get()`:同步等待 task 执行完毕的结果 @@ -208,7 +210,7 @@ public class MyRunnable implements Runnable{ 优缺点: -* 优点:同Runnable,并且能得到线程执行的结果 +* 优点:同 Runnable,并且能得到线程执行的结果 * 缺点:编码复杂 ```java @@ -256,11 +258,13 @@ Java Virtual Machine Stacks(Java 虚拟机栈):每个线程启动后,虚 * 有更高优先级的线程需要运行 * 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法 -程序计数器(Program Counter Register):记住下一条 jvm 指令的执行地址,是线程私有的 +程序计数器(Program Counter Register):记住下一条 JVM 指令的执行地址,是线程私有的 当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等 -Java创建的线程是内核级线程,**线程的调度是在内核态运行的,而线程中的代码是在用户态运行**,所以线程切换(状态改变)会导致用户与内核态转换,这是非常消耗性能 +Java 创建的线程是内核级线程,**线程的调度是在内核态运行的,而线程中的代码是在用户态运行**,所以线程切换(状态改变)会导致用户与内核态转换,这是非常消耗性能 + +Java 中 main 方法启动的是一个进程也是一个主线程,main 方法里面的其他线程均为子线程 @@ -366,16 +370,16 @@ synchronized (t1) { } ``` -* join方法是被synchronized修饰的,本质上是一个对象锁,其内部的wait方法调用也是释放锁的,但是**释放的是当前线程的对象锁,而不是外面的lock锁** +* join 方法是被 synchronized 修饰的,本质上是一个对象锁,其内部的 wait 方法调用也是释放锁的,但是**释放的是当前线程的对象锁,而不是外面的锁** -* t1会强占CPU资源,直至线程执行结束,当调用某个线程的join方法后,该线程抢占到CPU资源,就不再释放,直到线程执行完毕 +* t1 会强占 CPU 资源,直至线程执行结束,当调用某个线程的 join 方法后,该线程抢占到 CPU 资源,就不再释放,直到线程执行完毕 线程同步: -* join实现线程同步,因为会阻塞等待另一个线程的结束,才能继续向下运行 +* join 实现线程同步,因为会阻塞等待另一个线程的结束,才能继续向下运行 * 需要外部共享变量,不符合面向对象封装的思想 * 必须等待线程结束,不能配合线程池使用 -* Future 实现(同步):get()方法阻塞等待执行结果 +* Future 实现(同步):get() 方法阻塞等待执行结果 * main 线程接收结果 * get 方法是让调用线程同步等待 @@ -415,7 +419,7 @@ public class Test { `public static boolean interrupted()`:判断当前线程是否被打断,清除打断标记 `public boolean isInterrupted()`:判断当前线程是否被打断,不清除打断标记 -* sleep,wait,join方法都会让线程进入阻塞状态,打断进程会**清空打断状态** (false) +* sleep,wait,join 方法都会让线程进入阻塞状态,打断进程**会清空打断状态** (false) ```java public static void main(String[] args) throws InterruptedException { @@ -461,7 +465,7 @@ public class Test { ##### 打断park -park作用类似sleep,打断 park 线程,不会清空打断状态(true) +park 作用类似 sleep,打断 park 线程,不会清空打断状态(true) ```java public static void main(String[] args) throws Exception { @@ -705,7 +709,7 @@ Java: * 阻塞式的解决方案:synchronized,Lock * 非阻塞式的解决方案:原子变量 -管程(monitor):由局部于自己的若干公共变量和所有访问这些公共变量的过程所组成的软件模块,保证同一时刻只有一个进程在管程内活动,即管程内定义的操作在同一时刻只被一个进程调用(由编译器实现) +管程(monitor):由局部于自己的若干公共变量和所有访问这些公共变量的过程所组成的软件模块,保证同一时刻只有一个进程在管程内活动,即管程内定义的操作在同一时刻只被一个进程调用(由编译器实现) **synchronized:对象锁,保证了临界区内代码的原子性**,采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其它线程获取这个对象锁时会阻塞,保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换 @@ -731,15 +735,15 @@ Java: ##### 同步代码块 -锁对象:理论上可以是任意的“唯一”对象 +锁对象:理论上可以是任意的唯一对象 -synchronized是可重入、不公平的重量级锁 +synchronized 是可重入、不公平的重量级锁 原则上: * 锁对象建议使用共享资源 -* 在实例方法中建议用this作为锁对象,锁住的this正好是共享资源 -* 在静态方法中建议用类名.class字节码作为锁对象 +* 在实例方法中建议用 this 作为锁对象,锁住的 this 正好是共享资源 +* 在静态方法中建议用类名 .class 字节码作为锁对象 * 因为静态成员属于类,被所有实例对象共享,所以需要锁住类 * 锁住类以后,类的所有实例都相当于同一把锁,参考线程八锁 @@ -881,9 +885,9 @@ class Room { 说明:主要关注锁住的对象是不是同一个 * 锁住类对象,所有类的实例的方法都是安全的 -* 锁住this对象,只有在当前实例对象的线程内是安全的,如果有多个实例就不安全 +* 锁住 this 对象,只有在当前实例对象的线程内是安全的,如果有多个实例就不安全 -线程不安全:因为锁住的不是同一个对象,线程1调用a方法锁住的类对象,线程2调用b方法锁住的n2对象,不是同一个对象 +线程不安全:因为锁住的不是同一个对象,线程 1 调用 a 方法锁住的类对象,线程 2 调用 b 方法锁住的 n2 对象,不是同一个对象 ```java class Number{ @@ -903,7 +907,7 @@ public static void main(String[] args) { } ``` -线程安全:因为n1调用a()方法,锁住的是类对象,n2调用b()方法,锁住的也是类对象,所以线程安全 +线程安全:因为 n1 调用 a() 方法,锁住的是类对象,n2 调用 b() 方法,锁住的也是类对象,所以线程安全 ```java class Number{ @@ -1527,9 +1531,9 @@ class TestLiveLock { #### 基本使用 -需要获取对象锁后才可以调用`锁对象.wait()`,调用notify随机唤醒一个线程,notifyAll唤醒所有线程去竞争CPU +需要获取对象锁后才可以调用`锁对象.wait()`,notify 随机唤醒一个线程,notifyAll 唤醒所有线程去竞争 CPU -Object类API: +Object 类 API: ```java public final void notify():唤醒正在等待对象监视器的单个线程。 @@ -1538,11 +1542,11 @@ public final void wait():导致当前线程等待,直到另一个线程调用 public final native void wait(long timeout):有时限的等待, 到n毫秒后结束等待,或是被唤醒 ``` -对比sleep(): +对比 sleep(): -* 原理不同:sleep()方法是属于Thread类,是线程用来控制自身流程的,使此线程暂停执行一段时间而把执行机会让给其他线程;wait()方法属于Object类,用于线程间通信 -* 对锁的处理机制不同:调用sleep()方法的过程中,线程不会释放对象锁,当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,但是都会释放CPU -* 使用区域不同:wait()方法必须放在**同步控制方法和同步代码块(先获取锁)**中使用,sleep() 方法则可以放在任何地方使用 +* 原理不同:sleep() 方法是属于 Thread 类,是线程用来控制自身流程的,使此线程暂停执行一段时间而把执行机会让给其他线程;wait() 方法属于 Object 类,用于线程间通信 +* 对锁的处理机制不同:调用 sleep() 方法的过程中,线程不会释放对象锁,当调用 wait() 方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,但是都会释放 CPU +* 使用区域不同:wait() 方法必须放在**同步控制方法和同步代码块(先获取锁)**中使用,sleep() 方法则可以放在任何地方使用 底层原理: @@ -1564,7 +1568,7 @@ public final native void wait(long timeout):有时限的等待, 到n毫秒后结 虚假唤醒:notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程 -解决方法:采用notifyAll +解决方法:采用 notifyAll notifyAll 仅解决某个线程的唤醒问题,使用 if + wait 判断仅有一次机会,一旦条件不成立,无法重新判断 @@ -1942,7 +1946,7 @@ public static void main(String[] args) throws InterruptedException { #### 交替输出 -连续输出5次abc +连续输出 5 次 abc ```java public class day2_14 { @@ -2222,10 +2226,10 @@ Java 内存模型是 Java MemoryModel(JMM),本身是一种**抽象的概 作用: -* 屏蔽各种硬件和操作系统的内存访问差异,实现让Java程序在各种平台下都能达到一致的内存访问效果 +* 屏蔽各种硬件和操作系统的内存访问差异,实现让 Java 程序在各种平台下都能达到一致的内存访问效果 * 规定了线程和内存之间的一些关系 -根据JMM的设计,系统存在一个主内存(Main Memory),Java中所有变量都存储在主存中,对于所有线程都是共享的;每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些**变量的拷贝**,线程对所有变量的操作都是先对变量进行拷贝,然后在工作内存中进行,不能直接操作主内存中的变量;线程之间无法相互直接访问,线程间的通信(传递)必须通过主内存来完成 +根据JMM的设计,系统存在一个主内存(Main Memory),Java 中所有变量都存储在主存中,对于所有线程都是共享的;每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些**变量的拷贝**,线程对所有变量的操作都是先对变量进行拷贝,然后在工作内存中进行,不能直接操作主内存中的变量;线程之间无法相互直接访问,线程间的通信(传递)必须通过主内存来完成 ![](https://gitee.com/seazean/images/raw/master/Java/JMM内存模型.png) @@ -2252,7 +2256,7 @@ Java 内存模型是 Java MemoryModel(JMM),本身是一种**抽象的概 Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作: - + * lock:将一个变量标识为被一个线程独占状态 * unclock:将一个变量从独占状态释放出来,释放后的变量才可以被其他线程锁定 @@ -2332,13 +2336,13 @@ public static void main(String[] args) throws InterruptedException { 有序性:在本线程内观察,所有操作都是有序的;在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序 -CPU的基本工作是执行存储的指令序列,即程序,程序的执行过程实际上是不断地取出指令、分析指令、执行指令的过程,为了提高性能,编译器和处理器会对指令重排,一般分为以下三种: +CPU 的基本工作是执行存储的指令序列,即程序,程序的执行过程实际上是不断地取出指令、分析指令、执行指令的过程,为了提高性能,编译器和处理器会对指令重排,一般分为以下三种: ```java 源代码 -> 编译器优化的重排 -> 指令并行的重排 -> 内存系统的重排 -> 最终执行指令 ``` -现代 CPU 支持多级指令流水线,几乎所有的冯•诺伊曼型计算机的CPU,其工作都可以分为5个阶段:取指令、指令译码、执行指令、访存取数和结果写回,可以称之为**五级指令流水线**。这时 CPU 可以在一个时钟周期内,同时运行五条指令的**不同阶段**(每个线程不同的阶段),本质上流水线技术并不能缩短单条指令的执行时间,但变相地提高了指令地吞吐率 +现代 CPU 支持多级指令流水线,几乎所有的冯•诺伊曼型计算机的 CPU,其工作都可以分为 5 个阶段:取指令、指令译码、执行指令、访存取数和结果写回,可以称之为**五级指令流水线**。这时 CPU 可以在一个时钟周期内,同时运行五条指令的**不同阶段**(每个线程不同的阶段),本质上流水线技术并不能缩短单条指令的执行时间,但变相地提高了指令地吞吐率 处理器在进行重排序时,必须要考虑**指令之间的数据依赖性** @@ -2359,7 +2363,7 @@ CPU的基本工作是执行存储的指令序列,即程序,程序的执行 ##### 缓存结构 -在计算机系统中,CPU高速缓存(CPU Cache,简称缓存)是用于减少处理器访问内存所需平均时间的部件;在存储体系中位于自顶向下的第二层,仅次于CPU寄存器;其容量远小于内存,但速度却可以接近处理器的频率 +在计算机系统中,CPU 高速缓存(CPU Cache,简称缓存)是用于减少处理器访问内存所需平均时间的部件;在存储体系中位于自顶向下的第二层,仅次于 CPU 寄存器;其容量远小于内存,但速度却可以接近处理器的频率 CPU 处理器速度远远大于在主内存中的,为了解决速度差异,在它们之间架设了多级缓存,如 L1、L2、L3 级别的缓存,这些缓存离CPU越近就越快,将频繁操作的数据缓存到这里,加快访问速度 @@ -2416,7 +2420,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) @@ -5424,7 +5428,7 @@ ReentrantLock相对于 synchronized 它具备如下特点: 5. **公平锁**:公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁 * ReentrantLock 可以设置公平锁,synchronized 中的锁是非公平的 6. 锁超时:尝试获取锁,超时获取不到直接放弃,不进入阻塞队列 - * ReentrantLock 可以设置超时时间,synchronized会一直等待 + * ReentrantLock 可以设置超时时间,synchronized 会一直等待 7. 锁绑定多个条件:一个 ReentrantLock 可以同时绑定多个 Condition 对象 8. 两者都是可重入锁 @@ -5860,8 +5864,8 @@ public void getLock() { `public void lockInterruptibly()`:获得可打断的锁 -* 如果没有竞争此方法就会获取lock对象锁 -* 如果有竞争就进入阻塞队列,可以被其他线程用interrupt打断 +* 如果没有竞争此方法就会获取 lock 对象锁 +* 如果有竞争就进入阻塞队列,可以被其他线程用 interrupt 打断 注意:如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断 @@ -9323,7 +9327,7 @@ IO 复用让单个进程具有处理多个 I/O 事件的能力,又被称为 Ev -##### 异步IO +#### 异步IO 应用进程执行 aio_read 系统调用会立即返回,给内核传递描述符、缓冲区指针、缓冲区大小等。应用进程可以继续执行不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。 @@ -10421,7 +10425,7 @@ 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 为非阻塞模式 diff --git a/SSM.md b/SSM.md index 20deeee..d2f7214 100644 --- a/SSM.md +++ b/SSM.md @@ -4,64 +4,71 @@ 框架是一款半成品软件,我们可以基于这个半成品软件继续开发,来完成我们个性化的需求! -ORM(Object Relational Mapping): 对象关系映射 +ORM(Object Relational Mapping): 对象关系映射 指的是持久化数据和实体对象的映射模式,解决面向对象与关系型数据库存在的互不匹配的现象 ![](https://gitee.com/seazean/images/raw/master/Frame/ORM介绍.png) **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 框架执行 SQL 并将结果映射为 Java 对象并返回。采用 ORM 思想解决了实体和数据库映射的问题,对 JDBC 进行了封装,屏蔽了JDBC底层API的调用细节,使我们不用操作 JDBC API,就可以完成对数据库的持久化操作。 MyBatis官网地址:http://www.mybatis.org/mybatis-3/ + + *** + + ## 基本操作 ### 相关API #### Resources -org.apache.ibatis.io.Resources : 加载资源的工具类 +org.apache.ibatis.io.Resources:加载资源的工具类 -`InputStream getResourceAsStream(String fileName)` : 通过类加载器返回指定资源的字节流 +`InputStream getResourceAsStream(String fileName)`:通过类加载器返回指定资源的字节流 -* 参数fileName是放在src的核心配置文件名:MyBatisConfig.xml +* 参数 fileName 是放在 src 的核心配置文件名:MyBatisConfig.xml #### SqlSessionFactoryBuilder -org.apache.ibatis.session.SqlSessionFactoryBuilder : 构建器,用来获取 SqlSessionFactory 工厂对象 +org.apache.ibatis.session.SqlSessionFactoryBuilder:构建器,用来获取 SqlSessionFactory 工厂对象 -`SqlSessionFactory build(InputStream is)` : 通过指定资源的字节输入流获取SqlSession工厂对象 +`SqlSessionFactory build(InputStream is)`:通过指定资源的字节输入流获取 SqlSession 工厂对象 #### SqlSessionFactory -org.apache.ibatis.session.SqlSessionFactory : 获取 SqlSession 构建者对象的工厂接口 +org.apache.ibatis.session.SqlSessionFactory:获取 SqlSession 构建者对象的工厂接口 -`SqlSession openSession()` : 获取SqlSession构建者对象,并开启手动提交事务 +`SqlSession openSession()`:获取 SqlSession 构建者对象,并开启手动提交事务 -`SqlSession openSession(boolean)` : 获取SqlSession构建者对象,参数为true开启自动提交事务 +`SqlSession openSession(boolean)`:获取 SqlSession 构建者对象,参数为 true 开启自动提交事务 #### SqlSession -org.apache.ibatis.session.SqlSession : 构建者对象接口。用于执行 SQL、管理事务、接口代理 +org.apache.ibatis.session.SqlSession:构建者对象接口,用于执行 SQL、管理事务、接口代理 -注:**设计update数据需要提交事务,或开启默认提交** +* SqlSession 代表和数据库的一次会话,用完必须关闭 +* SqlSession 和 connection 一样都是非线程安全,每次使用都应该去获取新的对象 + +注:**update 数据需要提交事务,或开启默认提交** | 方法 | 说明 | | ----------------------------------------------------- | ------------------------------ | @@ -81,23 +88,6 @@ org.apache.ibatis.session.SqlSession : 构建者对象接口。用于执行 SQL -### #{}和${} - -**#{}:**占位符,传入的内容会作为字符串,加上引号,以预编译的方式传入,将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值,有效的防止SQL注入,提高系统安全性 - -**${}:**拼接符,传入的内容会直接替换拼接,不会加上引号,可能存在sql注入的安全隐患 - -* 能用 #{} 的地方就用 #{},不用或少用 ${}, -* 必须使用 ${} 的情况: - * 表名作参数时,如:`SELECT * FROM ${tableName}` - * order by 时,如:`SELECT * FROM t_user ORDER BY ${columnName}` - -* Sql语句使用#{},properties文件内容获取使用${} - - - - - ### 映射配置 映射配置文件包含了数据和对象之间的映射关系以及要执行的 SQL 语句,放在src目录下, @@ -118,6 +108,7 @@ org.apache.ibatis.session.SqlSession : 构建者对象接口。用于执行 SQL * namespace:属性,名称空间 * 功能标签: + * < select >:查询功能标签 * :新增功能标签 * :修改功能标签 @@ -125,6 +116,10 @@ org.apache.ibatis.session.SqlSession : 构建者对象接口。用于执行 SQL * id:属性,唯一标识,配合名称空间使用 * resultType:指定结果映射对象类型,和对应的方法的返回值类型(全限定名)保持一致,但是如果返回值是List则和其泛型保持一致 * parameterType:指定参数映射对象类型,必须和对应的方法的参数类型(全限定名)保持一致 + * statementType:可选 STATEMENT,PREPARED 或 CALLABLE,默认值:PREPARED + * STATEMENT:直接操作 sql,不进行预编译,获取数据:$ Statement + * PREPARED:预处理参数,进行预编译,获取数据:# PreparedStatement + * CALLABLE:执行存储过程,CallableStatement * 参数获取方式: * SQL 获取参数:#{属性名} @@ -139,6 +134,10 @@ org.apache.ibatis.session.SqlSession : 构建者对象接口。用于执行 SQL +强烈推荐官方文档:https://mybatis.org/mybatis-3/zh/sqlmap-xml.html + + + *** @@ -170,7 +169,9 @@ org.apache.ibatis.session.SqlSession : 构建者对象接口。用于执行 SQL ``` - +* 调整设置 + + * :可以改变 Mybatis 运行时行为 * 起别名: @@ -179,19 +180,20 @@ org.apache.ibatis.session.SqlSession : 构建者对象接口。用于执行 SQL * :为全类名起别名的子标签 * type:指定全类名 * alias:指定别名 - - * :为指定包下所有类起别名的子标签(别名就是类名,首字母小写) + + * :为指定包下所有类起别名的子标签,别名就是类名,首字母小写 ```xml-dtd - - + + + ``` - + * 自带别名: - + | 别名 | 数据类型 | | ------- | ----------------- | | string | java.lang.String | @@ -203,7 +205,7 @@ org.apache.ibatis.session.SqlSession : 构建者对象接口。用于执行 SQL -* 配置环境 +* 配置环境,可以配置多个标签 * :配置数据库环境标签。default属性:指定哪个environment * :配置数据库环境子标签。id属性:唯一标识,与default对应 * :事务管理标签。type属性:默认JDBC事务 @@ -215,7 +217,14 @@ org.apache.ibatis.session.SqlSession : 构建者对象接口。用于执行 SQL * 引入映射配置文件 * :引入映射配置文件标签 - * :引入映射配置文件子标签。resource属性指定映射配置文件的名称 + * :引入映射配置文件子标签 + * resource:属性指定映射配置文件的名称 + * url:引用网路路径或者磁盘路径下的 sql 映射文件 + * :批量注册 + + + +参考官方文档:https://mybatis.org/mybatis-3/zh/configuration.html @@ -223,11 +232,30 @@ org.apache.ibatis.session.SqlSession : 构建者对象接口。用于执行 SQL +### #{}和${} + +**#{}:**占位符,传入的内容会作为字符串,加上引号,以**预编译**的方式传入,将 sql 中的 #{} 替换为 ? 号,调用 PreparedStatement 的 set方法来赋值,有效的防止 SQL 注入,提高系统安全性 + +**${}:**拼接符,传入的内容会直接替换拼接,不会加上引号,可能存在 sql 注入的安全隐患 + +* 能用 #{} 的地方就用 #{},不用或少用 ${}, +* 必须使用 ${} 的情况: + * 表名作参数时,如:`SELECT * FROM ${tableName}` + * order by 时,如:`SELECT * FROM t_user ORDER BY ${columnName}` + +* sql 语句使用 #{},properties 文件内容获取使用 ${} + + + +**** + + + ### 日志文件 -在日常开发过程中,排查问题时难免需要输出 MyBatis 真正执行的 SQL 语句、参数、结果等信息,我们就可以借助 LOG4J 的功能来实现执行信息的输出。 +在日常开发过程中,排查问题时需要输出 MyBatis 真正执行的 SQL 语句、参数、结果等信息,就可以借助 log4j 的功能来实现执行信息的输出。 -* 在核心配置文件根标签内配置log4j +* 在核心配置文件根标签内配置 log4j ```xml @@ -236,7 +264,7 @@ org.apache.ibatis.session.SqlSession : 构建者对象接口。用于执行 SQL ``` -* 在src目录下创建log4j.properties +* 在 src 目录下创建 log4j.properties ```properties # Global logging configuration @@ -275,7 +303,7 @@ org.apache.ibatis.session.SqlSession : 构建者对象接口。用于执行 SQL -### 配置实现 +### 代码实现 * 实体类 @@ -431,7 +459,6 @@ org.apache.ibatis.session.SqlSession : 构建者对象接口。用于执行 SQL //1.加载核心配置文件 //2.获取SqlSession工厂对象 //3.通过工厂对象获取SqlSession对象 - //SqlSession sqlSession = sqlSessionFactory.openSession(); SqlSession sqlSession = sqlSessionFactory.openSession(true); //4.执行映射配置文件中的sql语句,并接收结果 @@ -450,31 +477,90 @@ org.apache.ibatis.session.SqlSession : 构建者对象接口。用于执行 SQL } ``` - -*** +**** -## 代理开发 +### 批量操作 + +两种方式实现批量操作: + +* 标签属性:这种方式属于全局批量 -### 分层思想 + ```xml + + + + ``` -分层思想:控制层(controller)、业务层(service)、持久层(dao) + defaultExecutorType:配置默认的执行器 -传统实现方式:参考JDBCTemplate + * SIMPLE 就是普通的执行器 + * REUSE 执行器会重用预处理语句(PreparedStatement) + * BATCH 执行器不仅重用语句还会执行批量更新 -调用流程: +* SqlSession 会话内批量操作: -![](https://gitee.com/seazean/images/raw/master/Frame/分层思想调用流程.png) + ```java + public void testBatch() throws IOException{ + SqlSessionFactory sqlSessionFactory = getSqlSessionFactory(); + + //可以执行批量操作的sqlSession + SqlSession openSession = sqlSessionFactory.openSession(ExecutorType.BATCH); + long start = System.currentTimeMillis(); + try{ + EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class); + for (int i = 0; i < 10000; i++) { + mapper.addEmp(new Employee(UUID.randomUUID().toString().substring(0, 5), "b", "1")); + } + openSession.commit(); + long end = System.currentTimeMillis(); + //批量:(预编译sql一次==>设置参数===>10000次===>执行1次(类似管道)) + //非批量:(预编译sql=设置参数=执行)==》10000 耗时更多 + System.out.println("执行时长:"+(end-start)); + }finally{ + openSession.close(); + } + } + ``` + +* Spring 配置文件方式(applicationContext.xml): + + ```xml + + + + + + ``` + + ```java + @Autowired + private SqlSession sqlSession; + ``` + + +*** + + + +## 代理开发 + ### 代理规则 -传统方式实现DAO层,需要写接口和实现类。采用 Mybatis 的代理开发方式实现 DAO 层的开发,只需要编写Mapper 接口(相当于Dao 接口),由Mybatis 框架根据接口定义创建接口的动态代理对象。 +分层思想:控制层(controller)、业务层(service)、持久层(dao) + +调用流程: + +![](https://gitee.com/seazean/images/raw/master/Frame/分层思想调用流程.png) + +传统方式实现 DAO 层,需要写接口和实现类。采用 Mybatis 的代理开发方式实现 DAO 层的开发,只需要编写Mapper 接口(相当于 Dao 接口),由 Mybatis 框架根据接口定义创建接口的动态代理对象。 接口开发方式: @@ -483,13 +569,13 @@ org.apache.ibatis.session.SqlSession : 构建者对象接口。用于执行 SQL Mapper 接口开发需要遵循以下规范: -* Mapper.xml文件中的namespace与DAO层mapper接口的全类名相同 +* Mapper.xml 文件中的 namespace 与 DAO 层 mapper 接口的全类名相同 -* Mapper.xml文件中的增删改查标签的id属性和DAO层Mapper接口方法名相同 +* Mapper.xml 文件中的增删改查标签的id属性和 DAO 层 Mapper 接口方法名相同 -* Mapper.xml文件中的增删改查标签的parameterType属性和DAO层Mapper接口方法的参数相同 +* Mapper.xml 文件中的增删改查标签的 parameterType 属性和 DAO 层 Mapper 接口方法的参数相同 -* Mapper.xml文件中的增删改查标签的resultType属性和DAO层Mapper接口方法的返回值相同 +* Mapper.xml 文件中的增删改查标签的 resultType 属性和 DAO 层 Mapper 接口方法的返回值相同 ![](https://gitee.com/seazean/images/raw/master/Frame/接口代理方式实现DAO层.png) @@ -499,15 +585,11 @@ Mapper 接口开发需要遵循以下规范: -### 源码分析 - -* 动态代理对象如何生成的? +### 实现原理 - 通过动态代理开发模式,我们只编写一个接口,不写实现类,我们通过 **getMapper()** 方法最终获取到 org.apache.ibatis.binding.MapperProxy 代理对象,然后执行功能,而这个代理对象正是 MyBatis 使用了 JDK 的动态代理技术,帮助我们生成了代理实现类对象。从而可以进行相关持久化操作 +通过动态代理开发模式,我们只编写一个接口,不写实现类,通过 **getMapper()** 方法最终获取到 org.apache.ibatis.binding.MapperProxy 代理对象,而这个代理对象是 MyBatis 使用了 JDK 的动态代理技术 -* 方法是如何执行的? - - 动态代理实现类对象在执行方法的时候最终调用了 **MapperMethod.execute()** 方法,这个方法中通过 switch case语句根据操作类型来判断是新增、修改、删除、查询操作,最后一步回到了 MyBatis 最原生的**SqlSession方式来执行增删改查**。 +动态代理实现类对象在执行方法时最终调用了 **MapperMethod.execute()** 方法,这个方法中通过 switch case 语句根据操作类型来判断是新增、修改、删除、查询操作,最后一步回到了 MyBatis 最原生的 **SqlSession 方式来执行增删改查**。 * 代码实现: @@ -559,31 +641,85 @@ Mapper 接口开发需要遵循以下规范: -## 动态SQL -逻辑复杂时,MyBatis映射配置文件中,SQL是动态变化的 -### where标签 +## 结果映射 -```xml-dtd -:条件标签。有动态条件,则使用该标签代替 WHERE关键字 -``` +### 相关标签 +:返回的是一个集合,要写集合中元素的类型 +:返回一条记录的Map,key 是列名,value 是对应的值,用来配置字段和对象属性的映射关系标签,结果映射(和 resultType 二选一) -### if标签 +* id 属性:唯一标识 +* type 属性:实体对象类型 -基本格式: +内的核心配置文件标签: -```xml - - 查询条件拼接 - -``` +* :配置主键映射关系标签。 +* :配置非主键映射关系标签 + * column 属性:表中字段名称 + * property 属性: 实体对象变量名称 -我们根据实体类的不同取值,使用不同的 SQL语句来进行查询。比如在 id如果不为空时可以根据id查询,如果username 不同空时还要加入用户名作为条件。这种情况在我们的多条件组合查询中经常会碰到。 +* :配置被包含对象的映射关系标签,嵌套封装结果集(多对一、一对一) + * property 属性:被包含对象的变量名,要进行映射的属性名(Java 中的 Bean 类) + * javaType 属性:被包含对象的数据类型,要进行映射的属性的类型 -* StudentMapper.xml +* :配置被包含集合对象的映射关系标签,嵌套封装结果集(一对多、多对多) + * property 属性:被包含集合对象的变量名 + * ofType 属性:集合中保存的对象数据类型 + +* :鉴别器,用来判断某列的值,根据得到某列的不同值做出不同自定义的封装行为 + +自定义封装规则可以将数据库中比较复杂的数据类型映射为 javaBean 中的属性 + + + +**** + + + +### 一对一 + +一对一实现: + +* 数据准备 + + ```mysql + CREATE TABLE person( + id INT PRIMARY KEY AUTO_INCREMENT, + name VARCHAR(20), + age INT + ); + INSERT INTO person VALUES (NULL,'张三',23),(NULL,'李四',24),(NULL,'王五',25); + + CREATE TABLE card( + id INT PRIMARY KEY AUTO_INCREMENT, + number VARCHAR(30), + pid INT, + CONSTRAINT cp_fk FOREIGN KEY (pid) REFERENCES person(id) + ); + INSERT INTO card VALUES (NULL,'12345',1),(NULL,'23456',2),(NULL,'34567',3); + ``` + +* bean 类 + + ```java + public class Card { + private Integer id; //主键id + private String number; //身份证号 + private Person p; //所属人的对象 + ...... + } + + public class Person { + private Integer id; //主键id + private String name; //人的姓名 + private Integer age; //人的年龄 + } + ``` + +* 配置文件 OneToOneMapper.xml,MyBatisConfig.xml 需要引入(可以把 bean 包下起别名) ```xml @@ -591,49 +727,49 @@ Mapper 接口开发需要遵循以下规范: PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> - - + + + + + + + + + + + + + + + + ``` -* MyBatisConfig.xml,引入映射配置文件 +* 核心配置文件 MyBatisConfig.xml ```xml + - - + + + ``` -* DAO层Mapper接口 - - ```java - public interface StudentMapper { - //多条件查询 - public abstract List selectCondition(Student stu); - } - ``` - -* 实现类 +* 测试类 ```java - public class DynamicTest01 { + public class Test01 { @Test - public void selectCondition() throws Exception{ + public void selectAll() throws Exception{ //1.加载核心配置文件 InputStream is = Resources.getResourceAsStream("MyBatisConfig.xml"); @@ -643,22 +779,17 @@ Mapper 接口开发需要遵循以下规范: //3.通过工厂对象获取SqlSession对象 SqlSession sqlSession = ssf.openSession(true); - //4.获取StudentMapper接口的实现类对象 - StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); - - Student stu = new Student(); - stu.setId(2); - stu.setName("李四"); - //stu.setAge(24); + //4.获取OneToOneMapper接口的实现类对象 + OneToOneMapper mapper = sqlSession.getMapper(OneToOneMapper.class); //5.调用实现类的方法,接收结果 - List list = mapper.selectCondition(stu); + List list = mapper.selectAll(); //6.处理结果 - for (Student student : list) { - System.out.println(student); + for (Card c : list) { + System.out.println(c); } - + //7.释放资源 sqlSession.close(); is.close(); @@ -668,107 +799,157 @@ Mapper 接口开发需要遵循以下规范: + + *** -### foreach标签 +### 一对多 -基本格式: +一对多实现: -```xml -:循环遍历标签。适用于多个参数或者的关系。 - - 获取参数 - -``` +* 数据准备 -属性: + ```mysql + CREATE TABLE classes( + id INT PRIMARY KEY AUTO_INCREMENT, + name VARCHAR(20) + ); + INSERT INTO classes VALUES (NULL,'程序一班'),(NULL,'程序二班') + + CREATE TABLE student( + id INT PRIMARY KEY AUTO_INCREMENT, + name VARCHAR(30), + age INT, + cid INT, + CONSTRAINT cs_fk FOREIGN KEY (cid) REFERENCES classes(id) + ); + INSERT INTO student VALUES (NULL,'张三',23,1),(NULL,'李四',24,1),(NULL,'王五',25,2); + ``` -* collection:参数容器类型, (list-集合, array-数组)。 -* open:开始的 SQL 语句。 -* close:结束的 SQL 语句。 -* item:参数变量名。 -* separator:分隔符。 +* bean 类 -需求:循环执行sql的拼接操作,SELECT * FROM student WHERE id IN (1,2,5) + ```java + public class Classes { + private Integer id; //主键id + private String name; //班级名称 + private List students; //班级中所有学生对象 + ........ + } + public class Student { + private Integer id; //主键id + private String name; //学生姓名 + private Integer age; //学生年龄 + } + ``` -* StudentMapper.xml片段 +* 映射配置文件 ```xml - + SELECT c.id cid,c.name cname,s.id sid,s.name sname,s.age sage FROM classes c,student s WHERE c.id=s.cid + ``` -* 测试代码片段 +* 代码实现片段 ```java - //4.获取StudentMapper接口的实现类对象 - StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); + //4.获取OneToManyMapper接口的实现类对象 + OneToManyMapper mapper = sqlSession.getMapper(OneToManyMapper.class); - List ids = new ArrayList<>(); - Collections.addAll(list, 1, 2); //5.调用实现类的方法,接收结果 - List list = mapper.selectByIds(ids); + List classes = mapper.selectAll(); - for (Student student : list) { - System.out.println(student); + //6.处理结果 + for (Classes cls : classes) { + System.out.println(cls.getId() + "," + cls.getName()); + List students = cls.getStudents(); + for (Student student : students) { + System.out.println("\t" + student); + } } ``` - - -*** - - -### SQL片段抽取 - -将一些重复性的 SQL 语句进行抽取,以达到复用的效果 -格式: -```xml -抽取的SQL语句 - -``` -使用: +*** -```xml -SELECT * FROM student - -``` +### 多对多 +学生课程例子,中间表不需要 bean 实体类 -*** +* 数据准备 + ```mysql + CREATE TABLE course( + id INT PRIMARY KEY AUTO_INCREMENT, + name VARCHAR(20) + ); + INSERT INTO course VALUES (NULL,'语文'),(NULL,'数学'); + + CREATE TABLE stu_cr( + id INT PRIMARY KEY AUTO_INCREMENT, + sid INT, + cid INT, + CONSTRAINT sc_fk1 FOREIGN KEY (sid) REFERENCES student(id), + CONSTRAINT sc_fk2 FOREIGN KEY (cid) REFERENCES course(id) + ); + INSERT INTO stu_cr VALUES (NULL,1,1),(NULL,1,2),(NULL,2,1),(NULL,2,2); + ``` +* bean类 -## 分页插件 + ```java + public class Student { + private Integer id; //主键id + private String name; //学生姓名 + private Integer age; //学生年龄 + private List courses; // 学生所选择的课程集合 + } + public class Course { + private Integer id; //主键id + private String name; //课程名称 + } + ``` -### 分页介绍 +* 配置文件 -![](https://gitee.com/seazean/images/raw/master/Frame/分页介绍.png) + ```xml + + + + + + + + + + + + + + ``` -* 分页可以将很多条结果进行分页显示。如果当前在第一页,则没有上一页。如果当前在最后一页,则没有下一页,需要明确当前是第几页,这一页中显示多少条结果。 -* MyBatis 是不带分页功能的,如果想实现分页功能,需要手动编写 LIMIT 语句,不同的数据库实现分页的 SQL 语句也是不同,手写分页 成本较高。 -* PageHelper:第三方分页助手,将复杂的分页操作进行封装,从而让分页功能变得非常简单 @@ -776,93 +957,66 @@ Mapper 接口开发需要遵循以下规范: -### 插件使用 - -开发步骤: +### 鉴别器 -1. 导入与PageHelper的jar包 +需求:如果查询结果是女性,则把部门信息查询出来,否则不查询 ;如果是男性,把 last_name 这一列的值赋值给emai -2. 在mybatis核心配置文件中配置PageHelper插件 - 注意:分页助手的插件配置在通用mapper之前 +```xml + + + + + + + + + + + + + + + +``` - ```xml - - - - - - - ......... - ``` -3. 与MySQL分页查询页数计算公式不同 - static Page startPage(int pageNum, int pageSize) : pageNum第几页,pageSize页面大小 - ```java - @Test - public void selectAll() { - //第一页:显示2条数据 - PageHelper.startPage(1,2); - List students = sqlSession.selectList("StudentMapper.selectAll"); - for (Student student : students) { - System.out.println(student); - } - } - ``` - **** -### 参数获取 - -PageInfo构造方法: - -* `PageInfo info = new PageInfo<>(list)` : list是SQL执行返回的结果集合,参考上一节 - -PageInfo相关API: - -1. startPage():设置分页参数 -2. PageInfo:分页相关参数功能类。 -3. getTotal():获取总条数 -4. getPages():获取总页数 -5. getPageNum():获取当前页 -6. getPageSize():获取每页显示条数 -7. getPrePage():获取上一页 -8. getNextPage():获取下一页 -9. isIsFirstPage():获取是否是第一页 -10. isIsLastPage():获取是否是最后一页 - - - -*** - +### 延迟加载 +#### 两种加载 -## 多表操作 +立即加载:只要调用方法,马上发起查询 -### 标签配置 +延迟加载:在需要用到数据时才进行加载,不需要用到数据时就不加载数据,延迟加载也称懒加载。 -核心配置文件标签: +优点: 先从单表查询,需要时再从关联表去关联查询,提高数据库性能,因为查询单表要比关联查询多张表速度要快,节省资源 -* :配置字段和对象属性的映射关系标签。 - * id 属性:唯一标识 - * type 属性:实体对象类型 +坏处:只有当需要用到数据时,才会进行数据库查询,这样在大批量数据查询时,查询工作也要消耗时间,所以可能造成用户等待时间变长,造成用户体验下降 -* :配置主键映射关系标签。 -* :配置非主键映射关系标签。 - * column 属性:表中字段名称 - * property 属性: 实体对象变量名称 +核心配置文件 -* :配置被包含对象的映射关系标签。(多对一、一对一) - * property 属性:被包含对象的变量名,要进行映射的属性名 - * javaType 属性:被包含对象的数据类型,要进行映射的属性的类型 +| 标签名 | 描述 | 默认值 | +| --------------------- | ------------------------------------------------------------ | ------ | +| lazyLoadingEnabled | 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。特定关联关系中可通过设置 `fetchType` 属性来覆盖该项的开关状态。 | false | +| aggressiveLazyLoading | 开启时,任一方法的调用都会加载该对象的所有延迟加载属性。否则每个延迟加载属性会按需加载(参考 lazyLoadTriggerMethods) | false | -* :配置被包含集合对象的映射关系标签。(一对多、多对多) - * property 属性:被包含集合对象的变量名 - * ofType 属性:集合中保存的对象数据类型 +```xml + + + + +``` @@ -870,379 +1024,527 @@ PageInfo相关API: -### 一对一 - -一对一实现: +#### assocation -* 数据准备 +分布查询:先按照身份 id 查询所属人的 id、然后根据所属人的 id 去查询人的全部信息,这就是分步查询 - ```mysql - CREATE TABLE person( - id INT PRIMARY KEY AUTO_INCREMENT, - name VARCHAR(20), - age INT - ); - INSERT INTO person VALUES (NULL,'张三',23),(NULL,'李四',24),(NULL,'王五',25); - - CREATE TABLE card( - id INT PRIMARY KEY AUTO_INCREMENT, - number VARCHAR(30), - pid INT, - CONSTRAINT cp_fk FOREIGN KEY (pid) REFERENCES person(id) - ); - INSERT INTO card VALUES (NULL,'12345',1),(NULL,'23456',2),(NULL,'34567',3); - ``` +* 映射配置文件 OneToOneMapper.xml -* bean类 + 一对一映射: - ```java - public class Card { - private Integer id; //主键id - private String number; //身份证号 - private Person p; //所属人的对象 - ...... - } - - public class Person { - private Integer id; //主键id - private String name; //人的姓名 - private Integer age; //人的年龄 - } - ``` - -* 配置文件OneToOneMapper.xml,MyBatisConfig.xml需要引入(可以把bean包下起别名) + * column 属性表示要调用的其它的 select 标签中传入参数 + * select 属性表示调用其它的 select 标签 + * fetchType="lazy" 表示延迟加载(局部配置,只有配置了这个的地方才会延迟加载) ```xml - - - - - + - - - - - + + - + SELECT * FROM card ``` -* 核心配置文件MyBatisConfig.xml +* PersonMapper.xml ```xml - - - - - - + + + ``` - -* 测试类 + +* PersonMapper.java ```java - public class Test01 { - @Test - public void selectAll() throws Exception{ - //1.加载核心配置文件 - InputStream is = Resources.getResourceAsStream("MyBatisConfig.xml"); - - //2.获取SqlSession工厂对象 + public interface PersonMapper { + /** + * 为了演示分步查询的一对多另写的一个方法 + */ + User findPersonByid(int id); + } + ``` + +* 测试文件 + + ```java + public class Test01 { + @Test + public void selectAll() throws Exception{ + InputStream is = Resources.getResourceAsStream("MyBatisConfig.xml"); SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(is); - - //3.通过工厂对象获取SqlSession对象 SqlSession sqlSession = ssf.openSession(true); - - //4.获取OneToOneMapper接口的实现类对象 OneToOneMapper mapper = sqlSession.getMapper(OneToOneMapper.class); - - //5.调用实现类的方法,接收结果 + //调用实现类的方法,接收结果 List list = mapper.selectAll(); - - //6.处理结果 - for (Card c : list) { - System.out.println(c); - } - - //7.释放资源 + + //不能遍历,遍历就是相当于使用了该数据,需要加载,不遍历就是没有使用。 + + //释放资源 sqlSession.close(); is.close(); } } ``` - - -*** - - - -### 一对多 -一对多实现: -* 数据准备 +*** - ```mysql - CREATE TABLE classes( - id INT PRIMARY KEY AUTO_INCREMENT, - name VARCHAR(20) - ); - INSERT INTO classes VALUES (NULL,'程序一班'),(NULL,'程序二班') - - CREATE TABLE student( - id INT PRIMARY KEY AUTO_INCREMENT, - name VARCHAR(30), - age INT, - cid INT, - CONSTRAINT cs_fk FOREIGN KEY (cid) REFERENCES classes(id) - ); - INSERT INTO student VALUES (NULL,'张三',23,1),(NULL,'李四',24,1),(NULL,'王五',25,2); - ``` -* bean类 - ```java - public class Classes { - private Integer id; //主键id - private String name; //班级名称 - private List students; //班级中所有学生对象 - ........ - } - public class Student { - private Integer id; //主键id - private String name; //学生姓名 - private Integer age; //学生年龄 - } - ``` +#### collection -* 映射配置文件 +同样在一对多关系配置的 结点中配置延迟加载策略, 结点中也有 select 属性和 column 属性。 +* 映射配置文件 OneToManyMapper.xml + + 一对多映射: + + * column 是用于指定使用哪个字段的值作为条件查询 + * select 是用于指定查询账户的唯一标识(账户的dao全限定类名加上方法名称) + ```xml - - + + - - - - + - + SELECT * FROM classes ``` -* 代码实现片段 +* StudentMapper.xml - ```java - //4.获取OneToManyMapper接口的实现类对象 - OneToManyMapper mapper = sqlSession.getMapper(OneToManyMapper.class); - - //5.调用实现类的方法,接收结果 - List classes = mapper.selectAll(); - - //6.处理结果 - for (Classes cls : classes) { - System.out.println(cls.getId() + "," + cls.getName()); - List students = cls.getStudents(); - for (Student student : students) { - System.out.println("\t" + student); - } - } + ```xml + + + ``` -*** +**** -### 多对多 -标签属性同上 -学生课程例子,中间表不需要bean实体类 +## 注解开发 -* 数据准备 +### 单表操作 - ```mysql - CREATE TABLE course( - id INT PRIMARY KEY AUTO_INCREMENT, - name VARCHAR(20) - ); - INSERT INTO course VALUES (NULL,'语文'),(NULL,'数学'); +注解可以简化开发操作,省略映射配置文件的编写 + +常用注解: + +* @Select(“查询的 SQL 语句”):执行查询操作注解 +* @Insert(“插入的 SQL 语句”):执行新增操作注解 +* @Update(“修改的 SQL 语句”):执行修改操作注解 +* @Delete(“删除的 SQL 语句”):执行删除操作注解 + +参数注解: + +* @Param:当 SQL 语句需要多个(大于1)参数时,用来指定参数的对应规则 + +核心配置文件配置映射关系: + +```xml + + + + + + + +``` + +基本增删改查: + +* 创建Mapper接口 + + ```java + package mapper; + public interface StudentMapper { + //查询全部 + @Select("SELECT * FROM student") + public abstract List selectAll(); - CREATE TABLE stu_cr( - id INT PRIMARY KEY AUTO_INCREMENT, - sid INT, - cid INT, - CONSTRAINT sc_fk1 FOREIGN KEY (sid) REFERENCES student(id), - CONSTRAINT sc_fk2 FOREIGN KEY (cid) REFERENCES course(id) - ); - INSERT INTO stu_cr VALUES (NULL,1,1),(NULL,1,2),(NULL,2,1),(NULL,2,2); + //新增数据 + @Insert("INSERT INTO student VALUES (#{id},#{name},#{age})") + public abstract Integer insert(Student student); + + //修改操作 + @Update("UPDATE student SET name=#{name},age=#{age} WHERE id=#{id}") + public abstract Integer update(Student student); + + //删除操作 + @Delete("DELETE FROM student WHERE id=#{id}") + public abstract Integer delete(Integer id); + + } + ``` + +* 修改MyBatis的核心配置文件 + + ```xml + + + ``` * bean类 ```java public class Student { - private Integer id; //主键id - private String name; //学生姓名 - private Integer age; //学生年龄 - private List courses; // 学生所选择的课程集合 - } - public class Course { - private Integer id; //主键id - private String name; //课程名称 + private Integer id; + private String name; + private Integer age; } ``` -* 配置文件 +* 测试类 - ```xml - - - - - + ```java + @Test + public void selectAll() throws Exception{ + //1.加载核心配置文件 + InputStream is = Resources.getResourceAsStream("MyBatisConfig.xml"); - - - - - - - + //2.获取SqlSession工厂对象 + SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(is); + + //3.通过工厂对象获取SqlSession对象 + SqlSession sqlSession = ssf.openSession(true); + + //4.获取StudentMapper接口的实现类对象 + StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); + + //5.调用实现类对象中的方法,接收结果 + List list = mapper.selectAll(); + + //6.处理结果 + for (Student student : list) { + System.out.println(student); + } + + //7.释放资源 + sqlSession.close(); + is.close(); + } ``` - +*** -**** +### 多表操作 -## 缓存机制 +#### 相关注解 -### 缓存概述 +实现复杂关系映射之前我们可以在映射文件中通过配置 来实现,使用注解开发后,可以使用@Results 注解,@Result 注解,@One 注解,@Many 注解组合完成复杂关系的配置 -缓存:缓存就是一块内存空间,保存临时数据 +| 注解 | 说明 | +| ------------- | ------------------------------------------------------------ | +| @Results | 代替标签,注解中使用单个@Result注解或者@Result集合
使用格式:@Results({ @Result(), @Result() })或@Results({ @Result() }) | +| @Result | 代替标签,@Result中属性介绍:
column:数据库的列名 property:封装类的变量名
one:需要使用@One注解(@Result(one = @One))
Many:需要使用@Many注解(@Result(many= @Many)) | +| @One(一对一) | 代替标签,多表查询的关键,用来指定子查询返回单一对象
select:指定调用Mapper接口中的某个方法
使用格式:@Result(column="", property="", one=@One(select="")) | +| @Many(多对一) | 代替标签,多表查询的关键,用来指定子查询返回对象集合
select:指定调用Mapper接口中的某个方法
使用格式:@Result(column="", property="", many=@Many(select="")) | -作用:将数据源(数据库或者文件)中的数据读取出来存放到缓存中,再次获取时直接从缓存中获取,可以减少和数据库交互的次数,提升程序的性能 -缓存适用: -+ 适用于缓存的:经常查询但不经常修改的(eg: 省市,类别数据),数据的正确与否对最终结果影响不大的 -+ 不适用缓存的:经常改变的数据 , 敏感数据(例如:股市的牌价,银行的汇率,银行卡里面的钱)等等 +*** -缓存类别: -* 一级缓存:sqlSession级别的缓存,自带的(不需要配置)不可卸载的(必须使用),一级缓存的生命周期与sqlSession一致 -* 二级缓存:SqlSessionFactory的缓存,只要是同一个SqlSessionFactory创建的SqlSession就共享二级缓存的内容,并且可以操作二级缓存。二级缓存的使用,需要手动开启(需要配置的) +#### 一对一 + +身份证对人 +* PersonMapper接口 -开启缓存:配置核心配置文件中标签 + ```java + public interface PersonMapper { + //根据id查询 + @Select("SELECT * FROM person WHERE id=#{id}") + public abstract Person selectById(Integer id); + } + ``` -* cacheEnabled:全局性地开启或关闭所有映射器配置文件中已配置的任何缓存,默认true +* CardMapper接口 + ```java + public interface CardMapper { + //查询全部 + @Select("SELECT * FROM card") + @Results({ + @Result(column = "id",property = "id"), + @Result(column = "number",property = "number"), + @Result( + property = "p", // 被包含对象的变量名 + javaType = Person.class, // 被包含对象的实际数据类型 + column = "pid", // 根据查询出的card表中的pid字段来查询person表 + /* + one、@One 一对一固定写法 + select属性:指定调用哪个接口中的哪个方法 + */ + one = @One(select = "one_to_one.PersonMapper.selectById") + ) + }) + public abstract List selectAll(); + } + ``` +* 测试类(详细代码参考单表操作) + ```java + //1.加载核心配置文件 + //2.获取SqlSession工厂对象 + //3.通过工厂对象获取SqlSession对象 + + //4.获取StudentMapper接口的实现类对象 + CardMapper mapper = sqlSession.getMapper(CardMapper.class); + //5.调用实现类对象中的方法,接收结果 + List list = mapper.selectAll(); + ``` + *** -### 一级缓存 +#### 一对多 + +班级和学生 + +* StudentMapper接口 + + ```java + public interface StudentMapper { + //根据cid查询student表 cid是外键约束列 + @Select("SELECT * FROM student WHERE cid=#{cid}") + public abstract List selectByCid(Integer cid); + } + ``` + +* ClassesMapper接口 + + ```java + public interface ClassesMapper { + //查询全部 + @Select("SELECT * FROM classes") + @Results({ + @Result(column = "id", property = "id"), + @Result(column = "name", property = "name"), + @Result( + property = "students", //被包含对象的变量名 + javaType = List.class, //被包含对象的实际数据类型 + column = "id", //根据id字段查询student表 + many = @Many(select = "one_to_many.StudentMapper.selectByCid") + ) + }) + public abstract List selectAll(); + } + ``` + +* 测试类 + + ```java + //4.获取StudentMapper接口的实现类对象 + ClassesMapper mapper = sqlSession.getMapper(ClassesMapper.class); + //5.调用实现类对象中的方法,接收结果 + List classes = mapper.selectAll(); + ``` + + + +*** + -一级缓存是SqlSession级别的缓存,调用SqlSession的修改添加删除、commit()、close()等方法时会清空一级缓存 -![](https://gitee.com/seazean/images/raw/master/Frame/MyBatis一级缓存.png) +#### 多对多 -第一次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,如果没有,从数据库查询用户信息,得到用户信息,将用户信息存储到一级缓存中;第二次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,缓存中有,直接从缓存中获取用户信息。 +学生和课程 -如果 sqlSession 去执行 commit操作(执行插入、更新、删除),清空 SqlSession 中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。 +* SQL查询语句 -* 测试一级缓存存在 + ```mysql + SELECT DISTINCT s.id,s.name,s.age FROM student s,stu_cr sc WHERE sc.sid=s.id + SELECT c.id,c.name FROM stu_cr sc,course c WHERE sc.cid=c.id AND sc.sid=#{id} + ``` + +* CourseMapper接口 ```java - public void testFirstLevelCache(){ - //1. 获取sqlSession对象 - SqlSession sqlSession = SqlSessionFactoryUtils.openSession(); - //2. 通过sqlSession对象获取UserDao接口的代理对象 - UserDao userDao1 = sqlSession.getMapper(UserDao.class); - //3. 调用UserDao接口的代理对象的findById方法获取信息 - User user1 = userDao1.findById(1); - System.out.println(user1); - - //sqlSession.clearCache() 清空缓存 - - UserDao userDao2 = sqlSession.getMapper(UserDao.class); - User user = userDao.findById(1); - System.out.println(user2); - - //4.测试两次结果是否一样 - System.out.println(user1 == user2);//true - - //5. 提交事务关闭资源 - SqlSessionFactoryUtils.commitAndClose(sqlSession); + public interface CourseMapper { + //根据学生id查询所选课程 + @Select("SELECT c.id,c.name FROM stu_cr sc,course c WHERE sc.cid=c.id AND sc.sid=#{id}") + public abstract List selectBySid(Integer id); + } + ``` + +* StudentMapper接口 + + ```java + public interface StudentMapper { + //查询全部 + @Select("SELECT DISTINCT s.id,s.name,s.age FROM student s,stu_cr sc WHERE sc.sid=s.id") + @Results({ + @Result(column = "id",property = "id"), + @Result(column = "name",property = "name"), + @Result(column = "age",property = "age"), + @Result( + property = "courses", //被包含对象的变量名 + javaType = List.class, //被包含对象的实际数据类型 + column = "id", //根据查询出的student表中的id字段查询中间表和课程表 + many = @Many(select = "many_to_many.CourseMapper.selectBySid") + ) + }) + public abstract List selectAll(); } + ``` -* 调用sqlSession的commit()或者clearCache()或者close()都能清除一级缓存 +* 测试类 - 1. sqlSession.close() - 2. sqlSession.commit() - 3. **sqlSession.clearCache()** - 4. 数据发生增删改 + ```java + //4.获取StudentMapper接口的实现类对象 + StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); + //5.调用实现类对象中的方法,接收结果 + List students = mapper.selectAll(); + ``` -**** +*** + + + + + +## 缓存机制 + +### 缓存概述 + +缓存:缓存就是一块内存空间,保存临时数据 + +作用:将数据源(数据库或者文件)中的数据读取出来存放到缓存中,再次获取时直接从缓存中获取,可以减少和数据库交互的次数,提升程序的性能 + +缓存适用: + ++ 适用于缓存的:经常查询但不经常修改的,数据的正确与否对最终结果影响不大的 ++ 不适用缓存的:经常改变的数据 , 敏感数据(例如:股市的牌价,银行的汇率,银行卡里面的钱)等等 + +缓存类别: + +* 一级缓存:sqlSession 级别的缓存,又叫本地会话缓存,自带的(不需要配置),一级缓存的生命周期与 sqlSession 一致。在操作数据库时需要构造 sqlSession 对象,在对象中有一个数据结构(HashMap)用于存储缓存数据,不同的 sqlSession 之间的缓存数据区域是互相不影响的 +* 二级缓存:mapper(namespace)级别的缓存,二级缓存的使用,需要手动开启(需要配置)。多个 SqlSession 去操作同一个 Mapper 的 sql,可以共用二级缓存,二级缓存是跨 SqlSession 的 + +开启缓存:配置核心配置文件中标签 + +* cacheEnabled:true 表示全局性地开启所有映射器配置文件中已配置的任何缓存 + +![](https://gitee.com/seazean/images/raw/master/Frame/MyBatis-缓存的实现原理.png) + + + +参考文章:https://www.cnblogs.com/ysocean/p/7342498.html + + + +*** + + + +### 一级缓存 + +一级缓存是 SqlSession 级别的缓存 + + + +工作流程:第一次发起查询用户 id 为 1 的用户信息,先去找缓存中是否有 id 为 1 的用户信息,如果没有,从数据库查询用户信息,得到用户信息,将用户信息存储到一级缓存中;第二次发起查询用户 id 为 1 的用户信息,先去找缓存中是否有 id 为 1 的用户信息,缓存中有,直接从缓存中获取用户信息。 + +一级缓存的失效: + +* SqlSession 不同时 +* SqlSession 相同,查询条件不同时(还未缓存该数据) +* SqlSession 相同,手动清除了一级缓存,调用`openSession.clearCache()` +* SqlSession 相同,执行 commit 操作(执行插入、更新、删除),清空 SqlSession 中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。 + +测试一级缓存存在 + +```java +public void testFirstLevelCache(){ + //1. 获取sqlSession对象 + SqlSession sqlSession = SqlSessionFactoryUtils.openSession(); + //2. 通过sqlSession对象获取UserDao接口的代理对象 + UserDao userDao1 = sqlSession.getMapper(UserDao.class); + //3. 调用UserDao接口的代理对象的findById方法获取信息 + User user1 = userDao1.findById(1); + System.out.println(user1); + + //sqlSession.clearCache() 清空缓存 + + UserDao userDao2 = sqlSession.getMapper(UserDao.class); + User user = userDao.findById(1); + System.out.println(user2); + + //4.测试两次结果是否一样 + System.out.println(user1 == user2);//true + + //5. 提交事务关闭资源 + SqlSessionFactoryUtils.commitAndClose(sqlSession); +} +``` -### 二级缓存 -二级缓存是SqlSessionFactory的缓存。只要是同一个SqlSessionFactory创建的SqlSession就共享二级缓存的内容,并且可以操作二级缓存 -![](https://gitee.com/seazean/images/raw/master/Frame/MyBatis二级缓存.png) +**** + + + +### 二级缓存 +二级缓存是 mapper 的缓存,只要是同一个 mapper 的 SqlSession 就共享二级缓存的内容,并且可以操作二级缓存 +工作流程:一个会话查询一条数据,这个数据就会被存放在当前会话的一级缓存中,如果**会话关闭**,一级缓存中的数据会被保存到二级缓存 -二级缓存的开启与关闭: +二级缓存的基本使用: -1. 在MyBatisConfig.xml文件开启二级缓存。**cacheEnabled默认值为true**,所以这一步可以省略不配置 +1. 在 MyBatisConfig.xml 文件开启二级缓存,**cacheEnabled 默认值为 true**,所以这一步可以省略不配置 ```xml @@ -1251,28 +1553,48 @@ PageInfo相关API: ``` -2. 配置Mapper映射文件 +2. 配置 Mapper 映射文件 `` 标签表示当前这个 mapper 映射将使用二级缓存,区分的标准就看 mapper 的 namespace 值 ```xml - - - - - delete from user where id=#{id} - + + 则表示所有属性使用默认值 ``` -3. 配置statement上面的useCache属性 + eviction(清除策略): + + - `LRU` – 最近最少使用:移除最长时间不被使用的对象,默认 + - `FIFO` – 先进先出:按对象进入缓存的顺序来移除它们 + - `SOFT` – 软引用:基于垃圾回收器状态和软引用规则移除对象 + - `WEAK` – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象 + + flushInterval(刷新间隔):可以设置为任意的正整数, 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新 + + size(引用数目):缓存存放多少元素,默认值是 1024 + + readOnly(只读):可以被设置为 true 或 false + + * 只读的缓存会给所有调用者返回缓存对象的相同实例,因此这些对象不能被修改,促进了性能提升 + * 可读写的缓存会(通过序列化)返回缓存对象的拷贝, 速度上会慢一些,但是更安全,因此默认值是 false + + type:指定自定义缓存的全类名,实现 Cache 接口即可 + +3. 要进行二级缓存的类必须实现 java.io.Serializable 接口,可以使用序列化方式来保存对象。 + + ```java + public class User implements Serializable{} + ``` + +相关属性: + +1. select 标签的 useCache 属性 - 映射文件中的``标签中设置`useCache=”true”`代表当前statement要使用二级缓存。 + 注意:针对每次查询都需要最新的数据 sql,要设置成useCache=false,禁用二级缓存 ```xml ``` -4. 要进行二级缓存的类必须实现java.io.Serializable 接口,可以使用序列化方式来保存对象。 +2. 每个增删改标签都有 flushCache 属性,默认为 true,代表在执行增删改之后就会清除一、二级缓存,而查询标签默认值为 false,所以查询不会清空缓存 - ```java - public class User implements Serializable{} - ``` +3. localCacheScope:本地缓存作用域,默认值为 SESSION,当前会话的所有数据保存在会话缓存中,设置为 STATEMENT 禁用一级缓存 +*** -**** +### 自定义 +自定义缓存 -## 延迟加载 +```xml + +``` -### 两种加载 +type 属性指定的类必须实现 org.apache.ibatis.cache.Cache 接口,且提供一个接受 String 参数作为 id 的构造器 -立即加载:只要调用方法,马上发起查询 +```java +public interface Cache { + String getId(); + int getSize(); + void putObject(Object key, Object value); + Object getObject(Object key); + boolean hasKey(Object key); + Object removeObject(Object key); + void clear(); +} +``` -延迟加载:在需要用到数据时才进行加载,不需要用到数据时就不加载数据,延迟加载也称懒加载。 +缓存的配置,只需要在缓存实现中添加公有的 JavaBean 属性,然后通过 cache 元素传递属性值,例如在缓存实现上调用一个名为 `setCacheFile(String file)` 的方法: + +```xml + + + +``` + +* 可以使用所有简单类型作为 JavaBean 属性的类型,MyBatis 会进行转换。 +* 可以使用占位符(如 `${cache.file}`),以便替换成在配置文件属性中定义的值 + +MyBatis 支持在所有属性设置完毕之后,调用一个初始化方法, 如果想要使用这个特性,可以在自定义缓存类里实现 `org.apache.ibatis.builder.InitializingObject` 接口 + +```java +public interface InitializingObject { + void initialize() throws Exception; +} +``` + +注意:对缓存的配置(如清除策略、可读或可读写等),不能应用于自定义缓存 + +对某一命名空间的语句,只会使用该命名空间的缓存进行缓存或刷新,在多个命名空间中共享相同的缓存配置和实例,可以使用 cache-ref 元素来引用另一个缓存 + +```xml + +``` -优点: 先从单表查询,需要时再从关联表去关联查询,提高数据库性能,因为查询单表要比关联查询多张表速度要快 -坏处:只有当需要用到数据时,才会进行数据库查询,这样在大批量数据查询时,查询工作也要消耗时间,所以可能造成用户等待时间变长,造成用户体验下降 @@ -1312,436 +1669,323 @@ PageInfo相关API: -### Assocation -一对多,多对多 -* 核心配置文件 +## 构造语句 - | 标签名 | 描述 | 默认值 | - | --------------------- | ------------------------------------------------------------ | -------------------- | - | lazyLoadingEnabled | 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。特定关联关系中可通过设置 `fetchType` 属性来覆盖该项的开关状态。 | false | - | aggressiveLazyLoading | 开启时,任一方法的调用都会加载该对象的所有延迟加载属性。否则,每个延迟加载属性会按需加载(参考 `lazyLoadTriggerMethods`)。 | false(3.4.1版本以后) | +### 动态SQL - ```xml - - - - - ``` +#### 基本介绍 -* 映射配置文件OneToOneMapper.xml - 一对一映射: - - * column属性表示往要调用的其它的select标签中传入参数 - * select属性表示调用其它的select标签 - * fetchType="lazy"表示延迟加载(局部配置,只有配置了这个的地方才会延迟加载) - - ```xml - - - - - - - - - - - - - ``` - -* PersonMapper.xml - - ```xml - - - - ``` - -* PersonMapper.java - - ```java - public interface PersonMapper { - /** - * 为了演示分步查询的一对多另写的一个方法 - */ - User findPersonByid(int id); - } - ``` - -* 测试文件 - - ```java - public class Test01 { - @Test - public void selectAll() throws Exception{ - InputStream is = Resources.getResourceAsStream("MyBatisConfig.xml"); - SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(is); - SqlSession sqlSession = ssf.openSession(true); - OneToOneMapper mapper = sqlSession.getMapper(OneToOneMapper.class); - //调用实现类的方法,接收结果 - List list = mapper.selectAll(); - //6.不能遍历,遍历就是相当于使用了该数据,需要加载,不遍历就是没有使用。 - - //释放资源 - sqlSession.close(); - is.close(); - } - } - ``` - - +动态 SQL 是 MyBatis 强大特性之一,逻辑复杂时,MyBatis 映射配置文件中,SQL 是动态变化的,所以引入动态 SQL 简化拼装 SQL 的操作 +DynamicSQL 包含的标签: +* if +* where +* set +* choose (when、otherwise) +* trim +* foreach -*** +各个标签都可以进行灵活嵌套和组合 +OGNL:Object Graphic Navigation Language(对象图导航语言),用于对数据进行访问 -### Collection -同样在一对多关系配置的结点中配置延迟加载策略。 结点中也有select属性,column属性。 +参考文章:https://www.cnblogs.com/ysocean/p/7289529.html -* 映射配置文件OneToManyMapper.xml - 一对多映射: - - * column是用于指定使用哪个字段的值作为条件查询 - * select是用于指定查询账户的唯一标识(账户的dao全限定类名加上方法名称) - - ```xml - - - - - - - - - - - - ``` - -* StudentMapper.xml - ```xml - - - - ``` +*** +#### where -*** +:条件标签,有动态条件则使用该标签代替 WHERE 关键字,封装查询条件 +作用:如果标签返回的内容是以 AND 或 OR 开头的,标签内会剔除掉 +表结构: -## 注解开发 +![](https://gitee.com/seazean/images/raw/master/Frame/MyBatis-动态sql用户表.png) -### 单表操作 -注解可以简化开发操作,省略映射配置文件的编写。 -常用注解: +**** -* @Select(“查询的SQL 语句”):执行查询操作注解 -* @Insert(“插入的SQL 语句”):执行新增操作注解 -* @Update(“修改的SQL 语句”):执行修改操作注解 -* @Delete(“删除的SQL 语句”):执行删除操作注解 -参数注解: -* @Param:当SQL语句需要多个(大于1)参数时,用来指定参数的对应规则 +#### if -核心配置文件配置映射关系: +基本格式: ```xml - - - - - - - + + 查询条件拼接 + ``` -基本增删改查: +我们根据实体类的不同取值,使用不同的 SQL 语句来进行查询。比如在 id 如果不为空时可以根据 id 查询,如果username 不同空时还要加入用户名作为条件,这种情况在我们的多条件组合查询中经常会碰到。 -* 创建Mapper接口 +* UserMapper.xml - ```java - package mapper; - public interface StudentMapper { - //查询全部 - @Select("SELECT * FROM student") - public abstract List selectAll(); - - //新增数据 - @Insert("INSERT INTO student VALUES (#{id},#{name},#{age})") - public abstract Integer insert(Student student); - - //修改操作 - @Update("UPDATE student SET name=#{name},age=#{age} WHERE id=#{id}") - public abstract Integer update(Student student); + ```xml + + - //删除操作 - @Delete("DELETE FROM student WHERE id=#{id}") - public abstract Integer delete(Integer id); + + - } + ``` -* 修改MyBatis的核心配置文件 +* MyBatisConfig.xml,引入映射配置文件 ```xml - + + ``` -* bean类 +* DAO层Mapper接口 ```java - public class Student { - private Integer id; - private String name; - private Integer age; + public interface UserMapper { + //多条件查询 + public abstract List selectCondition(Student stu); } ``` -* 测试类 +* 实现类 ```java - @Test - public void selectAll() throws Exception{ - //1.加载核心配置文件 - InputStream is = Resources.getResourceAsStream("MyBatisConfig.xml"); + public class DynamicTest { + @Test + public void selectCondition() throws Exception{ + //1.加载核心配置文件 + InputStream is = Resources.getResourceAsStream("MyBatisConfig.xml"); - //2.获取SqlSession工厂对象 - SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(is); + //2.获取SqlSession工厂对象 + SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(is); - //3.通过工厂对象获取SqlSession对象 - SqlSession sqlSession = ssf.openSession(true); + //3.通过工厂对象获取SqlSession对象 + SqlSession sqlSession = ssf.openSession(true); - //4.获取StudentMapper接口的实现类对象 - StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); + //4.获取StudentMapper接口的实现类对象 + UserMapper mapper = sqlSession.getMapper(UserMapper.class); - //5.调用实现类对象中的方法,接收结果 - List list = mapper.selectAll(); + User user = new User(); + user.setId(2); + user.setUsername("李四"); + //user.setSex(男); AND 后会自动剔除 - //6.处理结果 - for (Student student : list) { - System.out.println(student); + //5.调用实现类的方法,接收结果 + List list = mapper.selectCondition(user); + + //6.处理结果 + for (User user : list) { + System.out.println(user); + } + + //7.释放资源 + sqlSession.close(); + is.close(); } - - //7.释放资源 - sqlSession.close(); - is.close(); } ``` - + *** -### 多表操作 +#### set -#### 相关注解 +:进行更新操作的时候,含有 set 关键词,使用该标签 -实现复杂关系映射之前我们可以在映射文件中通过配置来实现,使用注解开发后,我们可以使用@Results注解,@Result注解,@One注解,@Many注解组合完成复杂关系的配置 +```xml + + + UPDATE user u + + + u.username = #{username}, + + + u.sex = #{sex} + + + WHERE id=#{id} + +``` -| 注解 | 说明 | -| ------------- | ------------------------------------------------------------ | -| @Results | 代替标签,注解中使用单个@Result注解或者@Result集合
使用格式:@Results({ @Result(), @Result() })或@Results({ @Result() }) | -| @Result | 代替标签,@Result中属性介绍:
column:数据库的列名 property:封装类的变量名
one:需要使用@One注解(@Result(one = @One))
Many:需要使用@Many注解(@Result(many= @Many)) | -| @One(一对一) | 代替标签,多表查询的关键,用来指定子查询返回单一对象
select:指定调用Mapper接口中的某个方法
使用格式:@Result(column="", property="", one=@One(select="")) | -| @Many(多对一) | 代替标签,多表查询的关键,用来指定子查询返回对象集合
select:指定调用Mapper接口中的某个方法
使用格式:@Result(column="", property="", many=@Many(select="")) | +* 如果第一个条件 username 为空,那么 sql 语句为:update user u set u.sex=? where id=? +* 如果第一个条件不为空,那么 sql 语句为:update user u set u.username = ? ,u.sex = ? where id=? -*** +**** -#### 一对一 -身份证对人 -* 数据准备 +#### choose - ```sql - CREATE TABLE person( - id INT PRIMARY KEY AUTO_INCREMENT, - name VARCHAR(20), - age INT - ); - INSERT INTO person VALUES (NULL,'张三',23),(NULL,'李四',24),(NULL,'王五',25); - - CREATE TABLE card( - id INT PRIMARY KEY AUTO_INCREMENT, - number VARCHAR(30), - pid INT, - CONSTRAINT cp_fk FOREIGN KEY (pid) REFERENCES person(id) - ); - INSERT INTO card VALUES (NULL,'12345',1),(NULL,'23456',2),(NULL,'34567',3); - ``` +假如不想用到所有的查询条件,只要查询条件有一个满足即可,使用 choose 标签可以解决此类问题,类似于 Java 的 switch 语句 -* bean类 +标签: - ```java - public class Card { - private Integer id; //主键id - private String number; //身份证号 - private Person p; //所属人的对象 - ...... - } - - public class Person { - private Integer id; //主键id - private String name; //人的姓名 - private Integer age; //人的年龄 - } - ``` +```xml + +``` -* PersonMapper接口 +有三个条件,id、username、sex,只能选择一个作为查询条件 - ```java - public interface PersonMapper { - //根据id查询 - @Select("SELECT * FROM person WHERE id=#{id}") - public abstract Person selectById(Integer id); - } - ``` +* 如果 id 不为空,那么查询语句为:select * from user where id=? -* CardMapper接口 +* 如果 id 为空,那么看 username 是否为空 + * 如果不为空,那么语句为:select * from user where username=? + * 如果 username 为空,那么查询语句为 select * from user where sex=? - ```java - public interface CardMapper { - //查询全部 - @Select("SELECT * FROM card") - @Results({ - @Result(column = "id",property = "id"), - @Result(column = "number",property = "number"), - @Result( - property = "p", // 被包含对象的变量名 - javaType = Person.class, // 被包含对象的实际数据类型 - column = "pid", // 根据查询出的card表中的pid字段来查询person表 - /* - one、@One 一对一固定写法 - select属性:指定调用哪个接口中的哪个方法 - */ - one = @One(select = "one_to_one.PersonMapper.selectById") - ) - }) - public abstract List selectAll(); - } - ``` -* 测试类(详细代码参考单表操作) - ```java - //1.加载核心配置文件 - //2.获取SqlSession工厂对象 - //3.通过工厂对象获取SqlSession对象 - - //4.获取StudentMapper接口的实现类对象 - CardMapper mapper = sqlSession.getMapper(CardMapper.class); - //5.调用实现类对象中的方法,接收结果 - List list = mapper.selectAll(); - ``` +*** - -*** +#### trim +trim 标记是一个格式化的标记,可以完成 set 或者是 where 标记的功能,自定义字符串截取 -#### 一对多 +* prefix:给拼串后的整个字符串加一个前缀,trim 标签体中是整个字符串拼串后的结果 +* prefixOverrides:去掉整个字符串前面多余的字符 +* suffix:给拼串后的整个字符串加一个后缀 +* suffixOverrides:去掉整个字符串后面多余的字符 -班级和学生 +改写 if+where 语句: -* 数据准备 +```xml + +``` - ```mysql - CREATE TABLE classes( - id INT PRIMARY KEY AUTO_INCREMENT, - name VARCHAR(20) - ); - CREATE TABLE student( - id INT PRIMARY KEY AUTO_INCREMENT, - name VARCHAR(30), - age INT, - cid INT, - CONSTRAINT cs_fk FOREIGN KEY (cid) REFERENCES classes(id) - ); - ``` +改写 if+set 语句: -* bean类 +```xml + + + UPDATE user u + + + u.username = #{username}, + + + u.sex = #{sex}, + + + WHERE id=#{id} + +``` - ```java - public class Classes { - private Integer id; //主键id - private String name; //班级名称 - private List students; //班级中所有学生对象:id、姓名、学生年龄 - ........ - } - ``` -* StudentMapper接口 - ```java - public interface StudentMapper { - //根据cid查询student表 cid是外键约束列 - @Select("SELECT * FROM student WHERE cid=#{cid}") - public abstract List selectByCid(Integer cid); - } - ``` +**** -* ClassesMapper接口 - ```java - public interface ClassesMapper { - //查询全部 - @Select("SELECT * FROM classes") - @Results({ - @Result(column = "id", property = "id"), - @Result(column = "name", property = "name"), - @Result( - property = "students", //被包含对象的变量名 - javaType = List.class, //被包含对象的实际数据类型 - column = "id", //根据id字段查询student表 - many = @Many(select = "one_to_many.StudentMapper.selectByCid") - ) - }) - public abstract List selectAll(); - } + +#### foreach + +基本格式: + +```xml +:循环遍历标签。适用于多个参数或者的关系。 + + 获取参数 + +``` + +属性: + +* collection:参数容器类型, (list-集合, array-数组) +* open:开始的 SQL 语句 +* close:结束的 SQL 语句 +* item:参数变量名 +* separator:分隔符 + +需求:循环执行 sql 的拼接操作,`SELECT * FROM user WHERE id IN (1,2,5)` + +* UserMapper.xml片段 + + ```xml + ``` -* 测试类 +* 测试代码片段 - ```java + ```java //4.获取StudentMapper接口的实现类对象 - ClassesMapper mapper = sqlSession.getMapper(ClassesMapper.class); - //5.调用实现类对象中的方法,接收结果 - List classes = mapper.selectAll(); + UserMapper mapper = sqlSession.getMapper(UserMapper.class); + + List ids = new ArrayList<>(); + Collections.addAll(list, 1, 2); + //5.调用实现类的方法,接收结果 + List list = mapper.selectByIds(ids); + + for (User user : list) { + System.out.println(user); + } ``` @@ -1750,82 +1994,119 @@ PageInfo相关API: -#### 多对多 +#### SQL片段 -学生和课程 +将一些重复性的 SQL 语句进行抽取,以达到复用的效果 -* 中间表 +格式: - ```mysql - CREATE TABLE stu_cr( - id INT PRIMARY KEY AUTO_INCREMENT, - sid INT, - cid INT, - CONSTRAINT sc_fk1 FOREIGN KEY (sid) REFERENCES student(id), - CONSTRAINT sc_fk2 FOREIGN KEY (cid) REFERENCES course(id) - ); - ``` +```xml +抽取的SQL语句 + +``` + +使用: + +```xml +SELECT * FROM user + + +``` + + + + + +**** -* SQL查询语句 - ```mysql - SELECT DISTINCT s.id,s.name,s.age FROM student s,stu_cr sc WHERE sc.sid=s.id - SELECT c.id,c.name FROM stu_cr sc,course c WHERE sc.cid=c.id AND sc.sid=#{id} - ``` -* bean类 +### 逆向工程 - ```java - public class Student { - private Integer id; //主键id - private String name; //学生姓名 - private Integer age; //学生年龄 - private List courses; //学生选择的课程 id、课程名 - } - ``` +MyBatis 逆向工程,可以针对**单表**自动生成 mybatis 执行所需要的代码(mapper.java、mapper.xml、pojo…) -* CourseMapper接口 +generatorConfig.xml - ```java - public interface CourseMapper { - //根据学生id查询所选课程 - @Select("SELECT c.id,c.name FROM stu_cr sc,course c WHERE sc.cid=c.id AND sc.sid=#{id}") - public abstract List selectBySid(Integer id); - } - ``` +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+``` -* StudentMapper接口 +生成代码: - ```java - public interface StudentMapper { - //查询全部 - @Select("SELECT DISTINCT s.id,s.name,s.age FROM student s,stu_cr sc WHERE sc.sid=s.id") - @Results({ - @Result(column = "id",property = "id"), - @Result(column = "name",property = "name"), - @Result(column = "age",property = "age"), - @Result( - property = "courses", //被包含对象的变量名 - javaType = List.class, //被包含对象的实际数据类型 - column = "id", //根据查询出的student表中的id字段查询中间表和课程表 - many = @Many(select = "many_to_many.CourseMapper.selectBySid") - ) - }) - public abstract List selectAll(); - } - - ``` +```java +public void testGenerator() throws Exception{ + List warnings = new ArrayList(); + boolean overwrite = true; + //指向逆向工程配置文件 + File configFile = new File(GeneratorTest.class. + getResource("/generatorConfig.xml").getFile()); + ConfigurationParser cp = new ConfigurationParser(warnings); + Configuration config = cp.parseConfiguration(configFile); + DefaultShellCallback callback = new DefaultShellCallback(overwrite); + MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, + callback, warnings); + myBatisGenerator.generate(null); -* 测试类 +} +``` - ```java - //4.获取StudentMapper接口的实现类对象 - StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); - //5.调用实现类对象中的方法,接收结果 - List students = mapper.selectAll(); - ``` +参考文章:https://www.cnblogs.com/ysocean/p/7360409.html @@ -1835,9 +2116,9 @@ PageInfo相关API: -## 构建SQL +### 构建SQL -### 基础语法 +#### 基础语法 MyBatis 提供了 org.apache.ibatis.jdbc.SQL 功能类,专门用于构建 SQL 语句 @@ -1866,7 +2147,7 @@ MyBatis 提供了 org.apache.ibatis.jdbc.SQL 功能类,专门用于构建 SQL -### 实现CRUD +#### 基本操作 * MyBatisConfig.xml配置 @@ -2006,6 +2287,345 @@ 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 对象 + +3. defaulSqlSession 通过 getMapper() 获取 mapper 接口的代理对象 mapperProxy + +4. 执行增删改查: + + * 通过 defaulSqlSession 中的属性 Executor 创建 statementHandler 对象 + * 创建 statementHandler 对象的同时也创建 parameterHandler 和 resultSetHandler + * 通过 parameterHandler 设置预编译参数及参数值 + + * 调用 statementHandler 执行增删改查 + + * 通过 resultsetHandler 封装查询结果 + +四大对象: + +- StatementHandler:处理 sql 语句预编译,设置参数等相关工作 +- ParameterHandler:设置预编译参数用的 +- ResultHandler:处理结果集 +- Executor:执行器,真正进行 java 与数据库交互的对象 + + + +参考视频:https://www.bilibili.com/video/BV1mW411M737?p=71 + + + +**** + + + +### 获取工厂 + +SqlSessionFactoryBuilder.build(InputStream, String, Properties):构建工厂 + +XMLConfigBuilder.parse():解析核心配置文件每个标签的信息 + +* `parseConfiguration(parser.evalNode("/configuration"))`:读取节点内数据,是 MyBatis 配置文件中的顶层标签 +* `mapperElement(root.evalNode("mappers"))`:解析 mappers 信息,分为 package 和 单个注册两种 +* `Configuration.addMappers()`:将 mapper 接口添加到 mapperRegistry 中,用来获取代理对象 + +* `XMLMapperBuilder.parse()`:解析 mapper 的标签的信息 + * `configurationElement(parser.evalNode("/mapper"))`:解析 mapper 文件,顶层节点 + * `buildStatementFromContext(context.evalNodes("select..."))`:解析操作标签 + * `XMLStatementBuilder.parseStatementNode()`:解析操作标签的所有的属性 + * `builderAssistant.addMappedStatement(...)`:封装成 MappedStatement 对象 + +return new DefaultSqlSessionFactory(config):返回工厂对象 + +![](https://gitee.com/seazean/images/raw/master/Frame/MyBatis-获取工厂对象.png) + +总结:解析 xml 是对 Configuration 中的属性进行填充,那么我们同样可以在一个类中创建 Configuration 对象,手动设置其中属性的值来达到配置的效果 + + + +*** + + + +### 获取会话 + +DefaultSqlSessionFactory.openSession():获取 Session 对象,并且创建 Executor 对象 + +DefaultSqlSessionFactory.openSessionFromDataSource(...):ExecutorType 为 Executor 的类型,TransactionIsolationLevel 为事务隔离级别,autoCommit 是否开启事务 + +* `transactionFactory.newTransaction(DataSource, IsolationLevel, boolean`:事务对象 + +* `configuration.newExecutor(tx, execType)`:根据参数创建指定类型的 Executor + * 批量操作笔记的部分有讲解到 的属性 defaultExecutorType,根据设置的创建对象 + * 如果开启了二级缓存,会包装 Executor 对象 `BaseExecutor.setExecutorWrapper(executor)` + + return new DefaultSqlSession(configuration, executor, autoCommit):返回 DefaultSqlSession 对象 + +![](https://gitee.com/seazean/images/raw/master/Frame/MyBatis-获取会话对象.png) + + + +**** + + + +### 获取代理 + +Configuration.getMapper(Class, SqlSession):获取代理的 mapper 对象 + +MapperRegistry.getMapper(Class, SqlSession):MapperRegistry 是 Configuration 属性,在获取工厂对象时初始化 + +* `(MapperProxyFactory) knownMappers.get(type)`:获取接口信息封装为 MapperProxyFactory 对象 +* `mapperProxyFactory.newInstance(sqlSession)`:创建代理对象 + * `new MapperProxy<>(sqlSession, mapperInterface, methodCache)`:包装对象 + * methodCache 是并发安全的 ConcurrentHashMap 集合,存放要执行的方法 + * `MapperProxy implements InvocationHandler` 是一个 InvocationHandler 对象 + * `Proxy.newProxyInstance()`:**JDK 动态代理**创建 MapperProxy 对象 + +![](https://gitee.com/seazean/images/raw/master/Frame/MyBatis-获取代理对象.png) + + + +**** + + + +### 执行SQL + +MapperProxy.invoke():执行 SQL 语句 + +cachedMapperMethod(method):包装成一个 MapperMethod 对象并初始化该对象 + +MapperMethod.execute():根据 switch-case 判断使用的什么类型的 SQL 进行逻辑处理 + +sqlSession.selectOne(String, Object):查询数据,底层调用 DefaultSqlSession.selectList(String, Object) + +configuration.getMappedStatement(statement):获取执行者对象 + +executor.query():开始执行查询语句,参数要被包装成集合类 + +* `CachingExecutor.query()`:先执行 + + * `MappedStatement.getBoundSql(parameterObject)`:把 **parameterObject** 封装成 BoundSql 对象 + 构造函数中有:`this.parameterObject = parameterObject` + + ![](https://gitee.com/seazean/images/raw/master/Frame/MyBatis-boundSql对象.png) + + * `CachingExecutor.createCacheKey()`:创建缓存对象 + + * `ms.getCache()`:获取二级缓存,`tcm.getObject(cache, key)`:尝试从**二级缓存**中获取数据 + +* `BaseExecutor.query()`: + + * `localCache.getObject(key) `:尝试从**本地缓存**(一级缓存)获取数据 + +* `BaseExecutor.queryFromDatabase()`:开始从数据库获取数据,并放入本地缓存 + + * `SimpleExecutor.doQuery()`:执行 query + * `configuration.newStatementHandler()`:创建 StatementHandler 对象 + * 根据 select 标签的 statementType 属性,根据属性选择创建哪种对象 + * 判断 BoundSql 是否被创建,没有创建会重新封装参数信息到 BoundSql + * 创建 StatementHandler 的构造方法中,创建了 ParameterHandler 和 ResultSetHandler 对象 + * `interceptorChain.pluginAll(statementHandler)`:拦截器链 + * `prepareStatement(StatementHandler, Log)`:**创建 JDBC 的 Statement 对象**,底层通过 JDBC 创建 + * `getConnection()`:获取 JDBC 的 Connection 对象 + * `handler.prepare()`:初始化 Statement 对象 + * `instantiateStatement(Connection connection)`:**Connection** 中的方法实例化对象 + * 获取普通执行者对象:`Connection.createStatement()` + * 获取预编译执行者对象:`Connection.prepareStatement()` + * `handler.parameterize()`:进行参数的设置 + * `ParameterHandler.setParameters()`:**通过 ParameterHandler 设置参数** + * `typeHandler.setParameter()`:**通过 TypeHandler 预编译 SQL** + * `StatementHandler.query()`:**封装成 PreparedStatement 执行 SQL** + + * `resultSetHandler.handleResultSets(ps)`:**通过 ResultSetHandler 对象封装结果集** + +`return list.get(0)`:返回结果集的第一个数据 + +![](https://gitee.com/seazean/images/raw/master/Frame/MyBatis-执行SQL过程.png) + + + + + + + +**** + + + + + +## 插件使用 + +### 插件原理 + +实现原理:插件是按照插件配置顺序创建层层包装对象,执行目标方法的之后,按照逆向顺序执行(栈) + + + +在四大对象创建时: + + * 每个创建出来的对象不是直接返回的,而是 `interceptorChain.pluginAll(parameterHandler)` + * 获取到所有 Interceptor(插件需要实现的接口),调用 `interceptor.plugin(target)`返回 target 包装后的对象 + * 插件机制可以使用插件为目标对象创建一个代理对象(AOP),代理对象可以拦截到四大对象的每一个执行 + +```java +@Intercepts( + { + @Signature(type=StatementHandler.class,method="parameterize",args=java.sql.Statement.class) + }) +public class MyFirstPlugin implements Interceptor{ + + //intercept:拦截目标对象的目标方法的执行; + @Override + public Object intercept(Invocation invocation) throws Throwable { + System.out.println("MyFirstPlugin...intercept:"+invocation.getMethod()); + //动态的改变一下sql运行的参数:以前1号员工,实际从数据库查询11号员工 + Object target = invocation.getTarget(); + System.out.println("当前拦截到的对象:"+target); + //拿到:StatementHandler==>ParameterHandler===>parameterObject + //拿到target的元数据 + MetaObject metaObject = SystemMetaObject.forObject(target); + Object value = metaObject.getValue("parameterHandler.parameterObject"); + System.out.println("sql语句用的参数是:"+value); + //修改完sql语句要用的参数 + metaObject.setValue("parameterHandler.parameterObject", 11); + //执行目标方法 + Object proceed = invocation.proceed(); + //返回执行后的返回值 + return proceed; + } + + // plugin:包装目标对象的,为目标对象创建一个代理对象 + @Override + public Object plugin(Object target) { + //可以借助 Plugin 的 wrap 方法来使用当前 Interceptor 包装我们目标对象 + System.out.println("MyFirstPlugin...plugin:mybatis将要包装的对象" + target); + Object wrap = Plugin.wrap(target, this); + //返回为当前target创建的动态代理 + return wrap; + } + + // setProperties:将插件注册时的property属性设置进来 + @Override + public void setProperties(Properties properties) { + System.out.println("插件配置的信息:" + properties); + } +} +``` + +核心配置文件: + +```xml + + + + + + + +``` + + + + + +**** + + + +### 分页插件 + +![](https://gitee.com/seazean/images/raw/master/Frame/分页介绍.png) + +* 分页可以将很多条结果进行分页显示。如果当前在第一页,则没有上一页。如果当前在最后一页,则没有下一页,需要明确当前是第几页,这一页中显示多少条结果。 +* MyBatis 是不带分页功能的,如果想实现分页功能,需要手动编写 LIMIT 语句,不同的数据库实现分页的 SQL 语句也是不同,手写分页 成本较高。 +* PageHelper:第三方分页助手,将复杂的分页操作进行封装,从而让分页功能变得非常简单 + + + +*** + + + +### 分页操作 + +开发步骤: + +1. 导入 PageHelper 的 Maven 坐标 + +2. 在 mybatis 核心配置文件中配置 PageHelper 插件 + + 注意:分页助手的插件配置在通用 mapper 之前 + + ```xml + + + + + + + ......... + ``` + +3. 与 MySQL 分页查询页数计算公式不同 + static Page startPage(int pageNum, int pageSize) : pageNum第几页,pageSize页面大小 + + ```java + @Test + public void selectAll() { + //第一页:显示2条数据 + PageHelper.startPage(1,2); + List students = sqlSession.selectList("StudentMapper.selectAll"); + for (Student student : students) { + System.out.println(student); + } + } + ``` + + + +**** + + + +### 参数获取 + +PageInfo构造方法: + +* `PageInfo info = new PageInfo<>(list)` : list是SQL执行返回的结果集合,参考上一节 + +PageInfo相关API: + +1. startPage():设置分页参数 +2. PageInfo:分页相关参数功能类。 +3. getTotal():获取总条数 +4. getPages():获取总页数 +5. getPageNum():获取当前页 +6. getPageSize():获取每页显示条数 +7. getPrePage():获取上一页 +8. getNextPage():获取下一页 +9. isIsFirstPage():获取是否是第一页 +10. isIsLastPage():获取是否是最后一页 + + + @@ -2103,7 +2723,7 @@ Spring优点: 步骤: -1. 导入spring坐标(5.1.9.release)—pom.xml文件 +1. 导入 spring 坐标(5.1.9.release) ```xml @@ -5615,7 +6235,7 @@ JDKProxy动态代理是针对对象做代理,要求原始对象具有接口实 静态代理和动态代理的区别: * 静态代理是在编译时就已经将接口、代理类、被代理类的字节码文件确定下来 -* 动态代理是程序在运行后通过反射创建字节码文件交由JVM加载 +* 动态代理是程序在运行后通过反射创建字节码文件交由 JVM 加载 ```java public class UserServiceJDKProxy { @@ -5648,13 +6268,13 @@ public class UserServiceJDKProxy { #### CGLIB -CGLIB(Code Generation Library):Code生成类库 +CGLIB(Code Generation Library):Code生成类库 -CGLIB特点: +CGLIB 特点: -* CGLIB动态代理**不限定**是否具有接口,可以对任意操作进行增强 -* CGLIB动态代理无需要原始被代理对象,动态创建出新的代理对象 -* CGLIB**继承被代理类**,如果代理类是final则不能实现 +* CGLIB 动态代理**不限定**是否具有接口,可以对任意操作进行增强 +* CGLIB 动态代理无需要原始被代理对象,动态创建出新的代理对象 +* CGLIB **继承被代理类**,如果代理类是final则不能实现 ![](https://gitee.com/seazean/images/raw/master/Frame/AOP底层原理-cglib.png) @@ -10014,17 +10634,17 @@ SSM(Spring+SpringMVC+MyBatis) * MyBatis:mysql+druid+pagehelper -* Spring整合MyBatis +* Spring 整合 MyBatis -* junit测试业务层接口 +* junit 测试业务层接口 * SpringMVC - * rest风格(postman测试请求结果) - * 数据封装json(jackson) + * rest 风格(postman 测试请求结果) + * 数据封装 json(jackson) -* Spring整合SpringMVC +* Spring 整合 SpringMVC - * Controller调用Service + * Controller 调用 Service * 其他 @@ -10044,7 +10664,7 @@ SSM(Spring+SpringMVC+MyBatis) * 数据层接口(代理自动创建实现类) - * 业务层接口+业务层实现类 + * 业务层接口 + 业务层实现类 * 表现层类 ![](https://gitee.com/seazean/images/raw/master/Frame/SSM目录结构.png) @@ -10301,9 +10921,10 @@ SSM(Spring+SpringMVC+MyBatis) ```java public class UserController { + } ``` - + *** @@ -10472,7 +11093,7 @@ SSM(Spring+SpringMVC+MyBatis) ### Junit -* 单元测试整合junit +* 单元测试整合 junit ```java @RunWith(SpringJUnit4ClassRunner.class) @@ -10692,7 +11313,7 @@ SSM(Spring+SpringMVC+MyBatis) 自定义异常消息返回时需要与业务正常执行的消息按照统一的格式进行处理 -* 定义BusinessException +* 定义 BusinessException ```java public class BusinessException extends RuntimeException { @@ -10966,11 +11587,11 @@ public class ProjectExceptionAdivce { * EnableWebMvc - 1. 支持ConversionService的配置,可以方便配置自定义类型转换器 - 2. 支持@NumberFormat注解格式化数字类型 - 3. 支持@DateTimeFormat注解格式化日期数据,日期包括Date,Calendar,JodaTime(JodaTime要导包) - 4. 支持@Valid的参数校验(需要导入JSR-303规范) - 5. 配合第三方jar包和SpringMVC提供的注解读写XML和JSON格式数据 + 1. 支持 ConversionService 的配置,可以方便配置自定义类型转换器 + 2. 支持 @NumberFormat 注解格式化数字类型 + 3. 支持 @DateTimeFormat 注解格式化日期数据,日期包括 Date、Calendar + 4. 支持 @Valid 的参数校验(需要导入 JSR-303 规范) + 5. 配合第三方 jar 包和 SpringMVC 提供的注解读写 XML 和 JSON 格式数据 diff --git a/Tool.md b/Tool.md index dc5fc2d..8d16914 100644 --- a/Tool.md +++ b/Tool.md @@ -573,24 +573,24 @@ Linux 文件系统目录结构和熟知的 windows 系统有较大区别,没 #### NAT -首先设置虚拟机中NAT模式的选项,打开VMware,点击“编辑”下的“虚拟网络编辑器”,设置NAT参数 +首先设置虚拟机中 NAT 模式的选项,打开 VMware,点击“编辑”下的“虚拟网络编辑器”,设置 NAT 参数 ![](https://gitee.com/seazean/images/raw/master/Tool/配置NAT.jpg) -**注意**:VMware Network Adapter VMnet8保证是启用状态 +**注意**:VMware Network Adapter VMnet8 保证是启用状态 ​ ![](https://gitee.com/seazean/images/raw/master/Tool/本地主机网络连接.jpg) -#### 静态IP -在普通用户下不能修改网卡的配置信息;所以我们要切换到root用户进行ip配置:su root/su -* 修改网卡配置文件: - vi /etc/sysconfig/network-scripts/ifcfg-ens33 - 或者命令前加sudo +#### 静态IP + +在普通用户下不能修改网卡的配置信息;所以我们要切换到 root 用户进行 ip 配置:su root/su +* 修改网卡配置文件:`vim /etc/sysconfig/network-scripts/ifcfg-ens33` + * 修改文件内容 - ``` + ```sh TYPE=Ethernet PROXY_METHOD=none BROWSER_ONLY=no @@ -609,7 +609,7 @@ Linux 文件系统目录结构和熟知的 windows 系统有较大区别,没 UUID=2c2371f1-ef29-4514-a568-c4904bd11c82 DEVICE=ens33 ONBOOT=true - ************************************* + ########################### BOOTPROTO设置为静态static IPADDR设置ip地址 NETMASK设置子网掩码 @@ -625,8 +625,8 @@ Linux 文件系统目录结构和熟知的 windows 系统有较大区别,没 * 重启网络:systemctl restart network * 查看IP:ifconfig -* 宿主机ping虚拟机,虚拟机ping宿主机 -* 在虚拟机中访问网络,需要增加一块NAT网卡 +* 宿主机 ping 虚拟机,虚拟机 ping 宿主机 +* 在虚拟机中访问网络,需要增加一块 NAT 网卡 * 【虚拟机】--【设置】--【添加】 * From 9ea2ec47d57bcb433f2626baf88971279ebf9f83 Mon Sep 17 00:00:00 2001 From: Seazean Date: Tue, 13 Jul 2021 12:45:45 +0800 Subject: [PATCH 070/242] Update Java Notes --- Java.md | 488 ++++++++++++++++++++++++++++++++------------------------ Prog.md | 92 ++++++----- SSM.md | 28 ++-- Tool.md | 31 +++- 4 files changed, 369 insertions(+), 270 deletions(-) diff --git a/Java.md b/Java.md index 2f86438..66048c7 100644 --- a/Java.md +++ b/Java.md @@ -1082,7 +1082,7 @@ public class ClassDemo { 1. **成员变量应该私有,用private修饰,只能在本类中直接访问** 2. **提供成套的getter和setter方法暴露成员变量的取值和赋值** -为什么使用private修饰成员变量:实现数据封装,不想让别人使用修改你的数据,比较安全 +使用 private 修饰成员变量的原因:实现数据封装,不想让别人使用修改你的数据,比较安全 @@ -1092,12 +1092,12 @@ public class ClassDemo { ### this -this关键字的作用: +this 关键字的作用: -* this关键字代表了当前对象的引用 -* this出现在方法中:**哪个对象调用这个方法this就代表谁** -* this可以出现在构造器中:代表构造器正在初始化的那个对象 -* this可以区分变量是访问的成员变量还是局部变量 +* this 关键字代表了当前对象的引用 +* this 出现在方法中:**哪个对象调用这个方法this就代表谁** +* this 可以出现在构造器中:代表构造器正在初始化的那个对象 +* this 可以区分变量是访问的成员变量还是局部变量 @@ -2439,6 +2439,35 @@ s = s + "cd"; //s = abccd 新对象 +#### 常用方法 + +`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 beginIndex, int endIndex)` : 返回原字符串指定索引处的字符串 +`public String toLowerCase()` : 将此String所有字符转换为小写,使用默认语言环境的规则 +`public String toUpperCase()` : 使用默认语言环境的规则将此String所有字符转换为大写 +`public String replace(CharSequence target, CharSequence replacement)` : 使用新值,将字符串中的旧值替换,得到新的字符串 + +```java +String s = 123-78; +s.replace("-","");//12378 +``` + + + +*** + + + #### 构造方式 构造方法: @@ -2484,36 +2513,6 @@ s = s + "cd"; //s = abccd 新对象 - -#### 常用方法 - -`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 beginIndex, int endIndex)` : 返回原字符串指定索引处的字符串 -`public String toLowerCase()` : 将此String所有字符转换为小写,使用默认语言环境的规则 -`public String toUpperCase()` : 使用默认语言环境的规则将此String所有字符转换为大写 -`public String replace(CharSequence target, CharSequence replacement)` : 使用新值,将字符串中的旧值替换,得到新的字符串 - -```java -String s = 123-78; -s.replace("-","");//12378 -``` - - - -*** - - - #### String Pool ##### 基本介绍 @@ -2521,18 +2520,24 @@ s.replace("-","");//12378 **字符串常量池(String Pool / StringTable / 串池)**保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定,常量池类似于Java系统级别提供的**缓存**,存放对象和引用 * StringTable,hashtable (哈希表 + 链表)结构,不能扩容,默认值大小长度是1009 - * 常量池中的字符串仅是符号,第一次使用时才变为对象,可以避免重复创建字符串对象 * 字符串**变量**的拼接的原理是StringBuilder(jdk1.8),append 效率要比字符串拼接高很多 * 字符串**常量**拼接的原理是编译期优化,结果在常量池 * 可以使用 String 的 intern() 方法在运行过程将字符串添加到 String Pool 中 - **intern()** : -* jdk1.8:当一个字符串调用 intern() 方法时,如果 String Pool 中: - * 存在一个字符串和该字符串值相等,就会返回 String Pool 中字符串的引用(需要变量接收) - * 不存在,会把对象的**引用地址**复制一份放入串池,并返回串池中的引用地址,前提是堆内存有该对象,因为 Pool 在堆中,为了节省内存不再创建新对象 -* jdk1.6:将这个字符串对象尝试放入串池,如果有就不放入,返回已有的串池中的对象的地址;如果没有会把此对象复制一份,放入串池,把串池中的对象返回 + +*** + + + +##### intern() + +jdk1.8:当一个字符串调用 intern() 方法时,如果 String Pool 中: +* 存在一个字符串和该字符串值相等,就会返回 String Pool 中字符串的引用(需要变量接收) +* 不存在,会把对象的**引用地址**复制一份放入串池,并返回串池中的引用地址,前提是堆内存有该对象,因为 Pool 在堆中,为了节省内存不再创建新对象 + +jdk1.6:将这个字符串对象尝试放入串池,如果有就不放入,返回已有的串池中的对象的地址;如果没有会把此对象复制一份,放入串池,把串池中的对象返回 ```java public class Demo { @@ -2564,15 +2569,25 @@ public class Demo { - == 比较基本数据类型:比较的是具体的值 - == 比较引用数据类型:比较的是对象地址值 -面试问题: +结论: ```java String s1 = "ab"; //串池 String s2 = new String("a") + new String("b"); //堆 -//上面两条指令的结果和下面的效果相同 +//上面两条指令的结果和下面的 效果 相同 String s = new String("ab"); ``` + + +**** + + + +##### 面试问题 + +问题一: + ```java public static void main(String[] args) { String s = new String("a") + new String("b");//new String("ab") @@ -2587,6 +2602,43 @@ 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 + + String str2 = new StringBuilder("ja").append("va").toString(); + System.out.println(str2 == str2.intern());//false +} +``` + +原因: + +* System 类当调用 Version 的静态方法,导致 Version 初始化: + + ```java + private static void initializeSystemClass() { + sun.misc.Version.init(); + } + ``` + +* Version类初始化时需要对静态常量字段初始化,被 launcher_name 静态常量字段所引用的"java"字符串字面量就被放入的字符串常量池: + + ```java + package sun.misc; + + public class Version { + private static final String launcher_name = "java"; + private static final String java_version = "1.8.0_221"; + private static final String java_runtime_name = "Java(TM) SE Runtime Environment"; + private static final String java_profile_name = ""; + private static final String java_runtime_version = "1.8.0_221-b11"; + //... + } + ``` + *** @@ -4171,13 +4223,17 @@ LinkedList 是一个实现了 List 接口的**双端链表**,支持高效的 ##### 概述 -Set系列集合:添加的元素是无序,不重复,无索引的。 +Set系列集合:添加的元素是无序,不重复,无索引的 -* HashSet:添加的元素是无序,不重复,无索引的。 -* LinkedHashSet:添加的元素是有序,不重复,无索引的。 -* TreeSet:不重复,无索引,按照大小默认升序排序!! +* HashSet:添加的元素是无序,不重复,无索引的 +* LinkedHashSet:添加的元素是有序,不重复,无索引的 +* TreeSet:不重复,无索引,按照大小默认升序排序 -**面试问题**:没有索引,不能使用普通for循环遍历 +**面试问题**:没有索引,不能使用普通 for 循环遍历 + + + +*** @@ -4262,13 +4318,13 @@ TreeSet 集合自排序的方式: 自定义的引用数据类型,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被比较者` @@ -4371,7 +4427,7 @@ public class Student{ >Collection是单值集合体系。 >Map集合是一种双列集合,每个元素包含两个值。 -Map集合的每个元素的格式:key=value(键值对元素)。Map集合也被称为“键值对集合” +Map集合的每个元素的格式:key=value(键值对元素),Map集合也被称为“键值对集合” Map集合的完整格式:`{key1=value1 , key2=value2 , key3=value3 , ...}` @@ -4412,14 +4468,14 @@ System.out.println(maps); #### 常用API Map集合的常用API - `public V put(K key, V value)` : 把指定的键与值添加到Map集合中,**重复的键会覆盖前面的值元素** - `public V remove(Object key)` : 把指定的键对应的键值对元素在集合中删除,返回被删除元素的值 - `public V get(Object key)` : 根据指定的键,在Map集合中获取对应的值。 - `public Set keySet()` : 获取Map集合中所有的键,存储到**Set集合**中。 - `public Collection values()` : 获取全部值的集合,存储到**Collection集合** - `public Set> entrySet()` : 获取Map集合中所有的键值对对象的集合(Set集合) - `public boolean containKey(Object key)` : 判断该集合中是否有此键。 - `public boolean containsKey(Object key)` : 判断集合是否为空。 + +* `public V put(K key, V value)`:把指定的键与值添加到 Map 集合中,**重复的键会覆盖前面的值元素** +* `public V remove(Object key)`:把指定的键对应的键值对元素在集合中删除,返回被删除元素的值 +* `public V get(Object key)`:根据指定的键,在 Map 集合中获取对应的值 +* `public Set keySet()`:获取 Map 集合中所有的键,存储到 **Set 集合**中 +* `public Collection values()`:获取全部值的集合,存储到 **Collection 集合** +* `public Set> entrySet()`:获取Map集合中所有的键值对对象的集合 +* `public boolean containsKey(Object key)`:判断该集合中是否有此键 ```java public class MapDemo { @@ -4445,11 +4501,12 @@ public class MapDemo { #### 遍历方式 Map集合的遍历方式有:3种。 - (1)“键找值”的方式遍历:先获取Map集合全部的键,再根据遍历键找值。 - (2)“键值对”的方式遍历:难度较大,采用增强for或者迭代器 - (3)JDK 1.8开始之后的新技术:foreach,采用Lambda表达式 -集合可以直接输出内容,因为底层重写了toString()方法。 +1. “键找值”的方式遍历:先获取 Map 集合全部的键,再根据遍历键找值。 +2. “键值对”的方式遍历:难度较大,采用增强 for 或者迭代器 +3. JDK 1.8 开始之后的新技术:foreach,采用 Lambda表 达式 + +集合可以直接输出内容,因为底层重写了 toString() 方法 ```java public static void main(String[] args){ @@ -4491,22 +4548,22 @@ public static void main(String[] args){ ##### 基本介绍 -HashMap基于哈希表的Map接口实现,是以key-value存储形式存在,主要用来存放键值对 +HashMap 基于哈希表的 Map 接口实现,是以 key-value 存储形式存在,主要用来存放键值对 特点: * HashMap的实现不是同步的,这意味着它不是线程安全的 -* key是唯一不重复的,底层的哈希表结构,依赖hashCode方法和equals方法保证键的唯一 -* key、value都可以为null,但是key位置只能是一个null +* key是唯一不重复的,底层的哈希表结构,依赖 hashCode 方法和 equals 方法保证键的唯一 +* key、value 都可以为null,但是 key 位置只能是一个null * HashMap中的映射不是有序的,即存取是无序的 * **key要存储的是自定义对象,需要重写hashCode和equals方法,防止出现地址不同内容相同的key** -JDK7对比JDK8: +JDK7 对比 JDK8: * 7 = 数组 + 链表,8 = 数组 + 链表 + 红黑树 -* 7中是头插法,多线程容易造成环,8中是尾插法 -* 7的扩容是全部数据重新定位,8中是位置不变或者当前位置 + 旧size大小来实现 -* 7是先判断是否要扩容再插入,8中是先插入再看是否要扩容 +* 7 中是头插法,多线程容易造成环,8 中是尾插法 +* 7 的扩容是全部数据重新定位,8 中是位置不变或者当前位置 + 旧 size 大小来实现 +* 7 是先判断是否要扩容再插入,8 中是先插入再看是否要扩容 底层数据结构: @@ -4747,9 +4804,8 @@ HashMap继承关系如下图所示: 有些人会觉得这里是一个bug应该这样书写: `this.threshold = tableSizeFor(initialCapacity) * this.loadFactor;` - 这样才符合threshold的概念(当HashMap的size到达threshold这个阈值时会扩容)。 - 但是在jdk8以后的构造方法中,并没有对table这个成员变量进行初始化,table的初始化被推迟到了put方法中,在put方法中会对threshold重新计算 - + 这样才符合 threshold 的概念,但是在 jdk8 以后的构造方法中,并没有对 table 这个成员变量进行初始化,table 的初始化被推迟到了 put 方法中,在 put 方法中会对 threshold 重新计算 + 4. 包含另一个`Map`的构造函数 ```java @@ -4890,7 +4946,7 @@ HashMap继承关系如下图所示: 4. tableSizeFor - 创建HashMap指定容量时,HashMap通过位移运算和或运算得到比指定初始化容量大的最小的2的n次幂 + 创建 HashMap 指定容量时,HashMap 通过位移运算和或运算得到比指定初始化容量大的最小的2的 n 次幂 ```java static final int tableSizeFor(int cap) {//int cap = 10 @@ -4906,12 +4962,12 @@ 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次幂 + 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: + 例如初始化的值为 10: * 第一次右移 @@ -4930,21 +4986,21 @@ HashMap继承关系如下图所示: ```java n |= n >>> 2;//n通过第一次右移变为了:n=13 00000000 00000000 00000000 00001101 // 13 - 00000000 00000000 00000000 00000011 //13右移之后变为3 + 00000000 00000000 00000000 00000011 // 13右移之后变为3 ------------------------------------------------- 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 + 注意:容量最大是 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 + * 得到的 capacity 被赋值给了 threshold ```java this.threshold = tableSizeFor(initialCapacity);//initialCapacity=10 ``` - * JDK11 + * JDK 11 ```java static final int tableSizeFor(int cap) { @@ -4974,24 +5030,36 @@ HashMap继承关系如下图所示: 当 HashMap 中的元素个数超过`(数组长度)*loadFactor(负载因子)`或者链表过长时,就会进行数组扩容,创建新的数组,伴随一次重新 hash 分配,并且遍历 hash 表中所有的元素非常耗时,所以要尽量避免 resize - 扩容机制为扩容为原来容量的2倍: + 扩容机制为扩容为原来容量的 2 倍: ```java - else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && - oldCap >= DEFAULT_INITIAL_CAPACITY) - newThr = oldThr << 1; // double threshold + 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的幂** - + + 注意:这里也要求**数组长度 2 的幂** + ![](https://gitee.com/seazean/images/raw/master/Java/HashMap-resize扩容.png) - + 普通节点: - + ```java //oldCap旧数组大小 if ((e.hash & oldCap) == 0) { @@ -5472,11 +5540,14 @@ public class MapDemo{ #### 概述 泛型(Generic): - 泛型就是一个标签:<数据类型> - 泛型可以在编译阶段约束只能操作某种数据类型。 -注意: JDK 1.7开始之后,泛型后面的申明可以省略不写!! - **泛型和集合都只能支持引用数据类型,不支持基本数据类型。** +* 泛型就是一个标签:<数据类型> +* 泛型可以在编译阶段约束只能操作某种数据类型。 + +注意: + +* JDK 1.7 开始之后,泛型后面的申明可以省略不写 +* **泛型和集合都只能支持引用数据类型,不支持基本数据类型。** ```java { @@ -5491,7 +5562,7 @@ public class MapDemo{ ``` 优点:泛型在编译阶段约束了操作的数据类型,从而不会出现类型转换异常 - 体现的是Java的严谨性和规范性,数据类型,经常需要进行统一! + 体现的是 Java 的严谨性和规范性,数据类型,经常需要进行统一 @@ -5501,7 +5572,7 @@ public class MapDemo{ #### 自定义 -##### 自定义泛型类 +##### 泛型类 泛型类:使用了泛型定义的类就是泛型类。 @@ -5530,9 +5601,14 @@ class MyArrayList{ -##### 自定义泛型方法 +**** + + + +##### 泛型方法 + +泛型方法:定义了泛型的方法就是泛型方法 -泛型方法:定义了泛型的方法就是泛型方法。 泛型方法的定义格式: ```java @@ -5542,6 +5618,7 @@ class MyArrayList{ ``` 方法定义了是什么泛型变量,后面就只能用什么泛型变量。 + 泛型类的核心思想:把出现泛型变量的地方全部替换成传输的真实数据类型 ```java @@ -5598,15 +5675,17 @@ class StudentData implements Data{重写所有方法} -#### 泛型通配符 +#### 通配符 + +通配符:? -* 通配符:? - ?可以用在使用泛型的时候代表一切类型。 - E , T , K , V是在定义泛型的时候使用代表一切类型。 +* ? 可以用在使用泛型的时候代表一切类型 +* E、T、K、V 是在定义泛型的时候使用代表一切类型 -* 泛型的上下限: - ? extends Car : 那么?必须是Car或者其子类。(泛型的上限) - ? super Car :那么?必须是Car或者其父类。(泛型的下限。不是很常见) +泛型的上下限: + +* ? extends Car:那么 ? 必须是 Car 或者其子类(泛型的上限) +* ? super Car:那么 ? 必须是 Car 或者其父类(泛型的下限,不是很常见) ```java //需求:开发一个极品飞车的游戏,所有的汽车都能一起参与比赛。 @@ -5638,11 +5717,12 @@ class Dog{} ### 不可变 -+ 在List、Set、Map接口中都存在of方法,可以创建一个不可变的集合 - + 这个集合不能添加,不能删除,不能修改 - + 但是可以结合集合的带参构造,实现集合的批量添加 -+ 在Map接口中,还有一个ofEntries方法可以提高代码的阅读性 - + 首先会把键值对封装成一个Entry对象,再把这个Entry对象添加到集合当中 +在 List、Set、Map 接口中都存在 of 方法,可以创建一个不可变的集合 ++ 这个集合不能添加,不能删除,不能修改 ++ 但是可以结合集合的带参构造,实现集合的批量添加 + +在Map接口中,还有一个ofEntries方法可以提高代码的阅读性 ++ 首先会把键值对封装成一个Entry对象,再把这个Entry对象添加到集合当中 ````java public class MyVariableParameter4 { @@ -5699,7 +5779,7 @@ public class MyVariableParameter4 { ## 异常 -### 概述 +### 基本介绍 异常:程序在"编译"或者"执行"的过程中可能出现的问题,Java为常见的代码异常都设计一个类来代表。 @@ -5717,12 +5797,10 @@ Java中异常继承的根类是:Throwable ``` -Exception异常的分类: +Exception 异常的分类: -* 编译时异常:继承自Exception的异常或者其子类,编译阶段就会报错, - 必须程序员处理的。否则代码编译就不能通过!! -* 运行时异常: 继承自RuntimeException的异常或者其子类,编译阶段是不会出错的,它是在 - 运行时阶段可能出现,编译阶段是不会出错的,但是运行阶段可能出现,建议提前处理!! +* 编译时异常:继承自Exception的异常或者其子类,编译阶段就会报错 +* 运行时异常: 继承自RuntimeException的异常或者其子类,编译阶段是不会出错的,在运行时阶段可能出现,编译阶段是不会出错的,但是运行阶段可能出现,建议提前处理 @@ -5732,13 +5810,13 @@ Exception异常的分类: ### 处理过程 -异常的产生默认的处理过程解析。(自动处理的过程!) +异常的产生默认的处理过程解析:(自动处理的过程) -(1)默认会在出现异常的代码那里自动的创建一个异常对象:ArithmeticException(算术异常) -(2)异常会从方法中出现的点这里抛出给调用者,调用者最终抛出给JVM虚拟机。 -(3)虚拟机接收到异常对象后,先在控制台直接输出**异常栈**信息数据。 -(4)直接从当前执行的异常点干掉当前程序。 -(5)后续代码没有机会执行了,因为程序已经死亡。 +1. 默认会在出现异常的代码那里自动的创建一个异常对象:ArithmeticException(算术异常) +2. 异常会从方法中出现的点这里抛出给调用者,调用者最终抛出给JVM虚拟机 +3. 虚拟机接收到异常对象后,先在控制台直接输出**异常栈**信息数据 +4. 直接从当前执行的异常点干掉当前程序 +5. 后续代码没有机会执行了,因为程序已经死亡 ```java public class ExceptionDemo { @@ -5760,17 +5838,16 @@ public class ExceptionDemo { -### 编译时异常 +### 编译异常 -#### 概念 +#### 基本介绍 -编译时异常:继承自Exception的异常或者其子类,没有继承RuntimeException - "编译时异常是编译阶段就会报错", - 必须程序员编译阶段就处理的。否则代码编译就报错!! +编译时异常:继承自Exception的异常或者其子类,没有继承 RuntimeException,编译时异常是编译阶段就会报错,必须程序员编译阶段就处理的。否则代码编译就报错 编译时异常的作用是什么: - 是担心程序员的技术不行,在编译阶段就爆出一个错误, 目的在于提醒! - 提醒程序员这里很可能出错,请检查并注意不要出bug。 + +* 是担心程序员的技术不行,在编译阶段就爆出一个错误, 目的在于提醒 +* 提醒程序员这里很可能出错,请检查并注意不要出bug ```java public static void main(String[] args) throws ParseException { @@ -5783,12 +5860,15 @@ public static void main(String[] args) throws ParseException { +**** + + + #### 处理机制 ##### throws -在出现编译时异常的地方层层把异常抛出去给调用者,调用者最终抛出给JVM虚拟机。 -JVM虚拟机输出异常信息,直接干掉程序,这种方式与默认方式是一样的。 +在出现编译时异常的地方层层把异常抛出去给调用者,调用者最终抛出给JVM虚拟机,JVM 虚拟机输出异常信息,直接干掉程序,这种方式与默认方式是一样的。 * 优点:可以解决代码编译时的错误 * 运行时出现异常,程序还是会立即死亡! @@ -5807,6 +5887,10 @@ public static void main(String[] args) throws Exception { +*** + + + ##### try/catch 可以处理异常,并且出现异常后代码也不会死亡。 @@ -5858,7 +5942,11 @@ public static void main(String[] args) { -##### 方法三 +*** + + + +##### 规范做法 在出现异常的地方把异常一层一层的抛出给最外层调用者,最外层调用者集中捕获处理!(**规范做法**) 这种方案最外层调用者可以知道底层执行的情况,同时程序在出现异常后也不会立即死亡(最好的方案) @@ -5884,22 +5972,20 @@ public class ExceptionDemo{ -### 运行时异常 +### 运行异常 -#### 概念 +#### 基本介绍 -​ 继承自RuntimeException的异常或者其子类, -​ 编译阶段是不会出错的,它是在运行时阶段可能出现的错误 -​ 运行时异常编译阶段可以处理也可以不处理,代码编译都能通过!! +继承自RuntimeException的异常或者其子类,编译阶段是不会出错的,它是在运行时阶段可能出现的错误,运行时异常编译阶段可以处理也可以不处理,代码编译都能通过!! -**常见的运行时异常。(面试题)** +**常见的运行时异常**: -​ 1.数组索引越界异常: ArrayIndexOutOfBoundsException -​ 2.空指针异常 : NullPointerException,直接输出没问题,调用空指针的变量的功能就会报错! -​ 3.类型转换异常:ClassCastException -​ 4.迭代器遍历没有此元素异常:NoSuchElementException -​ 5.算术异常(数学操作异常):ArithmeticException -​ 6.数字转换异常: NumberFormatException +1. 数组索引越界异常: ArrayIndexOutOfBoundsException +2. 空指针异常 : NullPointerException,直接输出没问题,调用空指针的变量的功能就会报错! +3. 类型转换异常:ClassCastException +4. 迭代器遍历没有此元素异常:NoSuchElementException +5. 算术异常(数学操作异常):ArithmeticException +6. 数字转换异常: NumberFormatException ```java public class ExceptionDemo { @@ -5932,11 +6018,13 @@ public class ExceptionDemo { +**** + + + #### 处理机制 -运行时异常在编译阶段是不会报错,在运行阶段才会出错。 -运行时异常在编译阶段不处理不会报错,但是运行时出错了程序还是会死亡,运行时异常也建议要处理。 -运行时异常是自动往外抛出的,不需要我们手工抛出。 +运行时异常在编译阶段是不会报错,在运行阶段才会出错,运行时出错了程序还是会停止,运行时异常也建议要处理,运行时异常是自动往外抛出的,不需要手工抛出 **运行时异常的处理规范**:直接在最外层捕获处理即可,底层会自动抛出!! @@ -5985,7 +6073,7 @@ finally: 0-1次 **finally的作用**:可以在代码执行完毕以后进行资源的释放操作 -资源:资源都是实现了Closeable接口的,都自带close()关闭方法! +资源:资源都是实现了 Closeable 接口的,都自带 close() 关闭方法! 注意:如果在 finally 中出现了 return,会吞掉异常 @@ -6028,43 +6116,20 @@ public class FinallyDemo { -**** - - - -### 注意事项 - -异常的语法注意: - -1. 运行时异常被抛出可以不处理,可以自动抛出;**编译时异常必须处理**;按照规范都应该处理 -2. **重写方法申明抛出的异常,应该与父类被重写方法申明抛出的异常一样或者范围更小** -3. 方法默认都可以自动抛出运行时异常, throws RuntimeException可以省略不写 -4. 当多异常处理时,捕获处理,前面的异常类不能是后面异常类的父类。 -5. 在try/catch后可以追加finally代码块,其中的代码一定会被执行,通常用于资源回收操作。 - - - *** -### 自定义异常 +### 自定义 自定义异常: -* 自定义编译时异常. - 1. 定义一个异常类继承Exception. - 2. 重写构造器。 - 3. 在出现异常的地方用throw new 自定义对象抛出! -* 自定义运行时异常. - 1. 定义一个异常类继承RuntimeException. - 2. 重写构造器。 - 3. 在出现异常的地方用throw new 自定义对象抛出! +* 自定义编译时异常:定义一个异常类继承 Exception,重写构造器,在出现异常的地方用throw new 自定义对象抛出 +* 自定义运行时异常:定义一个异常类继承 RuntimeException,重写构造器,在出现异常的地方用 throw new 自定义对象抛出! -**throws: 用在方法上,用于抛出方法中的异常。** - 用于告诉调用者,本方法内部可能会抛出异常,请你处理一下 -**throw: 用在出现异常的地方,创建异常对象且立即从此处抛出。** - 将这个异常对象传递到调用者处,并结束当前方法的执行 +**throws: 用在方法上,用于抛出方法中的异常** + +**throw: 用在出现异常的地方,创建异常对象且立即从此处抛出** ```java //需求:认为年龄小于0岁,大于200岁就是一个异常。 @@ -6106,10 +6171,21 @@ public class AgeIllegalRuntimeException extends RuntimeException{ -### 异常作用 +### 异常规范 + +异常的语法注意: + +1. 运行时异常被抛出可以不处理,可以自动抛出;**编译时异常必须处理**;按照规范都应该处理 +2. **重写方法申明抛出的异常,子类方法抛出的异常类型必须是父类抛出异常类型或为其子类型** +3. 方法默认都可以自动抛出运行时异常, throws RuntimeException 可以省略不写 +4. 当多异常处理时,捕获处理,前面的异常类不能是后面异常类的父类。 +5. 在 try/catch 后可以追加 finally 代码块,其中的代码一定会被执行,通常用于资源回收操作 -1、可以处理代码问题,防止程序出现异常后的死亡。 -2、提高了程序的健壮性和安全性。 +异常的作用: + +1. 可以处理代码问题,防止程序出现异常后的死亡 + +2. 提高了程序的健壮性和安全性 ```java public class Demo{ @@ -6434,7 +6510,7 @@ public class ConstructorDemo { -## IO +## I/O ### Stream @@ -10267,7 +10343,7 @@ GC Roots说明: * 将本对象引用到的其他对象全部挪到灰色集合中 * 将本对象挪到黑色集合里面 4. 重复步骤3,直至灰色集合为空时结束 -5. 结束后,仍在白色集合的对象即为GC Roots 不可达,可以进行回收 +5. 结束后,仍在白色集合的对象即为 GC Roots 不可达,可以进行回收 @@ -11537,7 +11613,7 @@ public class Test { ##### 时机 -类的初始化是懒惰的,只有在首次使用时才会被装载,JVM 不会无条件地装载 Class 类型,Java虚拟机规定,一个类或接口在初次使用前,必须要进行初始化 +类的初始化是懒惰的,只有在首次使用时才会被装载,JVM 不会无条件地装载 Class 类型,Java 虚拟机规定,一个类或接口在初次使用前,必须要进行初始化 **主动引用**:虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列情况必须对类进行初始化(加载、验证、准备都会随之发生): @@ -12810,7 +12886,7 @@ JVM 提供的操作数栈管理指令,可以用于直接操作操作数栈的 抛出异常指令:athrow 指令 -JVM 处理异常(catch语句)不是由字节码指令来实现的,而是**采用异常表来完成**的 +JVM 处理异常(catch 语句)不是由字节码指令来实现的,而是**采用异常表来完成**的 * 代码: @@ -12819,16 +12895,14 @@ JVM 处理异常(catch语句)不是由字节码指令来实现的,而是** int i = 0; try { i = 10; - } catch (ArithmeticException e) { - i = 30; - } catch (NullPointerException e) { - i = 40; } catch (Exception e) { - i = 50; + i = 20; + } finally { + i = 30; } } ``` - + * 字节码: * 多出一个 **Exception table** 的结构,[from, to) 是**前闭后开**的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号 @@ -12875,7 +12949,7 @@ JVM 处理异常(catch语句)不是由字节码指令来实现的,而是** ###### finally -finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程 +finally 中的代码被**复制了 3 份**,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程(上节案例) * 代码: @@ -14398,7 +14472,7 @@ public class BubbleSort { flag = 1;//发生了交换 } } - //没有发生交换,证明已经有序,不需要继续排序 + //没有发生交换,证明已经有序,不需要继续排序,节省时间 if(flag == 0) { break; } @@ -14438,7 +14512,6 @@ public class BubbleSort { ```java -// 0 1位置比较,小的放0位置,然后0 2位置比,小的继续放0位置,一轮循环0位置是最小值 public class SelectSort { public static void main(String[] args) { int[] arr = {55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88}; @@ -14496,19 +14569,20 @@ floor:向下取整 public class HeapSort { public static void main(String[] args) { int[] arr = {55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88}; - heapSort(arr, arr.length); + heapSort(arr, arr.length - 1); System.out.println(Arrays.toString(arr)); } //len为数组长度 private static void heapSort(int[] arr, int len) { //建堆,逆排序,因为堆排序定义的交换顺序是从当前结点往下交换,逆序排可以避免多余的交换 - for (int i = len / 2 - 1; i >= 0; i--) { + //i初始值是最后一个节点的父节点,如果是数组长度len,则 i = len / 2 -1 + for (int i = (len - 1) / 2; i >= 0; i--) { //调整函数 - sift(arr, i, len - 1); + sift(arr, i, len); } //从尾索引开始排序 - for (int i = len - 1; i > 0; i--) { + for (int i = len; i > 0; i--) { //将最大的节点放入末尾 int temp = arr[0]; arr[0] = arr[i]; @@ -14600,9 +14674,9 @@ public class InsertSort { 实现思路: -1. 选定一个增长量h,按照增长量h作为数据分组的依据,对数据进行分组 +1. 选定一个增长量 h,按照增长量 h 作为数据分组的依据,对数据进行分组 2. 对分好组的每一组数据完成插入排序 -3. 减小增长量,最小减为1,重复第二步操作 +3. 减小增长量,最小减为 1,重复第二步操作 @@ -14639,7 +14713,7 @@ public class ShellSort { } ``` -在希尔排序中,增长量h并没有固定的规则,有很多论文研究了各种不同的递增序列,但都无法证明某个序列是最好的,所以对于希尔排序的时间复杂度分析就认为 O(nlogn) +在希尔排序中,增长量 h 并没有固定的规则,有很多论文研究了各种不同的递增序列,但都无法证明某个序列是最好的,所以对于希尔排序的时间复杂度分析就认为 O(nlogn) diff --git a/Prog.md b/Prog.md index 7fbc016..8b20bc9 100644 --- a/Prog.md +++ b/Prog.md @@ -28,6 +28,8 @@ 参考视频:https://www.bilibili.com/video/BV16J411h7Rd(推荐观看) +笔记的整体内容依据视频编写,并且随着不断的学习补充了很多新知识 + *** @@ -272,11 +274,11 @@ Java 中 main 方法启动的是一个进程也是一个主线程,main 方法 -### 常用API +### 线程API #### API -Thread类API: +Thread 类 API: | 方法 | 说明 | | ------------------------------------------- | ------------------------------------------------------------ | @@ -311,7 +313,7 @@ start:使用 start 是启动新的线程,此线程处于就绪(可运行 说明:**线程控制资源类** -**面试问题**:run()方法中的异常不能抛出,只能try/catch +**面试问题**:run() 方法中的异常不能抛出,只能 try/catch * 因为父类中没有抛出任何异常,子类不能比父类抛出更多的异常 * 异常不能跨线程传播回 main() 中,因此必须在本地进行处理 @@ -327,7 +329,7 @@ start:使用 start 是启动新的线程,此线程处于就绪(可运行 sleep: * 调用 sleep 会让当前线程从 Running 进入 `Timed Waiting` 状态(阻塞) -* sleep()方法的过程中,线程不会释放对象锁 +* sleep() 方法的过程中,线程不会释放对象锁 * 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException * 睡眠结束后的线程未必会立刻得到执行 * 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性 @@ -336,7 +338,7 @@ yield: * 调用 yield 会让提示线程调度器让出当前线程对CPU的使用 * 具体的实现依赖于操作系统的任务调度器 -* **会放弃CPU资源,锁资源不会释放** +* **会放弃 CPU 资源,锁资源不会释放** @@ -346,8 +348,9 @@ yield: #### priority -* 线程优先级会提示(hint)调度器优先调度该线程,但这仅仅是一个提示,调度器可以忽略它 -* 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作 +线程优先级会提示(hint)调度器优先调度该线程,但这仅仅是一个提示,调度器可以忽略它 + +如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用 @@ -416,7 +419,7 @@ public class Test { ##### 打断线程 `public void interrupt()`:中断这个线程,异常处理机制 -`public static boolean interrupted()`:判断当前线程是否被打断,清除打断标记 +`public static boolean interrupted()`:判断当前线程是否被打断,,打断返回 true,清除打断标记 `public boolean isInterrupted()`:判断当前线程是否被打断,不清除打断标记 * sleep,wait,join 方法都会让线程进入阻塞状态,打断进程**会清空打断状态** (false) @@ -490,7 +493,9 @@ LockSupport.park();//失效,不会阻塞 System.out.println("unpark...");//和上一个unpark同时执行 ``` -可以修改获取打断状态方法,使用`Thread.interrupted()`,清除打断标记 +可以修改获取打断状态方法,使用 `Thread.interrupted()`,清除打断标记 + +LockSupport 类在 同步 → park-un 详解 @@ -689,7 +694,7 @@ Java: -## 管程 +## 同步 ### 临界区 @@ -709,7 +714,7 @@ Java: * 阻塞式的解决方案:synchronized,Lock * 非阻塞式的解决方案:原子变量 -管程(monitor):由局部于自己的若干公共变量和所有访问这些公共变量的过程所组成的软件模块,保证同一时刻只有一个进程在管程内活动,即管程内定义的操作在同一时刻只被一个进程调用(由编译器实现) +管程(monitor):由局部于自己的若干公共变量和所有访问这些公共变量的过程所组成的软件模块,保证同一时刻只有一个进程在管程内活动,即管程内定义的操作在同一时刻只被一个进程调用(由编译器实现) **synchronized:对象锁,保证了临界区内代码的原子性**,采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其它线程获取这个对象锁时会阻塞,保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换 @@ -1648,9 +1653,11 @@ public class demo { ### park-un +LockSupport 是用来创建锁和其他同步类的线程阻塞原语 + LockSupport 类方法: -* `LockSupport.park()`:暂停当前线程 +* `LockSupport.park()`:暂停当前线程,原语 * `LockSupport.unpark(暂停的线程对象)`:恢复某个线程的运行 ```java @@ -1670,16 +1677,16 @@ public static void main(String[] args) { } ``` -对比wait & notify: +LockSupport 出现就是为了增强 wait & notify 的功能: -* wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而park、unpark不需要 +* wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park、unpark 不需要 * park & unpark以线程为单位来阻塞和唤醒线程,而notify 只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程 * **park & unpark 可以先 unpark**,而 wait & notify 不能先 notify。类比生产消费,先消费发现有产品就消费,没有就等待;先生产就直接产生商品,然后线程直接消费 -* wait 会释放锁资源进入等待队列,park 不会释放锁资源,只负责阻塞当前线程,会释放CPU +* wait 会释放锁资源进入等待队列,park 不会释放锁资源,只负责阻塞当前线程,会释放 CPU 原理: -* 先park: +* 先 park: 1. 当前线程调用 Unsafe.park() 方法 2. 检查 _counter ,本情况为 0,这时获得 _mutex 互斥锁 3. 线程进入 _cond 条件变量阻塞 @@ -1688,7 +1695,7 @@ public static void main(String[] args) { ![](https://gitee.com/seazean/images/raw/master/Java/JUC-park原理1.png) -* 先unpark: +* 先 unpark: 1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1 2. 当前线程调用 Unsafe.park() 方法 @@ -1720,7 +1727,7 @@ public static void main(String[] args) { * 如果该对象没有逃离方法的作用访问,它是线程安全的(每一个方法有一个栈帧) * 如果该对象逃离方法的作用范围,需要考虑线程安全问题(暴露引用) -常见线程安全类:String、Integer、StringBuffer、Random、Vector、Hashtable、java.util.concurrent包 +常见线程安全类:String、Integer、StringBuffer、Random、Vector、Hashtable、java.util.concurrent 包 * 线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的 @@ -5200,8 +5207,8 @@ AQS:AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的 * 用 state 属性来表示资源的状态(分**独占模式和共享模式**),子类需要定义如何维护这个状态,控制如何获取 锁和释放锁 - * 独占模式是只有一个线程能够访问资源,如ReentrantLock - * 共享模式允许多个线程访问资源,如Semaphore,ReentrantReadWriteLock是组合式 + * 独占模式是只有一个线程能够访问资源,如 ReentrantLock + * 共享模式允许多个线程访问资源,如 Semaphore,ReentrantReadWriteLock 是组合式 * 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList(**同步队列:双向,便于出队入队**) * 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet(**条件队列:单向**) @@ -5211,7 +5218,7 @@ AQS 核心思想: * 被请求的共享资源被占用,AQS 用 CLH 队列实现线程阻塞等待以及被唤醒时锁分配的(park)机制,即将暂时获取不到锁的线程加入到队列中 - CLH是一种基于单向链表的**高性能、公平的自旋锁**,AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配 + CLH 是一种基于单向链表的**高性能、公平的自旋锁**,AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配 @@ -5244,21 +5251,21 @@ AQS 核心思想: } ``` -state设计: +AQS 中 state 设计: * state 使用了 32bit int 来维护同步状态 * state **使用 volatile 修饰配合 cas** 保证其修改时的原子性 -* state 表示线程重入的次数或者许可进入的线程数 +* 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 + `protected final boolean compareAndSetState(int expect,int update)`:**cas** 设置 state -waitstate设计: +Node 节点中 waitstate 设计: -* 使用**volatile 修饰配合 cas**保证其修改时的原子性 +* 使用 **volatile 修饰配合 CAS** 保证其修改时的原子性 -* 表示Node节点的状态,有以下几种状态: +* 表示 Node 节点的状态,有以下几种状态: ```java //由于超时或中断,此节点被取消,不会再改变状态 @@ -5300,6 +5307,7 @@ waitstate设计: node.prev = t; // 将 tail 从原来的 tail 设置为 node if (compareAndSetTail(t, node)) { + //将原来的尾节点(哑元节点)的 next 指向新节点 t.next = node; return t; } @@ -5307,7 +5315,7 @@ waitstate设计: } } ``` - + *** @@ -5419,7 +5427,7 @@ class MyLock implements Lock { #### 锁对比 -ReentrantLock相对于 synchronized 它具备如下特点: +ReentrantLock 相对于 synchronized 它具备如下特点: 1. 锁的实现:synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的 2. 性能:新版本 Java 对 synchronized 进行了很多优化,synchronized 与 ReentrantLock 大致相同 @@ -5509,7 +5517,7 @@ public ReentrantLock() { NonfairSync 继承自 AQS -没有竞争:ExclusiveOwnerThread属于 Thread-0,state设置为1 +没有竞争:ExclusiveOwnerThread属于 Thread-0,state 设置为1 ```java final void lock() { @@ -5566,13 +5574,14 @@ Thread-1执行: * 接下来进入 addWaiter 逻辑,构造 Node 队列 - * 图中黄色三角表示该 Node 的 waitStatus 状态,其中 0 为默认正常状态 + * 图中黄色三角表示该 Node 的 waitStatus 状态,其中 0 为默认**正常状态** * Node 的创建是懒惰的 - * 其中第一个 Node 称为 Dummy(哑元)或哨兵,用来占位,并不关联线程 + * 其中第一个 Node 称为 **Dummy(哑元)或哨兵**,用来占位,并不关联线程 ```java private Node addWaiter(Node mode) { - // 将当前线程关联到一个 Node 对象上, 模式为独占模式 + // 将当前线程关联到一个 Node 对象上, 模式为独占模式 + // Node.EXCLUSIVE for exclusive, Node.SHARED for shared Node node = new Node(Thread.currentThread(), mode); Node pred = tail; // 如果 tail 不为 null, cas 尝试将 Node 对象加入 AQS 队列尾部 @@ -5594,7 +5603,7 @@ Thread-1执行: * acquireQueued 会在一个死循环中不断尝试获得锁,失败后进入 park 阻塞 - * 如果当前线程是在head节点后,会再次 tryAcquire 尝试获取锁,state 仍为 1 则失败(第三次) + * 如果当前线程是在 head 节点后,会再次 tryAcquire 尝试获取锁,state 仍为 1 则失败(第三次) ```java final boolean acquireQueued(final Node node, int arg) { @@ -5646,7 +5655,7 @@ Thread-1执行: ``` * shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued ,再次 tryAcquire 尝试获取锁,这时state 仍为 1 获取失败(第四次) - * 当再次进入 shouldParkAfterFailedAcquire 时,这时其前驱 node 的 waitStatus 已经是 -1,返回true + * 当再次进入 shouldParkAfterFailedAcquire 时,这时其前驱 node 的 waitStatus 已经是 -1,返回 true * 进入 parkAndCheckInterrupt, Thread-1 park(灰色表示),再有多个线程经历竞争失败后: ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ReentrantLock-非公平锁3.png) @@ -5676,7 +5685,7 @@ Thread-0 释放锁,进入 release 流程 * 进入 tryRelease - * 设置exclusiveOwnerThread 为 null + * 设置 exclusiveOwnerThread 为 null * state = 0 * 当前队列不为 null,并且 head 的 waitStatus = -1,进入 unparkSuccessor @@ -5710,7 +5719,7 @@ Thread-0 释放锁,进入 release 流程 } ``` -* 进入unparkSuccessor 方法 +* 进入 unparkSuccessor 方法 * 找到队列中离 head 最近的一个 Node(没取消的),unpark 恢复其运行,本例中即为 Thread-1 * 回到 Thread-1 的 acquireQueued 流程 @@ -5759,7 +5768,7 @@ Thread-0 释放锁,进入 release 流程 ##### 公平原理 -与非公平锁主要区别在于 tryAcquire 方法:先检查 AQS 队列中是否有前驱节点,没有才去CAS竞争 +与非公平锁主要区别在于 tryAcquire 方法:先检查 AQS 队列中是否有前驱节点,没有才去 CAS 竞争 ```java static final class FairSync extends Sync { @@ -5833,7 +5842,7 @@ public static void method2() { } ``` -面试题:在Lock方法加两把锁会是什么情况呢? +面试题:在 Lock 方法加两把锁会是什么情况呢? * 加锁两次解锁两次:正常执行 * 加锁两次解锁一次:程序直接卡死,线程不能出来,也就说明**申请几把锁,最后需要解除几把锁** @@ -6126,10 +6135,9 @@ class Chopstick extends ReentrantLock { ##### 基本使用 -synchronized 中的条件变量,就是当条件不满足时进入 waitSet 等待 -ReentrantLock 的条件变量比 synchronized 强大之处在于,它支持多个条件变量 +synchronized 的条件变量,是当条件不满足时进入 waitSet 等待;ReentrantLock 的条件变量比 synchronized 强大之处在于,它支持多个条件变量 -ReentrantLock类获取Condition对象:`public Condition newCondition()` +ReentrantLock 类获取 Condition 对象:`public Condition newCondition()` Condition类API: diff --git a/SSM.md b/SSM.md index d2f7214..6653c76 100644 --- a/SSM.md +++ b/SSM.md @@ -2299,7 +2299,7 @@ MyBatis 提供了 org.apache.ibatis.jdbc.SQL 功能类,专门用于构建 SQL ![](https://gitee.com/seazean/images/raw/master/Frame/MyBatis-执行流程.png) -MyBatis运行原理: +MyBatis 运行原理: 1. 通过加载 mybatis 全局配置文件以及 mapper 映射文件初始化 configuration 对象 和 Executor 对象(通过全局配置文件中的 defaultExecutorType 初始化) @@ -2421,7 +2421,7 @@ executor.query():开始执行查询语句,参数要被包装成集合类 * `CachingExecutor.query()`:先执行 - * `MappedStatement.getBoundSql(parameterObject)`:把 **parameterObject** 封装成 BoundSql 对象 + * `MappedStatement.getBoundSql(parameterObject)`:**把 parameterObject 封装成 BoundSql 对象** 构造函数中有:`this.parameterObject = parameterObject` ![](https://gitee.com/seazean/images/raw/master/Frame/MyBatis-boundSql对象.png) @@ -2442,7 +2442,7 @@ executor.query():开始执行查询语句,参数要被包装成集合类 * 判断 BoundSql 是否被创建,没有创建会重新封装参数信息到 BoundSql * 创建 StatementHandler 的构造方法中,创建了 ParameterHandler 和 ResultSetHandler 对象 * `interceptorChain.pluginAll(statementHandler)`:拦截器链 - * `prepareStatement(StatementHandler, Log)`:**创建 JDBC 的 Statement 对象**,底层通过 JDBC 创建 + * `prepareStatement()`:通过 StatementHandler **创建 JDBC 的 Statement 对象** * `getConnection()`:获取 JDBC 的 Connection 对象 * `handler.prepare()`:初始化 Statement 对象 * `instantiateStatement(Connection connection)`:**Connection** 中的方法实例化对象 @@ -6388,7 +6388,7 @@ Spirng可以通过配置的形式控制使用的代理形式,Spring会先判 ## 事务 -### 基本介绍 +### 事务机制 #### 事务介绍 @@ -6409,7 +6409,7 @@ Spring在事务开始时,根据当前环境中设置的隔离级别,调整 * 在 MySQL 中,恢复机制是通过**回滚日志(undo log)** 实现,所有事务进行的修改都会先先记录到这个回滚日志中,然后再执行相关的操作。如果执行过程中遇到异常的话,直接利用回滚日志中的信息将数据回滚到修改之前的样子即可 * 回滚日志会先于数据持久化到磁盘上,这样保证了即使遇到数据库突然宕机等情况,当用户再次启动数据库的时候,数据库还能够通过查询回滚日志来回滚将之前未完成的事务 -事务不生效的问题:--> **Transactional注解** +事务不生效的问题:参考 **Transactional注解** @@ -6425,14 +6425,14 @@ TransactionDefinition 接口中定义了五个表示隔离级别的常量: - **TransactionDefinition.ISOLATION_READ_UNCOMMITTED**:最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读 - **TransactionDefinition.ISOLATION_READ_COMMITTED**:允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生 - **TransactionDefinition.ISOLATION_REPEATABLE_READ**:对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。 -- **TransactionDefinition.ISOLATION_SERIALIZABLE**:最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别 +- **TransactionDefinition.ISOLATION_SERIALIZABLE**:最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别 MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ(可重读)** **分布式事务**:允许多个独立的事务资源(transactional resources)参与到一个全局的事务中 事务资源通常是关系型数据库系统,但也可以是其他类型的资源,全局事务要求在其中的所有参与的事务要么都提交,要么都回滚,这对于事务原有的ACID要求又有了提高 -在使用分布式事务时,InnoDB存储引擎的事务隔离级别必须设置为SERIALIZABLE +在使用分布式事务时,InnoDB 存储引擎的事务隔离级别必须设置为 SERIALIZABLE @@ -6477,7 +6477,7 @@ MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ( **其他情况:** -* **TransactionDefinition.PROPAGATION_NESTED:** 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED +* TransactionDefinition.PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 TransactionDefinition.PROPAGATION_REQUIRED @@ -6577,9 +6577,9 @@ PlatformTransactionManager,平台事务管理器实现类: String transient4; // not persistent because of @Transient ``` -- JDO(Java Data Object )是Java对象持久化规范,用于存取某种数据库中的对象,并提供标准化API。与JDBC相比,JDBC仅针对关系数据库进行操作,JDO可以扩展到关系数据库、文件、XML、对象数据库(ODBMS)等,可移植性更强 +- JDO(Java Data Object)是Java对象持久化规范,用于存取某种数据库中的对象,并提供标准化API。JDBC 仅针对关系数据库进行操作,JDO 可以扩展到关系数据库、XML、对象数据库等,可移植性更强 -- JTA(Java Transaction API)Java EE 标准之一,允许应用程序执行分布式事务处理。与JDBC相比,JDBC事务则被限定在一个单一的数据库连接,而一个JTA事务可以有多个参与者,比如JDBC连接、JDO 都可以参与到一个JTA事务中 +- JTA(Java Transaction API)Java EE 标准之一,允许应用程序执行分布式事务处理。与 JDBC 相比,JDBC 事务则被限定在一个单一的数据库连接,而一个 JTA 事务可以有多个参与者,比如 JDBC 连接、JDO 都可以参与到一个 JTA 事务中 此接口定义了事务的基本操作: @@ -6632,7 +6632,7 @@ TransactionStatus 此接口定义了事务在执行过程中某个时间点上 -### 编程式事务 +### 编程式 #### 控制方式 @@ -6867,13 +6867,13 @@ TransactionStatus 此接口定义了事务在执行过程中某个时间点上 -### 声明式事务 +### 声明式 #### XML ##### tx使用 -* 删除TxAdvice通知类,开启tx命名空间,配置applicationContext.xml +* 删除 TxAdvice 通知类,开启 tx 命名空间,配置 applicationContext.xml ```xml @@ -11667,6 +11667,8 @@ public class ProjectExceptionAdivce { ## 基本概述 +(这部分笔记做的非常一般,更新完 Spring 源码马上就会完善) + SpringBoot提供了一种快速使用Spring的方式,基于约定优于配置的思想,可以让开发人员不必在配置与逻辑业务之间进行思维的切换,全身心的投入到逻辑业务的代码编写中,从而大大提高了开发的效率 SpringBoot功能: diff --git a/Tool.md b/Tool.md index 8d16914..16f6624 100644 --- a/Tool.md +++ b/Tool.md @@ -618,9 +618,6 @@ Linux 文件系统目录结构和熟知的 windows 系统有较大区别,没 执行保存 :wq! ``` - - - * 重启网络:systemctl restart network @@ -640,12 +637,12 @@ Linux 文件系统目录结构和熟知的 windows 系统有较大区别,没 ### 远程登陆 -**服务器维护工作** 都是在 远程 通过SSH客户端 来完成的, 并没有图形界面, 所有的维护工作都需要通过命令来完成,Linux 服务器需要安装SSH 相关服务。 +**服务器维护工作** 都是在 远程 通过 SSH 客户端 来完成的, 并没有图形界面, 所有的维护工作都需要通过命令来完成,Linux 服务器需要安装 SSH 相关服务。 首先执行 sudo apt-get install openssh-server 指令。接下来用 xshell 连接。 ![](https://gitee.com/seazean/images/raw/master/Tool/远程连接Linux.png) -先用普通用户登录,然后转成root +先用普通用户登录,然后转成 root @@ -655,6 +652,8 @@ Linux 文件系统目录结构和熟知的 windows 系统有较大区别,没 + + ## 用户管理 Linux 系统是一个多用户、多任务的操作系统。多用户是指在 linux 操作系统中可以创建多个用户,而这些多用户又可以同时执行各自不同的任务,而互不影响。和 windows 系统有很大区别。 @@ -843,10 +842,14 @@ gpasswd 是 Linux 工作组文件 /etc/group 和 /etc/gshadow 管理工具,用 + + *** + + ## 系统管理 ### man @@ -1197,6 +1200,8 @@ exit命令用于退出目前的shell。执行exit可使shell以指定的状态 + + ## 文件管理 ### 常用命令 @@ -2171,6 +2176,8 @@ ll + + ## 进程管理 ### 查看进程 @@ -2304,6 +2311,8 @@ pid_t waitpid(pid_t pid, int *status, int options) + + ## 网络管理 ### network @@ -2404,6 +2413,8 @@ netstat [-acCeFghilMnNoprstuvVwx][-A<网络类型>][--ip] + + ## 磁盘管理 ### 挂载概念 @@ -2514,7 +2525,9 @@ mount [-fnrsvw] [-t vfstype] [-o options] device dir ![挂载成功](https://gitee.com/seazean/images/raw/master/Tool/挂载成功.png) * 查看挂载内容:`ls -l -a ./mnt/cdrom/` -* 卸载cdrom:`umount /mnt/cdrom/` +* 卸载 cdrom:`umount /mnt/cdrom/` + + @@ -2522,6 +2535,8 @@ mount [-fnrsvw] [-t vfstype] [-o options] device dir + + ## 防火墙 ### 概述 @@ -2567,8 +2582,8 @@ mount [-fnrsvw] [-t vfstype] [-o options] device dir ## Shell -> shell脚本类似于我们在Windows中编写的批处理文件,它的扩展名是.bat,比如我们启动Tomcat(后面的课程我们会详细讲解)的时候经常启动的startup.bat,就是Windows下的批处理文件。 -> 而在Linux中,shell脚本编写的文件是以.sh结尾的。比如Tomcat下我们经常使用startup.sh来启动我们的Tomcat,这个startup.sh文件就是shell编写的。 +> shell 脚本类似于我们在 Windows 中编写的批处理文件,它的扩展名是.bat,比如我们启动 Tomcat(后面的课程我们会详细讲解)的时候经常启动的 startup.bat,就是 Windows 下的批处理文件。 +> 而在 Linux 中,shell脚本编写的文件是以 .sh 结尾的。比如 Tomcat 下我们经常使用 startup.sh 来启动我们的 Tomcat,这个 startup.sh 文件就是 shell 编写的。 ### 入门 From 65bbe59e89846dea44fab44310cbb58781d493f9 Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 15 Jul 2021 14:46:04 +0800 Subject: [PATCH 071/242] Update Java Notes --- Java.md | 1180 +++++++++++++++++++++++++++++++++++++++++++++++-------- Prog.md | 46 ++- Tool.md | 6 +- 3 files changed, 1037 insertions(+), 195 deletions(-) diff --git a/Java.md b/Java.md index 66048c7..69c1d54 100644 --- a/Java.md +++ b/Java.md @@ -2452,9 +2452,9 @@ s = s + "cd"; //s = abccd 新对象 `public int indexOf(String str)` : 返回指定子字符串第一次出现的字符串内的索引,没有返回-1 `public int lastIndexOf(String str)` : 返回字符串最后一次出现str的索引,没有返回-1 `public String substring(int beginIndex)` : 返回子字符串,以原字符串指定索引处到结尾 -`public String substring(int beginIndex, int endIndex)` : 返回原字符串指定索引处的字符串 -`public String toLowerCase()` : 将此String所有字符转换为小写,使用默认语言环境的规则 -`public String toUpperCase()` : 使用默认语言环境的规则将此String所有字符转换为大写 +`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 @@ -4070,7 +4070,7 @@ public class ArrayList extends AbstractList 同步:Vector的实现与 ArrayList 类似,但是方法上使用了 synchronized 进行同步 -构造:默认长度为10的数组 +构造:默认长度为 10 的数组 扩容:Vector 的构造函数可以传入 capacityIncrement 参数,作用是在扩容时使容量 capacity 增长 capacityIncrement,如果这个参数的值小于等于 0(默认0),扩容时每次都令 capacity 为原来的两倍 @@ -4319,11 +4319,11 @@ TreeSet 集合自排序的方式: * 直接为**对象的类**实现比较器规则接口 Comparable,重写比较方法: - 方法:`public int compareTo(Employee o): this是比较者, o是被比较者 ` + 方法:`public int compareTo(Employee o): this是比较者, o是被比较者` * 比较者大于被比较者,返回正数 * 比较者小于被比较者,返回负数 - * 比较者等于被比较者,返回0 + * 比较者等于被比较者,返回 0 * 直接为**集合**设置比较器 Comparator 对象,重写比较方法: @@ -4331,7 +4331,7 @@ TreeSet 集合自排序的方式: * 比较者大于被比较者,返回正数 * 比较者小于被比较者,返回负数 - * 比较者等于被比较者,返回0 + * 比较者等于被比较者,返回 0 注意:如果类和集合都带有比较规则,优先使用集合自带的比较规则 @@ -4364,6 +4364,7 @@ public class Student implements Comparable{ public int compareTo(Student o) { int result = this.age - o.age; return result == 0 ? this.getName().compareTo(o.getName):result; + } } ``` @@ -6631,7 +6632,7 @@ class Student{ #### 终结方法 -终结方法:Stream调用了终结方法,流的操作就全部终结,不能继续使用,如foreach , count方法等 +终结方法:Stream 调用了终结方法,流的操作就全部终结,不能继续使用,如 foreach,count 方法等 非终结方法:每次调用完成以后返回一个新的流对象,可以继续使用,支持**链式编程**! @@ -6690,16 +6691,16 @@ public static void main(String[] args) { ### File -#### 概述 +#### 文件类 -File类:代表操作系统的文件对象,是用来操作操作系统的文件对象的,删除文件,获取文件信息,创建文件(文件夹),广义来说操作系统认为文件包含(文件和文件夹) +File 类:代表操作系统的文件对象,是用来操作操作系统的文件对象的,删除文件,获取文件信息,创建文件(文件夹),广义来说操作系统认为文件包含(文件和文件夹) -File类构造器: +File 类构造器: `public File(String pathname)`:根据路径获取文件对象 `public File(String parent , String child)`:根据父路径和文件名称获取文件对象! `public File(File parent , String child)` -File类创建文件对象的格式: +File 类创建文件对象的格式: * `File f = new File("绝对路径/相对路径");` * 绝对路径:从磁盘的的盘符一路走到目的位置的路径。 @@ -6975,16 +6976,16 @@ public static void searchFiles(File dir , String fileName){ #### 概述 -IO输入输出流:输入/输出流 +IO 输入输出流:输入/输出流 * Input:输入 * Output:输出 引入:File类只能操作文件对象本身,不能读写文件对象的内容,读写数据内容,应该使用IO流 -IO流是一个水流模型:IO理解成水管,把数据理解成水流 +IO 流是一个水流模型:IO 理解成水管,把数据理解成水流 -IO流的分类: +IO 流的分类: * 按照流的方向分为:输入流,输出流。 * 输出流:以内存为基准,把内存中的数据写出到磁盘文件或者网络介质中去的流称为输出流 @@ -7023,7 +7024,7 @@ ObjectInputStream ObjectOutputStream ##### 字节输入 -FileInputStream文件字节输入流: +FileInputStream 文件字节输入流: * 作用:以内存为基准,把磁盘文件中的数据按照字节的形式读入到内存中的流 @@ -9395,9 +9396,9 @@ JVM:全称 Java Virtual Machine,即 Java 虚拟机,一种规范,本身 * Java 虚拟机基于**二进制字节码**执行,由一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆、一个方法区等组成 * JVM 屏蔽了与操作系统平台相关的信息,从而能够让 Java 程序只需要生成能够在 JVM 上运行的字节码文件,通过该机制实现的**跨平台性** -Java代码执行流程:java程序 --(编译)--> 字节码文件 --(解释执行)--> 操作系统(Win,Linux) +Java 代码执行流程:java程序 --(编译)--> 字节码文件 --(解释执行)--> 操作系统(Win,Linux) -JVM结构: +JVM 结构: @@ -9448,7 +9449,10 @@ JVM的生命周期分为三个阶段,分别为:启动、运行、死亡。 - 在 JVM 内部有两种线程类型,分别为:用户线程和守护线程,**JVM 使用的是守护线程,main() 和其他线程使用的是用户线程**,守护线程会随着用户线程的结束而结束 - 执行一个 Java 程序时,真真正正在执行的是一个 Java 虚拟机的进程 - JVM 有两种运行模式 Server 与 Client,两种模式的区别在于:Client 模式启动速度较快,Server 模式启动较慢;但是启动进入稳定期长期运行之后 Server 模式的程序运行速度比 Client 要快很多 + + Server 模式启动的 JVM 采用的是重量级的虚拟机,对程序采用了更多的优化,而 Client 模式启动的 JVM 采用的是轻量级的虚拟机 - **死亡**: + - 当程序中的用户线程都中止,JVM 才会退出 - 程序正常执行结束、程序异常或错误而异常终止、操作系统错误导致终止 - 线程调用 Runtime 类 halt 方法或 System 类 exit 方法,并且 java 安全管理器允许这次 exit 或 halt 操作 @@ -9456,37 +9460,10 @@ JVM的生命周期分为三个阶段,分别为:启动、运行、死亡。 -*** - - - -### 相关参数 - -进入 Run/Debug Configurations ---> VM options 设置参数 - -| 参数 | 功能 | -| ------------------------------------------------------------ | ------------------------------------------------------------ | -| -Xms | 堆初始大小(默认为物理内存的1/64) | -| -Xmx 或 -XX:MaxHeapSize=size | 堆最大大小(默认为物理内存的1/4) | -| -Xmn 或 -XX:NewSize=size + -XX:MaxNewSize=size | 新生代大小(初始值及最大值) | -| -XX:NewRatio | 新生代与老年代在堆结构的占比 | -| -XX:SurvivorRatio=ratio | 幸存区比例(Eden和S0/S1空间的比例) | -| -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy | 幸存区比例(动态) | -| -XX:MaxTenuringThreshold=threshold | 晋升阈值 | -| -XX:+PrintTenuringDistribution | 晋升详情 | -| -XX:+PrintFlagsInitial | 查看所有的参数的默认初始值 | -| -XX:+PrintFlagsFinal | 查看所有的参数的最终值
(可能会存在修改,不再是初始值) | -| -XX:+PrintGCDetails | GC详情,打印gc简要信息:
1. -XX:+PrintGC 2. - verbose:gc | -| -XX:+ScavengeBeforeFullGC | FullGC 前 MinorGC | -| -XX:+DisableExplicitGC | 禁用显式垃圾回收,让System.gc无效 | - -说明:参数前面是`+`号说明是开启,如果是`- `号说明是关闭 - - +*** -*** @@ -9498,10 +9475,10 @@ JVM的生命周期分为三个阶段,分别为:启动、运行、死亡。 JVM 内存结构规定了 Java 在运行过程中内存申请、分配、管理的策略,保证了 JVM 的高效稳定运行 -* Java1.8以前的内存结构图: +* Java1.8 以前的内存结构图: ![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java7内存结构图.png) -* Java1.8之后的内存结果图: +* Java1.8 之后的内存结果图: ![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java8内存结构图.png) @@ -9584,7 +9561,7 @@ Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所 局部变量表最基本的存储单元是 **slot(变量槽)**: -* 参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束,JVM为每一个slot都分配一个访问索引,通过索引即可访问到槽中的数据 +* 参数值的存放总是在局部变量数组的 index0 开始,到数组长度 -1 的索引结束,JVM 为每一个 slot 都分配一个访问索引,通过索引即可访问到槽中的数据 * 存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress 类型的变量 * 32 位以内的类型只占一个 slot(包括returnAddress类型),64 位的类型(long 和 double)占两个 slot * 局部变量表中的槽位是可以**重复利用**的,如果一个局部变量过了其作用域,那么之后申明的新的局部变量就可能会复用过期局部变量的槽位,从而达到节省资源的目的 @@ -9696,13 +9673,13 @@ Program Counter Register 程序计数器(寄存器) 原理: -* java虚拟机对于多线程是通过线程轮流切换并且分配线程执行时间,一个处理器只会处理执行一个线程 +* JVM 对于多线程是通过线程轮流切换并且分配线程执行时间,一个处理器只会处理执行一个线程 * 切换线程需要从程序计数器中来回去到当前的线程上一次执行的行号 特点: * **是线程私有的** -* 不会存在内存溢出,是JVM规范中唯一一个不出现OOM的区域,所以这个空间不会进行GC +* 不会存在内存溢出,是 JVM 规范中唯一一个不出现 OOM 的区域,所以这个空间不会进行 GC Java**反编译**指令:`javap -v Test.class` @@ -9734,13 +9711,13 @@ Heap 堆:是JVM内存中最大的一块,由所有线程共享,由垃圾回 * 对象实例:类初始化生成的对象,**基本数据类型的数组也是对象实例**,new 创建对象都使用堆内存 * 字符串常量池: * 字符串常量池原本存放于方法区,jdk7开始放置于堆中 - * 字符串常量池**存储的是string对象的直接引用或者对象**,是一张string table -* 静态变量:静态变量是有static修饰的变量,jdk7时从方法区迁移至堆中 -* 线程分配缓冲区(Thread Local Allocation Buffer):线程私有但不影响堆的共性,可以提升对象分配的效率 + * 字符串常量池**存储的是 string 对象的直接引用或者对象**,是一张 string table +* 静态变量:静态变量是有 static 修饰的变量,jdk7 时从方法区迁移至堆中 +* 线程分配缓冲区 Thread Local Allocation Buffer:线程私有但不影响堆的共性,可以提升对象分配的效率 设置堆内存指令:`-Xmx Size` -内存溢出:new出对象,循环添加字符数据,当堆中没有内存空间可分配给实例,也无法再扩展时,就会抛出OutOfMemoryError异常 +内存溢出:new 出对象,循环添加字符数据,当堆中没有内存空间可分配给实例,也无法再扩展时,就会抛出 OutOfMemoryError 异常 堆内存诊断工具:(控制台命令) @@ -9748,13 +9725,13 @@ Heap 堆:是JVM内存中最大的一块,由所有线程共享,由垃圾回 2. jmap:查看堆内存占用情况 `jhsdb jmap --heap --pid 进程id` 3. jconsole:图形界面的,多功能的监测工具,可以连续监测 -在Java7中堆内会存在**年轻代、老年代和方法区(永久代)**: +在 Java7 中堆内会存在**年轻代、老年代和方法区(永久代)**: -* Young区被划分为三部分,Eden区和两个大小严格相同的Survivor区。Survivor区间,某一时刻只有其中一个是被使用的,另外一个留做垃圾回收时复制对象。在Eden区变满的时候, GC就会将存活的对象移到空闲的Survivor区间中,根据JVM的策略,在经过几次垃圾回收后,仍然存活于Survivor的对象将被移动到Tenured区间 -* Tenured区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在Young复制转移一定的次数以后,对象就会被转移到Tenured区 -* Perm代主要保存**Class、ClassLoader、静态变量、常量、编译后的代码**,在java7中堆内方法区会受到GC的管理 +* Young 区被划分为三部分,Eden 区和两个大小严格相同的 Survivor 区。Survivo r区间,某一时刻只有其中一个是被使用的,另外一个留做垃圾回收时复制对象。在 Eden 区变满的时候, GC 就会将存活的对象移到空闲的 Survivor 区间中,根据JVM的策略,在经过几次垃圾回收后,仍然存活于 Survivor 的对象将被移动到 Tenured 区间 +* Tenured 区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在 Young 复制转移一定的次数以后,对象就会被转移到 Tenured 区 +* Perm 代主要保存**Class、ClassLoader、静态变量、常量、编译后的代码**,在 java7 中堆内方法区会受到 GC 的管理 -分代原因:不同对象的生命周期不同,70%-99%的对象都是临时对象,优化GC性能 +分代原因:不同对象的生命周期不同,70%-99% 的对象都是临时对象,优化 GC 性能 ```java public static void main(String[] args) { @@ -9942,6 +9919,8 @@ public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制 + + ## 内存管理 ### 内存分配 @@ -9965,7 +9944,7 @@ public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制 ##### 分代介绍 -在java8时,堆被分为了两份:新生代和老年代(1:2),在java7时,还存在一个永久代 +Java8 时,堆被分为了两份:新生代和老年代(1:2),在java7时,还存在一个永久代 - 新生代使用:复制算法 - 老年代使用:标记 - 清除 或者 标记 - 整理 算法 @@ -9992,12 +9971,10 @@ public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制 工作机制: * **对象优先在 Eden 分配**:当创建一个对象的时候,对象会被分配在新生代的 Eden 区,当Eden 区要满了时候,触发 YoungGC - * 当进行 YoungGC 后,此时在 Eden 区存活的对象被移动到 to 区,并且**当前对象的年龄会加1**,清空 Eden 区 - * 当再一次触发 YoungGC 的时候,会把 Eden 区中存活下来的对象和 to 中的对象,移动到 from 区中,这些对象的年龄会加 1,清空 Eden 区和 to 区 - -* to 区永远是空 Survivor 区,from 区是有数据的,每次 MinorGC 后两个区域互换 +* To 区永远是空 Survivor 区,From 区是有数据的,每次 MinorGC 后两个区域互换 +* From 区和 To 区 也可以叫做 S0 区和 S1 区 晋升到老年代: @@ -10148,13 +10125,13 @@ FullGC 同时回收新生代、老年代和方法区,只会存在一个FullGC * 空间分配担保失败 -* JDK 1.7 及以前的永久代空间不足 +* JDK 1.7 及以前的永久代(方法区)空间不足 * Concurrent Mode Failure: 执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC -手动GC测试,VM参数:`-XX:+PrintGcDetails` +手动 GC 测试,VM参数:`-XX:+PrintGcDetails` ```java public void localvarGC1() { @@ -10448,7 +10425,7 @@ Java语言提供了对象终止(finalization)机制来允许开发人员提 * 强引用可以直接访问目标对象 * 虚拟机宁愿抛出OOM异常,也不会回收强引用所指向对象 - * 强引用可能导致**内存泄漏**(引用计数法章节解释了什么是内存泄漏) + * 强引用可能导致**内存泄漏** ```java Object obj = new Object();//使用 new 一个新对象的方式来创建强引用 @@ -10646,7 +10623,7 @@ GC性能指标: - 垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例 - **暂停时间**:执行垃圾收集时,程序的工作线程被暂停的时间 - **收集频率**:相对于应用程序的执行,收集操作发生的频率 -- **内存占用**:Java堆区所占的内存大小 +- **内存占用**:Java 堆区所占的内存大小 - 快速:一个对象从诞生到被回收所经历的时间 **垃圾收集器的组合关系**: @@ -10681,12 +10658,12 @@ GC性能指标: **Serial old**:执行老年代垃圾回收的串行收集器,内存回收算法使用的是**标记-整理算法**,同样也采用了串行回收和"Stop the World"机制, -- Serial old是运行在Client模式下默认的老年代的垃圾回收器 -- Serial old在Server模式下主要有两个用途: +- Serial old 是运行在 Client 模式下默认的老年代的垃圾回收器 +- Serial old 在 Server 模式下主要有两个用途: - 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用 - - 作为老年代CMS收集器的**后备垃圾回收方案**,在并发收集发生 Concurrent Mode Failure 时使用 + - 作为老年代 CMS 收集器的**后备垃圾回收方案**,在并发收集发生 Concurrent Mode Failure 时使用 -开启参数:`-XX:+UseSerialGC == Serial + SerialOld` 等价于新生代用Serial GC且老年代用Serial old GC +开启参数:`-XX:+UseSerialGC` 等价于新生代用 Serial GC 且老年代用 Serial old GC ![](https://gitee.com/seazean/images/raw/master/Java/JVM-Serial收集器.png) @@ -10704,40 +10681,39 @@ GC性能指标: Parallel Scavenge 收集器是应用于新生代的并行垃圾回收器,**采用复制算法**、并行回收和"Stop the World"机制 -Parallel Old收集器:是一个应用于老年代的并行垃圾回收器,**采用标记-整理算法** +Parallel Old 收集器:是一个应用于老年代的并行垃圾回收器,**采用标记-整理算法** 对比其他回收器: * 其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间 -* Parallel目标是达到一个可控制的吞吐量,被称为**吞吐量优先**收集器 -* Parallel Scavenge对比ParNew拥有**自适应调节策略**,可以通过一个开关参数打开GC Ergonomics +* Parallel 目标是达到一个可控制的吞吐量,被称为**吞吐量优先**收集器 +* Parallel Scavenge 对比 ParNew 拥有**自适应调节策略**,可以通过一个开关参数打开 GC Ergonomics 应用场景: * 停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验 * 高吞吐量可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互 -停顿时间和吞吐量的关系:新生代空间变小 -> 缩短停顿时间 -> 垃圾回收变得频繁 -> 导致吞吐量下降 +停顿时间和吞吐量的关系:新生代空间变小 → 缩短停顿时间 → 垃圾回收变得频繁 → 导致吞吐量下降 -在注重吞吐量及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) 参数配置: -* `-XX:+UseAdaptivesizepplicy`:设置Parallel scavenge收集器具有**自适应调节策略**,在这种模式下,年轻代的大小、Eden和Survivor的比例、晋升老年代的对象年龄等参数会被自动调整,**虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量** * `-XX:+UseParallelGC`:手动指定年轻代使用Paralle并行收集器执行内存回收任务 - * `-XX:+UseParalleloldcc`:手动指定老年代使用并行回收收集器执行内存回收任务 * 上面两个参数,默认开启一个,另一个也会被开启(互相激活),默认jdk8是开启的 -* `-XX:ParallelGcrhreads`:设置年轻代并行收集器的线程数。一般最好与CPU数量相等,以避免过多的线程数影响垃圾收集性能 - * 在默认情况下,当CPU数量小于8个,ParallelGcThreads的值等于CPU数量 - * 当CPU数量大于8个,ParallelGCThreads的值等于3+[5*CPU Count]/8] -* `-XX:MaxGCPauseMillis`:设置垃圾收集器最大停顿时间(即STW的时间),单位是毫秒 +* `-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 + * 为了把停顿时间控制在 MaxGCPauseMillis 以内,收集器在工作时会调整 Java 堆大小或其他一些参数 +* `-XX:GCTimeRatio`:垃圾收集时间占总时间的比例 =1/(N+1),用于衡量吞吐量的大小 + * 取值范围(0,100)。默认值 99,也就是垃圾回收时间不超过1 * 与`-xx:MaxGCPauseMillis`参数有一定矛盾性,暂停时间越长,Radio参数就容易超过设定的比例 @@ -10748,15 +10724,17 @@ Parallel Old收集器:是一个应用于老年代的并行垃圾回收器,** #### ParNew -Par是Parallel并行的缩写,New:只能处理的是新生代 +Par 是 Parallel 并行的缩写,New:只能处理的是新生代 **并行垃圾收集器**在串行垃圾收集器的基础之上做了改进,**采用复制算法**,将单线程改为了多线程进行垃圾回收,这样可以缩短垃圾回收的时间 对于其他的行为(收集算法、stop the world、对象分配规则、回收策略等)同Serial收集器一样,应用在年轻代,除Serial外,只有**ParNew GC能与CMS收集器配合工作** -开启参数:`-XX:+UseParNewGC`,表示年轻代使用并行收集器,不影响老年代 +相关参数: -限制线程数量:`-XX:ParallelGCThreads`,默认开启和CPU数据相同的线程数 +* `-XX:+UseParNewGC`,表示年轻代使用并行收集器,不影响老年代 + +* `-XX:ParallelGCThreads`,默认开启和 CPU 数量相同的线程数 ![](https://gitee.com/seazean/images/raw/master/Java/JVM-ParNew收集器.png) @@ -10811,17 +10789,17 @@ Mark Sweep 会造成内存碎片,还不把算法换成 Mark Compact 的原因 * `-XX:CMSInitiatingoccupanyFraction`:设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收 - * JDK5及以前版本的默认值为68,即当老年代的空间使用率达到68%时,会执行一次CMS回收 - * JDK6及以上版本默认值为92% + * JDK5 及以前版本的默认值为 68,即当老年代的空间使用率达到 68% 时,会执行一次CMS回收 + * JDK6 及以上版本默认值为 92% -* `-XX:+UseCMSCompactAtFullCollection`:用于指定在执行完Full GC后对内存空间进行压缩整理,以此避免内存碎片的产生,由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长 +* `-XX:+UseCMSCompactAtFullCollection`:用于指定在执行完 Full GC 后对内存空间进行压缩整理,以此避免内存碎片的产生,由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长 -* `-XX:CMSFullGCsBeforecompaction`:设置在执行多少次Full GC后对内存空间进行压缩整理 +* `-XX:CMSFullGCsBeforecompaction`:设置在执行多少次 Full GC 后对内存空间进行压缩整理 * `-XX:ParallelCMSThreads`:**设置CMS的线程数量** - * CMS默认启动的线程数是(ParallelGCThreads+3)/4,ParallelGCThreads是年轻代并行收集器的线程数 - * 收集线程占用的CPU资源多于25%,对用户程序影响可能较大;当CPU资源比较紧张时,受到CMS收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕 + * CMS 默认启动的线程数是(ParallelGCThreads+3)/4,ParallelGCThreads 是年轻代并行收集器的线程数 + * 收集线程占用的 CPU 资源多于25%,对用户程序影响可能较大;当 CPU 资源比较紧张时,受到 CMS 收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕 @@ -10900,6 +10878,11 @@ G1垃圾收集器的缺点: 卡表(Card Table)在老年代中,是一种对记忆集的具体实现,主要定义了记忆集的记录精度、与堆内存的映射关系等,卡表中的每一个元素都对应着一块特定大小的内存块。这个内存块称之为卡页(card page),当存在跨代引用时,会将卡页标记为 dirty,JVM 对于卡页的维护也是通过写屏障的方式 +收集集合 CSet 代表每次 GC 暂停时回收的一系列目标分区,在任意一次收集暂停中,CSet 所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。年轻代收集 CSet 只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到 CSet 中 + +* CSet of Young Collection +* CSet of Mix Collection + *** @@ -10917,7 +10900,7 @@ G1中提供了三种垃圾回收模式:YoungGC、Mixed GC 和 FullGC,在不 顺时针:Young GC → Young GC + Concurrent Mark → Mixed GC 顺序,进行垃圾回收 -* **Young GC**:发生在年轻代的 GC 算法,一般对象(除了巨型对象)都是在 eden region 中分配内存,当所有 eden region 被耗尽无法申请内存时,就会触发一次 young gc,G1停止应用程序的执行 (Stop-The-World),把活跃对象放入老年代,垃圾对象回收 +* **Young GC**:发生在年轻代的 GC 算法,一般对象(除了巨型对象)都是在 eden region 中分配内存,当所有 eden region 被耗尽无法申请内存时,就会触发一次 young gc,G1 停止应用程序的执行 STW,把活跃对象放入老年代,垃圾对象回收 **回收过程**: @@ -10925,7 +10908,7 @@ G1中提供了三种垃圾回收模式:YoungGC、Mixed GC 和 FullGC,在不 2. 更新 RSet:处理 dirty card queue 更新 RS,此后 RSet 准确的反映对象的引用关系 * dirty card queue:类似缓存,产生了引用先记录在这里,然后更新到 RSet * 作用:产生引用直接更新 RSet 需要线程同步开销很大,使用队列性能好 - 3. 处理 RSet:识别被老年代对象指向的 Eden 中的对象,这些被指向的对象被认为是存活的对象 + 3. 处理 RSet:识别被老年代对象指向的 Eden 中的对象,这些被指向的对象被认为是存活的对象,把需要回收的放入 YoungCSet 中进行回收 4. 复制对象:Eden 区内存段中存活的对象会被复制到 survivor 区,survivor 区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到 old 区中空的内存分段,如果 survivor 空间不够,Eden 空间的部分数据会直接晋升到老年代空间 5. 处理引用:处理 Soft,Weak,Phantom,JNI Weak 等引用,最终 Eden 空间的数据为空,GC 停止工作 @@ -10933,9 +10916,9 @@ G1中提供了三种垃圾回收模式:YoungGC、Mixed GC 和 FullGC,在不 * 初始标记:标记从根节点直接可达的对象,这个阶段是 STW 的,并且会触发一次年轻代 GC * 根区域扫描 (Root Region Scanning):G1 扫描 survivor 区直接可达的老年代区域对象,并标记被引用的对象,这一过程必须在 Young GC 之前完成 - * 并发标记 (Concurrent Marking):在整个堆中进行并发标记(应用程序并发执行),可能被 YoungGC 中断。在并发标记阶段,**若发现区域对象中的所有对象都是垃圾,则这个区域会被立即回收(实时回收)**,同时并发标记过程中,会计算每个区域的对象活性,即区域中存活对象的比例 + * 并发标记 (Concurrent Marking):在整个堆中进行并发标记(应用程序并发执行),可能被 YoungGC 中断。会计算每个区域的对象活性,即区域中存活对象的比例,**若区域中的所有对象都是垃圾,则这个区域会被立即回收(实时回收)**,给浮动垃圾准备出更多的空间,把需要收集的 Region 放入 CSet 当中 * 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这阶段需要停顿线程,但是可并行执行 - * 筛选回收:并发清理阶段,首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率 + * 筛选回收:并发清理阶段,首先对 CSet 中各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率 ![](https://gitee.com/seazean/images/raw/master/Java/JVM-G1收集器.jpg) @@ -10943,7 +10926,7 @@ G1中提供了三种垃圾回收模式:YoungGC、Mixed GC 和 FullGC,在不 注意:**是一部分老年代,而不是全部老年代**,可以选择哪些 old region 收集,对垃圾回收的时间进行控制 - 在G1中,Mixed GC可以通过 `-XX:InitiatingHeapOccupancyPercent` 设置阈值 + 在 G1 中,Mixed GC 可以通过 `-XX:InitiatingHeapOccupancyPercent` 设置阈值 * **Full GC**:对象内存分配速度过快,Mixed GC 来不及回收,导致老年代被填满,就会触发一次 Full GC,G1 的 Full GC 算法就是单线程执行的垃圾回收,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免 Full GC @@ -10960,13 +10943,16 @@ G1中提供了三种垃圾回收模式:YoungGC、Mixed GC 和 FullGC,在不 ##### 相关参数 -- `-XX:+UseG1GC`:手动指定使用G1垃圾收集器执行内存回收任务 -- `-XX:G1HeapRegionSize`:设置每个Region的大小。值是2的幂,范围是1MB到32MB之间,目标是根据最小的Java堆大小划分出约2048个区域,默认是堆内存的1/2000 -- `-XX:MaxGCPauseMillis`:设置期望达到的最大GC停顿时间指标 (JVM会尽力实现,但不保证达到),默认值是200ms -- `-XX:+ParallelGcThread`:设置STW工作线程数的值,最多设置为8 -- `-XX:ConcGCThreads`:设置并发标记线程数,设置为并行垃圾回收线程数(ParallelGcThreads) 的1/4左右 -- `-XX:InitiatingHeapoccupancyPercent`:设置触发并发Mixed GC周期的Java堆占用率阈值,超过此值,就触发GC,默认值是45 +- `-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 区)溢出 @@ -10980,13 +10966,13 @@ G1 的设计原则就是简化 JVM 性能调优,只需要简单的三步即可 1. 开启 G1 垃圾收集器 2. 设置堆的最大内存 -3. 设置最大的停顿时间(stw) +3. 设置最大的停顿时间(STW) **不断调优暂停时间指标**: -* `XX:MaxGCPauseMillis=x` 可以设置启动应用程序暂停的时间,G1在运行的时候会根据这个参数选择CSet来满足响应时间的设置 -* 设置到100ms或者200ms都可以(不同情况下会不一样),但设置成50ms就不太合理 -* 暂停时间设置的太短,就会导致出现G1跟不上垃圾产生的速度,最终退化成 Full GC +* `XX:MaxGCPauseMillis=x` 可以设置启动应用程序暂停的时间,G1会根据这个参数选择 CSet 来满足响应时间的设置 +* 设置到 100ms 或者 200ms 都可以(不同情况下会不一样),但设置成50ms就不太合理 +* 暂停时间设置的太短,就会导致出现 G1 跟不上垃圾产生的速度,最终退化成 Full GC * 对这个参数的调优是一个持续的过程,逐步调整到最佳状态 **不要设置新生代和老年代的大小**: @@ -11007,12 +10993,12 @@ ZGC 收集器是一个可伸缩的、低延迟的垃圾收集器,基于 Region * 在 CMS 和 G1 中都用到了写屏障,而 ZGC 用到了读屏障 * 染色指针:直接将少量额外的信息存储在指针上的技术,从 64 位的指针中拿高 4 位来标识对象此时的状态 * 染色指针可以使某个 Region 的存活对象被移走之后,这个 Region 立即就能够被释放和重用 - * 可以直接从指针中看到引用对象的三色标记状态(Marked0、Marked1)、是否进入了重分配集、是否被移动过 Remapped、是否只能通过 finalize() 方法才能被访问到(Finalizable) + * 可以直接从指针中看到引用对象的三色标记状态(Marked0、Marked1)、是否进入了重分配集、是否被移动过(Remapped)、是否只能通过 finalize() 方法才能被访问到(Finalizable) * 可以大幅减少在垃圾收集过程中内存屏障的使用数量,写屏障的目的通常是为了记录对象引用的变动情况,如果将这些信息直接维护在指针中,显然就可以省去一些专门的记录操作 * 可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据 * 内存多重映射:多个虚拟地址指向同一个物理地址 -可并发的标记压缩算法:染色指针标识对象是否被标记或移动,读屏障保证在每次应用程序或 GC 程序访问对象时先根据染色指针的标识判断是否被移动,如果被移动就访问新的复制对象,并更新引用,不会像 G1 一样必须等待垃圾回收完成才能访问 +可并发的标记压缩算法:染色指针标识对象是否被标记或移动,读屏障保证在每次应用程序或 GC 程序访问对象时先根据染色指针的标识判断是否被移动,如果被移动就根据转发表访问新的复制对象,并更新引用,不会像 G1 一样必须等待垃圾回收完成才能访问 ZGC 目标: @@ -11045,10 +11031,10 @@ ZGC 几乎在所有地方并发执行的,除了初始标记的是 STW 的, #### 总结 -Serial GC、Parallel GC、Concurrent Mark Sweep GC这三个GC不同: +Serial GC、Parallel GC、Concurrent Mark Sweep GC 这三个 GC 不同: -- 最小化地使用内存和并行开销,选Serial GC -- 最大化应用程序的吞吐量,选Parallel GC +- 最小化地使用内存和并行开销,选 Serial GC +- 最大化应用程序的吞吐量,选 Parallel GC - 最小化GC的中断或停顿时间,选CMS GC ![](https://gitee.com/seazean/images/raw/master/Java/JVM-垃圾回收器总结.png) @@ -11061,18 +11047,59 @@ Serial GC、Parallel GC、Concurrent Mark Sweep GC这三个GC不同: -### 日志分析 +### 内存泄漏 + +#### 泄露溢出 + +内存泄露(memory leak)指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果 + +可达性分析算法来判断对象是否是不再使用的对象,本质都是判断一个对象是否还被引用。由于代码的实现不同就会出现很多种内存泄漏问题,让 JVM 误以为此对象还在引用中,无法回收,造成内存泄漏 + +内存溢出(out of memory)指的是申请内存时,没有足够的内存可以使用 + +内存泄漏和内存溢出的关系:内存泄漏的增多,最终会导致内存溢出 + -内存分配与垃圾回收的参数列表:进入 Run/Debug Configurations ---> VM options 设置参 -- `-XX:+PrintGC`:输出GC日志,类似:-verbose:gc -- `-XX:+PrintGcDetails`:输出GC的详细日志 -- `-XX:+PrintGcTimestamps`:输出GC的时间戳(以基准时间的形式) -- `-XX:+PrintGCDatestamps`:输出GC的时间戳(以日期的形式,如2013-05-04T21:53:59.234+0800) -- `-XX:+PrintHeapAtGC`:在进行GC的前后打印出堆的信息 -- `-Xloggc:../logs/gc.1og`:日志文件的输出路径 +*** + + + +#### 几种情况 + +##### 静态集合 + +静态集合类的生命周期与JVM程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。原因是长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收 + +```java +public class MemoryLeak { + static List list = new ArrayList(); + public void oomTests(){ + Object obj=new Object();//局部变量 + list.add(obj); + } +} +``` + + + +*** + +##### 单例模式 + +单例模式和静态集合导致内存泄露的原因类似,因为单例的静态特性,它的生命周期和 JVM 的生命周期一样长,所以如果单例对象持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏 + + + +**** + + + +##### 内部类 + +内部类持有外部类的情况,如果一个外部类的实例对象调用方法返回了一个内部类的实例对象,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象也不会被回收,造成内存泄漏 @@ -11080,26 +11107,149 @@ Serial GC、Parallel GC、Concurrent Mark Sweep GC这三个GC不同: +##### 连接相关 + +数据库连接、网络连接和IO连接等,当不再使用时,需要显式调用 close 方法来释放与连接,垃圾回收器才会回收对应的对象,否则将会造成大量的对象无法被回收,从而引起内存泄漏 + + + +**** + + + +##### 不合理域 + +变量不合理的作用域,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏;如果没有及时地把对象设置为 null,也有可能导致内存泄漏的发生 + +```java +public class UsingRandom { + private String msg; + public void receiveMsg(){ + msg = readFromNet();//从网络中接受数据保存到msg中 + saveDB(msg);//把msg保存到数据库中 + } +} +``` + +通过 readFromNet 方法把接收消息保存在 msg 中,然后调用 saveDB 方法把内容保存到数据库中,此时 msg 已经可以被回收,但是 msg 的生命周期与对象的生命周期相同,造成 msg 不能回收,产生内存泄漏 + +解决: + +* msg 变量可以放在 receiveMsg 方法内部,当方法使用完,msg 的生命周期也就结束,就可以被回收了 +* 在使用完 msg 后,把 msg 设置为 null,这样垃圾回收器也会回收 msg 的内存空间。 + + + +**** + + + +##### 改变哈希 + +当一个对象被存储进 HashSet 集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段,否则对象修改后的哈希值与最初存储进 HashSet 集合中时的哈希值不同,这种情况下使用该对象的当前引用作为的参数去 HashSet 集合中检索对象返回 false,导致无法从 HashSet 集合中单独删除当前对象,造成内存泄漏 + + + +*** + + + +##### 缓存泄露 + +内存泄漏的一个常见来源是缓存,一旦把对象引用放入到缓存中,就会很容易被遗忘 + +使用 WeakHashMap 代表缓存,当除了自身有对 key 的引用外没有其他引用,map 会自动丢弃此值 + + + +*** + + + +##### 监听器 + +监听器和其他回调情况,假如客户端在实现的 API 中注册回调,却没有显式的取消,那么就会一直积聚下去,所以确保回调立即被当作垃圾回收的最佳方法是只保存它的弱引用,比如保存为 WeakHashMap 中的键 + + + +*** + + + +#### 案例分析 + +```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]; + } + + public void push(Object e) { //入栈 + ensureCapacity(); + elements[size++] = e; + } + + public Object pop() { //出栈 + if (size == 0) + throw new EmptyStackException(); + return elements[--size]; + } + + private void ensureCapacity() { + if (elements.length == size) + elements = Arrays.copyOf(elements, 2 * size + 1); + } +} +``` + +程序并没有明显错误,但 pop 函数存在内存泄漏问题,因为 pop 函数只是把栈顶索引下移一位,并没有把上一个出栈索引处的引用置空,导致栈数组一直强引用着已经出栈的对象 + +解决方法: + +```java +public Object pop() { + if (size == 0) + throw new EmptyStackException(); + Object result = elements[--size]; + elements[size] = null; + return result; +} +``` + + + + + +*** + + + + + ## 类加载 ### 对象结构 -#### 基本构造 +#### 存储构造 一个 Java 对象内存中存储为三部分:对象头(Header)、实例数据(Instance Data)和对齐填充 (Padding) 对象头: -* 普通对象(32位系统,64位128位):分为两部分 +* 普通对象:分为两部分 - * Mark Word:用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,就是Mark Word + * 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 + * Klass Word:类型指针,对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;在64位系统中,开启指针压缩(-XX:+UseCompressedOops)或者 JVM 堆的最大值小于 32G,这个指针也是 4byte,否则是 8byte(就是 Java 中的一个引用的大小) ```ruby |-----------------------------------------------------| @@ -11118,17 +11268,19 @@ Serial GC、Parallel GC、Concurrent Mark Sweep GC这三个GC不同: | Mark Word(32bits) | Klass Word(32bits) | array length(32bits) | |-----------------------|-----------------------------|-------------------------| ``` - - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-对象头结构.png) -实例数据:实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来 +实例数据:实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的,都需要记录起来 -对齐填充:Padding 起占位符的作用。64位系统,由于 HotSpot VM 的自动内存管理系统要求对象起始地址必须是8字节的整数倍,就是对象的大小必须是8字节的整数倍,而对象头部分正好是8字节的倍数(1倍或者2倍),因此当对象实例数据部分没有对齐时,就需要通过对齐填充来补全 +对齐填充:Padding 起占位符的作用。64 位系统,由于 HotSpot VM 的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,就是对象的大小必须是 8 字节的整数倍,而对象头部分正好是 8 字节的倍数(1倍或者2倍),因此当对象实例数据部分没有对齐时,就需要通过对齐填充来补全 -32位系统 +32位系统: * 一个 int 在 java 中占据 4byte,所以 Integer 的大小为: + ```java + private final int value; + ``` + ```ruby # 需要补位4byte 4(Mark Word) + 4(Klass Word) + 4(data) + 4(Padding) = 16byte @@ -11147,6 +11299,50 @@ Serial GC、Parallel GC、Concurrent Mark Sweep GC这三个GC不同: +#### 实际大小 + +浅堆(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; +``` + +保留集(Retained Set):对象 A 的保留集指当对象 A 被垃圾回收后,可以被释放的所有的对象集合(包括 A 本身),所以对象 A 的保留集就是只能通过对象 A 被直接或间接访问到的所有对象的集合,就是仅被对象 A 所持有的对象的集合 + +深堆(Retained Heap):指对象的保留集中所有的对象的浅堆大小之和,一个对象的深堆指只能通过该对象访问到的(直接或间接)所有对象的浅堆之和,即对象被回收后,可以释放的真实空间 + +对象的实际大小:一个对象所能触及的所有对象的浅堆大小之和,也就是通常意义上我们说的对象大小 + +下图显示了一个简单的对象引用关系图,对象 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 提供了一种叫支配树的对象图,体现了对象实例间的支配关系 + +基本性质: + +- 对象 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 的直接支配者 + + + +*** + + + #### 节约内存 * 尽量使用基本类型 @@ -11162,7 +11358,7 @@ Serial GC、Parallel GC、Concurrent Mark Sweep GC这三个GC不同: private int size; ``` - Mark Word 占 4byte,Klass Word 占 4byte,一个int字段(size)占 4byte,elementData 数组本身占 12(4+4+4),数组中 10 个 Integer 对象占 10×16,所以整个集合空间大小为 184byte + Mark Word 占 4byte,Klass Word 占 4byte,一个 int 字段占 4byte,elementData 数组占 12(4+4+4),数组中 10 个 Integer 对象占 10×16,所以整个集合空间大小为 184byte(深堆) * 时间用 long/int 表示,不用 Date 或者 String @@ -11174,7 +11370,7 @@ Serial GC、Parallel GC、Concurrent Mark Sweep GC这三个GC不同: #### 对象访问 -JVM是通过栈帧中的对象引用访问到其内部的对象实例: +JVM 是通过栈帧中的对象引用访问到其内部的对象实例: * 句柄访问 使用该方式,Java 堆中会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息 @@ -12083,6 +12279,8 @@ public static void main(String[] args) { + + ## 运行机制 ### 执行过程 @@ -12149,8 +12347,8 @@ Java 语言:跨平台的语言(write once ,run anywhere) 指令集:不同的硬件平台支持的指令是有区别的,每个平台所支持的指令,称之为对应平台的指令集 -- x86指令集,对应的是x86架构的平台 -- ARM指令集,对应的是ARM架构的平台 +- x86 指令集,对应的是 x86 架构的平台 +- ARM 指令集,对应的是 ARM 架构的平台 汇编语言:用助记符代替机器指令的操作码,用地址符号或标号代替指令或操作数的地址 @@ -12297,19 +12495,19 @@ constant_pool 是一种表结构,以1 ~ constant_pool_count - 1为索引,表 * 描述符:用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值 - | 标志符 | 含义 | - | ------ | ---------------------------------------------------- | - | 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` | + | 标志符 | 含义 | + | ------ | --------------------------------------------------------- | + | 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` | 常量类型和结构: @@ -12543,7 +12741,7 @@ attributes[](属性表):属性表的每个项的值必须是 attribute_inf -#### javap +#### 编译指令 ##### javac @@ -12573,7 +12771,7 @@ javap 反编译生成的字节码文件,根据 class 字节码文件,反解 #常用的以下三个 -v -verbose 输出附加信息 -l 输出行号和本地变量表 --c 对代码进行反汇编 +-c 对代码进行反汇编 #反编译 -s 输出内部类型签名 -sysinfo 显示正在处理的类的系统信息 (路径, 大小, 日期, MD5 散列) @@ -12682,7 +12880,16 @@ 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 //存入操作数栈 + 5 iinc 1 by 1 //自增i++ + 8 istore_3 //把操作数栈没有自增的数据的存入局部变量表 + 9 iinc 2 by 1 //++i +12 iload_2 //加载到操作数栈 +13 istore 4 //存入局部变量表,这个存入没有 _ 符号,_只能到3 +``` ```java public class Demo { @@ -13082,7 +13289,14 @@ finally 中的代码被**复制了 3 份**,分别放入 try 流程,catch 流 原始 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); }} +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); + } +} ``` javap -v Demo.class:省略 @@ -13183,7 +13397,7 @@ HostSpot JVM的默认执行方式: * 当程序启动后,解释器可以马上发挥作用立即执行,省去编译器编译的时间(解释器存在的**必要性**) * 随着程序运行时间的推移,即时编译器逐渐发挥作用,根据热点探测功能,将有价值的字节码编译为本地机器指令,以换取更高的程序执行效率 -HotSpot VM 可以通过VM参数设置程序执行方式: +HotSpot VM 可以通过 VM 参数设置程序执行方式: - -Xint:完全采用解释器模式执行程序 - -Xcomp:完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行 @@ -13228,7 +13442,7 @@ HotSpot VM 采用的热点探测方式是基于计数器的热点探测,为每 HotSpot VM 内嵌两个 JIT 编译器,分别为 Client Compiler 和 Server Compiler,简称 C1 编译器和 C2 编译器 -C1编译器会对字节码进行简单可靠的优化,耗时短,以达到更快的编译速度,C1编译器的优化方法: +C1 编译器会对字节码进行简单可靠的优化,耗时短,以达到更快的编译速度,C1 编译器的优化方法: * 方法内联:**将引用的函数代码编译到引用点处**,这样可以减少栈帧的生成,减少参数传递以及跳转过程 @@ -13255,14 +13469,14 @@ C1编译器会对字节码进行简单可靠的优化,耗时短,以达到更 * 冗余消除:根据运行时状况进行代码折叠或削除 -* 内联缓存:是一种加快动态绑定的优化技术 +* 内联缓存:是一种加快动态绑定的优化技术(方法调用部分详解) C2 编译器进行耗时较长的优化以及激进优化,优化的代码执行效率更高,当激进优化的假设不成立时,再退回使用 C1 编译。C2 的优化主要是在全局层面,逃逸分析是优化的基础:标量替换、栈上分配、同步消除 VM 参数设置: -- -client:指定 Java 虚拟机运行在 Client 模式下,并使用C1编译器 -- -server:指定 Java 虚拟机运行在 Server 模式下,并使用C2编译器 +- -client:指定 Java 虚拟机运行在 Client 模式下,并使用 C1 编译器 +- -server:指定 Java 虚拟机运行在 Server 模式下,并使用 C2 编译器 - `-server -XX:+TieredCompilation`:在1.8之前,分层编译默认是关闭的,可以添加该参数开启 分层编译策略 (Tiered Compilation):程序解释执行可以触发 C1 编译,将字节码编译成机器码,加上性能监控,C2 编译会根据性能监控信息进行激进优化,JVM 将执行状态分成了 5 个层次: @@ -14158,20 +14372,60 @@ public class GeneratedMethodAccessor1 extends MethodAccessorImpl { + + ## JVM调优 -### 服务器性能 +### 性能调优 + +#### 性能指标 + +性能指标主要是吞吐量、响应时间、QPS、TPS 等、并发用户数等,而这些性能指标又依赖于系统服务器的资源,如 CPU、内存、磁盘IO、网络IO 等,对于这些指标数据的收集,通常可以根据Java本身的工具或指令进行查询 + +几个重要的指标: + +1. 停顿时间(响应时间):提交请求和返回该请求的响应之间使用的时间,比如垃圾回收中 STW 的时间 +2. 吞吐量:对单位时间内完成的工作量(请求)的量度(可以对比 GC 的性能指标) +3. 并发数:同一时刻,对服务器有实际交互的请求数 +4. QPS:Queries Per Second,每秒处理的查询量 +5. TPS:Transactions Per Second,每秒产生的事务数 +6. 内存占用:Java堆区所占的内存大小 + + + +*** + + + +#### 优化步骤 + +对于一个系统要部署上线时,则一定会对 JVM 进行调整,不经过任何调整直接上线,容易出现线上系统频繁 FullGC 造成系统卡顿、CPU 使用频率过高、系统无反应等问题 + +1. 性能监控:通过运行日志、堆栈信息、线程快照等信息监控是否出现 GC 频繁、OOM、内存泄漏、死锁、响应时间过长等情况 + +2. 性能分析: + + * 打印 GC 日志,通过 GCviewe r或者 http://gceasy.io 来分析异常信息 + + - 运用命令行工具、jstack、jmap、jinfo等 + + - dump 出堆文件,使用内存分析工具分析文件 -(调优部分笔记待优化) + - 使用阿里 Arthas、jconsole、JVisualVM 来实时查看 JVM 状态 -对于一个系统要部署上线时,则一定会对JVM进行调整,不经过任何调整直接上线,容易出现线上系统频繁FullGC造成系统卡顿、CPU使用频率过高、系统无反应等问题 + - jstack 查看堆栈信息 -对于一个应用来说通常重点关注的性能指标主要是吞吐量、响应时间、QPS、TPS等、并发用户数等,而这些性能指标又依赖于系统服务器的资源,如:CPU、内存、磁盘IO、网络IO等。对于这些指标数据的收集,通常可以根据Java本身的工具或指令进行查询 +3. 性能调优: -JDK 自带了**监控工具,位于 JDK 的 bin 目录下**,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具: + * 适当增加内存,根据业务背景选择垃圾回收器 -* jconsole:用于对 JVM 中的内存、线程和类等进行监控; -* jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等 + - 优化代码,控制内存使用 + + - 增加机器,分散节点压力 + + - 合理设置线程池线程数量 + + - 使用中间件提高程序效率,比如缓存、消息队列等 @@ -14179,7 +14433,7 @@ JDK 自带了**监控工具,位于 JDK 的 bin 目录下**,其中最常用 -### 参数调优 +#### 参数调优 对于 JVM 调优,主要就是调整年轻代、老年代、元空间的内存空间大小及使用的垃圾回收器类型 @@ -14190,31 +14444,611 @@ JDK 自带了**监控工具,位于 JDK 的 bin 目录下**,其中最常用 -Xmx:设置堆的最大大小 ``` -* 设置年轻代中Eden区和两个Survivor区的大小比例。该值如果不设置,则默认比例为8:1:1。Java官方通过增大Eden区的大小,来减少YGC发生的次数,但有时我们发现,虽然次数减少了,但Eden区满的时候,由于占用的空间较大,导致释放缓慢,此时STW的时间较长,因此需要按照程序情况去调优 +* 设置年轻代中 Eden 区和两个 Survivor 区的大小比例。该值如果不设置,则默认比例为 8:1:1。Java 官方通过增大 Eden 区的大小,来减少 YGC 发生的次数,虽然次数减少了,但 Eden 区满的时候,由于占用的空间较大,导致释放缓慢,此时 STW 的时间较长,因此需要按照程序情况去调优 ```sh -XX:SurvivorRatio ``` -* 年轻代和老年代默认比例为1:2。可以通过调整二者空间大小比率来设置两者的大小。 +* 年轻代和老年代默认比例为 1:2,可以通过调整二者空间大小比率来设置两者的大小。 ```sh -XX:newSize 设置年轻代的初始大小 -XX:MaxNewSize 设置年轻代的最大大小, 初始大小和最大大小两个值通常相同 ``` -* 线程堆栈的设置:**每个线程默认会开启1M的堆栈**,用于存放栈帧、调用参数、局部变量等,但一般256K就够用,通常减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统 +* 线程堆栈的设置:**每个线程默认会开启 1M 的堆栈**,用于存放栈帧、调用参数、局部变量等,但一般 256K 就够用,通常减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统 ```sh -Xss 对每个线程stack大小的调整,-Xss128k ``` -* 一般一天超过一次FullGC就是有问题,首先通过工具查看是否出现内存泄露,如果出现内存泄露则调整代码,没有的话则调整JVM参数 -* 系统CPU持续飙高的话,首先先排查代码问题,如果代码没问题,则咨询运维或者云服务器供应商,通常服务器重启或者服务器迁移即可解决 +* 一般一天超过一次 FullGC 就是有问题,首先通过工具查看是否出现内存泄露,如果出现内存泄露则调整代码,没有的话则调整 JVM 参数 + +* 系统 CPU 持续飙高的话,首先先排查代码问题,如果代码没问题,则咨询运维或者云服务器供应商,通常服务器重启或者服务器迁移即可解决 + * 如果数据查询性能很低下的话,如果系统并发量并没有多少,则应更加关注数据库的相关问题 -* 如果服务器配置还不错,JDK8开始尽量使用G1或者新生代和老年代组合使用并行垃圾回收器 + +* 如果服务器配置还不错,JDK8 开始尽量使用 G1 或者新生代和老年代组合使用并行垃圾回收器 + + + + + +**** + + + + + +### 命令行篇 + +#### 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 -
``` -2. 编写业务层与表现层(模拟)接口与实现类—service.UserService,service.impl.UserServiceImpl +2. 编写业务层与表现层(模拟)接口与实现类 service.UserService,service.impl.UserServiceImpl ```java public interface UserService { @@ -2752,7 +2758,7 @@ Spring优点: 3. 建立spring配置文件:resources.**applicationContext**.xml (名字一般使用该格式) -4. 配置所需资源(Service)为spring控制的资源 +4. 配置所需资源(Service)为 spring 控制的资源 ```xml @@ -2793,9 +2799,9 @@ Spring优点: ##### 基本属性 -标签:标签,的子标签 +标签: 标签, 的子标签 -作用:定义spring中的资源,受此标签定义的资源将受到spring控制 +作用:定义 Spring 中的资源,受此标签定义的资源将受到 Spring 控制 格式: @@ -2807,9 +2813,9 @@ Spring优点: 基本属性 -* id:bean的名称,通过id值获取bean (首字母小写) -* class:bean的类型,使用完全限定类名 -* name:bean的名称,可以通过name值获取bean,用于多人配合时给bean起别名 +* id:bean 的名称,通过id值获取bean (首字母小写) +* class:bean 的类型,使用完全限定类名 +* name:bean 的名称,可以通过 name值获取bean,用于多人配合时给bean起别名 ```xml @@ -2823,7 +2829,7 @@ ctx.getBean("beanId") == ctx.getBean("beanName1") == ctx.getBean("beanName2") ##### 作用范围 -作用:定义bean的作用范围 +作用:定义 bean 的作用范围 格式: @@ -2833,9 +2839,9 @@ ctx.getBean("beanId") == ctx.getBean("beanName1") == ctx.getBean("beanName2") 取值: -- singleton:设定创建出的对象保存在spring容器中,是一个单例的对象 -- prototype:设定创建出的对象保存在spring容器中,是一个非单例的对象 -- request、session、application、 websocket :设定创建出的对象放置在web容器对应的位置 +- singleton:设定创建出的对象保存在 Spring 容器中,是一个单例的对象 +- prototype:设定创建出的对象保存在 Spring 容器中,是一个非单例的对象 +- request、session、application、 websocket :设定创建出的对象放置在 web 容器对应的位置 @@ -2845,7 +2851,7 @@ ctx.getBean("beanId") == ctx.getBean("beanName1") == ctx.getBean("beanName2") ##### 生命周期 -作用:定义bean对象在初始化或销毁时完成的工作 +作用:定义 bean 对象在初始化或销毁时完成的工作 格式: @@ -2853,14 +2859,14 @@ ctx.getBean("beanId") == ctx.getBean("beanName1") == ctx.getBean("beanName2") ``` - 取值:工厂bean中用于获取对象的静态方法名 + 取值:工厂 bean 中用于获取对象的静态方法名 - 注意事项:class属性必须配置成静态工厂的类名 + 注意事项:class 属性必须配置成静态工厂的类名 bean配置: @@ -2947,7 +2953,7 @@ UserService userService = (UserService)ctx.getBean("userService3"); * factory-bean,factory-method - 作用:定义bean对象创建方式,使用实例工厂的形式创建bean,兼容早期遗留系统的升级工作 + 作用:定义 bean 对象创建方式,使用实例工厂的形式创建 bean,兼容早期遗留系统的升级工作 格式: @@ -2959,9 +2965,9 @@ UserService userService = (UserService)ctx.getBean("userService3"); 注意事项: - - 使用实例工厂创建bean首先需要将实例工厂配置bean,交由spring进行管理 + - 使用实例工厂创建 bean 首先需要将实例工厂配置 bean,交由 Spring 进行管理 - - factory-bean是实例工厂的beanId + - factory-bean 是实例工厂的 beanId bean配置: @@ -3012,13 +3018,13 @@ ApplicationContext子类相关API: ##### 依赖注入 -- IoC(Inversion Of Control)控制翻转,Spring反向控制应用程序所需要使用的外部资源 +- IoC(Inversion Of Control)控制翻转,Spring 反向控制应用程序所需要使用的外部资源 -- DI(Dependency Injection)依赖注入,应用程序运行依赖的资源由Spring为其提供,资源进入应用程序的方式称为注入。简单说就是利用反射机制为类的属性赋值的操作 +- DI(Dependency Injection)依赖注入,应用程序运行依赖的资源由 Spring 为其提供,资源进入应用程序的方式称为注入。简单说就是利用反射机制为类的属性赋值的操作 ![](https://gitee.com/seazean/images/raw/master/Frame/DI介绍.png) -IoC 和 DI 的关系:IoC 与 DI是同一件事站在不同角度看待问题 +IoC 和 DI 的关系:IoC 与 DI 是同一件事站在不同角度看待问题 @@ -3028,9 +3034,9 @@ IoC 和 DI 的关系:IoC 与 DI是同一件事站在不同角度看待问题 ##### set注入 -标签:标签,的子标签 +标签: 标签, 的子标签 -作用:使用set方法的形式为bean提供资源 +作用:使用 set 方法的形式为 bean 提供资源 格式: @@ -3044,10 +3050,10 @@ IoC 和 DI 的关系:IoC 与 DI是同一件事站在不同角度看待问题 基本属性: -* name:对应bean中的属性名,要注入的变量名,要求该属性必须提供可访问的set方法 - (严格规范此名称是set方法对应名称,首字母必须小写) -* value:设定非引用类型属性对应的值,不能与ref同时使用 -* ref:设定引用类型属性对应bean的id ,不能与value同时使用 +* name:对应 bean 中的属性名,要注入的变量名,要求该属性必须提供可访问的 set 方法 + (严格规范此名称是 set 方法对应名称,首字母必须小写) +* value:设定非引用类型属性对应的值,不能与 ref 同时使用 +* ref:设定引用类型属性对应 bean 的 id ,不能与 value 同时使用 ```xml @@ -3352,7 +3358,7 @@ IoC 和 DI 的关系:IoC 与 DI是同一件事站在不同角度看待问题 标签: -作用:为bean注入属性值 +作用:为 bean 注入属性值 格式: @@ -3360,7 +3366,7 @@ IoC 和 DI 的关系:IoC 与 DI是同一件事站在不同角度看待问题 ``` -开启p命令空间:开启spring对p命令空间的的支持,在beans标签中添加对应空间支持 +开启p命令空间:开启Spring对p命令空间的的支持,在beans标签中添加对应空间支持 ```xml 标签配置 + - XML 文件中使用 标签配置 - - 使用@Component及衍生注解配置 + - 使用 @Component 及衍生注解配置 -- **快速高效导入大量bean的方式,替代@Import({a.class,b.class}),无需在每个类上添加@Bean** +- **快速高效导入大量 bean 的方式,替代 @Import({a.class,b.class}),无需在每个类上添加 @Bean** 名称: ImportSelector @@ -7520,9 +7535,9 @@ ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext. -### 循环依赖 +### 依赖 -解决循环依赖:提前引用,提前暴露创建中的Bean +解决循环依赖:提前引用,提前暴露创建中的 Bean * 循环依赖的三级缓存: diff --git a/Tool.md b/Tool.md index e170efc..abb3ea0 100644 --- a/Tool.md +++ b/Tool.md @@ -2208,7 +2208,7 @@ pstree -A #查看所有进程树 父进程 ID 为 0 的进程通常是内核进程,它们作为系统自举过程的一部分而启动,init 进程是个例外,它的父进程是0,但它是用户进程 -主存 = RAM + BIOS部分的 ROM +主存 = RAM + BIOS 部分的 ROM 自举程序存储在内存中 ROM(BIOS 芯片),用来加载操作系统。CPU 的程序计数器指向 ROM 中自举程序第一条指令所对应的位置,当计算机通电,CPU 开始读取并执行自举程序,将操作系统(不是全部,只是启动计算机的那部分程序)装入RAM中,这个过程是自举过程。装入完成后 CPU 的程序计数器就被设置为 RAM 中操作系统的第一条指令所对应的位置,接下来 CPU 将开始执行操作系统的指令 @@ -2331,9 +2331,9 @@ pid_t waitpid(pid_t pid, int *status, int options) ### ifconfig -ifconfig是Linux中用于显示或配置网络设备的命令,英文全称是network interfaces configuring +ifconfig 是 Linux 中用于显示或配置网络设备的命令,英文全称是 network interfaces configuring -ifconfig命令用于显示或设置网络设备。ifconfig可设置网络设备的状态,或是显示目前的设置。 +ifconfig 命令用于显示或设置网络设备。ifconfig 可设置网络设备的状态,或是显示目前的设置 ```sh ifconfig [网络设备][down up -allmulti -arp -promisc][add<地址>][del<地址>][<硬件地址>][io_addr][irq][media<网络媒介类型>][mem_start<内存地址>][metric<数目>][mtu<字节>][netmask<子网掩码>][tunnel<地址>][-broadcast<地址>][-pointopoint<地址>][IP地址] @@ -2343,7 +2343,7 @@ ifconfig [网络设备][down up -allmulti -arp -promisc][add<地址>][del<地址 **ens33(有的是eth0)**表示第一块网卡。 - 表示ens33网卡的 IP地址是 192.168.0.137,广播地址,broadcast 192.168.0.255,掩码地址netmask:255.255.255.0 ,inet6对应的是ipv6 + 表示 ens33 网卡的 IP地址是 192.168.0.137,广播地址,broadcast 192.168.0.255,掩码地址netmask:255.255.255.0 ,inet6 对应的是ipv6 **lo** 是表示主机的回坏地址,这个一般是用来测试一个网络程序,但又不想让局域网或外网的用户能够查看,只能在此台主机上运行和查看所用的网络接口 @@ -2358,9 +2358,9 @@ ifconfig [网络设备][down up -allmulti -arp -promisc][add<地址>][del<地址 ### ping -ping命令用于检测主机。 +ping 命令用于检测主机。 -执行ping指令会使用ICMP传输协议,发出要求回应的信息,若远端主机的网络功能没有问题,就会回应该信息,因而得知该主机运作正常。 +执行 ping 指令会使用 ICMP 传输协议,发出要求回应的信息,若远端主机的网络功能没有问题,就会回应该信息,因而得知该主机运作正常。 ```shell ping [-dfnqrRv][-c<完成次数>][-i<间隔秒数>][-I<网络界面>][-l<前置载入>][-p<范本样式>][-s<数据包大小>][-t<存活数值>][主机名称或IP地址] @@ -2371,9 +2371,9 @@ ping [-dfnqrRv][-c<完成次数>][-i<间隔秒数>][-I<网络界面>][-l<前置 * `ping -c 2 www.baidu.com` ![](https://gitee.com/seazean/images/raw/master/Tool/ping百度.png) - icmp_seq:ping序列,从1开始 + icmp_seq:ping 序列,从1开始 - ttl:IP生存时间值 + ttl:IP 生存时间值 time:响应时间,数值越小,联通速度越快 @@ -2385,7 +2385,7 @@ ping [-dfnqrRv][-c<完成次数>][-i<间隔秒数>][-I<网络界面>][-l<前置 ### netstat -netstat命令用于显示网络状态 +netstat 命令用于显示网络状态 ```sh netstat [-acCeFghilMnNoprstuvVwx][-A<网络类型>][--ip] From d033bab97bad2b30986018d1f690f863a0e1a585 Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 22 Jul 2021 23:50:37 +0800 Subject: [PATCH 074/242] Update Java Notes --- Java.md | 221 +++++++++++++++++------------------ Prog.md | 4 +- SSM.md | 355 ++++++++++++++++++++++++++++++++++++++------------------ 3 files changed, 350 insertions(+), 230 deletions(-) diff --git a/Java.md b/Java.md index e5eff5d..b214d04 100644 --- a/Java.md +++ b/Java.md @@ -4916,10 +4916,19 @@ HashMap继承关系如下图所示: ```java final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //。。。。。。。。。。。。。。 - if ((p = tab[i = (n - 1) & hash]) == null)//这里的n表示数组长度16 - //。。。。。。。。。。。。。。 + 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`:计算下标位置 @@ -6930,41 +6939,31 @@ public static void searchFiles(File dir , String fileName){ ### Character -字符集:各个国家为自己国家的字符取的一套编号规则 +字符集:为字符编制的一套编号规则 -计算机的底层是不能直接存储字符的,只能存储二进制,010101。 +计算机的底层是不能直接存储字符的,只能存储二进制,010101 -美国人: - 8个开关一组就可以编码字符,1个字节。 - 2^8 = 256 - 一个字节存储一个字符完全够用了。 +ASCII 编码:8 个开关一组就可以编码字符,1 个字节 2^8 = 256, 一个字节存储一个字符完全够用,英文和数字在底层存储都是采用 1 个字节存储的 -​ a 97 -​ b 98 +``` +a 97 +b 98 -​ A 65 -​ B 66 +A 65 +B 66 -​ 0 48 -​ 1 49 -​ 这套编码是ASCII编码。 -​ 英文和数字在底层存储的时候都是采用1个字节存储的。 +0 48 +1 49 +``` -中国人: - 中国人的字符很多:9万左右字符。 - 2个字节编码一个中文字符,1个字节编码一个英文字符。 - 这套编码叫:GBK编码。 - 它也必须兼容ASCII编码表。 +中国人:中国人有 9 万左右字符,2 个字节编码一个中文字符,1 个字节编码一个英文字符,这套编码叫:GBK 编码,兼容 ASCII 编码表 -美国人: - 我来收集全球所有的字符,统一编号。这套编码叫 Unicode编码(万国码) - 一个英文等于两个字节,一个中文(含繁体)等于两个字节。中文标点占两个字节,英文标点占两个字节 +美国人:收集全球所有的字符,统一编号,这套编码叫 Unicode编码(万国码),一个英文等于两个字节,一个中文(含繁体)等于两个字节,中文标点占两个字节,英文标点占两个字节 -​ UTF-8就是变种形式,它也必须兼容ASCII编码表。 -​ UTF-8一个中文一般占3个字节,中文标点占3个。英文字母和数字1个字节 -​ +* UTF-8 是变种形式,也必须兼容ASCII编码表 +* UTF-8一个中文一般占 3 个字节,中文标点占 3 个,英文字母和数字 1 个字节 -编码前与编码后的编码集必须一致才不会乱码!! +编码前与编码后的编码集必须一致才不会乱码 @@ -7098,7 +7097,7 @@ System.out.println(rs); ##### 字节输出 -FileOutputStream文件字节输出流: +FileOutputStream 文件字节输出流: * 作用:以内存为基准,把内存中的数据,按照字节的形式写出到磁盘文件中去 @@ -7113,8 +7112,6 @@ FileOutputStream文件字节输出流: `public void write(byte[] buffer , int pos , int len)` : 写一个字节数组的一部分出去 参数一,字节数组;参数二:起始字节索引位置,参数三:写多少个字节数出去。 - - * FileOutputStream字节输出流每次启动写数据的时候都会先清空之前的全部数据,重新写入: `OutputStream os = new FileOutputStream("Day09Demo/out05")` : 覆盖数据管道 `OutputStream os = new FileOutputStream("Day09Demo/out05" , true)` : 追加数据的管道 @@ -7459,12 +7456,13 @@ public static void main(String[] args) throws Exception { ##### 乱码问题 -``` 字符流读取: - 代码编码 文件编码 中文情况。 - UTF-8 UTF-8 不乱码! - GBK GBK 不乱码! - UTF-8 GBK 乱码! + +``` +代码编码 文件编码 中文情况。 +UTF-8 UTF-8 不乱码! +GBK GBK 不乱码! +UTF-8 GBK 乱码! ``` 如果代码编码和读取的文件编码一致。字符流读取的时候不会乱码。 @@ -7472,15 +7470,19 @@ public static void main(String[] args) throws Exception { +*** + + + ##### 字符输入转换流 -字符输入转换流InputStreamReader +字符输入转换流:InputStreamReader 作用:解决字符流读取不同编码的乱码问题,把原始的**字节流**按照默认的编码或指定的编码**转换成字符输入流** 构造器: -* `public InputStreamReader(InputStream is)` : 使用当前代码默认编码(UTF-8)转换成字符流 +* `public InputStreamReader(InputStream is)` : 使用当前代码默认编码 UTF-8 转换成字符流 * `public InputStreamReader(InputStream is,String charset)` : 指定编码把字节流转换成字符流 ```java @@ -7511,7 +7513,7 @@ public class InputStreamReaderDemo{ 构造器: -* `public OutputStreamWriter(OutputStream os)` : 用默认编码UTF-8把字节输出流转换成字符输出流 +* `public OutputStreamWriter(OutputStream os)` : 用默认编码 UTF-8 把字节输出流转换成字符输出流 * `public OutputStreamWriter(OutputStream os ,String charset)` : 指定编码把字节输出流转换成 ```Java @@ -7529,7 +7531,7 @@ osw.close(); #### 序列化 -##### 介绍 +##### 基本介绍 对象序列化:把Java对象转换成字节序列的过程,将对象写入到IO流中。 对象 => 文件中 @@ -7539,6 +7541,10 @@ transient 关键字修饰的成员变量,将不参与序列化! +*** + + + ##### 序列化 对象序列化流(对象字节输出流):ObjectOutputStream @@ -7772,7 +7778,7 @@ public class PropertiesDemo02 { ### RandomIO -RandomAccessFile类:该类的实例支持读取和写入随机访问文件 +RandomAccessFile 类:该类的实例支持读取和写入随机访问文件 构造器: RandomAccessFile(File file, String mode):创建随机访问文件流,从File参数指定的文件读取,可选择写入 @@ -8322,8 +8328,8 @@ public class ReflectDemo { 注解:类的组成部分,可以给类携带一些额外的信息,提供一种安全的类似注释标记的机制,用来将任何信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联 -* 注解是JDK1.5的新特性 -* 注解是给编译器或JVM看的,编译器或JVM可以根据注解来完成对应的功能 +* 注解是 JDK1.5 的新特性 +* 注解是给编译器或 JVM 看的,编译器或 JVM 可以根据注解来完成对应的功能 * 注解类似修饰符,应用于包、类型、构造方法、方法、成员变量、参数及本地变量的声明语句中 注解作用: @@ -8487,15 +8493,16 @@ public class AnnotationDemo01{ 注解解析相关的接口: -* Annotation:注解类型,该类是所有注解的父类,注解都是一个Annotation的对象 +* Annotation:注解类型,该类是所有注解的父类,注解都是一个 Annotation 的对象 * AnnotatedElement:该接口定义了与注解解析相关的方法 -* Class、Method、Field、Constructor类成分:实现AnnotatedElement接口,拥有解析注解的能力 +* Class、Method、Field、Constructor 类成分:实现 AnnotatedElement 接口,拥有解析注解的能力 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)` : 判断对象是否使用了指定的注解 注解原理:注解本质是一个继承了`Annotation` 的特殊接口,其具体实现类是 Java 运行时生成的**动态代理类**,通过反射获取注解时,返回的是 Java 运行时生成的动态代理对象 `$Proxy1`,通过代理对象调用自定义注解(接口)的方法,会最终调用 `AnnotationInvocationHandler` 的 `invoke` 方法,该方法会从 `memberValues` 这个Map 中找出对应的值,而 `memberValues` 的来源是 Java 常量池 @@ -8550,10 +8557,10 @@ class BookStore{ ### 注解模拟 -注解模拟写一个Junit框架的基本使用 +注解模拟写一个 Junit 框架的基本使用 -1. 定义一个自定义注解MyTest,只能注解方法,存活范围一直都在。 -2. 定义若干个方法,只要有@MyTest注解的方法就能被触发执行,没有这个注解的方法不能执行!! +1. 定义一个自定义注解 MyTest,只能注解方法,存活范围一直都在。 +2. 定义若干个方法,只要有 @MyTest 注解的方法就能被触发执行,没有这个注解的方法不能执行!! ```java public class TestDemo{ @@ -8738,7 +8745,7 @@ XML文件中常见的组成元素有:文档声明、元素、属性、注释、 ##### DTD定义 -DTD是文档类型定义(Document Type Definition)。DTD 可以定义在 XML 文档中出现的元素、这些元素出现的次序、它们如何相互嵌套以及XML文档结构的其它详细信息。 +DTD 是文档类型定义(Document Type Definition)。DTD 可以定义在 XML 文档中出现的元素、这些元素出现的次序、它们如何相互嵌套以及XML文档结构的其它详细信息。 ##### DTD规则 @@ -8943,17 +8950,17 @@ persondtd.dtd文件 #### Schema -##### 定义 +##### XSD定义 1.Schema 语言也可作为 XSD(XML Schema Definition) -2.schema约束文件本身也是一个xml文件,符合xml的语法,这个文件的后缀名.xsd -3.一个xml中可以引用多个schema约束文件,多个schema使用名称空间区分(名称空间类似于java包名) -4.dtd里面元素类型的取值比较单一常见的是PCDATA类型,但是在schema里面可以支持很多个数据类型 -**5.Schema文件约束xml文件的同时也被别的文件约束着** +2.schema 约束文件本身也是一个 xml 文件,符合 xml 的语法,这个文件的后缀名.xsd +3.一个 xml 中可以引用多个 schema 约束文件,多个 schema 使用名称空间区分(名称空间类似于java包名) +4.dtd 里面元素类型的取值比较单一常见的是 PCDATA 类型,但是在 schema 里面可以支持很多个数据类型 +**5.Schema 文件约束 xml 文件的同时也被别的文件约束着** -##### 规则 +##### XSD规则 1、创建一个文件,这个文件的后缀名为.xsd。 2、定义文档声明 @@ -9000,7 +9007,7 @@ person.xsd -##### 引入 +##### XSD引入 1、在根标签上定义属性xmlns="http://www.w3.org/2001/XMLSchema-instance" 2、**通过xmlns引入约束文件的名称空间** @@ -9027,7 +9034,7 @@ person.xsd -##### Sc属性 +##### XSD属性 ```scheme @@ -9086,26 +9093,25 @@ person.xsd #### 解析 -* 概述:xml 解析就是从 xml 中获取到数据,DOM 是解析思想。 +xml 解析就是从 xml 中获取到数据,DOM 是解析思想 -* DOM(Document Object Model)文档对象模型:就是把文档的各个组成部分看做成对应的对象。 - 会把xml文件全部加载到内存,在内存中形成一个树形结构,再获取对应的值 +DOM(Document Object Model):文档对象模型,把文档的各个组成部分看做成对应的对象,把 xml 文件全部加载到内存,在内存中形成一个树形结构,再获取对应的值 -* 工具:dom4j 属于第三方技术,必须导入该框架 - https://dom4j.github.io/ 去下载dom4j,在idea中当前模块下新建一个lib文件夹,将jar包复制到文件夹中 - 选中jar包 -> 右键 -> 选择add as library即可 +dom4j 实现 +* dom4j 解析器构造方法:`SAXReader saxReader = new SAXReader();` -* dom4j 实现 - * dom4j 解析器构造方法:`SAXReader saxReader = new SAXReader();` - - * 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 +* 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)`:加载文件成为一个字节输入流返回 + `public InputStream getResourceAsStream(String path)`:加载文件成为一个字节输入流返回 + + + +**** @@ -9340,7 +9346,7 @@ public class XPathDemo { //4.3 在全文检索包含该属性的元素且属性值为该值的元素对象 Node nodeEle = document.selectSingleNode("//contact[@id=2]"); Element ele = (Element)nodeEle; - System.out.println(ele.elementTextTrim("name"));//武松 + System.out.println(ele.elementTextTrim("name"));//xi } } ``` @@ -9349,22 +9355,22 @@ public class XPathDemo { - 潘金莲 + 小白 - panpan@seazean.cn + bai@seazean.cn - 武松 + 小黑 - wusong@seazean.cn + hei@seazean.cn sql语句 - 武大狼 + 小虎 - wuda@seazean.cn + hu@seazean.cn 外面的名称 @@ -9871,14 +9877,14 @@ public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制 静态内部类和其他内部类: -* **一个class文件只能对应一个public类型的类**,这个类可以有内部类,但不会生成新的class文件 +* **一个 class 文件只能对应一个 public 类型的类**,这个类可以有内部类,但不会生成新的 class 文件 * 静态内部类属于类本身,加载到方法区,其他内部类属于内部类的属性,加载到栈(待考证) 类变量: -* 类变量是用static修饰符修饰,定义在方法外的变量,随着java进程产生和销毁 -* 在java8之前把静态变量存放于方法区,在java8时存放在**堆中的静态变量区** +* 类变量是用 static 修饰符修饰,定义在方法外的变量,随着 java 进程产生和销毁 +* 在 java8 之前把静态变量存放于方法区,在 java8 时存放在**堆中的静态变量区** 实例变量: @@ -9893,7 +9899,7 @@ public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制 类常量池、运行时常量池、字符串常量池有什么关系?有什么区别? -* 类常量池与运行时常量池都存储在方法区,而字符串常量池在jdk7时就已经从方法区迁移到了java堆中 +* 类常量池与运行时常量池都存储在方法区,而字符串常量池在 jdk7 时就已经从方法区迁移到了 java 堆中 * 在类编译过程中,会把类元信息放到方法区,类元信息的其中一部分便是类常量池,主要存放字面量和符号引用,而字面量的一部分便是文本字符 * 在类加载时将字面量和符号引用解析为直接引用存储在运行时常量池 * 对于文本字符来说,会在解析时查找字符串常量池,查出这个文本字符对应的字符串对象的直接引用,将直接引用存储在运行时常量池 @@ -9909,7 +9915,7 @@ public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制 * 符号引用:在编译过程中并不知道每个类的地址,因为可能这个类还没有加载,如果在一个类中引用了另一个类,无法知道它的内存地址,只能用他的类名作为符号引用,在类加载完后用这个符号引用去获取内存地址 - 例如:在com.demo.Solution类中引用了com.test.Quest,把`com.test.Quest`作为符号引用存进类常量池,在类加载完后,用这个符号引用去方法区找这个类的内存地址 + 例如:在 com.demo.Solution 类中引用了 com.test.Quest,把 `com.test.Quest` 作为符号引用存进类常量池,在类加载完后,用这个符号引用去方法区找这个类的内存地址 @@ -10178,15 +10184,15 @@ public void localvarGC4() { - 抢先式中断:没有虚拟机采用,首先中断所有线程,如果有线程不在安全点,就恢复线程让线程运行到安全点 - 主动式中断:设置一个中断标志,各个线程运行到各个 Safe Point 时就轮询这个标志,如果中断标志为真,则将自己进行中断挂起 -问题:Safepoint 保证程序执行时,在不太长的时间内就会遇到可进入GC 的 Safepoint,但是当线程处于 Waiting 状态或 Blocked 状态,线程无法响应 JVM 的中断请求,运行到安全点去中断挂起,JVM 也不可能等待线程被唤醒,对于这种情况,需要安全区域来解决 +问题:Safepoint 保证程序执行时,在不太长的时间内就会遇到可进入 GC 的 Safepoint,但是当线程处于 Waiting 状态或 Blocked 状态,线程无法响应 JVM 的中断请求,运行到安全点去中断挂起,JVM 也不可能等待线程被唤醒,对于这种情况,需要安全区域来解决 安全区域 (Safe Region):指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始 GC 都是安全的 运行流程: -- 当线程运行到 Safe Region 的代码时,首先标识已经进入了 Safe Region,如果这段时间内发生GC,JVM会忽略标识为 Safe Region 状态的线程 +- 当线程运行到 Safe Region 的代码时,首先标识已经进入了 Safe Region,如果这段时间内发生GC,JVM 会忽略标识为 Safe Region 状态的线程 -- 当线程即将离开 Safe Region 时,会检查JVM是否已经完成GC,如果完成了则继续运行,否则线程必须等待GC 完成,收到可以安全离开 SafeRegion 的信号 +- 当线程即将离开 Safe Region 时,会检查 JVM 是否已经完成 GC,如果完成了则继续运行,否则线程必须等待GC 完成,收到可以安全离开 SafeRegion 的信号 @@ -11987,6 +11993,7 @@ ClassLoader 类常用方法: * `findLoadedClass(String name)`:查找名称为 name 的已经被加载过的类,final 修饰无法重写 * `defineClass(String name,byte[] b,int off,int len)`:将字节流解析成 JVM 能够识别的类对象 * `resolveclass(Class c)`:链接指定的 Java 类,可以使类的 Class 对象创建完成的同时也被解析 +* `InputStream getResourceAsStream(String name)`:指定资源名称获取输入流 @@ -15408,16 +15415,16 @@ public class HeapSort { System.out.println(Arrays.toString(arr)); } - //len为数组长度 - private static void heapSort(int[] arr, int len) { + //high为数组最大索引 + private static void heapSort(int[] arr, int high) { //建堆,逆排序,因为堆排序定义的交换顺序是从当前结点往下交换,逆序排可以避免多余的交换 - //i初始值是最后一个节点的父节点,如果是数组长度len,则 i = len / 2 -1 - for (int i = (len - 1) / 2; i >= 0; i--) { + //i初始值是最后一个节点的父节点,如果参数是数组长度len,则 i = len / 2 -1 + for (int i = (high - 1) / 2; i >= 0; i--) { //调整函数 - sift(arr, i, len); + sift(arr, i, high); } //从尾索引开始排序 - for (int i = len; i > 0; i--) { + for (int i = high; i > 0; i--) { //将最大的节点放入末尾 int temp = arr[0]; arr[0] = arr[i]; @@ -15597,6 +15604,7 @@ public class MergeSort { } //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);//归并排序前半段 @@ -15992,7 +16000,7 @@ public static int match(String s,String t) { ### KMP -KMP匹配: +KMP 匹配: * next 数组的核心就是自己匹配自己,主串代表后缀,模式串代表前缀 * nextVal 数组的核心就是回退失配 @@ -16074,19 +16082,6 @@ public class Kmp { } return nextVal; } - /*根据next求nextVal - private static int[] getNextVal(String t, int[] next) { - int[] nextVal = new int[next.length]; - nextVal[0] = -1; - for (int i = 1; i < nextVal.length; i++) { - if (t.charAt(i) == t.charAt(next[i])) { - nextVal[i] = nextVal[next[i]]; - } else { - nextVal[i] = next[i]; - } - } - return nextVal; - }*/ } ``` diff --git a/Prog.md b/Prog.md index 578934b..c668413 100644 --- a/Prog.md +++ b/Prog.md @@ -3896,7 +3896,7 @@ JDK8 前后对比: ThreadLocalMap m = getMap(Thread.currentThread()); // 如果此map存在 if (m != null) - // 存在则调用map.remove,以当前ThreadLocal为key删除对应的实体entry + // 存在则调用map.remove,this时当前ThreadLocal,以this为key删除对应的实体 m.remove(this); } ``` @@ -4106,7 +4106,7 @@ Memory leak:内存泄漏是指程序中动态分配的堆内存由于某种原 解决方法:使用完 ThreadLocal 中存储的内容后将它 **remove** 掉就可以 -ThreadLocal 内部解决方法:在 ThreadLocalMap 中的 set/getEntry 方法中,会对 key 进行判断,如果为 null (ThreadLocal 为 null) 的话,那么会对 Entry 进行垃圾回收。所以**使用弱引用比强引用多一层保障**,就算不调用 remove,也有机会进行 GC +ThreadLocal 内部解决方法:在 ThreadLocalMap 中的 set/getEntry 方法中,会对 key 进行判断,如果为 null(ThreadLocal 为 null)的话,会对 Entry 进行垃圾回收。所以**使用弱引用比强引用多一层保障**,就算不调用 remove,也有机会进行 GC diff --git a/SSM.md b/SSM.md index 2ee8224..b1c742f 100644 --- a/SSM.md +++ b/SSM.md @@ -1591,8 +1591,8 @@ public void testFirstLevelCache(){ 1. select 标签的 useCache 属性 - 映射文件中的`` 标签中设置 `useCache="true"` 代表当前 statement 要使用二级缓存。 + 注意:针对每次查询都需要最新的数据 sql,要设置成 useCache=false,禁用二级缓存 ```xml - select * from t_user - -
- ``` - -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 080/242] 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 081/242] 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 082/242] 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 083/242] 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 084/242] 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 085/242] 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 086/242] 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 087/242] 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 088/242] 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 089/242] 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 090/242] 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 091/242] 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 092/242] 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 093/242] 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 094/242] 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 095/242] 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 096/242] 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 097/242] 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 098/242] 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 099/242] 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 100/242] 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 101/242] 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 102/242] 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 103/242] 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 104/242] 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 105/242] 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 106/242] 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 107/242] 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 108/242] 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 109/242] 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 110/242] 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 111/242] 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 112/242] 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 113/242] 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 114/242] 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 115/242] 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 116/242] 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 117/242] 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 118/242] 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 119/242] 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 120/242] 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 135/242] 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 136/242] 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 137/242] 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 138/242] 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 139/242] 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 140/242] 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 141/242] 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 142/242] 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 143/242] 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 144/242] 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 145/242] 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 146/242] 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 147/242] 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 148/242] 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 149/242] 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 150/242] 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 151/242] 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 152/242] 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 153/242] 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 154/242] 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 155/242] 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 156/242] 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 157/242] 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 158/242] 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 159/242] 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 160/242] 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 161/242] 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 162/242] 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 163/242] 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 164/242] 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 165/242] 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 166/242] 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 167/242] 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 168/242] 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 169/242] 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 170/242] 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 171/242] 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 172/242] 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 173/242] 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 174/242] 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 175/242] 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 176/242] 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 177/242] 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 178/242] 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 179/242] 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 180/242] 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 181/242] 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 182/242] 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 183/242] 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 184/242] 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 185/242] 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 186/242] 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 187/242] 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 188/242] 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 -