Java的TheadLocal使用
很多時候,當我們需要存儲線程私有變量或者要實現線程安全的變量時或者想減少線程資源競爭的時候,可以使用ThreadLocal來為每個線程存儲對應的私有變量。但是,如果你使用不當,會有可能造成嚴重的問題,最容易出現的就是內存泄漏。今天以一個案例分析出發,給大家介紹一下TheadLocal的原理及使用TheadLocal時注意的事項。
案例介紹
出于公司代碼的隱私,這里將相關的地方簡單寫成一個demo作為實際場景的還原。相關代碼如下:
public class ThreadPoolDemo {private static final ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>());public static void main(String[] args) throws InterruptedException {System.out.println("執行開始!!");for (int i = 0; i < 200; ++i) {poolExecutor.execute(new Runnable() {@Overridepublic void run() {ThreadLocal<BigObject> threadLocal = new ThreadLocal<>();threadLocal.set(new BigObject());// 其他業務代碼//最終清除//threadLocal.remove();}});Thread.sleep(1000);}System.out.println("執行完畢!!");}static class BigObject {// 200Mprivate byte[] bytes = new byte[200 * 1024 * 1024];} }對應的流程是這樣的:
別看流程比較簡單,但是需要注意的地方還是很多的。這不,正因為沒有注意,代碼上到生產環境以后導致服務器內存飆升,經過GC以后依然占據很多內存,最終導致內存泄露,報錯java.lang.OutOfMemoryError: Java heap space。
那么到底是什么原因,導致服務GC以后內存還高居不下呢?在正式分析原因之前,我們先來看一下線程在執行過程使用TheadLocal存儲變量的真實面目。
TheadLocal本質
對于存儲或者獲取,當然是離不開set/get方法。ThreadLocal類提供set/get方法存儲和獲取value值。但實際上ThreadLocal類并不存儲value值,真正存儲是靠ThreadLocalMap這個類,ThreadLocalMap是ThreadLocal的一個靜態內部類,它的key是ThreadLocal實例對象,value是任意Object對象。看一下ThreadLocalMap定義的源碼就很清楚了:
static class ThreadLocalMap {// 定義一個table數組,存儲多個threadLocal對象及其value值private Entry[] table;ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY];int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);table[i] = new Entry(firstKey, firstValue);size = 1;setThreshold(INITIAL_CAPACITY);}// 定義一個Entry類,key是一個弱引用的ThreadLocal對象// value是任意對象static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}// 省略其他 }TheadLocal類的set方法
可能到這里有的人比較迷惑,說的是TheadLocal,怎么又說到ThreadLocalMap呢?他們的關系到底是什么樣子的呢?別急,我們來看看TheadLocal類的set方法:
public class ThreadLocal<T> {public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);}ThreadLocalMap getMap(Thread t) {return t.threadLocals;}void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}// 省略其他方法 }它的邏輯比較簡單,獲取當前線程的ThreadLocalMap,然后往map里添加KV,K是當前ThreadLocal實例,V是我們傳入的value。我們看一下set方法的前兩行代碼:
Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);發現map的獲取是從Thread類對象里面取的ThreadLocalMap對象,然后我們看一下Thread類的定義:
public class Thread implements Runnable {ThreadLocal.ThreadLocalMap threadLocals = null;//省略其他 }在Thread類中維護了一個ThreadLocalMap的變量引用。所以總結起來ThreadLocal的set方法的邏輯是這樣的:
TheadLocal類的get方法
看了以上的set方法,get方法相對而言就更加簡單,get獲取當前線程的對應的私有變量,是之前set或者通過initialValue的值。源碼如下:
public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}return setInitialValue();} private T setInitialValue() {T value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);return value;}通過源碼我們總結ThreadLocal的set方法的邏輯是這樣的:
可能看到這里有的人還是不太明白這里面的關系,Thread,ThreadLocal,ThreadLocalMap,Entry等等名詞太多了,我們在這里再做一個小的總結:
- 每個線程是一個Thread實例,其內部維護一個threadLocals的實例成員,其類型是ThreadLocal.ThreadLocalMap;
- 通過實例化ThreadLocal實例,我們可以對當前運行的線程設置一些線程私有的變量,通過調用ThreadLocal的set和get方法存取;
- ThreadLocal本身并不是一個容器,我們存取的value實際上存儲在ThreadLocalMap中,ThreadLocal只是作為TheadLocalMap的key;
- 每個線程實例都對應一個TheadLocalMap實例,我們可以在同一個線程里實例化很多個ThreadLocal來存儲很多種類型的值,這些ThreadLocal實例分別作為key,對應各自的value,最終存儲在Entry table數組中;
- 當調用ThreadLocal的set/get進行賦值/取值操作時,首先獲取當前線程的ThreadLocalMap實例,然后就像操作一個普通的map一樣,進行put和get。
ThreadLocal內存模型
了解了ThreadLocal的基本操作原理,我們接下來看一下在使用ThreadLocal進行數據存儲的時候的內存模型是什么樣子的。
如上圖所示,左邊是棧,右邊是堆。線程的一些局部變量和引用使用的內存屬于Stack(棧)區,而普通的對象是存儲在Heap(堆)區。我們簡單描述一下上圖中的流程:
ThreadLocal實例的引用是個弱引用,這個在源碼里面就體現了:
這里簡單介紹幾種引用:
| 強引用 | 類似“Object obj=new Object()”這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象實例。 |
| 軟引用 | 軟引用關聯著的對象,在系統將要發生內存溢出異常之前,將會把這些對象實例列進回收范圍之中進行第二次回收。如果這次回收還沒有足夠的內存,才會拋出內存溢出異常。在JDK 1.2之后,提供了SoftReference類來實現軟引用。 |
| 弱引用 | 被弱引用關聯的對象實例只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象實例。在JDK 1.2之后,提供了WeakReference類來實現弱引用。 |
| 虛引用 | 最弱的一種引用關系。一個對象實例是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。為一個對象設置虛引用關聯的唯一目的就是能在這個對象實例被收集器回收時收到一個系統通知。在JDK 1.2之后,提供了PhantomReference類來實現虛引用。 |
泄露原理剖析
最后有了上面的理論,我們就能很好的解析內存泄漏的原因了:
- 使用ThreadLocal存儲對象,理論上引用ThreadLocal的對象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使沒有手動刪除,ThreadLocal也會被回收。value在下一次ThreadLocalMap調用set、get、remove的時候會被清除。
- 但是,如果沒有調用set、get、remove方法,由于ThreadLocalMap的生命周期跟Thread一樣長,如果自動回收同時也沒有手動刪除對應key,這樣會導致這些value沒有使用但是越堆積越多,內存越占越多,最終導致內存泄漏。
- 代碼中給threadLocal賦值了一個大的對象,但是執行完業務邏輯后沒有調用remove方法,最后導致線程池中10個線程的threadLocals變量中包含的大對象沒有被釋放掉,出現了內存泄露。
我們在代碼中加完threadLocal.remove(),即將下面的注釋放開以后,
程序正常執行完畢:
基于以上分析,我們在使用完ThreadLocal以后,建議調用它的remove()方法,清除數據,及時釋放內存空間。當然并不是所有使用ThreadLocal的地方,都要在最后remove(),還是得根據你實際實際使用的場景進行取舍,比如如果定義的ThreadLocal的生命周期可能是需要和項目的生存周期一樣長的,如果提前清除掉,則可能會出現業務邏輯錯誤。所以在以后使用ThreadLocal時,大家要注意哦!
想看更多文章,請點擊此處
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的Java的TheadLocal使用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MYSQL专题-MVCC多版本并发控制
- 下一篇: 带你玩转关键字Synchronized