ThreadLocal怎么实现的?

  首先,在Thread类中有一行:

1
2
3
4
5
/** 
* ThreadLocal values pertaining to this thread.
* This map is maintained by the ThreadLocal class.
*/
ThreadLocal.ThreadLocalMap threadLocals = null;

  其中ThreadLocalMap类的定义是在ThreadLocal类中,真正的引用却是在Thread类中。同时,ThreadLocalMap使用Entry对象数组存储数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

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

/** The initial capacity -- MUST be a power of two. */
private static final int INITIAL_CAPACITY = 16;

/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;

/** The number of entries in the table. */
private int size = 0;
... ...
}

  这里需要注意的是ThreadLocalMap哈希映射中的条目使用ThreadLocal对象作为键,这个键是一个WeakReference类型引用,当设置ThreadLocal为空键时,(即entry.get()== null)意味着不再引用该键,可以从表中删除该条目。value的话为设置的值。

  ThreadLocal类通过set和get方法设置或取得ThreadLocalMap的值:

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

  其中的getMap方法:

1
2
3
4
5
6
7
8
9
10
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

  给当前Thread类对象初始化ThreadlocalMap属性:

1
2
3
4
5
6
7
8
9
10
/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

  调用了这个ThreadLocalMap的构造方法:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
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);
}

总结一下ThreadLocal究竟是如何工作的 

  在每个线程Thread内部有一个ThreadLocalMap,这是用来存储实际的变量副本的,键值key为当前ThreadLocal变量,value为变量副本。初始时,在Thread里面,ThreadLocalMap为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的ThreadLocalMap进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到ThreadLocalMap。然后在当前线程里面,如果要使用副本变量,就可以通过get方法在ThreadLocalMap里面查找。 一个Thread中只有一个ThreadLocalMap,一个ThreadLocalMap中可以有多个ThreadLocal对象,其中一个ThreadLocal对象对应一个ThreadLocalMap中的一个Entry(即一个Thread可以依附有多个ThreadLocal对象)。

  ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用。作用:提供一个线程内公共变量(比如本次请求的用户信息),减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度,或者为线程提供一个私有的变量副本,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。

ThreadLocal的内存泄露问题

  Threadlocal内存泄漏通常在有界线程池中不是一个大问题,因为如果使用静态变量来保存threadlocal单例实例,Threadlocal变量在线程被再次使用时最终都可能被覆盖,在线程池中,每个线程只泄漏(最多)一个实例(一个ThreadLocal值);但是,如果你不使用静态变量保存单例实例,程序可能会一次又一次地创建新ThreadLocal实例,线程本地值不会被覆盖,并且会在每个线程的Threadlocal Map中累积。这可能会导致严重的泄漏。即使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永远无法回收,造成内存泄漏。JAVA的ThreadLocal对Key使用到了弱引用,但是为了保证不再内存泄露,在每次set.get的时候主动对key==null的entry做遍历回收。虽然不会造成内存泄露,但是因为只有在每次set,get的时候才会对entry做key==null的判断,从而释放内存,这并不能保证ThreadLocal不会发生内存泄漏,例如:

  • 使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏。
  • 分配使用了ThreadLocal又不再调用get()、set()、remove()方法,那么就会导致内存泄漏。

  唯一彻底的方法是调用ThreadLocal.remove()方法。要执行清理,通常需要确定线程在当前处理中完成的位置。例如,在servlet filter中,可以在线程返回到线程池之前删除threadlocal变量。您一般不会使用try-finally块,因为插入threadlocal对象的位置与清理它的位置相去甚远。假设您正在ka在webapp处理HTTP请求时创建/使用的线程局部变量,避免线程本地泄漏的一种方法是在webapp的ServletContext中注册一个ServletRequestListener,并实现该侦听器的requestDestroyed方法来清除当前线程的线程本地。注意,在这种情况下,您还需要考虑信息从一个请求泄漏到另一个请求的可能性。

ThreadLocal变量的存在周期

  存储在ThreadLocal中的对象将一直附在该线程,直到显式删除为止.

  正如javadoc中所说:

只要线程是活动的并且线程本地实例是可访问的,那么每个线程都持有对其线程本地变量副本的隐式引用。在线程消失之后,它的所有线程本地实例副本都将进入垃圾收集(除非存在对这些副本的其他引用)。

  例如,如果您的服务在servlet容器中执行,那么当请求完成时,它的线程将返回到池中。如果您还没有清理线程的ThreadLocal变量内容,那么在线程处理下一个请求时该数据将继续存在。每个线程都是GC根节点,附加到线程的线程本地变量在线程结束后才会被垃圾回收。

  ThreadLocalMap的getEntry函数的流程大概为:

  • 首先从ThreadLocal的直接索引位置(通过ThreadLocal.threadLocalHashCode & (table.length-1)运算得到)获取Entry e,如果e不为null并且key相同则返回e;
  • 如果e为null或者key不一致则向下一个位置查询,如果下一个位置的key和当前需要查询的key相等,则返回对应的Entry。否则,如果key值为null,则擦除该位置的Entry,并继续向下一个位置查询。在这个过程中遇到的key为null的Entry都会被擦除,那么Entry内的value也就没有强引用链,自然会被回收。仔细研究代码可以发现,set操作也有类似的思想,将key为null的这些Entry都删除,防止内存泄露。

  但是光这样还是不够的,上面的设计思路依赖一个前提条件:要调用ThreadLocalMap的getEntry函数或者set函数。这当然是不可能任何情况都成立的,所以很多情况下需要使用者手动调用ThreadLocal的remove函数,手动删除不再需要的ThreadLocal,防止内存泄露。所以JDK建议将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露

为什么不直接用线程id来作为ThreadLocalMap的key?

  这一点很容易理解,因为直接用线程id来作为ThreadLocalMap的key,无法区分放入ThreadLocalMap中的多个value。比如我们放入了两个字符串,你如何知道我要取出来的是哪一个字符串呢?
  而使用ThreadLocal作为key就不一样了,由于每一个ThreadLocal对象都可以由threadLocalHashCode属性唯一区分或者说每一个ThreadLocal对象都可以由这个对象的名字唯一区分(下面的例子),所以可以用不同的ThreadLocal作为key,区分不同的value,方便存取。

ThreadLocal使用场景

  1. 每个线程都需要维护一个自己专用的线程的上下文变量,比如jdbc连接,web应用中的session等。
  2. 包装一个线程不安全的成员变量,给其提供一个线程安全的环境,同时避免对该对象的同步访问(synchronized),比如Java里面的SimpleDateFormat是线程不安全的,所以在多线程下使用可以采用ThreadLocal包装,从而提供安全的访问。
  3. 对于一些线程级别,传递方法参数有许多层的时候,我们可以使用ThreadLocal包装,只在特定地方set一次,然后不管在什么地方都可以随便get出来,从而巧妙了避免了多层传参。如果上下文信息的范围仅限于一个service,那么最好通过方法参数传递信息,而不是使用ThreadLocal。ThreadLocal适用于需要跨不同服务或在不同层中传递提供信息的情况,如果只被一个service使用,那么你的代码就会显得过于复杂。如果你有数据被AOP advice在不同对象使用,那么将这些数据放到threadlocal中可能是一种不错的方法。
联系我

评论