关于volatile的一些知识

什么是volatile,它的作用是什么?

  • java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁更加方便。如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是一致的。

volatile有什么特性?

  • 可见性 : 对一个volatile的变量的读,总是能看到任意线程对这个变量最后的写入.

  • 原子性 : 对于单个volatile变量的读或者写具有原子性,复合操作不具有.(如i++)

  • 互斥性 : 同一时刻只允许一个线程对变量进行操作.(互斥锁的特点)

volatile缓存可见性的实现原理是什么?

volatile通过汇编lock前缀指令来实现,他会锁定这块内存区域的缓存(缓存行锁定)并回写到主内存。

lock指令的作用是什么?

  • 将当前处理器缓存行的数据立即写回到系统内存
  • 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效(通过MESI协议实现)
  • 提供内存屏障功能,使lock前后指令不能重复

缓存一致性协议(MESI)

多个CPU从主内存读取同一个数据到各自的高速缓存,当其中某个CPU修改了缓存里的数据,该数据会马上同步回主内存,其他CPU通过总线嗅探机制可以感知到数据的变化从而自己缓存里的数据失效。

内存屏障功能是什么?

  • CPU的乱序执行

    • CPU由于处理速度远远大于内存处理的速度,为了提高效率,通过引入多级高速缓存器的方式来提高行效率,并且为了使效率更高,采用了指令的乱序执行。也就是传输的指令顺序和执行的指令顺序并不同。
  • 对象创建的半初始化

    • 而这样也导致了另外一个问题,在高级语言层面的一些语句往往由多条语句组成,而这多条语句的执行却是乱序的,我们以new为例,其顺序执行过程如下:

      1. 在堆空间中申请一块内存

      2. 通过Invokespecial调用构造方法进行初始化

      3. 在栈中创建instance并通过aloac 0引用指向。

    • 在乱序执行中,可能执行了过程1和过程3,还没有来得及执行过程2,对象就被调用。这是由于对象没有进行初始化就会产生错误。

  • volatile的内存屏障功能的作用

    • 内存屏障功能相当于在aload指令前加锁(lock),使得过程2没有调用之前不允许调用过程3。这样就保证了new过程的完整。

多线程中CPU的读取数据流程

  • CPU读取数据的状态介绍:

    • read(读取):从主内存读取数据
    • load(载入):将主内存读取到的数据写入工作内存
    • use(使用):从工作内存读取数据来计算
    • assign(赋值):将计算好的值重新赋值到工作内存中
    • store(存储):将工作内存数据写入主内存
    • write(写入):将store过去的变量值赋值给主内存
    • lock(锁定):将主内存变量加锁,标识为线程独占状态
    • unlock(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量
  • 在没有使用lock指令时的工作流程

    image-20201206143605968

  • 在使用了lock指令后的工作流程

    image-20201206144428468

volatile的用途

  • 单例模式的双重检查

    • 在单例模式中有通过双重检查实现的方式,在双重检查的过程中为防止指令乱序导致线程不安全,使用了volatile。

    • private static volatile Instance instance;
      
      public static Instance getInstance() {
          if(instance == null) {
              synchronized (this) {
                  if(instance == null) {
                      instance = new Instance();
                  }
              }
          }
      
          return instance;
      }
      

Q.E.D.


If you don't come, I will snow.