关注我的公众号:帅哥趣谈,文章抢先看。
按照惯例,先来一波一键三连。
大家好,帅哥趣谈本期跟大家聊一下设计模式之单例模式。单例模式和工厂模式在23种设计模式中排名最靠前的两位,相交其他模式,大部分程序员在实际编程过程中都会接触到,是绕不开的。比如,大家肯定写过的工具类、线程池、定时任务等,一般都会封装成单例。考虑到多线程,自然就想到了线程安全,所以会写出线程安全的单例代码才是程序员必须要掌握的。(每个同学都必须要会工厂模式和单例模式,是必备技能包)
定义:确保一个类最多只有一个实例,并提供一个全局访问点。
大家都知道构建一个对象的实例是通过 new 执行构造方法创建,如果构造方法谁都可以访问,那么就不能保证唯一,所以单例模式对类的改造如下。
第一:要把构造方法私有化。
第二:类本身要对外提供访问入口,全局唯一,自然想到的就是静态方法static修饰的方法,具体如何使用,请往下看。
上面是通过类定义的方式来阐述单例模式的定义,但是生成对象实例还有一个方法,就是Java反射机制来构造对象,这样会绕过类定义的方式,全局唯一性会遭到破坏,于是就有了枚举类的方式,这种方式Java反射不能侵入(Java语言设计规范的原因)。
接下来就让我们带着问题去探索单例模式的前世今生。
学习方法提炼:在学习某个知识点的时候,大家一定要弄明白该知识点的重点是什么,这样就能找到更精准的学习资料,也能做到重要信息和次要的筛选,用最高效的方法快速达到学会的目的。
(这个学习方法是我多年经验的总结,分享出来大家一起共勉,千万不要眉毛胡子一把抓,到头什么收货都没有)
(知识点的总结有一个特点,就是一次完整总结,终身受益。正好跟Java的特性,一次编写,到处运行 Write once,Run anywhere 遥相呼应。如果这块不重视,你就会发现,你的整个职业生涯都是在重复整理这些知识点,每次效果都不好,不断的恶性循环,这是很多程序员不能突破瓶颈的根本原因。)
创建对象分三个步骤:①初始化内存空间、②初始化对象、③设置instance指向刚分配的内存地址。
- memory=allocate();//1:初始化内存空间
- ctorInstance(memory);//2:初始化对象
- instance=memory();//3:设置instance指向刚分配的内存地址
在不加锁的前提条件下,采用懒加载方式时,会存在多个请求同时访问的情况出现,第一次实例化对象的时候会出现多线程同步的问题,再加上Java本身会对指令执行的顺序重排优化,这就造成了乱序的问题,多个请求之间会彼此覆盖,所以会得到多个对象实例,或者拿到未初始化完成的对象出现异常,这违背了单例的设计初衷。系统设计就要求单模式必须是线程安全的,对象实例有且只有一次初始化。保证线程安全,第一要使用synchronized加锁,第二要使用volatile保证对象实例化过程的顺序性(防止指令重排,保证执行顺序严格按照步骤①②③执行)。
最简单的一种实现,在类装载过程中,完成实例化,避免多线程问题。
- public class EagerSingleton {
- private static final EagerSingleton INSTANCE = new EagerSingleton();
-
- private EagerSingleton() {
- }
-
- public static EagerSingleton getInstance() {
- return INSTANCE;
- }
- }
利用静态内部类的方式,将对象实例化延后。(预加载改成懒加载)(内部类不了解原理的一定要慎用,否则是给自己下毒,评论区讨论下是否给自己下过绊子)
- public class EagerSingleton {
- private static EagerSingleton INSTANCE = null;
-
- static {
- INSTANCE = new EagerSingleton();
- }
-
- private EagerSingleton() {
- }
-
- public static EagerSingleton getInstance() {
- return INSTANCE;
- }
- }
针对饿汉式而命名,在使用的时候完成初始化,相比较而言更灵活。但是坑也比较多,下面先看常见的坑。
在初始化的时候,存在多线程间覆盖。具体是,两个线程同时执行第8行代码,对象实例都是空,分别执行第9行,那么后边执行的就会覆盖前面执行,造成了资源的浪费。
- public class LazySingleton {
- private static LazySingleton INSTANCE = null;
-
- private LazySingleton() {
- }
-
- public static LazySingleton getInstance() {
- if (INSTANCE == null) {
- INSTANCE = new LazySingleton();
- }
- return INSTANCE;
- }
- }
锁的错误使用,这也解决了多线程的问题,但是也引入了新的性能问题:太慢。synchronized把整个方法包起来,也就是每个线程进入的时候,都需要等待其他线程结束调用,才能拿到实例,在性能敏感的场景,是比较致命的。
- public class LazySingleton {
- private static LazySingleton INSTANCE = null;
-
- private LazySingleton() {
- }
-
- public static synchronized LazySingleton getInstance() {
- if (INSTANCE == null) {
- INSTANCE = new LazySingleton();
- }
- return INSTANCE;
- }
- }
虽然有一步判断是否为空,但是并没有卵用,这种写法跟第一种情况相同,对象仍然会被实例化多次。比如两个线程同时执行第8行代码,线程A获取锁执行new操作,线程B等待获取锁执行new操作,当线程B拿到锁之后就会覆盖线程A的操作。
- public class LazySingleton {
- private static LazySingleton INSTANCE = null;
-
- private LazySingleton() {
- }
-
- public static LazySingleton getInstance() {
- if (INSTANCE == null) {
- synchronized (LazySingleton.class) {
- INSTANCE = new LazySingleton();
- }
- }
- return INSTANCE;
- }
- }
这种双重判断机制保证了单例对象只初始化一次,但是会面临指令重排导致的系列问题。(看上面解释)
- public class LazySingleton {
- private static LazySingleton INSTANCE = null;
-
- private LazySingleton() {
- }
-
- public static LazySingleton getInstance() {
- if (INSTANCE == null) {
- synchronized (LazySingleton.class) {
- if (INSTANCE == null) {
- INSTANCE = new LazySingleton();
- }
- }
- }
- return INSTANCE;
- }
- }
双重检查+阻止指令重排序。这根错误四的写法仅仅有一个volatile关键字之差,但是效果完全不同。
- public class LazySingleton {
- private static volatile LazySingleton INSTANCE = null;
-
- private LazySingleton() {
- }
-
- public static LazySingleton getInstance() {
- if (INSTANCE == null) {
- synchronized (LazySingleton.class) {
- if (INSTANCE == null) {
- INSTANCE = new LazySingleton();
- }
- }
- }
- return INSTANCE;
- }
- }
枚举式的出现是为了解决通过反序列化或反射实例化对象的问题。枚举的反序列化是通过name查找对象,不会产生新的对象;根据JVM规范,通过反射创建枚举对象时,会抛出IllegalArgumentException异常,这样相当于通过语法糖防止反序列化和反射破坏单例。
- public enum EnumSingleton {
- INSTANCE;
-
- public void method1() {
- // do something
- }
-
- public Object method2() {
- // do something and return something else
- return new Object();
- }
- }
大力推荐枚举式,其次是懒汉式的双重检查+阻止指令重排序(volatile)
实现方式 | 加载方式 | 是否线程安全 | 优点 | 缺点 | 优化 |
饿汉式 | 预加载 | 是 | 类装载过程就完成实例化 | 1.如果整个生命周期没有使用,造成内存资源浪费 2.构造函数不支持参数传递,不能满足个性化定制的需要 | 针对第一个缺点,可以借助静态内部类的方式,将对象实例化的时间延后 |
懒汉式 | 懒加载 | 否 | 灵活使用时可个性化定制 | 如果对多线程和指令重排序不了解,容易写出错误代码 | |
枚举式 | 懒加载 | 是 | 实现简单,且绝对安全 |