单例模式

意图

  • 单例模式用来保证一个类只有一个实例, 这个实例不是外界手动 new 出来的, 而是对外提供一个方法来访问到它.

  • 单例模式封装了初始化方法, 保证这个实例只被初始化一次.

要解决什么问题

有时候程序需要一个唯一的对象, 它在整个程序的声明周期中只存在一个实例. 比如:

  • Logger. 很多情况下. 我们希望所有对象都往一个文件中写 log. 在创建 Logger 的时候, 假如我们每次都用:

    Logger logger = new Logger(“dir/log.file”);
    

那么我们有可能会出现日志文件会被重新清空(打开文件时候选择, 清空打开), 甚至使得日志文件内容错乱(假如多个线程同时调用 log 方法).

  • Factory. 很多工厂只用初始化一次就够了.
  • Service. 有的 Service 可能初始化比较复杂, 但是功能知识相当于一个 Delegate, 这种情况下, Service 做成可以做成单例.

讨论

基本思路

通常情况下, 想要获得一个类的一个实例, 无非就是 new 出来. 只要有对构造函数的访问权限, 我们想要多少个实例就能 new 多少个实例. 那么, 我们怎么才能做到让一个类只能有一个实例呢?

答案就是: 控制访问权限.

因为, 不管 public 也好 protected 也好, package 也好, 只要有一定的可见性, 创建的权利就完全交给外界了. 只有设置为 private, 限制了构造方法可见性, 也就控制了外界创建对象的权利.

可是, 将构造方法设置为 private, 外界的确无法创建了, 那么这个类还有什么用? 实例都创建不了呀!

其实, 虽然外界创建不了这个实例, 但是类本身还是可以访问自己的构造函数从而创建实例出来. 我们用一个 field 保存一个实例, 然后我们对外提供一个 public 的方法, 以便外界通过这个方法访问这个实例. 现在, 我们来想象一下这个类的样子:

public class A {
    private A instance = new A();
    private A() {
    }
    public A getInstance() {
        return instance;
    }
}

这里有个问题, 这个 getInstance() 方法该怎么调用? 这个方法必须要有实例才能调用, 可是我们这个方法就是用来创建实例的. 该怎么办? 有没有一种方法能不让我们创建实例就可以调用类的方法的?

静态方法! 只需要这个类被加载且被初始化就能通过 类名.静态方法名() 就可以调用到. 好! 现在我们来修改一下啊这个类:

public class A {
    // 因为静态方法只能访问静态变量, 所以要把 instance 设为静态以供 getInstance() 访问
    private static A instance = new A();
    private A() {
    }
    public static A getInstance() {
        return instance;
    }
}

延时加载

现在, 让我们捋一捋这个实例是如何创建的. 我们有一个 A 类型的field. 这个filed会在类完成装载,链接后在初始化阶段(静态初始化器 static initializer)被赋值. 之后, 不管有没有调用类的 getInstance() 方法,都会存在于应用程序中. 这里又有问题了, 不管用没用到 getInstance() 方法, 这个 field 所代表的实例一定会被创建! 那初始化过程中虚拟机任务是多么繁重? 我们能不能改成用到的时候才初始化? 答案是可以的, 我们可以将这个实例的创建延时成第一次调用 getInstance() 时:

public class A {
    // 1. 因为静态方法只能访问静态变量, 所以要把 instance 设为静态以供 getInstance() 访问
    // 2. 设为 null
    private static A instance = null
    private A() {
    }
    public static A getInstance() {
        // 第一次调用 getInstance() 时, instance 一定为 null, 这时候要进行实例的创建
        if (instance == null) {
            instance = new A();
        }
        // instance 除了第一次之外, 都不为null, 可以直接返回.
        return instance;
    }
}

多线程问题

之前, 我们使用

private static A instance = new A();

这种方式的时候, 我们是不需要考虑多线程情况的, 因为 VM 会处理的很好. 但是现在放在了 getInstance() 里, 就需要我们自己控制多线程并发问题了. 试想一下, 假如有多个线程同时调用 getInstance() 方法会怎样? 当代码执行到

    if (instance == null) {
        instance = new A();
    }

时候, 第一个线程判断 instancenull, 然后打算执行 instance = new A(), 但是没有真的执行, instance 仍为 null; 然而在这时, 时间片用光了, 假设轮到第二个线程执行, 同样判断 instancenull, 执行了 instance = new A(), 这时第二个时间片用完了, 有可能切回第一个线程, 这时, 第一个线程继续执行 instance = new A(). 问题就来了, getInstance() 为这两个线程返回了两个不同的实例! 这不是我们想要的!

好在对于线程安全问题,我们早已经有很成熟的解决方法, 就是关键字 synchronized. 我们再修改一下代码吧:

public class A {
    // 1. 因为静态方法只能访问静态变量, 所以要把 instance 设为静态以供 getInstance() 访问
    // 2. 设为 null
    private static A instance = null
    private A() {
    }
    // 加上synchronized 关键字解决线程安全问题
    public synchronized static A getInstance() {
        // 第一次调用 getInstance() 时, instance 一定为 null, 这时候要进行实例的创建
        if (instance == null) {
            instance = new A();
        }
        // instance 除了第一次之外, 都不为null, 可以直接返回.
        return instance;
}
}

这样的单例模式已经满足大部分需求了. 但是假如有多个线程且调用 getInstance() 特别频繁, 那么加在方法上的 synchronized 关键字可能会降低程序的执行效率. 所以线程安全的代码还可以再优化一下:

public class A {
    // 1. 因为静态方法只能访问静态变量, 所以要把 instance 设为静态以供 getInstance() 访问
    // 2. 设为 null
    // 3. 使用 volatile 关键字, 保证多个线程能正确处理对 instance 的实例化
    private volatile* static A instance = null
    private A() {
    }
    public static A getInstance() {
        // 第一次调用 getInstance() 时, instance 一定为 null, 这时候要进行实例的创建
        if (instance == null) {
            synchronized (A.class) {
                // 还得判断问题, 防止不在同一个时间片中执行
                if (instance == null) { 
                    instance = new A();
                }        
            }
        }
        // instance 除了第一次之外, 都不为null, 可以直接返回.
        return instance;
    }
}

这样的单例模式的实现, 即照顾到了多线程, 又照顾到了效率.

要点

  1. 有一个 private static 修饰的属性
  2. 构造函数要被定义为 private
  3. 有一个 public static 修饰的方法的访问方法
  4. 访问方法中要延时加载
  5. 多线程安全问题要用 synchronized
  6. 外界适用时要通过访问方法来后去类的实例

声音

Q: 通过不同的参数来初始化单例模式是个好主意么? 比如:

`public static A getInstance(Config config)`

A: 不是, 根据单例的定义, 一个对象只能被初始化一次, 如果getInstance() 能被传入参数,那么外界可以用不同参数来获取不同的实例了. 这就不再是单例了. 用工厂模式可能会是个更好的选择.

详情请见: http://stackoverflow.com/questions/1050991/singleton-with-arguments-in-java

Q: 单例真的好么? A: 篇幅太长, 意见也不统一, 但是有一些观点还是值得我们借鉴.

http://stackoverflow.com/questions/137975/what-is-so-bad-about-singletons