高并发下的ThreadLocal

通常 ThreadLocal 可以提供线程内的局部变量,确保每个线程都有自己的专属变量值。但在使用线程池的场景下,线程经常会被复用,同一个线程可能会处理多个业务会话场景(比如多个用户 session),这样就可能会造成后续业务逻辑上的混乱和错误。

 

另外,尽管 ThreadLocal 底层利用弱引用和其它机制来回收资源,ThreadLocal 使用不当时仍可能会产生内存泄漏。ThreadLocal 使用本身实例作为key将用户的值保存在 Thread 的 ThreadLocalMap 中,如果一个 ThreadLocal 没有外部强引用来引用它,就将被 JVM 垃圾收集器回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,也就没有办法再访问这些key为null的Entry的value,它将成为一个不能被访问且又不被回收的区域。只有Thread结束后,Map,value才会被完全回收。但如果当前线程迟迟不能结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。虽然在ThreadLocalMap的设计中加上了一些防护措施,比如在 ThreadLocal  的get(),set(),remove()的时候都会清除线程 ThreadLocalMap里所有key为null的value。但是这些预防措施并不能绝对保证不会内存泄漏,比如在下面的场景下:

    • 使用 static 的 ThreadLocal,延长了 ThreadLocal 的生命周期,可能导致的内存泄漏。
    • 分配使用了 ThreadLocal 又不再调用get(), set() 和 remove()方法,同样也会导致内存泄漏。

 

典型案例

 

案例一:线上的一个根据模板(html)渲染pdf的服务在某些场景下报错,查看日志,发现待渲染pdf里的html内容里&Vision并没有包上cdata,原来这个渲染的逻辑是通过一个线程池异步来跑的,然后这个线程池另外一个服务也会用到(设置了ThreadLocal变量),并且用完之后也不会清理当前线程的ThreadLocal变量,这样下次请求进来,ThreadLocal变量已经被“污染”了,就出现了这个诡异的问题。

 

案例二:用户串号问题,用户A购买了某理财产品,但在交易记录里查不到,用户B却意外发现账单里多了一笔理财购买记录。这个服务使用了基于tengine的session_sticky模块,将同一个用户的多个请求持续请求到同一台Server,然后将Session对象缓存(SecurityContext)到Server中的内存中,用以达到较少 Tair 调用的目的。为了保障太多的Session对象不会造成内存泄露,采用一个LRUCache进行缓存处理。由于代码中没有在线程处理结束时及时清理 ThreadLocal ,导致 LRUCacheMap 中不同的 sessionId 对应的同一个SecurityContext对象,引用的OperationContext 却发生了非预期的改变, 最终产生了这个串号的问题。

 

案例三:商家后台服务异常无法报名,是由于商家端系统依赖的cargo服务接口出现大量的超时导致(大量 Full GC 引起 RT 增高)。通过zprofiler,发现有五个线程占用了接近500M的内存。定位是ThreadContext类的operationHandleInfoMap成员有近105W行的记录, 这个成员主要是存放着每次请求的操作详情,例如系统发布操作。推测是在某段逻辑游离在请求线程持有的ThreadContext外,而且ThreadContext不会被回收,随着调用不断发生,不断的聚集。RuleCheckPipeline这个类是专门为“发布”操作定制的类,因为发布这个节点校验的内容非常多,所以为了提高检查效率,把原来串行执行的校验点多线程并发执行。而RuleCheckPipeline使用的executorService线程池定义了最少保留5个线程,线程池中对线程管理都是采用线程复用的方法,所以这5个线程生命周期跟JVM一致,但是代码里面并没有每次都把ThreadContext清理掉,也就意味着他们持有的ThreadContext一直都不会被销毁。而每个发布操作valve检查的过程都往ThreadContext中的operationHandleInfoMap存入新的信息,最终导致了operationHandleInfoMap不断的膨胀,进而导致了内存溢出。

 

最佳实践

  • 必须回收自定义的 ThreadLocal 变量,尽量使用 try-finally 块进行回收。另外需要格外注意线程池的场景。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值