ThreadLocal详解
ThreadLocal是什么?
ThreadLocal
是Java中实现线程本地变量的一个类,ThreadLocal
填充的变量是当前线程的变量,该变量对其他线程是封闭且隔离的。它为每个线程提供独立的变量副本,确保线程间的数据隔离。
即多个线程访问同一个ThreadLocal对象,但操作的却是自己独有的一份数据。
为何多线程访问同一个ThreadLocal,却可以拿到不同的数据?
每个线程(Thread
类)内部维护一个ThreadLocal.ThreadLocalMap
的实例变量threadLocals
,不同线程访问ThreadLocal
时实际操作的是内部的ThreadLocalMap
,键为ThreadLocal
对象,值为存储的数据。
GC(垃圾回收)之后,ThreadLocalMap的key是否为null?
先说结论,在GC之后,ThreadLocalMap
的key也不一定为null。
若要解释,首先需要知道Java的四种引用类型:
- 强引用:我们常常 new 出来的对象就是强引用类型,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足的时候
- 软引用:使用 SoftReference 修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收
- 弱引用:使用 WeakReference 修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收
- 虚引用:虚引用是最弱的引用,在 Java 中使用 PhantomReference 进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知
ThreadLocalMap
会持有一个ThreadLocal
的弱引用,若ThreadLocal
未再被其他引用,则发生GC后ThreadLocal
会被回收,此时Entry
的key变为null,而值被Entry
强引用依然存在,则发生内存泄漏。
若ThreadLocal
被强引用,则即使发生GC,ThreadLocal
依然不会被回收,则ThreadLocalMap
的key不为null。例如:
ThreadLocal<Object> threadLocal = new ThreadLocal<>(); // 强引用指向 ThreadLocal 实例
threadLocal.set(s);
在这种情况下,threadLocal
变量持有ThreadLocal
的强引用,ThreadLocal
不会被GC回收。
ThreadLocalMap如何产生索引和解决Hash冲突的?
既然是Map结构,ThreadLocalMap
自然也需要产生索引和解决哈希冲突。
产生索引
int i = key.threadLocalHashCode & (len-1);
关键在于threadLocalHashCode
,每创建一个ThreadLocal
实例,nextHashCode
值便会增长HASH_INCREMENT
,也就是0x61c88647
,这个值是一个斐波那契数,使用斐波那契数的好处是产生的哈希结果非常均匀。
private final int threadLocalHashCode = nextHashCode();
/**
* The next hash code to be given out. Updated atomically. Starts at
* zero.
*/
private static AtomicInteger nextHashCode =
new AtomicInteger();
/**
* The difference between successively generated hash codes - turns
* implicit sequential thread-local IDs into near-optimally spread
* multiplicative hash values for power-of-two-sized tables.
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* Returns the next hash code.
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
解决哈希冲突
即使使用斐波那契数,依然会有哈希冲突,跟HashMap
使用链表+红黑树解决哈希冲突不同,ThreadLocalMap
没有链表和红黑树,ThreadLocalMap
使用线性探测法来解决哈希冲突。ThreadLocalMap
使用set时大概分为几种情况:
一、
当计算得到的位置Entry
为null
时,直接将Entry
放置在这里,key
为ThreadLocal
,value
为要存的数据。
二、
计算得到的位置Entry
不为null
,且位置上的Entry
的key
,即ThreadLocal
的内存地址与传入的ThreadLocal
内存地址一致,则更新当前位置的数据。
三、
计算得到的位置Entry
不为null
,则往后遍历,直到遇到为null
的Entry
,或者key
相等的Entry
,且中途没有遇到存在Entry
的key
为null
,value
不为null
的情况,则将数据放在此处。
四、
//TODO
ThreadLocal简单例子
在网站项目里,用户的请求可能会跨越多层并且调用多个方法,这多层和多个方法都需要进行用户信息的传递,这时就可以使用ThreadLocal
。
可以在拦截器(Interceptor)处验证用户信息,再将用户信息加入到ThreadLocal
,这样在用户访问的整个流程中都可以通过ThreadLocal
获取用户信息。需要注意在访问结束后,在拦截器处应该删除用户信息,避免内存泄漏。
拦截器相关代码:
public class RefreshTokenInterceptor implements HandlerInterceptor {
//此处省略相关变量
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("token");
if (StrUtil.isBlank(token)) {
return true;
}
//2.基于token获取redis中的用户
Map<Object, Object> userMap = stringRedisTemplate.opsForHash()
.entries(RedisConstants.LOGIN_USER_KEY + token);
//3.判断用户是否存在
if(userMap.isEmpty()){
return true;
}
//用户存在
UserDto userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDto(), false);
//保存用户信息到ThreadLocal
UserHolder.saveUser(userDTO);
//......
return true;
}
//......
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//必须要在完成后移除用户,否则可能造成内存泄漏!
UserHolder.removeUser();
}
}
UserHolder代码:
public class UserHolder {
private static final ThreadLocal<UserDto> tl = new ThreadLocal<>();
public static void saveUser(UserDto user){
tl.set(user);
}
public static UserDto getUser(){
return tl.get();
}
public static void removeUser(){
tl.remove();
}
}
评论区