关于java volatile关键字,以后别再面试中说不清楚了

3,928 阅读4分钟

问题

代码清单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++;

为作为 错误 例这个行代码的执行过程如下:

  1. 将 count 的值从内存加载到自己的线程栈中
  2. 在自己的线程栈中对count进行加一操作
  3. 把修改后的值放回到主内存中。

在多线程的情况下

  • 线程1执行了第一个操作
  • 之后线程2也执行了第一操作
  • 线程1执行了后面两个操作,此时主存中的count值变成了1;
  • 线程2继续执行第二个操作,它用的是自己栈中的副本其值为0进行加1,最后执行第三个操作把1写回到主存中。

看到问题了吧,加了两遍还是1,出事了啊兄弟!

volatile在其中起什么作用呢?

当用volatile修饰count后这样线程执行操作的时候也就是上述的****2步骤他不会在副本中取值,而是去主存中取值。

即便是这样也不能解决计数问题,为什么呢?

没有保证原子性,i++操作非原子操作

  • 线程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了。