1 为什么有ConcurrentHashMap 
hashmap是非线程安全的,hashtable是线程安全的,但是所有的写和读方法都有synchronized,所以同一时间只有一个线程可以持有对象,多线程情况下锁竞争会比较激烈,严重影响性能。基于这种情况,Doug Lee大师写了一个ConcurrentHashMap类。ConcurrentHashMap是对多线程各种特性深刻理解的经典范例,学习多线程编程不得不学ConcurrentHashMap。

2 特性 
ConcurrentHashMap通过锁拆分机制,降低了锁的争用,写时加锁,读时不加锁,降低了锁的持有时间,所以ConcurrentHashMap在高并发情况下的性能得到了大幅提升,ConcurrentHashMap非常适用于读多写少的场景中。

3 原理 
3.1 锁拆分 
ConcurrentHashMap引入了Segment,通过将键值做hash,数据可以均匀的分布到每个Segment中,每次put,remove等操作的时候,锁的都是当前的Segment,这样就减少了锁的争用。 
4 整体类图和关键数据结构 
4.1 类图

4.2 数据存储

代码片段1: 
ConcurrentHashMap: 
final int segmentMask; 
final int segmentShift; 
final Segment<K,V>[] segments;

代码片段2: 
SegMent: 
transient volatile int count; 
transient int modCount; 
transient int threshold; 
transient volatile HashEntry<K,V>[] table; 
final float loadFactor;

代码片段3: 
HashEntry: 
  final K key; 
  final int hash; 
  volatile V value; 
  final HashEntry<K,V> next;

由上可见,ConcurrentHashMap由Segment数组组成,Segment由table数组组成,每一个table元素都是一个由HashEntry组成的链表结构,hash冲突时会存放到同一个table的链表结构中。键值对保存在HashEntry对象中。

依次插入A B C后,Segment结构示意图:

4.3 Segment特性 
transient volatile HashEntry<K,V>[] table;

是volatile的,避免了读取时加锁,volatile特性约束变量的值在本地线程副本中修改后会立即同步到主线程中,保证了其他线程的可见性。 
4.3 HashEntry 
final K key; 
  final int hash; 
  volatile V value; 
  final HashEntry<K,V> next;

除value外,其他的属性都是final的,value是volatile类型的,都修饰为final表明不允许在此链表结构的中间或者尾部做添加删除操作,每次只允许操作链表的头部。删除元素后,删除元素之后的链表保持不变,删除元素之前的链表重新复制一份,并指向删除元素之后的元素。

例如删除C元素:

注意删除之后原来元素的顺序反转了。

5 关键点: 
5.1 put 
ConcurrentHashMap: 
public V put(K key, V value) { 
        //不允许value为空 
        if (value == null) 
            throw new NullPointerException(); 
        int hash = hash(key.hashCode()); 
//通过segmentFor(hash)找到找到数据所在的segment 
//调用Segment的put方法完成put操作 
        return segmentFor(hash).put(key, hash, value, false); 
    }

Segment: 
V put(K key, int hash, V value, boolean onlyIfAbsent) { 
         //put操作需要先获取锁 
            lock(); 
            try { 
                int c = count; 
//超出界限,进行rehash,table容量扩充1倍。 
                if (c++ > threshold) // ensure capacity 
                    rehash(); 
//找到HashEntry的头               
HashEntry<K,V>[] tab = table; 
                int index = hash & (tab.length - 1); 
                HashEntry<K,V> first = tab[index]; 
                HashEntry<K,V> e = first; 
//遍历查找key值是否已经存在 
                while (e != null && (e.hash != hash || !key.equals(e.key))) 
                    e = e.next;

V oldValue;

if (e != null) {//如果已经存在,则直接替换value 
                    oldValue = e.value; 
                    if (!onlyIfAbsent) 
                        e.value = value; 
                } 
                else {//如果不存在,则插入到表头 
                    oldValue = null; 
                    ++modCount;//用于记录链表结构化调整,跨段求size会用到 
                    tab[index] = new HashEntry<K,V>(key, hash, first, value); 
                    count = c; // write-volatile 
                } 
                return oldValue; 
            } finally { 
                unlock(); 
            } 
        }

5.2 get 
V get(Object key, int hash) { 
            if (count != 0) { // read-volatile 
//获取hashentry的头 
                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; 
//因为put的value不允许为空,所以如果值为空,说明有其他线程正在构造hashentry对象,发生了指令重排序,所以加锁重新读取一次。 
                        return readValueUnderLock(e); // recheck 
                    } 
                    e = e.next; 
                } 
            } 
            return null; 
        }

5.3 size 
final Segment<K,V>[] segments = this.segments; 
        long sum = 0; 
        long check = 0; 
        int[] mc = new int[segments.length]; 
        // Try a few times to get accurate count. On failure due to 
        // continuous async changes in table, resort to locking. 
        for (int k = 0; k < RETRIES_BEFORE_LOCK; ++k) { 
            check = 0; 
            sum = 0; 
            int mcsum = 0; 
//遍历,并记录下每个Segment的modCount值 
            for (int i = 0; i < segments.length; ++i) { 
                sum += segments[i].count; 
                mcsum += mc[i] = segments[i].modCount; 
            } 
            if (mcsum != 0) { 
//再遍历一次,看2次是否相同,如果不相同则再试一次,如果相同则返回size. 
                for (int i = 0; i < segments.length; ++i) { 
                    check += segments[i].count; 
                    if (mc[i] != segments[i].modCount) { 
                        check = -1; // force retry 
                        break; 
                    } 
                } 
            } 
            if (check == sum) 
                break; 
        } 
//尝试2次后,如果仍然不相等,则加锁重新读一遍。 
        if (check != sum) { // Resort to locking all segments 
            sum = 0; 
            for (int i = 0; i < segments.length; ++i) 
                segments[i].lock(); 
            for (int i = 0; i < segments.length; ++i) 
                sum += segments[i].count; 
            for (int i = 0; i < segments.length; ++i) 
                segments[i].unlock(); 
        } 
        if (sum > Integer.MAX_VALUE) 
            return Integer.MAX_VALUE; 
        else 
            return (int)sum;

6. 思考 
6.1 为什么查询可以不加锁? 
1)通过HashEntry的不变性降低读操作加锁的需求。

HashEntry的属性key,next,hash都是final类型的,保证只能在头部修改链表,另外value设置为了volatile,保证了写线程写入后,其他读线程都可以看到新值。

非结构化修改:对于非结构化修改,因为value是volatile类型的,所以写线程修改后,读线程立刻可以看到修改后的值。 
结构化修改:a)put,由于put插入到链表的表头,链表中的原有节点并没有改变,所以读线程可以正常遍历原有的链表 
b)remove ,参见4.3中的图,原有链表也继续保留,所以读线程可以正常遍历链表。

2)用volatile变量协调读写线程的可见性

假设线程M写入count后,线程N读取count。 
根据happen-before法则,A happen-before B, C happen-before D, 又根据volatile法则,B happen-bofere C,所以根据传递规则A happen-before D。 
从get的代码中看,get会首先读取count,所以读线程能够看到之前对链表做的修改。

6.2 什么时候会造成数据不一致? 
线程A先做put操作,线程B后做get操作。

假设put执行到红色注释处,切换到线程B则读到的是线程A put之前的操作,这个概率比较小,并且是允许的,如果要保证严格的一致性,那么只有给读操作加锁。这也印证了每种技术都有其适用的场景那句话,ConcurrentHashMap适用在读多写少的场景下。 
V put(K key, int hash, V value, boolean onlyIfAbsent) { 
              lock(); 
            try { 
                int c = count; 
                if (c++ > threshold) // ensure capacity 
                    rehash(); 
HashEntry<K,V>[] tab = 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; 
//-------------put执行到此处----------- 
                V oldValue;

if (e != null) { 
                    oldValue = e.value; 
                    if (!onlyIfAbsent) 
                        e.value = value; 
                } 
                else { 
                    oldValue = null; 
                    ++modCount; 
                    tab[index] = new HashEntry<K,V>(key, hash, first, value); 
                    count = c; // write-volatile 
                } 
                return oldValue; 
            } finally { 
                unlock(); 
            } 
        }

7 参考资料: 
http://blog.csdn.net/ykdsg/article/details/6257449 
http://bhdweb.iteye.com/blog/1722431 
http://bhdweb.iteye.com/blog/1722432 
http://www.360doc.com/content/12/1105/20/9462341_246041701.shtml 
http://www.gznote.com/2014/04/concurrenthashmap%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.html     * 
http://www.iteye.com/topic/344876 
http://andy136566.iteye.com/blog/1070493

ConcurrentHashMap 原理分析的更多相关文章

  1. ConcurrentHashMap原理分析(1.7与1.8)-put和 get 需要执行两次Hash

    ConcurrentHashMap 与HashMap和Hashtable 最大的不同在于:put和 get 两次Hash到达指定的HashEntry,第一次hash到达Segment,第二次到达Seg ...

  2. [转载] ConcurrentHashMap原理分析

    转载自http://blog.csdn.net/liuzhengkang/article/details/2916620 集合是编程中最常用的数据结构.而谈到并发,几乎总是离不开集合这类高级数据结构的 ...

  3. Java集合:ConcurrentHashMap原理分析

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

  4. 【Java并发编程】1、ConcurrentHashMap原理分析

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

  5. Java 中 ConcurrentHashMap 原理分析

    一.Java并发基础 当一个对象或变量可以被多个线程共享的时候,就有可能使得程序的逻辑出现问题. 在一个对象中有一个变量i=0,有两个线程A,B都想对i加1,这个时候便有问题显现出来,关键就是对i加1 ...

  6. ConcurrentHashMap原理分析(二)-扩容

    概述 在上一篇文章中介绍了ConcurrentHashMap的存储结构,以及put和get方法,那本篇文章就介绍一下其扩容原理.其实说到扩容,无非就是新建一个数组,然后把旧的数组中的数据拷贝到新的数组 ...

  7. ConcurrentHashMap原理分析

    当我们享受着jdk带来的便利时同样承受它带来的不幸恶果.通过分析Hashtable就知道,synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,安全的背后是巨大的浪费,而现在的解 ...

  8. ConcurrentHashMap原理分析(1.7与1.8)

    前言 以前写过介绍HashMap的文章,文中提到过HashMap在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新 ...

  9. 【转】ConcurrentHashMap原理分析(1.7与1.8)

    https://www.cnblogs.com/study-everyday/p/6430462.html 前言 以前写过介绍HashMap的文章,文中提到过HashMap在put的时候,插入的元素超 ...

随机推荐

  1. WordPress4.1新的函数介绍

    wordpress 4.1也更新了一阵子了,作为一般用户最关系的就是新的wordpress主题,作为开发者,新增的函数也给我们带来了更多的便捷和惊喜,今天就转载一篇介绍wordpress4.1中新增的 ...

  2. c++异常机制实现原理

    今天突然看到一篇文章,讲异常机制的实现,所以分享一下:http://baiy.cn/doc/cpp/inside_exception.htm 内容讲的很深,但是编译器的实现是不是真是这样就不知道了,我 ...

  3. PHPCMS 使用图示和PHPCMS二次开发教程(转)

    PHPCMS V9 核心文件说明 模块与控制器 模块: phpcms v9框架中的模块,位于phpcms/modules目录中 每一个目录称之为一个模块.即url访问中的m. 访问content模块示 ...

  4. Chrome rem bug

    遇到一个bug,发现chrome在初始化页面的时候,会错误的渲染rem单位,导致字体过大. 比如: 正常的应该是这样的: 原因是,为了使用rem单位,我们常常将 html 的font-size设置为6 ...

  5. Ubuntu14.04 安装QQ(国际版)

    1.在/etc/apt/source.list文件中添加: deb http://packages.linuxdeepin.com/deepin trusty main non-free univer ...

  6. JavaScript不可变原始值和可变的对象引用

    一.JavaScript不可变原始值 JavaScript中的原始值(undefined,null,布尔值,数字和字符串)与对象(包括了数组和函数)有着根本的区别.原始值是不可变的(undefined ...

  7. TatukGIS-TGIS_Editor.CreateShape

    procedure CreateShape(const _layer: TObject; const _ptg: TGIS_Point3D; const _type: TGIS_ShapeType; ...

  8. bzoj1311: 最优压缩

    Description  其中: Auv是与Aij相邻的像素(为了简化,认为(i-1,j),(i+1,j,(i,j-1),(i,j+1)为相邻元素); Wij取值0或者1,表示Aij修改后取V0或者V ...

  9. .NET 元数据

    1. 安装 ILDASM 工具 VS -- 外部工具 -- 添加 -- 命令行为:C:\Program Files (x86)\Microsoft SDKs\Windows\v8.1A\bin\NET ...

  10. 转:如何找出发生SEGV内存错误的程序

    原文来自于:http://www.searchtb.com/2014/03/%E5%A6%82%E4%BD%95%E6%89%BE%E5%87%BA%E5%8F%91%E7%94%9Fsegv%E5% ...