最近开发团队组织学习,有位漂亮的小姐姐讲解了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 都有自己的单例。服务开两份,就有两个编号生成器。
真正的线上订单号,会交给专门的编号方案处理。这里先记住:单例管不了多台服务。
其他写法先放一放
继续搜下去,还会碰到懒汉式、双重检查锁、静态内部类、枚举单例。
这些以后再学。第一遍先知道为什么要限制 new,能照着写出饿汉式,就够用了。
复习设计模式时,很容易急着把所有写法抄一遍。笔记看着挺满,过几天还是分不清什么时候用。
我现在更喜欢慢一点,先把一个小例子跑通。能自己写出来,比笔记堆得厚更实在。
🐾写在最后
单例的写法不多。构造方法改成私有,类里留一个对象,再提供 getInstance()。
它只管当前服务里的这个对象,也不适合所有类。
先把这篇的代码自己敲一遍。看到控制台里的 1、2、true,大概就有感觉了。
下一篇聊工厂方法模式。那个模式也经常被写得很绕,我想继续拿日常例子慢慢拆。
#Java #设计模式 #单例模式 #Java基础 #后端开发 #程序员入门