什么是ThreadLocal
JDK源码中这样描述ThreadLocal:
ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程的上下文。
为什么要有ThreadLocal
通常对于常用的业务逻辑有两种处理方法:包装成业务逻辑类,每次都进行New出一个新对象,业务逻辑封装成静态方法将必备参数传入进行调用,这就是完全无状态的Bean。
然而,第一种方式带有很多的新建,对应的GC也很多,使用也相对麻烦。而第二种,则能够直接调用,但是需要传递很多参数,线程敏感的任何参数都不能从静态类中获取,这就导致了很多业务难以实现。
当线程内部需要维持的变量很少的时候,为了这些很少的变量而每次都新建业务Bean是很不合算的,而每次调用方法都传递这些参数有很繁琐。那么就需要找到一个办法在线程的生命周期内保持这个变量,这就是ThreadLocal存在的价值:在单例Bean中保持线程局部变量。
因此,ThreadLocal的应用场合,最适合的是按多线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。
在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态采用ThreadLocal进行处理,让它们也成为线程安全的状态,因为有状态的Bean就可以在多线程中共享了。
实现原理
ThreadLocal类中有一个静态内部类ThreadLocalMap(其类似于Map),其中元素的key为当前ThreadLocal对象,即ThreadLocal实例是作为map的key来使用的。而value对应线程的变量副本,每个线程可能存在多个ThreadLocal。
通过ThreadLocal.set()将这个新创建的对象的引用保存到各线程的自己的ThreadLocalMap中,执行ThreadLocal.get()时,各线程从自己的map中取出放进去的对象,因此取出来的是各自自己线程中的对象。
/**
Returns the value in the current thread's copy of this
thread-local variable. If the variable has no value for thecurrent thread, it is first initialized to the value returned by an invocation of the {@link #initialValue} method.
@return the current thread's value of this thread-local
*/
public T get() {
Thread t = Thread.currentThread();//当前线程
ThreadLocalMap map = getMap(t);//获取当前线程对应的ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e =
map.getEntry(this);//获取对应ThreadLocal的变量值
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();//若当前线程还未创建ThreadLocalMap,则返回调用此方法并在其中调用createMap方法进行创建并返回初始值。
}
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;
}
//设置变量的值
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的threadlocals,并将第一个值存入到当前map中
@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对应的ThreadLocal
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
|
这个个过程和我们的理解是相反的:通常我们如果自己实现,会考虑维持一个全局Map,ThreadID作为Key,而Value存的是需要的对象。但是实际上,JDK实现中,是将这个Map放在了每个线程里面,跟我们的思路是反的。
这样每个map中的项数很少,而且当线程销毁时相应的东西也一起销毁了;而且可以保证这个Map的容量不会特别大,这样速度也会快很多;把map放到各自线程中带来的好处是 因为各线程访问的map是各自不同的map,所以不需要同步,速度会快些。
其实在jdk1.4之前的ThreadLocal的实现就是类似第一种情况的实现,jdk1.4就改成后面那种实现。
通过这个原理可知。
如果ThreadLocal.set()进去的东西本来就是多个线程共享的同一个对象,那么多个线程的ThreadLocal.get()取得的还是这个共享对象本身,还是有并发访问问题。
此外,也可以看出ThreadLocal变量在线程的生命周期内起作用。
使用方式
ThreadLocal类提供了四个对外开放的接口方法,这也是用户操作ThreadLocal类的基本方法:
(1) void set(Object value)设置当前线程的线程局部变量的值。
(2) public Object get()该方法返回当前线程所对应的线程局部变量。
(3) public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,
该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但是如果线程持续存在,那么就应该调用,具体原因见最后一节。
(4) protected Object
initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次,ThreadLocal中的缺省实现直接返回一个null。
深入所谓内存ThreadLocal的内存泄露问题(参考)
static class ThreadLocalMap {
//map中的每个节点Entry,其键key是ThreadLocal并且还是弱引用,这也导致了后续会产生内存泄漏问题的原因。
static class Entry extends
WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
/**
* 真正用于存储线程的每个ThreadLocal的数组,将ThreadLocal和其对应的值包装为一个Entry。
*/
private Entry[] table;
///....其他的方法和操作都和map的类似
}
|
map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key.
然后网上就传言,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的getEntry函数的流程:
- 首先从ThreadLocal的直接索引位置(通过ThreadLocal.threadLocalHashCode & (len-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它,防止内存泄露。
参考: