设计模式:创建型
设计模式(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()
这段代码其实是分为三步执行:
- 为 Singleton 对象分配内存空间;
- 初始化 Singleton 对象;
- 将 uniqueInstance 指向分配的内存地址。
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1 > 3 > 2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 newInstance()
后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。
1.4 静态嵌套类实现#
也叫做延迟初始化占位类模式。当 Singleton 类加载时,静态嵌套类 Holder 没有被加载进内存。只有当调用 newInstance()
方法从而触发 Holder.uniqueInstance
时 Holder 才会被加载,此时初始化 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());
}
}