在学习 Java 设计模式时,单例模式(Singleton Pattern) 是一个非常重要的模式,它确保一个类在整个应用程序运行期间只会有一个实例,并提供一个全局的访问点。本文将深入探讨单例模式的核心思想、不同实现方式、线程安全问题及其解决方案。
练习 1:基础单例模式实现
题目:请实现一个经典的单例模式,确保在多线程环境下也是安全的。要求:该单例类有一个方法 getInstance
,可以返回单例的实例对象。
提示:可以使用双重检查锁(Double-Checked Locking)来确保线程安全。
代码示例:
package org.example;
public class Singleton02 {
// 构造方法是私有的! 只有在类的内部才能访问,这意味着外部的类不可能通过 new 创建该类的实例。
// 这里用到了一个经典的小技巧:如果已经有实例了,就拒绝再次创建,避免意外的重复实例化。
private Singleton02() {
// 如果实例已经存在,扔出一个异常,提示你乖乖使用 getInstance() 方法。
// 不要直接用 new,这可是单例模式的大忌哦!
if (instance != null) {
throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
}
}
// 用 volatile 关键字修饰,确保多线程环境下的可见性和禁止指令重排。
private static volatile Singleton02 instance;
// 提供一个公共的静态方法,用来获取类的唯一实例。
public static Singleton02 getInstance() {
// 第一个 if 是一个快速检查——如果已经有实例,直接返回,节省时间!
if (instance == null) {
// 为了避免多线程的同时访问,使用 synchronized 锁住类对象。
synchronized (Singleton02.class) {
// 再次检查,如果还是 null,才创建实例!这是为了防止多个线程争抢锁之前有其他线程已经创建了实例。
if (instance == null) {
instance = new Singleton02(); // 就这一次,创造唯一的实例!
}
}
}
return instance; // 返回那唯一的、尊贵的实例对象!
}
}
测试:
import org.example.Singleton02;
import org.junit.Test;
public class test {
@Test
public void getSingletontest() {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
Singleton02 singleton02 = Singleton02.getInstance();
System.out.println(singleton02);
}).start();
}
}
}
类图:
classDiagram
class Client {
+main()
}
class Singleton02 {
-Singleton02 instance$
-Singleton02()
+getInstance()$ Singleton02
}
Client -- Singleton02 : uses
单例模式实现的基本步骤
- 私有化构造方法,防止外部通过
new
操作创建实例;- 提供一个静态私有变量来保存类的唯一实例;
- 提供一个静态共有方法用于获取唯一的实例对象。
我们为什么要使用volatile
关键字?
在这里,我们使用了volatile
关键字来保证可见性以及防止jvm指令重排。
在 Singleton02
类的双重检查锁定的实现中,instance = new Singleton02();
并不是一个原子操作,这个操作实际上涉及了多个操作,在这一过程,jvm至少做了以下3件事:
- 第一步给
Singleton02
对象分配内存空间; - 第二步开始调用
Singleton02
的构造方法,初始化对象的属性; - 第三步,将分配的内存地址赋值给
instance
变量。
如果我们没有使用volatile
关键字的话,jvm可能会发生指令重排序,使第二步第三步的顺序发生变化,如果这时有某个线程在instance
引用被赋值之后,它的对象还并没有被初始化的时候访问了instance
,这个时候就可能会发生程序报错。
为什么要进行第二次检查?
- 第一次检查,如果
instance
已经创建实例,则直接返回; - 第二次检查,是为了防止多个线程争抢锁之前有其他线程已经创建了实例。
练习 2:懒汉式单例和饿汉式单例的比较
题目:请分别实现“懒汉式单例”和“饿汉式单例”。然后写一个简单的测试类来验证它们的区别(例如,创建对象的时机和多线程环境下的表现)。
提示:饿汉式在类加载时就会创建实例,懒汉式在需要时才创建实例。
代码示例:
public class SingletonLazy {
// 懒汉式单例
// 私有静态实例,延迟加载
private static SingletonLazy instance;
private SingletonLazy() {
// 私有构造方法,防止外部实例化
if (instance != null) {
throw new RuntimeException("Use getInstance() method to get the single instance of this class."); } } // 防止反射攻击
public static synchronized SingletonLazy getInstance() {
// 公有的静态方法,通过同步来控制多线程访问
if (instance == null) {
instance = new SingletonLazy();
} return instance;
}
}
public class SingletonHungry {
// 饿汉式单例
// 在类加载时创建实例,静态成员变量
private static final SingletonHungry instance = new SingletonHungry();
// 私有构造方法,防止外部实例化
private SingletonHungry() {
// 防止反射攻击
if (instance != null) {
throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
}
}
// 公有的静态方法,直接返回实例
public static SingletonHungry getInstance() {
return instance;
}
}
类图:
classDiagram
class Client {
+main()
}
class SingletonLazy {
-SingletonLazy instance$
-SingletonLazy()
+getInstance()$ SingletonLazy
}
class SingletonHungry {
-SingletonHungry instance$
-SingletonHungry()
+getInstance()$ SingletonHungry
}
Client -- SingletonLazy : uses >
Client -- SingletonHungry : uses >
懒汉式与饿汉式单例模式的比较
特性 | 饿汉式单例模式 | 懒汉式单例模式 |
---|---|---|
实例化时机 | 在类加载时实例化 | 在第一次调用 getInstance() 时实例化 |
线程安全性 | 线程安全(类加载时创建实例,JVM 保证类加载过程是线程安全的) | 需要在 getInstance() 方法中使用同步机制保证线程安全 |
资源消耗 | 类加载时就会创建实例,占用一定内存 | 只有在需要时才创建实例,节省资源 |
实现难度 | 实现简单 | 需要使用同步,且可能涉及双重检查锁和 volatile 关键字 |
适用场景 | 适用于类加载就需要实例化的场景 | 适用于实例化比较消耗资源且在调用时才需要的场景 |
练习 3:枚举单例
题目:使用 Java 的 enum
枚举来实现一个单例模式。并创建测试类,验证其单例性。
提示:枚举类型是创建单例的最简洁和高效的方法之一,它可以防止反射攻击和序列化攻击。
public enum Singleton_06 {
INSTANCE; // 定义唯一的实例
private Object data; // 单例的属性
// 获取属性的方法
public Object getData() {
return data;
}
// 设置属性的方法
public void setData(Object data) {
this.data = data;
}
// 获取实例的静态方法
public static Singleton_06 getInstance() {
return INSTANCE;
}
}
类图:
classDiagram
class Client {
+main()
}
class Singleton_06 {
<<enumeration>>
INSTANCE$
-Object data
+getData() Object
+setData(Object) void
+getInstance()$ Singleton_06
}
Client -- Singleton_06 : uses >
实现要点:
- 唯一的实例 INSTANCE:在
enum
中定义了一个唯一的实例INSTANCE
,这是enum
实现单例模式的核心。在类加载时,Java 虚拟机(JVM)会自动创建该唯一实例,并在整个应用程序生命周期中保持唯一。即使在多线程环境下,也能确保这个实例的唯一性。 - 属性和方法:在枚举类
Singleton_06
中定义了属性data
,并通过setData()
和getData()
方法来访问和修改它。这样的设计可以让INSTANCE
存储某些全局共享的数据或状态,而不仅仅是一个简单的实例。这样我们不仅拥有单例模式的所有特性,还可以通过枚举实例来管理共享的数据或行为。 - 静态方法
getInstance()
:虽然直接通过Singleton_06.INSTANCE
访问实例已经非常直观,但为了提高代码的可读性和一致性,提供了一个静态方法getInstance()
来返回这个唯一实例。这样不仅让代码更清晰,也遵循了我们常见的单例模式命名约定。
enum
实现单例的优势
- 线程安全性:
enum
类型的线程安全性由 JVM 自动保证。每个enum
类型在类加载时只会被初始化一次,由 JVM 确保在多线程环境下不会有竞态条件。这比手动编写双重检查锁等实现方式更加简单和可靠。 - 防止反射攻击:传统的单例实现方式可能会通过反射破坏,因为反射可以访问私有构造方法。然而,
enum
在设计时通过特殊机制阻止了反射创建新的实例。当你尝试通过反射调用enum
的私有构造方法时,JVM 会抛出IllegalArgumentException
,有效地防止了反射攻击。 - 防止序列化攻击:在序列化和反序列化过程中,普通的单例模式可能会因为反序列化而创建新的实例。而
enum
类型在序列化时由 JVM 维护唯一性,无需手动重写readResolve()
方法来防止反序列化破坏单例。这大大简化了代码逻辑,同时提高了可靠性。 - 代码简洁性:相比传统的单例模式实现(如懒汉式、饿汉式或双重检查锁定),
enum
方式更为简洁明了。使用enum
省去了手动编写同步控制和双重检查等复杂代码,同时还解决了反射和序列化带来的潜在问题。其实现代码通常一到两行,显得简洁而优雅。
使用
enum
来实现单例模式是一种被广泛推崇的方式,因其代码简单、自然,并且由 JVM 提供了天然的安全性。与传统的单例模式实现相比,enum
更能有效避免反射和序列化攻击等棘手问题,使得代码更加健壮。通过这种方式,我们可以轻松定义一个安全且优雅的单例。
今天先不学了!我要吃晚饭!!!