您的位置 首页 java

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

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

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

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

但其实,写好一个单例模式,并没有大家想的那么简单。下面,通过一步步的迭代优化,从线程安全和防破坏两个维度,逐步的实现一个“铜墙铁壁”般牢固的单例模式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  thread Index = 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是否 静态内部类是否 枚举是是

文章来源:智云一二三科技

文章标题:逐步构建一个“铜墙铁壁”的单例模式

文章地址:https://www.zhihuclub.com/182415.shtml

关于作者: 智云科技

热门文章

网站地图