前言
又是一个风和日立的早上,这天小美遇到了一个难题:
原来是小美在做服务鉴权的时候,需要根据每个请求获取token:
//获取认证信息
Authenticationauthentication = tokenProvider.getAuthentication(jwt);
//设置认证信息
SecurityContext.setAuthentication(authentication);
然后经过层层的调用,在业务代码里根据认证信息进行权限的判断,也就是鉴权。小美心里琢磨着,如果每个方法参数中都传递SecurityContext信息,就显的太过冗余,而且看着也丑陋。那么怎么才能隐式传递参数呢?这个当然难不倒小美,她决定用ThreadLocal来传递这个变量:
classSecurityContextHolder{
private static final ThreadLocal<SecurityContext>contextHolder = newThreadLocal<SecurityContext>();
public SecurityContextgetContext(){
SecurityContextctx = contextHolder.get();
if(ctx==null){
contextHolder.set(createEmptyContext());
}
returnctx;
}
}
......(省略不必要的)
SecurityContextHolder.getContext().setAuthentication(authentication);
整体思路上就是将SecurityContext放入ThreadLocal,这样当一个线程缘起生灭的时候,这个值会贯穿始终。
完美,小美喜滋滋的提交了代码,然后发布出去了。
结果第二天系统就出现异常了,明明是这个用户A的发起的请求,到了数据库中,却发现是操作人是用户B的信息,一时间权限大乱。
完蛋了。。。
这是为什么呢?
我们得先扯一扯ThreadLocal,Thread,ThreadLocalMap之间的爱恨情仇。
图片解说:
- Thread即为线程,图中有ThreadLocal.ThreadLocalMap,key为ThreadLocal,而value为指定的变量的值;
- ThreadLocalMap里面有一个Entry[]数组,用来存储K-V值,之所以是数组,而不是一个Entry,是因为一个线程可能对应有多个ThreadLocal;
- ThreadLocal对象在线程外生成,多线程共享一个ThreadLocal对象,生成时需指定数据类型,每个ThreadLocal对象都自定义了不同的threadLocalHashCode;
- ThreadLocal.set首先根据当前线程Thread找到对应的ThreadLocalMap,然后将ThreadLocal的threadLocalHashCode转换为ThreadLocalMap里的Entry数组下标,并存放数据于Entry[]中;
- ThreadLocal.get首先根据当前线程Thread找到对应的ThreadLocalMap,然后将ThreadLocal的threadLocalHashCode转换为ThreadLocalMap里的Entry数组下标,根据下标从Entry[]中取出对应的数据;
-
由于Thread内部的ThreadLocal.ThreadLocalMap对象是每个线程私有的,所以做到了数据独立。
于是我们知道了ThreadLocal是如何实现线程私有变量的。
但是问题来了,如果线程数很多,一直往ThreadLocalMap中存值,那内存岂不是要撑死了?
当然不是,设计者使用了弱引用来解决这个问题:
static class Entry extends WeakReference<ThreadLocal<?>>{
Object value;
Entry(ThreadLocal<?> k,Object v){
super(k);
value=v;
}
}
不过这里的弱引用只是针对key。每个key都弱引用指向ThreadLocal。当把ThreadLocal实例置为null以后,没有任何强引用指向ThreadLocal实例,所以ThreadLocal将会被GC回收。然而,value不能被回收,因为当前线程存在对value的强引用。只有当前线程结束销毁后,强引用断开,所有值才将全部被GC回收,由此可推断出,只有这个线程被回收了,ThreadLocal以及value才会真正被回收。
听起来很正常?
那如果我们使用线程池呢?常驻线程不会被销毁。这就完蛋了,ThreadLocal和value永远无法被GC回收,造成内存泄漏那是必然的。
而我们的请求进入到系统时,并不是一个请求生成一个线程,而是请求先进入到线程池,再由线程池调配出一个线程进行执行,执行完毕后放回线程池,这样就会存在一个线程多次被复用的情况,这就产生了这个线程此次操作中获取到了上次操作的值。
怎么办呢?
解决办法就是每次使用完ThreadLocal对象后,都要调用其remove方法,清除ThreadLocal中的内容。
public class ThreadLocalTest{
static ThreadLocal<AtomicInteger> sequencer = ThreadLocal.withInitial(()->newAtomicInteger(0));
static class Task implements Runnable{
@Override
public void run(){
int value = sequencer.get().getAndIncrement();
System.out.println("-------"+value);
}
}
public static void main(String[]args){
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(newTask());
executor.execute(newTask());
executor.execute(newTask());
executor.execute(newTask());
executor.execute(newTask());
executor.execute(newTask());
executor.shutdown();
}
}
输出:
0
1
0
2
3
1
这个就是错误的,正确代码如下:
public class ThreadLocalTest{
static ThreadLocal<AtomicInteger> sequencer = ThreadLocal.withInitial(()->newAtomicInteger(0));
static class Task implements Runnable{
@Override
public void run(){
int value = sequencer.get().getAndIncrement();
System.out.println("-------"+value);
sequencer.remove();
}
}
public static void main(String[]args){
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(newTask());
executor.execute(newTask());
executor.execute(newTask());
executor.execute(newTask());
executor.execute(newTask());
executor.execute(newTask());
executor.shutdown();
}
}