ThreadLocal 作用:实现数据隔离
ThreadLocal 原理分析:
Thread.java
public class Thread implements Runnable {
...
ThreadLocal.ThreadLocalMap threadLocals = null;
...
}
每个线程Thread都维护了自己的threadLocals变量,ThreadLocalMap是由ThreadLocal维护的静态内部类,我们在使用ThreadLocal的get()、set()方法时,其实都是调用了ThreadLocalMap类对应的get()、set()方法。
set 源码:
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null)//校验对象是否为空
//如果已经存在,则通过ThreadLocalMap的set方法设置值,这里我们可以看到set中key为this,也就是当前ThreadLocal对象,而value值则是我们要存的值。
map.set(this, value);
else
//如果Thread中的对应属性为null,则创建一个ThreadLocalMap并赋值给Thread:
createMap(t, value);// 为空创建一个map对象
}
hreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
对应get 方法
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
上述set方法中,首先获取当前线程对象,然后通过getMap方法来获取当前线程中的threadLocals:
在get方法中可以看到同样通过当前线程,拿到当前线程的threadLocals属性,然后从中获取存储的值并返回。在get的时候,如果Thread中的threadLocals属性未进行初始化,则也会间接调用createMap方法进行初始化操作。
ThreadLocalMap是当前线程Thread一个叫threadLocals的变量中获取的。
public class Thread implements Runnable {
……
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
……
1、Thread中的这个变量的初始化通常是在首次调用ThreadLocal的get()、set()方法时进行的。判断存不存在,不存在就调用createMap()来创建
2、thread 类有个 ThreadLocalMap 成员变量,这个Map key是Threadlocal 对象,value是你要存放的线程局部变量。每个线程Thread都维护了自己的threadLocals变量,所以在每个线程创建ThreadLocal的时候,实际上数据是存在自己线程Thread的threadLocals变量里面的,别人没办法拿到,从而实现了隔离。
ThreadLocal使用实例
ThreadLocal<String> localName = new ThreadLocal();
localName.set("张三");
String name = localName.get();
localName.remove();
线程进来之后初始化一个可以泛型的ThreadLocal对象,之后这个线程只要在remove之前去get,都能拿到之前set的值,注意这里我说的是remove之前。
使用场景
项目中存在一个线程经常遇到横跨若干方法调用,需要传递的对象,也就是上下文(Context),它是一种状态,经常就是是用户身份、任务信息等,就会存在过渡传参的问题。
使用到类似责任链模式,给每个方法增加一个context参数非常麻烦,而且有些时候,如果调用链有无法修改源码的第三方库,对象参数就传不进去了,所以我使用到了ThreadLocal去做了一下改造,这样只需要在调用前在ThreadLocal中设置参数,其他地方get一下就好了。
before
void work(User user) {
getInfo(user);
checkInfo(user);
setSomeThing(user);
log(user);
}
then
void work(User user) {
try{
threadLocalUser.set(user);
// 他们内部 User u = threadLocalUser.get(); 就好了
getInfo();
checkInfo();
setSomeThing();
log();
} finally {
threadLocalUser.remove();
}
}
ThreadLoalMap的数据结构
ThreadLoalMap是ThreadLocal中的一个静态内部类,类似HashMap的数据结构,但并没有实现Map接口。
ThreadLoalMap中初始化了一个大小16的Entry数组,Entry对象用来保存每一个key-value键值对。通过上面的set方法,我们已经知道其中的key永远都是ThreadLocal对象。
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private static final int INITIAL_CAPACITY = 16;
// ...
构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
...
}
}
上述构造方法,创建了一个默认长度为16的Entry数组,通过hashCode与length位运算确定索引值i。而上面也提到,每个Thread都有一个ThreadLocalMap类型的变量。
hash冲突及解决
我们留意到构造方法中Entry在table中存储位置是通过hashcode算法获得。每个ThreadLocal对象都有一个hash值threadLocalHashCode,每初始化一个ThreadLocal对象,hash值就增加一个固定的大小0x61c88647。
在向ThreadLocalMap中的Entry数值存储Entry对象时,会根据ThreadLocal对象的hash值,定位到table中的位置i。这里分三种情况:
-
如果当前位置为空的,直接将Entry存放在对应位置;
-
如果位置i已经有值且这个Entry对象的key正好是即将设置的key,那么重新设置Entry中的value;
-
如果位置i的Entry对象和即将设置的key没关系,则寻找一个空位置;
上面的流程可以看出这里采用的是开放地址方法,如果当前位置有值,就继续寻找下一个位置,注意table[len-1]的下一个位置是table[0],就像是一个环形数组,所以也叫闭散列法。如果一直都找不到空位置就会出现死循环,发生内存溢出。当然有扩容机制,一般不会找不到空位置的。
ThreadLocal内存泄露
根据前面对ThreadLocal的分析,得知每个Thread维护一个ThreadLocalMap,它key是ThreadLocal实例本身,value是业务需要存储的Object。也就是说ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。
仔细观察ThreadLocalMap,这个map是使用ThreadLocal的弱引用作为Key的,弱引用的对象在GC时会被回收。
正常来说,当Thread执行完会被销毁,Thread.threadLocals指向的ThreadLocalMap实例也随之变为垃圾,它里面存放的Entity也会被回收。这种情况是不会发生内存泄漏的。
发生内存泄露的场景一般存在于线程池的情况下。此时,Thread生命周期比较长(存在循环使用),threadLocals引用一直存在,当其存放的ThreadLocal被回收(弱引用生命周期比较短)后,对应的Entity就成了key为null的实例,但value值不会被回收。如果此Entity一直不被get()、set()、remove(),就一直不会被回收,也就发生了内存泄漏。
所以,通常在使用完ThreadLocal后需要调用remove()方法进行内存的清除。
为什么使用弱引用而不是强引用?
我们先来假设一下,如果key使用强引用,那么在其他持有ThreadLocal引用的对象都回收了,但ThreadLocalMap依旧持有ThreadLocal的强引用,这就导致ThreadLocal不会被回收,从而导致Entry内存泄露。
对照一下,弱引用的情况。持有ThreadLocal引用的对象都回收了,ThreadLocalMap持有的是ThreadLocal的弱引用,会被自动回收。只不过对应的value值,需要在下次调用set/get/remove方法时会被清除。
综合对比会发现,采用弱引用反而多了一层保障,ThreadLocal被清理后key为null,对应的value在下一次ThreadLocalMap调用set、get、remove的时候可能会被清除。
所以,内存泄露的根本原因是是否手动清除操作,而不是弱引用。
网友评论