前言
在软件开发中,有时我们需要确保某个类在整个应用程序中只有一个实例。例如,数据库连接池、线程池或者全局配置管理器等。单例模式(Singleton Pattern)就是为解决这一需求而设计的。
本文将详细介绍单例模式的概念、实现方式以及应用场景,帮助读者全面理解这一设计模式并能在实际开发中灵活应用。
单例模式简介
单例模式是 Java 中最简单的设计模式之一,属于创建型模式。它的核心思想是:确保一个类只有一个实例,并提供一个全局访问点来访问该实例。
单例模式的三个要点:
- 单例类只能有一个实例
- 单例类必须自己创建自己的唯一实例
- 单例类必须给所有其他对象提供这一实例
单例模式解决的问题
- 资源共享:当某些资源(如数据库连接、文件、硬件设备等)只能被一个实例使用时
- 控制实例数目:节省系统资源,避免重复创建销毁实例
- 全局访问点:提供一个全局访问点,方便访问实例
单例模式的实现方式
单例模式有多种实现方式,每种方式都有其优缺点,适用于不同的场景。下面我们将介绍六种常见的实现方式。
1. 懒汉式(线程不安全)
这是最基本的实现方式,但不支持多线程环境。
/**
* 懒汉式单例模式(线程不安全)
* 优点:延迟加载,第一次使用时才创建实例
* 缺点:多线程环境下可能创建多个实例
*/
public class Singleton {
// 私有静态变量,用于存储唯一实例
private static Singleton instance;
// 私有构造函数,防止外部实例化
private Singleton() {}
// 公共静态方法,返回唯一实例
public static Singleton getInstance() {
// 如果实例不存在,则创建新实例
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
优点:
- 延迟初始化,节省资源
- 实现简单
缺点:
- 线程不安全,在多线程环境下可能创建多个实例
2. 懒汉式(线程安全)
通过同步方法确保线程安全。
/**
* 懒汉式单例模式(线程安全)
* 优点:线程安全,保证只有一个实例
* 缺点:每次获取实例都需要同步,效率低
*/
public class Singleton {
private static Singleton instance;
private Singleton() {}
// 使用synchronized关键字实现线程安全
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
优点:
- 线程安全,保证只有一个实例
- 延迟初始化
缺点:
- 每次调用 getInstance()都需要同步,即使实例已经创建,效率较低
3. 饿汉式
在类加载时就创建实例,天然线程安全。
/**
* 饿汉式单例模式
* 优点:线程安全,实现简单
* 缺点:类加载时就创建实例,不管是否使用,可能造成资源浪费
*/
public class Singleton {
// 在类加载时就创建实例
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
优点:
- 线程安全,实现简单
- 没有加锁,执行效率高
缺点:
- 类加载时就初始化,不能实现延迟加载,可能造成资源浪费
4. 双重检查锁(DCL,Double-Checked Locking)
结合了懒汉式和饿汉式的优点,既能实现延迟加载,又能保证线程安全和效率。
/**
* 双重检查锁单例模式
* 优点:线程安全,延迟加载,效率较高
* 缺点:实现复杂,需要关注Java内存模型
*/
public class Singleton {
// 使用volatile关键字确保多线程环境下内存可见性
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
// 第一次检查,避免不必要的同步
if (instance == null) {
// 同步锁,确保线程安全
synchronized (Singleton.class) {
// 第二次检查,避免重复创建实例
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
优点:
- 线程安全
- 延迟加载
- 效率较高,只有第一次创建实例时需要加锁
缺点:
- 实现复杂
- 需要使用 volatile 关键字,保证内存可见性
5. 静态内部类
利用类加载机制确保线程安全,同时实现延迟加载。
/**
* 静态内部类单例模式
* 优点:线程安全,延迟加载,实现简单
* 缺点:不能传递参数
*/
public class Singleton {
private Singleton() {}
// 静态内部类负责创建单例实例
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
// 当第一次调用getInstance()时,才会加载SingletonHolder类
// 从而初始化INSTANCE
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
优点:
- 线程安全,利用类加载机制保证单例
- 延迟加载,调用 getInstance()时才加载内部类
- 实现简单
缺点:
- 不容易理解
- 不能传递参数
6. 枚举
最简单的实现方式,自动支持序列化,绝对防止多次实例化。
/**
* 枚举单例模式
* 优点:线程安全,防止反射和序列化攻击,实现最简单
* 缺点:无法实现延迟加载
*/
public enum Singleton {
INSTANCE;
// 添加你需要的方法
public void doSomething() {
System.out.println("Singleton is doing something");
}
}
优点:
- 线程安全
- 防止反射、序列化攻击
- 实现最简单
缺点:
- 无法实现延迟加载
- 在某些场景下使用枚举可能不太自然
实际应用场景
单例模式在实际开发中有很多应用场景,下面列举几个常见的例子:
1. 配置管理
/**
* 应用配置管理器(单例)
*/
public class ConfigManager {
private static final ConfigManager instance = new ConfigManager();
private Properties properties;
private ConfigManager() {
properties = new Properties();
try {
properties.load(new FileInputStream("config.properties"));
} catch (IOException e) {
e.printStackTrace();
}
}
public static ConfigManager getInstance() {
return instance;
}
public String getProperty(String key) {
return properties.getProperty(key);
}
}
2. 数据库连接池
/**
* 简单的数据库连接池(单例)
*/
public class DBConnectionPool {
private static volatile DBConnectionPool instance;
private List<Connection> connectionPool;
private DBConnectionPool() {
connectionPool = new ArrayList<>();
// 初始化连接池
for (int i = 0; i < 10; i++) {
try {
connectionPool.add(createConnection());
} catch (SQLException e) {
e.printStackTrace();
}
}
}
public static DBConnectionPool getInstance() {
if (instance == null) {
synchronized (DBConnectionPool.class) {
if (instance == null) {
instance = new DBConnectionPool();
}
}
}
return instance;
}
private Connection createConnection() throws SQLException {
// 创建数据库连接的代码
return DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "password");
}
public synchronized Connection getConnection() {
if (connectionPool.isEmpty()) {
try {
return createConnection();
} catch (SQLException e) {
e.printStackTrace();
return null;
}
}
return connectionPool.remove(connectionPool.size() - 1);
}
public synchronized void releaseConnection(Connection connection) {
connectionPool.add(connection);
}
}
3. 日志管理器
/**
* 日志管理器(单例)
*/
public class LogManager {
private static class LogManagerHolder {
private static final LogManager INSTANCE = new LogManager();
}
private FileWriter logWriter;
private LogManager() {
try {
logWriter = new FileWriter("application.log", true);
} catch (IOException e) {
e.printStackTrace();
}
}
public static LogManager getInstance() {
return LogManagerHolder.INSTANCE;
}
public synchronized void log(String message) {
try {
logWriter.write(new Date() + ": " + message + "\n");
logWriter.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
// 关闭日志
public void close() {
try {
if (logWriter != null) {
logWriter.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
单例模式的注意事项
使用单例模式时,有一些常见问题需要注意:
1. 线程安全问题
确保在多线程环境下只创建一个实例。前面介绍的懒汉式(线程安全)、饿汉式、双重检查锁、静态内部类和枚举方式都能解决这个问题。
2. 序列化问题
当单例类实现了 Serializable 接口时,反序列化可能会创建新的实例。解决方案:
public class Singleton implements Serializable {
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
// 防止反序列化创建新的实例
protected Object readResolve() {
return getInstance();
}
}
3. 反射攻击
反射机制可以访问私有构造函数,从而创建多个实例。解决方案:
public class Singleton {
private static Singleton instance = new Singleton();
// 在构造函数中检查实例是否已存在
private Singleton() {
if (instance != null) {
throw new RuntimeException("单例已存在,不能重复创建");
}
}
public static Singleton getInstance() {
return instance;
}
}
枚举实现天然防止反射攻击。
4. 性能考虑
- 如果单例创建开销较大且可能不被使用,考虑使用懒加载方式(懒汉式、双重检查锁或静态内部类)
- 如果单例一定会被使用且创建成本不高,可以使用饿汉式或枚举方式
单例模式变种
1. 有限多例模式
有时我们需要创建有限数量的实例,而不是单个实例。
/**
* 连接池实现(有限多例)
*/
public class ConnectionPool {
private static final int MAX_CONNECTIONS = 10;
private static List<Connection> instances = new ArrayList<>();
static {
for (int i = 0; i < MAX_CONNECTIONS; i++) {
instances.add(new Connection());
}
}
private ConnectionPool() {}
public static Connection getConnection() {
synchronized (instances) {
if (instances.isEmpty()) {
try {
// 等待连接释放
instances.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return instances.remove(0);
}
}
public static void releaseConnection(Connection connection) {
synchronized (instances) {
instances.add(connection);
instances.notify(); // 唤醒等待线程
}
}
// 模拟连接类
public static class Connection {
private Connection() {}
}
}
2. 线程内单例
每个线程有自己的单例实例,不同线程的实例彼此独立。
/**
* 线程内单例
*/
public class ThreadLocalSingleton {
private static final ThreadLocal<ThreadLocalSingleton> instance =
ThreadLocal.withInitial(() -> new ThreadLocalSingleton());
private ThreadLocalSingleton() {}
public static ThreadLocalSingleton getInstance() {
return instance.get();
}
}
结语
单例模式是一种简单而强大的设计模式,它确保一个类只有一个实例,并提供全局访问点。在实际应用中,我们需要根据具体需求选择合适的实现方式。
推荐使用方式:
- 一般情况下,建议使用静态内部类方式,它兼顾了线程安全、延迟加载和实现简单的优点
- 如果需要防止反射和序列化攻击,可以使用枚举方式
- 对于资源敏感的应用,可以考虑双重检查锁方式
无论选择哪种实现方式,理解单例模式的本质和各种实现方式的优缺点,对于编写高质量的代码至关重要。