二十三天搞懂设计模式之单例模式的七种写法

307 阅读6分钟

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

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缓存行,看个人发挥
  • 最后,提一下静态内部类和枚举

祝大家都能拿到好的offer,有什么问题可以加我微信或者留言