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

  1. 内存泄露

  2. 线程池中线程上下文丢失

  3. 并行流中线程上下文丢失

内存泄露

由于ThreadLocalkey是弱引用,因此如果使用后不调用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的值被重置之后cacheInstanceThreadLocalMap中的value引用,无法被GC,但是其keyThreadLocal实例的引用是一个弱引用,本来ThreadLocal的实例被localCacheThreadLocalMapkey同时引用,但是当localCache的引用被重置之后,则ThreadLocal的实例只有ThreadLocalMapkey这样一个弱引用了,此时这个实例在GC的时候能够被清理。

其实看过ThreadLocal源码的同学会知道,ThreadLocal本身对于keynullEntity有自清理的过程,但是这个过程是依赖于后续对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,同样产生丢失上下文的问题。


往期推荐

额!Java中用户线程和守护线程区别这么大?

线程的故事:我的3位母亲成就了优秀的我!

Semaphore自白:限流器用我就对了!

CyclicBarrier:人齐了,老司机就发车了!

ThreadLocal中的3个大坑,内存泄露都是小儿科!相关推荐

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

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

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

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

  3. JavaScript 中的垃圾回收和内存泄露如何处理?| 技术头条

    作者 | 浪里行舟 责编 | 郭芮 程序的运行需要内存.只要程序提出要求,操作系统或者运行时就必须供给内存.所谓的内存泄漏简单来说是不再用到的内存,没有及时释放.为了更好避免内存泄漏,我们先介绍Jav ...

  4. 【转】JavaScript 中的垃圾回收和内存泄露如何处理

    转自 https://blog.csdn.net/csdnnews/article/details/89596750?ops_request_misc=%257B%2522request%255Fid ...

  5. 一文搞懂ThreadLocal及相关的内存泄露问题

    首先,看一张整体的结构图,来帮助理解 什么是ThreadLocal ThreadLocal用于创建线程局部变量,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副 ...

  6. inputstreamreader未关闭会导致oom_ThreadLocal 一定会导致内存泄露?

    在面试的时候,ThreadLocal作为高并发常用工具经常会被问到.而面试官比较喜欢问的问题有以下两个: 1.ThreadLocal是怎么实现来保证每个线程的变量副本的. 2.ThreadLocal的 ...

  7. Java内存泄露系列--内存泄露的原因及解决方案(大全)

    原文网址:Java内存泄露系列--内存泄露的原因及解决方案(大全)_IT利刃出鞘的博客-CSDN博客 简介 简介 本文介绍Java中内存泄露的一些原因与解决方案. 如果内存泄露的空间足够大,就会导致内 ...

  8. 什么是内存溢出(Out Of Memory---OOM)和内存泄露 (Memory Leak)

    1.内存溢出:(Out Of Memory---OOM) 系统已经不能再分配出你所需要的空间,比如系统现在只有1G的空间,但是你偏偏要2个G空间,这就叫内存溢出 例子:一个盘子用尽各种方法只能装4个果 ...

  9. JAVA 内存泄露详解(原因、例子及解决)

    Java的一个重要特性就是通过垃圾收集器(GC)自动管理内存的回收,而不需要程序员自己来释放内存.理论上Java中所有不会再被利用的对象所占用的内存,都可以被GC回收,但是Java也存在内存泄露,但它 ...

最新文章

  1. 十二 手游开发神器 cocos2d-x editor 之游戏暂停悬浮层
  2. 2015/09/09夜晚js继续学习
  3. 在重复3次的数组中查找
  4. 图解 Java 常用数据结构
  5. get请求可以传body吗_都9102年了,GET和POST的区别掌握了没有?
  6. matlab求心率,心电图QRS波检测(计算心跳次数)
  7. 14寸笔记本电脑_纯小白预算5000到6000有什么好的笔记本电脑推荐吗?
  8. python进入内置函数文件_python基础知识-set、函数、内置函数、文件操作
  9. react16-reactDom.render流程分析
  10. 钉钉、微信产品大PK,基因已经决定了结果
  11. 路由器如何设置无线桥接
  12. 该怎么把光纤接入家里预埋的网线中?
  13. IP地址-子网划分详解
  14. 关于未知的USB设备(设备描述符请求失败)的解决方法
  15. Habor数据迁移方式有多少,skopeo效率最好
  16. 虚拟机通过无线设置静态ip连外网,不同环境的wifi照常使用
  17. RSD 教程 —— §2.3  窗口布局
  18. 三星 android 4.4.4,三星Galaxy设备什么时候升级Android 4.4.4
  19. MFC的Dlg和App什么区别?应用程序类与对话框类
  20. 手机群控系统(补充篇)

热门文章

  1. JDK源码解析之java.util.ListIterator
  2. C#使用ListView更新数据出现闪烁解决办法
  3. 工作所用的模块回滚脚本
  4. Can’t Activate Reporting Services Service in SharePoint
  5. jfinal poi
  6. ActiveReports 9实战教程(1): 手把手搭建环境Visual Studio 2013 社区版
  7. “四不像”病毒冒充多款知名软件 窃取电脑隐私
  8. [转]C++中sizeof(struct)怎么计算?
  9. 配置文件管理服务器,06-配置文件管理
  10. linux系统高级管理工具包,linux两大系统的包管理工具