如何線程安全的使用 HashMap
這篇文章,我們聊聊線程安全使用 HashMap 的四種技巧。
圖片
1.方法內部:每個線程創(chuàng)建單獨的 HashMap
如下圖,tomcat 接收到到請求后,依次調用控制器 Controller、服務層 Service 、數據庫訪問層的相關方法。
每次訪問服務層方法 serviceMethod 時,都會在方法體內部創(chuàng)建一個單獨的 HashMap , 將相關請求參數拷貝到 HashMap 里,然后調用 DAO 方法進行數據庫操作。
圖片
每個 HTTP 處理線程在服務層方法體內部都有自己的 HashMap 實例,在多線程環(huán)境下,不需要對 HashMap 進行任何同步操作。
這也是我們使用最普遍也最安全的的方式,是 CRUD 最基本的操作。
2.配置數據:初始化單線程寫,后續(xù)只提供讀
系統(tǒng)啟動之后,我們可以將配置數據加載到本地緩存 HashMap 里 ,這些配置信息初始化之后,就不需要寫入了,后續(xù)只提供讀操作。
圖片
上圖中顯示一個非常簡單的配置類 SimpleConfig ,內部有一個 HashMap 對象 configMap 。構造函數調用初始化方法,初始化方法內部的邏輯是:將配置數據存儲到 HashMap 中。
SimpleConfig 類對外暴露了 getConfig 方法 ,當 main 線程初始化 SimpleConfig 對象之后,當其他線程調用 getConfig 方法時,因為只有讀,沒有寫操作,所以是線程安全的。
3.讀寫鎖:讀讀不互斥,讀寫互斥,寫寫互斥
讀寫鎖是一把鎖分為兩部分:讀鎖和寫鎖,其中讀鎖允許多個線程同時獲得,而寫鎖則是互斥鎖。
它的規(guī)則是:讀讀不互斥,讀寫互斥,寫寫互斥,適用于讀多寫少的業(yè)務場景。
我們一般都使用 ReentrantReadWriteLock ,該類實現了 ReadWriteLock 。ReadWriteLock 接口也很簡單,其內部主要提供了兩個方法,分別返回讀鎖和寫鎖 。
public interface ReadWriteLock {
//獲取讀鎖
Lock readLock();
//獲取寫鎖
Lock writeLock();
}
讀寫鎖的使用方式如下所示:
- 創(chuàng)建 ReentrantReadWriteLock 對象 , 當使用 ReadWriteLock 的時候,并不是直接使用,而是獲得其內部的讀鎖和寫鎖,然后分別調用 lock / unlock 方法 ;
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
- 讀取共享數據 ;
Lock readLock = readWriteLock.readLock();
readLock.lock();
try {
// TODO 查詢共享數據
} finally {
readLock.unlock();
}
- 寫入共享數據;
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 {
// 創(chuàng)建一個 HashMap 來存儲緩存的數據
private Map<String, String> map = new HashMap<>();
// 創(chuàng)建讀寫鎖對象
private ReadWriteLock rw = new ReentrantReadWriteLock();
// 放對象方法:向緩存中添加一個鍵值對
public void put(String key, String value) {
// 獲取寫鎖,以確保當前操作是獨占的
rw.writeLock().lock();
try {
// 執(zhí)行寫操作,將鍵值對放入 map
map.put(key, value);
} finally {
// 釋放寫鎖
rw.writeLock().unlock();
}
}
// 取對象方法:從緩存中獲取一個值
public String get(String key) {
// 獲取讀鎖,允許并發(fā)讀操作
rw.readLock().lock();
try {
// 執(zhí)行讀操作,從 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)方法內部:每個線程創(chuàng)建單獨的 HashMap
這是我們使用最普遍,也是非??煽康姆绞健C總€線程在方法體內部創(chuàng)建HashMap 實例,在多線程環(huán)境下,不需要對 HashMap 進行任何同步操作。
2) 配置數據:初始化單線程寫,后續(xù)只提供讀
中間件在啟動時,會讀取配置文件,將配置數據寫入到 HashMap 中,主線程寫完之后,以后不會再有寫入操作,其他的線程可以讀取,不會產生線程安全問題。
3)讀寫鎖:讀讀不互斥,讀寫互斥,寫寫互斥
讀寫鎖是一把鎖分為兩部分:讀鎖和寫鎖,其中讀鎖允許多個線程同時獲得,而寫鎖則是互斥鎖。
它的規(guī)則是:讀讀不互斥,讀寫互斥,寫寫互斥,適用于讀多寫少的業(yè)務場景。
使用讀寫鎖操作 HashMap 是一個非常經典的技巧,消息中間件 RockeMQ NameServer (名字服務)保存和查詢路由信息都是通過這種技巧實現的。
4)Collections.synchronizedMap : 讀寫均加鎖
Collections.synchronizedMap 方法使用了裝飾器模式為線程不安全的 HashMap 提供了一個線程安全的裝飾器類 SynchronizedMap。
通過 SynchronizedMap 來間接的保證對 HashMap 的操作是線程安全,而 SynchronizedMap 底層也是通過 synchronized 關鍵字來保證操作的線程安全。