Java 单例模式:确保一个类只有一个实例
·
Java 单例模式:确保一个类只有一个实例
单例模式是最常用、最重要的设计模式之一。它确保一个类在整个应用中只有一个实例,并提供一个全局访问点。
为什么需要单例?
场景:数据库连接池
// 如果没有单例,每个地方都 new 一个连接池
DatabasePool pool1 = new DatabasePool();
DatabasePool pool2 = new DatabasePool();
// 结果:创建了多个连接池,浪费资源,连接数超标
// 使用单例,整个应用只有一个连接池
DatabasePool pool = DatabasePool.getInstance();
// 无论哪里获取,都是同一个实例
单例模式的核心要求
- 一个类只能有一个实例
- 必须自行创建这个实例
- 必须向整个系统提供这个实例
🔧 实现单例的 5 种方式
1. 饿汉式(Eager Initialization) - 最简单
特点:类加载时就创建实例
public class Singleton {
// 1. 私有静态实例,类加载时就创建
private static final Singleton INSTANCE = new Singleton();
// 2. 私有构造器,防止外部 new
private Singleton() {
System.out.println("饿汉式单例被创建");
}
// 3. 公共静态方法,返回唯一实例
public static Singleton getInstance() {
return INSTANCE;
}
// 业务方法
public void showMessage() {
System.out.println("我是饿汉式单例");
}
}
// 使用
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2); // true,是同一个对象
优点:简单,线程安全
缺点:即使不用也会创建,可能浪费内存
2. 懒汉式(Lazy Initialization) - 延迟加载
特点:第一次使用时才创建实例
2.1 线程不安全的懒汉式(❌ 有问题)
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 线程1判断为null
instance = new Singleton(); // 线程2也判断为null,会创建第二个实例
}
return instance;
}
}
// ❌ 多线程环境下可能创建多个实例
2.2 线程安全的懒汉式(方法同步)
public class Singleton {
private static Singleton instance;
private Singleton() {}
// 使用 synchronized 保证线程安全
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
// ✅ 线程安全
// ❌ 每次获取都要同步,性能差
3. 双重检查锁(Double-Checked Locking)- 推荐
特点:只在第一次创建时同步,兼顾性能和线程安全
public class Singleton {
// 必须使用 volatile 防止指令重排序
private static volatile Singleton instance;
private Singleton() {
System.out.println("双重检查锁单例被创建");
}
public static Singleton getInstance() {
// 第一次检查:避免不必要的同步
if (instance == null) {
// 同步代码块
synchronized (Singleton.class) {
// 第二次检查:确保只有一个线程能创建实例
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
public void showMessage() {
System.out.println("我是双重检查锁单例");
}
}
工作原理:
线程A调用 getInstance()
↓
if (instance == null) // 第一次检查
↓
进入 synchronized 块
↓
if (instance == null) // 第二次检查
↓
创建实例
↓
返回实例
线程B调用 getInstance()
↓
if (instance == null) // 第一次检查,发现不为null
↓
直接返回已有实例
为什么需要 volatile?
instance = new Singleton(); // 这行代码分3步:
// 1. 分配内存空间
// 2. 初始化对象
// 3. 将instance指向内存地址
// 没有volatile,JVM可能重排序为1-3-2
// 导致其他线程拿到未初始化的对象
4. 静态内部类(Static Inner Class)- 最佳实现
特点:利用类加载机制保证线程安全,实现延迟加载
public class Singleton {
private Singleton() {
System.out.println("静态内部类单例被创建");
}
// 静态内部类
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
public void showMessage() {
System.out.println("我是静态内部类单例");
}
}
原理:
- 外部类加载时,内部类不会加载
- 调用
getInstance()时,才会加载内部类 - 类加载是线程安全的,由 JVM 保证
- 实现了延迟加载 + 线程安全
这是最推荐的单例实现方式!
5. 枚举(Enum)- 最安全
特点:JVM 保证线程安全,防止反射和序列化攻击
public enum Singleton {
INSTANCE; // 唯一的实例
// 可以添加方法
public void showMessage() {
System.out.println("我是枚举单例");
}
// 可以添加属性
private String data = "单例数据";
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
}
// 使用
Singleton singleton = Singleton.INSTANCE;
singleton.showMessage();
System.out.println(singleton.getData());
优点:
- 简单
- 线程安全
- 防止反射攻击
- 防止序列化破坏单例
- 防止克隆破坏单例
单例模式的应用场景
场景1:配置管理器
public class ConfigManager {
private static volatile ConfigManager instance;
private Properties config;
private ConfigManager() {
loadConfig();
}
public static ConfigManager getInstance() {
if (instance == null) {
synchronized (ConfigManager.class) {
if (instance == null) {
instance = new ConfigManager();
}
}
}
return instance;
}
private void loadConfig() {
config = new Properties();
try (InputStream input = getClass().getClassLoader()
.getResourceAsStream("config.properties")) {
config.load(input);
} catch (IOException e) {
throw new RuntimeException("加载配置文件失败", e);
}
}
public String getProperty(String key) {
return config.getProperty(key);
}
public int getIntProperty(String key) {
return Integer.parseInt(config.getProperty(key));
}
}
// 使用
String dbUrl = ConfigManager.getInstance().getProperty("database.url");
int timeout = ConfigManager.getInstance().getIntProperty("timeout");
场景2:数据库连接池
public class DatabasePool {
private static DatabasePool instance;
private List<Connection> connections;
private DatabasePool() {
// 初始化连接池
connections = new ArrayList<>();
for (int i = 0; i < 10; i++) {
connections.add(createConnection());
}
}
public static DatabasePool getInstance() {
if (instance == null) {
synchronized (DatabasePool.class) {
if (instance == null) {
instance = new DatabasePool();
}
}
}
return instance;
}
public Connection getConnection() {
// 从连接池获取连接
synchronized (connections) {
if (!connections.isEmpty()) {
return connections.remove(0);
}
}
// 连接池为空,创建新连接
return createConnection();
}
public void releaseConnection(Connection conn) {
// 释放连接回到连接池
synchronized (connections) {
if (connections.size() < 10) {
connections.add(conn);
} else {
// 关闭多余的连接
closeConnection(conn);
}
}
}
private Connection createConnection() {
// 创建数据库连接
return null; // 实际实现
}
private void closeConnection(Connection conn) {
// 关闭连接
}
}
场景3:日志记录器
public class Logger {
private static Logger instance;
private File logFile;
private FileWriter writer;
private Logger() {
try {
logFile = new File("app.log");
writer = new FileWriter(logFile, true);
} catch (IOException e) {
e.printStackTrace();
}
}
public static Logger getInstance() {
if (instance == null) {
synchronized (Logger.class) {
if (instance == null) {
instance = new Logger();
}
}
}
return instance;
}
public void log(String message) {
try {
writer.write(LocalDateTime.now() + " - " + message + "\n");
writer.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
public void close() {
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
场景4:缓存管理器
public class CacheManager {
private static CacheManager instance;
private Map<String, Object> cache;
private CacheManager() {
cache = new ConcurrentHashMap<>();
}
public static CacheManager getInstance() {
if (instance == null) {
synchronized (CacheManager.class) {
if (instance == null) {
instance = new CacheManager();
}
}
}
return instance;
}
public void put(String key, Object value) {
cache.put(key, value);
}
public Object get(String key) {
return cache.get(key);
}
public void remove(String key) {
cache.remove(key);
}
public void clear() {
cache.clear();
}
}
单例模式的问题和解决方案
问题1:反射攻击
反射可以调用私有构造器创建新实例
public class ReflectionAttackTest {
public static void main(String[] args) throws Exception {
Singleton s1 = Singleton.getInstance();
// 通过反射创建第二个实例
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton s2 = constructor.newInstance();
System.out.println(s1 == s2); // false,单例被破坏了!
}
}
解决方案:
public class Singleton {
private static volatile Singleton instance;
private static boolean flag = false;
private Singleton() {
// 防止反射攻击
if (flag) {
throw new RuntimeException("单例模式禁止反射创建");
}
flag = true;
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
问题2:序列化破坏
序列化和反序列化会创建新实例
public class SerializationAttackTest {
public static void main(String[] args) throws Exception {
Singleton s1 = Singleton.getInstance();
// 序列化
ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("singleton.ser")
);
oos.writeObject(s1);
oos.close();
// 反序列化
ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("singleton.ser")
);
Singleton s2 = (Singleton) ois.readObject();
ois.close();
System.out.println(s1 == s2); // false,单例被破坏了!
}
}
解决方案:
public class Singleton implements Serializable {
private static final long serialVersionUID = 1L;
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;
}
// 防止序列化破坏
protected Object readResolve() {
return getInstance(); // 返回已有实例
}
}
问题3:克隆破坏
clone() 方法会创建新实例
public class Singleton implements Cloneable {
// ... 单例实现
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone(); // ❌ 这样会破坏单例
}
// ✅ 正确:禁止克隆
@Override
protected Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException("单例模式禁止克隆");
}
}
线程安全的单例模式对比
| 实现方式 | 线程安全 | 延迟加载 | 防止反射 | 防止序列化 | 性能 |
|---|---|---|---|---|---|
| 饿汉式 | ✅ | ❌ | ❌ | ❌ | 好 |
| 懒汉式(同步方法) | ✅ | ✅ | ❌ | ❌ | 差 |
| 双重检查锁 | ✅ | ✅ | ❌ | ❌ | 好 |
| 静态内部类 | ✅ | ✅ | ❌ | ❌ | 好 |
| 枚举 | ✅ | ❌ | ✅ | ✅ | 好 |
最佳实践建议
1. 选择哪种实现?
// 1. 如果确定一定会用到,用饿汉式
public class EarlySingleton {
private static final EarlySingleton INSTANCE = new EarlySingleton();
}
// 2. 大多数情况,用静态内部类(推荐!)
public class InnerClassSingleton {
private static class Holder {
static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
}
}
// 3. 需要防止反射/序列化攻击,用枚举
public enum EnumSingleton {
INSTANCE;
}
// 4. 双重检查锁用于复杂初始化
public class ComplexSingleton {
private static volatile ComplexSingleton instance;
private SomeResource resource;
private ComplexSingleton() {
resource = new SomeResource(); // 复杂的初始化
resource.init();
}
}
2. 在 Spring 中使用单例
@Component // Spring 默认就是单例
@Scope("singleton") // 显式声明
public class UserService {
// Spring 容器管理的单例
}
// 或者使用 @Bean
@Configuration
public class AppConfig {
@Bean
@Scope("singleton")
public DataSource dataSource() {
return new HikariDataSource();
}
}
3. 单例模式的替代方案
// 1. 使用依赖注入(推荐)
public class UserService {
// 通过构造器注入
private final DatabasePool pool;
public UserService(DatabasePool pool) {
this.pool = pool;
}
}
// 2. 使用静态工具类
public final class StringUtils {
private StringUtils() {} // 私有构造器,防止实例化
public static boolean isEmpty(String str) {
return str == null || str.trim().isEmpty();
}
}
单例模式常见面试题
1. 为什么要用单例模式?
- 控制资源的使用(如连接池)
- 控制实例的数量
- 全局访问点
- 节省内存和计算资源
2. 单例模式的缺点是什么?
- 难以扩展(只能有一个实例)
- 违背单一职责原则(控制自己+业务逻辑)
- 多线程环境需要特殊处理
- 单元测试困难(全局状态)
3. 如何实现线程安全的单例?
- 使用 synchronized
- 使用双重检查锁
- 使用静态内部类
- 使用枚举
4. 单例模式 vs 静态类
| 单例模式 | 静态类 |
|---|---|
| 可以有实例,可以继承 | 不能实例化,不能继承 |
| 可以实现接口 | 不能实现接口 |
| 可以延迟初始化 | 类加载时就初始化 |
| 可以序列化 | 不能序列化 |
实战练习
创建一个计数器单例,要求:
- 整个应用只有一个计数器实例
- 线程安全
- 可以获取当前计数
- 可以增加计数
- 可以重置计数
// 你的实现
public class Counter {
// TODO: 实现线程安全的单例计数器
}
总结
单例模式的本质:控制实例数量,节省资源,提供全局访问。
选择指南:
- 简单场景 → 饿汉式
- 一般场景 → 静态内部类(最推荐)
- 防止攻击 → 枚举
- 复杂初始化 → 双重检查锁
记住核心:单例模式的核心是私有构造器 + 静态获取方法。根据具体需求选择合适的实现方式,并注意线程安全和可能的安全攻击。
更多推荐


所有评论(0)