Java 单例设计模式:为什么类里面可以有一个自己类型的字段?
最近学习 Java 设计模式时,看到了一个很常见的写法:
java
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return INSTANCE;
}
}
刚开始看到这段代码时,我最困惑的是这一行:
java
private static final Singleton INSTANCE = new Singleton();
Singleton 类里面,怎么又有一个 Singleton 类型的字段?
这不就是"自己里面套自己"吗?
如果 Singleton 里面有一个 Singleton,那这个里面是不是又有一个 Singleton,然后无限套娃?
后来理解之后发现,问题的关键在于:Java 里的对象和引用不是一回事。
1. 什么是单例模式?
单例模式的核心思想是:
一个类在整个程序运行期间,只创建一个对象,所有地方都使用这同一个对象。
也就是说,不能让外部代码随便这样写:
java
Singleton s1 = new Singleton();
Singleton s2 = new Singleton();
Singleton s3 = new Singleton();
如果可以随便 new,那就会创建多个对象,就不是单例了。
所以单例模式一般会做三件事:
java
public class Singleton {
// 1. 类内部自己保存唯一对象
private static final Singleton INSTANCE = new Singleton();
// 2. 构造方法私有,禁止外部 new
private Singleton() {
}
// 3. 提供公共方法,让外部拿到这个唯一对象
public static Singleton getInstance() {
return INSTANCE;
}
}
总结起来就是:
text
构造方法私有
类内部自己创建对象
外部只能通过 getInstance() 获取对象
2. 写一个带字段的单例例子
为了更好理解,我们写一个带业务字段的单例类。
比如有一个优惠券配置类:
java
public class CouponConfig {
private static final CouponConfig INSTANCE = new CouponConfig();
private int maxReceiveCount;
private boolean enableReminder;
private CouponConfig() {
this.maxReceiveCount = 3;
this.enableReminder = true;
}
public static CouponConfig getInstance() {
return INSTANCE;
}
public int getMaxReceiveCount() {
return maxReceiveCount;
}
public void setMaxReceiveCount(int maxReceiveCount) {
this.maxReceiveCount = maxReceiveCount;
}
public boolean isEnableReminder() {
return enableReminder;
}
public void setEnableReminder(boolean enableReminder) {
this.enableReminder = enableReminder;
}
}
使用方式如下:
java
public class Test {
public static void main(String[] args) {
CouponConfig config1 = CouponConfig.getInstance();
CouponConfig config2 = CouponConfig.getInstance();
System.out.println(config1 == config2);
// true
System.out.println(config1.getMaxReceiveCount());
// 3
config1.setMaxReceiveCount(5);
System.out.println(config2.getMaxReceiveCount());
// 5
}
}
为什么 config1 修改了 maxReceiveCount,config2 也能看到变化?
因为:
java
CouponConfig config1 = CouponConfig.getInstance();
CouponConfig config2 = CouponConfig.getInstance();
这两行拿到的是同一个对象。
可以理解成:
text
config1 ----+
|
v
CouponConfig 对象
^
|
config2 ----+
config1 和 config2 只是两个引用变量,它们都指向堆内存里的同一个 CouponConfig 对象。
3. 为什么类里面有自己类型的字段不是"无限套娃"?
真正容易误解的是这句:
java
private static final CouponConfig INSTANCE = new CouponConfig();
初学时很容易把它理解成:
text
CouponConfig 对象里面又放了一个 CouponConfig 对象
这个 CouponConfig 对象里面又放了一个 CouponConfig 对象
然后无限递归
但这其实是不对的。
因为 INSTANCE 是一个引用变量,不是直接把整个对象塞进类里面。
这句话可以拆开理解:
java
private static final CouponConfig INSTANCE;
这表示声明了一个 CouponConfig 类型的静态引用变量。
然后:
java
new CouponConfig();
这才是真正创建对象。
完整写法:
java
private static final CouponConfig INSTANCE = new CouponConfig();
意思是:
创建一个
CouponConfig对象,然后让INSTANCE这个引用指向它。
所以它不是无限套娃,而是:
text
CouponConfig 类本身
|
|-- static INSTANCE
|
v
堆内存中的唯一 CouponConfig 对象
|
|-- maxReceiveCount = 3
|-- enableReminder = true
注意,INSTANCE 是 static 的,它属于类本身,不属于某一个对象。
4. static 是理解单例的关键
在 Java 里,字段可以分成两类:
text
普通成员变量:属于对象
static 静态变量:属于类
比如:
java
private int maxReceiveCount;
private boolean enableReminder;
这两个字段属于具体的 CouponConfig 对象。
而:
java
private static final CouponConfig INSTANCE = new CouponConfig();
这个字段属于 CouponConfig 类本身。
所以内存结构不是这样:
text
错误理解:
CouponConfig 对象
|
|-- INSTANCE
|
v
CouponConfig 对象
|
|-- INSTANCE
|
v
CouponConfig 对象
|
|-- INSTANCE
...
正确理解是这样:
text
CouponConfig 类
|
|-- static INSTANCE
|
v
CouponConfig 对象
|
|-- maxReceiveCount
|-- enableReminder
也就是说:
类自己保存了一个指向唯一对象的引用。
不是对象里面又完整嵌套了一个对象。
5. 引用变量和对象不是一回事
这是理解单例最重要的点。
看这行代码:
java
CouponConfig config;
这里只是声明了一个变量。
此时没有创建对象。
它大概是:
text
config = null
只有写了:
java
new CouponConfig();
才是真正创建对象。
比如:
java
CouponConfig config = new CouponConfig();
可以拆成三步理解:
text
1. 声明一个 CouponConfig 类型的引用变量 config
2. new CouponConfig() 在堆内存中创建一个对象
3. 让 config 指向这个对象
所以单例里的:
java
private static final CouponConfig INSTANCE = new CouponConfig();
本质也是一样:
text
1. 声明一个 static 引用变量 INSTANCE
2. 创建一个 CouponConfig 对象
3. 让 INSTANCE 指向这个对象
它并不是"对象里面塞对象"。
6. 类里面有自己类型的字段很常见
其实不只是单例,Java 里很多结构都会出现"类里面有自己类型字段"的情况。
比如链表节点:
java
public class ListNode {
int value;
ListNode next;
}
这里的:
java
ListNode next;
也是当前类自己的类型。
但它也不会无限套娃。
因为 next 只是一个引用,默认是 null。
比如:
java
ListNode node1 = new ListNode();
ListNode node2 = new ListNode();
node1.next = node2;
node2.next = null;
对应结构是:
text
node1 -> node2 -> null
不是:
text
node1 里面自动 new node2
node2 里面自动 new node3
node3 里面自动 new node4
...
字段只是"可以指向某个对象",并不代表它会自动创建对象。
7. 为什么构造方法要 private?
单例模式里通常会这样写:
java
private CouponConfig() {
}
原因是:不让外部随便创建对象。
如果构造方法是 public:
java
public CouponConfig() {
}
外部就可以这样写:
java
CouponConfig c1 = new CouponConfig();
CouponConfig c2 = new CouponConfig();
这样就创建了两个对象,单例就被破坏了。
所以单例必须控制对象创建过程:
text
外部不能 new
类内部自己 new
外部只能 getInstance()
8. 饿汉式单例
前面的写法属于饿汉式单例:
java
public class CouponConfig {
private static final CouponConfig INSTANCE = new CouponConfig();
private CouponConfig() {
}
public static CouponConfig getInstance() {
return INSTANCE;
}
}
所谓饿汉式,就是:
类一加载,就马上创建对象。
优点:
text
写法简单
线程安全
缺点:
text
即使暂时用不到,也会提前创建对象
9. 懒汉式单例
懒汉式的思想是:
先不创建对象,等真正用到时再创建。
代码如下:
java
public class CouponConfig {
private static CouponConfig instance;
private CouponConfig() {
}
public static CouponConfig getInstance() {
if (instance == null) {
instance = new CouponConfig();
}
return instance;
}
}
这段代码在单线程下没问题,但在多线程下可能有问题。
假设两个线程同时执行:
java
if (instance == null) {
instance = new CouponConfig();
}
可能出现这种情况:
text
线程 A 判断 instance == null,准备创建对象
线程 B 也判断 instance == null,也准备创建对象
线程 A 创建了一个对象
线程 B 又创建了一个对象
这样就不是单例了。
所以普通懒汉式是线程不安全的。
10. 双重检查锁单例
为了兼顾懒加载和线程安全,可以使用双重检查锁:
java
public class CouponConfig {
private static volatile CouponConfig instance;
private int maxReceiveCount;
private boolean enableReminder;
private CouponConfig() {
this.maxReceiveCount = 3;
this.enableReminder = true;
}
public static CouponConfig getInstance() {
if (instance == null) {
synchronized (CouponConfig.class) {
if (instance == null) {
instance = new CouponConfig();
}
}
}
return instance;
}
}
这里有两个关键点。
第一个是 synchronized:
java
synchronized (CouponConfig.class) {
}
它保证同一时间只有一个线程能进入创建对象的代码。
第二个是 volatile:
java
private static volatile CouponConfig instance;
它主要是防止指令重排序,避免其他线程拿到一个"还没初始化完成"的对象。
11. 静态内部类单例
还有一种更推荐的写法:静态内部类。
java
public class CouponConfig {
private int maxReceiveCount;
private boolean enableReminder;
private CouponConfig() {
this.maxReceiveCount = 3;
this.enableReminder = true;
}
private static class Holder {
private static final CouponConfig INSTANCE = new CouponConfig();
}
public static CouponConfig getInstance() {
return Holder.INSTANCE;
}
public int getMaxReceiveCount() {
return maxReceiveCount;
}
public void setMaxReceiveCount(int maxReceiveCount) {
this.maxReceiveCount = maxReceiveCount;
}
public boolean isEnableReminder() {
return enableReminder;
}
public void setEnableReminder(boolean enableReminder) {
this.enableReminder = enableReminder;
}
}
这种写法的优点是:
text
懒加载
线程安全
代码相对简单
为什么它是懒加载?
因为 Holder 这个内部类只有在调用:
java
CouponConfig.getInstance();
的时候才会被加载。
为什么它线程安全?
因为 Java 的类加载机制保证:一个类只会被初始化一次。
12. Spring 里的 Bean 默认也是单例
在 Spring Boot 里,我们经常写:
java
@Service
public class UserService {
}
然后在别的地方注入:
java
@Autowired
private UserService userService;
默认情况下,Spring 容器里的 Bean 也是单例的。
也就是说,整个 Spring 容器中默认只有一个 UserService 对象。
这也是为什么实际项目里,我们很少自己手写单例类,而是把对象交给 Spring 容器管理。
比如:
java
@Component
public class CouponConfig {
}
默认就是单例 Bean。
13. Hutool Singleton.get 的理解
有时候也会看到 Hutool 的写法:
java
DefaultRedisScript<Long> buildLuaScript = Singleton.get(
STOCK_DECREMENT_AND_SAVE_USER_RECEIVE_LUA_PATH,
() -> {
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(
new ResourceScriptSource(
new ClassPathResource(STOCK_DECREMENT_AND_SAVE_USER_RECEIVE_LUA_PATH)
)
);
redisScript.setResultType(Long.class);
return redisScript;
}
);
可以这样理解:
text
Hutool 内部有一个单例容器
key 是 STOCK_DECREMENT_AND_SAVE_USER_RECEIVE_LUA_PATH
value 是 DefaultRedisScript 对象
第一次调用时:
text
容器里没有这个 key
执行 lambda 表达式
创建 DefaultRedisScript 对象
放入容器
返回对象
第二次调用时:
text
容器里已经有这个 key
不会再执行 lambda
直接返回之前创建好的对象
所以它的思想也是:
有就直接拿,没有就创建并缓存起来。
14. 最后总结
单例模式的本质是:
text
一个类只创建一个对象,并提供一个全局访问点。
它最经典的结构是:
java
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return INSTANCE;
}
}
初学时最容易误解的是:
java
private static final Singleton INSTANCE = new Singleton();
它不是无限套娃。
因为:
text
INSTANCE 是一个引用变量
new Singleton() 才是真正创建对象
static INSTANCE 属于类本身,不属于某一个对象
所以正确理解应该是:
text
类自己保存了一个指向唯一对象的引用。
不是:
text
对象里面无限嵌套对象。
可以用一句话记住:
单例模式就是:不让别人随便 new,我自己在类里面创建唯一对象,别人只能通过 getInstance() 拿这个唯一对象。