什么是ThreadLocal?它有什么作用?
ThreadLocal被称为线程局部变量,每个线程可以创建属于其独享的对象,一个线程不能访问另外一个对象创建的实例,达到里线程隔离效果。
ThreadLocal
一个线程可以使用不同的ThreadLocal实例来创建并访问其不同的线程特有对象,多个线程使用同一个ThreadLocal
Threadlocal的一个非常大的作用在于,既能够在多个方法中共用一个变量,而这个变量又和其他线程隔离。
ThreadLocal的使用场景
1.隐式传参(Implicit Parameter Passing)。在业务开发中,如果一个参数使用到的地方非常多,在各个类中需要传递,我们通常使用方法传参,这时候使用ThreadLocal就不需要传递了,在一条线程里可以随时用到。不过,也有的观点认为隐式传参使得系统难以理解。隐式传参的实现方式通常是使用一个(工具)类的静态方法来封装对线程特有对象的访问,相应的地方只需要调用静态方法就可以设置或者获线程特有对象。
2.使用线程安全对象或者非线程安全对象,不希望引入锁或者内部锁的使用带来资源开销问题。
3.特定于线程的单例(Singleton)模式。广泛使用的单例模式所实现的效果是在一个Java虚拟机中的一个类加载器下某个类有且只有一个实例。如果我们希望对某个类每个线程有且只有一个该类的实例,就可以用线程独有对象。
容易入坑?内存泄漏?
内存泄漏(Memory Leak)指由于对象永远无法被垃圾回收导致其占用Java虚拟机内存无法被释放,持续的内存占用会导致Java虚拟机可用内存逐渐减少,并最终可能导致Java虚拟机内存溢出(Out Of Memory),直到Java虚拟机宕机。
强引用 > 软引用 > 弱引用 > 虚引用
在Web应用中使用ThreadLocal中如果处理不当极易导致内存泄漏。先简单了解一下ThreadLocal的内部实现机制。ThreadLocalMap是Thread的成员变量,ThreadLocal的静态内部类,类似HashMap结构,每个ThreadLocalMap内部都会含有若干个Entry条目(就是Key-Value键值对),Entry的key为ThreadLocal实例,value是线程特有对象。
由于Entry对ThreadLocal实例的引用是一个弱引用(WeakReference),因此它不会阻止被引用的ThreadLocal实例被垃圾回收,Entry的key可能为null,这时候的Entry被称为无效条目(Stale Entry)。在value方面,Entry对其的引用为强引用,那么无效条目会阻止其引用的线程特有对象被垃圾回收。由于这个原因,当ThreadLoaclMap有新的Entry条目被添加进去的时候,ThreadLocalMap会把无效条目清理掉(ThreadLocalMap在创建新的映射关系的时候可以复用无效条目,不一定要创建新的条目)。
但是,这个处理有一定缺陷。当有无效条目积累但是线程处于非运行状态的时候,该线程的ThreadLocalMap不会有任何变化,这可能会导致各个线程的各个Entry所引用的线程特有对象都无法被垃圾回收,导致了内存溢出。
如何规避内存泄漏
项目中通常执行Threadlocal.remove()操作来规避内存泄漏,在以下场景:
-
Web服务器对于同一个Http请求进行处理的时候,Filter.doFilter方法的执行线程和Servlet的执行线程是相同的,所以可以自定义个过滤器来,在doFilter方法执行完后执行
-
使用try-finally保证执行
-
在拦截器中的afterCompletion方法中执行
子线程能获取父线程的ThreadLocal值吗
假如有一个场景,子线程需要需要使用存放在threadLocal变量里的用户信息(用户id、用户姓名或者用户类型等),再比如一些中间件需要把统一的id追踪的整个调用链路记录下来,会使用什么方式实现?
- 可以在父线程创建子线程的时候传入父线程的变量
- 使用InheritableThreadLocal,继承自ThreadLocal,提供了一个很重要的特性,就是让子线程可以访问在父线程中设置的本地变量
源码实现
主要几个方法:set、get、remove、setInitialValue
1. get & set
通过获取当前线程来获取ThreadLocalMap,通过传入this(注意这里是ThreadLocal实例而非Thread)获取Entry条目,若不为空,直接返回T,否则直接返回setInitialValue方法返回的value.
在getMap方法中,直接返回线程t的成员变量threadLocals
Thread类里:
ThreadLocal的静态内部类,可以看到ThreadLocalMap的Entry继承了WeakReference,并且使用ThreadLocal作为键
接下来看到setInitialValue()方法,initialValue()为protected类型,给子类重写的。后面是同样的,也是ThreadLocalMap不为空,设置键值对,否则创建Map
createMap创建一个ThreadLocalMap给线程的threadLocals
set()方法类似,不再赘述
至此,我们知道了每个线程内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threaLocals,用来存储线程特有对象,键为ThreadLoca变量,值为线程特有对象。初始化时,如果调用get或者set方法,就会对Thread.ThreadLocalMap进行初始化