23种设计模式--#2单例模式
一、简介
1. 什么是单例模式
单例模式是设计模式中创建型模式的一种,它的核心思想是保证一个类在整个应用程序的生命周期中,只存在一个实例对象,并且这个实例对象能够被系统中的其他组件统一访问。就像现实生活中一个国家只有一个首都,一个公司只有一个 CEO 一样,在软件系统中,某些类的对象也只需要存在一个,以避免重复创建对象造成的资源浪费,或是多个实例同时存在导致的状态不一致等问题。
单例模式看似简单,却是实际开发中应用非常广泛的设计模式之一。它通过对类的实例化过程进行严格控制,确保无论在什么情况下,都只能通过特定的方式获取到该类的唯一实例,从而为系统的稳定性和高效性提供保障。
2. 单例模式的核心特点
单例模式的核心特点可以概括为以下三点:
确保唯一实例:这是单例模式最核心的特性。单例类会通过内部机制阻止外部通过常规的构造方法创建多个实例,保证在整个系统运行过程中,该类始终只有一个实例存在。例如在多线程环境下,无论多少个线程尝试获取该类的实例,最终得到的都是同一个对象,避免了因多实例存在而引发的资源竞争、状态混乱等问题。
自行实例化:单例类不会依赖外部类来创建自身的实例,而是由类自身负责完成实例的创建。这种 “自给自足” 的特性使得单例类的实例化过程更加可控,减少了外部因素对实例创建的干扰。具体来说,单例类会在内部定义一个静态的实例对象,并通过特定的逻辑在合适的时机完成初始化,不需要外部通过new关键字或其他创建对象的方式来生成实例。
向整个系统提供实例:单例类会提供一个全局访问点,让系统中的其他类或模块都能方便地获取到该唯一实例。这个访问点通常是一个静态的方法,例如getInstance(),外部通过调用这个方法就能得到单例对象,无需关心实例的创建细节。这种全局可访问的特性,使得单例对象可以作为系统中的 “全局工具”,承担起全局配置管理、跨模块数据共享等职责。
3.适用场景
单例模式适用于需要确保系统中某个类只有一个实例的场景,例如配置管理器、日志记录器、数据库连接池或线程池等,这些场景中多个实例可能导致资源浪费、数据不一致或性能问题,通过提供一个全局访问点实现实例的复用和统一管理。
二、单例模式的实现方式
单例模式的实现有多种方式
饿汉式:类加载就会导致该单实例对象被创建。
懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建。
1、懒汉式,线程不安全
这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程
优点:
- 延迟加载:实例仅在首次调用getInstance()方法时创建,避免了类加载时的资源浪费,适合实例初始化成本较高的场景。
缺点:
- 线程不安全:在多线程环境下,若多个线程同时进入if (instance == null)判断,可能会创建多个实例,破坏单例的唯一性。例如线程 A 和线程 B 同时检测到instance为 null,线程 A 先创建实例,线程 B 未感知到已创建的实例,会再次创建新实例。
public class Singleton { private static Singleton instance; private Singleton (){} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }
}
2、懒汉式,线程安全
能够在多线程中很好的工作,但是,效率很低
优点:
- 线程安全:通过synchronized关键字保证了多线程环境下只有一个线程能进入getInstance()方法,避免了多实例问题。
- 延迟加载:保留了懒汉式延迟初始化的特性,仅在首次使用时创建实例。
缺点:
- 性能较差:每次调用getInstance()方法都需要获取同步锁,即使实例已经创建,后续所有调用仍需等待锁释放,在高并发场景下会显著影响性能。
public class Singleton { private static Singleton instance; private Singleton (){} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }
}
3、饿汉式
这种方式比较常用,但容易产生垃圾对象。
优点:
- 线程安全:由于实例在类加载时就已创建,且 Java 类加载机制保证了线程安全性,因此无需担心多线程环境下的实例唯一性问题。
- 实现简单:代码逻辑清晰,无需复杂的同步或延迟加载逻辑。
缺点:
- 可能造成资源浪费:无论该实例是否被使用,都会在类加载时初始化,若实例占用大量资源(如内存、数据库连接等),且程序运行过程中始终未使用,则会造成不必要的资源消耗。
public class Singleton { private static Singleton instance = new Singleton(); private Singleton (){} public static Singleton getInstance() { return instance; }
}
4、双检锁/双重校验锁
双重检查锁定(Double-Checked Locking)是对同步方法的优化,通过减少同步范围提升性能,同时保证线程安全。
public class Singleton { private volatile static Singleton singleton; private Singleton (){} public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; }
}
优点:
- 线程安全:通过双重检查和同步块,确保多线程环境下仅创建一个实例。
- 延迟加载:实例在首次调用时初始化,避免资源浪费。
- 性能较好:仅在实例未初始化时进行同步,实例创建后无需进入同步块,大幅减少了锁竞争。
注意事项:
- 必须使用volatile关键字修饰实例变量:instance = new DoubleCheckedLockingSingleton()可分解为三步(分配内存、初始化对象、将引用指向内存),若不加volatile,可能因指令重排序导致其他线程获取到未初始化的实例。volatile可禁止指令重排序,保证实例初始化完成后才被其他线程可见。
5. 静态内部类
静态内部类实现方式利用了 Java 类加载机制的特性,兼顾了延迟加载和线程安全,且无需显式同步。
public class StaticInnerClassSingleton {// 私有构造方法private StaticInnerClassSingleton() {}// 静态内部类,仅在被调用时才会加载private static class InnerClass {private static final StaticInnerClassSingleton instance = new StaticInnerClassSingleton();}// 公共访问方法,通过内部类获取实例public static StaticInnerClassSingleton getInstance() {return InnerClass.instance;}
}
优点:
- 线程安全:Java 类加载机制保证了静态内部类的加载过程是线程安全的,因此实例初始化过程不会出现多线程竞争。
- 延迟加载:静态内部类InnerClass仅在getInstance()方法被调用时才会加载,实现了实例的延迟初始化。
- 性能好:无需同步机制,避免了锁带来的性能损耗。
原理分析:
- 外部类StaticInnerClassSingleton加载时,内部类InnerClass不会被加载,因此实例不会被初始化。
- 当调用getInstance()时,InnerClass被加载,其静态变量instance被初始化,且类加载过程由 JVM 保证线程安全,因此只会创建一个实例。
6、枚举
枚举是 Java 5 引入的特性,用枚举实现单例是一种简洁且安全的方式,由 JVM 天然保证单例特性。
public enum EnumSingleton {// 枚举实例,全局唯一INSTANCE;// 枚举类可以定义其他方法public void doSomething() {// 业务逻辑}
}
优点:
- 线程安全:JVM 保证枚举实例的创建是线程安全的,且在任何情况下都只会有一个实例。
- 防止反射和序列化破坏:枚举的构造方法由 JVM 控制,反射无法通过newInstance()创建实例;序列化时枚举实例不会被重新创建,反序列化后仍为原实例。
- 简洁性:代码量极少,无需手动处理同步、延迟加载等问题,可读性高。
适用场景:
- 适合对安全性要求高的场景,如全局配置、核心工具类等。
- 由于枚举类型本身的特性,不适合需要继承的场景(枚举类默认继承Enum,Java 不支持多继承)。