• 设计模式之美 - 如何理解单例模式中的唯一性?


    目录

    单例模式的定义

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

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

    如何实现一个多例模式?

    第一种:一个类可以创建有限的多个对象

    第二种: 同一类型只能创建一个对象,不同类型可以创建多个对象

    Java 中的单例模式的唯一性


     

    单例模式的定义

    一个类只允许创建唯一一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫做单例设计模式,简称单例模式。

    这里提到的唯一性是针对进程而非线程,我们编写的代码通过编译、链接后组织在一起,构成了一个操作系统可以执行的文件,也就是我们平时所说的 “可执行文件” (比如Windows下的exe文件)。可执行文件实际上就是代码被翻译成操作系统可理解的一组指令。

    当使用命令行或者双击运行这个可执行文件时,操作系统会启动一个进程,将这个执行文件从磁盘加载到自己的进程地址空间(可以理解成操作系统为进程分配的内存存储区,用来存储代码和数据)。接着,进程就一条一条地执行可执行文件中包含的代码。比如,当进程读到代码中的 User user = new User(); 这条语句的时候,它就在自己的地址空间中创建一个user临时变量和一个User对象。

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

    所以,单例类在老进程中存在且只能存在一个对象,在新进程中也会存在且只能存在一个对象。而且,这两个对象并不是同一个对象,这也就是说,单例类中对象的唯一性的作用范围是进程内的,在进程间是不唯一的。


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

    先理解下面这段话

    线程唯一 != 进程唯一

    进程唯一 => 线程唯一 

    可以使用 ConcurrentHashMap 作为底层数据结构存储对象,其中 key 是线程Id,value是对象。

    1. public class IdGenerator {
    2. /** id生成器 */
    3. private AtomicLong id = new AtomicLong(1);
    4. /** 存储每个线程id生成器的map容器 */
    5. private static final ConcurrentHashMap instances
    6. = new ConcurrentHashMap<>();
    7. /** 私有构造器,不允许外部 new对象 */
    8. private IdGenerator() {
    9. }
    10. /** 获取当前线程的id生成器对象 */
    11. public static IdGenerator getInstance() {
    12. long threadId = Thread.currentThread().getId();
    13. instances.putIfAbsent(threadId, new IdGenerator());
    14. return instances.get(threadId);
    15. }
    16. /** 获取id */
    17. public long getId() {
    18. return id.getAndIncrement();
    19. }
    20. }

    这样就可以做到不同的线程对应不同的对象,同一个线程只能对应一个对象。实际上,Java语言本身提供了 ThreadLocal 工具类,可以更加轻松地实现线程唯一单例。


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

    经典的单例模式是进程内唯一的。所谓集群环境下的单例也就是进程间唯一,这里可以采用将单例对象序列化并存储到外部共享存储区(比如文件)。进程在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使用,使用完成之后还需要再存储回外部共享存储区。

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

    伪代码如下:

    1. public class DistributedIdGenerator {
    2. /** id生成 */
    3. private AtomicLong id = new AtomicLong(1);
    4. private static DistributedIdGenerator instance;
    5. /** 进程间共享对象外部存储 */
    6. @Resource
    7. private SharedObjectStorage storage;
    8. /** 分布式锁 */
    9. @Resource
    10. private DistributedLock lock;
    11. private DistributedIdGenerator() {
    12. }
    13. /** 获取进程间共享的DistributedIdGenerator对象 */
    14. public synchronized static DistributedIdGenerator getInstance() {
    15. if (instance == null) {
    16. lock.lock();
    17. instance = storage.load(DistributedIdGenerator.class);
    18. }
    19. return instance;
    20. }
    21. /** 当前进程使用完后需释放当前实例 */
    22. public synchronized void freeInstance() {
    23. storage.save(this, DistributedIdGenerator.class);
    24. instance = null;
    25. lock.unlock();
    26. }
    27. public long getId() {
    28. return id.getAndIncrement();
    29. }
    30. }

    如何实现一个多例模式?

    这里的多例模式可以理解成两种:

    • 一个类可以创建有限的多个对象
    • 同一类型只能创建一个对象,不同类型可以创建多个对象

    第一种:一个类可以创建有限的多个对象

    可以采取随机数或者是根据特征id进行分片。

    1. public class BackupServer {
    2. private long serverNo;
    3. private String serverAddress;
    4. public BackupServer(long serverNo, String serverAddress) {
    5. this.serverNo = serverNo;
    6. this.serverAddress = serverAddress;
    7. }
    8. /** 服务器数量 */
    9. private static final int SERVER_COUNT = 3;
    10. /** 存储服务器实例 */
    11. private static final Map SERVER_MAP = new HashMap<>();
    12. /** 初始化服务器实例 */
    13. static {
    14. for (long i = 1; i <= SERVER_COUNT; i++) {
    15. SERVER_MAP.put(i, new BackupServer(i, "192.168.22." + i +":8080"));
    16. }
    17. }
    18. public BackupServer getInstance(long serverNo) {
    19. return SERVER_MAP.get(serverNo);
    20. }
    21. /** 随机获取服务器 */
    22. public BackupServer getRandomInstance() {
    23. Random random = new Random();
    24. long no = random.nextInt(SERVER_COUNT) + 1;
    25. return SERVER_MAP.get(no);
    26. }
    27. }

    第二种: 同一类型只能创建一个对象,不同类型可以创建多个对象

    1. public class LogInstanceDemo {
    2. private static final ConcurrentHashMap instances
    3. = new ConcurrentHashMap<>();
    4. private LogInstanceDemo() {
    5. }
    6. public static LogInstanceDemo getInstance(String loggerName) {
    7. instances.putIfAbsent(loggerName, new LogInstanceDemo());
    8. return instances.get(loggerName);
    9. }
    10. }
    11. // log1 == log2, log1 != log3 && log2 != log3
    12. LogInstanceDemo log1 = LogInstanceDemo.getInstance("User.class");
    13. LogInstanceDemo log2 = LogInstanceDemo.getInstance("User.class");
    14. LogInstanceDemo log3 = LogInstanceDemo.getInstance("Person.class");

    这种多例模式类似工厂模式,但它跟工厂模式的不同之处在于,多例模式创建的对象都是同一个类的对象,而工厂模式创建的是不同子类的对象。


    Java 中的单例模式的唯一性

    上文中讲述单例唯一性的作用范围是进程,实际上,对于Java语言来说,单例类对象的唯一性的作用范围并非进程,而是类加载器(ClassLoader),这是因为不同类加载器之间命名空间不一样,不同的类加载器加载出来的类实例是不一样的,所以Java语言是类加载器内唯一。

     

  • 相关阅读:
    JavaSE、JavaEE与Spring的概念和异同点剖析以及规范13 个分析
    【问题解决】我遇到并解决PlatformIO无法使用的各种问题汇总及解决方法,简单粗暴使用的网络问题解决方法...
    logrotate:日志分割、压缩、删除
    Win:使用组策略启用和禁用 USB 驱动器
    Android Material Design之Chip, ChipGroup(十二)
    配置资源管理
    MySQL统计函数count详解
    解析JSON格式参数 & 修改对象的key
    1149 Dangerous Goods Packaging
    四 TypeScripe函数
  • 原文地址:https://blog.csdn.net/shuttlepro/article/details/127920753