【03】【创建型】【聊一聊,单例模式】

1 概述

1.1 简介

单例模式是一种创建型设计模式,其核心目的是确保一个类在整个应用程序生命周期中仅有一个实例对象,并提供一个全局统一的访问入口来获取该实例。这种模式常用于管理共享资源(如配置文件、数据库连接池、日志器等),避免因多次创建实例导致的资源浪费、状态不一致等问题。

单例模式的核心约束:

  • 构造方法私有化(禁止外部通过 new 关键字创建实例);
  • 提供静态方法作为全局访问入口(返回唯一实例);
  • 确保实例创建的线程安全性(多线程环境下避免创建多个实例);
  • 防止序列化/反序列化破坏单例(可选,视场景需求)。

1.2 主要角色

单例模式的角色结构极简,仅包含一个核心角色:

  • 单例类(Singleton Class)
    1. 私有化自身构造方法,阻止外部实例化;
    2. 内部维护一个私有的、静态的自身实例(唯一实例);
    3. 提供一个公有的、静态的方法(如 getInstance()),返回内部维护的唯一实例。

无其他辅助角色,通过类自身的结构设计实现单例约束。

1.3 优点

  1. 资源复用:避免重复创建实例,减少内存占用和系统资源消耗(如数据库连接、线程池等重量级资源);
  2. 状态统一:全局仅有一个实例,确保所有代码访问的是同一对象,避免多实例导致的状态不一致问题;
  3. 全局访问:提供统一的访问入口,简化代码中对共享资源的调用逻辑;
  4. 生命周期可控:单例实例的创建、初始化、销毁可集中管理,便于维护(如懒加载初始化、主动销毁资源)。

1.4 缺点

  1. 违背单一职责原则:单例类既负责自身的业务逻辑,又负责实例的创建和管理,职责耦合;
  2. 扩展性差:单例类通常没有接口(或难以通过接口扩展),若后续需要替换实例类型(如不同环境的配置类),需修改原有代码,违背开闭原则;
  3. 线程安全风险:若实现不当(如懒加载未加锁),多线程环境下可能创建多个实例,破坏单例特性;
  4. 测试困难:单例实例全局共享,可能导致测试用例之间相互干扰(如测试中修改了单例状态,影响后续测试);
  5. 序列化问题 :默认情况下,可序列化的单例类反序列化时会创建新实例,需额外处理(如重写 readResolve() 方法)。

2 实现

2.1 类图(mermaid语法)

单例模式的类图核心是体现"私有构造方法"和"静态实例+静态访问方法":
Singleton - static Singleton instance // 私有静态实例(唯一实例) -Singleton() +static Singleton getInstance() +businessMethod()

2.2 示例代码

单例模式有多种实现方式,以下是最常用的两种(懒汉式-线程安全、饿汉式):

(1)饿汉式(线程安全,立即加载)

核心逻辑:类加载时直接创建实例,借助JVM类加载机制保证线程安全(类加载过程是线程安全的)。

java 复制代码
public class HungrySingleton {
    // 1. 私有静态实例(类加载时初始化,唯一实例)
    private static final HungrySingleton INSTANCE = new HungrySingleton();

    // 2. 私有构造方法(禁止外部new)
    private HungrySingleton() {
        // 防止通过反射破坏单例(可选)
        if (INSTANCE != null) {
            throw new IllegalStateException("单例实例已存在,禁止重复创建");
        }
    }

    // 3. 公有静态访问方法(返回唯一实例)
    public static HungrySingleton getInstance() {
        return INSTANCE;
    }

    // 业务方法示例
    public void doSomething() {
        System.out.println("饿汉式单例执行业务逻辑");
    }
}

特点:简单高效、线程安全;但类加载时就创建实例,若实例未被使用会造成资源浪费(适合实例创建成本低的场景)。

(2)懒汉式(线程安全,延迟加载)

核心逻辑 :实例在第一次调用 getInstance() 时创建(延迟加载),通过 synchronized 关键字保证多线程安全。

java 复制代码
public class LazySingleton {
    // 1. 私有静态实例(volatile修饰,防止指令重排导致的线程安全问题)
    private static volatile LazySingleton instance;

    // 2. 私有构造方法(禁止外部new)
    private LazySingleton() {
        // 防止反射破坏单例(可选)
        if (instance != null) {
            throw new IllegalStateException("单例实例已存在,禁止重复创建");
        }
    }

    // 3. 公有静态访问方法(双重检查锁定,确保线程安全且高效)
    public static LazySingleton getInstance() {
        // 第一次检查:未加锁,快速判断实例是否已创建(避免每次加锁开销)
        if (instance == null) {
            // 加锁:保证同一时刻只有一个线程进入创建逻辑
            synchronized (LazySingleton.class) {
                // 第二次检查:防止多个线程等待锁后重复创建实例
                if (instance == null) {
                    instance = new LazySingleton(); // volatile防止指令重排
                }
            }
        }
        return instance;
    }

    // 业务方法示例
    public void doSomething() {
        System.out.println("懒汉式单例执行业务逻辑");
    }
}

特点 :延迟加载(节省资源)、线程安全;双重检查锁定(DCL)保证高效性(避免每次调用都加锁);volatile 关键字必须加(防止JVM指令重排导致的"半初始化实例"问题)。

3 具体应用

3.1 应用场景1:数据库连接池管理

背景:数据库连接是重量级资源(创建连接需消耗CPU、网络资源),若每次数据库操作都创建新连接,会导致系统性能下降、数据库负载过高。通过单例模式管理连接池,确保全局只有一个连接池实例,统一分配和回收连接。

实现示例

java 复制代码
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

// 数据库连接池单例类
public class DBConnectionPool {
    // 1. 单例实例(懒汉式,双重检查锁定)
    private static volatile DBConnectionPool instance;

    // 连接池配置
    private static final String URL = "jdbc:mysql://localhost:3306/test_db";
    private static final String USER = "root";
    private static final String PASSWORD = "123456";
    private static final int POOL_SIZE = 5; // 连接池初始大小

    // 存储连接的集合
    private List<Connection> connectionPool;

    // 2. 私有构造方法(初始化连接池)
    private DBConnectionPool() {
        connectionPool = new ArrayList<>(POOL_SIZE);
        // 初始化连接池,创建指定数量的数据库连接
        for (int i = 0; i < POOL_SIZE; i++) {
            try {
                Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
                connectionPool.add(conn);
            } catch (SQLException e) {
                throw new RuntimeException("创建数据库连接失败", e);
            }
        }
    }

    // 3. 单例访问方法
    public static DBConnectionPool getInstance() {
        if (instance == null) {
            synchronized (DBConnectionPool.class) {
                if (instance == null) {
                    instance = new DBConnectionPool();
                }
            }
        }
        return instance;
    }

    // 从连接池获取连接
    public synchronized Connection getConnection() {
        if (connectionPool.isEmpty()) {
            throw new RuntimeException("连接池无可用连接");
        }
        // 移除并返回最后一个连接(简化实现,实际可优化为队列)
        return connectionPool.remove(connectionPool.size() - 1);
    }

    // 归还连接到连接池
    public synchronized void releaseConnection(Connection conn) {
        if (conn != null) {
            connectionPool.add(conn);
        }
    }
}

// 使用示例
public class DBTest {
    public static void main(String[] args) {
        // 获取唯一的连接池实例
        DBConnectionPool pool = DBConnectionPool.getInstance();

        // 多线程获取连接(验证单例特性)
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                Connection conn = pool.getConnection();
                System.out.println(Thread.currentThread().getName() + " 获取连接:" + conn);
                // 业务操作...
                pool.releaseConnection(conn);
                System.out.println(Thread.currentThread().getName() + " 归还连接:" + conn);
            }).start();
        }
    }
}

核心价值:全局唯一的连接池统一管理连接,避免连接重复创建/销毁,提升系统性能和数据库稳定性。

3.2 应用场景2:系统配置管理器

背景:系统配置(如接口地址、超时时间、日志级别等)通常存储在配置文件(如application.properties、yaml)中,配置信息全局共享且无需频繁修改。通过单例模式创建配置管理器,加载一次配置后全局复用,避免重复读取文件。

实现示例

java 复制代码
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

// 系统配置管理器单例类
public class ConfigManager {
    // 1. 单例实例(饿汉式,类加载时初始化)
    private static final ConfigManager INSTANCE = new ConfigManager();

    // 存储配置信息的Properties对象
    private Properties configProps;

    // 2. 私有构造方法(加载配置文件)
    private ConfigManager() {
        configProps = new Properties();
        // 加载classpath下的配置文件
        try (InputStream is = ConfigManager.class.getClassLoader().getResourceAsStream("application.properties")) {
            if (is == null) {
                throw new RuntimeException("配置文件application.properties不存在");
            }
            configProps.load(is);
        } catch (IOException e) {
            throw new RuntimeException("加载配置文件失败", e);
        }
    }

    // 3. 单例访问方法
    public static ConfigManager getInstance() {
        return INSTANCE;
    }

    // 获取配置值(String类型)
    public String getConfig(String key) {
        return configProps.getProperty(key);
    }

    // 获取配置值(int类型,带默认值)
    public int getIntConfig(String key, int defaultValue) {
        String value = configProps.getProperty(key);
        return value != null ? Integer.parseInt(value) : defaultValue;
    }

    // 获取配置值(boolean类型,带默认值)
    public boolean getBoolConfig(String key, boolean defaultValue) {
        String value = configProps.getProperty(key);
        return value != null ? Boolean.parseBoolean(value) : defaultValue;
    }
}

// 配置文件(application.properties)
/*
api.base.url=http://api.example.com
timeout=5000
log.enable=true
*/

// 使用示例
public class ConfigTest {
    public static void main(String[] args) {
        // 获取唯一的配置管理器实例
        ConfigManager config = ConfigManager.getInstance();

        // 全局各处可直接获取配置,无需重复加载文件
        String apiUrl = config.getConfig("api.base.url");
        int timeout = config.getIntConfig("timeout", 3000);
        boolean logEnable = config.getBoolConfig("log.enable", false);

        System.out.println("接口地址:" + apiUrl);
        System.out.println("超时时间:" + timeout + "ms");
        System.out.println("日志启用:" + logEnable);
    }
}

核心价值:配置文件仅加载一次,减少IO开销;全局统一访问配置,避免配置不一致;便于后续扩展配置更新逻辑(如热更新)。

相关推荐
222you4 小时前
SpringIOC的注解开发
java·开发语言
William_cl4 小时前
【CSDN 专栏】C# ASP.NET Razor 视图引擎实战:.cshtml 从入门到避坑(图解 + 案例)
开发语言·c#·asp.net
charlie1145141914 小时前
深入理解CC++的编译与链接技术9:动态库细节
c语言·开发语言·c++·学习·动态库
god004 小时前
Selenium等待判断元素页面加载完成
java·开发语言
isyoungboy4 小时前
c++使用win新api替代DirectShow驱动uvc摄像头,可改c#驱动
开发语言·c++·c#
Dxy12393102165 小时前
python如何去掉字符串中最后一个字符
开发语言·python
云和数据.ChenGuang5 小时前
`post_max_size`、`max_execution_time`、`max_input_time` 是 **PHP 核心配置参数**
开发语言·mysql·php·zabbix·mariadb
听风吟丶5 小时前
Java HashMap 深度解析:从底层结构到性能优化实战
java·开发语言·性能优化
前端老曹5 小时前
vue3 三级路由无法缓存的终极解决方案
前端·javascript·vue.js·vue