專案裡兩個地方都用到了hashmap。但是感覺自己用的時候並沒有感覺非常的清晰。同時發現hashmap有執行緒不安全問題,而自己用的時候就是多執行緒來使用。於是在這裡介紹一下。
專案中兩個地方用到了hashmap。
1.策略模式
一個是使用hashmap來儲存service, 根據不同的事件呼叫不同的service。
在建構函式中,把service新增到map中, 然後使用的時候,根據eventName獲取相應的service進行處理。
@Service
1 public class GitLabNotifyServiceImpl implements GitLabNotifyService {
2 private Map<String, EventService> map = new HashMap<>();
3 public GitLabNotifyServiceImpl(PushEventService pushEventService,
4 IssueEventService issueEventService,
5 CommentEventService) {
6 this.addService(issueEventService);
7 this.addService(pushEventService););
}
@Override
8 public void handleEventData(String json, String eventName,String access_token) throws IOException {
9 EventService eventService = this.map.get(eventName);
}
2.合併事件
開發背景:
gitlab中有評論並關閉的按鈕
點選後釘釘機器人會傳送兩條通知
於是就有需求是合併事件。
測試發現,總是評論事件優於issue事件傳送。
實現邏輯
然後就寫了個方法。
由於spring boot的bean預設都是單例的,所以定義了一個hashmap, 記錄當前要執行的gitlab的事件
/**
* key: access_token, github釘釘機器人的token
* value GitlabEvent, gitlab事件
*/
private static final HashMap<String, String> hashMap = new HashMap<>();
兩個事件傳送後,會有comment請求和issue close請求短間隔被伺服器接受。
伺服器會開啟兩個執行緒。
兩個執行緒都對hashmap進行訪問。
當前事件為comment事件的時候: 向hashMap中記錄對應釘釘機器人事件是comment,執行緒先睡眠兩秒,若後續有issue close事件,則合併事件。即不傳送isssue close事件,並在comment事件上增加已關閉的提示資訊。
當前事件為issue close事件的時候: 若2s前有comment事件,並且是同一個issue,則不傳送issue close事件。
public String commentAndIssueClose(String json, String eventName, String access_token) throws IOException {
if (Objects.equals(eventName, GitlabEvent.issueHook)) {
// 若issue事件前面有comment事件,且為issue為close事件 則不傳送該事件
if (Objects.equals(hashMap.get(access_token), GitlabEvent.noteHook) && this.judgeIsIssueClose(json)) {
hashMap.put(access_token, GitlabEvent.issueHook);
// 返回null 表示不傳送
return null;
}
} else if (Objects.equals(eventName, GitlabEvent.noteHook)) {
try {
hashMap.put(access_token, GitlabEvent.noteHook);
// 因為評論事件先於issue事件傳送, 所以評論事件等待兩秒
TimeUnit.SECONDS.sleep(2);
// 若2s後存在issue事件 則事件合併
if (Objects.equals(hashMap.get(access_token), GitlabEvent.issueHook)) {
hashMap.put(access_token, "");
return this.setIssueTittleClose(json);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 清空value
hashMap.put(access_token, "");
return json;
}
合併結果,前面加上已關閉:
HashMap執行緒不安全
雖然寫完了能實現,但是找資料的時候,發現HashMap執行緒不安全。 就想著我也是多執行緒訪問,我這麼寫是不是會出bug。
先來說一下為什麼hashMap的結構以及為什麼執行緒不安全。
結構
HashMap是採用“連結串列陣列”的資料結構,即陣列和連結串列的結合體(在JDK1.8中還引入了紅黑樹結構,當一個連結串列上的結點達到8時改為紅黑樹結構)。
HashMap底層維護一個陣列,陣列中的每一項都是一個Entry, 如下
transient Entry<K,V>[] table;
我們向 HashMap 中所放置的物件實際上是儲存在該陣列當中。
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
而這個Entry應該放在陣列的哪一個位置上,是透過key的hashCode來計算的。
final int hash(Object k) {
int h = 0;
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
透過hash計算出來的值將會使用indexFor方法找到它應該所在的table下標:
static int indexFor(int h, int length) {
return h & (length-1);
}
當兩個key透過hashCode計算相同時,則發生了hash衝突(碰撞),HashMap解決hash衝突的方式是用連結串列。
總結:
當向 HashMap 中 put 一對鍵值時,
- 根據 key的 hashCode 值計算出一個位置, 該位置就是此物件準備往陣列中存放的位置。
- 如果該位置沒有物件存在,就將此物件直接放進陣列當中;
- 如果該位置已經有物件存在了,則順著此存在的物件的鏈開始尋找(為了判斷是否是否值相同,map不允許<key,value>鍵值對重複)
- 進行尾插法,插入到連結串列後面。(jdk1.8之前是頭插法)
HashMap執行緒不安全的體現:
JDK1.7 HashMap執行緒不安全體現在:死迴圈、資料丟失
JDK1.8 HashMap執行緒不安全體現在:資料覆蓋
JDK1.7 中,由於多執行緒對HashMap進行擴容,呼叫了HashMap.transfer(),具體原因:某個執行緒執行過程中,被掛起,其他執行緒已經完成資料遷移,等CPU資源釋放後被掛起的執行緒重新執行之前的邏輯,資料已經被改變,造成死迴圈、資料丟失
。
JDK1.8 中,由於多執行緒對HashMap進行put操作,呼叫了HashMap.putVal(),具體原因:假設兩個執行緒A、B都在進行put操作,並且hash函式計算出的插入下標是相同的,當執行緒A執行中後由於時間片耗盡導致被掛起,而執行緒B得到時間片後在該下標處插入了元素,完成了正常的插入,然後執行緒A獲得時間片,由於之前已經進行了hash碰撞的判斷,所有此時不會再進行判斷,而是直接進行插入,這就導致了執行緒B插入的資料被執行緒A覆蓋了,從而執行緒不安全。
改善:
資料丟失、死迴圈已經在在JDK1.8中已經得到了很好的解決,因為JDK1.8直接在HashMap#resize()中完成了資料遷移。
如何執行緒安全的使用HashMap
瞭解了 HashMap 為什麼執行緒不安全,那現在看看如何執行緒安全的使用 HashMap。以下三種方式:
- Hashtable
- ConcurrentHashMap
- Synchronized Map
//Hashtable
Map<String, String> hashtable = new Hashtable<>();
//synchronizedMap
Map<String, String> synchronizedHashMap = Collections.synchronizedMap(new HashMap<String, String>());
//ConcurrentHashMap
Map<String, String> concurrentHashMap = new ConcurrentHashMap<>();
前兩種是使用,synchronized 來保證執行緒安全的。 最後一種是是 JUC 包中的一個類, 效率比較高