单例设计模式

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 中整个生命周期中只存在一个对象实例。同时,也分析了单例被破坏的情况,反射和序列化。当然项目中不会故意去破坏,但是无意的破坏是可能的,比如反射破坏。好了,这就是笔者对单例模式的理解,如果不足之处,欢迎留言讨论!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,302评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,563评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,433评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,628评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,467评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,354评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,777评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,419评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,725评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,768评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,543评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,387评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,794评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,032评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,305评论 1 252
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,741评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,946评论 2 336