我的CSDN博客同步发布:ThreadLocal原理解析(2):ThreadLocalMap源码解析
转载请注明出处:【huachao1001的简书:http://www.jianshu.com/users/0a7e42698e4b/latest_articles】
跟上一篇文章【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在心中多多少的有个新的认识了吧!