线程安全之volatile关键字

仅做自己学习记录,有问题望诸位支出

是什么:

        因为java的内存模型并不能使一个线程以程序执行的顺序查看到另一个线程对共享变量的修改,除非两个线程都跨越了同一个内存屏障;而volatile就是保证可见性;

        是轻量级的sychronized,但是没有加锁操作;

        目的就是为了保证可见性

        用于标记一个变量应该保存在主存中,即每次读取到volatile关键字的时候,都是从内存中读取,而不是从CPU缓存中读取;

        每次写入到一个volatile变量的时候,都需要保存到主存中,而不是仅仅写到CPU缓存中;

完整的volatile的可见性保证:

        其意味着,当一个变量被volatile修饰的时候,线程中的所有可见变量也会写到主存

        如以下例子:days被volatile修饰,在写入days的时候,years和months也会被写到主存中去;

public class MyClass {
        private int years; 
        
        private int months; 
        
        private volatile int days;
        
        public void update(int years, int months, int days){
            this.years = years; 
            this.months = months;
            this.days = days; 
        } 
    }

        可以将对volatile变量的读写理解为一个触发刷新的操作,写入volatile变量时,线程中的所有变量也都会触发写入主存。而读取volatile变量时,也同样会触发线程中所有变量从主存中重新读取;

        因此在使用volatile的时候,写入的时候放在最后,读取的时候放在最前;这样就能保证其他变量也能写入到主存中或着从主存中读取到最新的值;如果写入放在最前边,则前边的值可能已经写入到缓存中去,而不是主存中,读取相似;

为什么用:

        volatile保证内存可见性防止指令重排

 1. 可见性:

        当一个线程修改一个共享变量时,另外一个线程能够读到修改的值;

        Java 编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排它锁单独获得这个变量

2. 防止指令重排:

        JVM和CPU允许对程序中的指令进行重新排序,只要保证重排后的指令保持一致就可以;

        指令问题会导致volatile关键字顺序发生改变,造成该线程某些变量无法写入到主存/无法从主存中读取值;               

 int a = 1;
 int b = 2; 
 a++; 
 b++ 
// 排序后的代码:只改变了顺序,则不改变程序的语义; 

 int a = 1;
 a++; 
 int b = 2;
 b++;

怎么用:

        如果只有一个线程读写一个值的时候,volatile就可以保证其读取到的是最新值;

        当两个线程同时读取一个值的时候,仅仅volatile还不够,还需要synchronized来保证读写变量是原子的。

JUC包提供的原子数据类型:如Atomic Long,Atomic Reference等;

volatile关键字对32位和64位变量都有效

问题:

1.可见性:

  • 首先,volatile 关键字修饰的共享变量可以提供这种可见性规范,也叫做读写可见。
  • 被 volatile 关键字修饰的共享变量在转换成汇编语言时,会加上一个以 lock 为前缀的指令,当CPU发现这个指令时,立即将当前内核高速缓存行的数据回写到内存,同时使在其他内核里缓存了该内存地址的数据无效。
  • 另外,在早期的 CPU 中,是通过在总线加 LOCK# 锁的方式实现的,但这种方式开销较大。所以Intel开发了缓存一致性协议,也就是 MESI 协议,该解决缓存一致性。
  • volatile 的好处,volatile 是一种非锁机制,这种机制可以避免锁机制引起的线程上下文切换和调度问题。所以,volatile 的执行成本比 synchronized 更低。
  • volatile 的不足,volatile 关键字只能保证可见性,不能保证原子性操作
  • Unsafe.loadFence(); 保证在这个屏障之前的所有读操作都已经完成

2. 指令重排问题:

  • 从字节码层面,添加 ACC_VOLATILE,在汇编指令的打印会有 lock addl $0x0,(%rsp)s
  • 从 JVM 层面,JMM 提供了8个 Happen-Before 规则来约束数据之间竞争、4个内存屏障 (LL LS SL SS)和As-if-serial
  • 从硬件层面,sfence、lfence、mfence

解决:Happends-before

原则内容:

  • 如果有读写操作发生在写入volatile变量之前,读写其他变量的指令不能重排到写入volatile变量之后。写入一个volatile变量之前的读写操作,对volatile变量是有happens-before保证的。注意,如果是写入volatile之后,有读写其他变量的操作,那么这些操作指令是有可能被重排到写入volatile操作指令之前的。但反之则不成立。即可以把位于写入volatile操作指令之后的其他指令移到写入volatile操作指令之前,而不能把位于写入volatile操作指令之前的其他指令移到写入volatile操作指令之后。
  • 如果有读写操作发生在读取volatile变量之后,读写其他变量的指令不能重排到读取volatile变量之前。注意,如果是读取volatile之前,有读取其他变量的操作,那么这些操作指令是有可能被重排到读取volatile操作指令之后的。但反之则不成立。即可以把位于读取volatile操作指令之前的指令移到读取volatile操作指令之后,而不能把位于读取volatile操作指令之后的指令移到读取volatile操作指令之前。

即JVM不管怎么去禁止/允许某些情况下的指令重排,最终就是保证“完整的volatile可见性保证”;

性能影响:

1. 因为从主存读写比从缓存中读写更加昂贵;

2. 访问一个volatile会导致指令不会重排,而指令重排是提升性能的技术;

总结:

1. volatile修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值,比如booleanflag;或者作为触发器,实现轻量级同步。

2. volatile属性的读写操作都是无锁的,它不能替代synchronized,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁_上,所以说它是低成本的。

3. volatile只能作用于属性,我们用volatile修饰属性,这样compilers就不会对这个属性做指令重排序。

4. volatile提供了可见性,任何一个线程对其的修改将立马对其他线程可见,volatile属性不会被线程缓存,始终从主 存中读取。

5. volatile提供了happens-before保证,对volatile变量v的写入happens-before所有其他线程后续对v的读操作。

6. volatile可以使得long和double的赋值是原子的。

7. volatile可以在单例双重检查中实现可见性和禁止指令重排序,从而保证安全性

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值