Java多线程之挥发性关键字及内存屏障实例解析

  

前面一篇文章在介绍Java内存模型的三大特性(原子性,可见性,有序性)时,在可见性和有序性中都提到了动荡的关键字,那这篇文章就来介绍动荡的关键字的内存语义以及实现其特性的内存屏障。

  

挥发性是JVM提供的一种最轻量级的同步机制,因为Java内存模型为不稳定定义特殊的访问规则,使其可以实现Java内存模型中的两大特性:可见性和有序性。正因为不稳定的关键字具有这两大特性,所以我们可以使用不稳定的关键字解决多线程中的某些同步问题。

  


  

  

挥发性的可见性是指当一个变量波动被修饰后,这个变量就对所有线程均可见。白话点就是说当一个线程修改了一个动荡的修饰的变量后,其他线程可以立刻得知这个变量的修改,拿到最这个变量最新的值。

  

结合前一篇文章提到Java内的存模型中线程,工作内存,主内存的交互关系,我们对不稳定的可见性也可以这么理解,定义修为波动饰的变量,在线程对其进行写入操作时不会把值缓存在工作内存中,而是直接把修改后的值刷新回写到主内存,而当处理器监控到其他线程中该变量在主内存中的内存地址发生变化时,会让这些线程重新到主内存中拷贝这个变量的最新值到工作内存中,而不是继续使用工作内存中旧的缓存。

  

下面我列举一个利用挥发性可见性解决多线程并发安全的示例:

        公开课VolatileDemo {//私有静态布尔isReady=false;   私有静态不稳定的布尔isReady=false;   静态类ReadyThread延伸线{   公共空间run () {   而(! isReady) {   }   system . out。println (“ReadyThread完成”);   }   }   公共静态void main (String [] args)抛出InterruptedException {   新的ReadyThread () .start ();   thread . sleep(1000);//睡眠1秒钟确保ReadyThread线程已经开始执行   isReady=true;   }   }      

上面这段代码运行之后最终会在控制台打印出:ReadyThread完成,而当你将变量isReady的挥发性修饰符去掉之后再运行则会发现程序一直运行而不结束,而控制台也没有任何打印输出。

  

我们分析下这个程序:初始时isReady为false,所以ReadyThread线程启动开始执行后,它的而代码块因标志位isReady为假会进入死循环,当用不稳定的关键字修饰isReady时,主方法所在的线程将isReady修改为真实之后,ReadyThread线程会立刻得知并获取这个最新的isReady值,紧接着而循环就会结束循环,所以最后打印出了相关文字。而当未用挥发性修饰时,主方法所在的线程虽然修改了isReady变量,但ReadyThread线程并不知道这个修改,所以使用的还是之前的旧值,因此会一直死循环执行而语句。

  


  

  

有序性是指程序代码的执行是按照代码的实现顺序来按序执行的。

  

挥发的有序性特性则是指禁止JVM指令重排优化。

  

我们来看一个例子:

        公共类单例{   私有静态单例实例=零;//私有静态不稳定单例实例=零;   私人单例(){}      公共静态单例getInstance () {//第一次判断   如果(实例==null) {   同步(Singleton.class) {   如果(实例==null) {//初始化,并非原子操作   实例=new Singleton ();   }   }   }   返回实例;   }   }      

上面的代码是一个很常见的单例模式实现方式,但是上述代码在多线程环境下是有问题的。为什么呢,问题出在实例对象的初始化上,因为<代码>实例=new Singleton ();这个初始化操作并不是原子的,在JVM上会对应下面的几条指令:

     =()分配的内存;//1。分配对象的内存空间   ctorInstance(内存);//2。初始化对象   实例=记忆;//3。设置实例指向刚分配的内存地址      

上面三个指令中,步骤2依赖步骤,但是步骤3不依赖步骤2,所以JVM可能针对他们进行指令重拍序优化,重排后的指令如下:

     =()分配的内存;//1。分配对象的内存空间   实例=记忆;//3。设置实例指向刚分配的内存地址   ctorInstance(内存);//2。初始化对象以前      

这样优化之后,内存的初始化被放到了实例分配内存地址的后面,这样的话当线程1执行步骤3这段赋值指令后,刚好有另外一个线程2进入getInstance方法判断实例不为null,这个时候线程2拿到的实例对应的内存其实还未初始化,这个时候拿去使用就会导致出错。

Java多线程之挥发性关键字及内存屏障实例解析