ConcurrentHashMap源码阅读

一、ConcurrentHashMap与HashMap、HashTable的区别

1.HashMap

我们知道HashMap是线程不安全的,在多线程环境下,使用HashMap进行put操作有可能引起数据丢失,也有可能因为扩容而导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。

2.HashTable

Hashtable和HashMap的实现原理几乎一样,差别无非是
(1)HashTable不允许key和value为null,而HashMap可以
(2)HashTable是线程安全的,HashMap是线程不安全的
(3)HashTable是JDK1.0就存在的,HashMap是JDK1.2才引入的
此外,HashTable线程安全的策略实现代价却太大了,简单粗暴,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁,如下图所示:
ConcurrentHashMap源码阅读_第1张图片
HashTable和HashMap都存在一些问题,因此就出现了ConcurrentHashMap来解决并发问题并且比HashTable有更好的性能。

二、ConcurrentHashMap在JDK1.7的实现

JDK1.7版本: 容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这 样便可以有效地提高并发效率。这就是ConcurrentHashMap所采用的"分段锁"思想,如图:
ConcurrentHashMap源码阅读_第2张图片
JDK1.7版本的ConcurrentHashMap源码如下:

public class ConcurrentHashMap extends AbstractMap
        implements ConcurrentMap, Serializable {

    // 将整个hashmap分成几个小的map,每个segment都是一个锁,每个segment都是一个HashMap;
    // 与hashtable相比,这么设计的目的是对于put, remove等操作,可以减少并发冲突,对
    // 不属于同一个片段的节点可以并发操作,大大提高了性能
    final Segment[] segments;

    // 本质上Segment类就是一个小的hashmap,里面table数组存储了各个节点的数据,继承了ReentrantLock, 也是一个锁
    static final class Segment extends ReentrantLock implements Serializable {
        transient volatile HashEntry[] table;
        transient int count;
    }

    // 基本节点,存储Key, Value值
    static final class HashEntry {
        final int hash;
        final K key;
        volatile V value;
        volatile HashEntry next;
    }
}

ConcurrentHashMap中的分段锁称为Segment,每一段内部即类似于HashMap的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表,同时又是一个ReentrantLock。ConcurrentHashMap将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,能够实现真正的并发访问。当然如果它需要定位一个元素,就需要两次hash操作,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部。
ConcurrentHashMap这样设计的坏处是:
这一种结构的带来的副作用是Hash的过程要比普通的HashMap要长。
ConcurrentHashMap这样设计的好处是:
写操作的时候可以只对元素所在的Segment进行加锁即可,不会影响到其他的Segment,这样,在最理想的情况下,ConcurrentHashMap可以最高同时支持Segment数量大小的写操作(刚好这些写操作都非常平均地分布在所有的Segment上)。
所以,通过这一种结构,ConcurrentHashMap的并发能力可以大大的提高。

三、ConcurrentHashMap在JDK1.8的实现

1、取消segments字段,直接采用transient volatile HashEntry[] table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,并发控制使用Synchronized和CAS来操作
2、将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构
3、JDK8中彻底放弃了Segment转而采用的是Node,其设计思想也不再是JDK1.7中的分段锁思想
源码如下:

// 数组中存储的元素Node,和HashMap差不多
static class Node implements Map.Entry {
    final int hash;    //key的hash值
    final K key;       //key
    //val和next都会在扩容时发生变化,所以加上volatile来保持可见性和禁止重排序
    volatile V val;   //get操作全程不需要加锁是因为Node的成员val是用volatile修饰
    volatile Node next;     //表示链表中的下一个节点,数组用volatile修饰主要是保证在数组扩容的时候保证可见性
    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; }
    //不允许更新value 
    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)));
    }
    //用于map中的get()方法,子类重写
    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;
    }
}
// put方法
final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();//K,V都不能为空,否则的话跑出异常
        int hash = spread(key.hashCode());    //取得key的hash值
        int binCount = 0;    //用来计算在这个节点总共有多少个元素,用来控制扩容或者转移为树
        for (Node[] tab = table;;) {    //
            Node f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)    
                tab = initTable();    //第一次put的时候table没有初始化,则初始化table
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {  
            	// 通过哈希计算出一个表中的位置因为n是数组的长度,所以(n-1)&hash肯定不会出现数组越界
            	// 如果这个位置没有元素的话,则通过CAS的方式尝试添加,注意这个时候是没有加锁的
            	// 创建一个Node添加到数组中区,null表示的是下一个节点为空
                if (casTabAt(tab, i, null, new Node(hash, key, value, null)))  
                    break;                  
            }
            else if ((fh = f.hash) == MOVED)
            	// 如果检测到某个节点的hash值是MOVED,则表示正在进行数组扩张的数据复制阶段,
            	// 则当前线程也会参与去复制,通过允许多线程复制的功能,一次来减少数组的复制所带来的性能损失
                tab = helpTransfer(tab, f);
            else {
                /*
                 * 如果在这个位置有元素的话,就采用synchronized的方式加锁,
                 * 如果是链表的话(hash大于0),就对这个链表的所有元素进行遍历,
                 * 如果找到了key和key的hash值都一样的节点,则把它的值替换到
                 * 如果没找到的话,则添加在链表的最后面
                 *  否则,是树的话,则调用putTreeVal方法添加到树中去
                 *  在添加完之后,会对该节点上关联的的数目进行判断,
                 *  如果在8个以上的话,则会调用treeifyBin方法,来尝试转化为树,或者是扩容
                 */
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {        //再次取出要存储的位置的元素,跟前面取出来的比较
                        if (fh >= 0) {                //取出来的元素的hash值大于0,当转换为树之后,hash值为-2
                            binCount = 1;            
                            for (Node e = f;; ++binCount) {    //遍历这个链表
                                K ek;
                                if (e.hash == hash &&        //要存的元素的hash,key跟要存储的位置的节点的相同的时候,替换掉该节点的value即可
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)        //当使用putIfAbsent的时候,只有在这个key没有设置值得时候才设置
                                        e.val = value;
                                    break;
                                }
                                Node pred = e;
                                if ((e = e.next) == null) {    //如果不是同样的hash,同样的key的时候,则判断该节点的下一个节点是否为空,
                                    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,    //调用putTreeVal方法,将该元素添加到树中去
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)    //当在同一个节点的数目达到8个的时候,则扩张数组或将给节点的数据转为tree
                        treeifyBin(tab, i);    
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);    //计数
        return null;
    }
/**
 *帮助从旧的table的元素复制到新的table中
 */
final Node[] helpTransfer(Node[] tab, Node f) {
    Node[] nextTab; int sc;
    if (tab != null && (f instanceof ForwardingNode) &&
        (nextTab = ((ForwardingNode)f).nextTable) != null) { //新的table nextTab已经存在前提下才能帮助扩容
        int rs = resizeStamp(tab.length);
        while (nextTab == nextTable && table == tab &&
               (sc = sizeCtl) < 0) {
            if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                sc == rs + MAX_RESIZERS || transferIndex <= 0)
                break;
             // 对现在的表长度做操作,如果没有改动,说明其他线程没有在扩容。
             // 如果其他线程在扩容,那么当前线程就不处理了。CAS保证原子性
            if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                transfer(tab, nextTab);//调用扩容方法
                break;
            }
        }
        return nextTab;
    }
    return table;
}
	/**相比put方法,get就很单纯了,支持并发操作,
     * 当key为null的时候回抛出NullPointerException的异常
     * get操作通过首先计算key的hash值来确定该元素放在数组的哪个位置
     * 然后遍历该位置的所有节点
     * 如果不存在的话返回null
     * 和HashMap的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;
}

ConcurrentHashMap总结:
其实可以看出JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发,从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树。

1.数据结构:取消了Segment分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。

2.保证线程安全机制:JDK1.7采用segment的分段锁机制实现线程安全,其中segment继承自ReentrantLock。JDK1.8采用CAS+Synchronized保证线程安全。

3.锁的粒度:原来是对需要进行数据操作的Segment加锁,现调整为对每个数组元素加锁(Node)。

4.链表转化为红黑树:定位结点的hash算法简化会带来弊端,Hash冲突加剧,因此在链表节点数量大于8时,会将链表转化为红黑树进行存储。

5.查询时间复杂度:从原来的遍历链表O(n),变成遍历红黑树O(logN)。

参考转载于:
https://youzhixueyuan.com/concurrenthashmap.html
https://blog.csdn.net/weixin_43185598/article/details/87938882

你可能感兴趣的:(Android源码,HashMap,Concurrent,并发Map)