尋找一種性能改進方法時,你可能會選擇像下面這樣重寫getInstance()方法:
- public static Singleton getInstance() {
- if(singleton == null) {
- synchronized(Singleton.class) {
- singleton = new Singleton();
- }
- }
- return singleton;
- }
這個代碼片段只同步了關鍵的代碼,而不是同步整個方法。然而這段代碼卻不是線程安全的。考慮一下下面的假定:線程1進入同步塊,并且在它給singleton成員變量賦值之前線程1被切換。接著另一個線程進入if塊。第二個線程將等待直到第一個線程完成,并且仍然會得到兩個不同的單例類實例。有修復這個問題的方法嗎?請讀下去。
雙重加鎖檢查
初看上去,雙重加鎖檢查似乎是一種使懶漢式實例化為線程安全的技術。下面的代碼片段展示了這種技術:
- public static Singleton getInstance() {
- if(singleton == null) {
- synchronized(Singleton.class) {
- if(singleton == null) {
- singleton = new Singleton();
- }
- }
- }
- return singleton;
- }
如果兩個線程同時訪問getInstance()方法會發生什么?想像一下線程1進行同步塊馬上又被切換。接著,第二個線程進入if 塊。當線程1退出同步塊時,線程2會重新檢查看是否singleton實例仍然為null。因為線程1設置了singleton成員變量,所以線程2的第二次檢查會失敗,第二個單例類實例也就不會被創建。似乎就是如此。
不幸的是,雙重加鎖檢查不會保證正常工作,因為編譯器會在Singleton的構造方法被調用之前隨意給singleton賦一個值。如果在singleton引用被賦值之后而被初始化之前線程1被切換,線程2就會被返回一個對未初始化的單例類實例的引用。
一個改進的線程安全的單例模式實現
例7列出了一個簡單、快速而又是線程安全的單例模式實現:
例7.一個簡單的單例類
- public class Singleton {
- public final static Singleton INSTANCE = new Singleton();
- private Singleton() {
- // Exists only to defeat instantiation.
- }
- }
這段代碼是線程安全的是因為靜態成員變量一定會在類被第一次訪問時被創建。你得到了一個自動使用了懶漢式實例化的線程安全的實現;你應該這樣使用它:
- Singleton singleton = Singleton.INSTANCE;
- singleton.dothis();
- singleton.dothat();
- ...
當然萬事并不完美,前面的Singleton只是一個折衷的方案;如果你使用那個實現,你就無法改變它以便后來你可能想要允許多個單例類的實例。用一種更折哀的單例模式實現(通過一個getInstance()方法獲得實例)你可以改變這個方法以便返回一個唯一的實例或者是數百個實例中的一個.你不能用一個公開且是靜態的(public static)成員變量這樣做.
你可以安全的使用例7的單例模式實現或者是例1的帶一個同步的getInstance()方法的實現.然而,我們必須要研究另一個問題:你必須在編譯期指定這個單例類,這樣就不是很靈活.一個單例類的注冊表會讓我們在運行期指定一個單例類.
使用注冊表
使用一個單例類注冊表可以:
在運行期指定單例類
防止產生多個單例類子類的實例
在例8的單例類中,保持了一個通過類名進行注冊的單例類注冊表:
例8 帶注冊表的單例類
- import java.util.HashMap;
- import org.apache.log4j.Logger;
- public class Singleton {
- private static HashMap map = new HashMap();
- private static Logger logger = Logger.getRootLogger();
- protected Singleton() {
- // Exists only to thwart instantiation
- }
- public static synchronized Singleton getInstance(String classname) {
- if(classname == null) throw new IllegalArgumentException("Illegal classname");
- Singleton singleton = (Singleton)map.get(classname);
- if(singleton != null) {
- logger.info("got singleton from map: " + singleton);
- return singleton;
- }
- if(classname.equals("SingeltonSubclass_One"))
- singleton = new SingletonSubclass_One();
- else if(classname.equals("SingeltonSubclass_Two"))
- singleton = new SingletonSubclass_Two();
- map.put(classname, singleton);
- logger.info("created singleton: " + singleton);
- return singleton;
- }
- // Assume functionality follows that's attractive to inherit
- }
這段代碼的基類首先創建出子類的實例,然后把它們存儲在一個Map中。但是基類卻得付出很高的代價因為你必須為每一個子類替換它的getInstance()方法。幸運的是我們可以使用反射處理這個問題。
使用反射
在例9的帶注冊表的單例類中,使用反射來實例化一個特殊的類的對象。與例8相對的是通過這種實現,Singleton.getInstance()方法不需要在每個被實現的子類中重寫了。
例9 使用反射實例化單例類
- import java.util.HashMap;
- import org.apache.log4j.Logger;
- public class Singleton {
- private static HashMap map = new HashMap();
- private static Logger logger = Logger.getRootLogger();
- protected Singleton() {
- // Exists only to thwart instantiation
- }
- public static synchronized Singleton getInstance(String classname) {
- Singleton singleton = (Singleton)map.get(classname);
- if(singleton != null) {
- logger.info("got singleton from map: " + singleton);
- return singleton;
- }
- try {
- singleton = (Singleton)Class.forName(classname).newInstance();
- }
- catch(ClassNotFoundException cnf) {
- logger.fatal("Couldn't find class " + classname);
- }
- catch(InstantiationException ie) {
- logger.fatal("Couldn't instantiate an object of type " + classname);
- }
- catch(IllegalAccessException ia) {
- logger.fatal("Couldn't access class " + classname);
- }
- map.put(classname, singleton);
- logger.info("created singleton: " + singleton);
- return singleton;
- }
- }
關于單例類的注冊表應該說明的是:它們應該被封裝在它們自己的類中以便最大限度的進行復用。
封裝注冊表
例10列出了一個單例注冊表類。
例10 一個SingletonRegistry類
- import java.util.HashMap;
- import org.apache.log4j.Logger;
- public class SingletonRegistry {
- public static SingletonRegistry REGISTRY = new SingletonRegistry();
- private static HashMap map = new HashMap();
- private static Logger logger = Logger.getRootLogger();
- protected SingletonRegistry() {
- // Exists to defeat instantiation
- }
- 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.fatal("Couldn't find class " + classname);
- }
- catch(InstantiationException ie) {
- logger.fatal("Couldn't instantiate an object of type " +
- classname);
- }
- catch(IllegalAccessException ia) {
- logger.fatal("Couldn't access class " + classname);
- }
- map.put(classname, singleton);
- return singleton;
- }
- }
注意我是把SingletonRegistry類作為一個單例模式實現的。我也通用化了這個注冊表以便它能存儲和取回任何類型的對象。例11顯示了的Singleton類使用了這個注冊表。
例11 使用了一個封裝的注冊表的Singleton類
- import java.util.HashMap;
- import org.apache.log4j.Logger;
- public class Singleton {
- protected Singleton() {
- // Exists only to thwart instantiation.
- }
- public static Singleton getInstance() {
- return (Singleton)SingletonRegistry.REGISTRY.getInstance(classname);
- }
- }
上面的Singleton類使用那個注冊表的唯一實例通過類名取得單例對象。
現在我們已經知道如何實現線程安全的單例類和如何使用一個注冊表去在運行期指定單例類名,接著讓我們考查一下如何安排類載入器和處理序列化。