youyichannel

志于道,据于德,依于仁,游于艺!

0%

ThreadLocal详解

ThreadLocal概述

通常情况下,在程序中创建的变量是可以被任何一个线程访问并修改的。那么如果想实现每一个线程都有自己的专属的本地变量该如何解决?

JDK原生的java.lang.ThreadLocal类作用就是在多线程中为每一个线程创建单独的变量副本,相当于线程单独的private static类型变量。

ThreadLocal的作用和同步机制:

  • ThreadLocal是为了保证多线程环境下数据的独立性
  • 同步机制是为了保证多线程环境下数据的一致性

ThreadLocal的使用

在《阿里巴巴Java开发手册》中提到了可以使用ThreadLocal类来在多线程环境下使用SimpleDateFormat

public class DateUtils {

public static final ThreadLocal<DateFormat> DF = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

// ...
}

在需要用到DateFormat对象的地方,调用:

DateUtils.DF.get().format(new Date());

ThreadLocal原理

先从Thread源码入手:

public class Thread implements Runnable {

// ...

/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

// ...
}

可以看出Thread类源码中存在变量threadLocalsinheritableThreadLocals,它们都是ThreadLocal.ThreadLocalMap (ThreadLocal类定制化的HashMap,和HashMap功能相似)类型的变量。这两个变量的初始值都是null,只有当前线程调用TheadLocalget()方法或者set()方法时才会去创建这两个变量。事实上,调用ThreadLocal#set()或者是ThreadLocal#get()方法本质上都是调用了ThreadLocalMap#get()ThreadLocalMap#set()方法。

ThreadLocal实现线程隔离的功能,主要也是用到了ThreadLocal类中的变量threadLocals,它负责存储对当前线程的独有对象。

ThreadLocal#set()

public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取Thread类内部的threadLocals变量(哈希表结构)
ThreadLocalMap map = getMap(t);
if (map != null) {
// 将需要存储的值放入map
map.set(this, value);
} else {
// 初始化map,并存储value
createMap(t, value);
}
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

上述代码可以得出结论:最终的变量存放在当前线程的TheadLocalMap中,并不是存储在ThreadLocal上,ThreadLocal可以理解为只是ThreadLocalMap的封装,传递了变量值。

每个线程Thread都具备一个ThreadLocalMapThreadLocalMap中可以存储ThreadLocal: Object的键值对。

/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// ...
}

假设我们在同一个线程中声明了多个ThreadLocal对象,Thread内部都是使用仅有的ThreadLocalMap存放数据的,数据格式是ThreadLocal: Object的键值对。

ThreadLocal#get()

public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取Thread类内部的threadLocals变量(哈希表结构)
ThreadLocalMap map = getMap(t);
// map不空
if (map != null) {
// 获取threadLocalMap中存储的键值对
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// map为空,初始化,结果是 null: null
return setInitialValue();
}

ThreadLocal#remove()

public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}

private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}

remove()方法直接将ThreadLocal对应的值从当前线程Thread的ThreadLocalMap中删除,这涉及到内存泄露的问题。实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉,这个问题我们后面再谈。

ThreadLocal内存泄露问题分析

ThreadLocalMap中使用的Entry键key是ThreadLocal的强引用,而值value是强引用。

static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

因此,如果ThreadLocal没有被外部强引用的情况下,在垃圾回收的时候,key会被清理掉,但是value不会被清理掉。如此这般,ThreadLocalMap中就会出现key为null的Entry,如果我们不做任何措施,value永远都无法被GC,这个时候就会造成内存泄露。

ThreadLocalMap实现的时候就已经考虑到了这种情况,在调用 set()get()remove() 方法的时候,会清理掉 key 为 null 的记录。在开发中,尽量使用完 ThreadLocal方法后手动调用remove()方法。

=> ThreadLocal内存泄露的根源:由于ThreadLocalMap的生命周期和Thread一样长,对于重复利用的线程来说,如果没有手动删除对应key就会导致key为null的Entry对象越来越多,从而导致内存泄露。

为什么不将ThreadLocalMap中的Key设置为强引用

如果Key是强引用

如果key设计成强引用且没有手动remove(),那么key会和value一样伴随线程的整个生命周期。

假设在代码中使用完ThreadLocal, ThreadLocal的引用被回收了,但是因为ThreadLocalMap的Entry强引用了ThreadLocal造成ThreadLocal无法被回收。在没有手动删除Entry并且CurrentThread依然运行的前提下, 始终有强引用链CurrentThread Ref → CurrentThread →Map(ThreadLocalMap)-> entry, Entry就不会被回收,包括了ThreadLocal实例和value,导致Entry内存泄漏

=> 如果ThreadLocalMap中的key使用了强引用, 是无法完全避免内存泄漏的

Key设计成弱引用的原因

在调用ThreadLocalMap的 set()get()remove() 方法时,会清理掉 key 为 null 的记录,这就意味着在使用threadLocal时,即使CurrentThread依然运行,忘记手动调用remove()方法,弱引用比强引用可以多一层保障:弱引用的 ThreadLocal 会被回收,对应的值value会在下一次调用ThreadLocalMap的 set()get()remove() 方法时被清理。

正确使用ThreadLocal

1)将ThreadLocal变量使用private static修饰,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后手动remove()它,防止内存泄露。

2)每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

参考文章

  • https://juejin.cn/post/6844903487193481224
  • https://pdai.tech/md/java/thread/java-thread-x-threadlocal.html
  • https://javaguide.cn/java/concurrent/java-concurrent-questions-03.html