当前位置:网站首页>ThreadLocal原理与使用
ThreadLocal原理与使用
2022-07-04 17:05:00 【什么是快乐】
ThreadLocal原理与使用
最近看到公司的系统管理组件中获取人员信息的一段代码:
//获取登录用户信息
UserLoginInfo userLoginInfo = ThreadLocalUtils.getUserLoginInfo();
getUserLoginInfo()方法本以为是通过token解析或者是通过token查redis。但是点进去却发现是从ThreadLocal中获取登陆信息?
private static final ThreadLocal<Map<String, Object>> local = ThreadLocal.withInitial(HashMap::new);
public static UserLoginInfo getUserLoginInfo() {
return get(Constants.USER_LOGIN_INFO);
}
/** * 从ThreadLocal中获取值 * * @param key 键 * @param <T> 值泛型 * @return 值, 不存在则返回null, 如果类型与泛型不一致, 可能抛出{@link ClassCastException} * @see Map#get(Object) * @see ClassCastException */
public static <T> T get(String key) {
return ((T) local.get().get(key));
}
一番百度学习后发现Threadlocal是线程间做数据隔离的。这时我又想起之前学习的volatile保证数据变量的可见性。数据为什么又隔离又可见。区别是什么?不禁混乱,下面是个人的梳理
volatile
volatile是Java的一个关键字,一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰后,就具备了两层语义:保证了不同线程对这个变量进行操作时的可见性和禁止了指令重排序。
Java中变量是存放在主内存中,线程并不能直接读取和操作主内存的变量,而是会将变量读到工作内存中进行操作。而volatile的作用是保证所有线程能第一时间获取变量的最新值。但是要注意的是,对volatile修饰的变量的更改操作并不是原子性的。因为更改操作包括读变量,修改变量和写入工作内存。而volatile只是保证 了读变量是读的最新值。所以,volatile无法保证对变量的任何操作都是原子性的。
ThreadLocal
ThreadLocal会为每个使用该变量的线程提供独立的变量副本,保证各个线程之间的隔离。探究底层,ThreadLocal是通过静态内部类ThreadLocalMap实现的。
下面是ThreadLocal的类图结构,从图中可知:Thread类中有两个变量threadLocals和inheritableThreadLocals,二者都是ThreadLocal内部类ThreadLocalMap类型的变量,我们通过查看内部内ThreadLocalMap可以发现实际上它类似于一个HashMap

和我所想的不同的是,每个线程的本地变量不是存放在ThreadLocal实例中,而是放在调用线程的ThreadLocals变量里面(前面也说过,该变量是Thread类的变量)。也就是说,ThreadLocal类型的本地变量是存放在具体的线程空间上,其本身相当于一个装载本地变量的工具壳,通过set方法将value添加到调用线程的threadLocals中,当调用线程调用get方法时候能够从它的threadLocals中取出变量。
set方法
public void set(T value) {
//(1)获取当前线程(调用者线程)
Thread t = Thread.currentThread();
//(2)以当前线程作为key值,去查找对应的线程变量,找到对应的map
ThreadLocalMap map = getMap(t);
//(3)如果map不为null,就直接添加本地变量,key为当前定义的ThreadLocal变量的this引用,值为添加的本地变量值
if (map != null)
map.set(this, value);
//(4)如果map为null,说明首次添加,需要首先创建出对应的map
else
createMap(t, value);
}
在上面的代码中,(2)处调用getMap方法获得当前线程对应的threadLocals(参照上面的图示和文字说明),该方法代码如下
ThreadLocalMap getMap(Thread t) {
return t.threadLocals; //获取线程自己的变量threadLocals,并绑定到当前调用线程的成员变量threadLocals上
}
如果调用getMap方法返回值不为null,就直接将value值设置到threadLocals中(key为当前线程引用,值为本地变量);如果getMap方法返回null说明是第一次调用set方法(前面说到过,threadLocals默认值为null,只有调用set方法的时候才会创建map),这个时候就需要调用createMap方法创建threadLocals,该方法如下所示
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
createMap方法不仅创建了threadLocals,同时也将要添加的本地变量值添加到了threadLocals中。
get方法
在get方法的实现中,首先获取当前调用者线程,如果当前线程的threadLocals不为null,就直接返回当前线程绑定的本地变量值,否则执行setInitialValue方法初始化threadLocals变量。在setInitialValue方法中,类似于set方法的实现,都是判断当前线程的threadLocals变量是否为null,是则添加本地变量(这个时候由于是初始化,所以添加的值为null),否则创建threadLocals变量,同样添加的值为null
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
threadLocal 和threadLocalMap的关系如下图所示。

threadlocal内存泄漏问题
上图我们可以看到,threadLocal最终还是调用的threadlocalMap类的方法,而threadlocalMap的内部其实是一个Entry数组
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
当前ThreadLocal的引用k被传递给WeakReference的构造函数,所以ThreadLocalMap中的key是ThreadLocal的弱引用。如果 key threadlocal 为 null 了,这个 entry就清除了。
当threadlocal为null或者被垃圾回收后,但ThreadLocalMap(threadlocals)是线程的内部属性,它的生命周期与所属线程同步。并不会被垃圾回收。所以此时ThreadLocalMap(threadlocals)就会出现一个现象:key为空,但是value值还在。这就是内存泄漏问题。
避免方法
在调用ThreadLocal最后一定要调用remove()方法。
接着文章开头的从threadLocal中获取用户登陆信息。发现项目中有一个拦截器,每次请求都会将token中获得的用户信息存入threadLocal中。这样后面的service层代码就可以直接通过threadlocal获取,而不需要通过参数传递。
/** * @author LSWXXY-WQ */
@Slf4j
public class UserInterceptor extends HandlerInterceptorAdapter {
@Autowired
UserService userService;
@Autowired
MenuService menuService;
@Resource
private AuthorizationService authorizationService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取token
String token = request.getHeader(Constants.USER_TOKEN_HEADER);
//通过token获取用户信息并放入ThreadLocal中
if (StringUtil.isBlank(token) ) {
UserloginInfo userloginInfo = userService.getLoginInfo(token)
ThreadLocalUtils.setUserLoginInfo(userloginInfo);
}
//后续一系列校验等等
.....
.....
.....
.....
return super.preHandle(request, response, handler);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//业务结束后清空threadlocal
ThreadLocalUtils.clear();
super.afterCompletion(request, response, handler, ex);
}
最后附上项目的threadLocalUtils吧。核心还是对ThreadLocal的get、set的使用
public final class ThreadLocalUtils {
private ThreadLocalUtils() {
}
private static final ThreadLocal<Map<String, Object>> local = ThreadLocal.withInitial(HashMap::new);
/** * @return threadLocal中的全部值 */
public static Map<String, Object> getAll() {
return local.get();
}
/** * 设置一个值到ThreadLocal * * @param key 键 * @param value 值 * @param <T> 值的类型 * @return 被放入的值 * @see Map#put(Object, Object) */
public static <T> T put(String key, T value) {
local.get().put(key, value);
return value;
}
/** * 删除参数对应的值 * * @param key * @see Map#remove(Object) */
public static void remove(String key) {
local.get().remove(key);
}
/** * 清空ThreadLocal * * @see Map#clear() */
public static void clear() {
local.remove();
}
/** * 从ThreadLocal中获取值 * * @param key 键 * @param <T> 值泛型 * @return 值, 不存在则返回null, 如果类型与泛型不一致, 可能抛出{@link ClassCastException} * @see Map#get(Object) * @see ClassCastException */
public static <T> T get(String key) {
return ((T) local.get().get(key));
}
/** * 从ThreadLocal中获取值,并指定一个当值不存在的提供者 * * @see Supplier * @since 3.0 */
public static <T> T get(String key, Supplier<T> supplierOnNull) {
return ((T) local.get().computeIfAbsent(key, k -> supplierOnNull.get()));
}
/** * 获取一个值后然后删除掉 * * @param key 键 * @param <T> 值类型 * @return 值, 不存在则返回null * @see this#get(String) * @see this#remove(String) */
public static <T> T getAndRemove(String key) {
try {
return get(key);
} finally {
remove(key);
}
}
public static void setUserLoginInfo(UserLoginInfo loginInfo) {
put(Constants.USER_LOGIN_INFO, loginInfo);
}
public static UserLoginInfo getUserLoginInfo() {
return get(Constants.USER_LOGIN_INFO);
}
public static void setAppLoginInfo(AppLoginInfo loginInfo) {
put(Constants.APP_LOGIN_INFO, loginInfo);
}
public static AppLoginInfo getAppLoginInfo() {
return get(Constants.APP_LOGIN_INFO);
}
public static void setAppName(String appName) {
put(Constants.APP_NAME, appName);
}
public static String getAppName() {
return get(Constants.APP_NAME);
}
public static void setSysLog(SysLog sysLog){
put(Constants.OPERATE_LOG,sysLog);
}
public static SysLog getSysLog(){
return get(Constants.OPERATE_LOG);
}
}
边栏推荐
- Nature Microbiology | 可感染阿斯加德古菌的六种深海沉积物中的病毒基因组
- Self reflection of a small VC after two years of entrepreneurship
- With the stock price plummeting and the market value shrinking, Naixue launched a virtual stock, which was deeply in dispute
- 【Hot100】32. Longest valid bracket
- [2022 Jiangxi graduate mathematical modeling] curling movement idea analysis and code implementation
- DB-Engines 2022年7月数据库排行榜:Microsoft SQL Server 大涨,Oracle 大跌
- Load test practice of pingcode performance test
- 同事悄悄告诉我,飞书通知还能这样玩
- Summary of subsidy policies across the country for dcmm certification in 2022
- Stars open stores, return, return, return
猜你喜欢

Open source PostgreSQL extension age for graph database was announced as the top-level project of Apache Software Foundation

力扣刷题日记/day7/6.30

Halcon template matching
![[cloud native] what is the](/img/00/0cb0f38bf3eb5dad02b3bc4ead36ba.jpg)
[cloud native] what is the "grid" of service grid?

Machine learning concept drift detection method (Apria)
![[go language question brushing chapter] go conclusion chapter | introduction to functions, structures, interfaces, and errors](/img/7a/16b481753d7d57f50dc8787eec8a1a.png)
[go language question brushing chapter] go conclusion chapter | introduction to functions, structures, interfaces, and errors

【Go语言刷题篇】Go完结篇|函数、结构体、接口、错误入门学习

线上MySQL的自增id用尽怎么办?

ISO27001认证办理流程及2022年补贴政策汇总

vbs或vbe如何修改图标
随机推荐
Li Kou brush question diary /day6/6.28
比李嘉诚还有钱的币圈大佬,刚在沙特买了楼
线上MySQL的自增id用尽怎么办?
mysql5.7安装教程图文详解
【Go语言刷题篇】Go完结篇|函数、结构体、接口、错误入门学习
李迟2022年6月工作生活总结
力扣刷題日記/day6/6.28
ISO27001认证办理流程及2022年补贴政策汇总
NBA赛事直播超清画质背后:阿里云视频云「窄带高清2.0」技术深度解读
Neglected problem: test environment configuration management
Scala基础教程--12--读写数据
Li Kou brush question diary /day7/6.30
一、C语言入门基础
【210】PHP 定界符的用法
项目通用环境使用说明
中国农科院基因组所汪鸿儒课题组诚邀加入
Li Kou brush question diary /day4/6.26
Scala基础教程--15--递归
机器学习概念漂移检测方法(Aporia)
用于图数据库的开源 PostgreSQL 扩展 AGE被宣布为 Apache 软件基金会顶级项目