一、什么是ThreadLocal
ThreadLocal并不是用来并发控制访问一个共同对象,而是为了给每个线程分配一个只属于该线程的变量,顾名思义它是local variable(线程局部变量)。它的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突,实现线程间的数据隔离。从线程的角度看,就好像每一个线程都完全拥有该变量。
二、源码实现
set方法实现
public void set(T value) {
Thread t = Thread.currentThread();//1
ThreadLocalMap map = getMap(t);//2
if (map != null)
map.set(this, value);//4
else
createMap(t, value);//3
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
接下来我们按代码行逐行讲解:
1、获取当前线程,即t为正在执行ThreadLocal的set方法的那个线程
2、获取ThreadLocalMap,其中ThreadLocalMap为ThreadLocal的一个内部类,后面再详细解释,其中getMap()方法参数列表为当前线程地址
接下来,我们看一下getMap方法,这个方法只有一句话return t.threadLocals,显然,threadLocals类型为ThreadLocalMap,是Thread线程类的一个成员变量,我们打开Thread类查看该变量,发现其初始值为null
ThreadLocal.ThreadLocalMap threadLocals = null;
3、<createMap(t, value)>由于首次执行set方法时,threadLocals变量为null,所以执行代码3,即创建一个ThreadLocalMap,接下来我们看一下createMap()这个方法。参数列表包含两个,分别为当前线程和准备存储的值。实现代码只有一行
t.threadLocals = new ThreadLocalMap(this, firstValue);
不难发现,这个方法的实际作用其实就是为了当前线程的threadLocals变量做初始化,接下来我们看一下初始化细节new ThreadLocalMap(this, firstValue)。参数列表包含两个,分别为当前ThreadLocal对象,准备存储的值。ThreadLocalMap初始化代码为
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);
}
ThreadLocalMap其实本质上就是一个弱化版的HashMap,其中维持了一个Entry数组,Entry数组的每一维分别为一个链表,其中Entry数组初始长度为INITIAL_CAPACITY=16,接下来看这句代码
firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
很显然,是根据传入的ThreadLocal对象的HashCode对数组长度除留取余,i则是firstValue对应的需要存储的Entry数组下标
4、<map.set(this, value)>当map不为null时,将值存入map,具体实现代码为
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();
}
存储过程类似HashMap存储
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();
}
获取过程为,先获取当前线程,然后获取当前线程持有的ThreadLocalMap,若map不为null,则调用ThreadLocalMap的getEntry方法,以ThreadLocal对象为key,获取对应的Entry对象,并获取值
remove方法实现
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
根据ThreadLocal对象,移除对应值
三、内存泄漏问题
由于每个thread中都持有一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收。
所以,只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露。