一. 原理
ThreadLocal就是一個類,他有get、set方法,可以起到一個保存、獲取某個值的作用。但是這個類的get、set方法有點特殊,各個線程調用時是互不干擾的,就好像線程在操作ThreadLocal對象時是在操作線程自己的私有屬性一樣。具體原因在於他的方法實現:
public T get() { Thread t = Thread.currentThread(); //先確定調用我的線程 ThreadLocalMap map = getMap(t); //根據調用我的線程,找到這個線程的ThreadLocalMap對象 if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); //以ThreadLocal對象為key,找到對應元素 if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; //講元素的value返回 return result; } } return setInitialValue(); //如果調用我的線程沒有ThreadLocalMap對象,則返回初始值 }
public void set(T value) { Thread t = Thread.currentThread(); //先確定調用我的是哪個線程 ThreadLocalMap map = getMap(t); //獲取調用我的線程的ThreadLocalMap if (map != null) map.set(this, value); //如果那個線程有map,就將此ThreadLocal對象為key的value設置好 else createMap(t, value); //如果那個線程還沒有map,先創建一個再設置 }
ThreadLocalMap是ThreadLocal的內部類,為了不造成混亂,可以把他看作一個普通的類。ThreadLocalMap其實類似與HashMap,也是通過key獲取某個值(key就是ThreadLocal對象),也是數組存儲鍵值對,拉鏈法解決沖突等。一個Thread類持有一個ThreadLocalMap實例。
通過上面的源碼也可以看出:線程互不干擾的操作ThreadLocal的原因就是,它的set、get方法是要先獲取當前線程,然后修改、操作這個線程對象的成員屬性。也就是說,調用ThreadLocal對象的set、get方法實際上是在操作當前線程的成員屬性,只不過這些屬性是通過ThreadLocal對象為key找到的而已。為了值觀明了,看下圖:
簡單概括過程:有ThreadLocal對象 tl,線程 t 調用 tl.get(), 則去線程 t 的ThreadLocalMap屬性對象里找到一個entry,若entry.key == tl返回true,則此entry是目標entry,此entry.value就是我們的目標。
ThreadLocal對象只是一個獲取當前線程某個私有屬性的渠道而已,提供了set、get的入口,同時作為key去獲取和設置目標值。真正的有效目標是屬於線程對象私自持有的,自然通過ThreadLocal對象獲取的值也就不會受其他線程影響啦。
二. 用法示例
public class ThreadLocalTest { private static ThreadLocal<Integer> tl = new ThreadLocal<Integer>(); //private是為了安全,是一個普遍做法;static是因為這個變量有可能在static方法中使用 public static void main(String[] args) { Thread t = new Thread(() -> { tl.set(1); tl.get(); ......... }); } }
理解了ThreadLocal的原理,使用起來很簡單,注意ThreadLocal對象的定義位置,檢查作用域,保證可以被要使用它的線程訪問到。
三. 關於Entry的弱類型引用
如果閱讀ThreadLocalMap的Entry源碼會發現,Entry的key是弱引用:
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); //由於Entry繼承了WeakReference,所以這里以一個弱引用指向ThreadLcoal對象 value = v; } }
為什么要這么做呢?看下面的這種場景:
public void func1() { ThreadLocal tl = new ThreadLocal<Integer>(); //line1 tl.set(100); //line2 tl.get(); //line3 }
line1新建了一個ThreadLocal對象,t1 是強引用指向這個對象;line2調用set()后,新建一個Entry,通過源碼可知entry對象里的 k是弱引用指向這個對象。如圖:
當func1方法執行完畢后,棧幀銷毀,強引用 tl 也就沒有了,但此時線程的ThreadLocalMap里某個entry的 k 引用還指向這個對象。若這個k 引用是強引用,就會導致k指向的ThreadLocal對象及v指向的對象不能被gc回收,造成內存泄漏,但是弱引用就不會有這個問題(弱引用及強引用等這里不說了)。使用弱引用,就可以使ThreadLocal對象在方法執行完畢后順利被回收,而且在entry的k引用為null后,再調用get,set或remove方法時,就會嘗試刪除key為null的entry,可以釋放value對象所占用的內存。
概括說就是:在方法中新建一個ThreadLocal對象,就有一個強引用指向它,在調用set()后,線程的ThreadLocalMap對象里的Entry對象又有一個引用 k 指向它。如果后面這個引用 k 是強引用就會使方法執行完,棧幀中的強引用銷毀了,對象還不能回收,造成嚴重的內存泄露。
注意:雖然弱引用,保證了k指向的ThreadLocal對象能被及時回收,但是v指向的value對象是需要ThreadLocalMap調用get、set時發現k為null時才會去回收整個entry、value,因此弱引用不能保證內存完全不泄露。我們要在不使用某個ThreadLocal對象后,手動調用remoev方法來刪除它,尤其是在線程池中,不僅僅是內存泄露的問題,因為線程池中的線程是重復使用的,意味着這個線程的ThreadLocalMap對象也是重復使用的,如果我們不手動調用remove方法,那么后面的線程就有可能獲取到上個線程遺留下來的value值,造成bug。
四. 用途舉例
從ThreadLocal類的特性就知道它的用途了,它可以看成專屬於線程的變量(實際上是通過它找到線程自己的某個Entry屬性對象),不受其他線程干擾,記錄着線程的某些信息。作用域比較特殊,它跟隨線程的一生,無論線程執行到哪個類的哪個方法,我都隨時可以用get()方法拿出來用。比如:在web后台中,可以將http請求的信息包裝到ThreadLocal對象 假設為tl,執行此請求的線程在開始前執行 tl.set(httpRequest),那么這個處理請求的線程無論執行到哪,都可以由tl.get()獲取當前的請求信息。
Spring的RequestContextHolder就是這么操作的,這樣在使用切面時,也可以獲取到請求信息了(切面編程時自身是只可以獲取到方法名+方法參數信息的):