Coder Social home page Coder Social logo

blog's Introduction

blog's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

Forkers

celleychen

blog's Issues

《深入理解Java虚拟机》之 类加载机制

虚拟机类加载机制

类加载的时机

类从被加载到虚拟机内存开始,到卸载出内存为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。其中验证、准备、解析这3个部分统称为连接。

加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段开始之后再开始,这是为了支持 Java 语言的运行时绑定(也称为动态绑定或晚期绑定)。注意,这里写的是按部就班的“开始”,强调这点是因为这些阶段通常都是互相交叉地混合式进行的,通常会在一个阶段执行的过程中调用激活另外一个阶段。

什么情况下需要开始类加载的一个阶段(加载),Java 虚拟机规范并没有强制约束。但是对于初始化阶段,虚拟机规范则严格规定了有且只有5种情况必须立即对类进行初始化(而加载、验证、准备自然要在之前开始):

  1. 遇到new、getstatic、putstatic 或 invokestatic 这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的 Java 代码场景是:使用 new 关键字实例化对象的时候、读取或者设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  2. 使用 java.lang.reflect 包的方法对类进行反射调用的时候。
  3. 当初始化一个类的时候,如果发现父类还没有进行过初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要需要指定一个要执行的主类(包括 main() 方法的那个类),虚拟机会先触发这个主类。
  5. 当使用 JDK 1.7的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。

这5中场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。下面举3个被动引用的例子。

/**
 * 被动使用类字段演示一
 * 通过子类引用父类的静态字段,不会导致子类初始化
 **/
public class SuperClass {

    static {
        System.out.println("SuperClass init!")
    }
  
    public static int value = 123;
}

public class SubClass extends SuperClass {
    
    static {
        System.out.println("SubClass init!")
    }
}

public class NotInitialization {
    public static void mian (String[] args) {
        System.out.println(SubClass.value);
    }
}

代码运行只会输出“SuperClass init!”,而不会输出“SubClass init!”。对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过子类来引用父类中定义的静态字段,只会触发父类的初始化。

/**
 * 被动使用类字段演示二
 * 通过数组定义来引用类,不会触发类的初始化
 **/
public class NotInitialization {
    public static void main (String[] args ) {
        SuperClass[] sca = new SuperClass[10];
    }
}

运行之后发现并没有触发 SuperClass 的初始化阶段。但是这段代码里面触发了另一个类的初始化阶段,它是一个由虚拟机自动生成的、直接继承于 java.lang.Object 的子类,创建动作由字节码指令 newarray 触发。这个类代表了一个元素类型为 SuperClass 的一维数组,数组中应有的属性和方法(用户可直接使用的只有被修饰为 public 的 length 属性和 clone() 方法)都实现在这个类里。Java 语言中对数组的访问比 C/C++ 相对安全是因为这个类封装了数组元素的访问方法,而 C/C++ 直接翻译为对数组指针的移动。在 Java 语言中,当检查到发生数组越界时会抛出 java.lang.ArrayIndexOutOfBoundsException 异常。

/**
 * 被动使用类字段演示三
 * 常量在编译阶段会存入调用类的常量池中,本质上并没引用到定义常量的类,因此不会触发定义常量的类的初始化
 **/
 public class ConstClass {
     static {
         System.out.println("ConstClass init!");
     }
     public static final String HELLOWORLD = "hello world";
 }
 public class NotInitialization {
    public static void main (String[] args ) {
		System.out.println(ConstClass.HELLOWORLD);
    }
}

程序运行也并没有输出“ConstClass init”,因为其实在编译阶段通过常量传播优化,已经将此常量的值“hello world”存储到了 NotInitialization 类的常量池中。实际上 NotInitialization 的 Class 文件中并没有 ConstClass 类的符号引用入口,这两个类在编译成 Class 文件之后就不存在任何联系了。

接口的加载过程和类的加载过程稍微有些不同,针对接口需要做一些特殊说明:接口也有初始化过程,这点与类是一致的。上面代码都是使用 “static { }”来输出初始化消息的,而接口中不能使用“ static { }”,但编译器会为接口生成“<clinit>()” 类构造器,用于初始化接口中所定义的成员变量。接口和类真正有所区别的是前面讲述的5中情况中的第3种:当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用父接口中定义的常量)才会初始化。

类加载的过程

加载

在加载阶段,虚拟机需要完成一下三件事:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。
  2. 将这个二进制字节流所代表的静态存储结构转换为方法区的运行时数据结构。
  3. 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据访问入口。

相对于类加载过程的其他阶段,一个非数组类的加载阶段(准确的说,是加载阶段中获取类的二进制字节流的动作)是开发人员可控性最强的,因为加载阶段既可以使用系统提供的引导类加载器来完成,也可以由用户自定义的类加载器去完成,开发人员可以通过定义自己的类加载器去控制字节流的获取方式(即重写一个类加载器的 loadClass() 方法)。

数组类则不一样,数组类本身并不通过类加载器创建,它是由 Java 虚拟机直接创建的。但数组类和类加载器仍然有很密切的关系,因为数组类的元素类型(指数组去掉维度的类型)最终是要靠加载器去创建,一个数组类的创建过程遵循如下规则:

  1. 如果数组类型是引用类型,那就递归采用本节中定义的加载过程去加载这个组件类型,数组 C 将在加载该组件类型的类加载器的类名称空间上被标识。
  2. 如果数组类的组件类型不是引用类型(如 int[ ] 数组),Java 虚拟机会把数组 C 标记为与引导类加载器关联。
  3. 数组类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为 public。

加载阶段完成后,二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机自行定义。然后在内存中实例化一个 java.lang.Class 对象(并没有明确规定是在 Java 堆中,对于 HotSpot 虚拟机而言,Class 对象比较特殊,它虽然是对象,但是存放在方法区里面),这个对象将作为程序访问方法区中这些类型数据的外部接口。

加载阶段和连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能就已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间依然保持着固定的先后顺序。

验证

验证是连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。从整体上来看,验证阶段大致上会完成下面4个阶段的检验工作:

  1. 文件格式验证。第一阶段要验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理。可能包括下面这些验证点:

    • 是否以魔数 0xCAFEBABE 开头。

    • 主、次版本号是否在当前虚拟机处理范围之内。

    • 常量池的常量中是否有不被支持的常量类型(检查常量 tag 标志)。

    • 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。

    • CONSTANT_Utf8_info 型的常量中是否有不符合 UTF8 的编码的数据。

    • Class 文件中的各个部分及文件本身是否有被删除的或附加的其他信息。

      …...

    实际上,第一阶段验证工作远不止上面这些。该验证阶段的主要目的是保证输入的字节流能正确的解析并存储于方法区之内,格式上符合描述一个 Java 类型信息的要求。该阶段的验证是基于二进制字节流进行的,只有通过了这个阶段的验证后,字节流才能进入内存的方法区进行存储,所有后面3个阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流。

  2. 元数据验证。第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合 Java 语言规范的要求,可能包括以下验证点:

    • 这个类是否有父类。
    • 这个类的父类是否继承了不允许被继承的类(被 final 修饰的类)。
    • 如果这个类不是抽象类,是否实现了其父类或者接口之中要求实现的所有方法。
    • 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的 final 字段,或者出现了不符合规则的方法重载,例如方法参数都一样,但返回类型却不同等)。

    第二阶段的目的主要是对元数据信息进行语义校验,保证不存在不符合 Java 语言规范的元数据信息。

  3. 字节码验证。第三阶段是整个验证过程中最复杂的的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在第二阶段对元数据信息中的数据类型做完检验后,这个阶段对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的时间,例如:

    • 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似这样的情况:在操作栈放置了一个 int 类型的数据,使用时却按 long 类型来加载入本地变量表。

    • 保证跳转指令不会跳转到方法体以外的字节码指令上。

    • 保证方法体中的类型转换是有效的,例如可以把一个子类对象赋值给父类数据类型,这是安全的,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险和不合法的。

      …...

    由于数据流验证的高复杂性,虚拟机设计团队为了避免过多的时间消耗在字节码验证阶段,在 JDK 1.6之后的javac 编译器和 Java 虚拟机中进行了一项优化,给方法体的 Code 属性的属性表中增加了一项名为“StackMapTable”的属性,这项属性描述了方法体中所有的基本块(按照控制流拆分的代码块)开始时本地变量表和操作栈应有的状态,在字节码验证期间,就不需要根据程序推导这些状态的合法性,只需要检查 StackMapTable 属性中的记录是否合法即可。理论上 StackMapTable 属性也存在错误或被篡改的可能。

  4. 符号引用验证。最后一个阶段检验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段(解析阶段)中发生。符号引用验证可以看作是对类自身以外(常量池中各种符号引用)的信息进行匹配性校验,通常需要校验一下内容:

    • 符号引用中通过字符串描述的全限定名是否能找到对应的类。

    • 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。

    • 符号引用中的类、字段、方法的访问性是否可以被当前类访问。

      …...

    符号引用验证的目的是确保解析动作能正常执行,如果无法通过符号引用验证,那么将会抛出一个 java.lang.IncompatibleClassChangeError 异常的子类,如 java.lang.IllegalAccessError、java.lang.noSuchFieldError、java.lang.noSuchMehodError 等。

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。首先,这时候进行内存分配的仅包括类变量(被 static 修饰的变量),而不包括实例变量,实例变量将在对象实例化的时候随着对象一起分配在 Java 堆。其次,这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为

public static int value = 123;

那变量 value 在准备阶段过后的初始值是0而不是123,因为这时候尚未执行任何 Java 方法,而把123赋值给 value 的 putstatic 指令是程序被编译后,存放在类构造器 <clinit>() 方法中,所以把 value 赋值为123的动作将在初始化阶段才会执行。特殊情况下:如果类字段的字段属性中存在 ConstantValue 属性,那在准备阶段变量 value 就会被初始化为 ConstantValue 属性所指定的值,例如 value 的定义为:

public static final int value = 123;

编译时 javac 将会为 value 生成 ConstantValue 属性,在准备阶段虚拟机就会根据 ConstantValue 的设置为 value 赋值为 123。

解析

解析阶段是虚拟机将常量池内符号引用替换为直接引用的过程。符号引用和直接引用关联如下:

  1. 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的引用到目标即可。符号引用和虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。
  2. 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能直接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那引用的目标就必定已经在内存中。

虚拟机规范中并未规定解析阶段发生的具体时间,只要求在执行 anewarray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、multianewarray、new、putfield 和 putstatic 这16个用于操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析。所以虚拟机实现可以根据需要判断到底是在类被加载器加载时就对常量池的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析它。

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行,分别对应常量池中的 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info、CONSTANT_invokeDynamic_info 7中常量类型。下面介绍前4种:

  1. 类或接口的解析。

    假设当前代码所处的类为 D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,那虚拟机完成整个解析过程需要以下三个步骤:

    • 如果C不是一个数组类型,那虚拟机将会把代表N的全限定名传递给D的类加载器去加载这个类C,在加载过程中,由于元数据验证、字节码验这个的需要,又可能触发其他相关类的加载动作,例如加载这个类的父类或者接口。一旦这个加载过程出现了任何异常,解析过程就宣告失败。
    • 如果C是一个数组类型,并且数组的元素类型为对象,也就是N的描述符会是类似”[Ljava/lang/Integer“的形式,那将会按照第一点的规则加载数组元素类型。接着由虚拟机生成一个代表此数组维度的数组对象。
    • 如果上面的步骤没有出现任何异常,那么C在虚拟机中实际上已经成为一个有效的类或接口了,但在解析完成之前还要进行符号引用验证,确认D是否具备对C的访问权限,如果发现不具备权限,将抛出 java.lang.IllegalAccessError 异常。
  2. 字段解析。

    要解析一个未解析过的字段符号引用,首先将会对字段表内 class_index项中索引的 CONSTANT_Class_info 符号引用进行解析,也就是字段所述的类或接口的符号引用。如果在解析这个类或接口符号引用的过程出现了任何异常,都会导致字段符号引用解析的失败。如果解析成功完成,那将这个字段所属的类或接口用C表示,虚拟机规范要求按照如下步骤对C进行后续字段的搜索。

    • 如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
    • 否则,如果在C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
    • 否则,如果C不是 java.lang.Object 的话,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
    • 否则,查找失败,抛出 java.lang.NoSuchFieldError 异常。

    如果查找过程中成功返回了引用,将会对这个字段进行权限验证,如果发现不具备对字段的访问权限,将抛出 java.lang.IllegalAccessError 异常。

  3. 类方法解析。

    类方法解析的第一个步骤和字段解析一样,也需要先解析出类方法表 class_index 项中索引的方法所属类或接口的符号引用,如果解析成功,我们依然用C来表示这个类,接下来虚拟机进行后续的类方法搜索:

    • 类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现class_index 中索引的是个接口,则直接抛出 java.lang.IncompatibleClassError 异常。
    • 如果通过第一步,在类C中查找是否有简单名称和字段描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
    • 否则,在类C的父类中递归查找是否有简单名称和字段描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
    • 否则,在类C实现的接口列表及它们的父接口中递归查找是否有简单名称和字段描述符都与目标相匹配的方法,如果存在匹配的方法,说明C是一个抽象类,这是查找结束,抛出 java.lang.AbstractMethodError 异常。
    • 否则,宣告方法查找失败,抛出 java.lang.NoSuchMethodError 异常。

    最后,如果查找过程中成功返回了引用,将会对这个方法进行权限验证,如果发现不具备对方法的访问权限,将抛出 java.lang.IllegalAccessError 异常。

  4. 接口方法解析。

    接口方法也需要先解析出接口方法表 class_index 项中索引的方法所属类或接口的符号引用,如果解析成功,我们依然用C来表示这个类,接下来虚拟机进行后续的类方法搜索:

    • 与类方法解析不同,如果在类方法表中发现class_index 中索引的是个类而不是接口,则直接抛出 java.lang.IncompatibleClassError 异常。
    • 否则,在接口C中查找是否有简单名称和字段描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
    • 否则,在接口C的父接口中递归查找,直到 java.lang.Object 类(查找范围会包括 Object 类)为止,看是否有简单名称和字段描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
    • 否则,宣告查找失败,抛出 java.lang.NoSuchMethodError 异常。

    由于接口中的方法默认都是 public 的,所以不存在访问权限的问题,因此接口方法的符号引用应当不会抛出 java.lang.IllegalAccessError 异常。

初始化

类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的 Java 程序代码(或者说是字节码)。

在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则是根据程序猿通过程序制定的主观计划去初始化类变量和其他资源,或者换句话说,初始化阶段是执行类构造器 <clinit>() 方法的过程。

  • <clinit>() 方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。代码如下:

    public class Test {
        static {
            i = 0;						//给变量赋值可以正常通过编译
            System.out.println(i);		//编译器会提示“非法向前引用”
        }
        static int i = 1;
    }
  • <clinit>() 方法与类的构造方法(或者说实例构造器 <init>() 方法)不同,它不需要显示调用父类构造器,虚拟机会保证在子类的 <clinit>() 方法执行之前,父类的 <clinit>() 方法已经执行完毕。

  • 由于父类的 <clinit>() 方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作,如下代码,字段 B 的值将会是2而不是1。

    public class Parent {
        public static int A = 1;
        static {
            A = 2;
        }
    }
    static class Sub extends Parent {
        public static int B = A;
    }
    public static void main(String[] args) {
        System.out.println(Sub.B);
    }
  • <clinit>() 方法对于类或接口来说并不是必须的,如果一个类并没有静态语句块,也没有对变量的赋值操作,那么编译器就可以不为这个类生成 <clinit>() 方法。

  • 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口和类一样都会生成 <clinit>() 方法,但接口和类不同的是,执行接口的 <clinit>() 方法不需要先执行父接口的 <clinit>() 方法,只有当父接口的中定义的变量使用时,父接口才会初始化,另外,接口的实现类在初始化时也一样不会执行接口的 <clinit>() 方法。

  • 虚拟机会保证一个类的 <clinit>() 方法 在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 <clinit>() 方法,其他线程都需要阻塞等待,直到活动线程执行 <clinit>() 方法完毕。如果在一个类的 <clinit>() 方法中有很耗时的操作,就可能导致多个线程阻塞,在实际应用中,这种阻塞往往是很隐蔽的。示例代码如下:

    static class DeadLoopClass {
        static {
            //如果不加上这个if语句,编译器将提示“Initializer dose not complete normally”并拒绝编译
          	if(true) {
                System.out.println(Thread.currentThread() + "init DeadLoopClass");
                while(true) {
                }
            }
        }
    }
    public static void main (String[] args) {
        Runnable script = new Runnable() {
            public void Run() {
                System.out.println(Thread.currentThread() + "start");
                DeadLoopClass dlc = new DeadLoopClass();
                System.out.println(Thread.currentThread() + "run over");
    		}
        };
        
        Thread thread1 = new Thread(script);
        Thread thread2 = new Thread(script);
        thread1.start();
        thread2.start();
    }

    运行结果如下,即一条线程在死循环模拟长时间操作,另一条线程在阻塞等待。

    Thread[Thread-0,5,mian]start
    Thread[Thread-1,5,mian]start
    Thread[Thread-0,5,mian]init DeadLoopClass

类加载器

虚拟机设计团队把类加载阶段的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到 Java 虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类,实现这个动作的代码模块称为“类加载器”。

类与类加载器

对于任意一个类,都需要由加载它的来加载器和这个类本身一同确立其在虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。换句话说:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

这里说的“相等”,包括代表类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果,也包括使用 instanceof 关键字做对象所属关系判定情况等。

双亲委派模型

从 Java 虚拟机角度来说,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用 C++ 语言实现,是虚拟机自身的一部分;另一种就是所有的其他类加载器,这些类加载器由 Java 语言实现,独立于 Java 虚拟机外部,并且全部都继承自抽象类 java.lang.ClassLoader。

从开发人员角度可以划分的更细一点,绝大部分 Java 程序都会使用到下面3中系统提供的类加载器。

  • 启动类加载器(Bootstrap ClassLoader)。这个类加载器负责将存放在 <JAVA_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那么直接使用 null 代替即可。
  • 扩展类加载器(Extension ClassLoader)。它负责加载 <JAVA_HOME>\lib\ext 目录中的, 或者被 java.ext.dirs 系统变量指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
  • 应用程序类加载器(Application ClassLoader)。由于这个类加载器时 ClassLoader 中的 getSystemClassLoader() 方法中的返回值,所以一般也称它为系统类加载器。负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自己定义过自己的类加载器,一般情况下这个就是程序中默认的类加载。

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

使用双亲委派模型的一个显而易见的好处就是, Java 类随着它的类加载器一起具备了一种带有优先级的层次关系。例如 java.lang.Object,它存放在 rt.jar 中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此 Object 类在程序的各种类计载器环境中都是同一个类。我们尝试去编写一个和 rt.jar 类库中已有类重名的 Java 类,将会发现能正常通过编译,但永远无法被加载运行。

ThreadPoolExecutor

简介

  • 对象的创建和销毁需要一定的开销,线程亦是对象,创建线程和销毁线程必然也需要同样的开销。
  • 在某些情况下,线程执行的任务耗时不长,但是任务很多。这样就导致频繁的创建、销毁线程,需要很大的时间开销和资源开销。线程池应运而生。
  • 线程池相当于在这一个池中维护多个线程,需要执行任务时从池中取出一个线程用来执行任务,任务执行完成后将线程放回池中。这样也就减少了开销。
  • 因为减少了每个任务调度的开销,所以它能在执行大量异步任务的场景中提供更好的性能。并且它提供了一种限定和管理资源(比如线程)的方式。他也会保存一些基本的统计信息,比如已完成的任务数量。

Executor

Executor 接口中只有一个方法:

void execute(Runnable command);

command 为待执行的任务。

ExecutorService

ExecutorService 接口继承自 Executor 接口,并扩展了几个方法:

关于状态:

// 发出关闭信号,不会等到现有任务执行完成再返回,但是现有任务还是会继续执行,
// 可以调用awaitTermination等待所有任务执行。不再接受新的任务。
void shutdown();
 
// 立刻关闭,尝试取消正在执行的任务(不保证会取消成功),返回未被执行的任务
List<Runnable> shutdownNow();
 
// 是否发出关闭信号
boolean isShutdown();
 
// 是否所有任务都执行完毕在shutdown之后,也就是如果不调用shutdownNow或者
// shutdown是不可能返回true
boolean isTerminated();
 
// 进行等待直到所有任务完成或者超时
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;

执行单个任务(立刻返回一个Future存储任务执行的实时状态):

<T> Future<T> submit(Callable<T> task);
 
<T> Future<T> submit(Runnable task, T result);
 
Future<?> submit(Runnable task);

执行多个任务(前两个方法等到所有任务完成才返回,后两个方法等到有一个任务完成,取消其他未完成的任务,返回执行完成的任务的执行结果):

<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException;
 
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                                  long timeout, TimeUnit unit)
        throws InterruptedException;
        
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
        throws InterruptedException, ExecutionException;
 
<T> T invokeAny(Collection<? extends Callable<T>> tasks,
                    long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;

AbstractExecutorService

该抽象类实现了 ExecutorService 接口,并提供了 ExecutorService 中各方法中的具体实现。例如 submit() 方法:

public Future<?> submit(Runnable task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<Void> ftask = newTaskFor(task, null);
    execute(ftask);
    return ftask;
}
 
public <T> Future<T> submit(Runnable task, T result) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<T> ftask = newTaskFor(task, result);
    execute(ftask);
    return ftask;
}
 
public <T> Future<T> submit(Callable<T> task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<T> ftask = newTaskFor(task);
    execute(ftask);
    return ftask;
}

并扩展了两个 newTaskFor() 方法:

protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
        return new FutureTask<T>(callable);
}
 
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
        return new FutureTask<T>(runnable, value);
}

FutureTask 是任务和执行结果的封装类。它实现了 RunnableFuture 接口,RunnableFuture 接口继承了 Runnable 和 Future 接口。可以通过 get() 方法获取任务的执行结果。

ThreadPoolExecutor

ThreadPoolExecutor 继承自 AbstractExecutorService。

初始化参数介绍

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ? null :
            AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
}
  • corePoolSize: 核心线程数。新任务提交过来时,如果当前活动的线程数少于 corePoolSize 会创建一个新线程来处理这个新任务即使当前有空闲线程。
  • maximumPoolSize:最大线程数。如果当前线程数大于 corePoolSize 小于 maximumPoolSize 且任务队列已满时也会创建新线程。
  • keepAliveTime:如果当前线程数量超出了 corePoolSize,超出的那部分非核心线程会在空闲超出 keepAliveTime 时被终止。这能够在线程池活跃状态不足时及时回收占用的资源。默认情况下核心线程超时不回收,可以通过配置 keepAliveTime 和 allowCoreThreadTimeOut 来允许核心线程超时回收。
  • unit:超时时间单位
  • workQueue:任务等待队列,存放任务
  • threadFactory:线程工厂用于生产线程。可以自定义实现
  • handler:也就是参数 maximumPoolSize 达到后丢弃处理的方法,java 提供了4种丢弃处理的方法;java 默认的是使用 AbortPolicy ,他的作用是当出现这中情况的时候会抛出一个异常。
    1. AbortPolicy: 拒绝提交,直接抛出异常,也是默认的饱和策略;
    2. CallerRunsPolicy: 线程池还未关闭时,用调用者的线程执行任务;
    3. DiscardPolicy: 直接丢弃任务
    4. DiscardOldestPolicy:线程池还未关闭时,丢掉阻塞队列最久为处理的任务,并且执行当前任务。

Executors 构造 ThreadPoolExecutor 对象

Executors 类提供了几个构造 ThreadPoolExecutor 对象的静态方法:

  1. newCachedThreadPool()

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
     
    public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>(),
                                      threadFactory);
    }

    线程数量没有上界(Integer.MAX_VALUE),有新任务提交并且没有空闲线程时,创建一个新线程执行该任务,每个线程空闲时间为 60s, 60s 空闲后线程会被移出缓存。使用 SynchronousQueue 作为任务队列的实现类。适用于执行大量生命周期短的异步任务。

  2. newFixedThreadPool()

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
     
    public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
    }

    固定容量的线程池。使用 LinkedBlockingQueue 作为任务队列的实现类。当新任务到达时,创建新线程,当线程数达到上限时,将任务放到队列中,任务队列中任务数量没有上界。当线程创建之后就一直存在直至显式的调用 shutdown() 方法。

  3. newSingleThreadExecutor()

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
     
    public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory));
    }

    单个 Worker 的线程池。和 newFixedThreadPool(1) 类似,区别在于这个实例经过了一次封装,不能对该实例的参数进行重配置,并且实现了 finalize() 方法,能够在 GC 时调用 shutdown() 方法关闭该线程池。

  4. newScheduledThreadPool()

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
     
    public static ScheduledExecutorService newScheduledThreadPool(
        	int corePoolSize, ThreadFactory threadFactory) {
        return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
    }

    ScheduledThreadPoolExecutor 的功能主要有两点:在固定的时间点执行(也可以认为是延迟执行),重复执行。

  5. newSingleThreadScheduledExecutor()

    public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
        return new DelegatedScheduledExecutorService
            (new ScheduledThreadPoolExecutor(1));
    }
     
    public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory) {
        return new DelegatedScheduledExecutorService
            (new ScheduledThreadPoolExecutor(1, threadFactory));
    }

    将 ScheduledThreadPoolExecutor 进行一层包装。

状态转换

ThreadPoolExecutor 中一些状态相关的变量和方法如下:

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;			//29
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;	//低29位表示
 
// runState is stored in the high-order bits
private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;
 
// Packing and unpacking ctl
private static int runStateOf(int c)     { return c & ~CAPACITY; }	//c & 111000...000
private static int workerCountOf(int c)  { return c & CAPACITY; }	//c & 000111...111
private static int ctlOf(int rs, int wc) { return rs | wc; }
 
/*
 * Bit field accessors that don't require unpacking ctl.
 * These depend on the bit layout and on workerCount being never negative.
 */
private static boolean runStateLessThan(int c, int s) {
    return c < s;
}
 
private static boolean runStateAtLeast(int c, int s) {
    return c >= s;
}
 
private static boolean isRunning(int c) {
    return c < SHUTDOWN;
}

线程池的状态在 ThreadPoolExecutor 中通过 AtomicInteger 类型的成员变量 ctl 的高3位表示。

  • RUNNING: 111
  • SHUTDOWN: 000
  • STOP: 001
  • TIDYING: 010
  • TERMINATED: 011

变量 ctl 的低29位表示的有效工作线程数。

各状态之间的转换如下图:

  • SHUTDOWN 想转化为 TIDYING,需要 workQueue 为空,同时 workerCount 为0。
  • STOP 转化为 TIDYING,需要 workerCount 为0

方法解析

execute()

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    //如果运行中的worker线程数少于设定的核心线程数,增加worker线程,把task分配给新建的worker线程
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // 如果任务可以被加入到任务队列中,即等待的任务数还在允许的范围内,
    // 再次检查线程池是否被关闭,如果关闭的话,则移除任务并拒绝该任务
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // 如果任务数超过了现有worker线程的承受范围,尝试新建worker线程
    // 如果无法添加新的worker线程,则会拒绝该任务
    else if (!addWorker(command, false))
        reject(command);
}

addWorker()

private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);
 
        // Check if queue empty only if necessary.
        // 1. rs > shutdown,即shutdown和running以外的状态
        // 2. rs = shutdown
        //     1)firstTask不为null,即有task分配
        //     2)没有task,但是workQueue(等待任务队列)为空
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            return false;
 
        for (;;) {
            // 1. 如果没有设定线程数的限制,worker线程数不能大于最大值(2的29次方-1)
            // 2. 如果是固定尺寸的线程池,不能大于固定尺寸
            // 3. 如果是可扩展的线程池,不能大于规定的线程数的上限
            int wc = workerCountOf(c);
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            // 用CAS操作增加线程数量,如果失败,重新循环
            if (compareAndIncrementWorkerCount(c))
                break retry;
            c = ctl.get();  // Re-read ctl
            if (runStateOf(c) != rs)
                continue retry;
            // else CAS failed due to workerCount change; retry inner loop
        }
    }
 
    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                // Recheck while holding lock.
                // Back out on ThreadFactory failure or if
                // shut down before lock acquired.
                int rs = runStateOf(ctl.get());
                // 检查以下任一状态是否出现
                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
                    workers.add(w);
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            //执行添加失败相对应的操作
            addWorkerFailed(w);
    }
    return workerStarted;
}

runWorker()

ThreadPoolExecutor 有一个成员类叫 Worker

private final class Worker extends AbstractQueuedSynchronizer implements Runnable

这里 AbstractQueuedSynchronizer 的作用是使Worker具有锁的功能,在执行任务时,会把 Worker 锁住,这个时候就无法中断 Worker。Worker 空闲时候是线程池可以通过获取锁,改变 Worker 的某些状态,在此期间因为锁被占用,Worker 就是不会执行任务的。

Worker工作的逻辑在ThreadPoolExecutor#runWorker方法中

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
        //task为null则从BlockingQueue中等待获取任务
        while (task != null || (task = getTask()) != null) {
            w.lock();
            // If pool is stopping, ensure thread is interrupted;
            // if not, ensure thread is not interrupted.  This
            // requires a recheck in second case to deal with
            // shutdownNow race while clearing interrupt
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                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);
                } finally {
                    afterExecute(task, thrown);
                }
            } finally {
                task = null;
                //完成任务数加一
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        //1. completedAbruptly为true,代表异常,则工作线程数减一
        //2. completedTaskCount += w.completedTasks; workers.remove(w);
        //3. tryTerminate();尝试停止线程池,正常运行的线程池调用该方法不会有任何动作
        //4. 如果线程池没有被关闭的话,Worker也不是异常退出,并且Worker线程数小于最小值(分类情况见源码),则新建一个Worker线程:addWorker(null, false);
        processWorkerExit(w, completedAbruptly);
    }
}

shutdown()

public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        //检查是否可以操作目标线程
        checkShutdownAccess();
        //设置状态为SHUTDOWN
        advanceRunState(SHUTDOWN);
        //中断所有空闲线程
        interruptIdleWorkers();
        onShutdown(); // hook for ScheduledThreadPoolExecutor
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
}

接着看下 interruptIdleWorkers() 方法:

private void interruptIdleWorkers() {
    interruptIdleWorkers(false);
}
 
private void interruptIdleWorkers(boolean onlyOne) {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        for (Worker w : workers) {
            Thread t = w.thread;
            //中断所有的空闲线程(正在从 workQueue 中取 Task,此时 Worker 没有加锁)
            if (!t.isInterrupted() && w.tryLock()) {
                try {
                    t.interrupt();
                } catch (SecurityException ignore) {
                } finally {
                    w.unlock();
                }
            }
            if (onlyOne)
                break;
        }
    } finally {
        mainLock.unlock();
    }
}

shutdownNow()

public List<Runnable> shutdownNow() {
    List<Runnable> tasks;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        advanceRunState(STOP);
        //中断所有的线程
        interruptWorkers();
        //拒绝所有新Task的加入,WorkerQueue中没有执行的线程全部抛弃。所以此时Pool是空的,WorkerQueue也是空的。
        //获取所有没有执行的Task,并且返回。
        tasks = drainQueue();
    } finally {
        mainLock.unlock();
    }
    //进行到TIDYING和TERMINATED的转化
    tryTerminate();
    return tasks;
}

总结

本文从 ThreadPoolExecutor 的类继承体系,到初始化参数详解,再到状态转换以及重要方法的解读,由浅入深的介绍了 ThreadPoolExecutor。通过本文可以对线程池的运行原理有一个最基本的理解。

SpringBoot 源码解析二

前言

要说 SpringBoot 有什么优点的话,比较核心的就是简化配置和自动配置了。前面我们分析了 SpringBoot 的启动流程还有一些监听器相关的源码,
今天来分析下 SpringBoot 的自动配置具体怎么实现的,以便日后遇到相关错误知道从哪里入手。

自动化配置报错

初学 SpringBoot 的我们在启动自己的第一个 SpringBoot demo 的时候,可能都遇到了以下这个报错:

Description:
 
Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.
 
Reason: Failed to determine a suitable driver class
 
 
Action:
 
Consider the following:
	If you want an embedded database (H2, HSQL or Derby), please put it on the classpath.
	If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active).

日志意思很明显是配置 datasource 的时候出错了,我们都没有配置 datasource 呢怎么就报错了。当时心想什么垃圾 SpringBoot!网上搜索以下,在启动类上添加一个注解:

@EnableAutoConfiguration(exclude = DataSourceAutoConfiguration.class)

或者去掉 pom 文件中的一个依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

然后启动成功后暗自高心了一把,SpringBoot 也没什么嘛,这么简单!

自动化配置解析

@EnableAutoConfiguration

上面说到 @EnableAutoConfiguration 注解意思开启自动配置。源码如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
    
    Class<?>[] exclude() default {};
    
    String[] excludeName() default {};
}

关键点在于用 @import 导入的 AutoConfigurationImportSelector。该类实现了 ImportSelector 接口,这个接口只有一个方法,代码如下:

public interface ImportSelector {
 
	/**
	 * Select and return the names of which class(es) should be imported based on
	 * the {@link AnnotationMetadata} of the importing @{@link Configuration} class.
	 */
	String[] selectImports(AnnotationMetadata importingClassMetadata);
 
}

注释写的很清楚,这个方法返回的 name 对应的 bean 都是要注入到 Spring 容器里的。然后我们再看实现方法:

@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
    if (!isEnabled(annotationMetadata)) {
        return NO_IMPORTS;
    }
    AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
            .loadMetadata(this.beanClassLoader);
    AnnotationAttributes attributes = getAttributes(annotationMetadata);
    // 重点看这,获取要自动配置的那些类名
    List<String> configurations = getCandidateConfigurations(annotationMetadata,
            attributes);
    // 去掉重复的
    configurations = removeDuplicates(configurations);
    Set<String> exclusions = getExclusions(annotationMetadata, attributes);
    checkExcludedClasses(configurations, exclusions);
    // 去掉我们手动排除在外的类
    configurations.removeAll(exclusions);
    configurations = filter(configurations, autoConfigurationMetadata);
    fireAutoConfigurationImportEvents(configurations, exclusions);
    return StringUtils.toStringArray(configurations);
}

其中参数 AnnotationMetadata 封装了注解的相关信息。
这个方法具体在什么调用,我们可以一层层往上看,会发现是在刷新上下文的时候调用。
现在直接看重点 getCandidateConfigurations(annotationMetadata, attributes); 代码如下:

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
        AnnotationAttributes attributes) {
    List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
            getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
    Assert.notEmpty(configurations,
            "No auto configuration classes found in META-INF/spring.factories. If you "
                    + "are using a custom packaging, make sure that file is correct.");
    return configurations;
}

代码很熟悉,在前一篇分析启动流程的时候看到过,主要就是从 META-INF/spring.factories 文件中获取。spring-boot-autoconfigure 包下这个文件中
内置了很多要自动配置的类(但是这些类并不会全部配置,例如前面说到的可以通过 @EnableAutoConfiguration(exclude = XXX.class) 去排除),其中就包括一个叫 DataSourceAutoConfiguration 的类。下面以这个类举例。

DataSourceAutoConfiguration

这个类上有几个注解:

@Configuration
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ DataSourcePoolMetadataProvidersConfiguration.class,
		DataSourceInitializationConfiguration.class })
  1. @configuration 表示这是一个配置类。
  2. @ConditionalOnClass 是一个条件,当存在指定的类时该条件才满足,当前类才配置。
  3. @EnableConfigurationProperties 启用配置属性。
  4. @import 导入指定的类。

注意这里 @ConditionalOnClass 条件中的 EmbeddedDatabaseType 类在 spring-jdbc 包里,这也是为什么前面说到去掉 pom 文件里 jdbc 的依赖就不会报错了。因为去掉 jdbc 的依赖后,这里的 @ConditionalOnClass 不满足,DataSourceAutoConfiguration 也就不会自动配置了。

接着看为什么 pom 文件里有 jdbc 依赖并且不配置 datasource 相关信息时就会报错呢?

先看 DataSourceAutoConfiguration 的一个内部类:

@Configuration
@Conditional(PooledDataSourceCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
        DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.Generic.class,
        DataSourceJmxConfiguration.class })
protected static class PooledDataSourceConfiguration {
 
}

这里导入 DataSourceConfiguration 的几个内部类 Hikari、Tomcat、Dbcp2、Generic。但是这里只有 Hikari 上的条件注解是满足的,其相关代码如下:

protected static <T> T createDataSource(DataSourceProperties properties,
                                        Class<? extends DataSource> type) {
    return (T) properties.initializeDataSourceBuilder().type(type).build();
}
 
/**
 * Hikari DataSource configuration.
 */
@ConditionalOnClass(HikariDataSource.class)
@ConditionalOnMissingBean(DataSource.class)
@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource", matchIfMissing = true)
static class Hikari {
 
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.hikari")
    public HikariDataSource dataSource(DataSourceProperties properties) {
        // 创建 HikariDataSource
        HikariDataSource dataSource = createDataSource(properties,
                                                       HikariDataSource.class);
        if (StringUtils.hasText(properties.getName())) {
            dataSource.setPoolName(properties.getName());
        }
        return dataSource;
    }
 
}

DataSourceProperties 中相关代码如下:

public DataSourceBuilder<?> initializeDataSourceBuilder() {
    return DataSourceBuilder.create(getClassLoader()).type(getType())
        .driverClassName(determineDriverClassName()).url(determineUrl())
        .username(determineUsername()).password(determinePassword());
}
 
public String determineDriverClassName() {
    if (StringUtils.hasText(this.driverClassName)) {
        Assert.state(driverClassIsLoadable(),
                     () -> "Cannot load driver class: " + this.driverClassName);
        return this.driverClassName;
    }
    String driverClassName = null;
    if (StringUtils.hasText(this.url)) {
        driverClassName = DatabaseDriver.fromJdbcUrl(this.url).getDriverClassName();
    }
    if (!StringUtils.hasText(driverClassName)) {
        driverClassName = this.embeddedDatabaseConnection.getDriverClassName();
    }
    // 这里就是前言自动化配置报错的地方,同理配置了驱动但是不配置url也会报错
    if (!StringUtils.hasText(driverClassName)) {
        throw new DataSourceBeanCreationException(
            "Failed to determine a suitable driver class", this,
            this.embeddedDatabaseConnection);
    }
    return driverClassName;
}

当我们在配置文件中配置了相关数据源信息后,这里就会创建一个 HikariDataSource。

最后 DataSourceAutoConfiguration 上还会通过注解注入其他的相关配置类,例如后置处理器什么的。

自定义 starter

springboot 开箱即用的特点是因为它针对现在的开发技术提供了很多现成的starter,我们可以理解为一种服务。

自动化配置功能就是 spring-boot-autoconfigure 这个 starter 为我们提供的。下面我们自定义一个 starter 看看它是怎么实现的。

创建一个 starter 项目

首先我们需要创建一个简单的自动配置类 MyRunnableAutoConfiguration 和一个 MyRunnable 类:

@Configuration
@ConditionalOnClass(MyRunnable.class)
public class MyRunnableAutoConfiguration {
 
    @Value("${myRunnableName:#{null}}")
    String myRunnableName;
 
    @Bean
    public MyRunnable myRunnable() {
        return new MyRunnable(myRunnableName);
    }
}
public class MyRunnable implements Runnable {
 
    private static final String DEFAULT_NAME = "myRunnable";
 
    private String name;
 
    public MyRunnable(String name) {
        this.name = (name == null ? DEFAULT_NAME : name);
    }
 
    @Override
    public void run() {
        System.out.println(this.toString());
    }
 
    @Override
    public String toString() {
        return "MyRunnable{" +
            "name='" + name + '\'' +
            '}';
    }
}
  • 这里 @ConditionalOnClass 条件很明显是满足的。

  • 然后用 @value 注入我们配置的名称,没有则是 null。

  • 最后创建一个 MyRunnable 对象。

引入自定义 starter 并测试 MyRunnable

我们在原来的项目中添加自定义的 starter 依赖:

<!-- 自定义 starter -->
<dependency>
	<groupId>com.lollipop</groupId>
	<artifactId>my-starter</artifactId>
	<version>0.0.1-SNAPSHOT</version>
</dependency>

然后在 resources 目录下创建目录 META-INF, 再在里面创建 spring.factories 文件并配置:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.lollipop.starter.MyRunnableAutoConfiguration

最后在启动类中添加测试代码:

ConfigurableApplicationContext context = application.run(args);
//获取 my-starter 项目中定义的bean runnable
Runnable runnable = (Runnable) context.getBean("myRunnable");
runnable.run();

启动项目,控制台打印MyRunnable{name='myRunnable'}。证明我们自定义的 starter 中的自动配置类配置 MyRunnable 成功。名称默认为 myRunnable。

我们在 yml 配置文件中添加配置:

myRunnableName: myRunnable12345

重新启动,控制台打印MyRunnable{name='myRunnable12345'}。证明我们的配置生效。

总结

至此,我们分析了 spring-boot-autoconfigure 是怎么通过 @EnableAutoConfiguration 注解帮我完成自动配置的。以及实现了一个自定义的 starter 并自动配置我们需要的 bean。

装饰模式 and 代理模式

你可能看到标题会觉得这两个模式有啥关系啊,很好,这两个模式确实没有关系哈哈,但是我今天把这两个模式放在一起说还是有原因的。

举个栗子

装饰模式

Component:抽象构建
ConcreteComponent:具体构建类(被装饰类)
Decorator:装饰类

interface Component{
    void method();
}
class ConcreteComponent implements Component{
    public void method(){
        System.out.println("method");
    }
}
class Decorator implements Component{
    private Component component;
    public Decorator(Component component){
        this.component = component;
    }
    public void method(){
        System.out.println("PreDecorate");
        component.method();
        System.out.println("PostDecorate");
    }
}
public class Test {
    public static void main(String args[]){
        ConcreteComponent concreteComponent = new ConcreteComponent();
        Decorator decorator = new Decorator(concreteComponent);
        decorator.method();
    }
}

代理模式

interface Subject{
    void request();
}
class RealSubject implements Subject{
    public void request(){
        System.out.println("request");
    }
}
class Proxy implements Subject{
    private RealSubject realSubject;
    public Proxy(){
        this.realSubject = new RealSubject();
    }
    public void request(){
        System.out.println("PreProcess");
        realSubject.request();
        System.out.println("PostProcess");
    }
}
public class ProxyDemo {
    public static void main(String args[]){
        Proxy p = new Proxy();
        p.request();
    }
}

栗子分析

上面的栗子中,第一个是装饰模式,第二个代理模式,在接口的实现上面以至于整个代码上面几乎都没有多大的差别。以至于很多刚开始学习设计模式的同学看的有点晕,分不清两个的区别在哪。我们先来看下代码上的区别在哪:

第一处区别

在客户端代码(也就是此处的测试代码)中,装饰模式先创建一个具体构建类(被装饰类),然后作为参数通过构造器创建一个装饰类。而代理模式中,直接创建一个代理类,此处你看不见被代理类是什么,也就是说被代理类对于客户端是透明的。

第二处区别

装饰类中,将客户端代码中传递过来了的被装饰类赋给装饰类持有的抽象构建引用
代理类中,持有的是被代理类引用,且在构造函数中创建一个被代理类实例。

比较分析

这两个区别说的仅仅是第一个栗子中代码上的区别,但是至少可以看出来一点,被代理对象对于客户端来说是透明的。

再举一栗

装饰模式

interface Arrangement {
    public void show();
}
class Person implements Arrangement {
    private String name;

    public Person(String name){
        this.name = name;
    }

    @Override
    public void show() {
        System.out.println(name + "的搭配:");
    }
}
abstract class Dress implements Arrangement {
    private Arrangement arrangement;
    public void Dress(Arrangement arrangement){
        this.arrangement = arrangement;
    }
    @Override
    public void show() {
        if (arrangement != null){
            arrangement.show();
        }
    }
}
class DKC extends Dress {
    @Override
    public void show() {
        super.show();
        System.out.println("大裤衩");
    }
}
class PX extends Dress {
    @Override
    public void show() {
        super.show();
        System.out.println("皮鞋");
    }
}
public class Test {
    public static void main(String[] args) {
        Person person = new Person("小明");
        DKC dkc = new DKC();
        PX px = new PX();

        dkc.Dress(person);
        px.Dress(dkc);
        px.show();
    }
}

代理模式

Girl:被追求的妹子
GiveGift:送礼物
Suitor:真实的追求者(由于比较害羞,找了一个代理帮自己追)
Proxy:代理类

class Girl {
    private String name;
    public Girl(String name){
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
interface GiveGift {
    public void GiveFlowers();
    public void GiveDove();
    public void GiveDolls();
}
class Suitor implements GiveGift {
    private Girl girl;
    public Suitor(Girl girl){
        this.girl = girl;
    }

    @Override
    public void GiveFlowers() {
        System.out.println(girl.getName()+",送你花!");
    }

    @Override
    public void GiveDove() {
        System.out.println(girl.getName()+",送你巧克力!");
    }

    @Override
    public void GiveDolls() {
        System.out.println(girl.getName()+",送你洋娃娃!");
    }
}
public class Proxy implements GiveGift {
    private Suitor suitor;
    public Proxy(Girl girl){
        this.suitor = new Suitor(girl);
    }

    @Override
    public void GiveFlowers() {
        suitor.GiveFlowers();
    }

    @Override
    public void GiveDove() {
        suitor.GiveDove();
    }

    @Override
    public void GiveDolls() {
        suitor.GiveDolls();
    }

    public static void main(String[] args) {
        Girl girl = new Girl("marry");
        Proxy proxy = new Proxy(girl);
        proxy.GiveDolls();
        proxy.GiveDove();
        proxy.GiveFlowers();
    }
}

结合这个栗子再来说明一下两个模式分别是什么。

装饰模式是什么

装饰模式可以说就是在已有功能的基础上添加更多的功能,把每个要添加的功能都放在一个单独的类中,并让这个类包装被装饰对象,然后在执行特定任务时,客户端代码就可以在运行时根据需要自由组合,按顺序地使用这些装饰功能。

代理模式是什么

代理模式就是为一个对象提供一个代理并控制着对这个对象的访问。并且被代理对象对于客户端代码是透明的。就像最后这个栗子中,客户端代码并不知道真实的追求者是谁。代理类控制着真实追求着的访问,当然也可以添加一些功能什么的(就像装饰模式那样)。

总结

现在理解了两个模式之后,就会发现两个模式的目的并没有什么关联。完全没必要纠结像第一个栗子中的代码相似问题。要说真实应用的话,JDK的IO包中的BufferedInputstream就是一个装饰类,Spring的AOP就用的是代理,不过是动态代理而已。
最后推荐学习设计模式的小伙伴们有兴趣可以看一下《大话设计模式》这本书,通过讲故事的形式把道理给讲明白了,个人还是觉得蛮好的。

SpringMVC容器那点事

SpringMVC容器

之前分析了过 Spring 的启动过程了,今天看下 SpringMVC 的启动。一样的,我们先看下 web.xml,SpringMVC 是以 Servlet 配置出现的

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>
      classpath:spring-application.xml
    </param-value>
  </context-param>

  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>

<servlet>
	<servlet-name>DispatcherServlet</servlet-name>
	<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	<init-param>
	  <param-name>contextConfigLocation</param-name>
	  <param-value>classpath:spring-mvc.xml</param-value>
	</init-param>
	<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
	<servlet-name>DispatcherServlet</servlet-name>
	<url-pattern>*.do</url-pattern>
</servlet-mapping>

之前分析了 ContextLoaderListener,实例化 IoC 容器,并将此容器实例注册到 ServletContext 中。我们先看下 DispatcherServlet 的类图及继承关系:

SpringMVC最核心的类就是 DispatcherServlet, 关于 Spring Context 的配置文件加载和创建是在 init() 方法中进行的,主要的调用顺序是 init-->initServletBean-->initWebApplicationContext 。 先来看一下 initWebApplicationContext 的实现:FrameworkServlet.java

先简单说下这些代码的功能:
514行:从 ServletContext 中获取 rootContext也就是SpringIOC容器
517行:如果一个 context 的实例被注入了,直接使用
538行:从 ServletContext 中获取 webApplicationContext也就是SpringMVC容器
543行:创建 SpringMVC 的容器,并将 rootContext 作为父容器
550行:刷新上下文(执行组件的初始化),这个方法由子类DispatchServlet的方法实现
556行:将 SpringMVC 容器作为属性设置进 ServletContext
这里多说一句,SpringMVC 容器在 ServletContext 中的属性名:

public String getServletContextAttributeName() {
	return SERVLET_CONTEXT_PREFIX + getServletName();
}

public static final String SERVLET_CONTEXT_PREFIX = FrameworkServlet.class.getName() + ".CONTEXT.";

而 SpringIOC 容器在 ServletContext 中的属性名:

String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT";

前面的没什么好说的,我们看下 onRefresh() 方法,调用了 initStrategies() 方法:

执行 MVC 的相关组件的初始化,我们以 HandlerMappings 为例看来看下:

detectAllHandlerMappings 默认为 true,从当前的 SpringMVC 容器及其父容器中查找所有的 HandlerMappings,否则只从当前的 SpringMVC 容器中查找 HandlerMapping,如果没有找到 handlerMappings,设置默认的 handlerMapping,默认值设置在 DispatcherServlet 同级目录的 DispatcherServlet.properties 中。

多说一句

上面的 findWebApplicationContext(),createWebApplicationContext(rootContext) 之类的方法点进去看看也很容易懂,我就不贴源码了,然后 createWebApplicationContext 中会层层调用直到 AbstractApplicationContext的 refresh 方法来初始化 bean,这个方法在之前分析 Spring 启动的时候看过,这里也就不看了。

还是那句话,以我现在水平分析源码并不指望能看懂并理解每一句每一行,但是看不懂的方法你就点进去看看,万一里面里面的东西你看过呢是不是,就怕看不懂然后觉得这行代码不重要就不看了。

嗯?说完了?怎么感觉看完之前的Spring容器那点事,再看这个好像也没什么了。我们再来简单说下 Spring 容器和 SpringMVC 容器的py(手动滑稽)关系。

Spring容器 和 SpringMVC容器的关系

ContextLoaderListener 中创建 ApplicationContext(SpringIOC容器)主要用于整个 Web 应用程序需要共享的一些组件 ,比如 DAO,数据库的 ConnectionFactory 等。而由 DispatcherServlet 创建的 ApplicationContext(SpringMVC容器)主要用于和该 Servlet 相关的一些组件 ,比如 Controller、ViewResovler 等。

对于作用范围而言, 在 DispatcherServlet 中可以引用由 ContextLoaderListener 所创建的 ApplicationContext ,而反过来不行。
在 Spring 的具体实现上,这两个 ApplicationContext 都是通过 ServletContext 的 setAttribute 方法放到 ServletContext 中的。但是, ContextLoaderListener 会先于 DispatcherServlet 创建 ApplicationContext,DispatcherServlet 在创建 ApplicationContext 时会先找到由 ContextLoaderListener 所创建的 ApplicationContext,再将后者的 ApplicationContext 作为参数传给 DispatcherServlet 的 ApplicationContext 的 setParent() 方法,

wac.setParent(parent);

其中, wac 即为由 DisptcherServlet 创建的 ApplicationContext,而 parent 则为 ContextLoaderListener 创建的ApplicationContext 。此后,框架又会调用 ServletContext 的 setAttribute() 方法将 wac 加入到 ServletContext 中。

当 Spring 在执行 ApplicationContext 的 getBean 时, 如果在自己 context 中找不到对应的 bean,则会在父容器中去找 。这也解释了为什么我们可以在 DispatcherServlet 中获取到由 ContextLoaderListener 对应的 ApplicationContext 中的 bean。举个例子就是,你可以在 controller 层中注入 service 层的 bean。

《深入理解Java虚拟机》之Java内存区域

Java内存区域

JAVA 虚拟机运行时数据区如图所示:

运行时数据区域

程序计数器

  • 程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
  • 为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的程序计数器互不影响,独立存储,我们称这一类内存区域为“线程私有”的内存。
  • 如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器的值为空(undefined)。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

Java 虚拟机栈

  • 虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应一个栈帧在虚拟机栈中入栈到出栈的过程。
  • 局部变量表存放了编译器可知的各种基本数据类型、对象引用( reference 类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是一个指向对象的句柄或其它与此对象相关的位置)和returnAddress 类型(指向一条字节码指令的地址)。
  • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;如果虚拟机栈可以动态扩展,如果扩展时申请不到足够的内存,就会抛出 OutOfMemoryError 异常。

本地方法栈

  • 本地方法栈和虚拟机栈的作用是非常相似的,它们之间的区别只不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。
  • 在虚拟机规范中对本地方法栈中方法使用的语言、使用方式和数据结构没有强制规定,因此具体的虚拟机可以自由实现。有的虚拟机(例如HotSpot)直接就把本地方法栈和虚拟机栈合二为一。
  • 与虚拟机栈一样,本地方法栈也会抛出 StackOverflowError 和 OutOfMemoryError异常。

Java堆

  • 对于大多数应用来说,Java 堆是 Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。Java 虚拟机规范中描述:所有的对象实例和数组都要在堆上分配,但随着 JIT 编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配在堆上也渐渐变得不是那么绝对。
  • Java 堆上各个区域的分配和回收下面进行讲解。
  • 在实现时,既可以实现成固定大小的,也可以实现成可扩展的,当前主流的虚拟机都是按照可扩展的来实现的(通过-Xmx 和 -Xms控制),如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常。

方法区

  • 方法区和 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
  • 当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。

运行时常量池

  • 运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
  • 运行时常量池相对于 Class 文件常量池的一个重要特征是具备动态性,Java 语言并不要求常量一定只有编译期才能产生,也就是并非预置Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是 String 类的 intern() 方法。
  • 当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。

HotSpot 虚拟机对象揭秘

对象的创建

  • 虚拟机遇到一条 new 指令时,首先将去检查这个质量的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
  • 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。在使用 Serial、ParNew 等带 Compact 过程的收集器时,系统采用的分配算法是指针碰撞,而使用 CMS 这种基于 Mark-Sweep 算法的收集器时,通常采用空闲列表。
  • 除如何划分可用空间外,还有另一个需要考虑的问题是对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的。解决这个问题有两种方案:一种是对分配内存空间的动作进行同步处理,实际上虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性;另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在 Java 堆中先预先分配一小块内存,成为本地线程分配缓冲(TLAB),虚拟机是否使用 TLAB ,可以通过 -XX:+/-UseTLAB 参数来设定。
  • 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用 TLAB ,这一工作过程也可以提前至 TLAB 分配时进行。这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就可以直接使用,程序能访问到这些字段的数据类型所对应的零值。
  • 接下来,虚拟机要对对象进行必要的设置。例如这个对象是哪个类的实例、如何才能找到类的元信息、对象的哈希码、对象的 GC 分代年龄等信息。这些信息存放在对象的对象头之中。
  • 经过上面的步骤,从虚拟机的视角来看,一个新的对象已经产生,但从 Java 程序的角度来看,对象创建才刚刚开始————<init>方法还没有执行,所有的字段还都为零。所以一般来说(由字节码中是否跟随 invokespecial 指令所决定),执行 new 指令之后会接着执行 init 指令,把对象按照我们的意愿进行初始化,这样一个真正有用的对象才算完全产生出来。

对象的内存布局

  • 在 HotSpot 虚拟机中,对象在内存中的存储的布局可以分为3块区域:对象头、实例数据和对齐填充。
  • HotSpot 虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启指针压缩)中分别为32bit 和 64bit ,官方称之为“Mark Word”。
  • 对象头的另一部分是类型指针。即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。如果对象是一个 Java 数组,那么在对象头中还必须有一块用于记录数组长度的数据。
  • 实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。这部分的存储顺序会受到虚拟机分配策略和字段在 Java 源码中定义的顺序影响。HotSpot 虚拟机默认的分配策略为 longs/doubles、ints、shorts/chars、bytes/booleans、oops,从分配策略中可以看出,相同宽度的字段总是被分配到一起。在满足这个条件的情况下,父类中定义的变量会出现在子类之前。如果 CompactFields 的参数值为 true(默认为true),那么字类中较窄的变量也可能会插入到父类的变量空隙之中。
  • 最后对齐填充部分并不是必然存在的。仅仅起着占位符的作用。由于 HotSpot 虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,对象的大小必须是8字节的整数倍。而对象头部正好是8字节的整数倍(1倍或者2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

对象的访问定位

目前主流的访问方式有句柄和直接指针两种:

  • 如果使用句柄访问的话,那么 Java 堆中将会划分一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象的实例数据与类型数据各自的具体地址信息。如图所示:

  • 如果使用直接指针访问,那么 Java 堆对象的布局就必须考虑如何放置访问类型数据的相关信息。而reference 中存储的直接就是对象地址。如图所示

  • 使用句柄来访问的最大好处就是 reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集时对象移动是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference 本身并不需要改变。
  • 使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在 Java 中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。就 HotSpot 而言,使用的是第二种方式进行对象访问的,但从整个软件开发的范围来看,各种语言和框架使用句柄来访问的情况也十分常见。

Netty 客户端启动源码

Netty 客户端启动代码基本如下所示,客户端启动时 Netty 内部为我们做了什么?

  • 客户端的线程模型是如何设置的?
  • 客户端的 channel 是如何创建并初始化的?
  • 具体又是怎么去 connect 服务端的?

带着以上几个问题,下面我们来一探究竟。

EventLoopGroup group = new NioEventLoopGroup();
try {
    Bootstrap b = new Bootstrap();
    //配置客户端 NIO 线程组
    b.group(group)
            // 设置 IO 模型
            .channel(NioSocketChannel.class)
            // 设置 TCP 参数
            .option(ChannelOption.TCP_NODELAY, true)
            // 设置业务处理 handler
            .handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel socketChannel) throws Exception {
                    socketChannel.pipeline().addLast(new MyClientHandler());
                }
            });
    //发起异步连接请求
    ChannelFuture future = b.connect(host, port).sync();
    future.channel().closeFuture().sync();
} finally {
    group.shutdownGracefully();
}

核心概念

  • Bootstrap

    作为抽象辅助类 AbstractBootstrap 的子类,Bootstrap 主要提供给客户端使用;

    基于此进行一些 group、channel、option 和 handler 的配置

  • NioSocketChannel

    封装了 jdk 的 SocketChannel,且存在一个属性 readInterestOp 记录对什么事件感兴趣,初始化时为 SelectionKey.OP_READ
    以及继承自父类 AbstractChannel 的三大属性 id(全局唯一标识)、unsafe(依附于 channel,负责处理操作) 和 pipeline(责任链模式处理请求);

  • Unsafe

    Unsafe 附属于 Channel,大部分情况下仅在内部使用;

  • ChannelPipeline

    初始化 DefaultChannelPipeline 时,设置 HeadContext、TailContext 两个节点组成双向链表结构,两个节点均继承自 AbstractChannelHandlerContext;

    换句话说 ChannelPipeline 就是一个双向链表 (head ⇄ handlerA ⇄ handlerB ⇄ tail),链表的每个节点是一个 AbstractChannelHandlerContext;

  • NioEventLoopGroup

    有个属性 EventExecutor[],而下面要说的 NioEventLoop 就是 EventExecutor;

    客户端只存在一个 NioEventLoopGroup;

  • NioEventLoop

    核心线程(其实看源码的话,好像理解成一个 task 更合理),含有重要属性 executor、taskQueue、selector 等;

创建 NioEventLoopGroup

创建 NioEventLoopGroup 的时候最终调用其父类 MultithreadEventExecutorGroup 的构造方法,主要代码如下:

protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
                                            EventExecutorChooserFactory chooserFactory, Object... args) {
        if (executor == null) {
            // 后面会将 NioEventLoop run 方法包装成一个 task,然后交给这个 executor 执行,
            // 而这个 executor 仅仅启动一个新线程去执行该 task,这个执行线程就是IO核心线程
            executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
        }
        // 初始化 EventExecutor[]
        children = new EventExecutor[nThreads];

        for (int i = 0; i < nThreads; i ++) {
            boolean success = false;
            try {
                // 调用 NioEventLoopGroup 的 newChild 方法创建 NioEventLoop,此时尚未创建核心线程
                children[i] = newChild(executor, args);
                success = true;
            } catch (Exception e) {
                // TODO: Think about if this is a good exception type
                throw new IllegalStateException("failed to create a child event loop", e);
            } finally {
                if (!success) {
                    ...
                }
            }
        }
        // chooser 主要是用来从 EventExecutor[] 中选取一个 NioEventLoop
        chooser = chooserFactory.newChooser(children);
    }

创建 NioEventLoopGroup 主要做了以下几件事:

  1. 没有 executor 则创建一个 executor
  2. 初始化一个 EventExecutor[],为创建 NioEventLoop 做准备
  3. 循环调用 newChild 方法(传入 executor )去创建 NioEventLoop 对象,置于 EventExecutor[] 数组当中
  4. 创建一个 chooser,用来从 EventExecutor[] 选取处一个 NioEventLoop

创建 NioEventLoop

创建 NioEventLoop 的时候会调用其父类 SingleThreadEventExecutor 的构造方法,主要代码如下:

protected SingleThreadEventExecutor(EventExecutorGroup parent, Executor executor,
                                    boolean addTaskWakesUp, int maxPendingTasks,
                                    RejectedExecutionHandler rejectedHandler) {
    super(parent);
    this.addTaskWakesUp = addTaskWakesUp;
    this.maxPendingTasks = Math.max(16, maxPendingTasks);
    this.executor = ObjectUtil.checkNotNull(executor, "executor");
    // 任务队列
    taskQueue = newTaskQueue(this.maxPendingTasks);
    rejectedExecutionHandler = ObjectUtil.checkNotNull(rejectedHandler, "rejectedHandler");
}
 
NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,
             SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) {
    super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler);
    if (selectorProvider == null) {
        throw new NullPointerException("selectorProvider");
    }
    if (strategy == null) {
        throw new NullPointerException("selectStrategy");
    }
    provider = selectorProvider;
    final SelectorTuple selectorTuple = openSelector();
    // 之后要把 channel 注册在 selector 上
    selector = selectorTuple.selector;
    unwrappedSelector = selectorTuple.unwrappedSelector;
    selectStrategy = strategy;
}

创建 NioEventLoop 主要做了以下几件事:

  1. 创建任务队列,以后调用 NioEventLoop.execute(Runnable task) 的时候均是把 task 放入该任务队列
  2. 创建一个 selector,之后要把 channel 注册到该 selector 上

设置 Bootstrap

这一步对 Bootstrap 进行配置,无论是 group、channel 还是 option、handler,均是继承的 AbstractBootstrap 的属性;
对其的配置目前也仅仅是在 AbstractBootstrap 中配置属性,尚未做过多的事情;

设置 NIO 线程组

// AbstractBootstrap.group(group)
public B group(EventLoopGroup group) {
    if (group == null) {
        throw new NullPointerException("group");
    }
    if (this.group != null) {
        throw new IllegalStateException("group set already");
    }
    this.group = group;
    return self();
}

设置 IO 模型

// AbstractBootstrap.channel(channelClass)
public B channel(Class<? extends C> channelClass) {
    if (channelClass == null) {
        throw new NullPointerException("channelClass");
    }
    // channelClass 就是上面传进来的 NioServerSocketChannel.class
    // 创建一个持有 channelClass 的 channelFactory 并保存,后面用来反射创建 NioServerSocketChannel 对象
    return channelFactory(new ReflectiveChannelFactory<C>(channelClass));
}

设置 TCP 参数

// AbstractBootstrap.option(option, value)
public <T> B option(ChannelOption<T> option, T value) {
    if (option == null) {
        throw new NullPointerException("option");
    }
    if (value == null) {
        synchronized (options) {
            options.remove(option);
        }
    } else {
        synchronized (options) {
            options.put(option, value);
        }
    }
    return self();
}

设置业务处理 handler

// AbstractBootstrap.handler(handler)
public B handler(ChannelHandler handler) {
    if (handler == null) {
        throw new NullPointerException("handler");
    }
    this.handler = handler;
    return self();
}

发起连接请求

封装好 SocketAddress 后调用 doResolveAndConnect 方法,代码如下:

// Bootstrap.doResolveAndConnect(remoteAddress, localAddress)
private ChannelFuture doResolveAndConnect(final SocketAddress remoteAddress, final SocketAddress localAddress) {
    // 同服务端一样,先初始化一个 channel 然后注册到 selector 上
    final ChannelFuture regFuture = initAndRegister();
    final Channel channel = regFuture.channel();

    // 因为上面注册是 封装 task 丢给 eventLoop 去执行的,也就是说它是异步的;
    // 这里根据 future 判断是否注册完成了
    if (regFuture.isDone()) {
        if (!regFuture.isSuccess()) {
            return regFuture;
        }
        // 发起连接请求
        return doResolveAndConnect0(channel, remoteAddress, localAddress, channel.newPromise());
    } else {
        // Registration future is almost always fulfilled already, but just in case it's not.
        final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
        regFuture.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                // Directly obtain the cause and do a null check so we only need one volatile read in case of a
                // failure.
                Throwable cause = future.cause();
                if (cause != null) {
                    // Registration on the EventLoop failed so fail the ChannelPromise directly to not cause an
                    // IllegalStateException once we try to access the EventLoop of the Channel.
                    promise.setFailure(cause);
                } else {
                    // Registration was successful, so set the correct executor to use.
                    // See https://github.com/netty/netty/issues/2586
                    promise.registered();
                    // 发起连接请求
                    doResolveAndConnect0(channel, remoteAddress, localAddress, promise);
                }
            }
        });
        return promise;
    }
}

初始化 channel(NioSocketChannel)

// AbstractBootstrap.initAndRegister()
final ChannelFuture initAndRegister() {
    Channel channel = null;
    try {
        // 首先通过 channelFactory 用反射创建一个 NioSocketChannel 对象(封装了 jdk 的 SocketChannel 和 SelectionKey.OP_READ)
        channel = channelFactory.newChannel();
        // 初始化该 channel 对象
        init(channel);
    } catch (Throwable t) {
        if (channel != null) {
            // channel can be null if newChannel crashed (eg SocketException("too many open files"))
            channel.unsafe().closeForcibly();
            // as the Channel is not registered yet we need to force the usage of the GlobalEventExecutor
            return new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE).setFailure(t);
        }
        // as the Channel is not registered yet we need to force the usage of the GlobalEventExecutor
        return new DefaultChannelPromise(new FailedChannel(), GlobalEventExecutor.INSTANCE).setFailure(t);
    }
    // 初始化后将 channel 注册到 selector 上,核心方法
    // 这里的 group() 方法返回 NioEventLoopGroup,它的 register 方法从数组中选出一个 NioEventLoop,然后将 channel 注册到它对应的 selector 上
    ChannelFuture regFuture = config().group().register(channel);
    if (regFuture.cause() != null) {
        if (channel.isRegistered()) {
            channel.close();
        } else {
            channel.unsafe().closeForcibly();
        }
    }
    return regFuture;
}

// Bootstrap.init(channel)
@Override
@SuppressWarnings("unchecked")
void init(Channel channel) throws Exception {
    ChannelPipeline p = channel.pipeline();
    // 将设置的业务 handler(demo 中是一个 ChannelInitializer)封装成 ChannelHandlerContext,添加到 pipeline 的 tail 节点前面
    p.addLast(config.handler());
    // 设置 option
    final Map<ChannelOption<?>, Object> options = options0();
    synchronized (options) {
        setChannelOptions(channel, options, logger);
    }
    // 设置 attr
    final Map<AttributeKey<?>, Object> attrs = attrs0();
    synchronized (attrs) {
        for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
            channel.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
        }
    }
}

注册 channel 到 selector

回到 initAndRegister() 方法中,在创建 channel 并调用 init(channel) 方法后,开始把 channel 注册到 selector 上,通过 EventLoopGroup 来注册;
NioEventLoopGroup 的 register(channel) 方法继承自父类 MultithreadEventLoopGroup

// NioEventLoopGroup.register(channel)
@Override
public ChannelFuture register(Channel channel) {
    // 所以这里变成了调用 NioEventLoop 的 register(channel) 方法
    return next().register(channel);
}

@Override
public EventLoop next() {
    // 父类则是通过 chooser.next() 来选取一个 NioEventLoop
    return (EventLoop) super.next();
}

NioEventLoop 的 register(channel) 方法继承自父类 SingleThreadEventLoop

// NioEventLoop.register(channel)
@Override
public ChannelFuture register(Channel channel) {
    // 这里根据 channel 和 NioEventLoop 自身创建了一个 DefaultChannelPromise
    return register(new DefaultChannelPromise(channel, this));
}

@Override
public ChannelFuture register(final ChannelPromise promise) {
    ObjectUtil.checkNotNull(promise, "promise");
    // unsafe() 方法返回 channel 对应的 AbstractUnsafe 对象,转为调用 AbstractUnsafe 的 register(eventLoop, promise) 方法
    promise.channel().unsafe().register(this, promise);
    return promise;
}

AbstractUnsafe 的 register(eventLoop, promise) 相关方法主要代码如下:

// AbstractUnsafe.register(eventLoop, promise)
@Override
public final void register(EventLoop eventLoop, final ChannelPromise promise) {
    // 给 channel 的 eventLoop 属性设值了
    AbstractChannel.this.eventLoop = eventLoop;
    // 如果当前是 eventLoop 线程调用的话则直接调用 register0(promise),否则封装成一个 task 丢给 eventLoop 去 execute
    // 正常来说,这里 eventLoop 线程还没有启动,第一次调用 eventLoop.execute(task) 的时候才会启动用 execute 启动 eventLoop 线程
    if (eventLoop.inEventLoop()) {
        register0(promise);
    } else {
        try {
            // 目前看来,这里是启动 eventLoop 线程的地方,下篇文章会对 eventLoop 线程的启动及执行做介绍,先不做深究
            // 把注册任务封装成 task 丢给 eventLoop 线程去执行
            eventLoop.execute(new Runnable() {
                @Override
                public void run() {
                    register0(promise);
                }
            });
        } catch (Throwable t) {
            logger.warn(
                    "Force-closing a channel whose registration task was not accepted by an event loop: {}",
                    AbstractChannel.this, t);
            closeForcibly();
            closeFuture.setClosed();
            safeSetFailure(promise, t);
        }
    }
}

// AbstractUnsafe.register0(promise)
private void register0(ChannelPromise promise) {
    try {
        // check if the channel is still open as it could be closed in the mean time when the register
        // call was outside of the eventLoop
        if (!promise.setUncancellable() || !ensureOpen(promise)) {
            return;
        }
        boolean firstRegistration = neverRegistered;
        // 最终注册的方法,在 AbstractNioChannel 中实现,其实也就是调用 jdk 的那个 SocketChannel 的 register 方法
        doRegister();
        neverRegistered = false;
        registered = true;

        // 注册已经算是成功了,是时候进行我们设置的 handlerAdded 方法了;
        // 然后此时会找到初始化 channel 的时候往 pipeline 中添加的那个 ChannelInitializer 的 handlerAdded 方法;
        // 接着会调用它重写的的 initChannel(ch) 方法,具体内容在上面初始化 channel 的时候说过了
        pipeline.invokeHandlerAddedIfNeeded();
        // 设置 promise 值
        safeSetSuccess(promise);
        // 开始在 pipeline 上一次调用 fireChannelRegistered
        pipeline.fireChannelRegistered();
        // Only fire a channelActive if the channel has never been registered. This prevents firing
        // multiple channel actives if the channel is deregistered and re-registered.
        // 这里还没有连接上服务端,所以不会执行
        if (isActive()) {
            if (firstRegistration) {
                pipeline.fireChannelActive();
            } else if (config().isAutoRead()) {
                // This channel was registered before and autoRead() is set. This means we need to begin read
                // again so that we process inbound data.
                //
                // See https://github.com/netty/netty/issues/4805
                beginRead();
            }
        }
    } catch (Throwable t) {
        // Close the channel directly to avoid FD leak.
        closeForcibly();
        closeFuture.setClosed();
        safeSetFailure(promise, t);
    }
}

触发 handlerAdded、ChannelRegistered 事件

在将 channel 注册到 selector 上后,进行 handlerAdded 事件的传播,如果客户端启动代码配置了 .handler(ChannelInitializer)的话,ChannelInitializer 的 handlerAdded 方法会调用重写 initChannel(socketChannel) 方法,该方法又会将我们实际的客户端处理 handler 按序添加到 pipeline 中。然后再进行 ChannelRegistered 在 pipeline 中的传播。

再次声明下,pipeline 是双向链表,节点是 ChannelHandlerContext,持有 handler 属性及 inbound、outbound 标识;

head 节点既是 inbound 节点也是 outbound 节点,tail 节点只是 inbound 节点;

像 ChannelRegistered 这种 inbound 事件,会从 pipeline 的 head 节点开始处理,然后按照链表顺序查找下一个 inbound 节点,依次处理直到 tail 节点

发起连接请求

在将 channel 注册到 selector 上后,对 remoteAddress 进行解析,解析完成后开始发起连接请求;
发起请求这个操作,也是封装成 task 丢给 eventLoop 线程去执行

// Bootstrap.doResolveAndConnect0(channel, remoteAddress, localAddress, promise)
private ChannelFuture doResolveAndConnect0(final Channel channel, SocketAddress remoteAddress,
                                           final SocketAddress localAddress, final ChannelPromise promise) {
    try {
        final EventLoop eventLoop = channel.eventLoop();
        final AddressResolver<SocketAddress> resolver = this.resolver.getResolver(eventLoop);

        if (!resolver.isSupported(remoteAddress) || resolver.isResolved(remoteAddress)) {
            // Resolver has no idea about what to do with the specified remote address or it's resolved already.
            doConnect(remoteAddress, localAddress, promise);
            return promise;
        }

        final Future<SocketAddress> resolveFuture = resolver.resolve(remoteAddress);

        if (resolveFuture.isDone()) {
            final Throwable resolveFailureCause = resolveFuture.cause();

            if (resolveFailureCause != null) {
                // Failed to resolve immediately
                channel.close();
                promise.setFailure(resolveFailureCause);
            } else {
                // Succeeded to resolve immediately; cached? (or did a blocking lookup)
                // 在成功解析地址后,进行连接
                doConnect(resolveFuture.getNow(), localAddress, promise);
            }
            return promise;
        }

        // Wait until the name resolution is finished.
        resolveFuture.addListener(new FutureListener<SocketAddress>() {
            @Override
            public void operationComplete(Future<SocketAddress> future) throws Exception {
                if (future.cause() != null) {
                    channel.close();
                    promise.setFailure(future.cause());
                } else {
                    doConnect(future.getNow(), localAddress, promise);
                }
            }
        });
    } catch (Throwable cause) {
        promise.tryFailure(cause);
    }
    return promise;
}

// Bootstrap.doConnect(remoteAddress, localAddress, connectPromise)
private static void doConnect(
        final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise connectPromise) {

    // This method is invoked before channelRegistered() is triggered.  Give user handlers a chance to set up
    // the pipeline in its channelRegistered() implementation.
    final Channel channel = connectPromise.channel();
    // 将发起连接请求这个操作封装成 task 丢给 eventLoop 线程去执行
    channel.eventLoop().execute(new Runnable() {
        @Override
        public void run() {
            if (localAddress == null) {
                // 当 eventLoop 线程执行到这个 task 的时候,通过 channel 进行发起连接
                channel.connect(remoteAddress, connectPromise);
            } else {
                channel.connect(remoteAddress, localAddress, connectPromise);
            }
            connectPromise.addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
        }
    });
}

// AbstractChannel.connect(remoteAddress, promise)
@Override
public ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) {
    // 交给 pipeline 去执行连接操作
    return pipeline.connect(remoteAddress, promise);
}

// DefaultChannelPipeline.connect(remoteAddress, promise)
@Override
public final ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) {
    // 交给 tail 节点去执行连接操作,tail 节点的 connect 方法继承自 AbstractChannelHandlerContext
    return tail.connect(remoteAddress, promise);
}

// AbstractChannelHandlerContext.connect(remoteAddress, promise)
@Override
public ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) {
    return connect(remoteAddress, null, promise);
}

// AbstractChannelHandlerContext.connect(remoteAddress, localAddress, promise)
@Override
public ChannelFuture connect(
        final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {

    if (remoteAddress == null) {
        throw new NullPointerException("remoteAddress");
    }
    if (isNotValidPromise(promise, false)) {
        // cancelled
        return promise;
    }
    // 从 tail 节点开始查找下一个 outbound 节点,直到 head 节点
    final AbstractChannelHandlerContext next = findContextOutbound();
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {
        // 因为当前发起连接请求的任务是由 eventLoop 线程执行的,所以会直接走这里
        next.invokeConnect(remoteAddress, localAddress, promise);
    } else {
        safeExecute(executor, new Runnable() {
            @Override
            public void run() {
                next.invokeConnect(remoteAddress, localAddress, promise);
            }
        }, promise, null);
    }
    return promise;
}

// AbstractChannelHandlerContext.invokeConnect(remoteAddress, localAddress, promise)
private void invokeConnect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) {
    if (invokeHandler()) {
        try {
            // 调用节点封装的 handler 的 connect 方法,最终调用的是 head 节点的 connect 方法
            ((ChannelOutboundHandler) handler()).connect(this, remoteAddress, localAddress, promise);
        } catch (Throwable t) {
            notifyOutboundHandlerException(t, promise);
        }
    } else {
        connect(remoteAddress, localAddress, promise);
    }
}

// HeadContext.connect(ctx, remoteAddress, localAddress, promise)
@Override
public void connect(
        ChannelHandlerContext ctx,
        SocketAddress remoteAddress, SocketAddress localAddress,
        ChannelPromise promise) throws Exception {
    // 交给 unsafe 去执行
    unsafe.connect(remoteAddress, localAddress, promise);
}

// AbstractNioUnsafe.connect(remoteAddress, localAddress, promise)
@Override
public final void connect(
        final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {
    if (!promise.setUncancellable() || !ensureOpen(promise)) {
        return;
    }

    try {
        if (connectPromise != null) {
            // Already a connect in process.
            throw new ConnectionPendingException();
        }

        boolean wasActive = isActive();
        // 发起连接请求
        if (doConnect(remoteAddress, localAddress)) {
            fulfillConnectPromise(promise, wasActive);
        } else {
            connectPromise = promise;
            requestedRemoteAddress = remoteAddress;

            // Schedule connect timeout.
            int connectTimeoutMillis = config().getConnectTimeoutMillis();
            if (connectTimeoutMillis > 0) {
                connectTimeoutFuture = eventLoop().schedule(new Runnable() {
                    @Override
                    public void run() {
                        ChannelPromise connectPromise = AbstractNioChannel.this.connectPromise;
                        ConnectTimeoutException cause =
                                new ConnectTimeoutException("connection timed out: " + remoteAddress);
                        if (connectPromise != null && connectPromise.tryFailure(cause)) {
                            close(voidPromise());
                        }
                    }
                }, connectTimeoutMillis, TimeUnit.MILLISECONDS);
            }

            promise.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    if (future.isCancelled()) {
                        if (connectTimeoutFuture != null) {
                            connectTimeoutFuture.cancel(false);
                        }
                        connectPromise = null;
                        close(voidPromise());
                    }
                }
            });
        }
    } catch (Throwable t) {
        promise.tryFailure(annotateConnectException(t, remoteAddress));
        closeIfClosed();
    }
}

// NioSocketChannel.doConnect(remoteAddress, localAddress)
@Override
protected boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress) throws Exception {
    if (localAddress != null) {
        doBind0(localAddress);
    }

    boolean success = false;
    try {
        // 最终调用的还是 jdk 的 SocketChannel 的 connect 方法
        // 目前看来,这里始终返回的是 false,所以设置 ops 为 SelectionKey.OP_CONNECT
        boolean connected = SocketUtils.connect(javaChannel(), remoteAddress);
        if (!connected) {
            selectionKey().interestOps(SelectionKey.OP_CONNECT);
        }
        success = true;
        return connected;
    } finally {
        if (!success) {
            doClose();
        }
    }
}

跟踪代码到 NioSocketChannel.doConnect(remoteAddress, localAddress) 方法为止,请求算是发出去了,但是连接算建立成功了么?当然没有,一个连接的建立当然还需要服务端的接收。

当服务端接收请求即建立连接后,客户端如何知晓呢?上面我们在 selector 上注册了 SelectionKey.OP_CONNECT 事件,selector 轮询到连接事件后,
最终会调用到 AbstractNioUnsafe 的 finishConnect() 方法。

// NioSocketChannel.finishConnect()
@Override
public final void finishConnect() {
    // Note this method is invoked by the event loop only if the connection attempt was
    // neither cancelled nor timed out.

    assert eventLoop().inEventLoop();

    try {
        boolean wasActive = isActive();
        // NioSocketChannel 仅仅判断下 SocketChannel 是否连接上了,没有的话则直接抛个 Error
        doFinishConnect();
        // 填充 connectPromise,就是往 connectPromise 里写个 success
        fulfillConnectPromise(connectPromise, wasActive);
    } catch (Throwable t) {
        fulfillConnectPromise(connectPromise, annotateConnectException(t, requestedRemoteAddress));
    } finally {
        // Check for null as the connectTimeoutFuture is only created if a connectTimeoutMillis > 0 is used
        // See https://github.com/netty/netty/issues/1770
        if (connectTimeoutFuture != null) {
            connectTimeoutFuture.cancel(false);
        }
        connectPromise = null;
    }
}

// NioSocketChannel.fulfillConnectPromise(promise, wasActive)
private void fulfillConnectPromise(ChannelPromise promise, boolean wasActive) {
    if (promise == null) {
        // Closed via cancellation and the promise has been notified already.
        return;
    }

    // Get the state as trySuccess() may trigger an ChannelFutureListener that will close the Channel.
    // We still need to ensure we call fireChannelActive() in this case.
    boolean active = isActive();

    // trySuccess() will return false if a user cancelled the connection attempt.
    boolean promiseSet = promise.trySuccess();

    // Regardless if the connection attempt was cancelled, channelActive() event should be triggered,
    // because what happened is what happened.
    if (!wasActive && active) {
        // 正常情况下连接建立上了的话,开始通过 pipeline 传播 ChannelActive 事件
        pipeline().fireChannelActive();
    }

    // If a user cancelled the connection attempt, close the channel, which is followed by channelInactive().
    if (!promiseSet) {
        close(voidPromise());
    }
}

ChannelActive 事件在 pipeline 上的传播

// DefaultChannelPipeline.fireChannelActive()
@Override
public final ChannelPipeline fireChannelActive() {
    // 从 head 节点开始传播
    AbstractChannelHandlerContext.invokeChannelActive(head);
    return this;
}

// AbstractChannelHandlerContext.invokeChannelActive(next)
static void invokeChannelActive(final AbstractChannelHandlerContext next) {
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {
        next.invokeChannelActive();
    } else {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                next.invokeChannelActive();
            }
        });
    }
}

// AbstractChannelHandlerContext.invokeChannelActive()
private void invokeChannelActive() {
    if (invokeHandler()) {
        try {
            // head 节点的 handler() 返回的是 this
            ((ChannelInboundHandler) handler()).channelActive(this);
        } catch (Throwable t) {
            notifyHandlerException(t);
        }
    } else {
        fireChannelActive();
    }
}

// HeadContext.channelActive(ctx)
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
    // 这里是将事件传递到下一个 ChannelInboundHandler
    ctx.fireChannelActive();

    // 下面来看看这行代码背后做了哪些工作
    readIfIsAutoRead();
}

// HeadContext.readIfIsAutoRead()
private void readIfIsAutoRead() {
    // isAutoRead() 方法默认就返回 true
    if (channel.config().isAutoRead()) {
        channel.read();
    }
}

// AbstractChannel.read()
@Override
public Channel read() {
    pipeline.read();
    return this;
}

// DefaultChannelPipeline.read()
@Override
public final ChannelPipeline read() {
    // tail 节点的 read() 继承自 AbstractChannelHandlerContext
    tail.read();
    return this;
}

// AbstractChannelHandlerContext.read()
@Override
public ChannelHandlerContext read() {
    // 从 tail 节点往前查找 ChannelOutboundHandler
    final AbstractChannelHandlerContext next = findContextOutbound();
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {
        // 我们是在 NioEventLoop 线程中执行的,所以直接调用 invokeRead() 方法
        next.invokeRead();
    } else {
        Runnable task = next.invokeReadTask;
        if (task == null) {
            next.invokeReadTask = task = new Runnable() {
                @Override
                public void run() {
                    next.invokeRead();
                }
            };
        }
        executor.execute(task);
    }

    return this;
}

// AbstractChannelHandlerContext.invokeRead()
private void invokeRead() {
    if (invokeHandler()) {
        try {
            // head 节点的 handler() 方法返回 this,所以最终调用 HeadContext.read(ctx)
            ((ChannelOutboundHandler) handler()).read(this);
        } catch (Throwable t) {
            notifyHandlerException(t);
        }
    } else {
        read();
    }
}

// HeadContext.read(ctx)
@Override
public void read(ChannelHandlerContext ctx) {
    unsafe.beginRead();
}

// AbstractUnsafe.beginRead()
@Override
public final void beginRead() {
    assertEventLoop();

    if (!isActive()) {
        return;
    }

    try {
        // 最终调用 AbstractNioChannel.doBeginRead()
        doBeginRead();
    } catch (final Exception e) {
        invokeLater(new Runnable() {
            @Override
            public void run() {
                pipeline.fireExceptionCaught(e);
            }
        });
        close(voidPromise());
    }
}

// AbstractNioChannel.doBeginRead()
@Override
protected void doBeginRead() throws Exception {
    // Channel.read() or ChannelHandlerContext.read() was called
    final SelectionKey selectionKey = this.selectionKey;
    if (!selectionKey.isValid()) {
        return;
    }

    readPending = true;

    // 这里我们通过 selectionKey.interestOps 方法将其修改为 SelectionKey.OP_READ
    final int interestOps = selectionKey.interestOps();
    if ((interestOps & readInterestOp) == 0) {
        selectionKey.interestOps(interestOps | readInterestOp);
    }
}

客户端启动总结

  1. 创建 NioEventLoopGroup,然后创建 NioEventLoop 来填充 NioEventLoopGroup 的 EventExecutor[] 数组

  2. 创建辅助类 Bootstrap,并设置 NIO 线程组、IO 模型、TCP 参数以及业务处理 handler 等

  3. 调用 Bootstrap 的 connect 方法进行连接

    1. 反射创建一个 NioSocketChannel 对象,并进行初始化(封装 handler 添加到 pipeline 的 tail 节点前)

    2. 从 NioEventLoopGroup 选出一个 NioEventLoop,将 channel 注册到它的 selector 上,最终调用的是 jdk 的 ServerSocketChannel 的注册方法,但是 ops 为 0

    3. 注册成功后,进行 handlerAdded、ChannelRegistered 等事件的触发

    4. 然后开始真正的请求连接,最终还是调用 jdk 的 SocketChannel 的 connect 方法,然后设置 ops 为 SelectionKey.OP_CONNECT

    5. 在连接建立后,进行 ChannelActive 事件的传播,其中会修改 channel 注册到 selector 上返回的 selectionKey 的 ops 为 SelectionKey.OP_READ

  • 一句话总结:初始化 channel 后注册到 selector 上,触发 handlerAdded、ChannelRegistered 事件后发送连接请求,在连接建立(服务端 accept)以后,接着触发 ChannelActive 事件,其中又会修改 selectionKey 的 ops 以便 selector 轮询到 服务端发来的消息

InnoDB B+树索引

MySQL 作为最(bu)流(yao)行(qian)的开源数据库,性能高、成本低、可靠性好,其中一部分原因离不开其支持的 MyISAM、InnoDB、Memory 等多种存储引擎。不同的存储引擎管理的表的存储结构可能不同,采用的存取算法也可能不同,本文基于目前 MySQL 默认的存储引擎 InnoDB 对数据的存储及索引进行简单的介绍。

InnoDB 索引页结构

页简介

是 InnoDB 管理存储空间的基本单位,一个页的大小一般是 16kb。InnoDB 针对不同功能设计了许多不同类型的 ,例如表空间头部信息页、Undo日志页、段信息页以及本文的主角 索引页

索引页

每个 索引页 包含以下几个部分:

  • File Header,表示页的一些通用信息,占固定的38字节。每个数据页的 File Header 部分都有上一个和下一个页的编号,所以所有的数据页会组成一个双向链表。

  • Page Header,表示数据页专有的一些信息,占固定的56个字节。

  • Infimum + Supremum,两个虚拟的伪记录,分别表示页中的最小和最大记录,占固定的26个字节。

  • User Records:真实存储我们插入的记录的部分,大小不固定。

  • Free Space:页中尚未使用的部分,大小不确定。

  • Page Directory:页中的某些记录相对位置,也就是各个槽在页面中的地址偏移量,大小不固定,插入的记录越多,这个部分占用的空间越多。

  • File Trailer:用于检验页是否完整的部分,占用固定的8个字节。

用户记录在索引页中的存储

上面介绍了 索引页 的几个部分,其中 User Records 部分就是用来储存用户写入的记录,但是一开始生成页的时候是没有这一部分的,当用户第一次写入记录的时候,会自动从 Free Space 中申请一个记录大小的空间划分到 User Records 中,当 Free Space 被用完之后还需要写入数据,就需要去申请一个新的 索引页

每个记录的头信息中都有一个 next_record 属性,从而使页中的所有记录串联成一个单链表。需要注意的是:

  • 索引页 中存在的 InfimumSupremum 代表页中的最小和最大记录,当不存在用户数据的时候,它们也是存在的。

  • Infimumnext_record 指向第一条用户记录。

  • 最后一条用户记录的 next_record 指向 Supremum

  • 这样从 Infimum 到用户记录再到 Supremum 就形成了一个单向链表。

  • 最重要的是,页内的用户数据是按主键值大小进行排序的。

当用户删除一个记录的时候:

  • 该记录并没有从存储空间中移除,而是把该条记录的 delete_mask 值设置为 1。
  • 该记录的 next_record 值变为了0,意味着该记录没有下一条记录了。
  • 该记录的上一条记录的 next_record 指向了该记录的下一条记录。

当数据页中存在多条被删除掉的记录时,这些记录的 next_record 属性将会把这些被删除掉的记录组成一个垃圾链表,以备之后重用这部分存储空间。

Page Directory(页目录)

上面介绍了用户记录在 索引页 中按主键值大小排序形成一条单向链表,那么用户按照主键去查找某条记录的时候,从 Infimum 开始往后依次查找即可。当页中的记录较少的时候没有问题,但是当页中有很多记录的时候这么查找的性能就有问题了。

为了缩小查找的范围,InnoDB 设计了一个分组的概念,将页中的记录分成多个组,每个组中记录数量有限,那么如果能定位到要查找的记录在哪个组的话,再遍历该组的记录链表即可。

分组的相关信息如下:

  • 没有用户记录的时候,Infimum 是第一个组,Supremum 是第二个组

  • 插入用户记录的时候,都会从 页目录 中找到主键值比本记录的索引列值大并且差值最小的槽

  • 在一个组中的记录数等于 8 个后再插入记录时,会将该组拆分成两个组,一个组 4 条记录,另一个 5 条记录

  • 对于最小记录所在的分组只能有 1 条记录,最大记录所在的分组拥有的记录条数只能在 1~8 条之间,剩下的分组中记录的条数范围只能在是 4~8 条之间

  • 每个组的最后一条记录(也就是组内最大的那条记录)的头信息中的 n_owned 属性表示该记录拥有多少条记录,也就是该组内共有几条记录

现在分组有了,那么怎么定位到用户记录在哪个分组呢?InnoDB 又设计了一个 的概念(也就是一个目录的概念):

  • 将每个组的最后一条记录的地址偏移量单独提取出来按顺序存储到靠近 的尾部的地方,这个地方就是所谓的 Page Directory,也就是 页目录
  • 页面目录中的这些地址偏移量被称为 (英文名:Slot),所以这个页面目录就是由 组成的。
  • 其实也就是说每个 对应一个分组中的最后一条记录。

那么现在问题就解决了,要在一个页中按索引列查找一个记录的话,步骤如下:

  1. Page Directory 上通过二分查找即可定位到要查找的记录所在的分组。
  2. 虽然不知道该分组的第一个记录,但是可以通过前一个分组的最后一条记录的 next_record 找到。
  3. 然后从该分组的第一条记录开始,通过 next_record 遍历该分组内的记录链表即可。

File Header

File Header 针对各种类型的页都通用,也就是说不同类型的页都会以 File Header 作为第一个组成部分,它描述了一些针对各种页都通用的一些信息,比方说这个页的编号是多少,它的上一个页、下一个页是谁。这里我们关注 FIL_PAGE_PREVFIL_PAGE_NEXT。它们是上一个页面和下一个页面的页号。这样页与页之间就形成了一个 双向链表,为什么会有多个页呢?那肯定是一个页中的记录太多了啊,导致了 页分裂

总结

  • 用户记录在页中按索引列大小排序形成单向链表

  • 页中的记录划分为多个分组

  • Page Directory 中存放每个分组最后一条记录的地址偏移量

  • 用户在单个页内查找记录过程:先二分查找定位分组,再遍历分组内记录

InnoDB 索引

前面介绍了单个 索引页 内记录的存储情况,但是当用户记录非常多的时候,索引页 肯定也不止一个,这个时候要查找一个记录的话,需要先定位到记录所在的页,然后再通过前面介绍的单页面查找方式进行查找。

InnoDB 索引从逻辑角度可以划分为主键索引(聚簇索引)、二级索引和联合索引等。

聚簇索引

InnoDB 会为主键自动创建聚簇索引,也就是一个 B+ 树,树的每一个节点都是一个上文所说 索引页,我们称叶子节点为数据页(存储用户数据),非叶子节点为目录项页(定位到数据页)。目录项页中存储的每个记录是 主键+页号。通过页号可以找到下一个层次的一个目录项 A,主键值就是目录项 A 中最小的主键值。

这样一来,我们想要通过主键查找一条用户记录的话:

  1. 通过根节点中的主键比对,然后通过页号导航到下一层次的目录项页
  2. 然后再从该目录项页导航到下下一层目录项页,直到抵达叶子结点,也就是一个数据页
  3. 再然后就可以通过上文所说的单页面查找方式查找。

这个 B+ 树有两个特点:

  1. 使用记录主键值的大小进行记录和页的排序
    • 页内的记录根据主键值大小排序形成 单向链表
    • 目录项页分为不同层次,同一层次的目录项页按照主键值大小排序形成 双向链表
    • 数据页也按主键值大小排序形成 双向链表
  2. 叶子结点中存储完整的用户记录,也就是存储了所有列的值(包括隐藏列)

我们把具有这两个特点的索引称为聚簇索引,由于叶子节点存储完整的用户记录,所有索引即数据,数据即索引。

二级索引

上面所说的聚簇索引只适用于搜索条件是主键的情况,那么搜索条件不是主键的话怎么办呢?例如搜索条件是索引列 c2 的话,其实也会创建一个 B+ 树,与聚簇索引不同的是:

  • 使用 c2 列值的大小进行记录和页的排序

    • 页内的记录根据 c2 列值大小排序形成一个 单向链表
    • 目录项页分为不同层次,同一层次的目录项页根据 c2 列值大小排序形成一个 双向链表
    • 存放用户记录的数据页也是根据 c2 列值大小排序形成一个 双向链表
  • 目录项记录中不再是 主键+页号 的搭配,而变成了 c2列+页号 的搭配

  • 叶子节点存储的并不是完整的用户记录,而只是 c2列+主键 这两个列的值

其实在目录项中不仅有 c2 列和页号,也会有主键,防止添加记录时由于和目录项记录中 c2 列值相同导致不知道应该放在那个页中。

这样我们按索引列 c2 搜索用户记录时:

  1. 先从该 B+ 树索引中获取到主键值
  2. 再用主键值去聚簇索引中查找记录(回表操作)

由于该 B+ 树索引的叶子节点只存储主键值,搜索数据时需要回表查找完整用户记录,所以该索引称为二级索引或辅助索引。

联合索引

我们也可以为多个列建立联合索引,以列 c2 和 c3 为例,排序规则为先按 c2 列排序,c2 列相同时按 c3 列排序。

与上面说到的二级索引一样,目录项页存的是 索引列+主键+页号,数据页存的是 索引列+主键。因此也可能需要回表操作。为什么说的是可能呢?因为如果我们的查询列(就是我们要 select 的列)就是 c2,那么该联合索引中本身就存了 c2 列值,也就不需要回表查找了。

B+ 树的形成过程

  • 每当为某个表创建一个 B+ 树索引(聚簇索引不是人为创建的,默认就有)的时候,都会为这个索引创建一个 根节点 页面。最开始表中没有数据的时候,每个 B+ 树索引对应的 根节点 中既没有用户记录,也没有目录项记录。
  • 随后向表中插入用户记录时,先把用户记录存储到这个 根节点 中。
  • 根节点 中的可用空间用完时继续插入记录,此时会将 根节点 中的所有记录复制到一个新分配的页,比如 页a 中,然后对这个新页进行 页分裂 的操作,得到另一个新页,比如 页b。这时新插入的记录根据键值(也就是聚簇索引中的主键值,二级索引中对应的索引列的值)的大小就会被分配到 页a 或者 页b 中,而 根节点 便升级为存储目录项记录的页。

B+ 树索引

上文所说的聚簇索引、二级索引等是从逻辑角度划分的,其实都是 B+ 树索引。下面说说 B+ 树索引的适用范围以及如何挑选索引列。

适用范围

以联合索引 idx_name_birthday_phone 为例,会先按 name 排序,name 相同按 birthday 排序,birthday 相同按 phone 排序。

全值匹配

SELECT * FROM person_info WHERE name = 'Ashburn' AND birthday = '1990-09-27' AND phone = '15123983239';

查询过程为:

  • 因为 B+ 树的页和记录先是按照 name 列的值进行排序的,所以先可以很快定位 name 列的值是 Ashburn 的记录位置。
  • name 列相同的记录里又是按照 birthday 列的值进行排序的,所以在 name 列的值是 Ashburn 的记录里又可以快速定位 birthday 列 的值是 '1990-09-27' 的记录。
  • 如果很不幸,namebirthday 列的值都是相同的,那记录是按照 phone 列的值排序的,所以联合索引中的三个列都可能被用到。

匹配左边的列

SELECT * FROM person_info WHERE name = 'Ashburn' AND birthday = '1990-09-27';

如果我们想使用联合索引中尽可能多的列,搜索条件中的各个列必须是联合索引中从最左边连续的列。

也就是说如果是上述的搜索条件,我们可以先根据 name 查找再根据 birthday 查找,但是如果搜索条件是:

SELECT * FROM person_info WHERE name = 'Ashburn' And phone = '15123983239';

这样只能用到 name 列的索引,birthdayphone 的索引就用不上了,因为 name 值相同的记录先按照 birthday 的值进行排序,birthday 值相同的记录才按照 phone 值进行排序。

匹配范围值

SELECT * FROM person_info WHERE name > 'Asa' AND name < 'Barlow'; 

找到 name 值为 Asa 的记录,找到 name 值为 Barlow 的记录,找到两者之间的记录的主键值,然后回表查找。

不过在使用联合进行范围查找的时候需要注意,如果对多个列同时进行范围查找的话,只有对索引最左边的那个列进行范围查找的时候才能用到 B+ 树索引。例如:

SELECT * FROM person_info WHERE name > 'Asa' AND name < 'Barlow' AND birthday > '1980-01-01';

这个查询只能用到联合索引 idx_name_birthday_phonename 列的部分。

精确匹配某一列并范围匹配另一列

对于同一个联合索引来说,虽然对多个列都进行范围查找时只能用到最左边那个索引列,但是如果左边的列是精确查找,则右边的列可以进行范围查找,例如:

SELECT * FROM person_info WHERE name = 'Ashburn' AND birthday > '1980-01-01' AND birthday < '2000-12-31' AND phone > '15100000000';

这个查询可以用到联合索引 idx_name_birthday_phonename 列和 birthday 列的部分,但是用不到 phone 列的部分。

用于排序

在 MySQL 中,把在内存中或者磁盘上进行排序的方式统称为文件排序(英文名:filesort),跟 文件 这个词儿一沾边儿,就显得这些排序操作非常慢了(磁盘和内存的速度比起来,就像是飞机和蜗牛的对比)。但是如果 ORDER BY 子句里使用到了我们的索引列,就有可能省去在内存或文件中排序的步骤,比如下边这个简单的查询语句:

SELECT * FROM person_info ORDER BY name, birthday, phone LIMIT 10;

这个查询的结果集需要先按照 name 值排序,如果记录的 name 值相同,则需要按照 birthday 来排序,如果 birthday 的值相同,则需要按照 phone 排序。巧了,联合索引 idx_name_birthday_phone 中记录刚好就是这么排序的,直接用就好了。

用于分组

有时候我们为了方便统计表中的一些信息,会把表中的记录按照某些列进行分组。比如下边这个分组查询:

SELECT name, birthday, phone, COUNT(*) FROM person_info GROUP BY name, birthday, phone;

这个查询语句相当于做了3次分组操作:

  1. 先把记录按照 name 值进行分组,所有 name 值相同的记录划分为一组。
  2. 将每个 name 值相同的分组里的记录再按照 birthday 的值进行分组,将 birthday 值相同的记录放到一个小分组里,所以看起来就像在一个大分组里又化分了好多小分组。
  3. 再将上一步中产生的小分组按照 phone 的值分成更小的分组,所以整体上看起来就像是先把记录分成一个大分组,然后把 大分组 分成若干个 小分组,然后把若干个 小分组 再细分成更多的 小小分组

然后针对那些 小小分组 进行统计,比如在我们这个查询语句中就是统计每个 小小分组 包含的记录条数。如果没有索引的话,这个分组过程全部需要在内存里实现,而如果有了索引的话,恰巧这个分组顺序又和我们的 B+ 树中的索引列的顺序是一致的,而我们的 B+ 树索引又是按照索引列排好序的,这不正好么,所以可以直接使用 B+ 树索引进行分组。

如何挑选索引列

只为用于搜索、排序或分组的列创建索引

也就是说,只为出现在 WHERE 子句中的列、连接子句中的连接列,或者出现在 ORDER BYGROUP BY 子句中的列创建索引。

考虑列的基数

列的基数 指的是某一列中去重后数据的个数,比方说某个列包含值 2, 5, 8, 2, 5, 8, 2, 5, 8,虽然有 9条记录,但该列的基数却是 3。也就是说,在记录行数一定的情况下,列的基数越大,该列中的值越分散,列的基数越小,该列中的值越集中。如果为一个基数为 1 的列建立一个索引,那么至少有两个问题:

  • 所有值都一样就无法排序,无法进行快速查找了
  • 使用这个二级索引查出的记录还可能要做回表操作,这样性能损耗就更大了

最好为那些列的基数大的列建立索引,为基数太小列的建立索引效果可能不好

索引列的类型尽量小

我们在定义表结构的时候要显式的指定列的类型,以整数类型为例,有 TINYINTMEDIUMINTINTBIGINT 这么几种,它们占用的存储空间依次递增,我们这里所说的 类型大小 指的就是该类型表示的数据范围的大小。能表示的整数范围当然也是依次递增,如果我们想要对某个整数列建立索引的话,在表示的整数范围允许的情况下,尽量让索引列使用较小的类型,比如我们能使用 INT 就不要使用 BIGINT,能使用 MEDIUMINT 就不要使用 INT ,这是因为:

  • 数据类型越小,在查询时进行的比较操作越快
  • 数据类型越小,索引占用的存储空间就越少,在一个数据页内就可以放下更多的记录,从而减少磁盘I/O带来的性能损耗,也就意味着可以把更多的数据页缓存在内存中,从而加快读写效率

这个建议对于表的主键来说更加适用,因为不仅是聚簇索引中会存储主键值,其他所有的二级索引的节点处都会存储一份记录的主键值,如果主键适用更小的数据类型,也就意味着节省更多的存储空间和更高效的I/O

索引字符串值的前缀

在建表语句中 KEY idx_name_birthday_phone (name(10), birthday, phone) 中的 name(10) 就表示在建立的 B+ 树索引中只保留记录的前 10 个字符的编码,这种只索引字符串值的前缀的策略是我们非常鼓励的,尤其是在字符串类型能存储的字符比较多的时候。

让索引列在比较表达式中单独出现

如果索引列在比较表达式中不是以单独列的形式出现,而是以某个表达式,或者函数调用形式出现的话,是用不到索引的。

总结

  • 是 InnoDB 管理存储空间的基本单位,用户记录是存在 索引页 中的
  • InnoDB 的聚簇索引、二级索引等都是一个 B+ 树,树的节点是 索引页
  • 聚簇索引非叶子节点存储目录项,叶子节点存储完整用户记录;非聚簇索引叶子节点存的是 索引列+主键
  • B+ 树索引适用于全值匹配、最左列匹配、范围匹配等情况
  • 在挑选索引列的时候,需要考虑列的基数、列的类型以及让索引列单独出现等

参考书籍:

Netty 服务端启动源码

Netty 服务端启动代码基本如下所示,服务端启动时 Netty 内部为我们做了什么?

  • 服务端的线程模型是如何设置的?
  • 服务端的 channel 是如何创建并初始化的?
  • 具体又是怎么绑定端口后监听客户端连接请求的?

带着以上几个问题,下面我们来一探究竟。

EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
    ServerBootstrap serverBootstrap = new ServerBootstrap();
    serverBootstrap
            // 指定线程模型
            .group(bossGroup, workerGroup)
            // 指定IO模型
            .channel(NioServerSocketChannel.class)
            // 设置 TCP 参数
            .option(ChannelOption.SO_BACKLOG, 1024)
            .childOption(ChannelOption.TCP_NODELAY, true)
            // 设置业务处理 handler
            .handler(new MyHandler())
            .childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new MyChildHandler());
                }
            });
    // 绑定端口
    ChannelFuture future = serverBootstrap.bind(port).sync();
    future.channel().closeFuture().sync();
} finally {
    // 优雅停机
    bossGroup.shutdownGracefully();
    workerGroup.shutdownGracefully();
}

核心概念

  • ServerBootstrap

    作为抽象辅助类 AbstractBootstrap 的子类,ServerBootstrap 主要提供给服务端使用;

    基于此进行一些 group、channel、option、handler、childOption 和 childHandler 的配置,其中 option 和 handler 是针对自身的,而 childOption 和 childHandler 是针对后面接收的每个连接的;

  • NioServerSocketChannel

    封装了 jdk 的 ServerSocketChannel,且存在一个属性 readInterestOp 记录对什么事件感兴趣,初始化时为 SelectionKey.OP_ACCEPT
    以及继承自父类 AbstractChannel 的三大属性 id(全局唯一标识)、unsafe(依附于 channel,负责处理操作) 和 pipeline(责任链模式处理请求);

  • Unsafe

    Unsafe 附属于 Channel,大部分情况下仅在内部使用;

  • ChannelPipeline

    初始化 DefaultChannelPipeline 时,设置 HeadContext、TailContext 两个节点组成双向链表结构,两个节点均继承自 AbstractChannelHandlerContext;

    换句话说 ChannelPipeline 就是一个双向链表 (head ⇄ handlerA ⇄ handlerB ⇄ tail),链表的每个节点是一个 AbstractChannelHandlerContext;

  • NioEventLoopGroup

    有个属性 EventExecutor[],而下面要说的 NioEventLoop 就是 EventExecutor;

    按 Reactor (主从)多线程模型来说,服务端存在两个 NioEventLoopGroup,一个 NioEventLoopGroup(bossGroup) 用来接收连接,然后将连接丢给另一个 NioEventLoopGroup(workerGroup),
    以后该连接的读写事件均有这个 workerGroup 的一个 NioEventLoop 来执行;

  • NioEventLoop

    核心线程(其实看源码的话,好像理解成一个 task 更合理),含有重要属性 executor、taskQueue、selector 等;

创建 NioEventLoopGroup

创建 NioEventLoopGroup 的时候最终调用其父类 MultithreadEventExecutorGroup 的构造方法,主要代码如下:

protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
                                            EventExecutorChooserFactory chooserFactory, Object... args) {
        if (executor == null) {
            // 后面会将 NioEventLoop run 方法包装成一个 task,然后交给这个 executor 执行,
            // 而这个 executor 仅仅启动一个新线程去执行该 task,这个执行线程就是IO核心线程
            executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
        }
        // 初始化 EventExecutor[]
        children = new EventExecutor[nThreads];

        for (int i = 0; i < nThreads; i ++) {
            boolean success = false;
            try {
                // 调用 NioEventLoopGroup 的 newChild 方法创建 NioEventLoop,此时尚未创建核心线程
                children[i] = newChild(executor, args);
                success = true;
            } catch (Exception e) {
                // TODO: Think about if this is a good exception type
                throw new IllegalStateException("failed to create a child event loop", e);
            } finally {
                if (!success) {
                    ...
                }
            }
        }
        // chooser 主要是用来从 EventExecutor[] 中选取一个 NioEventLoop
        chooser = chooserFactory.newChooser(children);
    }

创建 NioEventLoopGroup 主要做了以下几件事:

  1. 没有 executor 则创建一个 executor
  2. 初始化一个 EventExecutor[],为创建 NioEventLoop 做准备
  3. 循环调用 newChild 方法(传入 executor )去创建 NioEventLoop 对象,置于 EventExecutor[] 数组当中
  4. 创建一个 chooser,用来从 EventExecutor[] 选取处一个 NioEventLoop

创建 NioEventLoop

创建 NioEventLoop 的时候会调用其父类 SingleThreadEventExecutor 的构造方法,主要代码如下:

protected SingleThreadEventExecutor(EventExecutorGroup parent, Executor executor,
                                    boolean addTaskWakesUp, int maxPendingTasks,
                                    RejectedExecutionHandler rejectedHandler) {
    super(parent);
    this.addTaskWakesUp = addTaskWakesUp;
    this.maxPendingTasks = Math.max(16, maxPendingTasks);
    this.executor = ObjectUtil.checkNotNull(executor, "executor");
    // 任务队列
    taskQueue = newTaskQueue(this.maxPendingTasks);
    rejectedExecutionHandler = ObjectUtil.checkNotNull(rejectedHandler, "rejectedHandler");
}
 
NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,
             SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) {
    super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler);
    if (selectorProvider == null) {
        throw new NullPointerException("selectorProvider");
    }
    if (strategy == null) {
        throw new NullPointerException("selectStrategy");
    }
    provider = selectorProvider;
    final SelectorTuple selectorTuple = openSelector();
    // 之后要把 channel 注册在 selector 上
    selector = selectorTuple.selector;
    unwrappedSelector = selectorTuple.unwrappedSelector;
    selectStrategy = strategy;
}

创建 NioEventLoop 主要做了以下几件事:

  1. 创建任务队列,以后调用 NioEventLoop.execute(Runnable task) 的时候均是把 task 放入该任务队列
  2. 创建一个 selector,之后要把 channel 注册到该 selector 上

设置 ServerBootstrap

设置 Reactor 线程模型

// ServerBootstrap.group(group, group)
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
    // 这里的 parentGroup 就是我们创建的 NioEventLoopGroup(bossGroup)
    super.group(parentGroup);
    if (childGroup == null) {
        throw new NullPointerException("childGroup");
    }
    if (this.childGroup != null) {
        throw new IllegalStateException("childGroup set already");
    }
    // 这里的 childGroup 就是我们创建的 NioEventLoopGroup(workerGroup)
    this.childGroup = childGroup;
    return this;
}

设置 IO 模型

// AbstractBootstrap.channel(channelClass)
public B channel(Class<? extends C> channelClass) {
    if (channelClass == null) {
        throw new NullPointerException("channelClass");
    }
    // channelClass 就是上面传进来的 NioServerSocketChannel.class
    // 创建一个持有 channelClass 的 channelFactory 并保存,后面用来反射创建 NioServerSocketChannel 对象
    return channelFactory(new ReflectiveChannelFactory<C>(channelClass));
}

设置 TCP 参数

// AbstractBootstrap.option(option, value)
public <T> B option(ChannelOption<T> option, T value) {
    if (option == null) {
        throw new NullPointerException("option");
    }
    if (value == null) {
        synchronized (options) {
            options.remove(option);
        }
    } else {
        synchronized (options) {
            options.put(option, value);
        }
    }
    return self();
}

// ServerBootstrap.childOption(option, value)
public <T> ServerBootstrap childOption(ChannelOption<T> childOption, T value) {
    if (childOption == null) {
        throw new NullPointerException("childOption");
    }
    if (value == null) {
        synchronized (childOptions) {
            childOptions.remove(childOption);
        }
    } else {
        synchronized (childOptions) {
            childOptions.put(childOption, value);
        }
    }
    return this;
}

设置业务处理 handler

// AbstractBootstrap.handler(handler)
public B handler(ChannelHandler handler) {
    if (handler == null) {
        throw new NullPointerException("handler");
    }
    this.handler = handler;
    return self();
}

// ServerBootstrap.childHandler(childHandler)
public ServerBootstrap childHandler(ChannelHandler childHandler) {
    if (childHandler == null) {
        throw new NullPointerException("childHandler");
    }
    this.childHandler = childHandler;
    return this;
}

绑定端口

首先根据端口构造 SocketAddress 对象,然后调用 doBind(localAddress) 方法:

// AbstractBootstrap.doBind(localAddress)
private ChannelFuture doBind(final SocketAddress localAddress) {
    // 初始化一个 channel,然后注册到 selector 上
    final ChannelFuture regFuture = initAndRegister();
    final Channel channel = regFuture.channel();
    if (regFuture.cause() != null) {
        return regFuture;
    }
    // 如果注册完成的话,接下来就要绑定端口了
    if (regFuture.isDone()) {
        // At this point we know that the registration was complete and successful.
        ChannelPromise promise = channel.newPromise();
        // 绑定端口的核心方法
        doBind0(regFuture, channel, localAddress, promise);
        return promise;
    } else {
        // Registration future is almost always fulfilled already, but just in case it's not.
        final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
        // 给 regFuture 添加个监听器,一旦注册完成则执行该监听方法,注册成功的话依然调用 doBind0 绑定端口
        regFuture.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                Throwable cause = future.cause();
                if (cause != null) {
                    // Registration on the EventLoop failed so fail the ChannelPromise directly to not cause an
                    // IllegalStateException once we try to access the EventLoop of the Channel.
                    promise.setFailure(cause);
                } else {
                    // Registration was successful, so set the correct executor to use.
                    // See https://github.com/netty/netty/issues/2586
                    promise.registered();

                    doBind0(regFuture, channel, localAddress, promise);
                }
            }
        });
        return promise;
    }
}

初始化 channel(NioServerSocketChannel)

// AbstractBootstrap.initAndRegister()
final ChannelFuture initAndRegister() {
    Channel channel = null;
    try {
        // 首先通过 channelFactory 用反射创建一个 NioServerSocketChannel 对象(封装了 jdk 的 ServerSocketChannel 和 SelectionKey.OP_ACCEPT)
        channel = channelFactory.newChannel();
        // 初始化该 channel 对象
        init(channel);
    } catch (Throwable t) {
        if (channel != null) {
            // channel can be null if newChannel crashed (eg SocketException("too many open files"))
            channel.unsafe().closeForcibly();
            // as the Channel is not registered yet we need to force the usage of the GlobalEventExecutor
            return new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE).setFailure(t);
        }
        // as the Channel is not registered yet we need to force the usage of the GlobalEventExecutor
        return new DefaultChannelPromise(new FailedChannel(), GlobalEventExecutor.INSTANCE).setFailure(t);
    }
    // 初始化后将 channel 注册到 selector 上,核心方法
    // 这里的 group() 方法返回的就是 bossGroup,所以它的 register 方法肯定是从数组中选出一个 NioEventLoop,然后将 channel 注册到它对应的 selector 上
    ChannelFuture regFuture = config().group().register(channel);
    if (regFuture.cause() != null) {
        if (channel.isRegistered()) {
            channel.close();
        } else {
            channel.unsafe().closeForcibly();
        }
    }
    return regFuture;
}

// ServerBootstrap.init(channel)
void init(Channel channel) throws Exception {
    final Map<ChannelOption<?>, Object> options = options0();
    synchronized (options) {
        setChannelOptions(channel, options, logger);
    }

    final Map<AttributeKey<?>, Object> attrs = attrs0();
    synchronized (attrs) {
        for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
            @SuppressWarnings("unchecked")
            AttributeKey<Object> key = (AttributeKey<Object>) e.getKey();
            channel.attr(key).set(e.getValue());
        }
    }

    // 获取 channel 对应的 pipeline,也就是个双向链表
    ChannelPipeline p = channel.pipeline();

    final EventLoopGroup currentChildGroup = childGroup;
    final ChannelHandler currentChildHandler = childHandler;
    final Entry<ChannelOption<?>, Object>[] currentChildOptions;
    final Entry<AttributeKey<?>, Object>[] currentChildAttrs;
    synchronized (childOptions) {
        currentChildOptions = childOptions.entrySet().toArray(newOptionArray(0));
    }
    synchronized (childAttrs) {
        currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(0));
    }

    // 在链表尾部(实际上是 tail 节点前面)添加一个 ChannelHandlerContext 节点;
    // 该节点依据当前这个 pipeline 和 handle(ChannelInitializer)创建的。其中 pipeline 又关联着 channel,方便后面可以获取到 channel;
    p.addLast(new ChannelInitializer<Channel>() {
        // 目前来看,正常情况下该方法只会在其 handlerAdded 方法中调用,并且调用后立即将自身从 pipeline 中移除
        @Override
        public void initChannel(final Channel ch) throws Exception {
            final ChannelPipeline pipeline = ch.pipeline();
            // 这个 handler() 就是设置业务处理 handler 时设置的,注意区分 handler 和 childHandler
            ChannelHandler handler = config.handler();
            // 如果通过 serverBootstrap.handler(new MyHandler()) 设置了 handler,
            // 同上面一样,构造一个 ChannelHandlerContext 节点添加到 pipeline 的 tail 节点前面
            if (handler != null) {
                pipeline.addLast(handler);
            }
            // eventLoop() 方法返回一个 NioEventLoop,这个会在 channel 注册到 selector 的时候设置到 channel 中
            // 这个 execute 方法也是核心,这里往 NioEventLoop 里扔一个 task
            ch.eventLoop().execute(new Runnable() {
                @Override
                public void run() {
                    // 同上面一样,构造一个 ChannelHandlerContext 节点添加到 pipeline 的 tail 节点前面
                    pipeline.addLast(new ServerBootstrapAcceptor(
                            ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
                }
            });
        }
    });
}

注册 channel 到 selector

回到 initAndRegister() 方法中,在创建 channel 并调用 init(channel) 方法后,开始把 channel 注册到 selector 上,通过 EventLoopGroup 来注册;
首先我们看下 EventLoopGroup 接口中有哪些方法:

public interface EventLoopGroup extends EventExecutorGroup {
    /**
     * Return the next {@link EventLoop} to use
     */
    @Override
    EventLoop next();

    /**
     * Register a {@link Channel} with this {@link EventLoop}. The returned {@link ChannelFuture}
     * will get notified once the registration was complete.
     */
    ChannelFuture register(Channel channel);

    /**
     * Register a {@link Channel} with this {@link EventLoop} using a {@link ChannelFuture}. The passed
     * {@link ChannelFuture} will get notified once the registration was complete and also will get returned.
     */
    ChannelFuture register(ChannelPromise promise);
}

NioEventLoopGroup 的 register(channel) 方法继承自父类 MultithreadEventLoopGroup

// NioEventLoopGroup.register(channel)
@Override
public ChannelFuture register(Channel channel) {
    // 所以这里变成了调用 NioEventLoop 的 register(channel) 方法
    return next().register(channel);
}

@Override
public EventLoop next() {
    // 父类则是通过 chooser.next() 来选取一个 NioEventLoop
    return (EventLoop) super.next();
}

NioEventLoop 的 register(channel) 方法继承自父类 SingleThreadEventLoop

// NioEventLoop.register(channel)
@Override
public ChannelFuture register(Channel channel) {
    // 这里根据 channel 和 NioEventLoop 自身创建了一个 DefaultChannelPromise
    return register(new DefaultChannelPromise(channel, this));
}

@Override
public ChannelFuture register(final ChannelPromise promise) {
    ObjectUtil.checkNotNull(promise, "promise");
    // unsafe() 方法返回 channel 对应的 AbstractUnsafe 对象,转为调用 AbstractUnsafe 的 register(eventLoop, promise) 方法
    promise.channel().unsafe().register(this, promise);
    return promise;
}

AbstractUnsafe 的 register(eventLoop, promise) 相关方法主要代码如下:

// AbstractUnsafe.register(eventLoop, promise)
@Override
public final void register(EventLoop eventLoop, final ChannelPromise promise) {
    // 给 channel 的 eventLoop 属性设值了
    AbstractChannel.this.eventLoop = eventLoop;
    // 如果当前是 eventLoop 线程调用的话则直接调用 register0(promise),否则封装成一个 task 丢给 eventLoop 去 execute
    // 正常来说,这里 eventLoop 线程还没有启动,第一次调用 eventLoop.execute(task) 的时候才会启动用 execute 启动 eventLoop 线程
    if (eventLoop.inEventLoop()) {
        register0(promise);
    } else {
        try {
            // 目前看来,这里是启动 eventLoop 线程的地方,下篇文章会对 eventLoop 线程的启动及执行做介绍,先不做深究
            // 把注册任务封装成 task 丢给 eventLoop 线程去执行
            eventLoop.execute(new Runnable() {
                @Override
                public void run() {
                    register0(promise);
                }
            });
        } catch (Throwable t) {
            logger.warn(
                    "Force-closing a channel whose registration task was not accepted by an event loop: {}",
                    AbstractChannel.this, t);
            closeForcibly();
            closeFuture.setClosed();
            safeSetFailure(promise, t);
        }
    }
}

// AbstractUnsafe.register0(promise)
private void register0(ChannelPromise promise) {
    try {
        // check if the channel is still open as it could be closed in the mean time when the register
        // call was outside of the eventLoop
        if (!promise.setUncancellable() || !ensureOpen(promise)) {
            return;
        }
        boolean firstRegistration = neverRegistered;
        // 最终注册的方法,在 AbstractNioChannel 中实现,其实也就是调用 jdk 的那个 ServerSocketChannel 的 register 方法
        doRegister();
        neverRegistered = false;
        registered = true;

        // 注册已经算是成功了,是时候进行我们设置的 handlerAdded 方法了;
        // 然后此时会找到初始化 channel 的时候往 pipeline 中添加的那个 ChannelInitializer 的 handlerAdded 方法;
        // 接着会调用它重写的的 initChannel(ch) 方法,具体内容在上面初始化 channel 的时候说过了
        pipeline.invokeHandlerAddedIfNeeded();
        // 设置 promise 值
        safeSetSuccess(promise);
        // 开始在 pipeline 上一次调用 fireChannelRegistered
        pipeline.fireChannelRegistered();
        // Only fire a channelActive if the channel has never been registered. This prevents firing
        // multiple channel actives if the channel is deregistered and re-registered.
        // 这里尚未绑定上端口,所以不会执行
        if (isActive()) {
            if (firstRegistration) {
                pipeline.fireChannelActive();
            } else if (config().isAutoRead()) {
                // This channel was registered before and autoRead() is set. This means we need to begin read
                // again so that we process inbound data.
                //
                // See https://github.com/netty/netty/issues/4805
                beginRead();
            }
        }
    } catch (Throwable t) {
        // Close the channel directly to avoid FD leak.
        closeForcibly();
        closeFuture.setClosed();
        safeSetFailure(promise, t);
    }
}

ChannelRegistered 事件在 pipeline 上的传播

然后我们来看注册成功后,fireChannelRegistered 事件在 pipeline 上怎么传播:

// DefaultChannelPipeline.fireChannelRegistered()
@Override
public final ChannelPipeline fireChannelRegistered() {
    // 从 pipeline 的 head 节点开始传播
    AbstractChannelHandlerContext.invokeChannelRegistered(head);
    return this;
}

// AbstractChannelHandlerContext.invokeChannelRegistered(next)
static void invokeChannelRegistered(final AbstractChannelHandlerContext next) {
    // 这个 executor 实际上就是 NioEventLoop
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {
        // HeadContext 的方法继承自 AbstractChannelHandlerContext
        next.invokeChannelRegistered();
    } else {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                next.invokeChannelRegistered();
            }
        });
    }
}

// AbstractChannelHandlerContext.invokeChannelRegistered() 
private void invokeChannelRegistered() {
    if (invokeHandler()) {
        try {
            // 如果节点是 head 或者 tail,handler() 方法则返回 this
            // 如果节点是 DefaultChannelHandlerContext,handler() 方法则返回创建时传入的 handler
            ((ChannelInboundHandler) handler()).channelRegistered(this);
        } catch (Throwable t) {
            notifyHandlerException(t);
        }
    } else {
        fireChannelRegistered();
    }
}

如果我们添加了 handlerA、handlerB,那么内部会将其封装成两个 DefaultChannelHandlerContext 并添加到 pipeline 中,此时链表结构为:
head ⇄ handlerA ⇄ handlerB ⇄ tail,ChannelRegistered 事件从 head 节点开始往后传播,在经历了自定义的 ChannelInboundHandler 后,最终会到达 tail 节点,tail 节点对该事件不作处理。

// HeadContext.channelRegistered(ctx)
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
    // 这个方法我们上面看过了
    invokeHandlerAddedIfNeeded();
    // 该方法 AbstractChannelHandlerContext 的实现如下
    ctx.fireChannelRegistered();
}

// AbstractChannelHandlerContext.fireChannelRegistered()
@Override
public ChannelHandlerContext fireChannelRegistered() {
    invokeChannelRegistered(findContextInbound());
    return this;
}

// AbstractChannelHandlerContext.findContextInbound()
// 查找下一个 inbound 节点
private AbstractChannelHandlerContext findContextInbound() {
    // AbstractChannelHandlerContext 拥有两个属性 inbound 和 outbound
    // inbound 代表 handler instanceof ChannelInboundHandler
    // outbound 代表 handler instanceof ChannelOutboundHandler
    AbstractChannelHandlerContext ctx = this;
    do {
        ctx = ctx.next;
    } while (!ctx.inbound);
    return ctx;
}

// AbstractChannelHandlerContext.invokeChannelRegistered(next)
// 正常情况下,在经历我们自定义的 handlerA、handlerB 后,总是到达 tail 节点,因为 TailContext 实现了 ChannelInboundHandler
static void invokeChannelRegistered(final AbstractChannelHandlerContext next) {
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {
        next.invokeChannelRegistered();
    } else {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                next.invokeChannelRegistered();
            }
        });
    }
}

绑定端口

回到 AbstractBootstrap.doBind(localAddress) 方法中,可以看到在初始化 channel 并将其注册到 NioEventLoop 的 selector 上后,才开始调用 doBind0 执行真正的绑定动作。

// AbstractBootstrap.doBind0(future, channel, localAddress, promise)
private static void doBind0(
        final ChannelFuture regFuture, final Channel channel,
        final SocketAddress localAddress, final ChannelPromise promise) {

    // 把绑定端口封装成一个 task 丢给 NioEventLoop 去执行
    channel.eventLoop().execute(new Runnable() {
        @Override
        public void run() {
            if (regFuture.isSuccess()) {
                // 调用 channel bind 
                channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
            } else {
                promise.setFailure(regFuture.cause());
            }
        }
    });
}

// AbstractChannel.bind(localAddress, promise)
@Override
public ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) {
    // 交给 pipeline 去处理 bind 操作
    return pipeline.bind(localAddress, promise);
}

// DefaultChannelPipeline.bind(localAddress, promise)
@Override
public final ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) {
    // 交给 tail 节点去处理
    return tail.bind(localAddress, promise);
}

// AbstractChannelHandlerContext.bind(localAddress, promise)
@Override
public ChannelFuture bind(final SocketAddress localAddress, final ChannelPromise promise) {
    if (localAddress == null) {
        throw new NullPointerException("localAddress");
    }
    if (isNotValidPromise(promise, false)) {
        // cancelled
        return promise;
    }
    // 从 tail 节点往前查找 ChannelOutboundHandler,直到 head 节点
    final AbstractChannelHandlerContext next = findContextOutbound();
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {
        next.invokeBind(localAddress, promise);
    } else {
        safeExecute(executor, new Runnable() {
            @Override
            public void run() {
                next.invokeBind(localAddress, promise);
            }
        }, promise, null);
    }
    return promise;
}

// AbstractChannelHandlerContext.invokeBind(localAddress, promise)
private void invokeBind(SocketAddress localAddress, ChannelPromise promise) {
    if (invokeHandler()) {
        try {
            // 最终到达 head 节点,handler() 返回 this,即调用 HeadContext.bind(ctx, localAddress, promise)
            ((ChannelOutboundHandler) handler()).bind(this, localAddress, promise);
        } catch (Throwable t) {
            notifyOutboundHandlerException(t, promise);
        }
    } else {
        bind(localAddress, promise);
    }
}

// HeadContext.bind(ctx, localAddress, promise)
@Override
public void bind(
        ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise)
        throws Exception {
    // 调用 unsafe 执行 bind 操作
    unsafe.bind(localAddress, promise);
}

// AbstractUnsafe.bind(localAddress, promise) 主要代码如下:
@Override
public final void bind(final SocketAddress localAddress, final ChannelPromise promise) {
    assertEventLoop();
    boolean wasActive = isActive();
    try {
        // 最终还是调用 NioServerSocketChannel.doBind(localAddress) 方法
        doBind(localAddress);
    } catch (Throwable t) {
        safeSetFailure(promise, t);
        closeIfClosed();
        return;
    }

    if (!wasActive && isActive()) {
        // 把 pipeline.fireChannelActive() 封装成 task,丢给 NioEventLoop 线程去执行
        invokeLater(new Runnable() {
            @Override
            public void run() {
                pipeline.fireChannelActive();
            }
        });
    }

    safeSetSuccess(promise);
}

// NioServerSocketChannel.doBind(localAddress)
@Override
protected void doBind(SocketAddress localAddress) throws Exception {
    // 依旧是通过 jdk 的 ServerSocketChannel 去 bind 的
    if (PlatformDependent.javaVersion() >= 7) {
        javaChannel().bind(localAddress, config.getBacklog());
    } else {
        javaChannel().socket().bind(localAddress, config.getBacklog());
    }
}

ChannelActive 事件在 pipeline 上的传播

上面看到在调用 jdk 的 ServerSocketChannel 执行 bind 操作后,将 ChannelActive 事件在 pipeline 上的传播封装成 task 丢给 NioEventLoop 线程去执行了,那么我们接着看 ChannelActive 事件在 pipeline 上的传播是否像 ChannelRegistered 的传播一样呢。

// DefaultChannelPipeline.fireChannelActive()
@Override
public final ChannelPipeline fireChannelActive() {
    // 依旧是从 head 节点开始传播
    AbstractChannelHandlerContext.invokeChannelActive(head);
    return this;
}

// AbstractChannelHandlerContext.invokeChannelActive(next)
static void invokeChannelActive(final AbstractChannelHandlerContext next) {
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {
        next.invokeChannelActive();
    } else {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                next.invokeChannelActive();
            }
        });
    }
}

// AbstractChannelHandlerContext.invokeChannelActive()
private void invokeChannelActive() {
    if (invokeHandler()) {
        try {
            // head 节点的 handler() 返回的是 this
            ((ChannelInboundHandler) handler()).channelActive(this);
        } catch (Throwable t) {
            notifyHandlerException(t);
        }
    } else {
        fireChannelActive();
    }
}

// HeadContext.channelActive(ctx)
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
    // 这里是将事件传递到下一个 ChannelInboundHandler,可以跟踪下看看是不是跟之前 ChannelRegistered 的传播有什么不一样
    // 多了个之前添加的 ServerBootstrapAcceptor,虽然它并没有重写 channelActive(),所以这里只是通过它简单的往下传递而已
    ctx.fireChannelActive();

    // 下面来看看这行代码背后做了哪些工作
    readIfIsAutoRead();
}

// HeadContext.readIfIsAutoRead()
private void readIfIsAutoRead() {
    // isAutoRead() 方法默认就返回 true
    if (channel.config().isAutoRead()) {
        channel.read();
    }
}

// AbstractChannel.read()
@Override
public Channel read() {
    pipeline.read();
    return this;
}

// DefaultChannelPipeline.read()
@Override
public final ChannelPipeline read() {
    // tail 节点的 read() 继承自 AbstractChannelHandlerContext
    tail.read();
    return this;
}

// AbstractChannelHandlerContext.read()
@Override
public ChannelHandlerContext read() {
    // 从 tail 节点往前查找 ChannelOutboundHandler,直到 head 节点
    final AbstractChannelHandlerContext next = findContextOutbound();
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {
        // 我们是在 NioEventLoop 线程中执行的,所以直接调用 invokeRead() 方法
        next.invokeRead();
    } else {
        Runnable task = next.invokeReadTask;
        if (task == null) {
            next.invokeReadTask = task = new Runnable() {
                @Override
                public void run() {
                    next.invokeRead();
                }
            };
        }
        executor.execute(task);
    }

    return this;
}

// AbstractChannelHandlerContext.invokeRead()
private void invokeRead() {
    if (invokeHandler()) {
        try {
            // head 节点的 handler() 方法返回 this,所以最终调用 HeadContext.read(ctx)
            ((ChannelOutboundHandler) handler()).read(this);
        } catch (Throwable t) {
            notifyHandlerException(t);
        }
    } else {
        read();
    }
}

// HeadContext.read(ctx)
@Override
public void read(ChannelHandlerContext ctx) {
    unsafe.beginRead();
}

// AbstractUnsafe.beginRead()
@Override
public final void beginRead() {
    assertEventLoop();

    if (!isActive()) {
        return;
    }

    try {
        // 最终调用 AbstractNioChannel.doBeginRead()
        doBeginRead();
    } catch (final Exception e) {
        invokeLater(new Runnable() {
            @Override
            public void run() {
                pipeline.fireExceptionCaught(e);
            }
        });
        close(voidPromise());
    }
}

// AbstractNioChannel.doBeginRead()
@Override
protected void doBeginRead() throws Exception {
    // Channel.read() or ChannelHandlerContext.read() was called
    final SelectionKey selectionKey = this.selectionKey;
    if (!selectionKey.isValid()) {
        return;
    }

    readPending = true;

    // 我们之前把 channel 注册到 selector 的时候,注册的 ops 为 0,
    // 这里我们通过 selectionKey.interestOps 方法将其修改为 SelectionKey.OP_ACCEPT,以便用来监听客户端的连接
    final int interestOps = selectionKey.interestOps();
    if ((interestOps & readInterestOp) == 0) {
        selectionKey.interestOps(interestOps | readInterestOp);
    }
}

服务端启动总结

  1. 首先创建两个 NioEventLoopGroup,一个 bossGroup 用于接收客户端的连接请求;一个 workerGroup 用于处理连接的 IO 读写操作;每个 NioEventLoopGroup 持有一个 EventExecutor[] 数组,用来保存 NioEventLoop;

  2. 创建 NioEventLoop 对象填充 NioEventLoopGroup 的 EventExecutor[] 数组;每个 NioEventLoop 都有自己的 executor、taskQueue、selector 等属性;

  3. 创建辅助类 ServerBootstrap,并设置 Reactor 线程模型、IO 模型、TCP 参数以及业务处理 handler 等

  4. 调用 ServerBootstrap 的 bind 方法进行端口绑定

    1. 反射创建一个 NioServerSocketChannel 对象,并进行初始化(设置一些属性后,在 pipeline 后添加一个 ChannelInitializer,重写的 initChannel 方法在 handlerAdded 方法中调用)

    2. 从 NioEventLoopGroup 选出一个 NioEventLoop,将 channel 注册到它的 selector 上,最终调用的是 jdk 的 ServerSocketChannel 的注册方法,但是 ops 为 0

    3. 注册成功后,进行 handlerAdded、ChannelRegistered 等事件的触发

    4. 然后开始真正的绑定端口,最终还是调用 jdk 的 ServerSocketChannel 的 bind 方法

    5. 进行 ChannelActive 事件的传播,其中会修改 channel 注册到 selector 上返回的 selectionKey 的 ops 为 SelectionKey.OP_ACCEPT,以便用来监听客户端的连接

  • 一句话总结:初始化 channel 后,注册到 selector 上,触发 handlerAdded、ChannelRegistered 事件后绑定端口,接着触发 ChannelActive 事件,其中又会修改 selectionKey 的 ops 以便监听客户端的连接

Java并发编程之 synchronized

简介

synchronized 作为 Java 中的一个关键字,保证了多个线程访问同一个段代码时的数据安全。
本篇文章主要介绍其在 JVM 层面的实现原理以及 JDK1.6 之后的几种锁优化策略。

使用方式

普通同步方法

当一个线程要访问一个普通同步方法,访问前需要获取当前 实例对象 的锁,离开该方法时要释放该锁。使用方式如下:

private synchronized void testSynchronizedMethod() {
 
}

静态同步方法

当一个线程要访问一个静态同步方法,访问前需要获取当前 类对象 的锁,离开该方法时要释放该锁。使用方式如下:

private static synchronized void testSynchronizedStaticMethod() {
 
}

同步代码块

当一个线程要访问一个同步代码块,访问前需要获取 指定对象 的锁,离开该方法时要释放该锁。使用方式如下:

private void testSynchronizedBlock() {
    // 这里要获取this即当前实例对象的锁
    synchronized (this) {
    
    }
    
    // 这里要获取Test类对象的锁
    synchronized (Test.class) {
    
    }
}

JVM 层面原理解析

通过 javap 命令可以查看到同步方法是在该方法上添加了 ACC_SYNCHRONIZED 标识,而同步代码块则是添加了 monitorentermonitorexit 两个指令实现的。
monitorenter 指向同步代码块的开始位置,monitorexit 指向同步代码块的结束位置。下面逐步分析实现原理。

oop-klass model

我们都知道创建 Java 对象的时候是保存在堆中的,那么对象在 JVM 中是什么样的结构呢?下面基于 HotSpot 虚拟机来分析一下。

HotSpot 是基于 c++ 实现的,而 c++ 也是面向对象的,那么是否只要创建一个与 Java 对象对应的 c++ 对象就可以了呢?HotSpot 虚拟机并没有这么做,
而是采用了 oop-klass 模型,oop (Ordinary Object Pointer)指的是简单对象指针,而 klass 用来描述对象实例的具体类型。

oop

每创建一个 Java 对象,JVM 就会创建一个对应的 OOP 对象,例如当我们用 new 创建一个 Java 对象实例的时候,JVM 创建一个对应的 instanceOopDesc 对象来表示这个 Java 对象,
当我们用 new 创建一个 Java 数组实例的时候,JVM 创建一个对应的 arrayOopDesc 对象来表示这个数组对象。

在 HotSpot 中,oopDesc 类定义在 oop.hpp 中,instanceOopDesc 定义在 instanceOop.hpp 中,arrayOopDesc 定义在 arrayOop.hpp中。
instanceOopDesc 和 arrayOopDesc 均继承自 oopDesc,我们来看下 oopDesc 的定义:

class oopDesc {
  friend class VMStructs;
 private:
  volatile markOop  _mark;
  union _metadata {
    Klass*      _klass;
    narrowKlass _compressed_klass;
  } _metadata;

  // 其它一些field

}

回忆一下之前介绍的 Java 对象在内存中的布局,包含三个部分:对象头、实例数据和对象填充。我们这里看到的 markOop_metadata 就对应对象头中的 Mark Word 和元数据指针。
而实例数据存在其它的各种 field 中。

我们知道对象头中有和锁相关的运行时数据,这些运行时数据是 synchronized 以及其他类型的锁实现的重要基础,而关于锁标记、GC分代等信息均保存在 _mark 中。
_metadata 中包含了普通指针 _klass 和压缩类指针 _compressed_klass,这两个指针都指向下面要说的 instanceKlass 对象,它用来描述对象的具体类型。

klass

JVM在运行时,需要一种用来标识 Java 内部类型的机制。在 HotSpot 中的解决方案是:为每一个已加载的 Java 类创建一个 instanceKlass 对象,用来在 JVM 层表示 Java 类。
其实这个 instanceKlass 也就是我们平常所说的保存在方法区(或元空间)的类的元数据。

总结

当 JVM 加载一个类的时候,在方法区(或元空间)创建一个 instanceKlass 对象来表示该 Java 类。然后当我们通过 new 创建一个对象的时候,在堆中创建一个 instanceOopDesc
对象,包含了对象头和实例数据。对象头又包含 _mark_metadata,实例数据用其它一些 field 保存。
其中 _mark 就是对象头中的 Mark Word,其中存放有一些运行时数据,包括和多线程相关的锁的信息,_metadata 存放的指针,指向对象所属的类 instanceKlass(也就是我们说的类的元数据)。

Java 对象头

  • 第一部分用于存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启指针压缩)中分别为32bit 和 64bit ,官方称之为“Mark Word”。
  • 对象头的另一部分是类型指针。即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。如果对象是一个 Java 数组,那么在对象头中还必须有一块用于记录数组长度的数据。

markOop.hpp 中我们可以看到:

enum { age_bits                 = 4,
       lock_bits                = 2,
       biased_lock_bits         = 1,
       max_hash_bits            = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
       hash_bits                = max_hash_bits > 31 ? 31 : max_hash_bits,
       cms_bits                 = LP64_ONLY(1) NOT_LP64(0),
       epoch_bits               = 2
};

从上面的枚举定义中可以看出,对象头中主要包含了GC分代年龄、锁状态标记、哈希码、epoch等信息。

对象的状态一共有五种,分别是无锁态、轻量级锁、重量级锁、GC标记和偏向锁。在 32 位的虚拟机中有两个 bits 是用来存储锁的标记为的,但是我们都知道,
两个 bits 最多只能表示四种状态:00、01、10、11,那么第五种状态如何表示呢 ,就要额外依赖 1 bit 的空间,使用 0 和 1 来区分。

markOop.hpp 中我们可以看到对象的相关状态:

enum { locked_value             = 0,    // 轻量级锁:偏向锁状态0  锁状态00  即000
       unlocked_value           = 1,    // 无锁状态:偏向锁状态0  锁状态01  即001
       monitor_value            = 2,    // 重量级锁:偏向锁状态0  锁状态10  即010
       marked_value             = 3,    // gc标记: 偏向锁状态0  锁状态11  即011  
       biased_lock_pattern      = 5     // 偏向锁: 偏向锁状态1  锁状态01  即101
};
  • 当对象状态为偏向锁(biasable)时,Mark Word 存储的是偏向的线程ID
  • 当状态为轻量级锁(lightweight locked)时,Mark Word 存储的是指向线程栈中 Lock Record 的指针
  • 当状态为重量级锁(inflated)时,Mark Word 存储的是指向堆中的 monitor 对象的指针

Monitor 的实现原理

我们前面一直说的都是获取对象的锁,那么这个锁到底是什么,JVM 是这么处理获取锁和释放锁的呢?

管程

我们先来了解一下管程,维基百科的解释如下:

  • 管程 (英语:Monitors,也称为监视器) 是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。
    这些共享资源一般是硬件设备或一群变量。管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。
    与那些通过修改数据结构实现互斥访问的并发程序设计相比,管程实现很大程度上简化了程序设计。

这么解释还是有点抽象,简单来说,管程是一种封装了互斥量和信号量的机制,为了简化同步调用的过程。

ObjectMonitor

HotSpot 用 ObjectMonitor 来实现管程,这个也就是我们之前所说的锁。在 objectMonitor.hpp 中可以看到:

ObjectMonitor() {
  _header       = NULL;
  _count        = 0;
  _waiters      = 0,
  _recursions   = 0;
  _object       = NULL;
  _owner        = NULL;
  _WaitSet      = NULL;
  _WaitSetLock  = 0 ;
  _Responsible  = NULL ;
  _succ         = NULL ;
  _cxq          = NULL ;
  FreeNext      = NULL ;
  _EntryList    = NULL ;
  _SpinFreq     = 0 ;
  _SpinClock    = 0 ;
  OwnerIsThread = 0 ;
  _previous_owner_tid = 0;
}

其中有几个关键属性:

  1. _owner:指向持有该 ObjectMonitor 对象的线程
  2. _WaitSet:存放处于wait状态的线程队列
  3. _EntryList:存放处于等待锁block的线程队列
  4. _recursions:锁的重入次数

当多个线程同时访问同步代码的时候,会进入 EntryList,然后当某个线程获取到对象的 monitor 后,将 owner 变量设置为当前线程,同时计数器 count 加 1,即对象获得了锁。
若持有 monitor 的线程调用 wait() 方法,将会释放当前持有的 monitor,owner 变量恢复为 null,count 自减 1,同时该线程进入 WaitSet 集合中等待被唤醒。
若当前线程执行完同步代码也将释放持有的 monitor 并复位变量,以便其它的线程的可以获取该 monitor。

锁优化技术

事实上,只有在 JDK1.6 之前,synchronized 的实现才会直接调用 ObjectMonitor 的 enter 和 exit,是使用操作系统互斥量(mutex)来实现的传统锁,如果要阻塞或唤醒一个线程就需要操作系统的帮忙,
这就要从用户态转换到核心态,因此状态转换需要花费很多的处理器时间,所以这种锁被称之为重量级锁。

高效并发是从 JDK1.5 到 JDK1.6 的一个重要改进,HotSpot 虚拟机开发团队在这个版本中花费了很大的精力去对 Java 中的锁进行优化,如适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等。
这些技术都是为了在线程之间更高效的共享数据,以及解决竞争问题。

偏向锁

研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块的时候,通过 CAS 修改 Mark Word 的 ThreadID 为当前线程,
如果修改成功,则获得锁,那么以后线程再次进入和退出同步块时,就不需要使用 CAS 来获取锁,只是简单的测试一个对象头中的 Mark Word 字段中是否存储着指向当前线程的偏向锁;
如果使用 CAS 设置失败时,说明存在锁的竞争,那么将执行偏向锁的撤销操作 (revoke bias),将偏向锁升级为轻量级锁。

撤销偏向的操作需要在全局安全点执行。我们假设线程A曾经拥有锁(不确定是否释放锁), 线程B来竞争锁对象,如果当线程A不在拥有锁时或者死亡时,线程B直接去尝试获得锁(根据是否允许重偏向,
获得偏向锁或者轻量级锁);如果线程A仍然拥有锁,那么锁升级为轻量级锁,线程B自旋请求获得锁。

轻量级锁

在进入同步块前,在栈空间中创建用于存储锁记录的空间,并将对象头的 Mark Word 拷贝到锁记录中,官方称之为 Displaced Mark Word。然后线程尝试用 CAS 将对象头的 Mark Word 替换为指向锁记录的指针。
如果成功,当前线程获得锁;如果失败,先检查对象头的 Mark Word 是否指向当前线程栈桢的锁记录,如果指向则说明当前线程已经获得了这个对象的锁,否则说明有其它线程竞争锁,当前线程便尝试使用自旋来获取锁。
当竞争线程的自旋次数 达到界限值,轻量级锁将会膨胀为重量级锁。

轻量级锁解锁时,会使用 CAS 操作, 将 Displaced Mark Word 替换到对象头,如果成功,则表示没有竞争发生。如果失败, 表示当前锁存在锁竞争,其它竞争线程已经升级了轻量级锁,
也就是将 Mark Word 中关于锁的部分更新为指向 monitor 的指针了。当前解锁线程按重量级锁解锁流程处理。

自旋锁

获取锁失败的时候,通过操作系统挂起当前线程后,拥有锁的线程马上就释放了锁并唤醒了刚才挂起的那个线程,这种情况下还不如让线程在获取锁失败的时候一直在那自旋获取,虽然占用一定的 CPU 处理时间,但是远比线程的挂起、唤醒消耗小。

锁消除

虚拟机通过逃逸分析判断某个基于锁的操作根本不会存在竞争(例如局部变量),虚拟机会直接去除掉这个加锁操作。

锁粗化

通常来说,编写同步代码时,需要将同步的代码控制到一个比较小的范围,但是如果在一段代码中,频繁的基于同一个对象加锁解锁,还不如扩大加锁的范围,减少加锁解锁的次数。

总结

  • 偏向锁:只需要一次 CAS 修改 Mark Word 的操作,不需要额外的消耗,但是如果存在线程竞争,则会带来额外的锁撤销的消耗,因此适用于只有一个线程访问的情况。
  • 轻量级锁:竞争锁的线程通过自旋获取锁不会阻塞,提高了程序的响应速度,但是对于始终获取不到锁的线程,则会因为自旋消耗 CPU,因此适用于追求响应时间,同步块执行速度块,且锁竞争不激烈的情况。
  • 重量级锁:线程竞争不使用自旋不消耗 CPU,但是会因为线程阻塞导致响应时间缓慢,因此适用于追求吞吐量,同步块执行速度慢,竞争线程激烈的情况。

代理模式及动态代理

代理模式

定义:给某个对象提供一个代理对象,用户通过代理对象控制对原对象的访问。

上图中: - RealSubject 是原对象(本文把原对象称为"委托对象"),Proxy 是代理对象。 - Subject 是委托对象和代理对象都共同实现的接口。 - Request() 是委托对象和代理对象共同拥有的方法。

JAVA实现如下:

public class ProxyDemo {
    public static void main(String args[]){
        RealSubject subject = new RealSubject();
        Proxy p = new Proxy(subject);
        p.request();
    }
}

interface Subject{
    void request();
}

class RealSubject implements Subject{
    public void request(){
        System.out.println("request");
    }
}

class Proxy implements Subject{
    private Subject subject;
    public Proxy(Subject subject){
        this.subject = subject;
    }
    public void request(){
        System.out.println("PreProcess");
        subject.request();
        System.out.println("PostProcess");
    }
}

代理分为两类:

  • 静态代理:代理类实现就写好了,在Java编译后是一个实际存在的class文件。
  • 动态代理:代理类是在程序运行时动态生成的类字节码,并加载到JVM中。

上文中所说的是静态代理。其中动态代理又可以分为JDK动态代理和CGLIB代理。

动态代理

JDK动态代理

JDK动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandle来处理的。它只能对实现了接口的类进行代理。主要代码实现如下:

public class JDKProxy implements InvocationHandler {    
    private Object targetObject;//需要代理的目标对象    
    public Object newProxy(Object targetObject) {//将目标对象传入进行代理    
    	this.targetObject = targetObject;     
    	return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(),targetObject.getClass().getInterfaces(), this);//返回代理对象 
    }    
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {    
        checkPopedom();//一般我们进行逻辑处理的函数比如这个地方是模拟检查权限    
        Object ret = null;      // 设置方法的返回值    
        ret  = method.invoke(targetObject, args);       //调用invoke方法,ret存储该方法的返回值    
        return ret;    
    }    
    private void checkPopedom() {//模拟检查权限的例子    
        System.out.println(".:检查权限  checkPopedom()!");    
    }    
}    

Proxy类的newProxyInstance()方法大致如下:

public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h) throws IllegalArgumentException{
    if (h == null) {
        throw new NullPointerException();	//必须要有一个InvocationHandle实例
    }

    Class<?> cl = getProxyClass0(loader, interfaces); // 获得代理类(重要!进去看看)

    /*
     * 调用构造器获得一个实例
     * Invoke its constructor with the designated invocation handler.
     */
    try {
        final Constructor<?> cons = cl.getConstructor(constructorParams);
        final InvocationHandler ih = h;
        SecurityManager sm = System.getSecurityManager();
        if (sm != null && ProxyAccessHelper.needsNewInstanceCheck(cl)) {
            // create proxy instance with doPrivilege as the proxy class may
            // implement non-public interfaces that requires a special permission
            return AccessController.doPrivileged(new PrivilegedAction<Object>() {
                public Object run() {
                    return newInstance(cons, ih);
                }
            });
        } else {
            return newInstance(cons, ih);
        }
    } catch (NoSuchMethodException e) {
        throw new InternalError(e.toString());
    }
}

getProxyClass0()方法很长,挑些重点大致如下:

private static Class<?> getProxyClass0(ClassLoader loader,Class<?>... interfaces) {

    if (interfaces.length > 65535) {
        throw new IllegalArgumentException("interface limit exceeded");
    }

    Class<?> proxyClass = null;
    String[] interfaceNames = new String[interfaces.length];
    Set<Class<?>> interfaceSet = new HashSet<>();
    for (int i = 0; i < interfaces.length; i++) {	//根据接口名称反射生成对应的Class并放到Set中
        String interfaceName = interfaces[i].getName();
        Class<?> interfaceClass = null;
        try {
            interfaceClass = Class.forName(interfaceName, false, loader);
        } catch (ClassNotFoundException e) {
        }
        if (interfaceClass != interfaces[i]) {
            throw new IllegalArgumentException(
                interfaces[i] + " is not visible from class loader");
        }
        if (!interfaceClass.isInterface()) {
            throw new IllegalArgumentException(
                interfaceClass.getName() + " is not an interface");
        }
        if (interfaceSet.contains(interfaceClass)) {
            throw new IllegalArgumentException(
                "repeated interface: " + interfaceClass.getName());
        }
        interfaceSet.add(interfaceClass);
        interfaceNames[i] = interfaceName;
    }

    List<String> key = Arrays.asList(interfaceNames);	//用接口名称的List作为cache的key
    /*
     * Find or create the proxy class cache for the class loader.
     */
    Map<List<String>, Object> cache;
    synchronized (loaderToCache) {	//loadToCache是一个Map<ClassLoader, Map<List<String>, Object>>
        cache = loaderToCache.get(loader);
        if (cache == null) {
            cache = new HashMap<>();
            loaderToCache.put(loader, cache);
        }
    }

    synchronized (cache) {
		//对缓存的一些处理,暂时不看
    }

    try {
        String proxyPkg = null;     // package to define proxy class
        for (int i = 0; i < interfaces.length; i++) {	
            int flags = interfaces[i].getModifiers();
            if (!Modifier.isPublic(flags)) {		//对于非公共接口,代理类包名和接口包名相同
                String name = interfaces[i].getName();
                int n = name.lastIndexOf('.');
                String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
                if (proxyPkg == null) {
                    proxyPkg = pkg;
                } else if (!pkg.equals(proxyPkg)) {
                    throw new IllegalArgumentException(
                        "non-public interfaces from different packages");
                }
            }
        }

        if (proxyPkg == null) {		//对于公共接口,包名为默认的"com.sun.proxy"
            proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
        }

        {
            long num;
            synchronized (nextUniqueNumberLock) {
                num = nextUniqueNumber++;
            }
            String proxyName = proxyPkg + proxyClassNamePrefix + num;	//生成代理类的完全限定名
            byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces);	//产生代理类字节码(进去看看)
            try {
				//根据字节码返回相应的class实例
                proxyClass = defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length);
            } catch (ClassFormatError e) {
                throw new IllegalArgumentException(e.toString());
            }
        }
        // add to set of all generated proxy classes, for isProxyClass
        proxyClasses.put(proxyClass, null);

    } finally {
        synchronized (cache) {
            if (proxyClass != null) {
                cache.put(key, new WeakReference<Class<?>>(proxyClass));
            } else {
                cache.remove(key);
            }
            cache.notifyAll();
        }
    }
    return proxyClass;
}

ProxyGenerator是sun.misc包中的类,generateProxyClass()方法如下:

public static byte[] generateProxyClass(final String var0, Class[] var1) {
    ProxyGenerator var2 = new ProxyGenerator(var0, var1);
    final byte[] var3 = var2.generateClassFile();
	// 这里根据参数配置,决定是否把生成的字节码(.class文件)保存到本地磁盘,我们可以通过把相应的class文件保存到本地,再反编译来看看具体的实现,这样更直观 
    if(saveGeneratedFiles) {
        AccessController.doPrivileged(new PrivilegedAction() {
            public Void run() {
                try {
                    FileOutputStream var1 = new FileOutputStream(ProxyGenerator.dotToSlash(var0) + ".class");
                    var1.write(var3);
                    var1.close();
                    return null;
                } catch (IOException var2) {
                    throw new InternalError("I/O exception saving generated file: " + var2);
                }
            }
        });
    }

    return var3;
}

saveGeneratedFiles如下:

    private static final boolean saveGeneratedFiles = ((Boolean)AccessController.doPrivileged(new GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles"))).booleanValue();

GetBooleanAction实际上是调用Boolean.getBoolean(propName)来获得的,而Boolean.getBoolean(propName)调用了System.getProperty(name),所以我们可以设置sun.misc.ProxyGenerator.saveGeneratedFiles这个系统属性为true来把生成的class保存到本地文件来查看。
这里要注意,当把这个属性设置为true时,生成的class文件及其所在的路径都需要提前创建,否则会抛出FileNotFoundException异常。即我们要在运行当前main方法的路径下创建com/sun/proxy目录,并创建一个$Proxy0.class文件,才能够正常运行并保存class文件内容。
反编译$Proxy0.class文件,如下所示:

package com.sun.proxy;  
  
import com.mikan.proxy.HelloWorld;  
import java.lang.reflect.InvocationHandler;  
import java.lang.reflect.Method;  
import java.lang.reflect.Proxy;  
import java.lang.reflect.UndeclaredThrowableException;  

public final class $Proxy0 extends Proxy implements HelloWorld {  
  private static Method m1;  
  private static Method m3;  
  private static Method m0;  
  private static Method m2;  
  
  public $Proxy0(InvocationHandler paramInvocationHandler) {  
    super(paramInvocationHandler);  
  }  
  
  public final boolean equals(Object paramObject) {  
    try {  
      return ((Boolean)this.h.invoke(this, m1, new Object[] { paramObject })).booleanValue();  
    }  
    catch (Error|RuntimeException localError) {  
      throw localError;  
    }  
    catch (Throwable localThrowable) {  
      throw new UndeclaredThrowableException(localThrowable);  
    }  
  }  
  
  public final void sayHello(String paramString) {  
    try {  
      this.h.invoke(this, m3, new Object[] { paramString });  
      return;  
    }  
    catch (Error|RuntimeException localError) {  
      throw localError;  
    }  
    catch (Throwable localThrowable) {  
      throw new UndeclaredThrowableException(localThrowable);  
    }  
  }  
  
  public final int hashCode() {  
    try {  
      return ((Integer)this.h.invoke(this, m0, null)).intValue();  
    }  
    catch (Error|RuntimeException localError) {  
      throw localError;  
    }  
    catch (Throwable localThrowable) {  
      throw new UndeclaredThrowableException(localThrowable);  
    }  
  }  
  
  public final String toString() {  
    try {  
      return (String)this.h.invoke(this, m2, null);  
    }  
    catch (Error|RuntimeException localError) {  
      throw localError;  
    }  
    catch (Throwable localThrowable) {  
      throw new UndeclaredThrowableException(localThrowable);  
    }  
  }  
  
  static {  
    try {  
      m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });  
      m3 = Class.forName("com.mikan.proxy.HelloWorld").getMethod("sayHello", new Class[] { Class.forName("java.lang.String") });  
      m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);  
      m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);  
      return;  
    }  
    catch (NoSuchMethodException localNoSuchMethodException) {  
      throw new NoSuchMethodError(localNoSuchMethodException.getMessage());  
    }  
    catch (ClassNotFoundException localClassNotFoundException) {  
      throw new NoClassDefFoundError(localClassNotFoundException.getMessage());  
    }  
  }  
}  

可以看到动态生成的代理类有如下特点:

  1. 继承Proxy,实现代理的接口,由于java是单继承,所以JDK动态代理不支持对实现类的代理,只支持对接口的代理
  2. 提供一个InvocationHandle作为构造器参数
  3. 用静态代码块来初始化接口中方法的Method对象,以及Object中equals、hashCode、toString的Method对象
  4. 重写了Object类的equals、hashCode、toString,它们都只是简单的调用了InvocationHandler的invoke方法,即可以对其进行特殊的操作,也就是说JDK的动态代理还可以代理上述三个方法。
  5. 代理类实现代理接口的sayHello方法中,只是简单的调用了InvocationHandler的invoke方法,我们可以在invoke方法中进行一些特殊操作,甚至不调用实现的方法,直接返回。

CGLIB代理

CGLIB代理是利用asm开源包,通过修改指定的类加载后的字节码生成一个子类实现的。CGLIB是针对类实现代理(无需该类实现接口),主要是对指定的类生成一个子类,覆盖其中的方法,因为是继承,所以该类或方法不要声明成final的。主要代码实现如下:

public class CGLibProxy implements MethodInterceptor {    
    private Object targetObject;// CGLib需要代理的目标对象    
    public Object createProxyObject(Object obj) {    
        this.targetObject = obj;    
        Enhancer enhancer = new Enhancer();    
        enhancer.setSuperclass(obj.getClass());    
        enhancer.setCallback(this);    
        Object proxyObj = enhancer.create();    
        return proxyObj;// 返回代理对象    
    }    
    public Object intercept(Object proxy, Method method, Object[] args,    
            MethodProxy methodProxy) throws Throwable {    
        Object obj = null;    
        if ("addUser".equals(method.getName())) {// 过滤方法    
            checkPopedom();// 检查权限    
        }    
        obj = method.invoke(targetObject, args);    
        return obj;    
    }    
    private void checkPopedom() {    
        System.out.println(".:检查权限  checkPopedom()!");    
    }    
}    

动态代理的应用

AOP的实现方式就是通过对目标对象的代理在连接点前加入通知,完成统一的切面操作。Spring提供JDKProxy和CGLIB两种方式来生成代理对象,具体使用哪种方式生成由AopProxyFactory根据AdvisedSupport对象的配置来决定。
默认方式是如果目标类实现了接口,则使用JDK动态代理,如果目标类没有实现接口,则采用CGLIB代理。 如果目标对象实现了接口,可以强制使用CGLIB实现代理(添加CGLIB库,并在spring配置中加入<aop:aspectj-autoproxy proxy-target-class="true"/>)。

代理模式应用

  • 远程代理:也就是为一个对象在不同的地址空间提供局部代表,这样可以隐藏一个对象存在于不同地址空间的事实。
  • 虚拟代理:是根据需要创建开销很大的对象。通过它来存放实例化需要很长时间的真实对象。这样就可以达到性能的最优化,比如说你打开一个很大的HTML网页时,里面可能有很多的文字和图片,但是你还是能很快的打开它,此时你看到的是所有的文字,但图片确实一张一张加载后才能看到。那些未打开的图片框,就是通过虚拟代理来代替真实的图片,此时代理存储了真实图片的路径和尺寸。
  • 安全代理:用来控制真实对象访问时的权限。一般用于对象应该有不同的访问权限的时候。
  • 智能指引:是指当调用真实对象的时候,代理处理另外一些事。

Netty 入门使用总结

Netty 是什么? 为什么要用 Netty?

Netty 是一个异步事件驱动的网络应用框架,用于快速开发可维护的高性能服务器和客户端。

使用 Netty 而不使用 Java 原生 NIO 的原因如下:

  1. 使用 Java NIO 需要了解太多概念,编程复杂
  2. Netty 底层 IO 模型切换简单,改改参数,直接从 NIO 模型变为 IO 模型
  3. Netty 自带的拆包解包、异常检测等机制,让你只需要关注业务逻辑
  4. Netty 解决了 JDK 很多包括空轮询在内的 BUG
  5. Netty 底层对线程,selector 做了很多细小的优化,精心设计的 reactor 线程模型做到非常高效的并发处理
  6. 自带各种协议栈让你处理任何一种通用协议都几乎不用亲自动手
  7. Netty 已经历各大 RPC 框架,消息中间件,分布式通信中间件线上的广泛验证,健壮性无比强大

启动流程

服务端

public class NettyServer {
    public static void main(String[] args) {
        ServerBootstrap serverBootstrap = new ServerBootstrap();

        NioEventLoopGroup boss = new NioEventLoopGroup();
        NioEventLoopGroup worker = new NioEventLoopGroup();
        serverBootstrap
                // 指定线程模型
                .group(boss, worker)
                // 指定 IO 模型
                .channel(NioServerSocketChannel.class)
                // tcp 相关设置
                .option(ChannelOption.SO_BACKLOG, 1024)
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                .childOption(ChannelOption.TCP_NODELAY, true)
                // 指定业务处理逻辑
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    protected void initChannel(NioSocketChannel ch) {
                        ch.pipeline().addLast(new FirstServerHandler());
                    }
                })
                .bind(8888);
    }
}
  1. 创建一个服务端启动辅助类 ServerBootstrap。
  2. 通过 ServerBootstrap 配置线程模型、IO模型、tcp相关参数和 channelHandler 等。
  3. 绑定端口。

其中 boss 线程组负责创建新连接,worker 线程组负责读取数据以及业务处理。

客户端

public static void main(String[] args) {
    NioEventLoopGroup workerGroup = new NioEventLoopGroup();
    
    Bootstrap bootstrap = new Bootstrap();
    bootstrap
            // 1.指定线程模型
            .group(workerGroup)
            // 2.指定 IO 类型为 NIO
            .channel(NioSocketChannel.class)
            // 3.IO 处理逻辑
            .handler(new ChannelInitializer<SocketChannel>() {
                @Override
                public void initChannel(SocketChannel ch) {
                    ch.pipeline().addLast(new FirstClientHandler());
                }
            });
    // 4.建立连接
    bootstrap.connect("127.0.0.1", 8888).addListener(future -> {
        if (future.isSuccess()) {
            System.out.println("连接成功!");
        } else {
            System.err.println("连接失败!");
        }

    });
}
  1. 和服务端一样,创建一个辅助类 Bootstrap,然后配置线程模型、IO模型和 handler 等。
  2. 调用 connect 方法进行连接,可以看到 connect 方法有两个参数,第一个参数可以填写 IP 或者域名,第二个参数填写的是端口号,
    由于 connect 方法返回的是一个 Future,也就是说这个方是异步的,我们通过 addListener 方法可以监听到连接是否成功,进而打印出连接信息。

客户端和服务端双向通信

客户端发送数据到服务端

上面客户端启动代码中向 pipeline 添加了一个 FirstClientHandler。pipeline 是和这条连接相关的逻辑处理链,采用了责任链模式。
然后编写 FirstClientHandler 代码如下:

public class FirstClientHandle extends ChannelHandlerAdapter {

    /**
     * 这个方法会在客户端连接建立成功之后被调用
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println(new Date() + ": 客户端写出数据");

        ByteBuf buffer = getByteBuf(ctx);
        ctx.channel().writeAndFlush(buffer);
    }

    /**
     * 获取一个 netty 对二进制数据的抽象 ByteBuf
     * @param ctx
     * @return
     */
    private ByteBuf getByteBuf(ChannelHandlerContext ctx) {
        // 1. 获取二进制抽象 ByteBuf
        ByteBuf buffer = ctx.alloc().buffer();
        // 2. 准备数据,指定字符串的字符集为 utf-8
        byte[] bytes = "Hello Lollipop, This is my netty demo!".getBytes(Charset.forName("UTF-8"));
        // 3. 填充数据到 ByteBuf
        buffer.writeBytes(bytes);
        return buffer;
    }
}

写数据的逻辑分为两步:首先我们需要获取一个 netty 对二进制数据的抽象 ByteBuf,上面代码中, ctx.alloc() 获取到一个 ByteBuf 的内存管理器,
这个内存管理器的作用就是分配一个 ByteBuf,然后我们把字符串的二进制数据填充到 ByteBuf,这样我们就获取到了 Netty 需要的一个数据格式,
最后我们调用 ctx.channel().writeAndFlush() 把数据写到服务端。

服务端读取客户端发送的数据

在服务端启动代码中向 pipeline 中添加一个 FirstServerHandler。与 FirstClientHandler 类似:

public class FirstServerHandler extends ChannelHandlerAdapter {

    /**
     * 这个方法在接收到客户端发来的数据之后被回调。
     * @param ctx
     * @param msg
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf byteBuf = (ByteBuf) msg;

        System.out.println(new Date() + ": 服务端读到数据 -> " + byteBuf.toString(Charset.forName("utf-8")));
    }
}

这里的 msg 就是我们客户端发送的 ByteBuf,需要我们强转一下。然后我们直接打印。
之后启动服务端、客户端,可以看到服务端确实打印了客户端发送的数据。

服务端回复数据给客户端

与客户端发送数据类似,同样准备一个 ByteBuf 然后发送即可。修改 FirstServerHandler 代码如下:

/**
 * 这个方法在接收到客户端发来的数据之后被回调。
 * @param ctx
 * @param msg
 */
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    ByteBuf byteBuf = (ByteBuf) msg;
    System.out.println(new Date() + ": 服务端读到数据 -> " + byteBuf.toString(Charset.forName("utf-8")));

    // 服务端向客户端发送数据
    System.out.println(new Date() + ": 服务端写出数据");
    ByteBuf buf = getByteBuf(ctx);
    ctx.channel().writeAndFlush(buf);
}

private ByteBuf getByteBuf(ChannelHandlerContext ctx) {
    byte[] bytes = "你好,我已经收到了你的消息!".getBytes(Charset.forName("utf-8"));
    ByteBuf buffer = ctx.alloc().buffer();
    buffer.writeBytes(bytes);
    return buffer;
}

客户端读取服务端发送的数据

修改 FirstClientHandle 代码如下:

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    ByteBuf byteBuf = (ByteBuf) msg;

    System.out.println(new Date() + ": 客户端读到数据 -> " + byteBuf.toString(Charset.forName("utf-8")));
}

总结

  • 客户端和服务端的逻辑处理是均是在启动的时候,通过给逻辑处理链 pipeline 添加逻辑处理器,来编写数据的读写逻辑
  • 在客户端连接成功之后会回调到逻辑处理器的 channelActive() 方法,而不管是服务端还是客户端,收到数据之后都会调用到 channelRead 方法
  • 写数据调用writeAndFlush方法,客户端与服务端交互的二进制数据载体为 ByteBuf,ByteBuf 通过连接的内存管理器创建,
    字节数据填充到 ByteBuf 之后才能写到对端

拆包粘包理论与解决方案

ByteBuf

  • ByteBuf 是 Netty 中客户端与服务端交互的二进制数据载体
  • ByteBuf 通过 readerIndex 和 readerIndex 两个指针划分可读字节和可写字节
  • 从 ByteBuf 中每读取一个字节,readerIndex 自增1,ByteBuf 里面总共有 writerIndex-readerIndex 个字节可读,
    由此可以推论出当 readerIndex 与 writerIndex 相等的时候,ByteBuf 不可读
  • 写数据是从 writerIndex 指向的部分开始写,每写一个字节,writerIndex 自增1,直到增到 capacity,这个时候,表示 ByteBuf 已经不可写了
  • ByteBuf 里面其实还有一个参数 maxCapacity,当向 ByteBuf 写数据的时候,如果容量不足,那么这个时候可以进行扩容,
    直到 capacity 扩容到 maxCapacity,超过 maxCapacity 就会报错

为什么会有粘包半包现象

我们需要知道,尽管我们在应用层面使用了 Netty,但是对于操作系统来说,只认 TCP 协议,尽管我们的应用层是按照 ByteBuf 为 单位来发送数据,
但是到了底层操作系统仍然是按照字节流发送数据,因此,数据到了服务端,也是按照字节流的方式读入,然后到了 Netty 应用层面,重新拼装成 ByteBuf,
而这里的 ByteBuf 与客户端按顺序发送的 ByteBuf 可能是不对等的。因此,我们需要在客户端根据自定义协议来组装我们应用层的数据包,
然后在服务端根据我们的应用层的协议来组装数据包,这个过程通常在服务端称为拆包,而在客户端称为粘包。

拆包和粘包是相对的,一端粘了包,另外一端就需要将粘过的包拆开,举个栗子,发送端将三个数据包粘成两个 TCP 数据包发送到接收端,
接收端就需要根据应用协议将两个数据包重新组装成三个数据包。

拆包的原理

在没有 Netty 的情况下,用户如果自己需要拆包,基本原理就是不断从 TCP 缓冲区中读取数据,每次读取完都需要判断是否是一个完整的数据包

  • 如果当前读取的数据不足以拼接成一个完整的业务数据包,那就保留该数据,继续从 TCP 缓冲区中读取,直到得到一个完整的数据包。
  • 如果当前读到的数据加上已经读取的数据足够拼接成一个数据包,那就将已经读取的数据拼接上本次读取的数据,构成一个完整的业务数据包传递到业务逻辑,
    多余的数据仍然保留,以便和下次读到的数据尝试拼接。

如果我们自己实现拆包,这个过程将会非常麻烦,我们的每一种自定义协议,都需要自己实现,还需要考虑各种异常,而 Netty 自带的一些开箱即用的拆包器已经完全满足我们的需求了

Netty 自带的拆包器

  1. 固定长度的拆包器 FixedLengthFrameDecoder

    如果你的应用层协议非常简单,每个数据包的长度都是固定的,比如 100,那么只需要把这个拆包器加到 pipeline 中,Netty 会把一个个长度为 100 的数据包 (ByteBuf) 传递到下一个 channelHandler。

  2. 行拆包器 LineBasedFrameDecoder

    从字面意思来看,发送端发送数据包的时候,每个数据包之间以换行符作为分隔,接收端通过 LineBasedFrameDecoder 将粘过的 ByteBuf 拆分成一个个完整的应用层数据包。

  3. 分隔符拆包器 DelimiterBasedFrameDecoder

    DelimiterBasedFrameDecoder 是行拆包器的通用版本,只不过我们可以自定义分隔符。

  4. 基于长度域拆包器 LengthFieldBasedFrameDecoder

    最后一种拆包器是最通用的一种拆包器,只要你的自定义协议中包含长度域字段,均可以使用这个拆包器来实现应用层拆包。

Netty 性能优化

共享 handler

调用 pipeline().addLast() 方法的时候,都直接使用单例,不需要每次有新连接的时候都 new 一个 handler,提高效率,也避免了创建很多小的对象。

// 1. 加上注解标识,表明该 handler 是可以多个 channel 共享的
@ChannelHandler.Sharable
public class LoginRequestHandler extends SimpleChannelInboundHandler<LoginRequestPacket> {

    // 2. 构造单例
    public static final LoginRequestHandler INSTANCE = new LoginRequestHandler();

    protected LoginRequestHandler() {
    }

}

压缩 handler - 合并编解码器

Netty 内部提供了一个类,叫做 MessageToMessageCodec,使用它可以让我们的编解码操作放到一个类里面去实现。我们也可以同时使用单例。

@ChannelHandler.Sharable
public class PacketCodecHandler extends MessageToMessageCodec<ByteBuf, Packet> {
    public static final PacketCodecHandler INSTANCE = new PacketCodecHandler();

    private PacketCodecHandler() {

    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf byteBuf, List<Object> out) {
        out.add(PacketCodec.INSTANCE.decode(byteBuf));
    }

    @Override
    protected void encode(ChannelHandlerContext ctx, Packet packet, List<Object> out) {
        ByteBuf byteBuf = ctx.channel().alloc().ioBuffer();
        PacketCodec.INSTANCE.encode(byteBuf, packet);
        out.add(byteBuf);
    }
}

缩短事件传播路径

压缩 handler - 合并平行 handler

在有多个平行 handler(即一个指令只会有一个对应 handler 执行)的时候,我们可以合并成一个 handler,然后调用指定对应的 handler 处理即可。

更改事件传播源

在某个 inBound 类型的 handler 处理完逻辑之后,调用 ctx.writeAndFlush() 可以直接一口气把对象送到 codec 中编码,然后写出去。

在某个 inBound 类型的 handler 处理完逻辑之后,调用 ctx.channel().writeAndFlush(),对象会从最后一个 outBound 类型的 handler 开始,
逐个往前进行传播,路径是要比 ctx.writeAndFlush() 要长的。

在能够使用 ctx.writeAndFlush() 的地方用其代替 ctx.channel().writeAndFlush()。

减少阻塞主线程的操作

通常我们的应用程序会涉及到数据库和网络,例如:

protected void channelRead(ChannelHandlerContext ctx, T packet) {
    // 1. balabala 一些逻辑
    // 2. 数据库或者网络等一些耗时的操作
    // 3. writeAndFlush()
    // 4. balabala 其他的逻辑
}

由于单个 NIO 线程上绑定了多个 channel,一个 channel 上绑定了多个 handler,所以只要有一个 handler 的该方法阻塞了 NIO 线程,
最终都会拖慢绑定在该 NIO 线程上的其他所有的 channel。

所以对于耗时的操作,我们需要把这些耗时的操作丢到我们的业务线程池中去处理,下面是解决方案的伪代码:

ThreadPool threadPool = xxx;

protected void channelRead(ChannelHandlerContext ctx, T packet) {
    threadPool.submit(new Runnable() {
        // 1. balabala 一些逻辑
        // 2. 数据库或者网络等一些耗时的操作
        // 3. writeAndFlush()
        // 4. balabala 其他的逻辑
    })
}

这样,就可以避免一些耗时的操作影响 Netty 的 NIO 线程,从而影响其他的 channel。

准确统计处理时长

protected void channelRead0(ChannelHandlerContext ctx, T packet) {
    threadPool.submit(new Runnable() {
        long begin = System.currentTimeMillis();
        // 1. balabala 一些逻辑
        // 2. 数据库或者网络等一些耗时的操作
        
        // 3. writeAndFlush
        xxx.writeAndFlush().addListener(future -> {
            if (future.isDone()) {
                // 4. balabala 其他的逻辑
                long time =  System.currentTimeMillis() - begin;
            }
        });
    })
}

writeAndFlush() 方法会返回一个 ChannelFuture 对象,我们给这个对象添加一个监听器,然后在回调方法里面,我们可以监听这个方法执行的结果,
进而再执行其他逻辑,最后统计耗时,这样统计出来的耗时才是最准确的。

HashSet和TreeSet

前言

上一篇重点看了下 HashMap 以及简单说了说 LinkedHashMap,今天看下 HashSet 和 TreeSet。

HashSet

HashSet 很简单,没什么内容。先看下两个属性和几个主要的方法源码:

两个属性

private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
//作为map的value,没什么用处
private static final Object PRESENT = new Object();

HashSet( )

/**
 * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
 * default initial capacity (16) and load factor (0.75).
 */
public HashSet() {
    map = new HashMap<>();
}

size( )

/**
 * Returns the number of elements in this set (its cardinality).
 *
 * @return the number of elements in this set (its cardinality)
 */
public int size() {
    return map.size();
}

isEmpty( )

/**
 * Returns <tt>true</tt> if this set contains no elements.
 *
 * @return <tt>true</tt> if this set contains no elements
 */
public boolean isEmpty() {
    return map.isEmpty();
}

contains(Object)

public boolean contains(Object o) {
    return map.containsKey(o);
}

add(E)

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

remove(Object)

public boolean remove(Object o) {
    return map.remove(o)==PRESENT;
}

OK,这几个方法看完了就没了,一眼就可以看出来 HashSet 的元素就像 HashMap 的 key 一样。换句话说,HashMap 是 实现HashSet 的支撑。这个没啥意思,再来看下相比之下更有意思的 TreeSet

TreeSet

其实大多数类或者接口,看名字就知道有什么主要特性了,Set, 它是一个元素不重复的集合,TreeSet,它就是一个可以基于二叉树对元素进行排序的不可重复集合。TreeSet 是基于 TreeMap 实现的。TreeSet 中的元素支持2种排序方式:自然排序 或者 根据创建 TreeSet 时提供的 Comparator 进行排序。这取决于使用的构造方法。

成员变量

/**
 * The backing map.
 */
 private transient NavigableMap<E,Object> m;

 // Dummy value to associate with an Object in the backing Map
 private static final Object PRESENT = new Object();

构造方法

TreeSet(NavigableMap<E,Object> m) {
    this.m = m;
}

public TreeSet() {
    this(new TreeMap<E,Object>());
}

//传入一个进行元素排序的比较器
public TreeSet(Comparator<? super E> comparator) {
    this(new TreeMap<>(comparator));
}

//传入一个集合,将其中的元素添加至TreeSet中
public TreeSet(Collection<? extends E> c) {
    this();
    addAll(c);
}

//创建TreeSet,并将s中的全部元素都添加到TreeSet中
public TreeSet(SortedSet<E> s) {
    this(s.comparator());
    addAll(s);
}

排序方式

  • 让元素自身具备比较性,需要元素实现Comparable接口,覆盖compareTo方法,

    这种方式也称为元素的自然排序,或者叫做默认排序。

  • 当元素自身不具备比较性时,或者具备的比较性不是所需要的时候,

    需要让集合自身具备比较性。在集合初始化时,就有了比较性

​ 需要定义一个实现了Comparator接口的比较器,覆盖compare方法,并将该类对象作为

​ 参数传递给TreeSet集合的构造函数

add(E)

public boolean add(E e) {
    return m.put(e, PRESENT)==null;
}
public V put(K key, V value) {
    Entry<K,V> t = root;
    if (t == null) {
        compare(key, key); // type (and possibly null) check

        root = new Entry<>(key, value, null);
        size = 1;
        modCount++;
        return null;
    }
    int cmp;
    Entry<K,V> parent;
    // split comparator and comparable paths
    Comparator<? super K> cpr = comparator;
    //基于传入的比较器进行排序
    if (cpr != null) {
        do {
            parent = t;
            cmp = cpr.compare(key, t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
            } while (t != null);
        }
    //基于元素自身的比较性进行排序
    else {
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
        Comparable<? super K> k = (Comparable<? super K>) key;
        do {
            parent = t;
            cmp = k.compareTo(t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    Entry<K,V> e = new Entry<>(key, value, parent);
    if (cmp < 0)
        parent.left = e;
    else
        parent.right = e;
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}

这篇内容较少,因为它都是基于两个相关的Map实现的,也比较简单

《深入理解Java虚拟机》之GC

垃圾收集器与内存分配策略

先思考一下GC需要完成的3件事情:

  1. 哪些内存需要回收?
  2. 什么时候回收?
  3. 如何回收?

前面介绍了 Java 内存运行时区域的各个部分,其中程序计数器、虚拟机栈、本地方法栈三个区域随线程而生,随线程而灭;栈中的每一个栈帧分配多少内存基本上在类结构确定下来时就已知的。而 Java 堆和方法区则不一定,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,GC 和后面所说的内存分配关注的就是这部分内存。

对象是否“存活”

引用计数算法

给对象添加一个引用计数起,每当有一个地方引用它时,计数器值就加1,当引用失效时,计数器值就减1;任何时刻计数器值为0的对象就是不可能再被使用的。但是主流的 Java 虚拟机里面没有选用引用计数器算法来管理内存,其中最主要的原因的就是它很难解决相互循环引用的问题。举个例子,代码如下:

public class ReferenceCountingGC {
  public Object instance = null;
  private static final int _1MB = 1024 * 1024;
  private byte[] bigSize = new byte[2 * _1MB];
  public static void testGC {
    ReferenceCountingGC objA = new ReferenceCountingGC();
    ReferenceCountingGC objB = new ReferenceCountingGC();
    objA.instance = objB;
    objB.instance = objA;
    
    objA = null;
    objB = null;
    
    //假设在这里发生GC,objA和objB是否能被回收?
    System.gc();
  }
}

运行结果显示,Java 虚拟机并没有因为两个对象互相引用就不回收它们,这也从侧面说明虚拟机并不是使用引用计数算法来判断是否存活的。

可达性分析算法

该算法基本**就是:通过一系列称为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜索,所走过的路径称为引用链,当一个对象到GCRoots 没有任何引用链相连时,则证明此对象时不可用的。

Java语言中,可作为 GC Roots 的对象包括以下几种:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  2. 方法区中类静态属性引用的对象。
  3. 方法区中常量引用的对象。
  4. 本地方法栈中JNI(即一般说的Native方法)引用的对象。

再谈引用

在 JDK 1.2之后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用和虚引用。

  1. 强引用就是指在代码中普遍存在的,类似“Object obj = new Object()”这类的引用。只要强引用存在,垃圾收集器永远不会回收掉被引用的对象。
  2. 软引用是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2 之后,提供了 SoftReference 类来实现软引用。
  3. 弱引用也是用来描述非必需对象的,但它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。JDK 1.2之后提供了 WeakReference 类来实现弱引用。
  4. 虚引用也称为幽灵引用或者幻影引用。它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被垃圾收集器回收时收到一个系统通知。JDK 1.2之后提供PhantomReference 类来现实虚引用。

生存还是死亡

即使在可达性分析算法中不可达的对象,也不是“非死不可”的,这时候它们暂时处于“缓行”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析算法后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况视为“没有必要执行”。

如果这个对象被判定为有必要执行finalize() 方法,那么这个对象将会放置在一个叫做 F-Queue 的队列之中,并在稍后由一个虚拟机自动建立的、低优先级的 Finalize 线程去执行它。这里所谓的执行是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在 finalize() 方法中执行缓慢,活着发生了死循环,将很可能会导致 F-Queue 队列的其它对象永远处于等待,甚至导致整个内存回收系统崩溃。finalize() 方法是对象逃脱死亡命运的最后一次机会,稍后GC将对 F-Queue 队列中的对象进行第二次小规模的标记,如果对象要在 finalize() 方法中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将会被移除“即将收回”的集合;如果对象这个时候还没有逃脱,那基本上它就真的被回收了。例子如下:

public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK = null;
    public void isAlive() {
        System.out.println("yes, i am still alive!");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed!");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }

    public static void main(String[] args) throws Throwable {
        SAVA_HOOK = new FinalizeEscapeGC();

        //对象第一次成功拯救自己
        SAVA_HOOK = null;
        System.gc();
        //因为finalize方法优先级很低,所以暂停0.5s以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead!");
        }

        //下面这段代码与上面的完全相同,但是这次自救却失败了。
        SAVA_HOOK = null;
        System.gc();
        //因为finalize方法优先级很低,所以暂停0.5s以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead!");
        }
    }
}

运行结果如下:

finalize method executed!
yes, i am still alive!
no, i am dead

从运行结果可以看出,SAVE_HOOK 对象的finalize() 方法确实被GC收集器触发过,并且在收集器成功逃脱了。但是第二次自救却失败了,这是因为任何一个对象的finalize() 方法都只会被系统自动调用一次,如果对象面临下一次回收,它的 finalize() 方法不会再次被调用。

回收方法区

Java 虚拟机规范确实说过不要求虚拟机在方法区实现垃圾回收,而且在方法区中进行垃圾收集的性价比一般比较低:在堆中,尤其在新生代,常规应用进行一次垃圾收集一般可以回收70%-95%的空间,而永久代的垃圾收集则远低于此。

永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。回收废弃常量和回收 Java 堆中的对象非常相似。以常量池中字面量的回收为例,例如一个字符串“abc”已经进入了常量池,但是当前系统中没有一个 String 对象引用常量池中的“abc”常量,也没有其它地方引用这个字面量,如果这时方式内存回收,而且必要的话,这个“abc”常量就会被系统清理出常量池。常量池中其它类(接口)、方法、字段的符号引用也与此类似。

判定一个常量是否是“废弃常量”比较简单,而要判断一个类是否是“无用的类”的条件则相对苛刻许多,类需要同时满足下面3个条件才能算是“无用的类”:

  1. 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
  2. 加载该类的 ClassLoader 已经被回收。
  3. 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

虚拟机可以对满足上面3个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样,不使用了就必然会回收。是否对类回收,HotSpot 虚拟机提供了 -Xnoclassgc参数进行控制,还可以使用其它参数查看类加载和卸载信息。

垃圾收集算法

标记-清除算法

最基础的收集算法是“标记-收集(Mark-Sweep)算法”,如同它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一进行回收所有被标记的对象,它的标记过程在上一节讲述对象标记判定时已经介绍过了。它的主要不足有两个:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除后会产生大量不连续的内存碎片,空间碎片过大可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够大的连续内存而不得不提前触发另一次垃圾收集动作。执行过程如下:

复制算法

为解决效率问题,“复制”算法出现了。将可用内存按容量分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,它将还存活的对象复制到另一块上面,然后再把已使用过的内存一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况。只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半,未免太高了一点。执行过程如下:

现在的商业虚拟机都是采用这种算法来回收新生代,IBM 公司的专门研究表明,新生代的对象98%都是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的Survivor 空间,每次使用 Eden 和其中一块 Survivor 。当回收内存时,Eden 和 Survivor 中还存活的对象一次性复制到另一块 Survivor 空间上,最后清理掉 Eden 和 刚才使用过的Survivor空间。HosSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1。当然我们没有办法保证每次回收都只有不多余10%的对象存活,当 Survivor 空间不够用时,需要依赖其它内存(这里指的是老年代)进行分配担保。

标记-整理算法

复制收集算法在对象存活率较高时就需要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代中一般不能直接选用这种算法。

根据老年代的特点,有人提出了一种“标记-整理”算法。标记过程仍然和“标记-清除”算法一样。但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端点边界以外的内存,“标记-整理”算法的过程如下:

分代收集算法

这种算法只是根据对象存活周期的不同将内存分为几块。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法来进行回收。

HotSpot 算法实现

枚举根节点

从可达性分析中从 GC Roots节点查找引用链为例,可作为 GC Roots 的节点主要在全局性的引用(例如常量或者类静态属性)与执行上下文(例如栈帧中的本地变量表)中,现在很多应用仅仅方法区就有数百兆,如果要逐个检查这里面的引用,那么必然会消耗很多时间。

另外,可达性分析对执行时间敏感还体现在 GC 停顿上,因为这项工作确保在一个能保持一致性的快照中进行--这里的“一致性”是指在分析期间整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中引用关系还在不断变化的情况。

目前主流的 Java 虚拟机使用的都是准确式 GC,所以当执行系统停下来后,并不需要一个不漏的检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得知哪些地方存放着对象引用。HotSpot 实现中,是使用一组称为 OopMap 的数据结构来达到这个目的的,在类加载完的时候,HotSpot 就把对象内什么偏移量上是什么类型的数据计算出来,在 JIT 编译过程中,也会在特定位置记录下栈和寄存器中哪些位置是引用,这样 GC 就可以直接得知这些信息了。

安全点

在 OopMap 的协助下,HotSpot 可以快速准确地完成 GC Roots 枚举,但另一个问题就来了:可能导致引用关系变化。或者说 OopMap 内容变化的指令非常多,如果为每一条指令都生成对应的 OopMap,那么就需要大量的额外空间,这样 GC 的空间成本将会变的很高。

实际上,HotSpot 只是在“安全点(Safepoint)”记录了这些信息。Safepoint 的选定既不能太少以致于 GC 等待时间太长,也不能过于频繁以致于过分增大运行时的负荷。一般具有方法调用、循环跳转、异常跳转功能的指令才会产生 Safepoint。

对于 Safepoint,另一个需要考虑的问题是如何在 GC 发生时让所有线程(这里不包括执行 JNI 调用的线程)都“跑”到最近的安全点上在停顿下来。这里有两种方法:抢先式中断和主动式中断。抢先式中断不需要线程的执行代码主动配合,在 GC 发生时,首先把所有的线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程让它“跑”到安全点上。现在几乎没有虚拟机采用这种方式。

而主动式中断的**是当 GC 需要中断线程的时候,不直接对线程操作,仅仅简单的设置一个标志,各个线程执行时主动去轮询这个标志,发现标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。

安全区域

Safepoint 机制保证了程序执行时,在不太长的时间内就会遇到可进入 GC 的 Safepoint。但是程序不执行的时候(就是没有分配 CPU 时间,典型的例子就是线程处于 Sleep 状态或者 Blocked 状态)呢?这时候线程无法响应 JVM 的中断请求,“走”到安全的地方去中断挂起,JVM 也显然不太可能等待线程重新被分配 CPU 时间。这种情况就需要安全区域(Safe Region)来解决。

安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域的任何地方 GC 都是安全的。当线程执行到 Safe Region 中的代码时,首先标志自己已经进入了 Safe Region,那样,当在这段时间里 JVM 要发起 GC时,就不用管标志自己为 Safe Region 状态的线程了。当线程要离开 Safe Region 时,它要检查系统是否已经完成了根节点枚举(或者是整个 GC 过程),如果完成了,那线程就继续执行。否则就必须等待直到收到可以安全离开 Safe Region 的信号为止。

垃圾收集器

这里讨论的收集器基于 HotSpot 虚拟机,这个虚拟机包含的所有收集器如下图:

Serial 收集器

Serial 收集器是一个单线程的收集器,但这个“单线程”的意义并不仅仅说明它只会使用一个 CPU 或者一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其它所有的工作线程,知道它收集结束。

但是它也有着优于其它收集器的地方:简单而高效(与其它收集器的单线程比),对于限定的单个 CPU 环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。在用户的桌面应用场景中,分配给虚拟机管理的内存一般不会很大,收集几十兆甚至一两百兆的新生代(仅仅是新生代使用的内存,桌面应用基本上不会再大了),停顿时间完全可以控制在几十毫秒最多一百毫秒以内,只要不是频繁发生,这点停顿是可以接受的。所以,Serial 收集器对于运行在 Client 模式下的虚拟机来说是一个很好的选择。

ParNew 收集器

ParNew 收集器基本上就是 Serial 收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括 Serial 收集器可用的所有控制参数、收集算法、对象分配规则、回收策略等都与 Serial 收集器完全一样。

ParNew 收集器是许多运行在 Server 模式下的虚拟机首选的新生代收集器,其中有一个与性能无关但是很重要的原因是,除了 Serial 收集器外,目前只有它能与 CMS(后面介绍) 收集器配合工作。

ParNew 收集器在单 CPU 环境中绝对不会有比 Serial 收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个 CPU 的环境中都不能百分之百的保证可以超越 Serial 收集器。当然,随着可以使用的 CPU 的数量增加,它对于 GC 时系统资源的有效利用还是很有好处的。它默认开启的收集线程数与 CPU 的数量相同,在 CPU 非常多(譬如32个)的环境中,可以使用 -XX:ParallelGCThreads 参数来限制垃圾收集的线程数。

Parallel Scavenge 收集器

Parallel Scavenge 收集器的特点是它的关注点与其它收集器不同,CMS 等收集器的关注点是尽可能的缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge 收集器的目标是达到一个可控制的吞吐量(就是 CPU 用于运行用户代码的时间和 CPU 总消耗时间的比值)。

Parallel Scavenge 收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的 -XX:MaxGCPauseMillis 参数以及直接设置吞吐量大小的 -XX:GCTimeRatio 参数。

由于与吞吐量关系密切,Parallel Scavenge 收集器也经常被称为“吞吐量优先”收集器。除了上述两个参数外还有一个参数 -XX:+UseAdaptiveSizePolicy 值得关注。这是一个开关参数,当这个参数打开之后,就不需要手动指定新生代的大小(-Xmn)、Eden 与 Survivor 区的比例大小(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最适合的停顿时间或者最大的吞吐量,这种调节方式称为 GC 自适应的调节策略。这也是 Parallel Scavenge 收集器和 ParNew 收集器的一个重要区别。

Serial Old 收集器

Serial Old 收集器是 Serial 收集器的老年代版本,同样是一个单线程的收集器,使用“标记-整理”算法。这个收集器的主要意义也是在于给 Client 模式下的虚拟机使用。如果在 Server 模式下,那么它主要有两大用途:一种是在 JDK 1.5 及以前版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。

Parallel Old 收集器

Parallel Scavenge 的老年代版本,使用“标记-整理”算法。在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。

CMS 收集器

CMS(Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。基于“标记-清除”算法。运行过程包括下面四个步骤:

  1. 初始标记
  2. 并发标记
  3. 重新标记
  4. 并发清除

其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一个 GC Roots 能直接关联到的对象,速度很快,并发标记阶段就是在进行 GC Roots Tracing 的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。

虽然 CMS 具有并发收集、低停顿的优点,但是还远远达不到完美的程度,它有以下3个明显的缺点:

  1. CMS 收集器对 CPU 资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说 CPU 资源)而导致应用程序变慢,总吞吐量会降低。CMS 默认启动的回收线程数是(CPU 数量+3)/4,也就是当 CPU 在4个以上时,并发回收时垃圾收集线程不少于25%的 CPU 资源,并且随着 CPU 数量的增加而下降。但是当 CPU 不足4个(譬如2个)时,CMS 对用户程序的影响就可能变得很大。为了应付这种情况,虚拟机提供了一种称为“增量式并发收集器”的 CMS 收集器变种,所做的事情和单 CPU 年代 PC 机操作系统使用抢占式来模拟多任务机制的**一样,就是在并发标记、清理的时候让 GC 线程、用户线程交替运行,尽量减少 GC 线程独占资源的时间,这样整个垃圾收集的过程会更长,但对用户程序的影响就会显得少一些。实践证明,效果一般,目前版本已被声明为“deprecated”。
  2. CMS 收集器无法处理浮动垃圾,可能出现 “Concurrent Mode Failure” 失败而导致另一次 Full GC 的产生。CMS 并发清理阶段用户线程还在运行,伴随着程序自然就还会有新的垃圾产生,这一部分垃圾出现在标记过程之后,CMS 无法在当次收集中处理它们,只好留待下一次收集处理。这一部分垃圾就称为“浮动垃圾”。由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留足够的内存空间给用户线程使用,因此 CMS 收集器不能像其它收集器那样等到老年代机会完全被填满了再进行收集,需要预留一部分空间提供提供并发收集时的程序运作使用。要是 CMS 运行期间预留的内存无法满足程序需要,就会出现一次 “Concurrent Mode Failure” 失败,这时启动后备预案:临时启用 Serial Old 收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。
  3. CMS 基于“标记-清除”算法实现的,就意味着收集结束后会有大量空间碎片产生。为了解决这个问题,CMS 收集器提供了一个 -XX:+UseCMSCompactAtFullCollection 开关参数(默认开启),用于 CMS 收集器顶不住要进行 Full GC 时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。虚拟机还提供了另外一个参数:-XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的 Full GC 之后,跟着来一次压缩的(默认为0,表示每次进入 Full GC 都进行碎片整理)。

G1 收集器

与其它 GC 收集器相比, G1具备以下特点:

  1. 并行和并发。
  2. 分代收集。
  3. 空间整合:与 CMS 的“标记-清理”算法不同,G1从整体上看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着 G1 运行期间不会产生内存碎片。
  4. 可预测的停顿:这是 G1 相比 CMS 的另一大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿之外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不超过 N 毫秒,这几乎已经是实时 Java (RTSJ)的垃圾收集器的特征了。

在 G1 之前的收集器进行收集的范围都是整个新生代或者老年代,而使用 G1 收集器时,它将整个 Java 堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的,它们都是一部分 Region (不需要连续)的集合。

G1 之所以能建立可预测的停顿时间模型,是因为它可以有计划的避免在整个 Java 堆中进行全区域的垃圾收集。G1 跟踪各个 Region 里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在优先的时间内可以获得尽可能高的收集效率。

在 G1 收集器中,Region 之间的对象引用以及其他收集器中的新生代和老年代之间的对象引用,虚拟机都是使用 Remembered Set 来避免全堆扫描的。G1 中每个 Region 都有一个对应的 Remembered Set,虚拟机发现在对Reference 类型的数据进行写操作时,会产生一个 Write Barrier 暂时中断写操作,检查 Reference 引用的对象是否处于不同的 Region 之中,如果是,便通过 CardTable 把相关引用信息记录到被引用的对象所属的 Remembered Set 之中。当进行内存回收时,在 GC 根节点的枚举范围中加入 Remembered Set 即可保证不会对全堆扫描也不会有遗漏。

如果不计算维护 Remembered Set 的操作, G1 的运行大致分为以下几个步骤:

  1. 初始标记。
  2. 并发标记。
  3. 最终标记。
  4. 筛选回收。

初始标记阶段仅仅是标记一下 GC Roots 能直接关联到的对象,并且修改 TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的 Region 中创建新对象,这阶段需要停顿线程,但耗时很短。并发标记阶段是 GC Roots 开始对堆中的对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。而最终标记阶段则是为了矫正在并发标记阶段因用户程序继续运行而导致标记产生变动的那一部分标记记录,虚拟机将这段时间变化记录在线程 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这阶段需要停顿线程,但是可并行执行。最后在筛选回收阶段首先对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划,这个阶段其实也可以与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。

内存分配和回收策略

对象的内存分配,往大方向讲,就是在堆上分配(但是也可能经过 JIT 编译后被拆散为标量类型并间接的在栈上分配),对象主要分配在新生代的 Eden 区上,如果启动了本地线程分配缓冲,将按线程优先在 TLAB 上分配。少数情况也可能会直接分配到老年代中,分配的规则并不是百分之百固定的。其细节取决于使用哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数。下面所述都是在使用 Serial/Serial Old收集器下的内存分配和回收的策略。

对象优先在 Eden 区分配

  • Minor GC :指发生在新生代的垃圾收集动作,因为 Java 对象大多都具有朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也非常快。
  • Full GC/Major GC:指发生在老年代的 GC,出现了 Major GC,经常会伴随着至少一次的 Minor GC(但非绝对的,在 Parallel Scavenge 收集器的收集策略里就有直接进行 Major GC 的策略选择过程),Major GC 的速度一般会比 Minor GC 慢10倍以上。

大多数情况下,对象在新生代 Eden 区中分配,当 Eden 区没有足够的内存空间进行分配时,虚拟机发起一次 Minor GC。

大对象直接进入老年代

所谓的大对象是指,需要大量连续内存空间的 Java 对象,最典型的大对象就是那种很长的字符串以及数组。大对象对虚拟机的内存分配来说就是一个坏消息。经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。

虚拟机提供了一个 -XX:pretenureSizeThreshold 参数,令大于这个设置值的对象直接在老年代中分配。这样做的目的是避免在 Eden 区和两个 Survivor 区之间发生大量的内存复制(新生代是采用复制算法收集内存)。

长期存活的对象将进入老年代

虚拟机为每个对象定义了一个对象年龄计数器。如果对象在 Eden 区出生并经过第一次 Minor GC 后仍然存活,并且能够被 Survivor 区容纳的话,将被移动到 Survivor 区,并且对象年龄设为1。像这样每过一个 Minor GC,对象年龄就加一,当对象年龄增加到一定程度(默认15岁),就会被晋升到老年代。对象晋升到老年代的年龄阀值,可以使用参数 -XX:MaxTenuringThreshold 设置。

动态对象年龄判断

为了能更好的适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了 MaxTenuringThreshold 才能晋升到老年代,如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或者等于该年龄的对象就可以直接晋升到老年代。

空间分配担保

在发生 Minor GC 前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么 Minor GC 可以确保是安全的。如果不成立,则虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败。如果允许,那么就会继续检查老年代的最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC,如果小于,或者 HandlePromotionFailure 设置为不允许担保失败,那这时也要改为进行一次 Full GC。

Zookeeper Session 机制

Session 指的是 ZooKeeper 服务器与客户端会话。在 ZooKeeper 中,一个客户端连接是指客户端和服务器之间的一个 TCP 长连接。客户端启动的时候,首先会与服务器建立一个 TCP 连接,从第一次连接建立开始,客户端会话的生命周期也开始了。通过这个连接,客户端能够通过心跳检测与服务器保持有效的会话,也能够向Zookeeper服务器发送请求并接受响应,同时还能够通过该连接接收来自服务器的Watch事件通知。

会话创建

Session

Session 是 Zookeeper 中的会话实体,代表一个客户端连接。其包含以下4个基本属性。

  • SessionId:会话ID,唯一标识一个会话,每次客户端创建新会话的时候,Zookeeper 都会为其分配一个全局唯一的 SessionId。
  • TimeOut:会话超时时间。客户端在构造 Zookeeper 实例的时候,会配置一个 sessionTimeOut 参数用于指定会话的超时时间。服务器会根据自己的超时时间限制最终确定会话的超时时间。
  • TickTime:下次会话超时时间点。为了便于对会话实行 “分桶策略” 管理,同时也是为了高效低耗地实现会话的超时检查与清理,Zookeeper 会为每个会话标记一个下次会话超时时间点。TickTime是一个13位的 long 型数据,其值接近于当前时间加上 TimeOut,但不完全相等。
  • isClosing:用于标记一个会话是否已经被关闭。通常当服务端检测到一个会话已经超时失效的时候,会将该属性标记为 “已关闭”,这样就能确保不再处理来自该会话的请求。

sessionId

在 SessionTracker 初始化的时候,会调用 initializeNextSession 方法来生成一个初始化的 sessionId,之后在 Zookeeper 的正常运行过程中,会在该 sessionId 的基础上为每个会话进行分配,其初始化算法如下:

public static long initializeNextSession(long id) {
    long nextSid = 0;
    nextSid = (System.currentTimeMillis() << 24) >>> 8;
    nextSid =  nextSid | (id <<56);
    return nextSid;
}

算法结果可以概括为:高8位确定了所在机器,后56位使用当前的毫秒表示进行随机。

SessionTracker

SessionTracker 是 Zookeeper 服务端的会话管理器,负责会话的创建、管理和清理等工作。每一个会话在 SessionTracker 内部都保留了三份:

  • sessionsById:这是一个 HashMap<Long, SessionImpl> 类型的数据结构,用来根据 sessionId 管理 Session 实体。
  • sessionsWithTimeout:这是一个 ConcurrentHashMap<Long, Integer> 类型的数据结构,用于根据 sessionId 来管理会话的超时时间。该数据结构和 Zookeeper 内存数据库相连通,会被定期持久化到快照文件中去。
  • sessionSets:这是一个 HashMap<Long, SessionSet> 类型的数据结构,用于根据下次会话超时时间点来归档会话,便于进行会话管理和超时检查。

创建连接

服务端对于客户端的 “会话创建” 请求的处理,大体可以分为四个步骤,分别是处理 ConnectRequest 请求、会话创建、处理器链路处理和会话响应

  1. 首先将会由 NIOServerCnxn 来负责接收客户端的 “会话创建” 请求,并反序列化出 ConnectRequest 请求,然后根据 Zookeeper 服务端的配置完成会话超时时间的协商。

  2. 随后 SessionTracker 会为该会话分配一个 sessionId,并将其注册到 sessionsById 和 sessionsWithTimeout 中去,同时进行会话的激活。

  3. 之后,该 “会话请求” 还会在服务端的各个请求处理器之间进行顺序流转,最终完成会话的创建。

会话管理

分桶策略

所谓分桶策略,是指将类似的会话放在同一区块中进行管理,以便于 Zookeeper 对会话进行不同区块的隔离处理以及同一区块的统一处理。分配的原则是每个会话的 “下次超时时间点(nextExpirationTime)”。nextExpirationTime 是指该会话最近一次可能超时的时间点,对于一个新创建的会话而言,其会话创建完毕后,Zookeeper 就会为其计算 nextExpirationTime:

nextExpirationTime = CurrentTime + SessionTimeout

但是在 Zookeeper 的实际实现中,还做了一个处理。Zookeeper 的 Leader 服务器在运行期会定时地进行会话超时检查,其时间间隔是 expirationInterval,单位是毫秒,默认值是 tickTime 的值,即默认情况下,每隔 2000 毫秒进行一次会话超时检查。为了方便对多个会话同时进行超时检查,完成的 nextExpirationTime 计算方式如下:

nextExpirationTime = CurrentTime + SessionTimeout
nextExpirationTime = (nextExpirationTime / expirationInterval + 1) * expirationInterval;

会话激活

为了保持客户端会话的有效性,在 Zookeeper 的运行过程中,客户端会在会话超时时间过期范围内向服务端发送 PING 请求来保持会话的有效性,俗称 “心跳检测”。同时,服务端需要不断的接收来自客户端的这个心跳检测,并且需要重新激活对应的客户端会话,我们将这个重新激活的过程称为 TouchSession。

会话激活的过程,不仅能够使服务端检测到对应客户端会话的存活性,同时也能够让客户端自己保持连接状态。其主要流程如下:

  1. 检查该会话是否已经被关闭。

    Leader 会检查该会话是否已经被关闭,如果已经被关闭,那么不再继续激活该会话。

  2. 计算该会话新的超时时间 ExpirationTime_New。

    如果该会话尚未关闭,那么就开始激活会话。首先需要计算出该会话下次超时时间点,使用的就是上面提到的公式。

  3. 定位该会话当前的区块。

    获取该会话老的超时时间 ExpirationTime_Old,并根据该超时时间来定位其所在的区块。

  4. 迁移会话。

    将该会话从老的区块中取出,放入 ExpirationTime_New 对应的新区块中。

实际上,在 Zookeeper 服务端的设计中,只要客户端有请求发送到服务端,包括读请求和写请求,那么就会触发一次会话激活。因此总的来讲,大体会出现以下两种情况的会话激活:

  • 只要客户端向服务端发送请求,包括读请求和写请求,那么就会触发一次会话激活。
  • 如果客户端发现在 sessionTimeout/3 时间内尚未和服务器进行过任何通信,即没有向服务端发送任何请求,那么就会主动发起一个 PING 请求,服务端收到该请求后,就会触发上述第一种情况的会话激活。

会话超时检查

SessionTracker 中有一个单独的线程专门进行会话超时检查,我们称其为 “超时检查线程”,其工作机制的核心思路非常简单:逐个依次地对会话桶中剩下的会话进行清理

如果一个会话被激活,那么 Zookeeper 就会将其从上一个会话桶移到下一个会话桶中。于是上一个会话桶中留下的所有会话都是尚未被激活的。因此,超时检查线程的任务就是定时检查出这个会话桶中所有剩下的未被迁移的会话。

那么超时检查线程是如何做到定时检查的呢?在会话分桶策略中,我们将 expirationInterval 的倍数作为时间点来分布会话,因此,超时检查线程只要在这些指定的时间点上进行检查即可,这样既提高了会话检查的效率,而且由于是批量清理,因此性能非常好。这也是为什么 Zookeeper 要通过分桶策略来管理客户端会话的最主要的原因。因为在实际生产环境中,一个 Zookeeper 集群的客户端会话数可能会非常多,逐个依次检查会话的方式会非常耗费时间。

会话清理

当 SessionTracker 的会话超时检查线程整理出一些已经过期的会话后,那么就要开始进行会话清理了。步骤大致如下:

  1. 标记会话状态为 “已关闭”。

    为了保证会话清理过程中不再处理来自该客户端的新请求,SessionTracker 会首先将会话的 isClosing 属性标记为 true。这样,即使在会话清理期间接收到客户端的新请求,也无法继续处理了。

  2. 发起 “会话关闭” 请求。

    为了使对该会话的关闭操作在整个服务端集群中都生效,Zookeeper 使用了提交 “会话关闭” 请求的方式,并立即交付给 PrepRequestProcessor 处理器进行处理。

  3. 收集需要清理的临时节点。

    在 Zookeeper 内存数据库中,为每个会话都单独保存了一份由该会话维护的所有临时节点集合,因此在会话清理阶段,只需要根据当前即将关闭的会话的 sessionId 从内存数据库中取到这份临时节点列表即可。

    但是,在实际应用场景中,有如下的细节需要处理:在 Zookeeper 处理会话关闭请求之前,正好有以下两类请求到达了服务端并正在处理中。

    • 节点删除请求,删除的目标节点正好是上述临时节点中的一个。
    • 临时节点创建请求,创建的目标正好是上述临时节点中的一个。

    对于这两类请求,其共同点都是事务处理尚未完成,因此还没有应用到内存数据库中,所以上述获取到的临时节点列表在遇到这两类事物请求的时候,会出现不一致的情况。

    假如我们当前获取到的临时节点列表是 ephemerals,那么针对第一类请求,我们需要将所有这些请求对于的数据节点路径从 ephemerals 中移除,以避免重复删除。针对第二类请求,我们需要将所有这些请求对应的数据节点路径添加到 ephemerals 中去,以删除这些即将会被创建但是尚未保存到内存数据库中去的临时节点。

  4. 添加 “节点删除” 事务变更。

    完成该会话相关的临时节点收集后,Zookeeper 会逐个将这些临时节点转换成 “节点删除” 请求,并放入事务变更队列 outstandingChanges 中去。

  5. 删除临时节点。

    创建了对应的 “节点删除” 请求,FinalRequestProcessor 处理器会触发内存数据库,删除该会话对应的所有临时节点。

  6. 移除会话。

    完成节点删除后,需要将会话从 SessionTracker 中移除,主要就是上面提到的三个数据结构(sessionsById、sessionWithTimeout 和 sessionSets)中将该会话移除掉。

  7. 关闭 NIOServerCnxn。

    最后,从 NIOServerCnxnFactory 找到该会话对应的 NIOServerCnxn,将其关闭。

重连

当客户端和服务端之间的网络连接断开时,Zookeeper 客户端会自动进行反复的重连,直到最终成功连接上 Zookeeper 集群中的一台机器。在这种情况下,再次连接上服务端的客户端可能会处于以下两种状态之一。

  • CONNECTED:如果在会话超时时间内重新连接上 Zookeeper 集群中任意一台机器,那么被视为重连成功。
  • EXPIRED:如果是在会话超时时间以外重新连接上,那么服务端其实已经对该会话进行了会话清理操作,因此在此连接上的会话将被视为非法会话。

由于存在心跳检测来反复地进行会话激活,因此,在正常情况下,客户端会话一直是有效的。然而,当客户端与服务端之间的连接断开后,用户在客户端可能主要会看到两类异常:CONNECTION_LOSS(连接断开)和 SESSION_EXPIRED(会话过期)。那么该如何正确处理呢?

连接断开

在客户端与服务器断开连接后,Zookeeper 客户端会自动从地址列表重新逐个选取新的地址并尝试进行重新连接,直到最终成功连接上服务器。

举个例子,假设某个应用在使用 Zookeeper 客户端进行 setData 操作的时候,正好出现了 CONNECTION_LOSS 现象,那么客户端会立即接收到 None-Disconnected 通知,同时会抛出异常:ConnectionLossException。在这种情况下,我们的应用要做的事情就是捕获住 ConnectionLossException ,然后等待 Zookeeper 客户端自动完成重连。一旦客户端成功连接上一台 Zookeeper 机器后,那么客户端就会收到 None-SyncConnected 通知,之后就可以重试刚刚出错的 setData 操作。

会话失效

客户端和服务器连接断开之后,由于重连期间耗时过长,超过了会话超时时间(sessionTimeout)限制后还没有成功连接上服务器,那么服务器就认为这个会话已经结束了,就会开始进行会话清理。但是另一方面,客户端本身不知道会话已经失效,并且其客户端状态仍然是 DISCONNECTED。之后,如果客户端重新连接上了服务器,那么很不幸,服务器会告知客户端该会话已经失效(SESSION_EXPIRED)。在这种情况下,用户就需要重新实例话一个 Zookeeper 对象,并且看应用的负责情况,重新恢复临时数据。

会话转移

假如客户端和服务器 S1 之间的连接断开后,如果通过尝试重连后,成功连接上新的服务器 S2 并且延续了有效会话,那么就可以说会话从服务器 S1 转移到了 S2 上。

Zookeeper Watcher 机制

Watcher(事件监听器),是Zookeeper中的一个很重要的特性。Zookeeper允许用户在指定节点上注册一些Watcher,并且在一些特定事件触发的时候,ZooKeeper服务端会将事件通知到感兴趣的客户端上去,该机制是Zookeeper实现分布式协调服务的重要特性。

客户端注册 Watcher

以 getData() 为例:

封装 Watcher

public byte[] getData(final String path, Watcher watcher, Stat stat)
    throws KeeperException, InterruptedException
 {
    final String clientPath = path;
    PathUtils.validatePath(clientPath);
 
    // the watch contains the un-chroot path
    WatchRegistration wcb = null;
    if (watcher != null) {
        // 封装 Watcher 成 WatchRegistration,此处是 DataWatchRegistration
        wcb = new DataWatchRegistration(watcher, clientPath);
    }
 
    final String serverPath = prependChroot(clientPath);
 
    RequestHeader h = new RequestHeader();
    h.setType(ZooDefs.OpCode.getData);
    GetDataRequest request = new GetDataRequest();
    request.setPath(serverPath);
    // 仅仅标记是否需要注册 watcher
    request.setWatch(watcher != null);
    GetDataResponse response = new GetDataResponse();
    // 请求
    ReplyHeader r = cnxn.submitRequest(h, request, response, wcb);
    if (r.getErr() != 0) {
        throw KeeperException.create(KeeperException.Code.get(r.getErr()),
                clientPath);
    }
    if (stat != null) {
        // 复制节点状态到传进入的 stat 中
        DataTree.copyStat(response.getStat(), stat);
    }
    return response.getData();
}
public ReplyHeader submitRequest(RequestHeader h, Record request,
        Record response, WatchRegistration watchRegistration)
        throws InterruptedException {
    ReplyHeader r = new ReplyHeader();
    // 封装 WatchRegistration 成 Packet
    Packet packet = queuePacket(h, r, request, response, null, null, null,
                null, watchRegistration);
    synchronized (packet) {
        while (!packet.finished) {
            packet.wait();
        }
    }
    return r;
}

queuePacket() 方法会将 Packet 添加到发送队列中,随后 Zookeeper 客户端就会发送这个请求并等待返回。

由客户端 SendThread 线程的 readResponse 方法接收响应

private void finishPacket(Packet p) {
    if (p.watchRegistration != null) {
        // 取出 Watcher 并注册到 ZKWatchManager 的 dataWatches
        p.watchRegistration.register(p.replyHeader.getErr());
    }
 
    if (p.cb == null) {
        synchronized (p) {
            p.finished = true;
            p.notifyAll();
        }
    } else {
        p.finished = true;
        eventThread.queuePacket(p);
    }
}
 
public void register(int rc) {
    if (shouldAddWatch(rc)) {
        // return watchManager.dataWatches;
        Map<String, Set<Watcher>> watches = getWatches(rc);
        synchronized(watches) {
            Set<Watcher> watchers = watches.get(clientPath);
            if (watchers == null) {
                watchers = new HashSet<Watcher>();
                watches.put(clientPath, watchers);
            }
            watchers.add(watcher);
        }
    }
}

ZKWatchManager 的 dataWatches 是一个 Map<String, Set> 类型的数据结构,用于将数据节点和 Watcher 对象一一映射后管理起来。

传输对象

有一个问题:如果每次请求都带着 Watcher 对象传输,那么服务端肯定会出现内存紧张或者其他性能问题。Zookeeper 怎么做的呢?

上面提到把 WatchRegistration 封装到 Packet 对象中去,但是底层实际的网络传输序列化过程中,并没有将 WatchRegistration 对象完全的序列化到
底层字节数组中。为了证实这点,可以看下 Packet 内部的序列化过程:

public void createBB() {
    try {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        BinaryOutputArchive boa = BinaryOutputArchive.getArchive(baos);
        boa.writeInt(-1, "len"); // We'll fill this in later
        if (requestHeader != null) {
            requestHeader.serialize(boa, "header");
        }
        if (request instanceof ConnectRequest) {
            request.serialize(boa, "connect");
            // append "am-I-allowed-to-be-readonly" flag
            boa.writeBool(readOnly, "readOnly");
        } else if (request != null) {
            request.serialize(boa, "request");
        }
        baos.close();
        this.bb = ByteBuffer.wrap(baos.toByteArray());
        this.bb.putInt(this.bb.capacity() - 4);
        this.bb.rewind();
    } catch (IOException e) {
        LOG.warn("Ignoring unexpected exception", e);
    }
}

可以看到只会将 requestHeader 和 readOnly/request 两个属性进行序列化,而 WatchRegistration 并没有序列化到底层字节数组中。

总结

  1. 标记 request,封装 Watcher 成 WatchRegistration 对象。
  2. 封装 Packet 对象,Packet 可以看成最小的通信协议单元,任何需要传输的对象都需要封装成 Packet。
  3. 发送 request,但是并没有传输 Watcher。
  4. 接收响应,从 Packet 中取出 Watcher 并注册到 ZKWatchManager 的 Map<String, Set> 中。

服务端处理 Watcher

上面说到客户端并没有将 Watcher 传递到服务端,那么服务端怎么进行处理的呢?

ServerCnxn 存储

服务端接收到客户端的请求后,会在 FinalRequestProcessor 的 processRequest 方法中进行是否需要注册 Watcher 的判断,代码片段如下:

case OpCode.getData: {
    lastOp = "GETD";
    GetDataRequest getDataRequest = new GetDataRequest();
    ByteBufferInputStream.byteBuffer2Record(request.request,
            getDataRequest);
    DataNode n = zks.getZKDatabase().getNode(getDataRequest.getPath());
    if (n == null) {
        throw new KeeperException.NoNodeException();
    }
    PrepRequestProcessor.checkACL(zks, zks.getZKDatabase().aclForNode(n),
            ZooDefs.Perms.READ,
            request.authInfo);
    Stat stat = new Stat();
    // 获取数据,根据 getDataRequest.getWatch() 来判断是否需要注册 Watcher
    // 需要的话传入 ServerCnxn 对象
    byte b[] = zks.getZKDatabase().getData(getDataRequest.getPath(), stat,
            getDataRequest.getWatch() ? cnxn : null);
    rsp = new GetDataResponse(b, stat);
    break;
}

ServerCnxn 是客户端和服务端之间的连接接口,代表客户端和服务端之间的连接。

ServerCnxn 实现了 Watcher 接口,因此可以看成是一个 Watcher 对象。

上面 ZKDatabase.getData() 会调用 DataTree.getData() 方法,相关代码如下:

/**
 * 对应数据变更
 */
private final WatchManager dataWatches = new WatchManager();
/**
 * 对应子节点变更
 */ 
private final WatchManager childWatches = new WatchManager();
 
public byte[] getData(String path, Stat stat, Watcher watcher)
        throws KeeperException.NoNodeException {
    // 根据路径获取节点
    DataNode n = nodes.get(path);
    if (n == null) {
        throw new KeeperException.NoNodeException();
    }
    synchronized (n) {
        n.copyStat(stat);
        if (watcher != null) {
            // 保存 path 和 watcher 到 WatchManager
            dataWatches.addWatch(path, watcher);
        }
        return n.data;
    }
}

WatchManager 是服务端 Watcher 的管理者,内部用 watchTable 和 watch2Paths
从两个维度来管理 Watcher,其相关代码如下:

private final HashMap<String, HashSet<Watcher>> watchTable =
    new HashMap<String, HashSet<Watcher>>();
 
private final HashMap<Watcher, HashSet<String>> watch2Paths =
    new HashMap<Watcher, HashSet<String>>();
 
public synchronized void addWatch(String path, Watcher watcher) {
    HashSet<Watcher> list = watchTable.get(path);
    if (list == null) {
        // don't waste memory if there are few watches on a node
        // rehash when the 4th entry is added, doubling size thereafter
        // seems like a good compromise
        list = new HashSet<Watcher>(4);
        watchTable.put(path, list);
    }
    list.add(watcher);
 
    HashSet<String> paths = watch2Paths.get(watcher);
    if (paths == null) {
        // cnxns typically have many watches, so use default cap here
        paths = new HashSet<String>();
        watch2Paths.put(watcher, paths);
    }
    paths.add(path);
}

同时 WatchManager 还负责 Watcher 事件的触发,并移除已经被触发的 Watcher,可见 Zookeeper 的事件监听是一次性的。

这里的 WatchManager 是一个统称,在服务端,DataTree 中会托管两个 WatchManager:dataWatches 和 childWatches,分别对应数据变更 Watcher 和子节点变更 Watcher。这里因为是 getData() 方法,所以会保存到 dataWatches 中。

Watcher 触发

NodeDataChanged 事件的触发条件是 “Watcher 监听的对应数据节点的数据内容发生变更”,也就是 DataTree#setData() 方法,代码如下:

public Stat setData(String path, byte data[], int version, long zxid,
                    long time) throws KeeperException.NoNodeException {
    // ...
    // 触发相关事件
    dataWatches.triggerWatch(path, EventType.NodeDataChanged);
    return s;
}

在对指定的数据节点更新后,通过调用 WatchManager 的 triggerWatch 方法来触发相关的事件:

public Set<Watcher> triggerWatch(String path, EventType type) {
    return triggerWatch(path, type, null);
}

public Set<Watcher> triggerWatch(String path, EventType type, Set<Watcher> supress) {
    WatchedEvent e = new WatchedEvent(type,
                                      KeeperState.SyncConnected, path);
    HashSet<Watcher> watchers;
    synchronized (this) {
        // 从 watchTable 中移除
        watchers = watchTable.remove(path);
        // 如果不存在 watcher,则直接返回
        if (watchers == null || watchers.isEmpty()) {
            if (LOG.isTraceEnabled()) {
                ZooTrace.logTraceMessage(LOG,
                                         ZooTrace.EVENT_DELIVERY_TRACE_MASK,
                                         "No watchers for " + path);
            }
            return null;
        }
        // 从 watch2Paths 中移除
        for (Watcher w : watchers) {
            HashSet<String> paths = watch2Paths.get(w);
            if (paths != null) {
                paths.remove(path);
            }
        }
    }
    for (Watcher w : watchers) {
        if (supress != null && supress.contains(w)) {
            continue;
        }
        // 触发 Watcher
        w.process(e);
    }
    return watchers;
}

这里的 w 是之前存储的 ServerCnxn,其 process 方法如下:

synchronized public void process(WatchedEvent event) {
    ReplyHeader h = new ReplyHeader(-1, -1L, 0);
    if (LOG.isTraceEnabled()) {
        ZooTrace.logTraceMessage(LOG, ZooTrace.EVENT_DELIVERY_TRACE_MASK,
                                 "Deliver event " + event + " to 0x"
                                 + Long.toHexString(this.sessionId)
                                 + " through " + this);
    }
 
    // Convert WatchedEvent to a type that can be sent over the wire
    WatcherEvent e = event.getWrapper();
 
    sendResponse(h, e, "notification");
}
  • 在请求头中标记 ”-1“,表明当前是一个通知。
  • 将 WatchedEvent 包装成 WatcherEvent,以便进行网络传输序列化。
  • 向客户端发送通知。

可见 process 本质上并不是处理客户端 Watcher 真正的业务逻辑,而是借助当前客户端连接的 ServerCnxn 对象来实现对客户端的 WatchedEvent 传递,真正的客户端 Watcher 回调与业务逻辑执行都是在客户端。

总结

无论是 dataWatchers 还是 clildWatchers,事件触发逻辑都是一样的,基本步骤如下:

  1. 封装 WatchedEvent

    首先将通知状态(KeeperState)、事件类型(EventType)以及节点路径(Path)封装成一个WatchedEvent 对象。

  2. 查询 Watcher

    根据节点路径从 watchTable 中取出 Watcher,并从 watchTable 和 watch2Paths 中移除该 Watcher,说明 Watcher 在服务端是一次性的,触发一次就失效了。

  3. 调用 process 方法来触发 Watcher

    process 本质上并不是处理客户端 Watcher 真正的业务逻辑,而是借助当前客户端连接的 ServerCnxn 对象来实现对客户端的 WatchedEvent 传递,真正的客户端 Watcher 回调与业务逻辑执行都是在客户端。

客户端回调 Watcher

###SendThread 接收事件通知

对于一个来自服务端的响应,客户端都是由 SendThread.readResponse() 方法来统一进行处理的。如果响应头 replyHdr 中标识了 XID 为 -1,表明这是一个通知类型的响应。代码片段如下:

if (replyHdr.getXid() == -1) {
    // -1 means notification
    if (LOG.isDebugEnabled()) {
        LOG.debug("Got notification sessionid:0x"
                  + Long.toHexString(sessionId));
    }
    WatcherEvent event = new WatcherEvent();
    // 反序列化
    event.deserialize(bbia, "response");
 
    // convert from a server path to a client path
    // chrootPath 处理
    if (chrootPath != null) {
        String serverPath = event.getPath();
        if(serverPath.compareTo(chrootPath)==0)
            event.setPath("/");
        else if (serverPath.length() > chrootPath.length())
            event.setPath(serverPath.substring(chrootPath.length()));
        else {
            LOG.warn("Got server path " + event.getPath()
                     + " which is too short for chroot path "
                     + chrootPath);
        }
    }
 
    // 还原 WatchedEvent
    WatchedEvent we = new WatchedEvent(event);
    if (LOG.isDebugEnabled()) {
        LOG.debug("Got " + we + " for sessionid 0x"
                  + Long.toHexString(sessionId));
    }
 
    // 将 WatchedEvent 交给 EventThread 线程
    eventThread.queueEvent( we );
    return;
}

处理逻辑大致如下:

  1. 反序列化

    客户端接收到响应后,首先会将字节流转换成 WatcherEvent 对象。

  2. 处理 chrootPath

    如果客户端设置了 chrootPath 属性,那么对于服务端传过来的节点路径进行 chrootPath 处理,生成客户端的一个相对节点路径。例如 chrootPath 为 /app1,那么针对服务端传递的 /app1/locks,经过 chrootPath 处理,就会变成一个相对路径 :/locks。

  3. 还原 WatchedEvent

    将 WatcherEvent 对象还原成 WatchedEvent 对象。

  4. 回调 Watcher。

    最后将 WatchedEvent 对象交给一个 EventThread 线程,在下一个轮询周期中进行 Watcher 回调。

EventThread 处理事件通知

EventThread 线程是 Zookeeper 客户端中专门用来处理服务端通知事件的核心,上面说到 SendThread 接收到服务端的通知事件后,会通过 EventThread.queueEvent() 方法将事件传递给 EventThread 线程,其逻辑如下:

public void queueEvent(WatchedEvent event) {
    if (event.getType() == EventType.None
        && sessionState == event.getState()) {
        return;
    }
    sessionState = event.getState();
 
    // materialize the watchers based on the event
    WatcherSetEventPair pair = new WatcherSetEventPair(
        watcher.materialize(event.getState(), event.getType(),
                            event.getPath()),
        event);
    // queue the pair (watch set & event) for later processing
    waitingEvents.add(pair);
}

首先会根据该通知事件,从 ZKWatcherManager 中取出所有相关的 Watcher:

public Set<Watcher> materialize(Watcher.Event.KeeperState state,
                                Watcher.Event.EventType type,
                                String clientPath)
{
    Set<Watcher> result = new HashSet<Watcher>();

    switch (type) {
            // ...
        case NodeDataChanged:
        case NodeCreated:
            synchronized (dataWatches) {
                addTo(dataWatches.remove(clientPath), result);
            }
            synchronized (existWatches) {
                addTo(existWatches.remove(clientPath), result);
            }
            break;
            // ...
            return result;
    }
}
 
final private void addTo(Set<Watcher> from, Set<Watcher> to) {
    if (from != null) {
        to.addAll(from);
    }
}

客户端根据 EventType 会从相应的 Watcher 存储(即 dataWatchers、existWatchers 或 childWatchers 中的一个或多个,本例中就是从 dataWatchers 和 existWatchers 两个存储中获取)中去除对应的 Watcher。同样表明 Watcher 是一次性的。

获取到相关的 Watcher 后,会将其放入到 waitingEvents 这个队列中去。waitingEvents 是一个待处理 Watcher 的队列,EventThread 的 run 方法会不断对该队列进行处理:

public void run() {
    try {
        isRunning = true;
        while (true) {
            Object event = waitingEvents.take();
            if (event == eventOfDeath) {
                wasKilled = true;
            } else {
                processEvent(event);
            }
            if (wasKilled)
                synchronized (waitingEvents) {
                if (waitingEvents.isEmpty()) {
                    isRunning = false;
                    break;
                }
            }
        }
    }
    // ...
}
 
private void processEvent(Object event) {
    try {
        if (event instanceof WatcherSetEventPair) {
            // each watcher will process the event
            WatcherSetEventPair pair = (WatcherSetEventPair) event;
            for (Watcher watcher : pair.watchers) {
                try {
                    watcher.process(pair.event);
                } catch (Throwable t) {
                    LOG.error("Error while calling watcher ", t);
                }
            }
        }
    }
    // ...
}

可以看到 EventThread 线程每次都会从 waitingEvents 队列中取出一个 Watcher,并进行串行同步处理。processEvent 方法中的这个的 Watcher 才是之前客户端真正注册的 Watcher,调用其 process 方法就可以实现 Watcher 的回调了。

事件监听流程总结

  1. 客户端封装 Watcher,封装传输对象 Packet 后发送请求。
  2. 客户端 SendThread 线程接受响应,由 ZKWatchManager 的 dataWatches 进行 Watcher 管理。
  3. 服务端对应的 Watcher 对象是 ServerCnxn,它代表客户端和服务端之间的连接。
  4. WatchManager 是服务端 Watcher 的管理者,内部用 watchTable 和 watch2Paths
    从两个维度来管理 Watcher。
  5. Watcher 触发时,服务端并不真正执行监听逻辑。而是借助当前客户端连接的 ServerCnxn 对象来实现对客户端的 WatchedEvent 传递,真正的客户端 Watcher 回调与业务逻辑执行都是在客户端。
  6. 客户端 SendThread 接收事件通知,在一些处理后将 WatchedEvent 对象交给一个 EventThread 线程。
  7. EventThread 线程是 Zookeeper 客户端中专门用来处理服务端通知事件的核心,首先根据该通知事件,从 ZKWatcherManager 中取出所有相关的 Watcher。
  8. 客户端根据 EventType 会从相应的 Watcher 存储(即 dataWatchers、existWatchers 或 childWatchers 中的一个或多个)中去除对应的 Watcher。
  9. 获取到相关的 Watcher 后,会将其放入到 waitingEvents 这个队列中去。waitingEvents 是一个待处理 Watcher 的队列。
  10. EventThread 线程每次都会从 waitingEvents 队列中取出一个 Watcher,并进行串行同步处理。processEvent 方法中的这个的 Watcher 才是之前客户端真正注册的 Watcher,调用其 process 方法就可以实现 Watcher 的回调了。

Watcher 特性总结

  • 一次性

    无论客户端还是服务端,一旦一个 Watcher被触发,Zookeeper 都会从相应的存储中移除该 Watcher。这样的设计有效的减轻了服务端的压力。

  • 客户端串行执行

    客户端 Watcher 回调是一个串行同步的过程,这为我们保证了顺序。

  • 轻量

    WatchedEvent 是 Zookeeper 整个 Watcher 通知机制的最小通知单元,这个数据结构只包含三个部分:通知状态、事件类型和节点路径。也就是说,Watcher 通知非常简单,只会告诉客户端发生了事件,而不会说明事件的具体内容。例如 NodeDataChanged 事件只会通知客户端节点数据发生了变更,而对于原始数据和变更后的数据都无法从通知中获取,而是需要客户端主动重新去获取数据。

    另外客户端注册 Watcher 的时候,并不会把客户端真实的 Watcher 对象传递到服务端,仅仅只是在客户端请求中用 boolean 类型属性标记,同时客户端也仅仅保存了当前连接的 ServerCnxn 对象。

    如此轻量的 Watcher 机制设计,在网络开销和服务端内存开销上都是非常廉价的。

Netty Reactor线程启动及执行

前面通过源码分析了服务端启动时做了以下这些事:

  • 初始化 channel 注册到 eventLoop 的 selector 上
  • 触发 handlerAdded、ChannelRegistered 事件
  • 进行端口绑定
  • 触发 ChannelActive 事件,修改 selectionKey 的 interestOps 为 OP_ACCEPT

客户端启动时做了以下这些事:

  • 初始化 channel 注册到 eventLoop 的 selector 上
  • 触发 handlerAdded、ChannelRegistered 事件
  • 发送连接请求,修改 selectionKey 的 interestOps 为 OP_CONNECT
  • 连接建立后,触发 ChannelActive 事件,修改 selectionKey 的 interestOps 为 OP_READ

但是并没有去研究 Netty 的核心 Reactor 线程模型是什么样的,以及服务端如何接收一个客户端新连接的,现在我们通过跟踪源码来了解一下。

Reactor 线程启动及执行

Netty 中的 Reactor 模型可以自由配置成 单线程模型多线程模型主从多线程模型,这里不做过多解释;

服务端通常启用两个线程组 NioEventLoopGroup,一个用来接收客户端的连接请求,也就是本文的重点,我们称之为 bossGroup;另一个用来处理连接建立后的IO读写操作,我们称之为 workerGroup;其中每个 group 又包括一组 NioEventLoop;

一开始 NioEventLoop 线程并没有启动,直到第一次往 NioEventLoop 上注册 channel 的时候,会把注册任务封装成一个 task 丢到 NioEventLoop 的 taskQueue 中等待 NioEventLoop 线程启动后执行,然后将 NioEventLoop 中的 run() 方法封装成 task 交给 execute 去执行,execute 会单独起个线程去执行 NioEventLoop 的 run() 方法,这个核心线程才是我们前面说的 NioEventLoop 线程。

NioEventLoop 的 execute() 方法继承自 SingleThreadEventExecutor:

// SingleThreadEventExecutor.execute(task)
@Override
public void execute(Runnable task) {
    if (task == null) {
        throw new NullPointerException("task");
    }
    // 这里就是看当前线程是不是 NioEventLoop 线程,第一次的时候肯定返回 false
    boolean inEventLoop = inEventLoop();
    // 添加 task 到 taskQueue 中去
    addTask(task);
    if (!inEventLoop) {
        startThread();
        if (isShutdown() && removeTask(task)) {
            reject();
        }
    }

    if (!addTaskWakesUp && wakesUpForTask(task)) {
        wakeup(inEventLoop);
    }
}

// SingleThreadEventExecutor.startThread()
private void startThread() {
	// 当前未启动才启动
    if (state == ST_NOT_STARTED) {
        if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {
            try {
            	// CAS 修改状态成功才启动
                doStartThread();
            } catch (Throwable cause) {
                STATE_UPDATER.set(this, ST_NOT_STARTED);
                PlatformDependent.throwException(cause);
            }
        }
    }
}

// SingleThreadEventExecutor.doStartThread()
private void doStartThread() {
    assert thread == null;
    // 这里的 executor 一般是 ThreadPerTaskExecutor,其 execute 方法就是简单的 newThread(task).start();
    executor.execute(new Runnable() {
        @Override
        public void run() {
        	// 修改这个属性 thread 为当前线程,也就是 NioEventLoop 线程
        	// 上面 inEventLoop() 方法就是看执行 `inEventLoop()` 代码的线程是不是 NioEventLoop 线程
            thread = Thread.currentThread();
            if (interrupted) {
                thread.interrupt();
            }

            boolean success = false;
            updateLastExecutionTime();
            try {
            	// 核心方法,在 NioEventLoop 有实现
                SingleThreadEventExecutor.this.run();
                success = true;
            } catch (Throwable t) {
                logger.warn("Unexpected exception from an event executor: ", t);
            } finally {
                ...
            }
        }
    });
}

上面代码已经启动了 NioEventLoop 线程,并开始执行 NioEventLoop 的 run() 方法:

@Override
protected void run() {
    for (;;) {
        try {
        	// hasTasks 为 true 则返回调用 selectNow() 的返回值,否则返回 SelectStrategy.SELECT
            switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
                case SelectStrategy.CONTINUE:
                    continue;
                case SelectStrategy.SELECT:
                	// select 操作
                    select(wakenUp.getAndSet(false));
                    if (wakenUp.get()) {
                        selector.wakeup();
                    }
                    // fall through
                default:
            }

            cancelledKeys = 0;
            needsToSelectAgain = false;
            // io 操作的比率默认 50,也就是和 task 执行耗时尽量各占一半
            final int ioRatio = this.ioRatio;
            if (ioRatio == 100) {
                try {
                    processSelectedKeys();
                } finally {
                    // Ensure we always run tasks.
                    runAllTasks();
                }
            } else {
                final long ioStartTime = System.nanoTime();
                try {
                	// 处理轮询到的 SelectedKeys
                    processSelectedKeys();
                } finally {
                    // Ensure we always run tasks.
                    final long ioTime = System.nanoTime() - ioStartTime;
                    // 在超时时间内执行 taskQueue 中的任务
                    // ioRatio 为 50 的话,runAllTasks 的超时时间即为刚刚的IO耗时 ioTime
                    runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
                }
            }
        } catch (Throwable t) {
            handleLoopException(t);
        }
        // Always handle shutdown even if the loop processing threw an exception.
        try {
            if (isShuttingDown()) {
                closeAll();
                if (confirmShutdown()) {
                    return;
                }
            }
        } catch (Throwable t) {
            handleLoopException(t);
        }
    }
}

所以 NioEventLoop 线程主要做了以下几件事:

  • 在 selector 上轮询事件

  • 处理轮询到的事件

  • 在超时时间内执行 taskQueue 里的 task

select 操作

// oldWakenUp 表示是否唤醒正在阻塞的 select 操作
private void select(boolean oldWakenUp) throws IOException {
    Selector selector = this.selector;
    try {
        int selectCnt = 0;
        long currentTimeNanos = System.nanoTime();
        // delayNanos() 找到定时任务队列里等下最先要执行的任务,返回还要多久这个任务会执行
        long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);

        for (;;) {
        	// 计算定时任务队列里等下最先要执行的任务是不是马上就要执行了(<= 0.5ms)
            long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
            if (timeoutMillis <= 0) {
            	// 如果之前还没有 select 过,就 selectNow() 一次,并置 selectCnt 为 1
                if (selectCnt == 0) {
                    selector.selectNow();
                    selectCnt = 1;
                }
                // 跳出循环
                break;
            }

        	// 如果任务队列有任务,且 wakenUp.compareAndSet(false, true) 成功的话,就 selectNow() 一次,并置 selectCnt 为 1
            if (hasTasks() && wakenUp.compareAndSet(false, true)) {
                selector.selectNow();
                selectCnt = 1;
                break;
            }

        	// 阻塞式 select 操作
        	// 在外部线程添加任务的时候,会调用 wakeup 方法来唤醒它
            int selectedKeys = selector.select(timeoutMillis);
            selectCnt ++;

        	// 这几种情况的话,直接跳出循环
            if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
                // - Selected something,
                // - waken up by user, or
                // - the task queue has a pending task.
                // - a scheduled task is ready for processing
                break;
            }
            if (Thread.interrupted()) {
                // Thread was interrupted so reset selected keys and break so we not run into a busy loop.
                // As this is most likely a bug in the handler of the user or it's client library we will
                // also log it.
                //
                // See https://github.com/netty/netty/issues/2426
                if (logger.isDebugEnabled()) {
                    logger.debug("Selector.select() returned prematurely because " +
                            "Thread.currentThread().interrupt() was called. Use " +
                            "NioEventLoop.shutdownGracefully() to shutdown the NioEventLoop.");
                }
                selectCnt = 1;
                break;
            }

            long time = System.nanoTime();
            // 判断 select 操作是否至少持续了 timeoutMillis
            if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
                // timeoutMillis elapsed without anything selected.
                selectCnt = 1;
            } 
        	// 没有持续 timeoutMillis 则表明是一次空轮询,如果超过阈值(默认512),则重建 selector 用以避免 jdk 空轮询导致 cpu 100% 的 bug
            else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
                    selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
                // The selector returned prematurely many times in a row.
                // Rebuild the selector to work around the problem.
                logger.warn(
                        "Selector.select() returned prematurely {} times in a row; rebuilding Selector {}.",
                        selectCnt, selector);

                rebuildSelector();
                selector = this.selector;

                // Select again to populate selectedKeys.
                selector.selectNow();
                selectCnt = 1;
                break;
            }

            currentTimeNanos = time;
        }

        if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS) {
            if (logger.isDebugEnabled()) {
                logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.",
                        selectCnt - 1, selector);
            }
        }
    } catch (CancelledKeyException e) {
        if (logger.isDebugEnabled()) {
            logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector {} - JDK bug?",
                    selector, e);
        }
        // Harmless exception - log anyway
    }
}

select 操作可以总结如下:

  1. 检查是否有马上就要执行(delay<=0.5ms)的定时任务,有的话跳出循环;跳出前检查是否是第一次 select,是的话进行一次 selectNow() ,并置 selectCnt 为 1;

  2. 如果任务队列中有任务,且 wakenUp.compareAndSet(false, true) 成功的话,就 selectNow() 一次,并置 selectCnt 为 1,然后跳出循环;

  3. 调用 selector.select(timeoutMillis) 进行阻塞式 select;在此期间,外部线程添加任务可以唤醒阻塞;

  4. 如果存在以下情况,则直接跳出循环

  • 轮询到了某些事件

  • 被用户线程唤醒

  • 任务队列里存在待处理的任务

  • 存在已经准备好的待执行的定时任务

  1. 判断 select 操作是否至少持续了 timeoutMillis,持续了 timeoutMillis 的话则视为有效轮询,否则视为空轮询;当空轮询次数超过阈值(512)后,进行 selector 重建,避免 jdk 空轮询导致 cpu 100% 的 bug;

重建 selector

// NioEventLoop.rebuildSelector()
public void rebuildSelector() {
	// 当前线程不是 NioEventLoop 线程的话,则将重建任务封装成 task 丢到 NioEventLoop 的任务队列
    if (!inEventLoop()) {
        execute(new Runnable() {
            @Override
            public void run() {
                rebuildSelector0();
            }
        });
        return;
    }
    // 当前线程是 NioEventLoop 线程的话,直接调用重建方法
    rebuildSelector0();
}

// NioEventLoop.rebuildSelector0()
private void rebuildSelector0() {
    final Selector oldSelector = selector;
    final SelectorTuple newSelectorTuple;

    if (oldSelector == null) {
        return;
    }

    try {
    	// 新建一个 SelectorTuple
        newSelectorTuple = openSelector();
    } catch (Exception e) {
        logger.warn("Failed to create a new Selector.", e);
        return;
    }

    // Register all channels to the new Selector.
    int nChannels = 0;
    for (SelectionKey key: oldSelector.keys()) {
        Object a = key.attachment();
        try {
            if (!key.isValid() || key.channel().keyFor(newSelectorTuple.unwrappedSelector) != null) {
                continue;
            }

            int interestOps = key.interestOps();
            // 取消在旧 selector 上的注册
            key.cancel();
            // 将 channel 注册到新的 selector 上
            SelectionKey newKey = key.channel().register(newSelectorTuple.unwrappedSelector, interestOps, a);
            // 维护 channel 和新 selectionKey 的关系
            if (a instanceof AbstractNioChannel) {
                // Update SelectionKey
                ((AbstractNioChannel) a).selectionKey = newKey;
            }
            nChannels ++;
        } catch (Exception e) {
            logger.warn("Failed to re-register a Channel to the new Selector.", e);
            if (a instanceof AbstractNioChannel) {
                AbstractNioChannel ch = (AbstractNioChannel) a;
                ch.unsafe().close(ch.unsafe().voidPromise());
            } else {
                @SuppressWarnings("unchecked")
                NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
                invokeChannelUnregistered(task, key, e);
            }
        }
    }

    selector = newSelectorTuple.selector;
    unwrappedSelector = newSelectorTuple.unwrappedSelector;

    try {
        // time to close the old selector as everything else is registered to the new one
        oldSelector.close();
    } catch (Throwable t) {
        if (logger.isWarnEnabled()) {
            logger.warn("Failed to close the old Selector.", t);
        }
    }

    if (logger.isInfoEnabled()) {
        logger.info("Migrated " + nChannels + " channel(s) to the new Selector.");
    }
}

总结 Netty 解决 JDK 空轮询 BUG 的方式如下:

  1. 新建一个 selector

  2. 取消 channel 在旧 selector 上的注册

  3. 将 channel 注册到新的 selector 上

  4. 维护 channel 和新 selectionKey 的关系

处理轮询到的 SelectedKeys

// NioEventLoop.processSelectedKeysOptimized()
private void processSelectedKeysOptimized() {
    for (int i = 0; i < selectedKeys.size; ++i) {
        final SelectionKey k = selectedKeys.keys[i];
        // null out entry in the array to allow to have it GC'ed once the Channel close
        // See https://github.com/netty/netty/issues/2363
        selectedKeys.keys[i] = null;

        final Object a = k.attachment();
 		// 通过 SelectionKey 获取到 channel 后进行处理
        if (a instanceof AbstractNioChannel) {
            processSelectedKey(k, (AbstractNioChannel) a);
        } else {
            @SuppressWarnings("unchecked")
            NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
            processSelectedKey(k, task);
        }
    	// 判断是否需要再次 select,暂不关注
        if (needsToSelectAgain) {
            // null out entries in the array to allow to have it GC'ed once the Channel close
            // See https://github.com/netty/netty/issues/2363
            selectedKeys.reset(i + 1);

            selectAgain();
            i = -1;
        }
    }
}

// NioEventLoop.processSelectedKey(k, ch)
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
    final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
    ...
    try {
		// 准备就绪的 ops
        int readyOps = k.readyOps();
        // 连接建立成功,在 read/write 前客户端总是首先调用 finishConnect
        if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
            // remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking
            // See https://github.com/netty/netty/issues/924
            int ops = k.interestOps();
            ops &= ~SelectionKey.OP_CONNECT;
            k.interestOps(ops);

            unsafe.finishConnect();
        }

        // 处理 write 事件
        if ((readyOps & SelectionKey.OP_WRITE) != 0) {
            // Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write
            ch.unsafe().forceFlush();
        }

        // 处理 read 事件或者 accept 事件
        // 当客户端进行连接请求后,服务端需要在这里 accept 连接
        if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
            unsafe.read();
        }
    } catch (CancelledKeyException ignored) {
        unsafe.close(unsafe.voidPromise());
    }
}

执行任务 runAllTasks

在进行 select 时间并处理后,需要进行 taskQueue 里的任务处理,NioEventLoop 的 runAllTasks(timeoutNanos) 继承自 SingleThreadEventExecutor

// SingleThreadEventExecutor.runAllTasks(timeoutNanos)
protected boolean runAllTasks(long timeoutNanos) {
	// 将定时任务队列里已经准备就绪待执行的任务添加到 taskQueue 中
    fetchFromScheduledTaskQueue();
    // 从 taskQueue 中取一个 task
    Runnable task = pollTask();
    if (task == null) {
        afterRunningAllTasks();
        return false;
    }

    // 根据超时时间计算截止时间
    final long deadline = ScheduledFutureTask.nanoTime() + timeoutNanos;
    long runTasks = 0;
    long lastExecutionTime;
    // 循环执行任务,并关注是否超过截止时间
    for (;;) {
    	// 执行 task
        safeExecute(task);

        runTasks ++;

        // 每执行 64 个任务检查一次是否超过截止时间
        if ((runTasks & 0x3F) == 0) {
            lastExecutionTime = ScheduledFutureTask.nanoTime();
            if (lastExecutionTime >= deadline) {
                break;
            }
        }

        // 从 taskQueue 中取出下一个 task
        task = pollTask();
        if (task == null) {
            lastExecutionTime = ScheduledFutureTask.nanoTime();
            break;
        }
    }

    afterRunningAllTasks();
    this.lastExecutionTime = lastExecutionTime;
    return true;
}

服务端接收新连接

前面说到 NioEventLoop 线程轮询到事件后调用 NioEventLoop.processSelectedKey(k, ch) 进行处理,其中就包括接收客户端新连接的处理。

轮询到 OP_ACCEPT 事件

NioEventLoop.processSelectedKey(k, ch) 中轮询到 OP_ACCEPT 事件的相关代码如下:

int readyOps = k.readyOps();

...

if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
    unsafe.read();
}

这里服务端轮询到 OP_ACCEPT 事件后,交给 unsafe 来处理。前面介绍过,unsafe 依附于 channel,channel 的许多操作最终均交付于 unsafe 来处理。

将客户端新连接注册到 workerGroup

NioMessageUnsafe 的 read() 方法相关代码如下:

private final List<Object> readBuf = new ArrayList<Object>();

// NioMessageUnsafe.read()
@Override
public void read() {
    assert eventLoop().inEventLoop();
    final ChannelConfig config = config();
    final ChannelPipeline pipeline = pipeline();
    // 缓冲区内存分配处理器
    final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
    allocHandle.reset(config);

    boolean closed = false;
    Throwable exception = null;
    try {
        try {
            do {
            	// 不断读取消息并放入 readBuf 中
                int localRead = doReadMessages(readBuf);
                if (localRead == 0) {
                    break;
                }
                if (localRead < 0) {
                    closed = true;
                    break;
                }

                allocHandle.incMessagesRead(localRead);
            } while (allocHandle.continueReading());
        } catch (Throwable t) {
            exception = t;
        }

        int size = readBuf.size();
        // 循环处理 readBuf 中的元素,实际上就是一个个连接,交给 pipeline 去处理
        for (int i = 0; i < size; i ++) {
            readPending = false;
            // 在 pipeline 上传播 ChannelRead 事件
            pipeline.fireChannelRead(readBuf.get(i));
        }
        readBuf.clear();
        allocHandle.readComplete();
        // 在 pipeline 上传播 ChannelReadComplete 事件
        pipeline.fireChannelReadComplete();

        if (exception != null) {
            closed = closeOnReadError(exception);
			// 在 pipeline 上传播 ExceptionCaught 事件
            pipeline.fireExceptionCaught(exception);
        }

        if (closed) {
            inputShutdown = true;
            if (isOpen()) {
                close(voidPromise());
            }
        }
    } finally {
        if (!readPending && !config.isAutoRead()) {
            removeReadOp();
        }
    }
}

// NioServerSocketChannel.doReadMessages(list)
@Override
protected int doReadMessages(List<Object> buf) throws Exception {
	// 根据 jdk 的 ServerSocketChannel 去 accept 客户端连接
    SocketChannel ch = SocketUtils.accept(javaChannel());
    try {
        if (ch != null) {
        	// 将 NioServerSocketChannel 和 SocketChannel 封装成 NioSocketChannel 对象,放入到 list 中
            buf.add(new NioSocketChannel(this, ch));
            return 1;
        }
    } catch (Throwable t) {
        ...
    }
    return 0;
}

在 accept 到客户端连接,并封装成 NioSocketChannel 对象后,首先需要在 pipeline 上从 head 节点开始传播 ChannelRead 事件

// DefaultChannelPipeline.fireChannelRead(msg)
@Override
public final ChannelPipeline fireChannelRead(Object msg) {
	// 将 ChannelRead 在 pipeline 上从 head 节点开始传播
    AbstractChannelHandlerContext.invokeChannelRead(head, msg);
    return this;
}

// AbstractChannelHandlerContext.invokeChannelRead(next, msg)
static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
    final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {
        next.invokeChannelRead(m);
    } else {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                next.invokeChannelRead(m);
            }
        });
    }
}

// AbstractChannelHandlerContext.invokeChannelRead(msg)
private void invokeChannelRead(Object msg) {
    if (invokeHandler()) {
        try {
            ((ChannelInboundHandler) handler()).channelRead(this, msg);
        } catch (Throwable t) {
            notifyHandlerException(t);
        }
    } else {
        fireChannelRead(msg);
    }
}

然后 ChannelRead 事件从 head 节点经过业务 handler 会传递到 ServerBootstrapAcceptor 上。对服务端启动过程还有印象的话,ServerBootstrapAcceptor 添加时机相关流程如下:

  1. 初始化 NioServerSocketChannel 时在 pipeline 中添加 ChannelInitializer

  2. 在将 NioServerSocketChannel 注册到 selector 上后,触发 handlerAdded 事件

  3. ChannelInitializer 的 handlerAdded() 方法调用重写的 initChannel(ch) 方法

  4. initChannel(ch) 方法会在 pipeline 上添加自定义的业务 handler,然后给 eventLoop 添加一个异步任务,任务内容为将 ServerBootstrapAcceptor 添加到 pipeline 中

// ServerBootstrapAcceptor.channelRead(ctx, msg)
@Override
@SuppressWarnings("unchecked")
public void channelRead(ChannelHandlerContext ctx, Object msg) {

	// 这个 child 就是上面 accept 客户端连接后封装的 NioSocketChannel 对象
    final Channel child = (Channel) msg;
    // 在 NioSocketChannel 对象的 pipeline 上添加 childHandler
    child.pipeline().addLast(childHandler);
    // 将 childOptions 属性设置给 NioSocketChannel 对象
    setChannelOptions(child, childOptions, logger);

    for (Entry<AttributeKey<?>, Object> e: childAttrs) {
        child.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
    }

    try {
    	// 将 NioSocketChannel 对象注册到 childGroup 上,也就是我们说的 workerGroup
        childGroup.register(child).addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                if (!future.isSuccess()) {
                    forceClose(child, future.cause());
                }
            }
        });
    } catch (Throwable t) {
        forceClose(child, t);
    }
}

NioSocketChannel 对象在 childGroup 上的注册过程和之前的注册分析类似,最终调用到 AbstractUnsafe 的 register0(promise) 方法

// AbstractUnsafe.register0(promise)
private void register0(ChannelPromise promise) {
    try {
        // check if the channel is still open as it could be closed in the mean time when the register
        // call was outside of the eventLoop
        if (!promise.setUncancellable() || !ensureOpen(promise)) {
            return;
        }
        boolean firstRegistration = neverRegistered;
        // 调用注册方法
        doRegister();
        neverRegistered = false;
        registered = true;

        // 触发 HandlerAdded 事件
        pipeline.invokeHandlerAddedIfNeeded();

        safeSetSuccess(promise);

        // 触发 ChannelRegistered 事件
        pipeline.fireChannelRegistered();
        
        // 建立连接后 isActive() 为 true
        if (isActive()) {
        	// 第一次注册的话,head 节点处理 ChannelActive 时候会通过 readIfIsAutoRead(); 修改 ops 注册读事件
            if (firstRegistration) {
                pipeline.fireChannelActive();
            } else if (config().isAutoRead()) {
                // 否则需要在 read 之前注册 read 事件
                beginRead();
            }
        }
    } catch (Throwable t) {
        // Close the channel directly to avoid FD leak.
        closeForcibly();
        closeFuture.setClosed();
        safeSetFailure(promise, t);
    }
}

// AbstractNioChannel.doRegister()
@Override
protected void doRegister() throws Exception {
    boolean selected = false;
    for (;;) {
        try {
        	// 将 SocketChannel 注册到 selector 上,ops 为 0,并将 NioSocketChannel 作为附属对象
            selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
            return;
        } catch (CancelledKeyException e) {
            if (!selected) {
                // Force the Selector to select now as the "canceled" SelectionKey may still be
                // cached and not removed because no Select.select(..) operation was called yet.
                eventLoop().selectNow();
                selected = true;
            } else {
                // We forced a select operation on the selector before but the SelectionKey is still cached
                // for whatever reason. JDK bug ?
                throw e;
            }
        }
    }
}

注册 read 事件

上面我们看到个条件 isActive(),它对于服务端和客户端的含义是不一样的:

  • 服务端 NioServerSocketChannel 注册到 bossGroup 的 selector 的时候,它代表是否绑定好了端口

  • 客户端 NioSocketChannel 注册到 group 的 selector 的时候,它代表是否已经建立了连接(此时尚未与服务端建立连接)

  • 服务端 accept 客户端连接后,将 NioSocketChannel 注册到 workerGroup 的 selector 的时候,它代表是否已经建立了连接(此时已经建立了连接)

又由于 doRegister() 在将 SocketChannel 注册到 selector 上的时候 ops 为 0,所以我们需要在 read 之前注册一下 read 事件;
如果是第一次注册的话,在 pipeline 上传播 ChannelActive 事件时,head 节点会通过 readIfIsAutoRead() 修改 ops 注册读事件;
否则的话需要调用 beginRead() 方法去修改 ops 注册 read 事件;

// AbstractNioChannel.beginRead()
@Override
public final void beginRead() {
    assertEventLoop();

    if (!isActive()) {
        return;
    }

    try {
        doBeginRead();
    } catch (final Exception e) {
        invokeLater(new Runnable() {
            @Override
            public void run() {
                pipeline.fireExceptionCaught(e);
            }
        });
        close(voidPromise());
    }
}

// AbstractNioChannel.doBeginRead()
@Override
protected void doBeginRead() throws Exception {
    // Channel.read() or ChannelHandlerContext.read() was called
    final SelectionKey selectionKey = this.selectionKey;
    if (!selectionKey.isValid()) {
        return;
    }

    readPending = true;
    // 跟前面说的 readIfIsAutoRead() 最终调用都是这个地方
    final int interestOps = selectionKey.interestOps();
    if ((interestOps & readInterestOp) == 0) {
        selectionKey.interestOps(interestOps | readInterestOp);
    }
}

总结

Reactor 线程启动执行总结

在第一次调用 eventLoop.execute(task) 方法的时候,将 NioEventLoop 的 run() 方法封装成一个 task,然后交给 executor 去执行,ThreadPerTaskExecutor 只是简单的 new Thread(task).start() 去启动一个线程,即我们全篇所说的 NioEventLoop 线程。

NioEventLoop 的 run() 方法主要做了以下几件事:

  1. 轮询 selector 上事件,通过重建 selector 规避了 jdk 的空轮询导致的 cpu 100% 的 bug
  2. 处理轮询到的事件,包括 connect、accept、read、write 等事件
  3. 执行任务队列的任务,会根据上面处理事件的 ioTime 和设置的 ioRatio 计算一个超时时间

服务端接收新连接过程总结

  1. 轮询到 OP_ACCEPT 事件
  2. 根据 accept 到的 SocketChannel 构造一个 NioSocketChannel
  3. 将 NioSocketChannel 注册到 workerGroup 中一个 NioEventLoop 的 selector 上
  4. 进行 handlerAdded、ChannelRegistered 等一些事件的传播
  5. 在 read 之前需要通过修改 ops 去注册 read 事件

SpringBoot源码解析一

启动流程

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

首先从启动类开始看起,main() 方法很简单,调用 SpringApplication 的静态方法run()。

public static ConfigurableApplicationContext run(Class<?>[] primarySources, 
        String[] args) {
    return new SpringApplication(primarySources).run(args);
}

实例化一个 SpringApplication 对象

public SpringApplication(Class<?>... primarySources) {
    this(null, primarySources);
}
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    this.resourceLoader = resourceLoader;
    Assert.notNull(primarySources, "PrimarySources must not be null");
    //用Set<>保存传入的 DemoApplication.class(因为启动类可以传入多个class)
    this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    //应用类型:REACTIVE/NONE/SERVLET
    this.webApplicationType = deduceWebApplicationType();
    //设置初始化器
    setInitializers((Collection) getSpringFactoriesInstances(
            ApplicationContextInitializer.class));
    //设置监听器
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    // 找到 main 函数所在的类,并设置到 SpringApplication
    this.mainApplicationClass = deduceMainApplicationClass();
}

应用类型

看下 deduceWebApplicationType() 方法:

private WebApplicationType deduceWebApplicationType() {
    if (ClassUtils.isPresent(REACTIVE_WEB_ENVIRONMENT_CLASS, null)
            && !ClassUtils.isPresent(MVC_WEB_ENVIRONMENT_CLASS, null)
            && !ClassUtils.isPresent(JERSEY_WEB_ENVIRONMENT_CLASS, null)) {
        return WebApplicationType.REACTIVE;
    }
    for (String className : WEB_ENVIRONMENT_CLASSES) {
        if (!ClassUtils.isPresent(className, null)) {
            return WebApplicationType.NONE;
        }
    }
    return WebApplicationType.SERVLET;
}

web 应用的类型有三个:

  1. REACTIVE

    当类路径包含 REACTIVE_WEB_ENVIRONMENT_CLASS 且不包含 MVC_WEB_ENVIRONMENT_CLASS 和 JERSEY_WEB_ENVIRONMENT_CLASS 相关的类时返回

  2. NONE

    当类路径不存在 WEB_ENVIRONMENT_CLASSES 相关的类时返回

  3. SERVLET

    当类路径存在 WEB_ENVIRONMENT_CLASSES 相关的类时返回

设置初始化器和监听器

//设置初始化器
setInitializers((Collection) getSpringFactoriesInstances(
        ApplicationContextInitializer.class));
//设置监听器
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
    return getSpringFactoriesInstances(type, new Class<?>[] {});
}

getSpringFactoriesInstances(type) 方法一层层调用,目的是从 “META-INF/spring.factories”文件读取 key 为 type 的 value 名称集合,然后创建对象,返回对象集合。这里从文件中获取的是如下部分:

# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
org.springframework.boot.context.ContextIdApplicationContextInitializer,\
org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\
org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.ConfigFileApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener

然后调用 setInitializers 和 setListeners 方法设置 SpringApplication 的 变量 initializers 和 listeners

private List<ApplicationContextInitializer<?>> initializers;
private List<ApplicationListener<?>> listeners;

public void setInitializers(
        Collection<? extends ApplicationContextInitializer<?>> initializers) {
    this.initializers = new ArrayList<>();
    this.initializers.addAll(initializers);
}

public void setListeners(Collection<? extends ApplicationListener<?>> listeners) {
    this.listeners = new ArrayList<>();
    this.listeners.addAll(listeners);
}

springApplication.run()

该方法创建并刷新一个新的 ApplicationContext,代码如下:

public ConfigurableApplicationContext run(String... args) {
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    ConfigurableApplicationContext context = null;
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    configureHeadlessProperty();
    //会在 spring.factories 文件中寻找 SpringApplicationRunListener 对应的实现类的名字,并且利用反射构造实例 EventPublishingRunListener。
    SpringApplicationRunListeners listeners = getRunListeners(args);
    //会遍历 listeners 容器(ArrayList)里面所有的 SpringApplicationRunListener 对象的starting方法,其实就是调用 EventPublishingRunListener 的 starting() 方法。然后广播出去,让监听了 ApplicationStartingEvent 事件的监听器做相应处理。
    listeners.starting();
    try {
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(
                args);
        //准备环境,然后广播出去,让监听了 ApplicationEnvironmentPreparedEvent 事件的监听器做相应处理。
        ConfigurableEnvironment environment = prepareEnvironment(listeners,
                applicationArguments);
        configureIgnoreBeanInfo(environment);
        Banner printedBanner = printBanner(environment);
        //创建上下文,此时还未刷新。
        context = createApplicationContext();
        exceptionReporters = getSpringFactoriesInstances(
                SpringBootExceptionReporter.class,
                new Class[] { ConfigurableApplicationContext.class }, context);
        //准备上下文,让监听了 ApplicationPreparedEvent 事件的监听器做相应处理。
        prepareContext(context, environment, listeners, applicationArguments,
                printedBanner);
        //刷新上下文
        refreshContext(context);
        afterRefresh(context, applicationArguments);
        stopWatch.stop();
        if (this.logStartupInfo) {
            new StartupInfoLogger(this.mainApplicationClass)
                    .logStarted(getApplicationLog(), stopWatch);
        }
        //广播出去,让监听了 ApplicationStartedEvent 事件的监听器做相应处理。
        listeners.started(context);
        //执行各个自定义的 ApplicationRunner 和 CommandLineRunner
        callRunners(context, applicationArguments);
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, exceptionReporters, listeners);
                throw new IllegalStateException(ex);
    }
    try {
        //广播出去,让监听了 ApplicationReadyEvent 事件的监听器做相应处理。
        listeners.running(context);
    }
    catch (Throwable ex) {
        //让监听了 ApplicationFailedEvent 事件的监听器做相应处理。
        handleRunFailure(context, ex, exceptionReporters, null);
                throw new IllegalStateException(ex);
    }
    return context;
}

我们依次来看下上面比较重要的几个地方:

获取 SpringApplicationRunListeners

SpringApplicationRunListeners listeners = getRunListeners(args);
private SpringApplicationRunListeners getRunListeners(String[] args) {
    Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
    return new SpringApplicationRunListeners(logger, getSpringFactoriesInstances(
            SpringApplicationRunListener.class, types, this, args));
}

首先 getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args) 方法从 spring.factories 文件中找到 key 为 SpringApplicationRunListener 的 value集合,这里只有 EventPublishingRunListener 一个。然后实例化 EventPublishingRunListener 对象:

public EventPublishingRunListener(SpringApplication application, String[] args) {
    this.application = application;
    this.args = args;
    this.initialMulticaster = new SimpleApplicationEventMulticaster();
    //遍历上面 setListeners 方法设置到 application 的变量 listeners 中的监听器
    for (ApplicationListener<?> listener : application.getListeners()) {
        this.initialMulticaster.addApplicationListener(listener);
    }
}

然后据此实例化一个 SpringApplicationRunListeners 对象:

private final Log log;

private final List<SpringApplicationRunListener> listeners;

SpringApplicationRunListeners(Log log,
        Collection<? extends SpringApplicationRunListener> listeners) {
    this.log = log;
    this.listeners = new ArrayList<>(listeners);
}

listeners.starting()

SpringApplicationRunListeners 的 starting 方法如下:

public void starting() {
    //此处 listeners 中只有一个 EventPublishingRunListener 对象
    for (SpringApplicationRunListener listener : this.listeners) {
        listener.starting();
    }
}

我们再看 EventPublishingRunListener 的 starting 方法:

public void starting() {
    this.initialMulticaster.multicastEvent(
            new ApplicationStartingEvent(this.application, this.args));
}

这个我们后面详细说,先知道这里广播一个 ApplicationStartingEvent 事件出去,让监听了该事件的监听器能够做相应的处理。后面的 environmentPrepared 方法、started 方法、running 方法也是类似。

创建 ConfigurableEnvironment 对象并配置

private ConfigurableEnvironment prepareEnvironment(
        SpringApplicationRunListeners listeners,
        ApplicationArguments applicationArguments) {
    // Create and configure the environment
    ConfigurableEnvironment environment = getOrCreateEnvironment();
    configureEnvironment(environment, applicationArguments.getSourceArgs());
    //这里也是广播一个事件出去,让相关监听器做处理
    listeners.environmentPrepared(environment);
    //Bind the environment to the SpringApplication
    bindToSpringApplication(environment);
    if (!this.isCustomEnvironment) {
        environment = new EnvironmentConverter(getClassLoader())
                .convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());
    }
    ConfigurationPropertySources.attach(environment);
    return environment;
}
private ConfigurableEnvironment getOrCreateEnvironment() {
    if (this.environment != null) {
        return this.environment;
    }
    //根据 webApplicationType 创建一个 ConfigurableEnvironment 对象
    switch (this.webApplicationType) {
        case SERVLET:
            return new StandardServletEnvironment();
        case REACTIVE:
            return new StandardReactiveWebEnvironment();
        default:
            return new StandardEnvironment();
    }
}
protected void configureEnvironment(ConfigurableEnvironment environment,
                                    String[] args) {
    //配置属性源
    configurePropertySources(environment, args);
    //配置Profiles
    configureProfiles(environment, args);
}

创建上下文 ApplicationContext

protected ConfigurableApplicationContext createApplicationContext() {
    Class<?> contextClass = this.applicationContextClass;
    if (contextClass == null) {
        try {
            switch (this.webApplicationType) {
                case SERVLET:
                    contextClass = Class.forName(DEFAULT_WEB_CONTEXT_CLASS);
                    break;
                case REACTIVE:
                    contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
                    break;
                default:
                    contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
            }
        }
        catch (ClassNotFoundException ex) {
            throw new IllegalStateException(
                "Unable create a default ApplicationContext, "
                + "please specify an ApplicationContextClass",
                ex);
        }
    }
    return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}

相关常量定义如下:

/**
* The class name of application context that will be used by default for non-web
* environments.
*/
public static final String DEFAULT_CONTEXT_CLASS = "org.springframework.context."
    + "annotation.AnnotationConfigApplicationContext";

/**
* The class name of application context that will be used by default for web
* environments.
*/
public static final String DEFAULT_WEB_CONTEXT_CLASS = "org.springframework.boot."
    + "web.servlet.context.AnnotationConfigServletWebServerApplicationContext";

/**
* The class name of application context that will be used by default for reactive web
* environments.
*/
public static final String DEFAULT_REACTIVE_WEB_CONTEXT_CLASS = "org.springframework."
    + "boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext";

准备上下文

private void prepareContext(ConfigurableApplicationContext context,
                            ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
                            ApplicationArguments applicationArguments, Banner printedBanner) {
    //将 environment 设置到上下文 context
    context.setEnvironment(environment);
    //设置上下文的beanNameGenerator和resourceLoader(如果SpringApplication有的话)
    postProcessApplicationContext(context);
    //拿到之前实例化 SpringApplication 对象的时候设置的 ApplicationContextInitializer,调用它们的 initialize 方法,对上下文做初始化
    applyInitializers(context);
    //空实现
    listeners.contextPrepared(context);
    if (this.logStartupInfo) {
        logStartupInfo(context.getParent() == null);
        logStartupProfileInfo(context);
    }
    
    // Add boot specific singleton beans
    context.getBeanFactory().registerSingleton("springApplicationArguments",
                                               applicationArguments);
    if (printedBanner != null) {
        context.getBeanFactory().registerSingleton("springBootBanner", printedBanner);
    }
    
    // Load the sources
    Set<Object> sources = getAllSources();
    Assert.notEmpty(sources, "Sources must not be empty");
    //注册启动类的bean定义
    load(context, sources.toArray(new Object[0]));
    //广播 ApplicationPreparedEvent 事件
    listeners.contextLoaded(context);
}

刷新上下文

private void refreshContext(ConfigurableApplicationContext context) {
    refresh(context);
    if (this.registerShutdownHook) {
        try {
            context.registerShutdownHook();
        }
        catch (AccessControlException ex) {
            // Not allowed in some environments.
        }
    }
}
protected void refresh(ApplicationContext applicationContext) {
    Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
    ((AbstractApplicationContext) applicationContext).refresh();
}

AbstractApplicationContext 的 refresh 方法我们就不看了,前面介绍 SpringIOC 容器的时候看过了。。。

初始化器 ApplicationContextInitializer

在实例化 SpringApplication 对象的时候设置了相关的初始化器和监听器。这一部分我们先看下几个初始化器。

具体有从 spring-boot 的 META-INF/spring.factories 文件中获取到的四个:

# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
org.springframework.boot.context.ContextIdApplicationContextInitializer,\
org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\
org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer

从 spring-boot-autoconfigure 的 META-INF/spring.factories 文件中获取到的两个:

# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

然后会在 SpringApplication 调用 prepareContext() 方法中 applyInitializers(context) 的时候,依次执行各个初始化器的 initialize(context) 方法。

protected void applyInitializers(ConfigurableApplicationContext context) {
    for (ApplicationContextInitializer initializer : getInitializers()) {
        Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(
            initializer.getClass(), ApplicationContextInitializer.class);
        Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
        initializer.initialize(context);
    }
}

ConfigurationWarningsApplicationContextInitializer

其 initialize(context) 方法代码如下:

public void initialize(ConfigurableApplicationContext context) {
    context.addBeanFactoryPostProcessor(
        new ConfigurationWarningsPostProcessor(getChecks()));
}

在实例化 ConfigurationWarningsPostProcessor 时首先会调用 getChecks 获得 Check[],传入到 ConfigurationWarningsPostProcessor 的构造器中.代码如下:

protected Check[] getChecks() {
    return new Check[] { new ComponentScanPackageCheck() };
}

其返回了一个 ComponentScanPackageCheck. 其代码如下:

protected static class ComponentScanPackageCheck implements Check {
    
    private static final Set<String> PROBLEM_PACKAGES;
    
    static {
        Set<String> packages = new HashSet<>();
        packages.add("org.springframework");
        packages.add("org");
        PROBLEM_PACKAGES = Collections.unmodifiableSet(packages);
    }
}

将 org.springframework , org 加入到了PROBLEM_PACKAGES 中.

ConfigurationWarningsPostProcessor 构造器如下:

public ConfigurationWarningsPostProcessor(Check[] checks) {
    this.checks = checks;
}

这样 ConfigurationWarningsPostProcessor 就持有了 ComponentScanPackageCheck。然后将 ConfigurationWarningsPostProcessor 添加到 context 的 beanFactoryPostProcessors变量当中。

/** BeanFactoryPostProcessors to apply on refresh */
private final List<BeanFactoryPostProcessor> beanFactoryPostProcessors = new ArrayList<>();

ContextIdApplicationContextInitializer

其 initialize(context) 方法代码如下:

public void initialize(ConfigurableApplicationContext applicationContext) {
    ContextId contextId = getContextId(applicationContext);
    applicationContext.setId(contextId.getId());
    applicationContext.getBeanFactory().registerSingleton(ContextId.class.getName(),
                                                          contextId);
}
	
private ContextId getContextId(ConfigurableApplicationContext applicationContext) {
    ApplicationContext parent = applicationContext.getParent();
    if (parent != null && parent.containsBean(ContextId.class.getName())) {
        return parent.getBean(ContextId.class).createChildId();
    }
    return new ContextId(getApplicationId(applicationContext.getEnvironment()));
}
	
private String getApplicationId(ConfigurableEnvironment environment) {
    String name = environment.getProperty("spring.application.name");
    return StringUtils.hasText(name) ? name : "application";
}

这里主要是给 context 设置唯一 id,如果有父上下文,则根据父上下文的 ContextId 生成一个新的 ContextId,否则创建一个新的ContextId:从 environment 中读取 “spring.application.name” 变量的值,没有则设置 id 为 application。

DelegatingApplicationContextInitializer

其 initialize(context) 方法代码如下:

private static final String PROPERTY_NAME = "context.initializer.classes";
	
public void initialize(ConfigurableApplicationContext context) {
    ConfigurableEnvironment environment = context.getEnvironment();
    List<Class<?>> initializerClasses = getInitializerClasses(environment);
    if (!initializerClasses.isEmpty()) {
        applyInitializerClasses(context, initializerClasses);
    }
}
	
private List<Class<?>> getInitializerClasses(ConfigurableEnvironment env) {
    String classNames = env.getProperty(PROPERTY_NAME);
    List<Class<?>> classes = new ArrayList<>();
    if (StringUtils.hasLength(classNames)) {
        for (String className : StringUtils.tokenizeToStringArray(classNames, ",")) {
            classes.add(getInitializerClass(className));
        }
    }
    return classes;
}
	
private void applyInitializers(ConfigurableApplicationContext context,
                               List<ApplicationContextInitializer<?>> initializers) {
    initializers.sort(new AnnotationAwareOrderComparator());
    for (ApplicationContextInitializer initializer : initializers) {
        //依次调用配置的初始化器的 initialize 方法
        initializer.initialize(context);
    }
}

该初始化器的作用是:读取配置的 “context.initializer.classes” 值,也就是我们自定义的初始化类(实现ApplicationContextInitializer),然后实例化后依次调用其 initialize(context) 方法。

ServerPortInfoApplicationContextInitializer

其 initialize(context) 方法代码如下:

public class ServerPortInfoApplicationContextInitializer
    implements ApplicationContextInitializer<ConfigurableApplicationContext>,
ApplicationListener<WebServerInitializedEvent> {
	
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        applicationContext.addApplicationListener(this);
    }
	
    @Override
    public void onApplicationEvent(WebServerInitializedEvent event) {
        String propertyName = "local." + getName(event.getApplicationContext()) + ".port";
        setPortProperty(event.getApplicationContext(), propertyName,
                        event.getWebServer().getPort());
    }
}

将其添加的 context 的监听器集合中,监听到 WebServerInitializedEvent 事件时执行 onApplicationEvent() 方法

SharedMetadataReaderFactoryContextInitializer

其 initialize(context) 方法代码如下:

public void initialize(ConfigurableApplicationContext applicationContext) {
    applicationContext.addBeanFactoryPostProcessor(
        new CachingMetadataReaderFactoryPostProcessor());
}

将 CachingMetadataReaderFactoryPostProcessor 添加到 context 的 beanFactoryPostProcessors变量当中。

ConditionEvaluationReportLoggingListener

其 initialize(context) 方法代码如下:

public void initialize(ConfigurableApplicationContext applicationContext) {
    this.applicationContext = applicationContext;
    applicationContext
        .addApplicationListener(new ConditionEvaluationReportListener());
    if (applicationContext instanceof GenericApplicationContext) {
        // Get the report early in case the context fails to load
        this.report = ConditionEvaluationReport
            .get(this.applicationContext.getBeanFactory());
    }
}

将其添加的 context 的监听器集合中。

自定义初始化器

  1. 使用spring.factories方式:

    首先我们自定义个类实现了ApplicationContextInitializer,然后在resource下面新建/META-INF/spring.factories文件。

    org.springframework.context.ApplicationContextInitializer=\
    org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
  2. application.yml 添加配置方式:

    org.springframework.context.ApplicationContextInitializer=\
    org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
  3. 启动类直接添加方式:

    application.addInitializers((ApplicationContextInitializer<ConfigurableApplicationContext>) applicationContext -> {
        System.out.println("======MyApplicationContextInitializer======");
    });

监听器 ApplicationListener

在实例化 SpringApplication 对象的时候设置了相关的初始化器和监听器。这一部分我们看下几个监听器。

具体有从 spring-boot 的 META-INF/spring.factories 文件中获取到的九个:

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.ConfigFileApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener

从 spring-boot-autoconfigure 的 META-INF/spring.factories 文件中获取到的一个:

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer

SpringApplicationRunListeners

我们先来看下 SpringApplicationRunListeners,该对象是 SpringApplication.run() 方法执行一开始的时候创建的:

SpringApplicationRunListeners listeners = getRunListeners(args);
private SpringApplicationRunListeners getRunListeners(String[] args) {
    Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
    return new SpringApplicationRunListeners(logger, getSpringFactoriesInstances(
            SpringApplicationRunListener.class, types, this, args));
}

SpringApplicationRunListeners 内部持有 SpringApplicationRunListener 集合(也是从 spring.factories 文件中获取,只有一个 EventPublishingRunListener 对象)和1个 Log 日志类。用于SpringApplicationRunListener 监听器的批量执行。

private final Log log;
 
private final List<SpringApplicationRunListener> listeners;
 
SpringApplicationRunListeners(Log log,
                              Collection<? extends SpringApplicationRunListener> listeners) {
    this.log = log;
    this.listeners = new ArrayList<>(listeners);
}

SpringApplicationRunListener 看名字也知道用于监听 SpringApplication 的 run 方法的执行。run 方法中有几个步骤会出发对应的事件监听器:

  1. starting

    run方法执行后立即执行,对应事件 ApplicationStartingEvent

  2. environmentPrepared

    上下文 ApplicationContext 创建之前并且环境信息 ConfigurableEnvironment 准备好的时候调用;对应事件的类型是 ApplicationEnvironmentPreparedEvent

  3. contextPrepared

    上下文 ApplicationContext 创建之后并设置了环境并调用了各个初始化器的 initialize 方法之后调用;没有对应事件

  4. contextLoaded

    上下文 ApplicationContext 创建并加载之后并在refresh之前调用;对应事件的类型是ApplicationPreparedEvent

  5. started

    上下文 ApplicationContext 刷新之后调用;对应事件的类型是 ApplicationStartedEvent

  6. running

    run 方法结束之前调用;对应事件的类型是 ApplicationReadyEvent

  7. failure

    run 方法捕获异常的时候调用;对应事件的类型是 ApplicationFailedEvent

SpringApplicationRunListener

SpringApplicationRunListener 接口只有一个实现类 EventPublishingRunListener,其构造方法如下:

public EventPublishingRunListener(SpringApplication application, String[] args) {
    this.application = application;
    this.args = args;
    //创建一个事件广播器
    this.initialMulticaster = new SimpleApplicationEventMulticaster();
    for (ApplicationListener<?> listener : application.getListeners()) {
        this.initialMulticaster.addApplicationListener(listener);
    }
}

在实例化 EventPublishingRunListener 对象的时候,创建一个事件广播器 SimpleApplicationEventMulticaster,

并将所有的事件监听器保存起来:

public void addApplicationListener(ApplicationListener<?> listener) {
    synchronized (this.retrievalMutex) {
        // Explicitly remove target for a proxy, if registered already,
        // in order to avoid double invocations of the same listener.
        // 避免重复监听
        Object singletonTarget = AopProxyUtils.getSingletonTarget(listener);
        if (singletonTarget instanceof ApplicationListener) {
            this.defaultRetriever.applicationListeners.remove(singletonTarget);
        }
        this.defaultRetriever.applicationListeners.add(listener);
        this.retrieverCache.clear();
    }
}

我们以 listeners.starting() 为例, 会调用 EventPublishingRunListener 对象的 starting 方法:

public void starting() {
    this.initialMulticaster.multicastEvent(
        new ApplicationStartingEvent(this.application, this.args));
}

这里创建一个 ApplicationStartingEvent 对象作为调用事件广播器的 multicastEvent() 方法的参数,multicastEvent() 方法相关代码如下:

public void multicastEvent(ApplicationEvent event) {
    multicastEvent(event, resolveDefaultEventType(event));
}
 
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
    ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
    //获取匹配的事件监听器,遍历后通过 Executor 执行一个子线程去完成监听器listener.onApplicationEvent(event)方法。
    for (final ApplicationListener<?> listener : getApplicationListeners(event, type)) {
        Executor executor = getTaskExecutor();
        if (executor != null) {
            executor.execute(() -> invokeListener(listener, event));
        }
        else {
            invokeListener(listener, event);
        }
    }
}

我们先看下 getApplicationListeners 方法怎么根据事件类型获取匹配的事件监听器的:

protected Collection<ApplicationListener<?>> getApplicationListeners(
    ApplicationEvent event, ResolvableType eventType) {
    
    Object source = event.getSource();
    Class<?> sourceType = (source != null ? source.getClass() : null);
    ListenerCacheKey cacheKey = new ListenerCacheKey(eventType, sourceType);
    
    // Quick check for existing entry on ConcurrentHashMap...
    ListenerRetriever retriever = this.retrieverCache.get(cacheKey);
    if (retriever != null) {
        return retriever.getApplicationListeners();
    }
    
    if (this.beanClassLoader == null ||
        (ClassUtils.isCacheSafe(event.getClass(), this.beanClassLoader) &&
         (sourceType == null || ClassUtils.isCacheSafe(sourceType, this.beanClassLoader)))) {
        // Fully synchronized building and caching of a ListenerRetriever
        synchronized (this.retrievalMutex) {
            retriever = this.retrieverCache.get(cacheKey);
            if (retriever != null) {
                return retriever.getApplicationListeners();
            }
            retriever = new ListenerRetriever(true);
            Collection<ApplicationListener<?>> listeners =
                retrieveApplicationListeners(eventType, sourceType, retriever);
            this.retrieverCache.put(cacheKey, retriever);
            return listeners;
        }
    }
    else {
        // No ListenerRetriever caching -> no synchronization necessary
        return retrieveApplicationListeners(eventType, sourceType, null);
    }
}

缓存部分我们不看,主要看 retrieveApplicationListeners 方法就行,代码如下:

private Collection<ApplicationListener<?>> retrieveApplicationListeners(
    ResolvableType eventType, @Nullable Class<?> sourceType, @Nullable ListenerRetriever retriever) {
    
    List<ApplicationListener<?>> allListeners = new ArrayList<>();
    Set<ApplicationListener<?>> listeners;
    Set<String> listenerBeans;
    synchronized (this.retrievalMutex) {
        listeners = new LinkedHashSet<>(this.defaultRetriever.applicationListeners);
        // 这个里面在刷新上下文之前是不存在东西的
        listenerBeans = new LinkedHashSet<>(this.defaultRetriever.applicationListenerBeans);
    }
    for (ApplicationListener<?> listener : listeners) {
        // 查看监听器是否支持该事件,支持的话则保存到 allListeners 待返回
        if (supportsEvent(listener, eventType, sourceType)) {
            if (retriever != null) {
                //缓存起来
                retriever.applicationListeners.add(listener);
            }
            allListeners.add(listener);
        }
    }
    if (!listenerBeans.isEmpty()) {
        BeanFactory beanFactory = getBeanFactory();
        for (String listenerBeanName : listenerBeans) {
            try {
                Class<?> listenerType = beanFactory.getType(listenerBeanName);
                if (listenerType == null || supportsEvent(listenerType, eventType)) {
                    ApplicationListener<?> listener =
                        beanFactory.getBean(listenerBeanName, ApplicationListener.class);
                    if (!allListeners.contains(listener) && supportsEvent(listener, eventType, sourceType)) {
                        if (retriever != null) {
                            retriever.applicationListenerBeans.add(listenerBeanName);
                        }
                        allListeners.add(listener);
                    }
                }
            }
            catch (NoSuchBeanDefinitionException ex) {
                // Singleton listener instance (without backing bean definition) disappeared -
                // probably in the middle of the destruction phase
            }
        }
    }
    AnnotationAwareOrderComparator.sort(allListeners);
    return allListeners;
}

一个监听器支不支持该事件我们看 supportsEvent 方法:

protected boolean supportsEvent(
    ApplicationListener<?> listener, ResolvableType eventType, @Nullable Class<?> sourceType) {
    
    GenericApplicationListener smartListener = (listener instanceof GenericApplicationListener ?
                                                (GenericApplicationListener) listener : new GenericApplicationListenerAdapter(listener));
    return (smartListener.supportsEventType(eventType) && smartListener.supportsSourceType(sourceType));
}

这里可以看出 listener 不是 GenericApplicationListener 的话,则创建一个适配器对象GenericApplicationListenerAdapter。然后调用 smartListener.supportsEventType() 检查事件类型和事件源类型。如果是我们自定义的事件,则调用的是 GenericApplicationListenerAdapter 对象的 supportsEventType() 方法。代码如下:

public boolean supportsEventType(ResolvableType eventType) {
    if (this.delegate instanceof SmartApplicationListener) {
        Class<? extends ApplicationEvent> eventClass = (Class<? extends ApplicationEvent>) eventType.resolve();
        return (eventClass != null && ((SmartApplicationListener) this.delegate).supportsEventType(eventClass));
    }
    else {
        return (this.declaredEventType == null || this.declaredEventType.isAssignableFrom(eventType));
    }
}

这里判断事件监听器是不是 SmartApplicationListener 的实现类:

  1. 是则调用他们自己的 supportsEventType 方法。SmartApplicationListener 的实现类有三个:ConfigFileApplicationListener、GenericApplicationListenerAdapter、SourceFilteringListener。这里我们以 ConfigFileApplicationListener 为例,其 supportsEventType 方法如下:

    public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
        return ApplicationEnvironmentPreparedEvent.class.isAssignableFrom(eventType)
                || ApplicationPreparedEvent.class.isAssignableFrom(eventType);
    }

    很明显可以看出 ConfigFileApplicationListener 只支持 ApplicationEnvironmentPreparedEvent 和 ApplicationPreparedEvent。

  2. 不是则用监听器实现 ApplicationListener 接口时传递的泛型类型和该事件类型比较。

ConfigFileApplicationListener

ConfigFileApplicationListener 对事件的监听代码如下:

public void onApplicationEvent(ApplicationEvent event) {
    if (event instanceof ApplicationEnvironmentPreparedEvent) {
        onApplicationEnvironmentPreparedEvent(
                (ApplicationEnvironmentPreparedEvent) event);
    }
    if (event instanceof ApplicationPreparedEvent) {
        onApplicationPreparedEvent(event);
    }
}

监听两个事件:ApplicationEnvironmentPreparedEvent 和 ApplicationPreparedEvent。

加载配置文件

监听到 ApplicationEnvironmentPreparedEvent 时的处理代码如下:

private void onApplicationEnvironmentPreparedEvent(
    ApplicationEnvironmentPreparedEvent event) {
    // 从 spring.factories 文件中获取环境后置处理器
    List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
    // 同时添加 ConfigFileApplicationListener 自身
    postProcessors.add(this);
    // 利用 order 进行优先级排序
    AnnotationAwareOrderComparator.sort(postProcessors);
    // 遍历后置处理器,调用其 postProcessEnvironment 方法
    for (EnvironmentPostProcessor postProcessor : postProcessors) {
        postProcessor.postProcessEnvironment(event.getEnvironment(),
                                             event.getSpringApplication());
    }
}

这里我们主要看 ConfigFileApplicationListener 自身的 postProcessEnvironment 方法:

public void postProcessEnvironment(ConfigurableEnvironment environment,
                                   SpringApplication application) {
    addPropertySources(environment, application.getResourceLoader());
}
protected void addPropertySources(ConfigurableEnvironment environment,
                                  ResourceLoader resourceLoader) {
    // 将随机值属性源添加到环境
    RandomValuePropertySource.addToEnvironment(environment);
    // 这里的 resourceLoader 在构建 SpringApplication 的时候可以穿入,我们这里为 null
    new Loader(environment, resourceLoader).load();
}

这里创建了一个 Loader 对象并调用 load() 方法。

Loader 是 ConfigFileApplicationListener 的一个内部类,其作用是加载候选属性源并配置活动配置文件。其相关代码如下:

Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
    this.environment = environment;
    this.resourceLoader = (resourceLoader != null) ? resourceLoader
        : new DefaultResourceLoader();
    // 从 spring.factories 文件中获取 PropertySourceLoader
    // 1.PropertiesPropertySourceLoader 2.YamlPropertySourceLoader
    this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(
        PropertySourceLoader.class, getClass().getClassLoader());
}
 
public void load() {
    this.profiles = new LinkedList<>();
    this.processedProfiles = new LinkedList<>();
    this.activatedProfiles = false;
    this.loaded = new LinkedHashMap<>();
    // 初始化配置文件信息 添加到 deque 中
    initializeProfiles();
    while (!this.profiles.isEmpty()) {
        Profile profile = this.profiles.poll();
        if (profile != null && !profile.isDefaultProfile()) {
            addProfileToEnvironment(profile.getName());
        }
        load(profile, this::getPositiveProfileFilter,
             addToLoaded(MutablePropertySources::addLast, false));
        this.processedProfiles.add(profile);
    }
    resetEnvironmentProfiles(this.processedProfiles);
    // 加载配置文件
    load(null, this::getNegativeProfileFilter,
         addToLoaded(MutablePropertySources::addFirst, true));
    // 添加已加载的属性源
    addLoadedPropertySources();
}

加载配置文件信息的代码在 load() 方法里:

private void load(Profile profile, DocumentFilterFactory filterFactory,
                  DocumentConsumer consumer) {
    getSearchLocations().forEach((location) -> {
        boolean isFolder = location.endsWith("/");
        Set<String> names = isFolder ? getSearchNames() : NO_SEARCH_NAMES;
        names.forEach(
            (name) -> load(location, name, profile, filterFactory, consumer));
    });
}

getSearchLocations() 方法默认返回:classpath:/,classpath:/config/,file:./,file:./config/

getSearchNames() 方法默认返回:application

private void load(String location, String name, Profile profile,
                  DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
    if (!StringUtils.hasText(name)) {
        for (PropertySourceLoader loader : this.propertySourceLoaders) {
            if (canLoadFileExtension(loader, location)) {
                load(loader, location, profile,
                     filterFactory.getDocumentFilter(profile), consumer);
                return;
            }
        }
    }
    Set<String> processed = new HashSet<>();
    // this.propertySourceLoaders:
    // 1.PropertiesPropertySourceLoader (getFileExtensions: "properties", "xml")
    // 2.YamlPropertySourceLoader (getFileExtensions: "yml", "yaml")
    for (PropertySourceLoader loader : this.propertySourceLoaders) {
        for (String fileExtension : loader.getFileExtensions()) {
            if (processed.add(fileExtension)) {
                // 遍历每一种可能查找配置文件并加载 具体加载细节这里不看了
                loadForFileExtension(loader, location + name, "." + fileExtension,
                                     profile, filterFactory, consumer);
            }
        }
    }
}

监听 ApplicationPreparedEvent

ApplicationPreparedEvent 发生在准备好上下文但是刷新之前。

ConfigFileApplicationListener 监听到该事件的时候做了什么呢?

private void onApplicationPreparedEvent(ApplicationEvent event) {
    this.logger.replayTo(ConfigFileApplicationListener.class);
    addPostProcessors(((ApplicationPreparedEvent) event).getApplicationContext());
}
	
protected void addPostProcessors(ConfigurableApplicationContext context) {
    context.addBeanFactoryPostProcessor(
        new PropertySourceOrderingPostProcessor(context));
}

从代码可以看出来只做了一件事情:添加了一个叫 PropertySourceOrderingPostProcessor 的 BeanFactoryPostProcessor。

BeanFactoryPostProcessor 在刷新上下文的 invokeBeanFactoryPostProcessors(beanFactory) 时候会实例化并调用。

自定义监听器

public class MyApplicationStartedEventListener implements ApplicationListener<ApplicationStartedEvent> {
	
    private static final Logger LOGGER = LoggerFactory.getLogger(MyApplicationStartedEventListener.class);
	
    @Override
    public void onApplicationEvent(ApplicationStartedEvent event) {
        LOGGER.info("======MyApplicationStartedEventListener======");
    }

}

创建一个监听器类实现 ApplicationListener 并写明感兴趣的事件,然后在启动类中添加到 SpringApplication

SpringApplication application = new SpringApplication(DemoApplication.class);
application.addListeners(new MyApplicationStartedEventListener());

总结

这一部分介绍了启动流程 springApplication.run() 中做了哪些事情,以及其中涉及到的一些初始化器和事件监听器等。。。

源码解析mapper接口代理

突然想到 mybatis 的 mapper 接口没有实现类,却能在 service 层直接注入并调用其方法操作数据库,之前只是知道这么做就行,出于好奇网上找了一下答案跟着源码走了一遍。

<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
  <property name="mapperInterface" value="org.mybatis.spring.sample.mapper.UserMapper" />
  <property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>

如官方文档中所示,为了代替手工使用 SqlSessionDaoSupport 或 SqlSessionTemplate 编写数据访问对象 (DAO)的代码,MyBatis-Spring 提供了一个动态代理的实现:MapperFactoryBean。这个类 可以让你直接注入数据映射器接口到你的 service 层 bean 中。当使用映射器时,你仅仅如调用你的 DAO 一样调用它们就可以了,但是你不需要编写任何 DAO 实现的代码,因为 MyBatis-Spring 将会为你创建代理。

MapperFactoryBean 创建的代理类实现了 UserMapper 接口,并且注入到应用程序中。因为代理创建在运行时环境中(Runtime,译者注) ,那么指定的映射器必须是一个接口,而不是一个具体的实现类。

当映射器很多的时候,没有必要一个个的去注册,我们可以使用 MapperScannerConfigurer 来扫描一个或多个包路径(使用逗号或分号作为分隔符),递归搜索每个包路径下的每个映射器。

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
  <property name="basePackage" value="org.mybatis.spring.sample.mapper" />
  <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
</bean>

下面我们来看下 MapperScannerConfigurer 的源码,MapperScannerConfigurer 实现了 BeanDefinitionRegistryPostProcessor 接口,BeanDefinitionRegistryPostProcessor 接口是一个可以修改 spring 工厂中已定义的 bean 的接口,该接口有个 postProcessBeanDefinitionRegistry 方法。

重点看蓝色这行,从 scan() 就可以看出来这是对前面配置文件中定义时的包进行扫描,这个 scan()方法是从 ClassPathMapperScanner 的父类 ClassPathBeanDefinitionScanner 继承的,进去看看先。

父类的 doScan() 方法就不看了,返回一个 BeanDefinitionHolder 的Set集合,然后进 processBeanDefinitions() 方法看下

蓝色部分很明显了,对于每个 beanDefinition,将 mapperInterface 作为属性设置进去,并把 beanClass 设置为 MapperFactoryBean(原类型为mapper接口)。这也与前面用 MapperFactoryBean 的定义方式相对应。

我们再来看下 MapperFctoryBean,MapperFactoryBean 继承了 SqlSessionDaoSupport 类,SqlSessionDaoSupport 类继承 DaoSupport 抽象类,DaoSupport 抽象类实现了 InitializingBean 接口,因此实例个 MapperFactoryBean 的时候,都会调用 InitializingBean 接口的 afterPropertiesSet 方法。( InitializingBean 接口为 bean 提供了初始化方法的方式,它只包括 afterPropertiesSet 方法,凡是继承该接口的类,在初始化 bean 的时候会执行该方法。)

MapperFactoryBean重写了checkDaoConfig():

如果 mapperInterface 不在 configuration 中则添加进去,调用的是 Configuration 里的 MapperRegistry 对象的 addMapper()方法:

然后通过Spring工厂获取对应bean的时候:

这里的SqlSession是SqlSessionTemplate,SqlSessionTemplate的getMapper方法:

Configuration的getMapper方法,会使用MapperRegistry的getMapper方法:

MapperRegistry的getMapper方法:

MapperProxyFactory构造MapperProxy:

再来看下MapperProxy:

MapperProxy 实现了 InvocationHandler,这就很明显了啊,用的是 JDK 动态代理啊,具体与 CGLIB 代理区别见JDK动态代理与CGLIB动态代理。然后再看 mapperMethod 的
execute()方法:

整了半天原来还是sqlSession在操作啊(虽然以前就知道,但还是要表现的很惊讶哈哈)

ZooKeeper ZAB 协议

ZAB(ZooKeeper Atomic Broadcast 原子广播) 协议是为分布式协调服务 ZooKeeper 专门设计的一种支持崩溃恢复的原子广播协议。 在 ZooKeeper 中,主要依赖 ZAB 协议来实现分布式数据一致性,基于该协议,ZooKeeper 实现了一种主备模式的系统架构来保持集群中各个副本之间的数据一致性。

原子广播

  • 在zookeeper 的主备模式下,通过 zab 协议来保证集群中各个副本数据的一致性。

  • zookeeper 使用的是单一的主进程来接收并处理所有的事务请求,并采用 zab 协议,把数据的状态变更以事务请求的形式广播到其他的节点

  • 数据更新(事务请求) 客户端每发送一个更新请求,ZooKeeper 都会生成一个全局唯一的递增编号,这个编号反映了所有事务操作的先后顺序, 这个唯一编号就是事务ID(ZXID),只有更新请求才算是事务请求。

  • 为保证按照事务的 ZXID 先后顺序来处理,Leader 服务器会分别为每个 Follower 服务器创建一个队列,并将事务的先后顺序放入队列中, 并按照 FIFO 的策略进行消息发送。收到需要处理的事务后,Follower 服务器会首先以及事务日志的形式写入服务器的磁盘中,写入成功后 会向 Leader 服务器发送 ACK 响应。

  • 当 Leader 服务器收到超过一半的 Follower 服务器的 ACK 响应后,会向所有 Follower 服务器广播 COMMIT 消息, 收到 COMMIT 消息的 Follower 服务器也会完成对事务的提交。

  • 如果接收到事务请求的是 Follower 服务器,它会将请求转发给 Leader 服务器处理。

崩溃恢复

崩溃恢复是说 Leader 节点崩溃(或者是与过半集群机器失去通信)后,会在集群中重新选举出一个 Leader 节点,并且等原来的 Leader 节点恢复后加入到集群中,仍然能保证各节点数据的一致性。

这就需要解决如下两个问题:

  • 已经被旧 Leader 提交 的 proposal 不能丢
  • 保证丢弃那些只在旧 Leader 上 提出 的 proposal

不能丢的 proposal

场景:当 Leader 节点提出一个 proposal 并发送给其他 Follower 节点,当 Leader 收到足够数量 Follower 的 ACKs 后,就向各个 Follower 广播 COMMIT 命令,同时也会在本地执行 COMMIT 并向连接的客户端返回「成功」。
但是如果在各个 Follower 在收到 COMMIT 命令前 Leader 就挂了,导致剩下的服务器并没有执行都这条消息。

如何解决以上这种情况:已经被 Leader 节点 COMMIT 的 proposal 不能丢呢?

  1. 选举拥有最大 ZXID 的 proposal 的节点作为新的 Leader。由于 Leader 上所有 proposal 被 COMMIT 之前都要有足够数量的 Follower 的 ACKs,
    即足够数量的 Follower 节点上都有这个被 COMMIT 的 proposal,因此被选举出来的新 Leader 上保存了所有被 COMMIT 的 proposal。
  2. 新的 Leader 将自己的未被提交 proposal 进行 COMMIT。
  3. 新的 Leader 和 Follower 进行先进先出队列,先将自己有但是 Follower 没有的 proposal 发送给 Follower,
    再将这些 proposal 的 COMMIT 命令广播给 Follower节点,确保所有的 Follower 处理了所有的消息。

通过如上策略可以保证已经被旧 Leader 提交的 proposal 不会丢失。

注:ZXID 是 64 位,高 32 是纪元(epoch)编号,每经过一次 Leader 选举产生一个新的 Leader,新 Leader 会将 epoch 号 +1。
低 32 位是消息计数器,每接收到一条消息这个值 +1,新 Leader 选举后这个值重置为 0。

保证丢弃的 proposal

场景:当 Leader 节点提出一个 proposal 后立即崩溃了,导致所有的 Follower 节点都没有收到这个 proposal。
当该节点从崩溃状态恢复后以 Follower 角色重连上新的 Leader,如果他依旧保留这个 proposal 的状态,与集群其他节点的状态是不一致的。所以需要丢弃。

解决方案如下:
当旧的 Leader 作为 Follower 接入新的 Leader 后,新的 Leader 会让它将所有的拥有旧的 epoch 号的未被 COMMIT 的 proposal 清除。

总结

  • Zookeeper 通过投票(半数通过则通过)选举 Leader 节点
  • Leader 节点负责接收客户端事务请求并广播处理
  • Leader 将客户端事务请求以 proposal 形式广播给 Follower 节点,并等待 ACK 响应。如果收到过半数量的 Follower 节点的 ACK 响应则向 Follower 节点广播 COMMIT 命令,Follower 节点受到 COMMIT 命令则对事务进行提交处理。
  • Leader 节点崩溃恢复后,已经被 Leader 节点 COMMIT 的 proposal 不能丢
  • Leader 节点崩溃恢复后,保证丢弃那些只在旧 Leader 上 提出 的 proposal

Java并发编程之 volatile

简介

Java 中如果一个变量被声明是 volatile 的,则一个线程对该变量的修改对其它线程是立即可见的。但是并不能保证基于该变量的操作的原子性。
所以 volatile 变量可以看成功能被削的 synchronized,但是与 synchronized 相比,使用 volatile 编码较少,且运行时开销也较少,
因此在 volatile 能满足需求的时候不需要使用 synchronized。

volatile 的使用方式很简单,只要修饰一个能被多个线程访问的变量即可,下面是一个单例的代码:

public class Singleton {
    private volatile static Singleton singleton;  
    private Singleton (){}
    public static Singleton getSingleton() {  
        if (singleton == null) {
            synchronized (Singleton.class) {  
                if (singleton == null) {
                    singleton = new Singleton();  
                }
            }  
        }
        return singleton;  
    }
}

volatile 的原理

Java 内存模型(JMM)

JMM 规定了所有的变量的都是存储在 主内存 中的,每个线程都有自己的 工作内存,线程的 工作内存 中存储的是 主内存 中变量的
副本拷贝,线程对变量的操作只能发生在 工作内存 中,不能直接读写 主内存。不同线程之间无法访问对方的 工作线程,只能通过 主内存 的同步。

但是这样的话就可能发生线程A修改了某个变量但是线程B不知道的情况。(可见性问题)

内存屏障

内存屏障(英语:Memory barrier),也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是 CPU 或编译器在对内存随机访问的操作中的一个同步点,
使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作。

大多数现代计算机为了提高性能而采取乱序执行,这使得内存屏障成为必须。

语义上,内存屏障之前的所有写操作都要写入内存;内存屏障之后的读操作都可以获得同步屏障之前的写操作的结果。因此,对于敏感的程序块,写操作之后、读操作之前可以插入内存屏障。

  • LoadLoad Barriers:该屏障确保屏障前的读操作效果先于屏障后的读操作效果
  • StoreStore Barriers:该屏障确保屏障前的写操作效果先于屏障后的写操作效果
  • LoadStore Barriers:该屏障确保屏障前的读操作效果先于屏障后的写操作效果
  • StoreLoad Barriers:该屏障确保屏障前的写操作效果先于屏障后的读操作效果

volatile 变量的内存屏障使用如下:

  • 在每个 volatile 写操作的前面插入一个 StoreStore 屏障,保证之前所有的普通写在 volatile 写之前刷新回 主内存
  • 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障,避免 volatile 写与后面可能发生的 volatile 读写操作重排序
  • 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障,避免 volatile 读与后面的普通读重排序
  • 在每个 volatile 读操作的后面插入一个 LoadStore 屏障,避免 volatile 读与后面的普通写重排序

volatile 与可见性

通过汇编码可以看出 volatile 实际上是基于 lock 内存屏障指令来完成可见性的。

  • 修改 volatile 变量后立即刷新回 主内存
  • 工作内存 使用 volatile 变量时从 主内存 获取最新值

volatile 与有序性

  • StoreStore 屏障保证 volatile 写之前的操作不会被编译器重排序到 volatile 写之后。
  • LoadLoad、LoadStore 屏障保证 volatile 读之后的操作不会被编译器重排序到 volatile 读之前。
  • 当第一个操作是 volatile 写,第二个操作是 volatile 读时,不能重排序。

volatile 的使用场景

volatile 适用于下面两种情况:

  1. 变量的写操作不受变量的当前值影响
  2. 变量不与其它变量共同参与不变约束

状态标记

volatile boolean shutdownRequested;
 
...
 
public void shutdown() { shutdownRequested = true; }
 
public void doWork() { 
    while (!shutdownRequested) { 
        // do stuff
    }
}

用 volatile 变量作为一个状态标记,当其它线程转换了状态,当前线程对该状态立即可见,从而进行正确的处理,较使用 synchronized 编码简单许多。

一次性安全发布

public class BackgroundFloobleLoader {
    public volatile Flooble theFlooble;
 
    public void initInBackground() {
        // do lots of stuff
        theFlooble = new Flooble();  // this is the only write to theFlooble
    }
}
 
public class SomeOtherClass {
    public void doWork() {
        while (true) { 
            // do some stuff...
            // use the Flooble, but only if it is ready
            if (floobleLoader.theFlooble != null) 
                doSomething(floobleLoader.theFlooble);
        }
    }
}

与文章开头的单例代码类似,如果 theFlooble 变量不使用 volatile 修饰,通过 floobleLoader.theFlooble 引用到的对象可能是一个不完全构造的 Flooble。
因为 theFlooble = new Flooble() 可能先让变量指向一块内存,但是该内存中的 Flooble 对象还未初始化。

该模式的一个必要条件是:被发布的对象必须是线程安全的,或者是有效的不可变对象(有效不可变意味着对象的状态在发布之后永远不会被修改)。

独立观察

该模式定期"发布"观察结果供程序内部使用,示例代码如下:

public class UserManager {
    public volatile String lastUser;
 
    public boolean authenticate(String user, String password) {
        boolean valid = passwordIsValid(user, password);
        if (valid) {
            User u = new User();
            activeUsers.add(u);
            lastUser = user;
        }
        return valid;
    }
}

该代码用 volatile 修饰变量 lastUser,然后反复使用 lastUser 来引用最新的有效的用户名。

该模式是前面模式的扩展;将某个值发布以在程序内的其他地方使用,但是与一次性事件的发布不同,这是一系列独立事件。
这个模式要求被发布的值(即每次验证的String user)是有效不可变的 —— 即值的状态在发布后不会更改。

“volatile bean” 模式

volatile bean 模式的基本原理是:很多框架为易变数据的持有者(例如 HttpSession)提供了容器,但是放入这些容器中的对象必须是线程安全的。

在 volatile bean 模式中,JavaBean 的所有数据成员都是 volatile 类型的,并且 getter 和 setter 方法必须非常普通 —— 除了获取或设置相应的属性外,
不能包含任何逻辑。此外,对于对象引用的数据成员,引用的对象必须是有效不可变的。(这将禁止具有数组值的属性,因为当数组引用被声明为 volatile 时,
只有引用而不是数组本身具有 volatile 语义)。

@ThreadSafe
public class Person {
    private volatile String firstName;
    private volatile String lastName;
    private volatile int age;
 
    public String getFirstName() { return firstName; }
    public String getLastName() { return lastName; }
    public int getAge() { return age; }
 
    public void setFirstName(String firstName) { 
        this.firstName = firstName;
    }
 
    public void setLastName(String lastName) { 
        this.lastName = lastName;
    }
 
    public void setAge(int age) { 
        this.age = age;
    }
}

开销较低的读写锁策略

如果读操作远远超过写操作,您可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销。示例代码显示的线程安全的计数器使用 synchronized 确保增量操作是原子的,
并使用 volatile 保证当前结果的可见性。如果更新不频繁的话,该方法可实现更好的性能,因为读路径的开销仅仅涉及 volatile 读操作,这通常要优于一个无竞争的锁获取的开销。

public class CheesyCounter {
    private volatile int value;
    public int getValue() { return value; }
    public synchronized int increment() {
        return value++;
    }
}

总结

  • volatile 基于内存屏障保证了可见性和有序性,但不保证原子性。
  • 使用条件:变量真正独立于其它变量和自己以前的值。

Zookeeper 客户端和服务端

客户端

Zookeeper 客户端主要由以下几个核心组件组成:

  • Zookeeper 实例:客户端的入口。
  • ClientWatchManager:客户端 Watcher 管理器。
  • HostProvider:客户端地址列表管理器。
  • ClientCnxn:客户端核心线程,其内部又包含两个线程,SendThread 和 EventThread。前者是一个 I/O 线程,主要负责 Zookeeper 客户端与服务端之间的网络 I/O 通信;后者是一个事件线程,主要负责对服务端事件进行处理。

客户端整个初始化和启动过程大体分为以下三个步骤:

  1. 设置默认 Watcher
  2. 设置 Zookeeper 服务器地址列表
  3. 创建 ClientCnxn

一次会话的创建过程

初始化阶段

  1. 初始化 Zookeeper 对象。

    调用构造方法实例话一个 Zookeeper 对象,在初始化过程中,会创建一个客户端的 Watcher 管理器:ClientWatchManager(默认实现为 ZKWatchManager)。

  2. 设置会话默认 Watcher。

    如果构造方法中传入了一个 Watcher 对象,那么客户端会将这个对象作为默认 Watcher 保存在 ZKWatchManager 中。

  3. 构造 Zookeeper 服务器地址列表管理器:HostProvider。

    对于构造方法中传入的服务器地址,客户端会将其放在服务器地址列表管理器 HostProvider 中。

  4. 创建并初始化客户端网络连接器:ClientCnxn。

    客户端会首先创建一个网络连接器 ClientCnxn,用来管理客户端和服务端的网络交互。另外,客户端在创建 ClientCnxn 的同时还会初始化客户端的两个核心队列 outgoingQueue 和 pendingQueue,分别作为客户端的请求发送队列和服务端响应的等待队列。

    ClientCnxn 连接器的底层 I/O 处理器是 ClientCnxnSocket,因此在这一步中,客户端还会同时创建 ClientCnxnSocket 处理器。

  5. 初始化 SendThread 和 EventThread。

    客户端会创建两个核心网络线程 SendThread 和 EventThread,前者用于管理客户端和服务端之间的所有网络 I/O,后者则用于进行客户端的事件处理。同时客户端还会将 ClientCnxnSocket 分配给 SendThread 作为底层网络 I/O 处理器,并初始化 EventThread 的待处理事件队列 waitingEvents,用于存放所有等待被客户端处理的事件。

会话创建阶段

  1. 启动 SendThread 和 EventThread。

    SendThread 首先会判断当前客户端的状态,进行一系列清理性工作,为客户端发送 “会话创建” 请求做准备。

  2. 获取一个服务器地址。

    在创建 TCP 连接前,SendThread 首先需要获取一个服务器的目标地址,这通常是从 HostProvider 中随机获取出一个地址,然后委托给 ClientCnxnSocket 去创建与服务器之间的 TCP 连接。

  3. 创建 TCP 连接。

    获取到一个服务器地址后,ClientCnxnSocket 负责和服务器创建一个 TCP 长连接。

  4. 构造 ConnectRequest 请求。

    上一步只是纯粹地从网络 TCP 层面完成了客户端和服务端之间的 Socket 连接,但远未完成 Zookeeper 客户端的会话创建。

    SendThread 会负责根据当前客户端的实际设置,构造一个 ConnectRequest 请求,该请求代表了客户端试图和服务器创建一个会话。同时,Zookeeper 客户端还会进一步将请求包装成网络 I/O 层的 Packet 对象,放入请求发送队列 outgoingQueue 中去。

  5. 发送请求。

    当客户端请求准备完毕后,就可以开始向服务端发送请求了。ClientCnxnSocket 负责从 outgoingQueue 中取出一个待发送的 Packet 对象,将其序列化成 ByteBuffer 后,向服务端进行发送。

响应处理阶段

  1. 接收服务端响应。

    ClientCnxnSocket 接收到服务端的响应后,会首先判断当前客户端状态是否是 “已初始化”,如果尚未完成初始化,那么就认为该响应一定是会话创建请求的响应,直接交由 readConnectResult 方法来处理该响应。

  2. 处理 Response。

    ClientCnxnSocket 会对接收到的服务端响应进行反序列化,得到 ConnectResponse 对象,并从中获取到 Zookeeper 服务端分配的会话 sessionId。

  3. 连接成功。

    连接成功后,一方面需要通知 SendThread 线程,进一步对客户端进行会话参数设置,包括 readTimeout 和 connectTimeout 等,并更新客户端状态。另一方面,需要通知地址管理器 HostProvider 当前成功连接的服务器地址。

  4. 生成事件 SyncConnected-None。

    为了能够让上层应用感知到会话的成功创建,SendThread 会生成一个 SyncConnected-None 事件,代表客户端和服务器会话创建成功,并将该事件传递给 EventThread 线程。

  5. 查询 Watcher。

    EventThread 线程收到事件后,会从 ZKWatchManager 管理器中查询出对应的 Watcher,针对 SyncConnected-None 事件,那么就直接找出步骤 2 中存储的默认 Watcher,然后将其放到 EventThread 的 waitingEvents 队列中去。

  6. 处理事件。

    EventThread 不断地从 waitingEvents 队列中取出待处理的 Watcher 对象,然后直接调用该对象的 process 接口方法,以达到触发 Watcher 的目的。

ClientCnxn: 网络 I/O

ClientCnxn 是 Zookeeper 客户端的核心工作类,负责维护客户端和服务端的网络连接并进行一系列网络通信。

Packet

Packet 是 ClientCnxn 内部定义的一个对协议层的封装,作为 Zookeeper 中请求和响应的载体,其中包含了最基本的请求头(requestHeader)、响应头(replyHeader)、请求体(request)、响应体(response)、节点路径(clientPath/serverPath)和注册的 Watcher(watchRegistration)等信息。

针对 Packet 中这么多属性,是否都会在客户端和服务端之间进行网络传输呢?答案是否定的。Packet 的 createBB( ) 方法负责对 Packet 对象进行序列化,最终生成可用于底层网络传输的 ByteBuffer 对象。在这个过程中,只会将 requestHeader、request 和 readOnly 三个属性进行序列化,其余属性都保存在客户端的上下文中,不会进行与服务端之间的网络传输。

outgoingQueue 和 pendingQueue

ClientCnxn 中有两个比较核心的 LinkedList 队列:outgoingQueue 和 pendingQueue。

  • outgoingQueue 是一个请求发送队列,用于存储那些需要发送到服务端的 Packet 集合。

  • pendingQueue 用于存储已经从客户端发送到服务端,但是需要等待服务端响应的 Packet 集合。

ClientCnxnSocket:底层 Socket 通信层

ClientCnxnSocket 定义了底层 Socket 通信的接口。在 Zookeeper 中,其默认实现是 ClientCnxnSocketNIO。该实现类使用 Java 原生的 NIO 接口,其核心是 doIO 逻辑,主要负责对请求的发送和响应接受过程。

请求发送

在正常情况下(即 TCP 连接正常且会话有效),会从 outgoingQueue 队列中取出一个可发送的 Packet 对象,同时生成一个客户端请求序号 XID 并将其设置到 Packet 请求头中,然后将其序列化后发送。那什么样的 Packet 是 可发送 的呢?在 outgoingQueue 中的 Packet 整体上是按照先进先出的顺序被处理的,但是如果检测到客户端和服务端之间正在处理 SASL 权限的话,那么那些不含请求头(requestHeader)的 Packet(例如会话创建请求)是可以被发送的,其余的都无法被发送。

请求发送完毕后,会立即将该 Packet 保存到 pendingQueue 队列中,以便等待服务端响应返回后进行相应的处理。

响应接受

客户端获取到来自服务端的响应数据后,根据不同的客户端请求类型,会进行不同的处理。

  • 如果检测到当前客户端还尚未进行初始化,那么说明当前客户端和服务端之间正在进行会话创建,那么直接将接收到的 ByteBuffer(incomingBuffer)反序列化成 ConnectResponse 对象。
  • 如果当前客户端已经处于正常的会话周期,并且接收到的服务端响应是一个事件,那么 Zookeeper 客户端会将接收到的 ByteBuffer(incomingBuffer)反序列化成 WatcherEvent 对象,并将该事件放入待处理队列中。
  • 如果是一个常规的请求响应(指的是 Create、GetData 和 Exist 等操作请求),那么会从 pendingQueue 队列中取出一个 Packet 来进行相应的处理。Zookeeper 客户端首先会校验服务端响应中包含的 XID 值来确保请求处理的顺序性,然后将接收到的 ByteBuffer(incomingBuffer)反序列化成 Response 对象。

最后,会在 finishPacket 方法中处理 Watcher 注册等逻辑。

SendThread 和 EventThread

SendThread

SendThread 是 ClientCnxn 内部一个核心的 I/O 调度线程,用于管理客户端和服务端之间的所有网络 I/O 操作。

在客户端实际运行过程中,一方面,SendThread 维护了客户端和服务端之间的会话生命周期,其通过在一定的周期频率内向服务端发送一个 PING 包来实现心跳检测。同时,在会话周期内,如果客户端和服务端之间出现 TCP 连接断开的情况,那么就会自动且透明地完成重连操作。

另一方面,SendThread 管理了客户端所有的请求发送和响应接收操作,其将上层客户端 API 操作转换成相应的请求协议并发送到服务端,并完成对同步调用的返回和异步调用的回调。同时,SendThread 还负责将来自服务端的事件传递给 EventThread 去处理。

EventThread

EventThread 是 ClientCnxn 内部另一个核心线程,负责客户端的事件处理。并触发客户端注册的 Watcher 监听。其中的 waitingEvents 队列,用于临时存放那些需要被触发的 Object,包括那些客户端注册的 Watcher 和异步接口中注册的回调器 AsyncCallback。

同时,EventThread 会不断地从 waitingEvents 中取出 Object,识别出具体类型(Watcher 或者 AsyncCallback),并分别调用 process 和 processResult 接口方法来实现对事件的触发和回调。

服务器启动

单机版服务器启动

Zookeeper 服务器的启动,大体可以分为以下五个主要步骤:配置文件解析、初始化数据管理器、初始化网络 I/O 管理器、数据恢复和对外服务。启动流程如下:

预启动

  1. 统一由 QuorumPeerMain 作为启动类。

    无论是单机模式还是集群模式,在 zkServer.cmdzkserver.sh 两个脚本中,都配置了使用 QuorumPeerMain 作为启动入口类。

  2. 解析配置文件 zoo.cfg。

    Zookeeper 首先会进行配置文件的解析,zoo.cfg 配置了 Zookeeper 运行时的基本参数,包括 tickTime、dataDir、clientPort 等参数。

  3. 创建并启动历史文件清理器 DatadirCleanupManager。

    自动清理历史数据文件的机制,包括对事务日志和快照数据文件进行定时清理。

  4. 判断当前是集群模式还是单机模式的启动。

    如果是单机模式,则委托给 ZooKeeperServerMain 进行启动处理。

  5. 再次进行配置文件的解析。

  6. 创建服务器实例 ZookeeperServer。

    ZookeeperServer 是单机版 Zookeeper 服务端最为核心的实体类。Zookeeper 首先会进行服务器实例的创建,接下去的步骤则是对该服务器实例的初始化工作,包括连接器、内存数据库和请求处理器等组件的初始化。

初始化

  1. 创建服务器统计器 ServerStats。

    ServerStats 是服务器运行时的统计器,包含了最基本的运行时信息。如packetsSent(服务端向客户端发送的响应包次数)、packetsReceived(服务端接收到来自客户端的请求包次数)等。

  2. 创建 Zookeeper 数据管理器 FileTxnSnapLog。

    FileTxnSnapLog 是 Zookeeper 上层服务器和底层数据存储之间的对接层,提供了一系列操作数据文件的接口,包括事务日志文件和快照数据文件。Zookeeper 根据 zoo.cfg 文件中解析出来的快照数据目录 dataDir 和 事务日志目录 dataLogDir 来创建 FileTxnSnapLog。

  3. 设置服务器 tickTime 和会话超时时间限制。

  4. 创建 ServerCnxnFactory。

    可以通过配置系统属性 zookeeper.serverCnxnFactory 来指定使用 Zookeeper 自己实现的 NIO 还是使用 Netty 框架来作为 Zookeeper 服务端网络连接工厂。

  5. 初始化 ServerCnxnFactory。

    Zookeeper 首先会初始化一个 Thread,作为整个 ServerCnxnFactory 的主线程,然后再初始化 NIO 服务器。

  6. 启动 ServerCnxnFactory 主线程。

    步骤 5 中已经初始化的主线程 ServerCnxnFactory 的主逻辑(run 方法)。需要注意的是,虽然这里 Zookeeper 的 NIO 服务器已经对外开放端口,客户端能够访问到 Zookeeper 的客户端服务端口 2181,但是此时 Zookeeper 服务器是无法正常处理客户端请求的。

  7. 恢复本地数据。

    每次在 Zookeeper 启动的时候,都需要从本地快照数据文件和事务日志文件中进行数据恢复。

  8. 创建并启动会话管理器 SessionTracker。

    在 Zookeeper 启动阶段,会创建一个会话管理器 SessionTracker。SessionTracker 初始化完毕后,Zookeeper 就会立即开始会话管理器的会话超时检查。

  9. 初始化 Zookeeper 的请求处理链。

    Zookeeper 的请求处理方式是典型的责任链模式的实现,在 Zookeeper 服务器上,会有多个请求处理器依次来处理一个客户端请求。在服务器启动的时候,会将这些请求处理器串联起来形成一个请求处理链。单机版服务器的请求处理链主要包括 PrepRequestProcessor、SyncRequestProcessor 和 FinalRequestProcessor 三个请求处理器。

  10. 注册 JMX 服务。

    Zookeeper 会将服务器运行时的一些信息以 JMX 的方式暴露给外部。

  11. 注册 Zookeeper 服务器实例。

    在步骤 6 中,Zookeeper 已经将 ServerCnxnFactory 主线程启动,但是同时我们提到此时 Zookeeper 依旧无法处理客户端请求,原因就是此时网络层尚不能够访问 Zookeeper 服务器实例。在经过后续步骤的初始化后,Zookeeper 服务器实例已经初始化完毕,只需要注册给 ServerCnxnFactory 即可,之后,Zookeeper 就可以对外提供正常的服务了。

集群版服务器启动

预启动

  1. 统一由 QuorumPeerMain 作为启动类。

  2. 解析配置文件 zoo.cfg。

  3. 创建并启动历史文件清理器 DatadirCleanupManager。

  4. 判断当前是集群模式还是单机模式的启动。

    在集群模式中,由于已经在 zoo.cfg 中配置了多个服务器地址,因此此处选择集群模式启动。

初始化

  1. 创建 ServerCnxnFactory。

  2. 初始化 ServerCnxnFactory。

  3. 创建 Zookeeper 数据管理器 FileTxnSnapLog。

  4. 创建 QuorumPeer 实例。

    QuorumPeer 是集群模式下特有的对象,是 Zookeeper 服务器实例(ZookeeperServer)的托管者,从集群层面看,QuorumPeer 代表了 Zookeeper 集群中的一台机器。在运行期间,QuorumPeer 会不断检测当前服务器实例的运行状态,同时根据情况发起 Leader 选举。

  5. 创建内存数据库 ZKDatabase。

    ZKDatabase 是 Zookeeper 的内存数据库,负责管理 Zookeeper 的所有会话记录以及 DataTree 和 事务日志的存储。

  6. 初始化 QuorumPeer。

    将一些核心的组件注册到 QuorumPeer 中去,包括 FileTxnSnapLog、ServerCnxnFactory 和 ZKDatabase。同时 Zookeeper 还会对 QuorumPeer 配置一些参数,包括服务器地址列表、Leader 选举算法和会话超时时间限制等。

  7. 恢复本地数据。

  8. 启动 ServerCnxnFactory。

Leader 选举

  1. 初始化 Leader 选举。

    Zookeeper 首先会根据自身的 SID(服务器 ID)、lastLoggerdZxid(最新的 ZXID)和当前的服务器epoch(currentEpoch)来生成一个初始化的投票——简单地讲,在初始化过程中,每个服务器都会给自己投票

    在初始化阶段,Zookeeper 会首先创建 Leader 选举所需的网络 I/O 层 QuorumCnxManager,同时启动对 Leader 选举端口的监听,等待集群中其他服务器创建连接。

  2. 注册 JMX 服务。

  3. 检测当前服务器状态。

    上文说到 QuorumPeer 是 Zookeeper 服务器实例的托管者,在运行期间,QuorumPeer 的核心工作就是不断检查当前服务器的状态,并作出相应的处理。在正常情况下,Zookeeper 服务器的状态在 LOOKING、LEADING 和 FOLLOWING/OBSERVING 之间进行切换。而在启动阶段,QuorumPeer 的初始化状态是 LOOKING,因此开始进行 Leader 选举。

  4. Leader 选举。

    简单的讲,Leader 选举过程就是一个集群中所有机器相互之间进行一系列投票,选举产生最合适的机器称为 Leader,同时其他机器称为 Follower 或是 Observer 的集群机器角色初始化过程。

    关于 Leader 选举算法,简而言之,就是集群中哪个机器处理的数据越新(通常我们根据每个服务器处理过的最大 ZXID 来比较确定其数据是否更新),其越有可能成为 Leader。当然,如果集群中所有机器处理的 ZXID 一致的话,那么 SID 最大的服务器成为 Leader

Leader 和 Follower 启动期交互过程

  1. 创建 Leader 服务器和 Follower 服务器。

    完成 Leader 选举后,每个服务器都会根据自己的服务器角色创建相应的服务器实例,并开始进行各自角色的主流程。

  2. Leader 服务器启动 Follower 接收器 LearnerCnxAcceptor。

    在集群运行期间,Leader 服务器需要和所有其余的服务器(以下部分,我们使用 “Learner” 来指代这类机器)保持连接以确定集群的机器存活情况。LearnerCnxAcceptor 接收器用于负责接收所有非 Leader 服务器的连接请求。

  3. Learner 服务器开始和 Leader 建立连接。

    所有的 Learner 服务器在启动完毕后,会从 Leader 选举的投票结果中找到当前集群中的 Leader 服务器,然后与其建立连接。

  4. Leader 服务器创建 LearnerHandler。

    Leader 接收到来自其他机器的连接创建请求后,会创建一个 LearnerHandler 实例。每个 LearnerHandler 实例对应一个 Leader 与 Learner 服务器之间的连接,其负责 Leader 和 Learner 服务器之间几乎所有的消息通信和数据同步。

  5. 向 Leader 注册。

    当和 Leader 建立起连接后,Learner 就会开始向 Leader 进行注册——所谓的注册,其实就是将 Learner 服务器自己的基本信息发送给 Leader 服务器,我们称之为 LearnerInfo,包括当前服务器的 SID 和服务器处理的最新的 ZXID。

  6. Leader 解析 Learner 信息,计算新的 epoch。

    Leader 服务器在接收到 Learner 的基本信息后,会解析出该 Learner 的 SID 和 ZXID,然后根据该 Learner 的 ZXID 解析出其对应的 epoch_of_learner,和当前 Leader 服务器的 epoch_of_leader 进行比较,如果该 learner 的 epoch 更大的话,那么更新 Leader 的 epoch: epoch_of_leader = epoch_of_learner + 1 ,然后,LearnerHandler 会进行等待,直到过半的 Learner 已经向 Leader 进行了注册,同时更新了 epoch_of_leader 之后,Leader 就可以确定当前集群的 epoch 了。

  7. 发送 Leader 状态。

    计算出新的 epoch 之后,Leader 会将该信息以一个 LEADERINFO 消息的形式发送给 Learner,同时等待该 Learner 的响应。

  8. Learner 发送 ACK 消息。

    Follower 在接收到来自 Leader 的 LEADERINFO 消息后,会解析出 epoch 和 ZXID,然后向 Leader 反馈一个 ACKEPOCH 响应。

  9. 数据同步。

    Leader 服务器接收到 Learner 的这个 ACK 消息后,就可以与其进行数据同步了。

  10. 启动 Leader 和 Learner 服务器。

    当有过半的 Learner 已经完成了数据同步,那么 Leader 和 Learner 服务器实例就可以开始启动了。

Leader 和 Follower 启动

  1. 创建并启动会话管理器。

  2. 初始化 Zookeeper 的请求处理链。

    和单机版服务器一样,集群模式下,每个服务器都会在启动阶段串联请求处理链,只是根据服务器角色不同,会有不同的请求处理链路。

  3. 注册 JMX 服务。

HashMap (JAVA8版)

前言

回想之前几次的面试,没有一次不问到 hashmap 的,这也体现了 hashmap 的重要性了。记得当时的回答是底层是数组加链表的方式实现的,然后就是什么 get 时候怎么查找的。现在想想这些小白都知道的东西说出来也加不了分啊。现在挤点时间出来看看源码吧。

底层实现简介

数组加链表这个没什么好说的,看下面这个图就能明白了(java8当中当链表达到一定长度就会转换成红黑树,这个之后再说)。还是从源码来看吧,这里时间问题不可能每个方法都拿出来讲了,挑几个重要的方法来说。

HashMap(int, float)

第一个参数是容量默认为16,第二个参数是负载因子默认是0.75。源码如下:

public HashMap(int initialCapacity, float loadFactor) {
	if (initialCapacity < 0)		//初始化容量总不能小于0吧
	    throw new IllegalArgumentException("Illegal initial capacity: " +
                          initialCapacity);
	if (initialCapacity > MAXIMUM_CAPACITY)		//最大值为1<<30
		initialCapacity = MAXIMUM_CAPACITY;
	if (loadFactor <= 0 || Float.isNaN(loadFactor))
		throw new IllegalArgumentException("Illegal load factor: " +
                          loadFactor);
	this.loadFactor = loadFactor;
	this.threshold = tableSizeFor(initialCapacity);
}

直接看 tableSizeFor(initialCapacity) 这个方法,由于 hashmap 的容量总是2的幂,所以这个方法就是找到大于等于 initialCapacity 的最小的2的幂

/**
 * Returns a power of two size for the given target capacity.
 */
static final int tableSizeFor(int cap) {		//例如cap = 10
	int n = cap - 1;
    n |= n >>> 1;	//n = n|n无符号右移1位	1001|0100 = 1101
    n |= n >>> 2; 	//n = n|n无符号右移2位	1101|0011 = 1111
    n |= n >>> 4;	//n = n|n无符号右移2位	1111|0000 = 1111
    n |= n >>> 8;	//n = n|n无符号右移2位	1111|0000 = 1111
    n |= n >>> 16;	//n = n|n无符号右移2位	1111|0000 = 1111
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;	//返回16
}

tableSizeFor 方法为什么这么设计呢, 我们假设假设n只有最高位是1后面低位全是0,和无符号右移1位相或,得到最高位、次高位是1,后面低位均是0。再与无符号右移2位相或,得到高四位是1后面均是0。下面同理,或上无符号右移4位,得到高8位是1。或上无符号右移8位,得到高16位为1。或上无符号右移16位,得到32位全是1。此时已经大于 MAXIMUM_CAPACITY 了,没必要继续了。返回 MAXIMUM_CAPACITY 即可。这是在cap <= MAXIMUM_CAPACITY 最极致的情况了。

tableSizeFor 方法返回值赋值给了 threshold ?为什么不是下面这样了

this.threshold = tableSizeFor(initialCapacity) * this.loadFactor;

在这个构造方法中并没有对 table 数组初始化,初始化是第一次 put 操作的时候进行的,到时候会对 threshold 重新进行计算,这个不用我们担心😓。

put(K, V)

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);	//为了hash碰撞的概率
}
/**
 * Implements Map.put and related methods
 *
 * @param hash hash for key
 * @param key the key
 * @param value the value to put
 * @param onlyIfAbsent if true, don't change existing value
 * @param evict if false, the table is in creation mode.
 * @return previous value, or null if none
 */	
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
		//第一次put操作的时候会resize,稍后再看
        n = (tab = resize()).length;
    //当前index在数组中的位置的链表头节点空的话则直接创建一个新节点
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        //key相等且hash也相等,则是覆盖value操作
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)		//红黑树之后再说
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {		//则在链表尾部插入一个新节点
                    p.next = newNode(hash, key, value, null);
                    //如果追加节点后,链表长度>=8,则转化为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //遍历链表的过程中也可能存在hash和key都相等的情况
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                	break;
                //把e赋给p就是让链表一步一步往下走
                p = e;
            }
        }
        //e不为null则代表将要被覆盖value的那个节点
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            // onlyIfAbsent为true的话则不改变已经存在的value(不包括null噢)
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            //这个在linkedhashmap中实现
            afterNodeAccess(e);
            return oldValue;
        }
    }
    //这里代表插入了一个新的链表节点,修改modCount,返回null
    ++modCount;
    //更新hashmap的size,并判断是否需要resize
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

resize( )

这个方法会在初始化和 size 超过 threshold 的时候执行。

/**
 * Initializes or doubles table size.  If null, allocates in
 * accord with initial capacity target held in field threshold.
 * Otherwise, because we are using power-of-two expansion, the
 * elements from each bin must either stay at same index, or move
 * with a power of two offset in the new table.
 *
 * @return the table
 */
final Node<K,V>[] resize() {
	Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //在hashmap(int, float)构造方法中已经将 tableSizeFor 的返回值赋值给了 threshold
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
    	if (oldCap >= MAXIMUM_CAPACITY) {
            //容量已达上限,则设置阈值是2的31次方-1,同时不再扩容
        	threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //未达上限则扩容一倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
        	oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    //用上面的 tableSizeFor 返回值作为初始容量
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    //代表是没有容量/阈值参数的情况
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;	//16
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);	//0.75*16
    }
    //对应的是上面几行那个else if(oldThr > 0)的情况
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        //对阀值越界情况修复为 Integer.MAX_VALUE
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
  	//构造新的table数组
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    //将扩容前哈希桶中的元素移到扩容后的哈希桶
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                //不能有了老婆忘了娘吧,这里给原来的置为null
                oldTab[j] = null;
                if (e.next == null)
                    //要时刻记得index = hash & (table.length-1)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                //当前链表上不止一个节点
                else { // preserve order
                    //因为扩容是翻倍的,所以原链表上的每个节点,在扩容后的哈希桶中,要么还在原来下标位(low位),要么在旧下标+旧容量(high位)
                    //low位头节点、尾节点
                    Node<K,V> loHead = null, loTail = null;
                    //high位头节点、尾节点
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        //这里就很关键了,靠 e.hash & oldCap 来区分low位还是high位,稍后详细说下
                        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;
                    }
                    //可以看出low位和high位差一个旧容量
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

简单说下上面怎么区分原来链表的节点是应该放在 low 位还是 high 位的,假设一个 key 的 hash 为 00001100,哈希桶的容量是16,也就是 00010000。那么 00001100 & 00001111 = 00001100,也就是角标为12的桶中,而另一个 key 的 hash 为00011100,00011100 & 00001111 = 00001100,那么它也在角标为12的桶中。但是这两个 key 的 hash 分别和 00010000 相与,结果刚好差了一个旧容量的大小。也就是根据 key 的 hash 和旧容量为1的那位对应的是0还是1,是0的话就放在 low 位,是1就放在 high 位。

get(Object)

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        //根据下标检查第一个节点是不是我们要找的
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        //不是的话则遍历链表
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

红黑树相关方法

红黑树相关知识网上有很多不错的讲解,推荐一个github地址,感兴趣可以去看一下,教你透彻了解红黑树

先来 TreeNode 有什么属性吧

TreeNode<K,V> parent;  // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev;    // needed to unlink next upon deletion
boolean red;

记得之前在 putVal( ) 方法中,如果追加节点后链表长度>=8就会转换为树。其中 treeifyBin(tab, hash) 方法如下

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            //根据e节点构建一个新的treeNode
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

现在 hd 就是之前链表头节点转换成的 treeNode,它的 next 指向下一个 treeNode,并且这个 treeNode 的 prev 指向 hd,后面同理。然后来看下 hd.treeify(tab)

final void treeify(Node<K,V>[] tab) {
    TreeNode<K,V> root = null;
    for (TreeNode<K,V> x = this, next; x != null; x = next) {
    next = (TreeNode<K,V>)x.next;
        x.left = x.right = null;
        //此处执行,就是把hd作为根节点,设置red为false
        if (root == null) {
            x.parent = null;
            x.red = false;
            root = x;
        }
        else {
            K k = x.key;
            int h = x.hash;
            Class<?> kc = null;
            //往树中插入当前节点
            for (TreeNode<K,V> p = root;;) {
                int dir, ph;
                K pk = p.key;
                //根据当前节点的hash与root的hash大小区别开
                if ((ph = p.hash) > h)
                    dir = -1;
                else if (ph < h)
                    dir = 1;
                else if ((kc == null &&
                            //hash相同则判断key是否实现了Comparable接口
                            (kc = comparableClassFor(k)) == null) ||
                            //实现了Comparable接口则直接比较
                            (dir = compareComparables(kc, k, pk)) == 0)
                    //否则调用System.identityHashCode比较,这里就不研究了
                    dir = tieBreakOrder(k, pk);
                TreeNode<K,V> xp = p;
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    //把当前节点parent指向root
                    x.parent = xp;
                    //如果dir<=0,也就是当前节点的hash<=root的hash,把root的left指向当前节点
                    if (dir <= 0)
                        xp.left = x;
                    //如果dir>0,也就是当前节点的hash>root的hash,把root的right指向当前节点
                    else
                        xp.right = x;
                    //此处进行插入平衡操作,重新指定root,稍后再说
                    root = balanceInsertion(root, x);
                    break;
                }
            }
        }
    }
    //恢复root的位置,即起点作为根节点
    moveRootToFront(tab, root);
}

接着看下 balanceInsertion(root, x)怎么重新指定root的

static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
                                                    TreeNode<K,V> x) {
    x.red = true;
    for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
        //如果当前节点父节点为null,则令其为黑色,直接返回当前节点
        if ((xp = x.parent) == null) {
            x.red = false;
            return x;
        }
        //如果当前节点父节点为黑色,或者祖父节点为null,直接返回root
        else if (!xp.red || (xpp = xp.parent) == null)
            return root;
        //如果当前节点的父节点是祖父节点的左孩子
        if (xp == (xppl = xpp.left)) {
            //如果当前节点的祖父节点的右孩子不为null并且为红色,则令其为黑色
            //并且令父节点为黑色,令祖父节点为红色,把祖父节点设置为当前节点
            if ((xppr = xpp.right) != null && xppr.red) {
                xppr.red = false;
                xp.red = false;
                xpp.red = true;
                x = xpp;
            }
            else {
                //如果当前节点是父节点的右孩子
                if (x == xp.right) {
                    //左旋
                    root = rotateLeft(root, x = xp);
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                if (xp != null) {
                    xp.red = false;
                    if (xpp != null) {
                        xpp.red = true;
                        //右旋
                        root = rotateRight(root, xpp);
                    }
                }
            }
        }
        else {
            //祖父节点左孩子不为null且为红色
            if (xppl != null && xppl.red) {
                xppl.red = false;
                xp.red = false;
                xpp.red = true;
                x = xpp;
            }
            else {
                if (x == xp.left) {
                    root = rotateRight(root, x = xp);
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                if (xp != null) {
                    xp.red = false;
                    if (xpp != null) {
                        xpp.red = true;
                        root = rotateLeft(root, xpp);
                    }
                }
            }
        }
    }
}

上面这个方法看不懂的话主要还是红黑树没有理解。建议先理解下红黑树。移除修复操作大同小异,这里就不做记录了。

其他新增方法

  • 带有默认值的get

    @Override
    public V getOrDefault(Object key, V defaultValue) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
    }
  • 存在则不覆盖的put,调用putVal( )

    @Override
    public V putIfAbsent(K key, V value) {
        return putVal(hash(key), key, value, true, true);
    }
  • 根据key和value移除

    @Override
    public boolean remove(Object key, Object value) {
        return removeNode(hash(key), key, value, true, true) != null;
    }

还有一些其它的方法没有说到,感兴趣的可以去看下源码。

LinkedHashMap

顺便简单说下 LinkedHashMap 吧,与 HashMap 最主要的区别是,其内部维护的双向链表,可以按照插入顺序或者最近访问顺序遍历取值。

public LinkedHashMap() {
    super();
    //false则按插入顺序,true则按最近访问顺序
    accessOrder = false;
}
public LinkedHashMap(int initialCapacity,
						float loadFactor,
                        boolean accessOrder) {
	super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}

其中用 Entry<K, V> 继承了 HashMap.Node<K, V>,通过 before 和 after 实现了双向链表

static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

get(Object)

public V get(Object key) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) == null)
        return null;
    if (accessOrder)    //按照最近访问顺序
        afterNodeAccess(e);
    return e.value;
}
void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMap.Entry<K,V> last;
    //如果accessOrder为true且e不是tail
    if (accessOrder && (last = tail) != e) {
        //让p指向当前e节点,且让b指向前节点,a指向后节点
        LinkedHashMap.Entry<K,V> p =
        				(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        //让p(指向e)的after为null,后面会让tail指向它
        p.after = null;
        //如果前节点b为null,则让head指向后节点a
        if (b == null)
            head = a;
        //否则前节点b的after指向后节点a
        else
            b.after = a;
        //如果后节点a不为null,则让a的before指向前节点b
        if (a != null)
            a.before = b;
        //否则,让last指向b
        else
            last = b;
        //如果last为null,则让head指向p
        if (last == null)
            head = p;
        //否则,让p的before指向last,last的after指向p
        else {
            p.before = last;
            last.after = p;
        }
        //让tail指向p
        tail = p;
        ++modCount;
    }
}

如果 accessOrder 为 true,上面的操作就是调整 get 的那个节点e至尾部,且修复之前e节点的前节点的 after 指针,后节点的 before 指针的指向。

至于 HashMap 预留给 LinkedHashMap 的 afterNodeAccess()、afterNodeInsertion() 、afterNodeRemoval() 方法 平时不怎么用到,这里不做介绍了。

InnoDB undo log

众所周知,InnoDB 存储引擎有一个特点:支持事务。简单来说,事务就是一组要么都做,要么都不做的操作。那么在一个事务执行过程中,遇到异常或手动回滚时,怎么撤销刚刚执行的操作?这就要通过本篇文章要说的 undo log 来实现了。

undo 日志类型

INSERT 操作对应的 undo 日志

INSERT 操作对应的 undo 日志类型为 TRX_UNDO_INSERT_REC ,存储 undo type、undo no、table id、主键各列信息<len,value>列表等。其中 len 表示主键列占用的存储空间(例如 INT 类型占用的存储空间长度为 4 个字节),value 表示主键列的实际值。

我们在往一个表里插入一条记录的时候,不仅仅会往聚簇索引中插入记录,二级索引中也是需要插入记录的。我们在回滚操作时,根据这条记录的主键信息在聚簇索引上做删除操作,连带着在二级索引上做删除操作(因为聚簇索引和二级索引记录是一一对应的)。

还记得每条记录都会有三个隐藏列(row_id、trx_id、roll_pointer)么?其中这个 roll_pointer 就是指向 undo log 的指针。当插入一条记录时,生成一个 TRX_UNDO_INSERT_REC 类型的 undo log,同时插入的这条记录的 roll_pointer 指向该 undo log。

DELETE 操作对应的 undo 日志

之前介绍数据页结构的时候说过,数据页中的记录会根据记录头信息中的 next_record 属性组成一个单向链表。当删除记录的时候,这些记录也会根据记录头信息中的 next_record 属性组成一个单向链表,我们称之为垃圾链表。Page Header 部分有一个称之为 PAGE_FREE 的属性,它指向该垃圾链表中的头节点。

当我们用 delete 删除一条记录的时候,其实分为两个阶段:

  1. delete mark 阶段

    仅仅将记录的 delete_mark 标识位设置为 1,不会将该记录移动到垃圾链表。在删除语句所在的事务提交之前,被删除的记录一直都处于这种状态。

  2. purge 阶段

    当该删除语句所在的事务提交之后,会有专门的线程( purge thread )来真正的把记录删除掉。真正的删除也就是把记录移动到垃圾链表(头插法,所以会涉及到 PAGE_FREE 的修改),并且修改诸如页面用户记录数量、上次插入记录位置等信息。

由于阶段二是在事务提交后执行的,所以不考虑了。这里只考虑对阶段一所做操作进行回滚。InnoDB 设计了一种 TRX_UNDO_DEL_MARK_REC 类型的 undo 日志。它存储 undo type、undo no、table id、old trx_id、old roll_pointer、主键各列信息<len,value>列表、索引列各列信息<pos, len, value>列表等信息。

TRX_UNDO_INSERT_REC 相比,多了几个属性:

  • old trx_id:旧事务id
  • old roll_pointer:旧回滚指针
  • 索引列各列信息:这部分信息主要是用在事务提交后 purge 阶段中使用的

想象一下:当我们插入一条记录后,该记录的 roll_pointer 指向 TRX_UNDO_INSERT_REC 类型的 undo log(这里我们称之为 insert undo log),这时我们用 delete 删除该记录(事务未提交,执行阶段一),生成一条 TRX_UNDO_DEL_MARK_REC 类型的 undo log(这里我们称之为 delete undo log)。此时该记录仍存在,且 roll_pointer 指向 delete undo log,delete undo log 的 old roll_pointer 指向 insert undo log

UPDATE 操作对应的 undo 日志

在使用 update 更新数据时,可以分为不更新主键和更新主键两种情况

不更新主键的情况

在不更新主键的情况,又可以分为被更新的列占用的存储空间不发生变化和发生变化两种情况

  • 不发生变化

    对于被更新的每个列来说,如果更新前后占用存储空间不变,那么就可以直接在原记录上直接修改。

  • 发生变化

    如果有任意一个被更新的列,更新前后占用存储空间发生了变化,那么就不能直接在原记录上直接修改。而是需要先在聚簇索引中将该记录删除,再根据更新后的值创建一条新的记录插入。

    注意一下,这里所说的删除不是上面说的 delete mark,而是由用户线程同步执行的真正删除。

针对不更新主键的情况,InnoDB 设计了一种类型为 TRX_UNDO_UPD_EXIST_REC 的 undo 日志,它存储 undo type、undo no、table id、old trx_id、old roll_pointer、主键各列信息<len,value>列表、n_updated、被更新列更新前信息<pos, old_len, old_value>列表、索引列各列信息<pos, len, value>列表等信息。

TRX_UNDO_DEL_MARK_REC 相比,多了几个属性:

  • n_updated:有多少个列被更新
  • 被更新列更新前信息
  • 如果更新的列包含索引列,那么也会添加 索引列各列信息 这个部分,否则的话是不会添加这个部分的。

更新主键的情况

在 update 更新主键这种情况下,InnoDB 分两步执行:

  1. 将旧记录进行 delete mark 操作

    就是说在 update 语句所在事务提交前,不会把旧记录移动到垃圾链表。因为此时别的事务可能还需要访问到该记录,如果这里真正删了旧记录,别的事务就访问不到了。也就是 MVCC 相关的内容了,后面会说到。

  2. 根据更新后各列的值创建一条新记录,重新定位并将其插入到聚簇索引中

针对 update 语句更新记录主键值的这种情况:

  • 在对该记录进行 delete mark 操作前,会记录一条类型为 TRX_UNDO_DEL_MARK_REC 的 undo日志;

  • 之后插入新记录时,会记录一条类型为 TRX_UNDO_INSERT_REC 的 undo日志,也就是说每对一条记录的主键值做改动时,会记录 2 条 undo日志。

FIL_PAGE_UNDO_LOG 页面

正如 FIL_PAGE_INDEX 类型的页是用来存储索引的, 这里要说的 FIL_PAGE_UNDO_LOG 类型的页面(后文称 undo 页面)是用来专门存储 undo 日志的,除了 File Header 和 File Trailer 这种页面通用的部分,Undo Page Header 是 undo 页面所特有的,含有以下属性:

  • TRX_UNDO_PAGE_TYPE:该页面是用来存储什么类型的 undo 日志

    可选值有两个:TRX_UNDO_INSERT(TRX_UNDO_INSERT_REC 类型属于该类) 和 TRX_UNDO_UPDATE(除 TRX_UNDO_INSERT_REC 类型以外的都属于该类);之所以把 undo 日志分成两个大类,是因为类型为 TRX_UNDO_INSERT_REC 的 undo 日志在事务提交后可以直接删除掉,而其他类型的 undo 日志还需要为所谓的 MVCC 服务,不能直接删除掉,对它们的处理需要区别对待。

  • TRX_UNDO_PAGE_START:表示在当前页面中是从什么位置开始存储 undo 日志的

    或者说表示第一条 undo 日志在本页面中的起始偏移量。

  • TRX_UNDO_PAGE_FREE:表示当前页面中存储的最后一条 undo 日志结束时的偏移量

    或者说从这个位置开始,可以继续写入新的 undo 日志。

  • TRX_UNDO_PAGE_NODE:一个链表节点包含一下几个部分:

    • List Length 表明该链表一共有多少节点。
    • First Node Page Number 和 First Node Offset 的组合就是指向链表头节点的指针。
    • Last Node Page Number 和 Last Node Offset 的组合就是指向链表尾节点的指针。

undo 页面链表

单个事务的 undo 页面链表

在一个事务执行过程中可能产生很多 undo 日志,这些日志可能一个页面放不下,需要放到多个页面中,这些页面就通过我们上边介绍的 TRX_UNDO_PAGE_NODE 属性连成了链表;为事务分配链表策略如下:

  • 刚刚开启事务时,一个 undo 页面链表也不分配
  • 事务执行过程中向普通表中插入记录或者执行更新记录主键的操作之后,分配一个普通表的 insert undo 链表
  • 事务执行过程中删除或者更新了普通表中的记录之后,分配一个普通表的 update undo 链表
  • 事务执行过程中向临时表中插入记录或者执行更新记录主键的操作之后,分配一个临时表的 insert undo 链表
  • 事务执行过程中删除或者更新了临时表中的记录之后,就会为其分配一个临时表的 update undo 链表。

多个事务的 undo 页面链表

为了尽可能提高 undo 日志的写入效率,不同事务执行过程中产生的 undo 日志需要被写入到不同的 undo 页面链表中。

undo 日志写入过程

Segment(段)

段是一个逻辑概念,简单来说就是一些零散的页面和一些完整的区(每64个页作为一个区)组成的。

每一个段对应一个 INODE Entry 结构,这个 INODE Entry 结构描述了这个段的各种信息,比如段的 ID,段内的各种链表基节点,零散页面的页号有哪些等信息。为了定位到一个 INODE Entry,InnoDB 设计了一个 Segment Header (10个字节)的结构:

  • Space ID of the INODE Entry:INODE Entry 结构所在的表空间ID
  • Page Number of the INODE Entry:INODE Entry 结构所在的页面页号
  • Byte Offset of the INODE Entry:INODE Entry 结构在该页面中的偏移量

有了 Segment Header 的表空间ID、页面号、偏移量等信息,就可以很容易定位到对应的 INODE Entry 了。

Undo Log Segment Header

InnoDB 规定每一个 undo 页面链表都对应着一个段,称之为 Undo Log Segment。也就是说链表中的页面都是从这个段里边申请的,所以他们在 undo 页面链表的第一个页面(first undo page)中设计了一个称之为 Undo Log Segment Header 的部分,这个部分中包含了该链表对应的段的 segment header 信息以及其他的一些关于这个段的信息。

也就是说 undo 页面链表第一个页面比其他普通 undo 页面多个 Undo Log Segment Header 部分,结构如下:

  • TRX_UNDO_STATE:本 undo 页面链表处在什么状态。一个 Undo Log Segment 可能处在的状态包括:

    • TRX_UNDO_ACTIVE:活跃状态,也就是一个活跃的事务正在往这个段里边写入 undo 日志。

    • TRX_UNDO_CACHED:被缓存的状态。处在该状态的 undo 页面链表等待着之后被其他事务重用。

    • TRX_UNDO_TO_FREE:对于 insert undo 链表来说,如果在它对应的事务提交之后,该链表不能被重用,那么就会处于这种状态。

    • TRX_UNDO_TO_PURGE:对于 update undo 链表来说,如果在它对应的事务提交之后,该链表不能被重用,那么就会处于这种状态。

    • TRX_UNDO_PREPARED:包含处于 PREPARE 阶段的事务产生的 undo 日志。

  • TRX_UNDO_LAST_LOG:本 undo 页面链表中最后一个 Undo Log Header 的位置。

  • TRX_UNDO_FSEG_HEADER:本 undo 页面链表对应的段的 Segment Header 信息(通过这个信息可以找到该段对应的 INODE Entry)。

  • TRX_UNDO_PAGE_LIST:Undo页面链表的基节点。

前面说过,undo 页面的 Undo Page Header 部分有个属性 TRX_UNDO_PAGE_NODE,undo 页面通过这个属性连接成一个链表。这个 TRX_UNDO_PAGE_LIST 属性代表着这个链表的基节点,当然这个基节点只存在于 undo 页面链表的第一个页面中。

###Undo Log Header

我们把同一个事务向一个 undo 页面链表中写入的 undo日志算是一个组,例如事务 A 需要分配两个 undo 页面链表,就会写入两个组的 undo 日志。在每写入一组 undo 日志前,会先记录关于这个组的一些信息。这些信息存储在 Undo Log Header 中。

也就是说,undo 页面链表的第一个页面在真正写入 undo 日志前,其实都会被填充 Undo Page Header、Undo Log Segment Header、Undo Log Header 这3个部分,Undo Log Header 结构如下:

  • TRX_UNDO_TRX_ID:生成本组 undo 日志的事务 id。

  • TRX_UNDO_TRX_NO:事务提交后生成的一个需要序号,使用此序号来标记事务的提交顺序(先提交的此序号小,后提交的此序号大)。

  • TRX_UNDO_DEL_MARKS:标记本组 undo 日志中是否包含由于 delete mark 操作产生的 undo 日志。

  • TRX_UNDO_LOG_START:表示本组 undo 日志中第一条 undo 日志的在页面中的偏移量。

  • TRX_UNDO_XID_EXISTS:本组 undo 日志是否包含 XID 信息。

  • TRX_UNDO_DICT_TRANS:标记本组 undo 日志是不是由 DDL 语句产生的。

  • TRX_UNDO_TABLE_ID:如果 TRX_UNDO_DICT_TRANS 为真,那么本属性表示 DDL 语句操作的表的 table id。

  • TRX_UNDO_NEXT_LOG:下一组的 undo 日志在页面中开始的偏移量。

  • TRX_UNDO_PREV_LOG:上一组的 undo 日志在页面中开始的偏移量。

  • TRX_UNDO_HISTORY_NODE:一个12字节的 List Node 结构,代表一个称之为 History 链表的节点。

一般来说,一个 undo 页面链表只存储一个事务执行过程中产生的一组 undo 日志,但是在某些情况下,可能会在一个事务提交之后,之后开启的事务重复利用这个 undo 页面链表,这样就会导致一个 undo 页面中可能存放多组 undo 日志,TRX_UNDO_NEXT_LOG 和 TRX_UNDO_PREV_LOG 就是用来标记下一组和上一组undo 日志在页面中的偏移量的。

小结

对于没有重用的 undo 页面链表来说,链表的第一个页面在写入 undo 日志前,会填充 Undo Page Header、Undo Log Segment Header、Undo Log Header 这3个部分;后面的其他 undo 页面在写入 undo 日志前,只会填充 Undo Page Header。

链表的 List Base Node 存放到 undo 页面链表的第一个页面的 Undo Log Segment Header 部分,List Node 信息存放到每一个 undo 页面的 Undo Page Header 部分。

重用 undo 页面

前面说到为了提高多个事务并发写入 undo 日志的性能,InnoDB 会为每个事务分配单独的 undo 页面链表(最多四个)。但是大部分情况下,一个事务可能只修改了一条或几条记录,也就是产生了很少的 undo 日志,如果为这些事务都创建单独的 undo 页面链表(可能只有一个 undo 页面)来存储很少的 undo 日志的话,有点浪费了好像。所以 InnoDB 规定在事务提交后,某些情况下该事务的 undo 页面可以被其它事务重用。能否被重用的条件如下:

  • 该链表中只有一个 undo 页面
  • 该 undo 页面的使用量小于 3/4

前面说到,undo 页面链表分为 insert undo 链表和 update undo 链表,它们在重用时的规则也是不一样的:

  • insert undo 链表

    insert undo 链表中只存储类型为 TRX_UNDO_INSERT_REC 的 undo 日志,这种类型的 undo 日志在事务提交后就没有用了,如果符合上述两个条件的话,新事务重用该 undo 页面时可以直接覆盖写入。

  • update undo 链表

    对于 update undo 链表中的 undo 日志,在事务提交后可能还会有用(MVCC相关),所以不能像上面说的那样直接覆盖写入,这样就发生了一个 undo 页面中写入了多组 undo 日志的情况。

Rollback Segment(回滚段)

一个事务最多会分配 4 个 undo 页面链表,所以同一时刻可能会有很多的 undo 页面链表。InnoDB 为了更好的管理这些链表,用 Rollback Segment Header 页面来进行管理:存储每个 undo 页面链表的第一个页面的页号

每一个 Rollback Segment Header 页面都对应着一个段,这个段就称为 Rollback Segment(回滚段)。与我们之前介绍的各种段不同的是,这个 Rollback Segment 里其实只有一个页面。Rollback Segment Header 页面存储内容如下:

  • TRX_RSEG_MAX_SIZE:管理的所有 undo 页面链表中的 undo 页面数量之和的最大值。

    换句话说,本 Rollback Segment 中所有 undo 页面链表中的 undo 页面数量之和不能超过 TRX_RSEG_MAX_SIZE 代表的值。

  • TRX_RSEG_HISTORY_SIZE:History 链表占用的页面数量。

  • TRX_RSEG_HISTORY:History 链表的基节点。

  • TRX_RSEG_FSEG_HEADER:本 Rollback Segment 对应的10字节大小的 Segment Header 结构,通过它可以找到本段对应的 INODE Entry。

  • TRX_RSEG_UNDO_SLOTS:各个 undo 页面链表的第一个页面的页号集合,也就是 undo slot 集合。

    一个页号占用 4 个字节,对于 16KB 大小的页面来说,这个 TRX_RSEG_UNDO_SLOTS 部分共存储了 1024 个 undo slot,所以共需1024 × 4 = 4096个字节。

申请 undo 页面链表

对于一个 Rollback Segment Header 页面来说,初始情况下,各个 undo slot 都是 FIL_NULL,表示不指向任何页面。当需要为事务分配 undo 页面链表的时候,从第一个 undo slot 开始,判断是不是 FIL_NULL:

  • 如果是 FIL_NULL,那么在表空间中新创建一个段(也就是 Undo Log Segment),然后从段里申请一个页面作为 undo 页面链表的第一个页面,然后把该 undo slot 的值设置为刚刚申请的这个页面的页号,这样也就意味着这个 undo slot 被分配给了这个事务。
  • 如果不是 FIL_NULL,说明该 undo slot 已经指向了一个 undo 页面链表,那就跳到下一个 undo slot,判断该 undo slot 的值是不是 FIL_NULL,重复上边的步骤。

如果这 1024 个 undo slot 的值都不为 FIL_NULL,那么当前需要分配链表的事务就会申请不到,报错如下:

Too many active concurrent transactions

当一个事务提交后,对应的 undo slot 怎么处理呢?规则如下:

  • 如果该 undo slot 对应的 undo 页面链表符合被重用的条件

    该 undo slot 就处于被缓存的状态,undo 页面的 Undo Log Segment Header 部分的 TRX_UNDO_STATE 属性被设置为 TRX_UNDO_CACHED;被缓存的 undo slot 会被加入到一个链表: insert undo cached 链表/update undo cached 链表;

    一个回滚段就对应着上述两个 cached 链表,如果有新事务要分配 undo slot 时,先从对应的 cached 链表中找。如果没有被缓存的 undo slot,才会到回滚段的 Rollback Segment Header 页面中再去找。

  • 如果该 undo slot 对应的 undo 页面链表不符合被重用的条件

    • 如果对应的 undo 页面链表是 insert undo 链表

      则该 undo 页面链表的 TRX_UNDO_STATE 属性会被设置为 TRX_UNDO_TO_FREE;之后该 undo 页面链表对应的段会被释放掉;然后把该 undo slot 的值设置为 FIL_NULL。

    • 如果对应的 undo 页面链表是 update undo 链表

      则该 undo 页面链表的 TRX_UNDO_STATE 属性会被设置为 TRX_UNDO_TO_PRUGE;将该 undo slot 的值设置为 FIL_NULL;然后将本次事务写入的一组 undo 日志放到所谓的 History链表中(需要注意的是,这里并不会将 undo 页面链表对应的段给释放掉,因为这些 undo 日志还有用呢~)。

多个回滚段

假设一个读写事务执行过程中只分配 1 个 undo 页面链表,那 1024 个 undo slot 也只能支持 1024 个读写事务同时执行,再多了就崩溃了。所以 InnoDB 定义了 128 个回滚段。

每个回滚段都对应着一个 Rollback Segment Header 页面,有 128个 回滚段,自然就要有 128 个 Rollback Segment Header 页面,那么存哪呢?在系统表空间的第 5 号页面的某个区域包含了 128 个 8 字节大小的格子,
每个格子存着 Space ID 和 Page number,对应着一个 Rollback Segment Header 页面。

  • 第 0 号、第 33~127 号回滚段属于一类。其中第 0 号回滚段必须在系统表空间中,第 33~127 号回滚段既可以在系统表空间中,也可以在自己配置的 undo 表空间中

  • 第 1~32 号回滚段属于一类。这些回滚段必须在临时表空间(对应着数据目录中的ibtmp1文件)中。

针对普通表和临时表划分不同种类的回滚段的原因:在修改针对普通表的回滚段中的 undo 页面时,需要记录对应的 redo 日志,而修改针对临时表的回滚段中的 undo 页面时,不需要记录对应的 redo 日志。

分配 undo 页面链表过程

  1. 事务在执行过程中对普通表的记录首次做改动之前,首先会到系统表空间的第 5 号页面中分配一个回滚段(其实就是获取一个 Rollback Segment Header 页面的地址)。

  2. 在分配到回滚段后,首先看一下这个回滚段的两个 cached 链表有没有已经缓存了的 undo slot,比如如果事务做的是 INSERT 操作,就去回滚段对应的 insert undo cached 链表中看看有没有缓存的 undo slot;
    如果事务做的是 DELETE 操作,就去回滚段对应的 update undo cached 链表中看看有没有缓存的 undo slot。如果有缓存的 undo slot,那么就把这个缓存的 undo slot 分配给该事务。

  3. 如果没有缓存的 undo slot 可供分配,那么就要到 Rollback Segment Header 页面中找一个可用的 undo slot 分配给当前事务。

  4. 找到可用的 undo slot 后,如果该 undo slot 是从 cached 链表中获取的,那么它对应的 Undo Log Segment 已经分配了,否则的话需要重新分配一个 Undo Log Segment,然后从该 Undo Log Segment 中申请一个页面作为 undo 页面链表的 first undo page。

  5. 然后事务就可以把 undo 日志写入到上边申请的 undo 页面链表了。

回滚段相关配置

  • innodb_rollback_segments:配置回滚段的数量,范围1-128

    固定有 32 个针对临时表的可用回滚段。值大于 33 时,针对普通表的回滚段数量为 innodb_rollback_segments - 32

  • innodb_undo_directory:指定 undo 表空间所在的目录,默认在数据目录

  • innodb_undo_tablespaces:指定 undo 表空间的数量

    第 33~127 号回滚段可以平均分布到不同的 undo 表空间中

MVCC 简介

事务并发可能遇到的问题

  1. 脏读:事务读取到了另一个还未提交的事务修改的数据
  2. 不可重复读:事务连续读取同一条记录数据不一致,因为期间数据被另一个事务修改了
  3. 幻读:事务连续两次读取数据,第二次读取比第一次读取多了些数据,因为期间另一个事务插入了数据

事务隔离级别

  1. READ UNCOMMITTED:可能发生脏读、不可重复读和幻读
  2. READ COMMITTED:可能发生不可重复读和幻读
  3. REPEATABLE READ:可能发生幻读(InnoDB 使用 mvcc 和 next-key lock 解决该问题
  4. SERIALIZABLE:通过加锁方式读取记录,不会发生上述问题

ReadView

前面说到记录的回滚指针 roll_pointer 指向 undo log,会形成一个版本链,事务在读取记录时在不同的隔离级别下可能会看到不同的记录版本。这个功能通过 ReadView 来实现。

ReadView 主要包含以下几个部分:
m_ids:生成 ReadView 时系统中活跃的事务 id 集合
min_trx_id:生成 ReadView 时系统中活跃的最小事务 id,也就是 m_ids 中的最小值
max_trx_id:生成 ReadView 时系统中应该分配给下一个事务的 id 值,比 m_ids 的最大值要大
creator_trx_id:生成该 ReadView 的事务 id

由于 READ UNCOMMITTED 级别下每次读取最新记录,SERIALIZABLE 级别下通过加锁访问数据,
所以 ReadView 仅对 READ COMMITTED 和 REPEATABLE READ 有效。

怎么通过这个 ReadView 来判别当前事务能看到的版本呢?过程如下:

  1. 如果被访问版本的 trx_id 与 ReadView 中的 creator_trx_id 值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
  2. 如果被访问版本的 trx_id 小于 ReadView 中的 min_trx_id 值,表明生成该版本的事务在当前事务生成 ReadView 前已经提交,所以该版本可以被当前事务访问。
  3. 如果被访问版本的 trx_id 大于 ReadView 中的 max_trx_id 值,表明生成该版本的事务在当前事务生成 ReadView 后才开启,所以该版本不可以被当前事务访问。
  4. 如果被访问版本的 trx_id 属性值在 ReadView 的 min_trx_id 和 max_trx_id 之间,那就需要判断一下 trx_id 是不是在 m_ids 列表中,
    如果在,说明生成 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问;
    如果不在,说明生成 ReadView 时生成该版本的事务已经被提交,该版本可以被访问;
  5. 如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。
    如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录。

ReadView 生成时机:

  • READ COMMITTED —— 每次读取数据前都生成一个ReadView(不能保证可重复读)
  • REPEATABLE READ —— 在第一次读取数据时生成一个ReadView(能够保证可重复读)

RR 级别下幻读问题

上面说到 RR 级别下,会在第一次读取数据时就生成一个 ReadView,所以可以保证可重复读。但是 MVCC 能否解决幻读问题呢?答案是只能解决一部分情况下的幻读问题,也就是说能解决快照读情况下的幻读问题,但是不能解决当前读情况下的幻读问题(使用 next-key lock 来解决)。

  • 快照读

    快照读就是上面说到的 ReadView,可以读到数据的历史版本。普通的 select 都属于快照读。

  • 当前读

    当前读就是读取记录的最新版本。insert/update/delete/select...in share mode/select…for uodate 都属于当前读。

有关加锁的内容不在本文讨论范围。

总结

  • InnoDB 通过 undo log 来实现事务操作撤销的功能
  • undo log 在 FIL_PAGE_UNDO_LOG 类型的页面中
  • 每个事务都有最多四个 undo 页面链表,其中的 undo 页面能否重用视情况而定
  • InnoDB 使用 Rollback Segment Header 类型页面来管理多个 undo 页面链表
  • InnoDB 的 MVCC 可以通过 ReadView 解决可重复读和部分幻读问题

参考书籍:

Hexo+Github Pages写博客

小白想写点东西记录一下自己学习的道路,在网上看到用Hexo + Github Pages的居多,折腾了大概一天,终于搭起来了。以下为本次折腾的内容==。

相关准备

Git和NodeJS

这个没什么好说的,网上下载下来安装就ok

github pages相关

登录github,创建一个名为username.github.com的repository

Hexo安装及使用

安装

执行下面这个命令下载hexo

npm install -g hexo-cli

cd到一个目录下执行下面这个命令进行初始化

hexo init

安装hexo的扩展插件

npm install

生成静态页面

hexo g

开启本地服务器

hexo s

访问https://localhost:4000 就可以看到效果了,如果一直在加载可能是端口号4000被占用了,可以用下面这个命令开启本地服务器

hexo s -p 4001

使用

上面只是在本地上访问的,下面需要部署到github上
首先需要修改_config.yml文件最后三行为

deploy:
     type: git
     repo: https://github.com/Jimmy2Angel/Jimmy2Angel.github.com.git
     branch: master

然后使用下面的命令发布到github上

hexo generate 
hexo deploy

然后访问https://jimmy2angel.github.io/ 就可以看到hexo默认的HelloWorld了

新建一篇文章

hexo new "my first blog"

在source/_posts文件夹下生成一个my-first-blog.md文件,用markdown编辑器编辑好后,按上面所说发布到github上即可。在某些情况下需要在执行generate命令

hexo g

前先执行一个clean命令

hexo clean

然后在执行deploy命令

hexo d

常量池及字符串对象的创建

简介

java 中说到常量池,一般有下面三种:

  1. class文件常量池
  2. 运行时常量池
  3. 字符串常量池

class文件常量池

例如 Hello.java 代码如下:

public class Hello {
    public static void main(String[] args) {
        String s = "hello";
    }
}

在编译后通过 javap -v Hello 查看常量池部分如下:

Constant pool:
   #1 = Methodref          #4.#20         // java/lang/Object."<init>":()V
   #2 = String             #21            // hello
   #3 = Class              #22            // base/Hello
   #4 = Class              #23            // java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Utf8               LineNumberTable
   #9 = Utf8               LocalVariableTable
  #10 = Utf8               this
  #11 = Utf8               Lbase/Hello;
  #12 = Utf8               main
  #13 = Utf8               ([Ljava/lang/String;)V
  #14 = Utf8               args
  #15 = Utf8               [Ljava/lang/String;
  #16 = Utf8               s
  #17 = Utf8               Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               Hello.java
  #20 = NameAndType        #5:#6          // "<init>":()V
  #21 = Utf8               hello
  #22 = Utf8               base/Hello
  #23 = Utf8               java/lang/Object

其中 # 2 为 CONSTANT_String 类型,持有一个 # 21 的 index,指向一个 CONSTANT_Utf8 类型的常量 "hello"

运行时常量池

  • 运行时常量池以前位于永久代,jdk1.8 移除了永久代,取而代之的是元空间,位于 native memory。

  • 在类加载的时候,class文件常量池中的大部分数据都会进入到运行时常量池

  • CONSTANT_Utf8 类型对应的是一个 Symbol 类型的 C++ 对象,内容是跟 Class 文件同样格式的UTF-8编码的字符串

  • CONSTANT_String 类型对应的是一个实际的 Java 对象的引用,C++ 类型是 oop

  • CONSTANT_Utf8 类型对应的 Symbol 对象在类加载时候就已经创建了

  • CONSTANT_String 则是 lazy resolve 的,例如说在第一次引用该项的 ldc 指令被第一次执行到的时候才会 resolve。
    那么在尚未 resolve 的时候,HotSpot VM 把它的类型叫做 JVM_CONSTANT_UnresolvedString,内容跟Class文件里一样只是一个index;
    等到 resolve 过后这个项的常量类型就会变成最终的 JVM_CONSTANT_String,而内容则变成实际的那个 oop。

字符串常量池

  • jdk1.7 已经把字符串常量池从永久代中移到了堆中。
  • 字符串常量池中存的是引用,引用指向的字符串对象还是存储在堆中

以字面量形式"hello"创建

创建了几个对象?

2个对象。一个 String 对象和一个数组对象

什么时候创建对象?什么时候存入字符串常量池?

public class Hello {
    public static void main(String[] args) {
        String s = "hello";
    }
}
  1. 该代码在编译后的 class文件常量池中相关类型有 CONSTANT_String 和 CONSTANT_Utf8
  2. 在类加载的时候,CONSTANT_Utf8 类型对应的 Symbol 类型的 C++ 对象就已经创建,内容是跟 Class 文件同样格式的UTF-8编码的字符串"hello"
  3. 而 CONSTANT_String 类型的解析是延迟的,具体在第一次引用该项的 ldc 指令被第一次执行到的时候。
    解析 CONSTANT_String 时(该代码也就是执行到 Strings = "hello"; 的时候),根据 index 去运行时常量池查找 CONSTANT_UTF8,然后找到对应的 Symbol 对象,
    去到 StringTable,StringTable 支持以 Symbol 为 key 来查询是否已经有内容匹配的项存在与否,
    存在则直接返回匹配项,不存在则创建出内容匹配的java.lang.String 对象,然后将其引用放入 StringTable
  • 注:这里的 StringTable 是字符串常量池的实现方式。

以 new String("hello") 形式创建

创建了几个对象?

2个对象。两个 String 对象和一个共享的数组对象

什么时候创建对象?什么时候存入字符串常量池?

public class Hello {
    public static void main(String[] args) {
        String s = new String("hello");
    }
}

通过 javap -v Hello 查看 main 方法相关部分如下:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: new           #2                  // class java/lang/String
         3: dup
         4: ldc           #3                  // String hello
         6: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
         9: astore_1
        10: return
      LineNumberTable:
        line 10: 0
        line 11: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  args   [Ljava/lang/String;
           10       1     1     s   Ljava/lang/String;
  1. 该代码在编译后的 class文件常量池中相关类型有 CONSTANT_String 和 CONSTANT_Utf8
  2. 在类加载的时候,CONSTANT_Utf8 类型对应的 Symbol 类型的 C++ 对象就已经创建,内容是跟 Class 文件同样格式的UTF-8编码的字符串 "hello"
  3. 在运行时,执行到 String s = new String("hello"); 的时候,会执行一个引用该 CONSTANT_String 的 ldc 指令。也就是触发 CONSTANT_String 的解析。
    解析过程如上所述,因为是第一次,所以会在堆中创建一个内容为 "hello" 的 String 对象,然后将引用放到字符串常量池。随后才会通过 invokespecial 指令
    再在堆中创建一个 String 对象,该 String 对象引用存储在栈上。

String.intern()

jdk1.7 以后,该方法会查看字符串常量池中是否有引用指向该 String 对象,没有则添加该 String 对象的引用到字符串常量池。有则直接返回引用。

代码测试

  • 注:以下代码注释说法不考虑数组对象
    测试代码1如下:
private static void testStringIntern1() {
    
    // 堆上创建两个对象:hello 和 hello1。字符串常量池中引用 s 指向 hello 对象,栈上引用 s1 指向 hello1 对象
    String s1 = new String("hello");
    
    // 去常量池中寻找后发现字符串常量池中存在引用s,该引用指向的对象 hello 和栈上引用 s1 指向的 hello1 对象内容相同,直接返回(无变量接收)
    s1.intern();
    
    // 理解为没有创建对象,因为字符串常量池上存在引用 s 指向的 hello 对象的内容为 "hello",直接返回,此时栈上引用 s2 也指向引用 s 指向的 hello 对象
    String s2 = "hello";
    
    // s1 指向的对象为 hello1,s2 指向的对象为 hello,虽然两个对象的内容都是 "hello",但不是同一个对象
    System.out.println(s1 == s2); // false
    
    // 堆上创建一个对象:helloWorld。栈上引用 s3 指向 helloWorld 对象。字符串常量池中不存在该对象引用。
    // 至于 "hello" 和 "World" 我们不去讨论它们。
    String s3 = new String("hello") + new String("World");
    
    // 去常量池中寻找后未发现存在引用指向的对象的内容为 "helloWorld",但是jdk1.8(jdk1.7及以后版本)的字符串常量池可以直接存储引用,
    // 所以这里在字符串常量池中创建了一个引用 ss,该引用指向 s3 引用指向的堆上的 helloWorld 对象
    s3.intern();
    
    // 理解为没有创建对象,因为字符串常量池上存在引用 ss 指向的对象的内容为 "helloWorld",直接返回,此时栈上引用 s4 也指向 helloWorld 对象
    String s4 = "helloWorld";
    
    // 所以 s3 和 s4 指向同一个对象,也就是位于堆上的 helloWorld 对象
    System.out.println(s3 == s4); // true
}

测试代码2如下:

private static void testStringIntern2() {
    
    // 堆上创建两个对象:hello 和 hello1。字符串常量池中引用 s 指向 hello 对象,栈上引用 s1 指向 hello1 对象
    String s1 = new String("hello");
    
    // 理解为没有创建对象,因为字符串常量池上存在引用 s 指向的 hello 对象的内容为 "hello",直接返回,此时栈上引用 s2 也指向引用 s 指向的 hello 对象
    String s2 = "hello";
    
    // 去常量池中寻找后发现字符串常量池中存在引用s,该引用指向的对象 hello 和栈上引用 s1 指向的 hello1 对象内容相同,直接返回(无变量接收)
    s1.intern();
    
    // s1 指向的对象为 hello1,s2 指向的对象为 hello,虽然两个对象的内容都是 "hello",但不是同一个对象
    System.out.println(s1 == s2); // false
    
    // 堆上创建一个对象:helloWorld。栈上引用 s3 指向 helloWorld 对象。字符串常量池中不存在该对象引用。
    // 至于 "hello" 和 "World" 我们不去讨论它们。
    String s3 = new String("hello") + new String("World");
    
    // 因为此时常量池上不存在引用指向的对象内容为 "helloWorld"
    // 所以在堆上创建一个字符串对象 helloWorld2,添加其引用 ss 到字符串常量池,同时栈上引用 s4 也指向 helloWorld2 对象
    String s4 = "helloWorld";
    
    // 去字符串常量池查找发现存在引用 ss 指向的对象的内容为 "helloWorld",直接返回(无变量接收)
    s3.intern();
    
    // s3 指向堆上的对象 helloWorld,而 s4 指向对象的对象 helloWorld2,所以不是同一个对象
    System.out.println(s3 == s4); // false
}

测试代码3如下:

public static void main(String[] args) {
    String str1 = new StringBuilder("he").append("llo").toString();
    System.out.println(str1.intern() == str1);
    
    String str2 = new StringBuilder("ja").append("va").toString();
    System.out.println(str2.intern() == str2);
}

打印结果为true,false。
str1.intern() == str1 等于 true 很好理解。
但是 str2.intern() 不指向引用 str2 指向的对象?说明之前字符串常量池中已经有了引用指向的对象内容为"java"?
的确如此,因为 sun.misc.Version 类中定义了一个常量:

private static final String launcher_name = "java";

该类会在 JDK 类库的初始化过程中被加载和初始化。初始化时就创建了 "java" 对象,并保存引用到字符串常量池中了。

InnoDB redo log

MySQL 数据库对页中数据的修改都是在 Buffer Pool 中进行的,如果一个事务修改了 Buffer Pool 中的一个数据页,但是该数据页尚未刷新回磁盘,此时服务器挂了,Buffer Pool 中的数据修改就会丢失。
那么这个 持久性 怎么保证?MySQL 通过 redo log 来保证基于 Buffer Pool 的数据修改在服务器恢复后不会丢失,也就是会在事务执行期间将对数据库的修改通过 redo log 保存下来,待服务器恢复后通过重放该 redo log 达到数据不丢失的目的。

redo log 简介

redo log 实际上就是记录了事务执行过程中对数据库的修改,和刷新内存中数据页到磁盘相比,将 redo log 刷新到磁盘具有以下好处:

  • redo log 占用的空间小,因为只记录数据页修改的部分
  • 事务的一条语句产生的多条 redo log 是顺序写入磁盘的,也就是顺序IO

一条 redo log 主要由 type(日志类型)、space ID(表空间ID)、page number(页号) 和 data(日志的具体内容) 组成。

Mini-Transaction

MySQL 把对底层页面中的一次原子访问的过程称之为一个 Mini-Transaction,也就是一组更小的事务(对该页面的这组操作要么都完成要么都不完成)。比如上边所说的修改一次 Max Row ID 的值算是一个 Mini-Transaction,向某个索引对应的 B+ 树中插入一条记录的过程也算是一个 Mini-Transaction。

一个事务可以包含多条 SQL 语句,一条 SQL 语句可以包含多个 mtr,一个 mtr 可以产生多条 redo log。

redo log 写入过程

redo log block

InnoDB 把通过 mtr 生成的 redo log 放在大小为 512 字节的 redo log block 中,该 block 由 log block header(12字节)、log block body 和 log block trailer(4字节) 组成。

真正的 redo log 信息都存储在 log block body 中,log block header 和 log block trailer 存储了一些管理信息,包括以下几个信息:

  • LOG_BLOCK_HDR_NO:该 block 的唯一标记号

  • LOG_BLOCK_HDR_DATA_LEN :表示 block 中已经使用了多少字节,初始值为 12

  • LOG_BLOCK_FIRST_REC_GROUP:这个 block 里第一个 mtr 生成的第一条 redo log 的偏移量

  • LOG_BLOCK_CHECKPOINT_NO:checkpoint 的序号

redo log buffer

由于磁盘写入速度慢的问题,redo log 并不能直接写入磁盘,所在在服务器启动的时候就向操作系统申请一片称之为 redo log buffer 的连续内存空间。该内存被划分为若干个 redo log block

我们可以通过启动参数 innodb_log_buffer_size 来指定其大小,在 MySQL 5.7.21 这个版本中,该启动参数的默认值为 16MB

写入 redo log Buffer

  • 通过全局变量 buf_free 指明后续的 redo log 应该写到 redo log buffer 的哪个位置。

  • redo log 写入到 redo log Buffer 并不是一条一条写的,而是以 mtr 为单位写入的。不同事务的 mtr 的 redo log 可以交替写入。

redo 日志文件

刷盘时机

上文说到 redo log 在写入磁盘前会先保存到 redo log buffer 中,但是总要刷新到磁盘啊。在以下几种情况,redo log buffer 中的 redo log 会写入磁盘:

  • redo log buffer 空间不足时

    如果当前写入 redo log buffer 的 redo log 已经占满了 redo log buffer 总容量的大约一半左右,就需要把这些日志刷新到磁盘上。

  • 事务提交时

    在事务提交时可以不把修改过的 Buffer Pool 中的脏页刷新到磁盘,但是为了保证持久性,必须要把修改这些页面对应的 redo log 刷新到磁盘。

  • 后台线程定时执行

    后台有个线程,大约每秒会刷新到磁盘一次。

  • 正常关闭服务器时

  • checkpoint 时

redo 日志文件组

MySQL 的数据目录(使用 SHOW VARIABLES LIKE 'datadir' 查看)下默认有两个名为 ib_logfile0ib_logfile1 的文件,redo log buffer 中的日志默认情况下就是刷新到这两个磁盘文件中。也可以通过以下参数调整:

  • innodb_log_group_home_dir

    该参数指定了 redo 日志文件所在的目录,默认值就是当前的数据目录。

  • innodb_log_file_size

    该参数指定了每个 redo 日志文件的大小,在 MySQL 5.7.21 这个版本中的默认值为 48MB

  • innodb_log_files_in_group

    该参数指定 redo 日志文件的个数,默认值为2,最大值为100。

所以总共的 redo 日志文件大小其实就是:innodb_log_file_size × innodb_log_files_in_group

按顺序写入日志文件,如果最后一个日志文件也写满的话,则重新从第一个日志文件开始,也就是形成一个循环。那这样的话不是会覆盖掉前面写入的日志文件么?这就涉及到下面要说的 checkpoint 了。

redo 日志文件格式

前面说到 redo log buffer 由若干个 512 字节的 redo log block 组成,所以每个 redo 日志文件其实也是由若干个 512 字节的 block 组成。每个 redo 日志文件的前 4 个 block 用来存储管理信息,从第 5 个 block 开始存储 redo log。

那么前 4 个 block 存了些什么东西呢?

  • 第一个 block 为 log file header :描述该 redo 日志文件的一些整体属性
    • LOG_HEADER_FORMAT:日志版本
    • LOG_HEADER_START_LSN:标记本 redo 日志文件开始的 LSN 值
  • 第二个 block 为 checkpoint1:记录关于 checkpoint 的一些属性
    • LOG_CHECKPOINT_NO:checkpoint 的编号
    • LOG_CHECKPOINT_LSN:checkpoint 结束时的 LSN 值
    • LOG_CHECKPOINT_OFFSET:上个属性中的 LSN 值在 redo 日志文件组中的偏移量
    • LOG_CHECKPOINT_LOG_BUF_SIZE:服务器在做 checkpoint 操作时对应的 redo log buffer 的大小
  • 第三个 block 暂未使用
  • 第四个 block 为 checkpoint2:同 checkpoint1

上面这一大串的 checkpoint 和 LSN 下面会说到。

Log Sequeue Number

  • 规定初始的 lsn 值为 8704(也就是一条 redo 日志也没写入时,lsn 的值为 8704)
  • 系统第一次启动后初始化 redo log buffer 时,buf_free 就会指向第一个 block 的偏移量为12字节(log block header的大小)的地方,那么 lsn 值也会跟着增加 12
  • 如果某个 mtr 产生的一组 redo 日志占用的存储空间比较小,也就是待插入的 block 剩余空闲空间能容纳这个 mtr 提交的日志时,lsn 增长的量就是该 mtr 生成的 redo 日志占用的字节数
  • 如果某个 mtr 产生的一组 redo 日志占用的存储空间比较大,也就是待插入的 block 剩余空闲空间不足以容纳这个 mtr 提交的日志时,lsn 增长的量就是该 mtr 生成的 redo 日志占用的字节数加上额外占用的 log block header 和 log block trailer 的字节数
  • 每一组由 mtr 生成的 redo 日志都有一个唯一的 LSN 值与其对应,LSN 值越小,说明 redo 日志产生的越早。

flushed_to_disk_lsn

  • 全局变量 buf_next_to_write ,标记当前 redo log buffer 中已经有哪些日志被刷新到磁盘中了

  • 全局变量 flushed_to_disk_lsn,表示刷新到磁盘中的 redo 日志量

    系统第一次启动时,该变量的值和初始的 lsn 值是相同的,都是 8704。随着系统的运行,redo 日志被不断写入log buffer,但是并不会立即刷新到磁盘,lsn 的值增加,flushed_to_disk_lsn 的值不变,随着不断有 redo log buffer 中的日志被刷新到磁盘上,flushed_to_disk_lsn 的值也跟着增长。如果两者的值相同时,说明 redo log buffer 中的所有 redo 日志都已经刷新到磁盘中了。

flush链表中的 LSN

Buffer Pool 中数据页结构大致如下:

  • 每个缓存页有一个对应的控制块
  • 修改缓存页后会将对应的控制块加入到 flush 链表

  • 在 mtr 结束时,会把这一组 redo 日志写入到 redo log buffer 中。除此之外,在 mtr 结束时还要把在 mtr 执行过程中可能修改过的页面加入到 Buffer Pool 的 flush 链表

  • 当第一次修改某个缓存在 Buffer Pool 中的页面时,就会把这个页面对应的控制块插入到 flush 链表的头部,之后再修改对应控制块中记录两个关于页面何时修改的属性:

    • oldest_modification:如果某个页面被加载到 Buffer Pool 后进行第一次修改,那么就将修改该页面的 mtr 开始时对应的 lsn 值写入这个属性。
    • newest_modification: 每修改一次页面,都会将修改该页面的 mtr 结束时对应的 lsn 值写入这个属性。也就是说该属性表示页面最近一次修改后对应的系统 lsn 值。
  • flush 链表中的脏页按照修改发生的时间顺序进行排序,也就是按照 oldest_modification 代表的 LSN 值进行排序,被多次更新的页面不会重复插入到 flush 链表中,但是会更新 newest_modification 属性的值。

checkpoint

前面介绍日志文件组的时候说到,如果最后一个日志文件写满了的话会从回到第一个日志文件开始写。这样不就把第一个日志文件的内容覆盖了么?什么情况下可以覆盖呢?如果当日志文件中的 redo log 对应的脏页已经刷新回磁盘了,这个 redo log 部分就可以覆盖掉。

我们通过全局变量 checkpoint_lsn (代表当前系统中可以被覆盖的 redo 日志总量是多少,初始值也是8704)可以知道是否能覆盖。那么这个 checkpoint_lsn 值怎么来的呢?

比方说 Buffer Pool 数据页 A 刷新回磁盘了,此时做一次 checkpoint(也就是增加 checkpoint_lsn 值)。过程可以分为两步:

  1. 计算当前系统中可以被覆盖的 redo log 的 lsn 值最大是多少

    只要我们计算出当前系统中被最早修改的脏页对应的 oldest_modification 值,那凡是在系统 lsn 值小于该节点的 oldest_modification 值时产生的 redo 日志都是可以被覆盖掉的(因为对于缓存页已经刷新回磁盘了),我们就把该脏页的 oldest_modification 赋值给 checkpoint_lsn。

  2. 将 checkpoint_lsn 和对应的 redo 日志文件组偏移量以及此次 checkpoint 的编号写到日志文件的管理信息(就是上文说到的 checkpoint1 或者 checkpoint2)中。

innodb_flush_log_at_trx_commit

innodb_flush_log_at_trx_commit 用来控制事务提交时是否需要立即向磁盘同步 redo 日志:

  • 值为 0:表示在事务提交时不立即向磁盘中同步 redo 日志,这个任务是交给后台线程做的

  • 值为 1:表示在事务提交时需要将 redo 日志同步到磁盘,可以保证事务的持久性;默认值

    • 值为 2:表示在事务提交时需要将 redo 日志写到操作系统缓冲区中,并不需要保证将日志真正的刷新到磁盘

崩溃恢复

下面说说系统崩溃重启后怎么通过 redo log 来恢复数据。

确定起点

前面说到了一个 checkpoint_lsn ,小于它的 redo log 对应的脏页已经刷新回磁盘了,所以不用恢复了。但是大于 checkpoint_lsn 的 redo log 对应的脏页可能刷盘了也可能没刷盘,不能确定。所以需要从 checkpoint_lsn 开始读取 redo 日志来恢复页面。

从日志文件组的第一个文件的管理信息中的 checkpoint 的信息中获取 checkpoint_lsn 值以及它在 redo 日志文件组中的偏移量 checkpoint_offset。

确定终点

普通 block 的 log block header 部分有一个称之为 LOG_BLOCK_HDR_DATA_LEN 的属性,该属性值记录了当前 block 里使用了多少字节的空间。对于被填满的 block 来说,该值永远为 512。如果该属性的值不为 512,那么就是它了,它就是此次崩溃恢复中需要扫描的最后一个 block。

如何恢复

知道了起点和终点,那么按照顺序依次恢复即可,但是 InnoDB 做了两个优化:

  • 使用哈希表

    根据 redo 日志的 space ID 和 page number 属性计算出散列值,把 space ID 和 page number 相同的 redo 日志放到哈希表的同一个槽里;如果多个 redo log 的 space ID 和 page number 信息相同,也就是对应同一个数据页,那么它们之间按照生成顺序使用链表连接起来;

    之后就可以遍历哈希表,因为对同一个页面进行修改的 redo 日志都放在了一个槽里,所以可以一次性将一个页面修复好(避免了很多读取页面的随机IO),这样可以加快恢复速度。

  • 跳过已经刷新到磁盘的页面

    上面说到大于 checkpoint_lsn 值的 redo log 需不需要恢复是不确定的,原因是可能在做 checkpoint 之后,后台线程可能又从 LRU 链表和 flush 链表刷新了脏页回磁盘。那么怎么知道某个 redo log 对应的脏页是否在崩溃前就刷新回磁盘了呢?

    页面的 File Header 里有一个称之为 FIL_PAGE_LSN 的属性,该属性记载了最近一次修改页面时对应的 lsn 值,如果在做了某次 checkpoint 之后有脏页被刷新到磁盘中,那么该页对应的 FIL_PAGE_LSN 代表的 lsn 值肯定大于 checkpoint_lsn 的值。凡是符合这种情况的页面就不需要重复执行lsn值小于 FIL_PAGE_LSN 的 redo 日志了,所以更进一步提升了奔溃恢复的速度。

总结

  • 事务执行过程中对数据库的修改首先写入 redo log buffer,buffer 由 redo log block 组成
  • redo 日志文件组可以有多个日志文件,循环链表形式写入 redo log
  • lsn 作为 redo log 的 '年龄',越早产生的 redo log 年龄越小
  • 全局变量 buf_free 表示后续 redo log 应该写到 redo log buffer 什么位置
  • 全局变量 buf_next_to_write 表示 redo log buffer 中该位置之前的 redo log 已经写到磁盘日志文件了
  • 全局变量 flushed_to_disk_lsn 表示刷新到磁盘中的 redo 日志量
  • 全局变量 checkpoint_lsn 表示日志文件哪些可以被覆盖(该值之前的 redo log 对应页已经刷新回磁盘了)
  • 崩溃恢复以 checkpoint 为起点,采用哈希表等优化手段加速恢复

参考书籍:

Spring容器那点事

Spring 容器启动

对于一个web应用,部署在web容器中,web容器为其提供一个全局的上下文环境即ServletContext,其为SpringIOC容器提供宿主环境。
启动web项目后,会去加载web.xml中的内容,其中包括以下内容

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>
      classpath:spring-application.xml,
    </param-value>
  </context-param>

  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>

由于 ContextLoaderListener 实现了 ServletContextListener 接口,故在web容器启动的时候,ContextLoaderListener 会监听到这个事件,contextInitialized(event) 方法被调用。

跟踪该方法会发现它初始化一个上下文 WebApplicationContext,默认实现类为 XmlWebApplicationContext,也就是 SpringIOC 容器。在 ContextLoader 创建如下:

跟踪 determineContextClass(servletContext) 方法,由于我们未配置 contextClass,
所以走else走

一开始我就知道创建了一个 WebApplicationContext 没仔细看创建的是什么,后来打断点发现 contextClassName 为 XmlWebApplicationContext,383行的 defaultStrategies 原来在静态块中,真是大意了。(你去看下 org.springframework.web.context 包下的 ContextLoader.properties 就知道了)

然后返回看 initWebApplicationContext(servletContext),给 IOC 容器设置父容器,并把它作为属性设置进web容器。

我们主要看下306行的 configureAndRefreshWebApplicationContext(cwac,servletContext) 方法。源码不贴了,主要就是给设置一些web容器中的配置信息(包括将 ServletContext 设置成 XmlWebApplicationContext 的属性,这样 Spring 就能在上下文里轻松拿到 ServletContext 了)

Spring 加载 class 文件

看最后一行的 refresh()方法即可,在这个方法里,会完成资源文件的加载、配置文件解析、Bean定义的注册、组件的初始化等核心工作。该方法来自于 XmlWebApplicationContext 的父父父类 AbstractApplicationContext(调皮一下,我并不是个结巴==),其实不管是 web 容器装载的 XmlWebApplicationContext 还是直接 ClassPathXmlApplicationContext 都会调用这个方法。

讲道理我现在一眼看到 synchronized,我都不把它翻译成锁,我都翻译成安全哈哈哈。至于这为什么不是锁住整个 refresh() 方法,由于不属于今天所讲的范围就不说了,感兴趣的自己去查一下咯。
我们来看下初始化 BeanFactory,obtainFreshBeanFactory 是整个 refresh() 方法的核心,其中完成了配置文件的加载、解析、注册。

后面你看到的都是 getBeanFactory 的代码,也就是已经初始化好了,这个 refreshBeanFactory 方法是 AbstractRefreshableApplicationContext 中的方法,它是 AbstractApplicationContext 的子类,同样不论是 XmlWebApplicationContext 还是 ClassPathXmlApplicationContext 都继承了它,因此都能调用到这个一样的初始化方法。

createBeanFactory 就是实例化一个 beanFactory 没别的,我们要看的是 bean 在哪里加载的,现在貌似还没看到重点,继续跟踪 loadBeanDefinitions(DefaultListableBeanFactory) 方法,此处web项目中将会由类: XmlWebApplicationContext 来实现

这里有一个 XmlBeanDefineitionReader,是读取 XML 中 spring 的相关信息(也就是解析 spring-application.xml 的),这里通过 getConfigLocations() 获取到的就是这个或多个文件的路径,会循环,通过 XmlBeanDefineitionReader 来解析,跟踪到 loadBeanDefinitions 方法里面,会发现方法实现体在 XmlBeanDefineitionReader 的父类 AbstractBeanDefinitionReader 中,代码如下:

解析spring-application.xml

我们目前只解析到我们的 spring-application.xml 在哪里,但是还没解析到spring-application.xml 的内容是什么,可能有多个 spring 的配置文件,这里会出现多个 Resource,所以是一个数组。
XmlBeanDefinitionReader.loadBeanDefinitions(Resource) 和上面这个类是父子关系,接下来会做:doLoadBeanDefinitions、registerBeanDefinitions 的操作,在注册 beanDefinitions 的时候,其实就是要真正开始解析XML了
它调用了 DefaultBeanDefinitionDocumentReader 类的 registerBeanDefinitions 方法,然后调用了 doRegisterBeanDefinitions 方法,如下图所示:

这里创建了一个 BeanDefinitionParserDelegate 实例,解析 XML 的过程就是委托它完成的,我们先不管它是怎样解析XML的,我们看下它怎么加载类的,所以主要看 parseBeanDefinitions 这个方法,里面会调用到 BeanDefinitionParserDelegate 类的 parseCustomElement 方法,用来解析bean的信息:

这里解析了 XML 的信息,跟踪进去,会发现用了 NamespaceHandlerSupport 的 parse 方法,它会根据节点的类型,找到一种合适的解析 BeanDefinitionParser(接口),他们预先被 spring 注册好了,放在一个 HashMap 中,例如我们在 spring 中通常会配置:

<context:component-scan base-package="com.lb.supervision.service" use-default-filters="false">
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Service" />
    </context:component-scan>

此时根据名称“component-scan”就会找到对应的解析器来解析,而与之对应的就是 ComponentScanBeanDefinitionParser 的 parse 方法,这地方已经很明显有扫描 bean 的概念在里面了,这里的 parse 获取到后,中间有一个非常非常关键的步骤那就是定义了 ClassPathBeanDefinitionScanner 来扫描类的信息,它扫描的是什么?是加载的类还是class文件呢?答案是后者,为什么?,因为有些类在初始化化时根本还没被加载, ClassLoader 根本还没加载,只是 ClassLoader 可以找到这些 class 的路径而已:

咦,看到这个doScan有种似曾相识的感觉呢,啊,之前说 mybatis 的 mapper 接口代理的时候看到过。先不说这个了,跟进去看看。

这么多代码看着很难受,要说把每行代码都搞懂估计现在还有点困难,直接看重点 findCandidateComponents,也就是通过每个 basePackage 去获取匹配的 class 文件路径。

此处的 packageSearchPath = classpath*:com/lb/supervision/service/**/*.class,如果我们配置的是 * ,那么将会被组装为 classpath*:*/**/*.class,这个也能获取到? Spring 还有这种操作的?我们点进 getResources(packageSearchPath)方法看下:

这里会先判断表达式是否以 classpath*: 开头。前面我们看到 Spring 已经给我们添加了这个头,这里当然符合条件了。接着会进入 findPathMatchingResources 方法。在这里又把 **/*.class 去掉了,然后在调用 getResources 方法,然后再进入 findAllClassPathResources 方法。这里的参数只剩下包名了例如 com/lb/supervision/service/ 。,先看下 findPathMatchingResources 方法:

这里有一个 rootDirPath,这个地方有个容易出错的,是如果你配置的是 com.lb.supervision.service ,那么 rootDirPath 部分应该是: classpath*:com/lb/supervision/service , 而如果配置是 * 那么就是 classpath*: 只有这个结果,而不是 classpath*:* (这里我就不说截取字符串的源码了),回到上一段代码,这里再次调用了 getResources(String) 方法,又回到前面一个方法,这一次,依然是以 classpath*: 开头,所以第一层 if 语句会进去,而第二层不会。所以我们再看一下 findAllClassPathResources(location)方法里的 doFindAllClassPathResources(path):

真相大白了, Spring 也是用的 ClassLoader 加载的 class 文件。一路追踪,原始的 ClassLoader 是 Thread.currentThread().getContextClassLoader()。
到此为止,就拿到class文件了。
然后我们回到 ClassPathBeanDefinitionScanner 的 doScan() 方法,跟踪 registerBeanDefinition() 方法直到 DefaultListableBeanFactory 类中的registerBeanDefinition() 方法。 Spring 会将 class 信息封装成的 BeanDefinition 放进 DefaultListableBeanFactory 的 beanDefinitionMap 中。源码就不贴了,感兴趣的旁友自己看下。

Spring 实例化 非懒加载单例bean

我们回到 refresh() 方法中看 finishBeanFactoryInitialization(beanFactory) 方法,此方法主要的任务就是实例化非懒加载的单例 bean。来看下这个方法:

由于代码长没有截全,该方法首先将加载进来的 beanDefinitionNames 循环分析,如果是我们自己配置的 bean 就会走 else 中的 getBean(beanName)。

getBean(beanName) 中调用 doGetBean() 方法,二话不说点进去看看,卧槽这么长!唉,没办法一点点看吧。

我们先看下这里的 getSingleton() 方法:

这里能看到, Spring 会把实例化好的 bean 存入 singletonObjects,这是一个 ConcurrentHashMap 。当然这里我们 bean 并未实例化过,所以这里应该也不能 get 出什么东西来,也就是返回 null 了。 doGetBean() 中的第一个 if 子句也就不会执行了。那么接着看 else 子句的内容。

在这里拿到 RootBeanDefinition 并 check,并获得 bean 的依赖,并循环迭代实例化 bean。例如class A 依赖于 class B,就会先实例化B。下面的 if ... else ...就是真正实例化 bean 的地方。其实真正实例化 bean 的方法是 createBean(beanName, mbd, args),只是区分了 isSingleton 或 isPrototype ,两者的区别在于单例的(Singleton)被缓存起来,而 Prototype 是不用缓存的。首先看一下 createBean()。 createBean 方法中除了做了一些实例化 bean 前的检查准备工作外,最核心的方法就是

beanInstance = this.doCreateBean(beanName, mbd, args);

这里面代码那就多了,就不贴出所有代码了

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, Object[] args) {
        BeanWrapper instanceWrapper = null;
        if(mbd.isSingleton()) {
            instanceWrapper = (BeanWrapper)this.factoryBeanInstanceCache.remove(beanName);
        }

        if(instanceWrapper == null) {
            instanceWrapper = this.createBeanInstance(beanName, mbd, args);
        }

        final Object bean = instanceWrapper != null?instanceWrapper.getWrappedInstance():null;
        Class beanType = instanceWrapper != null?instanceWrapper.getWrappedClass():null;

首先就是创建一个bean的实例且封装到BeanWrapper中,在这里bean已经实例化了。具体的实现方法是在org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(RootBeanDefinition beanDefinition, String beanName, BeanFactory owner) 中。里面不难看出分两种情况,如果没有无参构造器是就生成CGLIB子类,否则就直接反射成实例。

既然已经有了实例对象了,那么, Spring 是如何将 bean 的属性注入到 bean 的呢?返回到上面的 doCreateBean 方法中。往下看找到 populateBean(beanName, mbd, instanceWrapper) ,内幕就在这里:

这里是调用 InstantiationAwareBeanPostProcessor 的具体子类的 ibp.postProcessPropertyValues 方法注入属性。当我们使用 @resource 注解的时候,具体的子类是 CommonAnnotationBeanPostProcessor;如果使用的是 @Autowired 注解,则具体的子类是 AutowiredAnnotationBeanPostProcessor。此方法内是委托 InjectionMetadata 对象来完成属性注入。以 AutowiredAnnotationBeanPostProcessor 为例:

findAutowiringMetadata 方法能拿到使用了特定注解的属性(Field)、方法(Method)及依赖的关系保存到 checkedElements 集合里,然后再执行自己的 inject 方法。

喏,终于快要完事了,来看 InjectedElement 的 inject 方法:

喏,还是用JDK反射完成的咯。讲道理,以前只知道反射能干嘛,但是不知道能用在什么地方。现在看看源码好多地方都用的反射。。。

Spring容器看到这里差不多了,有空再看下SpringMVC的。。。

BlockingQueue

首先先介绍一下 Queue、AbstractQueue等接口和类。

Queue

该接口针对添加元素、移除元素、获取第一个元素各有两个方法:

  1. 添加元素:
    • add(E e) 当队列为满时前者会抛出 IllegalStateException
    • offer(E e) 当队列为满时前者会返回 false
  2. 移除元素
    • remove(E e) 当队列为空时前者会抛出 NoSuchElementException
    • poll(E e) 当队列为空时前者会返回 null
  3. 获取第一个元素:
    • element( ) 当队列为空时前者会抛出 NoSuchElementException
    • peek( ) 当队列为空时前者会返回 null

BlockingQueue

该接口实现了 Queue 接口,针对添加元素、移除元素各有两个方法

  1. 添加元素:

    一直等待:

    void put(E e) throws InterruptedException;

    超时等待:

    boolean offer(E e, long timeout, TimeUnit unit)
            throws InterruptedException;
  2. 移除元素:

    一直等待:

    E take() throws InterruptedException;

    超时等待:

    E poll(long timeout, TimeUnit unit) throws InterruptedException;

AbstractQueue

add(E e)

public boolean add(E e) {
    if (offer(e))
        return true;
    else
        throw new IllegalStateException("Queue full");
}

remove()

public E remove() {
    E x = poll();
    if (x != null)
        return x;
    else
        throw new NoSuchElementException();
}

element()

public E element() {
    E x = peek();
    if (x != null)
        return x;
    else
        throw new NoSuchElementException();
}

ArrayBlockingQueue

简介

继承体系为:

public class ArrayBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable

拥有的变量如下:

/** The queued items */
final Object[] items;
 
/** items index for next take, poll, peek or remove */
int takeIndex;
 
/** items index for next put, offer, or add */
int putIndex;
 
/** Number of elements in the queue */
int count;
 
/*
* Concurrency control uses the classic two-condition algorithm
* found in any textbook.
*/
 
/** Main lock guarding all access */
final ReentrantLock lock;
 
/** Condition for waiting takes */
private final Condition notEmpty;
 
/** Condition for waiting puts */
private final Condition notFull;
 
/**
* Shared state for currently active iterators, or null if there
* are known not to be any.  Allows queue operations to update
* iterator state.
*/
transient Itrs itrs = null;

线程安全实现

线程安全主要是通过可重入锁 ReentrantLock 来实现的。

add(E e)

public boolean add(E e) {
    return super.add(e);
}

offer(E e)

public boolean offer(E e) {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    // 获取锁
    lock.lock();
    try {
        // 如果队列已满返回 false
        if (count == items.length)
            return false;
        else {
            // 添加元素
            enqueue(e);
            return true;
        }
    } finally {
        // 释放锁
        lock.unlock();
    }
}

enqueue(E e)

private void enqueue(E x) {
    // assert lock.getHoldCount() == 1;
    // assert items[putIndex] == null;
    final Object[] items = this.items;
    items[putIndex] = x;
    if (++putIndex == items.length)
        putIndex = 0;
    count++;
    // 类似于notify()
    notEmpty.signal();
}

poll()

public E poll() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return (count == 0) ? null : dequeue();
    } finally {
        lock.unlock();
    }
}

dequeue()

private E dequeue() {
    // assert lock.getHoldCount() == 1;
    // assert items[takeIndex] != null;
    final Object[] items = this.items;
    @SuppressWarnings("unchecked")
    E x = (E) items[takeIndex];
    items[takeIndex] = null;
    if (++takeIndex == items.length)
        takeIndex = 0;
    count--;
    if (itrs != null)
        itrs.elementDequeued();
    notFull.signal();
    return x;
}

阻塞

阻塞是基于 Condition 接口实现的。Condition 拥有类似的操作:await/signal。Condition 和一个 Lock 相关,由lock.newCondition() 来创建。只有当前线程获取了这把锁,才能调用 Condition 的 await 方法来等待通知,否则会抛出异常。

put(E e)

public void put(E e) throws InterruptedException {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == items.length)
            // 如果队列已满,则阻塞等待队列移除元素
            notFull.await();
        // 该方法中包含移除元素发生阻塞时需要 notEmpty.signal()
        enqueue(e);
    } finally {
        lock.unlock();
    }
}
  • 实现阻塞的关键就是就是这个 notFull 的 Condition,当队列已满,await 方法会阻塞当前线程,并且释放Lock,等待其他线程调用 notFull 的 signal 来唤醒这个阻塞的线程。那么这个操作必然会在拿走元素的操作中出现,这样一旦有元素被拿走,阻塞的线程就会被唤醒。
  • 发出 signal 的线程肯定拥有这把锁的,因此 await 方法所在的线程肯定是拿不到这把锁的,await 方法不能立刻返回,需要尝试获取锁直到拥有了锁才可以从 await 方法中返回。

take()

同样对于take方法会有一个 notEmpty 的 Condition。

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0)
            // 如果队列为空,则阻塞等待队列添加一个元素
            notEmpty.await();
        // 该方法中包含移除元素发生阻塞时需要 notFull.signal()
        return dequeue();
    } finally {
        lock.unlock();
    }
}

总结

  • 线程安全是基于可重入锁 ReentrantLock 实现的。
  • 阻塞是基于 Condition 的 await/signal 实现的。

LinkedBlockingQueue

简介

LinkedBlockingQueue 是使用一个链表来实现,拥有一个内部类 Node 其中 Node 结构如下:

/**
* Linked list node class
*/
static class Node<E> {
    E item;
     
    /**
    * One of:
    * - the real successor Node
    * - this Node, meaning the successor is head.next
    * - null, meaning there is no successor (this is the last node)
    */
    Node<E> next;
     
    Node(E x) { item = x; }
}

拥有成员变量如下,其中包括一个 head 节点(item 为 null,next 节点才是first 节点)和一个 last 节点(next 节点为 null),然后还包括两个锁:takeLock 和 putLock

/** The capacity bound, or Integer.MAX_VALUE if none */
private final int capacity;
 
/** Current number of elements */
private final AtomicInteger count = new AtomicInteger();
 
/**
* Head of linked list.
* Invariant: head.item == null
*/
transient Node<E> head;
 
/**
* Tail of linked list.
* Invariant: last.next == null
*/
private transient Node<E> last;
 
/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();
 
/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();
 
/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();
 
/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();

方法解析

offer(E e)

源码如下,另外还有一个重载方法 offer(E e, long timeout, TimeUnit unit),在队列已满的时候,进行超时阻塞。

public boolean offer(E e) {
    if (e == null) throw new NullPointerException();
    final AtomicInteger count = this.count;
    // 队列已满则返回 false
    if (count.get() == capacity)
        return false;
    int c = -1;
    // 根据待添加元素 e 创建一个 node 节点
    Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock;
    putLock.lock();
    try {
        if (count.get() < capacity) {
            // 将 last 节点的 next 设置为 node 节点 ,然后将 node 节点设置为 last 节点
            enqueue(node);
            c = count.getAndIncrement();
            // 如果添加该元素后队列还未满,则调用 notFull.signal() 方法,即唤醒添加元素时由于队列已满导致阻塞的线程
            if (c + 1 < capacity)
                notFull.signal();
        }
    } finally {
        putLock.unlock();
    }
    if (c == 0)
    // 一个获取 takeLock、notEmpty.signal()、释放 takeLock 操作过程。也就是通知其他线程可以移除元素了。
        signalNotEmpty();
    return c >= 0;
}

enqueue(Node e)

private void enqueue(Node<E> node) {
    // assert putLock.isHeldByCurrentThread();
    // assert last.next == null;
    last = last.next = node;
}

poll()

和 offer(E e) 方法类似,除此之外也提供了一个重载方法 poll(long timeout, TimeUnit unit),在队列为空的时候,进行超时阻塞。

public E poll() {
    final AtomicInteger count = this.count;
    // 队列为空则返回 null
    if (count.get() == 0)
        return null;
    E x = null;
    int c = -1;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        if (count.get() > 0) {
            // 获取 first 节点的 item,然后设置为 head 节点(first.item = null)
            x = dequeue();
            c = count.getAndDecrement();
            // 如果移除元素后队列不为空,则唤醒移除元素时由于队列为空导致阻塞的线程
            if (c > 1)
                notEmpty.signal();
        }
    } finally {
        takeLock.unlock();
    }
    // 一个获取 putLock、notFull.signal()、释放 putLock 操作过程。也就是通知其他线程可以添加元素了。
    if (c == capacity)
        signalNotFull();
    return x;
}

dequeue()

private E dequeue() {
    // assert takeLock.isHeldByCurrentThread();
    // assert head.item == null;
    Node<E> h = head;
    Node<E> first = h.next;
    h.next = h; // help GC
    head = first;
    E x = first.item;
    first.item = null;
    return x;
}

put(E e)

public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();
    // Note: convention in all put/take/etc is to preset local var
    // holding count negative to indicate failure unless set.
    int c = -1;
    Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    putLock.lockInterruptibly();
    try {
        /*
        * Note that count is used in wait guard even though it is
        * not protected by lock. This works because count can
        * only decrease at this point (all other puts are shut
        * out by lock), and we (or some other waiting put) are
        * signalled if it ever changes from capacity. Similarly
        * for all other uses of count in other wait guards.
        */
        while (count.get() == capacity) {
            notFull.await();
        }
        enqueue(node);
        c = count.getAndIncrement();
        if (c + 1 < capacity)
            notFull.signal();
    } finally {
        putLock.unlock();
    }
    if (c == 0)
        signalNotEmpty();
}

take()

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();
        }
        x = dequeue();
        c = count.getAndDecrement();
        if (c > 1)
            notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
    if (c == capacity)
        signalNotFull();
    return x;
}

remove(Object o)

因为需要操作整个链表,因此需要同时拥有两个锁才能操作。

public boolean remove(Object o) {
    if (o == null) return false;
    // putLock.lock(); takeLock.lock();
    fullyLock();
    try {
        // 遍历查找
        for (Node<E> trail = head, p = trail.next;
            p != null;
            trail = p, p = p.next) {
            if (o.equals(p.item)) {
                unlink(p, trail);
                return true;
            }
        }
        return false;
    } finally {
        fullyUnlock();
    }
}

contains(Object o)

因为需要操作整个链表,因此需要同时拥有两个锁才能操作。

public boolean contains(Object o) {
    if (o == null) return false;
    // putLock.lock(); takeLock.lock();
    fullyLock();
    try {
        // 遍历查找
        for (Node<E> p = head.next; p != null; p = p.next)
        if (o.equals(p.item))
            return true;
        return false;
    } finally {
        fullyUnlock();
    }
}

总结

  • 底层是基于链表实现的。
  • 拥有两个锁:putLock 和 takeLock。分别对应添加和移除元素操作。
  • 与 putLock 和 takeLock 相对应的两个 condition 分别为 notFull 和 notEmpty。
  • offer(E e) 方法在队列已满的时候返回 false,put(E e) 方法在队列已满的时候阻塞。
  • poll() 方法在队列为空的时候返回 null,take() 方法在队列为空的时候阻塞。
  • remove(Object o) 和 contains(Object o) ,因为需要操作整个链表,因此需要同时拥有两个锁才能操作。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.