关于java volatile关键字,以后别再面试中说不清楚了
文章出处:Drummor---原文地址
问题
代码清单1
class SingleInstance{ private static single = null; private SingeInstance(){} public SingleInstance getSingleInstance(){ if(single == null)//0 synchroznied(SingleInstance.class){ if(single == null){ single = new SingleInstance();//1 } } return single; } }
如上代码的问题是在1处不能保证有序性,即这句代码其实分为两个大的步骤
初始化SingleInstance
把这个对象赋值给single这个变量
这个步骤前后是不确定的。当线程一运行到1处的时候可能会先对象赋值给single了但是此时的single还没有初始化完成。线程2运行的0处的时候会发现这个条件是不符合的于是就返回了single。这时候的single虽然是一个非空的引用,但却不是一个正确的对象。 这个就是双重校验可能出现的问题。
volatile
可能你听说过JDK1.4以后用volatile修饰变量single可以解决这个问题,可你知道为什么能解决吗?
volatile的语义是能保持有序性和可见性,但是不能保证原子性
可见性
什么是可见性?
以
count = 0; couont++;
为例这个行代码的执行过程如下:
将 count 的值从内存加载到自己的线程栈中
在自己的线程栈中对count进行加一操作
把修改后的值放回到主内存中。
在多线程的情况下
线程1执行了第一个操作
之后线程2也执行了第一操作
线程1执行了后面两个操作,此时主存中的count值变成了1;
线程2继续执行第二个操作,它用的是自己栈中的副本其值为0进行加1,最后执行第三个操作把1写回到主存中。
看到问题了吧,加了两遍还是1,出事了啊兄弟!
volatile内在其中起什么作用呢?
当用volatile修饰count后这样线程执行操作的时候也就是上述的****2步骤他不会在副本中取值,而是去主存中取值。
即便是这样也不能解决计数问题,为什么呢?
线程1从内存中取值进行加1操作,线程副本count值变成了1。
然后线程2从主存中取值,这时候取到的值是0,进行加1操作,写会到主存,主存中count变成了1。
线程1执行步骤3把自己副本中值为1的count写回到主存,主存还是1。
小结
volatile的可见性语义是保证线程进行操作也就是上述的步骤2是从主存中取最新的值而不是在自己副本中取值。
有序性
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
例子: 代码清单2
线程A中 context = initContext();//1 flag = true;//2 线程B中while(!flag){ sleep(100); } dosomething(context);
在单线程中代码是没有问题的,但是如代码清单二中,线程A的代码可能会发生重排序也就是运行代码2再运行代码1这就有问题了。
如果用volatile修饰就会禁止他进行重排序。
原子性
原子性简单来说就是不可分割,如果是原子操作,那它必定是要么被执行完毕,要么完全没执行两种情况之一,不可能出现执行了一部分这种情况。
总结
volatile字段能保证可见性、禁止重排序,但并不能提供原子性。原因在于在多线程的条件下,不能保证执行顺序,中间会有线程切换的情况出现。
回到代码清单1还记得当初的问题吗?代码清单1中这个单例有什么问题我们已经说过了。怎么解决呢? 其实有了volatile这个关键字就好解决了,在single这个变量上添加volatile就可以完美解决了。原因是volatile具有禁止重排序的功能。所以会先进行初始化对象再赋值给变量,0处检测到的single不为空的时候就能正确返回single而不再是一个不完整的single了。
共同学习,写下你的评论
评论加载中...
作者其他优质文章