通过本文你将了解到如下内容
ThreadLocal的用途、场景
常见的实际应用场景
使用ThreaLocal所带来的好处
ThreadLocal原理剖析
主要方法介绍
ThreadLocal源码分析
ThreadLocal使用注意点
ThreadLocal的用途
从其名字我们就可以看到ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
通俗来看:线程存活,ThreadLocal 实例就可以被访问,线程消失,就会被垃圾回收。
ThreadLocal的使用场景
-
1.每个线程需要一个独享的对象,如 线程不安全的工具类,(线程隔离)
例子:比如对线程非安全变量SimpleDataFormate使用的迭代改进。
由于SimpleDataFormate对象属于线程非安全类对象信息,当涉及到对线程情况下使用时需要格外注意。
对于其的改进之路,大致可以经历如下过程:
1.每个线程任务内创建一个SimpleDataFormate对象信息,但这样当面临任务激增时所导致的问题就是内存占用空间太过明显。2.将SimpleDataFormate对象全局化,使用时加入synchronized关键字进行限制,此时虽然避免了大量构建对象带来的内存消耗,但带来时间方面的性能损耗。
3. 使用ThreadLocal来实现,每个线程内共享一个SimpleDataFormate对象信息,这样每个线程内都单独处理,避免了时间损耗和内存性能损耗。
-
2.每个线程内需要保存全局变量,如 拦截器中的用户信息参数,让不同方法直接使用,避免参数传递过多,(局部变量安全,参数传递)
ThreadLocal的优势
-
线程安全,无需进行有锁化操作,提升了程序执行性能。
-
如果在ThreadLocal中保存着全局变量的化,避免了同一线程内传参的繁琐操作,在确保变量安全的情况下,简化了参数的传递过程。
ThreadLocal原理
ThreadLocal内部结构
-
每个 Thread 维护着一个 ThreadLocalMap 的引用;ThreadLocalMap 是 ThreadLocal 的内部类,用 Entry 来进行存储;key就对应一个个ThreadLocal
-
get方法:取出当前线程的ThreadLocalMap,然后调用map.getEntry方法,把ThreadLocal作为key参数传入,取出对应的value
-
set方法:往 ThreadLocalMap 设置ThreadLocal对应值
initalValue方法:延迟加载,get的时候设置初始化
总结来看:Thread是线程,而ThreadLocal是一个线程工具类,ThreadLocalMap则是ThreadLocal维护的一个内部类。
常见方法
set用于赋值操作,get用于获取变量中的值,remove就是删除当前这个变量的值。需要注意的是initialValue方法会在第一次调用时被触发,用于初始化当前变量值。
ThreadLocal原理及部分源码分析
基本存储结构
ThreadLocalMap提供了一种为ThreadLocal定制的高效实现,其也皆在为了存放ThreadLocal对象信息,并提供一种基于弱引用的垃圾清理机制,确保垃圾信息可以回收。
不难发现Entry便是ThreadLocalMap里定义的节点,它继承了WeakReference类,其定义了一个类型为Object的value,用于保存放入ThreadLocal中的值信息。
如果这里使用普通的key-value形式来定义存储结构,实质上就会造成节点的生命周期与线程强绑定,只要线程没有销毁,那么节点在GC分析中一直处于可达状态,没办法被回收,而程序本身也无法判断是否可以清理节点。
弱引用是Java中四档引用的第三档,比软引用更加弱一些,如果一个对象没有强引用链可达,那么一般活不过下一次GC。当某个ThreadLocal已经没有强引用可达,则随着它被垃圾回收,在ThreadLocalMap里对应的Entry的键值会失效,这为ThreadLocalMap本身的垃圾清理提供了便利。
关于引用知识可以参看之前文章信息:《深入理解java虚拟机》——垃圾回收
基本成员变量和方法
通过上述成员信息不难发现:ThreadLocalMap维护了一个Entry表或者说Entry数组,并且要求表的大小必须为2的幂,同时记录表里面entry的个数以及下一次需要扩容的阈值。
同时,ThreadLocal需要维持一个最坏2/3的负载因子,所谓的负载因子就是 存放数据数/总的容量。
由于ThreadLocalMap使用线性探测法来解决散列冲突,所以实际上Entry[]数组在程序逻辑上是作为一个环形存在的。
至此,我们已经可以大致勾勒出ThreadLocalMap的内部存储结构。基本结构如下图所示:其中虚线表示弱引用,实线表示强引用。
ThreadLocalMap维护了Entry环形数组,数组中元素Entry的逻辑上的key为某个ThreadLocal对象(实际上是指向该ThreadLocal对象的弱引用),value为代向该线程ThreadLoacl变量中存入的值。
如果细心看到源码中注释,不难发现在指定Entry容量信息时,注释中给出必须为2的幂次。大致原因如下:
这是因为当表空间长度为2的N次方时,计算元素信息的索引位置可以可以用简单的与操作来代替的取模操作!
以hashMap为例分析: 假设某个对象的hashCode为35(二进制为100011),hashMap采用默认的initialCapacity(16),那么indexFor计算所得结果将会是100011 & 1111 = 11,即十进制的3,是不是恰好是35 Mod 16。 此时就可以快速进行计算hashcode为 35对应的索引信息。
get方法大致流程
从ThreadLocal读一个值可能遇到的情况:
首先根据入参threadLocal的threadLocalHashCode对表容量取模得到index
-
如果index对应的slot就是要读的threadLocal,则直接返回结果
-
调用getEntryAfterMiss线性探测,过程中每碰到无效slot,调用expungeStaleEntry进行段清理;如果找到了key,则返回结果entry
-
没有找到key,返回null
ThreadLocal中的get方法,其核心功能依托于getEntry方法
getEntryAfterMiss:调用getEntry未直接命中的时候调用此方法
Remove方法
基本逻辑:在table中找key,如果找到了,把弱引用断了做一次段清理。
使用注意点
关于ThreadLocal是否会引起内存泄漏也是一个比较有争议性的问题,其实就是要看对内存泄漏的准确定义是什么。
首先明白一点在ThreadLocalMap中entry的key是一个对threadLocal的弱引用,而对value则是一个强引用。
通常认为ThreadLocal会引起内存泄漏的说法是因为如果一个ThreadLocal对象被回收了,我们往里面放的value对于【当前线程->当前线程的threadLocals(ThreadLocal.ThreadLocalMap对象)->Entry数组->某个entry.value】这样一条强引用链是可达的,因此value不会被回收。
如果一个ThreadLocal不被使用,那么实际上set,remove等方法是不会被调用的,如果此时线程不停止,那么调用链一直存在,导致value的内存泄露的发生。
如果认为ThreadLocal不会引起内存泄漏的说法是因为ThreadLocal.ThreadLocalMap源码实现中自带一套自我清理的机制。所以有关于内存泄露的讨论是因为在有线程复用如线程池的场景中,一个线程的寿命很长,大对象长期不被回收影响系统运行效率与安全。如果线程不会复用,用完即销毁了也不会有ThreadLocal引发内存泄露的问题。《Effective Java》一书中的第6条对这种内存泄露称为
unintentional object retention
(无意识的对象保留)。
当我们仔细读过ThreadLocalMap的源码,我们可以推断,如果在使用的ThreadLocal的过程中,显式地进行remove是个很好的编码习惯,这样是不会引起内存泄漏。
那么如果没有显式地进行remove呢?只能说如果对应线程之后调用ThreadLocal的get和set方法都有很高的概率会顺便清理掉无效对象,断开value强引用,从而大对象被收集器回收。
所以使用完ThreadLocal对象后显式调用remove是一种良好的习惯,同时阿里规约中也强力推荐这样去做。
参考文章:ThreadLocal源码解读:https://www.cnblogs.com/micrari/p/6790229.html