(ThreadLocal)
什么是ThreadLocal?
ThreadLocal 被译为==线程本地变量”类==,在 Java 的多线程并发执行过程中,为保证多个线程对变量的安 全访问,可以将变量放到ThreadLocal 类型的对象中,使变量在每个线程中都有独立值,不会出现一个 线程读取变量时而被另一个线程修改的现象。
ThreadLocal 是解决线程安全问题一个较好方案,它通过为每个线程提供一个独立的本地值,去解决并 发访问的冲突问题。很多情况下,使用 ThreadLocal 比直接使用同步机制(如 synchronized)解决线 程安全问题更简单,更方便,且结果程序拥有更高的并发性。 举例:
- ThreadLocal在Spring中作用巨大,在管理Request作用域中的Bean、事务、任务调度、AOP等模 块都有它。
- Spring中绝大部分Bean都可以声明成Singleton作用域,采用ThreadLocal进行封装,因此有状态 的Bean就能够以singleton的方式在多线程中正常工作了。
ThreadLocal 使用场景
ThreadLocal 使用场景大致可以分为以下两类:
1. 解决线程安全问题
ThreadLocal 的主要价值在于解决线程安全问题, ThreadLocal 中数据只属于当前线程,其本地值对别 的线程是不可见的,在多线程环境下,可以防止自己的变量被其他线程篡改。另外,由于各个线程之间 的数据相互隔离,==避免同步加锁带来的性能损失==,大大提升了并发性的性能。 ==典型案例:可以每个线程绑定一个数据库连接,是的这个数据库连接为线程所独享,从而避免数据库连 接被混用而导致操作异常问题。==
private static final ThreadLocal localSqlSession = new ThreadLocal();
public void startManagedSession() {
this.localSqlSession.set(openSession());
}
@Override
public Connection getConnection() {
final SqlSession sqlSession = localSqlSession.get();
if (sqlSession == null) {
throw new SqlSessionException("Error: Cannot get connection. No
managed session is started.");
}
return sqlSession.getConnection();
}
2. 跨函数传递数据
通常用于同一个线程内,跨类、跨方法传递数据时,如果不用 ThreadLocal,那么相互之间的数据传递势必要靠返回值和参数,这样无形之中增加了这些类或者方法之间的耦合度。
“跨函数传递数据”场景典型案例:可以每个线程绑定一个 Session(用户会话)信息,这样一个线程的 所有调用到的代码,都可以非常方便地访问这个本地会话,而不需要通过参数传递。
//伪代码
public class SessionHolder{
private static final ThreadLocal<UserDTO> sessionUserLocal = new
ThreadLocal<>("sessionUserLocal");
// session 线程本地变量
private static final ThreadLocal<HttpSession> sessionLocal = new
ThreadLocal<>("sessionLocal");
//...省略其他
/**
*保存 session 在线程本地变量中 */
public static void setSession(HttpSession session){
sessionLocal.set(session);
}
/**
* 取得绑定在线程本地变量中的 session
*/
public static HttpSession getSession() {
HttpSession session = sessionLocal.get();
Assert.notNull(session, "session 未设置"); return session;
}
//...省略其他
}
底层原理
ThreadLocal内部结构演进:
早期ThreadLocal为一个 Map。当工作线程 Thread 实例向本地变量保持某个值时,会以“Key-Value 对” 的形式保存在 ThreadLocal 内部的 Map 中,其中 Key为线程 Thread 实例, Value 为待保存的值。当 工作线程 Thread 实例从 ThreadLocal 本地变量取值时,会以 Thread 实例为 Key,获取其绑定的 Value。
在 JDK8 版本中, ThreadLocal 的内部结构依然是Map结构,但是其拥有者为Thread线程对象,每一个 Thread 实例拥有一个ThreadLocalMap对象。Key 为 ThreadLocal 实例。
与早期版本的 ThreadLocalMap 实现相比,新版本的主要的变化为:
- 拥有者发生了变化:新版本的 ThreadLocalMap 拥有者为 Thread,早期版本的ThreadLocalMap 拥有者为 ThreadLocal。
- Key 发生了变化:新版本的 Key 为 ThreadLocal 实例,早期版本的 Key 为 Thread 实例。
与早期版本的 ThreadLocalMap 实现相比,新版本的主要优势为:
- ThreadLocalMap 存储的“Key-Value 对”数量变少
- Thread 实例销毁后, ThreadLocalMap 也会随之销毁,在一定程度上能减少内存的消耗。
Thread、ThreadLocal、ThreadLocalMap关系
Thread-->ThreadLocalMap-->Entry(ThreadLocalN,LocalValueN)*n
Entry 的 Key 为什么需要使用弱引用?
Entry 用于保存 ThreadLocalMap 的“Key-Value”条目,但是 Entry 使用了对 Threadlocal 实例进行包装之后的弱引用(WeakReference)作为 Key,其代码如下:
// Entry 继承了 WeakReference,并使用 WeakReference 对 Key 进行包装
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value; //值
Entry(ThreadLocal<?> k, Object v) {
super(k); //使用 WeakReference 对 Key 值进行包装
value = v;
}
}
为什么 Entry 需要使用弱引用对 Key 进行包装,而不是直接使用 Threadlocal 实例作为 Key呢比如 如下代码
//伪代码
public void funcA() {
//创建一个线程本地变量
ThreadLocal local = new ThreadLocal(); //设置值
local.set(100);
//获取值
local.get();
//函数末尾
}
当线程n 执行 funcA 方法到其末尾时,线程n 相关的 JVM 栈内存以及内部 ThreadLocalMap成员的结 构,大致如图所示。
线程n 调用 funcA()方法新建了一个 ThreadLocal 实例,并使用 local 局部变量指向这个实例,并且 此 local 是强引用;
在调用 local .set(100)之后,线程n 的 ThreadLocalMap 成员内部会新建一个 Entry 实例,其 Key 以 弱引用包装的方式指向 ThreadLocal 实例。
当线程n 执行完 funcA 方法后, funcA 的方法栈帧将被销毁,强引用 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 被调用时,通过 expungeStaleEntry方法, ThreadLocalMap 的内部代码会清除这些 Key 为 null 的 Entry,从而完成相 应的内存释放。