ThreadLocal中有个静态内部类ThreadLocalMap,ThreadlocalMap里面包含一个Entry数组,Entry的定义如下
static class Entry extends WeakReference> {
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
那ThreadLocal是如何保证每个线程内有一份单独的变量呢?原因在于,Thread类内部持有ThreadLocalMap的引用
ThreadLocal.ThreadLocalMap threadLocals = null;
public void set(T value)
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
在执行set的时候,首先获取当前线程,然后获取线程内部的ThreadlocalMap对象,如果map为null,就执行createMap方法
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
接着看看ThreadLocalMap的构造方法
ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
首先创建一个Entry数组,长度为INITIAL_CAPACITY
,然后我们对INITIAL_CAPACITY
进行取余运算,确定出当前Entry在数组中的位置,接着令size为1,标识当前table的大小为1,最后设置了table进行扩容的大小,装在因子大小为 2/3 ,也就是说当前table的size如果大于或等于 2/3 * length,就进行扩容。
如果从Thread中获取的map不为null,就执行map.set(this, value)
方法
private void set(ThreadLocal> key, Object value) {
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();
}
首先根据当前ThreadLocal的hashcode获取在table中的索引,将这个entry作为初始化参数,执行for循环,每次循环都会获取下一个entry,那么有个问题,为什么要执行for循环呢?一个ThreadLocal关联的value是确定的呀,直接通过ThreadLocal的hashcode就可以确定出其在table中的索引,然后将对应的entry取出来,不就好了吗?为什么这里要执行循环呢?原因在于可能存在hash冲突,而ThreadLocal解决hash冲突的办法是开放寻址法,也就是说产生了hash冲突就向后进行环形搜索空位,因此这里会执行for循环。
replaceStaleEntry
方法private void replaceStaleEntry(ThreadLocal> key, Object value, int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal> k = e.get();
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
staleSlot
是当前脏entry的索引,这个replaceStaleEntry
方法做了哪些事呢。
首先向前搜索
slotToExpunge
中,如果前向搜索没有找到脏数据,slotToExpunge
的值就为staleSlot
然后向后搜索
就更新的值填写在该entry中,并且将staleSlot
所在的脏entry与当前的entry交换,这样交换的结果就是把更新后的entry填在了staleSlot
的位置,因此通过ThreadLocal的hashcode就能直接索引到该位置
如果在搜索的过程中,还没有发现其余脏的entry,那么就以当前位置保存在slotToExpunge
,并以这个位置为起点,对脏entry执行清理工作。
最后
如果在查找过程中 ,没有找到可以覆盖的entry,就以value构造一个新的entry,并插在staleSlot
位置,如果还存在脏的entry,就执行清理工作。
那么就在当前null的位置构造出一个新的entry,然后可能需要进行rehash操作。
至此,set方法基本介绍完了,下面来看看get方法
public T get()
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
在执行get的时候,依旧首先获取当前的线程,然后获取线程内部的ThreadLocalMap对象,接着把当前的this传进去,即可获得与当前ThreadLocal相关联的变量。
我们看一下这个getEntry方法,首先通过ThreadLocal的hashcode索引到具体的table中的某个entry,如果找到的tntry不为null,并且tntry的k与当前的ThreadLocal引用相同,那么我们就返回对应的value。
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);
}
如果没找到符合条件的entry,就会去执行getEntryAfterMiss(key, i, e)
方法。
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;
}
我们看if语句里面的几个判断条件,第一个是k == key
,为什么会有k == key
这样的判断条件呢?我们在getEntry方法中,就进行了判断,如果entry不为null,并且entry的k等于key,我们就返回这个entry,那么剩下的entry的k肯定不等于key呀,其实还是同一个原因,存在hash冲突的可能,ThreadLocal解决hash冲突采用的是开放寻址法,而不是Hashmap采用的拉链法,因此会向后环形搜索,如果k = null
,就会去执行expungeStaleEntry(i)
方法。
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
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 {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
该方法主要做了四件事情
清理当前脏entry,即将其value引用置为null,那么这个value在下一次GC时就会被回收掉,同时也将table[staleSlot]为null后以便于存放新的entry,并将size-1。
从当前staleSlot位置向后环形(nextIndex)继续搜索,如果遇到哈希桶为null的时候退出;
若在搜索过程再次遇到脏entry,也同样将其继续清除。
如果threadLocal的hashcode定位的索引位置与当前threadLocal实际存放在table中的位置不一样,会执行rehash
remove方法就不做介绍了,相信有了set和get方法的认知,renove方法还是比较好理解的
对ThreadLocal有过了解的人,可能会这样觉得,ThreadLocal的内存泄漏是由ThreadLocalMap中的弱引用造成的,假设我们不使用弱引用,使用强引用,看看会发生什么情况。
假设ThreadLocal是强引用,示意图图下
如图所示,堆中的ThreadLocal有两个强引用指向着它,分别标号为1,2。
那么倘若我们想清除ThreadLocal,令ThreadLocalRef = null,那么当前的ThreadLocal对象也不会被清除,因为还有2这条强引用指向着它,糟糕的是,倘若当前的线程属于某个线程池,那么这个线程的生命周期将比较久,始终存在着引用链指向ThreadLocal,那么ThreadLocal这个对象就不会被回收,entry里对应的数据就白白占据着堆内存,得不到访问,也无法清除,除非我们结束当前的Thread。因此在这种情况下,也会存在内存泄漏,而且情况更加严重,还没有有效的办法清楚无用的数据。
尽管存在内存泄漏的可能,但是ThreadLocal的生命周期里,不管是set方法,还是get方法,都对key为null的脏entry做了清理,尽可能地去避免内存泄漏的情况。而且,倘若我们想清除ThreadLocal,直接令ThreadLocalRef = null即可,这也是符合我们的编程习惯的,这个ThreadLocal由于没有引用链指向,因此会被GC回收的,Thread内部的entry数组中还存在着与这个ThreadLocal相关联的数据,因此在清除ThreadLocal前,我们要调用remove方法,养成良好的编程习惯。