Handler机制之ThreadLocal

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> {
            /** 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>
  • 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 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 extends Reference {
    
    //创建一个弱引用给继承于WeakReference的对象
    public WeakReference(T referent) {
        super(referent);
    }
    
    //创建一个新的弱引用,并将这个对象在给定的队列中注册
    public WeakReference(T referent, ReferenceQueue q) {
        super(referent, q);
    }
}
  • 当handler在activity中使用的时候可能会造成内存泄漏,因为activity被销毁的时候其内部的handler的任务队列中还有任务正在被执行,handler内部隐藏着对activity的强引用,为了解决这个问题就可以将activity作为被弱引用的对象,弱引用对象在gc的时候是会被回收的。可以这样做:
WeakReference reference = new WeakReference<>(activity);
Activity activity = reference.get();

在handler中处理任务的时候先判断这个activity是否不为空,不为空再进行接下来的操作。

  • 第二个方法中有个ReferenceQueue对象。从名字上知道它是一个队列。在对象被回收后会把弱引用对象(WeakReferencef对象或者其子类)放入ReferenceQueue中。这里放入的是弱引用的对象,被弱引用的对象已经被回收了(就像上面的activity)。

ThreadLocal的使用

private ThreadLocal threadLocal = new ThreadLocal();
//主线程
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)