JVM并发机制的探讨——内存模型、内存可见性和指令重排序

原文出处:  oschina

并发原本就是个有意思的问题,尤为是如今又流行这么一句话:“高帅富加机器,穷矮搓搞优化”。从这句话能够看到,不管是高帅富仍是穷矮搓都须要深刻理解并发编程,高帅富加多了机器,须要协调多台机器或者多个CPU对共享资源的访问,所以须要了解并发,穷矮搓搞优化须要编写各类多线程的代码来压榨CPU的计算资源,让它在同一时刻作更多的事情,这个更须要了解并发。html

在我前一篇关于并发的文章http://my.oschina.net/chihz/blog/54731中提到过管程,管程的特点是在编程语言中对并发的细节进行封装,使程序员能够直接在语言中就获得并发的支持,而没必要本身去处理一些像是控制信号量之类容易出错且繁琐的细节问题。一些语言是经过在编译时解开语法糖的方式去实现管程,但Java在编译后生成的字节码层面上对并发仍然是一层封装,好比syncrhonized块在编译以后只是对应了两条指令:monitorenter和monitorexit。更多的并发细节是在JVM运行时去处理的,而不是编译。这篇文章主要是针对JVM处理并发的一些细节的探讨。java

JAVA内存模型

对于咱们平时开发的业务应用来讲,内存应该是访问速度最快的存储设备,对于频繁访问的数据,咱们老是习惯把它们放到内存缓存中,有句话不是说么,缓存就像是清凉油,哪里有问题就抹一抹。可是CPU的运算速度比起内存的访问速度还要快几个量级,为了平衡这个差距,因而就专门为CPU引入了高速缓存,频繁使用的数据放到高速缓存当中,CPU在使用这些数据进行运算的时候就没必要再去访问内存。可是在多CPU时代却有一个问题,每一个CPU都拥有本身的高速缓存,内存又是全部CPU共享的公共资源,因而内存此时就成了一个临界区,若是控制很差各个CPU对内存的并发访问,那么就会产生错误,出现数据不一致的状况。为了不这种状况,须要采起缓存一致性协议来保证,这类协议有不少,各个硬件平台和操做系统的实现不尽相同。
 
JVM须要实现跨平台的支持,它须要有一套本身的同步协议来屏蔽掉各类底层硬件和操做系统的不一样,所以就引入了Java内存模型。对于Java来讲开发者并不须要关心任何硬件细节,所以没有多核CPU和高速缓存的概念,多核CPU和高速缓存在JVM中对应的是Java语言内置的线程和每一个线程所拥有的独立内存空间,Java内存模型所规范的也就是数据在线程本身的独立内存空间和JVM共享内存之间同步的问题。下面这两张图说明了硬件平台和JVM内存模型的类似和差别之处。
 
硬件平台

 
JVM内存模型
 

Java内存模型规定,对于多个线程共享的变量,存储在主内存当中,每一个线程都有本身独立的工做内存,线程只能访问本身的工做内存,不能够访问其它线程的工做内存。工做内存中保存了主内存共享变量的副本,线程要操做这些共享变量,只能经过操做工做内存中的副原本实现,操做完毕以后再同步回到主内存当中。如何保证多个线程操做主内存的数据完整性是一个难题,Java内存模型也规定了工做内存与主内存之间交互的协议,首先是定义了8种原子操做:
(1) lock:将主内存中的变量锁定,为一个线程所独占
(2) unclock:将lock加的锁定解除,此时其它的线程能够有机会访问此变量
(3) read:将主内存中的变量值读到工做内存当中
(4) load:将read读取的值保存到工做内存中的变量副本中。
(5) use:将值传递给线程的代码执行引擎
(6) assign:将执行引擎处理返回的值从新赋值给变量副本
(7) store:将变量副本的值存储到主内存中。
(8) write:将store存储的值写入到主内存的共享变量当中。
 
咱们能够看到,要保证数据的同步,lock和unlock定义了一个线程访问一次共享内存的界限,有lock操做也必须有unlock操做,另一些操做也必需要成对出现才能够,像是read和load、store和write须要成对出现,若是单一指令出现,那么就会形成数据不一致的问题。Java内存模型也针对这些操做指定了必须知足的规则:
(1) read和load、store和write必需要成对出现,不容许单一的操做,不然会形成从主内存读取的值,工做内存不接受或者工做内存发起的写入操做而主内存没法接受的现象。
(2) 在线程中使用了assign操做改变了变量副本,那么就必须把这个副本经过store-write同步回主内存中。若是线程中没有发生assign操做,那么也不容许使用store-write同步到主内存。
(3) 在对一个变量实行use和store操做以前,必须实行过load和assign操做。
(4) 变量在同一时刻只容许一个线程对其进行lock,有多少次lock操做,就必须有多少次unlock操做。在lock操做以后会清空此变量在工做内存中原先的副本,须要再次从主内存read-load新的值。在执行unlock操做前,须要把改变的副本同步回主存。
 

内存可见性

经过上面Java内存模型的概述,咱们会注意到这么一个问题,每一个线程在获取锁以后会在本身的工做内存来操做共享变量,在工做内存中的副本回写到主内存,而且其它线程从主内存将变量同步回本身的工做内存以前,共享变量的改变对其它线程是不可见的。那么不少时候咱们须要一个线程对共享变量的改动,其它线程也须要当即得知这个改动该怎么办呢?好比如下的情景,有一个全局的状态变量open:
1
boolean open= true;
这个变量用来描述对一个资源的打开关闭状态,true表示打开,false表示关闭,假设有一个线程A,在执行一些操做后将open修改成false:
 
1
2
3
4
//线程A
 
resource.close();
open = false;

 

线程B随时关注open的状态,当open为true的时候经过访问资源来进行一些操做:
 
1
2
3
4
5
//线程B
 
while(open) {
         doSomethingWithResource(resource);
}

 

当A把资源关闭的时候,open变量对线程B不可见,若是此时open变量的改动还没有同步到线程B的工做内存中,那么线程B就会用一个已经关闭了的资源去作一些操做,所以产生错误。
 
因此对于上面的情景,要求一个线程对open的改变,其余的线程可以当即可见,Java为此提供了volatile关键字,在声明open变量的时候加入volatile关键字就能够保证open的内存可见性,即open的改变对全部的线程都是当即可见的。volatile保证可见性的原理是在每次访问变量时都会进行一次刷新,所以每次访问都是主内存中最新的版本。
 

指令重排序

不少介绍JVM并发的书或文章都会谈到JVM为了优化性能,采用了指令重排序,可是对于什么是指令重排序,为何重排序会优化性能却不多有说起,其实道理很简单,假设有这么两个共享变量a和b:
 
1
2
private int a;
private int b;

 

在线程A中有两条语句对这两个共享变量进行赋值操做:
1
2
a = 1;
b = 2;

 

假设当线程A对a进行复制操做的时候发现这个变量在主内存已经被其它的线程加了访问锁,那么此时线程A怎么办?等待释放锁?不,等待太浪费时间了,它会去尝试进行b的赋值操做,b这时候没被人占用,所以就会先为b赋值,再去为a赋值,那么执行的顺序就变成了:
1
2
b = 2;
a = 1;

 

对于在同一个线程内,这样的改变是不会对逻辑产生影响的,可是在多线程的状况下指令重排序会带来问题,看下面这个情景:
在线程A中:
1
2
context = loadContext();
inited = true;
在线程B中:
 

1
2
3
4
while(!inited ){
     sleep
}
doSomethingwithconfig(context);
假设A中发生了重排序:
 

1
2
inited = true;
context = loadContext();
那么B中极可能就会拿到一个还没有初始化或还没有初始化完成的context,从而引起程序错误。
 
想到有一条古老的原则很适合用在这个地方,那就是先要保证程序的正确而后再去优化性能。此处因为重排序产生的错误显然要比重排序带来的性能优化要重要的多。要解决重排序问题仍是经过volatile关键字,volatile关键字能确保变量在线程中的操做不会被重排序而是按照代码中规定的顺序进行访问。
 

最后的总结

这篇文章简单的介绍了Java内存模型、内存可见性和指令重排序。不过最后看来其实主要是在解释volatile这个关键字,我的感受volatile关键字是Java当中最使人困惑和最难理解的关键字。相对于synchronized块的代码锁,volatile应该是提供了一个轻量级的针对共享变量的锁,当咱们在多个线程间使用共享变量进行通讯的时候须要考虑将共享变量用volatile来修饰,对于须要使用volatile的各类情景,看到IBM Developer Works上有一篇文章总结的很不错,推荐一下: http://www.ibm.com/developerworks/cn/java/j-jtp06197.html程序员

 

补充说明:64位long和double

在JVM规范中Java内存模型要求lock、unlock、read、load、assign、use、store、write这8个操做必须是原子的,可是对于64位的long和double来讲,若是没有被volatile修饰符修饰,那么能够不是原子的,注意是能够,即虚拟机在实现的时候能够选择是不是原子操做。目前几乎全部的商用虚拟机都将此实现为原子操做,所以没必要每次用到它们都去加volatile修饰。
相关文章
相关标签/搜索