单例模式完全剖析_第1页
单例模式完全剖析_第2页
单例模式完全剖析_第3页
单例模式完全剖析_第4页
单例模式完全剖析_第5页
已阅读5页,还剩14页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

1、单例模式完全剖析概要 单例模式是最简单的设计模式之一,但是对于Java的开发者来说,它却有很多缺陷。在本月的专栏中,David Geary探讨了单例模式以及在面对多线程(multithreading)、类装载器(classloaders)和序列化(serialization)时如何处理这些缺陷。 单例模式适合于一个类只有一个实例的情况,比如窗口管理器,打印缓冲池和文件系统,它们都是原型的例子。典型的情况是,那些对象的类型被遍及一个软件系统的不同对象访问,因此需要一个全局的访问指针,这便是众所周知的单例模式的应用。当然这只有在你确信你不再需要任何多于一个的实例的情况下。 单例模式的用意在于前一段

2、中所关心的。通过单例模式你可以: 确保一个类只有一个实例被建立 提供了一个对对象的全局访问指针 在不影响单例类的客户端的情况下允许将来有多个实例 尽管单例设计模式如在下面的图中的所显示的一样是最简单的设计模式,但对于粗心的Java开发者来说却呈现出许多缺陷。这篇文章讨论了单例模式并揭示了那些缺陷。 注意:你可以从Resources下载这篇文章的源代码。 单例模式 在设计模式一书中,作者这样来叙述单例模式的:确保一个类只有一个实例并提供一个对它的全局访问指针。 下图说明了单例模式的类图。 (图1) 单例模式的类图 正如你在上图中所看到的,这不是单例模式的完整部分。此图中单例类保持了一个对唯一的单

3、例实例的静态引用,并且会从静态getInstance()方法中返回对那个实例的引用。 例1显示了一个经典的单例模式的实现。 例1.经典的单例模式 Java代码 1. public class ClassicSingleton     2.    private static ClassicSingleton instance = null;    3.      4.  

4、60; protected ClassicSingleton()     5.       / Exists only to defeat instantiation.    6.        7.    public static ClassicSingleton getIn

5、stance()     8.       if(instance = null)     9.          instance = new ClassicSingleton();    10.         &#

6、160; 11.       return instance;    12.        13.    public class ClassicSingleton private static ClassicSingleton instance = null; protected ClassicSingleton() / Exists only to defeat instantiation.

7、public static ClassicSingleton getInstance() if(instance = null) instance = new ClassicSingleton(); return instance; 在例1中的单例模式的实现很容易理解。ClassicSingleton类保持了一个对单独的单例实例的静态引用,并且从静态方法getInstance()中返回那个引用。 关于ClassicSingleton类,有几个让我们感兴趣的地方。首先,ClassicSingleton使用了一个众所周知的懒汉式实例化去创建那个单例类的引用;结果,这个单例类的实例直到getInst

8、ance()方法被第一次调用时才被创建。这种技巧可以确保单例类的实例只有在需要时才被建立出来。其次,注意ClassicSingleton实现了一个protected的构造方法,这样客户端不能直接实例化一个ClassicSingleton类的实例。然而,你会惊奇的发现下面的代码完全合法: Java代码 1. public class SingletonInstantiator      2.   public SingletonInstantiator()    &#

9、160; 3.    ClassicSingleton instance = ClassicSingleton.getInstance();    4. ClassicSingleton anotherInstance =    5. new ClassicSingleton();    6.        .   &

10、#160; 7.        8.    public class SingletonInstantiator public SingletonInstantiator() ClassicSingleton instance = ClassicSingleton.getInstance(); ClassicSingleton anotherInstance = new ClassicSingleton(); . 测试单例模式 接下来,我使用与log4j相对应的JUnit来测试单例类,它会贯穿在

11、这篇文章余下的部分。如果你对JUnit或log4j不很熟悉,请参考相关资源。 例2是一个用JUnit测试例1的单例模式的案例: 例2.一个单例模式的案例 Java代码 .      5. public class SingletonTest extends TestCase     6.    private ClassicSingleton sone = null, stwo

12、60;= null;    7.    private static Logger logger = Logger.getRootLogger();    8.      9.    public SingletonTest(String name)     10.     &#

13、160; super(name);    11.        12.    public void setUp()     13.       ("getting singleton.");    14.      

14、 sone = ClassicSingleton.getInstance();    15.       (".got singleton: " + sone);    16.      17.       ("getting 

15、;singleton.");    18.       stwo = ClassicSingleton.getInstance();    19.       (".got singleton: " + stwo);    20.     

16、;   21.    public void testUnique()     22.       ("checking singletons for equality");    23.       Assert.assertEquals(true, so

17、ne = stwo);    24.        25.     public class SingletonTest extends TestCase private ClassicSingleton sone = null, stwo = null; private static Logger logger = Logger.getRootLogger(); public SingletonTest(String name) super(name);

18、 public void setUp() ("getting singleton."); sone = ClassicSingleton.getInstance(); (".got singleton: " + sone); ("getting singleton."); stwo = ClassicSingleton.getInstance(); (".got singleton: " + stwo); public void

19、 testUnique() ("checking singletons for equality"); Assert.assertEquals(true, sone = stwo); 例2两次调用ClassicSingleton.getInstance(),并且把返回的引用存储在成员变量中。方法testUnique()会检查这些引用看它们是否相同。例3是这个测试案例的输出: 例3.是这个测试案例的输出 Java代码 1. Buildfile: build.xml    2.     

20、 3. init:    4.      echo Build 20030414 (14-04-2003 03:08)    5.      6. compile:    7.      8. run-test-text:    9.      ja

21、va .INFO main: bgetting singleton./b    10.      java INFO main: bcreated singleton:/b Singletone86f41    11.      java INFO main: .got singleton: Sin

22、gletone86f41    12.      java INFO main: bgetting singleton./b    13.      java INFO main: .got singleton: Singletone86f41    14.      

23、java INFO main: checking singletons for equality    15.      16.      java Time: 0.032    17.      18.      java OK (1 test)&

24、#160; Buildfile: build.xml init: echo Build 20030414 (14-04-2003 03:08) compile: run-test-text: java .INFO main: bgetting singleton./b java INFO main: bcreated singleton:/b Singletone86f41 java INFO main: .got singleton: Singletone86f41 java INFO main: bgetting singleton./b java INFO main: .got

25、 singleton: Singletone86f41 java INFO main: checking singletons for equality java Time: 0.032 java OK (1 test)正如前面的清单所示,例2的简单测试顺利通过-通过ClassicSingleton.getInstance()获得的两个单例类的引用确实相同;然而,你要知道这些引用是在单线程中得到的。下面的部分着重于用多线程测试单例类。 多线程因素的考虑 在例1中的ClassicSingleton.getInstance()方法由于下面的代码而不是线程安全的: Java代码 1. 1: 

26、;if(instance = null)     2. 2:    instance = new Singleton();    3. 3:    1: if(instance = null) 2: instance = new Singleton(); 3: 如果一个线程在第二行的赋值语句发生之前切换,那么成员变量instance仍然是null,然后另一个线程可能接下来进入到if块中。在这种情况

27、下,两个不同的单例类实例就被创建。不幸的是这种假定很少发生,这样这种假定也很难在测试期间出现(译注:在这可能是作者对很少出现这种情况而导致无法测试从而使人们放松警惕而感到叹惜)。为了演示这个线程轮换,我得重新实现例1中的那个类。例4就是修订后的单例类: 例4.人为安排的方式 Java代码 1.2.      3. public class Singleton     4.   private static Singleton singleton&

28、#160;= null;    5.   private static Logger logger = Logger.getRootLogger();    6.   private static boolean firstThread = true;    7.      8.   protect

29、ed Singleton()     9.     / Exists only to defeat instantiation.    10.       11.   public static Singleton getInstance()     12.   

30、60;  if(singleton = null)     13.         simulateRandomActivity();    14.         singleton = new Singleton();    15.   

31、0;      16.      ("created singleton: " + singleton);    17.      return singleton;    18.       19.   private

32、0;static void simulateRandomActivity()     20.      try     21.         if(firstThread)     22.            fir

33、stThread = false;    23.            ("sleeping.");    24.      25.            / This nap shoul

34、d give the second thread enough time    26.            / to get by the first thread.    27.           

35、60;  Thread.currentThread().sleep(50);    28.            29.          30.      catch(InterruptedException ex)     31.    

36、;     logger.warn("Sleep interrupted");    32.          33.       34.     public class Singleton private static Singleton singleton = null; private static Logger log

37、ger = Logger.getRootLogger(); private static boolean firstThread = true; protected Singleton() / Exists only to defeat instantiation. public static Singleton getInstance() if(singleton = null) simulateRandomActivity(); singleton = new Singleton(); ("created singleton: " + single

38、ton); return singleton; private static void simulateRandomActivity() try if(firstThread) firstThread = false; ("sleeping."); / This nap should give the second thread enough time / to get by the first thread. Thread.currentThread().sleep(50); catch(InterruptedException ex) logger

39、.warn("Sleep interrupted"); 除了在这个清单中的单例类强制使用了一个多线程错误处理,例4类似于例1中的单例类。在getInstance()方法第一次被调用时,调用这个方法的线程会休眠50毫秒以便另外的线程也有时间调用getInstance()并创建一个新的单例类实例。当休眠的线程觉醒时,它也会创建一个新的单例类实例,这样我们就有两个单例类实例。尽管例4是人为如此的,但它却模拟了第一个线程调用了getInstance()并在没有完成时被切换的真实情形。 例5测试了例4的单例类: 例5.失败的测试 Java代码 .   

40、;   5. public class SingletonTest extends TestCase     6.    private static Logger logger = Logger.getRootLogger();    7.    private static Singleton singleton 

41、= null;    8.      9.    public SingletonTest(String name)     10.       super(name);    11.        12.    public void&#

42、160;setUp()     13.       singleton = null;    14.        15.    public void testUnique() throws InterruptedException     16.   &#

43、160;   / Both threads call Singleton.getInstance().    17.       Thread threadOne = new Thread(new SingletonTestRunnable(),    18.         &

44、#160;    threadTwo = new Thread(new SingletonTestRunnable();    19.      20.       threadOne.start();    21.       threadTwo.start();   

45、0;22.      23.       threadOne.join();    24.       threadTwo.join();    25.        26.    private static class SingletonTestRunna

46、ble implements Runnable     27.       public void run()     28.          / Get a reference to the singleton.    29. 

47、0;        Singleton s = Singleton.getInstance();    30.      31.          / Protect singleton member variable from    32. 

48、60;        / multithreaded access.    33.          synchronized(SingletonTest.class)     34.             if(si

49、ngleton = null) / If local reference is null.    35.                singleton = s;     / .set it to the single

50、ton    36.              37.          / Local reference must be equal to the one and    38.      

51、;    / only instance of Singleton; otherwise, we have two    39.                   / Singleton instances.    4

52、0.          Assert.assertEquals(true, s = singleton);    41.           42.        43.     public class SingletonTest extends TestCase p

53、rivate static Logger logger = Logger.getRootLogger(); private static Singleton singleton = null; public SingletonTest(String name) super(name); public void setUp() singleton = null; public void testUnique() throws InterruptedException / Both threads call Singleton.getInstance(). Thread threadOne = n

54、ew Thread(new SingletonTestRunnable(), threadTwo = new Thread(new SingletonTestRunnable(); threadOne.start(); threadTwo.start(); threadOne.join(); threadTwo.join(); private static class SingletonTestRunnable implements Runnable public void run() / Get a reference to the singleton. Singleton s = Sing

55、leton.getInstance(); / Protect singleton member variable from / multithreaded access. synchronized(SingletonTest.class) if(singleton = null) / If local reference is null. singleton = s; / .set it to the singleton / Local reference must be equal to the one and / only instance of Singleton; otherwise,

56、 we have two / Singleton instances. Assert.assertEquals(true, s = singleton); 例5的测试案例创建两个线程,然后各自启动,等待完成。这个案例保持了一个对单例类的静态引用,每个线程都会调用Singleton.getInstance()。如果这个静态成员变量没有被设置,那么第一个线程就会将它设为通过调用getInstance()而得到的引用,然后这个静态变量会与一个局部变量比较是否相等。 在这个测试案例运行时会发生一系列的事情:第一个线程调用getInstance(),进入if块,然后休眠;接着,第二个线程也调用getIn

57、stance()并且创建了一个单例类的实例。第二个线程会设置这个静态成员变量为它所创建的引用。第二个线程检查这个静态成员变量与一个局部备份的相等性。然后测试通过。当第一个线程觉醒时,它也会创建一个单例类的实例,并且它不会设置那个静态成员变量(因为第二个线程已经设置过了),所以那个静态变量与那个局部变量脱离同步,相等性测试即告失败。例6列出了例5的输出: 例6.例5的输出 Java代码 1. Buildfile: build.xml    2. init:    3.      

58、;echo Build 20030414 (14-04-2003 03:06)    4. compile:    5. run-test-text:    6. INFO Thread-1: sleeping.    7. INFO Thread-2: created singleton: Singleton7e5cbd    8. INFO&#

59、160;Thread-1: created singleton: Singleton704ebb    9.10. 47)    11. 282)    12. 64)    13. 149)    14. 155)    15.    at SingletonTest$SingletonTestRunnable.run(Unknown 

60、Source)    16. 554)    17.      java .    18.      java Time: 0.577    19.      20.      java OK (1 test) 

61、0; Buildfile: build.xml init: echo Build 20030414 (14-04-2003 03:06) compile: run-test-text: INFO Thread-1: sleeping. INFO Thread-2: created singleton: Singleton7e5cbd INFO Thread-1: created singleton: Singleton704ebb at SingletonTest$SingletonTestRunnable.run(Unknown Source) java . java Time:

62、0.577 java OK (1 test) 到现在为止我们已经知道例4不是线程安全的,那就让我们看看如何修正它。 同步 要使例4的单例类为线程安全的很容易-只要像下面一个同步化getInstance()方法: Java代码 1. public synchronized static Singleton getInstance()     2.    if(singleton = null)     3.  

63、0;    simulateRandomActivity();    4.       singleton = new Singleton();    5.        6.    ("created singleton: " + si

64、ngleton);    7.    return singleton;    8.    public synchronized static Singleton getInstance() if(singleton = null) simulateRandomActivity(); singleton = new Singleton(); ("created singleton: " + singleton); retu

65、rn singleton; 在同步化getInstance()方法后,我们就可以得到例5的测试案例返回的下面的结果: Java代码 1. Buildfile: build.xml    2.      3. init:    4.      echo Build 20030414 (14-04-2003 03:15)    5.     

66、 6. compile:    7.     javac Compiling 2 source files    8.      9. run-test-text:    10. INFO Thread-1: sleeping.    11. INFO Thread-1: created singl

67、eton: Singletonef577d    12. INFO Thread-2: created singleton: Singletonef577d    13.      java .    14.      java Time: 0.513    15.    

68、60; 16.      java OK (1 test)   Buildfile: build.xml init: echo Build 20030414 (14-04-2003 03:15) compile: javac Compiling 2 source files run-test-text: INFO Thread-1: sleeping. INFO Thread-1: created singleton: Singletonef577d INFO Thread-

69、2: created singleton: Singletonef577d java . java Time: 0.513 java OK (1 test) 这此,这个测试案例工作正常,并且多线程的烦恼也被解决;然而,机敏的读者可能会认识到getInstance()方法只需要在第一次被调用时同步。因为同步的性能开销很昂贵(同步方法比非同步方法能降低到100次左右),或许我们可以引入一种性能改进方法,它只同步单例类的getInstance()方法中的赋值语句。 一种性能改进的方法 寻找一种性能改进方法时,你可能会选择像下面这样重写getInstance()方法: Java代码 1. public

70、 static Singleton getInstance()     2.    if(singleton = null)     3.       synchronized(Singleton.class)      4.         

71、60;singleton = new Singleton();    5.           6.        7.    return singleton;    8.    public static Singleton getInstance() if(singleton = n

72、ull) synchronized(Singleton.class) singleton = new Singleton(); return singleton; 这个代码片段只同步了关键的代码,而不是同步整个方法。然而这段代码却不是线程安全的。考虑一下下面的假定:线程1进入同步块,并且在它给singleton成员变量赋值之前线程1被切换。接着另一个线程进入if块。第二个线程将等待直到第一个线程完成,并且仍然会得到两个不同的单例类实例。有修复这个问题的方法吗?请读下去。 双重加锁检查 初看上去,双重加锁检查似乎是一种使懒汉式实例化为线程安全的技术。下面的代码片段展示了这种技术: Java代码

73、1. public static Singleton getInstance()     2.   if(singleton = null)     3.      synchronized(Singleton.class)     4.        if(singleton 

74、= null)     5.          singleton = new Singleton();    6.            7.         8.       

75、9.   return singleton;    10.    public static Singleton getInstance() if(singleton = null) synchronized(Singleton.class) if(singleton = null) singleton = new Singleton(); return singleton; 如果两个线程同时访问getInstance()方法会发生什么?想像一下线程1进行同步块马上又被切换。接着,第二个线程进入if 块。

76、当线程1退出同步块时,线程2会重新检查看是否singleton实例仍然为null。因为线程1设置了singleton成员变量,所以线程2的第二次检查会失败,第二个单例类实例也就不会被创建。似乎就是如此。 不幸的是,双重加锁检查不会保证正常工作,因为编译器会在Singleton的构造方法被调用之前随意给singleton赋一个值。如果在singleton引用被赋值之后而被初始化之前线程1被切换,线程2就会被返回一个对未初始化的单例类实例的引用。 一个改进的线程安全的单例模式实现 例7列出了一个简单、快速而又是线程安全的单例模式实现: 例7.一个简单的单例类 Java代码 1. public

77、60;class Singleton     2.    public final static Singleton INSTANCE = new Singleton();    3.    private Singleton()     4.         

78、; / Exists only to defeat instantiation.    5.           6.    public class Singleton public final static Singleton INSTANCE = new Singleton(); private Singleton() / Exists only to defeat instant

79、iation. 这段代码是线程安全的是因为静态成员变量一定会在类被第一次访问时被创建。你得到了一个自动使用了懒汉式实例化的线程安全的实现;你应该这样使用它: Java代码 1. Singleton singleton = Singleton.INSTANCE;    2. singleton.dothis();    3. singleton.dothat();    4. .    Singleton singleton = Singleton

80、.INSTANCE; singleton.dothis(); singleton.dothat(); . 当然万事并不完美,前面的Singleton只是一个折衷的方案;如果你使用那个实现,你就无法改变它以便后来你可能想要允许多个单例类的实例。用一种更折哀的单例模式实现(通过一个getInstance()方法获得实例)你可以改变这个方法以便返回一个唯一的实例或者是数百个实例中的一个你不能用一个公开且是静态的(public static)成员变量这样做 你可以安全的使用例的单例模式实现或者是例的带一个同步的getInstance()方法的实现然而,我们必须要研究另一个问题:你必须在编译期指定这个单

81、例类,这样就不是很灵活一个单例类的注册表会让我们在运行期指定一个单例类 使用注册表 使用一个单例类注册表可以: 在运行期指定单例类 防止产生多个单例类子类的实例 在例8的单例类中,保持了一个通过类名进行注册的单例类注册表: 例8 带注册表的单例类 Java代码 1.2.3.      4. public class Singleton     5.    private static HashMap map = ne

82、w HashMap();    6.    private static Logger logger = Logger.getRootLogger();    7.      8.    protected Singleton()     9.       / 

83、;Exists only to thwart instantiation    10.        11.    public static synchronized Singleton getInstance(String classname)     12.       if(cla

84、ssname = null) throw new IllegalArgumentException("Illegal classname");    13.          Singleton singleton = (Singleton)map.get(classname);    14.      15.       if(singleton != null)     16.          ("got singleton from&

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论