单例模式完全剖析(2)---- 探究简单却又使人迷惑的单例模式

原创 2003年11月13日 00:00:00

测试单例模式

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

例2是一个用JUnit测试例1的单例模式的案例:
例2.一个单例模式的案例
import org.apache.log4j.Logger;
import junit.framework.Assert;
import junit.framework.TestCase;

public class SingletonTest extends TestCase {
private ClassicSingleton sone = null, stwo = null;
private static Logger logger = Logger.getRootLogger();

public SingletonTest(String name) {
super(name);
}
public void setUp() {
logger.info();
sone = ClassicSingleton.getInstance();
logger.info( + sone);

logger.info();
stwo = ClassicSingleton.getInstance();
logger.info( + stwo);
}
public void testUnique() {
logger.info();
Assert.assertEquals(true, sone == stwo);
}
}

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

init:
[echo] Build 20030414 (14-04-2003 03:08)

compile:

run-test-text:
[java] .INFO main: [b]getting singleton...[/b]
[java] INFO main: [b]created singleton:[/b] Singleton@e86f41
[java] INFO main: ...got singleton: Singleton@e86f41
[java] INFO main: [b]getting singleton...[/b]
[java] INFO main: ...got singleton: Singleton@e86f41
[java] INFO main: checking singletons for equality

[java] Time: 0.032

[java] OK (1 test)

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

多线程因素的考虑


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

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

public class Singleton {
private static Singleton singleton = null;
private static Logger logger = Logger.getRootLogger();
private static boolean firstThread = true;

protected Singleton() {

}
public static Singleton getInstance() {
if(singleton == null) {
simulateRandomActivity();
singleton = new Singleton();
}
logger.info( + singleton);
return singleton;
}
private static void simulateRandomActivity() {
try {
if(firstThread) {
firstThread = false;
logger.info();



Thread.currentThread().sleep(50);
}
}
catch(InterruptedException ex) {
logger.warn();
}
}
}

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

public class SingletonTest extends TestCase {
private 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 {

Thread threadOne = new 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() {

Singleton s = Singleton.getInstance();



synchronized(SingletonTest.class) {
if(singleton == null)
singleton = s;
}



Assert.assertEquals(true, s == singleton);
}
}
}

例5的测试案例创建两个线程,然后各自启动,等待完成。这个案例保持了一个对单例类的静态引用,每个线程都会调用Singleton.getInstance()。如果这个静态成员变量没有被设置,那么第一个线程就会将它设为通过调用getInstance()而得到的引用,然后这个静态变量会与一个局部变量比较是否相等。
在这个测试案例运行时会发生一系列的事情:第一个线程调用getInstance(),进入if块,然后休眠;接着,第二个线程也调用getInstance()并且创建了一个单例类的实例。第二个线程会设置这个静态成员变量为它所创建的引用。第二个线程检查这个静态成员变量与一个局部备份的相等性。然后测试通过。当第一个线程觉醒时,它也会创建一个单例类的实例,并且它不会设置那个静态成员变量(因为第二个线程已经设置过了),所以那个静态变量与那个局部变量脱离同步,相等性测试即告失败。例6列出了例5的输出:
例6.例5的输出
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: Singleton@7e5cbd
INFO Thread-1: created singleton: Singleton@704ebb
junit.framework.AssertionFailedError: expected: but was:
at junit.framework.Assert.fail(Assert.java:47)
at junit.framework.Assert.failNotEquals(Assert.java:282)
at junit.framework.Assert.assertEquals(Assert.java:64)
at junit.framework.Assert.assertEquals(Assert.java:149)
at junit.framework.Assert.assertEquals(Assert.java:155)
at SingletonTest$SingletonTestRunnable.run(Unknown Source)
at java.lang.Thread.run(Thread.java:554)
[java] .
[java] Time: 0.577

[java] OK (1 test)

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

同步


要使例4的单例类为线程安全的很容易----只要像下面一个同步化getInstance()方法:
public synchronized static Singleton getInstance() {
if(singleton == null) {
simulateRandomActivity();
singleton = new Singleton();
}
logger.info( + singleton);
return singleton;
}

在同步化getInstance()方法后,我们就可以得到例5的测试案例返回的下面的结果:

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: Singleton@ef577d
INFO Thread-2: created singleton: Singleton@ef577d
[java] .
[java] Time: 0.513

[java] OK (1 test)

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

一种性能改进的方法


寻找一种性能改进方法时,你可能会选择像下面这样重写getInstance()方法:

public static Singleton getInstance() {
if(singleton == null) {
synchronized(Singleton.class) {
singleton = new Singleton();
}
}
return singleton;
}

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

双重加锁检查


初看上去,双重加锁检查似乎是一种使懒汉式实例化为线程安全的技术。下面的代码片段展示了这种技术:
public static Singleton getInstance() {
if(singleton == null) {
synchronized(Singleton.class) {
if(singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}

如果两个线程同时访问getInstance()方法会发生什么?想像一下线程1进行同步块马上又被切换。接着,第二个线程进入if 块。当线程1退出同步块时,线程2会重新检查看是否singleton实例仍然为null。因为线程1设置了singleton成员变量,所以线程2的第二次检查会失败,第二个单例类实例也就不会被创建。似乎就是如此。
不幸的是,双重加锁检查不会保证正常工作,因为编译器会在Singleton的构造方法被调用之前随意给singleton赋一个值。如果在singleton引用被赋值之后而被初始化之前线程1被切换,线程2就会被返回一个对未初始化的单例类实例的引用。

一个改进的线程安全的单例模式实现


例7列出了一个简单、快速而又是线程安全的单例模式实现:
例7.一个简单的单例类
public class Singleton {
public final static Singleton INSTANCE = new Singleton();
private Singleton() {

}
}

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

当然万事并不完美,前面的Singleton只是一个折衷的方案;如果你使用那个实现,你就无法改变它以便后来你可能想要允许多个单例类的实例。用一种更折哀的单例模式实现(通过一个getInstance()方法获得实例)你可以改变这个方法以便返回一个唯一的实例或者是数百个实例中的一个.你不能用一个公开且是静态的(public static)成员变量这样做.

你可以安全的使用例7的单例模式实现或者是例1的带一个同步的getInstance()方法的实现.然而,我们必须要研究另一个问题:你必须在编译期指定这个单例类,这样就不是很灵活.一个单例类的注册表会让我们在运行期指定一个单例类.

未完待续.

最简单的单例模式

如果一个类始终只能创建一个实例,则这个类被称为单例类。   在一些特殊的场景下,要求不允许自由创建该类的对象,而是只允许为该类创建一个对象。为了避免其他类自由创建该类的实例,我们把该类的构造器使...
  • u013700340
  • u013700340
  • 2014年04月03日 20:14
  • 1326

PHP设计模式之单例模式简单代码介绍

PHP设计模式之单例模式 单例模式是一种创建型模式,它会限制应用程序,使其只能创建某一特定类类型的一个单一的实例。举例来说,一个web站点将会需要一个数据库连接对象,但是应该有且只能有一个,因此我们...
  • u012675743
  • u012675743
  • 2015年04月09日 18:10
  • 1383

浅谈常见设计模式--单例模式 简单工厂模式

今题那站在这里和大家一起分享最近在一本书上看到的关于设计模式的内容,接下来要讲的设计模式有: 单例模式 简单工厂模式 工厂方法和抽象工厂 代理模式 命令模式 策略模式 门面模式 桥接模式 观察者模式 ...
  • xikai18827083487
  • xikai18827083487
  • 2016年11月13日 17:04
  • 902

单例模式常用的的两种写法

单例模式很常用,Java中写法各异,哪种更合理高效呢。一般单例都是五种写法。懒汉式,饿汉式,双重校验锁,枚举和静态内部类。什么时候用懒汉式,何时用饿汉式应该清楚。一, 单例模式特点:1)一个类只有一个...
  • dzsw0117
  • dzsw0117
  • 2016年05月07日 09:35
  • 1305

如何写出正确的单例模式

本文转载自http://wuchong.me/blog/2014/08/28/how-to-correctly-write-singleton-pattern/#comments 有关单例模式的文章...
  • Samuel__Liu
  • Samuel__Liu
  • 2016年10月23日 23:07
  • 804

单例模式和工厂模式

http://detail.tmall.com/item.htm?spm=a220m.1000858.1000725.6.lZNUC4&id=18800856374&_u=1nmsbjv7aa5&ar...
  • fangleijiang
  • fangleijiang
  • 2014年02月25日 15:00
  • 9610

单例模式的6种实现方式

为什么使用单例模式需要确保某个类只要一个对象,或创建一个类需要消耗的资源过多,如访问IO和数据库操作等,这时就需要考虑使用单例模式了。使用单例模式需要注意的关键点 将构造函数访问修饰符设置为priva...
  • ts1900
  • ts1900
  • 2016年05月13日 16:03
  • 1549

代理模式,简单(静态)工厂模式,单例模式,模板方法模式个人理解

简言:java中总共有23种设计模式,每个模式的出现都是为了解决某一方面的问题,所以这23种设计模式有他们各自适用的地方(废话有点多),而设计模式的产生主要是为了降低类与类之间的耦合度。下面我们就简单...
  • u013825231
  • u013825231
  • 2016年07月21日 19:53
  • 909

Javascript 设计模式 单例

一直很喜欢Js,,,今天写一个Js的单例模式实现以及用法。 1、单例模式的写法 单例模式写法相当简单: var singleTon = { m1: "memeber ...
  • lmj623565791
  • lmj623565791
  • 2014年06月13日 16:09
  • 16186

为什么要用单例模式?

我们在编程中最常用的模式就是单例模式了,然而单例模式都用在什么场合?为什么不用静态方法而要用单例模式呢?要搞清这些问题,需要从静态方法和非静态方法的区别和联系说起。   一、静态方法常驻内...
  • jiary5201314
  • jiary5201314
  • 2016年09月22日 22:30
  • 1147
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:单例模式完全剖析(2)---- 探究简单却又使人迷惑的单例模式
举报原因:
原因补充:

(最多只允许输入30个字)