ThreadLocal源码分析及使用场景

本文主要分析ThreadLocal的实现原理和开发中常见的内存泄露问题,以及给出一些ThreadLocal的应用场景。

源码分析

ThreadLocal是一个为线程提供线程局部变量的工具类。它为线程提供一个线程私有的变量副本,这样多个线程都可以自由修改自己线程局部的变量,不会影响到其他线程。

ThreadLocal中有一个叫做ThreadLocalMap的内部类,ThreadLocalMap正是用来存储变量副本的,它的key为ThreadLocal对象而且继承自WeakReference。

static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
...
}
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);
}

get

要获得当前线程私有的变量副本需要调用get()函数。首先,它会调用getMap()函数去获得当前线程的ThreadLocalMap,这个函数需要接收当前线程的实例作为参数。那么就去调用setInitialValue()函数来进行初始化,如果得到的ThreadLocalMap不为null,就通过map来获得变量副本并返回。如果为null,那么就先去调用setInitialValue()函数来进行初始化。

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();
}

setInitialValue()函数会去先调用initialValue()函数来生成初始值,该函数默认返回null,我们可以通过重写这个函数来返回我们想要在ThreadLocal中维护的变量。之后,去调用getMap()函数获得ThreadLocalMap,如果该map已经存在,那么就用新获得value去覆盖旧值,否则就调用createMap()函数来创建新的map。

private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}

set && remove

ThreadLocal的set()与remove()函数要比get()的实现还要简单,都只是通过getMap()来获得ThreadLocalMap然后对其进行操作。

public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}

getMap()函数与createMap()函数的实现也十分简单,但是通过观察这两个函数可以发现ThreadLocalMap是存放在Thread中保存的。

ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
// Thread中的源码
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

小结

Thread里面有一个MAP变量,初始化的时候为null,当我们使用ThreadLocal的时候,ThreadLocal会帮助当前线程初始化这个MAP,并且把我们需要和线程绑定的值放入改Map中,map的key为当前ThreadLocal。

大部分人的思维可能是在ThreadLocal中维护一个map,key为Thread表示,value为值。为什么不这样做?其实在jdk1.3之前就是用这种方式做的,但是之后就改成了现在的这种做法。这样做法一个优点是value放在了线程当中,随着线程的生命周期生存,线程死亡,value回收。

ThreadLocal中的内存泄漏

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(), set(), remove()的时候都会清除线程ThreadLocalMap里所有key为null的value,下面是getEntry的实现:

private ThreadLocal.ThreadLocalMap.Entry getEntry(ThreadLocal<?> var1) {
int var2 = var1.threadLocalHashCode & this.table.length - 1;
ThreadLocal.ThreadLocalMap.Entry var3 = this.table[var2];
return var3 != null && var3.get() == var1?
var3 : this.getEntryAfterMiss(var1, var2, var3);
}
private ThreadLocal.ThreadLocalMap.Entry getEntryAfterMiss(ThreadLocal<?> var1,
int var2, ThreadLocal.ThreadLocalMap.Entry var3) {
ThreadLocal.ThreadLocalMap.Entry[] var4 = this.table;
for(int var5 = var4.length; var3 != null; var3 = var4[var2]) {
ThreadLocal var6 = (ThreadLocal)var3.get();
if(var6 == var1) {
return var3;
}
if(var6 == null) {
this.expungeStaleEntry(var2); // 执行清除操作
} else {
var2 = nextIndex(var2, var5);
}
}
return null;
}

为什么在ThreadLocalMap的key要使用弱引用呢?使用强引用key与弱引用key的差别如下:

  1. key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
  2. key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set, get, remove的时候会被清除。

由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set, get, remove的时候会被清除。

为了安全地使用ThreadLocal,必须要像每次使用完锁就解锁一样,在每次使用完ThreadLocal后都要调用remove()来清理无用的Entry。

private void remove(ThreadLocal<?> var1) {
ThreadLocal.ThreadLocalMap.Entry[] var2 = this.table;
int var3 = var2.length;
int var4 = var1.threadLocalHashCode & var3 - 1;
for(ThreadLocal.ThreadLocalMap.Entry var5 = var2[var4];
var5 != null; var5 = var2[var4 = nextIndex(var4, var3)]) {
if(var5.get() == var1) {
var5.clear();
this.expungeStaleEntry(var4);
return;
}
}
}

应用场景

日期处理

SimpleDateFormat被大量使用于处理时间格式化过程,但是该类并非是线程安全的,在多线程使用format()和parse()方法时可能会遇到问题,使用ThreadLocal可以解决这个问题:

public static ThreadLocal df = new ThreadLocal() {
protected DateFormat initialValue() {
return new SimpleDateFormat("MM/dd/yy");
}
};
public String formatCurrentDate() {
return df.get().format(new Date());
}

数据库session

下面是在hibernate中应用ThreadLocal的一个例子,在getSession()方法中,首先会判断当前线程中有没有放进去session,如果还没有,那么通过sessionFactory().openSession()来创建一个session,再将session set到线程中,实际是放到当前线程的ThreadLocalMap这个map中。

正如上面提到的,大部分人的想法可能是通过map,将当前thread作为key,创建的session作为值,put到map中,但ThreadLocal的实现刚好相反,它是在每个线程中有一个map,而将ThreadLocal实例作为key,这样每个map的大小很少,当线程销毁时也一起销毁了。

private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() throws InfrastructureException {
Session s = (Session) threadSession.get();
try {
if (s == null) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}

Contact

GitHub: https://github.com/ziwenxie
Blog: https://www.ziwenxie.site