ThreadLocal,看我就够了!
ThreadLocal
开胃菜
研究过Handler的应该对ThreadLocal比较眼熟的,线程中的Handler对象就是通过ThreadLocal来存放的。初识ThreadLocal的可能被它的名字有所误导,ThreadLocal初一看可能会觉得这是某种线程实现,而实际并非如此。事实上,它是一个全局变量,用来存储对应Thread的本地变量,这也是为什么将其称之为Local。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
比如Handler,当我们在一个线程中创建了一个Handler时,在调用Looper.prepare()时通过ThreadLocal保存了当前线程下的Looper对象,而所有线程的Looper都由一个ThreadLocal来维护,也就是在所有线程中创建的Looper都存放在了一个ThreadLocal中,然后创建Handler将Handler与当前线程Looper关联,当调用Looper.loop()的时候通过myLooper()得到的就是当前线程的Looper,当在其他线程使用Handler来发送消息的时候,其实也就是将对应的Message存储到了对应Handler的MessageQueue中,当Looper去分发消息的时候,就是将当前线程中的Looper对应的MessageQueue中的Message通过Handler的回调返回给了Handler所在的线程。如对Handler不了解的,可参考Handler全面解读
ThreadLocal模拟
那么,如何来做到区分不同线程中的变量呢?我们这里模拟一个实现ThreadLocal功能的类,原理大致一样。
public class ThrealLocalImitation<T> {
private static Map<Thread, Object> sSaveValues = new HashMap<Thread, Object>();
public synchronized void set(T threadData) {
Thread thread = Thread.currentThread();
mSaveValues.put(thread, threadData);
}
public synchronized T get() {
Thread thread = Thread.currentThread();
return (T) mSaveValues.get(thread);
}
}
如上ThreadLocal模仿类里面,通过全局的Map变量sSaveValues,以Thread为key,value为对应线程需要保存的变量,实现了,每个线程对应保存了一个变量,在不同的线程存储不同的变量,通过get方法就能取回对应的值。
ThreadLocal原理分析
ThreadLocal类通过set、get方法来分别存取变量的,搞懂了这两个方法的功能也就明白ThreadLocal的原理了,所以重点分析这两个方法。
在进入正式的分析之前先来看一个类——ThreadLocalMap
和我们模拟的ThreadLocal稍有所区别,ThreadLocal不是直接通过一个Map来存储Thread和value对应关系的。
在Thread类中,有一个变量ThreadLocal.ThreadLocalMap。
先看下该类的其中一个构造方法
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);
}
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocalMap内部维护一个数组Entry[] table, Entry对应存储了key--value(ThreadLocal---value)。ThreadLocalMap实际上是一个实现了自定义的寻址方式的HashMap。
那么ThreadLocal是如何存储线程本地变量的呢?先给个简单的结论。
每个Thread在生命周期中都会维护着一个ThreadLocalMap,可以看成是一个存储了ThreadLocal(key)---value的HashMap,当ThreadLocal存储value时,先通过当前Thread得到其维护的ThreadLocalMap,然后将其存储到该map中,而获取value时则是先获取到当前线程的ThreadLocalMap,然后通过当前的ThreadLocal,获取到ThreadLocalMap存储的value值。
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);
}
set方法中,首先通过Thread.currentThread()获取到当前的线程,通过当前线程获得其维护的ThreadLocalMap,当map为空时,则为当前Thread创建一个ThreadLocalMap,不为空的话则将ThreadLocal--value存储到map中。
所以一个Thread对应着一个ThreadLocalMap,而一个ThreadLocalMap对应着多个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();
}
get()方法,同样是先获取到当前Thread,然后获取到当前Thread的ThreadLocalMap,然后根据ThreadLocal自身,通过ThreadLocalMap自身的寻址方式获取到存储ThreadLocal和value的Entry对象,进而得到value。
setInitialValue();是当ThreadLocalMap为空时,可以通过实现ThreadLocal的initialValue()来获得一个默认值,同时该默认值会被存储到线程的ThreadLocalMap中。
内存泄漏
分析到这里,ThreadLocal的原理已经很明朗了。但是一些使用不当的情况出现内存泄漏的风险,所以最后讲解下ThreadLocal会出现的内存泄漏风险,及如何避免。
ThreadLocalMap中存储的Entry为ThreadLocal--value,准确的描述应该是weakReference(ThreadLocal)--value,即,key(ThreadLocal为弱引用),而value则是强引用的,当ThreadLocal为空后,Thread不会再持有ThreadLocal引用,ThreadLocal可以被GC回收,但是Thread的ThreadLocalMap仍然还持有value的强引用,导致value需要等待线程生命周期结束才可能被GC回收。当出现一些长时间存在的线程,不断的存储了内存比较大的value,而value实际是不再被使用的,value由于线程没有被回收而不断的堆积,造成了内存泄漏。比如当使用到线程池是,Thread很有可能不会被马上结束,可能会被不断的重复利用。
所以这里引入ThreadLocal的另外一个方法——remove方法
remove()方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
//ThreadLocalMap中的remove
private void remove(ThreadLocal<?> key) {
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)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
所以在value不再使用时,应该及时调用remove,解除线程对该value的引用。