摘要:最近,朋友小王在参加某大厂的社招面试,面试官笑眯眯地问:“说说ThreadLocal的作用?有啥缺点?”
最近,朋友小王在参加某大厂的社招面试,面试官笑眯眯地问:“说说ThreadLocal的作用?有啥缺点?”
小王心里一喜,这可是老生常谈的问题,于是滔滔不绝地讲了一通,啥线程隔离、啥存储上下文信息、啥用户Session,统统都摆上了台面。
面试官听完点点头,接着抛出一个灵魂拷问:“那你能分析一下ThreadLocal的内存泄漏问题吗?以及如何避免?”
小王:“呃……这……ThreadLocal还能内存泄漏?”
完了!凉凉!
回来后,小王苦哈哈地跟我吐槽,我赶紧给他补了一课。今天,我们就一起来探究:ThreadLocal的内存泄漏问题及其解决方案!
ThreadLocal 的底层实现
ThreadLocal是 Java 提供的一种线程封闭机制,每个线程都可以存储自己的变量副本,互不干扰。那么问题来了:这些变量存储在哪里呢?
其实,它们存储在 Thread 里,每个 Thread 内部都有一个 ThreadLocalMap,专门用来存储 ThreadLocal 变量。
我们来看 Thread 类的源码(JDK 8):
嗯,这个 threadLocals 变量就是核心,它的类型是 ThreadLocalMap,专门用来存储 ThreadLocal 的值。
再看看 ThreadLocalMap 的内部结构(简化版):
我们发现了一个关键点:Entry 继承自 WeakReference(弱引用)。这意味着 ThreadLocal 本身是弱引用,但 value 却是强引用!
那么问题来了!
为什么会内存泄漏?
我们来看这样一段代码:
这段代码有两个问题:
threadLocal 被置为 null,但 value 依然存在!
ThreadLocal 是弱引用,GC 可能会回收它,但 value 依然被 ThreadLocalMap 强引用着!
当 GC 发生时:
ThreadLocal 变量本身会被清理掉,因为它是弱引用。
ThreadLocalMap 的 Entry.key == null,但 value 还在,占据大量内存!
这样,如果当前线程是线程池的线程,那么这个 value 就一直不会被回收,导致内存泄漏!
这,就是ThreadLocal 内存泄漏的真正原因!
如何避免 ThreadLocal 内存泄漏?
既然知道了原因,那解决方案也就呼之欲出了!
方案 1:手动 remove
最简单、最有效的方式,就是在使用完 ThreadLocal 变量后,手动调用 remove方法。
这样,ThreadLocalMap 里的 Entry 就会被清理掉,value 也就不会泄漏了!
正确示例:
为什么要用 finally?
因为如果发生异常,导致 remove 没有执行,那么 value 还是会泄漏!所以,我们一定要在 finally 代码块里手动清理。
方案 2:使用 Static 变量避免多个 ThreadLocal 实例
有时候,我们不希望 ThreadLocal 被 GC 过早回收,可以使用static 变量来持有它,确保 ThreadLocal 不会被回收:
不过,这种方式只适用于 ThreadLocal生命周期和应用一致的情况,否则可能会导致 ThreadLocal 变量不被回收,反而导致 OOM!
方案 3:使用 InheritableThreadLocal
如果是子线程需要继承父线程的 ThreadLocal 变量,可以使用 InheritableThreadLocal,避免子线程访问不到 ThreadLocal 变量:
但它不能解决内存泄漏问题,只是拓展了 ThreadLocal 的作用范围。
总结
常见错误
忘记 remove,导致 value 无法回收。
ThreadLocal 被回收,但 value 还在,导致内存泄漏。
线程池使用 ThreadLocal,但不清理,导致长期占用内存。
正确做法
在 finally 代码块里手动调用 remove,避免内存泄漏。
避免不必要的 ThreadLocal 实例,尽量复用。
如果一定要在线程池中使用 ThreadLocal,务必 remove 掉!
尾声小王看完这篇文章,恍然大悟:“原来 ThreadLocal 还有这么大的坑,难怪我面试挂了!”
“那你下次再面试,还怕被问到这个问题吗?”我笑着问。
“怕啥!我还想主动给面试官讲一遍,顺便聊聊 JVM 内存模型!”
如果你觉得这篇文章有用,欢迎点赞、收藏、转发!让更多人了解 ThreadLocal 的秘密,不再掉坑!
来源:茉茉课堂