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_handler.png


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的引用。

上一篇:死磕算法之快速排序


下一篇:Preact——无线端活动页的轻量级开发方案