美文网首页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的实现

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

  • ThreadLocal

    ThreadLocal简介 ThreadLocal是线程本地变量,是保证多线程并发数据安全的一种解决方法。当使...

  • ThreadLocal学习

    描述 ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储。ThreadLocal为变量在...

  • 源码阅读 - ThreadLocal

    0.什么是ThreadLocal 线程本地变量,线程间读写同一个ThreadLocal实例是线程隔离的。 1.实现...

  • ThreadLocal

    ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储。ThreadLocal为变量在每个线...

  • ThreadLocal及其扩展

    ThreadLocal ThreadLocal是线程本地变量,每个线程往这个ThreadLocal中读写是线程隔离...

  • JDK ThreadLocal解析

    Java ThreadLocal解析 ThreadLocal 线程本地变量, 线程私有, 在 Thread 类中用...

  • ThreadLocal源码解析

    1.概述 多线程并发时用于存储当前线程的本地变量副本。 2. ThreadLocal解析 set:用于存储当前线程...

  • 4-6 ThreadLocal

    一、什么是ThreadLocal java.lang.ThreadLocal,线程本地变量,也叫线程局部变量。。通...

  • ThreadLocal 作用

    ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,ThreadLocal 的作用是提供...

网友评论

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

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