1,CopyOnWriteArrayList
CopyOnWriteArrayList是java1.5版本提供的一個線程安全的ArrayList變體,ArrayList具有fast-fail特性,它是值在遍歷過程中,如果ArrayList的內容發生過修改,那么會拋出ConcurrentModificationException。
在多線程環境下,這種情況變得尤為突出。不使用迭代器形式而使用下標來遍歷就會帶來一個問題,讀寫沒有分離。寫操作戶影響到讀的准確性,甚至導致IndexOutOfBoundsException。不直接遍歷list,而是把list拷貝一份數組,再行遍歷。
寫時拷貝,自然實在做寫操作時,把原始數據拷貝到一個新的數組,與寫操作相關的主要有三個方法:add、remove、set,在每一次add操作里,數組都被拷貝了一個副本,這就是寫時拷貝的原理,那么寫時拷貝和讀時拷貝各有什么優勢,如何選擇呢?
如果一個list的遍歷操作比寫入操作更頻繁,那么用該使用CopyOnWriteArrayList,如果list的寫入操作比遍歷操作更頻繁,那么應該考慮讀時拷貝。
2,ConcurrentHashMap
ConcurrentHashMap是Java5中支持高並發、高吞吐量的線程安全HashMap實現,它由Segment數組結構和HashEntry數組結構組成。Segment在ConcurrentHashMap里扮演鎖的角色,HashEntry則用於存儲鍵值對數據。。一個ConcurrentHashMap里包含一個Segment數組,Segment的結構和HashMap類似,是一種數組和鏈表結構,一個Segment守護着一個HashEntry數組里的元素,當對HashEntry數組的數據進行修改時,首先必須獲得它對應的Segment鎖。
HashTable和ConcurrentHashMap存儲的內容為鍵值對,且他們都是現成安全的容器,下面通過簡要介紹他們的實現方法來對比他們的不同點。
HashTable所有的方法都是同步的,因此,它是線程安全的。HashTable是通過拉鏈法實現的哈希表,因此他使用數組+鏈表的方式來存儲實際元素。
最頂部標數組的不封為一個Entry數組,而Entry又是一個鏈表,當項HashTable中插入數據的時候,首先通過鍵的hashcode和Enttry數組的長度來計算這個值應該存放在數組中的位置index,如果index對應的位置沒有存放值,那么直接存放到數組的index位置即可,當index有沖突的時候,則采用“拉鏈法”來解決沖突。假如想往HashTable中插入的值的index相同,則以鏈表的形式存儲。
HashTable繼承自Dictionary,為了使HashTable擁有比較好的性能,數組的大小也需要根據實際插入的數據的多少來進行動態的調整,Hashtable類中定義了一個rehash方法,該方法可以用來動態地擴充HashTable的容量,該方法被調用的時機為:Hashtable中的鍵值對超過某一閾值。默認情況下,該閾值等於HashTable中Entry數組長度為*0.75.HashTable的默認大小為11,當達到閾值后每次按照下面的公式對容量進行擴容:newCapacity=oldCapacity*2 + 1;
HashTable通過使用synchronized修飾方法來實現多線程同步。因此,HashTable的同步會鎖住整個數組,在高並發的情況下,性能會非常差,java5中引入java.util.concurrent.ConcurrentHashMap作為高吞吐量的線程安全HashMap實現,它采用鎖分離的技術允許多個修改鎖操作並發進行。

ConcurrentHashMap采用了更細粒度的鎖來提高在高並發情況下的效率。ConcurrentHashMap將HashMap表默認分為16個桶(每一個桶可以被看作是一個HashMap),大部分操作都沒有用到鎖,而對應的put、remove等操作也只需要鎖住當前線程需要用到的桶,而不需要鎖整個數據。采用這種設計方式以后,在大並發的情況下,同時可以有16個線程來訪問數據,顯然大大提供了並發性。

只有個別方法,如size和contailsValue方法可能需要鎖定整個表而不是僅僅某個桶,在實現的時候,需要按順序鎖定所有桶,操作完畢后,又按順序釋放所有桶,按順序的好處是能防止死鎖的發生。
假設一個線程在讀取數據的時候,另外一個線程在Hash鏈的中間添加或刪除元素或者修改某一個節點的值,此時必定會讀取到不一致的數據。那么如何才能實現在讀取時不加鎖而又不會讀取到不一致的數據呢?ConcurrentHashMap使用不變量的方式實現,它通過把Hash鏈中的節點HashEntry設計成幾乎不可能的方式來實現。
在HashEntry的定義中,除了value以外,其他變量都被定義final類型,因此,增加節點的操作只能在Hash鏈的頭部增加,對於刪除操作,則無法直接從Hash鏈的中間刪除節點,因為next也被定義為不可變量。因此,remove操作的實現方式如下所示:把需要刪除的節點前面的所有節點都復制一遍,然后把復制后的Hash鏈的最后一個節點指向待刪除節點的后繼節點,由此可以看出,ConcurrentHashMap刪除操作是比較耗時的,此外,使用volatile修改value的方式使這個值被修改了以后對所有線程都是可見的,采用這種方式的好處:一方面,避免了鎖;另一方面,如果把value也設計成不能變量,那么每次修改value的操作都必須刪除已有的節點,然后插入新的節點,顯然,此時的效率會非常低下。
由於volatile只能保證變量所有的寫操作都能立即反應到其他線程之中,也就是說volatile變量在各個線程中是一致的,但是由於volatile不能保證操作的原子性,因此它不是線程安全的。此處的線程安全指的是put(1,i++)這里i++操作,在進行這里操作時最好使用原子類的方法,保證多線程進行增值或減值時的線程安全。
3,ConcurrentHashMap在JDK1.7與1.8中的不同實現
對於JDK1.7

哈希桶的size:
因為size用位於運算來計算(ssize <<=1),所以Segment的大小取值都是以2的N次方,無關concurrencyLevel的取值,當然concurrencyLevel最大只能用16位的二進制來表示,即65536,換句話說,Segment的大小最多65536個,沒有指定concurrencyLevel元素初始化,Segment的大小ssize默認為 DEFAULT_CONCURRENCY_LEVEL =16;
多個線程一起put時候,currentHashMap如何操作:
對於ConcurrentHashMap的數據插入,這里要進行兩次Hash去定位數據的存儲位置,Segment實現了ReentrantLock,也就帶有鎖的功能,當執行put操作時,會進行第一次key的hash來定位Segment的位置,如果該Segment還沒有初始化,即通過CAS操作進行賦值,然后進行第二次hash操作,找到相應的HashEntry的位置,這里會利用繼承過來的鎖的特性,在將數據插入指定的HashEntry位置時(鏈表的尾端),會通過繼承ReentrantLock的tryLock()方法嘗試去獲取鎖,如果獲取成功就直接插入相應的位置,如果已經有線程獲取該Segment的鎖,那當前線程會以自旋的方式去繼續的調用tryLock()方法去獲取鎖,超過指定次數就掛起,等待喚醒。
計算size方式:
1、使用不加鎖的模式去嘗試多次計算ConcurrentHashMap的size,最多三次,比較前后兩次計算的結果,結果一致就認為當前沒有元素加入,計算的結果是准確的
2、如果步驟一失敗,他就會給每個Segment加上鎖,然后計算ConcurrentHashMap的size返回(美團面試官的問題,多個線程下如何確定size)
JDK1.8的實現

改進一:取消segments字段,直接采用transient volatile HashEntry<K,V>[] table保存數據,采用table數組元素作為鎖,從而實現了對每一行數據進行加鎖,進一步減少並發沖突的概率。
改進二:將原先table數組+單向鏈表的數據結構,變更為table數組+單向鏈表+紅黑樹的結構。對於hash表來說,最核心的能力在於將key hash之后能均勻的分布在數組中。如果hash之后散列的很均勻,那么table數組中的每個隊列長度主要為0或者1。但實際情況並非總是如此理想,雖然ConcurrentHashMap類默認的加載因子為0.75,但是在數據量過大或者運氣不佳的情況下,還是會存在一些隊列長度過長的情況,如果還是采用單向列表方式,那么查詢某個節點的時間復雜度為O(n);因此,對於個數超過8(默認值)的列表,jdk1.8中采用了紅黑樹的結構,那么查詢的時間復雜度可以降低到O(logN),可以改進性能。
JDK1.8的實現已經摒棄了Segment的概念,而是直接用Node數組+鏈表+紅黑樹的數據結構來實現,並發控制使用Synchronized和CAS來操作,整個看起來就像是優化過且線程安全的HashMap,雖然在JDK1.8中還能看到Segment的數據結構,但是已經簡化了屬性,只是為了兼容舊版本
JDK1.7與1.8中ConcurrentHashMap區別:
1.7的ReentrantLock+Segment+HashEntry,1.8中synchronized+CAS+HashEntry+紅黑樹,JDK1.8的實現降低鎖的粒度,JDK1.7版本鎖的粒度是基於Segment的,包含多個HashEntry,而JDK1.8鎖的粒度就是HashEntry(鏈表首節點)。
1.7中put和 get 兩次Hash到達指定的HashEntry,第一次hash到達Segment,第二次到達Segment里面的Entry,然后在遍歷entry鏈表。1.8取消了segment,只需一次hash。
1.7中計算size 先不加鎖計算3次,如果不對再給每個segment加鎖計算一次,在JDK1.8版本中,對於size的計算,在put的擴容和addCount()方法就已經計算好了,直接給你。
HashEntry最小容量為2,1.7中segment初始容量為16,1.8中Node節點轉TreeNode的閾值為8;
