Java集合—ConcurrentHashMap原理分析

发表于:2016-9-29 09:54

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:牛奶、不加糖    来源:51Testing软件测试网采编

  整个操作是在持有段锁的情况下执行的,空白行之前的行主要是定位到要删除的节点e。接下来,如果不存在这个节点就直接返回null,否则就要将e前面的结点复制一遍,尾结点指向e的下一个结点。e后面的结点不需要复制,它们可以重用。
  中间那个for循环是做什么用的呢?(*号标记)从代码来看,就是将定位之后的所有entry克隆并拼回前面去,但有必要吗?每次删除一个元素就要将那之前的元素克隆一遍?这点其实是由entry的不变性来决定的,仔细观察entry定义,发现除了value,其他所有属性都是用final来修饰的,这意味着在第一次设置了next域之后便不能再改变它,取而代之的是将它之前的节点全都克隆一次。至于entry为什么要设置为不变性,这跟不变性的访问不需要同步从而节省时间有关
  下面是个示意图
  删除元素之前:
  删除元素3之后:
  第二个图其实有点问题,复制的结点中应该是值为2的结点在前面,值为1的结点在后面,也就是刚好和原来结点顺序相反,还好这不影响我们的讨论。
  整个remove实现并不复杂,但是需要注意如下几点。第一,当要删除的结点存在时,删除的最后一步操作要将count的值减一。这必须是最后一步操作,否则读取操作可能看不到之前对段所做的结构性修改。第二,remove执行的开始就将table赋给一个局部变量tab,这是因为table是 volatile变量,读写volatile变量的开销很大。编译器也不能对volatile变量的读写做任何优化,直接多次访问非volatile实例变量没有多大影响,编译器会做相应优化。
  接下来看put操作,同样地put操作也是委托给段的put方法。下面是段的put方法:
  1. V put(K key, int hash, V value, boolean onlyIfAbsent) {
  2.     lock();
  3.     try {
  4.         int c = count;
  5.         if (c++ > threshold) // ensure capacity
  6.             rehash();
  7.         HashEntry<K,V>[] tab = table;
  8.         int index = hash & (tab.length - 1);
  9.         HashEntry<K,V> first = tab[index];
  10.         HashEntry<K,V> e = first;
  11.         while (e != null && (e.hash != hash || !key.equals(e.key)))
  12.             e = e.next;
  13.
  14.         V oldValue;
  15.         if (e != null) {
  16.             oldValue = e.value;
  17.             if (!onlyIfAbsent)
  18.                 e.value = value;
  19.         }
  20.         else {
  21.             oldValue = null;
  22.             ++modCount;
  23.             tab[index] = new HashEntry<K,V>(key, hash, first, value);
  24.             count = c; // write-volatile
  25.         }
  26.         return oldValue;
  27.     } finally {
  28.         unlock();
  29.     }
  30. }
  该方法也是在持有段锁(锁定整个segment)的情况下执行的,这当然是为了并发的安全,修改数据是不能并发进行的,必须得有个判断是否超限的语句以确保容量不足时能够rehash。接着是找是否存在同样一个key的结点,如果存在就直接替换这个结点的值。否则创建一个新的结点并添加到hash链的头部,这时一定要修改modCount和count的值,同样修改count的值一定要放在最后一步。put方法调用了rehash方法,reash方法实现得也很精巧,主要利用了table的大小为2^n,这里就不介绍了。而比较难懂的是这句int index = hash & (tab.length - 1),原来segment里面才是真正的hashtable,即每个segment是一个传统意义上的hashtable,如上图,从两者的结构就可以看出区别,这里就是找出需要的entry在table的哪一个位置,之后得到的entry就是这个链的第一个节点,如果e!=null,说明找到了,这是就要替换节点的值(onlyIfAbsent == false),否则,我们需要new一个entry,它的后继是first,而让tab[index]指向它,什么意思呢?实际上就是将这个新entry插入到链头,剩下的就非常容易理解了
  修改操作还有putAll和replace。putAll就是多次调用put方法,没什么好说的。replace甚至不用做结构上的更改,实现要比put和delete要简单得多,理解了put和delete,理解replace就不在话下了,这里也不介绍了。
  获取操作
  首先看下get操作,同样ConcurrentHashMap的get操作是直接委托给Segment的get方法,直接看Segment的get方法:
  1. V get(Object key, int hash) {
  2.     if (count != 0) { // read-volatile 当前桶的数据个数是否为0
  3.         HashEntry<K,V> e = getFirst(hash);  得到头节点
  4.         while (e != null) {
  5.             if (e.hash == hash && key.equals(e.key)) {
  6.                 V v = e.value;
  7.                 if (v != null)
  8.                     return v;
  9.                 return readValueUnderLock(e); // recheck
  10.             }
  11.             e = e.next;
  12.         }
  13.     }
  14.     return null;
  15. }
  get操作不需要锁。第一步是访问count变量,这是一个volatile变量,由于所有的修改操作在进行结构修改时都会在最后一步写count 变量,通过这种机制保证get操作能够得到几乎最新的结构更新。对于非结构更新,也就是结点值的改变,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。接下来就是根据hash和key对hash链进行遍历找到要获取的结点,如果没有找到,直接访回null。对hash链进行遍历不需要加锁的原因在于链指针next是final的。但是头指针却不是final的,这是通过getFirst(hash)方法返回,也就是存在 table数组中的值。这使得getFirst(hash)可能返回过时的头结点,例如,当执行get方法时,刚执行完getFirst(hash)之后,另一个线程执行了删除操作并更新头结点,这就导致get方法中返回的头结点不是最新的。这是可以允许,通过对count变量的协调机制,get能读取到几乎最新的数据,虽然可能不是最新的。要得到最新的数据,只有采用完全的同步。
  最后,如果找到了所求的结点,判断它的值如果非空就直接返回,否则在有锁的状态下再读一次。这似乎有些费解,理论上结点的值不可能为空,这是因为 put的时候就进行了判断,如果为空就要抛NullPointerException。空值的唯一源头就是HashEntry中的默认值,因为 HashEntry中的value不是final的,非同步读取有可能读取到空值。仔细看下put操作的语句:tab[index] = new HashEntry<K,V>(key, hash, first, value),在这条语句中,HashEntry构造函数中对value的赋值以及对tab[index]的赋值可能被重新排序,这就可能导致结点的值为空。这里当v为空时,可能是一个线程正在改变节点,而之前的get操作都未进行锁定,根据bernstein条件,读后写或写后读都会引起数据的不一致,所以这里要对这个e重新上锁再读一遍,以保证得到的是正确值。
  1. V readValueUnderLock(HashEntry<K,V> e) {
  2.     lock();
  3.     try {
  4.         return e.value;
  5.     } finally {
  6.         unlock();
  7.     }
  8. }
  另一个操作是containsKey,这个实现就要简单得多了,因为它不需要读取值:
  1. boolean containsKey(Object key, int hash) {
  2.     if (count != 0) { // read-volatile
  3.         HashEntry<K,V> e = getFirst(hash);
  4.         while (e != null) {
  5.             if (e.hash == hash && key.equals(e.key))
  6.                 return true;
  7.             e = e.next;
  8.         }
  9.     }
  10.     return false;
  11. }
22/2<12
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号