单例模式

简单的说单例模式的作用就是保证整个应用程序的生命周期中,任何一时刻,单例类的实例都只存在一个。

饿汉式

public class Singleton {
    /**
     * 饿汉式
     */
    private static Singleton singleton=new Singleton();
    private Singleton (){}
    public static Singleton getInstance(){
        return singleton;
    }
}

饿汉式单例本身是线程安全的,但它采用空间换取时间的方式,当类加载时马上就实例化Singleton对象,不管使用者用不用,后续每次调用 getInstance() 方法的时候,就不需要判断它是否实例化,从而节约了时间。但有些情况下需要懒加载实例化对象,针对这种情形,于是有了懒汉式的单例模式。

懒汉式

public class Singleton {
    /**
     * 懒汉式
     */
    private static Singleton singleton;
    private Singleton (){}
    public static Singleton getInstance(){
        if(singleton==null){
            singleton=new Singleton();
        }
        return singleton;
    }
}

这就是我们常见的懒汉式单例模式!这种懒汉式单例模式在多线程环境下,存在线程安全的问题!

双重检验锁

线程安全,延迟初始化。这种方式采用双锁机制,安全且在多线程情况下能保持高性能。

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;
    }
}

双重检查模式,进行了两次的判断,第一次是为了避免不要的实例,第二次是为了进行同步,避免多线程问题。由于singleton=new Singleton()对象的创建在JVM中可能会进行重排序,在多线程访问下存在风险,使用volatile修饰signleton实例变量有效,解决该问题。

  • 双重检验锁也有问题,因为它无法解决反射对单例模式的破坏性。我将在静态内部类单例模式中加以阐述。

静态内部类

public class Singleton {
    /**
     * 静态内部类
     */
    private Singleton (){}

    private static final class SingletonHolder{
       private static Singleton singleton=new Singleton();
    }

    private static Singleton getInstance(){
        return SingletonHolder.singleton;
    }
}

静态内部类

在了解静态内部类单例模式之前,让我们先了解一下静态内部类的两个知识。

  • 静态内部类加载一个类时,其内部类不会同时被加载。
  • 一个类被加载,当且仅当其某个静态成员如静态域、构造器、静态方法等被调用时才会被加载。

我们先看一个静态内部类的测试,以验证上面这两个观点。

package com.chloneda.jutils.test;

public class OuterClassTest {

    private OuterClassTest() {}

    static {
        System.out.println("1、我是外部类静态模块...");
    }

    // 静态内部类
    private static final class StaticInnerTest {
        private static OuterClassTest oct = new OuterClassTest();

        static {
            System.out.println("2、我是静态内部类的静态模块... " + oct);
        }

        static void staticInnerMethod() {
            System.out.println("3、静态内部类方法模块... " + oct);
        }
    }

    public static void main(String[] args) {
        OuterClassTest oct = new OuterClassTest(); // 此刻内部类不会被加载
        System.out.println("===========分割线===========");
        OuterClassTest.StaticInnerTest.staticInnerMethod(); // 调用内部类的静态方法
    }
}

输出如下。

1、我是外部类静态模块...
=========分割线=========
2、我是静态内部类的静态模块... com.chloneda.jutils.test.OuterClassTest@b1bc7ed
3、静态内部类的方法模块... com.chloneda.jutils.test.OuterClassTest@b1bc7ed

从运行结果来看,验证是正确的!

由于静态内部类的特性,只有在其被第一次引用的时候才会被加载,所以可以保证其线程安全性。

由此可得出静态内部类单例模式的写法。

静态内部类

package com.chloneda.jutils.test;

/**
 *  单例模式使用静态内部类方式实现,优点:实现代码简洁、延迟初始化、线程安全
 */
public class Singleton {

    private static final class SingletonHolder {
        private static Singleton INSTANCE = new Singleton();
    }

    private Singleton(){}

    public static Singleton getInstance(){
        return SingletonHolder.INSTANCE;
    }
}

这种写法的单例,外部无法访问静态内部类 SingletonHolder,只有当调用 Singleton.getInstance() 方法的时候,才能得到单例对象 INSTANCE。

而且静态内部类单例的 getInstance() 方法中没有使用 synchronized 关键字,提高了执行效率,同时兼顾了懒汉模式的内存优化(使用时才初始化,节约空间,达到懒加载的目的)以及饿汉模式的安全性。

但这种单例也有问题!这种方式需要两个类去做到这一点,也就是说,虽然懒加载静态内部类的对象,但其 外部类及内部静态类的 Class 对象还是会被创建,同时也无法防止反射对单例的破坏性(很多单例的写法都有这个通病),从而无法保证对象的唯一性。

我们通过以下测试类测试反射对静态内部类的破坏性。

/**
 * @Description: 反射破坏静态内部类单例模式的测试类
 */
public class SingletonReflectTest {
    public static void main(String[] args) {
        //创建第一个实例
        Singleton instance1 = Singleton.getInstance();

        //通过反射创建第二个实例
        Singleton instance2 = null;
        try {
            Class<Singleton> clazz = Singleton.class;
            Constructor<Singleton> cons = clazz.getDeclaredConstructor();
            cons.setAccessible(true);
            instance2 = cons.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }

        //检查两个实例的hash值
        System.out.println("Instance1 hashCode: " + instance1.hashCode());
        System.out.println("Instance2 hashCode: " + instance2.hashCode());
    }
}

输出结果如下。

Instance1 hashCode: 186370029
Instance2 hashCode: 2094548358

从输出结果可以看出,通过反射获取构造函数,并调用 setAccessible(true) 就可以调用私有的构造函数,从而得到Instance1和Instance2两个不同的对象。

静态内部类改进

如何防止这种反射对单例的破坏呢?我们可以通过修改构造器,让它在被要求创建第二个实例的时候抛出异常。

静态内部类修改如下。

/**
 * @Description: 防止反射破坏静态内部类单例模式
 */
public class Singleton {

    private static boolean initialized = false;

    private static final class SingletonHolder {
        private static Singleton INSTANCE = new Singleton();
    }

    private Singleton() {
        synchronized (Singleton.class) {
            if (initialized == false) {
                initialized = !initialized;
            } else {
                throw new RuntimeException("单例模式禁止二次创建,防止反射!");
            }
        }
    }

    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
    
}

我们还用一个 SingletonReflectTest 测试类测试一下,输出结果如下。

java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
    at com.chloneda.jutils.test.SingletonReflectTest.main(Singleton.java:46)
Caused by: java.lang.RuntimeException: 单例模式禁止二次创建,防止反射!
    at com.chloneda.jutils.test.Singleton.<init>(Singleton.java:24)
    ... 5 more
Instance1 hashCode: 1053782781
Exception in thread "main" java.lang.NullPointerException
    at com.chloneda.jutils.test.SingletonReflectTest.main(Singleton.java:53)

所以我们通过修改构造器防止反射对单例的破坏性。

但是这种方式的单例也存在问题!什么问题呢?即序列化和反序列化之后无法继续保持单例(很多单例的写法也有这个通病)。

我们让上面防止反射破坏静态内部类的单例实现 Serializable 接口。

public class Singleton implements Serializable 

并通过以下测试类进行序列化和反序列化测试。

/**
 * @Description: 序列化破坏静态内部类单例模式的测试类
 */
pubic class SingletonSerializableTest {
    public static void main(String[] args) {
        try {
            Singleton instance1 = Singleton.getInstance();
            ObjectOutput out = null;

            out = new ObjectOutputStream(new FileOutputStream("Singleton.ser"));
            out.writeObject(instance1);
            out.close();

            //从文件中反序列化一个Singleton对象
            ObjectInput in = new ObjectInputStream(new FileInputStream("Singleton.ser"));
            Singleton instance2 = (Singleton) in.readObject();
            in.close();

            System.out.println("instance1 hashCode: " + instance1.hashCode());
            System.out.println("instance2 hashCode: " + instance2.hashCode());

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

输出结果如下。

instance1 hashCode: 240650537
instance2 hashCode: 1566502717

从结果可以看出,很明显不是同一个单例对象!

那如何解决这个问题呢?

静态内部类再改进

我们可以实现 readResolve() 方法,它代替了从流中读取对象,确保了在序列化和反序列化的过程中没人可以创建新的实例。

可以得到改进版的静态内部类单例,可以有效防止序列化及反射的破坏!

package com.chloneda.jutils.test;

import java.io.*;

/**
 * @Description: 可以防止序列化及反射破坏的静态内部类单例模式
 */
public class Singleton implements Serializable {

    private static boolean initialized = false;

    private static final class SingletonHolder {
        private static Singleton INSTANCE = new Singleton();
    }

    private Singleton() {
        synchronized (Singleton.class) {
            if (initialized == false) {
                initialized = !initialized;
            } else {
                throw new RuntimeException("单例模式禁止二次创建,防止反射!");
            }
        }
    }

    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }

    private Object readResolve() {
        return getInstance();
    }
}

我们再用上面的 SingletonSerializableTest 测试类测试一下结果。

输出结果如下。

instance1 hashCode: 240650537
instance2 hashCode: 240650537

此时就说明,单例在序列化和反序列化时的对象是一致的了。

其实上面饿汉式、懒汉式、双重校验锁及静态内部类单例所出现的问题,都可以通过枚举型单例进行解决,这也是《Effective Java》中推荐的写法。

枚举单例模式

public enum Singleton {
    INSTANCE;
}

默认枚举实例的创建是线程安全的,并且在任何情况下都是单例。实际上

  • 枚举类隐藏了私有的构造器。
  • 枚举类的域 是相应类型的一个实例对象
    那么枚举类型日常用例是这样子的:
public enum Singleton  {
    INSTANCE 
 
    //doSomething 该实例支持的行为
      
    //可以省略此方法,通过Singleton.INSTANCE进行操作
    public static Singleton get Instance() {
        return Singleton.INSTANCE;
    }
}

枚举单例模式在《Effective Java》中推荐的单例模式之一。但枚举实例在日常开发是很少使用的,就是很简单以导致可读性较差。
在以上所有的单例模式中,推荐静态内部类单例模式。主要是非常直观,即保证线程安全又保证唯一性。
众所周知,单例模式是创建型模式,都会新建一个实例。那么一个重要的问题就是反序列化。当实例被写入到文件到反序列化成实例时,我们需要重写readResolve方法,以让实例唯一。

private Object readResolve() throws ObjectStreamException{
        return singleton;
}

参考:
https://www.cnblogs.com/chloneda/p/pattern-singleton.html
https://www.jianshu.com/p/3bfd916f2bb2

原文地址:https://www.cnblogs.com/PoetryAndYou/p/12300542.html