ThreadLocal使用初探

[消息]
 
好文推薦到我的圈子

分類:Java核心技術

標籤: (可以給文章補個標籤喲,最多可添加3個標籤)

未引用的圈子:

 

 

本文分爲三部分,第一部分通過例子簡單列舉同步場景,第二部分則對ThreadLocal進行介紹,最後一部分通過例子展現在web場景(sofa)下使用的一些問題,並附上例子程序

java同步的基本知識

Java良好的支持了多線程,就java的基礎工具來說,java提供了synchronized關鍵字最爲我們熟悉,但使用不當會導致大量的等待,最後耗光線程池,synchronized和ThreadLocal都是用來解決多線程併發訪問的問題。大家可能對synchronized較爲熟悉,而對ThreadLocal就要陌生得多了。當一個對象被兩個線程同時訪問時,由於cpu在分配時間片的時候並不按照線程排隊執行,則可能有一個線程會得到不可預期的結果。

如下一個簡單例子,小貓釣魚,小貓有一個變量叫魚,用以記錄它釣魚的數目


小貓釣魚類,該類是一個多線程類,在這個類中,我們定義一個叫桶的變量,小貓釣魚後將魚兒裝到桶中,釣魚這個行爲用隨機數來模擬(也就是run方法中用一個隨機數來模擬釣魚的情況),在他釣魚前,我們將它釣魚的數量打印出來,然後線程等一會兒繼續執行,查看小貓釣魚的數量,在執行的過程中,我們打印出它釣魚執行的時間,這個時間包括兩個,一個是小貓從出門到釣魚結束的時間,另外一個是小貓釣魚的時間

系統讓兩個小貓開始釣魚:


輸出結果:


很明顯,小貓在釣魚的過程中,兩隻小貓釣魚是是是數量發生了干擾,小貓小貓2釣到了3結果變成了4,兩個釣魚基本上在同一個時間內執行完畢,程序的執行的時候遇到的問題就是小貓1和小貓2在執行釣魚的時候對桶進行了爭搶,小貓1在中途倒掉了小貓2的魚數目裝上了自己的魚,記過小貓1和小貓2都覺得自己釣到了4條。

很明顯,這樣的情況是一個很大的風險,競爭導致了對資源的集中佔用,如果不加控制就變會導致A線程的量到B線程從而引起問題。

在這個問題中,Java中最簡單的解決辦法就是使用synchronized關鍵字,如下,在在小貓釣魚的方法中加入該關鍵字即可產生小貓1用完桶。然後小貓2在繼續的效果。


輸出結果爲:


由此可見synchronized是實現java的同步機制。同步機制是爲了實現同步多線程對相同資源的併發訪問控制。保證多線程之間的通信。  可見,同步的主要目的是保證多線程間的數據共享。同步會帶來巨大的性能開銷,所以同步操作應該是細粒度的。如果同步使用得當,帶來的性能開銷是微不足道的。使用同步真正的風險是複雜性和可能破壞資源安全,而不是性能。

如上,小貓1和小貓2一起去釣魚,一起準備,然後小貓1釣魚的時候小貓2等待,小貓1釣完,小貓2纔開始釣魚,比剛纔的等待少了2000ms。小貓1和小貓2歸結起來就是對資源的競爭,小貓1和小貓2爲競爭一個裝魚的桶相互等待,即是時間換空間,這個時候小貓2含着眼淚想要是自己能有一個桶那該多好啊,在程序中使用同步是非常複雜的。並且同步會帶來性能的降低。好在Java提供了另外的思路,即通過ThreadLocal空間換時間

ThreadLocal在本例中的使用

從字面上理解,很容易會把ThreadLocal誤解爲一個線程的本地變量。其實ThreadLocal並不是代表當前線程,ThreadLocal其實是採用哈希表的方式來爲每個線程都提供一個變量的副本。從而保證各個線程間數據安全。每個線程的數據不會被另外線程訪問和破壞,我們可以簡單理解爲它就是一個隱式的包含在你啓動的線程中的變量,在這個線程執行的生命週期中它如影隨形。

例如我們在對上面的小貓釣魚中,我們使用ThreadLocal就相當於小貓在一出門的時候,就自己準備了一個桶用於裝魚。



在這個類中,每個線程維護自己的ThreadLocal,由於這個ThreadLocal是和線程一一對應,所以對於小貓1和小貓2來說,不存在對該資源的競爭,所以就不會出現等待。


如上,小貓由於有了各自的桶,所以很方便的釣到了魚。

ThreadLocal類接口很簡單,只有4個方法,並在jdk5以後支持泛型,方法列舉如下:

void set(Object value)

void set(T value)

設置當前線程的線程局部變量的值

public Object get()

該方法返回當前線程所對應的線程局部變量

public void remove()

將當前線程局部變量的值刪除,目的是爲了減少內存的佔用,該方法是JDK 5.0新增的方法。需要指出的是,當線程結束後,對應該線程的局部變量將自動被垃圾回收,所以顯式調用該方法清除線程的局部變量並不是必須的操作,但它可以加快內存回收的速度。

protected Object initialValue()

T initialValue()

返回該線程局部變量的初始值,該方法是一個protected的方法,顯然是爲了讓子類覆蓋而設計的。這個方法是一個延遲調用方法,在線程第1次調用get()或set(Object)時才執行,並且僅執行1次。ThreadLocal中的缺省實現直接返回一個null。

ThreadLocal是如何做到爲每一個線程維護變量的副本的呢?其實實現的思路很簡單:在ThreadLocal類中有一個Map,用於存儲每一個線程的變量副本,Map中元素的鍵爲線程對象,而值對應線程的變量副本。我們自己就可以提供一個簡單的實現版本:


一些注意事項:

l  線程和子線程的ThreadLocal

一種很常見的現象就是一個線程派生一個子線程,在結構上,線程和子線程是父子關係,我們在web請求中一種最常見的情況是如下:


在這個請求過程中,對於一些參數,需要從頭傳遞到尾,如果這樣的參數過多而彼此之間並沒有一個很強的邏輯關係,則不得不在方法體中聲明一大堆入參用於顯示的傳遞參數,這樣一方面導致代碼不夠優雅,此外,如果多個訪問實例使用該資源還可能導致競爭從而導致程序出現不必要的等待,所以ThreadLocal爲這個場景提供了一種新的思路,程序可以在請求的時候將變量放入到ThreadLocal中,在需要的時候取出,這個功能很像session的處理方式。

值得注意的是如果在線程中派生出了子線程,則子線程是無法直接獲取到父線程的ThreadLocal的


如下,我們用一個例子加以說明:


輸出爲:


由此可見子線程不能獲取到父線程的變量,但java提供了一個ThreadLocal的子類,該類能夠獲取到父線程的變量值。


如下輸出爲:


l  Web服務器中線程池的狀態問題

Web服務在創建線程的過程中,頻繁的創建線程對系能的影響巨大,故很多服務器都採用了線程池的方式來解決線程不斷創建鎖導致的問題,所以在使用線程

池的過程中,由於線程是不斷的回收和利用故ThreadLocal在服務器中也是被反覆利用的,在使用中如果不進行清查操作,很容易導致變量污染。儘管對於很多服務器來說,ThreadLocal是的確是相對於每個線程,每個線程會有自己的ThreadLocal。但考慮到服務器都會維護一套線程池。因此,不同用戶訪問,可能會接受到同樣的線程。因此,在做基於TheadLocal時,需要謹慎,避免出現ThreadLocal變量的緩存,導致其他線程訪問到本線程變量,如果運用不當,會導致系統效率低下,舉個例子,假設我們得系統在訪問的時候在ThreadLocal中加入變量不予以更新和刪除,則這個保存的對象就變成一個增量的容器對象,如果訪問量巨大,將導致jvm內存不足而頻繁觸發gc,gc在工作的時候會進行數據複製,頻繁的觸發gc對系統的性能會帶來不利影響,同時還有可能導致內存溢出。

 
評論      

 

提示信息
您確定要刪除嗎?

溫馨提示
您是遊客無法進行此操作,請先去我的ATA補充資料!

<a href="http://www.linezing.com"><img src="http://img.tongji.linezing.com/2951307/tongji.gif"/></a>
發佈了32 篇原創文章 · 獲贊 39 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章