设计模式与生活①:你的脸,为什么不能有第二个?

上个月,小王的一个朋友差点被骗走 40 万。

那天下午他接到"老板"的视频电话:脸是老板的脸,声音是老板的声音,说有个项目要紧急打款,让他先走流程。

他看了三秒,觉得哪里不对------老板的额头,在视频里的反光有点怪。他借口信号不好挂断,回拨了老板的私人号码。老板说:我没给你打过电话。

AI 换脸诈骗。各地警方通报过不止一起,这个剧本正被批量复用。

视频里的"老板",脸是真的吗?

今天不是聊反诈,是聊这场骗局里的一个技术问题:骗子从头到尾只干了一件事------复制了一个"本该唯一"的东西。

而"保证某个东西只有一个,谁也别想弄出第二份",正是设计模式的第一课:单例模式(Singleton Pattern)。系列第一篇,从你的脸开始。

01 · 你的脸,是天然的单例

你的脸,天生带着三条规矩:

  • 规矩一:全世界只有一张。你的身份、账户、信用,全押在这张脸的唯一性上
  • 规矩二:谁也不能凭空造出第二张------骗子的全部工作,就是想"造"出来一张
  • 规矩三:验你是不是你,必须走官方核身系统,不是"视频里那张脸说自己是你就行"

这三条规矩翻译成 Java,语义一行都不会走样------对照看(右列的写法,下面一步步实现):

左边是你的脸的规矩,右边是 Java 的写法

最直接的写法叫饿汉式------核身系统一上线,身份档案库就初始化好了,不管今天有没有人来办业务:

java 复制代码
public class Identity {

    // 程序一启动就创建好,像银行开业前就把核身台搭好
    private static final Identity INSTANCE = new Identity();

    // 私有构造:这个档案库,谁也别想 new 出第二个
    private Identity() {}

    // 官方验证入口:要验身份,只走这里
    public static Identity getInstance() {
        return INSTANCE;
    }
}

饿汉式:开业前先搭好,随时可验

02 · 能不能等要用时,再建档?

能。核身是重服务------占内存、耗算力,但可能一整天都没几个人用。平时不启动,第一次有人来核身才拉起来,用到才创建,不浪费,这就是懒汉式:

java 复制代码
public class Identity {

    private static Identity instance;  // 先不建档

    private Identity() {}

    public static Identity getInstance() {
        if (instance == null) {         // 查档案:还没建过?
            instance = new Identity();  // 这才建一份
        }
        return instance;
    }
}

懒汉式:第一次用到,才发起核身建档

看起来很聪明:没生意就不开门,一分冤枉钱不花。

03 · 两个渠道同时给"你"建档,会怎样?

骗子最喜欢的就是这种"用到才验证"的系统。想象一个画面:你本人前脚在手机银行提交了人脸建档,骗子后脚用伪造渠道也提交了一份------两个请求同时去查档案,查到的都是"还没建过"。

于是,两个渠道各自建了一份"你"的档案。系统里凭空出现两个"你"------其中一个,是骗子的。

多线程下,懒汉式会建出两份"唯一档案"

现实里不会这样------你的脸只有一张,档案建得再乱,本体不会变成两份。但代码里,new 一个对象就像批量印刷:零门槛、彼此看不见,两个线程各自 new,系统里就真的有两个"你"了。

"检查"和"行动"之间的时间差,就是并发 bug 的温床------也是骗子的空档。

补救办法:同一身份的建档请求,排成一队,一次只处理一个。对应到代码就是加锁:

java 复制代码
// 排队版:安全,但建档完成后,所有人还得陪着排队
public static synchronized Identity getInstance() {
    if (instance == null) {
        instance = new Identity();
    }
    return instance;
}

synchronized 版:安全但慢

更聪明的做法是静态内部类,把线程安全这件事直接交给 JVM:

java 复制代码
public class Identity {

    private Identity() {}

    // 内部类:第一次用到 getInstance() 才加载,
    // JVM 保证类只加载一次 ------ 天然线程安全
    private static class Holder {
        private static final Identity INSTANCE = new Identity();
    }

    public static Identity getInstance() {
        return Holder.INSTANCE;
    }
}

静态内部类:JVM 替你把锁管好了

懒加载有了,线程安全有了,性能也没损失,《Effective Java》认可的经典写法。日常开发,写到这就够了。

还有个更狠的:AI 换脸,本质是一次"反射攻击" ------不走常规入口(不偷你的证件、不破你的密码),直接把"造对象"这事绕过去。而枚举写法,是 JVM 层面从根上禁止"再造一个"的方案,反射和反序列化都撬不开。面试聊到这,就到顶了。

04 · 这些"唯一档案",你天天在用

  • 🧩 JDK 自带的 Runtime :Runtime.getRuntime(),整个 JVM 就一份;
  • 🌱 Spring 的 Bean 默认单例:容器里同一个 Bean 只有一个实例。注意,Spring 的单例是"每个容器一份",跟你手写的单例不是一个层面的事;
  • 💧 数据库连接池、线程池:创建成本高,必须全局一份,谁都能来借,用完还回去;
  • 🖥 Windows 任务管理器:你永远开不出第二个。

05 · 什么时候,别用这层"保险"

说点掏心窝的边界:

单例的本质是全局状态。全局状态是单元测试的噩梦------mock 掉一个全局单例,比想象中难得多,用例之间还会互相污染。

另外,带可变状态的单例是并发事故重灾区。只读的单例(配置、工具类)才让人放心。在 Spring 项目里,别手写单例,交给容器管理。

用不用之前,先分清一件事:你要的是"一人一份",还是"全局就这一份"?

这个原则在生活里也成立------涉及转账的视频电话,一律挂断后用你自己的通讯录回拨。你只信那个唯一的官方入口,不信对方递过来的新入口。反诈的第一原则,和单例的使用第一原则,是同一句话。

带走一句话:单例模式 = 给系统里的"你的脸"上保险------构造器私有(别人 new 不出第二份),唯一官方入口(验证只走 getInstance)。

你的脸之所以可信,是因为它不可复制。

参考资料

  1. Erich Gamma 等,《设计模式:可复用面向对象软件的基础》(GoF 原著)------单例模式的原始出处
  2. Joshua Bloch,《Effective Java(第 3 版)》------第 3 条:怎么写单例,才不会被人造出第二份(本文代码的出处);第 83 条:懒加载怎么写才线程安全(静态内部类写法的依据)
  3. Java 官方文档 · Runtime.getRuntime()
  4. Spring Framework 官方文档 · Singleton scope
相关推荐
高晶3 小时前
ARM 嵌入式 Linux 命令系统 BusyBox 实战指南
linux·设计模式·程序员
高晶3 小时前
语音合成与变声技术十大业务场景落地实战:从电商直播到规模化生产
设计模式·程序员
博风6 小时前
19、设计模式应用举例:桥接模式:制作电视节目
设计模式·桥接模式
知守观6 小时前
从 4 套 PDF 库的混战,聊聊老项目的依赖管理与“静默失败”
java·后端·设计模式
geovindu1 天前
rust: Mediator Pattern
开发语言·后端·设计模式·rust·中介者模式·行为模式
SENKS_DIGITAL1 天前
金属加工遇上数字化:通快展厅里的工业蜕变-森克思科技
科技·设计模式·展厅设计·展馆设计·科技展厅设计·展览设计·企业展厅设计
flower9803231 天前
C++消息总线软件开发框架AI重构版
c++·设计模式·架构
zzu123zsw2 天前
业务拆分后的治理深水区:解构企业级微服务六大核心设计模式与高可用落真实践
微服务·设计模式·架构·高可用·微服务架构设计
牧瀬クリスだ2 天前
观察者模式:一个对象变化,一群对象自动响应
java·观察者模式·设计模式