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。
示例代码
public class ThreadLocalDemo
public static ThreadLocal<String> threadLocal1 = new ThreadLocal<String>();
public static ThreadLocal<String> threadLocal2 = new ThreadLocal<String>();
public static void main(String[] args)
ThreadLocalDemo.threadLocal1.set("threadLocal1:hello world main");
ThreadLocalDemo.threadLocal2.set("threadLocal2:hello world main");
System.out.println("创建新线程前,主线程" + Thread.currentThread().getName() + "的threadlocal字符值为:" + ThreadLocalDemo.threadLocal.get());
try
Thread thread = new Thread()
@Override
public void run()
ThreadLocalDemo.threadLocal1.set("threadLocal1:new thread");
ThreadLocalDemo.threadLocal2.set("threadLocal2: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;
释放栈中引用后堆栈对应图
此时用户将threadLocal1和threadLocal2都赋值为null值。主方法栈和thread线程强引用都消失,只剩下Entry节点key指向ThreadLocal对象的弱引用。
ThreadLocal对象被GC回收后
弱引用所指向的ThreadLocal对象在GC回收的时候进行清除,所以此时ThreadLocalMap中该Entry的key为null,这就是为什么前面会出key为null的原因。
使用弱引用将key为null,对应的Entry如何清除?
set()方法中当发现key为null的Entry,会调用replaceStaleEntry()方法,该方法可以替换掉陈旧的Entry元素
get()方法中当发现key为null的Entry,会调用expungeStaleEntry()方法,该方法可以删除陈旧的Entry元素
remove()方法会将整个Entry节点清空,避免内存泄漏。并且也会调用expungeStaleEntry()方法,删除该节点后面的陈旧Entry元素,并重新哈希。
内存泄露问题
弱引用的对象拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。 不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
就比如线程池里面的线程,线程都是复用的,那么之前的线程实例处理完之后,出于复用的目的线程依然存活,所以,ThreadLocal设定的value值被持有,导致内存泄露。
按照道理一个线程使用完,ThreadLocalMap是应该要被清空的,但是现在线程被复用了。
解决
在代码的最后使用remove就好了,我们只要记得在使用的最后调用ThreadLocal的remove方法把值清空就好了。那整个Entry就会被清空不会有内存泄露的情况发生。
以上是关于ThreadLocal源码分析理解弱引用和内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章
深入浅出多线程编程实战ThreadLocal详解(内存泄漏)