java内存模型(jMM)(一)
在说java的内存模型之前先简单的了解计算机的主存和缓存的相关概念。
多任务和高并发是衡量一台计算机处理器的重要指标。一般衡量一个服务器性能的高低好坏,使用每秒事务处理数(Transactions Per Second,TPS),它代表着一秒内服务器平均能响应的请求数,而TPS值与程序的并发能力有着非常密切的关系。由于计算机的存储设备与处理器的运算能力之间有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无需等待缓慢的内存读写了。但是每个处理器都有自己单独的高速缓存,所以这导致了一个问题:当多个处理器同时对主存中的同一个变量进行操作的时候就会出现缓存不一致以及指令执行顺序错乱问题,而JMM(Java memory model)就是为了解决这些问题。
概念
JMM是一个抽象的概念,并不真实存在。它涵盖了缓存、写缓冲区,寄存器以及其他的硬件和编译器优化。在jvm中,线程共享的变量都存放在堆(Heap)中,每个线程都有其私有的栈空间,私有变量等信息就是存放在栈(Stack)内存中。JMM是一种语言级的内存模型,它确保在不同的编译器和处理器平台上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一直的内存可见性保证
内存交互操作
内存的交互操作定义了工作内存和主内存之前是怎么 进行交互的,java内存模型一共定义一下八种操作:
-
-
- lock(锁定):作用于主内存变量,把一个变量标识为一个线程独占状态
- unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放的变量才可以再次被其他的线程锁定
- read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中
- load(载入):作用于工作内存的变量,它把read操作从主内存中获取的变量值放入到工作内存的变量副本中
- use(使用):作用于工作内存的变量,把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的字节码指令时将会执行这个操作
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内内存中的变量,每当虚拟机遇到一个给变量赋值的字节码指令时将会执行这个操作
- store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存,一遍后续的write操作使用
- write(写入):作用 于主内存变量,塔把store操作从工作内存中获取到的值传送到主内存中的变量中
-
重排序
从java源码到最终实际执行的指令序列,会分别经历下面三种重排序:
- 编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序
- 指令级并行的重排序:现代处理器采用了指令级并行技术(Instruction-Level Parallelism ,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序
- 内存系统的重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行
处理器重排序和内存屏障指令
现代处理器使用写缓冲区来临时保存向内存中写入的数据。写缓冲区可以保证指令流水线持续执行,它可以避免由于处理器停顿下来等待向内训中写入数据而产生的延迟。同时,通过以批处理的方式刷新写缓冲区,以及合并写缓冲区中对同一内存地址的多次写,可以减少对内存总线的占用。虽然写缓冲区有这么多的好处,但每个处理器的写缓冲区仅仅对它所在的处理器可见。这个特性会对内存操作的执行顺序产生重要的影响:处理器对内存的读/写操作的执行顺序,并不一定于内存实际发生读/写操作顺序一致。所以为了保障内存的可见性,java编译器在生成指令序列的适当位置插入内存屏障指令来禁止特定类型的处理器重排序。JMM吧内存屏障指令分为以下四类:
屏障类型 | 指令示例 | 说明 |
LoadLoad Barriers | Load1; LoadLoad; Load2 | 确保Load1数据的装载,之前于Load2及所有后续装载指令的装载。 |
StoreStore Barriers | Store1; StoreStore; Store2 | 确保Store1数据对其他处理器可见(刷新到内存),之前于Store2及所有后续存储指令的存储。 |
LoadStore Barriers | Load1; LoadStore; Store2 | 确保Load1数据装载,之前于Store2及所有后续的存储指令刷新到内存。 |
StoreLoad Barriers | Store1; StoreLoad; Load2 | 确保Store1数据对其他处理器变得可见(指刷新到内存),之前于Load2及所有后续装载指令的装载。StoreLoad Barriers会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令。 |
StoreLoad Barriers是一个“全能型”的屏障,它同时具有其他三个屏障的效果。现代的大多数处理器都支持这个屏障(其他类型的内存屏障指令不一定所有的处理器都支持)。执行该屏障的开销会很昂贵,因为当前处理器 通常要吧写缓冲区的数据全部刷到内存中去。
顺序一致性
数据竞争与顺序一致性保证
当程序未正确同步时,就可能存在数据竞争。java内存模型对数据竞争的定义如下:
-
-
-
- 在一个线程中写一个变量
- 在另一个线程读同一个变量
- 而且读和写没有通过同步来排序
-
-
当代码中包含数据竞争时,程序的执行往往产生违反直觉的结果。如果一个多线程程序能够正确同步,这个程序将是一个没有数据竞争的程序,JMM对正确同步的
程序的内存一致性做了如下保证:
-
-
-
- 如果程序是正确同步的,程序的执行将有顺序一致性(sequentially consistent)--即程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同。这里指的同步是广义上的同步,包括对常用同步原语(synchronized、volatile、final)的正确使用
-
-
顺序一致性内存模型
顺序一致性内存模型是一个被计算机科学家理想化了的理论参考模型,它为程序员提供了极强的内存可见性保证。顺序一致性内存模型有量大特性:
-
-
-
- 一个线程中的所有操作必须按照程序的顺序来执行
- (不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须是原子操作且立即对所有线程可见
-
-
未同步程序的执行特性
对于未同步或未正确同步的程序,JMM只提供最小的安全性:线程执行读取到的值,要么是之前某个线程写入的值,要么是默认值(0,false,null)。JMM保证读取操作读取到的值不会无中生有(out of thin air),的冒出来。为了实现最小安全性,JMM在堆上分配内存时,首先会清零内存空间,然后才会在上面分配对象(JVM内部会同步这两个操作)。因此,在已清零的内存空间(pre-zeroed memory)分配对象时,域的默认初始化已经完成。
JMM不保证未同步程序的执行结果与该程序在顺序一致性模型中的执行结果一致。因为要想保证执行结果一致,JMM需要禁止大量的处理器和编译器的优化,这对程序的执行性能带来很大的影响。而且未同步程序在顺序一致性模型执行时,整体是无序的,其执行结果往往无法预知。所以保证未同步程序在这两种模型下的执行结果一致没什么 意义。
未同步程序在JMM中执行时,整体上是无序的,其执行结果无法预知。未同步程序在两个模型中的执行特性有一下几个差异:
-
-
- 顺序一致性模型保证单线程内的操作按照程序的顺序执行,而JMM不保证单线程内的操作会按程序的顺序执行
- 顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而JMM不保证所有线程能看到一致的操作顺序
- JMM不保证对64位的long/double型变量的读/写操作具有原子性,而顺序一致性模型保证对所有的内存读/写操作都具有原子性
-
参考资料:<<深入理解java内存模型>> 程晓明著