单例模式的 8 种实现

Posted 放开我我还能学

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单例模式的 8 种实现相关的知识,希望对你有一定的参考价值。

饿汉式单例模式

静态常量(线程安全)

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

静态代码块(线程安全)

public class Singleton2 {
private final static Singleton2 INSTANCE;
static { INSTANCE = new Singleton2(); }
private Singleton2() { }
public static Singleton2 getInstance() { return INSTANCE; }}

懒汉式单例模式

线程不安全

public class Singleton3 {
private static Singleton3 instance;
private Singleton3() { }
public static Singleton3 getInstance() { if (instance == null) { instance = new Singleton3(); } return instance; }}

安全同步方法(线程安全,但效率低)

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

部分同步方法(线程不安全)

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

部分同步方法(线程安全,推荐使用)

public class Singleton6 {
private volatile static Singleton6 instance;
private Singleton6() { }
public static Singleton6 getInstance() { if (instance == null) { // A synchronized (Singleton6.class) { if (instance == null) { // B instance = new Singleton6(); } } } return instance; }}

第一个 if 是并行的,可以提高效率;第二个 if 是为了防止多个线程等待进入锁而发生多次创建的。这个类的功能是,当实例已经被创建过,则不会经过加锁,直接返回当前实例;否则就加锁,创建实例。这样可以大幅度提高性能,不用每次都加锁,只要对象已经创建成功,getInstance()方法将不需要获取锁,直接返回已创建好的对象。

至于为什么要用 volatile修饰,这是因为在执行到 A 或 B 处,如果另一个线程 2 读取到instance不为 null 时,那么线程 2 就认为对象已经创建过了,直接返回,然后拿来使用,但是此时instance引用的对象可能还没有初始化完成,那么这样一来,如果线程 2 获取这个对象的属性时就会发生空指针异常。

实际上,对于instance = new Singleton6();这行代码,可以分解为如下 3 行伪代码:

memory = allocate(); // 1.分配对象的内存空间ctorInstance(memory); // 2.初始化对象instance = memory; // 3.设置instance指向刚分配的内存地址

由于 2 和 3 之间没有数据依赖性,因此可以进行重排序,重排序后如下:

memory = allocate(); // 1.分配对象的内存空间instance = memory; // 3.设置instance指向刚分配的内存地址(此时对象还未初始化)ctorInstance(memory); // 2.初始化对象

虽然 2 和 3 交换了顺序,但是单线程内执行的结果没有改变,然而多线程下执行的结果就不同了,如下图所示。



当线程 A 和 B 按上图顺序执行时,B 线程看到的是一个还没有被初始化的对象。

为了解决上述问题,我们需要在初始化的时候,给它加上一个 volatile 关键字禁止重排序。

volatile 禁止重排序的规则:

volatile 写之前禁止重排序volatile 读之后禁止重排序

静态内部类(线程安全,推荐使用)

public class Singleton7 {
private Singleton7() { }
private static class SingletonInstance {
private static final Singleton7 INSTANCE = new Singleton7(); }
public static Singleton7 getInstance() { return SingletonInstance.INSTANCE; }}

由于使用了静态内部类,相比饿汉式的直接实例化,静态内部类方式可以在需要初始化的时候才初始化,即按需加载。

枚举类型单例模式(线程安全,最佳方法)

public enum Singleton8 { INSTANCE;
public void getInstance() { // return whatever }}

具体示例:

class User { // 私有化构造函数 private User() { }
// 定义一个静态枚举类 static enum SingletonEnum { // 创建一个枚举对象,该对象天生为单例 INSTANCE; private User user;
// 私有化枚举的构造函数,只会调用一次 private SingletonEnum() { user = new User(); }
public User getInstance() { return user; } }
// 对外暴露一个获取User对象的静态方法 public static User getInstance() { return SingletonEnum.INSTANCE.getInstance(); }}
public class Test { public static void main(String[] args) { // true System.out.println(User.getInstance() == User.getInstance()); }}

彩蛋

反射攻击破坏单例

public class Singleton { private volatile static Singleton singleton;
private Singleton() { }
public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; }}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Singleton first = Singleton.getInstance(); Singleton second = Singleton.getInstance(); Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(); constructor.setAccessible(true); Singleton third = constructor.newInstance(); System.out.println(first + "\n" + second + "\n" + third); System.out.println("正常情况下,实例化两个实例是否相同:" + (first == second)); System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:" + (first == third));}
// 结果Singleton@74a14482Singleton@74a14482Singleton@1540e19d正常情况下,实例化两个实例是否相同:true通过反射攻击单例模式情况下,实例化两个实例是否相同:false

可以看到,反射获取的并不是同一个实例。

序列化破坏单例

// 让单例类实现序列化接口public class Singleton implements Serializable { private volatile static Singleton singleton;
private Singleton() { }
public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; }}
public static void main(String[] args) throws IOException, ClassNotFoundException {
File file = new File("singleton_file");
// 第一次获取实例 Singleton instance = Singleton.getInstance(); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file)); oos.writeObject(instance); ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
// 第二次获取实例 Singleton newInstance = (Singleton) ois.readObject(); System.out.println(instance); System.out.println(newInstance); System.out.println("序列化前后两个实例是否相同:" + (instance == newInstance));}
// 结果Singleton@7f31245aSingleton@568db2f2序列化前后两个实例是否相同:false

我们先将第一次获取的实例写入文件,然后第二次读取这个实例,再次获取这个实例,结果表明这两个实例是不同的实例,即通过序列化和反序列化拿了不同的对象。

但我们需要拿到相同对象,那么需要在单例类中加入一个 readResolve() 方法,由于任何一个readObject() 方法,不管是显式的还是默认的,它都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例,因此我们只需要让 readObject() 方法返回初始化时的实例对象即可。

修改如下:

public class Singleton implements Serializable { private volatile static Singleton singleton;
private Singleton() { }
public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; }
// 加上这个方法 private Object readResolve() { return singleton; }}
// 结果Singleton@7f31245aSingleton@7f31245a序列化前后两个实例是否相同:true

枚举类型避免反射攻击

public enum EnumSingleton { INSTANCE;
public EnumSingleton getInstance() { return INSTANCE; }
public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
EnumSingleton singleton1 = EnumSingleton.INSTANCE.getInstance(); EnumSingleton singleton2 = EnumSingleton.INSTANCE.getInstance(); System.out.println("正常情况下,实例化两个实例是否相同:" + (singleton1 == singleton2)); Constructor<EnumSingleton> constructor = null; constructor = EnumSingleton.class.getDeclaredConstructor(); constructor.setAccessible(true); EnumSingleton singleton3 = null; singleton3 = constructor.newInstance(); System.out.println(singleton1 + "\n" + singleton2 + "\n" + singleton3); System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:" + (singleton1 == singleton3)); }}
// 结果正常情况下,实例化两个实例是否相同:trueException in thread "main" java.lang.NoSuchMethodException: concurrency.singleton.EnumSingleton.<init>() at java.lang.Class.getConstructor0(Class.java:3082) at java.lang.Class.getDeclaredConstructor(Class.java:2178) at concurrency.singleton.EnumSingleton.main(EnumSingleton.java:22)

可以看到,反射攻击抛出异常了。

枚举类型避免序列化问题

public class Singleton implements Serializable { private volatile static Singleton singleton;
private Singleton() { }
public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; }
public static void main(String[] args) throws IOException, ClassNotFoundException {
File file = new File("singleton_file");
//第一次获取实例 EnumSingleton instance = EnumSingleton.INSTANCE.getInstance(); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file)); oos.writeObject(instance); ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
//第二次获取实例 EnumSingleton newInstance = (EnumSingleton) ois.readObject(); System.out.println(instance); System.out.println(newInstance); System.out.println("序列化前后两个实例是否相同:" + (instance == newInstance)); }}
// 输出INSTANCEINSTANCE序列化前后两个实例是否相同:true


以上是关于单例模式的 8 种实现的主要内容,如果未能解决你的问题,请参考以下文章

设计模式-单例模式的8种实现

单例模式的八种实现

模式--单例模式8种写法

java单例模式——详解JAVA单例模式及8种实现方式

8种单例模式的实现

设计模式1-单例模式的8种实现