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

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

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

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. 1.给 INSTANCE 分配堆内存
  2. 2.调用 Mgr06 的构造函数来初始化成员变量,形成实例
  3. 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缓存行,看个人发挥
  • 最后,提一下静态内部类和枚举



相关文章
|
2月前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
31 2
|
14天前
|
设计模式 存储 前端开发
前端必须掌握的设计模式——单例模式
单例模式是一种简单的创建型设计模式,确保一个类只有一个实例,并提供一个全局访问点。适用于窗口对象、登录弹窗等场景,优点包括易于维护、访问和低消耗,但也有安全隐患、可能形成巨石对象及扩展性差等缺点。文中展示了JavaScript和TypeScript的实现方法。
|
19天前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
21 2
|
1月前
|
设计模式 Java 数据库连接
Java编程中的设计模式:单例模式的深度剖析
【10月更文挑战第41天】本文深入探讨了Java中广泛使用的单例设计模式,旨在通过简明扼要的语言和实际示例,帮助读者理解其核心原理和应用。文章将介绍单例模式的重要性、实现方式以及在实际应用中如何优雅地处理多线程问题。
38 4
|
1月前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
|
25天前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
|
1月前
|
设计模式 存储 数据库连接
PHP中的设计模式:单例模式的深入理解与应用
【10月更文挑战第22天】 在软件开发中,设计模式是解决特定问题的通用解决方案。本文将通过通俗易懂的语言和实例,深入探讨PHP中单例模式的概念、实现方法及其在实际开发中的应用,帮助读者更好地理解和运用这一重要的设计模式。
21 1
|
1月前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
27 0
|
2月前
|
设计模式 存储 数据库连接
PHP中的设计模式:单例模式的深入解析与实践
在PHP开发中,设计模式是提高代码可维护性、扩展性和复用性的关键技术之一。本文将通过探讨单例模式,一种最常用的设计模式,来揭示其在PHP中的应用及优势。单例模式确保一个类仅有一个实例,并提供一个全局访问点。通过实际案例,我们将展示如何在PHP项目中有效实现单例模式,以及如何利用这一模式优化资源配置和管理。无论是PHP初学者还是经验丰富的开发者,都能从本文中获得有价值的见解和技巧,进而提升自己的编程实践。
|
2月前
|
设计模式 安全 Java
C# 一分钟浅谈:设计模式之单例模式
【10月更文挑战第9天】单例模式是软件开发中最常用的设计模式之一,旨在确保一个类只有一个实例,并提供一个全局访问点。本文介绍了单例模式的基本概念、实现方式(包括饿汉式、懒汉式和使用 `Lazy&lt;T&gt;` 的方法)、常见问题(如多线程和序列化问题)及其解决方案,并通过代码示例详细说明了这些内容。希望本文能帮助你在实际开发中更好地应用单例模式,提高代码质量和可维护性。
64 1