ConcurrentHashMap能完全替代HashTable吗?

转载自  ConcurrentHashMap能完全替代HashTable吗?

    关于ConcurrentHashMap在之前的ConcurrentHashMap原理分析中已经解释了原理,而HashTable其实大抵上只是对HashMap的线程安全的封装,在JDK7与JDK8中HashMap的实现中解释了HashMap的原理。

至此你应该能够明白,ConcurrentHashMap与HashTable都可以用于多线程的环境,但是当Hashtable的大小增加到一定的时候,性能会急剧下降,因为迭代时需要被锁定很长的时间。因为ConcurrentHashMap引入了分割(segmentation),不论它变得多么大,仅仅需要锁定map的某个部分,而其它的线程不需要等到迭代完成才能访问map。简而言之,在迭代的过程中,ConcurrentHashMap仅仅锁定map的某个部分,而Hashtable则会锁定整个map。

那么既然ConcurrentHashMap那么优秀,为什么还要有Hashtable的存在呢?ConcurrentHashMap能完全替代HashTable吗?

HashTable虽然性能上不如ConcurrentHashMap,但并不能完全被取代,两者的迭代器的一致性不同的,HashTable的迭代器是强一致性的,而ConcurrentHashMap是弱一致的。 ConcurrentHashMap的get,clear,iterator 都是弱一致性的。 Doug Lea 也将这个判断留给用户自己决定是否使用ConcurrentHashMap。

那么什么是强一致性和弱一致性呢?

get方法是弱一致的,是什么含义?可能你期望往ConcurrentHashMap底层数据结构中加入一个元素后,立马能对get可见,但ConcurrentHashMap并不能如你所愿。换句话说,put操作将一个元素加入到底层数据结构后,get可能在某段时间内还看不到这个元素,若不考虑内存模型,单从代码逻辑上来看,却是应该可以看得到的。

下面将结合代码和java内存模型相关内容来分析下put/get方法。put方法我们只需关注Segment#put,get方法只需关注Segment#get,在继续之前,先要说明一下Segment里有两个volatile变量:counttable;HashEntry里有一个volatile变量:value

Segment#put

V put(K key, int hash, V value, boolean onlyIfAbsent) {lock();try {int c = count;if (c++ > threshold) // ensure capacityrehash();HashEntry[] tab = table;int index = hash & (tab.length - 1);HashEntry first = 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();}
}

Segment#get

V get(Object key, int hash) {if (count != 0) { // read-volatileHashEntry 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;
}

我们如何确定线程1放入某个变量的值是否对线程2可见?当a hb(happen before) c时,a对c可见,那么我们接下来我们只要寻找put和get之间所有可能的执行轨迹上的hb关系。要找出hb关系,我们需要先找出与hb相关的Action。为方便,这里将两段代码放到了一张图片上。

可以注意到,同一个Segment实例中的put操作是加了锁的,而对应的get却没有。根据hb关系中的线程间Action类别,可以从上图中找出这些Action,主要是volatile读写和加解锁,也就是图中画了横线的那些。

put操作可以分为两种情况,一是key已经存在,修改对应的value;二是key不存在,将一个新的Entry加入底层数据结构。

key已经存在的情况比较简单,即if (e != null)部分,前面已经说过HashEntry的value是个volatile变量,当线程1给value赋值后,会立马对执行get的线程2可见,而不用等到put方法结束。

key不存在的情况稍微复杂一些,新加一个Entry的逻辑在else中。那么将new HashEntry赋值给tab[index]是否能立刻对执行get的线程可见呢?我们只需分析写tab[index]与读取tab[index]之间是否有hb关系即可。

假设执行put的线程与执行get的线程的轨迹是这样的

执行put的线程执行get的线程
⑧tab[index] = new HashEntry<K,V>(key, hash, first, value) 
②count = c 
 ③if (count != 0)
 ⑨HashEntry e = getFirst(hash);

tab变量是一个普通的变量,虽然给它赋值的是volatile的table。另外,虽然引用类型(数组类型)的变量table是volatile的,但table中的元素不是volatile的,因此⑧只是一个普通的写操作;count变量是volatile的,因此②是一个volatile写;③很显然是一个volatile读;⑨中getFirst方法中读取了table,因此包含一个volatile读。

根据Synchronization Order,对同一个volatile变量,有volatile写 hb volatile读。在这个执行轨迹中,时间上②在③之前发生,且②是写count,③是读count,都是针对同一个volatile变量count,因此有② hb ③;又因为⑧和②是同一个线程中的,③和⑨是同一个线程中的,根据Program Order,有⑧ hb ②,③ hb ⑨。目前我们有了三组关系了⑧ hb ②,② hb ③,③ hb ⑨,再根据hb关系是可传递的(即若有x hb y且y hb z,可得出x hb z),可以得出⑧ hb ⑨。因此,如果按照上述执行轨迹,⑧中写入的数组元素对⑨中的读取操作是可见的。

再考虑这样一个执行轨迹:

执行put的线程执行get的线程
⑧tab[index] = new HashEntry<K,V>(key, hash, first, value) 
 ③if (count != 0)
②count = c 
 ⑨HashEntry e = getFirst(hash);

这里只是变换了下执行顺序。每条语句的volatile读写含义同上,但它们之间的hb关系却改变了。Program Order是我们一直拥有的,即我们有⑧ hb ②,③ hb ⑨。但这次对volatile的count的读时间上发生在对count的写之前,我们无法得出② hb ⑨这层关系了。因此,通过count变量,在这个轨迹上是无法得出⑧ hb ⑨的。那么,存不存在其它可替换关系,让我们仍能得出⑧ hb ⑨呢?

我们要找的是,在⑧之后有一条语句或指令x,在⑨之前有一条语句或指令y,存在x hb y。这样我们可以有⑧ hb x,x hb y, y hb ⑨。就让我们来找一下是否存在这样的x和y。图中的⑤、⑥、⑦、①存在volatile读写,但是它们在⑧之前,因此对确立⑧ hb ⑨这个关系没有用处;同理,④在⑨之后,我们要找的是⑨之前的,因此也对这个问题无益。前面已经分析过了②,③之间没法确立hb关系。

在⑧之后,我们发现一个unlock操作,如果能在⑨之前找到一个lock操作,那么我们要找的x就是unlock,要找的y就是lock,因为Synchronization Order中有unlock hb lock的关系。但是,很不幸运,⑨之前没有lock操作。因此,对于这样的轨迹,是没有⑧ hb ⑨关系的,也就是说,如果某个Segment实例中的put将一个Entry加入到了table中,在未执行count赋值操作之前有另一个线程执行了同一个Segment实例中的get,来获取这个刚加入的Entry中的value,那么是有可能取不到的!

此外,如果getFirst(hash)先执行,tab[index] = new HashEntry<K,V>(key, hash, first, value)后执行,那么,这个get操作也是看不到put的结果的。

……

正是因为get操作几乎所有时候都是一个无锁操作(get中有一个readValueUnderLock调用,不过这句执行到的几率极小),使得同一个Segment实例上的put和get可以同时进行,这就是get操作是弱一致的根本原因。Java API中对此有一句简单的描述:

Retrievals reflect the results of the most recently completed update operations holding upon their onset.

也就是说API上保证get操作一定能看到已完成的put操作。已完成的put操作肯定在get读取count之前对count做了写入操作。因此,也就是我们第一个轨迹分析的情况。

ConcurrentHashMap#clear

clear方法很简单,看下代码即知。

public void clear() {for (int i = 0; i < segments.length; ++i)segments[i].clear();
}

因为没有全局的锁,在清除完一个segments之后,正在清理下一个segments的时候,已经清理segments可能又被加入了数据,因此clear返回的时候,ConcurrentHashMap中是可能存在数据的。因此,clear方法是弱一致的。

ConcurrentHashMap中的迭代器

ConcurrentHashMap中的迭代器主要包括entrySet、keySet、values方法。它们大同小异,这里选择entrySet解释。当我们调用entrySet返回值的iterator方法时,返回的是EntryIterator,在EntryIterator上调用next方法时,最终实际调用到了HashIterator.advance()方法,看下这个方法:

final void advance() {if (nextEntry != null && (nextEntry = nextEntry.next) != null)return;while (nextTableIndex >= 0) {if ( (nextEntry = currentTable[nextTableIndex--]) != null)return;}while (nextSegmentIndex >= 0) {Segment<K,V> seg = segments[nextSegmentIndex--];if (seg.count != 0) {currentTable = seg.table;for (int j = currentTable.length - 1; j >= 0; --j) {if ( (nextEntry = currentTable[j]) != null) {nextTableIndex = j - 1;return;}}}}
}

这个方法在遍历底层数组。在遍历过程中,如果已经遍历的数组上的内容变化了,迭代器不会抛出ConcurrentModificationException异常。如果未遍历的数组上的内容发生了变化,则有可能反映到迭代过程中。这就是ConcurrentHashMap迭代器弱一致的表现。

总结

ConcurrentHashMap的弱一致性主要是为了提升效率,是一致性与效率之间的一种权衡。要成为强一致性,就得到处使用锁,甚至是全局锁,这就与Hashtable和同步的HashMap一样了。

Reference:

1. http://blog.csdn.net/kobejayandy/article/details/16834311

2. http://ifeve.com/concurrenthashmap-vs-hashtable/

3. http://ifeve.com/concurrenthashmap-weakly-consistent/


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/329067.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

购物车的功能——界面源码

里面所用到的图片资源统一都在“我的资源”里面&#xff0c;相对应的图片是“ 购物车源码相关图片 ”http://download.csdn.net/detail/qq_34137397/9665878&#xff0c; <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.o…

Asp.Net Core 发布和部署( MacOS + Linux + Nginx )

前言 在上篇文章中&#xff0c;主要介绍了 Dotnet Core Run 命令&#xff0c;这篇文章主要是讲解如何在Linux中&#xff0c;对 Asp.Net Core 的程序进行发布和部署。 有关如何在 Jexus 中进行部署&#xff0c;请参见本人的另一篇文章&#xff1a;http://www.cnblogs.com/savorb…

php渐变字,jQuery_jQuery实现的立体文字渐变效果,先截两个图看看: 效果很 - phpStudy...

jQuery实现的立体文字渐变效果先截两个图看看&#xff1a;效果很不错吧&#xff1f;会不会误以为这些字体是图片&#xff1f;这可不是图片&#xff0c;而是用JS实现的在线演示 http://demo.phpstudy.net/js/gradient-test/demo.htm下面来简单分享下实现过程及原理(网站中使用了…

https 单向认证和双向认证

转载自 https 单向认证和双向认证 一、Http HyperText Transfer Protocol&#xff0c;超文本传输协议&#xff0c;是互联网上使用最广泛的一种协议&#xff0c;所有WWW文件必须遵循的标准。HTTP协议传输的数据都是未加密的&#xff0c;也就是明文的&#xff0c;因此使用HTT…

python中将整数转化为八进制的函数,Python进制转化

Python中的进制有二进制、八进制、十进制、十六进制&#xff0c;用python的内置函数可以方便的进行不同进制之间的转换&#xff0c;二、八、十六进制数字表示前面分别添加0b、0o、0x(前面为零)。二进制八进制十进制十六进制0b1010o1271230x1a转化为十进制int()可以接收一个或者…

Asp.Net Core 发布和部署(Linux + Jexus )

前言 在上篇文章中&#xff0c;主要介绍了 Dotnet Core Run 命令&#xff0c;这篇文章主要是讲解如何在 asp.net core 中对我们的已经完成的程序进行发布和部署。 有关如何使用 Nginx 进行部署&#xff0c;请参见本人的另一篇文章&#xff1a;http://www.cnblogs.com/savorboar…

购物车的功能——CSS源码

里面所用到的图片资源统一都在“我的资源”里面&#xff0c;相对应的图片是“ 购物车源码相关图片 ”http://download.csdn.net/detail/qq_34137397/9665878&#xff0c; 此CSS的对应的是“购物车的功能——界面源码”的内容 charset "gb2312"; /* CSS Document */…

Java NIO系列教程(十 五)Java NIO Path

转载自 Java NIO系列教程&#xff08;十 五&#xff09;Java NIO Path译文链接 译者&#xff1a;章筱虎 Java的Path接口是Java NIO2 的一部分&#xff0c;是对Java6 和Java7的 NIO的更新。Java的Path接口在Java7 中被添加到Java NIO&#xff0c;位于java.nio.file包中&#x…

discuz 版块导航function_forumlist.php,Discuz! X2“扩建”左侧版块导航 让社区层次一目了然...

一般情况下&#xff0c;当社区具有大量栏目和版块的时候&#xff0c;用户往往很容易迷失其中&#xff0c;不清楚自己所在的版块位置&#xff0c;同时也很难找到“目的”版块&#xff0c;容易造成不良的用户体验。Discuz!X2针对此问题在“用户体验”方面做了深度优化。Discuz!X2…

学习ASP.NET Core,你必须了解无处不在的“依赖注入”

ASP.NET Core的核心是通过一个Server和若干注册的Middleware构成的管道&#xff0c;不论是管道自身的构建&#xff0c;还是Server和Middleware自身的实现&#xff0c;以及构建在这个管道的应用&#xff0c;都需要相应的服务提供支持&#xff0c;ASP.NET Core自身提供了一个DI容…

Java 进程间文件锁FileLock详解

转载自 Java 进程间文件锁FileLock详解最近需要在两个进程中对同一个文件进行操作&#xff0c;正好Java 提供了文件锁FileLock类&#xff0c;利用这个类可以控制不同程序(JVM)对同一文件的并发访问&#xff0c;实现进程间文件同步操作。FileLock是java 1.4 版本后出现的一个类…

求瑞年的java程序,java 计算瑞年的方法

任何语言都有可能计算某一年是否为瑞年的方法&#xff0c;也就是说一年有 366 天&#xff0c;每隔4 年就出现一次。最基本的算法如下&#xff1a;if year is divisible by 400 thenis_leap_yearelse if year is divisible by 100 thennot_leap_yearelse if year is divisible b…

购物车的功能——JS源码

此CSS的对应的是“购物车的功能——界面源码”的内容和“购物车的功能——CSS源码”的内容&#xff0c;希望大家不要乱 $(function(){//点击复选框全选或全不选效果$("#allCheckBox").click(function(){var checked$(this).is(":checked");$(".cart_t…

.NET Core应用类型(Portable apps amp; Self-contained apps)

介绍 有许多种方式可以用来考虑构建应用的类型&#xff0c;通常类型用来描述一个特定的执行模型或者基于此的应用。举例说&#xff1a;控制台应用&#xff08;Console Application&#xff09;、Web应用&#xff08;Web Application&#xff09;等等。所有这些类型的应用都可以…

Java NIO系列教程(十二) Java NIO与IO

转载自 Java NIO系列教程&#xff08;十二&#xff09; Java NIO与IO 译文地址 作者&#xff1a;Jakob Jenkov 译者&#xff1a;郭蕾 校对&#xff1a;方腾飞 当学习了Java NIO和IO的API后&#xff0c;一个问题马上涌入脑海&#xff1a; 我应该何时使用IO&#xff0c;何…

php oracle 无查询结果,php - Oracle Insert查询不起作用,也不会抛出任何错误 - 堆栈内存溢出...

嗨&#xff0c;我是Oracle的新手。我试图在PHP中使用oracle将记录插入表中。 但它不起作用&#xff0c;也没有抛出任何错误。以下是我的代码。 请告诉我我做错了什么。$insertSQL "INSERT INTO GL_USR_MAIL_FOLDER(FK_GLUSR_USR_ID, GL_USR_MAIL_FOLDER_NAME) VALUES (:US…

.NET Core 1.0发布:微软开源跨平台大布局序幕

在6月27日的红帽DevNation峰会上&#xff0c;微软正式发布了.NET Core 1.0、ASP.NET 1.0和Entity Framework Core 1.0&#xff0c;这些产品将全部支持Windows、OS X和Linux三种操作系统。其中.NET Core最受瞩目&#xff0c;这是一款跨平台、开源且模块化的.NET平台&#xff0c;…

无缝滚动图片——源码

图片已上传&#xff0c;请到我的资源部去下载&#xff0c;不用积分&#xff0c;——无缝滚动图片http://download.csdn.net/detail/qq_34137397/9665933 </head> <body> <div class"control"><label id"chk_pause"><input type…

Java NIO系列教程(十一) Pipe

转载自 Java NIO系列教程&#xff08;十一&#xff09; Pipe 原文链接 作者&#xff1a;Jakob Jenkov 译者&#xff1a;黄忠 校对&#xff1a;丁一 Java NIO 管道是2个线程之间的单向数据连接。Pipe有一个source通道和一个sink通道。数据会被写到sink通道&#…

配置oracle网络连接命令,配置oracle网络环境

向数据库注册实例的方法有静态注册和动态注册两种。对于静态注册来说&#xff0c;我们可以将一列硬编码在listener.ora文件中。动态注册意味着实例在启动时要定位侦听器并注册到侦听器中。动态注册是向侦听器注册一个实例的首选方法&#xff0c;初始化参数LOCAL_LISTENER会告知…