天天看点

Java中内存模型定义一、内存模型由来二、内存模型三、Java内存模型的实现volatile

在此之前一直是听说过这个词——Java内存模型。而且在之前面试中也遇到过,说的感觉不好,没有一点逻辑性。

定义

首先我之前听到这个词之后就一直感觉是一种抽象的模型,但是实际上不是这个东西。实际上就是一种java规范。

java内存模型(JMM)就是java虚拟机规范定义的,用来屏蔽掉java程序在各种不同的硬件及操作系统中对内存访问的差异性,这样就能够使java在不同平台上都能达到内存访问一致的效果。

一、内存模型由来

仔细看定义就是硬件与系统存在差异性,还有就是保证内存访问一致性。

CPU和物理硬件

我们在买手机或者电脑的时候主要就是看cpu和内存的大小,这两个基本上就能够看出你设备的性能。

cpu:中文名称叫中央处理器,是计算机系统运算和控制的核心。主要功能就是即使计算机指令以及处理计算机软件中的数据。

内存:它是cpu与外部存储沟通的桥梁,计算机中所有的程序都运行在内存中。

在实际中cpu执行指令的时候又需要与数据打交道,而数据都是在内存中,所以一个程序执行的时候两者都需要参与其中。

但是cpu执行的速度是比内存快上好几个数量级,所以这就导致了cpu执行得很快,内存速度跟不上,导致cpu一直等待,导致资源浪费。把所有东西都交给cpu肯定是不可能的,都交给内存就太慢了,所以说要协调cpu与硬件之间的速度差异是非常重要的。

所以就有一类奇特的人想出在cpu和内存之间增加一个高速缓存。高速缓存具有速度快、内存小、昂贵等特点。

程序在执行时,会将运算的数据从主内存中拷贝一份到高速缓存中,当cpu需要数据时直接从高速缓存中读取,完成后写入到高速缓存中,运算结束之后再将高速缓存中的数据写入到内存中。

随着cpu速度越来越快,一级缓存的速度已经跟不上cpu的速度了,逐渐就有了多级缓存。

按照数据读取顺序和cpu结合的紧密程度将cpu缓存分成一级缓存(L1)、二级缓存(L2),部分高端cpu还有三级缓存(L3),每一级缓存所存储的数据都是下一级的部分数据。

程序在执行的时候,cpu需要读取一个数据,先从一级缓存中查找,如果没有就会从二级缓存中查找,如果还是没有则会从三级缓存或者内存中查找。

缓存不一致

Java中内存模型定义一、内存模型由来二、内存模型三、Java内存模型的实现volatile

随着cpu技术的不断发展,现在计算机中一般都是多核cpu.

Java中内存模型定义一、内存模型由来二、内存模型三、Java内存模型的实现volatile

在单核CPU情况下,cpu的缓存无论是在多线程还是在单线程,缓存都只能被一个线程访问,不会出现访问冲突等问题。

在多核CPU情况下,多进程同时访问某个共享内存,多个线程如果分别在不同的cpu上执行,则每个缓冲区都会保存这个数据共享内存,而且每个CPU之间是可以并行执行的,可能会出现多个线程同时写缓存中数据的情况,而各自之间的缓存有可能不同,导致出现缓存不一致的情况。

处理器优化和指令重排序

在cpu中为了让程序能够更加高效快速的运行,是内部资源能够充分利用,处理器可能会对代码进行乱序执行处理,这叫处理器优化。

除了处理器的乱序执行之外,现在很多语言的编译器也会有类似的优化,比如java的即时编译器会进行指令重排序。

可想而知如果代码中后面的代码需要前面代码的结果接可能会导致各种各样的错误出现。

总结一下,java内存模型出现的原因:CPU和物理硬件之间的速度差异、缓存不一致的问题以及处理器优化和指令重排序的问题。

指令重排序

计算机在执行程序时为了提高性能,会对程序中的指令进行重排序,一般分为三种重排序:

编译器重排序

编译器在不改变单线程下执行结果前提下,可以重新安排语句的执行顺序。

指令并行重排序

现代处理器采用了指令级并行技术将多条指令重叠执行,如果不存在数据依赖,则可以进行重排序。

内存系统重排序

由于处理器使用缓存和读写缓冲区,这使得load和store看起来是乱序执行,因为多级缓存的存在,导致内存与缓存之间存在时间差。

二、内存模型

在了解内存模型之前我们要先了解一下他的三个特征:

原子性:就是一个指令在CPU执行的时候不能暂停,也不能被中断,要不就执行完,要不就不执行。

可见性:当多个线程访问同一个变量时,一个线程修改了变量的值,其他的线程必须立马看到修改后的值。

有序性:程序的执行是按照代码的先后顺序来执行的。

这三个特征就对应上面的三个问题:原子性其实就是处理器的优化,有序性就是指缓存一致性问题,有序性就是指令重排序的问题。

为了更好地解决上面所提到的问题,我们可以理解为:在特定协议规范下,对特定的内存或高速缓存进行读写操作的抽象。通过这个协议规范,能够保证指令执行的正确性。

Java内存模型

在不同的物理架构的计算机中,可能都会有不同的内存模型,java虚拟机也有自己的内存模型。java虚拟机规范试图定义一种java内存模型(Java Memory Model  简称:JMM),来屏蔽掉各种物理硬件和操作系统之间的内存访问的差异性,实现java程序在各种平台中都能达到一致的内存访问效果。

三、Java内存模型的实现

在java内存模型中所有变量都存在内存中,而每一个线程都是有自己的工作内存,工作内存中保存了该线程用到的变量的内存拷贝副本,线程对变量的操作都是在工作内存进行,不能直接读写内存。而不同的线程是不能够直接访问对方的工作内存,都需要通过内存来进行交互。

Java中内存模型定义一、内存模型由来二、内存模型三、Java内存模型的实现volatile

主内存:java内存模型规定所有的变量都存在主内存中,也就是我们物理硬件所指的内存。

工作内存:每一个线程都有一个自己的工作内存,也称本地内存,该内存中保存了当前线程所使用的共享变量的内存副本。工作内存其实是一个抽象概念,并没有真实存在。

交互流程

Java中内存模型定义一、内存模型由来二、内存模型三、Java内存模型的实现volatile

如图就是他的交互流程:首先我们假定主内存中x初始值为0,所以两个现成的工作内存中读取到的x均为0,期中线程1对x进行操作之后变为1,之后写入到工作内存中,之后再更新到主内存中,线程2就要再重新读取x的值,再拷贝到工作内存中。这样线程2就得知了x的值更新了。

内存交互的基本操作

如何从主内存读取,又如何将工作内存中的值更新到主内存的呢?也就是线程间通信的问题。

Java中内存模型定义一、内存模型由来二、内存模型三、Java内存模型的实现volatile

java内存模型定义了八个步骤来完成之间的细节实现:

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

2.unlock(解锁):作用于主内存变量,将一个处于锁定状态的变量释放出来,释放后的变量才能被其他线程访问。

3.read(读取):作用于主内存变量,把一个变量从主内存传输到工作内存中,以便之后使用。

4.load(载入):作用于工作内存变量,将read到的变量放入到工作内存的变量的副本中。

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

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

7.store(存储):作用于工作内存的变量,把工作内存中的变量传递到主内存中,以便之后操作使用。

8.write(写入):作用于主内存变量,将从store中获取到的变量传送到主内存的变量中。

java内存模型还规定了在执行上述八种操作的时候必须遵守以下原则:

①如果要把一个变量从主内存复制到工作内存,就需要按顺序执行read和Load操作,如果把工作内存中变量同步回主内存中,就需要顺序的执行store和write。但java模型只要求上述操作必须按顺序执行,并没有保证必须连续执行。

②不允许read和load、store和write操作之一单独出现。

③不允许一个线程丢弃它的最近assign操作,也就是说变量在工作内存改变之后必须同步到主内存中。

④不允许一个线程无原因地把数据从工作内存中同步回主内存中。

⑤一个新的变量必须在主内存中产生,不能再工作内存中直接使用一个未被初始化的变量。就是对一个变量试试use和store操作之前,必须先执行了assign和load操作。

⑥一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行,多次执行lock操作之后,只有执行相同次数的unlock之后变量才会被解锁。lock和unlock要成对出现。

⑦如果对一个变量执行lock操作,将会清空工作内存中的此变量的值,在执行引擎使用这个变量前需要重新执行执行load或者assign错做初始化变量的值。

⑧对一个变量执行unlock之前,必须先把此变量同步到主内存中(执行store和write操作)

volatile

当一个变量被volatile修饰之后就具备了两个语义:

①一个线程修改了变量的值时,变量的新值对其他线程是立刻可见的。

②禁止指令重排序

volatile可见性

//线程1
boolean stop=false;
while(!stop){
    //doSomething
}
//线程2
stop=true;
           

我们日常开发中都会使用该方式中断线程,但是这段代码不一定会将线程中断,但如果一旦发生中断就会造成死循环。

线程1在运行的时候将stop拷贝到自己的工作内存中,当线程2更改了stop的值之后突然去做其他的操作,可能导致无法将变量更新会主内存当中,这样线程1就不会知道线程2修改了值,会一直循环下去。

当stop用volatile修饰之后,当线程2修改stop值之后会强制刷新到主内存中,这样线程1在读取的时候就会从主内存中重新读取。

volatile保证有序性

因为volatile禁止指令重排序所以保证了有序性。

当程序执行到volatile变量操作时,在其前面的操作已经全部执行完毕,并且结果会对后面的操作可见,在其后面的操作还没有执行。

在进行指令重排序的时候,在volatile变量之前的语句不能在volatile变量后执行。