关于后端:读书笔记之深入理解Java虚拟机JVM高级特性与最佳实践下

37次阅读

共计 11916 个字符,预计需要花费 30 分钟才能阅读完成。

💡 学而不思则罔,思而不学则殆。—— 孔子

👉 微信公众号已开启,菜农曰,没关注的同学们记得关注哦!

本篇带来的是周志明老师编写的《深刻了解 Java 虚拟机:JVM 高级个性与最佳实际》,非常硬核!

全书共分为 5 局部,围绕 内存治理 执行子系统 程序编译与优化 高效并发 等外围主题对 JVM 进行了全面而深刻的剖析,粗浅揭示了 JVM 工作原理。

全书整体 5 个局部,十三章,共 358929 字。整体构造相当清晰,以至于写读书笔记的时候无从摘抄(甚至想把全书复述一遍),以下是全书第三局部的内容,望读者细细品味!

一、第三局部 虚拟机执行子系统

代码编译的后果从本地机器码转变为字节码,是存储格局倒退的一小步,却是编程语言倒退的一大步

第六章 类文件构造

计算机只意识 0 和 1,所以咱们写的程序须要经编译器翻译成由 0 和 1 形成的二进制格局能力由计算机执行。

1)无关性的基石

各种不同平台的虚拟机与所有平台都对立应用的程序存储格局 — 字节码(ByteCode)是形成平台无关性的即时。

2)Class 类文件的构造

任何一个 Class 文件都对应着惟一一个类或接口的定义信息,但反过来,类或接口并不一定都得定义在文件里(譬如类或接口也能够通过类加载器间接生成)

Class 文件是一组以 8 位字节为根底单位的二进制流,各个数据我的项目严格依照程序紧凑地排列在 Class 文件之中,两头没有任何分隔符,这使得整个 Class 文件中存储的内容简直全副是程序运行的必要数据。

Class 文件格式采纳一种相似 C 语言构造体的伪构造来存储数据,这种伪构造中只有两种数据类型:

  • 无符号数:根本的数据类型,能够用来形容数字、索引援用、数量值或者依照 UTF- 8 编码形成字符串值
  • :由多个无符号数或者其余表作为数据项形成的复合数据类型,所有表都习惯性地以 _info 结尾。
1. 魔数与 Class 文件的版本

每个 Class 文件的头 4 个字节称为 魔数(0xCAFEBABE),它的惟一作用是确定这个文件是否为一个能被虚拟机承受的 Class 文件。

紧接着魔数的 4 个字节存储的是 Class 文件的版本号:第 5 和第 6 个字节是次版本好,第 7 和第 8 个字节是主版本号。

Java 的版本号是从 45 开始的,JDK 1.1 之后的每个 JDK 大版本公布主版本号向上加 1

2. 常量池

主次版本号之后便是 常量池的入口

常量池中常量的数量是不固定的,所以在常量池的入口会搁置一项 u2 类型的数据,代表常量池容量计数值。

常量池容量(偏移地址:0x00000008)为十六进制数 0x0016,即十进制的 22,这就代表常量池中有 21 项常量,索引值范畴为 1~21。

常量池次要寄存两大类常量:字面量 符号援用

符号援用包含了三类常量:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 办法的名称和描述符
3. 拜访标记

在常量池完结之后,紧接着的两个字节代表拜访标记(access_flags),这两个标记用于辨认一些类或接口档次的访问信息

  • 这个 Class 是类还是接口
  • 是否定义为 public 类型
  • 是否定义为 abstract 类型
  • 如果是类的话是否申明为 final
4. 类索引、父类索引和接口索引汇合

类索引和父类索引都是一个 u2 类型的数据,而接口索引汇合是一组 u2 类型的数据汇合,Class 文件中由这三项数据来确定这个类的继承关系。

从偏移地址 0x000000F1 开始的 3 个 u2 类型的值别离为0x00010x00030x0000,也就是类索引为 1,父类索引为 3,接口索引汇合大小为 0,而后通过 javap 命令计算出来的常量池,找出对应的类和父类的常量

5. 字段表汇合

字段表用于形容接口或类中申明的变量。

绝对于全限定名和简略名称来说,办法和字段的描述符就要简单一些。描述符的作用是用来形容字段的数据类型、办法的参数列表(包含数量、类型以及程序)和返回值。依据描述符规定,根本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的 void 类型都用一个大写字符来示意,而对象类型则用字符 L 加对象的全限定名来表。

对于数组类型,每一维度将应用一个前置的“[”字符来形容

6. 办法表汇合

办法表的构造如同字段表一样,顺次包含了拜访标记(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表汇合(attributes)几项。

而办法外面的代码,通过编译器编译器编译成字节码指令后,寄存在办法属性表汇合中一个名为 Code 的属性里,属性表作为 Class 文件格式中最具扩展性的一种数据我的项目。

如果父类办法在子类中没有被重写(Override),办法表汇合中就不会呈现来自父类的办法信息。但同样的,有可能会呈现由编译器主动增加的办法,最典型的便是类结构器 \<clinit> 办法和实例结构器 \<init> 办法。

7. 属性表汇合

属性表汇合的限度略微宽松一些,不再要求各个属性表具备严格程序,只有不与已有属性名反复,任何人实现的编译器都能够向属性表中写入本人定义的属性信息。

3)字节码指令简介

Java 虚拟机的指令由一个字节长度的、代表着某种特定操作含意的数字(称为操作码,Opcode)以及追随其后的零至少

个代表此操作所需参数(称为操作数,Operands)而形成。

第七章 虚拟机类加载机制

虚拟机把形容类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终造成能够被虚拟机间接应用的 Java 类型,这就是虚拟机的类加载机制。

1)类加载的机会

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包含:加载 (Loading)、 验证 (Verification)、 筹备 (Preparation)、 解析 (Resolution)、 初始化 (Initialization)、 应用 (Using) 和卸载(Unloading)7 个阶段。

加载、验证、筹备、初始化和卸载这 5 个阶段的程序是确定的,类的加载过程必须依照这种程序循序渐进地开始,而解析阶段则不肯定:它在某些状况下能够在初始化阶段之后再开始,这是为了反对 Java 语言的运行时绑定(也称为动静绑定或早期绑定)

初始化的机会

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

2)类加载的过程

1. 加载

在加载阶段,虚拟机须要实现以下 3 件事件:

  • 通过一个类的全限定名来获取定义此类的二进制字节流
  • 将这个字节流所代表的动态存储构造转化为办法区的运行时数据结构
  • 在内存中生成一个代表这个类的 java.lang.Class 对象,作为办法区这个类的各种数据的拜访入口
2. 验证

验证是连贯阶段的第一步,这一阶段的目标是为了确保 Class 文件的字节流中蕴含的信息合乎以后虚拟机的要求,并且不会危害虚拟机本身的平安。

Class 文件并不一定要求用 Java 源码编译而来,能够应用任何路径产生,甚至包含用十六进制编辑器间接编写来产生 Class 文件。

验证阶段会实现 4 个阶段的测验动作:

  • 文件格式验证
  1. 是否以魔数 OxCAFEBABE 结尾
  2. 主、次版本是否在以后虚拟机解决范畴之内
  3. 常量池的常量中是否有不被反对的常量类型(查看常量 tag 标记)

该验证阶段的次要目标是保障输出的字节流能正确地解析并存储在办法区之中,是基于二进制字节流进行的。

  • 元数据验证
  1. 该类是否有父类
  2. 这个类的父类是否继承了不容许被继承的类(final 润饰的类)
  3. 如果类不是形象的,是否实现了其父类或接口之中要求实现的所有办法

该阶段是对字节码形容的信息进行语义剖析,以保障其形容的信息合乎 Java 语言标准的要求。次要目标是对类的元数据进行语义校验,保障不存在不合乎 Java 语言标准的元数据信息。

  • 字节码验证
  1. 保障任意时刻操作数栈的数据类型与指令代码序列都能来配合工作
  2. 保障跳转指令不会跳转到办法体以外的字节码

该阶段的次要目标是通过数据流和控制流剖析,确定程序语义是非法的,合乎逻辑的。

  • 符号援用验证
  1. 符号援用中通过字符串形容的全限定名是否能找到对应的类
  2. 在指定类中是否存在合乎办法的字段形容以及简略名称所形容的办法和字段

该阶段的次要目标是对类本身以外(常量池中的各种符号援用)的信息进行匹配性校验,确保解析动作可能失常执行。

3. 筹备

筹备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所应用的内存都将在办法区中进行调配。

4. 解析

解析阶段是虚拟机将常量池的符号援用替换成间接援用的过程。

  • 符号援用:符号援用以一组符号来形容所援用的指标,符号能够是任何模式的字面量,只有应用时能无歧义地定到指标即可。符号援用与虚拟机实现的内存布局无关,援用的指标并不一定曾经加载到内存中。
  • 间接援用:间接援用能够是间接指向指标的指针、绝对偏移量或是一个能间接定位到指标的句柄。间接援用是和虚拟机实现的内存布局相干的,同一符号援用在不同虚拟机实例上翻译进去的间接援用个别不会雷同、如果有了间接援用,那援用的指标必然曾经在内存中存在。
5. 初始化

类初始化阶段是类加载过程的最初一步。初始化阶段是执行类结构器 \<clinit> 办法的过程。

3)类加载器

类加载阶段中 通过一个类的全限定名来获取形容此类的二进制字节流 这个动作放到了 Java 虚拟机内部去实现,以便让应用程序本人决定如何去获取所须要的类。实现这个动作的代码模块称为 类加载器

1. 类与类加载器

每一个类加载都领有一个独立的类名称空间。比拟两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即便这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只有加载它们的类加载器不同,那这两个类就必然不相等。

2. 双亲委派模型

类加载器能够划分为 3 类:

  • 启动类加载器(Bootstrap ClassLoader)

这个类加载器负责将寄存在 <JAVA_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的门路中的

  • 扩大类加载器(Extension ClassLoader)

这个加载器由 sun.misc.Launcher$ExtClassLoader 实现,它负责加载 <JAVA_HOME>\lib\ext 目录中的,或者被 java.ext.dirs 零碎变量所指定的门路中的所有类库,开发者能够间接应用扩大类加载器。

  • 应用程序类加载器(Application ClassLoader)

这个类加载器由 sun.misc.Launcher$App-ClassLoader 实现。因为这个类加载器是 ClassLoader 中的 getSystemClassLoader() 办法的返回值,所以个别也称它为零碎类加载器。如果应用程序中没有自定义过本人的类加载器,个别状况下这个就是程序中默认的类加载器

这里类加载器之间的父子关系个别不会以继承(Inheritance)的关系来实现,而是都应用组合(Composition)关系来复用父加载器的代码。

双亲委派模型的工作过程

如果一个类加载器收到了类加载的申请,它首先不会本人去尝试加载这个类,而是把这个申请委派给父类加载器去实现,每一个档次的类加载器都是如此,因而所有的加载申请最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈本人无奈实现这个加载申请(它的搜寻范畴中没有找到所需的类)时,子加载器才会尝试本人去加载。

3. 毁坏双亲委派模型
  1. JDK 1.2 之后已不提倡用户再去笼罩 loadClass() 办法,而该当把本人的类加载逻辑写到 findClass() 办法中,在 loadClass() 办法的逻辑里如果父类加载失败,则会调用本人的 findClass() 办法来实现加载,这样就能够保障新写进去的类加载器是合乎双亲委派规定的。
  2. 线程上下文类加载器(Thread Context ClassLoader)。这个类加载器能够通过 java.lang.Thread 类的 setContextClassLoaser() 办法进行设置,如果创立线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范畴内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

第八章 虚拟机字节码执行引擎

1)运行时栈帧构造

栈帧是用于反对虚拟机进行办法调用和办法执行的数据结构,它是虚拟机进行办法调用和办法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。

栈帧存储了办法的 局部变量表 操作数栈 动静连贯和办法返回地址 等信息。

一个线程办法中的调用链可能会很长,对于执行引擎来说,只有位于栈顶的栈帧才是无效的,称为 以后栈帧

1. 局部变量表

局部变量表是一组变量值存储空间,用于寄存办法参数和办法外部定义的局部变量。

局部变量表的容量以 变量槽 为最小单位。

2. 操作数栈

操作数栈也常称为操作栈,是一个后入先出栈。操作数栈的最大深度会在编译的时候写入到 Code 属性的 max_stacks 数据项中。

在概念模型中,两个栈帧作为虚拟机栈的元素,是齐全独立的。但在大多数虚拟机的实现中会有一部分优化重叠。这样在进行办法调用时就能够共用一部分数据,毋庸进行额定的参数复制传递。

3. 动静连贯

每个栈帧都蕴含一个指向运行时常量池中该栈帧所属办法的援用,持有这个援用是为了反对办法调用过程中的动静连贯。

4. 办法返回地址

进行办法的运行有两种形式:

  • 执行引擎遇到任意一个办法返回的字节码指令。这种退出形式称为失常实现进口
  • 在办法执行过程中遇到了异样,并且这个异样没有在办法体内遇到解决。这种退出办法的形式称为异样实现进口

2)办法调用

办法调用并不等同于办法执行,办法调用阶段惟一的工作就是确定被调用办法的版本(即调用哪一个办法),临时还不波及办法外部的具体运行过程。

在编译期间,所有办法调用在 Class 文件外面存储的都只是符号援用,只有在类加载期间,甚至到运行期间能力确定指标办法的间接援用

解析

在类加载的解析阶段,会将其中一部分符号援用转化为间接援用,而这种解析成立的条件为:办法在程序真正运行之前就有一个可确定的调用版本,并且这个办法的调用版本在运行期是不可扭转的。

二、第四局部 程序编译与代码优化

从计算机程序呈现的第一天起,对效率的谋求就是程序天生的动摇信奉,这个过程犹如一场没有起点、永不停歇的 F1 方程式比赛,程序员是车手,技术平台则是在赛道上飞驰的赛车

第十章 晚期(编译器)优化

1)Javac 编译器

Javac 编译器自身就是一个由 Java 语言编写的程序

编译过程大抵能够分为 3 个过程,别离是:

  • 解析与填充符号表过程
  • 插入式注解处理器的注解处理过程
  • 剖析与字节码生成过程

Javac 编译动作的入口是 com.sun.tools.javac.main.JavaCompiler

1. 解析与填充符号表

解析:

解析步骤包含了经典程序编译原理中的 词法剖析 语法分析 两个过程

  • 词法剖析过程由 com.sun.tools.javac.parser.Scanner 类来实现,依据 Token 序列结构形象语法树的过程。
  • 语法分析过程由 com.sun.tools.javac.parser.Parser 类来实现,这个阶段产出的形象语法树由 com.sun.tools.javac.tree.JCTree 类示意。

通过这个步骤之后,编译器根本就不会再对源代码进行操作了,后续的操作都建设在形象语法树上。

填充符号表

填充符号表的动作由 enterTrees() 办法实现。

符号表就是由一组符号地址和符号信息形成的表格,其中所注销的信息在编译的不同阶段都要用到。

填充符号表的过程由 com.sun.tools.javac.comp.Enter 类实现,此过程的进口是一个待处理列表(To Do List),蕴含了

每一个编译单元的形象语法树的顶级节点,以及 package-info.java(如果存在的话)的顶级节点

2. 注解处理器

Java 语言提供了对 注解 的反对,这些注解与一般的 Java 代码一样,是在运行期间发挥作用的。

3. 语义剖析与字节码生成

在上述步骤完结后,能够失去一个形象语法树,然而无奈保障源程序是合乎逻辑的,语义剖析的次要工作是对构造上正确的源程序进行上下文无关性质的审查。

  • 标注查看
  • 数据及控制流剖析
  • 解语法糖
  • 字节码生成

第十一章 早期(运行期)优化

1)解释器与编译器

当程序须要迅速启动和执行的时候,解释器能够首先发挥作用,省去编译的工夫,立刻执行。在程序运行后,随着工夫的推移,编译器逐步开始发挥作用,把越来越多的代码编译成本地代码之后,能够获取更高的执行效率。当程序运行环境中内存限度较大(如嵌入式零碎),能够应用解释执行节约内存,反之能够应用编译执行来晋升效率。

HotSpot 虚拟机中内置了两个即时编译器,别离称为 Client Compiler 和 Server Compiler,简称为 C1 编译器 和 C2 编译器。用户能够应用 -client 或 -server 来指定运行在 Client 模式还是 Server 模式

为了在程序启动响应速度与运行速度之间达到最佳均衡,引入了 分层编译

  • 第 0 层:程序解释执行,解释器不开启性能监控性能,可触发第 1 层编译
  • 第 1 层:也称为 C1 编译,将字节码编译为本地代码,进行简略,牢靠的优化,如有必要将退出性能监控的逻辑
  • 第 2 层:也称为 C2 编译,也是将字节码编译为本地代码,然而会启用一些编译耗时较长的优化,甚至会依据性能监控信息进行一些不牢靠的激进优化

2)编译对象与触发条件

在运行过程中会被即时编译器的 热点代码 有两种:

  • 被屡次调用的办法
  • 被屡次执行的循环体

判断一段代码是不是热点代码,是不是须要触发即时编译,这样的行为称为 热点探测

  • 基于采样的热点探测:虚构机会周期性地查看各个线程的栈顶,如果发现某个办法经常出现在栈顶,那这个办法就是热点办法。
  • 基于计数器的热点探测:采纳这种办法的虚构机会为每个办法(甚至是代码块)建设计数器,统计办法的执行次数,如果执行次数超过肯定的阈值就认为它是热点办法。(办法调用计数器 和 回边计数器)

回边计数器:作用是统计一个办法中循环体代码执行的次数,在字节码中遇到管制流向后跳转的指令称为回边。建设回边计数器统计的目标就是为了触发 OSR 编译。

3)编译优化技术

1. 公共子表达式打消

如果一个表达式 E 曾经计算过了,并且从先前的计算到当初 E 中所有变量的值都没有发生变化,那么 E 的这次呈现就成为了公共子表达式。

2. 数组边界查看打消

为了平安,数组边界查看是必须要做的,但不是在每一次运行期间都会进行查看。

3. 办法内联

办法内联的行为是把指标办法的代码复制到发动调用的办法之中,防止产生实在的办法调用。为了解决 Java 中虚办法内联的问题,引入了一种名为 “ 类型继承关系剖析(CHA)” 的 技术,这是一种基于整个应用程序的类型剖析技术,它用于确定在目前已加载的类中,某个接口是否有多于一种的实现,某个类是否存在子类,子类是否为抽象类等信息。

4. 逃逸剖析

逃逸剖析的根本行为就是剖析对象动静作用域:当一个对象在办法中被定义后,它可能被内部办法所援用,例如作为调用参数传递到其余办法中,称为办法逃逸。甚至还有可能被内部线程拜访到,称为线程逃逸。

要证实一个对象不会逃逸到办法或线程之外,须要对这个变量作一些优化:

  • 栈上调配
  • 同步打消
  • 标量替换

三、第五局部 高效并发

并发解决的广泛应用是使得 Amdahl 定律代替摩尔定律称为计算机性能倒退原动力的根本原因,也是人类压迫计算机运算能力的最无力的武器

第十二章 Java 内存模型与线程

1)硬件的效率与一致性

当多个处理器的运算工作都波及同一块主内存区域时,将可能导致各自的缓存数据不统一,因而在读写时要依据协定来操作,如 MSI、MESI、MOSI 等

2)主内存与工作内存

Java 内存模型的次要指标是定义程序中各个变量的拜访规定,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。

3)内存间交互操作

Java 内存模型中定义了以下 8 种操作来实现,虚拟机实现时必须保障上面提及的每一种操作都是原子的、不可再分

  • lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,开释后的变量才能够被其余线程锁定
  • read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作应用
  • load(载入):作用于工作内存的变量,它把 read 操作从主内存失去的变量值放入工作内存的变量正本中
  • use(应用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个须要应用到变量应用到变量的值的字节码指令时将会执行这个操作
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接管到值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
  • store(存储):作用于主内存的变量,它把 store 操作从工作内存中失去的变量的值放入主内存的变量
  • write(写入):作用于主内存的变量,它把 store 操作从工作内存中失去的变量的值放入主内存的变量中

如果要把一个变量从主内存复制到工作内存,那就要程序地执行 read 和 load 操作,如果要把变量从工作内存同步回主内存,就要程序地执行 store 和 write 操作。

Java 内存模型规定了在执行上述 8 种基本操作时需满足的以下规定:

  1. 不容许 read 和 load、store 和 write 操作之一独自呈现,即不容许一个变量从主内存读取了但工作内存不承受,或者从工作内存发动回写了但主内存不承受的状况呈现
  2. 不容许一个线程抛弃它的最近的 assign 操作,即变量在工作内存中扭转了之后必须把该变动同步回主内存
  3. 不容许一个线程无起因地(没有产生过任何 assign 操作)把数据从线程的工作内存同步回主内存中
  4. 一个新的变量只能在主内存中产生,不容许在工作内存中间接应用一个未被初始化(load 或 assign)的变量
  5. 一个变量在同一时刻只容许一条线程对其进行 lock 操作,但 lock 操作能够被同一条线程反复执行屡次,屡次执行 lock 后,只有执行雷同次数的 unlock 操作,变量才会被解锁
  6. 如果对一个变量执行 lock 操作,那将会清空工作内存中此变量的值,在执行引擎应用这个变量前,须要从新执行 load 或 assign 操作初始化变量的值
  7. 如果一个变量当时没有被 lock 操作锁定,那就不容许对它执行 unlock 操作,也不容许去 unlock 一个被其余线程锁定住的变量
  8. 对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存中。
1. 原子性、可见性与有序性

Java 内存模型是围绕着在并发过程中如何解决 原子性、可见性和有序性 这三个特色建设的

1. 原子性

Java 内存模型的 read、load、assign、use、store 和 write 能够间接保障原子性变量操作。

2. 可见性

可见性是指当一个线程批改了共享变量的值,其余线程可能立刻得悉这个批改。除了 volatile 之外,在 Java 中还能够通过 synchronize 和 final 来保障可见性。

3. 有序性

如果在本线程内察看,所有的操作都是有序的,如果在一个线程中察看另一个线程,所有操作都是无序的。

前半句是指:线程内体现为串行的语义,后半句是指:指令重排序景象和工作内存与主内存同步提早景象

4)Java 与线程

1. 线程的实现

各个线程之间既能够共享过程资源(内存地址、文件 I / O 等),又能够独立调度(线程是 CPU 调度的根本单位)

实现线程的次要有 3 种形式:

  1. 内核线程实现

间接由操作系统内核反对的线程,这种线程由内核来实现线程切换。程序个别不会间接取应用内核线程,而是去应用内核线程的一种高级接口— 轻量级过程 LWP = 线程

局限性:

因为基于内核线程实现,各种线程间的操作(创立、析构及同步)都须要进行零碎调用。而零碎调用的代价绝对较高,须要在 用户态 内核态 中来回切换。

  1. 用户线程实现

一个线程只有不是内核线程,就能够认为是用户线程。轻量级过程也属于用户线程。用户线程指的是齐全建设在用户空间的线程库上,零碎内核不能感知线程存在的实现。用户线程的建设、同步、销毁和调度齐全在用户态上实现,不须要内核的帮忙。

局限性:

没有零碎内核的声援,线程的所有操作都须要用户程序本人解决,在 阻塞、调度 之类的问题解决起来会异样艰难

  1. 用户线程加轻量级过程混合实现

在这种混合实现下,既存在用户线程、也存在轻量级过程。用户过程齐全是建设在用户空间中,因而用户线程的创立、切换、析构等操作仍然便宜,并且能够反对大规模的用户线程并发。

2. Java 线程调度

线程调度是指零碎为线程调配处理器使用权的过程,次要调度形式有两种:协同式线程调度 抢占式线程调度

  • 协同式线程调度

线程的执行工夫由线程自身来管制,线程把本人的工作执行完了之后,要被动告诉零碎切换到另外一个线程上。

特点:实现简略,但线程执行的工夫不可管制,如果一个线程编写有问题,就会导致始终阻塞

  • 抢占式线程调度

每个线程将由零碎来调配执行工夫,线程的切换不禁线程自身来决定。

特点:线程的执行工夫是零碎可控的,也不会有一个线程导致整个过程阻塞的问题。

3. 线程状态切换

Java 语言中定义了 5 种线程状态:

  • 新建(New):创键后尚未启动的线程处于这种状态
  • 运行(Runable):Runable 包含了操作系统状态中 Running 和 Ready,也就是处于此状态的线程有可能正在执行,也有可能正在期待着 CPU 为它调配执行工夫
  • 无限期期待(Waiting):处于这种状态的线程不会被调配 CPU 执行工夫,它们要期待被其余线程显式地唤醒
  • 限期期待(Timed Waiting):处于这种状态的线程也不会被调配 CPU 执行工夫,不过毋庸期待被其余线程显式唤醒,在肯定工夫之后它们会由零碎主动唤醒
  • 阻塞(Blocked):线程被阻塞了,期待着获取一个排他锁,这个事件将在另外一个线程放弃这个锁的时候产生
  • 完结(Terminated):已终止线程的线程状态,线程曾经完结执行

第十三章 线程平安与锁优化

1)线程平安

当多个线程拜访一个对象时,如果不必思考这些线程在运行时环境下的调度和交替执行,也不须要进行额定的同步,或者在调用方进行任何其余的协调操作,调用这个对象的行为都能够取得正确的后果,那这个对象就是线程平安的。

咱们能够将 Java 语言中各种操作共享的数据分为 5 类:

  1. 不可变

不可变的对象肯定是线程平安的。保障对象行为不影响本人状态的路径有很多种,其中最简略的就是把对象中带有状态的变量都申明为 final,这样在构造函数完结之后,它就是不可变的。

  1. 相对线程平安

在 Java API 中标注本人是线程平安的类,大多数都不是相对线程平安的。

  1. 绝对线程平安

绝对的线程平安就是咱们通常意义上的线程平安,它须要保障对这个对象独自的操作是线程平安的,咱们在调用的时候不须要做额定的保障措施,然而对于一些特定程序的间断调用,就可能须要在调用端应用额定的同步伎俩来保障调用的正确性。

  1. 线程兼容

线程兼容是指对象自身并不是线程平安的,然而能够通过在调用端正确地应用同步伎俩来保障对象在并发环境中能够平安地应用。

  1. 线程对抗

线程对抗是指无论调用端是否采取了同步措施,都无奈在多线程环境中并发应用的代码。

2)线程平安的实现办法

1. 互斥同步

互斥是办法,同步是目标。最根本的伎俩就是应用 synchronized 关键字,通过编译之后,会在同步块的前后别离造成 monitorentermonitorexit 这两个字节码指令。

除了应用 synchronized 关键字还能够应用 J.U.C 包下的 ReentrantLock 来实现同步。相比 synchronizedReentrantLock 减少了一些高级性能,次要有以下 3 项:期待可中断、可实现偏心锁,以及锁能够绑定多个条件。

2. 非阻塞同步

非阻塞同步是一种基于冲突检测的乐观并发策略。通常能够应用 CAS 来实现操作。

大部分状况下 ABA 问题不会影响程序并发的正确性,如果须要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。

3. 无同步计划

如果一个办法原本就不波及共享数据,那它天然就毋庸任何同步措施去保障正确性。

3)锁优化

HotSpot 虚拟机开发团队在这个版本上破费了大量的精力去实现各种锁优化技术,如 适应性自旋 锁打消 锁粗化 轻量级锁 偏差锁


这篇咱们次要是针对《深刻了解 Java 虚拟机:JVM 高级个性与最佳实际》下半局部做了相干的读书笔记。请读者缓缓浏览,转化成本人的常识~!👨💻

不要空谈,不要贪懒,和小菜一起做个 吹着牛 X 做架构 的程序猿吧~ 点个关注做个伴,让小菜不再孤独。咱们下文见!

👀 明天的你多致力一点,今天的你就能少说一句求人的话!

👉🏻 微信公众号:菜农曰,没关注的同学们记得关注哦!

正文完
 0