ThreadLocal原理解析(2):ThreadLocalMap源码解析
我的简书同步发布:ThreadLocal原理解析(2):ThreadLocalMap源码解析
转载请注明出处:【huachao1001的专栏:http://blog.csdn.net/huachao1001】
跟上一篇文章【ThreadLocal原理解析(1):数据存取】一样,本文是源码解析是基于JDK 1.7。
在上一篇文章【ThreadLocal原理解析(1):数据存取】中,我们介绍了ThreadLocal读取数据的过程及原理。我们知道,ThreadLocal将变量的各个副本值保存在各个线程Thread对象实例里面。而Thread对象实例是通过ThreadLocalMap数据结构来存储副本值。可见,ThreadLocalMap在整个ThreadLocal机制中,起到重要作用。我们今天来学习一下,ThreadLocalMap具体是如何模拟实现类似Map接口的方法。
1 ThreadLocalMap源码解析
1.1 存储结构
上一篇文章中,我们说道,ThreadLocalMap中存储的是ThreadLocalMap.Entry(为了书写简单,后面直接写成Entry对象)对象。因此,在ThreadLocalMap中管理的也就是Entry对象。也就是说,ThreadLocalMap里面的大部分函数都是针对Entry的。
首先ThreadLocalMap需要一个“容器”来存储这些Entry对象,ThreadLocalMap中定义了Entry数组实例table,用于存储Entry。
private Entry[] table;
也就是说,ThreadLocalMap维护一张哈希表(一个数组),表里面存储Entry。既然是哈希表,那肯定就会涉及到加载因子,即当表里面存储的对象达到容量的多少百分比的时候需要扩容。ThreadLocalMap中定义了threshold属性,当表里存储的对象数量超过threshold就会扩容。如下所示:
/** * The next size value at which to resize. */ private int threshold; // Default to 0 /** * Set the resize threshold to maintain at worst a 2/3 load factor. */ private void setThreshold(int len) { threshold = len * 2 / 3; }
从上面代码看出,加载因子设置为2/3。即每次容量超过设定的len的2/3时,需要扩容。
1.2 存储Entry对象
首先看看数据是如何被放入到哈希表里面:
/** * Set the value associated with key. * * @param key the thread local object * @param value the value to be set */ private void set(ThreadLocal key, Object value) { // We don't use a fast path as with get() because it is at // least as common to use set() to create new entries as // it is to replace existing ones, in which case, a fast // path would fail more often than not. Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal k = e.get(); if (k == key) { e.value = value; return; } if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
从上面代码中看出,通过key(ThreadLocal类型)的hashcode来计算存储的索引位置i。如果i位置已经存储了对象,那么就往后挪一个位置依次类推,直到找到空的位置,再将对象存放。另外,在最后还需要判断一下当前的存储的对象个数是否已经超出了阈值(threshold的值)大小,如果超出了,需要重新扩充并将所有的对象重新计算位置(rehash函数来实现)。那么我们看看rehash函数如何实现的:
private void rehash() { expungeStaleEntries(); // Use lower threshold for doubling to avoid hysteresis if (size >= threshold - threshold / 4) resize(); }
看到,rehash函数里面先调用了expungeStaleEntries函数,然后再判断当前存储对象的大小是否超出了阈值的3/4。如果超出了,再扩容。看的有点混乱。为什么不直接扩容并重新摆放对象?为啥要搞成这么复杂?
其实,上一篇文章我们提到,ThreadLocalMap里面存储的Entry对象本质上是一个WeakReference<ThreadLocal>。也就是说,ThreadLocalMap里面存储的对象本质是一个对ThreadLocal对象的弱引用,该ThreadLocal随时可能会被回收!即导致ThreadLocalMap里面对应的Value的Key是null。我们需要把这样的Entry给清除掉,不要让它们占坑。
expungeStaleEntries函数就是做这样的清理工作,清理完后,实际存储的对象数量自然会减少,这也不难理解后面的判断的约束条件为阈值的3/4,而不是阈值的大小。
那么如何判断哪些Entry是需要清理的呢?其实很简单,只需把ThreadLocalMap里面的key值遍历一遍,为null的直接删了即可。可是,前面我们说过,ThreadLocalMap并没有实现java.util.Map接口,即无法得到keySet。其实,不难发现,如果Key值为null,此时调用ThreadLocalMap的get(ThreadLocal)相当于get(null),get(null)返回的是null,这也就很好的解决了判断问题。也就是说,无需判断,直接根据get函数的返回值是不是null来判定需不需要将该Entry删除掉。注意,get返回null也有可能是key的值不为null,但是对于get返回为null的Entry,也没有占坑的必要,同样需要删掉,这么一来,就一举两得了。
注意,本文中所说的get函数是指getEntry.
1.3 获取Entry对象getEntry
我们看看getEntry函数:
/** * Get the entry associated with key. This method * itself handles only the fast path: a direct hit of existing * key. It otherwise relays to getEntryAfterMiss. This is * designed to maximize performance for direct hits, in part * by making this method readily inlinable. * * @param key the thread local object * @return the entry associated with key, or null if no such */ private Entry getEntry(ThreadLocal key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); }
getEntry函数很简单,直接通过哈希码计算位置i,然后把哈希表中对应i位置的Entry对象拿出来。如果对应位置的值为null,这就存在如下几种可能:
key对应的值确实为null
由于位置冲突,key对应的值存储的位置并不在i位置上,即i位置上的null并不属于key的值。
因此,需要一个函数再次去确认key对应的value的值,即getEntryAfterMiss函数:
/** * Version of getEntry method for use when key is not found in * its direct hash slot. * * @param key the thread local object * @param i the table index for key's hash code * @param e the entry at table[i] * @return the entry associated with key, or null if no such */ private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal k = e.get(); if (k == key) return e; if (k == null) expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null; }
2 ThreadLocalMap.Entry对象
前面很多地方都在收ThreadLocalMap里面存储的是ThreadLocalMap.Entry对象,那么ThreadLocalMap.Entry对象到底是如何存储键值对的?同时又是如何做到对ThreadLocal对象进行弱引用?
先看看Entry类的源码:
static class Entry extends WeakReference<ThreadLocal> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal k, Object v) { super(k); value = v; } }
从源码的继承关系可以看到,Entry 是继承WeakReference<ThreadLocal>。即Entry 本质上就是WeakReference<ThreadLocal>,换言之,Entry就是一个弱引用,具体讲,Entry实例就是对ThreadLocal某个实例的弱引用。只不过,Entry同时还保存了value。
好啦,到现在为止,相信你对Java中的ThreadLocalMap在心中多多少的有个新的认识了吧!