Java基礎知識總結(異常機制、集合、JUC、IO)

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的區別,如下所示。

11.1、備註:線程安全和線程不安全的集合:

Vector、HashTable、Properties是線程安全的。
ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等都是線程不安全的集合類型。

1)、注意:爲了保證集合線程是安全的,效率就比較低;線程不安全的集合效率相對會高一些。

2)、如果需要保證集合既是安全的而且效率高,可以使用Collections爲我們提出瞭解決方案,把這些集合包裝成線程安全的集合。

3)、Collections的工具類,將自己創建的集合類實例傳入進去,便可以包裝成一個線程安全的集合類實例。因爲SynchronizedMap有一個Object mutex互斥對象成員,對裏面的公共方法使用synchronized對mutex進行加鎖操作。相比SynchronizedMap使用的synchronized對mutex進行加鎖操作,hashtable線程安全的原因,是在公有的方法都加入了synchronized修飾符,此時獲取的是方法調用者的鎖。SynchronizedMap和hashtable的原理幾乎相同,唯一的區別就是鎖定的對象不同,因此這兩者在多線程環境下,由於都是串行執行的,效率比較低下,此時可以學習ConcurrentHashMap。

4)、無論是Hashtable還是Collections的工具類SynchronizedMap,當多線程併發的情況下,都要競爭同一把鎖,導致效率極其低下,而在jdk1.5之後,爲了改進hashTable的痛點,ConcurrentHashMap應運而生,

 

11.2、Tree的核心在於排序,保證元素排序。

答:1)、自然排序,讓對象所屬的類去實現comparable接口,無參構造,基於元素對象自身實現的comparable接口的自然排序。
  2)、比較器接口comparator,帶參構造,更爲靈活,不與單元綁定的comparator接口客戶化排序。

自然排序代碼實現,如下所示:

  1 package com.thread;
  2 
  3 import java.util.Set;
  4 import java.util.TreeSet;
  5 
  6 /**
  7  * 自然排序,實現了Comparable自然排序的接口,實現了該接口就要實現equals和hashcode方法和compareTo方法。
  8  * <p>
  9  * <p>
 10  * <p>
 11  * 爲了使Customer類在添加了treeSet之後可以正確排序,要求Customer類裏面的equals方法和compareTo方法按照相同的規則
 12  * 來比較兩個對象是否相等。
 13  */
 14 public class Customer implements Comparable {
 15 
 16     private String name;
 17     private int age;
 18 
 19     public Customer(String name, int age) {
 20         this.name = name;
 21         this.age = age;
 22     }
 23 
 24     public String getName() {
 25         return name;
 26     }
 27 
 28     public void setName(String name) {
 29         this.name = name;
 30     }
 31 
 32     public int getAge() {
 33         return age;
 34     }
 35 
 36     public void setAge(int age) {
 37         this.age = age;
 38     }
 39 
 40     @Override
 41     public boolean equals(Object obj) {
 42         // 兩個對象相等返回true
 43         if (this == obj) {
 44             return true;
 45         }
 46         if (!(obj instanceof Customer)) {
 47             return false;
 48         }
 49         final Customer other = (Customer) obj;
 50 
 51         if (this.name.equals(other.name) && this.age == other.getAge()) {
 52             return true;
 53         } else {
 54             return false;
 55         }
 56     }
 57 
 58     @Override
 59     public int hashCode() {
 60         // 重新了equals方法,就要重新hashcode方法
 61         int result;
 62         result = (name == null) ? 0 : name.hashCode();
 63         result = 29 * result + age;
 64         // 如果兩個對象是相等的,那麼hashcode返回值必須是相等的
 65         return result;
 66     }
 67 
 68     @Override
 69     public int compareTo(Object o) {
 70         // compareTo返回值大於0,說明Customer1大於Customer2,反之Customer1小於Customer2
 71         // 如果等於0,Customer1等於Customer2
 72         Customer other = (Customer) o;
 73         // 先按照name屬性排序
 74         if (this.name.compareTo(other.getName()) > 0) {
 75             return 1;
 76         }
 77         if (this.name.compareTo(other.getName()) < 0) {
 78             return -1;
 79         }
 80 
 81         // 再按照age屬性排序
 82         if (this.age > other.getAge()) {
 83             return 1;
 84         }
 85         if (this.age < other.getAge()) {
 86             return -1;
 87         }
 88         return 0;
 89     }
 90 
 91     public static void main(String[] args) {
 92         Set<Customer> set = new TreeSet<>();
 93         Customer customer1 = new Customer("tom", 16);
 94         Customer customer2 = new Customer("tom", 19);
 95         Customer customer3 = new Customer("tom", 20);
 96         set.add(customer1);
 97         set.add(customer2);
 98         set.add(customer3);
 99 
100         // 循環遍歷
101         for (Customer c : set) {
102             System.out.println(c.getName() + " , " + c.getAge());
103         }
104     }
105 }

客戶化排序,實現Comparator,然後實現compare方法:

 1 package com.thread;
 2 
 3 import java.util.*;
 4 
 5 /**
 6  * 客戶化排序,實現Comparator,然後實現compare方法
 7  */
 8 public class CustomerComparator implements Comparator<Customer> {
 9 
10     @Override
11     public int compare(Customer c1, Customer c2) {
12         // 對姓名進行排序
13         if (c1.getName().compareTo(c2.getName()) > 0) {
14             return -1;
15         }
16         if (c1.getName().compareTo(c2.getName()) < 0) {
17             return 1;
18         }
19 
20         // 對年齡進行排序
21         if (c1.getAge() - c2.getAge() > 0) {
22             return -1;
23         }
24         if (c1.getAge() - c2.getAge() < 0) {
25             return 1;
26         }
27         return 0;
28     }
29 
30     public static void main(String[] args) {
31         // 此時既使用了自然排序,也使用了客戶化排序,
32         // 在客戶化排序和自然排序共存的情況下,最終結果以客戶化排序優先。
33         // 可以查看TreeMap源碼的get(Object key) -> getEntry(key)方法。
34         // 可以看到先使用客戶化排序得到的結果。
35         Set<Customer> set = new TreeSet<>(new CustomerComparator());
36         Customer customer1 = new Customer("張三三", 16);
37         Customer customer2 = new Customer("李四四", 19);
38         Customer customer3 = new Customer("王五五", 20);
39         set.add(customer1);
40         set.add(customer2);
41         set.add(customer3);
42         Iterator<Customer> iterator = set.iterator();
43         while (iterator.hasNext()) {
44             Customer next = iterator.next();
45             System.out.println(next.getName() + " " + next.getAge());
46         }
47     }
48 
49 }

 

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)。

    HashMap的成員變量,Node<K,V>[] table可以看作是Node<K,V>這個數組和鏈表組成的複合結構,數組被分爲一個個的bucket桶,通過hash值決定了鍵值對在這個數組的尋址,hash值相同的鍵值對則以鏈表的形式來存儲,而鏈表的大小超過TREEIFY_THRESHOLD =8這個值的時候,就會被改造成紅黑樹,而當某個桶上面的元素總數因爲刪除變得低於閾值UNTREEIFY_THRESHOLD =6之後,紅黑樹又被轉換爲鏈表,以保證更高的性能。

  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)、Hashtable、HashMap、ConcurrentHashMap的區別,面試回答:

    a)、HashMap線程不安全的,底層是通過數組 + 鏈表 + 紅黑樹。鍵值對key-value均可以爲null,但是hashtable,ConcurrentHashMap兩個類都不支持。
    b)、Hashtable是線程安全的,鎖住整個對象,底層是數組 + 鏈表。實現線程安全的方式,是在修改數組的時候鎖住整個hashtable,效率很低下的。
    c)、ConcurrentHashMap是線程安全的,CAS + 同步鎖,底層是數組 + 鏈表 + 紅黑樹。則是對hashtable進行了優化,通過將鎖細粒度化到table的每個元素來提升併發性能。

    d)、HashMap的key、value均可以爲null,而其它的兩個類Hashtable、ConcurrentHashMap不支持的。

 

 

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 }

 

16.1、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?

  1)、hashMap的擴容,就是重新計算容量,向hashMap對象中不停的添加元素,而hashMap對象內部的數組無法裝載更多的元素的時候,對象就需要擴大數組的長度了,才能裝入更多的元素。java中的數組是無法進行自動擴容的,hashMap的擴容,是使用新的大的數組替換小的數組。

  2)、hashMap的默認負載因子是0.75f,當hashMap填滿了75%的bucket的時候呢,就會創建原來hashMap大小2倍的bucket數組,來重新調整map的大小,並將原來的對象放入的新的bucket數組中。

  3)、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 }

 

21、java.util.concurrent,提供了併發編程的解決方案,java.util.concurrent簡稱爲JUC,JUC包裏面有兩大核心。

答:1)、CAS是java.util.concurrent.atomic包的基礎。
  2)、AQS是java.util.concurrent.locks包以及一些常用類比如Semophore,ReentrantLock等類的基礎。

 

22、java.util.concurrent簡稱爲JUC,JUC包的分類。

答:1)、線程執行器executor,就是任務的執行和調度的框架,此外,在tools包可以看到和executor相關的Executors類,用來創建ExecutorService、ScheduledExecutorService、ThreadFactory、Callable對象等等。
  2)、鎖locks。在jdk1.5之前協調共享對象的訪問時,可以使用的機制只有synchronized和volatile,jdk1.5之後便出現了鎖locks,locks裏面引入了顯式鎖,方便線程間的共享資源做更細粒度的鎖控制,Condition對象是由locks鎖對象創建的,一個Lock對象可創建多個Condition對象,主要用於將線程的等待和喚醒,即將wait、notify、notifyAll方法對象化。不管是Condition對象、還是Lock對象都是基於AQS來實現的,而AQS得底層是通過調用LockSupport.unpark和LockSupport.park去實現線程的阻塞和喚醒的。ReentrantReadWriteLock是可重入讀寫鎖,沒有線程進行寫操作的時候,多個線程可同時進行讀操作,當有線程進行寫操作的時候,其它讀寫操作只能等待,即讀讀共存,但是讀寫不能共存,寫寫也不能共存,在讀多於寫的情況下,可重入讀寫鎖能夠提供比排它鎖ReentrantLock更好的併發性和吞吐量。
  3)、原子變量類atomic,中文是原子,指的是一個操作不可中斷的,在多個線程一起執行的時候,一個操作一旦開始就不會被其它線程所幹擾,所以原子類就是具有原子操作特徵的類,atomic包方便程序員在多線程環境下無鎖的進行原子操作。atomic包中一共有12個類,四種原子更新方式,分別是原子更新基本類型,原子更新數組,原子更新引用,原子更新字段。atomic使用的CAS的更新方式,當某個線程在執行atomic方法的時候不會被其它線程打斷,而別的線程j就像自旋鎖一樣,一直等到該方法執行完成纔有JVM從等待隊列中選擇一個線程來執行,在軟件層面上是非阻塞的,是在底層硬件上藉助處理器的原子指令來保證的。在面對多線程的累加操作可以適當運用atomic包裏面的類來解決。
  4)、併發工具類tools。
  5)、併發集合collections。

 

23、java.util.concurrent簡稱爲JUC,JUC包的併發工具類。

答:四個同步器,同步器的作用主要是用於協助線程的同步。

1)、閉鎖CountDownLatch。讓主線程等待一組事件發生後繼續執行,這裏面的事件指的是CountDownLatch裏的countDown()方法。

  值的注意的是其它線程調用完countDown()方法之後還是會繼續執行的,也就是說,countDown()方法調用之後並不代表該子線程已經執行完畢,而是告訴主線程說你可以繼續執行,至少我這邊不託你後腿了,具體還需要看其它線程給不給力了,如圖,引入了CountDownLatch之後了,主線程就進入了等待狀態,此時CountDownLatch裏面有一個cnt變量開始的時候初始化爲一個整數,這裏就是事件的個數,我們的變量初始化爲3,m每當其中一個子線程調用countDown()方法之後,這個計數器便會減一,直到所有的子線程都調用了countDown()方法,cnt變爲零之後,主線程才得以重新恢復到執行的狀態。

2)、柵欄CyclicBarrier,阻塞當前線程,等待其它線程。

a)、等待其它線程,且會阻塞自己當前線程,所有線程必須同時到達柵欄位置後,才能繼續執行。
b)、所有線程到達柵欄處,可以觸發執行另外一個預先設置的線程。

  CyclicBarrier和CountDownLatch一樣,內部也有一個計數器,如圖中的cnt,T1,T2,T3沒調用一次await()方法,計數器就會減一,且在它們調用await方法的時候,如果計數器不爲零,這些線程也會被阻塞,另外TA線程j即當前線程會在所有線程到達柵欄處即計數器爲0的時候纔會跟着T1,T2,T3一起去執行,同樣都是阻塞當前線程來等待其它線程,計數的時候CountDownLatch的其它子線程是可以繼續執行的,而CyclicBarrier的所有線程會被阻塞直到計數器變爲零,這是兩者作用上的區別。

3)、信號量Semaphore,控制某個資源可被同時訪問的線程個數。

  通過acquire()方法獲取一個許可,如果沒有就去等待,而一旦利用資源執行完業務邏輯之後,線程就會調用release方法去釋放出一個許可出來。

4)、交換器Exchanger,兩個線程到達同步點後,相互交換數據。

  Exchanger提供一個同步點,在這個同步點,兩個線程k可以交換彼此的數據,Exchanger會產生一個同步點,一個線程先執行到達同步點,就會被阻塞,直到另外一個線程也進入到同步點爲止,當兩個都到達了同步點之後就開始交換數據,線程中調用Exchanger.Exchange()的地方就是同步點了,Exchanger只能用於連個線程互相    交換數據。

 

24、BlockingQueue,阻塞隊列,提供了可阻塞的入隊和出隊操作。

答:Collections裏面除了ConcurrentHashMap之外,還有BlockingQueue。

  1)、如果隊列滿了,入隊操作將阻塞,直到有空間可用,如果隊列空了,出隊操作將阻塞,直到有元素可用。根據出入隊的規則和底層數據結構的實現,可以劃分出多個BlockingQueue的實現子類。

  2)、主要用於生產者-消費者模式,在多線程場景的時候生產者線程在隊列尾部添加元素,而消費者線程在隊列頭部消費元素,通過這種方式能夠達到將任務的生產和消費進行隔離的目的。

25、BlockingQueue主要有以下的七個隊列實現,它們都是線程安全的。

  1)、ArrayBlockingQueue,一個由數據結構組成的有界阻塞隊列,有邊界的意思容量是有限的,我們必須在其初始化的時候指定它的容量大小,容量大小一旦指定就不可改變了,以先進先出的方式來存儲數據的,最新插入的對象是在尾部,最先移除的對象是在頭部。
  2)、LinkedBlockingQueue,一個由鏈表結構組成的有界或者無界阻塞隊列,阻塞隊列大小配置是可選的,如果初始化的時候指定大小,那麼它就是有邊界的,如果不指定大小,它就是無邊界的,說是無邊界,其實是採用了默認大小的容量,內部實現是一個鏈表,和ArrayBlockingQueue一樣是採用先進先出的方式存儲數據。
  3)、PriorityBlockingQueue,一個支持優先級排序的無界阻塞隊列,不是先進先出的隊列,元素按照優先級順序被移除的,該隊列沒有上限,但是如果隊列爲空,那麼取元素的操作take就會被阻塞,所以它的檢索操作task是受阻的,另外該隊列的元素是要具備可比性的,這樣纔可以按照優先級來進行操作。
  4)、DealyQueue,一個使用優先級隊列實現的無界阻塞隊列,支持延遲獲取元素的無邊界阻塞隊列,隊列中的元素必須實現Delay接口,在創建元素的時候,可以指定多久才能從隊列中獲取當前元素,只有在延遲期滿的時候,才能從隊列中獲取元素。
  5)、SynchronousQueue,一個不存儲元素的阻塞隊列,隊列內部僅允許容納一個元素,當一個線程插入一個元素後,會被阻塞,直到這個元素被另外一個線程給消費掉。
  6)、LinkedTransferQueue,一個由鏈表結構組成的無界阻塞隊列,是SynchronousQueue和LinkedBlockingQueue合體,性能比LinkedBlockingQueue更高,因爲它是無鎖操作,比SynchronousQueue存儲更多的元素。
  7)、LinkedBlockingDeque,一個由鏈表結構組成的雙向阻塞隊列,是一個雙端隊列。

 

26、Java中 BIO、NIO、AIO的主要區別。

答:1)、BIO是Block-IO,是傳統的java.io以及部分java.net包下的接口或者類,java.net裏面比如socket、server socket、http,UrlConnection,因爲網絡通信同樣是IO行爲,因此都可以說是輸入BIO的範疇。

  a)、傳統IO基於字節流和字符流j進行操作,提供了InputStream和OutputStream,Reader和Writer。4
  b)、BIO是基於流模型實現的,這意味着其交互方式是同步阻塞的方式,在讀取輸入流或寫入輸出流的時候,在讀寫操作完成之前,線程會一直阻塞在哪裏,它們之間的調用是可靠的線性順序,程序發送請求給內核,然後有內核去進行通信,在內核準備好數據之前,這個線程是被掛起的,所以在兩個階段程序都處於掛起狀態,類比成Client/Server模式呢,則其實現模式爲一個連接,一個線程即客戶端要連接請求的時候服務端就需要啓動一個線程進行處理,待操作系統返回結果,如果這個連接不做任何事情,會造成不必要的線程開銷,當然可以通過線程池機制來改善。
  c)、BIO的特點就是在IO執行的兩個階段都被阻塞住了,好處就是代碼比較簡單,直觀。缺點就是IO效率和擴展性存在瓶頸。

2)、NIO是NonBlock-IO即非阻塞IO,在jdk1.4以後引入了NIO框架,提供了channel、selector、buffer等新的抽象,構建多路複用的,同步非阻塞的IO操作,同時提供了更接近操作系統底層高性能數據操作方式。

  a)、NIO與BIO明顯區別就是,在發起第一次請求之後,線程並沒有被阻塞,它是反覆去檢查數據是否已經準備好,把原來大塊不能用的阻塞的時間分成了許多小阻塞,檢查的是會有一些些阻塞,線程不斷有機會去被執行,檢查這個數據有沒有準備好,有點類似於輪詢,類比成client/server模式呢,其實現模式爲一個請求一個線程,即客戶端發送的連接請求都會註冊到多路複用器上,多路複用器輪循到有IO請求時,才啓動一個線程進行處理。NIO的特點就是程序不斷去主動詢問內核是否已經準備好,第一個階段是非阻塞的,第二個階段是阻塞的。

 

27、NIO的核心部分組成,Channels、Buffers、Selectors。

  基本上所有的IO在NIO中都是從一個Channel開始,Channel有點像流,數據可以從Channel讀到Buffer中,也可以從Buffer中寫到Channel中。

29、NIO-Channels的類型,涵蓋了TCP和UDP網絡IO以及文件IO。

  1)、FileChannel,擁有transferTo方法和transferFron方法。transferTo方法把FileChannel中的數據拷貝到另外一個Channel。transferFron方法把另外一個Channel中的數據拷貝到FileChannel中。該接口常被用於高效的網絡文件的數據傳輸和大文件拷貝,在操作系統支持的情況下,通過該方法傳輸數據,並不需要將源數據從內核態拷貝到用戶態,再從用戶態拷貝到目標通道的內核態。同時也避免了兩次用戶態和內核態間的上下文切換,即零拷貝,效率較高,其性能高於BIO中提供的方法。
  2)、DatagramChannel。
  3)、SocketChannel。
  4)、ServerSocketChannel。

 

30、NIO-Buffers的類型,這些Buffer覆蓋了我們能通過IO發送的基本數據類型。

  1)、ByteBuffer。
  2)、CharBuffer。
  3)、DoubleBuffer。
  4)、FloatBuffer。
  5)、IntBuffer。
  6)、LongBuffer。
  7)、ShortBuffer。
  8)、MappedByteBuffer,主要用於表示內存映射文件。

 

31、NIO-Selector。

  Selector允許單線程處理多個Channel,如果你的應用打開了多個連接即通道,但每個連接的流量都比較低,使用Selector就會很方便了,例如開發一個聊天服務器就排上用場了。如圖所示的是使用一個Selector處理三個Channel的時候,使用Selector得向Selector註冊Channel,然後調用它的select方法,這個方法會一直阻塞,直到某個註冊的通道有事件就緒,一旦這個方法返回呢,線程就可以處理這些事件了,事件可以是,比如說是有新的連接進來,或者說Buffer已經有內容可以讀取到了等等。

 

32、NIO的底層使用了操作系統底層的IO多路複用,調用系統級別的select、poll、epoll等不同方式,優點在於單線程可以同時處理多個網絡IO,IO多路複用調用系統級別的select、poll、epoll模型,由系統監控IO狀態,select輪詢可以監控許多的IO請求,當有一個socket的數據被準備好的時候就可以返回了。

  1)、支持一個進程所能打開的最大連接數。
    a)、select,單個進程所能打開的最大連接數由FD_SETSIZE宏定義,其大小是32個整數的大小(在32位的機器上,大小是32*32,64的機器上FD_SETSIZE爲32*64),我們可以對其進行修改,然後重新編譯內核,但是性能無法保證,需要做進一步測試。
    b)、poll,本質上與select沒有區別,但是它沒有最大連接數的限制,原因是它是基於鏈表來存儲的。
    c)、epoll,雖然連接數有上限,但是很大,1G內存的機器上可以打開10萬左右的連接。
  2)、FD劇增後帶來的IO效率問題。
    a)、select,因爲每次調用時候都會對連接進行線性遍歷,所以隨着FD的增加會造成遍歷速度的線性下降的性能問題。
    b)、poll,同上。
    c)、epoll,由於epoll是根據每個fd上的callback函數來實現的,只有活躍的socket纔會主動調用callback,所以在活躍socket較少的情況下,使用epoll不會有線性線性下降的性能問題,但是所有的socket都很活躍的情況下,可能會有性能問題。
  3)、消息傳遞方式。
    a)、select,內核需要將消息傳遞到用戶空間,需要內核的拷貝動作。
    b)、poll,同上。
  c)、epoll,通過內核的用戶空間共享一塊內存來實現,性能較高。

33、AIO,Asynchronous IO,基於事件和回調機制,異步非阻塞的方式,可以理解爲應用操作直接返回,而不會阻塞在哪裏,當後臺處理完成,操作系統就會通知相應線程進行後續工作。

AIO屬於異步模型,用戶線程可以同時處理別的事情,AIO如何進一步加功處理結果。Java提供了兩種方法。

  1)、基於回調,實現CompletionHandler接口,調用的時候觸發回調函數,在調用的時候,把回調函數傳遞給對應的API即可。
  2)、返回Future,通過isDone查看是否準備好,通過get方法等待返回數據。

 

34、BIO、NIO、AIO對比。

屬性\模型 阻塞BIO 非阻塞NIO 異步AIO
blocking 阻塞並同步。 非阻塞但同步 非阻塞並異步
線程數(server:client) 1:1 1:N 0:N
複雜度 簡單 較複雜 複雜
吞吐量

 

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