关于Volatile见解

1.基础

volatile就可以说是java虚拟机提供的最轻量级的同步机制

(1)Synchronized:保证可见性和原子性

    Synchronized能够实现原子性和可见性;在Java内存模型中,synchronized规定,线程在加锁时,先清空工作内存→在主内存中拷贝最新变量的副本到工作内存→执行完代码→将更改后的共享变量的值刷新到主内存中→释放互斥锁。

 

(2)Volatile:保证可见性,但不保证操作的原子性

    Volatile实现内存可见性是通过store和load指令完成的;也就是对volatile变量执行写操作时,会在写操作后加入一条store指令,即强迫线程将最新的值刷新到主内存中;而在读操作时,会加入一条load指令,即强迫从主内存中读入变量的值。但volatile不保证volatile变量的原子性.

 

   一般在多线程中使用volatile变量,为了安全,对变量的写入操作不能依赖当前变量的值:如Num++或者Num=Num*5这些操作。这个问题在阿里巴巴开发手册(泰山版)中给出了几个解决方案,以供参考。

  

 

原子性

可见性

volatile

不能

可以

synchronized

可以

可以

 

比较:

    1)Volatile不需要加锁,比Synchronized更轻量级,并不会阻塞线程(volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。)

    2)volatile标记的变量不会被编译器优化,而synchronized标记的变量可以被编译器优化(如编译器重排序的优化).

    3)volatile是变量修饰符,仅能用于变量,而synchronized是一个方法或块的修饰符。

      volatile本质是在告诉JVM当前变量在寄存器中的值是不确定的,使用前,需要先从主存中读取,因此可以实现可见性。而对n=n+1,n++等操作时,volatile关键字将失效,不能起到像synchronized一样的线程同步(原子性)的效果。

 

JAVA内存模型(jmm):各个线程会将共享变量从主内存中拷贝到工作内存,然后执行引擎会基于工作内存中的数据进行操作处理

 

volatile的store指令:

  1. 将当前处理器缓存行的数据写回系统内存;
  2. 这个写回内存的操作会使得其他CPU里缓存了该内存地址的数据无效

 

首先了解下现代计算机模型:

 

 

 

所有的共享变量都存储于主内存,这里所说的变量指的是实例变量和类变量,不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。

每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。

线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量。

不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存中转来完成。

 

关键:

       就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了。当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
       当然原则上说,volatile每次使用都会从主存中重新copy一份副本。

 

2.关于并发问题

缓存一致性--MESI

如果某个cpu在操作变量的时候发现该变量是voliate的时候,会发送信号给其他cpu,让其他cpu知道他们手上的该变量副本已经过期,如果需要要向cpu重新申请copy一份。

如何发现无效,过期的?

         每个处理器上有嗅探在总线传播上发现是否过期,当发现缓存地址发生改变则被视为过期,会重新加载这个数据

为什么提倡不要常用voliate?

        由上可见,需要不断的从内存中复制信息和嗅探 cas循环,则总线占用率会非常高。

        因此voliate需要慎用,一般主张在修改量小的变量进行使用。

 

3.内存屏障

内存屏障

JMM内存屏障分为四类见下图,

java编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。为了实现volatile的内存语义,JMM会限制特定类型的编译器和处理器重排序,JMM会针对编译器制定volatile重排序规则表:

 

 

StoreStore屏障:禁止上面的普通写和下面的volatile写重排序;

StoreLoad屏障:防止上面的volatile写与下面可能有的volatile读/写重排序

LoadLoad屏障:禁止下面所有的普通读操作和上面的volatile读重排序

LoadStore屏障:禁止下面所有的普通写操作和上面的volatile读重排序

 

 

 

 

 

 

 

4.总结

  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的赋值是原子的。

(因为会保证是一次性读写(因为内存屏障)

对于64位的long和double,如果没有被volatile修饰,那么对其操作可以不是原子的。 在操作的时候,可以分成两步,每次对32位操作。 如果使用volatile修饰long和double,那么其读写都是原子操作 对于64位的引用地址的读写,都是原子操作 在实现JVM时,可以自由选择是否把读写long和double作为原子操作 推荐JVM实现为原子操作 )

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

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值