什么是ThreadLocal?
如果你从字面上来理解,很容易将ThreadLocal理解为『本地线程
』,那么你就大错特错了。
首先,ThreadLocal不是线程,更不是本地线程,而是Thread的局部变量,也许把它命名为ThreadLocalVariable
更容易让人理解一些。
它是每个线程独享的本地变量,每个线程都有自己的ThreadLocal,它们是线程隔离的。接下来,我们通过一个生活案例来开始理解ThreadLocal。
一、使用场景
-
场景1:每个线程需要一个独享的对象,通常是工具类,比如典型的SimpleDateFormat和Random等。
-
场景2:每个线程内需要保存线程内的全局变量,这样线程在执行多个方法的时候,可以在多个方法中获取这个线程内的全局变量,避免了过度参数传递的问题。
-
Looper MessageQueue
-
二、原理
- 线程类Thread内部持有ThreadLocalMap的成员变量,而ThreadLocalMap是ThreadLocal的内部类
- ThreadLocal操作了ThreadLocalMap对象内部的数据,对外暴露的都是ThreadLocal的方法API,隐藏了ThreadLocalMap的具体实现
2.1 ThreadLocalMap数据结构
下图图中基本描述出了Thread、ThreadLocalMap以及ThreadLocal三者之间的包含关系
Thread类对象中维护了ThreadLocalMap成员变量,而ThreadLocalMap维护了以ThreadLocal为key,需要存储的数据为value的Entry数组
- 查看Thread类,内部维护了两个变量,threadLocals和inheritableThreadLocals
- 它们的默认值是null,它们的类型是
ThreadLocal.ThreadLocalMap
- 它们的默认值是null,它们的类型是
- 静态内部类ThreadLocalMap维护一个数据结构类型为Entry的数组
- Entry结构实际上是继承了一个ThreadLocal类型的弱引用并将其作为key,value为Object类型
2.2 ThreadLocal类set方法
我们并没有直接操作ThreadLocalMap来存取数据,而是通过一个静态的ThreadLocal变量来操作
ublic 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);
}
- 首先获取调用此方法的调用线程
- 将线程传递到getMap方法中来获取ThreadLocalMap,其实就是获取到当前线程的成员变量threadLocals所指向的ThreadLocalMap对象
- 判断map对象
- 如果Map为不空,说明当前线程内部已经有ThreadLocalMap对象了,那么直接将本ThreadLocal对象作为键,存入的value作为值存储到ThreadLocalMap中
- 否则,创建一个ThreadLocalMap对象并将值存入到该对象中,并赋值给当前线程的threadLocals成员变量
最终调用的还是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();
}
线性遍历,首先获取到指定下标的Entry对象,如果不为空,则进入到for循环体内,判断当前的ThreadLocal对象是否是同一个对象
如果是,那么直接进行值替换,并结束方法。如果不是,再判断当前Entry的key是否失效,如果失效,则直接将失效的key和值进行替换。
这两点都不满足的话,那么就调用nextIndex方法进行搜寻下一个合适的位置,进行同样的操作,直到找到某个位置,内部数据为空,也就是Entry为null,那么就直接将键值对设置到这个位置上。最后判断是否达到了扩容的条件,如果达到了,那么就进行扩容
2.2.1 nextIndex方法 开放寻址法
这里有两点需要注意:一是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对象发生哈希冲突的时候采用的是链表方式进行存储,而这里是去寻找下一个合适的位置,思想就是『开放寻址法
』。
2.3 ThreadLocal类get方法
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;
}
2.4 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;
}
}
}
三、理解ThreadLocalMap内存泄露问题
ThreadLocalMap中的Entry的key使用的是ThreadLocal对象的弱引用,在没有其他地方对ThreadLocal依赖,ThreadLocalMap中的ThreadLocal对象就会被回收掉,但是对应的值不会被回收,这个时候Map中就可能存在key为null但是值不为null的项,所以在使用ThreadLocal的时候要养成及时remove的习惯
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocalMap中的键是ThreadLocal的弱引用
- 如果当前线程一直存在且没有调用该ThreadLocal的remove方法,如果这个时候别的地方还有对ThreadLocal的引用,那么当前线程中的ThreadLocalMap中会存在对ThreadLocal变量的引用和value对象的引用,是不会释放的,就会造成内存泄漏。
- 考虑这个ThreadLocal变量没有其他强依赖,如果当前线程还存在,由于线程的ThreadLocalMap里面的key是弱引用,所以当前线程的ThreadLocalMap里面的ThreadLocal变量的弱引用在垃圾回收的时候就被回收,但是对应的value还是存在的这就可能造成内存泄漏(因为这个时候ThreadLocalMap会存在key为null但是value不为null的entry项)。