併發下的集合不安全問題

序言

由於最近項目上遇到了高併發問題,而自己對高併發,多線程這裏的知識點相對薄弱,尤其是基礎,所以想系統的學習一下,以後可能會出一系列的JUC文章及總結 ,同時也爲企業級的高併發項目做好準備。

本文是JUC文章的第三篇,如想看以往關於JUC文章,請點擊JUC系列總結

此係列文章的總結思路大致分爲三部分:

  1. 理論(概念);
  2. 實踐(代碼證明);
  3. 總結(心得及適用場景)。

在這裏提前說也是爲了防止大家看着看着就迷路了。

備註:本文的閱讀需要Volatile、CAS、Synchronized以及集合原理部分知識閱讀最佳。

集合不安全問題大綱

集合不安全問題.png


ArrayList

我們都知道ArrayList是線程不安全,但是具體的不安全體現在哪?或者說你用代碼去證明他的不安全性以及他的解決方案。

不安全原因

ArrayList是非線性安全,具體現在多線程環境下,對集合的操作:

  1. 一方線程在遍歷列表,另一方線程在修改列表時,會報ConcurrentModificationException。
  2. 多線程插入操作,即add方法,由於沒有同步操作,容易丟失數據,同時也可能出現索引越界異常(ArrayIndexOfBoundsException)。
 public boolean add(E e) {
	ensureCapacity(size + 1);  // Increments modCount!!
	elementData[size++] = e;//使用了size++操作,會產生多線程數據丟失問題。
	return true;
    }

而對於多線程操作問題,其最本質的問題就是通過鎖來解決。大致分爲3種解決方案:

  1. 使用VectorArrayList所有方法加synchronized,鎖作用範圍爲方法,比較重)。
  2. 使用Collections.synchronizedList()轉換成線程安全類(鎖作用範圍爲代碼塊)。
  3. 使用java.concurrent.CopyOnWriteArrayList(分場景使用)。

代碼證明不安全問題

併發修改異常證明

public class ArrayListNoSafe {

    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < 8; i++) {
            new Thread(() ->{
                list.add(UUID.randomUUID().toString().substring(0,8));
                System.out.println(Thread.currentThread().getName() + "\t" + list);
            },String.valueOf(i)).start();
        }
    }
}

出現了邊遍歷,邊修改的情況,結果就可能如下(拋出ConcurrentModificationException異常):

1	[3f790bed, b0566629]
7	[3f790bed, b0566629, 74fa619f, 1d4d9f94, d125b6c6, 0c2dee3f, ae9c6ce2, edb884c7]
4	[3f790bed, b0566629, 74fa619f, 1d4d9f94, d125b6c6, 0c2dee3f, ae9c6ce2]
5	[3f790bed, b0566629, 74fa619f, 1d4d9f94, d125b6c6, 0c2dee3f]
2	[3f790bed, b0566629, 74fa619f, 1d4d9f94, d125b6c6]
6	[3f790bed, b0566629, 74fa619f]
3	[3f790bed, b0566629]
Exception in thread "0" java.util.ConcurrentModificationException
	...省略

多線程環境下的添加操作,出現數據遺漏(其實最本質的問題就是size++)

public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(() ->{
                list.add(UUID.randomUUID().toString().substring(0,8));
            },String.valueOf(i)).start();
        }
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.err.println("=====長度:"+list.size());
    }

輸出結果:

=====長度:9997

其實不止會出現數據遺漏情況,有時候還會出現索引越界情況 ,原因是因爲:

  1. 線程A進入add方法,還未進行數組擴容,就被掛起;
  2. 線程B進入add方法,並完成了整個流程,但由於數組未擴容,但是索引已+1,所以會拋出索引越界異常;

輸出結果:

Exception in thread "4012" java.lang.ArrayIndexOutOfBoundsException: 4164
	at java.util.ArrayList.add(ArrayList.java:463)
	at com.company.thread.collection.ArrayListNoSafe.lambda$main$0(ArrayListNoSafe.java:31)
	at java.lang.Thread.run(Thread.java:748)
=====長度:9992

解決方案1 Vector

Vector之所以可以保證線程安全,是因爲它對所有操作都加上了synchronized關鍵字(注意:它鎖定的範圍時方法級別的),將整個方法都鎖住。但它的本質還是ArrayList,就是簡單粗暴的加上了synchronized,這種方式嚴重影響效率,而且在拓展性方面也不如Collections.SynchronizedList,僅作用於ArrayList。因此,不推薦使用Vector

Stackoverflow當中有這樣的描述:Why is Java Vector class considered obsolete or deprecated?

但是作爲ArrayList不安全的解決方式中,我們還是有必要說一下。代碼也很簡單,我們在這就不演示了。

解決方案2 Collections.SynchronizedList

與Vector相同的是它也加了synchronized關鍵字,但是最大不同的是它的synchronized鎖定的範圍是代碼塊,鎖定的是構造函數傳進來的list對象。(不僅限於ArrayList)

我們來翻閱一下源碼:

publicstatic <T> List<T> synchronizedList(List<T> list) {
    return (list instanceof RandomAccess ?
            //ArrayList使用了SynchronizedRandomAccessList類
            new SynchronizedRandomAccessList<>(list) :
            new SynchronizedList<>(list));
}

通過源碼可知,它其實就是通過 synchronizedList 靜態方法將一個非線程安全的List(並不僅限ArrayList)包裝爲線程安全的List。

SynchronizedList方法如下:

//SynchronizedRandomAccessList繼承自SynchronizedList
static class SynchronizedRandomAccessList<E> extends SynchronizedList<E> implements RandomAccess {
}

//SynchronizedList對代碼塊進行了synchronized修飾來實現線程安全性
static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> {
    final List<E> list;
    SynchronizedList(List<E> list) {
        super(list);
        this.list = list;
    }
    SynchronizedList(List<E> list, Object mutex) {
        super(list, mutex);
        this.list = list;
    }
    public E get(int index) {
    	synchronized (mutex) {return list.get(index);}
    }
    public E set(int index, E element) {
        synchronized (mutex) {return list.set(index, element);}
    }
    public void add(int index, E element) {
        synchronized (mutex) {list.add(index, element);}
    }
    public E remove(int index) {
        synchronized (mutex) {return list.remove(index);}
    }   
    
    //迭代操作並未加鎖,所以需要手動同步
    public ListIterator<E> listIterator() {
            return list.listIterator(); 
    }
}

Collections.synchronizedList生成了特定同步的SynchronizedCollection,生成的集合每個同步操作都是持有mutex這個鎖,而這個mutex即構造函數傳入的list,所以再進行操作時就是線程安全的集合了。

注意:這裏需要注意一個地方:

  1. 迭代操作必須加鎖,可以使用synchronized關鍵字修飾;
  2. synchronized持有的監視器對象必須是synchronized (list),即包裝後的list,使用其他對象如synchronized (new Object())會使add,remove等方法與迭代方法使用的鎖不一致,無法實現完全的線程安全性。

解決ArrayList不安全的代碼:

	public static void main(String[] args) {
        collectionsSynchronizedListTest();
	}
	//collection.synchronizedList安全性測試
	private static void collectionsSynchronizedListTest() {
        List<String> list = Collections.synchronizedList(new ArrayList<String>());
        for (int i = 0; i < 80; i++) {
            new Thread(() ->{
                list.add(UUID.randomUUID().toString().substring(0,8));
                System.out.println(Thread.currentThread().getName()+"\t"+list);
            },String.valueOf(i)).start();
        }
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("list的長度====================="+list.size());
        collectionsSynchronizedListIteratorTest(list);
    }
	//collections.synchronizedList的遍歷
    private static void collectionsSynchronizedListIteratorTest(List list) {
        synchronized (list){
            Iterator iterator = list.iterator();
            while (iterator.hasNext()){
                System.err.print(iterator.next() + ",");
            }
        }
    }

在這裏提醒一下,增強for循壞其實底層也是調用的迭代器…

解決方案3 copyOnWriteArrayList

顧名思義,CopyOnWrite~ArrayList,寫時複製,讀寫分離。即在進行寫操作(add,remove,set等)時會進行Copy操作,可以推測出在進行寫操作時CopyOnWriteArrayList性能應該不會很高,從而進一步可以推出它是適合在讀多寫少的情況下使用。

再者,我們可以發現這個類的所在包爲 java.util.concurrent,可想而知,這個類是爲併發而設計的.

爲了防止看源碼看的迷路,在這裏先提前總結一下其原理

通過寫時複製來實現讀寫分離。比如其add()方法,就是先複製一個新數組,長度爲原數組長度+1,然後將新數組最後一個元素設爲添加的元素。而讀操作(讀到的是修改前的數組)並沒有對數組修改,不會產生線程安全問題。但同時也會帶來一個新的問題,就是數據的一致性

線程1讀取集合裏面的數據,然後被掛起,線程2、線程3、線程4四個線程都修改/刪除了CopyOnWriteArrayList裏面的數據,操作完成後此時線程1被喚醒(由於太快了,volatile還未及時刷新主內存的值),線程1拿到的還是最老的那個Object[] array,此時可能會造成索引越界異常。所以線程1讀取的內容未必準確。

所以結論爲:在不要求數據實時一致性的情況下,讀不加鎖,寫加鎖,使用copyOnWriteArrayList以提高性能。

接下來,我們來看一下它的結構:

public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    //lock鎖
	final transient ReentrantLock lock = new ReentrantLock();
	//volatile保證可見性,一旦有線程修改,即可見。
    private transient volatile Object[] array;

    final Object[] getArray() {
        return array;
    }
	
    final void setArray(Object[] a) {
        array = a;
    }
	//構造方法,賦予初始值
    public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }
}

可以看到CopyOnWriteArrayList底層跟ArrayList一樣,實現同爲Object[] array數組。

添加操作

public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            //在原先數組基礎之上新建長度+1的數組,並將原先數組當中的內容拷貝到新數組當中。
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            //給最後一個元素設值
            newElements[len] = e;
            //對新數組進行賦值
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

可以看到每次添加元素時都會進行Arrays.copyOf操作,代價非常昂貴。

讀操作

 public E get(int index) {
        return (E)(getArray()[index]);
    }

讀的時候是不需要加鎖的,直接獲取。刪除和增加是需要加鎖的。

拓展

Vector與SynchronizedList的區別

  1. SynchronizedList它是對list集合的包裝類,所以在擴容上是List的擴容機制,即1.5倍,而vector擴容爲原來的2倍;
  2. SynchronizedList不止作用於ArrayList,它的作用域在整個List集合類。而vector相當於是線程安全下的ArrayList(因爲其底層是object數組);
  3. SynchronizedList的遍歷並沒有加鎖,所以遍歷時需要同步處理;
  4. SynchronizedList的鎖作用域爲同步代碼塊,vector的鎖作用域是整個方法。

Vector、SynchronizedList和copyOnWriteArrayList的效率對比

代補充

Vector、SynchronizedList和copyOnWriteArrayList的使用場景

  1. vector在所有的方法添加了synchronized關鍵字,併發性能差,不推薦使用;
  2. CopyOnWriteArrayList寫時複製,讀時沒有鎖,會出現數據性一致問題,所以在不要求數據實時一致性的情況下,使用copyOnWriteArrayList以提高性能;
  3. Collections.synchronizedList在寫多的情況下使用,但需要注意迭代操作未加鎖(其實還有一種場景,List的其他子類需要線程安全的條件下)。

HashSet

其實熟讀過源碼的同學都知道,HashSet底層是由HashMap實現 ,值存放於HashMap的key上 ,HashMap的value統一爲PRESENT

而對於它不安全問題的原因我們會在HashMap處做詳細的解釋。

解決不安全問題可以通過Collections.synchronizedSet去解決,在這裏,其原理跟上面的差不多,在這裏我們就不做詳述了。

HashMap

不安全原因

我們先說結論(以防在後面的介紹中迷失自我~~~)

這裏需要分以下JDK版本:

JDK1.7版本其中有兩點:

  1. 多線程環境操作下,在resize擴容的過程中,由於採用的是頭插法,所以會導致環形鏈表的產生;
  2. get方法的不可見性;(即上一秒put完值,下一秒get值,所get到的值不是最新值。)

JDK1.8版本:

1. 由於1.8版本將頭插法替換爲尾插法,所以環形鏈表問題解決;
2. **最主要的就體現在get方法這裏。**

接下來我們來詳細探討一下其多線程環境下的操作的操作細節,主要從以下思路講解:

  1. 什麼是頭插法,什麼尾插法;
  2. 爲什麼會形成環形鏈表;
  3. 數據的不一致性。

頭插法與尾插法

頭插法

20180926181043191.png

如上圖,這些過程可總結爲兩句話:

  1. 將頭指針指向下一結點的地址賦給新增結點的next;
  2. 再將新增節點的地址賦值給頭指針的下一個結點。
	node->next = head->next;
	head->next = node; 

通俗一點的白話就是說新來的值會取代原有的值,原有的值就順推到鏈表的下一個節點。

尾插法

20180926194847575.png

同樣的,總結爲:

從新增第二個結點開始,尾指針總指向下一個節點next,最後一結點指向null;

白話就是說新來的值會順序的添加到鏈表中。

爲什麼會形成環形鏈表(JDK1.7版本)

首先我們先來了解以下hashmap的擴容機制(resize過程):

分爲兩步:

  1. 創建一個新的Entry空數組,長度是原來的兩倍。(因爲hashmap的擴容機制是2^n);

  2. ReHash:由於數組長度改變,Hash的規則也隨之改變(公式爲hash&length-1),相當於重新將數據插入一遍。

    其實我所理解的ReHash,其實在另一方面是爲了減少hash衝突(減少鏈表長度),因爲rehash之後,每個桶上的節點數一定小於等於原來桶上的節點數。

然而其實問題也是出現在這個Resize,舉個例子把:

假設我們現在往一個容量大小爲2的put兩個值,負載因子是0.75,閥值:2*0.75 = 1,所以我們在put第二個的時候就會進行resize。

然後我們現在用不同線程插入A,B,C,在未進行resize之前,我們看到的可能是這個樣子的:

鏈表的指向A->B->C

A的下一個指針是指向B的

640.webp

因爲resize的賦值方式,也就是使用了單鏈表的頭插入方式,同一位置上新元素總會被放在鏈表的頭部位置,在舊數組中同一條Entry鏈上的元素,通過重新計算索引位置後,有可能被放到了新數組的不同位置上。

就可能出現下面的情況,大家發現問題沒有?

B的下一個指針指向了A

640-1591520413511.webp

一旦幾個線程都調整完成,就可能出現環形鏈表

640-1591520477693.webp

如果這個時候去取值,悲劇就出現了——Infinite Loop。

而如果使用尾插法的話,在擴容時會保持鏈表元素原本的順序,就不會出現鏈表成環的問題了。

解決方案1 collections.SynchronizedMap

原理與上述Collections.SynchronizedList相同,此處略過;

解決方案2 HashTable

其實感覺跟Vector有點類似,直接在方法上添加了Synchronized關鍵字,鎖住了整個方法,併發度很低。

[圖片來源]:https://cloud.tencent.com/developer/article/1447127

cq1m9nzc42.png

解決方案3 ConcurrentHashMap

首先,在說concurrentHashMap之前,我們首先需要了解到它JDK1.7版本跟1.8版本還是有很大區別的。

JDK1.7:

採用分段鎖segment實現,通過繼承ReentrantLock做同步處理,並在HashEntry部分元素結點處添加volatile關鍵字。底層爲數組+單鏈表的結構。

JDK1.8:

去除了segement機制,改爲了CAS與synchronized結合的同步方式,並在node部分元素結點處添加volatile關鍵字,保證了get方法時的可見性,(因爲get方法沒有添加鎖)。底層爲數組+單鏈表+紅黑樹的結構。

ConcurrentHashMap原理簡述

在這裏,我們不做大篇幅的原理介紹,如您對源碼感興趣,可自行查閱相關文章。

JDK1.7:

[圖片來源]:https://cloud.tencent.com/developer/article/1447127

0jmcnifo7n.png

對於1.7版本的ConcurrentHashMap來講,它的併發度爲16,因爲它最多隻允許創建16個segment。

put操作如下

首先它會先定位到具體的segment,然後進行添加元素操作

public V put(K key, V value) {
        Segment<K,V> s;
        //concurrentHashMap不允許key/value爲空
        if (value == null)
            throw new NullPointerException();
        //hash函數對key的hashCode重新散列,避免差勁的不合理的hashcode,保證散列均勻
        int hash = hash(key);
        //定位segment
        int j = (hash >>> segmentShift) & segmentMask;
        if ((s = (Segment<K,V>)UNSAFE.getObject          
             (segments, (j << SSHIFT) + SBASE)) == null) 
            s = ensureSegment(j);
        return s.put(key, hash, value, false);
    }

接下來時segment內部的put方法:

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
            HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);//此處是一個自旋操作,嘗試獲取鎖,其嘗試次數由MAX_SCAN_RETRIES 控制,如果超過該值,則改爲阻塞獲取。
            V oldValue;
            try {
                HashEntry<K,V>[] tab = table;
                int index = (tab.length - 1) & hash;//定位HashEntry
                HashEntry<K,V> first = entryAt(tab, index);
                for (HashEntry<K,V> e = first;;) {
                    if (e != null) {
                        K k;
                         // 遍歷該 HashEntry,如果不爲空則判斷傳入的 key 和當前遍歷的 key 是否相等,相等則覆蓋舊的 value。
                        if ((k = e.key) == key ||
                            (e.hash == hash && key.equals(k))) {
                            oldValue = e.value;
                            if (!onlyIfAbsent) {
                                e.value = value;
                                ++modCount;
                            }
                            break;
                        }
                        e = e.next;
                    }
                    else {
                        // 不爲空則需要新建一個 HashEntry 並加入到 Segment 中
                        if (node != null)
                            node.setNext(first);
                        else
                            node = new HashEntry<K,V>(hash, key, value, first);
                        //若c超出閾值threshold,需要擴容並rehash。
                        int c = count + 1;              
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                            rehash(node);
                        else
                            setEntryAt(tab, index, node);
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
                unlock();
            }
            return oldValue;
        }

get方法

public V get(Object key) {
        Segment<K,V> s; 
        HashEntry<K,V>[] tab;
        int h = hash(key);
        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;        //先定位Segment,再定位HashEntry
        if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
            (tab = s.table) != null) {
            for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
                     (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
                 e != null; e = e.next) {
                K k;
                if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                    return e.value;
            }
        }
        return null;
    }

將 Key 通過 Hash 之後定位到具體的 Segment ,再通過一次 Hash 定位到具體的元素上。

由於 HashEntry 中的 value 屬性是用 volatile 關鍵詞修飾的,保證了內存可見性,所以每次獲取時都是最新值。所以get在這裏是無鎖狀態的。

JDK1.8

[圖片來源]:https://cloud.tencent.com/developer/article/1447127

gwu3e99ry0.png

JDK1.8ConcurrentHashMap拋棄了原有的 Segment 分段鎖,而採用了 CAS + synchronized 來保證併發安全性。

跟1.8的HashMap很像,也把之前的HashEntry改成了Node,但是作用不變,把值和next採用了volatile去修飾,保證了可見性,並且也引入了紅黑樹,在鏈表大於一定值的時候會轉換(默認是8)。

注意這裏的synchronized,只鎖定了當前鏈表或紅黑二叉樹的首節點,也就是說只有發生了hash不衝突,纔會觸發synchronized同步機制。在效率上又會有提升。

put操作如下:

  1. 根據 key 計算出 hashcode 。
  2. 判斷是否需要進行初始化。
  3. 即爲當前 key 定位出的 Node,如果爲空表示當前位置可以寫入數據,利用 CAS 嘗試寫入,失敗則自旋保證成功。
  4. 如果當前位置的 hashcode == MOVED == -1,則需要進行擴容。
  5. 如果都不滿足,則利用 synchronized 鎖寫入數據。
  6. 如果數量大於 TREEIFY_THRESHOLD 則要轉換爲紅黑樹。
 final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();//K,V都不能爲空,否則的話跑出異常
        int hash = spread(key.hashCode());    //取得key的hash值
        int binCount = 0;    //用來計算在這個節點總共有多少個元素,用來控制擴容或者轉移爲樹
        for (Node<K,V>[] tab = table;;) {    //
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)    
                tab = initTable();    //第一次put的時候table沒有初始化,則初始化table
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {    //通過哈希計算出一個表中的位置因爲n是數組的長度,所以(n-1)&hash肯定不會出現數組越界
                if (casTabAt(tab, i, null,        //如果這個位置沒有元素的話,則通過cas的方式嘗試添加,注意這個時候是沒有加鎖的
                             new Node<K,V>(hash, key, value, null)))        //創建一個Node添加到數組中區,null表示的是下一個節點爲空
                    break;                   // no lock when adding to empty bin
            }
            /*
             * 如果檢測到某個節點的hash值是MOVED,則表示正在進行數組擴張的數據複製階段,
             * 則當前線程也會參與去複製,通過允許多線程複製的功能,一次來減少數組的複製所帶來的性能損失
             */
            else if ((fh = f.hash) == MOVED)    
                tab = helpTransfer(tab, f);
            else {
                /*
                 * 如果在這個位置有元素的話,就採用synchronized的方式加鎖,
                 *     如果是鏈表的話(hash大於0),就對這個鏈表的所有元素進行遍歷,
                 *         如果找到了key和key的hash值都一樣的節點,則把它的值替換到
                 *         如果沒找到的話,則添加在鏈表的最後面
                 *  否則,是樹的話,則調用putTreeVal方法添加到樹中去
                 *  
                 *  在添加完之後,會對該節點上關聯的的數目進行判斷,
                 *  如果在8個以上的話,則會調用treeifyBin方法,來嘗試轉化爲樹,或者是擴容
                 */
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {        //再次取出要存儲的位置的元素,跟前面取出來的比較
                        if (fh >= 0) {                //取出來的元素的hash值大於0,當轉換爲樹之後,hash值爲-2
                            binCount = 1;            
                            for (Node<K,V> e = f;; ++binCount) {    //遍歷這個鏈表
                                K ek;
                                if (e.hash == hash &&        //要存的元素的hash,key跟要存儲的位置的節點的相同的時候,替換掉該節點的value即可
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)        //當使用putIfAbsent的時候,只有在這個key沒有設置值得時候才設置
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {    //如果不是同樣的hash,同樣的key的時候,則判斷該節點的下一個節點是否爲空,
                                    pred.next = new Node<K,V>(hash, key,        //爲空的話把這個要加入的節點設置爲當前節點的下一個節點
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {    //表示已經轉化成紅黑樹類型了
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,    //調用putTreeVal方法,將該元素添加到樹中去
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)    //當在同一個節點的數目達到8個的時候,則擴張數組或將給節點的數據轉爲tree
                        treeifyBin(tab, i);    
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);    //計數
        return null;
    }

拓展

HashTable與HashMap的區別?

  1. 散列方式
    • hashMap採用的是hash&(length-1);
    • hashtable採用的取模運算;
  2. 容器整體結構
    • hashmap中的key和value值都允許爲null;1.7版本底層爲數組+鏈表,1.8版本之後則爲數組+鏈表+紅黑樹;
    • hashtable的key和value值都不允許爲null,否則返回NullPointerException;底層爲數組+鏈表。
  3. 擴容機制
    • hashmap默認初始化容量爲16,容器容量一定是2的N次方;
    • hashtable默認初始化容量爲11,擴容是以原容量的2倍+1擴容;
  4. 線程安全方面
    • hashtable的操作方法都帶有synchronized關鍵字修飾,爲線程安全;
    • hashmap爲線程不安全。

Collections.synchronizedMap、HashTable與ConcurrentHashMap的使用場景

首先,我認爲如果在併發度要求比較高的情況下,數據的一致性不是強一致性的話,首選ConcurrentHashMap。

相反的,對於強一致性問題,還是選擇hashTable,因爲在同一時間內,也只能讓一個線程操作。

至於Collections.synchronizedMap,其實我是糾結的,或者說有點不確定性,因爲我覺得它應該也可以保證強一致性,因爲它鎖住的當前對象,他對併發度的支持相對於hashTable來說,高一點。

如有大佬瞭解,或者我說的不正確,請聯繫我!!!!

總結

以上就是對集合不安全問題的總結了,其實要想完整的理解下來,對於初學者而言,難度還是很高的,其實有時候寫文章的時候,總在思考怎麼把文章寫的淺顯易懂,怎麼有條理性,其實我還是很排斥源碼的,因爲有時候讀者讀者讀者就會迷失。

這篇文章其實也花了很長的時間去寫,如有不正確的地方,歡迎大佬們來指正~~~

Reference

CopyOnWriteArrayList與Collections.synchronizedList的性能對比

吊打面試官》系列-HashMap

創建單鏈表的頭插法與尾插法詳解

ConcurrentHashMap實現原理

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章