开发手记 · 第 4 / 11 篇 合集目录 ‹ 上一篇 下一篇 ›

翻这套工业数据 WebApi 时,我注意到一个反复出现的小东西:一个叫 Holder 的私有静态内部类。它干的事,像全楼共用一台咖啡机。

楼里每台工位不需要各自买一台咖啡机,大家共用一台,谁想喝谁来接。代码里也是——某些全局只该有一份的东西(比如一个配置读取器、一个缓存容器),不用每人 new 一个,大家都从一个口拿同一份。README 曾给过这套单例 Holder 一个具体引用数,我顺便数了数,实测对不上;这个数我就不引了,只说"若干处"。

(等价改写,名字换掉了)它长这样:

public class BizCache
{
    private BizCache() { }
    private static class Holder
    {
        public static readonly BizCache Instance = new BizCache();
    }
    public static BizCache Get() => Holder.Instance;  // 懒加载、线程安全
}

它为什么线程安全

要点在那层嵌套静态类 Holder。它的静态字段 Instance 在类第一次被访问时才初始化,而 .NET 保证静态初始化的线程安全——不管多少个线程同时第一次来取,这个实例都只构造一次。外层的 BizCache 构造函数是私有的,谁也 new 不出来第二份。

这正是"全楼一台咖啡机"的模型:机器只有一台(私有构造 + 单实例),但谁第一个想喝,它才真正开机(懒加载),而且开机这件事不会被两个人同时触发两次。

代价也是共用的

共用意味着共用状态。这台"咖啡机"里的东西是全局共享的,一个地方写错了,全楼都尝到怪味。多线程下更要小心:它解决了"创建"的线程安全,却没解决"使用"的线程安全。如果那一份实例内部有可变状态,读写不加锁,照样会乱。

我没法确认当年每一处 Holder 是不是都该是单例,但更可能的情况是:大家都图省事,把"全局只要一份"的东西随手塞进同一个套路。套路本身没错,错在没人回头数清楚,到底有几台机器、哪台还在用。

一台咖啡机省了买机器的钱,也把"谁弄脏了机器"变成了全员的问题。单例好看在省,难看在它把责任也摊成了共用。