Netty源码学习8——从ThreadLocal到FastThreadLocal(如何让FastThreadLocal内存泄漏doge)
一丶引入
在前面的netty源码学习中经常看到FastThreadLocal的身影,这一篇我们将从ThreadLocal说起,来学习FastThreadLocal的设计(《ThreadLocal源码学习笔记》)
二丶从ThreadLocal说起
ThreadLocal是JDK中实现线程隔离的一个工具类。实现线程隔离maybe你第一反应会做出Map<Thread,V>的设计,但是Map在高并发的情况下需要使用锁or cas 来实现线程安全(如ConcurrentHashMap)锁or cas都将带来额外的开销。
那么ThreadLocal是如何实现的昵:
1.ThreadLocal基本结构
其基本结构如下:

- 每一个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巧妙的使用了弱引用来解决这个问题

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

细心的朋友这时候会指出:“key被回收了,value还存在哦,一样可能存在内存泄漏哦”
是的,但是ThreadLocal还留了一手:即在下次调用其他ThreadLocal#get,set的时候,会帮助我们清理
清理什么?清理entry数组中key为null的entry对象
为什么可以清理,因为此Entry中的ThreadLocal失去了强引用,不会再被使用到了
妙!
2.4 使用线性探测法,而不是拉链法

上面我们说到每一个Thread中有一个ThreadLocalMap,其内部使用Entry数组保存多个ThreadLocal和ThreadLocal#set传入的value
ThreadLocal#get就是从Entry数组中拿出Entry从而获取value
那么怎么根据ThreadLocal从table中快速定位到Entry昵?hash又是hash,使用hash和数组长度取模即可!

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

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

那为什么说ThreadLocal使用线性探测法妙昵?
- 空间效率:ThreadLocal使用数组存储数据,意味着数据在内存上是连续的,可以更好的利用CPU缓存减少寻址开销。如果使用拉链法将Entry来需要额外的保存下一个元素的引用指针,带来额外的开销
- 时间效率:通常ThreadLocal不会存储太多元素,线性探测法在处理冲突时更快——因为数组存储在内存上更加连续,可以更好的利用内存预读能力,避免了链表内存引用导致了缓存未命中。
其中时间效率这一点是建立在ThreadLocalMap中不会存储太多元素导致hash冲突严重的情况下,如果元素太多ThreadLocalMap也会进行扩容

如上:当前元素大于负载的3/4那么进行扩容
三丶FastThreadLocal 源码浅析
上面说了ThreadLocal的原理和其优秀设计,那么为什么还需要FastThreadLocal昵?
如同FastThreadLocal的名字一样,它在高并发的情况下拥有更高的性能!
1.FastThreadLocal最佳实践
我们结合Netty源码看看netty是如何使用FastThreadLocal的
使用FastThreadLocalThread

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

至于为什么要是有FastThreadLocalThread,我们后面再分析
将Runnable包装为FastThreadLocalRunnable

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

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

使用上和ThreadLocal类似
2.FastThreadLocalThread

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

ThreadLocal<InternalThreadLocalMap> slowThreadLocalMap,如果使用了FastThreadLocal,但是当前线程不是FastThread,那么会从这个ThreadLocal中获取InternalThreadLocalMapindexedVariables,除0之外的位置存储线程隔离数据,0位置存储所有的FastThreadLocal对象

3.FastThreadLocal源码解析
3.1 get

可以看到get就是获取当前线程的InternalThreadLocalMap,然后根据index获取内容(如果是缺省值,那么会调用initialize方法进行初始化)
每一个FastThreadLocal对应一个唯一的index,在FastThreadLocal构造的时候调用InternalThreadLocalMap#nextVariableIndex产生(使用AtomicInteger自旋+cas产生)


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

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

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

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

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


3.3 set

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

setIndexedVariable 就是向InternalThreadLocalMap中设置内容,
在当前index小于数组长度的时候会直接进行设置
如果旧值是UNSET缺省值那么说明之前没用设置过,进而调用addToVariablesToRemove将当前FastThreadLocal设置到InternalThrealLocal数组下标为1的Set中

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

首先是扩容到最接近当前index且大于index的2次幂大小(和hashMap一个道理)然后进行Arrays#copy实现数组拷贝,并存储当前值
这里可以看出FastThreadLocal快在哪里,设置值的时候使用扩容来解决hash冲突,虽然导致了一些空间的浪费,但是这也使得get的时候可以根据index直接获取数据,避免了线性探测的寻址,从而有更高的性能!
3.4 remove
remove分为两步,一是从InternalThreadLocalMap中移除index对应的元素,然后从InternalThreadLocal下标为0的Set中删除

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

四丶总结与思考
1.FastThreadLocal快在哪里
空间换时间,ThreadLocal慢在线性探测,那么直接通过更大数组空间的开辟,避免线性探测,这是一种空间换时间的思想
2. FastThreadLocal为什么不使用弱引用
追求极致的性能,使用弱引用带来如下缺点
GC开销:弱引用需要GC垃圾收集器额外的工作来确定何时回收对象,netty这种对性能敏感的网络框架,频繁的gc带来不可预测的延迟
访问速度:使用弱引用可以让Entry中key被回收,但是value还是存在,因此ThreadLocal会在get,set,等方法中检测key为null的元素进行删除,这也会带来一定的开销
显示控制:上面我们看到,FastThreadLocalThread会将runnable进行包装保证最后进行释放,一定程度上保证

3.如何让FastThreadLocal内存泄漏 doge
结合FastThreadLocal的原理,我们只要我不显示释放,也不让Runnable保证为FastThreadLocalRunnable,那么就不会被释放

如上这个例子,会持续输出 "泄露啦",但是如果使用ThreadLocal,再下次使用ThreadLocal的get,set方法的时候就会自动进行清理!
Netty源码学习8——从ThreadLocal到FastThreadLocal(如何让FastThreadLocal内存泄漏doge)的更多相关文章
- 【Netty源码学习】DefaultChannelPipeline(三)
上一篇博客中[Netty源码学习]ChannelPipeline(二)我们介绍了接口ChannelPipeline的提供的方法,接下来我们分析一下其实现类DefaultChannelPipeline具 ...
- 【Netty源码学习】ChannelPipeline(一)
ChannelPipeline类似于一个管道,管道中存放的是一系列对读取数据进行业务操作的ChannelHandler. 1.ChannelPipeline的结构图: 在之前的博客[Netty源码学习 ...
- 【Netty源码学习】ServerBootStrap
上一篇博客[Netty源码学习]BootStrap中我们介绍了客户端使用的启动服务,接下来我们介绍一下服务端使用的启动服务. 总体来说ServerBootStrap有两个主要功能: (1)调用父类Ab ...
- Netty 源码学习——EventLoop
Netty 源码学习--EventLoop 在前面 Netty 源码学习--客户端流程分析中我们已经知道了一个 EventLoop 大概的流程,这一章我们来详细的看一看. NioEventLoopGr ...
- Netty 源码学习——客户端流程分析
Netty 源码学习--客户端流程分析 友情提醒: 需要观看者具备一些 NIO 的知识,否则看起来有的地方可能会不明白. 使用版本依赖 <dependency> <groupId&g ...
- Netty源码学习系列之4-ServerBootstrap的bind方法
前言 今天研究ServerBootstrap的bind方法,该方法可以说是netty的重中之重.核心中的核心.前两节的NioEventLoopGroup和ServerBootstrap的初始化就是为b ...
- JUC源码学习笔记4——原子类,CAS,Volatile内存屏障,缓存伪共享与UnSafe相关方法
JUC源码学习笔记4--原子类,CAS,Volatile内存屏障,缓存伪共享与UnSafe相关方法 volatile的原理和内存屏障参考<Java并发编程的艺术> 原子类源码基于JDK8 ...
- 【Netty源码学习】EventLoopGroup
在上一篇博客[Netty源码解析]入门示例中我们介绍了一个Netty入门的示例代码,接下来的博客我们会分析一下整个demo工程运行过程的运行机制. 无论在Netty应用的客户端还是服务端都首先会初始化 ...
- (一)Netty源码学习笔记之概念解读
尊重原创,转载注明出处,原文地址:http://www.cnblogs.com/cishengchongyan/p/6121065.html 博主最近在做网络相关的项目,因此有契机学习netty,先 ...
- Netty源码学习(七)FastThreadLocal
0. FastThreadLocal简介 如同注释中所说:A special variant of ThreadLocal that yields higher access performance ...
随机推荐
- 7、Spring之基于注解管理bean
本质上:所有一切的操作都是Java代码来完成的,XML和注解只是告诉框架中的Java代码如何执行. 7.1.环境搭建 创建名为spring_ioc_annotation的新module,过程参考3.1 ...
- 【双系统】Win10/Win11 引导 Ubuntu
目录 纲要 注意 写在最前 1. Win 分区 2. Ubuntu刻盘 3. 安装 Ubuntu 4. 配置引导 纲要 本文主要介绍了如何在已安装 Win10/Win11 前提下安装 Ubuntu 双 ...
- 2023-09-01:用go语言编写。给出两个长度均为n的数组, A = { a1, a2, ... ,an }, B = { b1, b2, ... ,bn }。 你需要求出其有多少个区间[L,R]
2023-09-01:用go语言编写.给出两个长度均为n的数组, A = { a1, a2, ... ,an }, B = { b1, b2, ... ,bn }. 你需要求出其有多少个区间[L,R] ...
- [译]这几个CSS小技巧,你知道吗?
前言 在网页设计和前端开发中,CSS属性是非常重要的一部分.掌握常用的CSS属性不仅可以使你的网页看起来更美观,还能提升用户体验,今天小编为大家介绍8个常见的CSS小技巧: 1.修改滚动条样式 下图是 ...
- 触动精灵生成的APK文件如何加固保护
触动精灵是一款模拟手机触摸.按键操作的软件,通过制作脚本,可以让触动精灵代替双手,自动执行一系列触摸.按键操作, 深受一些极客开发者喜爱. 触动精灵生成的APK文件自带了一些基础的加密,可以保护APK ...
- 【解惑】时间规划,Linq的Aggregate函数在计算会议重叠时间中的应用
在繁忙的周五,小悦坐在会议室里,面前摆满了各种文件和会议安排表.她今天的工作任务是为公司安排下周的50个小会议,这让她感到有些头疼.但是,她深吸了一口气,决定耐心地一个一个去处理. 首先,小悦仔细地收 ...
- Oracle字符串函数-Translate()总结
Oracle的Translate(expr,from_string,to_string)是字符串操作函数,实现from_string,to_string字符的一 一替换 1)典型示例: select ...
- Django-rest-framework框架——Xadmin的使用、Book系列多表群操作、RBAC-基于角色的访问控制
@ 目录 一 过滤Filtering 二 排序 三 分页Pagination 可选分页器 应用 四 异常处理 Exceptions 4.1 使用方式 4.2 案例 4.3 REST framework ...
- Python3中的printable
import string characters = string.printable # printable 是用作字符串常量的预初始化字符串.里面包含所有的标点符号,数字 print(charac ...
- 循序渐进介绍基于CommunityToolkit.Mvvm 和HandyControl的WPF应用端开发(7) -- 图标列表展示和选择处理
我们在WPF应用端的界面中,使用lepoco/wpfui 来做主要的入口框架,这个项目它的菜单内置了不少图标,我们需要在动态菜单的配置中,使用它作为图标的展示处理,本篇随笔介绍如何基于图标枚举集合进行 ...