单例的实现方法总结

单例的实现方法总结

以下的内容不涉及基础,比如什么是单例?JVM类加载顺序?等等。

仅仅是对所有单例的实现方法进行汇总。

一、最经典的饿汉模式实现方式

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

另外一个变种的实现方法,是将静态成员改为静态代码块

public class Singleton1_2 {
    private static Singleton1_2 instance;
    static {
        instance = new Singleton1_2();
    }
    private Singleton1_2 (){}
    public static Singleton1_2 getInstance() {
        return instance;
    }
}

不管怎么写,本质上利用的都是“类的初始化过程(包含静态成员赋值,以及静态代码块的执行),只在类被加载到内存时执行一次”这一特性。

  1. 优点

  • 由于其原理,天然就是线程安全的
  • 结构简单理解容易
  1. 缺点

  • 相对于懒汉模式,饿汉模式最麻烦的地方在于,如果创建单例类的对象要依赖参数或者外部配置文件的话,也就是说,业务场景需要在调用getInstance方法时传入参数,决定用何种方式创建单例实例的话,饿汉模式就无法使用了。

二、懒汉模式实现方法

public class Singleton2 {
    private static Singleton2 instance;
    private Singleton2 (){
    }
    public static synchronized Singleton2 getInstance() {
        if (instance == null) {
            instance = new Singleton2();
        }
        return instance;
    }
}
  1. 优点

  • 在必须使用延迟加载的场景下,替代饿汉模式
  1. 缺点

  • 最重要的一点,为了确保线程安全,必须使用synchronized关键字进行同步,影响性能。

三、双重检查方法

双重检查其实就是对于懒汉模式的一种性能改进,减小了synchronized关键字锁定的代码块范围。
第二重检查的作用是:防止有别的线程,在第一重检查和拿锁之间创建了单例实例。

public class Singleton3 {
    private volatile static Singleton3 instance;
    private Singleton3 (){
    }
    public static Singleton3 getSingleton() {
        if (instance == null) {
            synchronized (Singleton3.class) {
                if (instance == null) {
                    instance = new Singleton3();
                }
            }
        }
        return instance;
    }
}

四、静态内部类方法

这种方法,也是利用了类加载的特性,在getInstance()方法调用静态内部类的静态成员变量时,静态内部类SingletonHolder才会被初始化,创建单例实例。
(复习:使用 Class.staticMember 方式引用类的静态成员变量,属于对类进行主动引用,在这种情况下会触发类加载的初始化过程)

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

优点

  • 延迟加载
  • 无锁,没有性能损耗
  • 天然线程安全

五、枚举类

这是一种最简洁但是最难理解的单例实现方法。但是《Effective Java》评价这是实现单例的最佳方法(参看该书第3条)

public enum Singleton5 {
    /**
     * 枚举实现单例
     */
    INSTANCE;
    public void businessMethod() {
    }
}  

其调用方法如下:

Singleton5.INSTANCE.businessMethod()

下面解释枚举类为什么能实现单例。

  1. 首先对于Singleton5编译好的class进行反编译

因为enum只是一个关键字,不是超类或者其他能看到源码的东西。因此利用反编译的手段来确认内部实现(利用jad)。

package singleton;


public final class Singleton5 extends Enum
{

    public static Singleton5[] values()
    {
        return (Singleton5[])$VALUES.clone();
    }

    public static Singleton5 valueOf(String name)
    {
        return (Singleton5)Enum.valueOf(singleton/Singleton5, name);
    }

    private Singleton5(String s, int i)
    {
        super(s, i);
    }

    public void businessMethod()
    {
    }

    public static final Singleton5 INSTANCE;
    private static final Singleton5 $VALUES[];

    static 
    {
        INSTANCE = new Singleton5("INSTANCE", 0);
        $VALUES = (new Singleton5[] {
            INSTANCE
        });
    }
}

  1. 枚举如何保证线程安全

可以看到,使用enum关键字的话,实际会生成一个继承了Enum,并且final的类。

public final class Singleton5 extends Enum

注意下面的这一段:

    public static final Singleton5 INSTANCE;
    private static final Singleton5 $VALUES[];

    static 
    {
        INSTANCE = new Singleton5("INSTANCE", 0);
        $VALUES = (new Singleton5[] {
            INSTANCE
        });
    }
  • 根据类加载过程,在“链接”的“准备”阶段,静态且final的静态成员INSTANCE被加载到了方法区(如果这里有赋值操作的话就有值了,不会等到初始化阶段,这是final与其他不同的地方)。
  • 到了初始化阶段,会执行静态代码块内的内容,开辟内存空间存放单例实例,并将地址赋值给INSTANCE。
  • 类加载过程是只会执行一次的,所以本质上还是利用jvm规定的类加载过程,形成了天然的线程安全
  • 另外,构造函数 new Singleton5(“INSTANCE”, 0) 实际上调用是 super,也就是 Enum 类的构造函数,第一个参数是枚举名称(name),第二个参数是顺序(ordinal)。
  1. 解决反序列化问题

  • 前面除了枚举类以外的单例的实现方法,都有一个弱点,如果需要进行序列化的话(implements Serializable),那么在反序列化的时候,每次调用readObject()方法都会生成一个不同于原单例的新实例,单例失效。
  • 对于枚举,为了保证枚举类型符合Java相关规范(JSR),每一个枚举类及其定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java有特殊处理
  • 序列化的时候,Java仅将枚举类的name属性输出到结果中,反序列化的时候通过Enum的valueOf方法来根据名字查找枚举对象。
    public static <T extends Enum<T>> T valueOf(Class<T> enumType,
                                                String name) {
        T result = enumType.enumConstantDirectory().get(name);
        if (result != null)
            return result;
        if (name == null)
            throw new NullPointerException("Name is null");
        throw new IllegalArgumentException(
            "No enum constant " + enumType.getCanonicalName() + "." + name);
    }

  • 而valueOf()调用enumConstantDirectory(),继而调用getEnumConstantsShared(),可以看到里面实际调用的是反编译出来的那段代码里的value()方法,使用的实际上就是那个$VALUES[]。
    Map<String, T> enumConstantDirectory() {
        if (enumConstantDirectory == null) {
            T[] universe = getEnumConstantsShared();
            if (universe == null)
                throw new IllegalArgumentException(
                    getName() + " is not an enum type");
            Map<String, T> m = new HashMap<>(2 * universe.length);
            for (T constant : universe)
                m.put(((Enum<?>)constant).name(), constant);
            enumConstantDirectory = m;
        }
        return enumConstantDirectory;
    }
    T[] getEnumConstantsShared() {
        if (enumConstants == null) {
            if (!isEnum()) return null;
            try {
                final Method values = getMethod("values");
                java.security.AccessController.doPrivileged(
                    new java.security.PrivilegedAction<Void>() {
                        public Void run() {
                                values.setAccessible(true);
                                return null;
                            }
                        });
                @SuppressWarnings("unchecked")
                T[] temporaryConstants = (T[])values.invoke(null);
                enumConstants = temporaryConstants;
            }
            // These can happen when users concoct enum-like classes
            // that don't comply with the enum spec.
            catch (InvocationTargetException | NoSuchMethodException |
                   IllegalAccessException ex) { return null; }
        }
        return enumConstants;
    }
  • 所以枚举即使被反序列化也不会创建对象。
  • 所以枚举即使被反序列化也不会创建对象。
  • 所以枚举即使被反序列化也不会创建对象。

六、应对多个类加载器

前面的所有方法都有一个共通的问题:被多个类加载器加载。

这问题不算是钻牛角尖,一些热启动机制的框架,就是利用多个类加载器实现的,这时候确实有可能造成单例变成多例。

在网上找到了一段代码来解决这个问题,就是增加下面这个私有静态类。

原理是在被调用getClass方法时,直接利用自身原来的类加载器进行类加载,确保自始至终一直是同一个类加载器在加载单例类。

private static Class getClass(String classname) throws ClassNotFoundException {     
      ClassLoader classLoader = Thread.currentThread().getContextClassLoader();     
      
      if(classLoader == null)     
         classLoader = Singleton.class.getClassLoader();     
      
      return (classLoader.loadClass(classname));     
   }     
}  
发布了33 篇原创文章 · 获赞 19 · 访问量 3169

猜你喜欢

转载自blog.csdn.net/zmflying8177/article/details/100553498