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

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

点击加入:
后端技术内卷群,一起学习!


前言

阅读源码的时候总会发现大神 对于单例 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是能拿得到的(这个具体由OtherSingletongetInstance负责 我们这里最多加一层检查 拦截抛异常 ) 不会出现 因为多线程 导致创建的时候 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

原文:https://blog.csdn.net/weixin_43178828


精彩推荐
最全的java面试题库
开源版的高仿 “ 微信 ”,吊炸天!

与其在网上拼命找题? 不如马上关注我们~

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


↓点阅读原文,Java面试题库尽情看!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/20540.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!