读ConcurrentHashMap源码笔记,体验和HashTable的区别

本文深入解析了Java 1.5中的ConcurrentHashMap实现原理及其高效并发读写的机制。通过对比Hashtable,介绍了ConcurrentHashMap如何通过分段锁提高并发性能。

      经研究过jkd1.5新特性,其中ConcurrentHashMap就是其中之一,其特点:效率比Hashtable高,并发性比hashmap好。结合了两者的特点。
      集合是编程中最常用的数据结构。而谈到并发,几乎总是离不开集合这类高级数据结构的支持。比如两个线程需要同时访问一个中间临界区(Queue),比如常会用缓存作为外部文件的副本(HashMap)。这篇文章主要分析jdk1.5的3种并发集合类型(concurrent,copyonright,queue)中的ConcurrentHashMap,让我们从原理上细致的了解它们,能够让我们在深度项目开发中获益非浅。

    在平常的开发中,我们使用得最多的数据结构之一就是HashMap和HashTable。大家都知道,HashMap中未进行同步考虑,而Hashtable则使用了synchronized,带来的直接影响就是可选择,我们可以在单线程时使用HashMap提高效率,而多线程时用Hashtable来保证安全。
    当我们享受着jdk带来的便利时同样承受它带来的不幸恶果。通过分析Hashtable就知道,synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,安全的背后是巨大的浪费,慧眼独具的DougLee立马拿出了解决方案----ConcurrentHashMap。

   ConcurrentHashMap和Hashtable主要区别就是围绕着锁的粒度以及如何锁。如图


左边便是Hashtable的实现方式---锁整个hash表;而右边则是ConcurrentHashMap的实现方式---锁桶(或段segment)。 ConcurrentHashMap将hash表分为16个桶(默认值),诸如get,put,remove等常用操作只锁当前需要用到的桶。试想,原来 只能一个线程进入,现在却能同时16个写线程进入(写线程才需要锁定,而读线程几乎不受限制,之后会提到),并发性的提升是显而易见的。其实ConcurrentHashMap的设计原理跟HashMap差不多,都是通过哈希表来维护数据,只是hashmap用一张哈希表维护数据而concurrenthashmap是俩张哈希表来维护数据的,它首先通过key的哈希值计算出目标元素在哪个segment中,找到对应的segment时再对次表加锁,然后哈希计算得出次表索引值,最后在数据链上找到目标元素做同步修改操作。

    更令人惊讶的是ConcurrentHashMap的读取并发,因为在读取的大多数时候都没有用到锁定,除非读取到的节点的value值为空,这时候会把,所以读取操作几乎是完全的并发操作,而写操作锁定的粒度又非常细,比起之前又更加快速(这一点在桶更多时表现得更明显些)。只有在求size等操作时才需要锁定整个表。而在迭代时,ConcurrentHashMap使用了不同于传统集合的快速失败迭代器(见之前的文章《JAVA API备忘---集合》)的另一种迭代方式,我们称为弱一致迭代器。在这种迭代方式中,当iterator被创建后集合再发生改变就不再是抛出 ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数 据,iterator完成后再将头指针替换为新的数据,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变,更重要的,这保证 了多个线程并发执行的连续性和扩展性,是性能提升的关键。
    ConcurrentHashMap中主要实体类就是三个:ConcurrentHashMap(内部Segment[] segments维护主hash表),Segment(内部HashEntry[] table维护次hash表),HashEntry(链式节点对象),对应上面的图可以看出之间的关系。

下面介绍下ConcurrentHashMap在get、put、remove操作数据时如何保持同步。

get操作

----------------concurrmenthashmap方法---------
public V get(Object key) {
        int hash = hash(key.hashCode());
        return segmentFor(hash).get(key, hash);
    }
//对主表求索引
final Segment<K,V> segmentFor(int hash) {
return segments[(hash >>> segmentShift) & segmentMask]; }--------------segment方法-----------------------V get(Object key, int hash) { if (count != 0) { // read-volatile HashEntry<K,V> e = getFirst(hash); while (e != null) { if (e.hash == hash && key.equals(e.key)) { V v = e.value; if (v != null) return v; return readValueUnderLock(e); // recheck } e = e.next; } } return null; }
//对次表求索引
HashEntry<K,V> getFirst(int hash) {
            HashEntry<K,V>[] tab = table;
            return tab[hash & (tab.length - 1)];
        } 
上面正好对应着前面所说的先求主表索引,再求次表索引。这里详细讲解下segment中的get操作,在找到目标节点时,如果目标节点对应的value不为null时,则直接返回,否则的话就说明在并发环境下,该节点的value值可能已经被其他线程修改,还没来得及写回就被读取,这时就调用readValueUnderLock方法
  V readValueUnderLock(HashEntry<K,V> e) {
            lock();
            try {
                return e.value;
            } finally {
                unlock();
            }
        }

对该节点进行同步读取。

Size方法

public int size() {
        final Segment<K,V>[] segments = this.segments;
        long sum = 0;           //保存所有段中节点数的和
        long check = 0;  //检查在执行size方法期间是否有其他线程删除或添加节点元素,如果check==sum则
           //说明期间没有修改,数据正确,否则说明有结构修改,重新加锁计算
     int[] mc = new int[segments.length];
        //尝试RETRIES_BEFORE_LOCK次计算节点总数,由于是并发环境下,很可能在计算期间节点数目被其他线程修改导致
    //数目不正确,因此循环计算多次,如果还是不正确则采用表加锁计算
    for (int k = 0; k < RETRIES_BEFORE_LOCK; ++k) {
            check = 0;
            sum = 0;
            int mcsum = 0;
            for (int i = 0; i < segments.length; ++i) {
                sum += segments[i].count;
                mcsum += mc[i] = segments[i].modCount;
            }
            if (mcsum != 0) {
                for (int i = 0; i < segments.length; ++i) {
                    check += segments[i].count;
                    if (mc[i] != segments[i].modCount) {
                        check = -1; // force retry
                        break;
                    }
                }
            }
            if (check == sum)
                break;
        }
        if (check != sum) { //如果数据还不正确,使用加锁同步计算
            sum = 0;
            for (int i = 0; i < segments.length; ++i)
                segments[i].lock();
            for (int i = 0; i < segments.length; ++i)
                sum += segments[i].count;
            for (int i = 0; i < segments.length; ++i)
                segments[i].unlock();
        }
        if (sum > Integer.MAX_VALUE)
            return Integer.MAX_VALUE;
        else
            return (int)sum;
    }

put操作

 public V put(K key, V value) {
        if (value == null)
            throw new NullPointerException();
        int hash = hash(key.hashCode());
        return segmentFor(hash).put(key, hash, value, false);//主表hash计算求索引
    }

 V put(K key, int hash, V value, boolean onlyIfAbsent) {
            lock();   //加锁同步
            try {
                int c = count;
                if (c++ > threshold)
                    rehash();        //如果超过容量,2倍扩容,并且从新计算hash值
                HashEntry<K,V>[] tab = table;
                int index = hash & (tab.length - 1); //次表hash计算求索引
                HashEntry<K,V> first = tab[index];
                HashEntry<K,V> e = first;
                while (e != null && (e.hash != hash || !key.equals(e.key)))
                    e = e.next;

                V oldValue;
                if (e != null) {           //修改操作
                    oldValue = e.value;
                    if (!onlyIfAbsent)
                        e.value = value;
                }
                else {        //添加操作
                    oldValue = null;
                    ++modCount;
                    tab[index] = new HashEntry<K,V>(key, hash, first, value);
                    count = c; // write-volatile
                }
                return oldValue;
            } finally {
                unlock();
            }
        }

remove操作

-------------concurrenthashmap方法----------------------
 public V remove(Object key) {
	int hash = hash(key.hashCode());
        return segmentFor(hash).remove(key, hash, null);//调用segment的remove方法
    }
--------------segment方法--------------------------------
 V remove(Object key, int hash, Object value) {
            lock();
            try {
                int c = count - 1;
                HashEntry<K,V>[] tab = table;
                int index = hash & (tab.length - 1);
                HashEntry<K,V> first = tab[index];
                HashEntry<K,V> e = first;
                while (e != null && (e.hash != hash || !key.equals(e.key)))
                    e = e.next;

                V oldValue = null;
                if (e != null) {
                    V v = e.value;
                    if (value == null || value.equals(v)) {
                        oldValue = v;
                        // All entries following removed node can stay
                        // in list, but all preceding ones need to be
                        // cloned.所有在删除节点后面的元素可以保留在队列中,删除节点前面的元素则需要克隆
                        ++modCount;
                        HashEntry<K,V> newFirst = e.next;
                        for (HashEntry<K,V> p = first; p != e; p = p.next)
                            newFirst = new HashEntry<K,V>(p.key, p.hash,
                                                          newFirst, p.value);
                        tab[index] = newFirst;
                        count = c; // write-volatile
                    }
                }
                return oldValue;
            } finally {
                unlock();
            }
        }

通过上面的代码可以看出concurrenthashmap只对put、remove等修改数据结构的操作做严格的同步限制,而对于get、size等只读操作只在需要的时候做同步。concurrenthashmap之所以在并发环境下比hashtable效率更高,是因为它设计了主、次表,通过在更小粒度上给segement的HashEntry[] table加锁,而不影响其他线程对其他segment的操作,从而提高效率。





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值