閱讀315 返回首頁    go 阿裏雲 go 技術社區[雲棲]


《Java特種兵》5.2 線程安全(二)

 5.2.4 final

在JMM中要求final域(屬性)的初始化動作必須在構造方法return之前完成。換言之,一個對象創建以及將其賦值給一個引用是兩個動作,對象創建還需要經曆分配空間和屬性初始化的過程,普通的屬性初始化允許發生在構造方法return之後(指令重排序)。
似乎這個問題變得很可怕,因為在Java程序中得到的對象竟然有可能還沒有執行完構造方法內的屬性賦值,但在大部分情況下,對象的使用都是在線程內部定義的,在單線程中是絕對可靠的,或者說在單線程中要求使用對象引用時,該對象已經被初始化好。但如果在此過程中有另一個線程通過這個未初始化好的對象引用讀取相應的屬性,那麼就可能讀取到的並不是真正想要的值。在Java中final可以保證這一點,所以它可以避免這種類型的逃逸問題。
但是它並不能完全解決所有的逃逸問題,而隻是確保在構造方法return以前是會被初始化的,無法確保不與其他的指令進行重排序,比如下麵的代碼:

01 private static TestObject testObject = null;
02 final int a;
03 public 構造方法() {
04     a = 100;
05     testObject = this;          //這個地方可能和a=100發生指令重排序
06 }
07 public static void read() {
08     if(testObject != null) {
09     //對變量testObject.a做操作
10     }
11 }

如果有另一個線程調用靜態方法read(),則可能得到testObject非空值,而此時有可能a=100這個動作還未執行(因為它可以與testObject = this進行重排序),那麼操作的數據就將是錯誤的。
進一步探討:如果final所修飾的不是普通變量,而是數組、對象,那麼它能保證自己本身的初始化在其外部對象的構造方法返回之前,但是它本身作為對象,對內部的屬性是無法保證的。如果是某些具有標誌性的屬性,則需要根據實際情況做進一步處理,才可以達到線程安全的目的。
經過JSR-133對final進行語義增強後,我們就可以比較放心地使用final語法了。但是我們想看看構造方法還沒做完,變量是什麼樣子呢?普通變量和final變量到底又有什麼區別呢?下麵我們就寫一段和並發編程沒多大關係的代碼來跑一跑看看。
代碼清單5-7 構造方法未結束,看看屬性是什麼樣子

01 public class FinalConstructorTest {
02  
03     static abstract class A {
04  
05         public A() {
06             display();
07         }
08  
09         public abstract void display();
10     }
11  
12     static class extends A {
13  
14         private int INT = 100;
15  
16         private final int FINAL_INT = 100;
17  
18         private final Integer FINAL_INTEGER = 100;
19  
20         private String STR1 = "abc";
21  
22         private final String FINAL_STR1 = "abc";
23  
24         private final String FINAL_STR2 = new String("abc");
25  
26         private final List<String> FINAL_LIST = new ArrayList<String>();
27  
28         public B() {
29             super();
30             System.out.println("abc");
31         }
32  
33         public void display() {
34             System.out.println(INT);
35             System.out.println(FINAL_INT);
36             System.out.println(FINAL_INTEGER);
37             System.out.println(STR1);
38             System.out.println(FINAL_STR1);
39             System.out.println(FINAL_STR2);
40             System.out.println(FINAL_LIST);
41         }
42     }
43  
44     public static void main(String []args) {
45         new B();
46     }
47 }

在這段代碼中,我們跳開了構造方法返回之前對final的初始化動作,而是在構造方法內部去輸出這些final屬性。這段代碼的輸出結果可能會讓我們意想不到,大家可以自行測試,如果在測試過程中使用斷點跟蹤去查看這些數據的值,則可能在斷點中看到的值與實際輸出的值還會有所區別,因為斷點也是通過另一個線程去看對象的屬性值的,看到的對象可能正好是沒有初始化好的對象。
這樣看來,volatile和final在程序中必不可少嗎?當然不是!
如果每個屬性都使用這樣的修飾符,那麼係統就沒有必要設置這樣的修飾符了!其實它們是有一定的性能開銷的!我們關注的是代碼是否真的有並發問題,如果數據本身就是某些隻讀數據,或者這些Java對象本身就是線程所私有的局部變量或類似於ThrealLocal的變量,那麼就沒有必要使用這些修飾符了。
提到final,我們再補充一個相關的話題(該話題與並發編程無關)。當在方法中使用匿名內部類時,匿名內部類的方法要直接使用外部方法中的局部變量,這個局部變量必須用final來聲明才可以被使用。很多人會問這到底是為什麼?偽代碼如下:

1 public void test() {
2     final int a = 100;//這個a必須定義為final,才能被匿名子類直接使用
3     new A() {
4         public void display() {
5             System.out.println(a);
6         }
7     }
8     其他操作
9 }

這其實是對一個語法的疑問,本身沒什麼好解釋的,但是如果非要解釋,或許我們可以從這個角度來理解:JVM本身也是一種軟件平台,它設計了一種語法規則,自然也有它的局限性和初衷。在編譯時,這個地方會自動生成一個匿名內部類,而本地變量a的作用域是在方法test()中,它如何能用到另一個內部類中呢?
其中一種方式是參數傳遞;另一種方式就是作為一個屬性存在於匿名內部類對象中。不論哪一種方式都會在這個匿名內部類對象中有一份數據拷貝(如果本地變量是引用,那麼拷貝將是引用的值),不過這似乎與外部定義本地變量時是否定義為final沒有關係。
JVM在設計時並不知道我們的代碼會怎麼寫,或者說它不明確在所創建的匿名內部類中到底會做什麼,例如在代碼中完全可以在內部創建一個線程來使用這個變量,或者創建一個任務提交給線程池來使用這個變量。如果是這樣,匿名內部類的運行將與該方法本身的運行處於兩個線程中,當外部方法可能已經結束時,那麼相應的局部變量的作用域已經結束,自動會被回收,要保證匿名內部類一直可以使用該變量,就隻能用拷貝的方法,這似乎還是與final沒有多大關係。
我們現在回過頭來回答:為何外部方法必須使用final定義這個這個變量?我們將上述結論反過來想,如果這個屬性不是final修飾的,在匿名內部類中使用“同名”的變量操作,並且可以對它做任意修改,自然外部也應當能感知到。但是事實上不能感知到,因為這是一份數據拷貝,就像傳遞的參數一樣,不是原來的數據。胖哥認為這種語法的設計手段是為了避免誤解,語法上強製約束它為final修飾。
如果這裏的a是一個引用,那麼就隻會拷貝引用值,而不是整個對象的內容,在內部修改引用所指向的對象不會影響外部,外部的final也無法約束其改變,但如果改變其對象內部的屬性,隻要外部還會使用這個對象,那麼就會受到影響。

 5.2.5 棧封閉

棧封閉算是一種概念,也就是線程操作的數據都是私有的,不會與其他的線程共享數據。簡單來說,如果每個線程所訪問的JVM區域是隔離的,那麼這個係統就像一個單線程係統一樣簡單了,我們把它叫作棧封閉。這樣說是不是有點抽象,下麵講實際的例子。
通常在做Web開發時不需要自己去關注多線程的各種內在,因為容器給我們做好了,從前端請求就給業務處理分配好了數據,我們無須關注那些並發的過程,Web容器會自動給我們提供私有的Reqeust、Response對象的處理,因為它不會被其他的線程所占用,所以可以放心使用,它是線程絕對安全的(當使用Session或ServletContext時,它也在內部給你封裝好了並發的控製)。
但這並不意味著程序永遠不關注多線程與異步的問題,當需要並發去訪問某些共享緩存數據時,當需要去操作共享文件數據時,當自定義多線程去並發做一些任務時,都必然會用到這些基本的知識體係來作為支撐,否則代碼出現某些詭異的問題還不知道怎麼回事。
比如在一個項目中,使用了Spring注入的DAO層,大家都應該知道Spring生成的對象默認是“單例”的,也就是一個類隻會生成一個對應的實例。在這個DAO的許多方法中,使用StringBuilder進行SQL拚接,在DAO裏麵定義了StringBuilder,認為這個變量可以提供給許多的DAO層的方法共同使用,以節約空間,每個相應的方法都是對它做一個或多個append操作後,通過toString()獲取結果String對象,最後再把這個StringBuilder清空。
先拋開並發本身的問題,這樣做也根本節約不了什麼空間,因為這個對象將擁有與這個DAO一樣永久的生命周期占用內存,由於DAO是“單例”的,所以相當於永久的生命周期。我們節約空間的方式通常是希望它短命,在Young空間就幹掉它,而不是讓它共享。
繼續看有什麼並發的問題。雖然這個StringBuilder不是static修飾的,但由於它所在的這個DAO對象實例是“單例”的,由Spring控製生成,所以它幾乎等價於全局對象。它至少會在這個類裏麵的所有方法被訪問時共享,就算這個類裏麵隻有一個方法也會有並發問題(因為同一個方法是可以被多個線程同時訪問的,為何?因為它是代碼段,程序運行時隻需要從這裏獲取到指令列表即可,或者反過來理解,如果所有的代碼都不能並行訪問,那麼多線程程序就完全被串行化了)。
如果數據區域發生共享就有問題了,多個線程可能在同時改一個數據段,這樣沒有任何安全策略的數據段,最終結果會是什麼樣子,誰也不清楚。本例中提到的StringBuilder就是一個共享的數據區域,假如有兩個線程在append(),然後一個線程toString()得到的結果將有可能是兩個線程共同寫入的數據,它在操作完成後可能還會將數據清空,那麼另一個線程就可能拿到一個空字符串甚至於更加詭異的結果。這樣的程序很明顯是有問題的。
如果改成StringBuffer,是否可行?
答曰:StringBuffer是同步的,但是並不代表它在業務上是絕對安全的,認為它安全是因為它在每一次做append()類似操作時都會加上synchronized的操作,但是在實際的程序中是可以對StringBuffer進行多次append()操作的,在這些append()操作之間可能還會有其他的代碼步驟,StringBuffer可以保證每次append()操作是線程安全的,但它無法保證多線程訪問時進行多次append()也能得到理想的結果。
難道我們還要在外層加一個鎖來控製?
如果是這樣的話,新的問題就出現了,這個類所有的方法訪問到這裏都是串行的,如果所有的DAO層都是這樣的情況,拋開鎖本身的開銷,此時係統就像單線程係統一樣在運行,外部的並發訪問到來時,係統將奇慢無比。如果訪問過大,就會堆積大量的線程阻塞,以及線程所持有的上下文無法釋放,而且會越堆積越多,後果可想而知。
鎖的開銷是巨大的,它對於並發編程中的性能是十分重要的,於是許多大牛開始對無鎖化的問題有了追求,或者說盡量靠近無鎖化。在大多數情況下我們希望事情是樂觀的,希望使用盡量細粒度化的鎖機製,不過對於大量循環調用鎖的情況會反過來使用粗粒度化的鎖機製,因為加鎖的開銷本身也是巨大的。
關於棧封閉,除了使用局部變量外,還有一種方式就是使用ThreadLocal,ThreadLocal使用一種變通的方式來達到棧封閉的目的,具體的請參看下一小節的內容。

 5.2.6 ThreadLocal

雖然ThreadLocal與並發問題相關,但是許多程序員僅僅將它作為一種用於“方便傳參”的工具,胖哥認為這也許並不是ThreadLocal設計的目的,它本身是為線程安全和某些特定場景的問題而設計的。
ThreadLocal是什麼呢!
每個ThreadLocal可以放一個線程級別的變量,但是它本身可以被多個線程共享使用,而且又可以達到線程安全的目的,且絕對線程安全。
例如:

1 public final static ThreadLocal<String> RESOURCE = new ThreadLocal<String>();

RESOURCE代表一個可以存放String類型的ThreadLocal對象,此時任何一個線程可以並發訪問這個變量,對它進行寫入、讀取操作,都是線程安全的。比如一個線程通過RESOURCE.set(“aaaa”);將數據寫入ThreadLocal中,在任何一個地方,都可以通過RESOURCE.get();將值獲取出來。
但是它也並不完美,有許多缺陷,就像大家依賴於它來做參數傳遞一樣,接下來我們就來分析它的一些不好的地方。
為什麼有些時候會將ThreadLocal作為方便傳遞參數的方式呢?例如當許多方法相互調用時,最初的設計可能沒有想太多,有多少個參數就傳遞多少個變量,那麼整個參數傳遞的過程就是零散的。進一步思考:若A方法調用B方法傳遞了8個參數,B方法接下來調用C方法->D方法->E方法->F方法等隻需要5個參數,此時在設計API時就涉及5個參數的入口,這些方法在業務發展的過程中被許多地方所複用。
某一天,我們發現F方法需要加一個參數,這個參數在A方法的入口參數中有,此時,如果要改中間方法牽涉麵會很大,而且不知道修改後會不會有Bug。作為程序員的我們可能會隨性一想,ThreadLocal反正是全局的,就放這裏吧,確實好解決。
但是此時你會發現係統中這種方式有點像在貼補丁,越貼越多,我們必須要求調用相關的代碼都使用ThreadLocal傳遞這個參數,有可能會搞得亂七八糟的。換句話說,並不是不讓用,而是我們要明確它的入口和出口是可控的。
詭異的ThreadLocal最難琢磨的是“作用域”,尤其是在代碼設計之初很亂的情況下,如果再增加許多ThreadLocal,係統就會逐漸變成神龍見首不見尾的情況。有了這樣一個省事的東西,可能許多小夥伴更加不在意設計,因為大家都認為這些問題都可以通過變化的手段來解決。胖哥認為這是一種惡性循環。
對於這類業務場景,應當提前有所準備,需要粗粒度化業務模型,即使要用ThreadLocal,也不是加一個參數就加一個ThreadLocal變量。例如,我們可以設計幾種對象來封裝入口參數,在接口設計時入口參數都以對象為基礎。
也許一個類無法表達所有的參數意思,而且那樣容易導致強耦合。
通常我們按照業務模型分解為幾大類型對象作為它們的參數包裝,並且將按照對象屬性共享情況進行抽象,在繼承關係的每一個層次各自擴展相應的參數,或者說加參數就在對象中加,共享參數就在父類中定義,這樣的參數就逐步規範化了。
我們回到正題,探討一下ThreadLocal到底是用來做什麼的?為此我們探討下文中的幾個話題。
(1)應用場景及使用方式
為了說明ThreadLocal的應用場景,我們來看一個框架的例子。Spring的事務管理器通過AOP切入業務代碼,在進入業務代碼前,會根據對應的事務管理器提取出相應的事務對象,假如事務管理器是DataSourceTransactionManager,就會從DataSource中獲取一個連接對象,通過一定的包裝後將其保存在ThreadLocal中。並且Spring也將DataSource進行了包裝,重寫了其中的getConnection()方法,或者說該方法的返回將由Spring來控製,這樣Spring就能讓線程內多次獲取到的Connection對象是同一個。
為什麼要放在ThreadLocal裏麵呢?因為Spring在AOP後並不能向應用程序傳遞參數,應用程序的每個業務代碼是事先定義好的,Spring並不會要求在業務代碼的入口參數中必須編寫Connection的入口參數。此時Spring選擇了ThreadLocal,通過它保證連接對象始終在線程內部,任何時候都能拿到,此時Spring非常清楚什麼時候回收這個連接,也就是非常清楚什麼時候從ThreadLocal中刪除這個元素(在9.2節中會詳細講解)。
從Spring事務管理器的設計上可以看出,Spring利用ThreadLocal得到了一個很完美的設計思路,同時它在設計時也十分清楚ThreadLocal中元素應該在什麼時候刪除。由此,我們簡單地認為ThreadLocal盡量使用在一個全局的設計上,而不是一種打補丁的間接方法。
了解了基本應用場景後,接下來看一個例子。定義一個類用於存放靜態的ThreadLocal對象,通過多個線程並行地對ThreadLocal對象進行set、get操作,並將值進行打印,來看看每個線程自己設置進去的值和取出來的值是否是一樣的。代碼如下:
代碼清單5-8 簡單的ThreadLocal例子

01 public class ThreadLocalTest {
02  
03     static class ResourceClass {
04  
05         public final static ThreadLocal<String> RESOURCE_1 =
06                                        new ThreadLocal<String>();
07  
08         public final static ThreadLocal<String> RESOURCE_2 =
09                                        new ThreadLocal<String>();
10  
11     }
12  
13     static class A {
14  
15         public void setOne(String value) {
16             ResourceClass.RESOURCE_1.set(value);
17         }
18  
19         public void setTwo(String value) {
20             ResourceClass.RESOURCE_2.set(value);
21         }
22     }
23  
24     static class B {
25         public void display() {
26             System.out.println(ResourceClass.RESOURCE_1.get()
27                         ":" + ResourceClass.RESOURCE_2.get());
28         }
29     }
30  
31     public static void main(String []args) {
32         final A a = new A();
33         final B b = new B();
34         for(int i = 0 ; i < 15 ; i ++) {
35             final String resouce1 = "線程-" + I;
36             final String resouce2 = " value = (" + i + ")";
37             new Thread() {
38                 public void run() {
39                 try {
40                     a.setOne(resouce1);
41                     a.setTwo(resouce2);
42                     b.display();
43                 }finally {
44                     ResourceClass.RESOURCE_1.remove();
45                     ResourceClass.RESOURCE_2.remove();
46                 }
47             }
48         }.start();
49         }
50     }
51 }

關於這段代碼,我們先說幾點。
◎ 定義了兩個ThreadLocal變量,最終的目的就是要看最後兩個值是否能對應上,這樣才有機會證明ThreadLocal所保存的數據可能是線程私有的。
◎ 使用兩個內部類隻是為了使測試簡單,方便大家直觀理解,大家也可以將這個例子的代碼拆分到多個類中,得到的結果是相同的。
◎ 測試代碼更像是為了方便傳遞參數,因為它確實傳遞參數很方便,但這僅僅是為了測試。
◎ 在finally裏麵有remove()操作,是為了清空數據而使用的。為何要清空數據,在後文中會繼續介紹細節。
測試結果如下:
線程-6: value = (6)
線程-9: value = (9)
線程-0: value = (0)
線程-10: value = (10)
線程-12: value = (12)
線程-14: value = (14)
線程-11: value = (11)
線程-3: value = (3)
線程-5: value = (5)
線程-13: value = (13)
線程-2: value = (2)
線程-4: value = (4)
線程-8: value = (8)
線程-7: value = (7)
線程-1: value = (1)
大家可以看到輸出的線程順序並非最初定義線程的順序,理論上可以說明多線程應當是並發執行的,但是依然可以保持每個線程裏麵的值是對應的,說明這些值已經達到了線程私有的目的。
不是說共享變量無法做到線程私有嗎?它又是如何做到線程私有的呢?這就需要我們知道一點點原理上的東西,否則用起來也沒那麼放心,請看下麵的介紹。
(2)ThreadLocal內在原理
從前麵的操作可以發現,ThreadLocal最常見的操作就是set、get、remove三個動作,下麵來看看這三個動作到底做了什麼事情。首先看set操作,源碼片段如圖5-5所示。
_JC(M6[OWFRR@Q5D$]KC`G3
圖5-5 ThreadLcoal.set源碼片段
圖5-5中的第一條代碼取出了當前線程t,然後調用getMap(t)方法時傳入了當前線程,換句話說,該方法返回的ThreadLocalMap和當前線程有點關係,我們先記錄下來。進一步判定如果這個map不為空,那麼設置到Map中的Key就是this,值就是外部傳入的參數。這個this是什麼呢?就是定義的ThreadLocal對象。
代碼中有兩條路徑需要追蹤,分別是getMap(Thread)和createMap(Thread , T)。首先來看看getMap(t)操作,如圖5-6所示。

圖5-6 getMap(Thread)操作
KN])~9L(HS_8K2E$K{Z$E_Q
在這裏,我們看到ThreadLocalMap其實就是線程裏麵的一個屬性,它在Thread類中的定義是:
ThreadLocal.ThreadLocalMap threadLocals = null;
這種方法很容易讓人混淆,因為這個ThreadLocalMap是ThreadLocal裏麵的內部類,放在了Thread類裏麵作為一個屬性而存在,ThreadLocal本身成為這個Map裏麵存放的Key,用戶輸入的值是Value。太亂了,理不清楚了,畫個圖來看看(見圖5-7)。
簡單來講,就是這個Map對象在Thread裏麵作為私有的變量而存在,所以是線程安全的。ThreadLocal通過Thread.currentThread()獲取當前的線程就能得到這個Map對象,同時將自身作為Key發起寫入和讀取,由於將自身作為Key,所以一個ThreadLocal對象就能存放一個線程中對應的Java對象,通過get也自然能找到這個對象。

圖5-7 Thread與ThreadLocal的偽代碼關聯關係
$1~N0C{0APXMYXG5J_4FWXY
如果還沒有理解,則可以將思維放寬一點。當定義變量String a時,這個“a”其實隻是一個名稱(在第3章中已經說到了常量池),虛擬機需要通過符號表來找到相應的信息,而這種方式正好就像一種K-V結構,底層的處理方式也確實很接近這樣,這裏的處理方式是顯式地使用Map來存放數據,這也是一種實現手段的變通。
現在有了思路,繼續回到上麵的話題,為了驗證前麵的推斷和理解,來看看createMap方法的細節,如圖5-8所示。
U4@_NXLBW]$FT784VKOZ)BS
圖5-8 createMap操作
這段代碼是執行一個創建新的Map的操作,並且將第一個值作為這個Map的初始化值,由於這個Map是線程私有的,不可能有另一個線程同時也在對它做put操作,因此這裏的賦值和初始化是絕對線程安全的,也同時保證了每一個外部寫入的值都將寫入到Map對象中。
最後來看看get()、remove()代碼,或許看到這裏就可以認定我們的理論是正確的,如圖5-9所示。
L_EK8~C5@BEGC1UE)_XL4ND
圖5-9 get()/remove()方法的代碼片段
給我們的感覺是,這樣實現是一種技巧,而不是一種技術。
其實是技巧還是技術完全是從某種角度來看的,或者說是從某種抽象層次來看的,如果這段代碼在C++中實現,難道就叫技術,不是技巧了嗎?當然不是!胖哥認為技術依然是建立在思想和方法基礎上的,隻是看實現的抽象層次在什麼級別。就像在本書中多個地方探討的一些基礎原理一樣,我們探討了它的思想,其實它的實現也是基於某種技巧和手段的,隻是對程序封裝後就變成了某種語法和API,因此胖哥認為,一旦學會使用技巧思考問題,就學會了通過技巧去看待技術本身。我們應當通過這種設計,學會一種變通和發散的思維,學會理解各種各樣的場景,這樣便可以積累許多真正的財富,這些財富不是通過某些工具的使用或測試就可以獲得的。
ThreadLocal的這種設計很完美嗎?
不是很完美,它依然有許多坑,在這裏對它容易誤導程序員當成傳參工具就不再多提了,下麵我們來看看它的使用不當會導致什麼技術上的問題。
(3)ThreadLocal的坑
通過上麵的分析,我們可以認識到ThreadLocal其實是與線程綁定的一個變量,如此就會出現一個問題:如果沒有將ThreadLocal內的變量刪除(remove)或替換,它的生命周期將會與線程共存。因此,ThreadLocal的一個很大的“坑”就是當使用不當時,導致使用者不知道它的作用域範圍。
大家可能認為線程結束後ThreadLocal應該就回收了,如果線程真的注銷了確實是這樣的,但是事實有可能並非如此,例如在線程池中對線程管理都是采用線程複用的方法(Web容器通常也會采用線程池),在線程池中線程很難結束甚至於永遠不會結束,這將意味著線程持續的時間將不可預測,甚至與JVM的生命周期一致。那麼相應的ThreadLocal變量的生命周期也將不可預測。
也許係統中定義少量幾個ThreadLocal變量也無所謂,因為每次set數據時是用ThreadLocal本身作為Key的,相同的Key肯定會替換原來的數據,原來的數據就可以被釋放了,理論上不會導致什麼問題。但世事無絕對,如果ThreadLocal中直接或間接包裝了集合類或複雜對象,每次在同一個ThreadLocal中取出對象後,再對內容做操作,那麼內部的集合類和複雜對象所占用的空間可能會開始膨脹。
拋開代碼本身的問題,舉一個極端的例子。如果不想定義太多的ThreadLocal變量,就用一個HashMap來存放,這貌似沒什麼問題。由於ThreadLocal在程序的任何一個地方都可以用得到,在某些設計不當的代碼中很難知道這個HashMap寫入的源頭,在代碼中為了保險起見,通常會先檢查這個HashMap是否存在,若不存在,則創建一個HashMap寫進去;若存在,通常也不會替換掉,因為代碼編寫者通常會“害怕”因為這種替換會丟掉一些來自“其他地方寫入HashMap的數據”,從而導致許多不可預見的問題。
在這樣的情況下,HashMap第一次放入ThreadLocal中也許就一直不會被釋放,而這個HashMap中可能開始存放許多Key-Value信息,如果業務上存放的Key值在不斷變化(例如,將業務的ID作為Key),那麼這個HashMap就開始不斷變長,並且很可能在每個線程中都有一個這樣的HashMap,逐漸地形成了間接的內存泄漏。曾經有很多人吃過這個虧,而且吃虧的時候發現這樣的代碼可能不是在自己的業務係統中,而是出現在某些二方包、三方包中(開源並不保證沒有問題)。
要處理這種問題很複雜,不過首先要保證自己編寫的代碼是沒問題的,要保證沒問題不是說我們不去用ThreadLocal,甚至不去學習它,因為它肯定有其應用價值。在使用時要明白ThreadLocal最難以捉摸的是“不知道哪裏是源頭”(通常是代碼設計不當導致的),隻有知道了源頭才能控製結束的部分,或者說我們從設計的角度要讓ThreadLocal的set、remove有始有終,通常在外部調用的代碼中使用finally來remove數據,隻要我們仔細思考和抽象是可以達到這個目的的。有些是二方包、三方包的問題,對於這些問題我們需要學會的是找到問題的根源後解決,關於二方包、三方包的運行跟蹤,可參看第3.7.9節介紹的BTrace工具。
補充:在任何異步程序中(包括異步I/O、非阻塞I/O),ThreadLocal的參數傳遞是不靠譜的,因為線程將請求發送後,就不再等待遠程返回結果繼續向下執行了,真正的返回結果得到後,處理的線程可能是另一個。

最後更新:2017-05-23 12:31:45

  上一篇:go  Java IO: OutputStream
  下一篇:go  《Java特種兵》5.2 線程安全(一)