threadlocal与弱引用
threadLocalMap
1. 内部结构
内部使用threadLocalMap存储线程私有变量,其中threadLocal作为key,用户存储数据作为value
//... ThreadLocal类内部
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
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();
}
2. 生命周期
threadLocalMap和线程绑定
1)threadLocal的get、set等方法,都是先获取当前线程,然后通过当前线程获取threadLocalMap。
2)而threadLocalMap其实是Thread类的一个成员变量,它和线程的生命周期相同。因此每次调用threadLocal.set、get、remove方法,都是获取了这个threadLocalMap,其中threadLocal作key,用户数据作为value
//... Thread类内部
ThreadLocal.ThreadLocalMap threadLocals = null;
3. 索引计算
每次ThreadLocal t = new ThreadLocal()
,并调用set()
,都会在这个map里添加一个threadLocal类型的key。
1)threadLocal的hash值对(entry长度-1)取模,得到索引。
int i = key.threadLocalHashCode & (table.length - 1);
2)获取entry数组中的此元素,如果不为空且key相等,返回该元素
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);
}
3) 否则用线性探测法寻找元素。遍历过程中清除null值的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;
}
4)在set()
方法时,如果遍历到entry的key为null,会替换这个entry。
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();
}
4. 扩容
1)数组的初始容量是16。
2)如果在set()
方法最后,遍历部分数组但没有清理掉任何数据时,且数组中entry数量 >= 数组长度的2/3(threshold容量),执行rehash()
方法
int sz = ++size;
// 从i开始查找key为null的非空entry(脏entry),找到后清理并继续往后,直到空entry
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
rehash()
方法方法中,清理key为null的entry,此时如果 size >= threshold*2/3
,扩容原来的2倍容量
private void rehash() {
// 清理null值key的entry
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
哈希冲突解决方案
- 线性探测法
出现hash冲突时(计算出索引冲突),继续探测下一个索引槽位,直到探测到可以插入为止。threadLocalMap索引冲突解决方式 - 再hash法
构造多个hash函数,发生冲突时,更换hash函数并计算,直到不冲突 - 链地址法
将hash冲突的节点生成一个链表。例如hashmap - 建立公共溢出区
将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表
弱引用
threadlocalMap的enrty对象继承了WeakReference,这个enrty对象持有对threadlocal的弱引用。
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
对象只被弱引用所引用,在下一次gc时该对象就会被回收。
内存泄漏
1)threadLocal的强引用丢失,例如手动把引用设置为null、或者这个强引用在局部方法里(前两者会有人这么做吗)。这样就会只剩下弱引用导致threadLocal对象被回收,entry对象的key变为了null,导致value无法被访问。但这种情况下似乎很少会出现。
2)在调用get、set方法时,因为线性探测法的原因进行遍历,会清除key为null的entry,并不能保证清除所有的key为null的entry。resize()扩容的时候会清楚所有脏entry。
3)或者说,线程池中的线程的生命足够长,某一段代码在向threadLocal设置value后,没有remove()就结束并重新将线程放回线程池,导致内存泄漏。就是在说线程没有即使结束,然后回收掉threadLocalMap,导致内存的泄漏。