ThreadLocal中的3个大坑,内存泄露都是小儿科!
我在参加Code Review的时候不止一次听到有同学说:我写的这个上下文工具没问题,在线上跑了好久了。其实这种想法是有问题的,ThreadLocal
写错难,但是用错就很容易,本文将会详细总结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
,同样产生丢失上下文的问题。
往期推荐
额!Java中用户线程和守护线程区别这么大?
线程的故事:我的3位母亲成就了优秀的我!
Semaphore自白:限流器用我就对了!
CyclicBarrier:人齐了,老司机就发车了!
ThreadLocal中的3个大坑,内存泄露都是小儿科!相关推荐
- Java中七个潜在的内存泄露风险,你知道几个?
虽然Java程序员不用像C/C++程序员那样时刻关注内存的使用情况,JVM会帮我们处理好这些,但并不是说有了GC就可以高枕无忧,内存泄露相关的问题一般在测试的时候很难发现,一旦上线流量起来可能马上就是 ...
- Java 中 7 个潜在的内存泄露风险
虽然Java程序员不用像C/C++程序员那样时刻关注内存的使用情况,JVM会帮我们处理好这些,但并不是说有了GC就可以高枕无忧,内存泄露相关的问题一般在测试的时候很难发现,一旦上线流量起来可能马上就是 ...
- JavaScript 中的垃圾回收和内存泄露如何处理?| 技术头条
作者 | 浪里行舟 责编 | 郭芮 程序的运行需要内存.只要程序提出要求,操作系统或者运行时就必须供给内存.所谓的内存泄漏简单来说是不再用到的内存,没有及时释放.为了更好避免内存泄漏,我们先介绍Jav ...
- 【转】JavaScript 中的垃圾回收和内存泄露如何处理
转自 https://blog.csdn.net/csdnnews/article/details/89596750?ops_request_misc=%257B%2522request%255Fid ...
- 一文搞懂ThreadLocal及相关的内存泄露问题
首先,看一张整体的结构图,来帮助理解 什么是ThreadLocal ThreadLocal用于创建线程局部变量,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副 ...
- inputstreamreader未关闭会导致oom_ThreadLocal 一定会导致内存泄露?
在面试的时候,ThreadLocal作为高并发常用工具经常会被问到.而面试官比较喜欢问的问题有以下两个: 1.ThreadLocal是怎么实现来保证每个线程的变量副本的. 2.ThreadLocal的 ...
- Java内存泄露系列--内存泄露的原因及解决方案(大全)
原文网址:Java内存泄露系列--内存泄露的原因及解决方案(大全)_IT利刃出鞘的博客-CSDN博客 简介 简介 本文介绍Java中内存泄露的一些原因与解决方案. 如果内存泄露的空间足够大,就会导致内 ...
- 什么是内存溢出(Out Of Memory---OOM)和内存泄露 (Memory Leak)
1.内存溢出:(Out Of Memory---OOM) 系统已经不能再分配出你所需要的空间,比如系统现在只有1G的空间,但是你偏偏要2个G空间,这就叫内存溢出 例子:一个盘子用尽各种方法只能装4个果 ...
- JAVA 内存泄露详解(原因、例子及解决)
Java的一个重要特性就是通过垃圾收集器(GC)自动管理内存的回收,而不需要程序员自己来释放内存.理论上Java中所有不会再被利用的对象所占用的内存,都可以被GC回收,但是Java也存在内存泄露,但它 ...
最新文章
- 十二 手游开发神器 cocos2d-x editor 之游戏暂停悬浮层
- 2015/09/09夜晚js继续学习
- 在重复3次的数组中查找
- 图解 Java 常用数据结构
- get请求可以传body吗_都9102年了,GET和POST的区别掌握了没有?
- matlab求心率,心电图QRS波检测(计算心跳次数)
- 14寸笔记本电脑_纯小白预算5000到6000有什么好的笔记本电脑推荐吗?
- python进入内置函数文件_python基础知识-set、函数、内置函数、文件操作
- react16-reactDom.render流程分析
- 钉钉、微信产品大PK,基因已经决定了结果
- 路由器如何设置无线桥接
- 该怎么把光纤接入家里预埋的网线中?
- IP地址-子网划分详解
- 关于未知的USB设备(设备描述符请求失败)的解决方法
- Habor数据迁移方式有多少,skopeo效率最好
- 虚拟机通过无线设置静态ip连外网,不同环境的wifi照常使用
- RSD 教程 —— §2.3 窗口布局
- 三星 android 4.4.4,三星Galaxy设备什么时候升级Android 4.4.4
- MFC的Dlg和App什么区别?应用程序类与对话框类
- 手机群控系统(补充篇)
热门文章
- JDK源码解析之java.util.ListIterator
- C#使用ListView更新数据出现闪烁解决办法
- 工作所用的模块回滚脚本
- Can’t Activate Reporting Services Service in SharePoint
- jfinal poi
- ActiveReports 9实战教程(1): 手把手搭建环境Visual Studio 2013 社区版
- “四不像”病毒冒充多款知名软件 窃取电脑隐私
- [转]C++中sizeof(struct)怎么计算?
- 配置文件管理服务器,06-配置文件管理
- linux系统高级管理工具包,linux两大系统的包管理工具