記錄本周問題

weiewiyi發表於2023-01-11

專案裡兩個地方都用到了hashmap。但是感覺自己用的時候並沒有感覺非常的清晰。同時發現hashmap有執行緒不安全問題,而自己用的時候就是多執行緒來使用。於是在這裡介紹一下。

專案中兩個地方用到了hashmap。

1.策略模式

一個是使用hashmap來儲存service, 根據不同的事件呼叫不同的service。

image.png

在建構函式中,把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中有評論並關閉的按鈕

image.png

點選後釘釘機器人會傳送兩條通知
image.png

於是就有需求是合併事件。

測試發現,總是評論事件優於issue事件傳送。
image.png

實現邏輯

然後就寫了個方法。

由於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;
}

合併結果,前面加上已關閉

image.png

HashMap執行緒不安全

雖然寫完了能實現,但是找資料的時候,發現HashMap執行緒不安全。 就想著我也是多執行緒訪問,我這麼寫是不是會出bug。

先來說一下為什麼hashMap的結構以及為什麼執行緒不安全。

結構
HashMap是採用“連結串列陣列”的資料結構,即陣列和連結串列的結合體(在JDK1.8中還引入了紅黑樹結構,當一個連結串列上的結點達到8時改為紅黑樹結構)。
image.png

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 一對鍵值時,

  1. 根據 key的 hashCode 值計算出一個位置, 該位置就是此物件準備往陣列中存放的位置。
  2. 如果該位置沒有物件存在,就將此物件直接放進陣列當中;
  3. 如果該位置已經有物件存在了,則順著此存在的物件的鏈開始尋找(為了判斷是否是否值相同,map不允許<key,value>鍵值對重複)
  4. 進行尾插法,插入到連結串列後面。(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 包中的一個類, 效率比較高

相關文章