管理好你的ThreadLocal

          Posted on 2010-01-25 22:10 周舒陽 閱讀(3461) 評論(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 }

                這里提供一個示意圖來展示注冊與重置的流程:

               
                這里給大家提供一些建議:
          1. 不管你如何使用ThreadLocal,請不要忘記重置它。
          2. 當你的ThreadLocal對象的有效期局限在一次請求中(或者是其他的周期性時間段中),你可以嘗試使用AutoResetThreadLocal和ThreadLocalRegistry來簡化你的代碼。
          3. 請注意!你還是需要在什么地方調用一下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://www.aygfsteel.com/Files/ShuyangZhou/ThreadLocalRegistry/src.zip

          Feedback

          # re: 管理好你的ThreadLocal  回復  更多評論   

          2010-01-26 13:43 by JiangMin
          我就喜歡看樓主這樣的文章!

          # re: 管理好你的ThreadLocal  回復  更多評論   

          2010-01-27 20:08 by john locke
          寫的不錯

          # re: 管理好你的ThreadLocal  回復  更多評論   

          2010-02-01 16:06 by yefeng
          我想問個問題,ThreadLocal是線程安全的呀,應該不會有你這樣問題啊

          # re: 管理好你的ThreadLocal  回復  更多評論   

          2010-02-01 16:17 by 周舒陽
          @yefeng
          這跟線程安全與否無關,這里描述的是當你的ThreadLocal變量逃離了它的作用域時會引起的問題,你仍然是在同一個線程的上下文下,但作用域已經改變了。你可以將ThreadLocal理解為一個線程內的全局變量,但你的應用規定這個ThreadLocal存在一定的邏輯作用域(比如一個request的處理),當你跨作用域傳遞它而又不進行重置操作的話就可能會引起問題。ThreadLocalRegistry的目的是提供集中的重置處理,以防止由于“馬虎”引起的問題。

          只有注冊用戶登錄后才能發表評論。


          網站導航:
           

          posts - 3, comments - 15, trackbacks - 0, articles - 0

          Copyright © 周舒陽

          主站蜘蛛池模板: 城口县| 天峻县| 运城市| 嘉义县| 兰考县| 凌源市| 清苑县| 福安市| 忻州市| 徐州市| 崇州市| 平乡县| 南京市| 利川市| 年辖:市辖区| 尉犁县| 满洲里市| 酒泉市| 苗栗市| 明水县| 阿拉善左旗| 甘肃省| 芒康县| 华宁县| 洪江市| 新建县| 怀来县| 出国| 彩票| 茂名市| 休宁县| 介休市| 柳州市| 北宁市| 阿坝县| 焉耆| 洪江市| 大厂| 绍兴县| 白玉县| 万荣县|