这里写目录标题
一、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中存储用户信息可以避免用户信息的传参麻烦。