【Java并发编程】深入分析ConcurrentHashMap(九)
来源:互联网 发布:什么样买卖数据 编辑:程序博客网 时间:2024/06/05 11:29
转自:http://blog.csdn.net/liulongling/article/details/50717706
本章是提高教程可能对于刚入门同学来说会有些难度,读懂本章你需要了解以下知识点:
一、 【Java基础提高】深入分析final关键字(一)
二、 【Java并发编程】深入分析volatile(四)
三、 【Java基础提高】HashTable源码分析(六)
一、Concurrent源码分析
ConcurrentHashMap是由Segment(桶)、HashEntry(节点)2大数据结构组成。如下图所示:
1.1 Segment类和属性
Segment继承了ReentrantLock,这意味着每个segment都可以当做一个锁,每一把锁只锁住整个容器中的部分数据,这样不影响线程访问其它数据,当然如果是对全局改变时会锁定所有的segment段。比如:size()和containsValue(),注意的是要按顺序锁定所有段,操作完毕后,再按顺序释放所有段的锁。如果不按顺序的话,有可能会出现死锁。
1.2 HashEntry类和属性
类似与HashMap节点Entry,HashEntry也是一个单向链表,它包含了key、hash、value和下一个节点信息。HashEntry和Entry的不同点:
不同点一:使用了多个final关键字(final class 、final hash) ,这意味着不能从链表的中间或尾部添加或删除节点,后面删除操作时会讲到。
不同点二:使用volatile,是为了更新值后能立即对其它线程可见。这里没有使用锁,效率更高。
1.3 类的初始化
ConcurrencyLevel默认情况下内部按并发级别为16来创建。对于每个segment的容量,默认情况也是16。其中concurrentLevel和segment的初始容量都是可以通过构造函数设定的。要注意的是ConcurrencyLevel一经指定,不可改变,后续如果ConcurrentHashMap的元素数量增加导致ConrruentHashMap需要扩容,ConcurrentHashMap不会增加Segment的数量,而只会增加Segment中链表数组的容量大小,这样的好处是扩容过程不需要对整个ConcurrentHashMap做rehash,而只需要对Segment里面的元素做一次rehash就可以了。
1.4 ensureSegment()方法
该方法返回给定索引位置的Segment,如果Segment不存在,则参考Segment表中的第一个Segment的参数创建一个Segment并通过CAS操作将它记录到Segment表中去。1.5 entryAt()方法
entryAt()方法是从链表中查找节点。在方法参数里注意到有传入tab链表数组和index索引,那为什么还要调用entryAt()方法获取数组项的值而不是通过tab[index]方式直接获取?那我们从源头(put)开始分析,见1.6put()操作。1.6 put()操作
1.6.1 锁分离技术
大家知道HashTable是使用了synchronized来保证线程安全,但是其效率非常差。它效率非常差的原因是多个线程访问HashTable时需要竞争同一把锁,如果我们有多把锁,每一把锁只锁住一部分数据,那么多线程在访问不同的数据时也就不会存在竞争,能提高访问效率。这种做法我们称为锁分离技术。在《Java并发编程实战》一书中作者提到过分拆锁和分离锁技术:分拆锁(lock spliting)就是若原先的程序中多处逻辑都采用同一个锁,但各个逻辑之间又相互独立,就可以拆(Spliting)为使用多个锁,每个锁守护不同的逻辑。
分拆锁有时候可以被扩展,分成可大可小加锁块的集合,并且它们归属于相互独立的对象,这样的情况就是分离锁(lock striping)。
而ConcurrentHashMap就是使用了分离锁技术,对每个Segment配置一把锁,如下图所示:
1.6.2 源码分析
Segment的put操作原理如下图所示,图中展示的不是很详细,其中关于加锁的步骤没有加上去,原因是加了几次觉得加锁后看着很复杂。用图片展示是为了更加简单和明了,如果看着复杂也就没有意义了,我尽量用文字说清楚它的步骤。
步骤一:进入Segment的put操作时先进行加锁保护。如果加锁没有成功,调用scanAndLockForPut方法(详细步骤见下面scanAndLockForPut()源码分析)进入自旋状态,该方法持续查找key对应的节点链中是已存在该机节点,如果没有找到,则预创建一个新节点,并且尝试n次,直到尝试次数操作限制,才真正进入加锁等待状态,自旋结束并返回节点(如果返回了一个非空节点,则表示在链表中没有找到相应的节点)。对最大尝试次数,目前的实现单核次数为1,多核为64。
步骤二:使用(tab.length - 1) & hash计算第一个节点位置,再通过entryAt()方法去查找第一个节点。如果节点存在,遍历链表找到key值所在的节点,如果找到了这个节点则直接更新旧value,结束循环。其中value使用了volatile,它更新后的值立马对其它线程可见。如果节点不存在,将步骤一预创建的新节点(如果没有则重新创建)添加到链表中,添加前先检查添加后节点数量是否超过容器大小,如果超过了,则rehash操作。没有的话调用setNext或setEntryAt方法添加新节点;
要注意的是在更新链表时使用了Unsafe.putOrderedObject()方法,这个方法能够实现非堵塞的写入,这些写入不会被Java的JIT重新排序指令(instruction reordering),使得它能更加快速的存储。
解决1.5问题:为什么还要调用entryAt()方法获取数组项的值而不是通过tab[index]方式直接获取?
虽然在开始时volatile table将引用赋值给了变量tab,但是多线程下table里的值可能发生改变,使用tab[index]并不能获得最新的值。。为了保证接下来的put操作能够读取到上一次的更新结果,需要使用volatile的语法去读取节点链的链头.
1.5 segmentForHash()方法
1.6 scanAndLockForPut()方法
在下面代码中,它先获取key对应的头节点,进入链表循环。如果链表中不存在要插入的节点,则预创建一个新节点,否则retries值递增,直到操作最大尝试次数而进入等待状态。这个方法要注意的是:当在自旋过程中发现链表链头发生了变化,则更新节点链的链头,并重置retries值为-1,重新为尝试获取锁而自旋遍历。1.7 get()操作
从代码可以看出get方法并没有调用锁,它使用了volatile的可见性来实现线程安全的。http://ifeve.com/concurrenthashmap/
http://www.jdon.com/performance/java-performance-optimizations-queue.html
- 【Java并发编程】深入分析ConcurrentHashMap(九)
- 【Java并发编程】深入分析ConcurrentHashMap(九)
- Java并发编程之ConcurrentHashMap原理分析
- 聊聊并发 深入分析ConcurrentHashMap
- java 深入分析ConcurrentHashMap
- java多线程-专题-聊聊并发(四)深入分析ConcurrentHashMap
- 深入学习java并发编程:ConcurrentHashMap<K, V>实现
- Java并发编程:ConcurrentHashMap原理分析(1.7与1.8)
- Java并发编程:ConcurrenthashMap原理分析(1.7)
- Java并发编程之ConcurrentHashMap
- Java并发编程之ConcurrentHashMap
- Java并发编程之ConcurrentHashMap
- Java并发编程之ConcurrentHashMap
- Java并发编程之ConcurrentHashMap
- Java并发编程之ConcurrentHashMap
- Java并发编程之ConcurrentHashMap
- Java并发编程之ConcurrentHashMap
- Java并发编程之ConcurrentHashMap
- Ubuntu 16.04 安装配置 Nginx 1.12.2
- android中关于屏幕适配的那些事。
- QTC++实现马士兵老师的网络版坦克大战
- centos虚拟机配置静态ip
- 基于tiny4412开发板的I2C子系统写法
- 【Java并发编程】深入分析ConcurrentHashMap(九)
- 在python中,利用GUI调用matlab应用程序
- ffmpeg hls.c分析
- 2017年12月16日17:40:24小记
- js判断键盘按键
- 数据结构C++实验之稀疏矩阵乘法
- 算法分析与设计第十五周
- python正则入门(一)
- js正则验证手机号码有效性