ConcurrentHashMap源码查看

102 阅读7分钟

JDK1.7中的ConcurrentHashMap

  • JDK1.7中的ConcurrentHashMap采用了分段锁的形式, 每一段为一个Segment类, 它内部类似HashMap的结构, 内部有一个Entry数组, 数组的每个元素是一个链表。同时Segment类继承自ReentrantLock

  • 底层结构大致如图(省去红黑树的逻辑)

image.png

- 在HashEntry中采用了volatile来修饰了HashEntry的 `当前值``next元素`。所以 `get` 方法在获取数据的时候是不需要加锁的, 这样就大大的提升了执行效率。
- 在执行 `put()` 方法的时候会先尝试获取锁(`tryLock()`), 如果获取锁失败, 说明存在竞争, 那么将通过`scanAndLockForPut()`方法执行自旋, 当自旋次数达到`MAX_SCAN_RETRIES`时会执行阻塞锁, 直到获取锁成功。
static final int MAX_SCAN_RETRIES = Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
	// 首先尝试获取锁, 获取失败则执行自旋, 自旋次数超过最大次数后改为阻塞锁, 直到获取锁成功;
     HashEntry<K,V> node = tryLock() ? null :
         //  CAS失败, 自旋 + lock实现逻辑;
         scanAndLockForPut(key, hash, value);
       //  CAS成功, 走下边流程;
     V oldValue;
     try {
         HashEntry<K,V>[] tab = table;
         //    计算哈希码;
         int index = (tab.length - 1) & hash;
         //    拿到 segment 首节点;
         HashEntry<K,V> first = entryAt(tab, index);
         //    遍历链表;
         for (HashEntry<K,V> e = first;;) {
             if (e != null) {
                 K k;
                 // 如果 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;
             }
             //   key 不存在, 进行队列节点插入;
             else {
                 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;    
                 ++modCount;
                 count = c;
                 oldValue = null;
                 break;
             }
         }
     } finally {
         //   你懂的;
         unlock();
     }
     return oldValue;
 }

JDK1.8后的ConcurrentHashMap

  • 在JDK1.8中, 放弃了Segment这种分段锁的形式, 而是采用了 CAS + Synchronized 的方式来保证并发操作, 采用了和HashMap一样的底层数据结构, 直接用数组加链表, 在链表长度大于8的时候为了提高查询效率会将链表转为红黑树(链表定位数据的时间复杂度为O(N), 红黑树定位数据的时间复杂度为O(logN))。
  • 在代码上也和JDK1.8的HashMap很像, 也是将原先的HashEntry改为了Node类, 但还是使用了volatile 修饰了 当前值next的值。从而保证了在获取数据时候的高效。
  • JDK1.8中的ConcurrentHashMap在执行 put() 方法的时候还是有些复杂的, 主要是为了保证线程安全才做了一系列的措施。
final V putVal(K key, V value, boolean onlyIfAbsent) {

    // 防止并发环境下歧义, 禁止空的键或值;
    if (key == null || value == null) throw new NullPointerException();
    
    // hash计算;
    int hash = spread(key.hashCode());
    
    // binCount 表示一个桶中的节点数量, 用于判断是否需要 链表 <-> 红黑树的转换;
    int binCount = 0;
    
    // 遍历所有的桶;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh; K fk; V fv;
        
        // 如果桶数组没有初始化, 则初始化;
        if (tab == null || (n = tab.length) == 0)
        
            // 初始化后重回遍历逻辑;
            tab = initTable();
            
            // 如果哈希码对应位无数据;
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
        
            // 如果CAS处理成功, 跳出循环;
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
                break;                   // no lock when adding to empty bin
        }
        
        // CAS失败, 说明存在竞争, 使用synchronized加锁;
        // 当前hash码 == moved(-1) 时候, 需要扩容;
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else if (onlyIfAbsent // check first node without acquiring lock
                 && fh == hash
                 && ((fk = f.key) == key || (fk != null && key.equals(fk)))
                 && (fv = f.val) != null)
            return fv;
            //  上述不成立时候, 加锁处理;
        else {
            V oldVal = null;
            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);
                                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;
                        }
                    }
                    else if (f instanceof ReservationNode)
                        throw new IllegalStateException("Recursive update");
                }
            }
            
            //  链表 <-> 红黑树;
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    
    // 统计, 走另外统计逻辑, 及时释放桶;
    addCount(1L, binCount);
    return null;
}
- 第一步通过key进行hash;
- 第二步判断是否需要初始化数据结构;
- 第三步根据 key 定位到当前 `Node`, 如果当前位置为空, 则可以写入数据, 利用 `CAS` 机制尝试写入数据, 如果写入失败, 说明存在竞争, 将会通过自旋来保证成功;
- 第四步如果当前的 哈希码 等于 `MOVED` 则需要进行扩容(扩容时也使用了`CAS`来保证了线程安全)。
- 第五步如果上面四步都不满足, 那么则通过 `synchronized` 阻塞锁将数据写入;
- 第六步如果数据量大于 `TREEIFY_THRESHOLD` 时需要转换成红黑树(默认为8);
  • JDK1.8的ConcurrentHashMap的 get() 方法就还是比较简单:
    • 根据 key 的 哈希码 寻址到具体的桶上;
    • 如果是红黑树则按照红黑树的方式去查找数据;
    • 如果是链表就按照遍历链表的方式去查找数据;
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;
 }
  • ConcurrentHashMap的 size() 方法
    • JDK1.7中的ConcurrentHashMap的size方法, 计算size的时候会先不加锁获取一次数据长度,然后再获取一次, 最多三次。比较前后两次的值, 如果相同的话说明不存在竞争的编辑操作, 就直接把值返回就可以了。但是如果前后获取的值不一样, 那么会将每个Segment都加上锁, 然后计算ConcurrentHashMap的size值。

    • JDK1.8中的ConcurrentHashMap的 size() 方法的源码如下:

public int size() {
    long n = sumCount();
    return ((n < 0L) ? 0 :
            (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
            (int)n);
}
// 这个方法最大会返回int的最大值, 但是 ConcurrentHashMap 的长度有可能超过int的最大值。
  • 在JDK1.8中增加了 mappingCount() 方法, 这个方法的返回值是long类型的, 所以JDK1.8以后更推荐用这个方法获取Map中数据的数量。
/**
 * @return the number of mappings
 * @since 1.8
 */
 public long mappingCount() {
     long n = sumCount();
     return (n < 0L) ? 0L : n; // ignore transient negative values
 }
  • 无论是size()方法还是mappingCount()方法,核心方法都是sumCount()方法。
final long sumCount() {
     CounterCell[] as = counterCells; CounterCell a;
     long sum = baseCount;
     if (as != null) {
         for (int i = 0; i < as.length; ++i) {
             if ((a = as[i]) != null)
                 sum += a.value;
         }
     }
     return sum;
 }
  • baseCount 是一个volatile变量, 那么我们来看在 put() 方法执行时是如何使用baseCount的? 在 put 方法的最后一段代码中会调用 addCount() 方法, 而addCount()方法的源码如下:
private final void addCount(long x, int check) {
    CounterCell[] cs; long b, s;
    // 首先对baseCount进行CAS自增操作;
    if ((cs = counterCells) != null ||
        !U.compareAndSetLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        
        // 如果并发导致了baseCount的 CAS自增 失败了, 则使用 counterCells 进行 CAS;
        CounterCell c; long v; int m;
        boolean uncontended = true;
        if (cs == null || (m = cs.length - 1) < 0 ||
            (c = cs[ThreadLocalRandom.getProbe() & m]) == null ||
            !(uncontended =
              U.compareAndSetLong(c, CELLVALUE, v = c.value, v + x))) {
              
              // 如果 counterCells 的 CAS 也失败了, 那么则进入`fullAddCount()`方法, `fullAddCount()`方法中会进入死循环, 直到成功为止;
            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) << RESIZE_STAMP_SHIFT;
            if (sc < 0) {
                if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
                    (nt = nextTable) == null || transferIndex <= 0)
                    break;
                if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt);
            }
            else if (U.compareAndSetInt(this, SIZECTL, sc, rs + 2))
                transfer(tab, null);
            s = sumCount();
        }
    }
}
private final void fullAddCount(long x, boolean wasUncontended) {
    int h;
    if ((h = ThreadLocalRandom.getProbe()) == 0) {
        ThreadLocalRandom.localInit();      // force initialization
        h = ThreadLocalRandom.getProbe();
        wasUncontended = true;
    }
    boolean collide = false;                // True if last slot nonempty
    
    //  死循环, 直到成功;
    for (;;) {
        CounterCell[] cs; CounterCell c; int n; long v;
        if ((cs = counterCells) != null && (n = cs.length) > 0) {
            if ((c = cs[(n - 1) & h]) == null) {
                if (cellsBusy == 0) {            // Try to attach new Cell
                    CounterCell r = new CounterCell(x); // Optimistic create
                    if (cellsBusy == 0 &&
                        U.compareAndSetInt(this, CELLSBUSY, 0, 1)) {
                        boolean created = false;
                        try {               // Recheck under lock
                            CounterCell[] rs; int m, j;
                            if ((rs = counterCells) != null &&
                                (m = rs.length) > 0 &&
                                rs[j = (m - 1) & h] == null) {
                                rs[j] = r;
                                created = true;
                            }
                        } finally {
                            cellsBusy = 0;
                        }
                        if (created)
                            break;
                        continue;           // Slot is now non-empty
                    }
                }
                collide = false;
            }
            else if (!wasUncontended)       // CAS already known to fail
                wasUncontended = true;      // Continue after rehash
            else if (U.compareAndSetLong(c, CELLVALUE, v = c.value, v + x))
                break;
            else if (counterCells != cs || n >= NCPU)
                collide = false;            // At max size or stale
            else if (!collide)
                collide = true;
            else if (cellsBusy == 0 &&
                     U.compareAndSetInt(this, CELLSBUSY, 0, 1)) {
                try {
                    if (counterCells == cs) // Expand table unless stale
                        counterCells = Arrays.copyOf(cs, n << 1);
                } finally {
                    cellsBusy = 0;
                }
                collide = false;
                continue;                   // Retry with expanded table
            }
            h = ThreadLocalRandom.advanceProbe(h);
        }
        else if (cellsBusy == 0 && counterCells == cs &&
                 U.compareAndSetInt(this, CELLSBUSY, 0, 1)) {
            boolean init = false;
            try {                           // Initialize table
                if (counterCells == cs) {
                    CounterCell[] rs = new CounterCell[2];
                    rs[h & 1] = new CounterCell(x);
                    counterCells = rs;
                    init = true;
                }
            } finally {
                cellsBusy = 0;
            }
            if (init)
                break;
        }
        else if (U.compareAndSetLong(this, BASECOUNT, v = baseCount, v + x))
            break;                          // Fall back on using base
    }
}
  • 无论是JDK1.7还是JDK1.8中, ConcurrentHashMap的 size() 方法都是线程安全的, 都是准确的计算出实际的数量, 但是这个数据在并发场景下是随时都在变的。