引言
源码版本:Android10
ThreadLocal
今天我们来谈一谈ThreadLocal,在说ThreadLocal之前,先提出几个问题,带着问题分析ThreadLocal,可能会事半功倍。
- ThreadLocal是什么,怎么用?
- ThreadLocal的内部原理是什么?
- ThreadLocal的应用场景?
- 使用ThreadLocal需要注意些什么?
ThreadLoal是什么,怎么用?
ThreadLoal 类提供一个线程本地变量,同一个 ThreadLocal 所包含的对象,通过它的get或者set方法访问到的,在不同的 Thread 中有不同的副本。
这句话是什么意思呢?下面我们来看一下这个例子
val threadLocal = ThreadLocal<String>()
val thread = Thread {
run {
println("thread s : ${threadLocal.get()}")
}
}
fun main() {
threadLocal.set("1")
thread.start()
println("main s : ${threadLocal.get()}")
}
复制代码
上面这段代码的打印结果是:
main s : 1
thread s : null
可以看到,我们在通过threadLocal的set方法,往其内部存储了一个”1″,在主线程get和在子线程get,获取的值,并不一样。这就是刚上面说的,它的get或者set方法访问到的,在不同的 Thread 中有不同的副本的意思。
另外,当我们不用set方法设置,也可以像下面这样,通过复写initialValue()方法,来给其设置一个初始值。
val threadLocal2 = object : ThreadLocal<String>() {
override fun initialValue(): String? {
return "2"
}
}
复制代码
总的来说,ThreadLocal的用法还是比较简单的,一句话总结就是,通过ThreadLocal对象,在主线程设置的值,你要从主线程取出来,从子线程设置的值,你要从子线程中取出来,存取都和当前线程相关。
ThreadLocal的原理
下面我们来看一下它的原理
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
复制代码
上面的代码,是ThreadLocal的set()方法,可以发现,传入的value值,最终存储到了map中去。接下来,我们看一下这个getMap(t)的实现。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
复制代码
这个方法很简单,就是通过返回t.threadLocals,首先t是通过Thread.currentThread()获取到的Thread对象,也就是当前运行的线程对象。
下面是Thread类的的threadLocals变量
ThreadLocal.ThreadLocalMap threadLocals = null;
复制代码
查看Thread类代码可知,其实Thread中的threadLocals默认是null,也没有在自身的构造方法中初始化。故第一次调用set的时候,map是空的,那么会进入createMap(t,value)方法。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
复制代码
可以发现,代码就是给Thread的threadLocals赋值。也可以理解为,这个ThreadLocalMap对象和线程绑定了。以后在这个ThreadLocalMap对象中存储的值都只和绑定的线程相关。
TheadLoclaMap
ThreadLocalMap是什么?
ThreadLocalMap其实就是一个自定义的HashMap,仅仅只适用于存储线程的本地变量。它的key是ThreadLocal对象,它的value是一个Object。注意,说是自定义的HashMap,其实不是内部维护了一个HashMap对象用来存储,而是和HashMap的实现原理类似。
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
复制代码
上面是ThreadLocalMap的构造方法,table是一个Entry数组,INITIAL_CAPACITY是16,即一个可以存储16个元素的数组。
firstKey.threadLocalHashCode & (INITIAL_CAPACITY – 1)是通过与操作,来确定构造的Entry应该插放在数组的那个位置。
感兴趣的可以去看一下,threadLocalHashCode 的值是如何变化的,数组是如何动态扩展的。
这里我们简单提一下,存储的逻辑,详细的可以去研究一下HashMap的源码,非常经典。
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
复制代码
上面是ThreadLocal对象的get方法,可以发现,取的时候也是通过ThreadLocalMap对象去取的。
到这里我们可以得出结论,ThreadLoal对象的存和取,其实是通过ThreadLocalMap的set和get方法,而一个Thread只关联了一个ThreadLocalMap对象,ThreadLocalMap对象可以存储多个线程相关的本地变量。
那么ThreadLocal永远只能存一个值吗?
这么理解不准确,应该说是一个ThreadLocal对象在一个线程下只能存储一个值。
ThreadLocal的应用场景
日常情况下,ThreadLocal使用场并不多。
但可以把使用场景归纳为:当某些数据的作用域是线程,并且在不同线程间都需要有自己的副本的时候,这时候可以考虑使用ThreadLocal。
比如,Android源码的Lopper、ActivityThread以及AMS中都用到了ThreadLocal。
使用ThreadLocal需要注意些什么
需要注意点就是,ThreadLocal可能引发内存泄漏。
为什么会引发内存泄漏?这就要看ThreadLocalMap中存储数据的内部类Entry
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
复制代码
Entry类是ThreadLocalMap中保存的元素类型,Entry构造方法接收一个软引用的ThreadLocal对象的key和一个Object的value。请注意这个ThreadLocal是软引用的。
我们设想一下,当ThreadLocal对象我们不在使用了,将ThreadLocal置为null。这时候,ThreadLocal这一个对象的内存,只有ThreadLocalMap中的Entry的key指向,但是这个key是ThreadLocal的弱引用,所以下次GC的时候,因为是弱引用,这个Entry的key会被置为空。只要线程没结束的话,这个Entry就是一个key为null的对象,从map中永远都取不出来。这就导致了这个Entry的内存被泄漏了,无法释放。
大家想一想,为什么这么设计呢?如何把这个Entry的key设置成强引用?
如果设计成强引用,还是会内存泄漏,而且更加明显。即使你在外部不想使用ThreadLocal对象了,把其置为null,但是ThreadLocalMap一直持有ThreadLocal对象的引用,ThreadLocal对象的内存依旧还是会泄漏。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
复制代码
所以使用ThreadLocal对象的时候,如果在某一个线程使用了ThreadLocal存储了数据,当不想在当前线程再用的时候,一定记得调用remove()方法,保持良好的编码习惯。