龙空技术网

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

Java武学秘籍 95

前言:

现在小伙伴们对“java中怎么开方”大约比较珍视,大家都想要剖析一些“java中怎么开方”的相关知识。那么小编同时在网络上网罗了一些对于“java中怎么开方””的相关内容,希望朋友们能喜欢,小伙伴们快快来学习一下吧!

转载:zhanht

原文链接:blog.csdn.net/zhanht/article/details/81915865

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

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

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

主要包含如下几块内容:

普通饱汉式和饿汉式线程安全的饱汉式 (加锁和DCL)静态内部类方式单例模式的破坏终极大法:枚举一把无坚不摧的矛:Unsafa类总结一:普通实现方式

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

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

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

二:线程安全的饱汉式

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

最简单的方式,通过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,可以定位到最初定义的那个具体实例,它的关键在于这行代码:

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是否静态内部类是否枚举是是

标签: #java中怎么开方