java温故笔记(二)java的数组HashMap、ConcurrentHashMap、ArrayList、LinkedList
為什么80%的碼農都做不了架構師?>>> ??
HashMap
摘要
HashMap是Java程序員使用頻率最高的用于映射(鍵值對)處理的數據類型。隨著JDK(Java Developmet Kit)版本的更新,JDK1.8對HashMap底層的實現進行了優化,例如引入紅黑樹的數據結構和擴容的優化等。本文結合JDK1.7和JDK1.8的區別,深入探討HashMap的結構實現和功能原理。
簡介
Java為數據結構中的映射定義了一個接口java.util.Map,此接口主要有四個常用的實現類,分別是HashMap、Hashtable、LinkedHashMap和TreeMap,類繼承關系如下圖所示:
下面針對各個實現類的特點做一些說明:
(1) HashMap:它根據鍵的hashCode值存儲數據,大多數情況下可以直接定位到它的值,因而具有很快的訪問速度,但遍歷順序卻是不確定的。 HashMap最多只允許一條記錄的鍵為null,允許多條記錄的值為null。HashMap非線程安全,即任一時刻可以有多個線程同時寫HashMap,可能會導致數據的不一致。如果需要滿足線程安全,可以用 Collections的synchronizedMap方法使HashMap具有線程安全的能力,或者使用ConcurrentHashMap。
(2) Hashtable:Hashtable是遺留類,很多映射的常用功能與HashMap類似,不同的是它承自Dictionary類,并且是線程安全的,任一時間只有一個線程能寫Hashtable,并發性不如ConcurrentHashMap,因為ConcurrentHashMap引入了分段鎖。Hashtable不建議在新代碼中使用,不需要線程安全的場合可以用HashMap替換,需要線程安全的場合可以用ConcurrentHashMap替換。
(3) LinkedHashMap:LinkedHashMap是HashMap的一個子類,保存了記錄的插入順序,在用Iterator遍歷LinkedHashMap時,先得到的記錄肯定是先插入的,也可以在構造時帶參數,按照訪問次序排序。
(4) TreeMap:TreeMap實現SortedMap接口,能夠把它保存的記錄根據鍵排序,默認是按鍵值的升序排序,也可以指定排序的比較器,當用Iterator遍歷TreeMap時,得到的記錄是排過序的。如果使用排序的映射,建議使用TreeMap。在使用TreeMap時,key必須實現Comparable接口或者在構造TreeMap傳入自定義的Comparator,否則會在運行時拋出java.lang.ClassCastException類型的異常。
對于上述四種Map類型的類,要求映射中的key是不可變對象。不可變對象是該對象在創建后它的哈希值不會被改變。如果對象的哈希值發生變化,Map對象很可能就定位不到映射的位置了。
通過上面的比較,我們知道了HashMap是Java的Map家族中一個普通成員,鑒于它可以滿足大多數場景的使用條件,所以是使用頻度最高的一個。下文我們主要結合源碼,從存儲結構、常用方法分析、擴容以及安全性等方面深入講解HashMap的工作原理。
內部實現
搞清楚HashMap,首先需要知道HashMap是什么,即它的存儲結構-字段;其次弄明白它能干什么,即它的功能實現-方法。下面我們針對這兩個方面詳細展開講解。
存儲結構-字段
從結構實現來講,HashMap是數組+鏈表+紅黑樹(JDK1.8增加了紅黑樹部分)實現的,如下如所示。
這里需要講明白兩個問題:數據底層具體存儲的是什么?這樣的存儲方式有什么優點呢?
(1) 從源碼可知,HashMap類中有一個非常重要的字段,就是 Node[] table,即哈希桶數組,明顯它是一個Node的數組。我們來看Node[JDK1.8]是何物。
static class Node<K,V> implements Map.Entry<K,V> {final int hash; //用來定位數組索引位置final K key;V value;Node<K,V> next; //鏈表的下一個nodeNode(int hash, K key, V value, Node<K,V> next) { ... }public final K getKey(){ ... }public final V getValue() { ... }public final String toString() { ... }public final int hashCode() { ... }public final V setValue(V newValue) { ... }public final boolean equals(Object o) { ... } }Node是HashMap的一個內部類,實現了Map.Entry接口,本質是就是一個映射(鍵值對)。上圖中的每個黑色圓點就是一個Node對象。
(2) HashMap就是使用哈希表來存儲的。哈希表為解決沖突,可以采用開放地址法和鏈地址法等來解決問題,Java中HashMap采用了鏈地址法。鏈地址法,簡單來說,就是數組加鏈表的結合。在每個數組元素上都一個鏈表結構,當數據被Hash后,得到數組下標,把數據放在對應下標元素的鏈表上。例如程序執行下面代碼:
map.put("美團","小美");系統將調用"美團"這個key的hashCode()方法得到其hashCode 值(該方法適用于每個Java對象),然后再通過Hash算法的后兩步運算(高位運算和取模運算,下文有介紹)來定位該鍵值對的存儲位置,有時兩個key會定位到相同的位置,表示發生了Hash碰撞。當然Hash算法計算結果越分散均勻,Hash碰撞的概率就越小,map的存取效率就會越高。
如果哈希桶數組很大,即使較差的Hash算法也會比較分散,如果哈希桶數組數組很小,即使好的Hash算法也會出現較多碰撞,所以就需要在空間成本和時間成本之間權衡,其實就是在根據實際情況確定哈希桶數組的大小,并在此基礎上設計好的hash算法減少Hash碰撞。那么通過什么方式來控制map使得Hash碰撞的概率又小,哈希桶數組(Node[] table)占用空間又少呢?答案就是好的Hash算法和擴容機制。
在理解Hash和擴容流程之前,我們得先了解下HashMap的幾個字段。從HashMap的默認構造函數源碼可知,構造函數就是對下面幾個字段進行初始化,源碼如下:
int threshold; // 所能容納的key-value對極限 final float loadFactor; // 負載因子int modCount; int size;首先,Node[] table的初始化長度length(默認值是16),Load factor為負載因子(默認值是0.75),threshold是HashMap所能容納的最大數據量的Node(鍵值對)個數。threshold = length * Load factor。也就是說,在數組定義好長度之后,負載因子越大,所能容納的鍵值對個數越多。
結合負載因子的定義公式可知,threshold就是在此Load factor和length(數組長度)對應下允許的最大元素數目,超過這個數目就重新resize(擴容),擴容后的HashMap容量是之前容量的兩倍。默認的負載因子0.75是對空間和時間效率的一個平衡選擇,建議大家不要修改,除非在時間和空間比較特殊的情況下,如果內存空間很多而又對時間效率要求很高,可以降低負載因子Load factor的值;相反,如果內存空間緊張而對時間效率要求不高,可以增加負載因子loadFactor的值,這個值可以大于1。
size這個字段其實很好理解,就是HashMap中實際存在的鍵值對數量。注意和table的長度length、容納最大鍵值對數量threshold的區別。而modCount字段主要用來記錄HashMap內部結構發生變化的次數,主要用于迭代的快速失敗。強調一點,內部結構發生變化指的是結構發生變化,例如put新鍵值對,但是某個key對應的value值被覆蓋不屬于結構變化。
在HashMap中,哈希桶數組table的長度length大小必須為2的n次方(一定是合數),這是一種非常規的設計,常規的設計是把桶的大小設計為素數。相對來說素數導致沖突的概率要小于合數,具體證明可以參考http://blog.csdn.net/liuqiyao_01/article/details/14475159,Hashtable初始化桶大小為11,就是桶大小設計為素數的應用(Hashtable擴容后不能保證還是素數)。HashMap采用這種非常規設計,主要是為了在取模和擴容時做優化,同時為了減少沖突,HashMap定位哈希桶索引位置時,也加入了高位參與運算的過程。
這里存在一個問題,即使負載因子和Hash算法設計的再合理,也免不了會出現拉鏈過長的情況,一旦出現拉鏈過長,則會嚴重影響HashMap的性能。于是,在JDK1.8版本中,對數據結構做了進一步的優化,引入了紅黑樹。而當鏈表長度太長(默認超過8)時,鏈表就轉換為紅黑樹,利用紅黑樹快速增刪改查的特點提高HashMap的性能,其中會用到紅黑樹的插入、刪除、查找等算法。本文不再對紅黑樹展開討論,想了解更多紅黑樹數據結構的工作原理可以參考http://blog.csdn.net/v_july_v/article/details/6105630。
功能實現-方法
HashMap的內部功能實現很多,本文主要從根據key獲取哈希桶數組索引位置、put方法的詳細執行、擴容過程三個具有代表性的點深入展開講解。
1. 確定哈希桶數組索引位置
不管增加、刪除、查找鍵值對,定位到哈希桶數組的位置都是很關鍵的第一步。前面說過HashMap的數據結構是數組和鏈表的結合,所以我們當然希望這個HashMap里面的元素位置盡量分布均勻些,盡量使得每個位置上的元素數量只有一個,那么當我們用hash算法求得這個位置的時候,馬上就可以知道對應位置的元素就是我們要的,不用遍歷鏈表,大大優化了查詢的效率。HashMap定位數組索引位置,直接決定了hash方法的離散性能。先看看源碼的實現(方法一+方法二):
方法一: static final int hash(Object key) { //jdk1.8 & jdk1.7int h;// h = key.hashCode() 為第一步 取hashCode值// h ^ (h >>> 16) 為第二步 高位參與運算return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } 方法二: static int indexFor(int h, int length) { //jdk1.7的源碼,jdk1.8沒有這個方法,但是實現原理一樣的return h & (length-1); //第三步 取模運算 }這里的Hash算法本質上就是三步:取key的hashCode值、高位運算、取模運算。
對于任意給定的對象,只要它的hashCode()返回值相同,那么程序調用方法一所計算得到的Hash碼值總是相同的。我們首先想到的就是把hash值對數組長度取模運算,這樣一來,元素的分布相對來說是比較均勻的。但是,模運算的消耗還是比較大的,在HashMap中是這樣做的:調用方法二來計算該對象應該保存在table數組的哪個索引處。
這個方法非常巧妙,它通過h & (table.length -1)來得到該對象的保存位,而HashMap底層數組的長度總是2的n次方,這是HashMap在速度上的優化。當length總是2的n次方時,h& (length-1)運算等價于對length取模,也就是h%length,但是&比%具有更高的效率。
在JDK1.8的實現中,優化了高位運算的算法,通過hashCode()的高16位異或低16位實現的:(h = k.hashCode()) ^ (h >>> 16),主要是從速度、功效、質量來考慮的,這么做可以在數組table的length比較小的時候,也能保證考慮到高低Bit都參與到Hash的計算中,同時不會有太大的開銷。
下面舉例說明下,n為table的長度。
2. 分析HashMap的put方法
HashMap的put方法執行過程可以通過下圖來理解,自己有興趣可以去對比源碼更清楚地研究學習。
①.判斷鍵值對數組table[i]是否為空或為null,否則執行resize()進行擴容;
②.根據鍵值key計算hash值得到插入的數組索引i,如果table[i]==null,直接新建節點添加,轉向⑥,如果table[i]不為空,轉向③;
③.判斷table[i]的首個元素是否和key一樣,如果相同直接覆蓋value,否則轉向④,這里的相同指的是hashCode以及equals;
④.判斷table[i] 是否為treeNode,即table[i] 是否是紅黑樹,如果是紅黑樹,則直接在樹中插入鍵值對,否則轉向⑤;
⑤.遍歷table[i],判斷鏈表長度是否大于8,大于8的話把鏈表轉換為紅黑樹,在紅黑樹中執行插入操作,否則進行鏈表的插入操作;遍歷過程中若發現key已經存在直接覆蓋value即可;
⑥.插入成功后,判斷實際存在的鍵值對數量size是否超多了最大容量threshold,如果超過,進行擴容。
JDK1.8HashMap的put方法源碼如下:
1 public V put(K key, V value) {2 // 對key的hashCode()做hash3 return putVal(hash(key), key, value, false, true);4 }5 6 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,7 boolean evict) {8 Node<K,V>[] tab; Node<K,V> p; int n, i;9 // 步驟①:tab為空則創建 10 if ((tab = table) == null || (n = tab.length) == 0) 11 n = (tab = resize()).length; 12 // 步驟②:計算index,并對null做處理 13 if ((p = tab[i = (n - 1) & hash]) == null) 14 tab[i] = newNode(hash, key, value, null); 15 else { 16 Node<K,V> e; K k; 17 // 步驟③:節點key存在,直接覆蓋value 18 if (p.hash == hash && 19 ((k = p.key) == key || (key != null && key.equals(k)))) 20 e = p; 21 // 步驟④:判斷該鏈為紅黑樹 22 else if (p instanceof TreeNode) 23 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 24 // 步驟⑤:該鏈為鏈表 25 else { 26 for (int binCount = 0; ; ++binCount) { 27 if ((e = p.next) == null) { 28 p.next = newNode(hash, key,value,null);//鏈表長度大于8轉換為紅黑樹進行處理 29 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 30 treeifyBin(tab, hash); 31 break; 32 }// key已經存在直接覆蓋value 33 if (e.hash == hash && 34 ((k = e.key) == key || (key != null && key.equals(k)))) 35 break; 36 p = e; 37 } 38 } 39 40 if (e != null) { // existing mapping for key 41 V oldValue = e.value; 42 if (!onlyIfAbsent || oldValue == null) 43 e.value = value; 44 afterNodeAccess(e); 45 return oldValue; 46 } 47 }48 ++modCount; 49 // 步驟⑥:超過最大容量 就擴容 50 if (++size > threshold) 51 resize(); 52 afterNodeInsertion(evict); 53 return null; 54 }3. 擴容機制
擴容(resize)就是重新計算容量,向HashMap對象里不停的添加元素,而HashMap對象內部的數組無法裝載更多的元素時,對象就需要擴大數組的長度,以便能裝入更多的元素。當然Java里的數組是無法自動擴容的,方法是使用一個新的數組代替已有的容量小的數組,就像我們用一個小桶裝水,如果想裝更多的水,就得換大水桶。
我們分析下resize的源碼,鑒于JDK1.8融入了紅黑樹,較復雜,為了便于理解我們仍然使用JDK1.7的代碼,好理解一些,本質上區別不大,具體區別后文再說。
1 void resize(int newCapacity) { //傳入新的容量2 Entry[] oldTable = table; //引用擴容前的Entry數組3 int oldCapacity = oldTable.length; 4 if (oldCapacity == MAXIMUM_CAPACITY) { //擴容前的數組大小如果已經達到最大(2^30)了5 threshold = Integer.MAX_VALUE; //修改閾值為int的最大值(2^31-1),這樣以后就不會擴容了6 return;7 }8 9 Entry[] newTable = new Entry[newCapacity]; //初始化一個新的Entry數組 10 transfer(newTable); //!!將數據轉移到新的Entry數組里 11 table = newTable; //HashMap的table屬性引用新的Entry數組 12 threshold = (int)(newCapacity * loadFactor);//修改閾值 13 }這里就是使用一個容量更大的數組來代替已有的容量小的數組,transfer()方法將原有Entry數組的元素拷貝到新的Entry數組里。
1 void transfer(Entry[] newTable) {2 Entry[] src = table; //src引用了舊的Entry數組3 int newCapacity = newTable.length;4 for (int j = 0; j < src.length; j++) { //遍歷舊的Entry數組5 Entry<K,V> e = src[j]; //取得舊Entry數組的每個元素6 if (e != null) {7 src[j] = null;//釋放舊Entry數組的對象引用(for循環后,舊的Entry數組不再引用任何對象)8 do {9 Entry<K,V> next = e.next; 10 int i = indexFor(e.hash, newCapacity); //!!重新計算每個元素在數組中的位置 11 e.next = newTable[i]; //標記[1] 12 newTable[i] = e; //將元素放在數組上 13 e = next; //訪問下一個Entry鏈上的元素 14 } while (e != null); 15 } 16 } 17 }newTable[i]的引用賦給了e.next,也就是使用了單鏈表的頭插入方式,同一位置上新元素總會被放在鏈表的頭部位置;這樣先放在一個索引上的元素終會被放到Entry鏈的尾部(如果發生了hash沖突的話),這一點和Jdk1.8有區別,下文詳解。在舊數組中同一條Entry鏈上的元素,通過重新計算索引位置后,有可能被放到了新數組的不同位置上。
下面舉個例子說明下擴容過程。假設了我們的hash算法就是簡單的用key mod 一下表的大小(也就是數組的長度)。其中的哈希桶數組table的size=2, 所以key = 3、7、5,put順序依次為 5、7、3。在mod 2以后都沖突在table[1]這里了。這里假設負載因子 loadFactor=1,即當鍵值對的實際大小size 大于 table的實際大小時進行擴容。接下來的三個步驟是哈希桶數組 resize成4,然后所有的Node重新rehash的過程。
下面我們講解下JDK1.8做了哪些優化。經過觀測可以發現,我們使用的是2次冪的擴展(指長度擴為原來2倍),所以,元素的位置要么是在原位置,要么是在原位置再移動2次冪的位置??聪聢D可以明白這句話的意思,n為table的長度,圖(a)表示擴容前的key1和key2兩種key確定索引位置的示例,圖(b)表示擴容后key1和key2兩種key確定索引位置的示例,其中hash1是key1對應的哈希與高位運算結果。
元素在重新計算hash之后,因為n變為2倍,那么n-1的mask范圍在高位多1bit(紅色),因此新的index就會發生這樣的變化:
因此,我們在擴充HashMap的時候,不需要像JDK1.7的實現那樣重新計算hash,只需要看看原來的hash值新增的那個bit是1還是0就好了,是0的話索引沒變,是1的話索引變成“原索引+oldCap”,可以看看下圖為16擴充為32的resize示意圖:
這個設計確實非常的巧妙,既省去了重新計算hash值的時間,而且同時,由于新增的1bit是0還是1可以認為是隨機的,因此resize的過程,均勻的把之前的沖突的節點分散到新的bucket了。這一塊就是JDK1.8新增的優化點。有一點注意區別,JDK1.7中rehash的時候,舊鏈表遷移新鏈表的時候,如果在新表的數組索引位置相同,則鏈表元素會倒置,但是從上圖可以看出,JDK1.8不會倒置。有興趣的同學可以研究下JDK1.8的resize源碼,寫的很贊,如下:
1 final Node<K,V>[] resize() {2 Node<K,V>[] oldTab = table;3 int oldCap = (oldTab == null) ? 0 : oldTab.length;4 int oldThr = threshold;5 int newCap, newThr = 0;6 if (oldCap > 0) {7 // 超過最大值就不再擴充了,就只好隨你碰撞去吧8 if (oldCap >= MAXIMUM_CAPACITY) {9 threshold = Integer.MAX_VALUE; 10 return oldTab; 11 } 12 // 沒超過最大值,就擴充為原來的2倍 13 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && 14 oldCap >= DEFAULT_INITIAL_CAPACITY) 15 newThr = oldThr << 1; // double threshold 16 } 17 else if (oldThr > 0) // initial capacity was placed in threshold 18 newCap = oldThr; 19 else { // zero initial threshold signifies using defaults 20 newCap = DEFAULT_INITIAL_CAPACITY; 21 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); 22 } 23 // 計算新的resize上限 24 if (newThr == 0) { 25 26 float ft = (float)newCap * loadFactor; 27 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? 28 (int)ft : Integer.MAX_VALUE); 29 } 30 threshold = newThr; 31 @SuppressWarnings({"rawtypes","unchecked"}) 32 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; 33 table = newTab; 34 if (oldTab != null) { 35 // 把每個bucket都移動到新的buckets中 36 for (int j = 0; j < oldCap; ++j) { 37 Node<K,V> e; 38 if ((e = oldTab[j]) != null) { 39 oldTab[j] = null; 40 if (e.next == null) 41 newTab[e.hash & (newCap - 1)] = e; 42 else if (e instanceof TreeNode) 43 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); 44 else { // 鏈表優化重hash的代碼塊 45 Node<K,V> loHead = null, loTail = null; 46 Node<K,V> hiHead = null, hiTail = null; 47 Node<K,V> next; 48 do { 49 next = e.next; 50 // 原索引 51 if ((e.hash & oldCap) == 0) { 52 if (loTail == null) 53 loHead = e; 54 else 55 loTail.next = e; 56 loTail = e; 57 } 58 // 原索引+oldCap 59 else { 60 if (hiTail == null) 61 hiHead = e; 62 else 63 hiTail.next = e; 64 hiTail = e; 65 } 66 } while ((e = next) != null); 67 // 原索引放到bucket里 68 if (loTail != null) { 69 loTail.next = null; 70 newTab[j] = loHead; 71 } 72 // 原索引+oldCap放到bucket里 73 if (hiTail != null) { 74 hiTail.next = null; 75 newTab[j + oldCap] = hiHead; 76 } 77 } 78 } 79 } 80 } 81 return newTab; 82 }線程安全性
在多線程使用場景中,應該盡量避免使用線程不安全的HashMap,而使用線程安全的ConcurrentHashMap。那么為什么說HashMap是線程不安全的,下面舉例子說明在并發的多線程使用場景中使用HashMap可能造成死循環。代碼例子如下(便于理解,仍然使用JDK1.7的環境):
public class HashMapInfiniteLoop { private static HashMap<Integer,String> map = new HashMap<Integer,String>(2,0.75f); public static void main(String[] args) { map.put(5, "C"); new Thread("Thread1") { public void run() { map.put(7, "B"); System.out.println(map); }; }.start(); new Thread("Thread2") { public void run() { map.put(3, "A); System.out.println(map); }; }.start(); } }其中,map初始化為一個長度為2的數組,loadFactor=0.75,threshold=2*0.75=1,也就是說當put第二個key的時候,map就需要進行resize。
通過設置斷點讓線程1和線程2同時debug到transfer方法(3.3小節代碼塊)的首行。注意此時兩個線程已經成功添加數據。放開thread1的斷點至transfer方法的“Entry?next = e.next;” 這一行;然后放開線程2的的斷點,讓線程2進行resize。結果如下圖。
注意,Thread1的 e 指向了key(3),而next指向了key(7),其在線程二rehash后,指向了線程二重組后的鏈表。
線程一被調度回來執行,先是執行 newTalbe[i] = e, 然后是e = next,導致了e指向了key(7),而下一次循環的next = e.next導致了next指向了key(3)。
e.next = newTable[i] 導致 key(3).next 指向了 key(7)。注意:此時的key(7).next 已經指向了key(3), 環形鏈表就這樣出現了。
于是,當我們用線程一調用map.get(11)時,悲劇就出現了——Infinite Loop。
JDK1.8與JDK1.7的性能對比
HashMap中,如果key經過hash算法得出的數組索引位置全部不相同,即Hash算法非常好,那樣的話,getKey方法的時間復雜度就是O(1),如果Hash算法技術的結果碰撞非常多,假如Hash算極其差,所有的Hash算法結果得出的索引位置一樣,那樣所有的鍵值對都集中到一個桶中,或者在一個鏈表中,或者在一個紅黑樹中,時間復雜度分別為O(n)和O(lgn)。 鑒于JDK1.8做了多方面的優化,總體性能優于JDK1.7,下面我們從兩個方面用例子證明這一點。
Hash較均勻的情況
為了便于測試,我們先寫一個類Key,如下:
class Key implements Comparable<Key> {private final int value;Key(int value) {this.value = value;}@Overridepublic int compareTo(Key o) {return Integer.compare(this.value, o.value);}@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass())return false;Key key = (Key) o;return value == key.value;}@Overridepublic int hashCode() {return value;} }這個類復寫了equals方法,并且提供了相當好的hashCode函數,任何一個值的hashCode都不會相同,因為直接使用value當做hashcode。為了避免頻繁的GC,我將不變的Key實例緩存了起來,而不是一遍一遍的創建它們。代碼如下:
public class Keys {public static final int MAX_KEY = 10_000_000;private static final Key[] KEYS_CACHE = new Key[MAX_KEY];static {for (int i = 0; i < MAX_KEY; ++i) {KEYS_CACHE[i] = new Key(i);}}public static Key of(int value) {return KEYS_CACHE[value];} }現在開始我們的試驗,測試需要做的僅僅是,創建不同size的HashMap(1、10、100、......10000000),屏蔽了擴容的情況,代碼如下:
static void test(int mapSize) {HashMap<Key, Integer> map = new HashMap<Key,Integer>(mapSize);for (int i = 0; i < mapSize; ++i) {map.put(Keys.of(i), i);}long beginTime = System.nanoTime(); //獲取納秒for (int i = 0; i < mapSize; i++) {map.get(Keys.of(i));}long endTime = System.nanoTime();System.out.println(endTime - beginTime);}public static void main(String[] args) {for(int i=10;i<= 1000 0000;i*= 10){test(i);}}在測試中會查找不同的值,然后度量花費的時間,為了計算getKey的平均時間,我們遍歷所有的get方法,計算總的時間,除以key的數量,計算一個平均值,主要用來比較,絕對值可能會受很多環境因素的影響。結果如下:
通過觀測測試結果可知,JDK1.8的性能要高于JDK1.7 15%以上,在某些size的區域上,甚至高于100%。由于Hash算法較均勻,JDK1.8引入的紅黑樹效果不明顯,下面我們看看Hash不均勻的的情況。
Hash極不均勻的情況
假設我們又一個非常差的Key,它們所有的實例都返回相同的hashCode值。這是使用HashMap最壞的情況。代碼修改如下:
class Key implements Comparable<Key> {//...@Overridepublic int hashCode() {return 1;} }仍然執行main方法,得出的結果如下表所示:
從表中結果中可知,隨著size的變大,JDK1.7的花費時間是增長的趨勢,而JDK1.8是明顯的降低趨勢,并且呈現對數增長穩定。當一個鏈表太長的時候,HashMap會動態的將它替換成一個紅黑樹,這話的話會將時間復雜度從O(n)降為O(logn)。hash算法均勻和不均勻所花費的時間明顯也不相同,這兩種情況的相對比較,可以說明一個好的hash算法的重要性。
??????測試環境:處理器為2.2 GHz Intel Core i7,內存為16 GB 1600 MHz DDR3,SSD硬盤,使用默認的JVM參數,運行在64位的OS X 10.10.1上。
小結
(1) 擴容是一個特別耗性能的操作,所以當程序員在使用HashMap的時候,估算map的大小,初始化的時候給一個大致的數值,避免map進行頻繁的擴容。
(2) 負載因子是可以修改的,也可以大于1,但是建議不要輕易修改,除非情況非常特殊。
(3) HashMap是線程不安全的,不要在并發的環境中同時操作HashMap,建議使用ConcurrentHashMap。
(4) JDK1.8引入紅黑樹大程度優化了HashMap的性能。
(5) 還沒升級JDK1.8的,現在開始升級吧。HashMap的性能提升僅僅是JDK1.8的冰山一角。
?
---------------------------------------------------------------------------
ThreadLocal
源碼分析
為了解釋ThreadLocal類的工作原理,必須同時介紹與其工作甚密的其他幾個類
- ThreadLocalMap(內部類)
- Thread
首先,在Thread類中有一行:
/* ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class. */ThreadLocal.ThreadLocalMap threadLocals = null;其中ThreadLocalMap類的定義是在ThreadLocal類中,真正的引用卻是在Thread類中。同時,ThreadLocalMap中用于存儲數據的entry定義:
static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}從中我們可以發現這個Map的key是ThreadLocal類的實例對象,value為用戶的值,并不是網上大多數的例子key是線程的名字或者標識。ThreadLocal的set和get方法代碼:
public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);}public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}return setInitialValue();}其中的getMap方法:
ThreadLocalMap getMap(Thread t) {return t.threadLocals;}給當前Thread類對象初始化ThreadlocalMap屬性:
void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}到這里,我們就可以理解ThreadLocal究竟是如何工作的了
由ThreadLocal的工作原理決定了:每個線程獨自擁有一個變量,并非是共享的,下面給出一個例子:
public class Son implements Cloneable{ public static void main(String[] args){Son p=new Son();System.out.println(p);Thread t = new Thread(new Runnable(){ public void run(){ThreadLocal<Son> threadLocal = new ThreadLocal<>();System.out.println(threadLocal);threadLocal.set(p);System.out.println(threadLocal.get());threadLocal.remove();try {threadLocal.set((Son) p.clone());System.out.println(threadLocal.get());} catch (CloneNotSupportedException e) {e.printStackTrace();}System.out.println(threadLocal);}}); t.start(); } }輸出:
Son@7852e922 java.lang.ThreadLocal@3ffc8195 Son@7852e922 Son@313b781a java.lang.ThreadLocal@3ffc8195也就是如果把一個共享的對象直接保存到ThreadLocal中,那么多個線程的ThreadLocal.get()取得的還是這個共享對象本身,還是有并發訪問問題。 所以要在保存到ThreadLocal之前,通過克隆或者new來創建新的對象,然后再進行保存。
ThreadLocal的作用是提供線程內的局部變量,這種變量在線程的生命周期內起作用。作用:提供一個線程內公共變量(比如本次請求的用戶信息),減少同一個線程內多個函數或者組件之間一些公共變量的傳遞的復雜度,或者為線程提供一個私有的變量副本,這樣每一個線程都可以隨意修改自己的變量副本,而不會對其他線程產生影響。
如何實現一個線程多個ThreadLocal對象,每一個ThreadLocal對象是如何區分的呢?
查看源碼,可以看到:
對于每一個ThreadLocal對象,都有一個final修飾的int型的threadLocalHashCode不可變屬性,對于基本數據類型,可以認為它在初始化后就不可以進行修改,所以可以唯一確定一個ThreadLocal對象。
但是如何保證兩個同時實例化的ThreadLocal對象有不同的threadLocalHashCode屬性:在ThreadLocal類中,還包含了一個static修飾的AtomicInteger([??t?m?k]提供原子操作的Integer類)成員變量(即類變量)和一個static final修飾的常量(作為兩個相鄰nextHashCode的差值)。由于nextHashCode是類變量,所以每一次調用ThreadLocal類都可以保證nextHashCode被更新到新的值,并且下一次調用ThreadLocal類這個被更新的值仍然可用,同時AtomicInteger保證了nextHashCode自增的原子性。
為什么不直接用線程id來作為ThreadLocalMap的key?
這一點很容易理解,因為直接用線程id來作為ThreadLocalMap的key,無法區分放入ThreadLocalMap中的多個value。比如我們放入了兩個字符串,你如何知道我要取出來的是哪一個字符串呢?
而使用ThreadLocal作為key就不一樣了,由于每一個ThreadLocal對象都可以由threadLocalHashCode屬性唯一區分或者說每一個ThreadLocal對象都可以由這個對象的名字唯一區分(下面的例子),所以可以用不同的ThreadLocal作為key,區分不同的value,方便存取。
ThreadLocal的內存泄露問題
根據上面Entry方法的源碼,我們知道ThreadLocalMap是使用ThreadLocal的弱引用作為Key的。下圖是本文介紹到的一些對象之間的引用關系圖,實線表示強引用,虛線表示弱引用:
如上圖,ThreadLocalMap使用ThreadLocal的弱引用作為key,如果一個ThreadLocal沒有外部強引用引用他,那么系統gc的時候,這個ThreadLocal勢必會被回收,這樣一來,ThreadLocalMap中就會出現key為null的Entry,就沒有辦法訪問這些key為null的Entry的value,如果當前線程再遲遲不結束的話,這些key為null的Entry的value就會一直存在一條強引用鏈:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永遠無法回收,造成內存泄露。
ThreadLocalMap設計時的對上面問題的對策:
ThreadLocalMap的getEntry函數的流程大概為:
但是光這樣還是不夠的,上面的設計思路依賴一個前提條件:要調用ThreadLocalMap的getEntry函數或者set函數。這當然是不可能任何情況都成立的,所以很多情況下需要使用者手動調用ThreadLocal的remove函數,手動刪除不再需要的ThreadLocal,防止內存泄露。所以JDK建議將ThreadLocal變量定義成private static的,這樣的話ThreadLocal的生命周期就更長,由于一直存在ThreadLocal的強引用,所以ThreadLocal也就不會被回收,也就能保證任何時候都能根據ThreadLocal的弱引用訪問到Entry的value值,然后remove它,防止內存泄露。
關于ThreadLocalMap內部類的簡單介紹
初始容量16,負載因子2/3,解決沖突的方法是再hash法,也就是:在當前hash的基礎上再自增一個常量。
?
ThreadLocal應用場景
1、數據庫連接池實現
jdbc連接數據庫,如下所示:
Class.forName("com.mysql.jdbc.Driver"); java.sql.Connection conn = DriverManager.getConnection(jdbcUrl);- 注意:一次Drivermanager.getConnection(jdbcurl)獲得只是一個connection,并不能滿足高并發情況。因為connection不是線程安全的,一個connection對應的是一個事物。
每次獲得connection都需要浪費cpu資源和內存資源,是很浪費資源的。所以誕生了數據庫連接池。數據庫連接池實現原理如下:
pool.getConnection(),都是先從threadlocal里面拿的,如果threadlocal里面有,則用,保證線程里的多個dao操作,用的是同一個connection,以保證事務。如果新線程,則將新的connection放在threadlocal里,再get給到線程。
將connection放進threadlocal里的,以保證每個線程從連接池中獲得的都是線程自己的connection。
Hibernate的數據庫連接池源碼實現:
public class ConnectionPool implements IConnectionPool { // 連接池配置屬性 private DBbean dbBean; private boolean isActive = false; // 連接池活動狀態 private int contActive = 0;// 記錄創建的總的連接數 // 空閑連接 private List<Connection> freeConnection = new Vector<Connection>(); // 活動連接 private List<Connection> activeConnection = new Vector<Connection>(); // 將線程和連接綁定,保證事務能統一執行private static ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>(); public ConnectionPool(DBbean dbBean) { super(); this.dbBean = dbBean; init(); cheackPool(); } // 初始化 public void init() { try { Class.forName(dbBean.getDriverName()); for (int i = 0; i < dbBean.getInitConnections(); i++) { Connection conn; conn = newConnection(); // 初始化最小連接數 if (conn != null) { freeConnection.add(conn); contActive++; } } isActive = true; } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (SQLException e) { e.printStackTrace(); } } // 獲得當前連接 public Connection getCurrentConnecton(){ // 默認線程里面取 Connection conn = threadLocal.get(); if(!isValid(conn)){ conn = getConnection(); } return conn; } // 獲得連接 public synchronized Connection getConnection() { Connection conn = null; try { // 判斷是否超過最大連接數限制 if(contActive < this.dbBean.getMaxActiveConnections()){ if (freeConnection.size() > 0) { conn = freeConnection.get(0); if (conn != null) { threadLocal.set(conn); } freeConnection.remove(0); } else { conn = newConnection(); } }else{ // 繼續獲得連接,直到從新獲得連接 wait(this.dbBean.getConnTimeOut()); conn = getConnection(); } if (isValid(conn)) { activeConnection.add(conn); contActive ++; } } catch (SQLException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } return conn; } // 獲得新連接 private synchronized Connection newConnection() throws ClassNotFoundException, SQLException { Connection conn = null; if (dbBean != null) { Class.forName(dbBean.getDriverName()); conn = DriverManager.getConnection(dbBean.getUrl(), dbBean.getUserName(), dbBean.getPassword()); } return conn; } // 釋放連接 public synchronized void releaseConn(Connection conn) throws SQLException { if (isValid(conn)&& !(freeConnection.size() > dbBean.getMaxConnections())) { freeConnection.add(conn); activeConnection.remove(conn); contActive --; threadLocal.remove(); // 喚醒所有正待等待的線程,去搶連接 notifyAll(); } } // 判斷連接是否可用 private boolean isValid(Connection conn) { try { if (conn == null || conn.isClosed()) { return false; } } catch (SQLException e) { e.printStackTrace(); } return true; } // 銷毀連接池 public synchronized void destroy() { for (Connection conn : freeConnection) { try { if (isValid(conn)) { conn.close(); } } catch (SQLException e) { e.printStackTrace(); } } for (Connection conn : activeConnection) { try { if (isValid(conn)) { conn.close(); } } catch (SQLException e) { e.printStackTrace(); } } isActive = false; contActive = 0; } // 連接池狀態 @Override public boolean isActive() { return isActive; } // 定時檢查連接池情況 @Override public void cheackPool() { if(dbBean.isCheakPool()){ new Timer().schedule(new TimerTask() { @Override public void run() { // 1.對線程里面的連接狀態 // 2.連接池最小 最大連接數 // 3.其他狀態進行檢查,因為這里還需要寫幾個線程管理的類,暫時就不添加了 System.out.println("空線池連接數:"+freeConnection.size()); System.out.println("活動連接數::"+activeConnection.size()); System.out.println("總的連接數:"+contActive); } },dbBean.getLazyCheck(),dbBean.getPeriodCheck()); } } }2、有時候ThreadLocal也可以用來避免一些參數傳遞,通過ThreadLocal來訪問對象。
比如一個方法調用另一個方法時傳入了8個參數,通過逐層調用到第N個方法,傳入了其中一個參數,此時最后一個方法需要增加一個參數,第一個方法變成9個參數是自然的,但是這個時候,相關的方法都會受到牽連,使得代碼變得臃腫不堪。這時候就可以將要添加的參數設置成線程本地變量,來避免參數傳遞。
上面提到的是ThreadLocal一種亡羊補牢的用途,不過也不是特別推薦使用的方式,它還有一些類似的方式用來使用,就是在框架級別有很多動態調用,調用過程中需要滿足一些協議,雖然協議我們會盡量的通用,而很多擴展的參數在定義協議時是不容易考慮完全的以及版本也是隨時在升級的,但是在框架擴展時也需要滿足接口的通用性和向下兼容,而一些擴展的內容我們就需要ThreadLocal來做方便簡單的支持。
簡單來說,ThreadLocal是將一些復雜的系統擴展變成了簡單定義,使得相關參數牽連的部分變得非常容易。
3、在某些情況下提升性能和安全。
用SimpleDateFormat這個對象,進行日期格式化。因為創建這個對象本身很費時的,而且我們也知道SimpleDateFormat本身不是線程安全的,也不能緩存一個共享的SimpleDateFormat實例,為此我們想到使用ThreadLocal來給每個線程緩存一個SimpleDateFormat實例,提高性能。同時因為每個Servlet會用到不同pattern的時間格式化類,所以我們對應每一種pattern生成了一個ThreadLocal實例。
public interface DateTimeFormat {String DATE_PATTERN = "yyyy-MM-dd";ThreadLocal<DateFormat> DATE_FORMAT = ThreadLocal.withInitial(() -> {return new SimpleDateFormat("yyyy-MM-dd");});String TIME_PATTERN = "HH:mm:ss";ThreadLocal<DateFormat> TIME_FORMAT = ThreadLocal.withInitial(() -> {return new SimpleDateFormat("HH:mm:ss");});String DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss";ThreadLocal<DateFormat> DATE_TIME_FORMAT = ThreadLocal.withInitial(() -> {return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");});}為什么SimpleDateFormat不安全,可以參考此篇博文:
SimpleDateFormat線程不安全及解決辦法
假如我們把SimpleDateFormat定義成static成員變量,那么多個thread之間會共享這個sdf對象, 所以Calendar對象也會共享。?
假定線程A和線程B都進入了parse(text, pos) 方法, 線程B執行到calendar.clear()后,線程A執行到calendar.getTime(), 那么就會有問題。
如果不用static修飾,將SimpleDateFormat定義成局部變量:?
每調用一次方法就會創建一個SimpleDateFormat對象,方法結束又要作為垃圾回收。加鎖性能較差,每次都要等待鎖釋放后其他線程才能進入。那么最好的辦法就是:使用ThreadLocal: 每個線程都將擁有自己的SimpleDateFormat對象副本。
附-SimpleDateFormat關鍵源碼:
public class SimpleDateFormat extends DateFormat { public Date parse(String text, ParsePosition pos){ calendar.clear(); // Clears all the time fields // other logic ... Date parsedDate = calendar.getTime(); } } abstract class DateFormat{ // other logic ... protected Calendar calendar; public Date parse(String source) throws ParseException{ ParsePosition pos = new ParsePosition(0); Date result = parse(source, pos); if (pos.index == 0) throw new ParseException("Unparseable date: \"" + source + "\"" , pos.errorIndex); return result; } }?
---------------------------------------------------------------------------
ConcurrentHashMap
主要參考:http://www.cnblogs.com/leesf456/p/5453341.html
? ? ? ? ? ? ? ??http://blog.csdn.net/sunyangwei1993/article/details/77001597
一、前言
最近幾天忙著做點別的東西,今天終于有時間分析源碼了,看源碼感覺很爽,并且發現ConcurrentHashMap在JDK1.8版本與之前的版本在并發控制上存在很大的差別,很有必要進行認真的分析,下面進行源碼分析。
二、ConcurrentHashMap數據結構(jdk1.8)
之前已經提及過,ConcurrentHashMap相比HashMap而言,是多線程安全的,其底層數據與HashMap的數據結構相同,數據結構如下
說明:ConcurrentHashMap的數據結構(數組+鏈表+紅黑樹),桶中的結構可能是鏈表,也可能是紅黑樹,紅黑樹是為了提高查找效率。
三、ConcurrentHashMap源碼分析(jdk1.8)
3.1 類的繼承關系
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>implements ConcurrentMap<K,V>, Serializable {}說明:ConcurrentHashMap繼承了AbstractMap抽象類,該抽象類定義了一些基本操作,同時,也實現了ConcurrentMap接口,ConcurrentMap接口也定義了一系列操作,實現了Serializable接口表示ConcurrentHashMap可以被序列化。
?
? ? 3.2 類的內部類
ConcurrentHashMap包含了很多內部類,其中主要的內部類框架圖如下圖所示
說明:可以看到,ConcurrentHashMap的內部類非常的龐大,第二個圖是在JDK1.8下增加的類。
下面對主要的內部類進行分析和講解。
1. Node類
Node類主要用于存儲具體鍵值對,其子類有ForwardingNode、ReservationNode、TreeNode和TreeBin四個子類。四個子類具體的代碼在之后的具體例子中進行分析講解。
?
2. Traverser類
Traverser類主要用于遍歷操作,其子類有BaseIterator、KeySpliterator、ValueSpliterator、EntrySpliterator四個類,BaseIterator用于遍歷操作。KeySplitertor、ValueSpliterator、EntrySpliterator則用于鍵、值、鍵值對的劃分。
3. CollectionView類
CollectionView抽象類主要定義了視圖操作,其子類KeySetView、ValueSetView、EntrySetView分別表示鍵視圖、值視圖、鍵值對視圖。對視圖均可以進行操作。
4. Segment類
Segment類在JDK1.8中與之前的版本的JDK作用存在很大的差別,JDK1.8下,其在普通的ConcurrentHashMap操作中已經沒有失效,其在序列化與反序列化的時候會發揮作用。
5. CounterCell
CounterCell類主要用于對baseCount的計數。
?
3.3 類的屬性
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>implements ConcurrentMap<K,V>, Serializable {private static final long serialVersionUID = 7249069246763182397L; // 表的最大容量private static final int MAXIMUM_CAPACITY = 1 << 30;// 默認表的大小private static final int DEFAULT_CAPACITY = 16;// 最大數組大小static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;// 默認并發數private static final int DEFAULT_CONCURRENCY_LEVEL = 16;// 裝載因子private static final float LOAD_FACTOR = 0.75f;// 轉化為紅黑樹的閾值static final int TREEIFY_THRESHOLD = 8;// 由紅黑樹轉化為鏈表的閾值static final int UNTREEIFY_THRESHOLD = 6;// 轉化為紅黑樹的表的最小容量static final int MIN_TREEIFY_CAPACITY = 64;// 每次進行轉移的最小值private static final int MIN_TRANSFER_STRIDE = 16;// 生成sizeCtl所使用的bit位數private static int RESIZE_STAMP_BITS = 16;// 進行擴容所允許的最大線程數private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;// 記錄sizeCtl中的大小所需要進行的偏移位數private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS; // 一系列的標識static final int MOVED = -1; // hash for forwarding nodesstatic final int TREEBIN = -2; // hash for roots of treesstatic final int RESERVED = -3; // hash for transient reservationsstatic final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash// /** Number of CPUS, to place bounds on some sizings */// 獲取可用的CPU個數static final int NCPU = Runtime.getRuntime().availableProcessors();// /** For serialization compatibility. */// 進行序列化的屬性private static final ObjectStreamField[] serialPersistentFields = {new ObjectStreamField("segments", Segment[].class),new ObjectStreamField("segmentMask", Integer.TYPE),new ObjectStreamField("segmentShift", Integer.TYPE)};// 表transient volatile Node<K,V>[] table;// 下一個表private transient volatile Node<K,V>[] nextTable;///*** Base counter value, used mainly when there is no contention,* but also as a fallback during table initialization* races. Updated via CAS.*/// 基本計數private transient volatile long baseCount;///*** Table initialization and resizing control. When negative, the* table is being initialized or resized: -1 for initialization,* else -(1 + the number of active resizing threads). Otherwise,* when table is null, holds the initial table size to use upon* creation, or 0 for default. After initialization, holds the* next element count value upon which to resize the table.*/// 對表初始化和擴容控制private transient volatile int sizeCtl;/*** The next table index (plus one) to split while resizing.*/// 擴容下另一個表的索引private transient volatile int transferIndex;/*** Spinlock (locked via CAS) used when resizing and/or creating CounterCells.*/// 旋轉鎖private transient volatile int cellsBusy;/*** Table of counter cells. When non-null, size is a power of 2.*/// counterCell表private transient volatile CounterCell[] counterCells;// views// 視圖private transient KeySetView<K,V> keySet;private transient ValuesView<K,V> values;private transient EntrySetView<K,V> entrySet;// Unsafe mechanicsprivate static final sun.misc.Unsafe U;private static final long SIZECTL;private static final long TRANSFERINDEX;private static final long BASECOUNT;private static final long CELLSBUSY;private static final long CELLVALUE;private static final long ABASE;private static final int ASHIFT;static {try {U = sun.misc.Unsafe.getUnsafe();Class<?> k = ConcurrentHashMap.class;SIZECTL = U.objectFieldOffset(k.getDeclaredField("sizeCtl"));TRANSFERINDEX = U.objectFieldOffset(k.getDeclaredField("transferIndex"));BASECOUNT = U.objectFieldOffset(k.getDeclaredField("baseCount"));CELLSBUSY = U.objectFieldOffset(k.getDeclaredField("cellsBusy"));Class<?> ck = CounterCell.class;CELLVALUE = U.objectFieldOffset(ck.getDeclaredField("value"));Class<?> ak = Node[].class;ABASE = U.arrayBaseOffset(ak);int scale = U.arrayIndexScale(ak);if ((scale & (scale - 1)) != 0)throw new Error("data type scale not a power of two");ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);} catch (Exception e) {throw new Error(e);}} }說明:ConcurrentHashMap的屬性很多,其中不少屬性在HashMap中就已經介紹過,而對于ConcurrentHashMap而言,添加了Unsafe實例,主要用于反射獲取對象相應的字段,需要注意的是
transient volatile Node<K,V>[] table;這里Node用了transient volatile來保證
3.4 類的構造函數
1. ConcurrentHashMap()型構造函數
public ConcurrentHashMap() {}說明:該構造函數用于創建一個帶有默認初始容量 (16)、加載因子 (0.75) 和 concurrencyLevel (16) 的新的空映射。
2. ConcurrentHashMap(int)型構造函數
public ConcurrentHashMap(int initialCapacity) {if (initialCapacity < 0) // 初始容量小于0,拋出異常throw new IllegalArgumentException();int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?MAXIMUM_CAPACITY :tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1)); // 找到最接近該容量的2的冪次方數// 初始化this.sizeCtl = cap;}說明:該構造函數用于創建一個帶有指定初始容量、默認加載因子 (0.75) 和 concurrencyLevel (16) 的新的空映射。
3. ConcurrentHashMap(Map<? extends K, ? extends V>)型構造函數
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {this.sizeCtl = DEFAULT_CAPACITY;// 將集合m的元素全部放入putAll(m);}說明:該構造函數用于構造一個與給定映射具有相同映射關系的新映射。
4. ConcurrentHashMap(int, float)型構造函數
public ConcurrentHashMap(int initialCapacity, float loadFactor) {this(initialCapacity, loadFactor, 1);}說明:該構造函數用于創建一個帶有指定初始容量、加載因子和默認 concurrencyLevel (1) 的新的空映射。
5. ConcurrentHashMap(int, float, int)型構造函數
public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel) {if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0) // 合法性判斷throw new IllegalArgumentException();if (initialCapacity < concurrencyLevel) // Use at least as many binsinitialCapacity = concurrencyLevel; // as estimated threadslong size = (long)(1.0 + (long)initialCapacity / loadFactor);int cap = (size >= (long)MAXIMUM_CAPACITY) ?MAXIMUM_CAPACITY : tableSizeFor((int)size);this.sizeCtl = cap;}說明:該構造函數用于創建一個帶有指定初始容量、加載因子和并發級別的新的空映射。
對于構造函數而言,會根據輸入的initialCapacity的大小來確定一個最小的且大于等于initialCapacity大小的2的n次冪,如initialCapacity為15,則sizeCtl為16,若initialCapacity為16,則sizeCtl為16。若initialCapacity大小超過了允許的最大值,則sizeCtl為最大值。值得注意的是,構造函數中的concurrencyLevel參數已經在JDK1.8中的意義發生了很大的變化,其并不代表所允許的并發數,其只是用來確定sizeCtl大小,在JDK1.8中的并發控制都是針對具體的桶而言,即有多少個桶就可以允許多少個并發數。
?
這時候我們先來回顧一下JDK1.7中的CocurrentHashMap:
HashMap的容量由負載因子決定,插入的元素超過了容量的范圍就會觸發擴容操作,就是rehash。?
在多線程環境下,若同時存在其他元素進行put操作,如果hash值相同,可能出現在同一數組下用鏈表表示,出現閉環,導致在get的操作會出現死循環,所以hashmap是線程不安全的。
Hashtable是線程安全的,它在所有都涉及到多線程操作時都加了synchronized關鍵字來鎖住整個table,意味著所有線程都在爭用一把鎖,在多線程的環境下,它是安全的,但效率低下。
ConcurrentHashMap采用鎖分離技術,將鎖的粒度降低,利用多個鎖來控制多個小的table。
ConcurrentHashMap的數據結構是由一個Segment數組和多個HashEntry組成,如下圖所示:
Segment數組的意義就是將一個大的table分割成多個小的table來進行加鎖,也就是上面的提到的鎖分離技術,而每一個Segment元素存儲的是HashEntry數組+鏈表,這個和HashMap的數據存儲結構一樣。
ConcurrentHashMap的初始化是會通過位與運算來初始化Segment的大小,用ssize來表示,如下所示
int sshift = 0; int ssize = 1; while (ssize < concurrencyLevel) {++sshift;ssize <<= 1; }如上所示,因為ssize用位于運算來計算(ssize <<=1),所以Segment的大小取值都是以2的N次方,無關concurrencyLevel的取值,當然concurrencyLevel最大只能用16位的二進制來表示,即65536,換句話說,Segment的大小最多65536個,沒有指定concurrencyLevel元素初始化,Segment的大小ssize默認為16。?
每一個Segment元素下的HashEntry的初始化也是按照位于運算來計算,用cap來表示,如下所示
如上所示,HashEntry大小的計算也是2的N次方(cap <<=1), cap的初始值為1,所以HashEntry最小的容量為2。
put操作
對于ConcurrentHashMap的數據插入,這里要進行兩次Hash去定位數據的存儲位置
static class Segment<K,V> extends ReentrantLock implements Serializable {從上Segment的繼承體系可以看出,Segment實現了ReentrantLock,也就帶有鎖的功能,當執行put操作時,會進行第一次key的hash來定位Segment的位置,如果該Segment還沒有初始化,即通過CAS操作進行賦值,然后進行第二次hash操作,找到相應的HashEntry的位置,這里會利用繼承過來的鎖的特性,在將數據插入指定的HashEntry位置時(鏈表的尾端),會通過繼承ReentrantLock的tryLock()方法嘗試去獲取鎖,如果獲取成功就直接插入相應的位置,如果已經有線程獲取該Segment的鎖,那當前線程會以自旋的方式去繼續的調用tryLock()方法去獲取鎖,超過指定次數就掛起,等待喚醒。
get操作
ConcurrentHashMap的get操作跟HashMap類似,只是首先要判斷volatile類型變量count是否不等于0,若不等于0則ConcurrentHashMap第一次需要經過一次hash定位到Segment的位置,然后再hash定位到指定的HashEntry,遍歷該HashEntry下的鏈表進行對比,成功就返回,不成功就返回null。是弱一致性的。
? ?因為count是volatile,所以對count的寫要happens-before于讀操作。寫線程 M 對鏈表做的結構性修改,在讀線程 N 讀取了同一個 volatile 變量后,對線程 N 也是可見的了。雖然線程 N 是在未加鎖的情況下訪問鏈表。Java 的內存模型可以保證:只要之前對鏈表做結構性修改操作的寫線程 M 在退出寫方法前寫 volatile 型變量 count,讀線程 N 在讀取這個 volatile 型變量 count 后,就一定能“看到”這些修改。使得在 ConcurrentHashMap 中,讀線程在讀取散列表時,基本不需要加鎖就能成功獲得需要的值,不僅減少了請求同一個鎖的頻率(讀操作一般不需要加鎖就能夠成功獲得值),也減少了持有同一個鎖的時間(只有讀到 value 域的值為 null 時 , 讀線程才需要加鎖后重讀)。
size操作
計算ConcurrentHashMap的元素大小是一個有趣的問題,因為他是并發操作的,就是在你計算size的時候,他還在并發的插入數據,可能會導致你計算出來的size和你實際的size有相差(在你return size的時候,插入了多個數據),要解決這個問題,JDK1.7版本用兩種方案。
try {for (;;) {if (retries++ == RETRIES_BEFORE_LOCK) {for (int j = 0; j < segments.length; ++j) ensureSegment(j).lock(); // force creation}sum = 0L;size = 0;overflow = false;for (int j = 0; j < segments.length; ++j) {Segment<K,V> seg = segmentAt(segments, j);if (seg != null) { sum += seg.modCount; int c = seg.count; if (c < 0 || (size += c) < 0)overflow = true;} }if (sum == last) break;last = sum; } } finally {if (retries > RETRIES_BEFORE_LOCK) {for (int j = 0; j < segments.length; ++j)segmentAt(segments, j).unlock();} }以上為JDK1.7中的實現。
在JDK1.8中:
? ??改變了HashMap的索引方法,詳見JDK1.8之HashMap,在put時并不會導致鏈表死循環,但依然不能保證HashMap的線程安全,即是多線程put的時候,當index相同而又同時達到鏈表的末尾時,另一個線程put的數據會把之前線程put的數據覆蓋掉,就會產生數據丟失。
? ? ??JDK1.8的實現已經摒棄了Segment的概念,而是直接用Node數組+鏈表+紅黑樹的數據結構來實現,并發控制使用Synchronized和CAS來操作,整個看起來就像是優化過且線程安全的HashMap,雖然在JDK1.8中還能看到Segment的數據結構,但是已經簡化了屬性,只是為了兼容舊版本,序列化與反序列化的時候會發揮作用。
? ? ?
?
Node
Node是ConcurrentHashMap存儲結構的基本單元,繼承于HashMap中的Entry,用于存儲數據,源代碼如下
static class Node<K,V> implements Map.Entry<K,V> {//鏈表的數據結構final int hash;final K key;//val和next都會在擴容時發生變化,所以加上volatile來保持可見性和禁止重排序volatile V val;volatile Node<K,V> next;Node(int hash, K key, V val, Node<K,V> next) {this.hash = hash;this.key = key;this.val = val;this.next = next;}public final K getKey() { return key; }public final V getValue() { return val; }public final int hashCode() { return key.hashCode() ^ val.hashCode(); }public final String toString(){ return key + "=" + val; }//不允許更新value public final V setValue(V value) {throw new UnsupportedOperationException();}public final boolean equals(Object o) {Object k, v, u; Map.Entry<?,?> e;return ((o instanceof Map.Entry) &&(k = (e = (Map.Entry<?,?>)o).getKey()) != null &&(v = e.getValue()) != null &&(k == key || k.equals(key)) &&(v == (u = val) || v.equals(u)));}//用于map中的get()方法,子類重寫Node<K,V> find(int h, Object k) {Node<K,V> e = this;if (k != null) {do {K ek;if (e.hash == h &&((ek = e.key) == k || (ek != null && k.equals(ek))))return e;} while ((e = e.next) != null);}return null;} }Node數據結構很簡單,它與HashMap中的定義很相似,但是但是有一些差別它對value和next屬性設置了volatile同步鎖,它不允許調用setValue方法直接改變Node的value域,它增加了find方法輔助map.get()方法。
?
TreeNode
TreeNode繼承與Node,但是數據結構換成了二叉樹結構,它是紅黑樹的數據的存儲結構,用于紅黑樹中存儲數據,當鏈表的節點數大于8時會轉換成紅黑樹的結構,樹節點類,另外一個核心的數據結構。當鏈表長度過長的時候,會轉換為TreeNode。但是與HashMap不相同的是,它并不是直接轉換為紅黑樹,而是把這些結點包裝成TreeNode放在TreeBin對象中,由TreeBin完成對紅黑樹的包裝。而且TreeNode在ConcurrentHashMap集成自Node類,而并非HashMap中的集成自LinkedHashMap.Entry<K,V>類,也就是說TreeNode帶有next指針,這樣做的目的是方便基于TreeBin的訪問。
static final class TreeNode<K,V> extends Node<K,V> {//樹形結構的屬性定義TreeNode<K,V> parent; // red-black tree linksTreeNode<K,V> left;TreeNode<K,V> right;TreeNode<K,V> prev; // needed to unlink next upon deletionboolean red; //標志紅黑樹的紅節點TreeNode(int hash, K key, V val, Node<K,V> next,TreeNode<K,V> parent) {super(hash, key, val, next);this.parent = parent;}Node<K,V> find(int h, Object k) {return findTreeNode(h, k, null);}//根據key查找 從根節點開始找出相應的TreeNode,final TreeNode<K,V> findTreeNode(int h, Object k, Class<?> kc) {if (k != null) {TreeNode<K,V> p = this;do {int ph, dir; K pk; TreeNode<K,V> q;TreeNode<K,V> pl = p.left, pr = p.right;if ((ph = p.hash) > h)p = pl;else if (ph < h)p = pr;else if ((pk = p.key) == k || (pk != null && k.equals(pk)))return p;else if (pl == null)p = pr;else if (pr == null)p = pl;else if ((kc != null ||(kc = comparableClassFor(k)) != null) &&(dir = compareComparables(kc, k, pk)) != 0)p = (dir < 0) ? pl : pr;else if ((q = pr.findTreeNode(h, k, kc)) != null)return q;elsep = pl;} while (p != null);}return null;} }TreeBin
TreeBin從字面含義中可以理解為存儲樹形結構的容器,而樹形結構就是指TreeNode,所以TreeBin就是封裝TreeNode的容器,它提供轉換黑紅樹的一些條件和鎖的控制。這個類并不負責包裝用戶的key、value信息,而是包裝的很多TreeNode節點。它代替了TreeNode的根節點,也就是說在實際的ConcurrentHashMap“數組”中,存放的是TreeBin對象,而不是TreeNode對象,這是與HashMap的區別。另外這個類還帶有了讀寫鎖。
static final class TreeBin<K,V> extends Node<K,V> {//指向TreeNode列表和根節點TreeNode<K,V> root;volatile TreeNode<K,V> first;volatile Thread waiter;volatile int lockState;// 讀寫鎖狀態static final int WRITER = 1; // 獲取寫鎖的狀態static final int WAITER = 2; // 等待寫鎖的狀態static final int READER = 4; // 增加數據時讀鎖的狀態/*** 初始化紅黑樹*/TreeBin(TreeNode<K,V> b) {super(TREEBIN, null, null, null);this.first = b;TreeNode<K,V> r = null;for (TreeNode<K,V> x = b, next; x != null; x = next) {next = (TreeNode<K,V>)x.next;x.left = x.right = null;if (r == null) {x.parent = null;x.red = false;r = x;}else {K k = x.key;int h = x.hash;Class<?> kc = null;for (TreeNode<K,V> p = r;;) {int dir, ph;K pk = p.key;if ((ph = p.hash) > h)dir = -1;else if (ph < h)dir = 1;else if ((kc == null &&(kc = comparableClassFor(k)) == null) ||(dir = compareComparables(kc, k, pk)) == 0)dir = tieBreakOrder(k, pk);TreeNode<K,V> xp = p;if ((p = (dir <= 0) ? p.left : p.right) == null) {x.parent = xp;if (dir <= 0)xp.left = x;elsexp.right = x;r = balanceInsertion(r, x);break;}}}}this.root = r;assert checkInvariants(root);}...... }?
ForwardingNode
? ? ? ? 一個用于連接兩個table的節點類。它包含一個nextTable指針,用于指向下一張表。而且這個節點的key value next指針全部為null,它的hash值為-1. 這里面定義的find的方法是從nextTable里進行查詢節點,而不是以自身為頭節點進行查找
/**?*?A?node?inserted?at?head?of?bins?during?transfer?operations.?*/??static?final?class?ForwardingNode<K,V>?extends?Node<K,V>?{??final?Node<K,V>[]?nextTable;??ForwardingNode(Node<K,V>[]?tab)?{??super(MOVED,?null,?null,?null);??this.nextTable?=?tab;??}??Node<K,V>?find(int?h,?Object?k)?{??//?loop?to?avoid?arbitrarily?deep?recursion?on?forwarding?nodes??outer:?for?(Node<K,V>[]?tab?=?nextTable;;)?{??Node<K,V>?e;?int?n;??if?(k?==?null?||?tab?==?null?||?(n?=?tab.length)?==?0?||??(e?=?tabAt(tab,?(n?-?1)?&?h))?==?null)??return?null;??for?(;;)?{??int?eh;?K?ek;??if?((eh?=?e.hash)?==?h?&&??((ek?=?e.key)?==?k?||?(ek?!=?null?&&?k.equals(ek))))??return?e;??if?(eh?<?0)?{??if?(e?instanceof?ForwardingNode)?{??tab?=?((ForwardingNode<K,V>)e).nextTable;??continue?outer;??}??else??return?e.find(h,?k);??}??if?((e?=?e.next)?==?null)??return?null;??}??}??}??}??Unsafe與CAS
? ? ? 在ConcurrentHashMap中,隨處可以看到U, 大量使用了U.compareAndSwapXXX的方法,這個方法是利用一個CAS算法實現無鎖化的修改值的操作,他可以大大降低鎖代理的性能消耗。這個算法的基本思想就是不斷地去比較當前內存中的變量值與你指定的一個變量值是否相等,如果相等,則接受你指定的修改的值,否則拒絕你的操作。因為當前線程中的值已經不是最新的值,你的修改很可能會覆蓋掉其他線程修改的結果。這一點與樂觀鎖,SVN的思想是比較類似的。
unsafe靜態塊
unsafe代碼塊控制了一些屬性的修改工作,比如最常用的SIZECTL 。? 在這一版本的concurrentHashMap中,大量應用來的CAS方法進行變量、屬性的修改工作。? 利用CAS進行無鎖操作,可以大大提高性能。
介紹了ConcurrentHashMap主要的屬性與內部的數據結構,現在通過一個簡單的例子以debug的視角看看ConcurrentHashMap的具體操作細節。
先通過new ConcurrentHashMap()來進行初始化
public ConcurrentHashMap() { }由上你會發現ConcurrentHashMap的初始化其實是一個空實現,并沒有做任何事,這里后面會講到,這也是和其他的集合類有區別的地方,初始化操作并不是在構造函數實現的,而是在put操作中實現,當然ConcurrentHashMap還提供了其他的構造函數。
?
put操作
在上面的例子中我們新增個人信息會調用put方法,我們來看下。
public V put(K key, V value) {return putVal(key, value, false); } /** Implementation for put and putIfAbsent */ final V putVal(K key, V value, boolean onlyIfAbsent) {if (key == null || value == null) throw new NullPointerException();int hash = spread(key.hashCode()); //兩次hash,減少hash沖突,可以均勻分布int binCount = 0;for (Node<K,V>[] tab = table;;) { //對這個table進行迭代Node<K,V> f; int n, i, fh;//這里就是上面構造方法沒有進行初始化,在這里進行判斷,為null就調用initTable進行初始化,屬于懶漢模式初始化if (tab == null || (n = tab.length) == 0)tab = initTable();else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//如果i位置沒有數據,就直接無鎖插入if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))break; // no lock when adding to empty bin}else if ((fh = f.hash) == MOVED)//如果在進行擴容,則先進行擴容操作tab = helpTransfer(tab, f);else {V oldVal = null;//如果以上條件都不滿足,那就要進行加鎖操作,也就是存在hash沖突,鎖住鏈表或者紅黑樹的頭結點synchronized (f) {if (tabAt(tab, i) == f) {if (fh >= 0) { //表示該節點是鏈表結構binCount = 1;for (Node<K,V> e = f;; ++binCount) {K ek;//這里涉及到相同的key進行put就會覆蓋原先的valueif (e.hash == hash &&((ek = e.key) == key ||(ek != null && key.equals(ek)))) {oldVal = e.val;if (!onlyIfAbsent)e.val = value;break;}Node<K,V> pred = e;if ((e = e.next) == null) { //插入鏈表尾部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,value)) != null) {oldVal = p.val;if (!onlyIfAbsent)p.val = value;}}}}if (binCount != 0) { //如果鏈表的長度大于8時就會進行紅黑樹的轉換if (binCount >= TREEIFY_THRESHOLD)treeifyBin(tab, i);if (oldVal != null)return oldVal;break;}}}addCount(1L, binCount);//統計size,并且檢查是否需要擴容return null; }ConcurrentHashMap不允許key或value為null值
這個put的過程很清晰,對當前的table進行無條件自循環直到put成功,可以分成以下六步流程來概述。?
① 判斷存儲的key、value是否為空,若為空,則拋出異常,否則,進入步驟②
② 計算key的hash值,隨后進入無限循環,該無限循環可以確保成功插入數據,若table表為空或者長度為0,則初始化table表,否則,進入步驟③
③ 根據key的hash值取出table表中的結點元素,若取出的結點為空(該桶為空),則使用CAS將key、value、hash值生成的結點放入桶中。否則,進入步驟④
④ 若該結點的的hash值為MOVED,則對該桶中的結點進行轉移,即該線程幫助其進行擴容。否則,進入步驟⑤
⑤ 對桶中的第一個結點(即table表中的結點)進行加鎖,對該桶進行遍歷,桶中的結點的hash值與key值與給定的hash值和key值相等,則根據標識選擇是否進行更新操作(用給定的value值替換該結點的value值),若遍歷完桶仍沒有找到hash值與key值和指定的hash值與key值相等的結點,則直接新生一個結點并賦值為之前最后一個結點的下一個結點。進入步驟⑥
⑥ 若binCount值達到紅黑樹轉化的閾值,則將桶中的結構轉化為紅黑樹存儲,最后,增加binCount的值。
其中:
? ?helpTransfer()方法的目的就是調用多個工作線程一起幫助進行擴容,這樣的效率就會更高,而不是只有檢查到要擴容的那個線程進行擴容操作,其他線程就要等待擴容操作完成才能工作。?幫助從舊的table的元素復制到新的table中,新的table即nextTba已經存在前提下才能幫助擴容。
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {Node<K,V>[] nextTab; int sc;if (tab != null && (f instanceof ForwardingNode) &&(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) { //新的table nextTba已經存在前提下才能幫助擴容int rs = resizeStamp(tab.length);while (nextTab == nextTable && table == tab &&(sc = sizeCtl) < 0) {if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||sc == rs + MAX_RESIZERS || transferIndex <= 0)break;if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {transfer(tab, nextTab);//調用擴容方法break;}}return nextTab;}return table; }?
擴容過程有點復雜,這里主要涉及到多線程并發擴容,ForwardingNode的作用就是支持擴容操作,將已處理的節點和空節點置為ForwardingNode,并發處理時多個線程經過ForwardingNode就表示已經遍歷了,就往后遍歷,下圖是多線程合作擴容的過程:
在并發處理中使用的是樂觀鎖,當有沖突的時候才進行并發處理,而且流程步驟很清晰,但是細節設計的很復雜,畢竟多線程的場景也復雜。
?
擴容方法 transfer()
當ConcurrentHashMap容量不足的時候,需要對table進行擴容。這個方法的基本思想跟HashMap是很像的,但是由于它是支持并發擴容的,所以要復雜的多。原因是它支持多線程進行擴容操作,而并沒有加鎖。我想這樣做的目的不僅僅是為了滿足concurrent的要求,而是希望利用并發處理去減少擴容帶來的時間影響。因為在擴容的時候,總是會涉及到從一個“數組”到另一個“數組”拷貝的操作,如果這個操作能夠并發進行,那真真是極好的了。
整個擴容操作分為兩個部分
-
?第一部分是構建一個nextTable,它的容量是原來的兩倍,這個操作是單線程完成的。這個單線程的保證是通過RESIZE_STAMP_SHIFT這個常量經過一次運算來保證的,這個地方在后面會有提到;
- 第二個部分就是將原來table中的元素復制到nextTable中,這里允許多線程進行操作。
先來看一下單線程是如何完成的:
它的大體思想就是遍歷、復制的過程。首先根據運算得到需要遍歷的次數i,然后利用tabAt方法獲得i位置的元素:
-
如果這個位置為空,就在原table中的i位置放入forwardNode節點,這個也是觸發并發擴容的關鍵點;
-
如果這個位置是Node節點(fh>=0),如果它是一個鏈表的頭節點,就構造一個反序鏈表,把他們分別放在nextTable的i和i+n的位置上
-
如果這個位置是TreeBin節點(fh<0),也做一個反序處理,并且判斷是否需要untreefi,把處理的結果分別放在nextTable的i和i+n的位置上
- 遍歷過所有的節點以后就完成了復制工作,這時讓nextTable作為新的table,并且更新sizeCtl為新容量的0.75倍 ,完成擴容。
再看一下多線程是如何完成的:
在代碼的69行有一個判斷,如果遍歷到的節點是forward節點,就向后繼續遍歷,再加上給節點上鎖的機制,就完成了多線程的控制。多線程遍歷節點,處理了一個節點,就把對應點的值set為forward,另一個線程看到forward,就向后遍歷。這樣交叉就完成了復制工作。而且還很好的解決了線程安全的問題。 這個方法的設計實在是讓我膜拜。
? /**?*?一個過渡的table表??只有在擴容的時候才會使用?*/??private?transient?volatile?Node<K,V>[]?nextTable;??/**?*?Moves?and/or?copies?the?nodes?in?each?bin?to?new?table.?See?*?above?for?explanation.?*/??private?final?void?transfer(Node<K,V>[]?tab,?Node<K,V>[]?nextTab)?{??int?n?=?tab.length,?stride;??if?((stride?=?(NCPU?>?1)???(n?>>>?3)?/?NCPU?:?n)?<?MIN_TRANSFER_STRIDE)??stride?=?MIN_TRANSFER_STRIDE;?//?subdivide?range??if?(nextTab?==?null)?{????????????//?initiating??try?{??@SuppressWarnings("unchecked")??Node<K,V>[]?nt?=?(Node<K,V>[])new?Node<?,?>[n?<<?1];//構造一個nextTable對象?它的容量是原來的兩倍??nextTab?=?nt;??}?catch?(Throwable?ex)?{??????//?try?to?cope?with?OOME??sizeCtl?=?Integer.MAX_VALUE;??return;??}??nextTable?=?nextTab;??transferIndex?=?n;??}??int?nextn?=?nextTab.length;??ForwardingNode<K,V>?fwd?=?new?ForwardingNode<K,V>(nextTab);//構造一個連節點指針?用于標志位??boolean?advance?=?true;//并發擴容的關鍵屬性?如果等于true?說明這個節點已經處理過??boolean?finishing?=?false;?//?to?ensure?sweep?before?committing?nextTab??for?(int?i?=?0,?bound?=?0;;)?{??Node<K,V>?f;?int?fh;??//這個while循環體的作用就是在控制i--??通過i--可以依次遍歷原hash表中的節點??while?(advance)?{??int?nextIndex,?nextBound;??if?(--i?>=?bound?||?finishing)??advance?=?false;??else?if?((nextIndex?=?transferIndex)?<=?0)?{??i?=?-1;??advance?=?false;??}??else?if?(U.compareAndSwapInt??(this,?TRANSFERINDEX,?nextIndex,??nextBound?=?(nextIndex?>?stride????nextIndex?-?stride?:?0)))?{??bound?=?nextBound;??i?=?nextIndex?-?1;??advance?=?false;??}??}??if?(i?<?0?||?i?>=?n?||?i?+?n?>=?nextn)?{??int?sc;??if?(finishing)?{??//如果所有的節點都已經完成復制工作??就把nextTable賦值給table?清空臨時對象nextTable??nextTable?=?null;??table?=?nextTab;??sizeCtl?=?(n?<<?1)?-?(n?>>>?1);//擴容閾值設置為原來容量的1.5倍??依然相當于現在容量的0.75倍??return;??}??//利用CAS方法更新這個擴容閾值,在這里面sizectl值減一,說明新加入一個線程參與到擴容操作??if?(U.compareAndSwapInt(this,?SIZECTL,?sc?=?sizeCtl,?sc?-?1))?{??if?((sc?-?2)?!=?resizeStamp(n)?<<?RESIZE_STAMP_SHIFT)??return;??finishing?=?advance?=?true;??i?=?n;?//?recheck?before?commit??}??}??//如果遍歷到的節點為空?則放入ForwardingNode指針??else?if?((f?=?tabAt(tab,?i))?==?null)??advance?=?casTabAt(tab,?i,?null,?fwd);??//如果遍歷到ForwardingNode節點??說明這個點已經被處理過了?直接跳過??這里是控制并發擴容的核心??else?if?((fh?=?f.hash)?==?MOVED)??advance?=?true;?//?already?processed??else?{??//節點上鎖??synchronized?(f)?{??if?(tabAt(tab,?i)?==?f)?{??Node<K,V>?ln,?hn;??//如果fh>=0?證明這是一個Node節點??if?(fh?>=?0)?{??int?runBit?=?fh?&?n;??//以下的部分在完成的工作是構造兩個鏈表??一個是原鏈表??另一個是原鏈表的反序排列??Node<K,V>?lastRun?=?f;??for?(Node<K,V>?p?=?f.next;?p?!=?null;?p?=?p.next)?{??int?b?=?p.hash?&?n;??if?(b?!=?runBit)?{??runBit?=?b;??lastRun?=?p;??}??}??if?(runBit?==?0)?{??ln?=?lastRun;??hn?=?null;??}??else?{??hn?=?lastRun;??ln?=?null;??}??for?(Node<K,V>?p?=?f;?p?!=?lastRun;?p?=?p.next)?{??int?ph?=?p.hash;?K?pk?=?p.key;?V?pv?=?p.val;??if?((ph?&?n)?==?0)??ln?=?new?Node<K,V>(ph,?pk,?pv,?ln);??else??hn?=?new?Node<K,V>(ph,?pk,?pv,?hn);??}??//在nextTable的i位置上插入一個鏈表??setTabAt(nextTab,?i,?ln);??//在nextTable的i+n的位置上插入另一個鏈表??setTabAt(nextTab,?i?+?n,?hn);??//在table的i位置上插入forwardNode節點??表示已經處理過該節點??setTabAt(tab,?i,?fwd);??//設置advance為true?返回到上面的while循環中?就可以執行i--操作??advance?=?true;??}??//對TreeBin對象進行處理??與上面的過程類似??else?if?(f?instanceof?TreeBin)?{??TreeBin<K,V>?t?=?(TreeBin<K,V>)f;??TreeNode<K,V>?lo?=?null,?loTail?=?null;??TreeNode<K,V>?hi?=?null,?hiTail?=?null;??int?lc?=?0,?hc?=?0;??//構造正序和反序兩個鏈表??for?(Node<K,V>?e?=?t.first;?e?!=?null;?e?=?e.next)?{??int?h?=?e.hash;??TreeNode<K,V>?p?=?new?TreeNode<K,V>??(h,?e.key,?e.val,?null,?null);??if?((h?&?n)?==?0)?{??if?((p.prev?=?loTail)?==?null)??lo?=?p;??else??loTail.next?=?p;??loTail?=?p;??++lc;??}??else?{??if?((p.prev?=?hiTail)?==?null)??hi?=?p;??else??hiTail.next?=?p;??hiTail?=?p;??++hc;??}??}??//如果擴容后已經不再需要tree的結構?反向轉換為鏈表結構??ln?=?(lc?<=?UNTREEIFY_THRESHOLD)???untreeify(lo)?:??(hc?!=?0)???new?TreeBin<K,V>(lo)?:?t;??hn?=?(hc?<=?UNTREEIFY_THRESHOLD)???untreeify(hi)?:??(lc?!=?0)???new?TreeBin<K,V>(hi)?:?t;??//在nextTable的i位置上插入一個鏈表??????setTabAt(nextTab,?i,?ln);??//在nextTable的i+n的位置上插入另一個鏈表??setTabAt(nextTab,?i?+?n,?hn);??//在table的i位置上插入forwardNode節點??表示已經處理過該節點??setTabAt(tab,?i,?fwd);??//設置advance為true?返回到上面的while循環中?就可以執行i--操作??advance?=?true;??}??}??}??}??}??} ??
get操作
使用String name = map.get(“name”)獲取新增的name信息,現在我們依舊用debug的方式來分析下ConcurrentHashMap的獲取方法get()
public V get(Object key) {Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;int h = spread(key.hashCode()); //計算兩次hashif ((tab = table) != null && (n = tab.length) > 0 &&(e = tabAt(tab, (n - 1) & h)) != null) {//讀取首節點的Node元素if ((eh = e.hash) == h) { //如果該節點就是首節點就返回if ((ek = e.key) == key || (ek != null && key.equals(ek)))return e.val;}//hash值為負值表示正在擴容,這個時候查的是ForwardingNode的find方法來定位到nextTable來//查找,查找到就返回else if (eh < 0)return (p = e.find(h, key)) != null ? p.val : null;while ((e = e.next) != null) {//既不是首節點也不是ForwardingNode,那就往下遍歷if (e.hash == h &&((ek = e.key) == key || (ek != null && key.equals(ek))))return e.val;}}return null; }ConcurrentHashMap的get操作的流程很簡單,也很清晰,可以分為三個步驟來描述:?
1. 計算hash值,定位到該table索引位置,如果是首節點符合就返回?
2. 如果遇到擴容的時候,會調用標志正在擴容節點ForwardingNode的find方法,查找該節點,匹配就返回?
3. 以上都不符合的話,就往下遍歷節點,匹配就返回,否則最后就返回null
?
? ? ?這個get請求,我們需要cas來保證變量的原子性。如果tab[i]正被鎖住,那么CAS就會失敗,失敗之后就會不斷的重試。這也保證了get在高并發情況下不會出錯。我們來分析下到底有多少種情況會導致get在并發的情況下可能取不到值。1、一個線程在get的時候,另一個線程在對同一個key的node進行remove操作;2、一個線程在get的時候,另一個線程正則重排table??赡軐е屡ftable取不到值。那么本質是,我在get的時候,有其他線程在對同一桶的鏈表或樹進行修改。那么get是怎么保證同步性的呢?我們看到e = tabAt(tab, (n - 1) & h)) != null,在看下tablAt到底是干嘛的:
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE); }? ? ? 它是對tab[i]進行原子性的讀取,因為我們知道putVal等對table的桶操作是有加鎖的,那么一般情況下我們對桶的讀也是要加鎖的,但是我們這邊為什么不需要加鎖呢?因為我們用了Unsafe的getObjectVolatile,因為table是volatile類型,所以對tab[i]的原子請求也是可見的。因為如果同步正確的情況下,根據happens-before原則,對volatile域的寫入操作happens-before于每一個后續對同一域的讀操作。所以不管其他線程對table鏈表或樹的修改,都對get讀取可見。
?
size操作
最后我們來看下例子中最后獲取size的方式int size = map.size();,現在讓我們看下size()方法:
public int size() {long n = sumCount();return ((n < 0L) ? 0 :(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :(int)n); } final long sumCount() {CounterCell[] as = counterCells; CounterCell a; //變化的數量long sum = baseCount;if (as != null) {for (int i = 0; i < as.length; ++i) {if ((a = as[i]) != null)sum += a.value;}}return sum; }在JDK1.8版本中,對于size的計算,在擴容和addCount()方法就已經有處理了,JDK1.7是在調用size()方法才去計算,其實在并發集合中去計算size是沒有多大的意義的,因為size是實時在變的,只能計算某一刻的大小,但是某一刻太快了,人的感知是一個時間段,所以并不是很精確。
總結與思考
其實可以看出JDK1.8版本的ConcurrentHashMap的數據結構已經接近HashMap,相對而言,ConcurrentHashMap只是增加了同步的操作來控制并發,從JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+紅黑樹,相對而言,總結如下思考:?
1. JDK1.8的實現降低鎖的粒度,JDK1.7版本鎖的粒度是基于Segment的,包含多個HashEntry,而JDK1.8鎖的粒度就是HashEntry(首節點)?
2. JDK1.8版本的數據結構變得更加簡單,使得操作也更加清晰流暢,因為已經使用synchronized來進行同步,所以不需要分段鎖的概念,也就不需要Segment這種數據結構了,由于粒度的降低,實現的復雜度也增加了?
3. JDK1.8使用紅黑樹來優化鏈表,基于長度很長的鏈表的遍歷是一個很漫長的過程,而紅黑樹的遍歷效率是很快的,代替一定閾值的鏈表,這樣形成一個最佳拍檔?
4. JDK1.8為什么使用內置鎖synchronized來代替重入鎖ReentrantLock,我覺得有以下幾點:因為粒度降低了,在相對而言的低粒度加鎖方式,synchronized并不比ReentrantLock差,在粗粒度加鎖中ReentrantLock可能通過Condition來控制各個低粒度的邊界,更加的靈活,而在低粒度中,Condition的優勢就沒有了?
5. JVM的開發團隊從來都沒有放棄synchronized,而且基于JVM的synchronized優化空間更大,使用內嵌的關鍵字比使用API更加自然?
6. 在大量的數據操作下,對于JVM的內存壓力,基于API的ReentrantLock會開銷更多的內存,雖然不是瓶頸,但是也是一個選擇依據
--------------------------
線性表及ArrayList/LinkedList源碼分析總結
?一.線性表
定義:零個或者多個元素的有限序列。
也就是說它得滿足以下幾個條件:
??①該序列的數據元素是有限的。
??②如果元素有多個,除了第一個和最后一個元素之外,每個元素只有一個前驅元素和一個后驅元素。
??③第一元素沒有前驅元素,最后一個元素沒有后繼元素。
??④序列中的元素數據類型相同。
則這樣的數據結構為線性結構。在復雜的線性表中,一個數據元素(對象)可以由若干個數據項組成,組成一張數據表,類似于數據庫。
二.線性表的抽象數據類型
1.相關概念
抽象數據類型(abstract data type,ADT)是帶有一組操作的一組對象的集合。這句話表明,線性表的抽閑數據類型主要包括兩個東西:數據集合和數據集合上的操作集合。
①數據集合:我們假設型如a0,a1,a2,...an-1的表,我們說這個表的大小是N,我們將大小為0的標稱為空表。
②集合上的操作:一般常用的操作包括,查找,插入,刪除,判斷是否為空等等。
2.線性表的順序儲存結構
線性表的順序儲存結構,指的是用一段地址連續的儲存單元依次儲存線性表的數據元素。我們可以用一維數組實現順序儲存結構,并且上述對表的所有操作都可由數組實現。但是一維數組有一個很大缺點就是它得長度是固定的,也就是我們創建的額時候就聲明的長度,一旦我們要存儲的數據超出了這個長度,我們就不得不手動的創建一個更長的新數組并將原來數組中的數據拷貝過去。
int [] arr = new int[10]......int [] newArr = new int[arr.length*2]for(int i=0;i<arr;i++){newArr[i] = arr[i];}arr = new Arr;顯然這樣做非常的不明智,因此java中的ArrayList就應用而生了。ArrayList也就是傳說中的動態數組,也就是我們可以隨著我們數據的添加動態增長的數組。
??實際上不管是簡單數組也好,動態數組也好,在具體的操作中都存在這樣的問題:
??①如果我們在線性表的第i個位置插入/刪除一個元素,那么我們需要怎么做呢?首先我們得從最后一個元素開始遍歷,到第i個位置,分辨將他們向后/前移動一個位置;在i位置處將要插入/刪除的元素進行相應的插入/刪除操作;整體的表長加/減1.
??②如果我們在線性表的第一個位置插入/刪除一個元素,那么整個表的所有元素都得向后/向前移動一個單位,那么此時操作的時間復雜度為O(N);如果我們在線性表的最末位置進行上面兩種操作,那么對應的時間復雜度為O(1)——綜合來看,在線性表中插入或者刪除一個元素的平均時間復雜度為O(N/2)。
??總結一下,線性表的缺點——插入和刪除操作需要移動大量的元素;造成內存空間的"碎片化"。這里有些童鞋就說了,ArrayList是一個線性表吧,我再ArrayList中添加/刪除一個元素直接調用add()/remove()方法就行了啊,也是一步到位啊——這樣想就不對了,如果我們看ArrayList的源碼就會發現,實際上他內部也是通過數組來實現的,remove()/add()操作也要通過上面說的一系列步驟才能完成,只不過做了封裝讓我們用起來爽。之后我們會通過源碼分析ArrayList等的內部的實現方式。
??當然了,優缺點就會有優點——由于順序儲存結構的元素數目,元素相對位置都確定的,那么我們在存取確定位置的元素數據的時候就比較方便,直接返回就行了。
3.線性表的鏈式儲存結構
上面我們說過,順序結構的最大不足是插入和刪除時需要移動大量元素;造成內存空間的"碎片化。那么造成這中缺點的原因是什么呢?原因就在于這個線性表中相鄰兩元素之間的儲存位置也就有相鄰關系,也就是說元素的位置是相對固定的,這樣就造成了"牽一發而動全身"的尷尬局面;同時,什么是"碎片化"呢?就是說上述順序結構中,以數組為例來說,如果我們先在內存中申請一個10位長度的數組,然后隔3個位置放一個5位長度的數組,這個時候如果再來一個8位長度的數組,那么顯然不能放在前兩個數組之間(他們之間只有三個空位),那只能另找地方,中間的這三個位置就空下了,久而久之類似的事情多了就發生了"碎片化"的現象。
(1)簡單鏈表
為了解決上述兩個問題,我們前輩的科學家們就發明了偉大的"鏈式儲存結構",也就是傳說中的鏈表(Linked List)。鏈表的結構有兩大特點:
??①用一組任意的儲存單元儲存線性表的數據元素,這組儲存單元可以是連續的,也可以是不連續的。這就意味著,這些數據元素可以存在內存中任意的未被占用的位置。
??②在上面的順序數據結構中,每個數據元素只要儲存數據信息就可以了,但是在鏈表中,由于數據元素之間的位置不是固定的,因此為了保證數據元素之間能相互找到前后的位置,每個數據元素不僅需要保存自己的數據信息,還需要保存指向下一個指針位置的信息。
簡單鏈表.png
如圖,我們畫出了簡單鏈表的示意圖。鏈表由一系列結點組成,這些結點不必再內存中相連,每個結點含有表元素(數據域)和到包含該元素后繼結點的鏈(link)(指針域)。
??對于一個線性表來說,總得有頭有尾,鏈表也不例外。我們把第一個鏈表儲存的位置叫做"頭指針",整個鏈表的存取就是從頭指針開始的。之后的每一個結點,其實就是上一個結點的指針域指向的位置。最后一個結點由于沒有后繼結點,因此它的指針域為NULL。
??有時我們會在第一個指針前面加上一個頭結點,頭結點的數據域可以不表示任何數值,也可以儲存鏈表長度等公共信息,頭結點的指針域儲存指向第一個結點的位置的信息。
??需要注意的是,頭結點不是鏈表的必要要素,但是有了頭結點,在對第一個結點之前的位置添加/刪除元素時,就與其他元素的操作統一了。
(1.1)簡單鏈表的讀取
??在上面的線性表的順序儲存結構中,我們知道它的一個優點就是存取確定位置的元素比較的方便。但是對于鏈式儲存結構來說,假設我們要讀取i位置上的元素信息,但是由于我們事先并不知道i元素到底在哪里,所以我們只能從頭開始找,知道找到第i個元素為止。由于這個算法的時間復雜度取決于i的位置,最壞的情況就是O(n),即元素在末尾。
??由于單鏈表沒有定義表長,所以我們沒有辦法使用for循環來查找。因此這里查找算法的核心思想是"工作指針后移",也就是當前的元素查完之后,移動到下一個位置,檢查下一個位置的元素,以此循環,直到找到第i個位置的元素。
??可以看到,這是鏈表結構的一個缺點,對于確定位置的元素查找平均時間復雜度為O(N/2)。
(1.2)簡單鏈表的插入與刪除
??當然了,有缺點就有優點,我們說過鏈表解決了順序表中"插入和刪除時需要移動大量元素"的缺點。也就是說,鏈表中插入刪除元素不需要移動其他的元素,那么鏈表是怎么做到的呢?
單鏈表的刪除與插入.png
我們用“.next”表示一個節點的后繼指針,X.next = Y表示將X的后繼指針指向Y節點。這里不得不說一下,由于Java中沒有指針的概念,而是引用(關于指針和引用的區別我們這里不做過多的說明,他們本質上都是指向儲存器中的一塊內存地址)。在java語言描述的鏈表中,上面所說的“指針域”更準確的說是“引用域”,也就是說,這里的x.next實際上是X的下一個節點x->next元素的引用。說的更直白一點,我們可以簡單的理解為x.next = x->next,就像我們經常做的Button mbutton = new Button();一樣,在實際操作中我們處理mbutton這個引用實際上就是在處理new Button()對象。
??我們先說刪除的做法,如上圖所示,我們假設一個x為鏈表節點,他的前一個結點為x->prev,后一個結點為x->next,我們用x.next表示他的后繼引用。現在我們要刪除x結點,需要怎么做呢?實際上很簡單,直接讓前一個結點元素的后繼引用指向x的下一個節點元素(向后跳過x)就可以了x->prev.next = x.next。
??同理插入一個節點呢?首先我們把Node的后繼節點Next的變成P的后繼節點,接著將Node的后繼引用指向P,用代碼表示就是:P.next = Node.next; Node.next = P;。解釋一下這兩句代碼,P.next = Node.next;實際上就是用Node.next的引用覆蓋了P.next的引用,P.next的引用本來是一個空引用(剛開始還沒插入鏈表不指向任何節點),而Node.next本來是指向Next節點的,這一步操作完之后實際上P.next這個引用就指向了Next節點;這個時候Node.next = P;這句代碼中,我們將Node.next這個引用重新賦值,也就是指向了P這個節點,這樣整個插入過程就完成了。
??為什么我們啰里啰嗦的為兩句代碼解釋了一堆內容呢?就是要強調,上面兩個步驟順序是不能顛倒的!為什么呢?我們不妨顛倒順序看一下——我們首先進行Node.next = P;這一步,開始的時候,P的引用域是空的(或者指向無關的地址),此時如果我們進行了這一步,那么Node.next這個引用就指向了P節點,這個時候我們再進行P.next = Node.next這一步就相當于P.next = p,p.next引用域指向自己,這沒有任何意義。
(2)雙鏈表(LinkedList)
上面我們說了簡單鏈表的各種事項,但是在實際的運用中,為了我們的鏈表更加靈活(比如既可以工作指針后移向后查找,也可以指針向前移動查詢),我們運用更多的是雙向鏈表,即每個節點持有前面節點和后面節點的引用。java的雙向鏈表通過LinkedList類實現,通過閱讀源碼我們在該類中可以看到關于結點的定義:
private static class Node<E> {E item;Node<E> next;Node<E> prev;Node(Node<E> prev, E element, Node<E> next) {this.item = element;this.next = next;this.prev = prev;}}Java雙鏈表中的結點是通過一個靜態內部類定義。一個結點包含自身元素(item),該節點對后一個節點的引用(next),該節點對前一個節點的引用(prev)。
(2.1)雙鏈表的刪除
雙鏈表的刪除.png
如圖,我們假設在一個雙向鏈表中有一個節點X,他的前繼節點是prev,后繼節點是next.現在我們展示刪除節點X的源碼(sources/ansroid-24/java/util/LinkedList):
public boolean remove(Object o) { //刪除分為兩種情況,一種是刪鏈表中的null元素,一種是刪正常元素if (o == null) { //刪除的具體操作沒什么區別,都是從第一個開始,向后移動工作指針,直到找到符合條件的for (Node<E> x = first; x != null; x = x.next) {if (x.item == null) {unlink(x);return true;}}} else {for (Node<E> x = first; x != null; x = x.next) {if (o.equals(x.item)) {unlink(x);return true;}}}return false;} E unlink(Node<E> x) { //上面是找元素,這個方法是真正刪除元素final E element = x.item; //x.item表示當前的x節點final Node<E> next = x.next; //x.next表示x后繼引用,next同final Node<E> prev = x.prev; //x.prev是x的前繼引用,prev同......if (prev == null) { //如果prev為null,則表示x為第一個結點,此時刪除x的做法只需要將x的first = next; //下一個節點設為第一個節點即可,first表示鏈表的第一節點。} else {① prev.next = next; //否則的話,x為普通節點。那么只要將x的前一個結點(prev)的后繼引用指向x的下一個 x.prev = null; //節點就行了,也就是(向后)跳過了x節點。x的前繼引用刪除,斷開與前面元素的聯系。}if (next == null) {last = prev; //如果x節點后繼無人,說明他是最后一個節點。直接把前一個結點作為鏈表的最后一個節點就行} else {② next.prev = prev; //否則x為普通節點,將x的下一個節點的前繼引用指向x的前一個節點,也就是(向前)跳過x. x.next = null; //x的后繼引用刪除,斷了x的與后面元素的聯系}x.item = null; //刪除x自身size--; //鏈表長度減1modCount++;return element;}我們在在上面的源碼中標出了刪除一個元素所需要的兩個步驟,即prev節點中原本指向X節點的后繼引用,現在向后越過X,直接指向next節點①(prev.next = next;);next節點中原本指向X節點的前繼引用,現在向前越過X節點,直接指向prev節點。;然后就是將X的前繼引用和后繼引用都設置為null,斷了他與前后節點之間的聯系;最后將X節點本身置為null,方便內存的釋放。
(2.2)雙鏈表的插入
雙鏈表的添加.png
這里我們選取較為容易的,在指定位置添加一個元素的add方法分析(sources/ansroid-24/java/util/LinkedList):
public void add(int index, E element) {checkPositionIndex(index);if (index == size) //size表示整個鏈表的長度,如果指定的索引等于鏈表的長度,那么就把這個元素添加到鏈表末尾linkLast(element);else //否則執行linkBefore()方法,添加到末尾之前的元素前面linkBefore(element, node(index));}這里我們看一下這個node(index)方法:
/*** Returns the (non-null) Node at the specified element index.返回指定索引處的非空元素*/Node<E> node(int index) {if (index < (size >> 1)) { //size >> 1,表示二進制size右移一位,相當于size除以2Node<E> x = first;for (int i = 0; i < index; i++) //如果指定的索引值小于size的一般,那么從第一個元素開始,x = x.next; //指針向后移動查找,一直到指定的索引處return x;} else { //else,從最后一個元素開始,向前查找Node<E> x = last;for (int i = size - 1; i > index; i--)x = x.prev;return x;}} /*** Inserts element e before non-null Node succ.*/void linkBefore(E e, Node<E> succ) { //e為我們要插入的元素,succ為我們要插入索引處的元素final Node<E> pred = succ.prev; //先將succ的前繼引用(或者前一個結點的元素)保存在pred變量中final Node<E> newNode = new Node<>(pred, e, succ); //創建一個新節點,也就是我們要插入的這個元素//注意new Node<>(pred, e, succ);這三個參數,可以參照(1.3)處的源碼① succ.prev = newNode; //succ的前繼引用指向NewNodeif (pred == null) //如果這個前繼引用為空,那就說明我們插入的元素是在最前面first = newNode; //直接讓newNode做第一個元素就行了else② pred.next = newNode; //否則的話,在讓pred的后繼引用指向newNode節點size++;modCount++;}可以看到這里實際上和簡單鏈表的添加一樣,也是分兩步走,而且兩步的順序不能顛倒。這里需要說明的一點是,我們在圖上用灰色的點線多畫了兩條箭頭,其中①②分別表示的是新元素的前繼引用和后繼引用分別指向pred和succ的賦值過程。在筆者參考的《大話數據結構》中,雙鏈表的添加算上點線的那兩步一共是四步,但實際上在java中創建newNode的時候new Node<>(pred, e, succ)這句代碼已經一次性做完了上述兩部過程,真正做事的就是我們在圖中用黑線標出的那兩步。
三.JAVA中相關的collection集合
java集合主要由兩個接口派生而出——Collection接口和Map接口。collection儲存一組類型相同的對象,且每個位置只能保存一個元素;Map保存的是鍵值對(key-value)。我們本節重點來說Collection集合。
??對于collection接口中的方法,我我們可以通過他的源碼來看:
上面我們列出了Collection集合的幾個主要的方法,從注釋中我們可以很清楚的知道他們的用途。
1.Iterable/Iterator接口
我們看上面的collection接口的時候,collection接口繼承了Iterable類,我們來看看這個Iterable類:
public interface Iterable<T> {/*** Returns an iterator over elements of type {@code T}.** @return an Iterator.*/Iterator<T> iterator();/** @param action The action to be performed for each element* @throws NullPointerException if the specified action is null* @since 1.8*/default void forEach(Consumer<? super T> action) {Objects.requireNonNull(action);for (T t : this) {action.accept(t);}}...... }我們先看一下第二個函數forEach(Consumer<? super T> action),在java8中,Iterator新加了這個個forEach循環(注意與java5中的foreach循環的區別,大小寫,用法不同),主要用于更加方便的循環遍歷集合中的元素并對每一個元素迭代做出相應的處理,其中的參數Consumer<? super T> action就是我們對集合中的元素所施加的操作。舉例說明具體的用法:
傳統方式循環List:
在java8中使用forEach循環+Lambda表達式遍歷list:
List<String> items = new ArrayList<>();items.add("A");items.add("B");items.add("C");items.add("D");items.add("E");//lambda//Output : A,B,C,D,Eitems.forEach(item->System.out.println(item));//Output : Citems.forEach(item->{if("C".equals(item)){System.out.println(item);}});回到Iterable類中,我們可以看到他還返回了一個Iterator接口的實例。而這個接口類是什么呢?
public interface Iterator<E> {boolean hasNext();E next();default void remove() {throw new UnsupportedOperationException("remove");}default void forEachRemaining(Consumer<? super E> action) {Objects.requireNonNull(action);while (hasNext())action.accept(next());} }可以看到,該接口類中一共有四個方法。其中forEachRemaining()方法是java8之后新增的方法,主要的作用和上面我們說過的foreach()循環是一樣的,用法也基本相同。
??Iterator類的作用是一個迭代器,主要用于操作colloection集合類中的元素。Iterator類必須依附于Collection對象,Iterator本身并不提供盛裝對象的能力。如果需要創建Iterator對象,則必需有一個被迭代的集合,如果沒有Colletion集合,Iterator也沒有存在的價值。
??Iterator類中的結果方法——hasNext(),用于判斷在遍歷時collection集合中是否還有剩下的元素;next()用于返回遍歷時集合中的下一個元素,這兩個方法很好理解。唯獨這個remove()方法有一些注意事項需要說明一下:
??我們可以先看看這個方法的注釋:
翻譯一下:從底層集合中刪除此迭代器返回的最后一個元素。這個方法只能在每一次調用完next()方法之后調用一次。如果next()方法沒有調用,或者remove()方法在上一次next()方法調用完了之后,又調用了一次,則會拋出IllegalStateException異常。
結合這段注釋,我們交代一下next()方法調用的幾個注意事項:
??①remove()只能在next()方法調用完后緊接著調用一次,此時他刪除的是在他之前調用的那個next()返回的元素
??②remove()在調用之心完之后,只能等待下一次next()方法執行完了之后,才可以調用。
同時我們還需要注意:③在使用Iterator迭代的過程中,我們不能手動修改collection集合中的元素,即不能手動調用collection類本身的remove(),add(),clear()等方法,只能調用Iterator的remove()方法。舉個例子:
上面是一段"正常"的程序,這里需要說明的一點,上一段代碼星號處我們將book的賦值為“666666”,但是當我們再次打印出books的集合時會發現集合中的元素沒有什么改變,還是原來的值。這說明——當使用Iterator迭代器進行迭代時,Iterator并不是把集合元素本身傳遞給了迭代變量,而是把集合元素的額值出給了迭代變量,因此我們在后邊進行的各種賦值并不影響集合本身的元素。
public class IteratorTest{public static void main(String[] args){...Iterator it = books.iterator();while(it.hasNext()){String book = (String)it.next();if("book.equals("hhhhhh"))books.remove(book); //錯誤,在迭代時調用了修改集合本身的方法}} }這是一段有問題的代碼。這里還需要注意一點,這個Iterator中的remove()和collection本身的remove(o)方法是不一樣的,一個有參數一個無參數。而且子刪除集合中元素時,我們優先考慮用Iterator的remve()方法,因為他有更高的效率,為什么呢?這里我們先作為一個遺留問題,后面我們再詳細證明。
??同樣我們可以用java5中的foreach循環來遍歷collection集合中的元素,這個更加簡潔一些:
2.List接口,ArrayList類,LinkedList類
colection接口有三個子類實現接口:List(有序集合,元素可以重復),Queue(對列),Set(無序集合,元素不可重復),其中List又有兩個最常見的實現類:ArrayList(動態數組)和LinkedList(雙向鏈表),這兩個也就是我們前面一直說的線性表的順序儲存結構和鏈式儲存結構。List接口作為collection接口的子類,當然實現了collection接口中的所有方法。并且由于List是有序集合,因此List集合中增加了一些根據元素索引來操作集合的方法。
(1)List源碼解析
public interface List<E> extends Collection<E> {...... //省略一些collection中展示過的方法/**同時List接口定義了一些自己的方來實現“有序”這一功能特點*//***返回列表中指定索引的元素*/E get(int index);/***設定某個列表索引的值*/E set(int index, E element);/***在指定位置插入元素,當前元素和后續元素后移*這是可選操作,類似于順序表的插入操作*/void add(int index, E element);/*** 刪除指定位置的元素(可選操作)*/E remove(int index);/*** 獲得指定對象的最小索引位置,沒有則返回-1*/int indexOf(Object o);/*** 獲得指定對象的最大索引位置* 可以知道的是若集合中無給定元素的重復元素下* 其和indexOf返回值是一樣的*/int lastIndexOf(Object o);/***一種更加強大的迭代器,支持向前遍歷,向后遍歷插入刪除操作*/ListIterator<E> listIterator(); *ListIterator<E> listIterator(int index); */*** 返回索引fromIndex(包括)和toIndex(不包括)之間的視圖。*/List<E> subList(int fromIndex, int toIndex); }這里解釋一下ListIterator類,該類繼承自Iterator類,提供了專門操作List的方法。ListIterator接口在Iterator接口的基礎上增加了洗下面幾個關鍵的方法:
public interface ListIterator<E> extends Iterator<E> {boolean hasNext();E next();void remove();/**下面是在Iterator基礎上增加的方法*/boolean hasPrevious(); //是否還有前繼元素E previous(); //返回前一個元素int nextIndex(); //返回下一個元素的索引int previousIndex(); //返回前一個元素的索引void set(E e); //替換由上一次next()或者previous()方法返回的元素.void add(E e); //在上一次由next()方法返回的元素之前,或者在上一次previous()方法返回的元素之后,添加一個元素 }可以看到,ListIterator增加了向前迭代的功能(Iterator只能向后迭代),而且ListIterator可以通過add()方法向List中添加元素(Iterator只能刪除)。
(2)ArrayList源碼解析
(2.1)ArrayList類的頭部:
public class ArrayList<E> extends AbstractList<E>implements List<E>, RandomAccess, Cloneable, java.io.Serializable{RandomAccess:RandmoAccess是一個標記接口,用于被List相關類實現。他主要的作用表明這個相關類支持快速隨機訪問。在ArrayList中,我們即可以通過元素的序號快速獲取元素對象——這就是快速隨機訪問。稍后,我們會比較List的“快速隨機訪問”和“通過Iterator迭代器訪問”的效率。
Cloneable:實現該接口的類可以對該類的實例進行克隆(按字段進行復制)
Serializable:ArrayList支持序列化,能通過序列化去傳輸。
(2.2)ArrayList屬性
從ArrayList的屬性元素我們可以看出,他的內部是由一個數組(elementData)實現的。這里需要說明一下transient Object[] elementData;這句中的transient關鍵字:
??我們都知道一個對象只要實現了Serilizable接口,這個對象就可以被序列化,java的這種序列化模式為開發者提供了很多便利,我們可以不必關系具體序列化的過程,只要這個類實現了Serilizable接口,這個類的所有屬性和方法都會自動序列化。
??然而在實際開發過程中,我們常常會遇到這樣的問題,這個類的有些屬性需要序列化,而其他屬性不需要被序列化,打個比方,如果一個用戶有一些敏感信息(如密碼,銀行卡號等),為了安全起見,不希望在網絡操作(主要涉及到序列化操作,本地序列化緩存也適用)中被傳輸,這些信息對應的變量就可以加上transient關鍵字。換句話說,這個字段的生命周期僅存于調用者的內存中而不會寫到磁盤里持久化。
??總之,java 的transient關鍵字為我們提供了便利,你只需要實現Serilizable接口,將不需要序列化的屬性前添加關鍵字transient,序列化對象的時候,這個屬性就不會序列化到指定的目的地中。
(3.1)ArrayList構造方法
可以看到ArrayList有三種構造方法:
??①指定長度的初始化
??②初始化一個空ArrayList,此時會自動將該初始化的ArrayList長度變為默認長度10。
??③將指定的集合作為參數提供給初始化的ArrayList,并在該構造函數內部,先通過toArray將該集合強制轉化為Object類型,然后通過Arrays.copyOf方法將其賦給elementData,也就是ArrayList緩存元素的地方。
(3.4)ArrayList的增加
這里我們需要注意的是~~在數組的增加過程中,有兩個過程是是比較耗費性能的:數組擴容(ensureCapacityInternal)與數組復制(System.arraycopy),這兩個步驟在上面四個添加方法中都存在,待會我們會詳細分析。
(3.5)ArrayList的刪除
增加和刪除使我們在ArrayLisy中比較常用的兩個方法。下面我們來說說上面遺留的那個關于擴容和復制的問題。首先我們來看看ensureCapacityInternal方法的源碼:
private void ensureCapacityInternal(int minCapacity) { //minCapacity就是我們需要的最小容量if (elementData == EMPTY_ELEMENTDATA) { //如果此時elementData等于空數組minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); //如果minCapacity比默認值10小,則//minCapacity為10,否則為他自己。}ensureExplicitCapacity(minCapacity);}private void ensureExplicitCapacity(int minCapacity) {modCount++;// overflow-conscious code//如果當前的數組長度小于我們所需要的minCapacity值(當前數組長度不夠),則進行擴容if (minCapacity - elementData.length > 0)grow(minCapacity);}private void grow(int minCapacity) {// overflow-conscious codeint oldCapacity = elementData.length; //oldCapacity等于當前數組的長度//oldCapacity >> 1,表示二進制的向右移一位,相當于十進制的除以2int newCapacity = oldCapacity + (oldCapacity >> 1); //newCapacity = 1.5 * oldCapacityif (newCapacity - minCapacity < 0)newCapacity = minCapacity; //如果此時newCapacity還是小于我們所需要的minCapacity,那就讓他等于minCapacityif (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);// minCapacity is usually close to size, so this is a win://以這個新的長度為標準重新創建,將原來數組中的元素拷貝一份到新的數組中去。Arrays.copyOf底層實現是System.arrayCopy()elementData = Arrays.copyOf(elementData, newCapacity);}擴容的方法中包含三個個過程:
??①判斷需要的大小(minCapacity)是否超出了默認長度10.
??②超出了就開始擴容,用他的1.5倍長度去和minCapacity作比較(有些java版本是2.5倍)。
??③如果1.5倍大小還是小于所需要的minCapacity大小,那就將原來的元素復制到一個以minCapacity為長度的新數組中,并將elementData引用指向這個新數組。
??可以看到,擴容的過程伴隨著數組的復制。如果數組初試容量過小,假設為默認的10個大小,而我們使用ArrayList的操作時需要不斷的增加元素。在這種場景下,不斷的增加,意味著數組需要不斷的進行擴容,擴容的過程就是數組拷貝System.arraycopy的過程,每一次擴容就會開辟一塊新的內存空間和數據的復制移動(但是數組復制不需要開辟新內存空間,只需將數據進行復制),這樣勢必對性能造成影響。那么在這種以寫為主(寫會擴容,刪不會縮容)場景下,提前預知性的設置一個大容量,便可減少擴容的次數,提高了性能。需要注意一點的是ensureCapacity()方法是public的,我們可以通過它手動的增加容量。
??增加元素可能會進行擴容,而刪除元素卻不會進行縮容,如果在以刪除為主的場景下使用list,一直不停的刪除而很少進行增加,或者數組進行一次大擴容后,我們后續只使用了幾個空間,那就會造成控件的極大浪費。這個時候我們就可以將底層的數組elementData的容量調整為當前實際元素的大小(縮容),來釋放空間。
總結一下:
??ArrayList底層以數組實現,允許重復,默認第一次插入元素時創建數組的大小為10,超出限制時會增加50%的容量,每次擴容都底層采用System.arrayCopy()復制到新的數組,初始化時最好能給出數組大小的預估值(采用給定值初始化)。
(3.6)ArrayList的遍歷方式
ArrayList支持三種遍歷方式
①通過迭代器Itertor去遍歷
②隨機訪問,通過索引值去遍歷,由于ArrayList實現了RandomAccess接口,它支持通過索引值去隨機訪問元素。
③foreach循環遍歷
??下面我們用三段程序來測試這三種遍歷方法哪一個效率最高,同時展示三種遍歷的寫法。為了測量更為精準,我們新建三個類分別測試——randomAccessTest.java;iteratorTest.java;foreachTest.java。同時我們采用多次測量(筆者用的eclipse測試時,測試結果經常跳票)并采用納秒計時(毫秒誤差慘不忍睹)。
最終的結果大致穩定為:
randomAccessTime = 7x10^5 ns iteratorTime = 6x10^6ns foreachTime = 5x10^6ns可以看到,雖然結果經常跳票,但八九不離十,randomAccessTime顯然是用時最快的,畢竟少了一個數量級,這點機器還是沒有算錯的。也就是說遍歷ArrayList時,使用隨機訪問(即,通過索引序號訪問)效率最高,這點毋庸置疑,使用迭代器遍歷的效率最低(這點是網上的答案,由于兩者的測試結果處于同一個數量級,加上機器誤差,這點筆者很難證實,讀者可以自行驗證)。
??其實產生上面結果,我們并不感到意外,因為關于randomAccessTime這個接口的注釋中就已經很明確的說明了這個問題:
(3)LinkedList源碼解析
上面我們在講雙鏈表的時候已經講了linkedList的remove(),add()等關鍵方法,以及LinkedList的一個結點(Node)的構成。下面我們來講一下LinkedList剩余的一些知識點:
(3.1)LinkedList的頭
可以看到LinkedList是一個繼承自AbstractSequentialList的雙鏈表,他可以被當做堆棧,隊列(實現了List接口),
雙端隊列(實現了Deque接口)使用。同時LinkedList 實現了Cloneable接口,即覆蓋了函數clone(),能克隆。LinkedList 實現java.io.Serializable接口,這意味著LinkedList支持序列化,能通過序列化去傳輸。
(3.2)LinkedList的屬性元素
其中size就是list的數量,和ArrayList一樣。這個Node<E> first和Node<E> last就是節點的前繼引用和后繼引用,Node表示鏈表上的一個結點。這里再貼一遍代碼,免得你忘了:
private static class Node<E> {E item;Node<E> next;Node<E> prev;Node(Node<E> prev, E element, Node<E> next) {this.item = element;this.next = next;this.prev = prev;}}(3.3)LinkedList的構造函數
/**構造一個空的構造函數,這個構造函數,也真夠空的**/public LinkedList() {}/**構造一個包含指定collection元素的表,這些元素按照collection的迭代器返回的順序排列**/public LinkedList(Collection<? extends E> c) {this();addAll(c);}LinkedList就兩個構造函數,一個空構造函數,一個包含指定collection的構造函數。
(3.4)LinkedList的增加方法
上面我們在講雙鏈表的時候講過在指定位置插入元素的add(int index, E element)方法,現在我們補充一下其他幾種添加方法:
①在雙鏈表的尾部添加一個元素:
②在指定索引處插入一個集合
public boolean addAll(int index, Collection<? extends E> c) {checkPositionIndex(index); //越域判斷Object[] a = c.toArray();int numNew = a.length; // 要插入元素的個數if (numNew == 0)return false;Node<E> pred, succ; //申明兩個節點類的前繼,后繼引用(這個循環就是在找出原列表中插入索引處的前后引用)if (index == size) { //索引等于鏈表的長度,說明是要求插在鏈表的最后面succ = null;pred = last; //此時后繼引用為空,前繼引用指向原鏈表的最后一個元素} else {succ = node(index); //否則后繼引用指向原鏈表索引處的元素。node()方法我們之前講過,二分法查找索引處元素pred = succ.prev; //前繼引用指向原鏈表索引處元素的前一個元素,完成插入}for (Object o : a) { //遍歷我們要插入的這個集合E e = (E) o;//新建一個結點,以pred作為前繼引用,該for循環中每次遍歷得到的collection集合中的e作為結點本身元素,//null作為后繼引用。在第一次進行該循環時,通過上一個if的賦值,pred指向原鏈表中指定索引處的前一個元素。Node<E> newNode = new Node<>(pred, e, null); //在第一次循環遍歷的時候,這個新節點就是collection集合的第一個元素if (pred == null) //如果pred為空,說明原鏈表為空first = newNode; //新節點等于第一個節點elsepred.next = newNode; //否則,原鏈表中指定索引處的前一個元素 的后繼引用指向這個新節點。pred = newNode; //然后將這個pred指向這個新的節點(在第一次循環中,這個新節點表示collection集合中的第一個元素),相當于工作指針后移,然后重復上述過程。}//上述循環結束后,pred指針已經指向了collection集合的最后一個元素,此時由于succ沒動過,因此他還是指向原鏈表中指定索引處的后一個元素if (succ == null) { //如果succ為null,和第一個if中的情況一樣,說明這是在原鏈表的末尾插入元素last = pred; //直接讓此時的pred也就是collection中的最后一個元素作為插入后鏈表的最后一個元素就可以了} else { //否則的話說明是在原鏈表中間插入元素pred.next = succ; //此時讓collection中最后一個元素的后繼指針指向 原鏈表索引處的后一個元素,完成差誒工作succ.prev = pred; //同時讓succ的前繼指針指向collection的最后一個元素,完成雙向鏈表的建立}size += numNew; //增加鏈表長度modCount++;return true;}這段代碼中,注釋已經寫的很詳細了,難點就是for (Object o : a)這個foreach循環中,我們插入的collection元素是如何建立雙鏈表聯系的,讀者務必要仔細分析流程。
(3.5)LinkedList的刪除方法
??刪除的方法我們上面講雙鏈表的時候已經說的很詳細了,這個沒有什么好說的,大家可以翻上去復習一下。這里我們通過LinkedList的remove()方法的幾種形式,來講一下算法選型的問題。
??這個例子的來源于《數據結構與算法分析(java語言描述)》這本書,筆者覺得很典型。題目的要求是,給出一個線性表,將表中所有偶數項全部刪除。
①首先第一種算法,使用普通for循環:
運行的結果是:
usuallyforLinkedlistTime = 57ms usuallyforArraylistTime = 7ms如果我們將其中的線性表大小SIZE改為20000(擴大兩倍),得到結果為:
usuallyforLinkedlistTime = 232ms usuallyforArraylistTime = 29ms很顯然,對于ArrayList和LinkedList而言,這個算法都是時間復雜度為O(N^2)的二次算法。
public static void usuallyfor(List<Integer> list){for(int i=0; i<list.size(); i++){if(list.get(i) % 2 == 0){list.remove(i);}}}這段代碼中,對于LinkedList而言,list.get(i)方法是O(N)時間,慢在尋址;而他的remove()方法確實O(1)的。對于ArrayList而言,他的get(i)方法是O(1)的,但是remove(i)方法卻是O(N)的,因為只要刪除一個元素,后面的元素都要跟著向前移位,并伴隨著數組的復制拷貝等耗時操作;但是他的get(i)卻是O(1)的。
??所以無論對誰,這種算法都不是明智的選擇。但是整體上來看,ArrayList用時更少一些(1/8的量)。
②使用迭代器Iterator
結果為:
iteratorLinkedlistTime = 2ms iteratorArraylistTime = 10ms將其中的線性表大小SIZE改為20000(擴大兩倍),得到結果為:
iteratorLinkedlistTime = 4ms iteratorArraylistTime = 34ms顯然,此時LikedList變成了O(N)一次時間,而ArrayList變成了O(N^2)二次時間,并且LinkedList所用的時間大大小于ArrayList所用的時間。為什么呢?因為此時用迭代器循環遍歷時,對于linkdList的next()方法,是O(1)時間關系,remove()也是一次時間關系;但是對于ArrayList而言,rwmove()仍然是O(N)時間關系。
??從這兩個例子中我們可以體驗到,算法的強大之處,實現同樣功能,采用不同的算法,成敗異變,功業相反~~妙哉!
3.總結——ArrayList和LinkedList的比較
寫的太多了,不知道該怎么總結,這里直接照搬一下Java集合干貨系列-(二)LinkedList源碼解析這篇文章最后的總結,這篇文章寫得很好,推薦讀者去看一下
??(1)順序插入速度ArrayList會比較快,因為ArrayList是基于數組實現的,數組是事先new好的,只要往指定位置塞一個數據就好了;LinkedList則不同,每次順序插入的時候LinkedList將new一個對象出來,如果對象比較大,那么new的時間勢必會長一點,再加上一些引用賦值的操作,所以順序插入LinkedList必然慢于ArrayList
??(2)基于上一點,因為LinkedList里面不僅維護了待插入的元素,還維護了Entry的前置Entry和后繼Entry,如果一個LinkedList中的Entry非常多,那么LinkedList將比ArrayList更耗費一些內存
??(3)數據遍歷的速度,看最后一部分,這里就不細講了,結論是:使用各自遍歷效率最高的方式,ArrayList的遍歷效率會比LinkedList的遍歷效率高一些
??(4)有些說法認為LinkedList做插入和刪除更快,這種說法其實是不準確的:
??①LinkedList做插入、刪除的時候,慢在尋址,快在只需要改變Node前后引用
??②ArrayList做插入、刪除的時候,慢在數組元素的批量copy,快在尋址
??所以,如果待插入、刪除的元素是在數據結構的前半段尤其是非常靠前的位置的時候,LinkedList的效率將大大快過ArrayList,因為ArrayList將批量copy大量的元素;越往后,對于LinkedList來說,因為它是雙向鏈表,所以在第2個元素后面插入一個數據和在倒數第2個元素后面插入一個元素在效率上基本沒有差別,但是ArrayList由于要批量copy的元素越來越少,操作速度必然追上乃至超過LinkedList。
從這個分析看出,如果你十分確定你插入、刪除的元素是在前半段,那么就使用LinkedList;如果你十分確定你刪除、刪除的元素在比較靠后的位置,那么可以考慮使用ArrayList。如果你不能確定你要做的插入、刪除是在哪兒呢?那還是建議你使用LinkedList吧,因為一來LinkedList整體插入、刪除的執行效率比較穩定,沒有ArrayList這種越往后越快的情況;二來插入元素的時候,弄得不好ArrayList就要進行一次擴容,記住,ArrayList底層數組擴容是一個既消耗時間又消耗空間的操作。
站在巨人的肩膀上摘蘋果:
Java集合干貨系列-(一)ArrayList源碼解析
Java集合干貨系列-(二)LinkedList源碼解析
《大話數據結構》
《數據結構與算法分析(java語言描述)》
《瘋狂java講義》
轉載于:https://my.oschina.net/oosc/blog/1614939
總結
以上是生活随笔為你收集整理的java温故笔记(二)java的数组HashMap、ConcurrentHashMap、ArrayList、LinkedList的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 做梦梦到输液流血是什么征兆
- 下一篇: 三角函数式的化简