美文网首页Java架构技术进阶程序员
并发原理“抽丝剥茧”——线程本地变量ThreadLocal的实现

并发原理“抽丝剥茧”——线程本地变量ThreadLocal的实现

作者: 代码小当家 | 来源:发表于2020-02-27 21:55 被阅读0次

    推荐阅读:

    47天时间,洒热血复习,我成功“挤进”了字节跳动(附Java面试题+学习笔记+算法刷题)​zhuanlan.zhihu.com

    图标 面试“阿里云”居然一面就惨被吊打?幸终得内推机会,4面喜提华为offer​zhuanlan.zhihu.com 图标

    关于ThreadLocal

    ThreadLocal我们经常称之为线程本地变量,通过它能够实现线程与变量之间的绑定,也就是说每个线程只能读写本线程对应的变量。对于同一个ThreadLocal对象,每个线程对该对象读写时只能看到属于自己的变量,这样来看ThreadLocal也是一种线程安全的模式。ThreadLocal的功能如下图所示,一个ThreadLocal对象就是一个线程本地变量,该变量可以保存多个变量值,比如线程一对应变量值一,其它两个线程也有自己的变量值。

    image

    <figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">ThreadLocal变量绑定</figcaption>

    ThreadLocal例子

    我们通过一个小例子来了解ThreadLocal的使用方法。首先创建一个ThreadLocal对象,由于是泛型所以需要指定保存的数据类型,这里保存的是String类型。然后启动五个线程,每个线程都通过ThreadLocal对象的set方法设置要绑定该线程的变量值,要保存什么值就传入什么值,而当我们要使用时则调用ThreadLocal对象的get方法,该方法无需传入参数值。最终的输出结果如下。

    Thread-1--->Thread-1的变量Thread-0--->Thread-0的变量Thread-4--->Thread-4的变量Thread-3--->Thread-3的变量Thread-2--->Thread-2的变量复制代码
    
    image

    <figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">ThreadLocal例子</figcaption>

    这个例子的效果如下图,五个线程都各自有各自对应的变量。

    image

    ThreadLocal三个主要方法

    1. set方法,用于设置当前线程本地变量的值,传入的参数为要设置的值。比如 threadLocal.set("value") 。
    2. get方法,用于获取当前线程本地变量的值,无需传入任何参数。比如 String threadLocalValue = (String) threadLocal.get() 。
    3. remove方法,用于删除当前线程本地变量,无需传入任何参数。比如 threadLocal.remove() 。

    如何模拟实现

    在了解了ThreadLocal的功能后我们试着想一个问题:ThreadLocal是如何实现的呢,变量与线程之间如何绑定的呢?实际上,如果让我们自己来实现ThreadLocal功能,我们只要通过一个Map结构就能实现该功能了。其中Map的key是当前线程,而Map的value则是变量值。下图展示了ThreadLocal的设计思想。

    image

    <figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">模拟实现</figcaption>

    模拟实现

    再看具体的模拟实现代码,该模拟类提供了set、get和remove三个方法,这三个方法都是间接操作Map对象。注意Map对象的key值都是当前线程,由Thread.currentThread()来获取,这个key值不必由调用方传入。这样就实现了一个简单的ThreadLocal,是不是很简单?

    image

    <figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">模拟实现</figcaption>

    JDK中ThreadLocal的实现思想

    上面的实现方式虽然简单且符合我们的思考方式,但是它存在多线程并发性能问题,这个怎么说呢?其实很明显,我们实现的ThreadLocal内部使用了一个Map对象,所有线程的操作都是针对该Map对象进行的操作,需要保证该对象访问的线程安全,这就需要额外的锁机制来保证,但与此同时也就带来了性能问题。

    JDK为我们提供的ThreadLocal的实现则比较巧妙,为了避免并发时涉及锁问题,它在每个线程对象中都放一个Map对象,但它并没有直接使用JDK的Map类,而是自己实现了一个key-value数据结构。每个线程都操作自己的Map对象则不存在并发问题,如下图,线程一包含了一个Map对象,该Map对象的key是ThreadLocal对象,而value则是变量值。注意这里的实现需要将思维转换一下,ThreadLocal对象变成了key,也就是说可能存在很多不同的ThreadLocal对象,要查找时需要传入对应的ThreadLocal对象。

    image

    <figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">ThreadLocal实现思想</figcaption>

    JDK的实现源码分析

    注意这里只分析实现的核心内容,并非包括所有源码细节,并且为了达到简洁清晰的效果,可能会删除或修改少量源码。我们先来看Thread类与ThreadLocal类的关系,看到Thread类中包含了一个threadLocals变量,它是一种ThreadLocal.ThreadLocalMap类型,该类型定义在ThreadLocal类里面,也就是一个内部类。而ThreadLocalMap这个内部类即是实现了一个Map结构,该类又包含了Entry内部类,ThreadLocal对象和变量值则是通过Entry来保存。

    image

    <figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">类关系</figcaption>

    Thread类里面声明了threadLocals变量用于关联ThreadLocal.ThreadLocalMap对象,注意默认为null。

    image

    <figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">Thread类</figcaption>

    而ThreadLocal类的大体结构如下,提供了主要的三个方法,其ThreadLocalMap内部类实现Map结构。Map结构具体由Entry类实现,该类继承了WeakReference类,目的是为了避免内存泄漏。下面将对三个主要方法进行分析。

    image

    <figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">ThreadLocal类</figcaption>

    对于多个线程与多个线程本地变量来说,它们的结构如下图。

    image

    关于ThreadLocalMap类

    ThreadLocalMap类实际上就是一个Map结构的实现,对于Java开发人员来说对Map再熟悉不过了,而且由于ThreadLocalMap类的实现涉及到很多细节,如果我们纯讲它繁琐的实现源码则会导致篇幅冗长,所以这里我们主要是了解它的结构和操作即可。ThreadLocalMap类使用数组来保存key-value,数组的每个元素对应一个key-value,所以新增、修改、删除等操作都是围绕着数组进行的。保存之前会先用哈希算法计算线程对象的哈希值,这是一个整型值,通过该值就能定位数组的某个位置的元素,这样就能找到对应的key-value进行操作。

    image

    ThreadLocal的set方法

    我们看set方法的实现,ThreadLocal类的set方法逻辑为:首先获取当前线程对象,然后通过getMap方法获取当前线程的ThreadLocalMap,其实就是从Thread对象中获取,最后调用ThreadLocalMap对象的set方法保存key-value。注意如果Thread对象中的ThreadLocalMap对象为空的话则需要调用createMap方法先创建ThreadLocalMap对象并关联到Thread对象中。

    image

    <figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">set方法</figcaption>

    ThreadLocal的get方法

    get方法的逻辑为:首先获取当前线程对象,然后通过getMap方法获取当前线程的ThreadLocalMap对象,如果该对象不为空则调用ThreadLocalMap对象的getEntry方法获取Entry,Entry对象即包含了我们要的value。如果获取不到值则最终还会执行setInitialValue方法,它是根据ThreadLocal对象的initialValue方法来设置初始值,默认是null,如果你想要设置一个初始值则可以重写initialValue方法。

    image

    <figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">get方法</figcaption>

    ThreadLocal的remove方法

    remove方法的逻辑很简单,直接获取当前线程对象的ThreadLocalMap对象,然后调用该对象的remove方法删除对应的key-value。

    image

    <figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">remove方法</figcaption>

    ThreadLocal的内存泄漏

    JDK的实现是让Entry继承了WeakReference类,所以可以指定对某个对象进行弱引用,弱引用类型在没有其它强引用的情况下会被JVM的垃圾回收器回收。我们通过下图来理解如何导致内存泄漏,我们知道ThreadLocal被创建后就会伴随Thread的整个生命周期,假如这个线程的生命周期很长则会导致严重的内存泄漏,下面看具体的情况。

    运行栈运行过程中假如某个时刻ThreadLocal引用不再指向ThreadLocal对象,则该对象仅仅剩下一个弱引用,这时该对象就会被JVM回收,从而导致Entry的key为null,key为null时就导致ThreadLocalMap无法再找到这个Entry的value。一旦运行时间被拉长,value将一直存在内存中而无法被回收,这样就造成了内存泄漏,整个引用关系为Thread对象->ThreadLocalMap对象->Entry对象->value。

    image

    那是不是不要继承WeakReference类,让它默认强引用就不会导致内存泄漏呢?那肯定不是,不然也就不用多此一举了。运行栈运行过程中假如某个时刻ThreadLocal引用不再指向ThreadLocal对象,则ThreadLocal对象因为存在强引用而不被JVM回收,此时除了value无法被回收外,ThreadLocal对象也无法被回收,同样产生内存泄漏问题。

    综上所述,不管Entry有没有继承WeakReference类都存在内存泄漏问题,如果我们不手动去执行remove操作的话都会导致内存泄漏。那么JDK团队为什么又要继承WeakReference类呢?那是因为他们想采取一些措施来尽量保证内存不泄漏,也就是说他们会在ThreadLocalMap类的get、set、remove方法中去执行一个清除操作,把ThreadLocalMap包含的所有Entry中key为null的value给清除掉,并且将对应的Entry也置为null,以便被JVM回收。

    所以我们在使用ThreadLocal时要注意的一点是:当我们使用完ThreadLocal时都要手动调用remove方法,从而避免内存泄漏。

    总结

    本篇文章介绍了ThreadLocal的相关知识,从简单的使用例子开始一步一步深入,而且我们还自己模拟实现了一个ThreadLocal类,模拟的方式简洁且容易理解,但却存在并发性能问题,所以JDK实现的ThreadLocal相对复杂很多。然后我们分析了JDK的ThreadLocal的实现思想,最后从源码级别分析它的实现,包括set、get和remove三个主要方法。最后,我们讲解了ThreadLocal存在的内存泄漏问题,并提出了使用ThreadLocal的注意点是要手动调用remove方法清理掉不再使用的key-value。

    作者:超人汪小建
    链接:https://juejin.im/post/5e573630f265da5769710d0b
    来源:掘金

    相关文章

      网友评论

        本文标题:并发原理“抽丝剥茧”——线程本地变量ThreadLocal的实现

        本文链接:https://www.haomeiwen.com/subject/ecufhhtx.html