概念
Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。
在并发编程中,需要处理两个关键问题:线程直接如何通信和同步。
- 通信:是指线程之间以何种机制来交换信息;在命令式编程中,通信机制有两种,共享内存和消息传递。
- 同步:是指程序用于控制不同线程之间操作发生相对顺序的机制。
Java的并发采用的是共享内存模型,线程之间的通信总是隐式的进行。Java线程之间的通信由内存模型(JMM)控制,JMM决定了一个线程对共享变量的写入何时对另一个线程可见。
共享变量指实例域、静态域和数组元素
所有的变量都存储在主内存中,每个线程还有自己的工作(本地)内存,工作内存存储在高速缓存或者寄存器中,保存了该线程使用的变量的主内存副本拷贝。
线程只能直接操作工作内存中的变量,不同线程之间的变量值传递需要通过主内存来完成。
Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作。
- read:把一个变量的值从主内存传输到工作内存中
- load:在 read 之后执行,把 read 得到的值放入工作内存的变量副本中
- use:把工作内存中一个变量的值传递给执行引擎
- assign:把一个从执行引擎接收到的值赋给工作内存的变量
- store:把工作内存的一个变量的值传送到主内存中
- write:在 store 之后执行,把 store 得到的值放入主内存的变量中
- lock:作用于主内存的变量
- unlock
重排序
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段
如果两个操作访问同一个变量,而且这两个操作中有一个为写操作,此时这两个操作之间存在数据依赖性。例如:
写后读: a=1; b=a;
写后写: a=1; a=2;
读后写: a=b; b=1;
上面3中情况,只要重新排序两个操作的执行顺序,程序的执行结果就会发生变化。
编译器和处理器不会改变存在数据依赖性关系的两个操作的执行结果,会遵守数据依赖性(遵守最终结果是一致的)
double pi = 3.14; //A
double r = 1.0; //B
double area = pi * r * r; //C
A和C存在数据依赖关系,同时B和C存在数据依赖关系。因此C不能重排序到A和B的前面。但是A和B没有数据依赖关系可以重排序
as-if-serial语义的意思是:不管怎么重排序,(单线程)程序的执行结果不能被改变。编译器处理器都必须遵守as-if-serial语义
内存屏障
为了保证内存可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。内存屏障指令分四类:
happens-before规则
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这俩操作之间必须存在happens-before关系。JMM使用一系列HappenBefore的偏序关系的规则方式来说明,要想保证执行操作B的线程看到操作A的结果(无论A和B是否在同一个线程中执行), 那么在A和B之间必须要满足HappenBefore关系,否则JVM可以对它们任意重排序。
happens-before:
- 程序顺序规则:一个线程中的每一个操作,happens-before 于该线程中的任意后续操作。
- 监视器锁规则:对一个监视器的解锁,happens-before 于任意后续对监视器的加锁。
- volatile变量规则:对于一个volatile域的写,happens-before 于任意后续对这个volatile域的读。
- 传递性:如果A happens-before B, 且 B happens-before C ,那么A happens-before C。
- 线程启动规则: Thread 对象的 start() 方法调用先行发生于此线程的每一个动作。
- 线程加入规则: Thread 对象的结束先行发生于 join() 方法返回。
- 线程中断规则: 对线程interrupt()方法的调用先行发生与被中断线程的代码检测。
- 对象终结规则: 一个对象的初始化完成先行发生于它的 finalize() 方法的开始。
两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前.
final
对于final域,编译器和处理器要遵守两个重排序规则:
- 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
- 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
为什么final引用不能从构造方法内”逸出“?
写final域的重排序规则可以确保:在引用变量为任意线程可见之前,该引用变量指向的对象的final域已经在构造函数中被正确初始化过了。其实要得到这个效果,还需要一个保证:在构造函数内部,不能让这个被构造对象的引用为其他线程可见,也就是对象引用不能在构造函数中“逸出”。
总结
JMM是一种规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。
在Java中提供了一系列和并发处理相关的关键字,比如volatile、synchronized、final、concurren包等。其实这些就是Java内存模型封装了底层的实现后提供给程序员使用的一些关键字。