ThreadLocal是什么
要理解ThreadLocal是什么,会涉及到3个类:Thread、ThreadLocalMap、ThreadLocal。在我们常用的线程Thread类中,有定义一个变量:
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap类是定义在ThreadLocal类中的内部类,我们通过操作ThreadLocal类中的set()、get()等方法,可以在线程生命周期内向这个map变量设置、获取值。
ThreadLocal有什么用
1.假如在我们的程序执行链路中,很多方法都需要用到userId(比如记录日志),如果在每个方法都增加userId参数,这样的话代码会变得很臃肿,也不优雅。没错,可以使用ThreadLocal将userId放在当前线程中。
2.Spring处理事务时,会用到Connection对象,需要保证一个事务中获取到的都是同一个Connection对象,肯定不能在每个service方法中都添加Connection参数,那Spring是怎么实现的呢?没错,就是ThreadLocal。
所以,以下这几个场景,可以考虑使用ThreadLocal:
1)方法调用链路中,多个方法用到的参数,且方法不方便传输。比如session、userId、requestId等。
2)线程数据隔离(创建一个变量,且这个变量只能当前线程可见)。
ThreadLocal怎么使用
ThreadLocal实际是将threadLocal对象作为key,值作为value,设置到当前线程的map中,所以一般使用时,会将threadLocal设置为静态变量或单例,确保同一个key才能获取到对应的value。
如果项目中出现类似于下面这种代码,会有什么问题?
public class ThreadLocalDemo {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("hello");
String value = threadLocal.get();
System.out.println(value);
}
}
}
项目中代码一般都是由线程池中线程执行,运行完成后线程不会销毁,这些threadLocal对象和value会一直被当前线程引用,导致GC不掉,那不就产生内存泄漏问题了嘛。ThreadLocal通过对key的弱引用(下次GC只存在弱引用的对象会被回收掉)设计,且在每次调用set()、get()、remove()方法时,都会自动将key为空的Entry对象清理掉,巧妙的解决了这个问题。
ThreadLocal使用建议:
1)每次使用完,都调用remove()方法。
2)threadLocal对象设置为静态或单例,确保后面能根据同一个key获取到对应value。
ThreadLocal源码解析
调用threadLocal.set()方法时源码解析:
public void set(T value) {
// 1.获取当前线程
Thread t = Thread.currentThread();
// 2.获取当前线程上的ThreadLocalMap变量
ThreadLocalMap map = getMap(t);
if (map != null)
// 3.将threadLocal作为key,值作为value,设置到map中
map.set(this, value);
else
createMap(t, value);
}
向map.set(this, value)源码逻辑:
private void set(ThreadLocal key, Object value) {
// 1.实际存放数据的Entry数组
Entry[] tab = table;
int len = tab.length;
// 2.hash到数组对应下标
int i = key.threadLocalHashCode & (len-1);
// 3.处理hash碰撞,下标一直向右移动,直到找到空位置
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal k = e.get();
// 3.1 同一个key,将value值覆盖
if (k == key) {
e.value = value;
return;
}
// 3.2 如果key已被回收,则进行替换处理
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 4.当前下标位置为空,new一个Entry,放在数组对应下标上
tab[i] = new Entry(key, value);
int sz = ++size;
// 5.清理key为空的数据 && 如果必要,则进行扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
调用threadLocal.get()方法时源码解析:
public T get() {
// 1.获取当前线程
Thread t = Thread.currentThread();
// 2.获取当前线程的ThreadLocalMap变量
ThreadLocalMap map = getMap(t);
if (map != null) {
// 3.获取到当前key对应的Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
map.getEntry(this)代码逻辑:
private Entry getEntry(ThreadLocal key) {
// 1.hash到数组对应下标
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
// 2.获取到了key对应的Entry
return e;
else
// 3.未获取到Entry,继续尝试获取
return getEntryAfterMiss(key, i, e);
}
getEntryAfterMiss(key, i, e)方法逻辑:
private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 1.当前位置Entry不为空,但又属于当前key,说明此位置发生了hash碰撞。
while (e != null) {
ThreadLocal k = e.get();
if (k == key)
return e;
if (k == null)
// 1.1 清理key为空的数据
expungeStaleEntry(i);
else
// 1.2 继续向右移动下标,尝试寻找Entry,对应set时hash碰撞逻辑。
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
源码逻辑整理:
1. key和value是保存在Entry数组中,且key是被弱引用。
2. 设置值时,如果产生hash碰撞,一直寻找下一个空下标位置,将Entry放入数组,还会清理key被回收掉的Entry及判断是否需要扩容。
3. 获取值时,如果key对应的数组下标位置存在Entry,且这个Entry属于当前key,则直接获取对应value即可;如果这个Entry不属于当前key,表示此位置存在hash碰撞,继续向右移动下标尝试获取Entry。同时也会清理移动下标碰到的key为空的Entry。
ThreadLocal内存泄漏问题
使用ThreadLocal,一定会讲到内存泄漏问题,下图是ThreadLocal数据结构:
也就是说,Thred中有一个ThreadLocalMap变量,ThreadLocalMap中有一个Entry[]数组变量,我们设置的值作为Entry的value,Entry的key弱引用threadLocal变量。两种情况:
1.如果threadLocal定义的是线程变量,当线程运行完成,由于是线程池,线程会一直存活,会一直保持对threadLocal和value的引用,由于key采用弱引用,在下一次GC时,threadLocal对象会被回收,需要等到下一次对这个线程的threadLocal操作,才会断开对value的引用,那么这段时间中,value对象是一直不能被GC掉的,从而导致内存泄漏。试想一下,如果key是强引用,这种情况key和value就永远都不会被GC掉了,想想好可怕。
2.如果threadLocal定义的是静态变量,那threadLocal对象会一直存活,在上面的场景下,value会一直被引用,导致value不会被GC,产生内存泄漏。
所以,当时使用完后,一定记得在finally中调用remove()方法,手动释放对key和value的引用,避免产生内存泄漏。