Loading [MathJax]/jax/input/TeX/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >单例模式(下)

单例模式(下)

作者头像
WindCoder
发布于 2020-04-28 02:31:50
发布于 2020-04-28 02:31:50
1K00
代码可运行
举报
文章被收录于专栏:WindCoderWindCoder
运行总次数:0
代码可运行

在上篇 《单例模式(上)》一文中介绍了单例定义、使用场景、实现方式以及不足,本篇继续整理针对不足的解决方案以及唯一性的相关讨论与实现等。

5. 单例代替方案

5.1 静态方法

为了保证全局唯一,除了使用单例, 可以用静态方法来实现。但静态方法比单例更加不灵活,比如,它无法支持延迟加载。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class IdGenerator {
    private AtomicLong id = new AtomicLong(0);
    // 静态方法实现
    public static long getId() {
        return id.incrementAndGet();
    }

}
// 使用示例
long id = IdGenerator.getId();

5.2 依赖注入

通过依赖注入,将单例生成的对象,作为参数传递给函数(也可以通过构造函数传递给类的成员变量),可以解决单例隐藏类之间依赖关系的问题。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//  旧使用方式
public  demoFuntion() {
   //...  
   long id = IdGenerator.getInstance().getId(); 
    //...
}

// 2. 新的使用方式:依赖注入
public demofunction(IdGenerator idGenerator) {  
    //... 
    long id = idGenerator.getId();
    //...
}
// 外部调用demofunction()的时候,传入idGeneratorIdGenerator
idGenerator = IdGenerator.getInsance();
demofunction(idGenerator);

5.3 通过其他方式保证

从根源上出发,类对象的全局唯一性可以通过多种不同的方式来保证:

  • 可以通过单例模式来强制保证。
  • 可以通过工厂模式、IOC 容器(比如 Spring IOC 容器)来保证。
  • 可以通过程序员自己来保证(自己在编写代码的时候自己保证不要创建两个类对象)。

6. 单例模式中的唯一性

6.1 进程唯一

单例模式创建的对象是进程唯一的

  • 使用命令行或者双击运行可执行文件的时候,操作系统会启动一个进程,将这个执行文件从磁盘加载到自己的进程地址空间(可以理解操作系统为进程分配的内存存储区,用来存储代码和数据)。
  • 接着,进程就一条一条地执行可执行文件中包含的代码。比如,当进程读到代码中的 User user = new User(); 这条语句的时候,它就在自己的地址空间中创建一个 user 临时变量和一个 User 对象。
  • 进程之间是不共享地址空间的,如果在一个进程中创建另外一个进程(比如,代码中有一个 fork() 语句,进程执行到这条语句的时候会创建一个新的进程),操作系统会给新进程分配新的地址空间,并且将老进程地址空间的所有内容,重新拷贝一份到新进程的地址空间中,这些内容包括代码、数据(比如 user 临时变量、User 对象)。
  • 所以,单例类在老进程中存在且只能存在一个对象,在新进程中也会存在且只能存在一个对象。而且,这两个对象并不是同一个对象,即,单例类中对象的唯一性的作用范围是进程内的,在进程间是不唯一的。

实际上,对于 Java 语言来说,单例类对象的唯一性的作用范围并非进程,而是类加载器(Class Loader),原因在于Java的双亲委派模型

6.1.1 进程唯一与线程唯一
  • “进程唯一”指的是进程内唯一,进程间不唯一。
  • “线程唯一”指的是线程内唯一,线程间可以不唯一。
  • 实际上,“进程唯一”还代表了线程内、线程间都唯一,这也是“进程唯一”和“线程唯一”的区别之处。

6.2 线程唯一的单例

在代码中,通过一个 HashMap 来存储对象,其中 key 是线程 ID,value 是对象。这样就可以做到,不同的线程对应不同的对象,同一个线程只能对应一个对象。

实际上,Java 语言本身提供了 ThreadLocal 工具类,可以更加轻松地实现线程唯一单例。ThreadLocal 底层实现原理也是基于HashMap的方式。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class IdGenerator6 {
    private AtomicLong id = new AtomicLong(0);
    private static final ConcurrentHashMap<Long, IdGenerator6> instances
            = new ConcurrentHashMap<>();

    private IdGenerator6() {}
    public static IdGenerator6 getInstance() {
        Long currentThreadId = Thread.currentThread().getId();
        instances.putIfAbsent(currentThreadId, new IdGenerator6());
        return instances.get(currentThreadId);
    }
    public long getId() {
        return id.incrementAndGet();
    }
}

6.3 集群环境下的单例

集群相当于多个进程构成的一个集合,“集群唯一”就相当于是进程内唯一、进程间也唯一。即,不同的进程间共享同一个对象,不能创建同一个类的多个对象。

  • 需要把这个单例对象序列化并存储到外部共享存储区(比如文件)。
  • 进程在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使用,使用完成之后还需要再存储回外部共享存储区。
  • 为了保证任何时刻,在进程间都只有一份对象存在,一个进程在获取到对象之后,需要对对象加锁,避免其他进程再将其获取。
  • 在进程使用完这个对象之后,还需要显式地将对象从内存中删除,并且释放对对象的加锁。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 集群唯一
 *
 * [设计模式之美](https://windcoder.com/go/JKDesignPattern)中仅提供了伪代码,这里根据伪代码尝试做了一个基于Redis的简单实现,代码本身没做测试,请勿直接粘贴复制到生产环境。
 *
 * Redisson 操作redis实现单例对象的序列化存储与反序列化读取。
 * Redisson 实现Redis分布式锁,用于取出前的加锁,与操作后的释放锁。
 * 
 *  关于为何配置自定义序列化与反序列化的问题,可以参考[redisson如何序列化](https://www.php.cn/redis/436670.html)。
 */
public class IdGenerator7 implements Serializable {
    private AtomicLong id = new AtomicLong(0);
    private static IdGenerator7 instance;


    static RedissonClient redisson = getRedissonClient();
    static RLock  lock = redisson.getLock("anyLock");


    private IdGenerator7() {}

    public synchronized static IdGenerator7 getInstance()   {
        if (instance == null) {
            lock.lock();
            RBucket<IdGenerator7> bucket = redisson.getBucket("IdGenerator");
            //如果key存在,就设置key的值为新值value
            //如果key不存在,就设置key的值为value
            // https://www.pianshen.com/article/5465125503/
            bucket.set(new IdGenerator7());
            instance = bucket.get();
        }
        return instance;
    }
    public synchronized void freeInstance() {
        RBucket<IdGenerator7> bucket = redisson.getBucket("IdGenerator");
        bucket.set(new IdGenerator7());
        instance = null; //释放对象
        lock.unlock();

    }
    public long getId() {
        return id.incrementAndGet();
    }

    /**
     * 读取Redisson关于Redis的相关配置,生成Redisson对象。
     * 
     * 这里仅为了方便实现与展示集群唯一的示例。
     * @return
     */
    private static RedissonClient getRedissonClient() {
        Config config = null;
        try {
            config = Config.fromYAML(new File("classpath:redisson.yaml"));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return Redisson.create(config);
    }
}

6.4 多例模式

核心是通过 **一个 Map ** 来存储对象类型和对象之间的对应关系,来控制对象的个数。

6.4.1 一个类创建有限多个对象

“单例”指的是,一个类只能创建一个对象。对应地,“多例”指的就是,一个类可以创建多个对象,但是个数是有限制的,比如只能创建 3 个对象。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class IdGenerator8 {
    private long serverNo;
    private String serverAddress;
    private static final int SERVER_COUNT = 3;

    private static final ConcurrentHashMap<Long, IdGenerator8> instances
            = new ConcurrentHashMap<>();

    static {
        instances.put(1L, new IdGenerator8(1L, "192.134.22.138:8080"));
        instances.put(2L, new IdGenerator8(2L, "192.134.22.139:8080"));
        instances.put(3L, new IdGenerator8(3L, "192.134.22.140:8080"));
    }

    private IdGenerator8(long serverNo, String address) {
        this.serverNo = serverNo;
        this.serverAddress = address;
    }

    public IdGenerator8 getInstance(long serverNo) {
        return instances.get(serverNo);
    }
    public IdGenerator8 getRandomInstance() {
        Random r = new Random();
        int no = r.nextInt(SERVER_COUNT)+1;
        return instances.get(no);
    }

}
6.4.2 同类型创建一个,不同类型创建多个对象

“多例”还有一种理解方式:同一类型的只能创建一个对象,不同类型的可以创建多个对象

这种多例模式的理解方式有点类似工厂模式。它跟工厂模式的不同之处是,多例模式创建的对象都是同一个类的对象,而工厂模式创建的是不同子类的对象。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class IdGenerator9 {
    private static final ConcurrentHashMap<String, IdGenerator9> instances
            = new ConcurrentHashMap<>();
    private IdGenerator9() {}

    public static IdGenerator9 getInstance(String name) {
        instances.putIfAbsent(name, new IdGenerator9());
        return instances.get(name);
    }

    public static void main(String[] args) {
        IdGenerator9 i1 =  IdGenerator9.getInstance("User.class");
        IdGenerator9 i2 =  IdGenerator9.getInstance("User.class");
        IdGenerator9 i3 =  IdGenerator9.getInstance("Order.class");
    }
}

7. 附录

7.1 是否应该延迟加载

7.1.1 应该延迟

如果实例占用资源多(比如占用内存多)或初始化耗时长(比如需要加载各种配置文件),提前初始化实例是一种浪费资源的行为。

7.1.2 不应该延迟

如果初始化耗时长,那我们最好不要等到真正要用它的时候,才去执行这个耗时长的初始化过程,这会影响到系统的性能(比如,在响应客户端接口请求的时候,做这个初始化操作,会导致此请求的响应时间变长,甚至超时)。采用饿汉式实现方式,将耗时的初始化操作,提前到程序启动的时候完成,这样就能避免在程序运行的时候,再去初始化导致的性能问题。

如果实例占用资源多,按照 fail-fast 的设计原则(有问题及早暴露),那我们也希望在程序启动时就将这个实例初始化好。如果资源不够,就会在程序启动的时候触发报错(比如 Java 中的 PermGen Space OOM),我们可以立即去修复。这样也能避免在程序运行一段时间后,突然因为初始化这个实例占用资源过多,导致系统崩溃,影响系统的可用性。

7.2 进程 (thread) 和线程 (process) 的区别

  • 根本区别:进程是资源分配的最小单位,线程是CPU调度的最小单位。
  • 地址空间:进程之间是独立的地址空间,同一进程的线程共享本进程的地址空间。
  • 执行过程:每个独立的进程都有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
  • 通信:进程之间的通信需要以通信的方式(IPC)进行,同一进程下的线程共享全局变量、静态变量等数据资源,从而线程之间的通信更方便。
  • 健壮:多进程程序更健壮,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉。
  • 效率:进程切换时,耗费资源较大,效率要差一些

7.2 参考资料

7.3 扩展

进程与线程的一个简单解释

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
创建型-Singleton
what 单例设计模式(Singleton Design Pattern)理解起来非常简单。一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。
acc8226
2022/05/17
4570
学习单例模式引发的思考
学习单例引发的思考 单例存在哪里问题? 1.单例对oop的支持不友好 OOP 四大特性: 封装 继承 多态 抽象 而单例这种设计模式对于其中的抽象 继承 多态 都支持的不好 为什么这么说呢? 我们先来
Java宝典
2021/01/14
5560
设计模式 | 创建型 | 单例模式
一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。
被水淹没
2023/02/25
4660
设计模式 | 创建型 | 单例模式
单例模式(上)
一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式(Singleton Design Pattern)。
WindCoder
2020/04/28
4350
深入理解几种单例模式的实现方式
饿汉式的单例实现方式就是说在类加载的时候就已经创建并初始化好了,所以实例的创建过程是线程安全的
测试轩
2020/04/28
2970
深入理解几种单例模式的实现方式
Java 史上最全单例模式实现大全
作为一名资深的 Java 架构师,我对单例模式可以说是了如指掌。这种设计模式无疑是 Java 开发中最常用也最重要的模式之一,它可以帮助我们控制对象的创建,保证全局唯一性,并且能够避免不必要的资源消耗。但是,你知道单例模式究竟有多少种实现方式吗?相信很多读者对此都不太清楚。
疯狂的KK
2024/05/10
2980
Java 史上最全单例模式实现大全
单例模式的常用实现方式
单例模式属于最常用的设计模式,Java中有很多实现单例模式的方式,各有其优缺点 实现方式对比 单例实现方式 线程安全 延迟加载 性能 复杂度 饿汉式 安全 不支持 好 简单 懒汉式 安全 支持 差 一般 双重检测 安全 支持 好 复杂 静态内部类 安全 支持 好 简单 枚举 安全 不支持 好 最简单 实现方式示例 实现一个ID生成器的单例 饿汉式: 不支持延迟加载 public class IdGenerator { private AtomicLong id = new AtomicLong(0
十毛
2021/12/03
3250
01.单例模式设计思想
本文详细介绍了单例模式的设计思想及其应用。首先阐述了单例模式的基本概念、特点与定义,并探讨其适用场景与常见问题。接着深入分析了为何使用单例模式,包括处理资源访问冲突和表示全局唯一类。随后详细讲解了几种常见的单例实现方式,如饿汉式、懒汉式、双重检查锁定、静态内部类及枚举等,对比了各自优缺点。最后讨论了单例模式可能带来的问题,如对OOP不友好、隐藏依赖关系、扩展性差等,并提出了一些替代解决方案。文章内容丰富,适合希望深入了解单例模式及其应用的读者。
杨充
2024/10/14
1750
01.创建型:单例设计模式
创建型:单例设计模式1目录介绍01.单例模式介绍02.单例模式定义03.单例使用场景04.思考几个问题05.为什么要使用单例06.处理资源访问冲突07.表示全局唯一类01.单例模式介绍单例模式是应用最广的模式也是最先知道的一种设计模式,在深入了解单例模式之前,每当遇到如:getInstance()这样的创建实例的代码时,我都会把它当做一种单例模式的实现。单例模式特点构造函数不对外开放,一般为private通过一个静态方法或者枚举返回单例类对象确保单例类的对象有且只有一个,尤其是在多线程的环境下确保单例类对象
杨充
2022/09/07
3820
我是如何用单例模式征服面试官的?
单例模式无论在我们面试,还是日常工作中,都会面对的问题。但很多单例模式的细节,值得我们深入探索一下。
苏三说技术
2021/10/19
4380
java单例模式
单例模式是一种常见的设计模式,其主要目的是确保在整个应用程序中只存在一个特定类型的对象。在Java中,单例模式是一种非常重要的设计模式,因为Java是一种面向对象的语言,它的许多库和框架都使用了单例模式。在本文中,我们将详细介绍Java单例模式的实现方式、使用场景、优点和缺点。
堕落飞鸟
2023/04/03
6010
单例模式和多例模式(懒汉式和饿汉式)
所谓单例就是所有的请求都用一个对象来处理,比如我们常用的service和dao层的对象通常都是单例的,而多例则指每个请求用一个新的对象来处理,比如action;
ha_lydms
2023/08/09
3750
单例模式 创建型 设计模式(六)
可以借助于全局变量,但是类就在那里,你不能防止实例化多个对象,可能一不小心谁就创建了一个对象
noteless
2018/11/21
4710
【设计模式】单例设计模式
单例模式是一种设计模式,它确保一个类只能创建一个实例,并提供一种全局访问这个实例的方式。在Java中,单例模式可以通过多种方式来实现,其中最常见的是使用私有构造函数和静态方法实现
陶然同学
2023/10/14
3560
设计模式系列:经典的单例模式
在小灰的知识星球里,有一位小伙伴分享了单例模式的知识,写得非常清晰详尽。小灰把这篇干货文章分享到公众号上,希望能够帮助到大家。
小灰
2023/09/02
1940
设计模式系列:经典的单例模式
单例模式各版本的原理与实践
单例模式是应用最广的模式之一,也是23种设计模式中最基本的一个。本文旨在总结通过Java实现单例模式的各个版本的优缺点及适用场景,详细分析如何实现线程安全的单例模式,并探讨单例模式的一些扩展。
用户4283147
2022/10/27
2750
单例模式各版本的原理与实践
设计模式(一)——单例模式
在实际生产的项目中我们一般很少自己使用单例模式,但是在有过间接的使用过他,例如spring的bean单例。 定义:确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。构造器私有化,不能被new出来。
小森啦啦啦
2019/07/14
3920
【设计模式系列(二)】彻底搞懂单例模式
概念:单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点
你好戴先生
2020/09/02
5320
【Java设计模式实战系列】好的单例模式是怎样的?
如何保证一个类只有一个实例,且该实例易于访问? 定义一个全局变量可以确保对象随时都可以被访问,但无法避免实例化多个对象。
JavaEdge
2021/10/18
6690
单例-无法绕过的设计模式
一些常用的工具类,由于其使用频率较高,如果每次需要使用时都新建一个对象,不仅会占用大量内存,还会导致系统负载增加,影响应用程序的性能。使用单例模式,可以在应用程序启动时就创建一个实例,直到应用程序结束时才销毁该实例,这样就能保证该工具类在整个应用程序中只有一个实例对象被使用,从而提高程序的效率和性能。
Coder昊白
2023/11/23
3060
相关推荐
创建型-Singleton
更多 >
LV.0
这个人很懒,什么都没有留下~
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验