我的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
在心中多多少的有个新的认识了吧!