寫作目的
以前的工作經(jīng)歷中,筆者本人有深度使用ThreadLocal的經(jīng)驗,它在合適的場景下,是非常好用的一個工具,因此打算分享一二,為各位看官們實際編碼過程中提供多一種選擇,促進大家共同進步
場景
先舉兩個大家熟悉的、Spring中用到的場景
場景一
在Spring管理的Singleton Bean中,如果期望調(diào)用同一個類里被事務(wù)注解的方法(m1調(diào)用m2),且希望事務(wù)能生效,可以考慮的實現(xiàn)如下:
@Service
public class FooService {
public void m1() {
// 拿到FooService的代理對象
FooService self = (FooService)AopContext.currentProxy();
// 通過代理對象調(diào)用m2方法,事務(wù)切面就能生效
self.m2();
}
@Transactional
public void m2() {
// save to db;
}
}
之所以要通過代理對象去調(diào)用,是因為若在m1中使用原始對象(this),不會進入代理邏輯,因此切面邏輯(事務(wù))是不會生效的。所以問題就轉(zhuǎn)變成了:在一個方法執(zhí)行過程中,如何拿到當(dāng)前方法所屬對象的代理對象?
在FooService中直接注入自己
通過調(diào)用AopContext.currentProxy()
為與本文扣題,我們的示例采用的是方式2。那么接下來需要探尋的是,為什么AopContext.currentProxy()就能拿到代理對象呢?
- 外部方法調(diào)用FooService#m1的時候,進入了代理邏輯(org.springframework.aop.framework.CglibAopProxy.DynamicAdvisedInterceptor#intercept),與此同時,會把代理對象放到AopContext

- AopContext#setCurrentProxy會將代理對象(proxy)放到
currentProxy中,而currentProxy是一個ThreadLocalimage

-
在m1方法中執(zhí)行
FooService self = (FooService)AopContext.currentProxy();,本質(zhì)就是從當(dāng)前線程的ThreadLocal中取出上邊1、2步放入的代理對象,接下來就可以用代理對象"搞事情"image -
代理對象切面邏輯結(jié)束后,用oldProxy將AopContext還原
image
如此這般,通過預(yù)先將代理對象放入當(dāng)前線程的ThreadLocal的方式,就可以做到在接下來的流程中,在任意位置都可以很方便獲取到該代理對象,而不需要通過方法參數(shù)一層層透傳下去
在理解上,可以將ThreadLocal理解成為當(dāng)前線程裝東西的"籃子":在線程執(zhí)行任務(wù)時,可以在某節(jié)點(方法)將一些東西放進"籃子",并可在后續(xù)的任意節(jié)點(方法)從"籃子"取出之前放入的東西

場景二
大家可能會經(jīng)常使用到Spring @Transactional事務(wù)注解,若不理解原理,可能容易踩坑。一個基本共識是:事務(wù)由連接管理,一個事務(wù)只屬于一個連接,若要使得事務(wù)生效(相關(guān)DB操作同時提交,同時回滾),必須確保是同一個連接
那么就會有如下場景:
@Service
public class FooService {
@Resource
private BarService barService;
@Transactional
public void foo() {
barService.bar();
// save foo
// ...
}
}
@Service
public class BarService {
@Transactional
public void bar() {
// save bar
}
}
FooService#foo調(diào)用BarService#bar進行關(guān)于Bar的數(shù)據(jù)庫操作,之后進行Foo的數(shù)據(jù)庫操作,由于方法foo與方法bar都被@Transactional注解,可以確保事務(wù)操作的同時提交或同時回滾。"確保事務(wù)操作的同時提交或同時回滾"是我們在大量的編程經(jīng)驗中可以輕易得出的結(jié)論,如果追究根因,其實就是共識:這兩個操作處于同一個事務(wù)當(dāng)中,都由同一個連接里的事務(wù)進行管理。因此,重點是需要確保"同一個事務(wù)"以及"同一個連接"。"同一個事務(wù)"是如何實現(xiàn)的,請參考org.springframework.transaction.support.AbstractPlatformTransactionManager#handleExistingTransaction并理解事務(wù)的傳播屬性(不是本文重點,略過)。
那么問題就來了:"同一個連接"又是如何實現(xiàn)的呢?代碼跑在不同類以及不同方法上,Spring如何做到前后兩次獲取的是同一個連接?如果看官們理解了場景一,相信此處應(yīng)該有了結(jié)論 -> ThreadLocal
-
外部調(diào)用
FooService#foo第一次開啟事務(wù)的時候,將從連接池中取出一個連接,并將連接放到 ConnectionHolder 中,然后將ConnectionHolder綁定到當(dāng)前線程// org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin protected void doBegin(Object transaction, TransactionDefinition definition) { DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; Connection con = null; try { if (!txObject.hasConnectionHolder() || txObject.getConnectionHolder().isSynchronizedWithTransaction()) { // 從連接池中取出連接,并將連接放到 ConnectionHolder 中 Connection newCon = this.dataSource.getConnection(); // ...(省略) txObject.setConnectionHolder(new ConnectionHolder(newCon), true); } // ...(省略) // Bind the connection holder to the thread. // 將 ConnectionHolder 綁定到當(dāng)前線程 if (txObject.isNewConnectionHolder()) { TransactionSynchronizationManager.bindResource(getDataSource(), txObject.getConnectionHolder()); } } // ...(省略) }
將value(ConnectionHolder)放到map之后,再放到當(dāng)前線程的ThreadLocal中,以實現(xiàn)與當(dāng)前線程的綁定
// org.springframework.transaction.support.TransactionSynchronizationManager#bindResource
public static void bindResource(Object key, Object value) throws IllegalStateException {
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key); // 忽略
// resources 是ThreadLocal類型的成員變量
Map<Object, Object> map = resources.get();
// set ThreadLocal Map if none found
if (map == null) {
map = new HashMap<Object, Object>();
resources.set(map);
}
// 將value(ConnectionHolder)放到map之后,再放到當(dāng)前線程的ThreadLocal中,以實現(xiàn)與當(dāng)前線程的綁定
Object oldValue = map.put(actualKey, value);
// ...(省略)
}

-
FooService#foo調(diào)用BarService#bar將觸發(fā)第二次開啟事務(wù),發(fā)現(xiàn)傳播屬性為TransactionDefinition.PROPAGATION_REQUIRED(默認(rèn)值),會將第二次事務(wù)直接"融入"第一次事務(wù),能實現(xiàn)的關(guān)鍵點在于前后兩次操作使用同一個連接。如下,是從resources(ThreadLocal)中取出map,并取出第1步中放入的ConnectionHolder,因此就可以確保拿到同一個連接// org.springframework.transaction.support.TransactionSynchronizationManager#getResource public static Object getResource(Object key) { Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key); return doGetResource(actualKey); } private static Object doGetResource(Object actualKey) { // resources 是ThreadLocal類型的成員變量 // 從ThreadLocal中取到第1步中放入的ConnectionHolder Map<Object, Object> map = resources.get(); if (map == null) { return null; } Object value = map.get(actualKey); // ...(省略) return value; }
實戰(zhàn)
以上使用ThreadLocal的場景,都是框架里提供的,是否業(yè)務(wù)開發(fā)中就用不到了呢?非也,業(yè)務(wù)開發(fā)中如果運用妥當(dāng),同樣能省掉很多事,實現(xiàn)精簡代碼的目的。筆者同樣舉兩個業(yè)務(wù)開發(fā)用到的場景,以供大家開闊思路
場景一
該場景比較普遍,幾乎所有公司都用的上,因與業(yè)務(wù)無關(guān),首先介紹,幫助沒有使用過ThreadLocal的看官們找找感覺
所有公司所有業(yè)務(wù)都會有用戶體系,進行業(yè)務(wù)操作都需要登錄、鑒權(quán),以識別某用戶是否有操作某項資源的權(quán)限,因此基本上很多業(yè)務(wù)都需要拿到當(dāng)前請求的用戶信息
一種可以考慮的方式是:每次請求到來時,可否在入口中統(tǒng)一鑒權(quán),然后將鑒權(quán)之后的用戶信息記錄下來,接下來但凡有業(yè)務(wù)要用到,可以直接從保存的地方獲取到用戶信息,不用再一次鑒權(quán),省去一次次的計算消耗;待請求結(jié)束后,就將用戶信息銷毀
- 注冊一個
HandlerInterceptor或Filter,用于在入口處攔截請求,并對當(dāng)前請求進行鑒權(quán),獲取用戶信息,放入UserHolder中,并在請求結(jié)束的時候清理掉該用戶信息(注:請求結(jié)束后清理用戶信息很重要,避免內(nèi)存泄露)
public class LoginCheckInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// 鑒權(quán)(獲取用戶信息)
User user = xxx(request);
if (user != null) {
// 放到UserHolder這個容器(Holder)中
UserHolder.putUser(user);
return true;
}
// 鑒權(quán)(獲取用戶信息)失敗,則直接攔截
return false;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 請求結(jié)束后,清理掉用戶信息(畫重點:很重要)
UserHolder.clear();
}
}
UserHolder是一個容器,包裝ThreadLocal,放置當(dāng)次請求中用戶的信息,如下示:
public class UserHolder {
// 內(nèi)部維護一個ThreadLocal成員變量
private static ThreadLocal<User> tl = new ThreadLocal<>();
public static void putUser(User user) {
tl.set(user);
}
public static User getUser() {
return tl.get();
}
public static Long getUid() {
return Optional.ofNullable(tl.get()).map(User::getId).orElse(null);
}
public static void clear() {
tl.remove();
}
}
- 在需要使用的地方,例如XXXService,直接在任意方法內(nèi)部調(diào)用
UserHolder#getUser即可獲取到當(dāng)前用戶的完整信息。如此這般,就不需要將User從Controller層層傳遞到Service里的某個小方法,避免了"依賴污染",對于[多層次]的業(yè)務(wù)代碼組織尤為有效,省去了多次參數(shù)傳遞的煩惱
反例:明明request、xxx, yyy 都不直接依賴User,只是在最后一層zzz用到User,卻不得不將用戶信息層層傳遞,造成"依賴污染"
// Service接口方法
public void request(User user) {
xxx(user);
}
private void xxx(User user) {
yyy(user);
}
private void yyy(User user) {
zzz(user);
}
private void zzz(User user) {
String email = user.getEmail();
}
使用ThreadLocal后,簡化如下,只有zzz需要用到User信息,那就在zzz中直接獲取用戶信息,并進行業(yè)務(wù)邏輯
public void request() {
xxx();
}
public void xxx() {
yyy();
}
private void yyy() {
zzz();
}
private void zzz() {
User user = UserHolder.getUser();
String email = user.getEmail();
}
看官們可能會有疑問:我們的后端服務(wù)并不需要鑒權(quán),而是由前邊的網(wǎng)關(guān)做好了鑒權(quán),然后通過某些方式(如請求頭)攜帶給后端服務(wù),那還能使用嗎?答案是肯定的,請看場景二
場景二
筆者以前從事廣告相關(guān)業(yè)務(wù),廣告投放邏輯里需要識別是哪個用戶,當(dāng)前手機型號、品牌是什么,操作系統(tǒng)版本號、APP版本號、設(shè)備尺寸等等總共幾十項信息,這些信息都是由請求頭攜帶到后端服務(wù),而廣告投放邏輯里面不同的模塊會使用不同頭字段來做相應(yīng)的業(yè)務(wù)邏輯

在實現(xiàn)上,同樣是在入口處,通過自定義攔截器或過濾器的方式攔截請求,獲取到所有的請求頭信息,封裝進HeaderHolder,以便在不同模塊間方便獲取請求頭信息做業(yè)務(wù)邏輯??梢韵胂?,一個業(yè)務(wù)有不同的模塊,不同模塊又分為不同的業(yè)務(wù)抽象層次,而不同層次代碼中可能需要使用到請求頭信息都不一樣,如果是通過函數(shù)傳參的方式將Header層層傳遞,代碼將變得多么糟糕
一般化推論
誠然,使用ThreadLocal會為我們的編程帶來許多好處,同樣的也為代碼的管理,依賴的梳理帶來了一些挑戰(zhàn)。因此真正要把ThreadLocal用好并不簡單,并非任何場景都可以使用的,用好是一個亮點,用不好就是坑:這也是所有技術(shù)選型所要面臨的問題,技術(shù)本身無所謂好壞,沒有任何一種技術(shù)能在所有方面碾壓同類的競品技術(shù),否則一定能將競品取代而不需要面臨trade off,拋開業(yè)務(wù)場景談技術(shù)(架構(gòu))就是耍流氓
結(jié)合筆者本人的實際使用經(jīng)驗,將使用ThreadLocal的場景總結(jié)為一句話:在盡可能早的時機將一些大多數(shù)后續(xù)流程要使用的只讀信息封裝到ThreadLocal里,供后續(xù)流程任意取用
- 盡可能早的時機:好事須趁早,越早生成,就越早能使用,可擴展性越高,最好是在與業(yè)務(wù)無關(guān)的入口處
- 大多數(shù): 有選擇性地將大多數(shù)后續(xù)都需要用到的信息放到ThreadLocal里,而不是所有信息無腦放入
- 只讀信息: 封裝的信息最好"Read Only",意味著一旦生成,將不可再更改,整個后續(xù)流程都只能讀取,該要求是為了讓代碼更可控,倘若信息可以修改,就有可能導(dǎo)致不同層次的代碼產(chǎn)生間接依賴(如下層依賴上層),系統(tǒng)行為將不可控
總結(jié)
本文從Spring使用ThreadLocal的案例為切入點,介紹了ThreadLocal在開源框架中使用的兩個場景;接著又分享了兩個業(yè)務(wù)開發(fā)中能夠用上的場景,幫助看官們更感性地理解ThreadLocal的作用;最后,針對ThreadLocal潛在"可維護性"問題,鮮明的給出筆者本人的觀點:技術(shù)無所謂好壞,謹(jǐn)慎而不盲目使用,并針對如何正確使用ThreadLocal提出了一般化的方法論,只要遵循相應(yīng)原則,基本上對于代碼還是相當(dāng)可控,而不致引入額外的維護成本


