深入解析HashMap和currentHashMap源碼以及實現原理

深入解析HashMap和ConcurrentHashMapy源碼以及底層原理

前言

HashMap 和ConcurrentHashMap,這兩個相信大家都不陌生,在面試中基本上是必問的,以及在實際開發過程中也是比用的,那麼看了這篇文章,無論在面試還是在實際開發中都可以順手拈來,得心應手了。

HashMap

基於Map接口實現,元素以鍵值對的方式存儲,並且允許使用null 建和null 值, 因爲key不允許重複,因此只能有一個鍵爲null,另外HashMap不能保證放入元素的順序,它是無序的,和放入的順序並不能相同。HashMap是線程不安全的。

下面來細說下jdk1.7和1.8的實現

JDK1.7版本

數據結構圖

在這裏插入圖片描述

底層原理

HashMap採用位桶+鏈表實現,即使用鏈表處理衝突,同一hash值的鏈表都存儲在一個鏈表裏。但是當位於一個桶中的元素較多,即hash值相等的元素較多時,通過key值依次查找的效率較低。

比較核心的成員變量

//初始化桶大小,因爲底層是數組,所以這是數組默認的大小 16。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//桶最大值
static final int MAXIMUM_CAPACITY = 1 << 30;
//默認的負載因子(0.75)
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//table 真正存放數據的數組
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
//Map 存放數量的大小
transient int size;
// (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
int threshold;
//負載因子,可在初始化時顯式指定。
final float loadFactor;
//記錄HashMap的修改次數
transient int modCount;

負載因子

在初始化的時候給定的默認的容量是16,負載因子是0.75,如下代碼是初始化過程

    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

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

Map 在使用過程中不斷的往裏面存放數據,當數量達到了 16 * 0.75 = 12 就需要將當前 16 的容量進行擴容,而擴容這個過程涉及到 rehash、複製數據等操作,所以非常消耗性能。所依建議能提前預估 HashMap 的大小最好,儘量的減少擴容帶來的性能損耗。

爲什麼負載因子設置的是0.75?

如果設置成1。空間利用更充分,但是這樣會發生大量的hash碰撞。有些位置的鏈表會很長,就不利於查詢。省空間而費時間。
如果設置成0.5,hash碰撞的機率小了很多,但是會頻繁擴容,費空間而省時間。

 /* <p>As a general rule, the default load factor (.75) offers a good
 * tradeoff between time and space costs.  Higher values decrease the
 * space overhead but increase the lookup cost (reflected in most of
 * the operations of the <tt>HashMap</tt> class, including
 * <tt>get</tt> and <tt>put</tt>).  The expected number of entries in
 * the map and its load factor should be taken into account when
 * setting its initial capacity, so as to minimize the number of
 * rehash operations.  If the initial capacity is greater than the
 * maximum number of entries divided by the load factor, no rehash
 * operations will ever occur.*/

看這段源碼大致的意思是0.75的時候,空間利用率比較高,而且避免了相當多的Hash衝突,使得底層的鏈表或者是紅黑樹的高度比較低,提升了空間效率。

modCount

用於記錄HashMap的修改次數, 在HashMap的put(),remove(),Interator()等方法中,使用了該屬性

由於HashMap不是線程安全的,所以在迭代的時候,會將modCount賦值到迭代器的expectedModCount屬性中,然後進行迭代, 如果在迭代的過程中HashMap被其他線程修改了,modCount的數值就會發生變化, 這個時候expectedModCount和ModCount不相等, 迭代器就會拋出ConcurrentModificationException()異常

具體代碼

    final Entry<K,V> nextEntry() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        Entry<K,V> e = next;
        if (e == null)
            throw new NoSuchElementException();

        if ((next = e.next) == null) {
            Entry[] t = table;
            while (index < t.length && (next = t[index++]) == null)
                ;
        }
        current = e;
        return e;
    }

真正存放數據的數組

transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

對於Entry我們來看源碼

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;


		 Entry(int h, K k, V v, Entry<K,V> n) {
			value = v;
			next = n;
			key = k;
			hash = h;
			}
		...
}

key寫入時的key

value寫入時的value

next用於實現鏈表結構

hash 存放的是當前 key 的 hashcode

存儲機制

put方法

    public V put(K key, V value) {
    	//判斷當前數組是否需要初始化
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        //key 爲空,則 put 一個空值進去
        if (key == null)
            return putForNullKey(value);
         //key 計算出 hashcode
        int hash = hash(key);
        //計算出的 hashcode 定位出所在桶
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //如果桶是一個鏈表則需要遍歷判斷裏面的 hashcode、key 是否和傳入 key 相等,如果相等則進行覆				//蓋,並返回原來的值
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
		
        modCount++;
        //如果桶是空的,說明當前位置沒有數據存入;新增一個 Entry 對象寫入當前位置。
        addEntry(hash, key, value, i);
        return null;
    }

addEntry(hash, key, value, i)

 void addEntry(int hash, K key, V value, int bucketIndex) {
 		//判斷是否需要擴容
        if ((size >= threshold) && (null != table[bucketIndex])) {
        	//進行兩倍擴充,並將當前的 key 重新 hash 並定位
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
		//新建一個entry
        createEntry(hash, key, value, bucketIndex);
    }
    
    void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

get 方法

    public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }
    
    final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }
		
		//根據 key 計算出 hashcode,然後定位到具體的桶中
        int hash = (key == null) ? 0 : hash(key);
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            //不是鏈表就根據 key、key 的 hashcode 是否相等來返回值
            //鏈表則需要遍歷直到 key 及 hashcode 相等時候就返回值
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

JDK1.8版本

對於1.7版本的hashmap,Hash 衝突嚴重時,在桶上形成的鏈表會變的越來越長,這樣在查詢時的效率就會越來越低;時間複雜度爲 O(N)

數據結構圖

在這裏插入圖片描述

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-VW9UgQdt-1589040683846)(E:\技術帖子\筆記\基礎\圖\hashmap\hashmap1.8.png)]

底層原理

HashMap採用位桶+鏈表+紅黑樹實現,當鏈表長度超過閾值(8)時,將鏈表轉換爲紅黑樹,這樣大大減少了查找時間。查詢效率直接提高到了 O(logn)

比較核心的成員變量

public class HashMap<k,v> extends AbstractMap<k,v> implements Map<k,v>, Cloneable, Serializable {
    private static final long serialVersionUID = 362498820763181265L;
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    static final int MAXIMUM_CAPACITY = 1 << 30;//最大容量
    static final float DEFAULT_LOAD_FACTOR = 0.75f;//填充比
    //當add一個元素到某個位桶,其鏈表長度達到8時將鏈表轉換爲紅黑樹
    static final int TREEIFY_THRESHOLD = 8;
    static final int UNTREEIFY_THRESHOLD = 6;
    static final int MIN_TREEIFY_CAPACITY = 64;
    transient Node<k,v>[] table;//存儲元素的數組
    transient Set<map.entry<k,v>> entrySet;
    transient int size;//存放元素的個數
    transient int modCount;//被修改的次數fast-fail機制
    int threshold;//臨界值 當實際大小(容量*填充比)超過臨界值時,會進行擴容 
    final float loadFactor;//填充比(......後面略)

node 的方法和1.7的entery的結構一樣

TreeNode

//紅黑樹
static final class TreeNode<k,v> extends LinkedHashMap.Entry<k,v> {
    TreeNode<k,v> parent;  // 父節點
    TreeNode<k,v> left; //左子樹
    TreeNode<k,v> right;//右子樹
    TreeNode<k,v> prev;    // needed to unlink next upon deletion
    boolean red;    //顏色屬性
    TreeNode(int hash, K key, V val, Node<k,v> next) {
        super(hash, key, val, next);
    }

//返回當前節點的根節點
final TreeNode<k,v> root() {
    for (TreeNode<k,v> r = this, p;;) {
        if ((p = r.parent) == null)
            return r;
        r = p;
    }
}

存取機制

put 方法

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //判斷當前桶是否爲空,空的就需要初始化(resize 中會判斷是否進行初始化)
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // key 的 hashcode 定位到具體的桶中並判斷是否爲空,爲空表明沒有 Hash 衝突就直接在當前位置創建一個新桶即可
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {//表示示有衝突,開始處理衝突
        Node<K,V> e; K k;
        //如果當前桶有值( Hash 衝突),那麼就要比較當前桶中的 key、key 的 hashcode 與寫入的 key 是否相等,相等就賦值給 e,檢查第一個Node,p是不是要找的值
        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) {
            //如果是個鏈表,就需要將當前的 key、value 封裝成一個新節點寫入到當前桶的後面(形成鏈表)
                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;
                }
                //在遍歷過程中找到 key 相同時直接退出遍歷
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        //如果 e != null 就相當於存在相同的 key,那就需要將值覆蓋
        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()

擴容機制,構造hash表時,如果不指明初始大小,默認大小爲16(即Node數組大小16),如果Node[]數組中的元素達到(填充比*Node.length)重新調整HashMap大小 變爲原來2倍大小,擴容很耗時

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;
        }
/*把新表的長度設置爲舊錶長度的兩倍,newCap=2*oldCap*/
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
      /*把新表的門限設置爲舊錶門限的兩倍,newThr=oldThr*2*/
            newThr = oldThr << 1; // double threshold
    }
 /*如果舊錶的長度的是0,就是說第一次初始化表*/
    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;//把新表賦值給table
    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)//說明這個node沒有鏈表直接放在新表的e.hash & (newCap - 1)位置
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
/*如果e後邊有鏈表,到這裏表示e後面帶着個單鏈表,需要遍歷單鏈表,將每個結點重*/
                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;//記錄下一個結點
		  //新表是舊錶的兩倍容量,實例上就把單鏈表拆分爲兩隊,

              //e.hash&oldCap爲偶數一隊,e.hash&oldCap爲奇數一對
                            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) {//lo隊不爲null,放在新表原位置
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {//hi隊不爲null,放在新表j+oldCap位置
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

treeifyBin(tab, hash)

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    //判斷當前hashMap的長度,如果不足64,只進行resize(),擴容table
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
    	//如果達到64,那麼將衝突的存儲結構爲紅黑樹
        TreeNode<K,V> hd = null, tl = null;
        do {
            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);
    }
}

put過程梳理:

1,判斷鍵值對數組tab[]是否爲空或爲null,否則以默認大小resize();
2,根據鍵值key計算hash值得到插入的數組索引i,如果tab[i]==null,直接新建節點添加,否則轉入3
3,判斷當前數組中處理hash衝突的方式爲鏈表還是紅黑樹(check第一個節點類型即可),分別處理

get 方法

 public V get(Object key) {
        Node<K,V> e;
        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;
        //將 key hash 之後取得所定位的桶,如果桶爲空則直接返回 null 
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //判斷桶的第一個位置(有可能是鏈表、紅黑樹)的 key 是否爲查詢的 key,是就直接返回 value
            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)
                    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;
    }

存在問題

在併發情況下會出現死循環,代碼如下

final HashMap<String, String> map = new HashMap<String, String>();
for (int i = 0; i < 1000; i++) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            map.put(UUID.randomUUID().toString(), "");
        }
    }).start();
}

在擴容的時候會調用 resize() 方法,就是這裏的併發操作容易在一個桶上形成環形鏈表;這樣當獲取一個不存在的 key 時,計算出的 index 正好是環形鏈表的下標就會出現死循環。

ConcurrentHashMap

在包java.util.concurrent下,和HashMap不同的是專門處理併發問題

jdk1.7版本

數據結構圖

在這裏插入圖片描述

如圖所示,是由 Segment 數組、HashEntry 組成,和 HashMap 一樣,仍然是數組加鏈表。

底層原理

ConcurrentHashMap 採用了分段鎖技術,其中 Segment 繼承於 ReentrantLock。不會像 HashTable 那樣不管是 put 還是 get 操作都需要做同步處理,理論上 ConcurrentHashMap 支持 CurrencyLevel (Segment 數組數量)的線程併發。每當一個線程佔用鎖訪問一個 Segment 時,不會影響到其他的 Segment。

核心的成員變量

    /**
     * Segment 數組,存放數據時首先需要定位到具體的 Segment 中。
     */
    final Segment<K,V>[] segments;

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

segments

    static final class Segment<K,V> extends ReentrantLock implements Serializable {

        private static final long serialVersionUID = 2249069246763182397L;
        
        // 和 HashMap 中的 HashEntry 作用一樣,真正存放數據的桶
        transient volatile HashEntry<K,V>[] table;

        transient int count;

        transient int modCount;

        transient int threshold;

        final float loadFactor;
        
    }

HashEntry

value 和next都保持着線程可見性

static final class HashEntry<K,V> {
        final int hash;
        final K key;
        volatile V value;
        volatile HashEntry<K,V> next;

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

存取機制

put 方法

public V put(K key, V value) {
        Segment<K,V> s;
        if (value == null)
            throw new NullPointerException();
        int hash = hash(key);
        int j = (hash >>> segmentShift) & segmentMask;
        //通過key定位到segment
        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            s = ensureSegment(j);
        return s.put(key, hash, value, false);
    }

s.put(key, hash, value, false)

        final V put(K key, int hash, V value, boolean onlyIfAbsent) {
        //將當前 Segment 中的 table 通過 key 的 hashcode 定位到 HashEntry
            HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);
            V oldValue;
            try {
                HashEntry<K,V>[] tab = table;
                int index = (tab.length - 1) & hash;
                HashEntry<K,V> first = entryAt(tab, index);
                for (HashEntry<K,V> e = first;;) {
                    if (e != null) {
                        K k;
                        //遍歷該 HashEntry,如果不爲空則判斷傳入的 key 和當前遍歷的 key 是否相等,相等則覆蓋舊的 value
                        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 {
                    //爲空則需要新建一個 HashEntry 並加入到 Segment 中,同時會先判斷是否需要擴容
                        if (node != null)
                            node.setNext(first);
                        else
                            node = new HashEntry<K,V>(hash, key, value, first);
                        int c = count + 1;
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                            rehash(node);
                        else
                            setEntryAt(tab, index, node);
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
            //解除在 scanAndLockForPut(key, hash, value) 中所獲取當前 Segment 的鎖
                unlock();
            }
            return oldValue;
        }

雖然 HashEntry 中的 value 是用 volatile 關鍵詞修飾的,但是並不能保證併發的原子性,所以 put 操作時仍然需要加鎖處理。用的是scanAndLockForPut(key, hash, value) 自旋鎖獲取

scanAndLockForPut(key, hash, value)

private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
            HashEntry<K,V> first = entryForHash(this, hash);
            HashEntry<K,V> e = first;
            HashEntry<K,V> node = null;
            int retries = -1; // negative while locating node
            //自旋獲取鎖
            while (!tryLock()) {
                HashEntry<K,V> f; // to recheck first below
                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;
                }
                //如果重試的次數達到了 MAX_SCAN_RETRIES 則改爲阻塞鎖獲取,保證能獲取成功
                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;
        }

MAX_SCAN_RETRIES

static final int MAX_SCAN_RETRIES =
            Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;

get 方法

將 Key 通過 Hash 之後定位到具體的 Segment ,再通過一次 Hash 定位到具體的元素上,整個過程不需要加鎖,

由於 HashEntry 中的 value 屬性是用 volatile 關鍵詞修飾的,保證了內存可見性,所以每次獲取時都是最新值。

    public V get(Object key) {
        Segment<K,V> s; // manually integrate access methods to reduce overhead
        HashEntry<K,V>[] tab;
        int h = hash(key);
        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
        if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
            (tab = s.table) != null) {
            for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
                     (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
                 e != null; e = e.next) {
                K k;
                if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                    return e.value;
            }
        }
        return null;
    }

jdk1.8版本

在1.7基礎上解決查詢遍歷鏈表效率太低問題

數據結構圖

在這裏插入圖片描述
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-GpSNZWsS-1589040683854)(E:\技術帖子\筆記\基礎\圖\hashmap\ConcurrentHashMap1.8.png)]

底層原理

和1.8的hashMap比較去除了原有的 Segment 分段鎖,而採用了 CAS + synchronized 來保證併發安全性。

而且將HashEntry 改成Node,功能上是一樣的

Node

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

   ...
}

存儲機制

put 方法

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    //根據key計算出hashcode
    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();
         //f 即爲當前 key 定位出的 Node,如果爲空表示當前位置可以寫入數據,利用 CAS 嘗試寫入,失敗則自旋保證成功。
        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
        }
        //當前位置的 hashcode == MOVED == -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) {
            	//數量大於 TREEIFY_THRESHOLD 則要轉換爲紅黑樹
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

get 方法

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    //計算出hashcode
    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;
}

如果大家對java架構相關感興趣,可以關注下面公衆號,會持續更新java基礎面試題, netty, spring boot,spring cloud等系列文章,一系列乾貨隨時送達, 超神之路從此展開, BTAJ不再是夢想!

架構殿堂

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