ThreadLocal实战之踩坑笔记

简要聊聊ThreadLocal

ThreadLocal提供线程内部的局部变量,我们可以将项目中的一些变量直接存放在当前线程中,在本线程内随时随地可取,隔离其他线程,获取保存的值时非常方便。

案例集锦

案例一:

public class RequestContextHolderEx {

    private static ThreadLocal<HttpServletRequest> REQUEST_THREAD_LOCAL = new ThreadLocal<>();

    public static void clear() {

        REQUEST_THREAD_LOCAL.remove();

    }


    public static HttpServletRequest getRequest() {

        HttpServletRequest request = REQUEST_THREAD_LOCAL.get();

        if (request != null)

            return request;

        ServletRequestAttributes requestAttributes = getRequestAttributes();

        if (requestAttributes != null)

            request = requestAttributes.getRequest();

        REQUEST_THREAD_LOCAL.set(request);

        return request;

    }

}


首先这是我们封装的一个处理Request相关的支持类,这个类我们定义了一个REQUEST_THREAD_LOCAL的ThreadLocal对象,专门用来存储HttpServletRequest对象

我们看看下面的这个getRequest()方法,方法里边,第一行就是直接调用ThreadLocal的get()方法,request对象不为空的话直接返回,为空的话通过getRequestAttributes().getRequest()重新查一遍request,查出来后再调用set()方法把request对象设置进去,等到同一线程下次再调用这个getRequest()方法时,就能够直接get()出request对象返回。

上面这段代码其实很简单,get()本地线程变量中存的值,存在的话直接返回,不存在的话,重新查一遍,把对象重新set()到本地线程变量中再返回。

emm…多看了几遍,好像有什么不对劲的地方,我们为啥要用本地线程呢,在这里我不用是不是也可以?答案是:可以。

确实,这个方法你直接这么写:

    public static HttpServletRequest getRequest() {

        ServletRequestAttributes requestAttributes = getRequestAttributes();

        if (requestAttributes != null)

            request = requestAttributes.getRequest();

        return request;

    }


说实话,没毛病,还少几行代码,还不会踩我案例二要说到的坑,但是我们引入ThreadLocal是不是真的多此一举了呢?

其实不然,你仔细想想,假如说整个请求多次调用这个方法,不用ThreadLocal的话,是不是每次都需要先getRequestAttributes(),再requestAttributes.getRequest(),没发现这样很多此一举吗,还很花费时间,如果你感觉这里耗时不多的话,那想过没有假如我们要查的值涉及很多逻辑,甚至是要查数据库,这个开销就大了啊;使用ThreadLocal的话,同一个请求查询多次这个方法,线程只会第一次调用该方法时跑一遍获取逻辑,再把值存储到本地线程中,其他时候直接从本地线程中获取就行了,方便多了,耗时少了,不会再重复的跑相同逻辑的事了,所以引入本地线程是很有必要的事啊。

那ThreadLocal这么好用,是不是可以到处都用呢?其实也不是啦,很简单嘛,假如你整个请求就调用一次这个方法,那使用ThreadLocal的意义在哪呢,这才是多此一举,所以只有那些一次请求有可能使用到多次的变量才存储到ThreadLocal中,像Request、SessionInfo信息等那些一次请求可能多次访问的数据都可以存储到ThreadLocal。

会了ThreadLocal使用姿势和使用场景,是不是就可以开始上手了呢,别急,咱们先踩个坑~

案例二:

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()方法就行。

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

————————————————

版权声明:本文为CSDN博主「木兮同学」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/qq_36221788/article/details/94884591

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,657评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,662评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,143评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,732评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,837评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,036评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,126评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,868评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,315评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,641评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,773评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,859评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,584评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,676评论 2 351

推荐阅读更多精彩内容

  • 一、线程状态转换新建(New)可运行(Runnable)阻塞(Blocking)无限期等待(Waiting)限期等...
    达微阅读 570评论 1 2
  • ThreadLocal源码深入分析 ThreadLocal :线程本地存储区(Thread Local Stora...
    wewarriors阅读 324评论 0 3
  • 没意见 你想怎么样我都随便 别逼一个最爱你的人 即兴表演 默默地,馥甄的 演员 在耳边拉扯 场景像昨日重现般拉开 ...
    桔儿阅读 268评论 0 1
  • 《黄帝内经》里提出,春夏两季可以晚睡早起,秋冬两季则应该早睡晚起。古代的早睡指的是初更,晚睡是二更。古时候初更是现...
    中医范儿青年梅片阅读 240评论 0 2
  • 你把回忆放我心上 如一股清泉在流淌 炎热的夏季里 我享受到了透心凉 我把回忆打包收藏 放到不为人知的地方 在寂寞无...
    波之角落阅读 768评论 9 20