AnthonyZero's Bolg

聊聊ThreadLocal

ThreadLocal是什么

ThreadLocal 用于提供线程局部变量,在多线程环境可以保证各个线程里的变量独立于其它线程里的变量。也就是说 ThreadLocal 可以为每个线程创建一个【单独的变量副本】。实现了线程的数据隔离。

简单来说就是:往ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。

ThreadLocal实现原理

ThreadLocal的内部结构如下图

threadlocal

ThreadLocal并不维护ThreadLocalMap,并不是一个存储数据的容器,它只是相当于一个工具包,提供了操作该容器的方法,如get、set、remove等。而ThreadLocal内部类ThreadLocalMap才是存储数据的容器,并且该容器由Thread维护。

每一个Thread对象均含有一个ThreadLocalMap类型的成员变量threadLocals,它存储本线程中所有ThreadLocal对象及其对应的值

set(T value)

1
2
3
4
5
6
7
8
9
10
11
12
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;
}

获取当前线程,然后在获取到当前线程的 ThreadLocalMap,如果 ThreadLocalMap 不为 null,则将 value 保存到 ThreadLocalMap 中,并用当前 ThreadLocal 作为 key;否则创建一个 ThreadLocalMap 并给到当前线程,然后保存 value

T get()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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();
}

static class Entry extends WeakReference<ThreadLocal<?>>
{  
Object value;   
Entry(ThreadLocal<?> k, Object v) {       
super(k);       
value = v;   
}
}

get方法执行过程与上面类似。ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,获取对应的value.

remove()

1
2
3
4
5
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}

remove() 方法,通过this ThreadLocal为键来移除ThreadLocalMap中对应的值。

内存泄漏问题

threadlocalleak.png

Entry中的key为一个threadlocal实例. 这个Entry使用了弱引用,不过弱引用只是针对key. 每个key弱引用指向threadlocal. 当把threadLocal引用(threadLocalRef)置为null以后,threadLocal就没有一条引用链路可达,很显然在gc(垃圾回收)的时候势必会被回收,因此entry就存在key为null的情况,无法通过一个key为null去访问到该entry的value

但是,与此同时我们的value却不能被回收,因为存在一条从CurrentThreadRef连接过来的强引用. 只有当前thread结束以后, CurrentThreadRef就不会存在栈中,强引用断开,gc进行可达性分析的时候, Current Thread, ThreadLocalMap, value valueMemory将全部被GC回收。就不会出现内存泄露。

但有时候使用线程池的时候,线程结束是不会销毁的,会再次使用的就可能出现内存泄露 (value访问不到,同时又回收不到)(比如在web应用中,每次http请求都是一个线程,tomcat容器配置使用线程池时会出现内存泄漏问题)

所以想要避免内存泄露就应该使用完threadLocal之后,最后手动remove()掉!

总结

  • TreadLocalMap是ThreadLocal的内部类,用Entry来进行存储
  • 每个Thread维护着一个ThreadLocalMap的引用
  • 调用ThreadLocal的set()方法时,实际上就是往当前线程的ThreadLocalMap设置值,key是ThreadLocal对象,值是传递进来的对象
  • 调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象
  • ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value