本文 首发于 🍀 永浩转载 请注明 来源

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