看完这篇volatile volatile什么用( 二 )


不过重排序也不是随随便便想重排就重排,发生指令重排序的前提是:在单线程下不影响执行结果、对没有数值依赖的代码进行重排序 。这就是as-if-serial语义 。在多线程情况下有一套更具体的规则,那就是Happens-before 。
Happens-before原则Happens-before由八大原则组成 。

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作(线程的执行结果有序)
  • 锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作
  • volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作
  • 传递规则:如果操作A先行发生于操作B,操作B先行发生于操作C,则可以得出操作A先行发生于操作C
  • 线程启动规则:Thread对象的start()方法先行发生于该线程的其他任何操作
  • 线程中断规则:对线程中断方法interrupt()的调用先行发生于被中断线程检测到中断事件的发生
  • 线程终结规则:线程中所有操作先行发生于线程的终止检测 。通过Thread.join()方法结束、Thread.isAlive()方法的返回值等手段检测到线程已经终止执行 。比如在A线程中调用B.join()方法,B线程执行完成后,B对共享变量的修改,对A来说是可见的
  • 对象终结规则:一个对象的初始化方法完成先行发生于该对象的finalize()方法的开始
如果两个操作不满足上述八大原则中的任意一个,那么这两个操作就没有顺序保证,虚拟机可以对这两个操作进行重排序 。如果操作A happens-before 操作B,那么A在内存所做的修改对B都是可见的 。
volatile了解了JMM之后再来看看volatile 。volatile关键字可以保证可见性和有序性,并不能保证原子性 。
  • 可见性:volatile关键字修饰的变量,如果在某个线程的的工作内存中被修改,会立即写回主内存 。并且写回主内存这个操作会让其他线程工作内存中该变量的副本失效(如果该变量也被其他线程修改了,则那个线程工作内存中的缓存不会失效) 。
  • 有序性:通过插入内存屏障(Memory Barrier)指令禁止在内存屏障前后的指令执行重排序优化 。volatile变量重排序规则表

看完这篇volatile volatile什么用

文章插图
与volatile相关的内存屏障指令有四条:StoreStore、StoreLoad、LoadLoad、LoadStore 。
  • 在每个volatile写操作之前加入StoreStore
  • 在每个volatile写操作之后加入StoreLoad
  • 在每个volatile读操作之后加入LoadLoad
  • 在每个volatile读操作之后加入LoadStore
注意:LoadLoad和LoadStore都是加在volatile操作之后 。
下面是volatile变量写操作时生成的指令示意图 。
看完这篇volatile volatile什么用

文章插图
StoreStore保证普通写操作对共享变量的修改均已刷新回主内存 。
下面是volatile变量读操作时生成的指令示意图 。
看完这篇volatile volatile什么用

文章插图
通过这四条内存屏障,便可以保证有序性 。
volatile与线程安全volatile与synchronized相比,使用更加简便,也更加轻量级 。但是volatile不能替代synchronized,因为仅仅靠一个volatile关键字不能保证线程安全 。举个例子:
public class VolatileDemo { // volatile变量private volatile static int count = 0;public static void main(String[] args) throws InterruptedException {Thread thread1 = new Thread(() -> {for (int i = 0; i < 1000; i++) {count++;}});Thread thread2 = new Thread(() -> {for (int i = 0; i < 1000; i++) {count++;}});thread1.start();thread2.start();// 让thread1、thread2执行完成thread1.join();thread2.join();System.out.println(count);}}
两个线程分别对count变量累加1000次,期望得到的结果是2000,但是运行结果不一定是2000,而是小于等于2000 。说明仅仅靠volatile关键字也不能保证线程安全 。
这个例子线程不安全的原因是count++不是原子操作,可以分为三步:读取count、count值+1、刷新到主内存 。所以存在这样的情况:thread1和thread2都把count变量(假设此时count的值为100)读入自己的工作内存,thread1和thread2也都完成了+1操作,但是还没有刷新到主内存 。此时thread1和thread2工作内存中的值均为101,,thread1把count最新值刷新到主内存,由于thread2工作内存中的值已经修改过了,所以thread2工作内存中的count缓存并不会失效,并且也会同步回主内存 。所以thread1、thread2均同步回主内存后,主内存中的count变量的值是101,而不是102 。
即:在不能保证原子性的情况下,volatile关键字并不能保证线程安全 。