优雅のJava(二)—— 优雅的单例是怎么实现的?static | DCL | 静态内部类

专栏导航

优雅のJava(零)—— 面向问题的学习

前言

阅读源码的时候总会发现大神 对于单例 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

别慌 先混个眼熟 在第六篇 我们来聊聊这个问题,到时候我们可以看看 spirngIOC容器 是怎么解决循环依赖问题的 是怎么个依赖注入法 提前说好 这是非常复杂的问题 尤其加上源码就变得更加麻烦了 做好心理准备!

在看第六篇之前 我还是想做些铺垫 比如 第三篇工厂模式 第四篇代理模式(主要是动态代理+反射)以及第五篇观察者模式 这些都是为了第六篇做些许铺垫 毕竟考虑到直接上第六篇太硬核了 还是有些铺垫为好:)

专栏导航
优雅のJava(零)—— 面向问题的学习

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值