Posted on 2010-01-25 22:10
周舒陽 閱讀(3458)
評論(4) 編輯 收藏
本期Blog原文參見:
http://www.liferay.com/web/shuyang.zhou/blog/-/blogs/master-your-threadlocals
ThreadLocal不是解決并發(fā)問題的"銀彈", 實際上許多關(guān)于并發(fā)的最佳實踐并不鼓勵使用它。
但有些時候它確實是必須的,或者它能夠極大程度的簡化你的設(shè)計。因此我們必須正視它的存在。由于它非常容易被誤用,我們必須找到一種方法來避免它導(dǎo)致麻煩。今天我們不是要講該在什么時候以及如何使用ThreadLocal,而是要談一談當(dāng)你必須要使用它時,如果能夠確保它不惹大麻煩。
開發(fā)者使用ThreadLocal時最容易犯的也是最嚴重的錯誤就是忘記重置它。假如你使用ThreadLocal來緩存用戶的認證信息,用戶A通過Worker Thread1登錄系統(tǒng),你將認證信息緩存在ThreadLocal中以提升性能。但在Worker Thread1完成對用戶A的服務(wù)后你忘記了重置ThreadLocal(清空緩存)。就在這時,用戶B在沒有登錄的情況下訪問你的系統(tǒng),湊巧的是它也接受了來自Worker Thread1的服務(wù),Worker Thread1檢查了一下它的緩存發(fā)現(xiàn)了認證信息,因此它會將用戶B當(dāng)作用戶A來服務(wù)。你應(yīng)該會想象到接下來將要發(fā)生什么。
對于這一問題,一個立即就會想到的解決方案是在結(jié)束一個request的服務(wù)后重置ThreadLocal。但問題的難點在于一個Worker Thread可能會擁有多個ThreadLocal對象,它們散落在你程序的各個角落,如何才能輕松的將它們?nèi)恐刂媚兀磕阈枰獮槊恳粋€Worker Thread的所有ThreadLocal對象提供一個ThreadLocal的注冊表。請注意!這個注冊表本身也必須是一個ThreadLocal對象(但它不注冊自身的引用),因此當(dāng)一個Worker Thread重置注冊表中的ThreadLocal對象時,它只會重置屬于自己的ThreadLocal對象,而不是其他線程的。一旦你有了這樣一個注冊表,你就可以在一個request的處理結(jié)束后重置全部ThreadLocal對象了,通常是在一個filter中執(zhí)行重置?,F(xiàn)在你應(yīng)該馬上想到的一個問題是:我們該如何將一個ThreadLocal對象添加到注冊表中呢?你當(dāng)然可以在每次使用ThreadLocal后添加一行注冊代碼,但這樣會讓你的代碼很丑,而且這種做法有著和原來一樣的問題:如果你忘了一行注冊代碼怎么辦?解決辦法是創(chuàng)建一個ThreadLocal的子類,重寫set()和initialValue()方法,每當(dāng)這些方法被調(diào)用時,它們會將自身注冊到注冊表中。這樣整個注冊和重置的過程對于開發(fā)者而言就是透明的了,你所要做的只是使用我創(chuàng)建的ThreadLocal子類。
這里列出ThreadLocal子類和注冊表的代碼:
1 public class AutoResetThreadLocal<T> extends InitialThreadLocal<T> {
2
3 public AutoResetThreadLocal() {
4 this(null);
5 }
6
7 public AutoResetThreadLocal(T initialValue) {
8 super(initialValue);
9 }
10
11 public void set(T value) {
12 ThreadLocalRegistry.registerThreadLocal(this);
13
14 super.set(value);
15 }
16
17 protected T initialValue() {
18 ThreadLocalRegistry.registerThreadLocal(this);
19
20 return super.initialValue();
21 }
22
23 }
1 public class ThreadLocalRegistry {
2
3 public static ThreadLocal<?>[] captureSnapshot() {
4 Set<ThreadLocal<?>> threadLocalSet = _threadLocalSet.get();
5
6 return threadLocalSet.toArray(
7 new ThreadLocal<?>[threadLocalSet.size()]);
8 }
9
10 public static void registerThreadLocal(ThreadLocal<?> threadLocal) {
11 Set<ThreadLocal<?>> threadLocalSet = _threadLocalSet.get();
12
13 threadLocalSet.add(threadLocal);
14 }
15
16 public static void resetThreadLocals() {
17 Set<ThreadLocal<?>> threadLocalSet = _threadLocalSet.get();
18
19 for (ThreadLocal<?> threadLocal : threadLocalSet) {
20 threadLocal.remove();
21 }
22 }
23
24 private static ThreadLocal<Set<ThreadLocal<?>>> _threadLocalSet =
25 new InitialThreadLocal<Set<ThreadLocal<?>>>(
26 new HashSet<ThreadLocal<?>>());
27
28 }
這里提供一個示意圖來展示注冊與重置的流程:
這里給大家提供一些建議:
- 不管你如何使用ThreadLocal,請不要忘記重置它。
- 當(dāng)你的ThreadLocal對象的有效期局限在一次請求中(或者是其他的周期性時間段中),你可以嘗試使用AutoResetThreadLocal和ThreadLocalRegistry來簡化你的代碼。
- 請注意!你還是需要在什么地方調(diào)用一下ThreadLocalRegistry.resetThreadLocals()的(通常是在一個filter中)。
補充說明!
細心的讀者可能已經(jīng)發(fā)現(xiàn)了,ThreadLocalRegistry.resetThreadLocals(),只是重置已注冊的ThreadLocal對象,并沒有將它們從注冊表中移除。你可能會擔(dān)心這樣的注冊表只會越長越大,最終導(dǎo)致內(nèi)存泄漏。
本文開篇時我就有說明,這里不講該如果使用ThreadLocal,但為了解釋這一問題還是要說明一個ThreadLocal的最佳實踐的。在Liferay中,所有的ThreadLocal對象都是static的,也就是說一旦使用ThreadLocal的類的數(shù)量確定了,一個線程可能使用到的最大ThreadLocal對象數(shù)量也就確定了。而且這個數(shù)字在Liferay中是相對比較小的,因此這個注冊表不存在無限增長的問題。
我確實見過有人不將ThreadLocal設(shè)置為static,大部分情況是打字漏掉了。如果你是存心這樣使用,建議你該重新思考一下你的設(shè)計了。
總之,推薦大家始終將ThreadLocal設(shè)置為static的。如果你確實有需要使用非static的ThreadLocal,你可以在ThreadLocalRegistry.resetThreadLocals() 的最后填上一行語句_threadLocalSet.get().clear();這樣可以確保不會產(chǎn)生內(nèi)存泄漏,但也增加了一些開銷。
這里我提供了一個消除了對Liferay其他類文件依賴的ThreadLocalRegistry供大家下載使用。
http://www.tkk7.com/Files/ShuyangZhou/ThreadLocalRegistry/src.zip