漫聊 ThreadLocal (内存泄漏,弱引用)
背景
本文漫聊 ThreadLocal,想到什么写什么。大概会谈到几个问题
- 关于ThreadLocal 和线程同步
- ThreadLocal 在 JDK 中的实现
- ThreadLocal 的内存泄漏风险,以及关于它弱引用,讨论 “弱引用是引起内存溢出的原因吗?”
- 如何避免内存泄漏的风险
1、关于ThreadLocal 和 线程同步
解决线程访问共享变量的问题,可以使用线程同步技术,无论是synchronized还是 JUC的lock。
使用synchronized和JUC的lock,本质是通过加锁的机制来保证共享数据被多个线程争抢的有序性。加锁自然会导致并发性能下降,但是只有一份数据从而节省了内存,所以这是 “以时间换空间” (以耗时的增加换取了资源的减少,资源比如:CPU、内存、磁盘等)
还有另外一个解决方法,是以 “空间换时间”。数据弄多份,不加锁,访问贼快,因为不存在争抢同一份资源的问题,所以也没有线程安全的问题。所谓的 “空间换时间” 就是指耗费更多的内存减少了争抢的耗时。这就是ThreadLocal,将变量绑定到线程上,每个线程独有一份,多线程情况下不存在共享变量被争抢。
2、各版本的JDK的ThreadLocal 实现
大概有两种,JDK8一种,8之前一种。
-
JDK8
-
之前
jdk8 这么做的好处,它为什么这么改?
肯定是更有好处才这么优化
减少了哈希冲突和ThreadLocalMap扩容
以前是ThreadLocal维护ThreadLocalMap,key是线程对象,这会导致很多线程的时候,往ThreadLocalMap塞,这个map就很长,也容易出现哈希冲突、并且需要扩容。现在改成Thread去维护ThreadLocalMap,而key是ThreadLocal对象,我们一般不会去new这么多ThreadLocal,减少了哈希冲突和扩容的问题。
3、关于内存泄漏
先说结论:
-
ThreadLocal 用得不好,会引起内存泄漏。
其实jdk的代码已经在极力挽救,有许多的措施防止内存泄漏,但是不能完全避免这种可能性。
-
弱引用不是导致内存泄漏的原因,相反,弱引用还促进了”内存节约”。
这里的 “弱引用” 是指ThreadLocalMap 中的 Entry 的 key对 ThreadLocal 对象的引用是弱引用。
为什么说弱引用不是导致内存泄漏的原因?
1) 我们先假设用的是强引用
假设 threadLocal用完了,那么栈中 “threadLocal引用” 对于堆中的 “ThreadLocal对象” 的强引用就消失了。
由于线程是在线程池复用的,线程不会被回收。所以如果在Entry不手动remove的话,就永远存在value对 “你存的值” 的强引用,并且也存在 key 对 “ThreadLocal对象” 的强引用(假设用强引用的话),导致这些对象都不能被回收。因此累积多了就可能造成内存泄漏。
如果key对ThreadLocal对象是弱引用,在这种情况下ThreadLocal对象会被回收,key变成null。反而能促进节约内存。
所以key 对 “ThreadLocal对象” 的强引用并不是造成内存泄漏的原因。(反而还是有利的)
用弱引用还有个目的,就是ThreadLocal对象被回收后key的值是null,这是一个信号,表示value也可以被回收了,而Josh Bloch 和 Doug Lea确实做了优化:在多个方法,ThreadLocal#set里判定如果key是null就清掉无用的value
就算是有了上述这么多层保障,如果你不手工remove掉Entry,其value所强引用的对象还是有机会逃过被回收的(比如长时间不触发 ThreadLocal#set)
有人说value也用弱引用行吗?肯定不行了,发生GC后你就无法ThreadLocal#get回你的值了!
弱引用的特性:如果有个对象,只有弱引用引用它,则不管内存是否足够,发生GC,这个对象就会被回收。
既然用弱引用也不能完全解决内存泄漏,那为什么要用呢?
因为不能完全解决,也部分解决了。
- 首先是因为使用弱引用导致了ThreadLocal对象可以被回收了
- 其次更加重要的是,Josh Bloch 和 Doug Lea 用弱引用,是因为ThreadLocal被回收后,Entry的key就是null了,在代码的很多地方,检测到key是null就会把整个Entry移除,这就促进value被回收了。其实就是利用了弱引用导致key变成null,成为一个可以回收的标记或者说信号。
上面提到的 “在代码的很多地方” 比如set方法和remove方法,好多地方都会触发检查Entry的key是不是null
4、怎么避免内存泄漏的隐患
- ThreadLocal 最好当然是局部变量了,方法运行完立即就无强引用了。
- ThreadLocal 在大多数的时候应该是避免不了要用做成员变量的,比如一般在拦截器里头,那就一定要remove,最好是在finally里进行remove(也可以在spring的拦截器,在无论是否出现异常都会被执行的接口内执行remove)
- 最好是不要将ThreadLocal 用作静态变量
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/135214.html