设计模式学习(二):单例模式

设计模式学习(二):单例模式

作者:Grey

原文地址:

博客园:设计模式学习(二):单例模式

CSDN:设计模式学习(二):单例模式

单例模式

单例模式是创建型模式。

单例的定义:”一个类只允许创建唯一一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。”定义中提到,”一个类只允许创建唯一一个对象”。那对象的唯一性的作用范围是指进程内只允许创建一个对象,也就是说,单例模式创建的对象是进程唯一的(而非线程)

设计模式学习(二):单例模式

为什么要使用单例

  1. 处理资源访问冲突,比如写日志的类,如果不使用单例,就必须使用锁机制来解决日志被覆盖的问题。
  2. 表示全局唯一类,比如配置信息类,在系统中,只有一个配置文件,当配置文件加载到内存中,以对象形式存在,也理所应当只有一份;唯一 ID 生成器也是类似的机制。如果程序中有两个对象,那就会存在生成重复 ID 的情况,所以,我们应该将 ID 生成器类设计为单例。

饿汉式

类加载的时候就会初始化这个实例,JVM 保证唯一实例,线程安全,但是可以通过反射破坏

方式一

public class Singleton1 {
    private final static Singleton1 INSTANCE = new Singleton1();

    private Singleton1() {
    }

    public static Singleton1 getInstance() {
        return INSTANCE;
    }
}

方式二

public class Singleton2 {
    private static final Singleton2 INSTANCE;

    static {
        INSTANCE = new Singleton2();
    }
    private Singleton2() {

    }
    public static Singleton2 getInstance() {
        return INSTANCE;
    }
}

注意:

这种方式不支持延迟加载,如果实例占用资源多(比如占用内存多)或初始化耗时长(比如需要加载各种配置文件),提前初始化实例是一种浪费资源的行为。最好的方法应该在用到的时候再去初始化。不过,如果初始化耗时长,那最好不要等到真正要用它的时候,才去执行这个耗时长的初始化过程,这会影响到系统的性能,我们可以将耗时的初始化操作,提前到程序启动的时候完成,这样就能避免在程序运行的时候,再去初始化导致的性能问题。如果实例占用资源多,按照 fail-fast 的设计原则(有问题及早暴露),那我们也希望在程序启动时就将这个实例初始化好。如果资源不够,就会在程序启动的时候触发报错(比如 Java 中的 PermGen Space OOM ),我们可以立即去修复。这样也能避免在程序运行一段时间后,突然因为初始化这个实例占用资源过多,导致系统崩溃,影响系统的可用性。

这两种方式都可以通过反射方式破坏,例如:

Class aClass=Class.forName("singleton.Singleton2",true,Thread.currentThread().getContextClassLoader());
Singleton2 instance1=(Singleton2)aClass.newInstance();
Singleton2 instance2=(Singleton2)aClass.newInstance();
System.out.println(instance1==instance2);

懒汉式

虽然可以实现按需初始化,但是线程不安全, 因为在判断 INSTANCE == null 的时候,有可能出现一个线程还没有把 INSTANCE初始化好,另外一个线程判断 INSTANCE==null 得到 true,就会继续初始化

public class Singleton3 {
    private static Singleton3 INSTANCE;

    private Singleton3() {
    }

    public static Singleton3 getInstance() {
        if (INSTANCE == null) {
            // 模拟初始化对象需要的耗时操作
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Singleton3();
        }
        return INSTANCE;
    }
}

为了防止线程不安全,可以在 getInstance 方法上加锁,这样既实现了按需初始化,又保证了线程安全,

但是加锁可能会导致一些性能的问题:我们给 getInstance()这个方法加了一把大锁,导致这个函数的并发度很低。量化一下的话,并发度是 1,也就相当于串行操作了。如果这个单例类偶尔会被用到,那这种实现方式还可以接受。但是,如果频繁地用到,那频繁加锁、释放锁及并发度低等问题,会导致性能瓶颈,这种实现方式就不可取了。

public class Singleton4 {
    private static Singleton4 INSTANCE;

    private Singleton4() {
    }

    public static synchronized Singleton4 getInstance() {
        if (INSTANCE == null) {
            // 模拟初始化对象需要的耗时操作
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Singleton4();
        }
        return INSTANCE;
    }
}

为了提升一点点性能,可以不给 getInstance() 整个方法加锁,而是对 INSTANCE 判空这段代码加锁, 但是这样一来又带来了线程不安全的问题

public class Singleton5 {
    private static Singleton5 INSTANCE;

    private Singleton5() {
    }

    public static Singleton5 getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton5.class) {
                // 模拟初始化对象需要的耗时操作
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                INSTANCE = new Singleton5();
            }
        }
        return INSTANCE;
    }
}

Double Check Locking 模式,就是 双加锁检查模式,这种方式中, volatile 关键字是必需的,目的为了防止指令重排,生成一个半初始化的的实例,导致生成两个实例。

具体可参考 双重检索(DCL)的思考: 为什么要加volatile?

public class Singleton6 {
    private volatile static Singleton6 INSTANCE;

    private Singleton6() {
    }

    public static Singleton6 getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton6.class) {
                if (INSTANCE == null) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new Singleton6();
                }
            }
        }
        return INSTANCE;
    }
}

以下两种更为优雅的方式,既保证了线程安全,又实现了按需加载。

方式一:静态内部类方式, JVM 保证单例,加载外部类时不会加载内部类,这样可以实现懒加载

public class Singleton7 {
    private Singleton7() {
    }

    public static Singleton7 getInstance() {
        return Holder.INSTANCE;
    }

    private static class Holder {
        private static final Singleton7 INSTANCE = new Singleton7();
    }

}

方式二: 使用枚举, 这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化,这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。

public enum Singleton8 {
    INSTANCE;
}

单例模式的替代方案

使用静态方法

   // 静态方法实现方式
public class IdGenerator {
    private static AtomicLong id = new AtomicLong(0);

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

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

使用依赖注入

   // 1. 老的使用方式
   public demofunction() {
     //...

     long id = IdGenerator.getInstance().getId();
     //...

   }

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

线程单例

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


public class IdGenerator {
  private AtomicLong id = new AtomicLong(0);

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

  private IdGenerator() {}

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

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

集群模式下单例

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

如何实现一个多例模式

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

单例模式的应用举例

JDK 的 Runtime 类

public class Runtime {
  private static Runtime currentRuntime = new Runtime();

  public static Runtime getRuntime() {
    return currentRuntime;
  }

  /** Don't let anyone else instantiate this class */
  private Runtime() {}
.......

}

还有就是 Spring 中 AbstractBeanFactory 中包含的两个功能。

功能一,就是从缓存中获取单例 Bean

功能二,就是从 Bean 的实例中获取对象。

UML 和 代码

UML 图

代码

更多

设计模式学习专栏

参考资料

Original: https://www.cnblogs.com/greyzeng/p/16866401.html
Author: Grey Zeng
Title: 设计模式学习(二):单例模式

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/799142/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • Python 学习笔记(六)–线程

    1.自定义进程 自定义进程类,继承Process类,重写run方法(重写Process的run方法)。 from multiprocessing import Process im…

    Python 2023年5月25日
    081
  • Dataframe取特定的行/列&按行/列遍历数据的值_python处理Excel入门

    文章目录 取Dataframe特定的行/列 * 取特定的列 – 按列名选取列 取特定的行 – 按行名选取行 按数字选取行 同时选取行和列 – 按…

    Python 2023年8月7日
    042
  • Flask源码解析(二):Flask的工作原理

    本文通过解析Flask0.1的源码,讲解一下Flask框架的主要工作流程。为了方便理解,后面涉及到的部分源码只保留核心部分,要看完整版可以点这里:Flask0.1的源码。 启动应用…

    Python 2023年8月10日
    067
  • Unity技术手册-UGUI零基础详细教程-ScrollBar和ScrollView

    往期文章分享 点击跳转=>《导航贴》- Unity手册,系统实战学习 点击跳转=>《导航贴》- Android手册,重温移动开发 本文约8千字,新手阅读需要20分钟,复…

    Python 2023年9月29日
    052
  • 用Python做童年回忆的游戏 贪吃蛇

    为了让大家对python产生兴趣,不让大家学编程枯燥无味,所以今天老袁还是准备了一个小游戏给大家来玩玩,喜欢的可以跟着我敲代码哟。那么废话不多说了直接开始吧! 我们选择好开发工具 …

    Python 2023年9月24日
    033
  • 第5章 pandas入门

    整述:pandas是有使数据清洗和分析工作变得更快更简单的数据结构和操作工具。pandas经常和其它工具⼀同使用,如数值计算工具NumPy和SciPy,分析库statsmodels…

    Python 2023年8月6日
    066
  • 一行Python代码即可实现数据可视化大屏

    今天分享一个 Python 可视化大屏项目,GitHub 地址: https://github.com/TurboWay/big_screen,项目结构简单使用方便,直接传数据就可…

    Python 2023年8月9日
    057
  • jenken

    一、Jenkins是什么? Jenkins是一个开源的,持续集成,支持设置定时任务的软件,需要安装配置,安装完成后通过浏览器地址输入http://localhost:8080就可以…

    Python 2023年6月12日
    061
  • 一行代码制作数据分析交叉表,太便捷了

    模块导入和数据读取 那我们按照惯例,首先导入模块并且来读取所要使用到的数据集,引用的依然是之前制作数据透视表的数据集 import pandas as pd def load_da…

    Python 2023年8月22日
    054
  • AI常用框架和工具丨3. 可视化库Matplotlib

    可视化库Matplotlib,AI常用框架和工具之一。理论知识结合代码实例,希望对您有所帮助。 文章目录 * – 环境说明 – 一、Matplotlib简介…

    Python 2023年9月1日
    062
  • 05-python数据可视化

    Flask入门 关于Flask 了解框架 ​ Flask作为Web框架,它的作用主要是为了开发Web应用程序。 ​ 1.所有Flask程序都必须创建一个程序实例。 ​ 2.当客户端…

    Python 2023年8月11日
    048
  • python数据分析之Mathplotlib

    什么是Matplotlib? Matplotlib 是一个 Python 的 2D绘图库,通过 Matplotlib,开发者可以仅需要几行代码,便可以生成绘图,直方图,功率谱,条形…

    Python 2023年9月4日
    054
  • 敏捷价值流管理

    对团队或企业来说,敏捷能够通过快速迭代、改进来更好地为客户或终端用户交付价值。但有些团队在引入敏捷项目管理模式之后,团队管理层看了看埋头工作的团队,”唉?团队的效率好像…

    Python 2023年10月11日
    044
  • 【IEEE】IEEE论文从投稿到发表全流程案例说明

    【IEEE】IEEE论文从投稿到发表全流程案例说明 1、选期刊-写论文 2、投稿 3、论文状态变化和应对流程 4、版权转移 5、自动更新orcid确认 6、预印版成果发行邮件 7、…

    Python 2023年10月11日
    086
  • Python爬虫学习-简单爬取网页数据

    疫情宅家无事,就随便写一些随笔吧QwQ… 这是一篇介绍如何用Python实现简单爬取网页数据并导入MySQL中的数据库的文章。主要用到BeautifulSoup req…

    Python 2023年8月1日
    051
  • linux下c语言绘图库_图形-Haskell像素绘图库linux

    在我看来,您实际上并没有在此处绘制任何内容.您确定setColors会按照您的想法工作吗?如果需要内存,那是为8位表面设置调色板,而您明确地设置为32位视频模式. 顺便说一句,如果…

    Python 2023年9月23日
    054
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球