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的变量复制代码
这个例子的效果以下图,五个线程都各自有各自对应的变量。安全
set方法,用于设置当前线程本地变量的值,传入的参数为要设置的值。好比 threadLocal.set("value") 。bash
get方法,用于获取当前线程本地变量的值,无需传入任何参数。好比 String threadLocalValue = (String) threadLocal.get() 。数据结构
remove方法,用于删除当前线程本地变量,无需传入任何参数。好比 threadLocal.remove() 。多线程
在了解了ThreadLocal的功能后咱们试着想一个问题:ThreadLocal是如何实现的呢,变量与线程之间如何绑定的呢?实际上,若是让咱们本身来实现ThreadLocal功能,咱们只要经过一个Map结构就能实现该功能了。其中Map的key是当前线程,而Map的value则是变量值。下图展现了ThreadLocal的设计思想。并发
再看具体的模拟实现代码,该模拟类提供了set、get和remove三个方法,这三个方法都是间接操做Map对象。注意Map对象的key值都是当前线程,由Thread.currentThread()来获取,这个key值没必要由调用方传入。这样就实现了一个简单的ThreadLocal,是否是很简单?机器学习
上面的实现方式虽然简单且符合咱们的思考方式,可是它存在多线程并发性能问题,这个怎么说呢?其实很明显,咱们实现的ThreadLocal内部使用了一个Map对象,全部线程的操做都是针对该Map对象进行的操做,须要保证该对象访问的线程安全,这就须要额外的锁机制来保证,但与此同时也就带来了性能问题。分布式
JDK为咱们提供的ThreadLocal的实现则比较巧妙,为了不并发时涉及锁问题,它在每一个线程对象中都放一个Map对象,但它并无直接使用JDK的Map类,而是本身实现了一个key-value数据结构。每一个线程都操做本身的Map对象则不存在并发问题,以下图,线程一包含了一个Map对象,该Map对象的key是ThreadLocal对象,而value则是变量值。注意这里的实现须要将思惟转换一下,ThreadLocal对象变成了key,也就是说可能存在不少不一样的ThreadLocal对象,要查找时须要传入对应的ThreadLocal对象。源码分析
注意这里只分析实现的核心内容,并不是包括全部源码细节,而且为了达到简洁清晰的效果,可能会删除或修改少许源码。咱们先来看Thread类与ThreadLocal类的关系,看到Thread类中包含了一个threadLocals变量,它是一种ThreadLocal.ThreadLocalMap类型,该类型定义在ThreadLocal类里面,也就是一个内部类。而ThreadLocalMap这个内部类便是实现了一个Map结构,该类又包含了Entry内部类,ThreadLocal对象和变量值则是经过Entry来保存。
Thread类里面声明了threadLocals变量用于关联ThreadLocal.ThreadLocalMap对象,注意默认为null。
而ThreadLocal类的大致结构以下,提供了主要的三个方法,其ThreadLocalMap内部类实现Map结构。Map结构具体由Entry类实现,该类继承了WeakReference类,目的是为了不内存泄漏。下面将对三个主要方法进行分析。
对于多个线程与多个线程本地变量来讲,它们的结构以下图。
ThreadLocalMap类实际上就是一个Map结构的实现,对于Java开发人员来讲对Map再熟悉不过了,并且因为ThreadLocalMap类的实现涉及到不少细节,若是咱们纯讲它繁琐的实现源码则会致使篇幅冗长,因此这里咱们主要是了解它的结构和操做便可。ThreadLocalMap类使用数组来保存key-value,数组的每一个元素对应一个key-value,因此新增、修改、删除等操做都是围绕着数组进行的。保存以前会先用哈希算法计算线程对象的哈希值,这是一个整型值,经过该值就能定位数组的某个位置的元素,这样就能找到对应的key-value进行操做。
咱们看set方法的实现,ThreadLocal类的set方法逻辑为:首先获取当前线程对象,而后经过getMap方法获取当前线程的ThreadLocalMap,其实就是从Thread对象中获取,最后调用ThreadLocalMap对象的set方法保存key-value。注意若是Thread对象中的ThreadLocalMap对象为空的话则须要调用createMap方法先建立ThreadLocalMap对象并关联到Thread对象中。
get方法的逻辑为:首先获取当前线程对象,而后经过getMap方法获取当前线程的ThreadLocalMap对象,若是该对象不为空则调用ThreadLocalMap对象的getEntry方法获取Entry,Entry对象即包含了咱们要的value。若是获取不到值则最终还会执行setInitialValue方法,它是根据ThreadLocal对象的initialValue方法来设置初始值,默认是null,若是你想要设置一个初始值则能够重写initialValue方法。
remove方法的逻辑很简单,直接获取当前线程对象的ThreadLocalMap对象,而后调用该对象的remove方法删除对应的key-value。
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内核等。