设计模式初探-创建型模式之单例模式

上一篇文章从NIO原理到性能优化中,我 们引入了性能调优的相关话题,着重了谈了一下互联网架构的演变之路。这篇文章,我们将会开启一个老生常谈的新专题-设计模式。
提剑跨骑挥鬼雨,白骨如山鸟惊飞。尘世如潮人如水,只叹江湖几人回!
如果将代码比作江湖,那么语法就是一招一式,而设计模式则是神秘莫测的内功心法,将其融会贯通之日,便是立于不败之时。本文分为如下部分:

  1. 前言
  2. 设计模式分类
  3. 单例
    3.1 使用场景
    3.2 单例的5种实现
    3.3 饿汉单例
    3.3.1 破坏单例
    3.3.2 序例化和反序例化的实现机制
    3.3.3 通过readResolve 来改善单例
    3.4 懒汉单例
    3.4.1 破坏单例
    3.5 双重锁单例
    3.5.1 破坏单例
    3.6 内部类单例
    3.6.1 破坏单例
    3.7 枚举单例
    3.7.1 破坏单例
    3.8 几种单例的比对
    3.9 基于单例编写文件加载
  4. 总结

1. 前言

在说设计模式之前我们先思考几个问题。

  1. 什么是设计模式?
  2. 什么情况下要用到设计模式?
    那么带着这些问题,我们推开设计模式的大门

2. 设计模式分类

所谓设计模式指的是在某些特定情境下,针对某些特定问题的某种解决方案。它不是语法规定,而是一套解决方案,使用设计模式帮助我们提高代码的复用性、可维护性、健壮性以及安全性。同时更方便和清晰的通过代码传递我们coding时的设计思路,毕竟好的代码自己就是注释。设计模式按照意图可以分为以下几类:

意图分类 解释 模式
创建型 创建型模式涉及到对象的实例化,这类模式提供一个方法,将客户从对象的创建种解耦 工厂方法、抽象工厂、单例、建造者、原型
行为型 涉及到类和对象如何交互及分配职责 模板方法、命令、迭代器、观察者、策略、状态、职责链、中介者、访问者、备忘者、解释器
结构型 结构型模式可以让你把类或对象组合到更大的结构中去 适配器、装饰器、代理、组合、门面、桥接、享元

值得注意的是:

  1. 设计模式职期间并不是毫无关联的,常常一种模式里会包含另一种模式(如用工工厂方法实现抽象工厂)
  2. 设计模式并不是一尘不变的,常常为了实现需求而做不同的改变(比如spring的bean是单例,但却不是我们常用的单例模式,而是注册表单例)
  3. 不要滥用设计模式,采用最简单直观的方式解决问题才是最好的选择,不要过分设计

3. 单例

所谓单例即确保一个类只有一个实例并提供一个全局的访问点。

3.1 使用场景

从单例模式的定义中我们就可以看出单例的使用场景,即只需要一个对象的时候。那么什么时候只需要一个对象呢?即当对象的创建和销毁代价比较高的时候。如配置文件的加载,如spring bean的创建。

3.2 单例模式的5种实现方式

单例模式一般有如下实现方式

  • 饿汉
  • 懒汉
  • 双重锁
  • 内部类
  • 枚举

3.3 饿汉单例

所谓饿汉,即类初始化的时候就实例化一个不可变的静态对象。代码如下

public class Singleton implements Serializable {
    private static final Singleton INSTANCE=new Singleton();
    private Singleton(){

    }

    public static Singleton getInstance(){
        return INSTANCE;
    }
}

3.3.1 破坏单例

上面的单例看起来有点薄弱,那么它能实现我们要的单利效果吗?让我们来测一下,毕竟不想当测试的程序员不是一个好UI。

  • 单线程模式
    /**
     * 单线程调用
     */
    public void testWithOneThread() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Singleton.getInstance());
        }
    }
   public static void main(String[] args) {
        SingletonTest singletonTest=new SingletonTest();
        singletonTest.testWithOneThread();
    }

输出结果如下:

com.designPatterns.singleton.eager.Singleton@610455d6
com.designPatterns.singleton.eager.Singleton@610455d6
com.designPatterns.singleton.eager.Singleton@610455d6
com.designPatterns.singleton.eager.Singleton@610455d6
com.designPatterns.singleton.eager.Singleton@610455d6
com.designPatterns.singleton.eager.Singleton@610455d6
com.designPatterns.singleton.eager.Singleton@610455d6
com.designPatterns.singleton.eager.Singleton@610455d6
com.designPatterns.singleton.eager.Singleton@610455d6
com.designPatterns.singleton.eager.Singleton@610455d6

看起来运行的很完美,但是这样就完了吗?

设计模式初探-创建型模式之单例模式_第1张图片

我们尝试从以下几方面尝试来打破单例
1. 多线程
2. 反射
3. 序列化和反序列化
ps:由于我们的类未考虑要实现Cloneable接口,故clone 的单利破坏不在讨论范围之内

  • 多线程
 /**
     * 多线程调用
     */
    public void testWithMultiThread() {
        ExecutorService pool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            pool.submit(() -> {
                System.out.println(Singleton.getInstance());
            });
        }
    }
 public static void main(String[] args) {
        SingletonTest singletonTest=new SingletonTest();
        singletonTest.testWithMultiThread();
    }

运行结果如下:

com.designPatterns.singleton.eager.Singleton@68674054
com.designPatterns.singleton.eager.Singleton@68674054
com.designPatterns.singleton.eager.Singleton@68674054
com.designPatterns.singleton.eager.Singleton@68674054
com.designPatterns.singleton.eager.Singleton@68674054
com.designPatterns.singleton.eager.Singleton@68674054
com.designPatterns.singleton.eager.Singleton@68674054
com.designPatterns.singleton.eager.Singleton@68674054
com.designPatterns.singleton.eager.Singleton@68674054
com.designPatterns.singleton.eager.Singleton@68674054

我们可以看到饿汉模式实现的单利,在多线程情况下运行良好,static 对象只在初始化的时候被加载一次,这点是由JVM保证的

  • 反射
 /**
     * 反射调用
     * @throws Exception
     */
    public void testWithReflect() throws Exception{
        Constructor[] constructors = Singleton.class.getDeclaredConstructors();
        Constructor defaultConstructor = constructors[0];
        defaultConstructor.setAccessible(true);
        Singleton    singleton = (Singleton) defaultConstructor.newInstance(null);
        System.out.println(Singleton.getInstance());
        System.out.println(singleton);
    }

    public static void main(String[] args) throws Exception{
        SingletonTest singletonTest=new SingletonTest();
        singletonTest.testWithReflect();
    }
com.designPatterns.singleton.eager.Singleton@610455d6
com.designPatterns.singleton.eager.Singleton@511d50c0

我们可以看到,通过反射调用构造方法,可以得到两个不同的对象,饿汉单利在反射情况下不能良好运行

  • 序列化和反序列化
  /**
     * 序列化反序列化调用
     *
     * @throws Exception
     */
    public void testWithStream() throws Exception {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("\\singleton"));
             ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("\\singleton")));) {
            oos.writeObject(Singleton.getInstance());
            Singleton singleton = (Singleton) ois.readObject();
            System.out.println(Singleton.getInstance());
            System.out.println(singleton);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    public static void main(String[] args) throws Exception {
        SingletonTest singletonTest = new SingletonTest();
        singletonTest.testWithStream();
    }
com.designPatterns.singleton.eager.Singleton@266474c2
com.designPatterns.singleton.eager.Singleton@2f4d3709

我们看到通过序列化和反序列化,我们也成功的打破了饿汉单利

通过上面的实例,我们看到,饿汉模式在单线程及多线程环境下可以良好运行。在反射、序列化和反序列化的情况下无法良好运行。

设计模式初探-创建型模式之单例模式_第2张图片

3.3.2 序列化和反序列化的实现机制

当我们将一个对象在磁盘上存储,或者网上传输的时候,需要将其转换成byte[],这个过程称之为序列化,其相反的过程为反序列化。实现序列化的方式有两种,实现Serializable\Externalizable 接口,下面为其异同

类目 是否要public constructor 实例化的方式 自定义逻辑的方式
Serializable no 查找最近父类,调用父类的无参构造器来实例化,逐个属性赋值 定义私有readObject\writeObject\readResolve
Externalizable yes 调用当前类的无参public constructor 实例化,调用readExternal为属性赋 重写writeExternal/readExternal 方法

下面以Serializable为例来做个示范

public class Student implements Serializable{
    private String name;
    private int age;
    private transient String addr;

    private Student(){
        System.out.println("构造方法被调用了");
    }


    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getAddr() {
        return addr;
    }

    public void setAddr(String addr) {
        this.addr = addr;
    }


    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", addr='" + addr + '\'' +
                '}';
    }

    /**
     *  定制序列化
     * @param oos
     * @throws Exception
     */
    private void writeObject(ObjectOutputStream oos)throws Exception{
        System.out.println("writeObject 调用了");
        oos.defaultWriteObject();
        oos.writeObject((addr+"test"));
    }

    /**
     * 定制反序列化
     * @param ois
     * @throws Exception
     */
    private void readObject(ObjectInputStream ois)throws Exception{
        System.out.println("readObject 调用了");
        ois.defaultReadObject();
        addr=(String) ois.readObject();

    }

    /**
     * 替换readObject后的值
     * @return
     */
    private Object readResolve(){
        System.out.println("readResolve 调用了");
        return this;
    }
    private static Student initStudent(){
        Student student=new Student();
        student.setAddr("上海市");
        student.setAge(20);
        student.setName("zhangsan");
        System.out.println("init student:"+student);
        return student;
    }

    public static void main(String[] args) throws Exception{
        Student student=initStudent();
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("\\sitx"));
        //将对象转为byte[] 输出
        oos.writeObject(student);
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream(new File("\\sitx")));
        //从byte[] 中读取对象
        Student std= (Student)ois.readObject();
        System.out.println("readStudent:"+std);
    }
}

输出结果:

构造方法被调用了
init student:Student{name='zhangsan', age=20, addr='上海市'}
writeObject 调用了
readObject 调用了
readResolve 调用了
readStudent:Student{name='zhangsan', age=20, addr='上海市test'}

我们可以看到readObject 实例化的时候,并没有调用我们私有的构造器。同时readObject的调用顺序要先于readResolve。同时我们发现了一个有趣的现象,Student类中的三个方法readObject\writeObject\readResolve均为私有方法,是怎么被外部调用的呢?

疑问.jpg

原来ObjectOutputStream.writeObject和ObjectInputStream.readObject的过程中,会通过先按照默认的方式序列化/反序列化所有的非transient属性,然后再利用反射调用类中的私有writeObject/readObject/readReSolve方法
源码如下:

  • 在ObjectStreamClass实例化的时候,通过反射查找并初始化上面的几个私有方法
if (serializable) {
            AccessController.doPrivileged(new PrivilegedAction() {
                public Void run() {
                    if (isEnum) {
                        suid = Long.valueOf(0);
                        fields = NO_FIELDS;
                        return null;
                    }
                    if (cl.isArray()) {
                        fields = NO_FIELDS;
                        return null;
                    }

                    suid = getDeclaredSUID(cl);
                    try {
                        fields = getSerialFields(cl);
                        computeFieldOffsets();
                    } catch (InvalidClassException e) {
                        serializeEx = deserializeEx =
                            new ExceptionInfo(e.classname, e.getMessage());
                        fields = NO_FIELDS;
                    }

                    if (externalizable) {
                        cons = getExternalizableConstructor(cl);
                    } else {
                        cons = getSerializableConstructor(cl);
                        writeObjectMethod = getPrivateMethod(cl, "writeObject",
                            new Class[] { ObjectOutputStream.class },
                            Void.TYPE);
                        readObjectMethod = getPrivateMethod(cl, "readObject",
                            new Class[] { ObjectInputStream.class },
                            Void.TYPE);
                        readObjectNoDataMethod = getPrivateMethod(
                            cl, "readObjectNoData", null, Void.TYPE);
                        hasWriteObjectData = (writeObjectMethod != null);
                    }
                    domains = getProtectionDomains(cons, cl);
                    writeReplaceMethod = getInheritableMethod(
                        cl, "writeReplace", null, Object.class);
                    readResolveMethod = getInheritableMethod(
                        cl, "readResolve", null, Object.class);
                    return null;
                }
            });
        }
  • 在ObjectOutputStream 中 invokeMethod
 private void writeSerialData(Object obj, ObjectStreamClass desc)
        throws IOException
    {
        ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
        for (int i = 0; i < slots.length; i++) {
            ObjectStreamClass slotDesc = slots[i].desc;
            if (slotDesc.hasWriteObjectMethod()) {
                PutFieldImpl oldPut = curPut;
                curPut = null;
                SerialCallbackContext oldContext = curContext;

                if (extendedDebugInfo) {
                    debugInfoStack.push(
                        "custom writeObject data (class \"" +
                        slotDesc.getName() + "\")");
                }
                try {
                    curContext = new SerialCallbackContext(obj, slotDesc);
                    bout.setBlockDataMode(true);
                    slotDesc.invokeWriteObject(obj, this);
                    bout.setBlockDataMode(false);
                    bout.writeByte(TC_ENDBLOCKDATA);
                } finally {
                    curContext.setUsed();
                    curContext = oldContext;
                    if (extendedDebugInfo) {
                        debugInfoStack.pop();
                    }
                }

                curPut = oldPut;
            } else {
                defaultWriteFields(obj, slotDesc);
            }
        }
    }
void invokeWriteObject(Object obj, ObjectOutputStream out)
        throws IOException, UnsupportedOperationException
    {
        requireInitialized();
        if (writeObjectMethod != null) {
            try {
                writeObjectMethod.invoke(obj, new Object[]{ out });
            } catch (InvocationTargetException ex) {
                Throwable th = ex.getTargetException();
                if (th instanceof IOException) {
                    throw (IOException) th;
                } else {
                    throwMiscException(th);
                }
            } catch (IllegalAccessException ex) {
                // should not occur, as access checks have been suppressed
                throw new InternalError(ex);
            }
        } else {
            throw new UnsupportedOperationException();
        }
    }

3.3.3 通过readResolve 来改善单例

通过上面的介绍,大致可以了解readResolve的用法。下面我们用其来完善饿汉单例。

public class Singleton implements Serializable {
    private static final Singleton INSTANCE=new Singleton();
    private Singleton(){

    }

    public static Singleton getInstance(){
        return INSTANCE;
    }

    private Object readResolve(){
        return INSTANCE;
    }
}
  • 序列化和反序列化测试
   /**
     * 序列化反序列化调用
     *
     * @throws Exception
     */
    public void testWithStream() {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("\\singleton"));
             ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("\\singleton")));) {
            oos.writeObject(Singleton.getInstance());
            Singleton singleton = (Singleton) ois.readObject();
            System.out.println(Singleton.getInstance());
            System.out.println(singleton);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    public static void main(String[] args) throws Exception {
        SingletonTest singletonTest = new SingletonTest();
        singletonTest.testWithStream();
    }

结果如下:

com.designPatterns.singleton.eager.Singleton@b4c966a
com.designPatterns.singleton.eager.Singleton@b4c966a

成功的防止了在序列化和反序列化情况下单利破坏。


3.4 懒汉单例

懒汉单利,即用到的时候才实例化的单例。这里的懒指的是懒加载。

public class Singleton implements Serializable {
    private static Singleton INSTANCE = null;

    private Singleton() {

    }

    public static Singleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }

}

3.4.1 破坏单例

我们分别在单线程环境、多线程环境、反射、序列化和反序列化的情况下获取单例。
由于代码与3.3.1 中相同,故只贴出运行结果

  • 单线程
com.designPatterns.singleton.lazy.Singleton@610455d6
com.designPatterns.singleton.lazy.Singleton@610455d6
com.designPatterns.singleton.lazy.Singleton@610455d6
com.designPatterns.singleton.lazy.Singleton@610455d6
com.designPatterns.singleton.lazy.Singleton@610455d6
com.designPatterns.singleton.lazy.Singleton@610455d6
com.designPatterns.singleton.lazy.Singleton@610455d6
com.designPatterns.singleton.lazy.Singleton@610455d6
com.designPatterns.singleton.lazy.Singleton@610455d6
com.designPatterns.singleton.lazy.Singleton@610455d6
  • 多线程
com.designPatterns.singleton.lazy.Singleton@49fe0fd2
com.designPatterns.singleton.lazy.Singleton@49fe0fd2
com.designPatterns.singleton.lazy.Singleton@7c837e63
com.designPatterns.singleton.lazy.Singleton@49fe0fd2
com.designPatterns.singleton.lazy.Singleton@49fe0fd2
com.designPatterns.singleton.lazy.Singleton@358b5609
com.designPatterns.singleton.lazy.Singleton@49fe0fd2
com.designPatterns.singleton.lazy.Singleton@49fe0fd2
com.designPatterns.singleton.lazy.Singleton@49fe0fd2
com.designPatterns.singleton.lazy.Singleton@5bbb3bd2
  • 反射
com.designPatterns.singleton.lazy.Singleton@610455d6
com.designPatterns.singleton.lazy.Singleton@511d50c0
  • 序列化和反序列化
com.designPatterns.singleton.lazy.Singleton@b4c966a
com.designPatterns.singleton.lazy.Singleton@2f4d3709

可以看到,懒汉模式只有在单线程环境下能良好运行。在多线程、反射、序列化和反序列化环境下均无法保证单例。
当然我们同样可以通过定义readResolve方法来让其在序列化和反序列的情况下正常运行。由于与3.3.3 的代码完全一致,此处不做演示。
值得注意的是,懒汉模式实现了懒加载,这点是饿汉无法实现的


3.5 双重锁单利

既然饿汉模式实现了懒加载但是无法保证线程安全,那么有没有一种即能实现懒加载,又能保证线程安全的单利模式呢?双重锁单利(double-check-singleton)就是这种单利。
所谓双重锁单利,即通过对代码加锁(可以是语言层面的synchronzied,也可以是jdk层面的 Lock)和双重非空判断的方式来实现单利。通过这种方式实现的单利,兼具线程安全及懒加载及性能。以下为代码实现

   private static volatile Singleton INSTANCE = null;

    private Singleton() {

    }

    public static Singleton getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }

        }
        return INSTANCE;
    }

}

有两点值得注意的地方

  1. synchronized 加锁的位置是出于性能的考虑(不加在方法上是为了保证互斥的最小性)
  2. volatile关键字是为了禁止指令的重排序及保证可见性(防止多线程环境下,拿到已分配内存但未初始化的对象)
    上面的第二点主要是因为new Singleton() 这个步骤,是一个非原子性操作。通过javap -c Singleton 查看字节码:
public static com.designPatterns.singleton.doubleCheck.Singleton getInstance();
    Code:
       0: getstatic     #2                  // Field INSTANCE:Lcom/designPatterns/singleton/doubleCheck/Singleton;
       3: ifnonnull     37
       6: ldc           #3                  // class com/designPatterns/singleton/doubleCheck/Singleton
       8: dup
       9: astore_0
      10: monitorenter
      11: getstatic     #2                  // Field INSTANCE:Lcom/designPatterns/singleton/doubleCheck/Singleton;
      14: ifnonnull     27
      17: new           #3                  // class com/designPatterns/singleton/doubleCheck/Singleton
      20: dup
      21: invokespecial #4                  // Method "":()V
      24: putstatic     #2                  // Field INSTANCE:Lcom/designPatterns/singleton/doubleCheck/Singleton;
      27: aload_0
      28: monitorexit
      29: goto          37
      32: astore_1
      33: aload_0
      34: monitorexit
      35: aload_1
      36: athrow
      37: getstatic     #2                  // Field INSTANCE:Lcom/designPatterns/singleton/doubleCheck/Singleton;
      40: areturn

我们可以看到 instance=new Singleton 分为几步

  1. 开辟对象空间-new
  2. 调用构造函数初始化-invokespecial
  3. 为引用赋值-putstatic
    步骤2和3 可能会发生指令的重排序,导致多线程环境下拿到未初始化的实例。

3.5.1 破坏单例

我们分别在单线程环境、多线程环境、反射、序列化和反序列化的情况下获取单例。
由于代码与3.3.1 中相同,故只贴出运行结果

  • 单线程
com.designPatterns.singleton.doubleCheck.Singleton@610455d6
com.designPatterns.singleton.doubleCheck.Singleton@610455d6
com.designPatterns.singleton.doubleCheck.Singleton@610455d6
com.designPatterns.singleton.doubleCheck.Singleton@610455d6
com.designPatterns.singleton.doubleCheck.Singleton@610455d6
com.designPatterns.singleton.doubleCheck.Singleton@610455d6
com.designPatterns.singleton.doubleCheck.Singleton@610455d6
com.designPatterns.singleton.doubleCheck.Singleton@610455d6
com.designPatterns.singleton.doubleCheck.Singleton@610455d6
com.designPatterns.singleton.doubleCheck.Singleton@610455d6
  • 多线程
com.designPatterns.singleton.doubleCheck.Singleton@3480e44e
com.designPatterns.singleton.doubleCheck.Singleton@3480e44e
com.designPatterns.singleton.doubleCheck.Singleton@3480e44e
com.designPatterns.singleton.doubleCheck.Singleton@3480e44e
com.designPatterns.singleton.doubleCheck.Singleton@3480e44e
com.designPatterns.singleton.doubleCheck.Singleton@3480e44e
com.designPatterns.singleton.doubleCheck.Singleton@3480e44e
com.designPatterns.singleton.doubleCheck.Singleton@3480e44e
com.designPatterns.singleton.doubleCheck.Singleton@3480e44e
com.designPatterns.singleton.doubleCheck.Singleton@3480e44e
  • 反射
com.designPatterns.singleton.doubleCheck.Singleton@610455d6
com.designPatterns.singleton.doubleCheck.Singleton@511d50c0
  • 序列化和反序列化
com.designPatterns.singleton.doubleCheck.Singleton@b4c966a
com.designPatterns.singleton.doubleCheck.Singleton@2f4d3709

可以看到,双重锁单例在单线程及多线程环境下均能很好的工作,但是在反射及序列化反序列化的情况下均无法正常工作。

3.6 内部类单例

所谓内部类单例,即通过内部类的方式来提供懒加载的单例。

public class Singleton {
    private Singleton() {

    }

    private static class Holder {
        private static Singleton singleton = new Singleton();
    }

    public static Singleton getInstance() {
        return Holder.singleton;
    }

}

值得注意的是:

  1. 实现了懒加载的单例
  2. 不用加锁
  3. 外部类可以访问内部类的私有变量(TODO)

3.6.1 破坏单例

我们分别在单线程环境、多线程环境、反射、序列化和反序列化的情况下获取单例。
由于代码与3.3.1 中相同,故只贴出运行结果

  • 单线程
com.designPatterns.singleton.holder.Singleton@610455d6
com.designPatterns.singleton.holder.Singleton@610455d6
com.designPatterns.singleton.holder.Singleton@610455d6
com.designPatterns.singleton.holder.Singleton@610455d6
com.designPatterns.singleton.holder.Singleton@610455d6
com.designPatterns.singleton.holder.Singleton@610455d6
com.designPatterns.singleton.holder.Singleton@610455d6
com.designPatterns.singleton.holder.Singleton@610455d6
com.designPatterns.singleton.holder.Singleton@610455d6
com.designPatterns.singleton.holder.Singleton@610455d6
  • 多线程
com.designPatterns.singleton.holder.Singleton@3480e44e
com.designPatterns.singleton.holder.Singleton@3480e44e
com.designPatterns.singleton.holder.Singleton@3480e44e
com.designPatterns.singleton.holder.Singleton@3480e44e
com.designPatterns.singleton.holder.Singleton@3480e44e
com.designPatterns.singleton.holder.Singleton@3480e44e
com.designPatterns.singleton.holder.Singleton@3480e44e
com.designPatterns.singleton.holder.Singleton@3480e44e
com.designPatterns.singleton.holder.Singleton@3480e44e
com.designPatterns.singleton.holder.Singleton@3480e44e
  • 反射
com.designPatterns.singleton.holder.Singleton@610455d6
com.designPatterns.singleton.holder.Singleton@511d50c0
  • 序列化和反序列化
com.designPatterns.singleton.holder.Singleton@b4c966a
com.designPatterns.singleton.holder.Singleton@2f4d3709

我们可以看到内部类单例在单线程及多线程环境下均能很好的运行,但是在反射和序列化反序列化的情况下无法保证单例。

3.7 枚举单例

上面的几种单例模式,不能同时满足 懒加载、线程安全、序列化反序列、反射的情况下单例。那么就没有一种同时满足以上几种情况的单例吗?
枚举单例既能保证线程安全,又能保证序列化和反序列化单例,也能保证反射单例。但是无法懒加载。

public enum Singleton {
    INSTANCE;
    public static Singleton getInstance(){
        return INSTANCE;
    }
}
吃惊.jpg

是不是特别简单,值得注意的是:

  1. jvm 会保证enum 构造器的私有,故此处不用写私有构造方法

3.7.1 破坏单例

我们分别在单线程环境、多线程环境、反射、序列化和反序列化的情况下获取单例。
由于代码与3.3.1 中相同,故只贴出运行结果

  • 单线程
INSTANCE
INSTANCE
INSTANCE
INSTANCE
INSTANCE/
INSTANCE
INSTANCE
INSTANCE
INSTANCE
INSTANCE
  • 多线程
INSTANCE
INSTANCE
INSTANCE
INSTANCE
INSTANCE
INSTANCE
INSTANCE
INSTANCE
INSTANCE
INSTANCE
  • 反射
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
    at com.designPatterns.singleton.SingletonTest.testWithReflect(SingletonTest.java:48)
    at com.designPatterns.singleton.SingletonTest.main(SingletonTest.java:74)
  • 序列化和反序列化
INSTANCE
INSTANCE

可以看到,我们无法通过反射调用枚举类型的构造方法。枚举单利,在单线程、多线程、反射、序列化反序列化的情况下均能很好的工作。无需加锁,实现简单,是首推的实现单例的方式。

3.8 几种单例的比对

单例类型 能否懒加载 是否线程安全 是否加锁 能否通过反射获取新的实例 能否通过序列化和反序列化获取新的实例
饿汉模式 非懒加载 线程安全 不加锁
懒汉模式 懒加载 线程不安全 不加锁
双重锁单例 非懒加载 线程安全 加锁
内部类单例 懒加载 线程安全 不加锁
枚举单例 非懒加载 线程安全 不加锁

可以看到枚举单例是我们实现起来最简单,也是最高效的单例,不用去考虑私有构造器,不用去考虑定制反序列化readResolve方法

3.9 基于单例编写文件加载

假设有个需求需要加载资源文件,这种资源文件的加载是比较耗时的,我们肯定只希望加载一次,尝试用枚举单例解决这个问题。

public enum PropertiesConfig {
    //实例
    CONFIG;
    //配置文件存储集合
    private ConcurrentHashMap sources = new ConcurrentHashMap();

    PropertiesConfig() {
        Properties properties = new Properties();
        try {
            properties.load(PropertiesConfig.class.getResourceAsStream("application.properties"));
        } catch (IOException e) {
            e.printStackTrace();
        }
        properties.entrySet().forEach(p -> sources.put(p.getKey(), p.getValue()));
    }

    private static PropertiesConfig getInstance() {
        return CONFIG;
    }

    private Object getPropertiesByKey(String key) {
        return sources.get(key);
    }


    public static void main(String[] args) {
        ExecutorService pool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            pool.submit(() -> {
                System.out.println(PropertiesConfig.getInstance().getPropertiesByKey("url"));
            });
        }
    }
}

4.总结

本文开始,我们开启了新的话题-设计模式。我们先是总结了设计模式的分类,然后着重剖析了单例模式,分析了单例模式的实现。but 本文的重点不是单例模式的实现,而是单例模式的破坏,以及小tips-序列化和反序列自定义。希望通本文,大家都能对单例的种种有些许新的感悟。

由于技术水平所限,文章难免有不足之处,欢迎大家指出。希望每位读者都能有新的收获,我们下一篇文章-设计模式初探-创建型模式之工厂模式 见
.....

参考文章

  • head first 设计模式

你可能感兴趣的:(设计模式初探-创建型模式之单例模式)