深入理解ThreadLocal

什么是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函数的流程:
  1. 首先从ThreadLocal的直接索引位置(通过ThreadLocal.threadLocalHashCode & (len-1)运算得到)获取Entry e,如果e不为null并且key相同则返回e;
  2. 如果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它,防止内存泄露。
    


参考:
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值