阅读本篇博客您将了解如下内容:
- TreadLocal的作用。
- ThreadLocal的实现原理。
- ThreadLocal是否会引起内存泄漏,在什么样的条件下引发,如何避免。
1、ThreadLocal的作用
使用线程封闭的指导思想来解决变量共享的并发安全问题,–可以简单理解为各玩各的雨女无瓜
2、ThreadLocal 源码初探
2.1 ThreadLocal的set方法的代码如下:
// 获取当前线程对象
Thread t = Thread.currentThread();
// 根据线程对象获取ThreadLocalMap对象(ThreadLocalMap被Thread持有 threadLocals )
ThreadLocalMap map = getMap(t);
// 如果ThreadLocalMap存在,则直接插入;不存在,则新建ThreadLocalMap
if (map != null){
map.set(this, value);
}else{
createMap(t, value);
}
threadLocals : 线程Thread 对象内部属性,这个属性默认就是null,由ThreadLocal.set 进行初始化。如果已经初始化了,则使用当前ThreadLocal的实例作为key,待存值作为value保存到当前线程的ThreadLocalMap变量中。
其实这里以前有个问题一直困扰我,为什么不直接给Thread的hreadLocals属性设置成object类型,然后set值的时候直接赋值给 threadLocals。
后面明白了一个Thread加一个ThreadLocal 只能保存一个val,但是一个Thread 可以对应多个ThreadLocal,一个线程对象属性可能被多个ThreadLocal共同持有。
ThreadLocal<String> th1 = new ThreadLocal<>();
ThreadLocal<String> th2 = new ThreadLocal<>();
th1.set("123");
th2.set("456");
System.out.println(th1.get());
System.out.println(th2.get());
2.2 ThreadLocalMap是ThreadLocal 一个内部类,其本质和Map没有任何关系,也没有实现Map接口。内部使用Entry(类似Map key-value 对象) 数组存储数据,使用Hash 算法计算下标。
static class ThreadLocalMap {
/**
* 使用弱引用包装ThreadLocal 作为map Key
* 在某些情况下key会被回收掉
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
//使用hashCode 计算下标
//这里使用hashCode 跟我们普通对象不一样,通过自增长计算出来
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
2.3 ThreadLocalMap的set方法,在出现hash冲突时,只是将下标向后移动,找到空闲的位置。正如set 方法注释上写,set 并不支持快速set、冲突了通过向后遍历数组找到空位置。
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
// 循环里面处理hash冲突情况
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;
}
}
// 当hash 冲突时,会一直向后找,直到有空位置
tab[i] = new Entry(key, value);
int sz = ++size;
//在位置i 后面搜寻是否有key 回收情况,则删除数组位置,返回true
// 当有删除,不需要判断扩容情况了,一个新增对应删除,容量都没有增加
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash(); //扩容数组
}
2.4 ThreadLocalMap的get方法会通过向后遍历匹配出来,这个以HashMap 相比差距挺大的。插入、查找效率都在N之间。
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);
}
//向后查找符合要去key
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) { //遇到null 就停下来
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
// k 已经被gc 了
//在数组中删除这个位置,这样可以帮助value 回收了
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
3、ThreadLocal会造成内存泄漏吗?
答案是会的。
1、当线程栈持有ThreadLocal,作为Entry key的它不会被gc,当ThreadLocalRef 引用失效时(线程生命周期结束),ThreadLocal 就会在下次gc时被回收掉。
但是目前咱们开发过程中都是使用池化的思想即使用线程池进而达到线程复用,所以线程并未结束,这种情况下ThreadLocalRef 就不会失效,但是它又作为ThreadLocalMap中Entry 的key,它不会被gc,导致内存泄漏,所以需要手动remove。