volatile特性详解

目录

1 前言

2 volatile变量读写的原子性和普通变量读写的非原子性

2.1 volatile读写原子性依据

2.2 普通变量读写非原子性依据

2.3 非原子性例子

3 可见性

4 禁止指令重排序


1 前言

volatile变量有三个特性:

  • 原子性:对任意单个volatile变量的读/写具有原子性。
  • 可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入(靠原子性保障)。
  • 能够防止相关指令重排序。

2 volatile变量读写的原子性和普通变量读写的非原子性

volatile变量的读和写操作都是原子的,在读和写的过程中其它线程不能对变量进行读或写。而普通变量是没办法做到的,因为将普通变量读和写并不是原子的,非原子操作的各条指令之间Anything can happen。

2.1 volatile读写原子性依据

根据《java并发编程的艺术》,可以把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步。示例代码如下:

class VolatileFeaturesExample {
    //使用volatile声明64位的long型变量
    volatile long vl = 0L;

    public void set(long l) {
        vl = l;   //单个volatile变量的写
    }
    public void getAndIncrement () {
        vl++;    //复合(多个)volatile变量的读/写
    }
    public long get() {
        return vl;   //单个volatile变量的读
    }
}

上面的代码可以等价于:

class VolatileFeaturesExample {
    long vl = 0L;               // 64位的long型普通变量

    //对单个的普通 变量的写用同一个锁同步
    public synchronized void set(long l) {             
       vl = l;
    }

    public void getAndIncrement () { //普通方法调用
        long temp = get();           //调用已同步的读方法
        temp += 1L;                  //普通写操作
        set(temp);                   //调用已同步的写方法
    }
    public synchronized long get() { 
        //对单个的普通变量的读用同一个锁同步
        return vl;
    }
}

其揭示了volatile变量的一个重要特性:volatile的读写操作可以看成是原子的。

2.2 普通变量读写非原子性依据

在java内存模型中,普通变量的读写操作是不具有原子性的,这里我们必须要提到java内存模型的内存交互操作。《深入理解java虚拟机》书中介绍了在java内存模型中,主内存与工作内存之间的具体交互是通过8种操作来完成的,虚拟机保证了这八种操作每一种都是原子的,8中操作如下:

lock(锁定):作用于主内存,它把一个变量标记为一条线程独占状态;
read(读取):作用于主内存,它把变量值从主内存传送到线程的工作内存中,以便随后的load动作使用;
load(载入):作用于工作内存,它把read操作的值放入工作内存中的变量副本中;
use(使用):作用于工作内存,它把工作内存中的值传递给执行引擎,每当虚拟机遇到一个需要使用这个变量的指令时候,将会执行这个动作;
assign(赋值):作用于工作内存,它把从执行引擎获取的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时候,执行该操作;
store(存储):作用于工作内存,它把工作内存中的一个变量传送给主内存中,以备随后的write操作使用;
write(写入):作用于主内存,它把store传送值放到主内存中的变量中。
unlock(解锁):作用于主内存,它将一个处于锁定状态的变量释放出来,释放后的变量才能够被其他线程锁定;

书中还介绍了:如果要把一个变量从主内存复制到工作内存,就要顺序地执行read和load操作(该过程对应全局变量的读操作);如果要把变量从工作内存同步回主内存,就要顺序地执行store和write操作(该过程对应全局变量的写操作)同时,java内存模型只要求上述两个操作必须按顺序执行,而没有保证是连续执行。也就是说,read与load之间、store与write之间是可以插入其它指令的(包括线程的切换),这就是为什么说普通变量的读写操作不具有原子性。

2.3 非原子性例子

如果有两个线程对主内存中的普通变量a进行访问,线程1先对a进行写操作,线程2随后对a进行读操作,则可能出现的顺序是store a(线程1)、read a(线程2)、load a(线程2)、write a(线程1),这个代码执行的结果是线程2读的仍是线程1修改之前的值,而从代码顺序上来看,我们的预期是线程2读到的是线程1修改之后的值,这就导致对变量a的修改不能立刻被其它线程所知道。

从上述过程我们可以得出一个结论,那就是对于一个普通的全局变量来说,组成读写操作的每一条指令是原子的,但读或写操作并不仅仅是由一条指令组成,且jvm中允许在一个操作的多条指令中插入其它指令,这就导致了对普通变量的写操作可能对其它线程不是立即可见的,对某个变量的读操作可能会出现读到过期数据(读期间被修改)的情况。

3 从JMM(Java内存模型)角度看立即可见性

volatile变量的可见性体现在两方面

  • 对一个volatile变量的读总是能看到任意线程对这个变量最后的写,即每次读取volatile变量的时候,会使线程工作内存中的变量副本无效,从而必须从主内存中更新变量值。
  • 对一个volatile变量进行写操作后,会立刻将修改的值同步到主内存,以便于后面读取该变量的线程获取到的是最新的值。

通过第2点可知,volatile变量克服了普通变量读写操作的非原子性弊端,在写回主内存的过程中其它线程不能对变量进行读或写,直到当前线程把变量从主内存读到工作内存或写到主内存为止。通过将读写操作变成原子操作,可以保证:线程1对变量a的写操作若先发生于线程2对变量a的读操作,那么线程2读到的a的值必定是线程1修改过的最新值。

3.1 从操作系统角度看立即可见性

对volatile变量进行的写操作代码,翻译成汇编语言后会发现在写指令的后面会多出另一行汇编代码。lock前缀指令的作用是:

  1. 将当前处理器缓存行中的数据写入到内存中。
  2. 使该数据在其它处理器中的缓存失效。

这样下次其它处理器读取该volatile变量数据时都会从内存中获取,不会直接从缓存中获取。

volatile Object instance = new Singleton();

// 汇编指令
movb addr1, addr2;
lock addl addr1,addr2;    // 多出的汇编代码

4 禁止指令重排序

在jvm中,指令重排序在多个线程同时访问一个普通变量的情况下,可能会导致程序执行结果发生错误,具体例子可以查看我的另一篇文章:指令重排序。为了避免这种错误,可以用volatile变量代替普通变量。

为了实现volatile语义,java内存模型(JMM)会分别限制这两种类型的重排序类型,具体见下表:

第一个操作(行) | 是否能重排序 | 第二个操作(列)普通读/写volatile读volatile写
普通读/写  NO
volatile读NONONO
volatile写 NONO

比如说,当第一个操作是对普通变量的读/写,第二个操作是对volatile变量的写时,不允许第一个操作重排序到第二个操作后面。

为了限制重排序,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,内存屏障的具体知识参见:指令重排序。对于一个编译器来说,发现一个最优不止来最小化插入屏障的总数几乎不可能,为此java内存模型采取保守策略:

  • 每个volatile写操作的前面插入一个StoreStore屏障。
  • 每个volatile写操作的后面插入一个StoreLoad屏障。
  • 每个volatile读操作的后面插入一个LoadLoad屏障。
  • 每个volatile读操作的后面插入一个LoadStore屏障。

上述内存屏障插入策略非常保守,但它可以保证在任意处理器平台,任意的程序中都能得到正确得volatile内存语义。

等等,这里好像有个问题,表中显示,当第一个操作时普通读、第二个操作时volatile写时,重排序不被允许。但volatile的4个内存屏障插入策略似乎不能防止这种情况,因为volatile写操作之前没有插入LoadStore屏障?这个问题留着之后去探究吧。

5 volatile可见性底层实现(缓存一致性MESI)

https://www.sohu.com/a/305919295_458015

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值