阅读 221

Java并发(三)java内存模型

人生有涯,学海无涯

一、物理计算机的内存模型

大家都知道,计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。

  也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。

cpu高速缓存.jpg

当CPU(处理器)要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找。一般来说,每级缓存的命中率大概都在80%左右,也就是说全部数据量的80%都可以在一级缓存中找到,只剩下20%的总数据量才需要从二级缓存、三级缓存或内存中读取。

高速缓存(Cache Memory)是位于CPU与内存之间的临时存储器,它的容量比内存小的多但是交换速度却比内存要快得多。高速缓存的出现主要是为了解决CPU运算速度与内存读写速度不匹配的矛盾,因为CPU运算速度要比内存读写速度快很多,这样会使CPU花费很长时间等待数据到来或把数据写入内存。在缓存中的数据是内存中的一小部分,但这一小部分是短时间内CPU即将访问的,当CPU调用大量数据时,就可先缓存中调用,从而加快读取速度。

缓存不一致的问题

虽然高速缓存提高了CPU(处理器)处理数据的速度问题。在多线程中运行就会有问题了。在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存(对单核CPU来说,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的)。这时CPU缓存中的值可能和缓存中的值不一样,这就会出现缓存不一致的问题。为了解决该问题。物理机算计提供了两种方案来解决该问题。

总线加LOCK#锁的方式

总线(Bus)是计算机各种功能部件之间传送信息的公共通信干线,它是由导线组成的传输线束,在计算机中数据是通过总线,在处理器和内存之间传递。

总线机制.png

在早期的CPU当中,是通过在总线上加LOCK#锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。在总线上发出了LCOK#锁的信号,那么只有等待这段代码完全执行完毕之后,其他CPU才能从其内存读取变量,然后进行相应的操作。这样就解决了缓存不一致的问题。

但是上面的方式会有一个问题,由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。所以就出现了缓存一致性协议

缓存一致性协议

最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

img

二、Java内存模型

Java内存模型(即Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图

这里写图片描述

从这张图我们可以看出,线程之间的通信是受jmm控制的,我们就这张图来说,线程A和线程B如何才能进行通信,假如线程A先执行,它会拿到共享变量,然后进行操作产生共享变量的副本,紧接着将本地内存中的共享变量副本刷新到主内存中,这时共享变量也就得到的更新,同理线程B 再去操作共享变量就达到了线程A和线程B之间的通信了。

主内存

主要存储的是Java实例对象,所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量),当然也包括了共享的类信息、常量、静态变量。由于是共享数据区域,多条线程对同一个变量进行访问可能会发现线程安全问题。

工作内存

主要存储当前方法的所有本地变量信息(工作内存中存储着主内存中的变量副本拷贝),每个线程只能访问自己的工作内存,即线程中的本地变量对其它线程是不可见的,就算是两个线程执行的是同一段代码,它们也会各自在自己的工作内存中创建属于当前线程的本地变量,当然也包括了字节码行号指示器、相关Native方法的信息。注意由于工作内存是每个线程的私有数据,线程间无法相互访问工作内存,因此存储在工作内存的数据不存在线程安全问题。

内存之间交互

主内存与工作内存之间的内存交互,也就是从线程的私有内存数据同步到主内存中,从主内存的读取数据到线程的私有内存中。Java内存模型定义了8种操作来完成。虚拟机在实现时保证下面提到的每一种操作都是原子的,不可再分的

img

lock:作用于主内存的变量,它把一个变量标识为一条线程独占的状态。

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

read:作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,一遍随后的load动作使用。

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

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

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

store:作用于工作内存的变量,它把工作内存中一个变量值传送到主内存中。以便随后的write操作。

write:作用于主内存的变量,它把store操作从工作内存中得到的变量的值,放入主内存的变量中。

八种原子操作规则

既然Java内存模型规定了内存之间交互的一些操作。那么我们来看看,它到底拥有哪些规则呢。

  • 不允许read和load、store和write操作之一单独出现。即不允许一个变量从主内存读取了但工作内存不接受。或者从工作内存发起回写了但主内存不接受的情况
  • 不允许一个线程丢弃它的最近的assign操作。即变量在工作内存改变了后必须把该变化同步到主内存中。
  • 不允许没有发生任何的assign操作就把数据同步到主内存中。
  • 一个新的变量只能在主内存中诞生,工作内存要使用或者赋值。必须要经过load或assign操作。
  • 一个变量在同一时刻只允许一条线程进行lock操作,但lock操作可以被同一线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
  • 如果对一个变量进行lock操作后,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作。
  • 如果一个变量事先没有被lock操作锁定,那就不允许对它进行unlock操作。也不允许去unlock一个被其他线程锁定的变量。
  • 对一个变量执行unLock操作之前,必须要把次变量同步到主内存中(执行store,write操作)。

上述规则规定了Java内存之间交互的流程。保证了数据在单线程情形下传输过程中的准确性与数据一致性。

文章分类
后端
文章标签