0x0 前言
单例模式是一个比较简单的模式,其目的在于确保某一个类只有一个实例,并且自行实例化并向整个系统提供这个实例。一般来说,对于一些创建、销毁比较昂贵的对象实例,也许使用单例模式是一个不错的选择。比如一个始终需要从键盘获取用户输入的系统,我们可以在类似 Utils 的静态类中设置一个全局唯一的Scanner类,始终用于获取用户的输入,从而避免每次创建删除同类对象产生的开销。
0x1 基本代码
很多设计模式相关的教程上都将单例模式分为饿汉单例和懒汉单例,它们的基本代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class Singleton { static private Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() { return instance; } }
class Singleton { static private Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
|
可以看到,为了限制客户端对该对象的多次实例化,两者的 constructor 均被设置为 private 可见性,并对外暴露静态方法 getInstance 用于返回内部的实例。区别在于,懒汉单例应用了 lazy loading 的思想,使得 instance 的实例化延迟到 getInstance 方法真正被调用时;而饿汉单例借助了 ClassLoader 的能力,让 instance 的实例化在 Singleton 类被加载时便进行了。
0x2 懒汉单例与线程安全
就像上面的注释所言,上述形式的懒汉模式并不是线程安全的,原因在于 instance == null
这句判断在并发的场景下是非常靠不住的,比如如下的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public class App { public static void main(String[] args) { for (int i=0; i<5; i++) { new Thread(() -> { Singleton.getInstance(); }).start(); } } }
class Singleton { static private Singleton instance = null;
private Singleton() { System.out.println("An instance has been created"); }
public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
|
在笔者的设备上共输出了5次 An instance has been created
,也就是产生了5个不同的对象,这并不符合单例模式。
针对上述问题,可以通过 synchronized 关键字对代码进行加锁,从而在保证线程安全的条件下实现懒汉单例。