死磕GOF23之单例模式

简介: 死磕GOF23之单例模式

无论什么开发语言,设计模式在软件开发的过程中是必不可少的,它是软件开发过程中常见问题的成熟解决方案,也是最佳实践。本系列课程带领大家领略一下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,随后会继续为大家带来更多的设计模式及实践案例。


目录
相关文章
|
17天前
|
存储 弹性计算 人工智能
【2025云栖精华内容】 打造持续领先,全球覆盖的澎湃算力底座——通用计算产品发布与行业实践专场回顾
2025年9月24日,阿里云弹性计算团队多位产品、技术专家及服务器团队技术专家共同在【2025云栖大会】现场带来了《通用计算产品发布与行业实践》的专场论坛,本论坛聚焦弹性计算多款通用算力产品发布。同时,ECS云服务器安全能力、资源售卖模式、计算AI助手等用户体验关键环节也宣布升级,让用云更简单、更智能。海尔三翼鸟云服务负责人刘建锋先生作为特邀嘉宾,莅临现场分享了关于阿里云ECS g9i推动AIoT平台的场景落地实践。
【2025云栖精华内容】 打造持续领先,全球覆盖的澎湃算力底座——通用计算产品发布与行业实践专场回顾
|
9天前
|
云安全 人工智能 安全
Dify平台集成阿里云AI安全护栏,构建AI Runtime安全防线
阿里云 AI 安全护栏加入Dify平台,打造可信赖的 AI
|
12天前
|
人工智能 运维 Java
Spring AI Alibaba Admin 开源!以数据为中心的 Agent 开发平台
Spring AI Alibaba Admin 正式发布!一站式实现 Prompt 管理、动态热更新、评测集构建、自动化评估与全链路可观测,助力企业高效构建可信赖的 AI Agent 应用。开源共建,现已上线!
1044 33
|
11天前
|
机器学习/深度学习 人工智能 搜索推荐
万字长文深度解析最新Deep Research技术:前沿架构、核心技术与未来展望
近期发生了什么自 2025 年 2 月 OpenAI 正式发布Deep Research以来,深度研究/深度搜索(Deep Research / Deep Search)正在成为信息检索与知识工作的全新范式:系统以多步推理驱动大规模联网检索、跨源证据。
806 55
|
9天前
|
文字识别 测试技术 开发者
Qwen3-VL新成员 2B、32B来啦!更适合开发者体质
Qwen3-VL家族重磅推出2B与32B双版本,轻量高效与超强推理兼备,一模型通吃多模态与纯文本任务!
697 11
下一篇
开通oss服务