ThreadLocal 底层原理
ThreadLocal
ThreadLocal中set的使用如下:
ThreadLocal<T> tl = new ThreadLocal<>();
tl.set(new T());
ThreadLocal类中的set()方法源码如下:
public void set(T value) {
Thread t = Thread.currentThread();//获取当前线程对象
ThreadLocalMap map = getMap(t);//获取map对象,源码在下面.
if (map != null)
map.set(this, value);//this指向tl,将tl作为key,set传入的对象作为value存入map.
else
createMap(t, value);
}
在ThreadLocal .set()方法中,会创建一个Map对象,然后以ThreadLocal的对象tl为key,以new T()为value存入Map对象中;
tl为key,T对象为value存入map中,而不是将T对象存入ThreadLocal中.
ThreadLocal类中的getMap()方法源码如下:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;//返回了当前线程中的一个成员变量threadLocals.
}
线程类(thread.java)中的threadLocals变量的源码如下:
ThreadLocal.ThreadLocalMap threadLocals = null;
- ThreadLocalMap是ThreadLocal类中的一个内部类;
- 每次new一个新的线程时,线程对象中就会创建一个新的ThreadLocalMap对象;
- 使用ThreadLocal对象去set存放数据时,其实就是将ThreadLocal对象作为当前线程中的map对象的key,set()中传入的对象作为map对象的value;
根据以上可知:
同一个ThreadLocal对象,使用set存放数据,其实是存在了当前线程中独有的map里面,而且是以ThreadLocal对象为key;
所以在不同的线程中使用同一个ThreadLocal对象set的数据只会放在各自的线程中,这样每个线程不会因为是同一个ThreadLocal对象而导致线程安全问题,数据不会混乱;
也就是说,当我们要在一个线程中的ThreadLocalMap中存放多组数据,那么我们就必须new多个ThreadLocal对象,而同一个ThreadLocal对象在多个线程中是不会混乱的.
线程独有的数据,在同一个线程上,任何地方都可以使用,类似与上下文对象ServletContext();使用ThreadLocal对象可以很方便的在同一个线程不同的层面去保存和调用我们想要的数据.
ThreadLocalMap
在ThreadLocal中使用了ThreadLocalMap这个类,而且ThreadLocalMap这个类是ThreadLocal的内部类.
ThreadLocal.set()方法中使用了ThreadLocalMap.set()方法.下图可知.
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;
int i = key.threadLocalHashCode & (len-1);
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();
}
ThreadLocalMap.set()方法用到了Entry(key,value)对象,Entry就是一组键值对.
Entry类的构造函数的源码如下:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);//这里通过显示调用WeakReference构造函数,将弱引用指向ThreadLocal对象
value = v;
}
}
从这段源码中可知:
Entry对象继承了弱引用对象,并且通过这个弱引用指向ThreadLocal对象
根据以上所有内容可得ThreadLocal调用set方法内部的全过程总结如下:
为什么要用到弱引用?
这与强引用和弱引用在GC时的特性有关,在使用完ThreadLocal对象后,如果不再用到该对象,那么该对象就是一个垃圾对象,GC回收掉即可,正常的做法就是直接让指向该ThreadLocal对象的变量赋值为null即可:tl = null,这样就把强引用给断开了,ThreadLocal对象此时只有一个弱引用,GC会直接回收;如果内部的Entry也是使用的强引用指向ThreadLocal对象,那么仅仅让tl = null不能做到断开内部的强引用,所以在内部的Entry使用了弱引用.
ThreadLocal造成内存泄漏
导致泄漏的原因:
- 不再使用的ThreadLocal对象没有回收,即没有做tl = null这一步;很多程序时不间断的运行的,这其中某些线程也会是不间断运行的,如果不回收这样的线程中的ThreadLocal对象,而又不断增加新的对象,势必会造成该线程内存泄漏.
- 即使ThreadLocal对象被回收了,也还是可能会有内存泄漏,ThreadLocal过程中,key的值变成了null,则导致整个value再也无法被访问到,因此存在内存泄漏.
第一点泄漏原因,通过手动的置为null + 弱引用即可解决;
第二点泄漏的原因,需要在不使用的ThreadLocal对象时,不使用到map中的这条记录时,通过remove()清除掉即可.{ 在每一次执行tl.get()和tl.set()方法时,其实源码中都有remove()操作,将key = null 的记录全部清除掉,但是编程时还是要养良好的习惯,手动的进行remove()操作,因为在后续的时间中get()和set()操作不一定还会使用到,如果后面都没有使用,那么这是value占用的空间就得不到释放.}
使用到了线程池的时候,ThreadLocal未被清理也会造成一定的影响,某个线程中ThreadLocalMap存放了数据,没有清理掉就把线程还给线程池,下一次使用到这个线程,又不清理,一直如此会造成内存泄漏,还有可能导致Map中存储的数据混乱.在使用到线程池中的线程前,养成良好的习惯,先将threadLocals清理一遍;
一定的影响,某个线程中ThreadLocalMap存放了数据,没有清理掉就把线程还给线程池,下一次使用到这个线程,又不清理,一直如此会造成内存泄漏,还有可能导致Map中存储的数据混乱.在使用到线程池中的线程前,养成良好的习惯,先将threadLocals清理一遍;**