首页 > 其他分享 >简单聊聊volatile关键字原理

简单聊聊volatile关键字原理

时间:2022-11-16 17:00:02浏览次数:75  
标签:读取 主存 关键字 线程 内存 聊聊 volatile inc

简单聊聊volatile关键字原理

 

volatile 关键字

一、说说你对 volatile 关键字的理解#

被 volatile 修饰的共享变量,就具有了以下两点特性:

  • 保证了不同线程对该变量操作的内存可见性;

  • 禁止指令重排序

二、内存可见性 和 禁止重排序分别怎么实现的?#

当一个变量被 volatile 修饰时,那么对它的修改会立刻刷新到主存,当其它线程需要读取该变量时,会去内存中重新读取新值。

禁止重排序,内部提供了内存屏障,写的指令不能往后排,读的指令不能往前排

1.可见性(Visibility): 说到可见性,Java就是利用volatile来提供可见性的。 当一个变量被volatile修饰时,那么对它的修改会立刻刷新到主存,当其它线程需要读取该变量时,会去内存中读取新值。而普通变量则不能保证这一点。 其实通过synchronized和Lock也能够保证可见性,线程在释放锁之前,会把共享变量值都刷回主存,但是synchronized和Lock的开销都更大。

2. 有序性(Ordering) JMM是允许编译器和处理器对指令重排序的,但是规定了as-if-serial语义,即不管怎么重排序,程序的执行结果不能改变。加上volatile关键字,禁止重排序,可以确保程序的有序性,也可以上重量级的synchronized和Lock来保证有序性,它们能保证那一块区域里的代码都是一次性执行完毕的。

三、volatile 关键字如何满足并发编程的三大特性的?#

当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量刷新到主内存,当读一个volatile变量时,JMM 会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量。

四、volatile 的两点内存语义能保证可见性和有序性,但是能保证原子性吗?#

不能保证原子性

比如下面的例子:

public class Test {
    public volatile int inc = 0;
 
    public void increase() {
        inc++;
    }
 
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
 
        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }

按道理来说结果是10000,但是运行下很可能是个小于10000的值。有人可能会说volatile不是保证了可见性啊,一个线程对inc的修改,另外一个线程应该立刻看到啊!可是这里的操作inc++是个复合操作啊,包括读取inc的值,对其自增,然后再写回主存。 假设线程A,读取了inc的值为10,这时候被阻塞了,因为没有对变量进行修改,触发不了volatile规则。 线程B此时也读读inc的值,主存里inc的值依旧为10,做自增,然后立刻就被写回主存了,为11。 此时又轮到线程A执行,由于工作内存里保存的是10,所以继续做自增,再写回主存,11又被写了一遍。所以虽然两个线程执行了两次 increase(),结果却只加了一次。 有人说,volatile 不是会使缓存行无效的吗?但是这里线程A读取到线程B也进行操作之前,并没有修改inc值,所以线程B读取的时候,还是读的10。 又有人说,线程B将11写回主存,不会把线程A的缓存行设为无效吗?但是线程A的读取操作已经做过了啊,只有在做读取操作时,发现自己缓存行无效,才会去读主存的值,所以这里线程A只能继续做自增了。 综上所述,在这种复合操作的情景下,原子性的功能是维持不了了。但是volatile在上面那种设置flag值的例子里,由于对flag的读/写操作都是单步的,所以还是能保证原子性的。 要想保证原子性,只能借助于synchronized,Lock以及并发包下的atomic的原子操作类了,即对基本数据类型的 自增(加1操作),自减(减1操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,保证这些操作是原子性操作。

五、知道 volatile 底层的实现机制?#

  1. 重排序时不能把后面的指令重排序到内存屏障之前的位置

  2. 使得本CPU的Cache写入内存

  3. 写入动作也会引起别的CPU或者别的内核无效化其Cache,相当于让新写入的值对别的线程可见。

六、 你在哪里会使用到volatile,举两个例子呢?#

1.状态量标记,就如上面对flag的标记,我重新提一下:

int a = 0;
volatile bool flag = false;

public void write() {
    a = 2;              //1
    flag = true;        //2
}

public void multiply() {
    if (flag) {         //3
        int ret = a * a;//4
    }
}

这种对变量的读写操作,标记为volatile可以保证修改对线程立刻可见。比synchronized,Lock有一定的效率提升。

2.单例模式的实现,典型的双重检查锁定(DCL)

class Singleton{
    private volatile static Singleton instance = null;
 
    private Singleton() {
 
    }
 
    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

这是一种懒汉的单例模式,使用时才创建对象,而且为了避免初始化操作的指令重排序,给instance加上了volatile。

标签:读取,主存,关键字,线程,内存,聊聊,volatile,inc
From: https://www.cnblogs.com/LLW521/p/16896464.html

相关文章

  • 聊聊Hive数据血缘——从Atlas没有列级血缘的Bug讲起
    前几天,Datahub提供了最新的字段级别数据血缘功能,很多朋友迫不及待想对比一下Datahub的字段级血缘与Atlas的区别。这个时候问题来了,在Atlas收集Hive血缘的时候,由于部分版本......
  • 聊聊Vuex原理
    背景Vuex是一个专为Vue.js应用程序开发的状态管理模式。Vuex是专门为Vue.js设计的状态管理库,以利用Vue.js的细粒度数据响应机制来进行高效的状态更新。如果你已......
  • C语言inline关键字
    一、inline关键字的概念inline关键字是C99标准的型关键字,其作用是将函数展开,把函数的代码复制到每一个调用处。这样调用函数的过程就可以直接执行函数代码,而不发生跳转、......
  • 一文彻底搞懂volatile用法
    一、常见说法volatile关键字和const对应,一样是一种类型修饰符,用它修饰的变量表示可以被某些编译器未知的因素更改,比如操作系统、硬件或者其它线程等。遇到这个关键字声......
  • this和super关键字
    this关键字this的本质就是:创建好的对象的地址->代表当前对象构造方法是创建java对象的重要途经,通过new关键字调用构造器时,构造器返回该类对象,但是该对象并不完全由构......
  • 聊聊FASTER和进程内混合缓存
    最近有一个朋友问我这样一个问题:我的业务依赖一些数据,因为数据库访问慢,我把它放在Redis里面,不过还是太慢了,有什么其它的方案吗?其实这个问题比较简单的是吧?Redis其实属......
  • 抽象类、abstract关键字
    目录abstract关键字抽象类的匿名子类抽象类的应用:模板方法的设计模式abstract关键字abstract:抽象的abstract可以用来修饰的结构:类、方法abstract修饰类:抽象类此类......
  • 接口、interface关键字
    目录interface关键字接口的匿名实现类的对象接口的应用:代理模式JDK8:除了定义全局常量和抽象方法之外,还可以定义静态方法、默认方法面试题:抽象类与接口有哪些异同?interface......
  • 代码块、final关键字
    目录代码块final关键字代码块代码块的作用:用来初始化类、对象代码块如果有修饰的话,只能使用static分类:静态代码块vs非静态代码块静态代码块: 内部可以有输......
  • static关键字
    static关键字static:静态的static可以用来修饰:属性、方法、代码块、内部类使用static修饰属性:静态变量(或类变量)属性,按是否使用static修饰,又分为:静态属性vs非静态属......