volatile关键字

(已迁移)

  volatile

  在多处理器开发中保证共享变量的可见性。

  比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。

  计算机存储体系中,自顶向下:CPU寄存器—>CPU缓存(L1、L2、L3)—>内存—>其它存储设备,如硬盘。

  使用volatile关键字修饰的变量,在多核处理器下会引发两件事情:

    --将当前处理器缓存行的数据写回到系统内存

    --这个写回内存的操作会使其他CPU里缓存了该内存的数据无效(缓存一致性协议,即每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是否过期,过期则在使用该变量时重新从内存读取该数据到该处理器缓存中)

  (Java并发编程的艺术)

 

  关于volatile变量的可见性,经常被人误解:“volatile变量对所有线程是立即可见的,换句话说,volatile变量在各个线程中是一致的,所以基于volatile变量的运算在并发下是线程安全的。”这句话论据部分没有问题,但是这样下结论是错的!

  例如,Java里面的运算操作并非原子操作,这就导致了volatile变量的运算在并发下一样是不安全的。

  

 1 //volatile变量自增运算测试
 2 
 3 public class VolatileTest{
 4     
 5     public static volatile int race = 0;
 6     
 7     public static void increase(){
 8         race++;
 9     }
10     
11     private static final int THREADS_COUNT = 20;
12 
13     public static void main(String[] args){
14         Thread[] threads = new Thread[THREADS_COUNT];
15         for(int i=0;i<THREADS_COUNT;++i){
16             threads[i] = new Thread(new Runnable(){
17                 @Override
18                 public void run(){
19                     for(int i=0;i<10000;++i){
20                         increase();
21                     }
22                 }
23             });
24         }
25         
26         //等待所有累加线程都结束
27         while(Thread.activeCount()>1){
28             Thread.yield();
29         }
30 
31         System.out.println(race);
32     }
33 }

  执行后发现,输出结果并非200000,问题出在“race++”这里,因为只有一行代码的increase()方法在Class文件中是由4条字节码指令构成的

 1 public static void increase();
 2     Code:
 3         Stack=2, Locals=0, Args_size=0
 4         0:    getstatic    #13;    //Field race:I
 5         3:    iconst_1
 6         4:    iadd
 7         5:    putstatic     #13;    //Field race:I
 8         8:    return
 9     LineNumberTable:
10         line    14:    0
11         line    15:    8

  虽然使用字节码并不严谨,但是已经能说明问题。由于volatile变量只能保证可见性,在类似上述场景中,我们仍应加锁(synchronized、java.util.concurrent中的锁或者原子类)来保证原子性。

  不过,类似下面这种场景就很适合volatile变量开控制并发,当shutdown()方法被调用时,能保证所有线程中执行的doWork()方法都立即停下来。

 1 volatile boolean shutdownRequested;
 2 
 3 public void shutdown(){
 4     shutdownRequested = true;
 5 }
 6 
 7 public void doWork(){
 8     while(!shutdownRequested){
 9         //其他代码逻辑
10     }
11 }

 

  使用volatile变量的第二个语义是禁止重排序优化(即“有序性”),普通的变量仅会保证该方法执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。因为在同一个线程的方法执行过程中无法感知到这点,这就是Java内存模型中描述的所谓“线程内表现为串行的语义”

  

 1 Map configOptions;
 2 char[] configText;
 3 //    此变量必须定义为volatile
 4 volatile boolean initialized = false;
 5 
 6 //假设这段代码在线程A执行
 7 //模拟读取配置信息,当读取完成后
 8 //将initialized变为true,通知其他线程配置可用
 9 configOptions = new HashMap();
10 configText = readConfigFile(fileName);
11 processConfigOptions(configText,configOptions);
12 initialized = true;
13 
14 //假设以下代码在线程B中执行
15 //等待initialized为true,代表线程A已经把配置信息初始化完成
16 while(!initialized){
17     sleep();
18 }
19 //使用线程A中初始化好的配置信息
20 doSomethingWithConfig();

  如果initialized变量不使用volatile修饰,则可能发生指令重排,导致线程A中最后一行代码“initialized=true”被提前执行,这样线程B中使用配置信息的代码就会存在问题,因此需要volatile关键字来避免类似的情况。

  类似的例子还有双锁检测(Double Check Lock,DCL)单例代码

 1 public class Singleton{
 2     private volatile static Singleton instance;
 3 
 4     public static Singleton getInstance(){
 5         if(instance == null){
 6             synchronized(Singleton.class){
 7                 if(instance == null){
 8                     instance = new Singleton();
 9                 }
10             }
11         }
12         return instance;
13     }
14     
15     public static void main(String[] args){
16         Singleton.getInstance();
17     }
18 }

  因“instance = new Singleton()”这句话可以分为三步:

    1、为instance分配内存空间

    2、初始化instance

    3、将instance指向分配的内存空间

  由于jvm可能指令重排为1-3-2,单线程下无问题,但是多线程下会导致一个线程获得一个未初始化的实例。例如,线程T1执行了1和3,此时T2线程调用getInstance()后发现instance不为空,因此返回instance,但此时instance还没有被初始化,所以需使用volatile来禁止指令重排。

  (深入理解Java虚拟机)

  (非原创,有一些参考内容时间久了找不到出处了,若有机会后期标明,在此表示感谢!仅为个人整理,不严谨之处望多包涵)

 

  

posted @ 2020-10-08 20:38  α_伊卡洛斯  阅读(152)  评论(0)    收藏  举报