前言
朋友们、朋友们,大家好!今天,我想和大家聊聊一个在咱们日常 Coding 中出镜率极高的设计模式——单例模式。别看它名字简单,想把它用得炉火纯青,可没那么容易。所以,今天我就打算从咱们都熟悉的“程序日记”——日志框架入手,一步步带你揭开单例模式的神秘面纱。
一、从日志框架说起
想必大家在项目开发中都用过日志框架,比如Log4j、Logback或SLF4J。你有没有想过,为什么我们能在项目的任何地方都可以使用同一个Logger实例,而不需要反复创建?(聪明的小伙伴应该猜到了)
// 在类A中
private static final Logger logger = LoggerFactory.getLogger(ClassA.class);
// 在类B中
private static final Logger logger = LoggerFactory.getLogger(ClassB.class);
你会发现,无论在哪个类中,我们都可以通过LoggerFactory获取Logger实例。虽然看起来我们在不同的类中创建了不同的Logger对象,但实际上,LoggerFactory内部为每个类名维护着唯一的一个Logger实例。
我们来看看简化版的LoggerFactory是如何实现的(番外:具体的实现可以去翻下源码,你会有惊喜的):
public class LoggerFactory {
// 使用ConcurrentHashMap存储已创建的Logger实例
private static final ConcurrentHashMap<String, Logger> loggerMap = new ConcurrentHashMap<>();
public static Logger getLogger(Class<?> clazz) {
return getLogger(clazz.getName());
}
public static Logger getLogger(String name) {
// 先检查缓存中是否存在
Logger logger = loggerMap.get(name);
if (logger != null) {
return logger;
}
// 不存在则创建,并放入缓存
logger = new Logger(name);
Logger existingLogger = loggerMap.putIfAbsent(name, logger);
return existingLogger != null ? existingLogger : logger;
}
}
这种模式看起来很眼熟,没错,这就是单例模式的一种变体——我们确保对于每个类名,只创建一个对应的Logger实例。
而LoggerFactory本身,实际上也是一个单例。通过这种方式,我们避免了创建过多重复的对象,既节省了内存,又保证了日志的一致性。
单例模式的核心思想就是:确保一个类只有一个实例,并提供一个全局访问点。
二、单例模式:让“唯一”成为现实
接下来,让我们深入探讨几种常见的单例模式实现方案,分析它们的优缺点和适用场景。
1. 饿汉式单例:急切的“单身汉
饿汉式单例在类加载的时候就创建了唯一的实例,就像一个迫不及待想要结婚的“单身汉”。
public class EagerSingleton {
// 在类加载时就创建实例
private static final EagerSingleton INSTANCE = new EagerSingleton();
// 私有构造函数,防止外部实例化
private EagerSingleton() {}
// 提供全局访问点
public static EagerSingleton getInstance() {
return INSTANCE;
}
}
优点:
- 实现简单,线程安全
- 类加载时就创建实例,不存在懒加载问题
缺点:
- 即使不使用该实例,也会在类加载时创建,可能造成资源浪费
- 不能处理异常情况
适用场景:
- 实例创建开销不大
- 程序一定会使用到该实例
- 不需要延迟加载
2. 懒汉式单例:需要时才“行动”
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
优点:
- 延迟加载,只有在第一次使用时才创建实例
缺点:
- 线程不安全,多线程环境下可能创建多个实例
适用场景:
- 单线程环境
- 实例创建开销大,需要延迟加载
3. 线程安全的懒汉式单例(synchronized版)
public class ThreadSafeLazySingleton {
private static ThreadSafeLazySingleton instance;
private ThreadSafeLazySingleton() {}
// 使用synchronized关键字保证线程安全
public static synchronized ThreadSafeLazySingleton getInstance() {
if (instance == null) {
instance = new ThreadSafeLazySingleton();
}
return instance;
}
}
优点:
- 线程安全,保证只有一个实例
- 延迟加载
缺点:
- 每次调用getInstance()都会同步,性能差
适用场景:
- 多线程环境,但对性能要求不高
- 实例使用频率不高
4. 双重检查锁定(Double-Check Locking):更聪明的懒汉式
public class DCLSingleton {
// 使用volatile关键字保证可见性和有序性
private static volatile DCLSingleton instance;
private DCLSingleton() {}
public static DCLSingleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (DCLSingleton.class) {
if (instance == null) { // 第二次检查
instance = new DCLSingleton();
}
}
}
return instance;
}
}
注意: instance 变量需要使用 volatile 关键字修饰,以防止指令重排序导致的问题。
优点:
- 线程安全
- 延迟加载
- 大部分情况下不需要同步,性能较好
缺点:
- 实现复杂
- 需要使用volatile关键字(Java 5及以上版本)
适用场景:
- 多线程环境
- 实例使用频率高
- 实例创建开销大,需要延迟加载
5. 静态内部类:优雅且安全
public class StaticInnerClassSingleton {
private StaticInnerClassSingleton() {}
// 静态内部类,只有在使用时才会被加载
private static class SingletonHolder {
private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
优点:
- 线程安全(利用类加载机制保证线程安全)
- 延迟加载(内部类只有在使用时才会被加载)
- 实现简单,性能好
缺点:
- 不易理解
适用场景:
- 需要线程安全的单例模式
- 实例使用频率高
- 对性能有要求
6. 枚举单例:告别传统
public enum EnumSingleton {
INSTANCE;
// 可以添加单例需要的方法
public void doSomething() {
System.out.println("Singleton is doing something");
}
}
优点:
- 最简洁的实现方式
- 线程安全(由JVM保证)
- 防止反射和序列化破坏单例
缺点:
- 不支持延迟加载
- 枚举中的所有实例在类加载时都会被创建
适用场景:
- 需要绝对线程安全的场景
- 防止通过反射或序列化破坏单例
- 实例创建开销不大
单例模式的选择建议
如何选择最合适的单例实现方式?可以根据以下几点考虑:
- 是否需要延迟加载:如果单例的创建开销大或者可能不会用到,选择懒加载方式
- 是否在多线程环境中使用:多线程环境必须考虑线程安全问题
- 性能要求:频繁使用单例时,应选择性能较好的实现方式
- 防止单例被破坏:如果担心通过反射或序列化破坏单例,可以选择枚举方式
综合考虑,推荐使用静态内部类或枚举实现单例模式。静态内部类兼顾了线程安全和延迟加载,而枚举则是最为简洁且安全的实现方式。
三、实现一个简单的日志框架
接下来,我们将实现一个简单的日志框架,命名为SimpleLog。这个框架将使用单例模式来确保日志管理的唯一性和一致性。
项目结构
src/
└── com/
└── simplelog/
├── Logger.java // 日志记录器
├── LoggerFactory.java // 日志工厂类,使用单例模式
├── LogLevel.java // 日志级别枚举
└── appender/
├── Appender.java // 日志输出接口
├── ConsoleAppender.java // 控制台输出实现
└── FileAppender.java // 文件输出实现
1. 定义日志级别
首先,我们定义日志级别枚举,用于控制日志的输出:
package com.simplelog;
public enum LogLevel {
DEBUG(1, "DEBUG"),
INFO(2, "INFO"),
WARN(3, "WARN"),
ERROR(4, "ERROR"),
FATAL(5, "FATAL");
private final int level;
private final String name;
LogLevel(int level, String name) {
this.level = level;
this.name = name;
}
public int getLevel() {
return level;
}
public String getName() {
return name;
}
public boolean isGreaterOrEqual(LogLevel other) {
return this.level >= other.level;
}
}
2. 定义日志输出接口和实现
package com.simplelog.appender;
import com.simplelog.LogLevel;
public interface Appender {
void append(String loggerName, LogLevel level, String message);
}
控制台输出实现:
package com.simplelog.appender;
import com.simplelog.LogLevel;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class ConsoleAppender implements Appender {
private static final DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
@Override
public void append(String loggerName, LogLevel level, String message) {
LocalDateTime now = LocalDateTime.now();
String timestamp = now.format(formatter);
String logMessage = String.format("[%s] [%s] [%s] - %s",
timestamp, level.getName(), loggerName, message);
// 根据日志级别选择不同的输出流
if (level.isGreaterOrEqual(LogLevel.WARN)) {
System.err.println(logMessage);
} else {
System.out.println(logMessage);
}
}
}
文件输出实现(简化版):
package com.simplelog.appender;
import com.simplelog.LogLevel;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class FileAppender implements Appender {
private static final DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
private final String logFilePath;
public FileAppender(String logFilePath) {
this.logFilePath = logFilePath;
}
@Override
public void append(String loggerName, LogLevel level, String message) {
LocalDateTime now = LocalDateTime.now();
String timestamp = now.format(formatter);
String logMessage = String.format("[%s] [%s] [%s] - %s%n",
timestamp, level.getName(), loggerName, message);
try (PrintWriter writer = new PrintWriter(new FileWriter(logFilePath, true))) {
writer.print(logMessage);
} catch (IOException e) {
System.err.println("Failed to write to log file: " + e.getMessage());
}
}
}
3. 实现Logger类
package com.simplelog;
import com.simplelog.appender.Appender;
import java.util.ArrayList;
import java.util.List;
public class Logger {
private final String name;
private LogLevel level = LogLevel.INFO; // 默认日志级别
private final List<Appender> appenders = new ArrayList<>();
Logger(String name) {
this.name = name;
}
public void addAppender(Appender appender) {
appenders.add(appender);
}
public void setLevel(LogLevel level) {
this.level = level;
}
public void debug(String message) {
log(LogLevel.DEBUG, message);
}
public void info(String message) {
log(LogLevel.INFO, message);
}
public void warn(String message) {
log(LogLevel.WARN, message);
}
public void error(String message) {
log(LogLevel.ERROR, message);
}
public void fatal(String message) {
log(LogLevel.FATAL, message);
}
private void log(LogLevel msgLevel, String message) {
if (msgLevel.isGreaterOrEqual(level)) {
// 遍历所有的appender,将日志消息输出
for (Appender appender : appenders) {
appender.append(name, msgLevel, message);
}
}
}
}
4. 实现LoggerFactory(单例模式)
最后,我们使用静态内部类的方式实现单例模式的LoggerFactory:
package com.simplelog;
import com.simplelog.appender.ConsoleAppender;
import java.util.concurrent.ConcurrentHashMap;
public class LoggerFactory {
// 使用ConcurrentHashMap存储Logger实例
private final ConcurrentHashMap<String, Logger> loggerMap = new ConcurrentHashMap<>();
// 私有构造函数
private LoggerFactory() {
// 初始化工作,如加载配置文件等
}
// 静态内部类实现单例
private static class SingletonHolder {
private static final LoggerFactory INSTANCE = new LoggerFactory();
}
// 全局访问点
public static LoggerFactory getInstance() {
return SingletonHolder.INSTANCE;
}
// 获取Logger的方法
public static Logger getLogger(Class<?> clazz) {
return getInstance().getLoggerInstance(clazz.getName());
}
public static Logger getLogger(String name) {
return getInstance().getLoggerInstance(name);
}
// 内部获取Logger实例的方法
private Logger getLoggerInstance(String name) {
// 检查缓存中是否存在
Logger logger = loggerMap.get(name);
if (logger != null) {
return logger;
}
// 不存在则创建新的Logger实例
logger = new Logger(name);
// 添加默认的控制台输出
logger.addAppender(new ConsoleAppender());
// 将新创建的Logger放入缓存
Logger existingLogger = loggerMap.putIfAbsent(name, logger);
return existingLogger != null ? existingLogger : logger;
}
// 清除所有Logger(主要用于测试)
public void clearLoggers() {
loggerMap.clear();
}
}
5. 使用示例
package com.example;
import com.simplelog.Logger;
import com.simplelog.LoggerFactory;
import com.simplelog.LogLevel;
import com.simplelog.appender.FileAppender;
public class SimpleLogDemo {
public static void main(String[] args) {
// 获取Logger实例
Logger logger = LoggerFactory.getLogger(SimpleLogDemo.class);
// 设置日志级别
logger.setLevel(LogLevel.DEBUG);
// 添加文件输出
logger.addAppender(new FileAppender("application.log"));
// 输出不同级别的日志
logger.debug("这是一条调试信息");
logger.info("这是一条普通信息");
logger.warn("这是一条警告信息");
logger.error("这是一条错误信息");
logger.fatal("这是一条致命错误信息");
// 在另一个类中使用Logger
AnotherClass.testLogger();
}
static class AnotherClass {
public static void testLogger() {
// 获取同名Logger实例,实际上是同一个实例
Logger logger = LoggerFactory.getLogger(SimpleLogDemo.class);
logger.info("在另一个类中使用同一个Logger");
// 获取不同名的Logger实例
Logger anotherLogger = LoggerFactory.getLogger(AnotherClass.class);
anotherLogger.info("这是另一个Logger");
}
}
}
项目改进思路
这个简单的日志框架还有很多可以改进的地方:
- 配置文件支持:通过XML或属性文件配置Logger和Appender
- 更多Appender实现:如数据库、远程服务器、消息队列等
- 日志格式化:可配置的日志格式
- 异步日志:使用线程池实现异步日志记录
- 滚动文件支持:按大小或时间滚动日志文件
四、总结
通过上面的实现,我们可以看到单例模式在日志框架中的应用。单例模式确保了LoggerFactory的唯一性,而LoggerFactory又通过缓存机制确保了每个名称对应的Logger实例的唯一性。
单例模式的核心思想是:确保一个类只有一个实例,并提供一个全局访问点。在实际应用中,我们可以根据需求选择不同的实现方式:
- 需要延迟加载且线程安全:静态内部类
- 需要绝对线程安全且防止被破坏:枚举单例
- 实例创建简单且必定使用:饿汉式
- 需要处理异常情况:双重检查锁定
单例模式虽然简单,但应用广泛,在日志框架、配置管理、线程池、缓存等场景都有应用。掌握好单例模式,是迈向设计模式大门的第一步。
在实际项目中,我们可能不需要自己实现日志框架,但了解其内部原理和设计思想,有助于我们更好地使用和扩展这些框架,同时也能将这些设计思想应用到自己的项目中。
最后,希望这篇文章能帮助你理解单例模式及其在实际项目中的应用。在下一篇文章中,我们将探讨更多设计模式及其实践应用。
参考资料
- 《设计模式之禅》
- 《重学Java设计模式》
- 《Effective Java》
- Log4j、Logback源码
- 单例模式 - 菜鸟教程
期待在评论区看到你的想法和建议!欢迎关注我,一起探索更多 Java 技术,共同进步!