秒懂设计模式——单例设计模式
(三)单例设计模式
1.先解释一下,什么是单例模式呢?
在Java中是这样定义的:“一个类有且仅有一个实例,并且自行实例化向整个系统提供。”
显然从单例模式的定义中,我们可以发现它有三个要点:
①某个类只能有一个实例;
②它必须自行创建这个实例;
③它必须自行向整个系统提供这个实例。
2.要满足这三个要点,应该如何实现呢?下面让我们来逐条分析:
①如何保证某个类只能有一个实例?
让我先来想一下,一个类的对象是如何创建的呢?答案是:一个类的对象的产生是由类构造函数来完成。那么,我们是不是可以通过私有类构造函数来实现呢?
②如何保证类定义中含有一个该类的静态私有对象?
这个就很简单了,让单例类自己创建自己的唯一实例。
③如何保证它自行向整个系统提供这个实例?
这个也不难,对外提供获取此对象的静态方法即可。
3.单例模式分为几种呢?
①懒汉式,线程不安全;
②懒汉式,线程安全;
③饿汉式;
④双检锁/双重校验锁(DCL,即 double-checked locking);
⑤登记式/静态内部类;
⑥枚举式;
下面仍然通过一个故事,逐一介绍这几种类型的单例模式,并且在最后,还会分析一下他们的性能差异。
【讲故事】某野鸡大学(类),只有一个学生会主席(唯一对象,自行实例化),还是一个漂亮妹子叫“M蓉”,其他附近野鸭大学的学长都喜欢请她去自己宿舍,连夜补习英语(向整个系统提供实例)。
(1)懒汉式,线程不安全
先解释一下为什么叫懒汉式:“懒”顾名思义就是延迟,懒汉式就是指,只有在获取此单例对象时,才会去创建此对象。
【Java代码】
①创建一个单例模式的类。
package com.liyan.lazy; /** * 懒汉式的M蓉(线程不安全) * <p>Title: LazyMRong</p> * @author Liyan * @date 2017年4月27日 下午2:00:44 */ public class LazyMRong { //1.私有空参构造,防止别人创建 private LazyMRong(){} //2.自己创建自己的唯一实例 private static LazyMRong lazyMRong ; //3.对外提供获取此对象的静态方法 public static LazyMRong getLazyMRong() { if (lazyMRong == null) { //懒汉式意味着,只有你在想获取时,才创建此对象 return new LazyMRong(); } return lazyMRong; } public void teachEnglish() { System.out.println("连夜补习外语!"); } }
②创建外部访问
package com.liyan.lazy; /** * S喆请M蓉补习外语 * <p>Title: SZhe</p> * @author Liyan * @date 2017年4月27日 下午2:15:23 */ public class SZhe { public static void main(String[] args) { //通过静态方法,获取到单例对象 LazyMRong lazyMRong = LazyMRong.getLazyMRong(); //调用补习外语方法 lazyMRong.teachEnglish(); } }分析:首先懒汉式肯定是延迟初始化的,但是不支持多线程。因为没有加锁synchronized,所以有些资料上认为,从严格意义上讲,它并不算单例模式。
(2)懒汉式,线程安全
这个跟上面的懒汉式,线程不安全,唯一的区别就是:在获取单例类的静态方法上,加上了synchronized关键字进行修饰;
package com.liyan.lazy; /** * 懒汉式的M蓉(线程安全) * <p>Title: LazyMRong</p> * @author Liyan * @date 2017年4月27日 下午2:00:44 */ public class LazyMRong { //1.私有空参构造,防止别人创建 private LazyMRong(){} //2.声明自己的唯一实例,先不实例化 private static LazyMRong lazyMRong ; //3.对外提供获取此对象的静态方法 public synchronized static LazyMRong getLazyMRong() { if (lazyMRong == null) { //懒汉式意味着,只有你在想获取时,才创建此对象 return new LazyMRong(); } return lazyMRong; } public void teachEnglish() { System.out.println("连夜补习外语!"); } }(3)饿汉式分析:毋庸置疑,它是延迟初始化的,能够在多线程中很好的工作,同时在第一次调用时,才进行初始化,避免了内存的浪费。但是加了synchronized 锁机制,所以效率很低。
先解释一下为什么叫饿汉式:“饿”意味着“着急”,饿汉式就是指,不管单例对象有没有外部访问,先实例化再说。
【Java代码】饿汉式的M蓉
package com.liyan.hungry; /** * 饿汉式的M蓉(线程安全) * <p>Title: HungryMRong</p> * @author Liyan * @date 2017年4月27日 下午3:02:18 */ public class HungryMRong { //1.私有空参构造,防止别人创建 private HungryMRong(){} //2.创建自己的唯一对象,并直接实例化(饿汉式,因为饿着急,不管三七二十一,直接实例化) private static HungryMRong hungryMRong = new HungryMRong(); //3.对外提供获取此对象的静态方法 public static HungryMRong getHungryMRong() { return hungryMRong; } }(4)双重检查锁(DCL,即 double-checked locking)分析:显然饿汉式,不懒,所以没有延迟初始化,同时它基于 classloder 机制,而没用加锁的方式,所以既避免了多线程的同步问题,又使得执行效率得到提高。但是,因为它在类加载时就初始化,所以会造成内存的浪费。
先解释一下为什么叫双重检查锁:顾名思义,会涉及到两把锁:
①进入方法过后,先检查实例是否存在,如果不存在才进入下面的同步块;
②进入同步块后,再次检查实例是否存在,如果不存在,就在同步的情况下创建该实例。
【Java代码】双重检查锁的M蓉
package com.liyan.dcl; /** * DCL双重检查锁的单例模式的M蓉 * <p>Title: DCLMrong</p> * @author Liyan * @date 2017年4月27日 下午4:54:30 */ public class DCLMrong { //1.私有空参构造,防止别人创建 private DCLMrong (){} //2.声明唯一的单例对象;注意这里用到了volatile关键字! private volatile static DCLMrong dclMrong ; //3.对外提供获取此对象的静态方法 public static CLMrong getDclMrong() { //第一把锁:先检查实例是否存在,如果不存在才进入下面的同步块; if (dclMrong == null) { synchronized (DCLMrong.class) { //第二把锁:再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例。 if (dclMrong == null) { return new DCLMrong(); } } } return dclMrong; } }分析:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。说明: 双重检查加锁机制的实现会使用一个关键字volatile,它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。
(5)登记式/静态内部类
先解释一下为什么叫登记式:见名知意,想要获取某单例对象,没登记的必须先登记,然后才能获取。此时我们还需要一个登记簿(Map集合),当然,这个登记簿可以记录一堆单例对象,而不仅仅是一个。
这个模式确实复杂了一些,而且很多网上的例子也是觉得有所不妥,我根据个人理解,写了如下的代码,如果各位看官觉得也有疑问,欢迎留言讨论。
【Java代码】
①创建登记式单例模式类
package com.liyan.register; import java.util.HashMap; import java.util.Map; /** * 登记式单例模式 * <p>Title: RegisterMrong</p> * @author Liyan * @date 2017年4月27日 下午6:38:22 */ public class RegSingleton { //1.私有空参构造,防止别人创建 private RegSingleton(){} //2.创建一个登记簿,在静态代码块中创建自己的唯一对象 private static Map<String, RegSingleton> map = new HashMap<String, RegSingleton>(0); //在登记簿上登记 private static void singletonHolder(String name) { if(name==null){ name="RegSingleton"; } RegSingleton singleton = new RegSingleton(); //利用反射获取类名,并作为map集合的key map.put(name, singleton); System.out.println("已将"+ name +"记录到登记簿!"); } //3.对外提供获取此对象的静态方法 public static RegSingleton getInstance(String name){ if(name==null){ name="RegSingleton"; System.out.println("名字为空,自动找RegSingleton!"); } //查询登记簿上是否登记过 if(map.get(name)==null){ System.out.println("名字"+name+"未在登记簿中登记!"); try { //如果没有登记过,则先进行登记 System.out.println("名字"+name+"开始在登记簿中登记!"); RegSingleton.singletonHolder(name); //登记之后再返回该对象 System.out.println("名字"+name+"已经登记完成!"); return map.get(name); } catch (Exception e) { e.printStackTrace(); } }else { //如果登记过,直接返回该单实例 System.out.println("名字"+name+"之前在登记簿中登记过了!"); return map.get(name); } return null; } public void teachEnglish() { System.out.println("连夜补习外语!"); } }②创建外部访问
package com.liyan.register; /** * S喆请M蓉补习外语 * <p>Title: SZhe</p> * @author Liyan * @date 2017年4月27日 下午2:15:23 */ public class SZhe { public static void main(String[] args) { RegSingleton mrong = RegSingleton.getInstance("Mrong"); mrong.teachEnglish(); } }③结果
名字Mrong未在登记簿中登记! 名字Mrong开始在登记簿中登记! 已将Mrong记录到登记簿! 名字Mrong已经登记完成! 连夜补习外语!①相同点:都利用ClassLoder机制,来保证初始化时只有一个线程。分析:主要是比较一下,登记模式和双重检验锁式有何异同?
②不同点:双重检验锁式在类一被装载是就被初始化了,所以它没有延迟的效果;而登记模式,只有再主动调用获取该对象的静态方法时,才被初始化,所以它有延迟效果。
(6)枚举式
先解释一下为什么叫枚举式:不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,但是《Effective Java》一书中的话有这样一段很经典的话:“单元素的枚举类型已经成为实现Singleton的最佳方法!”
【Java代码】枚举单例模式的M蓉
package com.liyan.enummodel; /** * 枚举单例模式的M蓉 * <p>Title: EnumMrong</p> * @author Liyan * @date 2017年4月27日 下午9:23:59 */ public class EnumMrong { //1.私有空参构造,防止别人创建 private EnumMrong() {} //2.申明自己的唯一对象 public static EnumMrong getInstance() { return Singleton.INSTANCE.getInstance(); } //3.对外提供获取此对象的静态方法 private static enum Singleton { INSTANCE; private EnumMrong singleton; //在构造方法中实例化对象,保证只调用一次 private Singleton() { singleton = new EnumMrong(); } public EnumMrong getInstance() { return singleton; } } }