Java 单例模式
2023-06-10

基本概念

顾名思义,单例模式,是指一个类只能生成一个唯一的对象。单例模式主要用于一些无状态的组件或者本质上唯一的组件。

实现单例模式,有比较多的方法,各种不同方法,各有优劣和适用场景。

一个单例模式的实现,需要按需实现以下功能:

  • 如果这个单例会被多个线程访问,则需要实现并发安全;
  • 考虑防止单例特性被反射方式破坏;
  • 考虑方式单例特性被序列化方式破坏。

几种常见单例模式

饿汉式单例模式

基础实现

饿汉式单例模式,主要是指构造方法私有,实例对象在类加载的时候就初始化,用共有方法提供对象访问点。代码如下所示:

public class HungrySingleton {
    private static final HungrySingleton hungrySingleton = new HungrySingleton();

    private HungrySingleton(){};

    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
}

这种单例模式优点是线程安全的,而且是无锁的,缺点有两个,一是每个实例不论是否调用,都被初始化,如果单例对象比较多,可能会有性能问题;二是私有化构造器不是绝对安全的,反射方式调用构造器是可以创建多个对象的。

针对缺点一,后文中的懒汉式单例模式是一种解决方案。对于缺点二,我们可以在构造器中做出限制,一旦多次调用构造器,就抛出异常。

防止反射破坏单例特性

为了防止单例特性被反射破坏,我们在构造器中加入限制,修改后的饿汉式单例模式如下:

public class HungrySingleton {
    private static final HungrySingleton hungrySingleton = new HungrySingleton();

    private HungrySingleton(){
        if (hungrySingleton != null) {
            throw new RuntimeException("Could not construct more than one object of a Singleton.");
        }
    };

    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
}

我们用下面的测试代码测试前面的两个单例模式代码,

import java.lang.reflect.Constructor;

public class HungrySingletonTest {
    public static void main(String[] args) {
        try {
            Class<?> cla = HungrySingleton.class;
            Constructor<?> c = cla.getDeclaredConstructor((Class<?>) null);
            c.setAccessible(true);
            Object o1 = c.newInstance();
            Object o2 = c.newInstance();
            System.out.println(o1 == o2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

没有限制的饿汉单例模式,会遭到破坏,产生两个实例,输出 false,而后一个对构造函数做了限制的代码会按照预期抛出运行时异常,组织单例模式被破坏。

防止序列化破坏单例特性

下面是实现序列化接口的单例模式:

public class HungrySingleton implements Serializable {
    private static final HungrySingleton hungrySingleton = new HungrySingleton();

    private HungrySingleton(){
        if (hungrySingleton != null) {
            throw new RuntimeException("Could not construct more than one object of a Singleton.");
        }
    };

    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
}

使用以下代码测试:

public class HungrySingletonTest {
    public static void main(String[] args) {
        HungrySingleton singleton1 = null;
        HungrySingleton singleton2 = HungrySingleton.getInstance();


        try {
            FileOutputStream fos = new FileOutputStream("Singleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(singleton2);
            oos.flush();
            oos.close();

            FileInputStream fis = new FileInputStream("Singleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            singleton1 = (HungrySingleton) ois.readObject();
            ois.close();

            System.out.println(singleton1);
            System.out.println(singleton2);
            System.out.println(singleton1 == singleton2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

得到的输出:

com.coolxxy.HungrySingleton@4590c9c3
com.coolxxy.HungrySingleton@737996a0
false

说明序列化破坏了单例模式,要解决这个问题,只需要实现 readResolve 方法:

public class HungrySingleton implements Serializable {
    private static final HungrySingleton hungrySingleton = new HungrySingleton();

    private HungrySingleton(){
        if (hungrySingleton != null) {
            throw new RuntimeException("Could not construct more than one object of a Singleton.");
        }
    };
    private Object readResolve() {
        return hungrySingleton;
    }

    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
}

再进行同样的测试,得到的输出如下:

com.coolxxy.HungrySingleton@737996a0
com.coolxxy.HungrySingleton@737996a0
true

至于为何添加这个方法的实现就可以解决序列化破坏单例问题,一句话粗略解释,就是如果实现了 readResolve 这个方法,反序列化时 readObject 方法会最终返回 readResolve 的执行结果,也就是我们指定的那个单例对象。这个点的详细解释,可以看这个链接 掘金:单例、序列化和readResolve()方法

懒汉式单例模式

懒汉单例模式是指,延迟单例对象的初始化,直到需要这个对象的时候,再初始化它,按照懒汉单例模式的语义,写出以下
代码:

public class LazySimpleSingleton {
    private LazySimpleSingleton(){};
    private static LazySimpleSingleton lazySimpleSingleton = null;
    public static  LazySimpleSingleton getInstance() {
        if (lazySimpleSingleton == null) {
            lazySimpleSingleton = new LazySimpleSingleton();
        }
        return lazySimpleSingleton;
    }
}

以上的代码中,当 getInstance 被调用时,检测是否被初始化,从而把初始化延迟到调用期,而不是类加载时。

但是以上代码不是并发安全的,因为 getInstance 函数里面的代码不是原子的,可能有多个线程同时检测到符合 if 语句的判断条件,从而各自进行初始化。针对此问题,最简单的修改方式是把 getInstance 方法加上 synchronized 关键字,将其变成同步方法,这样就解决了线程安全问题。

但是直接锁定 getInstance 方法,会引入性能问题,因为可能同时有多个线程访问单例对象,那么会造成大量线程阻塞,所以我们可以改成所谓的双重检查锁定单例代码,如下所示:

public class LazySimpleSingleton {
    private LazySimpleSingleton(){};
    private volatile static LazySimpleSingleton lazySimpleSingleton = null;
    public static LazySimpleSingleton getInstance() {
        if (lazySimpleSingleton == null) {
            synchronized (LazySimpleSingleton.class) {
                if(lazySimpleSingleton == null) {
                    lazySimpleSingleton = new LazySimpleSingleton();
                }
            }
        }
        return lazySimpleSingleton;
    }
}

这里的优化思路,主要是缩小加锁区域到初始化单例对象,而不是锁定整个 getInstance 方法,这样就可以减少因为获取不到锁而导致的阻塞。但是如果只是做这样的先检查再锁定是不对的,因为存在指令重排序,可能会发生,第一个访问单例对象的线程,执行 new LazySimpleSingleton() 时,初始化内存区域被重排到了设置对象引用之后,那么后面的其他线程会检测到单例对象不为 null 而直接去获取单例对象,从而获取到初始化未完成的对象,出现严重错误。因此把 lazySimpleSingleton 声明为 volatile 禁止指令重排是必须的。

实现懒汉式单例模式还有一种思路是基于静态内部类实现,代码如下:

public class HungrySingleton implements Serializable {
    private static class InstanceHolder {
        public static HungrySingleton hungrySingleton = new HungrySingleton();
    }

    public static HungrySingleton getInstance() {
        return InstanceHolder.hungrySingleton;
    }
}

这种思路首先用一个静态内部类持有单例对象,而静态内部类会在使用的时候,也就是调用外部类的 getInstance 时候再初始化,从而达到延迟初始化的目标。而单例的实现是依靠 jvm 的类加载机制,jvm 类加载时候,会用 Class 对象的初始化锁保证类被唯一加载,从而保证了单例特性。

针对懒汉单例模式解决序列化和反射破坏的思路是基本一致的,这里不在详细展开。

另外一点是,延迟初始化减少了初始化类的开销,但是初始化迟早是要做的,延迟初始化意味着首次访问时的开销必然增大。除非明确需要延迟初始化,否则没必要做这些额外工作。

枚举单例模式

利用枚举的特性,可以使用以下代码构建一个符合单例模式要求的 Enum:

public enum EnumSingleton {
    INSTANCE;
    public static EnumSingleton getInstance() {
        return INSTANCE;
    }
}

这种单例模式的代码非常优雅,天然线程安全,而且 Enum 的 JDK 实现,天然保障了即使刻意使用反射和序列化,也无法破坏单例特性。再没有其他限制的场景下,这是一种最简单优雅的单例模式,但是如果单例必须扩展一个超类来实现的话,这种方式不是很适合。(Enum 单例模式也是 《effective java》一书中推荐的方式)

总结

本文介绍了多种实现单例模式的方式,总结一个简单的单例模式实现思路:

  • 如果没有额外限制,就用 Enum 实现单例;
  • 否则,如果没有特殊要求,就用饿汉式,也就是不要延迟初始化;
  • 否则,考虑延迟懒汉式,其中 volatile 加双重检测锁定比较适合实例字段需要延迟初始化场景,静态内部类适合对静态字段延迟初始化;
  • 如果使用了 Enum 之外的方式,需要考虑序列化破坏和反射破坏。