1. ThreadLocal 的基本使用
-
在Java的多线程并发执行过程中,为了保证多个线程对变量的安全访问,可以将变量放到ThreadLocal类型的对象中,使变量在每个线程中都有独立值,不会出现一个线程读取变量时而被另一个线程修改的现象。ThreadLocal类通常被翻译为
线程本地变量
类或者线程局部变量
类。 -
ThreadLocal是位于JDK的
java.lang
核心包中。如果程序创建了一个ThreadLocal实例,那么在访问这个变量的值时,每个线程都会拥有一个独立的、自己的本地值。线程本地变量可以看成专属于线程的变量,不受其他线程干扰,保存着线程的专属数据。当线程结束后,每个线程所拥有的那个本地值会被释放。在多线程并发操作线程本地变量
的时候,线程各自操作的是自己的本地值,从而规避了线程安全问题。 -
ThreadLocal如何做到为每个线程存有一份独立的本地值呢?
- 一个ThreadLocal实例可以形象地理解为一个
Map
(早期版本的ThreadLocal是这样设计的)。 - 当工作线程Thread实例向本地变量保持某个值时,会以Key-Value对(即键值对)的形式保存在
ThreadLocal内部的Map
中,其中Key为线程Thread实例,Value为待保存的值。 - 当工作线程Thread实例从ThreadLocal本地变量取值时,会以Thread实例为Key,获取其绑定的Value。
- 一个ThreadLocal实例可以形象地理解为一个
方法 | 说明 |
---|---|
set(T value) |
设置当前线程在线程本地变量实例中绑定的本地值 |
get() |
获得当前线程在线程本地变量实例中绑定的本地值 |
remove() |
移除当前线程在线程本地变量实例中绑定的本地值 |
public class ThreadLocalTest {
@Data
static class Foo {
//实例总数
static final AtomicInteger AMOUNT = new AtomicInteger(0);
int index = 0; //对象的编号
int bar = 10; //对象的内容
//构造器
public Foo() {
index = AMOUNT.incrementAndGet(); //总数增加,并且给对象的编号
}
@Override
public String toString() {
return index + "@Foo{bar=" + bar + '}';
}
}
//定义线程本地变量
private static final ThreadLocal<Foo> LOCAL_FOO = new ThreadLocal<Foo>();
@Test
public void testThreadLocal() throws InterruptedException {
//获取自定义的混合线程池
ThreadPoolExecutor threadPool = ThreadUtil.getMixedTargetThreadPool();
//共5个线程
for (int i = 0; i < 5; i++) {
threadPool.execute(new Runnable() {
@Override
public void run() {
//获取“线程本地变量”中当前线程所绑定的值
if (LOCAL_FOO.get() == null) {
//设置“线程本地变量”中当前线程所绑定的值
LOCAL_FOO.set(new Foo());
}
System.out.println("初始的本地值:" + LOCAL_FOO.get());
//每个线程执行10次
for (int i = 0; i < 10; i++) {
Foo foo = LOCAL_FOO.get();
foo.setBar(foo.getBar() + 1);
sleepMilliSeconds(10);
}
System.out.println("累加10次之后的本地值:" + LOCAL_FOO.get());
//删除“线程本地变量”中当前线程所绑定的值,对于线程池中的线程尤其重要
LOCAL_FOO.remove();
}
});
}
ThreadUtil.sleepMilliSeconds(Integer.MAX_VALUE);
}
-
通过输出的结果可以看出,在线程本地变量(LOCAL_FOO)中,每一个线程都绑定了一个独立的值(Foo对象),这些值对象是线程私有的,可以理解为线程的本地值,每一次操作都是在自己的同一个本地值上进行的,从线程本地值的index始终一致可以看出,每个线程操作的是自己线程私有的Foo对象。
-
如果线程尚未在本地变量(如LOCAL_FOO)中绑定一个值,直接通过get()方法去获取本地值会获取到一个空值,此时可以通过调用set()方法设置一个值作为初始值,
//获取“线程本地变量”中当前线程所绑定的值 if (LOCAL_FOO.get() == null) { //设置“线程本地变量”中当前线程所绑定的值 LOCAL_FOO.set(new Foo()); }
-
在当前线程尚未绑定值时,如果希望能从线程本地变量获取到初始值,而且也不想采用以上的
判空后设值
这种相对烦琐的方式,可以调用ThreadLocal.withInitial(.)静态工厂方法,在定义ThreadLocal对象时设置一个获取初始值的回调函数ThreadLocal<Foo> LOCAL_F00 = ThreadLocal.withInitial(()-> new Foo());
-
线程尚未绑定值而直接从线程本地变量获取值时,将会取得回调函数被调用之后所返回的值。
2. ThreadLocal
使用场景
- ThreadLocal是解决线程安全问题一个较好方案,它通过为每个线程提供一个独立的本地值去解决并发访问的冲突问题。在很多情况下,使用ThreadLocal比直接使用同步机制(如synchronized)解决线程安全问题更简单、更方便,且结果程序拥有更高的并发性。
- ThreadLocal使用场景大致可以分为以下两类:
线程隔离
:- ThreadLocal的主要价值在于线程隔离,ThreadLocal中的数据只属于当前线程,其本地值对别的线程是不可见的,在多线程环境下,可以防止自己的变量被其他线程篡改。
- 由于各个线程之间的数据相互隔离,避免了同步加锁带来的性能损失,大大提升了并发性的性能。
- ThreadLocal在线程隔离的常用案例为: 可以每个线程绑定一个用户会话信息、数据库连接、HTTP请求等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。
- 常见的ThreadLocal使用场景为
数据库连接独享
、Session数据管理
等。 - 在线程隔离场景中,使用ThreadLocal的典型案例为: 可以每个线程绑定一个数据库连接,使得这个数据库连接为线程所独享,从而避免数据库连接被混用而导致操作异常问题。
跨函数传递数据
- 通常用于同一个线程内,跨类、跨方法传递数据时,如果不用ThreadLocal,那么相互之间的数据传递势必要靠返回值和参数,这样无形之中增加了这些类或者方法之间的耦合度。
- 由于ThreadLocal的特性,同一线程在某些地方进行设置,在随后的任意地方都可以获取到。线程执行过程中所执行到的函数都能读写ThreadLocal变量的线程本地值,从而可以方便地实现跨函数的数据传递。
- 使用ThreadLocal保存函数之间需要传递的数据,在需要的地方直接获取,也能避免通过参数传递数据带来的高耦合。
- 在跨函数传递数据场景中使用ThreadLocal的典型案例为: 可以为每个线程绑定一个
Session(用户会话)信息
,这样一个线程所有调用到的代码都可以非常方便地访问这个本地会话,而不需要通过参数传递。
2.1 使用 ThreadLocal 进行线程隔离
- ThreadLocal在线程隔离应用场景的典型应用为数据库连接独享。下面的代码来自Hibernate,代码中通过ThreadLocal进行数据库连接(Session)的线程本地化存储,主要的代码如下:
private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() throws InfrastructureException {
Session s=(Session)threadSession.get();
try {
if (s == null){
s = getSessionFactory().openSession();
threadSession.set(s):
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}
Hibernate对数据库连接进行了封装,一个Session代表一个数据库连接。
在Hibernate的getSession()方法中,首先判断当前线程中有没有放进去session,如果还没有,那么通过sessionFactory()).openSession()来创建一个Session,再将Session设置到ThreadLocal变量中,这个Session相当于线程的私有变量,而不是所有线程共用的,显然其他线程中是取不到这个Session。
一般来说,完成数据库操作之后程序会将Session关闭,从而节省数据库连接资源。如果Session的使用方式为共享而不是独占,在这种情况下,Session是多线程共享使用的,如果某个线程使用完成之后直接将Session关闭,其他线程在操作Session时就会报错。所以Hibernate通过ThreadLocal非常简单实现了数据库连接的安全使用。
2.2 使用 ThreadLocal 进行跨函数数据传递
-
ThreadLocal在跨函数数据传递应用场景的典型有很多:
- 用来传递请求过程中的用户ID。
- 用来传递请求过程中的用户会话(Session)
- 用来传递HTTP的用户请求实例 HttpRequest。
- 其他需要在函数之间频繁传递的数据。
public classSessionHolder { // session id,线程本地变量 private static final Threadlocal<String> sidLocal = new ThreadLocal<>("sidlocal"); // 用户信息,线程本地变量 private static final Threadlocal<UserDTo> sessionUserLocal = new ThreadLocal<>("sessionUserLocal"); //session,线程本地变量 private static final ThreadLocal<HttpSession> sessionLocal = new ThreadLocal<>("sessionLocal"); //省略其他 // 保存session在线程本地变量中 public static void setSession(HttpSession session) { sessionLocal.set(session); } // 取得绑定在线程本地变量中的session public static HttpSession getSession() { HttpSession session=sessionLocal.get(); Assert.notNull(session,"session未设置"); return session: } //省略其他 }
3. ThreadLocal 内部结构演进
-
在早期的JDK版本中,ThreadLocal的内部结构是一个Map,其中每一个线程实例作为Key,线程在线程本地变量中绑定的值为Vaue(本地值)。早期版本中的Map结构,其拥有者为ThreadLocal实例,每一个ThreadLocal实例拥有一个Map实例。
-
在大部分的应用中,实际上线程比较多,往往会配置数百个线程。反过来说,一个应用的线程局部变量又很少,可能就几个。
-
HashMap在扩容时存在高成本、低性能问题。为什么呢?
- 在HashMap的内部是一个槽位(slot)数组,这个数组也叫哈希表,存储的是Key的哈希值。当槽位数组中的元素个数超过
容量(默认为16) x 加载因子(默认为0.75)
也就是是12的时候,槽位数组会进行扩容,扩容成32个槽位。 - 对于每一个槽位,可以理解为一个桶(bucket),如果一个桶内元素超过8个,链表会转换成红黑树。无论是槽位数组扩容还是桶内链表转换成红黑树,这都是高成本、低性能的扩容工作。
- 在HashMap的内部是一个槽位(slot)数组,这个数组也叫哈希表,存储的是Key的哈希值。当槽位数组中的元素个数超过
-
ThreadLocal实例内部的Map结构叫作
ThreadLocalMap
,并没有直接采用HashMap对象,而是自定义的和HashMap类似的结构。与HashMap不同的是,ThreadLocalMap去掉了桶结构,如果发生哈希碰撞,将Key相同的Entry放在槽位后面相邻的空闲位置上。为了区分这两种处理碰撞的方案- 把HashMap(数组加链表)的处理方式叫作链地址法,即发生碰撞就把Entry放在桶的链表中。
- 把ThreadLocalMap的处理方式叫作开放地址法,即发生碰撞,就按照某种方法继续探测哈希表中的其
他存储单元,直到找到空位置为止。
-
ThreadLocalMap与HashMap一样,槽位数组(哈希表)在扩容时存在高成本、低性能问题。其槽位数组初始的容量为16,当槽位数组中的元素个数超过
容量(默认为16) x 加载因子(默认为0.75)
也是12的时候,槽位数组会进行扩容,扩容成32个槽位。这里需要创建一个新的数组,再进行Entry的哈希值的二次取模,在新数组找到新的位置后放入。由于ThreadLocalMap扩容存在性能问题,因此在线程比较多、线程局部变量少的场景下是不是可以转换思路,将ThreadLocal实例变成Key,一个线程一个Map呢?这是可以的,而且免去了ThreadLocalMap高成本、低性能的扩容工作。
-
在JDK 8版本中,ThreadLocal的内部结构发生了演进,虽然还是使用了Map结构,但是Map结构的拥有者已经发生了变化,其拥有者为Thread(线程)实例,每一个Thread实例拥有一个Map实例。另外,Map结构的Key值也发生了变化: 新的Key为ThreadLocal实例。
-
在JDK 8版本中,每一个Thread线程内部都有一个Map(ThreadLocalMap),如果我们给一个Thread创建多个ThreadLocal实例,然后放置本地数据,那么当前线程的ThreadLocalMap中就会有多个Key-Value对,其中ThreadLocal实例为Key,本地数据为Value。
-
新版本的ThreadLocalMap还是由ThreadLocal类维护的,由ThreadLocal负责ThreadLocalMap实例的获取和创建,并从中设置本地值、获取本地值。所以ThreadLocalMap还寄存于ThreadLocal内部,并没有被迁移到Thread内部。
- 每一个线程在获取本地值时,都会将ThreadLocal实例作为Key从自己拥有的ThreadLocalMap中获取值,别的线程无法访问自己的ThreadLocalMap实例,自己也无法访问别人的ThreadLocalMap实例,达到相互隔离,互不干扰。
3.1 变化
- 与早期版本的ThreadLocalMap实现相比,新版本的主要变化为:
- 拥有者发生了变化: 新版本的ThreadLocalMap拥有者为Thread,早期版本的ThreadLocalMap拥有者为ThreadLocal。
- Key发生了变化: 新版本的Key为ThreadLocal实例,早期版本的Key为Thread实例。
- 与早期版本的ThreadLocalMap实现相比,新版本的主要优势为:
- 每个ThreadLocalMap存储的Key-Value对数量变少。早期版本的Key-Value对数量与线程个数强关联,若线程数量多,则ThreadLocalMap存储Key-Value对数量也多。
- 新版本的ThreadLocalMap的Key为ThreadLocal实例,多线程情况下ThreadLocal实例比线程数少。
- 早期版本ThreadLocalMap的拥有者为ThreadLocal,在Thread(线程)实例被销毁后,ThreadLocalMap还是存在的,新版本的ThreadLocalMap的拥有者为Thread,现在当Thread实例被销毁后,ThreadLocalMap也会随之被销毁,在一定程度上能减少内存的消耗。
4. ThreadLocal源码分析
4.1 set(T value) 方法
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null)
// value被绑定到thradlocal上
map.set(this, value);
else
// 不存在就创建
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
通过以上的源码可以看出set(T value)方法的执行流程,大致如下:
- 获得当前线程,然后获得当前线程的ThreadLocalMap成员,暂存于map变量。
- 如果map不为空,就将Value设置到map中,当前的ThreadLocal作为key。
- 如果map为空,为该线程创建map,然后设置第一个Key-Value对,Key为当前的ThreadLocal实例,Value为set方法的参数value值。
4.2 get()方法
- get()方法用于获取“线程本地变量”在当前线程的ThreadLocalMap中对应的值,相当于获取线
程本地值,其核心源码如下:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// 以当前ThreadLocal为Key,尝试获得条目
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
// 条目存在 直接获取
T result = (T)e.value;
return result;
}
}
// 如果当前线程对应map不存在或者map存在,但是当前ThreadLocal实例没有对应的Key-Value对,返回初始值
return setInitialValue();
}
// 设置ThreadLocal关联的初始值并返回
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
通过以上的源码可以看出T get()方法的执行流程,大致如下:
1)先尝试获得当前线程,然后获得当前线程的ThreadLocalMap成员,暂存于map变量。
2)如果获得的map不为空,那么以当前ThreadLocal实例为Key尝试获得map中的Entry(条目)
3)如果Entry条目不为空,就返回Entry中的Value。
4)如果Entry为空,就通过调用initialValue初始化钩子函数获取ThreadLocal初始值,并设置在map中。如果map不存在,还会给当前线程创建新ThreadLocalMap成员,并绑定第一个Key-Value对
4.3 remove()方法
- remove()方法用于在当前线程的ThreadLocalMap中移除
线程本地变量
所对应的值,其核心源码如下:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
4.4 initialValue()方法
- 当线程本地变量在当前线程的ThreadLocalMap中尚未绑定值时,initialValue()方法用于获取初始值。
protected T initialValue() {
return null;
}
-
如果没有调用set()而直接调用get(),就会调用此方法,但是该方法只会被调用一次。
-
默认情况initialValue()方法返回null,如果不想返回null,可以继承ThreadLocal以覆盖此方法。
-
真的需要继承ThreadLocal去重写initialValue()方法吗? 其实没有必要。
- JDK已经定义了一个ThreadLocal的内部SuppliedThreadLocal静态子类
- 提供了ThreadLocal.withInitial(..)静态工厂方法,方便大家在定义ThreadLocal实例时设置初始值回调函数。
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) { return new SuppliedThreadLocal<>(supplier); } static final class SuppliedThreadLocal<T> extends ThreadLocal<T> { private final Supplier<? extends T> supplier; SuppliedThreadLocal(Supplier<? extends T> supplier) { this.supplier = Objects.requireNonNull(supplier); } @Override protected T initialValue() { return supplier.get(); } }
5. ThreadLocalMap 源码分析
5.1 ThreadLocalMap 的主要成员变量
public class ThreadLocal<T> {
static class ThreadLocalMap {
// Map条目初始容量
private static final int INITIAL_CAPACITY = 16;
// Map条目数组
private Entry[] table;
// Map条目数量
private int size = 0;
// 扩容因子
private int threshold; // Default to 0
// Map条目类型,一个静态的内部类,Entry继承子WeakReference,Key为ThreadLocal实例
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocal源码中get()、set()、remove()方法都涉及ThreadLocalMap的方法调用,主要调用了ThreadLocalMap的如下几个方法:
set(ThreadLocal<?>key,Object value)
: 向Map实例设置Key-Value对getEntry(ThreadLocal)
: 从Map实例获取Key(ThreadLocal实例)所属的Entry。remove(ThreadLocal)
: 根据Key(ThreadLocal 实例)
从Map实例移除所属的Entry。
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 根据 key 的 HashCode ,找到key在数组上的槽点i
int i = key.threadLocalHashCode & (len-1);
// 从槽点i开始向后循环搜索,找空余槽点(空余位置)或者找现有槽点
// 如果没有现有槽点,则必定有空余槽点,因为没有空间时会扩容
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 找到现有槽点: Key值为ThreadLocal实例
if (k == key) {
e.value = value;
return;
}
// 找到异常槽点: 槽点被GC掉,重设Key值和Value值
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 没有找到现有的槽点,增加新的Entry
tab[i] = new Entry(key, value);
// 设置ThreadLocal数量
int sz = ++size;
// 清理 Key 为 null 的无效Entry
// 没有可清理的Entry,并且现有条目数量大于扩容因子值,进行扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
5.2 Entry 的 Key 需要使用弱引用
- Entry用于保存ThreadLocalMap的Key-Value条目,但是Entry使用了对Threadlocal实例进行包装之后的弱引用(WeakReference)作为Key
// Entry继承了WeakReference,并使用WeakReference对Key进行包装
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); // 使用WeakReference对Key值进行包装
value = v;
}
}
为什么Entry需要使用
弱引用
对Key进行包装,而不是直接使用Threadlocal实例作为Key呢。这里从一个简单的例子入手,假设有一个方法funcA()创建了一个线程本地变量
public void funcA(){
//创建一个线程本地变量
ThreadLocal local=new ThreadLocal<Integer>();
//设置值
local.set(100);
//获取值
local.get();
//函数末尾
}
- 当线程tn执行funcA()方法到其末尾时,线程tn相关的JVM栈内存以及内部ThreadLocalMap成员的结构如图。
-
线程tn调用funcA()方法新建了一个ThreadLocal实例,使用local局部变量指向这个实例,并且此local是
强引用
-
在调用local.set(100)之后,线程tn的ThreadLocalMap成员内部会新建一个Entry实例,其Key以弱引用包装的方式指向ThreadLocal实例。
-
当线程tn执行完funcA()方法后,funcA()的方法栈帧将被销毁,强引用local的值也就没有了,但此时线程的ThreadLocalMap中对应的Entry的Key引用还指向了ThreadLocal实例。如果Entry的Key引用是强引用,就会导致Key引用指向的ThreadLocal实例及其Value值都不能被GC回收,这将造成严重的内存泄漏。
什么是弱引用? 弱引用(WeakReference)指向的对象只能生存到下一次垃圾回收之前。换句话说,当GC发生时,无论内存够不够,仅有弱引用所指向的对象都会被回收。而拥有强引用指向的对象则不会被直接回收。
- 由于ThreadLocalMap中Entry的Key使用了弱引用,在下次GC发生时,就可以使那些没有被其他强引用指向、仅被Entry的Key所指向的ThreadLocal实例能被顺利回收。
- 在Entry的Key引用被回收之后,其Entry的Key值变为null。后续当ThreadLocal的get()、set()或remove()被调用时,ThreadLocalMap的内部代码会清除这些Key为null的Entry,从而完成相应的内存释放。
使用ThreadLocal会
发生内存泄漏
的前提条件如下
- 线程长时间运行而没有被销毁。线程池中的Thread实例很容易满足此条件。
- ThreadLocal引用被设置为null,且后续在同一Thread实例的执行期间,没有发生对其他ThreadLocal实例的get()、set()或remove()操作。只要存在一个针对任何ThreadLocal实例的get()、set()或remove()操作,就会触发Thread实例拥有的ThreadLocalMap的Key为null的Entry清理工作,释放掉ThreadLocal弱引用为null的Entry。
综合以上两点可以看出: 使用ThreadLocal出现内存泄漏还是比较容易的。
5.3 避免ThreadLocal内存泄漏
-
编程规范推荐使用
static final
修饰 ThreadLocal 对象,ThreadLocal实例作为ThreadLocalMap的Key,针对一个线程内所有操作是共享的,所以建议设置static修饰符,以便被所有的对象共享。由于静态变量会在类第一次被使用时装载,只会分配一次存储空间,此类的所有实例都会共享这个存储空间,所以使用static修饰ThreadLocal就会节约内存空间。 -
为了确保ThreadLocal实例的唯一性,除了使用static修饰之外,还会使用final进行加强修饰,以防止其在使用过程中发生动态变更
// 推荐使用static final线程本地变量 private statie final ThreadLocal<Foo> LOCAL_F00 = new ThreadLocal<Foo>();
-
使用static、final修饰ThreadLocal实例也会带来副作用,使得Thread实例内部的ThreadLocalMap中Entry的Key在Thread实例的生命期内将始终保持为非null,从而导致Key所在的Entry不会被自动清空,这就会让Entry中的Value指向的对象一直存在强引用,于是Value指向的对象在线程生命期内不会被释放,最终导致内存泄漏。所以,在使用完static、final修饰ThreadLocal实例,使用完后必须使用remove()进行手动释放。
-
如果使用线程池,可以定制线程池的afterExecute()方法(任务执行完成之后的钩子方法),在任务执行完成之后,调用ThreadLocal实例的remove()方法对其手动释放,从而使得其线程内部的Entry得到释放
// 线程本地变量,用于记录线程异步任务的开始执行时间
private static final Threadlocal<Long> START_TIME= new Threadlocal<>();
ExecutorService pool = new ThreadPoolExecutor(
2,4, 60,TimeUnit.SECONDS,newLinkedBlockingQueue<>(2)){
// 异步任务执行完成之后的钩子方法
@Override
protected void afterExecute(Runnable target, Throwable t) {
// 清空ThreadLocal实例的本地值
START_TIME.remove();
}
};
6. ThreadLocal注意事项
由于ThreadLocal使用不当会导致严重的内存泄漏问题,所以为了更好地避免内存泄漏问题的发生,我们使用ThreadLocal时遵守以下两个原则:
- 尽量使用private static final修饰ThreadLocal实例。使用 private与final修饰符主要是尽可能不让他人修改、变更ThreadLocal变量的引用,使用static修饰符主要为了确保ThreadLocal实例的全局唯一。
- ThreadLocal使用完成之后务必调用remove()方法。这是简单、有效地避免ThreadLocal引发内存泄漏的方法。