共计 3538 个字符,预计需要花费 9 分钟才能阅读完成。
ThreadLocal如何实现线程本地化存储的
对象副本
线程不安全的问题原因在于变量的共享,为了解决线程不安全的问题,如果让每个线程都拥有自己的对象副本,也就不存在多线程变量的共享问题,我们来看图
从图中,我们知道,每个线程都拥有自己的对象副本,但是,如果我们每次想获取每个线程的私有对象的时候,都通过一堆复杂的代码逻辑去获取这个对象就有点麻烦了,从而我们想到了,用一个代理类把获取线程所持有的对象细节隐藏掉,请看下图
ThreadLocal原理解析
在java里提供了可支持线程本地化存储的ThreadLocal类,其实对于线程本地化存储,我们的目标就是让不同的线程拥有不同变量,那我们第一个能想到的就是Key-Value键值对的存储,Key表示的就是线程,Value对应的就是每个线程所拥有的变量,我们来看示意图和代码示例图
从上述图中,我们能清晰的看到,我们自己所设计的线程本地化存储,核心数据结构就是一个Map,key对应的就是线程,value对应的线程持有的对象,我们可以调用对应get和set方法可以获取和设置线程所拥有的对象,但是这样设计容易发生内存泄露,我们来看图
从上述图中,我们能清晰的看到,我们自己实现的线程本地化存储MyThreadLocal里的map是持有线程Thread对象的,只要MyThreadLocal对象存活在jvm中,那么map中的线程Thread对象是不会被jvm垃圾回收的,所以容易出现内存泄露。
ThreadLocal具体实现
从图中,能看到jdk中的ThreadLocal所拥有的的对象属性,但是好像这些变量都没有能存储线程本地化对象的地方,如果ThreadLocal实现了线程本地化的功能,就一定会提供给我们获取和设置本地化对象的方法
从图中,我们看到了ThreadLocal的get方法,get方法一定是从底层存储层获取数据的,那我们可以顺着get方法的逻辑,就能找到ThreadLocal的底层存储,按照这个逻辑,请看下图
在上图中,红线1,我们能看到有个叫ThreadLocalMap的东西,然后在红线2中,再从获取到的ThreadLocalMap中获取Entry,最后从取到的Entry中获取线程的value
从图中,我们能清晰的看到ThreadLocalMap是Thread对象里的属性,换句话说,每个Thread对象都拥有一个ThreadLocalMap对象属性,我们知道线程本地化对象就是存储在这个ThreadLocalMap里,但这ThreadLocalMap不是ThreadLocal持有的属性,我们通过一张图来描述一下,请看图
在JDK里,ThreadLocalMap是Thread自己的属性,因为ThreadLocal只是一个我们所说的ObjectProxy代理工具类,内部不应该持有任何与线程有关的属性,所以我们设置的线程本地化对象,在自己Thread类里所持有也更为容易理解些,也就是说ThreadLocalMap由Thread持有更为合理些。
ThreadLocalMap里有什么
如果我们想获取线程对应的value,首先就要先获取线程对应的ThreadLocalMap,然后再从ThreadLocalMap里获取Entry,最后从Entry获取对应的value,那么我们接下来就看看ThreadLocalMap里有什么,请看图
从图中,我们能看到,原来ThreadLocalMap是ThreadLocal一个内部类,在红线2处每个ThreadLocalMap持有一个Entry数组,且在红线1处每个Entry持有一个value对象,这个时候,同学们可能已经豁然开朗了,这个value不就是我们想找的嘛,原来整个获取value的顺序就是:线程Thread->ThreadLocalMap->Entry->value,但是我们在红线2处发现这个Entry的数组,
获取Entry
我们是怎么样从Entry的数组中获取某一个的Entry的呢?请看图
从图中红线2处,在获取到ThreadLocalMap之后,会执行TheadLocalMap的getEntry方法,方法如图12所示,在图12红线1处,我们清晰看到方法的参数是Threadlocal类型,红线2处,再根据传入的这个ThreadLocal的threadLocalHashCode计算坐标值,然后根据坐标值再从Entry数据里获取对应的Entry对象,从而获取到Entry里的value值,我们再用一张图来描述一下,请看下图
从图中,我们能清晰看到,整个线程Thread本地化存储结构,每个线程Thead里的ThreadLocalMap里可以存储多个ThreadLocal本地化对象,且每一个ThreadLocal本地化对象是通过自己的threadLocalHashCode来计算数组下标,分配到下标对应Entry数组中,从而可以进行本地化对象获取和设置操作,
如何避免内存泄漏
我们自己设计的方案容易产生内存泄露,那JDK实现就没有这个情况吗?那我们就来看看JDK是如何巧妙设计的请看下图
在图中,我们能看到Entry的定义,Entry继承了WeakReference类,他就是弱引用,这说明了ThreadLocalMap里的对Threadlocal的引用是弱引用,请看下图
只要Thread对象被垃圾回收,那么ThreadLocalMap就能被回收,所以就不会出现内存泄漏的情况,虽然JDK线程本地化的实现的复杂了一些,但是它更加安全,所以说JDK的方案还是很优秀的,最后,我们来对比一下我们自己设计的方案和JDK的方案,请看下图
最后,通过图对比来看,从数据亲缘性来看,JDK把线程本地化存储放在Thread的ThreadLocalMap持有,且可以设置多个ThreadLocal,而我们的设计是把线程本地化存储放在自己设计的ObjectProxy代理工具类里,且容易产生泄露,而JDK通过精巧的设计却避免了内存泄露的情况。