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

1,791 阅读9分钟


关于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三个主要方法

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

模拟实现

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

模拟实现

JDK中ThreadLocal的实现思想

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

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

ThreadLocal实现思想

JDK的实现源码分析

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

类关系

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

Thread类

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

ThreadLocal类

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

关于ThreadLocalMap类

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

ThreadLocal的set方法

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

set方法

ThreadLocal的get方法

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

get方法

ThreadLocal的remove方法

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

remove方法

ThreadLocal的内存泄漏

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

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

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

更多Java并发原理剖析可关注我的专栏。

专注于人工智能、读书与感想、聊聊数学、计算机科学、分布式、机器学习、深度学习、自然语言处理、算法与数据结构、Java深度、Tomcat内核等。