单例模式

单例模式

饿汉模式

public class Singleton {

    private static Singleton singleton  = new Singleton();

    private Singleton() {
    }
    public static Singleton getInstance(){
        return singleton;
    }
}

此方法能保证singleton唯一性,但一起动则初始化,如果有大量的动作,那么会极为耗费性能,所以引申出懒汉模式。

懒汉模式

public class Singleton {

    private static volatile Singleton singleton;

    private Singleton() {
    }
    public synchronized static Singleton getInstance(){
        if (singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

此方法能保证唯一性,而且系统初始化时也不会耗费性能,只有第一次调用才会耗费性能,但由于使用了synchronized,所以getInstance方法效率很低。

DCL单例模式

懒汉模式是这样的

/**
 * 普通的单例模式
 * @author
 */
public class Singleton {

    private static Singleton singleton;

    private Singleton(){}

    private synchronized static Singleton getInstance(){
        if (singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

又或者是这样的

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

但实质上synchronized放在方法上和代码块上的作用对于同步而言是一样的,都是使其包裹区域同一时间内只有一个线程可以运行(这是原子性),但是我们细心观察发现,我们其实需要保证同步的代码只有“singleton = new Singleton()”这一行代码,而且我们synchronized会造成性能损耗,那么对于这种情况,我们应该有更好的方法,那么就到了我们的DCL(double check locking)单例模式

DCL单例模式

 private static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.singleton) {	// flag1
                if (singleton == null){				// flag2
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

这样以后,只有第一个初始化的时候才会进入同步代码块,如果第一次初始化有thread1和thread2执行到flag1代码后,thread1释放完锁后thread2进入并不会再次new实例,这就保证了唯一性

这样看似很完美,但事实并不如此,我们先来看singleton = new Singleton被编译为指令后做了什么操作,做了三步操作:

  • 1、给singleton实例分配内存
  • 2、执行初始化构造方法
  • 3、将instance指向了实际分配的内存空间。

注意,在3这一步的时候因为singleton引用指向了实际的内存空间,所以它不为null了

但jvm指令重拍会造成一个情况,会造成2和3的顺序重排,原因是为了优化指令,所以实际上的指令顺序是这样的

  • 1、给singleton实例分配内存
  • 3、将instance指向了实际分配的内存空间
  • 2、执行初始化构造方法

然后当执行3完后,如果有另一个线程此时调用getInstance方法,那么第一行的判断则不成立,会返回singleton,此时返回的singleton是未初始化完的,那么使用肯定会报错“实例未初始化”,为解决此问题则引入一个volatile关键字,此关键字修饰的变量不会被指令重排(这是有序性,除此之外volatile还保证了可见性),注意volatile修饰的变量不会被指令重排,但其他的代码会被指令重排,但指令重排不会影响被volatile修饰的变量的代码顺序

所以使用volatile修饰

private static volatile Singleton singleton;

完整代码如下

public class Singleton {

    private static volatile Singleton singleton;

    private Singleton() {
    }

    private static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.singleton) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

最后做个总结:单例模式最初有饿汉懒汉的区分,但懒汉由于一般使用synchronized代码块修饰导致性能降低,所以我们更改了synchronized的位置保证只影响第一次初始化时的性能,然后又由于jvm指令重排的问题,我们引入了volatile关键字

我们思考一下,能不能使用其他的方法来实现第一次调用时才加载,且不使用synchronized机制呢?答案很显然有,就是Holder单例模式,它使用了静态内部类的方法

Holder单例模式

DCL是为了解决懒汉模式中存在的性能问题,但它的代码逻辑有些许复杂,我们可以用更简单的方式来实现

public class Singleton {

    private static class InSingleton{
        private static InSingleton inSingleton = new InSingleton();
    }

    public static InSingleton getInstance(){
        return InSingleton.inSingleton;
    }
}

为什么这个能保证唯一性,且能保证延时加载,为啥呢?

首先是延时加载的问题:

当我们的系统启动时Singleton被JVM加载,但此时我们内部类InSingleton并没有被加载,这是延时加载,当getInstance方法被调用的时候,InSingleton就被初始化了

然后是唯一性问题:

jvm解释中,虚拟机会保证一个类的init方法被正确的加锁,当多个线程同时去init的时候,只有一个线程会进入init方法,其他线程则会被阻塞,这就保证了唯一性

虽说Holder看起来是比较完美的单例模式,既解决了延时加载,也解决了唯一性安全的问题,但由于是内部类创建的实例,所以如果你想创建实例的时候根据传入参数来创建的话,那么是无法完成的,所以根据这一点来说,DCL和Holder自行评估使用

枚举单例模式

public enum SingletonEnum {
    /**
     * 需要的对象
     */
    INSTANCE
}

枚举单例模式特别简洁,且里面同样可以拥有属性方法(因为编译后枚举也是一个类,只是继承与Enum),且同样能保证唯一性,还有防止序列化和反射的不一致情况,具体可百度,太多不想写了

原文地址:https://www.cnblogs.com/daihang2366/p/14480197.html