什么是单例模式
什么是单例模式? 答:是设计模式的一种。
先来看下标准答案:单例模式是一种设计模式,确保一个类只有一个实例,并提供一个全局访问点来获取该实例。
乍一看有些不好理解,仔细看还是不好理解。
下面给出一个简单的demo。
public class Singleton {
// 1. 静态私有变量 并将静态变量指向自身类型,实现的是确保一个类只有一个实例
private static Singleton instance;
// 2. 私有构造函数:防止外部通过 new 创建实例
private Singleton() {
// 初始化代码(如加载配置、连接数据库等一些标准性的重复代码块
}
// 3. 全局访问点:获取唯一实例(懒汉式,线程不安全)
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
简单概括下,定义类的唯一静态变量instance,确保类对应的实例都是唯一的,私有构造函数使得创建实例的方式只能由类提供的公共方法给出,而构造函数可以作为一些标准流程比如数据库的读取的固定性用法,减少代码冗余,这种做法和我们对常用工具类的提取一样。全局可访问的公共方法则是使用者提供获取当前类实例的唯一途径。
懒汉式: 已经初始化过就拿来直接用,没有初始化就初始化一个,另外他只有在getInstance()方法被调用时才创建实例对象,这种静态方法内部去控制静态变量初始化的方式也可以叫懒加载。
线程不安全:多线程同时调用getInstance()去获取实例对象的时候可能创建出多个实例,因为if的判定在不同线程得到的结果不能保证一致。
饿汉式:你别管我要不要,我就是要有。在类初始化的同时也提供出一个初始化好的实例,这样会安全些,但是始终都会创建出多余的资源。
public class Singleton {
// 在类加载时直接初始化实例
private static Singleton instance = new Singleton();
private Singleton() {} // 私有构造函数
public static Singleton getInstance() {
return instance; // 直接返回已初始化的实例
}
}
关键点
- 唯一性:无论调用多少次 getInstance(),返回的都是同一个对象。
- 全局访问:任何地方都能通过Singleton.getInstance() 获取实例。
- 控制实例化:只有类自己能决定何时创建实例。
单例模式Demo
所以了解到这里:全局都能访问的一个资源 且 始终得到的内容 都是提前定义好的一个东西,那不就是一个工具类的性质吗。
下面给一个练习demo去体现单例模式的价值:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
/**
* 数据库连接单例管理类(线程安全版)
*/
public class DatabaseConnection {
// 静态内部类实现线程安全的懒加载(JVM保证线程安全)
private static class Holder {
// 在首次访问Holder时创建实例
private static final DatabaseConnection INSTANCE = new DatabaseConnection();
}
// 数据库连接对象
private Connection connection;
// 数据库配置信息 测试数据 ,实际应用中推荐使用配置文件去获取
private static final String URL = "jdbc:mysql://localhost:3306/mydb";
private static final String USER = "root";
private static final String PASSWORD = "password";
// 私有构造函数:封装数据库连接的创建
private DatabaseConnection() {
try {
// 1. 加载驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 2. 创建数据库连接
this.connection = DriverManager.getConnection(URL, USER, PASSWORD);
System.out.println("数据库连接已建立");
} catch (ClassNotFoundException | SQLException e) {
// 将检查异常转换为运行时异常
throw new RuntimeException("数据库连接失败", e);
}
}
/**
* 获取单例实例(线程安全)
*/
public static DatabaseConnection getInstance() {
return Holder.INSTANCE;
}
/**
* 获取数据库连接对象
*/
public Connection getConnection() {
// 验证连接有效性
try {
if (connection == null || connection.isClosed()) {
reconnect();
}
} catch (SQLException e) {
throw new RuntimeException("连接状态检查失败", e);
}
return connection;
}
/**
* 执行查询示例方法
*/
public void executeQuery(String sql) {
try {
// 实际应使用PreparedStatement
var statement = connection.createStatement();
var resultSet = statement.executeQuery(sql);
// 处理结果集...
System.out.println("执行查询成功: " + sql);
} catch (SQLException e) {
throw new RuntimeException("查询执行失败", e);
}
}
/**
* 关闭数据库连接
*/
public void closeConnection() {
try {
if (connection != null && !connection.isClosed()) {
connection.close();
System.out.println("数据库连接已关闭");
}
} catch (SQLException e) {
throw new RuntimeException("关闭连接失败", e);
}
}
/**
* 重新建立连接
*/
private void reconnect() {
try {
this.connection = DriverManager.getConnection(URL, USER, PASSWORD);
System.out.println("数据库连接已重建");
} catch (SQLException e) {
throw new RuntimeException("重连数据库失败", e);
}
}
}
// 应用层示例
class Application {
public static void main(String[] args) {
// 获取单例实例
DatabaseConnection dbManager = DatabaseConnection.getInstance();
// 使用数据库连接
dbManager.executeQuery("SELECT * FROM users");
// 关闭连接
dbManager.closeConnection();
}
}
上面的demo,单例模式通过定义唯一的实例,并且在私有的构造函数中定义好了数据库的连接过,提供一个公共方法提供外部去获取到当前数据库的连接,程序员要做的事情就只有利用当前单例类的公共方法去获取连接进行后续数据库操作即可,很好的减少了获取数据库连接的重复性操作。
其他
上面的demo中,多次提到了线程安全。
线程不安全:判断当前实例是否被创建存在时间差异,多个资源同时调用一定会出现获取状态不一致的情况。
解决方案:
双重检查锁:通过在首次访问的判断和加锁后的二次判断去保证线程安全。
public class Singleton {
// volatile 禁止 new 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;
}
}
静态内部类(推荐):由JVM类加载机制去保证线程安全。
public class Singleton {
private Singleton() {}
// 静态内部类 首次访问时才申请资源
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE; // 由 JVM 保证线程安全
}
}
为什么推荐静态内部类方式
- 代码简洁:无需手动加锁,依赖 JVM 的类加载机制。
- 线程安全:由 JVM 保证,无需担心指令重排序等问题。
- 性能高:只有在首次调用 getInstance() 时才会加载静态内部类,避免了不必要的同步开销。
JVM 的类加载机制:当一个类被加载时,JVM 会加锁,确保只有一个线程可以加载该类。在类加载过程中,静态变量的初始化是线程安全的。
小结
简单的一句话,不代表理解上就很简单。
读书的过程经常会有原来如此的惊叹,可是也常听见我又忘了的尴尬。
设计模式是一种逻辑,就像各式各样的框架一样,为的是都是实惠敲代码的那个人。