同一个threadlocal变量所包含的对象,在不同的线程中是不同的副本。
既然是不同的线程拥有不同的副本且不允许其他线程访问,所以不存在共享变量的问题。
解决的是变量在线程间隔离在方法或类之间共享的问题。
解决这个问题可能有的两种方案
第一种:一个threadlocal对应一个map,map中以thread为key
这种方法多个线程针对同一个threadlocal1是同一个map,新增线程或减少线程都需要改动map,这个map就变成了多个线程之间的共享变量,需要额外机制比如锁保证map的线程安全。
线程结束时,需要保证它所访问的所有 ThreadLocal 中对应的映射均删除,否则可能会引起内存泄漏。-----为什么会导致内存泄漏呢?见下文
第二种:一个thread对应一个map,map中以threadlocal为key区分
Thread中有一个变量
ThreadLocal.ThreadLocalMap threadLocals = null;//每个线程维护一个map
ThreadLocalMap
static class Entry extends WeakReference {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
private Entry[] table;
ThreadLocalMap维护了一个table数组,存储Entry类型对象,Entry类型对象以ThreadLocal为key,任意对象为值的健值对
ThreadLocal的get set
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);//返回当前线程维护的threadlocals变量
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
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;
}
为什么说threadlocal会存在内存泄漏:
每个thread维护的threadlocalmap key是指向threadlocal的弱引用,当没有任何其他强引用指向threadlocal的时候,gc会把key回收。
但value是是thread指向的强引用,thread不结束,value不会被回收。
所以当threadlocal不可用但thread还在的这段时间内,会存在所说的内存泄漏。尤其当使用线程池的时候,线程被复用。
jdk有没有相应的处理:
再回到threadlocalmap的get set方法
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); //获取当前线程的map
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); //找到当前的threadlocal
if (e != null)
return (T)e.value;//取值
}
return setInitialValue();// map为null或者map找不到指定key时,初始化基本值,不展开
}
//getEntry函数
private Entry getEntry(ThreadLocal key) {
int i = key.threadLocalHashCode & (table.length - 1);//根据key计算索引
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);//table中该索引位置对象e为null 或者 索引位置key不符进入getEntryAfterMiss
}
//getEntryAfterMiss函数
private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
//遍历table一直到找到了k=key的位置,返回相应对象e
//遍历过程中如果遇到了k为null,即调用expungeStaleEntry清理该entry,即前面所说的内存泄漏,这里是处理的一个时机
while (e != null) {
ThreadLocal k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len); //循环遍历table,ThreadLocal采用的是开放地址法,即有冲突后,把要插入的元素放在要插入的位置后面为null的地方
e = tab[i];
}
return null;//如果是e为null 返回null
}
//expungeStaleEntry 函数
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot:key为null的索引位置的对象.value置为null,对象也置为null
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null; //遍历是从staleSlot之后到遇到的第一个e为null
i = nextIndex(i, len)) {
ThreadLocal k = e.get();
if (k == null) {//遍历的过程中遇到key为null做和上面同样的处理
e.value = null;
tab[i] = null;
size--;
} else { //key不为null的重新hash
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;
}
//再看set
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
//重点在map.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)]) { //如果根据索引找到的entry不是空的
ThreadLocal k = e.get();
if (k == key) { //key相同,value直接覆盖
e.value = value;
return;
}
if (k == null) { //遍历过程中key为null,清除
replaceStaleEntry(key, value, i);
return;
}
}
//上面没有处理掉,找到第一个为null的能用的坑位,new一个entry放入
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
能大致看到,上面的代码中,threadlocalmap的get set都会做对key为null的清除工作,从而解决了上面说的内存泄漏问题,只是这种处理依赖对set get的调用
threadlocalmap的remove方法:
private void remove(ThreadLocal key) {
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)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
综上所述,很多地方会看到有这样的两条建议:
1 .使用者需要手动调用remove函数,删除不再使用的ThreadLocal.
2 .还有尽量将ThreadLocal设置成private static的,这样ThreadLocal会尽量和线程本身一起消亡。