Java内存模型

来源:互联网 发布:java 网店 数据设计 编辑:程序博客网 时间:2024/06/08 00:31

Java内存模型

一、Java内存模型内部原理

图示:
这里写图片描述
每个线程都有自己独立的工作内存,线程的所有操作都是在自己的工作内存中完成的,所以每个线程的操作数都是独立的,不共享。要实现其他线程可见必须将工作内存中的变量写回主存中。
通过以下操作可以进行内存间的数据交互(最后的“()”中给出简易版解析,方便理解):

  • lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。(锁定变量只能被唯一线程访问)
  • unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。(解锁,使变量可以被所有线程访问)
  • read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中。(将数据从主存读到工作内存)
  • load(载入):作用于工作内存的变量,将read操作得到的变量值放入工作内存的变量副本中。(将read得到的数据赋值给变量)
  • use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。(将变量传给执行引擎)
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。(将执行引擎中的值赋值给变量)
  • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。(将工作内存变量的值写回主存)
  • write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。(将store写到主存的值赋值给主存变量)
    PS:以上每点间的相对顺序保持一致,当然相互之间也可以插入其他操作,但不可改变它们的相对顺序。

二、内存屏障

1、内存屏障介绍

    现代的操作系统都是多处理器。每个处理器都有自己的缓存,并且这些缓存并非实时与内存发生信息交换。这样就可能出现一个cpu上的缓存数据与另一个cpu上的缓存数据不一致的问题。而这样在多线程开发中,就有可能导致出现一些异常行为。    内存屏障将指令集一分为二,每部分内部指令间可以进行重排序,但是两部分之间不可以进行重排序。也就是说,被内存屏障隔着的这两部分指令相对顺序保持不变,指令重排序不可以逾越内存屏障。    内存屏障提供了两个功能。首先,它们通过确保从不同CPU来看屏障的两边的所有指令都是正确的程序顺序,保持程序顺序的外部可见性;其次它们可以实现内存数据可见性,确保内存数据会同步到CPU缓存子系统,确保各CPU缓存的一致性。所以内存屏障确保了不同操作指令之间的顺序,解决了多CPU缓存不一致问题。    以下是内存屏障的分类:

这里写图片描述

2、内存屏障在Java中的应用

内存屏障在Java中的应用主要有两种,一是synchronized语句块,二是volitate关键字,其它的应用主要分布在UnSafe类中。
  • synchronized语句块:synchronized语句块保证了原子性,即某一时间段只能有一个线程对它进行访问,synchronized语句块内的操作是不可分割的。内存屏障在synchronized中的应用主要是保障它的可见性,当某线程在synchronized语句块中修改了变量时,当退出synchronized语句块时会将它写进主存,确保全局可见性。

  • volitate关键字:当程序对volitate变量进行了写操作时,JVM就会向处理器发送一条Lock前缀的指令,将工作内存中的变量值刷到主存中。同时每个处理器嗅探在总线上传播的数据,检查自己缓存的值是否过期,如果处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。volitate关键字只是确保可见性,不确保原子性,所以多线程修改volitate变量时会产生线程安全问题。

三、指令重排序

1、指令重排序原因

  • 虚拟机层面:内存操作速度远慢于CPU运行速度,所以大部分时间下CPU都处于空置状态。为了提高CPU的使用率,虚拟机会按照自己的规则将程序编写顺序打乱。比如当CPU给数组变量分配内存的时候,内存操作速度比较慢,因此会先执行后续的赋值等与数组变量无依赖性而且耗时比较少的工作。
  • 处理器层面:CPU执行速度比缓存快,所以当它接收到一批指令的时候会按照自己的规则进行指令重排序,提高CPU的利用率。

2、指令重排序原则

  • as-if-serial:不改变单线程的语义,也就是无论怎么重排序,程序在单线程内执行的结果不受影响。所以,编译器和处理器不会对存在数据依赖关系的操作做重排序,对不存在数据依赖关系的指令则可以进行重排序。
例子:int a = 1;  //1int b = 1;  //2int a = 2;  //3int c = a + 1;  //4

在上述程序中,第4行和第3行不可进行重排序,因为第4行的变量a依赖于第三行,而第2行对b赋值则可以在任何一处执行,因为对b赋值不依赖其他变量。

  • 当进行并行排序时,如果两个线程间不存在数据依赖性,则可进行指令重排序。如果线程b的执行依赖于线程a,则不可将a、b两个线程的指令进行重排序。
  • 内存系统:程序的读写操作不一定会按照它要求处理器的顺序执行,这也是指令重排序。

四、happens-before

  • 内存屏障前的读写操作先于屏障后的读写操作;
  • 对同一个锁,前一个线程的锁释放操作先于后一个线程的加锁操作;
  • happens-before有传递性,比如A先于B执行,B先于C执行,则A先于C执行;
  • 数据依赖:产生被依赖数据的程序先于依赖该数据的程序执行。
1 0