简介
单例模式是一种创建型设计模式,确保某个类仅有一个实例,并提供一个全局访问点来访问该实例。
在单例模式中,类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一对象的方式,允许直接访问而无需每次实例化该类的新对象。
主要场景
单例模式的主要应用场景包括:
- 需要频繁实例化然后销毁的对象。
- 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
- 有状态的工具类对象。
- 频繁访问数据库或文件的对象。
通过应用单例模式,我们可以控制实例的数量,节省系统资源,同时加快对象访问速度,尤其适合对象需要被公用的场合,例如多个模块使用同一个数据源连接对象等。
实现机制
实现单例模式的关键在于确保只有一个实例被创建,并提供一个全局访问点。这通常通过以下方式实现:
- 将构造函数设为私有,以防止其他类通过new操作符创建该类的实例。
- 在类内部创建一个静态的私有实例。
- 提供一个静态的公有方法,用于返回该类的唯一实例。如果该实例尚未创建,则通过调用私有构造函数来创建它;如果实例已经存在,则直接返回该实例。
注意多线程
需要注意的是,在多线程环境下,需要确保单例模式的线程安全性,以避免出现多个实例的情况。
单例模式确保线程安全的关键在于确保在多线程环境下,类的唯一实例能够被正确地创建和访问,而不会出现多个实例或者创建过程中的竞争条件。以下是一些常见的线程安全的单例模式实现方式:
饿汉式(静态初始化):
这种方式是最简单的线程安全的实现方式。因为静态初始化器由JVM在加载类时执行,且JVM保证类的加载过程是线程安全的,所以在多线程环境下,这种方式创建的实例也是线程安全的。
java
public class Singleton {
// 在加载时就完成了初始化,所以类加载较慢,但获取对象的速度快
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
双重检查锁定(Double-Checked Locking):
双重检查锁定是一种优化技术,旨在减少使用同步的开销。它首先检查实例是否已经存在,如果不存在才进行同步。但是,在Java中,双重检查锁定需要正确地使用volatile关键字来确保可见性和禁止指令重排
java
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的类加载机制保证了静态内部类只会被加载一次,从而保证了线程安全性。
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中,枚举类型是线程安全的,并且能防止反序列化重新创建新的对象,所以使用枚举实现单例模式也是线程安全的最佳实践。
java
public enum Singleton {
INSTANCE;
public void whateverMethod() {
// method body
}
}
通过枚举实现单例模式不仅简单,而且自动支持序列化机制,防止反序列化重新创建新的对象。
初始化时同步(Initialization on Demand Holder, IODH):
这种方式结合了饿汉式和双重检查锁定的思想,但在实现上更为复杂,且在某些JVM上可能并不是线程安全的。因此,它不如上面提到的方法常用。
在选择单例模式的实现方式时,应该考虑代码的简洁性、性能需求以及线程安全性。通常情况下,使用枚举或静态内部类实现单例模式是比较推荐的方式,因为它们不仅线程安全,而且实现简单,易于理解。
实际例子
日志系统
假设我们正在开发一个日志记录系统,我们想要确保整个应用程序中只有一个日志记录器实例,这样我们就可以保持日志的一致性和管理方便。
java
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));
}
}
数据库连接池
我们手撸一个最简单的数据库连接池,保证在并发情况下的单例访问。
java
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 方法则将使用完毕的连接回收回连接池。