ThreadLocal 簡介 案例 源碼分析 MD
Markdown版本筆記 | 我的GitHub首頁 | 我的博客 | 我的微信 | 我的郵箱 |
---|---|---|---|---|
MyAndroidBlogs | baiqiantao | baiqiantao | bqt20094 | [email protected] |
ThreadLocal 簡介 案例 源碼分析 MD
目錄
目錄先來看基本用法
對ThreadLocal的理解
ThreadLocal 詳解
正確理解 ThreadLocal
ThreadLocal 源碼分析
構造方法
set 方法
ThreadLocalMap
get 方法
總結
一個類型轉換的坑
先來看基本用法
class Person { ThreadLocal<Long> longLocal = new ThreadLocal<Long>(); //創建一個用於保存 Long 類型數據的 ThreadLocal ThreadLocal<String> stringLocal = new ThreadLocal<String>() {//創建一個用於保存 String 類型數據的 ThreadLocal @Override protected String initialValue() { return Thread.currentThread().getName(); //初始化數據第一種方式:重寫 initialValue 方法(推薦方式) } }; //ThreadLocal.withInitial(()-> "返回初始值"); //初始化數據第三種方式,在1.8中添加的API public void setLongValue() { longLocal.set(Thread.currentThread().getId()); //初始化數據第二種方式:調用 set 方法 } public long getLong() { return longLocal.get(); } public String getString() { return stringLocal.get(); } }
public class Test { public static void main(String[] args) throws InterruptedException { final Person person = new Person(); person.setLongValue(); //設置main線程中的對象的值 System.out.println("A:" + person.getLong()); //1 System.out.println("A:" + person.getString()); //main Thread thread = new Thread() { public void run() { person.setLongValue(); //設置子線程中的對象的值 System.out.println("B:" + person.getLong()); //10 System.out.println("B:" + person.getString()); //Thread-0 }; }; thread.start(); thread.join(); //效果等同於同步 System.out.println("C:" + person.getLong()); //1 System.out.println("C:" + person.getString()); //main } }
打印結果:
A:1
A:main
B:10
B:Thread-0
C:1
C:main
對ThreadLocal的理解
參考
常用的幾個 API
ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
protected T initialValue() //一般是用來在使用時進行重寫的,它是一個延遲加載方法
public void set(T value)
public T get()
public void remove()
ThreadLocal提供了線程獨有的局部變量
,可以在整個線程存活的過程中隨時取用
存儲單個線程上下文信息
使變量線程安全
:變量既然成為了每個線程內部的局部變量,自然就不會存在並發問題了減少參數傳遞
原理
ThreadLocal裏類型的變量,其實是放入了當前Thread裏。每個Thread都有一個threadLocals
,它是一個map,這個map的entry是ThreadLocal.ThreadLocalMap.Entry
,具體的key和value類型分別是ThreadLocal
和Object。
註:實際是ThreadLocal的弱引用
WeakReference<ThreadLocal<?>>
,但可以先簡單理解為ThreadLocal。
對於一個普通的map,取其中某個key對應的值分兩步:
- 找到這個map;
- 在map中,給出key,得到value。
想取出我們存放在當前線程裏的map裏的值同樣需要這兩步,但是,我們不需要告訴jvm map在哪兒,因為jvm知道當前線程,也知道其局部變量map。所以最終的get操作只需要知道key(即ThreadLocal
)就行了:longLocal.get()
。
為什麽key使用弱引用
不妨反過來想想,如果使用強引用,當ThreadLocal對象(假設為ThreadLocal@123456)的引用(即longLocal,是一個強引用,指向ThreadLocal@123456)被回收了,ThreadLocalMap本身依然還持有ThreadLocal@123456的強引用,如果沒有手動刪除這個key,則ThreadLocal@123456不會被回收,所以只要當前線程不消亡,ThreadLocalMap引用的那些對象就不會被回收,可以認為這導致Entry內存泄漏。
那使用弱引用的好處呢?
如果使用弱引用,那指向ThreadLocal@123456對象的引用就兩個:longLocal強引用,和ThreadLocalMap中Entry的弱引用。一旦longLocal被回收,則指向ThreadLocal@123456的就只有弱引用了,在下次gc的時候,這個ThreadLocal@123456就會被回收。
那麽問題來了,ThreadLocal@123456對象只是作為ThreadLocalMap的一個key
而存在的,現在它被回收了,但是它對應的value
並沒有被回收,內存泄露依然存在!而且key被刪了之後,變成了null
,value更是無法被訪問到了!針對這一問題,ThreadLocalMap類的設計本身已經有了這一問題的解決方案,那就是在每次get()/set()/remove()
ThreadLocalMap中的值的時候,會自動清理key為null的value。如此一來,value也能被回收了。
為什麽不對value使用弱引用
答案顯而易見,假設往ThreadLocalMap裏存了一個value,gc過後value便消失了,那就無法使用ThreadLocalMap來達到存儲全線程變量的效果了。
內存泄漏問題
弱引用一定程度上回收了無用對象,但前提是開發者手動清理掉ThreadLocal對象的強引用(如longLocal)。只要線程一直不死,ThreadLocalMap的key-value一直在漲。
解決方法是,當某個ThreadLocal變量不再使用時,調用 remove()
方法刪除該key。
使用線程池的問題
使用線程池可以達到線程復用的效果,但是歸還線程之前記得清除ThreadLocalMap
,要不然再取出該線程的時候,ThreadLocal
變量還會存在。這就不僅僅是內存泄露的問題了,整個業務邏輯都可能會出錯。
所以ThreadLocal最好還是不要和線程池一起使用。
ThreadLocal 詳解
參考
正確理解 ThreadLocal
首先,ThreadLocal不是用來解決共享對象的多線程訪問問題的
,一般情況下,通過ThreadLocal.set()
到線程中的對象是該線程自己使用的對象,其他線程是不需要訪問的,也訪問不到的,各個線程中訪問的是不同的對象。
另外,說ThreadLocal使得各線程能夠保持各自獨立的一個對象
,並不是通過ThreadLocal.set()來實現的
,而是通過每個線程中的new
對象的操作來創建的對象,每個線程創建一個,不是什麽對象的拷貝或副本
。通過ThreadLocal.set()
將這個新創建的對象的引用保存到各線程的自己的一個map中,每個線程都有這樣一個map,執行ThreadLocal.get()
時,各線程從自己的map中取出放進去的對象,因此取出來的是各自自己線程中的對象,ThreadLocal實例是作為map的key來使用的
。
如果ThreadLocal.set()
進去的東西本來就是多個線程共享的同一個對象,那麽多個線程的ThreadLocal.get()
取得的還是這個共享對象本身,還是有並發訪問問題。
下面來看一個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(); //創建一個session
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}
可以看到在getSession()
方法中,首先判斷當前線程中有沒有放進去session,如果還沒有,那麽通過sessionFactory().openSession()
來創建一個session,再將session set到線程中,實際是放到當前線程的ThreadLocalMap
這個map中,這時,對於這個session的唯一引用就是當前線程中的那個ThreadLocalMap
,而threadSession作為這個值的key
,要取得這個session可以通過threadSession.get()
來得到,裏面執行的操作實際是先取得當前線程中的ThreadLocalMap
,然後將threadSession作為key
將對應的值取出。
這個session相當於線程的私有變量,而不是public的。顯然,其他線程中是取不到這個session的,他們也只能取到自己的ThreadLocalMap
中的東西。要是session是多個線程共享使用的,那還不亂套了。
試想如果不用ThreadLocal怎麽來實現呢?可能就要在action中創建session,然後把session一個個傳到service和dao中,這可夠麻煩的。或者可以自己定義一個靜態的map
,將當前thread作為key,創建的session作為值,put到map中,應該也行,這也是一般人的想法。但事實上,ThreadLocal的實現剛好相反,它是在每個線程中有一個map
,而將ThreadLocal實例作為key,這樣每個map中的項數很少
,而且當線程銷毀時相應的東西也一起銷毀了
。
總之,ThreadLocal不是用來解決對象共享訪問問題的
,而主要是提供了保持對象的方法和避免參數傳遞
的方便的對象訪問方式。
歸納了兩點:
- 每個線程中都有一個自己的
ThreadLocalMap
類對象,可以將線程自己的對象保持到其中,各管各的,線程可以正確的訪問到自己的對象。 - 將一個共用的
ThreadLocal靜態實例
作為key,將不同對象的引用
保存到不同線程的ThreadLocalMap
中,然後在線程執行的各處通過這個靜態ThreadLocal實例
的get()方法取得自己線程保存的那個對象,避免了將這個對象作為參數傳遞的麻煩。
當然如果要把本來線程共享的對象通過ThreadLocal.set()
放到線程中也可以,可以實現避免參數傳遞的訪問方式,但是要註意get()
到的是那同一個共享對象
,並發訪問問題要靠其他手段來解決。但一般來說線程共享的對象通過設置為某類的靜態變量
就可以實現方便的訪問了,似乎沒必要放到線程中。
ThreadLocal的應用場合,我覺得最適合的是按線程多實例
(每個線程對應一個實例)的對象的訪問,並且這個對象很多地方都要用到。
ThreadLocal 源碼分析
API
ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
protected T initialValue() //一般是用來在使用時進行重寫的,它是一個延遲加載方法
public void set(T value)
public T get()
public void remove()
變量
private final int threadLocalHashCode = nextHashCode(); //唯一的實例變量,而且還是不可變的
private static int nextHashCode = 0; //靜態變量,表示即將分配的下一個ThreadLocal實例的threadLocalHashCode的值
private static final int HASH_INCREMENT = 0x61c88647; //常量,表示了連續分配的兩個ThreadLocal實例的threadLocalHashCode值的增量
構造方法
可以來看一下創建一個 ThreadLocal 實例即 new ThreadLocal() 時做了哪些操作
//Creates a thread local variable. @see #withInitial(java.util.function.Supplier)
public ThreadLocal() {
}
從上面看到構造函數 ThreadLocal() 裏什麽操作都沒有,唯一的操作是這句:
private final int threadLocalHashCode = nextHashCode();
而 nextHashCode() 就是將 ThreadLocal 類的下一個 hashCode 值即 nextHashCode 的值賦給實例的 threadLocalHashCode,然後 nextHashCode 的值增加 HASH_INCREMENT 這個值。
因此,ThreadLocal實例的變量只有這個threadLocalHashCode
,而且是final的,用來區分不同的ThreadLocal實例
,ThreadLocal類主要是作為工具類
來使用,那麽ThreadLocal.set()
進去的對象是放在哪兒的呢?
set 方法
看一下set()方法:
//Sets the current thread‘s copy of this thread-local variable to the specified value.
//Most subclasses will have no need to override this method, relying solely on the initialValue method to set the values of thread-locals.
//@param value :the value to be stored in the current thread‘s copy of this thread-local.
public void set(T value) {
Thread t = Thread.currentThread(); //取得當前線程
ThreadLocalMap map = t.threadLocals; //返回當前線程t中的一個成員變量 threadLocals
if (map != null) map.set(this, value); //將該 ThreadLocal 實例(而不是當前線程)作為key,要保持的對象作為值
else t.threadLocals = new ThreadLocalMap(this, value);
}
也就是將該 ThreadLocal 實例作為key,要保持的對象作為值,設置到當前線程的 ThreadLocalMap 中
ThreadLocalMap
這個 ThreadLocalMap 類是 ThreadLocal 中定義的內部類,但是它的實例卻用在Thread類中:
public class Thread implements Runnable {
//ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class.
ThreadLocal.ThreadLocalMap threadLocals = null;
}
我們繼續取看 ThreadLocalMap 的實現
static class ThreadLocalMap {
//The entries in this hash map extend WeakReference, using its main ref field as the key (which is always a ThreadLocal object).
//Note that null keys mean that the key is no longer referenced, so the entry can be expunged擦去、移除 from table.
//Such entries are referred to as "stale陳舊的 entries" in the code that follows.
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;//The value associated with this ThreadLocal.
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//...
}
可以看到 ThreadLocalMap 的 Entry 繼承了 WeakReference,並且使用 ThreadLocal 作為鍵值。
get 方法
看一下get()方法:
//Returns the value in the current thread‘s copy of this thread-local variable.
//If the variable has no value for the current thread, it is first initialized to the value returned by an invocation of the initialValue method.
//@return the current thread‘s value of this thread-local
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = t.threadLocals;
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); //註意這裏獲取鍵值對傳進去的是 ThreadLocal 實例,而不是當前線程 t
if (e != null) return (T)e.value;
}
return setInitialValue();
}
再看 setInitialValue() 方法的具體實現:
//Variant of set() to establish initialValue. Used instead of set() in case user has overridden the set() method.
//@return the initial value
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = t.threadLocals;
if (map != null) map.set(this, value);
else t.threadLocals = new ThreadLocalMap(this, value);
return value;
}
可以看到,除了添加第一行和最後一行外,其他邏輯和 set 方法完全一樣。
第一行是調用 初始化方法 initialValue() 獲取初始值,中間就是保持的這個值,最後一行就是返回這個值。
protected T initialValue() {
return null;
}
總結
- 通過 ThreadLocal 創建的對象是存儲在每個
線程
自己的 threadLocals 集合中的 - 集合 threadLocals 的類型為 ThreadLocalMap,存儲的實體為
WeakReference<ThreadLocal<?>>
,鍵為 ThreadLocal 對象 - set 方法就是將該 ThreadLocal 實例作為 key,將要保持的對象作為值,設置到當前線程的 threadLocals 中
- 在進行 get 之前,必須先 set,或者重寫 initialValue() 方法,否則返回的是 null
一個類型轉換的坑
如下代碼的執行結果是什麽:
public class Test {
public static void main(String[] args) {
System.out.println(new Person().getLong());
System.out.println(new Person().getLong2());
}
}
class Person {
ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
public Long getLong() {
return longLocal.get();
}
public long getLong2() {
return longLocal.get();
}
}
空指針異常
null
Exception in thread "main" java.lang.NullPointerException
at Person.getLong2(Test.java:17)
at Test.main(Test.java:5)
第二種寫法(返回基本類型 long 而非包裝類型 Long)是不是感覺很坑?
2019-1-21
ThreadLocal 簡介 案例 源碼分析 MD