深入学习java并发编程:ConcurrentHashMap<K, V>实现

原创 2016年05月31日 17:37:10
1、ConcurrentHashMap设计类图

        ConcurrentHashMap是线程安全并且高效的HashMap,通过使用锁分段技术提升并发访问率。HashTable在竞争激烈的并发环境下效率低下的原因是所有访问HashTable的线程必须竞争同一把锁,如果容器有多把锁,每一把锁都锁容器中一部分数据,那么当访问容器中不同数据段时,线程间就不存在锁竞争,从而可以提高并发访问效率,这句是ConcurrentHashMap所使用的锁分段技术。
          通过类图可以看到,ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment继承自RentrantLock,是一种可重入锁,扮演锁的角色;HashEntry则是用来存储键值对数据,与HashMap中Entry结构相似。一个ConcurrentHashMap里面组合一个Segment数组。Segment结构和HashMap类似,是一种数组和链表结构。一个Segment里面组合一个HashEntry数组,每个HashEntry结构是一个链表节点,各个Segment守护着HashEntry数组里面的元素,当对HashEntry数组进行修改时,必须先获得与他对应的Segment锁。
              

2、ConcurrentHashMap的构造实现以及重要方法分析   
      final Segment<K,V>[] segments; 
     ConcurrentHashMap中组合关联了一个Segment<K,V>数组。数组每一项可以看成是一分段HashMap,同时Segment锁守护这个分段。
1) 构造器
    和HashMap一样,ConcurrentHashMap同样有initialCapacity和loadFactor属性,不过还多了一个concurrencyLevel属性,默认的这三个参数值分别为16,0.75和16。

 public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        if (concurrencyLevel > MAX_SEGMENTS)
            concurrencyLevel = MAX_SEGMENTS;
        // Find power-of-two sizes best matching arguments
        int sshift = 0;
        int ssize = 1;
        while (ssize < concurrencyLevel) {     
            ++sshift;
            ssize <<= 1;
        }
        this.segmentShift = 32 - sshift;
        this.segmentMask = ssize - 1;
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        int c = initialCapacity / ssize;
        if (c * ssize < initialCapacity)
            ++c;
        int cap = MIN_SEGMENT_TABLE_CAPACITY;
        while (cap < c)
            cap <<= 1;
        // create segments and segments[0]
        Segment<K,V> s0 =
            new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                             (HashEntry<K,V>[])new HashEntry[cap]);
        Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
        UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
        this.segments = ss;
    }
当设置了initialCapacity、loadFactor和concurrencyLevel时,通过以下代码计算ssize值,决定Segment数组的大小。如果concurrencyLevel为16,则计算结果也是16,最终 会创建大小为16的Segment数组。
          int sshift = 0;
        int ssize = 1;
        while (ssize < concurrencyLevel) {     
            ++sshift;
            ssize <<= 1;
        }
接着计算每个Segment里面包含的HashEntry数组大小,代码如下,最终的计算结果是2的整数幂大小,且大于等于 initialCapacity / ssize。
  int c = initialCapacity / ssize;
        if (c * ssize < initialCapacity)
            ++c;
        int cap = MIN_SEGMENT_TABLE_CAPACITY;
        while (cap < c)
            cap <<= 1;

2)定位Segment
    ConcurrentHashMap使用分段锁,那么在插入和获取元素时,必须先通过散列算法定位到Segment。可以看到ConcurrentHashMap使用 Wang/Jenkins 哈希算法。
    private int hash(Object k) {
        int h = hashSeed;
        if ((0 != h) && (k instanceof String)) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
        h ^= k.hashCode();
        // Spread bits to regularize both segment and index locations,
        // using variant of single-word Wang/Jenkins hash.
        h += (h <<  15) ^ 0xffffcd7d;
        h ^= (h >>> 10);
        h += (h <<   3);
        h ^= (h >>>  6);
        h += (h <<   2) + (h << 14);
        return h ^ (h >>> 16);
    }
3)put(K key, V value): V  向map中添加元素的方法
  //添加时,并没有在该方法上加上synchronized关键字,首先判断value 是否为null,若为null抛出异常。然后通过计算哈希值,确定其对应Segment数组的Segment对象。在找到Segment对象后,接着调用Segment的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方法时,首先进行lock操作,锁住当前Segment,接着判断当前存储的对象个数加1后是否大于threshold。如大于,则将当前的HashEntry对象数组大小扩大两倍,并将之前存储的对象重新hash,转移到新的数组中。接下来,通过哈希,得到key需要存放的位置,接着寻找对应位置上是否有相同的key,如果有,则替换。否则,创建一个HashEntry对象,链接到对应链表中。完成以上过程后,释放锁,整个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;
        }

4) remove(Object key): V  移除元素方法
//移除时,首先对key进行哈希操作,基于得到的哈希值,寻找对相应的Segment对象,然后调用其remove方法进行操作。    
   public V remove(Object key) {
        int hash = hash(key);
        Segment<K,V> s = segmentForHash(hash);
        return s == null ? null : s.remove(key, hash, null);
    }
//在Segment的remove方法中,进行加锁操作,对hash值和对象数组大小减1的值进行按位与操作,获取到数组上对应位置的HashEntry对象,接着遍历寻找与key相同的元素,如果未找到,则返回null,否则,重建HashEntry链表中位于删除元素之前的所有HashEntry,位于其后的不做处理。最后释放锁。
  final V remove(Object key, int hash, Object value) { 
            if (!tryLock())
                scanAndLock(key, hash);
            V oldValue = null;
            try {
                HashEntry<K,V>[] tab = table;
                int index = (tab.length - 1) & hash;
                HashEntry<K,V> e = entryAt(tab, index);
                HashEntry<K,V> pred = null;
                while (e != null) {
                    K k;
                    HashEntry<K,V> next = e.next;
                    if ((k = e.key) == key ||
                        (e.hash == hash && key.equals(k))) {
                        V v = e.value;
                        if (value == null || value == v || value.equals(v)) {
                            if (pred == null)
                                setEntryAt(tab, index, next);
                            else
                                pred.setNext(next);
                            ++modCount;
                            --count;
                            oldValue = v;
                        }
                        break;
                    }
                    pred = e;
                    e = next;
                }
            } finally {
                unlock();
            }
            return oldValue;
        }

5)size(): int 计算当前map大小的方法

        前面的操作都是在单个Segment中进行的,但是ConcurrentHashMapsize的size操作是在多个Segment中进行。ConcurrentHashMap的size操作也采用了一种比较巧的方式,来尽量避免对所有的Segment都加锁。

  前面我们提到了一个Segment中的有一个modCount变量,代表的是对Segment中元素的数量造成影响的操作的次数,这个值只增不减,size操作就是遍历了两次Segment,每次记录Segment的modCount值,然后将两次的modCount进行比较,如果相同,则表示期间没有发生过写入操作,就将原先遍历的结果返回,如果不相同,则把这个过程再重复做一次,如果再不相同,则就需要将所有的Segment都锁住,然后一个一个遍历。

   public int size() {
        // Try a few times to get accurate count. On failure due to
        // continuous async changes in table, resort to locking.
        final Segment<K,V>[] 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<K,V> 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;
    }

 









 


版权声明:

相关文章推荐

深入学习java集合:LinkedHashMap<K,V>实现

1、LinkedHashMap类图           LinkedHashMap是Map接口的哈希表和链接列表实现,具有可预知的迭代顺序。此实现提供所有可选的映射操作,并允许使用nul...

深入学习java集合:Hashtable<K,V>实现

1、Hashtable类图          从中可以看出HashTable继承Dictionary类,实现Map接口。其中Dictionary类是任何可将键映射到相应值的类(如 Has...

深入学习java集合:HashMap<K,V>实现

1、HashMap类图         Map 用于保存具有映射关系的数据,因此 Map 集合里保存着两组值,一组值用于保存 Map 里的 Key,另外一组用于保存&#...

【Java并发编程】深入分析ConcurrentHashMap(九)

本章是提高教程可能对于刚入门同学来说会有些难度,读懂本章你需要了解以下知识点:一、 【Java基础提高】深入分析final关键字(一)二、 【Java并发编程】深入分析volatile(四)三、 【J...

Java并发编程(五)ConcurrentHashMap的实现原理和源码分析

在Java1.5中,并发编程大师Doug Lea给我们带来了concurrent包,而该包中提供的ConcurrentHashMap是线程安全并且高效的HashMap,本节我们就来研究下Concurr...

HashMap、HashTable 和 ConcurrentHashMap 的键值对<K,V>能否为null

本文目录本文目录 已有结论 HashMap的put源码 HashTable的put源码 ConcurrentHashMap的put源码已有结论 HashMap可以允许插入null key和null v...

HashMap,LinkedHashMap,TreeMap,HashTable,ConcurrentHashMap,ConcurrentSkipListMap 关于k,v是否为null,以及输出排序

Map的k,v是否为可为null,遍历输出顺序使用场景

Java并发编程 - 常用容器(ConcurrentHashMap, ConcurrentLinkedQueue)

ConcurrentHashMap的实现原理与使用传统的HashMap不是线程安全的, 所以多线程进行put()和get()操作的时候可能会引发问题. 还有一个叫做HashTable的数据结构, 它...

Java并发编程之ConcurrentHashMap

ConcurrentHashMap ConcurrentHashMap是一个线程安全的Hash Table,它的主要功能是提供了一组和HashTable功能相同但是线程安全的方法。Concurr...

Java并发编程之ConcurrentHashMap原理分析

前言: 集合是编程中最常用的数据结构。而谈到并发,几乎总是离不开集合这类高级数据结构的支持。比如两个线程需要同时访问一个中间临界区(Queue),比如常会用缓存作为外部文件的副本(HashMap)。...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)