版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進行舉報或認領(lǐng)
文檔簡介
第利用ThreadLocal實現(xiàn)一個上下文管理組件目錄1ThreadLocal原理set()方法get()方法withInitial()方法ThreadLocal中的內(nèi)存泄漏問題2自定義上下文Scope3在線程池中傳遞Scope4通過Filter、Scope實現(xiàn)Request上下文5總結(jié)源代碼本文基于ThreadLocal原理,實現(xiàn)了一個上下文狀態(tài)管理組件Scope,通過開啟一個自定義的Scope,在Scope范圍內(nèi),可以通過Scope各個方法讀寫數(shù)據(jù);
通過自定義線程池實現(xiàn)上下文狀態(tài)數(shù)據(jù)的線程間傳遞;
提出了一種基于Filter和Scope的Request粒度的上下文管理方案。
github:/pengchengSU/demo-request-scope
1ThreadLocal原理
ThreadLocal主要作用就是實現(xiàn)線程間變量隔離,對于一個變量,每個線程維護一個自己的實例,防止多線程環(huán)境下的資源競爭,那ThreadLocal是如何實現(xiàn)這一特性的呢?
圖1
從上圖可知:
每個Thread對象中都包含一個ThreadLocal.ThreadLocalMap類型的threadlocals成員變量;該map對應(yīng)的每個元素Entry對象中:key是ThreadLocal對象的弱引用,value是該threadlocal變量在當前線程中的對應(yīng)的變量實體;當某一線程執(zhí)行獲取該ThreadLocal對象對應(yīng)的變量時,首先從當前線程對象中獲取對應(yīng)的threadlocals哈希表,再以該ThreadLocal對象為key查詢哈希表中對應(yīng)的value;由于每個線程獨占一個threadlocals哈希表,因此線程間ThreadLocal對象對應(yīng)的變量實體也是獨占的,不存在競爭問題,也就避免了多線程問題。
有人可能會問:ThreadLocalMap是Thread成員變量(非public,只有包訪問權(quán)限,Thread和Threadlocal都在java.lang包下,Thread可以訪問ThreadLocal.ThreadLocalMap),定義卻在ThreadLocal中,為什么要這么設(shè)計?
源碼的注釋給出了解釋:ThreadLocalMap就是維護線程本地變量設(shè)計的,就是讓使用者知道ThreadLocalMap就只做保存線程局部變量這一件事。
set()方法
publicvoidset(Tvalue){
Threadt=Thread.currentThread();//獲取當前線程
ThreadLocalMapmap=getMap(t);//從當前線程對象中獲取threadlocals,該map保存了所用的變量實例
if(map!=null){
map.set(this,value);
}else{
createMap(t,value);//初始threadlocals,并設(shè)置當前變量
}
ThreadLocalMapgetMap(Threadt){
returnt.threadLocals;
voidcreateMap(Threadt,TfirstValue){
t.threadLocals=newThreadLocalMap(this,firstValue);
get()方法
publicTget(){
Threadt=Thread.currentThread();
ThreadLocalMapmap=getMap(t);//從當前線程對象中獲取threadlocals,該map保存了所用的變量實體
if(map!=null){
//獲取當前threadlocal對象對應(yīng)的變量實體
ThreadLocalMap.Entrye=map.getEntry(this);
if(e!=null){
@SuppressWarnings("unchecked")
Tresult=(T)e.value;
returnresult;
//如果map沒有初始化,那么在這里初始化一下
returnsetInitialValue();
withInitial()方法
由于通過ThreadLocal的set()設(shè)置的值,只會設(shè)置當前線程對應(yīng)變量實體,無法實現(xiàn)統(tǒng)一初始化所有線程的ThreadLocal的值。ThreadLocal提供了一個withInitial()方法實現(xiàn)這一功能:
ThreadLocalStringinitValue=ThreadLocal.withInitial(()-"initValue");
publicstaticSThreadLocalSwithInitial(SupplierextendsSsupplier){
//返回SuppliedThreadLocal類型對象
returnnewSuppliedThreadLocal(supplier);
staticfinalclassSuppliedThreadLocalTextendsThreadLocalT{
privatefinalSupplierextendsTsupplier;
SuppliedThreadLocal(SupplierextendsTsupplier){
this.supplier=Objects.requireNonNull(supplier);
@Override
protectedTinitialValue(){
//獲取初始化值
returnsupplier.get();
ThreadLocal中的內(nèi)存泄漏問題
由圖1可知,ThreadLocal.ThreadLocalMap對應(yīng)的Entry中,key為ThreadLocal對象的弱引用,方法執(zhí)行對應(yīng)棧幀中的ThreadLocal引用為強引用。當方法執(zhí)行過程中,由于棧幀銷毀或者主動釋放等原因,釋放了ThreadLocal對象的強引用,即表示該ThreadLocal對象可以被回收了。又因為Entry中key為ThreadLocal對象的弱引用,所以當jvm執(zhí)行GC操作時是能夠回收該ThreadLocal對象的。
而Entry中value對應(yīng)的是變量實體對象的強引用,因此釋放一個ThreadLocal對象,是無法釋放ThreadLocal.ThreadLocalMap中對應(yīng)的value對象的,也就造成了內(nèi)存泄漏。除非釋放當前線程對象,這樣整個threadlocals都被回收了。但是日常開發(fā)中會經(jīng)常使用線程池等線程池化技術(shù),釋放線程對象的條件往往無法達到。
JDK處理的方法是,在ThreadLocalMap進行set()、get()、remove()的時候,都會進行清理:
privateEntrygetEntry(ThreadLocalkey){
inti=key.threadLocalHashCode(table.length-1);
Entrye=table[i];
if(e!=nulle.get()==key)
returne;
else
returngetEntryAfterMiss(key,i,e);
privateEntrygetEntryAfterMiss(ThreadLocalkey,inti,Entrye){
Entry[]tab=table;
intlen=tab.length;
while(e!=null){
ThreadLocalk=e.get();
if(k==key)
returne;
if(k==null)
//如果key為null,對應(yīng)的threadlocal對象已經(jīng)被回收,清理該Entry
expungeStaleEntry(i);
else
i=nextIndex(i,len);
e=tab[i];
returnnull;
2自定義上下文Scope
在工作中,我們經(jīng)常需要維護一些上下文,這樣可以避免在方法調(diào)用過程中傳入過多的參數(shù),需要查詢/修改一些數(shù)據(jù)的時候,直接在當前上下文中操作就行了。舉個具體點的例子:當web服務(wù)器收到一個請求時,需要解析當前登錄態(tài)的用戶,在后續(xù)的業(yè)務(wù)執(zhí)行流程中都需要這個用戶名。
如果只需要維護一個上下文狀態(tài)數(shù)據(jù)還比較好處理,可以通過方法傳參的形式,執(zhí)行每個業(yè)務(wù)方法的時候都通過添加一個表示用戶名方法參數(shù)傳遞進去,但是如果需要維護上下文狀態(tài)數(shù)據(jù)比較多的話,這個方式就不太優(yōu)雅了。
一個可行的方案是通過Threadlocal實現(xiàn)請求線程的上下文,只要是同一線程的執(zhí)行過程,不同方法間不傳遞上下文狀態(tài)變量,直接操作ThreadLocal對象實現(xiàn)狀態(tài)數(shù)據(jù)的讀寫。當存在多個上下文狀態(tài)的話,則需要維護多個ThreadLocal,似乎也可以勉強接受。但是當遇到業(yè)務(wù)流程中使用線程池的情況下,從Tomcat傳遞這些ThreadLocal到線程池中的線程里就變的比較麻煩了。
基于以上考慮,下面介紹一種基于Threadlocal實現(xiàn)的上下文管理組件Scope:
Scope.java
publicclassScope{
//靜態(tài)變量,維護不同線程的上下文Scope
privatestaticfinalThreadLocalScopeSCOPE_THREAD_LOCAL=newThreadLocal();
//實例變量,維護每個上下文中所有的狀態(tài)數(shù)據(jù),為了區(qū)分不同的狀態(tài)數(shù)據(jù),使用ScopeKey類型的實例作為key
privatefinalConcurrentMapScopeKey,Objectvalues=newConcurrentHashMap();
//獲取當前上下文
publicstaticScopegetCurrentScope(){
returnSCOPE_THREAD_LOCAL.get();
//在當前上下文設(shè)置一個狀態(tài)數(shù)據(jù)
publicTvoidset(ScopeKeyTkey,Tvalue){
if(value!=null){
values.put(key,value);
}else{
values.remove(key);
//在當前上下文讀取一個狀態(tài)數(shù)據(jù)
publicTTget(ScopeKeyTkey){
Tvalue=(T)values.get(key);
if(value==nullkey.initializer()!=null){
value=key.initializer().get();
returnvalue;
//開啟一個上下文
publicstaticScopebeginScope(){
Scopescope=SCOPE_THREAD_LOCAL.get();
if(scope!=null){
thrownewIllegalStateException("startascopeinanexistscope.");
scope=newScope();
SCOPE_THREAD_LOCAL.set(scope);
returnscope;
//關(guān)閉當前上下文
publicstaticvoidendScope(){
SCOPE_THREAD_LOCAL.remove();
ScopeKey.java
publicfinalclassScopeKeyT{
//初始化器,參考ThreadLocal的withInitial()
privatefinalSupplierTinitializer;
publicScopeKey(){
this(null);
publicScopeKey(SupplierTinitializer){
this.initializer=initializer;
//統(tǒng)一初始化所有線程的ScopeKey對應(yīng)的值,參考ThreadLocal的withInitial()
publicstaticTScopeKeyTwithInitial(SupplierTinitializer){
returnnewScopeKey(initializer);
publicSupplierTinitializer(){
returnthis.initializer;
//獲取當前上下文中ScopeKey對應(yīng)的變量
publicTget(){
ScopecurrentScope=getCurrentScope();
returncurrentScope.get(this);
//設(shè)置當前上下文中ScopeKey對應(yīng)的變量
publicbooleanset(Tvalue){
ScopecurrentScope=getCurrentScope();
if(currentScope!=null){
currentScope.set(this,value);
returntrue;
}else{
returnfalse;
使用方式
@Test
publicvoidtestScopeKey(){
ScopeKeyStringlocalThreadName=newScopeKey();
//不同線程中執(zhí)行時,開啟獨占的Scope
Runnabler=()-{
//開啟Scope
Scope.beginScope();
try{
StringcurrentThreadName=Thread.currentThread().getName();
localThreadName.set(currentThreadName);
("currentThread:{}",localThreadName.get());
}finally{
//關(guān)閉Scope
Scope.endScope();
newThread(r,"thread-1").start();
newThread(r,"thread-2").start();
/**執(zhí)行結(jié)果
*[thread-1]INFOcom.example.demo.testscope.TestScope-currentThread:thread-1
*[thread-2]INFOcom.example.demo.testscope.TestScope-currentThread:thread-2
@Test
publicvoidtestWithInitial(){
ScopeKeyStringinitValue=ScopeKey.withInitial(()-"initVal");
Runnabler=()-{
Scope.beginScope();
try{
("initValue:{}",initValue.get());
}finally{
Scope.endScope();
newThread(r,"thread-1").start();
newThread(r,"thread-2").start();
/**執(zhí)行結(jié)果
*[thread-1]INFOcom.example.demo.testscope.TestScope-initValue:initVal
*[thread-2]INFOcom.example.demo.testscope.TestScope-initValue:initVal
上面的測試用例中在代碼中手動開啟和關(guān)閉Scope不太優(yōu)雅,可以在Scope中添加兩個個靜態(tài)方法包裝下Runnable和Supplier接口:
publicstaticXextendsThrowablevoidrunWithNewScope(@NonnullThrowableRunnableXrunnable)
throwsX{
supplyWithNewScope(()-{
runnable.run();
returnnull;
publicstaticT,XextendsThrowableT
supplyWithNewScope(@NonnullThrowableSupplierT,Xsupplier)throwsX{
beginScope();
try{
returnsupplier.get();
}finally{
endScope();
@FunctionalInterface
publicinterfaceThrowableRunnableXextendsThrowable{
voidrun()throwsX;
publicinterfaceThrowableSupplierT,XextendsThrowable{
Tget()throwsX;
以新的Scope執(zhí)行,可以這樣寫:
@Test
publicvoidtestRunWithNewScope(){
ScopeKeyStringlocalThreadName=newScopeKey();
ThrowableRunnabler=()-{
StringcurrentThreadName=Thread.currentThread().getName();
localThreadName.set(currentThreadName);
("currentThread:{}",localThreadName.get());
//不同線程中執(zhí)行時,開啟獨占的Scope
newThread(()-Scope.runWithNewScope(r),"thread-1").start();
newThread(()-Scope.runWithNewScope(r),"thread-2").start();
/**執(zhí)行結(jié)果
*[thread-2]INFOcom.example.demo.TestScope.testscope-currentThread:thread-2
*[thread-1]INFOcom.example.demo.TestScope.testscope-currentThread:thread-1
3在線程池中傳遞Scope
在上一節(jié)中實現(xiàn)的Scope,通過ThreadLocal實現(xiàn)了了一個自定義的上下文組件,在同一個線程中通過ScopeKey.set()/ScopeKey.get()讀寫同一個上下文中的狀態(tài)數(shù)據(jù)。
現(xiàn)在需要實現(xiàn)這樣一個功能,在一個線程執(zhí)行過程中開啟了一個Scope,隨后使用線程池執(zhí)行任務(wù),要求在線程池中也能獲取當前Scope中的狀態(tài)數(shù)據(jù)。典型的使用場景是:服務(wù)收到一個用戶請求,通過Scope將登陸態(tài)數(shù)據(jù)存到當前線程的上下文中,隨后使用線程池執(zhí)行一些耗時的操作,希望線程池中的線程也能拿到Scope中的登陸態(tài)數(shù)據(jù)。
由于線程池中的線程和請求線程不是一個線程,按照目前的實現(xiàn),線程池中的線程是無法拿到請求線程上下文中的數(shù)據(jù)的。
解決方法是,在提交runnable時,將當前的Scope引用存到runnable對象中,當獲得線程執(zhí)行時,將Scope替換到執(zhí)行線程中,執(zhí)行完成后,再恢復(fù)現(xiàn)場。在Scope中新增如下靜態(tài)方法:
//以給定的上下文執(zhí)行Runnable
publicstaticXextendsThrowablevoidrunWithExistScope(Scopescope,ThrowableRunnableXrunnable)throwsX{
supplyWithExistScope(scope,()-{
runnable.run();
returnnull;
//以給定的上下文執(zhí)行Supplier
publicstaticT,XextendsThrowableTsupplyWithExistScope(Scopescope,ThrowableSupplierT,Xsupplier)throwsX{
//保留現(xiàn)場
ScopeoldScope=SCOPE_THREAD_LOCAL.get();
//替換成外部傳入的Scope
SCOPE_THREAD_LOCAL.set(scope);
try{
returnsupplier.get();
}finally{
if(oldScope!=null){
//恢復(fù)線程
SCOPE_THREAD_LOCAL.set(oldScope);
}else{
SCOPE_THREAD_LOCAL.remove();
實現(xiàn)支持Scope切換的自定義線程池ScopeThreadPoolExecutor:
publicclassScopeThreadPoolExecutorextendsThreadPoolExecutor{
ScopeThreadPoolExecutor(intcorePoolSize,intmaximumPoolSize,longkeepAliveTime,
TimeUnitunit,BlockingQueueRunnableworkQueue){
super(corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue);
publicstaticScopeThreadPoolExecutornewFixedThreadPool(intnThreads){
returnnewScopeThreadPoolExecutor(nThreads,nThreads,0L,TimeUnit.MILLISECONDS,
newLinkedBlockingQueueRunnable
*只要override這一個方法就可以
*所有submit,invokeAll等方法都會代理到這里來
@Override
publicvoidexecute(Runnablecommand){
Scopescope=getCurrentScope();
//提交任務(wù)時,把執(zhí)行execute方法的線程中的Scope傳進去
super.execute(()-runWithExistScope(scope,command::run));
測試下ScopeThreadPoolExecutor是否生效:
@Test
publicvoidtestScopeThreadPoolExecutor(){
ScopeKeyStringlocalVariable=newScopeKey();
Scope.beginScope();
try{
localVariable.set("valueoutofthreadpool");
Runnabler=()-("localVariableinthreadpool:{}",localVariable.get());
//使用線程池執(zhí)行,能獲取到外部Scope中的數(shù)據(jù)
ExecutorServiceexecutor=ScopeThreadPoolExecutor.newFixedThreadPool(10);
executor.execute(r);
executor.submit(r);
}finally{
Scope.endScope();
/**執(zhí)行結(jié)果
*[pool-1-thread-1]INFOcom.example.demo.testscope.TestScope-localVariableinthreadpool:valueoutofthreadpool
*[pool-1-thread-2]INFOcom.example.demo.testscope.TestScope-localVariableinthreadpool:valueoutofthreadpool
@Test
publicvoidtestScopeThreadPoolExecutor2(){
ScopeKeyStringlocalVariable=newScopeKey();
Scope.runWithNewScope(()-{
localVariable.set("valueoutofthreadpool");
Runnabler=()-("localVariableinthreadpool:{}",localVariable.get());
//使用線程池執(zhí)行,能獲取到外部Scope中的數(shù)據(jù)
ExecutorServiceexecutor=ScopeThreadPoolExecutor.newFixedThreadPool(10);
executor.execute(r);
executor.submit(r);
/**執(zhí)行結(jié)果
*[pool-1-thread-2]INFOcom.example.demo.testscope.TestScope-localVariableinthreadpool:valueoutofthreadpool
*[pool-1-thread-1]INFOcom.example.demo.testscope.TestScope-localVariableinthreadpool:valueoutofthreadpool
以上兩個測試用例,分別通過手動開啟Scope、借助runWithNewScope工具方法自動開啟Scope兩種方式驗證了自定義線程池ScopeThreadPoolExecutor的Scope可傳遞性。
4通過Filter、Scope實現(xiàn)Request上下文
接下來介紹如何通過Filter和Scope實現(xiàn)Request粒度的Scope上下文。思路是:通過注入一個攔截器,在進入攔截器后開啟Scope作為一個請求的上下文,解析Request對象獲取獲取相關(guān)狀態(tài)信息(如登陸用戶),并在Scope中設(shè)置,在離開攔截器時關(guān)閉Scope。
AuthScope.java
//獲取登錄態(tài)的工具類
publicclassAuthScope{
privatestaticfinalScopeKeyStringLOGIN_USER=newScopeKey();
publicstaticStringgetLoginUser(){
returnLOGIN_USER.get();
publicstaticvoidsetLoginUser(StringloginUser){
if(loginUser==null){
loginUser="unknownUser";
LOGIN_USER.set(loginUser);
ScopeFilter.java
@Lazy
@Order(0)
@Service("scopeFilter")
publicclassScopeFilterextendsOncePerRequestFilter{
@Override
protectedStringgetAlreadyFilteredAttributeName(){
returnthis.getClass().getName();
@Override
protectedvoiddoFilterInternal(HttpServletRequestrequest,HttpServletResponseresponse,
FilterChainfilterChain)throwsServletException,IOException{
//開啟Scope
beginScope();
try{
Cookie[]cookies=request.getCookies();
StringloginUser="unknownUser";
if(cookies!=null){
for(Cookiecookie:cookies){
if(cookie.getName().equals("login_user")){
loginUser=cookie.getValue();
break;
//設(shè)置該Request上下文對用的登陸用戶
AuthScope.setLoginUser(loginUser);
filterChain.doFilter(request,response);
}finally{
//關(guān)閉Scope
endScope();
注入Filter
@Slf4j
@Configuration
publicclassFilterConfig{
@Bean
publi
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負責(zé)。
- 6. 下載文件中如有侵權(quán)或不適當內(nèi)容,請與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 銳明技術(shù)公司深度報告:商用車智能方案龍頭高階智駕打開成長空間
- 固井工崗前生產(chǎn)安全意識考核試卷含答案
- 刻瓷工8S考核試卷含答案
- 商場售后服務(wù)制度
- 公共交通停車場管理制度
- 城市道路施工環(huán)境保護措施制度
- 學(xué)校學(xué)生社會實踐指導(dǎo)制度
- 報表粉飾畢業(yè)論文
- 2025年抗腫瘤藥物處方權(quán)限考試試題及答案
- 2025年煤氣復(fù)審考試及考試題庫含答案
- 醫(yī)院醫(yī)療保險費用審核制度
- 村衛(wèi)生室醫(yī)療質(zhì)量相關(guān)管理制度
- 非遺傳承人激勵機制探索-深度研究
- 中小學(xué)校園中匹克球推廣策略與實踐研究
- 2024年世界職業(yè)院校技能大賽高職組“體育活動設(shè)計與實施組”賽項考試題庫(含答案)
- 高中地理選擇性必修一(湘教版)期末檢測卷02(原卷版)
- 滬教版九年級化學(xué)上冊(上海版)全套講義
- 三角函數(shù)圖像變化課件
- 《內(nèi)存條知識培訓(xùn)》課件
- 人教版(2024)七年級地理期末復(fù)習(xí)必背考點提綱
- 廣東省深圳市南山區(qū)2023-2024學(xué)年四年級上學(xué)期數(shù)學(xué)期末教學(xué)質(zhì)量監(jiān)測試卷
評論
0/150
提交評論