Skip to content

单例模式

最近在看到一篇关于单例模式的文章,在思考单例模式和静态方法后,我仔细看了看目前项目中的单例模式和静态方法的使用, 记录一下学习到的内容.在我目前开发的项目中静态方法基本都在Util包内,它们数多是用来处理加工判断对象/字符串等有 关的静态方法,而使用单例模式的包括一些读取配置文件的类和一些第三方包中需要使用的对象.

饿汉模式

普通饿汉模式

java
//无线程安全,在类定义的时候就被实例化了,不能延迟加载
public class Singleton1 {
    private static Singleton1 instance =new Singleton1();
    private Singleton1(){};//构造器私有化
    private static Singleton1 getInstance(){
        return instance;
    };
}

枚举

java
public class MyConfig {
    private MyConfig(){};//构造器私有化
    static enum SingletonEnum{
        INSTANCE;//单例对象
        private MyConfig myConfig;
        private SingletonEnum(){
            myConfig = new MyJsonConfig();
        }
        private MyConfig get(){
            return myConfig;
        }
    }
    public static MyConfig instance(){
        return SingletonEnum.INSTANCE.get();
    }

}

懒汉模式

双重检查锁

单个锁的话,会导致存在两个线程同时进入 if (singleton == null) 的时候,不同步的问题,而双重锁仍然存在问题,实际上利用静态内部类或者枚举实现单例是更好的做法.

DCK失效源于singleton = new Singleton()可以被分为三步:

~ Singleton对象分配空间

~ 在分配的空间中构造对象

~ 使instance指向分配的空间

由于编译器乱序执行,上述顺序可能被打乱,存在某种情况:

~. 线程A进入了getInstance方法,执行了1和3,然后挂起。此时:singleton不为null,但singleton指向的内存去没有对象

~. 线程B进入了getInstance方法,instance不为null,就直接return instance。

java
public class Singleton {
    private static volatile Singleton singleton;
    private Singleton() {};
    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {//二次判断,为了避免线程一在创建实例之后,线程二进来也创建了新实例
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
} 
//volatile 
//1.这个变量不会在多个线程中存在复本,直接从内存读取。
//2.这个关键字会禁止指令重排序优化。在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。

静态内部类

public class Singleton3 {
    private Singleton3(){};//构造器私有化
    private static class SingletonHolder{
        private static final Singleton3 instance = new Singleton3();
    }
    public Singleton3 getInstance(){//主动调用才会被实例化,无线程安全
        return SingletonHolder.instance;
    }
}

序列化问题

由于序列化为会生成一个新的对象,导致破坏单例.

    @Test
    void D(){
        Singleton4 singleton4 = Singleton4.getSingleton4Instance();
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try{
            ObjectOutputStream out = new ObjectOutputStream(bos);
            out.writeObject(singleton4);
            ByteArrayInputStream  bis = new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bis);
            Singleton4 singleton41 = (Singleton4)ois.readObject();
            System.out.println(singleton4==singleton41);
        }catch (Exception e){
        }
    }
//输出为 false

解决思路:添加readResolve方法,原因在于 readObject()方法中通过判断是否存在readRosolveMethod,来决定是否通过构造器创建对象

添加:

public Object readResolve(){
        return SingletonEnum.INSTANCE.getSinglton4Instance();
    }

静态方法和单例模式

什么时候用什么:静态方法和单例模式需要灵活使用,比如在SqlUtils中可以针对Spring中的JDBCTemplate对象通过单例模式创建该实例, 同时也可以包含有如sql拼接等工具方法可以作为静态方法。从面相对象的角度来说,静态方法是基于对象的, 而单例模式是面向对象的,比如系统读取配置文件,文件中的配置和属性是不变的, 无论是静态方法还是单例模式都可以实现获取其唯一一份数据的目的,不过这些数据是通过面向对象的思想来获取的, 所以用单例模式更好,对于工具方法而言,这些方法往往不依靠其他类方法变量,同时该方法与它所在的类无关,所以往往作为静态方法。

工厂模式

  • 工厂模式:抽象工厂,简单工厂,工厂方法,我认为工厂模式的核心要素在于充分利用java多态的特点, 实现一种可扩展的设计思路,且对象内部相对复杂,new创建相对不便,此时便可以使用工厂模式.
  • 有别于 策略模式,工厂模式注重 对象的创建,策略模式注重 行为的选择.
  • 工厂模式是一种建造者模式,也是一种解耦模式.

建造者模式

非常常用的设计模式,最简单的使用就是lombok中的@Builder, 当然自己写可以提供更完整的自定义功能. 我认为建造者模式比较适用于复杂对象的构建,且该对象内部往往包含了具体的方法,如果该方法的调用需要传递 的参数过多/参数可选等,为了避免大量的方法重载或者参数过多,就可以使用建造者模式.

通常情况下没有必要在类似model,vo的类中使用,如果单纯为了链式调用比较方便, 可以使用lombok中的@Accessors(chain = true)或者@With实现, 也可以自己在get/set方法中return this;

简单实现

java
public class Client<T>{

    public static<T> Client.Builder<T> builder() {
        return new Client.Builder<>();
    }
    
    private T t;
    
    public void doSomething(){
        //
    }
    public Client(Builder<T> builder) {
        this.t = builder.t;
    }
    
    public static class Builder<T> {
        private T t;
        public Builder<T> t(T t){
            this.t = t;
            return this;
        } 
        public Client<T> build(){
            return new Client<T>(this){};
        }
    }
}

模板模式

实际上模板模式应用非常广泛,比如 流程 a->b->c 三步操作是固定的,通过抽象方法对改流程的三步调用进行提取,为模板, 流程的外部调用永远只调用该模板

java
public interface flow{
    void a();
    void b();
    void c();
    default d(){
        a();
        b();
        c();
    }
}

策略模式

策略模式可以广泛应用到项目中来解决if-else过多的问题,策略模式优点是可以提高扩展性,更加灵活, 缺点是增加了更多类文件,且没有一个全局能看到所有分支结构的地方.

我认为策略模式核心基本遵从查表法,无论怎么实现,目的都是为了根据不同的条件找到对应的策略 以下是我在编程中使用的策略模式的方式

枚举

枚举中定义抽象方法,每个枚举实例中做具体的实现,调用方只需要根据枚举实例完成调用即可.

java
public enum ProcessType {
    P_A(){
        @Override
        public void process(Process process) {
            //toDo
        }
    },
    P_B(){
        @Override
        public void process(Process process) {
            //toDo
        }
    };
    public abstract void process(Process process);
}

缺点: 枚举更加适合本身就依赖枚举的模型,相比抽象接口该模式不利于第三方扩展.

spring bean 注入

在spring环境下可以直接注入某个接口的所有实现,在便利接口取到指定的实现. 比如:

java
public interface IHandler{
    String type();
    void execute();
}
public class AHandler implements IHandler{
    String type(){ return "A";};
    void execute(){};
}
public class BHandler implements IHandler{
    String type(){ return "B";};
    void execute(){};
}

调用方只需要遍历取值调用即可.

java
@Service
@RequiredArgsConstructor
public class HandlerService{
    private final List<IHandler> iHandlers;
    public void test(String type){
        Optional<IHandler> first = iHandlers.stream()
                .filter(a -> Objects.equals(a.type(), type)).findFirst();
    }
}

仿照spring注入

在非spring环境下,实际上可以通过ConcurrentHashMap模拟实现bean的注入, 该方法不需要IHandler专门声明一个type()方法用来区分.

java
public interface IHandler{
    void execute();
}
public class RegistryManger {
    private static ConcurrentHashMap<String, IHandler> registryies = new ConcurrentHashMap<>();
    public static void register(String eventType,IHandler handler) {
        registryies.put(eventType,handler);
    }
}

调用方可直接根据type取到相应的ihandler进行处理

适配器模式

适配器模式在项目中也是非常常用的设计思路,可以提高代码的扩展性,此外适配器模式可以用在外部插件中, 通过接口抽象提供一个基本能力.

  1. 抽象接口逻辑 I
  2. I的实现A
  3. B 继承A

通过B对A的重写,此时B的业务逻辑就可以认为是一种基于A的一种适配,实际上 接口中的抽象方法我认为也可以被理解为适配器模式, default实现在不同的实现类中可以重写也可以不重写,这也是一种适配器的思路.