Android消息机制之ThreadLocal浅析
概述
ThreadLocal 不是 Thread,它是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,对数据存储后,只有在指定线程中才可以获取到存储的数据,对于其他线程来说则是无法获取到数据的。
日常开发中用到 ThreadLocal 的场景不多,但是在 Android 系统中的 Looper、 ActivityThread、 AMS 等源码都用到了ThreadLocal。一般来说,当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal。比如对于 Handler 来说,它需要获取当前线程的 Looper,而 Looper 的作用域就是线程并且不同线程具有不同的 Looper,这时候通过 ThreadLocal 就可以轻松实现 Looper 在线程中的存取。
举例
下面通过一个简单的例子,来说明 ThreadLocal 的使用。
final ThreadLocal<String> threadLocal = new ThreadLocal<>();
new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set("Thread 1");
Log.e("twj", Thread.currentThread().getName() + " " + threadLocal.get());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set("Thread 2");
Log.e("twj", Thread.currentThread().getName() + " " + threadLocal.get());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
Log.e("twj", Thread.currentThread().getName() + " " + threadLocal.get());
}
}).start();
创建三个线程,第 1 和第 2 个 线程都通过 ThreadLocal 设置了值,第 3 个线程没有设置,看下结果:
2019-02-21 15:28:27.216 13845-14079/com.hust_twj.zademo E/twj: Thread-30 Thread 1
2019-02-21 15:28:27.216 13845-14080/com.hust_twj.zademo E/twj: Thread-31 Thread 2
2019-02-21 15:28:27.217 13845-14081/com.hust_twj.zademo E/twj: Thread-32 null
可以看到,第 1 和第 2 个 线程获取到了设置的值,第 3 个线程由于没有设置,获取到的数据为 null。也就说明,即使不同线程操作的是对同一个 ThreadLocal 对象,但对于ThreadLocal 中存储的数据,在不同的线程中具有不同的数据副本,只有存储后才能获取到相应的数据,否则就获取不到。
原理
ThreadLocal 之所以有这么奇妙的效果,在于 其get()
方法,ThreadLocal内部会从各自的线程中取出一个数组,然后再从数组中根据当前 ThreadLocal 的索引去查找出对应的 value 值,很显然,不同线程中的数组是不同的,这就是为什么通过ThreadLocal可以在不同的线程中维护一套数据的副本并且彼此互不干扰。
ThreadLocal 为泛型类:public class ThreadLocal<T>
,传进来的参数 T 即为要保存的数据的类型。那么数据是存储在哪里呢?答案是ThreadLocalMap
。ThreadLocalMap 为 ThreadLocal 存储数据的内部类:
ThreadLocalMap
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<ThreadLocal> {
/** 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;
/**
* The number of entries in the table.
*/
private int size = 0;
/**
* The next size value at which to resize.
*/
private int threshold; // Default to 0
// ...
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);
}
/**
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
}
可以看出,ThreadLocalMap 内部维护一个数组 Entry[] table, Entry 的 key 为 ThreadLocal,value 为 ThreadLocal 对应的值。需要注意的是,Entry 使用 WeakReference< ThreadLocal> 将 ThreadLocal 对象变成一个弱引用的对象,这样在线程销毁时,对应的实体就会被回收,不会出现内存泄漏。 Entry[] table 就是最后存放数据的地方,其默认大小为 16,当大于等于容量的 2/3 的时候会重新分配 table。
既然 ThreadLocal 需要保存数据,必然涉及到数据的存取,对应 ThreadLocal 中的 set() 和 get() 方法。先来看 set() 方法:
set()
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
//拿到当前 table 的长度
int len = tab.length;
//计算出key的下标
int i = key.threadLocalHashCode & (len-1);
//从计算出的下标开始循环:
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//如果当前指向的 Entry 是存储过的 ThreadLocal,就直接将以前的数据覆盖掉,并结束
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();
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
set() 方法首先会获取到当前线程 t,然后通过 t 来获取当前线程中的 ThreadLocal 数据 ThreadLocalMap。获取的方法就是:直接去当前Thread t 中访问。因为在 Thread 类中有一个成员变量 ThreadLocal.ThreadLocalMap threadLocals = null;
专门用于存储线程的 ThreadLocal 数据。这时候如果 threadLocals 不为 null,就将当前ThreadLocal 对象和值 value 存入 Entry 中;否则,调用 createMap(t, value);
进行初始化,并把 value 放进去。
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();
}
private T setInitialValue() {
T value = initialValue(); // return null
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
同样,getMap()
方法同样会获取当前线程的 threadLocals, 如果不为空,进一步获取 ThreadLocalMap 的 Entry,再获取其中的 value;若为空,调用 setInitialValue()
获取存储的数据。
通过分析 set()
和 get()
方法就可以知道 ThreadLocal 是如何存储线程本地变量的:每个 Thread 在生命周期中都会维护着一个 ThreadLocalMap 变量,可以看成是一个存储了 key 为 ThreadLocal、value 为所要存储的数据的 HashMap,当ThreadLocal 存储 value 时,先通过当前 Thread 得到其维护的 ThreadLocalMap,然后将其存储到该 map 中(实际是Entry中);而获取 value 时,仍然是先获取到当前 Thread 的 ThreadLocalMap,在获取到 ThreadLocalMap 中存储的value值。他们所操作的对象都是当前线程的 ThreadLocalMap 中的 table 数组,因此在不同线程中访问的都是同一个 ThreadLocal 的 set()
和 get()
,他们对 ThreadLocal 的读写操作仅限于各自线程的内部,这就是为什么ThreadLocal 可以在多个线程中可以互不干扰地存储和修改数据。
缺陷(内存泄露):
由于 ThreadLocalMap 是以弱引用的方式引用 ThreadLocal,如果 ThreadLocal 没有被 ThreadLocalMap 以外的对象引用,则在下一次 GC 的时候,ThreadLocal 实例就会被回收,那么此时 ThreadLocalMap 里的一组 KV 的 K 就是 null,因此在没有额外操作的情况下,此处的 V 便不会被外部访问到,而且只要 Thread 实例一直存在,Thread 实例就强引用着 ThreadLocalMap,因此 ThreadLocalMap 就不会被回收,那么这里 K 为 null 的 V 就一直占用着内存。
总结:
-
每一个线程都有变量 ThreadLocal.ThreadLocalMap threadLocals,保存着自己的 ThreadLocalMap。
-
ThreadLocal 所操作的是当前线程的 ThreadLocalMap 对象中的 table 数组,并把操作的 ThreadLocal 作为 key 进行存储。
参考:
- 任玉刚-艺术探索
- 一针见血理解ThreadLocal类(线程共享变量)
- ThreadLocal 原理(源码分析)
- Java面试必问,ThreadLocal终极篇
- 内存泄露相关参考:
1、https://www.jianshu.com/p/1a5d288bdaee
2、https://www.jianshu.com/p/250798f9ff76
3、https://blog.csdn.net/zhailuxu/article/details/79067467
4、https://www.cnblogs.com/coshaho/p/5127135.html
5、https://blog.csdn.net/puppylpg/article/details/80433271