Handler机制之ThreadLocal
ThreadLocal
在之前学习handler的时候不知道还有一个ThreadLocal类,要深入handler之前了解ThreadLocal的工作原理是非常有必要的。
在看了一遍ThreadLocal大概的工作原理之后,我有这几个疑问:
- ThreadLocal是如何获取到每个线程中的数组的?
- 这个数组的作用到底是什么?
- 如何通过ThreadLocal获取到每一个线程对应的Looper?
- 把变量存储在本地是为了什么?
- ThreadLocal的主要工作原理是怎样的?也就是ThreadLocal是如何把每个线程的数组存储的,它底层的结构?
ThreadLocal工作原理
ThreadLocal是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据。数据存储后,只有这个线程可以访问,其他线程都访问不到。
image
通过上面这张图可以知道ThreadLocal是可以通过线程去设置自己的私有变量的值的。因此可以到线程(Thread)类中的源代码去看看,有没有ThreadLocal。。。
Thread
class Thread implements Runnable {
```
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
```
}
Thread类中有关ThreadLocal的类只有ThreadLocal.ThreadLocalMap这个对象,说明ThreadLocalMap是个静态类。
ThreadLocal#ThreadLocalMap
static class ThreadLocalMap {
//存储的数据为Entry.且key为弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//table初始容量 16
private static final int INITIAL_CAPACITY = 16;
//该表根据需要调整大小,table.length必须始终为2的幂
private Entry[] table;
//负载因子 用于数组扩容
private int threshold;
//负载因子,默认情况下为当前数组长度的2/3
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
//第一次放入entry数组 初始化数组长度 定义扩容容量
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类可以看到它的内部结构和Map集合并没有关系,它的内部本身维护了一个Entry[] table数组。通过key可以找到这个数组中存的值。
几个想法:
- 为什么Entry对象要继承于WeakReference<ThreadLocal<?>>
- key从构造函数来看是ThreadLocal对象,hash映射是如何通过ThreadLocal对象来找到对应的值的?因为把Entry对象维护成了一个table数组,ThreadLocal在整个程序系统中应该只创建一次?为啥是ThreadLocal作为key?实在是没搞明白,先继续看下去吧。。。
ThreadLocalMap#set()
从给table数组设置来看下是如何执行的
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//通过ThreadLocal<?>来计算hash值 作为table数组的下标
int i = key.threadLocalHashCode & (len-1);
//遍历table数组 解决三种情况下的问题
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//情况一:判断key值是否相同,相同则将之前的数据覆盖掉
if (k == key) {
e.value = value;
return;
}
//情况二:如果当前Entry对象对应key值为null,就清空所有key为null的数据
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//以上情况都不满足,直接添加
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
}
上面所说的这三种情况需要再深入的理解一下。
- 第一种k == key,说明之前已经有过这类的数据了。比如说ThreadLocal<Boolean> threadLocal = new ThreadLocak<>(),之前已经创建过Boolean类型的ThreadLocal,这里有重新创建了这个类型的值发现之前的key已经存在,所以就把之前所存的数据覆盖掉。
- 第二种k== null,会清除当前所有key值为null的值。这里为什么要清除,就涉及到了内存泄漏的问题。在ThreadLocal要被gc掉的时候,ThreadLocalMap使用ThreadLocal的弱引用key,那么ThreadLocalMap中就会出现Entry的key为null的情况。考虑到这种情况的发生,就会在set之前进行处理。
- 第三种就是直接添加新值,table容量不够时可进行扩容。
从上面这段给ThreadLocalMap的table数组设值的时候我发现,其实是将ThreadLocal<?>对象经过hash运算得到table数组的下标(i),通过这个值来和存入数的数据做为映射也就是通过for循环来查找数组中的数据,再通过LocalThread<?>(key)和数组中的ThreadLocal<?>是否创建新的Entry数组单元。
ThreadLocalMap#get
再来看下是如何从Entry中得到ThreadLocal<?>的。
set方法中的e.get()方法
public T get() {
return getReferent();
}
我点击这个方法后,发现跳转到了Reference这个类中。Reference类是一个抽象类,定义了所有参考对象共有的操作,参考对象是与来及收集器紧密合作实施的,此类不能直接子类化。Entry继承于WeakReference,进去看一看。
WeakReference
进入WeakReference竟然只有两个方法
public class WeakReference<T> extends Reference<T> {
//创建一个弱引用给继承于WeakReference的对象
public WeakReference(T referent) {
super(referent);
}
//创建一个新的弱引用,并将这个对象在给定的队列中注册
public WeakReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}
- 当handler在activity中使用的时候可能会造成内存泄漏,因为activity被销毁的时候其内部的handler的任务队列中还有任务正在被执行,handler内部隐藏着对activity的强引用,为了解决这个问题就可以将activity作为被弱引用的对象,弱引用对象在gc的时候是会被回收的。可以这样做:
WeakReference<Activity> reference = new WeakReference<>(activity);
Activity activity = reference.get();
在handler中处理任务的时候先判断这个activity是否不为空,不为空再进行接下来的操作。
- 第二个方法中有个ReferenceQueue对象。从名字上知道它是一个队列。在对象被回收后会把弱引用对象(WeakReferencef对象或者其子类)放入ReferenceQueue中。这里放入的是弱引用的对象,被弱引用的对象已经被回收了(就像上面的activity)。
ThreadLocal的使用
private ThreadLocal<Boolean> threadLocal = new ThreadLocal<Boolean>();
//主线程
thread.set(true);
Log.d(TAG,"mainThread:"+threadLocal.get());
//子线程1
new Thread("Thread1"){
public void run(){
threadLocal.set(false);
Log.d(TAG,"Thread1:"+threadLocal.get());
}
}.start();
//子线程2
new Thread("Thread2"){
public void run(){
Log.d(TAG,"Thread2:"+threadLocal.get());
}
}.start();
运行结果:
F/TestActivity:mainThread:true
F/TestActivity:Thread1:false
F/TestActivity:Thread2:null
从结果可以看到不同的线程访问的ThreadLocal是同一个对象,但是他们ThreadLocal获取到的值是不一样的。原因就是Thread线程中有ThreadLocal.ThreadLocalMap这样一个变量,这个变量内部是一个数组,用来存储对应线程内的私有变量。通过当前的ThreadLocal<?>去找到对应的值。
ThreadLocal#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();
}
先获取当前线程对象,根据线程对象找到它所对应的的ThreadLocalMap对象,map不为空调用getEntry方法获取Entry存储数据的对象。
ThreadLocal#setInitialValue
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
//找不到要找的数据就放到table数组中
if (map != null)
map.set(this, value);
else
createMap(t, value);//创建map
return value;
}
//回到了ThreadLocal的构造方法中
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap#getEntry
private Entry getEntry(ThreadLocal<?> key) {
//根据key计算出数据下标索引
int i = key.threadLocalHashCode & (table.length - 1);
//得到Entry
Entry e = table[i];
//不为空就返回
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
ThreadLocalMap#getEntryAfterMiss
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
//key相同直接返回
if (k == key)
return e;
//key为空,清除key==null的所有数据
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
//没有数据直接返回
return null;
}
ThreadLocal#get
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内存泄漏问题
ThreadLocalMap中采用弱引用作为key,涉及到了java的回收机制。
- 弱引用:被弱引用关联的对象只能生存到下一次垃圾收集发生之前,无论当前内存是否够,都会回收掉被弱引用关联的对象。
ThreadLocal不能使用强引用
若key使用强引用,当引用的ThreadLocal被回收了,ThreadLocalMap中还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致内存泄漏
清除key的原因
ThreadLocal的set和get方法中都会去清除key==null的数据,具体有两个原因:
- ThreadLocalMap使用弱引用ThreadLocal作为key,当ThreadLocal被gc时,table中的key值也会变为null,也就是出现key为null的Entry,就无法访问这些key为null的Entry的value
- 若线程一直不结束,这些key为null的Entry就会一直存在一条强引用链:Thread ref(当前线程引用)-->Thread-->ThreadLocalMap-->Entry-->value,会造成Entry永远无法回收,造成内存泄漏。
避免使用static的ThreadLocal
使用static修饰的ThreadLocal,延长了ThreadLocal的生命周期,可能导致内存泄漏。原因:在java虚拟机加载类的过程中为静态变量分配内存。static变量的生命周期取决于类的生命周期,也就是类被卸载的时候,静态变量才会被销毁并释放内存空间。这里的目的就是为了保持线程被销毁的时候它内部不应该持有对ThreadLocal的引用。
类的生命周期结束和下面三个条件相关:
- 该类所有的实例都已经收回,也就是java堆中不存在该类的任何实例
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有任何地方被引用,没有任何地方通过反射访问该类的方法
总结
- ThreadLocal本质是线程中的ThreadLocalMap来实现本地线程变量的存储,该线程的ThreadLocalMap内的数据无法被任何线程访问
- ThreadLocalMap采用数组的方式来实现数据的存储,其中key指向当前ThreadLocal对象,且该对象为弱引用对象
- ThreadLocal为内存泄漏可能造成的Entry的key值为空,导致找不到想要的值。在ThreadLocal的set、get\remove方法中都会清楚Entry的key==null的值
- 在使用ThreadLocal时,避免使用static的ThreadLocal。分配了ThreadLocal后,一定要根据当前类的生命周期来判断是否需要手动的去清理ThreadLocalMap中key==null的Entry