ConcurrentHashMap 源码分析 (一)

     很早就想研究ConcurrentHashMap ,不过一直拖拉,我也是个很容易被新奇好玩的技术吸引的人,这个呢有好也有坏。废话不多说上干货。

    ConcurrentHashMap 最重要的就是引入了Segment 的概念,他在自己内部定义了这个Class来管理数据,这个Segment 类似于HashMap的定义,因为ConcurrentHashMap 会将对应的读写操作交给Segment 。Segment 在ConcurrentHashMap 内部维护一个Segment 数组(默认16个),先将key值的hash值定位到Segment 数组中,取得对应的Segment 之后再利用Segment 的相应方法(对应HashMap里的方法)来读写数据,写操作会加锁。这样做的好处就是,如果有多个线程对ConcurrentHashMap 操作的时候,理想情况下如果key hash到的Segment 是不同的,那么写操作是可以并发执行的。当然像size(),isEmpty(),containsValue(Object value)这些操作涉及到跨Segment 的操作,需要一定的机制来保证,极端情况下需要锁定所有Segment 来做统计。

     Segment 主要继承ReentrantLock,在Java5中,ReentrantLock的性能要远远高于内部锁。在Java6中,由于管理内部锁的算法采用了类似于 ReentrantLock使用的算法,因此内部锁和ReentrantLock之间的性能差别不大。
    ReentrantLock的构造函数提供了两种公平性选择:创建非公平锁(默认)或者公平锁。在公平锁中,如果锁已被其它线程占有,那么请求线程会加入到等待队列中,并按顺序获得锁;在非公平锁中,当请求锁的时候,如果锁的状态是可用,那么请求线程可以直接获得锁,而不管等待队列中是否有线程已经在等待该锁。公平锁的代价是更多的挂起和重新开始线程的性能开销。在多数情况下,非公平锁的性能高于公平锁。Java内部锁也没有提供确定的公平性保证, Java语言规范也没有要求JVM公平地实现内部锁,因此ReentrantLock并没有减少锁的公平性。在中等或者更高负荷下,ReentrantLock有更好的性能,并且拥有可轮询和可定时的请求锁等高级功能。具体请参考:http://www.blogjava.net/killme2008/archive/2007/09/14/145195.html

有时间的话也研究下ReentrantLock的源码。

 

Segment源码分析:

 

 

可以看到真正存放数据的是HashEntry<K, V>[] table ,这里用到了自定义的class HashEntry ,因为ConcurrentHashMap完全允许多个读操作并发进行,读操作并不需要加锁。如果使用传统的技术,如HashMap中的实现,如果允许可以在hash链的中间删除元素,读操作不加锁将得到不一致的数据。ConcurrentHashMap实现技术是保证HashEntry几乎是不可变的。HashEntry代表每个hash链中的一个节点,其结构如下所示:

可以看到除了value不是final的,其它值都是final的,这意味着不能从hash链的中间或尾部删除节点,因为这需要修改next引用值,所有的节点的修改只能从头部开始。对于put操作,可以一律添加到Hash链的头部。但是对于remove操作,可能需要从中间删除一个节点,这就需要将要删除节点的前面所有节点整个复制一遍,最后一个节点指向要删除结点的下一个结点。这在讲解删除操作时还会详述。为了确保读操作能够看到最新的值,将value设置成volatile,这避免了加锁。 

 

 

这里读取采用了加锁的方式,注释里也提到了原因。关于逃逸分析可以参考http://blog.csdn.net/ykdsg/archive/2011/03/17/6255618.aspx

 

其他的操作像:containsKey, containsValue,clear基本上也是采用get这样的循环方式。这里就不具体分析了。接下来看下put方法,因为涉及到扩容操作。

 

其中关键的地方是rehash()方法和++modCount;这个操作,后面会讲到为什么要维护modCount(在改变元素个数的情况下++)。

 

 

 

可以看到rehash()方法就是把table数组扩容一倍,再把原来的引用计算出在新数组的位置e.hash & sizeMask ,然后放过去,原来旧的table数组就交给垃圾回收。

 

remove方法其实就是需要重新建立next链,所以需要复制。

 基本上Segment特殊一点的方法就上面几个了。

 

接下来看ConcurrentHashMap是怎么使用的。因为ConcurrentHashMap做了二次hash,一些方法像entrySet()方法就要重写了。

 

其中HashIterator 的方法advance会循环Segment和其中的table数据,并分别记录下标,下次会在原来的下标继续循环。

 

构造函数

  

 

就是初始化一些基本的值,初始化好Segment数组,接下来的几个构造函数都是调用这个,只是提供了一些初始值,默认的Segment数组长度是16,装载因子是0.75f,初始容量是16。因为构造好Map之后,Segment数组是不会扩容的,如果要放的数据比较多的话,传入比较大的concurrencyLevel 可以支持比较好的并发性。

 

现在看下get方法的实现

 很简单,就是先根据hash找到对应的segment ,然后再调用segment 的get方法。其他的像put ,containsKey,replace 等这些值涉及到单个segment 的操作都是类似的。

 

下面看下涉及到跨段操作的几个方法

 

可以看到跨Segment的操作中,先是是不锁表的,但是在多线程的情况下,就会造成数据的不一致,这里就用到了Segment中的modCount来做比较,如果modCount有变化就说明被其他线程污染了,就需要重新做统计,这个时候也是不带锁的。但是这样的循环不可能无限进行下去,所以做了限制,在不带锁的情况下允许进行2次尝试,如果还是受到其他线程的污染,那就要加锁统计了。注意要顺序的加锁再顺序的解锁,不然可能会出现死锁。containsValue的是实现与size相似。

 

本文基于java6 的源码分析,对一些英文的翻译不一定很到位,一些理解上也存在偏颇,欢迎大家指正。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值