基于JVM原理、JMM模型和CPU缓存模型深入理解Java并发编程

许多以Java多线程开发为主题的技术书籍,都会把对Java虚拟机和Java内存模型的讲解,作为讲授Java并发编程开发的主要内容,有的还深入到计算机系统的内存、CPU、缓存等予以说明。实际上,在实际的Java开发工作中,仅仅了解并发编程的创建、启动、管理和通信等基本知识还是不够的。一方面,如果要开发出高效、安全的并发程序,就必须深入Java内存模型和Java虚拟机的工作原理,从底层了解并发编程的实质;更进一步地,在现今大数据的时代,要开发出高并发、高可用、考可靠的分布式应用及各种中间件,更需要深入到计算机工作原理的底层去进行代码开发。

本文尝试以一个较为全面的角度,以Java虚拟机工作原理和Java内存模型为切入,配合一些计算机CPU缓存的知识,深入理解Java多线程开发中的难点,包括线程安全和线程通信等内容。

如果需要先行了解Java并发编程的基础知识,可参考以下随笔:

Java并发编程之线程创建和启动(Thread、Runnable、Callable和Future)

Java并发编程之线程生命周期、守护线程、优先级、关闭和join、sleep、yield、interrupt

Java并发编程之线程安全、线程通信

CPU缓存模型

一般来说,逻辑上是先有了现行的计算机体系,再有基于计算机系统的高级编程语言及其编译器、虚拟机等构件。因此第一部分先介绍一下困扰许多初学者的Java多线程开发的源头——CPU缓存模型。

计算机中,所有的计算都是在CPU寄存器中完成,而指令完成所需要的数据读取和写入,都需要从RAM主存获取。受硬件工艺的影响,现在的CPU处理速度已经远远超过主存的访问速度,差额基本是成千上万的差距。

因此,CPU缓存设计应运而生。如下为CPU缓存架构图和CPU缓存与主存的速度对比:

基于JVM原理、JMM模型和CPU缓存模型深入理解Java并发编程

基于JVM原理、JMM模型和CPU缓存模型深入理解Java并发编程

使用CPU缓存来处理数据的步骤大致为:

1. 把需要的数据从主存复制一份到CPU缓存中;

2. CPU从缓存中读取数据并计算;

3. 计算完成的数据刷新到主存中。

“缓存一致性问题”

如上的工作机制,会在多线程环境下导致缓存不一致的问题。为此,使用“总线加锁”(已淘汰)和“缓存一致性协议”来解决,它大致的思想是:

当CPU操作缓存中的数据时,如果发现该变量是一个共享变量,意味着其它缓存中也会有这个变量的副本,然后——

1. 如果是读操作,不做任何处理,只是从缓存中读取数据到寄存器

2. 如果是写操作,发出信号通知其它CPU将该变量的cache line置为无效状态,其它CPU在运行该变量读取的时候需要从主存更新数据。

基于JVM原理、JMM模型和CPU缓存模型深入理解Java并发编程

Java虚拟机

受许多资料和书籍讲述不严谨所致,很多初学者往往简单地把Java虚拟机理解为类似编译器甚至解释器的存在,把Java虚拟机当做黑盒,认为输入了Java源代码,就可以输出计算机直接跑的程序了;因为JVM在不同操作系统上都有实现,所以可以做到“一份代码,多种机器运行的效果”。这样理解对小白或者外行人来说可能OK,但对于有想法深入学习Java的小伙伴,是远远不够的。

事实上,Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。包括编译器以及JRE在内的整套体系,构成了完整的JVM。JVM原生支持包括Java、Scala、Kotlin在内的语言编译后运行。而其中,JRE又是JVM的核心部分。JRE的体系结构图如下:

基于JVM原理、JMM模型和CPU缓存模型深入理解Java并发编程

JVM的类加载过程

当Java源文件经过javac编译完成,生成类文件之后,首先会被类加载器即ClassLoader加载。ClassLoader的主要职责是加载编译好的类文件,在对应的内存区域中生成该类的各个数据结构。类的加载分为加载、连接初始化三个阶段,如图:

基于JVM原理、JMM模型和CPU缓存模型深入理解Java并发编程

1. 加载:加载类的class文件

2. 连接

2.1 验证:确保class文件的正确性,如版本、魔术因子等

2.2 准备为类的静态变量分配内存,并且初始化默认值

2.3 解析:把类中的符号引用转为直接引用

3. 初始化为类的静态变量赋代码编写阶段锁赋的值

需要注意的是:类的加载实施的是懒加载,即用的时候才加载,并且在同一个运行时包下,一个类只会被初始化一次。

类的完整的生命周期,除了类加载,还包括使用卸载

关于使用,JVM定义了6种主动使用类的场景,会导致类的加载和初始化

new对象;访问类的静态变量(静态常量不会!);访问类的静态方法;使用反射;初始化子类会初始化父类;启动类

注意初始化一个类为元素的数组不会加载类。

类加载的最终产物,是堆内存中的Class对象。而对于同一个ClassLoader,不管类被加载多少次,指向的都是同一个Class对象

类被加载后在栈内存中的分布情况如图

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zywddj.html