单例模式的优化及指令重排序

欢迎访问我的blog http://www.codinglemon.cn/

1. 单例模式及指令重排序问题

什么是单例模式:

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

注意:
1. 单例类只能有一个实例。
2. 单例类必须自己创建自己的唯一实例。
3. 单例类必须给所有其他对象提供这一实例。

2 单例模式的优化过程

下面是一个最简单的单例模式代码:

public class Singleton1 {
    private static final Singleton1 INSTANCE = new Singleton1();

    public static Singleton1 getInstance(){
        return INSTANCE;
    }
}

此时代码没有任何问题,但是为了提高效率,想让INSTANCE对象在调用getInstace方法时创建对象,而不是加载Singleton1类时就创建对象,故改进代码如下:

public class Singleton2 {
    private static Singleton2 INSTANCE;

    public static Singleton2 getInstance(){
        if(INSTANCE == null){	//1.
	    //2.
            INSTANCE = new Singleton2();
        }
        return INSTANCE;
    }
}

现在看来,这么写应该没问题了吧?先判断INSTANCE是否为空,若为空的时候再创建对象。此时在单线程下时没有问题的,问题就出在如果是多线程访问时,多个线程同时执行了注释1处的代码,判断INSTANCE为空,同时进入了注释2处,此时每个线程都会new一个新的Singleton2对象,完全违背了单例模式的初衷。 那有的同学就说,那我加个锁不就完事儿了吗??就有了如下代码:

public class Singleton3 {
    private static Singleton3 INSTANCE;

    public synchronized static Singleton3 getInstance(){
        if(INSTANCE == null){
            INSTANCE = new Singleton3();
        }
        return INSTANCE;
    }
}

这么写当然没问题,但是又出现了另外一个问题,如果这个构造函数里面不仅仅要创建对象,要有其他许多复杂的内容需要执行,且调用频率很高的话,那么直接将这个方法整个加锁,会严重降低运行的效率。有的同学就说,那我只在创建对象的时候加锁就好了,就有了如下代码:

public class Singleton4 {
    private static Singleton4 INSTANCE;

    public static Singleton4 getInstance(){
        if(INSTANCE == null){
            //1.
            synchronized (Singleton4.class){
                //2.
                INSTANCE = new Singleton4();
            }
        }
        return INSTANCE;
    }
}

现在看,似乎是对的,但是问题真的解决了吗?其实如果你测试一下代码,发现问题并没有解决。因为当多个线程同时执行if(INSTANCE == null)语句时发现INSTANCE 为空,会通知进入注释1这里,然后其中某一个线程抢占了锁,执行了INSTANCE = new Singleton4()这段代码,其他进入if内部的线程会等待,直到线程释放锁后会,会抢占锁让自己得以执行,最终会创建多个INSTANCE对象。 此时应该这样修改代码:

public class Singleton5 {
    private static Singleton5 INSTANCE;

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

先在方法外部判断一次,然后在加锁之后,在内部在判断一次对象是否为空,这样即使多个线程在抢占锁,只要有其中一个线程创建了对象,其他线程就不会再次创建对象了。

3 指令重排序

什么是指令重排序:

为了尽可能减少内存操作速度远慢于CPU运行速度所带来的CPU空置的影响,虚拟机会按照自己的一些规则将程序编写顺序打乱——即写在后面的代码在时间顺序上可能会先执行,而写在前面的代码会后执行——以尽可能充分地利用CPU。

那刚才讲了这么多有关单例模式的问题,这跟指令重排序有什么关系呢?在最后我们修改的最终阶段的代码是Singleton5,这段代码真的就完全没有问题吗?其实不是, 刚才提到了有关指令重排序的问题,那么在Java中,创建一个对象的汇编语句如下:
单例模式的优化及指令重排序_第1张图片

其中第3行调用了T的构造方法和第4行将创建好的对象的地址赋值给t这两步可能引发指令重排序,也就是说有可能会先执行astore_1,再执行invokespecial。那这又怎么会导致Singleton5的代码出错呢?
我们知道,Java中一个class类的初始化过程如下:
单例模式的优化及指令重排序_第2张图片

首先将class文件进行加载(loading);然后是链接过程(linking),链接阶段有准备过程(preparation),初始化类中的静态变量赋默认值(static);然后是类的初始化过程(initializing),此时将静态变量赋值为初始值;最后是GC过程。

那么在执行INSTANCE = new Singleton5();语句时,如果发生指令重排序,会出现如下类似的情况:
单例模式的优化及指令重排序_第3张图片

按图中正常代码执行顺序,应该是线程A创建一个对象count值为1000,然后线程B访问count对象,让count++,count值变为1001。
但是如果发生了指令重排序,线程A在正常执行,执行到preparation阶段count值初始化为0的时候,就已经将地址给了创建的对象引用(正确的count值应该是1000);此时线程B再去访问该对象时,发现count已经不是null了,他已经有值了,此时count=0,那么线程B执行count++,count值变为了1,然后线程A执行count=1000,导致本来执行了count++,结果最终count的值还是1000。
那么怎么修改代码才能让他能正确执行呢?只需要在申明INSTANCE的时候加上volatile关键字即可。下面有有关volatile的介绍

public class Singleton6 {
    private static volatile Singleton6 INSTANCE;

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

4. volatile

volatile 是一个类型修饰符。volatile 的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略。

volatile 的特性

  • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)
  • 禁止进行指令重排序。(实现有序性)

所以,在遇到有关单例模式的问题的时候,现在你可以说清楚他的代码原理了吧?

你可能感兴趣的:(Java进阶,设计模式,java,多线程,编程语言)