作用

将资源副本保存各个线程中,不同线程间不会相互干扰,这种变量在线程的生命周期中起作用,降低了每个线程内公共变量在不同组建/模块间的传递复杂性。总而言之,在于三个点:

  1. 线程并发:在多线程的场景下, ThreadLocal 有大用途;如果仅想在单线程下使用也可以
  2. 数据传递:可以在同一线程里,不同组件/模块间传递公共变量
  3. 线程隔离:每个变量都是线程独有的,不会互相影响

与Synchronized的对比

在某些场景下,使用 synchronized 和 ThreadLocal 作用是一样的,但是它们之间有以下这样的区别:

synchrnoized ThreadLocal
原理 采用“以时间换空间”的方式,只提供了一个变量,让多个线程排队去修改/读取 采用“以空间换时间”的方式,为每一个线程都提供了一份变量副本,从而实现不同线程间的隔离
侧重点 多个线程访问资源的串行化 让各个线程里的资源独立,互不干扰

所以,可以使用 ThreadLocal 的场景下,尽量去使用 ThreadLocal ,因为不需要上锁,所以性能好一些。
总而言之, ThreadLocal 采用了资源隔离方法,各个线程的资源不共享,那么就不存在并发问题了。

内部结构

点击查看【processon】
两者相较而言, JDK8 的设计有以下几个好处:

  1. 不需要考虑多个新线程往 ThreadLocalMap 里设置数据的并发问题
    1. 在 JDK8 以前,仍然会有这样一种情况,多个线程对 ThreadLocalMap 同时修改的情况
    2. 在 JDK8 改进后, ThreadLocalMap 已经是线程私有的东西,不存在并发问题
  2. 每当 Thread 销毁的时候 ThreadLocalMap 也会随之销毁,减少内存的开销
    1. 在 JDK8 以前,线程销毁后, ThreadLocalMap 里保存的对应线程的资源不会立刻释放掉
    2. 在 JDK8 改进后, ThreadLocalMap 集成在 Thread 中,当 Thread 销毁, ThreadLocalMap 随之销毁

ThreadLocal

  • set(T value) :设置值

    1. public void set(T value) {
    2. // 获取当前线程
    3. Thread t = Thread.currentThread();
    4. // 获取Thread.threadLocals属性
    5. ThreadLocalMap map = getMap(t);
    6. // 如果map不为空,即已经存在了ThreadLocalMap
    7. if (map != null)
    8. // 单纯的设置value到对应的ThreadLocalMap里就行了
    9. map.set(this, value);
    10. else
    11. // 否则创建ThreadLocalMap
    12. createMap(t, value);
    13. }
    14. -----------------------------------我是分割线-------------------------------------------
    15. void createMap(Thread t, T firstValue) {
    16. // 如果当前的Thread没有ThreadLocalMap,就创建一个
    17. t.threadLocals = new ThreadLocalMap(this, firstValue);
    18. }
  • get() :获取值

    public T get() {
      // 获取当前线程
      Thread t = Thread.currentThread();
      // 获取Thread.threadLocals属性
      ThreadLocalMap map = getMap(t);
      // 如果存在map
      if (map != null) {
          // 获取map里,当前ThreadLocal对应的值
          ThreadLocalMap.Entry e = map.getEntry(this);
          // 如果entry不为空,就返回
          if (e != null) {
              @SuppressWarnings("unchecked")
              T result = (T)e.value;
              return result;
          }
      }
      // 如果e为空 或者 map为空,就初始化当前的ThreadLocalMap,并设置初始值
      return setInitialValue();
    }
    -----------------------------------我是分割线-------------------------------------------
    private T setInitialValue() {
      // 让子类实现的方法,如果不实现,默认是null
      T value = initialValue();
      Thread t = Thread.currentThread();
      // 获取当前Thread的ThreadLocalMap
      ThreadLocalMap map = getMap(t);
      // 如果已经存在ThreadLocalMap,则设置默认值
      if (map != null)
          map.set(this, value);
      else
          // 否则创建ThreadLocalMap,并创建默认值
          createMap(t, value);
      return value;
    }
    

    东西比较简单,就是要搞清楚哪个设置到哪个里面去。首先 ThreadLocalMap 是承载 ThreadLocal 对象和和对应值的容器;每个 ThreadLocal 对应着一个特定的值,所以要想保存多个值,就是创建多个不同的 ThreadLocal 即可。

    ThreadLocalMap

    ThreadLocalMap 的基本结构如下所示: 👀Java并发 - ThreadLocal - 图1

    Entry

    在 ThreadLocalMap 中使用 Entry 来作为键值对元素,而 Entry 的键只能是 ThreadLocal 。另外 Entry 通过继承 WeakReference 将 ThreadLocal 对象的生命周期和 Thread 的生命周期解绑,当没有强引用指向 ThreadLocal 变量时,它可被回收。

    /**
    * 继承自WeakReference
    */
    static class Entry extends WeakReference<ThreadLocal<?>> {
      // 该ThreadLocal对应的值
      Object value;
      Entry(ThreadLocal<?> k, Object v) {
          // ThreadLocal作为 WeakReference 保留下来
          super(k);
          // 保存值
          value = v;
      }
    }
    

    注意,这里仅仅是 Key ( ThreadLocal )可以被回收,但是整个 Entry 键值对元素还是存在内存泄漏的可能。

实践

ThreadLocal 用起来感觉挺不直观的,所以这里再记录一下:

@Test
public void testThreadBasic(){
    // 创建一个ThreadLocal对象(这货其实是一个Key)
    final ThreadLocal<SingletonBean2> myThreadLocal = new ThreadLocal<>();
    // 在set()过程中,会通过Thread.cuurentThread()获取当前线程
    // 然后给Thread.threadLocals添加(put)一个  ThreadLocal -> Value 的映射
    // 拿下面这个例子来说就是 给当前线程(main线程)的threadLocals(Map)添加了一个
    // ThreadLocal -> Value的映射
    myThreadLocal.set(singletonBean1);
    // 这里新建了一个线程
    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            // 尝试通过相同的ThreadLocal对象获取值
            // 注意一定得是相同的ThreadLocal对象(当然,这里是获取不到值的,因为映射保存在
            // main线程中)
            System.out.println(myThreadLocal.get());
        }
    });
    thread.start();
}

拓展

InheritableThreadLocal

这里提一个比较有趣的东西,今天学习 Thread 的时候看到的~~就是 InheritableThreadLocal 这货了,这货继承了 ThreadLocal ,然后覆盖了以下几个方法:

方法原型 方法释义
createMap(Thread, T) 给 Thread 创建一个 ThreadLocalMap ,并添加一个
this -> value 的映射
getMap(Thread) 用来获取 Thread.inheritableThreadLocals 属性
childValue(T) 用来获取子线程应该继承到的值,可以通过继承 InheritableThreadLocal 重新定义每个子线程从父线程继承过来的值的样子

简单来说,以前通过 ThreadLocal#set() 设置值是设置给 CurrentThread.threadLocals 属性;现在若通过 InheritableThreadLocal#set() 设置值,则是设置给当前线程 CurrentThread.inheritableThreadLocals 属性的,而且该属性是可以被子线程继承的(内置逻辑是有线程组就会开启继承,不是线程组就不开启)。我们来看看它的代码演示:

public void main(String[] args){
    // 可以继承的Bean
    SingletonBean2 canBeInheritableBean = new SingletonBean2("Hello");
    // 不可以继承的Bean
    SingletonBean2 notBeInheritableBean = new SingletonBean2("World");
    // InheritableThreadLocal
    final InheritableThreadLocal<SingletonBean2> inheritableThreadLocal = new InheritableThreadLocal<>();
    // 是设置给当前线程的inheritableThreadLocals属性里(一样是ThreadLocalMap)
    inheritableThreadLocal.set(canBeInheritableBean);
    // 创建ThreadLocal
    final ThreadLocal<SingletonBean2> myThreadLocal = new ThreadLocal<>();
    // 是设置给当前线程的threadLocals属性里
    myThreadLocal.set(notBeInheritableBean);
    // 创建一个子线程,通过源码点进去可以发现它开启了继承功能
    // 所以当前线程(main线程)会将inheritableThreadLocals设置到子线程里面去
    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            // 输出为null,因为这是一个新线程,它没有设置ThreadLocal
            System.out.println(myThreadLocal.get());
            // 输出为从父线程继承过来的值,即canBeInheritableBean
            System.out.println(inheritableThreadLocal.get());
        }
    });
    thread.start();
}

最后输出为:

null
SingletonBean2{title='Hello'}

除此之外,我们可以对 InheritableThreadLocal 进行拓展,重写赋值给子类的值(中间层):

static class MyInheritableThreadLocal extends InheritableThreadLocal<SingletonBean2>{
    @Override
    protected SingletonBean2 childValue(SingletonBean2 parentValue) {
        return new SingletonBean2(){
            @Override
            public String toString() {
                return "我重新定义了SingletonBean2的toString,你也可以做其他自定义的修改~";
            }
        };
    }
}

然后对我们前面的测试代码(第七行)稍作修改:

public void main(String[] args){
    // 省略
    ...
    // +++改成如下这样的代码
    final MyInheritableThreadLocal inheritableThreadLocal = new MyInheritableThreadLocal();
    // 省略
    ...
}

最后输出:

null
我重新定义了SingletonBean2的toString,你也可以做其他自定义的修改~

总结

是否存在内存泄漏

是的,仍然会有内存泄漏。示意图如下所示:
点击查看【processon】
即便 key 是弱引用,在GC时 key 能够被回收;但是 Entry 仍然会被 Map 持有着,所以对应 Entry 内的 Value 仍然释放不掉。在这种情况下,如果 Thread 还是线程池持有的,又没有手动释放,就会产生内存泄漏了。
总而言之,当 Thread 不是用完即毁的那种,而且又没有手动移除 **Entry ,那么就会造成内存泄漏**。

为什么使用弱引用作为Key

其实无论是弱引用还是强引用,都会存在内存泄漏的问题。那使用强引用作为 Key 行吗?

  • 强引用下 Key 是百分百无法移除了
  • 弱引用下外部的线程一旦不使用这个 Key 了,下次 gc 的时候,这个 Key 就会被回收,该 Reference 就是 null 值。

在弱引用这种情况下可以找到**不再被外部使用**的 Key ,间接通过其他手段实现 Entry 的清理;比如通过 set() 、 getEntry() 等方法,一旦发现 key == null 就进行清理工作。

使用WeakHashMap不行吗

可以是可以,但是太复杂了。 ThreadLocal 就只需要 get 、 set 、 remove 而已,所以自己实现一个简单的达到目的 ThreadLocalMap 即可。

解决内存泄漏

  • 每次用完手动移除 Entry
  • 线程用后即毁