執行緒安全使用 HashMap 的四種技巧

勇哥编程游记發表於2024-05-19

這篇文章,我們聊聊執行緒安全使用 HashMap 的四種技巧。

1方法內部:每個執行緒使用單獨的 HashMap

如下圖,tomcat 接收到到請求後,依次呼叫控制器 Controller、服務層 Service 、資料庫訪問層的相關方法。

每次訪問服務層方法 serviceMethod 時,都會在方法體內部建立一個單獨的 HashMap , 將相關請求引數複製到 HashMap 裡,然後呼叫 DAO 方法進行資料庫操作。

每個 HTTP 處理執行緒在服務層方法體內部都有自己的 HashMap 例項,在多執行緒環境下,不需要對 HashMap 進行任何同步操作。

這也是我們使用最普遍也最安全的的方式,是 CRUD 最基本的操作。

2 配置資料:初始化寫,後續只提供讀

系統啟動之後,我們可以將配置資料載入到本地快取 HashMap 裡 ,這些配置資訊初始化之後,就不需要寫入了,後續只提供讀操作。

上圖中顯示一個非常簡單的配置類 SimpleConfig ,內部有一個 HashMap 物件 configMap 。建構函式呼叫初始化方法,初始化方法內部的邏輯是:將配置資料儲存到 HashMap 中。

SimpleConfig 類對外暴露了 getConfig 方法 ,當 main 執行緒初始化 SimpleConfig 物件之後,當其他執行緒呼叫 getConfig 方法時,因為只有讀,沒有寫操作,所以是執行緒安全的。

3 讀寫鎖:寫時阻塞,並行讀,讀多寫少場景

讀寫鎖是一把鎖分為兩部分:讀鎖和寫鎖,其中讀鎖允許多個執行緒同時獲得,而寫鎖則是互斥鎖。

它的規則是:讀讀不互斥,讀寫互斥,寫寫互斥,適用於讀多寫少的業務場景。

我們一般都使用 ReentrantReadWriteLock ,該類實現了 ReadWriteLock 。ReadWriteLock 介面也很簡單,其內部主要提供了兩個方法,分別返回讀鎖和寫鎖 。

 public interface ReadWriteLock {
    //獲取讀鎖
    Lock readLock();
    //獲取寫鎖
    Lock writeLock();
}

讀寫鎖的使用方式如下所示:

  1. 建立 ReentrantReadWriteLock 物件 , 當使用 ReadWriteLock 的時候,並不是直接使用,而是獲得其內部的讀鎖和寫鎖,然後分別呼叫 lock / unlock 方法 ;
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
  1. 讀取共享資料 ;
Lock readLock = readWriteLock.readLock();
readLock.lock();
try {
   // TODO 查詢共享資料
} finally {
   readLock.unlock();
}
  1. 寫入共享資料;
Lock writeLock = readWriteLock.writeLock();
writeLock.lock();
try {
   // TODO 修改共享資料
} finally {
   writeLock.unlock();
}

下面的程式碼展示如何使用 ReadWriteLock 執行緒安全的使用 HashMap :

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockCache {
  
    // 建立一個 HashMap 來儲存快取的資料
    private Map<String, String> map = new HashMap<>();

    // 建立讀寫鎖物件
    private ReadWriteLock rw = new ReentrantReadWriteLock();

    // 放物件方法:向快取中新增一個鍵值對
    public void put(String key, String value) {
        // 獲取寫鎖,以確保當前操作是獨佔的
        rw.writeLock().lock();
        try {
            // 執行寫操作,將鍵值對放入 map
            map.put(key, value);
        } finally {
            // 釋放寫鎖
            rw.writeLock().unlock();
        }
    }

    // 取物件方法:從快取中獲取一個值
    public String get(String key) {
        // 獲取讀鎖,允許併發讀操作
        rw.readLock().lock();
        try {
            // 執行讀操作,從 map 中獲取值
            return map.get(key);
        } finally {
            // 釋放讀鎖
            rw.readLock().unlock();
        }
    }
}

使用讀寫鎖操作 HashMap 是一個非常經典的技巧,訊息中介軟體 RockeMQ NameServer (名字服務)儲存和查詢路由資訊都是透過這種技巧實現的。

另外,讀寫鎖可以操作多個 HashMap ,相比 ConcurrentHashMap 而言,ReadWriteLock 可以控制快取物件的顆粒度,具備更大的靈活性。

4 Collections.synchronizedMap : 讀寫均加鎖

如下程式碼,當我們多執行緒使用 userMap 時,

static Map<Long, User> userMap = Collections.synchronizedMap(new HashMap<Long, User>());

進入 synchronizedMap 方法:

public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
       return new SynchronizedMap<>(m);
}

SynchronizedMap 內部包含一個物件鎖 Object mutex ,它本質上是一個包裝類,將 HashMap 的讀寫操作重新實現了一次,我們看到每次讀寫時,都會用 synchronized 關鍵字來保證操作的執行緒安全。

雖然 Collections.synchronizedMap 這種技巧使用起來非常簡單,但是我們需要理解它的每次讀寫都會加鎖,效能並不會特別好。

5 總結

這篇文章,筆者總結了四種執行緒安全的使用 HashMap 的技巧。

1、方法內部:每個執行緒使用單獨的 HashMap

這是我們使用最普遍,也是非常可靠的方式。每個執行緒在方法體內部建立HashMap 例項,在多執行緒環境下,不需要對 HashMap 進行任何同步操作。

2、 配置資料:初始化寫,後續只提供讀

中介軟體在啟動時,會讀取配置檔案,將配置資料寫入到 HashMap 中,主執行緒寫完之後,以後不會再有寫入操作,其他的執行緒可以讀取,不會產生執行緒安全問題。

3、讀寫鎖:寫時阻塞,並行讀,讀多寫少場景

讀寫鎖是一把鎖分為兩部分:讀鎖和寫鎖,其中讀鎖允許多個執行緒同時獲得,而寫鎖則是互斥鎖。

它的規則是:讀讀不互斥,讀寫互斥,寫寫互斥,適用於讀多寫少的業務場景。

使用讀寫鎖操作 HashMap 是一個非常經典的技巧,訊息中介軟體 RockeMQ NameServer (名字服務)儲存和查詢路由資訊都是透過這種技巧實現的。

4、Collections.synchronizedMap : 讀寫均加鎖

Collections.synchronizedMap 方法使用了裝飾器模式為執行緒不安全的 HashMap 提供了一個執行緒安全的裝飾器類 SynchronizedMap。

透過SynchronizedMap來間接的保證對 HashMap 的操作是執行緒安全,而 SynchronizedMap 底層也是透過 synchronized 關鍵字來保證操作的執行緒安全。


如果我的文章對你有所幫助,還請幫忙點贊、在看、轉發一下,你的支援會激勵我輸出更高質量的文章,非常感謝!

相關文章