设计模式学习笔记 - 设计模式与范式 - 创建型:3.单例模式(下):线程、进程、集群环境讲下的单例

概述

上一节课《设计模式与范式 - 创建型:2.单例模式(中):为什么不推荐使用单例模式?又有何替代方案?》针对单例模式,讲解了单例的应用场景、几种场景的代码实现和存在问题,并粗略给出了替换单例模式的方式,比如工厂模式、IOC 容器。今天,再进一步延伸下,讨论下下面这几个问题:

  • 如何理解单例模式的唯一性?
  • 如何实现线程唯一的单例?
  • 如何实现集群环境的单例?
  • 如何实现一个多例模式?

如何理解单例模式的唯一性?

单例的定义:一个类只允许创建一个对象,那这个类就是一个单例类,这种设计模式叫做单例设计模式,简称单例模式。

定义中提到,"一个类支援许创建唯一一个对象"。那对象的唯一性的作用范围是什么?是指线程内只允许创建一个对象,还是指进程内只允许创建一个对象? 答案是后者,也就说,单例模式创建的对象是进程唯一的。在进程间是不唯一的。

如何实现线程唯一的单例?

刚刚讲了单例类对象是进程唯一的,一个进程只能有一个单例对象。那如何实现一个线程唯一的单例呢?

先看一下,什么是线程唯一的单例,以及 "线程唯一" 和 "进程唯一" 的区别。

"进程唯一" 指的是进程内唯一,进程间不唯一。类比一下, "线程唯一" 指的是线程唯一,线程间不唯一。实际上,"进程唯一" 还代表了线程内、线程间都唯一,这也是 "进程唯一" 和 "线程唯一" 的区别之处。这段话有点像绕口令,我举个例子解释下。

假如 IdGenertaor 是一个线程唯一的单例类。在线程 A 内,可以创建一个单例对象 a。因为线程内唯一,在线程 A 内就不能再创建新的 IdGenertaor 对象了,而线程间可以不唯一,所以,在另一个线程 B 内,还可以重新创建一个新的单例对象 b。

尽管概念理解起来比较复杂,但线程唯一单例的代码实现很简单,如下所示。在代码中,我们通过一个 HashMap 来存储对象,其中 key 是线程 ID,value 是对象。这样就可以做到,不同的线程对应不同的对象,同一个线程只能对应一个对象。实际上,Java 本身提供了 ThreadLocal 工具类,可以更加轻松地实现线程唯一单例。不过,ThreadLocal 底层实现原理也是基于下面代码中所示的 HashMap。

java 复制代码
public class IdGenerator {
    private static AtomicLong id = new AtomicLong(0);

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

    private IdGenerator() {}

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

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

如何实现集群环境的单例?

刚刚讲了 "进程唯一" 的单例和 "线程唯一" 的单例,现在再来看下, "集群唯一" 的单例。

首先,先解释下,什么是 "集群唯一" 的单例。

还是将它和 "进程唯一" "线程唯一" 做个对比。

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

经典的单例模式是进程内唯一的,那如何实现一个进程间也唯一的单例呢?如果严格按照不同的进程间共享同一个对象来实现,那集群唯一的单例实现起来就有点难度了。

具体来书哦,需要把这个单例对象序列化并存储到外部共享存储区(比如文件)。进程在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使用,使用完成之后还需要再存储回外部共享存储区。

为了保证任何时刻,在进程内都只有一份对象存在,一个进程在获取到对象之后,需要对对象加锁,避免其他进程再将其获取。在进程使用完这个对象后,还需要显式地将对象从内存中删除,并是否对象的加锁。

按照这个思路,使用伪代码实现了一下这个过程,具体如下所示:

java 复制代码
public class IdGenerator {
    private static AtomicLong id = new AtomicLong(0);
    private static IdGenerator instance;
    private static SharedObjectStorage storage = new FileSharedObjectStorage(/*入参省略*/);
    private static DistributedLock lock = new DistributedLock();

    private IdGenerator() {}

    public synchronized static IdGenerator getInstance() {
        if (instance == null) {
            lock.lock();
            instance = storage.load(IdGenerator.class);
        }
        return instance;
    }

    public synchronized void freeInstance() {
        storage.save(this, IdGenerator.class);
        instance = null;
        lock.unlock();
    }

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

// IdGenerator使用举例
IdGenerator idGenerator = IdGenerator.getInstance();
long id = idGenerator.getId();
idGenerator.freeInstance();

如何实现一个多例模式?

跟单例模式概念相对应的还有一个多例模式。

"单例" 是指一个类只能创建一个对象。对应的,"多例" 指的就是,一个类可以创建多个对象,但是个数是有限制的,比如只能创建 3 个对象。就比如下面这样子:

java 复制代码
public class BackendServer {
    private long serverNo;
    private String address;

    private static final int SERVER_COUNT = 3;
    private static final Map<Long, BackendServer> serverInstances = new HashMap<>();

    static {
        serverInstances.put(1L, new BackendServer(1L, "192.168.1.101:8080"));
        serverInstances.put(2L, new BackendServer(2L, "192.168.1.102:8080"));
        serverInstances.put(2L, new BackendServer(2L, "192.168.1.102:8080"));
    }

    private BackendServer(long serverNo, String address) {
        this.serverNo = serverNo;
        this.address = address;
    }
    
    public static BackendServer getInstance(long serverNo) {
        return serverInstances.get(serverNo);
    }
    
    public static BackendServer getRandomInstance() {
        Random r = new Random();
        int no = r.nextInt(SERVER_COUNT) + 1;
        return serverInstances.get(no);
    }
}

实际上,对于多例模式,还有一种理解方式:同一类型的只能创建一个对象,不同类型的可以创建多个对象。

这里的 "类型" 如何理解?

还是通过一个例子来解释下,具体代码如下所示。在代码中, logger name 就是刚刚所说的 "类型",同一个 logger name 获取到的对象实例是相同的,不同的 logger name 获取到的对象实例是不同的。

java 复制代码
public class Logger {
    private static final Map<String, Logger> instances = new ConcurrentHashMap<>();
    
    private Logger() {}
    
    private static Logger getInstance(String loggerName) {
        instances.putIfAbsent(loggerName, new Logger());
        return instances.get(loggerName);
    }
    
    public void log() {
        //...
    }
}

// l1==l2, l1!=l3
Logger l1 = Logger.getInstance("User.class");
Logger l2 = Logger.getInstance("User.class");
Logger l3 = Logger.getInstance("Order.class");

这种多例模式的理解方式有点类似工厂模式。它跟工厂模式的不同之处是,多例模式创建的对象都是同一个类的对象,而工厂模式创建的是不同子类对象。实际上,它还有点类似享元模式。此外,实际上,枚举类型也相当于多例模式,一个类型只能对应一个对象,一个类可以创建多个对象。

回顾

1.如何理解单例模式的唯一性

单例类中对象的唯一性的作用范围是 "进程唯一" 的。"进程唯一" 指的是进程内唯一,进程间不唯一;"线程唯一" 指的是线程内唯一,线程间不唯一。实际上,"进程唯一" 就意味着线程内、线程间都唯一,这也是 "进程唯一" 和 "线程唯一" 的区别之处。 "集群唯一" 指的是进程内唯一,进程间也唯一;

2.如何实现线程唯一的单例

通过一个 HashMap 来存储对象,其中 key 是线程 ID,value 是对象。这样我们就可以做到,不同的线程对应不同的对象,同一个线程只能对应一个对象。实际上,Java 语音本身提供了 ThreadLocal 并发工具类,可以更加轻松地实现线程唯一单例。

3.如何实现集群环境下的单例

需要把这个单例对象序列化并存储到外部共享存储区(如文件)。进程在使用这个单例对象的时候,需要先从外部共享存储区将它读取到内存,并反序列化成对象,然后再使用,使用完成后,还需要再存储回外部共享存储区。为了保证任何时刻在进程间都只有一份对象存在,一个进程在获取到对象之后,需要对对象加锁,避免其他进程再将其获取。在进程使用这个对象之后,需要显式地将对象从内存中删除,并释放对对象的加锁。

4.如何实现一个多例模式

"单例" 指的是一个类只能创建一个对象。对应的,"多例" 指的就是一个类可以创建多个对象,但是个数是有限制的,比如只能创建 3 个对象。多例的实现也比较简单,通过一个 Map 来存储对象类型和对象之间的关系,来控制对象的个数。

相关推荐
请你打开电视看看4 小时前
创建者模式-单例模式
单例模式
何朴尧4 小时前
单例模式入门
单例模式
旺代4 小时前
C++设计模式(单例模式)
c++·单例模式·设计模式
虎哥和你一起学编程4 小时前
如何防止序列化破坏单例模式
单例模式
LightOfNight5 小时前
【设计模式】创建型模式之单例模式(饿汉式 懒汉式 Golang实现)
单例模式·设计模式·golang
哥谭居民000110 小时前
在接口实现时使用自定义对象的方法(非工具类,和单例模式)
单例模式
岳轩子1 天前
23种设计模式之单例模式
java·单例模式·设计模式
吃汉堡吃到饱2 天前
【创建型设计模式】单例模式
单例模式·设计模式
程序员与背包客_CoderZ2 天前
C++设计模式——Singleton单例模式
c语言·开发语言·c++·单例模式·设计模式
白茶等风121383 天前
Unity 设计模式-单例模式(Singleton)详解
单例模式·设计模式