栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

深耕Java多线程 - ThreadLocal造成内存泄漏的原因和解决方案

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

深耕Java多线程 - ThreadLocal造成内存泄漏的原因和解决方案

文章目录

1. ThreadLocal造成内存泄漏的原因?2. ThreadLocal内存泄漏解决方案?
问题:

1、ThreadLocal造成内存泄漏的原因?
2、ThreadLocal内存泄漏解决方案?

1. ThreadLocal造成内存泄漏的原因?

ThreadLocal的操作都是基于ThreadLocalMap展开的,而ThreadLocalMap是ThreadLocal的一个静态内部类,其实现了一套简单的Map结构。Entry用于保存ThreadLocalMap的Key-Value对条目,但是Entry使用了对ThreadLocal实例进行包装之后的弱引用(WeakReference)作为Key,其代码如下:

static class ThreadLocalMap {
    // map的条目数,作为哈希表使用
    private Entry[] table;
    
    // Entry继承了WeakReference,并使用WeakReference对key进行了包装
    static class Entry extends WeakReference> {
        Object value;

        Entry(ThreadLocal k, Object v) {
            super(k);
            value = v;
        }
	}
}

为什么Entry需要使用弱引用对Key进行包装,而不是直接使用ThreadLocal实例作为Key呢?

这里从一个简单的例子入手,假设有一个方法funcA()创建了一个“线程本地变量”,具体如下:

public void funA(){
    // 创建一个线程本地变量
    ThreadLocal threadLocal = new ThreadLocal();
    threadLocal.set(100);
    threadLocal.get();
    // 函数末尾
}

当线程tn执行funcA()方法到其末尾时,线程tn相关的JVM栈内存以及内部ThreadLocalMap成员的结构大致如图:

当线程tn执行完funcA()方法后,funcA()的方法栈帧将被销毁,强引用local的值也就没有了,但此时线程的ThreadLocalMap中对应的Entry的Key引用还指向ThreadLocal实例。如果Entry的Key引用是强引用,就会导致Key引用指向的ThreadLocal实例及其Value值都不能被GC回收,这将造成严重的内存泄漏问题。

什么是弱引用呢?

仅有弱引用(Weak Reference)指向的对象只能生存到下一次垃圾回收之前。换句话说,当GC发生时,无论内存够不够,仅有弱引用所指向的对象都会被回收。而拥有强引用指向的对象则不会被直接回收。

什么是内存泄漏?

不再用到的内存没有及时释放(归还给系统),就叫作内存泄漏。对于持续运行的服务进程必须及时释放内存,否则内存占用率越来越高,轻则影响系统性能,重则导致进程崩溃甚至系统崩溃。

由于ThreadLocalMap中Entry的Key使用了弱引用,在下次GC发生时,就可以使那些没有被其他强引用指向、仅被Entry的Key所指向的ThreadLocal实例能被顺利回收。并且,在Entry的Key引用被回收之后,其Entry的Key值变为null。后续当ThreadLocal的get()、set()或remove()被调用时,ThreadLocalMap的内部代码会清除这些Key为null的Entry,从而完成相应的内存释放。

总结一下,使用ThreadLocal会发生内存泄漏的前提条件如下:

(1)线程长时间运行而没有被销毁。线程池中的Thread实例很容易满足此条件。

(2)ThreadLocal引用被设置为null,且后续在同一Thread实例执行期间,没有发生对其他ThreadLocal实例的get()、set()或remove()操作。只要存在一个针对任何ThreadLocal实例的get()、set()或remove()操作,就会触发Thread实例拥有的ThreadLocalMap的Key为null的Entry清理工作,释放掉ThreadLocal弱引用为null的Entry。

综合以上两点可以看出:使用ThreadLocal出现内存泄漏还是比较容易的。但是一般公司对如何使用ThreadLocal都有编程规范,只要大家按照规范编写程序,也没有那么容易发生内存泄漏。

2. ThreadLocal内存泄漏解决方案?

编程规范推荐使用static final修饰ThreadLocal对象。

ThreadLocal实例作为ThreadLocalMap的Key,针对一个线程内的所有操作是共享的,所以建议设置static修饰符,以便被所有的对象共享。由于静态变量会在类第一次被使用时装载,只会分配一次存储空间,此类的所有实例都会共享这个存储空间,所以使用static修饰ThreadLocal就会节约内存空间。另外,为了确保ThreadLocal实例的唯一性,除了使用static修饰之外,还会使用final进行加强修饰,以防止其在使用过程中发生动态变更。

@Data
public class Person {
    private int age = 10;
}
public class Test {
    private static final ThreadLocal threadLocal = new ThreadLocal<>();
}

凡事都有两面性,使用static、final修饰ThreadLocal实例也会带来副作用,使得Thread实例内部的ThreadLocalMap中Entry的Key在Thread实例的生命期内将始终保持为非null,从而导致Key所在的Entry不会被自动清空,这就会让Entry中的Value指向的对象(person)一直存在强引用,于是Value指向的对象(person)在线程生命期内不会被释放,最终导致内存泄漏。所以,在使用完static、final修饰的ThreadLocal实例之后,必须调用remove()来进行显式的释放操作。

总结:

由于ThreadLocal使用不当会导致严重的内存泄漏问题,所以为了更好地避免内存泄漏问题的发生,我们使用ThreadLocal时遵守以下两个原则:

(1)尽量使用private static final修饰ThreadLocal实例。使用private与final修饰符主要是为了尽可能不让他人修改、变更ThreadLocal变量的引用,使用static修饰符主要是为了确保ThreadLocal实例的全局唯一。

(2)ThreadLocal使用完成之后务必调用remove()方法。这是简单、有效地避免ThreadLocal引发内存泄漏问题的方法。

总之,使用ThreadLocal能实现每个线程都有一份变量的本地值,其原因是每个线程都有自己独立的ThreadLocalMap空间,本质上属于以空间换时间的设计思路,该设计思路属于另一种意义的“无锁编程”。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/760476.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号