(转载)ConcurrentHashMap 原理
集合是编程中最常用的数据结构。而谈到并发,几乎总是离不开集合这类高级数据结构的支持。比如两个线程需要同时访问一个中间临界区 (Queue),比如常会用缓存作为外部文件的副本(HashMap)。这篇文章主要分析jdk1.5的3种并发集合类型 (concurrent,copyonright,queue)中的ConcurrentHashMap,让我们从原理上细致的了解它们,能够让我们在深 度项目开发中获益非浅。
- V get(Object key, int hash) {
- if (count != 0) { // read-volatile
- HashEntry e = getFirst(hash);
- while (e != null) {
- if (e.hash == hash && key.equals(e.key)) {
- V v = e.value;
- if (v != null)
- return v;
- return readValueUnderLock(e); // recheck
- }
- e = e.next;
- }
- }
- return null;
- }
- V readValueUnderLock(HashEntry e) {
- lock();
- try {
- return e.value;
- } finally {
- unlock();
- }
- }
put操作一上来就锁定了整个segment,这当然是为了并发的安全,修改数据是不能并发进行的,必须得有个判断是否超限的语句以确保容量不足时能够rehash,而比较难懂的是这句int index = hash & (tab.length - 1),原来segment里面才是真正的hashtable,即每个segment是一个传统意义上的hashtable,如上图,从两者的结构就可以看出区别,这里就是找出需要的entry在table的哪一个位置,之后得到的entry就是这个链的第一个节点,如果e!=null,说明找到了,这是就要替换节点的值(onlyIfAbsent == false),否则,我们需要new一个entry,它的后继是first,而让tab[index]指向它,什么意思呢?实际上就是将这个新entry插入到链头,剩下的就非常容易理解了。
- V put(K key, int hash, V value, boolean onlyIfAbsent) {
- lock();
- try {
- int c = count;
- if (c++ > threshold) // ensure capacity
- rehash();
- HashEntry[] tab = table;
- int index = hash & (tab.length - 1);
- HashEntry first = (HashEntry) tab[index];
- HashEntry 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;
- }
- else {
- oldValue = null;
- ++modCount;
- tab[index] = new HashEntry(key, hash, first, value);
- count = c; // write-volatile
- }
- return oldValue;
- } finally {
- unlock();
- }
- }
remove操作非常类似put,但要注意一点区别,中间那个for循环是做什么用的呢?(*号标记)从代码来看,就是将定位之后的所有entry克隆并拼回前面去,但有必要吗?每次删除一个元素就要将那之前的元素克隆一遍?这点其实是由entry 的不变性来决定的,仔细观察entry定义,发现除了value,其他所有属性都是用final来修饰的,这意味着在第一次设置了next域之后便不能再 改变它,取而代之的是将它之前的节点全都克隆一次。至于entry为什么要设置为不变性,这跟不变性的访问不需要同步从而节省时间有关,关于不变性的更多 内容,请参阅之前的文章《线程高级---线程的一些编程技巧》
- V remove(Object key, int hash, Object value) {
- lock();
- try {
- int c = count - 1;
- HashEntry[] tab = table;
- int index = hash & (tab.length - 1);
- HashEntry first = (HashEntry)tab[index];
- HashEntry 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;
- // All entries following removed node can stay
- // in list, but all preceding ones need to be
- // cloned.
- ++modCount;
- HashEntry newFirst = e.next;
- * for (HashEntry p = first; p != e; p = p.next)
- * newFirst = new HashEntry(p.key, p.hash,
- newFirst, p.value);
- tab[index] = newFirst;
- count = c; // write-volatile
- }
- }
- return oldValue;
- } finally {
- unlock();
- }
- }
- static final class HashEntry {
- final K key;
- final int hash;
- volatile V value;
- final HashEntry next;
- HashEntry(K key, int hash, HashEntry next, V value) {
- this.key = key;
- this.hash = hash;
- this.next = next;
- this.value = value;
- }
- }
ConcurrentHashMap 和 HashTable 的速度比较:
util.concurrent
包中的 ConcurrentHashMap
类(也将出现在JDK 1.5中的 java.util.concurrent
包中)是对 Map
的线程安全的实现,比起 synchronizedMap
来,它提供了好得多的并发性。多个读操作几乎总可以并发地执行,同时进行的读和写操作通常也能并发地执行,而同时进行的写操作仍然可以不时地并发进行(相关的类也提供了类似的多个读线程的并发性,但是,只允许有一个活动的写线程) 。ConcurrentHashMap
被设计用来优化检索操作;实际上,成功的 get()
操作完成之后通常根本不会有锁着的资源。要在不使用锁的情况下取得线程安全性需要一定的技巧性,并且需要对Java内存模型(Java Memory Model)的细节有深入的理解。ConcurrentHashMap
实现,加上 util.concurrent
包的其他部分,已经被研究正确性和线程安全性的并发专家所正视。在下个月的文章中,我们将看看 ConcurrentHashMap
的实现的细节。
ConcurrentHashMap
通过稍微地松弛它对调用者的承诺而获得了更高的并发性。检索操作将可以返回由最近完成的插入操作所插入的值,也可以返回在步调上是并发的插入操作所添加的值(但是决不会返回一个没有意义的结果)。由 ConcurrentHashMap.iterator()
返回的 Iterators
将每次最多返回一个元素,并且决不会抛出ConcurrentModificationException
异常,但是可能会也可能不会反映在该迭代器被构建之后发生的插入操作或者移除操作。在对 集合进行迭代时,不需要表范围的锁就能提供线程安全性。在任何不依赖于锁整个表来防止更新的应用程序中,可以使用 ConcurrentHashMap
来替代 synchronizedMap
或 Hashtable
。
上述改进使得 ConcurrentHashMap
能够提供比 Hashtable
高得多的可伸缩性,而且,对于很多类型的公用案例(比如共享的cache)来说,还不用损失其效率。
好了多少?
表 1对 Hashtable
和 ConcurrentHashMap
的可伸缩性进行了粗略的比较。在每次运行过程中, n 个线程并发地执行一个死循环,在这个死循环中这些线程从一个 Hashtable
或者 ConcurrentHashMap
中检索随机的key value,发现在执行 put()
操作时有80%的检索失败率,在执行操作时有1%的检索成功率。测试所在的平台是一个双处理器的Xeon系统,操作系统是Linux。数据显示了10,000,000次迭代以毫秒计的运行时间,这个数据是在将对 ConcurrentHashMap的
操作标准化为一个线程的情况下进行统计的。您可以看到,当线程增加到多个时,ConcurrentHashMap
的性能仍然保持上升趋势,而 Hashtable
的性能则随着争用锁的情况的出现而立即降了下来。
比起通常情况下的服务器应用,这次测试中线程的数量看上去有点少。然而,因为每个线程都在不停地对表进行操作,所以这与实际环境下使用这个表的更多数量的线程的争用情况基本等同。
表 1.Hashtable 与 ConcurrentHashMap在可伸缩性方面的比较
线程数 | ConcurrentHashMap | Hashtable |
1 | 1.00 | 1.03 |
2 | 2.59 | 32.40 |
4 | 5.58 | 78.23 |
8 | 13.21 | 163.48 |
16 | 27.58 | 341.21 |
32 | 57.27 | 778.41 |
(转载)ConcurrentHashMap 原理的更多相关文章
- [转载] ConcurrentHashMap原理分析
转载自http://blog.csdn.net/liuzhengkang/article/details/2916620 集合是编程中最常用的数据结构.而谈到并发,几乎总是离不开集合这类高级数据结构的 ...
- ConcurrentHashMap原理
转载地址:http://www.blogjava.net/xylz/archive/2010/07/20/326661.html 在上一篇中介绍了HashMap的原理,这一节是ConcurrentMa ...
- java并发编程的艺术(四)---ConcurrentHashMap原理解析
本文来源于翁舒航的博客,点击即可跳转原文观看!!!(被转载或者拷贝走的内容可能缺失图片.视频等原文的内容) 若网站将链接屏蔽,可直接拷贝原文链接到地址栏跳转观看,原文链接:https://www.cn ...
- 004 ConcurrentHashMap原理
下面这部分内容转载自: http://www.haogongju.net/art/2350374 JDK5中添加了新的concurrent包,相对同步容器而言,并发容器通过一些机制改进了并发性能.因为 ...
- Java并发编程笔记之ConcurrentHashMap原理探究
在多线程环境下,使用HashMap进行put操作时存在丢失数据的情况,为了避免这种bug的隐患,强烈建议使用ConcurrentHashMap代替HashMap. HashTable是一个线程安全的类 ...
- ConcurrentHashMap原理分析(1.7与1.8)-put和 get 需要执行两次Hash
ConcurrentHashMap 与HashMap和Hashtable 最大的不同在于:put和 get 两次Hash到达指定的HashEntry,第一次hash到达Segment,第二次到达Seg ...
- [转载]SSD原理与实现
[转载]SSD原理与实现 这里只mark一下,对原论文讲解的很好的博文 https://zhuanlan.zhihu.com/p/33544892 这里有一个关于SSD的很好的程序实现,readme里 ...
- ConcurrentHashMap 原理分析
1 为什么有ConcurrentHashMap hashmap是非线程安全的,hashtable是线程安全的,但是所有的写和读方法都有synchronized,所以同一时间只有一个线程可以持有对象,多 ...
- [转载] Thrift原理简析(JAVA)
转载自http://shift-alt-ctrl.iteye.com/blog/1987416 Apache Thrift是一个跨语言的服务框架,本质上为RPC,同时具有序列化.发序列化机制:当我们开 ...
随机推荐
- Android实例-获取程序版本号(XE10+小米2)
相关资料: 383675978群号 实例源码: unit Unit1; interface uses System.SysUtils, System.Types, System.UITypes, Sy ...
- hdu 5745 La Vie en rose DP + bitset优化
http://acm.hdu.edu.cn/showproblem.php?pid=5745 这题好劲爆啊.dp容易想,但是要bitset优化,就想不到了. 先放一个tle的dp.复杂度O(n * m ...
- AOE关键路径
这个算法来求关键路径,其实就是利用拓扑排序,首先求出,每个节点最晚开始时间,再倒退求每个最早开始的时间. 从而算出活动最早开始的时间和最晚开始的时间,如果这两个时间相等,则为关键路径. 时间复杂度为O ...
- OpenStack official programs
What are programs ? The OpenStack project mission is to produce the ubiquitous Open Source Cloud Com ...
- Linux下的图形界面——X Window的安装
X Window即X Windows图形用户接口,是一种计算机软件系统和网络协议,提供了一个基础的图形用户界面(GUI)和丰富的输入设备能力联网计算机.其中软件编写使用广义的命令集,它创建了一个硬件抽 ...
- Java Nio 多线程网络下载
--> 默认最多50个线程 同一文件下载失败延迟超过30秒就结束下载 --> 下载5分钟超时时间,假设5分钟内未下载完就结束下载 --> 依赖 commons-httpclient ...
- Crystal Reports课程01-连接SQL Sever数据库
选择[OLE DB(ADO)] 选择[microsoft DB provider for SQL Sever],点击[下一步] 填写连接的服务器,数据库,用户名,密码等信息,然后点击[下一步] 选择[ ...
- android圆角View实现及不同版本这间的兼容(android3.0过后的版本)
http://blog.csdn.net/lovecluo/article/details/8710174 在做我们自己的APP的时候,为了让APP看起来更加的好看,我们就需要将我们的自己的View做 ...
- [AngularJS] Introduction to ui-router
Introduce to basic $stateProvider.state() with $stateParams services. Understand how nested router w ...
- 项目中oracle存储过程记录——经常使用语法备忘
项目中oracle存储过程记录--经常使用语法备忘 项目中须要写一个oracle存储过程,需求是收集一个复杂查询的内容(涉及到多张表),然后把符合条件的记录插入到目标表中.当中原表之中的一个的日期字段 ...