引言
- 设计模式概述
- 单例模式简介
基本概念
单例模式是一种设计模式,它确保一个类只能创建一个实例,并提供一个全局访问点来获取该实例。
单例模式的作用是确保在应用程序中只有一个实例对象存在,从而节省资源并提高性能。它常用于以下情况:
- 当一个类的实例化过程非常昂贵或复杂时,使用单例模式可以避免重复创建对象,提高性能。
- 当需要共享某个资源或数据时,例如日志文件、数据库连接等,使用单例模式可以确保只有一个实例访问这些资源,避免冲突和资源浪费。
- 当希望控制某个类的实例个数时,例如线程池、缓存管理器等,使用单例模式可以限制实例化的数量。
单例模式具有以下特点:
- 只能有一个实例:单例模式确保一个类只有一个实例对象存在。
- 全局访问点:单例模式通过提供一个全局访问点来获取该实例,方便其他对象使用。
- 延迟实例化:单例模式可以延迟实例化对象,即在第一次使用时才创建实例。
- 线程安全性:单例模式可以提供线程安全的访问方式,避免多个线程同时创建实例或导致数据不一致的问题。
- 防止继承:单例模式可以通过将构造函数定义为私有,防止其他类继承该类并创建多个实例。
实现方式
饿汉式
- 概述
- 实现步骤
- 优缺点分析
java
// 饿汉式单例类
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
懒汉式
概述
懒汉式是一种常用的单例模式实现方式。它与饿汉式不同,饿汉式在类加载时就立即创建实例,而懒汉式则是在第一次使用时才创建实例。
实现步骤
懒汉式的实现步骤如下:
- 定义一个私有静态变量来保存单例实例,并将其初始化为 null。
- 定义一个公有静态方法来获取单例实例,在该方法中检查实例是否已经被创建。如果未被创建,则创建一个新实例并将其赋值给静态变量,最后返回实例。
- 将类的构造函数定义为私有。这样做可以防止其他类直接通过 new 操作符来实例化该类的对象。
- 懒汉式需要注意多线程环境下的线程安全性问题,需要在获取单例实例的方法上加锁来保证线程安全。
优缺点分析
懒汉式单例模式具有以下优缺点:
优点:
- 延迟实例化:懒汉式可以在第一次使用时才创建实例,节省了内存空间和系统资源。
- 线程安全:通过在获取单例实例的方法上加锁,可以保证在多线程环境下线程安全。
缺点:
- 性能受限:由于在获取实例时需要加锁,因此会降低程序的性能。
- 可能存在线程安全性问题:虽然在获取单例实例的方法上加锁可以保证线程安全,但是在并发量大的情况下,会出现效率低下和阻塞等问题。
- 代码复杂度高:懒汉式需要考虑线程安全问题,因此代码实现较为复杂。
综上所述,懒汉式单例模式适用于类的实例化过程比较耗资源、并且不需要频繁调用的情况下。在多线程环境下需要考虑线程安全问题,通常需要在获取实例的方法上加锁,导致程序的性能有所降低。
懒汉式单例模式的示例代码:
java
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
双重检查锁(DCL)
概述
双重检查锁(Double-Checked Locking)是一种在懒汉式基础上改进的单例模式实现方式。它通过使用双重检查来减少锁的使用次数,提高了性能。
实现步骤
双重检查锁的实现步骤如下:
- 定义一个私有静态变量来保存单例实例,并将其初始化为 null。
- 定义一个公有静态方法来获取单例实例。在该方法中首先进行一次判空操作,如果实例已经被创建,则直接返回实例。如果未被创建,则加锁,并进行第二次判空操作。在锁内部再次判断实例是否为空,是则创建新实例并将其赋值给静态变量,最后返回实例。
- 将类的构造函数定义为私有。这样做可以防止其他类直接通过 new 操作符来实例化该类的对象。
下面是一个使用双重检查锁的单例模式示例代码:
java
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
关键字修饰。这是因为在多线程环境下,由于指令重排序等原因,可能会导致双重检查失效,从而造成获取到一个未完全初始化的实例。
优缺点分析
双重检查锁单例模式具有以下优缺点:
优点:
- 延迟实例化:双重检查锁可以在第一次使用时才创建实例,节省了内存空间和系统资源。
- 减少锁使用次数:通过双重检查,可以减少对锁的使用次数,提高了性能。
- 线程安全:通过使用锁来确保只有一个线程执行实例创建代码块,可以保证线程安全。
缺点:
- 可能存在线程安全性问题:尽管使用了双重检查锁来提高性能,但在某些情况下仍然可能出现并发问题。如果在创建实例的过程中,有其他线程进入了第一个判空条件,那么它们将直接返回一个未完全初始化的实例。
- 代码复杂度较高:双重检查锁的实现相对复杂,需要考虑线程安全以及使用
volatile
关键字来确保可见性。
综上所述,双重检查锁是一种在懒汉式单例模式基础上的改进,能够提供延迟实例化和减少锁使用次数的优势。在多线程环境下需要注意线程安全性问题,并且代码实现较为复杂。
静态内部类
概述
静态内部类是一种常用的单例模式实现方式。它利用了类加载机制和静态内部类的特性来实现延迟加载和线程安全。
实现步骤
静态内部类的实现步骤如下:
- 将类的构造函数定义为私有,防止其他类通过 new 操作符直接实例化该类的对象。
- 定义一个静态内部类,其中包含一个私有静态变量来保存单例实例,并在静态内部类中进行实例的创建。
- 提供一个公有静态方法来获取单例实例,在该方法中直接返回静态内部类中的实例。
下面是一个使用静态内部类的单例模式示例代码:
java
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
优缺点分析
静态内部类单例模式具有以下优缺点:
优点:
- 延迟实例化:静态内部类只有在第一次使用时才被加载,实现了延迟实例化。
- 线程安全:静态内部类在类加载时进行实例的创建,并且静态变量只会被初始化一次,保证了线程安全。
- 高效率:由于静态内部类的特性,只有在需要使用时才会加载,不会占用额外的系统资源。
缺点:
- 不支持传参的构造函数:静态内部类的实例创建是在类加载时进行的,无法通过构造函数传递参数。
综上所述,静态内部类单例模式是一种常用且高效的实现方式,它能够提供延迟实例化和线程安全的特性。但需要注意,静态内部类的实例无法传递参数给构造函数。
枚举类
概述
枚举类是一种特殊的类,用于定义一组有限的常量。在单例模式中,使用枚举类来实现单例是一种简洁、安全和可序列化的方式。
实现步骤
使用枚举类实现单例模式非常简单,只需要定义一个包含单个枚举常量的枚举类型即可。枚举常量就代表了单例的实例。
下面是使用枚举类实现单例模式的示例代码:
java
public enum Singleton {
INSTANCE;
// 可以在枚举类中定义其他方法和变量
public void doSomething() {
// ...
}
}
优缺点分析
枚举类单例模式具有以下优缺点:
优点:
- 简洁且易于理解:使用枚举类实现单例模式非常简洁,不需要考虑线程安全性、延迟加载等问题。
- 线程安全:枚举类的实例创建是在类加载时进行的,且由 JVM 确保只会创建一个实例,保证了线程安全。
- 防止反射攻击和序列化破坏:枚举类天生就具备防止反射攻击和序列化破坏单例的能力。
缺点:
- 无法延迟实例化:枚举类的实例是在类加载时创建的,无法实现延迟实例化。
- 无法通过构造函数传参:枚举类的实例创建时无法传递参数给构造函数。
综上所述,枚举类单例模式是一种简洁、安全和可序列化的实现方式。尽管无法实现延迟实例化,但使用枚举类可以有效地防止反射攻击和序列化破坏单例。适用于大多数单例场景。
使用场景
多线程环境下的单例模式
线程安全性问题
在多线程环境下,单例模式的实现可能会遇到线程安全性问题。主要有以下两个方面的问题:
-
并发创建多个实例:如果多个线程同时调用获取实例的方法,可能会导致每个线程都创建一个实例,违背了单例模式的初衷。
-
实例状态不一致:当多个线程并发地操作单例实例时,可能会引发状态竞争和不一致的问题,导致程序出错。
解决方案
为了在多线程环境下保证单例模式的线程安全性,可以采用以下几种解决方案:
-
懒汉模式加锁:在静态方法获取实例的代码块中使用 synchronized 关键字确保只有一个线程可以进入,从而避免并发创建多个实例。但是这种方式由于使用了锁机制,在高并发情况下会降低性能。
-
饿汉模式:在类加载时就创建好单例实例,保证了线程安全性。但是这种方式无法实现延迟加载,可能会浪费资源。
-
双重检查锁定(Double-Checked Locking):结合懒汉模式和饿汉模式的优点,在静态方法获取实例时先进行一次判断,如果实例已经被创建,则直接返回;如果没有被创建,再进行加锁创建实例。这种方式通过减少加锁的次数来提高性能,同时也实现了延迟加载和线程安全。
-
静态内部类:利用静态内部类的特性,在类加载时创建实例,保证了线程安全性和延迟加载。这种方式是一种常用且简单有效的解决方案。
需要根据具体的场景和需求选择适合的解决方案。在不同的应用场景中,可能会选择不同的线程安全的单例模式实现方式。
java
// 双重检查锁单例类 - 线程安全示例
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;
}
}
需要控制资源的访问权限
- 文件管理器示例
- 数据库连接池示例
java
// 文件管理器示例
public class FileManager {
private static final FileManager instance = new FileManager();
private static final int MAX_CONCURRENT_USERS = 5;
private int currentUsers;
private FileManager() {
currentUsers = 0;
}
public static FileManager getInstance() {
return instance;
}
public synchronized boolean grantAccess() {
if (currentUsers < MAX_CONCURRENT_USERS) {
currentUsers++;
return true;
}
return false;
}
public synchronized void releaseAccess() {
currentUsers--;
}
}
// 数据库连接池示例
public class ConnectionPool {
private static final ConnectionPool instance = new ConnectionPool();
private static final int MAX_CONNECTIONS = 10;
private int currentConnections;
private ConnectionPool() {
currentConnections = 0;
}
public static ConnectionPool getInstance() {
return instance;
}
public synchronized Connection getConnection() {
if (currentConnections < MAX_CONNECTIONS) {
currentConnections++;
// 创建新的连接并返回
}
return null;
}
public synchronized void releaseConnection(Connection connection) {
// 释放连接资源
currentConnections--;
}
}
总结
单例模式的优点包括:
- 保证只有一个实例存在,节省系统资源。
- 提供全局访问点,方便其他组件使用。
- 简化对象管理,集中处理对象创建和销毁。
- 控制实例化过程,可以延迟或按需创建对象。
- 可以确保单例对象的线程安全性。
单例模式的缺点包括:
- 过度使用单例模式可能导致代码复杂性增加。
- 单例对象的状态在全局可见,可能被误用。
- 单例对象较难进行单元测试。
- 单例模式可能违背了依赖倒置原则和单一职责原则。
适用场景总结:
- 需要保证全局唯一性的场景,如配置信息、日志记录器等。
- 需要频繁访问共享资源的场景,如数据库连接池、线程池等。
- 需要控制实例化过程的场景,如工厂模式。
- 需要提供全局访问点的场景,如事件发布/订阅系统。
- 需要确保线程安全性的场景,如多线程环境下的单例对象。