副标题#e#
不完全的单例类
什么是不完全的单例类
预计有些读者见过下面这样的“不完全”的单例类。
代码清单10:“不完全”单例类
package com.javapatterns.singleton.demos;
public class LazySingleton
{
private static LazySingleton
m_instance = null;
/**
* 果真的结构子,外界可以直接实例化
*/
public LazySingleton() { }
/**
* 静态工场要领
* @return 返还LazySingleton 类的惟一实例
*/
synchronized public static
LazySingleton getInstance()
{
if (m_instance == null)
{
m_instance = new LazySingleton();
}
return m_instance;
}
}
上面的代码乍看起来是一个“懒汉”式单例类,仔细一看,发明有一个果真的结构子。由于外界可以利用结构子建设出任意多个此类的实例,这违背了单例类只能有一个(或有限个)实例的特性,因此这个类不是完全的单例类。这种环境有时会呈现,好比javax.swing.TimerQueue 即是一例,关于这个类,请拜见《Java与模式》一书中的“调查者模式与Swing 按时器” 一章。
造成这种环境呈现的原因有以下几种大概:
(1) 初学者的错误。很多初学者没有认识到单例类的结构子不能是果真的,因此犯下这个错误。有些初学Java 语言的学员甚至不知道一个Java 类的结构子可以不是果真的。在 这种环境下,设计师大概会通过自我约束,也就是说不去挪用结构子的步伐,将这个不完全的单例类在利用中作为一个单例类利用。
在这种环境下,一个简朴的改正步伐,就是将果真的结构子改为私有的结构子。
(2) 当初出于思量不周,将一个类设计成为单例类,厥后发明此类该当有多于一个的实例。为了补充错误, 爽性将结构子改为果真的,以便在需要多于一个的实例时, 可以随时挪用结构子建设新的实例。要更正这种环境较为坚苦,必需按照详细环境做出改造的抉择。假如一个类在最初被设计成为单例类,但厥后发明实际上此类该当有有限多个实例,这时候该当思量是否将单例类改为多例类(Multiton)。
(3)设计师的Java 常识很好,并且也知道单例模式的正确利用要领,可是照旧有意利用这种不完全的单例模式,因为他意在利用一种“改善”的单例模式。这时候, 撤除共有的结构子不切合单例模式的要求之外,这个类必需是很好的单例模式。
#p#副标题#e#
默认实例模式
有些设计师将这种不完全的单例模式叫做“默认实例模式”(Default Instance Pattern)。在所谓的“ 默认实例模式”内里, 一个类提供静态的要领,如同单例模式一样, 同时又提供一个果真的结构子,如同普通的类一样。
这样做的惟一长处是,这种模式答允客户端选择如何将类实例化:建设新的本身独占的实例,可能利用共享的实例。这样一来,由于没有任何的强制性法子,客户端的选择不必然是公道的选择。其功效是设计师往往不会耗费时间在如何提供最好的选择上,而是不恰内地将这种选择交给客户端的措施员,这样一定会导致不抱负的设计和欠思量的实现。
本文发起读者不要这样做。
相关模式
有一些模式可以利用单例模式,如抽象工场模式可以利用单例模式,将详细工场类设计成单例类;制作模式可以利用单例模式,将详细制作类设计成单例类。
多例(Multiton)模式
正如同本章所说的,单例模式的精力可以推广到多于一个实例的环境。这时候这种类叫做多例类,这种模式叫做多例模式。单例类(左)和多例类(右)的类图如下所示。
关于多例模式,请见《Java与模式》一书中的“专题:多例(Multiton)模式与多语言支持”一章。
简朴工场(Simple Factory)模式
单例模式利用了简朴工场模式(又称为静态工场要领模式)来提供本身的实例。在上面ConfigManager 例子的代码中, 静态工场要领getInstance() 就是静态工场要领。在java.awt.Toolkit 类中,getDefaultToolkit() 要领就是静态工场要领。简朴工场模式的大略类图如下所示。
本章接头了单例模式的布局和实现要领。
单例模式是一个看上去很简朴的模式,许多设计师最先学会的往往是单例模式。然而,跟着Java 系统日益变得巨大化和分手化,单例模式的利用变得比已往坚苦。本书提醒读者在分手式的Java 系统中利用单例模式时,只管不要利用有状态的。
问答题
#p#分页标题#e#
1. 为什么不利用一个静态的“全程”原始变量,而要建一个类?一个静态的原始变量虽然只能有一个值,自然而然不就是“单例”的吗?
2. 举例说明如何挪用EagerSingleton 类。
3. 举例说明如何挪用RegSingleton 类和RegSingletonChild 类。
4. 请问java.lang.Math 类和java.lang.StrictMath 类是否是单例模式?
5. 我们公司只购置了一个JDBC 驱动软件的单用户利用许可,能否利用单例模式打点通过JDBC 驱动软件毗连的数据库?
问答题谜底
1. 单例模式可以提供很巨大的逻辑,而一个原始变量不能自制初始化,不行能有担任的干系,没有内部布局。因此单例模式有许多优越之处。
在Java 语言里并没有真正的“全程”变量,一个变量必需属于某一个类可能某一个实例。而在巨大的措施傍边,一个静态变量的初始化产生在那边经常是一个不易确定的问题。虽然,利用“全程”原始变量并没有什么错误,就仿佛选择利用Fortran 语言而非Java语言编程并不是一种对错的问题一样。
2. 几种单例类的利用要领如下。
代码清单11:几种单例类的利用要领
public class RegSingletonTest
{
public static void main(String[] args)
{
//(1) Test eager
System.out.println( EagerSingleton.getInstance());
//(2) Test reg
System.out.println(
RegSingleton.getInstance(
"com.javapatterns.singleton.demos.RegSingleton").about());
System.out.println( RegSingleton.getInstance(null).about() );
System.out.println(
RegSingleton.getInstance(
"com.javapatterns.singleton.demos.RegSingletonChild").about());
System.out.println( RegSingletonChild.getInstance().about());
}
}
3. 见上题谜底。
4. 它们都不是单例类。原因如下:
这两个类均有一个私有的结构子。可是这仅仅是单例模式的须要条件,而不是充实条件。回首在本章开始提出的单例模式的三个特性可以看出,无论是Math 照旧StrictMath 都没有为外界提供任何自身的实例。实际上,这两个类都是被设计来提供静态工场要领和常量的,因此从来就不需要它们的实例,这才是它们的结构子是私有的原因。Math和StrictMath 类的类图如下所示。
5. 这样做是可行的,只是必需留意当利用在分手式系统中的时候,不必然能担保单例类实例的惟一性。
附录:双重查抄成例的研究
成例是一种代码条理上的模式,是在比设计模式的条理更详细的条理上的代码能力。成例往往与编程语言密切相关。双重查抄成例(Double Check Idiom )是从C 语言移植过来的一种代码模式。在C 语言里,双重查抄成例经常用在多线程情况中类的晚实例化(Late Instantiation)里。
本节之所以要先容这个成例(严格来讲,是先容为什么这个成例不创立), 是因为有许多人认为双重查抄成例可以利用在“懒汉”单例模式内里。
什么是双重查抄成例
为了表明什么是双重查抄成例,请首先看看下面没有利用任何线程安详思量的错误例子。
从单线程的措施谈起
首先思量一个单线程的版本。
代码清单13:没有利用任何线程安详法子的一个例子
// Single threaded version
class Foo
{
private Helper helper = null;
public Helper getHelper()
{
if (helper == null)
{
helper = new Helper();
}
return helper;
}
// other functions and members...
}
这是一个错误的例子,详情请见下面的说明。
写出这样的代码,本意显然是要保持在整个JVM 中只有一个Helper 的实例;因此,才会有if (helper == null) 的查抄。很是明明的是,假如在多线程的情况中运行,上面的代码会有两个甚至两个以上的Helper 工具被建设出来,从而造成错误。
可是,想像一下在多线程情况中的景象就会发明,假如有两个线程A 和B 险些同时达到if (helper == null)语句的外面的话,假设线程A 比线程B 早一点点,那么:
(1)A 会首先进入if (helper == null) 块的内部,并开始执行new Helper() 语句。此时,helper 变量仍然是null,直到线程A 的new Helper() 语句返回并给helper 变量赋值为止。
(2) 可是,线程B 并不会在if (helper == null)语句的外面期待,因为此时helper == null 是创立的,它会马长进入if (helper == null)语句块的内部。这样,线程B 会不行制止地执行helper = new Helper();语句,从而建设出第二个实例来。
#p#分页标题#e#
(3)线程A 的helper = new Helper();语句执行完毕后,helper 变量获得了真实的工具引用,(helper == null)不再为真。第三个线程不会再进入if (helper == null) 语句块的内部了。
(4)线程B 的helper = new Helper(); 语句也执行完毕后,helper 变量的值被包围。可是第一个Helper 工具被线程A 引用的事实不会改变。
这时,线程A 和B 各自拥有一个独立的Helper 工具,而这是错误的。
线程安详的版本
为了降服没有线程安详的缺点,下面给出一个线程安详的例子。
代码清单14:这是一个正确的谜底
// Correct multithreaded version
class Foo
{
private Helper helper = null;
public synchronized Helper getHelper()
{
if (helper == null)
{
helper = new Helper();
return helper;
}
}
// other functions and members...
}
显然,由于整个静态工场要领都是同步化的,因此,不会有两个线程同时进入这个要领。因此,当线程A 和B 作为第一批挪用者同时或险些同时挪用此要领时:
(1)早到一点的线程A 会率先进入此要领,同时线程B 会在要领外部期待。
(2) 对线程A 来说,helper 变量的值是null ,因此helper = new Helper(); 语句会被执行。
(3)线程A 竣事对要领的执行,helper 变量的值不再是null。
(4)线程B 进入此要领,helper 变量的值不再是null ,因此helper = new Helper(); 语句不会被执行。线程B 取到的是helper 变量所含有的引用,也就是对线程A 所创建的Helper 实例的引用。
显然,线程A 和B 持有同一个Helper 实例,这是正确的。
多此一举的“双重查抄”
可是,仔细端详上面的正确谜底会发明,同步化实际上只在helper 变量第一次被赋值之前才有用。在helper 变量有了值今后,同步化实际上酿成了一个不须要的瓶颈。假如能有一个要领去掉这个小小的特别开销,不是越发完美了吗?因此,就有了下面这个设计“巧妙”的双重查抄成例。在读者向下继承读之前,有须要提醒一句:正如本小节的标题所标明的那样,这是一个后面课本,因为双重查抄成例在Java 编译器里无法实现。
代码清单15:利用双重查抄成例的懒汉式单例模式
// Broken multithreaded version
// "Double-Checked Locking" idiom
class Foo
{
private Helper helper = null;
public Helper getHelper()
{
if (helper == null) //第一次查抄(位置1)
{
//这里会有多于一个的线程同时达到 (位置2)
synchronized(this)
{
//这里在每个时刻只能有一个线程 (位置3)
if (helper == null) //第二次查抄 (位置4)
{
helper = new Helper();
}
}
}
return helper;
}
// other functions and members...
}
这是一个错误的例子,详情请见下面的表明。
对付初次打仗双重查抄成例的读者来说,这个能力的思路并不明明易懂。因此,本节在这里给出一个详尽的表明。同样,这里假设线程A 和B 作为第一批挪用者同时或险些同时挪用静态工场要领。
(1) 因为线程A 和B 是第一批挪用者,因此,当它们进入此静态工场要领时,helper 变量是null。因此,线程A 和B 会同时或险些同时达到位置1。
(2)假设线程A 会首先达到位置2,并进入synchronized(this) 达到位置3。这时,由于synchronized(this) 的同步化限制,线程B 无法达到位置3,而只能在位置2 等待。
(3)线程A 执行helper = new Helper() 语句,使得helper 变量获得一个值,即对一个Helper 工具的引用。此时,线程B 只能继承在位置2 等待。
(4)线程A 退出synchronized(this) ,返回Helper 工具,退出静态工场要领。
(5)线程B 进入synchronized(this) 块,到达位置3,进而到达位置4。由于helper 变量已经不是null 了,因此线程B 退出synchronized(this),返回helper 所引用的Helper 工具(也就是线程A 所建设的Helper 工具),退出静态工场要领。
到此为止,线程A 和线程B 获得了同一个Helper 工具。可以看到,在上面的要领
getInstance() 中,同步化仅用来制止多个线程同时初始化这个类,而不是同时挪用这个静态工场要领。假如这是正确的,那么利用这一个成例之后,“ 懒汉式”单例类就可以挣脱掉同步化瓶颈,到达一个很妙的地步。
代码清单16:利用了双重查抄成例的懒汉式单例类
#p#分页标题#e#
public class LazySingleton
{
private static LazySingleton m_instance = null;
private LazySingleton() { }
/**
* 静态工场要领
*/
public static LazySingleton getInstance()
{
if (m_instance == null)
{
//More than one threads might be here!!!
synchronized(LazySingleton.class)
{
if (m_instance == null)
{
m_instance = new LazySingleton();
}
}
}
return m_instance;
}
}
这是一个错误的例子,请见下面的表明。
第一次打仗到这个能力的读者肯定会有许多问题,诸如第一次查抄可能第二次查抄可不行以省掉等。答复是:凭据多线程的道理和双重查抄成例的预想方案,它们是不行以省掉的。本节不规划讲授的原因在于双重查抄成例在Java 编译器中基础不能创立。
双重查抄成例对Java 语言编译器不创立
令人受惊的是,在C 语言里获得普遍应用的双重查抄成例在大都的Java 语言编译器内里并不创立[BLOCH01, GOETZ01, DCL01] 。上面利用了双重查抄成例的“懒汉式”单例类,不能事情的根基原因在于,在Java 编译器中,LazySingleton 类的初始化与m_instance 变量赋值的顺序不行预料。假如一个线程在没有同步化的条件下读取m_instance 引用,并挪用这个工具的要领的话,大概会发明工具的初始化进程尚未完成,从而造成瓦解。
文献[BLOCH01] 指出:一般而言,双重查抄创立对Java 语言来说是不创立的。
给读者的一点发起
有许多很是智慧的人在这个成例的Java 版本上耗费了很是多的时间,到此刻为止人们得出的结论是:一般而言,双重查抄成例无法在现有的Java 语言编译器里事情[BLOCH01, GOETZ01, DCL01] 。
读者大概会问,是否有大概通过某种能力对上面的双重查抄的实现代码加以修改,从而使某种形式的双重查抄成例能在Java 编译器下事情呢?这种大概性虽然不能解除,可是除非读者对此有出格的乐趣,发起不要在这上面耗费太多的时间。
在一般环境下利用饿汉式单例模式可能对整个静态工场要领同步化的懒汉式单例模式足以办理在实际设计事情中碰着的问题。