它的意义是来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效。在此之前,主流程序语言(C/C++等)直接使用物理硬件和操作系统的内存模型。因此,会由于不同平台上的内存模型差异,有可能导致程序在不同平台上运算结果不同。因此在某些场景下必须对不同的平台来编写程序。
1. 主内存与工作内存
Java内存模型主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到主存和从内存中读取出变量这样的底层细节。此处变量包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量和方法参数。因为后者是线程私有的,不会被共享,也就不存在并发的问题。
Java内存模型规定:所有的变量都存储在主内存中。每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝。线程对变量的所有操作(读取、赋值)都必须在工作内存中完成,不能直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程之间变量值的传递均需要通过主内存来完成。三者之间的关系如下图:
2. 内存间的交互操作
Java内存模型定义了8种操作来完成内存间的交互,且虚拟机必须保证下面的每一种操作是原子的,不可分割的(long和double例外):
- lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
- unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
- load(载入):作用于主内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于主内存的变量,它把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用的变量的值的字节码指令时将会执行这个操作。
- assign(赋值):作用于主内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时将会执行这个操作。
- store(存储):作用于主内存的变量,它把工作内存中一个变量的值传递到主内存中,以便随后的write操作使用。
- write(写入):作用于主内存的变量,它把store操作从工作内存中获得的变量的值放入主内存的变量中。
若把一个变量从主内存复制到工作内存中,要顺序执行read和load操作;若要把变量从工作内存同步回主内存,要顺序执行store和write操作。注意,read和load、store和write之间可以插入其他指令。
注意:在JSR-133文档中,已经放弃采用这8种操作去定义内存模型的访问协议了。
3. volatile
它是Java虚拟机提供的最轻量级的同步机制。当把一个变量定义为volatile后,它将具备两种特性:
- 第一保证变量对所有线程的可见性。可见性是指当一个线程修改了这个变量的值,新值对其他所有线程来说是可以立即得知的。普通话标本量的值不能做到这一点,普通变量的值在线程间传递均需要通过主内存来完成。但要保证原子性,仍然要通过加锁来实现。
- 第二是禁止指令重排序优化。普通变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能得到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。
volatitle 变量读操作的性能消耗与普通变量几乎没有什么差别,但是写操作则可能会慢一些。但是大多数场景下volatile 的总开销要比锁低。
Java内存模型允许竟没有被 volatile 修饰的64位数据的读写操作划分为两次32位的操作进行,即允许虚拟机实现选择可以不保证64位数据类型的laod、store、read和write这4个操作的原子性。但是在各平台的商用虚拟机几乎都选择把64位数据的读写操作作为原子操作来对待。
4. 原子性、可见性和有序性
原子性:由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store和write。可以大致认为基本数据类型的访问读写具备原子性(long和double除外)。
可见性:是指当一个线程修改了共享变量,其他线程能够立即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取之前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是volatile 变量都是如此。普通变量与 volatile 变量的区别是,volatile 的特使规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。因此可以说volatile 保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。另外,synchronized 和 final 也可以实现可见性。final关键字可见性是指:被 final 修饰的字段在构造器中一旦初始化完成,并且构造器没有吧this的引用传递出去,那在其他线程中中就能看见 final 字段的值。
有序性:Java提供了 volatile 和 synchronized 关键字来保证线程之间操作的有序性, volatile关键字本身就包含了禁止指令重排的语义,而 synchronized 则是由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则获得的,这条规则决定了持有同一锁的两个同步块只能串行地进入。
5. 先行发生原则(happens-before)
它是Java内存模型中定义的两项操作之间的偏序关系。如果说操作A先行发生于操作B,换句话说就是,操作A产生的影响能被操作B观察到。影响包括:修改了内存中共享变量的值、发送了消息、调用了方法等。
下面是Java内存模型下一些“天然的”先行发生关系,这些先行发生关系五须任何同步器协助就已经存在,可以直接使用。若两个操作之间的关系不在此列,并且无法从下列规则推导出来的话,它们的顺序就没有顺序性保障,虚拟机可以对它们随意地进行重排序。
- 程序次序规则:在一个线程内,按照代码顺序,书写在前面的操作先行发生于后面的操作。
- 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须是同一个锁,而“后面”是指时间上的先后顺序。
- volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作。后面”是指时间上的先后顺序。
- 线程启动规则:Thread对象的 start() 方法先行发生于此线程的每一个操作。
- 线程终止规则:线程中所有的操作都先于对此线程的终止检测。
- 线程中断规则:对线程interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
- 对象中结规则:一个对象的初始化完成先行发生于它的finalize()方法的开始。
- 传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,则操作A先行发生于操作C。