Java中的HashMap和ConcurrentHashMap

HashMap和ConcurrentHashMap在Java7和Java8中原理不同败玉,所以這里分別介紹。

Java7 HashMap

在Java7中镜硕,HashMap內(nèi)部是一個(gè)數(shù)組运翼,每個(gè)數(shù)組元素是一個(gè)單向鏈表。

每個(gè)綠色實(shí)體是一個(gè)Entity實(shí)例兴枯,它包含4個(gè)屬性:key血淌、value、hashCode和指向下一個(gè)Entity實(shí)例的next指針。
每個(gè)HashMap中還包括:1. capacity(數(shù)組容量悠夯,大小為2^n)癌淮;2. loadFactor(負(fù)載銀子,默認(rèn)0.75)沦补;3. threshold(擴(kuò)容閾值乳蓄,capacity*loadFactory)
注:HashMap的初始化和擴(kuò)容大小都是2^n,因?yàn)镠ashMap在獲取key的hashCode后夕膀,需要與數(shù)組長(zhǎng)度進(jìn)行與運(yùn)算虚倒,以便確定存儲(chǔ)在數(shù)組中的index位置,為了不浪費(fèi)空間店诗,做與運(yùn)算時(shí)裹刮,所有位都不要出現(xiàn)0,否則會(huì)造成浪費(fèi)庞瘸。

static int indexFor(int h, int length) {
    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    return h & (length-1);
}
  • h:插入元素的hashCode值
  • length:hashMap的容量大小

length值為2次冪捧弃,那么length-1的值就是一個(gè)全為1的值:1111...11。與運(yùn)算效率高擦囊,全為1的值做與運(yùn)算不會(huì)有空間浪費(fèi)违霞。
如果length不為2^n,那么length-1可能有0位瞬场,例如:1110买鸽,在與h做與運(yùn)算時(shí),0001贯被、0011眼五、1001等這樣的位置就不可能被使用了,造成空間浪費(fèi)彤灶,同時(shí)增大碰撞檢測(cè)幾率看幼,減慢查詢(xún)效率。

put

public V put(K key, V value) {
    // 當(dāng)插入第一個(gè)元素的時(shí)候幌陕,需要先初始化數(shù)組大小
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    // 如果 key 為 null诵姜,感興趣的可以往里看,最終會(huì)將這個(gè) entry 放到 table[0] 中
    if (key == null)
        return putForNullKey(value);
    // 1. 求 key 的 hash 值
    int hash = hash(key);
    // 2. 找到對(duì)應(yīng)的數(shù)組下標(biāo)
    int i = indexFor(hash, table.length);
    // 3. 遍歷一下對(duì)應(yīng)下標(biāo)處的鏈表搏熄,看是否有重復(fù)的 key 已經(jīng)存在棚唆,
    // 如果有,直接覆蓋心例,put 方法返回舊值就結(jié)束了
    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++;
    // 4. 不存在重復(fù)的 key宵凌,將此 entry 添加到鏈表中,細(xì)節(jié)后面說(shuō)
    addEntry(hash, key, value, i);
    return null;
}
數(shù)組初始化

inflateTable()為HashMap中數(shù)組的初始化函數(shù):

private void inflateTable(int toSize) {
    // 保證數(shù)組大小一定是 2 的 n 次方契邀。
    // 比如這樣初始化:new HashMap(20)摆寄,那么處理成初始數(shù)組大小是 32
    int capacity = roundUpToPowerOf2(toSize);
    // 計(jì)算擴(kuò)容閾值:capacity * loadFactor
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    // 算是初始化數(shù)組吧
    table = new Entry[capacity];
    initHashSeedAsNeeded(capacity); //ignore
}

數(shù)組大小保持為 2 的 n 次方,具體原因前面已經(jīng)講過(guò)坯门。

計(jì)算具體數(shù)組位置

根據(jù)key計(jì)算出的hash值微饥,與length進(jìn)行與運(yùn)算。

static int indexFor(int hash, int length) {
    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    return hash & (length-1);
}

就是取 hash 值的低 n 位古戴。如在數(shù)組長(zhǎng)度為 32 的時(shí)候欠橘,其實(shí)取的就是 key 的 hash 值的低 5 位,作為它在數(shù)組中的下標(biāo)位置现恼。

添加節(jié)點(diǎn)到鏈表
void addEntry(int hash, K key, V value, int bucketIndex) {
    // 如果當(dāng)前 HashMap 大小已經(jīng)達(dá)到了閾值肃续,并且新值要插入的數(shù)組位置已經(jīng)有元素了,那么要擴(kuò)容
    if ((size >= threshold) && (null != table[bucketIndex])) {
        // 擴(kuò)容叉袍,后面會(huì)介紹一下
        resize(2 * table.length);
        // 擴(kuò)容以后始锚,重新計(jì)算 hash 值
       hash = (null != key) ? hash(key) : 0;
        // 重新計(jì)算擴(kuò)容后的新的下標(biāo)
        bucketIndex = indexFor(hash, table.length);
    }
    // 往下看
    createEntry(hash, key, value, bucketIndex);
}

// 這個(gè)很簡(jiǎn)單,其實(shí)就是將新值放到鏈表的表頭喳逛,然后 size++
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++;
}

首先判斷數(shù)組是否需要擴(kuò)容瞧捌,然后將新值放在鏈表表頭,size++润文。

數(shù)組擴(kuò)容

擴(kuò)容后姐呐,數(shù)組大小為原來(lái)的 2 倍。

void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
    // 新的數(shù)組
    Entry[] newTable = new Entry[newCapacity];
    // 將原來(lái)數(shù)組中的值遷移到新的更大的數(shù)組中
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

擴(kuò)容就是new一個(gè)新數(shù)組典蝌,并將老數(shù)組中的數(shù)據(jù)遷移至新數(shù)組宗曙砂。原來(lái)table[i]鏈表中的所有節(jié)點(diǎn),會(huì)被重新分配到table[i]和table[i + oldLength]中(如原來(lái)數(shù)組長(zhǎng)度是 16骏掀,那么擴(kuò)容后鸠澈,原來(lái) table[0] 處的鏈表中的所有元素會(huì)被分配到新數(shù)組中 newTable[0] 和 newTable[16] 這兩個(gè)位置)。

get

public V get(Object key) {
    // 之前說(shuō)過(guò)截驮,key 為 null 的話(huà)笑陈,會(huì)被放到 table[0],所以只要遍歷下 table[0] 處的鏈表就可以了
    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;
    }
    int hash = (key == null) ? 0 : hash(key);
    // 確定數(shù)組下標(biāo)侧纯,然后從頭開(kāi)始遍歷鏈表新锈,直到找到為止
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            return e;
    }
    return null;
}
  1. 根據(jù) key 計(jì)算 hash 值。
  2. 找到相應(yīng)的數(shù)組下標(biāo):hash & (length – 1)眶熬。
  3. 遍歷該數(shù)組位置處的鏈表妹笆,直到找到相等(==或equals)的 key。

Java7 ConcurrentHashMap

CuncurrentHashMap是一個(gè)數(shù)組娜氏,每個(gè)元素是一個(gè)Segment拳缠,每個(gè)segment繼承自ReentrantLook來(lái)實(shí)現(xiàn)鎖,也就是說(shuō)CuncurrentHashMap是通過(guò)鎖住每一個(gè)segment從而實(shí)現(xiàn)整體的線程安全贸弥。



初始化CuncurrentHashMap時(shí)可以定義數(shù)組大小窟坐,默認(rèn)為16,即最多支持16條線程同時(shí)讀寫(xiě)(分布于16個(gè)不同的Segment),一旦定義哲鸳,不可修改臣疑。

初始化

public ConcurrentHashMap(int initialCapacity,
                     float loadFactor, int concurrencyLevel) {
  if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
      throw new IllegalArgumentException();
  if (concurrencyLevel > MAX_SEGMENTS)
      concurrencyLevel = MAX_SEGMENTS;
  // Find power-of-two sizes best matching arguments
  int sshift = 0;
  int ssize = 1;
  // 計(jì)算并行級(jí)別 ssize,因?yàn)橐3植⑿屑?jí)別是 2 的 n 次方
  while (ssize < concurrencyLevel) {
      ++sshift;
      ssize <<= 1;
  }
  // 我們這里先不要那么燒腦徙菠,用默認(rèn)值讯沈,concurrencyLevel 為 16,sshift 為 4
  // 那么計(jì)算出 segmentShift 為 28婿奔,segmentMask 為 15缺狠,后面會(huì)用到這兩個(gè)值
  this.segmentShift = 32 - sshift;
  this.segmentMask = ssize - 1;

  if (initialCapacity > MAXIMUM_CAPACITY)
      initialCapacity = MAXIMUM_CAPACITY;

  // initialCapacity 是設(shè)置整個(gè) map 初始的大小,
  // 這里根據(jù) initialCapacity 計(jì)算 Segment 數(shù)組中每個(gè)位置可以分到的大小
  // 如 initialCapacity 為 64萍摊,那么每個(gè) Segment 或稱(chēng)之為"槽"可以分到 4 個(gè)
  int c = initialCapacity / ssize;
  if (c * ssize < initialCapacity)
      ++c;
  // 默認(rèn) MIN_SEGMENT_TABLE_CAPACITY 是 2挤茄,這個(gè)值也是有講究的,因?yàn)檫@樣的話(huà)冰木,對(duì)于具體的槽上穷劈,
  // 插入一個(gè)元素不至于擴(kuò)容,插入第二個(gè)的時(shí)候才會(huì)擴(kuò)容
  int cap = MIN_SEGMENT_TABLE_CAPACITY; 
  while (cap < c)
      cap <<= 1;

  // 創(chuàng)建 Segment 數(shù)組片酝,
  // 并創(chuàng)建數(shù)組的第一個(gè)元素 segment[0]
  Segment<K,V> s0 =
      new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                     (HashEntry<K,V>[])new HashEntry[cap]);
  Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
  // 往數(shù)組寫(xiě)入 segment[0]
  UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
  this.segments = ss;
}
  • initialCapacity:初始容量囚衔,這個(gè)值指的是整個(gè) ConcurrentHashMap 的初始容量,實(shí)際操作的時(shí)候需要平均分給每個(gè) Segment雕沿。
  • loadFactor:負(fù)載因子练湿,Segment 數(shù)組不可以擴(kuò)容,這個(gè)負(fù)載因子是給每個(gè) Segment 內(nèi)部使用的审轮。
  • cap是每個(gè)Segment的初始大小肥哎,默認(rèn)MIN_SEGMENT_TABLE_CAPACITY=2,而默認(rèn)的負(fù)載因子LoadFactor是0.75疾渣,所以每個(gè)Segment的初始閾值為2*0.75=1.5篡诽,即放入第一個(gè)值時(shí)不會(huì)擴(kuò)容,第二個(gè)值才開(kāi)始擴(kuò)容榴捡。
  • 初始化時(shí)只初始化了Segment[0]杈女,其他位置為null
  • 當(dāng)前 segmentShift 的值為 32 – 4 = 28,segmentMask 為 16 – 1 = 15

put

public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();
    // 1. 計(jì)算 key 的 hash 值
    int hash = hash(key);
    // 2. 根據(jù) hash 值找到 Segment 數(shù)組中的位置 j
    //    hash 是 32 位吊圾,無(wú)符號(hào)右移 segmentShift(28) 位达椰,剩下低 4 位,
    //    然后和 segmentMask(15) 做一次與操作项乒,也就是說(shuō) j 是 hash 值的最后 4 位啰劲,也就是槽的數(shù)組下標(biāo)
    int j = (hash >>> segmentShift) & segmentMask;
    // 剛剛說(shuō)了,初始化的時(shí)候初始化了 segment[0]檀何,但是其他位置還是 null蝇裤,
    // ensureSegment(j) 對(duì) segment[j] 進(jìn)行初始化
    if ((s = (Segment<K,V>)UNSAFE.getObject(segments, (j << SSHIFT) + SBASE)) == null)
        s = ensureSegment(j);
    // 3. 插入新值到槽s中
    return s.put(key, hash, value, false);
}
Segment.put()
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    // 在往該 segment 寫(xiě)入前廷支,需要先獲取該 segment 的獨(dú)占鎖
    // 先看主流程,后面還會(huì)具體介紹這部分內(nèi)容
    HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
    V oldValue;
    try {
        // 這個(gè)是 segment 內(nèi)部的數(shù)組
        HashEntry<K,V>[] tab = table;
        // 再利用 hash 值栓辜,求應(yīng)該放置的數(shù)組下標(biāo)
        int index = (tab.length - 1) & hash;
        // first 是數(shù)組該位置處的鏈表的表頭
        HashEntry<K,V> first = entryAt(tab, index);

        // 下面這串 for 循環(huán)雖然很長(zhǎng)恋拍,不過(guò)也很好理解,想想該位置沒(méi)有任何元素和已經(jīng)存在一個(gè)鏈表這兩種情況
        for (HashEntry<K,V> e = first;;) {
            if (e != null) {
                K k;
                if ((k = e.key) == key ||
                    (e.hash == hash && key.equals(k))) {
                    oldValue = e.value;
                    if (!onlyIfAbsent) {
                        // 覆蓋舊值
                        e.value = value;
                        ++modCount;
                   }
                    break;
                }
                // 繼續(xù)順著鏈表走
                e = e.next;
            }
            else {
                // node到底是不是null啃憎,這個(gè)要看獲取鎖的過(guò)程芝囤,不過(guò)和這里都沒(méi)有關(guān)系似炎。
               // 如果不為null辛萍,那就直接將它設(shè)置為鏈表表頭;如果是null羡藐,初始化并設(shè)置為鏈表表頭贩毕。
                if (node != null)
                    node.setNext(first);
                else
                    node = new HashEntry<K,V>(hash, key, value, first);

                int c = count + 1;
                // 如果超過(guò)了該 segment 的閾值,這個(gè) segment 需要擴(kuò)容
                if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                    rehash(node); // 擴(kuò)容后面也會(huì)具體分析
                else
                    // 沒(méi)有達(dá)到閾值仆嗦,將 node 放到數(shù)組 tab 的 index 位置辉阶,
                    // 其實(shí)就是將新的節(jié)點(diǎn)設(shè)置成原鏈表的表頭
                    setEntryAt(tab, index, node);
                ++modCount;
                count = c;
                oldValue = null;
                break;
            }
        }
    } finally {
        // 解鎖
        unlock();
    }
    return oldValue;
}

這里面,首先通過(guò)hash判斷value需要放在segment中的數(shù)組的那個(gè)位置瘩扼,然后獲取該位置的鏈表的表頭first谆甜,如果表頭first不為null,則插入鏈表頭集绰;如果first為空规辱,設(shè)置新鏈表,賦值栽燕。
每個(gè)Segment都有一個(gè)獨(dú)占鎖(tryLock())罕袋。

初始化槽: ensureSegment
private Segment<K,V> ensureSegment(int k) {
    final Segment<K,V>[] ss = this.segments;
    long u = (k << SSHIFT) + SBASE; // raw offset
    Segment<K,V> seg;
    if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
        // 這里看到為什么之前要初始化 segment[0] 了,
        // 使用當(dāng)前 segment[0] 處的數(shù)組長(zhǎng)度和負(fù)載因子來(lái)初始化 segment[k]
        // 為什么要用“當(dāng)前”碍岔,因?yàn)?segment[0] 可能早就擴(kuò)容過(guò)了
        Segment<K,V> proto = ss[0];
        int cap = proto.table.length;
        float lf = proto.loadFactor;
        int threshold = (int)(cap * lf);

        // 初始化 segment[k] 內(nèi)部的數(shù)組
        HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
        // 再次檢查一遍該槽是否被其他線程初始化了浴讯。
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) { 

            Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
            // 使用 while 循環(huán),內(nèi)部用 CAS蔼啦,當(dāng)前線程成功設(shè)值或其他線程成功設(shè)值后榆纽,退出
            while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
                if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                    break;
            }
        }
    }
    return seg;
}

通過(guò)之前初始化好的Segment[0]獲取基本信息,建立新的Segment[k]捏肢。并發(fā)操作使用 CAS 進(jìn)行控制奈籽,樂(lè)觀鎖,while循環(huán)獲取鎖猛计。

獲取寫(xiě)入鎖: scanAndLockForPut
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

   // 循環(huán)獲取鎖
    while (!tryLock()) {
        HashEntry<K,V> f; // to recheck first below
        if (retries < 0) {
            if (e == null) {
                if (node == null) // speculatively create node
                    // 進(jìn)到這里說(shuō)明數(shù)組該位置的鏈表是空的唠摹,沒(méi)有任何元素
                    // 當(dāng)然,進(jìn)到這里的另一個(gè)原因是 tryLock() 失敗奉瘤,所以該槽存在并發(fā)勾拉,不一定是該位置
                    node = new HashEntry<K,V>(hash, key, value, null);
                retries = 0;
            }
            else if (key.equals(e.key))
                retries = 0;
            else
                // 順著鏈表往下走
                e = e.next;
        }
        // 重試次數(shù)如果超過(guò) MAX_SCAN_RETRIES(單核1多核64)煮甥,那么不搶了,進(jìn)入到阻塞隊(duì)列等待鎖
        //    lock() 是阻塞方法藕赞,直到獲取鎖后返回
        else if (++retries > MAX_SCAN_RETRIES) {
            lock();
            break;
        }
        else if ((retries & 1) == 0 &&
                 // 這個(gè)時(shí)候是有大問(wèn)題了成肘,那就是有新的元素進(jìn)到了鏈表,成為了新的表頭
                 //     所以這邊的策略是斧蜕,相當(dāng)于重新走一遍這個(gè) scanAndLockForPut 方法
                 (f = entryForHash(this, hash)) != first) {
            e = first = f; // re-traverse if entry changed
            retries = -1;
        }
    }
    return node;
}

這個(gè)方法有兩個(gè)出口双霍,一個(gè)是 tryLock() 成功了,循環(huán)終止批销,另一個(gè)就是重試次數(shù)超過(guò)了 MAX_SCAN_RETRIES洒闸,進(jìn)到 lock() 方法,此方法會(huì)阻塞等待均芽,直到成功拿到獨(dú)占鎖丘逸。

擴(kuò)容: rehash

CuncurrentHashMap擴(kuò)容主要是指Segment內(nèi)部的數(shù)組HashEntry[] 擴(kuò)容,每次擴(kuò)容2倍

// 方法參數(shù)上的 node 是這次擴(kuò)容后掀宋,需要添加到新的數(shù)組中的數(shù)據(jù)甩牺。
private void rehash(HashEntry<K,V> node) {
    HashEntry<K,V>[] oldTable = table;
    int oldCapacity = oldTable.length;
    // 2 倍
    int newCapacity = oldCapacity << 1;
    threshold = (int)(newCapacity * loadFactor);
    // 創(chuàng)建新數(shù)組
    HashEntry<K,V>[] newTable = (HashEntry<K,V>[]) new HashEntry[newCapacity];
    // 新的掩碼城看,如從 16 擴(kuò)容到 32,那么 sizeMask 為 31,對(duì)應(yīng)二進(jìn)制 ‘000...00011111’
    int sizeMask = newCapacity - 1;

    // 遍歷原數(shù)組镐躲,老套路祠肥,將原數(shù)組位置 i 處的鏈表拆分到 新數(shù)組位置 i 和 i+oldCap 兩個(gè)位置
    for (int i = 0; i < oldCapacity ; i++) {
        // e 是鏈表的第一個(gè)元素
        HashEntry<K,V> e = oldTable[i];
        if (e != null) {
            HashEntry<K,V> next = e.next;
            // 計(jì)算應(yīng)該放置在新數(shù)組中的位置枪汪,
            // 假設(shè)原數(shù)組長(zhǎng)度為 16悉尾,e 在 oldTable[3] 處,那么 idx 只可能是 3 或者是 3 + 16 = 19
            int idx = e.hash & sizeMask;
            if (next == null)   // 該位置處只有一個(gè)元素唆途,那比較好辦
                newTable[idx] = e;
            else { 
                // Reuse consecutive sequence at same slot
                // e 是鏈表表頭  
                HashEntry<K,V> lastRun = e;
                // idx 是當(dāng)前鏈表的頭結(jié)點(diǎn) e 的新位置
                int lastIdx = idx;

                // 下面這個(gè) for 循環(huán)會(huì)找到一個(gè) lastRun 節(jié)點(diǎn)富雅,這個(gè)節(jié)點(diǎn)之后的所有元素是將要放到一起的
                for (HashEntry<K,V> last = next;
                    last != null;
                    last = last.next) {
                    int k = last.hash & sizeMask;
                    if (k != lastIdx) {
                        lastIdx = k;
                        lastRun = last;
                    }
                }
                // 將 lastRun 及其之后的所有節(jié)點(diǎn)組成的這個(gè)鏈表放到 lastIdx 這個(gè)位置
                newTable[lastIdx] = lastRun;
                // 下面的操作是處理 lastRun 之前的節(jié)點(diǎn),
                //    這些節(jié)點(diǎn)可能分配在另一個(gè)鏈表中肛搬,也可能分配到上面的那個(gè)鏈表中
                for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
                    V v = p.value;
                    int h = p.hash;
                    int k = h & sizeMask;
                    HashEntry<K,V> n = newTable[k];
                    newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
                }
            }
        }
    }
    // 將新來(lái)的 node 放到新數(shù)組中剛剛的 兩個(gè)鏈表之一 的 頭部
   int nodeIndex = node.hash & sizeMask; // add the new node
    node.setNext(newTable[nodeIndex]);
    newTable[nodeIndex] = node;
    table = newTable;
}

get 過(guò)程分析

public V get(Object key) {
    Segment<K,V> s; // manually integrate access methods to reduce overhead
    HashEntry<K,V>[] tab;
    // 1. hash 值
    int h = hash(key);
    long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
    // 2. 根據(jù) hash 找到對(duì)應(yīng)的 segment
    if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null 
                && (tab = s.table) != null) {
        // 3. 找到segment 內(nèi)部數(shù)組相應(yīng)位置的鏈表没佑,遍歷
        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;
}
  1. 計(jì)算 hash 值,找到 segment 數(shù)組中的具體位置温赔,或我們前面用的“槽”
  2. 槽中也是一個(gè)數(shù)組蛤奢,根據(jù) hash 找到數(shù)組中具體的位置
  3. 到這里是鏈表了,順著鏈表進(jìn)行查找即可

并發(fā)問(wèn)題分析

CuncurrentHashMap使用了CAS和volatite陶贼,使得雖然get()操作沒(méi)有加鎖啤贩,但是依然不會(huì)影響到多線程時(shí),get拜秧、put痹屹、remove時(shí)的線程安全性。
具體分析來(lái)源于:Java7/8 中的 HashMap 和 ConcurrentHashMap 全解析

  • 添加節(jié)點(diǎn)的操作 put 和刪除節(jié)點(diǎn)的操作 remove 都是要加 segment 上的獨(dú)占鎖的枉氮,所以它們之間自然不會(huì)有問(wèn)題志衍,我們需要考慮的問(wèn)題就是 get 的時(shí)候在同一個(gè) segment 中發(fā)生了 put 或 remove 操作暖庄。
  • put 操作的線程安全性
    • 初始化槽:這個(gè)我們之前就說(shuō)過(guò)了,使用了 CAS 來(lái)初始化 Segment 中的數(shù)組楼肪。
    • 添加節(jié)點(diǎn)到鏈表的操作是插入到表頭的培廓,所以,如果這個(gè)時(shí)候 get 操作在鏈表遍歷的過(guò)程已經(jīng)到了中間春叫,是不會(huì)影響的肩钠。當(dāng)然,另一個(gè)并發(fā)問(wèn)題就是 get 操作在 put 之后暂殖,需要保證剛剛插入表頭的節(jié)點(diǎn)被讀取价匠,這個(gè)依賴(lài)于 setEntryAt 方法中使用的 UNSAFE.putOrderedObject
    • 擴(kuò)容:擴(kuò)容是新創(chuàng)建了數(shù)組,然后進(jìn)行遷移數(shù)據(jù)央星,最后面將 newTable 設(shè)置給屬性 table霞怀。所以,如果 get 操作此時(shí)也在進(jìn)行莉给,那么也沒(méi)關(guān)系,如果 get 先行廉沮,那么就是在舊的 table 上做查詢(xún)操作颓遏;而 put 先行,那么 put 操作的可見(jiàn)性保證就是 table 使用了 volatile 關(guān)鍵字
  • remove 操作的線程安全性
    • 如果 remove 破壞的節(jié)點(diǎn) get 操作已經(jīng)過(guò)去了滞时,那么這里不存在任何問(wèn)題
    • 如果 remove 先破壞了一個(gè)節(jié)點(diǎn)叁幢,分兩種情況考慮。 1坪稽、如果此節(jié)點(diǎn)是頭結(jié)點(diǎn)曼玩,那么需要將頭結(jié)點(diǎn)的 next 設(shè)置為數(shù)組該位置的元素,table 雖然使用了 volatile 修飾窒百,但是 volatile 并不能提供數(shù)組內(nèi)部操作的可見(jiàn)性保證黍判,所以源碼中使用了 UNSAFE 來(lái)操作數(shù)組,請(qǐng)看方法 setEntryAt篙梢。2顷帖、如果要?jiǎng)h除的節(jié)點(diǎn)不是頭結(jié)點(diǎn),它會(huì)將要?jiǎng)h除節(jié)點(diǎn)的后繼節(jié)點(diǎn)接到前驅(qū)節(jié)點(diǎn)中渤滞,這里的并發(fā)保證就是 next 屬性是 volatile 的

Java8 HashMap

Java 7中的HashMap是由數(shù)組+鏈表構(gòu)成的贬墩,先根據(jù)hashCode定位到數(shù)組具體下標(biāo),然后再鏈表中一個(gè)一個(gè)按順序查找妄呕,所以時(shí)間復(fù)雜度是O(n)陶舞。
而Java 8中的HashMap利用了紅黑樹(shù),由數(shù)組+鏈表+紅黑樹(shù)組成绪励,當(dāng)鏈表的長(zhǎng)度超過(guò)8個(gè)后肿孵,會(huì)將鏈表轉(zhuǎn)化為紅黑樹(shù)论咏,所以時(shí)間復(fù)雜度為O(logN)。



Java 7中HashMap的數(shù)據(jù)節(jié)點(diǎn)是Entry颁井,而Java 8中使用Node(鏈表)和TreeNode(紅黑樹(shù))來(lái)代表節(jié)點(diǎn)厅贪。因此,可以根據(jù)當(dāng)前節(jié)點(diǎn)是Node還是TreeNode來(lái)判斷當(dāng)前使用的是鏈表還是紅黑樹(shù)雅宾。

put()

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

// 第三個(gè)參數(shù) onlyIfAbsent 如果是 true养涮,那么只有在不存在該 key 時(shí)才會(huì)進(jìn)行 put 操作
// 第四個(gè)參數(shù) evict 我們這里不關(guān)心
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 第一次 put 值的時(shí)候,會(huì)觸發(fā)下面的 resize()眉抬,類(lèi)似 java7 的第一次 put 也要初始化數(shù)組長(zhǎng)度
    // 第一次 resize 和后續(xù)的擴(kuò)容有些不一樣贯吓,因?yàn)檫@次是數(shù)組從 null 初始化到默認(rèn)的 16 或自定義的初始容量
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 找到具體的數(shù)組下標(biāo),如果此位置沒(méi)有值蜀变,那么直接初始化一下 Node 并放置在這個(gè)位置就可以了
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);

    else {// 數(shù)組該位置有數(shù)據(jù)
        Node<K,V> e; K k;
        // 首先悄谐,判斷該位置的第一個(gè)數(shù)據(jù)和我們要插入的數(shù)據(jù),key 是不是"相等"库北,如果是爬舰,取出這個(gè)節(jié)點(diǎn)
        if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
        // 如果該節(jié)點(diǎn)是代表紅黑樹(shù)的節(jié)點(diǎn),調(diào)用紅黑樹(shù)的插值方法寒瓦,本文不展開(kāi)說(shuō)紅黑樹(shù)
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 到這里情屹,說(shuō)明數(shù)組該位置上是一個(gè)鏈表
            for (int binCount = 0; ; ++binCount) {
                // 插入到鏈表的最后面(Java7 是插入到鏈表的最前面)
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // TREEIFY_THRESHOLD 為 8,所以杂腰,如果新插入的值是鏈表中的第 9 個(gè)
                    // 會(huì)觸發(fā)下面的 treeifyBin垃你,也就是將鏈表轉(zhuǎn)換為紅黑樹(shù)
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                }
                // 如果在該鏈表中找到了"相等"的 key(== 或 equals)
                if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                    // 此時(shí) break,那么 e 為鏈表中[與要插入的新值的 key "相等"]的 node
                    break;
                p = e;
            }
        }
        // e!=null 說(shuō)明存在舊值的key與要插入的key"相等"
        // 對(duì)于我們分析的put操作喂很,下面這個(gè) if 其實(shí)就是進(jìn)行 "值覆蓋"惜颇,然后返回舊值
        if (e != null) {
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 如果 HashMap 由于新插入這個(gè)值導(dǎo)致 size 已經(jīng)超過(guò)了閾值,需要進(jìn)行擴(kuò)容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}
resize() 擴(kuò)容: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) { // 對(duì)應(yīng)數(shù)組擴(kuò)容
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 將數(shù)組大小擴(kuò)大一倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 將閾值擴(kuò)大一倍
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // 對(duì)應(yīng)使用 new HashMap(int initialCapacity) 初始化后少辣,第一次 put 的時(shí)候
        newCap = oldThr;
    else {// 對(duì)應(yīng)使用 new HashMap() 初始化后凌摄,第一次 put 的時(shí)候
        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;

    // 用新的數(shù)組大小初始化新的數(shù)組
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab; // 如果是初始化數(shù)組,到這里就結(jié)束了毒坛,返回 newTab 即可

    if (oldTab != null) {
        // 開(kāi)始遍歷原數(shù)組望伦,進(jìn)行數(shù)據(jù)遷移。
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                // 如果該數(shù)組位置上只有單個(gè)元素煎殷,那就簡(jiǎn)單了屯伞,簡(jiǎn)單遷移這個(gè)元素就可以了
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                // 如果是紅黑樹(shù),具體我們就不展開(kāi)了
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { 
                    // 這塊是處理鏈表的情況豪直,
                    // 需要將此鏈表拆成兩個(gè)鏈表劣摇,放到新的數(shù)組中,并且保留原來(lái)的先后順序
                    // loHead弓乙、loTail 對(duì)應(yīng)一條鏈表末融,hiHead钧惧、hiTail 對(duì)應(yīng)另一條鏈表,代碼還是比較簡(jiǎn)單的
                    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;
                        // 第二條鏈表的新的位置是 j + oldCap勾习,這個(gè)很好理解
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

get()

  1. 計(jì)算 key 的 hash 值浓瞪,根據(jù) hash 值找到對(duì)應(yīng)數(shù)組下標(biāo): hash & (length-1)
  2. 判斷數(shù)組該位置處的元素是否剛好就是我們要找的,如果不是巧婶,走第三步
  3. 判斷該元素類(lèi)型是否是 TreeNode乾颁,如果是,用紅黑樹(shù)的方法取數(shù)據(jù)艺栈,如果不是英岭,走第四步
  4. 遍歷鏈表,直到找到相等(==或equals)的 key
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;
    if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
        // 判斷第一個(gè)節(jié)點(diǎn)是不是就是需要的
        if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            // 判斷是否是紅黑樹(shù)
            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;
}

Java8 ConcurrentHashMap

Java 8中ConcurrentHashMap的結(jié)構(gòu)與HashMap相同湿右,不過(guò)由于需要考慮線程安全诅妹,所以邏輯實(shí)現(xiàn)更復(fù)雜。

初始化

// 這構(gòu)造函數(shù)里毅人,什么都不干
public ConcurrentHashMap() {
}
public ConcurrentHashMap(int initialCapacity) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException();
    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
           tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
    this.sizeCtl = cap;
}

sizeCtl = 【 (1.5 * initialCapacity + 1)吭狡,然后向上取最近的 2 的 n 次方】。如 initialCapacity 為 10堰塌,那么得到 sizeCtl 為 16赵刑,如果 initialCapacity 為 11,得到 sizeCtl 為 32场刑。

put()

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市蚪战,隨后出現(xiàn)的幾起案子牵现,更是在濱河造成了極大的恐慌,老刑警劉巖邀桑,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瞎疼,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡壁畸,警方通過(guò)查閱死者的電腦和手機(jī)贼急,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)捏萍,“玉大人太抓,你說(shuō)我怎么就攤上這事×铊荆” “怎么了走敌?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)逗噩。 經(jīng)常有香客問(wèn)我掉丽,道長(zhǎng)跌榔,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任捶障,我火速辦了婚禮僧须,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘项炼。我一直安慰自己担平,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布芥挣。 她就那樣靜靜地躺著驱闷,像睡著了一般。 火紅的嫁衣襯著肌膚如雪空免。 梳的紋絲不亂的頭發(fā)上空另,一...
    開(kāi)封第一講書(shū)人閱讀 51,679評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音蹋砚,去河邊找鬼扼菠。 笑死,一個(gè)胖子當(dāng)著我的面吹牛坝咐,可吹牛的內(nèi)容都是我干的循榆。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼墨坚,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼秧饮!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起泽篮,我...
    開(kāi)封第一講書(shū)人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤盗尸,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后帽撑,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體泼各,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年亏拉,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了扣蜻。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡及塘,死狀恐怖莽使,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情磷蛹,我是刑警寧澤吮旅,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響庇勃,放射性物質(zhì)發(fā)生泄漏檬嘀。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一责嚷、第九天 我趴在偏房一處隱蔽的房頂上張望鸳兽。 院中可真熱鬧,春花似錦罕拂、人聲如沸揍异。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)衷掷。三九已至,卻和暖如春柿菩,著一層夾襖步出監(jiān)牢的瞬間戚嗅,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工枢舶, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留懦胞,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓凉泄,卻偏偏與公主長(zhǎng)得像躏尉,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子后众,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容