JavaDog程序狗
发布于 2026-10-27 / 0 阅读
0
0

【设计模式】跟着狗哥复习设计模式(一):单例模式

最近开发团队组织学习,有位漂亮的小姐姐讲解了23种设计模式。

于是乎,狗哥想把 Java 设计模式重新捋一遍,和大家一起复习一下。

以前看这类内容,总是先来一大段定义。读的时候感觉懂了,关掉页面,让我自己写,手还是会停一下。

所以这个系列换个办法。先看生活里的例子,再写代码。第一篇从单例开始,简单点,慢慢来。

🍊先从公司前台说起

公司里通常只有一个前台。

有人来领快递,有人登记访客,还有人忘带门禁卡。办的事不一样,最后找的还是那一个人。

总不能每进来一位同事,公司就再招一个前台。真这么干,工位先不够用了。

加瓦狗现实例子:快递、访客登记和门禁卡都由唯一前台处理

加瓦狗守着一个前台,三个请求都往这里走。

程序里也有类似的对象。配置读取器、缓存管理器,还有后面要写的编号生成器,通常留一份就够。

这就是我对单例最直观的理解:这个类不让外面反复创建对象,大家要用时,都来拿同一份。

🍪两台取号机,麻烦就来了

先不写单例,做一个普通的订单编号生成器。

OrderNumberGenerator.java(问题版)

public class OrderNumberGenerator {
    private int number = 0;

    public int nextNumber() {
        return ++number;
    }
}

代码挺正常。问题是,外面想 new 几个就能 new 几个。

先在测试代码里创建两个生成器:

OrderNumberGenerator generatorA = new OrderNumberGenerator();
OrderNumberGenerator generatorB = new OrderNumberGenerator();

System.out.println(generatorA.nextNumber()); // 1
System.out.println(generatorB.nextNumber()); // 又是 1

我觉得这里不用背定义,盯着输出看就行。

控制台会出现两个 1。A 有自己的计数,B 也有自己的计数,两边谁都不知道对方已经发过号了。

有点像公司大厅摆了两台没联网的取号机。机器都好好的,叫号的时候却撞了。青岛话讲,这多少有点潮乎乎的。

把创建对象的入口收回来

解决办法不复杂。先不让外面随手 new,再由类自己保存一个对象。

OrderNumberGenerator.java(单例版)

public class OrderNumberGenerator {

    private static final OrderNumberGenerator INSTANCE =
            new OrderNumberGenerator();

    private int number = 0;

    private OrderNumberGenerator() {
    }

    public static OrderNumberGenerator getInstance() {
        return INSTANCE;
    }

    public synchronized int nextNumber() {
        return ++number;
    }
}

这段里最容易漏掉的是构造方法前面的 private。

少了它,外面还是能创建新对象。加上以后,再去 IDEA 里写 new OrderNumberGenerator(),那行代码会直接标红。这个小细节挺管用,比背半天概念记得牢。

**INSTANCE 保存共用对象,getInstance() 负责把它交出去。**再把构造方法藏起来,外面就不能创建第二个了。

单例模式三步结构图

构造方法藏起来,对象自己保存,再留一个获取入口。

办公室里的共享打印机也差不多。三台电脑都把任务发给同一台打印机,不需要每次打印都搬一台新机器过来。

加瓦狗实际例子:多台设备共用一台办公室打印机

设备有好几台,大家共用的是同一台打印机。

这种写法一般叫饿汉式。类加载时,对象已经创建好了。

我更建议刚入门时先写这一版。代码少,线程安全,也不容易把自己绕木乱。用济南话说,入门拿它练手挺杠赛来。

跑一下,比看概念踏实

新建一个 SingletonDemo.java,把下面代码放进去运行。

public class SingletonDemo {
    public static void main(String[] args) {
        OrderNumberGenerator generatorA =
                OrderNumberGenerator.getInstance();
        OrderNumberGenerator generatorB =
                OrderNumberGenerator.getInstance();

        System.out.println(generatorA.nextNumber()); // 1
        System.out.println(generatorB.nextNumber()); // 2
        System.out.println(generatorA == generatorB); // true
    }
}

这次输出变成了:

1
2
true

我一般会多看最后那个 true。

A、B 是两个变量,但它们指向同一个对象。变量名换了,对象没换,所以编号能从 1 接着走到 2。

加瓦狗实际业务图例:多个订单窗口共用唯一取号机,连续生成编号

三个订单窗口共用一台取号机,号码才会顺着排下去。

代码里的 synchronized 可以先理解成取号机前的小栏杆。一次放一个人过去,避免两个线程挤在一起拿到重复号码。

**synchronized 保护的是编号递增,和对象是不是单例没有关系。**刚开始学,知道这点就行。

并发里的细节不少,这篇先不展开。要不刚把单例说明白,又绕进锁里面了。

💡单例也有管不到的地方

单例用起来顺手,很容易让人多用。

我自己的判断比较简单。**确实需要大家共用一个对象时,再考虑单例。**对象本来就该各管各的,硬凑到一起,后面改代码会挺难受。

说个青岛味儿的例子。一桌人哈啤酒,开瓶器有一个够用,酒杯可不能只留一个。能不能共享,还是得看这个东西拿来干什么。

加瓦狗青岛生活例子:开瓶器可以共用,杯子各用各的

开瓶器可以共用,杯子还是各用各的。单例不能乱套。

订单编号还有一个更现实的问题。

在本文这个普通示例里,编号只在当前服务进程里接着增长。服务重启,计数会归零。服务开两份,也会各算各的。

单例模式的 JVM 边界图解

每个 JVM 都有自己的单例。服务开两份,就有两个编号生成器。

真正的线上订单号,会交给专门的编号方案处理。这里先记住:单例管不了多台服务。

其他写法先放一放

继续搜下去,还会碰到懒汉式、双重检查锁、静态内部类、枚举单例。

这些以后再学。第一遍先知道为什么要限制 new,能照着写出饿汉式,就够用了。

复习设计模式时,很容易急着把所有写法抄一遍。笔记看着挺满,过几天还是分不清什么时候用。

我现在更喜欢慢一点,先把一个小例子跑通。能自己写出来,比笔记堆得厚更实在。

🐾写在最后

单例的写法不多。构造方法改成私有,类里留一个对象,再提供 getInstance()。

它只管当前服务里的这个对象,也不适合所有类。

先把这篇的代码自己敲一遍。看到控制台里的 1、2、true,大概就有感觉了。

下一篇聊工厂方法模式。那个模式也经常被写得很绕,我想继续拿日常例子慢慢拆。

#Java #设计模式 #单例模式 #Java基础 #后端开发 #程序员入门


评论