设计模式浅析(五) ·单例模式
日常叨逼叨
java设计模式浅析,如果觉得对你有帮助,记得一键三连,谢谢各位观众老爷😁😁
单例模式
概念
单例模式确保一个类只有一个实例,并提供一个全局访问点。
懒汉式:线程不安全
那么怎么构建一个单例模式,使得只返回唯一一个对象实例呢,我这里提供了一种方法
java
public class SingleInstance {
//利用一个静态变量来记录SingleInstance类的唯一实例。
public static SingleInstance singleInstance;
//把构造器声明为私有的,只有SingleInstance类内才可以调用构造器
private SingleInstance() {
}
public static SingleInstance getInstance() {
//如果它不存在,我们就利用私有的构造器产生一个SingleInstance实例并把它赋值到singleInstance静态变量中。请注意,如果我们不需要这个实例,它就永远不会产生。这就是"延迟实例化"(laxy instantiaze)
if (singleInstance == null) {
singleInstance = new SingleInstance();
}
//如果singleInstance不是null,就表示之前已经创建过对象。我们就直接跳到return语句。
return singleInstance;
}
}
看起来好像是没有什么大的问题 思路清晰,代码明确。那么我们写如下代码进行测试
java
public class Client extends Thread {
@Override
public void run() {
// 线程执行的代码
SingleInstance instance = SingleInstance.getInstance();
System.out.println(instance + " <线程"+Thread.currentThread().getId()+"正在运行>");
}
public static void main(String[] args) {
int i = 0;
while (i < 5) {
i++;
Client myThread = new Client();
myThread.start(); // 启动线程
}
}
}
运行结果:
com.jerry.singlePattern.SingleInstance@294eb59e <线程24正在运行>
com.jerry.singlePattern.SingleInstance@4c3154fa <线程23正在运行>
com.jerry.singlePattern.SingleInstance@294eb59e <线程20正在运行>
com.jerry.singlePattern.SingleInstance@294eb59e <线程21正在运行>
com.jerry.singlePattern.SingleInstance@4add1757 <线程22正在运行>
运行结果却有点出乎意料???我们不是单例模式吗?怎么创建出这么多的SingleInstance
对象??
出现上述问题的原因是多线程
在这个实现方式中,
getInstance()
方法会检查singleInstance
是否为null
,如果是则创建一个新的singleInstance
实例。问题出在多个线程可能同时检查到singleInstance
为null
,然后每个线程都创建一个新的实例,这就违背了单例模式的初衷。具体来说,假设有两个线程 T1 和 T2 同时调用
getInstance()
方法。由于singleInstance
的初始值为null
,T1 和 T2 都会进入if (singleInstance== null)
的判断。由于这两个线程是并发执行的,它们可能会同时进入这个判断条件,并且都通过判断,然后各自创建一个新的singleInstance
实例。这就导致了多个实例被创建,违反了单例模式的规则。
懒汉式:线程安全(同步方法)
我们可以将上述代码稍加修改,通过增加synchronized关键手到getInstance()方法中,我们迫使每个线程在进入这个方法之前,要先等候别的线程离开该方法。也就是说,不会有两个线程可以同时进入这个方法。
java
public class SingleInstance2 {
public static SingleInstance2 singleInstance;
private SingleInstance2() {
}
public static synchronized SingleInstance2 getInstance() {
if (singleInstance == null) {
singleInstance = new SingleInstance2();
}
return singleInstance;
}
}
运行结果:
com.jerry.singlePattern.SingleInstance2@5a506132 <线程23正在运行>
com.jerry.singlePattern.SingleInstance2@5a506132 <线程21正在运行>
com.jerry.singlePattern.SingleInstance2@5a506132 <线程22正在运行>
com.jerry.singlePattern.SingleInstance2@5a506132 <线程24正在运行>
com.jerry.singlePattern.SingleInstance2@5a506132 <线程20正在运行>
虽然实现了同步,实现了单例,但是上述方法的确是有一点不好。而比想象的还要严重一些的是:只有第一次执行此方法时,才真正需要同步。换句话说,一旦设置好singleInstance变量,就不再需要同步这个方法了。之后每次调用这个方法,同步都是一种累赘。
懒汉式:线程安全(双重检查锁定)
那么在进行一些程序上的修改,利用双重检查加锁(double-checked locking),首先检查是否实例已经创建了,如果尚未创建,"才"进行同步。这样一来,只有第一次会同步,这正是我们想要的。
java
public class SingleInstance3 {
public static volatile SingleInstance3 singleInstance;
private SingleInstance3() {
}
public static SingleInstance3 getInstance() {
if (singleInstance == null) {
synchronized (SingleInstance3.class) {
if (singleInstance == null) {
singleInstance = new SingleInstance3();
}
}
}
return singleInstance;
}
}
运行结果:
com.jerry.singlePattern.SingleInstance3@1aef3070 <线程24正在运行>
com.jerry.singlePattern.SingleInstance3@1aef3070 <线程21正在运行>
com.jerry.singlePattern.SingleInstance3@1aef3070 <线程20正在运行>
com.jerry.singlePattern.SingleInstance3@1aef3070 <线程23正在运行>
com.jerry.singlePattern.SingleInstance3@1aef3070 <线程22正在运行>
这种实现方式既保证了线程安全,又避免了不必要的同步,提高了效率。但是需要注意的是,Java 1.5 以前的版本对 volatile 的支持并不完善,因此在 Java 1.5 以前的版本中使用双重检查锁定可能会存在问题。
除了上述的几种方案,还有一些方案实现单例模式:
饿汉式:线程安全
java
public class SingleInstance1 {
// 在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快
public static SingleInstance1 singleInstance=new SingleInstance1();
private SingleInstance1() {
}
public static SingleInstance1 getInstance() {
return singleInstance;
}
}
运行结果
com.jerry.singlePattern.SingleInstance1@7070e0ed <线程20正在运行>
com.jerry.singlePattern.SingleInstance1@7070e0ed <线程24正在运行>
com.jerry.singlePattern.SingleInstance1@7070e0ed <线程21正在运行>
com.jerry.singlePattern.SingleInstance1@7070e0ed <线程23正在运行>
com.jerry.singlePattern.SingleInstance1@7070e0ed <线程22正在运行>
在这个例子中,SingleInstance1
类的singleInstance
成员变量在类加载时就被初始化了,由于JVM的类加载机制是线程安全的,所以这个过程是线程安全的。因此,后续的getInstance()
方法调用都是直接返回这个已经初始化好的instance
,不需要额外的同步措施。
这就是饿汉式单例模式如何保证线程安全的方式。由于它在类加载时就完成了初始化,所以不存在多线程并发访问的问题,因此是线程安全的。
静态内部类:线程安全
java
public class SingleInstance4 {
private static class SingletonHolder {
private static final SingleInstance4 INSTANCE = new SingleInstance4();
}
private SingleInstance4 (){}
public static final SingleInstance4 getInstance() {
return SingletonHolder.INSTANCE;
}
}
运行结果
com.jerry.singlePattern.SingleInstance4@7004943 <线程23正在运行>
com.jerry.singlePattern.SingleInstance4@7004943 <线程20正在运行>
com.jerry.singlePattern.SingleInstance4@7004943 <线程24正在运行>
com.jerry.singlePattern.SingleInstance4@7004943 <线程21正在运行>
com.jerry.singlePattern.SingleInstance4@7004943 <线程22正在运行>
静态内部类SingletonHolder
中包含了一个静态字段INSTANCE
,这个字段在静态内部类被加载时初始化。由于JVM的类加载机制保证了类的初始化过程是线程安全的,因此静态内部类中的静态字段只会被初始化一次,即在首次通过getInstance()
方法访问时。这就确保了单例的懒加载和线程安全。
当Singleton
类被加载时,SingletonHolder
类并不会立即被加载。只有当调用Singleton.getInstance()
方法时,SingletonHolder
类才会被加载,此时会初始化INSTANCE
字段。由于类的加载和初始化是由JVM在内部通过锁机制保证线程安全的,所以不需要额外的同步措施。
因此,静态内部类实现单例模式既能够实现懒加载,又能够利用JVM的类加载机制保证线程安全。这是单例模式中一种非常优雅且高效的实现方式。
枚举:线程安全
java
public enum SingleInstance5 implements Serializable {
INSTANCE_5;
// 如果需要,可以添加其他属性或方法
private String data;
public void setData(String data) {
this.data = data;
}
public String getData() {
return data;
}
// 其他需要的方法
}
运行结果:
INSTANCE_5 <线程20正在运行>
INSTANCE_5 <线程23正在运行>
INSTANCE_5 <线程22正在运行>
INSTANCE_5 <线程24正在运行>
INSTANCE_5 <线程21正在运行>
在这个例子中,SingleInstance5
是一个枚举类型,它只有一个实例 INSTANCE
。由于枚举的特性,这个实例是线程安全的,并且在整个应用程序中都是唯一的。
要获取这个单例的实例,只需要调用 SingleInstance5.INSTANCE
。要访问或修改它的属性,可以使用 SingleInstance5.INSTANCE.setData(data)
和 SingleInstance5.INSTANCE.getData()
方法。
关于序列化,由于枚举实例在反序列化时会被当作单个对象处理,因此不会出现重新创建实例的情况。也就是说,即使你尝试序列化并反序列化 SingletonEnum.INSTANCE
,你得到的仍然是同一个实例。
使用枚举实现单例模式的好处是简单、高效且线程安全,不需要担心多线程环境下的竞态条件或其他同步问题。此外,由于枚举类型在Java中是特殊的类,它们不能被继承或反射实例化,这进一步增强了单例的安全性。
优缺点
优点:
- 节省资源:由于系统内存中只存在一个实例,因此可以节约系统资源,尤其是对于那些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。
- 简化代码:单例模式提供了一个全局访问点,可以方便地在应用程序的任何地方访问该实例,无需频繁地创建和销毁对象,从而简化了代码。
- 避免数据不一致:单例模式可以确保所有对象都访问同一个实例,从而避免了由于多个实例导致的数据不一致问题。
缺点:
- 扩展性差:由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。如果需要扩展单例类的功能,通常需要修改源代码,这违反了开闭原则。
- 职责过重:单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起,导致职责过重,在一定程度上违背了"单一职责原则"。
- 线程安全问题:在多线程环境下,单例模式的实现需要考虑线程安全问题。如果实现不当,可能会导致多个实例被创建,从而引发数据不一致的问题。
- 滥用可能导致问题:如果滥用单例模式,例如将数据库连接池设计为单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。
代码相关代码可以参考 代码仓库🌐
ps:本文原创,转载请注明出处