日常设计模式(1):从一个常见组件聊起单例模式

71 阅读10分钟

前言

朋友们、朋友们,大家好!今天,我想和大家聊聊一个在咱们日常 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保证)
  • 防止反射和序列化破坏单例

缺点

  • 不支持延迟加载
  • 枚举中的所有实例在类加载时都会被创建

适用场景

  • 需要绝对线程安全的场景
  • 防止通过反射或序列化破坏单例
  • 实例创建开销不大

单例模式的选择建议

如何选择最合适的单例实现方式?可以根据以下几点考虑:

  1. 是否需要延迟加载:如果单例的创建开销大或者可能不会用到,选择懒加载方式
  2. 是否在多线程环境中使用:多线程环境必须考虑线程安全问题
  3. 性能要求:频繁使用单例时,应选择性能较好的实现方式
  4. 防止单例被破坏:如果担心通过反射或序列化破坏单例,可以选择枚举方式

综合考虑,推荐使用静态内部类或枚举实现单例模式。静态内部类兼顾了线程安全和延迟加载,而枚举则是最为简洁且安全的实现方式。

三、实现一个简单的日志框架

接下来,我们将实现一个简单的日志框架,命名为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");
        }
    }
}

项目改进思路

这个简单的日志框架还有很多可以改进的地方:

  1. 配置文件支持:通过XML或属性文件配置Logger和Appender
  2. 更多Appender实现:如数据库、远程服务器、消息队列等
  3. 日志格式化:可配置的日志格式
  4. 异步日志:使用线程池实现异步日志记录
  5. 滚动文件支持:按大小或时间滚动日志文件

四、总结

通过上面的实现,我们可以看到单例模式在日志框架中的应用。单例模式确保了LoggerFactory的唯一性,而LoggerFactory又通过缓存机制确保了每个名称对应的Logger实例的唯一性。

单例模式的核心思想是:确保一个类只有一个实例,并提供一个全局访问点。在实际应用中,我们可以根据需求选择不同的实现方式:

  • 需要延迟加载且线程安全:静态内部类
  • 需要绝对线程安全且防止被破坏:枚举单例
  • 实例创建简单且必定使用:饿汉式
  • 需要处理异常情况:双重检查锁定

单例模式虽然简单,但应用广泛,在日志框架、配置管理、线程池、缓存等场景都有应用。掌握好单例模式,是迈向设计模式大门的第一步。

在实际项目中,我们可能不需要自己实现日志框架,但了解其内部原理和设计思想,有助于我们更好地使用和扩展这些框架,同时也能将这些设计思想应用到自己的项目中。

最后,希望这篇文章能帮助你理解单例模式及其在实际项目中的应用。在下一篇文章中,我们将探讨更多设计模式及其实践应用。

参考资料

  1. 《设计模式之禅》
  2. 《重学Java设计模式》
  3. 《Effective Java》
  4. Log4j、Logback源码
  5. 单例模式 - 菜鸟教程

期待在评论区看到你的想法和建议!欢迎关注我,一起探索更多 Java 技术,共同进步!