主要定义了对于一个共享变量,另一个线程对这个共享变量执行写操作后,这个线程对这个共享变量的可见性。
JMM是用于高并发的,抽象了线程和主存之间的关系,比如线程之间共享的变量必须存储在主内存中,规定了java从源代码到CPU可执行指令的转化过程需要遵守哪些和并发相关的原则和规范,主要目的是简化多线程编程,增强程序的可移植性。
JVM是用于java虚拟机的运行时区域相关,定义了jvm在运行时如何分区存储数据,比如说堆主要用于存放对象实例。
CPU缓存的目的是为了平衡CPU的高速处理和内存之间速度不匹配的问题,而内存缓存的是硬盘数据,用于解决硬盘访问速度过慢的问题。
首先复制一份数据到达CPU Cache中,然后在CPU需要用到的时候直接从CPU Cache中取读取数据,运算结束后,在将数据刷回到Main Memory中,但是这就存在一个内存缓存不一致的问题。为了解决内存缓存不一致的问题,需要指定注入MESI协议这种缓存协议或者其他手段解决。
指令重排序简单地说就是系统在执行代码的时候并不一定是按照程序员写代码的顺序来执行,可能会重新安排语句的顺序。
另外内存系统也会有重排序,但并不是真正意义上的重排序,在JMM中表现为主存和本地内存的内容可能不一致,进而大道至程序在多线程的额执行下出现可能的问题。
java 源代码会经历 编译器优化重排序 -->指令并行重排序 --> 内存系统重排序
指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致
编译器和处理器的指令重排序的处理方式不一样。对于编译器,通过禁止特定类型的编译器重排序的方式来禁止重排序。对于处理器,通过插入内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)的方式来禁止特定类型的处理器重排序。指令并行重排和内存系统重排都属于是处理器级别的指令重排序。
一般来说,编程语言也可以直接复用操作系统的内存,但是java作为跨平台语言,需要有一套自己的内存模型
可以把JMM看做是java定义的并发编程相关的一些规定,除了抽象了线程和主内存之间的关系外,还规定了java从源代码到CPU可执行指令的这个转换过程需要遵守哪些和并发相关的原则和规范
在 JDK1.2 之前,Java 的内存模型实现总是从 主存 (即共享内存)读取变量,是不需要进行特别的注意的。而在当前的 Java 内存模型下,线程可以把变量保存 本地内存 (比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。
5.2 什么是主内存?什么是本地内存?
从上图来看,线程 1 与线程 2 之间如果要进行通信的话,必须要经历下面 2 个步骤:
也就是说,JMM 为共享变量提供了可见性的保障。
不过,多线程下,对主内存中的一个共享变量进行操作有可能诱发线程安全问题。举个例子:
关于主内存与工作内存直接的具体交互协议,即一个变量如何从主内存拷贝到工作内存,如何从工作内存同步到主内存之间的实现细节,Java 内存模型定义来以下八种同步操作(了解即可,无需死记硬背):
除了这 8 种同步操作之外,还规定了下面这些同步规则来保证这些同步操作的正确执行(了解即可,无需死记硬背):
1.设计思想:
2.原则定义:
happens-before原则更想表达的含义是前一个操作的结果对后一个操作可见,无论两个操作结果是否在同一个线程中.
1 happens-before 2, 即时1,2不在同一个线程,JMM也抱枕1->2
一次操作或者多次操作,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么都不执行。
在 Java 中,可以借助synchronized
、各种 Lock
以及各种原子类实现原子性。
synchronized
和各种 Lock
可以保证任一时刻只有一个线程访问该代码块,因此可以保障原子性。各种原子类是利用 CAS (compare and swap) 操作(可能也会用到 volatile
或者final
关键字)来保证原子操作。
当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。
在 Java 中,可以借助synchronized
、volatile
以及各种 Lock
实现可见性。
如果我们将变量声明为 volatile
,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
由于指令重排序问题,代码的执行顺序未必就是编写代码时候的顺序。
我们上面讲重排序的时候也提到过:
指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致 ,所以在多线程下,指令重排序可能会导致一些问题。
在 Java 中,volatile
关键字可以禁止指令进行重排序优化。