0%

ThreadLocal 的功能、用法和注意事项

面试分享~No.3

要解决的问题

在多线程的场景下,使用共享变量,线程无法隔离单独的使用这个变量

threadlocal可以让每个线程独享一份共享变量,实现共享变量的隔离

例子

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
public class ThreadLocalTest {

private static final ThreadLocal<String> threadLocal1 = new ThreadLocal<>();

private static final ThreadLocal<String> threadLocal2 = new ThreadLocal<>();

public static void main(String[] args) {
new Thread(() -> {
threadLocal1.set("local1 A");
threadLocal2.set("local2 A");
System.out.println(threadLocal1.get());
System.out.println(threadLocal2.get());
threadLocal1.remove();
threadLocal2.remove();
}).start();

new Thread(() -> {
threadLocal1.set("local1 B");
threadLocal2.set("local2 B");
System.out.println(threadLocal1.get());
System.out.println(threadLocal2.get());
threadLocal1.remove();
threadLocal2.remove();
}).start();
}

}

源码分析

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

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

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

ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

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

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

ThreadLocalMap

1
2
3
4
5
6
7
8
9
10
11
12
static class ThreadLocalMap {

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

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

为什么Entry使用弱引用类型?

Thread 与 ThreadLocal、ThreadLocalMap的关系

JDK8之前的设计

image-20221105202929562

JDK8的设计

image-20221105203046168

JDK8的设计说明:

  1. 每个Thread对象中有一个ThreadLocalMap属性
  2. ThreadLocalMap是一个字典,key是ThreadLocal类型,value是要保存的变量
  3. Thread中没有提供操作ThreadLocalMap的方法,而是通过ThreadLocal操作的

问题:

  1. JDK8之前的设计存在什么问题

    答:个人理解,Threadlocal 是为了将 Thread 和 value 进行关联,那么 value 的生命周期应该与 Thread 保持一致,当 Thread 销毁了,value 当然就不需要了。在JDK8之前,thread 是 map 的 key,当 thread 销毁时,value 依旧是存在的,在 JDK8 之后,value 在 thread 内的 threadLocalMap 中,当 thread 销毁后,value 会自动销毁。另外,JDK8 之前如果 threadlocal 对象销毁,则其下的所有 thread 和 value 的关系都不在存在了,但是 thread 对象可能依旧是存在的,这也是不合适的

  2. ThreadLocalMap做为Thread的属性,为什么不通过Thread对象来操作

    答:threadLocalMap 的 key 必须是 threadLocal 对象,如果允许通过 thread 的方法操作,可能传入其他对象作为 key,破坏了设计思想

应用场景

  • 存储全局信息,如用户Session
  • 处理数据库事务
  • 代替参数显式传递

ThreadLocal中内存泄漏的问题

内存泄露:指程序中动态分配的堆内存由于某种原因没有被释放或者无法释放,造成系统内存的浪费

弱引用:执行垃圾回收时,不管符不符合回收条件,都会回收

实验

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
29
30
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadLocalTest {
static class LocalVariable {
// 构造5M大小的数组
private byte[] arr = new byte[1024 * 1024 * 5];
}

private final static ThreadPoolExecutor executor =
new ThreadPoolExecutor(6, 6, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>());

static ThreadLocal<LocalVariable> tl = new ThreadLocal<>();

public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
executor.execute(() -> {
LocalVariable localVariable = new LocalVariable();
ThreadLocalTest.tl.set(localVariable);
System.out.println("thread name end: " + Thread.currentThread().getName() + ", value: " +
ThreadLocalTest.tl.get());
// ThreadLocalTest.tl.remove();
});
Thread.sleep(1000);
}
Thread.sleep(1000 * 3600);
}

}

执行GC,存在30M内存泄漏

image-20221106020255849

将threadlocal remove掉,GC后没有内存泄漏

1
ThreadLocalTest.tl.remove();
image-20221106021947237

引用关系分析

image-20221106135245970

Threadlocal 会产生内存泄漏吗?

前提:启动一个线程执行很多个计算任务,线程一直工作不会被销毁

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的引用有:

  1. 创建threadlocal对象的强引用 tl
  2. threadLocalMap中的弱引用key

object 的引用:

  1. threadlocalMap中的强引用value

当tl=null时,threadlocal可以被回收;当value=null时,object可以被回收

讨论

假设我们线程池中有100个线程,和一个 threadLocal 对象,我们用这100个线程执行100个数据任务对象(10M),并将数据任务通过 threadLocal set 到线程内的 threadLocalMap 中,这样每个线程中,都有一个 map,map 中有一个长度为16的数组,数组中有一个元素,即数据任务。执行完所有数据任务后,假设这100个线程都是核心线程,线程池会保持线程,这时,数据任务是有强引用指向的,即 Entry 的 value,不会被回收

有说法称 threadlocal 可能导致内存泄露,这个说法是不对的,顶多可以说是业务上的"内存泄漏",因为在业务层面我们是不需要这份数据的,但这份数据不符合GC的条件,不会被回收,所以需要我们手动清理

这种内存泄露会随着线程数量增大而增大,与线程执行次数无关

Entry 的 key 为什么用弱引用

threadlocal 对象有两个引用,一个是强引用 tl,Entry 中用弱引用指向了 threadlocal,如果代码中令tl=null,则 threadlocal 对象可以在 GC 时被回收

总结

本文介绍了threadlocal的使用场景,用法,注意事项:threadlocal使用后记得remove

参考

ThreadLocal原理及使用场景

ThreadLocal内存泄漏分析