前段時間在項目中遇到一個問題。當多個系統同時運行時,大部分系統可以良好運轉,部分卻卡死在了啓動界面。如下是我解決該問題的步驟和總結:web
一、復現問題。從新走了一遍出問題的過程,發現問題的確存在。說明這個問題不是偶然發生。數據庫
二、看日誌。肯定問題是必然發生以後,開始查看日誌,發現日誌中有問題的系統狀態一直不正常。一直處於任務過時的狀態。一個系統對應一個任務,任務過時以後,系統就處於卡死狀態。系統的邏輯是這樣的:當啓動系統的時候,會發起多個請求,每一個請求會產生一個任務,同時將這些任務寫到緩存(HashMap)和數據庫。任務的狀態(包括數據庫和緩存)會隨着任務的進度而發生改變。緩存
任務過時意味着該任務已經執行完畢或者歷來沒有這個任務。安全
若是說任務已經執行完畢致使這個問題的話,這個是不可能的。由於對於每一個任務,當他執行成功或者失敗時,垃圾回收器會在15分鐘後對任務進行清理。事實上,當咱們一開啓系統時,就觀察到該系統對應的任務在數據庫中存在,可是在緩存中卻不存在!就是說,當咱們從HashMap 中獲取相應的任務時,獲取到的值是不存在的!爲何獲取到的值會不存在呢?這可能有兩種緣由:併發
(1)任務根本就沒有寫入緩存;高併發
(2)任務寫入緩存後很快被清理掉了;spa
可是根據以上的分析,任務被很快清理掉是不可能的。由於至少得在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.