ConcurrentHashMap

在JDK1.4之前只有vector和HashTable是线程安全集合,在JDK1.5之后开始增加了安全的Map接口ConcurrentMap和线程安全的队列BlockIngQueue

通过继承关系可知:

ConcurrentHashMap是HashMap的安全版本

ConcurrentMap

也是键值对形式存储数据

public interface ConcurrentMap<K, V> extends Map<K, V> {

实现自Map接口,即Map中所有的方法ConcurrentMap同样具有

 特有方法说明:

//如果指定键已经不在和某个值关联,则它和给定值关联,相当于key存在则不会替换value值
V putIfAbsent(K key, V value);
//只有当目前key映射到指定的value时(即key和对应的value都存在),才移除键值对,返回Boolean类型  成功:true  失败:false
boolean remove(Object key, Object value);
//只有当key和oldvalue同时存在时,才会将oldvalue替换为newvalue
boolean replace(K key, V oldValue, V newValue);
//只有在集合中存在该key,才完成替换
V replace(K key, V value);

提供的原子操作方法

ConcurrentHashMap

Segment:

Segment 类继承于 ReentrantLock 类,从而使得 Segment 对象能充当锁的角色。每个 Segment 对象用来守护它的成员对象 table 中包含的若干个桶。table 是一个由 HashEntry 对象组成的链表数组,table 数组的每一个数组成员就是一个桶。

在Segment类中,count 变量是一个计数器,它表示每个 Segment 对象管理的 table 数组包含的 HashEntry 对象的个数,也就是 Segment 中包含的 HashEntry 对象的总数。特别需要注意的是,之所以在每个 Segment 对象中包含一个计数器,而不是在 ConcurrentHashMap 中使用全局的计数器,是对 ConcurrentHashMap 并发性的考虑:因为这样当需要更新计数器时,不用锁定整个ConcurrentHashMap。事实上,每次对段进行结构上的改变,如在段中进行增加/删除节点(修改节点的值不算结构上的改变),都要更新count的值,此外,在JDK的实现中每次读取操作开始都要先读取count的值。特别需要注意的是,count是volatile的,这使得对count的任何更新对其它线程都是立即可见的。modCount用于统计段结构改变的次数,主要是为了检测对多个段进行遍历过程中某个段是否发生改变,这一点具体在谈到跨段操作时会详述。threashold用来表示段需要进行重哈希的阈值。loadFactor表示段的负载因子,其值等同于ConcurrentHashMap的负载因子的值。table是一个典型的链表数组,而且也是volatile的,这使得对table的任何更新对其它线程也都是立即可见的。

ConcurrentHashMap允许多个修改(写)操作并发进行,其关键在于使用了锁分段技术,它使用了不同的锁来控制对哈希表的不同部分进行的修改(写),而 ConcurrentHashMap 内部使用段(Segment)来表示这些不同的部分。实际上,每个段实质上就是一个小的哈希表,每个段都有自己的锁(Segment 类继承了 ReentrantLock 类)。这样,只要多个修改(写)操作发生在不同的段上,它们就可以并发进行。下图是依次插入 ABC 三个 HashEntry 节点后,Segment 的结构示意图:
ConcurrentHashMap允许多个修改(写)操作并发进行,其关键在于使用了锁分段技术,它使用了不同的锁来控制对哈希表的不同部分进行的修改(写),而 ConcurrentHashMap 内部使用段(Segment)来表示这些不同的部分。实际上,每个段实质上就是一个小的哈希表,每个段都有自己的锁(Segment 类继承了 ReentrantLock 类)。这样,只要多个修改(写)操作发生在不同的段上,它们就可以并发进行。

//用来存储数据 是一个Segment数组
final Segment<K,V>[] segments;

//segment是继承自ReentrantLock,实现了锁机制
static final class Segment<K,V> extends ReentrantLock implements Serializable {
       //重入次数   加锁操作发生冲突需要考虑重入问题
        static final int MAX_SCAN_RETRIES =
            Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
        //数据存放在table中,是volatile修饰
        transient volatile HashEntry<K,V>[] table;
        Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
            this.loadFactor = lf;
            this.threshold = threshold;
            this.table = tab;
        }
}

 static final class HashEntry<K,V> {
        final int hash;
        final K key;
        volatile V value;
        volatile HashEntry<K,V> next;

        HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
 }

 HashEntry用来封装具体的键值对,是个典型的四元组。与HashMap中的Entry类似,HashEntry也包括同样的四个域,分别是key、hash、value和next。不同的是,在HashEntry类中,key,hash和next域都被声明为final的,因此HashEntry对象几乎是不可变的,这是ConcurrentHashmap读操作并不需要加锁的一个重要原因。next域被声明为final本身就意味着我们不能从hash链的中间或尾部添加或删除节点,因为这需要修改next引用值,因此所有的节点的修改只能从头部开始。对于put操作,可以一律添加到Hash链的头部。但是对于remove操作,可能需要从中间删除一个节点,这就需要将要删除节点的前面所有节点整个复制(重新new)一遍,最后一个节点指向要删除结点的下一个结点。特别地,由于value域被volatile修饰,所以其可以确保被读线程读到最新的值,这是ConcurrentHashmap读操作并不需要加锁的另一个重要原因。实际上,ConcurrentHashMap完全允许多个读操作并发进行,读操作并不需要加锁。(put、remove操作才需要加锁)
 

 

ConcurrentHashMap是segment数组+哈希表结构,ConcurrentHashMap本质上是一个Segment数组,而一个Segment实例又包含若干个桶,每个桶中都包含一条由若干个 HashEntry 对象链接起来的链表。

 

通过源码可知:segment是ReentrantLock来修饰的,其本质是一把锁,称之为分段锁

构造函数中concurrencyLevel称之为并发度,该参数是用来实例化segment数组,默认的大小是16个,即同一时刻并发线程量至少是16个,在ConcurrentHashMap中变更操作(put,remove,replace)加锁处理,针对get读操作是可以共享操作,读操作可以同时有多个线程操作。

并发度concurrencyLevel默认是16,也可以自行指定,指定的并发度需要满足2的倍数关系,目的是快速的进行key哈希找到对应存储位置,并发度一旦确定之后是不再改变的,在ConcurrentHashMap使用过程中数量超过扩容阈值时,也只是对segment下的哈希表进行扩容

通过使用段(Segment)将ConcurrentHashMap划分为不同的部分,ConcurrentHashMap就可以使用不同的锁来控制对哈希表的不同部分的修改,从而允许多个修改操作并发进行, 这正是ConcurrentHashMap锁分段技术的核心内涵。分段锁将数据分成一段一段的存储,然后给每一段数据进行加锁,当一个线程占用锁访问其中一段数据时,其他段的数据也会被其他线程访问

 在每个线程操作时只会锁住其中一个segment,不同的线程在操作ConcurrentHashMap时只要所操作的数据在不同的segment中,线程之间互不干扰

ConcurrentHashMap的高并发主要来源于:

1.采用分段锁实现多个线程间的共享访问

2.用HashEntry对象的不变性(因为HashEntry是通过final来修饰的)来降低执行读操作的线程在遍历链表期间对加锁的要求

3.对于同一个volatile变量的读/写操作,协调不同线程间的读写内存的可见性问题

1.8之后底层换成了CAS,把锁分段机制放弃了,CAS基本上是可以达到无锁境界

CAS+volatile无锁编程

ConcurrentHashMap的put流程

   public V put(K key, V value) {
       //key,value不能为null
        Segment<K,V> s;
        if (value == null)
            throw new NullPointerException();
       //通过key进行哈希到对应segment位置
        int hash = hash(key);
        int j = (hash >>> segmentShift) & segmentMask;
        //通过位置j获取当前的对应segment起始位置
        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方法
        final V put(K key, int hash, V value, boolean onlyIfAbsent) {
            //尝试性加锁
            HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);
            V oldValue;
            try {
                //当前segment下的table
                HashEntry<K,V>[] tab = table;
                //通过key的哈希值进行哈希找到对应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) {
                            //put方法处理:将新value替换oldvalue
                                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;
        }
        
     //扩容仅针对某个segment进行扩容,而不是对整个ConcurrentHashMap进行扩容
      private void rehash(HashEntry<K,V> node) {
          //在segment下的table
            HashEntry<K,V>[] oldTable = table;
            int oldCapacity = oldTable.length;
            //按照原大小2倍关系进行扩容 
            int newCapacity = oldCapacity << 1;
            threshold = (int)(newCapacity * loadFactor);
            HashEntry<K,V>[] newTable =(HashEntry<K,V>[]) new HashEntry[newCapacity];
            int sizeMask = newCapacity - 1;
            //将原有table上的所有hashentry节点进行重新哈希到新table上
            for (int i = 0; i < oldCapacity ; i++) {
                HashEntry<K,V> e = oldTable[i];
                if (e != null) {
                    HashEntry<K,V> next = e.next;
                    int idx = e.hash & sizeMask;
                    if (next == null)   //  Single node on list
                        newTable[idx] = e;
                    else { // Reuse consecutive sequence at same slot
                        HashEntry<K,V> lastRun = e;
                        int lastIdx = idx;
                        for (HashEntry<K,V> last = next;
                             last != null;
                             last = last.next) {
                            int k = last.hash & sizeMask;
                            if (k != lastIdx) {
                                lastIdx = k;
                                lastRun = last;
                            }
                        }
                        newTable[lastIdx] = lastRun;
                        // Clone remaining nodes
                        for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
                            V v = p.value;
                            int h = p.hash;
                            int k = h & sizeMask;
                            HashEntry<K,V> n = newTable[k];
                            newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
                        }
                    }
                }
            }
            int nodeIndex = node.hash & sizeMask; // add the new node
            node.setNext(newTable[nodeIndex]);
            newTable[nodeIndex] = node;
            table = newTable;
        }

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值