Java内存模型及Java关键字 volatile的做用和使用说明

先来看看这个关键字是什么意思:
volatile  [ˈvɒlətaɪl]
adj. 易变的,不稳定的;
从翻译上来看,volatile表示这个关键字是极易发生改变的。
volatile是java语言中,最轻量级的并发同步机制。这个关键字有以下两个做用:
一、任何对volatile变量的修改,java中的其余线程均可以感知到
二、volatile会禁止指令冲排序优化
  在详细讲解volatile关键字以前,须要对java的内存模型有所理解,不然很难深刻的认识到volatile的做用。java 内存能够像以前讲的那样,划分为堆、栈、方法区等等。可是从结合物理设备的角度来看,内存模型的布局设计以下:java

  之因此这样设计内存模型,是由于:相对于cpu的处理速度来讲,物理内存的IO操做耗时很是严重。这就形成了cpu线程快速计算结束后,须要浪费大量的时间来等待内存IO的操做。为了减小这种等待,java内存模型引入了工做内存的概念。工做内存主要是利用cpu或内存的寄存器、高速缓存等部分进行数据缓冲,减小cpu线程在内存IO期间的等待。
在java内存模型中,线程任何与数据有关的操做,都与而且只与工做内存相关。当线程需(防盗链接:本文首发自http://www.cnblogs.com/jilodream/ )要操做数据时,虚拟机会首先从主内存中读取数据,而后放置一份拷贝的数据到工做内存中。接着java线程读取工做内存中的拷贝数据,并操做获得一个全新的数据,然将将这个数据放回到工做内存中,覆盖原有的值。
  这样作能够充分利用物理硬件的优点:
  (1)主内存,存储区域大,可是速度不行,适于存储,不适于快速读写
  (2)工做内存、存储空间小,可是速度快,适于快速读写,不适于存储
同时还避免了Java线程读写主内存中数据同步问题。由于主内存对于各个Java线程都是可见的。若是java线程并发操做,就会致使主内存中的数据须要进行同步保护,不然就会出现错误的语义。
  可是这样作仍然会有一个问题:工做内存中的数据是拷贝数据。在Java线程操做的过程当中,主内存中的数据可能已经发生改变,Java线程至关因而在用过期的值在计算和回写。这个问题就是数据称之为“同步”的含义所在,也是锁要处理的可见性的问题(之后有文章我会专门讲这个问题)。
如何解决这个问题呢?
  只能是经过“锁”的形式来处理。volatile关键字的做用之一,就是造成这样一个“锁”:
  若是一个变量被定义了volatile,那么每次Java线程在写入这个变量时,都会加入一个“lock addl $Ox0"的操做指令。这样会造成一个“内存屏障”,当cpu将这条指令写入到主内存时,会告诉其余存有这份指令的工做内存加一个标识。表示这个变量已经发生了变化,当前工做内存中存储的拷贝数据已通过时(这个过程被称之为内核CacheInvalidate)。当其余线程须要使用该变量来操做时,系统会由于这个标识断定当前工做内存中的数据已通过时。从而主动刷新主内存中的值到本身下边的工做内存中。因为在整个过程当中,系统已经在线程操做数据以前,提早刷新了变量的值,因此线程没法看到已通过时的数据的。所以从表现上来看,能够认为是不存在数据不一致的问题。
  这里须要专门强调下long、double型。对于内存模型中定义的指令来讲,操做的数据都是32位的。若是数据是64位,那么就须要两次指令操做。对于虚拟机中64位数据类型:double、long型,就会由于须要两次操做的时间差,致使其余线程拿到的是一种修改的中间值。
  可是volatile的内存屏障专门对这里进行了处理,以保证这种中间值不会出如今其余cpu的工做内存中。同时目前商业的虚拟机已经都对这个问题专门进行了处理:对64位数据的读写也采用原子操做。为的就是防止long double这两个经常使用类型,因为没有增长volatile关键字,而致使在工做内存中出现奇怪的值。
  volatile的另一个做用是禁止指令重排序的优化
  cpu线程在执行指令的过程当中,为了保证速度更快,指令之间的顺序每每是经过优化重排序之后的顺序。为了保证重排序的指令不会有任何的歧义而仅仅是在速度上有所提高,系统会保证指令优化之后执行的结果是一致的。也就是你所得到的结果与没优化得到到的结果是同样的,不存在差别。可是因为指令顺序发生了变化,因此系统是没法保证这个过程当中,其余的线程获取到的数据是能正确表明当前状态的。这里最经典的就是单例模式下,实例初始化的问题。请参见文章:设计模式之单例模式 的第3个方法。
因为指令重排,系统会在变量没有初始化结束前,就已经给instance变量(防盗链接:本文首发自http://www.cnblogs.com/jilodream/ )赋予地址。这时候其余线程获取到的变量就是有问题的:instance!=null,可是里边的值却没有初始化完成。这里就须要使用volatile关键字禁止指令重排序:只有在实例初始化完毕后,才赋予变量instance引用。
  另一个常见的例子是:
  线程B在刷新线程A的处理结果时,可能因为线程A尚未对变量初始化完毕,却提早刷新了变量,致使了线程B所获取到的变量的状态是错误的。
所以在定义多线程可见变量时,前边必定要加volatile关键字,保证该变量不会被由于指令顺序被优化,而致使其余线程获取到的值是无心义的。
关于Java语言的有序性在《深刻理解Java虚拟机》中有一句话,总结的很是好:若是在本线程内观察,全部的操做都是有序的。若是在一个其它线程观察本线程,则全部的操做都是无序的。
  前边是指,不管虚拟机怎么优化指令,当前线程在执行的语义和结果上都应该是一致的。(“线程内表现为串行的语义"Within-Thread-As-If-Serial-Semantics)。后边是指指令会发生重排,其它线程中获取到的值,不能表明什么。
其实volatile的这两个做用是互相关联的:正是因为volatile须要保证变量的可见性,所以不能将系统无序的中间指令结果反映到主内存中,让其它线程拿去使用可见,因此须要禁止掉指令重排序。保证拿到的结果是反映出当前的执行状态的。(这里涉及到一个happens-before原则的概念,我会在后边的文章中介绍)
volatile存在的问题
  说了volatile的两个做用,volatile也有自身的不足。那就是volatile不能保证原子性:
举个前文讲过的例子,volatile变量值被修改之后,会直接刷新到主内存中,而且其余线程能感知到。可是其余线程继续使用这个变量进行计算时,却不能保证其一直是最新的值。举个经典例子程序员

1 volatile int a=02 int add()
3 {
4     a++;
5 }

  两个线程t1,t2前后执行add)方法,变量a发生了自增。可是a变量的最终结果多是1也多是2。这取决于t2读取变量a的值是在第一个线程刷新a到主内存以前,仍是主内存以后。
a++操做最终在执行时,会执行三条指令:
一、从主内存中读取a值
二、a=a+1
三、写入a的值到主内存中
  当t1执行完第二步时,假如此时t2也读取了a的值,则:主内存a=0;t1工做内存为a=1;t2工做内存为a=0;接下来t1执行回写a操做,可是t2因为已经读取了a的值在工做内存中,所以t2在执行了a++操做后,仍然会回写a=1到主内存中,这时尽管t1回写后,生成内存屏障,可是t2已经读取完毕,不会在自增阶段再主动刷新。(防盗链接:本文首发自http://www.cnblogs.com/jilodream/ )不然若是须要执行连续的多条指令,每次都要主动刷新变量,一旦发生变化就重头开始,这显然是不可能的。这种状况就须要程序员经过代码本身来保证没有问题。
这里咱们能够发现a变量不会由于volatile关键字,而使得自身的指令在外界看来是原子的。
所以volatile的使用存在以下限制场景:
  一、volatile能够写入,可是写入的值不该该依赖旧值
  二、在确认某个状态的不变性时,不能将volatile变量做为因子。
  这两点在《java并发编程实战》、《深刻理解java虚拟机》中都有提到相似的语义。第一点比较容易理解。第二点比较抽象,这里解释一下:就是说volatile适合于判断是否已经改变了,而不适合判断是否还没改变,由于volatile变量发生改变,则必定发生了变化,volatile没有发生变化,则不能说明必定没有发生变化。
如前文,a若是仍然等于0.此时不能认为:一、add方法没有被调用过二、总体没有被改变过。编程

相关文章
相关标签/搜索