首先先看一张图:
图中基本描述出了Thread、ThreadLocalMap以及ThreadLocal三者之间的包含关系。Thread类对象中维护了ThreadLocalMap成员变量,而ThreadLocalMap维护了以ThreadLocal为key,需要存储的数据为value的Entry数组。
Thread类
首先,查看Thread 类,内部有两个变量,类型是ThreadLocal.ThreadLocalMap
ThreadLocalMap类
查看 ThreadLocalMap,它其实是ThreadLocal 的一个静态内部类,维护了一个数据结构类型为Entry的数组
Entry结构实际上是继承了一个ThreadLocal类型的弱引用并将其作为key,value为Object类型。
查看ThrealLocalMap 的构造方法,从构造方法的注释中可以了解到,该构造方法是懒加载的,只有当我们创建一个Entry对象并需要放入到Entry数组的时候才会去初始化Entry数组。
ThreadLocal类
set方法
public void set(T value) {
// 首先获取调用此方法的线程
Thread t = Thread.currentThread();
// 将线程传递到getMap方法中来获取ThreadLocalMap,其实就是获取到当前线程的成员变量threadLocals所指向的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 判断Map是否为空
if (map != null)
// 如果Map为不空,说明当前线程内部已经有ThreadLocalMap对象了,那么直接将本ThreadLocal对象作为键,存入的value作为值存储到ThreadLocalMap中
map.set(this, value);
else
// 创建一个ThreadLocalMap对象并将值存入到该对象中,并赋值给当前线程的threadLocals成员变量
createMap(t, value);
}
// 获取到当前线程的成员变量threadLocals所指向的ThreadLocalMap对象
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// 创建一个ThreadLocalMap对象并将值存入到该对象中,并赋值给当前线程的threadLocals成员变量
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
上面的set方法是ThreadLocal的set方法,就是为了将指定的值存入到指定线程的threadLocals成员变量所指向的ThreadLocalMap对象中,那么具体是如何存取的,其实调用的还是ThreadLocalMap的set方法,源码分析如下所示:
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;
// 计算当前ThreadLocal对象作为键在Entry数组中的下标索引
int i = key.threadLocalHashCode & (len-1);
// 线性遍历,首先获取到指定下标的Entry对象,如果不为空,则进入到for循环体内,
// 判断当前的ThreadLocal对象是否是同一个对象,如果是,那么直接进行值替换,并结束方法,
// 如果不是,再判断当前Entry的key是否失效,如果失效,则直接将失效的key和值进行替换。
// 这两点都不满足的话,那么就调用nextIndex方法进行搜寻下一个合适的位置,进行同样的操作,
// 直到找到某个位置,内部数据为空,也就是Entry为null,那么就直接将键值对设置到这个位置上。
// 最后判断是否达到了扩容的条件,如果达到了,那么就进行扩容。
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
这里的代码核心的地方就是for循环这一块,代码上面加了详细的注释,这里在复述一遍:
线性遍历,首先获取到指定下标的Entry对象,如果不为空,则进入到for循环体内,判断当前的ThreadLocal对象是否是同一个对象。
如果是,那么直接进行值替换,并结束方法。如果不是,再判断当前Entry的key是否失效,如果失效,则直接将失效的key和值进行替换。
这两点都不满足的话,那么就调用nextIndex方法进行搜寻下一个合适的位置,进行同样的操作,直到找到某个位置,内部数据为空,也就是Entry为null,那么就直接将键值对设置到这个位置上。最后判断是否达到了扩容的条件,如果达到了,那么就进行扩容。
这里有两点需要注意:一是nextIndex方法,二是key失效,这里先解释第一个注意点,第二个注意点涉及到弱引用JVM GC问题,文章最后做出解释。
nextIndex方法的具体代码如下所示:
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
其实就是寻找下一个合适位置,找到最后一个后还不合适的话,那么从数组头部重新开始找,且一定可以找到,因为存在扩容阈值,数组必定有冗余的位置存放当前键值对所对应的Entry对象。其实nextIndex方法就是大名鼎鼎的『开放寻址法
』的应用。
这一点和HashMap不一样,HashMap存储HashEntry对象发生哈希冲突的时候采用的是链表方式进行存储,而这里是去寻找下一个合适的位置,思想就是『开放寻址法
』。
get方法
在实际的开发中,我们往往需要在代码中调用ThreadLocal对象的get方法来获取存储在ThreadLocalMap中的数据,具体的源码如下所示:
public T get() {
// 获取当前线程的ThreadLocalMap对象
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// 如果map不为空,那么尝试获取Entry数组中以当前ThreadLocal对象为键的Entry对象
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
// 如果找到,那么直接返回value
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果Map为空或者在Entry数组中没有找到以当前ThreadLocal对象为键的Entry对象,
// 那么就在这里进行值初始化,值初始化的过程是将null作为值,当前ThreadLocal对象作为键,
// 存入到当前线程的ThreadLocalMap对象中
return setInitialValue();
}
// 值初始化过程
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;
}
remove方法
使用ThreadLocal这个工具的时候,一般提倡使用完后及时清理存储在ThreadLocalMap中的值,防止内存泄露。这里一起来看下ThreadLocal的remove方法
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)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
这里加以总结:线程类Thread内部持有ThreadLocalMap的成员变量,而ThreadLocalMap是ThreadLocal的内部类,ThreadLocal操作了ThreadLocalMap对象内部的数据,对外暴露的都是ThreadLocal的方法API,隐藏了ThreadLocalMap的具体实现,理清了这一点,ThreadLocal就很容易理解了。
ThreadLocalMap内存泄露问题
这个问题要从ThreadLocalMap的构造方法说起:
这里简单介绍一下Java内的四大引用:
-
强引用:Java中默认的引用类型,一个对象如果具有强引用那么只要这种引用还存在就不会被回收。比如
String str = new String("Hello ThreadLocal");
,其中str就是一个强引用,当然,一旦强引用出了其作用域,那么强引用随着方法弹出线程栈,那么它所指向的对象将在合适的时机被JVM垃圾收集器回收。 -
软引用:如果一个对象具有软引用,在JVM发生内存溢出之前(即内存充足够使用),是不会GC这个对象的;只有到JVM内存不足的时候才会调用垃圾回收期回收掉这个对象。软引用和一个引用队列联合使用,如果软引用所引用的对象被回收之后,该引用就会加入到与之关联的引用队列中。
-
弱引用:这里讨论ThreadLocalMap中的Entry类的重点,如果一个对象只具有弱引用,那么这个对象就会被垃圾回收器回收掉(被弱引用所引用的对象只能生存到下一次GC之前,当发生GC时候,无论当前内存是否足够,弱引用所引用的对象都会被回收掉)。弱引用也是和一个引用队列联合使用,如果弱引用的对象被垃圾回收期回收掉,JVM会将这个引用加入到与之关联的引用队列中。若引用的对象可以通过弱引用的get方法得到,当引用的对象被回收掉之后,再调用get方法就会返回null。
-
虚引用:虚引用是所有引用中最弱的一种引用,其存在就是为了将关联虚引用的对象在被GC掉之后收到一个通知。
我们从ThreadLocal的内部静态类Entry的代码设计可知,ThreadLocal的引用k通过构造方法传递给了Entry类的父类WeakReference的构造方法,从这个层面来说,可以理解ThreadLocalMap中的键是ThreadLocal的所引用。
当一个线程调用ThreadLocal的set方法设置变量的时候,当前线程的ThreadLocalMap就会存放一个记录,这个记录的键为ThreadLocal的弱引用,value就是通过set设置的值,这个value值被强引用。
如果当前线程一直存在且没有调用该ThreadLocal的remove方法,如果这个时候别的地方还有对ThreadLocal的引用,那么当前线程中的ThreadLocalMap中会存在对ThreadLocal变量的引用和value对象的引用,是不会释放的,就会造成内存泄漏。
考虑这个ThreadLocal变量没有其他强依赖,如果当前线程还存在,由于线程的ThreadLocalMap里面的key是弱引用,所以当前线程的ThreadLocalMap里面的ThreadLocal变量的弱引用在垃圾回收的时候就被回收,但是对应的value还是存在的这就可能造成内存泄漏(因为这个时候ThreadLocalMap会存在key为null但是value不为null的entry项)。
总结:ThreadLocalMap中的Entry的key使用的是ThreadLocal对象的弱引用,在没有其他地方对ThreadLocal依赖,ThreadLocalMap中的ThreadLocal对象就会被回收掉,但是对应的值不会被回收,这个时候Map中就可能存在key为null但是值不为null的项,所以在使用ThreadLocal的时候要养成及时remove的习惯。
由于线程池的存在,线程用完不会销毁,线程重复利用,所以如果使用之后不remove 就会导致内存泄漏
为什么value不用弱引用呢?
value不像key那样,key还有一个外部的强引用,如果在业务执行过程中发生了gc,value被清理了,业务后边取值会出错的。