3 Java内存模型
1️⃣前言
在实际讲解并发与高并发之前我们还需要先学习一下Java虚拟机是怎么解决这些问题的;为了屏蔽掉硬件以及各种操作系统的内存访问差异以实现让java程序在各个平台下都能达到一致的并发效果,Java虚拟机规范中定义了Java内存模型(Java Memory model,简称JMM).
2️⃣Java内存模型概念
Java内存模型是一种规范,规范了Java虚拟机与系统内存之间是如何协同工作的,它规定了一个线程如何和何时可以看到其他线程修改过的共享变量的值以及在必要的时候如何同步的访问共享变量;在明确了Java内存模型是做什么的之后我们具体的来介绍一下Java内存模型.
3️⃣Java内存模型讲解
首先我们需要弄懂图片中的两个内存分配的概念:一个是堆Heap,一个是栈Stack;
① Java里的堆-Heap
Java里边的堆是一个运行的数据区,堆是由垃圾回收来负责的;堆的优势是可以动态的分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java的垃圾回收器会自动的收走这些不在使用的数据,但是堆也有缺点,由于要在运行时动态分配内存因此堆的存取速度相对来说会慢一些.
② Java里的栈-Stack
栈的优势是存取速度比堆要快,仅次于计算机里边的寄存器,栈的数据是可以共享的;但是它的缺点也比较明显,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性,栈中主要存放一些基本类型的变量(比如:小写的int short long等基本数据类型)
Java内存模型要求调用栈和本地变量存放在线程栈上,对象存放在堆上,这里我们需要具体说一下;一个本地变量也有可能是指向本地的引用,这种情况下引用本地变量是存在线程栈上的但是对象本身是存放在堆上的,一个对象它可能包含方法(上图中的methodOne()和methodTwo())这些方法可能包含本地变量(上图中的Local variable),这些本地变量仍然是存放在线程栈上的;即使这些方法所处的对象存放在堆上一个对象的成员变量可能会对象本身存放在堆上,不管这个成员变量是原始类型还是引用类型,静态成员变量跟随着类的定义一起存放在堆上,堆上的对象可以被所持有对这个对象引用的线程访问,当一个线程可以访问这个对象的时候他也可以访问这个对象的成员变量;如果两个线程同时访问堆上的同一个对象他们将会都访问这个对象的成员变量,但是每一个线程都拥有了这个对象成员变量的私有拷贝.
4️⃣计算机简单的硬件架构
① 硬件中的CPU
上图展示的是一个多CPU的情况,现在一个计算机通常都是多CPU的其中一个CPU还有可能是多核,从这里我们就可以看出在这样的计算机上,同时运行多个线程是非常有可能的,而且每个CPU在每个时刻同时运行一个线程是肯定没有问题的;这就意味着如果我们的Java程序是多线程的,在Java程序中每个CPU一个线程是可能同时并发执行的.
② CPU寄存器
每个CPU都包含一系列的寄存器,他们是CPU内存的基础,CPU在寄存器上执行操作的速度远大于在主存上执行的速度,这是因为CPU访问寄存器的速度远大于主存
③ 高速缓存
由于计算机的存储设备与处理器的运算速度有几个数量级的差距,因此现在的计算机都不得不加一层读写速度尽可能接近处理器运算速度的高级缓存来作为内存与处理器之间的缓冲,将运算需要用到的数据复制到缓存中让运算能快速的进行,当运算结束后再将数据同步回内存当中这样处理器就无需等待缓慢的内存读写了,CPU访问缓存层的速度快于访问主存的速度,但是通常比访问内部寄存器的速度还是要慢一点的,每个CPU可能有一个CPU的缓存层,一个CPU还有多层缓存,在某一个时刻一个或者多个缓存行可能被读到缓存,一个或者多个缓存行可能在被刷新到主存.
④ 内存
一个计算机还包含一个主存,所有的CPU都可以访问主存,主存通常比CPU中的缓存大得多
⑤ 运行原理
通常情况下当一个CPU需要读取主存的时候,它会将主存的部分读取到CPU缓存中,它甚至可能将缓存中的数据读到内部寄存器中,然后在寄存器中执行操作,当CPU需要将结果回写到主存的时候,它会将内部寄存器的值刷新到缓存中,然后在某个时间点将值刷新到主存
5️⃣Java内存模型与计算机硬件的关联
通过上图我们可以看出,Java内存模型与实际的计算机硬件架构是有一定差异的,硬件架构没有区分线程栈和堆,因为对于硬件架构来说所有的堆和栈都分布在主内存里边,部分栈和堆在某些时间可能会出现在CPU缓存中和CPU内部的寄存器里边
① Java内存模型抽象结构
线程之间共享变量存储在主内存里边,每个线程都有一个私有的本地内存,本地内存是Java内存模型的抽象概念(并不是真实存在的,它包含缓存 写缓存区 寄存器以及其他的硬件和编译器的优化),本地内存中它存储了该线程以读或写共享变量拷贝的一个副本,例如上图中如果线程A要使用共享变量,它会先拷贝出共享变量的一个副本放在自己的本地内存中;从更低的层次来说主内存就是硬件的内存,是为了获取更好的运行速度虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中;
Java内存模型中的工作内存是寄存器和高速缓存的一个抽象描述,而JVM的静态内存存储模型它只是对内存的一种物理划分,它只局限在内存局限在JVM的内存,现在如果线程A和线程B要进行通信必须要经历两个步骤:第一步线程A需要将本地内存中更新过的共享变量刷新到主内存中,第二步线程B从主内存中读取之前线程A刷新到主内存中的共享变量.说到这里大家是不是对之前计数器程序中所出现的问题有了一定的理解呢?接下来我们来推演一下;
计算器程序推演
①我们假设主内存中当前变量的值为1,此时线程A和线程B同时开始执行;
②线程A从主内存中拿到的值为1,然后存储到自己的本地内存A中最后执行add的操作也就是+1的动作;
③线程A将计算后的结果(此时的结果已经变成2)通过本地内存重新写回到主内存中;
④线程B此时拿到的值也可能是1,然后将这个值放入自己的本地内存中最后执行加1的操作,最后将计算后的值重新写入到主内存;
⑤线程A与线程B同时将新的结果写入到主内存中,而不是有序的交替读取-计算-写入;
⑥因为在程序执行期间两个线程之间的数据是互相不可见的,因此就计数就出现了问题;
原因已经分析过了,这个时候我们就需要增加一些同步的手段增加程序运行期间并发执行的准确性;
6️⃣Java内存模型-同步的八种操作
①lock(锁定) : 作用于主内存的变量,把一个变量标识为一条线程独占的状态;
②unlock(解锁) : 作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量可以被其他线程锁定;
③read(读取) : 作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用;
④load(载入) : 作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中;
⑤use(使用) : 作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎;
⑥assign(赋值) : 作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量;
⑦store(存储) : 作用于工作内存的变量,把工作内存中的一个变量值传送到主内存中,以便随后的write的操作;
⑧write(写入) : 作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中;
7️⃣Java内存模型-同步规则
①不允许read和load、store和write单独出现。即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内从发起会写了但主内存不接受的情况出现。
②不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步到主内存里。
③不允许一个线程无原因地把数据从线程工作内存同步回主内存中,即没有发生过任何的assign操作就同步到主内存中。
④一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个没有被初始化(load或assign)的变量,换句话说,就是对一个变量实施use、store操作之前,必须先执行assign和load操作。
⑤一个变量在同一时刻,只允许一个线程对其进行loack操作,但lock操作可以被同一个线程重复执行多次,多次执行lock后,只有执行相同次数的unloack操作,变量才能被解锁。
⑥如果对一个变量执行了lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
⑦如果一个变量事先没有被lock操作锁定,那就不允许对他执行unlock操作,也不允许去unlock一个被其他线程锁定住的变量
⑧对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。
8️⃣Java内存模型-同步操作与规则讲解
9️⃣总结
① Java内存模型是一个规范,它规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值以及在必要时如何同步的访问共享变量,它要求调用栈和本地变量存放在线程栈上,对象存放在堆上;
②线程间的通信必需要经过主内存;
③定义了同步的八种操作和八种基本规则;