惊!原来你是会内存泄漏的ThreadLocal
前言
在上周发的《求求你别用SimpleDateFormat了!》这篇文章中,有简单的提过在ThreadLocal的使用过程中不规范的话,可能会有坑,所以我们今天就来探讨一下有哪些坑需要我们在开发的过程中注意的。
正文
基本使用
ThreadLocal可以让你创建只能在自己线程里面读写的变量,也就是说,在同一个TreadLocal变量里面,不同的线程存取的变量只能自己看到,其他线程是访问不了的。
相关操作
//创建对象
ThreadLocal threadLocal = new ThreadLocal<String>();
//设值
threadLocal.set("深夜里的程序猿");
//取值
threadLocal.get(); // get 深夜里的程序猿
原理
threadlocal相关的操作还是比较简单的,那我们也来简单了解下它的实现原理,直接上源码。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
可以看到源码也是比较简洁,拿set()来说,先是获取当前的运行的线程,然后通过getMap()取出当前线程的threadLocals变量,它是一个ThreadLocalMap,接着对这个map进行赋值,key是threadlocal,value是存进来的值。
这里要提醒一下对源码比较生疏的同学,我们存进ThreadLocal的值,其实最后是存在了对应线程的ThreadLocalMap变量里面,并非在Threadlocal自己的变量里,大家可以结合源码跟上段的文字理解一下。
为啥会有坑?
那么问题来了,这一切看起来都很正常,那“内存泄漏”会出现在哪里呢?还是直接看源码。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
在这个实际存储内容的ThreadLocalMap类中中,使用了一个叫做Entry的静态内部类,它是针对于key也就是Threadlocal而存在的,对于Threadlocal来说是一个弱引用(关于强软弱虚引用有必要了解)。这意味这当ThreadLocal变量没有了强依赖以后,它会被GC回收掉,但是此时value不是弱引用,因此它还存在一个当前线程对它的引用,如果当前线程一直存活或者没调用threadlocal的remove方法的话。这个值将不会被销毁,这就发生了“内存泄漏”。(这段话需要细细品味)
那么什么时候会出现线程一直存在的情况呢?其实当我们在使用线程池的时候就是这样,为了避免频繁创建销毁线程,提高效率,线程池会重复利用一批线程,这就给我们使用不当的ThreadLocal埋下了炸弹。
那我们该如何避免呢?
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
//清理对ThreadLocal的弱引用
e.clear();
//清理key为null的元素
expungeStaleEntry(i);
return;
}
}
}
其实也很简单,在上面的源码中已经注释了,remove方法会对一些“无用的数据”进行清理,所以我们要养成习惯,当使用完threadlocal变量以后,及时remove掉就可以了。
结语
由此我们需要注意到,如果一个类的使用不当,随着时间的推移可能造成的后果是灾难性的,所以我们在用的过程中要注重一些“最佳实践”,勿“偷懒”。
喜欢的话,麻烦大家点个赞~关注一下微信公众号《深夜里的程序猿》,每天分享最干的干货
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
gitbook 入门教程之快速体验
本文主要介绍三种使用 gitbook 的方式,分别是 gitbook 命令行工具,Gitbook Editor 官方编辑器和 gitbook.com 官网. 总体来说,三种途径适合各自不同的人群,找到适合自己的方式就好,基本操作流程都是一样的. 命令行工具更适合具备编程经验开发者,具有简单高效易整合等特点. 编辑器更适合无任何编程经验的文学创作者,不熟悉 markdown 语法,不熟悉 git 工作流,这种情况下也推荐使用图形化操作的编辑器. 官网适合想要快速体验 gitbook 效果的萌新,只有觉得物超所值才能有动力搭建 gitbook 开发环境,不是吗? 当然,如果你想访问官网的话,你可能需要学会科学上网,网址见文章结尾. gitbook 命令行 首先需要创建存放书籍的目录,然后对该目录进行初始化,最后启动本地服务即可体验效果. 初始化项目 语法格式: gitbook init 如果是空目录会自动创建 README.md 和 SUMMARY.md 两个文件,当然也可以手动创建再初始化. 示例: # 创建 `gitbook` 演示项目 $ mkdir gitbook-demo # 初...
- 下一篇
关于RocketMQ Topic的创建机制,我还有一些细节上的思考
微信公众号「后端进阶」,专注后端技术分享:Java、Golang、WEB框架、分布式中间件、服务治理等等。 老司机倾囊相授,带你一路进阶,来不及解释了快上车! 在撸完RocketMQ Topic的创建机制后,我似乎还有一些意犹未尽的感觉,总觉得还缺一些什么。于是我就趁热打铁,提出以下两点我自己的一些思考。 当集群新增broker后,如何保证队列负载? 假设我现在有两个master broker分别为b1和b2组成了一个集群,我选择手动创建topic1,此时topic1的路由信息会发送到b1和b2,同时b1和b2会将路由信息注册到nameserver,发送topic1的消息时,会从nameserver获取topic1的路由信息,然后进行负载均衡平均分发到b1和b2。 以上是前提。 如果此时topic1的消息量剧增,b1和b2负载过高,集群这时加了b3和b4两个master broker节点,但此时b3和b4并没有topic1的路由信息,也就是topic1此时从nameserver获取到的broker信息只有b1和b2,因此虽然加了b3和b4,但topic1的消息并不会路由到b3和b4去,...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- CentOS7,CentOS8安装Elasticsearch6.8.6
- CentOS8安装Docker,最新的服务器搭配容器使用
- SpringBoot2整合Redis,开启缓存,提高访问速度
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- MySQL8.0.19开启GTID主从同步CentOS8
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题