写在前面
Hello,我是易元,这篇文章是我学习设计模式时的笔记和心得体会。如果其中有错误,欢迎大家留言指正!
场景模拟
第一话
电商系统突现订单丢失问题,因缺乏日志追踪能力:
- 客户投诉量单日激增300%
- 运维团队无法定位问题根源
- 新人工程师小易被要求48小时内实现日志系统
紧急实现方案
日志类
public class Logger {
private PrintWriter writer;
public Logger(String fileName) {
try {
writer = new PrintWriter(new FileWriter(fileName, true));
// 加载日志模板 等等操作
} catch (IOException e) {
e.printStackTrace();
}
}
public void log(String message) {
writer.println(message);
writer.flush();
}
public void close() {
writer.close();
}
}
测试类:
public class LoggerTest {
@Test
public void printTest() {
Logger logger1 = new Logger("system.log");
logger1.log("用户登录: ID=35876");
logger1.close();
Logger logger2 = new Logger("system.log");
logger2.log("用户下单: 商品ID=23423");
logger2.close();
}
}
短期成效
- 功能成功上线
- 日志可追踪到70%的订单问题
- 客户投诉量下降65%
第二话
订单问题在日志系统的帮助下迅速得以解决,但运维的投诉很快到来。
运维反馈:
- 系统响应时间从200ms升至1200ms
- 高峰时段出现服务不可用(错误码503)
- JVM监控显示GC频率异常(Young GC 5次/秒)
问题根因分析
- 每次调用创建Logger --> 频繁文件IO操作 --> 磁盘写入队列阻塞
- 大量对象创建/回收 --> GC压力剧增
以上两点同时导致系统整体性能下降
单例模式拯救计划
重构目标
- 消除资源浪费
- 单实例共享日志文件句柄
- 统一写入控制
- 避免多线程写入竞争
- 延长对象生命周期
- 减少GC压力
重构思路
- 私有构造函数:
- 防止外部直接创建 Logger 实例。
- 静态实例持有:
- 通过静态方法返回唯一的 Logger 实例,保证全局唯一性。
- 全局访问点:
- 提供统一访问入口,在第一次调用时创建实例(synchronized + 双重检查锁) ,避免资源浪费。
重构后代码
日志类
public class Logger {
private static volatile Logger instance;
private final File logFile;
private final FileWriter writer;
private Logger() throws IOException {
this.logFile = new File("system.log");
this.writer = new FileWriter(logFile, true);
Runtime.getRuntime().addShutdownHook(new Thread(this::close));
}
public static Logger getInstance() {
if (instance == null) {
synchronized (Logger.class) {
if (instance == null) {
try {
instance = new Logger();
} catch (IOException e) {
throw new RuntimeException("日志初始化失败", e);
}
}
}
}
return instance;
}
public synchronized void log(String message) {
try {
writer.write(LocalDateTime.now() + " - " + message + "\n");
writer.flush();
} catch (IOException e) {
System.err.println("日志写入异常: " + e.getMessage());
}
}
private void close() {
try {
writer.close();
} catch (IOException e) {
System.err.println("日志关闭失败: " + e.getMessage());
}
}
}
测试类:
@Test
public void printTest() {
Logger logger01 = Logger.getInstance();
logger01.log("用户登录: ID=35876");
Logger logger02 = Logger.getInstance();
logger01.log("用户下单: 商品ID=23423");
System.out.println(logger01.equals(logger02)); // true
}
重构效果对比
重构前
- 日志写入耗时 -> 120ms ~ 350ms
- Full GC频率 -> 2次/小时
- 文件描述符占用 -> 峰值800+
重构后
- 日志写入耗时 -> 15ms ~ 40ms
- Full GC频率 -> 0.2次/小时
- 文件描述符占用 -> 恒定1个
平稳运行成果
- 系统平均响应时间恢复至210ms
- 高峰时段吞吐量提升3倍
- 日志相关故障归零
开源项目中的应用
Apache HttpComponents Client 项目中单例模式的应用
PoolingHttpClientConnectionManager
public class PoolingHttpClientConnectionManager extends AbstractConnPool implements Closeable {
// 定义并初始化一个静态常量 DEFAULT_INSTANCE,使用final关键字确保创建后无法变更
// 它是整个 JVM 生命周期内唯一的 PoolingHttpClientConnectionManager 实例
private static final PoolingHttpClientConnectionManager DEFAULT_INSTANCE = createDefaultInstance();
// 创建默认 PoolingHttpClientConnectionManager 实例
private static PoolingHttpClientConnectionManager createDefaultInstance() {
try {
SSLSocketFactory sslsf = SSLContext.getDefault().getSocketFactory();
HostnameVerifier hostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
Registry registry = RegistryBuilder.create()
.register("http", PlainConnectionSocketFactory.getSocketFactory())
.register("https", new SSLConnectionSocketFactory(sslsf, hostnameVerifier))
.build();
return new PoolingHttpClientConnectionManager(registry);
} catch (NoSuchAlgorithmException | KeyManagementException ex) {
throw new RuntimeException(ex.getMessage(), ex);
}
}
// 获取预定义好配置参数的 `PoolingHttpClientConnectionManager` 实例
public static PoolingHttpClientConnectionManager getDefault() {
return DEFAULT_INSTANCE;
}
// Other methods related to managing connections...
protected PoolingHttpClientConnectionManager(final SchemeRegistry schemeRegistry,
final DnsResolver dnsResolver) {
super(schemeRegistry, dnsResolver);
}
protected PoolingHttpClientConnectionManager(
final Registry socketFactoryRegistry,
final DNSLookupService dnsResolver) {
super(socketFactoryRegistry, dnsResolver);
}
// Additional constructors and other utility methods...
@Override
public void close() throws IOException {
shutdown(); // Properly shuts down all managed connections in the pool.
}
}
- 全局唯一性: 只有一个 PoolingHttpClientConnectionManager 默认实例,在整个应用程序生命周期内有效,减少了重复创建工作带来的额外消耗。
- 集中控制: 所有基于该连接管理器建立的 HTTP/HTTPS 请求都会遵循一致的行为准则和服务质量标准。
长话短说
单例模式的核心内容
核心思想:
- 确保一个类只有一个实例,并提供一个全局访问点。
使用思路
- 私有化构造函数:
- 将类的构造函数设置为私有,所属类静态方法仍能调用构造函数,但其他外部类无法直接创建。
- 私有静态成员变量
- 在类中添加一个私有成员变量用于保存单例实例。
- 提供静态方法:
- 声明一个公有静态方法用于获取单例实例,若实例为空则创建一个新的对象并存储在静态成员变量中,并返回实例,若不为空则返回实例。
何时可以使用单例模式?
- 全局唯一对象:
- 当需要一个全局唯一的对象时(如配置管理器、日志系统)。
- 资源节省:
- 当对象的创建成本高,且需要频繁使用时。
- 统一管理:
- 当需要统一管理和配置某个对象时。
单例模式实现方式较多分别有:饿汉式(加载即创建实例)、懒汉式(延迟实例化)、双重锁(通过懒汉式+锁,保证线程安全)、静态内部类、枚举等。