JMM基础
参考书:《Java并发编程艺术》
附图均来自:《Java并发编程艺术》
先了解一下基础的概念:什么是JMM?
JMM(全称:Java Memory Model), JMM定义的是线程和主内存间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本,JMM控制一个线程的共享变量写入何时对另一个线程可见。本地内存是JMM的一个抽象概念,并不真实存在。(有个印象,下面具体介绍)
在学习JMM之前,其实要先保留两个问题。1.那些数据需要JMM控制?2.JMM是怎么进行干预线程的操作的?
源代码到可执行的变成指令序列(重排序)
首先,我们要知道的是,我们所写的代码,并不是原封不动的被转换成可执行的序列。为了提高性能,编译器和处理器经常会进行 重排序;
编译重排序的过程
JMM的编译器会对特定类型的编译器进行限制。 所以,首先要了解编译器的重排序处理。
不同处理器的编译器有所不同,但是都遵循以下规则:
1.数据的依赖性
含义:当两个操作访问一个变量,且两个操作中一个是写操作,这两个操作存在数据的依赖性。
这两个操作,可能是:1.先读后写;2.先写后写;3.先写后读
一般存在数据依赖性的数据操作,编译器和处理器不会重新排序两个操作的执行顺序(只针对单处理器);
2.as_if_serial语义
含义:不管怎么重排序,单线程的执行结果不能被改变。因此不会对存在数据以来的操作进行重排序;
上面的这些,我们可以看出,在单线程的情况下,根据数据的依赖性,数据执行结果不能被发生改变。
但是我们实际开发中经常会遇到多线程的情况,如下:
class Test{
int a = 0 ;
boolean flag = false;
public void wirte(){
a ++ ;
flag = true;
}
public int read(){
if(flag){
return ++a;
}
}
}
上面的这个类,例如有两个线程同时执行write方法和read方法,因为就单个方法来所编译器的出现重排符合数据依赖性,但是实际多线程访问会出现数据异常查询和写入的问题。
像上面的这种多线程操作同一个变量,且读和写操作不在同一个线程中时,就要通过JMM进行编译器约束了。
了解下JMM的原则
在开始了解JMM编译重排序之前,我们先了解一个模型:顺序一致性模型;
顺序一致性模型
顺序一致性内存模型是一个理论参考模型,处理器内存模型和编程内存模型以顺序一致性为参考。
顺序一致性特征:
1.一个线程的所有操作,必须按照程序的顺序来执行;
2.所有的线程都只能看到单一的操作执行顺序。在顺序一致性模型中,每个操作必须原子执行且立刻对所有线程可见。
happens-before(JMM顺序执行的原则)
happens-before是JMM最核心的概念,也是JMM顺序执行的要遵循的原则。
定义:
1.一个操作happens-before第二个操作,那么第一个操作执行顺序在第二个操作之前,并且第一个执行结果对第二个操作可见。
2.只描述两个操作的关系,java平台不一定按照这种关系执行。
happens-before的几种规则
1、程序顺序规则:一个线程中的每个操作,happens-before于该线程中任意的后续操作。
2、监视器锁规则:对一个锁的解锁操作,happens-before于随后对这个锁的加锁操作。
3、volatile域规则:对一个volatile域的写操作,happens-before于任意线程后续对这个volatile域的读。
4、传递性规则:如果 A happens-before B,且 B happens-before C,那么A happens-before C。
JMM控制
现在可以回答上面预留的两个问题的问题了。
1.那些数据需要JMM控制?
-
方法区的实例,域都是线程共享的;这些是不会涉及到重排序的问题,所以不是JMM管理的范围
-
局部变量的线程是私有的,才涉及到线程之间的通信;
2.JMM是怎么进行干预线程的操作的?
这就是我们接下来要了解的知识,内存屏障(barriers)和临界区
然鹅JMM并不是禁止所有重排序,对于不会改变程序运行结果的重排序,JMM不做要求;
内存屏障(阻止编译器重排序)
内存屏障的类型
-
volatile变量就是通过屏障阻止编译重排序的;
-
final修饰的变量也是通过屏障进行编译重排序的;
volatile变量
1.volatile重排序的规则表
2.举个栗子(来自《Java并发编程艺术》)
class VolatileBarrierExample {
int a;
volatile int v1 = 1;
volatile int v2 = 2;
void readAndWrite() {
int i = v1;
}
临界区(临界区内可以重排序,但是临界区内的代码不能“逸出”到临界区外)
显式的通过给代码增加锁,创建临界区;当线程释放锁的时候,JMM会把本地内存中的数据刷新到主内存中。
指令执行中,是通过#Lock前缀指令,指令在执行期间会锁住。
先看一个代码
class Test{
int a = 0 ;
boolean flag = false;
public synchronized void wirte(){
a ++ ;
flag = true;
}
public synchronized int read(){
if(flag){
return ++a;
}
}
}