Posted on 2010-01-25 22:10
周舒陽 閱讀(3458)
評論(4) 編輯 收藏
本期Blog原文參見:
http://www.liferay.com/web/shuyang.zhou/blog/-/blogs/master-your-threadlocals
ThreadLocal不是解決并發問題的"銀彈", 實際上許多關于并發的最佳實踐并不鼓勵使用它。
但有些時候它確實是必須的,或者它能夠極大程度的簡化你的設計。因此我們必須正視它的存在。由于它非常容易被誤用,我們必須找到一種方法來避免它導致麻煩。今天我們不是要講該在什么時候以及如何使用ThreadLocal,而是要談一談當你必須要使用它時,如果能夠確保它不惹大麻煩。
開發者使用ThreadLocal時最容易犯的也是最嚴重的錯誤就是忘記重置它。假如你使用ThreadLocal來緩存用戶的認證信息,用戶A通過Worker Thread1登錄系統,你將認證信息緩存在ThreadLocal中以提升性能。但在Worker Thread1完成對用戶A的服務后你忘記了重置ThreadLocal(清空緩存)。就在這時,用戶B在沒有登錄的情況下訪問你的系統,湊巧的是它也接受了來自Worker Thread1的服務,Worker Thread1檢查了一下它的緩存發現了認證信息,因此它會將用戶B當作用戶A來服務。你應該會想象到接下來將要發生什么。
對于這一問題,一個立即就會想到的解決方案是在結束一個request的服務后重置ThreadLocal。但問題的難點在于一個Worker Thread可能會擁有多個ThreadLocal對象,它們散落在你程序的各個角落,如何才能輕松的將它們全部重置呢?你需要為每一個Worker Thread的所有ThreadLocal對象提供一個ThreadLocal的注冊表。請注意!這個注冊表本身也必須是一個ThreadLocal對象(但它不注冊自身的引用),因此當一個Worker Thread重置注冊表中的ThreadLocal對象時,它只會重置屬于自己的ThreadLocal對象,而不是其他線程的。一旦你有了這樣一個注冊表,你就可以在一個request的處理結束后重置全部ThreadLocal對象了,通常是在一個filter中執行重置。現在你應該馬上想到的一個問題是:我們該如何將一個ThreadLocal對象添加到注冊表中呢?你當然可以在每次使用ThreadLocal后添加一行注冊代碼,但這樣會讓你的代碼很丑,而且這種做法有著和原來一樣的問題:如果你忘了一行注冊代碼怎么辦?解決辦法是創建一個ThreadLocal的子類,重寫set()和initialValue()方法,每當這些方法被調用時,它們會將自身注冊到注冊表中。這樣整個注冊和重置的過程對于開發者而言就是透明的了,你所要做的只是使用我創建的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,請不要忘記重置它。
- 當你的ThreadLocal對象的有效期局限在一次請求中(或者是其他的周期性時間段中),你可以嘗試使用AutoResetThreadLocal和ThreadLocalRegistry來簡化你的代碼。
- 請注意!你還是需要在什么地方調用一下ThreadLocalRegistry.resetThreadLocals()的(通常是在一個filter中)。
補充說明!
細心的讀者可能已經發現了,ThreadLocalRegistry.resetThreadLocals(),只是重置已注冊的ThreadLocal對象,并沒有將它們從注冊表中移除。你可能會擔心這樣的注冊表只會越長越大,最終導致內存泄漏。
本文開篇時我就有說明,這里不講該如果使用ThreadLocal,但為了解釋這一問題還是要說明一個ThreadLocal的最佳實踐的。在Liferay中,所有的ThreadLocal對象都是static的,也就是說一旦使用ThreadLocal的類的數量確定了,一個線程可能使用到的最大ThreadLocal對象數量也就確定了。而且這個數字在Liferay中是相對比較小的,因此這個注冊表不存在無限增長的問題。
我確實見過有人不將ThreadLocal設置為static,大部分情況是打字漏掉了。如果你是存心這樣使用,建議你該重新思考一下你的設計了。
總之,推薦大家始終將ThreadLocal設置為static的。如果你確實有需要使用非static的ThreadLocal,你可以在ThreadLocalRegistry.resetThreadLocals() 的最后填上一行語句_threadLocalSet.get().clear();這樣可以確保不會產生內存泄漏,但也增加了一些開銷。
這里我提供了一個消除了對Liferay其他類文件依賴的ThreadLocalRegistry供大家下載使用。
http://m.tkk7.com/Files/ShuyangZhou/ThreadLocalRegistry/src.zip