单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并 提供一个全局访问点。单例模式是创建型模式。单例模式在现实生活中应用也非常广泛。 例如,国家主席、公司 CEO、部门经理等。在 J2EE 标准中,ServletContext、 ServletContextConfig 等;在 Spring 框架应用中 ApplicationContext;数据库的连接 池也都是单例形式。单例模式结构图如下
image.png
单例模式通常有5种常见写法:

1. 饿汉模式

在类加载阶段就已经生成实例,在有线程之前就生成实例,绝对的线程安全。这种写法有缺点,导入该类的时候就生成实例,这样会浪费很多资源。

  1. public class HungrySingleton {
  2. private final static HungrySingleton single = new HungrySingleton();
  3. private HungrySingleton(){}
  4. public static HungrySingleton getInstance(){
  5. return single;
  6. }
  7. }

2. 懒汉模式

只有当外部类调用的时候才会加载,先看一种简单的懒汉模式,这种模式可以保证线程的安全,但是当有很多线程使用的话容易造成性能下降。

  1. public class LazySingleton {
  2. private static LazySingleton singleton = null;
  3. private LazySingleton(){}
  4. public static synchronized LazySingleton getInstance(){
  5. if(singleton == null)
  6. singleton = new LazySingleton();
  7. return singleton;
  8. }
  9. }

3.双重检查锁

  1. public class DoubleCheckSingleton {
  2. private static volatile DoubleCheckSingleton singleton = null;
  3. private DoubleCheckSingleton(){}
  4. public static DoubleCheckSingleton getInstance(){
  5. if(singleton == null){
  6. synchronized (DoubleCheckSingleton.class){
  7. if(singleton == null){
  8. DoubleCheckSingleton singleton = new DoubleCheckSingleton();
  9. }
  10. }
  11. }
  12. return singleton;
  13. }
  14. }

为什么需要volatile修饰?
new 实例背后的指令
这个被忽略的问题在于 Cache cache=new Cache() 这行代码并不是一个原子指令。使用 javap -c指令,可以快速查看字节码。

  1. // 创建 Cache 对象实例,分配内存
  2. 0: new #5 // class com/query/Cache
  3. // 复制栈顶地址,并再将其压入栈顶
  4. 3: dup
  5. // 调用构造器方法,初始化 Cache 对象
  6. 4: invokespecial #6 // Method "<init>":()V
  7. // 存入局部方法变量表
  8. 7: astore_1

从字节码可以看到创建一个对象实例,可以分为三步:

  1. 分配对象内存
  2. 调用构造器方法,执行初始化
  3. 将对象引用赋值给变量。

虚拟机实际运行时,以上指令可能发生重排序。以上代码 2,3 可能发生重排序,但是并不会重排序 1 的顺序。也就是说 1 这个指令都需要先执行,因为 2,3 指令需要依托 1 指令执行结果。
Java 语言规规定了线程执行程序时需要遵守 intra-thread semanticsintra-thread semantics 保证重排序不会改变单线程内的程序执行结果。这个重排序在没有改变单线程程序的执行结果的前提下,可以提高程序的执行性能。
虽然重排序并不影响单线程内的执行结果,但是在多线程的环境就带来一些问题。
3 使用私有构造方法或者枚举类实现Singleton属性 - 图2
上面错误双重检查锁定的示例代码中,如果线程 1 获取到锁进入创建对象实例,这个时候发生了指令重排序。当线程1 执行到 t3 时刻,线程 2 刚好进入,由于此时对象已经不为 Null,所以线程 2 可以自由访问该对象。然后该对象还未初始化,所以线程 2 访问时将会发生异常。
上述的单例模式都存在一个问题,通过反射机制可以破坏单例模式,生成多个实例,如下代码。

public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Logger logger = Logger.getLogger("test");
        Class clazz = SingleDoubleCheckLazy.class;
        Constructor constructor = clazz.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        SingleDoubleCheckLazy singleDoubleCheckLazy1 = (SingleDoubleCheckLazy)constructor.newInstance();
        SingleDoubleCheckLazy singleDoubleCheckLazy2 = SingleDoubleCheckLazy.getInstance();
        logger.info("singleDoubleCheckLazy1 == singleDoubleCheckLazy2 " + String.valueOf(singleDoubleCheckLazy1 == singleDoubleCheckLazy2));
    }

//信息: singleDoubleCheckLazy1 == singleDoubleCheckLazy2 false

4. 静态内部类单例模式

不会被反射破坏掉的单例模式。静态内部类只有当用到该实例的时候才会加载,因此不会出现饿汉模式资源浪费的情况。

public class LazyInnerClassSingleton {
    //默认使用 LazyInnerClassGeneral 的时候,会先初始化内部类 //如果没使用的话,内部类是不加载的
    private LazyInnerClassSingleton() {
        if (LazyHolder.LAZY != null) {
            throw new RuntimeException("不允许创建多个实例");
        }
    }

    //每一个关键字都不是多余的
//static 是为了使单例的空间共享
//保证这个方法不会被重写,重载
    public static final LazyInnerClassSingleton getInstance() {
//在返回结果以前,一定会先加载内部类
        return LazyHolder.LAZY;
    }

    //默认不加载
    private static class LazyHolder {
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

序列化会破坏单例模式,如下所示

public class SingleHungrySerializable implements Serializable {
    private final static SingleHungrySerializable singleHungrySerializable = new SingleHungrySerializable();
    private SingleHungrySerializable(){}
    public static SingleHungrySerializable getInstance(){
        return singleHungrySerializable;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        SingleHungrySerializable s1 = SingleHungrySerializable.getInstance();
        SingleHungrySerializable s2 = null;
        FileOutputStream fos = new FileOutputStream("SeriableSingleton.obj");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(s1);
        oos.flush();
        oos.close();

        FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
        ObjectInputStream ois = new ObjectInputStream(fis);
        s2 = (SingleHungrySerializable) ois.readObject();
        ois.close();
        System.out.println(s1);
        System.out.println(s2);
        System.out.println(s1 == s2);
    }
}

运行结果中,可以看出,反序列化后的对象和手动创建的对象是不一致的,实例化了两 次,违背了单例的设计初衷。那么,我们如何保证序列化的情况下也能够实现单例?其实很简单,只需要增加 readResolve()方法即可。来看优化代码

5. 枚举类单例(最好的单例模式)

枚举类的单例模式可以防止,反射,序列化的攻击,是最安全的单例模式

public class EnumSingleton implements Serializable {
    static class Person implements Serializable{
        @Override
        public String toString() {
            return "Person{" +
                    "a='" + a + '\'' +
                    ", date=" + date +
                    ", id=" + id +
                    '}';
        }

        private String a = null;
        private Date date = null;
        private int id = 0;
        public Person(String a, Date date, int id){
            this.a = a;
            this.date = date;
            this.id = id;
            System.out.println("create the person");
        }
    }
    public enum Singleton{
        INSTANCE;
        private Person instance = null;
        private Singleton(){
            instance = new Person("single person", new Date(), 1);
        }
        public Person getInstance(){
            return instance;
        }
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Person s1 = Singleton.INSTANCE.getInstance();
        Person s2 = null;
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(s1);
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bais);
        s2 = (Person)ois.readObject();
        System.out.println(s1);
        System.out.println(s2);
        System.out.println(s1 == s2);
    }
}
//create the person
// Person{a='single person', date=Fri Nov 27 13:30:39 CST 2020, id=1}
//Person{a='single person', date=Fri Nov 27 13:30:39 CST 2020, id=1}
//false