单例模式,也叫单子模式,是一种常用的软件设计模式。单例模式确保一个类只有一个实例,提供一个全局访问点以访问该实例,属于创建型模式的一种。
单例模式的运用场景
单例模式的主要特点包括:
- 唯一实例: 单例模式确保一个类只有一个实例存在。当程序首次请求该类的实例时,它会创建一个新的实例。随后的请求将返回该已创建的实例。
- 全局访问点: 单例模式提供一个全局的访问点,允许其他部分在应用程序中访问该唯一实例。这样可以方便地共享实例的状态和功能。
以下是一些常见的单例模式的运用场景:
- 配置管理器: 在应用程序中,可能会有一些全局配置需要在整个应用程序中共享。使用单例模式可以创建一个配置管理器类,确保在应用程序的任何地方都可以访问和更新配置信息。
- 日志记录器: 在应用程序中实现日志记录功能时,使用单例模式可以确保所有部分都共享同一个日志记录器实例,避免产生多个日志文件或混乱的日志输出。
- 数据库连接池: 在需要频繁访问数据库的应用程序中,使用单例模式可以创建一个数据库连接池,以确保连接资源得到有效的管理和重用。
- 线程池: 如果应用程序需要处理大量并发任务,可以使用单例模式创建一个线程池,以控制并发线程的数量,避免过多的线程创建和销毁开销。
- 缓存管理器: 在需要使用缓存来提高性能的场景中,可以使用单例模式来管理缓存,确保缓存的一致性和有效性。
- 打印机管理器: 在需要管理打印任务的应用程序中,使用单例模式可以创建一个打印机管理器,确保打印任务按顺序进行。
单例模式的实现
普通饿汉式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
public class HungryStyleSingleInstance {
private static final HungryStyleSingleInstance HUNGRY_STYLE_SINGLE_INSTANCE = new HungryStyleSingleInstance();
private HungryStyleSingleInstance() { }
public static HungryStyleSingleInstance getInstance() { return HUNGRY_STYLE_SINGLE_INSTANCE; } }
|
- 优缺点:
- 优点:
- 类加载时就进行实例化,之后的操作效率会很高。
- 实现简单,代码清晰。没有复杂的线程同步逻辑,易于理解和维护。
- 缺点:
- 由于类加载时就进行实例化,如果后续不对此类进行任何操作,就会导致内存的浪费。
普通懒汉式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
public class LazyStyleSingleInstance {
private static LazyStyleSingleInstance instance;
private LazyStyleSingleInstance() { }
public static LazyStyleSingleInstance getInstance() { if (instance == null) { instance = new LazyStyleSingleInstance(); } return instance; } }
|
- 优缺点:
- 优点:
- 实例只有在需要的时候才会被创建,避免了启动时的资源浪费。这对于大型对象或有较高初始化成本的对象来说很有用。
- 缺点:
- 尽管懒加载有其优势,但在多线程环境下,每次调用
getInstance
都需要进行线程安全的检查,这可能会降低性能。 - 当多个线程同时进入到
if(instance == null) {...}
时,会创建多个对象
同步锁懒汉式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
public class SynchronizedLazyStyleSingleInstance { private static SynchronizedLazyStyleSingleInstance instance;
private SynchronizedLazyStyleSingleInstance() { }
public static synchronized SynchronizedLazyStyleSingleInstance getInstance() { if (instance == null) { instance = new SynchronizedLazyStyleSingleInstance(); } return instance; } }
|
- 优缺点:
- 优点:
- 通过在
getInstance
方法上使用 synchronized
关键字,确保了在任何时候只有一个线程能够访问该方法,从而避免了在多线程环境下创建多个实例的问题。 - 与前述懒汉式实现方式一样,实例只有在需要的时候才会被创建,避免了启动时的资源浪费。
- 缺点:
- synchronized 关键字会导致线程竞争和阻塞,从而降低性能。
- 每次调用 getInstance 都需要获得锁,即使只有第一次调用时也需要。
- 这种实现方式仍然是延迟加载的,但由于同步,可能会导致访问该方法的其他线程等待较长时间。且在高并发情况下,同步锁可能成为性能瓶颈,导致程序效率低下。导致性能问题。
双检索饿汉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
public class DoubleCheckLockLazyStyleSingleInstance {
private static volatile DoubleCheckLockLazyStyleSingleInstance instance;
private DoubleCheckLockLazyStyleSingleInstance() { }
public static DoubleCheckLockLazyStyleSingleInstance getInstance() { if (instance == null) { synchronized (DoubleCheckLockLazyStyleSingleInstance.class) { if (instance == null) { instance = new DoubleCheckLockLazyStyleSingleInstance(); } } } return instance; } }
|
- 优缺点:
- 优点:
- 使用双重检查锁定和
volatile
关键字可以确保在多线程环境下创建唯一的实例,并且性能相对较好。这是因为只有在实例为 null 的情况下才会进入同步块,其他线程不会被阻塞。 - 实例只有在需要的时候才会被创建,避免了启动时的资源浪费。
- 与传统的同步方式相比,双重检查锁定方式提供了更高的并发度,减少了性能瓶颈。
volatile
关键字确保了在实例化对象时不会发生指令重排序,保证了实例的唯一性和正确性。
- 缺点:
- 双重检查锁定的实现相对复杂,需要开发人员充分理解并正确实现,以避免潜在的错误。
- JDK 5 及以后的版本中,
volatile
关键字的语义得到了改进,因此双检锁定须在 JDK 5 以后的版本才能确保安全 - 需要在第一次创建实例时获得锁,这可能会导致性能稍有下降。
静态内部类懒汉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
|
public class StaticInternalLazyStyleSingleInstance {
private static class Singleton {
private static final StaticInternalLazyStyleSingleInstance STATIC_INTERNAL_LAZY_STYLE_SINGLE_INSTANCE = new StaticInternalLazyStyleSingleInstance(); }
private StaticInternalLazyStyleSingleInstance() { }
public static StaticInternalLazyStyleSingleInstance getInstance() { return Singleton.STATIC_INTERNAL_LAZY_STYLE_SINGLE_INSTANCE; } }
|
- 优缺点:
- 优点:
- 利用类加载机制和类初始化锁定,保证了在首次访问 getInstance 方法时才会加载内部类,从而实现了懒加载并保证了线程安全性。
- 实例只有在需要的时候才会被创建,避免了启动时的资源浪费。
- 静态内部类的方式在获取实例时没有显式的锁操作,因此在性能上较为高效。
- 实现相对简洁,不需要开发人员显式地处理线程安全性,由 JVM 来保证。
- 由于类加载机制的特性,静态内部类的实例在加载过程中不会发生指令重排序,保证了实例的唯一性和正确性。
- 缺点:
- 与所有懒汉单例一样,不适合需要提前初始化的场景(数据库连接池、配置管理器、全局缓存等)
1 2 3 4 5 6 7 8 9 10
|
public enum EnumSingleInstance { INSTANCE; }
|
- 优缺点:
- 优点:
- 枚举类的实例在加载时就被创建,由于枚举类的加载是由 JVM 管理的,所以在多线程环境下也可以保证唯一实例。枚举类的加载是延迟加载的,在首次被访问时才会被创建。因此,这是一种绝对线程安全的懒加载(懒汉单例)单例模式实现方式。
- 枚举类天然地防止了反序列化攻击,因为枚举类的实例不能通过反序列化创建多个实例。
- 枚举单例是一种非常简洁明了的实现方式,不需要开发人员处理线程安全性和懒加载逻辑。
- 枚举类可以实现 Serializable 接口,从而实现了可序列化,可以在分布式系统中使用。
- 缺点:
- 枚举单例是一种最终类,无法被继承,因此无法扩展或添加额外的行为。
- 枚举是在 JDK 5 版本引入的新内容,所以需在 JDK 5 及之后版本使用。
单例模式的优缺点
优点:
- 单例模式提供了一个全局访问点,可以方便地访问唯一的实例,避免了需要频繁创建和传递实例的麻烦。
- 单例模式可以避免重复创建相同类型的实例,节省了系统资源和内存消耗。
- 懒汉式单例模式允许实例的延迟加载,只有在需要时才会创建,从而提高了系统的性能和启动速度。
- 线程安全的单例模式所提供的全局访问点,避免了多线程环境下的竞态条件和数据不一致问题。
缺点:
- 单例模式一般来说是静态的,不容易扩展为多个实例。如果在未来需要支持多个实例,可能需要修改现有的代码。
- 单例模式可能会导致一些高耦合的设计,因为它在整个系统中引入了全局状态。这可能使得代码的可测试性和维护性降低。
- 单例模式的实例既负责自己的职责,又充当了全局访问点,可能会导致一个类承担过多的职责,即违反单一职责原则(一个类应该只实现一个逻辑,而不用关心它是否是单例的。)。
- 单例模式可能会导致代码难以测试,因为全局状态的存在可能会影响测试用例之间的互相干扰。
- 单例模式隐藏了实例的创建和管理细节,可能使得代码可读性变差。
最后附上本文所写源代码:设计模式——单例模式