Java單例模式探究作為對象的創(chuàng)建模式[GOF95],單例模式確保某個類只有一個實例,而且自行實例化并向整個系統(tǒng)提供這個實例。這個類稱為單例類。由定義可以總結(jié)出單例模式的要點有三個:一是單例類只能有一個實例;二是它必須自行創(chuàng)建這個實例;三是它必須自行向整個系統(tǒng)提供這個實例。 在計算機系統(tǒng)中,線程池、緩存、日志對象、對話框、打印機、顯卡的驅(qū)動程序?qū)ο蟪1辉O(shè)計成單例。這些應(yīng)用都或多或少具有資源管理器的功能。每臺計算機可以有若干個打印機,但只能有一個Printer Spooler,以避免兩個打印作業(yè)同時輸出到打印機中。每臺計算機可以有若干通信端口,系統(tǒng)應(yīng)當集中管理這些通信端口,以避免一個通信端口同時被兩個請求同時調(diào)用??傊x擇單例模式就是為了避免不一致狀態(tài),避免政出多頭。 雖然從類圖上看,單例模式是最簡單的設(shè)計模式之一,但是真正正確地使用單例模式卻不是那么簡單的事。
首先看一個經(jīng)典的單例實現(xiàn)。 public class Singleton { private static Singleton uniqueInstance = null;
private Singleton() { // Exists only to defeat instantiation. }
public static Singleton getInstance() { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } return uniqueInstance; } // Other methods... } Singleton通過將構(gòu)造方法限定為private避免了類在外部被實例化,在同一個虛擬機范圍內(nèi),Singleton的唯一實例只能通過getInstance()方法訪問。(事實上,通過Java反射機制是能夠?qū)嵗瘶?gòu)造方法為private的類的,那基本上會使所有的Java單例實現(xiàn)失效。此問題在此處不做討論,姑且掩耳盜鈴地認為反射機制不存在。) 但是以上實現(xiàn)沒有考慮線程安全問題。所謂線程安全是指:如果你的代碼所在的進程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。如果每次運行結(jié)果和單線程運行的結(jié)果是一樣的,而且其他的變量的值也和預(yù)期的是一樣的,就是線程安全的?;蛘哒f:一個類或者程序所提供的接口對于線程來說是原子操作或者多個線程之間的切換不會導(dǎo)致該接口的執(zhí)行結(jié)果存在二義性,也就是說我們不用考慮同步的問題。顯然以上實現(xiàn)并不滿足線程安全的要求,在并發(fā)環(huán)境下很可能出現(xiàn)多個Singleton實例。 有很多種方法可以實現(xiàn)線程安全的單例模式,下面逐一介紹: 1. 一步到位的餓漢單例類 餓漢式單例類是在Java 語言里實現(xiàn)得最為簡便的單例類。在類被加載時,就會將自己實例化。 public class Singleton { private static Singleton uniqueInstance = new Singleton();
private Singleton() { // Exists only to defeat instantiation. }
public static Singleton getInstance() { return uniqueInstance; } // other methods... } 2. 改造經(jīng)典模式 首先是最簡單最直接的改造。 public class Singleton { private static Singleton uniqueInstance = null;
private Singleton() { // Exists only to defeat instantiation. }
public synchronized static Singleton getInstance() { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } return uniqueInstance; } //Other methods... } 通過synchronized關(guān)鍵字,同步了不同線程對getInstance()的訪問。這就是所謂的懶漢模式。與餓漢式單例類不同的是,懶漢式單例類在第一次被引用時將自己實例化。這種簡單實現(xiàn)的問題在于,每次訪問getInstance()都需要同步操作,而事實上同步只在第一次訪問時有意義。為了避免不必要的同步操作,在JDK1.5以后可以使用一種雙重檢查加鎖的方法。 public class Singleton { // volatile is very important for uniqueInstance consistency. private volatile static Singleton uniqueInstance = null;
private Singleton() { // Exists only to defeat instantiation. }
public static Singleton getInstance() { // first check no need to synchronize. if (uniqueInstance == null) { // second check need to synchronize, but only run limit times. synchronized (Singleton.class) { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } } } return uniqueInstance; } // Other methods... }
volatile確保uniqueInstance被初始化為單例后的改變對所有線程可見,多線程能夠正確處理uniqueInstance變量。getInstance()中包含兩次判空操作,第一次判空每次訪問都會執(zhí)行,而第二次判空只在初始訪問存在大量并發(fā)的情況下出現(xiàn)。通過兩次判空避免了不必要的線程同步。之所以限制必須在JDK1.5后使用是因為,之前的Java存儲模型不能保證volatile語義的完全正確實現(xiàn)。為了突破這種限制《Effective Java》中給出了一種精妙的解決方法,充分利用了Java虛擬機的特性。 public class Singleton { // an inner class holder the uniqueInstance. private static class SingletonHolder { static final Singleton uniqueInstance = new Singleton(); }
private Singleton() { // Exists only to defeat instantiation. }
public static Singleton getInstance() { return SingletonHolder.uniqueInstance; } // Other methods... } When the getInstance method is invoked for the first time, it reads SingletonHolder.uniqueInstance for the first time, causing the SingletonHolder class to get initialized.The beauty of this idiom is that the getInstance method is not synchronized and performs only a field access, so lazy initialization adds practically nothing to the cost of access. A modern VM will synchronize field access only to initialize the class.Once the class is initialized, the VM will patch the code so that subsequent access to the field does not involve any testing or synchronization. 3. 登記式單例類 登記式單例類是GoF 為了克服餓漢式單例類及懶漢式單例類均不可繼承的缺點而設(shè)計的。 public class RegSingleton { static private HashMap m_registry = new HashMap(); static { RegSingleton x = new RegSingleton(); m_registry.put(x.getClass().getName(), x); }
protected RegSingleton() { }
public static RegSingleton getInstance(String name) { if (name == null) { name = "com.javapatterns.singleton.demos.RegSingleton"; } if (m_registry.get(name) == null) { try { m_registry.put(name, Class.forName(name).newInstance()); } catch (ClassNotFoundException cnf) { System.out.println("Couldn't find class " + name); } catch (InstantiationException ie) { System.out.println("Couldn't instantiate an object of type "+ name); } catch (IllegalAccessException ia) { System.out.println("Couldn't access class " + name); } } return (RegSingleton) (m_registry.get(name)); } } // sub-class implements RegSingleton. public class RegSingletonChild extends RegSingleton { public RegSingletonChild() { }
static public RegSingletonChild getInstance() { return (RegSingletonChild) RegSingleton .getInstance("com.javapatterns.singleton.demos.RegSingletonChild"); }
public String about() { return "Hello, I am RegSingletonChild."; } } 在GoF 原始的例子中,并沒有getInstance() 方法,這樣得到子類必須調(diào)用的getInstance(String name)方法并傳入子類的名字,因此很不方便。加入getInstance() 方法的好處是RegSingletonChild 可以通過這個方法,返還自已的實例。而這樣做的缺點是,由于數(shù)據(jù)類型不同,無法在RegSingleton 提供這樣一個方法。由于子類必須允許父類以構(gòu)造子調(diào)用產(chǎn)生實例,因此,它的構(gòu)造子必須是公開的。這樣一來,就等于允許了以這樣方式產(chǎn)生實例而不在父類的登記中。這是登記式單例類的一個缺點。GoF 曾指出,由于父類的實例必須存在才可能有子類的實例,這在有些情況下是一個浪費。這是登記式單例類的另一個缺點。
現(xiàn)在我們已經(jīng)知道如何實現(xiàn)線程安全的單例類和如何使用一個注冊表去在運行期指定單例類名,接著讓我們考查一下如何安排類載入器、處理序列化以及單例模式與ThreadLocal的關(guān)系。 l Classloaders 在許多情況下,使用多個類載入器是很普遍的--包括servlet容器--所以不管你在實現(xiàn)你的單例類時是多么小心你都最終可以得到多個單例類的實例。如果你想要確保你的單例類只被同一個的類載入器裝入,那你就必須自己指定這個類載入器;例如: private static Class getClass(String classname) throws ClassNotFoundException { ClassLoader classLoader = Thread.currentThread() .getContextClassLoader();
if (classLoader == null) classLoader = Singleton.class.getClassLoader();
return (classLoader.loadClass(classname)); } 這個方法會嘗試把當前的線程與那個類載入器相關(guān)聯(lián);如果classloader為null,這個方法會使用與裝入單例類基類的那個類載入器。這個方法可以用Class.forName()代替。 l 序列化 如果你序列化一個單例類,然后兩次重構(gòu)它,那么你就會得到那個單例類的兩個實例,除非你實現(xiàn)readResolve()方法,像下面這樣: public class Singleton implements java.io.Serializable {
public static Singleton INSTANCE = new Singleton();
protected Singleton() { // Exists only to thwart instantiation. }
private Object readResolve() { return INSTANCE; } } 上面的單例類實現(xiàn)從readResolve()方法中返回一個唯一的實例;這樣無論Singleton類何時被重構(gòu),它都只會返回那個相同的單例類實例。無論是singleton,或是其他實例受控(instance-controlled)的類,必須使用readResolve方法來保護“實例-控制的約束”。從本質(zhì)上來講,readResovle方法把一個readObject方法從一個事實上的公有構(gòu)造函數(shù)變成一個事實上的公有靜態(tài)工廠。對于那些禁止包外繼承的類而言,readResolve方法作為保護性的readObject方法的一種替代,也是非常有用的。 l ThreadLocal 在利用Hibernate開發(fā)DAO模塊時,我們和Session打的交道最多,所以如何合理的管理Session,避免Session的頻繁創(chuàng)建和銷毀,對于提高系統(tǒng)的性能來說是非常重要的,以下代碼實現(xiàn)了Session管理功能。 import org.hibernate.HibernateException; import org.hibernate.Session; import org.hibernate.cfg.Configuration;
public class HibernateSessionFactory { private static String CONFIG_FILE_LOCATION = "/hibernate.cfg.xml"; private static final ThreadLocal threadLocal = new ThreadLocal(); private static Configuration configuration = new Configuration(); private static org.hibernate.SessionFactory sessionFactory; private static String configFile = CONFIG_FILE_LOCATION;
static { try { configuration.configure(configFile); sessionFactory = configuration.buildSessionFactory(); } catch (Exception e) { System.err.println("%%%% Error Creating SessionFactory %%%%"); e.printStackTrace(); } }
private HibernateSessionFactory() { }
public static Session getSession() throws HibernateException { Session session = (Session) threadLocal.get();
if (session == null || !session.isOpen()) { if (sessionFactory == null) { rebuildSessionFactory(); } session = (sessionFactory != null) ? sessionFactory.openSession() : null; threadLocal.set(session); }
return session; } // Other methods... } 我們知道Session是由SessionFactory負責創(chuàng)建的,而SessionFactory的實現(xiàn)是線程安全的,采用前面提到的“餓漢模式”創(chuàng)建單例。多個并發(fā)的線程可以同時訪問一個SessionFactory并從中獲取Session實例,那么Session是否是線程安全的呢?很遺憾,答案是否定的。Session中包含了數(shù)據(jù)庫操作相關(guān)的狀態(tài)信息,那么說如果多個線程同時使用一個Session實例進行CRUD,就很有可能導(dǎo)致數(shù)據(jù)存取的混亂,你能夠想像那些你根本不能預(yù)測執(zhí)行順序的線程對你的一條記錄進行操作的情形嗎?以上代碼使用ThreadLocal模式的解決了這一問題。 只要借助上面的工具類獲取Session實例,我們就可以實現(xiàn)線程范圍內(nèi)的Session共享,從而避免了線程中頻繁的創(chuàng)建和銷毀Session實例。當然,不要忘記在用完后關(guān)閉Session。 ThreadLocal和線程同步機制相比有什么優(yōu)勢呢? ThreadLocal和線程同步機制都是為了解決多線程中相同變量的訪問沖突問題。在同步機制中,通過對象的鎖機制保證同一時間只有一個線程訪問變量。這時該變量是多個線程共享的,使用同步機制要求程序慎密地分析什么時候?qū)ψ兞窟M行讀寫,什么時候需要鎖定某個對象,什么時候釋放對象鎖等繁雜的問題,程序設(shè)計和編寫難度相對較大。而ThreadLocal則從另一個角度來解決多線程的并發(fā)訪問。ThreadLocal會為每一個線程提供一個獨立的變量副本,從而隔離了多個線程對數(shù)據(jù)的訪問沖突。因為每一個線程都擁有自己的變量副本,從而也就沒有必要對該變量進行同步了。ThreadLocal提供了線程安全的共享對象,在編寫多線程代碼時,可以把不安全的變量封裝進ThreadLocal。
由于ThreadLocal中可以持有任何類型的對象,低版本JDK所提供的get()返回的是Object對象,需要強制類型轉(zhuǎn)換。但JDK 5.0通過泛型很好的解決了這個問題,在一定程度地簡化ThreadLocal的使用。 概括起來說,對于多線程資源共享的問題,同步機制采用了“以時間換空間”的方式,而ThreadLocal采用了“以空間換時間”的方式。前者僅提供一份變量,讓不同的線程排隊訪問,而后者為每一個線程都提供了一份變量,因此可以同時訪問而互不影響。 ThreadLocal是解決線程安全問題一個很好的思路,它通過為每個線程提供一個獨立的變量副本解決了變量并發(fā)訪問的沖突問題。在很多情況下,ThreadLocal比直接使用synchronized同步機制解決線程安全問題更簡單、更方便,且結(jié)果程序擁有更高的并發(fā)性。ThreadLocal在Spring中發(fā)揮著重要的作用,在管理request作用域的Bean、事務(wù)管理、任務(wù)調(diào)度、AOP等模塊都出現(xiàn)了它們的身影,起著舉足輕重的作用。 不過在使用線程池的情況下,使用ThreadLocal應(yīng)該慎重,因為線程池中的線程是可重用的。 ThreadLocal的內(nèi)容以后還要仔細學(xué)習一下。
|
|
來自: minwh > 《設(shè)計模式》