Java 中 ConcurrentHashMap 原理分析

一.Java并发基础

当一个对象或变量可以被多个线程共享的时候,就有可能使得程序的逻辑出现问题。
在一个对象中有一个变量i=0,有两个线程A,B都想对i加1,这个时候便有问题显现出来,关键就是对i加1的这个过程不是原子操作。要想对i进行递增,

第一步就是获取i的值,当A获取i的值为0,在A将新的值写入A之前,B也获取了A的值0,然后A写入,i变成1,然后B也写入i,i这个时候依然是1.
当然java的内存模型没有上面这么简单,在Java Memory Model中,Memory分为两类,main memory和working
memory,main memory为所有线程共享,working memory中存放的是线程所需要的变量的拷贝(线程要对main
memory中的内容进行操作的话,首先需要拷贝到自己的working memory,一般为了速度,working
memory一般是在cpu的cache中的)。Volatile的变量在被操作的时候不会产生working memory的拷贝,而是直接操作main memory,当然volatile虽然解决了变量的可见性问题,但没有解决变量操作的原子性的问题,这个还需要synchronized或者CAS相关操作配合进行。

多线程中几个重要的概念:

可见性

也就说假设一个对象中有一个变量i,那么i是保存在main memory中的,当某一个线程要操作i的时候,首先需要从main
memory中将i 加载到这个线程的working memory中,这个时候working
memory中就有了一个i的拷贝,这个时候此线程对i的修改都在其working memory中,直到其将i从working
memory写回到main memory中,新的i的值才能被其他线程所读取。从某个意义上说,可见性保证了各个线程的working
memory的数据的一致性。 可见性遵循下面一些规则:

  • 当一个线程运行结束的时候,所有写的变量都会被flush回main memory中。
  • 当一个线程第一次读取某个变量的时候,会从main memory中读取最新的。
  • volatile的变量会被立刻写到main memory中的,在jsr133中,对volatile的语义进行增强,后面会提到
  • 当一个线程释放锁后,所有的变量的变化都会flush到main memory中,然后一个使用了这个相同的同步锁的进程,将会重新加载所有的使用到的变量,这样就保证了可见性。

原子性

还拿上面的例子来说,原子性就是当某一个线程修改i的值的时候,从取出i到将新的i的值写给i之间不能有其他线程对i进行任何操作。也就是说保证某
个线程对i的操作是原子性的,这样就可以避免数据脏读。 通过锁机制或者CAS(Compare And Set
需要硬件CPU的支持)操作可以保证操作的原子性。

有序性

假设在main memory中存在两个变量i和j,初始值都为0,在某个线程A的代码中依次对i和j进行自增操作(i,j的操作不相互依赖)

i++;
j++;

由于,所以i,j修改操作的顺序可能会被重新排序。那么修改后的ij写到main
memory中的时候,顺序可能就不是按照i,j的顺序了,这就是所谓的reordering,在单线程的情况下,当线程A运行结束的后i,j的值都加1

了,在线程自己看来就好像是线程按照代码的顺序进行了运行(这些操作都是基于as-if-serial语义的),即使在实际运行过程中,i,j的自增可能

被重新排序了,当然计算机也不能帮你乱排序,存在上下逻辑关联的运行顺序肯定还是不会变的。但是在多线程环境下,问题就不一样了,比如另一个线程B的代码
如下


  1. if(j==1) { 
  2.     System.out.println(i); 

按照我们的思维方式,当j为1的时候那么i肯定也是1,因为代码中i在j之前就自增了,但实际的情况有可能当j为1的时候i还是为0。这就是
reordering产生的不好的后果,所以我们在某些时候为了避免这样的问题需要一些必要的策略,以保证多个线程一起工作的时候也存在一定的次序。
JMM提供了happens-before 的排序策略。这样我们可以得到多线程环境下的as-if-serial语义。
这里不对happens-before进行详细解释了,详细的请看这里http://www.ibm.com/developerworks/cn
/java/j-jtp03304/,这里主要讲一下volatile在新的java内存模型下的变化,在jsr133之前,下面的代码可能会出现问题


  1. Map configOptions; 
  2. char[] configText; 
  3. volatile boolean initialized = false; 
  4. // In Thread A 
  5. configOptions = new HashMap(); 
  6. configText = readConfigFile(fileName); 
  7. processConfigOptions(configText, configOptions); 
  8. initialized = true; 
  9. // In Thread B 
  10. while (!initialized) 
  11.   sleep(); 
  12. // use configOptions 

jsr133之前,虽然对 volatile 变量的读和写不能与对其他 volatile 变量的读和写一起重新排序,但是它们仍然可以与对
nonvolatile 变量的读写一起重新排序,所以上面的Thread
A的操作,就可能initialized变成true的时候,而configOptions还没有被初始化,所以initialized先于
configOptions被线程B看到,就产生问题了。

JSR 133 Expert Group 决定让 volatile 读写不能与其他内存操作一起重新排序,新的内存模型下,如果当线程 A
写入 volatile 变量 V 而线程 B 读取 V 时,那么在写入 V 时,A 可见的所有变量值现在都可以保证对 B 是可见的。

结果就是作用更大的 volatile 语义,代价是访问 volatile 字段时会对性能产生更大的影响。这一点在ConcurrentHashMap中的统计某个segment元素个数的count变量中使用到了。

二.线程安全的HashMap

什么时候我们需要使用线程安全的hashmap呢,比如一个hashmap在运行的时候只有读操作,那么很明显不会有问题,但是当涉及到同时有改变
也有读的时候,就要考虑线程安全问题了,在不考虑性能问题的时候,我们的解决方案有Hashtable或者
Collections.synchronizedMap(hashMap),这两种方式基本都是对整个hash表结构做锁定操作的,这样在锁表的期间,
别的线程就需要等待了,无疑性能不高。

三.ConcurrentHashMap实现原理

数据结构 ConcurrentHashMap的目标是实现支持高并发、高吞吐量的线程安全的HashMap。当然不能直接对整个hashtable加锁,所以在ConcurrentHashMap中,数据的组织结构和HashMap有所区别。

一个ConcurrentHashMap由多个segment组成,每一个segment都包含了一个HashEntry数组的
hashtable,
每一个segment包含了对自己的hashtable的操作,比如get,put,replace等操作,这些操作发生的时候,对自己的
hashtable进行锁定。由于每一个segment写操作只锁定自己的hashtable,所以可能存在多个线程同时写的情况,性能无疑好于只有一个
hashtable锁定的情况。

源码分析 在ConcurrentHashMap的remove,put操作还是比较简单的,都是将remove或者put操作交给key所对应的segment去做的,所以当几个操作不在同一个segment的时候就可以并发的进行。


  1. public V remove(Object key) { 
  2.     int hash = hash(key.hashCode()); 
  3.         return segmentFor(hash).remove(key, hash, null); 
  4.     } 

而segment中的remove操作除了加锁之外和HashMap中的remove操作基本无异。


  1. /** 
  2.          * Remove; match on key only if value null, else match both. 
  3.          */ 
  4.         V remove(Object key, int hash, Object value) { 
  5.             lock(); 
  6.             try { 
  7.                 int c = count - 1; 
  8.                 HashEntry<K,V>[] tab = table; 
  9.                 int index = hash & (tab.length - 1); 
  10.                 HashEntry<K,V> first = tab[index]; 
  11.                 HashEntry<K,V> e = first; 
  12.                 while (e != null && (e.hash != hash || !key.equals(e.key))) 
  13.                     e = e.next; 
  14.  
  15.                 V oldValue = null; 
  16.                 if (e != null) { 
  17.                     V v = e.value; 
  18.                     if (value == null || value.equals(v)) { 
  19.                         oldValue = v; 
  20.                         // All entries following removed node can stay 
  21.                         // in list, but all preceding ones need to be 
  22.                         // cloned. 
  23.                         ++modCount; 
  24.                         HashEntry<K,V> newFirst = e.next; 
  25.                         for (HashEntry<K,V> p = first; p != e; p = p.next) 
  26.                             newFirst = new HashEntry<K,V>(p.key, p.hash, 
  27.                                                           newFirst, p.value); 
  28.                         tab[index] = newFirst; 
  29.                         count = c; // write-volatile 
  30.                     } 
  31.                 } 
  32.                 return oldValue; 
  33.             } finally { 
  34.                 unlock(); 
  35.             } 
  36.         } 

上面的代码中关于volatile类型的变量count值得一提,这里充分利用了Java 5中对volatile语义的增强,count =
c的操作必须在modCount,table等操作的后面,这样才能保证这些变量操作的可见性。
Segment类继承于ReentrantLock,主要是为了使用ReentrantLock的锁,ReentrantLock的实现比
synchronized在多个线程争用下的总体开销小。 put操作和remove操作类似。

接下来我们来看下get操作。


  1. public V get(Object key) { 
  2.         int hash = hash(key.hashCode()); 
  3.         return segmentFor(hash).get(key, hash); 
  4.     } 
  5.  
  6. 也是使用了对应的segment的get 
  7.  
  8. V get(Object key, int hash) { 
  9.             if (count != 0) { // read-volatile 
  10.                 HashEntry<K,V> e = getFirst(hash); 
  11.                 while (e != null) { 
  12.                     if (e.hash == hash && key.equals(e.key)) { 
  13.                         V v = e.value; 
  14.                         if (v != null) 
  15.                             return v; 
  16.                         return readValueUnderLock(e); // recheck 
  17.                     } 
  18.                     e = e.next; 
  19.                 } 
  20.             } 
  21.             return null; 
  22.         } 

上面的代码中,一开始就对volatile变量count进行了读取比较,这个还是java5对volatile语义增强的作用,这样就可以获取变
量的可见性。所以count !=
0之后,我们可以认为对应的hashtable是最新的,当然由于读取的时候没有加锁,在get的过程中,可能会有更新。当发现根据key去找元素的时
候,但发现找得的key对应的value为null,这个时候可能会有其他线程正在对这个元素进行写操作,所以需要在使用锁的情况下在读取一下
value,以确保最终的值。

其他相关涉及读取的操作也都类似。

来源:51CTO

时间: 2024-09-14 21:49:39

Java 中 ConcurrentHashMap 原理分析的相关文章

Java中递归原理实例分析_java

本文实例分析了Java中递归原理.分享给大家供大家参考.具体分析如下: 解释:程序调用自身的编程技巧叫做递归. 程序调用自身的编程技巧称为递归( recursion).递归做为一种算法在程序设计语言中广泛应用. 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量.递归的能力在于用有限的语句来定义对象的无限集合.  递归的三个

Java 中ConcurrentHashMap的实现_java

ConcurrentHashMap(简称CHM)是在Java 1.5作为Hashtable的替代选择新引入的,是concurrent包的重要成员.在Java 1.5之前,如果想要实现一个可以在多线程和并发的程序中安全使用的Map,只能在HashTable和synchronized Map中选择,因为HashMap并不是线程安全的.但再引入了CHM之后,我们有了更好的选择.CHM不但是线程安全的,而且比HashTable和synchronizedMap的性能要好.相对于HashTable和sync

Java中getResourceAsStream用法分析_java

本文实例讲述了Java中getResourceAsStream用法.分享给大家供大家参考.具体如下: (一)Java中的getResourceAsStream有以下几种情况: 1. Class.getResourceAsStream(String path) : #path 不以'/'开头时默认是从此类所在的包下取资源: #以'/'开头则是从ClassPath根下获取,其原理是通过path构造一个绝对路径,最终还是由ClassLoader来获取资源. 2. Class.getClassLoade

Java中的浮点数分析

浮点数分为单精度和双精度,Java中的单精度和双精度分别为float和double.你们知道float和double是怎么存储的吗? float占4个字节,double占8个字节,为了方便起见,这里就只讨论float类型. float其实和一个int型的大小是一样的,一共32位,第一位表示符号,2-9表示指数,后面23位表示小数部分.这里不多说,请参考:http://blog.csdn.net/treeroot/archive/2004/09/05/95071.aspx 这里只举一个例子,希望能

Java中的浮点数分析_Java编程

文章来源:csdn 作者:treeroot 浮点数分为单精度和双精度,Java中的单精度和双精度分别为float和double.你们知道float和double是怎么存储的吗? float占4个字节,double占8个字节,为了方便起见,这里就只讨论float类型. float其实和一个int型的大小是一样的,一共32位,第一位表示符号,2-9表示指数,后面23位表示小数部分.这里不多说,请参考:http://blog.csdn.net/treeroot/archive/2004/09/05/9

Java中的ConcurrentHashMap原理分析节选

集合是编程中最常用的数据结构.而谈到并发,几乎总是离不开集合这类高级数据结构的支持.比如两个线程需要同时访问一个中间临界区Queue,比如常会用缓存作为外部文件的副本HashMap.这篇文章主要分析jdk1.5的3种并发集合类型concurrent,copyonright,queue中的ConcurrentHashMap,让我们从原理上细致的了解它们,能够让我们在深度项目开发中获益非浅.      在tiger之前我们使用得最多的数据结构之一就是HashMap和Hashtable.大HashMa

java中匿名内部类解读分析_java

这段时间在看android,看到了java里面的匿名内部类,在印象当中.net里面不支持匿名内部类. 匿名类是不能有名称的类,所以没办法引用它们.必须在创建时,作为new语句的一部分来声明它们.这就要采用另一种形式的new语句,如下所示: new <类或接口> <类的主体> 这种形式的new语句声明一个新的匿名类,它对一个给定的类进行扩展,或者实现一个给定的接口.它还创建那个类的一个新实例,并把它作为语句的结果而返回.要扩展的类和要实现的接口是new语句的操作数,后跟匿名类的主体.

mysql 数据库中索引原理分析说明

下面,我们举例来说明一下聚集索引和非聚集索引的区别: 其实,我们的汉语字典的正文本身就是一个聚集索引.比如,我们要查"安"字,就会很自然地翻开字典的前几页,因为"安"的拼音是"an",而按照拼音排序汉字的字典是以英文字母"a"开头并以"z"结尾的,那么"安"字就自然地排在字典的前部.如果您翻完了所有以"a"开头的部分仍然找不到这个字,那么就说明您的字典中没有这个字:同

JAVA HASHMAP的原理分析

还是来整体看一下HashMap的结构吧. 如下图所示(图没画好),方框代表Hash桶,椭图代表 桶内的元素,在这里就是Key-value对所组成Map.Entry对像. 如果有多个元索被Hash函数定位到同一个桶内,我们称之为hash冲突,桶内的元素组成单向 链表.让我们看一下hashMap JDK源码(因篇幅关系,删除了部分代码与注释,感兴可以查看 JDK1.6源码): public class HashMap<K,V> extends AbstractMap<K,V> impl