java开发:单例模式:为什么要加双重验校?为什么要加volatile?

//final类不可继承
final public class Single {
    //使用volatile修饰变量
    private static  Single single = null;
    public static Single create() {
        //第一次验校
        if (single == null) {
            //同步代码块(类锁)
            synchronized (Single.class) {
                //第一次验校
                if (single == null) {
                    single = new Single();
                }
            }
        }

        return single;
    }

    /**
     * 私有构造函数,外部访问不了
     */
    private Single() {
    }
}


这是一个典型的双重锁单例模式,在很多单例源码中经常可以看见。

1.为什么要进行第一次判空

我们知道单例模式只有第一次执行create()方法的时候才会走synchronized 中的代码,后面再次访问的时候直接返回single 对象。如果说我们没有第一次验校,每一个线程都要走synchronized 中的代码,而每一次线程都要去拿到同步锁才能执行。在多线程的情况下每一个线程要拿到single 对象都要排队等待同步锁释放。因此第一次验校作用就是为了提高程序的效率。

2.为什么要进行第二次判空

举个例子:假如现在没有第二次验校,线程A执行到第一次验校那里,它判断到single ==null。此时它的资源被线程B抢占了,B执行程序,进入同步代码块创建对象,然后释放同步锁,此时线程A又拿到了资源也拿到了同步锁,然后执行同步代码块,因为之前线程A它判断到single ==null,因此它会直接创建新的对象。所以就违反了我们设计的最终目的。

3.变量为什么要加volatile关键字

在上面例子中volatile保证代码指令不会被重排序,首先我们得先了解什么是volatile关键字与及它的特性。

volatile关键字的特性:

volatile具有可见性、有序性,不具备原子性。
注意,volatile不具备原子性,这是volatile与java中的synchronized、java.util.concurrent.locks.Lock最大的功能差异,这一点在面试中也是非常容易问到的点。
原子性:不可打断的操作,要么成功要么失败。例如基本数据类型的读写操作都是属于原子性int a = 10这一过程就是原子性(即可以看成当一个线程执行int a = 10这句代码时其他线程是处于等待获取cpu资源的状态,因为线程并发是通过cpu调度交替执行,并不是真正的并行执行),但是像a++这样的运算操作就是非原子性,因为a++虚拟机要运行三个指令:读取a,a+1,a赋值,这个过程是允许打断的。
volatile具有可见性是指当一个线程对变量进行原子操作的时候,另外的线程能立即获取到最新的数据。
java如何实现可见性需要了解Java的内存模型:

在这里插入图片描述
Java内存模型(即Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在。它描述了不同线程访问共享变量的规则。
我们程序所有的共享变量都是放在主内存中,而线程会从主内存拷贝它需要的共享变量到自己的工作内存
线程之间变量的传递则需要靠主内存。
Java内存模型规定对共享变量的操作只能在自己工作内存进行,即我读一个共享变量时需要先从工作内存找。写的时候也需要修改工作内存的变量后再push到主内存。

在多线程并发的情况下会发生什么问题?
假如主内存中有个共享变量 int a = 10,线程A和线程B的工作内存都有这个变量的副本。假如线程A在自己工作内存中修改了a的值,int a = 20,此时线程A还未来得及push到主内存中线程B就已经读取了a的值,线程B读取到的值是10,因此就造成了数据的混乱。
而java的volatile保证了共享变量的可见性:volatile修饰的变量当线程在工作内存修改后会立马push到主内存中,同时会把工作内存的变量设置为禁止读取,因此访问这个变量的时候不能从自己的工作内存访问,必须要去主内存中取。

还是举上面的例子:变量a被volatile修饰 volatile int a = 10,线程A修改了a的值立马push到主存中,线程B访问的时候到主存访问就可以得到最新的值。

其次volatile还可以禁止虚拟机对指令进行重排序。指令重排作用是虚拟机在不影响单线程程序执行结果的前提下对指令重新排序,提高程序运行效率。但是重排序在多线程并发的情况下也是容易出现问题的。

在上诉单例模式中volatile保证了虚拟机执行字节码的时候指令不会重排序。
single = new Single() 在我们看来就是一句话操作而已,但在虚拟机看来它一共分为了几个指令操作:
1.为对象分配内存空间
2.初始化对象
3.将引用指向对象的内存空间地址
虚拟机执行的时候不一定是按顺序123的执行,也有可能是132。这是虚拟机的重排序引起的,单线程情况下是没有什么bug的,最终都会创建出对象,只是先后顺序不同。
但是在上面例子中会出现这么一种情况:
假如线程A执行 single = new Single()虚拟机是按132排序执行,当执行到3的时候single 引用已经不为空。此时若线程B执行到第一次验校处(第一次验校不在同步代码中,因此所有线程随时都可以访问),它判断 single ==null 得到false,直接返回single对象。但是此时single对象还没初始化完成,因此很有可能就会发生bug。

发布了194 篇原创文章 · 获赞 42 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/qq_39027256/article/details/103457872
今日推荐