首先来看一下Lock和ThreadLocal的区别,Lock是用来协调多个线程之间数据共享的,ThreadLocal是用来解决多个线程之间数据隔离的。
下面就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);
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
通过源码我们可以看到每个线程都有一个类型为ThreadLocalMap的变量threadLocals,每个线程隔离的数据最后都保存在这个变量里面。这个map的key就是一个ThreadLocal对象,value就是线程隔离的数据。 由此可见ThreadLocal是通过ThreadLocalMap来实现线程数据隔离的。
下面再看一下ThreadLocal的get方法源代码:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
通过源码我们可以看到每次取值的时候,先从当前线程中获取对应的ThreadLocalMap变量,然后从变量中根据key(记得这里是ThreadLocal对象--this)获取对应的值(如果没有会直接返回初始值--setInitialValue)。在这里其实也可以将当前线程作为ThreadLocalMap变量的key,也能达到线程隔离,但是却只能实现一个变量的隔离,ThreadLocal作为key可以实现多个变量的隔离,每个变量对应一个ThreadLocal对象。至此,我们可以清晰的看到ThreadLocal是如何实现线程数据隔离的了。
但是我们还是有几个问题需要注意:
1)如果存在线程重用或是长时间存在的情况,则一定要将ThreadLocal声明为static,否则可能存在内存泄露,如果没有重用也可以不声明,但是声明也没有副作用,所以结论是使用ThreadLocal是一定要声明为static。
2)当ThreadLocal不使用时,一定要调用remove方法,调用set(null)方法还不够,在极端情况下会造成内存泄露。为什么呢?我们知道调用set(null)方法只会将ThreadLocal对应的值设置为null,但是ThreadLocal本身还是被线程变量threadLocals所引用的,导致垃圾回收时不会回收ThreadLocal对象,即便是该对象已不再使用。当调用remove方法时,会将当前的ThreadLocal对象从对应的线程的threadLocals里面移除,该ThreadLocal对象这个时候只被ThreadLocalMap.Entry对象引用,而该对象是继承自WeakReference,里面是通过弱引用指向ThreadLocal对象的,所以在垃圾回收时会回收ThreadLocal对象。具体的源代码如下:
ThreadLocal类的remove方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreadLocalMap
的remove方法
private void remove(ThreadLocal key) {
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)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
e.clear方法
public void clear() {
this.referent = null;
}
expungeStaleEntry方法(只取了一小段说明问题)
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
//这里省略了一大段无关代码
}
ThreadLocal执行remove方法的示意图如下:
一、当ThreadLocal没有定义为static且存在线程重用或是线程长时间存在时,系统中的ThreadLocal运行状况如下图。当创建业务对象被回收后ThreadLocal还是被ThreadLocalMap引用,不会被回收,所以长时间运行后ThreadLocal对象将会很多,也就是说ThreadLocalMap里面存放了太多的ThreadLocal对象,存在内存泄露的情况。
二、当ThreadLocal定义为static,即便是存在线程重用或是线程长时间存在的情况,从图中可以看出都是引用一个ThreadLocal对象。当创建业务对象被回收后ThreadLocal虽然还是被ThreadLocalMap引用,不会被回收,但是只会有一个ThreadLocal对象,不会像上图那样ThreadLocal对象会随着运行时间的增加而增加,一般不会存在内存泄露。
三、当ThreadLocal被声明为static且线程隔离的数据对象存在多个时,系统中ThreadLocal的运行情况如下图,每种不同颜色代表一种需要线程隔离的数据对象:
备注:如果ThreadLocal不设置为static,要防止内存泄露是否有其它办法,其实如果ThreadLocal的
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
修改为
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(new WeakReference(this), firstValue);
}
应该也可以解决。