单例模式确保一个类只有一个实例,并提供一个全局访问点。

饿汉式

变量在申明时即被初始化。

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

    private Singleton() { }
} 

优点:线程安全;直观

缺点:增加类初始化时间,类即使不使用,也会被实例化

懒汉式

先声明一个空变量,需要用时才初始化

方式一:双检锁方式实现的线程安全的单例模式,别漏掉volatile

public class Singleton {
    private static volatile Singleton instance = null;

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

    private Singleton() { }
}

方式二:静态内部类方式保证懒汉式单例的线程安全

public class Singleton {
    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }

    private static class SingletonHolder {
        public static Singleton instance = new Singleton();
    }

    private Singleton() { }
}

为什么这种方式能保证线程安全?来分析两个问题:

  • 静态内部类方式是怎么实现懒加载的
  • 静态内部类方式是怎么保证线程安全的

类在初始化的时候,会立即加载内部类,内部类会在使用时才加载。所以当此 Singleton 类加载时,SingletonHolder 并不会被立即加载,所以不会像饿汉式那样占用内存。另外,Java 虚拟机规定,当访问一个类的静态字段时,如果该类尚未初始化,则立即初始化此类。当调用Singleton 的 getInstance 方法时,由于其使用了 SingletonHolder 的静态变量instance,所以这时才会去初始化 SingletonHolder,在 SingletonHolder 中 new 出 Singleton 对象。这就实现了懒加载。

其次,虚拟机在加载类的 clinit 方法时,会保证 clinit 在多线程中被正确的加锁、同步,即使有多个线程同时去初始化一个类,一次也只有一个线程可以执行 clinit 方法,其他线程都需要阻塞等待,从而保证了线程安全。

如何权衡两种方式?一般的建议是:对于构建不复杂,加载完成后会立即使用的单例对象,推荐使用饿汉式。对于构建过程耗时较长,并不是所有使用此类都会用到的单例对象,推荐使用懒汉式。