单例模式Singleton
概念
单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。选择单例模式就是为了避免不一致状态。使用Singleton的好处还在于可以节省内存,因为它限制了实例的个数,有利于Java垃圾回收(garbage collection)
单例对象的类必须保证只有一个实例存在,可以作为对意图实现单例模式的代码进行检验的标准。
对单例的实现可以分为两大类——懒汉式和饿汉式,他们的区别在于:
懒汉式:指全局的单例实例在第一次被使用时构建。
饿汉式:指全局的单例实例在类装载时构建。
核心思想
- 使用private修改该类构造器,从而将其隐藏起来,避免程序自由创建该类实例
- 提供一个public方法获取该类实例,且此方法必须使用static修饰(调用之前还不存在对象,因此只能用类调用)
- 该类必须缓存已经创建的对象,否则该类无法知道是否曾经创建过实例,也就无法保证只创建一个实例。为此,该类需要一个静态属性来保持曾经创建的实例。
例子
1.饿汉:全局的单例实例在类装载时构建的实现方式
饿汉式是典型的空间换时间,当类装载的时候就会创建类的实例,不管你用不用,先创建出来,然后每次调用的时候,就不需要再判断,节省了运行时间。
由于类装载的过程是由类加载器(ClassLoader)来执行的,这个过程也是由JVM来保证同步的,所以这种方式先天就有一个优势——能够避免许多由多线程引起的问题。不过,instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到lazy loading的效果。
public class HungryBase { private static HungryBase instance = new HungryBase(); /** * 私有默认构造方法 */ private HungryBase() { } /** * 静态工厂方法 */ public static HungryBase getInstance() { return instance; } }
JDK例子(java.lang.Runtime):
public class Runtime { private static Runtime currentRuntime = new Runtime(); public static Runtime getRuntime() { return currentRuntime; } private Runtime() {} }
2.饿汉---静态代码块
public class HungryStaticBlock { private static HungryStaticBlock instance = null; static { instance = new HungryStaticBlock(); } /** * 私有默认构造方法 */ private HungryStaticBlock() { } /** * 静态工厂方法 */ public static HungryStaticBlock getInstance() { return instance; } }
3.饿汉---静态类
这种方式同样利用了classloder的机制来保证初始化instance时只有一个线程,它跟第1种和第2种方式不同的是(很细微的差别):第1种和第2种方式是只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy loading效果),而这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。想象一下,如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。这个时候,这种方式相比第1和第2种方式就显得很合理。
public class HungryStaticClass { private static class SingletonHolder { private static HungryStaticClass instance = new HungryStaticClass(); } /** * 私有默认构造方法 */ private HungryStaticClass() { } /** * 静态工厂方法 */ public static HungryStaticClass getInstance() { return SingletonHolder.instance; } }
4.懒汉:这种写法lazy loading很明显,但是致命的是在多线程不能正常工作。
懒汉式是典型的时间换空间,就是每次获取实例都会进行判断,看是否需要创建实例,浪费判断的时间。当然,如果一直没有人使用的话,那就不会创建实例,则节约内存空间
/** * 懒汉 线程不安全 lazy loading很明显,在多线程不能正常工作 */ public class LazyBase { //静态属性用来缓存创建实例 private static LazyBase instance = null; //私有构造方法避免程序自由创建实例 private LazyBase() { } //静态公共方法用于取得该类实例 public static LazyBase getInstance() { if (instance == null) { instance = new LazyBase(); } return instance; } }
JDK例子(java.awt.Desktop.getDesktop()):
public class Desktop { public static synchronized Desktop getDesktop(){ if (GraphicsEnvironment.isHeadless()) throw new HeadlessException(); if (!Desktop.isDesktopSupported()) { throw new UnsupportedOperationException("Desktop API is not " + "supported on the current platform"); } sun.awt.AppContext context = sun.awt.AppContext.getAppContext(); Desktop desktop = (Desktop)context.get(Desktop.class); if (desktop == null) { desktop = new Desktop(); context.put(Desktop.class, desktop); } return desktop; } }
5.懒汉---线程安全:这种写法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading,但是,遗憾的是,效率很低,99%情况下不需要同步。
如果有两个线程(T1、T2)同时执行到这个方法时,会有其中一个线程T1获得同步锁,得以继续执行,而另一个线程T2则需要等待,当第T1执行完毕getInstance之后(完成了null判断、对象创建、获得返回值之后),T2线程才会执行执行。——线程安全
但是给gitInstance方法加锁,避免了可能会出现的多个实例问题,但是会强制除T1之外的所有线程等待,实际上会对程序的执行效率造成负面影响。---效率低
/** * 懒汉 多线程安全 效率低(99%情况下不需要同步) */ public class LazySyn { //静态属性用来缓存创建实例 private static LazySyn instance = null; //私有构造方法避免程序自由创建实例 private LazySyn() { } //静态公共方法用于取得该类实例 public static synchronized LazySyn getInstance() { if (instance == null) { instance = new LazySyn(); } return instance; } }
6.1懒汉---双重检查
可以使用“双重检查加锁”的方式来实现,就可以既实现线程安全,又能够使性能不受很大的影响。
“双重检查加锁”指的是:并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法后,先检查实例是否存在,如果不存在才进行下面的同步块,这是第一重检查,进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。
/** * 懒汉 多线程安全 双重检查 */ public class LazyDoubleCheck { //静态属性用来缓存创建实例 private static LazyDoubleCheck instance = null; //私有构造方法避免程序自由创建实例 private LazyDoubleCheck() { } public static LazyDoubleCheck getInstance() { if (instance == null) { synchronized (LazyDoubleCheck.class) { if (instance == null) { instance = new LazyDoubleCheck(); } } } return instance; } }
第一个if (instance == null),是为了解决效率问题,只有instance为null的时候,才进入synchronized的代码段——大大减少了几率。
第二个if (instance == null),是为了防止可能出现多个实例的情况。
6.2懒汉---终极版本(volatile)
知识点:什么是原子操作?
简单来说,原子操作(atomic)就是不可分割的操作,在计算机中,就是指不会因为线程调度被打断的操作。
比如,简单的赋值是一个原子操作:
m = 6; // 这是个原子操作
假如m原先的值为0,那么对于这个操作,要么执行成功m变成了6,要么是没执行m还是0,而不会出现诸如m=3这种中间态——即使是在并发的线程中。
而,声明并赋值就不是一个原子操作:
int n = 6; // 这不是一个原子操作
对于这个语句,至少有两个操作:
①声明一个变量n
②给n赋值为6
——这样就会有一个中间状态:变量n已经被声明了但是还没有被赋值的状态。
——这样,在多线程中,由于线程执行顺序的不确定性,如果两个线程都使用m,就可能会导致不稳定的结果出现。
知识点:什么是指令重排?
简单来说,就是计算机为了提高执行效率,会做的一些优化,在不影响最终结果的情况下,可能会对一些语句的执行顺序进行调整。
比如,这一段代码:
int a ; // 语句1
a = 8 ; // 语句2
int b = 9 ; // 语句3
int c = a + b ; // 语句4
正常来说,对于顺序结构,执行的顺序是自上到下,也即1234。
但是,由于指令重排的原因,因为不影响最终的结果,所以,实际执行的顺序可能会变成3124或者1324。
由于语句3和4没有原子性的问题,语句3和语句4也可能会拆分成原子操作,再重排。
——也就是说,对于非原子性的操作,在不影响最终结果的情况下,其拆分成的原子操作可能会被重新排列执行顺序。
OK,了解了原子操作和指令重排的概念之后,我们再继续看6.1代码的问题。
主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。
1. 给 instance 分配内存
2. 调用 LazyDoubleCheck 的构造函数来初始化成员变量,形成实例
3. 将instance对象指向分配的内存空间(执行完这步 instance才是非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
再稍微解释一下,就是说,由于有一个『instance已经不为null但是仍没有完成初始化』的中间状态,而这个时候,如果有其他线程刚好运行到第一层if (instance == null)这里,这里读取到的instance已经不为null了,所以就直接把这个中间状态的instance拿去用了,就会产生问题。
这里的关键在于——线程T1对instance的写操作没有完成,线程T2就执行了读操作(当然这种概率已经非常小了,但毕竟还是有的嘛,这时候就要用到volatile)。
volatile:volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量;是禁止指令重排,把instance声明为volatile之后,对它的写操作就会有一个内存屏障,这样,在它的赋值完成之前,就不用会调用读操作。
public class LazyVolatile { //volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。 private volatile static LazyVolatile instance = null; //私有构造方法避免程序自由创建实例 private LazyVolatile() { } public static LazyVolatile getInstance() { if (instance == null) {////先检查实例是否存在,不存在,在进行同步 synchronized (LazyVolatile.class) { //同步块,线程安全的创建实例 if (instance == null) {//再次检查实例是否存在,如果不存在才真正的创建实例 instance = new LazyVolatile(); } } } return instance; } }
注意:volatile阻止的不singleton = new Singleton()这句话内部[1-2-3]的指令重排,而是保证了在一个写操作([1-2-3])完成之前,不会调用读操作(if (instance == null)),大名鼎鼎的EventBus中,其入口方法EventBus.getDefault()就是用这种方法来实现的。
7.枚举(jdk1.5+):不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。
/** * jdk1.5 enum特性 */ public enum EnumSingleton { INSTANCE; public void whateverMethod() { } }
大自然的搬运工(感谢以下作者):
hxxp://blog.csdn.net/zhshulin/article/details/38225733
hxxp://blog.csdn.net/picway/article/details/70163455
hxxp://blog.csdn.net/qq_27550755/article/details/49781683