华企号 软件设计 ThreadLocal深度解析和应用示例

ThreadLocal深度解析和应用示例

开篇明意

ThreadLocal是JDK包提供的线程本地变量,如果创建了ThreadLocal<T>变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。

ThreadLocal很容易让人望文生义,想当然地认为是一个“本地线程”。其实,ThreadLocal并不是一个Thread,而是Thread的一个局部变量(更准确的是ThreadLocal中的一个静态内部类ThreadLocalMap存储着线程中各自的独立变量值),也许把它命名ThreadLocalVariable更容易让人理解一些。

下面代码是Thread类中定义的ThreadLocals和inheritableThreadLocals,可以看到实际操作的对象是ThreadLocal类和其静态内部类ThreadLocalMap

public Thread implements Runnable{
    
  /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;  
}

 

来看看官方的定义:这个类提供线程局部变量。这些变量与正常的变量不同,每个线程访问一个(通过它的get或set方法)都有它自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,希望将状态与线程关联(例如,用户ID或事务ID)。

源码解析

  1.核心方法之   set(T t)

 1     /**
 2      * Sets the current thread's copy of this thread-local variable
 3      * to the specified value.  Most subclasses will have no need to
 4      * override this method, relying solely on the {@link #initialValue}
 5      * method to set the values of thread-locals.
 6      *
 7      * @param value the value to be stored in the current thread's copy of
 8      *        this thread-local.
 9      */
10     public void set(T value) {
11         Thread t = Thread.currentThread();
12         ThreadLocalMap map = getMap(t);
13         if (map != null)
14             map.set(this, value);
15         else
16             createMap(t, value);
17     }

解析:

当调用ThreadLocal的set(T t)的时候,代码首先会获取当前线程的 ThreadLocalMap(ThreadLocal中的静态内部类,同时也作为Thread的成员变量存在,后面会进一步了解ThreadLocalMap),如果ThreadLocalMap存在,将ThreadLocal作为map的key,要保存的值作为value来put进map中(如果map不存在就先创建map,然后再进行put);

  2.核心方法值 get()

/**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current 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);        //此处和set方法一致,也是通过当前线程获取对应的成员变量ThreadLocalMap,map中存放的是Entry(ThreadLocalMap的内部类(继承了弱引用))
    if (map != null) {
      ThreadLocalMap.Entry e = map.getEntry(this);
      if (e != null) { 
        @SuppressWarnings("unchecked")
        T result = (T)e.value; 
        return result;
      }
    } 
    return setInitialValue(); 
    }

解析:

  刚才把对象放到set到map中,现在根据key将其取出来,值得注意的是这里的map里面存的可不是键值对,而是继承了WeakReference<ThreadLocal<?>> 的Entry对象,关于ThreadLocalMap.Entry类,后面会有更加详尽的讲述。

核心方法之  remove()

    /**
     * Removes the current thread's value for this thread-local
     * variable.  If this thread-local variable is subsequently
     * {@linkplain #get read} by the current thread, its value will be
     * reinitialized by invoking its {@link #initialValue} method,
     * unless its value is {@linkplain #set set} by the current thread
     * in the interim.  This may result in multiple invocations of the
     * {@code initialValue} method in the current thread.
     *
     * @since 1.5
     */
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

解析:

通过getMap方法获取Thread中的成员变量ThreadLocalMap,在map中移除对应的ThreadLocal,由于ThreadLocal(key)是一种弱引用,弱引用中key为空,gc会回收变量value,看一下核心的m.remove(this);方法

        /**
         * Remove the entry for key.
         */
        private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1); //定义Entry在数组中的标号
            for (Entry e = tab[i];              //通过循环的方式remove掉Thread中所有的Entry
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {   
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

小结:

    结合官方对ThreadLocal类的定义,threadLocal主要满足某些变量或者示例是线程隔离的,但是在相同线程的多个类或者方法中都能使用的到,并且当线程结束时该变量也应该销毁。通俗点讲:ThreadLocal保证每个线程有自己的数据副本,当线程结束后可以独立回收。由于ThreadLocal的特性,同一线程在某地方进行设置,在随后的任意地方都可以获取到。从而可以用来保存线程上下文信息。常用的比如每个请求怎么把一串后续关联起来,就可以用ThreadLocal进行set,在后续的任意需要记录日志的方法里面进行get获取到请求id,从而把整个请求串起来。使用场景有很多,比如:
  • 基于用户请求线程的数据隔离(每次请求都绑定userId,userId的值存在于ThreadLoca中)
  • 跟踪一个请求,从接收请求,处理到返回的整个流程,有没有好的办法   思考:微服务中的链路追踪是否利用了ThreadLocal特性
  • 数据库的读写分离
  • 还有比如Spring的事务管理,用ThreadLocal存储Connection,从而各个DAO可以获取同一Connection,可以进行事务回滚,提交等操作。
  问:如果我启动另外一个线程。那么在主线程设置的Threadlocal值能被子线程拿到吗?
    原始的ThreadLocal是不具有继承(或者说传递)特性的
  问:那该如何解决ThreadLocal无法传递的问题呢?
    用ThreadLocal的子类 InheritableThreadLocal,InheritableThreadLocal是具有传递性的
  /**
  * 重写Threadlocal类中的getMap方法,在原Threadlocal中是返回
  * t.theadLocals,而在这么却是返回了inheritableThreadLocals,因为
  * Thread类中也有一个要保存父子传递的变量
  */
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }
    /**
     * 同理,在创建ThreadLocalMap的时候不是给t.threadlocal赋值
     *而是给inheritableThreadLocals变量赋值
     * 
     */
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }

解析:因为InheritableThreadLocal重写了ThreadLocal中的getMap 和createMap方法,这两个方法维护的是Thread中的另外一个成员变量  inheritableThreadLocals,线程在创建的时候回复制inheritableThreadLocals中的值 ;

/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
  //Thread类中维护的成员变量,ThreadLocal会维护该变量
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
//Thread中维护的成员变量 ,InheritableThreadLocal 中维护该变量
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;


 

//Thread init方法中的关键代码,简单来说是将父类中inheritableThreadLocals中的值拷贝到当前线程的inheritableThreadLocals中(浅拷贝,拷贝的是value的地址引用)
 if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

总结

  • ThreadLocal类封装了getMap()、Set()、Get()、Remove()4个核心方法。
  • 通过getMap()获取每个子线程Thread持有自己的ThreadLocalMap实例, 因此它们是不存在并发竞争的。可以理解为每个线程有自己的变量副本。
  • ThreadLocalMap中Entry[]数组存储数据,初始化长度16,后续每次都是1.5倍扩容。主线程中定义了几个ThreadLocal变量,Entry[]才有几个key。
  • Entry的key是对ThreadLocal的弱引用,当抛弃掉ThreadLocal对象时,垃圾收集器会忽略这个key的引用而清理掉ThreadLocal对象, 防止了内存泄漏。

拓展:

ThreadLocal在线程池中使用容易发生的问题: 内存泄漏

 

每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收.

所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露

PS.Java为了最小化减少内存泄露的可能性和影响,在ThreadLocal的get,set的时候都会清除线程Map里所有key为null的value。所以最怕的情况就是,threadLocal对象设null了,开始发生“内存泄露”,然后使用线程池,这个线程结束,线程放回线程池中不销毁,这个线程一直不被使用,或者分配使用了又不再调用get,set方法,那么这个期间就会发生真正的内存泄露。

  1. JVM利用设置ThreadLocalMap的Key为弱引用,来避免内存泄露。
  2. JVM利用调用remove、get、set方法的时候,回收弱引用。
  3. 当ThreadLocal存储很多Key为null的Entry的时候,而不再去调用remove、get、set方法,那么将导致内存泄漏。
  4. 当使用static ThreadLocal的时候,延长ThreadLocal的生命周期,那也可能导致内存泄漏。因为,static变量在类未加载的时候,它就已经加载,当线程结束的时候,static变量不一定会回收。那么,比起普通成员变量使用的时候才加载,static的生命周期加长将更容易导致内存泄漏危机。

在线程池中使用ThreadLocal

通过上面的分析可以知道InheritableThreadLocal是通过Thread()的inint方法实现父子之间的传递的,但是线程池是统一创建线程并实现复用的,这样就好导致下面的问题发生:

  •   线程不会销毁,ThreadLocal也不会被销毁,这样会导致ThreadLoca会随着Thread的复用而复用
  •   子线程无法通过InheritableThreadLocal实现传递性(因为没有单独的调用Thread的Init方法进行map的复制),子线程中get到的是null或者是其他线程复用的错乱值。

解决方案:

基本思路是重在使用完这个线程的时候清除所有的localMap,在submit新任务的时候在父线程中copy所有的Entry。然后重新给当前线程的t.inhertableThreadLocal赋值。这样就能够解决在线程池中每一个新的任务都能够获得父线程中ThreadLocal中的值而不受其他任务的影响,因为在生命周期完成的时候会自动clear所有的数据。Alibaba的一个库解决了这个问题:github:alibaba/transmittable-thread-local

上一篇
下一篇

发表回复

联系我们

联系我们

028-84868647

在线咨询: QQ交谈

邮箱: tech@68v8.com

工作时间:周一至周五,9:00-17:30,节假日休息

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

关注微博
返回顶部