添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
Skip to content

ThreadLocal线程变量

从JDK1.2开始,Java就提供了java.lang.ThreadLocal,ThreadLocal为每个使用线程都提供独立的变量副本,可以做到线程间的数据隔离,每个线程都可以访问各自内部的副本变量,因此不存在线程安全问题。ThreadLocal还是实现线程上下文传递的重要工具类。本章将介绍ThreadLocal的API、实现原理、典型应用和内存泄漏问题等。

5.1 常用API以及使用

ThreadLocal的API文档如下图5-1所示,主要的方法有get、initialValue、remove、set和withInitial等5个方法。

图5-1 ThreadLocal的API

图5-1 ThreadLocal的API

5.1.1 常用API

  • initialValue()

ThreadLocal提供了两种实例化的方式:继承ThreadLocal类,并重写initialValue()方法来定义初始化逻辑;创建ThreadLocal的匿名子类,并在其构造器中初始化。 以下是两种方式的示例代码:

withInitial()方法是Java8引入的一个简化的构造方法,允许使用Lambda表达式来赋值。

  • get()

要从ThreadLocal中获取值,可以调用get方法:

  • remove()

要从ThreadLocal中删除值,可以调用remove方法:

  • set()

设置当前线程的线程局部变量的值

5.1.2 基本使用

下面的示例来说明ThreadLocal的基本使用。

在这个示例中,每个线程都会打印其自己的线程ID,而不是其他线程的ID。

ThreadLocal的应用场景主要分为两类:

  • 避免对象在方法之间层层传递,打破层次间约束

例如请求调用链的唯一traceId,在很多地方都需要用到,层层往下传递,比较麻烦。这时候就可以把traceId放到ThreadLocal中,在需要的地方可以直接获取。

  • 拷贝对象副本,减少初始化操作,并保证线程安全

比如数据库连接、Spring事务管理和SimpleDataFormat格式化日期等场景,都是使用的ThreadLocal,即避免每个方法都初始化一个对象,又保证了多线程下的线程安全。

使用ThreadLocal保证SimpleDataFormat格式化日期的线程安全,代码如下。

5.2 源码解析

5.2.1 ThreadLocal类的UML图

使用Intellij Idea的UML插件绘制了ThreadLocal类图,如下图5-2所示。

图5-2 ThreadLocal类的UML图

图5-2 ThreadLocal类的UML图

在图5-2中,ThreadLocalMap是ThreadLocal的静态内部类,而Entry是ThreadLocalMap的静态内部类,并继承了弱引用类。ThreadLocal有2个子类:SuppliedThreadLocal和InheritableThreadLocal。线程Thread中持有一个ThreadLocalMap对象。

5.2.2 ThreadLocal源码解析

  • 属性字段

每个ThreadLocal实例都有一个threadLocalHashCode值,这个值由nextHashCode和常量HASH_INCREMENT计算出来。

  • 内部类SuppliedThreadLocal

SuppliedThreadLocal是JDK8新增的内部类,只是扩展了ThreadLocal的初始化值的方法而已,允许使用JDK8新增的Lambda表达式赋值。需要注意的是,函数式接口中Supplier不允许为null,使用方法可参考上面的使用示例。

  • 构造方法

可以看到其构造方法没有进行任何操作。

  • nextHashCode()

创建ThreadLocal实例时生成其对应的hashcode,每次原子增加HASH_INCREMENT的大小。

  • initialValue()

返回当前线程的ThreadLocal初始设置值。这个方法在当前线程第一次调用ThreadLocal.get方法时进行调用,如果之前已经通过set方法设置过值,则不会调用。这个方法需要自行实现,来完成定制操作,也就是希望ThreadLocal在每个线程中初始化值不同时可以进行定制。

  • withInitial()

Lambda表达式赋值,可参考上面示例。

  • get()

获取当前线程Thread对象的ThreadLocalMap对象,并获取当前ThreadLocal对应Entry。如果ThreadLocalMap还未初始化或当前ThreadLocal的Entry为空,则调用setInitialValue(),从此也能看出其使用的是懒加载,用到时才进行初始化。

  • setInitialValue()

初始化操作,返回初始化的值

  • set(T value)

set操作与setInitialValue类似,只是value是外部传入的。

  • remove()

移除当前线程中的ThreadLocalMap对应的ThreadLocal的Entry,如果当前线程调用了remove之后又调用get,则会重新调用initialValue,可参考上面的get方法。

  • getMap()

获取线程的threadLocals。

  • createMap()

创建(初始化)ThreadLocalMap,并通过firstValue设置初始值

5.3 线程关联

5.3.1 线程上下文丢失

ThreadLocal能够很好的解决线程内部的上下文传递问题,但是对于使用多线程的异步场景,线程上下文会丢失。下面的代码,在主线程中设置线程变量,然后启动一个子线程,在子线程中获取线程变量的值。

输出结果如下,可以看到子线程无法获取主线程设置的线程变量。

从线程变量的名称和作用来看,这个子线程获取为空是符合预期的,但是从线程上下文传递的功能角度来看,却是不满足需求的。于是Java官方又提供了ThreadLocal的子类InheritableThreadLocal来解决创建新线程时的上下文传递丢失的问题。

5.3.2 InheritableThreadLocal

使用TheadLocal时,子线程访问不了父线程的本地变量,InheritableThreadLocal很好的解决了该问题。InheritableThreadLocal源码如下。

不同于ThreadLocal,在使用InheritableThreadLocal对象时,变量保存在inheritableThreadLocals中。下面是Thread类中两个变量的定义。

再来看下在线程创建时如何实现线程变量的copy过程。

线程变量的map拷贝在ThreadLocal.createInheritedMap中,实际是创建一个新的map并将值复制一份。

5.3.3 transmittable-thread-local

JDK的InheritableThreadLocal类可以完成父线程到子线程的值传递。对于线程池场景,线程由线程池创建好,并且线程是池化起来反复使用的,这时父子线程关系的ThreadLocal值传递已经没有意义,应用需要的实际上是把任务提交给线程池时的ThreadLocal值传递到任务执行时。

TransmittableThreadLocal(TTL) 是阿里巴巴开源的项目,在使用线程池等会池化复用线程的执行组件情况下,提供ThreadLocal值的传递功能,解决异步执行时上下文传递的问题。

TransmittableThreadLocal继承InheritableThreadLocal,使用方式也类似。相比InheritableThreadLocal,添加了protected的transmitteeValue()方法,用于定制任务提交给线程池时的ThreadLocal值传递到任务执行时的传递方式。

5.3.3.1 简单使用

  • 父线程给子线程传递值

这其实是InheritableThreadLocal的功能,可以使用InheritableThreadLocal来完成。

5.3.3.2 线程池中传递值

  • 修饰Runnable和Callable

上面演示了Runnable,Callable的处理类似。

  • 修饰线程池

省去每次Runnable和Callable传入线程池时的修饰,这个逻辑可以在线程池中完成。 例子如下:

  • 使用Java Agent来修饰JDK线程池实现类

相比于SDK方式,这种方式,实现线程池上下文的传递是透明的,业务代码中没有修饰Runnable或是线程池的代码,即可以做到应用代码无侵入。

使用需要在应用启动参数中增加一个premain的agent,在应用启动之前修改线程的字节码,接入方式如下:

需要注意的是,如果有多个JavaAgent,需要将transmittable的Agent参数放到其他Agent参数之前。

5.4 内存泄露

用"水能载舟亦能覆舟"来形容用ThreadLocal的是十分贴切的,笔者在实际工作中遇到非常多的ThreadLocal问题, 如内存泄露、脏数据和线程上下文丢失等,特别是线程池场景,很容易因为使用不当导致线上事故。

5.4.1 内存泄露原因

ThreadLocal内存泄一般是如下原因造成:

  • ThreadLocal变量没有被明确的移除
  • ThreadLocal变量一直存在于ThreadLocalMap中

在使用ThreadLocal时,当线程结束,如果ThreadLocal变量没有被手动清除,就会导致这部分内存无法被回收,最终导致内存泄漏。

每个线程都有一个ThreadLocalMap,这个Map可以存放多个ThreadLocal变量。当ThreadLocal变量没有被移除时,它所引用的对象也会一直存放在线程的ThreadLocalMap中, 这会导致ThreadLocalMap变得很大,从而占用大量的内存空间,最终导致内存泄漏。

5.4.2 内存泄漏的检测与清除

一般的,在线程变量使用完成之后,应该立即调用remove()完成对变量的清除,并且最好将remove()方法放在finally块, 以确保一定能被执行到。如下所示:

但是上面的方式仅适合非常简单的场景,复杂场景下如多个线程变量或者线程变量在多个地方使用等,将显得无力。下面介绍开源中间件对线程变量的检测与清理。

5.4.3 tomcat中内存泄漏的检测

在前面的章节中,分析了tomcat在卸载war包的过程,在卸载war包时调用war的类加载器WebappClassLoaderBase的stop方法完成资源的关闭与清理操作。 其中就包括检测用户创建的线程变量是否得到了清除。来看下代码:

代码来源:apache-tomcat-10.1.13-src/java/org/apache/catalina/loader/WebappClassLoaderBase.java