java并发编程笔记之volatile关键字


Volatile关键字的作用主要有如下两个:
1. 线程的可见性:当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
2. 顺序一致性:禁止指令重排序。

volatile自身的特性

理解volatile特性的一个好方法是把volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步

  • 可见性:对于一个volatile变量的读,总是能看到任意线程对这个volatile变量最后的写。
  • 原子性:对任意单个volatile变量的读/写具有原子性(即使是64位的long型和double型变量),但类似于volatile++这种复合操作不具有原子性

volatile的内存语义

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

线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。

volatile内存语义的实现

JMM在执行程序语句时,出于优化的考虑可能会对执行语句进行重排序,而为了实现volatile内存语义,JMM会对重排序做一定的限制。
在这里插入图片描述

  • 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。【这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后】
  • 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。【这个规则确保volatile读之后的操作不会被编译器重排序到volatile写之前】
  • 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。

【问题】:为什么要禁止volatile读/写和普通读/写间的重排序?
引用自 https://blog.csdn.net/Unknownfuture/article/details/105023355

volatile int a = 0;
int b = 1;
public void A (){
    b = 2;    // 1 普通写
    a = 1;      // 2 volatile写
}
public void B() {
    int c = 0;
    if (a == 1)    // 3  volatile读
        c = b;      // 4 普通写
} 

假如不禁止

  1. 那么由于代码1和代码2两处没有数据依赖性,所以二者是可以重排序的。
  2. 我们假设代码2在代码1之前被执行,此时由于a是volatile变量,所以将a = 1, b = 1刷新进入主内存;
  3. 如果这时候方法A所在的线程cpu时间片用完了,轮到了方法B在另一个线程中执行,由于a是volatile变量所以代码3处执行的时候会将b = 1, a = 1从主内存中读出,此时代码4再执行的话c会变为1,而不是预想的2(按照书写的顺序来看,a=1发生在b=2之后)。

发生这种错误的原因在于:volatile变量写操作与在其之前的代码发生了重排序,使得刷新内存的时机提早了,可能会漏掉我们写在volatile变量赋值操作之前的那些共享变量的修改。
而这例子也对应:
第一个操作为普通读/写,第二个操作为volatile写,也验证了确实是不能重排序,因为是可能会产生影响的。

换成方法B先执行,代码3和4由于happens-before中的猜测执行机制会重排,用同样的思路可以推出volatile读

内存屏障

为了实现volatile的内存语义,编译器在生成字节码时,会在指今序列中插人内存屏障来禁止特定类型的处理器重排序。

  • 在每个volatile写操作的前面插入一个StoreStore屏障。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadStore屏障。
    在这里插入图片描述
    这里比较有意思的是, volatile写后面的StoreLoad屏障。此屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序。因为编译器常常无法准确判断在一个volatile写的后面是否需要插入一个StoreLoad屏障。为了保证能正确实现volatile的内存语义,JMM在采取了保守策略:在每个volatile写的后面,或者在每个volatile读的前面插人一个StoreLoad屏障。
    从整体执行效率的角度考虑,JMM最终选择了在每个volatile写的后面插入一个StoreLoad屏障因为volatile写-读内存语义的常见使用模式是:一个写线程写volatile变量,多个读线程读同一个volatile变量。当读线程的数量大大超过写线程时,选择在volatile写之后插入StoreLoad屏障将带来可观的执行效率的提升。
    在这里插入图片描述
    上述volatile写和volatile读的内存屏障插入策略非常保守。在实际执行时,只要不改变volatile写-读的内存语义,编译器可以根据具体情况省略不必要的屏障。

volatile关键字与Synchronized比较

引用 https://blog.csdn.net/xinghui_liu/article/details/124379221

  1. Volatile是轻量级的synchronized,因为它不会引起上下文的切换和调度,所以Volatile性能更好。
  2. Volatile只能修饰变量,synchronized可以修饰方法,静态方法,代码块。
  3. Volatile对任意单个变量的读/写具有原子性,但是类似于i++这种复合操作不具有原子性。而锁的互斥执行的特性可以确保对整个临界区代码执行具有原子性。
  4. 多线程访问volatile不会发生阻塞,而synchronized会发生阻塞。
  5. volatile是变量在多线程之间的可见性,synchronize是多线程之间访问资源的同步性。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值