引言
ThreadLocal,线程变量,线程可以将本次线程内经常使用的变量存储到ThreadLocal中,方便本次线程内其他的操作使用。
注:特别需要注意的是,有些博客说ThreadLocal可以保证线程安全,这是错误的认识,ThreadLocal存储的只是每一个线程的本地变量,并未涉及到临界区,不能保证线程安全。在使用的时候一定要注意使用场景,ThreadLocal存储的应该是每个线程内部共享的一些数据,而非临界区数据。
ThreadLocal源码解析
用过ThreadLocal的程序员们可能都知道,ThreadLocal最常用的三个方法无非就是get()
,set(T value)
和remove()
,本文就这三个常用的方法来分析ThreadLocal的相关源码。
ThreadLocalMap
在分析具体操作源码之前,先看看ThreadLocal底层的存储结构ThreadLocalLocalMap
吧,这里着重讲解它的get/set/remove
操作,关于其他部分的源码,有兴趣的读者可以自行阅读。
- get操作
private Entry getEntry(ThreadLocal> key) {
//找到key在table中的位置
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
//找到的值是当前查询的key的值,返回
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);}
从上面的源码可以看出,假如直接hash找到了对应的slot,返回即可,假若没找到呢。。。没找到执行下面的操作:
private Entry getEntryAfterMiss(ThreadLocal> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
//在table里查询,如若找到待查询entry,返回该entry
while (e != null) {
ThreadLocal> k = e.get();
//entry的key为待查询key
if (k == key)
return e;
//entry的key为null时,表明当前的entry并不是最新的,需要做相关的操作删除掉非最新的entry
if (k == null)
expungeStaleEntry(i);
else
//i++,i == len 时 i = 0
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
- set操作
//set操作,key为当前ThreadLocal对象,value为对应的变量值
private void set(ThreadLocal> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//hash出当前key所在slot
int i = key.threadLocalHashCode & (len-1);
//处理tab[i] != null
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal> k = e.get();
//key存在,覆盖value
if (k == key) {
e.value = value;
return;
}
//key不存在,需要在table对应位置插入entry
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//tab[i] == null,直接做插入
tab[i] = new Entry(key, value);
int sz = ++size;
//sz >= len * loadFactor(loadFactor = 2/3)需要resize
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
- 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)]) {
//找到对应的entry
if (e.get() == key) {
//clear entry
e.clear();
//删掉该entry
expungeStaleEntry(i);
return;
}
}
}
ThreadLocalMap的相关重要操作到这里就分析完毕,ThreadLocal的相关操作都是在ThreadLocalMap操作基础上封装的。
ThreadLocal.get()
public T get() {
Thread t = Thread.currentThread();
//获取当前线程的threadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//entry不为null,返回对应的value
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
//否则,返回null
return setInitialValue();
}
ThreadLocal.set(T value)
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
//当前线程的threadLocalMap已创建,直接往map中set值
if (map != null)
map.set(this, value);
//当前线程的threadLocalMap未创建,需要先创建再set
else
createMap(t, value);
}
ThreadLocal.remove()
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
后记
到这里为止,ThreadLocal的相关源码分析就结束了。在最后还需要讲讲之前我在使用ThreadLocal遇到的坑。
在业务线工作的时候,发现很多时候都有线程内共享变量的需求,ThreadLocal最适合这个场景,但是在使用ThreadLocal的时候,总是忘记remove,终于出现了一次巨大的故障。。。ThreadLocal和ThreadPoolExcutor一起使用时,出现了内存泄漏,在那一刹那,机器内存使用率蹭蹭往上涨,排查了很久,才发现是ThreadLocal忘记remove了,为什么ThreadLocal不remove会造成内存泄漏呢?
ThreadLocal导致的内存泄漏
之前查过很多资料,都是ThreadLocal不remove不会导致内存泄漏,但是事实却不是如此,血一般的教训啊。
每个Thread都有一个ThreadLocalMap,虽说它是弱引用,但是弱引用仅仅都是针对key,每个key都弱引用指向ThreadLocal。当把ThreadLocal实例置为null的时候,没有任何的强引用指向ThreadLocal实例,ThreadLocal将会被gc回收,但是,value却不能被回收,因为存在一条从当前线程连接过来的强引用,只有当前 thread结束以后,thread,map,value将会全部被gc回收。
针对这个问题,ThreadLocal为了减少内存泄漏的机会,在get/set的时候都会检查ThreadLocalMap中是否有key == null的value,会把这部分value删除掉。程序员们可能这时候有这个疑惑,既然都这么做来避免了,为什么会出现内存泄漏啊?但是,大家就没想过么,假若线程不被销毁,它的ThreadLocal的get/set也一直未被使用,这不就出现了实际意义上的内存泄漏么?刚好线程池里的一部分核心线程是不会被销毁的,假若出现这种情况是一定会出现内存泄漏的!!!所以,在使用ThreadLocal结束之后一定不要忘记remove!!!