死磕GOF23之单例模式

简介: 死磕GOF23之单例模式

内容目录

什么是GOF23设计模式分类什么是单例模式单例模式特点单例模式VS静态类单例模式的实现方式饿汉模式懒汉模式双重校验锁静态内部类枚举小结Spring单例模式实践总结

无论什么开发语言,设计模式在软件开发的过程中是必不可少的,它是软件开发过程中常见问题的成熟解决方案,也是最佳实践。本系列课程带领大家领略一下GOF23种设计模式。

什么是GOF23

Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides 四人合著出版了一本名为Design Patterns - Elements of Reusable Object-Oriented Software(中文译名:设计模式 - 可复用的面向对象软件元素)的书,该书首次提到了软件开发中设计模式的概念。

四位作者合称 GOF(四人帮,全拼 Gang of Four)。书中共提到23中设计模式,因此也称GOF23种设计模式。

设计模式分类

GOF23种设计模式可分为以下三类:

  • 创建型模式(Creational Patterns):关注对象的实例化,不是使用new创建对象,而是隐藏创建对象的逻辑,只提供对应的方法;
  • 结构型模式(Structural Patterns):关注类与对象的组合,从而获得新的能力;
  • 行为型模式(Behavioral Patterns):关注对象之间的通信;

什么是单例模式

单例模式(Singleton Patterns),当在程序中需要保证某个类只存在一个实例时,往往就会用到单例设计模式。单例模式能够避免实例对象重复创建,减少创建对象的时间开销和内存的存储开销;能够避免由于操作多个实例导致的逻辑错误。

单例模式特点

  • 单例类只能有一个实例;
  • 单例类只能自己创建自己的唯一实例;
  • 单例类必须提供给其他类获得此实例的方法;

单例模式VS静态类

单例模式和静态类有很多相似之处,那单例模式的优势是什么呢?

单例模式相对静态类的优势:

  • 面向对象编程,可继承、可实现接口,而静态类无法做到;
  • 单例模式可被延迟加载(懒汉模式),静态类一般在第一次加载时初始化;
  • 单例类可以被集成,方法可以被覆盖;
  • 静态方法中产生的对象会在执行后被释放,进而被GC清理,不会一直存在内存中;
  • 单例模式可以附带更多的其他信息,比如读取的配置信息;

那么,数据库链接是否能够做成单例模式呢?这里分两种情况:简单封装Connection对象和数据库连接池。

试想一下,如果简单封装一个Connection连接,做成单例模式,那么整个应用程序只能够使用一个数据库连接,那会死的很惨!但数据库连接池可以使用单例模式,初始化时创建指定数量的数据库连接,使用单例模式来保证只有一个对象来创建和初始化连接池。

单例模式的实现方式

饿汉模式

package com.secbro2.gof23.singleton;
/**
 * Hungry Singleton Patterns
 *
 * @author zzs
 */
public class HungrySingleton {
    private static HungrySingleton instance = new HungrySingleton();
    private HungrySingleton() {
    }
    public static HungrySingleton getInstance() {
        return instance;
    }
    public void helloSingleton() {
        System.out.println("Hello HungrySingleton!");
    }
}

饿汉模式,类初始化时即创建对应的对象实例,保证了线程的安全性,然后提供一个静态的方法返回实例对象。但饿汉模式实例存在于整个程序的声明周期,即使单例未被使用到也会被创建,会造成一定的内存资源浪费。

总结一下单例模式的基本实现:第一,单例类构造参数私有化,除了单例类自身其他类不可以直接创建该类的对象;第二,提供一个私有的static变量;第三、提供获取该类实例的public的方法;

懒汉模式

懒汉模式,当需要的时候才会创建对应的实例,避免内存浪费。如果某个单例使用的次数较少,并且创建单例消耗的资源较多,那么就需要按需创建,适合懒汉模式。懒汉模式分两种:线程不安全的和线程安全的。

先来看一下最简单的懒汉模式实现:

package com.secbro2.gof23.singleton;
/**
 * Singleton Patterns<br/>
 * <p>
 * Not Thread Safe;
 *
 * @author zzs
 */
public class Singleton {
    private static Singleton instance;
    private Singleton() {
    }
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
    public void helloSingleton() {
        System.out.println("Hello Singleton!");
    }
}

上面代码实现了一个线程不安全的单例模式。

下面,通过添加synchronized来保证在多线程的情况下也可以正常工作。

package com.secbro2.gof23.singleton;
/**
 * Singleton Patterns<br/>
 * <p>
 * Thread Safe;
 *
 * @author zzs
 */
public class SingletonThreadSafe {
    private static SingletonThreadSafe instance;
    private SingletonThreadSafe() {
    }
    public static synchronized SingletonThreadSafe getInstance() {
        if (instance == null) {
            instance = new SingletonThreadSafe();
        }
        return instance;
    }
    public void helloSingleton() {
        System.out.println("Hello SingletonThreadSafe!");
    }
}

双重校验锁

上面在获取方法上使用了synchronized关键字,将整个方法变为同步的,但如果此实例获取的次数较多,此处可能会出现瓶颈。因此,针对此项也进行进一步的优化,这便有了双重校验锁。

package com.secbro2.gof23.singleton;
/**
 * Singleton Patterns<br/>
 * <p>
 * Double checked lock;
 *
 * @author zzs
 */
public class SingletonThreadSafe1 {
    private static SingletonThreadSafe1 instance;
    private SingletonThreadSafe1() {
    }
    public static SingletonThreadSafe1 getInstance() {
        if (instance == null) {
            synchronized (SingletonThreadSafe1.class) {
                if (instance == null) {
                    instance = new SingletonThreadSafe1();
                }
            }
        }
        return instance;
    }
    public void helloSingleton() {
        System.out.println("Hello SingletonThreadSafe1!");
    }
}

将synchronized范围缩小到创建对象的代码,同时在同步代码外面多了一层instance为空的判断。这样既在创建时保证了线程的安全性又可以在后面使用中直接返回已经创建的对象,无需每次都对整个方法进行同步处理。双重校验锁即实现了延迟加载,又解决了线程并发问题,同时还解决了执行效率问题。

虽然上面的方式解决了效率问题,但又引入了其他的问题。此问题是因为JVM指令的重排优化导致的。在java中看似按照顺序执行的代码,在JVM中可能会出现编译器或者CPU对操作指令进行重新排序。

上面synchronized中处理如果按照代码顺序执行应该是这样:

  • 分配内存空间;
  • 初始化对象;
  • 将对象指向刚分配的内存空间;

但如果编译器为了性能的原因可能会将第二步和第三步进行重排:

  • 分配内存空间;
  • 将对象指向刚分配的内存空间;
  • 初始化对象;

这样在高并发的情况下会导致后面的线程判断instance并不为null,但又不是预期的对象。那么如何解决指令重排问题呢?在JDK1.5及以后版本引入了volitile关键字。volatile的一个语义是禁止指令重排序优化,这就保证了instance变量被赋值的时候对象已经是初始化过的,从而避免了上面说到的问题。代码如下:

package com.secbro2.gof23.singleton;
/**
 * Singleton Patterns<br/>
 * <p>
 * Double checked lock and volatile;
 *
 * @author zzs
 */
public class SingletonThreadSafe2 {
    private static volatile SingletonThreadSafe2 instance;
    private SingletonThreadSafe2() {}
    public static SingletonThreadSafe2 getInstance() {
        if (instance == null) {
            synchronized (SingletonThreadSafe2.class) {
                if (instance == null) {
                    instance = new SingletonThreadSafe2();
                }
            }
        }
        return instance;
    }
    public void helloSingleton() {
        System.out.println("Hello SingletonThreadSafe1!");
    }
}

静态内部类

首先看一下代码实现:

package com.secbro2.gof23.singleton;
/**
 * Singleton Patterns<br/>
 * <p>
 * Inner Class Singleton;
 *
 * @author zzs
 */
public class InnerClassSingleton {
    private InnerClassSingleton() {
    }
    private static class InnerClassSingletonHolder {
        public static InnerClassSingleton instance = new InnerClassSingleton();
    }
    public static InnerClassSingleton getInstance() {
        return InnerClassSingletonHolder.instance;
    }
    public void helloSingleton() {
        System.out.println("Hello Singleton!");
    }
}

这种方式同样利用了类加载机制来保证只创建一个instance实例,不存在多线程并发问题。利用内部类去创建对象实例,只要不使用内部类,JVM就不会加载此类,因此也就不会去创建对象,从而达到延迟加载的目的。

枚举

通过枚举实现单例模式,这是Effective Java作者Josh Bloch提倡的方式,不仅能避免线多线程同步问题,还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。支持JDK1.5及以后版本。

package com.secbro2.gof23.singleton;
/**
 * Enum Singleton Patterns
 *
 * @author zzs
 */
public enum EnumSingleton {
    INSTANCE;
    public void helloSingleton() {
        System.out.println("Hello HungrySingleton!");
    }
}

单元测试方法:

@Test
public void testEnumSingleton(){
    EnumSingleton enumSingleton = EnumSingleton.INSTANCE;
    enumSingleton.helloSingleton();
}

小结

上面介绍了这么多单例模式的实现,那么在具体实践中如何选择呢。如果没有特殊要求,一般情况下可采用饿汉模式,如果需要懒加载,则可使用双重校验锁和静态内部类的方式。当然也可以尝试使用枚举的方式。

Spring单例模式实践

下面我们在Spring的源代码中寻找一下单例模式的应用实践。在Spring中spring-bean 4.3.13中AbstractBeanFactory类里面提供了一个getSingleton方法,源代码如下:

/**
     * Return the (raw) singleton object registered under the given name.
     * <p>Checks already instantiated singletons and also allows for an early
     * reference to a currently created singleton (resolving a circular reference).
     * @param beanName the name of the bean to look for
     * @param allowEarlyReference whether early references should be created or not
     * @return the registered singleton object, or {@code null} if none found
     */
    protected Object getSingleton(String beanName, boolean allowEarlyReference) {
        Object singletonObject = this.singletonObjects.get(beanName);
        if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
            synchronized (this.singletonObjects) {
                singletonObject = this.earlySingletonObjects.get(beanName);
                if (singletonObject == null && allowEarlyReference) {
                    ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                    if (singletonFactory != null) {
                        singletonObject = singletonFactory.getObject();
                        this.earlySingletonObjects.put(beanName, singletonObject);
                        this.singletonFactories.remove(beanName);
                    }
                }
            }
        }
        return (singletonObject != NULL_OBJECT ? singletonObject : null);
    }

对照我们上面所讲的几种单例模式,很显然,Spring采用了双重校验锁的模式。先从缓存(ConcurrentHashMap)中获取bean实例,如果为null则对map进行加锁,然后再次从缓存中获取bean实例,如果仍然为null,则进行创建。

总结

今天这篇文章带领大家学习了单例模式的集中应用场景及使用案例。源代码可访问GitHub:https://github.com/secbr/gof23,随后会继续为大家带来更多的设计模式及实践案例。

目录
相关文章
|
10天前
|
存储 关系型数据库 分布式数据库
PostgreSQL 18 发布,快来 PolarDB 尝鲜!
PostgreSQL 18 发布,PolarDB for PostgreSQL 全面兼容。新版本支持异步I/O、UUIDv7、虚拟生成列、逻辑复制增强及OAuth认证,显著提升性能与安全。PolarDB-PG 18 支持存算分离架构,融合海量弹性存储与极致计算性能,搭配丰富插件生态,为企业提供高效、稳定、灵活的云数据库解决方案,助力企业数字化转型如虎添翼!
|
9天前
|
存储 人工智能 Java
AI 超级智能体全栈项目阶段二:Prompt 优化技巧与学术分析 AI 应用开发实现上下文联系多轮对话
本文讲解 Prompt 基本概念与 10 个优化技巧,结合学术分析 AI 应用的需求分析、设计方案,介绍 Spring AI 中 ChatClient 及 Advisors 的使用。
400 130
AI 超级智能体全栈项目阶段二:Prompt 优化技巧与学术分析 AI 应用开发实现上下文联系多轮对话
|
3天前
|
存储 安全 前端开发
如何将加密和解密函数应用到实际项目中?
如何将加密和解密函数应用到实际项目中?
197 138
|
9天前
|
人工智能 Java API
AI 超级智能体全栈项目阶段一:AI大模型概述、选型、项目初始化以及基于阿里云灵积模型 Qwen-Plus实现模型接入四种方式(SDK/HTTP/SpringAI/langchain4j)
本文介绍AI大模型的核心概念、分类及开发者学习路径,重点讲解如何选择与接入大模型。项目基于Spring Boot,使用阿里云灵积模型(Qwen-Plus),对比SDK、HTTP、Spring AI和LangChain4j四种接入方式,助力开发者高效构建AI应用。
376 122
AI 超级智能体全栈项目阶段一:AI大模型概述、选型、项目初始化以及基于阿里云灵积模型 Qwen-Plus实现模型接入四种方式(SDK/HTTP/SpringAI/langchain4j)
|
3天前
|
存储 JSON 安全
加密和解密函数的具体实现代码
加密和解密函数的具体实现代码
195 136
|
21天前
|
弹性计算 关系型数据库 微服务
基于 Docker 与 Kubernetes(K3s)的微服务:阿里云生产环境扩容实践
在微服务架构中,如何实现“稳定扩容”与“成本可控”是企业面临的核心挑战。本文结合 Python FastAPI 微服务实战,详解如何基于阿里云基础设施,利用 Docker 封装服务、K3s 实现容器编排,构建生产级微服务架构。内容涵盖容器构建、集群部署、自动扩缩容、可观测性等关键环节,适配阿里云资源特性与服务生态,助力企业打造低成本、高可靠、易扩展的微服务解决方案。
1347 8
|
8天前
|
监控 JavaScript Java
基于大模型技术的反欺诈知识问答系统
随着互联网与金融科技发展,网络欺诈频发,构建高效反欺诈平台成为迫切需求。本文基于Java、Vue.js、Spring Boot与MySQL技术,设计实现集欺诈识别、宣传教育、用户互动于一体的反欺诈系统,提升公众防范意识,助力企业合规与用户权益保护。
|
20天前
|
机器学习/深度学习 人工智能 前端开发
通义DeepResearch全面开源!同步分享可落地的高阶Agent构建方法论
通义研究团队开源发布通义 DeepResearch —— 首个在性能上可与 OpenAI DeepResearch 相媲美、并在多项权威基准测试中取得领先表现的全开源 Web Agent。
1454 87