Java中的ThreadLocal原理,内存泄露原因

一、ThreadLocal是什么

ThreadLocal是java中一个管理每个线程的独立变量的工具类,他允许每个线程用于各自独立的变量副本,确保每个线程拥有自己的独立数据而不被其他线程打扰。
(说白了就是每个线程都有维护了一个自己的map,可以在里面存值,而key就是ThreadLocal,想存什么类型的值,就创建什么类型的ThreadLocal,然后通过ThreadLocal实例来维护这些值)

二、ThreadLocal的原理

1、三个类的关系

首先,有三个类Thread,ThreadLocal,ThreadLocalMap,要先搞懂这三个类的关系。
1)、Thread线程类内部存在成员变量ThreadLocalMap。
2)、ThreadLocalMap就是一个Map,在ThreadLocal里定义,key就是一个具体的ThreadLocal变量实例,value就是要存储的独立变量。如图所示,下面的ThreadLocalMap里存了key为三种ThreadLocal的键值对,他们分别定义了要存储的value类型为User、Email和Role。
3)、ThreadLocal是管理Thread内部的ThreadLocalMap的工具类,内部的set(),get(),remove()等方法都是ThreadLocalMap里的key为当前ThreadLocal实例的键值对进行操作。
在这里插入图片描述

2、TheadLocal方法解析

1)set()方法

在这里插入图片描述
①获取当前线程的ThreadLocalMap
②如果ThreadLocalMap不存在,则创建:创建ThreadLocalMap的同时设置key为当前的ThreadLocal以及value为当前值,并将结果赋给当前线程的ThreadLocalMap
在这里插入图片描述
③如果ThreadLocalMap已经存在,则覆盖当前的Entry<ThreadLocal,value>键值对的value值。

2)get()方法

在这里插入图片描述
①获取当前线程的ThreadLocalMap(以下称为map)
②如果map不存在,则设置初始值;如果map存在,则通过当前线程获取到map的键值对的值,如果值不存在,那么设置初始值。
③设置初始值,首先获取初始值,点击进去发现return null,实际上需要通过ThreadLocal.withInitial()方法来创建ThreadLoal,同时将初始值传入。例如以下代码。

private static ThreadLocal<DateFormat> threadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

获取到初始值后,同理,判断当前线程的map是否创建,创建则覆盖,没创建则创建并赋值。
在这里插入图片描述

3)ThreadLocalMap的Entry解析

①、可以看到Entry继承了弱引用类WeakReference
在这里插入图片描述
②进入super方法,最终把当前的ThreadLocal指向引用类的参数referent
在这里插入图片描述
在这里插入图片描述
③引用类内部的get()和clear()方法返回和清除当前的referent
在这里插入图片描述
④可以看到,Entry处理hash冲突使用了最简单的线性探测法
在这里插入图片描述

4)remove()方法

①获取当前线程的map,将当前ThreadLcoal作为key带入map的remove方法。
在这里插入图片描述
②根据key来和当前的entry的key比较,其中e.get()就调用到了父类Reference的get()方法,直接返回e的key,如果相等则匹配到上,不等就会通过线性探测法与下一个位置的entry匹配。匹配成功,进入expungeStaleEntry方法进行Entry的清空。
在这里插入图片描述
③首先会清空当前staleSlot位置的键值对,然后会遍历下一个位置的key是否为空,直到下一个位置的entry为空位置。
情况一、如果下个位置key为null,则这种是经过gc之后留下的,通过这次方法的调用,可以清理掉这种key为null,但value还有值的entry,在一定程度上避免了内存泄露,但如果当前方法一直没被调用,而且当前线程一直没死,其实还是会存在内存泄露的情况,因此这就是为什么用完ThreadLocal后一定要调用remove()方法。
在这里插入图片描述
情况二、如果下个位置key不为null时,则要对这个key重hash也就是重新计算位置,为什么呢?因为可能这个key是经过hash及线性探测而来到这个位置的,而可能上一个位置才是他hash后的位置(当然也有可能上上上个),而因为上一个位置已经指向null了,如果这个key不回到上个位置(或者上个位置之前的位置),就会在get(可以看上面的get()方法,如果获取到的Entry为null,则直接设置初始值了)这个key的时候永远get不到了,所有之后④⑤部操作都是为了让它回到上一个指向null的位置(或者null之前的位置)
在这里插入图片描述
在这里插入图片描述

三、关于内存泄露的问题解析

1、ThreadLocal发生内存泄露的场景

由于一个ThreadLocal所存储的变量是存在线程中的ThreadLocalMap中的,所以当一个线程一直存活不死亡时,里面的ThreadLocalMap就会一直存在。当普通的线程执行完,内部的map也会被回收,所以内存泄露的前提是必须是存在一个长时间存活的线程。
在我们的项目中一般都是使用线程池来管理线程,线程是不消亡的,而对于一个线程任务来说,执行一次任务往往只需对ThreadLocal的执行一次读写就结束了,后续不会再次使用到该ThreadLocal,但是ThreadLocalMap却仍然存有该Entry,而如果不清理这些用不到的Entry,就会发生内存泄露。
因此,内存泄漏的根源是ThreadLocalMap的生命周期跟线程一样长,在线程一直存活却不再使用对应的Entry时,如果没有手动删除对应key就会导致内存泄漏

2、如何避免内存泄露

只要能及时清理这些用不到的Entry,其实就能避免内存泄露。
①每次使用ThreadLocal,如果后续不再用到,则调用remove方法,手动把Entry清除。
②ThreadLocal对ThreadLocalMap的key设置为弱引用,也一定程度上避免了内存泄漏,当下一次gc时,ThreadLocal实例会被回收,也就是key为null,随着之后ThreadLocalMap的set(),get()等操作,就会调用到expungeStaleEntry方法清除key为null的Entry。但如果之后再也没有对ThreadLocalMap进行操作也是会内存泄露的。

3、四种引用

①强引用:一般jvm在进行gc时,当一个对象在 GC Root 的引用链中时,它就是可以称为被强引用指向的对象,它是无法被回收的。
②软引用和弱引用:两者是相对强引用而言的,被软引用和弱引用指向的对象在即使存在GC Root引用链中,也会被回收。其中软引用指向的对象在内存不足时被回收,而弱引用指向的对象只有进行了gc就会回收。
③虚引用:

4、ThreadLocal为什么要将ThreadLocalMap的key设为弱引用

①如果设置为强引用,那么线程在一直存活的状态下,因为ThreadLocalMap也一直存活,所以Entry也会一直存活,而如果不使用remove方法清除掉,则会发生内存泄露。
②如果设置为弱引用,即使线程一直存活,也会因为下一次gc,Entry的key为null,从而之后再对ThreadLocalMap进行get,set,remove方法时清除掉key为null的Entry,从而一定程度上避免了内存泄漏。

四、ThreadLocal的应用场景

1、SimpleDateFormat

最常用的就是SimpleDateFormat场景,因为SimpleDateFormat是线程不安全的,所以不能多个线程同时共用一个SimpleDateFormat,解决方案
①加锁,同一时刻只能有一个线程使用SimpleDateFormat,但影响性能。
②每个线程new一个来用,当线程过多时,消耗的内存也会上升。
使用ThreadLocal,为每个线程初始化一个SimpleDateFormat,每个线程池内的线程都有独立的一个SimpleDateFormat,即避免了每个线程执行任务时要额外new,也避开了多线程共用SimpleDateFormat的线程不安全问题。

2、JDBC中与数据库的连接

程序每次对数据库进行查询时,需要建立数据库连接,如果每个线程在查询时都建立一次连接,无疑是非常浪费资源的,那每个线程去共用一个连接可以吗,也不行,在JDBC中,connection是线程不安全的,一个connection对应的是一个事物。因此在JDBC中,使用ThreadLocal对每个线程数据库连接进行的独立存储,保证了一个线程在一次业务中多次进行数据库访问时,使用的是同一个连接,既避免了多次创建连接浪费资源,也保证了其他线程无法干扰到当前线程的数据库访问。

3、SpringSecurity获取当前用户信息

在web程序中,一次用户的请求可能会用到当前用户的信息,这次请求可能会调用多个方法,而每个方法里都需要用到用户信息,在ThreadLocal中存储用户信息可以避免用户信息的传参麻烦。

  • 22
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ThreadLocalJava的一个线程封闭对象,它提供了一种在多线程环境下实现线程私有变量的机制。每个线程都可以通过ThreadLocal对象获取自己的私有变量,并且线程之间互不干扰。这样可以方便地在多线程场景下共享数据,而又不用担心线程安全的问题。 ThreadLocal原理是通过在每个线程维护一个ThreadLocalMap对象来存储线程私有变量。ThreadLocalMap是ThreadLocal类的一个静态内部类,它使用ThreadLocal对象作为key,实际的变量值作为value。每个线程对应一个ThreadLocalMap,通过ThreadLocal对象可以获取到当前线程对应的ThreadLocalMap。 当我们调用ThreadLocal的set方法来设置变量值时,实际上是将该值存储在当前线程的ThreadLocalMap。当我们调用ThreadLocal的get方法来获取变量值时,实际上是从当前线程的ThreadLocalMap查找对应的值。 至于内存泄露问题,ThreadLocal存在一定的潜在风险。如果我们在使用完ThreadLocal后没有进行及时的清理操作,就有可能导致内存泄露。这是因为ThreadLocalMap的Entry对象持有了对ThreadLocal对象的强引用,而线程的生命周期比较长,如果没有及时清理,那么即使线程已经结束了,ThreadLocal对象也无法被回收,从而导致内存泄露。 为了避免内存泄露,我们可以在使用完ThreadLocal后调用remove方法来手动清理对应的Entry对象。另外,使用ThreadLocal时要特别注意避免在高并发场景下出现内存泄露的问题,可以合理地使用线程池,控制ThreadLocal的使用范围,或者使用InheritableThreadLocal来代替ThreadLocal

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值