ConcurrentHashMap实现原理

原创 2016年03月29日 14:40:47

ConcurrentHashMap是Java1.5中引用的一个线程安全的支持高并发的HashMap集合类。这篇文章总结了ConcurrentHashMap的内部实现原理,是对于自己理解后的一些整理。


1.HashTable与ConcurrentHashMap的对比

HashTable本身是线程安全的,写过Java程序的都知道通过加Synchronized关键字实现线程安全,这样对整张表加锁实现同步的一个缺陷就在于使程序的效率变得很低。这就是为什么Java中会在1.5后引入ConcurrentHashMap的原因。

内部结构的对比

从图中可以看出,HashTable的锁加在整个Hash表上,而ConcurrentHashMap将锁加在segment上(每个段上),这样我们在对segment1操作的时候,同时也可以对segment2中的数据操作,这样效率就会高很多。

2.ConcurrentHashMap的内部结构

这里写图片描述

ConcurrentHashMap主要有三大结构:整个Hash表,segment(段),HashEntry(节点)。每个segment就相当于一个HashTable。

(1)HashEntry类

每个HashEntry代表Hash表中的一个节点,在其定义的结构中可以看到,除了value值没有定义final,其余的都定义为final类型,我们知道Java中关键词final修饰的域成为最终域。用关键词final修饰的变量一旦赋值,就不能改变,也称为修饰的标识为常量。这就意味着我们删除或者增加一个节点的时候,就必须从头开始重新建立Hash链,因为next引用值需要改变。

static final class HashEntry<K,V> { 
        final K key;                 // 声明 key 为 final 型
        final int hash;              // 声明 hash 值为 final 型 
        volatile V value;           // 声明 value 为 volatile 型
        final HashEntry<K,V> next;  // 声明 next 为 final 型 

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

由于这样的特性,所以插入Hash链中的数据都是从头开始插入的。例如将A,B,C插入空桶中,插入后的结构为:
这里写图片描述

(2)segment类

Segment 类继承于 ReentrantLock 类,从而使得 Segment 对象能充当锁的角色。每个 Segment 对象用来守护其(成员对象 table 中)包含的若干个桶。

table 是一个由 HashEntry 对象组成的数组。table 数组的每一个数组成员就是散列映射表的一个桶。

count 变量是一个计数器,它表示每个 Segment 对象管理的 table 数组(若干个 HashEntry 组成的链表)包含的 HashEntry 对象的个数。每一个 Segment 对象都有一个 count 对象来表示本 Segment 中包含的 HashEntry 对象的总数。注意,之所以在每个 Segment 对象中包含一个计数器,而不是在 ConcurrentHashMap 中使用全局的计数器,是为了避免出现“热点域”而影响 ConcurrentHashMap 的并发性。

static final class Segment<K,V> extends ReentrantLock implements Serializable {  
 private static final long serialVersionUID = 2249069246763182397L;  
         /** 
          * 在本 segment 范围内,包含的 HashEntry 元素的个数
          * 该变量被声明为 volatile 型,保证每次读取到最新的数据
          */  
         transient volatile int count;  

         /** 
          *table 被更新的次数
          */  
         transient int modCount;  

         /** 
          * 当 table 中包含的 HashEntry 元素的个数超过本变量值时,触发 table 的再散列
          */  
         transient int threshold;  

         /** 
          * table 是由 HashEntry 对象组成的数组
          * 如果散列时发生碰撞,碰撞的 HashEntry 对象就以链表的形式链接成一个链表
          * table 数组的数组成员代表散列映射表的一个桶
          * 每个 table 守护整个 ConcurrentHashMap 包含桶总数的一部分
          * 如果并发级别为 16,table 则守护 ConcurrentHashMap 包含的桶总数的 1/16 
          */  
         transient volatile HashEntry<K,V>[] table;  

         /** 
          * 装载因子
          */  
         final float loadFactor;  
 }
ConcurrentHashMap 类

默认的情况下,每个ConcurrentHashMap 类会创建16个并发的segment,每个segment里面包含多个Hash表,每个Hash链都是有HashEntry节点组成的。

 public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>  
         implements ConcurrentMap<K, V>, Serializable {  
     /** 
      * segments 的掩码值
      * key 的散列码的高位用来选择具体的 segment  
      */  
     final int segmentMask;  

     /** 
      * 偏移量
      */  
     final int segmentShift;  

     /** 
      * 由 Segment 对象组成的数组,每个都是一个特别的Hash Table
      */  
     final Segment<K,V>[] segments;  
 }

3.用分离锁实现多个线程间的并发写操作

(1)Put方法的实现
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);  
    }  

 V put(K key, int hash, V value, boolean onlyIfAbsent) {  
            lock();    //当前的segment加锁
            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(); //解锁 
            }  
        }  

整个代码通过注释很好理解了,稍微要注意的是这里的加锁是针对具体的segment,而不是对整个ConcurrentHashMap。Put方法从源码上可以看出是从链表的头部插入新的数据的。

(2)Get方法的实现
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; 
        }

ConcurrentHashMap中的读方法不需要加锁,所有的修改操作在进行结构修改时都会在最后一步写count 变量,通过这种机制保证get操作能够得到几乎最新的结构更新。

(3)Remove方法的实现
V remove(Object key, int hash, Object value) { 
            lock(); //加锁
            try{ 
                int c = count - 1; 
                HashEntry<K,V>[] tab = table; 
                //根据散列码找到 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 = null; 
                if(e != null) { 
                    V v = e.value; 
                    if(value == null|| value.equals(v)) { //找到要删除的节点
                        oldValue = v; 
                        ++modCount; 
                        //所有处于待删除节点之后的节点原样保留在链表中
                        //所有处于待删除节点之前的节点被克隆到新链表中
                        HashEntry<K,V> newFirst = e.next;// 待删节点的后继结点
                        for(HashEntry<K,V> p = first; p != e; p = p.next) 
                            newFirst = new HashEntry<K,V>(p.key, p.hash, 
                                                          newFirst, p.value); 
                        //把桶链接到新的头结点
                        //新的头结点是原链表中,删除节点之前的那个节点
                        tab[index] = newFirst; 
                        count = c;      //写 count 变量
                    } 
                } 
                return oldValue; 
            } finally{ 
                unlock(); //解锁
            } 
        }

整个操作是在持有段锁的情况下执行的,空白行之前的行主要是定位到要删除的节点e。接下来,如果不存在这个节点就直接返回null,否则就要将e前面的结点复制一遍,尾结点指向e的下一个结点。e后面的结点不需要复制,它们可以重用。

中间那个for循环是做什么用的呢?从代码来看,就是将定位之后的所有entry克隆并拼回前面去,但有必要吗?每次删除一个元素就要将那之前的元素克隆一遍?这点其实是由entry的不变性来决定的,仔细观察entry定义,发现除了value,其他所有属性都是用final来修饰的,这意味着在第一次设置了next域之后便不能再改变它,取而代之的是将它之前的节点全都克隆一次。至于entry为什么要设置为不变性,这跟不变性的访问不需要同步从而节省时间有关。

执行删除之前的原链表:
这里写图片描述

执行删除之后的新链表
这里写图片描述

注意:新链表在clone的时候。顺序发生反转,A->B变为B->A。

(4)containsKey方法的实现

containsKey方法操作相对简单,因为它不需要读取值。

boolean containsKey(Object key, int hash) {  
     if (count != 0) { // read-volatile  
         HashEntry<K,V> e = getFirst(hash);  
         while (e != null) {  
             if (e.hash == hash && key.equals(e.key))  
                 return true;  
             e = e.next;  
         }  
     }  
     return false;  
 } 

4.总结

在使用锁来协调多线程间并发访问的模式下,减小对锁的竞争可以有效提高并发性。有两种方式可以减小对锁的竞争:

  • 减小请求同一个锁的频率。
  • 减少持有锁的时间。

ConcurrentHashMap 的高并发性主要来自于三个方面:

  • 用分离锁实现多个线程间的更深层次的共享访问。
  • 用 HashEntery 对象的不变性来降低执行读操作的线程在遍历链表期间对加锁的需求。
  • 通过对同一个 Volatile 变量的写 / 读访问,协调不同线程间读 / 写操作的内存可见性。

使用分离锁,减小了请求同一个锁的频率。

5.感谢

本文是对以下两篇博客的整理加自己的理解,感谢大家的分享。
探索 ConcurrentHashMap 高并发性的实现机制
Java集合—ConcurrentHashMap原理分析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/dingji_ping/article/details/51005799

ConcurrentHashMap原理详解

下面这部分内容转载自:   http://www.haogongju.net/art/2350374   JDK5中添加了新的concurrent包,相对同步容器而言,并发容器通过一些机制改进了并...
  • Sherry_Rui
  • Sherry_Rui
  • 2016-05-20 16:15:58
  • 6981

ConcurrentHashMap原理分析

    集合是编程中最常用的数据结构。而谈到并发,几乎总是离不开集合这类高级数据结构的支持。比如两个线程需要同时访问一个中间临界区(Queue),比如常会用缓存作为外部文件的副本(HashMap)。这...
  • liuzhengkang
  • liuzhengkang
  • 2008-09-12 11:12:00
  • 61937

Java集合---ConcurrentHashMap原理分析(面试问题:ConcurrentHashMap实现原理是怎么样的)

集合是编程中最常用的数据结构。而谈到并发,几乎总是离不开集合这类高级数据结构的支持。比如两个线程需要同时访问一个中间临界区(Queue),比如常会用缓存作为外部文件的副本(HashMap)。这篇文章主...
  • hxpjava1
  • hxpjava1
  • 2017-02-18 19:22:21
  • 2267

HashTable和HashMap的区别详解

一、HashMap简介       HashMap是基于哈希表实现的,每一个元素是一个key-value对,其内部通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长。       Has...
  • fujiakai
  • fujiakai
  • 2016-06-04 19:35:53
  • 66349

jdk1.8中ConcurrentHashMap的实现原理

并发环境下为什么使用ConcurrentHashMap 1. HashMap在高并发的环境下,执行put操作会导致HashMap的Entry链表形成环形数据结构,从而导致Entry的next节点始终...
  • fjse51
  • fjse51
  • 2017-02-16 16:37:54
  • 7618

Java并发编程(五)ConcurrentHashMap的实现原理和源码分析

在Java1.5中,并发编程大师Doug Lea给我们带来了concurrent包,而该包中提供的ConcurrentHashMap是线程安全并且高效的HashMap,本节我们就来研究下Concurr...
  • itachi85
  • itachi85
  • 2016-07-21 11:12:28
  • 7805

ConcurrentHashMap的底层实现

并发基础概念内存模型Memory分为两类,main memory和working memory,main memory为所有线程共享,working memory中存放的是线程所需要的变量的拷贝(线程...
  • Adrian_Dai
  • Adrian_Dai
  • 2018-03-04 11:55:08
  • 48

JDK1.8逐字逐句带你理解ConcurrentHashMap

在前几篇博文中我详细介绍了HashMap的底层实现原理,后来我接连写了三天JVM和GC的一些知识,那些知识偏向于理论。今天换点口味,和大家一起研究学习一下ConcurrentHashMap的底层实现,...
  • u012403290
  • u012403290
  • 2017-03-28 14:30:42
  • 4792

java-并发-ConcurrentHashMap高并发机制-jdk1.8

JDK8的版本,与JDK6的版本有很大的差异。实现线程安全的思想也已经完全变了,它摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现,利用CAS算法。它沿用了与它同时期的HashMap版...
  • youdianjinjin
  • youdianjinjin
  • 2016-07-23 19:18:30
  • 3734

concurrenthashmap 锁分段机制

http://uule.iteye.com/blog/1513976    非常感谢!       concurrenthashmap是一个非常好的map实现,在...
  • qq546770908
  • qq546770908
  • 2016-11-16 11:30:28
  • 488
收藏助手
不良信息举报
您举报文章:ConcurrentHashMap实现原理
举报原因:
原因补充:

(最多只允许输入30个字)