(三):集合框架Map

1.對比 Hashtable、HashMap、TreeMap 有什么不同?

Hashtable 是早期 Java 類庫提供的一個哈希表實現(xiàn),本身是同步的,不支持 null 鍵和值,由于同步導致的性能開銷,所以已經(jīng)很少被推薦使用。
HashMap 是應用更加廣泛的哈希表實現(xiàn),行為上大致上與 HashTable 一致,主要區(qū)別在于 HashMap 不是同步的,支持 null 鍵和值等。通常情況下,HashMap 進行 put 或者 get 操作,可以達到常數(shù)時間的性能,所以它是絕大部分利用鍵值對存取場景的首選,比如,實現(xiàn)一個用戶 ID 和用戶信息對應的運行時存儲結(jié)構(gòu)。
TreeMap 則是基于紅黑樹的一種提供順序訪問的 Map,和 HashMap 不同,它的 get、put、remove 之類操作都是 O(log(n))的時間復雜度,具體順序可以由指定的 Comparator 來決定,或者根據(jù)鍵的自然順序來判斷。

2.介紹下對象的 hashCode()和equals(),使用場景

hashcode

頂級類Object里面的方法,所有的類都是繼承Object,返回是?個int類型的數(shù),根據(jù)?定的hash規(guī)則(存儲地址,字段,長度等),映射成?個數(shù)組,即散列值。

equals

頂級類Object里面的方法,所有的類都是繼承Object,返回是?個boolean類型,根據(jù)自定義的匹配規(guī)則,用于匹配兩個對象是否?樣,一般邏輯如下:
1.判斷地址是否?樣
2.非空判斷和Class類型判斷
3.強轉(zhuǎn)
4.對象里面的字段一一匹配
hashCode 和 equals 的一些基本約定,比如:
1.equals 相等,hashCode 一定要相等。
2.重寫了 hashCode 也要重寫 equals。
3.hashCode 需要保持一致性,狀態(tài)改變返回的哈希值仍然要一致。
4.equals 的對稱、反射、傳遞等特性。

3.HashMap和TreeMap應該怎么選擇,使用場景

hashMap: 散列桶(數(shù)組+鏈表),可以實現(xiàn)快速的存儲和檢索,但是確實包含無序的元素,適用于在map中插入刪除和定位元素。
treeMap:使用存儲結(jié)構(gòu)是?個平衡?叉樹->紅?樹,可以?定義排序規(guī)則,要實現(xiàn)Comparator接口,
能便捷的實現(xiàn)內(nèi)部元素的各種排序,但是?般性能比HashMap差,適用于按照自然排序或者自定義排
序規(guī)則。

4.Set和Map的關(guān)系

Set的核心就是不保存重復的元素,存儲?組唯?的對象。
set的每?種實現(xiàn)都是對應Map里面的一種封裝。
HashSet對應的就是HashMap,treeSet對應的就是treeMap。
例如:HashSet無參構(gòu)造方法,其實就是初始化了一個HashMap:

public HashSet() {
        map = new HashMap<>();
    }

5.LinkedHashMap 和 TreeMap

雖然 LinkedHashMap 和 TreeMap 都可以保證某種順序,但二者還是非常不同的。
LinkedHashMap 通常提供的是遍歷順序符合插入順序,它的實現(xiàn)是通過為條目(鍵值對)維護一個雙向鏈表。注意,通過特定構(gòu)造函數(shù),我們可以創(chuàng)建反映訪問順序的實例,所謂的 put、get、compute 等,都算作“訪問”。
對于 TreeMap,它的整體順序是由鍵的順序關(guān)系決定的,通過 Comparator 或 Comparable(自然順序)來決定。

6.如果需要線程安全,且效率高的Map,應該怎么做?

多線程環(huán)境下可以用concurrent包下的ConcurrentHashMap, 或者使用Collections.synchronizedMap(),
ConcurrentHashMap雖然是線程安全,但是他的效率比Hashtable要高很多。

        ConcurrentHashMap<String, String> concurrentHashMap = new ConcurrentHashMap<>();
        Map<Object, Object> map = Collections.synchronizedMap(new HashMap<>());

7.介紹下你了解的HashMap

HashMap 內(nèi)部的結(jié)構(gòu),它可以看作是數(shù)組(Node[] table)和鏈表結(jié)合組成的復合結(jié)構(gòu),數(shù)組被分為一個個桶(bucket),通過哈希值決定了鍵值對在這個數(shù)組的尋址;哈希值相同的鍵值對,則以鏈表形式存儲,你可以參考下面的示意圖。這里需要注意的是,如果鏈表大小超過閾值(TREEIFY_THRESHOLD, 8),圖中的鏈表就會被改造為樹形結(jié)構(gòu)。


HashMap.jpg

從非拷貝構(gòu)造函數(shù)的實現(xiàn)來看,這個表格(數(shù)組)似乎并沒有在最初就初始化好,僅僅設置了loadFactor和initialCapacity。

/**
     * The next size value at which to resize (capacity * load factor).
     *閾值:下一次擴容時候的值(容量*加載因子)
     * @serial
     */
    // (The javadoc description is true upon serialization.
    // Additionally, if the table array has not been allocated, this
    // field holds the initial array capacity, or zero signifying
    // DEFAULT_INITIAL_CAPACITY.)
    int threshold;

    /**
     * The load factor for the hash table.
     *
     * @serial
     */
    final float loadFactor;
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);
    }

put()方法

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    /**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @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) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //如果當前Node數(shù)組為空
        if ((tab = table) == null || (n = tab.length) == 0)
            //調(diào)用resize()方法,初始化table,并設置n=DEFAULT_INITIAL_CAPACITY,n=16
            n = (tab = resize()).length;
        //如果要放入鍵值對在哈希表中的位置,沒有節(jié)點。
        if ((p = tab[i = (n - 1) & hash]) == null)
            //創(chuàng)建新節(jié)點
            tab[i] = newNode(hash, key, value, null);
        //要放入鍵值對在哈希表中的位置,有節(jié)點。
        else {
            Node<K,V> e; K k;
            //如果要放入鍵的hash等于首節(jié)點hash,且,key相等,則覆蓋掉原來的value
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //如果該Node是一個紅黑樹
            else if (p instanceof TreeNode)
                //執(zhí)行紅黑樹插入邏輯
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //如果該Node是一個鏈表
            else {
                //遍歷當前鏈表
                for (int binCount = 0; ; ++binCount) {
                    //如果當前節(jié)點是尾節(jié)點
                    if ((e = p.next) == null) {
                        //插入鏈表
                        p.next = newNode(hash, key, value, null);
                        //如果當前鏈表索引>=樹化閾值-1,即(8-1)
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            //樹化當前鏈表
                            treeifyBin(tab, hash);
                        break;
                    }
                    //如果要放入元素的鍵,與當前鍵相等,跳出循環(huán)。
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    //將e指向P,相當于循環(huán)里面的第二個語句
                    p = e;
                }
            }
            //如果key已經(jīng)存在了,覆蓋舊的value。
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                //空實現(xiàn)
                afterNodeAccess(e);
                //返回舊的value
                return oldValue;
            }
        }
        //判斷是否有并發(fā)插入的變量。
        ++modCount;
        //如果當前數(shù)組長度+1>閾值(數(shù)組長度*加載因子)
        if (++size > threshold)
            //擴容
            resize();
        afterNodeInsertion(evict);
        //返回空值
        return null;
    }
put流程圖.jpg

get方法:

public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

    /**
     * Implements Map.get and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @return the node, or null if none
     */
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        //如果key對應的Node不為空,且頭節(jié)點不為空
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //如果頭節(jié)點的hash等于hash,且頭節(jié)點的key等于key
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            //如果頭節(jié)點的下一個節(jié)點不為空
            if ((e = first.next) != null) {
                如果頭節(jié)點是樹
                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;
    }

8.ConcurrentHashMap 如何實現(xiàn)高效地線程安全?

早期 ConcurrentHashMap,其實現(xiàn)是基于:
分離鎖,也就是將內(nèi)部進行分段(Segment),里面則是 HashEntry 的數(shù)組,和 HashMap 類似,哈希相同的條目也是以鏈表形式存放。
HashEntry 內(nèi)部使用 volatile 的 value 字段來保證可見性,也利用了不可變對象的機制以改進利用 Unsafe 提供的底層能力,比如 volatile access,去直接完成部分操作,以最優(yōu)化性能,畢竟 Unsafe 中的很多操作都是 JVM intrinsic 優(yōu)化過的。
可以參考下面這個早期 ConcurrentHashMap 內(nèi)部結(jié)構(gòu)的示意圖,其核心是利用分段設計,在進行并發(fā)操作的時候,只需要鎖定相應段,這樣就有效避免了類似 Hashtable 整體同步的問題,大大提高了性能。


早期ConcurrentHashMap.jpg

在 Java 8 和之后的版本中,ConcurrentHashMap 發(fā)生了哪些變化呢?
總體結(jié)構(gòu)上,它的內(nèi)部存儲變得 HashMap 結(jié)構(gòu)非常相似,同樣是大的桶(bucket)數(shù)組,然后內(nèi)部也是一個個所謂的鏈表結(jié)構(gòu)(bin),同步的粒度要更細致一些。
其內(nèi)部仍然有 Segment 定義,但僅僅是為了保證序列化時的兼容性而已,不再有任何結(jié)構(gòu)上的用處。因為不再使用 Segment,初始化操作大大簡化,修改為 lazy-load 形式,這樣可以有效避免初始開銷,解決了老版本很多人抱怨的這一點。
數(shù)據(jù)存儲利用 volatile 來保證可見性。
使用 CAS 等操作,在特定場景進行無鎖并發(fā)操作。
使用 Unsafe、LongAdder 之類底層手段,進行極端情況的優(yōu)化。
put方法

public V put(K key, V value) {
        return putVal(key, value, false);
    }

    /** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        //對key進行重哈希,減少哈希碰撞概率
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            //如果當前Node[]為空
            if (tab == null || (n = tab.length) == 0)
                //初始化Node[]
                tab = initTable();
            //如果key的hash,對應的數(shù)組元素為空
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                //利用CAS進行無鎖線程安全操作,
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            //如果是這個狀態(tài)則是擴容操作,先進行擴容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            //存在哈希沖突,利用synchronized (f) 加鎖保證線程安全
            else {
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        //如果當前節(jié)點是鏈表,則遍歷插入。
                        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;
                                }
                            }
                        }
                        // 如果是紅黑樹則按照紅黑樹規(guī)則插入
                        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;
                }
            }
        }
        //最后檢查是否需要擴容
        addCount(1L, binCount);
        return null;
    }

JDK8之前,ConcurrentHashMap使?鎖分段技術(shù),將數(shù)據(jù)分成?段段存儲,每個數(shù)據(jù)段配置?把
鎖,即segment類,這個類繼承ReentrantLock來保證線程安全
JKD8的版本取消Segment這個分段鎖數(shù)據(jù)結(jié)構(gòu),底層也是使用Node數(shù)組+鏈表+紅黑樹,從而實現(xiàn)對
每?段數(shù)據(jù)進行加鎖,也減少了并發(fā)沖突的概率,CAS(讀)+Synchronized(寫)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

  • 原文地址 Java集合 Java集合框架:是一種工具類,就像是一個容器可以存儲任意數(shù)量的具有共同屬性的對象。 Ja...
    gyl_coder閱讀 1,042評論 0 8
  • 集合類框架的介紹: ![Java 集合類框架](https://upload-images.jianshu.io/...
    LynnGuo閱讀 805評論 0 1
  • Java集合類可用于存儲數(shù)量不等的對象,并可以實現(xiàn)常用的數(shù)據(jù)結(jié)構(gòu)如棧,隊列等,Java集合還可以用于保存具有映射關(guān)...
    小徐andorid閱讀 2,100評論 0 13
  • 1.1集合(100%) 單列 雙列(k,v) 雙列集合的5種遍歷方式 Collection和Map,是集合框架的...
    侯亞超閱讀 328評論 0 0
  • 前面已經(jīng)介紹完了Collection接口下的集合實現(xiàn)類,今天我們來介紹Map接口下的兩個重要的集合實現(xiàn)類HashM...
    Ruheng閱讀 10,545評論 2 38

友情鏈接更多精彩內(nèi)容