Java ThreadLocal的使用详解
作者:KerryWu 发布时间:2023-11-29 04:48:43
目录
1. 应用场景
1.1. 保障线程安全
1.2. 显示传递参数
2. 实现原理
3. 注意事项
ThreadLocal是线程私有的局部变量存储容器,可以理解成每个线程都有自己专属的存储容器,用来存储线程私有变量。ThreadLocal 在日常开发框架中应用广泛,但用不好也会出现各种问题,本文就此讲解一下。
1. 应用场景
ThreadLocal 的常见应用场景有两种:
多线程并发场景中,用来保障线程安全。
处理较为复杂的业务时,使用ThreadLocal代替参数的显示传递。
1.1. 保障线程安全
多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性,如:synchronized、Lock之类的锁。
ThreadLocal是除了加锁这种同步方式之外的一种,规避多线程访问出现线程不安全的方法。当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量,这样就不会存在线程不安全问题。
ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。
1.2. 显示传递参数
这里举几个例子:
示例1:获取接口的当前请求用户
在后台接口业务逻辑的全过程中,如果需要在多个地方获取当前请求用户的信息。通常的一种做法就是:在接口请求时,通过过滤器、 * 、AOP等方式,从session或token中获取当前用户信息,存入ThreadLocal中。
在整个接口处理过程中,如果没有另外创建线程,都可以直接从ThreadLocal变量中获取当前用户,而无需再从Session、token中验证和获取用户。这种方案设计不仅提高性能,最重要的是将原本复杂的逻辑和代码实现,变得简洁明了。例如下面的这个例子:
(1)定义ThreadLocal变量:UserProfileThread.java
public class UserProfileThread {
private static ThreadLocal<UserProfile> USER_PROFILE_TL =new ThreadLocal<>();
public static void setUserProfile(UserProfile userProfile){
USER_PROFILE_TL.set(userProfile);
}
public static UserProfile getUserProfile() {
return USER_PROFILE_TL.get();
}
public static String getCurrentUser() {
return Optional.ofNullable(USER_PROFILE_TL.get())
.map(UserProfile::getUid)
.orElse(UserProfile.ANONYMOUS_USER);
}
}
(2)在过滤器中设置变量值:
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
UserProfile userProfile = null;
// ... 验证和获取用户信息 userProfile
UserProfileThread.setUserProfile(userProfile);
filterChain.doFilter(servletRequest, servletResponse);
}
(3)获取当前用户信息
//获取当前用户
String uid=UserProfileThread.getCurrentUser();
//获取当前用户对象
UserProfile user=UserProfileThread.getUserProfile();
示例2:spring框架中保证数据库事务在同一个连接下执行
要想实现jdbc事务, 就必须是在同一个连接对象中操作,多个连接下事务就会不可控,需要借助分布式事务完成。那spring框架如何保证数据库事务在同一个连接下执行的呢?
DataSourceTransactionManager 是spring的数据源事务管理器,它会在你调用getConnection()的时候从数据库连接池中获取一个connection, 然后将其与ThreadLocal绑定,事务完成后解除绑定。这样就保证了事务在同一连接下完成。
2. 实现原理
ThreadLocal类提供set/get方法存储和获取value值,但实际上ThreadLocal类并不存储value值,真正存储是靠ThreadLocalMap这个类。
每个线程实例都对应一个TheadLocalMap实例,我们可以在同一个线程里实例化很多个ThreadLocal来存储很多种类型的值,这些ThreadLocal实例分别作为key,对应各自的value,最终存储在Entry table数组中。
我们看看ThreadLocal的set方法:
public class ThreadLocal<T> {
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
// 省略其他方法
}
set的逻辑比较简单,就是获取当前线程的ThreadLocalMap,然后往map里添加KV,K是当前ThreadLocal实例,V是我们传入的value。这里需要注意一下,map的获取是需要从Thread类对象里面取,看一下Thread类的定义。
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
//省略其他
}
Thread类维护了一个ThreadLocalMap的变量引用。
因此,我们可以得出如下结论:
每个线程是一个Thread实例,其内部维护一个threadLocals的实例成员,其类型是ThreadLocal.ThreadLocalMap。
ThreadLocal本身并不是一个容器,我们存取的value实际上存储在ThreadLocalMap中,ThreadLocal只是作为TheadLocalMap的key。
3. 注意事项
ThreadLocal实例有提供remove()方法,用于回收对象,清除对应的内存占用。这个方法通常容易被忽略,而导致出现了各种问题。如下面几种:
线程复用:在“获取接口的当前请求用户”的例子中,Tomcat中是通过线程池来处理用户请求的,而线程池中线程是复用的。肯定会出现一个线程前后被不同用户的接口请求复用的情况,因此需要对用过的ThreaLocal变量进行覆盖或清除。
内存溢出:由于ThreadLocalMap的生命周期跟Thread一样长,如果创建的ThreadLocal变量很多,即对应的key占用的内存很大,但却没有手动删除,到了一定程度就会导致内存泄漏。
来源:https://segmentfault.com/a/1190000039998084
猜你喜欢
- 我的电脑环境win10vscode 1.36.1vscode安装插件安装完这个插件后会提示你安装 platformIOCore,按照提示安装
- Spring Data Jpa复杂查询总结只是做一个总结所以就不多说废话了实体类@Entity@Table(name = "t_h
- 面试题1:说一下抽象类和接口有哪些区别?正经回答:抽象类和接口的主要区别:从设计层面来说,抽象类是对类的抽象,是一种模板设计;接口是行为的抽
- 背景Java8的stream接口极大地减少了for循环写法的复杂性,stream提供了map/reduce/collect等一系列聚合接口,
- 大家好,在这篇文章中,我们将学习如何添加动画,同时从一个页面到其他在 Flutter。我们将覆盖不同类型的动画和实现基本动画 Flutter
- 写在前面 众所周知,kafka是现代流行的消息队列,它使用经典的消息订阅发布模式实现消息的流转,大部分代码结合kaf
- C++虚类相当于java中的抽象类,与接口的不同之处是:1.一个子类只能继承一个抽象类(虚类),但能实现多个接口2.一个抽象类可以有构造方法
- 逆时针画圆弧,原理:将360度分割成36份,分别标出每10度角度时的坐标点,然后将每个点连接起来。 #include <io
- 需求说明实际操作过程中,从D盘根目录下的ak.txt读取文件写入D盘根目录下的hello.txt文件内实现思路写两个方法,一个用于读取目标文
- C语言/C++怎样产生随机数:这里要用到的是rand()函数, srand()函数,和time()函数。需要说明的是,iostream头文件
- 在程序中封装了一个List集合对象,然后需要把该集合中的实体插入到数据库中,由于项目使用了Spring+MyBatis的配置,所以打算使用M
- 最近做一个需求,需求中的bean只用于生成一次json使用,所以想通过配置来动态的生成,查了一下,java还真有这个实现。java动态的生成
- 获取和释放 monitor 锁的时机本文我们研究下 synchronized 背后的 monitor 锁。我们都知道,最简单的同步方式就是利
- 概述 wsimport是jdk自带的命令,可以根据wsdl文档生成客户端中间代码,基于生成的代码编写客户端,可以省很多麻烦。先看两张截图:使
- 前言java处理excel转pdf一直没找到什么好用的免费jar包工具,自己手写的难度,恐怕高级程序员花费一年的事件,也不能做出来非常好用,
- 1.编写核心类MainApp:package com.yiidian.gson;import com.google.gson.Gson;im
- 概述对List进行分组是日常开发中,经常遇到的,在JDK 8中对List按照某个属性分组的代码,超级简单。package test;impo
- 前言近期有个业务需求,涉及用户付费相关的计算,需要一个日历组件,组件功能如下:仅支持从明天开始选择预定日期仅支持可选范围内的日期日期的选择是
- 1、一个示例回顾Future一些业务场景我们需要使用多线程异步执行任务,加快任务执行速度。JDK5新增了Future接口,用于描述一个异步计
- Java是面向对象的编程语言,在我们开发Java应用的程序员的专业术语里,Java这个单词其实指的是Java开发工具,也就是JDK(Java