首页 > 其他分享 >volatile 关键字

volatile 关键字

时间:2023-06-03 11:58:57浏览次数:40  
标签:变量 Thread 关键字 线程 new initialized volatile

一、volatile 关键字的适用与不适用场景

1. 什么是 volatile

  • volatile是一种同步机制,类似于 Lock 和 Synchronized ,但是他更轻量级,因为使用 volatile 并不会发生上下文切换等开销很大的行为。
  • 如果一个变量被volatile修饰,那么JVM会认为这个变量可能会被并发修改,会保证关于这个变量的修改能立即被其他线程看到。
  • 因为开销小,所以能力也小;他做不到像 synchronized 那样的原子保护,使用的场景比较有限。

2. volatile 作用

  • 可见性

读一个volatile修饰的变量之前,需要先使相应的本地缓存失效,这样就必须到主内存读取最新的值;写一个volatile属性会立刻刷入到主内存。这里的“可见性”是指当一个线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的

  • 禁止指令重排序化

使用 volatile 变量的第二个语义是禁止指令重排序优化,普通变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序(代码文件中的顺序)与程序代码中的执行顺序一致(实际执行顺序)。不过在一个线程的方法执行过程中无法感知到这点,这也就是 Java 内存模型中描述的所谓的“线程内表现为串行的语义”,我们可以通过一段代码来理解。

Map configOptions;
char[] configText;
//此变量必须定义为volatile
volatile boolean initialized = false;

//假设以下代码在线程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 关键字则可以避免此类情况的发生。

2. 不适用的场景:

(1) 对于 i++ 的操作,即使使用volatile 修饰了 i,也不能保证 i++ 的并发安全,代码演示如下:

/**
 *      不适用volatile场景
 */
public class NoVolatile implements Runnable {

    volatile int 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取反的情况
 */
public class UseVolatile2 implements Runnable {

    volatile boolean 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;
    }

}

运行结果:

image-20230531164356418

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

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

3. 适用的场景

(1)直接赋值

由于赋值操作具有原子性,所以能够保证变量执行完赋值操作能立即被其他线程看到,保证了可见性。

对 boolean 类型的变量赋值代码展示如下:

/**
  *      volatile适用情况:boolean flag标记位布尔值赋值
 */
public class UseVolatile1 implements Runnable {

    volatile boolean 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;
    volatile boolean 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 的特性:近朱者赤。他不仅可以帮助自己可见性,也可以帮助在他进行赋值之前进行的操作也具有可见性

4. volatile 的实现原理

volatile 内存屏障,分为两种:

  • Load Barrier 读屏障

    在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制重新从主内存加载数据;

  • Store Barrier 写屏障

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

内存屏障的作用:

  • 确保指令重排序时不会把屏障后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障后面。
  • 强制把写缓冲区/高速缓存中的数据等写回主内存,让缓存中相应的数据失效;

5. 使用 volatile 的意义

它能让我们的代码比使用其他的同步工具更快吗?

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

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

6. volatile & synchronized 的关系

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

volatile 不提供锁的机制,所以他低成本

volatile 只能修饰属性:对一个变量修饰volatile,那么compilers(编译器)就不会对这个属性做指令重排序,synchronized可以修饰代码块、方法、静态代码块、类;

文章来源:volatile 关键字

个人微信:CaiBaoDeCai

微信公众号名称:Java知者

微信公众号 ID: JavaZhiZhe

谢谢关注!

标签:变量,Thread,关键字,线程,new,initialized,volatile
From: https://www.cnblogs.com/javazhizhe/p/17453746.html

相关文章

  • synchronized 关键字
    1.synchronized关键字的作用synchronized关键字解决的是多个线程之间访问资源的同步性,用于保证在同一时刻最多只有一个线程执行该段代码,以达到保证并发安全的效果。2.作用范围修饰代码块:大括号括起来的代码,作用于调用的对象。修饰方法:整个方法,作用于调用的对象。修饰静......
  • python 仅关键字参数
    defstandard_arg(arg)defstandard_arg(arg):print(arg)standard_arg(2)standard_arg(arg=2)对参数没有限制defstandard_arg(arg,/)defstandard_arg(arg,/):print(arg)standard_arg(2)#正确standard_arg(arg=2)#报错仅限位置参数def stand......
  • 【电商api接口系列分享】按关键字搜索商品演示示例
     在电商平台中,关键词推荐是提高用户购物体验和销售业绩的一个重要手段。它的重要性体现在以下几个方面:提升购物体验:通过关键词推荐,电商平台可以根据用户的搜索意图和行为来向其推荐相关的商品。这样可以帮助用户更快地找到自己需要的商品,提高购物体验和满意度。增加销售......
  • C# 一些非常用关键字
    一、fixed 1、fixed语句禁止垃圾回收器重定位可移动的变量。fixed语句只能出现在不安全的上下文中。Fixed还可用于创建固定大小的缓冲区。2、fixed语句设置指向托管变量的指针并在statement执行期间“钉住”该变量。如果没有fixed语句,则指向可移动托管变量的指针的作用......
  • Day07-static关键字
    static关键字static修饰变量可直接被类名调用可被对象调用没有加static关键字时,无法直接被类名调用publicclassDemo01{  privatestaticintage;//静态的变量多线程!  privatedoublescore; //非静态的变量​  publicstaticvoidmain(String[]......
  • [C++学习]关键字
    const关键字const关键字表示该数据类型的值只读,不可赋值,也就意味着它必须初始化。const若是修饰常量,则只读变量的值会放在“符号常量表”中,而不会立即开辟空间,当数据取地址时则会开辟空间。constintdata=100;int*p=(int*)&data;//开辟空间,p指向该空间。*p=200......
  • volatile与java内存模型
    一、结论先说结论,volatile能保证可见性和有序性,不能保证原子性。二、volatile的内存语义当写一个volatile变量时,会将变量值刷新回主内存当读一个volatile变更时,会从主内存中读取最新值三、内存屏障是什么?内存屏障是一类同步屏障指令,是cpu或编译器在对内存随机访问操作的一......
  • volatile为什么不能保证原子性
    首先要了解的是,volatile可以保证可见性和顺序性,这些都很好理解,那么它为什么不能保证原子性呢?可见性可见性与Java的内存模型有关,模型采用缓存与主存的方式对变量进行操作,也就是说,每个线程都有自己的缓存空间,对变量的操作都是在缓存中进行的,之后再将修改后的值返回到主存中,这就带......
  • SQL注入绕过——主要是magic_quotes_gpc, is_int(只能跑路,无注入点),以及关键字绕过,WAF绕
       SQL注入点是可以在get、post、cookie、request、http头里等 ......
  • Java的volatile
    介绍volatilevolatile关键字可以说是Java虚拟机提供的最轻量级的同步机制,但是它并不容易被正确、完整地理解,以至于许多程序员都习惯去避免使用它,遇到需要处理多线程数据竞争问题的时候一律使用synchronized来进行同步。了解volatile变量的语义对理解多线程操作的其他特性......