Singleton单例模式

概述

单例模式可以说是大伙儿最熟悉的模式之一了。说简单是最简单的一种模式,但是深究复杂起来也可以是最复杂的模式。单例模式和其他的创建型模式不同,其他的创建型模式关心的问题是如何创建对象,获得所谓的产品;而单例模式却是关心对象创建的次数以及何时创建。

单例模式的优点使用等等GOF都简单的说明了,这里就不说了。

目的

希望对象只创建一个实例,并且提供一个全局的访问点。

分析

我们试想几个场景,慢慢来深入单例模式。

场景一:假设有一艘海盗船,船员会有很多个,而船长只有一名。

我们下意识的就会给出下面的代码:

   1: /**
   2:  * 单例模式
   3:  * @author  zhusw
   4:  * @version  [版本号, Jul 11, 2012]
   5:  * @see  [相关类/方法]
   6:  * @since  [产品/模块版本]
   7:  */
   8: public class SingletonCaptain
   9: {
  10:     private static SingletonCaptain instance = new SingletonCaptain();
  11:     
  12:     private SingletonCaptain()
  13:     {
  14:     }
  15:     
  16:     public static SingletonCaptain getInstance()
  17:     {
  18:         return instance;
  19:     }
  20: }

默认的构造方法是public的,所以我们需要重新定义构造方法为private,这样才能保证其他海盗势力不会暗中扶持某个船员做傀儡船长。这个场景中jvm在加载类的时候就会完成对象的实例化,创建好唯一的“船长”。

场景二:起初团伙规模较小,船长事必躬亲,慢慢的规模变大了,事情太多,船长就有要求了:我需要休息,不能一直在这守着,你们碰见打不过的再来找我。

这要求我们在用到类的时候才去创建它,而不是加载类的时候就进行实例化。这里牵扯到懒加载(lazyload)的概念,船长很忙,不希望屁大点事也来找他。

经过考虑,我们给出下面代码:

   1: /*
   2:  * 文 件 名:  SingletonCaptain.java
   3:  * 版    权:  Linkage Technology Co., Ltd. Copyright 2010-2011,  All rights reserved
   4:  * 描    述:  <描述>
   5:  * 版    本: <版本号> 
   6:  * 创 建 人:  zhusw
   7:  * 创建时间:  Jul 11, 2012
   8: 
   9:  */
  10: package com.lanshu.pattens.singleton;
  11:  
  12: /**
  13:  * 单例模式(懒加载)
  14:  * @author  zhusw
  15:  * @version  [版本号, Jul 11, 2012]
  16:  * @see  [相关类/方法]
  17:  * @since  [产品/模块版本]
  18:  */
  19: public class SingletonCaptain
  20: {
  21:     private static SingletonCaptain instance = null;
  22:     
  23:     private SingletonCaptain()
  24:     {
  25:     }
  26:     
  27:     public static SingletonCaptain getInstance()
  28:     {
  29:         if(instance == null)                            //lineA
  30:         {
  31:             instance = new SingletonCaptain();          //lineB
  32:         }
  33:         return instance;
  34:     }
  35: }

这下好了,只在真正需要该类的实例的时候才实例化。通常在创建和运行时负担比较重的时候选用此种方案。

场景三:船长有天不小心吃多撑死了。恰好A小队长和B小队长都来找船长。A队长首先发现船长死了,B队长随后也发现船长死了。A队长宣布船长死前任命自己为新的船长,B队长也干了同样的事情。OK,出现了2个船长,这肯定要乱套。

上面场景描述的其实就是懒加载单例模式在多个线程并发调用可能会出现的问题。

假设有2个线程A和B并发调用getInstance方法并按以下顺序执行:

  1. 线程A调用 getInstance() 方法并决定 instance 在 //lineA 处为 null
  2. 线程A进入 if代码块,但在执行 //lineB 处的代码行时被线程B 预占。
  3. 线程B 调用 getInstance() 方法并在 //lineA 处决定 instancenull
  4. 线程B 进入 if 代码块并创建一个新的 SingletonCaptain对象并在 //lineB处将变量 instance分配给这个新对象。
  5. 线程B 处返回 SingletonCaptain对象引用。
  6. 线程B 被线程A预占。
  7. 线程A在它停止的地方启动,并执行 //lineB 代码行,这导致创建另一个 SingletonCaptain对象。
  8. 线程A在返回这个对象。

结果是 getInstance() 方法创建了两个 SingletonCaptain对象,而它本该只创建一个对象。这个问题可以加上synchronized关键字同步解决,如下:

   1: /**
   2:  * 单例模式(Synchronized)
   3:  * @author  zhusw
   4:  * @version  [版本号, Jul 11, 2012]
   5:  * @see  [相关类/方法]
   6:  * @since  [产品/模块版本]
   7:  */
   8: public class SingletonCaptainSynchronized
   9: {
  10:     private static SingletonCaptainSynchronized instance = null;
  11:     
  12:     private SingletonCaptainSynchronized()
  13:     {
  14:     }
  15:     
  16:     public synchronized static SingletonCaptainSynchronized getInstance()
  17:     {
  18:         if(instance == null)                            //lineA
  19:         {
  20:             instance = new SingletonCaptainSynchronized();          //lineB
  21:         }
  22:         return instance;
  23:     }
  24: }

如果创建实例的过程很简单,那么这样做是简单而有效的,并且是线程安全的。但是这样做并不高效,因为只有在第一次调用方法时才需要同步。在除了第一次new一个实例,其余都是直接返回instance对象,返回对象这个操作消耗是很少的,大部分消耗都体现在synchronized同步操作上,这是个很大的性能问题,在早期,同步代价相当高。随着更高级的 JVM 的出现,同步的代价降低了,但出于 synchronized 方法或块仍然有性能损失。我所说的性能问题并不是说耗费了多长的时间,而是在一个操作过程中,某一步是无用操作却占了性能消耗很大的比例,这明显不划算。

于是,就出现了下面的写法,双重锁定检查(DCL double checked locking):

   1: /*
   2:  * 单例模式-双重锁定检查(DCL)
   3:  * 创 建 人:  zhusw
   4:  * 创建时间:  Jul 6, 2012
   5: 
   6:  */
   7: package com.lanshu.pattens.singleton;
   8:  
   9: public class SingletonCaptainDCL
  10: {
  11:     private static SingletonCaptainDCL instance = null;  
  12:    
  13:     public static SingletonCaptainDCL getInstance() {  
  14:         if (instance == null) {                             
  15:             synchronized (SingletonCaptainDCL.class) {          //lineA
  16:                 if (instance == null) {                         //lineB
  17:                     instance = new SingletonCaptainDCL();       //lineC
  18:                 }  
  19:             }  
  20:         }  
  21:         return instance;  
  22:     }  
  23: }

双重检查锁定背后的理论是:在 //lineA处的第二次检查使创建两个不同的 SingletonCaptainDCL对象成为不可能。假设有下列事件序列:

  1. 线程A 进入 getInstance()方法。
  2. 由于 instancenull,线程A在 //lineA处进入 synchronized块。
  3. 线程A被线程B预占。
  4. 线程B进入 getInstance()方法。
  5. 由于 instance 仍旧为 null,线程B试图获取 //lineA处的锁。然而,由于线程A持有该锁,线程B在 //lineA处阻塞。
  6. 线程B被线程A预占。
  7. 线程A执行,由于在 //lineB处实例仍旧为 null,线程A还创建一个 SingletonCaptainDCL对象并将其引用赋值给 instance
  8. 线程A退出 synchronized 块并从 getInstance()方法返回实例。
  9. 线程A 被线程B 预占。
  10. 线程B获取 //lineA 处的锁并检查 instance 是否为 null
  11. 由于 instance 是非 null 的,并没有创建第二个 SingletonCaptainDCL对象,由线程A创建的对象被返回。

稍微的深究一下

双重检查锁定看似是完美的,但是在实际运行中会有问题。这个和java内存模型JMM有关系。

先来简单介绍一下关于java内存模型的一点东西,属于现学现卖,很多东西写完一遍的效果比看完几遍要好的多。

简单来说,Java 语言的内存模型由一些规则组成,这些规则确定线程对内存的访问如何排序以及何时可以确保它们对线程是可见的。

JMM有个很重要的特性叫做:重排序

内存模型描述了程序的可能行为。具体的编译器实现可以产生任意它喜欢的代码 -- 只要所有执行这些代码产生的结果,能够和内存模型

预测的结果保持一致。这为编译器实现者提供了很大的自由,包括操作的重排序。

编译器生成指令的次序,可以不同于源代码所暗示的“显然”版本。重排序后的指令,对于优化执行以及成熟的全局寄存器分配算法的使

用,都是大有脾益的,它使得程序在计算性能上有了很大的提升。

重排序类型包括:

  • 编译器生成指令的次序,可以不同于源代码所暗示的“显然”版本。
  • 处理器可以乱序或者并行的执行指令。
  • 缓存会改变写入提交到主内存的变量的次序。

也就是说同样的代码针对不同的编译期可能会有不同的编译结果。

上面DCL单例模式getInstance代码编译后的字节码:

public static SingletonCaptainDCL getInstance();
Code:
0: getstatic #2; //Field instance:LSingletonCaptainDCL;
3: ifnonnull 38
6: ldc_w #3; //class SingletonCaptainDCL
9: dup
10: astore_0
11: monitorenter
12: getstatic #2; //Field instance:LSingletonCaptainDCL;
15: ifnonnull 28
18: new #3; //class SingletonCaptainDCL
21: dup
22: invokespecial #4; //Method "<init>":()V
25: putstatic #2; //Field instance:LSingletonCaptainDCL;
28: aload_0
   29:    monitorexit
30: goto 38
33: astore_1
34: aload_0
35: monitorexit
36: aload_1
37: athrow
38: getstatic #2; //Field instance:LSingletonCaptainDCL;
41: areturn
Exception table:
from to target type
12 30 33 any
33 36 33 any

红色标出的18-28行就是上面lineC编译后的字节码,在22行执行了实例的初始化方法,在25行将instance对象指向分配的内存空间,这时instance就不为null了。按照这样的顺序当然没有问题,但是22和25行的顺序由于JMM的乱序写入导致无法保证。也就是说存在下面一种情况:

  1. 线程A 进入 getInstance()方法。
  2. 由于 instancenull,线程A 在 //lineA 处进入 synchronized块。
  3. 线程A前进到 //lineC 处,但在构造函数执行之前,使实例成为非 null
  4. 线程A被线程B 预占。
  5. 线程B 检查实例是否为 null。因为实例不为 null,线程B将 instance引用返回给一个构造完整但部分初始化了的 SingletonCaptainDCL对象。
  6. 线程B被线程A预占。
  7. 线程A通过运行SingletonCaptainDCL对象的构造函数并将引用返回给它,来完成对该对象的初始化。

在jdk1.5以后的版本,我们可以使用volatile关键字来保证双重锁定模式是可以使用的,但是我们保证运行程序的是什么版本的jdk,如果是1.4或者之前的jdk,大部分的jvm并没有很好的支持volatile关键字。

所以结论引用别人的一句话:

底线就是:无论以何种形式,都不应使用双重检查锁定,因为您不能保证它在任何 JVM 实现上都能顺利运行。

那么我们讨论下解决的办法:

上面大部分的讨论都是源于我们不接受同步所带来的消耗,一种解决办法就是接受同步。

另外我们也可以使用JVM本身机制保证了线程安全问题,也就是本文一开始给出的饿汉式,缺点当然就是它是恶汉式的;

再来看看下面的写法:

/**
* 比较通用的写法
* @author zhusw
* @version [版本号, Jul 23, 2012]
* @see [相关类/方法]
* @since [产品/模块版本]
*/
public class SingletonCaptainPerfect
{
private static class SingletonHolder {
/**
* 单例对象实例
*/
static final SingletonCaptainPerfect INSTANCE = new SingletonCaptainPerfect();
}

public static SingletonCaptainPerfect getInstance() {
return SingletonHolder.INSTANCE;
}
}

 

上面的写法仍然使用JVM本身机制保证了线程安全问题;由于SingletonHolder是私有的,除了getInstance()之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖JDK版本。

单例模式也就讨论到这了,还有多classloder的情况,多jvm的情况就不深究了,也没达到那种高度。

本文参考了以下文章:

GOF的设计模式,java内存模型双重检查锁定及单例模式

posted @ 2012-07-23 09:33  朱样年华  阅读(1460)  评论(2编辑  收藏  举报