并发原理抽丝剥茧,线程本地变量ThreadLocal的实现原理 | 专栏五折活动,仅限三天

栏目: IT技术 · 发布时间: 4年前

内容简介:作者简介:笔名seaboat,擅长人工智能、计算机科学、数学原理、基础算法。出版书籍:《Tomcat内核设计剖析》、《图解数据结构与算法》、《人工智能原理科普》。应粉丝要求这个专栏优惠三天,五折优惠,三天后恢复原价,需要的朋友赶紧入手,购买包括答疑服务。

关于ThreadLocal

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

并发原理抽丝剥茧,线程本地变量ThreadLocal的实现原理 | 专栏五折活动,仅限三天
ThreadLocal变量绑定

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的变量
并发原理抽丝剥茧,线程本地变量ThreadLocal的实现原理 | 专栏五折活动,仅限三天
ThreadLocal例子

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

并发原理抽丝剥茧,线程本地变量ThreadLocal的实现原理 | 专栏五折活动,仅限三天

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的设计思想。

并发原理抽丝剥茧,线程本地变量ThreadLocal的实现原理 | 专栏五折活动,仅限三天
模拟实现

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

并发原理抽丝剥茧,线程本地变量ThreadLocal的实现原理 | 专栏五折活动,仅限三天
模拟实现

JDK中ThreadLocal的实现思想

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

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

并发原理抽丝剥茧,线程本地变量ThreadLocal的实现原理 | 专栏五折活动,仅限三天
ThreadLocal实现思想

JDK的实现源码分析

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

并发原理抽丝剥茧,线程本地变量ThreadLocal的实现原理 | 专栏五折活动,仅限三天
类关系

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

并发原理抽丝剥茧,线程本地变量ThreadLocal的实现原理 | 专栏五折活动,仅限三天
Thread类

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

并发原理抽丝剥茧,线程本地变量ThreadLocal的实现原理 | 专栏五折活动,仅限三天
ThreadLocal类

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

并发原理抽丝剥茧,线程本地变量ThreadLocal的实现原理 | 专栏五折活动,仅限三天

关于ThreadLocalMap类

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

并发原理抽丝剥茧,线程本地变量ThreadLocal的实现原理 | 专栏五折活动,仅限三天

ThreadLocal的set方法

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

并发原理抽丝剥茧,线程本地变量ThreadLocal的实现原理 | 专栏五折活动,仅限三天
set方法

ThreadLocal的get方法

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

并发原理抽丝剥茧,线程本地变量ThreadLocal的实现原理 | 专栏五折活动,仅限三天
get方法

ThreadLocal的remove方法

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

并发原理抽丝剥茧,线程本地变量ThreadLocal的实现原理 | 专栏五折活动,仅限三天
remove方法

ThreadLocal的内存泄漏

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

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

并发原理抽丝剥茧,线程本地变量ThreadLocal的实现原理 | 专栏五折活动,仅限三天

那是不是不要继承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。

作者简介:笔名seaboat,擅长人工智能、计算机科学、数学原理、基础算法。出版书籍:《Tomcat内核设计剖析》、《图解数据结构与算法》、《人工智能原理科普》。

应粉丝要求这个专栏优惠三天,五折优惠,三天后恢复原价,需要的朋友赶紧入手,购买包括答疑服务。

并发原理抽丝剥茧,线程本地变量ThreadLocal的实现原理 | 专栏五折活动,仅限三天


以上所述就是小编给大家介绍的《并发原理抽丝剥茧,线程本地变量ThreadLocal的实现原理 | 专栏五折活动,仅限三天》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

数据资本时代

数据资本时代

Viktor Mayer-Schnberger / 李晓霞、周涛 / 中信出版集团股份有限公司 / 2018-11-1 / CNY 58.00

【编辑推荐】 大数据除了能对我们的生活、工作、思维产生重大变革外,还能够做什么?畅销书《大数据时代》作者舍恩伯格在新书《数据资本时代》中,展示了大数据将如何从根本上改变经济——这并不是因为数据是一种新型石油,而是因为数据是一种新型润滑脂,它将给市场带来巨大能量,给公司带来巨大压力,使金融资本的作用大大削弱。赢家是市场,而并非资本。 这本书在当下国内出版,可以说恰逢其时。时下,中国经济正......一起来看看 《数据资本时代》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具