[java] ConcurrentHashMap

在分析ConcurrentHashMap的实现原理之前,先来介绍一下hashmap以及hashtable的特点和可能存在的问题.

TreeMap

//todo

HashTable

hashTable,它是线程安全的,它在所有涉及到多线程操作的都加上了synchronized关键字来锁住整个table,这就意味着所有的线程都在竞争一把锁,在多线程的环境下,它是安全的,但是无疑是效率低下的,尤其是在过去synchronized比较重的时候。

但是在jdk1.5之后,synchronized关键字的效率得到了很大提升,因此不确定HashTable的效率是否还像以前那样所说的低下。

ConcurrentHashMap

jdk1.7 基于分段锁

在jdk1.7之中,ConcurrentHashMap使用的是分段锁技术实现,把HashMap分割成若干个Segment,在put的时候需要锁住Segment,get时候不加锁,使用volatile来保证可见性。在concurrentHashMap中存放数据的是一个Node<K,V>的内部类,而存放所有数据的就是这些Node组合起来的一个table,它的定义如下:

/**
     * The array of bins. Lazily initialized upon first insertion.
     * Size is always a power of two. Accessed directly by iterators.
     */
    transient volatile Node<K,V>[] table;

使用了volatile修饰,保证可见性。

当要统计全局时(比如size),首先会尝试多次计算modcount来确定,这几次尝试中,是否有其他线程进行了修改操作,如果没有,则直接返回size。如果有,则需要依次锁住所有的Segment来计算。下图是ConcurrentHashMap的一个结构图:

jdk1.8 基于CAS

jdk7中ConcurrentHashmap中,当长度过长碰撞会很频繁,链表的增改删查操作都会消耗很长的时间,影响性能,所以jdk8 中完全重写了concurrentHashmap,代码量从原来的1000多行变成了 6000多 行,实现上也和原来的分段式存储有很大的区别。在jdk1.8之中,完全摒弃了segment的实现,而是借鉴了Hashmap的实现,也采用了数组加链表加红黑树的实现,并且也是在链表长度大于8时转换为红黑树。其结构大致如下:

put时不是锁住segment而是锁住一个具体的node,减小锁粒度,提高了效率。除此之外,put时使用的一个重要的思想就是CAS思想. put方法的源代码如下:

 /** Implementation for put and putIfAbsent */
    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<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            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, null);
                                    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;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

在for循环之前,他会根据key去计算一个int类型的hash值,计算的方式如下:

  static final int spread(int h) {
        return (h ^ (h >>> 16)) & HASH_BITS;
    }

最关键的逻辑在for循环遍历整个table里面,有一个if,两个else if,一个else,一共四个处理分支我们一个一个来看。

第一个if的判断条件是: if (tab == null || (n = tab.length) == 0). tab就是指的意思是当前遍历的这个table,如果当前这个table为空或者这个table的长度是0,那么就先初始化这个table。初始化table调用的方法为initTable,代码如下:

 /**
     * Initializes table, using the size recorded in sizeCtl.
     */
    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

这里是使用了CAS来保证只有一个线程能够实例化成功table. 他的实现调用了来自于类sun.misc.Unsafe. 在这个Unsafe类中它是一个native方法。关于CAS的内容请参见另一篇博客。

第二个else if 的判断条件是: else if ((f = tabAt(tab, i = (n - 1) & hash)) == null). 

注意,f已经在此处复制。这里的hash是一个int类型的值,它事实上就是put方法的参数key,更准确的说是是根据key计算出的hash值,这里判断的是根据这个key的hash值去当前table中找对应的元素,若找到的为空,则证明当前key在map里面是没有映射的。根据hash值去当前table中找元素的方法是tabAt,它的定义如下:

 static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
        return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
    }

这里获取数组中对应元素使用的方法也是来组Unsafe类的方法getObjectVolatile, 我们应该都知道,在数组中知道下标之后,可以直接使用table[index]就能获得对应的元素。那这里为什么要这么做呢?并且,该table已经是被volatile修饰了,这意味着它的任何改动应该都是立刻可的值得。暂且先放过这个问题留待之后回答。

那么现在就直接插入吗?这样想就错了,因为我们必须考虑并发,在这里若判断成立,则说明当前位置为空是第一次插入元素(这里在jdk源代码里的注释很有意思:// no lock when adding to empty bin),它则接着调用了一个方法:casTabAt(),这个方法的定义如下,这个方法的实现也是使用CAS的方式来进行put,与上面其实是一样的。使用了compareAndSwapObject方法插入Node节点。

  static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                        Node<K,V> c, Node<K,V> v) {
        return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
    }

若CAS成功,则说明Node插入成功,则break,然后调用addCount(1L,binCount)方法检查当前容量是否需要扩容

若CAS失败,说明在这个插入之前其他线程提前插入了节点,则自旋重新尝试在这个位置插入节点。问题:这个自旋锁是如何实现的?目前暂时没有看到解释。

第三个else if 的判断条件是:((fh = f.hash) == MOVED) 

注意,fh变量已经在此处赋值。MOVED的定义如下:

 static final int MOVED     = -1; // hash for forwarding nodes

这里的意思是若f的hash值为-1,说明当前f是一个ForwadingNode节点,有其他线程正在扩容,则一起进行扩容,调用方法helpTransfer

第四个else 就是除了上面三个判断条件的其他所有情况

其余情况把新的Node节点按链表或红黑树的方式插入到合适的位置,这个过程采用同步内置锁实现并发。具体代码如下:

 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, null);
                                    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;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }

可以看到在这里面首先是对f加上了同步锁(注意,f在第二个判断时,若不为null,则已经进行赋值),然后又分了两种情况,第一是普通链表时的put,第二是红黑树的put操作(若f是TreeBin类型节点,说明f是红黑树的节点)。在put操作里会根据是否已经有值决定是替换还是新增一个Node。

最后在put插座结束之后,判断binCount >= TREEIFY_THRESHOLD,即当节点数大于等于8时,就执行treeifyBin方法,把链表转换为红黑数结构。代码如下:

 /**
     * Replaces all linked nodes in bin at given index unless table is
     * too small, in which case resizes instead.
     */
    private final void treeifyBin(Node<K,V>[] tab, int index) {
        Node<K,V> b; int n, sc;
        if (tab != null) {
            if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
                tryPresize(n << 1);
            else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
                synchronized (b) {
                    if (tabAt(tab, index) == b) {
                        TreeNode<K,V> hd = null, tl = null;
                        for (Node<K,V> e = b; e != null; e = e.next) {
                            TreeNode<K,V> p =
                                new TreeNode<K,V>(e.hash, e.key, e.val,
                                                  null, null);
                            if ((p.prev = tl) == null)
                                hd = p;
                            else
                                tl.next = p;
                            tl = p;
                        }
                        setTabAt(tab, index, new TreeBin<K,V>(hd));
                    }
                }
            }
        }
    }

关于putIfAbsent方法,他的实现和put是一样的,只是如果(调用该方法时)key-value 已经存在,则返回那个 value 值。如果调用时 map 里没有找到 key 的 mapping,返回一个 null 值

扩容

当table容量不足的时候,即table的元素数量达到容量阈值sizeCtl,需要对table进行扩容。
整个扩容分为两部分:

  1. 构建一个nextTable,大小为table的两倍。
  2. 把table的数据复制到nextTable中。

代码大致如下:

private final void addCount(long x, int check) {
    ...
    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);
            if (sc < 0) {
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                    transferIndex <= 0)
                    break;
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt);
            }
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                transfer(tab, null);
            s = sumCount();
        }
    }
}

图片来源于:https://segmentfault.com/p/1210000010020931/read

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值