设计模式 - 单例模式

1,177 阅读15分钟

✨作者:猫十二懿

❤️‍🔥账号:CSDN掘金个人博客Github

🎉公众号:猫十二懿

单例模式

单例模式是一种创建型设计模式,它的目的是确保一个类只有一个实例,并提供一个全局访问点来访问该实例。在单例模式中,类自身负责创建自己的唯一实例,并确保在系统中只有一个实例存在。

1、单例模式介绍

单例模式(Singleton),保证一个类仅有一个实例,并提供一个访问它的全局访问点。

通常我们可以让一个全局变量使得一个对象被访问,但它不能防止你 实例化多个对象。一个最好的办法就是,让类自身负责保存它的唯一实例。 这个类可以保证没有其他实例可以被创建,并且它可以提供一个访问该实例 的方法。

1.1 单例模式结构图

image-20230514092119352

Singleton类,定义一个GetInstance操作,允许客户访问它的唯一实例。GetInstance是一个静态方法,主要负责创建自己的唯一实例。

/**
 * @author Shier
 * CreateTime 2023/5/14 9:23
 * 单例模式
 */
public class Singleton {
    private static Singleton singleton;

    /**
     * 无参构造 防止外部代码利用new来实例化的可能
     */
    private Singleton() {
    }

    /**
     * 只能通过此途径获取Singleton实例
     * @return
     */
    public static Singleton getInstance() {
        // 为空,则创建实例
        if (singleton == null) {
            singleton = new Singleton();
        }
        // 不为空,则已创建,直接返回实例
        return singleton;
    }
}

客户端:

/**
 * @author Shier
 * CreateTime 2023/5/14 9:27
 */
public class SingletonClient {
    public static void main(String[] args) {
        Singleton instance1 = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        if (instance1 == instance2) {
            System.out.println("两个是同一个实例对象");
        }
    }
}

最终得到的:“两个是同一个实例对象”,因此单例模式保证了可以被唯一实例化。当然单例模式不止这些好处。下面再来细说

2、具体例子说明

假设我们有一个流水号生成器,用于在系统中生成唯一的流水号。这个流水号生成器需要保证在整个系统中只有一个实例存在,以确保生成的流水号是唯一的。也就是类似于我们在外面吃饭点菜呀什么的,买单后,商家会给你一个单号,到你了,就让你去拿属于你的饭菜。

2.1 不使用单例模式

首先,我们创建一个名为SerialNumberGenerator的类,用于生成流水号:

/**
 * @author Shier
 * CreateTime 2023/5/14 9:30
 * 流水号例子
 */
public class SerialNumberGenerator {
    private int serialNumber;

    /**
     * 外界可以实例化SerialNumberGenerator
     */
    public SerialNumberGenerator() {
        serialNumber = 0;
    }

    public int generateSerialNumber() {
        serialNumber++;
        return serialNumber;
    }
}

现在,我们可以在不同的地方创建多个SerialNumberGenerator的实例并尝试生成流水号:

/**
 * @author Shier
 * CreateTime 2023/5/14 9:31
 * 客户端
 */
public class Main {
    public static void main(String[] args) {
        SerialNumberGenerator generator1 = new SerialNumberGenerator();
        int serialNumber1 = generator1.generateSerialNumber();
        System.out.println("流水号1: " + serialNumber1);

        SerialNumberGenerator generator2 = new SerialNumberGenerator();
        int serialNumber2 = generator2.generateSerialNumber();
        System.out.println("流水号2: " + serialNumber2);
    }
}

结果:

流水号1: 1
流水号2: 1

不使用单例模式存在的问题:

  1. 生成重复流水号:由于我们可以在不同的地方创建多个SerialNumberGenerator实例,每个实例都会有自己的serialNumber变量。这可能导致在不同的实例中生成相同的流水号,从而产生重复的流水号。
  2. 没有共享状态:每个SerialNumberGenerator实例都会维护自己的serialNumber变量,这导致每个实例生成的流水号不会互相影响。这意味着如果我们想要全局唯一的流水号,就需要在各个地方共享同一个SerialNumberGenerator实例,而不是创建多个实例。
  3. 难以管理和控制:在多个地方分别创建和管理SerialNumberGenerator实例会增加代码的复杂性。我们需要确保在需要流水号的每个地方都使用同一个实例,否则会出现上述问题。这增加了代码维护的难度,并且容易在系统的不同部分中出现错误使用的情况。
  4. 重复的流水号:如果多个地方都可以创建流水号生成器的实例,那么每个实例都有可能生成相同的流水号,导致重复的流水号出现。
  5. 资源浪费:如果每个部分都创建自己的流水号生成器实例,那么会造成资源的浪费,例如每个实例都需要独立维护和生成流水号的状态。
  6. 一致性问题:如果多个流水号生成器实例同时生成流水号,可能会导致不同实例生成的流水号在顺序上出现混乱。

2.2 使用单例模式

使用单例模式的实现方式有多种,这里我将使用线程安全的懒汉式单例模式,具体实现如下:

懒汉式单例模式下面再介绍

/**
 * @author Shier
 */
public class SerialNumberGenerator1 {
    private static volatile SerialNumberGenerator instance;
    private int serialNumber;

    /**
     * 防止直接实例化SerialNumberGenerator1
     */
    private SerialNumberGenerator1() {
        serialNumber = 0;
    }

    /**
     * 获取SerialNumberGenerator1的唯一方式
     * @return
     */
    public static SerialNumberGenerator getInstance() {
        if (instance == null) {
            synchronized (SerialNumberGenerator.class) {
                if (instance == null) {
                    instance = new SerialNumberGenerator();
                }
            }
        }
        return instance;
    }

    /**
     * 生成的流水号
     * @return
     */
    public int generateSerialNumber() {
        synchronized (SerialNumberGenerator.class) {
            serialNumber++;
            return serialNumber;
        }
    }
}

在这个实现中,我们使用了一个静态变量instance来保存SerialNumberGenerator的唯一实例。getInstance()方法返回这个实例,如果它不存在,则会创建一个新的实例。generateSerialNumber()方法负责生成流水号,使用synchronized关键字来确保线程安全。

现在,我们可以在不同的地方调用SerialNumberGenerator.getInstance().generateSerialNumber()来生成流水号,并确保它们是唯一的:

/**
 * @author Shier
 */
public class Main1 {
    public static void main(String[] args) {
        int serialNumber1 = SerialNumberGenerator1.getInstance().generateSerialNumber();
        System.out.println("流水号 1: " + serialNumber1);

        int serialNumber2 = SerialNumberGenerator1.getInstance().generateSerialNumber();
        System.out.println("流水号 2: " + serialNumber2);
    }
}

通过使用单例模式,我们解决了不使用单例模式存在的问题:

  1. 确保唯一性:SerialNumberGenerator的唯一实例可以在系统中被多次调用,但它们都会引用同一个实例,因此可以保证生成的流水号是唯一的。
  2. 共享状态:通过使用单例模式,所有生成的流水号共享同一个serialNumber变量,从而避免了重复生成流水号的问题。
  3. 管理和控制:由于只有一个SerialNumberGenerator实例存在于系统中,因此我们不必在各个地方管理和控制实例的创建和使用,这降低了代码复杂性并减少了出错的可能性。

使用单例模式来实现流水号生成器类,可以确保生成的流水号是唯一的,避免资源浪费,并保证生成的流水号顺序一致。所有部分都可以通过访问流水号生成器的全局访问点来获取唯一的流水号,从而简化了流水号的生成和管理。

3、单例模式实现方式

在上面例子当中使用到了懒汉式单例模式,下面我们再来看看单例模式的实现方式:

单例模式的实现方式有多种,以下是几种常见的实现方式:

  1. 懒汉式(Lazy Initialization):在首次使用时创建实例。懒汉式单例模式可以延迟实例化,避免不必要的资源消耗。但需要考虑线程安全性,可以通过加锁或使用双重检查锁定等方式来确保线程安全。
  2. 饿汉式(Eager Initialization):在类加载时即创建实例。饿汉式单例模式在类加载时就创建实例,因此不存在线程安全问题。但可能会提前创建实例,造成资源浪费。
  3. 静态内部类(Static Inner Class):利用类的静态内部类来实现单例模式。这种方式可以实现延迟加载和线程安全,且对外部类的加载不产生影响。
  4. 枚举(Enum):利用枚举类型实现单例模式。枚举类型的实例是线程安全的,并且保证只有一个实例存在。
  5. 双重检查锁定(Double-Checked Locking):结合懒汉式和加锁机制,通过双重检查来实现延迟加载和线程安全。
  6. 容器(Container):使用容器来存储实例,每次获取实例时先从容器中查找,如果不存在则创建并添加到容器中。这种方式适用于需要管理多个单例对象的情况。

以上是常见的几种单例模式的实现方式。每种实现方式都有其适用的场景和特点,选择哪种方式取决于具体的需求和设计考虑。同时,需要注意线程安全性和性能等因素,确保实现的单例模式符合要求。

下面再说说最常用的懒汉式和饿汉式单例模式实现

3.1 懒汉式单例模式

在第一次使用时才创建实例,称为懒汉式。实现方式是在类内部定义一个私有的静态变量作为实例,然后提供一个公有的静态方法来获取该实例。在方法内部判断实例是否已经存在,如果存在则直接返回,如果不存在则创建一个新的实例并返回。(我就是很懒,你不用我,我就不管你,当你用我的时候才回去给你提供需要的东西)

public class Singleton {
    private static Singleton instance;
    // 私有构造函数
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

在懒汉式单例模式中,我们将单例对象的实例化放在静态方法getInstance()中。当首次调用getInstance()时,会检查实例是否已经存在。如果实例为null,表示还未创建,则进行实例化。这种方式实现了延迟加载,只有在需要使用单例对象时才会创建它。

需要注意的是,在多线程环境下,懒汉式单例模式需要考虑线程安全性。上述代码的实现并未考虑线程安全,可能会导致多个线程同时创建实例。为了解决这个问题,可以在getInstance()方法中添加线程同步机制,如使用synchronized关键字或者双重检查锁定等方式,来确保在多线程环境下仅有一个实例被创建。

下面再来说多线程单例模式

3.2 饿汉式单例模式

饿汉式单例模式: 在类加载时就创建实例,称为饿汉式。实现方式是在类内部定义一个私有的静态变量,并直接创建实例赋值给它,然后提供一个公有的静态方法来获取该实例。(快要饿死了,很需要吃东西,所以说菜一上来就立马开吃,不管你三七二十四)

public class Singleton {
    private static final Singleton instance = new Singleton();
    // 私有构造函数
    private Singleton() {
    }
    public static Singleton getInstance() {
        return instance;
    }
}

在饿汉式单例模式中,我们将单例对象的实例化放在静态常量中,并且将构造函数设为私有,防止外部代码创建实例。在类加载时,静态常量instance就会被创建,并且通过公共的静态方法getInstance()返回该实例。由于实例在类加载时就被创建,因此可以保证单例的唯一性。

3.3 多线程的单例模式

3.3.1 同步方法

同步方法(Synchronized Method):在getInstance()方法上使用synchronized关键字,确保在同一时间只有一个线程可以进入方法,从而避免并发创建多个实例。这种方式简单易行,但可能存在性能问题,因为每次调用getInstance()都需要进行同步。

public class Singleton {
    private static Singleton instance;
    // 私有构造函数
    private Singleton() {
    }
    public synchronized static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

3.3.2 双重检查锁定

双重检查锁定(Double-Checked Locking):使用双重检查锁定机制,在getInstance()方法内使用synchronized块,只在实例为null时才进行同步,避免了每次调用都进行同步的开销。这种方式在多线程环境下能够保证线程安全性,同时也具有较好的性能。

public class Singleton {
    private static volatile Singleton instance;
    // 私有构造函数
    private Singleton() {
    }
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

需要注意的是,这种方式需要将instance变量声明为volatile,以确保在多线程环境下的可见性和禁止指令重排序。

3.3.3 静态内部类

静态内部类(Static Inner Class):利用类的静态内部类来实现单例模式。静态内部类在首次使用时加载,且只加载一次,因此保证了线程安全性和延迟加载。

public class Singleton {
    private Singleton() {
        // 私有构造函数
    }

    private static class SingletonHolder {
        private static final Singleton instance = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
}

在多线程环境下,上述实现方式都能够确保只有一个实例被创建,并提供线程安全的访问。选择哪种方式取决于具体的需求和性能要求。此外,还可以结合其他技术,如使用枚举类型实现单例,因为枚举类型的实例是线程安全且唯一的。

需要注意的是,在某些情况下,可能需要权衡线程安全和性能之间的取舍。在高并发环境下,可以考虑使用其他的并发控制方式,如使用锁、使用线程安全的并发容器等来实现单例模式。

4、单例模式总结

单例模式的核心思想是将类的实例化过程控制在一个特定的范围内,以确保只有一个实例被创建并且全局可访问。这种模式在需要共享资源或避免重复创建相同对象的场景中非常有用。

以下是单例模式的一般实现步骤:

  1. 类的构造函数设为私有,防止外部代码直接创建实例。
  2. 类内部创建一个私有的静态变量来保存单例实例
  3. 提供一个公共的静态方法来获取单例实例,该方法负责实例的创建和返回。
  4. 在获取实例的方法中,需要考虑线程安全性,确保在多线程环境下只有一个实例被创建。

单例模式的优点:

  1. 全局唯一性:确保只有一个实例存在,全局范围内可以访问该实例。
  2. 节省资源:避免重复创建相同的对象,节省了系统资源。
  3. 简化调用:通过单例模式可以将实例的管理和控制集中处理,简化了代码的使用和调用方式。

单例模式缺点:

  1. 隐藏依赖关系:单例模式将对象的创建和使用耦合在一起,使得对象之间的依赖关系难以识别和管理。其他组件或类可能会依赖单例对象,从而增加了代码的复杂性和耦合度。
  2. 可测试性降低:由于单例对象的全局性质,可能导致测试困难。在单元测试中,如果依赖于单例对象的方法或类,可能会难以模拟和替换实例,从而影响测试的可靠性和可维护性。
  3. 线程安全问题:在多线程环境下,需要特别注意单例模式的线程安全性。如果实现不当,可能导致多个线程同时创建实例,从而破坏了单例的唯一性。需要额外的同步措施来确保线程安全,这可能会引入性能开销。
  4. 限制扩展性:单例模式通常是通过将实例化逻辑封装在类内部来实现的,这限制了对类的扩展性。如果需要扩展功能或修改实例化逻辑,可能需要修改单例类的代码,从而影响其他代码的稳定性和可维护性。
  5. 对象生命周期管理困难:由于单例对象的生命周期长于其他对象,可能会导致内存泄漏问题。一旦单例对象被创建,它将一直存在于内存中,直到应用程序结束或显式销毁。在某些情况下,可能难以控制和管理单例对象的生命周期。

因此,应谨慎使用单例模式,并根据具体需求和设计考虑选择适当的实现方式,以确保单例的正确性和适用性。

单例模式使用场景:

  1. 全局资源共享:当系统中有多个模块或对象需要共享同一个资源时,可以使用单例模式来管理该资源,确保只有一个实例存在。例如,数据库连接池、线程池、日志管理器等。
  2. 配置信息管理:当需要全局访问和管理配置信息时,可以使用单例模式来存储和获取配置信息。这样可以避免重复读取配置文件,提高性能和效率。
  3. 日志记录器:在需要记录系统日志的场景下,使用单例模式可以确保日志记录器的唯一性和全局访问性。通过单例模式,可以集中管理日志记录器的配置和输出。
  4. 缓存管理:在需要管理全局缓存的场景下,可以使用单例模式来实现缓存管理器。单例模式确保只有一个缓存管理器实例存在,避免重复创建和维护多个缓存实例。
  5. 线程池管理:在多线程环境中,使用单例模式来管理线程池可以确保线程池的唯一性和可控性。通过单例模式,可以统一管理线程池的创建、销毁和任务调度。
  6. GUI应用程序中的窗口管理:在图形用户界面(GUI)应用程序中,使用单例模式来管理窗口对象可以确保每个窗口只有一个实例,方便对窗口进行控制和管理。