饿汉式
-
在类加载的时候就创建实例
public class Singleton { // new 一个对象 private static Singleton singleton = new Singleton(); // 将构造函数私有化 private Singleton() {} //提供一个获取该类的实例的公有方法 public static Singleton getInstance() { return singleton; } }
-
缺点就是当我们不使用这个对象的时候,就显得有点浪费资源了,因为在加载类的时候,该类的实例就已经创建好了
懒汉式——单线程
-
用到的时候才创建实例
public class Singleton { // 将对象初始化为null private static Singleton singleton = null; // 将构造函数私有化 private Singleton() {} //提供一个获取该类的实例的公有方法 public static Singleton getInstance() { if (singleton == null) { singleton = new Singleton(); } return singleton; } }
-
这种方式在单线程的时候是没有问题的,但在多线程情况下有可能就会出现问题
-
假设有两个线程 A 和 B,当线程 A 执行到 if 语句时,判断条件为真,但紧接着 CPU 的执行权就被线程 B 抢去了,注意此时线程 A 还没有实例化对象
-
于是线程 B 执行到 if 语句时,singleton 仍为 null,此时线程 B 就对 singleton 进行实例化
-
然后,CPU 的执行权又被线程 A 抢走了,由于之前线程 A 已经判断 if 条件为真,所以它也会对 singleton 进行实例化
-
因此,最终线程 A 和线程 B 返回的就不是同一个实例了,也就达不到单例模式想要实现的效果
懒汉式——多线程之同步方法
-
针对上面提到的懒汉式在多线程环境下的安全问题,我们可以通过在方法前加
synchronized
修饰来解决,也即使用同步方法的方式public class Singleton { // 将对象初始化为null private static Singleton singleton = null; // 将构造函数私有化 private Singleton() {} //提供一个获取该类的实例的公有方法 public static synchronized Singleton getInstance() { if (singleton == null) { singleton = new Singleton(); } return singleton; } }
-
这种方式虽然可以解决多线程环境下的安全问题,但效率比较低,因为每次执行
getInstance()
方法时都要先获得锁对象,然后再去执行方法体,如果拿不到锁对象,就需要等待,这样耗时就会比较长,感觉像是变成了串行处理
懒汉式——多线程之同步代码块
-
为了提高效率,我们可以使用同步代码块的方式,减少锁的颗粒大小,代码如下:
public class Singleton { // 将对象初始化为null private static Singleton singleton = null; // 将构造函数私有化 private Singleton() {} //提供一个获取该类的实例的公有方法 public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { singleton = new Singleton(); } } return singleton; } }
-
之所以可以这样做,是因为在 if 语句中判断 singleton 是否等于 null 是读的操作,不可能存在线程安全问题。因此,我们只需要对创建实例的代码进行同步代码块的处理,也就是所谓的对可能出现线程安全的代码进行同步代码块的处理即可
-
但是这样做,虽然效率有所提高,但还是会有可能出现之前那样的线程安全的问题,即当线程 A 执行完 if 语句后,CPU 的执行权被线程 B 抢走了,线程 B 就会对 singleton 进行实例化
-
如果此时的 CPU 的执行权又回到了线程 A 的手中,那么由于之前线程 A 已经判断过 if 条件成立,那么它也会去实例化 singleton,这样就得到了两个不同的实例,不符合我们的预期
懒汉式——多线程之同步代码块结合双重检查加锁机制
-
为了解决这个问题,我们可以在执行 new 操作之前,再判断一次 singleton 是否为 null,此时如果再出现之前那样的情况,当线程 A 在同步代码块内再去判断 singleton 是否为 null 时,由于线程 B 已经将 singleton 这个共享资源实例化了,所以同步块内的 if 条件就不成立了,线程 A 就不会再次执行实例化操作了。代码如下:
public class Singleton { // 将对象初始化为null private static Singleton singleton = null; // 将构造函数私有化 private Singleton() {} //提供一个获取该类的实例的公有方法 public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
-
上面演示的就是双重检查加锁机制
懒汉式——多线程之同步代码块结合双重检查加锁机制并解决指令重排问题
-
但是,双重检查加锁机制并不能完全保证没有线程安全问题,因为这里会涉及到一个指令重排的问题
指令重排是指在程序执行过程中,为了性能考虑,编译器和 CPU 可能会对指令重新排序
-
也就是说,
singleton = new Singleton()
其实可以大致分为以下几个步骤:
- 申请一块内存空间
- 在这块空间里实例化对象
- singleton 的引用指向这块地址空间
-
对于以上步骤,由于指令重排序的存在,很有可能不是按上面的步骤依次执行的。比如,先执行1申请一块内存空间,然后执行3步骤,singleton 的引用去指向刚刚申请的内存空间地址,那么,当它再去执行2步骤,判断 singleton 时,由于 singleton 已经指向了某一地址,它就不会再为 null 了。因此,也就不会实例化对象了,这就是所谓的指令重排序安全问题。那么,如何解决这个问题呢?
-
我们可以通过加上 volatile 关键字,因为
volatile 可以禁止指令重排序
public class Singleton { // 将对象初始化为null private static volatile Singleton singleton = null; // 将构造函数私有化 private Singleton() {} //提供一个获取该类的实例的公有方法 public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }