故事解说单例模式

73 阅读7分钟

今天就用通俗易懂的故事,来给您讲解下单例模式,并附上Java代码实现和优缺点分析。

故事解说单例模式

想象一下,你是一家公司的老板,公司里有一个非常重要的角色——财务总监。这位财务总监负责管理公司的所有财务事务,比如发工资、做账、报税等等。由于财务工作非常重要且敏感,你肯定不希望公司里有多个财务总监,各自为政,这样很容易造成混乱,甚至可能导致公司财务出现问题。

所以,你作为老板,会怎么做呢?你肯定会确保公司里只有一个财务总监,并且大家都只能通过这个唯一的财务总监来办理财务相关的事情。

在软件开发中,我们也经常遇到类似的情况。有些类在整个应用程序中只需要一个实例,比如:

  • 日志记录器:负责记录应用程序的运行日志,只需要一个实例来统一管理日志的写入和输出。
  • 数据库连接池:管理数据库连接,避免频繁地创建和关闭数据库连接,提高性能,只需要一个实例来管理所有的数据库连接。
  • 配置管理器:读取和管理应用程序的配置信息,只需要一个实例来确保配置信息的一致性。

这时候,我们就可以使用单例模式来解决这个问题。单例模式确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。就像公司里只有一个财务总监,大家都通过这个唯一的财务总监来办理财务事务一样。

Java实现常用单例模式

下面,我用Java代码来实现几种常用的单例模式,并讲解它们的优缺点。

1. 懒汉式(Lazy Initialization)

故事场景:财务总监平时可能在休息,只有当有人需要办理财务事务时,才会去“唤醒”他,让他开始工作。

java
	public class LazySingleton {

	    // 1. 私有静态变量,保存唯一实例,初始化为null

	    private static LazySingleton instance;

	 

	    // 2. 私有构造函数,防止外部直接实例化

	    private LazySingleton() {}

	 

	    // 3. 公共静态方法,提供全局访问点,按需创建实例

	    public static LazySingleton getInstance() {

	        if (instance == null) {

	            instance = new LazySingleton();

	        }

	        return instance;

	    }

	}

优点

  • 按需加载:实例在第一次被使用时才创建,节省了资源,提高了程序的启动速度。

缺点

  • 线程不安全:在多线程环境下,如果多个线程同时调用getInstance()方法,可能会创建多个实例,破坏单例模式。

2. 饿汉式(Eager Initialization)

故事场景:财务总监在公司刚成立时就已到位,随时准备为大家服务。

java
	public class EagerSingleton {

	    // 1. 私有静态变量,保存唯一实例,在类加载时就初始化

	    private static final EagerSingleton instance = new EagerSingleton();

	 

	    // 2. 私有构造函数,防止外部直接实例化

	    private EagerSingleton() {}

	 

	    // 3. 公共静态方法,提供全局访问点

	    public static EagerSingleton getInstance() {

	        return instance;

	    }

	}

优点

  • 线程安全:由于实例在类加载时就创建了,而类加载过程是线程安全的,所以饿汉式单例是线程安全的。
  • 实现简单:代码简单易懂,实现起来非常容易。

缺点

  • 资源浪费:即使实例从未被使用,也会在类加载时就创建,可能造成资源浪费,尤其是当实例创建成本较高时。
  • 启动速度可能受影响:如果实例的初始化过程比较耗时,可能会影响程序的启动速度。

3. 双重检查锁定(Double-Checked Locking)

故事场景:财务总监大部分时间在休息,但当有人需要办理财务事务时,会先检查一下财务总监是否在岗,如果不在岗,才会去“唤醒”他,并且为了防止多人同时去“唤醒”,会加个“锁”,确保只有一个“唤醒”操作。

java
	public class DoubleCheckedLockingSingleton {

	    // 1. 私有静态变量,保存唯一实例,使用volatile关键字保证可见性

	    private static volatile DoubleCheckedLockingSingleton instance;

	 

	    // 2. 私有构造函数,防止外部直接实例化

	    private DoubleCheckedLockingSingleton() {}

	 

	    // 3. 公共静态方法,提供全局访问点,双重检查锁定

	    public static DoubleCheckedLockingSingleton getInstance() {

	        if (instance == null) { // 第一次检查(无锁)

	            synchronized (DoubleCheckedLockingSingleton.class) { // 加锁

	                if (instance == null) { // 第二次检查(有锁)

	                    instance = new DoubleCheckedLockingSingleton();

	                }

	            }

	        }

	        return instance;

	    }

	}

优点

  • 按需加载:实例在第一次被使用时才创建,节省了资源。
  • 线程安全:通过双重检查锁定机制,保证了线程安全。
  • 性能较好:只有在第一次创建实例时需要加锁,后续访问不需要加锁,性能较好。

缺点

  • 代码复杂度略高:相对于懒汉式和饿汉式,代码稍微复杂一些,理解起来需要一些时间。
  • volatile关键字的必要性:必须使用volatile关键字来修饰实例变量,否则可能由于指令重排序导致问题。

4. 静态内部类(Static Inner Class)

故事场景:财务总监平时在一个“独立办公室”里休息,这个“独立办公室”只有当有人需要办理财务事务时才会被“打开”,财务总监才会开始工作。

java
	public class StaticInnerClassSingleton {

	 

	    // 1. 私有构造函数,防止外部直接实例化

	    private StaticInnerClassSingleton() {}

	 

	    // 2. 静态内部类,负责实例的创建

	    private static class SingletonHolder {

	        // 3. 静态常量,保存唯一实例,在SingletonHolder类加载时初始化

	        private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();

	    }

	 

	    // 4. 公共静态方法,提供全局访问点,通过静态内部类获取实例

	    public static StaticInnerClassSingleton getInstance() {

	        return SingletonHolder.INSTANCE;

	    }

	}

优点

  • 按需加载:实例在第一次被使用时才创建(当getInstance()方法第一次被调用时,静态内部类SingletonHolder才会被加载,实例才会被创建),节省了资源。
  • 线程安全:利用了类加载机制来保证线程安全,JVM在类加载过程中会保证线程安全。
  • 实现简单:代码简洁易懂,实现起来相对简单。

缺点

  • 反射攻击:理论上,可以通过反射机制破坏单例模式,创建多个实例(虽然在实际开发中这种情况比较少见,但需要注意)。

5. 枚举(Enum)

故事场景:财务总监是公司里一个“特殊职位”,这个职位只能有一个人担任,而且这个职位是“天生”就存在的,不需要去“创建”或“唤醒”。

java
	public enum EnumSingleton {

	    INSTANCE; // 枚举实例,天生就是单例

	 

	    // 可以在这里添加单例类需要的方法和字段

	    public void doSomething() {

	        System.out.println("EnumSingleton is doing something.");

	    }

	}

优点

  • 线程安全:枚举实例的创建是线程安全的,由JVM保证。
  • 防止反射攻击:枚举类型可以有效地防止通过反射机制创建多个实例,保证了单例的唯一性。
  • 序列化安全:枚举类型在序列化和反序列化时,也能保证单例的唯一性。
  • 实现简单:代码非常简洁,一行代码就能实现单例。

缺点

  • 灵活性较低:枚举类型不能继承其他类(只能实现接口),在某些需要继承的场景下可能不太适用。
  • 不太直观:对于不熟悉枚举的人来说,可能不太容易理解枚举如何实现单例模式。

总结

单例模式线程安全按需加载实现难度优点缺点
懒汉式简单按需加载,节省资源线程不安全,多线程环境下可能创建多个实例
饿汉式简单线程安全,实现简单资源浪费,可能造成资源浪费,启动速度可能受影响
双重检查锁定中等按需加载,线程安全,性能较好代码复杂度略高,需要volatile关键字
静态内部类简单按需加载,线程安全,实现简单反射攻击(理论上)
枚举简单线程安全,防止反射攻击,序列化安全,实现简单灵活性较低,不太直观

在实际开发中,可以根据具体的需求和场景,选择最合适的单例模式实现方式。如果对线程安全和防止反射攻击有较高要求,推荐使用枚举静态内部类方式实现单例模式。如果对按需加载和性能有较高要求,并且可以接受稍复杂的代码,可以选择双重检查锁定方式。