ConCurrentHashMap源码分析

内部结构

在这里插入图片描述
一个 ConcurrentHashMap 维护一个 Segment 数组,一个 Segment 维护一个HashEntry 数组。

JDK 1.7

实现原理

  • 其中 Segment 继承于 ReentrantLock。使用分段锁技术。将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。能给实现真正的并发访问。
  • Segment 继承了 ReentrantLock,表明每个 segment 都可以当做一个锁。这样对每个 segment 中的数据需要同步操作的话都是使用每个 segment 容器对象自身的锁来实现。只有对全局需要改变时锁定的是所有的 segment。

get

它也没有使用锁来同步,只是判断获取的 entry 的 value 是否为 null,为null 时才使用加锁的方式再次去获取。
这里可以看出并没有使用锁,但是value 的值为 null 时候才是使用了加锁!!!get方法无需加锁的,由于其中涉及到的共享变量都使用volatile修饰,volatile可以保证内存可见性,所以不会读取到过期数据。

先判断一下 count != 0;count 变量表示 segment 中存在 entry 的个数。如果为0就不用找了。假设这个时候恰好另一个线程 put 或者remove 了 这个 segment 中的一个 entry,会不会导致两个线程看到的count 值不一致呢?看一下 count 变量的定义: transient volatile int count;它使用了volatile 来修改。我们前文说过,Java5 之后,JMM 实现了对 volatile 的保证:对 volatile 域的写入操作happens-before 于每一个后续对同一个域 的读写操作。所以,每次判断 count 变量的时候,即使恰好其他线程改变了segment 也会体现出来。

如果另一个线程新增的这个 entry 又恰好是我们要 get 的,这事儿就比较微妙了。下图大致描述了 put 一个新的entry
的过程。因为每个HashEntry 中的next 也是final 的,没法对链表最后一个元素增加一个后续entry 所以新增一个 entry
的实现方式只能通过头结点来插入了。newEntry 对象是通过 new HashEntry(K k , V v, HashEntry
next) 来创建的。如果另一个线程刚好 new 这个对象时,当前线程来 get 它。因为没有同步,就可能会出现当前线程得到的
newEntry 对象是一个没有完全构造好的对象引用。 如果在这个 new 的对象的后面,则完全不影响,如果刚好是这个 new 的
对象,那么当刚好这个对象没有完全构造好,也就是说这个对象的 value 值为null,就出现了如下所示的代码,需要重新加锁再次读取这个值!

put

  1. 将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。
  2. 遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等, 相等
    则覆盖旧的 value。
  3. 不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需
    要扩容。
  4. 最后会解除在 1 中所获取当前 Segment 的锁。

Remove

  • 所有处于待删除节点之后的节点原样保留在链表中
  • 所有处于待删除节点之前的节点被克隆到新链表中
  • 新的头节点是原链表中,删除节点之前的那个节点
    在这里插入图片描述

JDK 1.8

在这里插入图片描述

  1. 其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发
    安全性。
  2. 大于 8 的时候才去红黑树链表转红黑树的阀值,当 table[i]下面的链表长度大于 8 时就转化为红黑树结构。

Put

  1. 根 据 key 计 算 出 hashcode。判断是否需要进行初始化。
  2. 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS
  3. 尝试写入,失败则自旋保证成功。
  4. 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
  5. 如果都不满足,则利用 synchronized 锁写入数据(分为链表写入和红黑树写入)。
  6. 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。

Concurrent HashMap 程安全吗,Concurrent HashMlap如何保证线程安全?

  • HashTable容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问HashTable的线程都必须竞争同一把锁,那
    假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他据程访问。
  • get操作的高效之处在于整个get过程不需要加锁,除非读到的值是空的才会加锁重读。get方法里将要使用的共享变量都定义成volatile,如用于统计当前Segement大小的count字段和用于存储值的HashEntry的value。定义成volatile的变量,能够在线程之间保持可见性,能够被多线程同时读,并且保证不会读到过期的值,但是只能被单线程写(有一种情况可以被多线程写,就是写入的值不依赖于原值),在get操作里只需要读不需要写共享变量count和value,所以可以不用加锁。
  • Put方法首先定位到Segment,然后在Segment里进行插入操作。插入操作需要经历两个步骤,第一步判断是否需要
    对Segment里的HashEntry数组进行扩容,第二步定位添加元素的位置然后放在HashEntry数组里。
  • Jdk1.8中把分段锁变成了Cas+Synchronized
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值