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

上个月,小王的一个朋友差点被骗走 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
相关推荐
杯子不吃紫菜17 小时前
LangGraph 体系实战(下):中断恢复、流式、子图与设计模式
单片机·嵌入式硬件·设计模式
是2的10次方啊20 小时前
设计模式与生活⑳:楼里的事都找前台,你就再没见过师傅
设计模式
是2的10次方啊2 天前
设计模式与生活⑲:投影连不上,别急着换投影仪
设计模式
cjz38993 天前
规则树:把一百行if-else拆成一棵树
设计模式
Adellle3 天前
设计模式指南 (Java 版) ㊁
java·开发语言·设计模式
是2的10次方啊3 天前
设计模式与生活⑱:工资还没焐热,账单已经凑成一摞
设计模式
百思不得小李3 天前
一个复杂的动态表单是如何实现的
前端·设计模式
爱你真是太好了3 天前
上下文组成以及如何压缩
设计模式
freejackman4 天前
【工程化】从“单兵作战”到“多引擎架构”:基于门面模式的重构实战
大数据·数据库·hive·sql·测试工具·设计模式·架构
Java成神之路-4 天前
从 if-else 到优雅解耦:策略 / 适配器 / 组合模式全流程实战
设计模式