HashMap原始碼分析

Howlet發表於2020-12-15


分析1.7、1.8的HashMap、ConcurrentHashMap的區別

越到後面越煩躁,寫不下去了,到時回來填坑把









1. 補充位運算

位運算是對2進位制而言的

符號 描述 運算規則
& 兩個都為1,結果才為1
| 兩個都為0,結果才是0
^ 異或 同0異1
~ 取反 0變1,1變0
<< 左移 各二進位全部左移若干位,高位丟棄,低位補0。表示2次冪
>> 右移 各二進位全部右移若干位,對無符號數,高位補0。表示除2
有符號數,各編譯器處理方法不一樣,有的補符號位(算術右移),有的補0(邏輯右移)
>>> 邏輯右移 不管符號位,直接右移,補零


1.1 位運算實現取模

// 其實很簡單,主要看length
// &運算,就算h全部為1,&之後都是看length有1的部分
// 那麼最大隻能是length,所以範圍限定在了length裡,比 % 運算快多了
// -1為了符合陣列0開始
// 這也是擴容為2次冪的原因,配套取模運算
h & (length-1)


1.2 二次冪

二次冪的二進位制只有一個1,其他位為0

如果減1,那麼二進位制中的1變成0,後面的0全部變成1,符合上面的length,配合實現取模運算

1---0001
2---0010
4---0100
8---1000






2. JDK 1.7

使用頭插法,連結串列放頭部是最快的,尾部需要遍歷的

真要put的才初始化,體現懶載入



2.1 建構函式

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;
    
    // 這裡注意講初始化容量大小,賦值給閥值,後面擴容表用到
    // 其實也就是擴容大小,不過為什麼用閥值賦值??
    threshold = initialCapacity;
    
    // 真正的初始化
    init();
}


2.2 二次冪

// 總之返回一個大於但最接近number的二次冪
private static int roundUpToPowerOf2(int number) {
    
    // assert number >= 0 : "number must be non-negative";
    return number >= MAXIMUM_CAPACITY 
        
        // -1保證本身不是2次冪,左移保證大於大於當前2次冪,而小於下一個2次冪
        ? MAXIMUM_CAPACITY : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}

// 返回小於等於的2次冪
public static int highestOneBit(int i) {
    // 多次右移再異或保證最高位的1後面全是1
    i |= (i >>  1);
    i |= (i >>  2);
    i |= (i >>  4);
    i |= (i >>  8);
    i |= (i >> 16);
    
    // 這裡保證除了最高位1以為,其餘全部變成0了
    return i - (i >>> 1);
}



2.3 擴容表

private void inflateTable(int toSize) {
    
    // Find a power of 2 >= toSize
    int capacity = roundUpToPowerOf2(toSize);

    // 和最大容量比咯,選擇小的。。。
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    
    // 建立雜湊表的桶子
    table = new Entry[capacity];
    
    // 是否再雜湊
    initHashSeedAsNeeded(capacity);
}



2.4 再雜湊

// 正常來說都不用
final boolean initHashSeedAsNeeded(int capacity) {
    
    // 雜湊種子,預設0,返回false
    boolean currentAltHashing = hashSeed != 0;
    
    boolean useAltHashing = sun.misc.VM.isBooted() &&
        
        	// 最關鍵在這裡,這裡要容量允許的最大值時才為true
        	// Holder裡面從JVM環境配置拿看有沒有設定自定義的最大容量
            (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    
    //						false			false
    boolean switching = currentAltHashing ^ useAltHashing;
    
    // false,意思不用再hash
    if (switching) {
        hashSeed = useAltHashing ? sun.misc.Hashing.randomHashSeed(this) : 0;
    }
    return switching;
}



2.5 雜湊值

// 注意:根據Key來獲取hash值的
final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }

    // 根據Key的雜湊值再和雜湊雜湊
    h ^= k.hashCode();
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}



2.6 獲取下標

static int indexFor(int h, int length) {
    // 取模,必須確保2次冪
    return h & (length-1);
}



2.7 get方法

public V get(Object key) {
    
    // 當然首先判斷是不是空,空就呼叫專門對NULL的方法,1.7有專門分開
    if (key == null) return getForNullKey();
    
    // 否則呼叫正常流程
    Entry<K,V> entry = getEntry(key);

    // Entry是從Tree裡面繼承的,K/V結構
    return null == entry ? null : entry.getValue();
}

// 看看NULL的實現
private V getForNullKey() {
    
    // 如果空表,返回null把
    if (size == 0) {
        return null;
    }
    
    // null 是固定放在0號下標的
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null)
            return e.value;
    }
    return null;
}

// 正常流程
final Entry<K,V> getEntry(Object key) {
    
    // 當然也有非空判斷
    if (size == 0) {
        return null;
    }

    // 這裡的null可能是直接呼叫,不用通過get的把
    // 比如containsKey:return getEntry(key) != null;
    int hash = (key == null) ? 0 : hash(key);
    
    // 流程才是重點
    // 1.根據Key的雜湊值獲取桶子下標,然後遍歷該桶子上的元素進行判斷
    // 2.判斷流程:
    // 	2.1 判斷元素的雜湊值
    // 	2.2 再判斷Key是否地址相同(比如String有常量池),或者值相同
    for (Entry<K,V> e = table[indexFor(hash, table.length)];e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
            return e;
    }
    return null;
}



2.8 put方法

// 也是注意流程
// 1.根據Key獲取桶下標進行遍歷,有相同的就替換
// 2.沒有相同則插入
// 	2.2
public V put(K key, V value) {
    
    // 體現懶載入,新增元素才去看看初始化沒
    if (table == EMPTY_TABLE) {
        // 前面函式有說明,擴容表
        // 看這裡傳進入的是閥值,也就是第一次賦值時傳進去的容量大小
        inflateTable(threshold);
    }
    
    // 看來為空都有特殊對待
    if (key == null) return putForNullKey(value);
    
    // 獲取hash值,以及對映對應的獲取桶子下標
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    
    // 連結串列遍歷咯,看是否有相同的,相同則替換
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        
        // 步驟都差不多
        // 第一步比hash,第二步比key地址,第三步比key值
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            
            // 如果已經存在值了,那麼替換,返回舊值
            V oldValue = e.value;
            e.value = value;
            
            // 這裡是空,主要給LinkedHashMap記錄訪問順序的
            e.recordAccess(this);
            
            // 返回舊值
            return oldValue;
        }
    }
 
    // 快速失敗機制
    modCount++;
    
    // 沒有重複的值,那麼就插入
    addEntry(hash, key, value, i);
    return null;
}

2.8.1 插入空值流程

// 所以這裡存在的就是為了減少運算??
// 直接從0下標遍歷,麻煩
private V putForNullKey(V value) {
    
    // 區別在於直接從0下標遍歷,因為NULL只存0下標
    // 也是先遍歷,看有沒有存在替換,沒有就插入
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    
    // 少不了的套路,快速失敗
    modCount++;
    
    // 這裡才是真正的插入方法
    addEntry(0, null, value, 0);
    return null;
}

2.8.2 插入非NULL值流程

// 這裡還是新增Entry的判斷(判斷條件也挺重要的)
void addEntry(int hash, K key, V value, int bucketIndex) {
    
    // 判斷閥值,且該桶子上不為空
    // 為空就不擴容,為了節省了一次擴容消耗???
    // 這裡是1.7的特點,目標下標為空可以直接插入,不進行擴容
    if ((size >= threshold) && (null != table[bucketIndex])) {
        
        // 擴容後面講,知道這裡2倍就是了
        // 擴容完,當然還要記得插入元素
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

    // 正真的頭插入過程
    createEntry(hash, key, value, bucketIndex);
}

2.8.3 真正的插入元素

// 1.7 使用頭插法,併發會出現死迴圈,1.8 使用尾插法
// 下面是頭插法的流程,慢慢看,可能有點難懂
void createEntry(int hash, K key, V value, int bucketIndex) {
   
    // 先獲取桶子指定下標桶的元素
    Entry<K,V> e = table[bucketIndex];
    
    // 然後將桶子上的元素換成將要插入的元素
    // 被頂掉的元素放入,插入元素的next屬性上就OK了
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    
    // 最後別忘實際存放的元素數量+1
    size++;
}



2.9 再雜湊(擴容)

// 擴容理由:
// 桶子不夠,雜湊衝突過多
// 連結串列過長,影響get遍歷效率

// 注意流程:
// 1.建新表,兩倍大小
// 2.資料轉移
// 	2.1 遍歷桶子的同時,遍歷桶子上的連結串列
//	2.2 然後逐個拆分連結串列元素,再移動新舊錶上。(1.8是拼接新舊拼接好後才移動的)
// 3.新表替舊錶
void resize(int newCapacity) {
    
    // 舊桶
    Entry[] oldTable = table;
    
    // 舊容量
    int oldCapacity = oldTable.length;
    
    // 如果已經最大了,那麼設定閥值,然後什麼都不做直接返回
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }

    // 建立一個新桶,2倍大小的
    Entry[] newTable = new Entry[newCapacity];
    
    // 資料轉移,第二個引數新容量決定是否再雜湊
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    
    // 新表替舊錶
    table = newTable;
    
    // 設定新閥值,其實可以直接*2的
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

2.9.1 資料轉移

void transfer(Entry[] newTable, boolean rehash) {
    
    // 取出新桶容量
    int newCapacity = newTable.length;
    
    // 遍歷舊桶
    for (Entry<K,V> e : table) {
        
        // 遍歷每個桶子上的連結串列
        while(null != e) {
            
            // 這裡獲取了當前遍歷的e(Entry),以及e後一個Entry,就是遍歷連結串列
            Entry<K,V> next = e.next;
            
            // 判斷需不需要再雜湊
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            
            // 在同一個桶子上,證明hash取模容量之後相同,只是下標相同,不一定hash相同
            // 如果容量變化了,那麼取模之後這些同連結串列的元素就會拆分
            // 這裡擴容完:
            // 問題一:連結串列因為頭插法,倒序了
            // 問題二:多執行緒新增元素擴容,死鎖。執行緒1元素丟失,執行緒2死迴圈
            
            // 下面是頭插法
            // 獲取新下標,因為桶長變了
            int i = indexFor(e.hash, newCapacity);
            
            // 這步和下一步共同組成移到桶子上方,然後再下移
            e.next = newTable[i];
            
            // 然後再往下移動
            newTable[i] = e;
            
            // 這個操作和上面合起來相當於:e = e.next,
            e = next;
        }
    }
}

2.9.2 多執行緒擴容

// 舊雜湊值兩個執行緒共有
// 對於新表,兩個執行緒各自會建立一個新表
// 轉移到新表的時候,頭插法混亂,執行緒1還沒移動完成,執行緒2就開始移動,二者的連結串列會形成環,從而死迴圈



2.10 ConcurrentHashMap

併發的HashMap,使用了ReentrantLock,下面看原理


3.10.1 原理

// 有內部類Segment,繼承了ReentranLock,且是個小的HashMap,那麼每個分段擴容不同大小也就不同了

// 內有兩陣列,Segment[]、Entry[]

// Segment[]:實現了分段鎖機制,往陣列插入防止併發要用到CAS

// Entry[]:是我們真正存放元素的

// 意思就是Segment陣列裡面有個Entry陣列,要往Entry放元素,得先獲取Segment的鎖

2.10.2 屬性

// 併發級別,預設16
// 就是總共有多少個鎖,分多少段(Segment),繼承ReentrantLock
static final int DEFAULT_CONCURRENCY_LEVEL = 16;

// 最多分段
static final int MAX_SEGMENTS = 1 << 16; // slightly conservative

2.10.3 構造方法

public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
    
    // 引數校驗
    if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    if (concurrencyLevel > MAX_SEGMENTS) concurrencyLevel = MAX_SEGMENTS;
    
    // Find power-of-two sizes best matching arguments
    int sshift = 0;
    int ssize = 1;
    
    // 找大於等於併發級別的2次冪最小值
    while (ssize < concurrencyLevel) {
        
        // 記錄併發級別的二進位制位數
        ++sshift;
        ssize <<= 1;
    }
    
    // 這裡保留併發級別相關的位數,後面左移用到了
    this.segmentShift = 32 - sshift;
    
    // 這個用來取模找Segment陣列下標
    this.segmentMask = ssize - 1;
    
    if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY;
    
    // 這裡看每個Segment存多少個
    int c = initialCapacity / ssize;
    
    // 除法的向上取整
    if (c * ssize < initialCapacity) ++c;
    
    // cap最小預設為2,這裡是Segment裡的HashMap用的容量大小,當然需要2次冪了
    int cap = MIN_SEGMENT_TABLE_CAPACITY;
    while (cap < c) cap <<= 1;
    
    // create segments and segments[0]
    // 建立一個S0的分段,不用每次都計算分段內HashMap的大小了
    Segment<K,V> s0 =
        
        // 載入因子,閥值,table建好放進去給你了
        new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                         (HashEntry<K,V>[])new HashEntry[cap]);
    Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
    
    // 意思是結構複製
    UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
    this.segments = ss;
}

2.10.4 put方法

public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null) throw new NullPointerException();
    int hash = hash(key);
    
    // 根據hash值,獲取Segment陣列的下標
    int j = (hash >>> segmentShift) & segmentMask;
    
    // 獲取對應下標的陣列元素,判斷是否為空
    if ((s = (Segment<K,V>)UNSAFE.getObject(segments, (j << SSHIFT) + SBASE)) == null)
        // 為空建立小HashMap咯
        s = ensureSegment(j);
    
    return s.put(key, hash, value, false);
}

2.10.5 生成Segment物件

private Segment<K,V> ensureSegment(int k) {
    
    // 又來一次判斷??
    final Segment<K,V>[] ss = this.segments;
    long u = (k << SSHIFT) + SBASE; // raw offset
    Segment<K,V> seg;
    
    // 看看有沒有其他執行緒已經生成新的了,UNSAF安全類
    if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
        
        // 從原型獲取
        Segment<K,V> proto = ss[0]; // use segment 0 as prototype
        int cap = proto.table.length;
        float lf = proto.loadFactor;
        int threshold = (int)(cap * lf);
        HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
        
        // 又判斷是不是空,再次檢查
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
            
            // new了一個Segment物件
            Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
            
            // 判空才自旋CAS新增第一層陣列元素
            while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
                
                // 根據CAS操作交換的
                if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                    break;
            }
        }
    }
    
    // 這裡不管是否自己建立的,都會返回一個Segment物件,前面有多次獲取最新。。。
    return seg;
}

2.10.6 Segment的put方法

// Segment插入當然使用繼承的ReentranLock
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    
    // Seg物件的put方法會有鎖,tryLock不阻塞的,和Lock比
    // tryLock失敗,可以用後面的獲取鎖方法
    HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
    V oldValue;
    
    try {
        
        // 獲取該Seg對應的table
        HashEntry<K,V>[] tab = table;
        int index = (tab.length - 1) & hash;
        
        // 獲取對應的table下標,返回第一個Entry,後面劇情應該是遍歷是否重複,然後頭插法
        HashEntry<K,V> first = entryAt(tab, index);
        for (HashEntry<K,V> e = first;;) {
            if (e != null) {
                K k;
                if ((k = e.key) == key || (e.hash == hash && key.equals(k))) {
                    oldValue = e.value;
                    if (!onlyIfAbsent) {
                        e.value = value;
                        ++modCount;
                    }
                    break;
                }
                e = e.next;
            }
            else {
                if (node != null)
                    node.setNext(first);
                
                // 頭插法
                else
                    node = new HashEntry<K,V>(hash, key, value, first);
                
                // 小HashMap的實際大小
                int c = count + 1;
                
                // 內部擴容機制
                if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                    rehash(node);
                else
                    
                    // 這個set內部用Unsafe方法,否則改的是執行緒的,不是記憶體的,可能還是有衝突
                    setEntryAt(tab, index, node);
                ++modCount;
                count = c;
                oldValue = null;
                break;
            }
        }
    } finally {
        unlock();
    }
    return oldValue;
}

2.10.7 獲取鎖函式

private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
    
    // 獲取table上對應的Entry
    HashEntry<K,V> first = entryForHash(this, hash);
    HashEntry<K,V> e = first;
    HashEntry<K,V> node = null;
    
    // 重試次數
    int retries = -1; 
    
    while (!tryLock()) {
        HashEntry<K,V> f; // to recheck first below
        
        // 先遍歷判斷要不要new一個Entry
        if (retries < 0) {
            if (e == null) {
                if (node == null) // speculatively create node
                    node = new HashEntry<K,V>(hash, key, value, null);
                retries = 0;
            }
            else if (key.equals(e.key))
                retries = 0;
            else
                e = e.next;
        }
        
        // 超過重試次數,直接lock()即可能被阻塞
        else if (++retries > MAX_SCAN_RETRIES) {
            lock();
            break;
        }
        
        // 判斷獲取的頭部,和最新的頭部是否相同。這裡就是判斷別人獲取鎖時,有否改變結構。
        else if ((retries & 1) == 0 && (f = entryForHash(this, hash)) != first) {
            e = first = f; // re-traverse if entry changed
            retries = -1;
        }
    }
    return node;
}

2.10.8 擴容

private void rehash(HashEntry<K,V> node) {

    HashEntry<K,V>[] oldTable = table;
    int oldCapacity = oldTable.length;
    int newCapacity = oldCapacity << 1;
    threshold = (int)(newCapacity * loadFactor);
    HashEntry<K,V>[] newTable = (HashEntry<K,V>[]) new HashEntry[newCapacity];
    int sizeMask = newCapacity - 1;
    
    // 遍歷轉移咯
    for (int i = 0; i < oldCapacity ; i++) {
        HashEntry<K,V> e = oldTable[i];
        if (e != null) {
            HashEntry<K,V> next = e.next;
            
            // 直接獲取了新下標,不用判斷是否重雜湊
            int idx = e.hash & sizeMask;
            
            // 只有一個元素直接放進桶子
            if (next == null)
                newTable[idx] = e;
            
            // 
            else { // Reuse consecutive sequence at same slot
                HashEntry<K,V> lastRun = e;
                int lastIdx = idx;
                
                // 這個迴圈為了:記錄最後下標相同的元素,轉移就一起轉移過去了
                // 只是最後的,前面的和後面的就被覆蓋了,即沒有記錄到
                for (HashEntry<K,V> last = next;last != null;last = last.next) {
                    int k = last.hash & sizeMask;
                    if (k != lastIdx) {
                        lastIdx = k;
                        lastRun = last;
                    }
                }
                newTable[lastIdx] = lastRun;
                // Clone remaining nodes
                for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
                    V v = p.value;
                    int h = p.hash;
                    int k = h & sizeMask;
                    HashEntry<K,V> n = newTable[k];
                    newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
                }
            }
        }
    }
    
    // 秉持1.7先擴容再插入元素
    int nodeIndex = node.hash & sizeMask; // add the new node
    node.setNext(newTable[nodeIndex]);
    newTable[nodeIndex] = node;
    table = newTable;
}






3. JDK 1.8

  • 判斷是否空
  • get
    • 判斷第一個元素
    • 然後下一個再判斷是否紅黑樹
    • 不是紅黑樹就用連結串列的方法
  • put
    • 判斷表是否為空,為空初始化
    • 判斷對應的桶子上是否為空,為空直接插入
    • 判斷是否匹配第一個元素,是就直接替換
    • 否則判斷是否紅黑樹,否則進入連結串列處理階段(迴圈遍歷看是否有重複元素)
    • 有重複元素退出迴圈,進入值替換
    • put之後,再判斷是否需要resize ()
  • 判斷節點是否相等。先比Hash值,再比地址(適用null),最後再比key值



3.1 變數

// 初始化容量大小16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

// 最大容量2^30
static final int MAXIMUM_CAPACITY = 1 << 30;

// 預設載入因子0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 轉化紅黑樹界限8
static final int TREEIFY_THRESHOLD = 8;

// 轉化成連結串列界限6,二者不同是為了不要在界限內一直轉換
static final int UNTREEIFY_THRESHOLD = 6;

// 當連結串列需要轉換成紅黑樹時,先先判斷陣列長度是否<64,是的話不轉換紅黑樹,先擴容
static final int MIN_TREEIFY_CAPACITY = 64;



3.2 節點

// Node實現了Entry,之前用的是Entry
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}



3.3 雜湊值

// 根據key生成, >>>無符號舍棄右邊16位,即取高16位
// 因為一般桶長度都在2位元組下,
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

// n = tab.length
// 一般桶長度都小於2^16即小於65536,即低16位才有效
// 這樣與雜湊異或的話,只能用到了低16位的
// 那麼上面hash自身與自身的高16位異或,就利用到了高16位,更隨機
tab[(n - 1) & hash]
// 返回桶下標



3.4 二次冪

// 1.8大於輸入引數且最近的2的整數次冪的數,而1.7是小於等於的
// 或,右移運算
// 這樣做為了讓二進位制中,最高位的1後面全置為1,後面加1轉換為從1開始計數,不是從0開始了
static final int tableSizeFor(int cap) {
    int n = cap - 1; // 防止已經是2次冪的情況
    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;
}



3.5 欄位

transient Node<K,V>[] table;

transient Set<Map.Entry<K,V>> entrySet;

// 實際儲存元素多少
transient int size;

// 記錄 hashMap 發生結構性變化的次數
// 呼叫迭代器的時候,將modCount賦值給迭代器內部
// 如果修改了結構,modCount就會+1
// 那麼迭代器迭代一次,就會判斷內部的expectedModCount 和 HashMap的是否相同,不同則丟擲異常
// 呼叫Iterator的remove方法即可,內部就是重新賦值迭代器內部的modCount而已
transient int modCount;

// 值等於table.length * loadFactor, size 超過這個值時進行 resize()擴容
int threshold;

// 載入因子
final float loadFactor;



3.6 建構函式

public HashMap(int initialCapacity, float loadFactor) {
    
    // 小於0當然丟擲異常
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
    
    // 不能最大2^30
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    
    // 載入因子異常
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    
    // 建構函式只是賦值,沒有初始化表,懶載入
    this.loadFactor = loadFactor;
    
    // 前面的2次冪用處,這裡就設定了閥值,用於後面的擴容,其實就是賦值容量給閥值
    this.threshold = tableSizeFor(initialCapacity);
    
    // 沒有了那個LinkedHashMap的訪問順序初始化函式
}

// 給了初始化大小,載入因子
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}


// 就給了載入因子
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
}



3.7 簡單函式

public int size() {
    return size;
}

public boolean isEmpty() {
	return size == 0;
}

public boolean containsKey(Object key) {
    return getNode(hash(key), key) != null;
}



3.8 get方法

public V get(Object key) {
    Node<K,V> e;
    
    // 這裡返回節點的.value
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

// 這裡返回節點
final Node<K,V> getNode(int hash, Object key) {
    
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    
    // 桶不為空,桶大於0,獲取桶下標
    if((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null){
        
        // 總是先對比桶上第一個元素,地址相同,或者內容相同(為了省去equal而先對比地址?)
        if (first.hash == hash && ((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);
            
            // 這裡非紅黑樹就是連結串列了,遍歷咯,找到就返回,沒找到null
            do {
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}



3.9 put方法

public V put(K key, V value) {
    
    // 第三個引數:onlyIfAbsent,第四個:訪問順序用的
    return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    
    // tab存放表,p存放節點,n表示桶長,i表示雜湊值
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    
    // 空表時resize
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    
    // 對應桶上為空,直接放桶上
    // tab[i = (n - 1) & hash],相當於 hash % n
    // 但一般是取最大的素數來取模的
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    
    // 這裡就是發生雜湊衝突
    else {
        Node<K,V> e; K k;
        
        // 首先判斷是否第一個元素,是的話賦值給e,直接進入替換值的階段,跳過下面兩個步驟
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        
        // 進入紅黑樹處理階段
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        
        // 連結串列處理階段
        else {
            
            // 連結串列遍歷
            for (int binCount = 0; ; ++binCount) {
                
                // 遍歷到一個空的,那就尾部插入,這裡和1.7的頭插法不同
                // 一旦插入成功,那麼退出這個迴圈
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    
                    // 是先插入元素再轉換紅黑樹的
                    // 鏈長超過8,(那麼實際上已經有9個了)轉換紅黑樹,轉換過程跳過,後面單獨抽出來講
                    // 特別注意,如果陣列長度小於64,不會樹化,是直接擴容
                    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))))
                    break;
                
                // 節點移動,這裡差點看不懂了
                p = e;
            }
        }
        
        // existing mapping for key
        if (e != null) { 
            
            // 舊值
            V oldValue = e.value;
            
            // 新值替換舊值,返回舊值
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    
    // put新元素,除了替換屬於結構改動
    // 需要快速失敗
    ++modCount;
    
    // 桶長超過了 實際元素大小 * 載入因子
    // 再雜湊
    if (++size > threshold)
        resize();
    
    // LinkedHashMap使用的,這裡為空函式
    afterNodeInsertion(evict);
    
    // 原來沒有覆蓋舊值是返回null
    return null;
}
// 進行樹化
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    
    // 表空,陣列長度小於64進行擴容,不樹化
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    
    // 當前選擇的桶子不為空
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        
        // 遍歷連結串列,轉化成樹節點
        do {
            
            // 雙向連結串列prev,next,輔助性屬性
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        
        // 樹化,節點內部有樹化功能
        // 這個樹化是從頭節點開始,即第一個節點當成根節點,然後根節點還沒樹化的連結串列遍歷,一個個插入樹中
        // 這個過程穿插著平衡
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

TreeNode節點繼承了LinkedHashMap.Entry所以內部屬性好多

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    
    TreeNode<K,V> parent;  // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;

    // next是Node的屬性



3.10 擴容

  • 判斷是否初始化過了
    • 是:判斷是否達到最大容量
      • 設定變數的初值,但還沒進行擴容
    • 否:判斷是否設定了載入因子,設定載入因子
    • 否:呼叫預設構造,賦值都是預設的
final Node<K,V>[] resize() {
    
    // 獲取舊錶
    Node<K,V>[] oldTab = table;
    
    // 獲取舊錶容量
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    
    // 舊,新閥值
    int oldThr = threshold;
    int newCap, newThr = 0;
    
    // 舊容量大於0,表示初始化過了
    if (oldCap > 0) {
        
        //如果已經最大容量了,設定閥值整型最大,不必要進行再雜湊了
        // 返回舊錶?
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        
        // 擴容是2倍,和ArrayList的1.5別搞錯了
        // 閥值當然也變成2倍了
        else if((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >=DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    
    // 這裡和上面對比,說明空表,但舊閥值大於0,說明呼叫了非空建構函式
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    
    // 這裡是容量和閥值都為0,即呼叫預設建構函式的結果,即要初始化
    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;
}
// 紅黑樹轉移
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
    TreeNode<K,V> b = this;
    // Relink into lo and hi lists, preserving order
    TreeNode<K,V> loHead = null, loTail = null;
    TreeNode<K,V> hiHead = null, hiTail = null;
    int lc = 0, hc = 0;
    
    // 和連結串列差不多,都是遍歷,然後記錄拆分位置
    for (TreeNode<K,V> e = b, next; e != null; e = next) {
        next = (TreeNode<K,V>)e.next;
        e.next = null;
        
        // 分別組裝新舊位置的部分
        if ((e.hash & bit) == 0) {
            if ((e.prev = loTail) == null)
                loHead = e;
            else
                loTail.next = e;
            loTail = e;
            ++lc;
        }
        else {
            if ((e.prev = hiTail) == null)
                hiHead = e;
            else
                hiTail.next = e;
            hiTail = e;
            ++hc;
        }
    }

    // 然後將這兩部分轉移過去,如果數量不超過‘6’,那麼反樹化
    if (loHead != null) {
        if (lc <= UNTREEIFY_THRESHOLD)
            
            // 對這個組裝好的部分,進行反樹化
            tab[index] = loHead.untreeify(map);
        else {
            
            // 整個樹都移動過去
            tab[index] = loHead;
            
            // 如果另一個位置不為空,即有轉移到另一個位置的元素
            // 這樣的話,低位就要樹化;如果高位為空,整個樹都轉移過去了,不用再樹化,本來就是一個樹
            if (hiHead != null)
                loHead.treeify(tab);
        }
    }
    if (hiHead != null) {
        if (hc <= UNTREEIFY_THRESHOLD)
            tab[index + bit] = hiHead.untreeify(map);
        else {
            tab[index + bit] = hiHead;
            if (loHead != null)
                hiHead.treeify(tab);
        }
    }
}



3.11 ConcurrentHashMap

換成普通的HashMap了,但增加了頭部鎖。桶子上的節點用CAS操作完成,而連結串列或紅黑樹裡面的用Synchronized


3.11.1 原理

// 是普通的1.8雜湊表

// 不同在於,每次修改結構會鎖住 連結串列的頭,紅黑樹的根

// Synchronized(Node / Root)

3.11.2 put方法

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    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();
        
        // 第一個是否為空,Unsafe類直接獲取記憶體值
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            
            // no lock when adding to empty bin
            // CAS操作
            if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))
                break;                  
        }
        
        // MOVDE = -1,表示整個陣列在擴容
        else if ((fh = f.hash) == MOVED)
            // 表示當前執行緒也去幫忙擴容
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            
            // 居然用了synchronized,鎖住了頭節點,意外
            synchronized (f) {
                
                // 若加鎖時,另外執行緒刪除了這個節點,鎖沒了
                // 那麼會再次迴圈,然後獲取最新的頭節點
                if (tabAt(tab, i) == f) {
                    
                    // 遍歷連結串列
                    if (fh >= 0) {
                        binCount = 1;
                        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) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    
    // 這裡才是真正的擴容,上面的helpTransfer表示幫助擴容
    addCount(1L, binCount);
    return null;
}

3.11.3 init初始化

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        if ((sc = sizeCtl) < 0)
            // 有可能一直競爭到時間片??? 不太可能把
            Thread.yield(); // lost initialization race; just spin
        
        // CAS改變了sc轉檯
        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;
}

3.11.4 擴容

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();
        }
    }
}

3.11.5 普通方法

public int size() {
    long n = sumCount();
    return ((n < 0L) ? 0 :
            (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
            (int)n);
}
final long sumCount() {
    CounterCell[] as = counterCells; CounterCell a;
    long sum = baseCount;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}






4. 總結



4.1 兩個版本的區別

  • 1.8 新增了紅黑樹(logN)
  • 1.7先擴容再新增元素,但空桶不擴容、1.8先新增元素再擴容
  • 1.7和1.8的2次冪方法不同
  • 1.7擴容是一個一個轉、1.8是分成兩個部分組裝好了,最後才轉移過去
  • 1.7有單獨的null方法,而1.8沒有使用==判斷了
  • 1.8擴容,當陣列長度<64時,優先擴容,不先轉化紅黑樹


4.2 併發區別

  • 1.7 使用分段鎖,ReentranLock、CAS的使用,使用雙重陣列
  • 1.8 使用頭部鎖,Synchronized、CAS的使用,使用普通雜湊表


相關文章