ThreadLocal 实现原理是什么?为什么要使用弱引用?
# ThreadLocal 实现原理是什么?为什么要使用弱引用?
ThreadLocal可以问的点有很多,比如:ThreadLocal解决什么问题?底层结构是什么?实现原理是什么?有什么应用场景?为什么要使用弱引用?子线程可以从父线程继承 ThreadLocal 吗?ThreadLocal使用不当会有内存泄漏是怎么回事?正确的使用姿势是?Netty 的FastThreadLocal fast在哪里?
下面我们一个个来看下这些问题。
# ThreadLocal解决什么问题?
通常情况下,我们创建的变量任何线程来读取读到的都是同一个值,如果想实现每一个线程都有自己的专属值该如何解决呢
?JDK中提供的ThreadLocal类正是为了解决这样的问题。
比如SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本
# ThreadLocal底层是什么数据结构?
实际上是Map,Key为ThreadLocal变量、value为值。
我们先看下Thread类的源码
public class Thread implements Runnable {
......
//与此线程有关的ThreadLocal值。由ThreadLocal类维护
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal,自父线程集成而来的ThreadLocalMap,
* 主要用于父子线程间ThreadLocal变量的传递
* 本文主要讨论的就是这个ThreadLocalMap
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
......
}
2
3
4
5
6
7
8
9
10
11
12
13
我们可以把 ThreadLocal.ThreadLocalMap 理解为ThreadLocal 类实现的定制化的 HashMap. 我们再来看下 ThreadLocal 类的源码
//调用Thread.set 实际上是往当前线程的 ThreadLocalMap 里面put一个键值对
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.getMap 可以获取到当前线程的 ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
//ThreadLocalMap可以存储以ThreadLocal为key ,Object 对象为 value的键值对。
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
......
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# ThreadLocal有什么应用场景?
- 非线程安全的工具类,需要每个线程持有一个副本,比如 SimpleDateFormat
- 异步传递traceId
# 为什么要使用弱引用?
ThreadLocalMap的内部类Entry被设计为实现了WeakReference,Entry用来存放数据。
弱引用简单理解就是当垃圾回收时,该对象只被WeakReference对象的弱引用字段所引用,而未被任何强类型的对象引用,那么,该弱引用的对象就会被回收。
注意:WeakReference引用本身是强引用,它内部的(T reference)才是真正的弱引用字段,WeakReference就是一个装弱引用的容器而已。
那 为什么要使用弱引用呢?
这是因为:ThreadLocalMap本身并没有为外界提供取出和存放数据的API,我们所能获得数据的方式只有通过ThreadLocal类提供的API来间接的从ThreadLocalMap取出数据,所以,当我们用不了key(ThreadLocal对象)的API也就无法从ThreadLocalMap里取出指定的数据。
一般我们new 一个ThreadLocal对象的时候,它一定会有强引用,在ThreadLocalMap中也一定会有它的弱引用
当强引用不在的时候一定是我们的程序不再需要这个ThreadLocal对象了 为什么这么说?
比如我定义了一个 ThreadLocal 变量 formatter,formatter 对 ThreadLocal 变量的强引用关系不存在的一个case 是getDate()方法执行完了,那么当然ThreadLocal 变量是可以回收的。
public void getDate(){
ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));
......
}
2
3
4
5
6
# 子线程可以从父线程继承 ThreadLocal 吗?
ThreadLocal 不可以。InheritableThreadLocals 是可以的,它重写了ThreadLocal的三个方法。childValue,createMap,getMap。
InheritableThreadLocal 不能和线程池搭配使用
因为线程池中的线程是复用的,并没有重新初始化线程,InheritableThreadLocal之所以起作用是因为在Thread类中最终会调用init()方法去把InheritableThreadLocal的map复制到子线程中。
由于线程池复用了已有线程,所以没有调用init()方法这个过程,也就不能将父线程中的InheritableThreadLocal值传给子线程。
# ThreadLocal使用不当会有内存泄漏是怎么回事?
ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。
所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。
这样一来,ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。
# 使用 ThreadLocal 如何防止内存泄漏 ?
使用完 ThreadLocal 手动调用remove方法。 看下 ThreadLocal 的 remove() 会清理 ThreadLocalMap 中 key 为 null的键值对。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
2
3
4
5
ThreadLocal 的 get() 有清除操作,那我们是不是不用手动调用 remove() 了?
不是的。因为 get() 中清除操作 只会检测本次get的 ThreadLocal 变量是否需要清理 。而 remove() 会检测 ThreadLocalMap 中的所有键值对。
所以,手动调用remove() 才是最保险的。
# Netty 的FastThreadLocal fast在哪里?
既然jdk已经有ThreadLocal,为何netty还要自己造个FastThreadLocal?FastThreadLocal快在哪里?
这需要从jdk ThreadLocal的本身说起。如下图:
在java线程中,每个线程都有一个ThreadLocalMap实例变量(如果不使用ThreadLocal,不会创建这个Map,一个线程第一次访问某个ThreadLocal变量时,才会创建)。该Map是使用线性探测的方式解决hash冲突的问题,如果没有找到空闲的slot,就不断往后尝试,直到找到一个空闲的位置,插入entry,这种方式在经常遇到hash冲突时,影响效率。
FastThreadLocal(下文简称ftl)直接使用数组避免了hash冲突的发生,具体做法是:每一个FastThreadLocal实例创建时,分配一个下标index;分配index使用AtomicInteger实现,每个FastThreadLocal都能获取到一个不重复的下标。当调用ftl.get()方法获取值时,直接从数组获取返回,如return array[index],如下图:
FastThreadLocal 底层结构代码
static final ThreadLocal<InternalThreadLocalMap> slowThreadLocalMap = new ThreadLocal<InternalThreadLocalMap>();
static final AtomicInteger nextIndex = new AtomicInteger();
Object[] indexedVariables;
2
3
# FastThreadLocal 性能 比 ThreadLocal 高多少?
远高于。
FastThreadLocal 对 ThreadLocal 的优化点在于,将元素放入 ThreadLocalMap 采用数组结构随机访问代替 原来的 线性探测。
所以我们测试场景为:单线程访问多 FastThreadLocal/ThreadLocal 变量:
/**
* 单线程访问多个ThreadLocal
*/
public static void testThreadLocalWithMultipleThreadLocal() {
ThreadLocal<String> threadLocal[] = new ThreadLocal[count];
for (int i = 0; i < count; i++) {
threadLocal[i] = new ThreadLocal<String>();
}
new Thread(new Runnable() {
@Override
public void run() {
long start = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
threadLocal[i].set("value" + i);
}
long middle = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
for (int j = 0; j < count; j++) {
threadLocal[i].get();
}
}
long end = System.currentTimeMillis();
System.out.println("testThreadLocalWithMultipleThreadLocal set:" + (middle - start) + ",get:" + (end - middle));
}
}).start();
}
/**
* 单线程访问多个FastThreadLocal
*/
public static void testFastThreadLocalWithMultipleFastThreadLocal() {
FastThreadLocal<String> threadLocal[] = new FastThreadLocal[count];
for (int i = 0; i < count; i++) {
threadLocal[i] = new FastThreadLocal<String>();
}
new FastThreadLocalThread(new Runnable() {
@Override
public void run() {
long start = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
threadLocal[i].set("value" + i);
}
long middle = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
for (int j = 0; j < count; j++) {
threadLocal[i].get();
}
}
long end = System.currentTimeMillis();
System.out.println("testFastThreadLocalWithMultipleFastThreadLocal set:" + (middle - start) + ",get:" + (end - middle));
}
}).start();
}
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
输出:
testThreadLocalWithMultipleThreadLocal set:68,get:21492
testFastThreadLocalWithMultipleFastThreadLocal set:61,get:8
2
3
4
有结果可知,FastThreadLocal 性能远高于 ThreadLocal。