java中volatile关键字的要解决的问题以及其中一个常见案例

74 阅读2分钟

volatile只能用于修饰变量(静态变量和属性变量),这个关键字的目的是希望在多个线程之间保持线程共享变量的可见性。
volatile关键字适合一个线程修改,多个线程读。
为什么不加volatile关键字,就不可以保证可见性?
原因在于,在线程之间为了高效运行,通常会在本线程所属的内存区域中保存一个变量副本,而不是频繁读取变量。因此,如果在线程运行期间对于变量进行了修改,则线程内是感受不到变量已经被修改了,因为变量的值存储在线程的工作内存了,一直沿用以前的副本值。
volatile关键字如何保证变量的可见性的?
volatile关键字是通过读写屏障保证可见性,在对变量进行写操作的时候,对这个变量写操作之前的所有线程共享变量都会直接修改主存中的值,而不是副本的数值。在进行读操作的时候,会直接读取这个指令后面共享变量的数值,而不是副本的数值。
volatile关键字可以保证指令的有序性,禁止指令重排,那么是如何禁止指令重排的?
在读该变量的时候,会禁止该行代码以下的代码重排序,在写该变量的时候,会禁止该行代码以上的代码重排序

以单例模式双重检验为例进行说明

class Singleton{
    private Singleton(){}
    
    private static volatile Singleton INSTANCE;
    
    public static Singleton getInstance(){
        if(INSTANCE == null){
            synchronized (Singleton.class){
                
                if(INSTANCE == null){
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

细节解释

  • new Singleton()这一行代码在具体运行时会执行四行指令
    1.创建 Singleton对象实例,并获得一个指向该对象的引用
    2.复制一份引用
    3.调用初始化方法
    4.返回在步骤2中复制的引用
  • 因为在创建对象的过程中采用了synchronized锁,因此,当一个线程在代码块内部完成了对于INSTANCE的修改后,在synchronized外部(第7行的位置)其他线程会看到这个变量的数值。

问题产生的原因以及解决的措施
在代码的11行位置,如果不加volatile关键字可能会出现指令重排,现返回这个对象的引用之后再调用初始化方法进行内部参数的补充。而其他的线程在代码的第7行是可以感知到INSTANCE已经非空,因此会返回一个未初始化完成的INSTANCE。因此加入volatile关键字可以避免该问题的出现