ThreadLocal该了解一下了

引言

源码版本:Android10

ThreadLocal

今天我们来谈一谈ThreadLocal,在说ThreadLocal之前,先提出几个问题,带着问题分析ThreadLocal,可能会事半功倍。

  1. ThreadLocal是什么,怎么用?
  2. ThreadLocal的内部原理是什么?
  3. ThreadLocal的应用场景?
  4. 使用ThreadLocal需要注意些什么?

ThreadLoal是什么,怎么用?

ThreadLoal 类提供一个线程本地变量,同一个 ThreadLocal 所包含的对象,通过它的get或者set方法访问到的,在不同的 Thread 中有不同的副本。
这句话是什么意思呢?下面我们来看一下这个例子

val threadLocal = ThreadLocal<String>()
val thread = Thread {
    run {
        println("thread s : ${threadLocal.get()}")
    }
}
fun main() {
    threadLocal.set("1")
    thread.start()
    println("main s : ${threadLocal.get()}")
}

复制代码

上面这段代码的打印结果是:

main s : 1
thread s : null

可以看到,我们在通过threadLocal的set方法,往其内部存储了一个”1″,在主线程get和在子线程get,获取的值,并不一样。这就是刚上面说的,它的get或者set方法访问到的,在不同的 Thread 中有不同的副本的意思。
另外,当我们不用set方法设置,也可以像下面这样,通过复写initialValue()方法,来给其设置一个初始值。

val threadLocal2 = object : ThreadLocal<String>() {
    override fun initialValue(): String? {
        return "2"
    }
}
复制代码

总的来说,ThreadLocal的用法还是比较简单的,一句话总结就是,通过ThreadLocal对象,在主线程设置的值,你要从主线程取出来,从子线程设置的值,你要从子线程中取出来,存取都和当前线程相关。

ThreadLocal的原理

下面我们来看一下它的原理

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的set()方法,可以发现,传入的value值,最终存储到了map中去。接下来,我们看一下这个getMap(t)的实现。

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
复制代码

这个方法很简单,就是通过返回t.threadLocals,首先t是通过Thread.currentThread()获取到的Thread对象,也就是当前运行的线程对象。
下面是Thread类的的threadLocals变量

ThreadLocal.ThreadLocalMap threadLocals = null;
复制代码

查看Thread类代码可知,其实Thread中的threadLocals默认是null,也没有在自身的构造方法中初始化。故第一次调用set的时候,map是空的,那么会进入createMap(t,value)方法。

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
复制代码

可以发现,代码就是给Thread的threadLocals赋值。也可以理解为,这个ThreadLocalMap对象和线程绑定了。以后在这个ThreadLocalMap对象中存储的值都只和绑定的线程相关。

TheadLoclaMap

ThreadLocalMap是什么?
ThreadLocalMap其实就是一个自定义的HashMap,仅仅只适用于存储线程的本地变量。它的key是ThreadLocal对象,它的value是一个Object。注意,说是自定义的HashMap,其实不是内部维护了一个HashMap对象用来存储,而是和HashMap的实现原理类似。

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的构造方法,table是一个Entry数组,INITIAL_CAPACITY是16,即一个可以存储16个元素的数组。
firstKey.threadLocalHashCode & (INITIAL_CAPACITY – 1)是通过与操作,来确定构造的Entry应该插放在数组的那个位置。
感兴趣的可以去看一下,threadLocalHashCode 的值是如何变化的,数组是如何动态扩展的。
这里我们简单提一下,存储的逻辑,详细的可以去研究一下HashMap的源码,非常经典。

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}
复制代码

上面是ThreadLocal对象的get方法,可以发现,取的时候也是通过ThreadLocalMap对象去取的。
到这里我们可以得出结论,ThreadLoal对象的存和取,其实是通过ThreadLocalMap的set和get方法,而一个Thread只关联了一个ThreadLocalMap对象,ThreadLocalMap对象可以存储多个线程相关的本地变量。
那么ThreadLocal永远只能存一个值吗?
这么理解不准确,应该说是一个ThreadLocal对象在一个线程下只能存储一个值。

ThreadLocal的应用场景

日常情况下,ThreadLocal使用场并不多。
但可以把使用场景归纳为:当某些数据的作用域是线程,并且在不同线程间都需要有自己的副本的时候,这时候可以考虑使用ThreadLocal。
比如,Android源码的Lopper、ActivityThread以及AMS中都用到了ThreadLocal。

使用ThreadLocal需要注意些什么

需要注意点就是,ThreadLocal可能引发内存泄漏。
为什么会引发内存泄漏?这就要看ThreadLocalMap中存储数据的内部类Entry

static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}
复制代码

Entry类是ThreadLocalMap中保存的元素类型,Entry构造方法接收一个软引用的ThreadLocal对象的key和一个Object的value。请注意这个ThreadLocal是软引用的。

我们设想一下,当ThreadLocal对象我们不在使用了,将ThreadLocal置为null。这时候,ThreadLocal这一个对象的内存,只有ThreadLocalMap中的Entry的key指向,但是这个key是ThreadLocal的弱引用,所以下次GC的时候,因为是弱引用,这个Entry的key会被置为空。只要线程没结束的话,这个Entry就是一个key为null的对象,从map中永远都取不出来。这就导致了这个Entry的内存被泄漏了,无法释放。

大家想一想,为什么这么设计呢?如何把这个Entry的key设置成强引用?

如果设计成强引用,还是会内存泄漏,而且更加明显。即使你在外部不想使用ThreadLocal对象了,把其置为null,但是ThreadLocalMap一直持有ThreadLocal对象的引用,ThreadLocal对象的内存依旧还是会泄漏。

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}
复制代码

所以使用ThreadLocal对象的时候,如果在某一个线程使用了ThreadLocal存储了数据,当不想在当前线程再用的时候,一定记得调用remove()方法,保持良好的编码习惯。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享