link:http://www.ibm.com/developerworks/cn/java/l-singleton/
單例對象(Singleton)是一種常用的設計模式。在Java應用中,單例對象能保證在一個JVM中,該對象只有一個執行個體存在。正是由於這個特點,單例對象通常作為程式中的存放配置資訊的載體,因為它能保證其他對象讀到一致的資訊。例如在某個伺服器程式中,該伺服器的配置資訊可能存放在資料庫或檔案中,這些配置資料由某個單例對象統一讀取,服務進程中的其他對象如果要擷取這些配置資訊,只需訪問該單例對象即可。這種方式極大地簡化了在複雜環境下,尤其是多線程環境下的組態管理,但是隨著應用情境的不同,也可能帶來一些同步問題。
本文將探討一下在多線程環境下,使用單例對象作配置資訊管理時可能會帶來的幾個同步問題,並針對每個問題給出可選的解決辦法。
問題描述
在多線程環境下,單例對象的同步問題主要體現在兩個方面,單例對象的初始化和單例對象的屬性更新。
本文描述的方法有如下假設:
- 單例對象的屬性(或成員變數)的擷取,是通過單例對象的初始化實現的。也就是說,在單例對象初始化時,會從檔案或資料庫中讀取最新的配置資訊。
- 其他對象不能直接改變單例對象的屬性,單例對象屬性的變化來源於設定檔或設定資料庫資料的變化。
1.1 單例對象的初始化
首先,討論一下單例對象的初始化同步。單例模式的通常處理方式是,在對象中有一個靜態成員變數,其類型就是單例類型本身;如果該變數為null,則建立該單例類型的對象,並將該變數指向這個對象;如果該變數不為null,則直接使用該變數。
其過程如下面代碼所示:
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 單例對象的初始化同步
對於初始化的同步,可以通過如下代碼所採用的方式解決。
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時,才能更新資料,同時要阻塞所有讀屬性的調用。代碼如下。
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 datasyncReadOut();return properties;} } |
2,採用"影子執行個體"的辦法
具體說,就是在更新屬性時,直接產生另一個單例對象執行個體,這個新產生的單例對象執行個體將從資料庫或檔案中讀取最新的配置資訊;然後將這些配置資訊直接賦值給舊單例對象的屬性。如下面代碼所示。
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屬性中。
上面兩個方法比較起來,第二個方法更好,首先,編程更簡單;其次,沒有那麼多的同步操作,對效能的影響也不大