聊一聊ThreadLocal
歡迎支持筆者新作:《深入理解Kafka:核心設計與實踐原理》和《RabbitMQ實戰指南》,同時歡迎關注筆者的微信公眾號:朱小廝的博客。
歡迎跳轉到本文的原文鏈接:https://honeypps.com/java/thread-local-analysis/
對于ThreadLocal感興趣是從一個問題開始的:ThreadLocal在何種情況下會發生內存泄露?對于這個問題的思考不得不去了解ThreadLocal本身的實現以及一些細節問題等。接下去依次介紹ThreadLocal的功能,實現細節,使用場景以及一些使用建議。
##概述
ThreadLocal不是用來解決對象共享訪問問題的,而主要提供了線程保持對象的方法和避免參數傳遞的方便的對象訪問方式。一般情況下,通過ThreadLocal.set()到線程中的對象是該線程自己使用的對象,其他線程是不需要訪問的,也訪問不到的。各個線程中訪問的是不同的對象。
ThreadLocal使用場合主要解決多線程中數據因并發產生不一致的問題。ThreadLocal為每個線程的中并發訪問的數據提供一個副本,通過訪問副本來運行業務,這樣的結果是耗費了內存,但大大減少了線程同步所帶來的線程消耗,也介紹了線程并發控制的復雜度。
另外,說ThreadLocal使得各線程能夠保持各自獨立的一個對象,并不是通過ThreadLocal.set()來實現的,而是通過每個線程中的new對象的操作來創建的對象,每個線程創建一個,不是什么對象的拷貝或副本。通過ThreadLocal.set()將這個新創建的對象的引用保存到各線程的自己的一個map(Thread類中的ThreadLocal.ThreadLocalMap的變量)中,每個線程都有這樣一個map,執行ThreadLocal.get()時,各線程從自己的map中取出放進去的對象,因此取出來的是各自自己線程中的對象,ThreadLocal實例是作為map的key來使用的。
【代碼1】
很多人會有這樣的無解:感覺這個ThreadLocal對象建立了一個類似于全局的map,然后每個線程作為map的key來存取對應的線程本地的value。其實是ThreadLocal類中有一個ThreadLocalMap靜態內部類,可以簡單的理解為一個map,這個map為每個線程復制一個變量的“拷貝”存儲其中。下面是ThreadLocalMap的部分源碼:
【代碼2】
ThreadLocal類中一共有4個方法:
- T get()
- protected T initialValue()
- void remove()
- void set(T value)
就以get()方法為例
【代碼3】
get()方法的源碼如上所示,可以看到map中真正的key是線程ThreadLocal實例本身(ThreadLocalMap.Entry e = map.getEntry(this);中的this)。可以看一下getEntry(ThreadLocal key)的源碼.
【代碼4】
那么map中的value是什么呢?我們繼續來看源碼:
【代碼5】
代碼5中只能夠觀察到通過[protected T initialValue()]方法設置了一個初始值,當然也可以通過set方法來賦值,繼續看源碼:
【代碼6】
ThreadLocal設置值有兩種方案:1. Override其initialValue方法;2. 通過set設置。
關于重寫initialValue方法可以參考下面這個例子簡便的實現:
【代碼7】
##內存泄露
通過代碼1和代碼2的片段可以看出,在Thread類中保有ThreadLocal.ThreadLocalMap的引用,即在一個Java線程棧中指向了堆內存中的一個ThreadLocal.ThreadLocalMap的對象,此對象中保存了若干個Entry,每個Entry的key(ThreadLocal實例)是弱引用,value是強引用(這點類似于WeakHashMap)。
用到弱引用的只是key,每個key都弱引用指向threadLocal,當把threadLocal實例置為null以后,沒有任何強引用指向threadLocal實例,所以threadLocal將會被gc回收,但是value卻不能被回收,因為其還存在于ThreadLocal.ThreadLocalMap的對象的Entry之中。只有當前Thread結束之后,所有與當前線程有關的資源才會被GC回收。所以,如果在線程池中使用ThreadLocal,由于線程會復用,而又沒有顯示的調用remove的話的確是會有可能發生內存泄露的問題。
其實在ThreadLocal.ThreadLocalMap的get或者set方法中會探測其中的key是否被回收(調用expungeStaleEntry方法),然后將其value設置為null,這個功能幾乎和WeakHashMap中的expungeStaleEntries()方法一樣。因此value在key被gc后可能還會存活一段時間,但最終也會被回收,但是若不再調用get或者set方法時,那么這個value就在線程存活期間無法被釋放。
【代碼8】
其實ThreadLocal本身可以看成是沒有內存泄露問題的,通過顯示的調用remove方法即可。
##使用場景及方式
ThreadLocal的應用場景,最適合的是按線程多實例(每個線程對應一個實例)的對象的訪問,并且這個對象很多地方都要用到。
對于多線程資源共享的問題,同步機制采用了“以時間換空間”的方式,比如定義一個static變量,同步訪問,而ThreadLocal采用了“以空間換時間”的方式。前者僅提供一份變量,讓不同的線程排隊訪問,而后者為每一個線程都提供了一份變量,因此可以同時訪問而互不影響。
在多線程的開發中,經常會考慮到的策略是對一些需要公開訪問的屬性通過設置同步的方式來訪問。這樣每次能保證只有一個線程訪問它,不會有沖突。但是這樣做的結果會使得性能和對高并發的支持不夠。在某些情況下,如果我們不一定非要對一個變量共享不可,而是給每個線程一個這樣的資源副本,讓他們可以獨立都各自跑各自的,這樣不是可以大幅度的提高并行度和性能了嗎?
還有的情況是有的數據本身不是線程安全的,或者說它只能被一個線程使用,不能被其它線程同時使用。如果等一個線程使用完了再給另一個線程使用就根本不現實。這樣的情況下,我們也可以考慮用ThreadLocal。
ThreadLocal建議:
##InheritableThreadLocal
InheritableThreadLocal是ThreadLocal的子類,代碼量很少,可以看一下:
【代碼9】
這里主要的還是一個childValue這個方法。
在代碼7中示范了ThreadLocal的方法,而使用類InheritableThreadLocal可以在子線程中取得父線程繼承下來的值。可以采用重寫childValue(Object parentValue)方法來更改繼承的值。
查看案例:
【代碼10】
運行結果:
Main: get value = 1467100984858 Thread-0: get value = 1467100984858 which plus in subThread.如果去掉@Override protected Object childValue(Object parentValue)方法運行結果:
Main: get value = 1461585396073 Thread-0: get value = 1461585396073參考資料
歡迎跳轉到本文的原文鏈接:https://honeypps.com/java/thread-local-analysis/
歡迎支持筆者新作:《深入理解Kafka:核心設計與實踐原理》和《RabbitMQ實戰指南》,同時歡迎關注筆者的微信公眾號:朱小廝的博客。
總結
以上是生活随笔為你收集整理的聊一聊ThreadLocal的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java多线程知识小抄集(四)——完结
- 下一篇: 这里有一份面筋请查收(二)