简介
单例模式是一种创建型设计模式,确保某个类仅有一个实例,并提供一个全局访问点来访问该实例。
在单例模式中,类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一对象的方式,允许直接访问而无需每次实例化该类的新对象。
主要场景
单例模式的主要应用场景包括:
- 需要频繁实例化然后销毁的对象。
- 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
- 有状态的工具类对象。
- 频繁访问数据库或文件的对象。
通过应用单例模式,我们可以控制实例的数量,节省系统资源,同时加快对象访问速度,尤其适合对象需要被公用的场合,例如多个模块使用同一个数据源连接对象等。
实现机制
实现单例模式的关键在于确保只有一个实例被创建,并提供一个全局访问点。这通常通过以下方式实现:
- 将构造函数设为私有,以防止其他类通过new操作符创建该类的实例。
- 在类内部创建一个静态的私有实例。
- 提供一个静态的公有方法,用于返回该类的唯一实例。如果该实例尚未创建,则通过调用私有构造函数来创建它;如果实例已经存在,则直接返回该实例。
注意多线程
需要注意的是,在多线程环境下,需要确保单例模式的线程安全性,以避免出现多个实例的情况。
单例模式确保线程安全的关键在于确保在多线程环境下,类的唯一实例能够被正确地创建和访问,而不会出现多个实例或者创建过程中的竞争条件。以下是一些常见的线程安全的单例模式实现方式:
饿汉式(静态初始化):
这种方式是最简单的线程安全的实现方式。因为静态初始化器由JVM在加载类时执行,且JVM保证类的加载过程是线程安全的,所以在多线程环境下,这种方式创建的实例也是线程安全的。
public class Singleton {// 在加载时就完成了初始化,所以类加载较慢,但获取对象的速度快private static Singleton instance = new Singleton();private Singleton() {}public static Singleton getInstance() {return instance;}
}
双重检查锁定(Double-Checked Locking):
双重检查锁定是一种优化技术,旨在减少使用同步的开销。它首先检查实例是否已经存在,如果不存在才进行同步。但是,在Java中,双重检查锁定需要正确地使用volatile关键字来确保可见性和禁止指令重排
public class Singleton { // volatile 保证多线程正确处理 instance 的可见性和禁止指令重排 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; }
}
在Java 5及以上版本中,双重检查锁定是可行的,但务必注意volatile关键字的正确使用。
静态内部类:
静态内部类实现单例模式的方式也是线程安全的。由于JVM的类加载机制保证了静态内部类只会被加载一次,从而保证了线程安全性。
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 whateverMethod() {// method body}
}
通过枚举实现单例模式不仅简单,而且自动支持序列化机制,防止反序列化重新创建新的对象。
初始化时同步(Initialization on Demand Holder, IODH):
这种方式结合了饿汉式和双重检查锁定的思想,但在实现上更为复杂,且在某些JVM上可能并不是线程安全的。因此,它不如上面提到的方法常用。
在选择单例模式的实现方式时,应该考虑代码的简洁性、性能需求以及线程安全性。通常情况下,使用枚举或静态内部类实现单例模式是比较推荐的方式,因为它们不仅线程安全,而且实现简单,易于理解。
实际例子
日志系统
假设我们正在开发一个日志记录系统,我们想要确保整个应用程序中只有一个日志记录器实例,这样我们就可以保持日志的一致性和管理方便。
public class Logger {// 私有构造方法,防止外部通过new创建实例private Logger() {// 初始化代码System.out.println("Logger is being initialized...");}// 静态内部类,持有单例的引用private static class LoggerHolder {// 静态初始化器,保证线程安全private static final Logger INSTANCE = new Logger();}// 获取单例的公共静态方法public static Logger getInstance() {// 返回LoggerHolder中持有的单例引用return LoggerHolder.INSTANCE;}// 日志记录方法public void log(String message) {System.out.println("[" + System.currentTimeMillis() + "] " + message);}// 示例:使用单例模式的Logger类public static void main(String[] args) {// 获取Logger单例Logger logger = Logger.getInstance();// 使用Logger实例记录日志logger.log("This is a log message.");// 尝试再次获取Logger实例,应该是同一个Logger anotherLogger = Logger.getInstance();// 验证两个引用是否指向同一对象System.out.println("Are loggers the same? " + (logger == anotherLogger));}
}
数据库连接池
我们手撸一个最简单的数据库连接池,保证在并发情况下的单例访问。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.concurrent.ConcurrentHashMap; public class ConnectionPoolManager { // 静态变量持有单例引用 private static ConnectionPoolManager instance; // 数据库连接池 private final ConcurrentHashMap<String, Connection> pool; // 初始化连接池的大小 private static final int MAX_CONNECTIONS = 10; // 当前已分配的连接数 private int currentConnections = 0; // 数据库连接信息 private static final String DB_URL = "jdbc:mysql://localhost:3306/mydb"; private static final String DB_USER = "username"; private static final String DB_PASSWORD = "password"; // 私有构造方法,防止外部通过new创建实例 private ConnectionPoolManager() { this.pool = new ConcurrentHashMap<>(); // 初始化连接池,预先创建一些连接 for (int i = 0; i < MAX_CONNECTIONS; i++) { try { Connection connection = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD); pool.put("connection" + i, connection); } catch (SQLException e) { e.printStackTrace(); } } } // 从连接池中获取一个连接 public synchronized Connection getConnection() { if (currentConnections < MAX_CONNECTIONS) { // 尝试从连接池中获取一个连接 for (String key : pool.keySet()) { Connection connection = pool.get(key); if (connection != null && !connection.isClosed()) { pool.remove(key); // 从连接池中移除,表示该连接已被使用 currentConnections++; return connection; } } // 如果连接池中没有可用连接,则创建新连接(此处简化处理,实际应检查是否超过最大连接数) try { Connection connection = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD); currentConnections++; return connection; } catch (SQLException e) { e.printStackTrace(); } } return null; // 如果没有可用连接,返回null } // 回收一个连接到连接池 public synchronized void releaseConnection(Connection connection) { if (connection != null && !connection.isClosed()) { pool.put("connection" + (MAX_CONNECTIONS - currentConnections), connection); currentConnections--; } } // 获取单例的公共静态方法,使用双重检查锁定确保线程安全 public static ConnectionPoolManager getInstance() { if (instance == null) { synchronized (ConnectionPoolManager.class) { if (instance == null) { instance = new ConnectionPoolManager(); } } } return instance; } // 示例:使用单例模式的ConnectionPoolManager类 public static void main(String[] args) { // 获取连接池管理器单例 ConnectionPoolManager connectionPoolManager = ConnectionPoolManager.getInstance(); // 从连接池获取一个连接 Connection connection = connectionPoolManager.getConnection(); if (connection != null) { // 使用连接执行数据库操作... System.out.println("Got a connection from the pool."); // 假设使用完连接后释放回连接池 connectionPoolManager.releaseConnection(connection); System.out.println("Released the connection back to the pool."); } else { System.out.println("No connections available in the pool."); } }
}
在这个例子中,ConnectionPoolManager 类负责管理一个数据库连接池。它使用了 ConcurrentHashMap 来存储连接,以便能够高效地处理并发请求。getConnection 方法尝试从连接池中获取一个可用连接,如果没有可用连接则尝试创建一个新连接(这里简化了处理逻辑,实际中可能需要考虑连接数是否已经达到最大值)。releaseConnection 方法则将使用完毕的连接回收回连接池。