Java 设计模式-单例模式

281 阅读5分钟
  1. 需求分析:
    在某些系统中, 我们希望某个类只能创建一个唯一的对象. 例如: 在一个员工管理系统中, CEO 有且仅能有一位. 这个时候我们就需要系统来保证只能创建这个类的唯一对象, 创建方式有好几种:
  • 饿汉模式(Eager Initialization)

    /**
     * Eager Initialization-线程安全
     */
    public class CEO {
        public static CEO getInstance() {
            return INSTANCE;
        }
        private static final CEO INSTANCE = new CEO();
        private CEO() {}
    }
    

    这种方式在类加载的时候就新建对象实例, 这个时候可能客户端并没有使用它, 这种方式称为预先初始化. 由于是在类加载的时候新建对象实例, 因此是线程安全的, 即多个线程调用 CEO#getInstance() 得到对象实例一定是同一个.

  • 饱汉模式(Lazy Initialization)

    /**
     * Lazy Initialization-线程不安全
     */
    public class CEO {
        public static CEO getInstance() {
            if (INSTANCE == null) {
                INSTANCE = new CEO();
            }
            return INSTANCE;
        }
        private static CEO INSTANCE;
        private CEO() {}
    }
    

    这种方法可以在客户端需要使用对象实例的才对实例进行初始化. 但是这种写法是线程不安全的, 即多个线程获取的对象实例可能不是同一个, 这就违背了只能创建一个对象的原则.

    下面分析出现线程不安全的原因: 假设现在有两个线程 ThreadA, ThreadB, 如下图所示. ThreadA 顺序执行到判断处, 这时候 INSTANCE 为 null, 条件为真, 继续往下执行. 这时候系统线程调度, 切换到 ThreadB 执行, ThreadB 顺序执行到判断处, 这时候 INSTANCE 为 null, 条件为真, 继续往下执行. 系统再次发生线程调度, 切换回 ThreadA 继续执行, 执行 INSTANCE = new CEO(), 产生一个 CEO 对象, 然后返回该对象, 这时候 ThreadA 执行完毕. 继续执行 ThreadB, 执行 INSTANCE = new CEO(), 产生一个 CEO 对象, 然后返回该对象. 由于两次都是通过 new 产生的新对象, 所以这两个对象不是同一个对象. 下面将提供改进的写法.

    synchronized method

  • 饱汉模式(Lazy Initialization)-方法加锁

    /**
     * Lazy Initialization-方法加锁
     */
    public class CEO {
        public static synchronized CEO getInstance() {
            if (INSTANCE == null) {
                INSTANCE = new CEO();
            }
            return INSTANCE;
        }
        private static CEO INSTANCE;
        private CEO() {}
    }
    

    CEO#getInstance()方法加上 synchronized 关键字就可以保证线程安全了. 但是这样有个问题, 每次调用方法都需对方法加锁, 如果有很多线程同时调用的话, 性能比较低. 其实只需要在第一次创建对象的时候才需要加锁来避免创建多个对象, 一旦对象创建之后, 就不需要同步了, 直接返回对象即可.

  • 饱汉模式(Lazy Initialization)-双重检查(double checked)

    /**
     * Lazy Initialization-双重加锁
     */
    public class CEO {
        public static synchronized CEO getInstance() {
            if (INSTANCE == null) {
                synchronized(CEO.class) {
                    if (INSTANCE == null) {
                        INSTANCE = new CEO();
                    }
                }
            }
            return INSTANCE;
        }
        private volatile static CEO INSTANCE;
        private CEO() {}
    }
    

    这里需要判断两次 INSTANCE 是否为 null. 原因如下: 假设有两个线程 ThreadA 和 ThreadB, 如下图所示. ThreadA 执行完 INSTANCE == nulll 后切换到 ThreadB, ThreadB 执行完 INSTANCE == nulll 又切换回线程 ThreadA. ThreadA 顺序往下执行, 直到执行完 INSTANCE = new CEO(), 然后返回结果. 此时对象已经不为空了, 切换到 ThreadB 执行. ThreadB 顺序往下执行, 若此时不进行第二次判断 INSTANCE == nulll, ThreadB 就会执行 INSTANCE = new CEO(), 这样又会产生一个新的对象. 若进行了第二次判断, 就可以直接返回对象了.

    至于为什么要加 volatile 关键字是因为: INSTANCE = new CEO() 这一句赋值操作并不是一步就能完成的, 在底层可能分为很多步执行, Java 编译器可能对这几步指令的顺序进行重新排列. 一般来说应该是先为对象分配空间并做初始化, 然后再让对象引用不为空. 可实际情况可能是先让对象引用不为空, 然后再分配空间并做初始化. 假设在执行完让对象引用不为空这一步骤之后, 切换到 ThreadC 执行, ThreadC 一来就判断对象不为空, 直接返回了对象, 可实际上对象并没有创建完毕, 这时候调用对象的方法或者访问对象的实例变量就会出错. 加了 volatile 关键字后, 就能让新建对象的时候, 先为对象分配空间并做初始化, 然后再让对象引用不为空. 这样访问对象才不会出错.

    double checked

  • 内部类写法(也叫 Bill Pugh 写法)

    /**
     * 内部类写法
     */
    public class CEO {
        public static synchronized CEO getInstance() {
            return CEOHolder.INSTANCE;
        }
        private static class CEOHolder {
            private static final CEO INSTANCE = new CEO();
        }
        private CEO() {}
    }
    

    加载 CEO 类的并不会导致内部类 CEOHolder 的加载, 当第一次调用CEO#getInstance()方法时, 会导致 CEOHolder 类的加载, 然后新建对象实例, 由于是在类加载的时候新建对象实例, 因此是线程安全的.

  • 枚举写法

    /**
     * 枚举写法
     */
    public class CEO {
        INSTANCE;
        CEO() {}
    }
    

    Effective Java 推荐的写法, 从本质上来讲也是一种 Eager Initilization.

  1. 单例模式定义:
    Ensure a class has only one instance, and provide a global point of access to it.(确保某一个类 只有一个实例, 而且自行实例化并向整个系统提供这个实例。 )

    singleton

  2. 单例模式的应用:
    java.lang.Runtime#getRuntime()
    在 Spring 中每个 Bean 默认就是单例的

  3. 单例模式的扩展:
    单例模式可以扩展为产生固定数量对象的模式就叫做有上限的多例模式

  4. 参考:
    [1] : Java 库中的设计模式
    [2] : 设计模式之禅
    [3] : Head First 设计模式
    [4] : Java 库中使用的设计模式
    [5] : 单例模式的写法
    [6] : 单例模式的七种写法
    [7] : 单例模式双重检查原因
    [8] : Java volatile 关键字解析
    [9] : Java 单例模式使用 volatile 关键字原因