贫瘠之地

华北无浪漫,死海扬起帆
多少个夜晚,独自望着天

0%

ThreadLocal & Memory Leak

基本使用

在项目中我们可以通过 ThreadLocal 来存储用户信息

其中一般会在过滤器/拦截器的入口处初始化用户信息,并在执行结束后对其进行清理

这样从请求进来一直到返回,我们只需要通过线程变量 ThreadLocal 获取用户信息即可,而不用每次都从数据库查出来

因为 ThreadLocal 是线程安全的,所以通常声明为一个静态单例变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class UserHolder {

private final static ThreadLocal<UserInfo> CURRENT_USER = new ThreadLocal<>();

public static UserInfo get() {
return CURRENT_USER.get();
}

public static void set(UserInfo userInfo) {
CURRENT_USER.set(userInfo);
}

public static void remove() {
CURRENT_USER.remove();
}
}

就可以在拦截器中通过 set() 方法存储鉴权成功的用户数据,在业务逻辑中通过 get() 获取用户数据了

实现原理

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own,independently initialized copy of the variable

构造方法

构造方法

ThreadLocal 仅存在一个无参构造方法

1
2
public ThreadLocal() {
}

设置值

set()

使用 set() 方法向其赋值

1
2
3
4
5
6
7
8
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

首先使用 Thread.currentThread() 获取当前线程;Thread.currentThread() 是一个 native 方法

ThreadLocalMap

接下来调用了 getMap(Thread t) 方法

1
2
3
4
5
6
7
8
9
/**
* Get the map associated with a ThreadLocal. Overridden in InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

返回的是线程对象的一个线程变量 ThreadLocal.ThreadLocalMap threadLocals = null,初始的默认值是 null,返回后回到 set()

当拿到的默认值是 null 时,则会创建一个 map 将值放入 map 中;不为 null 时将值直接放入 map

ThreadLocalMap 是线程安全的,除此之外在这里可以先视作 HashMap,也是基于散列存储的 Map

createMap()

当第一次使用 ThreadLocalMap 时,需要调用 createMap() 进行创建

createMap() 内调用了 ThreadLocalMap 的构造方法,把自己线程对象作为参数传了进去

1
2
3
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
1
2
3
4
5
6
7
8
9
private static final int INITIAL_CAPACITY = 16;

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}

除去创建 Entry 数组对象和重置负载因子的操作,本质上将 KV 根据线程对象的 threadLocalHashCode 放到了相应的数组位置上,K 就是线程对象,V 是 ThreadLocal 要存储的值

所以需要注意:

  • ThreadLocalMap 中的 K 是线程对象,V 是要存储的值
  • 所有的线程都在使用同一个 ThreadLocalMap 对象,K 是各自的线程对象

弱引用

1
2
3
4
5
6
7
8
9
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

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

ThreadLocal.ThreadLocalMap 内存储的是 Entry[]

Entry 的 key 是一个弱引用,而 value 为强引用

取值

get()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

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;
}

protected T initialValue() {
return null;
}

获取的方法,首先获取当前线程拥有的 ThreadLocalMap,然后将自己对象作为 K 进行取值

如果 ThreadLocalMap 不存在,则进行初始化 setInitialValue(),初始化结束后返回 null

所以也可以通过继承 ThreadLocal 后重写 initialValue() 来设置默认的返回值

删除值

remove()

1
2
3
4
5
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}

先获取线程对象中保存的 ThreadLocalMap,如果不为 null 则调用 remove()

ThreadLocalMap 的 remove()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}
}
}

简而言之就是以线程对象作为 K 对 V 进行删除

总结

比较反直觉的是,操作 ThreadLocal 对象,但数据并不存储在 ThrealLocal,而是存储在线程对象的 ThreadLocal.ThreadLocalMap

  • ThreadLocal 更像是一个工具类,用来操作 ThreadLocal.ThreadLocalMap
  • ThreadLocal.ThreadLocalMap 内存储 Entry[],因为一个线程可能使用了多个 ThreadLocal
  • ThreadLocal 对象被作为 key 进行使用
  • ThreadLocal.ThreadLocalMap 的 key 是弱引用,弱引用的目的是为了便于对 ThreadLocal 对象本身进行回收

内存泄漏

ThreadLocal 整个使用过程中会创建出哪些对象:

  • ThreadLocal 对象
  • ThreadLocal.ThreadLocalMap 对象,在 Thread 对象中被持有
  • ThreadLocal.ThreadLocalMap 中的 Entry 对象
  • ThreadLocal.ThreadLocalMap 中的 Entry 对象中的 K 和 V

ThreadLocal

ThreadLocal 对象一般不存在泄漏问题:

  • ThreadLocal 是操作 ThreadLocal.ThreadLocalMap 的 key,实例化数量不多
  • Entry 弱引用的设计就是为了及时回收 ThreadLocal 对象

ThreadLocal.ThreadLocalMap

基本也不会存在泄漏问题,该对象被 Thread 对象持有,并且只是一个 Map 结构的引用

Entry

Entry 本身是一个引用对象,其通过 ThreadLocal.ThreadLocalMap 的操作方法进行释放,例如 setremove 中的 replaceStaleEntryexpungeStaleEntry 方法

Entry 对象的 key 即为 ThreadLocal 对象,上面已经提到了因为弱引用的设计一般会被及时回收

Entry 对象的 value 为强引用,它只会因为 Entry 对象的回收而被回收,这是最有可能发生内存泄漏的地方,可能存在作为 key 的 ThreadLocal 对象已经被回收,但是 value 无法回收的情况

当然在设计中,ThreadLocal.ThreadLocalMap 的一些操作会检查整个 Map(replaceStaleEntryexpungeStaleEntry),从而对 key 已经回收的 Entry 进行释放,避免内存泄漏

总结

上述可见,Entry 时最有可能会造成内存泄漏的地方

  • 没有手动调用 remove 方法,同时 ThreadLocal 无论是否回收,value 都可能在一定时间内、或者一直无法被回收
  • 错误地创建 Thread 对象,而没有对旧的线程对象进行回收
  • 这里有一个 Tomcat 机制相关的问题从而导致泄漏,我并没看懂... java - ThreadLocal & Memory Leak - Stack Overflow

参考

为什么ThreadLocal是线程安全的? - 掘金 (juejin.cn)

Tomcat线程复用与Threadlocal引发的惨案_线程复用 threadlocal_小沈同学呀的博客-CSDN博客

java - ThreadLocal & Memory Leak - Stack Overflow