基于Java并发容器ConcurrentHashMap#put方法解析

jdk1.7.0_79

HashMap可以说是每个Java程序员用的最多的数据结构之一了,无处不见它的身影。关于HashMap,通常也能说出它不是线程安全的。这篇文章要提到的是在多线程并发环境下的HashMap——ConcurrentHashMap,显然它必然是线程安全的,同样我们不可避免的要讨论散列表,以及它是如何实现线程安全的,它的效率又是怎样的,因为对于映射容器还有一个Hashtable也是线程安全的但它似乎只出现在笔试、面试题里,在现实编码中它已经基本被遗弃。

关于HashMap的线程不安全,在多线程并发环境下它所带来的影响绝不仅仅是出现脏数据等数据不一致的情况,严重的是它有可能带来程序死循环,这可能有点不可思议,但确实在不久前的项目里同事有遇到了CPU100%满负荷运行,分析结果是在多线程环境下HashMap导致程序死循环。对于Hashtable,查看其源码可知,Hashtable保证线程安全的方式就是利用synchronized关键字,这样会导致效率低下,但对于ConcurrentHashMap则采用了不同的线程安全保证方式——分段锁。它不像Hashtable那样将整个table锁住而是将数组元素分段加锁,如果线程1访问的元素在分段segment1,而线程2访问的元素在分段segment2,则它们互不影响可以同时进行操作。如果合理的进行分段就是其关键问题。

ConcurrentHashMap和HashMap的结果基本一致,同样也是Entry作为存放数据的对象,另外一个就是上面提到的分段锁——Segment。它继承自ReentrantLock(关于ReentrantLock,可参考《5.Lock接口及其实现ReentrantLock》),故它具有ReentrantLock一切特性——可重入,独占等。

ConcurrentHashMap的结构图如下所示:

可以看到相比较于HashMap,ConcurrentHashMap在Entry数组之上是Segment,这个就是我们上面提到的分段锁,合理的确定分段数就能更好的提高并发效率,我们来看ConcurrentHashMap是如何确定分段数的。

ConcurrentHashMap的初始化时通过其构造函数public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel)完成的,若在不指定各参数的情况下,初始容量initialCapacity=DAFAULT_INITIAL_CAPACITY=16,负载因子loadFactor=DEFAULT_LOAD_FACTOR=0.75f,并发等级concurrencyLevel=DEFAULT_CONCURRENCY_LEVEL=16,前两者和HashMap相同。至于负载因子表示一个散列表的空间的使用程度,initialCapacity(总容量) * loadFactor(负载因子) = 数据量,有此公式可知,若负载因子越大,则散列表的装填程度越高,也就是能容纳更多的元素,但这样元素就多,链表就大,此时索引效率就会降低。若负载因子越小,则相反,索引效率就会高,换来的代价就是浪费的空间越多。并发等级它表示估计最多有多少个线程来共同修改这个Map,稍后可以看到它和segment数组相关,segment数组的长度就是通过concurrencyLevel计算得出。

//以默认参数为例initalCapacity=16,loadFactor=0.75,concurrencyLevel=16
public ConcurrentHashMap(int initalCapacity, float loadFactor, int concurrencyLevel) {
  if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
    throw new IllegalArgumentException();
  if (concurrencyLevel > MAX_SEGMENTS)
    concurrencyLevel = MAX_SEGMENTS;
  int sshift = 0;
  int ssize = 1;//segment数组长度
  while (ssize < concurrencyLevel) {
    ++sshift;
    ssize <= 1;
  }//经过ssize左移4位后,ssize=16,ssift=4
/*segmentShift用于参与散列运算的位数,segmentMask是散列运算的掩码,这里有关的散列函数运算和HashMap有类似之处*/
  this.segmentShift = 32 – ssift;//段偏移量segmentShift=28
  this.segmentMask = ssize – 1;//段掩码segmentMask=15(1111)
  if (initialCapacity > MAXIMUM_CAPACITY)
    initialCapacity = MAXIMUM_CAPACITY;
  int c = initialCapacity / ssize;//c = 1
  if (c * ssize < initialCapacity)
    ++c;
  int cap = MIN_SEGMENT_TABLE_CAPACITY;//MIN_SEGMENT_TABLE_CAPACITY=2
  while (cap < c)//cap = 2, c = 1,false
   cap <<= 1;//cap是segment里HashEntry数组的长度,最小为2
/*创建segments数组和segment[0]*/
  Segment<K,V> s0 = new Segment<K,V>(loadFactor, (int)(cap * loadFactor), (HashEntry<K,V>[]) new HashEntry[cap]);//参数意为:负载因子=1,数据容量=(int)(2 * 0.75)=1,总容量=2,故每个Segment的HashEntry总容量为2,实际数据容量为1
  Segment<K,V> ss = (Segment<K,V>[])new Segment[ssize];//segments数组大小为16
  UNSAFE.putOrderedObject(ss, SBASE, s0);
  this.segments = ss;
}

以上就是整个初始化过程,主要是初始化segments的长度大小以及通过负载因子确定每个Segment的容量大小。确定好Segment过后,接下来的重点就是如何准确定位Segment。定位Segment的方法就是通过散列函数来定位,先通过hash方法对元素进行二次散列,这个算法较为复杂,其目的只有一个——减少散列冲突,使元素能均匀分布在不同的Segment上,提高容器的存取效率。

我们通过最直观最常用的put方法来观察ConcurrentHashMap是如何通过key值计算hash值在定位到Segment的:

//ConcurrentHashMap#put
public V put(K key, V value) {
  Segment<K,V> s;
  if (value == null)
    throw new NullPointerException();
  int hash = hash(key);//根据散列函数,计算出key值的散列值
  int j = (hash >>> segmentShift) & segmentMask;//这个操作就是定位Segment的数组下标,jdk1.7之前是segmentFor返回Segment,1.7之后直接就取消了这个方法,直接计算数组下标,然后通过偏移量底层操作获取Segment
  if ((s = (Segment<K,V>)UNSAFE.getObject   // nonvolatile; recheck
      (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
    s = ensureSegment(j);//通过便宜量定位不到就调用ensureSegment方法定位Segment
  return s.put(key, hash, value, false);
}

Segment.put方法就是将键、值构造为Entry节点加入到对应的Segment段里,如果段中已经有元素(即表示两个key键值的hash值重复)则将最新加入的放到链表的头),整个过程必然是加锁安全的。

不妨继续深入Segment.put方法:

//Segment#put
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
  HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);//非阻塞获取锁,获取成功node=null,失败
  V oldValue;
  try {
    HashEntry<K,V>[] tab = table;//Segment对应的HashEntry数组长度
    int index = (tab.length - 1) & hash;
    HashEntry<K,V> first = entryAt(tab, index);//获取HashEntry数组的第一个值
    for (HashEntry<K,V> e = first;;) {
      if (e != null) {//HashEntry数组已经存在值
        K k;
        if ((k = e.key) == key || (e.hash == hash && key.equals(k))) {//key值和hash值都相等,则直接替换旧值
          oldValue = e.value;
          if (!onlyIfAbsent) {
            e.value = value;
            ++modCount;
          }
          break;
        }
        e = e.next;//不是同一个值则继续遍历,直到找到相等的key值或者为null的HashEntry数组元素
      }
      else {//HashEntry数组中的某个位置元素为null
        if (node != null)
          node.setNext(first);//将新加入节点(key)的next引用指向HashEntry数组第一个元素
        else//已经获取到了Segment锁
          node = new HashEntry<K,V>(hash, key, value, first)
        int c = count + 1;
        if (c > threshold && tab.lenth < MAXIUM_CAPACITY)//插入前先判断是否扩容,ConcurrentHashMap扩容与HashMap不同,ConcurrentHashMap只扩Segment的容量,HashMap则是整个扩容
          rehash(node);
        else
          setEntryAt(tab, index, node);//设置为头节点
        ++modCount;//总容量
        count = c;
        oldValue = null;
        break;
      }
     }
  } finally {
    unlock();
  }
  return oldValue;
}

上面大致就是ConcurrentHashMap加入一个元素的过程,需要明白的就是ConcurrentHashMap分段锁的概念。在JDK1.6中定位Segment较为简单,直接计算出Segment数组下标后就返回具体的Segment,而JDK1.7则通过偏移量来计算,算出为空时,还有一次检查获取Segment,猜测是1.7使用底层native是为了提高效率,JDK1.8的ConcurrentHashMap又有不同,暂未深入研究,它的数据结果似乎变成了红黑树。

有关ConcurrentHashMap的get方法不再分析,过程总结为一句话:根据key值计算出hash值,根据hash值计算出对应的Segment,再在Segment下的HashEntry链表遍历查找。

以上这篇基于Java并发容器ConcurrentHashMap#put方法解析就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • Java容器HashMap与HashTable详解

    1.HashMap HashMap继承抽象类AbstractMap,实现接口Map.Cloneable, Serializable接口.HashMap是一种以键值对存储数据的容器, 由数组+链表组成,其中key和value都可以为空,key的值唯一.HashMap是非线程安全的, 对于键值对<Key,Value>, HashMap内部会将其封装成一个对应的Entry<Key,Value>对象.HashMap的存储空间大小是可以动态改变的: 存储过程 每个对象都有一个对应的HashC

  • 深入理解Java线程编程中的阻塞队列容器

    1. 什么是阻塞队列? 阻塞队列(BlockingQueue)是一个支持两个附加操作的队列.这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空.当队列满时,存储元素的线程会等待队列可用.阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程.阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素. 阻塞队列提供了四种处理方法: 抛出异常:是指当阻塞队列满时候,再往队列里插入元素,会抛出IllegalStateException("Q

  • 用java的spring实现一个简单的IOC容器示例代码

    要想深入的理解IOC的技术原理,没有什么能比的上我们自己实现它.这次我们一起实现一个简单IOC容器.让大家更容易理解Spring IOC的基本原理. 这里会涉及到一些java反射的知识,如果有不了解的,可以自己去找些资料看看. 注意 在上一篇文章,我说,启动IOC容器时,Spring会将xml文件里面配置的bean扫描并实例化,其实这种说法不太准确,所以我在这里更正一下,xml文件里面配置的非单利模式的bean,会在第一次调用的时候被初始化,而不是启动容器的时候初始化.但是我们这次要做的例子是容

  • 迅速掌握Java容器中常用的ArrayList类与Vector类用法

    ArrayList类 List集合的实例化: List<String> l = new ArrayList<String>(); //使用ArrayList类实例化List集合 List<String> l2 = new LinkedList<String>(); //使用LinkedList类实例化List集合 ArrayList常用方法: add(int index, Object obj); addAll(int, Collection coll);

  • Java开发中的容器概念、分类与用法深入详解

    本文实例讲述了Java开发中的容器概念.分类与用法.分享给大家供大家参考,具体如下: 1.容器的概念 在Java当中,如果有一个类专门用来存放其它类的对象,这个类就叫做容器,或者就叫做集合,集合就是将若干性质相同或相近的类对象组合在一起而形成的一个整体 2.容器与数组的关系 之所以需要容器: ① 数组的长度难以扩充 ② 数组中数据的类型必须相同 容器与数组的区别与联系: ① 容器不是数组,不能通过下标的方式访问容器中的元素 ② 数组的所有功能通过Arraylist容器都可以实现,只是实现的方式不

  • 浅析Java的Spring框架中IOC容器容器的应用

    Spring容器是Spring框架的核心.容器将创建对象,它们连接在一起,配置它们,并从创建到销毁管理他们的整个生命周期.在Spring容器使用依赖注入(DI)来管理组成应用程序的组件.这些对象被称为Spring Beans. 容器获得其上的哪些对象进行实例化,配置和组装通过阅读提供的配置元数据的说明.配置元数据可以通过XML,Java注释或Java代码来表示.下面的图是Spring如何工作的高层次图. Spring IoC容器是利用Java的POJO类和配置元数据的产生完全配置和可执行的系统或

  • java容器详细解析

    前言:在java开发中我们肯定会大量的使用集合,在这里我将总结常见的集合类,每个集合类的优点和缺点,以便我们能更好的使用集合.下面我用一幅图来表示 其中淡绿色的表示接口,红色的表示我们经常使用的类. 1:基本概念 Java容器类类库的用途是保存对象,可以将其分为2个概念. 1.1:Collection 一个独立元素的序列,这些元素都服从一条或多条规则.其中List必须按照插入的顺序保存元素.Set不能有重复的元素.Queue按照排队规则来确定对象的产生顺序(通常也是和插入顺序相同) 1.2:Ma

  • 深入理解Java的Spring框架中的IOC容器

    Spring IOC的原型 spring框架的基础核心和起点毫无疑问就是IOC,IOC作为spring容器提供的核心技术,成功完成了依赖的反转:从主类的对依赖的主动管理反转为了spring容器对依赖的全局控制. 这样做的好处是什么呢? 当然就是所谓的"解耦"了,可以使得程序的各模块之间的关系更为独立,只需要spring控制这些模块之间的依赖关系并在容器启动和初始化的过程中将依据这些依赖关系创建.管理和维护这些模块就好,如果需要改变模块间的依赖关系的话,甚至都不需要改变程序代码,只需要将

  • Java的Swing编程中使用SwingWorker线程模式及顶层容器

    使用SwingWorker线程模式 谨慎地使用并发机制对Swing开发人员来说非常重要.一个好的Swing程序使用并发机制来创建不会失去响应的用户接口-不管是什么样的用户交互,程序总能够对其给出响应.创建一个有响应的程序,开发人员必须学会如何在Swing框架中使用多线程. 一个Swing开发人员将会与下面几类线程打交道: (1)Initial threads(初始线程),此类线程将执行初始化应用代码. (2)The event dispatch thread(事件派发线程),所有的事件处理代码在

  • Java多线程编程中的两种常用并发容器讲解

    ConcurrentHashMap并发容器 ConcurrentHashMap可以做到读取数据不加锁,并且其内部的结构可以让其在进行写操作的时候能够将锁的粒度保持地尽量地小,不用对整个ConcurrentHashMap加锁. ConcurrentHashMap的内部结构 ConcurrentHashMap为了提高本身的并发能力,在内部采用了一个叫做Segment的结构,一个Segment其实就是一个类Hash Table的结构,Segment内部维护了一个链表数组,我们用下面这一幅图来看下Con

  • Java容器类的深入理解

    Java容器类包含List.ArrayList.Vector及map.HashTable.HashMap ArrayList和HashMap是异步的,Vector和HashTable是同步的,所以Vector和HashTable是线程安全的,而ArrayList和HashMap并不是线程安全的.因为同步需要花费机器时间,所以Vector和HashTable的执行效率要低于ArrayList和HashMap.Collection├List       接口│├LinkedList       链表

  • java并发容器CopyOnWriteArrayList实现原理及源码分析

    CopyOnWriteArrayList是Java并发包中提供的一个并发容器,它是个线程安全且读操作无锁的ArrayList,写操作则通过创建底层数组的新副本来实现,是一种读写分离的并发策略,我们也可以称这种容器为"写时复制器",Java并发包中类似的容器还有CopyOnWriteSet.本文会对CopyOnWriteArrayList的实现原理及源码进行分析. 实现原理 我们都知道,集合框架中的ArrayList是非线程安全的,Vector虽是线程安全的,但由于简单粗暴的锁同步机制,

随机推荐