谈谈我对volatile的理解

文章目录

  • 引言
    • 什么是指令重排
  • volatile的作用及原理
    • Java内存模型JMM
    • 作用
    • 底层实现-内存屏障
  • 其他相关概念
      • 缓存行对齐
      • 指令重排
      • MESI协议

引言

什么是指令重排

大家在写懒汉单例模式的时候,一定见过这种double check lock的写法

public class Singleton05 {
    private static volatile Singleton05 instance;
    private Singleton05(){};

    public static Singleton05 getInstance(){
        if (instance == null) {
            synchronized (Singleton05.class) {
                if (instance == null) {//没有第二个判断会创建多个对象
                    instance = new Singleton05();
                }
            }
        }
        return instance;
    }
}

那么这里把instance变量加上volatile修饰有什么作用呢?

instance = new Singleton05();这行代码是由内部多行指令构成的。jvm可能会进行优化,打乱多条指令的顺序,导致先给instance赋值,然后再执行构造方法。

  • 若没有volatile
    假设有t1,t2两个线程,t1先进入临界区,执行到上述代码并发生了指令重排,此时t2进行了第一个if条件的判断,此时instance并不为空,便向下执行返回了instance,但是此时t1线程对instance的构造还没完成,导致t2获得的instance是未构造完成的对象。
  • 加了volatile
    volatile禁止了指令重排,这行代码一定是先执行完构造再赋值的,而且保证了可见性,其他线程读的instance的数据是最新的,不会被缓存影响。

volatile的作用及原理

现在我们知道volatile可以禁止指令重排序,那么它还有什么其他的作用呢?

Java内存模型JMM

要知道volatile原理必须了解什么是JMM,这里先简述一下:
Java 内存模型规定所有的变量都是存在主存当中(类似于物理内存),每个线程都有自己的工作内存(类似于高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。

作用

由上可知,volatile可以保证可见性、有序性,但不能保证原子性。它的作用为

  • 保证指令不会受到上下文切换的影响

  • 保证指令不会受cpu缓存的影响

  • 保证指令不会受cpu指令并行优化的影响

关于为什么不能保证原子性可以看以下例子:多个线程读取变量i并进行i++操作
i++可以拆分为三个步骤: 读取变量i => temp = i + 1 => i = temp
当 i=5 的时候A,B两个线程同时读入了 i 的值, 然后A线程执行了 temp = i + 1的操作, 要注意,此时的 i 的值还没有变化,然后B线程也执行了 temp = i + 1的操作,注意,此时A,B两个线程保存的 i 的值都是5,temp 的值都是6, 然后A线程执行了 i = temp 的操作,此时i的值(等于6)会立即刷新到主存并通知其他线程保存的 i 值失效, 此时B线程需要重新读取 i 的值那么此时B线程保存的 i 就是6,同时B线程保存的 temp 还仍然是6, 然后B线程执行 i=temp (6),所以导致了计算结果比预期少了1

但是在以下两个场景中可以使用volatile来代替synchronized

1、运算结果并不依赖变量的当前值,或者能够确保只有单一的线程会修改变量的值。

2、变量不需要与其他状态变量共同参与不变约束。

底层实现-内存屏障

可见性

写屏障:在该屏障之前会把共享变量的改动都同步到主存中

读屏障:在该屏障之后对共享变量的读取都是从主存中获得

读写屏障不能解决指令交错,写屏障不能保证之后的读操作跑到写屏障之前,而是保证屏障之后的每次读都能读到最新值

有序性也只能保证本线程内volatile相关代码不被重排

final原理就是加了写屏障,如果一个静态变量使用final关键字,会直接复制一份值使用,否则会getStatic去获取值。

当volatile修饰的变量进行写操作的时候,JVM就会向CPU发送LOCK#前缀指令,此时当前处理器的缓存行就会被锁定,通过缓存一致性机制确保修改的原子性,然后更新对应的主存地址的数据。

处理器会使用嗅探技术保证在当前处理器缓存行,主存和其他处理器缓存行的数据的在总线上保持一致。在JVM通过LOCK前缀指令更新了当前处理器的数据之后,其他处理器就会嗅探到数据不一致,从而使当前缓存行失效,当需要用到该数据时直接去内存中读取,保证读取到的数据时修改后的值。

其他相关概念

缓存行对齐

一个缓存行大小是64Byte,读取缓存会按照缓存行读取。当被volatile修饰的多个变量存在同一个缓存行且其中一个被修改时,CPU会通知另一个线程重新读取这个缓存行。如果一个类中填充一些变量使这个类被存在不同缓存行就会提高效率

指令重排

CPU会自动重排序数条前后执行顺序对运行结果无影响的指令,由于一条指令可以被分成五个部分,CPU为了吞吐量把使用不同资源的部分串行执行,这就是指令流水技术。

MESI协议

CPU各主存之间的缓存,以行为单位。缓存行大小: 64byte,缓存行单位: cell

缓存行伪共享,如果两个cell处于一个缓存行,其中cell[1]被改变,则另一个cpu的缓存行会失效,如果此时另一个cpu准备对cell[2]改变,则必须重新去内存获取值

@Contend可以解决这个问题,在注解的对象前后增加128byte的padding,则cpu将对象预读至缓存时会占用不同的缓存行,这样不会造成缓存行的失效

MESI中每个缓存行都有四个状态,分别是E(exclusive)、M(modified)、S(shared)、I(invalid)。

M:代表该缓存行中的内容被修改了,并且该缓存行只被缓存在该CPU中。这个状态的缓存行中的数据和内存中的不一样,在未来的某个时刻它会被写入到内存中(当其他CPU要读取该缓存行的内容时,或者其他CPU要修改该缓存对应的内存中的内容时)。

E:E代表该缓存行对应内存中的内容只被该CPU缓存,其他CPU没有缓存该缓存对应内存行中的内容。这个状态的缓存行中的内容和内存中的内容一致。该缓存可以在任何其他CPU读取该缓存对应内存中的内容时变成S状态。或者本地处理器写该缓存就会变成M状态。

S:该状态意味着数据不止存在本地CPU缓存中,还存在别的CPU的缓存中。这个状态的数据和内存中的数据是一致的。当有一个CPU修改该缓存行对应的内存的内容时会使该缓存行变成 I 状态。

I:代表该缓存行中的内容时无效的。

你可能感兴趣的:(后端必知必会,java,单例模式,jvm,多线程)