二十三天搞懂设计模式之单例模式的七种写法
1. 介绍
单例模式(Singletion Pattern)是 Java 中最简单的设计模式之一。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。
这个类提供了一种访问其唯一对象的方式,可以直接访问,不需要实例化该类的对象。
- 单例类只能有一个实例
- 单例类必须自己创建自己的唯一实例
- 单例类必须给其他对象提供这一实例
2. 使用场景
- 要求生产唯一的序列号
- WEB中的计数器,不用每次都去数据库里加一次,用单例缓存起来
- 创建一个对象需要消耗的资源过多,比如I/O与数据库的连接
优点
- 在内存中只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例
- 避免对资源的多重占用
缺点
- 没有接口、不能继承
- 只关心内部逻辑,不考虑外部如何实例化
3. 九种实现方法
3.1 饿汉式 V1.0
- 当该类被加载到内存时,就会实例化一个单例,JVM 保证其线程安全
- 唯一缺点:不管能不能用到,类装载时就会完成实例化
- 评价:简单实用,推荐使用
public class Mgr01 { private static final Mgr01 INSTANCE = new Mgr01(); private Mgr01() { } public static Mgr01 getInstance() { return INSTANCE; } public static void main(String[] args) { Mgr01 mgr01 = Mgr01.getInstance(); Mgr01 mgr02 = Mgr01.getInstance(); System.out.println(mgr01 == mgr02); } }
3.2 饿汉式 V2.0
- 饿汉式2.0版本对于饿汉式1.0版本来说,将原本的实例化放入了static代码块中
- 评价:面试可以简单提一下,除了装逼,没啥作用
public class Mgr02 { private static final Mgr02 INSTANCE; static { INSTANCE = new Mgr02(); } private Mgr02() { } public static Mgr02 getInstance() { return INSTANCE; } }
3.3 懒汉式 V1.0
- 只有我需要的时候,我才会去进行实例化,达到了按需初始化的目的
- 缺点:不支持多线程,因为没有加锁,在多线程状态下不能正常工作
public class Mgr03 { private static Mgr03 INSTANCE; private Mgr03() { } public static Mgr03 getInstance() { if (INSTANCE == null) { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } INSTANCE = new Mgr03(); } return INSTANCE; } public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(new Runnable() { @Override public void run() { System.out.println(Mgr03.getInstance().hashCode()); } }).start(); } } }
3.4 懒汉式 V2.0
- 相较于 1.0 版本,在 2.0 版本中加入了 synchronized 关键字保证其实例化。
- 缺点:加锁会影响效率
public class Mgr04 { private static Mgr04 INSTANCE; private Mgr04() { } public static synchronized Mgr04 getInstance() { if (INSTANCE == null) { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } INSTANCE = new Mgr04(); } return INSTANCE; } public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(new Runnable() { @Override public void run() { System.out.println(Mgr04.getInstance().hashCode()); } }).start(); } } }
3.4 懒汉式测试 V3.0
- 这是懒汉式的一个 测试 的升级版本
- 相较于 2.0 版本,主要对 synchronized 的位置进行了优化
- 缺点:当两个线程同时进入到该方法且 INSTANCE 为NULL,同样会产生线程不安全的情况
- 原因:当后来的线程夺取到CPU的执行权时,会再次创建一个Mgr05的实例化
- 总结:这个测试版本没什么用,只是为了引出DCL,面试的时候让面试官觉得你是个很有思考能力的人
public class Mgr05 { private static Mgr05 INSTANCE; private Mgr05(){}; public static Mgr05 getInstance(){ // 妄想通过减少同步代码块的方式去提高效率,然后不可行 if(INSTANCE == null){ synchronized (Mgr05.class){ try { Thread.sleep(1); }catch (Exception e){ e.getStackTrace(); } INSTANCE = new Mgr05(); } } return INSTANCE; } public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(new Runnable() { @Override public void run() { System.out.println(Mgr05.getInstance().hashCode()); } }).start(); } } }
3.5 双端检验锁(DCL,既:double-checked locking)
- 最重要的一个单例,面试装逼手撕必备,更含有 volatile 这一必考点,实乃装逼之神器
- 为什么用双端检锁?
- 主要为了解决上述懒汉式测试版本出现的无效问题
- 将 synchronized 直接放在外面,里面加一个判断null不可以吗?
- 可以是可以,但是在一般情况下,多个线程同时走到同一行代码的判断是比较少的
- 当我们的某个线程已经创建了实例化,我们在外面加一个判断,就会筛过之后的线程,不需要进行锁的争夺
- 为什么用 volatile ?
- Java 在进行编译的时候,为了使程序效率加快,会将没有相互联系的指令进行指令重排
- 对象在创建的时候,分为三个阶段
- 1.给 INSTANCE 分配堆内存
- 2.调用 Mgr06 的构造函数来初始化成员变量,形成实例
- 3.将 INSTANCE 指针指向分配的内存空间(执行完这步 INSTANCE 才是非 null了)
- 正常来讲:按照 1-2-3 的顺序是不会出错的,但是指令重排可能会出现 1-3-2 的情况,我们的对象还没有初始化成员变量,就已经分配好内存空间,造成数据的严重错误。
public class Mgr06 { private volatile static Mgr06 INSTANCE; private Mgr06() { } public static Mgr06 getInstance() { if (INSTANCE == null) { synchronized (Mgr06.class) { try { Thread.sleep(1); } catch (Exception e) { e.getStackTrace(); } if (INSTANCE == null) { INSTANCE = new Mgr06(); } } } return INSTANCE; } public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(new Runnable() { @Override public void run() { System.out.println(Mgr06.getInstance().hashCode()); } }).start(); } } }
3.6 静态内部类
- JVM 保证单例,只加载一次
- 加载外部类时不会加载内部类,这样可以实现懒加载,真正的实现了按需加载的目的
public class Mgr07 { private Mgr07(){ } private static class Mgr07Handle{ private static final Mgr07 INSTANCE = new Mgr07(); } public static Mgr07 getInstance(){ return Mgr07Handle.INSTANCE; } public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(new Runnable() { @Override public void run() { System.out.println(Mgr07.getInstance().hashCode()); } }).start(); } } }
3.7 枚举
- 面试可以简单一提,让面试官对你刮目相看
- 方法的出处:Effective Java 作者 Josh Bloch 提倡的方式
- 它不仅能避免多线程同步问题,还可以防止序列化和反序列化
- 枚举类没有构造方法
- 源码规定,在反射的时候,判断该类是否被ENUM修饰,如果是则直接抛出异常,反射失败
public enum Mgr08 { INSTANCE; public Mgr08 getInstance(){ return INSTANCE; } }
4. 总结
博主在面试小米、美团时被问到这个问题,回答方法也和本文类似,按以下流程回答,方可让面试官刮目相看
- 什么是单例?
- 单例的进化
- 杀手锏DCL双端检验锁
- 讲清为什么两次检验的原因
- 讲清 volatile 的指令重排,当然,可直接扩展至可见性,CPU缓存行,看个人发挥
- 最后,提一下静态内部类和枚举