【知识积累】通过单例模式学习volatile关键字

一、先看一段代码

/**
 * 单例模式的实现
 */
public class Singleton {

    private static Singleton singleton;

    private Singleton() {
    }

    public static Singleton getSingleton(){
        if (null == singleton){
            synchronized (Singleton.class){
                if (null == singleton){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

上例就是著名的单例模式的双检索实现。

在单线程情况下,并不会有什么问题,但是在多线程情况下,就会有问题。

会有什么问题呢?

在多线程环境下,一个线程在执行到第一次检查后,singleton不为空,但是此时的singleton还没有完成初始化,因为singleton = new Singleton();这一步是分为三步去完成的。

1、分配对象内存空间

2、初始化对象

3、设置instance指向刚分配的内存地址,此时instance!=null

假如此时发生重排序了,顺序就变成如下的情况:

1、分配对象内存空间

2、设置instance指向刚分配的内存地址,此时instance!=null

3、初始化对象

为什么会发生这种情况?什么是重排序?一脸懵逼。。。

因为2和3不存在数据依赖的关系,在单线程情况下,不管是在前面还是在后面,都不会影响程序的结果,这种重排序优化是允许的。当多线程情况下,有一个线程访问时发现instance!=null,但是instance还没初始化完成,这就造成了线程的安全性问题。

那怎么解决呢?

很简单,加上volatile关键字。那这个关键字做什么用的?

volatile:禁止指令重排序

/**
 * 单例模式的实现
 */
public class Singleton {

    private volatile static Singleton singleton;

    private Singleton() {
    }

    public static Singleton getSingleton(){
        if (null == singleton){
            synchronized (Singleton.class){
                if (null == singleton){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

二、volatile如何禁止重排序优化?

概念:内存屏障(Memory Barrier),一个CPU指令。

作用:

1、保证特定操作的执行顺序;

2、保证某些变量的内存可见性。

volatile变量正是通过内存屏障实现其在内存中的语义(可见性和禁止重排优化)。

三、volatile的变量为何立即可见?

当写一个volatile变量时,JMM会把该线程对应的工作内存中的共享变量刷新到主内存中;
当读一个volatile变量时,JMM会把该线程对应的工作内存置为无效,那么该线程将只能从主内存中重写读取共享变量。

四、volatile的内存语义

volatile:JVM提供的轻量级同步机制
volatile关键字有如下两个作用:
1、保证被volatile修饰的共享变量对所有线程总是可见的,即当一个线程修改了一个被volatile修饰的共享变量的值的时候,其他线程立即感知到变动;
2、禁止指令重排序优化。

关于volatile的可见性作用,我们必须意识到被volatile修饰的变量,对所有线程总是立即可见的。对volatile变量的所有写操作,总是能理解反应到其他线程中,但是对于volatile变量运算操作在多线程环境中并不保证安全性。

public class VolatileDemo {

    public static volatile int i = 0;

    public static void increase(){
        i++;
    }

}

value++并不是一个原子性操作,value++是先读取值,然后再写回一个新值,相当于原来的值加上1,分两步来完成。如果第二个线程在第一个线程读取旧址和写回新值期间,读取value的阈值,那么第二个线程就会与第一个线程一起看到同一个值,并执行相同的加1操作,也就引发了线程安全问题。因此对于increase方法必须使用synchronized修饰,以便保证线程安全。需要补充,并且注意的是,synchronized关键字解决的是执行控制的问题,它会阻止其他线程获取当前对象的监控锁,这就使得当前对象中被synchronized关键字保护的代码块无法被其他线程访问,也就无法并发执行。

public class VolatileDemo {

    public static volatile int i = 0;

    public synchronized static void increase(){
        i++;
    }

}

在increase方法前面添加了synchronized,更重要的是synchronized还会创建一个内存屏障,内存屏障指令保证了所有CPU结果都会直接刷到主存中,从而保证了操作的内存可见性,同时也使得先获得这个锁的线程的所有操作都happens-before于随后获得这个锁的所有操作。

一旦使用synchronized修饰方法之后,由于synchronized本身也具有与volatile相同的特性(即可见性),因此在这样的情况下,就完全可以省去volatile修饰变量

public class VolatileDemo {

    public static int i = 0;

    public synchronized static void increase(){
        i++;
    }

}

五、volatile和synchronize的区别?

1、volatile本质是在告诉JVM当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住直到该线程完成变量操作为止。
2、vlotaile仅能使用在变量级别;synchronized则可以使用变量、方法和类级别。
3、volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量修改的可见性和原子性。
4、volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
5、volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章