单件模式的陷阱
看过很多单件模式的文章,书上有,网上更多一些。一般来说,只有如何实现单件模式,而没有介绍具体情况单件模式的使用,也没有介绍过单件模式会出现问题。单件模式似乎不会产生逻辑上的问题。但是,这仅仅是似乎。
在描述我遇到的问题之前,先讲讲我对其原理的理解。
首先单件模式是自我创建的一个对象,并且在运行期始终保持只有唯一的对象。抛开什么东西能够自我创建不说,保持唯一对象要怎么理解呢?先看看一个普通的类:
public class SimpleClass {
}
对其进行单元测试:
SimpleClass s4 = new SimpleClass();
Assert.assertNotSame(s3, s4);
该测试总是正确的,s3和s4虽然是同一个类型,但是不是同一个东西。SimpleClass可以是苹果,是一种东西,而s3、s4是具体的一个苹果。世界上没有哪两片树叶是完全一样的,但是他们都是树叶。
就.net、java这种单根继承,具有垃圾回收机制的平台而言,s3和s4是被放到heap的两个地方的。可以用图1来描述。s3和s4都有其独立性。
图1
而单件模式产生的结果是什么呢?单件模式就是相当于这里的s3和s4都指向了001144这个地址。其结果是他们是同一个类型,同时也是同一个东西。可以理解为,大熊猫快要灭绝了(要保护大熊猫,嘿嘿,贫道喜欢大熊猫),世界上就一个大熊猫了。于是,大家都去看望它。而每个人眼里的大熊猫都是一个,拍出来的照片都是记录的同一个大熊猫。
单件模式基本代码为:
/**
* @author Birdshover
* @url http://birdshover.cnblogs.com
*/
public class SingletonClass {
private SingletonClass(){
}
private static SingletonClass instance;
/**
* @return
*/
public static SingletonClass getInstance(){
if(instance == null){
synchronized(SingletonClass.class){
if(instance == null){
instance = new SingletonClass();
}
}
}
return instance;
}
}
SingletonClass s2 = SingletonClass.getInstance();
Assert.assertSame(s1, s2);
结果符合预期。
现在的问题是,无论是谁拿到了SingletonClass的实例都是同一个东西,那么SingletonClass相当于是被静态化了。
假如我现在有个类A,而A有静态字段B。
class A{
static B b = new B();
}
class B{
private int f;
public void W(){ f++; }
}
我就假定,其它地方没有对b有赋值的操作,那么b在系统中也是一个单件,当然它不是单件模式。而A对象的数目是不定的,因此,A的N多实例对b的方法W的范围虽然安全,但是如果b里面含有字段f,而W对f有赋值操作,是不是有问题了?
也就是说,B如果有状态,那么就会造成麻烦。试想一下,现在有张画是黑的,有2位画家正准备改变其颜色。如果2位画家排队来操作,那么,但第一位画家操作完后,他可以告诉大家,现在画是蓝的。而第二位画家修改完后可以说画是红的。那如果两位画家一起画,第一位画完了,而第二位正在画。第一位画家宣布画是蓝的的时候,大家看到画是蓝色和红色的,两种颜色都有。这就是Singleton模式的陷阱。为了表现这个陷阱,修改了SingletonClass代码:
/**
* @author Birdshover
* @url http://birdshover.cnblogs.com
*/
public class SingletonClass {
private SingletonClass(){
}
private static SingletonClass instance;
/**
* @return
*/
public static SingletonClass getInstance(){
if(instance == null){
synchronized(SingletonClass.class){
if(instance == null){
instance = new SingletonClass();
}
}
}
return instance;
}
private int value;
public void Raise(){
for(int i = 0;i < 10;i++){
value++;
System.out.println("Thread:" + Thread.currentThread().getId());
}
}
/**
* @return
*/
public int getValue(){
return value;
}
/**
* @param value the value to set
*/
public void setValue(int value) {
this.value = value;
}
}
再准备好多线程测试的单元测试代码:
import java.util.Random;
import singleton.SimpleClass;
import singleton.SingletonClass;
import junit.framework.Assert;
import junit.framework.TestCase;
public class SingletonClassTest extends TestCase {
static final Random r =new Random();
public void testThread() throws InterruptedException{
for(int i = 0;i < 100;i++){
Thread thread = new Thread(new MyThread());
thread.start();
}
Thread.currentThread().sleep(2000);
SingletonClass s1 = SingletonClass.getInstance();
System.out.println(s1.getValue());
}
private class MyThread implements Runnable
{
public void run() {
SingletonClass s1 = SingletonClass.getInstance();
s1.Raise();
System.out.println("value:" + s1.getValue());
}
}
}
测试完成后发生了什么?我摘录上一组数据的几部分片段:
Thread:8
Thread:8
Thread:8
Thread:8
Thread:8
Thread:8
Thread:8
Thread:8
Thread:8
Thread:8
value:10
这部分结果表明,有时候只有一个线程在运行(我的是单核CPU),值是正确的。
Thread:12
Thread:13
Thread:14
Thread:15
Thread:16
Thread:17
Thread:18
Thread:19
Thread:20
Thread:22
Thread:21
Thread:23
Thread:24
这部分结果表明,线程执行顺序开始混乱了。
value:1000
value:1000
value:1000
value:1000
value:1000
这部分结果表明,后面几个线程拿到的数据是一样的,有可能会造成预期上的偏差,从而产生逻辑上的错误。
话说到这里就讲出了单件模式的陷阱。我又去网上随便翻阅了一下,大多数文章都是点在了如果构建单件模式,没有讲网站的实例。而TerryLee的文章讲到了一个完成示例,但是没有描述可能会遇到这个问题。