volatile关键字想必大家都不陌生,在java 5之前有着挺大的争议,在java 5之后才逐渐被大家接受,同时作为java的关键字之一,其作用自然是不可小觑的,要知道它是java.util.concurrent包的核心,没有volatile就没有这么多的并发类给我们使用。

一、定义:表明两个或者多个变量必须同步地发生变化。(源自百度百科)

二、作用:volatile关键字的作用是保证变量在多线程之间的可见性,同时禁止进行指令重排序,要想真正了解volatile关键字的实现原理,有必要对java的内存模型和cpu的缓存进行一定的了解,所以在文章的一开始,首先介绍一些关于这两方面的知识。

1.java的内存模型和cpu的缓存

CPU缓存的出现主要是为了解决CPU运算速度与内存读写速度不匹配的矛盾,因为CPU运算速度要比内存读写速度快得多,所以导致CPU可能会花费很长时间等待数据到来或把数据写入内存,因此cpu通常会将所需的数据存到缓存中,(缓存分为一级缓存,二级缓存,三级缓存,每一级缓存中所存储的数据全部都是下一级缓存中的一部分,这三种缓存的技术难度和制造成本是相对递减的,所以其容量也相对递增。)读取数据时就可以直接从缓存中读取,不必要每次都要去内存中读取,而缓存中读取数据的速度要远远高于内存,在单核中,这些不存在太大的问题,但是如果到了多核中,就会产生缓存不一致的问题,什么叫缓存不一致呢?

i = i + 1;

当我们执行上面代码时,会从内存中读取i的值,将其存到缓存中,最后执行完毕后再将缓存中的值刷新到内存中,整个流程如果放在单线程中执行,那不会有什么问题,但是若是放在多线程中来执行,那就不一样呢,假设有两个线程来执行上面代码,i的初始值都是0,线程1和线程2分别从内存中读取i的值0,将其存进各自的缓存中,等到线程1执行代码后,缓存中i的值变成1,然后再将缓存中i=1刷新到内存中,此时轮到线程2执行上述代码,和线程1一样,先从缓存中读取i的值,然后进行操作,最后更新到内存中去,可是此时i的值已经是1了,经过线程2后,i的值却根本没有什么变化,依旧是1,这就是一个Bug,那么,又该如何解决这个Bug呢?通常来说有两种解决方法:

1.1 通过在总线加LOCK#锁的方式

1.2 通过缓存一致性协议

在早期的CPU当中,是通过在总线上加LOCK#锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。比如上面例子中 如果一个线程在执行 i = i +1,如果在执行这段代码的过程中,在总线上发出了LCOK#锁的信号,那么只有等待这段代码完全执行完毕之后,其他CPU才能从变量i所在的内存读取变量,然后进行相应的操作。这样就解决了缓存不一致的问题。

但是上面的方式会有一个问题,由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。

所以就出现了缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

2.在谈及volatile关键字的作用时,提到了两个概念,一个是可见性,一个是指令重排序,有必要对这两个概念进行单独的说明。

2.1 可见性:是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

//线程1执行的代码
int i = 0;
i = 10; //线程2执行的代码
j = i;

假若执行线程1的是CPU1,执行线程2的是CPU2。当线程1执行 i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中。此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10。这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。

对于可见性,Java提供了volatile关键字来保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

2.2 指令重排序:是指处理器为了提高程序运行效率,可能会对代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。volatile关键字能禁止指令重排序,也就是说volatile能在一定程度上保证有序性。那么,有序性又是什么含义呢?

2.2.1 有序性:即程序执行的顺序按照代码的先后顺序执行。

int i = 0;
boolean flag = false;
i = 1; //语句1
flag = true; //语句2

上面代码定义了一个int型变量,定义了一个boolean类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗?不一定,为什么呢?这里可能会发生指令重排序(Instruction Reorder)。比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢?再看下面一个例子:

int a = 10;    //语句1
int r = 2; //语句2
a = a + 3; //语句3
r = a*a; //语句4

上面代码执行的顺序是多样的,可能是语句1,语句2,语句3,语句4;也有可能是语句2,语句1,语句3,语句4。那么,上述代码的执行顺序有没有可能是语句3在前面执行呢?不可能,因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。

在单线程中,重排序问题不会影响程序的最终运行结果,但是若是在多线程中,则会有可能产生比较严重的错误。

//线程1:
context = loadContext(); //语句1
inited = true; //语句2 //线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);

当有两个线程执行上述代码的时候,由于语句1和语句2之间不具有数据依赖性,所以就有可能出现重排序,如果发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。

在Java里面,可以通过volatile关键字来保证一定的有序性。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

下面就来具体介绍下happens-before原则(先行发生原则):

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  • 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

这8条原则摘自《深入理解Java虚拟机》。

这8条规则中,前4条规则是比较重要的,后4条规则都是显而易见的。下面我们来解释一下前4条规则:

对于程序次序规则来说,我的理解就是一段程序代码的执行在单个线程中看起来是有序的。注意,虽然这条规则中提到“书写在前面的操作先行发生于书写在后面的操作”,这个应该是程序看起来执行的顺序是按照代码顺序执行的,因为虚拟机可能会对程序代码进行指令重排序。虽然进行重排序,但是最终执行的结果是与程序顺序执行的结果一致的,它只会对不存在数据依赖性的指令进行重排序。因此,在单个线程中,程序执行看起来是有序执行的,这一点要注意理解。事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确性。

第二条规则也比较容易理解,也就是说无论在单线程中还是多线程中,同一个锁如果出于被锁定的状态,那么必须先对锁进行了释放操作,后面才能继续进行lock操作。

第三条规则是一条比较重要的规则,也是后文将要重点讲述的内容。直观地解释就是,如果一个线程先去写一个变量,然后一个线程去进行读取,那么写入操作肯定会先行发生于读操作。

第四条规则实际上就是体现happens-before原则具备传递性。

3.volatile原理和实现机制

前面讲述了源于volatile关键字的一些使用,下面我们来探讨一下volatile到底如何保证可见性和禁止指令重排序的。

下面这段话摘自《深入理解Java虚拟机》:

“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

  2)它会强制将对缓存的修改操作立即写入主存;

  3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

我们可以用下面的这个例子来说明一下:

public class LazySingleton {

    private static volatile LazySingleton instance = null;

    public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
} return instance;
} public static void main(String[] args) {
LazySingleton.getInstance();
} }

按照上文所说,我们将上述的代码转变为汇编语言

Java HotSpot(TM) 64-Bit Server VM warning: PrintAssembly is enabled; turning on DebugNonSafepoints to gain additional output
CompilerOracle: compileonly *LazySingleton.getInstance
Loaded disassembler from D:\JDK\jre\bin\server\hsdis-amd64.dll
Decoding compiled method 0x0000000002931150:
Code:
Argument 0 is unknown.RIP: 0x29312a0 Code size: 0x00000108
[Disassembling for mach='amd64']
[Entry Point]
[Verified Entry Point]
[Constants]
# {method} 'getInstance' '()Lorg/xrq/test/design/singleton/LazySingleton;' in 'org/xrq/test/design/singleton/LazySingleton'
# [sp+0x20] (sp of caller)
0x00000000029312a0: mov dword ptr [rsp+0ffffffffffffa000h],eax
0x00000000029312a7: push rbp
0x00000000029312a8: sub rsp,10h ;*synchronization entry
; - org.xrq.test.design.singleton.LazySingleton::getInstance@-1 (line 13)
0x00000000029312ac: mov r10,7ada9e428h ; {oop(a 'java/lang/Class' = 'org/xrq/test/design/singleton/LazySingleton')}
0x00000000029312b6: mov r11d,dword ptr [r10+58h]
;*getstatic instance
; - org.xrq.test.design.singleton.LazySingleton::getInstance@0 (line 13)
0x00000000029312ba: test r11d,r11d
0x00000000029312bd: je 29312e0h
0x00000000029312bf: mov r10,7ada9e428h ; {oop(a 'java/lang/Class' = 'org/xrq/test/design/singleton/LazySingleton')}
0x00000000029312c9: mov r11d,dword ptr [r10+58h]
0x00000000029312cd: mov rax,r11
0x00000000029312d0: shl rax,3h ;*getstatic instance
; - org.xrq.test.design.singleton.LazySingleton::getInstance@16 (line 17)
0x00000000029312d4: add rsp,10h
0x00000000029312d8: pop rbp
0x00000000029312d9: test dword ptr [330000h],eax ; {poll_return}
0x00000000029312df: ret
0x00000000029312e0: mov rax,qword ptr [r15+60h]
0x00000000029312e4: mov r10,rax
0x00000000029312e7: add r10,10h
0x00000000029312eb: cmp r10,qword ptr [r15+70h]
0x00000000029312ef: jnb 293135bh
0x00000000029312f1: mov qword ptr [r15+60h],r10
0x00000000029312f5: prefetchnta byte ptr [r10+0c0h]
0x00000000029312fd: mov r11d,0e07d00b2h ; {oop('org/xrq/test/design/singleton/LazySingleton')}
0x0000000002931303: mov r10,qword ptr [r12+r11*8+0b0h]
0x000000000293130b: mov qword ptr [rax],r10
0x000000000293130e: mov dword ptr [rax+8h],0e07d00b2h
; {oop('org/xrq/test/design/singleton/LazySingleton')}
0x0000000002931315: mov dword ptr [rax+0ch],r12d
0x0000000002931319: mov rbp,rax ;*new ; - org.xrq.test.design.singleton.LazySingleton::getInstance@6 (line 14)
0x000000000293131c: mov rdx,rbp
0x000000000293131f: call 2907c60h ; OopMap{rbp=Oop off=132}
;*invokespecial <init>
; - org.xrq.test.design.singleton.LazySingleton::getInstance@10 (line 14)
; {optimized virtual_call}
0x0000000002931324: mov r10,rbp
0x0000000002931327: shr r10,3h
0x000000000293132b: mov r11,7ada9e428h ; {oop(a 'java/lang/Class' = 'org/xrq/test/design/singleton/LazySingleton')}
0x0000000002931335: mov dword ptr [r11+58h],r10d
0x0000000002931339: mov r10,7ada9e428h ; {oop(a 'java/lang/Class' = 'org/xrq/test/design/singleton/LazySingleton')}
0x0000000002931343: shr r10,9h
0x0000000002931347: mov r11d,20b2000h
0x000000000293134d: mov byte ptr [r11+r10],r12l
0x0000000002931351: lock add dword ptr [rsp],0h ;*putstatic instance
; - org.xrq.test.design.singleton.LazySingleton::getInstance@13 (line 14)
0x0000000002931356: jmp 29312bfh
0x000000000293135b: mov rdx,703e80590h ; {oop('org/xrq/test/design/singleton/LazySingleton')}
0x0000000002931365: nop
0x0000000002931367: call 292fbe0h ; OopMap{off=204}
;*new ; - org.xrq.test.design.singleton.LazySingleton::getInstance@6 (line 14)
; {runtime_call}
0x000000000293136c: jmp 2931319h
0x000000000293136e: mov rdx,rax
0x0000000002931371: jmp 2931376h
0x0000000002931373: mov rdx,rax ;*new ; - org.xrq.test.design.singleton.LazySingleton::getInstance@6 (line 14)
0x0000000002931376: add rsp,10h
0x000000000293137a: pop rbp
0x000000000293137b: jmp 2932b20h ; {runtime_call}
[Stub Code]
0x0000000002931380: mov rbx,0h ; {no_reloc}
0x000000000293138a: jmp 293138ah ; {runtime_call}
[Exception Handler]
0x000000000293138f: jmp 292fca0h ; {runtime_call}
[Deopt Handler Code]
0x0000000002931394: call 2931399h
0x0000000002931399: sub qword ptr [rsp],5h
0x000000000293139e: jmp 2909000h ; {runtime_call}
0x00000000029313a3: hlt
0x00000000029313a4: hlt
0x00000000029313a5: hlt
0x00000000029313a6: hlt
0x00000000029313a7: hlt

仔细的查看一下汇编指令,我们可以找到两行我们需要的

 0x0000000002931351: lock add dword ptr [rsp],0h  ;*putstatic instance
; - org.xrq.test.design.singleton.LazySingleton::getInstance@13 (line 14)

在这里,我们找到了volatile变量instance赋值的地方。后面的add dword ptr [rsp],0h都是正常的汇编语句,意思是将双字节的栈指针寄存器+0,这里的关键就是add前面的lock指令,正如上文所说,加了volatile关键字修饰的变量instance前面多了一个lock指令。

4.volatile关键字的使用场景

通常来说,使用volatile关键字,就是需要保证操作是原子性操作,只有这样才能保证使用volatile关键字的程序在并发时能够正确执行。

5.补充说明:原子性

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。

volatile关键字可以保证可见性,那么它又能否保证原子性呢?

public class Test {
public volatile int inc = 0; public void increase() {
inc++;
} public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
} while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}

执行上述的代码,得到的结果是9997,并非是想象中的10000,这又是为啥呢?上面是对变量inc进行自增操作,由于volatile保证了可见性,那么在每个线程中对inc自增完之后,在其他线程中都能看到修改后的值啊,所以有10个线程分别进行了1000次操作,那么最终inc的值应该是1000*10=10000。

这里面就有一个误区了,volatile关键字能保证可见性没有错,但是上面的程序错在没能保证原子性。可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。

在前面已经提到过,自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存。那么就是说自增操作的三个子操作可能会分割开执行,就有可能导致下面这种情况出现:

假如某个时刻变量inc的值为10,线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了;然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存行无效,所以线程2会直接去主存读取inc的值,发现inc的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存。然后线程1接着进行加1操作,由于已经读取了inc的值,注意此时在线程1的工作内存中inc的值仍然为10,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。那么两个线程分别进行了一次自增操作后,inc只增加了1。

解释到这里,可能有朋友会有疑问,不对啊,前面不是保证一个变量在修改volatile变量时,会让缓存行无效吗?然后其他线程去读就会读到新的值,对,这个没错。这个就是上面的happens-before规则中的volatile变量规则,但是要注意,线程1对变量进行读取操作之后,被阻塞了的话,并没有对inc值进行修改。然后虽然volatile能保证线程2对变量inc的值读取是从内存中读取的,但是线程1没有进行修改,所以线程2根本就不会看到修改的值。根源就在这里,自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的。

参考文章:

http://www.cnblogs.com/dolphin0520/

http://www.importnew.com/27002.html

最新文章

  1. Mac下搭建git
  2. 设置apache https服务
  3. 移动Web单行文字垂直居中的问题
  4. CentOS镜像163更新源
  5. 【转】[WCF REST] 帮助页面与自动消息格式(JSON/XML)选择
  6. 让ecshop显示商品销量或者月销量
  7. JS学习笔记Day15
  8. 使用vue+iview实现上传文件及常用的下载文件的方法
  9. javascript之Map
  10. [Z] 从Uncaught SyntaxError: Unexpected token &quot;)&quot; 问题看javascript:void的作用
  11. 用2个DATETIMEPICKER分别输入时间和日期,再合并成一个DATETIME类型
  12. InstallShield2015创建安装包
  13. Android Custom View系列《圆形菜单一》
  14. Java如何查看线程的优先级?
  15. Qt 定时器Timer使用
  16. Win10如何搭建FTP服务器以实现快速传输文件
  17. POJ 1222 POJ 1830 POJ 1681 POJ 1753 POJ 3185 高斯消元求解一类开关问题
  18. java文件遍历
  19. 数组和集合(四)、Map集合的使用总结
  20. MATLAB 中gcf、gca 以及gco 的区别

热门文章

  1. vue 自己写组件。
  2. python 之模块引入
  3. 大话Ansible Ad-Hoc命令
  4. Python格式化字符串(格式化输出)
  5. AI技术原理|机器学习算法
  6. vue上的简单轮播图
  7. oracle的操作-表空间
  8. 每次找Internet选项感到抓狂?一键打开!
  9. Redis-Redis基本类型及使用Java操作
  10. sqoop-介绍及安装