一、List、Set 和 Map 的區別?
首先 List 和 Set 是 Collection 接口的子接口,而 Map 是獨立的一個接口,與 Collection 無關
- List:有序,可重復(有序是指存儲順序跟輸入的一樣,而不是說按某種排序方法排序的)。實現的類有:ArrayList、LinkedList、Vector。
- Set:無序,唯一(無序是指存儲順序跟輸入的不一樣,而不是說按某種排序方法排序的)。實現的類有:HashSet、TreeSet。
- Map:使用鍵值對存儲。Map 會維護與 Key 有關聯的值。兩個 Key 可以引用相同的對象,但是 Key 不能重復。
二、List
(一)、ArrayList 與 LinkedList 的區別?
- 線程安全:都是不同步的,所有都是線程不安全的;
- 底層數據結構:
- ArrayList:數組。因此,支持高效的隨機訪問,且隨機訪問的時間復雜度為 O(1)。但是插入和刪除的時間復雜度為 O(n)。
- LinkedList:雙向鏈表數據結構 (JDK1.6 之前為循環鏈表,JDK1.7 取消了循環,注意雙向鏈表和雙向循環列表的區別)。因此,插入和刪除的時間復雜度為 O(1)。隨機訪問的時間復雜度為 O(n)。
- 內存占用空間:ArrayList 主要體現在 list 列表的結尾會預留一定的容量空間,而 LinkedList 的空間花費主要體現在它的每一個元素都有節點,會占用空間。
(二)、ArrayList 與 Vector 的區別?為什么要用 ArrayList 取代 Vector?
區別:Vector 類中的所有方法都是同步的,所以是線程安全的。但一個線程訪問 Vector 的話,代碼要在同步操作上耗費大量的時間,因此效率低。
ArrayList 是不同步的,所以線程不安全,但是效率高。所以如果在不需要保證線程安全時,建議考慮使用 ArrayList。
(三) 、CopyOnWriteArryList
當迭代次數遠大於修改次數時使用該容器。沒次修改其都會復制底層的數組。
CopyOnWriteArrayList 是 ArrayList 的一個線程安全的變體,其中所有可變操作(add、set 等) 都是通過對底層數組進行一次新的復制來實現的。這一般需要很大的開銷,但是當遍歷操作數量大大超過了可變操作的數量時,這種方法可能比其他替代方法更有效。
三、Map
(一)HashMap 和 Hashtable 的區別?
- 繼承不同:HashMap 繼承 AbstractMap,而 Hashtable 繼承 Dictionary。
- 線程是否安全:HashMap 是非線程安全的,Hashtable 是線程安全的;Hashtable 的內部方法基本都被 synchronized 修飾過。如果你要保證線程安全的話就得使用 ConcurrentHashMap。因為線程安全的問題,所以 HashMap 會比 Hashtable 效率高。基本 Hashtable 要被淘汰了,盡量不要用 Hashtable。
- 對 Null 的支持:HashMap 中,Null 即可以作為鍵,又可以作為值,但是 Hashtable 里都不能使用 Null。所以 HashMap 不能由 get() 方法來判斷是否存在某個鍵,而應該用 containsKey() 方法來判斷。
- 初始容量大小和每次擴充容量大小的不同:
- 創建時如果不指定初始容量的初始值,Hashtable 默認的初始大小為 11,每次擴充,容量變為原來的 2n + 1。HashMap 默認的初始大小為 16,之后每次擴容都變為原來的 2 倍。
- 創建時如果給定了容量的初始值,那么Hashtable 會直接使用你給定的大小,而 HashMap 會將其擴充為 2 的冪次方大小(HashMap 中的 tableSizeFor() 方法保證),也就是說 HashMap 總是使用 2 的冪作為哈希表的大小。
- 底層數據結構:JDK1.8 以后的 HashMap 在解決哈希沖突時有了較大的變化,當鏈表長度大於閾值(默認為8)時,將鏈表轉化為紅黑樹,以減少搜索時間。Hashtable 沒有這樣的機制。
- 哈希的使用值不同:Hashtable 直接使對象 hashCode() 的值;而 HashMap 會重新計算 hash 值。
HashMap 部分源碼(JDK 1.8):
- 不帶初始容量的:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
- 帶初始容量的:
public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); }
public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR);
Hashtable 部分源碼 (JDK 1.8):
- 不帶初始容量的:
public Hashtable() { this(11, 0.75f); }
- 帶初始容量的:
public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); if (initialCapacity==0) initialCapacity = 1; this.loadFactor = loadFactor; table = new Entry<?>[initialCapacity]; threshold = (int)Math.min(initialCapacity * loadFactor, Max_ARRAY_SIZE + 1); }
(二)、HashMap 和 HashSet 的區別?
其實 HashSet 底層是基於 HashMap 實現的。
HashMap | HashSet |
實現了 Map 接口 | 實現 Set 接口 |
調用 put() 方法向集合 Map 添加元素 | 調用 add() 方法向 Set 中添加元素 |
HashMap 使用鍵 (Key) 計算hashCode | HashSet 使用成員對象來計算 hashCode 值,對於兩個對象來說 hashCode 值可能相等,所以 equals() 方法用來判斷對象的相等性 |
(三)、HashSet怎樣檢查重復 (屬於 Set 集合)
當你把對象加入 HashSet 時,HashSet 會先計算對象的 hashCode 值來判斷對象加入的位置,同時也會與其他加入對象的 hashCode 值作比較,如果沒有相符合的 hashCode 值,HashSet 會假設對象沒有重復出現。但是如果發現有相同值的對象,這時會調用 equals() 方法來檢查 hashCode 相等的對象是否是真的相同。如果兩者相同,HashSet 就不會允許其加入。
(四)、HashMap 的底層實現
在 JDK1.8 之前 HashMap 底層是數組和鏈表結合在一起使用,也就是鏈表散列。HashMap 通過 key 的 hashCode 經過擾動函數處理過后得到 hash 值,然后通過 (n-1) & hash 判斷當前元素存放的位置(這里的 n 為數組長度),如果當前位置存在元素的話,就判斷該元素與要存入的元素的 hash 值以及 key 是否相同,如果相同,直接覆蓋,不相同則通過拉鏈法解決沖突(哈希沖突)。
JDK 1.8 HashMap 的 hash 方法源碼:
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
對比 JDK 1.7 HashMap 的 hash 方法的源碼
static int hash(int h) { // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
相比 JDK 1.8 的 hash 方法,JDK 1.7 的 hash 方法的性能會差一點點,畢竟擾動了 4 次。
所謂 “拉鏈法” 就是:將鏈表和數組結合起來。也就是說創建一個鏈表數組,數組中的每一個位置就是一個鏈表。如果遇到哈希沖突,就將沖突的值加入到鏈表中。如下圖:
JDK 1.8 之前的內部結構
JDK 1.8 之后
相較於之前的版本,JDK 1.8 之后再解決哈希沖突時有了較大的變化,當鏈表長度大於閾值(默認為8)時,將鏈表轉化為紅黑樹,以減少搜索時間。
JDK 1.8 之后的 HashMap 底層數據結構
TreeMap、TreeSet 以及 JDK1.8之后的 HashMap 底層都用到了紅黑樹。紅黑樹就是為了解決二叉查找樹的缺陷,因為二叉查找樹在某些情況下會退化成一個線性結構。
(五)、HashMap 的長度為什么是 2 的冪次方?
首先,為了能讓 HashMap 存取高效,盡量較少碰撞,也就是要盡量把數據分配均勻。Hash 值的范圍值 -2147483648 到 2147483647,前后加起來大概 40 億的映射空間,只要哈希函數映射得比較均勻松散,一般應用是很難出現碰撞的。但問題是一個 40 億長度的數組,內存是放不下的。所以這個散列值是不能直接拿來用的。用之前還要先做對數組的長度取模運算,得到的余數才能用來要存放的位置也就是對應的數組下標。這個數組下標的計算方法是 "h & (length-1)" 。(h 代表實際的 hash 值, n 代表數組長度)。
其次,length 為 2 的整數次冪的話能夠保證 length 為偶數,這樣 length-1 就為奇數,奇數的二進制的嘴都以為都是 1,這樣就能保證 h & (length-1) 的最后一位可能為 0 也可能為 1 (二進制表示),換成十進制后最后的結果就有可能是偶數也有可能是奇數,這樣更能保證散列的均勻性。而如果 length 為奇數的話,很明顯 length-1 就為偶數,偶數的二進制表示最后一位為 0,這樣 h & (length-1) 的最后一位肯定也為 0 ,換成十進制就為偶數,這樣會造成極大的浪費。
(六)、HashMap 多線程操作導致死循環的問題
主要原因在於 並發下的Rehash 會造成元素之間會形成一個循環鏈表。不過,jdk 1.8 后解決了這個問題,但是還是不建議在多線程下使用 HashMap,因為多線程下使用 HashMap 還是會存在其他問題比如數據丟失。並發環境下推薦使用 ConcurrentHashMap 。
(七)、ConcurrentHashMap 是如何實現的?采用的是什么鎖?
首先來看看 ConcurrentHashMap 的結構:ConcurrentHashMap 是由 Segment 數組結構和 HashEntry 數組結構組成。而 Segment 主要是用於存放一種重入鎖 ReentrantLock,Segment 的數據結構與 HashMap 類似,是一種數組+鏈表結構;HashEntry 用來存放鍵值對數據,是一種鏈表結構。當要修改 HashEntry 的時候,就必須先通過 Segment 才能進行修改,不能獲得則休眠。ConcurrentHashMap 所使用的的所分段技術,首先將數據分成一段一段的存儲,然后給每一段數據配一把鎖,每個 Segment 利用 ReentrantLock 實現,當一個線程被占用鎖訪問其中一個數據的時候,其他段的數據也能被其他線程訪問。
從下圖可以看出 ConcurrentHashMap 主要有三大結構:整個 Hash 表,Segment(段),HashEntry (節點)。每個 Segment 就相當於一個 Hashtable。每個 HashEntry 代表 Hash 表中的一個節點,在其定義的結構中可以看到,出來 value 值與 next 沒有定義 final (value 和 next 定義為 volatile),其余的都為 final 類型。ConcurrentHashMap 讀不需要加鎖。
根據 hash 值定位 Segment 時會調用 Segment 方法,並返回相應的 Segment 在數組中的下標。Segment 的 get 操作實現非常簡單和高效。先經過一次哈希,然后使用這個哈希值通過運算定位到 Segment,再通過哈希算法定位到元素。get 操作的高效之處在於整個 get 過程中不需要加鎖,除非讀到的值是空的才會加鎖重讀,我們知道 Hashtable 容器的 get 方法是需要加鎖的,那么 ConcurrentHashMap 的 get 操作是如何做到不加鎖的?原因在於它的 get 方法里將要使用的共享變量都定義成了 volatile,如用於統計當前 Segment 大小的 count 字段和用於存儲值的 HashEntry 的 value。定義成 volatile 的變量,能夠在線程之間保持可見性,能夠被多線程同時讀,並且保證不會讀到過期的值,但是只能被單線寫(有一種情況可以被多線程寫,就是寫入的值不依賴於原值),在 get 操作里只需要讀不需要寫共享變量 count 和 value,所以可以不用加鎖。
之所以不會讀到過期值,是根據 Java 模型的 happen-before 原則,對 volatile 字段的寫入操作先於讀操作,即使兩個線程同時修改和獲取 volatile 變量,get 操作也能拿到最新的值,這就是用 volatile 替換所的景點應用場景。
Segment 類繼承於 ReentrantLock 類,從而使得 Segment 對象能夠充當鎖的角色。每個 Segment 對象用來守護其(成員變量 table 中)包含的若干個桶。count 變量是一個計數器,它表示每個 Segment 對象管理的 table 數組(若干個 HashEntry 組成鏈表) 包含的 HashEntry 對象的個數。每一個 Segment 對象都有一個 count 對象來表示本 Segment 中包含的 HashEntry 對象的總數。注意,之所有在每個 Segment 對象中都包含一個計數器,而不是在 ConcurrentHashMap 中使用全局的計數器,視為了避免出現 “熱點域” 而影響 ConcurrentHashMap 的並發性。
默認情況下,每個 ConcurrentHashMap 類會創建 16 個並發的 Segment,每個 Hash鏈都是有 HashEntry 節點組成的。ConcurrentHashMap 中的讀方法不許需要加鎖,所有的修改操作在進行結構修改時都會在最后一步寫 count 變量,通過這種機制保證 get 操作能夠得到幾乎最新的結構更新。
參考:https://blog.csdn.net/yansong_8686/article/details/50664351
(八)、CurrentHashMap 和 HashMap 的區別?
- 底層數據結構:JDK 1.7 的 ConcurrentHash 底層采用 分段的數組+鏈表 實現,JDK 1.8 采用的數據結構跟 HashMap 1.8 的結構一樣,數組+鏈表/紅黑二叉樹。Hashtable 和 JDK1.8 之前的 HashMap 的底層數據結構類似都是 數組+鏈表 的形式,數組是 HashMap 的主體,鏈表則是主要為了解決哈希沖突而存在的。
- 實現線程安全的方式(重要):
- 在 JDK1.7 的時候,ConcurrentHashMap(分段鎖) 對整個桶數組進行了分段(Segment),每一把鎖只所容器的一部分數據,多線程訪問容器里不同數據段的數據,就不會存在鎖競爭,提高並發訪問率。到了 JDK1.8 的時候,已經摒棄了 Segment 的概念,而是直接用 Node 數組+鏈表+紅黑樹的數據結構來實現,並發控制使用 synchronized 和 CAS 來操作。(JDK1.6 以后對 synchronized 鎖做了很多的優化) 整個看起來就像是優化過且線程安全的 HashMap,雖然在 JDK1.8 中還能看到 Segment 是數據結構,但是已經簡化了屬性,只是為了兼容舊版本;
- Hashtable(通一把鎖):使用 sunchronized 來保證線程安全,效率非常低下。當一個線程訪問同步方法的時,其他線程也訪問同步方法,可能會進入阻塞狀態,如使用 put 添加元素,另一個線程不能使用 put 添加元素,也不能使用 get,競爭會越來越激烈而效率也會越低。
Hashtable
JDK1.7 的 ConcurrentHashMap
JDK1.8 的 ConcurrentHashMap(TreeBin:紅黑樹節點;Node:鏈表節點)
(九)、ConcurrentHashMap 線程安全的具體實現方式/底層具體實現
JDK1.7:
首先將數據分為一段一段的存儲,然后給每一段數據配一把鎖,當一個線程占用鎖訪問其中一個段數據時,其他段數據也能被其他線程訪問。
ConcurrentHashMap 是由 Segment 數組結構和 HashEntry 數組結構組成
Segment 實現了 ReentrantLock,所以 Segment 是一種可重入鎖,扮演鎖的角色。HashEntry 用於存儲鍵值對數據。
static class Segment<K,V> extends ReentrantLock implements Serializable { }
一個 ConcurrentHashMap 里包含了一個 Segment 數組。Segment 的結構和 HashMap 類似,是一種數組+鏈表結構,一個 Segment 包含多個 HashEntry 組成的數組,每個 HashEntry 是一個鏈表結構的元素,每個 Segment 守護者一個 HashEntry 數組里的元素,當對 HashEntry 數組的數據進行修改時,必須首先獲得對應的 Segment 的鎖。
JDK1.8
ConcurrentHashMap 取消了 Segment 分段,采用 CAS 和 synchronized 來保證並發安全。數據結構跟 HashMap1.8 相似,數組+鏈表/紅黑樹。Java 8 在鏈表長度超過一定閾值(8)時將鏈表(尋址時間復雜度為O(n))轉換為紅黑樹(尋址時間復雜度為 O(log n))
synchronized 只鎖定當前鏈表或者紅黑二叉樹的首節點,這樣只要 hash 不沖突,就不會產生並發,效率得到提升。
參考:https://www.cnblogs.com/chengxiao/p/6059914.html、https://www.cnblogs.com/chengxiao/p/6842045.html