ThreadLocal實戰(zhàn)之踩坑筆記

簡要聊聊ThreadLocal

ThreadLocal提供線程內(nèi)部的局部變量,我們可以將項目中的一些變量直接存放在當前線程中顿苇,在本線程內(nèi)隨時隨地可取习蓬,隔離其他線程旺隙,獲取保存的值時非常方便。

案例集錦

案例一:

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相關(guān)的支持類扣泊,這個類我們定義了一個REQUEST_THREAD_LOCAL的ThreadLocal對象近范,專門用來存儲HttpServletRequest對象

我們看看下面的這個getRequest()方法,方法里邊延蟹,第一行就是直接調(diào)用ThreadLocal的get()方法评矩,request對象不為空的話直接返回,為空的話通過getRequestAttributes().getRequest()重新查一遍request阱飘,查出來后再調(diào)用set()方法把request對象設(shè)置進去斥杜,等到同一線程下次再調(diào)用這個getRequest()方法時,就能夠直接get()出request對象返回沥匈。

上面這段代碼其實很簡單蔗喂,get()本地線程變量中存的值,存在的話直接返回高帖,不存在的話缰儿,重新查一遍,把對象重新set()到本地線程變量中再返回散址。

emm…多看了幾遍乖阵,好像有什么不對勁的地方,我們?yōu)樯兑帽镜鼐€程呢预麸,在這里我不用是不是也可以瞪浸?答案是:可以。

確實吏祸,這個方法你直接這么寫:

? ? public static HttpServletRequest getRequest() {

? ? ? ? ServletRequestAttributes requestAttributes = getRequestAttributes();

? ? ? ? if (requestAttributes != null)

? ? ? ? ? ? request = requestAttributes.getRequest();

? ? ? ? return request;

? ? }


說實話对蒲,沒毛病,還少幾行代碼,還不會踩我案例二要說到的坑蹈矮,但是我們引入ThreadLocal是不是真的多此一舉了呢砰逻?

其實不然,你仔細想想含滴,假如說整個請求多次調(diào)用這個方法诱渤,不用ThreadLocal的話,是不是每次都需要先getRequestAttributes()谈况,再requestAttributes.getRequest(),沒發(fā)現(xiàn)這樣很多此一舉嗎递胧,還很花費時間碑韵,如果你感覺這里耗時不多的話,那想過沒有假如我們要查的值涉及很多邏輯缎脾,甚至是要查數(shù)據(jù)庫祝闻,這個開銷就大了啊遗菠;使用ThreadLocal的話联喘,同一個請求查詢多次這個方法,線程只會第一次調(diào)用該方法時跑一遍獲取邏輯辙纬,再把值存儲到本地線程中豁遭,其他時候直接從本地線程中獲取就行了,方便多了贺拣,耗時少了蓖谢,不會再重復(fù)的跑相同邏輯的事了,所以引入本地線程是很有必要的事啊譬涡。

那ThreadLocal這么好用闪幽,是不是可以到處都用呢?其實也不是啦涡匀,很簡單嘛盯腌,假如你整個請求就調(diào)用一次這個方法,那使用ThreadLocal的意義在哪呢陨瘩,這才是多此一舉腕够,所以只有那些一次請求有可能使用到多次的變量才存儲到ThreadLocal中,像Request拾酝、SessionInfo信息等那些一次請求可能多次訪問的數(shù)據(jù)都可以存儲到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底挫,先說說現(xiàn)象:

其實就是一個用戶getSessionInfo()獲取到了其他人的用戶登錄信息恒傻,導(dǎo)致出現(xiàn)了一些偶發(fā)的神奇問題,這個問題出現(xiàn)的時候真是頭疼建邓,當時自己也不是很懂ThreadLocal盈厘,只是定位到應(yīng)該是SessionInfo獲取的有問題。

先別急著往下看官边,思考一下沸手,是什么原因會導(dǎo)致獲取到了別人的SessionInfo。

這篇balabala了一堆注簿,其實上面的問題就是和ThreadLocal有關(guān)契吉,你們發(fā)現(xiàn)沒,我兩個案例都寫了一個一個我沒有提到的方法诡渴,是的捐晶,clear()方法,案例二出現(xiàn)的原因就是因為請求結(jié)束沒有remove()掉保存在本地線程中的信息妄辩。

我們來看看到底是不是因為沒有remove掉原信息

理論推斷:我們知道一個線程使用完之后并不會銷毀惑灵,而是會回到線程池進行復(fù)用,也就是說眼耀,如果你不調(diào)用remove()的話英支,保存在當前線程中的變量實例還是綁定在線程上的,當下一個用戶使用了其他用戶使用過的線程處理請求畔塔,直接get()的話潭辈,就會把原來在該線程中保存的信息給獲取出來,這就直接導(dǎo)致獲取到了別人的用戶信息澈吨,這是非常危險的把敢。

驗證:先來一段代碼

public class ThreadLocalTest {

? ? /**

? ? * 創(chuàng)建只有一個線程的線程池

? ? */

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

? ? /**

? ? * 測試用的ThreadLocal

? ? */

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

? ? @Test

? ? public void test() {

? ? ? ? //循環(huán)三次,模擬三個不同用戶的請求

? ? ? ? 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");

? ? ? ? ? ? ? ? //當前線程結(jié)束谅辣,移除本地線程中保存的信息

? ? ? ? ? ? ? ? //TEST_THREAD_LOCAL.remove();

? ? ? ? ? ? });

? ? ? ? }? ?

? ? ? ? //記得關(guān)閉線程池

? ? ? ? executor.shutdown();

? ? }

}


來看一下代碼修赞,第一行創(chuàng)建只有一個線程的線程池1,創(chuàng)建一個線程是便于測試桑阶,這里循環(huán)三次模擬了三個用戶發(fā)起的三次請求柏副,不同用戶都使用同一個線程,在不調(diào)用remove()方法的前提下蚣录,看看后面的用戶是否會get()到前面用戶保存的信息割择,結(jié)果:

模擬【第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

猜想沒錯,結(jié)果中(X)的兩行獲取到了之前用戶保存的信息萎河,我們把代碼中remove()方法打開注釋再跑一遍荔泳,看看結(jié)果:

模擬【第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

線程結(jié)束的時候remove()掉保存的信息蕉饼,現(xiàn)在的結(jié)果正確,我們前面的推斷沒有問題玛歌,整個驗證到此結(jié)束昧港。

最后來個思考題

問:我們這段驗證代碼是最后一行調(diào)用的remove()方法,那在項目中應(yīng)該什么時候調(diào)用remove()方法合理呢支子?在請求結(jié)束時创肥?好像行不通啊,請求什么時候結(jié)束值朋,線程什么時候回到線程池叹侄?而且要清理的是所有線程請求,不是某一個業(yè)務(wù)接口請求昨登,好像都沒法在請求結(jié)束時統(tǒng)一處理圈膏。

先思考一分鐘…

答:其實我們的目的是要保證,每個線程在處理請求之前是干凈的就行了篙骡,所以說只要在請求處理業(yè)務(wù)之前調(diào)用remove()接口就可以了,有什么東西能夠保證所有請求都經(jīng)過呢丈甸,過濾器糯俗,我們只要在過濾器那邊調(diào)用remove()方法就行。

注意一定要使用線程池睦擂,也就是說要保證線程處理完請求后直接回到線程池得湘,不能被銷毀 ??

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

版權(quán)聲明:本文為CSDN博主「木兮同學」的原創(chuàng)文章,遵循CC 4.0 by-sa版權(quán)協(xié)議顿仇,轉(zhuǎn)載請附上原文出處鏈接及本聲明淘正。

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市臼闻,隨后出現(xiàn)的幾起案子鸿吆,更是在濱河造成了極大的恐慌,老刑警劉巖述呐,帶你破解...
    沈念sama閱讀 217,734評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件惩淳,死亡現(xiàn)場離奇詭異,居然都是意外死亡乓搬,警方通過查閱死者的電腦和手機思犁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評論 3 394
  • 文/潘曉璐 我一進店門遂蛀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來谭羔,“玉大人,你說我怎么就攤上這事叁温〗冢” “怎么了学辱?”我有些...
    開封第一講書人閱讀 164,133評論 0 354
  • 文/不壞的土叔 我叫張陵乘瓤,是天一觀的道長。 經(jīng)常有香客問我项郊,道長馅扣,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,532評論 1 293
  • 正文 為了忘掉前任着降,我火速辦了婚禮差油,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘任洞。我一直安慰自己蓄喇,他們只是感情好,可當我...
    茶點故事閱讀 67,585評論 6 392
  • 文/花漫 我一把揭開白布交掏。 她就那樣靜靜地躺著妆偏,像睡著了一般。 火紅的嫁衣襯著肌膚如雪盅弛。 梳的紋絲不亂的頭發(fā)上钱骂,一...
    開封第一講書人閱讀 51,462評論 1 302
  • 那天,我揣著相機與錄音挪鹏,去河邊找鬼见秽。 笑死,一個胖子當著我的面吹牛讨盒,可吹牛的內(nèi)容都是我干的解取。 我是一名探鬼主播,決...
    沈念sama閱讀 40,262評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼返顺,長吁一口氣:“原來是場噩夢啊……” “哼禀苦!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起遂鹊,我...
    開封第一講書人閱讀 39,153評論 0 276
  • 序言:老撾萬榮一對情侶失蹤振乏,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后稿辙,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體昆码,經(jīng)...
    沈念sama閱讀 45,587評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,792評論 3 336
  • 正文 我和宋清朗相戀三年邻储,在試婚紗的時候發(fā)現(xiàn)自己被綠了赋咽。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,919評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡吨娜,死狀恐怖脓匿,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情宦赠,我是刑警寧澤陪毡,帶...
    沈念sama閱讀 35,635評論 5 345
  • 正文 年R本政府宣布米母,位于F島的核電站,受9級特大地震影響毡琉,放射性物質(zhì)發(fā)生泄漏铁瞒。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,237評論 3 329
  • 文/蒙蒙 一桅滋、第九天 我趴在偏房一處隱蔽的房頂上張望慧耍。 院中可真熱鬧,春花似錦丐谋、人聲如沸芍碧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽泌豆。三九已至,卻和暖如春吏饿,著一層夾襖步出監(jiān)牢的瞬間踪危,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評論 1 269
  • 我被黑心中介騙來泰國打工猪落, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留陨倡,地道東北人。 一個月前我還...
    沈念sama閱讀 48,048評論 3 370
  • 正文 我出身青樓许布,卻偏偏與公主長得像,于是被迫代替她去往敵國和親绎晃。 傳聞我的和親對象是個殘疾皇子蜜唾,可洞房花燭夜當晚...
    茶點故事閱讀 44,864評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 一、線程狀態(tài)轉(zhuǎn)換新建(New)可運行(Runnable)阻塞(Blocking)無限期等待(Waiting)限期等...
    達微閱讀 586評論 1 2
  • ThreadLocal源碼深入分析 ThreadLocal :線程本地存儲區(qū)(Thread Local Stora...
    wewarriors閱讀 326評論 0 3
  • 沒意見 你想怎么樣我都隨便 別逼一個最愛你的人 即興表演 默默地庶艾,馥甄的 演員 在耳邊拉扯 場景像昨日重現(xiàn)般拉開 ...
    桔兒閱讀 270評論 0 1
  • 《黃帝內(nèi)經(jīng)》里提出袁余,春夏兩季可以晚睡早起,秋冬兩季則應(yīng)該早睡晚起咱揍。古代的早睡指的是初更颖榜,晚睡是二更。古時候初更是現(xiàn)...
    中醫(yī)范兒青年梅片閱讀 250評論 0 2
  • 你把回憶放我心上 如一股清泉在流淌 炎熱的夏季里 我享受到了透心涼 我把回憶打包收藏 放到不為人知的地方 在寂寞無...
    波之角落閱讀 770評論 9 20