单例模式就是如此简单

在面试中相信不少人会被问到:说说你最了解的三个设计模式,平常开发中使用过哪些设计模式等等。最近几篇文章就来学习一下设计模式,这是第一篇文章,也是最多见的模式——单例模式。java

什么是单例

单例模式(Singleton Pattern),顾名思义,即保证一个类仅有一个实例,并在全局中提供一个访问点。git

在实现单例时,要保证一个类仅有一个实例,就不能提供公有的构造方法,任由其余类建立实例,对应变量也须要为 static,只在加载时初始化一次。另外呢,要在全局中都能访问到,还须要提供一个静态的公有方法来进行访问。github

具体实现方式比较多,对于不一样的场景,也应该选择不一样的方式,例如是否须要保证线程安全,是否须要延迟加载。下面具体来看一下。面试

饿汉式(线程安全)

根据上面对单例模式实现的说明,能够很容易地想到以下实现:设计模式

public class Singleton1 {

    private static Singleton1 instance = new Singleton1();

    private Singleton1() { }

    public static Singleton1 getInstance() {
        return instance;
    }
}
复制代码

这种方式在该类第一次被加载时,就会建立好该实例。这就是所谓的饿汉式,也就是,在想要使用实例时,马上就能拿到,而不须要进行等待。安全

另外这种方式,由 JVM 保证其线程安全。可是这种方式可能会形成资源消耗,由于有可能这个实例根本就用不到,而进行没必要要的加载。多线程

懒汉式(非线程安全)

上述方式在类加载时就进行实例化,可能会形成没必要要的加载。那么咱们能够在其真正被访问的时候,再进行实例化,因而能够写出以下方式:ide

public class Singleton2 {

    private static Singleton2 instance;

    private Singleton2() { }

    public static Singleton2 getInstance() {
        if (instance == null) {
            instance = new Singleton2();
        }
        return instance;
    }
}
复制代码

getInstance 方法中,第一次访问时,因为没有初始化,才去进行进行初始化,在后续访问时,直接返回该实例便可。这就是所谓的懒汉式,也就是,它不会提早把实例建立出来,而是将其延迟到第一次被访问的时候。性能

可是懒汉式存在线程安全问题,以下图:学习

在多线程场景下,若是有两个线程同时进入 if 语句中,则这两个线程分别建立了一个对象,在两个线程从 if 中退出时,就建立了两个不同的对象。

懒汉式(线程安全)

既然普通的懒汉式会出现线程安全问题,那么给建立对象的方法加锁便可:

public class Singleton3 {

    private static Singleton3 instance;

    private Singleton3() { }

    public static synchronized Singleton3 getInstance() {
        if (instance == null) {
            instance = new Singleton3();
        }
        return instance;
    }
}
复制代码

上述这种作法虽然在多线程场景下也能正常工做,也具有延迟加载。但因为 synchronized 方法锁住了整个方法,效率比较低。因而,聪明的小伙伴,能够很容易想到,使用同步方法块,来减少加锁的粒度。

看下面两种作法,加锁粒度确实减少了,可是它们却并不能保证线程安全:

synchronized (Singleton4.class) {
    if (instance == null) {
        instance = new Singleton4();
    }
}
复制代码

因为指定重排序出现问题,后面介绍双重校验锁时会详细说。

if (instance == null) {
    synchronized (Singleton4.class) {
        instance = new Singleton4();
    }
}
复制代码

若是 synchronized 加在 if 语句外面,这和普通的懒汉式作法同样,没有区别。若是有两个线程分别进入 if 语句,虽然也有加锁操做,可是两个线程都会执行实例化,也就是会进行两次实例化。

双重校验锁(线程安全)

因而引出了双重校验锁方式,能够先判断对象是否实例化,若是没有再进行加锁,再加锁以后,再次判断是否实例化,若是仍然没有实例化,才实例化对象。

这种作法的完整代码以下:

public class Singleton5 {
    
    private static volatile Singleton5 instance;
    
    private Singleton5() { }
    
    public static Singleton5 getInstance() {
        // 若是已经实例化,则直接返回,不用加锁,提高性能
        if (instance == null) {
            synchronized (Singleton5.class) {
                // 再次检查,保证线程安全
                if (instance == null) {
                    instance = new Singleton5();
                }
            }
        }
        return instance;
    }
}
复制代码

能够看到,在 synchronized 语句先后,有两个 if 判断,这就是所谓的双重校验锁。

使用 volatile

其实,若是仅仅是双重校验的话,仍然不能保证线程安全问题。这就要分析 instance = new Singleton5(); 这段代码。

虽然代码只有一句,但在 JVM 中它其实被分为三步执行:

  1. instance 分配内存空间;
  2. instance 进行初始化;
  3. instance 指向分配的内存地址;

但因为编译器或处理器可能会对指令重排序,执行的顺序就有可能变成 1->3->2。这在单线程环境下不会出现问题,可是在多线程环境下可能会致使一个线程得到尚未初始化的实例。

例如,线程 A 执行了第 13 步后,此时线程 B 调用 getInstance() 方法,判断 instance 不为空,所以返回 instance。但此时 instance 还未被初始化。

因此,就须要使用 volatile 关键字来修饰 instance,禁止编译器的指令重排序,保证在多线程环境下也能正常运行。

静态内部类式(线程安全)

目前双重校验锁的作法看起来不错,使用延迟加载,在保证线程安全的同时,加锁粒度也比较小,效率还不错。那还有没有其余方法呢?

那就是使用静态内部类来实现,来看一下它的实现:

public class Singleton6 {

    private Singleton6() { }

    private static class InnerSingleton {
        private static final Singleton6 INSTANCE = new Singleton6();
    }

    public static Singleton6 getInstance() {
        return InnerSingleton.INSTANCE;
    }
}
复制代码

在这种实现中,当外部类 Singleton6 类被加载时,静态内部类 InnerSingleton 并无被加载。

而是只有当调用 getInstance 方法,从而访问类的静态变量时,才会加载内部类,从而实例化 INSTANCE。而且 JVM 能确保 INSTANCE 只能被实例化一次,即它也是线程安全的。

枚举式(线程安全)

另外,使用枚举实现单例也是一种不错的方式,代码很是简单:

public enum Singleton6 {
    INSTANCE();

    Singleton6() { }
}
复制代码

枚举的实现中,类被定义为 final,其枚举值被定义为 static final,对枚举值的初始化放在静态语句块中。因此,对象在该类第一次被加载时实例化,这不只避免了线程安全问题,并且也避免了下面提到的反序列化对单例的破坏。

单例与序列化

如今来看一下,对象在序列化和反序列化时,是否还可以保证单例。

这里使用双重校验锁实现的单例类,对 Singleton5 类添加 Serializable 接口,而后进行测试:

public class SingletonTest {

    public static void main(String[] args) {
        Singleton5 instance1 = Singleton5.getInstance();
        try ( ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile")) ){
            oos.writeObject(instance1);
        } catch (IOException e) {
            e.printStackTrace();
        }

        Singleton5 instance2 = null;
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("tempFile"))) ){
            instance2 = (Singleton5) ois.readObject();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }

        System.out.println(instance1 == instance2);
    }
}
// false
复制代码

能够看到,对 Singleton5 进行反序列获得的是一个新的对象,如此就破坏了 Singleton5 的单例性。

咱们能够在 Singleton5 类中添加一个 readResolve() 方法,并在该方法中指定要返回的对象的生成策略:

public class Singleton5 implements Serializable {

    private static volatile Singleton5 instance;

    private Singleton5() { }

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

    // 添加 readResolve 方法
    private Object readResolve() {
        return instance;
    }
}
复制代码

经过 debug 方法查看源码,在 readObject 方法的调用栈中,能够看到 ObejctStreamClass 类的 invokeReadResolve 方法:

若是定义了 readResolve 方法,会经过反射进行调用,根据指定的策略来生成对象。

有哪些好的单例模式实践

JDK#Runtime

该类用于获取应用运行时的环境。能够看到这是一个饿汉式的单例。

public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    public static Runtime getRuntime() {
        return currentRuntime;
    }

    private Runtime() {}
}
复制代码

Spring#Singleton

Spring 中定义 Bean 时,能够指定是单例仍是多例(默认为单例):

@Scope("singleton")
复制代码

查看其源码,单例模式实现以下:

public abstract class AbstractFactoryBean<T> implements FactoryBean<T>, BeanClassLoaderAware, BeanFactoryAware, InitializingBean, DisposableBean {

    private T singletonInstance;
    
    @Override
	public void afterPropertiesSet() throws Exception {
	    // 扫描配置时,单例模式
	    // 就会将 initialized 置为 true
		if (isSingleton()) {
			this.initialized = true;
			// 调用子类方法建立对象
			this.singletonInstance = createInstance();
			this.earlySingletonInstance = null;
		}
	}
    
    @Override
	public final T getObject() throws Exception {
		if (isSingleton()) {
			return (this.initialized ? this.singletonInstance : getEarlySingletonInstance());
		}
		else {
			return createInstance();
		}
	}
}
复制代码

参考资料

相关文章
相关标签/搜索