单例模式及其4种推荐写法和3类保护手段

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/alinyua/article/details/79776613

本文介绍了单例模式及其4种推荐写法(饿汉模式,双重校验锁(DCL),Holder模式(静态内部类)和枚举模式)和3类保护手段(反序列化,反射,自定义类加载器)

单例模式(Singleton Pattern)的定义:

Ensure a class has only one instance,and provide a global point of access to it.
确保某一个类只有一个实例,并且自行实例化并向整个系统提供这个实例.

使用单例模式的优点

  1. 节省内存
  2. 减少性能开销
  3. 避免对资源的多重占用

单例模式的使用场景

  1. 无状态的工具类:比如日志工具类,不管是在哪里使用,我们需要的只是它帮我们记录日志信息,除此之外,并不需要在它的实例对象上存储任何状态,这时候我们就只需要一个实例对象即可。
  2. 全局信息类:比如我们在一个类上记录网站的访问次数,我们不希望有的访问被记录在对象A上,有的却记录在对象B上,这时候我们就让这个类成为单例。
  3. 实例化需要消耗过多资源的类
  4. 要求生成唯一序列化的环境

值得注意的是,单例往往都可以通过直接声明为static来实现,把一个实例方法变成静态方法,或者把一个实例变量变成静态变量,都可以起到单例的效果。这只是面向对象和面向过程的区别。

单例模式的4种推荐写法

这里列举了单例模式的4种安全的推荐写法,另外也可以直接在获取单例的方法上加synchronized,这种方法虽然安全但效率极低,不值得推荐.这里4种推荐写法分别为饿汉模式,双重校验锁(DCL),Holder模式(静态内部类)枚举模式.

1. 饿汉模式
public class Singleton {
    private final static Singleton INSTANCE= new Singleton ();
    private Singleton () {
    }
    public static Singleton getInstance() {  
        return INSTANCE;  
    }  
}
  1. 原理:通过类加载机制来保证单例,在类被加载的时候创建INSTANCE对象
  2. 优点:较简单
  3. 缺点:导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到lazy loading的效果。
2.双重校验锁(DCL)
public class Singleton {
   private volatile static Singleton instance = null;

   private Singleton () {
   }

   public static Singleton getInstance() {
       if(instance == null) {
           synchronized(Singleton.class) {
               if(instance == null) {
                   instance = new Singleton();
               }
           }
       }
       return instance;
   }
}
  1. 原理:通过双重锁机制和volatile关键字保证了单例
  2. 优点:只有需要单例对象又没有单例对象的时候才会创建,最大化地实现了lazy loading
  3. 缺点:复杂,效果和Holder模式相同,且更不易理解(虽然有些书可能会说加了volatile会影响性能,但其实影响微乎其微)
3.Holder模式(静态内部类)
public class Singleton {  
    private static class SingletonHolder {  
        public static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){
    }  
    public static final Singleton getInstance() {  
        return SingletonHolder.INSTANCE;  
    }  
}  
  1. 原理:又称为”延迟初始化占位类模式”,顾名思义,就是通过一个内部类占位来实现延迟初始化,原理也是通过类加载机制来保证单例,和饿汉模式的区别在于用一个内部类来减小由于类的其他静态方法被调用而导致单例被提前创建的可能性.
  2. 优点:最大化地实现了lazy loading,且更简单
5.枚举模式
public enum Singleton {  
    INSTANCE;
}  
  1. 原理:通过枚举的语言特性保证单例
  2. 优点:简洁,从JVM层面保证单例,更安全(不存在序列化,反射破坏单例的情况)

4种单例模式实现的比较

单例写法 单例保障机制 单例对象初始化时机 优点 缺点
饿汉模式 类加载机制 类加载 简单,易理解 难以保证懒加载,无法应对反射和反序列化
双重校验锁(DCL) 锁机制(需volatile防止重排序) 第一次调用getInstance() 实现懒加载 复杂,无法应对反射和反序列化
Holder模式(静态内部类) 类加载机制 第一次调用getInstance() 实现懒加载 无法应对反射和反序列化
枚举 枚举语言特性 第一次引用枚举对象 简洁,安全(语言级别防止通过反射和反序列化破坏单例)

防止非法创建单例对象的手段:

1.反序列化

前三种写法在实现序列化(implements java.io.Serializable)时为保证单例都需要重写readResolve()方法,如

public class Singleton implements java.io.Serializable {     
   public final static Singleton INSTANCE = new Singleton();     
   protected Singleton() {     
   }  
   public static final Singleton getInstance() {  
        return SingletonHolder.INSTANCE;  
    } 
   //反序列化时直接返回当前INSTANCE而不是反序列化出来的对象
   private Object readResolve() {     
            return INSTANCE;     
     }  

}   

然后再通过例子看看枚举为什么不会受到反序列化的破解

public enum  SingletonEnum {
    INSTANCE;
    private String name;
    public String getName(){
        return name;
    }
    public void setName(String name){
        this.name = name;
    }
}

在枚举类型的序列化和反序列化上,Java做了特殊的规定:
在序列化时Java仅仅是将枚举对象的name属性输出到结果中,
反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象
同时,编译器是不允许任何对这种序列化机制的定制的并禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法,从而保证了枚举实例的唯一性

2.反射

前三种写法默认无法应对反射(先把私有的构造器设为accessible,再用newInstance()方法),但newInstance()也是通过new来获得实例的,所以可以从构造方法入手,进行保护,如

public class Singleton implements java.io.Serializable {     
   public final static Singleton INSTANCE = new Singleton(); 
   //初始化标识
   private static volatile  boolean  flag = true;  

   public static final Singleton getInstance() {  
        return SingletonHolder.INSTANCE;  
    }    
    //在构建实例之前判断是否已经初始化过了,如果初始化过一次构造方法仍被调用则抛出异常
    private Singleton(){
        if(flag){
            flag = false;   
        }else{
            throw new RuntimeException("The instance  already exists !");
        }
    }


} 

再解释一下枚举单例不受反射破坏的原因,直接看源码:

 public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }

        //这里判断Modifier.ENUM是不是枚举修饰符,如果是就抛异常
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");

        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

说明确实无法使用反射创建枚举实例

3.使用不同的类加载器

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性
—— 《深入理解Java虚拟机》 第7章- 虚拟机类加载机制

如果是使用自定义的类加载器进行加载,就可以产生新的实例.(枚举也无能为力)
假定不是远端存取,例如一些servlet容器对每个servlet使用完全不同的类装载器,这样的话如果有两个servlet访问一个单例类,它们就都会有各自的实例。
这里有个获得单例类对象的方法

private static Class getClass(String classname)      
                                         throws ClassNotFoundException {     
      ClassLoader classLoader = Thread.currentThread().getContextClassLoader();     

      if(classLoader == null)     
         classLoader = Singleton.class.getClassLoader();     

      return (classLoader.loadClass(classname));     
   }     
}  

这段代码目的是每次获取的时候能获得同一个类加载器,然后再通过这个加载器去加载单例类来保证唯一性,所以这只是提供了获取同一个类加载器加载的单例类的途径,而非提供保护.
但是,从另一个方面来理解,使用不同的类加载器获得的本身就不是同一个类了,不同的类的实例仍然各只有一个,从这个角度看单例模式也不算被破坏,总不能要求两个不同的类的实例之和只能为1吧.

参考:
http://cantellow.iteye.com/blog/838473
https://zhuanlan.zhihu.com/p/32310340
https://blog.csdn.net/javazejian/article/details/71333103
《深入理解Java虚拟机》–周志明

阅读更多

没有更多推荐了,返回首页