系统架构设计之单例模式(下)

注册式单例

注册式单例模式又称为登记式单例模式,就是将每一个实例都登记到某一个地方,使用唯一的标识获取实例。

注册式单例模式有两种:一种为枚举式单例模式,另一种为容器式单例模式。

枚举式单例模式

编写一个 Demo 示例说明

  • 一个经典的枚举类单例
java 复制代码
public enum EnumSingleton {
    INSTANCE;

    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
}
  • 编写测试类
java 复制代码
    public static void main(String[] args) {
        EnumSingleton instance = EnumSingleton.getInstance();
        instance.setData(new Object());

        try {
            Class clazz = EnumSingleton.class;

            Constructor c = clazz.getDeclaredConstructor(String.class,int.class);
            c.setAccessible(true);
//            System.out.println(c);
            Object o = c.newInstance();
//            System.out.println(o);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
  • 运行看结果

返回异常提示,在源码中判断(clazz.getModifiers() & Modifier.ENUM) != 0条件,当类的修饰类型为 Enum 时返回Cannot reflectively create enum objects,即无法通过反射创建枚举对象。

我们还是习惯性地想来看看JDK源码,进入Constructor 的 newInstance()方法

java 复制代码
 public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        Class<?> caller = override ? null : Reflection.getCallerClass();
        return newInstanceWithCaller(initargs, !override, caller);
    }
newInstanceWithCaller(Object[] args, boolean checkAccess, Class<?> caller)
        throws InstantiationException, IllegalAccessException,
               InvocationTargetException
    {
        if (checkAccess)
            checkAccess(caller, clazz, clazz, 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(args);
        return inst;
    }

从上述代码可以看到,在 newInstance()方法中做了强制性的判断,如果修饰符是 Modifier.ENUM枚举类型,则直接抛出异常。这岂不是和静态内部类的处理方式有异曲同工之妙?对,但是我们自己再构造方法中写逻辑处理可能存在未知的风险,而JDK 的处理是最官方、最权威、最稳定的。因此枚举式单例模式也是《Effective Java》书中推荐的一种单例模式实现写法。

到此为止,我们是不是已经非常清晰明了呢?JDK 枚举的语法特殊性及反射也为枚举保驾护航,让枚举式单例模式成为一种比较优雅的实现。
至此我们知道了枚举式单例可以防止反射破坏单例,那线程安全又从何谈起呢?

我们不妨从 Enum 类型源码中 valueOf 方法中看看

java 复制代码
    public static <T extends Enum<T>> T valueOf(Class<T> enumClass,
                                                String name) {
        T result = enumClass.enumConstantDirectory().get(name);
        if (result != null)
            return result;
        if (name == null)
            throw new NullPointerException("Name is null");
        throw new IllegalArgumentException(
            "No enum constant " + enumClass.getCanonicalName() + "." + name);
    }
java 复制代码
    Map<String, T> enumConstantDirectory() {
        Map<String, T> directory = enumConstantDirectory;
        if (directory == null) {
            T[] universe = getEnumConstantsShared();
            if (universe == null)
                throw new IllegalArgumentException(
                    getName() + " is not an enum class");
            directory = new HashMap<>((int)(universe.length / 0.75f) + 1);
            for (T constant : universe) {
                directory.put(((Enum<?>)constant).name(), constant);
            }
            enumConstantDirectory = directory;
        }
        return directory;
    }
java 复制代码
  private transient volatile Map<String, T> enumConstantDirectory;

Enum 类型他的每个单例在类类声明的时候就创建好了,通过 valueOf 方法进行获取,而我们通过 valueOf 源码可以得知,当声明枚举时,该枚举就被当成常量enumConstantDirectory注册到 Map 容器中,其创建方式类似于饿汉式,当大批量创建对象时,又会造成内存浪费

容器式单例

其实枚举式单例,虽然写法优雅,但是也会有一些问题。因为它在类加载之时就将所有的对象初始化放在类内存中,这其实和饿汉式并无差异,不适合大量创建单例对象的场景。那么,接下来看注册式单例模式的另一种写法,即容器式单例模式,创建 ContainerSingleton 类:

java 复制代码
public class ContainerSingleton {
    //构造方法私有化
    private ContainerSingleton(){}

    //创建一个ioc容器
    private static Map<String,Object> ioc = new ConcurrentHashMap<String, Object>();

    public static Object getInstance(String className){
        Object instance = null;
        if(!ioc.containsKey(className)){
            try {
                instance = Class.forName(className).newInstance();
                ioc.put(className, instance);
            }catch (Exception e){
                e.printStackTrace();
            }
            return instance;
        }else{
            return ioc.get(className);
        }
    }

}
  • 创建一个 Pojo 类,通过 ioc 容器创建
java 复制代码
public class Pojo {}
  • 编写测试类
java 复制代码
public class ContainerSingletonTest {
    public static void main(String[] args) {
        Object instance1 = ContainerSingleton.getInstance("com.gupaoedu.vip.pattern.singleton.test.Pojo");
        Object instance2 = ContainerSingleton.getInstance("com.gupaoedu.vip.pattern.singleton.test.Pojo");
        System.out.println(instance1 == instance2);
    }
}
  • 运行结果

容器式单例如何解决线程安全问题?

ThreadLocal 单例

保证线程内部全局唯一,且天生线程安全

  • 编写一个简单的 Demo
java 复制代码
public class ThreadLocalSingleton {
    private static final ThreadLocal<ThreadLocalSingleton> threadLocaLInstance =
            new ThreadLocal<ThreadLocalSingleton>() {
                @Override
                protected ThreadLocalSingleton initialValue() {
                    return new ThreadLocalSingleton();
                }
            };

    private ThreadLocalSingleton() {
    }

    public static ThreadLocalSingleton getInstance() {
        return threadLocaLInstance.get();
    }
}
java 复制代码
    public static void main(String[] args) {
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
    }
  • 查看结果,在线程无关时,满足单实例

模拟多线程情况下,单例是否被破坏

  • 修改代码 ExecutorThread
java 复制代码
public class ExectorThread implements Runnable{
    public void run() {
//        LazySimpleSingleton instance = LazySimpleSingleton.getInstance();
        ThreadLocalSingleton instance = ThreadLocalSingleton.getInstance();
        System.out.println(Thread.currentThread().getName() + ":" + instance);
    }
}
  • 查看结果

我们发现,在主线程中无论调用多少次,获取到的实例都是同一个,都在两个子线程中分别获取到了不同的实例。那么 ThreadLocal 是如何实现这样的效果的呢?我们知道,单例模式为了达到线程安全的目的,会给方法上锁,以时间换空间。ThreadLoca 将所有的对象全部放在 ThreadLocalMap 中为每个线程都提供一个对象,实际上是以空间换时间来实现线程隔离的。

  • 可以看到单例在多线程情况下,单实例被"破坏"了,但实际上是正常的没因为ThreadLocal 单例是维持在线程内部单例,不同线程下可多实例
  • ThreadLocal本身是基于线程的,main 本身就是就是一个线程,因此前三次输出的为mian线程中的单例,都是一致的,而后面Thread-0Thread-1都是单独的另外的线程
    我们需要思考一下,ThreadLocal是如何实现线程之间的单例隔离呢?

这里我们需要进入ThreadLocal类中的一个方法get

java 复制代码
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

进入方法之后先判断 map 对象是否为空,若为空,以当前调用 get 方法的线程作为单例实例对象存入到 map 容器中,作为 key 值,并将线程信息存入到 map 中

单例模式在源码中的应用

  • 在 SpringBoot 源码中的应用AbstractFactoryBean.class
java 复制代码
..........
public final T getObject() throws Exception {
        if (this.isSingleton()) {
            return (T)(this.initialized ? this.singletonInstance : this.getEarlySingletonInstance());
        } else {
            return (T)this.createInstance();
        }
    }

    private T getEarlySingletonInstance() throws Exception {
        Class[] ifcs = this.getEarlySingletonInterfaces();
        if (ifcs == null) {
            throw new FactoryBeanNotInitializedException(this.getClass().getName() + " does not support circular references");
        } else {
            if (this.earlySingletonInstance == null) {
                this.earlySingletonInstance = (T)Proxy.newProxyInstance(this.beanClassLoader, ifcs, new EarlySingletonInvocationHandler());
            }

            return this.earlySingletonInstance;
        }
    }
..........
  • MyBatis (3.5)中的ErrorContext.class
  • 来看一个JDK 的一个经典应用 Runtime 类,其源码如下
java 复制代码
public class Runtime {
    private static final Runtime currentRuntime = new Runtime();

    private static Version version;

    public static Runtime getRuntime() {
        return currentRuntime;
    }

    private Runtime() {}
........
}

单例模式的优缺点

  • 优点
    • 在内存中只有一个实例,减少内存开销
    • 可以避免对资源的多重占用
    • 设置全局访问点,严格控制访问
  • 缺点
    • 没有接口,扩展困难
    • 若需扩展单例对象,只能修改代码,耦合度高

使用单例时应考关注的方向

  • 私有化构造方法
  • 是否多线程,是否保证线程安全
  • 是否可延时加载
  • 是否能够防止序列化与反序列化破坏单例
  • 是否防御反射攻击单例

相关推荐
小北方城市网2 小时前
第 4 课:前端工程化进阶 ——Vue 核心语法 + 组件化开发(前端能力质的飞跃)
大数据·开发语言·数据库·python·状态模式·数据库架构
zhaokuner2 小时前
02-通用语言与协作-DDD领域驱动设计
java·开发语言·设计模式·架构
㳺三才人子2 小时前
初探 Python + Django
开发语言·python·django
嵌入式×边缘AI:打怪升级日志2 小时前
USB设备枚举过程详解:从插入到正常工作
开发语言·数据库·笔记
C_心欲无痕2 小时前
react - createPortal魔法传送门
javascript·vue.js·react.js
前端小L2 小时前
双指针专题(五):灵活的起跳——「无重复字符的最长子串」
javascript·算法·双指针与滑动窗口
molaifeng2 小时前
深度解密 Go 语言调度器:GMP 模型精讲
开发语言·golang·gmp
资生算法程序员_畅想家_剑魔2 小时前
Java常见技术分享-27-事务安全-事务日志-事务日志框架
java·开发语言
靓仔建2 小时前
在Electron用npm install 失败。
javascript·electron·npm