36、【对线面试官】设计模式
熟悉哪些常见的设计模式?
- 常见的工厂模式、代理模式、模板方法模式、责任链模式、单例模式、包装设计模式、策略模式等都是有所了解的
- 项目手写代码用得比较多的,一般就模板方法模式、责任链模式、策略模式、单例模式吧
- 像工厂模式、代理模式这种,手写倒是不多,但毕竟Java后端一般环境下都用Spring嘛,所以还是比较熟悉的
手写单例模式
- 单例模式一般会有好几种写法
- 饿汉式、简单懒汉式(在方法声明时加锁)、DCL双重检验加锁(进阶懒汉式)、静态内部类(优雅懒汉式)、枚举
- 所谓「饿汉式」指的就是还没被用到,就直接初始化了对象。所谓「懒汉式」指的就是等用到的时候,才进行初始化
//DCL懒汉式
public class Singleton1 {
//第一次减少锁的开销、第二次防止重复、volatile防止重排序导致实例化未完成
private Singleton1(){}
private volatile static Singleton1 singleton;
public static Singleton1 getInstance(){
if(singleton == null){ //线程1,2,3到达这里
synchronized (Singleton1.class){//线程1到这里开始继续往下执行,线程2,3等待
if(singleton == null){//线程1到这里发现instance为空,继续执行if代码块
//执行晚后退出同步区域,然后线程2进入同步代码块,如果在这里不再加一次判断
//就会造成instance再次实例化
singleton = new Singleton1();
//new Singleton1();可以分解为3行伪代码
//1、memory = allocate() //分配内存
//2、ctorInstanc(memory) //初始化对象
//3.调用构造函数,
//4.返回地址给引用。而cpu为了优化程序,可能会进行指令重排序,打乱这3,4这几个步骤,导致实例内存还没分配,就被使用了。
//线程A和线程B举例。线程A执行到new Singleton(),开始初始化实例对象,由于存在指令重排序,这次new操作,先把引用赋值了,还没有执行构造函数。
//这时时间片结束了,切换到线程B执行,线程B调用new Singleton()方法,发现引用不等于null,就直接返回引用地址了,然后线程B执行了一些操作,就可能导致线程B使用了还没有被初始化的变量。
//volatile防止重排序导致实例化未完成,就将对象赋值使用
}
}
}
return singleton;
}
}
//静态内部类 懒汉式
public class Singleton2 {
private Singleton2() {
}
private static class SingletonHolder {
private final static Singleton2 INSTANCE = new Singleton2();
}
public static Singleton2 getInstance() {
return SingletonHolder.INSTANCE;
}
}
//枚举
public class Singleton3 {
private Singleton3(){
}
/**
* 枚举类型是线程安全的,并且只会装载一次
*/
private enum Singleton{
INSTANCE;
private final Singleton3 instance;
Singleton(){
instance = new Singleton3();
}
private Singleton3 getInstance(){
return instance;
}
}
public static Singleton3 getInstance(){
return Singleton.INSTANCE.getInstance();
}
}
//枚举
public class Singleton4 {
private Singleton4(){}
public enum SingletonEnum {
SINGLETON;
private Singleton4 instance = null;
SingletonEnum(){
instance = new Singleton4();
}
public Singleton4 getInstance(){
return instance;
}
}
public static Singleton4 getInstance() {
return SingletonEnum.SINGLETON.getInstance();
}
}
那你们用的哪种比较多?
- 一般我们项目里用静态内部类的方式实现单例会比较多(如果没有Springl的环境下),代码简洁易读
- 如果有Spring环境,那还是直接交由Spring容器管理会比较方便(Spring默认就是单例的)
- 枚举一般我们就用它来做「标识」吧,而DCL这种方式也有同学会在项目里写(在一些源码里也能看到其身影),但总体太不利于阅读和理解
- 总的来说,用哪一种都可以的,关键我觉得要看团队的代码风格吧(保持一致就行),即便都用「饿汉式」也没啥大的问题(现在内存也没那么稀缺,我认为可读性比较重要)
我看你在DCL的单例代码上,写了volatile修饰嘛?为什么呢?
- 指令是有可能乱序执行的(编译器优化导致乱序、CPU缓存架构导致乱序、CPU原生重排导致乱序)
- 在代码new Object的时候,不是一条原子的指令,它会由几个步骤组成,在这过程中,就可能会发生指令重排的问题,而volatile这个关键字就可以避免指令重排的发生。
那你说下你在项目里用到的设计模式吧?
嗯,比如说,我这边在处理请求的时候,会用到责任链模式进行处理(减免if else并且让项目结构更加清晰)
- 在处理公共逻辑时,会使用模板方法模式进行抽象,具体不同的逻辑会由不同的实现类处理(每种消息发送前都需要经过文案校验,所以可以把文案校验的逻辑写在抽象类上)
代理模式手写的机会比较少(因为项目一般有Spring:环境,直接用Spring的AOP代理就好了)
我之前使用过AOP把「监控客户端」封装以「注解」的方式进行使用(不用以硬编码的方式来进行监控,只要有注解就行)
那你能聊聊Spring使用到的常见设计模嘛?
- 比如,Spring IOC容器可以理解为应用了「工厂模式」(通过ApplicationContext或者BeanFactory去获取对象)
- Spring的对象默认都是单例的,所以肯定是用了「单例模式」(源码里对单例的实现是用的DCL来实现单例)
- Spring AOP的底层原理就是用了「代理模式」,实现可能是JDK动态代理,也可能是CGLIB动态代理
- Spring有很多地方都用了「模板方法模式」,比如事务管理器(AbstractPlatformTransactionManager),getTransaction定义了框架,其中很多都由子类实现
- Spring的事件驱动模型用了「观察者模式」,具体实现就是ApplicationContextEvent、ApplicationListener