设计模式:创建型

设计模式(Design pattern)代表了最佳实践,通常被有经验的软件开发人员所采用。

一、单例 - Singleton#

单例模式属于创建型设计模式,确保一个类只有一个实例,并提供该实例的全局访问点。一般有两种情况需要用到单例:

  • 资源共享:节约系统资源,不需要频繁创建和销毁的对象,如日志文件,应用配置等;
  • 资源控制:方便资源之间的互相通信,如线程池等。

以下是一些常见的单例应用场景:

  • Windows 的任务管理器(Task Manager)和回收站(Recycle Bin)在整个系统运行过程中,只维护仅有的一个实例。
  • 网站的计数器,一般也是采用单例模式实现,否则难以同步。
  • 在操作系统中,打印池(Print Spooler)是一个用于管理打印任务的应用程序,通过打印池用户可以删除、中止或者改变打印任务的优先级,在一个系统中只允许运行一个打印池对象,如果重复创建打印池则抛出异常。

实现: 使用一个私有构造函数、一个私有静态变量以及一个公有静态函数来实现。

1.1 饿汉式#

饿汉式是线程安全的,它采取直接实例化 uniqueInstance 的方式。

这种方式比较常用,它基于 classloader 机制避免了多线程的同步问题,但容易产生垃圾对象(丢失了延迟实例化带来的节约资源的好处)。

public class Singleton {
    // 急切的创建了uniqueInstance, 所以叫饿汉式
    private static Singleton uniqueInstance = new Singleton();

    private Singleton(){
    }

    public static Singleton newInstance(){
        return uniqueInstance;
    }

    // 如果我们只是想调用 Singleton.getStr(...),
    // 本来是没有必要生成 Singleton 实例的,但是饿汉式已经生成了
    public static String getStr(String str) {return "hello" + str;}
}

1.2 懒汉式#

所谓懒汉就是私有静态变量 uniqueInstance 被延迟实例化(lazy loading),这样做的好处是,如果没有用到该类,那么就不会实例化 uniqueInstance,从而节约资源。

这个实现在多线程环境下是不安全的,如果多个线程能够同时进入 if (uniqueInstance == null) ,并且此时 uniqueInstance == null,那么会有多个线程执行 uniqueInstance = new Singleton() 语句,这将导致实例化多次 uniqueInstance

public class Singleton {
    private static Singleton uniqueInstance;

    private Singleton(){}

    public static Singleton newInstance(){
        if(uniqueInstance == null)
            uniqueInstance = new Singleton();
        return uniqueInstance;
    }
}

为了解决线程安全的问题,我们可以直接在 newInstance() 方法上面直接加上一把 synchronized 同步锁。那么在一个时间点只能有一个线程能够进入该方法,从而避免了实例化多次 uniqueInstance

但是当一个线程进入该方法之后,其它试图进入该方法的线程都必须等待,即使 uniqueInstance 已经被实例化了。这会让线程阻塞时间过长,因此该方法有性能问题,不推荐使用

public static synchronized Singleton newInstance(){
    if(uniqueInstance == null)
        uniqueInstance = new Singleton();
    return uniqueInstance;
}

1.3 双重校验锁#

uniqueInstance 只需要被实例化一次,之后就可以直接使用了。加锁操作只需要对实例化那部分的代码进行,只有当 uniqueInstance 没有被实例化时,才需要进行加锁。

双重校验锁先判断 uniqueInstance 是否已经被实例化,如果没有被实例化,那么才对实例化语句进行加锁。

public class Singleton {
    // 必须使用 volatile,使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行
    private static volatile Singleton uniqueInstance;

    private Singleton() {}

    public static Singleton newInstance() {
        if (uniqueInstance == null) {
            synchronized (Singleton.class) {
                // 这一次判断也是必须的,不然会有并发问题
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

内层的第二次 if (uniqueInstance == null) 也是必须的。如果不加,当有两个线程都进入 if 语句块内,虽然在 if 语句块内有加锁操作,但是两个线程都会执行 uniqueInstance = new Singleton(),只是先后的问题。

volatile 关键字修饰也是很有必要的,使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行uniqueInstance = new Singleton() 这段代码其实是分为三步执行:

  1. Singleton 对象分配内存空间;
  2. 初始化 Singleton 对象;
  3. uniqueInstance 指向分配的内存地址。

但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1 > 3 > 2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 newInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。

1.4 静态嵌套类实现#

也叫做延迟初始化占位类模式。当 Singleton 类加载时,静态嵌套类 Holder 没有被加载进内存。只有当调用 newInstance() 方法从而触发 Holder.uniqueInstanceHolder 才会被加载,此时初始化 uniqueInstance 实例,并且 JVM 能确保 uniqueInstance 只被实例化一次

这种方式不仅具有延迟初始化的好处,而且由 JVM 提供了对线程安全的支持。

public class Singleton {
    
    private Singleton() {}

    private static class Holder {
        private static final Singleton uniqueInstance = new Singleton();
    }
    
    public static Singleton newInstance() {
        return Holder.uniqueInstance;
    }
}

1.5 枚举类#

该实现可以防止反射攻击。在其它实现中,通过 setAccessible() 方法可以将私有构造函数的访问级别设置为 public,然后调用构造函数从而实例化对象,如果要防止这种攻击,需要在构造函数中添加防止多次实例化的代码。该实现是由 JVM 保证只会实例化一次,因此不会出现上述的反射攻击。

该实现在多次序列化和序列化之后,不会得到多个实例。而其它实现需要使用 transient 修饰所有字段,并且实现序列化和反序列化的方法。

public enum Singleton {

    INSTANCE;

    private String objName;

    public String getObjName() {
        return objName;
    }

    public void setObjName(String objName) {
        this.objName = objName;
    }

    public static void main(String[] args) {

        // 单例测试
        Singleton firstSingleton = Singleton.INSTANCE;
        firstSingleton.setObjName("firstName");
        System.out.println(firstSingleton.getObjName());
        Singleton secondSingleton = Singleton.INSTANCE;
        secondSingleton.setObjName("secondName");
        System.out.println(firstSingleton.getObjName());
        System.out.println(secondSingleton.getObjName());

        // 反射获取实例测试
        try {
            Singleton[] enumConstants = Singleton.class.getEnumConstants();
            for (Singleton enumConstant : enumConstants) {
                System.out.println(enumConstant.getObjName());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
firstName
secondName
secondName
secondName

二、工厂 - Factory#

如果使用 new 的方式来创建对象的话,new 出来的这个对象和当前客户端(调用方)就耦合了,也就是说当前客户端(调用方)依赖着这个 new 出来的对象。工厂模式的好处就是解耦,这样对后续代码的修改,多方的合作都会带来好处。

工厂模式可以分成三类:

  • 简单工厂模式
  • 工厂方法模式
  • 抽象工厂模式

简单工厂模式是在工厂方法模式上缩减,抽象工厂模式是在工厂方法模式上再增强

2.1 简单工厂#

简单工厂把实例化的操作单独放到一个类中,这个类就成为简单工厂类,让简单工厂类来决定应该用哪个具体子类来实例化。

优点:

  • 屏蔽产品的具体实现,调用者只关心产品的接口;
  • 实现简单。

缺点:

  • 增加产品,需要修改工厂类,不符合开放-封闭原则;
  • 工厂类集中了所有实例的创建逻辑,违反了高内聚责任分配原则。

2.2 工厂#

定义一个用于创建对象的接口,让子类决定实例化哪个类。工厂方法使一个类的实例化延迟到其子类。

优点:

  • 继承了简单工厂模式的优点;
  • 符合开放-封闭原则。

缺点:

  • 增加产品,需要增加新的工厂类,导致系统类的个数成对增加,在一定程度上增加了系统的复杂性。

2.3 抽象工厂#

为创建一组相关或相互依赖的对象提供一个接口,而且无需指定它们的具体类。

抽象工厂模式创建的是对象家族,也就是很多对象而不是一个对象,并且这些对象是相关的,也就是说必须一起创建出来。而工厂方法模式只是用于创建一个对象,这和抽象工厂模式有很大不同。

优点

  • 多了一层抽象,减少了工厂的数量。

缺点:

  • 难以扩展产品族

三、建造者 - Builder#

建造者模式与抽象工厂模式有点相似,但是建造者模式返回一个完整的复杂产品,而抽象工厂模式返回一系列相关的产品;在抽象工厂模式中,客户端通过选择具体工厂来生成所需对象,而在建造者模式中,客户端通过指定具体建造者类型并指导 Director 类如何去生成对象,侧重于一步步构造一个复杂对象,然后将结果返回。如果将抽象工厂模式看成一个汽车配件生产厂,生成不同类型的汽车配件,那么建造者模式就是一个汽车组装厂,通过对配件进行组装返回一辆完整的汽车。

一个很常用的 Builder 就是 StringBuilder()

public class AbstractStringBuilder {
    protected char[] value;

    protected int count;

    public AbstractStringBuilder(int capacity) {
        count = 0;
        value = new char[capacity];
    }

    public AbstractStringBuilder append(char c) {
        ensureCapacityInternal(count + 1);
        value[count++] = c;
        return this;
    }

    private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        if (minimumCapacity - value.length > 0)
            expandCapacity(minimumCapacity);
    }

    void expandCapacity(int minimumCapacity) {
        int newCapacity = value.length * 2 + 2;
        if (newCapacity - minimumCapacity < 0)
            newCapacity = minimumCapacity;
        if (newCapacity < 0) {
            if (minimumCapacity < 0) // overflow
                throw new OutOfMemoryError();
            newCapacity = Integer.MAX_VALUE;
        }
        value = Arrays.copyOf(value, newCapacity);
    }
}
public class StringBuilder extends AbstractStringBuilder {
    public StringBuilder() {
        super(16);
    }

    @Override
    public String toString() {
        // Create a copy, don't share the array
        return new String(value, 0, count);
    }
}
public class Client {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder();
        final int count = 26;
        for (int i = 0; i < count; i++) {
            sb.append((char) ('a' + i));
        }
        System.out.println(sb.toString());
    }
}

四、原型 - Prototype#

原型模式要求对象实现一个可以 “克隆” 自身的接口,这样就可以通过复制一个实例对象本身来创建一个新的实例。这样一来,通过原型实例创建新的对象,就不再需要关心这个实例本身的类型,只要实现了克隆自身的方法,就可以通过这个方法来获取新的对象,而无须再去通过 new 来创建。

public abstract class Prototype {
    abstract Prototype myClone();
}
public class ConcretePrototype extends Prototype {

    private String filed;

    public ConcretePrototype(String filed) {
        this.filed = filed;
    }

    @Override
    Prototype myClone() {
        return new ConcretePrototype(filed);
    }

    @Override
    public String toString() {
        return filed;
    }
}
public class Client {
    public static void main(String[] args) {
        Prototype prototype = new ConcretePrototype("abc");
        Prototype clone = prototype.myClone();
        System.out.println(clone.toString());
    }
}

参考#

  1. 单例模式
  2. Java中的嵌套类、内部类、静态内部类
  3. 工厂模式
  4. 建造者模式

2019-2021 © lil-q