ThreadLocal,很多人都叫它做線程本地變量,也有些地方叫做線程本地存儲,其實意思差不多。
可能很多朋友都知道ThreadLocal為變量在每個線程中都創建了一個副本,那樣每個線程可以訪問自己內部的副本變量。
這句話從表面上看起來理解正確,但實際上這種理解是不太正確的。下面我們細細道來。
多線程並發執行時,需要數據共享,因此才有了volatile變量解決 多線程間的數據可見性,
也有了鎖的同步機制,使變量或代碼塊在某一時該,只能被一個線程訪問,確保共享數據的正確性。(Synchronized用於線程間的數據共享的)
多線程並發執行時,並不是所有數據都需要共享的,這些不需要共享的數據,讓每個線程去維護就OK了,ThreadLocal就是用於線程間的數據隔離的。
深入解析ThreadLocal類:
先我們來看一下ThreadLocal類是如何為每個線程創建一個變量的副本的。
先看下get方法的實現:
第一句是取得當前線程,然后通過getMap(t)方法獲取到一個map,map的類型為ThreadLocalMap。
然后接着下面獲取到Entry鍵值對,注意這里獲取Entry時參數傳進去的是 this,即ThreadLocal實例,而不是當前線程t。如果獲取成功,則返回value值。
如果map為空,則調用setInitialValue方法返回value。
接着看一下getMap方法中做了什么:
在getMap中,是調用當期線程t,返回當前線程t中的一個成員變量threadLocals,類型為ThreadLocalMap。
這里意味着每一個線程都自帶一個ThreadLocalMap成員變量。
繼續取看ThreadLocalMap的實現:
可以看到ThreadLocalMap的Entry繼承了WeakReference,並且使用ThreadLocal作為鍵值。
也就是說WeakReference封裝了ThreadLocal,並作為了ThreadLocalMap的Entry的Key。
總結一下,在每個線程Thread內部有一個ThreadLocalMap類型的成員變量threadLocals,
這個ThreadLocalMap成員變量的Entry的Key為,當前ThreadLocal變量的WeakReference封裝,value為變量。
為何ThreadLocalMap的鍵值為ThreadLocal對象? 因為每個線程中可能需要有多個threadLocal變量,也就是ThreadLocalMap里面可能會有多個Entry。
在每個線程內部 第一次調用ThreadLocal.get方法時,都會返回Null。因為默認情況下,initialValue方法返回的是null。
null 賦給(強轉) 基本數據類型時會拋的空指針,null賦給 引用類型沒問題。
可以在ThreadLocal的構造函數重寫initialValue()方法。如下
ThreadLocal<Long> longLocal =
new
ThreadLocal<Long>(){
protected
Long initialValue() {
return
Thread.currentThread().getId();
};
};
或者在調用ThreadLocal.get方法之前,需要先執行set(),以保證threadlocals中有值。
或者value為引用類型變量,null賦給 引用類型沒問題。,如下,hibernate中典型的ThreadLocal的應用:
- private static final ThreadLocal threadSession = new ThreadLocal();
- public static Session getSession() throws InfrastructureException {
- Session s = (Session) threadSession.get();
- try {
- if (s == null) {
- s = getSessionFactory().openSession(); //分發一個實例 的內存地址,一般就是new一個實例了
- threadSession.set(s);
- }
- } catch (HibernateException ex) {
- throw new InfrastructureException(ex);
- }
- return s;
- }
開篇說ThreadLocal創建副本 的說法是不太正確的。為什么?
從上面這個hibernate的例子來看,這是一個使用ThreadLocal解決數據庫連接的單例 在多線程中同時操作查詢和關閉的情況。
首先這里面不是創建副本,而是分發新的內存地址(即,新的數據庫連接的單例的內存地址),以當前ThreadLocal為key,value指向傳入新的數據庫連接的單例的內存地址。
從而達到單個線程獲取數據連接的線程安全而已,也就是每個線程都有一個獨立的數據庫連接的單例。
假設相反情況,一個數據庫連接單例 如果在2個線程中被同時引用,2線程分別同一時間操作讀取和close,肯定會出現沖突。
所以需要減少每次new的開銷還是得使用數據庫連接池。
ThreadLocal的內存泄露問題:
當使用線程池來復用線程時,一個線程使用完后並不會銷毀線程,那么 分發的那個實例會一直綁定在這個線程上。
由於WeakReference封裝了ThreadLocal,並作為了ThreadLocalMap的Entry的Key。如果在某些時候ThreadLocal對象被賦Null的話,弱引用會被GC收集,這樣就會導致Entry的Value對象找不到,
線程被復用后如果有調用ThreadLocal.get/set方法的話,方法里面會去做遍歷清除 以[ThreadLocal=Null ]為Key的Entry; 但如果一直沒調用ThreadLocal.get/set方法的話就會導致內存泄漏了。
所以一般線程用完ThreadLocal后,要調用threadLocal.remove(); 如下
- public static void close() {
- // 獲取當前線程內共享的Connection
- Connection conn = threadLocal.get();
- try {
- // 判斷是否已經關閉
- if(conn != null && !conn.isClosed()) {
- // 關閉資源
- conn.close();
- // 移除Connection
- threadLocal.remove();
- conn = null;
- }
- } catch (SQLException e) {
- // 異常處理
- }
- }