美文网首页设计模式
单例设计模式

单例设计模式

作者: 愤怒的奶牛 | 来源:发表于2019-12-04 23:15 被阅读0次
    1. 基本概念

    java 进程 内存中只有一个 对象实例。

    • 实现的基本原则:
    1. 构造器私有化,不允许外部创建对象。
    2. 提供public static 的访问点,返回创建的对象。
    • 应用场景:
    1. spring ioc 容器默认单例。
    2. 全局配置对象,全局一份
    3. 框架封装,结合其他设计模式一起使用。
    2. 实现方式
      1. 饿汉式
      1. 懒汉式

    双重检查锁
    静态内部类

    • 3.ThreadLocal
      1. 枚举
      1. CSA 原子类
      1. 注册式单例
    3. 破坏单例的方式
      1. 暴力反射
      1. 序列化和反序列化
    4.代码实现

    上面我们简单的总结了一下有关单例模式的相关知识点,接下来我们就来用代码实现一下常见单例的几种写法。以及如何保证单例的线程安全。

    4.1 饿汉式

    public class HungarySingleton {
    
        //类加载时进行初始化
        private static final  HungarySingleton instance = new HungarySingleton();
        
        // 构造器初始化
        private HungarySingleton() {}
    
        // 全局访问点
        public static HungarySingleton getInstance() {
            return instance;
        }
    }
    

    优点:饿汉式在类加载时就初始对象,并且只初始化一次,所以是线程安全的。
    缺点:这种写法是强引用,在JVM 里面永远不会被回收。同时在jvm 启动时也会消耗一定的资源,不管是否使用,都已经创建了,存在资源的浪费,如果在jvm 里面饿汉式单例太多了,就很浪费资源了,并且被创建的对象也无法被垃圾回收。后面我们会讲懒加载,这里我们首先来测试一下单例破坏的反射和序列化。

    • 反射破坏:
     /**
         * 反射破坏
         * @throws Exception
         */
        public static void test2() throws Exception {
            //得到默认构造器
            Constructor<HungarySingleton> declaredConstructor = HungarySingleton.class.getDeclaredConstructor();
            //强制访问
            declaredConstructor.setAccessible(true);
            // 创建对象
            HungarySingleton hungarySingleton = declaredConstructor.newInstance();
            System.out.println(hungarySingleton);
            HungarySingleton instance = HungarySingleton.getInstance();
            System.out.println(instance);
        }
    
    • 测试结果
    com.example.designpattern.singleton.HungarySingleton@7440e464
    com.example.designpattern.singleton.HungarySingleton@49476842
    
    Process finished with exit code 0
    

    从上面的测试结果看出来,饿汉式创建的单例可以被发射破坏。为了解决这个问题我们可以 在构造器那里做一下手脚:因为是反射调用构造器,所以我们可以在构造器中判断一下,如果对象已经存在了就抛出异常,防止再创建一次对象。

    • 修改构造器:
    public class HungarySingleton {
    
        //类加载时进行初始化
        private static final  HungarySingleton instance = new HungarySingleton();
    
        // 构造器初始化
        private HungarySingleton() {
            if (instance!=null) {//判断对象是否已经被创建
                throw new RuntimeException("请不要重复创建对象");
            }
        }
    
        // 全局访问点
        public static HungarySingleton getInstance() {
            return instance;
        }
    }
    
    • 测试结果:
    Exception in thread "main" java.lang.reflect.InvocationTargetException
        at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
        at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
        at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
        at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
        at com.example.designpattern.singleton.HungarySingleton.test2(HungarySingleton.java:62)
        at com.example.designpattern.singleton.HungarySingleton.main(HungarySingleton.java:48)
    Caused by: java.lang.RuntimeException: 请不要重复创建对象
        at com.example.designpattern.singleton.HungarySingleton.<init>(HungarySingleton.java:22)
        ... 6 more
    
    Process finished with exit code 1
    

    从上面的测试结果我们可以看出,反射是可以破坏单例的,当然针对饿汉式单例的反射破坏我们也可以有一些措施。接下来我们来看看 序列化和反序列化是如何破坏单例的。

    • 序列化和反序列化破坏单例
    /**
         * 序列化和反序列化破坏单例
         * */
        public static void test3() throws IOException, ClassNotFoundException {
    
            //将对象写到磁盘
            HungarySingleton hungarySingleton = HungarySingleton.getInstance();
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("hungarySingleton.obj"));
            outputStream.writeObject(hungarySingleton);
    
            System.out.println(hungarySingleton);
    
            //然后再将对象从磁盘读取出来
            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("hungarySingleton.obj"));
            HungarySingleton object = (HungarySingleton)inputStream.readObject();
    
            System.out.println(object);
        }
    
    • 测试结果
    饿汉式=com.example.designpattern.singleton.HungarySingleton@5451c3a8
    序列化和反序列化=com.example.designpattern.singleton.HungarySingleton@3d494fbf
    

    上述测试结果表明 序列化和反序列化也能够对单例造成破坏,可以阅读源码找到原因。注意这里我是实现了 Serializable 接口的。

    • 应对策略
    1. 不要实现 Serializable 接口。序列化和反序列化要求必须实现 Serializable 接口,所以为了 防止 序列化和反序列化对单例的破坏,可以不要实现 Serializable 接口。
    2. 如果业务要求必须要实现Serializable 接口,那么就只有下面这一种方式可以应对:重写 readResolve() 方法。
       /**
         * 防止序列化和反序列化对单例的破坏,返回单例对象
         * @return
         */
        public Object readResolve() {
            return instance;
        }
    

    上面这个返回会在 HungarySingleton object = (HungarySingleton)inputStream.readObject(); 这句话执行的时候 回调,直接就返回你自己 返回的对象,所以可以 应对 对单例的破坏。具体的可以看看 jdk 的源码,不能够找到答案。上面我们实现了饿汉式单例,并且分析了饿汉式单例的优缺点,以及反射和序列化,反序列化对单例的破坏。以及相应的应对策略。下面的内容我们就只针对 单例的一些实现展开谈论,对单例的破坏就不做分析了,可以自己去测试。

    4.2 懒加载

    • 双重检查锁

    在double check 之前我们先来看看 为什么会出现 double check 这种写法。

    最简单的懒加载:
    
    public class SimpleSingleton {
    
        //1.静态成员
        private static SimpleSingleton instance;
    
        //2. 构函数私有化
        private SimpleSingleton() {}
    
        //3. 提供全局访问方法
        public static SimpleSingleton getInstance() {
    
            if (instance == null) {
                instance = new SimpleSingleton();
            }
            return instance;
        }
    }
    
    • 单线程测试:
     //单线程测试
        public static void test() {
            SimpleSingleton instance = SimpleSingleton.getInstance();
            SimpleSingleton instance2 = SimpleSingleton.getInstance();
            SimpleSingleton instance3 = SimpleSingleton.getInstance();
            System.out.println(instance);
            System.out.println(instance2);
            System.out.println(instance3);
        }
    

    com.example.designpattern.singleton.SimpleSingleton@7440e464
    com.example.designpattern.singleton.SimpleSingleton@7440e464
    com.example.designpattern.singleton.SimpleSingleton@7440e464

    从上面的单线程测试中没有发现问题,接下来进行多线程测试,这里我们就使用简单的多线程测试,也可以并发测试。

    • 多线程测试
    //多线程测试
        public static void test2() {
            for (int i=0 ;i<3; i++) {
                new Thread(()->{
                    try {
                        TimeUnit.SECONDS.sleep(2);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    SimpleSingleton instance = SimpleSingleton.getInstance();
                    System.out.println(instance);
                }).start();
            }
        }
    

    com.example.designpattern.singleton.SimpleSingleton@21279f82
    com.example.designpattern.singleton.SimpleSingleton@25f209f5
    com.example.designpattern.singleton.SimpleSingleton@6e154e44

    这里我们看到直接就产生了3个不同的对象,显然违背单例的设计思想。针对线程安全问题,我们可以使用 锁来解决这个问题。于是就有了下面几种线程安全的写法。

    • 静态同步方法和同步代码块
    // 静态方法上面 加 synchronized  关键字
    public static synchronized SimpleSingleton getInstance() {
    
            if (instance == null) {
                instance = new SimpleSingleton();
            }
            return instance;
        }
    
    // 同步代码块
    public static  SimpleSingleton getInstance() {
            synchronized(SimpleSingleton.class) {//这里的锁 一般这样写,但也可以是 静态对象 作为锁
                if (instance == null) {
                    instance = new SimpleSingleton();
                }
                return instance;
            }
            
        }
    

    上面的 两种写法 效果完全一样,效率也一样,锁的范围也一样,都是 类锁。至于为什么是 类级别的锁,不是 对象级别的,有下面几个原因:
    1.静态方法本身是可以使用类直接调用,也就在类级别 ,在静态方法上面加锁的化 自然也就是类级别的锁了;
    2 . 静态方法里面的 同步代码块为啥 是 类级别的锁。注意这里因为我用的 SimpleSingleton.class 作为锁,所以说上面的两种写法是等效的。一般使用 SimpleSingleton.class作为锁是 避免创建 其他锁对象,这里是不能使用 this 作为锁的,这也是 因为有 static 关键字的原因。
    3 . 在这里能不能使用 对象锁呢?答案是可以的,但是必须是 static 对象 如:

    // 创建一个对象作为锁
        final static Object object = new Object();
        public static  SimpleSingleton getInstance() {
            synchronized(object) {// 使用对象锁
                if (instance == null) {
                    instance = new SimpleSingleton();
                }
                return instance;
            }
    
        }
    //显然这样的写法没有 SimpleSingleton.class 作为锁简单,
    //因为你自己又单独 创建了一个 Object 对象,而且还是 饿汉式创建的。
    

    分析完了上面的 锁的问题,我们再来分析一下 这种写法的优缺点,是否值得我们平时的项目中使用:
    1 .优点:synchronized 关键字保证了线程安全,同时也是懒加载的。
    2 .缺点:从上面的代码中我们可以看到 我们的锁都是全局锁,也就是说 每一个线程来访问我们的方法的时候 被要先去 获得锁,方法执行完了以后再去释放锁。我们知道,单例对象只在第一次访问的时候 创建就ok 了 ,也就是 下面这个逻辑 只在 第一次 访问该方法的 时候 instance == null ,然后创建 对象。如果在 线程安全的情况下 后续的线程 的 instance 都是不为空的,就不会去创建对象了,也就保证了线程安全了,那么我们对整个方法 都加上锁 就很低效了。

       if (instance == null) {// 第一次访问的时候 满足
                instance = new SimpleSingleton();
            }
    

    上面我们分析出 静态同步方法和同步代码块 虽然能够保证线程安全,但是也带来可一些性能问题,那么我们就 优化一下性能就ok 了。既然instance 只有在 第一次 访问的 时候 是 null 那么 我们就在 if 里面来 加一个锁 也就是在 第一次创建的时候 保证 线程安全就ok了,后面的线程 都不会 进入 if ,所以就有了下面的优化方案:

    public static  SimpleSingleton getInstance() {
                if (instance == null) {
                    synchronized (SimpleSingleton.class) {
                        instance = new SimpleSingleton();
                    }
                }
                return instance;
        }
    
    • 测试:
    public static void test2() {
            for (int i=0 ;i<3; i++) {
                new Thread(()->{
                    try {
                        TimeUnit.SECONDS.sleep(2);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    SimpleSingleton instance = SimpleSingleton.getInstance();
                    System.out.println(instance);
                }).start();
            }
        }
    

    com.example.designpattern.singleton.SimpleSingleton@6e154e44
    com.example.designpattern.singleton.SimpleSingleton@2fd04fd1
    com.example.designpattern.singleton.SimpleSingleton@21279f82

    测试结果我们发现,我擦,怎么会呢?不是加了锁了吗??我们仔细来分析一下为啥会是这样的:

    public static  SimpleSingleton getInstance() {
                if (instance == null) {// 1. 当第一次 到这里的就只有一个 线程 1 ,那么这个是线程安全的;2 . 当线程 1 和线程 2 同时到 这里了,不管是线程1 和线程 2 谁获得了锁 都会是 执行 创建对象的 语句,也就有了多个对象。
                    synchronized (SimpleSingleton.class) {
                        instance = new SimpleSingleton();
                    }
                }
                return instance;
        }
    

    针对上面的问题 我们自然就有了 下面的写法,也就是我们在 同步代码块里面再判断一次,就可以保证线程安全了,也就有了 双重检查锁的写法:

    public static  SimpleSingleton getInstance() {
                if (instance == null) {// 当线程1 和线程2 都执行到了这里,假设线程 1 获取了锁
                    synchronized (SimpleSingleton.class) {
                        if (instance == null) {
                            instance = new SimpleSingleton();// 线程1 创建了 对象,执行完毕 退出 同步代码块,释放锁,这时候 线程 2 获取了锁,然后 读取了 instance 的值 发现 不为空 ,第二个 if 条件就不满足了,不会执行 对象创建的语句。
                        }
                    }
                }
                return instance;
        }
    

    到这里我们就 把为啥会出现 double check 的过程分析了一下,但是 上面 的写法都还不是 线程安全的,因为 instance = new SimpleSingleton(); 在jvm 创建对象的指令 中 不是原子的,也就说 jvm 创建对象 至少有 下面几条指令:1. 申请一块内存空间; 2. 创建一个对象;3. 将地址值赋值个 变量;在并发量高的情况下,可能会发生可见性问题和指令重排序问题,

    https://www.cnblogs.com/goodAndyxublog/p/11356402.html

    为了解决这个问题 我们可以 使用 volatile 来 修饰 instance。下面我们来看看 完整的 double check 是怎么写的。

    public class DoubleCheckSingleton {
    // volatile  保证可见性,防止指令重排序
       private static volatile DoubleCheckSingleton singleton;
        public static DoubleCheckSingleton doubleCheckSingleton() {
            if (singleton == null) {
                synchronized (DoubleCheckSingleton.class) {
                    if (singleton == null ) {//为了防止 两个线程都进了 第一个if导致的线程安全问题,所以可以再加一次判断
                        singleton = new DoubleCheckSingleton();
                    }
                }
            }
            return singleton;
        }
    }
    

    上面我们分析了double check ,一切看上去都很完美,但是就是有一点,使用了线程同步机制,来保证线程的安全性,那么有没有一种不使用线程同步机制 也可以 实现线程安全和懒加载呢?答案是肯定的,那就是静态 内部类。另外,在这里我们没有分析 反射 ,序列化和反序列化对单例的破坏。答案是这两种都是可以破坏 double check 的单例,可以自己测试一下。接下来我们来分析一下 静态内部类的单例。

    4.2静态内部类

    public class InnerStaticSingleton {
    
        private InnerStaticSingleton() {}
    
        //但外部调用 SingletonHolder.innerStaticSingleton 时才会加载这里的静态内部类
         private static class SingletonHolder{
            private final static InnerStaticSingleton innerStaticSingleton = new InnerStaticSingleton();
    
         }
    
         public static InnerStaticSingleton getInstance() {
            return SingletonHolder.innerStaticSingleton;
         }
    }
    

    上面是静态内部类的实现方式,这里我解释一下,为什么是懒加载的。在外部类加载到jvm 时,静态内部类是不会被加载的,也就不会执行 private final static InnerStaticSingleton innerStaticSingleton = new InnerStaticSingleton(); 只有当 外部类的 static 方法被调用时,才会 加载 内部类,并实例化对象。静态在整个 jvm 运行周期中都只加载一次,所以是可以保证单例的,根据前面的分析,只有在调用时才会去初始化对象,所以是懒加载的。至于线程安全,也是利用jvm 内部机制保证的,到底是如何保证的,由于笔者水平有限,暂无法解释,希望大家留言谈论。该方式通常被认为是最优的 单例实现方式,但是也有一个缺点,就是参数传递的问题。所以到底要使用哪一种实现方式,是取决于 实际应用场景的。虽然该方式 优雅,但是同样可以被反射和序列化破坏。那么到底有没有一种单例是能够防止反射和序列化的破坏,答案是肯定的,那就是枚举式单例,终极杀招。

    4.3枚举式单例

    public enum EnumSingleton {
    
        INSTANCE;
    }
    

    这就是枚举式单例,是不是非常简单。下面我们来测试一下反射和序列化得破坏结果,看看是否能达到我们的预期。

    • 反射破坏
    public static void test() throws Exception{
            Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor();
            declaredConstructor.setAccessible(true);
            EnumSingleton enumSingleton = declaredConstructor.newInstance();
            System.out.println(enumSingleton);
        }
    
    • 测试结果

    Exception in thread "main" java.lang.NoSuchMethodException: com.example.designpattern.singleton.EnumSingleton.<init>()
    at java.lang.Class.getConstructor0(Class.java:3082)
    at java.lang.Class.getDeclaredConstructor(Class.java:2178)
    at com.example.designpattern.singleton.EnumSingleton.test(EnumSingleton.java:23)
    at com.example.designpattern.singleton.EnumSingleton.main(EnumSingleton.java:18)

    上面是反射的测试结果,直接给我们异常了,说没有默认构造器,后面我们会 分析一下 底层的原来,看看枚举单例到底是怎么回事。这里我们再来测试一下序列化。

    • 序列化测试
    public static void test2() throws Exception{
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("EnumSingleton.obj"));
            EnumSingleton instance = EnumSingleton.INSTANCE;
            System.out.println(instance);
            outputStream.writeObject(instance);
            ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("EnumSingleton.obj"));
    
            EnumSingleton enumSingleton =(EnumSingleton)objectInputStream.readObject();
            System.out.println(enumSingleton);
        }
    
    • 测试结果:

    INSTANCE
    INSTANCE

    结果返回了同一个对象,说明jdk 也为我们屏蔽了序列化对单例的影响。到这来是不是觉得很牛叉,枚举都帮我们做了,首先是线程安全的,其次反射和序列化也不能破坏它,但是是不是懒加载的呢?肯定不是,因为枚举也是在jvm 加载的时候就会初始化的。在 Effective java 那本书里面,作者就推荐使用 枚举式单例。当然到底使用哪一种,我们还是要根据业务场景来选择。好了,既然枚举这么牛掰,我们能不能看看jvm 在加载 枚举的时候,到时是怎么做到的?接下来我们就来看看 枚举的神秘面纱。首先,从代码层面看不出啥东西,那么,我们就要想办法看看他编译后的样子。

    反编辑工具 jad

    • 反编译 EnumSingleton.class

    jad EnumSingleton.class 。会生成一个 EnumSingleton.jad 的文件。

    // Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
    // Jad home page: http://www.kpdus.com/jad.html
    // Decompiler options: packimports(3) 
    // Source File Name:   EnumSingleton.java
    
    package com.example.designpattern.singleton;
    
    import java.io.*;
    import java.lang.reflect.Constructor;
    
    public final class EnumSingleton extends Enum
    {
        public static final EnumSingleton INSTANCE;
        private static final EnumSingleton $VALUES[];
    // 构造器,私有化,没有无参构造器,所以我们在测试的时候 会抛出异常,说没有无参构造器。
     private EnumSingleton(String s, int i)
         {
                super(s, i);
         }
    // 静态代码块初始化对象,饿汉式写法,是线程安全的。
        static
        {
            INSTANCE = new EnumSingleton("INSTANCE", 0);
            $VALUES = (new EnumSingleton[] {
                INSTANCE
            });
        }
        public static EnumSingleton[] values()
        {
            return (EnumSingleton[])$VALUES.clone();
        }
        public static EnumSingleton valueOf(String name)
        {
            return (EnumSingleton)Enum.valueOf(com/example/designpattern/singleton/EnumSingleton, name);
        }
    }
    

    上面是 EnumSingleton.class 反编译的结果。我们看到 EnumSingleton 枚举 继承的 Enum 对象,该对象是 jdk 自带的java.lang下面的抽象类

    public abstract class Enum<E extends Enum<E>>
            implements Comparable<E>, Serializable {
     protected Enum(String name, int ordinal) {//只有这一个构造函数
            this.name = name;
            this.ordinal = ordinal;
        }
    }
    

    上面解释了我们在反射 调用无参构造函数的时候,为啥会有异常抛出,那是因为枚举本身就没有无参构造函数。好了,到这里可能你又发现了,虽然没有无参构造函数,但是有 两个带参数的构造函数,我们能不能调用呢?我们来测试一下就知道了。

    public static void test() throws Exception{
            Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor(String.class,int.class); //得到带有参数的构造函数
            declaredConstructor.setAccessible(true);
            EnumSingleton enumSingleton = declaredConstructor.newInstance("测试",007);// 调用,创建对象
            System.out.println(enumSingleton);
        }
    
    • 测试结果:

    Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
    at com.example.designpattern.singleton.EnumSingleton.test(EnumSingleton.java:25)
    at com.example.designpattern.singleton.EnumSingleton.main(EnumSingleton.java:18)

    上面结果说,不能通过反射来创建 枚举对象。他说在 java.lang.reflect.Constructor.newInstance(Constructor.java:417) 417行 抛出的异常。我们就去看一下 :

    • Constructor
    @CallerSensitive
        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);
                }
            }
            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;
        }
    

    好了,上面我们就解释了,为啥枚举能够防止反射破坏单例,原来是jdk 帮我们做了这个事情了。我们还有一个序列化破坏没有找到原因。接下来我们就来看看序列化的原因,由于篇幅太长,可能已经忘记了 序列化测试代码,我们再来贴一下:

    public static void test2() throws Exception{
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("EnumSingleton.obj"));
            EnumSingleton instance = EnumSingleton.INSTANCE;
            System.out.println(instance);
            outputStream.writeObject(instance);
            ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("EnumSingleton.obj"));
    
            EnumSingleton enumSingleton =(EnumSingleton)objectInputStream.readObject();
            System.out.println(enumSingleton);
        }
    

    上面代码就两个意思,1 . 把对象写到磁盘;2 . 从磁盘读出来对象。既然读的时候得到的是一个对象,那么我们就直观 的先从读 开始,看看能不能找到答案,如果不能,我们再去 分析写。

    EnumSingleton enumSingleton =(EnumSingleton)objectInputStream.readObject();

    看看 readObject()里面

    public final Object readObject()
            throws IOException, ClassNotFoundException
        {
            if (enableOverride) {// 这里是false 
                return readObjectOverride();
            }
    
            // if nested read, passHandle contains handle of enclosing object
            int outerHandle = passHandle;
            try {
                Object obj = readObject0(false); //那么 对象就是从这里出来的,我们进去看看
                handles.markDependency(outerHandle, passHandle);
                ClassNotFoundException ex = handles.lookupException(passHandle);
                if (ex != null) {
                    throw ex;
                }
                if (depth == 0) {
                    vlist.doCallbacks();
                }
                return obj;
            } finally {
                passHandle = outerHandle;
                if (closed && depth == 0) {
                    clear();
                }
            }
    }
    
    ----- enableOverride 我们使用的这个构造器
     public ObjectInputStream(InputStream in) throws IOException {
            verifySubclass();
            bin = new BlockDataInputStream(in);
            handles = new HandleTable(10);
            vlist = new ValidationList();
            serialFilter = ObjectInputFilter.Config.getSerialFilter();
            enableOverride = false; // false
            readStreamHeader();
            bin.setBlockDataMode(true);
        }
    
    
    ------ Object obj = readObject0(false);
    private Object readObject0(boolean unshared) throws IOException {
            boolean oldMode = bin.getBlockDataMode();
            if (oldMode) {
                int remain = bin.currentBlockRemaining();
                if (remain > 0) {
                    throw new OptionalDataException(remain);
                } else if (defaultDataEnd) {
                    /*
                     * Fix for 4360508: stream is currently at the end of a field
                     * value block written via default serialization; since there
                     * is no terminating TC_ENDBLOCKDATA tag, simulate
                     * end-of-custom-data behavior explicitly.
                     */
                    throw new OptionalDataException(true);
                }
                bin.setBlockDataMode(false);
            }
    
            byte tc;
            while ((tc = bin.peekByte()) == TC_RESET) {
                bin.readByte();
                handleReset();
            }
    
            depth++;
            totalObjectRefs++;
            try {
                switch (tc) {
                    case TC_NULL:
                        return readNull();
    
                    case TC_REFERENCE:
                        return readHandle(unshared);
    
                    case TC_CLASS:
                        return readClass(unshared);
    
                    case TC_CLASSDESC:
                    case TC_PROXYCLASSDESC:
                        return readClassDesc(unshared);
    
                    case TC_STRING:
                    case TC_LONGSTRING:
                        return checkResolve(readString(unshared));
    
                    case TC_ARRAY:
                        return checkResolve(readArray(unshared));
    
                    case TC_ENUM: // 前面的 啥逻辑 我们也看不太懂,但是这里可以看到是和枚举相关的 ,那么我们 去看看 readEnum(unshared)
                        return checkResolve(readEnum(unshared));
    
                    case TC_OBJECT:
                        return checkResolve(readOrdinaryObject(unshared));
    
                    case TC_EXCEPTION:
                        IOException ex = readFatalException();
                        throw new WriteAbortedException("writing aborted", ex);
    
                    case TC_BLOCKDATA:
                    case TC_BLOCKDATALONG:
                        if (oldMode) {
                            bin.setBlockDataMode(true);
                            bin.peek();             // force header read
                            throw new OptionalDataException(
                                bin.currentBlockRemaining());
                        } else {
                            throw new StreamCorruptedException(
                                "unexpected block data");
                        }
    
                    case TC_ENDBLOCKDATA:
                        if (oldMode) {
                            throw new OptionalDataException(true);
                        } else {
                            throw new StreamCorruptedException(
                                "unexpected end of block data");
                        }
    
                    default:
                        throw new StreamCorruptedException(
                            String.format("invalid type code: %02X", tc));
                }
            } finally {
                depth--;
                bin.setBlockDataMode(oldMode);
            }
        }
    
    
    ------ readEnum(unshared)
    
    private Enum<?> readEnum(boolean unshared) throws IOException {
            if (bin.readByte() != TC_ENUM) {
                throw new InternalError();
            }
    
            ObjectStreamClass desc = readClassDesc(false);// 这里就是得到对象的 描述 ObjectStreamClass 对象,
            if (!desc.isEnum()) {// 判断是否是枚举
                throw new InvalidClassException("non-enum class: " + desc);
            }
    
            int enumHandle = handles.assign(unshared ? unsharedMarker : null);
            ClassNotFoundException resolveEx = desc.getResolveException();
            if (resolveEx != null) {
                handles.markException(enumHandle, resolveEx);
            }
    
            String name = readString(false);
            Enum<?> result = null;
            Class<?> cl = desc.forClass();
            if (cl != null) {
                try {
                    @SuppressWarnings("unchecked")
                    Enum<?> en = Enum.valueOf((Class)cl, name); // 得到 Enum 对象,我们知道 枚举 是继承 Enum 的,这里也是 返回的 Enum 。通过 枚举 class 对象 和 name 就得到 了一个唯一的对象,这个name 就是 我们通常自己定义的 枚举的对象的name。我们可以继续 下去,看看 Enum.valueOf((Class)cl, name); 里面是啥
                    result = en;
                } catch (IllegalArgumentException ex) {
                    throw (IOException) new InvalidObjectException(
                        "enum constant " + name + " does not exist in " +
                        cl).initCause(ex);
                }
                if (!unshared) {
                    handles.setObject(enumHandle, result);
                }
            }
    
            handles.finish(enumHandle);
            passHandle = enumHandle;
            return result;
    
    ----- name 枚举对象的name
    System.out.println(EnumSingleton.INSTANCE.name()); // INSTANCE
    
    ----- Enum.valueOf((Class)cl, name); 
    Enum :
     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);
        }
    
    
    -----------  T result = enumType.enumConstantDirectory().get(name); 
    Class<T> enumType:
     Map<String, T> enumConstantDirectory() {
            if (enumConstantDirectory == null) {
                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;
        }
    // 该map 不能被序列化
    private volatile transient Map<String, T> enumConstantDirectory = null;
    // 到这里我们也就看到了 原来 他还是一个 map 集合,map 里面就保存了枚举对象,在被调用的时候,就判断一下,如果是空,就存储枚举对象,然后返回,然后通过name 取获取,每次都是获取到的一个 对象,这个也叫做 注册式单例,spring 就是典型的注册式单例。
    

    上面我们分析了 为啥反序列化得时候 ,得到的也是相同的枚举对象,就是要因为 jvm 自己讲枚举对象存在了一个map 集合里面,然后每次都是去 map 里面取,对象也就只创建了一次,后面都是 读出来的自然也就 是单例了,这也就 注册式单例。到这里我们就分析完了,枚举能够防止反射和序列化破坏的原因了。

    4.4 ThreadLocal 单例

    上面枚举单例里面提及到了注册式单例,现在我们来看看另外一种注册式单例--ThreadLocal 单例。

    • ThreadLocalSingleton
    public class ThreadLocalSingleton {
    
        private ThreadLocalSingleton () {}
    
        private static final ThreadLocal<ThreadLocalSingleton> threadLocal  = new ThreadLocal<ThreadLocalSingleton>() {
            @Override
            protected ThreadLocalSingleton initialValue() {//初始化值
                return new ThreadLocalSingleton();
            }
        };
        public static ThreadLocalSingleton getInstance() {
            return threadLocal.get();
        }
    }
    

    ThreadLocal 本身的特点是变量和线程存在绑定的映射关系,我们先来看测试结果,就明白是啥意思了。为了方便理解ThreadLocal 的特点,我们使用线程池来测试。

    • 线程池测试
     public static void test() throws InterruptedException {
    // 创建有5个线程的线程池
            ExecutorService executorService = Executors.newFixedThreadPool(5);
            CountDownLatch countDownLatch = new CountDownLatch(5);
            for (int i = 0 ;i<10; i++) {// 开启10 个线程,有线程池去处理。
                executorService.submit(()->{
                    countDownLatch.countDown();
                    try {
                        TimeUnit.SECONDS.sleep(3);
                    } catch (InterruptedException e) {
    
                        e.printStackTrace();
                    }
    
                    ThreadLocalSingleton instance = ThreadLocalSingleton.getInstance();
                    System.out.println(Thread.currentThread().getName() + "___" + instance);
                });
            }
            countDownLatch.await();
        }
    
    • 测试结果
    pool-1-thread-5___com.example.designpattern.singleton.ThreadLocalSingleton@6c1553f
    pool-1-thread-1___com.example.designpattern.singleton.ThreadLocalSingleton@6f08d27f
    pool-1-thread-5___com.example.designpattern.singleton.ThreadLocalSingleton@6c1553f
    pool-1-thread-3___com.example.designpattern.singleton.ThreadLocalSingleton@3f46008f
    pool-1-thread-2___com.example.designpattern.singleton.ThreadLocalSingleton@d85fdfa
    pool-1-thread-4___com.example.designpattern.singleton.ThreadLocalSingleton@447a380d
    pool-1-thread-5___com.example.designpattern.singleton.ThreadLocalSingleton@6c1553f
    pool-1-thread-3___com.example.designpattern.singleton.ThreadLocalSingleton@3f46008f
    pool-1-thread-1___com.example.designpattern.singleton.ThreadLocalSingleton@6f08d27f
    pool-1-thread-2___com.example.designpattern.singleton.ThreadLocalSingleton@d85fdfa
    
    
    /**
     * 注册式单例
     */
    public class RegisterSingleton {
    
        private static Map<String,RegisterSingleton> map = new ConcurrentHashMap<>(1);
    
        private RegisterSingleton() {}
        private static volatile RegisterSingleton instance;
        public static RegisterSingleton getInstance() {
            instance = map.get("instance");
           if (null == instance) {
               synchronized (RegisterSingleton.class) {
                   instance = map.get("instance");
                   if (null == instance) {
                       instance = new RegisterSingleton();
                       map.put("instance",instance);
                   }
               }
           }
           return instance;
        }
    
        public static void main(String[] args) {
            for (int i=0;i<100; i++) {
                new Thread(()->{
                    RegisterSingleton instance = RegisterSingleton.getInstance();
                    System.out.println(instance);
                }).start();
            }
        }
    }
    

    我们观察相同线程 获取到的对象是一样的,这个就是ThreadLocal 本身的特点,也就说 同一个线程获取到的对象始终是一个,对单个线程来说 这也就是单例了。但是对于不同的线程来说 是获取到不同的对象。这个和ThreadLocal 本身的数据结构有关系。我们可以去看一看jdk 的源码,这里我们就不展开了,内部是维护了一个 ThreadLocalMap 静态内部来存储当前线程的值,所以每个线程都有一个ThreadLocalMap 对象与之对应,获取到值也只自己线程的。到此,我们可以总结出一个结论:注册式单例就是 对象创建一次,然后存放到 Map 中,后面去Map 里面直接获取就ok。

    上面我们说了注册式单例和 ThreadLocal 的单例,注册式单例的实现方式有很多种,但是唯一不变的就是 底层的数据结构一定是 Map 的,然后保证 在访问Map 的时候 是线程安全的就行。最后介绍一种CAS实现的 单例。

    • CAS 单例--原子引用类
    public class CASSingleton {
    
        private CASSingleton() {
        }
    
        private final static AtomicReference<CASSingleton> atomicReference = new AtomicReference<>();
    
        public static CASSingleton getInstance() {
    
            for (; ; ) {//自旋
                CASSingleton casSingleton = atomicReference.get();
                if (casSingleton != null) {
                    return casSingleton;
                }
                casSingleton = new CASSingleton();
                // CSA 操作:如果当前的值是 null,就更新 为 casSingleton
                boolean compareAndSet = atomicReference.compareAndSet(null, casSingleton);
                if (compareAndSet) {
                    return casSingleton;
                }
            }
        }
    
    • 测试:
     public static void main(String[] args) {
            for (int i = 0; i < 5; i++) {
                new Thread(() -> {
                    try {
                        TimeUnit.SECONDS.sleep(3);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    CASSingleton instance = CASSingleton.getInstance();
                    System.out.println(Thread.currentThread().getName() + ":" + instance);
                }).start();
            }
            CASSingleton instance = CASSingleton.getInstance();
            CASSingleton instance2 = CASSingleton.getInstance();
            CASSingleton instance3 = CASSingleton.getInstance();
            CASSingleton instance4 = CASSingleton.getInstance();
    
            System.out.println(instance);
            System.out.println(instance2);
            System.out.println(instance3);
            System.out.println(instance4);
        }
    

    com.example.designpattern.singleton.CASSingleton@52cc8049
    com.example.designpattern.singleton.CASSingleton@52cc8049
    com.example.designpattern.singleton.CASSingleton@52cc8049
    com.example.designpattern.singleton.CASSingleton@52cc8049
    Thread-1:com.example.designpattern.singleton.CASSingleton@52cc8049
    Thread-0:com.example.designpattern.singleton.CASSingleton@52cc8049
    Thread-3:com.example.designpattern.singleton.CASSingleton@52cc8049
    Thread-2:com.example.designpattern.singleton.CASSingleton@52cc8049
    Thread-4:com.example.designpattern.singleton.CASSingleton@52cc8049

    有关CAS 相关知识如果不熟悉的话,可以去学习一下并发编程相关的知识。

    总结一下:
    到此就介绍了单例的常见的实现方式:double check ,静态内部类,枚举,饿汉式,ThreadLocal,CAS 单例,注册式单例,当然肯定还有其他的变种写法,但是根本的原则不会改变-- JVM 中整个生命周期中只存在一个对象实例。同时,也分析了单例被破坏的情况,反射和序列化。当然项目中不会故意去破坏,但是无意的破坏是可能的,比如反射破坏。好了,这就是笔者对单例模式的理解,如果不足之处,欢迎留言讨论!

    相关文章

      网友评论

        本文标题:单例设计模式

        本文链接:https://www.haomeiwen.com/subject/lsdrgctx.html