2单例模式.docx
《2单例模式.docx》由会员分享,可在线阅读,更多相关《2单例模式.docx(36页珍藏版)》请在冰豆网上搜索。
2单例模式
概要
单例模式是最简单的设计模式之一,但是对于Java的开发者来说,它却有很多缺陷。
在本月的专栏中,DavidGeary探讨了单例模式以及在面对多线程(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.}
publicclassClassicSingleton{
privatestaticClassicSingletoninstance=null;
protectedClassicSingleton(){
//Existsonlytodefeatinstantiation.
}
publicstaticClassicSingletongetInstance(){
if(instance==null){
instance=newClassicSingleton();
}
returninstance;
}
}
在例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.}
publicclassSingletonInstantiator{
publicSingletonInstantiator(){
ClassicSingletoninstance=ClassicSingleton.getInstance();
ClassicSingletonanotherInstance=
newClassicSingleton();
...
}
}
前面这个代码片段为何能在没有继承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.}
importorg.apache.log4j.Logger;
importjunit.framework.Assert;
importjunit.framework.TestCase;
publicclassSingletonTestextendsTestCase{
privateClassicSingletonsone=null,stwo=null;
privatestaticLoggerlogger=Logger.getRootLogger();
publicSingletonTest(Stringname){
super(name);
}
publicvoidsetUp(){
logger.info("gettingsingleton...");
sone=ClassicSingleton.getInstance();
logger.info("...gotsingleton:
"+sone);
logger.info("gettingsingleton...");
stwo=ClassicSingleton.getInstance();
logger.info("...gotsingleton:
"+stwo);
}
publicvoidtestUnique(){
logger.info("checkingsingletonsforequality");
Assert.assertEquals(true,sone==stwo);
}
}
例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:
[b]getting singleton...[/b]
10. [java] INFO main:
[b]created singleton:
[/b] Singleton@e86f41
11. [java] INFO main:
...got singleton:
Singleton@e86f41
12. [java] INFO main:
[b]getting singleton...[/b]
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)
Buildfile:
build.xml
init:
[echo]Build20030414(14-04-200303:
08)
compile:
run-test-text:
[java].INFOmain:
[b]gettingsingleton...[/b]
[java]INFOmain:
[b]createdsingleton:
[/b]Singleton@e86f41
[java]INFOmain:
...gotsingleton:
Singleton@e86f41
[java]INFOmain:
[b]gettingsingleton...[/b]
[java]INFOmain:
...gotsingleton:
Singleton@e86f41
[java]INFOmain:
checkingsingletonsforequality
[java]Time:
0.032
[java]OK(1test)
正如前面的清单所示,例2的简单测试顺利通过----通过ClassicSingleton.getInstance()获得的两个单例类的引用确实相同;然而,你要知道这些引用是在单线程中得到的。
下面的部分着重于用多线程测试单例类。
多线程因素的考虑
在例1中的ClassicSingleton.getInstance()方法由于下面的代码而不是线程安全的:
Java代码
1.1:
if(instance == null) {
2.2:
instance = new Singleton();
3.3:
}
1:
if(instance==null){
2:
instance=newSingleton();
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.}
importorg.apache.log4j.Logger;
publicclassSingleton{
privatestaticSingletonsingleton=null;
privatestaticLoggerlogger=Logger.getRootLogger();
privatestaticbooleanfirstThread=true;
protectedSingleton(){
//Existsonlytodefeatinstantiation.
}
publicstaticSingletongetInstance(){
if(singleton==null){
simulateRandomActivity();
singleton=newSingleton();
}
logger.info("createdsingleton:
"+singleton);
returnsingleton;
}
privatestaticvoidsimulateRandomActivity(){
try{
if(firstThread){
firstThread=false;
logger.info("sleeping...");
//Thisnapshouldgivethesecondthreadenoughtime
//togetbythefirstthread.
Thread.currentThread().sleep(50);
}
}
catch(InterruptedExceptionex){
logger.warn("Sleepinterrupted");
}
}
}
除了在这个清单中的单例类强制使用了一个多线程错误处理,例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 ca