JAVA之ConcurrentHashMap源码深度分析

来源:互联网 发布:立项依据怎么写 知乎 编辑:程序博客网 时间:2024/03/28 19:38

欢迎转载,请附上出处:
http://blog.csdn.net/as02446418/article/details/47004573

我们首先来看一下ConcurrentHashMap类的声明:

    public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>        implements ConcurrentMap<K, V>, Serializable

其中,这个类继承了java.util.AbstractMap中已有的实现,这个在前面整理HashMap的时候已经提过了,重点看后面实现的接口ConcurrentMap和Serializable。Serializable是做序列化处理的,而ConcurrentMap的定义又如下:

public interface ConcurrentMap<K, V> extends Map<K, V> {    V putIfAbsent(K key, V value);    boolean remove(Object key, Object value);    boolean replace(K key, V oldValue, V newValue);    V replace(K key, V value);}

其中规定了4个方法。

  • V putIfAbsent(K key, V value); 如果没有这个key,则放入这个key-value,返回null,否则返回key对应的value。

  • boolean remove(Object key, Object value); 移除key和对应的value,如果key对应的不是value,移除失败

  • boolean replace(K key, V oldValue, V newValue); 替代key对应的值,仅当当前值为旧值

  • V replace(K key, V value); 替代key对应的值,只要当前有值

这些方法都在ConcurrentHashMap实现了,后文会部分提到这些。

1. 构造方法和ConcurrentHashMap的Segment实现
先看下构造方法:

 public ConcurrentHashMap(int initialCapacity,                             float loadFactor, int concurrencyLevel)

有几个重载的方法,说这个参数最全的,有3个参数,除了HashMap中涉及到的loadFactor和initialCapacity外,还有一个concurrencyLevel,翻译过来就是并发级别或者并发度。

与此对应,ConcurrentHashMap中有一个segments数组对象,元素类型是ConcurrentHashMap的内部类Segment,而concurrencyLevel就是这个segments数组的大小。

我们来看下这个Segment类:

static final class Segment<K,V> extends ReentrantLock implements Serializable

Segment扩展了ReentrantLock并实现了Serializable接口。除此之外,我们还发现这个类里实现的东西和java.util.HashMap非常相似。

实际上,这个类正是整个ConcurrentHashMap实现的关键。我想,作为这篇文章读者的您,应该会用到过各式各样的数据库,就拿Mysql的innoDB引擎来看,它除了支持表级锁意外,还支持行级锁,意义就在于这减小了锁粒度,当只对某行数据进行操作的时候,很可能没有必要限制同一个表中其它行的数据。在这个类中,这个Segment也是起到了同样的作用。每个Segment本身就是一个ReentrantLock,只有要修改的数据存在在同一个Segment,才有可能会需要锁定,这样就提高了多线程情况下效率,没必要所有线程全部等待锁。

2. get()方法源码分析
我们先看下get()方法的实现。

public V get(Object key) {    Segment<K,V> s; // manually integrate access methods to reduce overhead    HashEntry<K,V>[] tab;    int h = hash(key);    long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;    if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&        (tab = s.table) != null) {        for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile                 (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);             e != null; e = e.next) {            K k;            if ((k = e.key) == key || (e.hash == h && key.equals(k)))                return e.value;        }    }    return null;}

实际上,就是通过key计算得到的hash值,确定对应的Segment对象,并用原子操作获取到对应的table和table中hash值对应的对象。

我们可以看到,在这个过程中,是没有显式用到锁的,仅仅是通过Unsafe类和原子操作,避免了阻塞,提高了性能。

3. put()和putIfAbsent()方法分析
先看下put()方法的源码:

public V put(K key, V value) {    Segment<K,V> s;    if (value == null)        throw new NullPointerException();    int hash = hash(key);    int j = (hash >>> segmentShift) & segmentMask;    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment        s = ensureSegment(j);    return s.put(key, hash, value, false);}

我们看到其中最后是使用Segment的put()方法的调用,而putIfAbsent()的方法的调用,仅仅是最后一个参数不同。

我们进一步看下Segment的put()方法的调用:

final V put(K key, int hash, V value, boolean onlyIfAbsent) {    HashEntry<K,V> node = tryLock() ? null :        scanAndLockForPut(key, hash, value);    V oldValue;    try {        HashEntry<K,V>[] tab = table;        int index = (tab.length - 1) & hash;        HashEntry<K,V> first = entryAt(tab, index);        for (HashEntry<K,V> e = first;;) {            if (e != null) {                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 {                if (node != null)                    node.setNext(first);                else                    node = new HashEntry<K,V>(hash, key, value, first);                int c = count + 1;                if (c > threshold && tab.length < MAXIMUM_CAPACITY)                    rehash(node);                else                    setEntryAt(tab, index, node);                ++modCount;                count = c;                oldValue = null;                break;            }        }    } finally {        unlock();    }    return oldValue;}

和java.util.HashMap比起来,基本类似,有两个地方不同。一个是对onlyIfAbsent参数的判断处理。另一个则是对整个操作过程加锁,并在加锁的地方做了稍微巧妙的处理。就是在在等锁的过程中,不断的寻找和构建node节点对象,不管是否这个方法最终是否创建到了node节点,当这个方法返回时,一定是已经获得了这个Segment对应的锁。

而这个寻找和创建节点所在的循环,一方面是做节点的遍历查询,另一方面也是起到了自旋锁的作用,避免直接调用lock()而等锁阻塞,因为这样会在系统实现层面阻塞和唤醒线程,是有一定的切换成本的,对提高效率不利。

4. 综述
其实,纵观整个类的实现,都肯定会涉及到Segment的处理和其中方法的调用。对这些方法的调用,分为读操作和写处理,通常读操作没用用锁,而在修改操作,如remove() replace()等方法都和put()类似,在整个操作过程中对Segment进行了尝试加锁和自旋等锁的前提操作,并最后释放锁。这些写操作的等锁基本上和put()中的scanAndLockForPut()方法等同,除了需要创建节点。

从整体上看,由于Segment降低了并发中的锁粒度,并在写操作使用了锁。保证了整个ConcurrentHashMap的线程安全,也保证了并发运行时的效率。

5. 其它
在java.util.conccurent包中并没有TreeMap,但对于有序要求的容器,有基于skip list数据结构的Map实现ConcurrentSkipListMap,而且也有skip list的ConcurrentSkipListSet,和java.util包中的Set一样,是基于Map的。

0 0