序言
由於最近項目上遇到了高併發問題,而自己對高併發,多線程這裏的知識點相對薄弱,尤其是基礎,所以想系統的學習一下,以後可能會出一系列的JUC文章及總結 ,同時也爲企業級的高併發項目做好準備。
本文是JUC文章的第三篇,如想看以往關於JUC文章,請點擊JUC系列總結
此係列文章的總結思路大致分爲三部分:
- 理論(概念);
- 實踐(代碼證明);
- 總結(心得及適用場景)。
在這裏提前說也是爲了防止大家看着看着就迷路了。
備註:本文的閱讀需要Volatile、CAS、Synchronized以及集合原理部分知識閱讀最佳。
集合不安全問題大綱
ArrayList
我們都知道ArrayList是線程不安全,但是具體的不安全體現在哪?或者說你用代碼去證明他的不安全性以及他的解決方案。
不安全原因
ArrayList是非線性安全,具體現在多線程環境下,對集合的操作:
- 一方線程在遍歷列表,另一方線程在修改列表時,會報ConcurrentModificationException。
- 多線程插入操作,即add方法,由於沒有同步操作,容易丟失數據,同時也可能出現索引越界異常(ArrayIndexOfBoundsException)。
public boolean add(E e) {
ensureCapacity(size + 1); // Increments modCount!!
elementData[size++] = e;//使用了size++操作,會產生多線程數據丟失問題。
return true;
}
而對於多線程操作問題,其最本質的問題就是通過鎖來解決。大致分爲3種解決方案:
- 使用
Vector
(ArrayList
所有方法加synchronized
,鎖作用範圍爲方法,比較重)。 - 使用
Collections.synchronizedList()
轉換成線程安全類(鎖作用範圍爲代碼塊)。 - 使用
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
其實不止會出現數據遺漏情況,有時候還會出現索引越界情況 ,原因是因爲:
- 線程A進入add方法,還未進行數組擴容,就被掛起;
- 線程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,所以再進行操作時就是線程安全的集合了。
注意:這裏需要注意一個地方:
- 迭代操作必須加鎖,可以使用
synchronized
關鍵字修飾; - 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的區別
- SynchronizedList它是對list集合的包裝類,所以在擴容上是List的擴容機制,即1.5倍,而vector擴容爲原來的2倍;
- SynchronizedList不止作用於ArrayList,它的作用域在整個List集合類。而vector相當於是線程安全下的ArrayList(因爲其底層是object數組);
- SynchronizedList的遍歷並沒有加鎖,所以遍歷時需要同步處理;
- SynchronizedList的鎖作用域爲同步代碼塊,vector的鎖作用域是整個方法。
Vector、SynchronizedList和copyOnWriteArrayList的效率對比
代補充
Vector、SynchronizedList和copyOnWriteArrayList的使用場景
- vector在所有的方法添加了synchronized關鍵字,併發性能差,不推薦使用;
- CopyOnWriteArrayList寫時複製,讀時沒有鎖,會出現數據性一致問題,所以在不要求數據實時一致性的情況下,使用copyOnWriteArrayList以提高性能;
- Collections.synchronizedList在寫多的情況下使用,但需要注意迭代操作未加鎖(其實還有一種場景,List的其他子類需要線程安全的條件下)。
HashSet
其實熟讀過源碼的同學都知道,HashSet底層是由HashMap實現 ,值存放於HashMap的key上 ,HashMap的value統一爲PRESENT
而對於它不安全問題的原因我們會在HashMap處做詳細的解釋。
解決不安全問題可以通過Collections.synchronizedSet去解決,在這裏,其原理跟上面的差不多,在這裏我們就不做詳述了。
HashMap
不安全原因
我們先說結論(以防在後面的介紹中迷失自我~~~)
這裏需要分以下JDK版本:
JDK1.7版本其中有兩點:
- 多線程環境操作下,在resize擴容的過程中,由於採用的是頭插法,所以會導致環形鏈表的產生;
- get方法的不可見性;(即上一秒put完值,下一秒get值,所get到的值不是最新值。)
JDK1.8版本:
1. 由於1.8版本將頭插法替換爲尾插法,所以環形鏈表問題解決;
2. **最主要的就體現在get方法這裏。**
接下來我們來詳細探討一下其多線程環境下的操作的操作細節,主要從以下思路講解:
- 什麼是頭插法,什麼尾插法;
- 爲什麼會形成環形鏈表;
- 數據的不一致性。
頭插法與尾插法
頭插法
如上圖,這些過程可總結爲兩句話:
- 將頭指針指向下一結點的地址賦給新增結點的next;
- 再將新增節點的地址賦值給頭指針的下一個結點。
node->next = head->next;
head->next = node;
通俗一點的白話就是說新來的值會取代原有的值,原有的值就順推到鏈表的下一個節點。
尾插法
同樣的,總結爲:
從新增第二個結點開始,尾指針總指向下一個節點next,最後一結點指向null;
白話就是說新來的值會順序的添加到鏈表中。
爲什麼會形成環形鏈表(JDK1.7版本)
首先我們先來了解以下hashmap的擴容機制(resize過程):
分爲兩步:
-
創建一個新的Entry空數組,長度是原來的兩倍。(因爲hashmap的擴容機制是2^n);
-
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的
因爲resize的賦值方式,也就是使用了單鏈表的頭插入方式,同一位置上新元素總會被放在鏈表的頭部位置,在舊數組中同一條Entry鏈上的元素,通過重新計算索引位置後,有可能被放到了新數組的不同位置上。
就可能出現下面的情況,大家發現問題沒有?
B的下一個指針指向了A
一旦幾個線程都調整完成,就可能出現環形鏈表
如果這個時候去取值,悲劇就出現了——Infinite Loop。
而如果使用尾插法的話,在擴容時會保持鏈表元素原本的順序,就不會出現鏈表成環的問題了。
解決方案1 collections.SynchronizedMap
原理與上述Collections.SynchronizedList相同,此處略過;
解決方案2 HashTable
其實感覺跟Vector有點類似,直接在方法上添加了Synchronized關鍵字,鎖住了整個方法,併發度很低。
[圖片來源]:https://cloud.tencent.com/developer/article/1447127
解決方案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
對於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
JDK1.8ConcurrentHashMap拋棄了原有的 Segment 分段鎖,而採用了 CAS + synchronized
來保證併發安全性。
跟1.8的HashMap很像,也把之前的HashEntry改成了Node,但是作用不變,把值和next採用了volatile去修飾,保證了可見性,並且也引入了紅黑樹,在鏈表大於一定值的時候會轉換(默認是8)。
注意這裏的synchronized,只鎖定了當前鏈表或紅黑二叉樹的首節點,也就是說只有發生了hash不衝突,纔會觸發synchronized同步機制。在效率上又會有提升。
put操作如下:
- 根據 key 計算出 hashcode 。
- 判斷是否需要進行初始化。
- 即爲當前 key 定位出的 Node,如果爲空表示當前位置可以寫入數據,利用 CAS 嘗試寫入,失敗則自旋保證成功。
- 如果當前位置的
hashcode == MOVED == -1
,則需要進行擴容。 - 如果都不滿足,則利用 synchronized 鎖寫入數據。
- 如果數量大於
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的區別?
- 散列方式
- hashMap採用的是hash&(length-1);
- hashtable採用的取模運算;
- 容器整體結構
- hashmap中的key和value值都允許爲null;1.7版本底層爲數組+鏈表,1.8版本之後則爲數組+鏈表+紅黑樹;
- hashtable的key和value值都不允許爲null,否則返回NullPointerException;底層爲數組+鏈表。
- 擴容機制
- hashmap默認初始化容量爲16,容器容量一定是2的N次方;
- hashtable默認初始化容量爲11,擴容是以原容量的2倍+1擴容;
- 線程安全方面
- hashtable的操作方法都帶有synchronized關鍵字修飾,爲線程安全;
- hashmap爲線程不安全。
Collections.synchronizedMap、HashTable與ConcurrentHashMap的使用場景
首先,我認爲如果在併發度要求比較高的情況下,數據的一致性不是強一致性的話,首選ConcurrentHashMap。
相反的,對於強一致性問題,還是選擇hashTable,因爲在同一時間內,也只能讓一個線程操作。
至於Collections.synchronizedMap,其實我是糾結的,或者說有點不確定性,因爲我覺得它應該也可以保證強一致性,因爲它鎖住的當前對象,他對併發度的支持相對於hashTable來說,高一點。
如有大佬瞭解,或者我說的不正確,請聯繫我!!!!
總結
以上就是對集合不安全問題的總結了,其實要想完整的理解下來,對於初學者而言,難度還是很高的,其實有時候寫文章的時候,總在思考怎麼把文章寫的淺顯易懂,怎麼有條理性,其實我還是很排斥源碼的,因爲有時候讀者讀者讀者就會迷失。
這篇文章其實也花了很長的時間去寫,如有不正確的地方,歡迎大佬們來指正~~~