ThreadLocal和线程上下文
经过学习和理解,总结了以下经验:
真正的主角是线程上下文,而不是ThreadLocal
ThreadLocal的本质是实现Java线程私有map变量的一个工具类,所以,提供的最核心的方法就是set和get
如果引入线程上下文的概念,那么,ThreadLocal就是实现线程上下文功能的工具类
摘要
本文主要的目的是实现ThreadLocal的入门,基于自己的理解给出一个比较容易懂的ThreadLocal定义,并且结合线程上下文的概念,给出ThreadLocal的定位。本文主要包括以下内容:
- ThreadLocal的简单定义和理解
- 结合线程上下文的概念,总结了ThreadLocal的2大特性,特性决定了其用途
- 简单介绍了ThreadLocal在开发中的常见使用场景,分别使用了其私有特性和共享特性
入门
定义
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方法可以对变量进行操作,每个线程之间的Thread-Local变量是相互独立的。一般来说,ThreadLocal都是私有静态属性,用来存储一些需要和线程绑定的数据,比如事务ID,用户ID。
Each thread holds an implicit reference to its copy of a thread-local
variable as long as the thread is alive and the {@code ThreadLocal}
instance is accessible; after a thread goes away, all of its copies of
thread-local instances are subject to garbage collection (unless other
references to these copies exist).
每个线程持有对ThreadLocal变量的引用,只要线程存在,这个变量就一直存在。如果线程对象被回收,则Thread-Local 变量在没有别的引用的情况下也会被回收。
注意,ThreadLocal≠thread-local变量,在后面的描述中可以发现,ThreadLocal和thread-local变量是key-value的关系,共同存在Thread维护的map中。
小结:
- java实现线程私有变量的逻辑非常简单,就是在每个线程中维护一个map,所以,这个map自然就是线程私有的,每个线程只会访问自己的map。
- ThreadLocal相当于一个工具类,用来实现这个私有map的功能。
- 所以,给ThreadLocal的定位,就是实现Thread私有map的工具类。
所以,从本质上讲,就要把ThreadLocal理解成一个工具类,它并不存储数据,只是提供了让线程存储私有数据的方法。
为什么ThreadLocal难以理解?
根据上面的分析,在理解ThreadLocal时最容易犯的错误就是:
- 以为数据是存在ThreadLocal中
- 没有找准ThreadLocal的定位,没有梳理清楚ThreadLocal和Thread之间的关系
所以,要理解ThreadLocal,首先要下一个自己能够理解的定义,目前来说,我对于ThreadLocal的定义就是:
ThreadLocal就是一个工具类,为Thread对象提供了私有变量的操作方法
基于这个定义,就可以从工具类的角度去理解,而不需要关心各种key-value关系,理解起来还比较顺畅。
示例代码
使用ThreadLocal编程的思路和一般使用工具类没有太大的区别
- 首先定义N个ThreadLocal对象,需要多少个私有变量,就定义多少个。
- 然后就是调用ThreadLocal对象提供的工具方法,在每个线程的代码中进行数据的存和取即可。
在编程的过程中,重点不要放在ThreadLocal上,而应该放在它提供的set和get方法以及线程的私有变量逻辑上
public class ThreadLocalDemo {
public static final ThreadLocal<String> tl = new ThreadLocal<String>();
public static void main(String[] args) {
//创建一个线程,操作私有变量
Thread t1 = new Thread(() -> {
tl.set("线程1私有变量");
System.out.println(Thread.currentThread() + "---" + tl.get());
});
t1.start();
//set方法在哪里执行,私有变量就存到哪个线程对象中
tl.set("主线程私有变量");
System.out.println(Thread.currentThread() + "---" + tl.get());
}
}
ThreadLocal和线程上下文的特性
真正有价值的是线程上下文,ThreadLocal只是实现线程上下文的一种方式而已
- 线程上下文共享特性:对于同一个线程的所有方法,能够用线程上下文共享变量,不需要进行参数传递。这个特性特别适用于token、session、httpHeader等参数
- 线程私有特性:避免上下文变量不一致或者多个线程共享变量
使用场景
特性决定用途,共享特性和私有特性支持了线程上下文用于事务、全局session等场景。
在事务场景中使用ThreadLocal
在事务场景中使用,利用的是线程私有的特性
如果连接池,在事务的场景中,在Service层需要开启事务和关闭事务,在DAO层需要基于数据库连接操作数据。在Service和DAO中,必须使用同一个连接,不然就无法控制事务。
如果不加任何控制,在Service层和DAO层获取到的连接对象可能都不是同一个,所以,在单次业务流程中,是一个线程执行的,每个线程必须独享一个连接,直到业务结束。
因此,在获取连接的方法中,需要将连接放到本线程的私有map中,使用ThreadLocal刚好可以实现。
注意:
- 在Service和DAO的代码中不需要关心ThreadLocal,只需要在获取连接的方法中添加ThreadLocal的逻辑即可。
- Service、DAO、获取连接的方法,必须是串行的,不能使用异步,否则就不是同一个线程了
在业务代码中传递Session/Token等共享信息
在session和token场景中使用ThreadLocal使用的是共享特性,由于所有的方法中都有可能要用session,所以使用上下文共享特性,可以简化代码,这里不是为了私有,而是为了共享。
大致流程如下:
- 在SessionContext中,首先使用set操作把用户信息放到当前线程私有map中
- 然后在所有的方法中,就可以直接取出用户信息,不需要手动传递参数,简化了代码
ThreadLocal内存泄漏问题
简单原理分析
- 在ThreadLocal使用场景中,一共有3种对象,分别是Thread,ThreadLocalMap,ThreadLocal,相互的关系是:Thread中维护一个ThreadLocalMap,ThreadLocalMap的key就是ThreadLocal。
- 由于我们大部分场景是线程池,从而,map的生命周期也会很长,进而map中的key和value就一直会被某个entry引用。
- java的设计中,key到ThreadLocal的引用是弱引用,当ThreadLocal弹出栈之后,ThreadLocal对象就会被回收,所以,key不会导致内存泄漏。
- 但是entry的value到存储的变量之间是强引用,如果不删除entry,则一直会有entry指向存储的obj,这个obj就会导致内存泄漏。从设计上,这个obj的生命周期必须和ThreadLocal相同,但是如果不手动清理,就会一直存在,导致内存泄漏
示例图如下:参考链接
小结:
- 内存泄漏的根本原因在于线程的生命周期是很长的,而value是存在线程对象的map中的,所以value的生命周期默认也很长了。
ThreadLocal使用完一定要手动删除变量
- 由于可能存在内存泄漏问题,所以当ThreadLocal失效后,必须同步remove thread-local私有变量。