作者:猫十二懿
❤️账号:CSDN 、掘金 、个人博客 、Github
公众号:猫十二懿
单例模式是一种创建型设计模式,它的目的是 确保一个类只有一个实例,并提供一个全局访问点来访问该实例 。在单例模式中,类自身负责创建自己的唯一实例,并确保在系统中只有一个实例存在。
单例模式(Singleton),保证一个类仅有一个实例,并提供一个访问它的全局访问点。
通常我们可以让一个全局变量使得一个对象被访问,但它不能防止你 实例化多个对象。一个最好的办法就是,让类自身负责保存它的唯一实例。 这个类可以保证没有其他实例可以被创建,并且它可以提供一个访问该实例 的方法。
Singleton类,定义一个GetInstance操作,允许客户访问它的唯一实例。GetInstance是一个静态方法,主要负责创建自己的唯一实例。
/**
* @author Shier
* CreateTime 2023/5/14 9:23
* 单例模式
*/
public class Singleton {
private static Singleton singleton;
/**
* 无参构造 防止外部代码利用new来实例化的可能
*/
private Singleton() {
}
/**
* 只能通过此途径获取Singleton实例
* @return
*/
public static Singleton getInstance() {
// 为空,则创建实例
if (singleton == null) {
singleton = new Singleton();
}
// 不为空,则已创建,直接返回实例
return singleton;
}
}
客户端:
/**
* @author Shier
* CreateTime 2023/5/14 9:27
*/
public class SingletonClient {
public static void main(String[] args) {
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
if (instance1 == instance2) {
System.out.println("两个是同一个实例对象");
}
}
}
最终得到的:“两个是同一个实例对象”,因此单例模式保证了可以被唯一实例化。当然单例模式不止这些好处。下面再来细说
假设我们有一个流水号生成器,用于在系统中生成唯一的流水号。这个流水号生成器需要保证在整个系统中只有一个实例存在,以确保生成的流水号是唯一的。也就是类似于我们在外面吃饭点菜呀什么的,买单后,商家会给你一个单号,到你了,就让你去拿属于你的饭菜。
首先,我们创建一个名为SerialNumberGenerator
的类,用于生成流水号:
/**
* @author Shier
* CreateTime 2023/5/14 9:30
* 流水号例子
*/
public class SerialNumberGenerator {
private int serialNumber;
/**
* 外界可以实例化SerialNumberGenerator
*/
public SerialNumberGenerator() {
serialNumber = 0;
}
public int generateSerialNumber() {
serialNumber++;
return serialNumber;
}
}
现在,我们可以在不同的地方创建多个SerialNumberGenerator
的实例并尝试生成流水号:
/**
* @author Shier
* CreateTime 2023/5/14 9:31
* 客户端
*/
public class Main {
public static void main(String[] args) {
SerialNumberGenerator generator1 = new SerialNumberGenerator();
int serialNumber1 = generator1.generateSerialNumber();
System.out.println("流水号1: " + serialNumber1);
SerialNumberGenerator generator2 = new SerialNumberGenerator();
int serialNumber2 = generator2.generateSerialNumber();
System.out.println("流水号2: " + serialNumber2);
}
}
结果:
流水号1: 1
流水号2: 1
不使用单例模式存在的问题:
SerialNumberGenerator
实例,每个实例都会有自己的serialNumber
变量。这可能导致在不同的实例中生成相同的流水号,从而产生重复的流水号。SerialNumberGenerator
实例都会维护自己的serialNumber
变量,这导致每个实例生成的流水号不会互相影响。这意味着如果我们想要全局唯一的流水号,就需要在各个地方共享同一个SerialNumberGenerator
实例,而不是创建多个实例。SerialNumberGenerator
实例会增加代码的复杂性。我们需要确保在需要流水号的每个地方都使用同一个实例,否则会出现上述问题。这增加了代码维护的难度,并且容易在系统的不同部分中出现错误使用的情况。使用单例模式的实现方式有多种,这里我将使用线程安全的懒汉式单例模式,具体实现如下:
懒汉式单例模式下面再介绍
/**
* @author Shier
*/
public class SerialNumberGenerator1 {
private static volatile SerialNumberGenerator instance;
private int serialNumber;
/**
* 防止直接实例化SerialNumberGenerator1
*/
private SerialNumberGenerator1() {
serialNumber = 0;
}
/**
* 获取SerialNumberGenerator1的唯一方式
* @return
*/
public static SerialNumberGenerator getInstance() {
if (instance == null) {
synchronized (SerialNumberGenerator.class) {
if (instance == null) {
instance = new SerialNumberGenerator();
}
}
}
return instance;
}
/**
* 生成的流水号
* @return
*/
public int generateSerialNumber() {
synchronized (SerialNumberGenerator.class) {
serialNumber++;
return serialNumber;
}
}
}
在这个实现中,我们使用了一个静态变量instance
来保存SerialNumberGenerator
的唯一实例。getInstance()
方法返回这个实例,如果它不存在,则会创建一个新的实例。generateSerialNumber()
方法负责生成流水号,使用synchronized
关键字来确保线程安全。
现在,我们可以在不同的地方调用SerialNumberGenerator.getInstance().generateSerialNumber()
来生成流水号,并确保它们是唯一的:
/**
* @author Shier
*/
public class Main1 {
public static void main(String[] args) {
int serialNumber1 = SerialNumberGenerator1.getInstance().generateSerialNumber();
System.out.println("流水号 1: " + serialNumber1);
int serialNumber2 = SerialNumberGenerator1.getInstance().generateSerialNumber();
System.out.println("流水号 2: " + serialNumber2);
}
}
通过使用单例模式,我们解决了不使用单例模式存在的问题:
SerialNumberGenerator
的唯一实例可以在系统中被多次调用,但它们都会引用同一个实例,因此可以保证生成的流水号是唯一的。serialNumber
变量,从而避免了重复生成流水号的问题。SerialNumberGenerator
实例存在于系统中,因此我们不必在各个地方管理和控制实例的创建和使用,这降低了代码复杂性并减少了出错的可能性。使用单例模式来实现流水号生成器类,可以确保生成的流水号是唯一的,避免资源浪费,并保证生成的流水号顺序一致。所有部分都可以通过访问流水号生成器的全局访问点来获取唯一的流水号,从而简化了流水号的生成和管理。
在上面例子当中使用到了懒汉式单例模式,下面我们再来看看单例模式的实现方式:
单例模式的实现方式有多种,以下是几种常见的实现方式:
以上是常见的几种单例模式的实现方式。每种实现方式都有其适用的场景和特点,选择哪种方式取决于具体的需求和设计考虑。同时,需要注意线程安全性和性能等因素,确保实现的单例模式符合要求。
下面再说说最常用的懒汉式和饿汉式单例模式实现
在第一次使用时才创建实例,称为懒汉式。实现方式是在类内部定义一个私有的静态变量作为实例,然后提供一个公有的静态方法来获取该实例。在方法内部判断实例是否已经存在,如果存在则直接返回,如果不存在则创建一个新的实例并返回。(我就是很懒,你不用我,我就不管你,当你用我的时候才回去给你提供需要的东西)
public class Singleton {
private static Singleton instance;
// 私有构造函数
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
在懒汉式单例模式中,我们将单例对象的实例化放在静态方法
getInstance()
中。当首次调用getInstance()
时,会检查实例是否已经存在。如果实例为null,表示还未创建,则进行实例化。这种方式实现了延迟加载,只有在需要使用单例对象时才会创建它。
需要注意的是,在多线程环境下,懒汉式单例模式需要考虑线程安全性。上述代码的实现并未考虑线程安全,可能会导致多个线程同时创建实例。为了解决这个问题,可以在getInstance()
方法中添加线程同步机制,如使用synchronized
关键字或者双重检查锁定等方式,来确保在多线程环境下仅有一个实例被创建。
下面再来说多线程单例模式
饿汉式单例模式: 在类加载时就创建实例,称为饿汉式。实现方式是在类内部定义一个私有的静态变量,并直接创建实例赋值给它,然后提供一个公有的静态方法来获取该实例。(快要饿死了,很需要吃东西,所以说菜一上来就立马开吃,不管你三七二十四)
public class Singleton {
private static final Singleton instance = new Singleton();
// 私有构造函数
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
在饿汉式单例模式中,我们将单例对象的实例化放在静态常量中,并且将构造函数设为私有,防止外部代码创建实例。在类加载时,静态常量
instance
就会被创建,并且通过公共的静态方法getInstance()
返回该实例。由于实例在类加载时就被创建,因此可以保证单例的唯一性。
同步方法(Synchronized Method):在getInstance()
方法上使用synchronized
关键字,确保在同一时间只有一个线程可以进入方法,从而避免并发创建多个实例。这种方式简单易行,但 可能存在性能问题,因为每次调用getInstance()
都需要进行同步。
public class Singleton {
private static Singleton instance;
// 私有构造函数
private Singleton() {
}
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
双重检查锁定(Double-Checked Locking):使用双重检查锁定机制,在getInstance()
方法内使用synchronized
块,只在实例为null
时才进行同步,避免了每次调用都进行同步的开销。这种方式在多线程环境下能够保证线程安全性,同时也具有较好的性能。
public class Singleton {
private static volatile Singleton instance;
// 私有构造函数
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
需要注意的是,这种方式需要将instance变量声明为
volatile
,以确保在多线程环境下的可见性和禁止指令重排序。
静态内部类(Static Inner Class):利用类的静态内部类来实现单例模式。静态内部类在首次使用时加载,且只加载一次,因此保证了线程安全性和延迟加载。
public class Singleton {
private Singleton() {
// 私有构造函数
}
private static class SingletonHolder {
private static final Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
在多线程环境下,上述实现方式都能够确保只有一个实例被创建,并提供线程安全的访问。选择哪种方式取决于具体的需求和性能要求。此外,还可以结合其他技术,如使用枚举类型实现单例,因为枚举类型的实例是线程安全且唯一的。
需要注意的是,在某些情况下,可能需要权衡线程安全和性能之间的取舍。在高并发环境下,可以考虑使用其他的并发控制方式,如使用锁、使用线程安全的并发容器等来实现单例模式。
单例模式的核心思想是将类的实例化过程控制在一个特定的范围内,以确保只有一个实例被创建并且全局可访问。这种模式在需要共享资源或避免重复创建相同对象的场景中非常有用。
以下是单例模式的一般实现步骤:
单例模式的优点:
单例模式缺点:
因此,应谨慎使用单例模式,并根据具体需求和设计考虑选择适当的实现方式,以确保单例的正确性和适用性。
单例模式使用场景: