Volatile:可见性保证+禁止指令重排

Volatile

    • 1.可见性保证
        • 1.1 何为可见性
        • 1.2 JAVA内存模型
        • 1.3 voletile的实现原理
        • 1.4.synchronized 关键字和 volatile 关键字的区别
    • 2.禁止指令重排

volatile是java语言中的关键字,用来修饰会被多线程访问的共享变量,是JVM提供的轻量级的同步机制,相比同步代码块或者重入锁有更好的性能。
它主要有两重个作用,一是保证多个线程对共享变量访问的可见性,二防止指令重排序

1.可见性保证

1.1 何为可见性

volatile在Java中能够保证变量对多线程的可见性,那么何为可见性?
在Java8语言规范中的定义:
volatile Fields: A field may be declared volatile, in which case the Java Memory Model ensures that all threads see a consistent value for the variable .
大意为:被volatile修饰的变量,Java内存模型确保所有线程可以看到变量的一致值。

比如说一个线程对某个变量进行修改,其他线程能够立刻看到,即为可见。
而通常情况下,可见性是不能得到保证的,即当一个线程对某个变量修改后,其他线程并不能立刻看到
究其原因,这里涉及到java软件层面的内存模型(JMM);

1.2 JAVA内存模型

java内存模型(即Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例变量,静态字段和构成数组对象的元素)的访问方式。

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,栈空间对其他线程不共享,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:
Volatile:可见性保证+禁止指令重排_第1张图片

所以造成可见性问题在JMM层面的解释为:
每个线程对主内存共享变量的操作都是通过各个线程将主内存中的变量拷贝到自己的栈空间进行操作后再写入主内存中。即线程操作的不是主内存中的变量,而是一个副本

  • 当线程t1修改了变量a的值还未写回主内存时,另一个线程t2又对变量a进行操作,而此时t1线程中的变量a对线程t2来说是不可见的,这就导致了可见性问题 !
1.3 voletile的实现原理
  • 读一个变量时,JMM会把该线程对应的本地内存置为无效,并从主内存中重新读取共享变量。
  • 写一个volatile变量时,JMM首先修改工作内存中的变量值,然后立即刷新到主内存中。

对volatile变量的读写,可以说都是直接对主内存进行的操作,这样虽然会牺牲一些性能,但是解决了“可见性问题”,使得变量在多线程间的可见性得到了很好的保证。

而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

1.4.synchronized 关键字和 volatile 关键字的区别
  1. volatile关键字是线程同步的轻量级实现,所以volatile性能肯定⽐synchronized关键字要好。但是volatile关键字只能⽤于变量⽽synchronized关键字可以修饰⽅法以及代码块。synchronized关键字在JavaSE1.6之后进⾏了主要包括为了减少获得锁和释放锁带来的性能消耗⽽引⼊的偏向锁和轻量级锁以及其它各种优化之后执⾏效率有了显著提升,实际开发中使⽤synchronized 关键字的场景还是更多⼀些。
  2. 多线程访问volatile关键字不会发⽣阻塞,而synchronized关键字可能会发⽣阻塞
  3. volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证
  4. volatile关键字主要用于解决变量在多个线程之间的可见性,而synchronized关键字解决的是多个线程之间访问资源的同步性

2.禁止指令重排

为了优化程序性能,编译器和处理器会对java编译后的字节码和机器指令进行重排序,通俗的说代码的执行顺序和我们在程序中定义的顺序会有些不同,
在单线程环境下的执行结果不会改变但是在多线程环境下,结果可能会改变

在多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。这样就需要通过volatile来修饰,来保证线程安全性。

Volatile实现禁止指令重排优化,从而避免了多线程环境下程序出现乱序执行的现象。

Volatile针对指令重排做了什么:
首先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:

  1. 保证特定操作的顺序
  2. 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)

由于编译器和处理器都能执行指令重排的优化,如果在指令间插入一条Memory Barrier,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行;

【写操作之前】插入 StoreStore 屏障,指令后面的要执行前,必须让指令前面的指令都执行完并写到了主存;
【写操作之后】插入 StoreLoad 屏障,让指令前面的变量刷新到主存,然后后面的才能读;
禁止CPU对指令的重排序越过这些屏障。也就是说 通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化

四大屏障:
Volatile:可见性保证+禁止指令重排_第2张图片

参考:
https://blog.csdn.net/sltylzx/article/details/89812543
https://www.dazhuanlan.com/jkeylu/topics/1069961
https://www.cnblogs.com/takumicx/p/9302398.html
http://snailclimb.gitee.io/

你可能感兴趣的:(Java,java,开发语言,后端)