單例對象(Singleton)是一種經常使用的設計模式。在Java應用中,單例對象能保證在一個JVM中,該對象只有一個實例存在。正是因爲這個特 點,單例對象一般做爲程序中的存放配置信息的載體,由於它能保證其餘對象讀到一致的信息。例如在某個服務器程序中,該服務器的配置信息可能存放在數據庫或 文件中,這些配置數據由某個單例對象統一讀取,服務進程中的其餘對象若是要獲取這些配置信息,只需訪問該單例對象便可。這種方式極大地簡化了在複雜環境 下,尤爲是多線程環境下的配置管理,可是隨着應用場景的不一樣,也可能帶來一些同步問題。 本文將探討一下在多線程環境下,使用單例對象做配置信息管理時可能會帶來的幾個同步問題,並針對每一個問題給出可選的解決辦法。 問題描述 在多線程環境下,單例對象的同步問題主要體如今兩個方面,單例對象的初始化和單例對象的屬性更新。 本文描述的方法有以下假設: 1. 單例對象的屬性(或成員變量)的獲取,是經過單例對象的初始化實現的。也就是說,在單例對象初始化時,會從文件或數據庫中讀取最新的配置信息。 2. 其餘對象不能直接改變單例對象的屬性,單例對象屬性的變化來源於配置文件或配置數據庫數據的變化。 1.1 單例對象的初始化 首先,討論一下單例對象的初始化同步。單例模式的一般處理方式是,在對象中有一個靜態成員變量,其類型就是單例類型自己;若是該變量爲null,則建立該單例類型的對象,並將該變量指向這個對象;若是該變量不爲null,則直接使用該變量。 其過程以下面代碼所示: Java代碼 收藏代碼 public class GlobalConfig { private static GlobalConfig instance = null; private Vector properties = null; private GlobalConfig() { //Load configuration information from DB or file //Set values for properties } public static GlobalConfig getInstance() { if (instance == null) { instance = new GlobalConfig(); } return instance; } public Vector getProperties() { return properties; } } 這種處理方式在單線程的模式下能夠很好的運行;可是在多線程模式下,可能產生問題。若是第一個線程發現成員變量爲null,準備建立對象;這是第二 個線程同時也發現成員變量爲null,也會建立新對象。這就會形成在一個JVM中有多個單例類型的實例。若是這個單例類型的成員變量在運行過程當中變化,會 形成多個單例類型實例的不一致,產生一些很奇怪的現象。例如,某服務進程經過檢查單例對象的某個屬性來中止多個線程服務,若是存在多個單例對象的實例,就 會形成部分線程服務中止,部分線程服務不能中止的狀況。 1.2 單例對象的屬性更新 一般,爲了實現配置信息的實時更新,會有一個線程不停檢測配置文件或配置數據庫的內容,一旦發現變化,就更新到單例對象的屬性中。在更新這些信 息的時候,極可能還會有其餘線程正在讀取這些信息,形成意想不到的後果。仍是以經過單例對象屬性中止線程服務爲例,若是更新屬性時讀寫不一樣步,可能訪問該 屬性時這個屬性正好爲空(null),程序就會拋出異常。 解決方法 2.1 單例對象的初始化同步 對於初始化的同步,能夠經過以下代碼所採用的方式解決。 Java代碼 收藏代碼 public class GlobalConfig { private static GlobalConfig instance = null; private Vector properties = null; private GlobalConfig() { //Load configuration information from DB or file //Set values for properties } private static synchronized void syncInit() { if (instance == null) { instance = new GlobalConfig(); } } public static GlobalConfig getInstance() { if (instance == null) { syncInit(); } return instance; } public Vector getProperties() { return properties; } } 這種處理方式雖然引入了同步代碼,可是由於這段同步代碼只會在最開始的時候執行一次或屢次,因此對整個系統的性能不會有影響。 2.2 單例對象的屬性更新同步 爲了解決第2個問題,有兩種方法: 1,參照讀者/寫者的處理方式 設置一個讀計數器,每次讀取配置信息前,將計數器加1,讀完後將計數器減1.只有在讀計數器爲0時,才能更新數據,同時要阻塞全部讀屬性的調用。代碼以下。 Java代碼 收藏代碼 public class GlobalConfig { private static GlobalConfig instance; private Vector properties = null; private boolean isUpdating = false; private int readCount = 0; private GlobalConfig() { //Load configuration information from DB or file //Set values for properties } private static synchronized void syncInit() { if (instance == null) { instance = new GlobalConfig(); } } public static GlobalConfig getInstance() { if (instance==null) { syncInit(); } return instance; } public synchronized void update(String p_data) { syncUpdateIn(); //Update properties } private synchronized void syncUpdateIn() { while (readCount > 0) { try { wait(); } catch (Exception e) { } } } private synchronized void syncReadIn() { readCount++; } private synchronized void syncReadOut() { readCount--; notifyAll(); } public Vector getProperties() { syncReadIn(); //Process data syncReadOut(); return properties; } } 2,採用"影子實例"的辦法 具體說,就是在更新屬性時,直接生成另外一個單例對象實例,這個新生成的單例對象實例將從數據庫或文件中讀取最新的配置信息;而後將這些配置信息直接賦值給舊單例對象的屬性。以下面代碼所示。 Java代碼 收藏代碼 public class GlobalConfig { private static GlobalConfig instance = null; private Vector properties = null; private GlobalConfig() { //Load configuration information from DB or file //Set values for properties } private static synchronized void syncInit() { if (instance = null) { instance = new GlobalConfig(); } } public static GlobalConfig getInstance() { if (instance = null) { syncInit(); } return instance; } public Vector getProperties() { return properties; } public void updateProperties() { //Load updated configuration information by new a GlobalConfig object GlobalConfig shadow = new GlobalConfig(); properties = shadow.getProperties(); } } 注意:在更新方法中,經過生成新的GlobalConfig的實例,從文件或數據庫中獲得最新配置信息,並存放到properties屬性中。 上面兩個方法比較起來,第二個方法更好,首先,編程更簡單;其次,沒有那麼多的同步操做,對性能的影響也不大。