【面试系列】6种单例模式(Singleton)实现方法比较
转载文章,文章经 LiteCodes 授权,转载至本博客。
下述代码均省略了 Singleton 类的业务代码段,仅表现作为单例所需的代码部分。
懒汉式
普通懒汉式
1 public class Singleton { 2 3 /** 单例对象 */ 4 private static Singleton instance; 5 6 /** 7 * 私有构造方法. 8 */ 9 private Singleton() { 10 } 11 12 /** 13 * 静态方法, 用于获取单利对象. 14 * 如果单例对象未创建, 则创建新单例对象, 否则直接返回该对象. 15 * 16 * @return 单例对象. 17 */ 18 public static Singleton getInstance() { 19 if (instance == null) { 20 instance = new Singleton(); 21 } 22 return instance; 23 } 24 }
最简单的懒汉式单例,在首次调用 getInstance(); 时,会对单例对象进行实例化。
然而,这种方式明显无法在多线程模式下正常工作。当线程并发调用getInstance(); 时,由于线程之间没有进行同步,有可能两个线程同时进入 if 条件,导致实例化两次。
线程安全的懒汉式
1 public class Singleton { 2 3 /** 单例对象 */ 4 private static Singleton instance; 5 6 /** 7 * 私有构造方法. 8 */ 9 private Singleton() { 10 } 11 12 /** 13 * 静态方法, 用于获取单利对象. 14 * 如果单例对象未创建, 则创建新单例对象, 否则直接返回该对象. 15 * 16 * @return 单例对象. 17 */ 18 public static synchronized Singleton getInstance() { 19 if (instance == null) { 20 instance = new Singleton(); 21 } 22 return instance; 23 } 24 }
最简单的线程安全的懒汉模式,通过在 getInstance() 方法上添加 synchronized 关键字,保证同一时间仅有一个线程能够执行该代码段,以保证不会出现上面一种方法产生的问题。
然而,这种方法效率很低。每次调用 getInstance() 方法,都将为代码段加锁,同一时间该代码段只能被一个线程访问。然而除了首次调用外,都是不需要同步的,因为 instance 已经被实例化。
Double-Check
1 public class Singleton { 2 3 /** 单例对象 */ 4 private static volatile Singleton instance; 5 6 /** 7 * 私有构造方法. 8 */ 9 private Singleton() { 10 } 11 12 /** 13 * 静态方法, 用于获取单利对象. 14 * 如果单例对象未创建, 则创建新单例对象, 否则直接返回该对象. 15 * 16 * @return 单例对象. 17 */ 18 public static Singleton getInstance() { 19 if (instance == null) { 20 synchronized (Singleton.class) { 21 if (instance == null) { 22 instance = new Singleton(); 23 } 24 } 25 } 26 return instance; 27 } 28 }
Double-check 即双重校验,该方法是针对上述方法提出的一种改进方案。
在 getInstance() 方法中,通过不加锁判断 instance 是否实例化。如果没有实例化,再进行加锁、实例化过程,以减少在实例化后调用 getInstance() 方法导致的性能损耗。
需注意的是,在代码第4行, instance 的定义处,添加了 volatile 关键字。
为了保证 instance 在多个线程间同步,需要通过 volatile 关键字,指明该变量的值每次需要从主存中直接获取,避免从线程内存中获取,以保证线程间同步。
饿汉式
1 public class Singleton { 2 3 /** 单例对象, 类装载时进行实例化. */ 4 private static final Singleton singleton = new Singleton(); 5 6 /** 7 * 私有构造方法. 8 */ 9 private Singleton() { 10 } 11 12 /** 13 * 静态方法, 用于获取单利对象. 14 * 15 * @return 单例对象. 16 */ 17 public static Singleton getInstance() { 18 return singleton; 19 } 20 }
饿汉式单例的原理是 ClassLoader 装载类是单线程,通过这种机制避免了线程同步问题。
这种方式虽然避免了线程同步问题,但却有可能带来性能问题。
无论该类是否被使用, ClassLoader 都有可能(也有可能被 ClassLoader 忽略)加载该类并实例化该单例对象。所以在基础类库场景下,这种方法会无故消耗更多的资源。
静态内部类方式
1 public class Singleton { 2 3 /** 4 * 私有构造方法. 5 */ 6 private Singleton() { 7 } 8 9 /** 10 * 静态方法, 用于获取单利对象. 11 * 12 * @return 单例对象. 13 */ 14 public static Singleton getInstance() { 15 return SingletonHolder.instance; 16 } 17 18 private static class SingletonHolder { 19 20 /** 单例对象, 类装载时进行实例化. */ 21 private static final Singleton instance = new Singleton(); 22 } 23 }
这种方法同样利用了 ClassLoader 单线程装载的方式,避免了线程同步问题。然而他和上面一种方法不同的地方在于, instance 对象只有在 SingletonHolder 类被装载的时候才会被实例化。也就是说,只有当 getInstance() 方法调用时,才会被实例化,这样就避免了上述的资源损耗。
枚举方式
1 public enum Singleton { 2 INSTANCE; 3 }
在《Effective Java》一书中,Joshua Bloch 推荐使用这种方式实现单例模式。这种方式不仅能够避免线程同步问题,而且由于其语法级的约束,JVM 级的支持,保证了其极强的正确性。
如何选择
俗话说,No silver bullet,每一种实现都有其适用的场景。那么,我们如何选择单例的实现方式呢?答案是:取决于你所期望的内容。
如果你的单例类应用频繁,从系统启动后就需要使用,那么,饿汉式可能是一个不错的选择。类加载过程便已经完成了实例化的单例,在之后的调用过程中,无需再进行实例化,也无需害怕因为线程同步导致的性能损耗。
如果你的单例类占用较多资源,并且调用频率较低,那么或许 Double-Check 的懒汉式是一个不错的选择。在单例使用前,并不会被实例化,其所需要的资源也并不会被占用。
如果你的单例类属于某一个类库,或许 Double-Check 的懒汉式是一个不错的选择。一个功能丰富的类库中,并非所有的类都会被使用。然而 ClassLoader 的加载机制,并不一定会将其排除至外。所以,一个懒汉式的单例有可能降低类库使用者的资源损耗。
……
根据你的应用场景,选择一个合适的单例模式吧~