在多线程的情况下,ThreadLocal提供了一个种为每个线程访问相同的变量,并且线程对变量的更新互不影响的机制。也是对象实现线程安全的一种方式。
ThreadLocal的实现机制
我们常用的方法有get
、set
和initialValue
,这次将会围绕这几个方法的源码进行深入解析
- get方法
// 获取元素
public T get() {
// 当前线程
Thread t = Thread.currentThread();
// 通过当前线程获取ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null) {
// 获取Entry,其中key为ThreadLocal对象自身
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value; // 获取对象的值
return result;
}
}
// 返回initialValue的值
return setInitialValue();
}
首先,通过当前线程对象获取ThreadLocalMap
对象,然后以ThreadLocal
对象自身为key获取ThreadLocalMap.Entry
,最后在获取Entry
中的value
代码的逻辑非常简单,我们再来看看getMap
和map.getEntry
方法
- getMap方法
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
threadLocals
是Thread的一个属性
public class Thread implements Runnable {
// ......
// threadLocals是Thread的一个属性
ThreadLocal.ThreadLocalMap threadLocals = null;
}
- map.getEntry方法
ThreadLocalMap
是ThreadLocal
对象的一个内部类,Entry
是ThreadLocalMap
的一个内部类
// ThreadLocal的内部类
static class ThreadLocalMap {
// Entry是ThreadLocalMap的内部类,是一个弱引用对象
static class Entry extends WeakReference<ThreadLocal<?>> {
// ThreadLocal中的value
Object value;
// Entry的Key为ThreadLocal对象
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// Entry数组,用来存放一个线程的多个ThreadLocal变量
private Entry[] table;
// 根据ThreadLocal来获取对应的value
private Entry getEntry(ThreadLocal<?> key) {
// 通过hash算法获取key在数组中对应的下标
int i = key.threadLocalHashCode & (table.length - 1);
// 获取下标对应的Entry对象
Entry e = table[i];
// 获取value
if (e != null && e.get() == key)
return e;
else
// 当key不存在时获取值,有2中可能
// 1. 可能过期了
// 2. 可能扩缩容
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) {
ThreadLocal<?> k = e.get();
// 如果key存在,直接返回value
if (k == key)
return e;
// 如果key为空说明已经过期了,需要清除
if (k == null)
expungeStaleEntry(i);
else
// 获取下一个key,看看能否找到
// 这是由于清除已经过期的key,
// 改变了Entry数组的size引起的位置变更
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
- 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);
}
先获取ThreadLocalMap
对象,然后在以ThreadLocal
为key
,将value
设置到ThreadLocalMap
对象中
- map.set方法
// 将ThreadLocal对应的value存储到ThreadLocalMap对象中
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 计算table中的下标
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// 获取ThreadLocal对象
ThreadLocal<?> k = e.get();
// 如果Entry数组中存在ThreadLocal对象,则替换之前的值
if (k == key) {
e.value = value;
return;
}
// 去掉过期的ThreadLocal对象
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// Entry数组中不存在ThreadLocal对象,创建一个新的Entry对象
tab[i] = new Entry(key, value);
int sz = ++size;
// 清除过期的对象
if (!cleanSomeSlots(i, sz) && sz >= threshold)
// Entry数组的大小改变以后重新计算hash
rehash();
}
- createMap方法
void createMap(Thread t, T firstValue) {
// 当线程的threadLocals为null时,为线程初始化一个ThreadLocalMap对象
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
- initialValue方法
// 可以通过重写该方法来返回默认值
protected T initialValue() {
return null;
}
ThreadLocal内存泄漏问题
首先看一下ThreadLocal中对象的引用关系图
从ThreadLocal中对象的引用关系来看,Thread
、ThreadLocalMap
、Entry
对象之间都是强引用,如果可能出现内存泄漏那就是ThreadLocal
对象弱引用引起的。
什么时候会发生内存泄漏
当ThreadLocal
实例不在有强引用指向,只有弱引用存在,且GC回收了这部分空间时,也就是Entry
对象中的key
被回收了,但是value还没有被回收,这时会出现内存泄漏,因为value
无法得到释放。
如何避免内存泄漏
ThreadLocalMap
中是通过expungeStaleEntry
将key
为null
的对象对应的value
也设置为null
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 如果key为null,会将value也设置成null
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
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;
}
所以只要调用expungeStaleEntry
方法,且key
为null
时就可以回收掉value
了,我们可以通过调用ThreadLocal
的remove
方法进行释放
避免ThreadLocal出现内存泄漏的方式有:
- 调用
ThreadLocal
的remove
方法 - 将
ThreadLocal
变量定义成static
类型的,对ThreadLocal
的强引用不会消失,所以也不存在内存泄漏的问题,但是可能会有所浪费