Android ThreadLocal

1.ThreadLocal

ThreadLocal是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,数据存储以后,只有在指定的线程中才可以访问,其他线程无法获取。因此在多个线程执行时,为了互不干扰资源,可以使用ThreadLocal。

 

使用场景:

①某些数据是以线程为作用域且不同线程之间具有不同数据副本;

Android的Handler消息机制中,Looper的作用域就是线程且不同线程之间具有不同的Looper,这里使用的就是ThreadLocal对Looper与线程进行关联,如果不使用ThreadLocal,那么系统就必须提供一个全局的Hash表供Handler查找指定线程的Looper,这要比ThreadLocal复杂多了。

②当复杂逻辑下进行对象传递时,也可以考虑使用ThreadLocal。

例如一个线程中执行的任务比较复杂,需要一个监听器去贯穿整个任务的执行过程,如果不使用ThreadLocal,那么就需要将这个监听器从函数调用栈层层传递,这对程序设计来说是不可接受的,这时可以使用ThreadLocal让监听器作为线程内的全局对象而存在,在线程内部只需要通过get方法就可以获取到监听器。每个监听器对象都在自己的线程内部存储。

 

2.ThreadLocal用法

public class MainActivity extends Activity {

    private ThreadLocal<Object> mThreadLocal = new ThreadLocal<>();

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        //主线程

        mThreadLocal.set("123");

        new Thread() {

            @Override

            public void run() {

                super.run();

                //子线程

                mThreadLocal.set("456");

                mThreadLocal.set(false);

                Log.i("ThreadLocal子线程",String.valueOf(mThreadLocal.get()));

            }

        }.start();    

        try {

            getMainLooper().getThread().sleep( 3000); //保证子线程先去更新值

            Log.i("ThreadLocal主线程",String.valueOf(mThreadLocal.get()));

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

    }

}

执行结果:

ThreadLocal子线程: false

ThreadLocal主线程: 123

通过这个例子就可以看到ThreadLocal的隔离性。主线程和子线程共享一个ThreadLocal,主线程设置ThreadLocal保存值123,所以get出来是123,而子线程先设置值为456,又设置值为false,导致false把之前设置的456覆盖掉了,子线程打印出来就是false。

所以,不同的线程访问同一个ThreadLocal获取到的值是不一样的。也就是说ThreadLocal在当前线程操作数据只对当前线程有效。

 

3.源码分析

①构造方法

public ThreadLocal() {}

②set方法

public void set(T value) {

    Thread t = Thread.currentThread(); //获取当前线程

    ThreadLocalMap map = getMap(t); // 拿到当前线程的ThreadLocalMap对象threadLocals,每个线程都有一个该对象

    if (map != null) 

        map.set(this, value); //设置K为当前ThreadLocal对象,V为要存储的值

    else

        createMap(t, value); //创建新的ThreadLocalMap对象并赋值

}

当调用set(T value) 方法时,方法内部会获取当前线程中的ThreadLocalMap,然后进行判断,如果不为空就调用ThreadLocalMap的set方法(其中key为当前ThreadLocal对象,value为当前赋的值);如果为空就让当前线程创建新的ThreadLocalMap并设值。也就是说每个线程都有一个map,key是threadLocal变量,value是该变量在当前线程中的值。

其中getMap()与createMap()方法具体代码如下:

//创建线程t的ThreadLocalMap属性

void createMap(Thread t, firstValue){

    t.threadLocals = new ThreadLocalMap(this, firstValue);

}

//获取当前线程t的ThreadLocalMap属性

ThreadLocalMap getMap(Thread t){

    return t.threadLocals;

}

ThreadLocal中所有的数据操作都与线程中的ThreadLocalMap有关。ThreadLocalMap是ThreadLocal类里定义的内部类。

③get方法

public T get() {

    Thread t = Thread.currentThread();

    ThreadLocalMap map = getMap(t); //拿到线程中的ThreadLocalMap

    if (map != null) {

        ThreadLocalMap.Entry e = map.getEntry(this); //根据key(ThreadLocal)对象,获取存储的entry数据

        if(e != null){

            T result = (T)e.value;

            return result;

        }

    }

    return setInitialValue(); //如果当前线程的ThreadLocalMap为空,就为当前线程创建新的ThreadLocalMap对象

}

其实ThreadLocal的get方法其实很简单,就是获取当前线程中的ThreadLocalMap对象,如果没有就创建,如果有就根据当前的 key(当前ThreadLocal对象)获取相应的数据。其中内部调用了ThreadLocalMap的getEntry()方法区获取数据。

④ThreadLocalMap源码

ThreadLocalMap是ThreadLocal中的一个静态内部类,它是为了维护线程私有值创建的自定义哈希表HashMap。其中线程的私有数据都是非常大且使用寿命长的数据。

static class ThreadLocalMap {   

    //ThreadLocalMap中存储的数据为Entry,且key为弱引用 

    static class Entry extends WeakReference<ThreadLocal<?>> {

        Object value;

        Entry(ThreadLocal<?> k, Object v) {

                super(k);

                value = v;

        }

    }

    private Entry[] table; //用于存储数据

    private int size = 0;//table中entry的个数

    private int threshold; // 负载因子,用于数组扩容

    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {

        //第一次放入Entry数据时,初始化数组长度,定义扩容阈值

        table = new Entry[INITAL_CAPACITY]; 

        int i = firstKey.threadLocalHashCode & (INITAL_CAPACITY - 1);

        table[i] = new Entry(firstKey, firstValue);

        size = 1;

        setThreshold(INITAL_CAPACITY);

    }

    //set方法,把值存储到特定线程的Map里面

    private void set(ThreadLocal<?> key, Object value) {

        Entry[] tab = table;

        int len = tab.length;

        int i = key.threadLocalHashCode & (len-1); //计算出key的hash值计算位置

        //判断当前位置是否有数据,如果key值相同就替换,如果不同则找空位放数据

        for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {

            ThreadLocal<?> k = e.get();

            if (k == key) { //key值相同,直接覆盖

                e.value = value;

                return;

            }

            if (k == null) { //当前Entry对象对应key值为null,则清空所有key为null的数据

                replaceStaleEntry(key, value, i);

                return;

            }

        }

        //以上情况都不满足,则直接添加

        tab[i] = new Entry(key, value); 

        int sz = ++size;

        //如果当前数组达到阈值,就扩容

        if (!cleanSomeSlots(i, sz) && sz >= threshold) 

            rehash();

    }

    //getEntry(),获取当前线程的Map里的变量

    private Entry getEntry(ThreadLocal<?> key){

        //根据当前key哈希后计算的位置,去找数组中对应位置是否有数据,如果有就直接将数据放回,如果没有则调用getEntryAfterMiss()方法

        int i = key.threadLocalHashCode & (table.length -1); //获取hash值

        Entry e = table[i];

        if(e != null && e.get() == key)

            return e;

        else

            return getEntryAfterMiss(key, i, e);

    }

    private Entry getEntryAfterMiss( ThreadLocal<?> key, int i, Entry e) {

        //如果从数组中获取的key==null的情况下,get方法内部也会调用expungeStaleEntry()方法,去清除当前位置所有key==null的数据

        Entry[] tab = table;

        int len = tab.length;

        while (e != null) {

            ThreadLocal<?> k = e.get();

            if (k == key)//如果key相同,直接返回

                return e;

            if (k == null)//如果key==null,清除当前位置下所有key=null的数据

                expungeStaleEntry(i);

            else

                i = nextIndex(i, len);

            e = tab[i];

        }

        return null;//没有数据直接返回null

    }

}

可以发现,不管是调用ThreadLocal的set()还是get()方法,都会去清除Map中key==null的Entry数据。

虽然ThreadLocalMap是一个自定义哈希表,但是它与传统的HashMap等哈希表内部结构不一样。ThreadLocalMap内部仅仅维护了Entry[]数组。其中Entry实体中对应的key为弱引用,在第一次放入数据时会初始化数组长度(为16),定义数组扩容阀值(当前默认数组长度的2/3)。

ThreadLocalMap的set()方法主要分为三步:

①计算出当前ThreadLocal在table数组的位置,然后向后遍历,直到遍历到的Entry为null则停止,遍历到Entry的key与当前threadLocal实例的相等,直接更替value;

②如果遍历到Entry已过期(Entry的key为null),则调用replaceStaleEntry函数进行替换。

③在遍历结束后,未出现1和2两种情况,则直接创建新的Entry,保存到数组最后侧没有Entry的位置。

比如:第一种情况, Key值相同,直接覆盖

a89a9abfcae246c392e4d3018c019f16.png

 第二种情况,如果当前位置对应Entry的Key值为null

55f702c8c1de46dd9d38b5ea1ba080b0.png

从图中可以看出,当添加新Entry(key=19,value =200,index = 3)时,数组中已经存在旧Entry(key =null,value = 19),当出现这种情况时,方法内部会将新Entry的值全部赋值到旧Entry中,同时会将所有数组中key为null的Entry全部置为null。

在源码中,当新Entry对应位置存在数据,且key为null的情况下,会走replaceStaleEntry方法:

private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {

    Entry[] tab = table;

    int len = tab.length;

    Entry e;

   //记录当前要清除的位置

    int slotToExpunge = staleSlot;

    //往前找,找到第一个过期的Entry(key为空)

    for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))

        if (e.get() == null)//判断引用是否为空,如果为空,擦除的位置为第一个过期的Entry的位置

            slotToExpunge = i;

    //往后找,找到最后一个过期的Entry(key为空)

    for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {

        ThreadLocal<?> k = e.get();

        //在往后找的时候,如果获取key值相同的,那么就重新赋值。

        if (k == key) {

             //赋值到之前传入的staleSlot对应的位置

            e.value = value;

            tab[i] = tab[staleSlot];

            tab[staleSlot] = e;

            //如果往前找的时候,没有过期的Entry,那么就记录当前的位置(往后找相同key的位置)

            if (slotToExpunge == staleSlot)

                slotToExpunge = i;               

            //那么就清除slotToExpunge位置下所有key为null的数据

            cleanSomeSlots(expungeStaleEntry( slotToExpunge), len);

            return;

        }

        //如果往前找的时候,没有过期的Entry,且key =null那么就记录当前的位置(往后找key==null位置)

        if (k == null && slotToExpunge == staleSlot)

            slotToExpunge = i;

    }

    // 把当前key为null的对应的数据置为null,并创建新的Entry在该位置上

    tab[staleSlot].value = null;

    tab[staleSlot] = new Entry(key, value);

    //如果往后找,没有过期的实体, 且staleSlot之前能找到第一个过期的Entry(key为空),那么就清除slotToExpunge位置下所有key为null的数据

    if (slotToExpunge != staleSlot)

        cleanSomeSlots(expungeStaleEntry( slotToExpunge), len);

}

replaceStaleEntry函数主要分为两次遍历,以当前过期的Entry为分割线,一次向前遍历,一次向后遍历。

主要对四种情况进行了判断,具体情况如下图表所示:

f8a2b3d02beb457e89699f18c816ab02.png

 第三种情况,当前对应位置为null

c7f7db898d3043f99278ea0a2da39c50.png

图上为了方便理解清空上下数据的情况,并没有重新计算位置(希望大家注意!!!)

清除key==null的数据,判断当前数据的长度是不是到达阀值(默认没扩容前为INITIAL_CAPACITY *2/3,其中INITIAL_CAPACITY = 16),如果达到了重新计算数据的位置。

所以,ThreadLocalMap存储的是一个数组,可以用下面的图来表示Thread、ThreadLocal以及ThreadLocalMap之间的关系:

ad7ec8eb4d534511929cff62094ae8e4.png

其中key是当前ThreadLocal对象的Hash,value 是ThreadLocal对象的存储的值。

可以发现整个ThreadLocal的使用都涉及到线程中ThreadLocalMap,虽然在外部调用的是ThreadLocal.set(value)方法,但本质上调用的是线程中的ThreadLocalMap中的set(key,value)方法。

 

总结一下ThreadLocal的原理:

Thread类里面有一个成员变量ThreadLocalMap,ThreadLocalMap类定义在ThreadLocal类内部,ThreadLocalMap类里存储的事Entry数组,Entry是一个键值对,而且它的key是弱引用。当调用ThreadLocal.set()方法时实际上调用的当前线程内ThreadLocalMap对象的set()方法,往Map的Entry数组里添加数据,而ThreadLocalMap只属于该线程独有的成员变量,因此就可以做到数据在线程之间的相互隔离。

存入Entry的值以键值对的形式存在,key为ThreadLocal,value是存入的值,因此当一个线程需要让多个数据隔离的时候,需要new出多个ThreadLocal对象,并存入entry数组(一个线程中是可以有多个ThreadLocal对象的。如果不new多个对象的话,由于key都是同一个ThreadLocal对象,会导致数据覆盖。也就是说一个ThreadLocal对象只能确保一个数据与其他线程隔离)。

所以通过ThreadLocal可以在不同线程中维护一套数据的副本并且相互不干扰。ThreadLocal为不同线程维护了不同的变量副本,是一种空间换时间的策略,提供了一种简单的多线程的实现方式。

3ea2493728634f2fad4d6cd494246cdd.png

 9baf46eaaddf419d8fd37f2d9146ec25.png

 

4.ThreadLocal内存泄露问题

ThreadLocal可能会导致内存泄露。因为往Entry是以键值对的形式存放的,threadlocal为key,存放的需要进行线程隔离的值为value。而Entry的key是WeakReference弱引用类型,因此在GC执行垃圾回收的时候ThreadLocal对象可能会被回收,也就是ThreadLocalMap中的某个key会变为null,此时该map已经无法通过key取到被回收的threadlocal所对应的value,然而map又指向了该value,因此该value不会被回收,该内存区域依然被占据,因此造成内存泄漏。

也就是ThreadLocal存入到ThreadLocalMap之后,如果key被GC回收,这个ThreadLocal对象保存的内容将永远无法被使用,并且由于线程还存活,所以ThreadLocalMap不会被销毁,最终导致ThreadLocal的内容一直在内存里。

c2f1c6c7abb443a6b010f81b6c1f5086.png

设计者在设计上避免了这个问题,就是当再次调用get()、remove()、set()方法时,都会自动清理掉线程ThreadLocalMap中所有Entry用key为null的value,并将整个Entry设置为null,这样在下次回收时就能将Entry和value都回收。也就是说,ThreadLocal会在get和set的时候进行查询,如果发现key被回收了就会清理value,因此value也不是无限增长的情况。

所以,当线程执行完毕之后,如果用到了ThreadLocal,那么开发者要在最后添加ThreadLocal.remove();这样就能保证value被回收而不会引发内存泄露。

 

5.源码中的应用

Handler消息机制中,当在一个线程中使用Looper的时候,需要调用Looper的prepare方法和loop方法,不然就会出现异常。这里Looper的存取就使用到了ThreadLocal。由于Looper的作用域是线程且不同线程之间具有不同的Looper,所以使用了ThreadLocal对Looper与线程进行关联。

看看这两个方法的源码:

public static void prepare() {

    prepare(true);

}

private static void prepare(boolean quitAllowed){

    if (sThreadLocal.get() != null) {

        throw new RuntimeException("Only one Looper may be created per thread");

    }

    sThreadLocal.set(new Looper(quitAllowed));

}

loop方法内部先调用了myLooper方法:

public static Looper myLooper() {

    return sThreadLocal.get();

}

Looper的prepare方法先创建Looper,并使用ThreadLocal存储即与当前的线程进行关联,然后loop方法开启消息机制的时候,使用ThreadLocal方法获取到当前线程的Looper方法。

132d42ec299a4fa0928bec99e3e4fac2.jpg

 

6.使用InheritableThreadLocal继承父线程的局部存储

在业务开发的过程中,可能希望子线程可以访问主线程中的ThreadLocal数据,然而ThreadLocal是线程隔离的,包括在父子线程之间也是线程隔离的。为此,ThreadLocal提供了一个相似的子类InheritableThreadLocal,ThreadLocal和 InheritableThreadLocal分别对应于线程对象上的两块内存区域:

①ThreadLocal字段: 在所有线程间隔离;

②InheritableThreadLocal字段: 子线程会继承父线程的InheritableThreadLocal数据。父线程在创建子线程时,会批量将父线程的有效键值对数据拷贝到子线程的InheritableThreadLocal,因此子线程可以复用父线程的局部存储。

InheritableThreadLocal中可以重写childValue()方法修改拷贝到子线程的数据。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    // 参数:父线程的数据

    // 返回值:拷贝到子线程的数据,默认为直接传递

    protected T childValue(T parentValue) {

        return parentValue;

    }

}

注意:

①InheritableThreadLocal区域在拷贝后依然是线程隔离的: 在完成拷贝后,父子线程对InheritableThreadLocal的操作依然是相互独立的。子线程对InheritableThreadLocal的写不会影响父线程的InheritableThreadLocal,反之亦然;

②拷贝过程在父线程执行: 这是容易混淆的点,虽然拷贝数据的代码写在子线程的构造方法中,但是依然是在父线程执行的。子线程是在调用start()后才开始执行的。

2556da6b8f164c1c973d34f50fc6e396.webp

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值