大家好,我是
方圓
吶
準備認認真真寫完這篇博客,就準備考試周了
希望大家過得快樂呀!
目錄
1. Collections
1.1 List
特點:
有序(指的是存取有序)
可重複
可通過索引值操作元素
分類:
底層是數組,查詢快,增刪慢
;(ArrayList,線程不安全,效率高;Vector,線程安全,效率低)底層是鏈表,查詢慢,增刪快
;(LinkedList,線程不安全,效率高)
源碼:
- 擴容方法,grow()
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
1.2 Set
特點:
無序
元素唯一
分類
-
底層是HashMap
;(HashSet,保證元素的唯一性,利用的是hashCode()和equals()方法) -
底層是TreeMap
;(TreeSet,保證元素的有序性 -
進行的排序方式
對象所屬的類自己實現comparable接口
,向TreeSet中添加元素的時候,會調用compareTo()方法比較- 在創建TreeSet對象的時候,
構造函數中傳入comparator()
,源碼如下
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
2. HashMap
在JDK1.8之前
,底層是數組+鏈表
在JDK1.8
,底層是數組+鏈表+紅黑樹,下面主要介紹JDK1.8中的HashMap
2.1 幾個簡單的參數
//初始化大小爲16,左移運算符<<,移動一位*2
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//負載因子,默認0.75,不需要修改,這是經過實踐得出的最合適的
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//由鏈表變成數的閾值
static final int TREEIFY_THRESHOLD = 8;
//由樹變回鏈表的閾值
static final int UNTREEIFY_THRESHOLD = 6;
2.2 構造函數
//無參構造,大小爲16,負載因子爲0.75,不進行初始化,懶加載
//後邊put()方法中說明
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
//修改負載因子的構造函數,初始大小爲16,調用下方的構造函數
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//
public HashMap(int initialCapacity, float loadFactor) {
//初始容量不能小於0
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;
//初始容量準備好了,利用tableSizeFor()方法來計算table的閾值
//注意,並不進行初始化,只有在第一次put的時候才進行初始化
this.threshold = tableSizeFor(initialCapacity);
}
下面我們看一下tableSizeFor()方法,計算table大小的閾值
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
我們來圖解一下這個過程,以65爲例
2.3 put()方法源碼
- 方法邏輯
- 如果
HashMap未被初始化,則進行初始化
- 對Key求Hash值,然後再計算下標
- 如果沒有碰撞了,直接放入桶中
如果碰撞了,以鏈表的方式鏈接到後面
- 如果鏈表長度
超過8
,就把鏈表轉成紅黑樹 - 如果鏈表長度
低於6
,就把紅黑樹轉回鏈表 如果節點已經存在,就替換舊值
- 如果
桶滿了(超過容量*0.75),就需要resize,擴容2倍後重排
- 源碼
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods.
*
* @param hash key的hash值
* @param key key值
* @param value value值
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//tab代表Node數組,p爲數組中已存在的Node,n爲數組長度,i爲索引值
Node<K,V>[] tab; Node<K,V> p; int n, i;
//Node[]爲空或者長度爲0,當我們第一次進行put()的時候就是這樣
//通過resize()方法進行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//不發生碰撞的情況下,直接放入桶中
//計算索引採用的是(長度-1)與hash值進行位與運算
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//這裏是發生碰撞的情況
Node<K,V> e; K k;
//已存在的node的hash值和加入的hash值相等
//且key一致
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
//若新加入的node爲樹的節點的話,調用的是putTreeVal()方法
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//發生碰撞的時候,不是頭節點的key一致,那麼要對鏈表進行遍歷尋找
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
//到達尾節點的時候,那麼就把它插在末尾
p.next = newNode(hash, key, value, null);
//判斷是否超過了閾值8,超過就樹化
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
//在這裏找到了hash值相同的key的鏈表位置
break;
p = e;
}
}
//加入的節點不是空節點,且e已經到了key所在的鏈表位置
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
//這裏就是爲什麼我們插入成功的時候會返回舊值
return oldValue;
}
}
//操作計數+1
++modCount;
//若超過大小*0.75的閾值,需要進行擴容重排
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
2.3.1 resize()方法源碼
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
//容量爲零,這裏供調用無參構造就行初始化的時候使用
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
//最大容量情況不能再擴容
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//符合擴容條件,讓閾值*2
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//默認大小
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
2.3.2 擴容存在的問題
- 在多線程環境下,調整大小會存在條件競爭,造成死鎖
- rehashing是一個比較耗時的過程
2.3.3 HashMap如何減少碰撞?
擾動函數
:促使元素位置分佈更加均勻,減少碰撞機率使用被final修飾的對象
,並採用合適的equals()和hashcode()方法
2.4 get()方法源碼
//我們調用的get()方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
//內部實際調用的get()方法
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
//總會定位到數組中鏈表第一個頭節點的位置
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
//是樹節點的時候調用getTreeNode()方法
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
//比較hash值和key
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
2.5 hash()源碼
static final int hash(Object key) {
int h;
//如果key爲null,把它放在數組的第一個位置,HashMap是可以存null的
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
若key不爲null時,我們對其進行圖解
3. ConcurrentHashMap
3.1 讓HashMap線程安全的方法
- 使用
Collections.sychronizedMap(hashMap)
方法 HashTable是線程安全
的,因爲它的public方法都被sychronized修飾- 使用
ConcurrentHashMap
3.2 ConcurrentHashMap的底層原理
- 早期的ConcurrentHashMap是通過
分段鎖segement
來實現,segement繼承ReetrantLock,每個segement守護若干個entry - JDK1.8,
CAS+sychronized
使鎖更加細化,CAS(比較並交換)是CPU指令級的操作,只有一步原子操作,所以非常快
3.3 重要概念
sizeCtl
:默認爲0,用來控制table初始化和擴容的操作
-1
代表進行resize(),初始化或擴容重排
-n
代表有n-1個線程正在進行resize()操作
若table未初始化
,則代表需要初始化的大小
若table初始化完成
,表示table的容量
private transient volatile int sizeCtl;
Node
它的value和next都是volatile修飾的,保證併發的可見性
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
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; }
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)));
}
3.4 Table初始化
- 源碼
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
//表示其他線程正在進行操作,當前線程要讓出cpu時間片
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
sizeCtl默認值爲0
,只有我們在實例化ConcurrentHashMap時傳參
的時候,sizeCtl會調用tableSizeFor()方法
,賦值爲一個2的n次冪的值。第一次執行put方法時,其中U.compareAndSwapInt(this, SIZECTL, sc, -1)
會將其修改爲-1,這就代表只能有一個線程能對其進行修改,其他線程則Thread.yield()
,讓出CPU時間片。
3.5 put()方法
- 源碼
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
//不能添加null值
if (key == null || value == null) throw new NullPointerException();
//hash算法
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
//進行初始化
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//數組中鏈表的頭節點,直接插入
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)
//此時hash值爲-1,表示當前f是ForwardingNode節點
//表示正在進行擴容操作,那麼它要一起進行擴容
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
//其他情況,採用的是同步內部鎖保證併發
if (tabAt(tab, i) == f) {
if (fh >= 0) {
//表示f是鏈表的頭節點
binCount = 1;
//遍歷鏈表,若找到對應的節點,修改value值
//若沒有找到,則在尾部進行添加
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (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);
return null;
}
3.6 hash算法
與HashMap有些區別,多了一步和HASH_BITS進行位與運算
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
3.7 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());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
get()方法比較簡單,我們重點看一下其中的tabAt(tab, (n - 1)
方法
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);
}
U.getObjectVolatile()
,保證每次都能獲取到最新的數據,因爲每個線程都有一個自己的工作內存,裏邊存有table的副本,爲了保證它能每次都能從主內存中獲得最新的值,所以會用此方法
3.8 擴容
- 步驟
- 構建一個nextTable,大小爲table的兩倍
- 把table中的數據複製到nextTble中
- 源碼
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
通過Unsafe.compareAndSwapInt
修改sizeCtl值,保證只有一個線程能夠初始化nextTable
,擴容後的數組長度爲原來的2倍
,但是容量是原來的1.5倍
。
節點從table移動到nextTable,大體思想是遍歷、複製的過程。
-
首先根據運算得到需要遍歷的次數i,然後利用tabAt方法獲得i位置的元素f,初始化一個forwardNode實例fwd。
-
如果f == null,則在table中的i位置放入fwd,這個過程是採用
Unsafe.compareAndSwapObjectf
方法實現的,很巧妙的實現了節點的併發移動。 -
如果f是鏈表的頭節點,就構造一個
反序鏈表
,把他們分別放在nextTable的i和i+n的位置上,移動完成,採用Unsafe.putObjectVolatile方法給table原位置賦值fwd。 -
如果f是TreeBin節點,也做一個
反序處理
,並判斷是否需要untreeify,把處理的結果分別放在nextTable的i和i+n的位置上,移動完成,同樣採用Unsafe.putObjectVolatile方法給table原位置賦值fwd。
遍歷過所有的節點以後就完成了複製工作,把table指向nextTable,並更新sizeCtl爲新數組大小的0.75倍 ,擴容完成。
4. HashTable、HashMap、ConcurrentHashMap的區別
- HashMap
線程不安全
,底層是數組+鏈表+紅黑樹
- HashTable
線程安全
,它鎖住的是整個對象
,底層是數組+鏈表
- ConcurrentHashMap
線程安全
,利用的是CAS+synchronized
,底層是數組+鏈表+紅黑樹
參考
哇哇哇!寫完啦!爽到!