一、ThreadLocal的介绍
ThreadLocal 是线程thread的维护的一个变量,在jdk8中声明如下:
ThreadLocal.ThreadLocalMap threadLocals = null;
类型: ThreadLocalMap 类似于HashMap结构 ;key保存着ThreadLocal对象,value保存着对应的值。
作用: 每一个线程都可以独立地改变自己的副本,修改变量时候,不会影响其它线程。
二、ThreadLocal核心方法代码的介绍
2.1 set 方法
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
set方法很简单,就是获取当前线程t的threadLocals对象,并向map进行set操作
2.2 get 方法
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
get方法也是从threadLocals获取相应的值。
通过上面代码可知,get和set方法都是调用threadLocalMap中getEntry和set方法。 下面我们将详细介绍threadLocalMap,并分析getEntry和set方法的具体实现
三、ThreadLocalMap核心代码分析
3.1 ThreadLocalMap 定义
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table;
private static final int INITIAL_CAPACITY = 16;
private int threshold; // Default to 0
private void setThreshold(int len) {
threshold = len * 2 / 3;
1、关于ThreadLocalMap,类似Hashmap一样,有一个Entry类被用来存放Map的数据,也有一个Entry[]数组。
2、table 初始化大小是16
3、table 增长的阈值是 table长度的2/3
3.2 ThreadLocalMap set函数
private void set(ThreadLocal key, Object value) {
Entry[] tab = table;
int len = tab.length;
//首先获取新的entry位置 i
int i = key.threadLocalHashCode & (len-1);
//然后从i的位置开始遍历table
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal k = e.get();
//如果存在key相同,说明该线程设置过该ThreadLocal,那么就直接赋予新的值
if (k == key) {
e.value = value;
return;
}
//如果key是null,这是因为Entry继承的是WeakReference,在垃圾回收时候已经回收
if (k == null) {
//出现null需要进行置换过期entry,具体见下面代码
replaceStaleEntry(key, value, i);
return;
}
}
//执行到这里,tab[i]位置没有被占用,此时需要将entry放入到table中
tab[i] = new Entry(key, value);
int sz = ++size;
//由于弱引用,存在一些key为空的情况,所以先要清除无用数据,才能判断现在的size有没有达到阀值threshhold
//如果没有要清除的数据,并且达到阀值,那就要执行扩容:rehash()
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
3.3 ThreadLocalMap replaceslateentry函数
如果Entry中的key为空,则说明ThreadLocal已经被垃圾回收,则调用replaceStaleEntry方法,具体看看replaceStaleEntry做了哪些操作
/***
** key :set进来的key值
** value: set进来的value值
** staleSlot : set函数hash出来的位置
***/
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
//staleSlot向前找到table中第一个脏了的数据的下标
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// 找到参数key,后面的第一个过期Entry
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 如果我们找到了当前table存在此key,那么需要就需要把它跟staleSlot位置过期的数据进行交换,保证hash表的顺序
// 那么剩下的过期Entry呢,就可以交给expungeStaleEntry方法来擦除掉或者执行rehash方法。
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
if (slotToExpunge == staleSlot)
slotToExpunge = i;
// 清除过期数据
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// 如果没有在后向查找中找到过期数据,那么slotToExpunge设置成i,并在后面进行清除
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 如果以上的查找都没有找见key的话,首先将value设置null,然后new Entry<key, value>进tab[staleSlot]
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// 如果有其他的脏数据,依然要擦除
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
清理table方法的代码如下
/***
** 清理过期数据
** 传进来的staleSlot就是null键值的table数组的下标。
***/
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 擦除掉这个脏数据
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// 然后从staleSlot的nextIndex开始循环,停止条件是Entry e = null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
// 又发现一个过期的,擦除
e.value = null;
tab[i] = null;
size--;
} else {
// 重新计算哈希值,并且移动Entry
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
1、启发式扫描table,其中i:是扫描的位置;扫描log2n单元;
2、它试图在i和log2n步内找到一个key被gc清理过,如果找到调用expingeslateentry方法进行清理,然后在当前i位置继续寻找log2n步内被gc清理过的内容,以此类推。
3.4 ThreadLocalMap rehash函数
在set函数中最后一行, 如果没有要清除的数据,并且达到阀值,那就要执行扩容:rehash()
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
1、首先调用清除函数。
2、然后继续判断size是否大于等于 theshold*3/4,如果大于需要resize()来调整table数组长度
下面介绍一下resize函数
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
//resize会把容量扩大两倍
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
// 就算是扩容,也不能忘了为擦除过期数据做准备
e.value = null; // Help the GC
} else {
//重新计算哈希,并放到newTab原来没有被占用的桶中
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
//重新设置阀值,大小,table指针
setThreshold(newLen);
size = count;
table = newTab;
}
此代码注释是参考:https://www.jianshu.com/p/0ce314da0248
x