ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
## 一. 什么是单例设计模式? >单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的类一个类只有一个实例。即一个类只有一个对象实例。 **类结构图** ![](https://img.kancloud.cn/af/b4/afb47e72f941d91e60b062b85f44d7f4_686x145.png) **具体实现** ``` 1. 将构造方法私有化,使其不能在类的外部通过 new关键字实例化该类对象。 2. 在该类内部产生一个唯一的实例化对象,并且将其封装为 private static 类型。 3. 定义一个静态方法返回这个唯一对象。 ``` ## 二. 单例设计模式实现形式 >**饿汉式**,从名字上也很好理解,就是“比较勤”,实例在初始化的时候就已经建好了,不管你有没有用到,都先建好了再说。好处是没有线程安全的问题,坏处是浪费内存空间。 >**懒汉式**,顾名思义就是实例在用到的时候才去创建,“比较懒”,用的时候才去检查有没有实例,如果有则返回,没有则新建。有新线程安全和线程不安全两种写法,区别就是 Synchronized 关键字。 #### **1、饿汉式实现方式一(推荐)** >类加载到内存后,就实例化一个单例,JVM保证线程安全;简单使用; 但也有缺点:不管用到与否,类装载时就完成实例化 ``` /** * 饿汉式 * 类加载到内存后,就实例化一个单例,JVM保证线程安全 * 简单实用,推荐使用! * 唯一缺点:不管用到与否,类装载时就完成实例化 * Class.forName("") * (话说你不用的,你装载它干啥) */ public class Singleton_01 { private static final Singleton_01 INSTANCE = new Singleton_01(); // 构造方法私有 private Singleton_01() {} public static Singleton_01 getInstance() { return INSTANCE; } public void m() { System.out.println("m"); } public static void main(String[] args) { Singleton_01 singleton01 = Singleton_01.getInstance(); Singleton_01 singleton02 = Singleton_01.getInstance(); System.out.println(singleton01 == singleton02); } } ``` #### **2、饿汉式实现方式二** 与上述 Singleton_01 一样,通过静态代码块实现单例的实例化 ``` /** * 跟01是一个意思 */ public class Singleton_02 { private static final Singleton_02 INSTANCE; // 通过静态代码块实现单例的实例化 static { INSTANCE = new Singleton_02(); } // 构造方法私有 private Singleton_02() {} public static Singleton_02 getInstance() { return INSTANCE; } public void m() { System.out.println("m"); } public static void main(String[] args) { Singleton_02 singleton01 = Singleton_02.getInstance(); Singleton_02 singleton02 = Singleton_02.getInstance(); System.out.println(singleton01 == singleton02); } } ``` #### **3、懒汉式实现方式一(线程不安全)** >懒汉式即需要使用类的实例化时才创建对象,但下述懒汉式却带来了线程不安全的问题;通过使用多线程也获取 单例对象的 hashCode 非常容易拿到不一样的hashCode,证明单例对象不是同一个,即存在线程不安全问题。 ``` /** * lazy loading * 也称懒汉式 * 虽然达到了按需初始化的目的,但却带来线程不安全的问题 */ public class Singleton_03 { private static Singleton_03 INSTANCE; // 构造方法私有 private Singleton_03() {} public static Singleton_03 getInstance() { if (INSTANCE == null) { try { TimeUnit.MILLISECONDS.sleep(1); } catch (Exception e){ e.printStackTrace(); } INSTANCE = new Singleton_03(); } return INSTANCE; } public void m() { System.out.println("m"); } public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(() -> { System.out.println(Singleton_03.getInstance().hashCode()); }).start(); } } } ``` #### **4、懒汉式实现方式二(线程安全,但效率低)** >运行过 【懒汉式实现方式一】就会发现存在多线程不安全的问题,因为可以引入 Synchronized 同步方法来解决此问题,但却带来了效率下降问题。 补充说明下:Synchronized在这里是修饰静态同步方法,实际上是对该类 Class 对象加锁,俗称“类锁”。 ``` /** * lazy loading * 也称懒汉式 * 虽然达到了按需初始化的目的,但却带来线程不安全的问题 * 线程当安全的问题,可以通过 synchronized 解决,但也带来效率下降 */ public class Singleton_04 { private static Singleton_04 INSTANCE; // 构造方法私有 private Singleton_04() {} // 锁住 Singleton_04 对象 public static synchronized Singleton_04 getInstance() { if (INSTANCE == null) { try { TimeUnit.MILLISECONDS.sleep(1); } catch (Exception e){ e.printStackTrace(); } INSTANCE = new Singleton_04(); } return INSTANCE; } public void m() { System.out.println("m"); } public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(() -> { System.out.println(Singleton_04.getInstance().hashCode()); }).start(); } } } ``` #### **5、懒汉实现方式三(线程不安全)** >运行过 【懒汉式实现方式二】就会发现引入 Synchronized 同步方法来解决多线程不安全问题,但却带来了效率下降问题。为此,将 Synchronized 同步锁的粒度减小,发现效率是提升了,但依然没有解决多线程不安全问题 ``` /** * lazy loading * 也称懒汉式 * 虽然达到了按需初始化的目的,但却带来线程不安全的问题 * 可以通过 synchronized 解决,但也带来效率下降 * 通过减少 synchronized 同步锁粒度,发现多线程不安全问题依然存在 */ public class Singleton_05 { private static Singleton_05 INSTANCE; // 构造方法私有 private Singleton_05() {} public static Singleton_05 getInstance() { if (INSTANCE == null) { // 妄图通过减少同步代码块的方式提高效率,然后不可能 synchronized (Singleton_05.class) { try { TimeUnit.MILLISECONDS.sleep(1); } catch (Exception e){ e.printStackTrace(); } INSTANCE = new Singleton_05(); } } return INSTANCE; } public void m() { System.out.println("m"); } public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(() -> { System.out.println(Singleton_05.getInstance().hashCode()); }).start(); } } } ``` #### **6、懒汉实现方式四 - DCL 双端检查 + volatile(线程安全)** >减少 synchronize 效率是可以的,但依然存在多线程不安全问题;通过引入 DCL (Double Check Lock 双端检查方式) + volatile (禁止指令重排),解决了多线程安全问题,同时还提高了效率。 >DCL + Volatile,综合了懒汉式和饿汉式两者的优缺点整合而成。看下面代码实现中,特点是在 Synchronized 关键字内外都加了一层 if 条件判断,这样既保证了线程安全,又比直接上锁提高了执行效率,还节省了内存空间。在这里使用 volatile 会或多或少的影响性能,但考虑到程序的正确性,牺牲这点性能还是值得的。DCL 优点是资源利用率高,第一次执行 getInstance() 时单例对象才被实例化,效率高。缺点是第一次加载时反应稍慢一些,在高并发环境下也有一定的缺陷,虽然发生的概率很小。DCL虽然在一定程度解决了资源的消耗和多余的同步,线程安全等问题,但是它还是在某些情况会出现失效的问题,也就是DCL 失效,在《java并发编程实践》一书建议用**静态内部类单例模式**来替代DCL. ``` /** * lazy loading * 也称懒汉式 * 虽然达到了按需初始化的目的,但却带来线程不安全的问题 * 可以通过 synchronized 解决,但也带来效率下降 * 通过减少 synchronized 同步锁粒度,发现多线程不安全问题依然存在 * 通过采用 DCL 即 Double Check Lock 双端检查机制,出现多线程不安全问题概率大大降低了,但还有存在多线程不安全问题 */ public class Singleton_06 { private static volatile Singleton_06 INSTANCE; // 构造方法私有 private Singleton_06() {} public static Singleton_06 getInstance() { if (INSTANCE == null) { // 双重检查 synchronized (Singleton_06.class) { if (INSTANCE == null) { try { TimeUnit.MILLISECONDS.sleep(1); } catch (Exception e){ e.printStackTrace(); } INSTANCE = new Singleton_06(); } } } return INSTANCE; } public void m() { System.out.println("m"); } public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(() -> { System.out.println(Singleton_06.getInstance().hashCode()); }).start(); } } } ``` #### **7、静态内部类方式,JVM 保证单例(推荐)** >通过 JVM 加载外部类时不会加载内部类,这样可以实现懒加载,完美写法之一,比第一种要好一些 第一次加载 Singleton 类时并不会初始化 INSTANCE,只有第一次调用 getInstance 方法时虚拟机加载 Singleton\_07\_Holder 并初始化 INSTANCE,这样不仅能确保线程安全也能保证 Singleton 类的唯一性,所以推荐使用静态内部类单例模式。 ``` /** * 静态内部类方式 * JVM 保证单例 * 加载外部类时不会加载内部类,这样可以实现懒加载 * * 完美写法之一,比第一种要好一些 */ public class Singleton_07 { // 构造方法私有 private Singleton_07() {} private static class Singleton_07_Holder { private final static Singleton_07 INSTANCE = new Singleton_07(); } // 不但实现了懒加载,而且只加载一次 // Singleton_07如果实例化了,Singleton_07_Holder也不会实例化,只有在getInstance被调用时才加载 public static Singleton_07 getInstance() { return Singleton_07_Holder.INSTANCE; } public void m() { System.out.println("m"); } public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(() -> { System.out.println(Singleton_07.getInstance().hashCode()); }).start(); } } } ``` #### **8、通过枚举实现单例模式(不推荐)** >枚举的方式是比较少见的一种实现方式,但是看上面的代码实现,却更简洁清晰。不仅可以解决线程同步,还可以防止反序列化. >默认枚举实例的创建是线程安全的,并且在任何情况下都是单例,上述讲的几种单例模式实现在,有一种情况下他们会重新创建对象,那就是反序化,将一个单例实例对象写到磁盘再读回来,从而获得了一个实例。反序列化操作提供了 readResolve 方法,这个方法可以让开发人员控制对象的反序列化。在上述几个方法示例中如果要杜绝单例对象被反序列化是重新生成对象。 >**枚举单例的优点就是简单,但是大部分应用开发很少用枚举,可读性并不是很高,不建议用。** ``` /** * 不仅可以解决线程同步,还可以防止反序列化 */ public enum Singleton_08 { INSTANCE; public void m() {} public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(() -> { System.out.println(Singleton_08.INSTANCE.hashCode()); }).start(); } } } ``` ## 三. 总结 >至于在实际项目中选择哪种形式的单例模式,取决于你的项目本身,是否有复杂的并发环境, 还是需要控制单例对象的资源消耗。