ConcurrentHashMap

1.什么是ConcurrentHashMap

ConcurrentHashMap是java.util.concurrent包下AbstractMap的一个子类,此类遵守与Hashtable相同的功能规范,并且包括对应于Hashtable的每个方法的方法版本。

2.ConcurrentHashMap的设计思路

在JDK1.7中,ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。

在JDK1.8中,取消了Segment分段锁的数据结构,取而代之的是数组+链表(红黑树)的结构。对每个数组元素加锁。

3.ConcurrentHashMap的方法

Get方法:

//JDK1.7 get方法
public V get(Object key) {
    Segment s; 
    HashEntry[] tab;
    int h = hash(key); //找出对应的segment的位置
    long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
    if ((s = (Segment)UNSAFE.getObjectVolatile(segments, u)) != null &&
        (tab = s.table) != null) {  //使用Unsafe获取对应的Segmen
        for (HashEntry e = (HashEntry) UNSAFE.getObjectVolatile
                 (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
             e != null; e = e.next) { //找出对应的HashEntry,从头开始遍历
            K k;
            if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                return e.value;
        }
    }
    return null;
}

1.为输入的Key做Hash运算,得到hash值。
2.通过hash值,定位到对应的Segment对象。
3.再次通过hash值,定位到Segment当中数组的具体位置。

//JDK1.8 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;
    }

1.首先定位到table[]中的i。
2.若table[i]存在,则继续查找。
3.首先比较链表头部,如果是则返回。
4.然后如果为红黑树,查找树。
5.最后再循环链表查找。

Put方法:

//JDK1.7 put方法
//将一个HashEntry放入到该Segment中,使用自旋机制,减少了加锁的可能性
   final V put(K key, int hash, V value, boolean onlyIfAbsent) {
        HashEntry node = tryLock() ? null :
            scanAndLockForPut(key, hash, value); //如果加锁失败,则调用该方法
        V oldValue;
        try {
            HashEntry[] tab = table;
            int index = (tab.length - 1) & hash; //同hashMap相同的哈希定位方式
            HashEntry first = entryAt(tab, index);
            for (HashEntry e = first;;) {
                if (e != null) { 
            //若不为null,则持续查找,知道找到key和hash值相同的节点,将其value更新
                    K k;
                    if ((k = e.key) == key ||
                        (e.hash == hash && key.equals(k))) {
                        oldValue = e.value;
                        if (!onlyIfAbsent) {
                            e.value = value;
                            ++modCount;
                        }
                        break;
                    }
                    e = e.next;
                }
                else { //若头结点为null
                    if (node != null) //在遍历key对应节点链时没有找到相应的节点
                        node.setNext(first);
                        //当前修改并不需要让其他线程知道,在锁退出时修改自然会
                        //更新到内存中,可提升性能
                    else
                        node = new HashEntry(hash, key, value, first);
                    int c = count + 1;
                    if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                        rehash(node); //如果超过阈值,则进行rehash操作
                    else
                        setEntryAt(tab, index, node);
                    ++modCount;
                    count = c;
                    oldValue = null;
                    break;
                }
            }
        } finally {
            unlock();
        }
        return oldValue;
    }

1.为输入的Key做Hash运算,得到hash值。
2.通过hash值,定位到对应的Segment对象。
3.获取可重入锁。
4.再次通过hash值,定位到Segment当中数组的具体位置。
5.插入或覆盖HashEntry对象。
6.释放锁。

//JDK1.8 put方法

 public V put(K key, V value) {
        return putVal(key, value, false);
    }

    /** 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[] tab = table;;) {
            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) {
                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)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                synchronized (f) {
                    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)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

1.参数校验。
2.若table[]未创建,则初始化。
3.当table[i]后面无节点时,直接创建Node(无锁操作)。
4.如果当前正在扩容,则帮助扩容并返回最新table[]。
5.然后在链表或者红黑树中追加节点。
6.最后还回去判断是否到达阀值,如到达变为红黑树结构。

Size方法:

//JDK1.7 size方法,求出所有的HashEntry的数目
    public int size() {
        final Segment[] segments = this.segments;
        int size;
        boolean overflow; // true if size overflows 32 bits
        long sum;         // sum of modCounts
        long last = 0L;   // previous sum
        int retries = -1; // first iteration isn't retry
        try {
            for (;;) {
                if (retries++ == RETRIES_BEFORE_LOCK) {
                    for (int j = 0; j < segments.length; ++j)
                        ensureSegment(j).lock(); // force creation
                }
                sum = 0L;
                size = 0;
                overflow = false;
                for (int j = 0; j < segments.length; ++j) {
                    Segment seg = segmentAt(segments, j);
                    if (seg != null) {
                        sum += seg.modCount;
                        int c = seg.count;
                        if (c < 0 || (size += c) < 0)
                            overflow = true;
                    }
                }
                if (sum == last)
                    break;
                last = sum;
            }
        } finally {
            if (retries > RETRIES_BEFORE_LOCK) {
                for (int j = 0; j < segments.length; ++j)
                    segmentAt(segments, j).unlock();
            }
        }
        return overflow ? Integer.MAX_VALUE : size;
    }

1.遍历所有的Segment。
2.把Segment的元素数量累加起来。
3.把Segment的修改次数累加起来。
4.判断所有Segment的总修改次数是否大于上一次的总修改次数。如果大于,说明统计过程中有修改,重新统计,尝试次数+1;如果不是。说明没有修改,统计结束。
5.如果尝试次数超过阈值,则对每一个Segment加锁,再重新统计。
6.再次判断所有Segment的总修改次数是否大于上一次的总修改次数。由于已经加锁,次数一定和上次相等。
7.释放锁,统计结束。

//JDK1.8 size方法
    public int size() {
        long n = sumCount();
        return ((n < 0L) ? 0 :
                (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
                (int)n);
    }
    // 1.8加入的API  
    public long mappingCount() {  
        long n = sumCount();  
        return (n < 0L) ? 0L : n; // ignore transient negative values  
    }  

    final long sumCount() {  
        CounterCell[] as = counterCells; CounterCell a;  
        long sum = baseCount;  
        if (as != null) {  
            for (int i = 0; i < as.length; ++i) {  
                if ((a = as[i]) != null)  
                    sum += a.value;  
            }  
        }  
        return sum;  
    }  

1.每个table[i]都有一个CounterCell与之对应。
2.把所有的table[i] 加在一起。

4.ConcurrentHashMap和HashMap以及HashTable的区别

HashMap: 线程不安全,在多线程情况下,HashMap在做put操作时,会在扩容时形成环状链表,这样在进行get操作时会引起死循环;HashMap的key值和value值都可以是null;

HashTable: HashTable和HashMap的实现原理几乎一样,HashTable是线程安全的,但是实现原理是在整个方法上加锁,这样导致性能非常差;HashTable不允许key和value为null;

ConcurrentHashMap:所采用的"分段锁"思想,不会存在锁竞争问题,可以提高效率

5.总结

ConcurrentHashMap是一种线程安全且高效的哈希表的解决方案,相比HashTable的全表锁在性能上的提升非常大。

6.引用

第一次写博客,找了很多好的博客,跟着博客去查找jdk源码,可以更快的理解其中的含义。
参考:
http://tool.oschina.net/apidocs/apidoc?api=jdk-zh
https://blog.csdn.net/fouy_yun/article/details/77816587
http://www.importnew.com/22007.html
http://www.sohu.com/a/205451532_684445
http://www.cnblogs.com/yydcdut/p/3959815.html

你可能感兴趣的:(ConcurrentHashMap)