首先讲一下特性:ThreadLocal 以线程为作用域存取数据,不同线程有不同的数据副本,各个线程副本之间读取互不干扰
。
举个例子:
ThreadLocal<String> testThreadLocal = new ThreadLocal<>();
testThreadLocal.set("123");
Log.i("zx", "主线程中testThreadLocal值为" + testThreadLocal.get());
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
testThreadLocal.set("456");
Log.i("zx", "thread1中testThreadLocal值为" + testThreadLocal.get());
}
});
thread1.start();
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
Log.i("zx", "thread2中testThreadLocal值为" + testThreadLocal.get());
}
});
thread2.start();
复制代码
最终输出结果为
主线程中testThreadLocal值为123
thread1中testThreadLocal值为456
thread2中testThreadLocal值为null
复制代码
结果确实符合上边对它的描述:主线程设置了值为 123,所以取出的值也是 123,thread1 中设置值为 456,所以取出 456,thread2 中没有设置值,所以取出 null。可以看到同一个对象 testThreadLocal,在不同的线程调用 get 方法,竟然能取出不同的值。ThreadLocal 的 set 和 get 到底做了什么操作能支持这样的特性呢?看看源码吧。
public void set(T value) {
//获取set方法执行的线程
Thread t = Thread.currentThread();
//根据线程得到一个ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);//将数据放在ThreadLocalMap中
else
createMap(t, value);//ThreadLocalMap为空则新建一个带初始值的map
}
复制代码
set 方法就这几行,看起来比较简单,重点是存放数据的 ThreadLocalMap,ThreadLocalMap 是通过 getMap()
方法获取到的,下边看看 getMap()
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
复制代码
getMap()返回了线程的 threadLocals 属性,这个属性的类型是 ThreadLocalMap。也就是说每一个线程都有一个 ThreadLocalMap,在不同的线程中调用 set 方法,数据都是存放在每个线程自己的 ThreadLocalMap 里,所以就实现了存数据时互不影响。那由此推测,取数据时也是取各个线程自己的 ThreadLocalMap,下边来看看 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();
}
复制代码
可以看到取值时仍然调用了 getMap()
,也就是说取值时仍然是从各个线程自己的 ThreadLocalMap 去取的,所以验证了我们的猜想。
再深入思考一下,如下的代码,ThreadLocal 该如何去存值呢?
{
//同一线程下
ThreadLocal<String> testThreadLocal1 = new ThreadLocal<>();
ThreadLocal<String> testThreadLocal2 = new ThreadLocal<>();
ThreadLocal<String> testThreadLocal3 = new ThreadLocal<>();
testThreadLocal1.set("123");
testThreadLocal2.set("456");
testThreadLocal3.set("789");
}
复制代码
之前我们已经知道了在同一线程下,数据最终肯定是放在同一个 ThreadLocalMap 里,那这里 set 操作并没有传一个唯一的 key,那取的时候如何从同一个 ThreadLocalMap 里取出不同的值呢? 再来看看 set 方法
public void set(T value) {
//获取set方法执行的线程
Thread t = Thread.currentThread();
//根据线程得到一个ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);//将数据放在ThreadLocalMap中
else
createMap(t, value);//ThreadLocalMap为空则新建一个带初始值的map
}
复制代码
关键操作在 map.set(this, value)
这一行,继续看看 ThreadLocalMap 类的 set()的源码
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();
}
复制代码
再来看看获取值时的过程,ThreadLocal 的 get()方法里有一行关键代码
ThreadLocalMap.Entry e = map.getEntry(this);
复制代码
进入 ThreadLocalMap 类的 getEntry()方法,看看是否有计算索引值的代码
private Entry getEntry(ThreadLocal<?> key) {
//计算索引值的代码
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
复制代码
ThreadLocalMap 保存值的方法 set()和获取值的方法 getEntry() 都有这一行计算索引值的代码
key.threadLocalHashCode & (table.length - 1)//这里的key就是ThreadLocal对象
复制代码
下边是生成 threadLocalHashCode 的代码,关键信息已经在注释里了
private final int threadLocalHashCode = nextHashCode();
//下一个要给出的哈希码,从零开始。
//由于是static类型,所以每次创建时会获取到上次递增之后的值,每次递增HASH_INCREMENT
private static AtomicInteger nextHashCode = new AtomicInteger();
//每次的增量
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
//等同于nextHashCode++,但是是线程安全的
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
复制代码
以上的源码显示了,在每次创建 ThreadLocal 时会生成一个唯一标识 threadLocalHashCode, 这个 threadLocalHashCode 与存放数据的数组长度-1 做与运算,计算出的值作为存放数据的索引,每次取数据时同样使用这个索引。所以这就实现了同一个线程中,即使每次调用set()和get()时没有传一个key,也能根据ThreadLocal实例本身生成一个唯一的索引,这就保证了数据的正常读取。
总结一下:
数据并不是存放在ThreadLocal里,数据实际上是放在每个线程的ThreadLocalMap(threadLocals属性)中,只是使用ThreadLocal来管理数据。
同一个ThreadLocal在不同的线程中,它在每个线程的ThreadLocalMap中索引是相同的,但是ThreadLocalMap各不相同,所以实现了同一个ThreadLocal在不同线程中各有其值且互不干涉的特性;
同一个线程中不同的ThreadLocal,每个ThreadLocal实例创建时都有一个唯一标识符,并根据此标识符生成索引,根据此索引就可以实现在同一个ThreadLocalMap中读取。
如果我们需要以线程为作用域存取数据,不同线程有不同的数据副本时就可以考虑使用ThreadLocal。此外,熟悉ThreadLocal也可以方便我们理解Looper、ActivityThread等知识,这些地方都用到了ThreadLocal。