设计模式篇面试题
# 项目中用到哪些设计模式?
- 单例模式:DataSource单例
- 工厂模式:SqlSessionFactory创建SqlSession SqlSessionFactory.openSession()
- 模板方法模式:RabbitTemplate RedisTemplate RedisTemplate
- 代理模式:Spring的AOP做日志(JDK动态接口代理或Cglib类代理)
- 建造者模式:SqlSessionFactoryBuilder QueryBuilder
- **策略模式:**消息自动分发架构
# 什么是单例模式,有几种?
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
作用:通过单例模式可以保证系统中,应用该模式的这个类永远只有一个实例。节省内存空间。
**单例在JDK中的体现:**Runtime类、System类、Collections类
分类:
**饿汉式单例:**在类创建时就会创建实例对象
**懒汉式单例:**只有调用getinstance方法时才会创建实例对象
饿汉式又分静态变量饿汉式、静态代码块饿汉式、枚举饿汉式【饿汉式都是线程安全的】
懒汉式又分普通懒汉式【线程不安全】、DCL懒汉式【线程安全】、内部类懒汉式【线程安全】
饿汉式(工作中最常用的)
public class AudioPlayer {
// 私有构造器
private AudioPlayer() {}
// 这个对象是在类加载的时候进行创建的
private static final AudioPlayer instance = new AudioPlayer();
// 提供公共的访问方式
public static AudioPlayer getInstance() {
return instance;
}
}
2
3
4
5
6
7
8
9
10
11
静态代码块方式创建饿汉式单例
public class AudioPlayer {
// 私有构造器
private AudioPlayer() {}
private static final AudioPlayer instance;
static{
instance = new AudioPlayer()
}
// 提供公共的访问方式
public static AudioPlayer getInstance() {
return instance;
}
}
2
3
4
5
6
7
8
9
10
11
12
枚举饿汉式
public enum VideoPlayer{
INSTANCE;
}
2
3
懒汉式
public class VideoPlayer {
// 1.将构造器私有
private VideoPlayer() {}
// 2.使用成员变量保存创建好的对象
private static VideoPlayer instance;
// 3.提供公共的访问方式
public static VideoPlayer getInstance() {
// 在使用时创建单例对象
if (instance == null) {
instance = new VideoPlayer();
}
return instance;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
**风险:**并发下调用getInstance()方法可能会导致创建多个实例对象
**解决:**在getInstance()方法声明中使用synchronized修饰
**弊端:**导致线程每次调用这个方法时都需要获取锁和释放锁,影响性能
DCL懒汉式(双检锁) DCL:Double Check Locking
public class VideoPlayer {
// 1.将构造器私有
private VideoPlayer() {}
// 2.使用成员变量保存创建好的对象
private static volatile VideoPlayer instance;
// 3.提供公共的访问方式
public static VideoPlayer getInstance() {
// 先进行一次检查是否是第一次创建实例,若已创建则直接返回,减少锁的获取次数
if (instance == null) {
synchronized(VideoPlayer.class){
// 第二次检查是为了防止实例被重复创建
if(instance == null){
// 在使用时创建单例对象
instance = new VideoPlayer();
}
}
}
return instance;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
为什么要在instance上加volatile修饰?
volatile关键字能够保证线程的可见性和有序性,这里主要是用于保证线程的有序性;首先需要知道instance = new VideoPlayer();这样一句代码在CPU中其实分为几个步骤:
- 创建对象(分配内存空间)
- 调用构造方法(初始化成员变量)
- 给静态变量赋值
然后CPU会对我们的代码执行顺序进行一个优化,对于没有因果先后关系的代码,可能会改变执行顺序,比如在这段代码中,调用构造方法和静态变量赋值就没有因果关系,这两个的执行顺序就有可能被颠倒。因此在多线程的环境下就有可能会出现以下的执行顺序:(注:橙蓝代表不同线程)
导致多线程的情况下出现创建对象还未初始化,这个对象就被返回了的问题,最终拿到的对象属性不完整,从而引发更多奇奇怪怪的错误。
而被volatile修饰的变量就会在该变量的赋值语句之后产生一个内存屏障,规定在这条语句之前的赋值语句不能越过屏障执行,从而保证了线程的有序性,防止CPU在进行创建对象分配内存空间及初始化对象,也就是调用构造方法时出现乱序。
懒汉式 - 静态内部类(推荐)
public class VideoPlayer {
// 将构造器私有
private VideoPlayer() {}
// 创建内部类
private static class Holder {
static VideoPlayer instance = new VideoPlayer();
}
// 提供公共的访问方式
public static VideoPlayer getInstance() {
// 创建单例
return Holder.instance;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
从饿汉式可以看出,类加载时初始化静态成员变量是没有线程安全问题的,所以可以使用内部类的方式创建实例
**小贴士:**静态内部类和非静态内部类一样,都不会因为外部内的加载而加载,同时静态内部类的加载不需要依附外部类,在使用时才加载,不过在加载静态内部类的过程中也会加载外部类
面试话术
单例模式有懒汉式单例和饿汉式单例,饿汉式就是创建类的时候就创建实例,根据创建方式有分为了静态变量、静态代码块、枚举饿汉式,饿汉式的创建都是线程安全的,而懒汉式就是说要调用getinstance方法才会创建,不过这个方法名是自己定义的,规范一点是叫这个,懒汉式的话就有线程安全问题,于是有延伸了几个解决线程安全问题的懒汉式,比如DCL懒汉式,也就是双检锁懒汉式,以及内部类懒汉式,双检锁就是创建实例的时候要经过两层空判断,并且加锁创建和获取,而内部类懒汉式则是利用类似于饿汉式的方式,定义内部类,在内部类中通过静态变量创建实例,然后对外暴露获取实例的接口。
# 什么是策略模式?你项目中怎么运用策略模式的?
面试话术
策略模式就是利用各种策略类来替代冗余的if else代码,使得代码整洁,一定程度上提高了性能,并且新增策略场景不需要修改原代码,只需要添加一个策略类。让策略类都实现一个共同的策略接口,从而实现不同策略类的不同策略实现,然后将策略类都存放在一个集合中,根据策略类型来获取对应的策略类对象。
这个项目的一个消息分发架构采用的就是策略模式,这是基于EMQ的一个消息分发架构,我觉得它同样适用与kafka。大致的一个架构是这样的,最核心的两个部分是消息处理器加载器和消息分发处理器。
消息处理器加载器就如其名,加载消息处理器,也就是策略类,这里我们自定义了一个注解@Topic注解用于存放主题名,在定义消息处理器的时候加上这个注解也就代表着这个消息处理器订阅的主题,然后消息处理器加载器实现了ApplicationContextAware接口,在服务器启动的时候会自动调用里面的setApplicationContext()方法,通过这个方法获取应用上下文对象,然后通过策略类接口类型获取容器中所有的策略类,也就是容器中所有的消息处理器,然后以@Topic上的主题名为key,对应的消息处理器对象为value,存到一个Map里面方便后续获取。
消息分发器呢,由两个核心类组成,一个就是消息处理类,用来编写调用策略类方法的逻辑,就是通过主题名去存放消息处理器对象的map中获取对应的消息处理器,然后调用其业务方法。另一个则是EMQ的一个回调类,通过实现MqttCallbackExtended接口,实现里面的messageArrived()方法和connectComplete()方法,messageArrived会在接收到消息的时候调用,也就是在这里传入主题和接收到消息,调用消息处理类的消息处理方法。而connectComplete则是在EMQ连接成功时触发,这个方法的逻辑则是读取配置文件中配置的主题列表,遍历订阅这些主题,也就是主题的一个自动订阅。
这样一个全自动又优雅的消息分发架构就做好了,我们需要做的,只需要定义消息处理器,实现消息处理器接口,加上@Topic注解订阅主题就可以了。