回覆列表
  • 1 # 小紅的甜心

    在平時開發中,我們經常採用HashMap來作為本地快取的一種實現方式,將一些如系統變數等資料量比較少的引數儲存在HashMap中,並將其作為單例類的一個屬性。在系統執行中,使用到這些快取資料,都可以直接從該單例中獲取該屬性集合。但是,最近發現,HashMap並不是執行緒安全的,如果你的單例類沒有做程式碼同步或物件鎖的控制,就可能出現異常。

    首先看下在多執行緒的訪問下,非現場安全的HashMap的表現如何,在網上看了一些資料,自己也做了一下測試:

    1public class MainClass {

    2

    3 public static final HashMap<String, String> firstHashMap=new HashMap<String, String>();

    4

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

    6

    7 //執行緒一

    8 Thread t1=new Thread(){

    9 public void run() {

    10 for(int i=0;i<25;i++){

    11 firstHashMap.put(String.valueOf(i), String.valueOf(i));

    12 }

    13 }

    14 };

    15

    16 //執行緒二

    17 Thread t2=new Thread(){

    18 public void run() {

    19 for(int j=25;j<50;j++){

    20 firstHashMap.put(String.valueOf(j), String.valueOf(j));

    21 }

    22 }

    23 };

    24

    25 t1.start();

    26 t2.start();

    27

    28 //主執行緒休眠1秒鐘,以便t1和t2兩個執行緒將firstHashMap填裝完畢。

    29 Thread.currentThread().sleep(1000);

    30

    31 for(int l=0;l<50;l++){

    32 //如果key和value不同,說明在兩個執行緒put的過程中出現異常。

    33 if(!String.valueOf(l).equals(firstHashMap.get(String.valueOf(l)))){

    34 System.err.println(String.valueOf(l)+":"+firstHashMap.get(String.valueOf(l)));

    35 }

    36 }

    37

    38 }

    39

    40}

    上面的程式碼在多次執行後,發現表現很不穩定,有時沒有異常文案打出,有時則有個異常出現:

    為什麼會出現這種情況,主要看下HashMap的實現:

    1public V put(K key, V value) {

    2 if (key == null)

    3 return putForNullKey(value);

    4 int hash = hash(key.hashCode());

    5 int i = indexFor(hash, table.length);

    6 for (Entry<K,V> e = table[i]; e != null; e = e.next) {

    7 Object k;

    8 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {

    9 V oldValue = e.value;

    10 e.value = value;

    11 e.recordAccess(this);

    12 return oldValue;

    13 }

    14 }

    15

    16 modCount++;

    17 addEntry(hash, key, value, i);

    18 return null;

    19 }

    我覺得問題主要出現在方法addEntry,繼續看:

    1void addEntry(int hash, K key, V value, int bucketIndex) {

    2 Entry<K,V> e = table[bucketIndex];

    3 table[bucketIndex] = new Entry<K,V>(hash, key, value, e);

    4 if (size++ >= threshold)

    5 resize(2 * table.length);

    6 }

    從程式碼中,可以看到,如果發現雜湊表的大小超過閥值threshold,就會呼叫resize方法,擴大容量為原來的兩倍,而擴大容量的做法是新建一個Entry[]:

    1void resize(int newCapacity) {

    2 Entry[] oldTable = table;

    3 int oldCapacity = oldTable.length;

    4 if (oldCapacity == MAXIMUM_CAPACITY) {

    5 threshold = Integer.MAX_VALUE;

    6 return;

    7 }

    8

    9 Entry[] newTable = new Entry[newCapacity];

    10 transfer(newTable);

    11 table = newTable;

    12 threshold = (int)(newCapacity * loadFactor);

    13 }

    一般我們宣告HashMap時,使用的都是預設的構造方法:HashMap<K,V>,看了程式碼你會發現,它還有其它的構造方法:HashMap(int initialCapacity, float loadFactor),其中引數initialCapacity為初始容量,loadFactor為載入因子,而之前我們看到的threshold = (int)(capacity * loadFactor); 如果在預設情況下,一個HashMap的容量為16,載入因子為0.75,那麼閥值就是12,所以在往HashMap中put的值到達12時,它將自動擴容兩倍,如果兩個執行緒同時遇到HashMap的大小達到12的倍數時,就很有可能會出現在將oldTable轉移到newTable的過程中遇到問題,從而導致最終的HashMap的值儲存異常。

    當多個執行緒同時檢測到總數量超過門限值的時候就會同時呼叫resize操作,各自生成新的陣列並rehash後賦給該map底層的陣列table,結果最終只有最後一個執行緒生成的新陣列被賦給table變數,其他執行緒的均會丟失。而且當某些執行緒已經完成賦值而其他執行緒剛開始的時候,就會用已經被賦值的table作為原始陣列,這樣也會有問題。

    JDK1.0引入了第一個關聯的集合類HashTable,它是執行緒安全的。HashTable的所有方法都是同步的。

    JDK2.0引入了HashMap,它提供了一個不同步的基類和一個同步的包裝器synchronizedMap。synchronizedMap被稱為有條件的執行緒安全類。

    JDK5.0util.concurrent包中引入對Map執行緒安全的實現ConcurrentHashMap,比起synchronizedMap,它提供了更高的靈活性。同時進行的讀和寫操作都可以併發地執行。

    所以在開始的測試中,如果我們採用ConcurrentHashMap,它的表現就很穩定,所以以後如果使用Map實現本地快取,為了提高併發時的穩定性,還是建議使用ConcurrentHashMap。

  • 2 # 程式猿W
    HashMap 擴容的原始碼如下:

    resize 擴容方法中最重要的程式碼如下:

    resize 擴容步驟如下:

    根據 newCapacity 生成一個數組。遍歷舊的陣列,然後對其中的每一個值進行hash,重新進行插入。修改擴容的閥值:threshold 。

    下面我們分別展示 在單執行緒和多執行緒的環境的擴容

    我們定義的Map為 Map<Integer,String>

    單執行緒環境下的擴容

    我們先定義有個簡單的hash, hash = key%length

    預設hash表的長度為2,插入的元素為 5 9 11

    5%2 = 1;

    9%2 = 1;

    11%2=1;

    三個元素都碰撞在下標為1 的位置上。

    下面我們擴容到4:

    5%4 = 1;

    9%4 = 1;

    11%4=3;

    擴容步驟如下:

    併發環境下的擴容

    首先執行緒1 和 執行緒2 同時擴容

    執行緒1 和 執行緒2 的 e 為 5 。e.next = 9

    但是此時,執行緒1 由於排程問題暫停執行。

    執行緒2繼續執行,執行結束後如下:

    這時,執行緒1被喚醒了。這時執行緒1的e為5,e.next = 9;

    此時:

    處理完5後,我們就要處理9。

    此時

    因為線上程2中9的下一個節點為5,所以還要繼續處理5,會把5放到執行緒1的table[1] 處,

    這時就會迴圈生成一個迴圈列表。11這個元素時無法加入到執行緒1裡面了。

  • 中秋節和大豐收的關聯?
  • 瑟的瑟可以組什麼詞?