了解ThreadLocal
类结构图.png啰嗦一句:查看源码最重要的是先看类结构及类有哪些成员变量以及构造方法,这样可以先从宏观上把握各个类之间的关系,不至于跳来跳去被绕晕;
正所谓:先跳上去看全貌,再钻进去看细节
ThreadLocal:
- threadLocalHashCode/nextHashCode/HASH_INCREMENT:ThreadLocal的hashCode,没有使用Object中hashCode方便解决hash冲突问题
ThreadLocalMap:
- ThreadLocalMap是ThreadLocal的静态内部类
- INITIAL_CAPACITY/table(Entry[])/size/threshold:通过成员变量字段名和类型就很容易联想到HashMap,各个字段作用也跟HashMap类似
Entry:
- Entry又是ThreadLocalMap的静态内部类,并且作为ThreadLocalMap的(key-value)的存储结构
- value:需要存储的数据
- Entry(ThreadLocal<?> k, Object v):构造方法把ThreadLocal作为key
WeakReference:
- Entry的父类,Entry的key通过弱引用指向ThreadLocal
SuppliedThreadLocal:
- ThreadLocal的静态内部类继承ThreadLocal,提供Supplier构造方式;
例如:ThreadLocal local = ThreadLocal.withInitial(Object::new);
Thread:
- threadLocals:每个线程独有的ThreadLocalMap类型成员变量
- inheritableThreadLocals:可以共享父线程的ThreadLocalMap
重点源码分析
set(ThreadLocal<?> key, Object value)
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
//for循环使用拉链法(冲突时不断向后线性探测,到尾部时再从0开始形成环,直至找到不为空的slot放进去)解决hash冲突问题,区别于HashMap的链表法(冲突后直接转链表)
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 说明堆中ThreadLocal已经被GC或置空(Entry并不为空),是一个脏Entry,可以直接替换
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//for循环中没有return,说明找到了一个空的位置,直接new一个Entry放进去
tab[i] = new Entry(key, value);
int sz = ++size;
//set之后检测并清除空的Entry,防止内存泄漏
if (!cleanSomeSlots(i, sz) && sz >= threshold)
//先清除所有脏Entry,再进行扩容;Entry[]初始大小为16,加载因子为2/3,即初始可用容量为10
rehash();
}
cleanSomeSlots和replaceStaleEntry方法比较琐碎,可以参考这篇文章
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
//Entry[]扩容为原来2倍
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
//遍历过程中脏Entry直接将value置为null,帮助GC回收
if (k == null) {
e.value = null; // Help the GC
} else {
//重新hash
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
T get()
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
//找到满足条件的Entry,直接返回
if (e != null && e.get() == key)
return e;
else
//不满足条件,说明set时有hash冲突用拉链法把该Entry放到了其他位置,需要进行环形查找
return getEntryAfterMiss(key, i, e);
}
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)
//清除脏Entry
expungeStaleEntry(i);
else
//进行环形查找
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
void 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) {
//将Entry的key置为null,使其变成脏Entry
e.clear();
//将value置为null,tab[i] = null;,帮助GC时进行清理
expungeStaleEntry(i);
return;
}
}
}
Thread exit()
private void exit() {
if (group != null) {
group.threadTerminated(this);
group = null;
}
target = null;
//threadLocals置为空
threadLocals = null;
//inheritableThreadLocals置为空
inheritableThreadLocals = null;
inheritedAccessControlContext = null;
blocker = null;
uncaughtExceptionHandler = null;
}
ThreadLocal内存模型
引用网上常见的一张示意图
image.png
Thread currentThread = new Thread(() -> {
ThreadLocal threadLocal = new ThreadLocal();
threadLocal.set(new Object());
});
currentThread.start();
- 执行第一句代码时,堆中创建一个Thread对象(图中CurrentThread)
- 执行第四句代码时,jvm开辟新的线程栈,同时栈上保存一个指向CurrentThread对象的引用,即图中CurrentThreadRef
- 执行第二句代码时,堆中开辟空间new一个ThreadLocal对象,同时线程栈上保存一个指向该对象的引用,即图中ThreadLocalRef
- 执行第三句代码时,先获取CurrentThread对象的ThreadLocalMap实例,如果为空则创建并初始化一个;创建以后就可以通过set方法,把当前ThreadLocal作为key,new Object()作为value存储进去
- 通过文章开头的类结构图可以看出key对ThreadLocal对象的引用是个弱引用
内存泄漏分析
弱引用:一个对象如果只有一个弱应用指向它,那么GC执行时只要发现它就会直接回收掉
- 从上一节内存模型图来看,ThreadLocalRef到ThreadLcoal的引用失效时,则ThreadLcoal就只有key的一个弱引用指向它,那么下次GC时就会被回收。此时Entry中key就变成了null,此时value就再也不会被外部访问到,变成脏Entry
- 此时只要CurrentThread实例存在,ThreadLocalMap就不会被回收,则Entry中key为null的value就会造成内存泄漏
- 从前面源码中可以看到jdk为了规避该内存泄漏的风险,在set,resize,getEntry这些地方都会对这些脏entry进行清除
- 如果一个线程先set一个大对象在ThreadLocalMap中,则该对象在线程结束之前不会被回收
总结
- 两个问题一:使用拉链法解决Entry的冲突,到达加载因子时还需要扩容、rehash,效率不高
- 两个问题二:为了防止内存泄漏在get/set/resize等操作时都需要清理脏Entry,设计不够优雅
- 使用ThreadLocal结束时尽量都手动调用一次remove进行清除
网友评论