一、概述

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

TheadLocal东西涉及到的几个类:Thread、ThreadLocal、ThreadLocalMap,对于它们之间的联系咱们能够这样简单理解:每个Thread目标都具有一个独归于自己的Map容器-ThreadLocalMap,这儿咱们先把它理解为HashMap,该容器的效果是存储和保护独归于本线程的值,而它的key值就是TheadLocal目标,value值就是咱们需要存储的Object。

这就是ThreadLocal东西的结构,所以在ThreadLocal东西中真实重要的是ThreadLocalMap,它才是存储线程独有数据的当地。

搞懂ThreadLocal

图片出处

二、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的引证联系图。

搞懂ThreadLocal

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,导致内存走漏。

搞懂ThreadLocal

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()办法移除数据。

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