前言
最近在肝原神,略微有些玩物丧志的赶脚。但鲁迅先生说过任何人都逃不过真香定律,即使他再喜欢学习也不行。---------------原神真好玩QAQ。今天就简单了解下“拧螺丝”不太能用到的技能吧,TreadLocal简单了解。准备缩一缩原神时间把Leetecode实录第二篇写出来。
正文
一直听说ThreadLocal使用不当会造成内存溢出,但具体何时会造成内存泄漏以及为什么会造成内存泄漏也没有机会了解。实际还是应用不足,所以本次就对ThreadLocal做一个简单的了解以避免使用不当编写出难以调试的BUG。
源码分析
首先应该探究的是调用set
方法存储的内容被存储在了那里,那么首先观察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);
}
可以看到value被存储在了一个ThreadLocalMap当中,那么该变量被存储在哪里呢可以查看getMap(t)
方法一探究竟。打开getMap(t)
源码
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
直接返回了Thread的threadLocals属性,并且可以从set(T value)
中看到传递给getMap(t)
的参数即是当前线程,此时可以有一个初步的认识,每个Thread实例都有一个threadLocals属性用来存放线程的私有数据。不妨进入Thread类中查看一下。
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
当然默认情况下是null,并且可以看到其数据类型是ThreadLocal的一个内部类。
ThreadLocalMap 是一个类似HashMap的数据结构,其键是对当前ThreadLocal的一个弱引用。回到set
方法可以看到实际存储数据的操作为map.set(this,value)
这是ThradLocalMap的一个方法,不妨进入该方法看一下。
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;
//计算hash后在tab中的索引位置。key.threadLocalHashCode是一个科学数值,
//尽量确保不发生hash冲突,&操作是对长度取余,但长度必须是2的n次方,这个
//在纸上画一下就明白了(例如:15&3=15%4)
//15 二进制 1111
//4 二进制 0100->2^n 一定是0*n 1 0*n格式
//3 二进制 0011->2^n-1 则为 0*n 1*n格式
//就能看出为什么一定要2^n
int i = key.threadLocalHashCode & (len-1);
//发生hash碰撞时的操作
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal> k = e.get();
//如果key与当前key相同,替换原值
if (k == key) {
e.value = value;
return;
}
//如果节点过期,替换该节点
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
//清理过期Entry即key为null的对象
if (!cleanSomeSlots(i, sz) && sz >= threshold)
//如果清理后空间任达不到预期效果,则扩容
rehash();
}
可以看到最终数据被封装成为Entry存储在了Entry[]中,关键操作在注释中给出。
get()
方法相对简单。
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();
}
获取当前线程的TheadLocalMap实例属性并以ThreadLoacl获取相应数据,如果当前ThreadLocalMap没有初始化,则初始化该ThreadLocalMap,初始化代码如下
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
内存泄漏分析
上面说到,ThreadLocalMap 的key为ThreadLocal的弱引用,为什么使用弱引用。思考下如果使用强引用或发生什么。
首先分析下引用关系(这里网络上有不错的示意图)。
栈中ThreadLocal属性引用指向堆中的ThreadLocal实例,TheradLocalMap的key以弱引用的方式引用堆中的ThreadLocal,当前线程的threadLocas又引用堆中的ThreadLocalMap。
此时如果引把TreadLocalMap key对ThreadLocal实例的软引用换为强用,思考一下如果栈中的TheadLocal消亡(可能是方法执行完成或其他原因),此时通过可达性分析算法分析,假设当前线程的threadLocals为GCRoot那么threadLocals指向ThreadLocalMap,而ThreadLocalMap的key指向堆中的TreadLocal实例。只要线程不结束该ThreadLocal就永远不会被回收,但此时ThreadLocal本身已经没有任何意义,因为栈中ThreadLocal已经不存在了。
但TreadLocalMap key对ThreadLocal实例的引用是软引用时会怎样,栈中的ThreadLocal消亡,指向堆中ThreadLocal实例的引用只有ThreadLocalMap key的软引用,而根据软引用的性质,堆中的ThreadLocal实例将在下次GC时被回收。
但是现在万事大吉了吗,思考ThreadLocal被回收,那么ThreadLocalMap中就会出现key为null的Entry且该数据对应的是被回收的ThreadLocal的数据,此时它同样无法被访问到,积累多了同样会造成内存泄漏。ThreadLocal的set``get
以及remove
内置了清理过期Entry的操作,所以适当使用这几个方法避免因Entry过期产生的内存泄漏
最后:大幻梦森罗万象狂气断罪眼\ (•◡•) /