ThreadLocal 的作用和实现原理

ThreadLocal 简介

这个类提供一个线程内部变量。通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。而使用 ThreadLocal 创建的变量只能被当前线程访问,其他线程则无法访问和修改。

在日常开发中用到 ThreadLocal 的地方比较少,但是在某些特殊的场景下,通过 ThreadLocal 可以轻松地实现一些看起来很复杂的功能,这一点在 Android 的源码中也有所体现,比如 Looper、ActivityThread 以及 AMS 中都用到了 ThreadLocal。一般来说,当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用 ThreadLocal。

ThreadLocal 使用

创建,支持泛型

1
ThreadLocal<String> mStringThreadLocal = new ThreadLocal<String>();

set 方法

1
mStringThreadLocal.set("bleedyao.com");

get 方法

1
mStringThreadLocal.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
29
30
public static void main(String[] args) {
ThreadLocal<String> mStringThreadLocal = new ThreadLocal<String>() {
@Override
protected String initialValue() {
return Thread.currentThread().getName();
}
};
Thread t = new Thread("#Thread-0") {
@Override
public void run() {
super.run();
mStringThreadLocal.set("bleedyao.com");
System.out.println(Thread.currentThread().getName()+" "+mStringThreadLocal.get());
}
};
Thread t1 = new Thread("#Thread-1") {
@Override
public void run() {
super.run();
mStringThreadLocal.set("bleedyao.xyz");
System.out.println(Thread.currentThread().getName()+" "+mStringThreadLocal.get());
}
};
t.start();
t1.start();
System.out.println(Thread.currentThread().getName()+" "+mStringThreadLocal.get());
}

输入结果:

1
2
3
#Thread-0 bleedyao.com
#Thread-1 bleedyao.xyz
main main

ThreadLocal 工作原理

从上面日志可以看出,虽然在不同线程访问的是同一个 ThreadLocal 对象,但是它们通过 ThreadLocal 获取到的值却是不一样的。

下面分析 ThreadLocal 的内容实现,ThreadLocal 是一个泛型类,它的定义为 public class ThreadLocal<T>,只要搞清楚 ThreadLocal 的 get 和 set 方法就可以明白它的工作原理。

首先是 ThreadLocal 的 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);
}

此方法中,首先通过 getMap 拿到当前线程中的维护的 ThreadLocalMap,如果 map 为空,就创建一个 map,否则将值设置到 map 中。

ThreadLocalMap 是 ThreadLocal 的内部类,Thread 内部维护了这样一个 ThreadLocal.ThreadLocalMapThreadLocalMap 内部维护了一个弱引用类型的 Entry 这样的键值对,其键为 ThreadLocal 对象,值是 Object 类型。

创建一个 map 实际上就是 new 一个 ThreadLocalMap 这个没什么好说的;map.set() 设置值的逻辑是如果 键ThreadLocal 出现相同的引用,那么覆盖值,否则添加一个新的 Entry 对象保存 ThreadLocal 和 value 这一键值对。

源码如下:

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
private void set(ThreadLocal<?> key, Object value) {
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)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

下面分析 get 方法

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

get 方法同样要拿出 map,如果为空的话调用 setInitialValue(),这个方法是用来设置默认值的。如果 map 不为空,获取 ThreadLocalMap.Entry 对象,如果 Entry 对象不为空,获取它的值并将其返回。其中注解的意思是警告可能会出现 ClassCastException,但实际上不会出现的,是提供多虑了。

关于 setInitialValue(),我们可以在创建 ThreadLocal 的时候重写这个方法用于设置默认值。

1
2
3
4
5
6
ThreadLocal<String> mStringThreadLocal = new ThreadLocal<String>() {
@Override
protected String initialValue() {
return Thread.currentThread().getName();
}
};

使用场景

  • 实现单个线程单例以及单个线程上下文信息存储,比如交易 id 等。
  • 实现线程安全,非线程安全的对象使用 ThreadLocal 之后就会变得线程安全,因为每个线程都会有一个对应的实例。
  • 承载一些线程相关的数据,避免在方法中来回传递参数。

注意事项

慎用 ThreadLocal ,它是 Java 提供的一种保存线程私有信息的机制,因为其在整个线程周期内有效,所以可以方便地在一个线程关联的不同业务模块之间传递信息,比如事务id,Cookie 等上下文相关信息。

ThreadLocalMap 内部条目是弱引用,当 Key 为 null 时,改条目就变成「废弃条目」(可见上文源码 replaceStaleEntry(key, value, i);),相关「value」的回收,往往依赖于几个关键点,即 set、remove、rehash。

通常弱引用会和引用队列配合清理机制使用,但是 ThreadLocal 是个例外,它并没有这么做。

这意味着,废弃项目的回收依赖于显示地触发,否则就要等待线程结束,进而回收相应 ThreadLocalMap!这就是很多 OOM 的来源,所以通常会建议,应用一定要自己负责 remove,并且不要和线程池使用,应为工作(worker)线程往往不会退出的。

坚持原创技术分享,您的支持将鼓励我继续创作!