细说ThreadLocal&FastThreadLocal

系列文章目录

1. Netty网络应用基础
2. Java I/O
3. IO/模型
4. 网络应用编解码
5. Netty Pipeline
6. Netty EventLoopGroup&EventLoop
7. Netty ThreadLocal&FastThreadLocal
8. Netty Future&Promise
9. Netty内存管理–(旧)PoolChunk&伙伴分配
10. Netty内存管理–内存池空间规格化SizeClasses
11. Netty内存管理–PoolChunk&PoolSubPage
12. Netty内存管理–内存池PoolArena
13. Netty内存管理–内存分配器PooledByteBufAllocator
14. Netty ObjectPool

前言

本篇咱们聊下JDK中的ThreadLocal具体实现和Netty中FastThreadLocal所做的优化。


一、ThreadLocal

使用场景

  1. 作为数据副本,当某些数据是以线程为作用域并且不同线程有不同数副本, 比如session。
  2. 保存上下文信息,在任意需要的地方可以获取,避免显示传参, 比如Spring中的事务上下文信息。
  3. 解决线程安全问题,避免某些情况需要考虑线程安全必须同步带来的性能损失。比如DateFormat对象业务逻辑中到处使用但是这货不是线程安全的。

ThreadLocal示意图
版权声明:本图来自CSDN博主「半夏_2021」的原创文章。
原文链接:https://blog.csdn.net/fd2025/article/details/120019239

实现分析

假设现在已经个Thread对象, 我们要给这个对象关联一个属性, 你会怎么做? 显然直接给加个属性即可。如果是增加多个属性, 则增加一个集合, 后续的操作都是基于集合的操作。JDK的小伙伴也是这么实现的。

  1. 在Thread类中增加一个叫做ThreadLocalMap的对象;
  2. ThreadLocalMap中存储ThreadLocal Key和value的映射关系;
  3. 调用ThreadLocal#set(value)方法时, 会给当前Thread的ThreadLocalMap赋值, 并将value存储到目标entry数组中;
  4. 到这里基本上就是给Thread关联一个Map对象, 然后向这个Map中写入或者读取。
    ThreadLocalMap既然是一个Map, 我们有必要看看其K,V是怎么表示和存储。

Entry的表示

Entry本身继承了WeakReference, 也就意味着Entry本身不会影响GC对ThreadLocal对象的垃圾回收, 更直接说就是被ThreadLocal对象在GC层面做可达性统计时不考虑Entry的引用, 而Entry本身又增加了value字段用于存储对应的value。显然, 对entry的预期是当entry.getKey() == null时, 意味着key已经被回收, 此时应该将entry释放掉。

Entry的存储

在ThreadLocalMap中, 通过数组存储Entry。此外需要解决两个问题。一个是如何处理hash冲突, 二是如何进行扩容。在ThreadLocalMap中hash冲突通过开放地址法解决, 还有一种方案是拉链法。前者在数据量少, hash冲突比较低时使用, 后者则在存储数据比较多时使用(比如HashMap)。扩容则根据负载因子(一般是0.75), 容量达到这个比例就扩容。

Entry写入

  1. 以 获取hashcode & (len - 1) 为搜索开始索引, 线性探测(每次+1), 而hashcode初始值为0x61c88647, 该魔数据可以通过& (len - 1) 得到一个在2的n次方范围内相对均匀分布的数列;
  2. 如果val[i].get() == 当前key, 则直接替换value;
  3. 如果val[i].key == null, 此时理论上该位置是可用的, 但需要执行一波entry清理;
    原因是GC在运行时往往是批量内存回收, 当遇到一个key为null的entry时, 说明GC已经运行了一波, 很有可能还会有别的entry需要清理, 由于清理完对应的entry后还需要进行rehash, 因此一波批处理掉比多次分开处理效率更高。
  4. 清理过程可谓复杂, 首先得从当前位置向前向后搜索, 直到遇到下一个null的位置并这个过程中对entry做删除和rehash
  5. 整理完成后将当前数据写入, 此时按照实际size执行必要的扩容处理。

Entry读取

  1. 以 获取hashcode & (len - 1) 为搜索开始索引, 如果key相等则返回;
  2. 如果key == null, 执行清理;
  3. 否则索引+1重复1,2 直到返回或者索引位置为null;

关于缩容

实际情况下, ThreadLocalMap中的ThreadLocal对象比较少, 并且考虑到正确的使用姿势应该是使用完毕即remove因此空间是复用的, 因此缩容的意义不大。

内存泄漏

ThreadLocal 在 ThreadLocalMap 中是以一个弱引用身份被Entry中的Key引用的,因此如果ThreadLocal没有外部强引用来引用它,那么ThreadLocal会在下次JVM垃圾收集时被回收。这个时候就会出现Entry中Key已经被回收,出现一个null Key的情况,外部读取ThreadLocalMap中的元素是无法通过null Key来找到Value的。因此如果当前线程的生命周期很长,一直存在,那么其内部的ThreadLocalMap对象也一直生存下来,这些null key就存在一条强引用链的关系一直存在:Thread --> ThreadLocalMap–>Entry–>Value,这条强引用链会导致Entry不会回收,Value也不会回收,但Entry中的Key却已经被回收的情况,造成内存泄漏。

JVM团队已经考虑到这样的情况,并做了一些措施来保证ThreadLocal尽量不会内存泄漏:在ThreadLocal的get()、set()、remove()方法调用的时候会清除掉线程ThreadLocalMap中所有Entry中Key为null的Value,并将整个Entry设置为null,利于下次内存由于ThreadLocalMap的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
版权声明:本部分来自CSDN博主「半夏_2021」的原创文章。
原文链接:https://blog.csdn.net/fd2025/article/details/120019239

二、FastThreadLocal

Netty作为一个深度优化的网络框架, 你能看到其线程上下文都围绕EventLoop展开, 也就是围绕单个Thread展开, 因此必定是一个ThreadLocal的使用大户。鉴于网络场景下对ThreadLocal的使用是一个高频动作, 因此对其优化是值得的。首先ThreadLocal本身已经是基于数组存储的,元素访问通过下标完成, 理论上已经是最快的数据结构了。但美中不足就是当hash冲突的时候, 需要继续向后探测, 并且在读取和写入时需要做整理。Netty框架则想招把hash冲突这回事给避免了。

避免hash冲突

  1. hash冲突的根源是hash函数产生的返回值可能会重复, 于是FastThreadLocal直接使用+1作为hashcode, 显然就不会重复了;
  2. 副作用就是如果空间被回收了, 这个索引值却头也不回地增长, 所以有空间浪费。
  3. 站在使用的角度, 应该创建一个FastThreadLocal对象, 然后不断地复用更新其value而不是完全释放创建新的FastThreadLocal对象。所以, 这玩意对使用场景是有要求的。

空间管理

  1. 通过BitSet记录空间占用情况,如果空间未占用则使用UNSET站位;
  2. 如果索引达到了当前空间的长度, 则直接扩容;
  3. 由于索引的增长是全局的, 假设现在nextIndex = 30, 在一个新的InternalThreadLocalMap对象中分配空间, 则需要直接从nextIndex开始, 前面30个位置都会是空闲的。这就是空间浪费的原因。

Thread挂钩

包装为ThreadLocal

把InternalThreadLocalMap对象放在ThreadLocal中, 进而关联到普通的Thread上。操作上与常规ThreadLocal的区别如下:
常规操作: Thread -> ThreadLocalMap -> ThreadLocal -> #get() -> value;
新操作: Thread -> ThreadLocalMap -> ThreadLocal -> #get() -> InternalThreadLocalMap -> #get() -> value;

继承Thread

在Netty中定义为FastThreadLocalThread, 其中代理对ThreadLocalMap的操作到InternalThreadlLocalMap上, 这样才能真正体现出FastThreadLocal的性能优势。如果是普通Thread+FastThreadLocal, 不仅性能没有提升, 反而由于访问链路过长降低性能。


总结

本篇咱们聊下JDK中的ThreadLocal具体实现和Netty中FastThreadLocal所做的优化, 可见Netty对细节的孜孜追求。其中对业务场景的深刻洞察和对所用技术优缺点的精准掌控值得我们学习。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值