当前位置:   article > 正文

深入理解java虚拟机 - jvm高级特性与最佳实践(第三版)_深入理解 Java 虚拟机——JVM 高级特性与最佳实践要点总结...

深入理解java虚拟机第三版

0807a1e70fcab04b7a3aa884cd7d30eb.png

第一部分 走进 Java

第一章

Java的优点:

  1. 平台无关
  2. 相对安全的内存管理与访问机制
  3. 热点代码检测,运行时编译优化
  4. 完善的应用程序接口

Java 发展简史:

  1. 1991 年 James Gosling 领导开发了 Java 语言的前身:Oak(橡树)
  2. 1995 年 Oak 正式改名为 Java
  3. 1996 年 JDK 1.0 发布
  4. 1999 年 HotSpot 虚拟机发布
  5. 2006 年 JDK 1.6 发布,并抛弃 J2EE、J2SE、J2ME 的命名方式,改为 Java SE、Java EE、Java ME
  6. 2006 年开放Java源代码并建立 OpenJDK 组织管理(OpenJDK 与 Sun JDK 代码基本一致)
  7. 2009 年 Oracle 收购 Sun 公司

多核并行:

使用 Fork/Join 模式可以轻松利用多个 CPU 核心完成计算任务

64 位虚拟机

Java 程序在 64 位虚拟机上需要付出更大的代价:

  1. 由于指针膨胀和各种数据类型对齐补白的原因,导致需要消耗更多的内存(可使用指针压缩功能缓解问题)
  2. 运行速度全面落后于 32 位虚拟机

第二部分 自动内存管理机制

第二章 Java 内存区域与内存溢出异常

运行时数据区域:

84adc1b48b16c5aaaec6499864c1e085.png
程序计数器

指向下一条需要执行的指令;

每条线程都需要一个独立的程序计数器,各线程的计数器互不影响,独立存储,是线程私有内存;

若线程正在执行 Java 方法,则计数器记录的是正在执行的虚拟机字节码指令的地址;

若线程正在执行 Native 方法,则计数器为空;

此内存区域是唯 一 一 个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域;

虚拟机栈

属于线程私有的内存区域,生命周期随线程;

虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时会创建一个栈帧用于存放局部变量、操作数栈等信息,每一个方法从调用到执行完成的过程就是栈帧在虚拟机栈中入栈到出栈的过程。因此,在递归调用时,如果递归的太深,会导致 StackOverflowError,如下面的代码:

  1. public int sum(int n) {
  2. if (n == 1) return 1;
  3. return sum(n - 1) + n;
  4. }

经常说的内存里面的栈,指的就是虚拟机栈,或者说是虚拟机栈中局部变量表部分;

这个局部变量表所需要的内存空间是在编译期间就确定的;

64 位的 long 与 double 占据两个局部变量空间;

此内存区域有两种异常:

  1. 线程请求的栈太深,超出允许值,抛出 StackOverflowError 异常;
  2. 虚拟机栈动态扩展时无法申请到足够的内存,抛出 OutOfMemoryError 异常;
本地方法栈

类似于虚拟机栈,不同之处就是虚拟机栈为执行 Java 方法服务,而本地方法栈为执行 Native 方法服务;

由于机器的内存是有限的,当机器捏内存不足时,抛出 OOM 异常

Java 堆

所有线程均可使用的内存区域,在虚拟机启动时创建,作用 就是 存放对象实例,所有的对象实例以及数组均在堆上面创建;

(但是 JIT 技术的发展会将频繁使用的对象,变成本地调用,所以“所有的对象均在堆上创建”不是那么绝对)

堆是GC的主要区域;

堆还可以继续细分,看图!

堆可以物理上不连续,但逻辑上必须连续;

此内存区域无法完成内存分配时且无法扩展时,抛出 OutOfMemoryError 异常;

方法区

所有线程均可使用的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据;

方法区,不属于堆,但是又被称为“永久代”,原因是:在 HotSoprt 虚拟机下,GC 被扩展到方法区,GC 本来主要工作在 新生代、老年代上,但此时又可以工作在方法区,于是取名“永久代”,其他虚拟机如:BEA 根本不存在永久代的说法;

用永久代实现方法区不好,会造成内存溢出,因为这一块的 GC 很难令人满意,它 GC 的主要目标是对常量池的回收和对类型的卸载;

JDK 1.7 中已经把原本放在永久代的字符串常量池移出到堆里面;

JDK 1.8 永久废除永久区;

此内存区域无法满足内存分配时,抛出 OutOfMemoryError 溢异常;

运行时常量池是方法区的一部分,【class文件含有常量池信息,类加载到内存后,JVM 会把 class 的常量池放到运行时常量池中,所以每一个类都有一个运行时常量池】常量还可以在运行期间产生,通过String.intern() 方法可以将该 String 存放到常量池中并返回该常量的引用,若下一次需要一个该 String 的时候,先检查常量池中是否存在同样的值,若存在返回引用,否则直接创建

关于 intern 方法的更多细节请访问:我的知乎

此内存区域无法完成内存分配时且无法扩展时、常量池无法申请到内存时,抛出 OutOfMemoryError 异常;

总结:OutOfMemoryError 可能的致因:内存溢出 或 内存泄漏,一般发生在老年代

非运行时数据区域:

直接内存,不是虚拟机运行时数据区域;

NIO 可以通过 Native 函数直接分配堆外内存,然后通过 Java 堆里面的 DirectByteBuffer 对象作为这个堆外内存的引用并操作它;

  1. 堆内存数据在 flush 远程时,会先复制到 直接内存 ,也就是从堆内存到非堆内存,然后再发送
  2. 堆外内存直接可发送,没有 copy 的过程;
  3. 在 Socket 通信中就常常需要这么操作,否则 GC 会导致数据失效;

此内存区域无法完成内存分配时且无法扩展时,抛出 OutOfMemoryError 异常;


对象的创建:划分空间 + 初始化

遇到 new 关键字的时候就需要加载类信息并创建对象,创建对象的过程就是把堆空闲内存划分一块出来,划分的方式又分为“指针碰撞” 和 “空闲列表”

分配又分为直接在堆上面分配(并发时需要同步控制:CAS + 失败重试)和本地线程分配缓冲(就是预先给此线程足够额外空间以给未来可能产生的对象提供内存空间,因此无需同步操作)

对象的访问:句柄 或 reference

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

GC 需要考虑三件事:哪些内存需要回收、什么时候回收、如何回收

而 GC 主要关注动态分配的内存,其余的会随线程消亡而消失

如何判断对象是否存活
  • 引用计数法

原理:给对象中添加一个引用计数器,被引用计数器就加一,引用失效就减一,当引用为 0 的时候,则该对象就是“死了的”

缺点:无法处理循环引用的问题,例如 A 引用了 B,B 又引用了 A,虽然 A、B在未来不会被使用了,但是由于他们的引用不为 0,所以他们依然是“存活的”,导致 GC 无法回收;

  • 可达性分析法(主流的实现办法)

通过一系列的 GC Roots 的对象作为起点,从这些节点开始往下搜索,如果某一个对象不在搜索范围内,则称该对象不可达,不可达的对象就是 GC 的目标,如图:由于对象 4、5、6 不可达,所以在 GC 范围内

7ef89c6ad66ccb44f9ab78ffd09047a7.png

即使对象4、5、6 没有与 GC Roots 关联,他们也不会立即被 GC,会有一次二次筛选,如果对象4、5、6 实现了 finalize() 方法那么他们有自救的机会,即虚拟机会判断是否执行他们的 finalize 方法,只要他们在 finalize 方法里面把自己关联上 GC Roots,就不会被 GC 调,但要是没有实现 finalize 方法或者在方法里面没有关联上 GC Roots,这一次筛选后在下一次 GC 时,对象就会被回收。finalize 方法只会执行一次,不推荐使用此方法。

引用的分类:
  1. 强引用:只要强引用存在,就不会被 GC
  2. 软引用:不会立即对他 GC,第一次 GC 后,内存依然不足,才对他进行 GC
  3. 弱引用:会立即被 GC
  4. 虚引用:唯一作用就是,对象被 GC 时收到一个系统通知
方法区也可以被回收

方法区里面的废弃常量无用的类会被回收,没有被任何对象引用的常量就叫废弃常量,废弃常量会被清除,

无效的类(不存在该类的任何实例、该类的ClassLoader已被回收、没有被反射)会被清除;

垃圾收集算法
  • 标记——清除算法(不常用,但属于老年代回收算法)

原理:先标记要清理的对象(通过引用计数法、可达性分析法确定标记哪些对象),然后统一回收;

缺点:标记与回收的效率低、容易产生内存碎片(要么导致一次新的 GC,要么 OOM)

  • 复制算法(新生代回收算法)

原理:将可用内存划分为大小相等的 A、B 两块,每次只使用其中的一块,当 A 块不够用了,就将 A 中还存活的对象复制到 B 上,然后一次性清空 A,对整个半区进行 GC 可解决效率与内存碎片问题;

新生代回收过程:GC 开始前对象只会存在于 Eden 区和 From Survivor 区,To Survivor 区为空,GC 开始后 Eden 区存活的对象会被复制到 To 区,From 里面达到老年年纪的对象会被放到老年区,其余的会被复制到 To 区,然后 Eden 和 From 区被清空,然后 To 区和 From 区交换角色,最终保证 To 区一直是空的

  • 标记——整理算法(老年代回收算法)

原理:先标记要清理的对象(通过引用计数法、可达性分析法确定标记哪些对象),然后将所有存活的对象往一端移动,最后直接清理端外的全部“死亡”的内存,解决了内存碎片问题;

GC 时需要中断全部线程,分为:抢先式中断 和 主动式中断

抢先式中断:中断全部线程进行 GC

主动式中断:设置中断标志位,线程轮询此标志位,为 ture 则挂起自己

垃圾收集器
  • Serial 收集器:单线程的,会中断全部线程,直到 GC 结束,是 客户端 首选 GC 方式
  • ParNew 收集器:Serial 收集器的多线程版本,可与 CMS 收集器合作,是 服务端 首选 GC 方式
  • CMS 收集器:基于“标记——清除”算法实现的并发收集器,响应速度快,停顿短
  • G1 收集器:目前最好的收集器
内存分配与回收策略

对象主要在 Eden 上分配,如果启用了线程分配缓冲,则优先在 TLAB 上分配;但是大对象(大型数组或字符串)直接进入老年代;新生代内存不足会发起 Minor GC

长期存活的对象直接进入老年代,每个对象都有一个 Age,默认当 Age >= 15 的时候会从新生代进入老年代,但是这个 Age 的规定并不是死的,也会动态调整,当老年代内存不足时发起 Major GC

Full GC:清理整个堆空间

第四章 虚拟机性能监控与故障处理工具

目前,略

未来再更新

第五章 调优案例分析与实战

目前,略

未来再更新

第三部分 虚拟机执行子系统

第六章 类文件结构

JVM 与任何语言无关,包括 Java !他只与 class 文件绑定,所以不管什么语言,只要生成合格的 class 文件,就可以在 JVM 上运行,class 文件中包含了 Java 虚拟机指令集和符号表以及若干其他辅助信息;

Java 语言最终是由多条字节码命令组合而成的,因此字节码指令会比 Java 语言更强大,类似的可以说,Java 语言无法完成的任务可以让其他语言完成,大家都在 JVM 上运行;

class 文件是一组以 8 位字节为基础单位的二进制流,他的文件格式采用一种类似于 C 语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表

魔数与 class 文件版本

很多文件存储标准中都使用魔数来进行身份识别,确保文件类型正确(因为文件类型后缀可以任意修改,而魔数进一步确定文件格式),class 文件的魔数为:0xCAFEBABE

class 文件还存储着版本号,版本号的确定保证了向下兼容,即高版本的 JVM 可以执行低版本的 class 文件,但是低版本的 JVM 不可以执行高版本的 class 文件;

常量池

常量池是 class 文件中的资源仓库,存放:字面量与符号引用;

字面量:常量、字符串、final 常量值;

符号引用:类和接口的全限定名、字段名和描述符、方法名和描述符

Java 虚拟机指令只含操作码,没有操作数

/***

由于内容比较繁琐,更多内容日后更新

**/

第七章 虚拟机类加载机制

在 Java 语言里面,类型的加载、连接和初始化过程都是在程序运行期间都是在程序运行期间完成的,多态就是基于这一点实现的:接口的实现类有很多,等到运行时动态确定用哪一个实现类,另外,RPC 调用也是基于 Java 语言的动态性实现的

类加载的生命周期:

ab39d9db321e4e2883956dc57d0d9468.png

加载、验证、准备、初始化、卸载的顺序是确定的,但是解析却不一定,有时在动态绑定时可能在初始化之后才开始解析;

什么时候“加载”,JVM 并没有强制规定,但是却对初始化做了强制规定:有且只有 5 种情况必须立即对“类”初始化

  1. 遇到 new、获取静态值、设置静态值、调用静态方法时
  2. 使用反射时
  3. 使用一个类,发现其父类没有初始化时需要立即初始化父类
  4. main 类必须立即初始化
  5. 方法句柄对应的类没有初始化的时候

注意一下实例:

子类 A 继承 父类 B,B 含静态字段 value ,主类里面通过 A 调用父类的 value,即:A.value,结果是 B 被初始化了,A 没有,原因是:对于静态字段,只有直接定义这个字段的类才会被初始化;

类 C 含静态字段 value,在主类 main 里面直接调用 C.value(无继承关系),发现 C 类没有被初始化,原因是:编译阶段存在常量传播优化措施,C 的常量被转移到了主类中,于是运行时根本不会涉及到类 C;

当一个类在运行时要求其父类全部初始化了,但是不要求接口的父接口全部初始化;

加载一个类可以是从 class 文件中,也可以是动态生成的类,甚至可以是从网络中获取的字节等,来源不限;

两个代码一样的类如果是使用不同的加载器加载的,则这两个类不同,但要是是一个加载器加载的,那就相同了;

类加载器使用双亲委派模型:

c94ee18d26e1f5c423c65beb52ebc85b.png

此模型要求除了顶级加载器之外,其余的加载器都必须有自己的父类加载器(不是用继承实现的,代码里面组合实现),通常先将加载请求发送到顶级父类处理,如果父类无法处理再交给下一级加载器处理;

例如,现在有两个 Object 类,一个是 Java 原生的,一个是自己手写的 java.lang.Object ,如果不使用双亲委派模型,则系统中将会出现多个 Object,会变得非常混乱,安全性无法保证;

但是此模型只是一种规范,可以打破他,例如 SPI,JNDI 使用线程上下文加载器加载 SPI 代码,即父类加载器请求子类加载器加载类,虽然违背了双亲委派模型,但是却完成了功能,可以算一种【创新】了吧;

/**

其余内容日后再更

**/

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

栈帧:支持虚拟机进行方法调用与方法执行的数据结构,属于虚拟机栈内存区域的栈元素;存储了方法的局部变量、操作数栈、动态链接和方法返回地址;

每一个方法从调用到结束的过程都对应着栈帧在虚拟机栈里面的入栈与出栈的过程;

只有位于栈顶的栈帧才是有效的,称之为当前栈帧:

d2b17a8af592561be5050b656d57f4b6.png

Javac 完成词法分析、语法分析、语义分析、字节码生成

第九章 类加载及执行子系统的案例与实战

日后更

第四部分 程序编译与代码优化

第十章 早期(编译期)优化

前端编译器:Javac

后端编译器:JIT(C1、C2){ C1指:Client Compiler,C2指:Server Compiler }

静态提前编译器:AOT 编译器(GNU Compiler for the Java(GCJ)),直接把 *.java 文件编译成本地机器代码

Java 虚拟机设计团队通过添加“语法糖”(语法糖:对语言原有功能无影响,但是可以提高编码效率)来改进编码效率,而不是从底层上改进,例如:泛型,Java 的泛型是伪泛型,在编译后的字节码文件中被替换为原来的原生类型,并插入了强制转换代码,举例:

这是源代码:

  1. public class Test {
  2. public static void main(String[] args) {
  3. Map<String, String> map = new HashMap<>();
  4. map.put("key", "value");
  5. System.out.println(map.get("key"));
  6. }
  7. }

这是编译后的反编译代码:

  1. public class Test {
  2. public Test() {
  3. }
  4. public static void main(String[] args) {
  5. Map<String, String> map = new HashMap();
  6. map.put("key", "value");
  7. System.out.println((String)map.get("key"));
  8. }
  9. }

可以发现,存在代码强转,原来的泛型不存在了,此称之为:泛型擦除

扩展:此代码无法编译通过,由于存在泛型擦除,method 方法虽然被不同数据类型重载了,但是编译后 method 方法的参数都退化成 List<E> 了,存在签名一直的方法,于是编译不通过;

  1. public void method(List<String> list) {
  2. }
  3. public void method(List<Integer> list) {
  4. }

同样的自动装箱、拆箱、ForEach 都是语法糖


条件编译:需要在方法里面铜鼓条件编译的时候可以这样写

  1. if (true) {
  2. // do
  3. } else {
  4. // do
  5. }
  6. /************/
  7. while (false) {
  8. System.out.println(123);// 报错:方法不可达
  9. /**
  10. 条件为 true 时才会触发编译,
  11. false 没有编译的必要
  12. **/
  13. }

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

被执行的方法被 JIT 优化过了,方法执行时先走 JIT 编译的代码,不然再检测是否出现热点代码

Java 最初是解释执行的,但是会把热点代码通过 JIT 编译成本地机器码;

现在的 Java 是解释器与编译器并存的架构,通过终端输入:java -version 可以查看你的 JDK 是属于解释的、编译的、还是混合的:

  1. java version "1.8.0_171"
  2. Java(TM) SE Runtime Environment (build 1.8.0_171-b11)
  3. Java HotSpot(TM) 64-Bit Server VM (build 25.171-b11, mixed mode)

最后一行【mixed mode】,说明我的 JDK 是混合编译的

解释器的优势:启动快、节约内存

编译器的优势:执行快

触发 JIT 编译的条件是代码够“热”,判断热点代码的方法是:基于采样的热点探测、基于计数器的热点探测

  • 基于采样的热点探测

虚拟机定期检查线程的栈顶,如果发现某个方法经常出现在栈顶,那他就是热点方法;

优点:简单、高效;

缺点:易受干扰;

  • 基于计数器的热点探测(HotSpot 虚拟机的实现方式)

虚拟机为每个方法建立计数器,当计数器达到阈值就判定为热点方法并进行 JIT 编译;

HotSport 虚拟机又为每个方法准备了方法调用计数器、回边计数器,这两个计数器

方法调用计数器:统计方法被调用的次数,执行方法是如果该方法没有 JIT 编译的版本则解释执行此方法,然后计数器加一,接着判断方法调用计数器、回边计数器的和是否大于方法调用计数器的阈值,大于就提交 JIT 编译请求;但是如果一个方法虽然被方法调用计数器计数了,没有达到阈值,又在一段时间内没有被调用过,那么他的计数值将会在一定时间内衰退【热度衰减,与实现世界一样,烧红的烙铁不继续加热就会降温】

回边计数器:统计循环体代码执行的次数;原理:回边计数器达到阈值后会触发 OSR 编译【On-Stack Rplacement,循环体替换不像 JIT 编译后可以等待下一次调用,循环体需要编译后立即生效且在循环体结束前立即生效,所以需要动态替换栈顶代码】

  1. 前面提到了,Javac 是编译前端,完成词法、语法、语义的分析并生成字节码,再由 JVM 来动态链接,执行
  2. 因此,Java 代码在解释执行模式下或混合模式下,触发 JIT 编译前是解释执行的,之所以需要解释字节码而不是源代码,因为 1. JVM 只识别字节码、2. 字节码解释效率更高【虽然有直接解释 Java 源代码的技术】
  3. 其实 Python 这门解释性语言也是先编译成字节码,再解释字节码的
  4. JDK 9 开始自带一个 jshell ,可直接解释执行 Java 源代码
  5. 编译流程:
  6. 源代码:
  7. --词法分析——>token 串
  8. --语法分析——>语法树
  9. --语义分析——>抽象语法树
  10. --代码生成——>目标代码
  11. 执行:
  12. 目标代码:
  13. --操作系统/硬件——> 结果

可以使用 -XX: +PrintCompilation 参数输出被 JIT 编译的方法名

/**

其余内容日后更新

**/

第五部分 高效并发

第十二章 Java 内存模型

Amdahl 定律:加快某一部件执行速度所能获得的系统性能加速比受限于该部件的执行时间占系统中总执行时间的百分比

加速比 = 改进前的总执行时间 / 改进后的总执行时间

由于 CPU 与内存速度不匹配,所以需要 Cache ,但是引入缓存又引发了缓存一致性的问题,为了解决 Java 语言在不同平台上访问内存一致性的问题,引入 Java 内存模型(JMM);

JMM 要点如下:

  • 线程有自己的工作内存,变量存在主内存中,线程对变量的操作必须在工作内存中进行,即对变量做一个副本拷贝到工作内存中,不能直接在主内存中进行,线程间内存互相隔离,线程通过主内存交互;
  • 主内存相当于堆,工作内存相当于虚拟机栈;
  • volatile 关键字的作用:
    • 保证可见性【一条线程对变量的修改堆其余线程是可见的】【不保证原子性】
    • 禁止指令重排序【通过添加内存屏障实现】
    • synchronized 也能保证可见性与禁止指令重排序,但是使用 volatile 开销比加锁低,另外他可以实现【先行-发生原则】
  • Java 对基本数据类型的操作是原子性的,但 long、double 类型的变量是 64 位的,不具备原子性
  • 原子性:按照 8 个内存访问规定进行操作就是原子性的;处理器优化可以导致原子性问题
  • 可见性:一个线程修改了某值,其余线程可以立即看见【volatile、synchronized】final 均可实现】;缓存一致性问题其实就是可见性问题
  • 有序性: 程序执行的顺序按照代码的先后顺序执行;【volatile、synchronized、happens-before 原则均可实现】指令重排即会导致有序性问题

以上三个特性出现问题将导致线程不安全

线程安全在下一节讲

happens-before 原则:

  • 程序次序规则:写在前面的代码先执行
  • 管程锁定规则:unlock 操作先发生于对后面通一个锁的 lock 操作
  • volatile 变量规则:对一个 volatile 变量的写操作先发生于后面对这个变量的读操作
  • 线程启动规则:线程的 start() 方法,是线程的第一个操作
  • 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测
  • 线程中断规则:对线程的 interrupt() 方法调用先行发生于被中断线程的代码检测到中断事件的发生
  • 对象终结规则:对象的 init 优先于他的 finalize() 方法
  • 传递性规则:操作 A 优先于操作 B,操作 B 优先于操作 C,则操作 A 优先于操作 C

举例:

  1. i = 1;// A线程执行
  2. j = i;// B线程执行
  3. i = 2;// C线程执行

三个线程同时执行后,请问 j 为 1 还是 2?

答案:不知道

因为线程 B 现在无法观察到 i 的变化,说 j = 1,是依据先行发生原则的“程序次序规则”,B 线程可以观察到 A 线程的操作;说 j = 2,是因为线程 C 提前在 A 和 B 之间执行了,所以如何优化它?

按照 happens-before 原则套,让他符合这些原则之一即可,例如,给变量 i 加上 volatile 关键字

时间上优先发生并不代表代码写在前面,时间顺序与 happens-before 原则无关

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

Java 的线程使用的是操作系统的线程模型,因此 Thread 类里面的核心方法全是 Native 方法;

何为线程安全:并发下的执行结果是正确的就叫线程安全;

一般涉及到线程安全的时候讨论的都是共享数据,数据独占不存在线程安全性问题;

线程安全的实现方法:

  • 不可变【final】

对共享数据加 final 关键字,即可将其变为线程安全的,例如 final int a = 3;

又例如 String 类就是一个 final 类,它是安全的;

但是自定义的 final 类可能依然不安全,例如:final User 类,User 里面有一个字段 money 不是 final 类型的,于是此 User 依然不安全,要学 String,所有属性均是 final 的才安全;

枚举类天生就是安全的,所以用它做线程安全的单例模式特别好

  • 互斥同步(阻塞同步,需要挂起线程)

同步:共享数据在某一时刻只能被一个线程占用

互斥:实现同步的手段,如使用信号量、临界区、互斥量

实例:synchronized 关键字

  • 非阻塞同步(无需挂起线程)

通俗地说就是,先尝试操作,如果没有其他线程争用共享数据,那么操作成功;如果存在线程争用共享数据,则不断重试直到成功

实例:CAS 操作

CAS(V, A, B)

V:内存正储存的值

A:旧值

B:新值

原理:如果 旧值 == 内存 里面的值,就用 新值 更新 内存 里面的值

  • 可充入代码(无需同步)

例如递归,执行的时候被中断执行其他任务,然后再返回执行自己的事,这样的代码是安全的

  • ThreadLocal (无需同步)

示例代码:

  1. import java.util.concurrent.ExecutorService;
  2. import java.util.concurrent.Executors;
  3. public class ThreadLocalExample1 {
  4. private static final ThreadLocal<Integer> count = new ThreadLocal<Integer>() {
  5. @Override
  6. protected Integer initialValue() {
  7. return 1;
  8. }
  9. };
  10. public static void main(String[] args) {
  11. ExecutorService service = Executors.newCachedThreadPool();
  12. for (int i = 0; i < 1000; i++) {
  13. service.execute(new Runnable() {
  14. @Override
  15. public void run() {
  16. count.set(count.get() + 99);
  17. System.out.println(count.get());
  18. /**
  19. * 在线程池中使用ThreadLocal的时候更要注意清理threadLocal
  20. * 因为线程池会复用线程,
  21. * 在复用线程执行任务时会使用被之前的线程操作过的 value 对象
  22. * 所以要是不清理,就会出现线程拿到的变量不是原始变量的副本,
  23. * 导致结果出错
  24. */
  25. count.remove();
  26. }
  27. });
  28. }
  29. /**
  30. * 不管其他线程如何修改可变变量,他们都是修改的此变量的副本,不影响原来的值【但是那些线程却得到了自己想要的结果】
  31. *
  32. * 最终的输出结果还是原来的初始设定
  33. */
  34. System.out.println("final: " + count.get());
  35. service.shutdown();
  36. count.remove();
  37. }
  38. }
  39. /**********************************************/
  40. public class ThreadLocalExample2 implements Runnable {
  41. private static final ThreadLocal<Integer> value = new ThreadLocal<Integer>() {
  42. @Override
  43. protected Integer initialValue() {
  44. return 0;
  45. }
  46. };
  47. private int index;
  48. public ThreadLocalExample2(int index) {
  49. this.index = index;
  50. }
  51. @Override
  52. public void run() {
  53. System.out.println("线程" + index + "的初始value:" + value.get());
  54. for (int i = 0; i < 5000; i++) {
  55. value.set(value.get() + 1);
  56. }
  57. System.out.println("线程" + index + "的【累加】value:" + value.get());
  58. }
  59. public static void main(String[] args) {
  60. for (int i = 0; i < 100; i++) {
  61. new Thread(new ThreadLocalExample2(i)).start();
  62. }
  63. }
  64. }
  65. /**********************************************/
  66. import java.util.concurrent.ExecutorService;
  67. import java.util.concurrent.Executors;
  68. public class ThreadLocalExample3 extends Thread{
  69. private static final ThreadLocal<Integer> count = new ThreadLocal<Integer>() {
  70. @Override
  71. protected Integer initialValue() {
  72. return 1;
  73. }
  74. };
  75. @Override
  76. public void run() {
  77. count.set(count.get() + 99);
  78. System.out.println(count.get());
  79. }
  80. public static void main(String[] args) {
  81. for (int i = 0; i < 100; i++) {
  82. ThreadLocalExample3 example3 = new ThreadLocalExample3();
  83. Thread thread = new Thread(example3);
  84. example3.start();
  85. //System.out.println(count.get());
  86. // ThreadLocal最佳实践:为了防止内存泄漏,必须手动清空Map
  87. count.remove();
  88. }
  89. }
  90. }

锁优化:

  • 自旋锁:空转 CPU 不挂起线程,适用于等待时间短的场景
  • 适应自旋锁:自旋锁的自适应版本,自动优化自旋的次数与位置
  • 锁消除:字符串拼接 “+” 号是通过 StringBuilder 完成的,但是在 append 的时候会加锁,通过 JIT 编译优化后,再拼接时就不需要加锁了
  • 锁粗化:一段代码需要多次加锁释放锁、不如一次性加锁再释放,从而减少性能开销
  • 轻量级锁:减少无竞争下的性能开销
  • 偏向锁:偏向于第一个获得此锁的线程并在下一次执行此线程的时候(该锁还没有被其他线程获取过)直接执行无需加锁
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/212326
推荐阅读
相关标签
  

闽ICP备14008679号