美文网首页
Handler机制之ThreadLocal

Handler机制之ThreadLocal

作者: 李die喋 | 来源:发表于2019-10-25 22:12 被阅读0次

    ThreadLocal

    在之前学习handler的时候不知道还有一个ThreadLocal类,要深入handler之前了解ThreadLocal的工作原理是非常有必要的。

    在看了一遍ThreadLocal大概的工作原理之后,我有这几个疑问:

    1. ThreadLocal是如何获取到每个线程中的数组的?
    2. 这个数组的作用到底是什么?
    3. 如何通过ThreadLocal获取到每一个线程对应的Looper?
    4. 把变量存储在本地是为了什么?
    5. 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

    参考文章

    Android Handler机制之ThreadLocal

    相关文章

      网友评论

          本文标题:Handler机制之ThreadLocal

          本文链接:https://www.haomeiwen.com/subject/crwevctx.html