终于有人把Java内存模型说清楚了

内部原理
JVM 中试图定义一种 JMM 来屏蔽各类硬件和操做系统的内存访问差别,以实现让 Java 程序在各类平台下都能达到一致的内存访问效果。编程

JMM 的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量与 Java 编程中的变量有所区别,它包括了实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,由于后者是线程私有的,不会被共享,天然就不会存在竞争问题。为了得到较好的执行效能,Java 内存模型并无限制执行引擎使用处理器的特定寄存器或缓存来和主存进行交互,也没有限制即便编译器进行调整代码执行顺序这类优化措施。数组

JMM 是围绕着在并发过程当中如何处理原子性、可见性和有序性这 3 个特征来创建的。缓存

JMM 是经过各类操做来定义的,包括对变量的读写操做,监视器的加锁和释放操做,以及线程的启动和合并操做。多线程

内存模型结构
Java 内存模型把 Java 虚拟机内部划分为线程栈和堆。架构

线程栈
每个运行在 Java 虚拟机里的线程都拥有本身的线程栈。这个线程栈包含了这个线程调用的方法当前执行点相关的信息。一个线程仅能访问本身的线程栈。一个线程建立的本地变量对其它线程不可见,仅本身可见。即便两个线程执行一样的代码,这两个线程任然在在本身的线程栈中的代码来建立本地变量。所以,每一个线程拥有每一个本地变量的独有版本。并发

全部原始类型的本地变量都存放在线程栈上,所以对其它线程不可见。一个线程可能向另外一个线程传递一个原始类型变量的拷贝,可是它不能共享这个原始类型变量自身。app


堆上包含在 Java 程序中建立的全部对象,不管是哪个对象建立的。这包括原始类型的对象版本。若是一个对象被建立而后赋值给一个局部变量,或者用来做为另外一个对象的成员变量,这个对象任然是存放在堆上。机器学习

一个本地变量多是原始类型,在这种状况下,它老是在线程栈上。
一个本地变量也多是指向一个对象的一个引用。在这种状况下,引用(这个本地变量)存放在线程栈上,可是对象自己存放在堆上。
一个对象可能包含方法,这些方法可能包含本地变量。这些本地变量任然存放在线程栈上,即便这些方法所属的对象存放在堆上。
一个对象的成员变量可能随着这个对象自身存放在堆上。无论这个成员变量是原始类型仍是引用类型。
静态成员变量跟随着类定义一块儿也存放在堆上。
存放在堆上的对象能够被全部持有对这个对象引用的线程访问。当一个线程能够访问一个对象时,它也能够访问这个对象的成员变量。若是两个线程同时调用同一个对象上的同一个方法,它们将会都访问这个对象的成员变量,可是每个线程都拥有这个本地变量的私有拷贝。分布式

clipboard.png

硬件内存架构
现代硬件内存模型与 Java 内存模型有一些不一样。理解内存模型架构以及 Java 内存模型如何与它协同工做也是很是重要的。这部分描述了通用的硬件内存架构,下面的部分将会描述 Java 内存是如何与它“联手”工做的。函数

clipboard.png

一个现代计算机一般由两个或者多个 CPU。其中一些 CPU 还有多核。从这一点能够看出,在一个有两个或者多个 CPU 的现代计算机上同时运行多个线程是可能的。每一个 CPU 在某一时刻运行一个线程是没有问题的。这意味着,若是你的 Java 程序是多线程的,在你的 Java 程序中每一个 CPU 上一个线程可能同时(并发)执行。

每一个 CPU 都包含一系列的寄存器,它们是 CPU 内内存的基础。CPU 在寄存器上执行操做的速度远大于在主存上执行的速度。这是由于 CPU 访问寄存器的速度远大于主存。

每一个 CPU 可能还有一个 CPU 缓存层。实际上,绝大多数的现代 CPU 都有必定大小的缓存层。CPU 访问缓存层的速度快于访问主存的速度,但一般比访问内部寄存器的速度还要慢一点。一些 CPU 还有多层缓存,但这些对理解 Java 内存模型如何和内存交互不是那么重要。只要知道 CPU 中能够有一个缓存层就能够了。

一个计算机还包含一个主存。全部的 CPU 均可以访问主存。主存一般比 CPU 中的缓存大得多。

一般状况下,当一个 CPU 须要读取主存时,它会将主存的部分读到 CPU 缓存中。它甚至可能将缓存中的部份内容读到它的内部寄存器中,而后在寄存器中执行操做。当 CPU 须要将结果写回到主存中去时,它会将内部寄存器的值刷新到缓存中,而后在某个时间点将值刷新回主存。

当 CPU 须要在缓存层存放一些东西的时候,存放在缓存中的内容一般会被刷新回主存。CPU 缓存能够在某一时刻将数据局部写到它的内存中,和在某一时刻局部刷新它的内存。它不会再某一时刻读/写整个缓存。一般,在一个被称做“cache lines”的更小的内存块中缓存被更新。一个或者多个缓存行可能被读到缓存,一个或者多个缓存行可能再被刷新回主存。

JMM 和硬件内存架构之间的桥接
上面已经提到,Java 内存模型与硬件内存架构之间存在差别。硬件内存架构没有区分线程栈和堆。对于硬件,全部的线程栈和堆都分布在主内中。部分线程栈和堆可能有时候会出如今 CPU 缓存中和 CPU 内部的寄存器中。以下图所示:

clipboard.png

当对象和变量被存放在计算机中各类不一样的内存区域中时,就可能会出现一些具体的问题。主要包括以下两个方面:

线程对共享变量修改的可见性
当读,写和检查共享变量时出现 race conditions

clipboard.png

共享对象可见性
若是两个或者更多的线程在没有正确的使用 volatile 声明或者同步的状况下共享一个对象,一个线程更新这个共享对象可能对其它线程来讲是不接见的。

想象一下,共享对象被初始化在主存中。跑在 CPU 上的一个线程将这个共享对象读到 CPU 缓存中。而后修改了这个对象。只要 CPU 缓存没有被刷新会主存,对象修改后的版本对跑在其它 CPU 上的线程都是不可见的。这种方式可能致使每一个线程拥有这个共享对象的私有拷贝,每一个拷贝停留在不一样的 CPU 缓存中。

上图示意了这种情形。跑在左边 CPU 的线程拷贝这个共享对象到它的 CPU 缓存中,而后将 count 变量的值修改成 2。这个修改对跑在右边 CPU 上的其它线程是不可见的,由于修改后的 count 的值尚未被刷新回主存中去。

解决这个问题你可使用 Java 中的 volatile 关键字。volatile 关键字能够保证直接从主存中读取一个变量,若是这个变量被修改后,老是会被写回到主存中去。

竞态条件
若是两个或者更多的线程共享一个对象,多个线程在这个共享对象上更新变量,就有可能发生 race conditions。

想象一下,若是线程 A 读一个共享对象的变量 count 到它的 CPU 缓存中。再想象一下,线程 B 也作了一样的事情,可是往一个不一样的 CPU 缓存中。如今线程 A 将 count 加 1,线程 B 也作了一样的事情。如今 count 已经被增在了两个,每一个 CPU 缓存中一次。

若是这些增长操做被顺序的执行,变量 count 应该被增长两次,而后原值+2 被写回到主存中去。

然而,两次增长都是在没有适当的同步下并发执行的。不管是线程 A 仍是线程 B 将 count 修改后的版本写回到主存中取,修改后的值仅会被原值大 1,尽管增长了两次。

解决这个问题可使用 Java 同步块。一个同步块能够保证在同一时刻仅有一个线程能够进入代码的临界区。同步块还能够保证代码块中全部被访问的变量将会从主存中读入,当线程退出同步代码块时,全部被更新的变量都会被刷新回主存中去,无论这个变量是否被声明为 volatile。

Happens-Before
JMM 为程序中全部的操做定义了一个偏序关系,称之为 Happens-Before。

程序顺序规则:若是程序中操做 A 在操做 B 以前,那么在线程中操做 A 将在操做 B 以前执行。
监视器锁规则:在监视器锁上的解锁操做必须在同一个监视器锁上的加锁操做以前执行。
volatile 变量规则:对 volatile 变量的写入操做必须在对该变量的读操做以前执行。
线程启动规则:在线程上对 Thread.start 的调用必须在该线程中执行任何操做以前执行。
线程结束规则:线程中的任何操做都必须在其余线程检测到该线程已经结束以前执行,或者从 Thread.join 中成功返回,或者在调用 Thread.isAlive 时返回 false。
中断规则:当一个线程在另外一个线程上调用 interrupt 时,必须在被中断线程检测到 interrupt 调用以前执行(经过抛出 InterruptException,或者调用 isInterrupted 和 interrupted)。
终结器规则:对象的构造函数必须在启动该对象的终结器以前执行完成。
传递性:若是操做 A 在操做 B 以前执行,而且操做 B 在操做 C 以前执行,那么操做 A 必须在操做 C 以前执行。
免费Java资料须要本身领取,涵盖了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo/Kafka、Hadoop、Hbase、Flink等高并发分布式、大数据、机器学习等技术。
传送门:https://mp.weixin.qq.com/s/Jz...

相关文章
相关标签/搜索