注:此文源码摘自 sun jdk 1.8
ThreadLocal 是什么
打开 ThreadLocal 的源码我们可以看到如下的注释:
大致翻译如下:
该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。
例如,以下类生成对每个线程唯一的局部标识符。 线程 ID 是在第一次调用 UniqueThreadIdGenerator.getCurrentThreadId() 时分配的,在后续调用中不会更改。
import java.util.concurrent.atomic.AtomicInteger;
public class UniqueThreadIdGenerator {
private static final AtomicInteger uniqueId = new AtomicInteger(0);
private static final ThreadLocal < Integer > uniqueNum =
new ThreadLocal < Integer > () {
@Override protected Integer initialValue() {
return uniqueId.getAndIncrement();
}
};
public static int getCurrentThreadId() {
return uniqueId.get();
}
} // UniqueThreadIdGenerator
每个线程都保持对其线程局部变量副本的隐式引用,只要线程是活动的并且 ThreadLocal 实例是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。
简单而言,ThreadLocal
是一个线程自身内部的数据存储类,其它线程是无法访问该线程的数据的。这样说起来还是挺抽象的,我们下面来介绍一个例子。
ThreadLocal 的使用
代码如下:
public class Test {
private static final ThreadLocal<String> threadLocal = new ThreadLocal<String>() {
@Override
protected String initialValue() {
return "default";
}
};
public static void main(String[] args) {
threadLocal.set("thread#0");
System.out.println("thread#0 " + threadLocal.get());
new Thread("thread#1") {
@Override
public void run() {
System.out.println(currentThread().getName() + " " + threadLocal.get());
}
}.start();
new Thread("thread#2") {
@Override
public void run() {
threadLocal.set("thread#2");
System.out.println(currentThread().getName() + " " + threadLocal.get());
}
}.start();
}
}
我们首先创建一个 ThreadLocal<String>
对象,并重写它的 initialValue()
方法,这样它默认情况下就是返回一个字符串为 default
的字段。然后我们再 main()
方法里面创建两个线程,线程 thread#1
和 thread#2
,当然,加上它本身,一共是三个线程,我们分别在主线程调用 set("thread#0")
;thread#1
线程不调用 set()
方法;thread#2
调用 set("thread#2")
方法。我们发现,输出结果如下:
对于主线程,由于我们调用了 set("thread#0")
方法,所以 threadLocal.get()
的值就是字符串 thread#0
;对于 thread#1
线程没有调用 set()
方法,故 threadLocal.get()
返回的就是默认值 default
字符串;对于 thread#2
线程,我们调用了 set("thread#2")
方法,故 threadLoca.get()
返回的就是字符串 thread#2
。
所以我们可以发现,即使是同一个 ThreadLocal
对象,我们的 set()
或 get()
方法都是仅对当前线程可见的,各个线程之间不可见不可相互影响。
ThreadLocal 源码解析
要想搞清楚 ThreadLocal
为什么会有这样的特性,我们其实只需要搞清楚我们上面所使用到的 set()
和 get()
方法即可。set()
方法源码如下:
第一行代码不解释了。我们看到第二行代码可以获取到一个 ThreadLocalMap,我们不妨戳进 getMap()
方法里面看一下,ThreadLocalMap
是如何和 Thread
参数关联在一起的,源码如下:
我们再戳进 Thread
类看一下 ——
这下清楚了,先获取当前的线程对象,再获取这个对象中的 ThreadLocalMap
对象,并且调用它的 set()
方法将当前的 ThreadLocal
对象和传入的 value 值存入。那么 ThreadLocalMap
又是个什么呢?它的 set()
方法肯定是个关键点,又是怎样的呢?我们再来看看 ——
ThreadLocalMap
其实就是 ThreadLocal
的一个内部类
到这里就一目了然了,ThreadLocalMap
中维护了一个 Entry[]
(Entry
是一个泛型类,其构造函数需要传入两个参数,一个是 ThreadLocal
对象,作为 Reference,另一个是 Object 对象,作为 value),然后我们就只需要将当前的 ThreadLocal
对象和传入的值放入这个数组的某个位置就可以了。
接下来我们再来看看 get()
方法,其源码如下:
这个源码很简单了,就是获取到当前线程对象的所持有的 ThreadLocalMap
对象,传入当前的 ThreadLocal
对象由此获得到对应的 ThreadLocalMap.Entry
对象,再取出它的 value 即可。
到此所有的流程已经走了一遍了,我们再来理一遍思路:首先创建一个全局共享的 ThreadLocal
对象,因为 ThreadLocal
不应该是依赖某一个具体的线程的,然后假设我们使用了三个线程 A、B、C 来对该 ThreadLocal
对象进行操作,线程 A 先调用 ThreadLocal
对象的 set()
方法,那么线程 A 中的 ThreadLocalMap
里面的 Entry
数组就会在某个位置保存这个 ThreadLocal
对象和 set()
进来的值;然后线程 B 调用 ThreadLocal
对象的 set()
方法,那么线程 B 中的 ThreadLocalMap
里面的 Entry
数组就会在某个位置保存这个 ThreadLocal
对象和 set()
进来的值;然后线程 C 调用 ThreadLocal
对象的 set()
方法,那么线程 C 中的 ThreadLocalMap
里面的 Entry
数组就会在某个位置保存这个 ThreadLocal
对象和 set()
进来的值。每个线程所持有的 ThreadLocalMap
对象是不同的,故其 ThreadLocalMap
里面的 Entry
数组也是不同的,故它们之间的 set()
或 get()
方法是互不相见的。
ThreadLocal 使用场景
在 Android 中,ThreadLocal
比较知名的一个场景就是 Looper
的使用,我们可以在 Lopper
类的源码中找到 ThreadLocal
的影子,因为一个线程对应一个 Looper
,这时使用 ThreadLocal
就再好不过了。那么为什么要在 Looper
中使用 ThreadLocal
,而不能是别的什么替代方案呢?假如我们不使用 ThreadLocal
,那么我们可以使用一个全局的哈希表来维护线程和 Looper
的关系,这样显然不如 ThreadLocal
来的方便。一般来说,当某些数据是以线程为作用域并且不同线程之间有不同的数据副本的话,就可以采用 ThreadLocal
。
扩展链接:理解Java中的ThreadLocal