上个月,小王的一个朋友差点被骗走 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)。
你的脸之所以可信,是因为它不可复制。
参考资料
- Erich Gamma 等,《设计模式:可复用面向对象软件的基础》(GoF 原著)------单例模式的原始出处
- Joshua Bloch,《Effective Java(第 3 版)》------第 3 条:怎么写单例,才不会被人造出第二份(本文代码的出处);第 83 条:懒加载怎么写才线程安全(静态内部类写法的依据)
- Java 官方文档 · Runtime.getRuntime()
- Spring Framework 官方文档 · Singleton scope