volatile关键字解析

volatile 保证此变量对所有线程的可见性。

这个可见性是指,当一个线程读取volatile修饰的变量时,永远读取的都是最后一个线程写回主内存的最新值。某个线程在读取数据之后,另一个线程对变量值做了修改,这个线程是不知道的,这就导致当前线程读取的值是过期的,当前线程将过期的数据经过计算写回主内存时,就会出现问题。看下面代码:

public class VolatileTest {
    public static volatile int race = 0;

    /**
     * 每次都对race累加
     */
    public static void increase() {
        race++;
    }

    private static int THREAD_COUNT = 20;

    public static void main(String[] args) {
        // start 20 thread, every thread invoke increase function 20 times
        for (int tCount = 0; tCount < THREAD_COUNT; tCount++) {
            new Thread(() -> {
                for (int i = 0; i < 10000; i++) {
                    increase();
                }
                System.out.println(Thread.currentThread().getName() + "is finished");
            }).start();
        }

        /**
         * 当还存在线程运行时,不结束主线程。
         */
        while (Thread.activeCount() > 1) {
            System.out.println("main thread yield, the active count is " + Thread.activeCount());
            Thread.yield();
        }
        // in theory, the result is 20 * 10000 = 200000
        System.out.println(race);
    }
}

看结果,race变量的值基本上不会是20000。
javap -verbose 类文件, 查看方法的字节码片段
给出方法编译的字节码

  public static void increase();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #2                  // Field race:I
         3: iconst_1
         4: iadd
         5: putstatic     #2                  // Field race:I
         8: return
      LineNumberTable:
        line 14: 0
        line 15: 8

字节码还是变成了几个命令行,假如当前线程执行了getstatic时volatile修饰的变量是正确的,当在执行iconst_1和iadd时,其他线程可能将主内存中的race值加大了,当前线程将变量写回主内存时,就会覆盖之前的值,导致race的值一直累加不到20000。
通过以上分析,发现如果需要保证方法的原子性还是需要使用synchronized或者java.util.concurrent中的原子类。

volatile 的第二个作用是禁止指令重排优化。

先看一段代码:

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

      public static Singleton getInstance() {
          if (instance == null) {
              synchronized (Singleton.class) {
                  if (instance == null) {
                      instance = new Singleton();
                  }
              }
          }

          return instance;
      }
  }

上述代码是基于双锁检测实现的单例模式。对比instance添加volatile修饰符前后汇编代码:
添加volatile之前,

添加volatile之后,

发现添加volatile修饰符后,汇编多生成了一条

0x0000000002b83350: lock add dword ptr [rsp],0h

add dword ptr [rsp],0h,意思是将双字节的栈指针寄存器加0,这句代码没有问题,关键是其前面的修饰符lock。lock的作用使得本CPU的Cache写入内存,该写入动作也会引起其他CPU或者核无效化,这样就保证了本次值的修改对其他CPU可见了。

lock是如何禁止指令重排的呢?指令重排是指CPU允许多条执行不按程序规定顺序执行。但如果指令之间如果有依赖,那么指令就不能重排。例如一个指令,给地址A加1,另一个指令为将地址A的数据乘以2,还有另一条指令是将B地址的数据加5。可以看出指令1和指令2之间是有依赖关系的,不能重排,但是对于指令3 将B地址的数据加5,和指令1和指令2没有依赖关系的,所以可以重排,而且重排完成后依然是有序的。当使用lock指令,将计算的数据更新到主内存后,意味着所有之前的操作都已经执行完成,这样便形成了“指令重排无法越过的内存屏障”。

posted @ 2018-07-10 19:26  arax  阅读(212)  评论(1编辑  收藏  举报