此文主要详细讲下ThreadLocal
和ThreadLocal
延伸的一些知识。
- 什么是
ThreadLocal
ThreadLocal
源码解析ThreadLocal
内存泄漏分析
1.什么是ThreadLocal
ThreadLocal
,拆开来看Thread线程,Local本地,线程本地,可以看出,是和本地线程有关的。再简单通俗的讲,ThreadLocal
是用于隔离线程之间的数据。我们先看一下这个例子。:
public class ThreadLocalTest {
static ThreadLocal<Student> tl = new ThreadLocal<Student>();
public static void main(String[] args) {
new Thread(()->{
tl.set(new Student());
System.out.println("t1:"+tl.get().name);
},"t1").start();
new Thread(()->{
try {
//睡一秒,确保线程t1执行完再执行线程2
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2:"+tl.get());
},"t2").start();
}
static class Student {
String name = "xiaoming";
}
}
结果:
t1:xiaoming
t2:null
这里先在t1线程new了个Student,再set到ThreadLocal里,最后打印出xiaoming,而t2打印出null。由此可以看出,ThreadLocal
把两个线程的数据隔离开了。那问题来了,ThreadLocal
是怎么实现的呢?我们下面来分析下源码。
2.ThreadLocal
源码解析
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
先来看一个ThreadLocal
源码的set方法,set方法里有一个容器ThreadLocalMap,这个容器是一个map, 是一个key/value对,然后再往下读你会发现,其实这个值是设置到了map里面,而且这个map是什么样的,key设置的是this,value设置的是我们想要的那个值,这个this就是当前对象ThreadLocal
, value就是Student类,如果map != null就设置进去就行了,如果等于空呢?就创建一个map。
再回过头来看这个map,ThreadLocalMap map=getMap(t),点击到了getMap这个方法看到,它的返回值是t.threadLocals。进入这个t.threadLocals可以看出这个t.threadLocals是在Thread类里面的,也就是说这个map是在Thread类里的。所以这个set()其实相当于Thread.currentThread.map(ThreadLocal,student)。
public class Thread implements Runnable{
ThreadLocal.ThreadLocalMap threadLocals = null;
}
所以,这个Student类被set到了当前线程里的某一个map里面去了,t1线程set了一个Student对象到自己的map里,t2线程去访问的也是自己的属于t2线程的 map,所以是读不到值的。
3.ThreadLocal
内存泄漏分析
分析ThreadLocal
内存泄漏前,先简单的说下java的四种引用:强引用,软引用,弱引用,虚引用。
强引用(StrongReference):比如我们平时编码Object obj = new Object(),通过new来创建对象的就是强应用。当JVM内存空间不足,JVM宁愿抛出OutOfMemoryError运行时错(OOM),使程序异常终止,也不会去回收它。
软引用(SoftReference):如果一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
弱引用(WeakReference):只要进行了垃圾回收,就会回收掉他。
虚引用(PhantomReference):与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
了解了这四种应用后,我们再看回刚刚ThreadLocal
里的set方法。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
点进去map.set(this, value)看这个set的实现。
private void set(ThreadLocal<?> key, Object value) {
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();
}
这里有个Entry,先看下这个Entry是什么东东。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
我们可以看到Entry是继承WeakReference的,也就说Entry是个弱引用。再进一步说,就是ThreadLocalMap的key是个弱引用。那问题来了,为什么Entry要继承弱引用呢?ThreadLocalMap的key是弱引用,为什么会存在内存泄漏的情况呢?
先看下我们创建的ThreadLocal
ThreadLocal tl = new ThreadLocal();
这里tl是一个强引用指向这个ThreadLocal
对象,而Map里的key是通过一个弱引用指向了一个ThreadLocal
对象,假设这是个强引用,当tl指向这个ThreadLocal
对象消失的时候,tl是个局部变量,方法已结束它就消失了,如果这个ThreadLocal
对象还被一个强引用的key指向的时候,这个ThreadLocal
对象就不能被回收了,就会有内存泄漏,所以这就是Entry继承弱引用的原因。
但是还是有内存泄漏的问题,当我们tl这个强引用消失了,key的指向也被回收了,key指向了null,但是这个threadLocals的Map是永远存在的,相当于说key/value对,这个key是null的,value指向的东西就永远访问不到了。
所以,使用 ThreadLocal里面的对象不用了,务必要remove掉,不然还会有内存泄漏。
ThreadLocal<Object> tl = new ThreadLocal<>();
tl.set(new Object());
tl.remove();