簡要聊聊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