逐步构建一个“铜墙铁壁”的单例模式

单例模式被公认为是设计模式中最简单的一种,用于保证系统中,某个类只有一个实例,运用非常广泛。

单例模式,往简单了说,其实关键就是,控制构造函数的访问权限,然后对外提供统一的访问点。

但其实,写好一个单例模式,并没有大家想的那么简单。下面,通过一步步的迭代优化,从线程安全和防破坏两个维度,逐步的实现一个“铜墙铁壁”般牢固的单例模式demo。

主要包含如下几块内容:

  1. 普通饱汉式和饿汉式

  2. 线程安全的饱汉式 (加锁和DCL)

  3. 静态内部类方式

  4. 单例模式的破坏

  5. 终极大法:枚举

  6. 一把无坚不摧的矛:Unsafa类

  7. 总结

一:普通实现方式

普通的饱汉式和饿汉式单例模式实现,应该是大家接触的最多的实现方式,他们实现简单,便于理解。

饱汉式的优势在于懒加载,对于非常消耗资源,占内存的对象尤其有效,但其实线程不安全的。

而饿汉式的优势在于,其天生的线程安全性。因此在大多数的场景下,饿汉式的单例模式已经够用。

二:线程安全的饱汉式

第一节讲了,饱汉式的实现方式是线程不安全的,因为它的非空判断和初始化是多步操作,不是原子的。

最简单的方式,通过synchronize关键字直接给方法加锁,但这样的方式比较低效,比较简单,这里就不过多阐述了。

更好的方式是DCL (Double Check Lock),双重check机制。第一个check是为了对象已经创建后,产生不必要的同步。第二个check,是避免第一个判空之后,进入同步方法前,有其他线程创建了实例。

需要注意的是,instance这个属性的volatile关键字,因为实例的创建不是原子操作,它包含了:(1) 分配内存,(2) 初始化对象,(3) 引用指向新的内存空间 三个步骤,其中2依赖于1,但是3不依赖于2,所以由于CPU指令重排序的影响,其他线程可能看到的是“半个”对象,而加上volatile关键字就是为了避免指令重排序。

public class TestSingleton {

   public static void main(String[] args) {

       // 多线程环境下,创建实例
       final Map<String, HungrySingleton> HungryMap = new ConcurrentHashMap(10);
       final Map<String, FullSingleton> FullMap = new ConcurrentHashMap(10);
       final Map<String, DCL> DCLMap = new ConcurrentHashMap(10);

       for (int i = 0; i < 1000; i++) {
           final int threadIndex = i;
           new Thread(new Runnable() {
               public void run() {
                   HungryMap.put("thread" + threadIndex, HungrySingleton.getInstance());
                   FullMap.put("thread" + threadIndex, FullSingleton.getInstance());
                   DCLMap.put("thread" + threadIndex, DCL.getInstance());
               }
           }).start();
       }

       // 通过set的size大小,来判断是否创建了不同的实例
       Set<HungrySingleton> hungrySingletonSet = new HashSet<HungrySingleton>();
       hungrySingletonSet.addAll(HungryMap.values());
       System.out.println("饿汉式单例多线程下是否产生了不同的对象:" + (hungrySingletonSet.size() > 1));// 偶尔会为:true

       Set<FullSingleton> FullSingletonSet = new HashSet<FullSingleton>();
       FullSingletonSet.addAll(FullMap.values());
       System.out.println("饱汉式单例多线程下是否产生了不同的对象:" + (FullSingletonSet.size() > 1)); // 一直 false

       Set<DCL> dclSingletonSet = new HashSet<DCL>();
       dclSingletonSet.addAll(DCLMap.values());
       System.out.println("DCL式单例多线程下是否产生了不同的对象:" + (dclSingletonSet.size() > 1)); // 一直 false
   }
}

class HungrySingleton {
   private static HungrySingleton instance = null;
   private HungrySingleton(){
   }

   public static HungrySingleton getInstance() {
       if (instance == null) {
           instance = new HungrySingleton();
       }

       return instance;
   }
}


class FullSingleton {
   private static final FullSingleton instance = new FullSingleton();

   private FullSingleton(){
   }

   public static FullSingleton getInstance() {
       return instance;
   }
}


class DCL {
   private static volatile DCL instance = null;

   private DCL() {
   }

   public static DCL getInstance() {
       if (instance == null) {
           synchronized (DCL.class) {
               if (instance == null) {
                   instance = new DCL();
               }
           }
       }

       return instance;
   }
}

                                                                       代码1:多线程环境下的单例模式

三:静态内部类方式

说完了饱汉式和饿汉式,那么,有没有哪种方式可以结合两者的优点呢?既能实现懒加载,又能线程安全。

通过静态内部类就能实现这一要求。静态内部类和其外部类没有啥太多的必然联系,可以看成连个独立的类,外围类的加载不会触发静态内部类的类加载,只有调用静态内部类的静态变量时,才会触发类加载。

class InnerClassSingleton {

   private static class SingletonHolder{

       private static final InnerClassSingleton instance = new InnerClassSingleton();

       private SingletonHolder(){

       }
   }


   private InnerClassSingleton() {
   }


   public static InnerClassSingleton getInstance() {
       return SingletonHolder.instance;
   }
}

                                                                  代码2:静态内部类实现方式

四:单例模式的破坏

上诉的方式,看上去貌似完美的实现了单例模式,既能做到线程安全,又能实现懒加载,但他们都是基于一点:私有的构造函数。

这就意味着,上诉方式实现的单例都能通过反射或者序列化进行破坏。示例代码如下,所有的输出均为false。

public class Code3 {

   public static void main(String[] args) throws Exception {

       /* 测试饿汉式 */
       HungrySingleton hungryOrigin = HungrySingleton.getInstance();
       // 反射
       Class clazz = Class.forName("zhanht.HungrySingleton");
       Constructor[] constructors = clazz.getDeclaredConstructors();
       constructors[0].setAccessible(true);
       HungrySingleton hungryReflect = (HungrySingleton) constructors[0].newInstance();
       System.out.println(hungryOrigin == hungryReflect);
       
       // 反序列化
       String jsonStr = JSON.toJSONString(hungryOrigin);
       HungrySingleton hungryJson = JSON.parseObject(jsonStr, HungrySingleton.class);
       System.out.println(hungryOrigin == hungryJson);

       /* 测试饱汉式 */
       FullSingleton fullOrigin = FullSingleton.getInstance();
       // 反射
       Class clazzFull = Class.forName("zhanht.FullSingleton");
       Constructor[] constructorsFull = clazzFull.getDeclaredConstructors();
       constructorsFull[0].setAccessible(true);
       FullSingleton fullReflect = (FullSingleton) constructorsFull[0].newInstance();
       System.out.println(fullOrigin == fullReflect);

       // 反序列化
       String jsonStrFull = JSON.toJSONString(fullOrigin);
       FullSingleton fullJson = JSON.parseObject(jsonStrFull, FullSingleton.class);
       System.out.println(fullOrigin == fullJson);

       /* 测试DCL */
       DCL dclOrigin = DCL.getInstance();
       // 反射
       Class clazzDcl = Class.forName("zhanht.DCL");
       Constructor[] constructorsDcl = clazzDcl.getDeclaredConstructors();
       constructorsDcl[0].setAccessible(true);
       DCL dclReflect = (DCL) constructorsDcl[0].newInstance();
       System.out.println(dclOrigin == dclReflect);

       // 反序列化
       String jsonStrDcl = JSON.toJSONString(dclOrigin);
       DCL dclJson = JSON.parseObject(jsonStrDcl, DCL.class);
       System.out.println(dclOrigin == dclJson);

       /* 测试静态内部类 */
       InnerClassSingleton innerOrigin = InnerClassSingleton.getInstance();
       // 反射
       Class clazzInner = Class.forName("zhanht.InnerClassSingleton");
       Constructor[] constructorsInner = clazzInner.getDeclaredConstructors();
       constructorsInner[0].setAccessible(true);
       InnerClassSingleton innerReflect = (InnerClassSingleton) constructorsInner[0].newInstance();
       System.out.println(innerOrigin == innerReflect);

       // 反序列化
       String jsonStrInner = JSON.toJSONString(dclOrigin);
       InnerClassSingleton innerJson = JSON.parseObject(jsonStrInner, InnerClassSingleton.class);
       System.out.println(innerOrigin == innerJson);
   }
}

                                                              代码3:反射和序列化破坏单例

五:终极大法:枚举

那么,是否存在一种实现方式,把反射和序列化也考虑进去了呢?还真有,那就是:枚举。

大家对枚举的使用,一般都是停留在定义各种类型,操作码等。可能也听过枚举实现单例,但可能并没有深究其原因。

接下来,咱们就具体分析分析为什么通过枚举能够完美的实现单例模式。

枚举类型是java语言中的又一块语法糖,用于作为预先定义好的常量的集合。除了它自动继承自Enum类,所以没法继承自枚举类型。除此之外,它和一般的类没太大区别,一样能定义自己的属性和方法。

通过反编译,可以看到,枚举中定义的常量自动是public static final的,构造函数默认是私有的,通过静态代码块调用私有的构造函数对常量进行初始化,因此可以用于实现单例模式,具体大家可以自行通过javap命令或者第三方反编译软件进行查看。

这里大家可能就有疑问了,枚举的实现和前面的方式差不多啊,只不过是隐式的而已。那么枚举通过反射和反序列化后生成的对象还是原来的对象吗?下面通过一个例子来试验下。

public class Test2 {

   public static void main(String[] args) throws Exception {

       CodeEnum origin = CodeEnum.A;

       /* 反射方式 */
       Class clazz = Class.forName("zhanht.CodeEnum");
       Constructor[] constructors = clazz.getDeclaredConstructors();
       constructors[0].setAccessible(true);
       CodeEnum reflect = (CodeEnum) constructors[0].newInstance("success", 0);
       System.out.println("reflect == origin : " + (reflect == origin));// 运行时异常:Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects


       /* 反序列化方式 */
       // fastJson 方式实验
       String jsonStr = JSON.toJSONString(origin);
       CodeEnum json = JSON.parseObject(jsonStr, CodeEnum.class);
       System.out.println("json == origin " + (json == origin)); // true

       // ObjectOutputStream 方式实验
       ByteArrayOutputStream byteArrayInputStream = new ByteArrayOutputStream();
       ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayInputStream);
       objectOutputStream.writeObject(origin);
       objectOutputStream.close();

       ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(byteArrayInputStream.toByteArray()));
       CodeEnum stream = (CodeEnum) objectInputStream.readObject();
       objectInputStream.close();
       System.out.println("stream == origin " + (stream == origin)); // true
   }
}

enum CodeEnum {
   A("success", 0);

   private String desc;
   private int code;

   CodeEnum(String desc, int code) {
       this.desc = desc;
       this.code = code;
   }
}

                                                             代码4:枚举的反射和反序列化

通过实验结果,我们可以发现,枚举通过对反射的拦截来防止反射的破坏。实现的地方在Constructor类的newInstance中,如果发现类型是Enum,就直接抛异常。 

那么,枚举怎么保证反序列化后的对象依然是原来的对象呢?通过debug一步步往里面跟,你会发现,最后反序列化的枚举都是通过:Enum.valueOf(Class enumType, String name) 这个方法返回的,此方法通过枚举的具体类型和name,可以定位到最初定义的那个具体实例,它的关键在于这行代码:

  1. T result = enumType.enumConstantDirectory().get(name);

进入enumConstantDirectory方法的具体实现,代码和解释如下。

Map<String, T> enumConstantDirectory() {

        /* 懒加载,第一次调用此方法的时候,会取初始化 enumConstantDirectory这个map
        * map的key是枚举实例的name,value是name对应的具体实例 */
       if (enumConstantDirectory == null) {

           // 这个方法通过反射调用枚举具体类型的 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);
           for (T constant : universe)
               m.put(((Enum<?>)constant).name(), constant);
           enumConstantDirectory = m;
       }

       return enumConstantDirectory;
   }

                                                         代码5:枚举反序列化依然为原对象的原因

六:一把无坚不摧的矛:Unsafe类

通过上面的讲解,大家应该明白了,枚举是实现单例模式的一种简单并且安全的方式,也明白了枚举在反射和发序列化情形下,依然能保持单例的实现原理。

那么,枚举实现的单例就无法破坏了吗?

大多数情况下,是的。但是,我们也能有特殊的方式去破坏它,那就是通过sun.misc.Unsafe类。

Unsafe类能够直接和系统底层进行交互,能够直接操作内存,最常见的就是各大高性能组件中经常使用的CAS操作。

由于Unsafe的高危性,所以Java并不鼓励大家直接使用它,所以它被设计成单例,并且只能通过系统引导类进行加载。

下面简单演示下,通过反射调用unsafe直接在内存上绕开一切限制,直接创建对象。

public class Code6 {

   public static void main(String[] args) throws Exception {

       CodeEnum origin = CodeEnum.A;

       Field f = Unsafe.class.getDeclaredField("theUnsafe");

       f.setAccessible(true);

       Unsafe unsafe = (Unsafe) f.get(null);

       CodeEnum unSafe = (CodeEnum) unsafe.allocateInstance(CodeEnum.class);

       System.out.println(origin == unSafe); // false

   }

}

                                                                     代码6:unsafe直接创建对象

但这种方式使用毕竟很少,也不鼓励使用,所以一般不予以考虑。所以,通过枚举实现的单例,一般可以认为是安全的。

七:总结

通过如下几块内容的分析讲解,现在总结如下:

实现方式 线程安全 防反射和反序列化
普通饿汉式
普通懒汉式
加锁或者DCL
静态内部类
枚举

猜你喜欢

转载自blog.csdn.net/zhanht/article/details/81915865