JVM规范了视图定义一种JMM来屏蔽各个硬件平台和OS的内存访问差异,属于语言级的内存模型,实现让Java程序在各平台下都能达到一致的内存访问效果,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。
JMM定义了什么东西?
JMM定义了程序中的变量访问规则(程序执行的次序),但是,为了更好的执行性能,JMM没有限制执行引擎使用缓存,也没有限制编译器对指令进行重排序,即,在JMM中,会存在缓存一致性问题和指令重排序问题。
JMM规定所有的变量都在主存中,每个线程有自己的工作内存,线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作,并且每个线程不能直接访问其他线程的工作内存。
一些概念
-
数据依赖性
两个操作访问同一个变量,且有一个操作为写,此时会发生数据依赖性(写后读,读后写,写后写)
-
顺序一致性(as-if-serial)
顺序一致性内存模型是一个理论参考模型,在设计的时候,处理器的内存模型和编程语言的内存模型都会以顺序一致性内存模型作为参照。
顺序一致性内存模型要求:
- 线程中所有操作按照程序的顺序进行
- (不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序
不管怎么重排序,(单线程)程序的执行结果不能被改变
-
总线仲裁
总线机制会把所有处理器对内存的访问以串行化方式执行
3个特性
-
原子性
要么做要么不做,举个例子
x = 10; //语句1 y = x; //语句2 x++; //语句3 x = x + 1; //语句4
只有语句1是原子性操作,其他三个都不是
也就是说,简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的互相赋值不是原子操作)才是原子操作,JMM仅保证了基本读取和赋值(除去对long、double的操作)是原子操作,如果要实现更大范围的原子性操作,可以通过锁机制。
-
可见性
java提供volatile关键字保证可见性,普通共享变量不能保证可见性。
通过synchronized或者Lock也可以保证可见性,因为只有一个线程执行同步代码,执行完后会刷新主存。
-
有序性
允许编译器和处理器对指令进行重排序,不影响单线程,但影响并发。
java中,可以通过volatile保证一定的“有序性”(它能禁止指令重排序)。另外,通过synchronized和Lock也可以。
另外,JMM也具有一些先天的“有序性”(不用任何手段),也就是happens-before原则。如果两个操作的执行次序无法从happens-before原则推导出来,则不能保证它们的有序性,虚拟机可以随意对它们进行重排。
JMM定义线程和主寸的抽象关系
线程的共享变量存在主存中,每个线程有一个私有的本地内存,本地内存存储了该线程以读/写共享变量的副本。
t2.png如果线程A与线程B之间要通信的话,必须要经历下面2个步骤。
- 线程A把本地内存A中更新过的共享变量刷新到主内存中去
- 线程B到主内存中去读取线程A之前已更新过的共享变量
指令重排序
执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分3种类型。
-
编译器优化的重排序
编译器在不改变单线程程序语义的前提下,可以重排源代码的执行顺序。多数处理器都允许写-读重排序,即将主存的共享变量都先读了下来,这样导致如果不加其他措施将会使得内部对外部共享变量的修改不可见。
-
指令级并行的重排序
现代处理器采用了指令级并行技术(Instruction-LevelParallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。 -
内存系统的重排序
由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
1为编译器重排序,2和3为处理器重排序。重排序会涉及内存可见性问题。
对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。
对于处理器,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器重排序。
happens-before原则:
happens-before规则对应一个或多个编译器和处理器重排序规则。如果一个操作的执行结果对另一个操作可见,那么这两个操作必须要存在happens-before关系(两个操作可以是位于一个线程或不同线程)
-
程序次序规则:线程内,按照代码顺序,书写在前面的操作先于后者
也就是,指令重排序仅对不存在数据依赖性的指令进行重排序,保证程序在单线程中执行结果的正确性。
-
锁定规则:一个unLock操作线性发生于后面对同一个锁的lock操作(先释放锁才能加锁)
-
volatile变量规则:对一个变量的写操作先于读操作
-
传递规则:若操作A先发生于B,B先发生于C,那么操作A先发生于C
-
线程启动规则:Thread对象的start()先行发生于此线程的每一个动作
-
线程中断规则:对线程interrupt()的调用先行发生于被中断线程的代码检测到中断事件的发生
-
线程终结规则:线程中所有的操作都先行发生于线程的终止检测
-
对象终结规则:一个对象的初始化完成先发生于他的finalize()的开始
未同步程序的执行特性
对于未同步的多线程程序,JVM提供最小的安全性:设置默认值(0,Null,false),因而JVM在分配对象申请空间之后,会先将空间进行清零。
未同步程序整体是无序的,和顺序一致模型有如下差异:
- 不保证单线程内的操作按照程序顺序执行
- 不保证所有线程看到一致的操作执行顺序
- 不保证对64位的long型和double型变量的写操作具有原子性(在JSR-133之前的就内存模型中,对64位的long型和double型变量的读/写操作可以被拆分成两个32位的读/写操作来执行,之后进允许把一个64位的long型和double型变量的写操作可以被拆分成两个32位的写操作来执行,任意读操作都必须具有原子性)
参考:
《java并发编程的艺术》
网友评论