ConcurrentHashMap的实现原理

一、为什么要是用ConcurrentHashMap

在并发编程中使用HashMap可能导致程序死循环(这是在JDK1.7中,在JDK1.8中导致的线程不安全主要是put方法可能会导致值被覆盖)。而使用线程安全的HashTable效率又非常低下(全表数据被synchronized修饰),基于以上两个原因,便有了ConcurrentHashMap的登场机会。

1、HashMap线程不安全

HashMap原理请参考:HashMap原理

  • JDK1.7
    • 线程不安全主要体现在发生扩容的时候,重新哈希(rehash)的过程中出现了数据环路,导致的死循环。因为在1.7中,插入新的节点的操作是通过头插法实现的。A、B两个线程同时对HashMap进行扩容操作就容易形成链表的循环。
      在这里插入图片描述
  • JDK1.8
    • 在JDK1.8的时候对环路问题进行了很好地解决,1.8采用的便是尾插法,而且,在JDK8中引入了红黑树的应用,当链表长度大于8并且数组长度大于等于64的时候的时候就换转化为红黑树。
    • 上述死循环问题解决了,但是HashMap仍是线程不安全的。比如说在操作put()方法的时候,此时线程A和线程B同时进行添加操作
      在这里插入图片描述
    • 线程A执行完毕这行代码之后,因为时间片消耗完了,处于挂起状态,注意,此时A已经将数据插入到HashMap中;线程B执行,也进行了相同的操作,假如说他们两个的hashCode相同,便会将原有A插入的节点的值进行覆盖,从而导致线程不安全。
    • 除此之外HashMap的size的数值也会受到影响(此时A、B插入不同的key),A线程执行到这里挂起,注意A进行了++(原来是10,现在变为了11)操作,B线程此前也拿到了原来的size值(10),也对他进行++操作(成为11),此时命名插入了两个节点,但是size的值只加了1,导致线程不安全。
      在这里插入图片描述

2、Hashtable

Hashtable的线程是安全的,我们读他的源码就可以发现,Hashtable在几乎所有的方法上都加上了synchronized锁,从而避免线程不安全的情况,但是Hashtable正是由于这种全表加锁的情况,导致在并发操作的情况下错,导致性能特别低下,效率比较低。
在这里插入图片描述

二、ConcurrentHashMap的结构以及线程安全实现

基于以上两点的考虑,引入了ConcurrentHashMap,他作为Java并发的一种容器进行使用。ConcurrentHashMap在JDK1.7和JDK1.8里面也是不同的,我们分别来讲解。

  • JDK1.7

    ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重 入锁(ReentrantLock),在ConcurrentHashMap里扮演锁的角色;HashEntry则用于存储键值对数 据。一个ConcurrentHashMap里包含一个Segment数组。Segment的结构和HashMap类似,是一种 数组和链表结构。
    在这里插入图片描述

    • 使用分段锁的技术,提升了并发访问的效率。(启发于HashTable)
      HashTable容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问HashTable的线程都必须竞争同一把锁,假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术。首先将数据分成一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
  • JDK1.8

    • 在JDK1.8 ,ConcurrentHashMap主要通过Synchronized + CAS来实现线程安全。
      添加元素的时候
    • 其中initTable()用于里面table数组的初始化。table初始化是没有加锁的,当要初始化时会通过CAS操作将sizeCtl置为-1,而sizeCtl由volatile修饰,保证修改对后面线程可见。这之后如果再有线程执行到此方法时检测到sizeCtl为负数,说明已经有线程在给扩容了,这个线程就会调用Thread.yield()让出一次CPU执行时间。
      private final Node<K,V>[] initTable() {
              Node<K,V>[] tab; int sc;
              while ((tab = table) == null || tab.length == 0) {
                  if ((sc = sizeCtl) < 0)
                      Thread.yield(); 
                  else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { //正在初始化时将sizeCtl设为-1
                      try {
                          if ((tab = table) == null || tab.length == 0) {
                              int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                              @SuppressWarnings("unchecked")
                              Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                              table = tab = nt;
                              sc = n - (n >>> 2);  // 扩容的最大值为新容量的0.75倍
                          }
                      } finally {
                          sizeCtl = sc;
                      }
                      break;
                  }
              }
              return tab;
          }
      
    • put的流程解析
      • 如果数组桶为空,初始化数组桶(自旋 + CAS)
      • 如果桶内为空,CAS放入,不加锁,成功了就直接 break 跳出
      • 如果桶内元素处于移动状态(扩容),就协助扩容
      • 使用 synchronized 加锁加入节点
        在这里插入图片描述
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值