java十:ThreadLocal源码分析

Posted 人之初丶呵呵哒

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java十:ThreadLocal源码分析相关的知识,希望对你有一定的参考价值。


ThreadLocal解决了什么问题?

多线程操作一个资源变量,可能会出现安全问题。

OK,你可能会说啦,线程安全可以用锁来解决呀?对,可以用synchronized这种东西实现同步,但是,重点来啦,synchronized实现线程安全是以时间为代价啦,为啦防止多个线程同时操作一个共享变量的情况发生,它导致啦某一时刻,只有一个线程可以使用这个资源变量,其它线程无法等待可以理解成是一种时间上的浪费!因此,我说它是以时间为代价实现的同步。

volatile也是如此,为了实现线程安全也是牺牲了一部分时间,比如,线程的每次切换都要从主内存中找到最新的共享变量然后才会复制副本到工作内存中去!!!它实现的线程安全也以时间为代价的!!!

但是ThreadLocal就不一样啦,他是典型的“空间换时间”!!!接下来我解释一下,ThradLocal是怎样“空间换时间”来达到线程安全的。

(1)每一个线程对象都有一个属性字段,ThreadLocal.ThreadLocalMap threadLocals = null;,它是在package java.lang.Thread里面定义的,

(2)ThreadLocalMap是ThreadLocal的内部类。

(3)每一个Thread实例对象的threadLocals字段都会维护一个ThreadLocalMap,并且ThreadLocalMap里面的Key是ThreadLocal实例对象,value是你要操作的共享资源变量的副本!!!

(4)每一个Thread实例对象通过维护一个ThreadLocalMap,每个线程都是操作ThreadLocalMap里面的共享资源变量副本达到的线程安全!各个线程之间的共享资源变量副本互不影响!

(5)ThreadLocal实例本身不维护任何数据!!!数据的维护都在线程对象的ThreadLocalMap里面。

读到这我相信你已经懵圈啦,等我一步一步带你走一遍这个流程你会过来再看上面的几句话你就明白啦。

下图是一个网上的小例子,根据输出结果表名,确实实现啦线程安全!

我们先看一下ThreadLocal里面的方法:

java十:ThreadLocal源码分析

(1)get()方法

java十:ThreadLocal源码分析

当一个线程第一次调用get()方法的时候,会先这里ThreadLocalMap map = getMap(t)判断一下该线程threadLocals字段是否null,线程的threadLocals初始值是null因此get()方法会直接执行return setInitialValue();,


我们看一下setInitialValue()方法:

java十:ThreadLocal源码分析

可以看到,这里先调用initialValue();方法获取共享资源变量的副本,因此我们在使用的时候应该重写这个方法!!!

setInitialValue()方法同样也是先判断一下该线程threadLocals字段是否null,所以它会执行这个方法createMap(t, value);


接下来我们看一下createMap(t, value);方法

java十:ThreadLocal源码分析

这个方法就很简单啦,它是创建啦一个ThreadLocalMap对象,然后赋值给啦该对象的threadLocals字段。


我们进入ThreadLocalMap看一下:

java十:ThreadLocal源码分析

你会发现里面有一个table数组,里面存储的是Entry对象,而这个Entry对象的Key就是弱引用型的ThreadLocal对象本身,而value,则是我们要使用的全局变量副本!又因为,一个线程可以调用很多个ThreadLocal对象的get()方法,因此每一个Entry里Key之所以放的是ThreadLocal对象本身,就是为了解决一个线程中调用不同ThreadLocal对象的get()方法的情况!至于为什么要使用弱引用类型修饰ThreadLocal作为key,我最后再说!先记住这句话,


我们到ThreadLocalMap构造函数里看一下

java十:ThreadLocal源码分析

答案很明显啦,第一次创建ThreadLocalMap对象的时候,会创建一个Entry类型的定长数组,然后通过根据firstKey算出一个值 i ,然后根据ThreadLocal对象和全局变量副本value创建一个Entry对象存入table[ i ]中,刚刚你也看到了我强调了table是一个定长数组,因为ThreadLocalMap每次调用set()方法或者remove()方法的时候,都要会对table[ ]的长度做判断,来看一下当前table[ ]是否需要扩容!!!至于根据什么来判断,就是根据ThreadLocalMap里面的private int threshold;字段,而这个字段的初始值也就是在setThreshold(INITIAL_CAPACITY);的时候候赋予的。


接下来我们看一下Entry类的代码:

java十:ThreadLocal源码分析

Entry继承啦WeakReference这个弱引用类,当创建Entry对象的时候,传递进来的ThreadLocal对象会被封装成一个弱引用类型,文章最后我再讲所谓的“弱引用内存泄漏”,实际上合理使用ThreadLocal是不可能发生这种情况的!!!OK,到了这里就是线程第一次调用ThreadLocal的get()方法的全部过程!!!


(2)我们分析一下ThreadLocal的set()方法:

java十:ThreadLocal源码分析


跟get()方法很相似,判断一下该线程threadLocals字段是否null,上面已经把createMap(t, value);讲过啦,所以我们这次只讲ThreadLocalMap不为null,更新map里面value值的的问题!


看一下,set(ThreadLocal<?> key, Object value)方法

java十:ThreadLocal源码分析

java十:ThreadLocal源码分析

首先ThreadLocalMap的set()方法,它会根据传进来的ThreadLocal对象计算出一个i值,然后在一个for循环里面,根据这个 i 值遍历table[ ]数组和取出里面的Entry,取出Entry里面的key和传进来的ThreadLocal对象比较,如果相等,就更新Entry里面value的值!!!如果没有当前Entry的key为null,说明这个有一个ThreadLocal对象被GC回收,则调用replaceStaleEntry(key, value, i);方法,回收内存,防止内存泄漏!如果循环结束之后还没有命中传进来的ThreadLocal对象,说明这个table【】数组里面没有这个ThreadLocal对象,说明该线程在使用ThrealLocal对象的时候,第一次没有调用get()方法,而是调用的set()方法,所以会新创建一个Enrty放入table数组中,并且 ++size更新数组长度,这时候就需要判断数组是否进行动态扩容啦!!!


我们看一下数组动态扩若的方法rehash()

java十:ThreadLocal源码分析


看一下里面的expungeStaleEntries()方法

java十:ThreadLocal源码分析

注释上写的,它会清空所有旧的Entry,即ThreadLocal被GC回收的Entry。当你进入expungeStaleEntry(int staleSlot)方法你会看到对应的table【i】被赋值成啦null,并且Entry对应的value也被赋值成啦null,这样当GC回收的时候,Entry里面的value就会被垃圾回收啦!这个方法的代码我就不分析啦,因为有几个地方没看懂!!!!


我们退出expungeStaleEntries()方法,进入resize()方法

java十:ThreadLocal源码分析

是一个old数组向new数组赋值的过程,table【】最后扩容2倍!!!并且你会发现,扩容的过程也有对已经Entry里面ThreadLocal对象置null,来帮助GC回收的处理!!!

到这里,ThreadLocal的set( )方法已经分析完毕啦,总结一下:set()方法实际就是更新线程对象threadLocals 字段里面的ThreadLocalMap里面的,对应Key的value,只不过在更新的过程中可能会进行数组扩容,以及对已经被GC回收的ThreadLocal对象对应的Entry,防止内存泄漏。


(3)ThreadLocal的 remove()

java十:ThreadLocal源码分析

跟get()方法很相似,判断一下该线程threadLocals字段是否null,进入里面的remove(this)看一下(强调一下,这个this是ThreadLocal对象):

java十:ThreadLocal源码分析

就是对table【】里面对应的Enrty清除,并且对Entry里面的key 和 value都清除!!!


(4)ThreadLocalMap的 getEntry(ThreadLocal<?> key)

通过传来的ThreadLocal对象到table【】里面找值,


进入里面的getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)

可以看出来,它同样进行了对失效Entry里面失效Key的处理。


OK,其实ThreadLocal的大部分源码我已经讲完啦!里面设计到hashCode的地方我没看懂,你们自己看吧!核心逻辑和代码我已经讲完啦!接下来仔细的聊一下所谓的“内存泄漏”问题。


为什么会造成内存泄漏呢?

因为一个线程对象里面threadLocals字段是一个ThreadLocalMap,Map里面的的key是ThreadLocal对象的弱引用,如果ThreadLocal对象没有一个强引用类型引用它,那么就会在GC回收的时候将ThreadLocal对象回收,这样threadLocals字段里面的Map的key就会是null,这样就无法通过null访问到对应的value,这样就造成啦内存泄漏!!!

实际上,ThreadLocal在调用getEntry()或者 set()方法的时候都会对Entry的key为null的处理,而且remove()就是专门做这件事情的!因此,只要使用完之后,调用一下remove()就好啦。

还有一点需要提醒一下,用线程对象的threadLocals字段保存ThreadLocalMap这样设计有一个优点,那就是线程对象被销毁的时候,对应的threadLocals字段保存ThreadLocalMap也同样会被销毁,节省内存!


最后为什么说TheadLocal是一种“空间换时间”的设计思想呢?

因为每一个线程对象为啦实现线程安全都会在threadLocals字段保存ThreadLocalMap,这样线程对象就可以直接操作自己Map里面保存的,不用考虑线程安全问题,因为每一个线程操作的都是共享资源变量的副本,并且副本和副本之间相互独立,线程和线程即使同时执行也不会相互影响,和synchronized 的同步等待锁 , 以及volatile线程切换的每次重新复制副本完全不一样!!!

我不是想说明谁比谁慢,谁比谁好,因为我觉得,不考虑实际场景和需求就妄自评论,都是耍流氓!!!


以上是关于java十:ThreadLocal源码分析的主要内容,如果未能解决你的问题,请参考以下文章

Java并发编程之ThreadLocal源码分析

深入浅出Java并发编程指南「源码分析篇」透析ThreadLocal线程私有区域的运作机制和源码体系

Java -- 基于JDK1.8的ThreadLocal源码分析

ThreadLocal介绍以及源码分析

(转)Java多线程学习之ThreadLocal源码分析

java ThreadLocal线程设置私有变量底层源码分析