ConcurrentHashMap源码分析

来源:互联网 发布:中国历史教科书知乎 编辑:程序博客网 时间:2024/05/16 02:46

前言


ConcurrentHashMap是concurrent包里面出镜率很高的一个类,这个类是线程安全的Map,原来jdk1.8以前的ConcurrentHashMap采用的是锁分段机制来保证线程安全。

如果关注这个锁分段技术的,可以参考这篇博文:

http://blog.csdn.net/yansong_8686/article/details/50664351

至于我,可能就不详细说锁分段机制了,这篇文章主要针对的是JDK1.8中的ConcurrentHashMap的实现方法,锁分段技术,作为一种思想,我会简单说说我的理解,我也不知道全不全面,正不正确,也是一种理解吧。

先说说锁分段技术吧


先从一个问题说起,常常在面试中,都会问有关于HashMap的相关问题:HashTable和HashMap的区别是什么?

答案一般人可能会回答HashTable是线程安全的,HashMap不是线程安全的。

OK,这没错,那HashTable现在还在用吗?

不再用了。

为什么呢?

效率低。

效率怎么低了?

如果查看底层源码的时候,就会发现,它的多线程同步处理,简直简单粗暴。就是把所有的Map中的方法加上synchronized关键字。这样一来有什么问题呢?

这里写图片描述

一个线程正在做一个什么样的操作的话,其他的N个线程都要等待,这样的多线程在线程数非常大的情况下,根本就运转不开。那咋办?除了我们自己写,在jdk1.5之后, 出现的ConcurrentHashMap就提供的一种解决思路。

可以看到,下面这张图的话,就是1.8版本以前的分段加锁的机制, 这里的ConcurrentHashMap是分成了N个Segment,这个就是片段。每个Segment又有HashEntry的数组,数组又挂着链。因此,如果加锁的话,其实也就是加一个Segment的锁,对其他的Segment没有影响。这样的做法呢,实际上是把加锁的粒度变小了。锁还是加的,还是不是全加,加一部分。

这里写图片描述

JDK1.8有什么改变


从结构图上看,它放弃了原有的Segment的结构,直接用Entry数组的方式。

这里写图片描述

当然这个图,也不是特备完整,因为在JDK1.8中,当链表长度超过8的时候,链表会转换成红黑树。当然,这不在我们的ConcurrentHashMap的讨论范围之中,我们还是主要讨论线程安全怎么保证。

要探究,底层源码跑不掉

// 截取部分的ConcurrentHashMap的源码分析// put方法的底层是putVal方法public V put(K key, V value) {    return putVal(key, value, false);}// 这个方法是不能被overridefinal V putVal(K key, V value, boolean onlyIfAbsent) {        //  ConcurrentHashMap是不能put null的        if (key == null || value == null) throw new NullPointerException();        // 得到key对应的hash位置        int hash = spread(key.hashCode());        // 这个主要看到底一个index下挂的有几个Node,初始当然是没有Node        int binCount = 0;        // 无线循环        for (Node<K,V>[] tab = table;;) {            Node<K,V> f; int n, i, fh;            // 如果这个table都还没有初始化过            if (tab == null || (n = tab.length) == 0)                // 初始化table                tab = initTable();                // 如果index=hash%n的那个位置Node是空            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {                // 就把新的Node放到table的那个index处,如果是把新Node放到null的index处,不加锁。                if (casTabAt(tab, i, null,                             new Node<K,V>(hash, key, value, null)))                    break;                   // no lock when adding to empty bin            }            // 如果取出的那个Node正在移动,那就帮助扩容            else if ((fh = f.hash) == MOVED)                tab = helpTransfer(tab, f);            // 否则就是说呢hash为index的这个地方本来就有Node,到底是修改值,还是加链表            else {                V oldVal = null;                // 在这个地方加了锁的操作,这个粒度是table中的某个index取出来的Node,粒度很小。                synchronized (f) {                    // 下面的这两个if就是再次判断的作用,是不是那个Node,是不是那个hash值                    if (tabAt(tab, i) == f) {                        if (fh >= 0) {                            // 这个时候把binCount置为1,因为有一个Node嘛                            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)))) {                                    // value赋为新值,并且跳出循环                                    oldVal = e.val;                                    if (!onlyIfAbsent)                                        e.val = value;                                    break;                                }                                Node<K,V> pred = e;                                // 如果遍历完了还没找到                                if ((e = e.next) == null) {                                    // 把value变成链表的下一个Node,并跳出循环                                    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;                            }                        }                    }                }                // 如果这个链表的Node的个数大于8了,转成红黑树                if (binCount != 0) {                    if (binCount >= TREEIFY_THRESHOLD)                        treeifyBin(tab, i);                    if (oldVal != null)                        return oldVal;                    break;                }            }        }        // 这个方法主要做了两件事,第一Map中的元素个数+1,第二判断需不需要扩容,如果需要,扩容。        addCount(1L, binCount);        return null;    }

初始化table

private final Node<K,V>[] initTable() {        Node<K,V>[] tab; int sc;        while ((tab = table) == null || tab.length == 0) {     /**     * SizCtl的解释     * Table initialization and resizing control.  When negative, the     * table is being initialized or resized: -1 for initialization,     * else -(1 + the number of active resizing threads).  Otherwise,     * when table is null, holds the initial table size to use upon     * creation, or 0 for default. After initialization, holds the     * next element count value upon which to resize the table.     */                 // sc<0时,线程暂停,让CPU重新选择            if ((sc = sizeCtl) < 0)                Thread.yield(); // lost initialization race; just spin            // 如果>=0,看过我Atomic类介绍的亲是不是很熟悉这个方法,正式CAS方法实现同步            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {                try {                    // 再三确定table是空的                    if ((tab = table) == null || tab.length == 0) {                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;                        @SuppressWarnings("unchecked")                        // 按照初始值16去创建Node数组                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];                        table = tab = nt;                        // 此时sc=12                        sc = n - (n >>> 2);                    }                } finally {                    // sizeCtl=12,说明下次在size=12的时候,resize这个数组                    sizeCtl = sc;                }                break;            }        }        // 返回initial之后的table        return tab;    }

get方法

public V get(Object key) {        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;        //计算hash值        int h = spread(key.hashCode());        //根据hash值确定节点位置        if ((tab = table) != null && (n = tab.length) > 0 &&            (e = tabAt(tab, (n - 1) & h)) != null) {            //如果搜索到的节点key与传入的key相同且不为null,直接返回这个节点              if ((eh = e.hash) == h) {                if ((ek = e.key) == key || (ek != null && key.equals(ek)))                    return e.val;            }            //如果eh<0 说明这个节点在树上 直接寻找            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扩容机制并没有做详细的介绍,如果想进一步了解的朋友,可以看看参考文献中的文章,写的很好,受益良多。

参考文献


http://www.jianshu.com/p/e694f1e868ec
http://blog.csdn.net/u010723709/article/details/48007881
http://blog.csdn.net/yansong_8686/article/details/50664351

原创粉丝点击