Java 多线程 —— ThreadLocal
一、引言
ThreadLocal是Java幫助實現線程封閉性的典型手段。
作用:提供線程內的局部變量,這種變量在線程的生命周期內起作用,減少同一個線程內多個函數或組件之間一些公共變量的傳遞復雜度。同時也用來維護線程中的變量不被其他線程干擾。
這個類能使線程中的某個值與保存值的對象關聯起來。ThreadLocal提供了get 與set方法,這些方法為每個使用該變量的線程都存有一份獨立的副本,因此get總是返回由當前執行線程在調用set時設置的最新值。
二、ThreadLocal的簡單應用
?ThreadLocal是使用空間換時間,synchronized是使用時間換空間,比如在hibernate中session就存在于ThreadLocal中,避免synchronized的使用。
下面程序的輸出結果為null,因為從ThreadLocal中取出的對象一定是本線程中set的對象,別的線程無法取出,因為線程自己放入的對象只能自己取得,因此無需進行加鎖處理,執行效率上ThreadLocal比synchronized要高。
public class ThreadLocal_02 {static ThreadLocal<Person> tl = new ThreadLocal<>();public static void main(String[] args) {new Thread(() -> {try {TimeUnit.SECONDS.sleep(2);} catch (Exception e) {e.printStackTrace();}System.out.println(tl.get()); // output : null}).start();new Thread(() -> {try {TimeUnit.SECONDS.sleep(1);} catch (Exception e) {e.printStackTrace();}tl.set(new Person("張三"));}).start();}static class Person {String name;public Person(String name) {this.name = name;}} }三、對ThreadLocal的理解
ThreadLocal對象通常用于防止對可變的單例變量或全局變量進行共享。
例如,在單線程應用程序中可能會維持一個全局的數據庫連接,并在程序啟動時初始化這個連接對象,從而避免在調用每個方法時都要傳遞一個Connection對象。由于JDBC的連接對象不一定是線程安全的,因此,當多線程應用程序在沒有協同的情況下使用全局變量時,就不是線程安全的。通過將JDBC的連接保存到ThreadLocal對象中,每個線程都會擁有屬于自己的連接:
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>(){public Connection initialValue() {return DriverManager.getConnection(DB_URL);}};public static Connection getConnection() {return connectionHolder.get();}在比如,當某個頻繁執行的操作需要一個臨時對象,例如一個緩沖區,而同時又希望避免在每次執行時都重新分配該臨時對象,就可以使用ThreadLocal。
四、ThreadLocal的實現原理
ThreadLocal內部提供了四個對外開放的接口方法,這也是用戶操作ThreadLocal對象的基本方法:
1、public T get() :取得線程局部變量
2、public void set(T value) :設置線程局部變量
3、public void remove() :刪除線程局部變量
4、protected T initialValue() :返回該線程局部變量初始值
思考:ThreadLocal的實例是如何為每一個線程維護變量副本的呢?
上圖來自http://www.importnew.com/22039.html
其實,每一個線程Thread其內部都維護一個ThreadLocal.ThreadLocalMap的實例對象(變量名為:threadLocals)。
你可以將這個ThreadLocalMap對象理解為一個Map,但實際上它是一個數組,一個以封裝了ThreadLocal為鍵,Object為值的元素的數組。也就是說ThreadLocal本身不存儲值,它只是作為一個key來讓當前線程從ThreadLocalMap中獲取value。值得注意的是,ThreadLocalMap是使用?ThreadLocal的弱引用作為?Key?的,弱引用的對象在GC時會被回收。
static class ThreadLocalMap {//map中的每個節點Entry,其鍵key是ThreadLocal并且還是弱引用static class Entry extends WeakReference<ThreadLocal<?>> {Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}// 初始化容量為16,以為對其擴充也必須是2的指數private static final int INITIAL_CAPACITY = 16;// 真正用于存儲線程的每個ThreadLocal的數組,將ThreadLocal和其對應的值包裝為一個Entryprivate Entry[] table;///....其他方法和操作都和map類似 }由此,我們可以大概了解到了其線程局部變量的維護機制:為不同的線程創建不同的ThreadLocalMap,以線程本身作為區分,每個線程之間沒有任何聯系。
下面感興趣可以看一下get()、set()的源碼:
public T get() {Thread t = Thread.currentThread();//當前線程ThreadLocalMap map = getMap(t);//獲取當前線程對應的ThreadLocalMapif (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);//獲取對應ThreadLocal的變量值if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}//若當前線程還未創建ThreadLocalMap,則返回調用此方法并在其中調用createMap方法進行創建并返回初始值。return setInitialValue(); } public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value); }五、ThreadLocal內存泄漏問題
5.1 ThreadLocal為什么會內存泄漏?
ThreadLocalMap使用ThreadLocal的弱引用作為key,如果一個ThreadLocal沒有外部強引用來引用它,那么系統 GC 的時候,這個ThreadLocal勢必會被回收,這樣一來,ThreadLocalMap中就會出現key為null的Entry,就沒有辦法訪問這些key為null的Entry的value,如果當前線程再遲遲不結束(如線程池的線程回收)的話,這些key為null的Entry的value就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value?永遠無法回收,造成內存泄漏。?
其實,ThreadLocalMap的設計中已經考慮到這種情況,也加上了一些防護措施:在ThreadLocal的get(),set(),remove()的時候都會清除線程ThreadLocalMap里所有key為null的value。
但這些被動的預防措施并不能保證不會內存泄漏。
5.2 為什么使用弱引用?
從表面上看內存泄漏的根源在于使用了弱引用。網上的文章大多著重分析ThreadLocal使用了弱引用會導致內存泄漏,但是另一個問題也同樣值得思考:為什么使用弱引用而不是強引用?
我們先來看看官方文檔的說法:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
為了應對非常大和長時間的用途,哈希表使用弱引用的 key。
下面我們分兩種情況討論:
- key 使用強引用:引用的ThreadLocal的對象被回收了,但是ThreadLocalMap還持有ThreadLocal的強引用,如果沒有手動刪除,ThreadLocal不會被回收,導致Entry內存泄漏。
- key 使用弱引用:引用的ThreadLocal的對象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使沒有手動刪除,ThreadLocal也會被回收。value在下一次ThreadLocalMap調用set,get,remove的時候會被清除。
比較兩種情況,我們可以發現:由于ThreadLocalMap的生命周期跟Thread一樣長,如果都沒有手動刪除對應key,都會導致內存泄漏,但是使用弱引用可以多一層保障:弱引用ThreadLocal不會內存泄漏,對應的value在下一次ThreadLocalMap調用set,get,remove的時候會被清除。
因此,ThreadLocal內存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一樣長,如果沒有手動刪除對應Key就會導致內存泄漏,而不是因為弱引用。
5.3 有效避免內存泄漏的最佳實踐
每次使用完ThreadLocal,都調用它的remove()方法,清除數據。
六、鳴謝
《深入剖析ThreadLocal實現原理以及內存泄漏問題》
《深入分析 ThreadLocal 內存泄漏問題》
總結
以上是生活随笔為你收集整理的Java 多线程 —— ThreadLocal的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java面试宝典————基础篇
- 下一篇: Spring Boot + Mybati