【集合】HashMap和ConcurrentHashMap的知識總結

哈希表(hash table)也叫散列表,是一種非常重要的數據結構,應用場景及其豐富,許多緩存技術(比如memcached)的核心其實就是在內存中維護一張大的哈希表。

一、什麼是哈希表

在討論哈希表之前,我們先大概瞭解下其他數據結構在新增,查找等基礎操作執行性能

  • 數組
    採用一段連續的存儲單元來存儲數據。對於指定下標的查找,時間複雜度爲O(1);
    通過給定值進行查找,需要遍歷數組,逐一比對給定關鍵字和數組元素,時間複雜度爲O(n),當然,對於有序數組,則可採用二分查找,插值查找,斐波那契查找等方式,可將查找複雜度提高爲O(logn);
    對於一般的插入刪除操作,涉及到數組元素的移動,其平均複雜度也爲O(n)

  • 線性鏈表
    對於鏈表的新增,刪除等操作(在找到指定操作位置後),僅需處理結點間的引用即可,時間複雜度爲O(1),而查找操作需要遍歷鏈表逐一進行比對,複雜度爲O(n)

  • 二叉樹
    對一棵相對平衡的有序二叉樹,對其進行插入,查找,刪除等操作,平均複雜度均爲O(logn)。

  • 哈希表
    相比上述幾種數據結構,在哈希表中進行添加,刪除,查找等操作,性能十分之高,不考慮哈希衝突的情況下,僅需一次定位即可完成,時間複雜度爲O(1).

哈希表上面的特性,哈希表的主幹就是數組。

 

哈希表

比如我們要新增或查找某個元素,我們通過把當前元素的關鍵字 通過某個函數映射到數組中的某個位置,通過數組下標一次定位就可完成操作。
存儲位置 = f(關鍵字)
其中,這個函數f一般稱爲哈希函數,這個函數的設計好壞會直接影響到哈希表的優劣。
查找操作同理,先通過哈希函數計算出實際存儲地址,然後從數組中對應地址取出即可。

二、哈希衝突

通過哈希函數得出的實際存儲地址相同怎麼辦?也就是說,當我們對某個元素進行哈希運算,得到一個存儲地址,然後要進行插入的時候,發現已經被其他元素佔用了,其實這就是所謂的哈希衝突,也叫哈希碰撞。

哈希函數的設計至關重要,好的哈希函數會儘可能地保證 計算簡單和散列地址分佈均勻,但是不可能設計出一個絕對完美的哈希函數,我們需要清楚的是,數組是一塊連續的固定長度的內存空間,再好的哈希函數也不能保證得到的存儲地址絕對不發生衝突。

哈希衝突的解決方案有多種:開放定址法(發生衝突,繼續尋找下一塊未被佔用的存儲地址),再散列函數法,鏈地址法.
HashMap即是採用了鏈地址法.

  • JDK7 使用了數組+鏈表的方式
  • JDK8 使用了數組+鏈表+紅黑樹的方式

三、HashMap的實現原理

HashMap的主幹是一個Entry數組。Entry是HashMap的基本組成單元,每一個Entry包含一個key-value鍵值對。

 

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

Entry是HashMap中的一個靜態內部類。

 

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;//存儲指向下一個Entry的引用,單鏈表結構
        int hash;//對key的hashcode值進行hash運算後得到的值,存儲在Entry,避免重複計算

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

HashMap的整體結構如下:

 

HashMap的結構

  • 解決衝突的鏈表的長度影響到HashMap查詢的效率
    簡單來說,HashMap由數組+鏈表組成的,數組是HashMap的主體,鏈表則是主要爲了解決哈希衝突而存在的,如果定位到的數組位置不含鏈表(當前entry的next指向null),那麼對於查找,添加等操作很快,僅需一次尋址即可;如果定位到的數組包含鏈表,對於添加操作,其時間複雜度爲O(n),首先遍歷鏈表,存在即覆蓋,否則新增;對於查找操作來講,仍需遍歷鏈表,然後通過key對象的equals方法逐一比對查找。所以,性能考慮,HashMap中的鏈表出現越少,性能纔會越好。

  • 發生衝突關於entry節點插入鏈表還是鏈頭呢?
    JDK7:插入鏈表的頭部,頭插法
    JDK8:插入鏈表的尾部,尾插法

JDK7

 

public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        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;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

閱讀源碼發現,如果遍歷鏈表都沒法發現相應的key值的話,則會調用addEntry方法在鏈表添加一個Entry,重點就在與addEntry方法是如何插入鏈表的,addEntry方法源碼如下:

 

 void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        if (size++ >= threshold)
            resize(2 * table.length);
    }

這裏構造了一個新的Entry對象(構造方法的最後一個參數傳入了當前的Entry鏈表),然後直接用這個新的Entry對象取代了舊的Entry鏈表,看一下Entry的構造方法可以知道是頭插法。

 

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

從構造方法中的next=n可以看出確實是把原本的鏈表直接鏈在了新建的Entry對象的後邊,可以斷定是插入頭部。

JDK8
還是繼續查看put方法的源碼查看插入節點的代碼:

 

//e是p的下一個節點
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;
}

從這段代碼中可以很顯然地看出當到達鏈表尾部(即p是鏈表的最後一個節點)時,e被賦爲null,會進入這個分支代碼,然後會用newNode方法建立一個新的節點插入尾部。

四、HashMap的默認參數理解

1.爲什麼HashMap的Entry數組長度默認爲16呢?爲什麼數組長度一定要是2的n次冪呢?

查看HashMap計算hashcode的方法獲取存儲的位置:
爲了減少hash值的碰撞,需要實現一個儘量均勻分佈的hash函數,在HashMap中通過利用key的hashcode值,來進行位運算

 

webpuploading.4e448015.gif轉存失敗重新上傳取消

存儲的流程

 

 /**
     * Returns index for hash code h.
     */
    static int indexFor(int h, int length) {
        return h & (length-1);
    }

舉個例子
1.計算"book"的hashcode
十進制 : 3029737
二進制 : 101110001110101110 1001

2.HashMap長度是默認的16,length - 1的結果
十進制 : 15
二進制 : 1111

3.把以上兩個結果做與運算
101110001110101110 1001 & 1111 = 1001
1001的十進制 : 9,所以 index=9

結論:hash算法最終得到的index結果,取決於hashcode值的最後幾位
現在,我們假設HashMap的長度是10,重複剛纔的運算步驟:
hashcode : 101110001110101110 1001
length - 1 :                                     1001
index :                                           1001


再換一個hashcode 101110001110101110 1111 試試:
hashcode : 101110001110101110 1111
length - 1 :                                    1001
index :                                           1001
從結果可以看出,雖然hashcode變化了,但是運算的結果都是1001,也就是說,當HashMap長度爲10的時候,有些index結果的出現機率會更大而有些index結果永遠不會出現(比如0111),這樣就不符合hash均勻分佈的原則
反觀長度16或者其他2的冪,length - 1的值是所有二進制位全爲1,這種情況下,index的結果等同於hashcode後幾位的值,只要輸入的hashcode本身分佈均勻,hash算法的結果就是均勻的。


結論

  • HashMap的默認長度爲16和規定數組長度爲2的冪,是爲了降低hash碰撞的機率。

2.HashMap擴容限制的負載因子爲什麼是0.75呢?爲什麼不能是0.1或者1呢?

由HashMap的put方法中實現中的addEntry的實現代碼可知當數組長度達到限制條件的閾值就要進行數組的擴容。
擴容的方式是:
新建一個長度爲之前數組2倍的新的數組,然後將當前的Entry數組中的元素全部傳輸過去,擴容後的新數組長度爲之前的2倍,所以擴容相對來說是個耗資源的操作。
擴容的觸發條件:
閾值 = 數組默認的長度 x 負載因子(閾值=16x0.75=12)

 

threshold = (int)(capacity * loadFactor);

 

void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);//當size超過臨界閾值threshold,並且即將發生哈希衝突時進行擴容
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }

由上面的內容可知,

  • 如果負載因子爲0.5甚至更低的可能的話,最後得到的臨時閾值明顯會很小,這樣的情況就會造成分配的內存的浪費,存在多餘的沒用的內存空間,也不滿足了哈希表均勻分佈的情況。
  • 如果負載因子達到了1的情況,也就是Entry數組存滿了才發生擴容,這樣會出現大量的哈希衝突的情況,出現鏈表過長,因此造成get查詢數據的效率。
  • 因此選擇了0.5~1的折中數也就是0.75,均衡解決了上面出現的情況。

五、JDK8下的HashMap的實現

區別:

  • 使用一個Node數組取代了JDK7的Entry數組來存儲數據,這個Node可能是鏈表結構,也可能是紅黑樹結構;
  • 如果插入的元素key的hashcode值相同,那麼這些key也會被定位到Node數組的同一個格子裏,如果不超過8個使用鏈表存儲;
  • 超過8個,會調用treeifyBin函數,將鏈表轉換爲紅黑樹。那麼即使所有key的hashcode完全相同,由於紅黑樹的特點,查找某個特定元素,也只需要O(logn)的開銷。

     

    JDK8的HashMap

上圖是示意圖,主要是描述結構,不會達到這個狀態的,因爲這麼多數據的時候早就擴容了。

put

  • 和 Java7 稍微有點不一樣的地方就是,Java7 是先擴容後插入新值的,Java8 先插值再擴容,不過這個不重要。

get

  • 計算 key 的 hash 值,根據 hash 值找到對應數組下標: hash & (length-1)
  • 判斷數組該位置處的元素是否剛好就是我們要找的,如果不是,走第三步
  • 判斷該元素類型是否是 TreeNode,如果是,用紅黑樹的方法取數據,如果不是,走第四步 遍歷鏈表,直到找到相等(==或equals)的 key

六、CurrentHashMap的原理

由於HashMap是線程不同步的,雖然處理數據的效率高,但是在多線程的情況下存在着安全問題,因此設計了CurrentHashMap來解決多線程安全問題。

HashMap在put的時候,插入的元素超過了容量(由負載因子決定)的範圍就會觸發擴容操作,就是rehash,這個會重新將原數組的內容重新hash到新的擴容數組中,在多線程的環境下,存在同時其他的元素也在進行put操作,如果hash值相同,可能出現同時在同一數組下用鏈表表示,造成閉環,導致在get時會出現死循環,所以HashMap是線程不安全的。

JDK7下的CurrentHashMap

在JDK1.7版本中,ConcurrentHashMap的數據結構是由一個Segment數組和多個HashEntry組成,主要實現原理是實現了鎖分離的思路解決了多線程的安全問題,如下圖所示:

CurrentHashMap的結構

 

Segment數組的意義就是將一個大的table分割成多個小的table來進行加鎖,也就是上面的提到的鎖分離技術,而每一個Segment元素存儲的是HashEntry數組+鏈表,這個和HashMap的數據存儲結構一樣。

ConcurrentHashMap 與HashMap和Hashtable 最大的不同在於:put和 get 兩次Hash到達指定的HashEntry,第一次hash到達Segment,第二次到達Segment裏面的Entry,然後在遍歷entry鏈表.

初始化

ConcurrentHashMap的初始化是會通過位與運算來初始化Segment的大小,用ssize來表示,源碼如下所示

 

private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
        // For serialization compatibility
        // Emulate segment calculation from previous version of this class
        int sshift = 0;
        int ssize = 1;
        while (ssize < DEFAULT_CONCURRENCY_LEVEL) {
            ++sshift;
            ssize <<= 1;
        }
        int segmentShift = 32 - sshift;
        int segmentMask = ssize - 1;

因爲ssize用位於運算來計算(ssize <<=1),所以Segment的大小取值都是以2的N次方,無關concurrencyLevel的取值,當然concurrencyLevel最大隻能用16位的二進制來表示,即65536,換句話說,Segment的大小最多65536個,沒有指定concurrencyLevel元素初始化,Segment的大小ssize默認爲 DEFAULT_CONCURRENCY_LEVEL =16

每一個Segment元素下的HashEntry的初始化也是按照位與運算來計算,用cap來表示,如下:

 

int cap = 1;
while (cap < c)
    cap <<= 1

上所示,HashEntry大小的計算也是2的N次方(cap <<=1), cap的初始值爲1,所以HashEntry最小的容量爲2

put操作

 

 static class Segment<K,V> extends ReentrantLock implements Serializable {
        private static final long serialVersionUID = 2249069246763182397L;
        final float loadFactor;
        Segment(float lf) { this.loadFactor = lf; }
    }

從上Segment的繼承體系可以看出,Segment實現了ReentrantLock,也就帶有鎖的功能,當執行put操作時,會進行第一次key的hash來定位Segment的位置,如果該Segment還沒有初始化,即通過CAS操作進行賦值,然後進行第二次hash操作,找到相應的HashEntry的位置,這裏會利用繼承過來的鎖的特性,在將數據插入指定的HashEntry位置時(鏈表的尾端),會通過繼承ReentrantLock的tryLock()方法嘗試去獲取鎖,如果獲取成功就直接插入相應的位置,如果已經有線程獲取該Segment的鎖,那當前線程會以自旋的方式(如果不瞭解自旋鎖,請參考:自旋鎖原理及java自旋鎖)去繼續的調用tryLock()方法去獲取鎖,超過指定次數就掛起,等待喚醒.

get

ConcurrentHashMap的get操作跟HashMap類似,只是ConcurrentHashMap第一次需要經過一次hash定位到Segment的位置,然後再hash定位到指定的HashEntry,遍歷該HashEntry下的鏈表進行對比,成功就返回,不成功就返回null

size 返回ConcurrentHashMap元素大小

計算ConcurrentHashMap的元素大小是一個有趣的問題,因爲他是併發操作的,就是在你計算size的時候,他還在併發的插入數據,可能會導致你計算出來的size和你實際的size有相差(在你return size的時候,插入了多個數據),要解決這個問題,JDK1.7版本用兩種方案

 

try {
    for (;;) {
        if (retries++ == RETRIES_BEFORE_LOCK) {
            for (int j = 0; j < segments.length; ++j) ensureSegment(j).lock(); // force creation
        }
        sum = 0L;
        size = 0;
        overflow = false;
        for (int j = 0; j < segments.length; ++j) {
            Segment<K,V> seg = segmentAt(segments, j);
            if (seg != null) { sum += seg.modCount; int c = seg.count; if (c < 0 || (size += c) < 0)
               overflow = true;
            } }
        if (sum == last) break;
        last = sum; } }
finally {
    if (retries > RETRIES_BEFORE_LOCK) {
        for (int j = 0; j < segments.length; ++j)
            segmentAt(segments, j).unlock();
    }
}

1、第一種方案他會使用不加鎖的模式去嘗試多次計算ConcurrentHashMap的size,最多三次,比較前後兩次計算的結果,結果一致就認爲當前沒有元素加入,計算的結果是準確的.
2、第二種方案是如果第一種方案不符合,他就會給每個Segment加上鎖,然後計算ConcurrentHashMap的size返回.

JDK8的ConcurrentHashMap

JDK1.8的實現已經摒棄了Segment的概念,而是直接用Node數組+鏈表+紅黑樹的數據結構來實現,併發控制使用Synchronized和CAS來操作,整個看起來就像是優化過且線程安全的HashMap,雖然在JDK1.8中還能看到Segment的數據結構,但是已經簡化了屬性,只是爲了兼容舊版本.

 

webpuploading.4e448015.gif轉存失敗重新上傳取消

JDK8的ConcurrentHashMap

在深入JDK1.8的put和get實現之前要知道一些常量設計和數據結構,這些是構成ConcurrentHashMap實現結構的基礎,下面看一下基本屬性:

 

// node數組最大容量:2^30=1073741824
private static final int MAXIMUM_CAPACITY = 1 << 30;
// 默認初始值,必須是2的幕數
private static final int DEFAULT_CAPACITY = 16
//數組可能最大值,需要與toArray()相關方法關聯
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//併發級別,遺留下來的,爲兼容以前的版本
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
// 負載因子
private static final float LOAD_FACTOR = 0.75f;
// 鏈表轉紅黑樹閥值,> 8 鏈表轉換爲紅黑樹
static final int TREEIFY_THRESHOLD = 8;
//樹轉鏈表閥值,小於等於6(tranfer時,lc、hc=0兩個計數器分別++記錄原bin、新binTreeNode數量,<=UNTREEIFY_THRESHOLD 則untreeify(lo))
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
private static final int MIN_TRANSFER_STRIDE = 16;
private static int RESIZE_STAMP_BITS = 16;
// 2^15-1,help resize的最大線程數
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
// 32-16=16,sizeCtl中記錄size大小的偏移量
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
// forwarding nodes的hash值
static final int MOVED     = -1;
// 樹根節點的hash值
static final int TREEBIN   = -2;
// ReservationNode的hash值
static final int RESERVED  = -3;
// 可用處理器數量
static final int NCPU = Runtime.getRuntime().availableProcessors();
//存放node的數組
transient volatile Node<K,V>[] table;
/*控制標識符,用來控制table的初始化和擴容的操作,不同的值有不同的含義
 *當爲負數時:-1代表正在初始化,-N代表有N-1個線程正在 進行擴容
 *當爲0時:代表當時的table還沒有被初始化
 *當爲正數時:表示初始化或者下一次進行擴容的大小
private transient volatile int sizeCtl;

JDK8的Node

Node是ConcurrentHashMap存儲結構的基本單元,繼承於HashMap中的Entry,用於存儲數據,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;
        }

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

        /**
         * Virtualized support for map.get(); overridden in subclasses.
         */
        Node<K,V> find(int h, Object k) {
            Node<K,V> e = this;
            if (k != null) {
                do {
                    K ek;
                    if (e.hash == h &&
                        ((ek = e.key) == k || (ek != null && k.equals(ek))))
                        return e;
                } while ((e = e.next) != null);
            }
            return null;
        }
    }

TreeNode

TreeNode繼承於Node,但是數據結構換成了二叉樹結構,它是紅黑樹的數據的存儲結構,用於紅黑樹中存儲數據,當鏈表的節點數大於8時會轉換成紅黑樹的結構,他就是通過TreeNode作爲存儲結構代替Node來轉換成黑紅樹源代碼如下

 

static final class TreeNode<K,V> extends Node<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;

        TreeNode(int hash, K key, V val, Node<K,V> next,
                 TreeNode<K,V> parent) {
            super(hash, key, val, next);
            this.parent = parent;
        }

        Node<K,V> find(int h, Object k) {
            return findTreeNode(h, k, null);
        }

        /**
         * Returns the TreeNode (or null if not found) for the given key
         * starting at given root.
         */
        final TreeNode<K,V> findTreeNode(int h, Object k, Class<?> kc) {
            if (k != null) {
                TreeNode<K,V> p = this;
                do  {
                    int ph, dir; K pk; TreeNode<K,V> q;
                    TreeNode<K,V> pl = p.left, pr = p.right;
                    if ((ph = p.hash) > h)
                        p = pl;
                    else if (ph < h)
                        p = pr;
                    else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
                        return p;
                    else if (pl == null)
                        p = pr;
                    else if (pr == null)
                        p = pl;
                    else if ((kc != null ||
                              (kc = comparableClassFor(k)) != null) &&
                             (dir = compareComparables(kc, k, pk)) != 0)
                        p = (dir < 0) ? pl : pr;
                    else if ((q = pr.findTreeNode(h, k, kc)) != null)
                        return q;
                    else
                        p = pl;
                } while (p != null);
            }
            return null;
        }
    }

TreeBin

TreeBin從字面含義中可以理解爲存儲樹形結構的容器,而樹形結構就是指TreeNode,所以TreeBin就是封裝TreeNode的容器,它提供轉換黑紅樹的一些條件和鎖的控制.

總結和思考

其實可以看出JDK1.8版本的ConcurrentHashMap的數據結構已經接近HashMap,相對而言,ConcurrentHashMap只是增加了同步的操作來控制併發,從JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+紅黑樹,相對而言,總結如下思考

  • JDK1.8的實現降低鎖的粒度,JDK1.7版本鎖的粒度是基於Segment的,包含多個HashEntry,而JDK1.8鎖的粒度就是HashEntry(首節點)

  • JDK1.8版本的數據結構變得更加簡單,使得操作也更加清晰流暢,因爲已經使用synchronized來進行同步,所以不需要分段鎖的概念,也就不需要Segment這種數據結構了,由於粒度的降低,實現的複雜度也增加了

  • JDK1.8使用紅黑樹來優化鏈表,基於長度很長的鏈表的遍歷是一個很漫長的過程,而紅黑樹的遍歷效率是很快的,代替一定閾值的鏈表,這樣形成一個最佳拍檔

  • JDK1.8爲什麼使用內置鎖synchronized來代替重入鎖ReentrantLock,我覺得有以下幾點
    1.因爲粒度降低了,在相對而言的低粒度加鎖方式,synchronized並不比ReentrantLock差,在粗粒度加鎖中ReentrantLock可能通過Condition來控制各個低粒度的邊界,更加的靈活,而在低粒度中,Condition的優勢就沒有了
    2.JVM的開發團隊從來都沒有放棄synchronized,而且基於JVM的synchronized優化空間更大,使用內嵌的關鍵字比使用API更加自然
    3.在大量的數據操作下,對於JVM的內存壓力,基於API的ReentrantLock會開銷更多的內存,雖然不是瓶頸,但是也是一個選擇依據



作者:淺藍色的麻吉
鏈接:https://www.jianshu.com/p/a7767e6ff2a2
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

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