Java内存模型-jsr133规范介绍
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
Java内存模型-jsr133规范介绍
最近在看《深⼊理解Java虚拟机:JVM⾼级特性与最佳实践》讲到了线程相关的细节知识,⾥⾯讲述了关于java内存模型,也就是jsr 133定义的规范。
系统的看了jsr 133规范的前⾯⼏个章节的内容,觉得受益匪浅。
废话不说,简要的介绍⼀下java内存规范。
什么是内存规范
在jsr-133中是这么定义的
A memory model describes, given a program and an execution trace of that program, whether
the execution trace is a legal execution of the program. For the Java programming language, the
memory model works by examining each read in an execution trace and checking that the write
observed by that read is valid according to certain rules.
也就是说⼀个内存模型描述了⼀个给定的程序和和它的执⾏路径是否⼀个合法的执⾏路径。
对于java序⾔来说,内存模型通过考察在程序执⾏路径中每⼀个读操作,根据特定的规则,检查写操作对应的读操作是否能是有效的。
java内存模型只是定义了⼀个规范,具体的实现可以是根据实际情况⾃由实现的。
但是实现要满⾜java内存模型定义的规范。
处理器和内存的交互
这个要感谢硅⼯业的发展,导致⽬前处理器的性能越来越强⼤。
⽬前市场上基本上都是多核处理器。
如何利⽤多核处理器执⾏程序的优势,使得程序性能得到极⼤的提升,是⽬前来说最重要的。
⽬前所有的运算都是处理器来执⾏的,我们在⼤学的时候就学习过⼀个基本概念程序 = 数据 + 算法,那么处理器负责计算,数据从哪⾥获取了?
数据可以存放在处理器寄存器⾥⾯(⽬前x86处理都是基于寄存器架构的),处理器缓存⾥⾯,内存,磁盘,光驱等。
处理器访问这些数据的速度从快到慢依次为:寄存器,处理器缓存,内存,磁盘,光驱。
为了加快程序运⾏速度,数据离处理器越近越好。
但是寄存器,处理器缓存都是处理器私有数据,只有内存,磁盘,光驱才是才是所有处理器都可以访问的全局数据(磁盘和光驱我们这⾥不讨论,只讨论内存)如果程序是多线程的,那么不同的线程可能分配到不同的处理器来执⾏,这些处理器需要把数据从主内存加载到处理器缓存和寄存器⾥⾯才可以执⾏(这个⼤学操作系统概念⾥⾯有介绍),数据执⾏完成之后,在把执⾏结果同步到主内存。
如果这些数据是所有线程共享的,那么就会发⽣同步问题。
处理器需要解决何时同步主内存数据,以及处理执⾏结果何时同步到主内存,因为同⼀个处理器可能会先把数据放在处理器缓存⾥⾯,以便程序后续继续对数据进⾏操作。
所以对于内存数据,由于多处理器的情况,会变的很复杂。
下⾯是⼀个例⼦:
初始值 a = b = 0
process1 process2
1:load a 5:load b
2:write a:2 6:add b:1
3:load b 7: load a
4:write b:1 8:write a:1
假设处理器1先加载内存变量a,写⼊a的值为2,然后加载b,写⼊b的值为1,同时处理2先加载b,执⾏b+1,那么b在处理器2的结果可能是1可能是3。
因为在load b之前,不知道处理器1是否已经吧b写会到主内存。
对于a来说,假设处理器1后于处理器2把a写会到主内存,那么a的值则为2。
⽽内存模型就是规定了⼀个规则,处理器如何同主内存同步数据的⼀个规则。
内存模型介绍
在介绍java内存模型之前,我们先看看两个内存模型
Sequential Consistency Memory Model:连续⼀致性模型。
这个模型定义了程序执⾏的顺序和代码执⾏的顺序是⼀致的。
也就是说如果两个线程,⼀个线程T1对共享变量A进⾏写操作,另外⼀个线程T2对A进⾏读操作。
如果线程T1在时间上先于T2执⾏,那么T2就可以看见T1修改之后的值。
这个内存模型⽐较简单,也⽐较直观,⽐较符合现实世界的逻辑。
但是这个模型定义⽐较严格,在多处理器并发执⾏程序的时候,会严重的影响程序的性能。
因为每次对共享变量的修改都要⽴刻同步会主内存,不能把变量保存到处理器寄存器⾥⾯或者处理器缓存⾥⾯。
导致频繁的读写内存影响性能。
Happens-Before Memory Model :先⾏发⽣模型。
这个模型理解起来就⽐较困难。
先介绍⼀个现⾏发⽣关系(Happens-Before Relationship)
如果有两个操作A和B存在A Happens-Before B,那么操作A对变量的修改对操作B来说是可见的。
这个现⾏并不是代码执⾏时间上的先后关系,⽽是保证执⾏结果是顺序的。
看下⾯例⼦来说明现⾏发⽣
A,B为共享变量,r1,r2为局部变量
初始 A=B=0
Thread1 | Thread2
1: r2=A | 3: r1=B
2: B=2 | 4: A=2
凭借直观感觉,线程1先执⾏ r2=A,则r2=0 ,然后赋值B=1,线程2执⾏r1=B,由于线程1修改了B的值为1,所以r1=1。
但是在现⾏发⽣内存模型⾥⾯,有可能最终结果为r1 = r2 = 2。
为什么会这样,因为编译器或者多处理器可能对指令进⾏乱序执⾏,线程1 从代码流上⾯看是先执⾏r2 = A,B = 1,但是处理器执⾏的时候会先执⾏ B = 2 ,在执⾏ r2 = A,线程2 可能先执⾏ A = 2 ,在执⾏r1 = B,这样可能会导致r1 = r2 = 2。
那我们先看看先⾏发⽣关系的规则
1 在同⼀个线程⾥⾯,按照代码执⾏的顺序(也就是代码语义的顺序),前⼀个操作先于后⾯⼀个操作发⽣
2 对⼀个monitor对象的解锁操作先于后续对同⼀个monitor对象的锁操作
3 对volatile字段的写操作先于后⾯的对此字段的读操作
4 对线程的start操作(调⽤线程对象的start()⽅法)先于这个线程的其他任何操作
5 ⼀个线程中所有的操作先于其他任何线程在此线程上调⽤ join()⽅法
6 如果A操作优先于B,B操作优先于C,那么A操作优先于C
解释⼀下以上⼏个先⾏发⽣规则的含义
规则1应该⽐较好理解,因为⽐较适合⼈正常的思维。
⽐如在同⼀个线程t⾥⾯,代码的顺序如下:
thread 1
共享变量A、B
局部变量r1、r2
代码顺序
1: A =1
2: r1 = A
3: B = 2
4: r2 = B
执⾏结果就是 A=1 ,B=2 ,r1=1 ,r2=2
因为以上是在同⼀个线程⾥⾯,按照规则1 也就是按照代码顺序,A = 1 先⾏发⽣ r1 =A ,那么r1 = 1
再看规则2,下⾯是jsr133的例⼦
按照规则2,由于unlock操作先于发⽣于lock操作,所以X=1对线程2⾥⾯就是可见的,所以r2 = 1
在分析以下,看这个例⼦,由于unlock操作先于lock操作,所以线程x=1对于线程2不⼀定是可见(不⼀定是现⾏发⽣的),所以r2的值不⼀定是1,有可能是x赋值为1之前的那个状态值(假设x初始值为0,那么此时r2的值可能为0)
对于规则3,我们可以稍微修改⼀下我们说明的第⼀个例⼦
A,B为共享变量,并且B是valotile类型的
r1,r2为局部变量
初始 A=B=0
Thread1 | Thread2
1: r2=A | 3: r1=B
2: B=2 | 4: A=2
那么r1 = 2, r2可能为0或者2
因为对于volatile类型的变量B,线程1对B的更新马上线程2就是可见的,所以r1的值就是确定的。
由于A是⾮valotile类型的,所以值不确定。
规则4,5,6这⾥就不解释了,知道规则就可以了。
可以从以上的看出,先⾏发⽣的规则有很⼤的灵活性,编译器可以对指令进⾏重新排序,以便满⾜处理器性能的需要。
只要重新排序之后的结果,在单⼀线程⾥⾯执⾏结果是可见的(也就是在同⼀个线程⾥⾯满⾜先⾏发⽣原则1就可以了)。
java内存模型是建⽴在先⾏发⽣的内存模型之上的,并且再此基础上,增强了⼀些。
因为现⾏发⽣是⼀个弱约束的内存模型,在多线程竞争访问共享数据的时候,会导致不可预期的结果。
有⼀些是java内存模型可以接受的,有⼀些是java内存模型不可以接受的。
具体细节这⾥⾯就不详细说明了。
这⾥只说明关于java新的内存模型重要点。
final字段的语义
在java⾥⾯,如果⼀个类定义了⼀个final属性,那么这个属性在初始化之后就不可以在改变。
⼀般认为final字段是不变的。
在java内存模型⾥⾯,对final有⼀个特殊的处理。
如果⼀个类C定义了⼀个⾮static的final属性A,以及⾮static final属性B,在C的构造器⾥⾯对A,B进⾏初始化,如果⼀个线程T1创建了类C的⼀个对象co,同⼀时刻线程T2访问co对象的A和B属性,如果t2获取到已经构造完成的co对象,那么属性A的值是可以确定的,属性B的值可能还未初始化,
下⾯⼀段代码演⽰了这个情况
public class FinalVarClass {
public final int a ;
public int b = 0;
static FinalVarClass co;
public FinalVarClass(){
a = 1;
b = 1;
}
//线程1创建FinalVarClass对象 co
public static void create(){
if(co == null){
co = new FinalVarClass();
}
}
//线程2访问co对象的a,b属性
public static void vistor(){
if(co != null){
System.out.println(co.a);//这⾥返回的⼀定是1,a⼀定初始化完成
System.out.println(co.b);//这⾥返回的可能是0,因为b还未初始化完成
}
}
}
为什么会发⽣这种情况,原因可能是处理器对创建对象的指令进⾏重新排序。
正常情况下,对象创建语句co = new FinalVarClass()并不是原⼦的,简单来说,可以分为⼏个步骤,1 分配内存空间 2 创建空的对象 3 初始化空的对象 4 把初始化完成的对象引⽤指向 co ,由于这⼏个步骤处理器可能并发执⾏,⽐如3,4 并发执⾏,所以在create操作完成之后,co不⼀定马上初始化完成,所以在vistor⽅法的时候,b的值可能还未初始化。
但是如果是final字段,必须保证在对应返回引⽤之前初始化完成。
volatile语义
对于volatile字段,在现⾏发⽣规则⾥⾯已经介绍过,对volatile变量的写操作先于对变量的读操作。
也就是说任何对volatile变量的修改,都可以在其他线程⾥⾯反应出来。
对于volatile变量的介绍可以参考本⼈写的⼀篇⽂章《》⾥⾯有详细的介绍。
volatile在java新的内存规范⾥⾯还加强了新的语义。
在⽼的内存规范⾥⾯,volatile变量与⾮volatile变量的顺序是可以重新排序的。
举个例⼦
public class VolatileClass {
int x = 0;
volatile boolean v = false;
//线程1write
public void writer() {
x = 42;
v = true;
}
//线程2 read
public void reader() {
if (v == true) {
System.out.println(x);//结果可能为0,可能为2
}
}
}
线程1先调⽤writer⽅法,对x和v进⾏写操作,线程reader判断,如果v=true,则打印x。
在⽼的内存规范⾥⾯,可能对v和x赋值顺序发⽣改变,导致v的写操作先⾏于x的写操作执⾏,同时另外⼀个线程判断v的结果,由于v的写操作先⾏于v的读操作,所以if(v==true)返回真,于是程序执⾏打印x,此时x不⼀定先⾏与System.out.println指令之前。
所以显⽰的结果可能为0,不⼀定为2
但是java新的内存模型jsr133修正了这个问题,对于volatile语义的变量,⾃动进⾏lock 和 unlock操作包围对变量volatile的读写操作。
那么以上语句的顺序可以表⽰为
thread1 thread2
1 :write x=1 5:lock(m)
2 :lock(m) 6:read v
3 :write v=true 7:unlock(m)
4 :unlock 8 :if(v==true)
9: System.out.print(x)
由于unlock操作先于lock操作,所以x写操作5先于发⽣x的读操作9。