java 、HashMap 和單例

一寧發表於2014-06-15

前段時間在專案中遇到一個問題。當多個系統同時執行時,大部分系統能夠良好運轉,部分卻卡死在了啟動介面。以下是我解決該問題的步驟和總結:

 
1、復現問題。重新走了一遍出問題的過程,發現問題的確存在。說明這個問題不是偶然發生。
2、看日誌。確定問題是必然發生之後,開始檢視日誌,發現日誌中有問題的系統狀態一直不正常。一直處於任務過期的狀態。一個系統對應一個任務,任務過期之後,系統就處於卡死狀態。系統的邏輯是這樣的:當啟動系統的時候,會發起多個請求,每個請求會產生一個任務,同時將這些任務寫到快取(HashMap)和資料庫。任務的狀態(包括資料庫和快取)會隨著任務的進度而發生改變。
 
 
任務過期意味著該任務已經執行完畢或者從來沒有這個任務。
如果說任務已經執行完畢導致這個問題的話,這個是不可能的。因為對於每個任務,當他執行成功或者失敗時,垃圾回收器會在15分鐘後對任務進行清理。事實上,當我們一開啟系統時,就觀察到該系統對應的任務在資料庫中存在,但是在快取中卻不存在!就是說,當我們從HashMap 中獲取相應的任務時,獲取到的值是不存在的!為什麼獲取到的值會不存在呢?這可能有兩種原因:
(1)任務根本就沒有寫入快取;
(2)任務寫入快取後很快被清理掉了;
但是根據以上的分析,任務被很快清理掉是不可能的。因為至少得在15分鐘之後,才能清理。那就只有第一種可能了:任務根本沒有寫入快取!
 
開始著手看程式碼。發現寫入快取的關鍵一行程式碼:
MyMap. getInstance().put( taskId"hello" );
 
繼續跟蹤MyMap,主要的類相關內容如下:
 
public class MyMap {
     
      private Map<Integer, Object> map = new HashMap<Integer, Object>();
      private Object lock = new Object();
     
      private static MyMap instance new MyMap();
      private MyMap(){}
      public static MyMap getInstance() {
           if (instance == null) {
               instance new MyMap();
           }
           return instance ;
      }
      public void put(Integer taskId, String name) {
           synchronized (lock ) {
               map.put(taskId, name);
           }
      }
     
      public Map<Integer, Object> getMap() {
           return map ;
      }
 
}
 
 
該類使用單例模式,使用HashMap來儲存所有的任務。每次執行一個任務,都會將這個任務寫入快取。然後根據taskId獲取相應的任務。這段程式碼看起來沒有多大問題。
 
但是在高併發的情況下,這個單例是不安全的:
public static MyMap getInstance() {
           if (instance == null) {
               instance new MyMap();
          }
           return instance ;
     }
 
在多個執行緒同時請求getInstance時,某個執行緒,判斷instance == null 為true,會繼續執行instance = new MyMap(); 
 
這行程式碼會先new MyMap(),在heap上分配記憶體空間,然後將instance 指向該記憶體地址。在instance 未指向該記憶體空間時,如果其他執行緒也呼叫getInstance時,發現instance == null 為真,也會執行new MyMap()。這時,不同的執行緒拿到的就不是同一個例項了。呼叫put後,會將不同的資料寫入到不同物件對應的map中。所以我們拿到的例項有可能是所有執行緒共享的例項,也有可能是某些執行緒共享的例項,當然我們就只能獲取到部分資料,另外的資料就丟失了。或者說資料依然在某個記憶體中,但是我們丟失了指向該資料的引用。所以部分任務就這麼丟失了,導致系統處於卡死狀態。
 
如何來處理這種不安全的單例呢?
使用兩種方式可以解決:
 
(1)給getInstance()方法新增關鍵字synchronized,保證當前只有一個執行緒執行該方法。
 
public synchronized static MyMap getInstance() {
           if (instance == null) {
               instance new MyMap();
           }
           return instance ;
 }
(2)
private static MyMap instance = new MyMap();
private MyMap(){}
public static MyMap getInstance() {
           return instance ;
}
 
第一種方式使用效率較低。第二種方式在類載入時便生成物件。沒有使用類的延遲載入。
另外還有兩種方式可以實現:內部靜態類和雙重校驗鎖(暫且不討論)。
 
通過這兩種方式,即可以解決單例模式的執行緒安全問題。同時,為了提高效率,將快取從HashMap改為ConcurrentHashMap.
 
 
 

相關文章