JDK1.7的ConcurrentHashMap 的put、get、remove工作原理

    技术2022-07-12  71

    很详细,很不错,好理解,具体请看

    原文:https://www.cnblogs.com/heqiyoujing/p/10928423.htmlJDK1.8的ConcurrentHashMap原理戳这里

    ConcurrentHashMap使用了分段锁技术,每个Segment都是一把锁,本身Segment继承了ReentranLock

    ConcurrentHashMap由 Segment 数组、每个Segment包含一个HashEntry 数组(HashEntry<K,V>[] table),为一个小的hash表。 Segment和 HashMap 一样,仍然是数组加链表。

    put总结:

    put只对对应的Segment加锁,其他的Segment不受影响,提高并发度。

    就是说如果容量大小是16他的并发度就是16,可以同时允许16个线程操作16个Segment而且还是线程安全的。

    1)根据散列码hash( 调用segmentFor(hash) )找到对应的 Segment

    2)在这个 Segment 中执行具体的真正的 put 操作(segment.put( key, hash, value, false) )

    3)加锁。对该Segment 对象而不是整个concurrentHashMap加锁

    4)得到该散列码对应的 table 数组的下标值index,并找到散列码对应的具体的那个桶 tab[index]

    5)while寻找,若键 / 值对已存在,覆盖新值返回旧值; 键 / 值对不存在 ,创建新节点,并添加到链表的头部 ,返回 null

    6)解锁该Segment

     

    //并非完整源码,只是关键部分 public V put(K key, V value) { if (value == null) //ConcurrentHashMap 中不允许用 null 作为映射值 throw new NullPointerException(); int hash = hash(key.hashCode()); // 计算键对应的散列码 // 根据散列码找到对应的 Segment return segmentFor(hash).put(key, hash, value, false); } 根据 hash 值找到对应的 Segment: /** * 使用 key 的散列码来得到 segments 数组中对应的 Segment */ final Segment<K,V> segmentFor(int hash) { // 将散列值右移 segmentShift 个位,并在高位填充 0 // 然后把得到的值与 segmentMask 相“与” // 从而得到 hash 值对应的 segments 数组的下标值 // 最后根据下标值返回散列码对应的 Segment 对象 return segments[(hash >>> segmentShift) & segmentMask]; } V put(K key, int hash, V value, boolean onlyIfAbsent) { lock(); // 加锁,这里是锁定某个 Segment 对象而非整个 ConcurrentHashMap try { int c = count; if (c++ > threshold) // 如果超过再散列的阈值 rehash(); // 执行再散列,table 数组的长度将扩充一倍 HashEntry<K,V>[] tab = table; // 把散列码值与 table 数组的长度减 1 的值相“与” // 得到该散列码对应的 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; if (e != null) { // 如果键 / 值对以经存在 oldValue = e.value; if (!onlyIfAbsent) e.value = value; // 设置 value 值 } else { // 键 / 值对不存在 oldValue = null; ++modCount; // 要添加新节点到链表中,所以 modCont 要加 1 // 创建新节点,并添加到链表的头部 tab[index] = new HashEntry<K,V>(key, hash, first, value); count = c; // 写 count 变量 } return oldValue; } finally { unlock(); // 解锁 } `} get总结:

    在该Segment内部get。get全程不加锁,并发度高。

    get操作可以无锁是由于HashEntry的元素val和指针next是用volatile修饰的,在多线程环境下线程A修改结点的val或者新增节点的时候是对线程B可见的。

    1)key 通过 hash 之后定位到具体的 Segment,再进行get操作。

    2)count不为0,使用hash得到对应桶的第一个HashEntry

    注:count为在本 segment 范围内,包含的 HashEntry 元素的个数。该变量被声明为 volatile 型,保证每次读取到最新的数据

    3)while循环依次遍历,使用if(e.hash == hash && key.equals(e.key)) 寻找目标值。

    V get(Object key, int hash) { if(count != 0) { // 首先读 count 变量 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; // 如果读到 value 域为 null,说明发生了重排序,加锁后重新读取 return readValueUnderLock(e); } e = e.next; } } return null; } V readValueUnderLock(HashEntry<K,V> e) { lock(); try { return e.value; } finally { unlock(); } }

    是的,因为基本上还是数组加链表的方式,我们去查询的时候,还得遍历链表,会导致效率很低,这个跟jdk1.7的HashMap是存在的一样问题,所以他在jdk1.8完全优化了。其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。

     

     

    Processed: 0.013, SQL: 10