JVM执行引擎是Java虚拟机核心组件之一。物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统层面上,而虚拟机的执行引擎是自己实现的,可以自行制定指令集与执行引擎的结构体系,并且能够执行那些不被硬件直接支持的指令集格式。

运行时栈帧结构

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。

每一个栈帧都包括了局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的Code属性之中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。

StackFrame

局部变量表

局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。

局部变量表的容量以变量槽(Variable Slot,下称Slot)为最小单位,虚拟机规范中并没有明确指明一个Slot应占用的内存空间大小,只是很有导向性地说到每个Slot都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据,这8种数据类型,都可以使用32位或更小的物理内存来存放。

方法执行时,如果执行的是实例方法,那局部变量表中第0位索引的默认是this的引用,即实例本身。

注1:与虚拟机模型设计不同的是,执行引擎的实现为了节约局部变量表的空间,局部变量表的Slothi可以重用的。

LocalVariable

注2:局部变量定义了但没有赋初始值是不能使用的,因为局部变量的加载没有类加载的准备和初始化阶段。

操作数栈

操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出(Last In First Out, LIFO)栈。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到Code属性的max_stacks数据项中。操作数栈的每一个元素可以是任意的Java数据类型,包括long和double。

动态链接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接

方法返回地址

当一个方法开始执行后,只有两种方式可以退出这个方法。

第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口(Normal Method Invocation Completion)。

另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为异常完成出口(Abrupt Method Invocation Completion)。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者产生任何返回值的。

附加信息

调试信息等,属于虚拟机可以自由实现的部分。

方法调用

方法调用阶段是确定被调用方法版本的过程。Java的编译过程并不存在连接过程,是在JVM运行时进行动态调用的。

解析

在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法的调用称为解析(Resolution)。

  • invokestatic:调用静态方法。
  • invokespecial:调用实例构造器<init>方法、私有方法和父类方法。
  • invokevirtual:调用所有的虚方法。
  • invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象。
  • invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法,在此之前的4条调用指令,分派逻辑是固化在Java虚拟机内部的。其中只要能被invokestatic和invokespecial指令调用的方法(即非虚方法),都属于静态解析可以确定调用版本的,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。

分派Dispatch

分派调用过程是Java多态的一种基本体现,主要是有重载、重写两块。

静态分派

在编译阶段,依赖静态类型来定位方法执行版本的动作成为静态分派。典型应用是方法重载。但是,在很多情况下,重载版本并不唯一,所以虚拟机在运行时也会选更加合适的版本。

静态分派示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

package org.fenixsoft.polymorphic;

public class StaticDispatch {

static abstract class Human{

}

static class Man extends Human {

}

static class Woman extends Human {

}

public void sayHello(Human guy) {
System.out.println("hello, guy!");
}

public void sayHello(Man guy) {
System.out.println("hello, gentleman!");
}

public void sayHello(Woman guy){
System.out.println("hello, lady!");
}

public static void main(String[] args){
Human man = new Man();
Human woman = new Woman();
StaticDispatch sr = new StaticDispatch();
sr.sayHello(man);
sr.sayHello(woman);


// 实际类型变化,编译器并不能在编译时就这道,只能在运行时才可以确定的。
man = new Woman();
sr.sayHello(man);

// 静态类型变化
sr.sayHello((Woman) man);
}
}

1
2
3
4
hello, guy!
hello, guy!
hello, guy!
hello, lady!

重载方法匹配优先级代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package org.fenixsoft.polymorphic;

public class Overload {
public static void sayHello(Object arg) {
System.out.println("hello Object");
}

public static void sayHello(int arg) {
System.out.println("hello int");
}

public static void sayHello(long arg) {
System.out.println("hello long");
}

public static void sayHello(Character arg) {
System.out.println("hello Character");
}

public static void sayHello(char arg) {
System.out.println("hello char");
}

public static void sayHello(char... arg) {
System.out.println("hello char ...");
}

public static void sayHello(Serializable arg){
System.out.println("hello Serializable");
}

public static void main(String[] args) {
sayHello('a');
}
}

  • 代码输出:
    1
    hello char
  • 注释掉sayHello(char arg)方法,代码输出:
    1
    hello int
  • 注释掉sayHello(int arg),代码输出:
    1
    hello long
  • 注释掉sayHello(long arg),代码输出:
    1
    hello Character
  • 注释掉sayHello(Character arg),代码输出:
    1
    hello Serializable
  • 注释掉sayHello(Serializable arg),代码输出:
    1
    hello Object
  • 注释掉sayHello(Object arg),代码输出:
    1
    hello char ...
    这个示例生动的展示了JVM在运行时静态分派时,是从继承关系中从下往上开始搜索,越接近上层的优先级越低。即使方法调用传入的参数值为null,这个规则仍然适用。变长参数的重载优先级是最低的。

动态分派

动态分派是重写的重要体现。

动态分派示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package org.fenixsoft.polymorphic;

public class DynamicDispatch {
static abstract class Human {
protected abstract void sayHello();
}

static class Man extends Human {
@Override
protected void sayHello(){
System.out.println("man say hello");
}
}

static class Woman extends Human {
@Override
protected void sayHello(){
System.out.println("woman say hello");
}
}

public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello(); // invokevirtual #21 // Method org/fenixsoft/polymorphic/Dynamic-Dispatch$Human.sayHello:()V
woman.sayHello(); // invokevirtual #21
man = new Woman();
man.sayHello(); // invokevirtual #21
}
}

运行结果:

1
2
3
man say hello
woman say hello
woman say hello

从字节码的角度来看, sayHello()方法均是通过invokevirtual指令触发,但是最终的执行方法版本却完全不同,invokevirtual执行的运行时解析过程如下:

  1. 找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
  2. 如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang. IllegalAccessError异常。
  3. 否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。
  4. 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。

单分派与多分派

方法的宗量,即方法的接收者与方法的参数统称。可以有单宗量分派,即根据一个宗量对目标方法进行选择。也可以有多宗量分派,即根据多个宗量对目标方法进行选择。Java的静态分派属于多分派类型。JVM在运行时动态分派属于单宗量分派。

单分派和多分派代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class Dispatch {
static class QQ {}

static class _360 {}

public static class Father {
public void hardChoice(QQ arg) {
System.out.println("father choose qq");
}

public static hardChoice(_360 arg) {
System.out.println("father choose 360");
}
}



public static class Son {
public static hardChoice(QQ arg) {
System.out.println("son choose qq");
}

public static hardChoice(_360 arg) {
System.out.println("son choose 360");
}
}

public static void main(String[] args) {
Father father = new Father();
Father son = new Son();
// 静态分派:多宗量分派,方法接收者静态类型是Father,方法参数是_360 invokevirtual Father.hardChoice(360)
// 动态分派:单宗量分派,方法接收者实际类型是Father,执行 Father.hardChoice(360)
father.hardChoice(new _360());
// 静态分派:静态类型是Father,方法参数是QQ invokevirtual Father.hardChoice(QQ)
// 动态分派:实际类型是Son,执行Son.hardChoice(QQ)
son.hardChoice(new QQ());
}
}

运行结果:

1
2
father choose 360
son choose qq

虚拟机动态分派的实现

处于性能考虑,动态分派常用”稳定优化“手段:在类的方法区建立一个虚方法表(Virtual Method Table, vtable),和接口方法表(Interface Method Table, itable)。从而虚拟机不需要进行元数据查找,直接通过虚方法表确定应该执行的方法版本。

vtable

动态类型语言支持

动态语言的关键特征是它的类型检查的主体过程实在运行期而不是编译期,代码会更加简洁。而静态语言在编译器确定类型,最显著的好处是编译器可以提供严谨的类型检查,利于稳定性及代码达到更大规模。目前JVM支持的动态语言有Clojure, Groovy, Jython, JRuby等。

字节码解释执行引擎

本节探讨的是JVM将会如何对方法中的字节码进行解释执行的。

  • 传统编译过程是从程序源码到目标代码的一个过程,代表有C/C++语言。
  • Java是采用了现代的编译原理思路,把源码转化成抽象语法树,再由JVM进行解释执行,属于编译半独立实现。C#也是一种半独立实现的编译语言。
  • 而有些语言则将词法分析,抽象语法树,解释执行都封装在一起,例如JavaScript执行器,这类语言一般属于动态语言。

JIT

指令集架构

现在的指令执行主要有两种执行方式:

  1. 基于栈的指令集架构
    • 可移植
    • 执行速度相对较慢
  2. 基于寄存器的指令集架构
    • 执行速度快

Java是基于栈的指令集架构。

基于栈的解释器执行过程示例

Class文件结构

Class文件时一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据。

Class文件本身是由下图的这些数据类型组成,这些数据项之间并没有分隔符,而是通过约定好的规范和表结构填入对应的信息,从而将Java语言代码翻译成字节码。Class文件数据项包含如下这些类型。

ClassFileDataType

1. 魔数

第1~4字节:0xCAFEBABE

2. Class文件的版本

第5、6字节:次版本号(4x.0~4x.65535)

第7、8字节:主版本号 (45~)

3. 常量池

常量池入口放置一项u2类型的数据,代表常量池容量计数值,从1开始。0作为没有常量池的表述。

  • 字面量:接近Java语言层面常量概念,如文本字符串、声明为final的常量值等。

  • 符号引用:编译原理概念,包括三类常量:

    1. 类和接口的全限定名
    2. 字段的名称和描述符
    3. 方法的名称和描述符

常量池中每一项常量都是一个表。JDK中定义了14种结构的表结构数据,如下图所示:

ConstantFlag

表开始的第一位都是一个u1类型的标志位(binary 0000 0000 0000 0000 中某几位为1),代表本表属于哪种常量类型,而后则遵从常量表自己的格式填入数据,结构总表如下所示。

ConstantPool

4. 访问标志

常量池后面两个字节:以16个标志位识别类或接口层次的访问信息,包括:

1. Class是类还是接口;
2. 是否为public类型;
3. 是否为abstract类型;
4. 如果是类的话,是否被声明为final。

访问标志本身总共由16个标志位可以使用,具体代表如下图所示:

AccessFlag

5. 类索引、父类索引和接口索引集合

类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合,Class文件中由这三项数据确定这个类的全限定名。

类索引和父类索引都指向了一个CONSTANT_Class_info类,而接口所以则第一位是接口的个数,后面跟了相应个数的索引分别指向CONSTANT_Class_info类。

6. 字段表集合

字段表用于描述接口或者类中声明的变量。字段包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。

字段结构如下所示:

Field

字段的访问标志位有如下这些表格,标志位值和Class访问标志定义一样,但是支持的标志个数不一样:

FieldAccessFlag

  1. name_index保存的是对常量池CONSTANT_Utf8的引用,保存了方法简单名称。

    全限定名和简单名称很好理解,“org/fenixsoft/clazz/TestClass”是这个类的全限定名,仅仅是把类全名中的“.”替换成了“/”而已,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“;”表示全限定名结束。简单名称是指没有类型和参数修饰的方法或者字段名称,这个类中的inc()方法和m字段的简单名称分别是“inc”和“m”。

  2. 描述符引用指向了一个字段/方法描述符CONSTANT_Utf8。

    • 字段描述符,比如int实例变量的描述符是“I”;java.lang.Object 的实例描述符是 “Ljava/lang/Object;”,“double[][][]”的描述符为“[[[D”;

    • 方法描述符,比如Object mymethod(int i, double d, Thread t)的描述符为 (IDLjava/lang/Thread;)Ljava/lang/Object;。

    描述符支持如下类型:

    Descriptor

7. 方法表集合

方法表用于描述接口或者类中声明的方法,包括类级方法以及实例方法。表结构跟字段表相似,在此不再赘述。

Field

方法的访问标志与字段不同,如下图:

MethodAccessFlag

8. 属性表集合

属性表并不是单独存在的表,而是在Class文件、字段表、方法表都可以携带自己的属性表集合,用于描述某些场景专有的信息。属性表结构对后续属性进行了总表,结构如下:

AttrubuteInfo

目前虚拟机规范定义了下列属性:

Attrubute_info

字节码指令

Java编译器将代码的操作本身处理生成了字节码指令,放在了Code属性中,JVM读取指令进行执行。JVM支持的字节码指令和操作数如表格所示:

OptCode

Class加载时机

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的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

不会出发加载的三个被动引用例子:

  1. 通过子类引用父类的静态字段,不会导致子类初始化。
  2. 通过数组定义来引用类,不会出发此类的初始化。
  3. 常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义敞亮的类,因此不会触发定义常量的类的初始化。

Class加载过程

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中验证、准备、解析3个部分统称为连接(Linking),这7个阶段的发生顺序如下图所示。

ClassLoader

加载

  1. 通过一个类的全限定名来获取定义此类的二进制字节流

  2. 将字节流所代表的静态存储结构转化为方法区的运行时数据结构:

    • 如果是数组,JVM会直接创建数组类
      1. 引用类型的数组: 递归采用类加载过程去加载这个类型
      2. 如果是非引用类型的数组:JVM将会把数组标记为引导类加载器关联。
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口。

验证

  1. 文件格式验证

验证字节流是否符合Class文件格式的规范。

  1. 元数据验证

对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范要求。

  1. 字节码验证

通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

  1. 符号引用验证

发生在符号引用转化为直接引用的时候,是对类自身意外(常量池里的各种符号引用)信息进行匹配性校验。

准备

正式为变量分配内存并设置类变量为初始值阶段。引用类型的内存分配具体步骤参考JVM新对象创建

解析

虚拟机将常量池内的符号引用替换为直接引用的过程。

  • 符号引用:符号引用以一组符号来描述引用的目标,符号可与是任何形式的字面量,只要能无歧义的定义到目标即可。

  • 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。

解析动作主要针对如下:

  1. 类或接口 CONSTANT_Class_info
  2. 字段 CONSTANT_Fieldref_info
  3. 类方法 CONSTANT_Methodref_info
  4. 接口方法 CONSTANT_InterfaceMethodref_info
  5. 方法类型 CONSTANT_MethodType_info
  6. 方法句柄 CONSTANT_MethodHandle_info
  7. 和调用点限定符 CONSTANT_InvokeDynamic_info

这7类符号引用进行。

  • 类或接口的解析

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

    1. 首先将会对字段表内class_index项中索引的CONSTANT_Class_info符号引用进行解析,也就是字段所属的类或接口的符号引用。如果在解析这个类或接口符号引用的过程中出现了任何异常,都会导致字段符号引用解析的失败。
    2. 如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
    3. 否则,如果在C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
    4. 否则,如果C不是java.lang.Object的话,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
    5. 否则,查找失败,抛出java.lang.NoSuchFieldError异常。
  • 类方法解析

    1. 首先将会对字段表内class_index项中索引的CONSTANT_Class_info符号引用进行解析
    2. 类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现class_index中索引的C是个接口,那就直接抛出java.lang.IncompatibleClassChangeError异常。
    3. 如果通过了第1步,在类C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
    4. 否则,在类C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
    5. 否则,在类C实现的接口列表及它们的父接口之中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果存在匹配的方法,说明类C是一个抽象类,这时查找结束,抛出java.lang.AbstractMethodError异常。
    6. 否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError。
  • 接口方法解析

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

初始化

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

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

  • <clinit>()方法与类的构造函数(或者说实例构造器<init>()方法)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Test {
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>()方法中有耗时很长的操作,就可能造成多个进程阻塞,在实际应用中这种阻塞往往是很隐蔽的。

使用

卸载

Class加载器

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

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。

类的相等语义,只有在这两个类是由同一个类加载器加载的前提下才有意义,只要类加载器不同,即使是加载自同一个Class文件,两个类也是不等的。(相等指的是,Class对象的equals(), isAssignableFrom(), isInstance(), instantof 的返回结果。例如下面代码运行结果则是:

1
2
class org.fenixsoft.classloading.ClassLoaderTest
false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class ClassLoaderTest{
public static void main(String[] args) throw Exception {
// Class ClassLoader define and override
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throw ClassNotFoundException{
try {
String fileName = name.substring(name.lastIndextOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if(is == null){
return super.loadClasse(name);
}
byte[] b = new bye[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
}catch(IOException e){
throw new ClassNotFoundException(name);
}
}
};

Object obj = myLoader.loadClass("org.fenixsoft.classloading.ClassLoaderTest").newInstance();

System.out.println(obj.getClass());
System.out.println(obj instanceof org.fenixsoft.classloading.ClassLoaderTest);
}
}

类加载器类型

目前只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),C++语言实现,虚拟机自身的一部分;另一种就是所有其他类加载器,继承自抽象类java.lang.ClassLoader。

  • 启动类加载器(Bootstrap ClassLoader):前面已经介绍过,这个类将器负责将存放在\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那直接使用null代替即可,如下列代码清单所示为java.lang.ClassLoader.getClassLoader()方法的代码片段。
1
2
3
4
5
6
7
8
9
10
11
12
13
public ClassLoader getClassLoader() {
ClassLoader cl = getClassLoader0();
if(cl == null)
return null;
SecurityManager sm = System.getSecurityManger();
if(sm != null){
ClassLoader ccl = ClassLoader.getCallerClassLoader();
if(ccl != null && ccl != cl && !cl.isAncestor(ccl)){
sm.checkPermission(SecurityConstants.GET_CLASSLOADER_PERMISSION);
}
}
return cl;
}
  • 扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

  • 应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$App-ClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

双亲委派模型

图中展示的类加载器之间的这种层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码。

ParentDelegationModel

双亲委派模型的工作过程是:

  1. 所有的加载请求都委派给父类加载器去完成。
  2. 当父类加载器反馈自己无法完成加载请求,子加载器才会尝试自己加载。

双亲委派模型保证了Java程序优先从启动类加载器进行搜索加载,使得java.lang.Object类型在程序的各种类加载环境中都是同一个类,能够稳定运行程序。

双亲委派模型实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
//首先,检查请求的类是否已经加载过了
Class c = findLoadedClass(name);
if( c == null){
try {
if(parent != null){
// 委派父类加载器先去尝试加载
c = parent.loadClass(name, false);
}else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器抛出异常,说明父类加载器不能加载,因此调用子类进行加载
c = findClass(name);
}
}
if(resolve) {
resolveClass(c);
}
return c;
}

破坏双亲委派模型情况

  1. loadClass没有按照双亲委派模型进行实现: 目前已经不推荐重写loadClass方法,而是重写findClass方法,从而保证模型安全。

  2. 第三方JNDI接口提供者/SPI的代码不能被启动类加载器加载:线程上下文类加载器(Tread Context ClassLoader)。可以通过java.lang.Thread类的setContextClassLoader()进行设置。如果父类及应用全局都没有设置过,则默认就是应用程序类加载器。这样就让父类加载器请求子类加载器去加载SPI的代码。

  3. 为程序动态性的追求导致:代码热替换(HotSwap),模块热部署(Hot Deployment)等。每一个程序模块都有自己的类加载器,当需要更换一块程序模块是,就把这块代码以及类加载器一起换掉以实现代码的热替换。OSGi收到类加载请求时:

    1. 将以java.*开头的类委派给父类加载器加载。
    2. 否则,将委派列表名单内的类委派给父类加载器加载。
    3. 否则,将Import列表中的类委派给Export这个类的Bundle的类加载器加载。
    4. 否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
    5. 否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
    6. 否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
    7. 否则,类查找失败。

    此实现只有前两个点符合双亲委派模型,后面的都是平级的类加载器中进行。

本文将从JMM的理论模型和系统设计角度切入讲述并发工具的内存语义与实现细节。

JMM存在的目的

Java虚拟机规范中试图定义一种Java内存模型来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各个平台下都能达到一致的内存访问效果。

JMM

JVM内存模型操作

主内存操作

  • lock:将一个变量表示为一条线程独占的状态。
  • unlock: 将一个处于锁定状态的变量释放,释放后的变量才可以被其他线程锁定。
  • read: 将一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
  • write: 将store操作从工作内存中得到的变量值放入主内存的变量中。

工作内存操作

  • load: 把read操作从主内存得到的变量值放入到工作内存的变量副本中。
  • use: 把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到需要使用变量复制的字节码指令时执行这个操作。
  • assign: 把一个执行引擎的接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store: 把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。

内存操作执行基本规则

  • 不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况出现。

  • 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。

  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。

  • 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说,就是对一个变量实施use、store操作之前,必须先执行过了assign和load操作。

  • 一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。

  • 如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。

  • 如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定住的变量。

  • 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。

JVM内存模型特性

  • 原子性 JVM对基本数据类型的访问读写(上述操作)是具备原子性的。
  • 可见性 当一个线程修改了共享变量的值,其他线程能够立刻知道这个修改。而volatile变量较普通变量能够保证多线程场景下线程在每次读写前都能刷新。
  • 有序性 本线程内,操作都是有序;多线程场景下,线程间操作是无序的。

Happen-Before先行发生法则

先行发生是JMM中定义的两项操作之前的偏序关系,如果说操作A先行发生于操作B,其实就是说发生操作B之前,操作A产生的影响能够被B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等。

具体体现:

  • 程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。

  • 管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而“后面”是指时间上的先后顺序。

  • volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序。

  • 线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。

  • 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。

  • 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。

  • 传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

指令重排

编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。

单线程重排序:

  • 数据依赖性:程序的任意两个操作的执行是可能具有一定的依赖性,不能改变。
  • as-if-serial语义:单线程程序的执行结果不能改变。
  • 程序顺序规则: happens-before的顺序规则不能修改。

多线程重排序:

  • 顺序一致性模型:概念上模型只有一个单一的全局内存,所有操作线程在每一步操作后看到的内存内容都是一致的。实际上并不能完全保证,只能保证同步程序在进出临界区内代码各个线程的内存视图能够一致。

内存屏障

硬件层的内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。
内存屏障有两个作用:

  1. 阻止屏障两侧的指令重排序;
  2. 强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。

对于Load Barrier来说,在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制从新从主内存加载数据;

对于Store Barrier来说,在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。

java内存屏障

java的内存屏障通常所谓的四种即LoadLoad,StoreStore,LoadStore,StoreLoad实际上也是上述两种的组合,完成一系列的屏障和数据同步功能。

  • LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
  • StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
  • LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
  • StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能

Java同步工具的内存语义及实现

锁的内存语义及实现

锁的语义决定了临界区代码的执行具有原子性。

内存语义

锁的释放可以让线程向获取同一个锁的线程发送消息。
锁的获取可以让线程对应的内存失效使得临界代码必须从主内存获取共享变量。

实现细节

公平锁获取通过AbstractQueuedSynchronizer即AQS实现,通过一个整型的volatile变量state来维护同步状态。拿锁时,tryAcquire方法会查看state值是否为0,即无锁状态,并将state值设置为传入变量acquires,如果state不为0,且owner不是current线程,则返回false.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected final boolean tryAcquire(int acquires){
final Thread current = Thread.currentThread();
int c = getState();
if(c == 0) {
if( isFirst(current)) && compareAndSetState(0, acquires) {
setExclusiveOwnerThread(current);
return true;
}
}
else if(current == getExclusiveOwnerThread()){
int nextc = c + acquires;
if(nextc < 0)
throw new Error("Max lock count exceeded");
setState(nextc);
return true;
}
return false;
}

非公平锁的获取不需要tryAcquire方法中通过isFirst(current))方法进行竞争,而是直接调用compareAndSetState(int expect, int update)。

(非)公平锁释放通过tryRelease(int releases)实现:

1
2
3
4
5
6
7
8
9
10
11
12
protected final boolean tryRelease(int releases){
int c = getState() - releases;
if( Thread.currentThread() != getExclusiveOwnerThread())
throw new IlleagalMonitorStateException();
boolean free = false;
if( c == 0){
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}

CAS 内存语义及操作内容

内存语义

CAS更新操作,同时具有volatile读和volatile写的内存语义。

操作内容

CAS是处理器的一种操作,是native方法API。

  1. 确保对内存读-改-写的原子性。
  2. 禁止CAS指令前后读写指令重排。
  3. 把缓存区的所有数据刷新到内存中。

volatile 内存语义及实现

内存语义

volatile写与锁的释放有相同的内存语义,volatile读与锁的获取有相同内存语义。

实现细节

  • 通过插入内存屏障,来组织编译器/操作系统进行指令重排序。

  • 通过关联读/写操作和使用操作(用之前必须从主内存读,assign后必须写入主内存,以及写happens-before读规则)强制CPU的缓存失效来保证内存可见性。
    volatile的内存屏障策略如下:

  • 在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障;
    volatile-write

  • 在每个volatile读操作后分别插入LoadLoad屏障,和LoadStore屏障;
    volatile-read

由于内存屏障的作用,避免了volatile变量和其它指令重排序、线程之间实现了通信,使得volatile表现出了锁的特性。

volatile强制缓存失效策略如下:

  • 线程的Load、read和Use进行关联:只有当线程T对变量V执行的前一个动作是load的时候,线程T才能对变量V执行use动作;并且,只有当线程T对变量V执行的后一个动作是use的时候,线程T才能对变量V执行load动作。线程T对变量V的use动作可以认为是和线程T对变量V的load、read动作相关联,必须连续一起出现(这条规则要求在工作内存中,每次使用V前都必须先从主内存刷新最新的值,用于保证能看见其他线程对变量V所做的修改后的值)。

  • Assign和所有线程的store,write进行关联只有当线程T对变量V执行的前一个动作是assign的时候,线程T才能对变量V执行store动作;并且,只有当线程T对变量V执行的后一个动作是store的时候,线程T才能对变量V执行assign动作。线程T对变量V的assign动作可以认为是和线程T对变量V的store、write动作相关联,必须连续一起出现(这条规则要求在工作内存中,每次修改V后都必须立刻同步回主内存中,用于保证其他线程可以看到自己对变量V所做的修改)。

  • 不同变量的上述的两段操作顺序一致假定动作A是线程T对变量V实施的use或assign动作,假定动作F是和动作A相关联的load或store动作,假定动作P是和动作F相应的对变量V的read或write动作;类似的,假定动作B是线程T对变量W实施的use或assign动作,假定动作G是和动作B相关联的load或store动作,假定动作Q是和动作G相应的对变量W的read或write动作。如果A先于B,那么P先于Q(这条规则要求volatile修饰的变量不会被指令重排序优化,保证代码的执行顺序与程序的顺序相同)。

    Happens before法则: 前一个操作的执行结果要对第二个操作可见。

final 内存语义与实现细节

final关键字可以放在static域,实例成员域,和局部变量三种变量前。其中final修饰的局部变量的可以作为线程的局部变量传递给子线程。也能保证并发情况下的内存语义。

内存语义

对于final域,编译器和CPU会遵循两个重排序规则:

  • 新建对象过程中,构造体中对final域的初始化写入和这个对象赋值给其他引用变量,这两个操作不能重排序;(废话嘛)
  • 初次读包含final域的对象引用和读取这个final域,这两个操作不能重排序;(晦涩,意思就是先赋值引用,再调用final值)

总之上面规则的意思可以这样理解,必需保证一个对象的所有final域被写入完毕后才能引用和读取。这也是内存屏障的起的作用:

实现细节

  • 写final域:在编译器写final域完毕,构造体结束之前,会插入一个StoreStore屏障,保证前面的对final写入对其他线程/CPU可见,并阻止this指针赋值与final域写被重排序(this = new Object(){ finalField = …})。(如果没有,普通域的写可以被重排到构造函数外)
    final-write

  • 写final域的成员域:构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
    final-element-write

  • 读final域:在上述规则2中,两步操作不能重排序的机理就是在读final域前插入了LoadLoad屏障,这个阻止了读取this引用和读取final域的重排序(isntance.finalField)。
    final-read

X86处理器中,由于CPU不会对写-写操作进行重排序,所以StoreStore屏障会被省略;而X86也不会对逻辑上有先后依赖关系的操作进行重排序,所以LoadLoad也会变省略。

只要对象是正确构造的(被构造对象的引用在构造函数中没有“逸出”),那么不需要使用同步(指 lock 和 volatile 的使用),就可以保证任意线程都能看到这个 final 域在构造函数中被初始化之后的值。

this逸出代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

public class FinalReferenceEscapeExample {
final int i;
static FinalReferenceEscapeExample obj;
public FinalReferenceEscapeExample () {
    i = 1;                              //1写final域
    obj = this;                          //2 this引用在此“逸出”
}
public static void writer() {
    new FinalReferenceEscapeExample ();
}
public static void reader {
    if (obj != null) {                    //3
        int temp = obj.i;                //4
    }
}
}

Concurrent包的内存语义及实现

Concurrent包底层实现依赖如下图所示:

Cocurrent

延迟初始化问题讨论

延迟初始化是在需要实例的时候再进行初始化,从而达到提升程序初始化性能的目的。然而延迟初始化需要考虑多线程并发访问,和指令重排序问题。

静态域延迟初始化

静态域的延迟初始化能通过final关键词实现,因为final静态域能保证多线程安全初始化,同事也能保证computeFieldValue()方法不会溢出FieldHolder的构造方法。

1
2
3
4
5
6
private static class FieldHolder {
static final FieldType field = computeFieldValue();
}
private static FieldType getField(){
return FieldHolder.field;
}

成员域延迟初始化

单重检查模式

单重检查模式能够确保大多数情况的fiel的同步,但是当computeFieldValue()执行和field赋值可以重排序,导致在第一次检查时其他线程可能看到不完整的field值,并返回。

1
2
3
4
5
6
7
8
9
private volatile FieldType field;

private FieldType getField(){
FieldType result = field;
if(result == null){
field = result = computeFieldValue();
return result;
}
}

双重检查模式

双重检查模式通过synchronized和volatile的内存语义,3, 4对其他线程可见,且其他线程在1处的读不会重排序到2语块的内部,能够确保在线程更新field值时,与其他线程查看field值之间的读写能够顺序执行。

局部变量result的使用能够保证尽量少次数的访问field和取锁,提升运行效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private volatile FieldType field;

private FieldType getField(){
FieldType result = field;
if(result == null){
result = field;
if(result == null){ // 1
synchornized(this){ // 2
field = result = computeFieldValue(); //3, 4
}
}
}
return result;
}

线程概念

线程是一种轻量级的进程内的执行单元,线程共用进程中的内存地址空间,但是拥有自己的调用栈,寄存器,程序计数器和局部变量。线程状态如下:

ThreadStatus

  1. 新建状态(New) : 线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
  2. 就绪状态(Runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
  3. 运行状态(Running) : 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
  4. 阻塞状态(Blocked) : 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
    1. 等待阻塞 – 通过调用线程的wait()方法,让线程等待某工作的完成。
    2. 同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
    3. 其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
  5. 死亡状态(Dead/Terminated) : 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

多线程概念

多线程是利用线程并行和并发处理的优势提升程序性能的一种编程方法。

优点:

  1. 可以使每个线程做自己的任务,代码上语义更明确
  2. 利用多核CPU的优势
  3. 可以把占据时间长如阻塞UI的任务放到后台处理从而保证界面/线程响应
  4. 可以提升CPU利用率,通过回调方式而不是阻塞方式处理IO操作

缺点:

  1. 线程安全问题(脏数据,死锁)
  2. 性能问题(活锁,饥饿,上下文切换开销)
  3. 线程本身需要更多的内存

线程管理的基本工具(及Executor框架)

创建一个新线程

  1. Thread类是一个实体类,继承Thread类,重写RUN方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 也可以 public class MyThread extends Thread {
    public class MyThread implements Runnable{
    static Object o = new Object();
    static int number = 0;

    @Override
    public void Run(){
    for(int i = 0; i < 1000;i++){
    sychronized(o){
    number++;
    }
    }
    }
    }
  2. 实现RUNABLE 接口,实例对象作为THREAD的构造函数的传参。

    1
    2
    Thread t = new Thread(new MyThread());
    t.start();
  3. 实现CALLABLE接口,通过FUTURETASK来创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

public class MyCallable implements Callable<String> {

@Override
public String call() throws Exception {
Thread.sleep(1000);
//return the thread name executing this callable task
return Thread.currentThread().getName();
}

public static void main(String args[]){

Callable<String> oneCallable = new MyCallable();
FutureTask<String> oneTask = new FutureTask<String>(oneCallable);

Thread t = new Thread(oneTask);
System.out.println(Thead.currentThread().getName());

t.start();
}

}
  1. 通过线程池本身ThreadPoolExecutor进行task管理。线程池是一个功能强大的多线程工具,在每一个新的Runnable提交的时候,会有如下流程处理:Workflow
    在线程池处理task过程中,会需要如下参数控制流程:ThreadPool.jpg

  2. 通过线程池工厂Executors生成ExecutorService接口下的实例,创建并执行任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const int POOL_NUM = 5;
// 通过 Executors 静态方法创建一个线程池
ExecutorService es = Executors.newFixedThreadPool(POOL_NUM);


for(int i = 0; i < POOL_NUM); i++) {
es.execute(oneTask);
//Future<String> anotherTask = es.submit(oneCallabe);也可以执行
}
//类似Task in c#
while(oneTask.isDone()){
System.out.println(oneTask.get());
}
es.shutdown();

通过Object方法控制线程并发

wait/notify(wait(), await(), notify() notifyAll())是Object的方法,运用了实例本身的锁功能控制多线程的并发访问和修改问题。
* wait的语义是释放当前拿到的锁,让本线程进入睡眠状态。
* notify的语义是通知其他线程唤醒,让原本是waiting状态的线程变成了blocked(同步块中释放锁,重新等待拿锁恢复同步块内代码wait()后继续执行)。

一般的应用场景是,wait释放锁,notify别的线程来拿锁,并唤醒继续执行。这些方法必须在已经获得锁的同步块中书写,否则会抛出illeagalmonitorStateException。这属于线程的基本工具,一般推荐使用已有的并发框架,而非此类方法。

1
2
3
4
5
6
7
8
9
10
11
// the standard idiom for using the wait method
synchonized(o){
// 防止线程被无意唤醒,需要while loop保证代码安全
while(!condition){
o.wait();
// do something
// notifyAll能保证需要被唤醒的线程的活性。如果都在等待同一个条件,可以用notify()
o.notifyAll();
}
// do something when condition is fulfilled.
}

线程调度器Scheduler

  1. static Thread.yeild():可以让步出当前线程的优先级,让其他同优先级的线程先跑。
  2. Thread.join(): 当前线程等待一个线程t(join的实例)完成后再继续执行。类似于
    1
    2
    3
    4
    5
    6
    7
    8
    synchronized(this){
    // if t is alive, keep waiting
    while(isAlive()){
    wait();
    }
    // continue current thread
    }

  3. static Thread.sleep(): 保持拿锁,线程睡眠一定时间。与wait()不同之处在于前者锁并没有释放。
  4. Thread.interrupt(): 调用中断的线程去中断别的线程,被中断的线程如果处于等待/睡眠状态,会抛出InterrupException,如果处于阻塞于IO状态,会抛出ClosedByInterruptException,并且连接中断,如果阻塞与selector,则会出发selector’s wakeup方法,并且状态Thread.interrupted()变成true。其他情况,直接Thread.isInterrupted()/static Thread.interrupted()变成true。在异常处理完毕后,线程的中断标志位会复位,从而允许再一次中断。
  5. Deprecated方法有suspend(),resume(),stop()能让线程暂停,恢复运行和完全停止。需要留意的是线程被suspend和stop并不会释放线程已经拿到的锁,所以不是一个很好的终止线程的方法,而中断则可以在异常捕捉处理好锁释放,资源释放的逻辑。

线程通信

  1. volatile, synchronized(略)

  2. wait/notify(略)

  3. pipewriter/pipereader:将管道的输出连接到其他线程的输入从而达到通信目的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    public class Piped{
    public static void main(String[] args){
    PipedWriter out = new PipedWriter();
    PipedReader in = new PipedReader();
    //连接
    out.connect(in);

    Thread printThread = new Thread(new Print(in), "PrintThread");
    printThread.start();

    int receive = 0;
    try{
    while((receive = System.in.read()) != -1){
    out.write(receive);
    }
    } finally{
    out.close();
    }
    }

    static class Print implements Runnable{
    private PipedReader in;
    public Print(PipedReader in){
    this.in = in;
    }

    public void run(){
    int receive = 0;
    try{
    while((receive = in.read()) != -1){
    System.out.print((char) receive);
    }
    }catch(IOException ex){

    }
    }
    }
    }
  4. join(略)

  5. ThreadLocal: ThreadLocal集合类型可以为线程提供局部变量,在多线程场景下,保证线程自由变量的安全。常用于AOP代码,例如计时器。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public class Profiler{
    private static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal<Long>(){
    protected Long initialValue(){
    return System.currentTimeMillis();
    }
    };

    public static final void begin(){
    TIME_THREADLOCAL.set(Stystem.currentTimeMillis());
    }

    public static final long end(){
    return System.currentTimeMillis() - TIME_THREADLOCAL.get();
    }

    public static void main(String[] args){
    Profiler.begin();
    TimeUnit.SECONDS.sleep(1);
    System.out.println("Cost: " + Profiler.end() + " mills");
    }
    }
  6. Exchanger类: Exchanger是一个用于线程间协作的工具类。Exchanger创建了一个同步点,当两个线程都到达同步点时,数据进行交换。应用场景如下:

  • 遗传算法(1/2基因数据交换)

  • 校对工作(AB岗两人进行录入对稿)

    Exchanger使用示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    public class ExchangerTest {
    private static final Exchanger<String> exgr = new Exchanger<String>();
    private static ExecutorService threadPool = Executors.newFixedThreadPool(2);

    public static main(String[] args) {
    threadPool.execute(new Runnable() {
    @Override
    public void run() {
    try{
    String A = "流水数据A";
    //A录入银行流水数据
    exgr.exchange(A);
    }catch (InterruptedException e) {
    }
    }
    });

    threadPool.execute(new Runnable() {
    @Override
    public void run() {
    try{
    String B = "流水数据B";
    //B录入银行流水
    String A = exgr.exchange(B);
    System.out.println("A和B数据是否一致:" + A.equals(B));
    }catch (InterruptedException e){}
    }
    });

    threadPool.shutdown();
    }
    }

线程上下文管理工具

  1. Executor框架

    Executor框架定义了各个Task在线程池/单线程执行的上下文。Developer可以自己实现Executor框架来自定义Task执行上下文。Executor支持的线程池框架有ThreadPoolExecutor和ScheduleThreadPoolExecutor,主要负责处理相对独立的任务。

    在新线程执行

    1
    2
    3
    4
    5
    6
    7
    public class ThreadPerTask implements Executor {

    public void execute( Runnable task){
    new Thread(task).start();
    }
    }

    在同一个线程执行

    1
    2
    3
    4
    5
    public class InThreadTask implements Executor {
    public void execute( Runnable task){
    task.run();
    }
    }

    ExecutorService接口扩展了Executor接口,支持更加多的接口去控制executor的周期。当ExecutorService被shutdown()后,不再接受submit(),当其执行完所有task后,就终止。

    • Running ===>

    • Shutting down ===>

    • Terminated

    • CoompletionService

    Executor和BlockingQueue的组合。

  2. Fork/Join框架

    Fork/Join框架适合大量task并发执行,由于task可以在不同的线程进行根据空闲程度自由调度,所以具有特定的执行设计场景(一般是只读场景):

    1. 归并排序
    2. map/reduce
    3. 递归多线程计算

    ForkJoinPool线程池是Fork/Join执行框架的线程池,也是Executor框架的一种。也是CompletableFuture的底层线程池实现。

    Fork/Join使用示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    public class CountTask extends RecursiveTask<Integer> {
    private static final int THRESHOLD = 2; //阈值
    private int start;
    private int end;

    public CountTask(int start, int end) {
    this.start = start;
    this.end = end;
    }

    @Override
    protected Integer compute() {
    int sum = 0;

    //如果任务足够小就计算任务
    boolean canCompute = (end - start) <= THRESHOLD;
    if ( canCompute) {
    for (int i = start; i <= end; i++) {
    sum += i;
    }
    }else {
    //如果任务大于阈值,就分裂成两个子任务计算
    int middle = (start + end) / 2;
    CountTask leftTask = new CountTask(start, middle);
    CountTask rightTask = new CountTask(middle + 1, end);

    //执行子任务
    leftTask.fork();
    rightTask.fork();
    //等待子任务执行完,并取到结果
    int leftResult = leftTask.join();
    int rightResult = rightTask.join();
    //合并结果
    sum = leftResult + rightResult;
    }
    return sum;
    }

    public static void main(String[] args){
    ForkJoinPool forkJoinPool = new ForkJoinPool();
    //生成一个计算任务,负责计算1+2+3+4
    CountTask task = new CountTask(1, 4);
    //执行任务
    Future<Integer> result = forkJoinPool.submit(task);
    try {
    //检查task执行结果
    if(task.isCompletedAbnormally()) {
    System.out.println(task.getException());
    }
    System.out.println(result.get());
    } catch (InterruptedException e) {
    } catch (ExecutionException e) {}
    }
    }

  3. BlockingQueue阻塞队列

提供两个可以阻塞当前线程的方法扩展Queue的操作,常用于生产者和消费者场景。

  • take() 移除元素,当队列为空时,获取元素的线程会等待队列变为非空。
  • put() 增加元素,当队列满的时候,队列会阻塞插入元素的线程,直到队列不满。

BlockingQueues

阻塞队列支持四种处理方式,如上图所示,包含7个阻塞队列:

  1. ArrayBlockingQueue, 由数组结构组成的有界阻塞队列。
  2. LinkedBlockingQueue, 由链表结构组成的有界阻塞队列。
  3. PriorityBlockingQueue, 支持优先级排序的无界阻塞队列。
  4. SynchronousQueue,一个不存储元素的阻塞队列。
  5. DelayQueue, 使用优先级队列实现的无界阻塞队列。
  6. LinkedTransferQueue, 链表结构组成的无界阻塞队列。
  7. LinkedBlockingDeque, 链表结构组成的双向阻塞队列。

线程同步器Synchronizer

同步器是使线程能够等待另一个线程,允许他们协调动作的工具类。

  1. 信号量Semaphore

信号量是用来控制同时访问特定资源的线程数量的锁,某个时候只能由n个线程同时访问该同步资源,n=1时候信号量和简单互斥锁一样。

  1. CountDownLatch

CountDownLatch允许一个或多个线程等待其他线程完成操作。当这n个线程都完成时,当前等待线程再执行, CountDownLatch内部的计数器不能重置。

  1. CyclicBarrier

CyclicBarrier,让一组线程到达一个同步点后再一起继续运行,在其中任意一个线程未达到同步点,其他到达的线程均会被阻塞。

  1. Phaser

Phaser可以理解为CyclicBarrier的更复杂应用,通过控制每个阶段的锁来控制线程行为。Phaser在n个线程完成一个阶段后才进入下一个多线程阶段。

  1. Condition 类

Condition类提供了wait(), notify(), notifyAll()接口方法,可以灵活制定锁的行为,同时避免了锁和object的一对一对应关系。condition.wait()释放锁等待,condition.signal()唤醒等待的线程(需要尽快释放锁保证notify成功)。Condition类是BlockingQueue实现的关键类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();

final Object[] items = new Object[100];
int putptr, takeptr, count;

public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}

public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await();
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
  1. LockSupport 类
    LockSupport定义了一组公共静态方法,是一个基础工具,提供了线程阻塞和唤醒功能。
方法名 描述
park() 阻塞当前线程,如果调用unpark(),park()才会返回
parkNanos(long nanos 阻塞当前线程不超过nanos秒
parkUntil(long deadline) 阻塞当前线程,直到deadline时间
unpark(Thread t) 唤醒处于阻塞状态的线程t

多线程问题

  1. 数据访问问题(读写)
  2. 资源生产与消费问题(生产者-消费者模式)<= 常用来解决数据的强耦合问题

下文会展开描述这两类问题。

多线程数据读写访问问题(同步/并发问题)

由于多线程情况下JVM的内存模型,实际上是存在主内存和工作内存之间的同步问题。读写操作实际上是read-load, store-write操作,在多线程并发时,操作的并发会导致主内存和工作内存某变量值的不同步问题。

synchronized 关键字

synchronized可以在三个地方使用,一种是在方法体内部,可以进行instance level或者class level(通过synchronized(AccountSync.Class))进行锁定。最后一种是在一个代码块进行锁定,可以指定拿锁的object。

  • 当synchronized锁定在(静态)方法级别,所有(静态)method只能有一个method被线程调用,其他线程需要等待。同一个线程可重入synchronized区块/方法。

  • 当synchronized锁定在instance级别时,取决于instance的状态,所有需要拿该instance的线程需要等待。也就是说,即使是同一个instance的不同的synchronized method,在某时刻,只能有一个线程访问其中某个synchronized method。

  • 当synchronized锁定在block级别是,需要制定block的owner,即一个object,只有拿到了该object的锁,才能执行相应的block。只是比较推荐的方法,因为可以最小化同步块,同时也能避免基类方法和父类方法的访问造成的“互相绊住脚”的行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class AccountSync {
Object o = new Object();

public static void main(String []){
synchronized(o){
//synchronized block

}
}

static synchronized void myMethod(){
// lock class level access
}
}

Lock是java的一个interface,所有实现了该接口的类型都具有锁的特质。

  1. 不可重入锁(简单锁)
    Java底层为每个object提供了mutex,没有拿到锁的线程需要忙等待,没有优先级控制。
    简单锁本身在java内部没有直接的实现,可以通过Semaphore计数值为1来实现。

  2. 可重入锁ReentrantLock
    同一个线程可以重复进入该锁(不会因为同步代码自己调用自己而被死锁)

1
2
3
4
5
6
7
8
Lock l = new ReentrantLock();
l.lock();
try {
// do something
} finally {
// must in finally 解锁。
l.unlock();
}
  1. 读写锁
    读写锁是两个锁,分别对应
    ReentrantReadWriteLock.ReadLock,ReentrantReadWriteLock.WriteLock

读锁,没有线程hold写锁的时候,写锁可以给出,或者读锁可以给多线程访问。
写锁,没有线程hold读锁和写锁的时候,读锁可以给出,一旦写锁先被hold,读锁是不允许再被hold。

读写锁的特性如下:

RWLock.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ReadWriteMap{
final Map<String, Data> m = new TreeMap<String, Data>();

final ReentrantReadWriteLock rw1 = new ReentrantReadWriteLock();
final Lock r = rw1.readLock();
final Lock w = rw1.writeLock();

public Data get (String key){
r.lock();
try {
return m.get(key)
} finally { r.unlock();}
}

public Data put (String key, Data value){
w.lock();
try{
return m.put(key, value);
} finally{
w.unlock();
}
}

并发集合Concurrent collection

  1. ConcurrentHashMap

https://www.cnblogs.com/ITtangtang/p/3948786.html

HashMap并发问题:

HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法时,其他线程访问HashTable的同步方法时,可能会进入阻塞或轮询状态。如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,并且也不能使用get方法来获取元素,所以竞争越激烈效率越低。

ConcurrentHashMap解决并发问题:

ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。这里“按顺序”是很重要的,否则极有可能出现死锁,在ConcurrentHashMap内部,段数组是final的,并且其成员变量实际上也是final的,但是,仅仅是将数组声明为final的并不保证数组成员也是final的,这需要实现上的保证。这可以确保不会出现死锁,因为获得锁的顺序是固定的。

ConcurrentHashMap的使用和HashMap相同,API如下:

|API名|操作含义|
|:---|:---:|
|get()|拿key对应的value|
|put()|放元素V|
|size()|获取大小|
|remove()|删除元素|
|putIfAbsent()|存放元素如果没有|
  1. ConcurrentLinkedQueue
    线程安全队列有两种实现方式,一种是阻塞算法加锁,另一种是使用循环CAS的方式。ConcurrentLinkedQueue采用的是后者,基于链接节点的无界限线程安全队列。

offer()是入队,将节点添加到队尾。
poll()是出队,将首节点拿出。

减少死锁优化

  • 减少锁的持有时间(同步方法改同步块)
  • 减少锁的粒度(CONCURRENT HASHMAP)
  • 锁分离(读写锁,读锁写锁)(LinkedBlockingQueue,PUT一把锁,锁尾巴,TAKE一把锁锁头)

无锁并发工具

在多核操作系统中,Java提供了很多无锁并发工具。没有需要加锁的需要,避免了developer自己处理线程的阻塞行为,减少这部分开销。原来某些需要锁的场景,通过限制计算机指令执行和强制线程内部缓存失效,可以达到并发读写的需求,而不需要用锁来控制线程访问。

  1. volatile 关键字

volatile的读写,可以看作是一个锁,对该变量的读写操作进行了同步。概括的说,保证内存可见性,防止指令重排序。

  1. final 关键字

凡是对成员变量或者本地变量(在方法中的或者代码块中的变量称为本地变量)声明为final的都叫作final变量。final变量经常和static关键字一起使用,作为常量。final变量能保证其初始化的同步操作。

  1. 原子操作

原子操作是指不受多线程影响的最基本单元操作,可以保证同步。
Java在JVM层面也支持了同步,lock-free操作,利用CAS无所算法和乐观锁假设(仅在修改数据时候检查锁状态,适合并发修改比较少的情况)。底层是基于读写锁实现。

CAS是利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法,实现原子操作。是直接调用CPU 的cmpxchg(是汇编指令)指令。

CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

下列操作为原子操作:

  1. all assignment of primitive types except for long and double. (并不是说primitive types的所有操作都是原子操作)
  2. all assignment of references
  3. all operations of Java.Concurrent.Atomic.* classes
  4. all assignments to volatile longs and doubles

原子类型by Java

java.util.concurrent.atomic包提供了primitive类型的atomic类,它们可以自动的保证对于他们的操作是原子的并且不需要使用同步。但是使用方法和primitive类型完全不同,atomic类型里提供了各种操作方法保证方法/方法流执行的原子性。

  • AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference

通过原子的方式更新数组里的某个元素,Atomic包提供了以3类

  • AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

如果需原子地更新某个类里的某个字段时,就需要使用原子更新字段类,Atomic包提供了以下3个类进行原子字段更新。

  • AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

public class AtomicIntegerFieldUpdaterTest {
// 创建原子更新器,并设置需要更新的对象类和对象的属性
private static AtomicIntegerFieldUpdater<User> a =
AtomicIntegerFieldUpdater.newUpdater(User.class, "old");

public static void main(String[] args) throws InterruptedException {
// 设置柯南的年龄是10岁
User conan = new User("conan", 10);
// 柯南长了一岁,但是仍然会输出旧的年龄
System.out.println(a.getAndIncrement(conan));
// 输出柯南现在的年龄
System.out.println(a.get(conan));
}

public static class User {
private String name;
public volatile int old;

public User(String name, int old) {
this.name = name;
this.old = old;
}

public String getName() {
return name;
}

public int getOld() {
return old;
}
}
}

多线程的资源生产与消费问题

Java的Vector容器实际上是线程安全的数据结构,也就是说Vector的各个操作都能保证其原子性,但是也会存在多线程问题。这是因为在Vector的删除和添加操作中,如果存在多线程并发,删除操作的对象是可能是一个空Vector容器,造成问题。Developer需要有很好的上下文控制来避免这种生产消费问题。

多线程管理工具

Object方法

线程调度器

线程同步器

线程上下文管理工具

题外话:C#多线程工具

Java Streams

Java在JDK1.8中引入了Stream API,支持对流的处理。流处理类似于对于数据库数据流进行只读操作后求得某种结果,有如下特点:

Java-Streams

  1. stream不存储数据
  2. stream不改变源数据
  3. stream的延迟执行特性

Stream API 简述

创建Stream

Stream.of(Collection collections)时Stream类的静态方法,可以将集合数据转化为Stream。

1
2
3
4
5
//Stream.of()
Stream<Student> stream = Stream.of(stuArr);
//Arrays.stream
int[] arr = new int[]{1,2,34,5};
IntStream intStream = Arrays.stream(arr);

Stream操作

Reference Link: https://www.cnblogs.com/CarpenterLee/p/6545321.html

操作类型 接口方法
中间操作 concat() distinct() filter() flatMap() limit() map() peek() skip() sorted() parallel() sequential() unordered()
结束操作 allMatch() anyMatch() collect() count() findAny() findFirst() forEach() forEachOrdered() max() min() noneMatch() reduce() toArray()

对于接口方法的传入参数,是各种函数接口,可以用lamda表达式方便的书写。下面介绍几个经典的API使用:

  1. forEach()

forEach(Consumer<? super E> action)

1
2
3
// 使用Stream.forEach()迭代
Stream<String> stream = Stream.of("I", "love", "you", "too");
stream.forEach(str -> System.out.println(str));
  1. filter()

filter(Predicate<? super E> predicate)

1
2
3
4
// 保留长度等于3的字符串
Stream<String> stream= Stream.of("I", "love", "you", "too");
stream.filter(str -> str.length()==3)
.forEach(str -> System.out.println(str));
  1. map()

Stream map(Function<? super T,? extends R> mapper)

1
2
3
Stream<String> stream = Stream.of("I", "love", "you", "too");
stream.map(str -> str.toUpperCase())
.forEach(str -> System.out.println(str));
  1. reduce()

reference Link: http://www.cnblogs.com/CarpenterLee/p/6550212.html

  • Optional reduce(BinaryOperator accumulator)
  • T reduce(T identity, BinaryOperator accumulator)
  • <U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)
1
2
3
4
5
6
// 找出最长的单词

Stream<String> stream = Stream.of("I", "love", "you", "too");
Optional<String> longest = stream.reduce((s1, s2) -> s1.length()>=s2.length() ? s1 : s2);
//Optional<String> longest = stream.max((s1, s2) -> s1.length()-s2.length());
System.out.println(longest.get());
  1. collect()
  • R collect(Supplier supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)
  • <R,A> R collect(Collector<? super T,A,R> collector)
1
2
3
4
5
// 将Stream规约成List
Stream<String> stream = Stream.of("I", "love", "you", "too");
List<String> list = stream.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);// 方式1
//List<String> list = stream.collect(Collectors.toList());// 方式2
System.out.println(list);

LINQ in C#

与Java语言相比,C#引入了LINQ,lamda表达式和扩展方法来更好的支持chaining operation。LinQ支持所有实现了Enumberable接口的类型。

  1. ForEach()

    .ForEach(Action action)

1
2
3
4
5
6
List<string> stringList = new List<string>();
stringList.Add("I");
stringList.Add("love");
stringList.Add("you");
stringList.Add("too");
stringList.ForEach(a => Console.WriteLine(a));
  1. Where()

    .Where(Func<T, bool> function)

1
2
stringList.Where( x=> x.Length == 3)
.ForEach( x=> Console.WriteLine(x));
  1. Select()

    .Select(Func<T, int, R> function)

1
2
stringList.Selct((x, i) => x.ToUpper())
.ForEach( x=> Console.WriteLine(x));
  1. Aggregate()

    .Aggregate(A, Func<A, T, A> function, Func<A, R> function2)

1
stringList.Aggregate("", (cur, next) => cur.Length > next.Length ? cur : next, x => Console.WriteLine(x));
  1. 扩展方法,Java collect()方法是.NET扩展方法功能的一种实现,C#通过定义扩展方法更好的支持LINQ的Chaining功能,使得Java Collectors的接口都可以直接通过相似的方法实现。
1
stringList.Select(x=>x).toList();

Java集合框架

Reference Link: http://www.runoob.com/java/java-collections.html

集合框架被设计成要满足以下几个目标。

  • 该框架必须是高性能的。基本集合(动态数组,链表,树,哈希表)的实现也必须是高效的。
  • 该框架允许不同类型的集合,以类似的方式工作,具有高度的互操作性。
  • 对一个集合的扩展和适应必须是简单的。
    为此,整个集合框架就围绕一组标准接口而设计。你可以直接使用这些接口的标准实现,如下图所示,诸如: LinkedList, HashSet, 和 TreeSet 等,除此之外你也可以通过这些接口实现自己的集合。

JavaCollectionsUML

从上面的集合框架图可以看到,Java 集合框架主要包括两种类型的容器,一种是集合(Collection),存储一个元素集合,另一种是图(Map),存储键/值对映射。Collection 接口又有 3 种子类型,List、Set 和 Queue,再下面是一些抽象类,最后是具体实现类,常用的有 ArrayList、LinkedList、HashSet、LinkedHashSet、HashMap、LinkedHashMap 等等。

集合框架是一个用来代表和操纵集合的统一架构。所有的集合框架都包含如下内容:

  • 接口:是代表集合的抽象数据类型。例如 Collection、List、Set、Map 等。之所以定义多个接口,是为了以不同的方式操作集合对象
  • 实现(类):是集合接口的具体实现。从本质上讲,它们是可重复使用的数据结构,例如:ArrayList、LinkedList、HashSet、HashMap。
  • 算法:是实现集合接口的对象里的方法执行的一些有用的计算,例如:搜索和排序。这些算法被称为多态,那是因为相同的方法可以在相似的接口上有着不同的实现。
    除了集合,该框架也定义了几个 Map 接口和类。Map 里存储的是键/值对。尽管 Map 不是集合,但是它们完全整合在集合中。

集合框架体系如图所示:

JavaCollArch

集合接口

  1. Collection 接口

Collection 是最基本的集合接口,一个 Collection 代表一组 Object,即 Collection 的元素, Java不提供直接继承自Collection的类,只提供继承于的子接口(如List和set)。
Collection 接口存储一组不唯一,无序的对象。

  1. List 接口

List接口是一个有序的 Collection,使用此接口能够精确的控制每个元素插入的位置,能够通过索引(元素在List中位置,类似于数组的下标)来访问List中的元素,第一个元素的索引为 0,而且允许有相同的元素。
List 接口存储一组不唯一,有序(插入顺序)的对象。

  1. Set 接口

Set 具有与 Collection 完全一样的接口,只是行为上不同,Set 不保存重复的元素。
Set 接口存储一组唯一,无序的对象。

  1. SortedSet 接口

继承于Set保存有序的集合。

  1. Map 接口

Map 接口存储一组键值对象,提供key(键)到value(值)的映射。

  1. Map.Entry 接口

描述在一个Map中的一个元素(键/值对)。是一个Map的内部类。

  1. SortedMap 接口

继承于 Map,使 Key 保持在升序排列。

  1. Enumeration 接口

这是一个传统的接口和定义的方法,通过它可以枚举(一次获得一个)对象集合中的元素。这个传统接口已被迭代器取代。

  1. Queue 接口

Queue是队列实现,实现了先进先出功能。

集合实现类(集合类)

抽象类

类名 内容
AbstractCollection 实现了大部分的集合接口。
AbstractList 继承于AbstractCollection 并且实现了大部分List接口。
AbstractSequentialList 继承于 AbstractList ,提供了对数据元素的链式访问而不是随机访问。
AbstractSet 继承于AbstractCollection 并且实现了大部分Set接口。
AbstractMap 实现了大部分的Map接口。

集合类

类名 内容
LinkedList 该类实现了List接口,允许有null(空)元素。主要用于创建链表数据结构,该类没有同步方法,如果多个线程同时访问一个List,则必须自己实现访问同步,解决方法就是在创建List时候构造一个同步的List。例如: Listlist=Collections.synchronizedList(newLinkedList(...)); LinkedList 查找效率低。
ArrayList 该类也是实现了List的接口,实现了可变大小的数组,随机访问和遍历元素时,提供更好的性能。该类也是非同步的,在多线程的情况下不要使用。ArrayList 增长当前长度的50%,插入删除效率低。
HashSet 该类实现了Set接口,不允许出现重复元素,不保证集合中元素的顺序,允许包含值为null的元素,但最多只能一个。
LinkedHashSet 具有可预知迭代顺序的 Set 接口的哈希表和链接列表实现。
TreeSet 该类实现了Set接口,可以实现排序等功能。
HashMap HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。该类实现了Map接口,根据键的HashCode值存储数据,具有很快的访问速度,最多允许一条记录的键为null,不支持线程同步。
TreeMap 继承了AbstractMap,并且使用一颗树。
WeakHashMap 继承AbstractMap类,使用弱密钥的哈希表。
LinkedHashMap 继承于HashMap,使用元素的自然顺序对元素进行排序.
IdentityHashMap 继承AbstractMap类,比较文档时使用引用相等。

其他:数据结构类

类名 内容
Vector 该类和ArrayList非常相似,但是该类是同步的,可以用在多线程的情况,该类允许设置默认的增长长度,默认扩容方式为原来的2倍。
Stack 栈是Vector的一个子类,它实现了一个标准的后进先出的栈。
Dictionary Dictionary 类是一个抽象类,用来存储键/值对,作用和Map类相似。
Hashtable Hashtable 是 Dictionary(字典) 类的子类,位于 java.util 包中。
Properties Properties 继承于 Hashtable,表示一个持久的属性集,属性列表中每个键及其对应值都是一个字符串。
BitSet 一个Bitset类创建一种特殊类型的数组来保存位值。BitSet中数组大小会随需要增加。

遍历示例代码

遍历ArrayList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.util.*;

public class Test{
public static void main(String[] args) {
List<String> list=new ArrayList<String>();
list.add("Hello");
list.add("World");
list.add("HAHAHAHA");
//第一种遍历方法使用foreach遍历List
for (String str : list) { //也可以改写for(int i=0;i<list.size();i++)这种形式
System.out.println(str);
}

//第二种遍历,把链表变为数组相关的内容进行遍历
String[] strArray=new String[list.size()];
list.toArray(strArray);
for(int i=0;i<strArray.length;i++) //这里也可以改写为 foreach(String str:strArray)这种形式
{
System.out.println(strArray[i]);
}

//第三种遍历 使用迭代器进行相关遍历

Iterator<String> ite=list.iterator();
while(ite.hasNext())//判断下一个元素之后有值
{
System.out.println(ite.next());
}
}
}

遍历Map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.util.*;

public class Test{
public static void main(String[] args) {
Map<String, String> map = new HashMap<String, String>();
map.put("1", "value1");
map.put("2", "value2");
map.put("3", "value3");

//第一种:普遍使用,二次取值
System.out.println("通过Map.keySet遍历key和value:");
for (String key : map.keySet()) {
System.out.println("key= "+ key + " and value= " + map.get(key));
}

//第二种
System.out.println("通过Map.entrySet使用iterator遍历key和value:");
Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}

//第三种:推荐,尤其是容量大时
System.out.println("通过Map.entrySet遍历key和value");
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}

//第四种
System.out.println("通过Map.values()遍历所有的value,但不能遍历key");
for (String v : map.values()) {
System.out.println("value= " + v);
}
}
}

题外话: Java 数组

Java 数组是很容易和Java动态数组(ArrayList)进行混淆的数据结构。其实Java数组本身并不具有方法,唯一的属性是length。对于数组的大小也不能改变,很多功能需要借助Utility类Arrays提供的静态方法。

Java中的所有数组都是通过Array类实例化。Array类没有public的构造方法,数组是通过Array的newInstance()方法进行实例化。Java数组是支持所有类型的,也就是说对于自定义引用类型,也能创建数组类型,但是与动态数组的方式不同,内存分配机制上也有所不同。

Array类源码

数组类的实现主要基于本地方法创建,申请分配一段连续内存,每一个数组元素支持强类型检查。长度不支持扩展。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.lang;

public final class System {
//System 类不能被实例化。
private System() {}
//在 System 类提供的设施中,有标准输入、标准输出和错误输出流;对外部定义的属性
//和环境变量的访问;加载文件和库的方法;还有快速复制数组的一部分的实用方法。
/**
* src and dest都必须是同类型或者可以进行转换类型的数组.
* @param src the source array.
* @param srcPos starting position in the source array.
* @param dest the destination array.
* @param destPos starting position in the destination data.
* @param length the number of array elements to be copied.
*/
public static native void arraycopy(Object src, int srcPos, Object dest,
int destPos, int length);
}
package com.lang.reflect;

public final class Array {
private Array() {}

//创建一个具有指定的组件类型和维度的新数组。
public static Object newInstance(Class<?> componentType, int length)
throws NegativeArraySizeException {
return newArray(componentType, length);
}

private static native Object newArray(Class componentType, int length)
throws NegativeArraySizeException;
}

数组声明与初始化

1
2
3
4
5
6
//直接声明空数组
dataType[] arrayRefVar = new dataType[arraySize];
//直接引入数组元素,用花括弧创建
int [] arrayRefVar2 = {1, 2, 3};
//访问时通过下标访问
System.out.println(arrayRefVar2[0]);

多维数组声明与初始化

多维数组其实就是一维数组的嵌套扩展,维度取决于数组各个元素的类型,如果是一个数组则能增加一个维度。因此多维数组的创建与访问也是基于一维数组的方法。Array类属性只有length一个,访问是基于下标的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//直接声明矩阵二维数组
dataType[][] arrayRefVar = new dataType[colsize][rowsize];
//分段申明非矩阵二维数组
dataType[][] arrayRefVar2 = new dataType[colsize][];
arrayRefVar2[0] = new dataType[row1size];
arrayRefVar2[1] = new dataType[row2size];
//直接引入数组元素,用嵌套花括弧创建,按照维度分配,第一个是第一列
int[][] arrayint = {
{1, 2, 3},
{2, 3, 4}
}
//直接引入数组元素,用下标顺序填入
int[][] array2 = {new int[2], new int[3]};

//数组的访问是基于下标 array2[col][row]

题外话: Arrays 类

java.util.Arrays 类能方便地操作数组,它提供的所有方法都是静态的。
具有以下功能:

  • 给数组赋值:通过 fill 方法。
  • 对数组排序:通过 sort 方法,按升序。
  • 比较数组:通过 equals 方法比较数组中元素值是否相等。
  • 查找数组元素:通过 binarySearch 方法能对排序好的数组进行二分查找法操作。
方法名 内容
public static int binarySearch(Object[] a, Object key) 用二分查找算法在给定数组中搜索给定值的对象(Byte,Int,double等)。数组在调用前必须排序好的。如果查找值包含在数组中,则返回搜索键的索引;否则返回 (-(插入点) - 1)。
public static boolean equals(long[] a, long[] a2) 如果两个指定的 long 型数组彼此相等,则返回 true。如果两个数组包含相同数量的元素,并且两个数组中的所有相应元素对都是相等的,则认为这两个数组是相等的。换句话说,如果两个数组以相同顺序包含相同的元素,则两个数组是相等的。同样的方法适用于所有的其他基本数据类型(Byte,short,Int等)。
public static void fill(int[] a, int val) 将指定的 int 值分配给指定 int 型数组指定范围中的每个元素。同样的方法适用于所有的其他基本数据类型(Byte,short,Int等)。
public static void sort(Object[] a) 对指定对象数组根据其元素的自然顺序进行升序排列。同样的方法适用于所有的其他基本数据类型(Byte,short,Int等)。

题外话: Collections 类

读书笔记 —— 《凤凰项目:一个IT运维的传奇故事》

本书从IT运维管理者的第一视角,讲述了IT部门每天都会发生的事情,并通过从工厂管理经验中借鉴到的方法,改善了IT部分的产能的故事。

IT工作者的每日工作项目

IT人员的工作往往分为如下几个项目。所以每一个程序员需要重视这四类工作。

业务项目

这是多数开发项目所包含的业务举措,通常隶属于项目管理办公室。虽然是IT项目,但大多数是跟业务的利润有直接相关联。

IT内部项目

IT内部项目包括可能由业务项目衍生出的基础架构或IT运维项目,亿级内部生成的改进项目(例如创建新环境和部署自动化等)。这类项目业务部门并不是直接集中跟踪管理的,而属于预算所有者(例如数据库经理、存储管理经理和分布式系统经理)。

变更

经常由前两种类型的工作引起,往往在保修系统中被跟踪(例如IT运维补救、JIRA或者用于开发的敏捷计划工具)。事实上,在价值流的两个不同部分中存在两个工作跟踪系统,这会引起问题,尤其在交接工作的时候。

某些情况下,在一些兼具功能开发和服务交付指责的专门团队中,所有工作都会处在同一个系统中。这样做的好处是,操作层面的故障会和功能缺陷以及新的特性功能一起,在存量工作和现行工作中显现。

在生产环境的变更是需要严格关注的,有的公司会专门对生产环境的变更进行记录。尤其会对变更本身的原因,步骤,以及风险评估和处理细节都进行记录。变更记录会交由管理者进行审批从而在计划时间窗口生效。

计划外项目/救火

包括操作事故和操作问题,通常由上述三类型的工作导致,而且往往以牺牲其他计划内工作为代价。

布伦特——约束点问题

文中的工程师布伦特是IT运维部的约束点,原因是:

  • IT运维工作中最优秀的救火队员
  • IT运维视角唯一的项目系统架构师
  • IT变更项目中时常存在的执行者
  • 项目半成品堆积:由于救火项目或者忙等待问题造成。

每个工作中心中都存在他的身影,个人能力的有限决定了IT部门的产出有限。

约束点问题解决了,工作产能问题便能解决。所有在非约束点做的改进都是假象。

对应的改善约束点问题的方案:

  • 建立L3支持工程师代替执行并学习布伦特的救火技术,建立其他人可以借鉴的trouble shooting检查清单。
  • 专一做到资深架构应该做的工作:关键项目的系统架构
  • 将需要布伦特参与的项目进行工作和流程标准化管理
  • IT工作可视化并控制半成品

等待时间是工作中心中某个资源忙碌成都的函数。下图横轴坐标上是工作重心中给定资源的忙碌百分比,纵坐标轴上是大致的等待时间(更确切的说是队列长度)。等待时间=忙碌时间百分比/空闲时间百分比。曲线的形状表明,当资源使用率超过80%时,等待时间就会直线上升。这会对项目交付产生灾难性后果。

waitingtime

看板对于IT中的半成品问题是最有效、最简单的一种对策。两本书推荐阅读:

  1. 《个人看板:了解工作/驾驭生活》by吉姆·本森和托尼安妮·德马里亚·巴里
  2. 《看板方法:科技企业渐进变革成功之道》by戴维·J·安德森

工作流程改进————形

为了将IT的工作项目完善的进行预先评估和量化分析,每一项工作类型都可以进行执行周期的管理与规划,尽量减少工作半成品的积压,并且最大化工作成品的“吞吐量”。本文提出需要进行“改进形”计划:

以两周为周期,进行两个“计划-执行-审核-落实”改进周期,确立项目需要的四大要素:机器、方法、人员与测评。每两周必须做出一些改进,无论任何形式的改进。

积压的项目分为三类:需要布伦特参与的项目,可以提高布伦特生产力的项目,其他项目。优先开展后两类项目;对于需要布伦特参与的项目遵循约束点问题解决方案进行优化。

“改进形”带来的好处:

  • 提供一种适用于各种问题或挑战的系统化科学规程;
  • 组织成员普遍养成解决问题的习惯;
  • 通过让经理开展周期性指导,让其向教练和导师的角色转变;
  • 通过让人们每天慢慢进步的方法,形成PDCA(Plan-Do-Check-Action)。

微软IT案例研究《在9个月内实现逆袭:在微软IT部门应用鼓点-缓冲-绳子解决法(译注:即限制驱导式排程法)》,作者是戴维·J.安德森和德拉戈什·杜密特里乌。

当时安德森和杜密特里乌两人都在微软,他们描述了以前那种糟透了的状态。大多数IT从业人员>对那种状态都再熟悉不过了。

❑ 完成业务部门要求的工作耗时过长:平均交付周期是155天。

❑ 对于延误和长交付周期的不满迫使IT管理作出“更多的工作预估”,这让经理们不得不把全部时
间都用来做PPT,而不是干实事(因为业务部门的结论是他们没有作出正确的工作预估)。

❑ 不管业务部门提出什么要求,回答永远是“做这件事需要5个月”。

❑ 每项任务都预计在20天内完成,但是没人知道多出的那135天都去哪儿了。

杜密特里乌在报修系统中创建了一个名叫“等待德拉戈什”的新字段(实际上,这个报修系统是微>软缺陷跟踪系统),以及时发现工作阻碍。他很快得出结论,项目团队70%的时间都卡在了别人>那里——也就是说,在70%的时间里,工作都在排队。
杜密特里乌认为,他的团队一个月只能完成3项工作,按照这个速度,需要三年才能完成所有的>工作。以下是他提出的对策及其惊人结果。

❑ 他们不再预估工作,而采用根据历史数据得出的实际时间——他们有80个人——报修系统里有多>年的工作记录供他们使用。这样做的结果是开发和测试效率立刻提高了30%。

❑ 他们不再采用成本核算,而采用简单的“基于预算贡献的ROI(Return on Investment,投资>回报率)”。所节省的时间让PM能力立刻提高了20%。

❑ 发现约束点是开发部之后,PM接管了许多开发任务,把开发能力提高了20%。这样做也让开发>人员更加高兴,因为他们可以专心写代码,不用再作任务预估了。

❑ 他们引进了一名可用性专家来调整变更申请表。(他打趣说:“我们得填完4页表格,才能得到>一杯水;我们把4页表格换成2页的,上面还有很多自由格式字段,目的是保证从事这项工作的人>了解其所需的全部信息。”)

❑ 接着他们减少了系统中允许的半成品数量:一开始平均有40到60个未结项目,他们把未结项目>数减少到了5个。

❑ 然后他们创建了工作缓冲区,任何遇到阻碍的开发或测试人员都能在缓冲区里做一些工作。

❑ 交付周期从155天下降到22天。这么短的交付周期让他们创造了一个新的SLA认证(SLA >guarantee),25天(哇哦!)。

❑ 他们的下一轮生产能力大幅提高来自于增加开发人员数量,因为每两天的开发工作就需要一天>的测试工作。他们提拔了愿意参与开发工作的测试人员,把开发人员和测试人员的比例从1∶1提>高到2∶1。

❑ 上述种种的结果是什么?他们在9个月里完成了整整3年积压下来的工作;对他们服务的需求量>也增加了,然后他们继续在其后的每个月都顺利完成并交付了所有业务部门要求的工作;不仅没>人被解雇,而且很多人还升了职。
杜密特里乌说:“我们始终致力于降低交付周期,而不是开发和测试自身的优化,因此我们成功>了。”

这只是详细描述的众多惊人转变之一。难以置信的是,转变主要不是基于自动化,相反,这种不>可思议的改进来自于调整关于工作系统的策略和控制半成品的策略,确保有一个高效的跨职能团>队,让所有事情都为约束点服务,以及管理好工作交接。

IT工作管理核心思想

三步工作法

第一工作法是关于从开发到IT运维再到客户的整个自左向右的工作流。为了使流量最大化,我们需要小的批量规模和工作间隔,决不让缺陷刘翔下游工作中心,并且不断为了整体目标(相对于开发功能完成率、测试发现/修复比率或运维有效性指标等局部目标)进行优化。

必要的做法包括持续构建、集成以及部署,按需创建环境,严控半成品,以及构建起能够顺利变更的安全系统和组织。

第二工作法是关于价值流各阶段自右向左的快速持续反馈流,放大其效益以确保防止问题再次发生,或者更快地发现和修复问题。这样,我们就能在所需之处获取或嵌入知识,从源头上保证质量。

必要的做法包括:在部署管道中的构建和测试失败时“停止生产线”;日复一日地持续改进日常工作;创建快速的自动化测试套装软件,以确保代码总是处于可部署的状态;在开发和IT运维之间建立共同的目标和共同解决问题的机制;建立普遍的产品遥测技术,让每个人都能知道,代码和环境是否在按照设定的运行,以及是否达到了客户的目标。

第三工作法是关于创造公司文化,该文化可带动两种风气的形成:不断尝试,这需要承担风险并从成功和失败中吸取经验教训;理解重复和练习是熟练掌握的前提。

尝试和承担风险让我们能够不懈地改进工作系统,这经常要求我们去做一些与几十年来的做法大不相同的事。一旦出了问题,不断重复的日常操练赋予我们的技能和经验,令我们可以撤回至安全区域并恢复正常运作。

必要的做法包括营造一种勇于创新、敢于冒险(相对于畏惧或盲目服从命令)以及高信任度(相对于低信任度和命令控制)的文化,把至少20%的开发和IT运维周期划拨给非功能性需求,并且不断鼓励进行改进。

团队领导力的寓言

这是帕特里克·兰西奥尼在《团队领导的五大障碍:关于领导力的寓言》(The Five Dysfunctions of a Team: A Leadership Fable)一书中描述的方法。
他认为,团队无法达成目标的一个核心诱因是信任缺失。在他的模型中,五大障碍被描述为:

  • 信任缺失——不愿在团队中显示弱点;
  • 惧怕冲突——在充满激情的建设性辩论中寻求和谐的假象;
  • 缺乏诚意——假意与团队的决策达成一致,形成模棱两可的公司氛围;
  • 回避问责——面对员工的失职行为,逃避追责,降低了工作标准;
  • 忽视结果——对个人成就、地位和自我价值的关注超过了对团队成功的关注。

考虑到开发部和IT运维部之间,以及IT和“业务部门”之间存在着长期、剧烈的部门斗争,我想我们非常需要兰西奥尼先生的教诲以实现开发运维的理想。
通常来说,运用兰西奥尼先生方法论的第一步,是领导人要展示自己的弱点(或者起码要从塑造示弱的行为着手)。在《凤凰计划》中,史蒂夫多年来已将这一实践内化于心,并主导了一场关于个人经历的分享活动。

信息安全问题解决思想

以不对IT系统做过多无用功就保护公司不受审计困扰,才是最终的胜利。而不是一昧的强制加入新的安全补丁,限制IT系统的功能与产出更多的维护问题。正如QA不需要测试不再需要的功能和不可能发生的性能压力,不要犯“眼界的错误”。

优先项目永远优先

关乎公司存亡的项目永远放在第一位。必要时可以冻结其他项目进度以提供充足的资源,人力和时间。

Java memory allocation

Java虚拟机管理内存会包括以下几个运行时数据区域,如下图所示。

jvm

JVM运行时数据区域简介

程序计数器(Program Counter Register)

程序计数器时当前线程执行字节码的行号指示器。每条线程有独立的程序计数器。

  • 如果线程执行Java方法,这个技术及记录的时真该执行的虚拟机字节码指令的地址。
  • 如果线程执行的时Native方法,这个计数器值则为Undefined。

Java虚拟机栈(JVM stacks)

JVM栈也是线程私有的。虚拟机栈描述的时Java方法执行的内存模型:

  1. 每个方法会先创建一个栈帧Stack Frame,用于存储局部变量表,操作数栈,动态链接,方法出口等信息。
  2. 在方法执行完后,该栈帧会出栈。

栈内存说的就是虚拟机中局部变量表部分。

Native方法栈

本地方法栈为虚拟机使用到的Native方法服务。

Java堆

Java堆是虚拟机所管理的内存,被所有线程共享的,目的是存放对象实例。Java堆是垃圾收集器(GC)主要管理的区域。

方法区

方法区是用于存储虚拟机的类信息,常量,静态变量,即时编译器编译后的代码等数据,被所有线程共享。在HotSpot虚拟机中,方法区又叫做“永久代”,是因为GC分代收集扩展至方法区,使得方法区由GC中的永久代区域实现。

运行时常量池

运行时常量池是方法区的一部分。本来在方法区中的类信息就包含了除类的版本、字段、方法、接口等描述信息之外,还有一项信息便是常量池,用于存放编译生成的各种字面量和符号引用。常量池信息将在类加载后进入方法区的运行时常量池中存放。同时,一些方法如String类的intern()方法也能将字符串加入运行时常量池。所以在类信息加载完成后,常量池也不是大小就不变的。

直接内存

直接内存并不是与JVM运行时数据区的一部分。JDK1.4后引入的NIO(New I/O)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式。它可以使用Native函数库直接分配对外内存,然后通过Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。直接内存不属于JVM GC的管理范畴,可以用-Xmx进行设定。

JVM对象的创建

当JVM遇到一条new指令时,会做出什么样的处理呢?

  1. 检查指令参数是否能在常量池中定位到一个类的符号引用,并确保其被正确的加载、解析和初始化;

  2. 在Java堆上分配内存,有两种空闲内存分配方式:

    • 空闲列表: 基于Mark-Sweep算法的收集器的GC,如CMS;
    • 指针碰撞: 具有compact过程的收集器的GC,如Serial, ParNew等。
  3. 为了避免竞争效应即操作的原子性,系统采用如下两种其一的方法:

    • 分配内存动作进行同步处理,CAS(Compare and swap)+失败重试机制,
    • 分配内存按照线程划分不同的空间之中进行,即本地线程缓冲机制(TLAB, Thread local allocation buffer)。
  4. 为新创建对象设置好初始值;

  5. 对对象的对象头信息(Object header)进行相关必要设置,如:

    • 类型指向
    • 类的元数据
    • 对象哈希值
    • 对象的GC年代信息
  6. 类文件bytecode中的< init>方法执行;

init方法是Java的class文件中的各种构造方法经过JIT解释后生成的bytecode代码,一般由invokespecial操作码所调用。

自此,一个完整的对象就被创建好了。

JVM对象的内存布局

当JVM对象被创建好了,会被分配在Java堆上,存储布局可以分为三个区域:对象头(header)、实例数据(instance data)和对齐填充(padding)。

对象头

对象头包括两部分,一部分是”Mark Word”,另一部分是类型指针。

  • Mark word: 长度为32bit或64bit。HotSpot 32位虚拟机中具体的对象头存储内容取决于对象的锁状态值,如下:
    Markword
  • 类型指针: 长度为32bit或64bit,用于存储指向类元数据的指针,并不是所有的虚拟机实现都必须在对象数据上保留类型指针。
  • 数组长度:长度为32bit,当对象为数组时,用于存储数组的长度。注:此数组并非ArrayList泛型,后者属于引用类型。

实例数据

实例数据部分存储了类对象的所有类型的字段内容。每种虚拟机有自己定义好的参数和字段的分配策略。

对齐填充

对齐填充的存在是为了满足HotSpot VM自动内存管理系统要求,保证所有对象的地址都是8字节的整数倍。

Java基础类型内存布局

java的基本数据类型共有8种,即int,short,long,byte,float,double,boolean,char(注意,并没有String的基本类型)。Java基础类型变量是在(Java虚拟机)栈上分配的,当变量的作用域运行结束后,通过出栈的方式回收分配在栈上的变量内存。

当声明分配一个int类型变量a = 3时,JVM会先为该变量创建一个变量为a的引用,再在栈上搜索是否存在字面值为3的引用。

  • 如果找到,就直接将a指向3的地址。
  • 如果没有找到,就分配一个内存存放字面值3,并将a指向这个地址。
    因此说,基础类型字面值在同一个栈上是共享的。

问题:已知int类型变量需要32bit内存,具体stack frame上内存分配是什么样子的呢? 变量a是怎么存放的? int类型信息又是放在那里的呢?

JVM对象的访问定位

对象的访问定位如下图,HOTSOPT用的是第2种算法:

  1. 使用句柄(先指向堆里的句柄池,再从句柄池找到指针,优点是只需要修改句柄, 缺点就是句柄池也是开销);
  2. 直接指针(减少性能开销): 需要存2个数据, 到对象实例数据的指针,到对象类数据的指针。
    reference

Garbage Collection of JVM

GC定义

Garbage Collection(垃圾回收/GC)是JVM对于Java堆上内存在运行时进行的动态管理,主要是对Java堆上不再被引用的对象进行回收。Minor GC是主要快速回收Eden区和Survivor区对象内存,Full GC则会对老年代也进行回收,后者可能会影响性能。

如何确定对象是否需要回收?

引用计数算法(Reference Counting)

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

缺点:存在循环引用的问题。

可达性分析算法(Reachability Analysis)

通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。

GC Roots:

  • 虚拟机栈中引用的对象
  • 方法去中类静态属性引用的对象
  • 方法去中常量引用的对象
  • 本地方法栈中JNI(Native方法)引用的对象。

如何对对象进行回收?

标记——清除算法

MarkSwap

复制算法

Copying

标记——整理算法

MarkCompact

分代收集算法

对于新生代和老年代的对象进行不同的清理算法,一般来说,复制算法适合新生代,标记-清除算法和标记整理算法更适合老年代内存。

JVM对象内存管理策略

GC管理的内存分为三类区域,分别是Eden+Survivor(新生代),Tenured(老年代)和Permanent(永久代)。

GCregion

  1. 对象优先在Eden分配

  2. 大对象直接进入老年代

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

  4. 动态对象年龄判定:当Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。这是为了防止Survivor区溢出。

JVM常用的垃圾收集器

GCs

Serial收集器

单线程处理新生代GC。复制算法。STW

Serial/SerialOld

ParNew收集器

采用多线程处理新生代GC。复制算法。STW

ParNew

Parallel Scavenge收集器

处理算法和ParNewGC完全一样。
但是,Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 +垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

ParallelScavengeAndParOld

Serial Old收集器

单线程处理老年代GC。采用标记-整理算法。STW

Parallel Old收集器

多线程处理老年代GC。采用标记整理算法。STW

CMS(Concurrent Mark Sweep)收集器

四个阶段(基于标记-清理算法):

  • 初始标记 STW
  • 并发标记
  • 再次标记 STW
  • 并发清理

CMS

问题:

  1. 并发清理时预留空间不够造成并发清理(Concurrent Mode Failure)失败=>浮动垃圾(Floating Gabage)过多。
  2. 内存碎片化问题。一旦发生大对象触发的FullGC,Serial Old回收则会出现长时间STW。
  • CMS并发三色标记法

    1. 黑色:已经标记完引用对象的颜色
    2. 灰色:没有标记完引用对象的颜色
    3. 白色:默认垃圾(没有被标记颜色)
    • 标记问题:

      1. 本来A->B, B->D;
      2. 在A标记完,B部分标记后,B->D引用消失,D没有被标记,A->D引用建立
      3. 由于D从始至终都没有被标记
    • 标记问题一Incremental Update更正:

      1. 对于A->D(白)的引用建立,把A修正成灰色。
    • Incremental Update更正存在的ABA问题:

      1. 回收线程一:标记A属性1,正在标记属性2
      2. 业务逻辑线程二:把属性1指向白色D, A保持灰色
      3. 回收线程三: 更新属性2的标记,将A标记为黑色
    • CMS最终解决方案:必须STW从头扫描一次

G1(Garbage First)收集器

启动G1需要参数-XX:+UseG1GC,G1不是与其他GC分代处理垃圾的,而是对新生代和老年代均进行不同的GC。

Young GC:

  • 标记-清除-复制算法整理 STW
    只对新生代区块进行清理,但是也会需要扫描所有region的Rset,否则不知道有哪些Old->Young的引用。

Mixed GC:
处理Mixed GC时只将将部分old区块进行回收。Rset记录了其他区块对本区块的引用。最终的扫描区域为Young+对Rset进行扫描,缩短了原来需要扫描整个Old时间。而且Young<->Old的引用都能快速找到。

并发标记分为四个阶段(基于标记-整理算法):

  • 初始标记 STW
  • 并发标记
  • 最终标记 STW
  • 筛选回收 STW 根据停顿时间要求筛选出Old中的Cset集合,作为回收目标。

回收evacuation阶段(小区块进行复制整理避免碎片):
需要STW,将选出的Cset中的对象进行复制到新的区块,清除掉原来的区块,达到收集的效果。

G1

ZGC(Z Garbage Collector)收集器

ZGC(Z Garbage Collector)是一款由Oracle公司研发的,以低延迟为首要目标的一款垃圾收集器。它是基于动态Region内存布局,(暂时)不设年龄分代,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的收集器。在JDK 11新加入,还在实验阶段,主要特点是:回收TB级内存(最大4T),停顿时间不超过10ms。m目前ZGC是实验性功能,可以通过-XX:+UnlockExperimentalVMOptions -XX:+UseZGC参数启动ZGC。

垃圾收集器参数

GCArgs

Problem

Write a class StockSpanner which collects daily price quotes for some stock, and returns the span of that stock’s price for the current day.

The span of the stock’s price today is defined as the maximum number of consecutive days (starting from today and going backwards) for which the price of the stock was less than or equal to today’s price.

For example, if the price of a stock over the next 7 days were [100, 80, 60, 70, 60, 75, 85], then the stock spans would be [1, 1, 1, 2, 1, 4, 6].

Example 1:

1
2
3
4
5
6
7
8
9
10
11
Input: ["StockSpanner","next","next","next","next","next","next","next"], [[],[100],[80],[60],[70],[60],[75],[85]]
Output: [null,1,1,1,2,1,4,6]
Explanation:
First, S = StockSpanner() is initialized. Then:
S.next(100) is called and returns 1,
S.next(80) is called and returns 1,
S.next(60) is called and returns 1,
S.next(70) is called and returns 2,
S.next(60) is called and returns 1,
S.next(75) is called and returns 4,
S.next(85) is called and returns 6.

Note that (for example) S.next(75) returned 4, because the last 4 prices
(including today’s price of 75) were less than or equal to today’s price.

Note:

  1. Calls to StockSpanner.next(int price) will have 1 <= price <= 10^5.
  2. There will be at most 10000 calls to StockSpanner.next per test case.
  3. There will be at most 150000 calls to StockSpanner.next across all test cases.
  4. The total time limit for this problem has been reduced by 75% for C++, and 50% for all other languages.

Solution

解题思路

本题考验的是对Java Stack类的熟练使用。

O(n)复杂度思路

不难发现,运用两个Stack可以灵活的将数组中的数字进行遍历比较,从而得出结果。但是由于Leetcode对于时间复杂度要求较高,因此同为O(n)算法,需要最大化的优化其系数,从而通过时间限制测试。

Leetcode solution中提出的一种简化计算的方法

在仔细分析对比数组的计算结果后,不难得出以下几个结论:

  • 当数组元素值减少时,权重结果为1;
  • 当数组元素值增加时,倒推前面小于该值的连续区间可以替换成局部最大值和其局部权重weight。局部最大值即当前元素值;而最末元素的权重,正好是其前面小于该值的连续区间的权重和+1。权重值则为我们需要的返回值。

以Example中的数组为例,用Stack<int[]>表示分步计算结果为:

  1. [100, 1]
  2. [100, 1], [80, 1]
  3. [100, 1], [80, 1], [60, 1]
  4. [100, 1], [80, 1], [70, 2]
  5. [100, 1], [80, 1], [70, 2], [60, 1]
  6. [100, 1], [80, 1], [75, 4]
  7. [100, 1], [85, 6]

实现技巧

需要注意的本解法并没有简化时间复杂度,因为在最差情况下(数列递减),计算的复杂度为O(n)。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class StockSpanner {
Stack<Integer> prices, weights;

public StockSpanner() {
prices = new Stack();
weights = new Stack();
}

public int next(int price) {
int w = 1;
while (!prices.isEmpty() && prices.peek() <= price) {
prices.pop();
w += weights.pop();
}

prices.push(price);
weights.push(w);
return w;
}
}

Angular binding brief introduction

This blog is talking about the template syntax of Angular 5. The reference topic is at https://angular.io/guide/template-syntax.

Angular one-way binding with DOM elements

For DOM elmenets that does simple display, Angular one-way binding is very useful. For example, pure text, or un-editable tables, one-way binding can quick get rendered by directly binding the Angular variable or using *ngFor iteration. There are three examples of one-way binding in the html template. THe binding objective can be expression, which does not change variables.

1
2
3
<p>Hello {{username}}.</p> <!-- pure one-way interpolation with {{}} syntax-->
<input [value]="username"> <!-- DOM property with [] binding -->
bind-target="expression" <!-- bind- prefix target binding-->

** Remember to user [] brackets to make the DOM property actively linked with Angular variables, otherwise the binding is worked as string type initialization only.

Angular two-way binding with DOM elements

For DOM elements that also can have user interactions like input elements, select elments, two-way binding or event binding is required to allow Angular know about the user action. The grammar to bind a two-way variable is to use [(target)].

There are some examples of Angular two-way binding:

1
2
<input [(ngModel)]="username"> <!-- property, like ngModel for form elements, with [()] syntax-->
bindon-target="expression" <!-- bindon- prefix target two-way binding-->

Angular event binding with DOM elements

Some DOM elements are not interact with text but events link click or text change, Angular also provides sytax for event binding so it can track user’s behavior based on DOM events. Vairables, like #event template input variable (let here), and template reference variable (#heroForm), which can be passed to the event handlers. The grammar to bind a event of a DOM is to use parenthesis with Angular event like (click). Event handlers can only be statements like methods of the component instance.

There are some examples of Angular event bindings:

1
2
(click)="click($event)" <!-- use DOM property binding with () syntax-->
on-target="statement" <!-- use on- prefix target binding -->

Angular template binding targets

It’s not hard to see, binding targets in Angular includes HTML properties and events as below table.

Type Target Examples
Property Element property,
Component property,
Directive property
<img [src]="heroImageUrl">
<app-hero-detail [hero]="currentHero"></app-hero-detail>
<div [ngClass]="{'special': isSpecial}"></div>
Event Element event,
Component event,
Directive event
<button (click)="onSave()">Save</button>
<app-hero-detail (deleteRequest)="deleteHero()"></app-hero-detail>
<div (myClick)="clicked=$event" clickable>click me</div>
Two-way Event and property <input [(ngModel)]="name">
Attributes Attribute (the exception) <button [attr.aria-label]="help">help</button>
Class class property <div [class.special]="isSpecial">Special</div>
Style style property <button [style.color]="isSpecial ? 'red' : 'green'">

Besides, Angular also supports built-in directives as below table.

Directive Type Target Use Case Examples
Attribute NgClass add and remove a set of CSS classes <!-- toggle the "special" class on/off with a property -->
<div [class.special]="isSpecial">The class binding is special</div>
Attribute NgStyle add and remove a set of HTML styles <button [style.color]="isSpecial ? 'red' : 'green'">
Attribute NgModel two-way data binding to an HTML form element <input [(ngModel)]="name">
Structural NgIf conditionally add or remove an element from the DOM <app-hero-detail *ngIf="isActive"></app-hero-detail>
Structural NgSwitch a set of directives that switch among alternative views <div *ngFor="let hero of heroes">{{hero.name}}</div>
Structural NgForOf repeat a template for each item in a list <div [ngSwitch]="currentHero.emotion">
<app-happy-hero *ngSwitchCase="'happy'" [hero]="currentHero"></app-happy-hero>
<app-sad-hero *ngSwitchCase="'sad'" [hero]="currentHero"></app-sad-hero>
</div>

Template reference variables

A template reference variable is often a reference to a DOM element within a template. It can also be a reference to an Angular component or directive or a web component.
Use the hash symbol (#) to declare a reference variable. The #phone declares a phone variable on an <input> element.

Example:

1
2
3
4
<input #phone placeholder="phone number">
<!-- lots of other elements -->
<!-- phone refers to the input element; pass its `value` to an event handler -->
<button (click)="callPhone(phone.value)">Call</button>

Template expression operators

The template expression language employs a subset of JavaScript syntax supplemented with a few special operators for specific scenarios. The next sections cover two of these operators: pipe and safe navigation operator.

The pipe operator ( | )

Angular pipes are a good choice for small transformations such as these. Pipes are simple functions that accept an input value and return a transformed value. They’re easy to apply within template expressions, using the pipe operator (|):

Example:

1
<div>Title through uppercase pipe: {{title | uppercase}}</div>

The safe navigation operator ( ?. ) and null property paths

The Angular safe navigation operator (?.) is a fluent and convenient way to guard against null and undefined values in property paths. Here it is, protecting against a view render failure if the currentHero is null.

Example:

1
The current hero's name is {{currentHero?.name}}

The Angular non-null assertion operator (!) serves the same purpose in an Angular template.

Example:

1
2
3
4
5
<!--No hero, no text -->
<div *ngIf="hero">
The hero's name is {{hero!.name}}
</div>

The $any type cast function ($any( ))

Sometimes a binding expression will be reported as a type error and it is not possible or difficult to fully specify the type. To silence the error, you can use the $any cast function to cast the expression to the any type.

1
2
3
4
<!-- Accessing an undeclared member -->
<div>
The hero's marker is {{$any(hero).marker}}
</div>

Angular binding different component properties

We usually binding a template to its own component class. In such binding expressions, the component’s property or method is to the right of the (=).

The Angular compiler won’t bind to properties of a different component unless they are Input or Output properties. You can’t use the TypeScript public and private access modifiers to shape the component’s public binding API.

Declaring Input and Output properties

An Input property is a settable property annotated with an @Input decorator. Values flow into the property when it is data bound with a property binding
An Output property is an observable property annotated with an @Output decorator. The property almost always returns an Angular EventEmitter. Values flow out of the component as events bound with an event binding.

Example:

In the sample for this guide, the bindings to HeroDetailComponent do not fail because the data bound properties are annotated with @Input() and @Output() decorators.

In src/app/app.component.html file, below code will through compile error as template of app component does not recognize the property of hero-detail component:

1
2
<app-hero-detail [hero]="currentHero" (deleteRequest)="deleteHero($event)">
</app-hero-detail>

In src/app/hero-detail.component.ts file, set @Input and @Output decorator with corresponding properties. Then the error is resolved.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...

@Component({
inputs: ['hero'],
outputs: ['deleteRequest'],
})

...

@Input() hero: Hero;
@Output() deleteRequest = new EventEmitter<Hero>();

...

0%