`

java之单例模式祥解

阅读更多

转自javaeye的作者calmness,看完以后我很佩服,他的连接地址:

http://www.iteye.com/topic/60179 

概要
单例模式是最简单的设计模式之一,但是对于Java的开发者来说,它却有很多缺陷。在本月的专栏中,David Geary探讨了单例模式以及在面对多线程(multithreading)、类装载器(classloaders)和序列化(serialization)时如何处理这些缺陷。

单例模式适合于一个类只有一个实例的情况,比如窗口管理器,打印缓冲池和文件系统,它们都是原型的例子。典型的情况是,那些对象的类型被遍及一个软件系统的不同对象访问,因此需要一个全局的访问指针,这便是众所周知的单例模式的应用。当然这只有在你确信你不再需要任何多于一个的实例的情况下。
单例模式的用意在于前一段中所关心的。通过单例模式你可以:


确保一个类只有一个实例被建立
提供了一个对对象的全局访问指针
在不影响单例类的客户端的情况下允许将来有多个实例

尽管单例设计模式如在下面的图中的所显示的一样是最简单的设计模式,但对于粗心的Java开发者来说却呈现出许多缺陷。这篇文章讨论了单例模式并揭示了那些缺陷。
注意:你可以从Resources下载这篇文章的源代码。

单例模式

在《设计模式》一书中,作者这样来叙述单例模式的:确保一个类只有一个实例并提供一个对它的全局访问指针。
下图说明了单例模式的类图。
(图1)

单例模式的类图

正如你在上图中所看到的,这不是单例模式的完整部分。此图中单例类保持了一个对唯一的单例实例的静态引用,并且会从静态getInstance()方法中返回对那个实例的引用。
例1显示了一个经典的单例模式的实现。
例1.经典的单例模式

Java代码 复制代码
  1. public class ClassicSingleton {    
  2.    private static ClassicSingleton instance = null;    
  3.      
  4.    protected ClassicSingleton() {    
  5.       // Exists only to defeat instantiation.    
  6.    }    
  7.    public static ClassicSingleton getInstance() {    
  8.       if(instance == null) {    
  9.          instance = new ClassicSingleton();    
  10.       }    
  11.       return instance;    
  12.    }    
  13. }   


在例1中的单例模式的实现很容易理解。ClassicSingleton类保持了一个对单独的单例实例的静态引用,并且从静态方法getInstance()中返回那个引用。
关于ClassicSingleton类,有几个让我们感兴趣的地方。首先,ClassicSingleton使用了一个众所周知的懒汉式实例化去创建那个单例类的引用;结果,这个单例类的实例直到getInstance()方法被第一次调用时才被创建。这种技巧可以确保单例类的实例只有在需要时才被建立出来。其次,注意ClassicSingleton实现了一个protected的构造方法,这样客户端不能直接实例化一个ClassicSingleton类的实例。然而,你会惊奇的发现下面的代码完全合法:
Java代码 复制代码
  1. public class SingletonInstantiator {     
  2.   public SingletonInstantiator() {     
  3.    ClassicSingleton instance = ClassicSingleton.getInstance();    
  4. ClassicSingleton anotherInstance =    
  5. new ClassicSingleton();    
  6.        ...     
  7.   }     
  8. }   


前面这个代码片段为何能在没有继承ClassicSingleton并且ClassicSingleton类的构造方法是protected的情况下创建其实例?答案是protected的构造方法可以被其子类以及在同一个包中的其它类调用。因为ClassicSingleton和SingletonInstantiator位于相同的包(缺省的包),所以SingletonInstantiator方法能创建ClasicSingleton的实例。
这种情况下有两种解决方案:一是你可以使ClassicSingleton的构造方法变化私有的(private)这样只有ClassicSingleton的方法能调用它;然而这也意味着ClassicSingleton不能有子类。有时这是一种很合意的解决方法,如果确实如此,那声明你的单例类为final是一个好主意,这样意图明确,并且让编译器去使用一些性能优化选项。另一种解决方法是把你的单例类放到一个外在的包中,以便在其它包中的类(包括缺省的包)无法实例化一个单例类。
关于ClassicSingleton的第三点感兴趣的地方是,如果单例由不同的类装载器装入,那便有可能存在多个单例类的实例。假定不是远端存取,例如一些servlet容器对每个servlet使用完全不同的类装载器,这样的话如果有两个servlet访问一个单例类,它们就都会有各自的实例。
第四点,如果ClasicSingleton实现了java.io.Serializable接口,那么这个类的实例就可能被序列化和复原。不管怎样,如果你序列化一个单例类的对象,接下来复原多个那个对象,那你就会有多个单例类的实例。
最后也许是最重要的一点,就是例1中的ClassicSingleton类不是线程安全的。如果两个线程,我们称它们为线程1和线程2,在同一时间调用ClassicSingleton.getInstance()方法,如果线程1先进入if块,然后线程2进行控制,那么就会有ClassicSingleton的两个的实例被创建。

正如你从前面的讨论中所看到的,尽管单例模式是最简单的设计模式之一,在Java中实现它也是决非想象的那么简单。这篇文章接下来会揭示Java规范对单例模式进行的考虑,但是首先让我们近水楼台的看看你如何才能测试你的单例类。

测试单例模式

接下来,我使用与log4j相对应的JUnit来测试单例类,它会贯穿在这篇文章余下的部分。如果你对JUnit或log4j不很熟悉,请参考相关资源。

例2是一个用JUnit测试例1的单例模式的案例:
例2.一个单例模式的案例

Java代码 复制代码
  1. import org.apache.log4j.Logger;    
  2. import junit.framework.Assert;    
  3. import junit.framework.TestCase;    
  4.      
  5. public class SingletonTest extends TestCase {    
  6.    private ClassicSingleton sone = null, stwo = null;    
  7.    private static Logger logger = Logger.getRootLogger();    
  8.      
  9.    public SingletonTest(String name) {    
  10.       super(name);    
  11.    }    
  12.    public void setUp() {    
  13.       logger.info("getting singleton...");    
  14.       sone = ClassicSingleton.getInstance();    
  15.       logger.info("...got singleton: " + sone);    
  16.      
  17.       logger.info("getting singleton...");    
  18.       stwo = ClassicSingleton.getInstance();    
  19.       logger.info("...got singleton: " + stwo);    
  20.    }    
  21.    public void testUnique() {    
  22.       logger.info("checking singletons for equality");    
  23.       Assert.assertEquals(true, sone == stwo);    
  24.    }    
  25. }   


例2两次调用ClassicSingleton.getInstance(),并且把返回的引用存储在成员变量中。方法testUnique()会检查这些引用看它们是否相同。例3是这个测试案例的输出:
例3.是这个测试案例的输出

Java代码 复制代码
  1. Buildfile: build.xml    
  2.      
  3. init:    
  4.      [echo] Build 20030414 (14-04-2003 03:08)    
  5.      
  6. compile:    
  7.      
  8. run-test-text:    
  9.      [java] .INFO main: <STRONG>getting singleton...</STRONG>    
  10.      [java] INFO main: <STRONG>created singleton:</STRONG> Singleton@e86f41    
  11.      [java] INFO main: ...got singleton: Singleton@e86f41    
  12.      [java] INFO main: <STRONG>getting singleton...</STRONG>    
  13.      [java] INFO main: ...got singleton: Singleton@e86f41    
  14.      [java] INFO main: checking singletons for equality    
  15.      
  16.      [java] Time: 0.032    
  17.      
  18.      [java] OK (1 test)  


正如前面的清单所示,例2的简单测试顺利通过----通过ClassicSingleton.getInstance()获得的两个单例类的引用确实相同;然而,你要知道这些引用是在单线程中得到的。下面的部分着重于用多线程测试单例类。


多线程因素的考虑

在例1中的ClassicSingleton.getInstance()方法由于下面的代码而不是线程安全的:
Java代码 复制代码
  1. 1if(instance == null) {    
  2. 2:    instance = new Singleton();    
  3. 3: }   


如果一个线程在第二行的赋值语句发生之前切换,那么成员变量instance仍然是null,然后另一个线程可能接下来进入到if块中。在这种情况下,两个不同的单例类实例就被创建。不幸的是这种假定很少发生,这样这种假定也很难在测试期间出现(译注:在这可能是作者对很少出现这种情况而导致无法测试从而使人们放松警惕而感到叹惜)。为了演示这个线程轮换,我得重新实现例1中的那个类。例4就是修订后的单例类:
例4.人为安排的方式

Java代码 复制代码
  1. import org.apache.log4j.Logger;    
  2.      
  3. public class Singleton {    
  4.   private static Singleton singleton = null;    
  5.   private static Logger logger = Logger.getRootLogger();    
  6.   private static boolean firstThread = true;    
  7.      
  8.   protected Singleton() {    
  9.     // Exists only to defeat instantiation.    
  10.   }    
  11.   public static Singleton getInstance() {    
  12.      if(singleton == null) {    
  13.         simulateRandomActivity();    
  14.         singleton = new Singleton();    
  15.      }    
  16.      logger.info("created singleton: " + singleton);    
  17.      return singleton;    
  18.   }    
  19.   private static void simulateRandomActivity() {    
  20.      try {    
  21.         if(firstThread) {    
  22.            firstThread = false;    
  23.            logger.info("sleeping...");    
  24.      
  25.            // This nap should give the second thread enough time    
  26.            // to get by the first thread.    
  27.              Thread.currentThread().sleep(50);    
  28.        }    
  29.      }    
  30.      catch(InterruptedException ex) {    
  31.         logger.warn("Sleep interrupted");    
  32.      }    
  33.   }    
  34. }   


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

Java代码 复制代码
  1. import org.apache.log4j.Logger;    
  2. import junit.framework.Assert;    
  3. import junit.framework.TestCase;    
  4.      
  5. public class SingletonTest extends TestCase {    
  6.    private static Logger logger = Logger.getRootLogger();    
  7.    private static Singleton singleton = null;    
  8.      
  9.    public SingletonTest(String name) {    
  10.       super(name);    
  11.    }    
  12.    public void setUp() {    
  13.       singleton = null;    
  14.    }    
  15.    public void testUnique() throws InterruptedException {    
  16.       // Both threads call Singleton.getInstance().    
  17.       Thread threadOne = new Thread(new SingletonTestRunnable()),    
  18.              threadTwo = new Thread(new SingletonTestRunnable());    
  19.      
  20.       threadOne.start();    
  21.       threadTwo.start();    
  22.      
  23.       threadOne.join();    
  24.       threadTwo.join();    
  25.    }    
  26.    private static class SingletonTestRunnable implements Runnable {    
  27.       public void run() {    
  28.          // Get a reference to the singleton.    
  29.          Singleton s = Singleton.getInstance();    
  30.      
  31.          // Protect singleton member variable from    
  32.          // multithreaded access.    
  33.          synchronized(SingletonTest.class) {    
  34.             if(singleton == null// If local reference is null...    
  35.                singleton = s;     // ...set it to the singleton    
  36.          }    
  37.          // Local reference must be equal to the one and    
  38.          // only instance of Singleton; otherwise, we have two    
  39.                   // Singleton instances.    
  40.          Assert.assertEquals(true, s == singleton);    
  41.       }    
  42.    }    
  43. }   


例5的测试案例创建两个线程,然后各自启动,等待完成。这个案例保持了一个对单例类的静态引用,每个线程都会调用Singleton.getInstance()。如果这个静态成员变量没有被设置,那么第一个线程就会将它设为通过调用getInstance()而得到的引用,然后这个静态变量会与一个局部变量比较是否相等。
在这个测试案例运行时会发生一系列的事情:第一个线程调用getInstance(),进入if块,然后休眠;接着,第二个线程也调用getInstance()并且创建了一个单例类的实例。第二个线程会设置这个静态成员变量为它所创建的引用。第二个线程检查这个静态成员变量与一个局部备份的相等性。然后测试通过。当第一个线程觉醒时,它也会创建一个单例类的实例,并且它不会设置那个静态成员变量(因为第二个线程已经设置过了),所以那个静态变量与那个局部变量脱离同步,相等性测试即告失败。例6列出了例5的输出:
例6.例5的输出

Java代码 复制代码
  1. Buildfile: build.xml    
  2. init:    
  3.      [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: Singleton@7e5cbd    
  8. INFO Thread-1: created singleton: Singleton@704ebb    
  9. junit.framework.AssertionFailedError: expected: but was:    
  10.    at junit.framework.Assert.fail(Assert.java:47)    
  11.    at junit.framework.Assert.failNotEquals(Assert.java:282)    
  12.    at junit.framework.Assert.assertEquals(Assert.java:64)    
  13.    at junit.framework.Assert.assertEquals(Assert.java:149)    
  14.    at junit.framework.Assert.assertEquals(Assert.java:155)    
  15.    at SingletonTest$SingletonTestRunnable.run(Unknown Source)    
  16.    at java.lang.Thread.run(Thread.java:554)    
  17.      [java] .    
  18.      [java] Time: 0.577    
  19.      
  20.      [java] OK (1 test)   


到现在为止我们已经知道例4不是线程安全的,那就让我们看看如何修正它。


同步

要使例4的单例类为线程安全的很容易----只要像下面一个同步化getInstance()方法:
Java代码 复制代码
  1. public synchronized static Singleton getInstance() {    
  2.    if(singleton == null) {    
  3.       simulateRandomActivity();    
  4.       singleton = new Singleton();    
  5.    }    
  6.    logger.info("created singleton: " + singleton);    
  7.    return singleton;    
  8. }   

在同步化getInstance()方法后,我们就可以得到例5的测试案例返回的下面的结果:
Java代码 复制代码
  1. Buildfile: build.xml    
  2.      
  3. init:    
  4.      [echo] Build 20030414 (14-04-2003 03:15)    
  5.      
  6. compile:    
  7.     [javac] Compiling 2 source files    
  8.      
  9. run-test-text:    
  10. INFO Thread-1: sleeping...    
  11. INFO Thread-1: created singleton: Singleton@ef577d    
  12. INFO Thread-2: created singleton: Singleton@ef577d    
  13.      [java] .    
  14.      [java] Time: 0.513    
  15.      
  16.      [java] OK (1 test)   


这此,这个测试案例工作正常,并且多线程的烦恼也被解决;然而,机敏的读者可能会认识到getInstance()方法只需要在第一次被调用时同步。因为同步的性能开销很昂贵(同步方法比非同步方法能降低到100次左右),或许我们可以引入一种性能改进方法,它只同步单例类的getInstance()方法中的赋值语句。

一种性能改进的方法

寻找一种性能改进方法时,你可能会选择像下面这样重写getInstance()方法:
Java代码 复制代码
  1. public static Singleton getInstance() {    
  2.    if(singleton == null) {    
  3.       synchronized(Singleton.class) {     
  4.          singleton = new Singleton();    
  5.       }    
  6.    }    
  7.    return singleton;    
  8. }   


这个代码片段只同步了关键的代码,而不是同步整个方法。然而这段代码却不是线程安全的。考虑一下下面的假定:线程1进入同步块,并且在它给singleton成员变量赋值之前线程1被切换。接着另一个线程进入if块。第二个线程将等待直到第一个线程完成,并且仍然会得到两个不同的单例类实例。有修复这个问题的方法吗?请读下去。

双重加锁检查

初看上去,双重加锁检查似乎是一种使懒汉式实例化为线程安全的技术。下面的代码片段展示了这种技术:
分享到:
评论
1 楼 david.org 2010-04-24  
楼主的文章没有转全吧.

一种性能的改进方法, 这里总结一下, 它属于并发编程中的竞争条件, 其结果是不可靠的. 这里称为“检查再运行(check-then-act)”. 它的用法是Lazy initialization。

关于双重加锁检查虽然会解决上面线程不安全的问题, 但却极大的浪费了系统资源. 所以比较好的做法是预初使化.

相关推荐

    单例模式详解~~单例模式详解~~

    在Java中,实现单例模式有多种方式,但最常见的问题是线程安全问题。例如,上述代码展示了一个简单的单例实现,称为"饿汉式"单例,因为无论是否需要,它都会在类加载时立即创建实例。这种方式虽然简单,但在多线程...

    java设计模式之单例模式详解

    Java设计模式之单例模式详解 一、单例模式概览 单例模式(Singleton Pattern)是面向对象设计模式中的一种,属于创建型模式。它确保一个类仅有一个实例,并提供一个全局访问点来访问该实例。单例模式的核心在于控制...

    Java设计模式-单例模式详解

    Java设计模式-单例模式详解 单例模式是 Java 设计模式中的一种常用的设计模式,旨在保证一个类仅有一个实例,并提供一个访问它的全局访问点。单例模式的目的是为了保证在一个进程中,某个类有且仅有一个实例。 ...

    java 多线程单例模式详解

    ### Java多线程单例模式详解 #### 一、单例模式概述 单例模式(Singleton Pattern)是一种常用的软件设计模式,它确保一个类仅有一个实例,并提供一个全局访问点。这种模式通常用于那些需要频繁实例化然后销毁的...

    设计模式之单例模式详解.pdf

    设计模式之单例模式详解 单例模式是一种常用的软件设计模式,其定义是单例对象的类只能允许一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。 单例模式的实现主要是...

    Java 单例模式.pptx

    ### Java 单例模式详解 #### 一、什么是单例模式? 单例模式是一种常用的软件设计模式,在这种模式中,一个类只能拥有一个实例,并且该类必须自行创建并提供这个实例。通常,单例模式用于确保某个类在整个应用程序...

    java单例模式详解

    Java单例模式是一种常见的设计模式,它用于控制类的实例化过程,确保一个类在整个应用程序中只有一个实例存在。这种模式通常用于管理共享资源或者需要频繁创建和销毁的对象,以节省系统资源并提供全局访问点。 单例...

    java单例模式详解Java系列2021.pdf

    在软件工程中,单例模式是创建型设计模式之一,其目的是确保一个类仅有一个实例,并提供一个全局访问点。Java语言中的单例模式实现有多种方式,包括饿汉式、懒汉式和登记式单例。不同的实现方式适应不同的场景和需求...

    单例模式详解

    单例模式详解 单例模式是 Java 设计模式中的一种常见模式,旨在保证整个应用中只有一个类的实例存在。单例模式分为三种:懒汉式单例、饿汉式单例、登记式单例。下面对单例模式的特点、实现方式和应用场景进行详细...

    Java双重检查加锁单例模式的详解

    "Java双重检查加锁单例模式的详解" Java双重检查加锁单例模式是一种常用的单例模式实现方法,但是在多线程环境下,它存在一些问题。在这篇文章中,我们将探讨Java双重检查加锁单例模式的详解,包括它的优点和缺点,...

    9种Java单例模式详解(推荐)

    Java单例模式详解 Java单例模式是Java编程中的一种设计模式,旨在保证一个类仅有一个实例,并提供一个全局访问点。下面我们将对9种Java单例模式进行详细的介绍。 首先,单例模式的特点是: 1. 一个类只允许产生...

    java模式设计之单例模式

    【单例模式详解】 单例模式是设计模式中的一种,它的核心思想是限制类的实例化,确保在程序运行过程中,对于某个类,全局只存在一个实例。这种模式常用于资源管理器,如打印服务、数据库连接池等,因为这些资源通常...

    【课堂笔记】单例模式详解

    单例模式是软件设计模式中的一种经典模式,它在Java编程中被广泛使用。这个模式的主要目的是确保一个类只有一个实例,并提供一个全局访问点。这样做的好处在于可以控制资源的共享,减少内存开销,以及简化对全局配置...

    JAVA单例模式的登录案例

    根据给定的信息,我们可以深入探讨Java单例模式的登录案例,并从中提炼出多个重要的知识点。 ### 单例模式概述 单例模式是一种常用的软件设计模式,它保证一个类只有一个实例,并提供一个全局访问点。这种模式通常...

    Java 单例模式线程安全问题

    Java 单例模式线程安全问题详解 Java 单例模式线程安全问题是指在 Java 中实现单例模式时,如何确保线程安全的问题。单例模式是指在整个应用程序生命周期中,只有一个实例存在的设计模式。这种模式可以提高性能,...

    单例模式.ppt

    【单例模式】是一种常用的软件设计模式,其主要目的是确保一个类只有一个实例,并提供一个全局访问点。在软件开发中,单例模式常用于管理共享资源或者需要协调多个组件交互的场景,比如数据库连接池、日志服务等。 ...

    详解JAVA单例模式及多种实现.doc

    以下是几种常见的Java单例模式实现方式: 1. **饿汉式(静态常量)**: 这种方式在类加载时即初始化实例,线程安全,但可能导致不必要的内存占用。 ```java public class Singleton1 { private Singleton1() {} ...

    java 单例模式的实例详解

    java 单例模式的实例详解 java 单例模式是指一种设计模式,确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在java中,单例模式分三种:懒汉式单例、饿汉式单例、登记式单例。单例模式有以下特点...

    单例模式,single

    ### 单例模式详解 #### 概述与应用场景 单例模式是一种常用的设计模式,它的主要目的是确保某个类仅有一个实例,并提供一个全局访问点。这种模式非常适合那些在整个应用程序生命周期中只需要一个实例的对象,例如...

Global site tag (gtag.js) - Google Analytics