20

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

 5 years ago
source link: http://mp.weixin.qq.com/s?__biz=MjM5MzA1Mzc3Nw%3D%3D&%3Bmid=2247485249&%3Bidx=1&%3Bsn=415e6b3516eb879c8c7ec1ea17d50699
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

关于ThreadLocal

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

yEF3imR.jpg!web 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的变量
QbA3UrE.jpg!web ThreadLocal例子

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

ba2iiaj.jpg!web

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

ey2qqa3.jpg!web 模拟实现

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

AfEnAjv.jpg!web 模拟实现

JDK中ThreadLocal的实现思想

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

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

M7fa6vy.jpg!web ThreadLocal实现思想

JDK的实现源码分析

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

URNjeay.jpg!web 类关系

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

2yiU3eF.jpg!web Thread类

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

eaaqymn.jpg!web ThreadLocal类

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

bueuieB.jpg!web

关于ThreadLocalMap类

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

au2aIbz.jpg!web

ThreadLocal的set方法

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

7bAVBzM.jpg!web set方法

ThreadLocal的get方法

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

zEjIVj2.jpg!web get方法

ThreadLocal的remove方法

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

reMFzei.jpg!web remove方法

ThreadLocal的内存泄漏

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

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

FvUZven.jpg!web

那是不是不要继承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内核设计剖析》、《图解数据结构与算法》、《人工智能原理科普》。

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

Vb2QBfi.jpg!web


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK