注册

搞懂ThreadLocal

前言

ThreadLocal可以说是面试的常客了,虽然在日常开发中用到的次数并不多,但因为其在Handler、ActivityThread中都发挥着重要的作用,使得面试官在问其他问题的时候会顺便考查一下ThreadLocal。为了彻底理清其逻辑,这里系统的整理一遍ThreadLocal结构,帮助自己理解。

一、概述

在分析ThreadLocal之前先不要看源码,我们先来大致建立起关于ThreadLocal整体的认知。

TheadLocal工具涉及到的几个类:Thread、ThreadLocal、ThreadLocalMap,对于它们之间的关系我们可以这样简单理解:每个Thread对象都拥有一个独属于自己的Map容器-ThreadLocalMap,这里我们先把它理解为HashMap,该容器的作用是存储和维护独属于本线程的值,而它的key值就是TheadLocal对象,value值就是我们需要存储的Object。

这就是ThreadLocal工具的结构,所以在ThreadLocal工具中真正重要的是ThreadLocalMap,它才是存储线程独有数据的地方。

图片出处

二、ThreadLocal是什么

看了概述之后你其实已经对ThreadLocal有了一个大致的认知了,但是仅仅这些还不够,还需要更加深入的了解ThreadLocal。

ThreadLocal,即线程的本地变量,设计目的是为了让线程中拥有属于自己的变量,主要用于线程间数据隔离,是用来解决线程安全性问题的一个工具。它相当于为每一个线程都开辟了一块内存空间,用来存储共享变量的副本,每个线程访问共享变量时只能去访问和操作自己共享变量的副本,从而避免多线程竞争同一个共享数据,保证了在多线程环境下各个线程里的变量相对独立于其他线程内的变量。

在这里所谓开辟的内存空间就是 ThreadLocalMap,共享变量就是 ThreadLocal,共享变量的副本就是存储到ThreadLocalMap中的key。

//创建一个ThreadLocal共享变量
static final ThreadLocal<String> sThreadLocal = new ThreadLocal<String>();

创建一个ThreadLocal修饰的共享变量,当线程访问该共享变量时,这个线程就会在自己的成员变量ThreadLocalMap中保存一份数据副本,多个线程操作这个变量的时候,实际是在操作自身线程本地内存里面的变量,从而起到线程隔离的作用,避免了并发场景下的线程安全问题。

三、Thread源码分析

class Thread implements Runnable {
...
ThreadLocal.ThreadLocalMap threadLocals = null;
...
}

每个线程都有一个成员变量-ThreadLocalMap,但是该变量并没有设置引用,也就是说内存并没有为它分配空间,它的引用实际是在ThreadLocal#set方法中设置的,这样的话,虽然每个Thread对象都会有一个ThreadLocalMap变量,但是只有在使用ThreadLocal工具实现线程数据隔离的时候才会实例化,不使用则不会实例化,避免了内存占用。

四、ThreadLocal源码分析

既然每个Thread对象都有一个属于自己的容器ThreadLocalMap,那么对于数据的管理无外乎添加、获取、删除,也就是就是set、get、remove,但是这些操作并不是线程直接对ThreadLocalMap进行,而是通过ThreadLocal来间接实现的,ThreadLocalMap是ThreadLocal的静态内部类

1、ThreadLocal#set()

    public void set(T value) {
//获取当前线程对象
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);//this表示当前ThreadLocal对象
else
createMap(t, value);
}

//获取thread对象的成员变量ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

首先会获取当前线程的变量ThreadLocalMap,如果该变量为null,那么会调用createMap方法初始化ThreadLocalMap,如果不为null,则调用ThreadLocalMap#set方法将数据存储起来。

2、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();
}

private T setInitialValue() {
T value = initialValue();//initialValue方法会返回null
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}

与set方法同理,首先会获取ThreadLocalMap,根据ThreadLocalMap是否为null来进行操作。如果不为null,则根据key值-ThreadLocal对象直接从ThreadLocalMap中取值并返回。如果为null,则调用setInitialValue方法,该方法逻辑几乎和set方法相同,不同的是value值为null,所以最终返回的也是null。

从上面的方法中我们可以看到不管是set方法还是get方法,都会先获取当前的Thread对象,然后获取Thread对象的成员变量ThreadLocalMap,最终对Map进行操作,这样也就保证了所有操作都是作用在Thread对象的同一个ThreadLocalMap上。

五、ThreadLocalMap

static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}

ThreadLocal没有直接使用HashMap而是自己重新开发了一个 map,最主要的作用是让它的key为虚引用类型,这样当ThreadLocal对象销毁时,多个持有其引用的线程不会影响它的回收。 ThreadLocalMap是一个很像HashMap的数据结构,但他并没有实现 Map接口,而且它的 Entry是继承WeakReference的,也没有 next 指针,所以不存在链表。对于hash冲突,采用的是开放地址法来进行解决。 ThreadLocaMap的扩容机制也不同于HashMap,ThreadLocalMap的扩容阈值是长度的2/3,当表中的元素数量达到阈值时,不会立即进行扩容,而是会触发一次rehash操作清除过期数据,如果清除过期数据之后元素数量大于等于总容量的3/4才会进行真正意义上的扩容。

六、ThreadLocal的内存泄漏

我们都知道内存泄漏必然和对象的引用有关,先来看一下ThreadLocal的引用关系图。

image.png

Thread中的成员变量ThreadLocalMap,它里面的key指向ThreadLocal成员变量,并且是一个弱引用。

1、为什么Entry的key使用弱引用?

如果 Entry 的key为强引用,则会导致ThreadLocal对象在被创建它的线程销毁时,由于ThreadLocalMap的持有而导致ThreadLocal对象无法被回收,进而导致严重的内存泄漏问题,因此Eetry的key被声明为弱引用来避免这种问题

2、ThreadLocal弱引用下为什么会导致内存泄漏?

所谓弱引用,是指对象允许在这种引用关系存在的情况下被GC回收。

前面也说过,ThreadLocalMap中的key是一个弱引用,当ThreadLocal变量被设置为null,即此时ThreadLocal对象仅有一个弱引用-key,而没有任何外部强引用关系。发生一次系统GC后,ThreadLocal对象会被GC回收,key的引用就变成一个null,导致这部分内存永远无法被访问,造成内存泄漏的问题。因此这些value就会一直存在一条强引用链: Thread变量 -> Thread对象 -> ThreaLocalMap -> Entry -> value -> Object 无法回收,造成内存泄漏。

所以说,从ThreadLocal本身的设计来看,是一定存在内存泄漏的。有的朋友可能会说不会出现内存泄漏啊,如果线程被回收了,线程里面的成员变量也都会被回收,也就不存在内存泄漏了,这是不对的。首先,在线程执行期间,始终有一块无法访问的内存被占用。其次,我们在实际开发中多数情况下使用线程池,而线程池是重复利用的,线程池不会销毁线程,那么线程中会一直存在这种类型的value,导致内存泄漏。

image.png

3、如何避免内存泄漏

既然已经知道弱引用下内存泄漏的原因,那么解决方案也就很清晰了,将不再被使用的Entry及时从线程的ThreadLocalMap中删除,或者延长ThreadLocal的生命周期。

而删除不再使用的Entry有两种方式。

  • 主动清除:使用完ThreadLocal后,手动调用ThreadLocal#remove()方法,将Entry从ThreadLocalMap中删除。
  • 条件触发清除:当然,为了避免内存泄漏的问题,ThreadLocal也做了一些工作。ThreadLocalMap拥有自动清除机制去清除过期Entry,当调用ThreadLocalMapget()、set()对数据进行读写时,都会触发对Entry里面key为null的数据的清除。

我们也能看到系统自动清除是需要一定的触发条件的,不能完全避免内存泄漏,所以正确的做法是调用ThreadLocal#remove()主动清除。

还可以将ThreadLocal声名为private static,使它的生命周期与线程保持一致,保证一直存在与之关联的强引用。

总的来说,有两个方法可以避免内存泄漏

  1. 每次使用完ThreadLocal之后,主动调用remove()方法移除数据。
  2. 扩大成员变量ThreadLocal的作用域,把ThreadLocal声名为private static,使它无法被GC回收。这种方法虽然避免了key为null的情况,但是如果后续线程不再继续访问这个key,也就会导致这个内存一直占用不被释放,最后造成内存溢出的问题。

所以说来说去,最好的方式还是在使用完之后,调用remove方法去移除掉这个数据

七、总结

  • ThreadLocal为每一个线程创建一个ThreadLocalMap,用于存储独属于线程自己的数据。
  • ThreadLocal的设计并不是为了解决并发问题,而是解决变量在线程内部的共享问题,线程内部可以访问独属于自己的变量。
  • 因为每个线程都只会访问自己ThreadLocalMap 保存的变量,所以不存在线程安全问题。
  • 为了避免ThreadLocal造成的内存泄漏,最好在每次使用完ThreadLocal之后,主动调用remove()方法移除数据。

个人能力经验有限,文章如有错误,还望指正。


作者:zkl687
链接:https://juejin.cn/post/7248606302913642555
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册