一般性的介绍(内存泄漏,基本用法,应用场景,最佳实践等)官网和其他博客都说的很清楚,这里主要记录一下我认为threadlocal的最核心的地方和难点。
主要会包括以下方面:1. 内存泄漏问题,对象引用关系 2. threadLocalHashCode值的选取 3. 深入探究set方法。
注:本文中代码选自jdk8。
其实这两个问题,乃至其他的问题我认为都是对象引用关系,这个是最核心的。
1. 内存泄漏问题,对象引用关系
这个图引用自(https://mp.weixin.qq.com/s/bxIkMaCQ0PriZtSWT8wrXw##)架构师修炼宝典 这个公众号。
上图已经将引用关系讲的十分清楚了,由于每个线程对他自己的ThreadLocal持有的引用是放在线程私有的栈里面的,那一旦这个线程丢失了对自己的ThreadLocal的引用之后,如果gc了,那么这个线程里面的ThreadLocalMap里面的对应的Entry的key对ThreadLocal的weakReference将会被回收,那么这里的value值将会变成不可达状态,如果这个entry没有及时被remove掉,那么就会导致内存泄漏问题。
2. threadLocalHashCode值的选取
先说说这个是什么 看代码:
/**
* 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);
}
实际上就是每个threadlocal对应的hashCode!看看他的生成原理:
private final int threadLocalHashCode = nextHashCode();
继续
/**
* The difference between successively generated hash codes - turns
* implicit sequential thread-local IDs into near-optimally spread
* multiplicative hash values for power-of-two-sized tables.
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* Returns the next hash code.
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
就是每个threadlocal的hash值就是i*HASH_INCREMENT+HASH_INCREMENT;
i为第i个出引用这个threadLocalHashCode的线程。
注意这里是final修饰的threadLocalHashCode值,每个线程的threadlocal只会有一次赋值的机会。
这个数是魔数,由于每一次扩容都是2的整数次方,用这个算法产生的hash值能够均匀地落到数组的每一个小格子里面,也就是说如果正常流程添加满,是不会冲突的,是可以达到空间效益最大化并且减少hash冲突的。(当然不可能会有加满的时刻,threadlocalMap在被加满前会扩容的)
(https://mp.weixin.qq.com/s/bxIkMaCQ0PriZtSWT8wrXw##)
3.深入探究set方法。
在我们看最核心的代码,set方法,前先看看简单的get,最终get会调用set的。
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
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();
}
这里的getMaps一开始我还以为是把t作为key值去map里面查,实际上不是的,是返回当前thread持有的threadlocalmap。
/**
* 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);
}
这里的get就是直接算出hashcode和len-1的与,然后去找到中国entry取值,getEntryAfterMiss这个分支我们先不看。我们先看上面的setInitialValue。
/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
这里有一个小地方,就是初始化方法是在第一次调用get的时候触发。
然后我们接着看TheadLocal的内部类ThreadLocalMap.set。
/**
* 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;
}
//走到以上都是常规逻辑,走到这里就有点奇怪了。
/*其实这里就是为了解决内存泄漏问题的,当走到k==null且entry非空的地方就意味着,弱引用被gc了。因此这个staleentry需要被替换掉。*/
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
继续看replaceStaleEntry
private void replaceStaleEntry(ThreadLocal> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
int slotToExpunge = staleSlot;
//循环往前找需要被替换掉的staleslot,直到空为止,然后将最前面的i值赋值给slotToExpunge,表不可能满,肯定不能走完一个循环,下面的同理
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;
//找到相同的key之后,交换掉i和staleSlot,之后i就是坏节点,是需要被擦除的
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
/*如果只有staleSlot前面没有节点,那就把i擦除掉,为什么要这样,可能是因为staleSlot是冲突之后偏移更加短的值,会更加接近真实hash值*/
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
/*如果这个k为空的话把这个节点变成坏节点,为什么要这样,因为staleSlot要么会拿来交换,要么就会拿来建新值,终究会变成好人,但是这个铁定是坑爹的,如果交换了之后,在下面的cleanSomeSlots之后不久交换后的坑爹也会被删掉*/
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 如果找不到相同的key,那就直接赋值
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// 如果这两个值不等才去删除,因为这样就找到了坑爹货,staleSlot到了这一步都是好人
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
看expungeStaleEntry
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 能进来的这个方法的staleSlot都铁定是坑爹的。
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--;
//一直对当前的entry里面的key进行修正位置,(有可能之前的线性探测之后,位置偏移过多,优化表),这里总会遇到空的, 因为表不可能满,会扩容。
} else {
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;
}
最后我们看cleanSomeSlot
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
//下面的代码是清除操作e.get获取的是key值,也就是threadlocal
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;
}
这个方法一开始看是不太好理解为什么要循环这个次数:(n >>>= 1) != 0。实际上代码注释也说了,这个是一个tradeoff,可以理解为预期。如果遇到的空值了,理解为可能内存泄漏情况严重,而且刚gc过,那就是继续加大预期继续清除。
回过头来看我们之前忽略过的一个方法getEntryAfterMiss,这个是在一开始key匹配不上的时候调用的。
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;
}
这个方法其实中间这里会调用expungeStaleEntry这个东西,我们也知道这个东西里面会清除掉当前slot的entry,并且还会做一个循环遍历的rehash操作,那会不会发生那种原本能找到这个key,但是却在rehash之后找不到的情况呢??我一开始还有这个担心,但是实际上肯定也不会的。
因为这里rehash只会去占用null的entry,那假如我们在expungeStaleEntry里面恰好遇到了合适的key,那结论就是当前的e就是我们要找的那个entry,因为在循环体里面k==null的时候i是不会步进的,而且i之前和这个key对应的hash之后的这一段是被证明过entry非空的!
然后还有一个问题是,这个get能够完全保证能找到这个合适的key吗,中间会不会有null之类的事情发生?我觉得应该是可以的,因为就算内存泄漏了,但是entry还是非空,而且几乎每个重要操作都有清除stale entry和rehash的操作,应该是足够健壮的!