日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

如何线程安全的使用HashMap

發(fā)布時間:2025/3/21 编程问答 25 豆豆
生活随笔 收集整理的這篇文章主要介紹了 如何线程安全的使用HashMap 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

在周二面試時(我轉(zhuǎn)的,當(dāng)然不是我),一面的面試官有問到HashMap是否是線程安全的,如何在線程安全的前提下使用HashMap,其實(shí)也就是HashMap,Hashtable,ConcurrentHashMap和synchronized Map的原理和區(qū)別。當(dāng)時有些緊張只是簡單說了下HashMap不是線程安全的;Hashtable線程安全,但效率低,因?yàn)槭荋ashtable是使用synchronized的,所有線程競爭同一把鎖;而ConcurrentHashMap不僅線程安全而且效率高,因?yàn)樗粋€segment數(shù)組,將數(shù)據(jù)分段存儲,給每一段數(shù)據(jù)配一把鎖,也就是所謂的鎖分段技術(shù)。當(dāng)時忘記了synchronized Map和解釋一下HashMap為什么線程不安全。面試結(jié)束后問了下面試官哪里有些不足,面試官說上面這個問題的回答算過關(guān),但可以在深入一些或者自己動手嘗試一下。so~~~雖然拿到了offer,但還是再整理一下,不能得過且過啊。

為什么HashMap是線程不安全的

總說HashMap是線程不安全的,不安全的,不安全的,那么到底為什么它是線程不安全的呢?要回答這個問題就要先來簡單了解一下HashMap源碼中的使用的存儲結(jié)構(gòu)(這里引用的是Java 8的源碼,與7是不一樣的)和它的擴(kuò)容機(jī)制。

HashMap的內(nèi)部存儲結(jié)構(gòu)

下面是HashMap使用的存儲結(jié)構(gòu):

transient Node<K,V>[] table;static class Node<K,V> implements Map.Entry<K,V> {final int hash;final K key;V value;Node<K,V> next; }

可以看到HashMap內(nèi)部存儲使用了一個Node數(shù)組(默認(rèn)大小是16),而Node類包含一個類型為Node的next的變量,也就是相當(dāng)于一個鏈表,所有hash值相同(即產(chǎn)生了沖突)的key會存儲到同一個鏈表里

需要注意的是,在Java 8中如果hash值相同的key數(shù)量大于指定值(默認(rèn)是8)時使用平衡樹來代替鏈表,這會將get()方法的性能從O(n)提高到O(logn)。具體的可以看我的另一篇博客Java 8中HashMap和LinkedHashMap如何解決沖突。

HashMap的自動擴(kuò)容機(jī)制

HashMap內(nèi)部的Node數(shù)組默認(rèn)的大小是16,假設(shè)有100萬個元素,那么最好的情況下每個hash桶里都有62500個元素,這時get(),put(),remove()等方法效率都會降低。為了解決這個問題,HashMap提供了自動擴(kuò)容機(jī)制,當(dāng)元素個數(shù)達(dá)到數(shù)組大小loadFactor后會擴(kuò)大數(shù)組的大小,在默認(rèn)情況下,數(shù)組大小為16,loadFactor為0.75,也就是說當(dāng)HashMap中的元素超過16\0.75=12時,會把數(shù)組大小擴(kuò)展為2*16=32,并且重新計算每個元素在新數(shù)組中的位置。

沒擴(kuò)容前,獲取EntryE需要遍歷5個元素,擴(kuò)容之后只需要2次。

為什么線程不安全

個人覺得HashMap在并發(fā)時可能出現(xiàn)的問題主要是兩方面,首先如果多個線程同時使用put方法添加元素,而且假設(shè)正好存在兩個put的key發(fā)生了碰撞(hash值一樣),那么根據(jù)HashMap的實(shí)現(xiàn),這兩個key會添加到數(shù)組的同一個位置,這樣最終就會發(fā)生其中一個線程的put的數(shù)據(jù)被覆蓋。第二就是如果多個線程同時檢測到元素個數(shù)超過數(shù)組大小*loadFactor,這樣就會發(fā)生多個線程同時對Node數(shù)組進(jìn)行擴(kuò)容,都在重新計算元素位置以及復(fù)制數(shù)據(jù),但是最終只有一個線程擴(kuò)容后的數(shù)組會賦給table,也就是說其他線程的都會丟失,并且各自線程put的數(shù)據(jù)也丟失。
關(guān)于HashMap線程不安全這一點(diǎn),《Java并發(fā)編程的藝術(shù)》一書中是這樣說的:

HashMap在并發(fā)執(zhí)行put操作時會引起死循環(huán),導(dǎo)致CPU利用率接近100%。因?yàn)槎嗑€程會導(dǎo)致HashMap的Node鏈表形成環(huán)形數(shù)據(jù)結(jié)構(gòu),一旦形成環(huán)形數(shù)據(jù)結(jié)構(gòu),Node的next節(jié)點(diǎn)永遠(yuǎn)不為空,就會在獲取Node時產(chǎn)生死循環(huán)。

哇塞,聽上去si不si好神奇,居然會產(chǎn)生死循環(huán)。。。。google了一下,才知道死循環(huán)并不是發(fā)生在put操作時,而是發(fā)生在擴(kuò)容時。詳細(xì)的解釋可以看下面幾篇博客:

  • 酷殼-Java HashMap的死循環(huán)
  • HashMap在java并發(fā)中如何發(fā)生死循環(huán)
  • How does a HashMap work in JAVA

如何線程安全的使用HashMap

了解了HashMap為什么線程不安全,那現(xiàn)在看看如何線程安全的使用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<>();

依次來看看。

Hashtable

先稍微吐槽一下,為啥命名不是HashTable啊,看著好難受,不管了就裝作它叫HashTable吧。這貨已經(jīng)不常用了,就簡單說說吧。HashTable源碼中是使用synchronized來保證線程安全的,比如下面的get方法和put方法:

public synchronized V get(Object key) {// 省略實(shí)現(xiàn)}public synchronized V put(K key, V value) {// 省略實(shí)現(xiàn)}


ConcurrentHashMap所以當(dāng)一個線程訪問HashTable的同步方法時,其他線程如果也要訪問同步方法,會被阻塞住。舉個例子,當(dāng)一個線程使用put方法時,另一個線程不但不可以使用put方法,連get方法都不可以,好霸道啊!!!so~~,效率很低,現(xiàn)在基本不會選擇它了。

ConcurrentHashMap(以下簡稱CHM)是JUC包中的一個類,Spring的源碼中有很多使用CHM的地方。之前已經(jīng)翻譯過一篇關(guān)于ConcurrentHashMap的博客,如何在java中使用ConcurrentHashMap,里面介紹了CHM在Java中的實(shí)現(xiàn),CHM的一些重要特性和什么情況下應(yīng)該使用CHM。需要注意的是,上面博客是基于Java 7的,和8有區(qū)別,在8中CHM摒棄了Segment(鎖段)的概念,而是啟用了一種全新的方式實(shí)現(xiàn),利用CAS算法,有時間會重新總結(jié)一下。

SynchronizedMap

看了一下源碼,SynchronizedMap的實(shí)現(xiàn)還是很簡單的。

// synchronizedMap方法 public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {return new SynchronizedMap<>(m);}// SynchronizedMap類 private static class SynchronizedMap<K,V>implements Map<K,V>, Serializable {private static final long serialVersionUID = 1978198479659022715L;private final Map<K,V> m;???? // Backing Mapfinal Object????? mutex;??????? // Object on which to synchronizeSynchronizedMap(Map<K,V> m) {this.m = Objects.requireNonNull(m);mutex = this;}SynchronizedMap(Map<K,V> m, Object mutex) {this.m = m;this.mutex = mutex;}public int size() {synchronized (mutex) {return m.size();}}public boolean isEmpty() {synchronized (mutex) {return m.isEmpty();}}public boolean containsKey(Object key) {synchronized (mutex) {return m.containsKey(key);}}public boolean containsValue(Object value) {synchronized (mutex) {return m.containsValue(value);}}public V get(Object key) {synchronized (mutex) {return m.get(key);}}public V put(K key, V value) {synchronized (mutex) {return m.put(key, value);}}public V remove(Object key) {synchronized (mutex) {return m.remove(key);}}// 省略其他方法}

從源碼中可以看出調(diào)用synchronizedMap()方法后會返回一個SynchronizedMap類的對象,而在SynchronizedMap類中使用了synchronized同步關(guān)鍵字來保證對Map的操作是線程安全的。

性能對比

這是要靠數(shù)據(jù)說話的時代,所以不能只靠嘴說CHM快,它就快了。寫個測試用例,實(shí)際的比較一下這三種方式的效率(源碼來源),下面的代碼分別通過三種方式創(chuàng)建Map對象,使用ExecutorService來并發(fā)運(yùn)行5個線程,每個線程添加/獲取500K個元素。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

public class CrunchifyConcurrentHashMapVsSynchronizedMap {

?

????public final static int THREAD_POOL_SIZE = 5;

?

????public static Map<String, Integer> crunchifyHashTableObject = null;

????public static Map<String, Integer> crunchifySynchronizedMapObject = null;

????public static Map<String, Integer> crunchifyConcurrentHashMapObject = null;

?

????public static void main(String[] args) throws InterruptedException {

?

????????// Test with Hashtable Object

????????crunchifyHashTableObject = new Hashtable<>();

????????crunchifyPerformTest(crunchifyHashTableObject);

?

????????// Test with synchronizedMap Object

????????crunchifySynchronizedMapObject = Collections.synchronizedMap(new HashMap<String, Integer>());

????????crunchifyPerformTest(crunchifySynchronizedMapObject);

?

????????// Test with ConcurrentHashMap Object

????????crunchifyConcurrentHashMapObject = new ConcurrentHashMap<>();

????????crunchifyPerformTest(crunchifyConcurrentHashMapObject);

?

????}

?

????public static void crunchifyPerformTest(final Map<String, Integer> crunchifyThreads) throws InterruptedException {

?

????????System.out.println("Test started for: " + crunchifyThreads.getClass());

????????long averageTime = 0;

????????for (int i = 0; i < 5; i++) {

?

????????????long startTime = System.nanoTime();

????????????ExecutorService crunchifyExServer = Executors.newFixedThreadPool(THREAD_POOL_SIZE);

?

????????????for (int j = 0; j < THREAD_POOL_SIZE; j++) {

????????????????crunchifyExServer.execute(new Runnable() {

????????????????????@SuppressWarnings("unused")

????????????????????@Override

????????????????????public void run() {

?

????????????????????????for (int i = 0; i < 500000; i++) {

????????????????????????????Integer crunchifyRandomNumber = (int) Math.ceil(Math.random() * 550000);

?

????????????????????????????// Retrieve value. We are not using it anywhere

????????????????????????????Integer crunchifyValue = crunchifyThreads.get(String.valueOf(crunchifyRandomNumber));

?

????????????????????????????// Put value

????????????????????????????crunchifyThreads.put(String.valueOf(crunchifyRandomNumber), crunchifyRandomNumber);

????????????????????????}

????????????????????}

????????????????});

????????????}

?

????????????// Make sure executor stops

????????????crunchifyExServer.shutdown();

?

????????????// Blocks until all tasks have completed execution after a shutdown request

????????????crunchifyExServer.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);

?

????????????long entTime = System.nanoTime();

????????????long totalTime = (entTime - startTime) / 1000000L;

????????????averageTime += totalTime;

????????????System.out.println("2500K entried added/retrieved in " + totalTime + " ms");

????????}

????????System.out.println("For " + crunchifyThreads.getClass() + " the average time is " + averageTime / 5 + " ms\n");

????}

}

測試結(jié)果:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

Test started for: class java.util.Hashtable

2500K entried added/retrieved in 2018 ms

2500K entried added/retrieved in 1746 ms

2500K entried added/retrieved in 1806 ms

2500K entried added/retrieved in 1801 ms

2500K entried added/retrieved in 1804 ms

For class java.util.Hashtable the average time is 1835 ms

?

Test started for: class java.util.Collections$SynchronizedMap

2500K entried added/retrieved in 3041 ms

2500K entried added/retrieved in 1690 ms

2500K entried added/retrieved in 1740 ms

2500K entried added/retrieved in 1649 ms

2500K entried added/retrieved in 1696 ms

For class java.util.Collections$SynchronizedMap the average time is 1963 ms

?

Test started for: class java.util.concurrent.ConcurrentHashMap

2500K entried added/retrieved in 738 ms

2500K entried added/retrieved in 696 ms

2500K entried added/retrieved in 548 ms

2500K entried added/retrieved in 1447 ms

2500K entried added/retrieved in 531 ms

For class java.util.concurrent.ConcurrentHashMap the average time is 792 ms

這個就不用廢話了,CHM性能是明顯優(yōu)于Hashtable和SynchronizedMap的,CHM花費(fèi)的時間比前兩個的一半還少,哈哈,以后再有人問就可以甩數(shù)據(jù)了。

?

原文鏈接:https://yemengying.com/2016/05/07/threadsafe-hashmap/

《新程序員》:云原生和全面數(shù)字化實(shí)踐50位技術(shù)專家共同創(chuàng)作,文字、視頻、音頻交互閱讀

總結(jié)

以上是生活随笔為你收集整理的如何线程安全的使用HashMap的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。