首页 > 系统相关 >Netty源码学习8——从ThreadLocal到FastThreadLocal(如何让FastThreadLocal内存泄漏doge)

Netty源码学习8——从ThreadLocal到FastThreadLocal(如何让FastThreadLocal内存泄漏doge)

时间:2023-12-10 17:22:08浏览次数:41  
标签:Netty FastThreadLocal ThreadLocalMap ThreadLocal 源码 线程 使用 Entry

系列文章目录和关于我

一丶引入

在前面的netty源码学习中经常看到FastThreadLocal的身影,这一篇我们将从ThreadLocal说起,来学习FastThreadLocal的设计(《ThreadLocal源码学习笔记》

二丶从ThreadLocal说起

ThreadLocal是JDK中实现线程隔离的一个工具类。实现线程隔离maybe你第一反应会做出Map<Thread,V>的设计,但是Map在高并发的情况下需要使用锁or cas 来实现线程安全(如ConcurrentHashMap)锁or cas都将带来额外的开销。

那么ThreadLocal是如何实现的昵:

1.ThreadLocal基本结构

其基本结构如下:

image-20231210114501908
  • 每一个Thread对象都有一个名为threadLocals类型为ThreadLocal.ThreadLocalMap的属性。
  • ThreadLocal.ThreadLocalMap对象内部存在一个Entry数组,其中存储的Entry对象key是ThreadLocal,value便是我们绑定在线程上的值。
  • ThreadLocal可以做到线程隔离是由于每一个线程对象持有一个ThreadLocalMap,每一个线程对ThreadLocalMap的处理是互不影响的。

2.ThreadLocal的优秀设计

2.1 线程内部属性实现线程隔离,避免锁竞争

如果使用Map<Thread,V>,不可避免的要处理线程安全问题,但是ThreadLocal巧妙的在Thread内部使用ThreadLocalMap来避免此问题

2.2 对开发者屏蔽细节

如果你不深入看ThreadLocal的源码,maybe你会认为是ThreadLocal里面存储了数据。你只需要使用ThreadLocal#get,set,remove即可,你完全不需要关注其底层细节。

对开发者来说好像ThreadLocal就是存储货物的仓库,其实ThreadLocal只是打开仓库的钥匙(使用ThreadLocal去ThreadLocalMap获取value)

2.3 巧妙的利用弱引用避免内存泄漏

上面我们了解到ThreadLocal是ThreadLocalMap中的key,思考一下,如果使用ThreadLocal#set但是没调用ThreadLocal#remove,是不是意味着ThreadLocalMap中一直会存储这个ThreadLocal和对应的Value昵?

答案是No,ThreadLocal巧妙的使用了弱引用来解决这个问题

image-20231210120228319

ThreadLocalMap中存储的Entry继承了WeakRefrence,根据上面的源码可以看出Entry对ThreadLocal是弱引用

  • 因此:在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用指向的对象,不管当前内存空间足够与否,都会回收它的内存。
  • 也就是说,如果ThreadLocal失去强引用(比如方法中局部变量,方法结束了也就失去了强引用),只存在Entry的弱引用,在发生GC的时候将回收ThreadLocal==>从而带导致Entry的key为null

结合下图看一下

image-20220912172845742

细心的朋友这时候会指出:“key被回收了,value还存在哦,一样可能存在内存泄漏哦”

是的,但是ThreadLocal还留了一手:即在下次调用其他ThreadLocal#get,set的时候,会帮助我们清理

清理什么?清理entry数组中key为null的entry对象

为什么可以清理,因为此Entry中的ThreadLocal失去了强引用,不会再被使用到了

妙!

2.4 使用线性探测法,而不是拉链法

image-20231210121519691

上面我们说到每一个Thread中有一个ThreadLocalMap,其内部使用Entry数组保存多个ThreadLocal和ThreadLocal#set传入的value

ThreadLocal#get就是从Entry数组中拿出Entry从而获取value

那么怎么根据ThreadLocal从table中快速定位到Entry昵?hash又是hash,使用hash和数组长度取模即可!

image-20231210121848108

Hash虽好,但是不要忘记Hash冲突哦!ThreadLocal解决hash冲突使用了线性探测法,而不是拉链法。

下图是拉链法

image-20231210122033193

下图是线性探测法:如果找不到可以存放的位置,那么继续探测下去,直至扩容

image-20231210122140220

那为什么说ThreadLocal使用线性探测法妙昵?

  • 空间效率:ThreadLocal使用数组存储数据,意味着数据在内存上是连续的,可以更好的利用CPU缓存减少寻址开销。如果使用拉链法将Entry来需要额外的保存下一个元素的引用指针,带来额外的开销
  • 时间效率:通常ThreadLocal不会存储太多元素,线性探测法在处理冲突时更快——因为数组存储在内存上更加连续,可以更好的利用内存预读能力,避免了链表内存引用导致了缓存未命中。

其中时间效率这一点是建立在ThreadLocalMap中不会存储太多元素导致hash冲突严重的情况下,如果元素太多ThreadLocalMap也会进行扩容

image-20231210123235866

如上:当前元素大于负载的3/4那么进行扩容

三丶FastThreadLocal 源码浅析

上面说了ThreadLocal的原理和其优秀设计,那么为什么还需要FastThreadLocal昵?

如同FastThreadLocal的名字一样,它在高并发的情况下拥有更高的性能!

1.FastThreadLocal最佳实践

我们结合Netty源码看看netty是如何使用FastThreadLocal的

  • 使用FastThreadLocalThread

    image-20231210152637360

    netty在创建EventLoopGroup中的线程的时候,默认使用DefaultThreadFactory,它会创建出FastThreadLocalThread

    image-20231210152850192

    至于为什么要是有FastThreadLocalThread,我们后面再分析

  • 将Runnable包装为FastThreadLocalRunnable

    image-20231210160002921

    Netty会使用FastThreadLocalRunnable对原Runnable进行包装,确保Runnable指向完后进行FastThreadLocal#removeAll释放

    image-20231210153231219

    这一点再工作也经常使用,比如在分布式链路追踪使用多线程处理业务逻辑,也需要将traceId对应的ThreadLocal进行传递和释放,也是类似的手法。

  • 使用

    image-20231210153647335

    使用上和ThreadLocal类似

2.FastThreadLocalThread

image-20231210160104490

可以看到FastThreadLocalThread是继承了Thread,其中内部有一个InternalThreadLocalMap类型的属性,这便是FastThreadLocal实现的奥秘。

3.InternalThreadLocalMap

InternalThreadLocalMap 中有两个关键的属性

image-20231210160519161

  • ThreadLocal<InternalThreadLocalMap> slowThreadLocalMap,如果使用了FastThreadLocal,但是当前线程不是FastThread,那么会从这个ThreadLocal中获取InternalThreadLocalMap

  • indexedVariables,除0之外的位置存储线程隔离数据,0位置存储所有的FastThreadLocal对象

    image-20231210160719300

3.FastThreadLocal源码解析

3.1 get

image-20231210161621516

可以看到get就是获取当前线程的InternalThreadLocalMap,然后根据index获取内容(如果是缺省值,那么会调用initialize方法进行初始化)

每一个FastThreadLocal对应一个唯一的index,在FastThreadLocal构造的时候调用InternalThreadLocalMap#nextVariableIndex产生(使用AtomicInteger自旋+cas产生)

image-20231210161736020

image-20231210161825990

如下是InternalThreadLocalMap#get方法源码,可以看到根据当前线程是否是FastThreadLocalThread有不同的动作

image-20231210161858213

如果是FastThreadLocalThread那么直接获取属性即可

image-20231210162017477

如果非FastThreadLocalThread那么从ThreadLocal<InternalThreadLocalMap> slowThreadLocalMap中获取

image-20231210162111950

3.2 initialize

如果FastThreadLocal中没用值,那么会调用initialValue进行初始化,initialValue是netty留给子类的扩展的方法

image-20231210162243701

初始化之后会设置到InternalThreadLocalMap中,并调用addToVariablesToRemove将当前FastThreadLocal加入到variablesToRemove中,variablesToRemove位于InternalThreadLocalMap数组的0位置,即如下红色框内容

image-20231210162513046

image-20231210162349394

3.3 set

image-20231210162815328

可以看到如果存入的值不是缺省值,那么调用setKnownNotUnset进行设置

反之调用remove进行删除

3.3.1 setKnownNotUnset

image-20231210162927420

setIndexedVariable 就是向InternalThreadLocalMap中设置内容,

  • 在当前index小于数组长度的时候会直接进行设置

    如果旧值是UNSET缺省值那么说明之前没用设置过,进而调用addToVariablesToRemove将当前FastThreadLocal设置到InternalThrealLocal数组下标为1的Set中

image-20231210162951518

  • 如果当前index大于等于数组长度,相当于出现了hash冲突,这时候不会进行拉链,也不会进行线性探测,而是扩容,扩容逻辑如下

    image-20231210163429378

    首先是扩容到最接近当前index且大于index的2次幂大小(和hashMap一个道理)然后进行Arrays#copy实现数组拷贝,并存储当前值

    这里可以看出FastThreadLocal快在哪里,设置值的时候使用扩容来解决hash冲突,虽然导致了一些空间的浪费,但是这也使得get的时候可以根据index直接获取数据,避免了线性探测的寻址,从而有更高的性能!

3.4 remove

remove分为两步,一是从InternalThreadLocalMap中移除index对应的元素,然后从InternalThreadLocal下标为0的Set中删除

image-20231210164400786

3.5 removeAll

FastThreadLocalRunnable在run方法指向完后自动指向此方法,即删除当前线程所有的FastThreadLocal内容,避免内存泄漏

image-20231210164639220

四丶总结与思考

1.FastThreadLocal快在哪里

空间换时间,ThreadLocal慢在线性探测,那么直接通过更大数组空间的开辟,避免线性探测,这是一种空间换时间的思想

2. FastThreadLocal为什么不使用弱引用

追求极致的性能,使用弱引用带来如下缺点

  • GC开销:弱引用需要GC垃圾收集器额外的工作来确定何时回收对象,netty这种对性能敏感的网络框架,频繁的gc带来不可预测的延迟

  • 访问速度:使用弱引用可以让Entry中key被回收,但是value还是存在,因此ThreadLocal会在get,set,等方法中检测key为null的元素进行删除,这也会带来一定的开销

  • 显示控制:上面我们看到,FastThreadLocalThread会将runnable进行包装保证最后进行释放,一定程度上保证

    image-20231210170832452

3.如何让FastThreadLocal内存泄漏 doge

结合FastThreadLocal的原理,我们只要我不显示释放,也不让Runnable保证为FastThreadLocalRunnable,那么就不会被释放

image-20231210171244273

如上这个例子,会持续输出 "泄露啦",但是如果使用ThreadLocal,再下次使用ThreadLocal的get,set方法的时候就会自动进行清理!

标签:Netty,FastThreadLocal,ThreadLocalMap,ThreadLocal,源码,线程,使用,Entry
From: https://www.cnblogs.com/cuzzz/p/17892929.html

相关文章

  • Java基于云端的云HIS服务平台源码
    云HIS是针对中小医疗机构推出的一套基于云端的云HIS服务平台,借助云his,将医院业务流程化,大大提高医院的服务效率和服务质量,为客户提供医院一体化的信息解决方案。云his主要功能:包含门诊收费管理,住院收费管理,门诊医生工作站,住院医生工作站,住院护士工作站,辅助检查科室管理,药房药品管......
  • Netty内置的http报文解码流程
    netty解码netty通过内置处理器HttpRequestDecoder和HttpObjectAggregator对Http请求报文进行解码之后,Netty会将Http请求封装成一个FullHttpRequest实例,然后发送给下一站。Netty内置的与Http请求报文相对应的类大致有如下几个:(1)FullHttpRequest:包含整个Http请求的信息,包含对Htt......
  • springboot023学生宿舍管理系统的设计与开发-计算机毕业设计源码+LW文档
    学生宿舍管理系统的设计与开发摘要:随着信息技术的日益发展深入到社会的各个角落,学生宿舍管理也不例外。为了适应现代社会人们高度强烈的时间观念,学生宿舍管理系统为学校的教学管理带来了极大的方便。我所开发的系统采用JAVA语言和IntelliJ软件作为开发工具,利用HTML、CSS,SpringM......
  • Vue源码学习(十八):实现组件注册(一)Vue.component()和Vue.extend()
    好家伙, 0.完整代码已开源https://github.com/Fattiger4399/analytic-vue.git 1.思路1.1.什么是组件化? Vue组件化是指将复杂的应用程序拆分成多个独立的、可复用的组件,这些组件可以实现特定的功能或局部功能。组件化有助于提高开发效率、方便重复使用、简化调试步骤......
  • 【JavaSE】数据结构-哈希表(HashSet/HashMap底层哈希表详解,源码分析)
    哈希表结构JDK8版本之前:数组+链表JDK8版本及之后:数组+链表+红黑树哈希表HashMapput()方法的添加流程创建HashSet集合时,构造方法中自动创建HashMap集合;HashMap空参构造方法会创建一个默认长度为16,默认加载因子为0.75的数组,数组名为table(tips:实际上,HashSet对象创建后,第......
  • 直播系统源码,常见的混音算法有哪些?
    声音是由于物体的振动对周围的空气产生压力而传播的一种压力波,转成电信号后经过抽样,量化,仍然是连续平滑的波形信号,量化后的波形信号的频率与声音的频率对应,振幅与声音的音量对应,在直播系统源码中,量化的语音信号的叠加等价于空气中声波的叠加,所以当采样率一致时,混音可以实现为将各......
  • 成品直播源码,如何在开发时自定义缓存策略
    缓存在成品直播源码中所占用的空间往往会成为迫使用户卸载应用的最后一根稻草。开发者不能无上限对音视频资源进行缓存,通常的维护手法是通过限制空间大小,比如,用户通常可以接受视频类应用有1G左右的缓存空间,即时通信类应用也许会更大些。因此我们的成品直播源码缓存库也需要提供......
  • 视频直播app源码,在开发时配置 lint 风格检查与修正
    在开发视频直播app源码时引入工具辅助,可以强制性地实现编码书写和提交过程中的lint校验。下面以当前流行的GitHook方案举例供参考。一、开发编辑器及lint工具配置我们在视频直播app源码中配置TSLint插件以校验typeScript;配置styleLint插件以校验CSS/LESS。我们约定......
  • 基于mysql、laravel、vue2框架开发的手术麻醉临床信息系统源码,自主版权
    手术麻醉系统源码技术架构:PHP、js、mysql、laravel、vue2手术麻醉临床信息管理系统是数字化手段应用于手术过程中的重要组成部分,用数字形式获取并存储手术相关信息,既便捷又高效。既然是管理系统,那就是一整套流程,管理患者手术、麻醉的申请、审批、安排以及术后有关各项数据的记录......
  • Spring Security 6.x 系列(10)—— SecurityConfigurer 配置器及其分支实现源码分析(二)
    一、前言在本系列文章:SpringSecurity6.x系列(4)——基于过滤器链的源码分析(一)中着重分析了SpringSecurity在SpringBoot自动配置、 DefaultSecurityFilterChain和FilterChainProxy 的构造过程。SpringSecurity6.x系列(7)——SecurityBuilder继承链源码分析中详细分析了......