ThreadLocal 是什么?
ThreadLocal,即线程局部变量,是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,数据存储后,只有在指定线程中可以获取到存储的数据。首先,我们来看下简单使用吧。
public class ThreadLocalTest {
//线程局部变量
private static ThreadLocal localNum = new ThreadLocal<>();
//正常变量
private int shareNum;
public int getShareNum() {
return shareNum;
}
public void setShareNum(int shareNum) {
this.shareNum = shareNum;
}
public static void main(String[] args) {
ThreadLocalTest threadLocal = new ThreadLocalTest();
new Thread("#1"){
@Override
public void run() {
//获取当前线程 "#1" 线程局部变量
Integer integer = localNum.get(); //默认值为 null
System.out.println("#1线程局部变量默认值 localNum:" + integer + " #1正常变量 shareNum:" + threadLocal.getShareNum());
//在当前线程修改线程局部变量的值
localNum.set(5);
//修改正常变量的值
threadLocal.setShareNum(3);
System.out.println("#1 修改后\n" +"线程局部变量的值 localNum:" + localNum.get() + " 正常变量 shareNum:" + threadLocal.getShareNum());
}
}.start();
new Thread("#2"){
@Override
public void run() {
System.out.println("\n#2 线程局部变量的值 localNum:" + localNum.get() + " 正常变量的值 shareNum" + threadLocal.getShareNum());
}
}.start();
}
}
输出结果:
可以看到,在线程 #1 中一开始 localNum 的值没有赋值,所以为null,shareNum 理所当然为0,而且修改后也输出修改后的值,localNum 的值为5,shareNum 为3。很正常。但是当我们看线程 #2 的时候可以发现,shareNum 为3,但是 localNum的值依然为 null,也就是说没有赋值。我们可以简单地理解为ThreadLocal 是一个 HashMap(事实上并不是,后面我们会讲到),get()方法和 set()方法都是以当前线程为key进行取值和赋值。
源码看一看
首先,先上 ThreadLocal 类的结构图
我们挑比较重要的几个方法进去看看。
- public T get()
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()方法首先是获得当前的线程,然后通过 getMap(t) 获取到 ThreadLocalMap 的实例,这里我们需要知道,如果在 get()之前没有调用 set()方法的时候,map 是等于 null的,然后返回 setInitialValue,如果之前调用了 set() 方法,则会返回其设置的值。
- private T 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;
}
可以看到 setIniitalValue 首先调用 initialValue 获取初始值,而 initialValue()就更简单了,直接返回了 null,这也解释了我们的实例代码在没有调用 set() 时直接调用 get()获取到的值是 null。当然,如果我们有需要可以派生出 ThreadLocal 的子类然后重写 inititalValue()方法设置我们需要的默认值。
- public void set(T value)
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
set()方法也很简单,就是获取当前线程,然后通过当前线程获取ThreadLocalMap,然后设置 value,如果ThreadLocalMap 为空就创建 ThreadLocalMap 并设置 value。
相信聪明的你也看到了上面这几种方法都是围绕着 TheadLocalMap 来操作的,那么ThreadLocalMap 到底是何方神圣?其实 ThreadLocalMap 是 ThreadLocal 的一个静态内部类。
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
//省略部分代码
}
这里简要说下ThreadLocalMap 的机制,首先是 ThreadLocalMap 的变量是属于 Thread 的内部属性,不同的 Thread 拥有完全不同的 ThreadLocalMap 变量,然后ThreadLocalMap内部又维持了一个 Entry[ ] table 数组,数据的读取都是根据当前 ThreadLocal 的索引进行操作的。
总结
- ThreadLocal 主要操作方法是 set 方法和 get 方法,它们最后都会转移到ThreadLocalMap 中。
- 我们可以通过重写 initialValue 方法来定义我们ThreadLocal 返回的默认值,如果不这么做的话默认返回 null。
- 因为每个 Thread 在进行 ThreadLocal 对象访问的时候,访问的都是各自线程自己的 ThreadLocalMap,所以保证了 Thead 和 Thead 之间的数据访问隔离。
- ThredLocalMap 内部维护了一个 Entry[ ] 数组,不同的 ThreadLocal 对象操作同一 Thread 时,ThreadLocalMap 在存储时采用当前 ThreadLocal 的实例作为 key 来保持数据访问隔离。
多说两句
在 Android 中,如果我们子线程中直接创建 Handler 是会报错的,这是因为 Handler 的创建时需要获取当前线程的 Looper 的,主线程默置了 Looper。那么 Handler 是如何获取每个线程的 Looper 的呢?答案就是通过 ThreadLocal,有兴趣的同学可以自己去查阅源码。
参考资料
[1] 任玉刚. Android 开发艺术探索[M]. 北京 : 电子工业出版社 , 2015.9.
[2] Mr-YangCheng ThreadLocal的使用规则和源码分析.md