草庐IT

初识设计模式 - 单例模式

程序员翔仔 2023-03-28 原文

简介

一个类只允许创建一个对象(或实例),那么这个类就是一个单例类,这种设计模式称作单例设计模式(Singleton Design Pattern),简称单例模式。

单例模式保证系统内存中只存在一个对象,非常节省系统资源,对于一些需要频繁销毁的对象,使用单例模式可以提高系统性能。

一个普通单例模式的实现方式主要是以下三个步骤:

  1. 将单例类的构造方法定义为私有方法,禁止外部直接调用构造方法来实例化单例类的对象;
  2. 在类的内部创建并保存类的唯一实例,并设置成私有变量,禁止外部直接调用这个实例变量;
  3. 创建一个公开的静态方法,对外暴露类的唯一实例。

具体实现

饿汉式

饿汉式的实现方式就是,在类装载的期间,将类的实例初始化好,然后通过静态方法拿到实例化的对象。

对应的 Java 代码片段如下:

public class Singleton {
    // 静态实例化
    private static final Singleton instance = new Singleton();

    // 构造器私有化
    private Singleton() {}

    // 公有静态方法,返回实例对象
    public static Singleton getInstance() {
        return instance;
    }
}

除了通过使用静态常量初始化实例的方式以外,还可以通过静态代码块的方式实现饿汉式单例模式。

对应的 Java 代码片段如下:

public class Singleton {
    // 静态变量
    private static final Singleton instance;

    // 构造器私有化
    private Singleton() {}

    // 静态代码块
    static {
        instance = new Singleton();
    }

    // 公有静态方法,返回实例对象
    public static Singleton getInstance() {
        return instance;
    }
}

饿汉式的优点是,在类装载的时候就完成了实例化,避免了线程同步问题。

但是,这样的实现方式不支持延迟加载实例,如果从始至终未使用过这个实例,就会造成内存浪费。

并且,饿汉式在一些场景中无法使用:比如单例类实例的创建是依赖参数或者配置文件的,在通过 getInstance() 方法获取实例对象之前需要调用某个方法设置参数给对象实例,则这种方式将无法使用。

懒汉式

懒汉式相对于饿汉式的优势是支持延迟加载,可以在需要使用实例的时候才进行初始化。

对应的 Java 代码片段如下:

public class Singleton {
    // 静态变量
    private static Singleton instance;

    // 构造器私有化
    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            // 实例不存在时初始化
            instance = new Singleton();
        }
        return instance;
    }
}

上述的实现方式是线程不安全的,如果有两个线程同时进入到 getInstance() 方法,并且正好都通过了判断语句,这时便会产生多个实例。通常不建议在生产环境中使用线程不安全的懒汉式创建单例类。

为了做到线程安全,可以给 getInstance() 方法加一把锁。

对应的 Java 代码片段如下:

public class Singleton {
    // 静态变量
    private static Singleton instance;

    // 构造器私有化
    private Singleton() {}

    // 使用 synchronized 对方法进行加锁
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            // 实例不存在时初始化
            instance = new Singleton();
        }
        return instance;
    }
}

上述在 getInstance() 方法加锁的方式解决了线程不安全的问题,但是,由于加锁的粒度较大,实际的效率非常低。

如果这个单例类偶尔会被使用到,那这种实现方式还可以接受。但是,如果频繁地用到,那频繁加锁、释放锁则会出现并发度低的问题,造成性能瓶颈。

因此,也不建议在生产环境中使用线程安全的懒汉式创建单例类。

双重检测

饿汉式和懒汉式的实现方式都有一定的限制,而双重检测的实现方式是一种既支持延迟加载、又支持高并发的单例实现方式。

对应的 Java 代码片段如下:

public class Singleton {
    // 静态变量
    private static Singleton instance;

    // 构造器私有化
    private Singleton() {}

    public static Singleton getInstance() {
        // 一次检测
        if (instance == null) {
            synchronized (Singleton.class) {
                // 二次检测
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

当有两个线程同时进入到 getInstance() 方法时,虽然会出现都通过第一次检查的判断语句,但是只会有一个线程获得锁并实例化对象,即使后续再有线程进入到同步代码块中,也会被第二次检查的判断语句挡在外面。

双重检测方式在多线程开发中常使用到,其优点是线程安全、支持延迟加载、效率较高。在实际开发中比较推荐使用这种方式实现单例模式。

静态内部类

静态内部类是一种比双重检测更加简单的实现方式。它有点类似饿汉式,但又能做到延迟加载。

对应的 Java 代码片段如下:

public class Singleton {
    // 静态内部类
    private static class SingletonHolder {
        // 初始化实例
        private static final Singleton instance = new Singleton();
    }

    // 构造器私有化
    private Singleton() {}

    public static Singleton getInstance() {
        // 返回内部类的静态实例
        return SingletonHolder.instance;
    }
}

这种方式采用类装载机制来保证初始化实例时只有一个线程。

静态内部类方式在单例类被加载的时候并不会立即实例化,而是在调用 getInstance() 方法的时候,才会装载 SingletonHolder 类,从而实现单例类的实例化。

类的静态属性只会在第一次加载类的时候初始化,实例的唯一性、创建过程的线程安全性,都由 JVM 来保证。

所以,这种实现方法既保证了线程安全,又能做到延迟加载,效率也比较高,也是一种推荐使用的实现方式。

枚举

基于枚举类型的单例实现,是最简单的实现方式。

对应的 Java 代码片段如下:

public enum Singleton {
    // 实例属性
    INSTANCE;

    public void doSomething() {
        // 通过以下方式调用此方法
        // Singleton.INSTANCE.doSomething();
    }
}

这种方式是通过 Java 枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性,还能防止反序列化重新创建新的对象。

这种方式是 Effective Java中文版(第3版) 作者提倡的方式,推荐在生产环境中使用。

深度理解

单例模式唯一性的范围

单例类只允许创建唯一对象(或实例),这里对象的唯一性范围指的是进程内只允许创建一个对象。

进程之间是不共享地址空间的,如果在一个进程中创建另一个进程,操作系统会给新进程分配新的地址空间,并且将老进程地址空间的所有内容重新拷贝一份到新进程的地址空间中,这些内容包括代码、数据。

所以,单例类在老进程中存在且只能存在一个对象,在新进程中也会存在且只能存在一个对象。而且,这两个对象不是同一个对象。

实现线程唯一的单例

“进程唯一”指的是进程内唯一,进程间不唯一。类比得知,“线程唯一”指的是线程内唯一,线程间不唯一。

其实,“进程唯一”的单例在同一个进程中的线程间唯一,若要做到“线程唯一”,主要是做到线程间保持不唯一。

实现线程唯一单例的代码很简单,可以通过一个键值对做关联存储,其中 key 是线程 ID,value 是对象。

对应的 Java 代码片段如下:

import java.util.concurrent.ConcurrentHashMap;

public class Singleton {
    // 保证线程唯一的键值对
    private static final ConcurrentHashMap<Long, Singleton> instanceMap = new ConcurrentHashMap<>();

    // 构造器私有化
    private Singleton() {}

    public static Singleton getInstance() {
        Long currentThreadId = Thread.currentThread().getId();
        instanceMap.putIfAbsent(currentThreadId, new Singleton());
        return instanceMap.get(currentThreadId);
    }
}

实现集群唯一的单例

这里的集群表示进程集群,类比可知,“集群唯一”相当于进程间也唯一,即在不同的进程间共享同一个对象,不创建同一个类的多个对象。

实现集群唯一单例需要依赖到外部共享存储区:将单例对象序列化并存储到外部共享存储区,在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使用,使用完成之后还需要再存储回外部共享存储区。

为了保证任何时刻在集群中都只有一份对象存在,一个进程在获取到对象之后,需要对对象加锁,避免其他进程再将其获取。

在进程使用完这个对象之后,还需要显式地将对象从内存中删除,并且释放对象的锁。

实现一个多例模式

“多例”指的是,一个类可以创建多个对象,但是个数是有限制的,同无限个有一些区别。

多例模式的实现也比较简单,通过一个键值对存储索引和对象之间的对应关系,并且需要控制对象的个数。

对应的 Java 代码片段如下:

import java.util.Map;
import java.util.HashMap;
import java.util.Random;

public class Multipleton {
    // 限制实例数量
    private static final int COUNT = 3;

    // 存储对应关系的键值对
    private static final Map<Integer, Multipleton> instanceMap = new HashMap<>();

    // 饿汉式实现
    static {
        instanceMap.put(0, new Multipleton());
        instanceMap.put(1, new Multipleton());
        instanceMap.put(2, new Multipleton());
    }

    // 构造器私有化
    private Multipleton() {}

    // 公有静态方法,返回对应索引的实例对象
    public static Multipleton getInstance(Integer index) {
        return instanceMap.get(index);
    }

    // 公有静态方法,返回随机索引的实例对象
    public static Multipleton getRandomInstance() {
        Random random = new Random();
        Integer index = random.nextInt(COUNT);
        return instanceMap.get(index);
    }
}

总结

优点

单例模式的主要优点如下:

  • 提供了对唯一实例的受控访问,封装性非常好
  • 系统内存中只存在一个对象,可以节省系统资源
  • 基于单例模式,可扩展实现多例类,既节省系统资源,又解决了由于单例模式共享过多有损性能的问题

缺点

单例模式的主要缺点如下:

  • 单例模式对面向对象特性的支持不友好,违背了基于接口而非实现的设计原则
  • 单例模式对代码的扩展性不友好,如要扩展则会导致改动较大
  • 常规的单例模式不支持有参数的构造函数,只能通过其他方式改动单例类中的成员变量
  • 对于有 GC 的编程语言,如果长时间不使用实例化的对象,则单例对象有可能会被销毁

适用场景

单例模式的适用场景如下:

  • 单例模式主要针对需要频繁地创建和销毁的对象,可以理解成创建对象时耗时过多或耗费资源较大但又经常用到的对象。如工具类对象、频繁访问的数据库或文件对象
  • 从业务概念上看,有些数据在系统中只应该保存一份,就比较适合设计成单例类。比如,系统的配置信息类
  • 可以使用单例解决资源访问冲突的问题,单例模式可以只提供一个公共访问点

源码

在 JDK 中,java.lang.Runtime 是经典的单例模式,其用于与 Java 运行时环境进行交互。

Runtime 类是一个典型的饿汉式单例模式实现,如下是其的一些实现逻辑:

public class Runtime {
    // 静态实例化
    private static final Runtime currentRuntime = new Runtime();

    private static Version version;

    // 静态方法获取静态实例
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    // 构造器私有化
    private Runtime() {}

有关初识设计模式 - 单例模式的更多相关文章

  1. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

    我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

  2. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  3. ruby - 如何在续集中重新加载表模式? - 2

    鉴于我有以下迁移:Sequel.migrationdoupdoalter_table:usersdoadd_column:is_admin,:default=>falseend#SequelrunsaDESCRIBEtablestatement,whenthemodelisloaded.#Atthispoint,itdoesnotknowthatusershaveais_adminflag.#Soitfails.@user=User.find(:email=>"admin@fancy-startup.example")@user.is_admin=true@user.save!ende

  4. ruby-on-rails - 使用 rails 4 设计而不更新用户 - 2

    我将应用程序升级到Rails4,一切正常。我可以登录并转到我的编辑页面。也更新了观点。使用标准View时,用户会更新。但是当我添加例如字段:name时,它​​不会在表单中更新。使用devise3.1.1和gem'protected_attributes'我需要在设备或数据库上运行某种更新命令吗?我也搜索过这个地方,找到了许多不同的解决方案,但没有一个会更新我的用户字段。我没有添加任何自定义字段。 最佳答案 如果您想允许额外的参数,您可以在ApplicationController中使用beforefilter,因为Rails4将参数

  5. ruby - 是否有用于序列化和反序列化各种格式的对象层次结构的模式? - 2

    给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最

  6. LC滤波器设计学习笔记(一)滤波电路入门 - 2

    目录前言滤波电路科普主要分类实际情况单位的概念常用评价参数函数型滤波器简单分析滤波电路构成低通滤波器RC低通滤波器RL低通滤波器高通滤波器RC高通滤波器RL高通滤波器部分摘自《LC滤波器设计与制作》,侵权删。前言最近需要学习放大电路和滤波电路,但是由于只在之前做音乐频谱分析仪的时候简单了解过一点点运放,所以也是相当从零开始学习了。滤波电路科普主要分类滤波器:主要是从不同频率的成分中提取出特定频率的信号。有源滤波器:由RC元件与运算放大器组成的滤波器。可滤除某一次或多次谐波,最普通易于采用的无源滤波器结构是将电感与电容串联,可对主要次谐波(3、5、7)构成低阻抗旁路。无源滤波器:无源滤波器,又称

  7. 计算机毕业设计ssm+vue基本微信小程序的小学生兴趣延时班预约小程序 - 2

    项目介绍随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱小学生兴趣延时班预约小程序的设计与开发被用户普遍使用,为方便用户能够可以随时进行小学生兴趣延时班预约小程序的设计与开发的数据信息管理,特开发了小程序的设计与开发的管理系统。小学生兴趣延时班预约小程序的设计与开发的开发利用现有的成熟技术参考,以源代码为模板,分析功能调整与小学生兴趣延时班预约小程序的设计与开发的实际需求相结合,讨论了小学生兴趣延时班预约小程序的设计与开发的使用。开发环境开发说明:前端使用微信微信小程序开发工具:后端使用ssm:VU

  8. ruby-on-rails - environment.rb 中设置的常量在开发模式中消失 - 2

    了解Rails缓存如何工作的人可以真正帮助我。这是嵌套在Rails::Initializer.runblock中的代码:config.after_initializedoSomeClass.const_set'SOME_CONST','SOME_VAL'end现在,如果我运行script/server并发出请求,一切都很好。然而,在我的Rails应用程序的第二个请求中,一切都因单元化常量错误而变得糟糕。在生产模式下,我可以成功发出第二个请求,这意味着常量仍然存在。我已通过将以上内容更改为以下内容来解决问题:config.after_initializedorequire'some_cl

  9. ruby-on-rails - 设计注册确认 - 2

    我在我的项目中有一个用户和一个管理员角色。我使用Devise创建了身份验证。在我的管理员角色中,我没有任何确认。在我的用户模型中,我有以下内容:devise:database_authenticatable,:confirmable,:recoverable,:rememberable,:trackable,:validatable,:timeoutable,:registerable#Setupaccessible(orprotected)attributesforyourmodelattr_accessible:email,:username,:prename,:surname,:

  10. Ruby:标准递归模式 - 2

    我经常迷上ruby​​的一件事是递归模式。例如,假设我有一个数组,它可能包含无限深度的数组作为元素。所以,例如:my_array=[1,[2,3,[4,5,[6,7]]]]我想创建一个方法,可以将数组展平为[1,2,3,4,5,6,7]。我知道.flatten可以完成这项工作,但这个问题是作为我经常遇到的递归问题的一个例子-因此我试图找到一个更可重用的解决方案。简而言之-我猜这种事情有一个标准模式,但我想不出任何特别优雅的东西。任何想法表示赞赏 最佳答案 递归是一种方法,它不依赖于语言。您在编写算法时要考虑两种情况:再次调用函数的情

随机推荐