精灵王


  • 首页

  • 文章归档

  • 所有分类

  • 关于我

  • 搜索
设计模式-行为型 设计模式-创建型 设计模式-结构型 设计 系统设计 设计模式之美 分布式 Redis 并发编程 个人成长 周志明的软件架构课 架构 单元测试 LeetCode 工具 位运算 读书笔记 操作系统 MySQL 异步编程 技术方案设计 集合 设计模式 三亚 游玩 转载 Linux 观察者模式 事件 Spring SpringCloud 实战 实战,SpringCloud 源码分析 线程池 同步 锁 线程 线程模型 动态代理 字节码 类加载 垃圾收集器 垃圾回收算法 对象创建 虚拟机内存 内存结构 Java

设计模式:创建型—单例模式

发表于 2021-09-05 | 分类于 设计模式 | 0

定义

单例模式是属于创建型模式;

其英文定义的原话是:Ensure a class has only one instance,and provide a global point of access to it.

我们通俗点理解就是:一个类只允许创建一个对象(或者实例),那这个类就是一个单例类。

如何实现

单例类的实现也非常的简单,实现一个简单的单例类,一般需要注意以下几点:

  1. 构造函数需要是 private 访问权限,这样才能避免外部通过 new 创建实例;
  2. 考虑对象创建时的线程安全问题;
  3. 考虑是否支持延迟加载;
  4. 考虑 getInstance() 性能是否高(是否加锁)。

下面Java语言实现单例的几种常见写法

1.饿汉式

饿汉式的实现方式比较简单。一个“饿”字的精髓就体现于在类加载的时候,instance 静态实例就已经创建并初始化好了。不过,这种方式的缺点也很明显,那就是不支持延迟加载,但是instance 实例的创建过程是线程安全的。

下面是一个简单的单例ID生成器具体的Java实现

public class IdGenerator { 
  private AtomicLong id = new AtomicLong(0);
  private static final IdGenerator instance = new IdGenerator();// 饿汉式的精髓
  private IdGenerator() {}
  public static IdGenerator getInstance() {
    return instance;
  }
  public long getId() { 
    return id.incrementAndGet();
  }
}

采用饿汉式实现方式,将耗时的初始化操作,提前到程序启动的时候完成,这样就可以避免在程序运行的时候,再去初始化导致的性能问题。

2.懒汉式

懒汉式和饿汉式的思想差不多,其相对于饿汉式的优势就是支持延迟加载。

具体的代码实现如下所示:

public class IdGenerator { 
  private AtomicLong id = new AtomicLong(0);
  private static IdGenerator instance; // 懒汉式,调用getInstance时才实例化
  private IdGenerator() {}
  public static synchronized IdGenerator getInstance() {
    if (instance == null) {
      instance = new IdGenerator();
    }
    return instance;
  }
  public long getId() { 
    return id.incrementAndGet();
  }
}

这种懒汉式的缺点也很明显,给 getInstance() 这个方法加了一把大锁(synchronzed),导致这个函数的并发度会很低,在使用这个单例的时候都将是串行操作。

3.双重检测

懒汉式是给获取实例的方法加了一把大锁,实际上我们只需要在第一次获取的时候需要去初始化类,后面都无需再初始化了,双重检测就是懒汉式的改进版本,即支持延迟加载、又支持高并发(锁粒度减小或无锁),在这种实现方式中,只要 instance 被创建之后,即便再调用 getInstance() 函数也不会再进入到加锁逻辑中了(第一次检测就已经初始化好了)。

public class IdGenerator { 
  private AtomicLong id = new AtomicLong(0);
  private static IdGenerator instance;
  private IdGenerator() {}
  public static IdGenerator getInstance() {
    if (instance == null) { //1. 第一次检测
      synchronized(IdGenerator.class) { // 此处为类级别的锁
        if (instance == null) {  //2. 第二次检测
          instance = new IdGenerator();
        }
      }
    }
    return instance;
  }
  public long getId() { 
    return id.incrementAndGet();
  }
}

有人说,这种实现方式有些问题。因为指令重排序,可能会导致 IdGenerator 对象被 new 出来,并且赋值给 instance 之后,还没来得及初始化(执行构造函数中的代码逻辑),就被另一个线程使用了。

要解决这个问题,我们可以给 instance 成员变量加上 volatile 关键字,禁止指令重排序才行。实际上,只有很低版本的 Java 才会有这个问题。我们现在用的高版本的 Java 已经在 JDK 内部实现中解决了这个问题(解决的方法很简单,只要把对象 new 操作和初始化操作设计为原子操作,就自然能禁止重排序)。

4.静态内部类

再来看一种比双重检测更加简单的实现方法,利用 Java 的静态内部类。

它有点类似饿汉式,但又能做到了延迟加载。

看它的代码实现。

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

  private static class SingletonHolder{
    private static final IdGenerator instance = new IdGenerator();
  }
  
  public static IdGenerator getInstance() {
    return SingletonHolder.instance;
  }
 
  public long getId() { 
    return id.incrementAndGet();
  }
}

SingletonHolder 是一个静态内部类,当外部类 IdGenerator 被加载的时候,并不会创建 SingletonHolder 实例对象。

只有当调用 getInstance() 方法时,SingletonHolder 才会被加载,这个时候才会创建 instance。instance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。

所以,这种实现方法既保证了线程安全,又能做到延迟加载。

5. 枚举

最后介绍一种最简单的实现方式,基于枚举类型的单例实现。这种实现方式通过 Java 枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。

具体的代码如下所示:

public enum IdGenerator {
  INSTANCE;
  private AtomicLong id = new AtomicLong(0);
 
  public long getId() { 
    return id.incrementAndGet();
  }
}

单例存在哪些问题?

  1. 单例对 OOP 特性的支持不友好
    OOP 的四大特性是封装、抽象、继承、多态。单例这种设计模式对于其中的抽象、继承、多态都支持得不好。比如上面我们实现的IdGenerator,如果未来某一天,我们希望针对不同的业务采用不同的 ID 生成算法。比如,订单 ID 和用户 ID 采用不同的 ID 生成器来生成。为了应对这个需求变化,我们需要修改所有用到 IdGenerator 类的地方,这样代码的改动就会比较大。
  2. 单例会隐藏类之间的依赖关系
    单例无法通过构造函数、参数传递的方式来进行创建,无法通过函数的定义来查看类的依赖关系,具体实现逻辑都是隐藏在内部的,需要仔细查看代码实现才能知道这个类到底依赖哪些类。
  3. 单例对代码的扩展性不友好
    如果以后我们要在程序中允许创建两个实例或者多个实例,则需要对单例模式做很大的改动,虽然这种需求很少,但并不是没有,比如线程池。
  4. 单例对代码的可测试性不友好
    单例类这种硬编码式的使用方式,会导致在编写单元测试的时候很难实现 对依赖的外部资源进行mock 替换。
  5. 单例不支持有参数的构造函数
    单例不支持有参数的构造函数,比如我们创建一个连接池的单例对象,我们没法通过参数来指定连接池的大小。
    如何解决呢?
    1、先调用 init(param) 方法初始化参数,然后调用 getInstance()获取实例。
    2、将参数放到 getIntance() 方法中。
    3、将参数放到另外一个全局变量中。
精 灵 王 wechat
👆🏼欢迎扫码关注微信公众号👆🏼
  • 本文作者: 精 灵 王
  • 本文链接: https://jinglingwang.cn/archives/singleton
  • 版权声明: 本博客所有文章除特别声明外,均采用CC BY-NC-SA 3.0 许可协议。转载请注明出处!
# 设计模式-行为型 # 设计模式-创建型 # 设计模式-结构型 # 设计 # 系统设计 # 设计模式之美 # 分布式 # Redis # 并发编程 # 个人成长 # 周志明的软件架构课 # 架构 # 单元测试 # LeetCode # 工具 # 位运算 # 读书笔记 # 操作系统 # MySQL # 异步编程 # 技术方案设计 # 集合 # 设计模式 # 三亚 # 游玩 # 转载 # Linux # 观察者模式 # 事件 # Spring # SpringCloud # 实战 # 实战,SpringCloud # 源码分析 # 线程池 # 同步 # 锁 # 线程 # 线程模型 # 动态代理 # 字节码 # 类加载 # 垃圾收集器 # 垃圾回收算法 # 对象创建 # 虚拟机内存 # 内存结构 # Java
面向对象设计9大原则
设计模式:创建型—工厂模式
  • 文章目录
  • 站点概览
精 灵 王

精 灵 王

青春岁月,以此为伴

106 日志
14 分类
48 标签
RSS
Github E-mail
Creative Commons
Links
  • 添加友链说明
© 2023 精 灵 王
渝ICP备2020013371号
0%