• volatile 关键字的适用与不适用场景
  • 发布于 2个月前
  • 190 热度
    0 评论
一、volatile 关键字的适用与不适用场景
1. 什么是 volatile
volatile是一种同步机制,类似于 Lock 和 Synchronized ,但是他更轻量级,因为使用 volatile 并不会发生上下文切换等开销很大的行为。
如果一个变量被volatile修饰,那么JVM会认为这个变量可能会被并发修改,会保证关于这个变量的修改能立即被其他线程看到。
因为开销小,所以能力也小;他做不到像 synchronized 那样的原子保护,使用的场景比较有限。
2. volatile 作用
可见性
.读一个volatile修饰的变量之前,需要先使相应的本地缓存失效,这样就必须到主内存读取最新的值;
.写一个volatile属性会立刻刷入到主内存。

这里的“可见性”是指当一个线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。

禁止指令重排序化
使用 volatile 变量的第二个语义是禁止指令重排序优化,普通变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序(代码文件中的顺序)与程序代码中的执行顺序一致(实际执行顺序)。不过在一个线程的方法执行过程中无法感知到这点,这也就是 Java 内存模型中描述的所谓的“线程内表现为串行的语义”,我们可以通过一段代码来理解。
Map configOptions;
char[] configText;
//此变量必须定义为volatile
volatileboolean initialized = false;
//堆代码 duidaima.com
//假设以下代码在线程A中执行
//模拟读取配置信息,当读取完成后将initialized设置为true以通知其他线程配置可用
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText, configOptions);
initialized = true;

//假设以下代码在线程B中执行
//等待initialized为true,代表线程A已经把配置信息初始化完成
while(!initialized){
    sleep();
}
//使用线程A中初始化好的配置信息
这是一段伪代码,这里如果定义 initialized 变量时没有使用 volatile 修饰,就可能会由于指令重排序的优化,导致位于线程 A 中最后一句的代码 “initialized=true” 被提前执行,这样在线程 B 中使用配置信息的代码就可能出现错误,而volatile 关键字则可以避免此类情况的发生。

二. 不适用的场景
(1) 对于 i++ 的操作,即使使用volatile 修饰了 i,也不能保证 i++ 的并发安全,代码演示如下:
/**
 *      不适用volatile场景
 */
publicclass NoVolatile implements Runnable {

    volatileint a;

    AtomicInteger realA = new AtomicInteger();

    public static void main(String[] args) throws InterruptedException {
        NoVolatile noVolatile = new NoVolatile();

        Thread thread1 = new Thread(noVolatile);
        Thread thread2 = new Thread(noVolatile);

        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();

        System.out.println("使用volatile修饰的a:"+noVolatile.a);
        System.out.println("AtomicInteger的realA:"+noVolatile.realA.get());
    }

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            a++;
            realA.incrementAndGet();
        }
    }
}
运行结果:

使用 volatile 修饰的变量,在多线程下执行 i++ 操作,发现最终结果不对,说明 volatile 无法保证并发安全。

(2) 对于依赖之前的状态的操作,比如对 boolean 类型的变量取反,就需要先获取该变量之前的值,然后再取反
/**
  *      volatile不适用情况:boolean flag取反的情况
 */
publicclass UseVolatile2 implements Runnable {

    volatileboolean flag = false;
    AtomicInteger realA = new AtomicInteger();

    public static void main(String[] args) throws InterruptedException {
        UseVolatile2 r = new UseVolatile2();

        Thread thread1 = new Thread(r);
        Thread thread2 = new Thread(r);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();

        System.out.println(r.flag);
        System.out.println("AtomicInteger的realA:" + r.realA.get());
    }

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            setTrue();
            realA.incrementAndGet();
        }
    }

    private void setTrue() {
        flag = true;
    }

}
运行结果:

经过 20000 次取反,应该为false,但是这里的结果是 true,说明没有实现并发安全。

其实这里可以总结为:当使用 volatile 修饰变量 x 时,若 x 在多线程下执行的操作不具有原子性,则不能保证并发安全;而如果 x 执行的操作具有原子性,则由于volatile具有可见性以及防止重排序的特性,会让 x 的操作立即被其他线程看到。

三. 适用的场景
(1)直接赋值
由于赋值操作具有原子性,所以能够保证变量执行完赋值操作能立即被其他线程看到,保证了可见性。
对 boolean 类型的变量赋值代码展示如下:
/**
  *      volatile适用情况:boolean flag标记位布尔值赋值
 */
publicclass UseVolatile1 implements Runnable {

    volatileboolean flag = false;
    AtomicInteger realA = new AtomicInteger();

    public static void main(String[] args) throws InterruptedException {
        UseVolatile1 r = new UseVolatile1();

        Thread thread1 = new Thread(r);
        Thread thread2 = new Thread(r);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();

        System.out.println(r.flag);
        System.out.println("AtomicInteger的realA:" + r.realA.get());
    }

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            setTrue();
            realA.incrementAndGet();
        }
    }

    private void setTrue() {
        flag = true;
    }

}
(2) 作为刷新之前变量的触发器
volatile 具有可见性和防止重排序的特性,所有由它修饰的变量 x 在执行某个操作之后,x 之前的代码已经被执行过了(防止重排序),因为 x 会被其它线程立即看到(可见性),所以 x 之前的代码中的变量也会被其他线程看到 (happens-before原则,也可以说是 近朱者赤 原则,近朱者赤是 volatile 的一个特性)。这里使用上面的一个案例来说明,如下:
    Map configOptions;
    char[] configText;
    volatileboolean initialized = false;

    // Thread A 中执行
    configOptions = new HashMap();
    configText = readConfigFile(fileName);
    processConfigOptions(configText, configOptions);
    initialized = true;

    // Thread B 中执行    
    // 这里的initialized就是一个触发器, 当这个while为true时才能进入接下来的操作,initialized 为true 意味着initialized = true;已经执行,同时也意味着在它之前的操作也都已经执行了(防止重排序),并且能被其它线程看到(近朱者赤的特性)
    while (!initialized) 
        sleep();
   // use configOptions
volatile 的特性:近朱者赤。他不仅可以帮助自己可见性,也可以帮助在他进行赋值之前进行的操作也具有可见性
四. volatile 的实现原理
volatile 内存屏障,分为两种:
Load Barrier 读屏障:在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制重新从主内存加载数据;

Store Barrier 写屏障:利用缓存一致性机制强制将对变量的修改操作立即写入主内存,并且让其他线程缓存中变量失效,并且缓存一致性机制会阻止同时修改由两个以上CPU缓存的内存区域数据。

内存屏障的作用:确保指令重排序时不会把屏障后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障后面。
强制把写缓冲区/高速缓存中的数据等写回主内存,让缓存中相应的数据失效;
五. 使用 volatile 的意义
它能让我们的代码比使用其他的同步工具更快吗?

在某些情况下,volatile 的同步机制的性能确实要优于锁,但由于虚拟机对锁实行的许多消除和优化,使得我们很难量化地认为 volatile 就会比 synchronized 快多少。

如果让 volatile 变量与普通变量比较,那可以确定一个原则:volatile 变量读操作的性能消耗与普通变量几乎没有什么差别,但是写操作则可能会慢一些,因为他需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。不过即便如此,大多数场景下 volatile 的总开销仍然要比锁低,我们在 volatile 与锁之间选择的唯一依据仅仅是 volatile 的语义能否满足使用场景的需求。

六. volatile & synchronized 的关系
volatile 在这方面可以看做是轻量版的synchronized:如果一个共享变量自始至终只被各个线程赋值(或类似的原子操作),而没有其他的操作,那么就可以用volatile来代替synchronized来修饰变量,因为赋值自身是有原子性的,而volatile又保证了可见性,所以就足以保证线程安全;

volatile 不提供锁的机制,所以他低成本;volatile 只能修饰属性:对一个变量修饰volatile,那么compilers(编译器)就不会对这个属性做指令重排序;synchronized可以修饰代码块、方法、静态代码块、类;
用户评论