提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
目录
一、ThreadLocal是什么?
ThreadLocal叫做本地线程变量,在使用中填充的是当前线程的变量,该变量对其他线程都是封闭且隔离的,ThreadLoacl为变量在每个线程中创建了一个副本,这样每个线程都可以访问自己内部的副本变量
二、ThreadLocal怎么用
创建一个静态的ThreadLocal,该类中包含设置id,获取id,删除线程3个方法
public class BaseContext { public static ThreadLocal<Long> threadLocal = new ThreadLocal<>(); public static void setCurrentId(Long id) { threadLocal.set(id); } public static Long getCurrentId() { return threadLocal.get(); } public static void removeCurrentId() { threadLocal.remove(); } }
在拦截器中,我们能够获取实体类,通过将登陆id设置到ThreadLoacl上,我们能共享这个id
// 把登陆ID设置到ThreadLocal上
BaseContext.setCurrentId(empId);
@Component
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {
@Autowired
private JwtProperties jwtProperties;
/**
* 校验jwt
* 在Handler之前执行。就是Controller中标记了@XxxMapping注解的方式
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断当前拦截到的是Controller的方法还是其他资源
if (!(handler instanceof HandlerMethod)) {
//当前拦截到的不是动态方法,直接放行
return true;
}
//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getAdminTokenName());
//2、校验令牌
try {
log.info("jwt校验:{}", token);
Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
log.info("当前员工id:", empId);
//把登陆ID设置到ThreadLocal上
BaseContext.setCurrentId(empId);
//3、通过,放行
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
BaseContext.removeCurrentId();;
}
}
如下,该类就是通过(Long currentId = BaseContext.getCurrentId();) ThreadLoacl来获取当前的id,来达到用户在登陆时,进行修改密码,前端传输新密码和就密码就行
public Result<String> editPassword(PasswordEditDTO passwordEditDTO) {
String oldPassword = passwordEditDTO.getOldPassword();
String newPassword = passwordEditDTO.getNewPassword();
Long currentId = BaseContext.getCurrentId();
Employee employee = employeeMapper.findById(currentId);
oldPassword = DigestUtils.md5DigestAsHex(oldPassword.getBytes());
if (!oldPassword.equals(employee.getPassword())) {
//密码错误
return Result.error(MessageConstant.PASSWORD_ERROR);
}
employee.setPassword(DigestUtils.md5DigestAsHex(newPassword.getBytes()));
employeeMapper.update(employee);
return Result.success(MessageConstant.UPDATE_SUCCESS);
}
三、ThreadLocal源码分析
1、set
方法
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
//首先获取当前线程对象
Thread t = Thread.currentThread();
//获取线程中变量 ThreadLocal.ThreadLocalMap
ThreadLocalMap map = getMap(t);
//如果不为空,
if (map != null)
map.set(this, value);
else
//如果为空,初始化该线程对象的map变量,其中key 为当前的threadlocal 变量
createMap(t, value);
}
/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
*/
//初始化线程内部变量 threadLocals ,key 为当前 threadlocal
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
汇总下,ThreadLocalMap
为 ThreadLocal
的一个静态内部类,里面定义了Entry
来保存数据。而且是继承的弱引用。在Entry
内部使用ThreadLocal
作为key
,使用我们设置的value
作为value
。
对于每个线程内部有个ThreadLocal.ThreadLocalMap
变量,存取值的时候,也是从这个容器中来获取。
2、get
方法
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
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();
}
通过上面的分析,相信你对该方法已经有所理解了,首先获取当前线程,然后通过key threadlocal
获取 设置的value
。
四、ThreadLocal内存泄漏问题
我们首先来看下,下面这个类:
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
注释说的很清楚了,Note that null keys (i.e. entry.get()* == null)
如果 key threadlocal
为 null
了,这个 entry
就可以清除了。
ThreadLocal
是一个弱引用,当为null
时,会被当成垃圾回收 。
五、为什么key使用弱引用
如果使用强引用,当ThreadLocal
对象的引用(强引用)被回收了,ThreadLocalMap
本身依然还持有ThreadLocal
的强引用,如果没有手动删除这个key ,则ThreadLocal
不会被回收,所以只要当前线程不消亡,ThreadLocalMap
引用的那些对象就不会被回收, 可以认为这导致Entry
内存泄漏。
附:强引用-软引用-弱引用
- 强引用:普通的引用,强引用指向的对象不会被回收;
- 软引用:仅有软引用指向的对象,只有发生gc且内存不足,才会被回收;
- 弱引用:仅有弱引用指向的对象,只要发生gc就会被回收。
总结
线程map在下次调用get的时候,会清除key为null的value
使用ThreadLocal务必要在本次线程使用结束之前调用remove方法(一般是在切面的after 中进行)