本文将会详细总结 ThreadLocal 容易用错的三个坑:

  • 内存泄露

  • 线程池中线程上下文丢失

  • 并行流中线程上下文丢失

内存泄露

由于 ThreadLocal 的 key 是弱引用,因此如果使用后不调用 remove 清理的话会导致对应的 value 内存泄露。

@Test
public void testThreadLocalMemoryLeaks() {ThreadLocal<List<Integer>> localCache = new ThreadLocal<>();List<Integer> cacheInstance = new ArrayList<>(10000);localCache.set(cacheInstance);localCache = new ThreadLocal<>();
}

当 localCache 的值被重置之后 cacheInstance 被 ThreadLocalMap 中的 value 引用,无法被 GC,但是其 key 对 ThreadLocal 实例的引用是一个弱引用。

本来 ThreadLocal 的实例被 localCache 和 ThreadLocalMap 的 key 同时引用,但是当 localCache 的引用被重置之后,则 ThreadLocal 的实例只有 ThreadLocalMap 的 key 这样一个弱引用了,此时这个实例在 GC 的时候能够被清理。

其实看过 ThreadLocal 源码的同学会知道,ThreadLocal 本身对于 key 为 null 的 Entity 有自清理的过程,但是这个过程是依赖于后续对 ThreadLocal 的继续使用。

假如上面的这段代码是处于一个秒杀场景下,会有一个瞬间的流量峰值,这个流量峰值也会将集群的内存打到高位(或者运气不好的话直接将集群内存打满导致故障)。

后面由于峰值流量已过,对 ThreadLocal 的调用也下降,会使得 ThreadLocal 的自清理能力下降,造成内存泄露。

ThreadLocal 的自清理是锦上添花,千万不要指望他雪中送碳。

相比于 ThreadLocal 中存储的 value 对象泄露,ThreadLocal 用在 web 容器中时更需要注意其引起的 ClassLoader 泄露。

Tomcat 官网对在 web 容器中使用 ThreadLocal 引起的内存泄露做了一个总结,详见:

https://cwiki.apache.org/confluence/display/tomcat/MemoryLeakProtection

这里我们列举其中的一个例子,熟悉 Tomcat 的同学知道,Tomcat 中的 web 应用由 Webapp Classloader 这个类加载器的。

并且 Webapp Classloader 是破坏双亲委派机制实现的,即所有的 web 应用先由 Webapp classloader 加载,这样的好处就是可以让同一个容器中的 web 应用以及依赖隔离。

下面我们看具体的内存泄露的例子:

public class MyCounter {private int count = 0;public void increment() {count++;}public int getCount() {return count;}
}public class MyThreadLocal extends ThreadLocal<MyCounter> {
}public class LeakingServlet extends HttpServlet {private static MyThreadLocal myThreadLocal = new MyThreadLocal();protected void doGet(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException {MyCounter counter = myThreadLocal.get();if (counter == null) {counter = new MyCounter();myThreadLocal.set(counter);}response.getWriter().println("The current thread served this servlet " + counter.getCount()+ " times");counter.increment();}
}

需要注意这个例子中的两个非常关键的点:

  • MyCounter以及MyThreadLocal必须放到web应用的路径中,保被 Webapp Classloader 加载。

  • ThreadLocal 类一定得是 ThreadLocal 的继承类,比如例子中的 MyThreadLocal,因为 ThreadLocal 本来被 Common Classloader 加载,其生命周期与 Tomcat 容器一致。ThreadLocal 的继承类包括比较常见的 NamedThreadLocal,注意不要踩坑。

假如 LeakingServlet 所在的 Web 应用启动,MyThreadLocal 类也会被 Webapp Classloader 加载。

如果此时 web 应用下线,而线程的生命周期未结束(比如为LeakingServlet 提供服务的线程是一个线程池中的线程)。

那会导致 myThreadLocal 的实例仍然被这个线程引用,而不能被 GC,期初看来这个带来的问题也不大,因为 myThreadLocal 所引用的对象占用的内存空间不太多。

问题在于 myThreadLocal 间接持有加载 web 应用的 webapp classloader 的引用(通过 myThreadLocal.getClass().getClassLoader() 可以引用到)。

而加载 web 应用的 webapp classloader 有持有它加载的所有类的引用,这就引起了 Classloader 泄露,它泄露的内存就非常可观了。

线程池中线程上下文丢失

ThreadLocal 不能在父子线程中传递,因此最常见的做法是把父线程中的 ThreadLocal 值拷贝到子线程中。

因此大家会经常看到类似下面的这段代码:

for(value in valueList){Future<?> taskResult = threadPool.submit(new BizTask(ContextHolder.get()));//提交任务,并设置拷贝Context到子线程results.add(taskResult);
}
for(result in results){result.get();//阻塞等待任务执行完成
}

提交的任务定义长这样:

class BizTask<T> implements Callable<T>  {private String session = null;public BizTask(String session) {this.session = session;}@Overridepublic T call(){try {ContextHolder.set(this.session);// 执行业务逻辑} catch(Exception e){//log error} finally {ContextHolder.remove(); // 清理 ThreadLocal 的上下文,避免线程复用时context互串}return null;}
}

对应的线程上下文管理类为:

class ContextHolder {private static ThreadLocal<String> localThreadCache = new ThreadLocal<>();public static void set(String cacheValue) {localThreadCache.set(cacheValue);}public static String get() {return localThreadCache.get();}public static void remove() {localThreadCache.remove();}}

这么写倒也没有问题,我们再看看线程池的设置:

ThreadPoolExecutor executorPool = new ThreadPoolExecutor(20, 40, 30, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(40), new XXXThreadFactory(), ThreadPoolExecutor.CallerRunsPolicy);

其中最后一个参数控制着当线程池满时,该如何处理提交的任务,内置有 4 种策略:

ThreadPoolExecutor.AbortPolicy //直接抛出异常
ThreadPoolExecutor.DiscardPolicy //丢弃当前任务
ThreadPoolExecutor.DiscardOldestPolicy //丢弃工作队列头部的任务
ThreadPoolExecutor.CallerRunsPolicy //转串行执行

可以看到,我们初始化线程池的时候指定如果线程池满,则新提交的任务转为串行执行。

那我们之前的写法就会有问题了,串行执行的时候调用 ContextHolder.remove(); 会将主线程的上下文也清理,即使后面线程池继续并行工作,传给子线程的上下文也已经是 null 了,而且这样的问题很难在预发测试的时候发现。

并行流中线程上下文丢失

如果 ThreadLocal 碰到并行流,也会有很多有意思的事情发生。

比如有下面的代码:

class ParallelProcessor<T> {public void process(List<T> dataList) {// 先校验参数,篇幅限制先省略不写dataList.parallelStream().forEach(entry -> {doIt();});}private void doIt() {String session = ContextHolder.get();// do something}
}

这段代码很容易在线下测试的过程中发现不能按照预期工作,因为并行流底层的实现也是一个 ForkJoin 线程池,既然是线程池,那 ContextHolder.get() 可能取出来的就是一个 null。

我们顺着这个思路把代码再改一下:

class ParallelProcessor<T> {private String session;public ParallelProcessor(String session) {this.session = session;}public void process(List<T> dataList) {// 先校验参数,篇幅限制先省略不写dataList.parallelStream().forEach(entry -> {try {ContextHolder.set(session);// 业务处理doIt();} catch (Exception e) {// log it} finally {ContextHolder.remove();}});}private void doIt() {String session = ContextHolder.get();// do something}
}

修改完后的这段代码可以工作吗?如果运气好,你会发现这样改又有问题,运气不好,这段代码在线下运行良好,这段代码就顺利上线了。不久你就会发现系统中会有一些其他很诡异的 bug。

原因在于并行流的设计比较特殊,父线程也有可能参与到并行流线程池的调度,那如果上面的 process 方法被父线程执行,那么父线程的上下文会被清理。导致后续拷贝到子线程的上下文都为 null,同样产生丢失上下文的问题。

ThreadLocal巨坑!内存泄露只是小儿科相关推荐

  1. c++ socket线程池原理_一篇文章看懂 ThreadLocal 原理,内存泄露,缺点以及线程池复用的值传递问题...

    编辑:业余草来源:https://www.xttblog.com/?p=4946 一篇文章看懂 ThreadLocal 原理,内存泄露,缺点以及线程池复用的值传递问题. ThreadLocal 相信不 ...

  2. c++ thread 内存泄漏_使用 ThreadLocal如何避免内存泄露?

    作者:鲁毅 juejin.im/post/5e0d8765f265da5d332cde44 1.ThreadLocal的使用场景 1.1 场景1 每个线程需要一个独享对象(通常是工具类,典型需要使用的 ...

  3. java thread 内存泄露_记一次ThreadLocal引发的内存泄露

    概念 ​首先解释下内存溢出和内存泄露的概念.内存溢出一般指的是out of memory,也就是我们经常说的OOM,常发生在堆,方法区和方法栈.内存泄露指的是一段程序在申请内存空间后,无法释放已经申请 ...

  4. ThreadLocal中的3个大坑,内存泄露都是小儿科!

    我在参加Code Review的时候不止一次听到有同学说:我写的这个上下文工具没问题,在线上跑了好久了.其实这种想法是有问题的,ThreadLocal写错难,但是用错就很容易,本文将会详细总结Thre ...

  5. java thread 内存泄露_Java ThreadLocal 内存泄露问题分析及解决方法。

    前言 在分析ThreadLocal导致的内存泄露前,需要普及了解一下内存泄露.强引用与弱引用以及GC回收机制,这样才能更好的分析为什么ThreadLocal会导致内存泄露呢?更重要的是知道该如何避免这 ...

  6. 使用ThreadLocal不当可能会导致内存泄露

    使用ThreadLocal不当可能会导致内存泄露 基础篇已经讲解了ThreadLocal的原理,本节着重来讲解下使用ThreadLocal会导致内存泄露的原因,并讲解使用ThreadLocal导致内存 ...

  7. 分析Threadlocal内部实现原理,并解决Threadlocal的ThreadLocalMap的hash冲突与内存泄露

    前言 ThreadLocal 的经典使用场景是数据库连接. session 管理.多线程等-- 比如在Spring中,发挥着巨大的作用,在管理Request作用域中的Bean.事务管理.任务调度.AO ...

  8. Java中七个潜在的内存泄露风险,你知道几个?

    虽然Java程序员不用像C/C++程序员那样时刻关注内存的使用情况,JVM会帮我们处理好这些,但并不是说有了GC就可以高枕无忧,内存泄露相关的问题一般在测试的时候很难发现,一旦上线流量起来可能马上就是 ...

  9. Java 中 7 个潜在的内存泄露风险

    虽然Java程序员不用像C/C++程序员那样时刻关注内存的使用情况,JVM会帮我们处理好这些,但并不是说有了GC就可以高枕无忧,内存泄露相关的问题一般在测试的时候很难发现,一旦上线流量起来可能马上就是 ...

最新文章

  1. 语文教学中如何运用计算机辅助教学,计算机辅助教学在语文教学过程中的运用...
  2. 题目1209:最小邮票数
  3. 搭建本地LNMP开发环境(1)-VMware内安装debian
  4. win10直接获得文件绝对路径的方法总结
  5. HBuilderX 连接电脑的模拟器问题
  6. HDU1824 2-sat
  7. 【Python基础】当变量有值时,为什么会出现UnboundLocalError?
  8. linux调试crontab,linux - crontab 的调试,启动thin服务器
  9. java taken_java-是否有正确的方法在slf4j中传递参数?
  10. c++起始(名词修饰,extern “C” ,引用)
  11. Spring Bean装配
  12. hibernate二级缓存(一)一级缓存与二级缓存
  13. 浙工商电信闪讯老毛子路由器设置
  14. mysql distinct count_MySQL中distinct和count(*)的使用方法比较
  15. 程序媛必备之日常BGM
  16. Kubernetes 报错小结
  17. vue3 注册全局方法 定义全局方法
  18. Android-传感器开发-方向判断
  19. mysql pga_PGA的监控与调整
  20. 简单的语音合成与语音识别(科大讯飞)

热门文章

  1. 已经有了synchronized为什么需要volatile
  2. 常见面试题整理(2022-11)
  3. mysql 字段命名is__数据库表字段命名规则
  4. 重写与重载的区别和用途
  5. PCA降维工作原理及代码案例实现
  6. 计算机网络实验报告西南科技大学,西南科技大学计算机网络 实验一.doc
  7. Ubuntu 美化系统为Mac
  8. 什么是redo log和undo log
  9. apache httpd 服务器申请免费CA证书
  10. 出海困局 | 国内增长出现瓶颈,大厂的出海“野心”也藏不住了!