已经介绍和学习了两个建立型模式了,今天来学习一下另外一个很是常见的建立型模式,单例模式。html
单例模式也被称为单件模式(或单体模式),主要做用是控制某个类型的实例数量是一个,并且只有一个。缓存
实现单例模式的方式有不少种,大致上能够划分为以下两种。安全
在使用某些全局对象时,作一些“try-Use”的工做。就是若是要使用的这个全局对象不存在,就本身建立一个,把它放到全局的位置上;若是原本就有,则直接拿来使用。分布式
类型本身控制正常实例的数量,不管客户程序是否尝试过了,类型本身本身控制只提供一个实例,客户程序使用的都是这个现成的惟一实例。学习
目前随着集群、多核技术的广泛应用,想经过简单的类型内部控制失效真正的Singleton愈来愈难,试图经过经典单例模式实现分布式环境下的“单例”并不现实。因此目前介绍的这个单例是有语义限制的。测试
虽然单例模式也属于建立型模式,淡水它是有本身独特的特色的。spa
还有须要注意的一点,单例模式只关心类实例的建立问题,并不关心具体的业务功能。线程
目前Java里面实现的单例是一个ClassLoader及其子ClassLoader的范围。由于ClassLoader在装载饿汉式实现的单例类时,会响应地建立一个类的实例。这也说明,若是一个虚拟机里有多个ClassLoader(虽说ClassLoader遵循双亲委派模型,可是也会有父加载器处理不了,而后自定义的加载器执行类加载的状况。),并且这些ClassLoader都装载着某一个类的话,就算这个类是单例,它也会产生不少个实例。若是一个机器上有多个虚拟机,那么每一个虚拟机里面都应该至少有一个这个类的实例,也就是说整个机器上就有不少个实例,更不会是单例了。code
还有一点再次强调,目前讨论的单例范围不适用于集群环境。htm
饿汉式单例是指在类被加载的时候,惟一实例已经被建立。
以下代码的例子:
/** * 饿汉式单例模式 * */ public class HungrySingleton { /** * 定义一个静态变量用来存储实例,在类加载的时候建立,只会建立一次。 */ private static HungrySingleton hungrySingleton = new HungrySingleton(); /** * 私有化构造方法,禁止外部建立实例。 */ private HungrySingleton(){ System.out.println("建立实例"); } /** * 外部获取惟一实例的方法 * @return */ public static HungrySingleton getInstance(){ return hungrySingleton; } }
懒汉式单例是指在类加载的时候不建立单例的对象,只有在第一次使用的时候建立,而且在第一次建立后,之后再也不建立该类的实例。
以下代码的例子:
/** * 懒汉式单例 */ public class LazySingleton { /** * 定义一个静态变量用来存储实例。 */ private static LazySingleton lazySingleton = null; /** * 私有化构造方法,禁止外部建立实例。 */ private LazySingleton(){} /** * 外部获取惟一实例的方法
* 当发现没有初始化的时候,才初始化静态变量。 * @return */ public static LazySingleton getInstance(){ if(null==lazySingleton){ lazySingleton = new LazySingleton(); } return lazySingleton; } }
登记式单例实际上维护的是一组单例类的实例,将这些实例存在在一个登记薄(例如Map)中,使用已经登记过的实例,直接从登记簿上返回,没有登记的,则先登记,后返回。
以下代码例子:
/** * 登记式单例 */ public class RegisterSingleton { /** * 建立一个登记簿,用来存放全部单例对象 */ private static Map<String,RegisterSingleton> registerBook = new HashMap<>(); /** * 私有化构造方法,禁止外部建立实例 */ private RegisterSingleton(){} /** * 注册实例 * @param name 登记簿上的名字 * @param registerSingleton 登记簿上的实例 */ public static void registerInstance(String name,RegisterSingleton registerSingleton){ if(!registerBook.containsKey(name)){ registerBook.put(name,registerSingleton); } } /** * 获取实例,若是在未注册时调用将返回null * @param name 登记簿上的名字 * @return */ public static RegisterSingleton getInstance(String name){ return registerBook.get(name); } }
因为饿汉式的单例在类加载的时候就建立了一个实例,因此这个实例一直都不会变,所以也是线程安全的。可是懒汉式单例就不是线程安全的了,在懒汉式单例中有可能会出现两个线程建立了两个不一样的实例,由于懒汉式单例中的getInstance()方法不是线程安全的。因此若是想让懒汉式变成线程安全的,须要在getInstance()方法中加锁。
以下所示:
/** * 外部获取惟一实例的方法 * 当发现没有被初始化的时候,才初始化静态变量 * @return */ public static synchronized LazySingleton getInstance(){ if(null==lazySingleton){ lazySingleton = new LazySingleton(); } return lazySingleton; }
可是这样增长的资源消耗,延迟加载的效果虽然达到了,可是在使用的时候资源消耗确更大了,因此不建议这样用。既要实现线程安全,又要保证延迟加载。基于这样的问题就出现了另外一种方式的单例模式,静态内部类式单例。
静态内部类式单例饿汉式和懒汉式的结合。
以下代码例子:
/** * 内部静态类式单例 */ public class StaticClassSingleton { /** * 私有化构造方法,禁止外部建立实例。 */ private StaticClassSingleton(){ System.out.println("建立实例了"); } /** * 私有静态内部类,只能经过内部调用。 */ private static class SingleClass{ private static StaticClassSingleton singleton = new StaticClassSingleton(); } /** * 外部获取惟一实例的方法 * @return */ public static StaticClassSingleton getInstance(){ return SingleClass.singleton; } }
上面静态内部类的方式经过结合饿汉式和懒汉式来实现了即延迟加载了又线程安全了。下面也来介绍另外一种即实现了延迟加载有保证了线程安全的方式的单例。
以下代码例子:
/** * 双重检查加锁式单例 */ public class DoubleCheckLockSingleton { /** * 静态变量,用来存放实例。 */ private volatile static DoubleCheckLockSingleton doubleCheckLockSingleton = null; /** * 私有化构造方法,禁止外部建立实例。 */ private DoubleCheckLockSingleton(){} /** * 双重检查加锁的方式保证线程安全又能得到到惟一实例 * @return */ public static DoubleCheckLockSingleton getInstance(){ //先检查实例是否已经存在,不存在则进入代码块 if(null == doubleCheckLockSingleton){ synchronized (DoubleCheckLockSingleton.class){ //因为synchronized也是重入锁,即一个线程有可能屡次进入到此同步块中若是第一次进入时已经建立了实例,那么第二次进入时就不建立了。 if(null==doubleCheckLockSingleton){ doubleCheckLockSingleton = new DoubleCheckLockSingleton(); } } } return doubleCheckLockSingleton; } }
如上所示,所谓“双重检查加锁”机制,并非每次进入getInstance()方法都须要加锁,而是当进入方法后,先检查实例是否已经存在,若是不存在才进行下面的同步块,这是第一重检查,进入同步块后,再次检查实例是否已经存在,若是不存在,就在同步块中建立一个实例,这是第二重检查。这个过程是只须要同步一次的。
还须要注意的一点是,在使用“双重检查加锁”时,须要在变量上使用关键字volatile,这个关键字的做用是,被volatile修饰的变量的值不会被本地线程缓存,全部对该变量的读写都是直接操做共享内存,从而确保多个线程能正确地处理该变量。可能不了解Java内存模式的朋友不太好理解这句话的意思,能够去看看(JVM学习记录-Java内存模型(一),JVM学习记录-Java内存模型(二))了解一下Java内存模型,我简单说明一下,volatile这个关键字能够保证每一个线程操做的变量都会被其余线程所看到,就是说若是第一个线程已经建立了实例,可是把建立的这个实例只放在了本身的这个线程中,其余线程是看不到的,这个时候若是其余线程再去判断实例是否已经存在了实例的时候,发现没有仍是没有实例就会又建立了一个实例,而后也放在了本身的线程中,若是这样的话咱们写的单例模式就没意义了。在JDK1.5之前的版本中对volatile的支持存在问题,可能会致使“双重检查加锁”失败,因此若是要使用“双重检查加锁”式单例,只能使用JDK1.5以上的版本。
在JDK1.5中引入了一个新的特性,枚举,经过枚举来实现单例,在目前看来是最佳的方法了。Java的枚举类型实质上是功能齐全的类,所以能够有本身的属性和方法。
仍是经过代码示例来解释吧。
以下代码例子:
/** * 单元素枚举实现单例模式 */ public enum EnumSingleton { /** * 必须是单元素,由于一个元素就是一个实例。 */ INSTANCE; /** * 测试方法1 * @return */ public void doSomeThing() { System.out.println("#####测试方法######"); } /** * 测试方法2 * @return */ public String getSomeThing(){ return "得到到了一些内容"; } }
上面例子中EnumSingleton.INSTANCE就能够得到到想要的实例了,调用单例的方法能够种EnumSingleotn.INSTANCE.doSomeThing()等方法。
下面来看看枚举是如何保证单例的:
首先枚举的构造方法明确是私有的,在使用枚举实例时会执行构造方法,同时每一个枚举实例都是static final类型的,代表枚举实例只能被赋值一次,这样在类初始化的时候就会把实例建立出来,这也说明了枚举单例,实际上是饿汉式单例方式。这样就用最简单的代码既保证了线程安全,又保证了代码的简洁。
还有一点很值得注意的是,枚举实现的单例保证了序列化后的单例安全。除了枚举式的单例,其余方式的单例,均可能会经过反射或反序列化来建立多个实例。
因此在使用单例的时候最好的办法就是用枚举的方式。既简洁又安全。