目录
【写在前面:学习时参考了很多博客、文章等,本文所使用的图片大部分来自网络(若有人看到了我没列出来的网址可以留言,或侵权可删),部分源码理解的也不是很深,只是勉强刚够面试的水平,以后遇到需要掌握的新的知识点 或者 需要更深一步理解源码 等,会不定时更新本文~】
1、ThreadLocal有什么用?
通常情况下,创建的变量可以被任意一个线程访问并修改。从而导致线程安全问题。ThreadLocal类可以实现每个线程都有自己的专属本地变量。如果创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,每个线程都可以使用get()、set()方法来获取默认值或者将默认值改为当前线程所存的副本的值。即ThreadLocal解决了多线程间共享变量时的线程安全问题。
2、如何使用ThreadLocal
一般会将ThreadLocal声明为一个静态字段,初始化如下:
(private) static ThreadLocal<Object> threadlocal = new ThreadLocal<>();
其中,Object就是原本堆中共享变量的数据。eg:有个User对象需要在不同线程之间进行隔离访问,此时可以将ThreadLocal定义为:
(private) static ThreadLocal<User> threadlocal = new ThreadLocal<>();
ThreadLocal中常用的方法:set() ——> 设置线程本地变量的内容;
get() ——>获取线程本地变量的内容;
remove() ——>移除线程本地变量;
PS:注意在线程池的线程复用场景中,在线程执行完毕时一定要调用remove,避免在线程被重新放入线程池时,本地变量的旧状态仍然被保存。(否则ThreadLocal会发生内存泄漏)
3、ThreadLocal的原理
Java中的线程其实是一个Thread类的实例对象。而一个实例对象中 实例成员字段 的内容肯定是这个对象所独有的。所以我们可以将 ThreadLocal线程本地变量作为Thread类的一个成员字段。
ThreadLocal.ThreadLocalMap threadlocals = null;
ThreadLocal.ThreadLocalMap inheritable = null;
即,Thread类中有一个threadlocals和一个inheritable变量,这两个变量都是ThreadLocalMap类型的变量。ThreadLocalMap可以看作是ThreadLocal类实现的定制化的HashMap,它的key存放的是 ThreadLocal实例本身,value存放的是通过set()设置的值。
上图中,一个线程池有两个线程时,相同的key在不同的散列表中存放不同的值。
4、ThreadLocal在线程池中使用时存在的问题
【内存泄漏】
上图ThreadLocal Ref 对ThreadLocal是一个强引用;一个Entry对象中,存储了ThreadLocal对象的弱引用和其对应value的强引用。当一个任务执行完成后,可以将ThreadLocal Ref设置为null,此时ThreadLocal Ref 对ThreadLocal的强引用链断开,ThreadLocal只剩下Entry的弱引用。垃圾回收器在内存资源紧张时对弱引用进行回收,此时Entry中的key为null,但是对应的value中还有值。即造成了内存泄漏。(内存泄漏:不会再使用的变量或对象 所占用的内存 不能被回收)
【线程安全】
在使用 ThreadLocal 时,每个线程都会维护自己的变量副本,因此在多线程环境下,ThreadLocal 变量不存在线程安全问题。但是,在使用线程池时,线程会被复用,因此与Thread绑定的ThreadLocal也会被复用,如果没有及时清除该变量的值,那么就可能会出现线程间数据交叉的问题,线程间数据交叉的问题属于线程安全问题。(线程安全:多个线程同时访问共享资源时,不会出现不正确的结果或者异常的情况。)
5、ThreadLocal如何解决上述问题
【内存泄漏】
事实上,ThreadLocalMap的get,set方法中,会对key(ThreadLocal)进行null判断,如果为null,value也设置为null.也可以手动条调用ThreadLocal.remove()方法,清除掉key为null的元素。
【线程安全】
为了避免这个问题,需要在使用完 ThreadLocal 变量后,及时清除其值,确保该值不会被长时间持有。通常在 finally 块中调用ThreadLocal.remove()方法来清除变量值,或者使用 try-with-resources 语句块来自动关闭资源,确保变量不会被长时间持有,从而避免线程间数据交叉的问题。另外,也可以使用 InheritableThreadLocal 来继承父线程的 ThreadLocal 值,确保在线程池中传递值的正确性。
6、如何解决ThreadLocalMap的hash冲突
开放寻址之线性探测
ThreadLocalMap,它是ThreadLocal中的一个内部类。ThreadLocalMap作为hash表的一种实现方式,使用了开放寻址法来解决hash冲突。
【元素插入】
开放寻址法的核心是如果出现了散列冲突,就重新探测一个空闲位置,将其插入。当我们往散列表中插入数据时,如果某个数据经过散列函数散列之后,存储位置已经被占用了,我们就从当前位置开始,依次往后查找,看是否有空闲位置,直到找到为止。
从图中可以看出,散列表的大小为 10 ,在元素 x 插入散列表之前,已经 6 个元素插入到散列表中。 x 经过 Hash 算法之后,被散列到位置下标为 7 的位置,但是这个位置已经有数据了,所以就产生了冲突。于是我们就顺序地往后一个一个找,看有没有空闲的位置,遍历到尾部都没有找到空闲的位置,于是我们再从表头开始找,直到找到空闲位置 2 ,于是将其插入到这个位置。
【Entry默认长度16,扩容时长度也为2^n ——此时得到key,通过HsahCode()计算出hash值后,hash & (length - 1)等价于 hash % length ,提高了运算效率】
7、ThreadLocal的应用场景
1、在重入方法中替代参数的显式传递(重要)
在重入方法中,如果需要传递参数,可以使用 ThreadLocal 来替代参数的显式传递。假设有一个方法 A 调用了方法 B,B 又调用了方法 C,现在需要在方法 A 中传递参数到方法 C,可以使用 ThreadLocal 来实现:
首先,在方法 A 中创建一个 ThreadLocal 对象,并将参数存储到 ThreadLocal 中:
public void methodA(String param) {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set(param);
methodB();
}
在方法 B 中获取 ThreadLocal 中的参数,并传递给方法 C:
public void methodB() {
String param = threadLocal.get();
methodC(param);
}
在方法 C 中不需要传递参数,直接从 ThreadLocal 中获取即可:
public void methodC() {
String param = threadLocal.get();
// do something with param
}
通过使用 ThreadLocal,我们可以在方法 A 中将参数存储到 ThreadLocal 中,在方法 B 中获取参数并传递给方法 C,在方法 C 中直接从 ThreadLocal 中获取参数,避免了参数的显式传递。使用 ThreadLocal 的好处是可以减少参数传递的复杂性和代码的耦合性,提高程序的可读性和可维护性。同时,由于 ThreadLocal 保证了每个线程都有自己独立的数据副本,所以不会出现线程安全问题。
2、全局存储用户信息
使用ThreadLocal替代Session的使用,当用户要访问需要授权的接口的时候,可以先在拦截器中将用户的Token存入ThreadLocal中;之后在本次访问中任何需要用户用户信息的都可以直接从ThreadLocal中拿取数据。(ThreadLocal 只能在同一线程内共享数据,因此如果需要在多台机器上共享用户数据,则需要使用其他的技术实现,如分布式缓存或者数据库。)
3、解决线程安全问题
依赖于ThreadLocal本身的特性,对于需要进行线程隔离的变量可以使用ThreadLocal进行封装。
eg:线程安全的日期格式化类:SimpleDateFormat 类是非线程安全的,如果多个线程共享一个 SimpleDateFormat 实例,就可能导致数据交叉和线程安全问题,可以使用 ThreadLocal 来为每个线程提供一个独立的 SimpleDateFormat 实例,从而避免线程安全问题。
参考博客、文章等地址:
分析Threadlocal内部实现原理,并解决Threadlocal的ThreadLocalMap的hash冲突与内存泄露_threadlocal hash冲突_阿啄debugIT的博客-CSDN博客
ThreadLocal原理及使用场景_小机double的博客-CSDN博客