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

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;
    }

 









 


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值