1、Java異常以及常用工具類體系。異常處理機制主要回答了三個問題?
答:1)、第一個是異常類型回答了什麼被拋出。
2)、第二個是異常堆棧跟蹤回答了在哪裏拋出。
3)、第三個是異常信息回答了爲什麼被拋出。Throwable是所有異常體系的頂級父類,包含了Error類和Exception類。從概念角度分析Java的異常處理機制。
2、Java的異常體系,Error和Exception的區別?
答:1)、Error,程序無法處理的系統錯誤,編譯器不做檢查。表示系統致命的錯誤,程序無法處理這些錯誤,Error類一般是指與JVM相關的問題,如果系統奔潰、虛擬機錯誤、內存空間不足、方法調用棧溢出等等錯誤。
2)、Exception,程序可以處理的異常,捕獲後可能恢復。遇到此類異常,儘可能去處理,使程序恢復運行,而不應該隨意中止異常。
3)、總結,Error是程序無法處理的錯誤,Exception是可以處理的異常。
3、Exception主要包含兩類,一類是RuntimeException、另一類是非RuntimeException。
答:1)、RuntimeException(運行時異常)異常表示不可預知的,程序應當自行避免,例如數組下標越界,訪問空指針異常等等。
2)、非RuntimeException(非運行時異常)異常是可以預知的,從編譯器校驗的異常。從編譯器角度來說是必須處理的異常,如果不處理此類異常,編譯不能夠通過的。
4、Java的異常體系,從責任角度來看。
答:Error屬於JVM需要負擔的責任。RuntimeException是程序應該負擔的責任。Checked Exception可檢查異常是Java編譯器應該負擔的責任。
5、常見Error以及Exception。RuntimeException運行時異常。
答:1)、第一種,NullPointerException空指針引用異常。
2)、第二種,ClassCastException類型強轉轉換異常。
3)、第三種,IllegalArgumentException傳遞非法參數異常。
4)、第四種,IndexOutOfBoundsException下標越界異常。
5)、第五種,NumberFormatException數字格式異常。
6、非RuntimeException非運行時異常。
答:1)、第一種,ClassNotFoundException,找不到指定的class的異常。
2)、第二種,IOException,IO操作異常。
7、Error錯誤異常。
答:1)、第一種,NoClassDefFoundError,找不到class定義的異常。造成的原因包含,類依賴的class或者jar包不存在。類文件存在,但是存在不同的域中。大小寫問題,javac編譯的時候無視大小寫的,很有可能編譯出來的class文件就與想要的不一樣。
2)、第二種,StackOverflowError,深遞歸導致棧被耗盡而拋出的異常。
3)、第三種,OutOfMemoryError內存溢出異常。
8、Java的異常處理機制,Exception的處理機制。
答:1)、第一步、拋出異常,創建異常對象,交由運行時系統處理。當一個方法出現錯誤引發異常的時候,方法創建異常對象,並交付給運行時系統,系統對象中包含了異常類型,異常出現時的程序狀態等異常信息,運行時系統負責尋找處置異常的代碼並執行。
2)、第二步、捕獲異常,尋找合適的異常處理器處理異常,否則終止運行。方法拋出異常以後,運行時系統將轉爲尋找合適的異常處理器,即ExceptionHandle。潛在的異常處理是異常發生時依次存留在調用棧方法的集合,當異常處理器所能處理的異常類型與拋出的異常類型相符的時候,即爲合適的異常處理器,運行時系統從發生異常的方法開始依次回查調用棧中的方法直至找到含有異常處理器的方法並執行。當運行時系統遍歷了調用棧都沒有找到合適的異常處理器,則運行時系統終止,java程序終止。
9、Java異常的處理規則。
答:具體明確,拋出的異常應能通過異常類名和message準確說明異常的類型和產生異常的原因。
提早拋出,應儘可能早的發現並拋出異常,便於精準定位問題。
延遲捕獲,異常的捕獲和處理應該儘可能延遲,讓掌握更多信息的作用域來處理異常。
10、try-catch的性能問題。Java異常處理消耗性能的地方。
答:第一點、try-catch塊影響JVM的優化。
第二點、異常對象實例需要保存棧快照等等信息,開銷較大,這是一個相對較重的操作。所以一定要捕獲可能出現異常的代碼,不要使用一個大大的try-ccatch包起來整段代碼,不要使用異常控制代碼的流程,因爲此效率遠遠沒有if-else判斷的效率高。
11、集合之List和Set的區別,如下所示。
12、Map集合,Map集合用於保存具有映射關係的數據,Map保存的數據都是key-value對的形式的,也就是key-value組成的鍵值對形式的,Map裏面的key是不可以重複的,key是用於標示集合裏面的每項數據的,Map裏面的value則是可以重複的。
13、Hashtable、HashMap、ConcurrentHashMap的區別,如下所示:
答:1)、HashMap,存儲特點是鍵值對映射,在Java8以前,是數組+鏈表的組成,HashMap結合了數組和鏈表的優勢進行編寫的。數組的特點是查詢快,增刪慢,而鏈表的特點是查詢慢,增刪快。HashMap是非Synchronized,所以是線程不安全的,但是效率高。HashMap是由數組和鏈表組成的,HashMap的數組長度在未賦初始值的時候,默認長度是16的,一個長度爲16的數組中,每個元素存儲的就是鏈表的頭節點,通過類似於hash(key.hashCode) % len,哈希函數取模的操作獲得要添加的元素所要存放的數組的位置,實際上,HashMap的哈希算法是通過位運算來進行的,相對於取模運算呢,效率更高。這裏面有一個極端的情況,如果添加到哈希表裏面的不同的值的鍵位來通過哈希散列運算,總是得出相同的值即分配到同一個桶中,這樣會是某個桶中鏈表的長度變得很長,由於鏈表查詢需要從頭部開始遍歷,因此,在最壞的情況下呢,HashMap性能惡化,從O(1)變成了O(n)。
HashMap,存儲特點是鍵值對映射,在Java8以後,HashMap採用了數組 + 鏈表 + 紅黑樹的組成。Java8以後使用常量TREEIFY_THRESHOLD來控制是否將鏈表轉換爲紅黑樹,來存儲數據,這意味着,即使在最壞的情況下,HashMap的性能從O(n)提高到O(logn)。
2)、Hashtable是線程安全的,是因爲在方法都加了synchronized關鍵字,和Collections.synchronizedMap(map)效果一樣,都是串行執行的,效率比較低,唯一的區別就是鎖定的對象不同而已。爲了提升多線程下的執行性能,引入了ConcurrentHashMap。
3)、ConcurrentHashMap,無論是Hashtable還是使用synchronizedMap包裝了的hashMap,當多線程併發的情況下,都要競爭同一把鎖,導致效率極其低下,而在jdk1.5以後,爲了改進HashTable的缺點,引入了ConcurrentHashMap。
4)、如何優化Hashtable呢?如何設計ConcurrentHashMap呢?
a)、通過鎖細粒度化,將整鎖拆解成多個鎖進行優化。對象鎖之間是不相互制約的,因此,我們可以將原本一個鎖的行爲拆分多個鎖,早期的ConcurrentHashMap也是這樣做的,ConcurrentHashMap早期使用的是分段鎖技術,通過分段鎖Segment來實現,將鎖一段一段的進行存儲,然後給每一段數據配一把鎖即Segment,當一個線程佔用一把鎖即Segment的時候,然後訪問其中一段數據的時候呢,位於其他Segment的數據也能被其他線程同時訪問,默認是分配16個Segment,理論上比Hashtable效率提升了16倍,相比於早期的HashMap,就是將hashMap的table數組邏輯上拆分成多個子數組,每個子數組配置一把鎖,線程在獲取到某把分段鎖的時候,比如,獲取到編號爲8的Segment之後呢,才能操作這個子數組,而其他線程想要操作該子數組的時候,只能被阻塞,但是如果其他線程操作的是其他未被佔用的Segment所管轄的子數組,那麼是不會被阻塞的。此時呢,可以將分段鎖拆分的更細,或者不使用分段鎖,而是table裏面的每個bucket都用一把不同的鎖進行管理,ConcurrentHashMap的效率就得到了更好的提高。
b)、jdk1.8以後,當前的ConcurrentHashMap,使用的CAS + synchronized使鎖更加細化,保證併發安全。同時,也做了進一步的優化,使用了數組 + 鏈表 + 紅黑樹的組合。synchronized只鎖定當前鏈表或者紅黑樹的首節點,這樣,只要哈希不衝突,就不會產生併發,效率得到了進一步的提高,ConcurrentHashMap的結構參考了jdk1.8以後的hashMap來設計的。
5)、HashMap線程不安全的,底層是通過數組 + 鏈表 + 紅黑樹。鍵值對key-value均可以爲null,但是hashtable,ConcurrentHashMap兩個類都不支持。
Hashtable是線程安全的,鎖住整個對象,底層是數組 + 鏈表。實現線程安全的方式,是在修改數組的時候鎖住整個hashtable,效率很低下的。
ConcurrentHashMap是線程安全的,底層是CAS + 同步鎖,數組 + 鏈表 + 紅黑樹。則是對hashtable進行了優化,通過將鎖細粒度化到table的每個元素來提升併發性能。
14、HashMap中的put方法的邏輯,如下所示:
1)、如果HashMap未被初始化過,則進行初始化操作。
2)、對Key求Hash值,然後再計算table數組的下標。
3)、如果沒有碰撞,table數組裏面對應的位置還沒有鍵值對,則將鍵值對直接放入對應的table數組位置(桶)中。
4)、如果碰撞了,table數組這個位置有元素了,以鏈表的方式鏈接到後面。
5)、如果鏈表長度超過閾值,就把鏈表轉成紅黑樹。
6)、如果鏈表長度低於6,就把紅黑樹轉回鏈表。
7)、如果節點已經存在就鍵位對應的舊值進行替換。所謂的節點存在也就是,即key值已經存在在了HashMap中了,我們找到這個key值就key對應的新值替換掉它對應的舊值。
8)、如果桶滿了(容量16*加載因子0.75),需要擴容了,就需要resize(擴容2倍後重排)。
1 /** 2 * The default initial capacity - MUST be a power of two. 3 */ 4 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 5 6 /** 7 * The maximum capacity, used if a higher value is implicitly specified 8 * by either of the constructors with arguments. 9 * MUST be a power of two <= 1<<30. 10 */ 11 // 12 static final int MAXIMUM_CAPACITY = 1 << 30; 13 14 /** 15 * The load factor used when none specified in constructor. 16 */ 17 // 默認的擴容因子DEFAULT_LOAD_FACTOR = 0.75f 18 static final float DEFAULT_LOAD_FACTOR = 0.75f; 19 20 /** 21 * The bin count threshold for using a tree rather than list for a 22 * bin. Bins are converted to trees when adding an element to a 23 * bin with at least this many nodes. The value must be greater 24 * than 2 and should be at least 8 to mesh with assumptions in 25 * tree removal about conversion back to plain bins upon 26 * shrinkage. 27 */ 28 // 如果鏈表的長度超過TREEIFY_THRESHOLD = 8的時候呢,就會被改造成紅黑樹。 29 static final int TREEIFY_THRESHOLD = 8; 30 31 /** 32 * The bin count threshold for untreeifying a (split) bin during a 33 * resize operation. Should be less than TREEIFY_THRESHOLD, and at 34 * most 6 to mesh with shrinkage detection under removal. 35 */ 36 // 如果某個桶上面元素的總數,因爲刪除而低於閾值之後呢,即低於UNTREEIFY_THRESHOLD = 6的時候,紅黑樹又被轉換成了鏈表以保證更高的性能。 37 static final int UNTREEIFY_THRESHOLD = 6; 38 39 /** 40 * The smallest table capacity for which bins may be treeified. 41 * (Otherwise the table is resized if too many nodes in a bin.) 42 * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts 43 * between resizing and treeification thresholds. 44 */ 45 // 46 static final int MIN_TREEIFY_CAPACITY = 64; 47 48 49 50 /** 51 * Constructs an empty <tt>HashMap</tt> with the default initial capacity 52 * (16) and the default load factor (0.75). 53 */ 54 // 無參構造函數,table的數組並沒有在構造函數中進行初始化,而是僅僅給了一些成員變量賦初始值。HashMap是按照LazyLoad的原則,在首次使用的時候纔會被初始化的。 55 public HashMap() { 56 this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted 57 } 58 59 60 61 62 ...... 63 64 65 66 67 68 /** 69 * Basic hash bin node, used for most entries. (See below for 70 * TreeNode subclass, and in LinkedHashMap for its Entry subclass.) 71 */ 72 static class Node<K,V> implements Map.Entry<K,V> { 73 final int hash; // hash值 74 final K key; //鍵值key 75 V value; //鍵值value 76 Node<K,V> next; //指向下一個節點的next 77 78 Node(int hash, K key, V value, Node<K,V> next) { 79 this.hash = hash; 80 this.key = key; 81 this.value = value; 82 this.next = next; 83 } 84 85 public final K getKey() { return key; } 86 public final V getValue() { return value; } 87 public final String toString() { return key + "=" + value; } 88 89 public final int hashCode() { 90 return Objects.hashCode(key) ^ Objects.hashCode(value); 91 } 92 93 public final V setValue(V newValue) { 94 V oldValue = value; 95 value = newValue; 96 return oldValue; 97 } 98 99 public final boolean equals(Object o) { 100 if (o == this) 101 return true; 102 if (o instanceof Map.Entry) { 103 Map.Entry<?,?> e = (Map.Entry<?,?>)o; 104 if (Objects.equals(key, e.getKey()) && 105 Objects.equals(value, e.getValue())) 106 return true; 107 } 108 return false; 109 } 110 } 111 112 113 114 115 116 117 /** 118 * The table, initialized on first use, and resized as 119 * necessary. When allocated, length is always a power of two. 120 * (We also tolerate length zero in some operations to allow 121 * bootstrapping mechanics that are currently not needed.) 122 */ 123 // HashMap的內部結構,HashMap可以看作是通過數組Node<K,V>[] table,和鏈表組合而成的複合結構,數組被分爲一個個的bucket,通過hash值決定了鍵值對在這個數組的尋址,hash值相同的鍵值對則以鏈表的形式來存儲。 124 transient Node<K,V>[] table; 125 126 127 128 129 ...... 130 131 132 133 134 135 /** 136 * Associates the specified value with the specified key in this map. 137 * If the map previously contained a mapping for the key, the old 138 * value is replaced. 139 * 140 * @param key key with which the specified value is to be associated 141 * @param value value to be associated with the specified key 142 * @return the previous value associated with <tt>key</tt>, or 143 * <tt>null</tt> if there was no mapping for <tt>key</tt>. 144 * (A <tt>null</tt> return can also indicate that the map 145 * previously associated <tt>null</tt> with <tt>key</tt>.) 146 */ 147 public V put(K key, V value) { 148 return putVal(hash(key), key, value, false, true); 149 } 150 151 152 /** 153 * Implements Map.put and related methods 154 * 155 * @param hash hash for key 156 * @param key the key 157 * @param value the value to put 158 * @param onlyIfAbsent if true, don't change existing value 159 * @param evict if false, the table is in creation mode. 160 * @return previous value, or null if none 161 */ 162 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, 163 boolean evict) { 164 Node<K,V>[] tab; Node<K,V> p; int n, i; 165 // 如果table爲null的時候,或者table的長度爲0的時候 166 if ((tab = table) == null || (n = tab.length) == 0) 167 // 調用resize()方法初始化table,resize()方法的作用是進行初始化和擴容的功能。 168 n = (tab = resize()).length; 169 // 做hash運算,算出鍵值對在table裏面的具體位置。hash哈希值並不是key本身的hashCode的,而是來自於hash與或產生的結果的。 170 if ((p = tab[i = (n - 1) & hash]) == null) 171 // 如果通過hash運算得到的位置還沒有元素存儲到裏面的時候,則會直接new該鍵值對的Node,放到該數組的位置當中tab[i]。 172 tab[i] = newNode(hash, key, value, null); 173 // 否則就繼續向下走。 174 else { 175 Node<K,V> e; K k; 176 // 如果發現同樣的位置,已經存在鍵值對的時候,且鍵和傳進來的鍵是一致的, 177 if (p.hash == hash && 178 ((k = p.key) == key || (key != null && key.equals(k)))) 179 // 則直接替換數組裏面的元素值 180 e = p; 181 // 否則,如果當前數組位置存儲的是否已經是樹化後的節點 182 else if (p instanceof TreeNode) 183 // 如果是樹化了,就按照樹的方式嘗試存儲鍵值對 184 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 185 else { 186 // 如果未樹化,則按照鏈表的插入方式往鏈表後面添加元素 187 for (int binCount = 0; ; ++binCount) { 188 if ((e = p.next) == null) { 189 p.next = newNode(hash, key, value, null); 190 // 判斷鏈表元素的總數,一旦超過了TREEIFY_THRESHOLD,則將鏈表進行樹化操作。 191 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 192 // 樹化操作哦 193 treeifyBin(tab, hash); 194 break; 195 } 196 // 197 if (e.hash == hash && 198 ((k = e.key) == key || (key != null && key.equals(k)))) 199 break; 200 p = e; 201 } 202 } 203 // 如果插入的鍵位存在於hashMap中的時候,則對對應的鍵位進行值的更新操作。 204 if (e != null) { // existing mapping for key 205 V oldValue = e.value; 206 if (!onlyIfAbsent || oldValue == null) 207 e.value = value; 208 afterNodeAccess(e); 209 return oldValue; 210 } 211 } 212 ++modCount; 213 // 當hashMap裏面的szie大於閾值的時候呢,就對HashMap進行擴容 214 if (++size > threshold) 215 resize(); 216 afterNodeInsertion(evict); 217 return null; 218 }
15、HashMap中的get方法的邏輯,如下所示:
1 /** 2 * Returns the value to which the specified key is mapped, 3 * or {@code null} if this map contains no mapping for the key. 4 * 5 * <p>More formally, if this map contains a mapping from a key 6 * {@code k} to a value {@code v} such that {@code (key==null ? k==null : 7 * key.equals(k))}, then this method returns {@code v}; otherwise 8 * it returns {@code null}. (There can be at most one such mapping.) 9 * 10 * <p>A return value of {@code null} does not <i>necessarily</i> 11 * indicate that the map contains no mapping for the key; it's also 12 * possible that the map explicitly maps the key to {@code null}. 13 * The {@link #containsKey containsKey} operation may be used to 14 * distinguish these two cases. 15 * 16 * @see #put(Object, Object) 17 */ 18 public V get(Object key) { 19 Node<K,V> e; 20 // 通過傳入的key值進行調用getNode()方法 21 return (e = getNode(hash(key), key)) == null ? null : e.value; 22 } 23 24 25 26 27 ...... 28 29 30 31 32 33 /** 34 * Implements Map.get and related methods 35 * 36 * @param hash hash for key 37 * @param key the key 38 * @return the node, or null if none 39 */ 40 final Node<K,V> getNode(int hash, Object key) { 41 Node<K,V>[] tab; Node<K,V> first, e; int n; K k; 42 // 鍵對象的hashcode,通過哈希算法,找到bucket的位置 43 if ((tab = table) != null && (n = tab.length) > 0 && 44 (first = tab[(n - 1) & hash]) != null) { 45 // 找到Bucket的位置以後,調用key.equals(k))方法找到鏈表中正確的節點,最終找到要找的值 46 if (first.hash == hash && // always check first node 47 ((k = first.key) == key || (key != null && key.equals(k)))) 48 return first; 49 if ((e = first.next) != null) { 50 if (first instanceof TreeNode) 51 return ((TreeNode<K,V>)first).getTreeNode(hash, key); 52 do { 53 if (e.hash == hash && 54 ((k = e.key) == key || (key != null && key.equals(k)))) 55 return e; 56 } while ((e = e.next) != null); 57 } 58 } 59 return null; 60 }
16、HashMap,如何有效減少碰撞?
答:樹化這種被動的方式可以提升性能,哈希運算也是可以提升性能的關鍵。
1)、擾動函數,促使元素位置分佈均勻,減少碰撞的機率,原理就是如果兩個不相等的對象返回不同的hashcode的話,或者說元素位置儘量的分佈均勻些,那麼碰撞的機率就會小些,意味着有些元素就可以通過數組來直接去獲取了,這樣可以提升hashMap的性能的。哈希算法的內部實現,是讓不同對象返回不同的hashcode值。
2)、其次,如果使用final對象,並採用合適的equals()和hashCode()方法,將會減少碰撞的發生,不可變性使得能夠緩存不同鍵的hashcode,這將提供獲取對象的速度,而使用String,Integer,這種是非常好的選擇,因爲他們是final,並且重寫了hashcode方法和equals方法的。不可變性final是必要的,因爲爲了要計算hashcode,就要防止鍵值改變,如果鍵值在放入的時候和獲取的時候返回不同的hashcode的話呢,就不能從hashMap中找到想要的對象了。
1 /** 2 * Computes key.hashCode() and spreads (XORs) higher bits of hash 3 * to lower. Because the table uses power-of-two masking, sets of 4 * hashes that vary only in bits above the current mask will 5 * always collide. (Among known examples are sets of Float keys 6 * holding consecutive whole numbers in small tables.) So we 7 * apply a transform that spreads the impact of higher bits 8 * downward. There is a tradeoff between speed, utility, and 9 * quality of bit-spreading. Because many common sets of hashes 10 * are already reasonably distributed (so don't benefit from 11 * spreading), and because we use trees to handle large sets of 12 * collisions in bins, we just XOR some shifted bits in the 13 * cheapest possible way to reduce systematic lossage, as well as 14 * to incorporate impact of the highest bits that would otherwise 15 * never be used in index calculations because of table bounds. 16 */ 17 static final int hash(Object key) { 18 int h; 19 // 即先獲取key.hashCode(),hashCode方法返回值是int類型的,是32位的,然後再將高位數移位到低位,移動16位,最後進行異或運算。 20 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); 21 }
HashMap,從獲取hash到散列的過程。
1)、不使用hashcode()方法獲取的值,是因爲key.hashCode();方法返回的是int類型的散列值,如果直接使用這個散列值作爲下標去訪問hashMap數組的話呢,考慮到二進制的32位帶符號的int值的範圍呢-2147483648——2147483647,前後區間大概有40億的映射空間,只要哈希函數映射的均勻鬆散,一般應用是很難出現碰撞的,但是40億長度的數組在內存中是放不下的,況且,HashMap在擴容之前數組默認大小纔是16,所以直接拿這個散列值使用不現實的。
2)、h >>> 16,右移16位,再和自己做異或操作。這樣做,就是爲了混合原始哈希碼的高位與低位,依次來加大低位的隨機性,而且混合後的低位參雜了高位部分的特徵,這樣高位的信息也變相的保存了下來,這樣做主要從速度,質量,功效進行考慮的,可以在數組table的length在比較小的時候,也能保證考慮到高低bit都參與到哈希的運算中,同時也不會有太大的開銷。
17、hashMap含參的構造器,可以傳入初始化的hashMap的初始化大小的,根據傳入的初始化值,換算成2的n次方,轉換成最接近的2的倍數的值,這樣做,就是爲了通過哈希運算定位桶的時候呢,能實現用與操作來代替取模進而獲得更好的效果。
1 /** 2 * Constructs an empty <tt>HashMap</tt> with the specified initial 3 * capacity and the default load factor (0.75). 4 * 5 * @param initialCapacity the initial capacity. 6 * @throws IllegalArgumentException if the initial capacity is negative. 7 */ 8 // hashMap含參的構造器,可以傳入初始化的hashMap的初始化大小的 9 public HashMap(int initialCapacity) { 10 this(initialCapacity, DEFAULT_LOAD_FACTOR); 11 } 12 13 14 15 /** 16 * Constructs an empty <tt>HashMap</tt> with the specified initial 17 * capacity and load factor. 18 * 19 * @param initialCapacity the initial capacity 20 * @param loadFactor the load factor 21 * @throws IllegalArgumentException if the initial capacity is negative 22 * or the load factor is nonpositive 23 */ 24 // hashMap含參的構造器,調用該構造器。 25 public HashMap(int initialCapacity, float loadFactor) { 26 if (initialCapacity < 0) 27 throw new IllegalArgumentException("Illegal initial capacity: " + 28 initialCapacity); 29 if (initialCapacity > MAXIMUM_CAPACITY) 30 initialCapacity = MAXIMUM_CAPACITY; 31 if (loadFactor <= 0 || Float.isNaN(loadFactor)) 32 throw new IllegalArgumentException("Illegal load factor: " + 33 loadFactor); 34 this.loadFactor = loadFactor; 35 // 根據傳入的hashMap的初始化值,並不是傳入的初始化值多大,就是多大的 36 this.threshold = tableSizeFor(initialCapacity); 37 } 38 39 40 41 42 ....... 43 44 45 46 47 /** 48 * Returns a power of two size for the given target capacity. 49 */ 50 // 根據傳入的初始化值,換算成2的n次方,轉換成最接近的2的倍數的值,這樣做,就是爲了通過哈希運算定位桶的時候呢,能實現用與操作來代替取模進而獲得更好的效果。 51 static final int tableSizeFor(int cap) { 52 int n = cap - 1; 53 n |= n >>> 1; 54 n |= n >>> 2; 55 n |= n >>> 4; 56 n |= n >>> 8; 57 n |= n >>> 16; 58 return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; 59 }
18 、hashMap的擴容resize?
hashMap的擴容,就是重新計算容量,向hashMap對象中不停的添加元素,而hashMap對象內部的數組無法裝載更多的元素的時候,對象就需要擴大數組的長度了,才能裝入更多的元素。java中的數組是無法進行自動擴容的,hashMap的擴容,是使用新的大的數組替換小的數組。
hashMap的默認負載因子是0.75f,當hashMap填滿了75%的bucket的時候呢,就會創建原來hashMap大小2倍的bucket數組,來重新調整map的大小,並將原來的對象放入的新的bucket數組中。
HashMap擴容的問題,多線程環境下,調整大小會存在條件競爭,容易造成死鎖。rehashing是一個比較耗時的過程,由於需要將原先的hashMap中的鍵值對重新移動的新的hashMap中去,是一個比較耗時的過程。
1 /** 2 * The load factor used when none specified in constructor. 3 */ 4 // 默認的負載因子 5 static final float DEFAULT_LOAD_FACTOR = 0.75f; 6 7 8 9 10 ...... 11 12 13 14 15 16 /** 17 * Initializes or doubles table size. If null, allocates in 18 * accord with initial capacity target held in field threshold. 19 * Otherwise, because we are using power-of-two expansion, the 20 * elements from each bin must either stay at same index, or move 21 * with a power of two offset in the new table. 22 * 23 * @return the table 24 */ 25 // hashMap的擴容resize,就是重新計算容量,向hashMap對象中不停的添加元素,而hashMap對象內部的數組無法裝載更多的元素的時候,對象就需要擴大數組的長度了,才能裝入更多的元素。java中的數組是無法進行自動擴容的,hashMap的擴容,是使用新的大的數組替換小的數組。 26 27 // hashMap的默認負載因子是0.75f,當hashMap填滿了75%的bucket的時候呢,就會創建原來hashMap大小2倍的bucket數組,來重新調整map的大小,並將原來的對象放入的新的bucket數組中。 28 final Node<K,V>[] resize() { 29 Node<K,V>[] oldTab = table; 30 int oldCap = (oldTab == null) ? 0 : oldTab.length; 31 int oldThr = threshold; 32 int newCap, newThr = 0; 33 if (oldCap > 0) { 34 if (oldCap >= MAXIMUM_CAPACITY) { 35 threshold = Integer.MAX_VALUE; 36 return oldTab; 37 } 38 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && 39 oldCap >= DEFAULT_INITIAL_CAPACITY) 40 newThr = oldThr << 1; // double threshold 41 } 42 else if (oldThr > 0) // initial capacity was placed in threshold 43 newCap = oldThr; 44 else { // zero initial threshold signifies using defaults 45 newCap = DEFAULT_INITIAL_CAPACITY; 46 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); 47 } 48 if (newThr == 0) { 49 float ft = (float)newCap * loadFactor; 50 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? 51 (int)ft : Integer.MAX_VALUE); 52 } 53 threshold = newThr; 54 @SuppressWarnings({"rawtypes","unchecked"}) 55 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; 56 table = newTab; 57 if (oldTab != null) { 58 for (int j = 0; j < oldCap; ++j) { 59 Node<K,V> e; 60 if ((e = oldTab[j]) != null) { 61 oldTab[j] = null; 62 if (e.next == null) 63 newTab[e.hash & (newCap - 1)] = e; 64 else if (e instanceof TreeNode) 65 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); 66 else { // preserve order 67 Node<K,V> loHead = null, loTail = null; 68 Node<K,V> hiHead = null, hiTail = null; 69 Node<K,V> next; 70 do { 71 next = e.next; 72 if ((e.hash & oldCap) == 0) { 73 if (loTail == null) 74 loHead = e; 75 else 76 loTail.next = e; 77 loTail = e; 78 } 79 else { 80 if (hiTail == null) 81 hiHead = e; 82 else 83 hiTail.next = e; 84 hiTail = e; 85 } 86 } while ((e = next) != null); 87 if (loTail != null) { 88 loTail.next = null; 89 newTab[j] = loHead; 90 } 91 if (hiTail != null) { 92 hiTail.next = null; 93 newTab[j + oldCap] = hiHead; 94 } 95 } 96 } 97 } 98 } 99 return newTab; 100 }
19、 ConcurrentHashMap是出自於JUC包的,ConcurrentHashMap有很多地方和hashMap類似的,包含屬性參數之類的。ConcurrentHashMap使用的CAS + synchronized進行高效的同步更新數據的。
ConcurrentHashMap總結,jdk1.8的實現,也是鎖分離的思想,比起Segment,鎖拆的更細,只要哈希不衝突,就不會出現併發或者鎖的情況。
1)、首先使用無鎖操作CAS插入頭節點,失敗則循環重試,如果插入失敗,則說明有別的線程插入頭節點了,需要再次循環進行操作。
2)、若頭節點已經存在,則通過synchronized嘗試獲取頭節點的同步鎖,再進行操作。性能比Segment分段鎖又提高了很多。
20、ConcurrentHashMap的put方法的邏輯。
1)、判斷Node[]數組是否初始化,沒有則進行初始化操作。
2)、通過hash定位數組的索引座標,是否有Node節點,如果沒有則使用CAS進行添加(鏈表的頭節點),添加失敗則進入下次循環,繼續嘗試添加。
3)、檢查到內部正在擴容,如果正在擴容,就調用helpTransfer方法,就幫助它一塊擴容。
4)、如果f!=null,頭節點不爲空,則使用synchronized鎖住f元素(鏈表/紅黑二叉樹的頭元素)。如果是Node鏈表結構,則執行鏈表的添加操作。如果是TreeNode(樹形結構)則執行樹添加操作。
5)、判斷鏈表長度已經到達臨界值8,當然這個8是默認值,大家可以去做調整,當節點數超過這個值就需要把鏈表轉換成樹結構了。
1 private static final int MAXIMUM_CAPACITY = 1 << 30; 2 3 /** 4 * The default initial table capacity. Must be a power of 2 5 * (i.e., at least 1) and at most MAXIMUM_CAPACITY. 6 */ 7 private static final int DEFAULT_CAPACITY = 16; 8 9 /** 10 * The largest possible (non-power of two) array size. 11 * Needed by toArray and related methods. 12 */ 13 static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; 14 15 /** 16 * The default concurrency level for this table. Unused but 17 * defined for compatibility with previous versions of this class. 18 */ 19 private static final int DEFAULT_CONCURRENCY_LEVEL = 16; 20 21 /** 22 * The load factor for this table. Overrides of this value in 23 * constructors affect only the initial table capacity. The 24 * actual floating point value isn't normally used -- it is 25 * simpler to use expressions such as {@code n - (n >>> 2)} for 26 * the associated resizing threshold. 27 */ 28 private static final float LOAD_FACTOR = 0.75f; 29 30 /** 31 * The bin count threshold for using a tree rather than list for a 32 * bin. Bins are converted to trees when adding an element to a 33 * bin with at least this many nodes. The value must be greater 34 * than 2, and should be at least 8 to mesh with assumptions in 35 * tree removal about conversion back to plain bins upon 36 * shrinkage. 37 */ 38 static final int TREEIFY_THRESHOLD = 8; 39 40 /** 41 * The bin count threshold for untreeifying a (split) bin during a 42 * resize operation. Should be less than TREEIFY_THRESHOLD, and at 43 * most 6 to mesh with shrinkage detection under removal. 44 */ 45 static final int UNTREEIFY_THRESHOLD = 6; 46 47 /** 48 * The smallest table capacity for which bins may be treeified. 49 * (Otherwise the table is resized if too many nodes in a bin.) 50 * The value should be at least 4 * TREEIFY_THRESHOLD to avoid 51 * conflicts between resizing and treeification thresholds. 52 */ 53 static final int MIN_TREEIFY_CAPACITY = 64; 54 55 /** 56 * Minimum number of rebinnings per transfer step. Ranges are 57 * subdivided to allow multiple resizer threads. This value 58 * serves as a lower bound to avoid resizers encountering 59 * excessive memory contention. The value should be at least 60 * DEFAULT_CAPACITY. 61 */ 62 private static final int MIN_TRANSFER_STRIDE = 16; 63 64 /** 65 * The number of bits used for generation stamp in sizeCtl. 66 * Must be at least 6 for 32bit arrays. 67 */ 68 private static int RESIZE_STAMP_BITS = 16; 69 70 /** 71 * The maximum number of threads that can help resize. 72 * Must fit in 32 - RESIZE_STAMP_BITS bits. 73 */ 74 private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1; 75 76 /** 77 * The bit shift for recording size stamp in sizeCtl. 78 */ 79 private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS; 80 81 82 83 84 85 86 ...... 87 88 89 90 91 /* 92 * Encodings for Node hash fields. See above for explanation. 93 */ 94 // 其它成員變量主要用來控制線程之間的併發操作,比如可以同時可以進行擴容的線程數等等。 95 static final int MOVED = -1; // hash for forwarding nodes 96 static final int TREEBIN = -2; // hash for roots of trees 97 static final int RESERVED = -3; // hash for transient reservations 98 static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash 99 100 101 102 ....... 103 104 105 106 107 /** 108 * Table initialization and resizing control. When negative, the 109 * table is being initialized or resized: -1 for initialization, 110 * else -(1 + the number of active resizing threads). Otherwise, 111 * when table is null, holds the initial table size to use upon 112 * creation, or 0 for default. After initialization, holds the 113 * next element count value upon which to resize the table. 114 */ 115 // sizeCtl是size control,是做大小控制的標識符,是哈希表初始化和擴容時候的一個控制位標識量,負數代表正在進行初始化或者擴容操作,-1代表正在初始化,-n代表有n-1個線程正在擴容操作,正數或者0代表哈希表還沒有被初始化操作。這個數值表示初始化或者下一次進行擴容的大小,因爲有了volatile修飾符, sizeCtl是多線程之間可見的,對它的改動,其他線程可以立即看得到,確實可以起到控制的作用的。 116 private transient volatile int sizeCtl; 117 118 119 120 121 122 ....... 123 124 125 126 127 128 129 /** 130 * Maps the specified key to the specified value in this table. 131 * Neither the key nor the value can be null. 132 * 133 * <p>The value can be retrieved by calling the {@code get} method 134 * with a key that is equal to the original key. 135 * 136 * @param key key with which the specified value is to be associated 137 * @param value value to be associated with the specified key 138 * @return the previous value associated with {@code key}, or 139 * {@code null} if there was no mapping for {@code key} 140 * @throws NullPointerException if the specified key or value is null 141 */ 142 // ConcurrentHashMap的put方法。 143 public V put(K key, V value) { 144 return putVal(key, value, false); 145 } 146 147 /** Implementation for put and putIfAbsent */ 148 final V putVal(K key, V value, boolean onlyIfAbsent) { 149 // ConcurrentHashMap不允許插入null的鍵值對,即key不能爲null或者value不能爲null 150 if (key == null || value == null) throw new NullPointerException(); 151 // 計算key的哈希值 152 int hash = spread(key.hashCode()); 153 int binCount = 0; 154 // for循環,因爲我們對數組元素的更新是使用CAS的機制進行更新的,需要不斷的做失敗重試,直到成功爲止,因此這裏使用了for循環。 155 for (Node<K,V>[] tab = table;;) { 156 Node<K,V> f; int n, i, fh; 157 // 先判斷數組是否爲空,如果爲空或者length等於0 158 if (tab == null || (n = tab.length) == 0) 159 // 就進行初始化操作 160 tab = initTable(); 161 // 如果不爲空,且不等於0,就使用哈希值來找到f,f表示的是鏈表或者紅黑二叉樹的頭節點,即我們數組裏面的元素,根據哈希值定位到的元素來檢查元素是否存在 162 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { 163 // 如果不存在,嘗試使用CAS進行添加,如果添加失敗,則break掉,進入下一次循環 164 if (casTabAt(tab, i, null, 165 new Node<K,V>(hash, key, value, null))) 166 break; // no lock when adding to empty bin 167 } 168 // 如果我們發現原先的元素已經存在了,此時,由於我們的ConcurrentHashMap是隨時存在於多線程環境下的,有可能別的線程正在移動它,也就是說,ConcurrentHashMap的內部呢,正在移動元素,那麼我們就協助其擴容。 169 else if ((fh = f.hash) == MOVED) 170 tab = helpTransfer(tab, f); 171 else { 172 // 這裏表示發生了哈希碰撞 173 V oldVal = null; 174 // 此時,鎖住鏈表或者紅黑二叉樹的頭節點,即我們的數組元素 175 synchronized (f) { 176 // 判斷,f是否的鏈表的頭節點 177 if (tabAt(tab, i) == f) { 178 // fh代表的是頭節點的哈希值 179 if (fh >= 0) { 180 // 如果是鏈表的頭節點,就初始化鏈表的計數器 181 binCount = 1; 182 // 遍歷該鏈表,每遍歷一次,就將計數器加一 183 for (Node<K,V> e = f;; ++binCount) { 184 K ek; 185 // 此時,發現,如果節點存在呢,就去更新對應的value值 186 if (e.hash == hash && 187 ((ek = e.key) == key || 188 (ek != null && key.equals(ek)))) { 189 oldVal = e.val; 190 if (!onlyIfAbsent) 191 e.val = value; 192 break; 193 } 194 Node<K,V> pred = e; 195 // 如果不存在,就在鏈表尾部,添加新的節點 196 if ((e = e.next) == null) { 197 pred.next = new Node<K,V>(hash, key, 198 value, null); 199 break; 200 } 201 } 202 } 203 // 如果頭節點是紅黑二叉樹的節點 204 else if (f instanceof TreeBin) { 205 Node<K,V> p; 206 binCount = 2; 207 // 則嘗試調用紅黑二叉樹的操作邏輯,去嘗試往樹裏面添加節點 208 if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, 209 value)) != null) { 210 oldVal = p.val; 211 if (!onlyIfAbsent) 212 p.val = value; 213 } 214 } 215 } 216 } 217 // 如果鏈表長度已經已經達到了臨界值8 218 if (binCount != 0) { 219 if (binCount >= TREEIFY_THRESHOLD) 220 // 那麼,就將鏈表轉化爲樹結構 221 treeifyBin(tab, i); 222 if (oldVal != null) 223 return oldVal; 224 break; 225 } 226 } 227 } 228 // 在添加完節點之後呢,就將當前的ConcurrentHashMap的size數量呢,加上1。 229 addCount(1L, binCount); 230 return null; 231 }
待續.......