ThreadLocal源码分析理解弱引用和内存泄漏
Posted lllllLiangjia
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ThreadLocal源码分析理解弱引用和内存泄漏相关的知识,希望对你有一定的参考价值。
目录
一、说明
ThreadLocal 是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,适用于各个线程不共享变量值的操作。
cookie,session等数据隔离都是通过ThreadLocal去做实现的
三个API
void set(T value):将此线程局部变量的当前线程副本设置为指定值。
T get():返回此线程局部变量的当前线程副本中的值。
void remove():删除此线程局部变量的当前线程值。
二、源码分析
set
得到当前线程,根据当前线程获取到ThreadLocalMap(ThreadLocal的内部类)集合,判断该集合是否为null,如果为null那就创建这个Map,如果不为null,那就set新值进去
public void set(T value) {
Thread t = Thread.currentThread();// 获取当前线程
ThreadLocalMap map = getMap(t);// 获取ThreadLocalMap对象
if (map != null) // 校验对象是否为空
map.set(this, value); // 不为空set
else
createMap(t, value); // 为空创建一个map对象
}
由这个getMap方法可以看出,ThreadLocalMap类型的threadLocals是线程Thread中的一个属性,那Thread中包含ThreadLocalMap的实例对象时,Thread有指向TheadLocalMap的引用。这点先记住,后面会用。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
ThreadLocalMap底层结构
static class ThreadLocalMap {
private Entry[] table;
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
……
}
它的Entry是继承WeakReference(弱引用)的,每个线程对应一个ThreadLocalMap数组,但是每个线程中可能存在多个ThreadLocal类型的变量,这些ThreadLocal变量为Map的key值,变量存放的数据为Value。ThreadLocalMap数组初始化长度为16,对应16个Entry节点,每个ThreadLocal类型的变量对应一个节点。
结构图
ThreadLocalMap是一个Entry数组
map.set
用hashcode得到散列值获取到应该放在的数组索引
- 如果该索引有值,并且key与要插入的值相等,那么就覆盖原来的旧value值。
- 如果该索引有值,并且key等于null,说明旧Entry对应key的ThreadLocal变量已经被清空回收(此处有疑问,最后会解答),那这个Entry已经没有意义。新添加的key、value可以将这个旧的Entry替换掉。
- 如果key值不等也不为null,此时发生hash冲突,那么遍历数组的下一个索引,直到为null为止跳出for循环,根据key和value初始化一个Entry对象放在tab数组对应的索引上。
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
// 不为空,for循环进来
e != null;
// 如果位置i的不为空,而且引用地址也不等,那就找下一个空位置,直到为空为止
e = tab[i = nextIndex(i, len)]) {
// 指向的引用地址
ThreadLocal<?> k = e.get();
// 引用地址相等,直接替换返回
if (k == key) {
e.value = value;
return;
}
// GC回收弱引用的对象key,此时这个Entry的key为null,但是value还有值
if (k == null) {
// 在该索引位置,用新的key、value替换旧的Entry值
replaceStaleEntry(key, value, i);
return;
}
}
// 如果tab[i]的Entry等于null,初始化一个Entry对象
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
get
- 在get()方法中调用getEntry(this)方法。
- 判断entry数组不为空,并且散列值得到的数组索引下的Entry的key等于要查询的key值,那么直接返回。
- 如果两个key不相等,那么说明插入的时候发生了hash冲突。进入getEntryAfterMiss方法,while循环遍历数组。
- 如果Entry的key和要查询的key值相等,那么返回该Entry
- 如果遍历时Entry的key为null,那清除key为null的Entry,并且重新散列发生哈希冲突的Entry(此处有疑问,最后会解答)。
- 遍历完之后如果没有相等值,那就返回null。
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// 从ThreadLocalMap集合中查找
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
//entry数组不为空并且散列值得到的数组索引下的Entry的key等于要查询的key值
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// get的时候一样是根据ThreadLocal获取到table的i值,然后查找数据拿到后会对比key是否相等 if (e != null && e.get() == key)。
while (e != null) {
ThreadLocal<?> k = e.get();
// 相等就直接返回,不相等就继续查找,找到相等位置。
if (k == key)
return e;
// key为null,说明被GC回收了对应的ThreadLocal变量
if (k == null)
// 清除key为null的Entry,并且重新散列发生哈希冲突的Entry
expungeStaleEntry(i);
else
//如果key不一致,就判断下一个位置,如果set的时候哈希冲突严重的话,效率还是很低的。
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
remove
- 调用remove方法,获取到本线程对应的ThreadLocalMap对象,如果不为null,则调用ThreadLocalMap内部类的方法remove(ThreadLocal<?> key)。
- 根据哈希值获取对应的索引对应的entry,由于可能有哈希冲突的存在还要判断key是否相等
- 如果key不相等那么向下遍历数组。
- 如果key相等,那将entry对应的引用置为null,方便GC回收,并调用expungeStaleEntry方法,清除key为null的Entry,并且重新散列发生哈希冲突的Entry
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
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,由于可能有哈希冲突的存在还要判断key是否相等
if (e.get() == key) {
// entry对应的引用置为null,方便GC回收
e.clear();
// 清除key为null的Entry,并且重新散列发生哈希冲突的Entry
expungeStaleEntry(i);
return;
}
}
}
expungeStaleEntry
该方法做三件事
- 清空staleSlot对应索引的entry。
- 如果该索引后面entry的key为null,那么就进行清空。
- 如果该索引后面entry的key不为null,那就重新哈希。方便之后的get操作。
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 将旧的entry以及对应的value置为null,方便回收
tab[staleSlot].value = null;
tab[staleSlot] = null;
// 集合中的数量减一
size--;
// 重新哈希直到遇到 null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 将key为null的Entry值给清除
if (k == null) {
// 将旧的entry以及对应的value置为null,方便回收
e.value = null;
tab[i] = null;
size--;
} else {
// 因为前面已经清空了一些旧的entry,前面的空位增多,
// 那些由于哈希冲突往后放置的entry可以重新散列,就可以往前放,这样get操作时遍历次数会减少
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
resize扩容
*2扩容
int newLen = oldLen * 2;
三、思考与总结
想共享线程的ThreadLocal数据怎么办?
使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值,我们在主线程中创建一个InheritableThreadLocal的实例,然后在子线程中得到这个InheritableThreadLocal实例设置的值。
private void test() {
final ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set("帅得一匹");
Thread t = new Thread() {
@Override
public void run() {
super.run();
Log.i( "张三帅么 =" + threadLocal.get());
}
};
t.start();
}
为什么ThreadLocalMap的key要设计成弱引用?
key不设置成弱引用的话就会造成和entry中value一样内存泄漏的场景。
每一个Thread都包含一个ThreadLocalMap,ThreadLocalMap可以包含多个Entry,Entry的key是不同的ThreadLocal变量。
Thread->ThreadLocalMap->ThreadLocal
这样的话,虽然ThreadLocal在客户端代码中置为null,那只是将各个线程栈指向堆中ThreadLocal对象的引用删除。但是作为key的ThreadLocal,在ThreadLocalMap里指向它的引用还存在,所以需要用弱引用,在没有其他强引用指向的时候,通过GC回收来删除key。
如果此时还困惑,借用知乎一篇博客(为什么Thread作为key用弱引用--图文解释)进行解释。
示例代码
public class ThreadLocalDemo {
public static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
public static void main(String[] args) {
ThreadLocalDemo.threadLocal.set("hello world main");
System.out.println("创建新线程前,主线程" + Thread.currentThread().getName() + "的threadlocal字符值为:" + ThreadLocalDemo.threadLocal.get());
try {
Thread thread = new Thread() {
@Override
public void run() {
ThreadLocalDemo.threadLocal.set("new thread");
System.out.println("新线程" + Thread.currentThread().getName() + "的threadlocal字符值为:" + ThreadLocalDemo.threadLocal.get());
}
};
thread.start();
thread.join();
} catch (Exception e) {
System.out.println(e);
}
System.out.println("创建新线程后,主线程" + Thread.currentThread().getName() + "的threadlocal字符值为:" + ThreadLocalDemo.threadLocal.get());
}
}
代码中对象所在堆栈的对应图
ThreadLocal对象的引用有两种
- main方法和thread的run方法中都有对ThreadLocal对象的直接引用,所以主方法栈和thread线程栈有引用(强引用)指向。
- 前面讲set方法时也提到了,每个线程对象中有ThreadLocalMap属性,ThreadLocalMap集合存储的Entry的key也是ThreadLocal变量,此时主线程对象和thread线程对象各自的ThreadLocalMap也都有引用(弱引用)指向。
当我们主动释放main线程栈和thread线程栈中的threadLocal引用,那此时堆栈图如下所示。
ThreadLocalDemo.threadLocal = null;
释放线程中引用后堆栈对应图
此时主方法栈和thread线程强引用都消失,只剩下弱引用。弱引用在GC回收的时候进行清除,所以此时ThreadLocalMap中该Entry的key为null,这就是为什么前面会出key为null的原因。
使用弱引用将key为null,对应的Entry如何清除?
set()方法中当发现key为null的Entry,会调用replaceStaleEntry()方法,该方法可以替换掉陈旧的Entry元素
get()方法中当发现key为null的Entry,会调用expungeStaleEntry()方法,该方法可以删除陈旧的Entry元素
remove()方法中也会调用expungeStaleEntry()方法。
内存泄露问题
弱引用的对象拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。 不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
就比如线程池里面的线程,线程都是复用的,那么之前的线程实例处理完之后,出于复用的目的线程依然存活,所以,ThreadLocal设定的value值被持有,导致内存泄露。
按照道理一个线程使用完,ThreadLocalMap是应该要被清空的,但是现在线程被复用了。
解决
在代码的最后使用remove就好了,我们只要记得在使用的最后用remove把值清空就好了。Entry就没有被指向的引用了
以上是关于ThreadLocal源码分析理解弱引用和内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章
深入浅出多线程编程实战ThreadLocal详解(内存泄漏)