聊聊如何实现更优雅的单例?

2022-06-17 00:00:00 创建 对象 实例 保证 懒汉

来自:https://blog.csdn.net/weixin_43178828

前言

阅读源码的时候总会发现大神 对于单例 Singleton, 比如数据库连接对象 比如线程池 还有spring的容器 都有些细微的操作 但不知道啥含义 今天我们聊聊优雅的(效率高 数据安全的)单例怎么实现
向着优雅Java的道路前进!

从简单的开始 getInstance

我们总是能看到getInstance方法 why?我一开始也不理解 为啥不直接调用这个实例呢 我直接访问那个实例对象就好了啊
随着读源码的深入 我萌生出几个问题:
  • 你访问这个实例对象的时候 这个实例真的已经创建出来了吗?
  • 这个实例对象是怎么创建出来的?创建的机制其实各有不同
    • 比如所谓懒汉式的创建 是需要你 依赖他这个实例的时候 才会触发 从而创建单例的 为的是节省内存空间——没人用我创建他出来干嘛?
    • 所以 在你获取对象之前 是不是有个触发的东西呢?说白了前边还有些代码逻辑实现懒汉式的思路
  • 如果能够直接访问到这个实例对象 怎么保证是单例呢?我上次访问的和这次 是同一个对象?
  • 如何保证创建的是单例 尤其是多线程的环境下
基于上述的问题 一个经典的实现套路:
public class MySingleton {
 private static MySingleton instance = new MySingleton();
 
 private Singleton() {
 }
  
 public static MySingleton getInstance() {
  return instance;
 } 
}
这样我们使用只需要MySingleton singleton = MySingleton.getInstance();
注意一个细节 为了保证单例 我们这里使用static 保证单例 和Class对象绑定在一起 所以必能访问的实例

复杂单例的创建过程——用串行化的static代码块解决

我们拓展一下 假设创建的过程很复杂 可能需要别的bean来辅助 即我依赖别的类的实例对象 辅助我完成创建 还有很多细节的创建过程 这时应该怎么进行单例的初始化呢?
使用static代码块 因为这个代码块执行顺序是严格串行的,JLS标准保证了这一点
所以不会有虚拟机优化 指令重排序的问题 也不会有多线程的数据安全问题
public class MySingleton {
 private static MySingleton instance = null;
 private static OtherSingleton helper = null;
 static{
  helper = OtherSingleton.getInstance();
  instance = MySingleton(helper);
 }

 private MySingleton(OtherSingleton helper) {
  if(helper) this.helper = helper;
  else throw new MyException("OtherSingleton getInstance failed");
 }
  
 public static MySingleton getInstance() {
  return instance;
 } 
}
可以发现 static的串行化 保证我创建单例的时候 依赖的helper是能拿得到的(这个具体由OtherSingleton 的getInstance负责 我们这里多加一层检查 拦截抛异常 ) 不会出现 因为多线程 导致创建的时候 helper拿不到的情况。。
getInstance里边也可以添加独特的东西(懒汉式我们后边再聊)

懒汉式(延迟创建)

基本思路,访问getInstance
  • 如果没有创建 则 开始创建 并返回单例
  • 已经创建 则直接返回
public class MySingleton {
 private static MySingleton instance = null;
 private static OtherSingleton helper = null;
 private MySingleton(OtherSingleton helper) {
  if(helper) this.helper = helper;
  else throw new MyException("OtherSingleton getInstance failed");
 }
 
 public static MySingleton getInstance() {
  if(instance == null)
   instance = new MySingleton(OtherSingleton.getInstance());
  return instance;
 } 
}
但是问题在于 多线程情况下 这个if的判断也未必准确 假设同时有两个线程都进到这个if里边执行 就会创建出两个实例 而不是单例 而为什么有两个能进去?一个线程创建单例的时候 另外一个直接进来了(那个时候单例还没创造出来 instance == null 当然进的来)

内存泄漏?

有人觉得 java有内存回收机制 没人用那个多余的单例 就会被回收 问题是 这么执行会导致两个单例都会被用到 创建的时候有多少个线程进去if里边 那就有多少单例产生并被使用 所以导致严重的内存泄漏!
解决方案:
我们认为这个if里边是个临界区域 就只能有一个线程在里边才对!所以可以粗暴的使用synchronized 让整个代码块顺序执行 就类似static代码块一样:
public class MySingleton {
 private static MySingleton instance = null;
 private static OtherSingleton helper = null;
 private MySingleton(OtherSingleton helper) {
  if(helper) this.helper = helper;
  else throw new MyException("OtherSingleton getInstance failed");
 }
 
 public static synchronized MySingleton getInstance() {
  if(instance == null)
   instance = new MySingleton(OtherSingleton.getInstance());
  return instance;
 } 
}

更高的性能 double check locking

整个上锁 由于锁粒度不够细 导致性能比较低 因此思路是尽量降低锁的粒度 范围:
public class MySingleton {
 private static MySingleton instance = null;
 private static OtherSingleton helper = null;
 private MySingleton(OtherSingleton helper) {
  if(helper) this.helper = helper;
  else throw new MyException("OtherSingleton getInstance failed");
 }
 
 public static synchronized MySingleton getInstance() {
  if(instance == null){
   synchronized (MySingleton.class){
    if(instance == null){
     instance = new MySingleton(OtherSingleton.getInstance());
    } 
   }
  }
   
  return instance;
 } 
}
这里 我们设定MySingleton.class对象 作为临界 意图明显 class对象 所以我们锁类 这样就保证了单线程的创建实例 其实和static静态块异曲同工 因为static也是类初始化的时候执行的 同样也是保证串行 绑定了class对象的执行

可见性 volatile

但这里其实还有个问题 就是创建实例对象的一瞬间 真的别的线程就能立马知道了嘛(可见性)?当然是不可能的 注意 我们电脑CPU和内存的数据一致性 或者说缓存一致性也是不一定有保证的 毕竟存在频率(访问速率)的差异 自然会存在缓存没有更新的情况
比如这里的实例对象变量instance!多线程在下一个指令周期 抢到了CPU计算的时间片 执行 那个时候缓存默认是不更新的
除非 我们调用java的volatile 他自然是个native的关键字 底层依赖C来实现变量的可见性!所以我们终极的程序应当是:
public class MySingleton {
 private volatile static MySingleton instance = null;
 private static OtherSingleton helper = null;
 private MySingleton(OtherSingleton helper) {
  if(helper) this.helper = helper;
  else throw new MyException("OtherSingleton getInstance failed");
 }
 
 public static synchronized MySingleton getInstance() {
  if(instance == null){
   synchronized (MySingleton.class){
    if(instance == null){
     instance = new MySingleton(OtherSingleton.getInstance());
    } 
   }
  }
   
  return instance;
 } 
}

另一种懒汉式创建单例——静态内部类

既然是懒汉式 自然没办法直接用static来创建了 但是可不可能用另外一个类的static来保证懒汉式单例呢?
public class MySingleton {
 private MySingleton(OtherSingleton helper) {
  if(helper) this.helper = helper;
  else throw new MyException("OtherSingleton getInstance failed");
 }
 
 public static class MySingletonAdapter {
  private static final MySingleton instance = new MySingleton(OtherSingleton.getInstance());
 } 
 public static getInstance() {
  return MySingletonAdapter.instance;
 }
}
这种方式被称为:Initialization on demand holder

序列化单例

实现了Serializable接口的单例 序列化倒没什么问题 但是反序列化时会产生新的实例对象 这里我们得改改readResolve()方法 使得返回的实例保证单例
public class MySingleton {
 private static final long serialVersionUID = -3453453414141241L;
 private static MySingleton instance = new MySingleton(OtherSingleton.getInstance());
 
 private MySingleton(OtherSingleton helper) {
  if(helper) this.helper = helper;
  else throw new MyException("OtherSingleton getInstance failed");
 }
 
 private Object readResolve() {
  return instance;
 }
}

后记

其实有没有考虑过另一个问题 这里我们很快乐的使用了OtherSingleton.getInstance() 但是有没有想过 系统刚开始一启动 实例化 谁先谁后呢?假设Other是后边才实例化的 前边的MySingleton的创建不是吃瘪了嘛???
假设我们控制一个创建单例的串行顺序 就好像玩游戏mod 有个依赖顺序 设计一个排序 那看起来虽然很麻烦 应该没啥问题
但是!如果 两个互相依赖怎么办?OtherSingleton初始化是需要MySingleton的 怎么办呢?如果几百个bean 初始化的时候互相依赖 该怎么解决?
这时就需要 有一种思想可以用于 解决 各种由于依赖导致的问题 ——IOC(invertion of controll) 控制翻转思想 其实际实现是通过依赖注入dependency injection。
<END>

相关文章