问题
代码清单 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 了.
来源: https://juejin.im/post/5ab794c751882555635e497a