单例模式与工厂模式
# 单例模式与工厂模式
# 单例模式
单例模式(Singleton Pattern)是一个比较简单的模式,其定义如下:某一个类只有一个实例。
Singleton类称为单例类,通过使用private的构造函数确保了在一个应用中只产生一个实例,并且是自行实例化的(在Singleton中自己使用new Singleton())。
# 优点
- 唯一性:确保一个类只有一个实例,这对于需要频繁创建和销毁的对象非常有用,可以节省系统资源。
- 全局访问:提供了一个全局访问点,方便在整个应用程序中使用同一个对象实例。
- 资源控制:由于只有一个实例,可以更好地控制对资源的访问,例如数据库连接、线程池等。
- 性能优化:避免了频繁创建和销毁对象带来的性能开销,特别是在对象创建成本较高的情况下
# 饿汉式
public class Singleton {
private static final Singleton singleton = new Singleton();
private Singleton(){
}
public static Singleton getSingleton(){
return singleton;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
上述代码,是为类加载时就创建实例,适用于实例创建成本较低的情况,也称作饿汉式-单例
。
那么它是线程安全的吗?答案是肯定的,是线程安全的。
singleton
是一个静态常量,它在类加载时就被初始化。由于 Java 类加载机制是线程安全的
,因此在类加载时创建单例对象也是线程安全的。
# 懒汉式
除了饿汉式-单例模式,还有懒汉式-单例模式
,这是从性能上考虑的。
以下是几个主要原因:
- 延迟创建:在某些情况下,单例对象的创建可能需要消耗大量的资源(如数据库连接、文件读取等)。如果这些资源不是一开始就必需的,那么可以在需要的时候才创建,从而节省资源。
- 性能优化:如果单例对象不是一开始就必需的,那么在程序启动时创建它可能会拖慢启动速度。通过懒汉式创建,可以将创建时间推迟到真正需要的时候,从而加快程序的启动速度。
- 按需加载:某些情况下,单例对象可能在整个应用程序的生命周期中只被使用一次或几次。如果在类加载时就创建对象,可能会浪费资源。懒汉式创建可以确保对象在首次使用时才被创建,从而节省资源
public class Singleton {
private static Singleton instance = null;
private Singleton() {
// 私有构造函数,防止外部实例化
}
public static Singleton getSingleton() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
上述代码就是懒汉模式,不过它存在线程安全问题,在多线程环境下,如果多个线程几乎同时到达 if (instance == null)
判断语句,那么它们可能会同时进入该分支并创建多个实例,从而破坏单例模式的唯一性。
# 双检测锁模式
为了避免多线程环境下的线程安全问题,可以将 getSingleton()
方法声明为 synchronized
,以确保同一时刻只有一个线程能够进入该方法。
public static synchronized Singleton getSingleton() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
2
3
4
5
6
尽管解决了线程安全问题,但它还不够高效,一定要同步整个方法吗?
其实,当对象已经创建之后,就没必要加锁了,每次调用 getSingleton()
方法时都加锁,这会影响性能。那该如何改进呢?
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
如此一来,锁只会在创建对象的时候生效,性能得到了进一步提高。但是改的似乎不彻底,这次改动带来了线程安全问题。
# 加第二道检测
假设有两个线程 T1 和 T2 同时调用 getInstance
方法:
- T1 进入
getInstance
方法:- T1 检查
instance == null
,结果为true
。 - T1 准备进入
synchronized
块,但还没有进入。
- T1 检查
- T2 进入
getInstance
方法:- T2 检查
instance == null
,结果也为true
。 - T2 准备进入
synchronized
块,但还没有进入。
- T2 检查
- T1 进入
synchronized
块:- T1 创建
Singleton
实例并赋值给instance
。
- T1 创建
- T2 进入
synchronized
块:- T2 也创建
Singleton
实例并赋值给instance
,此时instance
被覆盖,导致两个线程创建了两个不同的实例。
- T2 也创建
为了解决上述问题,我们还需要在锁的代码中加一个检测。判断当前对象是否为空,并实现,T1线程创建对象后,T2线程在外面能够及时感知到变化。于是又进行了一番优化:
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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
使用 volatile
关键字可以确保 instance
变量的内存可见性。在多线程环境下,如果没有 volatile
,可能会出现指令重排序的问题,导致尚未构造完成的对象被其他线程提前访问到。
以上就是著名的懒汉模式的双检锁模式(Double-Checked Locking, DCL)模式。
在Java中,对象默认是不可以被复制的,若实现了Cloneable接口,并实现了clone方法,则可以直接通过对象复制方式创建一个新对象,对象复制是不用调用类的构造函数,因此即使是私有的构造函数,对象仍然可以被复制。
# 适用场景
单例模式适用于以下场景:
- 需要频繁访问的全局配置:例如,配置管理器、日志记录器等。
- 资源管理:例如,数据库连接池、线程池等。
- 状态维护:需要在全局范围内保持某个状态的对象。
- 协调共享资源:例如,缓存管理器、消息队列等。
# spring中的单例
Spring中,每个Bean默认就是单例的,这样做的优点是Spring容器可以管理这些Bean的生命期,决定什么时候创建出来,什么时候销毁,销毁的时候要如何处理,等等。如果采用非单例模式(Prototype类型),则Bean初始化后的管理交由J2EE容器,Spring容器不再跟踪管理Bean的生命周期。
“bean”是指由Spring IoC(Inversion of Control,控制反转)容器管理的对象。Spring容器负责创建这些对象,注入它们所需的依赖项,并管理它们的生命周期。简单来说,bean就是一个在Spring容器中注册并管理的对象实例。
Bean默认是单实例(Singleton)模式,这是出于性能和资源管理方面的考虑。以下是单实例模式的一些好处和劣势:
# 单实例模式的好处
- 资源节约:单实例模式可以减少对象的创建次数,从而减少内存消耗和其他资源的使用。如果一个对象的创建成本较高,那么单实例模式可以显著提高性能。
- 全局唯一性:单实例模式确保了在整个应用程序中,特定的Bean定义只有一个实例存在。这意味着无论何时从容器中请求该Bean,都会得到同一个实例,这对于需要全局唯一性的组件非常有用。
- 依赖注入的简便性:单实例模式使得依赖注入变得更加简单,因为所有依赖关系只需要在容器初始化时注入一次,之后就可以随时使用。
- 性能优化:由于单实例模式减少了对象创建的开销,因此在多次请求同一对象时,可以提高应用程序的整体性能。
- 状态管理:对于需要维护状态的对象,单实例模式可以确保状态的一致性,因为只有一个实例存在,不会出现状态不一致的问题。
# 单实例模式的劣势
- 状态共享:如果单实例Bean维护了一些可变的状态,并且这些状态在多个地方被访问或修改,那么可能会引发状态不一致的问题。在这种情况下,单实例模式可能会导致数据竞争和并发问题。
- 不适合多线程环境:如果单实例Bean的方法在多线程环境中被并发访问,并且这些方法修改了实例的内部状态,那么需要特别注意线程安全问题。否则,可能会导致数据不一致或其他并发问题。
- 单元测试复杂化:单实例模式可能会使得单元测试变得复杂,因为测试时可能需要重置或清理状态,或者需要模拟全局唯一的实例。
- 灵活性降低:单实例模式限制了在不同上下文中使用不同实例的能力。如果在某些场景下需要不同的配置或行为,单实例模式可能无法满足这些需求。
- 依赖注入的副作用:如果一个单实例Bean被大量依赖,那么它的行为或状态的变化会影响到整个应用程序。这可能会增加系统的复杂性和调试难度。
说到这,你还能记起常用的三种注入bean的方式是哪些吗?
构造器注入、setter方法注入、字段注入
# 工厂模式
它是一种常用的软件设计模式,属于创建型模式之一。
工厂设计模式主要有两种类型:
- 简单工厂模式(Simple Factory Pattern)
- 工厂方法模式(Factory Method Pattern)
# 简单工厂模式
简单工厂模式(Simple Factory Pattern)是由一个工厂对象决定创建出哪一个产品类的实例。简单工厂模式的核心是定义一个工厂类来负责创建产品对象,客户端通过调用工厂类的方法来获取产品对象。
// 简单工厂类
class AnimalFactory {
public static Animal createAnimal(String type) {
if ("cat".equals(type)) {
return new Cat();
} else if ("dog".equals(type)) {
return new Dog();
}
return null;
}
}
// 客户端代码
public class Main {
public static void main(String[] args) {
Animal cat = AnimalFactory.createAnimal("cat");
cat.makeSound(); // 输出: Meow
Animal dog = AnimalFactory.createAnimal("dog");
dog.makeSound(); // 输出: Woof
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
上述AnimalFactory
即为一个简单的动物工厂类。
简单工厂模式将对象的创建过程封装在一个工厂类中,客户端只需要调用工厂类的方法即可获得所需的产品对象,不需要关心对象的具体创建过程。实现了职责分离,不论对象(或者产品)如何变化,都不会影响工厂类,这是优势。
但它的弊端也显而易见,随着系统的发展,产品种类可能会越来越多,导致工厂类变得非常庞大,难以维护。且所有的产品创建都依赖于同一个工厂类,如果工厂类出现错误,会导致整个系统受到影响。
综上所述,它的应用有限。
# 过渡
既然,简单工厂模式存在问题,那是否可以改进下呢?但,改进有一个前提,你需要明确它目前存在哪些问题。那接下来一起看下————简单工厂模式存在什么问题吧
识别局限性
- 集中式管理:所有对象创建逻辑都集中在一个
xxFactory
的createXxx
方法中,这使得每次添加新类型的产品时都需要修改该方法。 - 违反开闭原则:每当需要引入新产品类型时,就需要修改现有代码,违背了“对扩展开放,对修改关闭”的原则。
- 耦合度高:客户端直接依赖于具体工厂类,如果工厂逻辑发生变化,可能会影响多个调用者。
- 集中式管理:所有对象创建逻辑都集中在一个
思考:
- 既然工厂类只有一个很危险,如果异常,会导致其余调用者都出问题,那我们就增加它,怎么增加呢?这些工厂类之间应该是有关系的?
- 通过接口、还是通过继承?
最终的结果是:抽象工厂+多个实现的工厂
为什么是抽象工厂,而不是接口工厂?
选择某样,或者某种方式,一定是基于一些因素考虑的。选择抽象工厂的因素有这些(仅供参考):
- 接口无法有具体实现(当然jdk8后 允许有默认实现),但抽象类允许多个实现方法,因为工厂可能有一些共有的方法
- 继承还有使用父类的方法,不必再去实现
那产品类又该如何实现?产品类其实可以随意定义,因为它不是关键点,但还是建议定义一个接口产品类。因为这些产品应该都有拥有相同的功能。可以后续扩写时,更便于识别。
# 工厂方法模式
那再来说说第二种——工厂方法模式。
Define an interface for creating an object,but let subclasses decide which class to instantiate.Factory Method lets a class defer instantiation to subclasses.(定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。)
工厂方法模式(Factory Method Pattern)定义了一个创建产品对象的接口,但是让实现这个接口的类决定实例化哪一个产品类。工厂方法模式使一个类的实例化延迟到其子类。这段话有点不容易理解,那我们直接上代码。
// 产品接口
interface Animal {
void makeSound();
}
// 具体产品类
class Cat implements Animal {
@Override
public void makeSound() {
System.out.println("Meow");
}
}
class Dog implements Animal {
@Override
public void makeSound() {
System.out.println("Woof");
}
}
// 抽象工厂类
abstract class AnimalFactory {
abstract Animal createAnimal();
}
// 具体工厂类
class CatFactory extends AnimalFactory {
@Override
Animal createAnimal() {
return new Cat();
}
}
class DogFactory extends AnimalFactory {
@Override
Animal createAnimal() {
return new Dog();
}
}
// 客户端代码
public class Main {
public static void main(String[] args) {
AnimalFactory catFactory = new CatFactory();
Animal cat = catFactory.createAnimal();
cat.makeSound(); // 输出: Meow
AnimalFactory dogFactory = new DogFactory();
Animal dog = dogFactory.createAnimal();
dog.makeSound(); // 输出: Woof
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
在工厂方法模式中,产品类是否需要实现同一个接口或继承同一个基类取决于具体的设计需求。如果所有产品类需要提供相同的行为或共享一些通用的状态和行为,可以定义一个公共接口或基类。如果产品类之间没有共同的行为或状态,可以定义各自独立的产品类,或者说,抽象工厂类使用Object类。
// 抽象工厂类
abstract class AnimalFactory {
abstract Object createAnimal();
}
2
3
4
工厂方式模式比简单工厂多了一个步骤,工厂方法——抽象工厂,然后是工厂类,最后才是产品。而简单工厂则是——工厂,然后是产品。很明显,工厂方法的解耦程度更高,它解决了简单工厂没实现的开闭原则。
当需要添加新的产品类时,只需新增一个具体的工厂子类即可,而不需要修改已有的工厂类。这样既实现了系统的扩展,又保持了现有代码的稳定性。现有的工厂类不需要做任何修改,符合开闭原则,即软件实体应当对扩展开放,对修改封闭。它的灵活性比简单工厂高。
但它也带来了新问题:
- 增加类的数量
- 类的增加:每增加一个产品类,就需要增加一个对应的工厂类,导致类的数量增加。
- 维护成本:随着类的数量增加,维护成本也会相应增加。
- 产品等级结构
- 产品等级结构:工厂方法模式适用于产品等级结构比较稳定的情况,如果产品等级结构经常发生变化,则不适合使用此模式。
这就是现实,尽可能的向设计原则靠近,可能会导致产品的复杂度增加、维护成本增加,因此在设计时,我们应该衡量两者从中取一个合适的度。
# Spring 中的工厂方法模式应用
工厂方法模式:Spring使用工厂模式通过BeanFactory ApplicationContext
简单工厂模式:BeanFactory.getBean() 根据id从IoC中获取Bean
# 1. Bean 的创建
Spring 容器负责创建和管理 Bean,这类似于工厂方法模式中的工厂类负责创建产品类的实例。在Spring中,通过配置文件或者注解来声明哪些类是Bean,并通过特定的方式(如构造函数、静态工厂方法等)来创建这些Bean。
# 2. 使用 @Bean
注解
在Spring Boot中,通常使用@Configuration
注解来标记配置类,并在其中使用@Bean
注解来声明Bean。这实际上是在定义一个工厂方法,该方法返回一个Bean的实例。
@Configuration
public class AppConfig {
@Bean
public Animal createCat() {
return new Cat();
}
@Bean
public Animal createDog() {
return new Dog();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
这里,createCat
和createDog
方法充当了工厂方法的角色,它们分别返回Cat
和Dog
类型的实例。