Open Clustering HashMap实现原理:ThreadLocalMap中HashMap实现原理的深度剖析

一、前言

在前面一篇文章HashMap原理详解:探测技术(Probing)、数据聚集(Clustering)、寻址方式(Addressing)、墓碑删除(tombstones)等技术的深度剖析中,我们已经详解介绍过了广义HashMap的基本原理,同时重点介绍了Open Clustering HashMap。咱们那么隆重的介绍Open Clustering HashMap,那在实际开发中我们到底有没有使用Open Clustering HashMap的场景呢?

答案当然是有。咱们在多线程中经常使用的ThreadLocal类就是Open Clustering HashMap的一个实现例子。

接下来我们就重点给大家介绍一下比较常见的Open Clustering HashMap的实现实例:ThreadLocalMap。

 

二、ThreadLocal

1、ThreadLocal的数据结构简介

提到ThreadLocal类,我们都知道它是用来在多线程中实现共享资源拷贝的一个常用类。同时大家都知道ThreadLocal存类型泄露的问题。那么为什么他存在内存泄露呢?其实最根本原因就是因为它底层实现了Open Clustering HashMap。下面我们深入分析下ThreadLocal中是如何实现Open Clustering HashMap的。

通过源码我们知道ThreadLocal底层数据结构就是一个Entry数组。该数组的元素就是一个普通的Entry,其实一个WeakReference子类。因此Entry数组的每个元素实际就两个值:一个Key(主要其Key是ThreadLocal本身),一个Value。其中的Value就是我们自己存储的值。

PS :

1、至于这里为什么Enrty是WeakReference的子类,并不在本文的讨论范围内。大家可以去搜索下,网上很多关于这里的WeakReference导致内存泄露的文章。

2、本文重点剖析ThreadLocal是如何实现的Open Clustering HashMap,并不对ThreadLocal的详细结构和实现做介绍。

通过上面对ThreadLocal数据结构的简单分析我们知道,ThreadLocal的HashMap在遇到Hash冲突的并不会像咱们Java中的HashMap一样通过额外(相对数组结构本身而言)的数据空间来解决。那么它是怎么解决Hash冲突的呢?看过咱们之前对Open Clustering HashMap介绍的同学都应该知道。其实这里使用的就是Open Clustering HashMap实现。

2、ThreadLocalMap实现原理剖析

简单说来,ThreadLocal的HashMap的实现原理为:每当有新的ThreadLocal插入ThreadLocal的HashMap中时,首先计算Hash,然后根据Hash值取模直接找到其在数组Entry[]中的位置。如果发现该位置存在已有元素(且该元素不是墓碑元素),则接续向下查找可用的位置。直到找到空位置,然后插入该元素。接下来我们就从源码角度来详细剖析下ThreadLocal是怎么利用Open Clustering HashMap思想实现的。

首先ThreadLocal中的HashMap实体类叫做ThreadLocalMap。启动元素定位方式就也比较简单,直接计算ThreadLocal的HashCode(之前计算好的,不是每次计算);然后和数组的长度取模即可。通过如下源码我们可以看到其直接定位的位置如果没有找到该元素(即Hash冲突了),接着就会寻找找一个位置。

我们知道在Open Clustering HashMap中,当出现Hash冲突的时候,我们会使用探测技术去寻找下一个可用的位置。而常用的探测技术有三种:Linear Probing、Quadratic Probing和Double Hashing。那ThreadLocal中使用的是哪种探测技术呢?通过如下源码我们可以知道它使用了最简单的线性探测技术(Linear Probing)。因此TheadLocal会通过线性探测的方式不断寻找下一个位置,直到找到一个可用的位置。

通过上述的代码,大家应该发一段特殊的逻辑expungeSstableEntry,即删除无效元素。为什么这里的key会等于null,且该值还没有数组中删除呢?原因是因为该key为弱引用,在JVM执行GC的时候就会将该key清楚掉,而其value还存在,整个Entry在数组也依然存在,但是其已经变成无效了,该元素就是我们在Open Clustering HashMap中所所谈到的墓碑元素了。这里的墓碑元素也就是大家经常讨论的ThreadLocal存内存泄露的根本所在。该元素变成墓碑了,但是他的Value仍然持有内存,且暂时不会释放,这就导致了内存泄露。

针对这类墓碑元素,ThreadLocal在访问元素或者删除的元素的过程中,如果遇到了都会调用expungeSstableEntry来删除。注意该方法只删除本次访问(删除、查询等)定位到的元素后面的所有的Clustering数据。接下来我们从源码的角度剖析下该方法如何删除墓碑元素的。

从代码来看比较复杂,我给大家画个图总结下。如下图,当每次访问(查询或者删除)到元素5时,此时发现其为墓碑元素,于是就会从元素5开始,将后面的所有的Clustering数据(从5到9)重新处理(不会处理11到12,因为它们和4-9并不是一个Clustering)。其主要做两件事情:

1、将遇到的新墓碑元素删除,下图会删除元素5和6

2、对于非墓碑元素重新,如果其当前位置不是最开始定位的位置,即发生过Hash冲突导致重新探测寻址了。那么会重新对该元素取出来重新进行一次插入操作。这就是我们上一篇文章提到的backward shift deletion法:即删除无效元素,将后续元素向前移动(重新定位,即重新执行一次插入操作)。下图会对7和8进行重新定位。

讨论了ThreadLocal的墓碑元素的产生,以及其如何通过backward shift deletion法删除后,接下来我们来看看ThreadLocal的HasMap删除元素的逻辑。

3、ThreadLocalMap删除元素

我们仍然从源码层面来分析。通过如下源码可知,其删除也是常规的Open Clustering HashMap的backward shift deletion法。即删除元素的同时,使用expungeStableEntry方法将该元素以后的所有clustering元素向前移动,同时会将由于JVM导致的墓碑元素删除或者重新定位(上面已经讲解过了)。

4、ThreadLocalMap的Open Clustering实现总结

总的来看ThreadLocalMap基于Open Clustering HashMap思想的实现有如下几个关键点:

A、使用Hash & length的方式初次定位元素。

B、访问元素的过程中如果在Clustering中发现了墓碑元素,其可以会将该墓碑后面的所有墓碑元素都删除,即backward shift deletion法。

C、删除元素也是使用的backward shift deletion法。

PS :和常规的Open Clustering HashMap的实现不同的是,ThreadLocalMap的墓碑元素不是因为删除元素导致的,而是因为弱引用由于JVM回收导致的。而常规Open Clustering HashMap的墓碑元素则是通过删除操作导致的。

三、ThreadLocalMap为什么要这样实现

通过前面的分析我们已经知道ThreadLocalMap和Java的HashMap实现存在本质上的区别。那么为什么ThreadLocalMap要这样实现呢?我认为它这样实现主要考虑了如下两个方面:

1、元素少扩容少和Hash冲突少

通过读Open Clustering HashMap原理的学习我们知道,该方式下特别是使用线性探测的方式解决Hash冲突时,当数据量很大的时候,很容易造成数据Clustering,这样严重影响数据的查询效率。且因为数组固定大小,很容易触发Rehash。

但是在ThreadLocalMap的场景中,该Map只是用在单个线程中使用了ThreadLocal的地方。大家可以去翻阅下自己的代码,你可以发现在咱们平时编码的过程中,很少使用ThreadLoal。那么其在单个线程先ThreadLocal的个数也就很少。所以在数据量很少的场景下,Open Clustering HashMap并不会存在上面所提到的Clustering现象以及大量的Rehash。

同时因为其不使用额外的空间解决Hash冲突,它相对普通的HashMap反而更节约内存。

2、充分利用CPU缓存行提升读取效率

利用Open Clustering HashMap原理实现,能够充分的利用CPU缓存,从而提高读写性能。因为所有元素放到一个数组中,那么Clustering聚集的元素就很容易被读取到一个缓存行中,这样就算Hash冲突了,根据线性探测方式,它也可以迅速从缓存行中读取到数组元素后面的数据。

比如如下场景中:4-9是一个Clustering,假设我们需要访问的元素是8,但是该元素直接通过hash & length的方式是定位到4的位置,即Hash冲突了。那么我们子访问的时候过程如下:

A、首选通过hash & length的方位定位到4

B、发现Hash冲突,然后通过线性探测在数组中一个一个向后找

C、依次读取对比5、6、7、8之后,最终找到元素8,完成元素读取。

在这个过程中我们需要访问4、5、6、7、8五个元素。但是由于缓存行的特性,这5个元素极有可能被放到同一个缓存行中。这样其读取这个5个元素时,CPU根本不需要去主存读取,而是直接在CPU当前缓存行就可以读取。因此此时读取5个元素的时间和读取1个元素的时间几乎相同(CPU读取极快)。

利用CPU的缓存行特性,在ThreadLocalMap中的大部分Hash冲突并不会因为线性探测导致性能变差。

关于ThreadLocalMap的Open Clustering HashMap思想实现我们就讲到这里,下一期我们将深入讲解Open Clustering HashMap最有代表性的实现实例:Robin Hood HashMap(罗宾汉哈希)。

四、惯例

如果你对本文有任何疑问或者高见,欢迎添加公众号lifeofcoder共同交流探讨(添加公众号可以获得楼主最新博文推送以及”Java高级架构“上10G视频和图文资料哦)。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值