关于 java.io.ThreadLocal 的部分笔记,ThreadLocal维护了多线程环境中每个线程的私有变量内容,保证这些私有变量内容只对特定的线程可见,其他线程无法访问、修改非自身线程中的变量内容。本文演示代码段的执行环境基于JDK版本1.7。
概述
ThreadLocal实现了多线程环境中每个线程独自访问、维护其私有的变量实例的特性。在实际的多线程环境中,每个线程内部维护了一个ThreadLocalMap集合,集合中的每个Entry维护了一个Threadlocal引用和线程内变量的映射关系。每个Threadlocal引用具有唯一的用于执行散列操作的threadLocalHashCode字段值,因此如果某个线程内部需要多个变量的话只需要创建多个Threadlocal实例,并将Threadlocal实例和变量的映射关系维护到线程的ThreadLocalMap集合中即可。当前线程可以通过ThreadLocal提供的get和set()等方法完成对线程内私有变量的访问操作。
由于每个线程都维护了一个ThreadLocalMap集合,因此Thread、ThreadLocalMap、ThreadLocal这三者之间的关系如图1所示:
ThreadLocalMap是Threadlocal的内部类,用来维护单个线程内的可用变量实例。Entry是ThreadLocalMap集合的底层数据结构。Entry的父类是弱引用的实现类,内部拥有一个类型为Object的value字段。通过Entry实现了Threadlocal实例和线程内变量对象的映射关系。ThreadLocal的结构如图1所示(PS:实线箭头为强引用,虚线箭头为弱引用):
Entry之所以继承自弱引用,是出于如下的考虑:
如图2所示,Entry实例的key以弱引用的方式指向ThreadLocal实例,因此当ThreadLocalRef不再引用ThreadLocal实例时,弱引用可以保证ThreadLocal实例对象在每次GC时都能得到清除以释放其占用的内存空间。如果该ThreadLocal实例不再使用,那么还需要断开value和Map对该Entry的指向关系,这样当前Entry和value指向的对象实例就都会被GC处理并释放空间了。
ThreadLocal如果使用不当的话容易出现内存泄漏的问题。图2中出现的弱引用解决了ThreadLocal实例可以被GC回收的问题,但是图2中value引用和ThreadLocalMap与Entry实例的引用都是强引用。当ThreadLocal被回收了以后,其对应的value也应该被回收掉,但是强引用不允许这么做,所以在确定线程内私有变量不会再次使用时需要调用remove()方法断开value以及Entry实例的引用关系,保证内存资源可以顺利被回收。除此之外,在set()方法中也包含了检测并删除无效元素的操作过程。
继承关系
1 | // ThreadLocal |
实现接口
NIL
ThreadLocal<T>
Constructor Summary
public ObjectInputStream(InputStream in)
1 | public ThreadLocal() { |
创建一个ThreadLocal实例。
部分方法
ThreadLocalMap getMap(Thread t)
1 | ThreadLocalMap getMap(Thread t) { |
获取当前线程t维护的ThreadLocal实例。
public T get()
1 | public T get() { |
获取当前线程t维护的线程私有变量副本的值。
基本操作是首先取得当前线程信息,然后拿到当前线程维护的ThreadLocalMap集合,然后从中取出当前ThreadLocal实例映射的变量并返回。如果当前线程维护的ThreadLocalMap集合不存在,或者当前线程维护的局部变量的值不存在,那么设置一个初始值并返回这个初始值。底层调用ThreadLocalMap.getEntry方法完成操作。
1 | private T setInitialValue() { |
生成初始值并赋值。
由于ThreadLocal存储的是Object,所以生成初始值的过程直接返回null。得到当前线程和当前线程维护的ThreadLocalMap后,将当前线程的变量的初始值设置为value。最后返回设置的value。底层调用ThreadLocalMap.set方法完成操作。
initialValue()的代码非常简单,具体如下:
1 | protected T initialValue() { |
public void set(T value)
1 | public void set(T value) { |
将当前线程中的局部变量的值设置为入参value。
操作过程很简单,取得当前线程和线程维护的ThreadLocalMap实例,之后建立当前value和ThreadLocal实例的映射关系。如果map不存在,那么就为当前线程实例化一个map集合并将ThreadLocal实例和value的映射关系存入集合中。底层调用ThreadLocalMap.set方法完成操作。
public void remove()
1 | public void remove() { |
协助子类读取和验证它们自己的流头部,过程中会读取和校验魔数和版本号。这两个数都是序列化过程中写入到流里的。底层调用ThreadLocalMap.remove方法完成操作。
void createMap(Thread t, T firstValue)
1 | void createMap(Thread t, T firstValue) { |
为当前线程创建一个ThreadLocalMap集合。构造参数是当前ThreadLocal实例引用和value值。
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap)
1 | static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) { |
为当前线程创建一个ThreadLocalMap集合。构造参数是一个ThreadLocalMap集合。
T childValue(T parentValue)
1 | T childValue(T parentValue) { |
这个方法是为InheritableThreadLocal设计的,ThreadLocal自身对该方法不支持,所以直接抛出异常。
ThreadLocalMap
Fields
Entry
1 | static class Entry extends WeakReference<ThreadLocal> { |
Entry是ThreadLocalMap集合中的底层数据结构。Entry自身是一个弱引用对象,该弱引用指向的对象是当前ThreadLocal实例。Entry内部还有一个value属性,该属性字段存储了单个线程中的一个变量的数据内容。
Constructor Summary
ThreadLocalMap(ThreadLocal firstKey, Object firstValue)
1 | ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { |
初始化一个当前线程需要维护的ThreadLocalMap集合实例。该集合中维护了当前线程独享的若干个变量信息。
根据入参ThreadLocal实例来确定ThreadLocal-value键值对在ThreadLocalMap中底层数组结构的下标索引,之后生成一个新的Entry实例并将该对象放入到计算得到的下标位置处。最后完成扩容阈值的设定。
private ThreadLocalMap(ThreadLocalMap parentMap)
1 | private ThreadLocalMap(ThreadLocalMap parentMap) { |
初始化一个当前线程需要维护的ThreadLocalMap集合实例。该方法仅适用于InheritableThreadLocal实现。
在完成ThreadLocalMap实例对象的初始化相关工作后,遍历入参ParentMap中的元素,依次将其加入到新创建的ThreadLocalMap集合中。
部分方法
private Entry getEntry(ThreadLocal key)
1 | private Entry getEntry(ThreadLocal key) { |
获取当前线程中某个变量的键值对映射实体。
在获取过程中,首先根据ThreadLocal的threadLocalHashCode属性来确定该键值对在当前ThreadLocalMap集合中的下标位置。threadLocalHashCode保证了每个ThreadLocal实例都会有一个唯一的int值,以此来保证ThreadLocal实例可以正确的参与到ThreadLocalMap集合的运算中。该值是通过如下代码来获取的:
1 | /** |
在获取了下标位置之后,如果该位置存在有效的键值对实体对象且该对象的弱引用指向的就是当前传入的ThreadLocal实例,那么就返回该键值对,否则调用getEntryAfterMiss()方法按照散列冲突的场景查找对象。
private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e)
1 | private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) { |
通过key未找到对应Entry实例时调用当前方法完成数据查找过程。
当该方法被调用时,说明要获取的Entry可能存在散列冲突,而在ThreadLocalMap中,解决散列冲突的思想是开放定址法法中的线性探测再散列,所以从入参下标位置 i 开始,每次向后移动一个空间位置检查是否存在和入参key匹配的Entry实体。
在比较过程中如果发现了可以匹配入参key的Entry实例,那么就返回该实例。如果发现引用为null,那么需要执行清空操作,清除无效的Entry释放空间。
private static int nextIndex(int i, int len)
1 | private static int nextIndex(int i, int len) { |
开发定址法法中的线性探测再散列函数,每次向后偏移一个位置。
private static int prevIndex(int i, int len)
1 | private static int prevIndex(int i, int len) { |
和nextIndex()函数相反,当前方法执行的是前向遍历,每次向前偏移一个位置。
private int expungeStaleEntry(int staleSlot)
1 | private int expungeStaleEntry(int staleSlot) { |
清除当前线程的ThreadLocalMap集合中的无效Entry实例。
第6 ~ 7行代码的执行顺序保证了Entry实例中的ThreadLocal实例对象和当前线程中的变量对象的引用都会被置为null从而在GC过程中会被回收掉。如果这两行代码的执行顺序颠倒一下则会导致当前线程的变量对象的引用仍然和Entry实例有联系使得该变量占用的内存得不到及时释放。
从入参staleSlot执行的位置开始遍历,如果发现Entry实例的引用已经无效,那么就把该Entry及其内部的value全部清空,反之则重新计算该Entry在当前ThreadLocalMap集合中的下标位置。
private void set(ThreadLocal key, Object value)
1 | private void set(ThreadLocal key, Object value) { |
将某个ThreadLocal实例和对应的变量内容存入到当前线程的ThreadLocalMap集合中。
第9行代码根据Threadlocal的threadLocalHashCode字段做取模运算得到了在集合的下标位置。从该下标位置开始遍历,如果发现了key一致的Entry实例,那么用入参value值替换当前Entry实例的value。如果发现当前Entry的引用指向无效,那么就调用replaceStaleEntry()函数完成替换操作。
如果在当前ThreadLocalMap集合中尚未发现可以匹配当前ThreadLocal的Entry实例,那么就新建一个Entry实例,并将该实例加入到当前集合的下标位置处。最后根据当时的实际情况决定是否需要调用rehash()方法完成元素调整操作。
private void replaceStaleEntry(ThreadLocal key, Object value, int staleSlot)
1 | private void replaceStaleEntry(ThreadLocal key, Object value, int staleSlot) { |
用入参value替换掉当前ThreadLocalMap集合中key为入参key且匹配key的第一个无效元素的下标位置staleSlot。
方法最开始会遍历集合,确定第一个无效的元素位置。所以第10 ~ 15行代码做的就是这个事情,从当前staleSlot位置向前遍历找到第一个无效元素的下标索引。从第19行代码开始,执行真正的替换操作。这里有个地方需要注意一下:开始遍历的位置是在staleSlot位置的下一个位置。因为在调用当前方法时,已经确认了传入的staleSlot位置是无效元素的,所以可以正确的从下一个位置开始遍历和检查。在遍历过程中,如果当前Entry元素的引用指向和入参ThreadLocal一致,那么就将当前位置的Entry元素和staleSlot位置的元素完成交换操作。
第36 ~ 37代码的判断表明如果在第10 ~ 15行代码执行完成后发现staleSlot位置之前的元素都是有效的,那么当前位置 i 就是找到的第一个无效元素位置。第38行代码会在完成无效元素的清除工作后返回到方法调用处。
第45 ~ 46行代码中,如果当前Entry元素无效,且发现staleSlot位置之前的元素都是有效的,那么当前位置 i 就是找到的第一个无效元素位置。
第19 ~ 47行的遍历会一直进行直到遇到第一个不存储Entry实例的下标位置。
第50 ~ 51行代码完成了value值的替换更新操作。如果发现维护的第一个无效元素位置和入参staleSlot位置不一致,那么就清除自第一个无效元素位置起的无效元素内容。
private boolean cleanSomeSlots(int i, int n)
1 | private boolean cleanSomeSlots(int i, int n) { |
判断是否需要清空当前ThreadLocalMap集合中的无效元素。
从当前下标 i 开始(不包括 i),判断集合中的元素是否有效,如果无效,那么置removed标签为true,且调用方法expungeStaleEntry()完成无效元素的清除操作。
需要注意的是,这个方法的时间复杂度因为实际情况的不同而不一样。如果当前ThreadLocalMap集合确实存在无效元素,那么由于需要遍历和移动元素的缘故,导致其复杂度为O(n)。相反,如果在最开始的$log(n)$次操作中都没有发现无效的元素,那么就认为不需要做无效元素清除。
个人认为这么做的原因可能是出于执行效率的考虑吧。
private void remove(ThreadLocal key)
1 | private void remove(ThreadLocal key) { |
删除当前线程中指定的ThreadLocal变量实例。
根据threadLocalHashCode找到对应的元素,执行Entry.clear()方法完成引用关系断开的操作。这样就完成了Entry实例的删除操作,之后在GC执行时这些被断开的引用就会被GC回收掉并释放了其占用的内存。
最后会调用expungeStaleEntry()方法重新整理当前ThreadLocalMap集合中元素的存储位置关系。
private void rehash()
1 | private void rehash() { |
重新排列当前ThreadLocalMap集合的元素存储位置。
首先调用 expungeStaleEntries()方法完成无效元素的清空操作。之后如果当前集合存储的Entry实例个数超过了当前ThreadLocalMap集合容量的一半(threshold是当前容量的$\frac{2}{3}$,所以第5行代码>=的右边部分的值为$\frac{1}{2}$当前容量),那么就调用resize()方法执行扩容处理。扩容规则是按照当前集合容量的两倍进行扩容。
private void expungeStaleEntries()
1 | private void expungeStaleEntries() { |
遍历整个ThreadLocalMap集合,清除无效的Entry引用。底层直接调用expungeStaleEntry()方法完成操作。
private void resize()
1 | private void resize() { |
实际完成ThreadLocalMap集合扩容操作的方法。扩容规则是按照当前集合容量的两倍进行扩容。
在得到当前集合的容量后,按照两倍于当前集合容量的规则实例化一个新的Entry数组,遍历整个当前集合的数组,按照插入的过程将当前集合中的所有Entry元素全部插入到新的数组中。最后重新计算新的扩容阈值,并将新的数组指向table属性字段。
涉及基础知识点
Hash冲突的解决方案:
常用的方法有开放定址法、再哈希法、链地址法、建立公共溢出区等。其中,开放定址法又可细分为线性探测再散列、二次探测再散列和伪随机探测再散列等方法。
ThreadLocalMap采用的开放定址法中的线性探测再散列思想。开放定址法的基本思想是:如果关键字key的散列地址根据散列函数计算后出现冲突时,以计算得到的地址为基础,调用如下的再散列函数完成新的散列地址的计算:
在线性探测再散列法中,$d_i = 1,2,3,4,5,6,\cdots,m$ 。其实就是当发生冲突时每次向后偏移一个单位,检查是否为空闲位置。
强引用、软引用、弱引用的区别:
强引用:应用最频繁的一种引用方式,如果一个对象拥有强引用,那么GC将不会对其尝试进行垃圾回收操作,内存空间不足时通过抛出OutOfMemoryException异常的方式来提示GC回收异常。只有当对象的引用被清除后,GC才会对其尝试进行回收操作。示例如下:
1
2Object testObj = new Object(); // 强引用
testObj = null; //GC可回收软引用:如果一个对象拥有软引用,那么当内存空间足够时,GC不会对其尝试进行垃圾回收操作,如果内存空间不足时,GC会尝试回收其占用的内存空间。示例如下:
1
2String str=new String("MARCUS"); // 强引用
SoftReference<String> testObj=new SoftReference<String>(str); // 软引用弱引用:如果一个对象拥有弱引用,不管内存空间是否足够,GC工作时都会回收弱引用对象占用的内存空间。示例如下:
1
2String str=new String("MARCUS"); // 强引用
WeakReference<String> testObj = new WeakReference<String>(str); // 弱引用如图3和图4所示:
图 - 3 弱引用关系建立创建的弱引用关系在str还持有对“marcus”对象的引用时,GC不会对其做任何处理。
图 - 4 强引用关系丢弃,弱引用对象被GC处理当str对象不再持有对“marcus”对象的引用时,此时“marcus”对象是一个弱引用,原来对它的强引用str不再对其持有引用,所以GC可以对其进行回收处理。
参考文献
- 陶邦仁. 深入JDK源码之ThreadLocal类 [E]
- 技术世界. Java进阶(七)正确理解Thread Local的原理与适用场景 [E]
- bear13. ThreadLocal源码浅析 [E]
- 你听__. 一篇文章,从源码深入详解ThreadLocal内存泄漏问题 [E]