JUC源码解析:深入理解 volatile

JUC源码解析:深入理解 volatile

volatile 的定义

volatile 的作用:

  • 保证可见性
  • 禁止指令重排序

volatile 可以被看作是轻量版的 synchronized,volatile 保证了多线程中共享变量的“可见性”,就是说,当volatile 变量的值被修改时,其他线程能读取到被修改的变量值。

如果volatile 使用恰当的话,它比synchronized的执行成本更低,因为volatile 不需要线程上下文的切换,并且在“读多写少”的情况下,volatile的效率更好。

为什么在“读多写少”的情况下,volatile的效率更好?

因为在 volatile 的JMM内存屏障中定义,如果发生对 volatile 变量的读操作,会在 读之后 设置内存屏障,而读之前是没有内存屏障的。在 写前后 却都有内存屏障。所以读多写少的情况下volatile的效率会更高些。

关于JMM内存屏障,请看下一节的介绍

volatile 禁止指令重排序原理

volatile 会在编译器生成字节码时,插入JMM内存屏障来保障 volatile 变量不被重排序。

JMM有四个volatile内存策略:

  • 写操作:
    • 写之前插入 StroeStore 屏障
    • 写之后插入 StoreLoad 屏障
  • 读操作:
    • 读之后插入 LoadLoad 屏障
    • 读之后插入 LoadStore 屏障

这是非常保守的屏障策略,实际中,如果有这样操作:

//伪代码
volatile int a, b;
a = 1; // volatile 写操作
b = 1; // volatile 写操作

两个写操作在一起时,JMM可以不那么保守。因为两个写操作并在一起,可以在中间省略一些屏障,让程序更有效率。

单例模式线程不安全的核心原因

​ 有些单例模式是线程不安全的,为什么,看看下面的代码:

public class Main {

    private static Object instance;

    public static Object getInstance() {

        if (instance == null) {	// 第一次检查
            synchronized (Main.class) {	// 加锁
                if (instance == null) { // 第二次检查
                    instance = new Object(); // 问题的根源出现在这里!!!
                }
            }
        }

        return instance;
    }
}

很显然,这是一个不适应 volatile 的线程不安全的单例模式。为什么非线程安全,核心漏洞在 instance = new Object();这一行语句上,我们把它拆解为下面三行伪代码:

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

记住,此时的instance是没有volatile关键字的,它是允许被重排序的,在一些多线程情况加,它可能会被重排序成这种情况

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

还没有在内存中初始化对象,就已经分配地址了!

这时候,如果有另外一个线程钻空子,就可能拿到一个分配了内存地址、但还没有真正初始化的对象

因此,单例模式在多线程环境下,使用 volatile 禁止重排序是必要的!


使用 volatile 优化后的代码:

public class Main {

    private static volatile Object instance;

    public static Object getInstance() {

        if (instance == null) {	
            synchronized (Main.class) {
                if (instance == null) { 
                    instance = new Object(); // 现在不会被重排序了
                }
            }
        }

        return instance;
    }
}

深层解读:

instance = new Object(); 是 volatile 写操作,JMM会在编译期间为它设置内存屏障。在这句代码之前,会加入 storestroe 屏障,在这句代码之后,会加入 storeload 屏障,这样,其他线程就必须在 stroeload 屏障后面读取,就保证了创建对象时的原子性。

posted @ 2024-05-11 22:34  yangruomao  阅读(7)  评论(0编辑  收藏  举报