1.volatile变量的可见性

Java虚拟机规范中定义了一种Java内存 模型(Java Memory Model,即JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果。Java内存模型的主要目标就是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的细节。

JMM中规定所有的变量都存储在主内存(Main Memory)中,每条线程都有自己的工作内存(Work Memory),线程的工作内存中保存了该线程所使用的变量的从主内存中拷贝的副本。线程对于变量的读、写都必须在工作内存中进行,而不能直接读、写主内存中的变量。同时,本线程的工作内存的变量也无法被其他线程直接访问,必须通过主内存完成。
image.png

对于普通共享变量,线程A将变量修改后,体现在此线程的工作内存。在尚未同步到主内存时,若线程B使用此变量,从主内存中获取到的是修改前的值,便发生了共享变量值的不一致,也就是出现了线程的可见性问题。

主内存和本地内存间的交互

主内存和本地内存的交互即一个变量是如何从主内存中拷贝到本地内存又是如何从本地内存中回写到主内存中的实现,Java内存模型提供了8中操作来完成主内存和本地内存之间的交互。它们分别如下:

  • lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才能被其它线程锁定。
  • read(读取):作用于主内存的变量,它把一个变量从主内存传输到线程的本地内存中,以便随后的load动作使用。
  • load(载入):作用于本地内存的变量,它把read操作从主内存中的到的变量值放入本地内存的变量副本中。
  • use(使用):作用于本地内存的变量,它把本地内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量值的字节码指令时将会执行这个操作。
  • assign(赋值):作用于本地内存的变量,它把一个从执行引擎接收到的变量赋予给本地内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时将会执行这个操作。
  • store(存储):作用于本地内存的变量,它把本地内存中的变量的值传递给主内存中,以便后面的write操作使用。
  • write(写入):作用于主内存的变量,它把store操作从本地内存中得到的变量的值放入主内存的变量中。

从上面8种操作中,我们可以看出,当一个变量从主内存复制到线程的本地内存中时,需要顺序的执行read和load操作,当一个变量从本地内存同步到主内存中时,需要顺序的执行store和write操作。Java内存模型只要求上述的2组操作是顺序的执行的,但并不要求连续执行。比如对主内存中的变量a 和 b 进行访问时,有可能出现的顺序是read a read b load b load a。除此之外,Java内存模型还规定了在执行上述8种基本操作时必须满足以下规则:

  • 不允许read和load,store和write操作单独出现,这2组操作必须是成对的。
  • 不允许一个线程丢弃它最近的assign操作。即变量在线程的本地内存中改变后必须同步到主内存中。
  • 不允许一个线程无原因的把数据从线程的本地内存同步到主内存中。
  • 不允许线程的本地内存中使用一个未被初始化的变量。
  • 一个变量在同一时刻只允许一个线程对其进行lock操作,但是一个线程可以对一个变量进行多次的lock操作,当线程对同一变量进行了多次lock操作后需要进行同样次数的unlock操作才能将变量释放。
  • 如果一个变量执行了lock操作,则会清空本地内存中变量的拷贝,当需要使用这个变量时需要重新执行read和load操作。
  • 如果一个变量没有执行lock操作,那么就不能对这个变量执行unlock操作,同样也不允许unlock一个被其它线程执行了lock操作的变量。也就是说lock 和unlock操作是成对出现的并且是在同一个线程中。
  • 对一个变量执行unlock操作之前,必须将这个变量的值同步到主内存中去。

volatile定义:

  • 当对volatile变量执行写操作后,JMM会把工作内存中的最新变量值强制刷新到主内存
  • 写操作会导致其他线程中的缓存无效
    这样,其他线程使用缓存时,发现本地工作内存中此变量无效,便从主内存中获取,这样获取到的变量便是最新的值,实现了线程的可见性

2.volatile变量的禁止指令重排序

volatile是通过编译器在生成字节码时,在指令序列中添加内存屏障来禁止指令重排序的。

硬件层面的内存屏障:

  • sfence:即写屏障(Store Barrier),在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存,以保证写入的数据立刻对其他线程可见
  • lfence:即读屏障(Load Barrier),在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据,以保证读取的是最新的数据。
  • mfence:即全能屏障(modify/mix Barrier ),兼具sfence和lfence的功能
  • lock 前缀:lock不是内存屏障,而是一种锁。执行时会锁住内存子系统来确保执行顺序,甚至跨多个CPU。

JMM层面的内存屏障:

  • 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原理看这一篇就够了