jdk1.8之ConcurrentHashMap源码解析

说到ConcurrentHashMap,就会很自然地想到HashTable。有了HashTable为什么还需要ConcurrentHashMap呢?
原来虽然HashTable是多线程安全的,但是因为HashTable实现多线程的原理是通过给每一个put、get等函数都加上synchronized关键字。也就是说。同一个时刻,只能有一个线程去操作HashTable,另外一个想要并发操作HashTable的线程只能等待锁释放后才能操作线程,可见HashTable的效率十分低。为了解决HashTable这个低效问题所以出现了ConcurrentHashMap。
通俗地说,把HashTable理解为一个大仓库,这个大仓库里面只能有一个人(线程)进去,要等里面的人出来了才能进入下一个人(线程)。然后,ConcurrentHashMap也是一个大仓库,但是ConcurrentHashMap很智能,它给这个大仓库划分了很多小房间,每个小房间只能进入一个人(线程),要等房间里的人(线程)出来之后才能进入下一个人(线程),这样子就能多个想去不同房间的人(线程)能够同时进入大仓库了。所以ConcurrentHashMap明显比HashTable要高效得多。

源码解析

注意:此篇源码解析是基于jdk1.8
先研究一下concurrentHashMap.put()

public V put(K key, V value) {
		//其实调用putVal()
        return putVal(key, value, false);
    }

    /** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
    	//这里可以知道concurrentHashMap不能存放null的key或者null的value
        if (key == null || value == null) throw new NullPointerException();
        //对object中的hashCode进行二次运算,得到了key的哈希值
        int hash = spread(key.hashCode());
        int binCount = 0;
        //死循环
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            //如果数组为空或者数组长度为0,则初始化数组
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            // (n - 1) & hash算出的结果i是这个新插入的Node节点应该放在tab中的下标,如果tab中下标i上对应的元素为null,说明没有发生哈希冲突
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            	//用cas操作在tab的下标i处加入数据新的node,如果cas操作成功,可以直接break退出循环。
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            //执行到这里,说明发生了哈希冲突了,tab的i下标的地方已经存放了f了,fh==MOVE说明该节点有线程处理过了(这里有点疑问,先打个TODO标记)
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                //在这里上锁了,相当于给数组中的每一个下标位置上了锁(锁住的对象是这个数组上面每一个下标对应的对象)
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                    	//fh大于0,说明不是树节点
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                //如果发现链表中已经存放了相同的key
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                     //只要改变这个key对应的value就好了
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                //如果寻找到尾部都没有找到这个key,则在链表尾部插入这个node(这里体现了尾插入法)
                                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) {
                	//节点个数大于等于8,则变成红黑树
                    if (binCount >= TREEIFY_THRESHOLD)
                    	//tab中i下标转成红黑树
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        //不是很懂这个函数干嘛的,先打个问号(TODO)
        addCount(1L, binCount);
        return null;
    }

看一下spread(),函数的参数传入值是key.hashCode(),所以spread函数是对这个hash值的二次运算,(h ^ (h >>> 16))的处理和jdk1.8中的hashmap处理一样,但是多了一步&HASH_BITS的运算,其中HASH_BITS = 0x7fffffff,这显然有点像子网掩码一样的作用,把高位的去掉,留下低位。spread函数其实为了减少哈希冲突的。

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

看一下ConcurrentHashMap的get函数

public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        //根据key中的hashCode再经过扰动函数,算出h
        int h = spread(key.hashCode());
        //该key存放在table数组中的位置是:(n - 1) & h),
        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;
            }
            //eh小于0,则另外处理。看到find里面是对链表的遍历。为什么下面还会有while循环对链表进行遍历的呢?这里不是很懂,打个标记TODO
            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;
    }

为什么hash会有小于0这种情况的呢?

hash值大于等于0,则是链表节点 hash值为-1 MOVED,则是forwarding
nodes,存储nextTable的引用。只有table发生扩容的时候,ForwardingNode才会发挥作用,作为一个占位符放在table中表示当前节点为null或则已经被移动。
hash值为-2 TREEBIN,则是红黑树根,TreeBin类型 hash值为-3 RESERVED,则是reservation
nodes, static final int MOVED = -1; // hash for forwarding nodes
static final int TREEBIN = -2; // hash for roots of trees static
final int RESERVED = -3; // hash for transient reservations

看源码看到,对其中一段代码这里产生了一些疑问。eh小于0,则另外处理。看到find里面是对链表的遍历。为什么下面还会有while循环对链表进行遍历的呢?这里的代码不就是重复了吗?

else if (eh < 0)
    return (p = e.find(h, key)) != null ? p.val : null;
//这个while不就和上面调用的find函数重复了吗?,
while ((e = e.next) != null) {
    if (e.hash == h &&
        ((ek = e.key) == key || (ek != null && key.equals(ek))))
        return e.val;
}

接着看一下find函数,其实就是对链表进行了遍历。这个函数的注释的意思是:对map.get()的虚拟化支持;在子类覆盖。

/**
 * 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;
}

解决问题:又回去看了几遍源码,把问题给理清楚了。为什么还需要多出一个find函数呢?正如官方所说的

对map.get()的虚拟化支持;在子类覆盖。

eh是key对应的hash值,如果eh大于等于零的话,就会执行下面的那个while循环去执行里面的链表遍历操作。如果eh小于0,就要看tabAt(tab, (n - 1) & h))这语句代码返回来的Node对象是TreeBin还是Node了,如果是TreeBin,就会调用TreeBin中的find方法,如果还是Node的话,继续保持原来的执行操作。只能说,这么设计有点点巧妙了。
看一下TreeBin中的find函数

final Node<K,V> find(int h, Object k) {
			//key不能为null
            if (k != null) {
                for (Node<K,V> e = first; e != null; ) {
                    int s; K ek;
                    if (((s = lockState) & (WAITER|WRITER)) != 0) {
                        if (e.hash == h &&
                            ((ek = e.key) == k || (ek != null && k.equals(ek))))
                            return e;
                        e = e.next;
                    }
                    else if (U.compareAndSwapInt(this, LOCKSTATE, s,
                                                 s + READER)) {
                        TreeNode<K,V> r, p;
                        try {
                            p = ((r = root) == null ? null :
                                 r.findTreeNode(h, k, null));
                        } finally {
                            Thread w;
                            if (U.getAndAddInt(this, LOCKSTATE, -READER) ==
                                (READER|WAITER) && (w = waiter) != null)
                                LockSupport.unpark(w);
                        }
                        return p;
                    }
                }
            }
            return null;
        }

小结

其实,jdk1.8的concurrentHashMap的底层原理和jdk1.8的hashmap的相似度还是挺高的。concurrentHashMap其实也是一个数组,只不过数组里面存储的一个一个的链表(链表长度大于等于8的时候会变成红黑树,以提高查询效率)。使用put函数的过程就是:调用key的hashCode函数得到哈希值,然后将这个hash经过扰动函数spread函数处理(扰动函数是为了能够减少哈希值冲突的)。在通过((n - 1) & hash)这一步运算,因为n为2的次幂,所以((n - 1) & hash)相当于hash%n,这步计算结果就是这个包含key、value的entry该存储在数组中的位置,如果,那个位置上面已经存储有其他的元素了,说明就是发生哈希冲突了,jdk1.8的concurrentHashMap是使用链表法解决了这个哈希冲突的问题,就是使用尾插入法来将要插入的entry放在原来的的entry的后面,形成一个链表。如果链表的长度超过8的时候,会变成红黑苏,以提高查询效率。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值