圣誕節(jié)到了述吸,是時(shí)候?qū)卫幸粋€(gè)新的認(rèn)識(shí)了,不然一個(gè)就會(huì)變成兩個(gè)锣笨、四個(gè)...很多個(gè)...嗯蝌矛,我說(shuō)的是圣誕老人...
很久之前看到一篇講單例的文章,看完才知道看似簡(jiǎn)單的單例模式错英,其實(shí)有很大的考究入撒,最近又看到了幾篇類似的文章,發(fā)現(xiàn)單例其實(shí)很復(fù)雜椭岩。費(fèi)了很大力氣茅逮,理順了思路,頓時(shí)又覺(jué)得單例模式可以不用那么復(fù)雜了判哥。
首先献雅,我們得問(wèn)自己一個(gè)問(wèn)題:為什么要使用單例?
為什么要使用單例
單例塌计,顧名思義挺身,就是讓一個(gè)類只存在一個(gè)實(shí)例對(duì)象,那么什么時(shí)候我們會(huì)需要單例呢锌仅?最常見(jiàn)的有以下兩種情形:
- 無(wú)狀態(tài)的工具類:比如日志工具類章钾,不管是在哪里使用,我們需要的只是它幫我們記錄日志信息热芹,除此之外,并不需要在它的實(shí)例對(duì)象上存儲(chǔ)任何狀態(tài)窍箍,這時(shí)候我們就只需要一個(gè)實(shí)例對(duì)象即可。
- 全局信息類:比如我們?cè)谝粋€(gè)類上記錄網(wǎng)站的訪問(wèn)次數(shù)椰棘,我們不希望有的訪問(wèn)被記錄在對(duì)象A上纺棺,有的卻記錄在對(duì)象B上,這時(shí)候我們就讓這個(gè)類成為單例祷蝌。
單例起到的好處主要有兩點(diǎn):
- 節(jié)省內(nèi)存
- 方便管理
值得注意的是帆卓,單例往往都可以通過(guò)static來(lái)實(shí)現(xiàn)巨朦,把一個(gè)實(shí)例方法變成靜態(tài)方法,或者把一個(gè)實(shí)例變量變成靜態(tài)變量剑令,都可以起到單例的效果。在我看來(lái)吁津,這只是面向?qū)ο蠛兔嫦蜻^(guò)程的區(qū)別。
一個(gè)完美的懶漢模式
了解完為什么要使用單例梭依,接下來(lái)讓我們來(lái)實(shí)現(xiàn)一個(gè)完美的單例模式典尾。
實(shí)現(xiàn)單例模式,你只需要注意以下幾點(diǎn):
- 將構(gòu)造函數(shù)私有化河闰,防止別的開(kāi)發(fā)人員調(diào)用而創(chuàng)建出多個(gè)實(shí)例
- 在類的內(nèi)部創(chuàng)建實(shí)例勃教,創(chuàng)建時(shí)要注意多線程并發(fā)訪問(wèn)可能導(dǎo)致的new出多個(gè)實(shí)例的問(wèn)題
- 提供獲取唯一實(shí)例的方法
基于以上三點(diǎn)故源,我們實(shí)現(xiàn)了下面這個(gè)“懶漢”單例模式(本文的所有代碼汞贸,可到Github上下載):
public class PerfectLazyManSingleton {
private volatile static PerfectLazyManSingleton instance = null;
private PerfectLazyManSingleton() {
}
public static PerfectLazyManSingleton getInstance() {
if(instance == null) {
synchronized (PerfectLazyManSingleton.class) {
if(instance == null) {
instance = new PerfectLazyManSingleton();
}
}
}
return instance;
}
}
這個(gè)單例在實(shí)際使用中已經(jīng)是完美的了:
- 使用私有構(gòu)造函數(shù)防止new出多個(gè)實(shí)例
- 使用Double-Check + synchronized同步鎖矢腻,解決多線程并發(fā)訪問(wèn)可能導(dǎo)致的在內(nèi)部調(diào)用多次new的問(wèn)題
- 使用volatile關(guān)鍵字,解決由于指令重排而可能出現(xiàn)的在內(nèi)部調(diào)用多次new的問(wèn)題
至于很多文章里說(shuō)的利用類加載器奶是、利用反射等創(chuàng)建多個(gè)實(shí)例的問(wèn)題,我們只需要知道有這個(gè)可能性就好聂沙,因?yàn)檫@些都不是正常創(chuàng)建對(duì)象的方式,我們使用單例模式是為了防止其他開(kāi)發(fā)人員不小心new出多個(gè)實(shí)例沮趣,而如果開(kāi)發(fā)人員都動(dòng)用了反射和ClassLoader這些重型武器了房铭,那我想這絕對(duì)不是“不小心”了温眉。
與其浪費(fèi)心思、犧牲代碼可讀性豪嗽、犧牲性能豌骏,去獲取“絕對(duì)意義”上的單例,還不如在類上面加上行注釋——“This is a single-instance class. Do not try to create another instance”计贰,來(lái)提示那些看到私有構(gòu)造函數(shù)還不知道這是個(gè)單例的新手們蒂窒,不要嘗試創(chuàng)建新的實(shí)例了洒琢!
如果真想實(shí)現(xiàn)“絕對(duì)意義”上的單例,那就使用枚舉吧象迎。
單例工廠
消除重復(fù)是程序員的天性呛踊,如果我們每次需要單例對(duì)象時(shí),都按照上面的模式把類設(shè)計(jì)成單例谭网,那顯然是不可接受的愉择。這時(shí)候我們就可以設(shè)計(jì)一個(gè)單例工廠织中,這個(gè)單例工廠就像民政局一樣衷戈,我給他一個(gè)身份證號(hào)碼脱惰,他給我返回唯一一個(gè)對(duì)應(yīng)的人。
public class SingletonRegistry {
public static SingletonRegistry REGISTRY = new SingletonRegistry();
private static HashMap map = new HashMap();
private static Logger logger = LoggerFactory.getLogger(SingletonRegistry.class);
private SingletonRegistry() {
}
public static synchronized Object getInstance(String classname) {
Object singleton = map.get(classname);
if (singleton != null) {
return singleton;
}
try {
singleton = Class.forName(classname).newInstance();
logger.info("created singleton: " + singleton);
} catch (ClassNotFoundException cnf) {
logger.warn("Couldn't find class " + classname);
} catch (InstantiationException ie) {
logger.warn("Couldn't instantiate an object of type " +
classname);
} catch (IllegalAccessException ia) {
logger.warn("Couldn't access class " + classname);
}
map.put(classname, singleton);
return singleton;
}
}
關(guān)于這個(gè)SingletonRegistry采盒,有以下幾點(diǎn)需要注意的:
- 這個(gè)SingletonRegistry本身也是單例磅氨,使用的是“餓漢”版的單例模式
- 由于getInstance方法要返回的實(shí)例不再是類的成員變量嫡纠,因此不再能夠使用volatile來(lái)獲得線程之間的可見(jiàn)性,因此要將整個(gè)getInstance方法加上同步鎖
這個(gè)單例工廠的用法非常簡(jiǎn)單:
public class Singleton {
private Singleton() {
}
public static Singleton getInstance() {
return (Singleton)SingletonRegistry.REGISTRY.getInstance(classname);
}
}
餓漢版單例模式
餓漢版的單例模式非常簡(jiǎn)單叉橱,上面的SingletonRegistry其實(shí)就是“餓漢”版的單例模式窃祝,一個(gè)完美的餓漢單例模式代碼如下:
public class SingletonHungryMan {
public final static SingletonHungryMan INSTANCE = new SingletonHungryMan();
private SingletonHungryMan() {
// Exists only to defeat instantiation.
}
public void sayHello() {
System.out.println("hello");
}
}
為什么這里就不用擔(dān)心多線程并發(fā)導(dǎo)致的new了多個(gè)示例呢踱侣?
關(guān)鍵在于這是static靜態(tài)變量抡句,而靜態(tài)變量歸屬于類,會(huì)在類加載的過(guò)程中被初始化逞壁,而Java類加載的過(guò)程默認(rèn)是線程安全的究抓,除非自定義的類加載器覆寫(xiě)了loadClass函數(shù)袭灯。
下面就是ClassLoader的loadClass方法稽荧,這個(gè)方法很好的展示了什么是雙親委派模型:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
當(dāng)然了,餓漢版的單例模式如果受到非常規(guī)的攻擊擅腰,還是會(huì)生出二胎出來(lái)的翁潘,比如利用反射把私有的構(gòu)造器設(shè)為Accessible,抑或是使用自定義的類加載器進(jìn)行加載渗勘,產(chǎn)生新的實(shí)例俩莽。
對(duì)于任意一個(gè)類扮超,都需要由加載它的類加載器和這個(gè)類本身一同確立其在Java虛擬機(jī)中的唯一性 —— 《深入理解Java虛擬機(jī)》 第7章 虛擬機(jī)類加載機(jī)制
我分別使用了反射和類加載器,對(duì)上面的SingletonHungryMan進(jìn)行了攻擊璧疗,代碼如下:
public class SingletonHungryManTest {
private SingletonHungryMan sone = null;
private Object stwo = null;
private Object sthree = null;
private static Logger logger = LoggerFactory.getLogger(SingletonHungryManTest.class);
@Before
public void setUp() throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
sone = SingletonHungryMan.INSTANCE;
stwo = createAnotherInstanceUsingRelection();
sthree = createAnotherInstanceUsingAnotherClassLoader();
}
private Object createAnotherInstanceUsingRelection() throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
Class<SingletonHungryMan> singletonHungryManClass = SingletonHungryMan.class;
Constructor<?> declaredConstructor = singletonHungryManClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
return declaredConstructor.newInstance();
}
private Object createAnotherInstanceUsingAnotherClassLoader() throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchFieldException {
// use custom class loader to load class
ClassLoader myLoader = getMyLoader();
Class<?> myClass = myLoader.loadClass("com.sexycode.codepractice.singleton.SingletonHungryMan");
// use reflection to get field
Field field = myClass.getField("INSTANCE");
// return the field's value
return field.get(null);
}
private ClassLoader getMyLoader() throws ClassNotFoundException {
return new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
}
@Test
public void testUnique() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
logger.info("checking singletons for equality");
sone.sayHello();
invokeMethod(stwo, "sayHello");
invokeMethod(sthree, "sayHello");
Assert.assertNotEquals(true, sone == stwo);
Assert.assertNotEquals(true, sone == sthree);
}
private void invokeMethod(Object obj, String method) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
Method sayHello = obj.getClass().getMethod(method);
sayHello.invoke(obj);
}
}
可序列化對(duì)象的單例
可序列化對(duì)象,在進(jìn)行序列化之后啦膜,可以進(jìn)行多次的反序列化淌喻,這時(shí)候如果要維持單例,就要實(shí)現(xiàn)readResolve方法:
public class SingletonSerializable implements java.io.Serializable {
public static SingletonSerializable INSTANCE = new SingletonSerializable();
private SingletonSerializable() {
// Exists only to thwart instantiation.
}
private Object readResolve() {
return INSTANCE;
}
}
小結(jié)
實(shí)現(xiàn)單例模式八拱,其實(shí)沒(méi)有那么復(fù)雜肌稻,我們要考慮的只是如何防止其他開(kāi)發(fā)人員在常規(guī)操作下創(chuàng)建多個(gè)實(shí)例匕荸,至于那些非常規(guī)的手段榛搔,并不值得犧牲代碼可讀性和性能去進(jìn)行防御东揣。
最后再拋出一個(gè)問(wèn)題腹泌,Spring的@Scope("singleton")是怎么實(shí)現(xiàn)單例的呢?
最最重要的是芥吟,圣誕節(jié)來(lái)了专甩,你知道怎么實(shí)現(xiàn)單例配深、防止多例了么?