单例模式的几种实现方式

单例模式的几种实现方式

一、懒汉式单例模式

懒汉式单例模式,即是在需要用到该对象的时候才去进行初始化,代码如下:

public class Singleton {
    private static final Singleton instance;

    private Singleton () {}

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

        return instance;
    }
}

此种单例模式在单线程情况下可以很正常的运行,但是在多线程的情况下就可能导致同时创建多个 instance 实例:

TimeThread AThread B
1调用 getInstance,此时 instance 为 null
2调用 getInstance,此时 instance 为 null
3创建 Singleton 对象 instanceA 并返回
4创建 Singleton 对象 instanceB 并返回

此时 instance 对象被实例化了两次,不符合单例模式的初衷。

线程安全的懒汉式单例模式

为了确保线程安全,我们可以对 getInstance 方法加锁:

public class Singleton {
    private static final Singleton instance;

    private Singleton () {}

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

        return instance;
    }
}

这样做确实可以保证在多线程情况下的线程安全,但是这样子每次调用 getInstance 方法去获取 instance 对象的时候都会进行加锁操作,会造成比较大的开销,而实际上只有在对 instance 对象进行初始化的时候才需要加锁。

二、双重检测锁实现单例模式

//错误的双重检测锁
public class Singleton {
    private static final Singleton instance;

    private Singleton () {}

    public static Singleton getInstance() {
        if (instance == null) {  //第一次空检测
            synchronized (Singleton.class) {
                if (instance == null) {  //第二次空检测
                    instance = new Singleton();  //实例化对象
                }
            }
        }

        return instance;
    }
}

我们将加锁的位置后移,在检查变量是否已经初始化的时候不加锁,在变量未被初始化,需要去初始化的时候才加锁;这样子后续调用 getInstance 方法或取 instance 对象时不需要再加锁,减少了性能消耗。

  • 为什么要对 instance 进行两次空检测:可能存在多个线程同时通过了第一次空检测的情况,此时其中一个线程获得锁并初始化对象成功,那么第二次空检测可以避免其它通过第一次检测的线程重复初始化对象。

但是正如代码中注释的,这种加锁方式是错误的,为什么呢?

在代码中实例化对象的那一行实际上由三个步骤组成:

  1. 分配内存空间
  2. 初始化对象
  3. 将对象指向刚分配的内存空间

在一些编译器中为了提高运行速度可能会进行指令重排序,那么执行顺序会变成:

  1. 分配内存空间
  2. 将对象指向刚分配的内存空间
  3. 初始化对象

那么在多线程情况下可能会发生下面这种情况:

TimeThread AThread B
1调用 getInstance,检测到 instance 为 null
2获取锁
3检测到 instance 为 null
4为 instance 分配内存空间
5将 instance 指向刚分配的内存空间
6检测到 instance 不为 null,返回 instance
7访问 instance
8初始化 instance 对象

在这种情况下,线程 B 访问了一个初始化未完成的 instance 的对象,会发生错误,因此我们需要添加 volatile 关键字来避免指令重排序,因此正确的双重加锁代码如下:

//正确的双重检测锁
public class Singleton {
    private volatile static final Singleton instance;

    private Singleton () {}

    public static Singleton getInstance() {
        if (instance == null) {  //第一次空检测
            synchronized (Singleton.class) {
                if (instance == null) {  //第二次空检测
                    instance = new Singleton();  //实例化对象
                }
            }
        }

        return instance;
    }
}

三、饿汉式单例模式

饿汉式单例模式,即是在类加载的时候就创建并初始化单例对象,其代码如下:

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

    private Singleton () {}

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

四、静态内部类实现单例模式

使用静态内部类利用了 Java 静态内部类的特性:Java 加载外部类的时候,不会创建内部类的实例,只有在外部类使用到内部类的时候才会创建内部类实例。其代码如下:

public class Singleton {
    private Singleton () {}

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

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

五、枚举实现单例模式

使用枚举实现单例模式利用了枚举的特性:全局唯一。其代码如下:

public enum Singleton {
    INSTANCE;
}