Java深刻研究HashMap實現原理

承接上篇《Java深刻研究Collection集合框架》文章中的HashMap、ConcurrentHashMap源碼分析,在Java中經常使用的四個實現Map接口的類,分別是HashMap、TreeMap、LinkedHashMap以及繼承自Dictionary抽象類的Hashtable,下面簡單概述下各實現類的特色 :node

HashMap

根據鍵的hashcode存儲數據,容許null鍵/值(null鍵只容許一條,value能夠有多條null),非synchronized、元素無序,順序也可能隨時改變,底層基於鏈表+紅黑樹實現【JDK1.8】算法

TreeMap

實現SortedMap接口,能夠根據鍵排序,默認按鍵值升序排序,也能夠指定排序的比較器,在使用時key必須實現Comparable接口,TreeMap在Iterator遍歷是排過序的數組

LinkedHashMap

屬於HashMap的一個子類,保存了記錄的插入順序,在用Iterator遍歷LinkedHashMap時,先獲得的記錄確定是先插入的,也能夠在構造時帶參數,按照訪問次序排序安全

Hashtable

經常使用功能跟HashMap相似,不支持null鍵/值,synchronized線程安全,Hashtable默認的初始大小爲11,以後每次擴充,容量變爲原來的2n+1.HashMap默認的初始化大小爲16.以後每次擴充,容量變爲原來的2倍,併發性不如ConcurrentHashMap,由於ConcurrentHashMap引入了分段鎖bash

HashMap重要的常量定義

DEFAULT_INITIAL_CAPACITY =16 默認容量
MAXIMUM_CAPACITY =1 << 30 最大容量
DEFAULT_LOAD_FACTOR = 0.75f 默認負載因子
TREEIFY_THRESHOLD=8 鏈表轉換紅黑樹的閥值
UNTREEIFY_THRESHOLD=6 紅黑樹轉換鏈表的閥值
MIN_TREEIFY_CAPACITY=64 桶中bin最小hash容量,若是大於這個值會進行resize擴容操做,
此值至少是TREEIFY_THRESHOLD的4倍
複製代碼

HashMap構造函數

首先看初始化容量、負載因子的有參函數源碼數據結構

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);
    }
複製代碼

常規的邊界判斷、賦值操做,經過tableSizeFor方法計算初始容量多線程

HashMap put方法源碼分析

方法調用
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    傳入key的hash計算
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    實際調用方法
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        //局部node節點tab
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //將初始化的table賦值給tab並判null,若是爲空則進行tab初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //根據hash計算tab[i]位置,判斷若是爲空則調用newNode()存儲新的node<K,V>中
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            //根據hash值和equals判斷key,若是key相同就把老的node賦值給變量e
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //key不一樣,判斷是否時紅黑樹,若是是則調用putTreeVal()放在樹中
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                //循環鏈表
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        //沒有下一個元素,則把當前元素傳入newNode()做爲下一個元素
                        p.next = newNode(hash, key, value, null);
                        //鏈表長度超過閾值TREEIFY_THRESHOLD=8
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);//轉換成紅黑樹
                        break;
                    }
                    //判斷key相同則賦值替換
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //判斷value是否替換
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)//判斷擴容閾值
            resize();//擴容方法
        afterNodeInsertion(evict);
        return null;
    }
複製代碼
  • resize實現 當put時,若是bucke佔用程度已經超過了DEFAULT_LOAD_FACTOR參數初始比例,就把bucket擴充爲2倍,以後從新計算index,再把節點放到新的bucket中,源代碼說明以下

Initializes or doubles table size. If null, allocates in accord with initial capacity target held in field threshold. Otherwise, because we are using power-of-two expansion, the elements from each bin must either stay at same index, or move with a power of two offset in the new table併發

實現方法在【JDK1.7】和【JDK1.8】中有差別(1.8引入紅黑樹),感興趣能夠研究JDK源碼對reszie()的實現
複製代碼

HashMap get方法源碼分析

public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    //hash值同put操做
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        //判斷tab節點是否爲空,根據hash算出下標
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //在第一個node中查找
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            //若是有下一個元素
            if ((e = first.next) != null) {
                //若是是樹,調用getTreeNode()在紅黑樹中查找
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    //循環鏈表
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }
複製代碼

HashMap put、get方法思路總結

put方法大體思路

  1. 根據鍵值key作hash計算,獲得插入數組下標,若是tab[i]爲null,直接newNode插入新節點
  2. 若是tab[i]不爲null,判斷tab[i]首個元素和key是否相同,相同就把老元素賦值給局部變量
  3. 若是比較的key不一樣,判斷是否爲TreeNode(紅黑樹),若是是,調用putTreeVal()插入樹中
  4. 若是不是TreeNode,對鏈表作遍歷,鏈表長度超過閾值TREEIFY_THRESHOLD=8轉換成紅黑樹

get方法大體思路

  1. 判斷tab節點是否爲空,根據hash算出下標
  2. 在第一個node節點中查找,若是有直接return first
  3. 若是沒有,判斷是否有下一個元素,若是有判斷是否爲TreeNode,若是是則在樹中查找
  4. 若是不是,循環鏈表查找

equals()和hashCode()的做用

經過對key的hashCode()進行hashing,並計算下標( n-1 & hash),從而得到buckets的位置。若是比較的key相同,則利用key.equals()方法去鏈表或樹中去查找對應的節點app


ConcurrentHashMap實現原理

ConcurrentHashMap在Java 8中取消了Segment分段鎖的數據結構,採用數組+鏈表+紅黑樹的數據結構,而對於鎖的粒度,調整爲對每一個數組元素加鎖(Node節點),簡化定位節點的hash算法,這樣帶來的弊端是hash碰撞會增大,所以在鏈表節點數量大於8時,會將鏈表轉化爲紅黑樹進行存儲。這樣一來,查詢的時間複雜度就會由原先的O(n)變爲O(logN)框架

關於CAS算法

CAS的全稱叫"Compare And Swap",也就是比較並交換,使用時主要涉及到三個操做數,內存值V預期值A新值B,若是在執行時發現內存值V預期值A相匹配,那麼他會將內存值V更新爲新值B,相反處理器就不會執行任何操做

核心屬性

//用於table[]的初始化和擴容操做,-1表示正在初始化,-N表示有N個線程正在擴容,非負數時,表示初始化table[]的大小,已經初始化則表示擴容閾值,默認爲table[]容量的0.75倍
    private transient volatile int sizeCtl;
    //表示默認的併發級別,也就是table[]的默認大小
    private static finalint DEFAULT_CONCURRENCY_LEVEL = 16;
    //默認的負載因子
    private static final float LOAD_FACTOR = 0.75f;
    //鏈表轉紅黑樹的閥值
    static final int TREEIFY_THRESHOLD = 8;
    //紅黑樹轉鏈表的閥值,
    static final int UNTREEIFY_THRESHOLD = 6;
    //哈希表的最小樹形化容量
    static final int MIN_TREEIFY_CAPACITY = 64;
複製代碼

構造函數

public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        if (initialCapacity < concurrencyLevel)   // Use at least as many bins
            initialCapacity = concurrencyLevel;   // as estimated threads
        long size = (long)(1.0 + (long)initialCapacity / loadFactor);
        int cap = (size >= (long)MAXIMUM_CAPACITY) ?
            MAXIMUM_CAPACITY : tableSizeFor((int)size);
        this.sizeCtl = cap;

        //主要是初始化map容量size、concurrencyLevel併發級別
    }
複製代碼

put操做

//常規put入口
    public V put(K key, V value) {
        return putVal(key, value, false);
    }

    final V putVal(K key, V value, boolean onlyIfAbsent) {
        //不容許空鍵空值
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());//計算key hash
        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();//常規初始化tab[]
            //根據hash值與運算確認下標並將節點賦值給f,而後判null
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                //若是爲空,採用CAS算法將新值插入Node節點
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break; // no lock when adding to empty bin
            }
            //hash值==-1,說明正在擴容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);//擴容後返回最新tab[]
            else { 
                V oldVal = null;
                synchronized (f) {//獲取數組同步鎖,
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {//hash大於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;//key相同,進行value替換,退出循環
                                }
                                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;
                            }
                        }
                        else if (f instanceof ReservationNode)//空節點,佔位符
                            throw new IllegalStateException("Recursive update");
                    }
                }
                if (binCount != 0) {
                    //鏈表轉換紅黑樹閾值判斷
                    if (binCount >= TREEIFY_THRESHOLD)
                        //與HashMap類中轉換紅黑樹有區別,當hash表長度小於MIN_TREEIFY_CAPACITY屬性值時嘗試擴容操做,相反進行樹形化
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }
複製代碼

ConcurrentHashMap的put()操做大體流程

  • 初始化map容量size、concurrencyLevel併發級別
  • 對鍵、值非null判斷,計算hash值,判斷table[]是否建立,沒有就初始化
  • 若是table[i]爲null,採用CAS算法將新值插入Node節點
  • 若是不爲null,判斷hash值是否爲-1,若是是則調用helpTransfer()擴容
  • 若是hash值不爲-1,就在鏈表尾部或者和紅黑樹中插入節點
  • 最後對鏈表轉紅黑樹閾值作判斷,當hash表長度小於MIN_TREEIFY_CAPACITY屬性值時嘗試擴容操做,相反進行樹形化

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());//計算key hash
        //判斷table[]是否爲null,根據下標確認table[i]節點並作非null約束
        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;
            }
            //若是頭結點的 hash 小於 0,說明正在擴容,或者該位置是紅黑樹
            else if (eh < 0)
                //e.find可對比查看ForwardingNode類的find()、TreeBin類的find()源碼
                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;
    }
複製代碼

ConcurrentHashMap的get()操做大體流程(不加鎖)

  • 計算key的hash值,判斷table[]是否爲null,同時根據下標判斷table[i]是否爲null
  • 若是table[i]則比較鏈表頭部元素是否相同,若是是直接返回該鍵位置所對應的值
  • 若是hash不相同,判斷是不是紅黑樹或是正在擴容操做,若是是則在樹中查找
  • 若是不是紅黑樹或是正在擴容操做,則遍歷鏈表查找

Java8對ConcurrentHashMap實現改進

  • 不採用segment而採用node,鎖住node來實現減少鎖粒度,加入紅黑樹機制
  • 換回Synchronized關鍵字,替換ReentrantLock分段鎖
  • 設計了MOVED狀態,容許多線程進行幫助擴容操做
  • 使用CAS操做來確保node的一些操做的原子性,這種方式代替了鎖
  • 採用sizeCtl的不一樣值來表明不一樣含義,起到了控制的做用

以上涉及JDK源碼部分均來自 JDK 1.8

個人我的新球

加入星球一塊兒討論項目、研究新技術,共同成長!

相關文章
相關標籤/搜索