java设计模式-单例模式

单例模式

单例模式是设计模式中使用最为普遍的模式之一。它是一种对象创建模式,用于产生一个对象的具体实例,它可以确保系统中一个类只产生一个实例。在Java语言中,这样的行为能带来两大好处:

(1)对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销。

(2)由于new操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻GC压力,缩短GC停顿时间。

因此对于系统的关键组件和被频繁使用的对象,使用单例模式便可以有效地改善系统的性能。

单例模式的参与者非常简单,只有单例类和使用者两个,如表2.1所示。

表2.1  单例模式角色

角  色

作    用

单例类

提供单例的工厂,返回单例

使用者

获取并使用单例类

它的基本结构如图2.1所示。

 
图2.1  单例模式类图
单例模式的核心在于通过一个接口返回唯一的对象实例。一个简单的单例实现如下:
 
 
  1. public class Singleton {  
  2.     private Singleton(){  
  3.         System.out.println("Singleton is create"); //创建单例的过程可能会比较慢  
  4.     }  
  5.     private static Singleton instance = new Singleton();  
  6.     public static Singleton getInstance() {  
  7.         return instance;  
  8.     }   
  9. }  

注意代码中的重点标注部分,首先单例类必须要有一个private访问级别的构造函数,只有这样,才能确保单例不会在系统中的其他代码内被实例化,这点是相当重要的;其次,instance成员变量和getInstance()方法必须是static的。

注意:单例模式是非常常用的一种结构,几乎所有的系统中都可以找到它的身影。因此,希望读者可以通过本节,了解单例模式的几种实现方式及其各自的特点。

这种单例的实现方式非常简单,而且十分可靠。它唯一的不足仅是无法对instance实例做延迟加载。假如单例的创建过程很慢,而由于instance成员变量是static定义的,因此在JVM加载单例类时,单例对象就会被建立,如果此时,这个单例类在系统中还扮演其他角色,那么在任何使用这个单例类的地方都会初始化这个单例变量,而不管是否会被用到。比如单例类作为String工厂,用于创建一些字符串(该类既用于创建单例Singleton,又用于创建String对象):

 
 
  1. public class Singleton {  
  2.     private Singleton() {  
  3.         System.out.println("Singleton is create");   
  4.                                             //创建单例的过程可能会比较慢  
  5.     }  
  6.  
  7.     private static Singleton instance = new Singleton();  
  8.     public static Singleton getInstance() {  
  9.         return instance;  
  10.     }  
  11.  
  12.     public static void createString(){      //这是模拟单例类扮演其他角色  
  13.         System.out.println("createString in Singleton");  
  14.     }  
  15. }  

    当使用Singleton.createString()执行任务时,程序输出:

        
        
    1. Singleton is create  
    2. createString in Singleton 

    可以看到,虽然此时并没有使用单例类,但它还是被创建出来,这也许是开发人员所不愿意见到的。为了解决这个问题,并以此提高系统在相关函数调用时的反应速度,就需要引入延迟加载机制。

        
        
    1. public class LazySingleton {  
    2.     private LazySingleton(){  
    3.         System.out.println("LazySingleton is create");  
    4.                                             //创建单例的过程可能会比较慢  
    5.     }  
    6.     private static LazySingleton instance = null;  
    7.         public static synchronized LazySingleton getInstance() {  
    8.         if (instance==null)  
    9.             instance=new LazySingleton();  
    10.         return instance;  
    11.     }  
    12. }  

    首先,对于静态成员变量instance初始值赋予null,确保系统启动时没有额外的负载;其次,在getInstance()工厂方法中,判断当前单例是否已经存在,若存在则返回,不存在则再建立单例。这里尤其还要注意,getInstance()方法必须是同步的,否则在多线程环境下,当线程1正新建单例时,完成赋值操作前,线程2可能判断instance为null,故线程2也将启动新建单例的程序,而导致多个实例被创建,故同步关键字是必须的。

    使用上例中的单例实现,虽然实现了延迟加载的功能,但和第一种方法相比,它引入了同步关键字,因此在多线程环境中,它的时耗要远远大于第一种单例模式。以下测试代码就说明了这个问题:

        
        
    1. @Override  
    2. public void run(){  
    3.     for(int i=0;i<100000;i++)  
    4.         Singleton.getInstance();  
    5.         //LazySingleton.getInstance();  
    6.     System.out.println("spend:"+(System.currentTimeMillis()-begintime));      
    7. }  

    开启5个线程同时完成以上代码的运行,使用第1种类型的单例耗时0ms,而使用LazySingleton却相对耗时约390ms。性能至少相差2个数量级。

    注意:在本书中,会使用很多类似的代码片段用于测试不同代码的执行速度。在不同的计算机上其测试结果很可能与笔者不同。读者大可不必关心测试数据的绝对值,只要观察用于比较的目标代码间的相对耗时即可。

    为了使用延迟加载引入的同步关键字反而降低了系统性能,是不是有点得不偿失呢?为了解决这个为问题,还需要对其进行改进:

        
        
    1. public class StaticSingleton {  
    2.     private StaticSingleton(){  
    3.         System.out.println("StaticSingleton is create");  
    4.     }  
    5.     private static class SingletonHolder {  
    6.         private static StaticSingleton instance = new StaticSingleton();  
    7.     }  
    8.     public static StaticSingleton getInstance() {  
    9.         return SingletonHolder.instance;  
    10.     }  
    11. }  

    在这个实现中,单例模式使用内部类来维护单例的实例,当StaticSingleton被加载时,其内部类并不会被初始化,故可以确保当StaticSingleton类被载入JVM时,不会初始化单例类,而当getInstance()方法被调用时,才会加载SingletonHolder,从而初始化instance。同时,由于实例的建立是在类加载时完成,故天生对多线程友好,getInstance()方法也不需要使用同步关键字。因此,这种实现方式同时兼备以上两种实现的优点。

    注意:使用内部类的方式实现单例,既可以做到延迟加载,也不必使用同步关键字,是一种比较完善的实现。

    通常情况下,用以上方式实现的单例已经可以确保在系统中只存在唯一实例了。但仍然有例外情况,可能导致系统生成多个实例,比如,在代码中,通过反射机制,强行调用单例类的私有构造函数,生成多个单例。考虑到情况的特殊性,本书中不对这种极端的方式进行讨论。但仍有些合法的方法,可能导致系统出现多个单例类的实例。

    一个可以被串行化的单例:

        
        
    1. public class SerSingleton implements java.io.Serializable{  
    2.     String name;  
    3.       
    4.     private SerSingleton() {  
    5.         System.out.println("Singleton is create");  
    6.                                         //创建单例的过程可能会比较慢  
    7.         name="SerSingleton";  
    8.     }  
    9.  
    10.     private static SerSingleton instance = new SerSingleton();  
    11.     public static SerSingleton getInstance() {  
    12.         return instance;  
    13.     }  
    14.  
    15.     public static void createString(){  
    16.         System.out.println("createString in Singleton");  
    17.     }  
    18.       
    19.     private Object readResolve(){       //阻止生成新的实例,总是返回当前对象  
    20.         return instance;    
    21.     }    
    22. }  

    测试代码如下:
        
        
    1. @Test  
    2. public void test() throws Exception {  
    3.     SerSingleton s1 = null;  
    4.     SerSingleton s = SerSingleton.getInstance();  
    5.     //先将实例串行化到文件  
    6.     FileOutputStream fos = new FileOutputStream("SerSingleton.txt");  
    7.     ObjectOutputStream oos = new ObjectOutputStream(fos);  
    8.     oos.writeObject(s);  
    9.     oos.flush();  
    10.     oos.close();  
    11.     //从文件读出原有的单例类  
    12.     FileInputStream fis = new FileInputStream("SerSingleton.txt");  
    13.     ObjectInputStream ois = new ObjectInputStream(fis);  
    14.     s1 = (SerSingleton) ois.readObject();  
    15.       
    16.     Assert.assertEquals(s, s1);  
    17. }  

    使用一段测试代码测试单例的串行化和反串行化,当去掉SerSingleton代码中加粗的readReslove()函数时,以下测试代码抛出异常:


        
        
    1. junit.framework.AssertionFailedError: expected:javatuning.ch2.singleton.  
    2. serialization.SerSingleton@5224ee  
    3.  but was:javatuning.ch2.singleton.serialization.SerSingleton@18fe7c3  

    说明测试代码中s和s1指向了不同的实例,在反序列化后,生成多个对象实例。而加上readReslove()函数的,程序正常退出。说明,即便经过反序列化,仍然保持了单例的特征。事实上,在实现了私有的readReslove()方法后,readObject()已经形同虚设,它直接使用readReslove()替换了原本的返回值,从而在形式上构造了单例。

    注意:序列化和反序列化可能会破坏单例。一般来说,对单例进行序列化和反序列化的场景并不多见,但如果存在,就要多加注意。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值