一:ThreadLocal是什么?
学习JDK的类最好的办法就是先看一下源码上的注解
从JAVA官方对ThreadLocal类的说明定义(定义在示例代码中):ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程上下文。
我们可以得知ThreadLocal的作用是:ThreadLocal的作用是提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量的传递的复杂度。
上述可以概述为:ThreadLocal提供线程内部的局部变量,在本线程内随时随地可取,隔离其他线程。
二:ThreadLocal如何使用?
学习一个JDK工具类,首先是使用它,再回头了解源码和设计理念,所以我们先看它是如何去运用的。
首先我定义一下这次运用所使用的数据载体
ThreadLocal存放介质可以看到上图,我定义了一个类,这个类就是描述语言环境的一个结构体。
在很多RPC或者webservice交互中,或多或少有国际化的需求,那么为了减少重复设置语言环境以及方法入参增加语言参数,将国际化语言环境放入ThreadLocal中是一个不错的选择,它也是代表一个上下文Transaction的限界,所以本文的样例选用这个类作为ThreadLocalMap的载体,另外提供Builder模式进行create,覆写toString进行测试。
下面看一下ThreadLocal是如何去运用的
ThreadLocal运用如上图,ThreadLocal可以以这种方式去运用,首先定义一个工具类的概念,将Context的概念封装在工具类里。
而存放Context载体的介质就是ThreadLocal,他提供initialValue、set、get、remove等方法,而工具类中静态方法对这些方法做了一层适配,其实有点类似于代理或者适配,不过这里的目的是使setLanguageContext等方法更明确含义,封装了底层细节,对上层模块友好。
当然,这里的例子泛型也可以换为String或者其他类型的,并且也可以省略初始化方法的覆写,一切取决于需求,这里只是提供方法并不纠结具象。
Ok,我们来试一下这个类的使用效果。
ThreadLocal测试可以看到,他能够隔离开线程,使用自己的私有变量,第一次循环和第二次循环打印出的language字段并不同。
三:ThreadLocal源码与设计思想
上面已经介绍了ThreadLocal的作用和运用方式,那么在使用完它以后,我们来探寻一下它实现的原理,和它设计的思想是什么,学习原理,知其然知其所以然,有助于我们避开ThreadLocal的使用误区,也有助于理解如何选择什么样的场景适合ThreadLocal。
看一下源码
ThreadLocal源码先看一下它的静态内部类,ThreadLocalMap
ThreadLocalMap维护了Entry环形数组,数组中元素Entry的逻辑上的key为某个ThreadLocal对象(实际上是指向该ThreadLocal对象的弱引用),value为代码中该线程往该ThreadLoacl变量实际塞入的值。
读到这里,如果不问不答为什么是这样的定义形式,为什么要用弱引用,等于没读懂源码。
因为如果这里使用普通的key-value形式来定义存储结构,实质上就会造成节点的生命周期与线程强绑定,只要线程没有销毁,那么节点在GC分析中一直处于可达状态,没办法被回收,而程序本身也无法判断是否可以清理节点。弱引用是Java中四档引用的第三档,比软引用更加弱一些,如果一个对象没有强引用链可达,那么一般活不过下一次GC。当某个ThreadLocal已经没有强引用可达,则随着它被垃圾回收,在ThreadLocalMap里对应的Entry的键值会失效,这为ThreadLocalMap本身的垃圾清理提供了便利。
ThreadLocalMap源码上面的这些属性以及方法,是Entry的定义。
可以看到rehash的负载因子是设置最坏三分之二容量扩容。
而容量默认是16,注解Must be a power of two,为什么一定要是2的次幂,我理解的原因有两个,第一是ThreadLocalMap使用的是线性探测法,均匀分布的好处在于很快就能探测到下一个临近的可用slot,从而保证效率。第二是位运算比取模效率高,rehash的时候只需要判断0还是1。
来看一看关于ThreadLocal的set方法实现
ThreadLocal set这个代码再简单不过了,不多介绍了,getMap方法的实现是传入一个Thread引用,返回一个ThreadLocalMap,见下图
Thread源码原来Thread类将ThreadLocalMap作为了一个全局变量,getMap方法只是拿到这个变量,如果不是空,就放值进entry,如果是空,就创建一个ThreadLocalMap指向当前线程threadLocals变量。
再看看get方法
ThreadLocal get还是一样的流程,先拿到当前线程,然后拿到当前线程内的ThreadLocalMap
重点是getEntry方法。
ThreadLocal getEntry上图的table是Object[],其实就是个map,这里的map用的是线性探测解决冲突,而hashmap是用拉链法。
调用getEntryAfterMiss线性探测,过程中每碰到无效slot,调用expungeStaleEntry进行段清理;如果找到了key,则返回结果entry,所以这里get也有去除一些无效引用的作用,上面的while语句就是个线性探测。
看一下remove
ThreadLocal removeremove方法相对于getEntry和set方法比较简单,直接在table中找key,如果找到了,把弱引用断了做一次段清理。
至此源码部分就简单介绍完了,关于Entry内部实现还有很多没有详细介绍的,因为我觉得关于散列还有性能方面设计,其实并不是我这次分享主要的目的,主要的目的是介绍另一种线程安全的实现手段,是一个宏观的概念,如果读者自己对于ThreadLocal在性能上,内存泄漏上,斐波那契散列等内容感兴趣,可以自己去仔细阅读源码。
最后总结一下ThreadLocal的设计理念。
首先堆内存是共享的,那么必须共享堆的时候,有加锁和不变性等方式去解决数据共享问题。
还有还有以空间换时间的思路,ThreadLocal就属于后者。
堆栈图项目代码:https://github.com/Spring5945/Concurrent
网友评论