volatile关键字解析

匿名 (未验证) 提交于 2019-12-03 00:40:02

这个可见性是指,当一个线程读取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中的原子类。

先看一段代码:

  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指令,将计算的数据更新到主内存后,意味着所有之前的操作都已经执行完成,这样便形成了“指令重排无法越过的内存屏障”。

原文:https://www.cnblogs.com/arax/p/9291046.html

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!