ConcurrentHashMap实现原理总结--下

       主要研究ConcurrentHashMap的get、put和remove 这3个操作。对于哈希表,Java中采用链表的方式来解决hash冲突的。
        实现了同步的HashTable也是这样的结构,它的同步使用锁来保证的,并且所有同步操作使用的是同一个锁对象。这样若有n个线程同时在get时,这n个线程要串行的等待来获取锁。

       ConcurrentHashMap中对这个数据结构,针对并发稍微做了一点调整。它把区间按照并发级别(concurrentLevel),分成了若干个segment段。默认情况下内部按并发级别为16来创建。对于每个segment的容量,默认情况也是16。当然并发级别(concurrentLevel)和每个段(segment)的初始容量都是可以通过构造函数设定的。

 

       从上面的结构我们可以了解到,ConcurrentHashMap定位一个元素的过程需要进行两次Hash操作,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部,因此,这一种结构的带来的副作用是Hash的过程要比普通的HashMap要长,但是带来的好处是写操作的时候可以只对元素所在的Segment进行加锁即可,不会影响到其他的Segment,这样,在最理想的情况下,ConcurrentHashMap可以最高同时支持Segment数量大小的写操作(刚好这些写操作都非常平均地分布在所有的Segment上),所以,通过这一种结构,ConcurrentHashMap的并发能力可以大大的提高。

  • segment的定义:



       Segment继承了ReentrantLock,表明每个segment都可以当做一个锁。这样对每个segment中的数据需要同步操作的话都是使用每个segment容器对象自身的锁来实现。只有对全局需要改变时锁定的是所有的segment。这种做法,就称之为“分离锁”。

       由此可见,ConcurrentHashMap的实现使用了一个包含 16 个锁的数组,每一个锁都守护 HashMap 的 1/16 。假设 Hash 值均匀分布,这将会把对于锁的请求减少到约为原来的 1/16 。这项技术使得 ConcurrentHashMap 能够支持 16 个的并发 Writer 。当多处理器系统的大负荷访问需要更好的并发性时,锁的数量还可以增加。
       看上去,单是这样就已经能大大提高多线程并发的性能了。还没完,继续看我们关注的get,put,remove这三个函数怎么保证数据同步的。

  • get

        它没有使用同步控制,segmentFor这个函数用于确定操作应该在哪一个segment中进行。

        这个函数用了位操作来确定Segment,根据传入的hash值向右无符号右移segmentShift位,然后和segmentMask进行与操作,结合我们之前说的segmentShift和segmentMask的值,就可以得出以下结论:假设Segment的数量是2的n次方,根据元素的hash值的高n位就可以确定元素到底在哪一个Segment中。
        在确定了需要在哪一个segment中进行操作以后,接下来的事情就是调用对应的Segment的get方法:

        它也没有使用锁来同步,只是判断获取的entry的value是否为null,为null时才使用加锁的方式再次去获取。
        这个实现很微妙,没有锁同步的话,靠什么保证同步呢?我们一步步分析。
        第一步,先判断一下 count != 0;count变量表示segment中存在entry的个数。如果为0就不用找了。
        假设这个时候恰好另一个线程put或者remove了这个segment中的一个entry,会不会导致两个线程看到的count值不一致呢?
        看一下count变量的定义: transient volatile int count;
        它使用了volatile来修改。我们前文说过,Java5之后,JMM实现了对volatile的保证:对volatile域的写入操作happens-before于每一个后续对同一个域的读写操作。所以,每次判断count变量的时候,即使恰好其他线程改变了segment也会体现出来。
        第二步,获取到要该key所在segment中的索引地址,调用了getFirst()来取得链表的头部:同样,这里也是用位操作来确定链表的头部,hash值和HashTable的长度减一做与操作,最后的结果就是hash值的低n位,其中n是HashTable的长度以2为底的结果。
        如果该地址有相同的hash对象,顺着链表一直比较下去找到该entry。当找到entry的时候,先做了一次比较: if(v != null) 我们用红色注释的地方。
这是为何呢?
        考虑一下,如果这个时候,另一个线程恰好新增/删除了entry,或者改变了entry的value,会如何?
        Segment中的元素是以HashEntry的形式存放在链表数组中的,先看一下HashEntry类结构。

         除了 value和next,其它成员都是final修饰的,也就是说value和next可以被改变,其它都不可以改变。
         1) 在get代码的①和②之间,另一个线程新增了一个entry。
         一个Entry对象是通过 new HashEntry(K k , V v, HashEntry next) 来创建的。如果另一个线程刚好new 这个对象时,当前线程来get它。因为没有同步,就可能会出现当前线程得到的newEntry对象是一个没有完全构造好的对象引用。
        回想一下我们之前讨论的双重检测的问题,这里也一样,没有锁同步的话,new 一个对象对于多线程看到这个对象的状态是没有保障的,这里同样有可能一个线程new这个对象的时候还没有执行完构造函数就被另一个线程得到这个对象引用。所以才需要判断一下:if (v != null) 如果确实是一个不完整的对象,则使用锁的方式再次get一次。
       有没有可能会put进一个value为null的entry? 不会的,已经做了检查,这种情况会抛出异常,所以 ②处的判断完全是出于对多线程下访问一个new出来的对象的状态检测。
       2) 在get代码的①和②之间,另一个线程修改了一个entry的value
       value是用volitale修饰的,可以保证读取时获取到的是修改后的值。
       3) 在get代码的①之后,另一个线程删除了一个entry
       假设我们的链表元素是:e1-> e2 -> e3 -> e4 我们要删除 e3这个entry。
如果我们get的也恰巧是e3, 这里没有办法实时保证了。
       我们第①处就判断了count变量,它保障了在 ①处能看到其他线程修改后的。
       ①之后到②之间,如果再次发生了其他线程再删除了entry节点,就没法保证看到最新的了。
       不过这也没什么关系,即使我们返回e3的时候,它被其他线程删除了,暴漏出去的e3也不会对我们新的链表造成影响。
       这其实是一种乐观设计,设计者假设 ①之后到②之间 发生被其它线程增、删、改的操作可能性很小,所以不采用同步设计,而是采用了事后(其它线程这期间也来操作,并且可能发生非安全事件)弥补的方式。
而因为其他线程的“改”和“删”对我们的数据都不会造成影响,所以只有对“新增”操作进行了安全检查,就是②处的非null检查,如果确认不安全事件发生,则采用加锁的方式再次get。
       这样做减少了使用互斥锁对并发性能的影响。可能有人怀疑remove操作中复制链表的方式是否代价太大,这里我没有深入比较,不过既然Java5中这么实现,我想new一个对象的代价应该已经没有早期认为的那么严重。
       我们基本分析完了get操作。对于put和remove操作,是使用锁同步来进行的,不过是用的ReentrantLock而不是synchronized,性能上要更高一些。
       ConcurrentHashMap的迭代器不是Fast-Fail的方式,所以在迭代的过程中别其他线程添加/删除了元素,不会抛出异常,也不能体现出元素的改动。但也没有关系,因为每个entry的成员除了value都是final修饰的,暴漏出去也不会对其他元素造成影响。
       最后,既然是线程安全,那么ConcurrentHashMap可以代替HashTable吗?不可以!
       将“一致性强度”和“扩展性”之间的对比交给用户来权衡,所以大多数集合都提供了synchronized和concurrent两个版本。如果你的环境要求“强一致性”的话,就不能用ConcurrentHashMap了,它的get,clear方法和迭代器都是“弱一致性”的。不过真正需要“强一致性”的场景可能非常少,我们大多应用中ConcurrentHashMap是满足的。
       所谓“弱一致性”:如果线程b执行了迭代遍历到first,而此时线程a还没有remove掉first,那么即使后续删除了first,迭代器里不会反应出来,也不抛出异常,这种迭代器被称为“弱一致性”(weakly consistent)迭代器。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值