利用ThreadLocal實現(xiàn)一個上下文管理組件_第1頁
利用ThreadLocal實現(xiàn)一個上下文管理組件_第2頁
利用ThreadLocal實現(xiàn)一個上下文管理組件_第3頁
利用ThreadLocal實現(xiàn)一個上下文管理組件_第4頁
利用ThreadLocal實現(xiàn)一個上下文管理組件_第5頁
已閱讀5頁,還剩14頁未讀, 繼續(xù)免費閱讀

下載本文檔

版權(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)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論