ConcurrentHashMap源码浅析 Base 1.8

188 阅读4分钟

JDK版本1.8

数据结构
底层实现
初始容量
扩容
负载因子(默认值)
线程安全
链表插入值方法
ConcurrentHashMap
Node数组+链表/红黑树
16
采用多线程扩容,整个扩容过程,通过CAS设置sizeCtl,transferIndex等变量协调多个线程进行并发扩容
0.75
是(采用CAS + synchronized
保证并发安全)
尾插法

ConcurrentHashMap

1.7 已经解决了并发问题,并且能支持 N 个 Segment 这么多次数的并发,但依然存在 HashMap 在 1.7 版本中的问题。
那就是查询遍历链表效率太低
因此 1.8 做了一些数据结构上的调整,首先来看下底层的组成结构:

  • key和value都不能为null
  • 其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性
  • 链表的对象是用Node对象
  • 红黑树是使用TreeBin对象,TreeBin对象中包含TreeNode对象属性,使用TreeBin对象包裹TreeNode对象是为了保证红黑树在操作的时候将锁加载TreeBin对象上保证锁的一致性,因为如果将锁加在TreeNode对象上可能会出现红黑树操作时根节点发生变化,导致旧的根节点上的锁变成了子节点锁,这样就失去了红黑树锁的作用
  • 初始化initTable时使用synchronized锁保证多线程并发安全
  • ConcurrentHashMap的数量计数方式是通过basecount属性来计算个数,不过在多线程并发的时候会通过CounterCell数组来将计数的basecount属性的锁的竞争分散开,例如一个线程竞争到了basecount的锁,这个时候其他线程就不会在去竞争basecount的锁,而是去生成一个随机数和CounterCell数组的length-1进行&操作得到一个CounterCell的数组下标,将CounterCell里的Value值+1,如图:

源码解析

1.初识ConcurrentHashMap实现

static class Node implements Map.Entry {
        final int hash;
        final K key;
        volatile V val;
        volatile Node next;

        Node(int hash, K key, V val, Node 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 find(int h, Object k) {
            Node 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;
        }
    }
  • 其中将 1.7 中存放数据的 HashEntry 改为 Node,但作用都是相同的
  • 其中的 val next 都用了 volatile 修饰,保证了可见性

2.put方法

2-1 put方法一览

final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node[] tab = table;;) {   //根据 key 计算出 hashcode
            Node f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)   //判断是否需要进行初始化
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {   //f 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功
                if (casTabAt(tab, i, null,
                             new Node(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)    //如果当前位置的 hashcode == MOVED == -1,则需要进行扩容
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                synchronized (f) {    //如果都不满足,则利用 synchronized 锁写入数据
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node 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 pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {
                            Node p;
                            binCount = 2;
                            if ((p = ((TreeBin)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)   //如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

put方法大致过程如下:

  • 根据 key 计算出 hashcode
  • 判断是否需要进行初始化
  • f 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功
  • 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容
  • 如果都不满足,则利用 synchronized 锁写入数据
  • 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树

2.get方法

2-1 get方法一览

public V get(Object key) {
        Node[] tab; Node 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方法大致过程如下:

  • 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值
  • 如果是红黑树那就按照树的方式获取值
  • 如果不满足红黑树那就按照链表的方式遍历获取值
1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(O(logn)),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。