Java的内存模型

简介

Java内存模型的主要目的是定义程序中各种变量的访问规则,即关注在虚拟机中把变量的值写入内存和从内存中取出的底层细节。这里的变量指的是实例字段、静态字段、构成数组的元素等可被共享的变量。

主内存与工作内存

Java内存模型规定所有的变量都存储在主内存(Main Memory)中,每个线程拥有自己的工作内存(Working Memory)。

线程的工作内存保存了被该线程使用的变量的主内存副本,线程对变量的读取和赋值操作只能在自己的工作内存中进行,而不能直接读写主内存中的数据。

不同的线程不能直接访问对方的工作内存中的变量,线程间变量数据的传递必须通过主内存作为中介完成。线程、工作内存和主内存三者的交互关系如图:
Java的内存模型_第1张图片

内存间的交互

关于主内存与工作内存之间的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存的实现细节,Java内存模型定义了八种操作,这八种操作每个都是原子的,不可拆分的,具体如下:

  • lock(锁定):作用于主内存中的变量,它把一个变量标记为线程独占状态
  • unlock(解锁):作用于主内存中的变量,它把一个标记为锁定状态的变量进行锁释放
  • read(读取):作用于主内存中的变量,它把一个变量从主内存中传输到线程的工作内存,以便后续的load操作
  • load(载入):作用于线程工作内存的变量,把read操作从主内存读取的变量加载到工作内存的变量副本中
  • use(使用):作用于线程工作内存的变量,把工作内存中的变量的值传递给执行引擎,当虚拟机遇到一个需要使用变量的字节码指令时将会执行这个操作
  • assign(赋值):作用于线程工作内存的变量,把一个从执行引擎接受的值赋值给工作内存中的变量,当线程遇到一个执行引擎需要赋值的字节码指令时将执行这个操作
  • store(存储):作用于线程工作内存的变量,把一个工作内存中变量的值传递到主内存,以便后面的write操作来写入主内存
  • write(写入):作用于主内存,把store过程传递过来的变量的值写入到主内存中

原子性、可见性与有序性

Java内存模型的定义和规则都是围绕着在多线程并发执行过程中如何保证原子性、可见性和有序性这三个特征来建立的。

原子性(Atomicity)

Java内存模型直接保证的原子性变量操作包括:read、load、assign、use、store、write这六个,基于此,基本数据类型(char,short,int,boolean)访问、读写都具备原子性(例外就是long和double的非原子性协议)。

如果应用场景需要一个更大范围的原子性保证,Java内存模型提供了lock和unlock的操作来满足这个需求,虚拟机虽未把lock和unlock直接放开给用户使用,但是提供了更高层次的字节码指令monitorenter和monitorexit来隐式的实现这两个操作,这两个字节码指令反映到Java中的实现就是同步块synchronized关键字。

可见性(Visibility)

可见性指的是线程对工作内存中共享变量的值的修改对其他线程立即可见。Java的内存模型通过在变量修改后把变量的值同步到主内存,在读取变量前从主内存中刷新变量的值到工作内存这种依赖来实现可见性,无论是普通变量还是volatile变量都是如此。

普通变量和volatile的区别在于,volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前都强制从主内存刷新,因此,volatile可以保证多线程操作时变量的可见性,而普通变量不能保证这一点。

除了volatile以外,synchronized和final也可以保证共享变量的可见性,其中synchronized的可见性是通过对一个变量执行unlock之前必须强制把变量的值同步到主内存实现的。final关键字的可见性是指,在被final修饰的变量在构造器中一旦被初始化完成,并且构造器没有把“this”的引用传递出去(this引用逃逸会导致其他线程有可能通过这个引用访问到“初始化一半”的对象),那么其他线程中就可以看到final字段的值。

有序性

Java虚拟机的即时编译器会对代码指令进行重排序优化,普通变量仅会保证在线程内代码执行过程中依赖这个变量结果的地方都能获取到正确的结果,但是不能保证变量赋值操作的顺序和程序代码的顺序是一致的。这种重排序优化在多线程操作下可能会导致意料不到的问题,用下面一段伪代码说明下:

Map configOptions;
char[] configText;
// 此变量需声明为volatile变量
volatile boolean initialized = false;

// 下面代码在线程A中运行
// 模拟读取配置信息,当读取完成后initialized设置为true
configOptions = new HashMap<>();
configText = readConfigFile(fileName);
processConfigOptions(configText, configOptions);
initialized = true;

// 下面代码在线程B中执行
while(!initialized) {
    sleep();
}
// 使用线程A中初始化好的配置信息
doSomethingWithConfig();

如果指令重排序,线程A中的initialized = true指令可能会在加载配置文件之前就被执行,那么线程B中会出现使用未加载配置信息的错误。

volatile关键字可以禁用指令重排序,即不允许改变initialized = true在代码指令中原有的位置,从而保证了代码按照预期的执行顺序执行,确保不会出现意料之外的问题。

除了volatile关键字之外,synchronized也可以保证多线程环境下的有序性,synchronized的特征保证了同一时刻只能有一个线程执行,多个线程是串行执行的,每一次unlock操作共享变量的修改必须强制刷新到主内存。

先行发生原则

Java中只使用volatile和synchronized来保证有序性,那么在操作上就会比较繁琐,Java语言定义了“先行发生”(Happends-Before)原则,先行发生的操作结果对于后面的操作可见,这些原则无需任何同步器协助天然存在,可以在代码中直接使用。具体的规则如下:

  • 程序次序规则:在一个线程内,按照控制流顺序,书写在前面的操作先行发生与书序在后面的操作
  • 管程锁定规则:一个unlock操作先行发生于后续对同一个锁的的lock操作
  • volatile变量规则:对volatile变量的修改操作先行于对该变量的读取操作
  • 线程启动规则:Thread对象的start()方法先行发生于线程的每个操作
  • 线程终止规则:线程中的所有操作先行发生于线程的终止操作
  • 对象终止规则:一个对象的初始化操作(构造函数执行结束)先行发生于它的finalize()方法的开始
  • 传递性:如果A操作先行发生与B操作,B操作先行发生与C操作,那么A操作先行发生于C操作

对于可见性的判断,我们可以使用上面的规则直接进行判断,或者通过多个规则推导进行判断。

你可能感兴趣的:(jmmjava)