ThreadLocal 看这一篇就够了

375 阅读4分钟

基本介绍及使用场景

ThreadLocal提供了一个线程本地的变量,在线程中都可以访问到该变量。一般来说threadlocal是一个private static的变量,这样方便多个线程使用。常用的场景:
- 1. 在线程中传递参数,这样不用在调用链的每个方法都传递,eg: transaction id,user id等
- 2. 每个线程一个持有一个副本,避免线程安全的问题

基本使用方法

   private static ThreadLocal<Long> userIdThreadLocal = new ThreadLocal<Long>() {
        @Override
        protected Long initialValue() {
            return -1L;
        }
   };
   

然后在不同的线程中使用userIdThreadLocal:

   Thread thread1 = new Thread(new Runnable() {
         @Override
         public void run() {
             userIdThreadLocal.set(100L);
             try {
                 Thread.sleep(3 * 100L);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }

             System.out.println("Thread1 :" + userIdThreadLocal.get());
         }
     });

     Thread thread2 = new Thread(new Runnable() {
         @Override
         public void run() {
             System.out.println("Thread2 :" + userIdThreadLocal.get());
             userIdThreadLocal.set(200L);
             System.out.println("Thread2 :" + userIdThreadLocal.get());
         }
     });

     thread1.start();
     thread2.start();

输出结果为: Thread2 :-1 Thread2 :200 Thread1 :100

内部原理

看下调用过程:

ThreadLocal的get方法:

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

ThreadLocal的get()方法,会直接获取当前线程,然后获取当前线程的threadlocals变量,因此threadlocalMap其实是存在thread内部的。 进一步看下ThreadLocalMap的结构,内部是一个Entry数组,key是ThreadLocal,value是线程要保存的值。因此一个Thread是可以有持有多个ThreadLocal变量的,就是存在ThreadLocalMap中。 ThreadLocal.get() -> Thread.threadLocals -> ThreadLocalMap.getEntry 逐个判断数组中Entry的可以是否为当前要找的ThreadLocal。

内存泄漏问题

首先界定下什么是内存泄露。内存泄露是指一块内存不再使用了,但是永远不会被回收。一般说起内存泄露,大家都会跟OOM联系起来,其实不一定引起OOM,如果泄露的内存占整体内存的比例很小,或者不是持续性的,那么并不会导致OOM,因为可用内存还多着呢。 ThreadLocal的内存泄露是如何产生的呢?如果Thread执行完毕并退出,那么对应的ThreadLocalMap对象以及其中的Entry都会被回收,没有内存泄漏的问题。如果是线程一直运行,但是threadLocal中保存的value对象已经不再需要了,此时对象没有用但是一直没有被回收,产生内存泄漏。下图为内存引用关系:

如何避免内存泄露呢?

  1. 如果将外部的static userIdThreadLocal变量赋为null,结合WeakReference是否就可以避免内存泄漏呢? 答案是不行。因为Entry中Key是软引用,如果外部引用断开后,Entry中的Key会被回收,但是value对象并不会回收,而且永远也访问不到,因为key变成了Null。
  2. 最佳实践是如果不再使用,手动的调用remove方法。
  3. 调用ThreadLocal的get,set 等方法时也会触发调用 expungeStaleEntry 方法,清理掉threadlocal已经为Null的value对象。

父子线程问题

实际应用中可能出现子线程需要使用父线程的threadlocal变量的情况,此时直接threadlocal是不可继承的。JDK已经考虑了这种情况,可以使用InheritableThreadLocal,子线程可以继承父线程的threadlocal。用法同ThreadLocal完全一样。完美?那看下是怎么实现的,下面是Thread的init方法中有关的一部分:
`
...
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
...
`
这个实现方式决定了只有每次使用新线程都new一个出来,才能共享父线程的inheritThreadLocals,实战中大部分场景用到的都是线程池,那就会出现只有第一次使用线程池时inheritThreadLocals中的值是正确的,之后就不会再改变。显然是有问题的。

线程池中如何正确使用ThreadLocal

常用的方式:

  1. 在提交任务到线程池时继承Runnable或者Callable类,手动将需要的参数传递进去。这种方式一定程度上违背了我们使用ThreadLocal的初衷,我们就是不想到处传递参数才用ThreadLocal的。
  2. 使用Spring的ThreadPoolTaskExecutor,该方法提供了setTaskDecorator方法,设置一个装饰器,我们可以在装饰器中做上面 inheritThreadLocals 赋值的操作
  3. 使用阿里的TransmitThreadLocal 最好的方式。

阿里的TransmitThreadLocal

TransmitThreadLocal常用的两种用法,

  1. 使用其提供的TtlRunnable、TtlCallable
  2. 使用TtlExecutors提供的getExecutor,getExecutorService,getScheduledExecutorService等方法 其本质都是,在创建任务类时拷贝父线程的threadlocal,任务执行完成后做清理。 用法比较简单,可以直接点开参考资料中的UserGuide在此不多赘述。

欢迎大家多多讨论,批评指正,如果您觉得不错,点赞转发关注走起。 参考资料: TransmitThreadLocal GitHub地址