ThreadLocal原理学习

什么是ThreadLocal

ThreadLocal是一种数据类型,类似HashMap的key : value存储。主要用来存储线程独有的变量,各线程间互不干扰。通过get、set方法存入读取数据,操作简单。

常用场景:保存用户登录信息,可以在任意地方获取到用户信息。

ThreadLocal、ThreadLocalMap、Thread

ThreadLocal担任get、set、remove等操作方法。ThreadLocalMap是底层数据结构,存储数据,使用动态数组Entry[]实现,它是ThreadLocal的一个内部类。Thread维护了一个类变量ThreadLocal.ThreadLocalMap,其他线程不可访问。

数据结构

引用自: https://blog.csdn.net/moakun/article/details/79911989

Thread维护threadLocals变量,threadLocals是线程变量,保证了各线程间不会相互干扰。ThreadLocalMap是一个默认大小16的Entry数组,虽然它也是一种Map结构,但它并没有实现Map接口。执行set方法时对ThreadLocal对象进行hash算法,决定在数组中的存储位置,当发生hash冲突时会直接往数组的后面进行移位,当到达数据最后一位时还没有找到空位置刚从数组首位继续查找可存储位置。当数组长度达到负载因子2/3时会进行扩容操作。

为什么要用ThreadLocalMap

ThreadLocal的set方法只有一个value参数,为什么要用key : value的Map形式数据结构呢?

一个ThreadLocal虽然只属于一个线程所有,但一个线程可以有多个ThreadLocal对象,为了避免多个ThreadLocal之前的干扰,ThreadLocalMap需要以ThreadLocal对象为key来存储。

内存泄露

首先我们明白,强引用在发生gc时不能回收的,当某个对象只有弱引用时,发生gc时该对象是可以被回收的。

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

    Entry(ThreadLocal<?> k, Object v) {
	    // 这里key使用的是父类弱引用的构造方法
        super(k);
        value = v;
    }
}

通过源码我们可以知道,ThreadLocalMap中Entry的key就是一个弱引用,而value是强引用,jdk开发者为什么这样设计呢,通过下面这个图很好明白为什么要这样设计。

r1、r2、r3、r4、r5代表强引用,r6代表弱引用

当线程A启动时在Stack中会创建一个线程栈,并指向在Heap中创建的Thread对象。Thread对象引用了ThreadLocalMap对象。当方法2开始使用ThreadLocal时,会在Heap中创建创建一个ThreadLocal对象当调用ThreadLocal的set方法后会在Heap中创建一个Entry对象,entry的key指向ThreadLocal对象,使用的是弱引用,value指向Heap中创建的新的value对象。当方法栈2执行完成后r1引用这会失效,理论上ThreadLocal对象和Entry对象都可以被回收了。但因为Entry对象被ThreadLocalMap引用,所以Entry对象是不能被回收的,若r6为强引用的话,当发生gc时ThreadLocal对象是不能被回收的,当r6使用弱引用时ThreadLocal对象则可以顺利被回收,所以entry中的key使用弱引用保证了ThreadLocal对象可以及时被回收掉。那Entry中的value为什么不使用弱引用呢,因为value对象只有Entry一个引用,如果使用弱引用,则可能在方法栈2还未执行完成时就发生gc导致value被回收出现空指针异常。正因为Entry对象在方法栈2执行完成继续执行方法栈3时不能被及时回收,所以存在内存泄露问题。ThreadLocal的get、set、remove方法帮我们清理了Entry中key为null的对象(即删除了r4引用),保证value对象可以被及时回收。故我们在使用完ThreadLocal后要调用remove方法,保证不会出现内存泄露。

注:我们要在不使用某个ThreadLocal对象后,手动调用remoev方法来删除它,尤其是在线程池中,不仅仅是内存泄露的问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果我们不手动调用remove方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug。


版权声明:本文为z562743237原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。