Java ThreadLocal 攻略

558 阅读8分钟

攻略大全

1. 粘贴攻略

在Java的多线程并发执行过程中,为了保证多个线程对变量的安全访问,可以将变量放到ThreadLocal类型的对象中,使变量在每个线程中都有独立值,不会出现一个线程读取变量时被另一个线程修改的现象。ThreadLocal类通常被翻译为“线程本地变量”类或者“线程局部变量”类。

1.1 ThreadLocal的基本使用

ThreadLocal位于JDK的java.lang核心包中。如果程序创建了一个ThreadLocal实例,那么在访问这个变量的值时,每个线程都会拥有一个独立的、自己的本地值。“线程本地变量”可以看成专属于线程的变量,不受其他线程干扰,保存着线程的专属数据。当线程结束后,每个线程所拥有的那个本地值会被释放。在多线程并发操作“线程本地变量”的时候,线程各自操作的是自己的本地值,从而规避了线程安全问题。

ThreadLocal如何做到为每个线程存有一份独立的本地值呢?一个ThreadLocal实例可以形象地理解为一个Map(早期版本的ThreadLocal是这样设计的)。当工作线程Thread实例向本地变量保持某个值时,会以“Key-Value对”(即键-值对)的形式保存在ThreadLocal内部的Map中,其中Key为线程Thread实例,Value为待保存的值。当工作线程Thread实例从ThreadLocal本地变量取值时,会以Thread实例为Key,获取其绑定的Value。

Image [41].png

Image [42].png

1.2 ThreadLocal的使用场景

ThreadLocal的使用场景大致可以分为以下两类:

  • (1)线程隔离

    ThreadLocal的主要价值在于线程隔离,ThreadLocal中的数据只属于当前线程,其本地值对别的线程是不可见的,在多线程环境下,可以防止自己的变量被其他线程篡改。另外,由于各个线程之间的数据相互隔离,避免了同步加锁带来的性能损失,大大提升了并发性的性能。

  • (2)跨函数传递数据

    通常用于同一个线程内,跨类、跨方法传递数据时,如果不用ThreadLocal,那么相互之间的数据传递势必要靠返回值和参数,这样无形之中增加了这些类或者方法之间的耦合度。 由于ThreadLocal的特性,同一线程在某些地方进行设置,在随后的任意地方都可以获取到。线程执行过程中所执行到的函数都能读写ThreadLocal变量的线程本地值,从而可以方便地实现跨函数的数据传递。使用ThreadLocal保存函数之间需要传递的数据,在需要的地方直接获取,也能避免通过参数传递数据带来的高耦合。

1.3 使用ThreadLocal进行线程隔离

在“线程隔离”场景中,使用ThreadLocal的典型案例为:可以为每个线程绑定一个数据库连接,使得这个数据库连接为线程所独享,从而避免数据库连接被混用而导致操作异常问题。

1.4 使用ThreadLocal进行跨函数数据传递

ThreadLocal在“跨函数数据传递”场景的典型应用有很多:

  • (1)用来传递请求过程中的用户ID。
  • (2)用来传递请求过程中的用户会话(Session)。
  • (3)用来传递HTTP的用户请求实例HttpRequest。
  • (4)其他需要在函数之间频繁传递的数据。

1.5 ThreadLocal内部结构演进

在早期的JDK版本中,ThreadLocal的内部结构是一个Map,其中每一个线程实例作为Key,线程在“线程本地变量”中绑定的值为Value(本地值)。早期版本中的Map结构,其拥有者为ThreadLocal,每一个ThreadLocal实例拥有一个Map实例。

在JDK 8版本中,ThreadLocal的内部结构发生了演进,虽然还是使用了Map结构,但是Map结构的拥有者已经发生了变化,其拥有者为Thread(线程)实例,每一个Thread实例拥有一个Map实例。另外,Map结构的Key值也发生了变化:新的Key为ThreadLocal实例。 在JDK 8版本中,每一个Thread线程内部都有一个Map(ThreadLocalMap),如果给一个Thread创建多个ThreadLocal实例,然后放置本地数据,那么当前线程的ThreadLocalMap中就会有多个“Key-Value对”,其中ThreadLocal实例为Key,本地数据为Value。 从代码的层面来说,新版本的ThreadLocalMap还是由ThreadLocal类维护的,由ThreadLocal负责ThreadLocalMap实例的获取和创建,并从中设置本地值、获取本地值。所以ThreadLocalMap还寄存于ThreadLocal内部,并没有被迁移到Thread内部。

Image [43].png

Image [44].png

与早期版本的ThreadLocalMap实现相比,新版本的主要变化为:

(1)拥有者发生了变化:新版本的ThreadLocalMap拥有者为Thread,早期版本的ThreadLocalMap拥有者为ThreadLocal。

(2)Key发生了变化:新版本的Key为ThreadLocal实例,早期版本的Key为Thread实例。

与早期版本的ThreadLocalMap实现相比,新版本的主要优势为:

(1)每个ThreadLocalMap存储的“Key-Value对”数量变少。早期版本的“Key-Value对”数量与线程个数强关联,若线程数量多,则ThreadLocalMap存储的“Key-Value对”数量也多。新版本的ThreadLocalMap的Key为ThreadLocal实例,多线程情况下ThreadLocal实例比线程数少。

(2)早期版本ThreadLocalMap的拥有者为ThreadLocal,在Thread(线程)实例销毁后,ThreadLocalMap还是存在的;新版本的ThreadLocalMap的拥有者为Thread,现在当Thread实例销毁后,ThreadLocalMap也会随之销毁,在一定程度上能减少内存的消耗。

1.6 ThreadLocal源码分析

Image [45].png

Image [46].png

Image [47].png

Image [48].png

Image [49].png

Image [50].png

1.7 ThreadLocalMap源码分析

1.ThreadLocalMap的主要成员变量

Image [51].png

2.Entry的Key需要使用弱引用

当线程执行完方法后,方法栈帧将被销毁,强引用local的值也就没有了,但此时线程的ThreadLocalMap中对应的Entry的Key引用还指向ThreadLocal实例。如果Entry的Key引用是强引用,就会导致Key引用指向的ThreadLocal实例及其Value值都不能被GC回收,这将造成严重的内存泄漏问题。

由于ThreadLocalMap中Entry的Key使用了弱引用,在下次GC发生时,就可以使那些没有被其他强引用指向、仅被Entry的Key所指向的ThreadLocal实例能被顺利回收。并且,在Entry的Key引用被回收之后,其Entry的Key值变为null。后续当ThreadLocal的get()、set()或remove()被调用时,ThreadLocalMap的内部代码会清除这些Key为null的Entry,从而完成相应的内存释放。

3.编程规范推荐使用static final修饰ThreadLocal对象

编程规范有云:ThreadLocal实例作为ThreadLocalMap的Key,针对一个线程内的所有操作是共享的,所以建议设置static修饰符,以便被所有的对象共享。由于静态变量会在类第一次被使用时装载,只会分配一次存储空间,此类的所有实例都会共享这个存储空间,所以使用static修饰ThreadLocal就会节约内存空间。另外,为了确保ThreadLocal实例的唯一性,除了使用static修饰之外,还会使用final进行加强修饰,以防止其在使用过程中发生动态变更。

除了添加static final修饰外,还常常添加private修饰,主要目的是缩小使用的范围,尽可能不让他人引用。

凡事都有两面性,使用static、final修饰ThreadLocal实例也会带来副作用,使得Thread实例内部的ThreadLocalMap中Entry的Key在Thread实例的生命期内将始终保持为非null,从而导致Key所在的Entry不会被自动清空,这就会让Entry中的Value指向的对象一直存在强引用,于是Value指向的对象在线程生命期内不会被释放,最终导致内存泄漏。所以,在使用完static、final修饰的ThreadLocal实例之后,必须调用remove()来进行显式的释放操作。

1.8 ThreadLocal综合使用案例

使用ThreadLocal时遵守以下两个原则:

(1)尽量使用private static final修饰ThreadLocal实例。使用private与final修饰符主要是为了尽可能不让他人修改、变更ThreadLocal变量的引用,使用static修饰符主要是为了确保ThreadLocal实例的全局唯一。

(2)ThreadLocal使用完成之后务必调用remove()方法。这是简单、有效地避免ThreadLocal引发内存泄漏问题的方法。

总之,使用ThreadLocal能实现每个线程都有一份变量的本地值,其原因是每个线程都有自己独立的ThreadLocalMap空间,本质上属于以空间换时间的设计思路,该设计思路属于另一种意义的“无锁编程”。

2. 造火箭攻略

2.1 ThreadLocal与同步机制的区别

944365-2a901aa6887eeac8.png

3. 拧螺丝攻略

4. 复制攻略

4.1 《Java高并发核心编程(卷2)》