关闭
Hit
enter
to search or
ESC
to close
May I Suggest ?
#leanote #leanote blog #code #hello world
Okeeper's Blog
Home
Archives
Tags
DevOps
软件笔记
Spring
学习
JVM系列
关于我
关于volatile关键字原理解析
无
565
0
0
zhangyue
volatile这个关键字我们在实际开发过程中,或多或少都使用过,但是对于其背后的实现原理机制还是不够清晰,接下来满满梳理一番。 ## 我们来看一段代码 ``` i = i + 1; ``` 一个简单的对i变量的递增操作,在单线程执行的情况下,没有什么问题,但是在多线程情况下,结果就会有问题了。原因是因为计算机的内存模型为每个线程分配了CPU高速缓存,来加快CPU的处理速度。 假如现i的初始值为0,有两个线程同时对其做递增1的操作,那么过程如下: 所以执行过程就是线程A从主内存中获取i的值0到缓冲内存中,然后将其数值+1重新复制给缓冲区的i=1,接着在写入主内存中的i=1; 同时线程B也从主内存中获取i的值为0到缓冲内存中,最终也是递增+1赋值给i=1,接着吸入主内存中的i=1; 那么最终的结果有可能就还是1; 为什么会出现以上这个情况,这里涉及到两个并发问题: - 缓存一致性 - 原子性 - 为了解决缓存不一致性问题,通常来说有以下2种解决方法: 1)通过在总线加LOCK#锁的方式 2)通过缓存一致性协议 这两种方式都是通过硬件指令方式,早期是使用第一种方式实现的(CPU与内存通讯都要通过内存总线),在LOCK期间CPU不能访问对应的整个内存,效率极低; 所以后期就出现了基于CPU指令的缓存一致性解决方案,例如Intel 的MESI协议,原理是当线程对共享变量进行写操作时,立即更新到主内存,并将其他线程的内存缓存中的拷贝失效;这样当其他线程读取共享变量发现是无效时重新获取主内存中的最新值。这也是Volatile关键字的原理,后面会详细讲到。 ## Java中的内存模型 在前面我们谈到计算机的物理内存模型,现在我们来说说JVM的内存模型。 JVM中定义一种Java内存模型来屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。Java内存模型中规定所有的变量都存在主内存中(相当于前面说的物理主内存),每个线程都有自己的工作内存(相当于计算机的高速缓存),但是它同样保持了CPU的高速缓存即CPU指令重排序的优化方案。也就是说,在Java内存模型中,也会存在缓存一致性问题和指令重排序的问题。 关于指令重排序的意思是,在cpu执行具体指令时,通过重写排序执行语句的先后顺序来提高执行效率,但是重排必不会影响最终的结果,当前后语句有依赖关系时不会被重新排序。 但是要注意,虽然指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性,来看下面的例子: ``` //线程1: context = loadContext(); //语句1 inited = true; //语句2 //线程2: while(!inited ){ sleep() } doSomethingwithconfig(context); ``` 试想,如果在多线程情况下,由于语句1与语句2并没有依赖关系,是有可能发生指令重排序的,假如线程1先执行了语句2,再执行语句1,这时候线程2读取到inited未真就接着往下走,此时线程1的语句1还没有没初始化好context,线程2就有可能报空指针异常了。 那么这里引申出了多线程并发情况下的第三个问题 **有序性** ## 并发编程中的三个概念 前面我们讲到并发过程中的几个问题: - 原子性(那么成功要么失败,没有中间状态) - 可见性(一致性) - 有序性(指令重排序问题) ### 我们一一看下怎么解决这三个问题 1. 原子性 在java中通过JDK5的Lock或者synchronized来实现,由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。 2. 可见性 可见性,就可以文章开头讲的volatile关键字做修饰变量,当一个变量使用volatile关键字修饰时,对变量的修改会立即写入主内存中,当读取volatile变量时能够最新的更新值。 此外,Lock或者synchronized也能实现变量的可见性,因为同时只有一个线程对变量操作,并且在释放锁之后会更新至主内存,从而其他线程能够获取最新值。 3. 有序性 即如何避免指令重排对并发线程执行的结果影响。 在java中可以使用volatile关键字来避免指令重排影响,也可以通过锁来保持单线程执行从而避免。 在JVM中规定了原则,保证了在一下情况下将不会发生指令重排: - 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作,(就是程序的执行结果就是你看到代码的编写顺序执行的结果,但是底层执行时是有可能发生指令重排序的,只不过不会影响最终的执行结果的意思) - 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作 - volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作 - 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C - 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作 - 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生 - 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行 - 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始 以上就是JVM中定义的《happens-before》原则。除此之外在程序执行过程中是有可能发生指令重排的。 ## 深入剖析volatile关键字 以上可以看到,在并发过程中,volatile关键字能够解决可见性问题和有序性问题,那么那是怎么实现的呢? 一个变量被volatile关键字修饰时包含了两个含义: - 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。 - 禁止进行指令重排序,即禁止了操作volatile变量的重排,以及禁止这个语句之后的所有语句的重排序 举个例子 ``` //x、y为非volatile变量 //flag为volatile变量 x = 2; //语句1 y = 0; //语句2 flag = true; //语句3 x = 4; //语句4 y = -1; //语句5 ``` 由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。 观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令. lock前缀指令的意思是在变量操作时生成一个内存屏障,而内存屏障主要主要有三个特点: - 不会把当前指令重排序,也不会吧当前指令之后的命令排在当前指令之前,但是也会方式重排 - 当是对变量进行写操作时,立即更新值主内存中,并将其他工作内存中的变量失效,确切的讲是缓存行的失效,有关缓存行的更多内存请查看[《高性能无锁阻塞队列——Disruptor》](http://okeeper.leanote.com/post/%E9%AB%98%E6%80%A7%E8%83%BD%E6%97%A0%E9%94%81%E9%98%BB%E5%A1%9E%E9%98%9F%E5%88%97%E2%80%94%E2%80%94Disruptor) 这也就是volatile关键字的作用,它既能保证变量的可见性也能保证操作volatile关键字的变量不进行重排的有序性。 ## 总结 volatile是为了解决内存模型中的缓存一致性问题,让共享变量的修改能够立马在其他线程可见,同时他还避免了对volatile关键字修饰的变量操作的指令重排序问题。 ### 场景一 关于volatile的使用场景一般就是在多线程之间共享状态变量时,如下: ``` volatile boolean flag = false; while(!flag){ doSomething(); } public void setFlag() { flag = true; } ``` 当flag为false时结束线程。 ### 场景二 实现双重确认单例模式 ``` class Singleton{ private volatile static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if(instance==null) { synchronized (Singleton.class) { if(instance==null) instance = new Singleton(); } } return instance; } } ``` 参考文献: 《深入理解Java虚拟机》 [Java并发编程:volatile关键字解析](http://www.importnew.com/18126.html)
觉得不错,点个赞?
Please enable JavaScript to view the
comments powered by Disqus.
comments powered by
Disqus
文章目录