设计模式之单例模式

单例模式确保一个类只有一个实例,并提供一个全局访问点。

咋一看这个定义,先解决一个疑惑,什么时候需要保证类只有一个实例。在实际生产场景中,有许多这样的实例,大多出于两个目的,防止资源浪费和保证数据一致性。比如常见的数据库连接池、线程池、日志记录器、缓存实例、配置信息实例等通常只需要一个实例来管理资源,能够节省开销,避免多实例带来的数据不一致。

明确了实际用处后,再来解析定义:

  1. 确保一个类只有一个实例”,类是如何实例化的,通过调用构造函数,谁调动构造函数谁就能创建实例,确保只有一个实例,就是要控制构造函数被调用的权限,防止外部代码通过构造函数或其他方式创建多个实例。
    1. 私有化构造函数:将类的构造函数设为私有(或受保护),禁止外部直接调用构造函数创建实例。构造函数私有了,构造出的唯一实例也必须是本类自身维护了
    2. 控制实例化过程:在类内部管理实例的创建,确保只有一个实例存在。
  2. 全局访问点”,为了让外部代码能够方便地获取这个唯一的实例,使用静态方法即可。
💡
日志记录器(Logger),应用程序的多个模块需要记录日志。日志记录器应该是全局唯一的,避免多个日志文件或日志信息混乱。日志记录器需要提供简单的接口,如 log(message),用于记录日志。
import java.io.FileWriter;
import java.io.IOException;

public class Logger {
    // 私有静态变量,存储唯一实例
    private static Logger instance;
    private String logFile = "app.log"; // 日志文件

    // 私有构造函数,禁止外部直接创建实例
    private Logger() {
        if (instance != null) {
            throw new IllegalStateException("Logger 是单例类,不能直接创建实例!");
        }
    }

    public static Logger getInstance() {
        instance = new Logger();
        return instance;
    }

    // 记录日志的方法
    public void log(String message) {
        try (FileWriter writer = new FileWriter(logFile, true)) {
            writer.write(message + "\n"); // 将日志信息写入文件
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在这个实现中,对定义中解析的重点都遵循到了,但从更严格的生产环境来看,存在线性安全问题,当多线程同时调用getInstance时,会有几率创建多个Logger实例。为什么?问题就出这两句中,如a线程都运行到【1】处,但还未到【2】处,此时b线程也运行到【1】处,也会判断instance == null,创建第二个Logger实例,导致多个实例,不符合单例要求。

  if (instance != null) {                   【1】 
    throw new IllegalStateException("Logger 是单例类,不能直接创建实例!");
  } 
                                            【2】

增加线程安全控制,利用sychronized控制并发。

import java.io.FileWriter;
import java.io.IOException;

public class Logger {
    // 私有静态变量,存储唯一实例
    private static Logger instance;
    private String logFile = "app.log"; // 日志文件

    // 私有构造函数,禁止外部直接创建实例
    private Logger() {
        if (instance != null) {
            throw new IllegalStateException("Logger 是单例类,不能直接创建实例!");
        }
    }

    // 公有静态方法,提供全局访问点
    public static Logger getInstance() {
        if (instance == null) {
            synchronized (Logger.class) { // 线程安全
                if (instance == null) {
                    instance = new Logger(); // 创建唯一实例
                }
            }
        }
        return instance;
    }

    // 记录日志的方法
    public void log(String message) {
        try (FileWriter writer = new FileWriter(logFile, true)) {
            writer.write(message + "\n"); // 将日志信息写入文件
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

上述实现只有在真实调用的时候,才会创建实例,称为“懒汉式”。synchronized是一个重量级操作,每次调用 getInstance() 时都加锁对性能消耗大,因此做了双重检查锁定。

有“懒汉式”,就有一个与之对应的“饿汉式”,是指在类加载时就创建单例实例。这种方式避免了线程安全问题,但无法实现懒加载。

public class Singleton {
    private static final Singleton instance = new Singleton(); // 类加载时创建实例

    private Singleton() {
        // 私有构造函数
    }

    public static Singleton getInstance() {
        return instance;
    }
}

这种实现简单,线程安全。但即使不需要使用单例实例,也会在类加载时创建,可能导致资源浪费。

还有一种静态内部类方式结合了懒汉式和饿汉式的优点,既实现了懒加载,又保证了线程安全。

public class Singleton {
    private Singleton() {
        // 私有构造函数
    }

    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton(); // 静态内部类中创建实例
    }

    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE; // 第一次调用时加载静态内部类
    }
}

静态内部类方式,实现了懒加载:只有在第一次调用 getInstance() 时,才会加载 SingletonHolder 类并创建实例。

单例模式的三种实现方式各有优缺点,适用于不同的场景:

  1. 懒汉式:适合需要懒加载的场景,但需要考虑线程安全问题。
  2. 饿汉式:适合单例实例创建成本低且不需要懒加载的场景。
  3. 静态内部类:结合了懒汉式和饿汉式的优点,是最推荐的方式。

不管哪一种方式,都遵循开篇对定义解析的重点,构造函数私有化,通过静态函数提供外部访问,不同的在于实例创建的时机

Read more

痛风带来的思考

昨晚一罐冰啤酒下去,睡觉时就感觉脚踝隐隐发作,果然早上起床直接下不来地。跟崴脚的感觉十分相似,无法行走,只能坐在一起上滑动,公司上班也去不了了,呆呆得躺在家里,下午疼痛感加剧,整个心思都在左脚的疼痛上,没有其他任何多余的精力去关注其他事情,而此刻的阳台,乃最美人间四月天,春日的微风吹拂着阳台的花儿,温暖的阳光抛洒下来,一切都如此惬意,而我却无心欣赏。 人在健康时,生活中有好多问题,但人在不健康时,生活中只剩一个问题。 我对这句话的理解更深刻了。人是健忘的,在疫情期间、在手术期间,这种感悟其实已经很深刻了,但是病情好转之后,人还是会被日常的琐碎、工作的烦扰搅乱心绪,没有专注的去享受生活本身的美好。 幸福的秘密在于,去享受我们所拥有的,而不是顽固的去追求所没有拥有的。阳光、草木、微风,都是幸福的玩意儿,应尽情的享受。 再等两天,脚完全恢复好了,身体健健康康后,我要以更轻盈的姿态去生活,不纠结他人的看法,不执着别人的认可,关注自己的能力,享受拥有的生后。 还有一个反省,针对咖啡、酒、烟,

By 李浩

设计模式之命令模式

命令模式将“请求”封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象,命令模式也支持可撤销的操作。 来解析这个定义: 1. “将一个请求封装为一个对象”,请求原本是一个方法,现在要封装成一个对象,说明要新增类来完成。 2. “可以用不同的请求对客户进行参数化”,说明是将命令对象作为参数进行传递。 3. “队列”说明需要维护命令多个命令的列表队列。 4. “撤销”说明有命令对象有undo撤销方法。 命令模式在设计模式中,算是一个比较不好理解的模式,很重要的原因是不清楚设计意图,不清楚不用这个模式前有何问题,这个模式带了哪些好处,能解决什么问题。 上一篇状态模式中,看到了状态模式抽离的是状态(属性),向上提成状态对象。有了这个基础,再来理解命令模式就相对简单了。命令模式抽离的是行为(方法),向上提成命令对象。 两者都通过“对象化”来解耦和扩展系统,但解决的问题不同: * 状态模式:处理对象内部状态驱动的行为变化。 * 命令模式:处理行为请求的封装与调度。 💡智能家居遥控器,假设我们有一个智能家居遥控器,可以控制 灯(Light) 和

By 李浩

设计模式之状态模式

状态模式允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。 来解析这个定义: 1. “内部状态”表明对象内部有一个属性来表示状态。 2. “内部状态改变时改变它的行为,对象看起来好像修改了它的类”说明状态改变后对象的行为发生了非常大的变化,不像是同一类的行为。 从目前的分析中似乎无法推导出状态模式的类图结构。 从实际的例子出来,来看看状态模式是如何演进而来。 💡我们有一个文档审批系统,文档有以下状态和转换: 1. 草稿(Draft) → 提交 → 待审批(PendingReview) 2. 待审批 → 批准 → 已发布(Published) 3. 待审批 → 拒绝 → 草稿 4. 已发布 → 撤回 → 草稿 从直觉出发,会使用条件语句实现需求逻辑。 public class Document { private String state = "DRAFT"; // 初始状态为草稿 public void submit(

By 李浩

设计模式之代理模式

代理模式为另一个对象提供一个替身或占位符以控制对这个对象的访问。 解析这个定义: 1. “替身”表明在客户端看来,代理类与被代理类是同一类别,对客户端来说看上去没什么区别,依然能够满足诉求。如此可以看出代理类与被代理类来自同一个超类。 2. “控制对这个对象的访问”,能够控制访问,说明前提是能够访问,才能在访问之前做这个限制,即代理持有对真实对象的引用(或能创建它)。 当然可能会质疑,用继承的方式不是也能完成目标吗,用代理类去继承被代理类,然后重写方法,加入控制逻辑。但这违背了"组合优于继承"原则,代理类与被代理类强耦合。 💡我们要开发一个图片查看器,需求如下: 1. 图片加载开销大(从磁盘或网络加载耗时),希望首次显示时才加载(延迟加载)。 2. 某些图片需要权限校验,只有授权用户才能查看。 3. 客户端代码应统一接口,无需关心是直接加载图片还是通过代理。 // 1. 抽象接口(Subject) interface Image { void display(); } // 2. 真实对象(RealSubject)

By 李浩