美文网首页
ThreadLocal的get的安全问题

ThreadLocal的get的安全问题

作者: 舒尔诚 | 来源:发表于2020-05-27 12:57 被阅读0次

    案例二:

    public class SessionContext {

    private static final ThreadLocal<SessionInfo> SESSION_INFO_THREAD_LOCAL = new ThreadLocal<>();

    public void clear() {

    SESSION_INFO_THREAD_LOCAL.remove();

    }

    public SessionInfo getSessionInfo() {

    SessionInfo si = SESSION_INFO_THREAD_LOCAL.get();

    if (null != si) {

    return si;

    }

    //balabala...,省略一堆获取SessionInfo的逻辑

    SESSION_INFO_THREAD_LOCAL.set(si);

    return si;

    }

    }

    这个代码和案例一的代码基本一样,只不过案例二存储的是用户的登录信息SessionInfo,先说说现象:

    其实就是一个用户getSessionInfo()获取到了其他人的用户登录信息,导致出现了一些偶发的神奇问题,这个问题出现的时候真是头疼,当时自己也不是很懂ThreadLocal,只是定位到应该是SessionInfo获取的有问题。

    先别急着往下看,思考一下,是什么原因会导致获取到了别人的SessionInfo。

    这篇balabala了一堆,其实上面的问题就是和ThreadLocal有关,你们发现没,我两个案例都写了一个一个我没有提到的方法,是的,clear()方法,案例二出现的原因就是因为请求结束没有remove()掉保存在本地线程中的信息。

    我们来看看到底是不是因为没有remove掉原信息

    理论推断:我们知道一个线程使用完之后并不会销毁,而是会回到线程池进行复用,也就是说,如果你不调用remove()的话,保存在当前线程中的变量实例还是绑定在线程上的,当下一个用户使用了其他用户使用过的线程处理请求,直接get()的话,就会把原来在该线程中保存的信息给获取出来,这就直接导致获取到了别人的用户信息,这是非常危险的。

    验证:先来一段代码

    public class ThreadLocalTest {

    /**

    • 创建只有一个线程的线程池

    */

    private static ExecutorService executor = Executors.newFixedThreadPool(1);

    /**

    • 测试用的ThreadLocal

    */

    private static ThreadLocal<String> TEST_THREAD_LOCAL = new ThreadLocal<>();

    @Test

    public void test() {

    //循环三次,模拟三个不同用户的请求

    for (int i = 1; i <= 3; i++) {

    int finalI = i;

    executor.execute(() -> {

    System.out.println("模拟【第" + finalI + "个】用户请求");

    //先get()一遍本地线程中的信息

    System.out.println("线程【" + Thread.currentThread().getName() + "】的ThreadLocal保存的信息:" + TEST_THREAD_LOCAL.get());

    //重新set()用户信息

    TEST_THREAD_LOCAL.set("用户" + finalI + "的信息");

    //再get()一次用户信息

    System.out.println("线程【" + Thread.currentThread().getName() + "】的ThreadLocal保存的信息:" + TEST_THREAD_LOCAL.get() + "\n");

    //当前线程结束,移除本地线程中保存的信息

    //TEST_THREAD_LOCAL.remove();

    });

    }

    //记得关闭线程池

    executor.shutdown();

    }

    }

    来看一下代码,第一行创建只有一个线程的线程池1,创建一个线程是便于测试,这里循环三次模拟了三个用户发起的三次请求,不同用户都使用同一个线程,在不调用remove()方法的前提下,看看后面的用户是否会get()到前面用户保存的信息,结果:

    模拟【第1个】用户请求

    线程【pool-1-thread-1】的ThreadLocal保存的信息:null

    线程【pool-1-thread-1】的ThreadLocal保存的信息:用户1的信息

    模拟【第2个】用户请求

    线程【pool-1-thread-1】的ThreadLocal保存的信息:用户1的信息(X)

    线程【pool-1-thread-1】的ThreadLocal保存的信息:用户2的信息

    模拟【第3个】用户请求

    线程【pool-1-thread-1】的ThreadLocal保存的信息:用户2的信息(X)

    线程【pool-1-thread-1】的ThreadLocal保存的信息:用户3的信息

    1234567891011

    猜想没错,结果中(X)的两行获取到了之前用户保存的信息,我们把代码中remove()方法打开注释再跑一遍,看看结果:

    模拟【第1个】用户请求

    线程【pool-1-thread-1】的ThreadLocal保存的信息:null

    线程【pool-1-thread-1】的ThreadLocal保存的信息:用户1的信息

    模拟【第2个】用户请求

    线程【pool-1-thread-1】的ThreadLocal保存的信息:null

    线程【pool-1-thread-1】的ThreadLocal保存的信息:用户2的信息

    模拟【第3个】用户请求

    线程【pool-1-thread-1】的ThreadLocal保存的信息:null

    线程【pool-1-thread-1】的ThreadLocal保存的信息:用户3的信息

    1234567891011

    线程结束的时候remove()掉保存的信息,现在的结果正确,我们前面的推断没有问题,整个验证到此结束。

    最后来个思考题

    问:我们这段验证代码是最后一行调用的remove()方法,那在项目中应该什么时候调用remove()方法合理呢?在请求结束时?好像行不通啊,请求什么时候结束,线程什么时候回到线程池?而且要清理的是所有线程请求,不是某一个业务接口请求,好像都没法在请求结束时统一处理。

    先思考一分钟…

    答:其实我们的目的是要保证,每个线程在处理请求之前是干净的就行了,所以说只要在请求处理业务之前调用remove()接口就可以了,有什么东西能够保证所有请求都经过呢,过滤器,我们只要在过滤器那边调用remove()方法就行。

    注意一定要使用线程池,也就是说要保证线程处理完请求后直接回到线程池,不能被销毁

    相关文章

      网友评论

          本文标题:ThreadLocal的get的安全问题

          本文链接:https://www.haomeiwen.com/subject/bplwahtx.html