Volatile关键字

  Java语言提供了一种稍弱的同步机制,即Volatile变量,用来确保变量的更新操作通知到其他线程。volatile变量不会被缓存在寄存器或对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

Volatile变量具备两种特性:

变量可见性就是保证该变量对所有线程可见,可见性是指当一个线程修改了某个变量的值,那么新值对与其他线程是可以立即获取的。

  第一,使用volatile关键字会强制将修改的值立即写入主存;

  第二,使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);

  第三,由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。

  那么,在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。

  那么线程1读取到的就是最新的正确的值。   

禁止指令重排序,阻止编译器对代码的优化;

volatile关键字禁止指令重排序有两层意思:

  1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

  2)在进行指令优化时,不能把volatile变量前面的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

  为了保证volatile的内存语义,加入volatile关键字时,编译器在生成字节码时,会在指令序列中插入内存屏障,会多出一个lock前缀指令。内存屏障是一组处理器指令,解决禁止指令重排序和内可见性的问题。编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。处理器在进行重排序时会考虑指令之间的数据依赖性。

  内存屏障,有2个作用:1、优先于这个内存屏障的指令必须先执行,后于这个内存屏障的指令必须后执行。2、使得内存可见性。所以,如果你的字段是volatile,在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据。在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存。

  lock前缀指令,在多核处理器下回引发的两件事情:

  1)将当前处理中这个变量所在缓存行的数据会写回到系统内存。这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。但是就算写回到内存,如果其他处理器缓存得知还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在主线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据及逆行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。

  2)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。

内存屏障可以被分为以下几种类型:

  LoadLoad屏障:对于这样的语句Load1;LoadLoad;Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

  StoreStore屏障:对于这样的语句Store1;StoreStore;Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其他处理器可见。

  LoadStore屏障:对于这样的语句Load1;LoadStore;Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。

  StoreLoad屏障:对于这样的语句Store1;StoreLoad;Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种平章中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其他三种内存屏障的功能。

 

在访问volatile变量时不会执行加锁操作,所以不会使线程阻塞,因此volatile是比synchronized关键字更轻量级的同步机制。

  volatile适合场景:一个变量被多个线程共享,线程直接给这个变量赋值。

            

  当对非volatile变量进行读写时,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的CPU cache中。而声明变量是volatile的,JVM保证了每次读变量都从内存中读,跳过CPU cache这一步。

适用场景:

  对于volatile变量的单次读写操作可以保证原子性的,如long和double类型变量,但是并不能保证i++这种操作的原子性,因为本质上i++是读、写两次操作。在某些场景下可以代替synchronized。但是volatile并不能完全取代synchronized的位置,只有在一些特殊的场景下,才能适用volatile。总的来说,必须同时满足下面两个条件才能保证在并发环境的线程安全。

  1)对变量的写操作不依赖于当前值(比如i++),或者说是单纯的变量赋值(boolean flag=true)。

  2)该变量没有包含在具有其他变量的不变式中,也就是说,不同的volatile变量之间,不能互相依赖。只有在状态真正独立于程序内其他内容时才能使用volatile。

posted @ 2019-11-22 12:29  MrHH  阅读(246)  评论(0编辑  收藏  举报