目录

创建型_单例模式

概述

单例模式的定义就是确保某一个类只有一个实例,并且提供一个全局访问点。属于设计模式三大类中的创建型模式

单例模式具有典型的三个特点

  • 只有一个实例。
  • 自我实例化。
  • 提供全局访问点。

优缺点

优点

由于单例模式只生成了一个实例,所以能够节约系统资源,减少性能开销,提高系统效率,同时也能够严格控制客户对它的访问。

缺点

也正是因为系统中只有一个实例,这样就导致了单例类的职责过重,违背了“单一职责原则”,同时也没有抽象类,这样扩展起来有一定的困难。

常见应用场景

  • 网站计数器。
  • 项目中用于读取配置文件的类。
  • 线程池,数据库连接池。
  • 缓存
  • 日志对象
  • Spring中,每个Bean默认都是单例的,这样便于Spring容器进行管理。
  • Servlet中 Application
  • Windows中任务管理器,回收站。 等等。

实现方式

饿汉模式

1
2
3
4
public class SingleTon{
   private static SingleTon INSTANCE = new SingleTon();
 private SingleTon(){}
public static SingleTon getInstance(){ return INSTANCE; }}

在类加载(初始化阶段)时就完成了初始化,所以类加载比较慢,但获取对象的速度快。以空间换时间,故不存在线程安全问题。

懒汉模式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class SingleTon{
   private static SingleTon  INSTANCE = null;
   private SingleTon(){}
   public static SingleTon getInstance() {  
   if(INSTANCE == null){
      INSTANCE = new SingleTon(); 
    } 
    return INSTANCE
  }
}

懒汉模式在方法被调用后才创建对象,以时间换空间,在多线程环境下存在风险。

DCL

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class Singleton {
    private static volatile Singleton singleton = null;

    private Singleton(){}

    public static Singleton getSingleton(){
        if(singleton == null){
            synchronized (Singleton.class){
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }    
}

DCL模式的优点就是,只有在对象需要被使用时才创建,第一次判断 INSTANCE == null为了避免非必要加锁,当第一次加载时才对实例进行加锁再实例化。这样既可以节约内存空间,又可以保证线程安全。volatile确保INSTANCE每次均在主内存中读取,这样虽然会牺牲一点效率,但也无伤大雅。

静态内部类模式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class SingleTon{
  private SingleTon(){}
 
  private static class SingleTonHoler{
     private static SingleTon INSTANCE = new SingleTon();
 }
 
  public static SingleTon getInstance(){
    return SingleTonHoler.INSTANCE;
  }
}

静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。即当SingleTon第一次被加载时,并不需要去加载SingleTonHoler,只有当getInstance()方法第一次被调用时,才会去初始化INSTANCE,第一次调用getInstance()方法会导致虚拟机加载SingleTonHoler类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。

那么,静态内部类又是如何实现线程安全的呢?首先,我们先了解下类的加载时机。

类加载时机

Java程序对类的使用方式分为:主动使用和被动使用。 主动使用,又分为七种情况:

  • 创建类的实例
  • 访问某个类或接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法I
  • 反射(比如:Class.forName(“com.atguigu.Test”))
  • 初始化一个类的子类
  • Java虚拟机启动时被标明为启动类的类
  • JDK7开始提供的动态语言支持:
  • java.lang.invoke.MethodHandle实例的解析结果REF getStatic、REF putStatic、REF invokeStatic句柄对应的类没有初始化,则初始化

除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化。比如通过子类引用父类的静态字段,为子类的被动使用,不会导致子类初始化。

我们再回头看下getInstance()方法,调用的是SingleTonHoler.INSTANCE,取的是SingleTonHoler里的INSTANCE对象,跟上面那个DCL方法不同的是,getInstance()方法并没有多次去new对象,故不管多少个线程去调用getInstance()方法,取的都是同一个INSTANCE对象,而不用去重新创建。当getInstance()方法被调用时,SingleTonHoler才在SingleTon的运行时常量池里,把符号引用替换为直接引用,这时静态对象INSTANCE也真正被创建,然后再被getInstance()方法返回出去,这点同饿汉模式。那么INSTANCE在创建过程中又是如何保证线程安全的呢?在《深入理解JAVA虚拟机》中,有这么一句话:

虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行<clinit>()方法后,其他线程唤醒之后不会再次进入<clinit>()方法。同一个加载器下,一个类型只会初始化一次。),在实际应用中,这种阻塞往往是很隐蔽的。

故而,可以看出INSTANCE在创建过程中是线程安全的,所以说静态内部类形式的单例可保证线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。

那么,是不是可以说静态内部类单例就是最完美的单例模式了呢?其实不然,静态内部类也有着一个致命的缺点,就是传参的问题,由于是静态内部类的形式去创建单例的,故外部无法传递参数进去,例如Context这种参数,所以,我们创建单例时,可以在静态内部类与DCL模式里自己斟酌。

在看枚举模式之前先看破坏单例的情况

枚举模式

枚举类实现单例模式是 effective java 作者极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public enum SingletomEnum {
    INSTANCE {
        @Override
        protected void printName() {
            System.out.println(getName());
        }
    };

    private String name;

    protected abstract void printName();

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

代码相当简洁,我们也可以像常规类一样编写enum类,为其添加变量和方法,访问方式也更简单,使用SingletonEnum.INSTANCE进行访问,这样也就避免调用getInstance方法,更重要的是使用枚举单例的写法,我们完全不用考虑序列化和反射的问题

序列化

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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);
  }

实际上通过调用enumType(Class对象的引用)的enumConstantDirectory方法获取到的是一个Map集合,在该集合中存放了以枚举name为key和以枚举实例变量为value的Key&Value数据,因此通过name的值就可以获取到枚举实例,看看enumConstantDirectory方法源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
Map<String, T> enumConstantDirectory() {
        if (enumConstantDirectory == null) {
            //getEnumConstantsShared最终通过反射调用枚举类的values方法
            T[] universe = getEnumConstantsShared();
            if (universe == null)
                throw new IllegalArgumentException(
                    getName() + " is not an enum type");
            Map<String, T> m = new HashMap<>(2 * universe.length);
            //map存放了当前enum类的所有枚举实例变量,以name为key值
            for (T constant : universe)
                m.put(((Enum<?>)constant).name(), constant);
            enumConstantDirectory = m;
        }
        return enumConstantDirectory;
    }
    private volatile transient Map<String, T> enumConstantDirectory = null;

到这里我们也就可以看出枚举序列化确实不会重新创建新实例,jvm保证了每个枚举实例变量的唯一性。

反射

来看看反射到底能不能创建枚举,下面试图通过反射获取构造器并创建枚举

1
2
3
4
5
6
7
public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
  //获取枚举类的构造函数(前面的源码已分析过)
   Constructor<SingletonEnum> constructor=SingletonEnum.class.getDeclaredConstructor(String.class,int.class);
   constructor.setAccessible(true);
   //创建枚举
   SingletonEnum singleton=constructor.newInstance("otherInstance",9);
  }

执行报错

1
2
3
4
5
6
7
8
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
    at zejian.SingletonEnum.main(SingletonEnum.java:38)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

显然告诉我们不能使用反射创建枚举类,这是为什么呢?不妨看看newInstance方法源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
 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;
    }

源码很了然,确实无法使用反射创建枚举实例,也就是说明了创建枚举实例只有编译器能够做到而已。

显然枚举单例模式确实是很不错的选择,因此我们推荐使用它。但是这总不是万能的,对于android平台这个可能未必是最好的选择,在android开发中,内存优化是个大块头,而使用枚举时占用的内存常常是静态变量的两倍还多,因此android官方在内存优化方面给出的建议是尽量避免在android中使用enum。但是不管如何,关于单例,我们总是应该记住:线程安全,延迟加载,序列化与反序列化安全,反射安全是很重重要的。

破坏单例

在介绍枚举模式之前先来看看上述的几种模式是否真的无懈可击

破坏单例的可能性有哪些

我们知道要破坏单例,则必须创建对象,那么我们顺着这个思路走,创建对象的方式无非就是new,clone,反序列化,以及反射。

  • new 单例模式的首要条件就是构造方法私有化,所以new这种方式去破坏单例的可能性是不存在的(在保障线程安全的情况下)
  • clone 要调用clone方法,那么必须实现Cloneable接口,但是单例模式是不能实现这个接口的,因此排除这种可能性
  • 序列化
  • 反射

序列化

攻击示例

为方便测试,我们写个最简单的饿汉式单例模式代码,便于以后测试,其他三种情况(除枚举)一样遭受序列化破坏

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class HungrySingleton implements Serializable {
    private final static HungrySingleton instance;
    static {
        instance=new HungrySingleton();
    }
    private HungrySingleton() {}
    public static HungrySingleton getInstance(){
        return instance;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class DestroySingletonTest {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //序列化方式破坏单例   测试
        serializeDestroyMethod();
    }

    private static void serializeDestroyMethod() throws IOException, ClassNotFoundException {
        HungrySingleton hungrySingleton=null;
        HungrySingleton hungrySingleton_new=null;

      // 获取
        hungrySingleton=HungrySingleton.getInstance();

      // 序列化  序列化到二进制流bos中
        ByteArrayOutputStream bos=new ByteArrayOutputStream();
        ObjectOutputStream oos=new ObjectOutputStream(bos);
        oos.writeObject(hungrySingleton);

      // 反序列化 从二进制流bos中反序列化
        ByteArrayInputStream bis=new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois=new ObjectInputStream(bis);
        hungrySingleton_new= (HungrySingleton) ois.readObject();

        System.out.println(hungrySingleton==hungrySingleton_new);
    }
}

我们运行程序,结果打印false,显然单例被破坏了。

预防攻击

那么,我们有什么解决办法能抵挡这种序列化破坏呢? 根据{%post_link 工作/200_编程语言/java/java基础/序列化和反序列化%}中源码的分析,当在序列化的类(或者父类)中定义了readResolve方法,会将该方法保存在序列化的类的元数据ObjectStreamClass里。反序列化时会通过反射该方法直接返回该方法的返回体。

1
2
3
4
5
6
7
if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod()) // 是否存在符合要求的readResolve方法
        {
            Object rep = desc.invokeReadResolve(obj);
...
        }

所以我们在单例代码中增加readResolve方法即可预防序列化破坏单例

1
2
3
4
private Object readResolve()
    {
        return instance;
    }

反射

使用反射强行调用私有构造器,解决方式可以修改构造器,让它在创建第二个实例的时候抛异常

1
2
3
4
5
6
7
8
9
public static Singleton INSTANCE = new Singleton();     
private static volatile  boolean  flag = true;
private Singleton(){
    if(flag){
    flag = false;   
    }else{
        throw new RuntimeException("The instance  already exists !");
    }
}

但是这种也不能真正防止反射攻击,因为可以通过反射改变标志位的值。要想防止反射破坏,可以使用枚举单例来实现。