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

Posted 海涛技术漫谈

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了逐步构建一个“铜墙铁壁”的单例模式相关的知识,希望对你有一定的参考价值。

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

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

但其实,写好一个单例模式,并没有大家想的那么简单。下面,通过一步步的迭代优化,从线程安全和防破坏两个维度,逐步的实现一个“铜墙铁壁”般牢固的单例模式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关键字就是为了避免指令重排序。

 
   
   
 
  1. public class TestSingleton {

  2.    public static void main(String[] args) {

  3.        // 多线程环境下,创建实例

  4.        final Map<String, HungrySingleton> HungryMap = new ConcurrentHashMap(10);

  5.        final Map<String, FullSingleton> FullMap = new ConcurrentHashMap(10);

  6.        final Map<String, DCL> DCLMap = new ConcurrentHashMap(10);

  7.        for (int i = 0; i < 1000; i++) {

  8.            final int threadIndex = i;

  9.            new Thread(new Runnable() {

  10.                public void run() {

  11.                    HungryMap.put("thread" + threadIndex, HungrySingleton.getInstance());

  12.                    FullMap.put("thread" + threadIndex, FullSingleton.getInstance());

  13.                    DCLMap.put("thread" + threadIndex, DCL.getInstance());

  14.                }

  15.            }).start();

  16.        }

  17.        // 通过set的size大小,来判断是否创建了不同的实例

  18.        Set<HungrySingleton> hungrySingletonSet = new HashSet<HungrySingleton>();

  19.        hungrySingletonSet.addAll(HungryMap.values());

  20.        System.out.println("饿汉式单例多线程下是否产生了不同的对象:" + (hungrySingletonSet.size() > 1));// 偶尔会为:true

  21.        Set<FullSingleton> FullSingletonSet = new HashSet<FullSingleton>();

  22.        FullSingletonSet.addAll(FullMap.values());

  23.        System.out.println("饱汉式单例多线程下是否产生了不同的对象:" + (FullSingletonSet.size() > 1)); // 一直 false

  24.        Set<DCL> dclSingletonSet = new HashSet<DCL>();

  25.        dclSingletonSet.addAll(DCLMap.values());

  26.        System.out.println("DCL式单例多线程下是否产生了不同的对象:" + (dclSingletonSet.size() > 1)); // 一直 false

  27.    }

  28. }

  29. class HungrySingleton {

  30.    private static HungrySingleton instance = null;

  31.    private HungrySingleton(){

  32.    }

  33.    public static HungrySingleton getInstance() {

  34.        if (instance == null) {

  35.            instance = new HungrySingleton();

  36.        }

  37.        return instance;

  38.    }

  39. }

  40. class FullSingleton {

  41.    private static final FullSingleton instance = new FullSingleton();

  42.    private FullSingleton(){

  43.    }

  44.    public static FullSingleton getInstance() {

  45.        return instance;

  46.    }

  47. }

  48. class DCL {

  49.    private static volatile DCL instance = null;

  50.    private DCL() {

  51.    }

  52.    public static DCL getInstance() {

  53.        if (instance == null) {

  54.            synchronized (DCL.class) {

  55.                if (instance == null) {

  56.                    instance = new DCL();

  57.                }

  58.            }

  59.        }

  60.        return instance;

  61.    }

  62. }

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


三:静态内部类方式

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

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

 
   
   
 
  1. class InnerClassSingleton {

  2.    private static class SingletonHolder{

  3.        private static final InnerClassSingleton instance = new InnerClassSingleton();

  4.        private SingletonHolder(){

  5.        }

  6.    }

  7.    private InnerClassSingleton() {

  8.    }

  9.    public static InnerClassSingleton getInstance() {

  10.        return SingletonHolder.instance;

  11.    }

  12. }

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


四:单例模式的破坏

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

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

 
   
   
 
  1. public class Code3 {

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

  3.        /* 测试饿汉式 */

  4.        HungrySingleton hungryOrigin = HungrySingleton.getInstance();

  5.        // 反射

  6.        Class clazz = Class.forName("zhanht.HungrySingleton");

  7.        Constructor[] constructors = clazz.getDeclaredConstructors();

  8.        constructors[0].setAccessible(true);

  9.        HungrySingleton hungryReflect = (HungrySingleton) constructors[0].newInstance();

  10.        System.out.println(hungryOrigin == hungryReflect);

  11.        // 反序列化

  12.        String jsonStr = JSON.toJSONString(hungryOrigin);

  13.        HungrySingleton hungryJson = JSON.parseObject(jsonStr, HungrySingleton.class);

  14.        System.out.println(hungryOrigin == hungryJson);

  15.        /* 测试饱汉式 */

  16.        FullSingleton fullOrigin = FullSingleton.getInstance();

  17.        // 反射

  18.        Class clazzFull = Class.forName("zhanht.FullSingleton");

  19.        Constructor[] constructorsFull = clazzFull.getDeclaredConstructors();

  20.        constructorsFull[0].setAccessible(true);

  21.        FullSingleton fullReflect = (FullSingleton) constructorsFull[0].newInstance();

  22.        System.out.println(fullOrigin == fullReflect);

  23.        // 反序列化

  24.        String jsonStrFull = JSON.toJSONString(fullOrigin);

  25.        FullSingleton fullJson = JSON.parseObject(jsonStrFull, FullSingleton.class);

  26.        System.out.println(fullOrigin == fullJson);

  27.        /* 测试DCL */

  28.        DCL dclOrigin = DCL.getInstance();

  29.        // 反射

  30.        Class clazzDcl = Class.forName("zhanht.DCL");

  31.        Constructor[] constructorsDcl = clazzDcl.getDeclaredConstructors();

  32.        constructorsDcl[0].setAccessible(true);

  33.        DCL dclReflect = (DCL) constructorsDcl[0].newInstance();

  34.        System.out.println(dclOrigin == dclReflect);

  35.        // 反序列化

  36.        String jsonStrDcl = JSON.toJSONString(dclOrigin);

  37.        DCL dclJson = JSON.parseObject(jsonStrDcl, DCL.class);

  38.        System.out.println(dclOrigin == dclJson);

  39.        /* 测试静态内部类 */

  40.        InnerClassSingleton innerOrigin = InnerClassSingleton.getInstance();

  41.        // 反射

  42.        Class clazzInner = Class.forName("zhanht.InnerClassSingleton");

  43.        Constructor[] constructorsInner = clazzInner.getDeclaredConstructors();

  44.        constructorsInner[0].setAccessible(true);

  45.        InnerClassSingleton innerReflect = (InnerClassSingleton) constructorsInner[0].newInstance();

  46.        System.out.println(innerOrigin == innerReflect);

  47.        // 反序列化

  48.        String jsonStrInner = JSON.toJSONString(dclOrigin);

  49.        InnerClassSingleton innerJson = JSON.parseObject(jsonStrInner, InnerClassSingleton.class);

  50.        System.out.println(innerOrigin == innerJson);

  51.    }

  52. }

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


五:终极大法:枚举

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

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

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

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

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

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

 
   
   
 
  1. public class Test2 {

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

  3.        CodeEnum origin = CodeEnum.A;

  4.        /* 反射方式 */

  5.        Class clazz = Class.forName("zhanht.CodeEnum");

  6.        Constructor[] constructors = clazz.getDeclaredConstructors();

  7.        constructors[0].setAccessible(true);

  8.        CodeEnum reflect = (CodeEnum) constructors[0].newInstance("success", 0);

  9.        System.out.println("reflect == origin : " + (reflect == origin));// 运行时异常:Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects

  10.        /* 反序列化方式 */

  11.        // fastJson 方式实验

  12.        String jsonStr = JSON.toJSONString(origin);

  13.        CodeEnum json = JSON.parseObject(jsonStr, CodeEnum.class);

  14.        System.out.println("json == origin " + (json == origin)); // true

  15.        // ObjectOutputStream 方式实验

  16.        ByteArrayOutputStream byteArrayInputStream = new ByteArrayOutputStream();

  17.        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayInputStream);

  18.        objectOutputStream.writeObject(origin);

  19.        objectOutputStream.close();

  20.        ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(byteArrayInputStream.toByteArray()));

  21.        CodeEnum stream = (CodeEnum) objectInputStream.readObject();

  22.        objectInputStream.close();

  23.        System.out.println("stream == origin " + (stream == origin)); // true

  24.    }

  25. }

  26. enum CodeEnum {

  27.    A("success", 0);

  28.    private String desc;

  29.    private int code;

  30.    CodeEnum(String desc, int code) {

  31.        this.desc = desc;

  32.        this.code = code;

  33.    }

  34. }

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

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

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

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

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

 
   
   
 
  1. Map<String, T> enumConstantDirectory() {

  2.         /* 懒加载,第一次调用此方法的时候,会取初始化 enumConstantDirectory这个map

  3.         * map的key是枚举实例的name,value是name对应的具体实例 */

  4.        if (enumConstantDirectory == null) {

  5.            // 这个方法通过反射调用枚举具体类型的 values方法,得到所有的实例

  6.            T[] universe = getEnumConstantsShared();

  7.            if (universe == null)

  8.                throw new IllegalArgumentException(

  9.                        getName() + " is not an enum type");

  10.            Map<String, T> m = new HashMap<>(2 * universe.length);

  11.            for (T constant : universe)

  12.                m.put(((Enum<?>)constant).name(), constant);

  13.            enumConstantDirectory = m;

  14.        }

  15.        return enumConstantDirectory;

  16.    }

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


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

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

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

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

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

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

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

 
   
   
 
  1. public class Code6 {

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

  3.        CodeEnum origin = CodeEnum.A;

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

  5.        f.setAccessible(true);

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

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

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

  9.    }

  10. }

代码6:unsafe直接创建对象

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


七:总结

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

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


以上是关于逐步构建一个“铜墙铁壁”的单例模式的主要内容,如果未能解决你的问题,请参考以下文章

片段作为 Android 中的单例

单例设计模式

单例设计模式

如何写一个简单的单例模式?

只用一行代码的单例模式

怎么实现一个线程安全的单例模式