漫谈设计模式-技术要点详解

开发 后端
本篇文章给出其中的一章供参考,想阅读书籍全部内容,请参见原文附件, 示例代码也在附件里,也可以登录http://code.google.com/p/rambling-on-patterns/下载代码。

第3章 单例(Singleton)模式

3.1 概述

如果要保证系统里一个类最多只能存在一个实例时,我们就需要单例模式。这种情况在我们应用中经常碰到,例如缓存池,数据库连接池,线程池,一些应用服务实例等。在多线程环境中,为了保证实例的唯一性其实并不简单,这章将和读者一起探讨如何实现单例模式。

3.2 最简单的单例

为了限制该类的对象被随意地创建,我们保证该类构造方法是私有的,这样外部类就无法创建该类型的对象了;另外,为了给客户对象提供对此单例对象的使用,我们为它提供一个全局访问点,代码如下所示:

 

  1. public class Singleton {   
  2.     private static Singleton instance = new Singleton();     
  3.     //other fields…   
  4.  
  5.     private Singleton() {   
  6.     }   
  7.  
  8.     public static Singleton getInstance() {   
  9.         return instance;   
  10.     }   
  11.      
  12.     //other methods…   
  13. }   

 

代码注解:

l Singleton类的只有一个构造方法,它是被private修饰的,客户对象无法创建该类实例。

l 我们为此单例实现的全局访问点是public static Singleton getInstance()方法,注意,instance变量是私有的,外界无法访问的。

读者还可以定义instance变量是public的,这样把属性直接暴露给其他对象,就没必要实现public static Singleton getInstance()方法,但是可读性没有方法来的直接,而且把该实例变量的名字直接暴露给客户程序,增加了代码的耦合度,如果改变此变量名称,会引起客户类的改变。

还有一点,如果该实例需要比较复杂的初始化过程时,把这个过程应该写在static{…}代码块中。

l 此实现是线程安全的,当多个线程同时去访问该类的getInstance()方法时,不会初始化多个不同的对象,这是因为,JVM(Java Virtual Machine)在加载此类时,对于static属性的初始化只能由一个线程执行且仅一次[1]。

由于此单例提供了静态的公有方法,那么客户使用单例模式的代码也就非常简单了,如下所示:

Singleton singleton = Singleton.getInstance();

3.3 进阶

3.3.1 延迟创建

如果出于性能等的考虑,我们希望延迟实例化单例对象(Static属性在加载类是就会被初始化),只有在第一次使用该类的实例时才去实例化,我们应该怎么办呢?

这个其实并不难做到,我们把单例的实例化过程移至getInstance()方法,而不在加载类时预先创建。当访问此方法时,首先判断该实例是不是已经被实例化过了,如果已被初始化,则直接返回这个对象的引用;否则,创建这个实例并初始化,最后返回这个对象引用。代码片段如下所示:

 

  1. public class UnThreadSafeSingelton {   
  2.     //variables and constructors…   
  3.  
  4.     public static UnThreadSafeSingelton getInstance() {   
  5.         if(instatnce ==null){   
  6.           instatnce = new UnThreadSafeSingelton();   
  7.         }   
  8.         return instatnce;   
  9.     }   
  10. }   

 

我们使用这句if(instatnce ==null) 判断是否实例化完成了。此方法不是线程安全的,接下来我们将会讨论。

3.3.2 线程安全

上节我们创建了可延迟初始化的单例,然而不幸的是,在高并发的环境中,getInstance()方法返回了多个指向不同的该类实例,究竟是什么原因呢?我们针对此方法,给出两个线程并发访问getInstance()方法时的一种情况,如下所示:

t1 t2

1 if(instatnce ==null)

2 if(instatnce ==null)

3 instatnce = new UnThreadSafeSingelton();

4 return instatnce;

5 instatnce = new UnThreadSafeSingelton()

6 return instatnce;

如果这两个线程按照上述步骤执行,不难发现,在时刻1和2,由于还没有创建单例对象,Thread1和Thread2都会进入创建单例实例的代码块分别创建实例。在时刻3,Thread1创建了一个实例对象,但是Thread2此时已无法知道,继续创建一个新的实例对象,于是这两个线程持有的实例并非为同一个。更为糟糕的是,在没有自动内存回收机制的语言平台上运行这样的单例模式,例如使用C++编写此模式,因为我们认为创建了一个单例实例,忽略了其他线程所产生的对象,不会手动去回收它们,引起了内存泄露。

为了解决这个问题,我们给此方法添加synchronized关键字,代码如下:

 

  1. public class ThreadSafeSingelton {   
  2.     //variables and constructors…   
  3.  
  4.     public static synchronized ThreadSafeSingelton getInstance() {   
  5.         if(instatnce ==null){   
  6.             instatnce = new ThreadSafeSingelton();   
  7.         }   
  8.         return instatnce;   
  9.     }   
  10. }   

 

这样,再多的线程访问都只会实例化一个单例对象。

3.3.3 Double-Check Locking

上述途径虽然实现了多线程的安全访问,但是在多线程高并发访问的情况下,给此方法加上synchronized关键字会使得性能大不如前。我们仔细分析一下不难发现,使用了synchronized关键字对整个getInstance()方法进行同步是没有必要的:我们只要保证实例化这个对象的那段逻辑被一个线程执行就可以了,而返回引用的那段代码是没有必要同步的。按照这个想法,我们的代码片段大致如下所示:

 

  1. public class DoubleCheckSingleton {   
  2.     private volatile static DoubleCheckSingleton instatnce = null;   
  3.  
  4.     //constructors   
  5.  
  6.     public static DoubleCheckSingleton getInstance() {   
  7.         if (instatnce == null) {   //check if it is created.   
  8.             synchronized (DoubleCheckSingleton.class) {  //synchronize creation block   
  9.                 if (instatnce == null)   //double check if it is created   
  10.                     instatnce = new DoubleCheckSingleton();   
  11.             }   
  12.         }   
  13.         return instatnce;   
  14.     }   
  15. }   

 

代码注解:

l 在getInstance()方法里,我们首先判断此实例是否已经被创建了,如果还没有创建,首先使用synchronized同步实例化代码块。在同步代码块里,我们还需要再次检查是否已经创建了此类的实例,这是因为:如果没有第二次检查,这时有两个线程Thread A和Thread B同时进入该方法,它们都检测到instatnce为null,不管哪一个线程先占据同步锁创建实例对象,都不会阻止另外一个线程继续进入实例化代码块重新创建实例对象,这样,同样会生成两个实例对象。所以,我们在同步的代码块里,进行第二次判断判断该对象是否已被创建。

正是由于使用了两次的检查,我们称之为double-checked locking模式。

l 属性instatnce是被volatile修饰的,因为volatile具有synchronized的可见性特点,也就是说线程能够自动发现volatile变量的最新值。这样,如果instatnce实例化成功,其他线程便能立即发现。

注意:

此程序只有在JAVA 5及以上版本才能正常运行,在以前版本不能保证其正常运行。这是由于Java平台的内存模式容许out-of-order writes引起的,假定有两个线程,Thread 1和Thread 2,它们执行以下步骤:

1. Thread 1发现instatnce没有被实例化,它获得锁并去实例化此对象,JVM容许在没有完全实例化完成时,instance变量就指向此实例,因为这些步骤可以是out-of-order writes的,此时instance==null为false,之前的版本即使用volatile关键字修饰也无效。

2. 在初始化完成之前,Thread 2进入此方法,发现instance已经不为null了,Thread 2便认为该实例初始化完成了,使用这个未完全初始化的实例对象,则很可能引起系统的崩溃。

3.3.4 Initialization on demand holder

要使用线程安全的延迟的单例初始化,我们还有一种方法,称为Initialization on demand holder模式,代码如下所示:

 

  1. public class LazyLoadedSingleton {   
  2.     private LazyLoadedSingleton() {   
  3.        }   
  4.  
  5.        private static class LazyHolder {  //holds the singleton class   
  6.               private static final LazyLoadedSingleton singletonInstatnce = new LazyLoadedSingleton();   
  7.        }   
  8.  
  9.        public static LazyLoadedSingleton getInstance() {   
  10.               return LazyHolder.singletonInstatnce;   
  11.        }   
  12. }   

 

当JVM加载LazyLoadedSingleton类时,由于该类没有static属性,所以加载完成后便即可返回。只有第一次调用getInstance()方法时,JVM才会加载LazyHolder类,由于它包含一个static属性singletonInstatnce,所以会首先初始化这个变量,根据前面的介绍,我们知道此过程并不会出现并发问题(JLS保证),这样即实现了一个既线程安全又支持延迟加载的单例模式。

3.3.5 Singleton的序列化

如果单例类实现了Serializable接口,这时我们得特别注意,因为我们知道在默认情况下,每次反序列化(Desierialization)总会创建一个新的实例对象,这样一个系统会出现多个对象供使用。我们应该怎么办呢?

熟悉Java序列化的读者可能知道,我们需要在readResolve()方法里做文章,此方法在反序列化完成之前被执行,我们在此方法里替换掉反序列化出来的那个新的实例,让其指向内存中的那个单例对象即可,代码实现如下:

 

  1. import java.io.Serializable;   
  2.  
  3. public class SerialibleSingleton implements Serializable {   
  4.     private static final long serialVersionUID = -6099617126325157499L;   
  5.     static SerialibleSingleton singleton = new SerialibleSingleton();   
  6.  
  7.     private SerialibleSingleton() {   
  8.     }   
  9.  
  10.     // This method is called immediately after an object of this class is deserialized.   
  11.     // This method returns the singleton instance.   
  12.     private Object readResolve() {   
  13.         return singleton;   
  14.     }   
  15. }   

 

方法readResolve()直接返回singleton单例,这样,我们在内存中始终保持了一个唯一的单例对象。

3.4 总结

通过这一章的学习,我相信大家对于基本的单例模式已经有了一个比较充分的认识。其实我们这章讨论的是在同一个JVM中,如何保证一个类只有一个单例,如果在分布式环境中,我们可能需要考虑如何保证在整个应用(可能分布在不同JVM上)只有一个实例,但这也超出本书范畴,在这里将不再做深入研究,有兴趣的读者可以查阅相关资料深入研究。

________________________________________

[1] Static属性和Static初始化块(Static Initializers)的初始化过程是串行的,这个由JLS(Java Language Specification)保证,参见James Gosling, Bill Joy, Guy Steele and Gilad Bracha编写的《 The Java™ Language Specification Third Edition》一书的12.4一节。

原文链接:http://redhat.iteye.com/blog/1007884

责任编辑:金贺 来源: ITEYE博客
相关推荐

2010-04-19 09:30:00

工厂模式PHP设计模式

2010-03-25 08:52:30

PHP设计模式代理模式

2010-04-13 08:54:28

PHP设计模式命令模式

2010-04-21 08:38:18

解释器模式PHP设计模式

2010-04-08 09:27:04

PHP设计模式结构模式

2010-07-08 14:25:12

HART协议

2010-04-01 09:10:03

PHP设计模式责任链模式

2010-04-29 08:53:11

PHP迭代器模式

2011-06-02 18:02:50

iPhone MVC

2010-05-06 08:44:37

调解者模式

2009-10-15 13:11:28

综合布线系统

2009-11-11 17:48:36

OSPF路由技术

2021-06-29 08:54:23

设计模式代理模式远程代理

2009-08-18 11:03:31

Observer设计模

2009-07-10 16:14:29

MVC设计模式Swing

2018-02-07 15:25:41

2015-09-15 09:20:22

Neutron技术虚拟化

2011-07-26 15:29:36

Cocoa 模式

2011-07-26 17:31:52

iOS 设计模式

2010-01-21 09:08:53

.NET设计模式
点赞
收藏

51CTO技术栈公众号