【ThreadLocal】

ThreadLocal是Java中用于线程局部变量的类,它为每个线程提供独立的变量副本,解决了多线程共享变量的线程安全问题。使用时需要注意在线程池中及时调用remove避免内存泄漏。ThreadLocalMap使用自定义的散列结构解决hash冲突,并且在get/set时处理了key为null的情况。ThreadLocal主要应用于重入方法的参数传递、全局用户信息存储和线程安全问题的解决。
摘要由CSDN通过智能技术生成

目录

1、ThreadLocal有什么用?

2、如何使用ThreadLocal

3、ThreadLocal的原理

4、ThreadLocal在线程池中使用时存在的问题

5、ThreadLocal如何解决上述问题

6、如何解决ThreadLocalMap的hash冲突

7、ThreadLocal的应用场景


【写在前面:学习时参考了很多博客、文章等,本文所使用的图片大部分来自网络(若有人看到了我没列出来的网址可以留言,或侵权可删),部分源码理解的也不是很深,只是勉强刚够面试的水平,以后遇到需要掌握的新的知识点 或者 需要更深一步理解源码 等,会不定时更新本文~】

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博客

Java 并发常见面试题总结(下) | JavaGuide(Java面试+学习指南)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值