设计模式

单例设计模式

2019-12-04  本文已影响0人  愤怒的奶牛
1. 基本概念

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

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

双重检查锁
静态内部类

3. 破坏单例的方式
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

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行 抛出的异常。我们就去看一下 :

@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 单例。

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实现的 单例。

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

上一篇 下一篇

猜你喜欢

热点阅读