设计模式(一)——创建型设计模式

739 阅读6分钟

单例模式

所谓单例,就是整个程序有且仅有一个实例。该类负责创建自己的对象,同时确保只有一个对象被创建。

定义

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

私有构造函数保证了不能通过构造函数来创建对象实例,只能通过公有静态函数返回唯一的私有静态变量。

实现方式

懒汉式-线程不安全

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

public class Singleton {

    private static Singleton single;

    private Singleton() {
    }

    public static Singleton getSingle() {
        if (single == null) {
            single = new Singleton();
        }
        return single;
    }
}

饿汉式-线程安全

线程安全,比较常用,但容易产生垃圾,因为一开始就初始化,也丢失了延迟实例化带来的节约资源的好处。

public class Singleton {  
    private static Singleton single = new Singleton();  
    private Singleton (){
        
    }  
    public static Singleton getSingle() {  
        return single;  
    }  
}

懒汉式-线程安全

只需要对 getSingle() 方法加锁,那么在一个时间点只能有一个线程能够进入该方法,从而避免了实例化多次 single。

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

public static synchronized Singleton getSingle() {
    if (single == null) {
        single = new Singleton();
    }
    return single;
}

双重校验锁(DCL)-线程安全

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

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

public class Singleton {

    private volatile static Singleton single;

    private Singleton() {
    }

    public static Singleton getSingle() {
        if (single == null) {
            synchronized (Singleton.class) {
                if (single == null) {
                    single = new Singleton();
                }
            }
        }
        return single;
    }
}

考虑下面的实现,也就是只使用了一个 if 语句。在 single == null 的情况下,如果两个线程都执行了 if 语句,那么两个线程都会进入 if 语句块内。虽然在 if 语句块内有加锁操作,但是两个线程都会执行 single = new Singleton(); 这条语句,只是先后的问题,那么就会进行两次实例化。因此必须使用双重校验锁,也就是需要使用两个 if 语句:第一个 if 语句用来避免 single 已经被实例化之后的加锁操作,而第二个 if 语句进行了加锁,所以只能有一个线程进入,就不会出现 single == null 时两个线程同时进行实例化操作

if (single == null) {
    synchronized (Singleton.class) {
        single = new Singleton();
    }
}

single 采用 volatile 关键字修饰也是很有必要的, single = new Singleton(); 这段代码其实是分为三步执行:

  • 为 single 分配内存空间
  • 初始化 single
  • 将 single 指向分配的内存地址

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

使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。

指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。

静态内部类实现

只有第一次调用 getSingle 方法时,虚拟机才加载 Inner 并初始化 instance ,只有一个线程可以获得对象的初始化锁,其他线程无法进行初始化,保证对象的唯一性。目前此方式是所有单例模式中最推荐的模式,但具体还是根据项目选择。

public class Singleton {

    private Singleton() {
    }

    private static class Inner {
        private static final Singleton instance = new Singleton();
    }

    public static Singleton getSingle() {
        return Inner.instance;
    }
}

枚举实现

默认枚举实例的创建是线程安全的,并且在任何情况下都是单例。

  • 枚举类隐藏了私有的构造器。
  • 枚举类的域是相应类型的一个实例对象
public enum Singleton  {
    INSTANCE 
 
    //doSomething 该实例支持的行为
      
    //可以省略此方法,通过Singleton.INSTANCE进行操作
    public static Singleton getInstance() {
        return Singleton.INSTANCE;
    }
}

枚举实例在日常开发是很少使用的,就是很简单以导致可读性较差。

在以上所有的单例模式中,推荐静态内部类单例模式。主要是非常直观,即保证线程安全又保证唯一性。

众所周知,单例模式是创建型模式,都会新建一个实例。那么一个重要的问题就是反序列化,他会破坏单例模式。因此当实例被写入到文件到反序列化成实例时,我们需要重写readResolve方法,这样的话就会浅拷贝一份,作为返回值,并且无视掉反序列化的值,即使那个字节码已经被解析。

private Object readResolve() throws ObjectStreamException{
        return single;
}

简单工厂

在创建一个对象时不向客户暴露内部细节,并提供一个创建对象的通用接口。

定义

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

实现方式

public interface Product {
}
public class ConcreteProduct implements Product {
}
public class ConcreteProduct1 implements Product {
}

以下是简单工厂的实现,他被所需要实例化的类调用。

public class SimpleFactory {

    public Product createProduct(int type) {
        if (type == 1) {
            return new ConcreteProduct();
        } else {
            return new ConcreteProduct1();
        }
    }
}
public class Client {

    public static void main(String[] args) {
        SimpleFactory simpleFactory = new SimpleFactory();
        Product product = simpleFactory.createProduct(1);
        // do something with the product
    }
}

工厂方法

定义了一个创建对象的接口,但由子类决定要实例化哪个类。工厂方法把实例化操作推迟到子类。

定义

在简单工厂中,创建对象的是另一个类,而在工厂方法中,是由子类来创建对象。

实现方式

public abstract class Factory {
    abstract public Product factoryMethod();
    public void doSomething() {
        Product product = factoryMethod();
        // do something with the product
    }
}
public class ConcreteFactory extends Factory {
    public Product factoryMethod() {
        return new ConcreteProduct();
    }
}
public class ConcreteFactory1 extends Factory {
    public Product factoryMethod() {
        return new ConcreteProduct1();
    }
}

原型模型

使用原型实例指定要创建对象的类型,通过复制这个原型来创建新对象。

实现方式

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);
    }
}
public class Client {
    public static void main(String[] args) {
        Prototype prototype = new ConcretePrototype("abc");
        Prototype clone = prototype.myClone();
        System.out.println(clone.toString());
    }
}

此文章参考CS-Note