真就爲了HashMap,千千萬萬遍???看完這篇博客,一遍就行!!

大家好,我是方圓
準備認認真真寫完這篇博客,就準備考試周了
希望大家過得快樂呀!


1. Collections

1.1 List

特點:

  1. 有序(指的是存取有序)
  2. 可重複
  3. 可通過索引值操作元素

分類:

  • 底層是數組,查詢快,增刪慢;(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

特點:

  1. 無序
  2. 元素唯一

分類

  • 底層是HashMap;(HashSet,保證元素的唯一性,利用的是hashCode()和equals()方法)

  • 底層是TreeMap;(TreeSet,保證元素的有序性

  • 進行的排序方式

  1. 對象所屬的類自己實現comparable接口,向TreeSet中添加元素的時候,會調用compareTo()方法比較
  2. 在創建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()方法源碼

  • 方法邏輯
  1. 如果HashMap未被初始化,則進行初始化
  2. 對Key求Hash值,然後再計算下標
  3. 如果沒有碰撞了,直接放入桶中
  4. 如果碰撞了,以鏈表的方式鏈接到後面
  5. 如果鏈表長度超過8,就把鏈表轉成紅黑樹
  6. 如果鏈表長度低於6,就把紅黑樹轉回鏈表
  7. 如果節點已經存在,就替換舊值
  8. 如果桶滿了(超過容量*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線程安全的方法

  1. 使用Collections.sychronizedMap(hashMap)方法
  2. HashTable是線程安全的,因爲它的public方法都被sychronized修飾
  3. 使用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 擴容

  • 步驟
  1. 構建一個nextTable,大小爲table的兩倍
  2. 把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,大體思想是遍歷、複製的過程。

  1. 首先根據運算得到需要遍歷的次數i,然後利用tabAt方法獲得i位置的元素f,初始化一個forwardNode實例fwd。

  2. 如果f == null,則在table中的i位置放入fwd,這個過程是採用Unsafe.compareAndSwapObjectf方法實現的,很巧妙的實現了節點的併發移動。

  3. 如果f是鏈表的頭節點,就構造一個反序鏈表,把他們分別放在nextTable的i和i+n的位置上,移動完成,採用Unsafe.putObjectVolatile方法給table原位置賦值fwd。

  4. 如果f是TreeBin節點,也做一個反序處理,並判斷是否需要untreeify,把處理的結果分別放在nextTable的i和i+n的位置上,移動完成,同樣採用Unsafe.putObjectVolatile方法給table原位置賦值fwd。

遍歷過所有的節點以後就完成了複製工作,把table指向nextTable,並更新sizeCtl爲新數組大小的0.75倍 ,擴容完成。

4. HashTable、HashMap、ConcurrentHashMap的區別

  1. HashMap線程不安全,底層是數組+鏈表+紅黑樹
  2. HashTable線程安全,它鎖住的是整個對象,底層是數組+鏈表
  3. ConcurrentHashMap線程安全,利用的是CAS+synchronized,底層是數組+鏈表+紅黑樹

參考

ConcurrentHashMap詳解


哇哇哇!寫完啦!爽到!

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