第 4 章 走进 JVM

4.1 字节码

通过 javac Test.java 来编译源文件,生成 Test.class 字节码文件,这个 class 字节码文件,是以二进制的形式存储的,我们以十六进制的形式进行查看。这里我使用 Vim 进行查看,在命令行模式输入:%!xxd,来采用十六进制的格式查看,得到下面的输出:

起始的四个字节非常特殊,cafe babe 是一个魔法数。它的作用是:标志该文件是一个 Java 类文件。接下来的两个字节(0000 0034) ,代表当前版本号,0x34 十进制为52。是jdk 8 的版本号。

Java 所有的指令有200个左右,一个字节(8位)可以存储256种不同的指令信息,一个这样的字节称为字节码(Bytecode)。在代码的执行过程中,JVM 将字节码解释执行,屏蔽对底层操作系统的依赖;JVM 也可以将字节码编译执行,如果是热点代码,会通过 JIT 动态地编译为 机器码,提高执行效率。

JVM 在字节码上也设计了一套操作码助记符,使用特殊单词来标记这些数字。如 ICONST_0 代表 0000 0011,即十六进制数为 0x03; ALOAD_0 代表 0010 1010,即 0x2a; POP 代表 0101 0111,即 0x57。ICONST 和 ALOAD 的首字母表示具体的数据类型,如 A 代表引用类型变量,I 代表 int 类型相关操作,其他类型均是其类型的首字母,例如 FLOAD_0、 LLOAD_0、FCONST_0 等。

字节码主要指令
  1. 加载或存储指令

    在某个栈帧中,通过指令操作数据在虚拟机栈的局部变量表与操作栈之间来回传输,常见指令如下:

    1. 将局部变量加载到操作栈中。如 ILOAD(将int 类型的局部变量压入栈) 和 ALOAD(将对象引用的局部变量压入栈)等。
    2. 从操作栈顶存储到局部变量表。如 ISTORE、ASTORE 等。
    3. 将常量加载到操作栈顶,这是极为高频使用的指令。如 ICONST、BIPUSH、SIPUSH、LDC 等。
      • ICONST 加载的是 -1 ~ 5 的数 ( ICONST 与 BIPUSH 的加载界限)。
      • BIPUSH,即 Byte Immediate PUSH, 加载 -128 ~ 127 之间的数。
      • SIPUSH,即 Short Immediate PUSH, 加载 -32768 ~ 32767 之间的数。
      • LDC, 即 Load Constant, 在 -2147483648 ~ 2147483647 或者是字符串时,JVM 采用LDC 指令压入栈中。
  2. 运算指令

    对两个操作栈帧上的值进行运算,并把结果写入操作栈顶,如 IADD、IMUL 等。

  3. 类型转换指令

    显式转换两种不同的数值类型。如 I2L、D2F 等。

  4. 对象创建与访问指令

    根据类进行对象的创建、初始化、方法调用相关的指令,常见指令如下:

    1. 创建对象指令。如 NEW、NEWARRAY 等。
    2. 访问属性指令。如 GETFIELD、PUTFIELD、GETSTATIC 等。
    3. 检查实例类型指令。如 INSTANCEOF、CHECKCAST 等。
  5. 操作栈管理指令

    JVM 提供了直接控制操作栈的指令,常见指令如下:

    1. 出栈操作。如POP 即一个元素,POP2 即两个元素。
    2. 复制栈顶元素并压入栈。如 DUP。
  6. 方法调用与返回指令

    常见指令如下:

    1. INVOKEVIRTUAL 指令:调用对象的实例方法。
    2. INVOKESPECIAL 指令:调用实例初始化方法、私有方法、父类方法等。
    3. INVOKESTATIC 指令:调用类静态方法。
    4. RETURN 指令:返回VOID 类型。
  7. 同步指令

    JVM 使用方法结构中的 ACC_SYNCHRONIZED 标记同步方法,指令集中有 MONITORENTER 和 MONITOREXIT 支持 synchronized 语义。

除字节码指令外,还包含一些额外信息。例如,LINENUMBER 存储了字节码与源码行号的对应关系,方便调试的时候正确定位到代码的所在行;LOCALVARIABLE 存储当前方法中使用到的局部变量表。

源码转化成字节码的过程
image-20190325174008565
image-20190325174008565

词法解析是通过空格分隔出单词、操作符、控制符等信息,将其形成 token 信息流,传递给语法解析器;在语法解析时,把词法解析得到的 token 信息流按照 Java 语法规则组装成一棵语法树,如图 4-2 虚线框所示;在语义分析阶段,需要检查关键字的使用是否合理、类型是否匹配、作用域是否正确等;当语义分析完成之后,既可生成字节码。

字节码必须通过类加载过程到加载到 JVM 环境后,才可以执行。执行有三种模式:第一,解释执行;第二,JIT 编译执行;第三,JIT 编译与解释混合执行(主流 JVM 默认执行模式)。混合执行模式的优势在于解释器在启动时先解释执行,省去编译时间。随着时间推进,JVM 通过热点代码统计分析,识别高频的方法调用、循环体、公共模块等,基于强大的 JIT 动态编译技术,将热点代码转换成机器码,直接交给CPU 执行。JIT 的作用是将 Java 字节码动态地编译成可以直接发送给处理器指令执行的机器码。简要流程如下:

image-20190326105939705
image-20190326105939705

4.2 类加载过程

在冯·诺依曼定义的的计算机模型中,任何程序都需要加载到内存才能 CPU 进行交流。字节码 .class 文件同样需要加载到内存中,才可以实例化类。ClassLoader 的使命就是提前加载 .class 类文件到内存中。在加载类时,使用的是 Parents Delegation Model,译为双亲委派模型。

Java 的类加载器是一个运行时核心基础设施模块,主要是在启动之初进行类的Load、Link 和 Init,即加载、链接、初始化。

  1. 第一步,加载(Load) 阶段读取类文件产生二进制流,并转化为特定的数据结构,初步校验 cafe babe 魔法数、常量池、文件长度、是否有父类等,然后创建对应类的 java.lang.Class 实例 (堆区)。

    加载阶段完成后,虚拟机外部的 二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。

  2. 第二步,链接(Link) 阶段包括 验证、准备、解析 三个步骤。

    • 验证是更详细的校验,比如 final 是否合规、类型是否正确、静态变量是否合理等;
    • 准备阶段是为静态变量分配内存,并设定默认值,解析类和方法确保类与类之间的相互引用正确性,完成内存结构布局;
      • 假设一个类变量的定义为:public static int value = 3;那么变量value在准备阶段过后的初始值为0,而不是3,因为这时候尚未开始执行任何 Java方法,而把 value 赋值为 3 的 putstatic 指令是在程序编译后,存放于类构造器 \() 方法之中的,所以把 value 赋值为 3 的动作将在初始化阶段才会执行。
    • 解析:把类中的符号引用转换为直接引用。
  3. 第三步,初始化(Init) 阶段执行类构造器 \ 方法,如果赋值运算是通过其他类的静态方法来完成的,那么会马上解析另外一个类,在虚拟机栈中执行完毕后通过返回值进行赋值。

    image-20190326114613480

类加载是一个将 .class 字节码文件实例化成 Class 对象并进行相关初始化的过程。在这个过程中,JVM 会初始化继承树上还没有被初始化过的所有父类。并且会执行这个链路上所有未执行过的静态代码块、静态变量赋值语句等。某些类在使用时,也可以按需由类加载器进行加载。

类加载器

类加载器类似于原始部落结构,存在权力等级制度。

  • Bootstrap,启动类加载器,在 JVM 启动时创建,通常由与操作系统相关的本地代码,是最根基的类加载器,负责装载最核心的 Java 类,比如 Object、System、String 等。
  • Platform ClassLoader (jdk9)/ Extension ClassLoader(jdk 9之前) 平台(扩展)类加载器。用以加载一些扩展的系统类,比如 XML、加密、压缩相关的功能类等。
  • Application ClassLoader, 应用类加载器。主要是加载用户定义的CLASSPATH 路径下的类。

类加载器具有等级制度,但是并非继承关系,以组合的方式复用父加载器的功能,这也符合组合优先原则,详细的双亲委派模型如下图:

低层次的当前类加载器,不能覆盖更高层次类加载器已经加载的类。如果低层次的类加载器想加载一个未知类,要非常礼貌地向上逐级询问:”请问,这个类被加载了吗?” 被询问的高层次类加载器会自问两个问题:第一,我是否已加载过此类?第二,如果没有,是否可以加载此类?只有当所有高层次类加载器在两个问题上的答案均为 “否” 时,才可以让当前类加载器加载这个未知类。如上图所示,左侧绿色箭头向上逐级询问是否已加载此类,直至 Bootstrap ClassLoader,然后向下逐级尝试是否能够加载此类,如果都加载不了,则通知发起请求的当前类加载器,准予加载。

需要用类加载器的场景
  1. 隔离加载类。在某些框架内进行中间件与应用的模块儿隔离,把类加载到不同的环境。
  2. 修改类加载方式。类的加载模型并非强制,除 Bootstrap 外,其他的加载并非一定要引入,或者根据实际情况在某个时间点进行按需进行动态加载。
  3. 扩展加载源。
  4. 防止源码泄露。Java 代码容易被编译和篡改,可以进行编译加密。那么类加载器也需要自定义,还原加密的字节码。

4.3 内存布局

内存是硬盘和 CPU 的中间仓库及桥梁,承载着操作系统和应用程序的实时运行。JVM 内存布局规定了 Java 在运行过程中内存申请、分配、管理的策略,保证了 JVM 的高效稳定运行。

image-20190327105854062
image-20190327105854062

image-20190327161714341

VisualVM 内存布局与GC 图。

  1. Heap ( 堆区 )
image-20190327161641897
image-20190327161641897
  1. Metaspace (元空间)

    在 JDK8 版本中,元空间的前身 Perm 区已经被淘汰。在JDK 7及之前的版本中,只有Hotspot 才有 Perm 区,译为永久代。

    区别于永久代,元空间在本地内存中分配。在 JDK8 里,Perm 区中的所有内容中 字符串常量 移至堆内存,其他内容包括类元信息、字段、静态属性、方法、常量等都移动至元空间内。

  2. JVM Stack (虚拟机栈)

    描述 Java 方法执行的内存区域,它是线程私有的。栈中的元素用于支持虚拟机进行方法调用,每个方法从开始调用到执行完成的过程,就是栈帧从入栈到出栈的过程。在活动线程中,只有处于栈顶的帧才是有效的,称为当前栈帧。栈帧是当前方法运行的基本结构。在执行引擎运行时,所有指令都只能针对当前栈帧进行操作。而 StackOverflowError 表示请求的栈溢出,导致内存耗尽,通常出现在递归方法中。

    image-20190327175155645

    • 局部变量表

      存放方法参数和局部变量的区域。

    • 操作栈

      初始状态为空的桶式结构栈,在方法执行过程中,会有各种指令往栈中写入和提取信息。

    • 动态连接

      每个栈帧中包含一个在常量池中对当前方法的引用,目的是支持方法调用过程的动态链接。

    • 方法返回地址

      方法执行时有两种退出情况:第一,正常退出,即正常执行到任何方法的返回字节码指令,如RETURN、IRETURN、ARETURN 等。第二,异常退出。无论何种退出情况,都将返回至方法当前被调用的位置。方法退出的过程相当于弹出当前栈帧,退出可能有三种方式:

      • 返回值压入上层调用栈帧。
      • 异常信息抛给能够处理的栈帧。
      • PC 计数器指向方法调用后的下一条指令。
  3. Native Method Stacks (本地方法栈)

    本地方法栈在 JVM 内存布局中,也是线程对象私有的,但是虚拟机栈”主内”,而本地方法栈”主外”。内外针对 JVM 而言。对于内存不足的情况,本地方法栈还是会抛出 native heap OutOfMemory。

  4. Program Counter Register(程序计数寄存器)

    程序计数器用来存放执行指令的偏移量和行号指示器等,线程执行或恢复都要依赖程序计数器。程序计数器在各个线程之间互不影响,此区域也不会发生内存溢出异常。

Java 的线程与内存
image-20190327171801739
image-20190327171801739

4.4 对象实例化

  • 字节码角度
    • NEW: 如果找不到 Class 对象,则进行类加载。加载成功后,则在堆中分配内存,从 Object 开始到本类路径上的所有属性值都要分配内存。分配完毕后,进行零值初始化。在分配过程中,注意引用是占据存储空间的,它是一个变量,占用 4 个字节。这个指令完毕后,将指向实例对象的引用变量压入虚拟机栈顶。
    • DUP: 在栈顶复制该引用变量,这时的栈顶有两个指向堆内实例对象的引用变量。如果 \ 方法有参数,还需要把参数压入操作栈中。两个引用变量的目的不同,其中压至底下的引用用于赋值,或者保存到局部变量表,另一个栈顶的引用变量作为句柄调用相关方法。
    • INVOKESPECIAL:调用对象实例方法,通过栈顶的引用变量调用 \ 方法。\ 是类初始化时执行的方法,而 \ 是对象初始化时执行的方法。
  • 执行步骤角度分析
    • 确认类元信息是否存在。
    • 分配对象内存。
    • 设定默认值。
    • 设置对象头。
    • 执行 init 方法。

4.5 垃圾回收

垃圾回收 (Garbage Collection, GC)。垃圾回收的主要目的是清除不再使用的对象,自动释放内存。

可以作为GC Roots 的对象:类静态属性中引用的对象、常量引用的对象、虚拟机栈中引用的对象、本地方法栈中引用的对象等。

垃圾回收算法

标记 - 清除算法:从每个 GC Roots 出发,依次标记有引用关系的对象,最后将没有被标记的对象清除。该算法会带来大量的空间碎片。

标记 - 整理算法:从 GC Roots 出发标记存活的对象,然后将存活对象整理到内存空间的一段,形成连续的已使用空间,最后把已使用空间之外的部分全部清理掉,这样就不会产生空间碎片的问题。

Mark - Copy 算法:为了能够并行地标记和整理将空间分为两块,每次只激活其中一块,垃圾回收时只需把存活的对象复制到另一块未激活空间上,将未激活空间标记为已激活,将已激活空间标记为未激活,然后清除原空间中的原对象。堆内存空间分为较大的 Eden 和两块较小的 Survivor,每次只使用 Eden 和 Survivor 区的一块。这种情形下的”Mark - Copy” 减少了内存空间的浪费。 “Mark - Copy” 现作为主流的 YGC 算法进行新生代的垃圾回收。

垃圾回收器

垃圾回收器(Garbage Collector) 是实现垃圾回收算法并应用在 JVM 环境中的内存管理模块。

Serial(串行) 回收器,主要应用于 YGC 的垃圾回收器,采用串行单线程的方式完成 GC 任务。FGC 的时间相对较长,频繁 FGC 会严重影响应用程序的性能。

image-20190328142851197
image-20190328142851197

CMS 回收器(Concurrent Mark Sweep Collector) 是回收停顿时间比较短、目前比较常用的垃圾收集器。它通过 初始标记(Initial Mark) 、并发标记(Concurrent Mark)、整理标记(Remark)、并发清除(Concurrent Sweep) 四个步骤完成垃圾回收工作。CMS 采用的是 “标记 - 清除算法”,因此产生大量的空间碎片。为了解决这个问题,CMS 可以通过配置 -XX: +UseCMSCompactAtFullCollection 参数,强制 JVM 在 FGC 完成后对老年代进行压缩,执行一次空间碎片整理。

G1(Garbage - First Garbage Collector) 垃圾回收,通过 -XX: +UseG1GC 参数启动。和 CMS 相比,G1 具备压缩功能,能避免碎片问题,G1 的暂停时间更加可控。G1 采用的是 “Mark - Copy”, 有非常好的空间整合能力,不会产生大量的空间碎片。G1 的一大优势在于可预测的停顿时间,能够尽可能快地在指定时间内完成垃圾回收任务。在JDK11 内,已经将 G1 设为默认垃圾回收器。

参考

https://www.cnblogs.com/xiaoxi/p/6959615.html

第 6 章 数据结构与集合

常言道,”程序 = 数据结构 + 算法”。集合作为数据结构的载体,可对元素进行加工和输出,以一定的算法实现最基本的增删改查功能,因此集合是所有编程语言的基础。

高并发编程时代,集合引发的相关故障,比如:多线程共享集合时出现的脏数据问题;某些集合在 数据扩容 时出现节点之间的 死链问题 (例如:hashMap);写多读少的场景误用某些集合导致性能下降问题等。

6.1 数据结构

1. 数据结构定义

数据结构是指 逻辑意义 上的 数据组织方式 及其相应的 处理方式

(1) 什么是逻辑意义? 数据结构的抽象表达非常丰富,而实际物理存储的方式相对单一。

(2) 什么是数据组织方式? 逻辑意义上的组织方式有很多,比如树、图、队列、哈希等。

(3) 什么是数据处理方式?在既定的数据组织方式上,以某种特定的算法实现数据的增加、删除、修改、查找和遍历。

2. 数据结构分类

直接前继直接后继 个数的维度来看,大体可以将数据结构分为以下四类。

(1) 线性结构:0 至 1 个直接前继和直接后继。线性结构包括 顺序表、链表、栈、队列等。

(2) 树结构: 0至 1个直接前继和 0 至 n 个直接后继(n 大于或等于 2)。

(3) 图结构: 0 至 n 个直接前继和直接后继(n 大于或等于2)。图结构包含简单图、多重图、有向图、和无向图等。

(4) 哈希结构: 没有直接前继和直接后继。哈希结构通过某种特定的哈希函数将索引与存储的值关联起来。

3. 常用算法复杂度排序

常数级 $O(1)$、对数级 $O(logn)$、线性级 $O(n)$、线性对数级 $O(nlogn)$、平方级 $O(n^2)$、立方级 $O(n^3)$、指数级 $O(2^n)$

image-20190415134824007
image-20190415134824007

典型的增长数量级函数

6.2 集合框架图

红色代表接口,蓝色代表抽象类,绿色代表并发包中的类,灰色代表早期线程安全的类(基本已经弃用)。

6.2.1 List集合

最常用的为ArrayList 和 LinkedList。LinkedList 的本质是 双向链表,除继承 AbstractSequentialList (抽象顺序访问结构,ArrayList 为随机访问结构) 外,还实现了另一个接口 Deque,既 double-ended queue。这个接口同时具有队列和栈的性质。LinkedList的优点在于可以将零散的内存单元通过附加引用的方式关联起来,形成按链路顺序查找的线性结构,内存利用率较高。

6.2.2 Queue集合
6.2.3 Map集合
6.2.4 Set 集合

6.3 集合初始化

集合默认值大小,ArrayList 大小为10,而 HashMap 默认值为 16.

6.4 数组与集合

6.5 集合与泛型

6.6 元素的比较

6.7 fail - fast 机制

集合世界中比较常见的错误检测机制。

6.8 Map 类集合

1
2
3
4
5
6
7
// 返回Map 类对象中的Key 的Set 视图
Set<K> keySet();
// 返回Map 类对象中的所有Value 集合的Collection 视图
// 返回的集合实现类为 Values extends AbstractCollection<V>
Collection<V> values();
// 返回Map 类对象中的 Key-Value 对的Set 视图
Set<Map.Entry<K, V>> entrySet();

通常这些视图支持清除操作,但是修改和增加元素会抛出异常。

image-20190415173944013
image-20190415173944013
6.8.1 红黑树
1. 树

高度:从某节点出发,到叶子节点为止,最长简单路径上边的条数

深度:从根节点出发,到某节点边的条数

叶子节点:如果某节点下方没有任何分叉的话。

2. 平衡二叉树

为什么需要平衡二叉树,如果有些树的结构和链表差不多,这样表示的话,就不能充分利用树的特性,为了让像链表一样的树更有层次结构,这时候就需要转换为平衡二叉树。

  • 树的左右高度差不能超过1,只能为0 或者 1
  • 任何向下递归的左子树与右子树,必须符合第一条性质,左右子树的高度差也不能超过1
  • 没有任何节点的空树或只有根节点的树也是平衡二叉树
3. 二叉查找树

又称为 二叉搜索树 ,即 Binary Search Tree ,其中 Search 也可以替换为 Sort,所以也称为 二叉排序树 。对于任意节点来说,它的左子树上所有节点的值都小于它,而它的右子树上所有节点的值都大于它。

  • 遍历的方式通常有三种:前序遍历、中序遍历、后序遍历。按中序遍历二叉查找树,就是坐标轴的顺序。
    • 在任何递归子树中,左节点一定在右节点之前先遍历
    • 前序、中序、后序,仅指 根节点 在遍历时的位置顺序。
  • 二叉查找树由于随着数据不断地增加或删除容易失衡,为了保持二叉树重要的平衡性,有很多算法的实现,如AVL树、红黑树、SBT( Size Balanced Tree )、Treap( 树堆)等。Java 底层很多以红黑树为主。
4. AVL 树

一种平衡二叉查找树,增加和删除节点后通过树形旋转重新达到平衡。

  • 右旋:以某个节点为中心,将它沉入当前右子节点的位置,而让它当前的左节点作为新树的根节点,也称为顺时针旋转。
  • 左旋:以某个节点为中心,将它沉入当前左子节点的位置,而让它当前的右节点作为新树的根节点,也称为逆时针旋转。

5. 红黑树

主要特征是在每个节点上增加一个属性来表示节点的颜色,可以是红色,也可以是黑色。

  • 红黑树与AVL树类似,都是在进行插入和删除元素时,通过特定的旋转来保持自身平衡的,从而获得较高的查找性能。与AVL树相比,不用保证每个递归子树的高度差不超过1,而是保证从 根节点到叶子节点的最长路径不超过最短路径的2倍,所以它的最坏运行时间也是 $ O(log n)$。红黑树通过重新着色和左右旋转,更加高效地完成了插入和删除操作后的自平衡调整。

  • 5个约束条件

    1. 节点只能是红色或黑色

    2. 根节点必须是黑色

    3. 所有 NIL 节点都是黑色的。 NIL,即叶子节点下挂的两个虚节点。

    4. 一条路径上不能出现相邻的两个红色节点

    5. 在任何递归子树上,根节点到叶子节点的所有路径上包含相同数目的黑色节点。

总结一下:有红必有黑,红红不相连, 上述5个约束条件保证了红黑树的新增、删除、查找的最坏时间复杂度均为 $O(logn)$。如果一个树的左子节点或右子节点不存在,则均认定为黑色。红黑树的任何旋转在3次之内均可完成。

6. 红黑树与 AVL树的比较

面对频繁的插入和删除,红黑树更加合适;面对低频修改、大量查询时,AVL树将更加合适。

6.8.2 TreeMap
6.8.3 HashMap
6.8.4 ConcurrentHashMap

在转化过程中,使用同步块锁住当前槽的首元素,防止其他线程对当前槽进行增删改操作,转化完成后利用CAS替换原有链表

第 7 章 并发与多线程

并发(Concurrency)与并行(Parallelism):并发是指在某个时间段内,多任务交替处理的能力。并行是指同时处理多任务的能力。目前,cpu 已经发展为多核,可以同时执行多个互不依赖的 指令及执行块。它们的核心区别在于进程是否同时执行。以KTV 唱歌为例,并行指的是有很多人可以使用话筒同时唱歌;并发指的是同一话筒被多个人同时使用。

并发的特点:

  1. 并发程序之间有互相制约的关系。直接制约体现为一个程序需要另一个程序的计算结果;间接制约体现为多个程序竞争共享资源,如处理器、缓冲区等。
  2. 并发程序的执行过程是断断续续的。程序需要记忆 现场指令及执行点
  3. 并发数设置合理 并且 cpu 拥有足够的处理能力时,并发会提高程序的运行效率。

7.1 线程安全

线程可以拥有自己的 操作栈、程序计数器、局部变量表 等资源,它与同一进程内的其他线程共享该进程的所有资源。线程在生命周期内存在多种状态。有 NEW (新建状态)、RUNNABLE (就绪状态)、RUNNING (运行状态)、BLOCKED(阻塞状态)、DEAD(终止状态) 五种状态。线程安全的核心理念就是:“要么只读,要么加锁”。

image-20190305105908153
image-20190305105908153
  1. new,即新建状态,是线程被创建且未启动的状态。创建线程的方式有三种:第一种是继承自Thread 类,第二种是实现 Runnable 接口,第三种是实现Callable 接口。
  2. runnable, 即就绪状态,是调用start() 之后运行之前的状态。线程的start() 不能被多次调用,否则会抛出IllegalStateException 异常。
  3. running,即运行状态,是run() 正在执行时线程的状态。线程可能会由于某些因素而退出 running,如时间、异常、锁、调度等。
  4. blocked,即阻塞状态,进入此状态,有以下几种情况
    • 同步阻塞:锁被其他线程占用。
    • 主动阻塞:调用 Thread 的某些方法,主动让出 CPU 执行权,比如 sleep()、join() 等。
    • 等待阻塞:执行了 wait()。
  5. dead,既终止状态,是run() 执行结束,或因异常退出后的状态,此状态不可逆转。

保证高并发场景下的线程安全,可以从以下四个维度考量:

  1. 数据单线程内可见。单线程总是安全的,通过限制数据仅在单线程内可见,可以避免数据被其他线程篡改。最典型的就是线程局部变量,它存储在独立虚拟机栈帧的局部变量表中,与其他线程毫无瓜葛。ThreadLocal 就是采用这种方式来实现线程安全的。
  2. 只读对象。只读对象总是安全的。它的特性是允许复制、拒绝写入。最典型的只读对象有 String、Integer等。一个对象想要拒绝任何写入,必须要满足以下条件:1. 使用 final关键字修饰类,避免被继承; 2. 使用 private final 关键字避免属性被中途修改; 3. 没有任何更新方法; 4. 返回值不能可变对象为引用。
  3. 线程安全类。某些线程安全类的内部有非常明确的线程安全机制。比如StringBuffer就是一个线程安全类,它采用 synchronized关键字来修饰相关方法。
  4. 同步与锁机制。如果想要对某个对象进行并发更新操作,但又不属于上述三类,需要工程师在代码中实现安全的同步机制。

并发包主要包括以下几个类族

  1. 线程同步类。这些类使线程间的协调更加容易,支持了更加丰富的线程协调场景,逐步淘汰了使用 Object 的 wait()和 notify()进行同步的方式。主要代表为CountDownLatch、 Semaphore、 CyclicBarrier 等。
  2. 并发集合类。集合并发操作的要求是执行速度快,提取数据准。最著名的类非 ConcurrentHashMap 莫属,它不断地优化,由刚开始的锁分段到后来的 CAS,不断地提升并发性能。其他还有 ConcurrentSkipListMap、 CopyOnWriteArrayList、BlockingQueue 等。
  3. 线程管理类。如 Executors静态工厂、ThreadPoolExecutor 。ScheduledExecutorService 来执行定时任务。
  4. 锁相关类。锁以 Lock 接口为核心,派生出在一些实际场景中进行互斥操作的锁相关类。最有名的是ReentrantLock。

7.2 什么是锁

锁主要提供了两种特性:互斥性和不可见性。

AbstractQueuedSynchronizer ( AQS ),是 JUC 包实现同步的基础工具。在 AQS 中, 定义了一个 volatile int state变量作为共享资源,如果线程获取资源失败,则进入同步 FIFO 队列中等待; 如果成功获取资源就执行临界区代码。执行完释放资源时, 会通知同步队列中的等待线程来获取资源后出队并执行。

7.3 线程同步

7.3.1 同步是什么

资源共享的两个原因是 资源紧缺共建需求 。线程共享 cpu 是从资源紧缺的维度来考虑的,而多线程共享同一变量,通常是从共建需求的维度来考虑的。

7.4 线程池

7.4.1 线程池的好处

线程池的作用包括:

  1. 利用线程池管理并复用线程、控制最大并发数等。
  2. 实现任务线程队列缓存策略和拒绝机制。
  3. 实现某些与时间相关的功能,如定时执行、周期执行等。
  4. 隔离线程环境。比如,交易服务和搜索服务在同一台服务器上,分别开启两个线程池,交易线程的资源消耗明显要大;因此,通过配置独立的线程池,将较慢的交易服务与搜索服务隔离开,避免各服务线程互相影响。
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
public ThreadPoolExecutor(int corePoolSize,    			// 参数1
int maximumPoolSize, // 参数2
long keepAliveTime, // 参数3
TimeUnit unit, // 参数4
BlockingQueue<Runnable> workQueue, // 参数5
ThreadFactory threadFactory, // 参数6
RejectedExecutionHandler handler) { // 参数7
if (corePoolSize < 0 ||
// maximumPoolSize 必须大于或等于1 也要大于或等于 corePoolSize
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;
}
  1. 第一个参数:corePoolSize 表示常驻核心线程数。如果等于0,则任务执行完之后,没有任何请求进入时销毁线程池的线程;如果大于0,即使本地任务执行完毕,核心线程也不会被销毁。这个值的设置非常关键,设置过大会浪费资源,设置过小会导致线程频繁地创建或销毁。
  2. 第二个参数:maximumPoolSize 表示线程池能够容纳同时执行的最大线程池。必须大于或等于1。当运行的线程数大于corePoolSize,小于maximumPoolSize的时候,新来一个任务就会加入到队列中,只有在队列满的时候才会新创建线程来执行。
  3. 第三个参数:keepAliveTime 表示线程池中的线程空闲时间,当空闲时间达到 keepAliveTime 值时,线程会被销毁,直到只剩下 corePoolSize 个线程为止,避免浪费内存和旬柄资源。在默认情况下,当线程池的线程数大于 corePoolSize 时,keepAliveTime 才会起作用。但是当 ThreadPoolExecutor 的allowCoreThreadTimeOut 变量设置为 true 时, 核心线程超时后也会被回收。
  4. 第四个参数:TimeUnit表示时间单位。 keepAliveTime 的时间单位通常是TimeUnit.SECONDS。
  5. 第五个参数:workQueue 表示缓存队列。
  6. 第六个参数:threadFactory 表示线程工厂。它用来生产一组相同任务的线程。线程池的命名是通过给这个 factory 增加组名前缀来实现的。在虚拟机分析时,就可以知道线程任务是由那个线程工厂产生的。
  7. 第七个参数:handler 表示执行拒绝策略的对象。当超过第五个参数 workQueue 的任务缓存区上限的时候,就可以通过该策略处理请求,这是一种简单的限流保护。友好的拒绝策略可以是如下三种:
    1. 保护到数据库进行削峰填谷。在空闲时再提取出来执行。
    2. 转向某个提示页面。
    3. 打印日志。
7.4.2 线程池源码详解

7.5 ThreadLocal

7.5.1 引用类型
image-20190313093800269
image-20190313093800269

引用类型分为四种,按照由强到弱分别为强引用 > 软引用 > 弱引用 > 虚引用。

  • 强引用,即 Strong Reference。只要对象有强引用指向,并且 GC Roots 可达,那么 Java 内存回收时,即便频临内存耗尽,也不会回收该对象。
  • 软引用,即 Soft Reference,在即将 OOM 之前,垃圾回收器会把这些软引用指向的对象加入回收范围,以获得更多的内存空间,让程序能够继续健康运行。主要用来缓存服务器中间计算结果及不需要实时保存的用户行为等。
  • 弱引用,即 Weak Reference,如果弱引用指向的对象只存在弱引用这一条线路,则在下一次 YGC 时会被回收。由于 YGC 时间的不确定性,弱引用何时被回收也具有不确定性。弱引用主要用于指向某个易消失的对象,在强引用断开后,此引用不会劫持对象。调用 WeakReference.get() 可能返回 null,要注意空指针异常。
  • 虚引用,即 Phantom Reference,是极弱的一种引用关系,定义完成后,就无法通过该引用获取指向的对象。为一个对象设置虚引用的唯一目的就是希望能在这个对象被回收时收到一个系统通知。虚引用必须与引用队列联合使用,当垃圾回收时,如果发现存在虚引用,就会在回收对象内存前,把这个虚引用加入与之关联的引用队列中。
7.5.2 ThreadLocal 价值

构造这样一个对象,将这个对象设置为共享变量,统一设置初始值,但是每个线程对这个值的修改都是互相独立的。这个对象就是 ThreadLocal。英文的恰当名称应该叫作:CopyValueIntoEveryThread。

ThreadLocal 采用哈希表的方式来为每个线程都提供一个变量的副本。

1
2
// Thread 类的成员变量
ThreadLocal.ThreadLocalMap threadLocals = null;
1
2
3
4
// 给每个线程创建 一个 ThreadLoaclMap  
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}