ThreadLocal的使用和坑点

34次阅读

共计 2873 个字符,预计需要花费 8 分钟才能阅读完成。

概念:

ThreadLocal 的概念:摘自 ThreaLocal 的注释

This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * {@code get} or {@code set} method) has its own, independently initialized
 * copy of the variable.  {@code ThreadLocal} instances are typically private
 * static fields in classes that wish to associate state with a thread (e.g.,
 * a user ID or Transaction ID).

这段话的大概意思是 ThreadLocal 是保存的线程的本地变量,访问 get/set 方法都是对线程独立的。
大白话就是 ThreadLocal 是和线程相关的,在一个线程没有结束之前,在任意方法中 get/set 在 ThreadLocal 中设置的值都是只和当前线程有关。
因此呢,ThreadLocal 的使用场景也可以推测出来,可以用来在一个线程中传递参数,或者某些情况(比如 session,数据库操作句柄)只跟线程相关的时候来使用。

源码分析:

我们接下来在看看源代码中,ThreadLocal 是什么样的?
简答的贴了一下类和常用的几个方法的源代码(此博客是基于 JDK1.8)
类定义:

public class ThreadLocal<T>

从类定义上可以看出 ThreadLocal 是支持泛型的

ThreadLocalMap:

static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {super(k);
                value = v;
            }
        }
    // 后面代码省略
}

这里首先我们要看到 ThreadLocal 中有一个静态类叫做 ThreadLocalMap,它里面有一个静态类 Entry(可以类比 Map 中的 entry,保存实际的 key 和 value,),ThreadLocalMap 其实就是保存了 ThreadLocal 调用 set 方法设置的 value,key 就是 ThreadLocal。
总结一下就是当我们使用 ThreadLocal 的 set 方法时,ThreadLocal 为 key,保存的泛型对象为 value,存到了 ThreadLocal 的内部类 ThreadLocalMap 中,然后 ThreaLocalMap 的键值对实际上是放在静态类 Entry 里面。这里稍微提一句
Entry 是继承了 WeakReference 弱引用,key 是被弱引用的构造函数给创建的,value 是强引用。(java 的四种引用可以参考我之前写的文章 Java 四种引用简介)

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

get 方法可以看出,会先获取当前线程,然后获取 ThreadLocalMap,然后把值从 ThreadLocalMap 中取出来,如果没有 ThreadLocalMap 就去调用 setInitialValue()设置完初始值,并返回。
set:

public void set(T value) {Thread t = Thread.currentThread();
      ThreadLocalMap map = getMap(t);
      if (map != null)
          map.set(this, value);
      else
          createMap(t, value);
  }

set 方法根据当前的线程从 ThreadLocalMap 中取得,没有 map 就创建一个。thread 中的 ThreadLocal.ThreadLocalMap threadLocals 变量会指向刚才创建的 ThreadLocalMap。

其他用法

1. 初始化值
这样看下来,应该对 ThreadLocal 的实现和原理有了一个大概的认识。这里提一句,ThreadLocal 直接 new 出来,然后去 get 的值是 null。某些情况下所有的线程都需要有一个初始化的值,这时候可以重写 protected T initialValue()方法,如下:

   private static ThreadLocal<String> threadLocal2 = new ThreadLocal<String>() {
       @Override
       protected String initialValue() {return "override initialValue 的初始值";}
   };

或者 jdk1.8 可以使用 ThreadLocal.withInitial 初始化

private static ThreadLocal<String> threadLocal1 = ThreadLocal.withInitial(() -> "withInitial 的初始值");

这样所有的线程使用 ThreadLocal 的 get 都会是相同的一个初始化值。

2. 多个线程有可以相同的值
可以使用 InheritableThreadLocal,父线程中设置的值,子线程中可以访问到。这个用法和上面差不多就不举例子了,有兴趣的可以自己研究下~

坑点:

1. 上面看 ThreadLocalMap 的时候,咱们知道 key 是弱引用,gc 的时候 key 会被回收,但是 value 和 ThreadLocalMap 的引用不会被回收。如果这种情况的 Thread 很多,而且一直没有执行完,就可能会出现内存泄漏。
2. 在使用线程池的时候,当使用 ThreadLocal 调用 set 方法后,然后没有调用 remove 的话,因为线程池的线程是复用的,如果同一个线程再去调用 get 方法可能拿到的值并不是当时 set 进去的,导致程序数据异常之类的。尽量用完值后就 remove 掉。

总结:

1.ThreadLocal 在同一个线程传值,或者只跟线程相关的场景使用
2. 初始化 ThreadLocal 的值可以用 ThreadLocal.withInitial,或重写 initialValue()
3. 父子线程共享相同的值使用 InheritableThreadLocal
4.不管是正常使用还是线程池使用 ThreadLocal 都一定要使用完就 remove,否则会内存泄漏或者数据出错

正文完
 0