JMM-Java内存模型

1.计算机内存模型

计算机在执行程序时,每条指令都是在 CPU 中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题:由于 CPU 执行速度很快,而从内存读取数据和向内存写入数据的过程跟 CPU 执行指令的速度比起来要差几个数量级,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。所以现代计算机不得在CPU和主存之间加入一层读写速度近可能接近CPU运算速度的高速缓存(寄存器)。

也就是说,在程序运行过程中,会将运算需要的数据从主存复制一份到 CPU 的高速缓存当中,那么, CPU 进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据同步到主存当中。举个简单的例子,比如下面的这段代码:

int i=0;
i=i+1;

比如,同时有两个线程执行这段代码,假如初始时 i 的值为 0,那么我们希望两个线程执行完之后 i 的值变为 2。但是事实会是这样吗?

可能存在下面一种情况:初始时,两个线程分别读取 i 的值存入各自所在的 CPU 的高速缓存当中,然后线程1 进行加 1 操作,然后把 i 的最新值 1 写入到内存。此时线程 2 的高速缓存当中 i 的值还是 0,进行加 1 操作之后,i 的值为 1,然后线程 2 把 i 的值写入内存。

最终结果 i 的值是 1,而不是 2 。这就是著名的缓存一致性问题。

如下图所示
每个CPU都有自己的高速缓存(寄存器),而它们又共享同一主存(Main Memory)。当多个CPU的运算任务都涉及到同一变量时,在每个CPU对应的高速缓存都会缓存一份该变量的值,那同步回主存时以谁的为准呢?
在这里插入图片描述
为了解决一致性的问题所以,就出现了 缓存一致性协议 ,其中最出名的就是 Intel 的 MESI 协议。MESI 协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是: 当 CPU 写数据时,如果发现操作的变量是共享变量,即在其他 CPU 中也存在该变量的副本,会发出信号通知其他 CPU 将该变量的缓存行置为无效状态。因此,当其他 CPU 需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取

2.Java内存模型-JMM

2.1概述

Java内存模型,一般指的是JDK 5 开始使用的新的内存模型。
JMM就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。
内存模型可以理解为在特定的操作协议下,对特定的内存或者高速缓存进行读写访问的过程抽象,不同架构下的物理机拥有不一样的内存模型,Java虚拟机也有自己的内存模型,即Java内存模型(Java Memory Model, JMM)。在C/C++语言中直接使用物理硬件和操作系统内存模型,导致不同平台下并发访问出错。而JMM的出现,能够屏蔽掉各种硬件和操作系统的内存访问差异,实现平台一致性,是的Java程序能够“一次编写,到处运行”。

2.2内存模型结构

Java 内存模型定义了线程和内存的交互方式,在 JMM 抽象模型中,分为主内存、工作内存。主内存是所有线程共享的,工作内存是每个线程独有的。线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,不能直接读写主内存中的变量。并且不同的线程之间无法访问对方工作内存中的变量,线程间的变量值的传递都需要通过主内存来完成,他们三者的交互关系如下 :
在这里插入图片描述

2.3内存间交互操作

主内存和工作内存的交互协议,即使一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存的实现细节,JMM定义了以下八种操作来完成,虚拟机实现必须保证下面的每一个操作都是原子的。

  • lock(锁定): 作用于主内存变量,把一个变量标识为一条线程独占的状态

  • unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放,释放后的变量才可以被其它线程锁定;unlock之前必须将变量值同步回主内存

  • read(读取):作用于主内存变量,把一个变量的值从主内存传输到工作内存,以便随后的load动作使用

  • load(载入):作用于工作内存变量,把read操作从主内存中得到的变量值放入工作内存的变量副本中

  • use(使用):作用于工作内存变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到的变量的值的字节码指令时将会执行这个操作

  • assign(赋值):作用于工作内存变量,把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作

  • store(存储):作用于工作内存变量,把工作内存中一个变量的值传送到主内存,以便随后的write操作使用

  • write(写入):作用于主内存变量,把store操作从工作内存中得到的值放入主内存的变量中在这里插入图片描述

这些行为是不可分解的原子操作,在使用上相互依赖,read-load从主内存复制变量到当前工作内存,use-assign执行代码改变共享变量值,store-write用工作内存数据刷新主存相关内容。

JMM还规定执行上述八种基本操作时必须满足如下规则:

  • 不允许read和load、store和write操作之一单独出现
  • 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
  • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
  • 一个变量在同一时刻只允许一条线程对其进行lock操作(同一条线程可以重复执行多次lock),lock和unlock必须成对出现
  • 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
  • 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
  • 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。

2.4先行发生原则:happens-before

对于上面八种规则,定义了一个等效判断原则-先行发生原则-happens-before:

  • 程序顺序原则:在一个线程内一段代码的执行结果是有序的。虽然还会指令重排,但是随便它怎么排,结果是按照我们代码的顺序生成的不会变。
  • 锁规则:无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果。
  • volatile变量规则:如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见。
  • 线程启动规则:在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见。
  • 线程结束规则:在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见。
  • 中断规则:线程interrupt()方法的调用比检测线程中断的事件发生的早,可以通过Thread.interrupted()检测到是否发生中断。
  • 终结器规则:一个对象的初始化的完成,也就是构造函数执行的结束一定早于happens-before它的finalize()方法。 传递性:A
  • happens-before B , B happens-before C,那么A happens-before C。

2.5原子性

Java内存模型来直接保证的8种原子性操作(red 、load、use、assign、store、write、lock、unlock);本数据类型的访问是具备原子性(例外long和double非原子性协定)。

如果应用到更大范围的原子性保证,JMM提供了lock和unlock,这两个操作未直接开放给用户使用,但提供了更高层次的字节码指令mointorenter和monitorexit来隐式的使用这两个操作,这两个字节码指令反映到java代码就是synchronized关键字,因此synchronized也保证原子性。

2.6可见性

当一个线程修改了线程共享变量的值,其它线程能够立即得知这个修改后的值。 JMM通过在变量修改后将新值同步回主内存,在要读取时它会去内存中读取新值。这种依赖主内存作为传递媒介的方法来实现可见性。

volatile之可见性:
  • 当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

  • 而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

synchronized之可见性:

JMM关于synchronized的规定

线程解锁前(unlock操作),必须把共享变量的最新值刷新到主内存
线程加锁时(lock操作),将清空工作内存中共享变量的值,从而在使用共享变量时需要从主内存中重新读取最新的值(加锁与解锁是同一把锁)

另外,通过Lock也能够保证可见性,Lock和synchronized一样能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

2.7有序性

在 Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
在 Java 中,可以通过 volatile 关键字来保证一定的“有序性”(禁止指令重排序)。另外,我们千万不能想当然地认为,因为synchronized 和 Lock 可以保证有序性,就认为它们不会发生指令重排序;因为 synchronized 和 Lock 保证每个时刻是有一个线程执行同步代码,相当于让线程串行执行同步代码,并不保证指令不会发生重排序

volatile之有序性:

原理:volatile的可见性和有序性都是通过加入内存屏障来实现
为了实现上述的volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

  • 在每个volatile写操作的前面插入一个StoreStore屏障。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadStore屏障。

在这里插入图片描述
在这里插入图片描述

内存屏障会提供3个功能:

1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

2)它会强制将对缓存的修改操作立即写入主存;

3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

  • 对volatile变量写操作时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存
  • 对volatile变量读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量
synchronized之有序性:

MM中有一条关于synchronized关键字的规则如下: ”一个变量在同一时刻只允许一条线程对其进行lock操作" 这条规则决定了持有同一个对象锁的两个线程只能串行的进入某个同步块(同步块中的操作相当于单线程执行),而且上面提到的重排序过程不会影响到单线程程序的执行,所以对外就表现出了有序性。
例子:指令重排导致单例模式失效

我们都知道一个经典的懒加载方式的单例模式:

public class Singleton {
	 // 指向自己实例的私有静态引用
    private static Singleton instance = null;
	// 私有的构造方法
    private Singleton() { }
	// 以自己实例为返回值的静态的公有方法,静态工厂方法
    public static Singleton getInstance() {
			// 被动创建,在真正需要使用时才去创建
            if(instance == null) {
				//同一时刻只有一个线程进入同步块执行创建对象
                synchronzied(Singleton.class) {
                    if(instance == null) {
                        instance = new Singleton();
                    }
                }
            }
        return instance;
    }
}

看似简单的一段赋值语句:instance = new Singleton();,其实JVM内部已经转换为多条指令:

memory = allocate(); //1:分配对象的内存空间

ctorInstance(memory); //2:初始化对象

instance = memory; //3:设置instance指向刚分配的内存地址

但是有可能经过重排序后如下:

memory = allocate(); //1:分配对象的内存空间

instance = memory; //3:设置instance指向刚分配的内存地址,此时对象还没被初始化

ctorInstance(memory); //2:初始化对象

可以看到指令重排之后,instance指向分配好的内存放在了前面,而这段内存的初始化被排在了后面,在线程A初始化完成这段内存之前,线程B虽然进不去同步代码块,但是在同步代码块之前的判断就会发现instance不为空,此时线程B获得一个不完整(未初始化)的 Singleton 对象进行使用就可能发生错误。

在1.5或之后的版本中,我们可以将instance设置为volatile就可以了,这样就会确保将实例域的数据写回到主内存的动作在将实例赋值给instance引用动作之前发生(即volatile的 happens-before 规则),所以这样就确保了在使用前对象已完全初始化完成。

参考:
链接: 《深入理解java虚拟机》周志明著.
https://blog.csdn.net/wandoubi/article/details/80147858

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值