Volatile关键字的底层实现原理以及单例模式中使用Volatile关键字的原因

Volatile关键字在多处理器开发环境中或者多线程环境下,保证共享变量的可见性。
可见性:当一个线程修改共享变量的值之后,其它线程可以立即读取到它修改的值。
如果 volatile 使用得当的话,它会比 synchronized 的成本更低,因为它不会造成线程的阻塞,也就不会导致上下文切换和调度,所以性能更好,开销更低。

Volatile的定义

Java 编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排它锁单独获得这个变量。
如果一个字段被声明为 volatile,Java 的线程内存模型(JMM)确保所有线程看到这个变量的值是一致的。

java变量的读写

Java通过几种原子操作完成工作内存主内存的交互:
1.lock:作用于主内存,把变量标识为线程独占状态。
2.unlock:作用于主内存,解除独占状态。
3.read:作用主内存,把一个变量的值从主内存传输到线程的工作内存。
4.load:作用于工作内存,把read操作传过来的变量值放入工作内存的变量副本中。
5.use:作用工作内存,把工作内存当中的一个变量值传给执行引擎。
6.assign:作用工作内存,把一个从执行引擎接收到的值赋值给工作内存的变量。
7.store:作用于工作内存的变量,把工作内存的一个变量的值传送到主内存中。
8.write:作用于主内存的变量,把store操作传来的变量的值放入主内存的变量中。

volatile如何保持内存可见性

volatile的特殊规则就是:

  1. read、load、use动作必须连续出现。
  2. assign、store、write动作必须连续出现。

所以,使用volatile变量能够保证::

  1. 每次读取前必须先从主内存刷新最新的值。
  2. 每次写入后必须立即同步回主内存当中。

Volatile的实现原理(MESI[2])

由volatile关键字修饰的共享变量,在进行写操作的时候会多出一行汇编代码,该指令带有lock前缀,带有lock前缀的指令在多核处理器下会引发两个操作:

  • 将当前处理器缓存的内容写回主内存
  • 使其它引用写回内存中内容的CPU缓存中的数据无效

上述两个额外的事件保证了Volatile关键字的可见性。

单例模式下的Volatile关键字的作用

//使用DCL(Double Check Lock)双重检查锁机制
class Singleton {
    private static Singleton instance;
    
    public int f1 = 1;   // 触发部分初始化问题
    public int f2 = 2;
    	
    private Singleton(){}
	
    public static Singleton getInstance() {
        if (instance == null) { // 当instance不为null时,可能指向一个“被部分初始化的对象”
            synchronized (Singleton.class) {
                if ( instance == null ) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

“看起来”非常完美:既减少了阻塞,又避免了竞态条件。不错,但实际上仍然存在一个问题——当instance不为null时,仍可能指向一个"被部分初始化的对象"。
如果Singleton没有字段,自然也不会有部分初始化之说。因此,这里添加了两个字段,已触发部分初始化问题。
问题出在这行简单的赋值语句:

instance = new Singleton();

它并不是一个原子操作。事实上,它可以”抽象“为下面几条JVM指令:

memory = allocate();	//1:分配对象的内存空间
initInstance(memory);	//2:初始化对象(对f1、f2初始化)
instance = memory;		//3:设置instance指向刚分配的内存地址

上面操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM可以以“优化”为目的对它们进行重排序,经过重排序后如下:

memory = allocate();	//1:分配对象的内存空间
instance = memory;		//3:设置instance指向刚分配的内存地址(此时对象还未初始化)
ctorInstance(memory);	//2:初始化对象

可以看到指令重排之后,操作 3 排在了操作 2 之前,即引用instance指向内存memory时,这段崭新的内存还没有初始化——即,引用instance指向了一个”被部分初始化的对象”。此时,如果另一个线程调用getInstance方法,由于instance已经指向了一块内存空间,从而if条件判为false,方法返回instance引用,用户得到了没有完成初始化的“半个”单例。
解决这个该问题,只需要将instance声明为volatile变量。因为Volatile能够防止指令重排。

扫描二维码关注公众号,回复: 8581459 查看本文章

[1]https://monkeysayhi.github.io/2016/11/29/volatile%E5%85%B3%E9%94%AE%E5%AD%97%E7%9A%84%E4%BD%9C%E7%94%A8%E3%80%81%E5%8E%9F%E7%90%86/
[2]https://www.jianshu.com/p/64240319ed60

发布了8 篇原创文章 · 获赞 1 · 访问量 55

猜你喜欢

转载自blog.csdn.net/qq_33898680/article/details/103931011
今日推荐