彻底搞懂

编程入门 行业动态 更新时间:2024-10-10 07:25:17

彻底搞懂

彻底搞懂

一. 设计原理

可以通俗的理解,方法里有方法的局部变量、类里有类的局部变量,而线程也可以有线程的局部变量,他就是ThreadLocal。每个线程访问ThreadLocal变量时,都是访问自己内部的变量副本,线程之间无法互相访问

ThreadLocal的设计思路其实是应用了线程封闭的思想,来解决线程间的对象安全问题,将对象封闭在线程内部,也就不存在并发,对象就是安全的。

由此我们可以想到,为了实现这个目的,每个线程内部,应该持有一个属于自己的Entry,我们可以对它进行set get操作。实际上的设计也确实如此,但我们不可能只设计一个Entry,那他的作用就太小了,而是应该设计一个Map,我们可以在这个Map中随意的放置Entry。

好,存储方法设计完成后,我们还需要一个特殊的声明方法,让每个线程在操作时能够知道,我们声明的变量,要求每个线程放在它自己的Map中,考虑到这种类型的变量,是“线程本地”的,所以就叫它ThreadLocal。使用时,我们通过施加泛型,如ThreadLocal来标识真实的数据类型。有了它以后,我们的Entry中的key就不需要单独定义了,只需要将ThreadLocal对象作为Entry的key就可以了。

每个线程使用这个ThreadLocal变量时,应当首先看自己有没有创建Map对象,如果没创建,就创建一个,如果已经创建了,就直接把ThreadLocal对象作为key,放置在自己的私有Map中即可。就像下边这样:

    public static void main(String[] args) {ThreadLocal<String> name = new ThreadLocal<>();new Thread(() -> {name.set(Thread.currentThread().getName());System.out.println(name.get());}).start();System.out.println(name.get());}

非常完美,下边我们从内存模型角度画个图,梳理一下上边的设计:

1.每个线程Thread内部持有一个ThreadLocalMap,其中有一个Entry数组,里边存储着所有的线程私有Entry。
2.每个Entry中,key是ThreadLocal对象的引用,value则是对真实对象值的引用。
这样,我们就达成了目的:每个线程对ThreadLocal变量操作时,实际上都是操作的自己内部的变量副本,线程将对象封闭在自己内部,实现对象安全。

二. 问题分析

上边这个设计看起来很完美,但我们需要再仔细思考一下,我们上边只考虑了set、get对象的场景,可当我们不需要再使用这个ThreadLocal对象时,会怎样呢?

我们肯定首先希望jvm回收掉ThreadLocal对象,假设我们操作ThreadLocal的引用 = null,希望通过不可达使ThreadLocal对象被回收,但事实会这样吗?不会,因为ThreadLocal还在被Entry的key引用,因此,官方在设计Entry的key与ThreadLocal对象的引用关系时,设计成了弱引用,使得ThreadLocal对象很容易被回收。回收掉ThreadLocal对象以后,其对应在Entry中的key自然也会变成null。那这个Entry会被回收吗?

答案也是否定的,因为Entry对象还被ThreadLocalMap引用,ThreadLocalMap又被Thread引用。这意味着,只要线程不关闭,这些已经分配的value就不会被回收,并且也永远访问不到,有相关基础的同学这时应该已经意识到,这就是内存泄漏。

官方设计ThreadLocal时,自然也考虑到了这一问题,所以他们在ThreadLocal中所有的操作,如set、get、remove时,都会去自动清理这些key为null的entry,以此缓解内存泄漏问题。当然这一切也依赖我们良好的使用习惯,那就是用完ThreadLocal对象后,记得要remove()掉.

注意: Entry中的key与ThreadLocal对象引用关系设计成弱引用,以及ThreadLocal各种操作自动清理key为null的Entry,都是官方为了缓解内存泄漏而做的补救,有些文章认为弱引用是导致内存泄漏的原因,这是不对的。

三. 应用场景

ThreadLocal设计的如此复杂,甚至有内存泄漏风险,那它是为了什么而设计的呢?我们在什么场景下可以使用它呢?
首先,ThreadLocal的使用场景非常明确,它适用于那些,需要在线程间隔离,但在同一线程内的多个方法或类间共享的变量。一般我们用它保存一些线程上下文信息,例如,请求ID,事务ID,session等等。Spring的声明式事务就是用它实现的。有些时候,不是必须使用ThreadLocal来解决这些问题,只是ThreadLocal实现来的更为优雅。
还有一个经典的场景是,微服务中traceId的传递,在线程内部,使用ThreadLocal,在线程间采用InheritableThreadLocal(下文会提到),在服务之间,则将traceId添加到http请求的header中,由被调用端做拦截。

四. 扩展

使用ThreadLocal,除了在单一线程内用于变量共享,还可以用于父子线程共享,这里就涉及到ThreadLocal的继承了,默认的ThreadLocal,是无法在父子线程间继承的,但InheritableThreadLocal可以,InheritableThreadLocal是ThreadLocal的子类,他重写了几个ThreadLocal的方法,配合Thread类实现了当一个线程被创建init时,会去检查父进程的InheritableThreadLocal,如果有值就会copy一个引用给自己,使其可以被继承。

但是使用InheritableThreadLocal也面临很多问题,例如由于该变量可以在父子线程共享,那子进程的修改就会对父进程造成影响,使得该变量不再安全。另一方面,在使用线程池时,由于是复用已有线程,没有init阶段,InheritableThreadLocal也无法被继承下来。使用场景比较窄,需要考虑的问题会比较多。

面对InheritableThreadLocal在池化下的各种不足,阿里开源了一个新的类,名为TransmittableThreadLocal,它继承并扩展了InheritableThreadLocal,补足了其在池化场景下的缺陷。其主要思路,是将任务提交到线程池前,把线程的TransmittableThreadLocal变量保存到需要执行的任务内,在执行时,再拿出来放置给执行任务的线程,以此实现池化场景下的继承。

更多推荐

彻底搞懂

本文发布于:2024-02-27 21:43:44,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1766483.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!