彻底理解ThreadLocal、ITL、TTL

懒人改变了世界,简化了世界,却隐瞒了世界的真相.

大家好,今天跟大家剖析一下ThreadLocal.

文章目录

  1. 痛苦的回忆
  2. 抛几个问题
  3. 前奏铺垫
  4. 正片详解

痛苦的回忆

不知道大家有没有面向JDBC编程的经历.如果有的话,可以回想一下那是一种怎样的体验;如果没有,也可以假装有这样的经历,然后可以构想一下那是一种什么体验.
我体验过.太痛苦了!

由于要面向对象编程,我们的业务逻辑要写在Service层,然后一个Service内经常会用到多个dao层方法.
因为我们每次操作都要打开数据库连接,为了不经常打开/关闭数据库连接,我们需要在service层打开数据库连接,然后把数据库的connection传到dao层.service层处理完之后,再统一关闭数据库连接.
数据库连接是稀缺资源嘛.
注意:我们做了两件事:
1在service层的方法都要打开数据库连接,finally里面关闭连接.
2每个dao层接口,都要加一个connection参数.
每一个Service类的方法,都要写这么多程式化的代码,想想这个操作过程,再对比现在大家都在使用的简洁易用的SpringBoot,有多痛苦不言而喻了.

虽然SpringBoot以及这些类似的框架,简化了我们的工作量,但是我们需要知道,程序实际执行的时候,没一个打开连接、关闭连接的操作都是不能省略的,那框架是如何实现省略程式化开发,专注业务代码的呢?
动态修改字节码的AOP+ThreadLocal.
这里我就不给大家做实现了,相信大多数人也知道怎么玩.也欢迎你留言区一起讨论.

当然,ThreadLocal在做项目开发时,也会有很多应用.
比如,经过软负载的服务,有一些是存在状态的,当然实现方式有很多啦,假如使用请求携带token的方式,那么可以考虑ThreadLocal,这样在这次请求中,无论是验签,还是其他地方使用.利用ThreadLocal,你都可以少传递一个参数.

抛几个问题

我们是如何使用ThreadLocal的?
ThreadLocal如何工作的?
系统中的异步,基本都会依赖线程池,threadLocal和线程池可以同时使用么?
什么情况下会有内存泄漏的问题?
使用线程池+threadLocal时,主线程和子线程可以实现数据隔离么?
主线程和子线程可以实现数据共享么?
ThreadLocalMap内,为什么key做成弱引用,而value做成强引用?
InheritableThreadLocal 关于父子线程,ThreadLocal是如何处理的?

前奏铺垫

1、本文研究范围仅限Java8.
2、ThreadLocalMap
ThreadLocal内部维护了一个ThreadLocalMap静态内部类.ThreadLocalMap内部又维护了一个Entry静态内部类.静态内部类的作用,无疑就是对数据进行封装呗,不对外暴露.
我之前看源码时,经常会见到内部类 VS 静态内部类,因此我对此进行了较为详细的阐述:
https://blog.csdn.net/liu765023051/article/details/100884988

ThreadLocalMap提供了一种为ThreadLocal定制的高效实现,并且自带一种基于弱引用的垃圾清理机制。
ThreadLocalMap提供一个名为table的Entry数组.
Entry类内,使用ThreadLocal对象作为key,key为弱引用,变量值存储为value,value为强引用.

在这里插入图片描述

如图. 可知:ThreadLocalMap其实就是提供了一个名为table的Entry数组.该table为强引用,entry内key为ThreadLocal对象,且为弱饮用;entry内value为存储变量值,且为强引用.

3、ThreadLocalMap VS HashMap

在数据结构中,哈希是非常高效的一种思路.应用也非常广泛.数据库引擎有哈希索引,分库分表可以利用哈希进行,负载均衡都会有一种策略叫哈希策略等等.哈希随处可见.
哈希在JDK里面,实现时也是信手拈来.尽管实现方式有所不同,但是思路都是一样的,哈希算法有两个关键点:如何做散列的哈希算法、冲突后如何处理的冲突算法.
下面对比下ThreadLocalMap VS HashMap的冲突算法.

HashMap的数据结构比较清楚吧,数组+单链表.哈希冲突时,通过单链表解决哈希冲突.
即散列冲突时,遍历链表,以定位数据.

ThreadLocalMap是如何做散列的呢?
ThreadLocalMap使用一个名为table的Entry数组,但是Entry没有使用单链表,而是使用了线性探测的方式,解决哈希冲突.
所以,这个Entry数组,其实是一个逻辑环.
(线性探测自己去了解一下哦,此处不细讲)

环形示意图:

环形逻辑:

/**
 * 负载因子为2/3,初始容量默认16,设置扩容阈值.
 */
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

/**
 * 获取下一个元素索引
 */
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

/**
 * 获取上一个元素索引
 */
private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}

4、调试ThreadLocal源代码时,尽量要创建新线程,在新线程内调试.原因很简单.线程对象里面有ThreadLocalMap对象的引用.
Main函数是主线程,运行时,会初始化主线程需要的ThreadLocalMap信息.对你Debug算是一种干扰.

正片详解

什么是ThreadLocal?
顾名思义,ThreadLocal可以理解为线程本地变量。也就是说如果定义了一个ThreadLocal,每个线程往这个ThreadLocal中读写是线程隔离,各个线程之间是不会影响的。
它提供了一种将可变数据,通过每个线程有自己的独立副本 ,从而实现线程封闭的机制。

ThreadLocal大致的实现思路是怎样的?
Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap。
ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal,value为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用)。每个线程在往某个ThreadLocal里设置值的时候,都会往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。

其实理解ThreadLocal有一个弯,也是ThreadLocal有意思的地方:
一个线程内,可以使用多个ThreadLocal;多个线程间完全隔离.

怎么理解呢?
一个线程内,可以使用多个ThreadLocal.
一个线程对象内部,维护了一个ThreadLocalMap对象,该对象内 存储了该线程内所有创建的ThreadLocal.
ThreadLocalMap对象内,使用ThreadLocal对象做Key,相应的待存储的变量为value.这样就实现了一个线程内,可以使用多个ThreadLocal变量.这多个ThreadLocal变量通过一个ThreadLocalMap维护.

多个线程间完全隔离.
一个线程对象内部,维护了一个ThreadLocalMap对象,即 多个线程,会分别维护他们自己的ThreadLocal对象.通过对象隔离,实现数据隔离.

ThreadLocal是如何工作的?
线程对象thread,持有一个ThreadLocalMap对象,ThreadLocalMap对象内持有entry数组,名为table,
entry内key就是ThreadLocal对象,value为 相应的变量value.

所以,可以通过ThreadLocal对象,获取value 的值,这个实现过程等价于该线程对象所保存的值.

在这里插入图片描述

如图, 该线程对象ThreadObjA,成员变量threadLocals持有强引用,即ThreadLocalMap,
threadLocals跟ThreadLocalMap是聚合关系,注意是聚合,不是组合.关于这组关系,本文不细讲,以后讲设计模式的时候,碰到这组关系时,好好讲讲.
ThreadLocalMap对象持有名为table的Entry数组,每个entry又用弱引用ThreadLocalRef1做key,变量作为value.

此外,该线程对象内,又创建了threadLocalRef1和threadLocalRef2两个对象.
易知:线程执行完毕,方法出栈.该线程内的变量threadLocalRef1和threadLocalRef2,以及线程对象的成员变量threadLocals,它们的强引用均消失.
那么垃圾回收时,从GCRoot分析可达性时,该ThreadLocalMap和ThreadLocalObj对象率先失去强引用.
先分析ThreadLocalMap,垃圾回收时优先会被回收,其次是名为table的entry数组,然后是value值.
再看ThreadLocal对象,随着线程对象消亡,ThreadLocal对象也会优先失去强引用.仅仅被entry持有弱引用.
什么是弱引用呢?简单了解下,在垃圾回收器线程扫描内存区域时,一旦发现弱引用对象,不管当前内存空间是否足够,均会回收它的内存.但是,由于垃圾回收器是一个优先级很低的线程,因此不一定很快发现哪些具有若引用的对象.
总结一下:
Entry的key回收:就是失去了强引用的ThreadLocal对象,只剩下Entry的一条弱引用,随时可能会被回收掉.
Entry的value回收:因为存在一条从当前线程对象连接过来的一条强引用. 只有当前thread结束以后, 当前线程对象就不会存在栈中,强引用断开, 当前线程对象, ThreadLocalMap, value将全部被GC回收.

先总结一下这个例子
创建一个ThreadLocal对象,其实有两条引用指向了ThreadLocal对象.一个外引用,一个内引用.
在这个例子中,这个线程执行方式时,创建了两个ThreadLocal对象,其引用分别是:threadLocalRef1和threadLocalRef2.这两个引用可以看做是外引用.
内引用是指Thread–>ThreadLocalMap–>Entry–>Value这一条链.如图所示.共有weakRef1和weakRef2两个弱引用.

这是正常情况,对吧.那如果发生了异常线程并未退出呢?
如果线程执行时发生了异常,导致自己并未退出,那么该线程持有的成员变量threadLocals,以及内部创建的对象引用ThreadLocalRef,它们拥有的是强引用,均不会释放,那么该ThreadLocal在GC线程回收时,就会逃过GCroot追溯.
如果这样的线程越变越多,且内部的ThreadLocal及其相关的对象,每次都能比过gc线程的回收操作,那么就会导致了内存泄漏.

那线程池+ThreadLocal呢,真的会导致内存泄漏么?
极少情况,存在内存泄漏的可能.

1、正常情况下,会出现脏读.
如果是线程池在执行.我们知道,线程池内的线程,是会重复执行任务的.即线程池内某个线程在执行完某个任务之后,没有立即消亡,而是去执行另一个任务.
这时如果直接执行get方法,那么它就会读到上一个任务中的set的值,即脏数据.
注意,这时该线程对象所持有的强引用并未释放,即线程对象内的ThreadLocal对象,仍然持有强引用.
所以线程池+ThreadLocal,使用不当的话,会导致脏读.

所以,在使用线程池时,使用完ThreadLocal,应该调用threadLocal.remove方法,或者设置threadLocal的变量 为null.便于gc线程回收.
同时,有可能会有脏读,但是不会出现内存泄漏.

2、那在使用线程池时,使用ThreadLocal,什么时候会导致内存泄漏呢?
正常情况下,线程对象都会被gc线程回收(同时相关threadLocalMap、entry、key、value等对象都会被回收),那这时就不会出现内存泄漏.
但是在ThreadLocal设为null,但是线程尚未结束,且要存活一段时间,那这时慢慢会达到一种:作为Entry的key的ThreadLocal对象已经回收了,为null了;而value却随线程存活而一直存在.
此时,我们假设该线程曾创建了10个ThreadLocal对象,那么此时,就会有10个Entry对象. 此时每个Entry对象key为null,value不为空.

此时,如果我们项目里面有了很多很多这样的线程,就有可能发生内存泄漏.

此时有几个注意点:
1、如果key不设置成null,那么就不构成内存泄漏了,因为这种情况,栈内引用持有着ThreadLocal的强引用,会被认为是该ThreadLocal还在被正常使用;
2、此时,这样很多很多的线程,同时 不使用threadLocal的方法.即:get、set、remove方法.
因为ThreadLocal开发者在value的可用性与内存泄漏的可能性之间做了权衡 ,即分别在get、set、remove方法内,对key为null的Entry进行了处理.即如果你调用了任意一个threadLocal对象的get、set、remove方法,就会检查相应key为null的Entry,并进行清理.
3、更多情况,ThreadLocalMap内会一直累加之前存储过的KV,即过多无意义的Entry对象.少数使用不当的情况会发生脏读.

在ThreadLocalMap中,Entry中的key,即ThreadLocal为什么要使用弱引用呢?
这就比较好理解了.
因为ThreadLocal在没有外部强引用来引用它的时候,那么ThreadLocal可以被GC线程回收.
如果Entry中的key即,ThreadLocal为强引用,那么该ThreadLocal对象,只有在ThreadLocalMap、table都被回收了之后,才有可能被回收.即ThreadLocalMap不会回收,ThreadLocal永远不会被gc线程回收.
所以,此处设计成弱引用,提前了ThreadLocal的可gc时间.

那好,现在ThreadLocal使用完了,执行了threadLocal==null操作,那么该ThreadLocal就只剩下这个一个内部entry的弱引用了,可以随时被gc线程回收了.
ThreadLocal被回收完之后,那么此时就会出现一个Entry中key为null,value却存在的情况.
如果该线程生活周期很长,那么ThreadLocalMap对象一定一直存在.因为该线程对象内部的持有ThreadLocalMap的强引用.这些key为null的entry就会存在一条强引用链的关系:
Thread–>ThreadLocalMap–>Entry–>Value.这条强引用链会导致Entry、value、ThreadLocalMap都不会被回收,有可能会造成内存泄漏.上面已经分析过这种情况.

那为什么Entry中的value,即变量值要使用强引用呢?
这也是JVM团队严谨的考虑吧.像上面,执行了threadLocal==null操作之后,threadLocal对象只剩下一个弱引用,可能随时被gc线程回收.
但是这时,注意这时,你的ThreadLocal使用完了,但是你的业务逻辑并未处理完,你的程序仍继续运行,并且还会使用该value值,那这时候,该value仍然可以使用.
JVM设计的工具类,考虑的角度,确实很全面(真的是把使用者当猪一样啊),不过这种理念设计出来的软件,确实才更加严谨.

OK,value使用强引用有道理.那么此处的value什么时候回收呢?
首先,Thread线程对象销毁了,那么这一个null链条肯定有机会被gc线程回收了;
然后你看源码会发现,当你调用ThreadLocal的get、set、remove方法时,它都回去检查key为null的entry.key为null时,释放value对象所占空间.
此处这样做,也是极大可能避免垃圾堆积吧.
此时,设置Entry的key=null,基本就可以保证不会造成内存泄漏了.
比较好理解吧.key=null了,ThreadLocal被回收了,只要你执行该线程上其他ThreadLocal对象的get、set、remove方法,都会去处理对应的key为null时的Entry对象,释放其空间.

线程池+ThreadLocal,推荐使用方式
不过,做好使用ThreadLocal的方式,还是在使用完之后,在finally里面调用一下threadLocal的remove方法.
主动删除entry,完全避免因ThreadLocal可能导致内存泄漏的可能性.提前threadLocal的gc时间.

InheritableThreadLocal 关于父子线程,ThreadLocal是如何处理的?

ThreadLocal声明的变量是线程私有的成员变量,每个线程都有该变量的副本,线程对变量的修改对其他线程不可见.

InheritableThreadLocal声明的变量同样是线程私有的,但是子线程可以使用同样的InheritableThreadLocal类型变量从父线程继承InheritableThreadLocal声明的变量,父线程无法拿到其子线程的。即使可以继承,但是子线程对变量的修改对父线程也是不可见的。

/* ThreadLocal values pertaining to this thread. This map is maintained

  • by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

/*

  • InheritableThreadLocal values pertaining to this thread. This map is
  • maintained by the InheritableThreadLocal class.
    */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

ThreadLocal VS InheritableThreadLocal VS TransmittableThreadLocal

ThreadLocal线程变量副本,实现线程隔离,也可以实现父子线程隔离;

InheritableThreadLocal子类,可以访问到父类ThreadLocal的变量值.即父线程修改的值,可以使用InheritableThreadLocal子类,使子线程获取到父类更新的变量;子线程只能用,修改了不会影响父线程的东西.
严谨一些的说法是 :在创建子线程时,将父线程当前存在的本地线程变量拷贝到子线程的本地线程变量中。

具体是如何操作的呢?
在创建 Thread 类对象时,将父线程中的 table 对象以复制的方式赋值给子线程的 table 数组.也就是说当子线程对象创建完毕后,子线程中的数据就是主线程中旧的数据,主线程使用新的数据时,子线程还是使用旧的数据,因为主子线程使用两个 Entry[] 对象数组各自存储自己的值.
此时需要看一下代码:
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];

for (int j = 0; j < len; j++) {
    Entry e = parentTable[j];
    if (e != null) {
        @SuppressWarnings("unchecked")
        ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
        if (key != null) {
            Object value = key.childValue(e.value);
            Entry c = new Entry(key, value);
            int h = key.threadLocalHashCode & (len - 1);
            while (table[h] != null)
                h = nextIndex(h, len);
            table[h] = c;
            size++;
        }
    }
}

}

第13行,调用了key.childValue方法.这里涉及了Java的值传递. 默认实现采用了浅拷贝.就是不管父线程value里面存储的是基本数据类型还是对象类型,都直接赋值到子线程对应的Entry内.

稍微解释一下:

对于对象类型来说,值的内容其实是对象的引用。当在父线程中修改对象的某一属性,由于子线程引用着相同对象,所以可以子线程可以感知到,本质上父线程和子线程在操作同一块内存地址,即同一个value。

对于基本数据类型来说,由于传递的是值,在父线程改变了数据后,子线程依旧使用的是旧的数据。
这里尤其要提 String 字符串,String 虽然不是基本数据类型,但是由于内部字符数组被 final 修饰带来的不可变型,
当父线程修改其 String 类型数据时,等于替换掉该 String 对象,而并不是修改原 String 对象的值.
所以子线程创建后,父线程值修改时,子线程内的value依旧不会发生变化。

这里的key.childValue()方法,是JDK的默认实现,在创建InheritableThreadLocal时,可以由使用者自行重写.

由的实现,可知其有局限性.父线程变量 复制到子线程的时机,是在子线程创建的时候.
InheritableThreadLocal + 线程池时,线程池中的线程拷贝的数据来自于第一个提交任务的外部线程.
即后面的外部线程向线程池中提交任务时,子线程访问的本地变量都来源于第一个外部线程,造成线程本地变量混乱.

TransmittableThreadLocal对此做了改进.在线程池模式下,也可以很好的将父线程本地变量传递下去.

TransmittableThreadLocal有哪些应用呢?官方给出了应用场景:
分布式跟踪系统 或 全链路压测(即链路打标)
日志收集记录系统上下文
Session级Cache
应用容器或上层框架跨应用代码给下层SDK传递信息

InheritableThreadLocal不支持线程池的根本原因是InheritableThreadLocal是在父线程创建子线程时复制的,由于线程池的复用机制,“子线程”只会复制一次。
不实用线程池时,没有问题.
要支持线程池中能访问提交任务线程的本地变量,其实只需要在父线程向线程池提交任务时复制父线程的上下环境,那在子线程中就能够如愿访问到父线程中的本地变量,实现本地环境变量在线程池调用中的透传这也就是TransmittableThreadLocal最本质的实现原理.

TransmittableThreadLocal是阿里实现的.具体的实现也很精妙.
我们一起来简单看一下.
TransmittableThread继承了InheritableThreadLocal,并丰富了它的功能.
InheritableThreadLocal是在子线程创建的时候,将父线程内变量,复制给子线程,所以出现了上述的问题:线程池+InheritableThreadLocal,导致变量混乱.
TransmittableThreadLocal相比于InheritableThreadLocal,做了改进:
1、TransmittableThreadLocal内部保存了父线程的上下文,即名为holder的全局静态变量.在执行get、set时,会将对应的value,维护在holder中;
2、在向线程池提交任务时,会通过重放、和恢复,帮助子线程同步主线程的变量值.

总结

本文试图站在一个大而全的角度,分析一下ThreadLocal以及相关的实现.
希望对你有所收获.

引用:
git官网:https://github.com/alibaba/transmittable-thread-local
源码:https://github.com/alibaba/transmittable-thread-local/blob/master/src/main/java/com/alibaba/ttl/TransmittableThreadLocal.java
不错的文章:https://mp.weixin.qq.com/s/a6IGrOtn1mi0r05355L5Ng

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值