一、什么是单例模式
单例模式是一种创建型设计模式
,它限制了一个类只能实例化一个对象,并提供一个全局访问点以供其他对象使用
二、为什么要用单例模式
-
避免了资源浪费:保证了全局唯一实例,这样可以确保系统的行为一致性,避免了资源的重复创建与浪费。
-
访问简单:通过单例模式,其他对象可直接访问,不需要考虑复杂参数传递及上下文切换
-
避免了对同一资源的竞争冲突:对于需要共享资源的场景,单例模式可以避免对同一资源的竞争冲突。
-
实现了延迟加载:只有在需要使用时才创建实例对象,减少了系统的启动时间和资源占用。
-
可维护性强:只有一个实例对象,不需要考虑多个实例之间的一致性问题,只需要关注一个实例对象的状态和行为。
总的来说,单例模式可以提供全局唯一的实例对象,简化了代码的设计和实现,提高了系统的性能和可维护性。但需要注意的是,滥用单例模式可能导致代码的可测试性和可扩展性降低
,因此在使用时需谨慎考虑。
三、单例模式的多种实现
1. 懒汉式:
- 线程不安全:在第一次调用获取实例方法时才创建实例对象。
- 线程安全:使用同步锁或双重检查锁定(Double-Checked Locking)机制来确保线程安全。
懒汉式实现示例如下:
java
public class LazySingleton {
private static LazySingleton instance;
// 私有构造方法,防止外部直接实例化
private LazySingleton() {}
// 获取唯一实例的方法
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
在懒汉式中,使用了一个静态变量 instance
来保存实例对象,初始时为 null。在 getInstance()
方法中,首先判断 instance
是否为 null,如果是,则创建一个新的实例对象,然后返回。如果 instance
已经存在,则直接返回现有的实例对象。这样可以实现延迟加载,只有在第一次调用 getInstance()
方法时才会创建实例。
需要注意的是,懒汉式的简单实现在多线程环境下是线程不安全的。如果多个线程同时调用 getInstance()
方法,有可能会创建多个实例对象。为了确保线程安全,可以在 getInstance()
方法上加上同步锁,或者使用双重检查锁定(Double-Checked Locking)机制。
2. 饿汉式:
- 在类加载时就创建实例对象,因此线程安全。
枚举方式实现示例如下:
java
public class Singleton {
// 创建私有的静态实例对象
private static Singleton instance = new Singleton();
// 私有化构造函数,防止外部直接创建实例对象
private Singleton() {
// do something
}
// 提供公共的静态访问方法,返回实例对象
public static Singleton getInstance() {
return instance;
}
}
3. 枚举方式:
- 枚举类型本身就是单例模式,枚举类型的实例在类加载时就被实例化,因此线程安全。
枚举方式实现示例如下:
java
public enum Singleton {
INSTANCE;
// 添加自己需要的属性和方法
private int count;
private Singleton() {
// do something
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
// 添加自己需要的其他方法
public void incrementCount() {
count++;
}
}
在上述代码中,Singleton枚举类型只有一个实例 INSTANCE
。私有的构造函数保证了其他类不能直接实例化Singleton。可以在枚举类型中添加所需的属性和方法。
使用示例:
java
public class Main {
public static void main(String[] args) {
Singleton instance = Singleton.INSTANCE;
instance.setCount(10);
System.out.println("Count: " + instance.getCount()); // Output: Count: 10
Singleton anotherInstance = Singleton.INSTANCE;
anotherInstance.incrementCount();
System.out.println("Count: " + instance.getCount()); // Output: Count: 11
}
}
在上述示例中,我们可以通过Singleton.INSTANCE
来访问单例实例。两次获取到的实例对象是相同的,因此对实例的操作是共享的。
枚举方式实现的单例模式具有线程安全性,同时也可以防止反射和反序列化攻击
。因此,枚举方式是实现单例模式的推荐方式之一。
4. 双重校验锁(Double-Checked Locking):
- 使用同步锁和双重检查来确保只有一个实例对象被创建。
- 单例模式的双重检查锁(Double-Checked Locking)是一种延迟初始化的实现方式,既能保证线程安全性,又能提高性能。
双重检查锁实现示例如下:
java
public class Singleton {
private volatile static Singleton instance;
// 添加自己需要的属性和方法
private int count;
private Singleton() {
// do something
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
// 添加自己需要的其他方法
public void incrementCount() {
count++;
}
}
在上述代码中,使用了关键字 volatile
来保证 instance 变量的可见性。在 getInstance()
方法中,首先检查 instance 是否已经被实例化,如果没有,再通过 synchronized
关键字加锁,进行实例化。这样可以避免多个线程同时进入实例化代码块,从而保证线程安全性。使用双重检查锁方式,可以避免每次调用 getInstance()
方法时都进行同步,提高了性能。
使用示例:
java
public class Main {
public static void main(String[] args) {
Singleton instance = Singleton.getInstance();
instance.setCount(10);
System.out.println("Count: " + instance.getCount()); // Output: Count: 10
Singleton anotherInstance = Singleton.getInstance();
anotherInstance.incrementCount();
System.out.println("Count: " + instance.getCount()); // Output: Count: 11
}
}
在上述示例中,通过 Singleton.getInstance()
来获取单例实例。两次获取到的实例对象是相同的,因此对实例的操作是共享的。
双重检查锁方式实现的单例模式能够保证线程安全性,并且在实例化时加锁,避免了不必要的同步开销。因此,双重检查锁方式是一种推荐的单例模式实现方式。
5. 静态内部类:
- 利用类的静态内部类特性,外部类加载时不会加载内部类,只有在第一次使用内部类时才会创建实例,因此具有延迟加载效果。
静态内部类实现示例如下:
java
public class Singleton {
private Singleton() {
// do something
}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
// 添加自己需要的属性和方法
private int count;
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
// 添加自己需要的其他方法
public void incrementCount() {
count++;
}
}
在上述代码中,私有的静态内部类 SingletonHolder
内部维护了一个私有的静态 final 实例 INSTANCE
。在静态内部类中创建单例对象,利用了类加载的特性,保证只有在需要时才会加载静态内部类,从而实现了延迟初始化。而且静态内部类的加载是线程安全的。通过 getInstance()
方法返回单例对象。
使用示例:
java
public class Main {
public static void main(String[] args) {
Singleton instance = Singleton.getInstance();
instance.setCount(10);
System.out.println("Count: " + instance.getCount()); // Output: Count: 10
Singleton anotherInstance = Singleton.getInstance();
anotherInstance.incrementCount();
System.out.println("Count: " + instance.getCount()); // Output: Count: 11
}
}
在上述示例中,通过 Singleton.getInstance()
来获取单例实例。两次获取到的实例对象是相同的,因此对实例的操作是共享的。
静态内部类实现的单例模式能够保证线程安全性,并且在需要时才创建单例对象。而且相比双重检查锁方式,静态内部类的方式更加简单、直观
。因此,静态内部类方式也是一种常用的单例模式实现方式。
6. 上面5种方式的使用抉择:
这些实现方式各有优缺点,选择合适的方式取决于具体的使用场景和需求。在并发环境下,需要考虑线程安全性。在某些情况下,需要延迟加载实例对象,避免资源浪费。
四、Spring Boot中单例模式的使用方式
1. 使用@Component注解
在需要使用单例对象的类上使用@Component注解来标识,Spring Boot会自动扫描并创建该对象的单一实例。
java
@Component
public class SingletonService {
// ...
}
在其他类中可以通过@Autowired注解将SingletonService注入进来,并且每次注入的都是同一个实例。
2. 使用@Scope注解
使用@Scope注解来指定Bean的作用域为单例模式。
java
@Component
@Scope("singleton")
public class SingletonService {
// ...
}
这样定义的SingletonService就会在整个应用程序中被共享,每次注入或获取该Bean时都会得到同一个实例。
3. 使用@Lazy注解
使用@Lazy注解来延迟加载单例Bean,只有在需要使用时才会创建。
java
@Component
@Lazy
public class SingletonService {
// ...
}
在使用过程中,如果没有主动使用该Bean,Spring Boot不会创建该实例。只有在需要使用时,才会创建并注入。