作用

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

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

与Synchronized的对比

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

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 通过继承 WeakReferenceThreadLocal 对象的生命周期和 Thread 的生命周期解绑,当没有强引用指向 ThreadLocal 变量时,它可被回收。

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

    注意,这里仅仅是 KeyThreadLocal )可以被回收,但是整个 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 就只需要 getsetremove 而已,所以自己实现一个简单的达到目的 ThreadLocalMap 即可。

解决内存泄漏

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